当前位置: 首页 > news >正文

深度解析 Elasticsearch 搜索过程:Query Then Fetch 两阶段详解

深度解析 Elasticsearch 搜索过程:Query Then Fetch 两阶段详解

    • 前言
    • 一、搜索流程全景图
      • 1.1 两阶段概览
      • 1.2 为什么需要两个阶段?
    • 二、示例集群环境
    • 三、第一阶段:Query 阶段
      • 3.1 步骤一:协调节点广播请求
      • 3.2 步骤二:每个分片本地查询
      • 3.3 步骤三:协调节点合并排序
    • 四、第二阶段:Fetch 阶段
      • 4.1 步骤一:协调节点发起 MultiGet 请求
      • 4.2 步骤二:分片返回完整文档
      • 4.3 步骤三:协调节点组装并返回
    • 五、深度剖析:关键设计细节
      • 5.1 为什么 Query 阶段返回 `from+size` 条而非只返回 `size` 条?
      • 5.2 深度分页问题
      • 5.3 分片选择策略(主 vs 副本)
      • 5.4 搜索类型(已废弃)
    • 六、完整时序图
    • 七、性能优化建议
    • 八、常见面试题
      • Q1:Query 阶段和 Fetch 阶段各自做了什么?
      • Q2:为什么需要两个阶段,不能合二为一?
      • Q3:什么是深度分页问题?如何解决?
      • Q4:协调节点如何决定查询主分片还是副本分片?
    • 九、总结
    • 十、面试加分回答

)
🌺The Begin🌺点点关注,收藏不迷路🌺

前言

搜索是 Elasticsearch 最核心的功能之一,但很多开发者对 ES 内部如何执行搜索请求一知半解。为什么搜索分为两个阶段?协调节点做了什么?分片如何返回结果?本文将围绕官方定义的“Query Then Fetch”两阶段模型,逐步拆解分布式搜索的完整流程。


一、搜索流程全景图

1.1 两阶段概览

┌─────────────────────────────────────────────────────────────────────┐ │ 客户端发送搜索请求 │ │ GET /my_index/_search?q=keyword │ └─────────────────────────────────┬───────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────┐ │ 【第一阶段:Query 阶段】 │ │ 目的:定位到哪些文档匹配,但不返回文档内容 │ │ │ │ 1. 协调节点将请求广播到所有分片(主或副本) │ │ 2. 每个分片本地查询,返回文档ID + 排序值到优先队列 │ │ 3. 协调节点合并各分片结果,生成全局排序列表 │ └─────────────────────────────────┬───────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────┐ │ 【第二阶段:Fetch 阶段】 │ │ 目的:根据 Query 阶段得到的文档ID,获取完整的文档内容 │ │ │ │ 1. 协调节点向相关分片发送 MultiGet 请求 │ │ 2. 分片返回完整文档内容 │ │ 3. 协调节点组装结果,返回给客户端 │ └─────────────────────────────────────────────────────────────────────┘

1.2 为什么需要两个阶段?

如果合并为一个阶段两阶段分离的好处
每个分片返回完整文档 → 大量网络传输Query 阶段只传ID和排序值,数据量极小
协调节点需要丢弃超出size的文档Fetch 阶段只取最终需要的文档
无法做全局排序先全局排序,再按需获取

一句话总结先定位,后取数,避免网络和内存浪费。


二、示例集群环境

为便于理解,设定以下集群状态:

配置项
索引名my_index
主分片数5
副本数1
总分片数10(5主 + 5副本)
文档总数10,000 条

搜索请求

GET/my_index/_search{"from":0,"size":10,"query":{"match":{"title":"elasticsearch"}},"sort":[{"_score":"desc"}]}

三、第一阶段:Query 阶段

3.1 步骤一:协调节点广播请求

┌─────────────────┐ │ 客户端 │ └────────┬────────┘ │ ▼ ┌─────────────────┐ │ 协调节点 │ │ (接收搜索请求) │ └────────┬────────┘ │ ┌──────────────┬───────────────┼───────────────┬──────────────┐ │ │ │ │ │ ▼ ▼ ▼ ▼ ▼ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ 分片0 │ │ 分片1 │ │ 分片2 │ │ 分片3 │ │ 分片4 │ │ (主/副本) │ │ (主/副本) │ │ (主/副本) │ │ (主/副本) │ │ (主/副本) │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘

关键要点

  • 协调节点向所有分片发送请求(每个分片只命中主或副本中的一个)
  • 采用轮询策略选择主还是副本(负载均衡)
  • 请求参数完全相同(query、from、size、sort)

3.2 步骤二:每个分片本地查询

每个分片收到请求后,独立执行以下操作:

┌─────────────────────────────────────────────────────────────────┐ │ 单个分片内部处理流程 │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ 1. 解析查询语句 → 生成 Lucene Query │ │ ▼ │ │ 2. 遍历倒排索引 → 找到匹配的文档ID列表 │ │ ▼ │ │ 3. 计算每个文档的 _score(相关性评分) │ │ ▼ │ │ 4. 按排序字段排序(默认按 _score 降序) │ │ ▼ │ │ 5. 截取 [from, from+size] 范围的文档(本地优先队列) │ │ ▼ │ │ 6. 返回 (文档ID + 排序值) 给协调节点(不返回文档内容) │ │ │ └─────────────────────────────────────────────────────────────────┘

示例:假设每个分片有 2000 条匹配文档,from=0, size=10,每个分片只返回前 10 条(按排序值)。

// 每个分片返回给协调节点的数据格式{"shard":0,"hits":[{"_id":"doc_123","_score":9.5,"sort_values":[9.5]},{"_id":"doc_456","_score":9.2,"sort_values":[9.2]},...{"_id":"doc_789","_score":8.1,"sort_values":[8.1]}// 共10条]}

3.3 步骤三:协调节点合并排序

协调节点收到所有分片的返回结果后:

┌─────────────────────────────────────────────────────────────────┐ │ 协调节点合并流程 │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ 输入:5个分片 × 10条结果 = 50条候选记录 │ │ ▼ │ │ 1. 将所有50条记录放入全局优先队列 │ │ ▼ │ │ 2. 按排序规则重新排序(_score 降序) │ │ ▼ │ │ 3. 截取 [0, 10] 条(即最终需要的10条文档) │ │ ▼ │ │ 4. 记录这10条文档分别来自哪个分片 │ │ 文档A → 分片0 │ │ 文档B → 分片2 │ │ 文档C → 分片1 │ │ ... │ │ │ └─────────────────────────────────────────────────────────────────┘

此时 Query 阶段结束,协调节点知道:

  • 哪 10 条文档需要返回
  • 每条文档所在的分片位置

四、第二阶段:Fetch 阶段

4.1 步骤一:协调节点发起 MultiGet 请求

协调节点根据 Query 阶段的结果,向相关分片批量获取文档内容:

协调节点 │ ┌──────────────────┼──────────────────┐ │ │ │ ▼ ▼ ▼ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ 分片0 │ │ 分片1 │ │ 分片2 │ │ 需要文档 │ │ 需要文档 │ │ 需要文档 │ │ [A, D] │ │ [C, E] │ │ [B, F] │ └─────────┘ └─────────┘ └─────────┘
// 协调节点发送的 MultiGet 请求示例GET/_mget{"docs":[{"_index":"my_index","_id":"doc_A","_shard":"0"},{"_index":"my_index","_id":"doc_D","_shard":"0"},{"_index":"my_index","_id":"doc_C","_shard":"1"},...]}

4.2 步骤二:分片返回完整文档

每个分片从Lucene 段Translog中读取完整的文档源(_source):

// 分片返回的文档内容{"_id":"doc_A","_index":"my_index","_score":9.5,"_source":{"title":"Elasticsearch 入门教程","content":"本文介绍 Elasticsearch 的基本概念...","timestamp":"2026-01-26T10:00:00Z"}}

4.3 步骤三:协调节点组装并返回

协调节点收集所有文档,按 Query 阶段排好的顺序组装,返回给客户端:

{"took":15,"timed_out":false,"_shards":{"total":10,"successful":10,"skipped":0,"failed":0},"hits":{"total":{"value":10000,"relation":"eq"},"max_score":9.5,"hits":[{"_index":"my_index","_id":"doc_A","_score":9.5,"_source":{"title":"Elasticsearch 入门教程",...}},{"_index":"my_index","_id":"doc_B","_score":9.3,"_source":{"title":"Elasticsearch 高级查询",...}}// ... 共10条]}}

五、深度剖析:关键设计细节

5.1 为什么 Query 阶段返回from+size条而非只返回size条?

如果只返回 size 条实际做法(返回 from+size 条)
每个分片返回前 10 条每个分片返回前 50 条(from=0, size=10 → 0+10=10?等等)

纠正:实际上每个分片返回from + size条,而不是size条!

原因:假设from=90, size=10,全局第 91-100 条文档可能分散在各分片的前 100 条中。如果每个分片只返回 10 条,会丢失数据。

示例计算

from = 90, size = 10 每个分片需要返回:from + size = 100 条(本地排序后的前100条) 协调节点收到:5个分片 × 100条 = 500条候选记录 ↓ 全局排序后取 [90, 100) 共10条

5.2 深度分页问题

上述机制导致深度分页性能极差

页码from每个分片返回数协调节点处理数
第1页0105×10=50
第10页901005×100=500
第100页99010005×1000=5000
第1000页9990100005×10000=50000

解决方案

  • Search After:基于上一页最后一条文档的排序值继续查询
  • Scroll API:用于大量数据导出(不再推荐)
  • Point in Time (PIT):ES 7.10+ 引入的游标机制

5.3 分片选择策略(主 vs 副本)

协调节点决定每个分片查询主还是副本,遵循负载均衡原则:

默认策略:自适应副本选择(Adaptive Replica Selection) - 优先选择响应最快的节点 - 考虑节点历史延迟和队列长度

好处

  • 分摊主节点压力
  • 提升查询吞吐量

5.4 搜索类型(已废弃)

早期 ES 支持多种搜索类型,现已统一为query_then_fetch

搜索类型说明状态
query_and_fetch查询取回合并废弃
dfs_query_then_fetch全局词频计算废弃(默认已优化)
query_then_fetch标准两阶段当前唯一

六、完整时序图

客户端 协调节点 分片0(主) 分片1(副本) 分片2(主) ... │ │ │ │ │ │──搜索请求────▶│ │ │ │ │ │ │ │ │ │ │──Query请求───▶│ │ │ │ │──Query请求──────────────────▶│ │ │ │──Query请求──────────────────────────────────▶│ │ │ │ │ │ │ │◀──返回ID列表──│ │ │ │ │◀──返回ID列表──────────────────│ │ │ │◀──返回ID列表──────────────────────────────────│ │ │ │ │ │ │ │ (合并排序) │ │ │ │ │ │ │ │ │ │──Fetch请求────▶│ │ │ │ │──Fetch请求──────────────────▶│ │ │ │ │ │ │ │ │◀──完整文档────│ │ │ │ │◀──完整文档──────────────────│ │ │ │ │ │ │ │◀──最终结果────│ │ │ │

七、性能优化建议

优化点建议效果
控制返回字段使用_source只返回必要字段减少网络传输
避免 depth 分页使用search_after替代from/size性能提升 10-100x
合理设置分片数单分片 30-50GB避免过度分片
使用索引排序index.sort减少排序开销查询提速 30%+
开启自适应副本默认已开启负载更均衡

八、常见面试题

Q1:Query 阶段和 Fetch 阶段各自做了什么?

回答

Query 阶段:协调节点将搜索请求广播到所有分片,每个分片本地查询后返回文档ID + 排序值(不返回文档内容)。协调节点合并所有结果,生成全局排序列表,确定最终需要获取的文档ID及所在分片。

Fetch 阶段:协调节点向相关分片发送MultiGet 批量请求,获取完整文档内容(_source),组装后返回给客户端。

Q2:为什么需要两个阶段,不能合二为一?

回答

如果合为一个阶段,每个分片会返回大量完整文档,造成巨大的网络开销。例如from=990, size=10,每个分片需要返回 1000 条完整文档,5 个分片就是 5000 条,但最终只取 10 条。两阶段分离后,Query 阶段只传轻量级的 ID 和排序值(约 100 字节/条),Fetch 阶段只取最终的 10 条,效率大幅提升。

Q3:什么是深度分页问题?如何解决?

回答

深度分页指跳转到很深页码(如第 1000 页)的情况。由于 Query 阶段每个分片必须返回from+size条记录,翻页越深,协调节点处理的数据量越大(第 1000 页需要每个分片返回 10000 条,5 分片处理 50000 条)。

解决方案

  • Search After:使用上一页最后一条文档的排序值,像游标一样向前翻页
  • Point in Time (PIT):固定时间点的快照视图,结合 search_after 使用
  • Scroll API:适合大量数据导出(已不推荐用于实时搜索)

Q4:协调节点如何决定查询主分片还是副本分片?

回答

ES 7.x 后默认使用自适应副本选择(Adaptive Replica Selection):协调节点会维护每个节点的响应延迟、历史失败率、搜索线程池队列长度等指标,动态选择当前最优的节点(主或副本)发送请求。这样既实现了负载均衡,又能自动避开慢节点。


九、总结

维度Query 阶段Fetch 阶段
目的定位匹配文档获取完整内容
传输数据文档ID + 排序值完整_source
数据量小(每个分片from+size条)大(仅size条)
涉及的节点所有分片只包含最终文档的分片
关键操作倒排索引检索+本地排序Lucene 读取_source
时间复杂度O(分片数 × 查询开销)O(文档条数)

核心要点

  1. 搜索采用Query Then Fetch两阶段模型
  2. Query 阶段:坐标 + 不取→ 轻量级定位
  3. Fetch 阶段:取数 + 组装→ 按需获取
  4. 深度分页用Search After替代from/size
  5. 协调节点通过自适应副本选择实现负载均衡

十、面试加分回答

面试官:请详细描述 Elasticsearch 的搜索过程。

候选人
“ES 的搜索采用Query Then Fetch两阶段模型。

Query 阶段:协调节点将请求广播到所有分片(主或副本之一)。每个分片独立执行查询,从倒排索引中找到匹配文档,计算_score并按排序字段截取from+size条结果,只返回文档ID和排序值给协调节点。协调节点合并所有分片结果,进行全局排序,最终确定需要返回的size条文档及其所在分片。这一阶段的核心价值是轻量级传输,避免早期传输大量无用数据。

Fetch 阶段:协调节点向包含目标文档的分片发送 MultiGet 批量请求,获取完整的_source内容,按 Query 阶段排好的顺序组装,返回给客户端。

这里有一个关键设计:每个分片必须返回from+size条记录而非size条,因为全局第 N 条可能来自任意分片的前 N 条。这也导致了深度分页问题,解决方案是使用Search After + Point in Time代替传统的 from/size 翻页。

另外,ES 通过自适应副本选择来决定查询主还是副本,优先选择响应最快的节点,兼顾负载均衡和性能。”



🌺The End🌺点点关注,收藏不迷路🌺
http://www.jsqmd.com/news/704940/

相关文章:

  • 2026携程任我行卡回收平台排行榜:鼎鼎收实测第一,闲置卡处理避坑指南 - 鼎鼎收礼品卡回收
  • Python中如何快速创建全零数组_使用NumPy的zeros函数初始化内存
  • 10、FileInputStream和RandomAccessFile的源码分析和使用方法详细分析(windows操作系统,JDK8)
  • 【2026年AI DevOps分水岭】:Docker AI Toolkit全新Agent编排框架上线,支持AutoGen/MetaGPT原生集成——现在不装,下周CI/CD流水线将自动拒绝旧版镜像
  • 沃尔玛购物卡回收平台TOP榜:2026闲置商超卡安全处理实测 - 鼎鼎收礼品卡回收
  • 从LlamaDeploy到Llama-Agents:智能体工作流生产级部署实战指南
  • SpringBoot 集成 OAuth2.0 资源服务器与授权服务器
  • 解密高效PDF文本提取:3个创新方法提升工作效率
  • 魔兽世界API与宏工具实战指南:一站式开发与游戏优化方案
  • MCP 2026多租户隔离配置全链路解析,从vCPU亲和性到TLS 1.3租户证书绑定,覆盖7层隔离面
  • 2026年4月防静电地板品牌权威排名榜 TOP6(最新数据版) - 小艾信息发布
  • 风控实时特征总拖慢 RT?滑动窗口、实时计数、聚合更新到底该怎么做(可落地版)
  • [C# 开发] FolderIconFix
  • 3大突破:快速掌握XLeRobot强化学习训练实战技巧
  • 如何排查ORA-12514报错_监听程序当前无法识别连接描述符
  • OpenFace完全指南:如何快速掌握面部行为分析技术
  • 06华夏之光永存:电磁弹射+一次性火箭航天入轨方案【第六篇:电磁弹射核心电池组参数与供配电优化方案】
  • VS Code Copilot Next 配置失效?立即诊断你的自动化工作流:4类典型故障码+实时修复CLI工具(v1.3.0限时开源)
  • ncmppGui:终极免费NCM音乐解密工具完整指南
  • LightGBM核心原理与工业级应用实战指南
  • Qwen3.5-2B图文理解效果展示:复杂流程图自动解析与说明生成
  • 5分钟掌握:百度网盘直链解析工具完全手册
  • 携程任我行卡回收平台TOP榜:鼎鼎收2026闲置出行卡安全处理指南 - 鼎鼎收礼品卡回收
  • Phi-4-mini-flash-reasoning多场景:从单题求解到批量PRD分析的扩展路径
  • 网络受限环境下的OOTDiffusion虚拟试衣AI完整部署实战指南
  • AI提效Android开发全景图:从需求到上线的AI工具链
  • 如何彻底解决Windows和Office激活问题:KMS_VL_ALL_AIO完整使用方案
  • CCPC 2024 河南省赛
  • GLM-4V-9B实战体验:上传图片就能问答,小白也能轻松玩
  • Cursor Pro免费激活解决方案:三步解锁AI编程完整功能