「相关性与打分进阶:从 BM25 到 function_score」

深入讲解 Elasticsearch 相关性打分:BM25 的 k1 b 参数调优、boost 权重语义、function_score 的脚本与因子、多字段查询加权,以及自定义评分的实践与陷阱。

搜索引擎的质量最终由排序决定,而排序来自打分。Elasticsearch 默认用 BM25 计算相关度,但业务排序几乎总会叠加权重、衰减与人工干预。本文从 BM25 的 k1 b 参数讲起,覆盖 boost 语义、function_score 的多种函数与脚本打分,最后给出多字段加权与调优的实战方法。

1. 相关性评分原理回顾

一句话总结: 打分是把查询与文档的匹配程度折叠成一个可排序的数值,BM25 是默认的相关度模型。

1.1 打分的基本组成

文档得分来自三部分:查询词项的词频(TF)、倒排文档频率(IDF)与文档长度归一化。词在文档中越频繁、在全局越稀有、文档越短,得分越高。_explain 可以看到每个词的贡献明细。

1.2 看一条打分明细

curl -X POST "localhost:9200/products/_explain/1" -H "Content-Type: application/json" -d'
{
  "query": { "match": { "title": "无线降噪耳机" } }
}
'

_explain 返回计算树:每个词的 weight、tf、idf 与乘积,是排查「为什么它排前面」的第一工具。生产排查时对 top 文档逐个 _explain,能直观看到权重分配。

1.3 从 TF-IDF 到 BM25

Lucene 早先用 TF-IDF,后被 BM25 取代。BM25 引入 k1 与 b 两个参数:k1 控制词频饱和速度,b 控制文档长度归一化的强度。这两个参数几乎决定相关度的宏观形态。

2. BM25 参数调优

一句话总结: k1 控制词频的收益曲线,b 控制长文档惩罚强度,调优需基于自身语料用标注集验证。

2.1 k1 与 b 的含义

k1 越大,词频带来的边际收益衰减越慢,高词频词对分数影响更大;k1 趋于 0 时词频几乎不影响分数。b 越大,长文档被惩罚得越狠;b=0 时完全不考虑文档长度。

2.2 在索引设置里配置

BM25 参数在索引创建时配置,作用于该索引的 scoring,通过 similarity 设置项指定:

PUT /articles
{
  "settings": {
    "index.similarity.default.type": "BM25",
    "index.similarity.default.k1": 1.2,
    "index.similarity.default.b": 0.75
  }
}

settings 一旦建立即固化,参数后续只能通过 reindex 到新索引调整。若希望对不同字段用不同相似度,可自定义命名相似度并挂到字段映射上,但复杂度高,非必要不建议。

2.3 调优方法

默认 k1=1.2、b=0.75 适合大多数文档库。短标题为主的商品库可适当降 b 减少长标题惩罚;正文长短差异极大的文库适当升 b。调优用离线标注集对比排序指标(NDCG),而非凭感觉改参数。

3. boost 权重

一句话总结: boost 给查询或字段加权,让命中的某部分权重更高,是最常用的排序干预手段。

3.1 字段级 boost

匹配标题的权重高于匹配正文:

{
  "query": {
    "multi_match": {
      "query": "无线降噪耳机",
      "fields": [ "title^3", "keywords^2", "body" ]
    }
  }
}

title^3 表示标题命中权重是正文的三倍。字段级 boost 直接乘到 BM25 得分上,是最简单也最直观的加权方式。

3.2 查询级 boost

bool 查询中 should 子句可各自 boost:

{
  "query": {
    "bool": {
      "should": [
        { "match": { "title": "降噪耳机" } },
        { "match": { "brand": "sony" }, "boost": 2.0 }
      ]
    }
  }
}

品牌精确命中加权,可以让「正品索尼」排在普通结果之前。boost 不改变命中集合,只改变排序。

3.3 boost 的语义注意

boost 是乘性而非加性,字段长度与词频差异会被放大。boost 设为 0 表示不贡献分数但仍参与过滤(用于 constant_score 场景需谨慎);boost 也可用 boost_mode 控制组合方式,见下节 function_score。

4. function_score

一句话总结: function_score 用自定义函数叠加或替换原始相关度分数,支持衰减、随机、脚本等丰富干预。

4.1 结构与模式

function_score 由 query、functions、boost_mode、score_mode 组成。boost_mode 决定函数结果如何与原始分数合并:multiply 相乘、sum 相加、replace 替换、avg 平均等。functions 里可放 filter + 函数组合。

4.2 衰减函数实现「越近越高」

商品推荐常见「越新越高」,用 gauss 或 linear 衰减按时间衰减:

{
  "query": {
    "function_score": {
      "query": { "match": { "title": "无线降噪耳机" } },
      "functions": [
        {
          "gauss": {
            "publish_date": { "origin": "2026-10-01", "scale": "30d", "offset": "7d", "decay": 0.5 }
          }
        }
      ],
      "boost_mode": "multiply"
    }
  }
}

4.3 weight 与 filter 组合

对特定条件的命中给固定权重,例如 VIP 商品加权:

{
  "query": {
    "function_score": {
      "query": { "match_all": {} },
      "functions": [
        { "filter": { "term": { "is_vip": true } }, "weight": 5 }
      ],
      "boost_mode": "sum"
    }
  }
}

boost_mode=sum 把 weight 加进原始分数,适合做离散的人工干预;multiply 适合连续因子。

5. 多字段与查询加权

一句话总结: 多字段搜索要处理字段间重复词的问题,multi_match 的 type 决定加权与去重的策略。

5.1 best_fields 与 most_fields

multi_match 的 type 决定如何合并多字段分数:best_fields 取最高字段分(默认),most_fields 把各字段分相加。标题+正文场景通常 best_fields 更合理;同义词多字段场景用 cross_fields 处理跨字段词项。

{
  "query": {
    "multi_match": {
      "query": "无线降噪耳机",
      "fields": [ "title^3", "body" ],
      "type": "best_fields"
    }
  }
}

5.2 cross_fields 的词项合并

查询词被分词后分散在不同字段(如「无线」在 title、「耳机」在 subtitle),best_fields 会因单个字段命中不全而误判不相关,cross_fields 按查询词项合并再统一打分,适合字段划分细碎的结构化文本。

5.3 字段加权与 IDF 问题

多字段加权要注意字段间 IDF 相互干扰:同一查询词在不同字段的 IDF 不同,加权会放大低频词的稀有度。用 field_value_factor 或自定义相似度可规避,必要时对字段统一 analyzer 减少干扰。

6. 自定义评分与脚本

一句话总结: script_score 用 Painless 直接算分,把业务因子与向量相似度等折进排序,是最灵活也最需要测试的打分方式。

6.1 script_score 基础

当内置函数不够时,用 script_score 对候选集逐个算分:

{
  "query": {
    "script_score": {
      "query": { "match": { "title": "无线降噪耳机" } },
      "script": {
        "source": "_score * (1.0 + params.weight * doc['sales'].value / doc['max_sales'].value)",
        "params": { "weight": 0.5 }
      }
    }
  }
}

6.2 组合向量相似度

script_score 也是向量精确检索的入口,把语义相似度与业务因子相乘,实现「语义 + 销量」综合排序:

{
  "query": {
    "script_score": {
      "query": { "bool": { "filter": { "term": { "category": "audio" } } } },
      "script": {
        "source": "(cosineSimilarity(params.qvec, 'title_vector') + 1.0) * (0.5 + 0.5 * doc['sales_score'].value)",
        "params": { "qvec": [0.1, -0.2, 0.3, 0.05] }
      }
    }
  }
}

6.3 脚本打分的坑

脚本对每个候选文档执行,候选集过大时性能陡降,务必先过滤再算分。脚本不可缓存、结果不稳定时难排查,正式上线前用 _explain 逐条核对;能用 function_score 表达的优先用内置函数。

7. 调优实践

一句话总结: 排序调优要建立评估集与回归基线,逐因子叠加、逐个验证,避免一次引入多个变量。

7.1 建立评估集

从线上日志抽样真实查询,人工标注理想排序前 5,作为回归基线。每次改动跑同一评估集对比 NDCG 与倒置率,改动可量化、可回滚。

7.2 分层调优顺序

先定字段加权(title/body 比例),再调 BM25 参数,最后叠加业务函数(时间、销量、VIP)。顺序颠倒容易互相掩盖;每层收敛后再进入下一层,最终组合在评估集上验证。

7.3 监控与灰度

排序改动用查询级别参数灰度,避免全量索引重建。线上对比点击率、加购率与搜索无结果率,异常时快速回退。打分公式版本号写进响应日志,便于问题溯源。

7.4 rescorer 二次打分

前 100 条用轻量查询粗排,再用昂贵打分(向量相似度、脚本公式)只对前 50 精排,控制成本的同时拿到高质量排序:

{
  "query": { "match": { "title": "无线降噪耳机" } },
  "rescore": {
    "window_size": 50,
    "query": {
      "rescore_query": {
        "script_score": {
          "query": { "match_all": {} },
          "script": { "source": "cosineSimilarity(params.qvec, 'title_vector') + 1.0", "params": { "qvec": [0.1, -0.2, 0.3] } }
        }
      }
    }
  }
}
手段作用适用场景
字段 boost字段权重差异标题 vs 正文
查询 boost条件加权品牌/类目加权
BM25 参数相关度宏观形态语料长短差异大
function_score时间/销量衰减新鲜度、热度
script_score任意公式混合业务因子

8. 总结

环节要点
打分原理TF×IDF×长度归一化,_explain 查明细
BM25k1 词频饱和、b 长度惩罚,标注集调优
boost字段/查询乘性加权,最常用干预
function_scoregauss/weight/脚本,boost_mode 控制合并
多字段best_fields/cross_fields 处理重复词
script_scorePainless 算分,先过滤再算分
调优实践评估集回归、分层叠加、灰度监控

排序是搜索引擎价值的最后一步。先吃透 BM25 的默认行为,再按「字段加权 → BM25 参数 → 业务函数」的顺序逐层叠加;每层改动都在固定评估集上量化验证,线上灰度并快速回滚。打分公式越简单越可控,能用内置函数就不要上脚本。相关性打分的上游原理见《Query DSL 与相关性打分》,向量融合排序见《向量检索与语义搜索》,字段设计参考《数据建模与 Mapping 设计》。

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「elasticsearch」更多文章

  1. 可搜索快照与冻结层:把冷数据放进对象存储还能查
  2. 分页与深度分页:from/size、search_after、PIT 与 scroll
  3. 嵌套与父子关联查询:nested、join 字段与性能取舍