高 基础
ES架构与核心概念#
一句话答案#
ES 是基于 Lucene 的分布式搜索引擎,核心概念:Index(类似数据库表)→ Shard(分片,Lucene 实例)→ Document(JSON 文档);通过倒排索引实现全文检索,通过分片+副本实现水平扩展和高可用。
核心要点
核心概念类比#
| ES 概念 | MySQL 类比 | 说明 |
|---|---|---|
| Index | Table | 一类文档的集合 |
| Document | Row | 一条 JSON 数据 |
| Field | Column | 文档中的字段 |
| Mapping | Schema | 字段类型定义 |
| Shard | Partition | 数据分片 |
| Replica | Slave | 分片副本 |
架构分层#
┌─────────────────────────────────────────┐
│ ES Cluster │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐│
│ │ Node 1 │ │ Node 2 │ │ Node 3 ││
│ │(Master) │ │(Data) │ │(Data) ││
│ │ P0 R1 │ │ P1 R2 │ │ P2 R0 ││
│ └──────────┘ └──────────┘ └──────────┘│
└─────────────────────────────────────────┘
P = Primary Shard, R = Replica Shardplaintext节点角色#
| 角色 | 职责 | 配置建议 |
|---|---|---|
| Master | 集群元数据管理、分片分配 | 3 个专用 Master 节点 |
| Data | 存储数据、执行查询 | 高磁盘/内存 |
| Coordinating | 请求路由、结果聚合 | 高 CPU |
| Ingest | 数据预处理(pipeline) | 可选 |
写入流程#
1. 客户端发请求到任意节点(Coordinating Node)
2. 路由计算: shard = hash(doc._id) % number_of_shards
3. 请求转发到对应 Primary Shard 所在节点
4. Primary 写入成功后,并行复制到所有 Replica
5. 所有 Replica 确认后返回客户端成功
6. 文档先写入 In-Memory Buffer → refresh(1s) → Segment(可搜索)
7. Segment → flush → 持久化到磁盘plaintext查询流程(Query-then-Fetch)#
1. Coordinating Node 收到查询请求
2. Query Phase: 广播到所有相关 Shard
- 每个 Shard 本地查询,返回 Top-N 的 doc_id + score
3. Fetch Phase: Coordinating Node 汇总排序
- 取全局 Top-N 的 doc_id
- 去对应 Shard 获取完整文档内容
4. 返回结果给客户端plaintext为什么要拆成两阶段(深挖):
- Query 阶段只回元数据:每个分片本地查询后,只返回 Top-N 的 文档 id + 排序值(score 或 sort 字段),不返回
_source。协调节点据此做全局归并排序,选出真正的全局 Top-N。 - Fetch 阶段才取正文:协调节点拿到全局 Top-N 的 id 后,只去对应分片按 id 拉取这几条的完整
_source。 - 核心动机:避免网络放大。若一阶段就让每个分片把 Top-N 的全部
_source都传回,假设 N=10、5 个分片,就要传 50 份完整文档,而最终只要 10 份——大量正文在网络上传了又被丢弃。两阶段把”排序”和”取正文”解耦,只在最后对真正需要的几条取_source,传输量从分片数×N×文档大小降到N×文档大小。文档越大、分片越多,省得越多。
深分页(from+size)为什么爆炸:
查 from=10000, size=10(要第 1 万条后的 10 条):
每个分片都必须返回 from+size = 10010 条 doc_id+score
(因为分片不知道自己的第 10001 条会不会进全局 Top)
协调节点要在内存里归并 分片数 × (from+size) 条 = 5 × 10010 ≈ 5 万条
→ from 越深,每个分片回传越多、协调节点排序内存越大 → O(分片数×(from+size))plaintext- ES 默认
index.max_result_window = 10000就是为了拦住深分页。 - 替代方案:
- search_after:用上一页最后一条的排序值做游标,每个分片只需返回 size 条(不再是 from+size),翻页代价恒定,适合无状态深翻页/导出。
- scroll:生成快照游标,适合一次性全量遍历(如 reindex),但占资源且看不到快照后的新数据,不适合实时分页。
dfs_query_then_fetch(小数据集打分失真):
- 默认
query_then_fetch下,每个分片用自己本地的文档统计独立算 IDF(IDF 依赖 N=文档总数、n=含该词文档数)。数据量大时各分片统计接近、影响可忽略;但数据量小或分布不均时,同一个词在不同分片的 IDF 差很多,导致相同文档因落在不同分片而打分不一致,排序失真。 dfs_query_then_fetch在 Query 前多加一个 DFS(Distributed Frequency Search) 阶段:先收集全局的词频/文档频率统计,再用统一的全局 IDF 打分,结果更准。代价是多一轮网络往返,一般只在小索引或测试时用。
近实时搜索(NRT)#
写入 → Buffer → refresh(1s) → 新 Segment → 可搜索
↓
flush(30min) → 持久化磁盘
- refresh 默认 1s:写入后约 1s 可搜到(非实时)
- translog:crash 恢复用,类似 MySQL redo logplaintext面试回答(2分钟版)
ES 是基于 Lucene 的分布式搜索引擎。核心概念:Index 相当于 MySQL 的表,Document 是一条 JSON 记录,Shard 是数据分片(每个 Shard 是独立的 Lucene 实例)。架构上分 Master 节点管元数据和分片分配、Data 节点存数据执行查询、Coordinating 节点路由请求和聚合结果。写入时按 hash(doc_id) % shard_num 路由到 Primary Shard,写完后并行复制到 Replica。查询采用 Query-then-Fetch 两阶段:先广播到所有 Shard 各自查 Top-N 返回 doc_id+score,Coordinating 节点汇总全局排序后再去取完整文档。ES 是近实时的:文档写入后经过 refresh(默认 1 秒)才可搜索,不是强实时。通过 translog 保证 crash 恢复不丢数据,类似 MySQL 的 redo log。
追问与易错
追问方向:
- “ES 和 MySQL 的区别?”→ ES 擅长全文检索和聚合分析,MySQL 擅长事务和关联查询
- “为什么 ES 查询快?”→ 倒排索引 + 内存缓存 + 并行分片查询
- “refresh 和 flush 区别?”→ refresh 写入内存 Segment 变可搜索,flush 持久化到磁盘
- “分片数能改吗?”→ 创建后不能改(需要 reindex),所以要提前规划
- “为什么查询要分 Query 和 Fetch 两阶段?”→ Query 阶段只回 id+排序值做全局排序,Fetch 阶段才取正文,避免每个分片回传全部 _source 的网络放大
- “深分页为什么慢?怎么解决?”→ from+size 下每个分片要回 from+size 条、协调节点归并 分片数×(from+size) 条;用 search_after(游标翻页)或 scroll(全量遍历)替代
易错点:
- ❌ “ES 是实时的”——是近实时(NRT),有 1s refresh 延迟
- ❌ “ES 可以替代 MySQL”——ES 不支持事务、JOIN、强一致,不是关系数据库
- ❌ “分片越多越好”——每个分片有开销,建议单个 shard 10-50GB