数据库基础体系 · 第 126/139 篇。文章以各产品官方稳定版本的公开语义为准;示例会明确引擎、事务与部署边界。
Elasticsearch 相关性调优:BM25、Boost、Function Score 和评测集
在 Elasticsearch 中,“相关性”不是一个单独的开关,而是一条完整的排序链路:
- 查询文本经过分析器拆分为词项;
- 查询子句决定哪些文档可以进入候选集;
- 相似度模型计算文本匹配分数,默认通常是 BM25;
- 查询和字段上的 boost 改变分数权重;
function_score可以把业务信号加入原始文本分数;- 结果按
_score排序; - 通过评测集和离线指标判断排序是否真的改善。
如果只修改 boost,通常只能改变已有匹配结果的相对顺序;如果候选文档根本没有被查询召回,boost 和 function_score 都无法把它“变出来”。因此,相关性调优必须同时区分:
- 召回:相关文档是否进入候选集;
- 排序:相关文档是否排在不相关文档前面;
- 评测:修改是否在一组稳定查询上产生真实收益。
一、先明确 Elasticsearch 中的 _score
Elasticsearch 的全文查询通常会为每个匹配文档计算 _score。这个分数用于排序,但它不是概率,也没有跨查询直接比较的统计意义。
例如,查询“数据库事务”得到的最高分是 8.2,查询“Redis”得到的最高分是 2.1,不能据此判断前一个查询的结果“相关性是后一个的四倍”。不同查询包含的词项、词频、文档频率和查询结构不同,分数尺度也会变化。
下面几类查询的评分行为不同:
{
"query": {
"bool": {
"must": [
{
"match": {
"title": "Elasticsearch"
}
}
],
"filter": [
{
"term": {
"status": "published"
}
}
]
}
}
}
must中的查询负责匹配,并通常参与评分;filter只负责过滤,不计算相关性分数;- 如果把全文查询放入
filter,结果仍可能正确,但_score通常是0,不能再依赖文本相关性排序; bool中多个评分子句的分数会按照查询结构组合。
这也是一个常见错误:
{
"query": {
"bool": {
"filter": [
{
"match": {
"content": "事务隔离级别"
}
}
]
}
},
"sort": [
"_score"
]
}
这段查询可以过滤出包含相关词项的文档,但因为全文查询在 filter 上下文中不参与评分,按 _score 排序没有实际意义。若需要按文本相关性排序,应使用 must 或 should:
{
"query": {
"bool": {
"must": [
{
"match": {
"content": "事务隔离级别"
}
}
],
"filter": [
{
"term": {
"status": "published"
}
}
]
}
},
"sort": [
{
"_score": "desc"
}
]
}
二、BM25:默认文本相关性模型
2.1 BM25 解决什么问题
BM25 是一种基于词项统计的文本相关性模型。它主要回答:
查询词在某篇文档中出现得多不多?这个词在整个索引中稀不稀有?文档长度是否造成了偏差?
BM25 的核心因素有三个:
- 词项频率:查询词在当前文档中出现的次数;
- 逆文档频率:查询词在整个索引中是否常见;
- 文档长度归一化:较长文档天然更容易包含查询词,需要进行校正。
Elasticsearch 的默认文本相似度通常基于 Lucene 的 BM25 实现。具体分数会受到 Lucene 和 Elasticsearch 版本、字段类型、分析器以及查询结构影响,因此不能把公式当作所有复杂查询的逐字节实现,但它准确表达了 BM25 的基本机制。
2.2 BM25 公式
对于查询中的一个词项,BM25 的典型形式为:
其中:
- :查询中的第 个词项;
- :当前文档;
- :词项 在文档 中的词频;
- :文档长度,通常是该字段的 token 数量;
- :该字段在索引中的平均长度;
- :词频饱和参数;
- :文档长度归一化参数;
- :逆文档频率。
逆文档频率常写作:
其中:
- :参与统计的文档数;
- :包含词项 的文档数。
多个查询词项的分数通常相加:
实际 Elasticsearch 查询可能还会引入查询 boost、字段权重、布尔组合方式等额外因素。
2.3 词频为什么会“饱和”
BM25 不会让词项出现次数越多,分数无限线性增长。
取:
- 文档长度
- 平均长度
- 词频
长度归一化项为:
词频部分为:
如果词频从 2 增加到 20:
词频增加了十倍,但词频因子只从约 1.285 增加到约 2.054。这就是饱和效应:词出现几次通常很有帮助,但重复几十次不会被认为是几十倍相关。
这能抑制关键词堆砌,但不能完全防止垃圾文本。若业务允许用户输入大量重复词项,还应在分析、内容清洗和召回策略层面处理。
2.4 逆文档频率为什么重要
假设索引中有 1000 篇文档,查询词在 10 篇文档中出现:
如果另一个词出现在 900 篇文档中:
前一个词更能区分文档,因此贡献更大的分数。常见词并非没有价值,但它们对区分相关文档的帮助更小。
这也解释了一个反直觉现象:
文档中出现了更多查询词,不一定意味着它得分更高;词项是否稀有、出现位置所在字段以及文档长度同样重要。
2.5 文档长度归一化与长文档偏差
在其他条件相同的情况下,长文档更容易偶然包含查询词。BM25 使用 对此进行归一化:
- :不进行长度归一化;
- :完全按照文档长度进行归一化;
- 常见默认值约为
0.75,但这不是所有领域的最佳值。
例如:
title通常很短,长度差异小;content可能从几百字到几十万字,长度差异大;tags字段长度几乎固定,长度归一化的收益可能有限。
因此,不能只因为某个字段的默认 BM25 表现不好,就直接调整全局参数。更常见的原因可能是:
- 查询词被错误分析;
- 字段内容混入了不应参与检索的文本;
- 标题和正文没有分开建模;
- 业务要求精确匹配,但使用了宽松的全文查询;
- 文档长度包含了模板、导航、重复内容。
2.6 修改 BM25 参数
可以在创建索引时定义自定义相似度:
curl -X PUT 'http://localhost:9200/articles' \
-H 'Content-Type: application/json' \
-d '
{
"settings": {
"index": {
"similarity": {
"article_bm25": {
"type": "BM25",
"k1": 1.2,
"b": 0.5
}
}
}
},
"mappings": {
"properties": {
"title": {
"type": "text",
"similarity": "article_bm25"
},
"content": {
"type": "text",
"similarity": "article_bm25"
}
}
}
}
'
这里将 b 从常见默认值调低到 0.5,表示降低文档长度对分数的影响。
这不是“文档越长越好”,而是假设:
在这个业务中,较长的文章不应因为长度被过度惩罚。
是否成立必须通过评测集验证。一个适合新闻标题的参数,未必适合法律条款、代码搜索或商品描述。
修改相似度后,应明确验证:
- 新旧索引的映射和相似度配置;
- 同一查询下
_score和排序变化; - 评测集指标变化;
- 长文档和短文档的边界案例;
- 重建索引或重新写入后的线上结果。
不要只观察单条查询的分数。BM25 的参数改变通常会改变大量文档之间的相对顺序。
三、Boost:改变查询结构中的相对权重
3.1 Boost 的含义
Boost 是一个权重因子,用于提高或降低某个查询子句、字段或查询整体对最终分数的影响。
例如:
{
"query": {
"multi_match": {
"query": "Elasticsearch 相关性",
"fields": [
"title^3",
"summary^2",
"content"
]
}
}
}
这里:
title^3表示标题字段的权重为 3;summary^2表示摘要字段的权重为 2;content使用默认权重 1。
它表达的是业务假设:
在其他条件相近时,标题中的匹配比正文中的匹配更重要。
Boost 不是概率,也不是百分制。^3 不保证标题匹配分数一定是正文匹配的三倍,因为不同字段的词频、文档频率、长度和查询结构都可能不同。
3.2 Boost 只影响已匹配的文档
下面两个文档:
文档 A:title = "Elasticsearch 相关性调优"
文档 B:title = "Redis 缓存设计"
查询:
{
"query": {
"match": {
"title": {
"query": "Elasticsearch",
"boost": 5
}
}
}
}
文档 A 会获得较高分,文档 B 不会因为 boost 变成匹配文档。Boost 不能突破查询的匹配条件。
如果想让“标题匹配”成为强约束,可以使用 must;如果只是额外加分,可以使用 should:
{
"query": {
"bool": {
"must": [
{
"match": {
"content": "事务"
}
}
],
"should": [
{
"match": {
"title": {
"query": "事务",
"boost": 3
}
}
}
]
}
}
}
语义是:
- 正文必须匹配“事务”;
- 标题也匹配“事务”的文档获得额外分数;
- 标题不匹配的文档仍可能进入结果。
这与下面的查询不同:
{
"query": {
"bool": {
"must": [
{
"match": {
"title": {
"query": "事务",
"boost": 3
}
}
}
]
}
}
}
第二种写法要求标题必须匹配,boost 不再只是排序偏好,而同时参与候选集约束。
3.3 Boost 的组合方式
对于不同字段,multi_match 的类型会影响分数如何组合。常见类型包括:
best_fields:倾向于使用单个字段中得分最高的匹配;most_fields:多个字段的匹配分数可以累加;cross_fields:尝试把多个字段视作一个组合字段;phrase或phrase_prefix:改用短语匹配逻辑。
例如,文档标题中有“数据库”,正文中有“事务”,查询为“数据库事务”:
best_fields可能偏向某一个字段的最佳匹配;most_fields可能让多个字段共同贡献分数;cross_fields更适合姓名、地址等被拆分到多个字段但逻辑上属于同一字段的场景。
因此,调整 title^3 之前,必须先确认查询类型和字段组合方式。否则可能误以为 boost 没生效,实际上是分数组合规则与预期不同。
3.4 Boost 的常见失败表现
失败一:把 boost 当成绝对规则
"title^10"
并不意味着标题匹配必然压过正文匹配。若正文中的稀有词项具有很高 IDF,仍可能获得更高分。
若业务规则是“标题精确匹配必须排在所有正文匹配前”,单纯提高 BM25 boost 不一定足够。可以考虑:
- 分层
bool should; dis_max;- 使用
constant_score表示明确的规则分; - 在排序阶段使用显式的业务字段;
- 通过
function_score或二次排序表达硬规则。
失败二:在分词不符合预期时盲目加 boost
查询“PostgreSQL事务”,如果分析器把它拆成了意外的词项,给字段增加十倍 boost 只会放大错误匹配。应该先检查:
curl -X GET 'http://localhost:9200/articles/_analyze' \
-H 'Content-Type: application/json' \
-d '
{
"field": "content",
"text": "PostgreSQL事务"
}
'
预期结果应是实际使用的分析器输出的 token 列表。只有知道查询被拆成了什么,才能解释 BM25 和 boost 的结果。
失败三:用 boost 解决召回缺失
如果同义词、拼写变体、中文分词或字段映射导致文档没有匹配,boost 没有作用。应先通过 _analyze、_explain 和查询改写确认召回链路。
四、用 _explain 拆解一条具体分数
对于某个文档,可以使用:
curl -X GET 'http://localhost:9200/articles/_explain/1' \
-H 'Content-Type: application/json' \
-d '
{
"query": {
"multi_match": {
"query": "Elasticsearch 相关性",
"fields": [
"title^3",
"content"
]
}
}
}
'
响应中的 explanation 会以树形结构说明:
- 文档是否匹配;
- 哪个查询子句贡献了分数;
- 词项频率是多少;
- 文档频率是多少;
- 字段长度如何影响 BM25;
- boost 乘在了哪一层;
- 布尔查询如何组合子分数。
诊断时应从叶子节点向上阅读:
- 查询词是否真的被分析出来;
- 该词是否出现在目标字段;
term frequency是否符合预期;document frequency是否异常;- 字段 boost 是否应用;
- 父级布尔或
multi_match如何合并。
_explain 适合单文档、单查询的原因分析,不适合直接对生产流量中的所有结果启用。批量解释会增加 CPU 和响应开销。
五、Function Score:把业务信号加入文本相关性
5.1 为什么需要 Function Score
BM25 主要使用文本统计信息,但业务排序通常还需要:
- 文档新鲜度;
- 点击量或热度;
- 商品销量;
- 内容质量分;
- 用户或租户权限之外的业务偏好;
- 地理距离;
- 人工配置的权重。
function_score 允许先执行一个查询,再使用函数修改查询分数。
它的概念结构是:
原始查询分数 query_score
↓
若干评分函数 function_i
↓
score_mode 合并函数结果
↓
boost_mode 与原始查询分数组合
↓
最终 _score
这里必须区分两个参数:
score_mode:多个函数之间如何合并;boost_mode:函数结果与原始查询分数如何合并。
5.2 一个可执行示例
假设索引包含:
{
"title": "Elasticsearch 相关性调优",
"content": "介绍 BM25、Boost 和评测集",
"popularity": 128,
"published_at": "2025-01-10T00:00:00Z"
}
查询希望:
- 先根据标题和正文的文本相关性召回;
- 热度较高的文档获得一定加分;
- 最近发布的文档更有优势;
- 函数结果与 BM25 分数相乘;
- 避免函数分数无限放大。
{
"query": {
"function_score": {
"query": {
"multi_match": {
"query": "Elasticsearch 相关性",
"fields": [
"title^3",
"content"
]
}
},
"functions": [
{
"field_value_factor": {
"field": "popularity",
"modifier": "log1p",
"missing": 0
},
"weight": 0.2
},
{
"gauss": {
"published_at": {
"origin": "now",
"scale": "30d",
"offset": "7d",
"decay": 0.5
}
},
"weight": 1.0
}
],
"score_mode": "sum",
"boost_mode": "multiply",
"max_boost": 10
}
}
}
各部分含义如下。
文本查询
"multi_match": {
"query": "Elasticsearch 相关性",
"fields": ["title^3", "content"]
}
先由 BM25 计算文本分数。标题中的匹配相对正文更重要。
热度函数
"field_value_factor": {
"field": "popularity",
"modifier": "log1p",
"missing": 0
}
如果 popularity=128,log1p 会使用对数形式压缩数值差异,避免 100000 次点击的文档直接把文本相关性完全压倒。
missing: 0 用于处理字段缺失。若不处理缺失值,可能出现查询失败或分数行为与预期不同,具体应以所用版本的函数语义和字段映射为准。
新鲜度函数
gauss 根据日期距离产生衰减分数:
- 在
offset范围内基本不衰减; - 超出后按
scale衰减; decay: 0.5表示达到指定尺度时,函数值大约衰减到一半。
新鲜度并不是“越新越相关”的证明,而是业务偏好。对于永久有效的技术文档,过强的新鲜度权重可能导致旧但正确的文档被压下去。
函数之间的组合
"score_mode": "sum"
表示热度函数和新鲜度函数的结果先相加。
函数与文本分数的组合
"boost_mode": "multiply"
表示最终分数大致遵循:
随后受到 max_boost 的限制。
5.3 score_mode 与 boost_mode 的区别
假设:
原始文本分数 = 4
函数 A = 2
函数 B = 0.5
score_mode: sum
如果 boost_mode: multiply:
score_mode: max
再乘以文本分数:
boost_mode: sum
若函数合并结果为 2.5:
boost_mode: replace
最终分数只使用函数结果:
这会完全丢弃 BM25 分数,只有在业务规则明确要求如此时才适合使用。
5.4 使用过滤条件限制函数作用范围
不同文档可以使用不同评分函数:
{
"query": {
"function_score": {
"query": {
"match": {
"content": "Elasticsearch"
}
},
"functions": [
{
"filter": {
"term": {
"category": "official"
}
},
"weight": 1.5
},
{
"filter": {
"term": {
"category": "community"
}
},
"weight": 1.0
}
],
"score_mode": "sum",
"boost_mode": "multiply"
}
}
}
这里的 filter 只决定某个函数是否应用,不等同于外层查询的文档过滤条件。外层查询仍然决定文档是否进入候选集。
5.5 Function Score 的风险
业务分数没有归一化
若 popularity 取值从 0 到数百万,直接使用:
{
"field_value_factor": {
"field": "popularity"
}
}
可能让热度完全压过文本相关性。对数、平方根、截断、分桶或预计算归一化字段通常更可控,但具体选择必须结合数据分布和评测结果。
乘法放大极端值
boost_mode: multiply 会放大原始分数差异。一个文本分数很高、函数分数也很高的文档可能迅速领先;函数值接近零时,也可能把原本高度相关的文档压到很后面。
script_score 成本高且容易失控
自定义脚本可以表达更复杂的逻辑,但会带来:
- 更高的 CPU 消耗;
- 脚本执行次数随候选文档数增长;
- 字段缺失、类型转换和异常处理风险;
- 分片之间分数分布不一致时的调试困难。
脚本应尽量使用 doc values 可直接访问的字段,避免在脚本中进行昂贵计算或访问外部系统。不能把 script_score 当成普通业务代码执行环境。
random_score 不适合稳定相关性评测
随机排序可用于探索或打散,但会导致同一查询的结果不稳定。若要评测排序改动,必须固定随机种子,或在评测阶段不要使用随机函数。
六、相关性评测集:从“感觉变好了”变成可重复实验
6.1 评测集的组成
一个基本的相关性评测集至少包含:
查询 query
候选文档 document
人工相关性等级 rating
可以表示为:
{
"query_id": "q-001",
"query": "Elasticsearch 相关性调优",
"ratings": [
{
"doc_id": "doc-101",
"rating": 3
},
{
"doc_id": "doc-205",
"rating": 2
},
{
"doc_id": "doc-309",
"rating": 0
}
]
}
相关性等级应先定义清楚。例如:
0:不相关;1:部分相关,但不能解决问题;2:基本相关;3:高度相关,直接满足意图。
等级不是越多越好。若标注人员无法稳定区分 2 和 3,增加等级只会增加噪声。
6.2 标注边界必须先于模型调参
以下问题需要在标注规范中明确:
- 文档只要包含查询词,是否算相关?
- 内容正确但版本过旧,是否降低等级?
- 同一主题的概览文档和深入文档是否同等级?
- 查询有多个意图时,一个文档满足其中一个意图算什么?
- 结果重复、软删除或无权限文档如何处理?
- 新鲜度是相关性的一部分,还是单独业务排序因素?
如果标注标准不断变化,那么模型指标的变化无法解释。一次评测可能实际上比较的是两套标注规则,而不是两套排序方案。
6.3 常用离线指标
设查询结果前 个文档为:
Precision@k
前 个结果中相关文档的比例:
它适合衡量用户看到的前几条结果是否大多有用。
Recall@k
已知相关文档中,有多少出现在前 个结果:
Recall 依赖“全部相关文档”这一分母。如果评测集只标注了少量文档,Recall 可能被低估。
MRR
对于每个查询,只关注第一个相关结果的位置:
所有查询的平均值为 MRR。它适合问答、导航、单一最佳结果等场景,但不充分反映前十条中有多个高质量结果的情况。
DCG 与 NDCG
对于有等级相关性的结果,DCG 常写为:
其中 是第 个结果的相关性等级。位置越靠前,折损越小;相关性等级越高,收益越大。
NDCG 用理想排序的 DCG 进行归一化:
它适合比较不同查询之间的排序质量。
6.4 一个 NDCG@3 算例
假设某查询的理想相关性等级为:
[3, 2, 1]
某次排序得到:
[1, 3, 0]
使用:
则实际 DCG 为:
理想 DCG 为:
因此:
这个结果说明:虽然前 3 个结果中有高相关文档,但等级为 3 的文档排在第二位,排序仍有明显损失。
七、使用 Elasticsearch _rank_eval 进行离线评测
Elasticsearch 提供了 Rank Evaluation API,可对多个查询及其人工评级进行评估。
示例:
curl -X POST 'http://localhost:9200/articles/_rank_eval' \
-H 'Content-Type: application/json' \
-d '
{
"requests": [
{
"id": "q-001",
"request": {
"query": {
"multi_match": {
"query": "Elasticsearch 相关性",
"fields": [
"title^3",
"content"
]
}
}
},
"ratings": [
{
"_index": "articles",
"_id": "doc-101",
"rating": 3
},
{
"_index": "articles",
"_id": "doc-205",
"rating": 2
},
{
"_index": "articles",
"_id": "doc-309",
"rating": 0
}
]
}
],
"metric": {
"dcg": {
"k": 10,
"normalize": true
}
}
}
'
请求中的关键部分是:
requests:每个查询一个评测请求;request:实际执行的 Query DSL;ratings:已知文档及其人工相关性等级;metric:评测指标,这里使用归一化的DCG@10,即 NDCG 风格的评估。
评测返回通常包括:
- 总体指标;
- 每个查询的指标;
- 查询实际返回的文档;
- 评级文档是否被召回;
- 未评级结果等诊断信息。
评测 API 的请求结构和可用指标应以当前部署版本的官方文档为准。不同版本在指标参数名和细节上可能存在差异,升级后应重新验证评测脚本,而不要把评测请求当作永久不变的协议。
八、正确的调优实验顺序
一次可靠的实验应只改变一个主要变量。
8.1 固定索引和运行环境
比较两个查询版本时,至少固定:
- 相同的索引快照或相同数据版本;
- 相同的分片数量;
- 相同的分析器和映射;
- 相同的过滤条件;
- 相同的分页窗口;
- 相同的评测集;
- 相同的相关性等级定义。
如果一边重建了索引,一边修改了查询,就无法判断收益来自 BM25、数据变化还是倒排结构变化。
8.2 先检查召回,再调排序
对每个评测查询,记录:
已知相关文档总数
前 10、前 20、前 100 的召回数
第一个相关结果的位置
相关文档的排序位置
如果相关文档在前 100 名都没有出现,优先检查:
- 分词;
- 同义词;
minimum_should_match;- 字段是否选错;
must与filter是否使用错误;- 时间和权限过滤是否过严;
- 查询是否被
operator: and过度收紧。
不要先加 title^10。
8.3 再调整字段 boost
建议使用一个小范围实验,例如:
title^1, title^2, title^3, title^5
而不是直接尝试 title^100。观察:
- NDCG@10;
- MRR;
- 长文档与短文档的分布;
- 不同查询意图下的结果;
- 标题误匹配是否增加。
8.4 最后加入业务函数
Function Score 应在文本基线稳定后加入。实验至少应包含:
BM25 基线
BM25 + 字段 boost
BM25 + 热度
BM25 + 新鲜度
BM25 + 热度 + 新鲜度
否则多个因素同时变化,无法知道是哪一项造成回归或收益。
九、线上诊断:分数、查询和分页
9.1 不要把 _score 作为跨查询业务分值
同一查询内比较 _score 通常是有意义的;跨查询比较则通常没有意义。若需要跨查询统一排序,应使用显式业务分、归一化模型或后处理策略。
9.2 深分页会改变评测和线上成本
相关性排序通常需要协调各分片的候选结果。使用:
{
"from": 10000,
"size": 20
}
会增加深分页成本,并可能受到 index.max_result_window 限制。对于大规模遍历,应使用 search_after;如果需要稳定排序,通常需要 _score 加一个唯一且稳定的次排序字段:
{
"sort": [
{
"_score": "desc"
},
{
"_id": "asc"
}
],
"search_after": [
12.345,
"doc-101"
],
"size": 20
}
需要注意,_score 可能受到索引分片、刷新时机和查询执行环境影响。离线评测应尽量固定数据和排序条件,线上分页也应设计稳定的 tie-breaker。
9.3 使用 Profile 分析查询执行
profile 可以帮助观察查询各阶段的执行时间:
{
"profile": true,
"query": {
"function_score": {
"query": {
"match": {
"content": "BM25"
}
},
"functions": [
{
"field_value_factor": {
"field": "popularity",
"modifier": "log1p",
"missing": 0
}
}
]
}
}
}
它适合测试和诊断,不应默认用于所有生产请求,因为响应会变大并增加额外开销。Profile 主要告诉你“哪里耗时”,不直接告诉你“排序是否相关”。
十、与混合检索和 RAG 的关系
在混合检索中,BM25 往往承担词法召回,向量检索承担语义召回。两者的分数通常不在同一尺度上:
- BM25 分数取决于词频、文档频率和字段长度;
- 向量相似度取决于向量模型和相似度函数;
- 直接把两种原始分数相加,通常没有稳定统计依据。
常见流程是:
BM25 召回候选
向量召回候选
融合候选集
二次重排
截取 Top K
生成 RAG 上下文
无论使用何种融合方法,都应评测:
- 词法精确查询;
- 语义改写查询;
- 专有名词和数字;
- 长尾查询;
- 无答案查询;
- 文档版本和时效性;
- 引用文档是否真的支持生成结论。
对于 RAG,检索相关性不等于生成正确性。至少应分开评估:
- 相关文档是否被召回;
- 相关文档是否排在上下文窗口前部;
- 生成答案是否忠实于检索结果;
- 引用是否指向支持答案的具体内容。
BM25 boost 和 Function Score 可以改善候选排序,但不能替代向量召回、重排模型或引用验证。
十一、常见误解与对应诊断
误解一:分数高就代表业务上最相关
BM25 分数是模型内部排序信号,不是人工相关性标签。高分文档可能只是:
- 包含稀有词;
- 文本较短;
- 查询词重复较多;
- 命中了高 boost 字段;
- 获得了过强的业务函数加分。
应使用评测集验证,而不是只看 _score。
误解二:提高 boost 就能解决所有排序问题
Boost 只能改变匹配文档的相对权重。召回缺失、分析器错误、过滤过严的问题必须从查询和索引层修复。
误解三:热门文档一定应该排前
热度是业务偏好,不是文本相关性。若热门文档与查询意图无关,Function Score 可能造成明显回归。应使用候选门槛、函数上限、对数压缩或分层排序限制其影响。
误解四:离线指标提高就可以直接上线
离线评测集可能存在:
- 查询分布过窄;
- 标注样本不足;
- 热门查询过多;
- 评测文档泄漏;
- 与线上过滤条件不一致;
- 只评估 Top 10,却线上展示 Top 50。
因此应同时保留回归集和探索集,并在上线后观察点击、停留、改写、无结果率等线上信号。但点击数据本身也有位置偏差,不能不加校正地当作人工相关性。
十二、生产中的取舍
一个可解释的相关性系统,通常应遵循这样的分层:
索引与分析器:决定词项和基本召回能力
BM25:提供文本统计相关性
Boost:表达字段和查询结构的相对偏好
Function Score:加入有限、可解释的业务信号
评测集:验证修改是否改善目标查询
线上监控:验证离线收益是否在真实流量中成立
其中每层的责任不同:
- 分词错误,不应靠 boost 修复;
- 召回不足,不应靠 Function Score 修复;
- 业务热度过大,不应靠提高 BM25 参数解决;
- 单条查询看起来变好,不足以证明整体排序改善;
- 评测指标提高,也不代表所有查询类型都没有回归。
最终应把每次调优记录为可重放的实验:
数据版本
索引映射与相似度配置
查询 DSL
参数变化
评测集版本
指标变化
回归查询
线上灰度结果
这样,BM25 参数、字段 boost 和函数评分才不会变成只能凭经验反复试错的黑箱。
系列导航与关联阅读
- 系列入口:数据库完整学习路线:从关系模型、事务索引到分布式与向量检索
- 上一篇:Elasticsearch 中文检索:分词器、词典、同义词、拼音和版本治理
- 下一篇:Elasticsearch 深分页与一致性:search_after、PIT、Scroll 和导出
- 延伸:Elasticsearch 查询与聚合:Query DSL、相关性、分页和统计
- 延伸:混合检索与 RAG 数据层:全文、向量、融合、重排和引用
官方资料
本文依据数据库官方文档重新梳理;正文、示例与生产检查清单由 WR BLOG 编写。

评论
0 条讨论