BM25 是什么?手把手拆解搜索引擎的核心算法

29 阅读3分钟

BM25 是什么?手把手拆解搜索引擎的核心算法

上篇我们理解了 TF-IDF 的基本逻辑。BM25 是它的改进版,修掉了两个明显缺陷。本文从公式结构到实际计算,逐步拆解每一个细节,无需数学基础也能读懂。


一、TF-IDF 的两个缺陷

回顾上篇的结论:TF-IDF 用"词频 × 逆文档频率"来衡量一个词的重要性。但它有两个问题:

缺陷一:词频线性增长 一个词出现 10 次,得分是出现 5 次的整整两倍。但现实中,同一个词反复出现,边际价值是递减的——从 0 次到 1 次是质变,从 9 次到 10 次几乎没什么意义。

缺陷二:没有校正文档长度 同一个词在一篇 500 字的短文里出现 3 次,和在一篇 5 万字的长文里出现 3 次,重要性截然不同。短文里它更"密集",更能代表主题。TF-IDF 对此视而不见。

BM25 就是针对这两点的经典修正。


二、BM25 公式全貌

Score(Q,D)=iIDFBM25(qi)TF(qi,D)(k1+1)TF(qi,D)+k1(1b+bDavgdl)\text{Score}(Q, D) = \sum_i \text{IDF}_{\text{BM25}}(q_i) \cdot \frac{\text{TF}(q_i, D)(k_1 + 1)}{\text{TF}(q_i, D) + k_1\left(1 - b + b \cdot \frac{|D|}{\text{avgdl}}\right)}

看起来很长,但拆开来只有三个独立模块,加上一个求和符号。


三、模块一:∑ 求和符号

i\sum_i

查询 Q 通常包含多个词,比如"模型蒸馏"会被拆成"模型"和"蒸馏"两个词。

对每个词 qiq_i 分别算一个得分,最后加总,就是这篇文档对这条查询的总得分。多个词同时命中,得分会叠加——这也是为什么"同时包含两个关键词的文档"往往排名更高。


四、模块二:IDF 部分(稀有词加权)

BM25 对 IDF 的写法和 TF-IDF 略有不同:

IDFBM25(t)=lnNDF(t)+0.5DF(t)+0.5\text{IDF}_{\text{BM25}}(t) = \ln \frac{N - \text{DF}(t) + 0.5}{\text{DF}(t) + 0.5}

  • N = 语料库文档总数
  • DF(t) = 包含词 t 的文档数

和 TF-IDF 相比,分子从"N(总数)"换成了"N − DF(t)(不含该词的文档数)":

TF-IDFBM25
分子含义文档总数不含该词的文档数
传达的意思总共有多少文档不含它的文档是含它的多少倍

分子分母各加 0.5,是为了防止极端情况(比如某个词出现在所有文档里,DF = N,分子变成 0.5,公式不会崩掉)。

代入例子感受一下,假设语料库共 10 篇文档:

"模型"出现在 3 篇(df = 3):

IDF=ln103+0.53+0.5=ln7.53.5=ln(2.14)0.76\text{IDF} = \ln\frac{10 - 3 + 0.5}{3 + 0.5} = \ln\frac{7.5}{3.5} = \ln(2.14) \approx 0.76

"蒸馏"出现在 2 篇(df = 2):

IDF=ln102+0.52+0.5=ln8.52.5=ln(3.4)1.22\text{IDF} = \ln\frac{10 - 2 + 0.5}{2 + 0.5} = \ln\frac{8.5}{2.5} = \ln(3.4) \approx 1.22

"蒸馏"更稀有,IDF 更高,符合直觉。


五、模块三:TF 饱和(BM25 最核心的改进)

TF×(k1+1)TF+k1\frac{\text{TF} \times (k_1 + 1)}{\text{TF} + k_1}

(先忽略长度部分,只看 k1k_1 的作用)

当 TF 非常大时,这个分数会趋近于 (k1+1)(k_1 + 1),不会无限增长——这就是饱和

用数字感受一下(k1=1.5k_1 = 1.5):

出现次数(TF)得分增量
11.00
21.40+0.40
52.00+0.60
102.20+0.20
1002.49几乎不动

出现 10 次不会是出现 5 次的两倍,边际贡献越来越小——这才符合现实。

k1k_1 的值控制饱和的速度,k1k_1 越小,越早到达上限;k1k_1 越大,增长空间越大。


六、模块四:长度归一化(分母里的 b)

分母里这一段负责惩罚长文档:

k1(1b+bDavgdl)k_1\left(1 - b + b \cdot \frac{|D|}{\text{avgdl}}\right)

逐个拆解符号:

D|D| 是什么?

D|D| 表示文档 D 包含的词数,两侧的竖线是数学里"取大小"的符号。比如 D=200|D| = 200,就是说这篇文档有 200 个词。

avgdl 是什么?

avgdl 是 average document length 的缩写,即语料库里所有文档长度的平均值

比如语料库有 3 篇文档,长度分别是 200、300、250 词:

avgdl=200+300+2503=250 词\text{avgdl} = \frac{200 + 300 + 250}{3} = 250 \text{ 词}

它的作用是提供一个基准线——当前文档比这个均值长还是短,决定了是否需要被惩罚。

b[0,1]b \in [0, 1] 是什么意思?

\in 读作"属于",表示取值范围。[0,1][0, 1] 是闭区间,方括号表示包含两端端点。

所以 b[0,1]b \in [0, 1] 就是说:b 可以取 0、0.1、0.5、0.75、1 等任意 0 到 1 之间的值(含 0 和 1)。

顺便,数学里括号的区别:

写法含义
[0,1][0, 1]包含两端,b 可以 = 0 或 = 1
(0,1)(0, 1)不含两端,b 只能在 0 和 1 之间
[0,1)[0, 1)含左不含右,b 可以 = 0,但不能 = 1

D/avgdl|D|/\text{avgdl} 影响的是哪里?

注意:D/avgdl|D|/\text{avgdl} 只出现在分母里,IDF 和分子完全不受影响:

Score=i IDF×TF×(k1+1)TF+k1(1b+bDavgdl)\text{Score} = \sum_i \ \text{IDF} \times \frac{\text{TF} \times (k_1+1)}{\text{TF} + k_1\left(1-b+b\cdot\frac{|D|}{\text{avgdl}}\right)}

其中 D/avgdl|D|/\text{avgdl} 只出现在分母,IDF 和分子完全不受影响。

逻辑链是:

文档变长 → D/avgdl|D|/\text{avgdl} 变大 → 分母里的长度惩罚项变大 → TF 部分的分数被压低 → 最终 Score 降低

是通过"把分母撑大"间接压低得分,而不是直接作用在整个 Score 上。

b 的值效果
b = 0完全不考虑文档长度
b = 1完全按长度归一化
b = 0.75实践中最常用的默认值

以 doc_1(文档长度 200 词,avgdl = 250)为例

1.5×(10.75+0.75×200250)=1.5×(0.25+0.6)=1.5×0.85=1.2751.5 \times \left(1 - 0.75 + 0.75 \times \frac{200}{250}\right) = 1.5 \times (0.25 + 0.6) = 1.5 \times 0.85 = 1.275

doc_1 比平均文档短,惩罚项(1.275)小于 k1k_1(1.5),因此短文档得到了轻微奖励


七、手算一遍:验证日志里的数字

以下是一段真实的 BM25 运行日志。先逐行读懂它,再动手验证数字。

查询分词: ["模型", "蒸馏"]

词 "模型" → df=3, IDF=0.76
  doc_1: TF=5, 文档长度=200词, BM25贡献=1.52
  doc_3: TF=2, 文档长度=500词, BM25贡献=0.82
  doc_7: TF=8, 文档长度=150词, BM25贡献=1.68

词 "蒸馏" → df=2, IDF=1.22
  doc_1: TF=3, 文档长度=200词, BM25贡献=2.15
  doc_5: TF=1, 文档长度=250词, BM25贡献=1.22

最终排序: doc_1 (3.67) > doc_7 (1.68) > doc_5 (1.22) > doc_3 (0.82)

逐行解释:

第 1 行:查询"模型蒸馏"被拆成两个词,后面对每个词分别处理。

第 2~5 行:"模型"这个词的检索结果。

  • df=3 → 整个语料库里有 3 篇文档包含"模型"
  • IDF=0.76 → 代入公式算出来的稀有度得分
  • doc_1 那一行的意思:在 doc_1 里,"模型"出现了 5 次(TF=5),这篇文档有 200 个词,算出来"模型"对 doc_1 的贡献是 1.52 分

第 6~8 行:"蒸馏"这个词的检索结果。

  • df=2 → 只有 2 篇文档包含"蒸馏",比"模型"更稀有
  • IDF=1.22 → 稀有度得分更高
  • 注意 doc_5 只被"蒸馏"命中,没有被"模型"命中

最后一行:每篇文档把所有词的贡献加起来,就是最终得分:

文档"模型"贡献"蒸馏"贡献总分
doc_11.522.153.67
doc_71.68未命中1.68
doc_5未命中1.221.22
doc_30.82未命中0.82

doc_1 同时命中两个词,得分叠加,远超其他文档,排第一。


现在来手算验证,参数:k1=1.5k_1 = 1.5b=0.75b = 0.75avgdl=250\text{avgdl} = 250


第一步:算 doc_1 的长度惩罚项

k1(1b+bDavgdl)=1.5×(0.25+0.75×200250)=1.5×0.85=1.275k_1\left(1 - b + b \cdot \frac{|D|}{\text{avgdl}}\right) = 1.5 \times \left(0.25 + 0.75 \times \frac{200}{250}\right) = 1.5 \times 0.85 = 1.275


第二步:算"模型"在 doc_1 的贡献(TF = 5)

先算完整精度的 IDF:

IDF=ln7.53.5=ln(2.1428)=0.7621\text{IDF} = \ln\frac{7.5}{3.5} = \ln(2.1428) = 0.7621

代入 BM25 公式:

0.7621×5×(1.5+1)5+1.275=0.7621×12.56.275=0.7621×1.9921.520.7621 \times \frac{5 \times (1.5 + 1)}{5 + 1.275} = 0.7621 \times \frac{12.5}{6.275} = 0.7621 \times 1.992 \approx 1.52 \checkmark


第三步:算"蒸馏"在 doc_1 的贡献(TF = 3)

先算完整精度的 IDF:

IDF=ln8.52.5=ln(3.4)=1.2238\text{IDF} = \ln\frac{8.5}{2.5} = \ln(3.4) = 1.2238

代入 BM25 公式:

1.2238×3×(1.5+1)3+1.275=1.2238×7.54.275=1.2238×1.7542.151.2238 \times \frac{3 \times (1.5 + 1)}{3 + 1.275} = 1.2238 \times \frac{7.5}{4.275} = 1.2238 \times 1.754 \approx 2.15 \checkmark


第四步:doc_1 总分

贡献
"模型"1.52
"蒸馏"2.15
总分3.67

八、为什么"蒸馏"贡献反而更大?

"蒸馏"在 doc_1 里的词频(TF = 3)明明低于"模型"(TF = 5),为什么贡献更大?

IDFTF饱和后的TF项最终贡献
"模型"0.76(常见)51.991.52
"蒸馏"1.22(稀有)31.752.15

"蒸馏"的 IDF 比"模型"高出约 60%,完全弥补了 TF 的差距。

这正是 BM25 的核心权衡:稀有性比出现次数更重要。一个词在文档集合里越罕见,哪怕只出现一两次,也能提供极强的区分信号。


九、稀疏检索的优势与局限

通过上面的计算,BM25(稀疏检索)的特点已经很清晰:

方面说明
优势精确关键词匹配,在技术术语、人名、代码等查询上表现极佳
局限读不懂同义表达——查"汽车",匹配不到只写"车辆"的文档

这种局限正是下一步引入混合检索(稀疏 + 稠密向量)的动机:用语义向量弥补关键词匹配的盲区。


十、三个模块一句话总结

模块作用
IDF稀有词加权
TF 饱和部分词频边际递减,出现再多也有上限
长度归一化压制长文档的天然优势
∑ 求和多词得分叠加,命中越多排名越高

BM25 没有引入全新的概念,只是把 TF-IDF 的两个缺陷——线性词频忽略文档长度——用数学方式优雅地修掉了。


理解 BM25,是读懂稀疏检索、RAG 管道和现代搜索引擎的关键一步。