BM25 是什么?手把手拆解搜索引擎的核心算法
上篇我们理解了 TF-IDF 的基本逻辑。BM25 是它的改进版,修掉了两个明显缺陷。本文从公式结构到实际计算,逐步拆解每一个细节,无需数学基础也能读懂。
一、TF-IDF 的两个缺陷
回顾上篇的结论:TF-IDF 用"词频 × 逆文档频率"来衡量一个词的重要性。但它有两个问题:
缺陷一:词频线性增长 一个词出现 10 次,得分是出现 5 次的整整两倍。但现实中,同一个词反复出现,边际价值是递减的——从 0 次到 1 次是质变,从 9 次到 10 次几乎没什么意义。
缺陷二:没有校正文档长度 同一个词在一篇 500 字的短文里出现 3 次,和在一篇 5 万字的长文里出现 3 次,重要性截然不同。短文里它更"密集",更能代表主题。TF-IDF 对此视而不见。
BM25 就是针对这两点的经典修正。
二、BM25 公式全貌
看起来很长,但拆开来只有三个独立模块,加上一个求和符号。
三、模块一:∑ 求和符号
查询 Q 通常包含多个词,比如"模型蒸馏"会被拆成"模型"和"蒸馏"两个词。
对每个词 分别算一个得分,最后加总,就是这篇文档对这条查询的总得分。多个词同时命中,得分会叠加——这也是为什么"同时包含两个关键词的文档"往往排名更高。
四、模块二:IDF 部分(稀有词加权)
BM25 对 IDF 的写法和 TF-IDF 略有不同:
- N = 语料库文档总数
- DF(t) = 包含词 t 的文档数
和 TF-IDF 相比,分子从"N(总数)"换成了"N − DF(t)(不含该词的文档数)":
| TF-IDF | BM25 | |
|---|---|---|
| 分子含义 | 文档总数 | 不含该词的文档数 |
| 传达的意思 | 总共有多少文档 | 不含它的文档是含它的多少倍 |
分子分母各加 0.5,是为了防止极端情况(比如某个词出现在所有文档里,DF = N,分子变成 0.5,公式不会崩掉)。
代入例子感受一下,假设语料库共 10 篇文档:
"模型"出现在 3 篇(df = 3):
"蒸馏"出现在 2 篇(df = 2):
"蒸馏"更稀有,IDF 更高,符合直觉。
五、模块三:TF 饱和(BM25 最核心的改进)
(先忽略长度部分,只看 的作用)
当 TF 非常大时,这个分数会趋近于 ,不会无限增长——这就是饱和。
用数字感受一下():
| 出现次数(TF) | 得分 | 增量 |
|---|---|---|
| 1 | 1.00 | — |
| 2 | 1.40 | +0.40 |
| 5 | 2.00 | +0.60 |
| 10 | 2.20 | +0.20 |
| 100 | 2.49 | 几乎不动 |
出现 10 次不会是出现 5 次的两倍,边际贡献越来越小——这才符合现实。
的值控制饱和的速度, 越小,越早到达上限; 越大,增长空间越大。
六、模块四:长度归一化(分母里的 b)
分母里这一段负责惩罚长文档:
逐个拆解符号:
是什么?
表示文档 D 包含的词数,两侧的竖线是数学里"取大小"的符号。比如 ,就是说这篇文档有 200 个词。
avgdl 是什么?
avgdl 是 average document length 的缩写,即语料库里所有文档长度的平均值。
比如语料库有 3 篇文档,长度分别是 200、300、250 词:
它的作用是提供一个基准线——当前文档比这个均值长还是短,决定了是否需要被惩罚。
是什么意思?
读作"属于",表示取值范围。 是闭区间,方括号表示包含两端端点。
所以 就是说:b 可以取 0、0.1、0.5、0.75、1 等任意 0 到 1 之间的值(含 0 和 1)。
顺便,数学里括号的区别:
| 写法 | 含义 |
|---|---|
| 包含两端,b 可以 = 0 或 = 1 | |
| 不含两端,b 只能在 0 和 1 之间 | |
| 含左不含右,b 可以 = 0,但不能 = 1 |
影响的是哪里?
注意: 只出现在分母里,IDF 和分子完全不受影响:
其中 只出现在分母,IDF 和分子完全不受影响。
逻辑链是:
文档变长 → 变大 → 分母里的长度惩罚项变大 → TF 部分的分数被压低 → 最终 Score 降低
是通过"把分母撑大"间接压低得分,而不是直接作用在整个 Score 上。
| b 的值 | 效果 |
|---|---|
| b = 0 | 完全不考虑文档长度 |
| b = 1 | 完全按长度归一化 |
| b = 0.75 | 实践中最常用的默认值 |
以 doc_1(文档长度 200 词,avgdl = 250)为例:
doc_1 比平均文档短,惩罚项(1.275)小于 (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_1 | 1.52 | 2.15 | 3.67 |
| doc_7 | 1.68 | 未命中 | 1.68 |
| doc_5 | 未命中 | 1.22 | 1.22 |
| doc_3 | 0.82 | 未命中 | 0.82 |
doc_1 同时命中两个词,得分叠加,远超其他文档,排第一。
现在来手算验证,参数:,,
第一步:算 doc_1 的长度惩罚项
第二步:算"模型"在 doc_1 的贡献(TF = 5)
先算完整精度的 IDF:
代入 BM25 公式:
第三步:算"蒸馏"在 doc_1 的贡献(TF = 3)
先算完整精度的 IDF:
代入 BM25 公式:
第四步:doc_1 总分
| 词 | 贡献 |
|---|---|
| "模型" | 1.52 |
| "蒸馏" | 2.15 |
| 总分 | 3.67 |
八、为什么"蒸馏"贡献反而更大?
"蒸馏"在 doc_1 里的词频(TF = 3)明明低于"模型"(TF = 5),为什么贡献更大?
| IDF | TF | 饱和后的TF项 | 最终贡献 | |
|---|---|---|---|---|
| "模型" | 0.76(常见) | 5 | 1.99 | 1.52 |
| "蒸馏" | 1.22(稀有) | 3 | 1.75 | 2.15 |
"蒸馏"的 IDF 比"模型"高出约 60%,完全弥补了 TF 的差距。
这正是 BM25 的核心权衡:稀有性比出现次数更重要。一个词在文档集合里越罕见,哪怕只出现一两次,也能提供极强的区分信号。
九、稀疏检索的优势与局限
通过上面的计算,BM25(稀疏检索)的特点已经很清晰:
| 方面 | 说明 |
|---|---|
| 优势 | 精确关键词匹配,在技术术语、人名、代码等查询上表现极佳 |
| 局限 | 读不懂同义表达——查"汽车",匹配不到只写"车辆"的文档 |
这种局限正是下一步引入混合检索(稀疏 + 稠密向量)的动机:用语义向量弥补关键词匹配的盲区。
十、三个模块一句话总结
| 模块 | 作用 |
|---|---|
| IDF | 稀有词加权 |
| TF 饱和部分 | 词频边际递减,出现再多也有上限 |
| 长度归一化 | 压制长文档的天然优势 |
| ∑ 求和 | 多词得分叠加,命中越多排名越高 |
BM25 没有引入全新的概念,只是把 TF-IDF 的两个缺陷——线性词频和忽略文档长度——用数学方式优雅地修掉了。
理解 BM25,是读懂稀疏检索、RAG 管道和现代搜索引擎的关键一步。