《Elasticsearch:The Definitive Guide》读书分享
Published in:2025-08-15 | category: 读书笔记

分享人:李孟瑶 日期:2025.8.20

本文是对《Elasticsearch: The Definitive Guide》一书核心知识点的梳理与总结,并结合 Elasticsearch 新版本的变化做了补充说明。

写在前面:关于这本书

《Elasticsearch: The Definitive Guide》是由 Elasticsearch 的核心创始人 Zachary TongClinton Gormley 撰写的经典著作,最初由 O’Reilly Media 出版,被誉为学习 Elasticsearch 的”圣经”。

本书主要针对 Elasticsearch 2.x 版本。由于 Elasticsearch 在后续版本(尤其是 5.0、6.0、7.0、8.0)中发生了巨大变化——例如移除了 type、重写了聚合语法、增加了安全性功能等——书中很多具体 API 和细节已经过时。但其核心思想、基本原理和架构概念(倒排索引、分片、副本、查询与过滤的区别、分析过程、聚合逻辑等)至今依然完全适用,仍是理解 Elasticsearch”为什么这么设计”的最佳读物之一。

本书四大特点:

特点说明
权威性作者是 Elasticsearch 早期核心贡献者,内容直接来源于对源码的理解和项目发展的亲身经历
全面性几乎涵盖 Elasticsearch 所有核心概念和功能,从安装、API 使用到聚合分析、集群管理、性能调优
系统性由浅入深:基本概念 → 搜索/映射/分析 → 分布式原理与运维管理
实践性大量可操作的代码示例,是一本”操作手册”式教程而非空洞理论书

书籍封面

目录

  1. 基本知识
  2. 集群
  3. 文档的简单操作
  4. 分布式增删改查

一、基本知识

1.1 Elasticsearch 是什么

  • 面向文档(document oriented):可以存储整个对象或文档,并且会索引(index)每个文档的内容使之可被搜索。
  • 提供丰富灵活的查询语言 Query DSL,支持构建复杂、强大的查询。
  • 本质是一个实时分布式搜索和分析引擎,用于全文搜索、结构化搜索、分析以及三者混合使用。
  • 基于 Apache Lucene 构建,Lucene 是目前最先进、性能最好、功能最全的搜索引擎库之一,但它只是一个 Java 库,需要深入理解检索原理才能使用。Elasticsearch 用 Java 开发并以 Lucene 为核心,通过简单的 RESTful API 隐藏了 Lucene 的复杂性,让全文搜索变得简单易用。

小贴士:可以把 Elasticsearch 理解为”Lucene 的分布式封装层”——Lucene 负责单机的索引与检索算法,ES 负责分片、副本、集群协调、RESTful 接口等工程化能力。

Elasticsearch架构示意图

1.2 集群和节点

  • 节点(node):一个 Elasticsearch 实例。
  • 集群(cluster):由一个或多个具有相同 cluster.name 的节点组成,协同工作、共享数据和负载。

ES 节点角色分类:

角色职责
Master集群启动时选举产生;负责分片分配、维护集群状态与元数据、创建/删除索引;不负责数据读写处理
Data Node实际存储数据、执行 CRUD 与聚合计算,对机器配置(CPU/内存/磁盘)要求较高
Client Node(协调节点)作为请求的负载均衡器,客户端应只与协调节点通信,不直接连接数据节点或主节点

只有候选主节点才具有选举权和被选举权。配置方式:修改 config/elasticsearch.yml 中的 cluster.namenode.roles

新版本提示:7.x 之后 node.master / node.data 等布尔配置已被统一为 node.roles: [master, data, ingest, ...] 数组形式,配置时需注意版本差异。

集群节点关系图

1.3 分片和副本

Shards 分片:索引(index)是指向一个或多个分片(shard)的”逻辑命名空间”,数据分散存储在多个分片上。

  • 分片是最小级别的”工作单元”,只保存索引数据的一部分。
  • 分片数量在创建索引时确定后不能修改(因为路由计算依赖主分片数量),默认是 5 个主分片(注:ES 7.x 起默认主分片数已改为 1 个)。

Replicas 副本:主分片的拷贝。

  • 防止硬件故障导致数据丢失,同时可分担读请求(搜索)。
  • 主分片和副本分片都能处理查询,但只有主分片能处理写入(修改)
  • 索引创建后主分片数量固定,但副本数量可随时动态调整。

重要规则:同一个主分片和它的副本分片永远不会被分配到同一个节点上,避免单点故障导致数据彻底丢失。

分片与副本结构图

1.4 索引、类型、文档、字段

  • 索引(Index):类似数据库的”表”,组织和存储文档,提供快速数据访问。
  • 类型(Type):⚠️ Elasticsearch 7.x 起已移除,一个索引下只保留唯一类型 _doc(6.x 为过渡期,仅允许单 type;8.x 彻底取消相关 API 参数)。
  • 文档(Document):ES 中最小的数据单元,以 JSON 格式表示。
  • 字段(Field):文档的属性,每个字段有名称和类型。

类比关系:索引 ≈ 数据库,类型 ≈ 表(已废弃),文档 ≈ 行,字段 ≈ 列。

索引层级关系图


二、集群

2.1 空集群

启动单独节点,无索引和数据时,该节点自动成为主节点。

1
GET /_cluster/health
  • status:集群健康状态的综合指标(green / yellow / red)
  • number_of_data_nodes:数据节点数量

空集群示意图

2.2 添加索引的集群

创建索引时设置 3 个主分片、1 个副本分片(即每个主分片各对应一个副本)。

  • 单节点下所有副本分片处于 unassigned 状态——因为同一节点保存相同数据副本没有意义,一旦节点故障所有副本也会丢失。
  • 启动第二个节点后,文档先写入主分片,再并发复制到对应副本节点,确保主/副节点数据均可检索。

添加索引后的集群分布图

2.3 横向扩展

启动第三个节点,集群自动感知并重新分配分片:

  • 原本集中在 Node1、Node2 的分片会迁移部分到 Node3,每节点分片数从 3 降为 2,硬件资源(CPU/RAM/I/O)被更少分片共享,单个分片性能更好。
  • 副本数量可在运行中的集群动态变更,按需扩缩容(例如将副本数从 1 调整为 2)。

集群横向扩展图

2.4 应对故障

假设杀掉主节点进程:

  1. 集群重新选举新主节点(如 Node2)。
  2. 原主节点上的主分片暂时丢失,集群健康状态变为 red(不是所有主分片都可用)。
  3. 新主节点会将其它节点上对应的副本分片提升为主分片,集群恢复到 yellow 状态(数据完整但副本数不足)。该提升过程几乎瞬时完成。

故障转移流程图

集群健康状态速查:green = 主副分片均正常;yellow = 主分片正常但副本未全部分配;red = 部分主分片不可用,数据可能丢失或暂不可读。


三、文档的简单操作

3.1 文档基本概念

文档(document)特指最顶层结构(root object)序列化成的 JSON 数据,以唯一 ID 标识并存储。常见元数据字段:_index_type(已废弃)、_id_version_seq_no_primary_term 等。

3.2 添加文档

1
2
3
4
5
6
7
# 自定义 _id,使用 PUT
PUT /my_index/_doc/1
{ "title": "Elasticsearch 入门" }

# 不指定 _id,自动生成 UUID,使用 POST
POST /my_index/_doc
{ "title": "Elasticsearch 入门" }
  • 自定义 ID 用 PUT(”在这个 URL 中存储文档”);自动生成 ID 用 POST(”在这个集合下存储一个文档”)。
  • 每个文档都有版本号,文档每次变化(含删除)_version 都会递增。

3.3 检索文档

1
2
3
4
GET /my_index/_doc/1?pretty
GET /my_index/_doc/1?_source=title,author
GET /my_index/_doc/1?_source=false
HEAD /my_index/_doc/1 # 仅检查文档是否存在

检索请求json示例

  • pretty 参数美化 JSON 输出(不影响 _source 内容本身)。
  • _source 参数可指定只返回需要的字段,多个字段用逗号分隔。

3.4 更新与删除

文档在 Elasticsearch 中不可变,所谓”更新”本质是标记旧文档删除并重建索引(reindex)写入新文档。旧版本不会立即物理删除,会在后续索引清理时回收空间。删除操作同样会使 _version 递增。

3.5 处理写冲突

ES 是分布式系统,复制请求并行、无序到达各节点,需要机制防止旧版本覆盖新版本:

方式特点
悲观并发控制关系型数据库常用,先加锁阻塞访问,确保同一时刻只有一个线程可修改
乐观并发控制(ES 采用)不加锁,假设冲突很少发生;若读写期间数据变化则更新失败,由程序决定重试/刷新/反馈用户

内部版本控制(推荐,6.7+ / 7.x 起)

使用 if_seq_no + if_primary_term 组合替代旧的 _version 做并发控制:

1
2
PUT /my_index/_doc/1?if_seq_no=10&if_primary_term=2
{ "title": "更新内容" }
  • 匹配成功 → 更新成功;不匹配 → 返回 409 Conflict
  • (primary_term, seq_no) 在整个集群范围内唯一标识一次操作,即使旧主分片”复活”重新写入,也会因 primary_term 过旧而被拒绝,避免脑裂下的数据冲突。
  • _version 字段仍存在并递增,但仅用于展示和外部版本控制,不再参与内部并发判断。

外部版本控制

1
PUT /my_index/_doc/1?version=5&version_type=external

用于将外部系统(如关系型数据库)的版本状态同步进 Elasticsearch,核心思想是”外部系统是权威,ES 版本要追上外部系统版本”。

对比项内部版本控制外部版本控制
核心思想读取之后若被他人修改过,则放弃本次修改以外部系统版本为权威,ES 版本需追赶同步
适用场景ES 内部一致性保障跨系统数据同步

3.6 局部更新(Update API)

1
2
POST /my_index/_doc/1/_update
{ "doc": { "views": 10 } }
  • doc 参数会与现有文档合并:已有标量字段被覆盖,新字段被追加。
  • 底层依然是”检索旧文档 → 修改 → 重建索引”,本质仍是替换而非真正的局部写入。

脚本局部更新(Painless)

5.4+ 后默认脚本语言由 Groovy 改为 Painless

ES版本与脚本语言对照表

1
2
3
4
5
6
7
8
POST /my_index/_doc/1/_update
{
"script": {
"source": "ctx._source.views += params.count",
"params": { "count": 1 }
},
"upsert": { "views": 1 }
}
  • ctx._source 表示文档当前内容,可直接修改。
  • upsert 用于文档不存在时的初始化创建。
  • 设置 ctx.op = "delete" 可在脚本中根据条件删除文档。

retry_on_conflict

1
POST /my_index/_doc/1/_update?retry_on_conflict=5
  • 默认值为 0:遇到版本冲突立即失败,需自行重试。
  • 设置 N > 0:ES 内部自动重试最多 N 次,降低因短暂并发冲突导致的失败率。

3.7 批量操作

mget:批量获取

1
2
3
4
5
6
7
GET /_mget
{
"docs": [
{ "_index": "my_index", "_id": "1" },
{ "_index": "my_index", "_id": "2", "_source": ["title"] }
]
}

每个文档的检索结果相互独立,某一文档不存在不影响其它文档返回。

mget批量获取json示例

bulk:批量增删改

1
2
3
4
5
6
POST /_bulk
{ "index": { "_index": "my_index", "_id": "1" } }
{ "title": "doc1" }
{ "delete": { "_index": "my_index", "_id": "2" } }
{ "update": { "_index": "my_index", "_id": "3" } }
{ "doc": { "views": 100 } }
  • 响应中的 items 数组按请求顺序逐一返回每个子操作结果。
  • 整体请求是原子接收的:要么整体被接受解析(返回 200,内部子操作各自独立判定成败),要么因严重错误整体被拒绝(如语法错误 400、索引不存在 404)。
  • 每个子操作彼此独立执行,没有事务关系,一个操作失败不会导致其它操作回滚。

四、分布式增删改查

4.1 文档路由

文档存储在哪个分片由路由公式决定:

1
shard_num = hash(routing) % number_of_primary_shards
  • routing 默认值为 _id,也可自定义(如用 user_id),以保证同一用户的所有文档落在同一分片,便于关联查询。
  • 这正是主分片数量在索引创建后不能修改的原因:一旦改变,所有历史路由值全部失效,文档将无法定位。
  • getindexdeletebulkupdatemget 等所有文档级 API 均支持 routing 参数自定义。

文档路由算法示意图

4.2 新建 / 索引 / 删除(写操作)

写操作必须先在主分片成功,再复制到副本分片:

  1. 客户端向协调节点(如 ES1)发送写请求,按路由公式计算出目标主分片 P0。
  2. ES1 将请求转发至 P0 所在节点(如 ES3),ES3 接受并写入磁盘。
  3. ES3 并发将数据复制到所有副本分片 R0(期间通过乐观锁处理并发冲突)。所有副本确认成功后,ES3 向协调节点报告成功,协调节点再回应客户端。

写操作流程图

4.3 检索文档(读操作)

读请求可从主分片或任一副本分片获取数据,协调节点会轮询所有分片副本以均衡负载:

  1. 请求到达协调节点,解析查询语句并定位目标分片,转发给对应数据节点。
  2. 数据节点用 Lucene 执行搜索,生成结果集并返回协调节点。
  3. 若涉及多个分片,协调节点聚合各分片结果,统一排序、分页等处理。
  4. 协调节点封装统一响应格式返回客户端。

检索文档流程图

4.4 局部更新文档

update API 本质是”读+写”的组合:

  1. 客户端向 Node1 发送更新请求。
  2. Node1 转发到主分片所在节点 Node3。
  3. Node3 检索旧文档、修改 _source,在主分片上重建索引;若检测到并发修改,按 retry_on_conflict 设定次数重试,超出仍失败则放弃。
  4. 更新成功后,Node3 将完整的新文档版本(而非更新指令本身)异步转发给副本节点重建索引;所有副本确认后逐级返回客户端。

为什么转发的是整个文档而非更新指令? 因为副本间复制是异步且无序到达的,如果只转发”修改指令”,多个并发修改到达顺序错乱会导致文档损坏;而转发完整的最终文档版本则不存在顺序依赖问题。

局部更新流程图

4.5 mget 批量获取

请求节点了解每个文档所在分片,将批量请求拆分为”按分片归类”的子请求并行转发,待各节点应答后合并为统一响应返回客户端。

4.6 bulk 批量执行

  1. 客户端向 Node1 发送 bulk 请求。
  2. Node1 按分片拆分请求,转发至对应主分片所在节点。
  3. 主分片按序逐条执行操作,每完成一条立即异步转发给副本节点重建索引,再处理下一条;全部完成后逐级汇总响应返回客户端。

bulk执行流程图


总结

虽然本书基于 ES 2.x,但分片路由、乐观并发控制、主从复制流程等分布式设计思想至今未变,是理解新版本 ES(甚至其它分布式存储系统)底层机制的优秀参考资料。结合官方最新文档(_seq_no / _primary_term 并发控制、node.roles 配置等)阅读,可以更好地把”经典原理”和”当下实践”对应起来。

参考资料


感谢阅读,欢迎交流探讨 🙌

Prev:
RocketMQ 重平衡机制与消费者组跨环境冲突问题
Next:
《代码整洁之道》读书笔记