关键词检索只能匹配字面,语义搜索则把文本映射成向量,让「苹果」与「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 List | HNSW 图 |
| 典型场景 | 关键词搜索 | 语义检索、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 与相关性打分》,性能治理看《性能调优与缓存策略》。
延伸阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。