从技术设计的角度来看,Elasticsearch 的每一个组件都不是凭空产生的,它们是为了解决分布式环境下的高并发写入、海量数据检索和系统高可用这三大难题。

我们可以把 ES 的架构拆解为四个核心维度,剖析每一个组件存在的“必要性”。


1. 存储层:Segment、Translog 与 Lucene

核心问题:如何在保证高性能写入的同时,实现数据不丢失并支持近实时搜索?

Segment (段)

  • 为何设计:如果每次写入都更新整个索引,磁盘 I/O 会导致系统瘫痪。
  • 原理:ES 将索引拆分为多个小的 Segment。它是不可变的。
  • 设计价值:不可变意味着查询不需要加锁,且能利用操作系统的文件缓存(OS Cache)。

Translog (事务日志)

  • 为何设计:数据写入后先存在内存(Buffer)中,如果此时断电,数据会丢失。
  • 原理:在写内存的同时,同步写一份磁盘日志(Translog)。
  • 设计价值:保证了数据的持久化(Durability)。即使 JVM 崩溃,重启后也能通过 Translog 恢复尚未固化到 Segment 的数据。

2. 索引层:FST 与 Doc Values

核心问题:如何既能快速“搜词”,又能快速“排序/聚合”?

FST (有限状态转移机)

  • 为何设计:词典(Term Dictionary)太大,内存放不下,磁盘搜太慢。
  • 原理:将词典索引压缩成一个高度复用前缀和后缀的图结构。
  • 设计价值:让海量词项的索引能够常驻内存,实现 \(O(length)\) 的搜索效率。

Doc Values (正排索引)

  • 为何设计:倒排索引擅长找文档,但做 SORT BY priceGROUP BY category 时需要遍历所有倒排表,极慢且费内存(Fielddata 容易 OOM)。
  • 原理:在构建倒排索引的同时,额外生成一份列式存储的正排索引。
  • 设计价值:将聚合运算转为顺序磁盘 I/O,极大地降低了内存压力,是 ES 能够胜任分析场景的核心。

3. 分布式层:Master、Data 与 Shard

核心问题:如何处理单机存不下的数据?如何保证一台机器挂了系统还能跑?

Shard (分片)

  • 为何设计:单机纵向扩展(加 CPU/内存)有极限,且大索引查询慢。
  • 原理:将一个索引物理上切分成多个分片,分布到不同机器。
  • 设计价值:实现了水平扩展(Scale-out),让查询压力分散到集群所有节点。

Replica (副本)

  • 为何设计:硬件总会损坏。
  • 原理:每个分片都有一个或多个拷贝,且主副本(Primary)与从副本(Replica)永远不在同一台机器。
  • 设计价值:提供了高可用性。当主分片挂掉,副本会自动升为主分片;同时副本还能分担读请求压力。

Master Node (主节点)

  • 为何设计:集群需要一个“大脑”来协调。
  • 原理:负责管理集群元数据(创建索引、分配分片、节点加入/退出)。
  • 设计价值:避免每个节点都去维护全局状态,保证了集群配置的一致性。

4. 协作层:Ingest Pipeline 与 Coordinate Node

核心问题:如何简化开发逻辑,并平衡集群负载?

Ingest Node (预处理节点)

  • 为何设计:原始数据往往很乱(如日志),在写入前需要清洗。
  • 原理:在数据真正索引前,通过一系列 Processor(正则、Grok、日期转换)处理数据。
  • 设计价值:把数据清洗的压力从业务逻辑层转移到 ES 内部,减少了架构复杂度。

Coordinate Node (协调节点)

  • 为何设计:当一个查询涉及多个分片时,谁来合并结果?
  • 原理:任何节点都可以充当协调节点。它负责分发请求到各个 Shard,并在收回结果后进行排序、分页合并(Reduce)。
  • 设计价值:隐藏了分布式复杂性。对于客户端来说,访问 ES 集群就像访问单机数据库一样简单。

总结:为什么要这么设计?

如果用一句话概括 ES 的设计哲学:通过不可变的物理结构(Segment)换取查询高效率,通过分片机制(Shard)解决规模问题,通过多种索引形式(Inverted Index + Doc Values)兼顾搜索与分析。

每一个组件的引入,都是在处理海量数据时,对时间(响应速度)空间(存储成本)可靠性三者之间做的最优权衡。

理解 ES 组件如何工作


1. 写入旅程:数据如何变成“近实时”搜索

当你发送一个 POST /index/_doc 请求时,ES 内部组件像流水线一样配合:

  1. 协调节点 (Coordinating Node) 接单:请求随机打到集群某个节点。该节点根据 Routing(通常是文档 ID)计算出数据该去哪个 Primary Shard(主分片)
  2. 主分片节点执行“双写”:数据到达目标节点后,同时进入两个地方:
  • Memory Buffer:内存缓冲区(此时还搜不到)。
  • Translog:事务日志(顺序写磁盘,用于宕机恢复,保证数据持久性)。
  1. Refresh(关键点):默认每秒一次,ES 把 Buffer 里的数据“刷”进操作系统的 Cache,生成一个 Segment
  • 结果:一旦进入 Cache 变成 Segment,数据就可以被搜索到了。
  1. 副本同步:主分片写完后,并行发给所有 Replica Shards。等副本也写完 Translog,协调节点才向你返回“成功”。
  2. Flush(落地):每 30 分钟或 Translog 满了,才执行一次真正的磁盘 fsync,把内存里的 Segment 彻底固化到硬件。

2. 检索旅程:如何从海量数据中“捞鱼”

当你执行 GET /index/_search 时,组件的工作逻辑是 Scatter-Gather(分发-聚合)

  1. 分发 (Scatter):协调节点收到请求,由于它知道所有分片的位置,它会将请求转发给该索引的每一个分片(主或副本选其一)。
  2. 并行检索:每个数据节点(Data Node)在本地的各个 Segment 中利用 倒排索引 查找。
  • Query 阶段:每个分片只返回极少的信息(文档 ID 和分数 Score)给协调节点。
  1. 聚合 (Gather):协调节点收到所有分片的回复,在内存中进行全局排序(比如只要前 10 条)。
  2. 取回 (Fetch):协调节点拿着排好序的最后 10 个 ID,再次请求对应分片,拉取完整的 JSON 文档内容。
  3. 返回:把最终结果吐给用户。

3. 集群“大脑”:Master Node 的日常

主节点不处理数据读写(这样才不会被业务压力压垮),它只干三件事:

  • 状态维护:谁加入集群了?谁宕机了?
  • 路由决策:如果某个节点挂了,它负责把上面的分片重新分配到其他健康的节点。
  • 元数据同步:新建了索引、修改了 Mapping,它负责通知集群里所有节点更新自己的“配置单”。

4. 2026 年的新成员:Workflows 与 AI Agents

在 2026 年的实践中,ES 引入了 Elastic Workflows 组件,它改变了组件的工作流:

  • 自动化闭环:以前你需要外部程序(如 Python 脚本)定时查询 ES 并执行逻辑;现在 ES 内部集成了 Workflow Engine
  • Agent 联动:当 Workflow 发现数据异常(比如日志报错),它会直接调用内部的 AI Agent 进行推理分析,并自动执行修复步骤(如扩容、告警)。

总结:组件协作表

组件 核心职责 为什么设计它?
Master Node 管理元数据、维护集群一致性。 避免“无头苍蝇”,保证分布式协调。
Data Node 存储 Segment、执行搜索和聚合。 实现存储与计算的水平扩展。
Coordinating Node 请求分发、结果合并(Reduce)。 隐藏分布式复杂性,统一接口。
Ingest Node 写入前的预处理(清洗、转换)。 减轻业务端负担,实现 ETL 离心化。
Translog 记录操作日志。 弥补内存写入的不稳定性,防丢失。

如果你现在手头有一个查询很慢,你觉得问题更可能出在分发阶段的节点负载不均,还是数据节点扫描 Segment 太多了?

ES 的搜索之所以快,本质上是把“全文搜索”变成了“查字典”。


1. 倒排索引的构成:词典 + 倒排表

倒排索引(Inverted Index)主要由两个核心部分组成:

词典 (Term Dictionary)

记录了所有文档中出现过的、分词后的唯一词项(Term)。

  • 功能:快速找到某个词在哪。
  • 存储:通常由于词项巨大,无法全部放入内存。

倒排表 (Posting List)

记录了每个词项出现在哪些文档中。

  • 数据内容
  • 文档 ID(DocID)。
  • 词频(TF):用于相关性评分。
  • 位置(Position):用于短语搜索(Match Phrase)。
  • 偏移量(Offset):用于高亮显示。

2. 核心设计:如何处理海量词项?(FST 的引入)

词典太大,磁盘检索太慢,全放内存又放不下,怎么办?ES 引入了 Term Index (词典索引),并使用 FST (Finite State Transducer,有限状态转移机) 算法进行压缩。

什么是 FST?

FST 是一种类似于前缀树(Trie Tree)的抽象图结构,但它不仅能复用前缀,还能复用后缀

  • 设计原理
  1. 高度压缩:利用词项的公共前缀和后缀,极大地减少了内存占用。
  2. 常驻内存:FST 在 ES 启动时就被加载到内存(或者通过 MMap 映射),搜索时先在 FST 中匹配。
  3. 确定性:给定一个词,FST 能在 \(O(length)\) 时间复杂度内告诉你这个词在词典文件中的位置偏移量。

FST 的作用是将海量的词典索引压缩到极致,使其能常驻内存,从而将磁盘寻道次数减少到近乎 0。


3. 倒排表的压缩:For & Roaring Bitmaps

解决了“找词”的问题,接下来要解决“存 DocID”的问题。倒排表可能包含数亿个 DocID,直接存储非常占空间。

FOR (Frame Of Reference)

  • 原理:利用增量编码 (Delta Encoding)。不存 [100, 101, 102],而是存 [100, 1, 1]。因为差值很小,可以使用极少的 bit 位来存储,极大压缩了空间。

Roaring Bitmaps (咆哮位图)

  • 原理:当 DocID 非常稀疏时,FOR 的效果不好,此时会切换为 Roaring Bitmaps。它将 32 位无符号整数分为高 16 位和低 16 位,根据数据的密集程度选择不同的存储容器(Array 或 Bitmap)。
  • 优势:在做多个条件的 ANDOR 运算(位运算)时,速度极快。

4. 总结:一个搜索请求的完整路径

  1. 查询词解析:用户搜“Java”,先去内存里的 FST 中匹配。
  2. 定位词典:FST 告诉引擎,“Java”这个词在磁盘 Term Dictionary 的具体 Offset。
  3. 获取倒排表:从磁盘读取该词对应的 Posting List(通过压缩算法快速解压)。
  4. 求交/并集:如果有多个搜索条件,利用 Roaring Bitmaps 在内存中快速做位运算。
  5. 取回结果:根据得到的 DocID,去 Stored FieldsDoc Values 中拉取原始数据。

“既然倒排索引这么快,为什么还要有 Doc Values?”

倒排索引擅长的是“词 -> 文档”的搜索,但不擅长“文档 -> 词”的排序、聚合和分面查询。为了解决这个问题,ES 另外设计了 Doc Values(正排索引),采用列式存储,专为聚合和排序优化。


FST 的数学特性或者位图运算的细节

从技术实现的底层视角来看,FST 和 Roaring Bitmaps 的设计极其精妙,它们分别代表了空间换时间自适应数据结构的巅峰。


1. FST (Finite State Transducer) 的数学与逻辑细节

FST 经常被拿来和 Trie(前缀树)比较,但 FST 在数学本质上是一个 有向无环图 (DAG),且具备 输出值 (Output) 的特性。

数学特性:状态压缩与幂等性

FST 能够将所有的 Key 映射为一个 Value(在 ES 中,这个 Value 是词典块在文件中的 Offset)。

  • 后缀合并(Suffix Sharing)
    相比 Trie 只共享前缀,FST 会从后往前扫描,将具有相同后缀的状态节点合并。

示例:单词 "mon""tues" 后面都接 "day"。在 Trie 中,day 会被重复存储两次;在 FST 中,指向 d 的边会汇聚到同一个节点。

  • 有序性与前缀累加
    FST 内部是按照字节序排列的。当你沿着路径查找时,路径上的 Output 值是累加的

  • 查找 apple

  • a 状态输出 10

  • p 状态输出 5

  • 最终 Offset = \(10 + 5 + ... = 100\)

  • 这种设计允许极其高效的范围查询(Range Scan),因为路径是有序且确定的。

算法复杂度

  • 空间:接近于最小生成树的极限压缩,通常比 Trie 小 5-10 倍。
  • 时间\(O(L)\)\(L\) 是 Key 的长度。与词典中总词数 \(N\) 无关。

2. Roaring Bitmaps (咆哮位图) 的运算细节

在处理多个查询条件的交集(AND)时,ES 需要对比两个巨大的 DocID 列表。传统的线性扫描 \(O(N+M)\) 太慢,而 Roaring Bitmaps 将其优化到了极致。

分桶存储 (The Container Strategy)

Roaring Bitmaps 将一个 32 位的 DocID 分为两部分:

  1. 高 16 位 (Most Significant Bits):作为索引,定位到一个 Container(最多 \(2^{16} = 65536\) 个桶)。
  2. 低 16 位 (Least Significant Bits):存储在具体的 Container 中。

自适应的三种容器(核心精髓)

根据桶内数据的稀疏程度,Roaring Bitmaps 会动态转换底层实现:

  1. Array Container (稀疏型)
  • 条件:桶内 DocID 数量 \(< 4096\) 个。
  • 实现:直接用 short[] 数组存低 16 位。
  • 内存:每个 ID 占 2 字节,最大 \(4096 \times 2B = 8KB\)
  1. Bitmap Container (密集型)
  • 条件:桶内 DocID 数量 \(\ge 4096\) 个。
  • 实现:用一个 \(65536\) 位的位图(Bitset)表示。
  • 内存:固定占 \(65536 / 8 = 8KB\)
  • 数学逻辑:一旦数据超过 4096 个,位图占用的空间(固定 8KB)就会比数组更小。
  1. Run Container (连续型)
  • 实现:存储范围,如 [100, 1000] 表示从 100 到 1000 的连续 ID。

位运算的细节优化

当 ES 执行 Filter A AND Filter B 时:

  • Bitmap vs Bitmap:直接进行 CPU 向量化指令 (SIMD) 操作,通过 AND 指令一次处理 64 个文档。
  • Bitmap vs Array:遍历 Array,在 Bitmap 中检查对应位是否为 1(\(O(1)\) 操作)。
  • Array vs Array:采用类似归并排序的双指针扫描。

3. 为什么这些设计对开发很重要?

从底层看这两者,你会明白 ES 的性能瓶颈通常在哪里:

  1. 词项爆炸 (Term Explosion):如果你生成了大量唯一的随机字符串(如 UUID)作为搜索词,FST 的后缀合并会失效,导致 FST 占用内存剧增,甚至撑爆 JVM 堆。
  2. Segment 合并:每次写入新数据都会生成新的倒排索引(包含新的 FST 和 Roaring Bitmaps)。Segment 太多,搜索就要遍历多个 FST。Force Merge 的本质就是重新计算 FST 和合并位图。
  3. 缓存友好:位图运算对 CPU 缓存(L1/L2 Cache)非常友好。这意味着 ES 在处理 过滤 (Filter) 查询时,其效率接近硬件极限。

总结一个底层规律:

  • FST 解决了“在海量词汇中定位”的问题。
  • Roaring Bitmaps 解决了“在海量结果中筛选”的问题。

这种将数学上的图论(FST)与位运算优化(Bitmaps)结合的架构,正是 ES 处理 PB 级数据依然能秒回的技术基石。


理解倒排索引最好的方式是:看数据是如何一步步从“非结构化文本”变成“高度压缩且可计算的二进制结构”的。

假设我们有一个电商系统,索引里有三条简单的商品描述数据:

  • Doc 1: "Apple iPhone 15"
  • Doc 2: "Apple iPad Pro"
  • Doc 3: "Samsung Galaxy S24"

1. 词典 (Term Dictionary) 的构建:从文本到 ID

当我们把这三个文档写进 ES 时,分析器(Analyzer)会把它们打碎。

分词与映射

ES 会建立一张表,把分出来的每个词(Term)映射到对应的文档 ID 上:

Term Posting List (Doc IDs)
apple [1, 2]
galaxy [3]
ipad [2]
iphone [1]
pro [2]
s24 [3]
samsung [3]

开发者视角:你会发现词典是有序的。这非常重要,因为有序意味着我们可以用二分查找。但当词项达到亿级时,内存根本塞不下这张表。


2. FST (Finite State Transducer):极致的内存压缩

为了让搜索不产生频繁的磁盘 IO,ES 必须把“词典索引”放进内存。FST 的设计哲学是:利用公共前缀和后缀来共享空间。

实际例子

假设我们有三个词:cat, deep, do

  • Trie (前缀树):只能共享 d 这个前缀。
  • FST (有限状态转移机):不仅共享前缀,如果 catshats 共享后缀 ats,它也能把路径合二为一。

为什么 FST 适合开发?

  1. 确定性查询:它像一个 state machine。你输入 a-p-p-l-e,它沿着路径走,最终输出一个数字
  2. 这个数字是什么? 它不是 DocID,而是这个词在磁盘上 Term Dictionary 文件里的偏移量 (Offset)
  3. 结果:内存里存的是几百 MB 的 FST,它能帮你定位到磁盘上几十 GB 的词典文件。

3. 倒排表 (Posting List) 的压缩:二进制的魔术

现在我们通过 FST 找到了 apple 在磁盘上的位置,读出了它的倒排表 [1, 2, 5, 8, 15...]。如果这个表有 100 万个 ID,直接存整数需要 4MB。ES 怎么省钱?

FOR (Frame Of Reference)

ES 并不直接存 15, 16, 17,它存 差值 (Delta)

  • 原数据:100, 102, 103, 108
  • 计算差值:100, 2, 1, 5
  • 原理:原本需要 32 位存一个大数,现在最大的差值是 5,只需要 3 位(\(2^3=8\))就能存下。ES 把这些差值打包成块,极大压缩了磁盘占用和读取时的 IO 带宽。

4. 搜索时的“位运算”:如何处理复杂查询?

作为开发,你肯定写过这样的查询:WHERE brand = 'apple' AND category = 'tablet'

在 ES 内部,这变成了两个倒排表的合并:

  • apple 的表:[1, 5, 10, 12]
  • tablet 的表:[5, 8, 10]

Roaring Bitmaps (咆哮位图)

为了快速求交集,ES 使用位图。如果用普通位图,存储 1 和 1 亿这两个数,中间会有大量的 0 浪费空间。
Roaring Bitmaps 的策略

  1. 把 32 位的 DocID 拆成高 16 位和低 16 位。
  2. 高 16 位决定它在哪个“桶”里。
  3. 低 16 位如果数据少(少于 4096 个),就存 数组 (Array);如果数据多,就存 位图 (Bitmap)

开发者视角:这是一种自适应数据结构。在求交集时,两个位图做 AND 运算是 CPU 级别的指令,速度比遍历数组快几个数量级。


总结:从代码逻辑看搜索

当你执行一次查询时,底层发生的逻辑如下:

  1. 内存层 (FST):快速通过前缀/后缀匹配,找到词项在磁盘上的地址。
  2. IO 层 (Term Dictionary):去磁盘读出一小段数据,拿到压缩后的倒排表。
  3. 解压层 (FOR/Roaring):在内存中快速解压,把增量变回 DocID。
  4. 运算层 (Bitsets):如果有多个条件,在内存里进行高效的位运算。

这种“内存索引定位 + 磁盘块读取 + 高效位运算”的设计,就是 ES 能够在毫秒级处理海量数据的核心逻辑。


shard和segment的区别

简单来说,分片(Shard)是逻辑上的“容器”和分布式单位,而 Segment 是物理上的“数据块”和检索单位。

我们可以用“图书馆”的类比来彻底理清它们的关系:


1. 分片(Shard)是如何分片的?

分片是 ES 实现横向扩展的基石。当你向一个索引(Index)写入文档时,ES 需要决定这个文档去哪个分片。

  • 路由算法:ES 使用一个非常简单的公式:

\[shard\_num = hash(routing\_id) \ \% \ num\_of\_primary\_shards \]

  • routing_id 默认是文档的 _id

  • 因为使用了取模运算,所以主分片数量一旦确定就不能修改(否则 hash 结果就找不对地方了)。

  • 物理本质:每一个分片本质上就是一个完整的 Lucene 索引实例。它拥有自己的内存 Buffer、磁盘文件和计算资源。


2. Segment 与分片的关系

如果说分片是一个文件夹,那么 Segment 就是文件夹里一个个独立的文件包。

  • 一对多关系:一个分片由多个 Segment 组成。
  • Segment 的生成
  1. 数据先进入分片的 Memory Buffer
  2. 执行 Refresh(默认 1 秒一次)时,Buffer 里的数据被写成一个新的 Segment 放到 OS Cache 中。
  3. 此时,这个 Segment 就可以被搜索了。
  • 搜索的颗粒度:当一个查询请求到达分片时,分片会并行调用它内部的所有 Segment 进行搜索,然后把结果合并。

3. 核心对比:分片 vs Segment

维度 分片 (Shard) Segment
性质 分布式的基本单位(逻辑+物理容器) 数据的物理存储单位
可见性 开发者在 API 层直接操作和配置 隐藏在底层,由 ES 自动管理
可变性 可以增减副本,但主分片数不可变 完全不可变(一旦生成即只读)
数量影响 分片过多会导致主节点压力大、查询碎片化 Segment 越多,单个分片内的查询越慢

4. 为什么要这样设计?(底层逻辑剖析)

为什么不直接把分片弄成一个大文件,非要拆成 Segment?

  • 为了写入性能:如果是一个大文件,每次写入都要加锁或重写,性能极差。拆成 Segment 后,写入就是“追加新文件”,是顺序 IO,极快。
  • 为了搜索近实时:1 秒生成一个小 Segment 比 1 秒更新一个大索引要容易得多。

既然 Segment 好,为什么还要自动合并(Merge)?

  • 搜索成本:每个 Segment 都有自己的倒排索引。如果一个分片里有 1000 个小 Segment,搜索时就要打开 1000 个文件,CPU 和磁盘压力巨大。
  • 空间回收:ES 的删除是逻辑删除(在 .del 文件标记)。只有通过 Segment Merge 把多个小 Segment 合并成大 Segment 时,那些被标记删除的数据才会被真正物理剔除。

技术开发避坑指南:

  1. 控制分片大小:建议单个分片在 20GB - 50GB。太小了浪费元数据管理资源,太大了在节点间迁移(Relocate)会非常慢。
  2. 减少 Segment 碎片:如果你在进行批量导入,可以临时把 index.refresh_interval 设为 -1。这样不会产生大量碎片小 Segment,等导入完成后再手动触发一次 _forcemerge,搜索效率会提升数倍。

一句话总结:
分片决定了你的数据能分布到多少台机器上(解决的问题);Segment 决定了数据在机器磁盘上怎么组织(解决的问题)。

es的doc values

在理解了 Query(倒排索引)和 Filter(位图)之后,Doc Values 是 Elasticsearch 架构中另一个至关重要的基石。

如果说倒排索引是搜索的“字典”,那么 Doc Values 就是为了排序(Sort)、聚合(Aggregation)脚本执行(Scripting)而设计的“表格”。


1. 为什么需要 Doc Values?

倒排索引的结构是 Term -> Document ID。这种结构在搜索时极快,但在进行聚合(如:求平均价格)时却非常低效。

  • 倒排索引的痛点:如果你要计算所有文档的平均价格,ES 必须扫描整个倒排索引,找到每个文档对应的价格。这相当于为了看每个人的身高,你要翻遍整本字典去查“170cm”、“171cm”这些词条背后都有谁。
  • Doc Values 的解法:它将数据结构反转为 Document ID -> Value。这是一种正向列式存储结构。

2. 设计原理

核心结构:列式存储

Doc Values 在磁盘上按列存储数据。这意味着同一列的所有值都物理连续地存储在一起。

  • 压缩高效:由于同一列的数据类型相同(比如全是数字),ES 可以使用极其高效的压缩算法(如 Delta Encoding 差值压缩)。
  • OS Page Cache 友好:由于数据物理连续,读取时可以利用操作系统的文件缓存,极大减少了磁盘 I/O 开销。

存储位置:磁盘而非 JVM 堆

与早期的 FieldData 不同,Doc Values 是在索引时(Index Time)生成的,并存储在磁盘上。

  • 避免 OOM:因为它不占用 JVM Heap,所以不会导致内存溢出。
  • 内存映射:ES 依靠操作系统的文件系统缓存(File System Cache)来加速 Doc Values 的访问。只要机器有足够的剩余内存,Doc Values 就会被操作系统留在内存中,性能接近内存操作。

3. Doc Values vs. 倒排索引

这两者在 ES 中通常是共存的(对于非 text 类型字段):

特性 倒排索引 (Inverted Index) Doc Values
结构 Term -> Doc List Doc ID -> Value
主要用途 搜索、过滤(Where 子句) 排序、聚合、脚本(Group by/Sort)
存储介质 磁盘 磁盘(列式存储)
字段类型 所有支持搜索的类型 keyword, numerical, date, boolean 等(text 除外)

4. 为什么 text 字段没有 Doc Values?

这是一个常见的问题。text 字段默认不支持 Doc Values。

  • 原因text 字段会被分词(Tokenized)。如果一个字段包含一段 500 字的描述,将其作为列式存储会消耗巨大的磁盘空间,且对长文本进行排序或聚合通常没有业务意义。
  • 解决方案:如果确实需要对 text 字段进行聚合或排序,通常会使用 fields 属性为其定义一个 keyword 子字段。
"title": {
  "type": "text",
  "fields": {
    "raw": { 
      "type": "keyword" // 这个子字段会生成 Doc Values
    }
  }
}


5. 性能优化的关键

由于 Doc Values 依赖操作系统的文件缓存,因此在部署 ES 节点时,通常建议 JVM 堆内存不要超过机器物理内存的一半。留下的另一半内存正是为了给 Doc Values 提供缓存空间,从而保证排序和聚合的极速响应。

总结一句话: 倒排索引负责找文档,Doc Values 负责在找到文档后提取字段值进行计算。


在 Elasticsearch 中,你不需要在查询时显式地写“我要用 Doc Values”,因为 ES 的引擎非常聪明:只要你执行排序(Sort)、聚合(Aggregation)或在脚本中访问字段,它就会在后台自动调用 Doc Values。

以下通过几个具体的业务场景来展示 Doc Values 是如何被触发和使用的。


1. 聚合分析(Aggregation)

这是 Doc Values 最主要的使用场景。当你需要计算某个指标时,ES 会通过 Doc Values 快速提取所有匹配文档的数值。

GET /sales_orders/_search
{
  "size": 0, 
  "aggs": {
    "avg_price": {
      "avg": {
        "field": "price"  // 自动从 Doc Values 中提取 price 列进行累加和计算
      }
    },
    "category_count": {
      "terms": {
        "field": "category.keyword" // 从 Doc Values 中提取分类名称进行分组计数
      }
    }
  }
}


2. 排序(Sorting)

当你请求结果按某个字段排序时,ES 不会去查倒排索引,而是直接扫描 Doc Values 列。

GET /sales_orders/_search
{
  "query": {
    "match": { "customer_name": "张三" }
  },
  "sort": [
    {
      "order_date": { "order": "desc" } // 触发 order_date 字段的 Doc Values 顺序读取
    }
  ]
}


3. 脚本计算(Scripting)

在脚本中,使用 doc['field_name'].value 这种语法就是在直接访问 Doc Values。这比使用 _source 访问字段快得多,因为 _source 需要解压和解析整个 JSON 文档。

GET /sales_orders/_search
{
  "script_fields": {
    "total_with_tax": {
      "script": {
        "source": "doc['price'].value * 1.13" // 直接从 Doc Values 列读取数值,效率极高
      }
    }
  }
}


4. 字段折叠(Field Collapsing)

如果你想在搜索结果中对某个字段进行去重(类似 SQL 的 DISTINCTGROUP BY),也会用到 Doc Values。

GET /sales_orders/_search
{
  "query": {
    "match": { "item_name": "手机" }
  },
  "collapse": {
    "field": "brand_id" // 基于 brand_id 的 Doc Values 进行去重
  }
}


5. 设计时的开关(Mapping)

虽然 keywordnumericdate 等类型默认开启 Doc Values,但如果你确定某个字段永远不会用于排序、聚合或脚本(仅用于过滤和搜索),你可以关闭它以节省磁盘空间。

PUT /my_index
{
  "mappings": {
    "properties": {
      "session_id": {
        "type": "keyword",
        "doc_values": false  // 关闭后,该字段无法进行聚合和排序,节省空间
      }
    }
  }
}


💡 核心总结

  • 读取方式:它是按列读取的,非常适合做“纵向”计算(如:把 100 万行数据的价格加起来)。
  • 内存位置:它存在磁盘上,由 OS Cache 缓存。如果你发现聚合查询变慢了,通常是因为系统的 File System Cache 不够大,导致了频繁的磁盘 IO。