Skip to content

Elasticsearch 从零到生产级掌握

Elasticsearch 不能只学会写几个 _search。真正到商业项目里,你要能解释:为什么 ES 查询快,为什么叫近实时,为什么写入成功不一定马上搜到,为什么 Mapping 错了要重建索引,为什么 termtext 经常搜不到,为什么深分页慢,为什么 MySQL 和 ES 只能最终一致,为什么 ES 更新失败不能回滚 MySQL。

一句话建立主线:

Elasticsearch 是基于 Lucene 的分布式搜索和分析引擎。MySQL 保存事实数据,ES 保存面向搜索的文档视图;ES 用倒排索引、分词、doc values、分片并行和 Query/Fetch 流程解决全文检索、过滤、排序、聚合和日志分析。

学习目标

学完这一页,你要能做到:

  1. 解释 ES、Lucene、索引、文档、Mapping、Analyzer、Shard、Replica 的关系。
  2. 根据商业搜索页面设计索引文档,而不是照搬 MySQL 表。
  3. 解释倒排索引为什么让全文搜索快,doc values 为什么让排序聚合快。
  4. 解释写入、refresh、segment、translog、flush、merge 的全过程。
  5. 解释 Query Phase、Fetch Phase、分片 TopN、协调节点合并的查询全过程。
  6. 写出商品搜索、订单检索、日志检索常见 Query DSL。
  7. 处理搜不到、搜不准、查询慢、写入慢、yellow/red、磁盘水位、JVM 压力。
  8. 设计 MySQL 到 ES 的同步链路、失败重试、乱序幂等、别名重建和补偿对账。

如果你已经读完主线,但还不知道怎么把 ES 原理落到商品搜索、MySQL 同步、更新失败补偿和查询排查里,继续做:Elasticsearch 商业场景训练营。它把搜索文档设计、Mapping、近实时、查询快、同步一致性、别名重建和慢查询排查串成可验证训练。

学习路线

mermaid
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 保存搜索视图。

mermaid
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:

能力MySQLElasticsearch
事务强事务、ACID不适合作交易事务主库
数据模型表、行、关系、JoinJSON 文档,倾向冗余
查询强项精确查询、事务读写全文检索、过滤、聚合
一致性主库事实源近实时、最终一致搜索视图
更新方式原地更新行Lucene segment 不可变,更新成本更高

结论:订单、支付、库存、账户余额必须以 MySQL 或业务服务为准。ES 搜到的结果可以用于展示、筛选、检索,但关键交易要回源校验。

第二步:文档模型不是照搬表

商品搜索页面通常需要:关键词、品牌、类目、价格、库存、销量、标签、高亮、聚合筛选。

MySQL 可能拆成多张表:

内容
product商品主信息
brand品牌
category类目
product_tag标签
product_stock库存

ES 不适合查询时做多表 Join,所以搜索文档应该冗余成一个面向搜索的 JSON。

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"
}

为什么要冗余:

  1. 搜索时不再 Join,查询链路更短。
  2. 品牌名、类目名、标签可以直接高亮、过滤、聚合。
  3. ES 文档就是搜索页面需要的读模型。
  4. 写入同步复杂一点,换取查询稳定和高性能。

第三步:Mapping 怎么设计

创建商品搜索索引:

json
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" }
    }
  }
}

字段类型选择:

字段类型原因
productNametext要全文检索和高亮
productName.keywordkeyword需要精确匹配或排序时使用
brandNamekeyword品牌过滤和聚合,不需要分词
pricescaled_float金额范围和排序,避免浮点误差
stockStatuskeyword精确过滤
saleCountlong排序和加权
updatedAtdate同步排查和时间过滤

Mapping 错了为什么常要重建索引:

  1. text 写入时已经按 analyzer 建了倒排索引。
  2. keyword 写入时按完整值建索引和 doc values。
  3. 历史数据不会因为你改配置自动重新分词。
  4. 字段类型很多情况下不能原地修改。
  5. 标准做法是新建索引、全量导入、增量追平、别名切换。

第四步:倒排索引为什么快

数据库 like '%耳机%' 难以高效使用普通 B+Tree,因为关键词在字符串中间。

ES 写入时提前分词,建立“词 -> 文档”的倒排索引。

mermaid
flowchart TD
    A["文档1: 无线蓝牙降噪耳机"] --> B["Analyzer 分词"]
    B --> C["无线 / 蓝牙 / 降噪 / 耳机"]
    C --> D["倒排索引"]
    D --> E["耳机 -> doc1, doc2, doc8"]
    D --> F["降噪 -> doc1, doc8"]

搜索“降噪耳机”时:

mermaid
flowchart TD
    A["查询: 降噪耳机"] --> B["查询分词"]
    B --> C["降噪 / 耳机"]
    C --> D["查倒排表"]
    D --> E["取候选文档集合"]
    E --> F["BM25 评分"]
    F --> G["按分数和业务排序返回"]

倒排索引里不只是存词和文档 ID,还可能存:

信息用途
文档 ID找到包含词的文档
词频BM25 评分
位置短语查询
偏移量高亮
字段长度评分归一

这就是 ES 查询快的核心之一:搜索时不是全表扫字符串,而是直接按词找候选文档。

第五步:textkeywordmatchterm

最常见搜不到问题来自类型和查询不匹配。

字段/查询行为适合
text写入时分词全文检索
keyword不分词,完整值索引精确过滤、排序、聚合
match查询词会分析text
term查询词不分析keyword、状态、ID

错误示例:

json
GET /product_search_v1/_search
{
  "query": {
    "term": {
      "productName": "无线蓝牙降噪耳机"
    }
  }
}

productNametext,写入时已经被拆成多个 token。你用 term 查整句话,很可能搜不到。

正确示例:

json
GET /product_search_v1/_search
{
  "query": {
    "match": {
      "productName": "无线蓝牙降噪耳机"
    }
  }
}

查状态:

json
{
  "term": {
    "stockStatus": "IN_STOCK"
  }
}

第六步:写入、Refresh 与近实时

ES 为什么叫近实时?因为写入成功不等于立刻可搜索。

mermaid
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,让新文档可查
segmentLucene 不可变索引段
flushLucene commit,裁剪 translog
merge合并小 segment,清理删除标记

为什么更新成本高:

  1. Lucene segment 不可变。
  2. 更新不是原地改字段。
  3. 旧文档打删除标记。
  4. 新文档重新写入。
  5. 后续 merge 清理旧版本。

所以 ES 不适合高频单文档强一致更新主链路。高频事实更新仍应落主库,ES 做搜索视图。

第七步:查询 Query 与 Fetch 全过程

一次搜索不是单节点完成,而是分布式执行。

mermaid
flowchart TD
    A["搜索请求"] --> B["协调节点"]
    B --> C["分发 Query 到相关分片"]
    C --> D["每个分片本地查询 TopN"]
    D --> E["协调节点合并全局 TopN"]
    E --> F["Fetch 阶段取 _source"]
    F --> G["返回结果、高亮、聚合"]

为什么深分页慢:

json
GET /product_search_v1/_search
{
  "from": 100000,
  "size": 20,
  "query": { "match_all": {} }
}

每个分片都要取 from + size 的候选结果,协调节点再合并并丢弃前面的数据。分页越深,浪费越大。

解决:

方案适合
限制最大页数普通搜索页面
search_after下一页翻页
PIT + search_after稳定快照翻页
Scroll后台批量导出,不适合用户实时分页

第八步:Query DSL 怎么写

商品搜索示例:

json
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。

json
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

优化方向:

  1. 先 filter 缩小范围。
  2. 聚合字段使用 keyword、数值、日期。
  3. 控制 bucket 数量。
  4. 大盘统计做预聚合或离线统计。
  5. 不要让前端任意指定大聚合。

第十步:MySQL 到 ES 同步

推荐思路:MySQL 是事实源,ES 是搜索副本。

mermaid
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:

  1. 搜索文档可能来自多张表。
  2. 消费时查主库最新快照,可以避免旧消息覆盖新数据。
  3. 消息更小、更稳定。
  4. 重试时能重新组装最新文档。

幂等设计:

问题方案
消息重复_id 使用业务 ID,重复 upsert 同一文档
消息乱序消费端查 MySQL 最新快照,或用版本号判断
删除和下架按主库状态决定删除文档还是标记不可搜
写 ES 超时不能判断成功与否时重试,依赖幂等
ES 失败重试、死信、告警、补偿对账

ES 更新失败不能回滚 MySQL 主事务。因为 ES 是搜索视图,主库事务已经成功时,应该让同步链路最终补上,而不是让搜索系统影响核心写入链路。

第十一步:零停机重建索引

Mapping 改错、分词器调整、字段类型变化,通常要重建索引。

mermaid
flowchart TD
    A["业务访问别名 product_search"] --> B["旧索引 product_search_v1"]
    C["创建新索引 product_search_v2"] --> D["全量导入历史数据"]
    D --> E["增量同步追平"]
    E --> F["校验数量和核心查询"]
    F --> G["原子切换别名到 v2"]
    G --> H["观察和回滚预案"]

别名切换:

json
POST /_aliases
{
  "actions": [
    { "remove": { "index": "product_search_v1", "alias": "product_search" } },
    { "add": { "index": "product_search_v2", "alias": "product_search" } }
  ]
}

为什么用别名:

  1. 业务代码不用改索引名。
  2. 新旧索引可以并行存在。
  3. 切换是原子操作。
  4. 出问题可以快速切回旧索引。

第十二步:集群、分片和副本

分片和副本解决容量、并行和高可用。

mermaid
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

写入文档:

json
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"
}

分析分词:

json
GET /product_search_v1/_analyze
{
  "field": "productName",
  "text": "无线蓝牙降噪耳机"
}

按 ID 查文档:

json
GET /product_search_v1/_doc/1001

解释评分:

json
GET /product_search_v1/_explain/1001
{
  "query": {
    "match": {
      "productName": "降噪耳机"
    }
  }
}

查看查询 profile:

json
GET /product_search_v1/_search
{
  "profile": true,
  "query": {
    "match": {
      "productName": "降噪耳机"
    }
  }
}

线上排查总流程

mermaid
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"]

常用命令:

bash
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
termtext搜不到textmatch
前端传任意 DSL越权或拖垮集群后端翻译受控 DSL
深分页不限分片大量取数再丢弃限页、search_after、PIT
同步失败吞掉ES 长期旧数据重试、死信、告警、补偿
Mapping 随意动态生成类型推错,后期难改显式 Mapping,关键索引用 strict
分片越多越好协调和管理成本升高按数据量和节点规划

面试标准回答

ES 怎么从零学到生产可用

text
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 为什么查询快

text
ES 查询快的核心是倒排索引、doc values、过滤缓存和分片并行。全文搜索时,文本在写入阶段已经分词并建立词到文档的倒排表,查询时可以直接按词找到候选文档,而不是全表扫描字符串。排序和聚合常用列式 doc values,多个分片可以并行查询,协调节点再合并 TopN。但如果 DSL 过重、深分页、大聚合、wildcard、分片过多或磁盘/GC 压力大,ES 仍然会慢。

ES 为什么是近实时

text
ES 写入成功后,文档先进入内存 buffer 和 translog,并复制到副本后返回;只有 refresh 生成新的可搜索 segment 后,搜索请求才能查到这批数据。默认 refresh 通常有短暂间隔,所以 ES 是 Near Real-Time 近实时搜索,不是写入后强实时可见。业务上要接受短暂延迟,强一致判断要回主库。

MySQL 和 ES 如何保证一致性

text
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,也知道出了问题怎么逐层定位。