大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
Query DSL:叶子查询与 bool 复合查询
这一课你将能做到
- 根据字段类型选对叶子查询:term/terms 精确查、match 全文查、range 范围查、exists 是否存在
- 用 bool 查询把条件组合起来,说清 must、should、must_not、filter 四者在「是否参与打分」上的区别
- 写出本站题库最常用的组合查询:关键词搜题干解析 + 课程/难度/标签过滤,并解释为什么过滤条件要放 filter
为什么要学
DSL(Domain Specific Language)是 ES 查询的 JSON 本体,所有高级功能(排序、高亮、聚合、向量)都挂在它的骨架上。初学者最典型的两个错误:① 对 keyword 字段用 match、对 text 字段用 term,导致结果「莫名其妙为空」;② 所有条件都塞 must,过滤和匹配不分,既浪费打分开销,又算不准相关性。
人话:叶子查询是「一个条件」,复合查询是「把条件用逻辑关系拼起来」。term 查的是倒排索引里的精确词项,match 查的是「先把你输入的文字分词,再去查词项并算分」。理解「词项」这个概念,90% 的查询问题都能自己解释。
先建立图像
叶子查询选型表:
| 查询 | 作用 | 是否分词 | 是否打分 | 适用字段类型 |
|---|---|---|---|---|
| term | 词项精确相等 | 否 | 是(放 filter 则否) | keyword、数值、布尔 |
| terms | 一个字段匹配多个值(IN) | 否 | 是 | keyword、数值 |
| match | 全文匹配 | 是 | 是 | text |
| match_phrase | 短语匹配(词序相邻) | 是 | 是 | text |
| multi_match | 同一关键词查多个字段 | 是 | 是 | 多个 text |
| range | 范围(gte/lte) | 否 | 是 | 数值、date |
| exists | 字段有值 | — | 是 | 任意 |
bool 的四个子句:
must 必须满足,且参与打分(逻辑 AND)
should 满足更好,影响打分;默认不强制满足(逻辑 OR 的软约束)
must_not 必须不满足,不打分;会自动走 filter 上下文
filter 必须满足,但不打分;可被缓存 —— 过滤条件一律放这里
跟着做
步骤 1:term 与 match 的对照实验
# term 查 keyword 字段:精确命中
curl -u elastic:abcd1234 -X GET "http://localhost:9200/questions/_search" -H "Content-Type: application/json" -d'
{
"query": { "term": { "tags": "spring" } }
}'
# match 查 text 字段:输入会被分词
curl -u elastic:abcd1234 -X GET "http://localhost:9200/questions/_search" -H "Content-Type: application/json" -d'
{
"query": { "match": { "stem": "事务失效" } }
}'
# 反例:term 查 text 字段,几乎必然查不到
curl -u elastic:abcd1234 -X GET "http://localhost:9200/questions/_search" -H "Content-Type: application/json" -d'
{
"query": { "term": { "stem": "Spring 事务在什么情况下会失效" } }
}'
最后一条为空,因为 text 字段的倒排索引里根本不存在这个完整句子的词项,只有 spring、事务、失效 这些分词。这就是「term 不打 text」的由来。
步骤 2:terms、range、exists
curl -u elastic:abcd1234 -X GET "http://localhost:9200/questions/_search" -H "Content-Type: application/json" -d'
{
"query": {
"terms": { "tags": ["redis", "mysql"] }
}
}'
# 难度在 3 到 5 之间、且近 30 天更新过
curl -u elastic:abcd1234 -X GET "http://localhost:9200/questions/_search" -H "Content-Type: application/json" -d'
{
"query": {
"range": {
"difficulty": { "gte": 3, "lte": 5 }
}
}
}'
# 有解析字段的题目(非空)
curl -u elastic:abcd1234 -X GET "http://localhost:9200/questions/_search" -H "Content-Type: application/json" -d'
{
"query": { "exists": { "field": "analysis" } }
}'
注意 exists 判断的是「字段有索引值」,空字符串 "" 也算存在,null 或字段缺失算不存在。
步骤 3:match_phrase 与 multi_match
# 短语:要求「事务」「失效」两词按顺序相邻,命中更精确
curl -u elastic:abcd1234 -X GET "http://localhost:9200/questions/_search" -H "Content-Type: application/json" -d'
{
"query": {
"match_phrase": { "stem": "事务 失效" }
}
}'
# 同一关键词同时搜题干、解析、课时标题(搜题+搜课时合一的基础)
curl -u elastic:abcd1234 -X GET "http://localhost:9200/questions/_search" -H "Content-Type: application/json" -d'
{
"query": {
"multi_match": {
"query": "倒排索引",
"fields": ["stem^3", "analysis", "lessonTitle^2"]
}
}
}'
stem^3 是字段权重:题干命中的得分乘 3,因为题干命中比解析命中更相关。这是调搜索质量最便宜的旋钮。
步骤 4:bool 组装本站真实查询
需求:关键词搜题干/解析;限定在 Java 课程;难度 3 以上;排除已下架;标签最好命中「面试」。
curl -u elastic:abcd1234 -X GET "http://localhost:9200/questions/_search" -H "Content-Type: application/json" -d'
{
"query": {
"bool": {
"must": [
{
"multi_match": {
"query": "Spring 事务",
"fields": ["stem^3", "analysis"]
}
}
],
"filter": [
{ "term": { "courseId": "C_JAVA" } },
{ "range": { "difficulty": { "gte": 3 } } },
{ "term": { "published": true } }
],
"must_not": [
{ "term": { "tags": "归档" } }
],
"should": [
{ "term": { "tags": "面试" } }
]
}
}
}'
步骤 5:should 的强制规则与 nested 查询
单独一个 bool 里只有 should 时,默认至少满足 1 条(minimum_should_match 在无 must/filter 时隐式为 1);一旦有了 must,should 默认一条都不满足也能命中,只加分。需要强制数量就显式写:
"minimum_should_match": 1
第 4 课建的 nested 评论字段,查询必须这样套一层:
curl -u elastic:abcd1234 -X GET "http://localhost:9200/questions/_search" -H "Content-Type: application/json" -d'
{
"query": {
"nested": {
"path": "comments",
"query": {
"bool": {
"must": [
{ "term": { "comments.user": "张三" } },
{ "match": { "comments.content": "没看懂" } }
]
}
},
"inner_hits": {}
}
}
}'
inner_hits 让响应里带出具体匹配的是哪条评论,否则只知道「这道题的评论里有匹配」。
讲清楚要点
- 查询上下文 vs 过滤上下文:query context 回答「有多匹配」,计算
_score;filter context 回答「是否满足」,只返回是非,结果可被缓存。范围、状态、归属类条件放 filter,既省 CPU 又能复用缓存;相关性条件放 must。 - must_not 不打分且可缓存:它内部自动以 filter 方式执行,别担心 must_not 的性能。大量「不等于」条件也安全。
- 打分只决定排序,不决定能不能命中:filter 命中的文档
_score为 0;如果全查询只有 filter,所有命中文档分数都是 0,排序退化为无意义顺序——这时你需要下一课的 sort 或给关键词条件。 - match 的默认算子是 OR:搜「事务 失效」拆成两词,默认命中任一即返回(两词都中的得分更高排前面)。要求两词都中可设
"operator": "and",或用 minimum_should_match 调松紧。 - 组合可以嵌套:bool 里可以再放 bool,构建任意复杂逻辑。实践建议保持结构扁平、条件命名清晰,嵌套过深只是给后人(和自己)制造阅读障碍。
你可能会懵的地方
-
我 term 查一个明确存在的值,为什么 0 条? 按这个顺序排查:字段是不是 text(term 要查分词后的词项,应用 match 或查
.keyword);大小写是否一致(term 大小写敏感,Spring≠spring,标准分词器会小写化,但 keyword 不会);值里有没有多余空格。 -
must 和 filter 写同样的条件,结果一样吗? 命中文档集合完全一样,差别只在 must 计算相关性得分、filter 不算且可缓存。只做过滤时用 must 是「花了算分的钱,没改变结果」。
-
should 写了却像没生效? 有 must/filter 时 should 默认不要求命中,只是加分项;如果你发现「should 一条没中也返回」,那是正常行为。要强制至少中一条,设 minimum_should_match。
-
match_phrase 和 match 加 operator:and 有啥不同? 后者只要求词都出现,不要求相邻和顺序(「失效的事务」也中);phrase 还要求词序和位置相邻,更严格。可用
slop允许中间间隔若干词。 -
ES 怎么做「不等于」? bool.must_not 里放 term。要注意它同时会排除「字段不存在」的文档吗?不会——must_not 只排除「字段存在且等于该值」的,字段缺失的文档不受影响。缺失也要排除时再加一句 exists 的 must_not。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
| term 查 text 长句 | 词项不存在,0 命中 | match,或查 .keyword 精确值 |
| match 查 keyword 字段 | 输入若含空格被当整串处理,易漏 | 精确过滤用 term |
| 过滤条件全放 must | 白付算分开销,无法缓存 | 放 filter |
| 以为 should 等于强制 OR | 有 must 时 should 不强制 | minimum_should_match |
| 多字段搜索分别写多个 must match | 打分模型不如 multi_match 合理 | multi_match 统一查 |
| nested 字段用普通 term 查 | 直接报错或查不到 | 套 nested 查询 |
卡点急救
- 查询结果和预期不符,不知道是分词问题还是逻辑问题:先用
POST /_analyze看你的输入被切成什么词(下一课细讲),再_search?explain=true看某条文档具体的打分和命中解释,两步定位。 - 条件越加越多,命中归零:bool 条件是「收紧」过程。逐个注释 filter 排查哪个条件过严;检查 term 的大小写、range 的边界(gte 与 gt)、keyword 是否存在
.keyword误用。 - 深 JSON 里的字段查询报错「field is not nested」或无结果:确认 mapping 实际路径(
_mapping),多层对象路径写全(如comments.replies.user);nested 查询的 path 与字段名必须对齐。
面试常问
-
bool 查询里 must、filter、should、must_not 的区别? must 必须满足且参与相关性打分;filter 必须满足但不打分、结果可缓存,适合状态/范围等过滤;must_not 必须不满足,以过滤上下文执行不打分;should 为加分项,满足时提升得分,默认不强制,可用 minimum_should_match 改变行为。
-
term 和 match 的区别?什么场景用哪个? term 对倒排索引做精确词项匹配,不分词不做相关性处理,用于 keyword/数值等结构化字段;match 对输入先按字段分词器分词,再逐词查询并用 BM25 打分,用于 text 全文检索。核心判断依据是字段类型和「你要精确值还是相关内容」。
-
为什么过滤推荐用 filter 上下文? filter 只判断是否匹配、跳过打分,并且结果可以被节点缓存复用,在高频重复过滤(如课程、上下架状态)下显著降低 CPU;把算分开销留给真正影响相关性的 must 条件,排序也更合理。
自测三问
term: {stem: "Spring 事务在什么情况下会失效"}为什么查不到?给出两种正确写法并说明取舍。- 一个 bool 查询同时有 must、filter、should,命中文档的分数由哪几部分构成?filter 贡献分数吗?
- 搜「缓存 失效」希望两词都中且尽量相邻,你能给出两种实现并比较严格程度吗?
小结一句话
查询 DSL 的核心是「按字段类型选叶子、按是否算分选上下文」:精确与范围走 filter,全文与相关走 must,match 查分词、term 查词项。
延伸
- Query DSL
- Boolean query
- Term-level queries
- 下一课:搜索结果控制——深分页为什么危险、高亮怎么配、排序和打分微调。