一条 GET /products/_search
返回的不仅是倒排相交结果,还经历
分布式调度:某个节点充当
协调节点(coordinating
node),把查询扇出到目标索引的
每个分片副本组,收集局部命中,再
归并(reduce) 成全局 Top-K,最后
二次扇出 fetch 拉取 _source 或
stored fields。若只读过 第 9
篇 的 Lucene BooleanScorer,容易以为瓶颈在 postings
相交;生产尾延迟里,协调节点归并、跨分片
fetch、聚合桶合并 常常与单分片 CPU
同级显眼。
本文锚定 Elasticsearch 8.x
Reference(Search、Search your
data — How searches work),说明默认
query_then_fetch
两阶段路径、reduce
在排序与聚合上的职责,以及可选
dfs_query_then_fetch 如何为
BM25 提供 全局词项统计(与 第 6
篇 衔接)。不写伪造的集群 QPS;机制与 architecture/42
查询路径节 一致,本篇补充 分片选择、阶段边界与
DFS 代价。
本文是「全文检索引擎」系列第 13 篇(共 18 篇)。→ 系列目录
版本锚定:Elasticsearch 8.x;底层 Lucene 9.x。DFS 在 8.x 中通过
search_type/ 等价参数触发,行为以 Reference 为准。
一、协调节点:入口、路由与扇出
任何节点都可接收 REST 请求。若该节点
不持有 目标分片数据,它自动成为
协调节点:解析索引表达式(含别名、通配、_routing
裁剪),从 cluster state(第
11 篇)解析 分片 → 节点 映射,构建
shard
iterator(每个主分片副本组选一个活跃副本,默认轮询)。
flowchart TB
Client["Client"]
Coord["Coordinating node<br/>parse · route · reduce"]
S0["Shard 0<br/>query / fetch"]
S1["Shard 1<br/>query / fetch"]
S2["Shard 2<br/>query / fetch"]
Client --> Coord
Coord -->|"query phase"| S0
Coord -->|"query phase"| S1
Coord -->|"query phase"| S2
S0 --> Coord
S1 --> Coord
S2 --> Coord
Coord -->|"fetch phase"| S0
Coord -->|"fetch phase"| S2
扇出度 近似等于 参与查询的主分片数 × 阶段数。索引过碎(\(N_{\text{primary}}\) 过大)时,协调开销与 architecture/42 所述 分片膨胀 叠加,表现为 固定协调延迟 上升——即使每个分片数据很少。
二、Query phase:局部打分、局部 Top-K
Query phase 在每个分片本地执行 Lucene 查询(第 9 篇):
- 用 当前 Searcher 上的段(受 refresh 影响)做倒排遍历 / 跳跃。
- 按
Similarity(默认 BM25)计算得分;排序字段走 DocValues(第 10 篇)。 - 分片本地维护 优先队列,仅保留
from + size(或track_total_hits相关)所需的 轻量命中:通常是_shard_doc_id、_score、排序键、routing——不 默认拉取完整_source。
分片返回给协调节点的结构体可理解为:「我在本分片上的局部有序截断」。协调节点 不能 假设分片返回了全局最优,必须在 reduce 阶段 重新归并。
2.1 与单机 Lucene 的差异
单机 Lucene 上 IndexSearcher.search
一次完成遍历 + TopDocs。分布式层把 TopDocs
截断前移到分片,是为
减少网络:若每分片返回全部命中,协调节点会
O(全命中) 爆炸。代价是:分片局部 Top-K
可能漏掉 全局边界附近的文档——因此需要
足够大的分片级队列;size
很大或深度分页时问题恶化(官方推荐 search_after
/ PIT,Reference Paginate search results)。
三、Reduce:全局排序、聚合与 suggest
协调节点收到各分片 query 结果后执行 reduce:
| 结果类型 | Reduce 操作 | 注意点 |
|---|---|---|
| 相关性排序 | 多路归并 _score |
分片局部 IDF 不一致时 BM25 有偏(见第四节 DFS) |
sort 字段 |
多路归并排序键 | 字段需 DocValues;missing
策略全局一致 |
aggregations |
合并桶 / 子聚合 | 见 第 14 篇 |
track_total_hits |
求和或截断计数 | 精确总数贵;默认阈值以 8.x 为准 |
sequenceDiagram
participant C as Coordinating node
participant A as Shard A
participant B as Shard B
C->>A: query phase
C->>B: query phase
A-->>C: local top (id, score)
B-->>C: local top (id, score)
Note over C: reduce → global top doc ids
C->>A: fetch phase (ids on A)
C->>B: fetch phase (ids on B)
A-->>C: stored / _source
B-->>C: stored / _source
C-->>C: merge hits for response
Reduce 完成后,协调节点持有 全局 Top
文档的逻辑坐标(索引、分片、_id、routing),进入
fetch phase。
四、Fetch phase:按 doc id 取正文
Fetch phase 不再做全文打分,而是对 reduce 产出的 doc id 列表,向 持有该文档的分片 发起 multi-get 风格 的字段读取:
_source(若stored或_sourceenabled)stored_fields、highlight 所需 positions 等
因此搜索延迟 ≈ query 扇出 +
reduce + fetch
扇出。"_source": false 或
stored_fields 精简可降低 fetch 成本——RAG
场景若只要 id 做二段召回,应显式关闭
_source(与 llm-infra sparse
召回 接口设计相关)。
副本一致性:fetch 可能命中与 query
不同副本(若两阶段间发生 relocation,极少见);依赖
Lucene 段复制 与 refresh 传播。写后读应配合
第
12 篇 的 wait_for。
五、可选
DFS:dfs_query_then_fetch 与全局统计
默认 query_then_fetch 下,BM25 的
文档频率 df、平均字段长度
avgdl 等统计来自 分片本地
Searcher。词项在分片间分布不均时,同一查询在不同分片上的
IDF
不一致,全局排序可能偏离「把所有文档当作一个大索引」时的理想
BM25(第 6
篇 公式中的 collection 统计)。
search_type=dfs_query_then_fetch(或 8.x
中等价设置)增加 前置 DFS phase:
- 协调节点向各分片收集 词项级全局统计(df、collection size 等)。
- 再执行常规 query phase,分片用 统一统计 打分。
| 模式 | 往返次数 | 打分一致性 | 适用 |
|---|---|---|---|
query_then_fetch(默认) |
2 | 分片局部统计 | 大多数生产查询 |
dfs_query_then_fetch |
3+ | 全局统计近似单机 | 小索引、教学、对 IDF 敏感实验 |
代价:多一轮扇出与聚合统计,协调延迟上升;不能 免费修正跨分片 文档长度归一 的所有偏差。学习排序(LTR)与向量混合(第 15 篇)进一步把「全局特征」推到模型侧,DFS 仍是 稀疏 BM25 时代的工程补丁。
六、查询裁剪:routing、别名与
preference
减少扇出度的手段(与 第 11 篇 联动):
GET /orders/_search?routing=user_42
{
"query": { "term": { "user_id": "user_42" } }
}routing:协调节点只联系 一个主分片组(写入时须用同一 routing)。- 别名 + 时间索引:日志场景只搜
logs-2026.07.*,避免全集群索引。 preference=_local:优先本地分片,利于 协调节点与数据同机 的部署(降低 hop,不减少分片数)。
_search 的 并发 msearch
把多条查询批到一次 HTTP,但
每条子查询仍独立扇出——不能当作减少分片数的银弹。
6.1 深度分页与
search_after
from + size
深度分页时,协调节点需让每个分片返回
足够深的局部队列,内存与网络随
from 线性恶化。ES 8.x 推荐
search_after(排序游标)或
PIT(point in time)+ search_after
做稳定翻页(Reference Paginate search
results):
POST /products/_search
{
"size": 10,
"sort": [
{ "created_at": "desc" },
{ "_id": "desc" }
],
"search_after": [1710000000000, "doc_9981"]
}机制要点:游标建立在 reduce
后的全局排序键上,下一页请求仍触发 全分片
query,但避免协调层为巨大 from
保留中间结果;PIT 固定 索引时间点,减轻
refresh 导致的 翻页重复/漏文档。这与 第
12 篇 的 NRT 语义直接相关。
6.2 profile
与慢查询判读
单分片热点还是协调归并瓶颈,可用
_profile: true 区分(Reference Profile
API):
POST /products/_search
{
"profile": true,
"query": { "match": { "title": "lucene" } }
}响应中 分片级 query 时间 与
collect/fetch
时间 分开列出;若各分片均匀偏慢,查 postings 与
merge(第 5、8
篇);若分片快而总延迟高,查 扇出度、协调
heap、聚合 reduce(第 14
篇)。本篇不粘贴未在固定环境复现的 profile
树,读者应在目标集群上对慢查询取样。
七、学术谱系、争论与开放问题
谱系:分布式 IR 的 collection merging(Zobel & Moffat, 2006)→ 各引擎 分片局部 Top-K + 协调归并 → ES 显式 query/fetch 分裂 以省带宽 → DFS 作为可选全局统计轮。
争论:
- 两阶段 vs 单阶段
query_and_fetch:后者合并阶段但传 大 payload,官方默认弃用路径;与 MapReduce 式 map 输出Combiner 的权衡同构。 - 精确 total hits vs
性能:
track_total_hits: true迫使分片报告全量计数,与 早停 Top-K 优化冲突——产品默认在「够用的近似」与「审计级精确」间折中。
开放问题:
- 跨分片 join(ES|QL 等)在协调层的 shuffle 成本 缺乏与 Trino coordinator-worker 同口径的公开 benchmark;本篇不编造数值。
- 向量 kNN + BM25 混合查询的 reduce 语义在 8.x 仍快速演进(第 15 篇),协调路径是开放设计点。
八、常见误解
| 误解 | 事实 |
|---|---|
| 「协调节点只做转发」 | 负责 解析、扇出、reduce、fetch 编排 |
| 「query 阶段返回完整文档」 | 默认只返 id + score + sort keys;正文在 fetch |
| 「分片局部 Top-K = 全局 Top-K」 | 仅在得分 可分片独立 或分布极均匀时近似成立 |
| 「加副本减少 query 扇出」 | 扇出按 主分片数;副本只 负载均衡到组内不同物理分片 |
| 「DFS 是默认且免费」 | 默认 关闭;多一轮网络与统计合并 |
「size: 10000 只是内存问题」 |
分片级队列与 fetch 都随 size 线性恶化 |
九、小结
三句话小结:
- 默认搜索是
query_then_fetch:协调节点 query 扇出 → reduce 全局 Top doc id → fetch 扇出 取_source/stored。 - Query phase 在分片本地用 Lucene 执行并 截断;reduce 归并得分、排序与聚合;fetch 才拉正文,两阶段设计为 省网络。
dfs_query_then_fetch用额外一轮收集 全局词项统计,缓解跨分片 BM25 偏差,但增加延迟;扇出度仍由 主分片数 主导。
下一篇进入 聚合管线 与 mapping 爆炸 失效模式:聚合与 mapping。
参考资料
规范(A 级)
- Elasticsearch 8.x Reference, Run a search — How searches work(query / fetch 两阶段)。
- Elasticsearch 8.x Reference, Search —
search_type、dfs_query_then_fetch。 - Elasticsearch 8.x Reference, Paginate search
results —
search_after、PIT。 - Elasticsearch 8.x Reference, Preference、Routing。
- Apache Lucene 9.x,
IndexSearcher/TopDocsCollector(与 第 9 篇 一致)。
论文(A 级)
- Robertson, S. & Zaragoza, H., The Probabilistic Relevance Framework: BM25 and Beyond, FnTIR 2009(全局 collection 统计假设)。
- Zobel, J. & Moffat, A., Inverted Files for Text Search, 2006(分区合并)。
站内对读(B 级)
返回 系列目录 | 上一篇:refresh · flush · translog | 下一篇:聚合与 mapping
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【全文检索引擎】Index · Shard · Replica:路由与 cluster state
在 Lucene 段模型之上拆解 Elasticsearch 8.x 的 Index、主分片哈希路由、副本读写分工,以及 cluster state 元数据膨胀如何成为 master 瓶颈;与 architecture/42 分片陷阱对读,不复述全文。
【列存引擎内核】ReplicatedMergeTree
ReplicatedMergeTree 副本协调:Log entry、ClickHouse Keeper/ZooKeeper 路径、副本同步与 recovery。双节点实验步骤(本环境未部署)。24.x LTS 默认推荐 Keeper。
【列存引擎内核】Distributed 引擎与分布式查询路由
ClickHouse Distributed 表的分片键、写入路由与 SELECT 下推;GLOBAL IN/JOIN 的代价与替代方案;与 ReplicatedMergeTree 副本层的关系;对照 PG Citus 的边界。
【流式数据处理】流处理全景:从日志到有状态计算
从批、流、微批四维度对比出发,建立「可重放日志 + 有状态计算」心智模型,厘清 Lambda/Kappa 边界与流表对偶,并给出与 lakehouse 入湖侧对称的全系列地图。