Elasticsearch 容易被归类成"分布式搜索引擎"或"日志分析平台"。这些标签描述了它的用途,但没有说清楚它的内部结构。把 Elasticsearch 拆到最底层,剩下的核心数据结构只有一个:倒排索引(inverted index)。mapping、analysis、shard、replica、aggregation 五个机制围绕这个数据结构逐层展开。

Elasticsearch 是一个以倒排索引为核心数据结构的分布式搜索与分析引擎。

本篇是系列导读。核心任务只有一个:说清楚倒排索引是什么,以及它和关系型数据库里的 B+Tree 索引有什么本质区别。

倒排索引的基本结构

关系型数据库的 B+Tree 索引从行出发。给定一个索引键值,B+Tree 能定位到存储这行数据的磁盘页。查找路径是:

1
索引键值 → B+Tree 内部节点 → 叶子节点 → 行指针 → 数据页

倒排索引从词项(term)出发。给定一个词项,倒排索引能定位到包含这个词项的所有文档。查找路径是:

1
词项 → Term Dictionary → Posting List → [doc1, doc2, doc3, ...]

两条路径的起点不同。B+Tree 回答的问题是"这个键值对应哪一行",倒排索引回答的问题是"哪些文档包含这个词"。

倒排索引的核心由三层结构组成:

1
2
3
4
5
6
7
8
Term Dictionary        Posting List             Documents
┌──────────┐ ┌─────────────────────┐ ┌─────────────┐
│ "search" │────→│ doc1:2, doc3:1 │───→│ doc1: {...} │
│ "engine" │────→│ doc1:1, doc2:3 │ │ doc2: {...} │
│ "index" │────→│ doc2:1, doc3:2 │ │ doc3: {...} │
│ "lucene" │────→│ doc1:1 │ └─────────────┘
└──────────┘ └─────────────────────┘
docId : term_frequency

Term Dictionary 是一个有序的词项表,通常用 FST(Finite State Transducer)压缩存储。Posting List 记录每个词项出现在哪些文档中,以及出现的频率、位置等信息。Documents 是原始文档的存储。

下面这张图把三层结构和查询方向放在一起,回答的问题是:一个搜索词经过哪些环节最终定位到文档。

graph LR
    Q["搜索词<br/>e.g. search"] --> TD["Term Dictionary<br/>FST 有序词项表"]
    TD --> PL["Posting List<br/>doc1:2, doc3:1"]
    PL --> D1["doc1"]
    PL --> D3["doc3"]

    style TD fill:#e8f4fd,stroke:#1a73e8
    style PL fill:#fef7e0,stroke:#f9ab00
    style D1 fill:#e6f4ea,stroke:#34a853
    style D3 fill:#e6f4ea,stroke:#34a853

三层之间是单向的包含关系:Term Dictionary 负责定位词项,Posting List 负责枚举文档 ID 和频率,Documents 负责存储原始内容。搜索时只需要前两层;需要返回原文时才访问第三层。

B+Tree 索引 vs 倒排索引

维度 B+Tree 索引 倒排索引
查找起点 索引键值(如主键、列值) 词项(文本分析后的 token)
查找结果 一行或少数几行 包含该词项的所有文档集合
擅长的查询 等值查询、范围查询、排序 全文搜索、关键词匹配
更新方式 原地更新(in-place update) 追加新段(append new segment)
数据组织 行式(一行的所有列在一起) 词项式(一个词指向多个文档)
典型系统 MySQL, PostgreSQL, Oracle Elasticsearch, Solr, Lucene

B+Tree 的优势在精确定位。当查询条件是"找到 id=42 的那行数据"时,B+Tree 从根节点开始二分,经过 3-4 层就能到达目标。

倒排索引的优势在全文检索。当查询条件是"找到包含 search 和 engine 这两个词的所有文档"时,倒排索引分别找到两个词项的 Posting List,做交集运算,就得到同时包含两个词的文档。

为什么 Elasticsearch 选择倒排索引

Elasticsearch 底层使用 Apache Lucene 作为存储和搜索引擎。每个 ES shard 本质上就是一个 Lucene index。Lucene 的核心数据结构就是倒排索引。

这个选择不是偶然的。Elasticsearch 要解决的核心问题是全文搜索——给定一段自然语言查询,快速找到最相关的文档。这个问题的性质决定了数据结构的选择:

  • 查询的输入不是精确键值,而是自然语言文本。文本需要先经过分析(analysis)变成词项,然后在词项级别做匹配。
  • 查询的结果不是精确一行,而是一个按相关性排序的文档列表。相关性计算需要词频(TF)和逆文档频率(IDF)等统计信息,这些信息天然存储在倒排索引的 Posting List 中。
  • 查询需要跨多个字段搜索。倒排索引为每个字段独立建索引,多字段搜索就是多个 Posting List 的集合运算。

从这个视角看,Elasticsearch 的其他机制都是围绕倒排索引展开的:

1
2
3
4
5
6
倒排索引
├── Mapping:定义文档的字段结构,决定每个字段怎样建索引
├── Analysis:把原始文本变成可索引的词项
├── Shard:把一个大的倒排索引切分到多个节点上
├── Replica:把倒排索引复制多份以提供高可用和读扩展
└── Aggregation:在倒排索引的基础上做实时统计分析

文档从写入到可搜索要经过一条完整的处理管道。原始文本先由 Analysis 拆成词项,词项写入倒排索引形成 Segment,多个 Segment 构成 Shard,多个 Shard 分布在集群节点上。

flowchart LR
    RAW["原始文档"] --> ANA["Analysis<br/>分词 + 标准化"]
    ANA --> SEG["Segment<br/>倒排索引写入"]
    SEG --> SHARD["Shard<br/>= Lucene Index"]
    SHARD --> CLUSTER["Cluster<br/>多节点分布"]

    style RAW fill:#fce4ec,stroke:#d32f2f
    style ANA fill:#e8f4fd,stroke:#1a73e8
    style SEG fill:#fef7e0,stroke:#f9ab00
    style SHARD fill:#e6f4ea,stroke:#34a853
    style CLUSTER fill:#f3e5f5,stroke:#7b1fa2

这条管道中,倒排索引处于中间位置——它是 Analysis 的输出目标,也是 Shard / Replica / Aggregation 的底层依赖。后续文章会逐层展开管道中的每个环节。

实验:观察倒排索引的工作方式

以下实验使用 Elasticsearch REST API。可以用 curl 或 Kibana Dev Tools 执行。

创建一个测试 index 并写入三条文档:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 创建 index
PUT /test-inverted-index
{
"settings": {
"number_of_shards": 1,
"number_of_replicas": 0
}
}

# 写入文档
POST /test-inverted-index/_bulk
{"index": {"_id": "1"}}
{"title": "Elasticsearch is a distributed search engine"}
{"index": {"_id": "2"}}
{"title": "Lucene is the search library behind Elasticsearch"}
{"index": {"_id": "3"}}
{"title": "A search engine indexes documents using inverted index"}

_analyze API 观察分词结果:

1
2
3
4
5
POST /test-inverted-index/_analyze
{
"field": "title",
"text": "Elasticsearch is a distributed search engine"
}

返回结果(使用默认 standard analyzer):

1
2
3
4
5
6
7
8
9
10
{
"tokens": [
{"token": "elasticsearch", "position": 0},
{"token": "is", "position": 1},
{"token": "a", "position": 2},
{"token": "distributed", "position": 3},
{"token": "search", "position": 4},
{"token": "engine", "position": 5}
]
}

原始文本 “Elasticsearch” 被转成了小写的 “elasticsearch”。这就是 analysis 的作用——把原始输入标准化为可索引的词项。

搜索包含 “search” 的文档:

1
2
3
4
5
6
7
8
GET /test-inverted-index/_search
{
"query": {
"match": {
"title": "search"
}
}
}

三条文档都会被返回,因为 “search” 这个词项出现在所有三条文档的倒排索引中。返回结果按 BM25 相关性评分排序。

搜索同时包含 “search” 和 “distributed” 的文档:

1
2
3
4
5
6
7
8
9
10
11
GET /test-inverted-index/_search
{
"query": {
"match": {
"title": {
"query": "distributed search",
"operator": "and"
}
}
}
}

只有文档 1 被返回。match 查询默认对查询文本做分析,产生 “distributed” 和 “search” 两个词项,然后要求两个词项的 Posting List 做交集。

模式提炼:从行定位到词项定位

1
2
B+Tree 范式:   key → row location → data page
Inverted Index:term → posting list → document set

两种索引结构代表两种不同的数据访问范式。B+Tree 围绕"行"组织——给定一个键,定位到一行。倒排索引围绕"词项"组织——给定一个词,定位到一组文档。

范式 核心操作 典型查询 结果特征
行定位(B+Tree) 二分查找 → 精确定位 WHERE id = 42 通常返回一行或少数行
词项定位(Inverted Index) 词典查找 → 集合运算 搜索 “distributed search” 返回文档集合,按相关性排序

这两种范式不是互斥的。Elasticsearch 内部也使用类似 B+Tree 的结构(BKD-tree)来处理数值范围查询和地理坐标查询。倒排索引负责文本搜索,BKD-tree 负责数值和空间查询,Doc Values 负责排序和聚合——但倒排索引是 Elasticsearch 存在的根本原因。

工程迁移表

概念 RDBMS (MySQL/PostgreSQL) Elasticsearch Apache Solr
核心索引结构 B+Tree 倒排索引(FST + Posting List) 倒排索引(同 Lucene)
数据存储单元 行(row) 文档(document) 文档(document)
模式定义 DDL (CREATE TABLE) Mapping schema.xml / managed-schema
文本处理 有限(LIKE, FULLTEXT) Analysis pipeline(丰富) Analysis chain(同等丰富)
分布式策略 分库分表(手动或中间件) 原生 shard + replica SolrCloud + ZooKeeper
底层引擎 InnoDB / PostgreSQL storage Apache Lucene Apache Lucene

常见误解

误解一:倒排索引就是"反向的索引"。 “inverted” 不是"反向"的意思。它描述的是数据组织方向的反转——从"文档包含哪些词"反转为"词出现在哪些文档中"。正排(forward index)是 document → terms,倒排是 term → documents。

误解二:Elasticsearch 是一个数据库。 Elasticsearch 可以存储和检索数据,但它的核心优化方向是搜索而不是事务。它没有 ACID 事务,没有外键约束,更新操作实际上是删除旧文档再写入新文档。把 Elasticsearch 当主数据库使用是一个常见的架构错误。

误解三:倒排索引只能做全文搜索。 倒排索引最初是为全文搜索设计的,但 Lucene 在倒排索引之外还提供了 Doc Values(列式存储,用于排序和聚合)、BKD-tree(用于数值和地理范围查询)、Stored Fields(用于返回原始文档)。Elasticsearch 的能力来自这些数据结构的组合,不仅仅是倒排索引。

练习

  1. 在 Docker 中启动一个单节点 Elasticsearch 集群,创建上面的测试 index,用 _analyze API 观察不同 analyzer(standard, simple, whitespace)对同一段文本的分词差异。

  2. 写入 10 条包含不同关键词的文档,用 match 查询搜索某个词,观察返回结果的 _score 字段。尝试理解为什么有些文档的分数更高(提示:词频和文档长度)。

  3. 对比 match 查询和 term 查询的结果差异。对 title 字段分别用 match: "Search"term: "Search" 查询,观察哪个有结果哪个没有。思考原因(提示:match 会先做分析,term 不会)。

系列路线图

本系列共 18 篇,从倒排索引出发,逐层展开 Elasticsearch 的存储、搜索、聚合和分布式机制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
核心抽象
00 导读:倒排索引(本篇)
01 架构总览:Node、Cluster 与集群状态

存储层
02 Lucene 内部:Segment、倒排索引与 Doc Values

数据建模与文本处理
03 Mapping 与字段类型
04 Analysis 管道

写入路径
05 近实时、Translog 与 Refresh/Flush

搜索路径
06 搜索执行模型:Query-Then-Fetch
07 相关性评分:BM25
08 Query DSL:从 match 到 bool

聚合与分析
09 Aggregation 框架

分布式
10 分片与路由
11 副本与高可用
12 集群协调:Master 选举

存储生命周期
13 Segment Merge 与 ILM

运维与工程
14 性能模型
15 安全体系
16 生产运维
17 ES vs Solr

系列导航

上一篇 下一篇
架构总览:Node、Cluster 与集群状态

参考资料