Appearance
Elasticsearch搜索流程
要理解Elasticsearch(以下简称ES)的搜索流程,必须先掌握其底层依赖的Lucene核心机制(倒排索引、查询执行模型),以及ES作为分布式系统的分片协作逻辑。以下是从客户端请求到结果返回的完整流程拆解,重点聚焦实现原理与关键细节。
一、前置基础:ES的核心模型
在讲解搜索流程前,先明确ES的几个核心概念,这些是理解后续流程的前提:
1. 倒排索引(Inverted Index)
ES的搜索能力完全基于Lucene的倒排索引,这是搜索的“引擎心脏”。倒排索引的核心结构包括:
- 词项字典(Term Dictionary):存储所有文档中提取的唯一词项(比如“elasticsearch”“教程”),并通过FST(有限状态转换器) 压缩存储,加速词项查找。
- postings list(词项 postings):每个词项对应一个列表,包含:
- 包含该词项的文档ID(Doc ID);ß
- 词项在文档中的出现次数(TF,Term Frequency);
- 词项在文档中的位置(Position)(用于短语查询,比如“elasticsearch tutorial”需要两个词相邻);
- 词项在文档中的偏移量(Offset)(用于高亮显示,比如标记“elasticsearch”在文本中的起始和结束位置)。
- 文档频率(DF,Document Frequency):该词项在所有文档中出现的文档数(用于计算逆文档频率IDF)。
举个例子:
假设有两篇文档:
- Doc1:“Elasticsearch是分布式搜索引擎”
- Doc2:“Elasticsearch教程很有用”
倒排索引中“Elasticsearch”的postings list是[Doc1, Doc2],TF分别是1和1,DF是2;“教程”的postings list是[Doc2],TF是1,DF是1。
2. 分片与节点模型
ES是分布式系统,索引数据被拆分为多个分片(Shard),每个分片是一个独立的Lucene实例(包含完整的倒排索引)。分片分为:
- 主分片(Primary Shard):数据写入的第一目的地,不可修改数量(索引创建时指定)。
- 副本分片(Replica Shard):主分片的备份,用于高可用和分担查询压力(可动态调整数量)。
搜索流程中,**协调节点(Coordinating Node)**是请求的入口,负责:
- 解析查询请求;
- 分发请求到数据节点(Data Node,存储分片的节点);
- 合并分片结果并返回客户端。
二、完整搜索流程拆解
ES的搜索流程可分为7个核心阶段,从客户端请求到结果返回,每个阶段都有深入的底层逻辑:
阶段1:请求接收与初步解析
客户端通过RESTful API发送查询请求(支持URI搜索和Request Body搜索,后者更灵活),例如:
json
POST /my_index/_search
{
"query": {
"match": {
"content": "elasticsearch tutorial"
}
},
"sort": [{"publish_time": "desc"}],
"size": 10,
"from": 0,
"aggs": {
"top_authors": {
"terms": {"field": "author.keyword", "size": 5}
}
}
}核心操作:
- 路由解析:协调节点根据请求中的
index参数,确定要查询的索引,并获取该索引的分片分布信息(主分片/副本分片的位置)。 - 参数验证:检查查询语法是否合法(比如
match查询的字段是否存在、sort字段是否可排序)。 - 查询类型判断:区分全文查询(如
match,会分词)和精确查询(如term,不会分词)。
阶段2:查询预处理(分词与查询树构建)
这一步是全文搜索的核心差异点——将用户输入的查询文本转换为Lucene可执行的查询结构。
2.1 分词(Analysis)
对于全文查询(如match),ES会使用指定的**分析器(Analyzer)**对查询文本进行分词。分析器由三部分组成:
- 字符过滤器(Char Filter):预处理原始文本(比如替换
&为and、去除HTML标签)。 - 分词器(Tokenizer):将文本拆分为词项(Term)(比如标准分词器将“elasticsearch tutorial”拆分为
elasticsearch和tutorial)。 - 词项过滤器(Token Filter):对词项进行二次处理(比如小写转换、去除停用词“的”“是”、同义词替换“ES→elasticsearch”)。
关键原则:查询时的分析器必须与索引时的分析器一致,否则会导致“索引的词项与查询的词项不匹配”(比如索引时用了小写转换,查询时没⽤,则“Elasticsearch”无法匹配“elasticsearch”)。
2.2 查询树构建
ES将JSON格式的查询DSL转换为Lucene查询树(Query Tree),每个查询类型对应Lucene的具体实现:
match查询:转换为BooleanQuery(多个词项的“或”关系,默认operator: or);term查询:转换为TermQuery(精确匹配词项);range查询:转换为RangeQuery(匹配数值/日期范围);bool查询:转换为BooleanQuery(组合must/should/must_not子句)。
例如,上述match查询会被转换为:
BooleanQuery(
should=[
TermQuery(term="elasticsearch"),
TermQuery(term="tutorial")
]
)阶段3:分布式查询计划生成
协调节点根据分片分布和查询类型,生成查询执行计划,决定将请求分发到哪些分片。
3.1 分片选择策略
- 对于单索引查询:协调节点会选择该索引的所有主分片或副本分片(默认轮询,分担压力)。
- 对于多索引查询:协调节点会合并所有索引的分片列表,再分发请求。
3.2 查询模式选择
ES支持两种分布式查询模式,核心差异在于是否提前收集全局词频统计:
| 模式 | 流程 | 适用场景 | 缺点 |
|---|---|---|---|
| Query Then Fetch(默认) | 1. 分发查询到分片→2. 分片返回候选结果→3. 合并结果→4. fetch完整文档 | 大多数场景,性能优先 | 评分可能有偏差(局部统计) |
| DFS Query Then Fetch | 1. 收集所有分片的词频统计→2. 分发查询→3. 分片返回结果→4. 合并→5. fetch | 需要精确评分的场景(如排序) | 性能损耗(多一次网络请求) |
评分偏差的根源:Lucene的评分算法(如BM25)依赖全局词频统计(比如DF),但默认模式下,每个分片仅用局部统计(比如分片A的DF是10,分片B的DF是20,全局DF是30,但分片A的查询会用DF=10计算评分)。DFS模式会先收集所有分片的DF,再计算评分,解决偏差问题。
阶段4:分片本地查询执行(Query Phase)
每个数据节点收到查询请求后,执行本地Lucene查询,生成候选结果集。
4.1 Lucene查询执行流程
Lucene的查询执行基于加权迭代模型,核心步骤:
- 词项查找:根据查询树中的词项(如
elasticsearch),在倒排索引的词项字典中找到对应的postings list。 - 文档匹配:合并多个词项的
postings list(比如BooleanQuery的should子句是并集,must子句是交集)。 - 评分计算:对每个匹配的文档,用相似度算法(默认BM25)计算评分(
_score)。
BM25的公式:
$$ score(q,d) = \sum_{t \in q} \left( idf(t)^2 \times \frac{tf(t,d) \times (k1 + 1)}{tf(t,d) + k1 \times (1 - b + b \times \frac{|d|}{avgdl})} \right) $$
各参数含义:idf(t):逆文档频率,衡量词项的“稀有度”(idf(t) = log((N - DF(t) + 0.5) / (DF(t) + 0.5) + 1),N是总文档数);tf(t,d):词项t在文档d中的出现次数;k1:控制TF饱和的参数(默认1.2,值越大,TF对评分的影响越大);b:控制文档长度对评分的影响(默认0.75,值越大,长文档的评分越低);|d|:文档d的长度(词数);avgdl:索引中所有文档的平均长度。
- 优先队列(Priority Queue):将匹配的文档按评分(或排序字段)排序,保留前N条(N=from+size,比如from=0、size=10时,保留前10条)。优先队列用最小堆实现,内存占用低(仅保存top N条,无需加载所有结果)。
4.2 结果返回
分片将候选结果返回给协调节点,内容包括:
- 文档ID(
_id); - 排序值(
_score或sort字段的值); - 分片ID(用于后续fetch阶段定位分片)。
阶段5:全局结果合并(Merge Phase)
协调节点收到所有分片的候选结果后,执行全局合并,生成最终的结果集。
核心操作:
- 合并优先队列:将所有分片的候选结果(每个分片的top N条)合并成一个全局优先队列,再按排序规则取前
size条(比如from=0、size=10时,取全局前10条)。 - 去重(可选):如果查询涉及多个索引或分片,且文档有重复(比如副本分片的重复数据),则去重。
- 深分页处理:当
from较大时(比如from=10000、size=10),每个分片需要返回from+size=10010条结果,协调节点需要合并所有分片的10010条结果,再取第10001-10010条。这会导致内存和网络开销暴增,因此ES推荐用scroll或search_after做深分页(search_after更高效,基于前一页的最后一条结果的排序值)。
阶段6:完整文档获取(Fetch Phase)
全局合并后,协调节点仅拥有文档的ID和排序值,需要从对应的分片获取完整文档数据。
核心流程:
- 分片定位:根据文档的
_id和路由规则(默认按_id哈希),找到存储该文档的主分片或副本分片。 - 文档读取:向目标分片发送
fetch请求,获取文档的原始数据(_source字段,默认存储)。 - 结果处理:对文档进行高亮、字段过滤(如
_source指定返回字段)等操作。
阶段7:聚合与最终结果返回(Aggregation Phase)
如果查询包含聚合(Aggregation),协调节点会在fetch阶段后执行聚合计算,再将结果返回客户端。
聚合的底层原理:
ES的聚合分为桶聚合(Bucket)和指标聚合(Metric):
- 桶聚合:将文档分组(比如
terms按作者分组、date_histogram按日期分组); - 指标聚合:对每组文档计算统计值(比如
sum计算总阅读量、avg计算平均评分)。
聚合执行流程:
- 分片本地聚合:每个分片执行本地聚合,生成中间结果(比如每个分片统计自己的作者计数)。
- 全局聚合合并:协调节点合并所有分片的中间结果,生成最终聚合结果(比如合并所有分片的作者计数,取前5名)。
聚合的准确性问题:
当数据分布不均匀时,分片本地聚合的结果可能不完整(比如作者A在分片1有100条,分片2有200条,但分片1的top 5没有作者A,分片2的top 5有,合并后作者A的计数是200,而实际是300)。解决方法是设置shard_size参数(默认是size×1.5+10),让分片返回更多的中间结果,提高准确性(但会增加网络开销)。
三、关键细节与优化点
1. 实时性与Refresh机制
ES是近实时(NRT)系统,数据写入后需要Refresh(默认1秒)才能被搜索到。Refresh的本质是:
- 将内存中的索引缓冲(Index Buffer)数据刷到内存中的倒排索引段(Lucene的Segment);
- 新的Segment会被打开(可搜索),但未写入磁盘(持久化由
Flush操作完成,默认30分钟或日志文件达到512MB)。
优化:如果需要更高的实时性,可以手动调用_refresh API,或设置refresh_interval: 100ms(但会增加IO压力)。
2. 缓存机制
ES通过缓存减少重复查询的开销:
- 查询缓存(Query Cache):缓存过滤查询(如
bool的filter子句)的结果(文档ID列表),默认缓存10%的堆内存。 - 字段数据缓存(Field Data Cache):缓存text类型字段的排序/聚合数据(因为text类型没有
doc_values,需要加载到内存)。优化:尽量用keyword类型做排序/聚合(keyword类型默认开启doc_values,存储在磁盘,内存占用低)。 - 分片请求缓存(Shard Request Cache):缓存分片查询的结果(如聚合结果、搜索结果),默认开启,可通过
_cache: true控制。
3. 高亮(Highlighting)的实现
高亮需要找到文档中匹配查询的文本片段,并添加标签(如<em>)。ES支持三种高亮器:
- Plain Highlighter(默认):基于Lucene的
Highlighter,通过postings list中的位置信息找到匹配片段,适用于大多数场景。 - Fast Vector Highlighter:需要字段设置
term_vector: with_positions_offsets(存储词项的位置和偏移量),速度更快,适用于大文本字段。 - Postings Highlighter:需要字段设置
index_options: offsets(存储词项的偏移量),内存占用低,适用于需要高亮但不想存储term_vector的场景。
四、总结:完整流程回顾
将上述阶段串联,一个搜索请求的完整路径是:
客户端 → 协调节点(解析请求) → 分词/构建查询树 → 分发查询到分片 → 分片执行本地查询(倒排索引匹配→评分→优先队列) → 分片返回候选结果 → 协调节点合并结果 → fetch完整文档 → 聚合计算 → 返回客户端五、常见问题解答
Q1:为什么term查询不能匹配text类型的字段?
A:text类型的字段会被分词(比如“Elasticsearch教程”会拆分为“elasticsearch”和“教程”),而term查询是精确匹配(比如查询“Elasticsearch教程”会直接查找词项“Elasticsearch教程”,但倒排索引中没有这个词项)。解决方法:用match查询(会分词),或用keyword类型(不会分词)。
Q2:为什么深分页(from=10000)会很慢?
A:因为每个分片需要返回from+size条结果(比如from=10000、size=10时,每个分片返回10010条),协调节点需要合并所有分片的10010条结果,再取第10001-10010条。数据量越大,内存和网络开销越大。优化方法:用search_after(基于前一页的最后一条结果的排序值)或scroll(保持搜索上下文)。
Q3:为什么聚合结果有时不准确?
A:因为分片本地聚合的结果可能不完整(比如作者A在分片1有100条,分片2有200条,但分片1的top 5没有作者A)。优化方法:设置shard_size参数(比如shard_size: 100),让分片返回更多的中间结果,提高准确性。
六、扩展阅读
- Lucene官方文档:https://lucene.apache.org/core/
- Elasticsearch官方指南:https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html
- 《Elasticsearch权威指南》(中文版):https://www.elastic.co/guide/cn/elasticsearch/guide/current/index.html
通过以上讲解,相信你已经掌握了ES搜索的核心流程和底层原理。ES的强大之处在于将Lucene的复杂机制封装成简单的API,同时通过分布式架构解决了大规模数据的搜索问题。理解这些原理后,你可以更高效地优化查询性能、解决搜索问题。
