Elasticsearch 从零到生产级掌握
Elasticsearch 不能只学会写几个 _search。真正到商业项目里,你要能解释:为什么 ES 查询快,为什么叫近实时,为什么写入成功不一定马上搜到,为什么 Mapping 错了要重建索引,为什么 term 查 text 经常搜不到,为什么深分页慢,为什么 MySQL 和 ES 只能最终一致,为什么 ES 更新失败不能回滚 MySQL。
一句话建立主线:
Elasticsearch 是基于 Lucene 的分布式搜索和分析引擎。MySQL 保存事实数据,ES 保存面向搜索的文档视图;ES 用倒排索引、分词、doc values、分片并行和 Query/Fetch 流程解决全文检索、过滤、排序、聚合和日志分析。
学习目标
学完这一页,你要能做到:
- 解释 ES、Lucene、索引、文档、Mapping、Analyzer、Shard、Replica 的关系。
- 根据商业搜索页面设计索引文档,而不是照搬 MySQL 表。
- 解释倒排索引为什么让全文搜索快,doc values 为什么让排序聚合快。
- 解释写入、refresh、segment、translog、flush、merge 的全过程。
- 解释 Query Phase、Fetch Phase、分片 TopN、协调节点合并的查询全过程。
- 写出商品搜索、订单检索、日志检索常见 Query DSL。
- 处理搜不到、搜不准、查询慢、写入慢、yellow/red、磁盘水位、JVM 压力。
- 设计 MySQL 到 ES 的同步链路、失败重试、乱序幂等、别名重建和补偿对账。
如果你已经读完主线,但还不知道怎么把 ES 原理落到商品搜索、MySQL 同步、更新失败补偿和查询排查里,继续做:Elasticsearch 商业场景训练营。它把搜索文档设计、Mapping、近实时、查询快、同步一致性、别名重建和慢查询排查串成可验证训练。
学习路线
flowchart TD
A["定位<br/>ES 是搜索视图,不是主库"] --> B["文档模型<br/>Index、Document、Mapping"]
B --> C["搜索原理<br/>倒排索引、分词、BM25"]
C --> D["写入原理<br/>buffer、translog、refresh、segment"]
D --> E["查询原理<br/>Query Phase、Fetch Phase"]
E --> F["工程能力<br/>同步、重建、别名、一致性"]
F --> G["集群能力<br/>分片、副本、扩容、高可用"]
G --> H["生产排查<br/>搜不到、搜不准、查询慢、写入慢"]顺序不能乱。先明确 ES 不是主库,再学文档模型;先理解倒排索引,再谈分词和评分;先理解 refresh,再解释近实时;先理解 Query/Fetch,再排查深分页;先理解 MySQL 是事实源,再设计同步和补偿。
第一步:ES 在系统中的位置
商业系统常见架构是:MySQL 保存业务事实,ES 保存搜索视图。
flowchart TD
A["用户写入商品/订单/资产"] --> B["业务服务"]
B --> C["MySQL 事务提交"]
C --> D["MQ / Binlog CDC / 本地消息表"]
D --> E["同步服务组装搜索文档"]
E --> F["写入 ES"]
G["用户搜索"] --> H["搜索服务组装受控 DSL"]
H --> F
F --> I["返回搜索结果"]为什么不能把 ES 当 MySQL:
| 能力 | MySQL | Elasticsearch |
|---|---|---|
| 事务 | 强事务、ACID | 不适合作交易事务主库 |
| 数据模型 | 表、行、关系、Join | JSON 文档,倾向冗余 |
| 查询强项 | 精确查询、事务读写 | 全文检索、过滤、聚合 |
| 一致性 | 主库事实源 | 近实时、最终一致搜索视图 |
| 更新方式 | 原地更新行 | Lucene segment 不可变,更新成本更高 |
结论:订单、支付、库存、账户余额必须以 MySQL 或业务服务为准。ES 搜到的结果可以用于展示、筛选、检索,但关键交易要回源校验。
第二步:文档模型不是照搬表
商品搜索页面通常需要:关键词、品牌、类目、价格、库存、销量、标签、高亮、聚合筛选。
MySQL 可能拆成多张表:
| 表 | 内容 |
|---|---|
product | 商品主信息 |
brand | 品牌 |
category | 类目 |
product_tag | 标签 |
product_stock | 库存 |
ES 不适合查询时做多表 Join,所以搜索文档应该冗余成一个面向搜索的 JSON。
{
"id": 1001,
"productName": "无线蓝牙降噪耳机 Pro",
"brandId": 20,
"brandName": "SoundMax",
"categoryId": 300,
"categoryName": "耳机",
"tags": ["蓝牙耳机", "主动降噪", "官方旗舰"],
"price": 29900,
"stockStatus": "IN_STOCK",
"saleCount": 5821,
"updatedAt": "2026-07-01T10:00:00"
}为什么要冗余:
- 搜索时不再 Join,查询链路更短。
- 品牌名、类目名、标签可以直接高亮、过滤、聚合。
- ES 文档就是搜索页面需要的读模型。
- 写入同步复杂一点,换取查询稳定和高性能。
第三步:Mapping 怎么设计
创建商品搜索索引:
PUT /product_search_v1
{
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1,
"refresh_interval": "1s"
},
"mappings": {
"dynamic": "strict",
"properties": {
"id": { "type": "long" },
"productName": {
"type": "text",
"analyzer": "standard",
"fields": {
"keyword": { "type": "keyword", "ignore_above": 256 }
}
},
"brandId": { "type": "long" },
"brandName": { "type": "keyword" },
"categoryId": { "type": "long" },
"categoryName": { "type": "keyword" },
"tags": { "type": "keyword" },
"price": { "type": "scaled_float", "scaling_factor": 100 },
"stockStatus": { "type": "keyword" },
"saleCount": { "type": "long" },
"updatedAt": { "type": "date" }
}
}
}字段类型选择:
| 字段 | 类型 | 原因 |
|---|---|---|
productName | text | 要全文检索和高亮 |
productName.keyword | keyword | 需要精确匹配或排序时使用 |
brandName | keyword | 品牌过滤和聚合,不需要分词 |
price | scaled_float | 金额范围和排序,避免浮点误差 |
stockStatus | keyword | 精确过滤 |
saleCount | long | 排序和加权 |
updatedAt | date | 同步排查和时间过滤 |
Mapping 错了为什么常要重建索引:
text写入时已经按 analyzer 建了倒排索引。keyword写入时按完整值建索引和 doc values。- 历史数据不会因为你改配置自动重新分词。
- 字段类型很多情况下不能原地修改。
- 标准做法是新建索引、全量导入、增量追平、别名切换。
第四步:倒排索引为什么快
数据库 like '%耳机%' 难以高效使用普通 B+Tree,因为关键词在字符串中间。
ES 写入时提前分词,建立“词 -> 文档”的倒排索引。
flowchart TD
A["文档1: 无线蓝牙降噪耳机"] --> B["Analyzer 分词"]
B --> C["无线 / 蓝牙 / 降噪 / 耳机"]
C --> D["倒排索引"]
D --> E["耳机 -> doc1, doc2, doc8"]
D --> F["降噪 -> doc1, doc8"]搜索“降噪耳机”时:
flowchart TD
A["查询: 降噪耳机"] --> B["查询分词"]
B --> C["降噪 / 耳机"]
C --> D["查倒排表"]
D --> E["取候选文档集合"]
E --> F["BM25 评分"]
F --> G["按分数和业务排序返回"]倒排索引里不只是存词和文档 ID,还可能存:
| 信息 | 用途 |
|---|---|
| 文档 ID | 找到包含词的文档 |
| 词频 | BM25 评分 |
| 位置 | 短语查询 |
| 偏移量 | 高亮 |
| 字段长度 | 评分归一 |
这就是 ES 查询快的核心之一:搜索时不是全表扫字符串,而是直接按词找候选文档。
第五步:text、keyword、match、term
最常见搜不到问题来自类型和查询不匹配。
| 字段/查询 | 行为 | 适合 |
|---|---|---|
text | 写入时分词 | 全文检索 |
keyword | 不分词,完整值索引 | 精确过滤、排序、聚合 |
match | 查询词会分析 | 查 text |
term | 查询词不分析 | 查 keyword、状态、ID |
错误示例:
GET /product_search_v1/_search
{
"query": {
"term": {
"productName": "无线蓝牙降噪耳机"
}
}
}productName 是 text,写入时已经被拆成多个 token。你用 term 查整句话,很可能搜不到。
正确示例:
GET /product_search_v1/_search
{
"query": {
"match": {
"productName": "无线蓝牙降噪耳机"
}
}
}查状态:
{
"term": {
"stockStatus": "IN_STOCK"
}
}第六步:写入、Refresh 与近实时
ES 为什么叫近实时?因为写入成功不等于立刻可搜索。
flowchart TD
A["写入请求"] --> B["协调节点路由到主分片"]
B --> C["主分片写内存 buffer"]
C --> D["写 translog"]
D --> E["复制到副本分片"]
E --> F["返回写入成功"]
C --> G["refresh"]
G --> H["生成可搜索 segment"]
H --> I["搜索可见"]关键概念:
| 概念 | 作用 |
|---|---|
| buffer | 新写入文档先进入内存缓冲 |
| translog | 记录写入操作,崩溃后可恢复 |
| refresh | 生成可搜索 segment,让新文档可查 |
| segment | Lucene 不可变索引段 |
| flush | Lucene commit,裁剪 translog |
| merge | 合并小 segment,清理删除标记 |
为什么更新成本高:
- Lucene segment 不可变。
- 更新不是原地改字段。
- 旧文档打删除标记。
- 新文档重新写入。
- 后续 merge 清理旧版本。
所以 ES 不适合高频单文档强一致更新主链路。高频事实更新仍应落主库,ES 做搜索视图。
第七步:查询 Query 与 Fetch 全过程
一次搜索不是单节点完成,而是分布式执行。
flowchart TD
A["搜索请求"] --> B["协调节点"]
B --> C["分发 Query 到相关分片"]
C --> D["每个分片本地查询 TopN"]
D --> E["协调节点合并全局 TopN"]
E --> F["Fetch 阶段取 _source"]
F --> G["返回结果、高亮、聚合"]为什么深分页慢:
GET /product_search_v1/_search
{
"from": 100000,
"size": 20,
"query": { "match_all": {} }
}每个分片都要取 from + size 的候选结果,协调节点再合并并丢弃前面的数据。分页越深,浪费越大。
解决:
| 方案 | 适合 |
|---|---|
| 限制最大页数 | 普通搜索页面 |
search_after | 下一页翻页 |
PIT + search_after | 稳定快照翻页 |
| Scroll | 后台批量导出,不适合用户实时分页 |
第八步:Query DSL 怎么写
商品搜索示例:
GET /product_search_v1/_search
{
"from": 0,
"size": 10,
"query": {
"bool": {
"must": [
{
"multi_match": {
"query": "无线降噪耳机",
"fields": ["productName^5", "tags^2"]
}
}
],
"filter": [
{ "term": { "stockStatus": "IN_STOCK" } },
{ "range": { "price": { "gte": 10000, "lte": 30000 } } }
],
"should": [
{ "term": { "tags": { "value": "官方旗舰", "boost": 2 } } },
{ "range": { "saleCount": { "gte": 1000, "boost": 1.5 } } }
]
}
},
"sort": [
{ "_score": "desc" },
{ "saleCount": "desc" },
{ "id": "desc" }
],
"highlight": {
"fields": {
"productName": {}
}
}
}设计原则:
| 部分 | 放什么 |
|---|---|
must | 关键词全文检索,需要参与评分 |
filter | 品牌、类目、状态、权限、时间范围 |
should | 运营加权、标签加权、销量加权 |
sort | 分数、销量、时间、ID 兜底 |
不要把前端传来的任意 JSON DSL 直接透传到 ES。服务端应该接收业务参数,统一加权限、租户、状态过滤,并限制分页、聚合和 wildcard。
第九步:聚合为什么快也可能慢
聚合通常依赖 doc values。
GET /product_search_v1/_search
{
"size": 0,
"query": {
"term": {
"stockStatus": "IN_STOCK"
}
},
"aggs": {
"brand_count": {
"terms": {
"field": "brandName",
"size": 20
}
}
}
}聚合慢常见原因:
| 原因 | 说明 |
|---|---|
| 聚合范围太大 | 没有时间、类目、状态等过滤 |
| bucket 太多 | 高基数字段聚合成本高 |
对 text 聚合 | text 分词,不适合直接聚合 |
| 分片太多 | 每个分片都聚合,协调节点合并成本高 |
| 内存压力 | 大聚合可能触发 circuit breaker |
优化方向:
- 先 filter 缩小范围。
- 聚合字段使用
keyword、数值、日期。 - 控制 bucket 数量。
- 大盘统计做预聚合或离线统计。
- 不要让前端任意指定大聚合。
第十步:MySQL 到 ES 同步
推荐思路:MySQL 是事实源,ES 是搜索副本。
flowchart TD
A["业务写 MySQL"] --> B["事务提交"]
B --> C["写本地消息表 / MQ / Binlog"]
C --> D["同步消费者"]
D --> E["按 ID 查 MySQL 最新快照"]
E --> F["组装 ES 文档"]
F --> G["upsert ES"]
G --> H{"成功吗"}
H -- "成功" --> I["记录同步成功"]
H -- "失败" --> J["重试 / 死信 / 告警"]
J --> K["补偿任务对账修复"]为什么消息里常只放 ID:
- 搜索文档可能来自多张表。
- 消费时查主库最新快照,可以避免旧消息覆盖新数据。
- 消息更小、更稳定。
- 重试时能重新组装最新文档。
幂等设计:
| 问题 | 方案 |
|---|---|
| 消息重复 | _id 使用业务 ID,重复 upsert 同一文档 |
| 消息乱序 | 消费端查 MySQL 最新快照,或用版本号判断 |
| 删除和下架 | 按主库状态决定删除文档还是标记不可搜 |
| 写 ES 超时 | 不能判断成功与否时重试,依赖幂等 |
| ES 失败 | 重试、死信、告警、补偿对账 |
ES 更新失败不能回滚 MySQL 主事务。因为 ES 是搜索视图,主库事务已经成功时,应该让同步链路最终补上,而不是让搜索系统影响核心写入链路。
第十一步:零停机重建索引
Mapping 改错、分词器调整、字段类型变化,通常要重建索引。
flowchart TD
A["业务访问别名 product_search"] --> B["旧索引 product_search_v1"]
C["创建新索引 product_search_v2"] --> D["全量导入历史数据"]
D --> E["增量同步追平"]
E --> F["校验数量和核心查询"]
F --> G["原子切换别名到 v2"]
G --> H["观察和回滚预案"]别名切换:
POST /_aliases
{
"actions": [
{ "remove": { "index": "product_search_v1", "alias": "product_search" } },
{ "add": { "index": "product_search_v2", "alias": "product_search" } }
]
}为什么用别名:
- 业务代码不用改索引名。
- 新旧索引可以并行存在。
- 切换是原子操作。
- 出问题可以快速切回旧索引。
第十二步:集群、分片和副本
分片和副本解决容量、并行和高可用。
flowchart TD
A["Index product_search"] --> B["Primary Shard 0"]
A --> C["Primary Shard 1"]
A --> D["Primary Shard 2"]
B --> E["Replica 0"]
C --> F["Replica 1"]
D --> G["Replica 2"]分片不是越多越好:
| 太少 | 太多 |
|---|---|
| 单分片过大,迁移恢复慢 | 元数据、文件句柄、查询协调成本高 |
| 扩容不灵活 | 小分片太多,集群管理压力大 |
| 单分片查询压力大 | 每次查询分发和合并成本高 |
健康状态:
| 状态 | 含义 |
|---|---|
| green | 主分片和副本都正常 |
| yellow | 主分片正常,部分副本未分配 |
| red | 部分主分片不可用 |
单节点开发环境有副本时 yellow 很常见,因为副本不能和主分片放同一节点。生产 red 要优先处理。
第十三步:完整 Demo
写入文档:
PUT /product_search_v1/_doc/1001
{
"id": 1001,
"productName": "无线蓝牙降噪耳机 Pro",
"brandId": 20,
"brandName": "SoundMax",
"categoryId": 300,
"categoryName": "耳机",
"tags": ["蓝牙耳机", "主动降噪"],
"price": 29900,
"stockStatus": "IN_STOCK",
"saleCount": 5821,
"updatedAt": "2026-07-01T10:00:00"
}分析分词:
GET /product_search_v1/_analyze
{
"field": "productName",
"text": "无线蓝牙降噪耳机"
}按 ID 查文档:
GET /product_search_v1/_doc/1001解释评分:
GET /product_search_v1/_explain/1001
{
"query": {
"match": {
"productName": "降噪耳机"
}
}
}查看查询 profile:
GET /product_search_v1/_search
{
"profile": true,
"query": {
"match": {
"productName": "降噪耳机"
}
}
}线上排查总流程
flowchart TD
A["ES 线上问题"] --> B{"表现是什么"}
B -- "搜不到" --> C["查 MySQL、ES _doc、同步链路"]
C --> D["查 Mapping、分词、term/match、filter"]
B -- "搜不准" --> E["_analyze / _explain / boost / should"]
B -- "查询慢" --> F["slowlog / profile / 深分页 / 聚合 / wildcard"]
B -- "写入慢" --> G["bulk、refresh、replica、merge、磁盘、线程池"]
B -- "yellow/red" --> H["_cluster/allocation/explain"]
B -- "内存高/GC" --> I["heap、fielddata、聚合、circuit breaker"]常用命令:
GET /_cluster/health?pretty
GET /_cat/indices?v
GET /_cat/shards?v
GET /_cluster/allocation/explain
GET /_nodes/hot_threads
GET /_cat/thread_pool/search?v
GET /_cat/thread_pool/write?v排查清单:
| 问题 | 先看什么 |
|---|---|
| 搜不到 | 主库是否有、ES 是否有、同步是否延迟、Mapping/分词/过滤 |
| 搜不准 | _analyze、_explain、字段 boost、业务排序 |
| 查询慢 | slowlog、profile、深分页、聚合、wildcard、脚本 |
| 写入慢 | bulk 大小、refresh、replica、merge、磁盘 IO |
| ES 更新失败 | 错误类型、重试、死信、补偿、Mapping 冲突 |
| red/yellow | 分片分配、节点、磁盘水位、副本数 |
| GC 高 | 大聚合、fielddata、heap、查询并发 |
常见坑
| 坑 | 后果 | 正确做法 |
|---|---|---|
| ES 替代 MySQL | 事务和强一致出问题 | MySQL 做事实源,ES 做搜索视图 |
所有字段用 text | 过滤、排序、聚合困难 | 精确字段用 keyword/数值/date |
用 term 查 text | 搜不到 | text 用 match |
| 前端传任意 DSL | 越权或拖垮集群 | 后端翻译受控 DSL |
| 深分页不限 | 分片大量取数再丢弃 | 限页、search_after、PIT |
| 同步失败吞掉 | ES 长期旧数据 | 重试、死信、告警、补偿 |
| Mapping 随意动态生成 | 类型推错,后期难改 | 显式 Mapping,关键索引用 strict |
| 分片越多越好 | 协调和管理成本升高 | 按数据量和节点规划 |
面试标准回答
ES 怎么从零学到生产可用
Elasticsearch 要按定位、文档模型、Mapping、倒排索引、分词、写入 refresh、Query/Fetch、集群分片、同步一致性和生产排查这条线学习。先明确 MySQL 是事实源,ES 是搜索视图;再按搜索页面设计文档和 Mapping。底层要理解 text/keyword、match/term、倒排索引、BM25、doc values、refresh、segment、translog。工程上要掌握 MySQL 到 ES 的 MQ/CDC 同步、幂等、乱序、失败重试、死信、补偿、别名重建,以及搜不到、搜不准、查询慢、写入慢和 red/yellow 的排查流程。ES 为什么查询快
ES 查询快的核心是倒排索引、doc values、过滤缓存和分片并行。全文搜索时,文本在写入阶段已经分词并建立词到文档的倒排表,查询时可以直接按词找到候选文档,而不是全表扫描字符串。排序和聚合常用列式 doc values,多个分片可以并行查询,协调节点再合并 TopN。但如果 DSL 过重、深分页、大聚合、wildcard、分片过多或磁盘/GC 压力大,ES 仍然会慢。ES 为什么是近实时
ES 写入成功后,文档先进入内存 buffer 和 translog,并复制到副本后返回;只有 refresh 生成新的可搜索 segment 后,搜索请求才能查到这批数据。默认 refresh 通常有短暂间隔,所以 ES 是 Near Real-Time 近实时搜索,不是写入后强实时可见。业务上要接受短暂延迟,强一致判断要回主库。MySQL 和 ES 如何保证一致性
MySQL 是事实源,ES 是搜索视图,一般保证最终一致。常见做法是业务先写 MySQL,事务提交后通过 MQ、Binlog CDC 或本地消息表发送变更事件,消费者按业务 ID 查 MySQL 最新快照并 upsert ES。写 ES 要用业务 ID 做 _id 保证幂等,处理重复和乱序,失败进入重试、死信和告警,定时补偿对账。ES 更新失败不能回滚 MySQL 主事务,关键交易读主库。关联知识点
| 知识点 | 说明 |
|---|---|
| Elasticsearch 总览 | 专栏入口和学习顺序 |
| 写入、Refresh 与 Segment | 近实时和写入过程 |
| 查询 Query 与 Fetch | 分布式搜索全过程 |
| 倒排索引与 BM25 | 查询快和评分原理 |
| Mapping 与查询 | 字段类型和 DSL |
| 同步与重建索引 | 别名切换和重建 |
| MySQL 与 ES 一致性 | 同步、失败、补偿 |
| 集群 | 分片、副本、健康状态 |
| 性能优化与排查 | 线上排查 |
| Elasticsearch 面试 | 标准回答和追问 |
本章小结
ES 从零到生产级掌握,关键是把“搜索视图、文档模型、Mapping、倒排索引、分词、写入 refresh、Query/Fetch、分片副本、同步一致性、生产排查”串起来。你要知道 ES 为什么适合搜索,也要知道为什么不能替代主库;知道为什么快,也知道什么时候会慢;知道怎么写 DSL,也知道出了问题怎么逐层定位。
