跳到正文

Elasticsearch 与 AI ​

一句话:ES 的 AI 能力不是「加一个向量字段」,而是把 BM25、向量、稀疏模型放进同一套检索器,用 RRF 融合——它卖的是相关性调优和运维成熟度。

适用版本:Elasticsearch 8.15+(semantic_text 预览)、9.x(semantic_text GA、BBQ 默认)。

为什么需要 ​

RAG 的召回经常死在两个字上:漏和偏。

  • 纯向量:问「ERR_CONN_REFUSED 怎么处理」,语义上能召回「连接被拒绝」,但错误码本身可能被淹没。
  • 纯 BM25:问「付款失败怎么办」,匹配不到文档里的「支付未成功」。

真实系统通常两路都跑,再融合。ES 的优势是这三件事在一个引擎里:倒排索引、向量索引、融合检索器,外加成熟的分片、副本、快照和监控。

核心机制 ​

向量字段与 kNN ​

json
PUT chunks
{
  "mappings": {
    "properties": {
      "content":   { "type": "text" },
      "tenant_id": { "type": "keyword" },
      "embedding": {
        "type": "dense_vector",
        "dims": 3,
        "index": true,
        "similarity": "cosine"
      }
    }
  }
}

dense_vector 开启 index: true 才会建 HNSW 图;不开启只能脚本算距离,等于暴力扫描。

查询用 knn:

json
GET chunks/_search
{
  "knn": {
    "field": "embedding",
    "query_vector": [0.1, 0.2, 0.3],
    "k": 5,
    "num_candidates": 50,
    "filter": { "term": { "tenant_id": "t1" } }
  },
  "size": 5
}

num_candidates 是每个分片参与图的候选数,通常取 10–20 × k。它和 k 都是每分片语义,分片多时总计算量会放大。

混合检索:RRF 而不是分数相加 ​

json
GET chunks/_search
{
  "retriever": {
    "rrf": {
      "retrievers": [
        { "standard": { "query": { "match": { "content": "付款失败" } } } },
        { "knn": { "field": "embedding", "query_vector": [0.1,0.2,0.3],
                   "k": 10, "num_candidates": 100 } }
      ],
      "rank_window_size": 50
    }
  }
}

RRF 只用排名,不用分数,所以不需要把 BM25 分数和余弦距离归一化——这是它比「加权求和」更稳的原因。

semantic_text:把模型调用收进索引 ​

json
PUT chunks
{
  "mappings": {
    "properties": {
      "content": { "type": "semantic_text", "inference_id": "my-embedding-endpoint" }
    }
  }
}

写入时 ES 自动切块、调用 inference endpoint 生成向量;查询时自动把 query 转成向量。9.0 起 GA。

代价是把「模型可用性」变成「索引可用性」的一部分:endpoint 挂了,写入和查询都会受影响。

量化与过滤 ​

  • BBQ(9.1 起,384 维以上的 dense_vector 默认 bbq_hnsw):二值量化 + 过采样精排,内存约降到 1/32,召回通常不降反升。
  • ACORN(9.1):过滤条件很严时,在 HNSW 图遍历中直接处理过滤,避免「先取 topK 再过滤」导致结果为空。

最小上手 ​

bash
curl -s -X PUT "localhost:9200/chunks" -H 'Content-Type: application/json' -d '
{"mappings":{"properties":{
  "content":{"type":"text"},
  "tenant_id":{"type":"keyword"},
  "embedding":{"type":"dense_vector","dims":3,"index":true,"similarity":"cosine"}
}}}'

curl -s -X POST "localhost:9200/chunks/_doc/1?refresh=wait_for" \
  -H 'Content-Type: application/json' \
  -d '{"content":"退款政策","tenant_id":"t1","embedding":[0.1,0.2,0.3]}'

curl -s -X POST "localhost:9200/chunks/_search" -H 'Content-Type: application/json' -d '
{"knn":{"field":"embedding","query_vector":[0.1,0.2,0.3],"k":1,"num_candidates":10},
 "size":1}'

refresh=wait_for 只用于验证;批量导入时不要每条都加,会把段切碎。

生产动作 ​

  • 分片规划:单分片 10–50 GB;向量索引常驻堆外内存,分片数 × 副本数 × 向量数据量要能放进集群内存。
  • num_candidates:先按 10–20 倍 k 设,再用评测集调;过滤严格时优先靠 ACORN(9.1+),不要一味加候选。
  • 写入:批量用 _bulk;导入期把 refresh_interval 调到 30s 或 -1,结束后恢复。
  • 段合并:只读索引 forcemerge 到 1 段;写入型索引不要频繁 forcemerge。
  • 推理端点:semantic_text 依赖 inference endpoint,监控其延迟和失败率;端点故障要有降级到纯 BM25 的开关。
  • 观测:_search 的 profile 看查询耗时分布;_nodes/stats/indices/search 看 kNN 查询量;慢日志开 query 与 fetch 两段。

边界 ​

  • 不是专用向量数据库。亿级向量、极高写入吞吐、独立弹性扩缩,Milvus 更合适;ES 的强项是「检索 + 过滤 + 聚合 + 运维」一体。
  • 近实时,不是实时。默认 refresh_interval=1s,写入后最多 1 秒可见;不要用它做「写完立即读」的一致性校验。
  • 向量更新是重写段。频繁更新向量字段会导致大量段合并,成本比追加高得多。
  • 过滤 + kNN 仍可能掉召回,尤其是多分片、过滤条件分散时;必须用真实数据测 recall,不要只看延迟。
  • 模型与索引耦合:换 embedding 模型等于换维度,必须新建字段/索引并全量重灌。

讲清楚 ​

ES 在 AI 链路里的定位不是「又一个向量库」,而是「混合检索 + 相关性调优」。RAG 召回最怕两件事:纯向量漏掉错误码、型号这类精确词,纯 BM25 漏掉同义表达。ES 能在一个查询里同时跑 BM25 和 kNN,再用 RRF 按排名融合,不需要归一化两路分数。

机制上有三个关键点:dense_vector 必须 index: true 才建 HNSW;num_candidates 控制每个分片的候选集,直接影响召回和延迟;9.1 之后 384 维以上默认 BBQ 量化,内存能降一个数量级,过滤严格时用 ACORN 避免「先召回再过滤」把结果筛空。

所以选型要看规模:千万级以内、需要全文和向量混合、团队已经在用 ES,它是最省运维的选择;上亿向量、写入很猛、要独立扩缩,还是交给 Milvus。

把一门技术讲透到既懂也会用