先把结论写在前面

第一次写 ES 查询,很容易对着 JSON 发懵:

为什么有的写 bool,有的写 term,还要 nested?
must 和 filter 为什么是同级?
should 不写行不行?写了会不会影响搜索结果总数?

它们不是同一层级的「三选一」,而是分工不同、经常嵌套在一起用:

关键字管什么一句话
bool逻辑组合这些条件怎么 AND / OR / NOT
term / match具体条件每个条件查什么值
nested查询范围多个字段是否属于同一个嵌套对象
must / filter / should / must_notbool 的子句必须满足 / 只过滤 / 加分 / 排除

上表后四项都写在 bool 里面,不是和 bool 平级的顶层查询类型。

记住一句就够:

term 是「查什么值」,bool 是「怎么组合」,nested 是「值之间有没有关联」。


本文结构

章节内容
一、bool 逻辑组合must / filter / should / must_not
二、term 与 match字段类型 → 查询语法 → 倒排索引 → title.keyword
三、nested 嵌套对象何时必须用,为何 term 组合不够
四、复杂示例串讲需求列表 + 带编号注释的 DSL
五、常见疑问 FAQ对话里最容易卡住的几个点
踩坑清单生产环境注意项

nested 深层原理(扁平化 vs 子文档)另读:为什么 nested 不能用 term 组合代替?


一、bool 逻辑组合

bool 本身不查具体值,只负责编排多个子查询:

{
  "bool": {
    "must":     [ ... ],
    "should":   [ ... ],
    "must_not": [ ... ],
    "filter":   [ ... ],
    "minimum_should_match": 1
  }
}

1.1 四个子句

子句含义是否算分类比
must必须全部满足✅ 参与 _scoreAND(且影响排序)
filter必须全部满足❌ 不算分,可缓存WHERE(硬过滤)
should满足越多越好✅ 命中加分加分项 / 软 OR
must_not必须不满足❌ 排除NOT

四个子句的值都是数组,每一项是一条子查询;同一子句里写多条 = 全部都要满足(AND)。

1.2 简单例子

「已发布 且 标题含 vue 且 描述含组件,标签有 nuxt 或 react 更好,排除草稿」:

{
  "bool": {
    "filter": [{ "term": { "status": "published" } }],
    "must": [
      { "match": { "title": "vue" } },
      { "match": { "description": "组件" } }
    ],
    "should": [{ "term": { "tags": "nuxt" } }, { "term": { "tags": "react" } }],
    "must_not": [{ "term": { "draft": true } }]
  }
}

此例 should 仅加分:没命中 nuxt/react 标签也会出现在结果里(未设 minimum_should_match,详见 FAQ Q5)。

filter 里的条件放 must 行不行? 行。例如 { "term": { "status": "published" } } 放进 must,未发布的文档同样不会出现。差别在于:放 filter 不参与算分、可走缓存(硬条件更推荐);放 must 会参与 _score,对 term 这类条件通常没有排序意义,还可能让分数变乱。所以「过不过」的放 filter,「搜什么、怎么排序」的放 must。

1.3 filter / must / should 怎么分工

filter + must + must_not  →  「能不能出现在结果里」
should                    →  「在能出现的里面,谁排更前」(有时也影响总数,见 FAQ)
子句典型用途
filter状态、价格区间、权限、库存
must搜索框关键词、影响相关性的条件
should标签加分、高评分优先(可选)
must_not排除草稿、黑名单

1.4 层级关系

query
 └── bool
      ├── filter → term / range / nested …
      ├── must   → match / multi_match …
      ├── should → term / range …
      └── must_not

二、term 与 match

这一层回答:每个条件查哪个字段、用什么方式查。

阅读顺序建议:2.1 字段类型 → 2.2 查询语法 → 2.3 倒排索引 → 2.4 title.keyword(重点)。

查询族是否分词适合字段SQL 类比
term / terms否keyword、数字= / IN
match 系列是text搜索引擎搜词

2.1 keyword 与 text:两种字段类型

ES 里最常见的两种字段类型,核心差别:

text 会分词,适合搜;keyword 不分词,适合筛、排、聚合。

keywordtext
索引方式整词原样存分词后存(analyzer)
典型用途精确匹配、排序、聚合全文搜索
常用查询term / termsmatch / multi_match
SQL 类比= / IN搜索引擎搜词

同一段文字,存法不同

你写入的 _source 原文(两份 mapping 都一样):

{ "title": "Vue3 实战教程" }

mapping 为 text — 索引里存的是拆开的词,不是整句:

_source 原文:  "Vue3 实战教程"
倒排索引里:    vue3 | 实战 | 教程        ← 只有这 3 个词,没有整句
// match 会先分词 "vue 实战" → vue、实战,再去索引里找 → 命中「实战」→ ✅
{ "match": { "title": "vue 实战" } }

// term 查整句 "Vue3 实战教程",索引里没有整句 → ❌
{ "term": { "title": "Vue3 实战教程" } }

mapping 为 keyword — 索引里整句原样存,不分词:

_source 原文:  "Vue3 实战教程"
倒排索引里:    Vue3 实战教程            ← 只有这 1 个词,没有 vue3、实战 等
// term 查 "Vue3 实战教程",和索引里的词完全一致 → ✅
{ "term": { "title": "Vue3 实战教程" } }

// match 会分词 "vue" → vue,索引里没有 vue 这个词 → ❌
{ "match": { "title": "vue" } }

各适合什么字段

字段推荐类型原因
status、tags、categorykeyword枚举值,精确筛
userId、订单号keyword精确匹配
标题、正文、描述text分词搜索

博文里的 status(单值)、tags(数组)通常都用 keyword——数组只是「一篇文档多个 keyword 值」,每个元素仍整词进索引。单值 vs 数组对 term 的影响见 2.3。

2.2 term、terms 与 match 系列

{ "term":  { "category": "book" } }
{ "terms": { "category": ["book", "course"] } }
查询含义SQL
term等于 1 个值category = 'book'
terms等于多个值之一category IN ('book', 'course')

terms = 把多个 term + should 合并成一条 IN 查询。

全文搜索用 match 系列(字段须为 text):

{ "match": { "title": "vue nuxt" } }

{
  "multi_match": {
    "query": "vue",
    "fields": ["title^3", "description"]
  }
}

{ "match_phrase": { "title": "实战教程" } }
查询用途
match单字段搜,默认 OR(有 vue 或有 nuxt 即可)
multi_match多字段一起搜;^3 表示该字段权重 ×3
match_phrase短语必须连续

match 与 match_phrase 对同一标题「Vue3 实战入门教程」:

查询输入能否命中
match实战 教程✅ 两个词都有即可
match_phrase实战教程❌ 中间隔了「入门」

2.3 倒排索引:单值 vs 数组

要理解 term 为什么有时像 =、有时像「其中一个就行」,得先看 ES 实际怎么存。

你写入的文档

// post-1
{ "title": "Vue3 实战", "status": "published", "tags": ["vue", "nuxt"] }

// post-2
{ "title": "React 入门", "status": "published", "tags": ["react"] }

// post-3
{ "title": "草稿一篇",   "status": "draft",     "tags": ["vue", "react"] }
字段存的内容每篇文档几个值
status"published"1 个
tags["vue", "nuxt"]多个

简化倒排索引

ES 用倒排索引记录:「某个词出现在哪些文档里」。

status(keyword,单值)
  published → [ post-1, post-2 ]
  draft     → [ post-3 ]

tags(keyword,数组 → 每个元素单独一条)
  vue       → [ post-1, post-3 ]
  nuxt      → [ post-1 ]
  react     → [ post-2, post-3 ]

term 实际在干什么

term 只做一件事:去倒排索引里查——包含这个词的文档列表里,有没有这篇文档?

{ "term": { "status": "published" } }
{ "term": { "tags": "vue" } }
查询post-1为何
term status=published✅status 只有 1 个词:published
term tags=vue✅tags 里有 vue(还有 nuxt)
term tags=react❌tags 里没有 react

写法完全一样;语义差异来自字段是单值还是数组(见 FAQ Q6)。

tags 要同时有 vue 和 nuxt → 两个 term 放 bool.must(两次查询取交集):

{
  "bool": {
    "must": [
      { "term": { "tags": "vue" } },
      { "term": { "tags": "nuxt" } }
    ]
  }
}

2.4 ⭐ title 与 title.keyword 怎么选(生产最常用)

标题往往两个需求都要:搜索框能搜关键词,又要能整句精确匹配、排序、统计。
做法:同一份 title,mapping 里配 text + keyword 子字段,查询时选对字段名。

用法速查(建议直接记这张表):

你想干什么用哪个字段用什么查询示例
搜关键词(vue、实战)titlematch{ "match": { "title": "vue" } }
整句必须完全一致title.keywordterm{ "term": { "title.keyword": "Vue3 实战教程" } }
按标题字母序排序title.keywordsort"sort": [{ "title.keyword": "asc" }]
按标题分组计数title.keywordaggs"terms": { "field": "title.keyword" }
记法:要「搜」→ title + match;要「整句 / 排序 / 统计」→ title.keyword + term/sort/aggs

mapping 长这样:

{
  "title": {
    "type": "text",
    "fields": {
      "keyword": { "type": "keyword" }
    }
  }
}

你写入的 _source 还是一条:

{ "title": "Vue3 实战教程" }

ES 会按两个字段名各建一份倒排索引(同一句标题,存了两遍,方式不同):

字段 title(text,拆词存)
  ES 里实际有的词:vue3、实战、教程
  (整句「Vue3 实战教程」并不存在)

字段 title.keyword(keyword,整句存)
  ES 里实际有的词:Vue3 实战教程
  (没有 vue3、实战、教程 这些小块)

正确用法:

// 搜「vue」→ 查 title → ES 里有 vue3、实战、教程 → 能匹配到 → ✅
{ "match": { "title": "vue" } }

// 整句相等 → 查 title.keyword → ES 里有「Vue3 实战教程」→ 完全一致 → ✅
{ "term": { "title.keyword": "Vue3 实战教程" } }

错配用法(字段和查询对不上):

// 你想整句匹配「Vue3 实战教程」,却查了 title
// title 里只有 vue3、实战、教程,没有整句 → ❌
{ "term": { "title": "Vue3 实战教程" } }

// 你想搜关键词「vue」,却查了 title.keyword
// title.keyword 里只有整句,没有单独的 vue → ❌
{ "match": { "title.keyword": "vue" } }

用白话总结错配原因:

错在哪你查的内容那个字段里实际有什么
term 查了 title整句「Vue3 实战教程」只有 vue3、实战、教程(拆开的词)
match 查了 title.keyword关键词「vue」只有整句「Vue3 实战教程」

排序、聚合示例(都要 .keyword):

"sort": [{ "title.keyword": "asc" }],

"aggs": {
  "titles": { "terms": { "field": "title.keyword" } }
}

一句话:_source 一条标题,ES 里两个字段名——title 拆词用来搜,title.keyword 整句用来比相等 / 排序 / 统计;写查询时先想「搜还是整句」,再选字段名。

2.5 查询类型速查

查询分词典型字段用什么场景
term否keyword精确等于 1 个值
terms否keyword精确等于多个值之一(IN)
match是text单字段搜关键词
multi_match是多个 text标题+描述一起搜
match_phrase是text短语必须连续
range否数字、日期区间过滤

配对口诀:keyword 配 term/terms,text 配 match;排序/聚合/整句精确 → 字段.keyword(见 2.4)。


三、nested 嵌套对象

对象数组若用普通 object,ES 会拍扁存储,数组内字段的归属关系丢失:

{
  "user": [
    { "first": "Alice", "last": "White" },
    { "first": "John", "last": "Smith" }
  ]
}
user.first: [Alice, John]
user.last:  [White, Smith]    ← Alice 和 Smith 可能误配

两个 term 放 bool.must 会产生跨对象错误匹配。
nested 把每个数组元素存成独立子文档,查询时限定在同一子文档内:

{
  "nested": {
    "path": "user",
    "query": {
      "bool": {
        "must": [
          { "term": { "user.first": "Alice" } },
          { "term": { "user.last": "Smith" } }
        ]
      }
    }
  }
}

何时必须用 nested:对象数组 + 数组内多字段联合条件 + 归属关系重要(SKU、订单明细等)。
只对数组内单个字段过滤,普通 object + term 通常够用。

别和 2.3 的 tags 数组 搞混:tags 用 term 查「有没有这个标签」就够,因为每个标签只是单个字符串。nested 解决的是 对象数组里多个字段必须属于同一条子对象(如 SKU 的 color + size + stock 必须来自同一 SKU)。


四、复杂示例串讲

电商商品搜索:先列需求,再看 DSL;JSON 里用 #1 #2 … 注释对应上表。

需求

#需求
1已发布、非草稿
2标题/描述含 vue 或 nuxt,标题权重更高
3价格 50~150
4同一 SKU:color=blue、size=L、有库存
5同一促销:type=discount 且在有效期内
6品牌名以 Tech 开头,且有 rating 字段
7-1tags 含 frontend → 加分
7-2tags 含 typescript → 加分
7-3标题含短语 实战教程 → 加分
7-4rating ≥ 4.5 → 加分
7-5category 为 book 或 course → 加分
87-1~7-5 至少命中 1 个(minimum_should_match)
—排除 tags=legacy;按相关度→评分→价格排序,取 10 条

#7 拆成 7-1~7-5,是为把 should 里每一条和需求对上;frontend、typescript 等具体 tag 只是演示取值,业务里换成你们自己的标签名即可,不必照抄。

skus、promotions 须 mapping 为 nested;status、tags 等为 keyword;title、description 为 text。

DSL

GET /products/_search
{
  "query": {
    "bool": {
      "filter": [
        { "term": { "status": "published" } },           // #1
        { "term": { "draft": false } },                  // #1
        { "range": { "price": { "gte": 50, "lte": 150 } } },  // #3
        {
          "nested": {                                    // #4
            "path": "skus",
            "query": {
              "bool": {
                "filter": [
                  { "term": { "skus.color": "blue" } },
                  { "term": { "skus.size": "L" } },
                  { "range": { "skus.stock": { "gt": 0 } } }
                ]
              }
            }
          }
        },
        {
          "nested": {                                    // #5
            "path": "promotions",
            "query": {
              "bool": {
                "filter": [
                  { "term": { "promotions.type": "discount" } },
                  { "range": { "promotions.start": { "lte": "now/d" } } },
                  { "range": { "promotions.end": { "gte": "now/d" } } }
                ]
              }
            }
          }
        },
        { "prefix": { "brand.name": "Tech" } },          // #6
        { "exists": { "field": "rating" } }               // #6
      ],
      "must": [
        {
          "multi_match": {                               // #2
            "query": "vue nuxt",
            "fields": ["title^3", "description"],
            "type": "best_fields",
            "operator": "or"
          }
        }
      ],
      "should": [
        { "term": { "tags": "frontend" } },              // #7-1
        { "term": { "tags": "typescript" } },            // #7-2
        { "match_phrase": { "title": { "query": "实战教程", "boost": 2 } } },  // #7-3
        { "range": { "rating": { "gte": 4.5, "boost": 1.5 } } },               // #7-4
        { "terms": { "category": ["book", "course"], "boost": 1.2 } }          // #7-5
      ],
      "must_not": [
        { "term": { "tags": "legacy" } }                 // 排除
      ],
      "minimum_should_match": 1                          // #8
    }
  },
  "sort": [                                              // 排序
    { "_score": { "order": "desc" } },
    { "rating": { "order": "desc" } },
    { "price": { "order": "asc" } }
  ],
  "size": 10
}

不需要 #7、#8 时,删掉整个 should 和 minimum_should_match 即可(见 FAQ Q4)。生产里 #7 往往只保留 1~2 条 should,不必写满 5 条。


五、常见疑问 FAQ

Q1:must 为什么不能写进 filter 里?

语法上不行——filter 的值是查询数组,不能直接嵌 must 关键字:

// ❌
{ "bool": { "filter": { "must": [ ... ] } } }

逻辑上可以嵌套 bool,但语义变了:

// ✅ 合法,但内部条件不参与算分
{
  "bool": {
    "filter": [
      {
        "bool": {
          "must": [
            { "term": { "status": "published" } },
            { "range": { "price": { "gte": 50 } } }
          ]
        }
      }
    ]
  }
}
bool.must嵌在 filter 里的 bool
必须满足✅✅
参与 _score✅❌
走 filter 缓存❌✅

简单记:要影响排序 → must / should;只要过不过 → filter。


Q2:must 里可以写多条吗?

可以。 must / filter / should / must_not 都是数组,多条 = 全部满足(AND)。

"must": [
  { "match": { "title": "vue" } },
  { "match": { "description": "组件" } }
]

→ 标题含 vue 且 描述含组件。


Q3:为什么 must 里用 multi_match,不用两个 match?

两个 match 放 must = 标题 AND 描述都要匹配,比「标题或描述有一个就行」更严。

需求是搜 title 或 description → 用 multi_match 一次搜多字段。

放 must 而非 filter:匹配程度影响分数,标题命中排更前。


Q4:should 要不要写?

不需要加分就不写。

需求写法
硬筛选 + 按价格/日期排序filter + sort,无 should
搜索词 + 按相关性排序filter + must,无 should
还要「更好的排前面」加 should

Q5:should 只影响分数,不影响搜索结果总数吗?

不完全是——这是最容易误解的点。

场景should 影响总数?should 影响分数?
有 must/filter,无 minimum_should_match❌ 不影响✅ 影响
有 must/filter,且 minimum_should_match: 1✅ 影响✅ 影响
只有 should,无 must/filter✅ 影响(默认至少 1 条)✅ 影响

默认情况(有 must/filter、没设 minimum_should_match):

{
  "bool": {
    "filter": [{ "term": { "status": "published" } }],
    "must": [{ "match": { "title": "vue" } }],
    "should": [{ "term": { "tags": "frontend" } }]
  }
}
  • should 命中 → 分数更高
  • should 不命中 → 照样出现在结果里,总数不变

复杂示例的情况(设了 minimum_should_match: 1):

  • should 一条都没中 → 不进结果,总数会变少
有 must/filter + 无 minimum_should_match  →  should 纯加分
有 minimum_should_match                   →  should 也当门槛
只有 should                               →  should 决定谁能出现

Q6:term 写法一样,为什么 status 像 =、tags 像「有一个就行」?

不是 term 规则不同,是你存的数据结构不同。

字段文档里倒排索引里(每篇)term 效果
status"published"1 个词像 =
tags["vue","nuxt"]多个词像「有没有这一项」

用法完全相同:{ "term": { "字段": "值" } }。


踩坑清单

  1. text 用 match,keyword 用 term — 类型和查询要配对。
  2. 对象数组多字段联合条件 — 必须 nested,见 专题文。
  3. 硬条件放 filter — 不算分、通常更快。
  4. should 默认只加分 — 除非设了 minimum_should_match。
  5. wildcard 慎用 — 大数据量很慢。

一句话总结

bool       → 逻辑组合(must / filter / should / must_not)
term/match → 查什么值(看字段类型 + 倒排索引)
nested     → 多字段是否同一嵌套对象
filter     → 过不过,不计分
must       → 必须满足,计分
should     → 默认只加分;设 minimum_should_match 时也筛数量

延伸阅读