Skip to content

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}
    }
  }
}

核心操作:

  1. 路由解析:协调节点根据请求中的index参数,确定要查询的索引,并获取该索引的分片分布信息(主分片/副本分片的位置)。
  2. 参数验证:检查查询语法是否合法(比如match查询的字段是否存在、sort字段是否可排序)。
  3. 查询类型判断:区分全文查询(如match,会分词)和精确查询(如term,不会分词)。

阶段2:查询预处理(分词与查询树构建)

这一步是全文搜索的核心差异点——将用户输入的查询文本转换为Lucene可执行的查询结构。

2.1 分词(Analysis)

对于全文查询(如match),ES会使用指定的**分析器(Analyzer)**对查询文本进行分词。分析器由三部分组成:

  • 字符过滤器(Char Filter):预处理原始文本(比如替换&and、去除HTML标签)。
  • 分词器(Tokenizer):将文本拆分为词项(Term)(比如标准分词器将“elasticsearch tutorial”拆分为elasticsearchtutorial)。
  • 词项过滤器(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 Fetch1. 收集所有分片的词频统计→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的查询执行基于加权迭代模型,核心步骤:

  1. 词项查找:根据查询树中的词项(如elasticsearch),在倒排索引的词项字典中找到对应的postings list
  2. 文档匹配:合并多个词项的postings list(比如BooleanQuery的should子句是并集,must子句是交集)。
  3. 评分计算:对每个匹配的文档,用相似度算法(默认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:索引中所有文档的平均长度。
  4. 优先队列(Priority Queue):将匹配的文档按评分(或排序字段)排序,保留前N条(N=from+size,比如from=0、size=10时,保留前10条)。优先队列用最小堆实现,内存占用低(仅保存top N条,无需加载所有结果)。

4.2 结果返回

分片将候选结果返回给协调节点,内容包括:

  • 文档ID(_id);
  • 排序值(_scoresort字段的值);
  • 分片ID(用于后续fetch阶段定位分片)。

阶段5:全局结果合并(Merge Phase)

协调节点收到所有分片的候选结果后,执行全局合并,生成最终的结果集。

核心操作:

  1. 合并优先队列:将所有分片的候选结果(每个分片的top N条)合并成一个全局优先队列,再按排序规则取前size条(比如from=0、size=10时,取全局前10条)。
  2. 去重(可选):如果查询涉及多个索引或分片,且文档有重复(比如副本分片的重复数据),则去重。
  3. 深分页处理:当from较大时(比如from=10000、size=10),每个分片需要返回from+size=10010条结果,协调节点需要合并所有分片的10010条结果,再取第10001-10010条。这会导致内存和网络开销暴增,因此ES推荐用scrollsearch_after做深分页(search_after更高效,基于前一页的最后一条结果的排序值)。

阶段6:完整文档获取(Fetch Phase)

全局合并后,协调节点仅拥有文档的ID和排序值,需要从对应的分片获取完整文档数据

核心流程:

  1. 分片定位:根据文档的_id路由规则(默认按_id哈希),找到存储该文档的主分片或副本分片
  2. 文档读取:向目标分片发送fetch请求,获取文档的原始数据_source字段,默认存储)。
  3. 结果处理:对文档进行高亮字段过滤(如_source指定返回字段)等操作。

阶段7:聚合与最终结果返回(Aggregation Phase)

如果查询包含聚合(Aggregation),协调节点会在fetch阶段后执行聚合计算,再将结果返回客户端。

聚合的底层原理:

ES的聚合分为桶聚合(Bucket)指标聚合(Metric)

  • 桶聚合:将文档分组(比如terms按作者分组、date_histogram按日期分组);
  • 指标聚合:对每组文档计算统计值(比如sum计算总阅读量、avg计算平均评分)。

聚合执行流程

  1. 分片本地聚合:每个分片执行本地聚合,生成中间结果(比如每个分片统计自己的作者计数)。
  2. 全局聚合合并:协调节点合并所有分片的中间结果,生成最终聚合结果(比如合并所有分片的作者计数,取前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):缓存过滤查询(如boolfilter子句)的结果(文档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),让分片返回更多的中间结果,提高准确性。

六、扩展阅读

通过以上讲解,相信你已经掌握了ES搜索的核心流程和底层原理。ES的强大之处在于将Lucene的复杂机制封装成简单的API,同时通过分布式架构解决了大规模数据的搜索问题。理解这些原理后,你可以更高效地优化查询性能、解决搜索问题。