Postings
与 codec 把词项如何编码成可跳跃的 docID/freq
列表讲清了;查询侧还要回答:两个文档都包含关键词,凭什么
A 排在 B 前面? 这不是 UI 偏好,而是
Similarity 在 postings 上为每个
(query term, docID) 算出的分数如何累加。
本文是「全文检索引擎」系列第 6 篇。目标不是复述信息检索教材,而是钉住三条链:
- Robertson & Zaragoza (2009) 的 BM25 公式与参数 \(k_1\)、\(b\) 各自抑制什么失效模式;
- Lucene
BM25Similarity如何把avgdl、norms、IDF 接到 第 5 篇 的 postings 访问路径上; - BM25 作为可解释基线,与学习排序(LTR)在生产里分工在哪里。
版本锚定:Apache Lucene 9.x / 10.x(
org.apache.lucene.search.similarities.BM25Similarity);Elasticsearch 8.x 默认similarity: BM25。公式以 Robertson & Zaragoza, The Probabilistic Relevance Framework: BM25 and Beyond, FnTIR 2009 为准。
一、问题从哪来:TF 线性增长为何在工程上失效
经典向量空间模型里,词频 TF 常线性进入打分:同一词出现越多,文档越「相关」。这在短查询、长短文档混杂的 Web 语料上很快失效:
- 词频饱和:第 10 次出现「database」带来的边际相关性远低于第 1 次;
- 长度偏置:长文档天然更容易累积高 TF,把真正聚焦主题的短文挤下去。
BM25(Best Matching 25)来自 Robertson & Walker 等在
Okapi 系统上的概率相关框架;Robertson & Zaragoza (2009)
把它整理为 PRF 教程中的标准写法,并讨论 Beyond BM25
的扩展。Lucene 自 6.0 起把默认 Similarity 从
TF-IDF 切到 BM25——这不是「新算法噱头」,而是把饱和
TF + 长度归一写进默认路径。
二、BM25 公式与参数语义
对查询 \(Q\)、文档 \(D\),设 \(f(q,D)\) 为词项 \(q\) 在 \(D\) 中的词频,\(|D|\) 为文档长度(通常用该字段总词数),\(\mathrm{avgdl}\) 为索引内同字段平均文档长度,\(N\) 为文档总数,\(n_q\) 为包含 \(q\) 的文档数。Robertson & Zaragoza (2009) 给出的 BM25 项级打分可写为:
\[ \mathrm{score}(q, D) = \mathrm{IDF}(q) \cdot \frac{f(q,D)\,(k_1 + 1)}{f(q,D) + k_1 \cdot \left(1 - b + b \cdot \dfrac{|D|}{\mathrm{avgdl}}\right)} \]
查询总分对 \(Q\) 中各
\(q\) 求和(Lucene
默认对多词查询按词项分数相加,具体组合由
BooleanQuery 与协调因子决定,见 第 9
篇)。
IDF 在 Lucene
BM25Similarity 中使用 Robertson-Spark Jones
风格平滑:
\[ \mathrm{IDF}(q) = \ln\left(1 + \frac{N - n_q + 0.5}{n_q + 0.5}\right) \]
(自然对数;实现里对负 IDF 截断为 0。)
| 符号 / 参数 | 典型值(Lucene 默认) | 抑制什么 |
|---|---|---|
| \(k_1\) | 1.2 | TF 饱和速度:越大,高频词越晚「封顶」 |
| \(b\) | 0.75 | 长度归一强度:\(b=0\) 忽略长度;\(b=1\) 完全按 \(|D|/\mathrm{avgdl}\) 惩罚 |
| \(\mathrm{avgdl}\) | 索引统计 | 把绝对长度变成相对平均长度的比值 |
| \(\mathrm{IDF}\) | 由 \(N, n_q\) 得 | 常见词降权、稀有词升权,但带平滑避免 \(n_q=0\) 爆炸 |
2.1 TF 饱和的曲线形状
固定 \(k_1=1.2\)、长度因子为 1 时,TF 贡献 \(\dfrac{f(k_1+1)}{f+k_1}\) 随 \(f\) 单调增但渐近上限为 \(k_1+1\)。这与 TF-IDF 里 TF 未饱和(或仅 \(\sqrt{\mathrm{tf}}\) 等弱饱和)形成鲜明对比——也是「长文堆砌关键词」在 BM25 下更难刷分的根因。
flowchart LR
subgraph tf ["TF leg"]
f["freq from postings"]
sat["saturating fn<br/>k1=1.2"]
f --> sat
end
subgraph len ["Length leg"]
dl["doc length |D|"]
norm["norms byte"]
avg["avgdl stats"]
dl --> norm
avg --> sat
norm --> sat
end
subgraph idf ["IDF leg"]
N["total docs N"]
nq["doc freq nq"]
idf["ln smoothed IDF"]
N --> idf
nq --> idf
end
sat --> sum["term score"]
idf --> sum
三、Lucene 落点:BM25Similarity、norms 与 avgdl
3.1 索引期:norms 编码字段长度
Lucene 在索引带 IndexOptions
的文本字段时,默认写入
norms(每文档每字段一字节),编码该字段长度信息供打分使用。BM25Similarity
在 computeNorm 中把 token 计数映射为可被
$b$
使用的长度因子——查询期不再扫描全文数词,只需读
norms 与 postings 里的 freq。
这与 第
2 篇 的字段正交开关一致:omitNorms=true
会去掉长度归一(所有文档长度因子视为
1),适合长度分布极均匀或刻意不做长度惩罚的场景。
3.2 查询期:CollectionStatistics 与 avgdl
打开 IndexSearcher
时,BM25Similarity 从
CollectionStatistics 读取 \(N\)、总词数等,计算:
\[ \mathrm{avgdl} = \frac{\text{sumTotalTermFreq}}{\text{docCount}} \]
(对应该字段;多段索引时由 reader 聚合段统计。)每个
(term, doc) 的打分在 Scorer
里按需计算:从 postings 取 freq,从 norms
解码长度,再套上一节的公式。
3.3 与 explain 对齐
Elasticsearch _explain 与 Lucene
Explanation 会展开
idf、tf、fieldNorm
等子项——排障时应对照公式逐项核对,而不是只看最终
_score。本系列不在此粘贴未在本机执行的 explain
输出;读者可在单节点用官方文档中的 explain API
自行核对。
| 组件 | 索引期 | 查询期 |
|---|---|---|
postings freq |
写入 | 读取 \(f(q,D)\) |
| norms | 编码 \(\|D\|\) | 解码长度因子 |
| 词典 docFreq | 写入 \(n_q\) | 计算 IDF |
| 段/索引统计 | — | \(N\)、\(\mathrm{avgdl}\) |
四、与 TF-IDF 的工程差异
Lucene 仍提供 ClassicSimilarity(传统 TF-IDF
风格)供显式切换;Elasticsearch mapping 里也可设
similarity: boolean 等。对照
BM25,工程上应记住:
| 维度 | Classic / TF-IDF 路径 | BM25 默认路径 |
|---|---|---|
| TF | 常 \(\sqrt{\mathrm{tf}}\) 或未饱和线性 | 显式饱和,上限由 \(k_1\) 控制 |
| 长度 | lengthNorm 乘子 |
\(b\) 与 \(\mathrm{avgdl}\) 进入 TF 分母 |
| 稀有词 | IDF | 同族 IDF 平滑,但 TF 腿不同 |
| 默认 | 历史默认 | Lucene 6+ / ES 现代默认 |
争论(有文献支撑):BM25 在 TREC 类 ad-hoc 检索上长期作为强基线(Robertson & Zaragoza 2009 综述);但在域内排序(电商、招聘)上,手工特征 + 学习排序常能压过纯 BM25——前提是标注数据、特征管线与 serving 成本可承受。BM25 的价值在于:零训练、可解释、与 postings 紧密耦合、延迟可预测。
4.1 常见误解
- 「BM25 的 IDF 就是 \(\ln(N/n_q)\)」。Lucene 实现用 \((N-n_q+0.5)/(n_q+0.5)\) 平滑;与教科书简化式数值不同,explain 里应对 implementation 而非背错公式。
- 「调大 \(k_1\) 一定提升召回」。\(k_1\) 只改 TF 饱和形状;极端大 \(k_1\) 接近线性 TF,重新引入堆砌关键词偏置。
- 「similarity 只影响排序,不影响倒排」。Similarity 不改 postings 编码,但 norms 在索引时写入;改 \(b\) 或换 Similarity 类通常需 reindex 才能一致生效(除非仅 IDF 腿且 norms 未变)。
五、与学习排序的工程边界
学习排序(Learning to Rank, LTR)把相关性变成监督学习:LambdaMART、RankNet 等在特征向量上训练,再在生产对候选文档重排。代表工作包括 Burges (2010) 对 LTR 的综述,以及后续将 BM25 分数、字段匹配、点击信号等作为特征的工业实践。
flowchart TB
q["User query"]
recall["Recall layer<br/>BM25 / Boolean / kNN"]
cand["Candidate docIDs<br/>top N per shard"]
feat["Feature extraction<br/>BM25 score, boosts, DV"]
ltr["LTR model optional"]
out["Final ranking"]
q --> recall --> cand --> feat
feat --> out
feat --> ltr --> out
工程分界(本系列立场,非学术终局判断):
| 层 | 典型实现 | BM25 角色 |
|---|---|---|
| 召回 | match / dis_max / 混合
kNN |
主力或强特征;决定候选集是否漏 doc |
| 粗排 | 分片内 Top-K | BM25 分数直接截断 |
| 精排 | ES rescore、外部 LTR |
BM25 常作特征之一,非唯一分数 |
| RAG 稀疏路 | llm-infra 召回 | BM25 零训练、易复现;语义缺口由向量路补 |
工程间隙:论文里的 LTR
常在固定候选集上测 NDCG;生产里 候选集由 BM25 先砍到
Top-N,LTR 再精排——BM25
参数恶化会造成功能性漏召回,精排模型救不回来。Elasticsearch
的 sltr 插件与外部 rerank 服务把模型 serving
挪出 Lucene 核心;Lucene Similarity
接口仍是嵌入式场景的扩展点,但大多数 ES 用户不会改
Java Similarity,而是改 boost、function_score
或二次 rescore。
开放问题:
- 稠密检索 + BM25 融合 的统一打分函数尚无社区共识(见本系列 第 15 篇)。
- 跨语言 / 多字段 下 \(\mathrm{avgdl}\) 与 \(b\) 是否应字段级定制,仍依赖域内评测而非单一全局默认。
- LLM 重排 成本与延迟下,BM25 粗排窗口该多大,缺少与硬件无关的普适公式。
5.1 Elasticsearch 侧如何改 BM25
Elasticsearch mapping 允许字段级
similarity:
{
"mappings": {
"properties": {
"title": {
"type": "text",
"similarity": "BM25",
"index_options": "positions"
}
}
}
}BM25 类型可配
k1、b(8.x Reference
Similarity)。修改后
仅影响后续索引 的 norms
与统计;已有段仍按旧参数写入的 norms 解释。全索引统一需
reindex 或 rollover 新索引——这与「只改查询
DSL」完全不同。
bool 查询里的 boost 乘在 Lucene
子句 Weight 上,不改变 IDF
公式本身;function_score 可把 BM25
分与 DocValues 字段组合(见 第 10
篇),已超出纯 Similarity 范畴。
六、调参与失效模式(无 benchmark 结论)
下列判断来自机制推导与官方文档,非本机吞吐排名:
- 新闻 / 日志:文档长度方差大,保持默认
\(b=0.75\)
通常合理;极短字段(标题)可单独字段并视情况
omitNorms或降低 \(b\)。 - 词典字段(SKU、ISBN):长度几乎恒定,norms 收益低;更应依赖精确词项与 boost。
- 多语言混合索引:\(\mathrm{avgdl}\) 对「中文长文 + 英文摘要」同字段混合时会失真——优先 分字段 而非只调 \(k_1\)。
6.1 与 RAG 稀疏召回的衔接
llm-infra 第 17 篇 在应用层组合 BM25 与向量召回。引擎侧应明确:
| 环节 | BM25 作用 | 常见坑 |
|---|---|---|
| chunk 索引 | 按 chunk 字段算 \(\mathrm{avgdl}\) | 父文档过长导致 chunk norms 失真 |
| hybrid | BM25 与 kNN 分数量纲不同 | 需 rank_window_size / RRF
等融合,非直接相加 |
| explain | 调试漏召回 | 先看是否 未索引到词项(Analyzer),再看 IDF/TF |
BM25 不能保证语义等价(「汽车」vs「车辆」),这是 学习排序或向量路 存在的理由,不是 BM25 实现缺陷。
七、小结
三句话小结
- BM25 用 饱和 TF + \(\mathrm{avgdl}\) 长度归一 +
平滑 IDF 解决线性 TF 与长文偏置,公式以 Robertson
& Zaragoza (2009) 为准,Lucene 在
BM25Similarity中落到 postings/norms/段统计。 - norms 索引期写入、avgdl 查询期聚合——改 Similarity 往往意味着 reindex,不能只当查询侧开关。
- 生产里 BM25 仍是 召回与可解释基线;LTR/LLM 精排在候选集之上工作,不能替代 postings 相交与截断层。
下一篇看写入路径如何把新文档变成可搜段:IndexWriter 与 NRT。
参考资料
论文 / 教程
- Robertson, S. & Zaragoza, H., The Probabilistic Relevance Framework: BM25 and Beyond, Foundations and Trends in Information Retrieval, 2009(BM25 公式与 PRF 谱系)。
- Robertson, S. & Walker, S., Some Simple Effective Approximations to the 2-Poisson Model for Probabilistic Weighted Retrieval, SIGIR 1994(Okapi/BM25 源头之一)。
- Burges, C. J. C., From RankNet to LambdaRank to LambdaMART: An Overview, Microsoft Technical Report 2010(LTR 工程特征常含 BM25 分)。
规范 / 源码
- Apache Lucene 9.x/10.x
Javadoc:
org.apache.lucene.search.similarities.BM25Similarity、ClassicSimilarity、Similarity。 - Elasticsearch 8.x Reference:Mapping parameters
—
similarity、norms;Explain API。
站内
返回 系列目录 | 上一篇:Postings codec | 下一篇:IndexWriter NRT
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【全文检索引擎】全文引擎全景:架构叙事与 Lucene 内核之间的一层
定位 Lucene 9.x/10.x 库内核与 Elasticsearch 8.x 服务层相对 architecture/42、storage/29、向量引擎与 PG GIN 的分工;以最小索引故事建立坐标系,交代倒排索引学术谱系与 18 篇路线图。
【全文检索引擎】混合检索边界:BM25 与 dense_vector
只谈 Elasticsearch 8.x / Lucene 上 BM25 与 kNN 的组合接口与段内共存代价,ANN 算法与专用向量引擎细节外链 vector-engine 与 db-frontier/08;指出倒排与向量同 Segment 生命周期尚未统一的开放问题。
【全文检索引擎】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 层各自保证什么。