Elasticsearch 核心概念
Elasticsearch 的核心思想可以概括成一句话:
写入时把业务文档加工成可搜索结构,查询时不扫全量数据,而是通过倒排索引快速找到候选结果。
零基础学习 ES,要先把几个概念吃透:文档、字段、Mapping、倒排索引、分片、副本、refresh、segment。否则后面看到 match、term、bool、agg 时很容易只会写语法,不知道为什么。
为什么数据库 LIKE 不够
假设商品表有几百万条数据,用户搜索:
where product_name like '%无线降噪耳机%'会遇到几个问题:
- 前后都有
%时,普通 BTree 索引很难发挥作用。 - 中文没有空格,数据库不知道“无线”“降噪”“耳机”哪些是有意义的词。
- 数据库只能判断“包含不包含”,很难知道哪个商品更相关。
- 高亮、同义词、搜索建议、聚合筛选都要额外开发。
ES 的做法是提前分词并建立倒排索引。搜索时不是逐行扫描商品名,而是根据词直接找到包含这些词的文档。
基本名词
Index
Index 是索引库,可以理解成一类搜索文档的集合。
商业系统常见索引:
product_search:商品搜索。order_search:后台订单检索。ticket_search:工单检索。app_log-2026.07.01:日志索引。
Index 不等于数据库表。数据库表按事务存储设计,ES Index 按搜索场景设计。
Document
Document 是 ES 中的一条 JSON 数据。
例如一件商品可以是一条 document:
{
"id": 1001,
"productName": "无线蓝牙降噪耳机 Pro",
"brandName": "SoundMax",
"price": 29900,
"stockStatus": "IN_STOCK"
}这条文档可能由商品表、品牌表、类目表、库存表、销量统计表组合而来。ES 更推荐把搜索需要的字段提前冗余进文档,而不是查询时临时 Join。
Field
Field 是文档里的字段,例如:
productNamebrandNamecategoryIdpricestockStatusupdatedAt
不同字段的搜索目标不同,所以字段类型也不同。商品名要分词,品牌名要精确过滤,价格要范围查询,时间要排序。
Mapping
Mapping 是字段映射,决定字段:
- 用什么类型。
- 是否分词。
- 是否能排序。
- 是否能聚合。
- 用哪个 analyzer。
Mapping 设计错了,后期经常不能直接改,需要新建索引并重建数据。
ES 是怎么完成搜索的
flowchart TD
A["原始业务数据"] --> B["组装搜索文档"]
B --> C["根据 Mapping 处理字段"]
C --> D["text 字段分词"]
D --> E["建立倒排索引"]
F["用户输入关键词"] --> G["查询分词"]
G --> H["匹配倒排索引"]
H --> I["结构化条件过滤"]
I --> J["相关性排序"]
J --> K["返回结果"]写入和查询是配套的。写入时怎么分词,查询时就要用兼容的分词方式;字段写入时是什么类型,查询时就要使用匹配的查询语义。
文档写入后的内部结构
ES 中的一条 JSON 文档不会原样用于全文检索。它会被拆成多个字段,每个字段根据 Mapping 决定如何索引。
flowchart TD
A["商品 JSON 文档"] --> B["productName"]
A --> C["brandName"]
A --> D["price"]
A --> E["stockStatus"]
B --> F["text: 分词"]
C --> G["keyword: 整体值"]
D --> H["number: 数值索引"]
E --> I["keyword: 整体值"]
F --> J["倒排索引"]
G --> K["精确值索引"]
H --> L["范围和排序结构"]
I --> K例如:
{
"productName": "无线蓝牙降噪耳机 Pro",
"stockStatus": "IN_STOCK"
}productName 可能会被拆成:
无线 -> doc1001
蓝牙 -> doc1001
降噪 -> doc1001
耳机 -> doc1001
pro -> doc1001而 stockStatus 通常作为整体值写入:
IN_STOCK -> doc1001这就是为什么 productName 适合 match,stockStatus 适合 term。
什么是倒排索引
倒排索引是理解 ES 的关键。
正排思路
先看文档,再看文档里有哪些词。
doc1001 -> 无线, 蓝牙, 降噪, 耳机
doc1002 -> 有线, 游戏, 耳机
doc1003 -> 蓝牙, 音箱倒排思路
先看词,再看这个词出现在哪些文档里。
蓝牙 -> doc1001, doc1003
耳机 -> doc1001, doc1002
降噪 -> doc1001搜索“蓝牙耳机”时,ES 可以快速找到 蓝牙 和 耳机 对应的文档集合,再计算哪些文档更相关。
倒排索引里通常存什么
倒排索引不只是“词 -> 文档 ID”。为了支持相关性排序、短语查询和高亮,它还可能保存更多信息:
| 信息 | 用途 |
|---|---|
| term | 被索引的词 |
| document id | 哪些文档包含这个词 |
| term frequency | 词在文档中出现几次 |
| position | 词在文本中的位置,支持短语查询 |
| offset | 词在原文中的字符位置,支持高亮 |
这也是为什么 ES 比普通 like 更适合全文搜索:它不是临时扫描字符串,而是提前为搜索准备了结构化索引。
Segment、Refresh、Flush、Merge
ES 是近实时搜索,不是每次写入都马上变成永久索引文件。
flowchart TD
A["写入文档"] --> B["内存缓冲区"]
A --> C["translog 事务日志"]
B --> D["refresh"]
D --> E["生成可搜索 segment"]
E --> F["多个小 segment"]
F --> G["merge 合并成较大 segment"]
C --> H["flush 持久化并截断 translog"]| 概念 | 作用 | 初学者怎么理解 |
|---|---|---|
| refresh | 把内存中的数据变成可搜索 segment | 写入后多久能搜到 |
| segment | Lucene 的不可变索引文件 | 搜索真正读取的文件单元 |
| translog | 写入日志,用于故障恢复 | 防止还没刷盘就宕机导致数据丢失 |
| flush | 持久化并清理旧 translog | 做一次更完整的落盘整理 |
| merge | 合并多个小 segment | 减少小文件,提高查询效率 |
为什么说 segment 不可变:已经写入的 Lucene 段文件不会原地修改。更新文档时,本质上是标记旧文档删除,再写入新文档。删除也不是马上物理删除,而是先打删除标记,后续 merge 时再真正清理。
这解释了几个线上现象:
| 现象 | 原因 |
|---|---|
| 写入后短时间搜不到 | refresh 还没发生 |
| 更新文档也会增加磁盘压力 | 更新是删除旧文档再写新文档 |
| 大量小批量写入影响性能 | 频繁 refresh 产生很多小 segment |
| 删除后磁盘不立刻下降 | 需要等待 merge |
| 写入高峰查询变慢 | merge、refresh、索引写入都会消耗资源 |
分词为什么重要
用户搜索的通常不是数据库中的完整字段,而是自然语言片段。
例如商品名:
Apple iPhone 15 Pro Max 256G 官方旗舰用户可能搜索:
苹果手机 15pro 256如果没有分词、大小写归一、同义词和型号词处理,系统可能搜不到或排序很差。
ES 更像搜索引擎,不是关系型数据库替代品
ES 适合:
- 搜索。
- 聚合分析。
- 海量文本检索。
- 近实时日志查询。
ES 不适合直接替代事务型数据库。
| 原因 | 解释 |
|---|---|
| 事务能力不同 | ES 不提供关系型数据库那种多行强事务 |
| 更新成本不同 | ES 更新文档是删除旧版本再写新版本 |
| Join 能力有限 | ES 更推荐反范式文档模型 |
| 近实时特性 | 写入后受 refresh 影响,不一定立即可搜索 |
| 建模目标不同 | ES 为搜索和分析优化,不为复杂事务建模优化 |
正确做法是让数据库作为事实来源,ES 作为搜索副本。这样即使 ES 数据异常,也可以从数据库重建。
数据为什么常常要同步到 ES
真实数据通常先存 MySQL,再同步一份到 ES 建立搜索索引。
flowchart TD
A["业务操作<br/>改价、上下架、创建订单"] --> B["写入 MySQL"]
B --> C["发送 MQ 或写 Binlog"]
C --> D["同步服务消费变更"]
D --> E["组装 ES 文档"]
E --> F["写入 Elasticsearch"]
G["用户搜索"] --> F如果直接把 ES 当主库,会把交易一致性、搜索性能、索引变更、数据恢复混在一起,系统会非常难维护。
商业系统常见字段
商品搜索:
idproductNamesubTitlebrandIdbrandNamecategoryIdcategoryNametagspricestockStatussaleCountupdatedAt
订单检索:
orderIdbuyerIdbuyerMobileSuffixproductNameorderStatuspayAmountcreatedAt
日志检索:
traceIdservicelevelpathmessagecostMs@timestamp
常用 REST API Demo
查看集群状态:
GET /_cluster/health?pretty查看索引:
GET /_cat/indices?v查看 Mapping:
GET /product_search/_mapping查看单条文档:
GET /product_search/_doc/1001按 ID 批量查询:
GET /product_search/_mget
{
"ids": ["1001", "1002", "1003"]
}批量写入:
POST /_bulk
{ "index": { "_index": "product_search", "_id": "1001" } }
{ "id": 1001, "productName": "无线蓝牙耳机", "stockStatus": "IN_STOCK" }
{ "index": { "_index": "product_search", "_id": "1002" } }
{ "id": 1002, "productName": "有线游戏耳机", "stockStatus": "IN_STOCK" }Bulk 的每一行都是一段 JSON,最后也要保留换行。批量写入适合初始化索引、同步历史数据和高吞吐写入。
本章小结
Elasticsearch 的核心思路是:
把文本提前拆词并建立倒排索引,让搜索时不再扫整库,而是直接根据词快速定位结果。
理解了文档模型、倒排索引、Mapping、分词、refresh 和 segment,后面的 Query DSL、聚合、搜索实战和线上排查就能真正串起来。
