Elasticsearch 底层原理
学习 ES 不能只停留在会写 match、term、bool。真正能排查线上问题,必须知道 ES 为什么查得快、为什么写入后不是马上可见、为什么更新成本比数据库高、为什么深分页慢、为什么相关性排序会受字段长度和词频影响。
一句话理解:
ES 是把业务文档提前加工成适合搜索的数据结构,查询时不再逐条扫描,而是通过倒排索引、列式 doc values、分片并行和相关性评分快速找到结果。
学习目标
| 目标 | 需要掌握什么 |
|---|---|
| 知道为什么快 | 倒排索引、跳表、缓存、doc values、分片并行 |
| 知道为什么近实时 | refresh、segment、translog、flush 的关系 |
| 知道写入怎么工作 | 路由、主分片、副本、内存 buffer、segment、merge |
| 知道查询怎么工作 | query phase、fetch phase、协调节点合并 TopN |
| 知道评分怎么来 | TF、IDF、字段长度、BM25、boost、filter 不评分 |
| 知道更新为什么贵 | Lucene segment 不可变,更新是删除旧文档再写新文档 |
| 会验证原理 | 使用 _analyze、_explain、profile、slowlog 排查 |
ES 为什么查询快
数据库里如果用:
select * from product where product_name like '%降噪耳机%';当数据量很大时,普通 B+Tree 索引很难高效支持前后都有 % 的模糊搜索,往往要扫描大量记录。ES 的思路不一样:写入时就把文本拆成词,并建立“词到文档”的映射。
flowchart TD
A["商品文档"] --> B["分词"]
B --> C["降噪"]
B --> D["耳机"]
C --> E["倒排索引:降噪 -> 文档列表"]
D --> F["倒排索引:耳机 -> 文档列表"]
G["用户搜索 降噪耳机"] --> H["查询分词"]
H --> I["查倒排索引"]
I --> J["合并候选文档"]
J --> K["评分和排序"]ES 快主要来自这些点:
| 能力 | 原理 | 解决什么问题 |
|---|---|---|
| 倒排索引 | 词到文档列表 | 全文搜索不扫全表 |
| 分词 | 把文本拆成 token | 中文、英文、型号词能被召回 |
| postings list | 每个词对应有序文档列表 | 快速求交集、并集和过滤 |
| skip data | 文档列表中跳跃查找 | 跳过不可能匹配的大段文档 |
| doc values | 面向列的字段存储 | 排序、聚合、脚本读取更高效 |
| filter cache | 过滤条件可缓存 | 状态、类目、权限范围重复过滤更快 |
| 分片并行 | 多个分片同时查询 | 数据量大时利用多节点 CPU 和 IO |
注意:ES 查询快不是因为它“魔法般比数据库高级”,而是它为了搜索提前付出了写入、存储、同步和维护成本。
倒排索引内部有什么
倒排索引不只是:
词 -> 文档 ID 列表为了支持评分、短语搜索、高亮,它通常还会保存更多信息。
| 信息 | 作用 |
|---|---|
| term | 分词后的词 |
| doc id | 包含这个词的文档编号 |
| term frequency | 词在文档里出现多少次 |
| position | 词在文本中的位置,支持短语查询 |
| offset | 词在原文中的字符位置,支持高亮 |
| norm | 字段长度等归一化信息,影响评分 |
举个简化例子:
| 文档 ID | productName |
|---|---|
| 1 | 无线蓝牙降噪耳机 |
| 2 | 入耳式蓝牙耳机 |
| 3 | 机械键盘 |
倒排后可以理解成:
蓝牙 -> [1, 2]
耳机 -> [1, 2]
降噪 -> [1]
机械 -> [3]
键盘 -> [3]用户搜“蓝牙降噪耳机”,ES 不需要逐条看所有商品名,而是先找到这些词对应的文档列表,再合并、评分、排序。
为什么 ES 适合搜索但不适合当主库
ES 为了搜索做了很多反数据库设计。
| 对比项 | MySQL | Elasticsearch |
|---|---|---|
| 主要目标 | 事务、关系、精确查询 | 搜索、聚合、近实时分析 |
| 数据模型 | 表、行、列、外键或关联 | JSON 文档 |
| 查询方式 | SQL、Join、事务 | Query DSL、倒排索引 |
| 一致性 | 强事务能力更强 | 搜索视图通常最终一致 |
| 更新方式 | 原地更新行数据 | 删除旧文档标记,再写新文档 |
| 复杂 Join | 擅长 | 不擅长,推荐写入前冗余 |
商业系统里更稳的架构是:
flowchart TD
A["业务写入"] --> B["MySQL 保存事实数据"]
B --> C["MQ / CDC / 补偿任务"]
C --> D["ES 保存搜索视图"]
E["交易判断"] --> B
F["搜索和聚合"] --> D库存、支付、订单状态、账户余额必须以数据库或业务服务为准。ES 适合回答“哪些文档符合搜索条件”,不适合负责强一致业务决策。
写入流程
一条文档写入 ES,不是简单放进一个文件。
flowchart TD
A["客户端写入文档"] --> B["协调节点接收"]
B --> C["根据 _id 或 routing 计算分片"]
C --> D["转发到主分片"]
D --> E["写入内存 buffer"]
D --> F["写入 translog"]
D --> G["复制到副本分片"]
G --> H["达到确认条件"]
H --> I["返回写入成功"]
E --> J["refresh 生成可搜索 segment"]
J --> K["后续 merge 合并小 segment"]关键点:
- 写入先到主分片,再复制到副本。
- translog 用于故障恢复。
- 写入成功不等于立刻能搜到。
- refresh 后生成新的可搜索 segment。
- segment 是不可变的,小 segment 后续会 merge。
为什么叫近实时搜索
ES 常被称为 Near Real-Time Search,近实时搜索。原因是:写入成功后,文档通常不会马上出现在搜索结果里,需要等 refresh。
flowchart TD
A["写入文档成功"] --> B["进入内存 buffer 和 translog"]
B --> C{"是否 refresh"}
C -- "未 refresh" --> D["搜索不到新文档"]
C -- "已 refresh" --> E["生成 segment"]
E --> F["搜索可见"]默认情况下,常见配置会每隔一段时间自动 refresh。这个间隔越短,文档越快可见,但系统开销越大。
| 配置 | 效果 | 代价 |
|---|---|---|
refresh_interval: 1s | 写入后通常 1 秒左右可见 | 适合普通搜索 |
| 调大 refresh | 写入吞吐更好 | 新数据可见更慢 |
| 手动 refresh | 立刻可见 | 开销大,不适合高频调用 |
| 批量导入时关闭或调大 refresh | 提升导入速度 | 导入期间不适合实时搜索 |
所以商品改价、上下架、订单状态同步到 ES 后,搜索页短时间看到旧数据是可能的。业务上要设计最终一致和补偿,而不是要求 ES 当强一致数据库。
refresh、flush、merge 的区别
这三个词很容易混。
| 概念 | 做什么 | 解决什么问题 |
|---|---|---|
| refresh | 让内存中的新数据生成可搜索 segment | 新文档什么时候能被搜索到 |
| flush | 提交 Lucene commit,清理 translog | 故障恢复和持久化边界 |
| merge | 把多个小 segment 合并成大 segment | 减少小文件,提高查询效率,清理删除标记 |
flowchart TD
A["写入数据"] --> B["内存 buffer"]
A --> C["translog"]
B --> D["refresh"]
D --> E["新的 segment 可搜索"]
E --> F["多个小 segment"]
F --> G["merge 合并"]
C --> H["flush 后 translog 可清理"]如果写入量很大但 refresh 太频繁,会产生大量小 segment,查询和 merge 压力都会上升。
更新和删除为什么成本高
Lucene segment 是不可变的。不可变的好处是查询时结构稳定、缓存友好;代价是更新不能像数据库那样直接原地改。
更新文档可以理解成:
flowchart TD
A["更新文档 1001"] --> B["旧文档打删除标记"]
B --> C["写入新版本文档"]
C --> D["refresh 后新版本可见"]
D --> E["merge 时真正清理旧文档"]所以:
- 高频更新会产生更多删除标记和 merge 压力。
- 大字段文档局部更新,本质仍要重新索引文档。
- ES 不适合做高频强一致状态表。
- 商品、订单搜索可以同步变化,但事实仍应以主库为准。
查询流程:Query Phase 和 Fetch Phase
分布式查询通常分两阶段。
flowchart TD
A["客户端查询"] --> B["协调节点"]
B --> C["Query Phase"]
C --> D["每个分片本地查 TopN 文档ID和分数"]
D --> E["协调节点合并全局 TopN"]
E --> F["Fetch Phase"]
F --> G["到对应分片取 _source"]
G --> H["返回最终结果"]Query Phase 负责找候选和评分,Fetch Phase 负责取原文档内容。深分页慢的原因就在这里:如果 from=10000&size=20,每个分片都要先拿出更多候选,协调节点合并后再丢弃前面大量结果。
filter 为什么通常更快
must 和 filter 都能限制结果,但语义不同:
| 子句 | 是否影响 _score | 常见用途 |
|---|---|---|
must | 影响 | 关键词全文匹配 |
should | 影响 | 加权、召回扩展 |
filter | 不影响 | 状态、类目、品牌、权限、时间范围 |
must_not | 不影响或弱相关 | 排除条件 |
过滤条件不需要计算相关性分数,并且相同过滤条件可能被缓存。因此品牌、类目、库存状态、租户权限、时间范围这类硬条件,应该尽量放在 filter。
相关性评分:为什么有的结果排前面
ES 常见评分模型是 BM25。零基础可以先这样理解:
一个文档越多次命中用户搜索词,且这些词越稀有、字段越聚焦,它的相关性分数通常越高。
影响评分的常见因素:
| 因素 | 含义 | 例子 |
|---|---|---|
| TF | 词在当前文档出现次数 | 商品名多次出现“降噪” |
| IDF | 词在全局是否稀有 | “降噪”比“商品”更有区分度 |
| 字段长度 | 字段越短且命中越集中,通常越相关 | 标题命中比长详情命中更强 |
| boost | 人工提高字段或条件权重 | productName^5 |
| filter | 只过滤不评分 | stockStatus=IN_STOCK |
GET /product_search/_search
{
"query": {
"multi_match": {
"query": "无线降噪耳机",
"fields": ["productName^5", "subTitle^2", "tags"]
}
}
}这里 productName^5 表示商品标题命中更重要。商业搜索里通常还会结合销量、库存、运营标签、新品权重等业务规则。
_explain:看一条文档为什么被命中
当你觉得“这个结果为什么排这么前”时,可以用 _explain。
GET /product_search/_explain/10001
{
"query": {
"match": {
"productName": "无线降噪耳机"
}
}
}它会返回该文档如何匹配、每部分分数怎么来的。生产排查时不要对大批量请求都开 _explain,它适合定位单条异常结果。
profile:看查询慢在哪里
profile 可以分析查询 DSL 每个部分的耗时。
GET /product_search/_search
{
"profile": true,
"query": {
"bool": {
"must": [
{ "match": { "productName": "无线降噪耳机" } }
],
"filter": [
{ "term": { "stockStatus": "IN_STOCK" } }
]
}
}
}profile 本身会增加开销,适合测试和故障定位,不适合长期对线上接口开启。
doc values 为什么重要
倒排索引适合“根据词找文档”,但排序和聚合经常需要“按文档读取某个字段的值”。这时 doc values 更合适。
| 数据结构 | 访问方向 | 适合 |
|---|---|---|
| 倒排索引 | term -> docs | 全文检索、精确过滤 |
| doc values | doc -> field value | 排序、聚合、脚本读取 |
这也是为什么聚合、排序字段通常要用 keyword、数值、日期等类型,而不是直接对 text 字段聚合。
常见误区
| 误区 | 正确理解 |
|---|---|
| ES 写入成功就一定马上搜到 | 要等 refresh 后才可搜索 |
| 更新只是改一个字段 | 底层接近删除旧文档再写新文档 |
| 分片越多查询越快 | 分片过多会增加协调、内存和恢复成本 |
_score 是固定不变的 | 评分受查询、字段、分词、全局统计、boost 影响 |
| filter 和 must 没区别 | filter 不评分,更适合硬过滤条件 |
| ES 快所以可以替代 MySQL | ES 是搜索视图,不是事务事实源 |
关联知识点
| 知识点 | 继续学习什么 |
|---|---|
| 核心概念 | 文档、Mapping、倒排索引、refresh、segment |
| Mapping 与查询 | 字段类型和 Query DSL 如何配合 |
| 分词 | 中文分词、索引分词、查询分词 |
| 集群 | 分片、副本、协调节点、分片分配 |
| 性能优化与排查 | 慢查询、写入慢、堆内存、磁盘水位 |
小结
ES 查询快,是因为它在写入时提前分词、建立倒排索引和列式 doc values,查询时通过分片并行、过滤缓存、评分排序快速得到 TopN。它不是强一致事务库,而是近实时搜索引擎。理解 refresh、segment、translog、merge、query/fetch、BM25 和 doc values,才能真正解释 ES 为什么快、为什么有延迟、为什么更新贵、为什么深分页慢。
