概述

本文深入解析 Elasticsearch 基于 Lucene 的倒排索引原理,以及文档的读写流程。

一、倒排索引核心概念

1.1 什么是倒排索引

倒排索引是一种用于快速查找包含特定词汇的文档的数据结构。它类似于一本书的索引页,但结构上是”倒过来”的,因此得名。

1.2 正向索引 vs 倒排索引

正向索引(Forward Index)

  • 是基于文档的索引结构,以文档为中心组织数据
  • 存储方式:为每个文档建立一个条目,记录该文档包含的所有词汇及其出现位置
  • 典型实现:文档ID → [词项1:位置列表, 词项2:位置列表,…]
  • 应用场景:文档内容检索、文档相似度计算、搜索引擎的文档存储层

倒排索引(Inverted Index)

  • 是基于词汇的索引结构,以词项为中心组织数据
  • 存储方式:为每个词项建立一个条目,记录包含该词项的所有文档及出现位置
  • 典型实现:词项 → [文档ID1:位置列表, 文档ID2:位置列表,…]
  • 核心优势:快速定位包含特定词项的文档、支持高效的布尔查询(AND/OR/NOT)、便于实现相关性排序

例如,假设有三篇文档如下:

  • “Elasticsearch 是一个分布式搜索引擎”
  • “分布式系统可以提供高可用性”
  • “搜索引擎使用倒排索引进行高效搜索”

正向索引会记录每个文档中有哪些词:

Doc1: ["Elasticsearch", "是", "一个", "分布式", "搜索", "引擎"]
Doc2: ["分布式", "系统", "可以", "提供", "高", "可用性"]
Doc3: ["搜索", "引擎", "使用", "倒排索引", "进行", "高效", "搜索"]

倒排索引则会记录每个词在哪些文档中出现:

"Elasticsearch": [1]
"分布式": [1, 2]
"搜索": [1, 3]
"引擎": [1, 3]
"倒排索引": [3]

1.3 倒排索引的构建

在 Elasticsearch 中,文档首先会被分析和处理,然后生成倒排索引。其过程大致如下:

  1. 文档分词:每个文档都会经过分析器(Analyzer)的处理,分析器负责将文档的文本分解成词项(terms)
  2. 标准化和过滤:分词后,分析器通常会进行进一步的处理,例如将所有词项转为小写、删除停用词等
  3. 构建倒排列表:倒排索引将每个词项与它所出现的文档ID关联

1.4 倒排索引的结构

倒排索引的核心部分可以分为以下几个组成部分:

  1. 词汇表(Term Dictionary):保存了所有被索引的词项,通常是以字典形式存储
  2. 倒排列表(Posting List):对于每个词项,倒排列表记录了所有包含该词项的文档ID,还可包含:
    • 词项频率(Term Frequency, TF):记录该词项在文档中出现的次数
    • 文档频率(Document Frequency, DF):记录该词项在整个索引中出现的文档总数
    • 位置(Position):词项在文档中的位置,用于支持短语和邻近查询

1.5 倒排索引的查询原理

  1. 查找词项:Elasticsearch 首先在词汇表中查找查询词项,找到它们的倒排列表
  2. 合并倒排列表:合并两个词项的倒排列表,找到同时包含所有查询词的文档
  3. 计算相关性:根据 TF-IDF 或 BM25 算法对文档进行评分

1.6 倒排索引的优势

  • 快速检索:对于某个词项的检索速度极快,尤其在海量文档中,查询性能仍能保持在毫秒级别
  • 低内存占用:通过压缩算法和优化的数据结构,保持较低的内存使用率
  • 支持复杂查询:不仅支持简单的关键词查询,还支持短语、前缀、邻近查询等复杂搜索条件

二、读写流程

2.1 创建文档

向 Elasticsearch 中添加文档时,由于 ES 是分布式集群,底层设计了一个索引由众多 Shard 分片,所以添加文档时需要确定该文档属于哪个分片,确定规则为:

shard = hash(routing) % number_of_primary_shards
  • routing 是一个可变值,默认是文档的 ID,也可以设置成一个自定义的值
  • routing 通过 hash 函数生成一个数字,然后这个数字再除以 number_of_primary_shards(主分片的数量)后得到余数

2.2 写文档流程

写操作必须在主分片上面完成之后,才能被复制到其他节点作为分片副本。新建、索引和删除请求都是写操作。

  1. 客户端向 Master 发送写入请求,该节点作为协调节点
  2. 根据文档的 _id 确定分片,协调节点请求转到节点的主分片
  3. 在主分片上执行请求,成功之后,主分片根据副本数将请求并行转到副本分片对应节点
  4. 一旦副本分片执行完成,都向主分片报告成功,主分片将协调节点报告成功,协调节点向客户端报告成功

2.3 读文档流程

一个搜索请求必须询问请求的索引中所有分片的某个副本来进行匹配。一次检索流程主要分为两个阶段:

Query 阶段

  1. 客户端发送 Search 请求到协调节点
  2. 协调节点将查询请求转发到索引的每个主分片或副分片中
  3. 每个分片在本地执行查询,并使用本地的 Term/Document Frequency 信息进行打分,添加结果到大小 from+size 的本地优先队列中
  4. 每个分片返回各自优先队列中所有文档的 ID 和排序值给协调节点,协调节点合并这些值到自己的优先队列中,产生一个全局排序后的列表

Fetch 阶段

  1. 协调节点向相关节点发送 GET 请求
  2. 分片所在节点向协调节点返回数据
  3. 协调节点等待所有文档数据被取得,然后返回给客户端

三、错误速查

症状根因定位修复关键词
能写入,搜索却查不到或结果很少分词器不一致:索引时和查询时使用了不同 analyzer用 _analyze API 查看索引与查询实际生成的 terms
查询只命中部分文档,相关性排序”很奇怪”倒排索引中 TF/DF 与 BM25 打分机制未理解使用 explain API 查看单条命中文档的评分构成
写入后立刻搜索,短时间内查不到新文档不了解 refresh 机制:索引到倒排索引之间存在 refresh 周期查看索引的 refresh_interval 配置
部分分片 QPS 明显过高,集群负载不均routing 设计不当,导致文档集中落在少数分片合理选择 routing 字段,避免低基数字段
搜索整体变慢,CPU 使用率高倒排索引膨胀:字段过多、text 字段滥用对只用于展示的字段设置 index: false
短语/邻近查询效果差未开启或未正确使用位置(position)信息使用 match_phrase/span 系列查询
集群偶发写入失败或”版本冲突”异常对同一文档频繁更新,未理解主分片 + 副本异步复制与乐观锁版本机制合理设计更新策略,减少无意义的全量覆盖更新
搜索结果跨页翻页越深越慢使用高 from + size 深分页,导致多个分片上的倒排结果集合反复合并对业务深分页使用 search_after 或游标/滚动查询

四、总结

倒排索引是 Elasticsearch 实现毫秒级全文搜索的核心数据结构。通过理解倒排索引的原理、分片路由机制以及 Query/Fetch 两阶段搜索流程,可以更好地优化 Elasticsearch 查询性能、设计合适的分词策略以及解决搜索相关问题。