Elasticsearch 底层原理系列(五):查询执行原理
2020-11-18·5 分钟阅读
前言
Elasticsearch 的强大之处在于其灵活的查询能力和智能的评分机制。理解查询执行原理,对于编写高效查询语句和优化搜索性能至关重要。
本章将深入解析 ES 的查询执行流程、评分机制以及查询优化策略。
技术亮点
| 技术点 | 难度 | 面试价值 | 本文覆盖 |
|---|---|---|---|
| 查询执行流程 | ⭐⭐⭐ | 高频考点 | ✅ |
| BM25 评分算法 | ⭐⭐⭐⭐ | 高频考点 | ✅ |
| Query vs Filter | ⭐⭐⭐ | 高频考点 | ✅ |
| 查询优化策略 | ⭐⭐⭐⭐ | 实战价值 | ✅ |
面试考点
- Elasticsearch 查询执行的完整流程是什么?
- BM25 评分算法的原理是什么?
- Query 和 Filter 有什么区别?什么时候使用 Filter?
- 如何优化 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 节点职责
- 集群状态管理
- 节点角色配置最佳实践
相关文章
Elasticsearch 底层原理系列(九):生产实践与性能调优
2020-12-31·6 分钟阅读
深入解析 Elasticsearch 生产环境最佳实践,包括硬件选型、JVM 调优、索引设计、性能监控与调优、常见问题排查。
Elasticsearch 底层原理系列(八):分布式一致性
2020-12-20·5 分钟阅读
深入解析 Elasticsearch 分布式一致性机制,包括 Master 选举、脑裂问题、故障检测与恢复、以及数据一致性保证。
Elasticsearch 底层原理系列(七):分片与路由机制
2020-12-13·5 分钟阅读
深入解析 Elasticsearch 分片路由机制,包括路由算法、自定义路由、分片数量规划、以及分片重平衡策略。