第 8 章:ES 查询 DSL
学习目标
- 掌握 match / term / bool 三大查询
- 学会分页、排序、高亮
- 理解 Query vs Filter 区别
- 用 Java 构造复杂查询
一、查询 DSL 基础
ES 用 JSON 描述查询,基本结构:
{
"query": { ... },
"from": 0, "size": 10,
"sort": [ ... ],
"_source": [ ... ],
"highlight": { ... }
}二、Leaf Query(基础查询)
2.1 match(全文搜索)
GET /articles/_search # ① 查 articles 索引(/_search 是固定端点)
{
"query": { # ② 查询体
"match": { # ③ 查询类型:match(全文搜索)
"title": "Java 入门" # ④ 在 title 字段找 "Java 入门"
} # ES 会按 search_analyzer 分词
} # → ["java", "入门"],任一词命中就返回(OR)
}与 term 对比:
| 查询 | 干啥 | 用在什么字段 |
|---|---|---|
match | 分词后查(OR 匹配) | text |
term | 整体精确查(必须完全相等) | keyword |
// 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(精确匹配)
GET /articles/_search
{
"query": {
"term": {
"status": "PUBLISHED"
}
}
}term 不分词,用于 keyword / integer / boolean。
⚠️ 坑 2:
match查text字段,term查keyword/ 数字 / 布尔。搞反就是"查不到数据"。
2.3 range(范围)
GET /articles/_search
{
"query": {
"range": {
"price": { "gte": 10, "lte": 100 }
}
}
}gt / gte / lt / lte,支持 date / number。
三、Compound Query(组合查询)
3.1 bool
{
"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 实操:组合查询
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(浅分页)
GET /articles/_search
{
"from": 0, "size": 10
}底层是"全局排序 + 丢弃前 N 条",深分页(from=10000)性能极差。
⚠️ 坑 4:
from + size上限默认 10000,超过会报错Result window is too large。
4.2 search_after(深分页推荐)
# 第一页
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(导出数据用)
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 区别:
| scroll | search_after | |
|---|---|---|
| 用途 | 一次性导出全量 | 实时翻页 |
| 上下文 | 占集群内存 | 无状态 |
| 性能 | 慢,大数据集一次性遍历 | 快 |
| 实时性 | ❌ 快照(导出期间数据不变) | ✅ 实时 |
⚠️ 坑 5:
scroll占用集群资源,实时搜索不要用,只在数据导出场景用。
五、高亮
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):
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/数字/布尔 |
| range | gt/gte/lt/lte |
| bool | must / filter / should / must_not |
| filter | 不算分、可缓存,筛选优先用 |
| 分页 | from/size 浅分页,search_after 深分页 |
| 高亮 | highlight 字段,前后标签 |
动手练习
- 查"标题含 Redis 且状态为 PUBLISHED 且阅读量 > 50"的文章,用 bool + filter
- 用 search_after 翻 3 页,观察每次
sort字段值怎么传 - 加高亮,让结果里 "Redis" 被
<em>包裹
下一章:第 9 章:ES 聚合 →