最近在优化一个全文检索业务时,遇到了一个典型的排序场景:用户要求搜索结果首先按相关性排序,相关性相同时按创建时间倒序。听起来很简单对吧?但当我真正上手写 DSL 时,才发现 must、filter、should 以及 sort 之间的微妙关系远比想象中复杂。
研究发现了 term 查询在 must 与 filter 中的评分差异、terms 为何默认得分恒定、以及如何利用 bool + should + sort 实现“硬过滤 + 软评分”的混合排序。
问题背景
业务场景是文章搜索,核心需求如下:
- 硬性筛选:只返回状态为
published、且创建时间在某个时间点之后的文章。 - 相关性排序:根据用户输入的关键词(如标签、作者)计算匹配度,分数越高越靠前。
- 时间兜底:当两篇文章的相关性分数完全一致时,最新的文章排在前面。
直觉上,这个需求可以拆解为 filter 做硬过滤,should 做加分项,最后 sort 按 _score 和 createTime 排序。但在实际测试中,发现 _score 的权重似乎“不听话”,甚至有时候加了 sort 后,_score 直接“失效”了。
核心机制梳理:评分与排序的底层逻辑
在解决问题之前,有必要先理清 Elasticsearch 的几个基础机制。
1. must 与 filter:评分与不评分的分水岭
must:必须匹配,且参与评分。每个must子句都会基于 BM25 计算文档的相关性得分,所有得分相加得到最终_score。filter:必须匹配,但不参与评分。它只做二值判断(匹配/不匹配),结果会被缓存,性能极高。
所以,如果需要用 term 做精确匹配且不关心它对排序的影响,优先用 filter。
2. term vs terms:评分策略完全不同
term:计算 BM25 相关性分数。词频(TF)、逆文档频率(IDF)、字段长度都会影响得分。terms:默认采用 常量分数(Constant Score),所有匹配文档得分均为 1.0。这是为了性能考虑,因为terms通常用于筛选(如status in ["active", "pending"]),而非排序。
如果想让 terms 像 term 一样计算分数,需要设置 "rewrite": "scoring_boolean",将其改写为 bool + should 的形式。
3. sort 的“覆盖”效应
一旦在查询中显式指定 sort 字段,排序将完全由该字段决定,_score 只被计算但被忽略。默认情况下,_score 甚至不会返回(显示为 null 或 0)。
如果需要在排序后仍然看到 _score,可以加 "track_scores": true,但这只影响返回值,不影响排序。
最终解决方案:filter + should + 二级排序
经过多次尝试,最终采用了以下结构,完美解决了需求:
{
"query": {
"bool": {
"filter": [
{ "term": { "status": "published" } },
{ "range": { "createTime": { "gte": "2025-01-01" } } },
{ "terms": { "tags": ["elasticsearch", "search"] } },
{ "term": { "author": "john" } }
],
"should": [
{ "term": { "tags": "elasticsearch" } },
{ "term": { "tags": "search" } },
{ "term": { "author": "john" } }
],
}
},
"sort": [
{ "_score": { "order": "desc" } },
{ "createTime": { "order": "desc" } }
]
}
执行逻辑拆解
filter:先过滤出符合条件的文档,这部分结果会被缓存,不参与评分。should:在过滤后的结果集上,计算每个文档匹配了多少个should条件,匹配越多,_score越高。sort:- 第一优先级:
_score降序,相关性高的排前面。 - 第二优先级:
createTime降序,分数相同时新文章优先。
- 第一优先级:
不同场景下的排序表现
匹配 should 数量 |
_score 范围 |
createTime |
最终排名 |
|---|---|---|---|
| 3 个 | 最高 | 任意 | 第 1 名 |
| 2 个 | 中等 | 最新 | 第 2 名 |
| 1 个 | 较低 | 最新 | 第 3 名(分数优先于时间) |
| 0 个 | 0 | 最新 | 最后(分数为 0,按时间倒序) |
这个逻辑很好地满足了“相关性优先,时间辅助”的需求。
性能与最佳实践
- 能用
filter就别用must:对于状态、分类、ID 范围等硬性条件,用filter更高效,且不影响相关性。 terms默认不评分:如果希望terms参与评分,记得加rewrite参数,但要评估性能开销。sort会覆盖_score排序:如果希望混合排序,要么用二级排序,要么用function_score,不要试图在sort里同时兼顾。- 调试时善用
explain:加"explain": true可以详细查看每个文档的分数计算过程,是排查评分问题的利器。
总结
这次排查的核心收获是:Elasticsearch 的排序和评分是两个独立的阶段。查询阶段计算出 _score,排序阶段决定最终顺序。理解 must / filter / should / sort 各自的职责边界,才能在复杂业务中写出既准确又高性能的查询。
最终采用的 filter + should + 二级排序方案,既保证了过滤条件的缓存效率,又实现了相关性优先的排序需求,同时兼顾了时间兜底。如果未来业务需要更复杂的“时间权重”融合,function_score 会是更灵活的选择。