分布式搜索执行过程

分布式搜索执行过程 #

当你向 Easysearch 发起一个 /_search 请求时,集群内部会发生一系列分布式协作。理解这个过程能帮助你解释很多“看起来奇怪”的现象:为什么分页越深越慢、为什么结果顺序会抖动、为什么加副本能提高吞吐、为什么有时相关性会“不一致”。

在一个典型的搜索执行中,Easysearch 会经历两个阶段:

  • Query phase(查询阶段):找出每个分片上的 top-n 候选,并在协调节点合并成全局 top-n
  • Fetch phase(取回阶段):只取回最终需要返回的那一页文档内容,并做必要的“丰富”

Query phase:每个分片产出本地 top-n #

查询阶段会把请求广播到目标索引涉及的每个分片拷贝(主分片或副本分片)。每个分片本地执行查询,并构建一个 优先队列(priority queue),保存本分片的 top-n 匹配结果。

优先队列的大小取决于分页参数:

GET /_search
{
  "from": 90,
  "size": 10
}

这个请求意味着需要找出“第 91~100 条”结果,所以每个分片需要构建长度为 from + size = 100 的优先队列,才有可能保证全局合并后不漏掉候选。

查询阶段(概念流程):

  1. 客户端把 search 请求发给某个节点,该节点成为协调节点
  2. 协调节点将请求转发到所有相关分片(主或副本)
  3. 每个分片本地执行查询,返回:
    • 文档 ID
    • 排序所需的值(例如 _score,或排序字段的值)
  4. 协调节点把所有分片返回的候选合并到全局优先队列,得到“全局有序的 top-(from+size)”

为什么副本能提高吞吐:搜索请求可以由主分片或副本分片处理,副本越多、硬件越多,可并行处理的搜索请求也越多。

Fetch phase:只取回最终需要返回的那一页 #

查询阶段只确定“哪些文档应该在结果里”,但并没有把文档内容取回。取回阶段会:

  1. 协调节点根据 from/size 丢弃前 from 条,只保留最终要返回的 size
  2. 协调节点向这些文档所在的分片发起批量 GET(multi-get)请求
  3. 分片加载文档内容(通常来自 _source),并按需做“丰富”,例如高亮片段
  4. 协调节点汇总后返回给客户端

深分页(Deep pagination):为什么会越来越慢 #

Query-then-fetch 支持 from/size 分页,但深分页会把成本放大:

  • 每个分片都要构建 from + size 的队列
  • 协调节点需要对 number_of_shards * (from + size) 的候选做合并排序
  • 过程中会消耗更多 CPU、内存、网络带宽

经验上,深分页到 10,000~50,000 级别在一些场景仍可接受,但当 from 很大时,排序合并会变得非常沉重。更重要的是:深分页很少符合真实用户行为;大量深分页请求常来自爬虫/机器人。

如果你需要批量遍历大量结果,更推荐使用更合适的检索方式(例如滚动/游标类方案,或 search_after,见「分页与排序」章节)。

搜索选项:影响路由、稳定性与相关性 #

preference:让结果不“抖动” #

分片副本会被轮询使用。若你按某个字段排序,而多个文档在该排序键上“相同”,不同分片拷贝的合并顺序可能会导致结果顺序在刷新页面时发生变化(bouncing results)。

使用 preference 传入一个稳定的值(例如 session id / user id),可以让同一个用户更倾向于命中同一组分片拷贝,从而减少顺序抖动:

GET /_search?preference=<session-id>

timeout:允许返回“部分结果” #

搜索总体耗时通常由“最慢分片 + 合并”决定。timeout 用来限制分片允许执行的最大时间:超时后可能返回部分结果,并在响应中用 timed_out 标记。

需要注意:

  • 超时检查往往是“分片逐文档评估”的,并不覆盖所有前置开销
  • 某些昂贵步骤可能让总体耗时仍超过你设定的超时时间

routing:只搜相关分片 #

如果你在写入时使用了自定义 routing,使相关文档落在同一分片上,那么搜索时也可以带上 routing 值,避免扫描所有分片:

GET /_search?routing=user_1,user_2

这对多租户/按用户隔离的数据模型尤其有效(见「多租户」章节)。

search_type:相关性更精确(代价更高) #

默认搜索类型是 query_then_fetch。在某些需要更精确相关性统计的场景,可以选择 dfs_query_then_fetch(多一个分布式词频统计的预查询阶段),以获得更“全局一致”的词频/相关性计算,但代价是更高的开销:

GET /_search?search_type=dfs_query_then_fetch

游标查询(Scroll) #

from/size 分页不适合大批量文档遍历。如果你需要从 Easysearch 中高效地取回大量文档,可以使用 Scroll 查询。

游标查询类似于传统数据库中的 cursor,先做查询初始化,然后批量拉取结果:

GET /my_index/_search?scroll=1m
{
    "query": { "match_all": {} },
    "sort" : ["_doc"],
    "size":  1000
}
  • scroll=1m:保持游标查询窗口一分钟
  • _doc 排序:最高效的排序方式(去掉全局排序的开销)

查询返回结果中包含 _scroll_id 字段,传递它获取下一批结果:

GET /_search/scroll
{
    "scroll": "1m",
    "scroll_id": "cXVlcnlUaGVuRmV0Y2g7NTsx..."
}

⚠️ 游标查询取的是某个时间点的快照数据,初始化之后索引上的任何变化会被忽略。每次查询都需要用前一次返回的 _scroll_id,当没有更多结果返回时,表示所有匹配文档已处理完毕。

💡 字段 size 作用于单个分片,所以每个批次实际返回的文档数量最大为 size * number_of_primary_shards

小结 #

  • 分布式搜索通常是 query/fetch 两阶段:先找候选,再取回文档内容
  • from/size 深分页会放大每分片队列与协调节点合并成本,应谨慎使用
  • preference 可减少结果顺序抖动;routing 可减少扫描分片范围
  • timeout 可能返回部分结果;dfs_query_then_fetch 以更高成本换取更一致的相关性统计
  • 大批量遍历应使用 Scroll 游标查询或 search_after

下一步可以继续阅读: