从技术设计的角度来看,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 price或GROUP 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 内部组件像流水线一样配合:
- 协调节点 (Coordinating Node) 接单:请求随机打到集群某个节点。该节点根据
Routing(通常是文档 ID)计算出数据该去哪个 Primary Shard(主分片)。 - 主分片节点执行“双写”:数据到达目标节点后,同时进入两个地方:
- Memory Buffer:内存缓冲区(此时还搜不到)。
- Translog:事务日志(顺序写磁盘,用于宕机恢复,保证数据持久性)。
- Refresh(关键点):默认每秒一次,ES 把 Buffer 里的数据“刷”进操作系统的 Cache,生成一个 Segment。
- 结果:一旦进入 Cache 变成 Segment,数据就可以被搜索到了。
- 副本同步:主分片写完后,并行发给所有 Replica Shards。等副本也写完 Translog,协调节点才向你返回“成功”。
- Flush(落地):每 30 分钟或 Translog 满了,才执行一次真正的磁盘
fsync,把内存里的 Segment 彻底固化到硬件。
2. 检索旅程:如何从海量数据中“捞鱼”
当你执行 GET /index/_search 时,组件的工作逻辑是 Scatter-Gather(分发-聚合):
- 分发 (Scatter):协调节点收到请求,由于它知道所有分片的位置,它会将请求转发给该索引的每一个分片(主或副本选其一)。
- 并行检索:每个数据节点(Data Node)在本地的各个 Segment 中利用 倒排索引 查找。
- Query 阶段:每个分片只返回极少的信息(文档 ID 和分数 Score)给协调节点。
- 聚合 (Gather):协调节点收到所有分片的回复,在内存中进行全局排序(比如只要前 10 条)。
- 取回 (Fetch):协调节点拿着排好序的最后 10 个 ID,再次请求对应分片,拉取完整的 JSON 文档内容。
- 返回:把最终结果吐给用户。
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)的抽象图结构,但它不仅能复用前缀,还能复用后缀。
- 设计原理:
- 高度压缩:利用词项的公共前缀和后缀,极大地减少了内存占用。
- 常驻内存:FST 在 ES 启动时就被加载到内存(或者通过 MMap 映射),搜索时先在 FST 中匹配。
- 确定性:给定一个词,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)。
- 优势:在做多个条件的
AND或OR运算(位运算)时,速度极快。
4. 总结:一个搜索请求的完整路径
- 查询词解析:用户搜“Java”,先去内存里的 FST 中匹配。
- 定位词典:FST 告诉引擎,“Java”这个词在磁盘 Term Dictionary 的具体 Offset。
- 获取倒排表:从磁盘读取该词对应的 Posting List(通过压缩算法快速解压)。
- 求交/并集:如果有多个搜索条件,利用 Roaring Bitmaps 在内存中快速做位运算。
- 取回结果:根据得到的 DocID,去 Stored Fields 或 Doc 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 分为两部分:
- 高 16 位 (Most Significant Bits):作为索引,定位到一个 Container(最多 \(2^{16} = 65536\) 个桶)。
- 低 16 位 (Least Significant Bits):存储在具体的 Container 中。
自适应的三种容器(核心精髓)
根据桶内数据的稀疏程度,Roaring Bitmaps 会动态转换底层实现:
- Array Container (稀疏型):
- 条件:桶内 DocID 数量 \(< 4096\) 个。
- 实现:直接用
short[]数组存低 16 位。 - 内存:每个 ID 占 2 字节,最大 \(4096 \times 2B = 8KB\)。
- Bitmap Container (密集型):
- 条件:桶内 DocID 数量 \(\ge 4096\) 个。
- 实现:用一个 \(65536\) 位的位图(Bitset)表示。
- 内存:固定占 \(65536 / 8 = 8KB\)。
- 数学逻辑:一旦数据超过 4096 个,位图占用的空间(固定 8KB)就会比数组更小。
- 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 的性能瓶颈通常在哪里:
- 词项爆炸 (Term Explosion):如果你生成了大量唯一的随机字符串(如 UUID)作为搜索词,FST 的后缀合并会失效,导致 FST 占用内存剧增,甚至撑爆 JVM 堆。
- Segment 合并:每次写入新数据都会生成新的倒排索引(包含新的 FST 和 Roaring Bitmaps)。Segment 太多,搜索就要遍历多个 FST。Force Merge 的本质就是重新计算 FST 和合并位图。
- 缓存友好:位图运算对 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 (有限状态转移机):不仅共享前缀,如果
cats和hats共享后缀ats,它也能把路径合二为一。
为什么 FST 适合开发?
- 确定性查询:它像一个 state machine。你输入
a-p-p-l-e,它沿着路径走,最终输出一个数字。 - 这个数字是什么? 它不是 DocID,而是这个词在磁盘上 Term Dictionary 文件里的偏移量 (Offset)。
- 结果:内存里存的是几百 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 的策略:
- 把 32 位的 DocID 拆成高 16 位和低 16 位。
- 高 16 位决定它在哪个“桶”里。
- 低 16 位如果数据少(少于 4096 个),就存 数组 (Array);如果数据多,就存 位图 (Bitmap)。
开发者视角:这是一种自适应数据结构。在求交集时,两个位图做 AND 运算是 CPU 级别的指令,速度比遍历数组快几个数量级。
总结:从代码逻辑看搜索
当你执行一次查询时,底层发生的逻辑如下:
- 内存层 (FST):快速通过前缀/后缀匹配,找到词项在磁盘上的地址。
- IO 层 (Term Dictionary):去磁盘读出一小段数据,拿到压缩后的倒排表。
- 解压层 (FOR/Roaring):在内存中快速解压,把增量变回 DocID。
- 运算层 (Bitsets):如果有多个条件,在内存里进行高效的位运算。
这种“内存索引定位 + 磁盘块读取 + 高效位运算”的设计,就是 ES 能够在毫秒级处理海量数据的核心逻辑。
shard和segment的区别
简单来说,分片(Shard)是逻辑上的“容器”和分布式单位,而 Segment 是物理上的“数据块”和检索单位。
我们可以用“图书馆”的类比来彻底理清它们的关系:
1. 分片(Shard)是如何分片的?
分片是 ES 实现横向扩展的基石。当你向一个索引(Index)写入文档时,ES 需要决定这个文档去哪个分片。
- 路由算法:ES 使用一个非常简单的公式:
-
routing_id默认是文档的_id。 -
因为使用了取模运算,所以主分片数量一旦确定就不能修改(否则 hash 结果就找不对地方了)。
-
物理本质:每一个分片本质上就是一个完整的 Lucene 索引实例。它拥有自己的内存 Buffer、磁盘文件和计算资源。
2. Segment 与分片的关系
如果说分片是一个文件夹,那么 Segment 就是文件夹里一个个独立的文件包。
- 一对多关系:一个分片由多个 Segment 组成。
- Segment 的生成:
- 数据先进入分片的 Memory Buffer。
- 执行 Refresh(默认 1 秒一次)时,Buffer 里的数据被写成一个新的 Segment 放到 OS Cache 中。
- 此时,这个 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 时,那些被标记删除的数据才会被真正物理剔除。
技术开发避坑指南:
- 控制分片大小:建议单个分片在 20GB - 50GB。太小了浪费元数据管理资源,太大了在节点间迁移(Relocate)会非常慢。
- 减少 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 的 DISTINCT 或 GROUP BY),也会用到 Doc Values。
GET /sales_orders/_search
{
"query": {
"match": { "item_name": "手机" }
},
"collapse": {
"field": "brand_id" // 基于 brand_id 的 Doc Values 进行去重
}
}
5. 设计时的开关(Mapping)
虽然 keyword、numeric、date 等类型默认开启 Doc Values,但如果你确定某个字段永远不会用于排序、聚合或脚本(仅用于过滤和搜索),你可以关闭它以节省磁盘空间。
PUT /my_index
{
"mappings": {
"properties": {
"session_id": {
"type": "keyword",
"doc_values": false // 关闭后,该字段无法进行聚合和排序,节省空间
}
}
}
}
💡 核心总结
- 读取方式:它是按列读取的,非常适合做“纵向”计算(如:把 100 万行数据的价格加起来)。
- 内存位置:它存在磁盘上,由 OS Cache 缓存。如果你发现聚合查询变慢了,通常是因为系统的 File System Cache 不够大,导致了频繁的磁盘 IO。