Skip to content
第 8 章 ⏱ 14 分钟阅读

第 8 章:ES 查询 DSL ​

学习目标 ​

  • 掌握 match / term / bool 三大查询
  • 学会分页、排序、高亮
  • 理解 Query vs Filter 区别
  • 用 Java 构造复杂查询

一、查询 DSL 基础 ​

ES 用 JSON 描述查询,基本结构:

json
{
  "query": { ... },
  "from": 0, "size": 10,
  "sort":  [ ... ],
  "_source": [ ... ],
  "highlight": { ... }
}

二、Leaf Query(基础查询) ​

2.1 match(全文搜索) ​

bash
GET /articles/_search                                  # ① 查 articles 索引(/_search 是固定端点)
{
  "query": {                                            # ② 查询体
    "match": {                                          # ③ 查询类型:match(全文搜索)
      "title": "Java 入门"                               # ④ 在 title 字段找 "Java 入门"
    }                                                   #    ES 会按 search_analyzer 分词
  }                                                     #    → ["java", "入门"],任一词命中就返回(OR)
}

与 term 对比:

查询干啥用在什么字段
match分词后查(OR 匹配)text
term整体精确查(必须完全相等)keyword
json
// match:title 含 "java" 或 "入门" 都命中
{ "match": { "title": "Java 入门" } }

// term:title 必须完全等于 "Java 入门" 才命中
{ "term": { "title.keyword": "Java 入门" } }

match 会分词,搜"Java 入门"等同于搜"Java" OR "入门"。

⚠️ 坑 1:match 默认是 OR,词越多结果越多。要严格匹配用 match + operator: "and"。

2.2 term(精确匹配) ​

bash
GET /articles/_search
{
  "query": {
    "term": {
      "status": "PUBLISHED"
    }
  }
}

term 不分词,用于 keyword / integer / boolean。

⚠️ 坑 2:match 查 text 字段,term 查 keyword / 数字 / 布尔。搞反就是"查不到数据"。

2.3 range(范围) ​

bash
GET /articles/_search
{
  "query": {
    "range": {
      "price": { "gte": 10, "lte": 100 }
    }
  }
}

gt / gte / lt / lte,支持 date / number。

三、Compound Query(组合查询) ​

3.1 bool ​

json
{
  "query": {
    "bool": {
      "must":     [ { "match": { "title": "Redis" } } ],
      "must_not": [ { "term":  { "status": "DELETED" } } ],
      "should":   [ { "match": { "content": "缓存" } } ],
      "filter":   [ { "term":  { "category": "tech" } } ]
    }
  }
}
子句作用是否影响评分
must必须命中,影响评分✅
filter必须命中,不影响评分,可缓存❌
should可选命中,影响评分✅
must_not必须不命中❌

⚠️ 坑 3:搜索条件 → must / should;精确筛选(状态、分类)→ filter(性能更好,会被 ES 自动缓存)。

3.2 实操:组合查询 ​

bash
GET /articles/_search                              # ① 查 articles 索引
{
  "query": {                                       # ② 查询体
    "bool": {                                       # ③ 组合查询
      "must": [{ "match": { "title": "Spring Boot" } }],  # ④ 标题含 "Spring Boot"(打分)
      "filter": [                                   # ⑤ 精确过滤(不打分)
        { "term":  { "status": "PUBLISHED" } },     #    状态必须 = 已发布
        { "range": { "views":  { "gte": 100 } } }   #    浏览数 ≥ 100
      ]
    }
  },
  "sort": [                                        # ⑥ 排序
    { "views": "desc" },                            #    第一排序:浏览数降序
    { "_score": "desc" }                            #    第二排序:分数降序(并列时按相关度)
  ],
  "from": 0, "size": 20                            # ⑦ 分页:从第 0 条开始,取 20 条
}

等价 SQL:WHERE title LIKE '%Spring Boot%' AND status='PUBLISHED' AND views>=100 ORDER BY views DESC, _score DESC LIMIT 20

四、分页 ​

4.1 from / size(浅分页) ​

bash
GET /articles/_search
{
  "from": 0, "size": 10
}

底层是"全局排序 + 丢弃前 N 条",深分页(from=10000)性能极差。

⚠️ 坑 4:from + size 上限默认 10000,超过会报错 Result window is too large。

4.2 search_after(深分页推荐) ​

bash
# 第一页
GET /articles/_search                              # ① 第一页请求
{
  "size": 10,                                      # ② 取 10 条
  "query": { ... },                                # ③ 查询条件(省略)
  "sort": [                                        # ④ 排序(必须带 _id 作为唯一排序)
    { "createTime": "desc" },                      #    第一排序:创建时间倒序
    { "_id": "desc" }                              #    第二排序:_id 倒序(打破平局)
  ]
}

# 拿最后一条的 sort 值,作为下一页 search_after
GET /articles/_search                              # ⑤ 第二页请求
{
  "size": 10,
  "query": { ... },
  "search_after": [1640995200000, "abc123"],       # ⑥ 上一页最后一条的 sort 值(时间戳, _id)
  "sort": [                                        #    sort 必须和上一页完全一致
    { "createTime": "desc" },
    { "_id": "desc" }
  ]
}

为什么必须加 _id 排序? createTime 可能有重复值,加 _id 兜底保证唯一性,不然会漏数据。

4.3 scroll(导出数据用) ​

bash
POST /articles/_search?scroll=1m                    # ① 第一次请求,创建 scroll 上下文(1m = 上下文保留 1 分钟)
{ "size": 1000 }                                    #    每批取 1000 条

# 之后每次
POST /_search/scroll                                # ② 后续请求(用 scroll_id 拿下一批)
{
  "scroll": "1m",                                   #    续命 1 分钟(每批都得续)
  "scroll_id": "..."                                #    第一次返回的 scroll_id(从响应里拿)
}

完整流程:

第 1 次:POST /articles/_search?scroll=1m  → 返回 1000 条 + scroll_id_1
第 2 次:POST /_search/scroll { scroll_id_1 }     → 返回 1000 条 + scroll_id_2
第 3 次:POST /_search/scroll { scroll_id_2 }     → 返回 1000 条 + scroll_id_3
...
直到:返回空数组 → 全部拿完

和 search_after 区别:

scrollsearch_after
用途一次性导出全量实时翻页
上下文占集群内存无状态
性能慢,大数据集一次性遍历快
实时性❌ 快照(导出期间数据不变)✅ 实时

⚠️ 坑 5:scroll 占用集群资源,实时搜索不要用,只在数据导出场景用。

五、高亮 ​

bash
GET /articles/_search
{
  "query": { "match": { "title": "Redis" } },
  "highlight": {
    "fields": {
      "title": {
        "pre_tags":  ["<em>"],
        "post_tags": ["</em>"]
      }
    }
  }
}

返回的每个 hit 会带 highlight.title: ["<em>Redis</em> 入门"]。

六、Java 构造 DSL ​

用 NativeQuery(Spring Data ES 5.x):

java
NativeQuery query = NativeQuery.builder()                                          // ① 链式构造查询
    .withQuery(q -> q.bool(b -> b                                                  // ② 组合查询(bool)
        .must(m -> m.match(mq -> mq                                                // ③ must:title 含 "Spring Boot"
            .field("title").query("Spring Boot")))
        .filter(f -> f.term(t -> t                                                 // ④ filter:status 必须 = PUBLISHED
            .field("status").value("PUBLISHED")))
    ))
    .withSort(s -> s.field(f -> f.field("views")                                   // ⑤ 排序:views 降序
        .order(SortOrder.Desc)))
    .withPageable(PageRequest.of(0, 20))                                           // ⑥ 分页:第 0 页 20 条
    .build();

SearchHits<Article> hits = elasticsearchOperations                                // ⑦ 执行查询
    .search(query, Article.class);                                                 //    返回 SearchHits 包装结果

⚠️ 坑 6:Spring Data ES 5.x 把 QueryBuilder 改成 co.elastic.clients.elasticsearch._types.query_dsl 包,旧 API 已弃用。

七、本章小结 ​

要点关键
match分词全文搜索,默认 OR
term精确匹配 keyword/数字/布尔
rangegt/gte/lt/lte
boolmust / filter / should / must_not
filter不算分、可缓存,筛选优先用
分页from/size 浅分页,search_after 深分页
高亮highlight 字段,前后标签

动手练习 ​

  1. 查"标题含 Redis 且状态为 PUBLISHED 且阅读量 > 50"的文章,用 bool + filter
  2. 用 search_after 翻 3 页,观察每次 sort 字段值怎么传
  3. 加高亮,让结果里 "Redis" 被 <em> 包裹

下一章:第 9 章:ES 聚合 →

本站基于 VitePress 构建 · 由 StackHub 团队维护