3.1 Elasticsearch-TF-IDF vs BM25 评分公式拆解

在这里插入图片描述

3.1 Elasticsearch-TF-IDF vs BM25 评分公式拆解

在 Elasticsearch 5.0 之前,默认的文本相似度计算模型是 TF-IDF(Lucene 的 Practical Scoring Function)。5.0 之后,官方把默认模型换成了 BM25。很多老项目升级后发现“同样的查询语句,打分变了,排序也变了”,根源就在这两条公式的差异。下面把两条公式拆开到字段级别,逐行对比它们对“词频饱和”和“文档长度归一化”这两个核心问题的不同态度,并给出可直接在 Kibana Dev-Tools 里验证的实验脚本。


1. 公式骨架对照
维度TF-IDF(Lucene 实际实现)BM25(Lucene 实现)
词频因子tf(t in d) = √freq`freq / (freq + k₁·(1-b+b·
逆文档频率idf(t) = 1 + log((N-n+0.5)/(n+0.5))同左,Lucene 直接复用
长度归一化`norm(d) = 1/√d
查询 boost外部乘 q.boost外部乘 q.boost
协调因子coord(q,d)(命中查询子句比例)无(BM25 本身已含饱和度)

注意:Lucene 的 TF-IDF 并不是教科书里的“纯 TF·IDF”,而是做了平方根平滑和长度归一化之后的 Practical Scoring Function,下文简称 TF-IDF。


2. 逐因子拆解
2.1 词频饱和度(Saturation)
  • TF-IDF
    tf = √freq 永无止境增长,freq=100 时 tf=10,freq=400 时 tf=20,线性放大。结果是:一篇堆砌关键词 400 次的文档,得分可以碾压真正相关的短文档。

  • BM25
    分母里 freq + k₁·(...) 让 tf 随 freq 增加而趋近上限 1/k₁。k₁ 默认 1.2,意味着 freq 从 1→10→100,tf 分量从 0.45→0.89→0.99,几乎“封顶”。垃圾堆砌不再有效。

实验:

PUT idx_satu
{ "settings": { "number_of_shards": 1, "similarity": { "classic": { "type": "classic" }, "bm25": { "type": "BM25", "k1": 1.2 } } }, "mappings": { "properties": { "title": { "type": "text", "similarity": "classic", "fields": { "bm25": { "type": "text", "similarity": "bm25" } } } } } }

POST idx_satu/_doc/1
{ "title": "java" }
POST idx_satu/_doc/2
{ "title": "java java java java java java java java java java" }  // 10次

GET idx_satu/_search
{ "explain": true, "query": { "match": { "title": "java" } } }

对比 _explanationtf(freq=1)tf(freq=10) 的得分倍数:

  • classic: 1 → 3.16(√10)
  • bm25: 1 → 1.9(接近 2 倍即饱和)
2.2 文档长度归一化
  • TF-IDF
    索引时把 1/√|d| 乘进 norm,查询阶段不可调。短文档天生自带 buff;长文档被惩罚过度,常常“有理说不出”。

  • BM25
    长度惩罚因子 (1-b+b·|d|/avgdl) 把“相对长度”放在分母,且 b 可动态调:

    • b=0:完全取消长度惩罚,适合标题、标签等短字段;
    • b=1:全额惩罚,适合正文。
      由此带来一个实战技巧:同一条查询可以跨多个字段,各自使用不同的 b 值。

实验:

PUT idx_len
{ "settings": { "number_of_shards": 1, "similarity": {
    "short": { "type": "BM25", "b": 0 },
    "long":  { "type": "BM25", "b": 1 }
} }, "mappings": { "properties": {
    "title": { "type": "text", "similarity": "short" },
    "body":  { "type": "text", "similarity": "long"  }
} } }

POST idx_len/_doc/1
{ "title": "java", "body": "java is a language" }
POST idx_len/_doc/2
{ "title": "java java", "body": "java".repeat(200) }  // 200次

GET idx_len/_search
{ "query": { "multi_match": { "query": "java", "fields": [ "title^2", "body" ] } } }

结果:

  • title 字段 b=0,长度几乎不影响,doc1 的短标题仍拿高分;
  • body 字段 b=1,doc2 因长度被大幅降权,尽管词频高也追不上 doc1。

3. 参数速查表
参数作用TF-IDFBM25
k₁词频饱和度拐点无(固定√freq)默认 1.2,区间 [1.2,2.0]
b长度惩罚力度无(固定 1/√d)默认 0.75,可 0~1

调参口诀:

  1. 堆砌党多 → 把 k₁ 降到 0.5~1.0,更快饱和;
  2. 短字段(标题、标签)→ b=0;
  3. 长字段(文章、日志)→ b=1;
  4. 想复现老 TF-IDF 的排序 → 直接把 similarity 改回 “classic”,但会丢失 BM25 的饱和度好处。

4. 升级兼容性建议
  • 索引级回退
    在 7.x/8.x 集群里仍可显式指定 "similarity": "classic",但官方已标记为 deprecated,未来版本会移除。

  • 字段级混合
    同一索引让 title 用 BM25(b=0),body 用 BM25(b=1),兼顾短字段高权、长字段防刷。

  • 重新打分测试
    升级前先用 _reindex 把数据拷到测试索引,改 similarity 后跑一遍 _search?explain=true,把 TOP100 id 与线上对比,可提前发现“排序漂移”。


5. 小结一句话

TF-IDF 像“少年时期的搜索引擎”,词频无限放大、长度一刀砍;BM25 则是“成年版”,用饱和曲线和可调长度因子,让堆砌失效、让短长字段各得其所。在 Elasticsearch 里,这两条公式只差一个 similarity 名字,却可能让同一批查询结果面目全非。理解公式的每一寸弯曲,才能在升级、调参、排障时做到“手中有公式,心里不慌张”。
更多技术文章见公众号: 大城市小农民

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

乔丹搞IT

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值