bigywater.tech
Elasticsearch · 42

BM25 评分公式实测

真实用_explain拿到BM25评分的完整计算明细,用Python手工重算公式验证与ES内部完全一致,并用4篇相同长度、词频依次翻倍的文档量化k1参数带来的真实饱和效应。

explain明细
真实暴露idf、tf、boost每个分量的具体数值
手工Python重算
真实算出跟ES完全一致的分数,精确到小数点后7位
词频1/2/4/8饱和曲线
真实验证k1带来的边际收益递减

做的事:真实用_explain拿到一次term查询的完整BM25计算明细

代码3篇文档: doc1{body:"cat cat cat dog bird"}(5词,cat出现3次) doc2{body:"cat dog dog bird fish mouse"}(6词,cat出现1次) doc3{body:"dog bird fish"}(3词,不含cat) POST /bm25_test/_explain/1 {"query":{"term":{"body":"cat"}}}

验证:explain真实把idf、tf、boost三个分量和它们各自的计算参数全部摊开了

实测 · 本机 Elasticsearch 8.15.0 执行输出
doc1命中"cat"的完整explain:
→ score = boost * idf * tf = 0.72744286
→ boost = 2.2
→ idf = log(1 + (N-n+0.5)/(n+0.5)) = 0.47000363
  其中 n(含cat的文档数)=2, N(总文档数)=3
→ tf = freq/(freq + k1*(1-b+b*dl/avgdl)) = 0.70351756
  其中 freq=3.0, k1=1.2, b=0.75, dl(doc1长度)=5.0, avgdl(平均长度)=4.6666665

这份真实的explain明细直接把BM25公式的每一个分量都摊开展示了出来:idf(逆文档频率)衡量"cat"这个词在整个语料库里有多稀有,tf(词频饱和项)衡量"cat"在当前这篇文档里出现得有多频繁,并且真实考虑了文档长度的归一化。两者相乘再乘以boost,就是这次匹配真实贡献的最终得分。这不是理论公式,而是ES在这次真实查询里真实用到的每一个具体数值。

真实按官方公式用Python手工重算一遍,验证算出来的结果跟ES返回的是否完全一致

代码import math N, n, freq, k1, b, dl, avgdl, boost = 3, 2, 3.0, 1.2, 0.75, 5.0, 4.6666665, 2.2 idf = math.log(1 + (N-n+0.5)/(n+0.5)) tf = freq/(freq + k1*(1-b+b*dl/avgdl)) score = boost * idf * tf

验证:手工用Python重算出的分数真实跟ES返回的分数完全对上,精确到小数点后7位

实测 · 本机Python手工计算输出
手工计算结果:
→ idf = 0.47000362924573563   (ES真实返回0.47000363,一致)
→ tf  = 0.7035175822580235    (ES真实返回0.70351756,一致)
→ score = 0.7274427971788041  (ES真实返回0.72744286,一致)
这组真实的手工验证证明了BM25在ES里真实不是一个黑箱——它真实就是一个可以被精确复现的数学公式,explain暴露出来的每一个中间变量(N、n、freq、k1、b、dl、avgdl)真实都能直接代入公式算出跟ES内部一模一样的结果。这对于真实排查"为什么这篇文档排名比那篇高"这类相关性问题很有价值:不用猜测,真实按这个公式把两篇文档的得分拆开对比,就能看清楚到底是因为词频更高、文档更短、还是词更稀有导致的排名差异。

真实验证k1参数带来的词频饱和效应:4篇长度相同、cat词频分别是1/2/4/8的文档

代码真实构造4篇长度都是10个词的文档,cat出现次数依次翻倍: 1次/2次/4次/8次(其余用x填充凑够长度) POST /bm25_sat/_explain/1 ... /_explain/4 {"query":{"term":{"body":"cat"}}}

验证:cat词频翻倍,得分真实不会跟着翻倍,涨幅真实逐级递减

实测 · 本机执行输出
freq=1: score=0.10536051
freq=2: score=0.14487070   (比freq=1真实涨了约37.5%,不是100%)
freq=4: score=0.17830240   (比freq=2真实只涨了约23.1%)
freq=8: score=0.20155925   (比freq=4真实只涨了约13.0%)

这组真实的递增曲线直接展示了k1参数存在的意义:BM25真实故意让词频的贡献呈现边际收益递减,而不是简单线性累加。如果没有这个饱和机制,一篇文档靠疯狂堆砌同一个关键词(关键词堆砌,SEO作弊的经典手法)就能无限推高相关性得分;而真实的BM25公式里,词频从1到2带来的得分提升(约37.5%)明显大于从4到8带来的提升(约13.0%),词频越高,继续增加带来的边际收益真实越小。k1这个参数(本次实测固定在ES默认值1.2)真实控制的就是这条饱和曲线弯曲的速度,k1越大,饱和发生得越慢;本文这组真实数据直接把"BM25不是简单的词频计数"这个结论用具体数字量化了出来。

分步演示:BM25评分公式从explain明细拆解到手工验证与饱和效应量化的完整过程

每一步都是本文上面在本机真实的Elasticsearch 8.15.0上跑出来的原始结果。

从explain明细到手工公式验证,再到真实量化的词频饱和曲线
这一步新验证的BM25分量

    本文用到的工具

    • 本机真实部署的 Elasticsearch 8.15.0(单节点,tar包直接启动)
    • 真实的 _explain API 与 Python 手工公式验证
    京ICP备2026067502号-1京公网安备11010502063487号