Elasticsearch 相关度评分:从 BM25 到 Function Score 实战
Published in:2026-06-22 | category: 中间件

本文是 ES 存储引擎系列的进阶篇,深入剖析相关度评分:从 BM25 算法的 k1/b 参数,到分布式 IDF 问题,再到 Function Score 的各类函数(weight、field_value_factor、decay、random_score、script_score)实战。

第一部分:控制相关度(BM25)

评分框架演进

现代 ES 默认使用 BM25 概率模型替代了早期的 TF-IDF 向量空间模型:

旧”实用评分函数”组件在现代框架中的状态
布尔模型不变,仍是匹配的第一阶段
词频(TF)进化,从线性增长变为 BM25 的非线性饱和函数(由 k1 控制)
逆向文档频率(IDF)进化,使用更稳健的 BM25 IDF 公式(含平滑项)
字段长度归一化进化,变为 BM25 中由 b 参数控制的、基于平均长度的归一化
向量空间模型被 BM25 概率模型取代

查询上下文 vs 过滤上下文

  • 查询上下文:回答”这个文档和查询语句有多匹配?”,计算 _score。使用 matchterm(在 should 中)等。
  • 过滤上下文:回答”是否匹配?”,答案是 是/否。不计算 _score,但结果会被缓存,性能极高。

实际搜索通常是混合模型:用布尔逻辑(filter)快速精确过滤,用评分查询(mustshould)对结果集相关度排序。


BM25 饱和度参数 k1

k1 控制词频对相关度贡献的边际效益递减速度。传统 TF-IDF 中词频线性增长(出现 100 次得分就是 100),存在明显问题——“的”字出现 100 次并不代表相关 100 倍。

BM25 TF 公式(文档长度等于平均长度时的简化):

$$\text{TF}_{\text{BM25}} = \frac{f \cdot (k_1 + 1)}{f + k_1}$$

BM25 词频饱和效果(k1=1.2):

词频 fBM25 TF 值
11.000
51.548
101.710
501.902
→ 2.2(上限为 k1+1)

完整公式(含长度归一化):

$$\text{TF}_{\text{BM25}} = \frac{f \cdot (k_1 + 1)}{f + k_1 \cdot \left(1 - b + b \cdot \dfrac{|D|}{\text{avgdl}}\right)}$$

k1 取值场景参考:

k1 值场景特征适用案例
0.5 – 1.2(低)短文本、标题搜索,需快速饱和推文搜索、商品标题、日志错误匹配
1.2 – 1.8(中)通用网页搜索,安全默认值Google/Bing 式搜索、企业文档库
1.8 – 2.5+(高)长文档,一个相关词需出现多次学术论文全文、小说/剧本、技术手册

BM25 长度归一化参数 b

b 决定文档长度与平均长度的差异如何影响词频贡献。

  • b = 0:禁用长度归一化
  • b = 0.75(默认):中度惩罚长文档,奖励短文档
  • b = 1:强烈惩罚长文档

自定义 BM25 配置示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
PUT /my_index
{
"settings": {
"index": {
"similarity": {
"my_custom_bm25": { "type": "BM25", "b": 0.3, "k1": 1.6 }
}
}
},
"mappings": {
"properties": {
"title": { "type": "text", "similarity": "my_custom_bm25" }
}
}
}

逆向文档频率(IDF)

词在集合所有文档里出现的频率越高,权重越低

$$\text{IDF}(q_i) = \log\left(1 + \frac{N - n(q_i) + 0.5}{n(q_i) + 0.5}\right)$$

与经典 IDF 的关键区别: +1/+0.5 平滑防止极端值;永远为正;对高频词的惩罚更温和。


分布式 IDF 问题

在分布式集群中,数据被水平分割到多个分片,每个分片是独立的 Lucene 索引,分片之间不共享 IDF 统计信息

**默认 QUERY_THEN_FETCH**:各分片独立计算 IDF(各自为政),导致同一个词在不同分片中 IDF 不同:

1
2
3
分片1:idf₁ = log(1 + (3000-150+0.5)/(150+0.5)) ≈ 3.04
分片2:idf₂ = log(1 + (3500-200+0.5)/(200+0.5)) ≈ 2.88
分片3:idf₃ = log(1 + (2500-100+0.5)/(100+0.5)) ≈ 3.24

**精确方式 DFS_QUERY_THEN_FETCH**:先预查询收集全局统计,广播给所有分片统一计算 IDF。

1
GET /my_index/_search?search_type=dfs_query_then_fetch

⚠️ dfs_query_then_fetch 需要额外预查询阶段,代价较高,生产中谨慎使用。数据量大、分片多时,默认模式的近似误差通常可接受。


第二部分:Function Score

Function Score 是 ES 的”评分增强器”,允许在基础查询评分上通过一系列函数二次评分。

1
2
3
4
5
6
7
8
9
10
{
"function_score": {
"query": { "match": { "title": "咖啡" } },
"functions": [ ... ],
"score_mode": "sum", // 函数得分之间如何组合
"boost_mode": "multiply", // 函数分与基础分如何组合
"max_boost": 10,
"min_score": 2.0
}
}

函数组合方式:score_mode

示例:func1=1.5, func2=2.0, func3=0.5

score_mode计算结果适用场景
sum相加4.0多个独立因素叠加
multiply相乘1.5因素相互依赖
avg平均1.33平滑影响
first第一个非空1.5优先级明确
max取最大2.0取最优因素
min取最小0.5限流

与基础分组合:boost_mode

示例:_score=2.0functions_score=4.0

boost_mode计算结果效果
multiply_score × func8.0基础相关性 × 业务规则(最常用)
replacefunc4.0完全用函数分替代
sum相加6.0相关性 + 业务规则
avg平均3.0各占一半

Weight:最简单的加权

1
{ "weight": 2.0, "filter": { "term": { "featured": true } } }

⚠️ weight 不是乘数,是固定值!匹配时函数得分为 2,不是乘以 2 倍;不匹配时为 1.0。

Field Value Factor:字段值参与评分

1
2
3
4
5
6
7
8
{
"field_value_factor": {
"field": "popularity",
"factor": 1.2,
"modifier": "log1p", // 推荐:平滑且正值
"missing": 1
}
}

modifier 函数对比:

modifier公式特点
nonefactor × field线性,易导致分数爆炸
log1plog(1 + factor × field)推荐!平滑且正值
square(factor × field)²放大差异(如 4-5 星区间)
sqrt√(factor × field)缩小差异
reciprocal1 / (factor × field)反转关系(如越便宜越靠前)

Decay 衰减函数:时间、距离、数值的平滑衰减

函数曲线特点最适用场景
gauss(高斯)平滑柔和,钟形曲线地理距离、评分衰减
exp(指数)一开始掉得快,越往后越平新闻时效性、热点快速衰退
linear(线性)匀速直线下降价格偏好、简单规则

高斯衰减示例(以上海为原点的地理距离):

1
2
3
4
5
6
7
8
9
10
{
"gauss": {
"location": {
"origin": { "lat": 31.2304, "lon": 121.4737 },
"scale": "10km",
"offset": "1km",
"decay": 0.3
}
}
}

衰减三阶段:offset 内(≤1km)几乎不衰减 → offset ~ offset+scale(1~11km)平滑下降至 decay(0.3)→ 超过后继续趋近 0 但越来越平缓。


Random Score:引入随机性

1
{ "random_score": { "seed": "用户ID", "field": "_seq_no" } }

相同种子 + 相同字段 → 相同随机分数(可复现,保证分页连贯)。

适用场景:商品推荐(避免每次展示同样商品)、内容发现、打散同分文档、探索与利用。


Script Score:完全自定义评分

当内置函数无法满足需求时的终极方案,可访问 doc['field'].value_scoreparams.*

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"script_score": {
"script": {
"source": """
double price = doc['price'].value;
double rating = doc['rating'].value;
double priceFactor = 1.0 / (1.0 + price * 0.001);
double ratingFactor = rating / 5.0;
return _score * (priceFactor * params.price_weight
+ ratingFactor * params.rating_weight);
""",
"params": { "price_weight": 0.6, "rating_weight": 0.4 },
"lang": "painless"
}
}
}

总结

层次工具核心作用
基础相关性BM25(k1/b词频饱和 + 长度归一化,替代 TF-IDF
分布式准确性dfs_query_then_fetch统一全局 IDF,代价较高
业务加权weight / field_value_factor分类加权、热度/价格/评分参与排序
平滑衰减gauss / exp / linear时间、距离、数值的自然衰减
多样性random_score打散、探索发现
终极定制script_score完全自定义评分逻辑

相关度调优的核心:先用 BM25 保证基础相关性,再用 Function Score 叠加业务规则,boost_mode: multiply 是最常用的组合方式。

Next:
Elasticsearch 深度分页:PIT(Point-In-Time)时间窗口详解