5.2 Query DSL 实战:从 match 到 bool 本节摘要:Query DSL 是用 JSON 表达查询的语言,核心两族:match 一族做全文检索(查询词先过分析器,再对倒排表暗号),term 一族做精确过滤(整串直达词条)。bool 查询把两族组合成业务逻辑,其中 filter 子句不算分且可缓存,是性能的第一杠杆。本节从暗号游戏讲到组合拳,附上分数构成的读法。 从 match 开始 一、全文检索族:match 的暗号游戏 match 的工作分两步:先把查询文本送进字段的同一个分析器切成词条,再把词条逐个去倒排表对暗号。第 2 章那次"搜退款命中 doc-1001"的内部就是:查询侧切出"退款",词条级倒排表命中,返回打分。
本节摘要:Query DSL 是用 JSON 表达查询的语言,核心两族:match 一族做全文检索(查询词先过分析器,再对倒排表暗号),term 一族做精确过滤(整串直达词条)。bool 查询把两族组合成业务逻辑,其中 filter 子句不算分且可缓存,是性能的第一杠杆。本节从暗号游戏讲到组合拳,附上分数构成的读法。
match 的工作分两步:先把查询文本送进字段的同一个分析器切成词条,再把词条逐个去倒排表对暗号。第 2 章那次"搜退款命中 doc-1001"的内部就是:查询侧切出"退款",词条级倒排表命中,返回打分。
GET tickets/_search { "query": { "match": { "title": { "query": "退款 处理", "operator": "and" } } } }
默认 operator 是 or——"退款 处理"任一命中即入围;改成 and 要求两个词条都出现。介于两者之间的是 minimum_should_match,比如七成命中,长查询的容错常用它。想保留词序("退款处理"而非"处理退款"),换短语查询:
GET tickets/_search { "query": { "match_phrase": { "title": "退款太慢" } } }
短语匹配额外用到了倒排表里的位置信息(3.2 的工位二记录的偏移)——词条都命中还要位置相邻才作数,精确但更贵。多字段检索用 multi_match,一份查询词投向几个字段,各算各的再合计。
term 拿整串直接对词条,不过分析器。这条铁律埋着新手第一大坑:
GET tickets/_search { "query": { "term": { "status": "pending" } } }
GET tickets/_search { "query": { "term": { "title": "退款" } } }
第一条正确:status 是 keyword,词条就是"pending"整串。第二条大概率空手而归:title 是 text,倒排表里的词条由分词决定——若写入切出的是"退"和"款"两个单字,拿"退款"整串对暗号当然对不上。口诀:term 只配 keyword 与数值日期,查 text 用 match。range 处理范围:日期区间、优先级区间、数值比较,边界参数 gte、lte、gt、lt 各取所需:
GET tickets/_search { "query": { "range": { "created_at": { "gte": "2026-08-01", "lt": "2026-09-01" } } } }
真实需求几乎都是复合的:"标题含退款、状态是待处理或已升级、优先级不低于二、排除测试租户"。bool 查询四种子句各管一摊:
GET tickets/_search { "query": { "bool": { "must": [ { "match": { "title": "退款" } } ], "filter": [ { "terms": { "status": ["pending", "escalated"] } }, { "range": { "priority": { "lte": 2 } } } ], "must_not": [ { "term": { "tenant": "test" } } ], "should": [ { "match": { "tags": "加急" } } ] } } }
| 子句 | 语义 | 算分吗 | 典型用途 |
|---|---|---|---|
| must | 必须满足 | 算 | 全文检索主体 |
| filter | 必须满足 | 不算,可缓存 | 状态、时间、租户过滤 |
| must_not | 必须不满足 | 不算(过滤语义) | 排除项 |
| should | 可选,满足加分 | 算(或单独作 or) | 提权项 |
filter 与 must 的差别是本节的胜负手:过滤条件"匹不匹配"与分数无关,引擎把它交给无评分的执行路径,还能缓存词条位图——重复的过滤条件几乎零成本。工程纪律:凡与相关性无关的条件一律进 filter,让 must 只装真正影响排序的东西。should 单独出现时退化为或查询(任一命中即入围);与 must 并存时变成提权器,命中者加分不入围改变。
想知道某条文档 3.7 分的构成,给子句起名字再看解释接口:
GET tickets/_search { "query": { "bool": { "should": [ { "match": { "title": { "query": "退款", "_name": "标题命中" } } }, { "term": { "tags": { "value": "加急", "_name": "标签加急" } } } ] } }, "explain": true }
解释输出会把每个命名子句的贡献拆开:标题命中的词频项、字段长度归一项、标签命中的常数项,加权求和即总分。词频高加分、字段短加分(标题里出现比正文里出现更有信息量)、查询词本身的稀有度加权——这套打分模型(经典算法是词频乘逆文档频率的变体,再除以字段长度)解释了"为什么标题命中的排前面"。排序不合业务直觉时,先用解释接口看构成,再决定是调权重(boost)还是改用 filter 加固定排序。
背景:客服主管要"退款相关、近三十天、状态非已解决"的工单,希望加急的排前面。操作:第一版把所有条件都塞进 must;结果状态过滤参与了打分,已解决工单因字段匹配"漂亮"挤进前排,业务侧反馈排序诡异。第二版把时间与状态挪进 filter、must 只留标题 match、should 放加急提权。结果:排序符合直觉,且同样的过滤条件在仪表盘与列表页共享缓存,整体耗时下降三成。解读:语义错位(过滤条件算分)与性能浪费(不可缓存)常同时出现,filter 化一举两得。变式:需要"退款"绝对优先于"加急"时,把 should 换成 constant_score 包裹的提权,或者干脆 5.3 的功能排序(先按加急排序再按分数)。
常见坑:对 text 字段用 term 查询整串中文,结果永远为空;对 keyword 字段用 match,单词条虽然也能命中,但浪费了分词开销。字段形态与查询类型对号入座,是 DSL 的第一课。
关键直觉:match 问"多匹配",term 问"是不是"。把查询写成两个问题的组合——相关性交给 match,确定性交给 filter——九成的业务检索都能翻译干净。