上一篇【第44篇】Elasticsearch分布式索引原理——分片路由与写入流程下一篇【第46篇】Elasticsearch分布式文档更新原理摘要分布式检索是Elasticsearch应对海量数据场景的核心能力但其原理远比单机搜索复杂。本文以Query Then Fetch两阶段模型为主线首先详解Query阶段协调节点如何将搜索请求广播到所有分片各分片独立执行本地搜索后返回Top N文档的ID与分数协调节点再合并全局排序然后深入Fetch阶段协调节点如何根据全局Top N结果向目标分片发起MultiGet请求获取完整文档内容。通过完整的API示例和ASCII流程图读者将透彻理解两阶段搜索的数据流向和网络开销。此外本文还深入探讨分布式环境下的相关性评分问题——各分片独立统计IDF导致的评分不一致以及DFS搜索类型的解决策略与代价。关键词Elasticsearch分布式搜索、Query Then Fetch、DFS搜索、相关性评分、两阶段搜索。一、分布式检索的挑战在单机环境下搜索过程非常直观Lucene在本地索引中查找匹配文档并按照相关性评分排序返回。但在分布式环境下索引被拆分成了多个分片分散在不同的节点上。这就引出了一个核心问题如何在一个由多个分片组成的分布式索引中执行搜索并返回全局正确的排序结果简单方案是不可行的——我们不能简单地将所有分片的数据全部拉取到协调节点再排序因为全网数据量可能达到TB级别全量数据传输会直接耗尽网络带宽和内存。Elasticsearch采用了一种精巧的两阶段策略Query Then Fetch。二、Query Then Fetch 两阶段搜索模型2.1 架构总览┌──────────────────────────┐ │ 客户端 (Client) │ └───────────┬──────────────┘ │ ① 发送搜索请求 ▼ ┌──────────────────────────┐ │ 协调节点 (Node 1) │ │ - 接收请求 │ │ - 路由分发 │ │ - 结果合并 │ │ - 全局排序 │ └────┬───────┬───────┬─────┘ ②广播│ │ │②广播 ┌──────▼──┐ ┌──▼──────┐ ┌──▼──────┐ │ Node 2 │ │ Node 3 │ │ Node 4 │ │ Shard 0 │ │ Shard 1 │ │ Shard 2 │ │ (主分片) │ │ (主分片) │ │ (主分片) │ └─────────┘ └─────────┘ └─────────┘ │ │ │ ③返回 ③返回 ③返回 top N top N top N (ID分数) (ID分数) (ID分数) │ │ │ └─────┬─────┴─────┬─────┘ │ │ ▼ │ ┌────────────────────┴─────┐ │ 协调节点 (Node 1) │ │ ④ 全局归并排序 │ │ ⑤ 确定最终Top N │ └──────────┬───────────────┘ │ ⑥ 向目标分片请求完整文档 ▼ ┌──────────┴───────────────┐ │ 协调节点 → 目标分片 │ │ ⑦ Fetch阶段 │ │ - 获取完整_source │ │ - 组装最终结果 │ └──────────┬───────────────┘ │ ⑧ 返回结果 ▼ ┌──────────────────────────┐ │ 客户端 (Client) │ └──────────────────────────┘2.2 两阶段的核心设计哲学Query Then Fetch模型的设计目标是在保证结果正确性的前提下最小化网络传输开销Query阶段只传输文档ID和评分几十字节不传输完整的_source可能几KB到几MBFetch阶段仅对最终确定需要返回的少量文档获取完整内容这样分片→协调节点→目标分片的两轮通信虽然增加了请求次数但极大地减少了数据传输总量。三、Query阶段详解3.1 步骤分解Query阶段完成从用户查询到全局排序文档ID列表的转换。具体包括以下子步骤Query阶段内部流程 ① 协调节点接收查询请求 - 解析DSL查询语句 - 确定搜索涉及的所有分片主分片或副本分片的活跃列表 ② 协调节点向每个目标分片广播查询请求 - 每个分片收到的是一个完整的、独立的查询 - 请求中包含 sizefromsize确保获取足够多结果 ③ 每个分片独立执行本地搜索 ┌──────────────────────────────────────────┐ │ 分片本地搜索步骤 │ │ a. 查询语法分析Query Parsing │ │ b. 倒排索引查找Term Lookup │ │ c. 文档列表合并AND/OR/交集/并集 │ │ d. 相关性评分计算独立计算可能不准确 │ │ e. 返回top(fromsize)个文档的ID评分 │ └──────────────────────────────────────────┘ ④ 协调节点收集所有分片的返回结果 - 假设有5个分片每个返回fromsize20个文档 - 协调节点收到 5×20100 个文档的ID评分 ⑤ 协调节点进行全局归并排序 - 将100个文档按评分降序排列 - 截取最终的fromsize条如第0-9条3.2 Query阶段的代码体现// 发起一个跨分片搜索请求GET/shop/_search{from:0,size:10,query:{match:{title:iPhone 14}},sort:[{_score:desc}]}// 在这个查询中// - 如果shop索引有5个主分片// - Query阶段每个分片返回 top 10 个文档的(_id _score)// - 协调节点收集 5×1050 个文档// - 全局排序后取前10个// - 进入Fetch阶段获取这10个文档的完整内容3.3 Query阶段的关键细节每个分片返回多少文档默认情况下每个分片返回from size条结果。例如查询from90, size10时即第10页每页10条每个分片返回100条。这就引出了深度分页的性能问题当我们请求第1000页时每个分片需要返回(1000-1)×10 10 10000条协调节点需要处理5×10000 50000条数据的内存排序两个重要的限制参数PUT/shop/_settings{index.max_result_window:10000// 默认为10000超过需用search_after}四、Fetch阶段详解4.1 步骤分解Query阶段结束后协调节点已确定了最终需要返回的文档ID列表。Fetch阶段的任务就是去获取这些文档的完整内容。Fetch阶段内部流程 ① 协调节点确定目标文档分片分布 - 从Query阶段结果中得到最终的N个文档ID - 通过路由公式反算每个文档所在的分片 ② 协调节点向目标分片发起MultiGet请求 ┌────────────────────────────────────────┐ │ 按分片聚合请求高效批量获取 │ │ │ │ → Node2 (Shard 0): [doc_3, doc_7, doc_15] │ → Node3 (Shard 1): [doc_1, doc_9] │ │ → Node4 (Shard 2): [doc_2, doc_5, doc_6] │ └────────────────────────────────────────┘ ③ 各目标分片根据文档ID返回完整文档 - 返回_source字段、stored fields等 ④ 协调节点组装最终响应 - 按排序顺序排列文档 - 添加hit元数据_index, _id, _score等 - 返回给客户端4.2 两阶段的网络开销分析假设5个分片查询from0, size10每个文档_source约1KB每个文档ID分数约50字节Query阶段网络传输 分片→协调节点: 5 × 10 × 50字节 2.5KB Fetch阶段网络传输 协调节点→分片: 请求10个文档ID ≈ 300字节 分片→协调节点: 10 × 1KB 10KB 总网络传输: 约12.8KB 如果不分两阶段每个分片返回完整文档: 分片→协调节点: 5 × 10 × 1KB 50KB近4倍差距随着分片数增加或文档体积增大两阶段优化带来的节省效果更加明显。五、分布式相关性评分问题5.1 问题根源各分片独立统计IDFQuery阶段每个分片独立计算BM25评分。问题在于BM25中的IDF逆文档频率需要知道全局的文档频率统计IDF(qi) log(1 (N - n(qi) 0.5) / (n(qi) 0.5)) 其中 N 总文档数, n(qi) 包含词qi的文档数在分布式环境下每个分片只有自己那部分数据的统计信息全局索引: 总共100万文档Elasticsearch出现1000次 → IDF log(1 (1000000 - 1000 0.5) / (1000 0.5)) ≈ log(1000) ≈ 6.9 Shard 0 (30万文档): Elasticsearch出现800次 → IDF log(1 (300000 - 800 0.5) / (800 0.5)) ≈ log(375) ≈ 5.9 ← 偏低 Shard 1 (30万文档): Elasticsearch出现100次 → IDF log(1 (300000 - 100 0.5) / (100 0.5)) ≈ log(3000) ≈ 8.0 ← 偏高 Shard 2 (40万文档): Elasticsearch出现100次 → IDF log(1 (400000 - 100 0.5) / (100 0.5)) ≈ log(4000) ≈ 8.3 ← 更高结果是同一个文档在不同分片上的评分因为IDF的差异而不一致。当文档不均匀分布时实际生产环境几乎必然如此最终全局排序可能出现偏差——来自小IDF分片的文档得分可能系统性地偏低。5.2 问题场景示例假设搜索词Elasticsearch Shard A: 1000篇文档其中800篇包含Elasticsearch → IDF很低词很常见 Shard B: 1000篇文档其中10篇包含Elasticsearch → IDF很高词很稀有 Shard A中一篇标题为Elasticsearch入门的文档 - 本地得分: 3.5因为本地IDF低 Shard B中一篇标题为Elasticsearch入门的文档 - 本地得分: 8.2因为本地IDF高 全局排序时Shard B的文档可能排在Shard A前面 尽管两篇文档的相关性实际上是相同的。5.3 影响程度评估数据分布情况IDF偏差程度对排序的影响文档均匀分布★☆☆☆☆几乎无影响各分片IDF接近按时间分片日志★★☆☆☆轻微影响IDF随时间波动按业务线分片routing★★★★☆较大影响相同词在不同分片频率差异大小样本查询★★★☆☆样本足够大时影响可忽略六、DFS Query Then Fetch解决评分不一致6.1 DFS搜索类型DFSDistributed Frequency Search是Elasticsearch提供的另一种搜索方式通过在Query阶段之前先执行一次预查询获取全局的词频统计信息然后传递给各分片用于准确的IDF计算。DFS Query Then Fetch 执行流程 ┌─────────────────────────────────────────────┐ │ 预Query阶段DFS Phase │ │ ① 协调节点向所有分片请求各Term的文档频率 │ │ ② 各分片返回本地频率统计 │ │ ③ 协调节点汇总为全局频率统计 │ └────────────────┬────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────┐ │ Query阶段使用全局IDF │ │ ④ 协调节点携带全局频率统计广播Query到各分片 │ │ ⑤ 各分片使用全局频率计算准确的IDF和评分 │ │ ⑥ 返回文档ID 准确评分 │ └────────────────┬────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────┐ │ Fetch阶段与普通Query Then Fetch相同 │ │ ⑦ 协调节点全局排序后获取完整文档 │ └─────────────────────────────────────────────┘6.2 三种搜索类型对比搜索类型网络往返次数评分准确性性能开销适用场景query_then_fetch2次Query Fetch★★☆☆☆★★★★★最优默认类型日常搜索dfs_query_then_fetch3次DFS Query Fetch★★★★★★★★☆☆相关性要求极高的场景query_and_fetch仅单个分片时1次★★★★★★★★★★仅一个分片时自动启用6.3 使用DFS搜索// 使用DFS搜索类型GET/shop/_search?search_typedfs_query_then_fetch{query:{match:{title:Elasticsearch}}}// 也可以通过_profile分析各阶段的耗时GET/shop/_search?search_typedfs_query_then_fetch{profile:true,query:{match:{title:Elasticsearch}}}6.4 何时使用DFS建议使用DFS的场景文档在不同分片上分布极不均匀如按业务线routing搜索结果较少100条文档频率统计差异被放大相关性排序是业务核心指标如电商搜索、文档检索无需使用DFS的场景文档在各分片均匀分布日志搜索、数据分析等不需要精确相关性排序大数据量搜索DFS的额外网络开销不值得使用自定义排序或function_score的场景七、深度分页与替代方案7.1 深度分页的问题Query Then Fetch模型在处理深度分页时会遇到严重的性能瓶颈第1000页 (from9990, size10) 每个分片需要返回10000 个文档ID评分 5个分片协调节点合并 50000 个文档 内存压力极大CPU归并排序开销高 根本原因Query阶段必须保证前面的文档不会在后续跳出来7.2 search_after深度分页的推荐方案// 使用 search_after 替代深度分页GET/shop/_search{size:10,query:{match:{title:iPhone 14}},sort:[{price:asc},{_id:asc}],search_after:[5999,doc_123]// 上一页最后一条的sort值}方案适用深度性能实时性要求fromsize10000浅分页优秀深分页糟糕支持跳页search_after无限制始终高效不支持跳页scroll无限制高效快照模式不实时八、总结与最佳实践核心要点回顾Query Then Fetch两阶段模型通过先传ID分数再取完整文档的设计实现了网络传输和内存使用的双重优化是Elasticsearch分布式搜索的基石。Query阶段各分片独立搜索并返回Top N文档ID协调节点全局归并排序Fetch阶段仅对最终保留的少量文档进行MultiGet获取。分布式IDF不一致是Query Then Fetch模型的固有问题每个分片独立统计词频导致评分偏差数据分布越不均匀问题越明显。DFS搜索通过增加一次预查询获取全局词频以额外的一次网络往返为代价换取准确的评分适用于相关性要求高的场景。最佳实践清单实践建议详细说明默认使用query_then_fetch绝大多数场景下性能最优保持默认设置相关性敏感场景启用DFS电商搜索、知识库检索等场合适用dfs_query_then_fetch避免深度分页fromsize之和不宜超过10000深翻场景使用search_after监控profile输出使用profile: true分析各阶段耗时分布副本分片分担查询搜索负载高时增加副本数利用副本分片并行搜索preference参数控制使用preference参数绑定用户到固定分片用于A/B测试或缓存上一篇【第44篇】Elasticsearch分布式索引原理——分片路由与写入流程下一篇【第46篇】Elasticsearch分布式文档更新原理