BM25
五层读懂一个词。这次拆的是:BM25--关键词检索的事实工业标准,Elasticsearch / Lucene / OpenSearch 默认打分。
L1 · 一句话点破
BM25 是 TF-IDF 的概率论升级版:词频饱和(不无限增长)+ 文档长度归一化(长文档不再占便宜)+ 概率推导的 IDF。
三个改进合起来,让 1994 年提出的 BM25 至今仍是稀疏检索的默认打分函数。Lucene 6+、Elasticsearch、OpenSearch、Solr 全部默认 BM25。
L2 · 通俗类比
TF-IDF 的两个老毛病,BM25 都治了。
病 1:词频无上限。TF-IDF 里一个词出现 1000 次的权重是 10 次的 100 倍。但现实里,"苹果"出现 10 次已经说明这篇文档在讲苹果了,1000 次和 100 次的相关性差距根本没那么大--边际收益递减。
BM25 的药方:饱和函数。词频涨到一定程度就涨不动了,逼近一个上限。像充电池,前期涨得快,后期涨得慢,最后充满就停了。
病 2:长文档天然占便宜。一篇 10000 字的文档,"AI"出现 50 次;一篇 100 字的文档,"AI"出现 5 次。TF-IDF 算下来长文档远高,但相对密度长文档其实更低。除以文档长度只是粗略补救。
BM25 的药方:长度归一化参数 $b$。文档比平均长多少,就按比例压多少权重,且可调。
病 3:IDF 的概率意义不清。TF-IDF 的 IDF 是经验公式,BM25 从"二值独立检索模型(BIR)"的概率推导出 IDF,理论根基更扎实。
一句话:BM25 不是新发明,是把 TF-IDF 的经验直觉用概率论重新推导一遍,顺手修了两个毛病。
L3 · 正经定义
BM25 (Best Matching 25):对查询 $q$ 和文档 $d$,分数为
$$ \text{BM25}(q, d) = \sum_{t \in q} \text{IDF}(t) \cdot \frac{f(t, d) \cdot (k_1 + 1)}{f(t, d) + k_1 \cdot (1 - b + b \cdot \frac{|d|}{\text{avgdl}}))} $$
参数:
- $f(t, d)$:词项 $t$ 在文档 $d$ 中的原始频次
- $|d|$:文档 $d$ 的长度(词数)
- $\text{avgdl}$:语料库平均文档长度
- $k_1$:词频饱和参数,典型 $1.2 \sim 2.0$,Lucene 默认 1.2
- $b$:长度归一化参数,$0 \le b \le 1$,Lucene 默认 0.75
- $\text{IDF}(t)$:BM25 的 IDF,来自概率检索模型:
$$ \text{IDF}_{\text{BM25}}(t) = \log \frac{N - \text{df}(t) + 0.5}{\text{df}(t) + 0.5} + 1 $$
(+1 是 Lucene 改造,避免负值;理论原版可能为负,工程上常用 $\log(1 + \frac{N - \text{df} + 0.5}{\text{df} + 0.5})$ 避负)
伪代码:
import math
def bm25_score(query, doc, df_map, N, avgdl, k1=1.2, b=0.75):
"""query, doc: 词列表"""
tf = Counter(doc)
doc_len = len(doc)
score = 0.0
for t in query:
if t not in tf:
continue
f = tf[t]
df = df_map.get(t, 0)
idf = math.log((N - df + 0.5) / (df + 0.5) + 1) # Lucene 变体
# 词频饱和 + 长度归一化
tf_norm = (f * (k1 + 1)) / (f + k1 * (1 - b + b * doc_len / avgdl))
score += idf * tf_norm
return score关键性质:
- $f \to \infty$ 时,$\frac{f(k_1+1)}{f + k_1(\dots)} \to k_1 + 1$(饱和到上限)
- $b = 0$:完全不做长度归一化,回到原始 TF
- $b = 1$:完全按文档长度反比缩放
- $k_1 = 0$:TF 项变成常数 1,BM25 退化为纯 IDF 求和
L4 · 原理深挖
4.1 概率检索模型:BM25 从哪推出
BM25 属于 Robertson-Spärck Jones 二值独立检索模型(BIR) 的延伸。核心假设:
给定查询 $q$ 和文档 $d$,定义相关性 $R \in {0, 1}$。词项 $t$ 出现在相关文档的概率 $P(t \mid R=1) = p_t$,出现在不相关文档的概率 $P(t \mid R=0) = u_t$。
由贝叶斯和 odds 比推导,每个查询词项 $t$ 对相关性的对数贡献为:
$$ \log \frac{p_t (1 - u_t)}{u_t (1 - p_t)} $$
用 $\text{df}(t)/N$ 估计 $u_t$,假设 $p_t = 0.5$(相关文档中词出现的概率均匀),化简得到:
$$ \log \frac{0.5}{\text{df}(t)/N} + \log \frac{1 - \text{df}(t)/N}{0.5} = \log \frac{N - \text{df}(t) + 0.5}{\text{df}(t) + 0.5} $$
这就是 BM25 IDF 的来历--不是拍脑袋,是从概率检索模型推出来的。$+0.5$ 是 $p_t, u_t$ 的贝叶斯平滑(先验)。
4.2 词频饱和的 $k_1$ 参数
TF-IDF 用 $1 + \log f$ 压词频,BM25 用双曲饱和函数:
$$ \text{TF}_{\text{BM25}}(f) = \frac{f(k_1+1)}{f + k_1} $$
(先忽略长度归一化项)
直观:
| $f$ | $k_1=1.2$ 时 TF | $k_1=2.0$ 时 TF |
|---|---|---|
| 0 | 0 | 0 |
| 1 | 1.09 | 1.50 |
| 5 | 1.93 | 3.21 |
| 10 | 2.10 | 4.29 |
| 50 | 2.21 | 4.81 |
| $\infty$ | 2.20 (= $k_1+1$) | 5.00 (= $k_1+1$) |
无论词频多大,TF 项最多涨到 $k_1 + 1$,不会无限增长。
$k_1$ 调参经验:
- $k_1 = 0$:忽略 TF,只用 IDF(适用于短查询、关键词命中即相关)
- $k_1 = 1.2$(默认):通用场景,饱和较早
- $k_1 = 2.0$:饱和较慢,更看重词频差异(适用于长文档、专业语料)
- $k_1$ 越大,TF 项越线性(饱和越慢)
工程经验:多数场景 $k_1 \in [1.0, 2.0]$,调它影响不大;$b$ 影响更大。
4.3 长度归一化的 $b$ 参数
完整 TF 项:
$$ \text{TF}_{\text{BM25}}(f, |d|) = \frac{f(k_1+1)}{f + k_1\left(1 - b + b \cdot \frac{|d|}{\text{avgdl}}\right)} $$
分母里的 $(1 - b + b \cdot |d|/\text{avgdl})$ 是长度归一化因子:
- $b = 0$:因子恒为 1,完全不归一化。长文档天然 TF 大,BM25 退化为饱和版 TF-IDF。
- $b = 1$:因子 $= |d|/\text{avgdl}$,完全按长度反比缩放。长文档被压得最狠。
- $b = 0.75$(默认):折中,长文档被压但不到完全反比。
$b$ 调参经验:
- 短文档语料(推文、标题):$b$ 调小($0.3 \sim 0.5$),因为短文档长度差异多源于噪声,不应惩罚。
- 长文档语料(论文、网页全文):$b$ 保持 $0.75$ 甚至调高,长文档确实稀释了关键词。
- 全部同长(如截断到固定长度的片段):$b$ 不起作用,可设 0。
4.4 IDF 的负值问题
理论版 IDF:
$$ \text{IDF}(t) = \log \frac{N - \text{df}(t) + 0.5}{\text{df}(t) + 0.5} $$
当 $\text{df}(t) > N/2$(超过一半文档包含 $t$),分子小于分母,IDF 为负。这意味着"普遍的词反而惩罚分数"--理论上有道理(普遍词更可能在不相关文档出现),但工程上有问题:
- 负分数会拉低整篇文档总分,导致一篇其实相关的文档因为含几个常见词被排到后面。
- 完全匹配查询的文档可能因某个高频词得负分,反直觉。
不同实现的应对:
| 实现 | 处理方式 |
|---|---|
| Lucene / Elasticsearch | $\log(1 + \frac{N - \text{df} + 0.5}{\text{df} + 0.5})$,恒正 |
| 原版 BM25 论文 | 允许负值,截断到 0 |
| _rank_bm25 (Python 库) | 默认 Lucene 变体 |
工程经验:用 Lucene 变体(恒正),几乎没人想看到负分数。
4.5 BM25 的变体家族
BM25 是一个家族,原版 BM25 是最常用,但有诸多变体:
- BM25F:字段加权 BM25。文档有多个字段(标题、正文、标签),每个字段分别算 TF,再加权求和。Elasticsearch 用
multi_match+ 字段 boost 实现。 - BM25+(Lv & Zhai 2011):解决 $f=0$ 时 BM25 完全不贡献的问题(即使词在文档不出现,理论上也应有微小负贡献)。加常数 $\delta$: $$ \text{TF}_{\text{BM25+}} = \frac{f(k_1+1)}{f + k_1} + \delta $$
- BM25L(Lv & Zhai 2011):修正 BM25 在短文档上的偏置,用 $\frac{f + k_1}{1 - b + b \cdot |d|/\text{avgdl}}$ 重新缩放。
- BM25-Adpt:自适应调 $k_1$,按语料统计自动选最优值。
实际生产中,多数场景就用原版 BM25 + 手调 $k_1, b$,变体用得少。
4.6 BM25 vs 现代 Embedding 检索
| 维度 | BM25 | Dense Retrieval |
|---|---|---|
| 匹配方式 | 字面(词形一致) | 语义(向量近邻) |
| 训练 | 不需要 | 需要训练数据 |
| 资源 | CPU 即可,毫秒级 | 需 GPU 推理编码 |
| 精确匹配 | 强(专有名词、ID、代码) | 中(可能漏精确词) |
| 语义召回 | 弱 | 强 |
| 长尾词 | 强(只要索引中有) | 弱(训练时没见过) |
| 可解释性 | 强(分数可拆解到词项) | 弱(黑盒) |
| 索引大小 | 紧凑(倒排索引) | 大(每文档一个稠密向量) |
工程结论:两者互补。现代 RAG 系统标配混合检索(hybrid search),BM25 召回精确匹配 + Dense 召回语义匹配,再用 RRF 或加权融合合并。详见后续 hybrid-search 词条。
4.7 BM25 在工程实现中的优化
优化 1:词频预编码。Lucene 把 $\text{norm}(t, d) = 1/\sqrt{|d|/b \cdot \text{avgdl}}$ 编码到一个字节(8-bit 量化),存索引里,检索时直接查表,不用实时算。代价:损失精度,但实测影响不大。
优化 2:跳表(skip list)。倒排索引的 posting list 排序后,加跳表加速 AND 查询的合并。Lucene 的 postings 默认带 skip list。
优化 3:WAND / MaxScore 剪枝。检索时按 IDF 排序词项,对每个候选文档算上界分数,低于当前 top-k 最小分数的直接跳过。这是 Lucene / Elasticsearch 在大规模语料上能秒级返回的关键。
优化 4:分片并行。Elasticsearch 把索引分到多 shard,每个 shard 独立打分,最后 merge top-k。线性扩展。
L5 · 沿革与坑
5.1 历史脉络
- 1960s–1970s:Maron-Kuhns(1960)、Robertson-Spärck Jones(1976)奠定概率检索模型基础。
- 1980s:Robertson 等在伦敦城市大学研发 Okapi 检索系统,BM 系列在 TREC 评测中表现优异。
- 1994:Robertson & Walker 在 TREC-3 正式提出 BM25,作为 Okapi 系统的打分函数。
- 1990s–2000s:BM25 在 TREC、CLEF 等评测中持续是强基线,击败多数学习排序方法。
- 2010:Trotman 等综述论文 "Efficiency and Effectiveness of BM25" 确认 BM25 工业地位。
- 2016:Lucene 6 把默认
ClassicSimilarity(TF-IDF)切到BM25Similarity,BM25 成为 Elasticsearch / Solr 的事实默认。 - 2020+:Dense retrieval 兴起,但 BM25 仍是稀疏检索最强基线,混合检索成为 RAG 标配。
5.2 BM25 调参的常见坑
坑 1:照搬默认参数,不验证。
Lucene 默认 $k_1=1.2, b=0.75$ 是通用经验值,不一定适合你的语料。短文本(标题、推文)建议 $b \approx 0.3$;长文档(论文、网页)建议 $b \approx 0.85$。务必用评估集调参。
坑 2:忘了分词器对 BM25 的影响。
中文用 jieba / IK / HanLP 分词,结果天差地别。调分词器比调 BM25 参数影响大得多。代码、专业术语、混合语料尤其要调分词器。
坑 3:字段 boost 设过头。
Elasticsearch 允许 title^10 这种字段加权。设过头会让标题命中文档碾压正文命中,反而漏召回。^2 ~ ^5 是常见区间,先用小值评估。
坑 4:BM25 跨 shard 不公平。
Elasticsearch 默认每 shard 独立打分,shard 间的文档长度分布、IDF 统计可能不一致,导致同样文档在不同 shard 分数不同。解决:用 ?search_type=dfs_query_then_fetch 让 IDF 全局统计。代价:慢一点。
坑 5:拿 BM25 当语义检索用。
"我要买手机"和"想购入智能电话"在 BM25 下完全 0 分(无共同词)。语义检索必须用 Dense Retrieval。BM25 只解决字面匹配,不要让它做不擅长的事。
坑 6:分词后未去掉停用词,IDF 失真。
"的、是、和"这种词 df 几乎等于 N,IDF 趋近 0,理论上不贡献分数。但有些实现没显式停用词,依赖 IDF 自然压低--这没问题但浪费算力。建议显式去停用词,索引更小、查询更快。
坑 7:高频词的负 IDF 拉低总分。
用原版 BM25(非 Lucene 变体)时,查询含高频词会拉低分数。要么用 Lucene 恒正变体,要么把 IDF 截断到 0,要么把高频词当停用词处理。
坑 8:BM25 分数没归一化,跨查询不可比。
BM25 分数绝对值受查询长度、文档长度分布影响,不同查询的分数没有可比性。如果要跨查询比分数(如设阈值过滤),需先归一化(如除以最大可能分数)或用 z-score。
5.3 BM25 vs TF-IDF 关键差异
| 改进点 | TF-IDF | BM25 |
|---|---|---|
| TF 缩放 | 对数 $1 + \log f$ 或开方 $\sqrt{f}$ | 双曲饱和 $\frac{f(k_1+1)}{f + k_1(\dots)}$ |
| 长度归一化 | 简单除以长度 | 可调 $b$ 参数 |
| IDF | 经验 $\log(N/\text{df})$ | 概率 $\log\frac{N-\text{df}+0.5}{\text{df}+0.5}$ |
| 理论根基 | 启发式 | 概率检索模型 |
| 参数 | 无 | $k_1, b$ 可调 |
| 工业默认 | 已退出 | Lucene 6+/ES/Solr/OpenSearch |
核心改进:BM25 把 TF-IDF 的经验公式用概率论重写一遍,并加入可调参数,让经验直觉变成可证伪的概率模型。
5.4 何时仍该用 BM25
哪怕 Dense Retrieval 兴起,BM25 仍是这些场景的最优选择:
- 专有名词 / 代码 / ID 检索:精确匹配场景 Dense 反而漏召回
- 极低延迟:CPU 即可毫秒级,Dense 需 GPU
- 小语料:训练 Dense 不值,BM25 直接可用
- 长尾词:训练时没见过的词 Dense 容易失效
- 可解释性场景:法律、医疗需分数可追溯
- 混合检索的稀疏分支:现代 RAG 标配
工程结论:Dense Retrieval 没有取代 BM25,而是把 BM25 从"唯一答案"变成"必备组件之一"。
速记卡
| 维度 | BM25 |
|---|---|
| 公式 | $\sum_t \text{IDF}(t) \cdot \frac{f(t,d)(k_1+1)}{f(t,d) + k_1(1 - b + b \cdot |
| IDF | $\log \frac{N - \text{df}(t) + 0.5}{\text{df}(t) + 0.5}$(概率推导) |
| 关键参数 | $k_1$(饱和),$b$(长度归一化) |
| 默认值 | $k_1=1.2, b=0.75$(Lucene) |
| 关键改进 | TF 饱和 + 长度归一化 + 概率 IDF |
| 接班者 | Dense Retrieval(语义)+ Hybrid(混合) |
| 工业地位 | Lucene / ES / Solr / OpenSearch 默认 |
一句话记忆:BM25 = 概率推导的 IDF × 词频饱和函数 × 可调长度归一化。把 TF-IDF 的经验直觉用概率论重新推导,顺手修了词频无界和长文档偏置两个毛病。
上一篇:TF-IDF -- 经典的词项权重方案,BM25 的思想前身。下一篇:KNN / ANN -- 从精确最近邻到近似最近邻,向量检索的基础。