2318 字
约 7 分钟
16
S13-B05 · ES 查询 DSL 入门:match、term、bool 与它们的差别

{"term": {"title": "索引"}} 一条都搜不到,换成 match 全出来了;同一个 bool 里的 filter 和 must 结果一样,性能却差一截。这两件事都指向同一个概念:ES 查的是"词",不是你写进去的那个字符串。

这篇按"为什么查不到 → 怎么查对 → 怎么算分 → 怎么翻页聚合"的顺序走一遍。


1. 查询语境与过滤语境

语境 写法 算不算分 用途
query query.bool.must / should 算 _score 相关性排序
filter query.bool.filter 不算分,可缓存 精确条件、范围、时间窗

ES 8.x 里两者都写在 query 下,用子句区分。filter 不参与打分,所以能用 filter 的判断一律放 filter:既省算分开销,又命中 filter cache。下面两段结果集相同,代价不同:

{"query": {"bool": {"must":   [{"term": {"status": "PAID"}}]}}}
{"query": {"bool": {"filter": [{"term": {"status": "PAID"}}]}}}

2. 七种查询的语义边界

查询 是否分析查询词 匹配单元 典型场景
match 是 分词后任一词命中(默认 or) 全文搜索
match_phrase 是 分词后顺序相邻 短语、固定搭配
term 否 索引里的原词 keyword 字段、枚举、id
terms 否 多个原词任一命中 多值枚举、状态列表
range 否 区间比较 时间、金额、数值
prefix 否 词典前缀 补全、少量前缀匹配
wildcard 否 通配(* ?) 兜底,慎用

"term 当等于用"是第一个坑:text 字段写入时被 analyzer 切成词,倒排索引里存的是词;查询 term 又不过 analyzer,拿整个原字符串去对词,自然对不上。要精确匹配就建 keyword 字段。

3. analyzer 在写入和查询两端

写入:title = "学习索引原理"
      → analyzer 切词 → [学习, 索引, 原理] → 倒排索引记录这三个词
查询:{"match": {"title": "索引"}}
      → 同一个 analyzer 切词 → [索引] → 命中
查询:{"term": {"title": "学习索引原理"}}
      → 不切词 → 拿整串找词 → 无命中
阶段 是否走 analyzer 用哪个 analyzer
写入(index) 是 字段的 analyzer
match 查询 是 字段的 search_analyzer,未设则同 analyzer
term / terms 否 —
中文 需 ik 或内置 smartcn 按插件版本与字段配置核对

两端不一致是"写入能搜到一半、搜另一半为空"的根因:写入用 ik_max_word(细粒度)而查询用 ik_smart(粗粒度)时,词表对不上就会漏。

4. bool 四子句与打分

子句 是否必须 是否算分 说明
must 必须 算 参与 _score 计算
filter 必须 不算 可缓存,优先用
should 见下 算 命中可加分
must_not 必须不满足 不算 只做排除,不参与分

should 的默认行为随上下文变化,这是第二个大坑(ES 7.x / 8.x 行为一致,按版本核对):

上下文 should 的默认语义
同级还有 must 或 filter 只需加分,不要求命中
同级只有 should 至少命中一个

所以要"必须命中其一"却又有别的 must 时,必须显式写 minimum_should_match:

{"query": {"bool": {
  "must":   [{"term": {"status": "PAID"}}],
  "should": [{"match": {"title": "索引"}}, {"match": {"title": "分词"}}],
  "minimum_should_match": 1
}}}

5. 多字段查询:best_fields 与 most_fields

类型 取分方式 效果
best_fields(默认) 取单个字段的最高分 强调"某一个字段最匹配"
most_fields 各字段分数相加 强调"多个字段都有命中"
cross_fields 把字段视作一个整体 词分散在不同字段也能拼起来
{"query": {"multi_match": {
  "query": "索引 原理",
  "fields": ["title^3", "summary", "content"],
  "type": "best_fields"
}}}

^3 是字段加权,只影响打分不影响命中。

6. 排序与分页

方式 写法 代价
浅分页 from + size 每个分片都要取 from+size 条再归并
深分页 from 很大 内存与协调节点开销随 from 增长
游标 search_after + sort 无深翻页代价,只能顺序往后
导出 PIT + search_after 稳定快照,适合全量遍历
{"size": 20,
 "query": {"range": {"created_at": {"gte": "now-7d"}}},
 "sort": [{"created_at": "desc"}, {"_id": "asc"}],
 "search_after": [1735689600000, "doc-100"]}

排序键必须唯一且稳定(通常加 _id 兜底),否则同一时间戳的一组文档顺序会漂移,翻页漏数据或重复。

7. 聚合的基本结构

{"size": 0,
 "aggs": {
   "by_day": {
     "date_histogram": {"field": "created_at", "calendar_interval": "day"},
     "aggs": {"by_status": {"terms": {"field": "status", "size": 10}}}
   }
 }}
要点 说明
size: 0 只要聚合结果,不取 hits
terms 桶数由 size 控制,默认只回前若干桶
date_histogram calendar_interval 按日历切,fixed_interval 按固定时长
嵌套 aggs 内层在每个外层桶内各跑一次
基数爆炸 高基数字段做嵌套 terms,桶数相乘会拖垮协调节点

聚合字段必须可用 doc values(text 默认关闭),keyword 与数值类型默认开启。

8. _source 与 docvalue_fields

需求 手段 代价
回原始文档 _source(默认全回) 网络传输大
只回部分字段 "_source": {"includes": [...]} 需反序列化
只取排序/展示字段 docvalue_fields 列存读取,更快更省
{"_source": {"includes": ["title", "created_at"]},
 "docvalue_fields": ["status"],
 "query": {"match_all": {}}}

9. explain 与 profile 的区别

工具 回答的问题 开销
"explain": true 为什么这条文档得这个分 每条命中都算分细节,较重
"profile": true 这次查询每个子句花了多久 逐子句打点,更重

调相关性用 explain,查慢在哪个子句用 profile。两者都只该在单条或小批量查询里开。

10. 最小可跑示例

PUT /blog
{"mappings": {"properties": {
  "title":  {"type": "text", "analyzer": "standard", "fields": {"kw": {"type": "keyword"}}},
  "status": {"type": "keyword"},
  "created_at": {"type": "date"}
}}}

POST /blog/_doc/1
{"title": "学习索引原理", "status": "PAID", "created_at": "2026-01-05T10:00:00Z"}

GET /blog/_search
{"query": {"match": {"title": "索引"}}}                 # 命中
GET /blog/_search
{"query": {"term": {"title": "索引"}}}                  # 不命中:term 不切词
GET /blog/_search
{"query": {"term": {"title.kw": "学习索引原理"}}}       # 命中:整串对 keyword

把三种写法对着跑一遍,比记十遍规则都管用。

11. 三个按版本核对过的细节

细节 行为 影响
index.sort 未设 段内顺序不稳定 同一 search_after 可能重复/漏
track_total_hits 默认(7.x 起) 上限 10000 后不再精确计数 分页总数要显式设 true
date_histogram 空桶 min_doc_count 默认 0,回空桶 画图时出现断点或锯齿

search_after 需要排序值可比较且唯一;只按 created_at 排序时,同一秒内的多条会互相顶掉。加一个稳定的 tiebreaker 字段是硬要求,不是优化。

命中数为 10000 是分页脚本"总数永远显示 1 万"的典型来源:需要精确总数就写 "track_total_hits": true,代价是多走一次计数。

12. 排错

现象 原因 处理
term 查中文为空 字段是 text,值被切词 用 keyword 子字段或改 match
should 不生效 同级有 must,should 只加分 加 minimum_should_match
深分页变慢或 OOM 大 from 归并开销 改 search_after,控制 size
前缀通配很慢 wildcard/prefix 前置通配要扫词典 改用 ngram 或 completion
聚合报 fielddata disabled text 字段做聚合 聚合 keyword 子字段
写入后立刻搜不到 段未刷新(默认约 1 秒量级) 测试用 refresh=wait_for,生产不要频繁 refresh

13. 边界

需求 更合适的做法 理由
精确等值、唯一键 关系库 / get by id 全文检索堆不擅长点查
事务、强一致 关系库 ES 是近实时、无事务
复杂关联与 join 应用层拼装 / 反范式 join 字段限制多、代价高
词级相关性调优 自定义 similarity、rerank 默认 BM25 够用但不够细
海量日志归档 冷热分层 + 快照 单索引硬扛成本高

14. 自测

  1. term 查一个 text 字段为什么常常空结果?
  2. must 与 filter 结果相同,为什么还建议把判断放 filter?
  3. 同级已有 must 时,单独的 should 会不会强制命中?
  4. best_fields 与 most_fields 在多字段查询里各取什么分?
  5. 深分页为什么比浅分页贵?替代写法是什么?
  6. 聚合 text 字段报错时,字段层面的正确改法是什么?
S13-B05 · ES 查询 DSL 入门:match、term、bool 与它们的差别
https://www.clxhxhhr.top/posts/6078/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。