Elasticsearch 读取/搜索文档(Read)的流程
Elasticsearch 的读取操作主要分为两种场景,流程有很大的区别:
- 根据 ID 查询文档 (Get by ID):这是最简单的“读取”操作,类似于数据库的主键查询。
- 搜索文档 (Search):这是复杂的查询操作(如关键词搜索、聚合),涉及“Query Then Fetch”两个阶段。
以下是这两种流程的详细解析。
一、 根据 ID 读取文档 (Get Workflow)
当你发送 GET /index_name/_doc/id 请求时,Elasticsearch 可以通过哈希计算直接定位到文档所在的分片。
核心公式:
(默认情况下,routing 就是文档的 ID)
详细步骤:
- 客户端请求:客户端向集群中的任意节点发送读取请求。接收请求的这个节点成为协调节点 (Coordinating Node)。
- 路由计算:协调节点根据文档 ID(或指定的 routing 值)进行哈希计算,确定该文档属于哪个主分片 (Primary Shard)。
- 负载均衡:
- 协调节点查看集群状态,找到该主分片及其所有副本分片 (Replica Shards) 所在的节点。
- 为了负载均衡,协调节点通常会使用轮询 (Round-Robin) 算法在主分片和副本分片中选择一个节点(例如,这次选副本,下次选主分片)。
- 转发请求:协调节点将请求转发给持有目标分片的节点。
- 检索数据:目标节点在本地索引中查找文档,如果找到,将文档数据返回给协调节点。
- 返回结果:协调节点将最终结果返回给客户端。
特点:快速、直接,不需要查询所有分片。
二、 搜索文档 (Search Workflow)
当你发送 POST /index_name/_search 请求时,协调节点不知道数据在哪里,因此需要查询所有分片。这个过程被称为 "Query Then Fetch" (先查询后取回),分为两个阶段。
第一阶段:查询阶段 (Query Phase)
目的是找到“哪些文档匹配”以及“它们的排序如何”,只返回 ID 和分数,不返回具体内容。
- 客户端请求:客户端向协调节点发送搜索请求。
- 广播请求:协调节点将搜索请求转发给该索引的所有分片(Primary 或 Replica 均可,每个分片组选一个即可)。
- 本地搜索:
- 每个分片在本地执行查询。
- 每个分片会计算文档的评分 (Score),并创建一个优先队列 (Priority Queue)。
- 如果客户端请求
from=0, size=10,每个分片都会返回它本地的前 10 条结果的元数据(主要是 文档 ID 和 Score),不会返回_source原始内容。
- 合并结果:
- 所有分片将结果返回给协调节点。
- 协调节点将所有分片的结果(假设有 5 个分片,每个返回 10 条,共 50 条)合并到一个全局的优先队列中。
- 协调节点进行全局排序,选出最终的 Top 10。
第二阶段:取回阶段 (Fetch Phase)
目的是根据第一阶段拿到的 ID,去获取真正的文档内容 (_source)。
- 定位文档:协调节点现在知道了最终要返回的 10 个文档的 ID,以及它们分别位于哪些分片上。
- 发送 MGET:协调节点向持有这些文档的分片发送
Multi-GET请求。 - 读取内容:各个分片根据 ID 读取文档的
_source内容,并返回给协调节点。 - 最终响应:协调节点收集所有文档内容,拼接成最终的 JSON 响应,返回给客户端。
三、 流程对比与性能隐患
| 特性 | Get (按 ID 读取) | Search (搜索) |
|---|---|---|
| 定位方式 | 哈希计算,直接定位 | 广播到所有分片 |
| 涉及节点 | 仅涉及持有目标数据的 1 个节点 | 涉及所有分片所在的节点 |
| 阶段 | 单一阶段 | Query 阶段 + Fetch 阶段 |
| 性能 | 极快 (O(1)) | 较慢,取决于数据量和分片数 |
关键性能问题:深度分页 (Deep Paging)
在搜索流程中,如果请求是 from=10000, size=10 (即获取第 10001 到 10010 条数据):
- 每个分片都必须查询出本地的前 10010 条数据(排序并保存在内存中)。
- 假设有 5 个分片,协调节点会收到 条记录。
- 协调节点必须在内存中对这 50050 条记录进行全局排序,然后丢弃前 10000 条,只取最后 10 条。
后果:这对内存和 CPU 的消耗非常大。因此,Elasticsearch 默认限制 max_result_window 为 10,000,防止深度分页导致集群崩溃。如果需要深度分页,建议使用 search_after 或 Scroll API。
总结图示
plaintext
[Client]
|
v
[Coordinating Node] <-- 接收请求
|
+--- (1. Query Phase: 广播) ---> [Shard A] (返回 ID+Score)
| [Shard B] (返回 ID+Score)
| [Shard C] (返回 ID+Score)
|
| (2. Merge & Sort: 确定最终 Top N)
|
+--- (3. Fetch Phase: 定向抓取) -> [Shard A] (请求具体 _source)
| [Shard C] (请求具体 _source)
|
v
[Client] <-- 返回最终 JSON