本文是 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。使用match、term(在should中)等。 - 过滤上下文:回答”是否匹配?”,答案是 是/否。不计算
_score,但结果会被缓存,性能极高。
实际搜索通常是混合模型:用布尔逻辑(
filter)快速精确过滤,用评分查询(must、should)对结果集相关度排序。
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):
| 词频 f | BM25 TF 值 |
|---|---|
| 1 | 1.000 |
| 5 | 1.548 |
| 10 | 1.710 |
| 50 | 1.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 | PUT /my_index |
逆向文档频率(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 | 分片1:idf₁ = log(1 + (3000-150+0.5)/(150+0.5)) ≈ 3.04 |
**精确方式 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 | { |
函数组合方式: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.0,functions_score=4.0
| boost_mode | 计算 | 结果 | 效果 |
|---|---|---|---|
multiply | _score × func | 8.0 | 基础相关性 × 业务规则(最常用) |
replace | func | 4.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 | { |
modifier 函数对比:
| modifier | 公式 | 特点 |
|---|---|---|
none | factor × field | 线性,易导致分数爆炸 |
log1p | log(1 + factor × field) | 推荐!平滑且正值 |
square | (factor × field)² | 放大差异(如 4-5 星区间) |
sqrt | √(factor × field) | 缩小差异 |
reciprocal | 1 / (factor × field) | 反转关系(如越便宜越靠前) |
Decay 衰减函数:时间、距离、数值的平滑衰减
| 函数 | 曲线特点 | 最适用场景 |
|---|---|---|
gauss(高斯) | 平滑柔和,钟形曲线 | 地理距离、评分衰减 |
exp(指数) | 一开始掉得快,越往后越平 | 新闻时效性、热点快速衰退 |
linear(线性) | 匀速直线下降 | 价格偏好、简单规则 |
高斯衰减示例(以上海为原点的地理距离):
1 | { |
衰减三阶段: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、_score、params.*:
1 | { |
总结
| 层次 | 工具 | 核心作用 |
|---|---|---|
| 基础相关性 | 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是最常用的组合方式。

