系列 :ElasticSearch 从入门到架构师
定位:本章是全书核心,掌握 DSL 检索语法是高效使用 ES 的关键所在。读完本章,你将能独立编写任意复杂查询,并理解查询性能背后的设计逻辑。
一、检索核心结构:查询上下文 vs 过滤上下文
1.1 两种上下文的本质区别
ES 在执行一次查询时,始终在以下两种语义下工作:
| 维度 | 查询上下文(Query Context) | 过滤上下文(Filter Context) |
|---|---|---|
| 核心问题 | 文档有多匹配? | 文档是否匹配? |
| 是否计算相关性分数 | ✅ 计算 _score |
❌ 不计算,分数固定为 0 |
| 是否缓存 | ❌ 不缓存 | ✅ 自动缓存(bitset) |
| 典型使用场景 | 全文检索、相关性排序 | 精确过滤、范围筛选 |
| 性能 | 相对较慢 | 显著更快 |
💡 架构师视角:能用 filter 的地方绝对不用 query。filter 的结果会被 ES 缓存为 bitset,后续相同过滤条件可直接复用,这是 ES 性能优化的第一要素。
1.2 DSL 请求的完整骨架
json
GET /index_name/_search
{
"query": { // ← 这里是查询上下文
"bool": {
"must": [...], // ← 查询上下文(影响评分)
"filter": [...] // ← 过滤上下文(不影响评分,走缓存)
}
},
"sort": [...],
"from": 0,
"size": 10,
"_source": ["field1", "field2"],
"highlight": {...}
}
1.3 _score 相关性分数的计算逻辑
ES 默认使用 BM25 算法(Elasticsearch 5.0+ 默认),相关性受三个因素影响:
- 词频(TF):词条在文档中出现的频率越高,分数越高
- 逆文档频率(IDF):词条在全量文档中越稀有,分数越高
- 字段长度归一化:字段越短,相同词条的权重越高
json
// 查看文档的详细评分解释
GET /index/_search
{
"explain": true,
"query": {
"match": { "title": "Elasticsearch" }
}
}
二、基础查询:match、term、terms、range、exists、wildcard
2.1 match 查询 ------ 全文检索的标准入口
match 是最常用的全文查询,会对搜索词进行分词处理后再匹配。
json
// 基础用法
GET /articles/_search
{
"query": {
"match": {
"content": "Elasticsearch 全文检索"
}
}
}
上述查询会将 "Elasticsearch 全文检索" 分词后,默认以 OR 逻辑匹配(分词结果中任意一个命中即可)。
精细化控制:
json
GET /articles/_search
{
"query": {
"match": {
"content": {
"query": "Elasticsearch 全文检索",
"operator": "AND", // 所有分词结果都必须命中
"minimum_should_match": "75%" // 或指定最小匹配比例
}
}
}
}
match_phrase ------ 短语匹配(词序不可乱):
json
GET /articles/_search
{
"query": {
"match_phrase": {
"content": {
"query": "快速入门指南",
"slop": 1 // 允许词条之间最多间隔1个词
}
}
}
}
multi_match ------ 多字段全文检索:
json
GET /articles/_search
{
"query": {
"multi_match": {
"query": "Elasticsearch 架构",
"fields": ["title^3", "content", "tags^2"], // ^N 表示字段权重加成
"type": "best_fields" // most_fields | cross_fields | phrase
}
}
}
type 值 |
说明 |
|---|---|
best_fields(默认) |
取得分最高的字段分数,适合字段互斥场景 |
most_fields |
多字段得分累加,适合同一内容分散在多字段 |
cross_fields |
跨字段视为一个整体,适合姓名/地址类场景 |
phrase |
短语匹配模式 |
2.2 term 查询 ------ 精确值匹配
term 不对搜索词做分词,直接用原始值匹配倒排索引中的词条。适用于 keyword、数字、布尔值等精确字段。
json
// 精确匹配状态字段
GET /orders/_search
{
"query": {
"term": {
"status": {
"value": "completed"
}
}
}
}
// 简写形式
GET /orders/_search
{
"query": {
"term": { "status": "completed" }
}
}
⚠️ 常见踩坑 :对
text类型字段使用term查询,往往命中率为 0!原因是 text 字段存储的是分词后的词条 ,而不是原始值。应该使用该字段的.keyword子字段:"title.keyword": "完整标题"
2.3 terms 查询 ------ 多值精确匹配(IN 语义)
json
GET /products/_search
{
"query": {
"terms": {
"category": ["手机", "平板", "电脑"],
"boost": 1.5 // 可选:提升该条件的权重
}
}
}
terms lookup ------ 从另一个文档动态获取过滤值(高级用法):
json
// 用户 uid=1001 关注了哪些标签,查询这些标签下的文章
GET /articles/_search
{
"query": {
"terms": {
"tags": {
"index": "users",
"id": "1001",
"path": "followed_tags"
}
}
}
}
2.4 range 查询 ------ 范围匹配
json
GET /logs/_search
{
"query": {
"range": {
"timestamp": {
"gte": "2024-01-01T00:00:00",
"lte": "2024-12-31T23:59:59",
"format": "yyyy-MM-dd'T'HH:mm:ss",
"time_zone": "+08:00"
}
}
}
}
| 参数 | 含义 |
|---|---|
gt |
大于 |
gte |
大于等于 |
lt |
小于 |
lte |
小于等于 |
数值范围示例:
json
GET /products/_search
{
"query": {
"range": {
"price": {
"gte": 100,
"lte": 500
}
}
}
}
2.5 exists 查询 ------ 字段存在性检查
json
// 查询 email 字段存在(非 null、非空数组)的文档
GET /users/_search
{
"query": {
"exists": { "field": "email" }
}
}
// 配合 bool 查询,查找字段不存在的文档
GET /users/_search
{
"query": {
"bool": {
"must_not": [
{ "exists": { "field": "deleted_at" } }
]
}
}
}
2.6 wildcard 查询 ------ 通配符匹配(基础版)
json
GET /files/_search
{
"query": {
"wildcard": {
"filename.keyword": {
"value": "report_2024_*",
"boost": 1.0,
"rewrite": "constant_score"
}
}
}
}
| 通配符 | 含义 |
|---|---|
* |
匹配 0 到多个字符 |
? |
匹配任意单个字符 |
⚠️ 性能警告 :以
*开头的通配符查询(如*keyword)会触发全索引扫描,严禁在生产环境使用!
三、复合查询:bool 多条件组合
bool 查询是 ES 中最强大、最常用的复合查询,通过组合多个子查询实现复杂逻辑。
3.1 bool 查询的四个子句详解
json
GET /products/_search
{
"query": {
"bool": {
"must": [ // 必须匹配,影响评分
{ "match": { "name": "苹果手机" } }
],
"should": [ // 可选匹配,命中则加分
{ "term": { "brand": "Apple" } },
{ "range": { "rating": { "gte": 4.5 } } }
],
"must_not": [ // 必须不匹配,不影响评分
{ "term": { "status": "discontinued" } }
],
"filter": [ // 必须匹配,不影响评分,走缓存
{ "range": { "price": { "lte": 8000 } } },
{ "term": { "in_stock": true } }
],
"minimum_should_match": 1 // should 中至少匹配 1 个
}
}
}
3.2 四个子句的核心对比
| 子句 | 匹配要求 | 影响 _score |
缓存 | 对应 SQL 语义 |
|---|---|---|---|---|
must |
必须匹配 | ✅ 是 | ❌ | AND(全文) |
filter |
必须匹配 | ❌ 否 | ✅ | AND(精确) |
should |
可以匹配 | ✅ 是 | ❌ | OR |
must_not |
必须不匹配 | ❌ 否 | ✅ | NOT |
💡 最佳实践 :把精确条件(状态、分类、时间范围等)放入
filter,把全文检索条件放入must。这样既能保证精确过滤走缓存,又能得到准确的相关性评分。
3.3 嵌套 bool 查询实战
实现 SQL:(category='手机' OR category='平板') AND price < 5000 AND name LIKE '%华为%'
json
GET /products/_search
{
"query": {
"bool": {
"must": [
{ "match": { "name": "华为" } }
],
"filter": [
{ "range": { "price": { "lt": 5000 } } },
{
"bool": {
"should": [
{ "term": { "category": "手机" } },
{ "term": { "category": "平板" } }
],
"minimum_should_match": 1
}
}
]
}
}
}
3.4 boost 参数 ------ 精细化控制评分权重
json
GET /articles/_search
{
"query": {
"bool": {
"should": [
{
"match": {
"title": {
"query": "Elasticsearch",
"boost": 3.0 // 标题命中权重 3 倍
}
}
},
{
"match": {
"content": {
"query": "Elasticsearch",
"boost": 1.0 // 内容命中权重正常
}
}
}
]
}
}
}
四、模糊查询、前缀查询、通配符查询、正则查询
4.1 fuzzy 查询 ------ 模糊容错搜索
fuzzy 基于编辑距离(Levenshtein Distance),允许搜索词有若干字符的错误。
json
GET /users/_search
{
"query": {
"fuzzy": {
"username": {
"value": "elasticsaerch", // 拼写有误
"fuzziness": "AUTO", // AUTO | 0 | 1 | 2
"prefix_length": 2, // 前 N 个字符不允许模糊
"max_expansions": 50 // 最多扩展多少个变体词
}
}
}
}
fuzziness 值 |
说明 |
|---|---|
AUTO(推荐) |
词长 1-2 → 不允许模糊;3-5 → 允许1个;6+ → 允许2个 |
0 |
精确匹配 |
1 |
允许 1 个字符差异 |
2 |
允许 2 个字符差异(最大值) |
⚠️
fuzziness最大值为 2,超过 2 个编辑距离的模糊检索意义不大且性能差。
match 查询中使用 fuzziness:
json
GET /products/_search
{
"query": {
"match": {
"name": {
"query": "iphon",
"fuzziness": "AUTO",
"prefix_length": 1
}
}
}
}
4.2 prefix 查询 ------ 前缀匹配
json
GET /autocomplete/_search
{
"query": {
"prefix": {
"title.keyword": {
"value": "Elastic",
"rewrite": "constant_score_blended"
}
}
}
}
⚠️ 性能问题 :
prefix查询会遍历倒排索引中所有以该前缀开头的词条。推荐方案 :对需要前缀搜索的字段使用edge_ngram分词器,将前缀在索引时预处理,查询时直接用term,性能提升数十倍。
json
// 推荐方案:edge_ngram 映射配置
PUT /autocomplete
{
"settings": {
"analysis": {
"analyzer": {
"autocomplete_analyzer": {
"type": "custom",
"tokenizer": "standard",
"filter": ["lowercase", "autocomplete_filter"]
}
},
"filter": {
"autocomplete_filter": {
"type": "edge_ngram",
"min_gram": 1,
"max_gram": 20
}
}
}
},
"mappings": {
"properties": {
"title": {
"type": "text",
"analyzer": "autocomplete_analyzer",
"search_analyzer": "standard"
}
}
}
}
4.3 wildcard 查询 ------ 通配符查询(进阶)
json
GET /logs/_search
{
"query": {
"wildcard": {
"path.keyword": {
"value": "/api/v*/users/*",
"case_insensitive": true // ES 7.10+ 支持
}
}
}
}
性能黄金法则:
- ✅ 后缀通配:
"value": "prefix_*"--- 可接受 - ❌ 前缀通配:
"value": "*_suffix"--- 严禁生产使用 - ❌ 两端通配:
"value": "*keyword*"--- 严禁生产使用(等价于全表扫描)
4.4 regexp 查询 ------ 正则表达式查询
json
GET /phones/_search
{
"query": {
"regexp": {
"phone.keyword": {
"value": "1[3-9][0-9]{9}", // 匹配手机号格式
"flags": "ALL",
"case_insensitive": false,
"max_determinized_states": 10000 // 限制正则复杂度,防止 OOM
}
}
}
}
⚠️ 生产使用准则 :regexp 查询对所有词条逐一执行正则匹配,复杂正则 + 大索引 = 集群雪崩。生产环境务必:
- 设置
max_determinized_states限制复杂度- 避免
.*类贪婪匹配- 考虑在应用层做正则过滤,ES 只做范围预筛选
4.5 ids 查询 ------ 按文档 ID 批量查询
json
GET /orders/_search
{
"query": {
"ids": {
"values": ["order_001", "order_002", "order_003"]
}
}
}
五、排序、分页、高亮、字段过滤、结果截断
5.1 排序(sort)
单字段排序:
json
GET /products/_search
{
"query": { "match_all": {} },
"sort": [
{ "price": { "order": "asc" } }
]
}
多字段排序(组合排序):
json
GET /products/_search
{
"sort": [
{ "sales_volume": { "order": "desc" } }, // 第一排序键
{ "price": { "order": "asc" } }, // 第二排序键
"_score" // 第三排序键(相关性)
]
}
按相关性分数排序(默认):
json
"sort": ["_score"] // 等价于不指定 sort
按嵌套对象字段排序:
json
GET /offers/_search
{
"sort": [
{
"offers.price": {
"order": "asc",
"nested": {
"path": "offers",
"filter": {
"term": { "offers.currency": "CNY" }
}
},
"mode": "min" // min | max | avg | sum(多值字段取哪个值排序)
}
}
]
}
地理位置排序(geo_distance):
json
GET /restaurants/_search
{
"sort": [
{
"_geo_distance": {
"location": {
"lat": 39.9042,
"lon": 116.4074
},
"order": "asc",
"unit": "km"
}
}
]
}
⚠️ 排序注意事项 :对
text类型字段排序会报错!排序只能用于keyword、数字、日期类型。如需对文本字段排序,需在 mapping 中为其设置fielddata: true(内存消耗大,不推荐)或使用.keyword子字段。
5.2 分页(from + size)
json
GET /articles/_search
{
"from": 0, // 从第几条开始(0-based)
"size": 10, // 返回多少条
"query": { "match_all": {} }
}
// 第 N 页的计算公式:from = (N-1) * size
// 第 3 页,每页 10 条:from = 20, size = 10
默认限制:from + size 最大值为 10000
超出后 ES 会报错:Result window is too large, from + size must be less than or equal to: [10000]
可临时调整(不推荐):
json
PUT /index/_settings
{
"index.max_result_window": 50000
}
深度分页问题的根本解决方案见第六节。
5.3 高亮(highlight)
json
GET /articles/_search
{
"query": {
"match": { "content": "Elasticsearch 性能优化" }
},
"highlight": {
"pre_tags": ["<mark>"], // 高亮标签前缀(默认 <em>)
"post_tags": ["</mark>"], // 高亮标签后缀
"fields": {
"content": {
"fragment_size": 150, // 每个高亮片段的字符数
"number_of_fragments": 3, // 返回多少个高亮片段
"fragmenter": "span" // simple | span
},
"title": {
"number_of_fragments": 0 // 0 表示返回整个字段(不截断)
}
},
"require_field_match": false, // true=只高亮匹配字段,false=所有字段都高亮
"type": "unified" // unified(默认)| plain | fvh(最快)
}
}
| 高亮器类型 | 说明 | 适用场景 |
|---|---|---|
unified(默认) |
基于 Lucene Unified Highlighter | 通用场景 |
plain |
逐段分析,内存占用低 | 简单文本,内存敏感 |
fvh(Fast Vector Highlighter) |
需要 term_vector 支持,速度最快 |
大字段、高并发 |
返回结果示例:
json
{
"_source": { "title": "...", "content": "..." },
"highlight": {
"content": [
"在进行<mark>Elasticsearch</mark>的<mark>性能优化</mark>时,需要...",
"...通过合理配置<mark>Elasticsearch</mark>集群参数..."
]
}
}
5.4 字段过滤(_source filtering)
控制返回哪些字段,减少网络传输量:
json
GET /articles/_search
{
"_source": ["title", "author", "created_at"], // 只返回这些字段
"query": { "match_all": {} }
}
// 排除特定字段
GET /articles/_search
{
"_source": {
"excludes": ["content", "raw_html"]
}
}
// 包含与排除结合
GET /articles/_search
{
"_source": {
"includes": ["user.*", "title"],
"excludes": ["user.password"]
}
}
// 完全不返回 _source(只需判断文档是否存在时使用)
GET /articles/_search
{
"_source": false
}
使用 fields API 返回特定字段(推荐方式,支持运行时字段):
json
GET /articles/_search
{
"fields": ["title", "author", "@timestamp"],
"_source": false
}
5.5 结果截断(terminate_after / timeout)
json
// 每个分片找到 N 条就停止(牺牲完整性换速度)
GET /logs/_search
{
"terminate_after": 1000,
"query": { "match": { "level": "ERROR" } }
}
// 整个查询的超时时间
GET /logs/_search
{
"timeout": "3s",
"query": { "match_all": {} }
}
响应中会包含超时标识:
json
{
"timed_out": false,
"terminated_early": true,
"hits": { ... }
}
六、深度分页问题与 From+Size 失效解决方案
6.1 深度分页为什么是个问题?
理解问题的本质,需要了解 ES 的分布式查询流程:
客户端请求 from=9000, size=10
↓
协调节点(Coordinator)
/ | \
分片1 分片2 分片3 ← 每个分片都要返回前 9010 条!
\ | /
协调节点汇总 9010 × 3 = 27030 条数据
↓
全局排序后取第 9001-9010 条
↓
返回给客户端 10 条
问题核心:深度翻页时,每个分片需要加载的数据量随页码线性增长,协调节点的内存和 CPU 压力巨大。
max_result_window = 10000正是 ES 为保护集群稳定性设置的防护机制。
6.2 解决方案一:Scroll API(游标翻页)
适用场景 :数据导出、全量遍历(不适合实时翻页,因为数据是快照)
json
// 第一步:发起 Scroll 查询,创建快照(指定快照保留时间)
POST /logs/_search?scroll=5m
{
"size": 1000,
"query": { "match": { "level": "ERROR" } },
"sort": ["_doc"] // _doc 是最快的排序方式,无需相关性计算
}
// 响应中会返回 scroll_id
{
"_scroll_id": "FGluY2x1ZGVfY29udGV4dF...",
"hits": { ... }
}
json
// 第二步:用 scroll_id 翻页(无需指定 from)
POST /_search/scroll
{
"scroll": "5m", // 每次翻页都要续期
"scroll_id": "FGluY2x1ZGVfY29udGV4dF..."
}
json
// 第三步:使用完毕后及时清理(释放内存)
DELETE /_search/scroll
{
"scroll_id": ["FGluY2x1ZGVfY29udGV4dF..."]
}
// 清理所有 scroll
DELETE /_search/scroll/_all
Scroll 缺点:
- 快照机制:查询期间新写入的数据不可见
- 消耗集群内存:每个 scroll 上下文占用资源
- 无法跳页:只能顺序翻页
- 不支持实时搜索场景
6.3 解决方案二:Search After(推荐,实时翻页)
适用场景 :实时翻页、无限下拉加载(生产环境首选)
核心思想 :用上一页最后一条文档的排序值作为下一页的起点,完全规避 from 的性能问题。
json
// 第一页:必须指定唯一的排序字段(通常包含 _id 保证唯一性)
GET /orders/_search
{
"size": 10,
"query": { "match": { "status": "paid" } },
"sort": [
{ "created_at": { "order": "desc" } },
{ "_id": "asc" } // 必须有一个唯一字段作兜底
]
}
// 响应中记录最后一条的 sort 值
{
"hits": {
"hits": [
{
"_id": "order_523",
"sort": [1704067200000, "order_523"] // ← 记录这个值
}
]
}
}
json
// 第二页:用上一页最后一条的 sort 值作为 search_after
GET /orders/_search
{
"size": 10,
"query": { "match": { "status": "paid" } },
"sort": [
{ "created_at": { "order": "desc" } },
{ "_id": "asc" }
],
"search_after": [1704067200000, "order_523"] // ← 传入上一页最后一条的 sort 值
}
Search After 的优势:
| 特性 | from+size | scroll | search_after |
|---|---|---|---|
| 支持跳页 | ✅ | ❌ | ❌ |
| 实时数据 | ✅ | ❌(快照) | ✅ |
| 深度翻页性能 | ❌ | ✅ | ✅ |
| 内存占用 | 高 | 高(快照) | 低 |
| 适合场景 | 前几页 | 数据导出 | 无限翻页 |
6.4 解决方案三:Point In Time(PIT)+ Search After(最新最佳实践)
ES 7.10+ 引入 PIT(时间点快照),结合 search_after 可实现:既有实时翻页的效率,又保证同一次翻页会话数据一致性。
json
// 第一步:创建 PIT 快照
POST /orders/_pit?keep_alive=5m
// 响应
{ "id": "46ToAwMDaWR5BXV1..." }
json
// 第二步:使用 PIT + search_after 翻页(不需要指定索引名!)
GET /_search
{
"size": 10,
"query": { "match": { "status": "paid" } },
"sort": [
{ "created_at": "desc" },
{ "_shard_doc": "asc" } // PIT 特有的隐式唯一排序字段
],
"pit": {
"id": "46ToAwMDaWR5BXV1...",
"keep_alive": "5m"
}
}
json
// 第三步:翻页完毕后删除 PIT
DELETE /_pit
{
"id": "46ToAwMDaWR5BXV1..."
}
PIT + Search After 是目前官方推荐的深度分页最佳实践。
6.5 三种分页方案对比总结
┌─────────────────────────────────────────┐
│ ES 分页方案决策树 │
└──────────────┬──────────────────────────┘
│
┌───────────────┼───────────────┐
↓ ↓ ↓
需要跳页? 数据导出? 实时翻页?
│ │ │
┌───────┴───┐ ┌─────┴────┐ ┌─────┴──────────┐
│ 页码 < 100?│ │ Scroll │ │ ES 7.10+? │
└───────┬───┘ │ (游标翻页)│ └─────┬──────────┘
│ └──────────┘ │
┌─────────┴────────┐ ┌─────┴───────┐
↓ ↓ ↓ ↓
from+size 重新设计需求 PIT + Search Search After
(前几页可用) (ES不擅长跳页) After(推荐) (7.10以下)
6.6 最终选型建议
| 场景 | 推荐方案 |
|---|---|
| 搜索结果前 3-5 页(普通用户习惯) | from + size(简单可靠) |
| 数据全量导出、ETL | Scroll API |
| 无限下拉、游标翻页(ES 7.10+) | PIT + search_after |
| 无限下拉、游标翻页(ES < 7.10) | search_after |
| 需要任意跳页 | 重新评估需求,或维护外部页码映射 |
本章总结
DSL 检索语法核心脉络
├── 两种上下文
│ ├── query context → 计算相关性分数
│ └── filter context → 精确过滤 + 缓存
├── 基础查询
│ ├── match/match_phrase/multi_match → 全文检索(分词)
│ ├── term/terms → 精确匹配(不分词)
│ ├── range → 范围查询
│ └── exists/wildcard → 存在性/通配符
├── 复合查询 bool
│ ├── must → AND + 计分
│ ├── filter → AND + 缓存(推荐替代 must 做精确条件)
│ ├── should → OR + 计分
│ └── must_not → NOT + 缓存
├── 模糊查询族
│ ├── fuzzy → 编辑距离容错
│ ├── prefix → 前缀匹配(注意性能)
│ ├── wildcard → 通配符(禁止前置*)
│ └── regexp → 正则(谨慎使用)
├── 结果控制
│ ├── sort → 排序
│ ├── from/size → 基础分页
│ ├── highlight → 高亮
│ └── _source → 字段过滤
└── 深度分页
├── Scroll → 数据导出
├── Search After → 游标翻页
└── PIT + Search After → 最佳实践(7.10+)