2623 字
约 8 分钟
16
S13-B06 · 相关性排序入门:打分、Boost 与重排

文档里明明有那个词,却排在第五位;把 boost 从 2 调到 10,结果更乱。这不是引擎排不准,而是三件事被混在一起:BM25 算"这段文字有多像查询",boost 只是在乘常数,重排才是你想要的业务判断。分开看,每一层都能验证。


1. BM25 在算什么

组成 作用 直觉 参数
词频 TF 词在这篇文档出现几次 越多越相关,但增益递减 k1(默认 1.2)
逆文档频率 IDF 这个词在全库多稀有 越稀有越有区分度 由语料决定
长度归一 这篇比平均长多少 长文档天然词多,要打折 b(默认 0.75)
score(q,d) = Σ  IDF(qi) · tf·(k1+1) / ( tf + k1·(1 - b + b·|d|/avgdl) )
IDF(qi)    = ln( 1 + (N - n + 0.5) / (n + 0.5) )

两条结论直接读自公式:k1 控制饱和速度(越小越快顶格),b 控制长度惩罚(0 不罚、1 按比例罚)。停用词与全库常见词的 IDF 接近 0,加多少次都不动排序。按 Elasticsearch 8.x 核对,这两个默认值成立,BM25 是默认相似度。


2. 词频为什么不是线性加分

线性加分等于说"出现 10 次 = 相关 10 倍"。真实语料里立刻破产:讲 Redis 的长文出现 40 次,不代表比出现 8 次的短文相关 5 倍,它只是更长。

出现次数 增益趋势(k1=1.2 定性) 说明
1 → 2 明显上升 从"提到"到"在讲"
2 → 5 缓慢上升 主题确认,边际递减
5 → 20 几乎不动 已饱和
20 → 40 可视为不动 排序转由 IDF 与长度归一定
查询 "redis 持久化":A 是 3000 字长文(redis 出现 30 次),B 是 400 字短文(6 次)
A 词频高,但 |d|/avgdl 打折;B 词频低,长度归一几乎不惩罚。
"词频高"≠"排前面",这是"明明有这个词却排后面"的第一个原因。

3. 字段权重与多字段叠加

每个字段单独跑一次 BM25,bool 的 should 再把分数相加。

GET /articles/_search
{ "query": { "multi_match": { "query": "redis 持久化",
  "fields": ["title^3", "tags^2", "body"], "type": "best_fields" } } }
写法 打分行为 用在哪
best_fields 取单字段最高分,不累加 关键词集中在一处,标题命中即最优
most_fields 多字段分数累加 同一信息分散在多个字段
cross_fields 把多字段当一个大字段 查询词分散("redis"在标题、"持久化"在正文)
title^3 该字段分数乘 3 标题命中应显著优先

best_fields 与 most_fields 选错,是"分数有但排序不对"的高频原因:A 只在标题命中一次,B 在标题和正文各命中一次,most_fields 会把 B 顶到 A 前面,而你想要的通常是 A。


4. boost 的三种位置

用法 写法 生效时机 代价
查询期 "title^3"、"boost": 2.0 每次查询 改完即生效
索引期 mapping 内字段 boost(旧版) 建索引时 8.x 已废弃
函数加权 function_score 乘系数 每次查询,可在算分后介入 不能进 filter 缓存
{ "query": { "function_score": {
  "query": { "match": { "body": "redis 持久化" } },
  "functions": [
    { "filter": { "term": { "is_pinned": true } }, "weight": 2 },
    { "gauss": { "publish_at": { "origin": "now", "scale": "30d", "decay": 0.5 } } } ],
  "score_mode": "sum", "boost_mode": "multiply" } } }

score_mode 管多个函数之间怎么合,boost_mode 管函数分与原查询分怎么合(multiply 最常用,replace 会完全丢弃 BM25)。

boost 调到 5 以上还没解决,问题就不在 boost。 大倍数的后果是"某几篇永远第一",该上来的一篇仍上不来——缺口来自 analyzer、字段映射或查询结构,乘常数补不回来。


5. 同义词与停用词怎么影响打分

配置 对 TF 对 IDF 后果
同义词归一 词频合并,TF 上升 文档数变多,IDF 下降 召回变多,排序被摊平
同义词展开 每词项独立计分 各自 IDF 分数虚高,需 minimum_should_match 收敛
停用词移除 少算几个词项 不再占分 总分变低,相对排序不变
换 analyzer 词项粒度变,TF 全变 IDF 全变 新旧 _score 不可比

最后一行最易忽略:换分词器等于换了一套打分基准,改完不要拿新旧分数对比,只能重跑评测集看排序。


6. 排序与过滤分工:filter 不打分

子句 算分 可缓存 用途
must / should 是 否 影响相关性
filter 否 是 硬条件:状态、时间、租户
must_not 否 是 排除
{ "query": { "bool": {
  "must":   [ { "match": { "body": "redis 持久化" } } ],
  "filter": [ { "term": { "status": "published" } },
              { "range": { "publish_at": { "gte": "now-1y" } } } ] } } }

"已发布""近一年"放进 must 会参与算分:文档因为"状态是 published"而加分,这对相关性纯属噪声。凡不影响"像不像"的条件,一律进 filter。


7. 二次重排:插在哪、值不值

手段 插入位置 代价 适合
rescore 首轮取 Top-N 后 只重算 N 篇 精细的 script_score
业务加权 function_score 内 每篇参与,不可缓存 置顶、加权规则
时效衰减 高斯/线性衰减 每篇参与 资讯流、帖子流
外部 rerank 应用层拿 Top-50 一次额外调用 语义模型、跨字段规则
分页陷阱:重排只覆盖首轮 Top-N。N=50 而要第 3 页时,
第 51 名之后没被重排,翻页结果会"跳变"。
做法:把 N 提到覆盖可翻页范围,或用 search_after 限制深翻。

8. 可解释:explain 怎么读,_score 能不能比

weight(body:redis in 12) -- 2.31
  score(doc=12, freq=4.0), product of:
    2.31 = idf            <- 这个词有多稀有,主要贡献者
    1.00 = tfNorm         <- 由 (freq*(k1+1))/(freq + k1*(1-b+b*len/avgLen)) 算出
读法:先 idf(词重不重要)→ 再 tfNorm(是否饱和)→ 最后长度归一
误解 事实
两个查询的 _score 能比大小 不能,换查询就换基准
12 比 6 相关一倍 不成立,BM25 无绝对单位
同一查询内不同文档可比 可比,这是唯一可比范围
跨页分数可比 同查询内可比,但深翻 + rescore 会破坏排序稳定

9. 规则排序与学习排序

维度 规则排序 学习排序(LTR)
上线成本 改配置即生效 标注 + 特征管道 + 模型部署
可解释性 高(explain 逐项拆) 低(权重难归因)
迭代速度 分钟级 天到周级
适用阶段 数据少、需求常变 有稳定点击/转化标注

判断标准:说不清"什么算相关"时,先别上 LTR。 规则排序的价值不只是效果,它把"相关性定义"写了下来——那正是 LTR 的标注规范。


10. 最小可跑示例

curl -s -XPUT localhost:9200/art -H 'Content-Type: application/json' -d '{
 "mappings":{"properties":{"title":{"type":"text"},"body":{"type":"text"},
 "publish_at":{"type":"date"},"hot":{"type":"integer"}}}}'
curl -s -XPOST 'localhost:9200/art/_bulk?refresh=true' -H 'Content-Type: application/json' --data-binary '
{"index":{"_id":"1"}}
{"title":"Redis 持久化详解","body":"RDB 与 AOF 的取舍","publish_at":"2026-11-01","hot":5}
{"index":{"_id":"2"}}
{"title":"缓存杂谈","body":"redis redis redis 持久化 持久化 持久化 等等","publish_at":"2026-11-01","hot":1}
{"index":{"_id":"3"}}
{"title":"面试题集","body":"redis 持久化 常见问题","publish_at":"2024-01-01","hot":50}'
curl -s 'localhost:9200/art/_search?explain=true' -H 'Content-Type: application/json' \
  -d '{"query":{"match":{"body":"redis 持久化"}},"size":3}'

观察:文档 2 靠词频堆量可能压过 1,而 1 的标题才是强信号。第二步改结构,而不是调大 body 的 boost:

{ "query": { "multi_match": { "query": "redis 持久化",
  "fields": ["title^4", "body"], "type": "best_fields" } } }

第三步加时效衰减,让老文档不再靠 hot 压新人,再对照 explain 里的 idf/tfNorm,确认排序变化来自结构而非常数。


11. 排错:六个高频坑

现象 根因 处理
跨查询比 _score 把分数当绝对相关度 只看同查询内排序,跨查询用 NDCG/点击率
boost 越调越乱 用常数补结构缺陷 回查 analyzer、字段映射、best/most_fields
业务规则全塞进打分 用 function_score 做置顶+屏蔽 硬条件走 filter,置顶走独立排序层
换分词器后排序全变 TF/IDF 基准变了 重建索引后重跑评测集
过滤条件写成 must 状态与时间参与了算分 改 filter,顺带拿缓存
深翻页结果跳变 rescore 只覆盖首轮 Top-N 调大窗口或 search_after

12. 边界:打分之外的手段

场景 该用的东西 理由
精确匹配、枚举过滤 term + filter 要命中,不要相关性
语义相近但字面不同 向量 / 混合检索 BM25 只认词项
稳定业务顺序 显式 sort 排序字段优先于打分
千人千面 LTR 或外部重排 规则表达不了用户维度
需要归因 保留 explain + 规则排序 模型分难拆解

13. 自测

  1. BM25 的三块各解决什么问题?去掉长度归一后长文档会怎样?
  2. k1 调大后,"词频高"的文档更容易还是更难排前面?
  3. best_fields 与 most_fields 在多字段下差在哪?
  4. 为什么"已发布"写进 must 会污染排序?该改成什么?
  5. 两个不同查询的 _score 为什么不可比?可比范围有多大?
  6. rescore 只作用于 Top-N,翻到第 3 页会出现什么?怎么规避?
S13-B06 · 相关性排序入门:打分、Boost 与重排
https://www.clxhxhhr.top/posts/6089/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。