Elasticsearch 倒排索引底层原理详解:从 Term Dictionary、FST 到海量数据性能优化

Elasticsearch 倒排索引底层原理详解:从 Term Dictionary、FST 到海量数据性能优化

摘要: 本文从 Elasticsearch 倒排索引的基本原理出发,逐步深入 Lucene 的词项词典(Term Dictionary)、词项索引(Term Index)、倒排列表(Posting List)、FST、Segment 以及 BM25 相关性评分机制。同时重点分析三个常见问题:词项词典是如何排序的?为什么利用公共前缀能够加速词项查找?当文档数量达到千万甚至上亿时,词项索引是否会越来越慢?

一、什么是倒排索引?

倒排索引(Inverted Index)是 Elasticsearch 实现高效全文检索的核心数据结构之一。

可以用一句话概括:

倒排索引就是将"文档中包含哪些词",转换为"某个词出现在哪些文档中"的数据结构。

1.1 正向索引与倒排索引

假设有三篇文章:

文档 ID 文章内容
1 Java Spring Boot
2 Java Spring Cloud
3 Python Flask

如果按照文档存储内容,可以抽象为:

text 复制代码
Doc 1 → Java, Spring, Boot
Doc 2 → Java, Spring, Cloud
Doc 3 → Python, Flask

这种形式可以称为正向映射:

Document → Terms

如果需要查询包含 Spring 的文章,在没有合适索引的情况下,就可能需要逐篇检查内容。

但 Elasticsearch 使用倒排索引,将数据组织成:

text 复制代码
java   → [1, 2]
spring → [1, 2]
boot   → [1]
cloud  → [2]
python → [3]
flask  → [3]

也就是:

Term → Documents

当用户搜索 Spring 时,ES 可以通过倒排索引快速找到文档 1 和文档 2,而不必扫描所有文章。

这就是倒排索引能够提升全文搜索性能的根本原因。

需要注意,倒排索引并不意味着任意查询都是 O(1)。实际搜索耗时还取决于词项数量、匹配文档数量、查询条件、磁盘与缓存状态等因素。


二、倒排索引内部存储了什么?

实际倒排索引不仅保存 Term 和 DocID,还可能保存词频、位置和偏移信息。

2.1 Term(词项)

Term 是参与索引和搜索的基本单位。

例如:

text 复制代码
Java Spring Boot

经过分词器处理,可能得到:

text 复制代码
java
spring
boot

需要注意:

  • Term 不一定是自然语言中的完整单词。
  • 英文通常会经过大小写转换。
  • 中文需要考虑合适的中文分词器。
  • keyword 类型的字段通常将整个字段值视为一个 Term。
  • 在 Lucene 中,Term 需要结合字段(Field)来理解,而不是整个索引共享一个没有字段区分的词表。

2.2 DocID(文档编号)

DocID 表示词项出现在哪些文档中。

例如:

text 复制代码
spring → [1, 2, 5, 8]

这里有一个容易混淆的知识点:

Lucene 的内部 DocID 与 Elasticsearch 的 _id 并不是同一个概念。

  • _id 是 Elasticsearch 对外暴露的文档标识。
  • Lucene DocID 是 Segment 内部用于访问文档的编号。
  • Segment 合并后,Lucene 内部 DocID 可能发生变化。

2.3 Term Frequency(TF,词频)

表示词项在某篇文档中出现了多少次。

例如:

text 复制代码
Doc 1: java java spring
Doc 2: java mysql

那么:

text 复制代码
java:
    Doc 1 → TF = 2
    Doc 2 → TF = 1

词频可以参与相关性评分,但并不是简单地按照出现次数进行线性加分。

2.4 Position(词项位置)

Position 记录词项在分词序列中的位置。

例如:

text 复制代码
Doc 1: quick brown fox
        0     1    2

Doc 2: quick fox jumps
        0    1     2

搜索短语:

text 复制代码
quick fox

在默认精确短语匹配条件下:

  • Doc 1 不匹配,因为中间存在 brown。
  • Doc 2 匹配,因为两个词相邻且顺序正确。

这就是 match_phrase 可以完成短语搜索的重要基础。

另外,Lucene 也可以保存 Offset(字符偏移量),为部分高亮功能提供支持。

具体保存哪些信息,取决于字段的 index_options 等配置。


三、Lucene 倒排索引的核心组成

Elasticsearch 底层使用 Apache Lucene 提供搜索能力。

从逻辑上看,Lucene 的倒排索引可以分为三个重要部分:

  1. Term Dictionary(词项词典)
  2. Term Index(词项索引)
  3. Posting List(倒排列表)

它们之间的关系如下:

text 复制代码
                 查询 spring
                      |
                      v
            +------------------+
            |   Term Index     |
            |    词项索引       |
            +------------------+
                      |
               定位词项区域
                      |
                      v
            +------------------+
            | Term Dictionary  |
            |    词项词典       |
            +------------------+
                      |
               找到具体 Term
                      |
                      v
            +------------------+
            |   Posting List   |
            |    倒排列表       |
            +------------------+
                      |
                      v
                Doc 1, Doc 2

这张图是逻辑示意图,实际的词典文件、索引文件与倒排数据组织会根据 Lucene 版本和 Codec 实现有所不同。

3.1 Term Dictionary(词项词典)

Term Dictionary 保存某个字段已经索引的不同词项及其相关统计信息和元数据。

假设文档中出现了:

text 复制代码
spring
springboot
springcloud
apple
application
banana

那么词项词典会按照字典序组织这些词项。

例如:

text 复制代码
apple
application
banana
spring
springboot
springcloud

Lucene 不需要为每个文档重复存储相同的词项字符串,而是通过词典和倒排列表建立关联。

例如,100 万篇文档中都包含 spring,并不意味着需要在词典中保存 100 万份 spring。

3.2 Term Index(词项索引)

假设词项词典中有 100 万个不同的 Term。

如果每次查找都需要从头遍历词典,成本会很高。

为此,Lucene 会构建词项索引,帮助快速定位对应词项在词典中的位置。

可以将它理解为一本大型字典前面的目录。

例如,需要查找 springboot 时:

text 复制代码
Term Index
    |
    +-- a...
    |
    +-- b...
    |
    +-- s...
         |
         +-- sp...
              |
              +-- spr...
                   |
                   +-- spring...

词项索引利用前缀关系帮助定位相关词项区域。

但要注意:

词项索引主要解决的是"如何找到 Term",而不是直接保存这个 Term 对应的所有文档。

3.3 Posting List(倒排列表)

Posting List 保存某个 Term 对应的文档信息。

例如:

text 复制代码
spring:
    Doc 1 → TF: 2, Position: [0, 5]
    Doc 2 → TF: 1, Position: [1]
    Doc 8 → TF: 1, Position: [3]

Posting List 中的 DocID 通常按顺序组织。

这种有序结构非常重要,因为查询多个 Term 时,可以利用有序迭代器、跳跃机制等进行高效处理。

例如:

text 复制代码
spring → [1, 2, 5, 8]
java   → [1, 3, 5, 9]

如果查询条件是:

text 复制代码
spring AND java

则匹配文档为:

text 复制代码
[1, 5]

如果查询条件是:

text 复制代码
spring OR java

则匹配文档为:

text 复制代码
[1, 2, 3, 5, 8, 9]

这是集合运算层面的简化表示。Lucene 的实际查询执行不会总是完整展开列表再进行集合计算,还会结合迭代器和查询优化机制。


四、词项词典到底是怎么排序的?

这是理解词项索引的重要基础。

4.1 是按照首字母排序吗?

可以这样理解,但不够准确。

Lucene 的词项按照字节字典序排序,而不是只根据第一个字母。

假设词典包含:

text 复制代码
springboot
apple
spring
application
banana
apply

排序结果:

text 复制代码
apple
application
apply
banana
spring
springboot

比较过程是:

  1. 首先比较第一个字节。
  2. 如果相同,继续比较第二个字节。
  3. 如果仍然相同,继续比较后续字节。
  4. 如果一个词是另一个词的完整前缀,通常较短的词排在前面。

例如:

text 复制代码
spring
springboot
springcloud

三者具有公共前缀:

text 复制代码
spring

因此会出现在相邻的词典区域。

对于普通 ASCII 小写英文,可以近似理解为我们熟悉的英文词典排序。

不过,对于中文,Lucene 默认不会按照拼音排序,而是按照词项编码后的字节序列排序。

4.2 为什么排序很重要?

因为排序可以让具有相同前缀的词项聚集在一起。

例如:

text 复制代码
spring
springboot
springcloud
springdata
springsecurity
springweb

它们都包含公共前缀:

text 复制代码
spring

这为前缀压缩、词项分块和前缀索引创造了条件。


五、为什么可以利用公共前缀加速查找?

我们可以先设计一个简单的词项索引方案。

假设词典中有:

text 复制代码
apple
application
apply
banana
spring
springboot
springcloud

可以先按照首字母分组:

text 复制代码
a → apple、application、apply

b → banana

s → spring、springboot、springcloud

当搜索 springboot 时,先定位 s,就可以排除 a 和 b 对应的词项。

但是,这种方案存在一个问题:

如果 s 开头的词项有几十万个,定位范围还是太大。

因此,可以继续利用更长的公共前缀:

text 复制代码
s
|
p
|
r
|
i
|
n
|
g
|-------------------|
|         |         |
[Term]    b         c
spring    |         |
          o         l
          |         |
          o         o
          |         |
          t         u
                    |
                    d

这样便可以通过前缀逐步缩小搜索范围。

这其实已经接近 Trie(前缀树)的基本思想。

5.1 FST 是什么?

FST 全称为 Finite State Transducer,即有限状态转换器。

它可以理解为一种紧凑的、能够关联输出信息的有限状态结构。

在经典 Lucene BlockTree 词项词典实现中,FST 被用于构建词项索引。

其主要特点包括:

  • 利用词项公共前缀组织索引。
  • 能够共享部分状态和路径,减少冗余。
  • 可以根据词项前缀定位对应的词项块。
  • 支持高效的词项查找。

假设词项有:

text 复制代码
spring
springboot
springcloud

它们具有相同前缀:

text 复制代码
spring

如果每个词都独立保存完整路径,会产生重复。

而利用公共前缀结构,可以复用这一部分。

需要注意:FST 并不是简单的普通 Trie,它还具有状态共享、输出关联等特点。

5.2 FST 是否直接对应每个 Posting List?

不一定。

以 Lucene 9.x 的 BlockTree 词项词典为例,典型实现采用:

FST 词项索引 + 按公共前缀组织的词项块。

大致流程如下:

text 复制代码
搜索 springboot
        |
        v
  FST 词项索引
        |
        v
定位相关的 Term Block
        |
        v
在词项块中找到 springboot
        |
        v
读取对应倒排数据
        |
        v
获取匹配的文档

这样可以避免为每个 Term 都在内存索引中建立一个独立、完整的倒排列表指针入口。

另外,不同 Lucene 版本的实现存在差异。

例如,Lucene 9.x 的 Lucene90 BlockTree 实现使用 FST 词项索引,而 Lucene 10.3 的 Lucene103 BlockTree 实现采用前缀 Trie 词项索引。

因此,不能认为 Lucene 所有版本的词项索引都一定使用 FST。

但是,公共前缀导航、词项分块和高效定位的核心思想是一致的。

5.3 为什么不直接增加一个首字母索引?

假设为每个首字母建立一个索引:

text 复制代码
a → ...
b → ...
c → ...
...
s → ...

这种方式确实能够缩小查找范围。

但是,它存在三个问题。

第一,首字母区分能力有限。

例如,以下词项全部以 s 开头:

text 复制代码
spring
springboot
springcloud
springdata
springsecurity

只靠首字母,很难进一步提高查找效率。

第二,适用范围有限。

Lucene 不仅需要索引英文,还需要索引中文、数字及其他字符。

因此,通用的字节前缀索引更加合适。

第三,额外索引不一定更快。

既然前缀结构本身已经支持从第一个字节开始导航,再额外增加首字母索引,反而可能增加空间和维护成本。

所以,可以这样理解:

首字母索引是前缀索引的一种简单形式,而 Lucene 使用的是更加通用、更加精细的前缀定位结构。


六、Elasticsearch 写入数据时如何建立倒排索引?

理解了倒排索引的数据结构,接下来看看实际写入流程。

假设我们写入以下数据:

json 复制代码
{
  "title": "Java Spring Boot",
  "category": "technology"
}

6.1 Analyzer(分析器)

对于 text 类型字段,ES 会使用 Analyzer 对文本进行处理。

Analyzer 通常由以下组件组成:

组件 主要作用
Character Filter 对原始字符进行预处理
Tokenizer 将文本切分为 Token
Token Filter 对 Token 进行进一步处理

例如:

text 复制代码
原始字符串:
Java Spring Boot

经过 Tokenizer:
Java
Spring
Boot

经过 lowercase Token Filter:
java
spring
boot

然后 Lucene 根据生成的 Term 建立倒排数据。

需要注意,具体分词结果取决于 Analyzer 配置。

6.2 写入流程

text 复制代码
Java 应用写入 JSON
        |
        v
Elasticsearch 接收文档
        |
        v
根据 Mapping 识别字段类型
        |
        v
Analyzer 处理 text 字段
        |
        v
生成 Term 和相关索引信息
        |
        v
Lucene 建立索引结构
        |
        v
进入 Segment 管理体系
        |
        v
Refresh 后可被搜索

索引过程中还涉及内存缓冲、事务日志等机制,上图主要展示与倒排索引相关的逻辑。

6.3 text 与 keyword 的区别

这是实际项目中必须理解的一个问题。

假设有一个字段:

json 复制代码
{
  "title": "Java Spring Boot"
}

如果类型为 text,经过标准分词后可能产生:

text 复制代码
java
spring
boot

如果类型为 keyword,则通常将整个值作为一个 Term:

text 复制代码
Java Spring Boot

两种类型的区别如下:

特性 text keyword
默认是否分词 是 否
适合全文搜索 是 通常不用于分词全文搜索
适合精确匹配 需要注意分词 是
适合排序与聚合 通常不直接使用 是
是否可以使用倒排索引 是 是

需要特别强调:

keyword 字段也可以建立倒排索引,只不过它通常将整个字段值作为一个词项。

在实际开发中,可以使用多字段 Mapping:

json 复制代码
{
  "title": {
    "type": "text",
    "analyzer": "standard",
    "fields": {
      "keyword": {
        "type": "keyword"
      }
    }
  }
}

这样:

  • title 负责全文搜索。
  • title.keyword 负责完整值精确匹配、排序或聚合。

七、Elasticsearch 搜索时如何使用倒排索引?

当用户提交一个查询:

text 复制代码
Java Spring

对于使用标准分词器的 text 字段,查询文本通常会被分析为:

text 复制代码
java
spring

假设倒排列表为:

text 复制代码
java   → [1, 2, 5, 8]
spring → [1, 3, 5, 9]

7.1 AND 查询

如果要求两个 Term 都必须存在:

text 复制代码
java AND spring

相当于求交集:

text 复制代码
[1, 2, 5, 8]
      ∩
[1, 3, 5, 9]
      =
    [1, 5]

对应的查询 DSL:

json 复制代码
GET articles/_search
{
  "query": {
    "match": {
      "title": {
        "query": "Java Spring",
        "operator": "and"
      }
    }
  }
}

7.2 OR 查询

如果只需要匹配其中一个 Term:

text 复制代码
java OR spring

相当于求并集:

text 复制代码
[1, 2, 3, 5, 8, 9]

对应 DSL:

json 复制代码
GET articles/_search
{
  "query": {
    "match": {
      "title": {
        "query": "Java Spring",
        "operator": "or"
      }
    }
  }
}

对于常见的 match 查询,多词项默认使用 OR 语义。

7.3 match_phrase 查询

假设文档内容如下:

text 复制代码
Doc 1: Java Spring Boot

Doc 2: Java Advanced Spring Boot

搜索:

text 复制代码
Java Spring

如果使用 match + and,两个文档都可能匹配,因为它们都包含 java 和 spring。

但如果使用:

json 复制代码
GET articles/_search
{
  "query": {
    "match_phrase": {
      "title": "Java Spring"
    }
  }
}

在默认 slop=0 的情况下,只有 Doc 1 匹配。

原因是 match_phrase 会利用词项位置信息检查顺序和距离。


八、BM25:找到文档之后,为什么还要计算评分?

倒排索引主要解决:

哪些文档包含查询词?

但 Elasticsearch 还需要解决:

哪些文档更加符合用户的搜索意图?

这就涉及相关性评分。

Elasticsearch 传统的词法全文搜索通常默认使用 BM25 相似度算法。

BM25 主要考虑以下因素:

8.1 TF(词频)

查询词在文档中出现了多少次。

一般来说,词频越高,可能意味着相关性越高,但 BM25 对词频具有饱和处理。

8.2 IDF(逆文档频率)

查询词在整个文档集合中有多稀有。

例如:

text 复制代码
java → 出现在 100 万篇文章中

lucene → 出现在 1 万篇文章中

在其他条件相近时,lucene 可能具有更强的区分能力。

8.3 文档长度

假设:

text 复制代码
Doc 1:100 个词,其中 spring 出现 3 次

Doc 2:10000 个词,其中 spring 出现 3 次

其他条件相同的情况下,Doc 1 往往更有优势。

因此,BM25 并不是简单地按照词频排序,而是综合考虑词频、文档频率和字段长度等因素。

可以这样记忆:

倒排索引负责找到候选文档,BM25 负责计算词法相关性。


九、Segment 与倒排索引是什么关系?

理解 Segment 才能进一步理解 Elasticsearch 的写入、更新、删除和搜索性能。

9.1 什么是 Segment?

一个 Elasticsearch Index 通常包含多个 Shard。

每个 Shard 底层是一个 Lucene 索引,而每个 Lucene 索引可以包含多个 Segment。

text 复制代码
Elasticsearch Index
       |
       +--- Shard 0
       |       |
       |       +--- Segment A
       |       |
       |       +--- Segment B
       |       |
       |       +--- Segment C
       |
       +--- Shard 1
               |
               +--- Segment D
               |
               +--- Segment E

每个 Segment 都包含自己的词项和倒排索引数据。

因此,一个 Elasticsearch Index 并不是只有一份全局词项词典。

9.2 为什么使用 Segment?

Segment 基本上是不可变的索引存储单元。

这样可以避免每写入一条新文档,都需要修改一个巨大的索引文件。

随着新数据写入,Lucene 可以生成新的 Segment。

同时,后台 Merge 机制会适时合并已有 Segment。

9.3 更新与删除如何处理?

假设更新一篇文章:

text 复制代码
旧文档:
Java Spring Boot

更新为:

text 复制代码
新文档:
Java Spring Cloud

Lucene 通常不会直接修改原有 Segment 中的全部倒排数据。

而是将旧的 Lucene 文档标记删除,并重新索引新版本文档。

后续 Segment Merge 会逐步清理部分已删除数据占用的空间。

因此,频繁更新也会增加索引维护和合并开销。

9.4 Refresh、Flush、Merge 的区别

操作 核心作用
Refresh 让近期写入的数据对搜索可见
Flush 执行持久化提交及相关事务日志处理
Merge 合并 Segment,改善存储结构并清理部分删除数据

需要注意:

Refresh 不等于 Flush,也不代表数据在此之前完全没有落盘。

Elasticsearch 被称为近实时搜索引擎,就是因为写入数据通常需要经过 Refresh 才能被普通搜索查询到。


十、核心问题:文档越来越多,词项索引会越来越慢吗?

这是理解 ES 海量数据搜索性能的关键问题。

假设最初有 100 万篇文档,后来增长到 1 亿篇。

那么:

  • 文档数量增加了 100 倍。
  • 不同 Term 的数量可能增加。
  • Posting List 的长度可能增加。
  • Segment 和磁盘数据规模可能发生变化。

但这里需要区分三个指标。

指标 含义
Document Count 文档总数量
Unique Term Count 不同词项的数量
Posting List Length 某个 Term 对应的文档数量

它们对性能的影响并不相同。

10.1 词项增多,不代表查找成本线性增长

假设初始词典只有:

text 复制代码
apple
banana
spring
springboot

后来增长到 100 万个不同 Term。

如果使用顺序扫描,查找成本可能随词项数量增长而显著增加。

但是使用前缀索引,就不必检查所有 Term。

例如,查找:

text 复制代码
springboot

依然可以沿着相关前缀定位目标词项块。

如果只考虑理想化的前缀树,查找成本主要与词项长度 L 有关,可以近似理解为 O(L),而不是与全部词项数量 T 成正比。

不过,真实 Lucene 还涉及块内查找、磁盘读取、缓存命中等因素,不能直接用 O(L) 描述完整查询成本。

结论是:

从 1 万个 Term 增长到 100 万个 Term,并不意味着定位一个固定 Term 的耗时就会增长 100 倍。

10.2 为什么词项过多仍然可能导致性能下降?

原因主要有三个。

原因一:词项索引和词典规模增加

例如有两个字段。

第一个是文章类型:

text 复制代码
technology
finance
education
sports

尽管文档很多,但是不同 Term 的数量较少。

第二个是 UUID:

text 复制代码
9f82a1...
4d73c6...
8b19e2...
...

如果每条文档的 UUID 都不一样,那么不同 Term 的数量可能接近文档数量。

这会使词典和倒排数据增大,增加索引构建、存储和缓存成本。

但由于 UUID 通常只对应很少的文档,精确查找单个 UUID 仍然可能很快。

原因二:倒排列表变长

假设:

text 复制代码
java → [1, 2, 3, ..., 10000]

后来文档数量增长,变成:

text 复制代码
java → [1, 2, 3, ..., 10000000]

此时定位 java 这个 Term 可能仍然很快。

但是,后续查询可能需要处理大量匹配文档。

例如:

  • 计算相关性评分。
  • 与其他查询条件组合。
  • 收集 Top K 结果。
  • 进行统计与聚合。

这时候,真正的瓶颈不一定是 Term Index,而可能是 Posting List 的处理成本。

Lucene 会通过跳跃、块级评分优化等机制减少不必要的操作,但无法保证所有高频词查询都很快。

原因三:Segment 数量过多

因为每个 Segment 都有自己的词项词典和倒排数据,所以一个 Shard 包含过多 Segment 时,可能增加:

  • 词项查找开销。
  • 查询执行与结果合并开销。
  • 文件和元数据管理成本。
  • 内存及缓存压力。

这也是 Lucene 需要 Merge 机制的原因之一。

10.3 为什么有些查询比另一些慢很多?

假设一个索引包含 100 万个不同 Term。

精确查询:

text 复制代码
springboot

可以直接定位明确的词项。

前缀查询:

text 复制代码
spring*

可能匹配:

text 复制代码
spring
springboot
springcloud
springdata
springframework
springsecurity
...

如果满足前缀条件的词项有 10 万个,查询就可能需要进行大量词项枚举及后续处理。

前导通配符查询:

text 复制代码
*spring

由于词项的结尾不等于词项索引所使用的开头前缀,普通前缀导航不容易直接用于这种查询。

具体成本还取决于 Lucene 的自动机执行和字段索引方式。

因此,这类查询通常需要格外关注性能。

10.4 关键结论

影响 Elasticsearch 搜索性能的因素并不只有文档数量。

可以总结为:

text 复制代码
搜索性能
   |
   +--- Term 查找成本
   |
   +--- Posting List 处理成本
   |
   +--- Segment 数量和大小
   |
   +--- 磁盘 I/O 与缓存命中率
   |
   +--- 查询类型与评分方式
   |
   +--- Shard 分布和查询协调开销

所以:

文档数量增加,不意味着 Term Index 查找速度必然线性下降。

但是,词项基数、倒排列表长度及系统资源使用情况的变化,确实可能使整个查询变慢。


十一、倒排索引与 Doc Values 的区别

倒排索引擅长:

text 复制代码
Term → DocID

例如:

text 复制代码
java → [1, 2, 5]

但假设有三个商品:

text 复制代码
Doc 1 → price = 5999
Doc 2 → price = 4999
Doc 3 → price = 2999

如果需要按价格排序:

sql 复制代码
ORDER BY price DESC

或者计算平均价格:

sql 复制代码
AVG(price)

那么按列读取字段值往往更合适。

ES 中的 Doc Values 就是专门服务于这类访问模式的重要数据结构。

对比项 倒排索引 Doc Values
数据访问方向 Term → Documents Field → 各文档对应的值
主要作用 搜索、匹配、过滤 排序、聚合、脚本访问
数据组织 倒排结构 列式结构
典型字段 text、keyword keyword、数值、日期等

需要注意,Doc Values 并不是传统意义上的完整正排索引,而是专门优化的列式数据结构。

另外,Elasticsearch 也不完全只使用倒排索引:

  • 文本字段:通常使用倒排索引。
  • 数值和日期字段:可以使用 BKD Tree 等结构。
  • 排序和聚合:大量使用 Doc Values。
  • 向量检索:可能使用 HNSW 等结构。

十二、Elasticsearch DSL 实战验证

下面通过一个实际例子来理解全文搜索和倒排索引。

12.1 创建索引

在 Kibana Dev Tools 中执行:

json 复制代码
PUT demo_articles
{
  "mappings": {
    "properties": {
      "title": {
        "type": "text",
        "analyzer": "standard",
        "fields": {
          "keyword": {
            "type": "keyword"
          }
        }
      },
      "category": {
        "type": "keyword"
      }
    }
  }
}

12.2 写入测试文档

json 复制代码
PUT demo_articles/_doc/1
{
  "title": "Quick Brown Fox",
  "category": "animal"
}
json 复制代码
PUT demo_articles/_doc/2
{
  "title": "Quick Fox Jumps",
  "category": "animal"
}
json 复制代码
PUT demo_articles/_doc/3
{
  "title": "Brown Dog Sleeps",
  "category": "animal"
}

为了方便实验,手动 Refresh:

http 复制代码
POST demo_articles/_refresh

实际生产环境中,不应为了每一次写入都立即调用 Refresh,因为频繁 Refresh 可能增加系统开销。

12.3 查看 Analyzer 分词结果

json 复制代码
GET demo_articles/_analyze
{
  "field": "title",
  "text": "Quick FOX Jumps"
}

主要结果:

json 复制代码
{
  "tokens": [
    {
      "token": "quick",
      "position": 0
    },
    {
      "token": "fox",
      "position": 1
    },
    {
      "token": "jumps",
      "position": 2
    }
  ]
}

这里只展示关键字段,实际响应还包含其他信息。

12.4 match 查询

json 复制代码
GET demo_articles/_search
{
  "query": {
    "match": {
      "title": {
        "query": "quick fox",
        "operator": "and"
      }
    }
  }
}

会匹配:

text 复制代码
Doc 1: Quick Brown Fox
Doc 2: Quick Fox Jumps

因为两个文档都包含 quick 和 fox。

12.5 match_phrase 查询

json 复制代码
GET demo_articles/_search
{
  "query": {
    "match_phrase": {
      "title": "quick fox"
    }
  }
}

仅匹配:

text 复制代码
Doc 2: Quick Fox Jumps

因为两个词相邻。

12.6 term 精确查询

json 复制代码
GET demo_articles/_search
{
  "query": {
    "term": {
      "title.keyword": "Quick Brown Fox"
    }
  }
}

会匹配 Doc 1。

需要注意,term 查询不会像 match 一样对普通 text 查询字符串进行分词分析。

因此,不建议直接使用 term 查询来代替 text 字段的全文搜索。

12.7 查看词项统计信息

可以利用 Term Vectors API 观察某篇文档中的词项信息:

json 复制代码
GET demo_articles/_termvectors/1
{
  "fields": ["title"],
  "positions": true,
  "term_statistics": true
}

其中可以查看词频、位置信息,以及请求启用的词项统计数据。

这比单纯观察搜索结果更有助于理解倒排索引。

不过,该 API 返回的是词项统计和向量视图,不是 Lucene 词项词典底层文件的完整展示。

12.8 查看 Segment

http 复制代码
GET _cat/segments/demo_articles?v

可以查看 Segment 的数量、大小等信息。

12.9 分析查询性能

json 复制代码
GET demo_articles/_search
{
  "profile": true,
  "query": {
    "match": {
      "title": "quick fox"
    }
  }
}

Profile API 可以提供查询执行过程的耗时信息,帮助定位搜索性能问题。

注意,开启 Profile 本身存在额外开销,不应将它当作不受干扰的性能基准测试。


十三、实际开发中如何优化 Elasticsearch 索引?

理解倒排索引后,可以从以下几个方面进行优化。

13.1 合理设计 Mapping

不是所有字符串字段都需要使用 text。

例如:

text 复制代码
订单状态 → keyword

订单编号 → keyword

文章标题 → text

文章内容 → text

商品价格 → 数值类型

创建时间 → date

避免不必要的分词和索引数据膨胀。

13.2 避免无意义的高基数字段分析

对于 UUID、订单号等字段,如果只需要完整值精确匹配,通常使用 keyword 更合适。

对于根本不需要检索的字段,还可以根据业务情况考虑关闭索引能力。

不过,关闭索引前需要评估字段是否需要参与过滤、排序或聚合。

13.3 谨慎使用通配符查询

尽量避免随意执行:

text 复制代码
*spring

尤其是在海量高基数字段中。

如果业务确实需要前缀、后缀或者子串搜索,应针对具体需求选择合适的 Mapping 和查询方案。

13.4 合理设置 Shard

并不是 Shard 越多,查询性能就越好。

过多 Shard 可能增加资源开销和查询协调成本。

过大的 Shard 又可能增加单分片维护、恢复以及查询压力。

应该结合索引规模、业务负载、硬件资源和实际性能测试进行设计。

13.5 关注 Segment 数量

Segment 过多可能增加查询成本。

通常应让 Lucene 自动完成合并,不要对持续写入的索引频繁执行强制合并。

13.6 使用索引生命周期管理

对于日志、监控、历史订单等持续增长的数据,可以结合时间或索引规模使用 Rollover 和索引生命周期管理策略。

这样可以避免单个写入目标无限增长,也便于冷热数据分层和过期数据清理。

13.7 通过实际指标定位瓶颈

分析性能时,应区分:

  • Term 查找是否慢。
  • 是否命中了过多文档。
  • 是否存在昂贵的排序或聚合。
  • Segment 和 Shard 是否过多。
  • 是否发生频繁的磁盘 I/O。
  • 查询是否需要枚举大量 Term。

不能仅根据文档数量判断一个索引是否应该优化。


十四、常见面试问题总结

Q1:Elasticsearch 为什么搜索速度快?

因为 Elasticsearch 底层基于 Lucene,使用倒排索引等数据结构,能够根据查询词直接定位匹配文档,避免在常见全文搜索场景下逐篇扫描所有原始文档。

同时结合词项索引、压缩、跳跃机制、缓存、Segment 管理和分布式查询等技术提升效率。

Q2:Term Index 和 Term Dictionary 有什么区别?

Term Dictionary 负责保存和组织词项及相关元数据。

Term Index 负责帮助快速定位词项词典中的相关区域。

前者类似字典正文,后者类似字典目录。

Q3:词项词典是按照首字母排序的吗?

不是仅按照首字母,而是通常按照词项的字节字典序排列。

对于小写英文,表现为依次比较第一个、第二个、第三个字符。

Q4:FST 为什么可以提高查找效率?

因为它能够利用词项之间的公共前缀和紧凑状态结构进行索引,减少冗余,并高效定位相关的词项范围。

但不是所有 Lucene 版本的默认词项索引都使用 FST。

Q5:文档增加后,词项索引一定会越来越慢吗?

不一定。

查找一个确定 Term 的成本通常不会随词项总数线性增长。

但是,随着词典规模、倒排列表长度、Segment 数量和 I/O 压力增加,整个查询仍然可能变慢。

Q6:text 和 keyword 都有倒排索引吗?

默认情况下,两者都可以使用倒排索引。

主要区别在于:

  • text 通常先经过 Analyzer 分词。
  • keyword 通常将整个字段值作为一个词项。

Q7:Segment 为什么需要 Merge?

因为 Lucene Segment 基本不可变。

Merge 可以合并已有 Segment,减少部分索引管理开销,并清理部分已删除文档占用的空间。

Q8:倒排索引和 BM25 是什么关系?

倒排索引提供匹配文档及相关词项信息。

BM25 利用词频、文档频率、字段长度等信息计算词法相关性分数。

一个主要解决候选匹配问题,另一个主要解决结果排序问题。


十五、全文总结

经过前面的分析,我们可以将 Elasticsearch 全文搜索的核心流程总结为:

text 复制代码
                  写入阶段

             JSON 文档
                 |
                 v
               Mapping
                 |
                 v
               Analyzer
                 |
                 v
              生成 Term
                 |
                 v
             Lucene 索引
                 |
                 v
          Term Dictionary
          Term Index
          Posting List
                 |
                 v
               Segment


                  查询阶段

              用户查询
                 |
                 v
              查询分析
                 |
                 v
              生成 Term
                 |
                 v
             Term Index
                 |
                 v
           Term Dictionary
                 |
                 v
             Posting List
                 |
                 v
             文档匹配
                 |
                 v
             BM25 评分
                 |
                 v
            返回搜索结果

最终需要记住以下几个核心结论:

第一,倒排索引的本质是 Term → Documents。

它通过提前构建词项与文档之间的映射,避免普通全文搜索时扫描全部文档。

第二,词项词典和词项索引并不是同一个东西。

词项词典负责组织 Term,词项索引帮助快速定位 Term,倒排列表负责关联匹配文档。

第三,词项词典按字节字典序组织,公共前缀是优化的重要基础。

Lucene 通过 FST 或 Trie 等实现,以及分块等机制,高效管理和定位大量词项。

第四,文档数量增加,不代表词项查找速度会线性下降。

真正影响整体查询性能的,还包括词项基数、倒排列表长度、Segment 数量、缓存、I/O 以及查询方式。

第五,Elasticsearch 的高性能来源于一整套协作机制,而不仅仅是倒排索引。

倒排索引解决候选文档定位问题,Lucene 的相关数据结构和执行优化进一步降低成本,Segment 与分片机制支持大规模数据管理,BM25 等算法负责相关性计算。

理解这些内容之后,再深入研究 Lucene 的 BlockTree、FST/Trie、Posting List 压缩和跳跃机制,以及 Elasticsearch 的 Query Phase、Fetch Phase,就可以逐步建立完整的搜索引擎底层知识体系。


参考资料

  1. Elasticsearch 官方文档 --- Index fundamentals
  2. Elasticsearch 官方文档 --- Index and search analysis
  3. Elasticsearch 官方文档 --- Near real-time search
  4. Elasticsearch 官方文档 --- Term query
  5. Elasticsearch 官方文档 --- Analyze index disk usage
  6. Lucene 9.12 --- BlockTree Terms Writer
  7. Lucene 10.3 --- BlockTree Terms Reader

适用读者: Java 后端开发工程师、Elasticsearch 学习者、搜索引擎原理学习者。

相关推荐
Navicat中国7 小时前
Navicat 18 内置 AI 助手让数据库开发从繁琐操作迈向智能协作
大数据·人工智能·数据库开发
知了学历孙老师9 小时前
小自考的校考和统考,出题方和难度差在哪
java·大数据·前端
XCDoris10 小时前
郴州logo设计机构
大数据·人工智能·python
物联网软硬件开发-轨物科技10 小时前
【技术白皮书】光伏清检一体化机器人系统解决方案技术白皮书
大数据·运维·网络·人工智能·机器人
zyseo811 小时前
谷歌SEO 准首页词挖掘脚本:找出排在第 8-20 名的隐藏流量
大数据·数据库·人工智能
xiaogai_gai13 小时前
仅查询策略实战:从金蝶云·星空旗舰版定时拉取收料通知单
大数据·数据库·python
jonyleek14 小时前
JVS数字底座私有化部署实践:Spring Cloud + K8S 实现文档平台开箱即用
elasticsearch·kubernetes·私有化部署·springcloud·minio·jvs企业文档·文档治理
skywsp14 小时前
Agent智能体架构设计-知识整理
大数据·人工智能
starzy199014 小时前
KafkaProducer 加载元数据:一条 MetadataRequest 的完整旅程
大数据·kafka