土法炼钢兴趣小组的算法知识备份

【全文检索引擎】混合检索边界:BM25 与 dense_vector

文章导航

分类入口
databasestorage
标签入口
#elasticsearch#lucene#hybrid-search#bm25#knn#dense_vector#rrf#full-text-search

目录

RAG 流水线里常见一条查询:既要按关键词命中政策编号,又要按 embedding 找语义相近段落。llm-infra RAG 应用层 讨论的是召回策略与评测;向量检索引擎 拆的是 HNSW、段合并与过滤选择度。夹在中间的问题是:在 Elasticsearch / Lucene 这一条栈上,稀疏 BM25 与稠密 kNN 如何在同一次 search 里组合,段文件里两种索引如何共存,代价落在哪一层。本文只钉接口与生命周期边界,不重讲 ANN 图遍历——算法谱系见 db-frontier/08 向量索引

本文是「全文检索引擎」系列第 15 篇(共 18 篇)。→ 系列目录

版本锚定:Elasticsearch 8.x Reference kNN searchdense_vectorretrievers;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_vectordimsindex_options(如 mef_construction)只影响向量侧建图,不改变 Analyzer 对 text 的分词结果。RAG 常见模式:title/body 走 BM25,embedding 走 kNN,tenant_id 走 filter——过滤语义与 vector-engine 第 11 篇 讨论的 bitset 路径不同,但在 ES 上通常表现为 bool filter 或 knnfilter 子句。

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"]
  1. bool 组合must 里同时放 matchknn(或 script_score 包装)。两路都参与 Boolean 打分树,适合需要显式权重或必须同时满足硬条件的场景;代价是查询计划更复杂,explain 更难读(第 17 篇)。
  2. 顶层 knn + queryknn 负责向量候选,query 侧 BM25 作预过滤或二次打分。向量侧通常先截断 Top-\(k'\),再与文本条件相交——与 frontier/09 的 post-filter 风险同构:\(k'\) 过小会漏召回。
  3. 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-enginedb-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。此时:

单 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 目录,但 统一生命周期管理 仍开放:

  1. Merge 策略耦合TieredMergePolicy 对 postings 与向量图的合并成本模型并不对称;大向量段与小倒排段混并时,是否应分区策略或独立 merge 调度——公开文档未给出与纯倒排相同的成熟度结论。
  2. 刷新与资源隔离:同一分片上高频 refresh(第 12 篇)同时暴露新 postings 与新 HNSW 视图;向量建图峰值内存与 refresh 频率的联动阈值,运维侧多靠经验而非单一公式。
  3. 跨引擎一致性:若 BM25 索引在 ES、向量在 Milvus,应用层双写与融合的一致性边界,比单引擎混合更复杂——llm-infra 的 RAG 流水线需显式版本化召回配置。
  4. 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 用 retrieverbool 一次请求返回融合结果。契约应对齐:


六、常见误解

误解一:开了 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 能自动修复的。


七、小结

三句话小结:

  1. 混合检索在 ES/Lucene 上是 正交字段(text + dense_vector)+ 查询层组合(bool / knn+query / RRF retriever),ANN 内核与专用向量运维外链 vector-enginedb-frontier/08
  2. 倒排与向量 共享 docID 与同一次 flush 的 Segment,混合查询的正确性依赖 liveDocs 与过滤语义一致,而非两引擎简单各取 Top-k 再在应用里拼接。
  3. 统一 Segment 生命周期(尤其 merge 与 refresh 对两路的非对称代价)在公开资料中仍无成熟定论,是连接全文引擎与向量引擎系列的未决接缝。

下一篇转向发行版对照:OpenSearch 与 Solr——许可分叉与 Lucene 同源,不重复内核拆解。


参考资料

核心论文

  1. Robertson, S., Zaragoza, H., The Probabilistic Relevance Framework: BM25 and Beyond, FnTIR 2009.
  2. Malkov, Y., Yashunin, D., Efficient and Robust Approximate Nearest Neighbor Search Using HNSW, TPAMI 2020(算法背景;实现细节见 vector-engine / frontier/08)。
  3. Cormack, G., Clarke, C., Büttcher, S., Reciprocal Rank Fusion outperforms Condorcet and individual Rank Learning Methods, SIGIR 2009.

文档

  1. Elasticsearch 8.x Reference, dense_vectorkNN searchretrievers.
  2. Apache Lucene 9.x/10.x, Vector FieldsVector Format.
  3. 第 6–9、12–14 篇vector-engine 系列db-frontier/08.

返回 系列目录 | 上一篇:聚合与 mapping | 下一篇:OpenSearch 与 Solr

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-07-15 · database / storage

【全文检索引擎】Lucene · BM25 · Segment · Elasticsearch NRT

补齐搜索架构叙事与 RAG/向量引擎之间的全文检索内核层:以 Lucene 9.x/10.x 拆解 Analyzer、FST、postings、BM25 与 IndexWriter,并以 Elasticsearch 8.x 拆解分片、refresh/translog 与查询路径;OpenSearch 对照与选型收束。


By .