「向量检索与语义搜索:从 dense_vector 到混合检索」

讲解 Elasticsearch 向量检索与语义搜索:dense_vector 映射、kNN 搜索与参数、混合检索与 RRF、embedding 接入流程,以及向量检索的性能与效果调优。

关键词检索只能匹配字面,语义搜索则把文本映射成向量,让「苹果」与「iPhone」在向量空间彼此靠近。Elasticsearch 从 7.x 引入 dense_vector,8.x 补齐原生 kNN 搜索,正成为 RAG 与语义检索的重要底座。本文覆盖向量映射、kNN 查询、混合检索与 RRF 融合,以及接入 embedding 的全流程。

1. 向量检索基础与 dense_vector

一句话总结: dense_vector 字段把文档映射成稠密向量,向量检索按距离或相似度召回语义相近的文档。

1.1 从词袋到向量空间

倒排索引按词匹配,向量检索按「距离」匹配。文本先经 embedding 模型转成固定维度的浮点数组,两个向量越接近表示语义越相似。dense_vector 需要显式声明维度与相似度度量:

PUT /products
{
  "mappings": {
    "properties": {
      "title_vector": {
        "type": "dense_vector",
        "dims": 384,
        "index": true,
        "similarity": "cosine"
      }
    }
  }
}

1.2 向量字段的映射选项

dims 必须与 embedding 模型输出维度一致;similarity 支持 l2_norm、dot_product 与 cosine;index: true 启用 HNSW 图索引以支持近似 kNN。仅存储不检索时设 index: false 节省内存。

1.3 写入向量文档

向量作为普通字段随文档写入,_source 里会保存原始数组:

PUT /products/_doc/1
{
  "name": "无线降噪耳机",
  "title_vector": [0.031, -0.072, 0.198, -0.104, 0.055]
}

1.4 倒排与向量并存

一个字段集可以同时拥有 text 倒排字段与 dense_vector 向量字段:倒排服务精确词检索与聚合,向量服务语义召回。二者共享同一份 _source,只是建立了不同的索引结构,这也是混合检索的字段前提。

维度倒排索引向量索引
匹配方式分词后词项匹配向量距离最近
擅长精确词、ID、稀有词语义近义、同义改写
数据结构Posting ListHNSW 图
典型场景关键词搜索语义检索、RAG

2. kNN 搜索与精确度

一句话总结: kNN 搜索用 HNSW 图近似召回 top-k,返回与查询向量最相似的文档,支持 filter 与 min_score。

2.1 knn 查询语法

{
  "knn": {
    "field": "title_vector",
    "query_vector": [0.021, -0.081, 0.205, -0.098, 0.047],
    "k": 10,
    "num_candidates": 100
  }
}

k 是最终返回的最相似文档数,num_candidates 控制每分片进入候选集的图节点数,越大召回越准、开销越高。

2.2 精确 kNN 与近似 kNN

当向量字段 index: false 或使用 script_score 时退化为精确暴力扫描,适合数据量小、必须精确的场景;生产海量数据用 HNSW 近似,召回率可调且内存可控。精确搜索需要遍历全部文档计算距离,代价随文档量线性增长:

{
  "query": {
    "script_score": {
      "query": { "match_all": {} },
      "script": {
        "source": "cosineSimilarity(params.queryVector, 'title_vector') + 1.0",
        "params": { "queryVector": [0.021, -0.081, 0.205, -0.098, 0.047] }
      }
    }
  }
}

2.3 过滤与打分

kNN 支持 filter 缩小搜索空间,例如限定品牌后再做相似召回。8.11 后还可用 min_score 过滤相似度过低的命中:

{
  "knn": {
    "field": "title_vector",
    "query_vector": [0.021, -0.081, 0.205, -0.098, 0.047],
    "k": 10,
    "num_candidates": 100,
    "filter": { "term": { "brand": "acme" } }
  }
}

3. 混合检索与 RRF

一句话总结: 混合检索把词法命中与向量命中合并,RRF 用倒数排名融合消除量纲差异,兼顾精确词与语义相似。

3.1 为什么需要混合

向量检索擅长语义近义,但会漏掉精确术语、ID 与稀有词;BM25 恰好相反。二者合并后,既保证「iPhone 15」这类精确词直接命中,又召回「最新款智能手机」的语义近邻。

3.2 RRF 融合原理

RRF(Reciprocal Rank Fusion)不比较原始分数,而比较排名:每个结果在多个查询中的排名倒数求和,再统一排序。由于不同查询打分尺度不同,基于排名融合天然鲁棒:

{
  "query": {
    "bool": {
      "should": [
        { "match": { "title": "无线降噪耳机" } },
        { "knn": { "field": "title_vector", "query_vector": [0.021, -0.081, 0.205], "k": 20 } }
      ]
    }
  },
  "rank": { "rrf": { "window_size": 50, "rank_constant": 20 } }
}

3.3 RRF 参数

window_size 限制参与融合的每路结果数,rank_constant 控制排名权重,值越小头部结果越突出。默认参数适合多数场景,调优时先固定 window_size,再试 rank_constant 的 20/30/60。

4. embedding 接入

一句话总结: 接入 embedding 的关键是模型与维度统一,推理与索引解耦,查询与写入使用同一编码器。

4.1 模型与维度匹配

选用开源 embedding 模型(如 text2vec、BGE、bge-m3 等)并统一输出维度。dense_vector 的 dims 必须与模型一致;换模型意味着重建索引,接入前先冻结模型版本。

4.2 推理管道落库

推荐在接入层批量推理后写入,避免索引请求内做模型推理拖垮节点。也可用 Ingest 管道配合推理处理器,把 embedding 结果写入向量字段:

{
  "processors": [
    { "inference": { "model_id": "bge_embedding", "input_output": { "input_field": "text", "output_field": "text_vector" } } }
  ]
}

4.3 查询向量同样要编码

查询文本必须用同一模型编码成向量再提交,不同模型产出的向量空间不可比。建议查询端缓存已编码向量,避免每个请求都推理一次:

# 伪代码:查询前用 Python 编码
python3 -c "from sentence_transformers import SentenceTransformer; m = SentenceTransformer('bge-small'); print(m.encode('无线降噪耳机').tolist())"

4.4 批量入库与一致性

文档更新会改变向量内容,需要「旧文档删除 + 新向量写入」保持一致性。最简单是走 reindex 或别名切换:写新索引,切别名,删旧索引。批次编码后通过 bulk 写入,降低请求数:

# 伪代码:批量编码并写入
for batch in chunks:
    vectors = model.encode(batch)
    bulk_payload = build_bulk(batch, vectors)
    resp = es.bulk(index="products", body=bulk_payload)

5. 效果调优与分桶

一句话总结: 向量效果取决于分块粒度、字段选型与相似度度量,海量库用分桶限定候选范围。

5.1 分块策略

长文档直接向量化会稀释语义,应按段落或固定窗口分块后再向量化,并在映射中用 nested 或独立字段保存块级向量与原文片段,检索后回显原文。

5.2 多字段向量加权

标题、正文、标签各自向量化,检索时对多个向量字段加权合并,可显著提升命中质量:

{
  "knn": [
    { "field": "title_vector", "query_vector": [0.1, -0.2, 0.3], "k": 10, "boost": 2.0 },
    { "field": "body_vector", "query_vector": [0.1, -0.2, 0.3], "k": 10, "boost": 1.0 }
  ]
}

5.3 分桶限定候选

亿级向量库为控制内存与延迟,可按分类、租户做向量分桶(filter 限定),或按 embedding 聚类预分组,检索时只扫描相关桶,kNN 召回质量与吞吐双提升。分桶后每个桶的文档量要大致均衡,否则热桶仍会成为瓶颈;冷热分层也适用于向量库,热数据用小维度向量、冷数据降维或降采样。

5.4 关键词干预

向量检索对品牌词、型号等精确实体容易漂移,可在混合检索中叠加短语匹配与实体加权,让「iPhone 15 256G」这类查询既走语义召回,又对精确型号做显式 boost,避免语义相近但不精确的结果排到前面。

6. 性能与内存

一句话总结: HNSW 图驻留堆外内存,维度与 num_candidates 越高开销越大,需平衡召回率与资源。

6.1 内存模型

HNSW 图保存在堆外(mmap 的 Lucene 段文件),不占 JVM heap,但占用文件系统缓存与磁盘。向量维度 384/768 与百万级文档叠加后,单分片内存不可忽视,应纳入容量规划。

6.2 查询开销

num_candidates 增大召回更准但查询变慢;k 过大则返回排序成本上升。建议从 num_candidates = k × 10 起步,用召回率曲线找性价比拐点。

6.3 资源治理

向量索引可用 force merge 收敛段数降低图副本开销;关闭不用的向量字段索引(index: false);监控 segments.memory 与文件系统缓存水位,避免向量段挤占其他检索的缓存。

参数影响建议
dims内存与图复杂度与模型一致,够用即止
num_candidates召回率 vs 延迟起步 k×10
k返回条数按业务取 5~20
index:false省内存仅存储不检索

7. 实战案例

一句话总结: 一个完整语义搜索落地:映射向量字段、批量编码入库、混合检索融合、A/B 验证效果。

7.1 商品语义搜索

对商品标题向量化,检索时先 BM25 精确命中、再向量召回近义款,RRF 融合后返回推荐列表。冷启动无点击数据时,向量相似本身就是排序依据。上线后持续用点击日志回填人工标注,把「用户点过的相似款」纳入评估集,防止向量排序被个别热门词带偏。

# 伪代码:混合检索请求
resp = es.search(index="products", query={
  "bool": {"should": [{"match": {"title": query}}]},
  "knn": {"field": "title_vector", "query_vector": encode(query), "k": 20},
  "rank": {"rrf": {}}
})

7.2 知识库问答

文档切块向量化,用户提问编码后 kNN 召回 top-k 片段作为上下文拼给 LLM。块粒度 200~500 token、重叠 10% 是常见起点,用召回的命中率衡量分块质量。分块质量直接决定答案质量:过粗的块混入无关信息稀释语义,过细的块丢失上下文。可先用 BM25 与向量双重召回,对用户反馈做标注集回归,逐步收敛分块与参数。

7.3 效果评估

用标注好的「查询-相关文档」集合测召回率与 MRR。对比纯 BM25、纯向量、RRF 三档,多数场景 RRF 综合最优。线上用 A/B 观察点击率与转化,向量排序效果以业务指标收口。

8. 总结

环节要点
向量基础dense_vector 声明维度与相似度
kNN 查询knn 子句 + num_candidates 控制召回
混合检索BM25 + 向量 + RRF 排名融合
embedding统一模型与维度,查询写入同编码
效果调优分块粒度、多字段加权、分桶限定
性能内存HNSW 驻留页缓存,平衡召回与资源
实战落地语义搜索、知识库问答、A/B 评估

向量检索补上了词法匹配的天花板,让搜索从「字面匹配」走向「语义理解」。落地路径很清晰:选模型、定维度、建向量字段、批量编码入库,再用 RRF 与 BM25 融合保证精确词不丢、语义近邻必达。向量是搜索的增量能力而非替代品,混排才是生产形态。向量字段与分块的字段设计可阅读《数据建模与 Mapping 设计》,打分机制参考《Query DSL 与相关性打分》,性能治理看《性能调优与缓存策略》。

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「elasticsearch」更多文章

  1. 可搜索快照与冻结层:把冷数据放进对象存储还能查
  2. 分页与深度分页:from/size、search_after、PIT 与 scroll
  3. 嵌套与父子关联查询:nested、join 字段与性能取舍