Query DSL 是 Elasticsearch 的检索语言。写对了查询语法只解决能否搜到,而结果按什么顺序排列取决于相关性打分。本文覆盖叶子查询、复合查询、BM25 算法与 function_score,帮助你写出召回准确、排序合理、性能可控的检索语句。
1. Query DSL 基础
1.1 叶子查询与复合查询
Query DSL 分为两类:叶子查询直接匹配具体字段,如 term、match、range、exists;复合查询包装多个子查询,如 bool、dis_max、boosting。
| 查询类型 | 名称 | 作用 | 打分 |
|---|---|---|---|
| 叶子 | term | 精确词项匹配 | 固定分 |
| 叶子 | match | 全文分词匹配 | BM25 |
| 叶子 | range | 数值/日期范围 | 固定分 |
| 叶子 | exists | 字段存在性 | 固定分 |
| 复合 | bool | 组合 must/should/filter | 子查询求和 |
| 复合 | dis_max | 取最高分子查询 | 最大值 |
1.2 match_all 与 range
{ "query": { "match_all": {} } }
match_all 给所有文档固定分数 1.0,常用于导出全量数据。不带 query 的 _search 默认也是 match_all,会造成全分片扫描,大数据集应避免。range 在数值与日期字段上走 BKD 树快速定位,配合 exists 实现字段非空过滤,等价于 SQL 的 IS NOT NULL。
{
"query": {
"bool": {
"filter": [
{ "range": { "price": { "gte": 100, "lte": 1000 } } },
{ "exists": { "field": "stock" } }
]
}
}
}
2. match 查询与全文检索
2.1 match 的分词与 minimum_should_match
match 默认把查询串分词后按 OR 合并,任一词命中即返回,分数为各词之和。用 operator 为 and 或 minimum_should_match 可控制最少命中词数。
| 查询串 | minimum_should_match | 行为 |
|---|---|---|
| elasticsearch tutorial guide | 1 | 至少命中任意 1 词 |
| elasticsearch tutorial guide | 2 | 至少命中任意 2 词 |
| elasticsearch tutorial guide | 75% | 至少命中 3 × 75% = 2 词 |
| elasticsearch tutorial guide | -1 | 允许最多 1 个词不命中 |
{
"query": {
"match": {
"content": {
"query": "分布式搜索引擎架构设计",
"minimum_should_match": "75%"
}
}
}
}
百分比按向下取整计算,是控召回精度的利器;但百分比与词数耦合,词数变化时行为不稳定,线上建议用绝对数字或压测确定的百分比。
2.2 match_phrase 与 slop
{
"query": {
"match_phrase": {
"title": {
"query": "搜索引擎 架构",
"slop": 2
}
}
}
}
match_phrase 要求词按顺序出现,slop 允许中间插入其他词的数量。短语查询依赖倒排索引中的 Position 信息,代价略高于普通 match。
2.3 multi_match 跨字段检索
{
"query": {
"multi_match": {
"query": "Elasticsearch",
"fields": ["title", "content", "tags"],
"type": "best_fields"
}
}
}
multi_match 把同一查询分发到多个字段,best_fields 取最高分、most_fields 求和。常见坑是字段长度不同导致分数失衡,正文很长的文档词频被稀释,可用 field_value_factor 或提升权重字段修正。
3. bool 复合查询
3.1 must should must_not filter
bool 查询是 DSL 的核心组合器,四个子句各司其职。
{
"query": {
"bool": {
"must": [ { "match": { "title": "搜索引擎" } } ],
"should": [ { "match": { "tags": "分布式" } },
{ "match": { "tags": "高可用" } } ],
"must_not": [ { "term": { "status": "deleted" } } ],
"filter": [ { "range": { "publish_date": { "gte": "2025-01-01" } } } ]
}
}
}
| 子句 | 语义 | 参与打分 | 结果集缓存 |
|---|---|---|---|
| must | 必须命中 | 是 | 否 |
| should | 加分条件 | 是 | 否 |
| must_not | 必须排除 | 否 | 否 |
| filter | 必须命中 | 否 | 是 |
filter 子句不参与打分,其命中集合被缓存为位图,重复使用同一 filter 几乎零开销。纯过滤条件应放 filter 而非 must,这是查询性能优化最廉价的一步。
3.2 should 与 minimum_should_match
只有 should 没有 must/filter 时默认至少命中一个 should;当 must 存在时 should 变纯加分项,需显式设置 minimum_should_match 才强制生效。
{
"query": {
"bool": {
"must": [ { "match": { "title": "数据库" } } ],
"should": [
{ "match": { "tags": "MySQL" } },
{ "match": { "tags": "PostgreSQL" } },
{ "match": { "tags": "TiDB" } }
],
"minimum_should_match": 2
}
}
}
该查询要求标题含数据库且标签至少命中三个中的两个,适合类目商品的属性组合筛选。
3.3 boosting 负向加权
{
"query": {
"boosting": {
"positive": { "match": { "title": "搜索引擎" } },
"negative": { "match": { "content": "营销" } },
"negative_boost": 0.3
}
}
}
boosting 保留正查询命中文档,对命中负查询的乘以 negative_boost 压低分数,用于弱化而非完全排除某类结果。
4. BM25 相关性算法
4.1 BM25 公式
Lucene 旧版默认 TF-IDF,ES 5 之后切换到 BM25。BM25 对词频做饱和处理,词频再高对分数的贡献也趋于平稳,避免重复词无限放大分数。
score(d, q) = Σ IDF(qi) × (f(qi, d) × (k1 + 1))
/ (f(qi, d) + k1 × (1 - b + b × |d| / avgdl))
k1 控制词频饱和速度,默认 1.2
b 控制文档长度归一化强度,默认 0.75
f 词在文档中的频率
avgdl 字段平均长度
4.2 参数调节与高频词
{
"settings": {
"index": {
"similarity": {
"default": { "type": "BM25", "k1": 1.4, "b": 0.9 }
}
}
}
}
k1 越大词频影响越大,适合型号类重复词有意义的场景;b 越大长度归一化越强,适合短文本优先。一般不需大改默认值,先用 explain 分析打分再微调。BM25 的 IDF 会让高频词(的、了、the)分数趋近零甚至为负,因此中文检索要过滤停用词,详见《倒排索引与分词原理》。
5. function_score 自定义打分
5.1 field_value_factor 字段加权
{
"query": {
"function_score": {
"query": { "match": { "title": "手机" } },
"field_value_factor": {
"field": "sales",
"factor": 1.0,
"modifier": "log1p"
},
"boost_mode": "multiply"
}
}
}
field_value_factor 把字段值(销量、评分、点击量)变换后乘入分数,modifier 可选 log1p、sqrt、ln 等防数值悬殊导致分数爆炸。
5.2 gauss 时间衰减与 script_score
{
"query": {
"function_score": {
"query": { "match": { "category": "electronics" } },
"functions": [
{
"gauss": {
"publish_date": {
"origin": "2026-09-30",
"scale": "30d",
"decay": 0.5
}
}
}
],
"boost_mode": "multiply"
}
}
}
gauss 对日期字段做衰减,距离 origin 越远分数越低,适合新闻流、招聘等需要新鲜度加权的场景。script_score 用 Painless 完全接管打分,灵活但每次匹配都执行,性能敏感,应优先用 field_value_factor 或预计算字段。
| 场景 | 推荐函数 | 原因 |
|---|---|---|
| 按销量/热度加权 | field_value_factor | 简单可缓存 |
| 按时间新鲜度 | gauss | 平滑衰减 |
| 按距离排序 | decay(geo) | 地理衰减 |
| 复杂业务规则 | script_score | 完全可控但慢 |
| 类目固定加权 | boost | 常数乘子 |
6. 排序与分页
6.1 sort 与 search_after
{
"query": { "match": { "title": "搜索引擎" } },
"sort": [
{ "publish_date": "desc" },
{ "id": "asc" }
],
"search_after": [1287321],
"size": 10
}
默认按 _score 降序;指定 sort 字段后需显式加入 _score 才参与排序。sort 依赖 Doc Values,未启用的 text 字段无法排序,必须用 keyword 子字段。search_after 用上一页最后一条排序值作游标,翻页越深性能越稳,是深分页的标准方案。
6.2 分页方式对比
| 方式 | 翻页性能 | 一致性 | 适用场景 |
|---|---|---|---|
| from + size | 深翻页退化 | 无保证 | 前 100 条 |
| search_after | 稳定 | 追加式 | 实时流式加载 |
| scroll | 稳定 | 快照 | 全量导出 |
| PIT | 稳定 | 快照 | 复合场景 |
scroll 在快照视图上滚动,适合全量导出但维护上下文内存;PIT(Point In Time)是 ES 7.10 引入的轻量级替代,与 search_after 配合做增量同步。
7. 高亮与 explain 调试
7.1 highlight 高亮
{
"query": { "match": { "content": "分布式" } },
"highlight": {
"fields": { "content": { "fragment_size": 150, "number_of_fragments": 2 } },
"pre_tags": ["<em>"],
"post_tags": ["</em>"]
}
}
高亮基于分词后的词项定位原文位置,_source 片段被拆成 fragments。中文高亮注意 fragment_size 按字符而非词,过小会切碎语义。
7.2 explain 与 profile
curl -s 'http://localhost:9200/article/_explain/42?pretty' \
-H 'Content-Type: application/json' \
-d '{"query": {"match": {"title": "搜索引擎"}}}'
_explain 返回某文档的逐项打分明细,包括 IDF、词频、长度归一化,是排查排序异常的必备工具。profile 则返回每个查询阶段(TermQuery、PhraseQuery、Collector)的耗时占比,定位查询慢在解压 Posting List、collect 还是 fetch,与 explain 搭配分别解决排序正确性与查询性能。
8. 总结
| 环节 | 要点 |
|---|---|
| 查询分类 | 叶子查询匹配字段,复合查询组合子句 |
| match 语义 | 先分词后合并,operator 与 minimum_should_match 控召回 |
| bool 结构 | must/should/filter 各司其职,filter 走位图缓存 |
| BM25 | k1 控词频饱和,b 控长度归一化 |
| 自定义打分 | field_value_factor、gauss、script_score 按场景选择 |
| 深分页 | search_after 游标优于 from,scroll 适合离线导出 |
| 调试工具 | explain 看打分明细,profile 看耗时分布 |
| 性能红线 | 避免深 from 分页、避免 script 打全量 |
Query DSL 决定检索能力,相关性打分决定检索质量。写查询前先想清楚业务排序目标,把过滤条件放进 filter,用 explain 验证打分,再决定是否引入 function_score。与倒排索引底层的结合可阅读《倒排索引与分词原理》,索引结构与字段设计可阅读《数据建模与 Mapping 设计》。
延伸阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。