搜索引擎的质量最终由排序决定,而排序来自打分。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 查明细 |
| BM25 | k1 词频饱和、b 长度惩罚,标注集调优 |
| boost | 字段/查询乘性加权,最常用干预 |
| function_score | gauss/weight/脚本,boost_mode 控制合并 |
| 多字段 | best_fields/cross_fields 处理重复词 |
| script_score | Painless 算分,先过滤再算分 |
| 调优实践 | 评估集回归、分层叠加、灰度监控 |
排序是搜索引擎价值的最后一步。先吃透 BM25 的默认行为,再按「字段加权 → BM25 参数 → 业务函数」的顺序逐层叠加;每层改动都在固定评估集上量化验证,线上灰度并快速回滚。打分公式越简单越可控,能用内置函数就不要上脚本。相关性打分的上游原理见《Query DSL 与相关性打分》,向量融合排序见《向量检索与语义搜索》,字段设计参考《数据建模与 Mapping 设计》。
延伸阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。