主题
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。