Elasticsearch 商业场景训练营
Elasticsearch 不能只学会写几个 _search。商业项目里真正要会的是:能把 MySQL 表设计成 ES 搜索文档,能解释为什么 ES 查询快,能解释为什么写入成功后不是立刻可搜,能处理 MySQL 和 ES 数据不一致,能在 ES 更新失败时补偿,能重建索引并平滑切换,能排查搜不到、搜不准、查询慢和写入慢。
训练目标:用商品搜索、订单检索、医疗资产搜索、MySQL 同步 ES、索引重建、查询排查这些场景,把 ES 原理变成可执行的工程能力。
训练总流程
flowchart TD
A["确定搜索场景"] --> B["设计搜索文档"]
B --> C["设计 Mapping"]
C --> D["写入测试数据"]
D --> E["编写 Query DSL"]
E --> F["解释倒排索引和 doc values"]
F --> G["处理 MySQL 同步和补偿"]
G --> H["重建索引和别名切换"]
H --> I["排查搜不到/搜不准/查询慢"]训练一:商品搜索文档设计
场景
商品搜索页要支持关键词搜索、品牌筛选、类目筛选、价格排序、销量排序、标签检索和高亮。MySQL 里这些数据可能分散在多张表,但 ES 查询时不适合临时 Join,所以要设计成面向搜索的一份文档。
文档 Demo
{
"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"
}为什么不能照搬 MySQL 表
| MySQL 思路 | ES 思路 | 原因 |
|---|---|---|
| 查询时 Join 品牌表 | 写入文档时冗余 brandName | ES 不擅长运行时 Join |
| 商品名一个字段即可 | productName 用 text,必要时加 .keyword | 既要全文搜索,也可能排序/聚合 |
| 状态用字符串随便查 | stockStatus 用 keyword | 精确过滤不需要分词 |
| 价格金额直接文本 | price 用数字类型 | 排序和范围过滤需要数值索引 |
训练二:Mapping 设计
curl -X PUT "http://localhost:9200/product_search_v1" \
-H "Content-Type: application/json" \
-d '{
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1,
"refresh_interval": "1s"
},
"mappings": {
"properties": {
"id": { "type": "long" },
"productName": {
"type": "text",
"analyzer": "standard",
"fields": {
"keyword": { "type": "keyword" }
}
},
"brandId": { "type": "long" },
"brandName": { "type": "keyword" },
"categoryId": { "type": "long" },
"categoryName": { "type": "keyword" },
"tags": { "type": "keyword" },
"price": { "type": "long" },
"stockStatus": { "type": "keyword" },
"saleCount": { "type": "long" },
"updatedAt": { "type": "date" }
}
}
}'原理解释
text 会分词,用于全文检索;keyword 不分词,用于精确过滤、聚合和排序;数字和日期字段使用专门的数据结构,适合范围查询和排序。
flowchart TD
A["字段进入 ES"] --> B{"字段类型"}
B -- "text" --> C["Analyzer 分词"]
C --> D["建立倒排索引"]
B -- "keyword" --> E["整体作为一个词项"]
E --> F["精确过滤/聚合"]
B -- "number/date" --> G["范围查询和排序结构"]不这样会怎样
| 错误 | 后果 |
|---|---|
productName 只用 keyword | 搜“蓝牙耳机”可能搜不到包含部分词的商品 |
brandName 用 text 聚合 | 分词后聚合结果异常 |
| 金额用字符串 | 范围查询和排序可能不符合数值语义 |
| Mapping 错了再改字段类型 | 通常需要重建索引 |
训练三:写入和近实时搜索
写入文档
curl -X POST "http://localhost:9200/product_search_v1/_doc/1001" \
-H "Content-Type: application/json" \
-d '{
"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"
}'马上搜索:
curl -X GET "http://localhost:9200/product_search_v1/_search" \
-H "Content-Type: application/json" \
-d '{
"query": {
"match": {
"productName": "降噪耳机"
}
}
}'为什么叫近实时
ES 写入成功后,文档先进入内存 buffer 和 translog。只有 refresh 后,新的 segment 被打开用于搜索,文档才对搜索可见。默认 refresh_interval 常见为 1 秒,所以 ES 是 Near Real Time,不是强实时。
flowchart TD
A["写入文档"] --> B["写内存 buffer"]
B --> C["写 translog"]
C --> D["返回写入成功"]
D --> E["refresh"]
E --> F["生成可搜索 segment"]
F --> G["搜索可见"]业务后果
支付、库存、订单状态不能依赖 ES 立即可见。搜索列表可以接受短暂延迟;关键交易必须回源 MySQL 或业务服务校验。
训练四:为什么 ES 查询快
关键词查询
curl -X GET "http://localhost:9200/product_search_v1/_search" \
-H "Content-Type: application/json" \
-d '{
"query": {
"bool": {
"must": [
{ "match": { "productName": "蓝牙降噪" } }
],
"filter": [
{ "term": { "brandName": "SoundMax" } },
{ "range": { "price": { "lte": 50000 } } }
]
}
},
"sort": [
{ "saleCount": "desc" }
],
"from": 0,
"size": 20
}'原理
ES 快不是因为“比 MySQL 高级”,而是搜索模型不同:
- 全文检索用倒排索引,从词项快速找到文档 ID。
- filter 不参与评分,适合缓存和快速过滤。
keyword/date/number有适合过滤、排序、聚合的数据结构。- 分片并行查询,协调节点合并 TopN。
- Lucene segment 不可变,读取结构更稳定。
flowchart TD
A["关键词 蓝牙"] --> B["倒排索引"]
B --> C["找到候选 docId"]
C --> D["filter 过滤品牌和价格"]
D --> E["每个分片计算 TopN"]
E --> F["协调节点合并排序"]
F --> G["Fetch 原文返回"]训练五:term 和 match 为什么经常用错
错误示例
{
"query": {
"term": {
"productName": "蓝牙耳机"
}
}
}如果 productName 是 text,索引时已经被分词,term 不会对查询词再分词,可能匹配不到。
正确理解
| 查询 | 是否分词 | 适合字段 |
|---|---|---|
match | 会分析查询文本 | text |
term | 不分析,精确匹配 | keyword、数字、枚举 |
range | 范围 | 数字、日期 |
bool filter | 不评分过滤 | 状态、品牌、价格 |
训练六:MySQL 同步 ES
推荐链路
MySQL 是事实源,ES 是搜索视图。常用同步方式包括 Outbox、本地消息表、MQ、binlog CDC。
flowchart TD
A["业务写 MySQL"] --> B["事务提交"]
B --> C["Outbox / Binlog / MQ"]
C --> D["同步服务"]
D --> E["组装 ES 文档"]
E --> F["写入 ES"]
F --> G{"成功"}
G -- "是" --> H["记录同步成功"]
G -- "否" --> I["重试 / 异常表 / 死信"]同步消息 Demo
{
"eventId": "evt-product-1001-17",
"entityType": "PRODUCT",
"entityId": 1001,
"version": 17,
"eventType": "PRODUCT_UPDATED",
"occurredAt": "2026-07-06T10:00:00"
}同步服务不要完全相信消息里的商品内容,可靠做法是根据 entityId 回查 MySQL,组装最新搜索文档,再写 ES。这样可以减少消息乱序导致旧数据覆盖新数据。
训练七:ES 更新失败怎么办
错误做法
业务更新 MySQL 成功,调用 ES 失败,然后直接忽略。
后果:搜索页长期展示旧数据,用户点进去发现详情不一致。
正确补偿
flowchart TD
A["ES 写入失败"] --> B["记录失败事件"]
B --> C["进入重试队列"]
C --> D{"重试成功"}
D -- "是" --> E["标记成功"]
D -- "否" --> F["进入异常表/死信"]
F --> G["定时补偿从 MySQL 重建文档"]
G --> H["人工告警和对账"]异常表 Demo
create table es_sync_fail_event (
id bigint primary key auto_increment,
event_id varchar(64) not null,
entity_type varchar(32) not null,
entity_id bigint not null,
version bigint not null,
error_message varchar(1024) not null,
retry_count int not null default 0,
next_retry_time datetime not null,
status varchar(20) not null,
created_at datetime not null,
updated_at datetime not null,
unique key uk_event_id (event_id),
key idx_status_retry_time (status, next_retry_time)
);标准回答
ES 更新失败不能回滚已经提交的 MySQL,因为 MySQL 是事实源,ES 是搜索视图。正确做法是记录失败事件,进入重试或死信,后续补偿任务从 MySQL 重新查询最新数据并写入 ES。必要时做版本号幂等,避免旧消息覆盖新文档,并通过对账任务发现 MySQL 和 ES 不一致。训练八:乱序和版本幂等
问题
商品先更新到版本 18,又因为网络抖动收到了版本 17 的旧消息。如果直接写 ES,旧数据会覆盖新数据。
解决思路
文档中保存业务版本:
{
"id": 1001,
"productName": "无线蓝牙降噪耳机 Pro",
"version": 18,
"updatedAt": "2026-07-06T10:00:00"
}同步时只允许新版本覆盖旧版本。工程上可以:
- 同步服务回查 MySQL 最新数据再写。
- 使用外部版本控制。
- 在业务侧记录同步版本。
- 对账发现旧版本文档并重刷。
flowchart TD
A["收到同步事件"] --> B["读取 event version"]
B --> C["查询 MySQL 最新 version"]
C --> D{"事件是否过期"}
D -- "是" --> E["丢弃旧事件"]
D -- "否" --> F["组装最新文档写 ES"]训练九:重建索引和别名切换
Mapping 字段类型错了,通常不能直接把已有字段从 text 改成 keyword 或从字符串改成数字,需要重建索引。
别名流程
flowchart TD
A["当前别名 product_search"] --> B["指向 product_search_v1"]
B --> C["创建 product_search_v2"]
C --> D["全量重建数据"]
D --> E["增量同步双写或追平"]
E --> F["校验数量和抽样"]
F --> G["别名切到 v2"]
G --> H["观察后删除 v1"]命令示例
curl -X POST "http://localhost:9200/_aliases" \
-H "Content-Type: application/json" \
-d '{
"actions": [
{ "remove": { "index": "product_search_v1", "alias": "product_search" } },
{ "add": { "index": "product_search_v2", "alias": "product_search" } }
]
}'应用访问别名 product_search,不要直接写死物理索引 product_search_v1。
训练十:查询慢排查
常见慢查询
| 问题 | 表现 | 优化 |
|---|---|---|
| 深分页 | from 很大 | search_after |
| 通配符前缀 | *abc | 改索引设计、ngram |
| 聚合范围大 | CPU/内存高 | 缩小时间范围、预聚合 |
| 高亮字段大 | Fetch 慢 | 控制字段长度 |
| 分片太多 | 协调开销大 | 合理分片 |
| 排序字段无 doc values | 排序慢 | Mapping 修正并重建 |
排查流程
flowchart TD
A["查询慢"] --> B["看 DSL"]
B --> C["看 took 和 profile"]
C --> D{"Query 慢还是 Fetch 慢"}
D -- "Query 慢" --> E["看分词、filter、聚合、分片"]
D -- "Fetch 慢" --> F["看返回字段、高亮、大文档"]
E --> G["看集群 CPU/GC/磁盘"]
F --> G
G --> H["调整 Mapping / DSL / 分片 / 业务分页"]最终验收清单
做完这页后,你应该能回答:
- ES 为什么不是 MySQL 的替代品?
- 搜索文档为什么不能照搬 MySQL 表?
text和keyword怎么选?- ES 为什么叫近实时搜索?
- ES 为什么查询快?
term查text为什么可能搜不到?- MySQL 到 ES 同步链路怎么设计?
- ES 更新失败怎么办?
- 乱序消息怎么避免旧数据覆盖新数据?
- 为什么 Mapping 错了常要重建索引?
- 别名切换为什么能平滑重建索引?
- 深分页、聚合、高亮、Fetch 慢分别怎么排查?
关联知识点
| 知识点 | 入口 |
|---|---|
| ES 主线 | 从零到生产级掌握 |
| 写入原理 | 写入、Refresh 与 Segment 全过程 |
| 查询原理 | 查询 Query 与 Fetch 全过程 |
| 倒排索引和评分 | 倒排索引、分词与 BM25 |
| Mapping 和查询 | Mapping 与查询 |
| 同步与重建 | 同步与重建索引 |
| MySQL 与 ES 一致性 | MySQL 与 ES 数据一致性 |
| 性能排查 | 性能优化与排查 |
| 面试 | ES 面试题 |
