《Elasticsearch 权威指南》详解:零基础读懂 ES 的核心知识体系
- 约 19 分钟读完 - 全文 7526 字《Elasticsearch 权威指南》详解:零基础读懂 ES 的核心知识体系
《Elasticsearch 权威指南》(Elasticsearch: The Definitive Guide,Clinton Gormley & Zachary Tong 著)是学习 Elasticsearch 公认的第一本书。中文版由 looly 等人组织社区翻译。它不是 API 手册,而是沿着"为什么这样设计“的路线,把 ES 从安装、文档 CRUD 一路讲到分布式内核、分词、相关性算分和集群运维。
这本书基于 ES 1.x/2.x 时代写成,个别细节(如 type 概念、TF/IDF 默认评分)在新版本有变化,但核心思想至今不变。本文面向从未读过这本书的人,按"基础概念 → 分布式原理 → 搜索 → 语言处理 → 聚合 → 建模 → 运维"的顺序完整梳理,配图解和可直接运行的实例。
一、Elasticsearch 是什么:先建立直觉
书的开篇用一句话定义 ES:
Elasticsearch 是一个分布式的、实时的文档存储与搜索引擎,基于 Apache Lucene 构建,通过 RESTful API 暴露 Lucene 的全部威力。
Lucene 本身是一个 Java 搜索库,强大但难用——ES 的价值在于把 Lucene 包装成一个可以水平扩展、开箱即用的分布式系统。书中列举的典型用户包括 Wikipedia(全文搜索 + 高亮 + 输入即搜 + 拼写纠错)、GitHub(检索 1300 亿行代码)、StackOverflow(“你可能想问"的 more-like-this 推荐)。
与关系型数据库的概念对照(这是全书最重要的入门表格):
| 关系型数据库 | Elasticsearch | 说明 |
|---|---|---|
| Database(库) | Index(索引) | 逻辑命名空间 |
| Table(表) | Type(类型) | ⚠️ 新版已废弃,理解旧书时需要 |
| Row(行) | Document(文档) | 一条 JSON 记录 |
| Column(列) | Field(字段) | 文档的属性 |
| Schema(表结构) | Mapping(映射) | 字段类型定义 |
| SQL | Query DSL | 查询语言 |
| index(索引,动词) | index(索引,动词) | 把文档写入 |
关键区别:数据库假设"每行结构相同”,ES 的文档是无模式的 JSON(虽然实际上有 mapping 约束);数据库为"精确存取"优化,ES 为”相关性检索“优化——它回答的问题不是"有没有”,而是"有多相关"。
1.1 十分钟跑起来:书中的第一个例子
全书第一章用一家虚构公司 megacorp 的员工数据贯穿:
# 写入一个员工文档(PUT = 指定 ID 创建/覆盖)
PUT /megacorp/employee/1
{
"first_name": "John",
"last_name": "Smith",
"age": 25,
"about": "I love to go rock climbing",
"interests": ["sports", "music"]
}
# 取回(GET)
GET /megacorp/employee/1
# 轻量搜索:查询字符串
GET /megacorp/employee/_search?q=last_name:Smith
# 正式搜索:Query DSL(JSON 请求体)
GET /megacorp/employee/_search
{
"query": { "match": { "last_name": "Smith" } }
}注意这里没有建库、没有建表、没有定义字段类型——这就是"无模式"体验。但书里马上提醒:无模式是假象,ES 在背后做了动态映射(dynamic mapping),第一次见 "age": 25 就推断 age 是 long。生产环境应该显式定义 mapping(第四章详解)。
1.2 一个展示"相关性"的例子
GET /megacorp/employee/_search
{
"query": {
"bool": {
"must": { "match": { "about": "rock climbing" } },
"filter": { "range": { "age": { "gt": 30 } } }
}
}
}返回结果中每个文档带一个 _score(相关性得分),按得分降序排列。must 里的 match 参与算分,filter 里的 range 只过滤不算分——这是全书反复出现的 query/filter 二分法,第六章详解。
二、分布式内核:ES 为什么能水平扩展
这是全书的硬核部分,也是 ES 区别于"单机 Lucene"的根本。
2.1 集群、节点与分片
graph TB
subgraph CLUSTER["集群 cluster(一个 cluster.name)"]
N1["节点 Node 1<br/>(一个 ES 进程)"]
N2["节点 Node 2"]
N3["节点 Node 3"]
end
subgraph IDX["索引 blogs(3 主分片 × 1 副本)"]
P0["主分片 P0"] --- R0["副本分片 R0"]
P1["主分片 P1"] --- R1["副本分片 R1"]
P2["主分片 P2"] --- R2["副本分片 R2"]
end
N1 -.承载.- P0
N1 -.承载.- R1
N2 -.承载.- P1
N2 -.承载.- R2
N3 -.承载.- P2
N3 -.承载.- R0
style CLUSTER fill:#e8f0fe
要点:
- 分片(shard)是 ES 分布式能力的最小单元:一个分片就是一个完整的 Lucene 实例。索引只是"一组分片的逻辑命名空间"。
- 主分片数在创建索引时定死(路由哈希依赖它),副本分片数可以随时调整。
- 主副本不允许在同一节点——这样任意一台机器宕机,数据不丢、搜索不中断。
- 集群健康三态:
green(所有主副本都就位)/yellow(主分片齐了,副本缺)/red(有主分片丢失,数据不可用)。
GET /_cluster/health # 全书最常用的运维命令2.2 扩容为什么是"免费"的
graph LR
subgraph 三节点["3 节点"]
A1["P0 R1"]
B1["P1 R2"]
C1["P2 R0"]
end
subgraph 加一节点["加入第 4 节点后:自动再平衡"]
A2["P0"]
B2["P1 R0"]
C2["P2"]
D2["R1 R2"]
end
三节点 -->|"分片自动迁移<br/>无需停机"| 加一节点
分片是 Lucene 实例,搬分片 = 搬一个独立的小搜索引擎。ES 自动把分片挪到新节点,流量自动均摊。垂直扩容(换大机器)之外,ES 天生支持水平扩容(加机器)——这是书里"scale up vs scale out"一节的核心论点。
2.3 文档是怎么找到家的:路由
写入/读取一个文档时,ES 用它定位分片:
shard = hash(routing) % number_of_primary_shards
routing 默认是文档的 _id,也可以自定义(比如按用户 ID 路由,让同一用户的数据落在同一分片)。这解释了主分片数为什么不能改:改了模数,所有已有文档的哈希结果全部失效。
2.4 写入与读取的分布式流程
sequenceDiagram
participant C as 客户端
participant N as 协调节点<br/>(收到请求的任意节点)
participant P as 主分片所在节点
participant R1 as 副本节点1
participant R2 as 副本节点2
Note over C,N: 写入流程
C->>N: PUT /blogs/post/123
N->>N: 计算路由 hash(_id)%3 → P1
N->>P: 转发写入
P->>P: 本地写 Lucene + translog
par 并行同步副本
P->>R1: 复制写入
P->>R2: 复制写入
end
R1-->>P: ack
R2-->>P: ack
P-->>N: 成功
N-->>C: 201 + _version
Note over C,N: 读取流程
C->>N: GET /blogs/post/123
N->>N: 路由计算 → 分片 P1
N->>R2: 主/副本轮询负载均衡
R2-->>N: 文档
N-->>C: 200 + _source
两个关键设计:
- 任何节点都能当协调节点——没有单点入口;
- 写走主分片,读可以走主或副本——副本不仅容灾,还分摊读流量。加副本 = 加读吞吐。
三、近实时的秘密:refresh、translog 与段合并
“文档写入后为什么默认 1 秒才能搜到?“回答这个问题需要理解 Lucene 的存储模型。
3.1 倒排索引与不可变段
ES 快的根源是倒排索引:
graph LR
subgraph 词典["Term Dictionary"]
T1["climbing"]
T2["music"]
T3["rock"]
end
subgraph 倒排表["Posting List"]
L1["doc 1, doc 5"]
L2["doc 1, doc 3"]
L3["doc 1"]
end
T1 --> L1
T2 --> L2
T3 --> L3
Lucene 把倒排索引持久化为段(segment),段一旦写出就不可变(immutable)。不可变性带来一串好处:不需要锁、可以进文件系统缓存、删除只需打标记。代价是:更新 = 删旧 + 建新,新写的文档必须先攒成新段才能被搜索。
3.2 完整链路
flowchart TD
W["文档写入请求"] --> MEM["内存缓冲区"]
W --> TL["translog<br/>(先写 WAL,防宕机丢数据)"]
MEM -->|"refresh<br/>默认每 1 秒"| SEG["新 segment<br/>写入文件系统缓存<br/>→ 立即可被搜索"]
SEG -->|"flush<br/>translog 满或定时"| DISK["fsync 落盘<br/>清空 translog"]
SEG -.->|"后台 merge"| BIG["小段合并成大段<br/>物理清除已删文档"]
三个动作的分工:
- refresh:内存 → 新的可搜索 segment(在文件系统缓存里,不落盘)。这就是"近实时(NRT)“的来源——默认 1 秒延迟。批量导入时可以调大
refresh_interval换吞吐。 - translog:预写日志。每个写入先记 translog,节点宕机后用它重放还没 flush 的数据。这是"ES 不丢数据"的底气。
- flush:真正 fsync 落盘 + 清空 translog。
- merge:小段太多会拖慢搜索(每个查询都要遍历所有段),后台线程持续把小段合并成大段,并顺带物理删除被标记删除的文档。
给初学者的记忆法:refresh 管"搜得到”,flush 管"丢不了”,merge 管"搜得快”。
四、映射与分析:数据进 ES 之前发生了什么
4.1 Mapping:字段的类型决定它能怎么被查
PUT /blogs
{
"mappings": {
"properties": {
"title": { "type": "text", "analyzer": "ik_max_word" },
"status": { "type": "keyword" },
"views": { "type": "integer" },
"created": { "type": "date" }
}
}
}最核心的类型二分:
text:全文字段,会被分词后存入倒排索引,用于match全文检索;keyword:精确值字段,原样入索引,用于过滤、排序、聚合。
书中金句:"exact value 和 full text 的区别,是 SQL 世界和搜索世界的分界"。用 keyword 字段做 match 全文搜索(或反过来)是新手最常见的错误。
⚠️ 版本提示:原书写作时还是 string + analyzed/not_analyzed 的写法,ES 5.x 起拆分为 text/keyword,思想完全一致。
4.2 分析链:分词是怎么发生的
graph LR
A["原始文本<br/>'The Quick Brown Fox!'"] --> CF["Character Filter<br/>字符过滤<br/>(去 HTML、字符映射)"]
CF --> TK["Tokenizer<br/>分词器<br/>'The','Quick','Brown','Fox'"]
TK --> TF["Token Filter<br/>词项过滤器<br/>lowercase → 停用词 → 词干提取"]
TF --> R["最终词项<br/>'quick','brown','fox'"]
- Character Filter:分词前做字符级预处理;
- Tokenizer:把文本切成词项(一个分析器只能有一个 tokenizer);
- Token Filter:对词项再加工,可串联多个(lowercase、停用词、同义词、词干提取)。
内置的 standard 分析器 = standard tokenizer + lowercase + stop。可以用 _analyze API 眼见为实:
POST /_analyze
{ "analyzer": "standard", "text": "The Quick Brown Fox!" }
# → the, quick, brown, fox('The' 被小写化)中文场景:标准分词器会把中文逐字切开,效果很差。社区方案是 IK 分词器(ik_max_word 细粒度建索引、ik_smart 粗粒度搜索),这是中文 ES 项目的标配。
索引时与搜索时的分析必须配套:文档里的 "Fox" 入库时被分析成 fox;搜索 "FOX" 时查询词也会被同一个分析器处理成 fox 去匹配——这就是为什么 ES 搜索"不区分大小写":不是查询忽略了大小写,而是两边都被 lowercased 了。
五、搜索:Query DSL 的全景
5.1 Query 与 Filter:全书最重要的性能概念
graph TD
Q["Query 上下文<br/>'这个文档和查询有多相关?'"]
F["Filter 上下文<br/>'这个文档是否匹配?'"]
Q --> A1["计算 _score<br/>结果按相关性排序"]
F --> A2["只答 yes/no<br/>不算分、结果可被缓存"]
style Q fill:#fef3e8
style F fill:#e1f5e1
经验法则:能用 filter 就不用 query。过滤条件(状态、时间范围、ID 列表)放 filter,既省算分开销又能命中 bitset 缓存。
5.2 常用查询速查
| 查询 | 用途 | 例子 |
|---|---|---|
match |
全文搜索(会分词) | 标题含"登山 装备" |
term |
精确匹配(不分词) | status 等于 “published” |
terms |
多值精确匹配 | 标签 in [“科幻”,“动作”] |
range |
范围 | age ≥ 30 |
exists |
字段非空 | 必须有 poster 字段 |
multi_match |
跨多字段全文 | 同时在标题和简介里搜 |
match_phrase |
短语匹配(词序相邻) | 搜 “rock climbing” 不拆词 |
bool |
组合一切 | 见下 |
5.3 bool 查询:乐高积木
GET /megacorp/employee/_search
{
"query": {
"bool": {
"must": [ { "match": { "about": "rock climbing" } } ],
"should": [ { "match": { "interests": "music" } } ],
"must_not": [ { "term": { "last_name": "smith" } } ],
"filter": [ { "range": { "age": { "gte": 30 } } } ]
}
}
}must:必须匹配,参与算分;should:最好匹配,匹配上加分(没有 must/filter 时至少命中一个);must_not:必须不匹配,不参与算分(在 filter 上下文执行);filter:必须匹配,不参与算分。
所有复杂查询都是这四块积木的嵌套。
5.4 相关性算分:_score 是怎么来的
书里讲了经典的 TF/IDF 模型(新版本默认换成 BM25,思想相同,多了文档长度归一化):
- 词频 TF:词在本文档出现越多,越相关;
- 逆文档频率 IDF:词在所有文档中越罕见,区分度越高(“的"不值钱,“攀岩"值钱);
- 字段长度:字段越短,匹配越"精悍”,得分越高。
对结果有疑问时用 _explain API 看算分过程——这是调搜索体验的第一件工具。
5.5 深分页陷阱
GET /_search?from=10000&size=10 # ⚠️ 危险分布式搜索是 scatter-gather 模式:
sequenceDiagram
participant C as 客户端
participant CO as 协调节点
participant S0 as 分片0
participant S1 as 分片1
participant S2 as 分片2
C->>CO: from=10000, size=10
par scatter
CO->>S0: 各分片先取 Top 10010
CO->>S1: 各分片先取 Top 10010
CO->>S2: 各分片先取 Top 10010
end
S0-->>CO: 10010 条
S1-->>CO: 10010 条
S2-->>CO: 10010 条
CO->>CO: 归并 30030 条,取 10001~10010
CO-->>C: 10 条
取第 1001 页,要在每个分片各取 10010 条再归并。深度翻页成本线性爆炸,书里的忠告是"不要在搜索场景做深翻页”;确实需要遍历全量时,用 scroll(游标快照)或新版本的 search_after。
六、处理人类语言
这一部分是本书区别于普通教程的精华:搜索的本质对手是自然语言的模糊性。
- 语言分析器:英语词干提取(
foxes→fox)、德语的复合词拆分、中文/日文的无空格分词。每种语言的正确处理方式完全不同——选对语言分析器比调任何参数都重要。 - 拼写纠错(did-you-mean):
term suggester/phrase suggester,“你搜的是不是 elasticsearch?” - 输入即搜(search-as-you-type):用
edge ngram(前缀 ngram)在索引时把"elasticsearch"切成e, el, ela, ...,用户每敲一个字母就能命中。 - 近似匹配:
match的fuzziness参数基于编辑距离,容忍拼写错误(searching打成serching也能命中)。 - 近似短语:
match_phrase的slop参数允许词与词之间隔着几个词仍算匹配。
这一部分的共同心法:召回率与准确率的权衡。ngram 开得越大、fuzziness 越高,召回越好但噪音越多;工程上常用"多层子字段"(精确字段 + 分词字段 + ngram 字段)分层兜底。
七、聚合:从搜索到分析
搜索回答"哪些文档匹配",聚合(aggregations) 回答"数据整体长什么样"。聚合分两大类,可以无限嵌套:
- 桶(Bucket)聚合:把文档分组——
terms(按字段值)、date_histogram(按时间)、range(按区间); - 度量(Metric)聚合:对组内文档算统计——
avg/sum/min/max/stats/cardinality。
7.1 实例:员工数据统计
GET /megacorp/employee/_search
{
"size": 0,
"aggs": {
"by_interest": { # 桶:按兴趣分组
"terms": { "field": "interests" },
"aggs": {
"avg_age": { # 嵌套度量:每组平均年龄
"avg": { "field": "age" }
}
}
}
}
}graph TD
A["全部员工文档"] -->|"terms 桶 by_interest"| B1["sports 组"]
A --> B2["music 组"]
A --> B3["forestry 组"]
B1 --> C1["avg_age = 28.5"]
B2 --> C2["avg_age = 31.2"]
B3 --> C3["avg_age = 35.0"]
7.2 要点
size: 0:只要聚合不要文档,省掉无用传输;- 聚合只能跑在
keyword/数值/日期等精确值字段上——在text字段上聚合会失败或慢到崩溃(需要 fielddata,默认关闭,这是个性能地雷); - 高基数
terms聚合(如按 userId 分桶)吃内存,新版有 circuit breaker 兜底; - 近似聚合是 ES 的特色:
cardinality(去重计数)和percentiles(百分位)都是近似算法,用微小误差换海量数据下的可用性。
八、关系建模:ES 不是数据库
书里最"劝退"也最重要的一章:ES 没有 join。建模关系的四种方式按推荐顺序:
| 方式 | 思路 | 代价 | 适用 |
|---|---|---|---|
| 反规范化(Denormalize) | 把关联数据冗余进每个文档 | 数据重复、更新要改多处 | 首选,读多写少 |
| 应用层 join | 查两次,在代码里拼接 | 多一次网络往返 | 一对多且"多"侧很大 |
| nested 对象 | 对象数组整体索引,保持内部关联 | 查询稍慢,更新整文档重建 | 对象数组(如文章的评论列表) |
| parent-child(join 字段) | 父子文档独立存储,同分片关联 | 查询开销最大 | 子文档频繁独立更新 |
默认答案永远是先反规范化。搜索系统的哲学是"用写入时的冗余,换查询时的简单"——磁盘便宜,join 昂贵。
九、集群管理与运维要点
最后一部分面向生产:
- 节点角色分工:master 节点管集群元数据(建议 3 个专职 master 防脑裂)、data 节点存数据、协调节点转发请求。大集群应该角色分离。
- 脑裂与 minimum_master_nodes:网络分区时两个子集群各自选主 = 数据写烂。经典配置是
master 节点数 / 2 + 1(新版本的集群选举机制已自动化)。 - 分片规划:分片不是越多越好——每个分片都有固定开销(文件句柄、内存、段管理)。单分片控制在几十 GB 量级、节点堆内存 32GB 以内(压指针上限)、堆内存不超过物理内存一半(另一半留给文件系统缓存)。
- 监控集群健康:
green/yellow/red+hot_threads+catAPI 家族。 - 升级与备份:滚动升级逐个节点来;快照(snapshot)到 S3/HDFS 做备份。
十、全书知识地图
graph TD
CORE["倒排索引<br/>(一切的根基)"]
CORE --> WRITE["写入链路<br/>refresh/translog/merge<br/>→ 近实时 + 不丢数据"]
CORE --> SEARCH["搜索<br/>Query/Filter + bool 组合<br/>TF-IDF 相关性算分"]
CORE --> AGG["聚合<br/>桶 × 度量嵌套"]
WRITE --> DIST["分布式<br/>分片路由 scatter-gather<br/>主副本容灾"]
SEARCH --> LANG["语言处理<br/>分析器/纠错/输入即搜"]
SEARCH --> MODEL["建模<br/>反规范化优先"]
DIST --> OPS["运维<br/>健康监控/分片规划/防脑裂"]
style CORE fill:#e1f5e1
如果只能记住三句话:
- 一切性能与行为的答案都在倒排索引里:不可变段决定了 NRT,分词决定了查不查得到,keyword/text 之分决定了能不能聚合;
- query 算分、filter 缓存——写任何查询前先想这一条;
- 分片是分布式的原子:路由公式决定了主分片数不能改,副本决定了读吞吐和容灾。
参考资料:Clinton Gormley, Zachary Tong. 《Elasticsearch: The Definitive Guide》. O’Reilly, 2015. 中文版:elasticsearch-definitive-guide-cn(looly 等译)。