← 返回文章列表

Elasticsearch 底层原理系列(五):查询执行原理

2020-11-18·5 分钟阅读

前言

Elasticsearch 的强大之处在于其灵活的查询能力和智能的评分机制。理解查询执行原理,对于编写高效查询语句和优化搜索性能至关重要。

本章将深入解析 ES 的查询执行流程、评分机制以及查询优化策略。

技术亮点

技术点难度面试价值本文覆盖
查询执行流程⭐⭐⭐高频考点
BM25 评分算法⭐⭐⭐⭐高频考点
Query vs Filter⭐⭐⭐高频考点
查询优化策略⭐⭐⭐⭐实战价值

面试考点

  1. Elasticsearch 查询执行的完整流程是什么?
  2. BM25 评分算法的原理是什么?
  3. Query 和 Filter 有什么区别?什么时候使用 Filter?
  4. 如何优化 ES 查询性能?

查询执行流程

整体架构

┌─────────────────────────────────────────────────────────────────────────┐
│                        查询执行流程                                      │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  Client                                                                 │
│     │                                                                   │
│     │  1. 发送查询请求                                                  │
│     ▼                                                                   │
│  ┌─────────────┐                                                       │
│  │ Coordinating│                                                       │
│  │    Node     │ ─── 解析查询、路由分发、结果聚合                        │
│  └──────┬──────┘                                                       │
│         │                                                               │
│         │  2. 广播查询到相关分片                                        │
│         │                                                               │
│         ├────────────────┬────────────────┐                            │
│         ▼                ▼                ▼                            │
│    ┌─────────┐      ┌─────────┐      ┌─────────┐                      │
│    │ Shard 0 │      │ Shard 1 │      │ Shard 2 │                      │
│    │         │      │         │      │         │                      │
│    │ Query   │      │ Query   │      │ Query   │                      │
│    │ Fetch   │      │ Fetch   │      │ Fetch   │                      │
│    └────┬────┘      └────┬────┘      └────┬────┘                      │
│         │                │                │                            │
│         │  3. 返回 Top N 结果             │                            │
│         ▼                ▼                ▼                            │
│         └────────────────┴────────────────┘                            │
│                          │                                              │
│                          ▼                                              │
│                   ┌─────────────┐                                      │
│                   │  协调节点    │                                      │
│                   │  全局排序    │                                      │
│                   │  分页处理    │                                      │
│                   └──────┬──────┘                                      │
│                          │                                              │
│                          ▼                                              │
│                    返回给 Client                                        │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

两阶段查询

ES 的查询分为两个阶段:Query 阶段Fetch 阶段

┌─────────────────────────────────────────────────────────────────────────┐
│                        两阶段查询详解                                    │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  Phase 1: Query 阶段(轻量)                                            │
│  ┌─────────────────────────────────────────────────────────────────┐   │
│  │                                                                 │   │
│  │  目的:找出匹配的文档 ID 和分数                                  │   │
│  │                                                                 │   │
│  │  1. 协调节点解析查询语句                                         │   │
│  │  2. 向所有相关分片发送查询请求                                   │   │
│  │  3. 每个分片本地执行查询                                         │   │
│  │     • 遍历倒排索引                                              │   │
│  │     • 计算相关性分数                                            │   │
│  │     • 排序并返回 Top N 的 (DocID, Score)                        │   │
│  │  4. 协调节点合并各分片结果                                       │   │
│  │     • 全局排序                                                  │   │
│  │     • 取出 from + size 个文档 ID                                │   │
│  │                                                                 │   │
│  │  返回数据:仅 DocID + Score(数据量小)                          │   │
│  │                                                                 │   │
│  └─────────────────────────────────────────────────────────────────┘   │
│                              │                                          │
│                              ▼                                          │
│  Phase 2: Fetch 阶段(按需)                                            │
│  ┌─────────────────────────────────────────────────────────────────┐   │
│  │                                                                 │   │
│  │  目的:获取完整的文档内容                                        │   │
│  │                                                                 │   │
│  │  1. 协调节点根据 DocID 路由到对应分片                            │   │
│  │  2. 从分片获取完整的 _source                                    │   │
│  │  3. 可能包含:高亮、字段、脚本处理等                             │   │
│  │  4. 返回给客户端                                                 │   │
│  │                                                                 │   │
│  │  优化:只对最终结果执行 Fetch,减少网络传输                      │   │
│  │                                                                 │   │
│  └─────────────────────────────────────────────────────────────────┘   │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

Query 阶段详解

┌─────────────────────────────────────────────────────────────────────────┐
│                    Query 阶段执行过程                                    │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  单个分片的 Query 执行:                                                 │
│                                                                         │
│  ┌─────────────────────────────────────────────────────────────────┐   │
│  │                                                                 │   │
│  │  1. 查询解析                                                    │   │
│  │     Query DSL → Lucene Query 对象                              │   │
│  │                                                                 │   │
│  │  2. 创建 Weight(权重计算器)                                   │   │
│  │     每个 Query 创建对应的 Weight                                │   │
│  │                                                                 │   │
│  │  3. 创建 Scorer(评分器)                                       │   │
│  │     遍历所有 Segment,创建 Scorer                               │   │
│  │                                                                 │   │
│  │  4. 收集结果                                                    │   │
│  │     Collector 收集匹配的 DocID 和 Score                        │   │
│  │                                                                 │   │
│  │     ┌───────────────────────────────────────────────────┐      │   │
│  │     │ Segment 1    │ Segment 2    │ Segment N          │      │   │
│  │     │ Scorer       │ Scorer       │ Scorer             │      │   │
│  │     │   │          │   │          │   │                │      │   │
│  │     │   ▼          │   ▼          │   ▼                │      │   │
│  │     │ Collector ◄──┴──────────────┴────────────────────┘      │   │
│  │     │   │                                                    │      │   │
│  │     │   ▼                                                    │      │   │
│  │     │ Top N Results                                          │      │   │
│  │     └───────────────────────────────────────────────────────┘      │   │
│  │                                                                 │   │
│  └─────────────────────────────────────────────────────────────────┘   │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

BM25 评分算法

TF-IDF vs BM25

┌─────────────────────────────────────────────────────────────────────────┐
│                    评分算法演进                                          │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  TF-IDF(ES 5.0 之前)                                                  │
│  ┌─────────────────────────────────────────────────────────────────┐   │
│  │                                                                 │   │
│  │  TF (Term Frequency): 词频                                     │   │
│  │  TF = 词在文档中出现次数 / 文档总词数                           │   │
│  │                                                                 │   │
│  │  IDF (Inverse Document Frequency): 逆文档频率                  │   │
│  │  IDF = log(总文档数 / 包含该词的文档数)                         │   │
│  │                                                                 │   │
│  │  Score = TF × IDF                                              │   │
│  │                                                                 │   │
│  │  问题:                                                         │   │
│  │  • TF 无上限,长文档优势过大                                    │   │
│  │  • 评分曲线不合理                                               │   │
│  │                                                                 │   │
│  └─────────────────────────────────────────────────────────────────┘   │
│                              │                                          │
│                              ▼ ES 5.0+                                  │
│  BM25(当前默认)                                                        │
│  ┌─────────────────────────────────────────────────────────────────┐   │
│  │                                                                 │   │
│  │  Score = IDF × (tf × (k1 + 1)) / (tf + k1 × (1 - b + b × dl/avgdl))│ │
│  │                                                                 │   │
│  │  参数说明:                                                     │   │
│  │  • tf: 词频                                                    │   │
│  │  • k1: 饱和参数(默认 1.2),控制 TF 饱和速度                   │   │
│  │  • b: 长度归一化参数(默认 0.75),控制文档长度影响              │   │
│  │  • dl: 当前文档长度                                            │   │
│  │  • avgdl: 平均文档长度                                         │   │
│  │                                                                 │   │
│  │  改进:                                                         │   │
│  │  • TF 有饱和上限,避免长文档优势过大                            │   │
│  │  • 考虑文档长度归一化                                          │   │
│  │  • 更符合实际相关性                                            │   │
│  │                                                                 │   │
│  └─────────────────────────────────────────────────────────────────┘   │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

BM25 评分曲线

┌─────────────────────────────────────────────────────────────────────────┐
│                    BM25 评分曲线示意                                     │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  Score                                                                  │
│    │                                                                    │
│    │         TF-IDF                                                     │
│    │            ╲                                                       │
│    │             ╲                                                      │
│    │              ╲                                                     │
│    │               ╲                                                    │
│    │                ╲───────                                            │
│    │                        ╲                                           │
│    │                         ──────                                     │
│    │                                                                    │
│    │       BM25 (饱和曲线)                                              │
│    │            ╱────────────────────────────                           │
│    │           ╱                                                        │
│    │          ╱                                                         │
│    │         ╱                                                          │
│    │        ╱                                                           │
│    │───────╱───────────────────────────────────────── TF               │
│    │      1   2   3   4   5   6   7   8   9   10                       │
│    │                                                                    │
│    BM25 特点:随着 TF 增加,评分趋近饱和,而非无限增长                  │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

自定义评分

// 使用 Function Score 自定义评分
{
  "query": {
    "function_score": {
      "query": {
        "match": { "title": "elasticsearch" }
      },
      "functions": [
        {
          "filter": { "term": { "status": "published" } },
          "weight": 2
        },
        {
          "field_value_factor": {
            "field": "likes",
            "factor": 1.2,
            "modifier": "sqrt",
            "missing": 1
          }
        },
        {
          "gauss": {
            "publish_date": {
              "origin": "now",
              "scale": "30d",
              "decay": 0.5
            }
          }
        }
      ],
      "score_mode": "sum",
      "boost_mode": "multiply"
    }
  }
}

Query vs Filter

核心区别

┌─────────────────────────────────────────────────────────────────────────┐
│                    Query vs Filter 对比                                  │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  ┌───────────────────────────┬───────────────────────────┐             │
│  │          Query            │          Filter           │             │
│  ├───────────────────────────┼───────────────────────────┤             │
│  │ 计算相关性分数            │ 不计算分数,只判断匹配    │             │
│  ├───────────────────────────┼───────────────────────────┤             │
│  │ 结果按分数排序            │ 结果无特定顺序            │             │
│  ├───────────────────────────┼───────────────────────────┤             │
│  │ 不缓存结果                │ 缓存结果(加速后续查询)  │             │
│  ├───────────────────────────┼───────────────────────────┤             │
│  │ 适用于全文搜索            │ 适用于精确匹配、范围查询  │             │
│  ├───────────────────────────┼───────────────────────────┤             │
│  │ 较慢                      │ 较快                      │             │
│  └───────────────────────────┴───────────────────────────┘             │
│                                                                         │
│  使用场景:                                                             │
│  • Query: 全文搜索、相关性排序                                          │
│  • Filter: 状态筛选、日期范围、精确匹配                                  │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

Filter 缓存机制

┌─────────────────────────────────────────────────────────────────────────┐
│                    Filter 缓存原理                                       │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  Filter 查询执行流程:                                                   │
│                                                                         │
│  ┌─────────────────────────────────────────────────────────────────┐   │
│  │                                                                 │   │
│  │  1. 检查缓存                                                    │   │
│  │     ┌─────────────────────────────────────┐                    │   │
│  │     │ Filter Cache                        │                    │   │
│  │     │ ┌─────────────────────────────────┐ │                    │   │
│  │     │ │ status:published → [1,3,5,7]    │ │                    │   │
│  │     │ │ category:tech    → [2,4,6,8]    │ │                    │   │
│  │     │ │ date:[2023-01 TO 2023-12] → [...]│ │                    │   │
│  │     │ └─────────────────────────────────┘ │                    │   │
│  │     └─────────────────────────────────────┘                    │   │
│  │                    │                                            │   │
│  │                    ▼                                            │   │
│  │  2. 命中缓存:直接返回 DocID 列表                               │   │
│  │     未命中:执行查询并缓存结果                                   │   │
│  │                                                                 │   │
│  │  3. 位集(BitSet)存储                                          │   │
│  │     • 每个 Segment 一个 BitSet                                  │   │
│  │     • 位为 1 表示匹配,0 表示不匹配                             │   │
│  │     • 内存占用:文档数 / 8 字节                                  │   │
│  │                                                                 │   │
│  └─────────────────────────────────────────────────────────────────┘   │
│                                                                         │
│  缓存淘汰:LRU 策略,默认占用堆内存 10%                                  │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

Bool 查询组合

// 最佳实践:Filter 放在 bool.filter 中
{
  "query": {
    "bool": {
      "must": [
        { "match": { "title": "elasticsearch" } }
      ],
      "filter": [
        { "term": { "status": "published" } },
        { "range": { "publish_date": { "gte": "2023-01-01" } } }
      ]
    }
  }
}

查询优化策略

1. 使用 Filter 代替 Query

// ❌ 不推荐:所有条件都计算分数
{
  "query": {
    "bool": {
      "must": [
        { "match": { "title": "search" } },
        { "term": { "status": "published" } },
        { "range": { "date": { "gte": "2023-01-01" } } }
      ]
    }
  }
}

// ✅ 推荐:精确匹配用 Filter
{
  "query": {
    "bool": {
      "must": [
        { "match": { "title": "search" } }
      ],
      "filter": [
        { "term": { "status": "published" } },
        { "range": { "date": { "gte": "2023-01-01" } } }
      ]
    }
  }
}

2. 避免深度分页

┌─────────────────────────────────────────────────────────────────────────┐
│                    深度分页问题                                          │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  问题:from + size 过大                                                 │
│                                                                         │
│  示例:from=10000, size=10                                              │
│  • 每个分片需要返回 10010 条数据                                        │
│  • 协调节点需要处理 分片数 × 10010 条数据                               │
│  • 可能导致内存溢出                                                     │
│                                                                         │
│  解决方案:                                                              │
│                                                                         │
│  1. Scroll API(适合全量导出)                                          │
│     POST /my_index/_search?scroll=1m                                   │
│     { "size": 100, "query": { ... } }                                  │
│                                                                         │
│  2. Search After(适合实时深度分页)                                     │
│     {                                                                  │
│       "size": 10,                                                      │
│       "query": { ... },                                                │
│       "sort": [                                                        │
│         { "date": "desc" },                                            │
│         { "_id": "asc" }                                               │
│       ],                                                               │
│       "search_after": ["2023-08-30", "doc_123"]                        │
│     }                                                                  │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

3. 合理使用 _source 过滤

// ✅ 只返回需要的字段
{
  "_source": ["title", "author", "date"],
  "query": { ... }
}

// ✅ 排除不需要的字段
{
  "_source": {
    "excludes": ["content", "raw_data"]
  },
  "query": { ... }
}

4. 使用批量查询

// ✅ 使用 _msearch 批量查询
GET /_msearch
{ "index": "index1" }
{ "query": { "match_all": {} } }
{ "index": "index2" }
{ "query": { "match": { "title": "test" } } }

5. 预热缓存

// 预热特定查询
POST /my_index/_cache/clear
POST /my_index/_search
{
  "query": { ... },
  "request_cache": true
}

慢查询分析

开启慢查询日志

# elasticsearch.yml
index.search.slowlog.threshold.query.warn: 10s
index.search.slowlog.threshold.query.info: 5s
index.search.slowlog.threshold.query.debug: 2s
index.search.slowlog.threshold.fetch.warn: 1s

分析查询性能

// 使用 Profile API 分析查询
GET /my_index/_search
{
  "profile": true,
  "query": {
    "match": { "title": "elasticsearch" }
  }
}

总结

本章深入解析了 ES 查询执行原理:

要点说明
两阶段查询Query(找ID)→ Fetch(取文档)
BM25 评分TF 饱和、文档长度归一化
Filter 优化缓存结果、不计算分数
深度分页使用 Scroll 或 Search After
性能优化合理使用 Filter、Source 过滤、Profile 分析

参考资料

下一章预告

下一章将探讨 集群架构与节点角色,包括:

  • Master/Data/Coordinating 节点职责
  • 集群状态管理
  • 节点角色配置最佳实践
分享: