RAG 流水线里常见一条查询:既要按关键词命中政策编号,又要按 embedding 找语义相近段落。llm-infra RAG 应用层 讨论的是召回策略与评测;向量检索引擎 拆的是 HNSW、段合并与过滤选择度。夹在中间的问题是:在 Elasticsearch / Lucene 这一条栈上,稀疏 BM25 与稠密 kNN 如何在同一次 search 里组合,段文件里两种索引如何共存,代价落在哪一层。本文只钉接口与生命周期边界,不重讲 ANN 图遍历——算法谱系见 db-frontier/08 向量索引。
本文是「全文检索引擎」系列第 15 篇(共 18 篇)。→ 系列目录
版本锚定:Elasticsearch 8.x Reference kNN search、dense_vector、retrievers;Lucene 9.x/10.x
KnnVectorField与向量 codec 文档。
一、形式化问题:两种排序函数如何合成
稀疏通道在词项 postings 上算 BM25(第 6 篇):
\[ \mathrm{score}_{\mathrm{BM25}}(q,d)=\sum_{t\in q}\mathrm{IDF}(t)\cdot\frac{f(t,d)\cdot(k_1+1)}{f(t,d)+k_1\cdot(1-b+b\cdot|d|/\mathrm{avgdl})} \]
稠密通道在向量近邻图上算距离或相似度(符号依
similarity 配置,常见 cosine / dot_product /
l2_norm):
\[ \mathrm{score}_{\mathrm{kNN}}(q,d)=-\mathrm{dist}(\mathbf{v}_q,\mathbf{v}_d)\quad\text{或}\quad \mathbf{v}_q^\top \mathbf{v}_d \]
混合检索要的是:在候选文档集合上同时利用两路信号,输出单一排序列表。难点不在「能不能各跑一遍再手工合并」,而在 (1) 两路候选集是否对齐同一可见 Segment 快照、(2) 分数是否可比、(3) 组合发生在协调节点还是分片本地——这三点决定延迟、召回与运维复杂度。
二、引擎落点:字段、查询与 retriever 三层
2.1
Mapping:dense_vector 与倒排字段正交
同一条文档可同时携带:
| 字段类型 | 索引产物 | 服务哪类查询 |
|---|---|---|
text + index: true |
倒排 postings、norms | match / BM25 |
dense_vector +
index: true |
HNSW 图(Lucene 向量 codec) | knn |
keyword / 数值 +
doc_values: true |
DocValues | 过滤、聚合(第 10、14 篇) |
dense_vector 的
dims、index_options(如
m、ef_construction)只影响向量侧建图,不改变
Analyzer 对 text 的分词结果。RAG
常见模式:title/body 走
BM25,embedding 走 kNN,tenant_id
走 filter——过滤语义与 vector-engine
第 11 篇 讨论的 bitset 路径不同,但在 ES 上通常表现为
bool filter 或 knn 的
filter 子句。
2.2 三条组合路径(接口层)
Elasticsearch 8.x 文档给出的组合方式可归纳为:
flowchart TD
req["Search request"]
req --> boolPath["bool query<br/>must: match + knn subquery"]
req --> knnPath["knn top-level<br/>+ query filter for BM25"]
req --> rrfPath["retriever: RRF<br/>BM25 retriever + knn retriever"]
boolPath --> shardExec["Per-shard Lucene search"]
knnPath --> shardExec
rrfPath --> shardExec
shardExec --> reduce["Coordinator reduce / rank fusion"]
bool组合:must里同时放match与knn(或script_score包装)。两路都参与 Boolean 打分树,适合需要显式权重或必须同时满足硬条件的场景;代价是查询计划更复杂,explain更难读(第 17 篇)。- 顶层
knn+query:knn负责向量候选,query侧 BM25 作预过滤或二次打分。向量侧通常先截断 Top-\(k'\),再与文本条件相交——与 frontier/09 的 post-filter 风险同构:\(k'\) 过小会漏召回。 retriever+ RRF(Reciprocal Rank Fusion):各 retriever 独立产出排名,协调节点按位次融合:
\[ \mathrm{RRF}(d)=\sum_{i\in \mathrm{retrievers}}\frac{1}{k+\mathrm{rank}_i(d)} \]
\(k\) 为常数(ES 默认与文档一致)。优点是无需把 BM25 分数与 cosine 距离归一化到同一量纲;代价是多路完整检索 + 融合,扇出与内存随 retriever 数上升。
2.3 Lucene 段内:倒排文件与向量文件并存
一次 refresh 后,单个 Segment 目录里同时存在
postings 族文件与向量索引文件(具体扩展名随 Lucene codec
版本而变,见官方 Vector Format
说明)。读路径上:
flowchart LR
seg["One Lucene Segment"]
seg --> inv["Inverted: terms + postings + norms"]
seg --> vec["Vector: HNSW graph + vector values"]
seg --> dv["DocValues / stored"]
qBM25["BM25 query"] --> inv
qKNN["kNN query"] --> vec
qFilter["Filter / agg"] --> dv
ANN 遍历、图参数、磁盘驻留假设不在此展开——那是 vector-engine 与 db-frontier/08 的主题。本篇只强调:同一段里两套路引 共享 docID 空间,混合查询的正确性依赖「被 BM25 命中的 docID」与「被 kNN 返回的 docID」指向同一批 liveDocs(第 7–8 篇软删与 merge 语义)。
2.4 配置片段(形态示例,非伪造输出)
以下为 DSL 形态 教学,须在目标版本集群上对照 Reference 校验字段名与参数可用性:
PUT my-rag-index/_mappings
{
"properties": {
"title": { "type": "text" },
"body": { "type": "text" },
"embedding": {
"type": "dense_vector",
"dims": 768,
"index": true,
"similarity": "cosine"
},
"tenant_id": { "type": "keyword" }
}
}POST my-rag-index/_search
{
"retriever": {
"rrf": {
"retrievers": [
{ "standard": { "query": { "match": { "body": "退款政策" } } } },
{ "knn": {
"field": "embedding",
"query_vector": [0.01, 0.02],
"k": 50,
"num_candidates": 200
}
}
],
"rank_window_size": 100,
"rank_constant": 60
}
},
"size": 10
}num_candidates 控制 HNSW
探针规模——调参语义见 vector-engine
第 8 篇 对 ef/search 类参数的类比说明;Lucene
具体默认值以 ES 8.x kNN search 为准。
三、写入与可见性:共享 IndexWriter,分裂的代价模型
3.1 同一条写入路径
文档经 Bulk API 进入分片后,仍由 Lucene
IndexWriter 缓冲、flush、merge(第 7–8
篇)。向量字段与倒排字段在同一次 addDocument
里编码进同一 Segment——不存在「BM25 索引已
refresh、向量索引还落后」的两套 NRT 读者,除非人为拆成两个
index 同步。
3.2 代价不对称(工程判断,非 benchmark)
| 维度 | 倒排侧 | 向量侧 |
|---|---|---|
| 分析链 | Analyzer 分词、写放大(第 3 篇) | 无分词;维度 \(d\) 固定 |
| 段合并 | postings 合并成熟 | 向量图合并更吃 CPU/内存(官方 tuning 文档列为独立调优点) |
| 查询 | postings 相交 + BM25(第 9 篇) | 图游走,参数 ef_search 等(外链
vector-engine) |
| 过滤 | bitset / doc_values |
knn filter 或 bool 子句 |
因此「混合索引」不是简单多加一列 embedding:merge 与 heap 压力由两路叠加,circuit breaker 触发面变宽(第 17 篇)。没有在本机跑过的集群数字,本文不写成吞吐结论。
3.3 与专用向量引擎的边界
| 能力 | ES/Lucene 混合 | 专用向量引擎(Milvus 等) |
|---|---|---|
| BM25 + 结构化过滤 + 聚合 | 原生同分片 | 稀疏通道常外接或弱于 ES |
| 十亿级 ANN + 独立建索池 | 与全文共享 Data Node 资源 | 存算分离、Growing/Sealed(vector-engine 01–14) |
| 分数融合 | RRF / bool / script | 多由应用层或 Proxy 合并 |
选型不在本篇下结论——见第 18 篇。此处只钉:RAG 稀疏召回可落在 ES;纯向量主导、过滤选择度极端时应读 vector-engine 第 11 篇再决定是否迁出。
3.4 双 index 反模式
部分团队把 全文 index 与 向量 index 拆开同步 document id。此时:
- 两路
refresh_interval、merge 进度、软删传播 不再原子; - RRF 只能在协调节点对 已对齐的 id 列表 融合,双写延迟会造成「BM25 有、向量无」的幽灵文档;
- 运维上多一套 reindex / alias 切换(第 11 篇)。
单 index 单分片混合字段是 ES/Lucene 接口设计默认假设;拆 index 是架构例外,需自建一致性协议,不在官方 hybrid 文档覆盖范围内。
四、学术谱系与争论
| 主题 | 代表 work | 与本文关系 |
|---|---|---|
| 概率检索基线 | Robertson & Zaragoza, BM25 and Beyond, FnTIR 2009 | 稀疏通道公式来源(第 6 篇) |
| ANN 图索引 | Malkov & Yashunin, HNSW, TPAMI 2020 | Lucene/ES 默认向量索引族;细节外链 frontier/08 |
| 融合排序 | Cormack, Clarke & Büttcher, Reciprocal Rank Fusion, SIGIR 2009 | ES retriever RRF 的算法出处 |
| 学习排序 | Burges, From RankNet to LambdaRank, 2007 系列 | 生产常见 LTR 叠在 BM25 之上;与 kNN 分数融合是不同层 |
争论(有文献支撑):RRF 等 rank-based
融合避免分数标定,但在 单通道噪声大(如极低
IDF 的 BM25 尖刺)时,融合结果对 \(k\) 与各路 size
敏感;learning-to-rank
派主张用标注数据学权重,工程上依赖特征管线与冷启动标注——RAG
场景常缺稳定标注,故接口层 RRF/bool 仍常见。这不是「RRF
永远优于线性加权」的共识,而是
缺标注时的默认工程折中。
五、开放问题:统一 Segment 生命周期
Lucene 已让倒排与向量 共存于同一 Segment 目录,但 统一生命周期管理 仍开放:
- Merge
策略耦合:
TieredMergePolicy对 postings 与向量图的合并成本模型并不对称;大向量段与小倒排段混并时,是否应分区策略或独立 merge 调度——公开文档未给出与纯倒排相同的成熟度结论。 - 刷新与资源隔离:同一分片上高频
refresh(第 12 篇)同时暴露新 postings 与新 HNSW 视图;向量建图峰值内存与 refresh 频率的联动阈值,运维侧多靠经验而非单一公式。 - 跨引擎一致性:若 BM25 索引在 ES、向量在 Milvus,应用层双写与融合的一致性边界,比单引擎混合更复杂——llm-infra 的 RAG 流水线需显式版本化召回配置。
- RRF 各路
size与分片 reduce:协调节点融合前,各分片已做局部 Top-k 截断;分片数与rank_window_size不匹配时,融合结果对长尾文档不敏感——这是 分布式混合 特有的开放点,单节点 Lucene 教程通常不涉及。
这些问题在 CIDR/SIGMOD 的 HTAP/统一索引讨论里有平行叙事(行列/向量多索引合一),但 Lucene 路径上尚无 peer-reviewed 的「统一 merge 代价模型」可引用为生产定论——本篇将其标为开放问题,不预言即将统一。
5.1 与 llm-infra 的接口契约
应用侧常定义「稀疏 Top-\(k_s\) + 稠密 Top-\(k_d\) + 融合」;引擎侧 ES 用
retriever 或 bool
一次请求返回融合结果。契约应对齐:
- 同一快照:
preference/routing是否固定到同一副本; - 过滤先行:租户 ACL 应在两路 retriever 上 同谓词,避免一路泄露;
- 评测口径:MRR、Recall@k 应对 融合后列表 测,而非单路(llm-infra 评测篇)。
六、常见误解
误解一:开了 dense_vector
就会自动做混合检索。 Mapping 只声明字段可
kNN;是否组合 BM25 取决于查询 DSL(bool /
retriever / 顶层
knn+query)。两路默认
不会 自动融合。
误解二:BM25
分数和向量距离可以直接相加。
量纲与分布不同;生产应用 RRF 或学习排序,而非裸加。ES
retriever 文档明确走 rank fusion 路径。
误解三:混合检索慢一定是因为 HNSW
参数不对。 第 12 篇 refresh 过频、第 14 篇动态
mapping 爆炸、第 11 篇分片过多都会放大
query+fetch;应先拆协调节点等待与分片本地 postings/kNN
耗时(第 13、17 篇),再调 ef_search。
误解四:同一文档的向量和全文一定在同一 refresh
边界可见。 单 index 单分片下共享
IndexWriter,可见性一致;若应用拆成「全文 index
+ 向量 index」双写,则存在 NRT
窗口不一致风险——这是架构选择问题,不是 kNN API
能自动修复的。
七、小结
三句话小结:
- 混合检索在 ES/Lucene 上是 正交字段(text + dense_vector)+ 查询层组合(bool / knn+query / RRF retriever),ANN 内核与专用向量运维外链 vector-engine 与 db-frontier/08。
- 倒排与向量 共享 docID 与同一次 flush 的 Segment,混合查询的正确性依赖 liveDocs 与过滤语义一致,而非两引擎简单各取 Top-k 再在应用里拼接。
- 统一 Segment 生命周期(尤其 merge 与 refresh 对两路的非对称代价)在公开资料中仍无成熟定论,是连接全文引擎与向量引擎系列的未决接缝。
下一篇转向发行版对照:OpenSearch 与 Solr——许可分叉与 Lucene 同源,不重复内核拆解。
参考资料
核心论文
- Robertson, S., Zaragoza, H., The Probabilistic Relevance Framework: BM25 and Beyond, FnTIR 2009.
- Malkov, Y., Yashunin, D., Efficient and Robust Approximate Nearest Neighbor Search Using HNSW, TPAMI 2020(算法背景;实现细节见 vector-engine / frontier/08)。
- Cormack, G., Clarke, C., Büttcher, S., Reciprocal Rank Fusion outperforms Condorcet and individual Rank Learning Methods, SIGIR 2009.
文档
- Elasticsearch 8.x Reference, dense_vector、kNN search、retrievers.
- Apache Lucene 9.x/10.x, Vector Fields、Vector Format.
- 第 6–9、12–14 篇、vector-engine 系列、db-frontier/08.
返回 系列目录 | 上一篇:聚合与 mapping | 下一篇:OpenSearch 与 Solr
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【全文检索引擎】全文引擎全景:架构叙事与 Lucene 内核之间的一层
定位 Lucene 9.x/10.x 库内核与 Elasticsearch 8.x 服务层相对 architecture/42、storage/29、向量引擎与 PG GIN 的分工;以最小索引故事建立坐标系,交代倒排索引学术谱系与 18 篇路线图。
【全文检索引擎】BM25 与 Similarity:公式如何落到 Lucene
从 Robertson & Zaragoza (2009) 的 BM25 公式出发,拆解 Lucene BM25Similarity 如何把 TF 饱和、文档长度归一与 IDF 落到 postings 与 norms;对照 TF-IDF 工程差异,并划定与学习排序的生产边界。
【全文检索引擎】Lucene · BM25 · Segment · Elasticsearch NRT
补齐搜索架构叙事与 RAG/向量引擎之间的全文检索内核层:以 Lucene 9.x/10.x 拆解 Analyzer、FST、postings、BM25 与 IndexWriter,并以 Elasticsearch 8.x 拆解分片、refresh/translog 与查询路径;OpenSearch 对照与选型收束。
【全文检索引擎】IndexWriter 与 NRT:从缓冲到可打开的 Reader
拆解 Lucene IndexWriter 的 RAM 缓冲、flush 出段与 DirectoryReader.open(IndexWriter) 近实时语义;说明 Searcher 刷新与「刚写入即可搜」在库层与 ES refresh 层各自保证什么。