欢迎光临
我们一直在努力

【ElasticSearch从入门到架构师】第5章:ES DSL 检索语法精讲(核心重点)

系列:ElasticSearch 从入门到架构师
定位:本章是全书核心,掌握 DSL 检索语法是高效使用 ES 的关键所在。读完本章,你将能独立编写任意复杂查询,并理解查询性能背后的设计逻辑。


一、检索核心结构:查询上下文 vs 过滤上下文

1.1 两种上下文的本质区别

ES 在执行一次查询时,始终在以下两种语义下工作:

维度查询上下文(Query Context)过滤上下文(Filter Context)
核心问题 文档有多匹配? 文档是否匹配?
是否计算相关性分数 ✅ 计算 _score ❌ 不计算,分数固定为 0
是否缓存 ❌ 不缓存 ✅ 自动缓存(bitset)
典型使用场景 全文检索、相关性排序 精确过滤、范围筛选
性能 相对较慢 显著更快

💡 架构师视角:能用 filter 的地方绝对不用 query。filter 的结果会被 ES 缓存为 bitset,后续相同过滤条件可直接复用,这是 ES 性能优化的第一要素。

1.2 DSL 请求的完整骨架

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):词条在全量文档中越稀有,分数越高
  • 字段长度归一化:字段越短,相同词条的权重越高

// 查看文档的详细评分解释
GET /index/_search
{
"explain": true,
"query": {
"match": { "title": "Elasticsearch" }
}
}


二、基础查询:match、term、terms、range、exists、wildcard

2.1 match 查询 —— 全文检索的标准入口

match 是最常用的全文查询,会对搜索词进行分词处理后再匹配。

// 基础用法
GET /articles/_search
{
"query": {
"match": {
"content": "Elasticsearch 全文检索"
}
}
}

上述查询会将 "Elasticsearch 全文检索" 分词后,默认以 OR 逻辑匹配(分词结果中任意一个命中即可)。

精细化控制:

GET /articles/_search
{
"query": {
"match": {
"content": {
"query": "Elasticsearch 全文检索",
"operator": "AND", // 所有分词结果都必须命中
"minimum_should_match": "75%" // 或指定最小匹配比例
}
}
}
}

match_phrase —— 短语匹配(词序不可乱):

GET /articles/_search
{
"query": {
"match_phrase": {
"content": {
"query": "快速入门指南",
"slop": 1 // 允许词条之间最多间隔1个词
}
}
}
}

multi_match —— 多字段全文检索:

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、数字、布尔值等精确字段。

// 精确匹配状态字段
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 语义)

GET /products/_search
{
"query": {
"terms": {
"category": ["手机", "平板", "电脑"],
"boost": 1.5 // 可选:提升该条件的权重
}
}
}

terms lookup —— 从另一个文档动态获取过滤值(高级用法):

// 用户 uid=1001 关注了哪些标签,查询这些标签下的文章
GET /articles/_search
{
"query": {
"terms": {
"tags": {
"index": "users",
"id": "1001",
"path": "followed_tags"
}
}
}
}


2.4 range 查询 —— 范围匹配

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 小于等于

数值范围示例:

GET /products/_search
{
"query": {
"range": {
"price": {
"gte": 100,
"lte": 500
}
}
}
}


2.5 exists 查询 —— 字段存在性检查

// 查询 email 字段存在(非 null、非空数组)的文档
GET /users/_search
{
"query": {
"exists": { "field": "email" }
}
}

// 配合 bool 查询,查找字段不存在的文档
GET /users/_search
{
"query": {
"bool": {
"must_not": [
{ "exists": { "field": "deleted_at" } }
]
}
}
}


2.6 wildcard 查询 —— 通配符匹配(基础版)

GET /files/_search
{
"query": {
"wildcard": {
"filename.keyword": {
"value": "report_2024_*",
"boost": 1.0,
"rewrite": "constant_score"
}
}
}
}

通配符含义
* 匹配 0 到多个字符
? 匹配任意单个字符

⚠️ 性能警告:以 * 开头的通配符查询(如 *keyword)会触发全索引扫描,严禁在生产环境使用!


三、复合查询:bool 多条件组合

bool 查询是 ES 中最强大、最常用的复合查询,通过组合多个子查询实现复杂逻辑。

3.1 bool 查询的四个子句详解

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 '%华为%'

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 参数 —— 精细化控制评分权重

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),允许搜索词有若干字符的错误。

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:

GET /products/_search
{
"query": {
"match": {
"name": {
"query": "iphon",
"fuzziness": "AUTO",
"prefix_length": 1
}
}
}
}


4.2 prefix 查询 —— 前缀匹配

GET /autocomplete/_search
{
"query": {
"prefix": {
"title.keyword": {
"value": "Elastic",
"rewrite": "constant_score_blended"
}
}
}
}

⚠️ 性能问题:prefix 查询会遍历倒排索引中所有以该前缀开头的词条。推荐方案:对需要前缀搜索的字段使用 edge_ngram 分词器,将前缀在索引时预处理,查询时直接用 term,性能提升数十倍。

// 推荐方案: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 查询 —— 通配符查询(进阶)

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 查询 —— 正则表达式查询

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 批量查询

    GET /orders/_search
    {
    "query": {
    "ids": {
    "values": ["order_001", "order_002", "order_003"]
    }
    }
    }


    五、排序、分页、高亮、字段过滤、结果截断

    5.1 排序(sort)

    单字段排序:

    GET /products/_search
    {
    "query": { "match_all": {} },
    "sort": [
    { "price": { "order": "asc" } }
    ]
    }

    多字段排序(组合排序):

    GET /products/_search
    {
    "sort": [
    { "sales_volume": { "order": "desc" } }, // 第一排序键
    { "price": { "order": "asc" } }, // 第二排序键
    "_score" // 第三排序键(相关性)
    ]
    }

    按相关性分数排序(默认):

    "sort": ["_score"] // 等价于不指定 sort

    按嵌套对象字段排序:

    GET /offers/_search
    {
    "sort": [
    {
    "offers.price": {
    "order": "asc",
    "nested": {
    "path": "offers",
    "filter": {
    "term": { "offers.currency": "CNY" }
    }
    },
    "mode": "min" // min | max | avg | sum(多值字段取哪个值排序)
    }
    }
    ]
    }

    地理位置排序(geo_distance):

    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)

    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]

    可临时调整(不推荐):

    PUT /index/_settings
    {
    "index.max_result_window": 50000
    }

    深度分页问题的根本解决方案见第六节。


    5.3 高亮(highlight)

    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 支持,速度最快 大字段、高并发

    返回结果示例:

    {
    "_source": { "title": "…", "content": "…" },
    "highlight": {
    "content": [
    "在进行<mark>Elasticsearch</mark>的<mark>性能优化</mark>时,需要…",
    "…通过合理配置<mark>Elasticsearch</mark>集群参数…"
    ]
    }
    }


    5.4 字段过滤(_source filtering)

    控制返回哪些字段,减少网络传输量:

    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 返回特定字段(推荐方式,支持运行时字段):

    GET /articles/_search
    {
    "fields": ["title", "author", "@timestamp"],
    "_source": false
    }


    5.5 结果截断(terminate_after / timeout)

    // 每个分片找到 N 条就停止(牺牲完整性换速度)
    GET /logs/_search
    {
    "terminate_after": 1000,
    "query": { "match": { "level": "ERROR" } }
    }

    // 整个查询的超时时间
    GET /logs/_search
    {
    "timeout": "3s",
    "query": { "match_all": {} }
    }

    响应中会包含超时标识:

    {
    "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(游标翻页)

    适用场景:数据导出、全量遍历(不适合实时翻页,因为数据是快照)

    // 第一步:发起 Scroll 查询,创建快照(指定快照保留时间)
    POST /logs/_search?scroll=5m
    {
    "size": 1000,
    "query": { "match": { "level": "ERROR" } },
    "sort": ["_doc"] // _doc 是最快的排序方式,无需相关性计算
    }

    // 响应中会返回 scroll_id
    {
    "_scroll_id": "FGluY2x1ZGVfY29udGV4dF…",
    "hits": { }
    }

    // 第二步:用 scroll_id 翻页(无需指定 from)
    POST /_search/scroll
    {
    "scroll": "5m", // 每次翻页都要续期
    "scroll_id": "FGluY2x1ZGVfY29udGV4dF…"
    }

    // 第三步:使用完毕后及时清理(释放内存)
    DELETE /_search/scroll
    {
    "scroll_id": ["FGluY2x1ZGVfY29udGV4dF…"]
    }

    // 清理所有 scroll
    DELETE /_search/scroll/_all

    Scroll 缺点:

    • 快照机制:查询期间新写入的数据不可见
    • 消耗集群内存:每个 scroll 上下文占用资源
    • 无法跳页:只能顺序翻页
    • 不支持实时搜索场景

    6.3 解决方案二:Search After(推荐,实时翻页)

    适用场景:实时翻页、无限下拉加载(生产环境首选)

    核心思想:用上一页最后一条文档的排序值作为下一页的起点,完全规避 from 的性能问题。

    // 第一页:必须指定唯一的排序字段(通常包含 _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"] // ← 记录这个值
    }
    ]
    }
    }

    // 第二页:用上一页最后一条的 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+sizescrollsearch_after
    支持跳页
    实时数据 ❌(快照)
    深度翻页性能
    内存占用 高(快照)
    适合场景 前几页 数据导出 无限翻页

    6.4 解决方案三:Point In Time(PIT)+ Search After(最新最佳实践)

    ES 7.10+ 引入 PIT(时间点快照),结合 search_after 可实现:既有实时翻页的效率,又保证同一次翻页会话数据一致性。

    // 第一步:创建 PIT 快照
    POST /orders/_pit?keep_alive=5m

    // 响应
    { "id": "46ToAwMDaWR5BXV1…" }

    // 第二步:使用 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"
    }
    }

    // 第三步:翻页完毕后删除 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+)

    赞(0)
    未经允许不得转载:171主机测评 » 【ElasticSearch从入门到架构师】第5章:ES DSL 检索语法精讲(核心重点)
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址