Elasticsearch搜索原理
Elasticsearch 的搜索过程是一个高效、分布式的流程,设计用于在大量数据中快速定位相关信息。以下是其详细步骤:
核心流程:分散/聚集模型 (Scatter/Gather)
Elasticsearch 的搜索本质上是 分散 请求到相关分片,然后 聚集/合并 结果的过程。
1. 客户端发起请求 (Client Request Initiation)
- 用户或应用程序向 任意一个 Elasticsearch 节点(通常通过 REST API)发送搜索请求 (
GET /<index>/_search
)。 - 请求包含查询 DSL(如
match
,term
,bool
查询)、过滤条件、排序、分页(from
/size
)、高亮、聚合等参数。 - 收到请求的节点自动成为本次搜索请求的 协调节点 (Coordinating Node)。
2. 查询解析与路由 (Query Parsing & Routing)
- 协调节点 解析查询 DSL,确定需要搜索哪些 索引 (Indices)。
- 对于每个目标索引,协调节点根据索引的 分片 (Shards) 配置(主分片数量)和 路由策略(通常基于文档 ID 的哈希):
- 计算查询应该发送到哪些 分片副本 (Shard Replicas)。
- ES 默认会将搜索请求 轮询 (Round-Robin) 发送到索引的所有分片(包括主分片和副本分片)的一个副本。这样做是为了充分利用所有节点的资源,实现负载均衡。
- 如果查询指定了路由参数 (
routing
),协调节点会根据路由值计算出目标分片 ID,并只将请求发送到该特定分片(及其副本)。
3. 查询阶段 - 分散到分片 (Query Phase - Scatter)
- 协调节点 将解析和处理后的搜索请求 并行地 发送到在步骤 2 中确定的所有 相关分片副本(目标分片的主分片或副本分片)。
- 每个目标分片副本 在其本地独立执行查询:
- 加载倒排索引: 查找查询词项在倒排索引中对应的文档 ID (
_id
) 列表。 - 执行查询逻辑: 根据查询类型(布尔组合、短语匹配、范围查询等)处理这些文档 ID 列表,过滤掉不匹配的文档。例如:
Term Query
: 直接查找对应 term 的倒排列表。Match Query
: 分词后对每个词项查找倒排列表,再按逻辑(如 OR)合并。Bool Query
: 组合多个子查询的 MUST/MUST_NOT/SHOULD/FILTER 逻辑。Range Query
: 查找数值或日期范围字段的倒排索引或 Doc Values。
- 评分 (Scoring - 如果适用):
- 对于需要相关性排序 (
"sort": [{"_score": "desc"}]
) 或track_scores: true
的查询,每个分片使用选定的相似度算法(如 TF/IDF, BM25)计算匹配文档的 本地相关性分数 (_score
)。评分基于该分片本地的词频、逆文档频率等统计信息。 - FILTER 上下文 中的查询条件(如
filter
子句、bool
的filter
或must_not
)不参与评分,只用于二元过滤,效率通常更高。
- 对于需要相关性排序 (
- 构建优先级队列:
- 每个分片根据请求的
size
和from
(或search_after
)参数以及排序规则(默认按_score
降序),在本地维护一个优先级队列 (Priority Queue)。 - 队列的大小是
size + from
(对于 Top-K 结果合并足够)。 - 该队列保存了当前分片本地得分最高(或按指定排序规则最靠前)的
size + from
个文档的元数据:文档 ID (_id
)、分片 ID、排序字段值(主要是_score
)。注意:此时并不获取文档的实际源数据 (_source
) 或存储字段。
- 每个分片根据请求的
- 加载倒排索引: 查找查询词项在倒排索引中对应的文档 ID (
4. 查询阶段 - 聚集结果 (Query Phase - Gather)
- 所有目标分片副本将其本地执行的查询结果(即包含文档 ID、分片 ID、排序值的优先级队列)并行地 发送回 协调节点。
- 协调节点 接收来自所有分片的结果。
- 协调节点合并与排序:
- 协调节点将来自各个分片的优先级队列 合并 成一个全局的优先级队列。
- 这个全局队列的大小是用户请求的
size + from
。 - 合并时,根据请求指定的排序规则(默认
_score
降序)决定文档在全局结果中的最终顺序。 - 关键点: 协调节点此时只持有全局 Top
(size + from)
结果的 文档 ID (_id
)、分片位置、_score
等排序元数据,仍然没有文档内容 (_source
)。
5. 取回阶段 (Fetch Phase)
- 一旦协调节点确定了全局排序后的最终文档列表(
from
到from + size
之间的文档),它需要获取这些文档的完整内容 (_source
) 和/或任何显式请求的存储字段(stored_fields
)。 - 协调节点 向 持有这些文档实际数据的分片 发送 多文档获取请求 (Multi-Get Request /
mget
)。- 请求包含需要获取的文档的
_id
和它们所在的具体分片 ID。
- 请求包含需要获取的文档的
- 相关分片 接收到
mget
请求后:- 根据
_id
在其本地存储中查找对应的文档。 - 加载
_source
: 从存储(通常是文件系统缓存中的 Lucene 段文件)中读取文档的原始 JSON 源数据 (_source
)。 - 应用高亮 (Highlighting): 如果需要,对
_source
中指定字段的内容执行高亮处理(查找匹配词并添加高亮标签)。 - 应用字段过滤: 如果请求指定了
_source
过滤(如"_source": ["title", "date"]
),则只返回请求的字段。 - 将处理好的文档数据(
_id
,_source
, 高亮结果,存储字段等)返回给协调节点。
- 根据
6. 最终响应组装与返回 (Response Assembly & Return)
- 协调节点 收集来自各个分片的
mget
响应。 - 它组装最终的搜索结果响应 (Search Response):
hits
数组:包含排好序的实际文档数据 (_source
),高亮信息等。数组长度由size
决定,跳过前from
个结果。hits.total
:匹配文档的总数 (这是一个下限值,如需精确值需设置track_total_hits: true
,代价较高)。took
:整个搜索请求消耗的总时间(毫秒)。_shards
:报告参与搜索的分片总数、成功数、失败数。aggregations
(如果请求了聚合):包含聚合结果。- 其他信息:如是否超时 (
timed_out
)、滚动 ID (_scroll_id
) 等。
- 协调节点 将组装好的最终 JSON 响应返回给原始客户端。
关键概念与注意事项
-
分布式本质:
- 查询在每个分片本地并行执行,极大提高速度。
- 协调节点负责路由、分发、合并和最终组装。
-
深度分页 (Deep Pagination) 问题:
- 当
from
+size
的值非常大时(如from: 10000, size: 10
),每个分片都需要构建一个大小为 10010 的本地优先级队列,协调节点需要合并number_of_shards * 10010
个结果来找到全局的 Top 10010,然后只返回最后 10 个。这对 CPU、内存和网络带宽消耗巨大,性能极差。 - 解决方案: 使用
search_after
参数(基于上一页最后一个结果的排序值)或滚动 API (scroll
)(用于深度遍历或导出,非实时)替代传统的from
/size
。
- 当
-
相关性评分 (
_score
):- 评分在每个分片本地计算,基于该分片的本地统计信息(如 IDF)。这在大集群中通常是足够准确的近似值。
- 如果索引非常小或要求极端精确的全局评分(代价很高),可设置
search_type: dfs_query_then_fetch
。它会在查询阶段增加一个额外的步骤,先收集所有分片的全局词频统计信息,再分发下去重新评分。通常不推荐使用,除非绝对必要。
-
聚合 (Aggregations):
- 聚合计算也是在查询阶段在每个分片上并行执行的(构建桶、计算指标)。
- 每个分片返回其本地聚合结果(部分桶和指标)。
- 协调节点负责将来自所有分片的聚合结果合并成全局聚合结果(例如,合并桶、累加总和、计算全局平均值等)。这通常非常高效。
- 某些聚合(如
terms
)默认返回的是每个分片的 Top 桶合并后的结果,可能遗漏低频项(可通过增大shard_size
缓解)。cardinality
和percentiles
等聚合使用近似算法。
-
过滤上下文 (Filter Context) vs. 查询上下文 (Query Context):
- 查询上下文: 影响评分 (
_score
),用于全文搜索和相关性排序。must
/should
通常在此上下文。 - 过滤上下文: 只关心文档是否匹配(是/否),不评分。结果会被缓存,性能更高。
filter
/must_not
/constant_score
通常在此上下文。将不关心评分的条件放入filter
可以显著提升性能。
- 查询上下文: 影响评分 (
-
缓存:
- 分片级请求缓存: 缓存整个查询请求的结果(通常是
size=0
的聚合请求或频繁重复的查询)。默认开启,但仅缓存特定查询(如bool
的filter
部分)。 - 分片级查询缓存 (Query Cache): Lucene 级别,缓存查询结果的文档 ID 位集 (bitset)。对过滤 (
filter
上下文) 性能提升明显,但对文本评分查询效果有限。默认开启但大小有限。 - 文件系统缓存: OS 会将频繁访问的 Lucene 段文件缓存在内存中,极大加速索引读取。确保有足够内存给文件系统缓存是 ES 性能的关键。
- 分片级请求缓存: 缓存整个查询请求的结果(通常是
-
副本的作用:
- 副本不仅提供高可用性,也能分担读负载(搜索请求)。协调节点会将请求发送到分片副本(主或副),实现负载均衡。
总结流程图示:
[客户端]|| (1. 发送搜索请求)V
[协调节点]|| (2. 解析查询, 确定目标分片)|--------------------------------------------------| |V (3a. 发送查询请求) V (3a. 发送查询请求)
[分片副本 A] [分片副本 B]| (执行本地查询, 构建优先级队列) | (执行本地查询, 构建优先级队列)| (4a. 返回文档ID/分片/分数队列) | (4a. 返回文档ID/分片/分数队列)| ||<-------------------------------------------------||V
[协调节点]| (4b. 合并所有分片的队列, 确定全局Top-K)|| (5a. 发送mget请求获取具体文档)|--------------------------------------------------| |V (5b. 返回文档_source/高亮) V (5b. 返回文档_source/高亮)
[分片副本 A] (持有Doc X) [分片副本 N] (持有Doc Y)| ||<-------------------------------------------------||V (6. 组装最终响应)
[协调节点]|| (返回搜索结果给客户端)V
[客户端]
理解这个分散/聚集流程对于诊断 Elasticsearch 搜索性能问题、优化查询 DSL 和配置集群至关重要。