1. ElasticSearch是什么?
ElasticSearch(简称 ES)是一个基于 Lucene 的分布式搜索和分析引擎。由 Elastic 公司开发维护,于 2010 年首次发布,目前已成为最流行的企业级搜索引擎。
1.1 核心特点
- 倒排索引:基于 Lucene 的倒排索引,实现高效的全文搜索
- 分布式架构:天然支持数据分片和副本,可水平扩展
- 近实时搜索:写入数据约 1 秒后即可被搜索到
- RESTful API:通过 HTTP + JSON 进行交互,使用门槛低
- Schema Free:支持动态映射,灵活的文档结构
- 丰富的查询 DSL:支持全文搜索、结构化搜索、聚合分析
1.2 核心组件
@startuml
skinparam backgroundColor transparent
skinparam componentStyle rectangle
package "ElasticSearch 集群" {
[Cluster\n集群] as Cluster
[Node\n节点] as Node
[Index\n索引] as Index
[Shard\n分片] as Shard
[Segment\n段] as Segment
}
package "Lucene 层" {
[Inverted Index\n倒排索引] as InvertedIndex
[Doc Values\n列式存储] as DocValues
[Stored Fields\n原始文档] as StoredFields
}
package "节点角色" {
[Master Node\n主节点] as Master
[Data Node\n数据节点] as Data
[Coordinating Node\n协调节点] as Coord
}
Cluster --> Node : 包含多个
Node --> Index : 存储多个
Index --> Shard : 分成多个
Shard --> Segment : 包含多个
Segment --> InvertedIndex
Segment --> DocValues
Segment --> StoredFields
Node --> Master : 可以是
Node --> Data : 可以是
Node --> Coord : 可以是
note right of Cluster
**集群**
多个节点组成
共享集群名称
end note
note right of Shard
**分片**
Primary: 主分片(写入)
Replica: 副本分片(读取+容灾)
end note
note right of InvertedIndex
**核心数据结构**
Term → DocId 列表
支持高效全文搜索
end note
@enduml正在加载图表...
![PlantUML Diagram]()
组件说明
| 组件 | 说明 | 类比 |
|---|
| Cluster | 集群,多个节点的集合 | 数据库集群 |
| Node | 节点,一个 ES 实例 | 数据库实例 |
| Index | 索引,文档的集合 | 数据库表 |
| Shard | 分片,索引的子集 | 分区/分表 |
| Segment | 段,Lucene 的存储单元 | 数据文件 |
| Document | 文档,一条数据记录 | 表中的一行 |
1.3 适用场景
✅ 适用场景:
- 全文搜索(站内搜索、商品搜索)
- 日志分析(ELK Stack)
- 实时数据分析和可视化
- 应用性能监控(APM)
- 安全分析(SIEM)
- 地理位置搜索
❌ 不适用场景:
- 事务性操作(OLTP)
- 频繁的更新操作
- 强一致性要求的场景
- 复杂的关联查询(Join)
- 作为主数据存储(建议配合 MySQL 等使用)
1.4 与其他系统对比
| 特性 | ElasticSearch | MySQL | ClickHouse | MongoDB |
|---|
| 存储方式 | 倒排索引+行列混合 | 行式 | 列式 | 文档(BSON) |
| 搜索能力 | 极强(全文搜索) | 弱 | 中等 | 中等 |
| 分析能力 | 强(聚合) | 中等 | 极强 | 中等 |
| 写入性能 | 中等 | 高 | 极高 | 高 |
| 更新性能 | 弱 | 高 | 弱 | 高 |
| 事务支持 | 无 | 完整 | 无 | 部分 |
| 分布式 | 原生支持 | 需中间件 | 原生支持 | 原生支持 |
2. ElasticSearch解决什么问题?
2.1 传统数据库的搜索局限
在传统关系型数据库中,全文搜索存在以下问题:
LIKE 查询效率低:
WHERE content LIKE '%关键词%' 无法使用索引- 全表扫描,性能随数据量线性下降
- 百万级数据查询可能需要数秒
不支持分词:
- 中文 “我爱北京天安门” 无法按词搜索
- 无法处理同义词、近义词
- 无法进行相关性排序
扩展性差:
- 单机性能瓶颈
- 分库分表后全文搜索更加困难
- 跨库搜索需要在应用层聚合
2.2 搜索场景的特点
- 读多写少:搜索请求远多于写入请求
- 模糊匹配:用户输入不精确,需要容错
- 相关性排序:返回最匹配的结果,而非全部结果
- 高并发:需要支持大量并发搜索请求
- 低延迟:用户期望毫秒级响应
2.3 核心需求
- 高效的全文搜索:毫秒级返回搜索结果
- 灵活的分词能力:支持中英文分词、同义词等
- 相关性评分:返回最相关的文档
- 水平扩展:支持 PB 级数据存储和搜索
- 高可用性:节点故障不影响服务
3. 怎么解决的?
ElasticSearch 通过以下核心机制解决上述问题:
3.1 倒排索引
核心思想:建立词条(Term)到文档(Document)的映射,而非文档到词条的映射。
@startuml
skinparam backgroundColor transparent
skinparam rectangleBackgroundColor LightYellow
skinparam rectangleBorderColor Black
rectangle "原始文档" as Docs {
rectangle "Doc1: 我爱北京天安门" as D1
rectangle "Doc2: 北京欢迎你" as D2
rectangle "Doc3: 天安门广场" as D3
}
note bottom of Docs
**传统查询**
遍历每个文档
检查是否包含关键词
O(n) 复杂度
end note
rectangle "倒排索引" as InvertedIndex {
rectangle "我 → [Doc1]" as T1
rectangle "爱 → [Doc1]" as T2
rectangle "北京 → [Doc1, Doc2]" as T3 #LightGreen
rectangle "天安门 → [Doc1, Doc3]" as T4 #LightGreen
rectangle "欢迎 → [Doc2]" as T5
rectangle "广场 → [Doc3]" as T6
}
note bottom of InvertedIndex
**倒排查询**
直接定位包含词条的文档
O(1) 复杂度 ✓
end note
Docs -down[hidden]-> InvertedIndex
@enduml正在加载图表...
![PlantUML Diagram]()
倒排索引的组成:
- Term Dictionary:词条字典,存储所有词条,支持快速查找
- Posting List:倒排列表,存储包含该词条的文档 ID 列表
- Term Frequency:词频,词条在文档中出现的次数
- Position:位置信息,用于短语查询
3.2 分布式架构
- 数据分片:将索引拆分为多个 Shard,分布到不同节点
- 副本机制:每个 Shard 有多个 Replica,保证高可用
- 自动路由:根据文档 ID 自动路由到对应分片
- 并行查询:查询时并行搜索所有分片,合并结果
3.3 分段存储(Segment)
- 数据写入时先进入内存缓冲区(Buffer)
- 定期刷新为不可变的 Segment 文件
- Segment 不可修改,删除和更新通过标记实现
- 后台合并小 Segment 为大 Segment
3.4 近实时搜索
- Refresh:每秒将内存缓冲区数据刷新到文件系统缓存,生成新 Segment
- Flush:将文件系统缓存的数据持久化到磁盘
- Translog:事务日志,保证数据不丢失
4. 核心流程
4.0 ES 整体工作流程
@startuml
skinparam backgroundColor transparent
skinparam sequenceArrowThickness 2
skinparam roundcorner 20
skinparam maxmessagesize 250
actor "客户端" as Client
participant "Coordinating Node\n协调节点" as Coord
participant "Primary Shard\n主分片" as Primary
participant "Replica Shard\n副本分片" as Replica
participant "Lucene\n存储引擎" as Lucene
database "Segment\n段文件" as Segment
== 写入流程 ==
Client -> Coord: 写入请求
activate Coord
Coord -> Coord: 路由计算\nshard = hash(doc_id) % num_shards
Coord -> Primary: 转发到主分片
deactivate Coord
activate Primary
Primary -> Lucene: 写入 Buffer
activate Lucene
Lucene -> Lucene: 1. 分词处理\n2. 构建倒排索引\n3. 写入内存
Lucene --> Primary: 写入成功
deactivate Lucene
Primary -> Replica: 同步到副本
activate Replica
Replica -> Replica: 写入本地
Replica --> Primary: 同步完成
deactivate Replica
Primary --> Client: 返回成功
deactivate Primary
== Refresh 机制 (每秒) ==
Lucene -> Segment: 生成新 Segment
note right of Segment
**近实时搜索**
数据写入约 1 秒后可搜索
end note
== 查询流程 (Query Then Fetch) ==
Client -> Coord: 查询请求
activate Coord
Coord -> Primary: Query 阶段
Coord -> Replica: Query 阶段
activate Primary
activate Replica
Primary -> Primary: 搜索本地 Segment
Primary --> Coord: 返回 DocId + Score
deactivate Primary
Replica -> Replica: 搜索本地 Segment
Replica --> Coord: 返回 DocId + Score
deactivate Replica
Coord -> Coord: 合并排序\n取 Top N 文档
Coord -> Primary: Fetch 阶段(获取文档内容)
activate Primary
Primary --> Coord: 返回文档
deactivate Primary
Coord --> Client: 返回结果
deactivate Coord
@enduml
正在加载图表...
![PlantUML Diagram]()
流程说明
写入流程:
- 协调节点接收请求,计算路由确定目标分片
- 主分片写入数据,同步到副本分片
- 数据先写入内存 Buffer,定期 Refresh 生成 Segment
查询流程(Query Then Fetch):
- Query 阶段:并行查询所有分片,返回匹配文档的 ID 和评分
- Fetch 阶段:根据排序后的 Top N 文档 ID,获取完整文档内容
4.1 底层存储结构
Lucene 索引结构
@startuml
skinparam backgroundColor transparent
skinparam classAttributeIconSize 0
class "Index\n索引" as Index {
+ index_name: String
+ settings: Settings
+ mappings: Mappings
+ shards: Shard[]
}
class "Shard\n分片" as Shard {
+ shard_id: int
+ is_primary: boolean
+ segments: Segment[]
+ translog: Translog
}
class "Segment\n段" as Segment {
+ segment_name: String
+ doc_count: int
+ deleted_docs: int
+ files: SegmentFiles
}
class "Segment Files\n段文件" as Files {
+ .tip: Term Index(词条索引)
+ .tim: Term Dictionary(词条字典)
+ .doc: Posting List(倒排列表)
+ .pos: Position(位置信息)
+ .fdt: Stored Fields(原始文档)
+ .dvd: Doc Values(列式存储)
+ .liv: Live Docs(存活文档标记)
}
Index "1" o-- "1..*" Shard : 包含
Shard "1" o-- "0..*" Segment : 包含
Segment "1" o-- "1" Files : 包含
note right of Index
**索引 = 分片的集合**
逻辑概念,类似数据库表
end note
note right of Shard
**分片 = Lucene 索引**
实际的物理存储单元
包含完整的倒排索引
end note
note right of Segment
**Segment = 不可变文件**
写入后不能修改
只能通过 Merge 合并
end note
note bottom of Files
**Lucene 文件格式**
- 倒排索引:.tip + .tim + .doc
- 原始文档:.fdt
- 列式存储:.dvd(聚合/排序)
- 删除标记:.liv
end note
@enduml正在加载图表...
![PlantUML Diagram]()
倒排索引详细结构
倒排索引组成:
├── Term Index (.tip) # FST 结构,前缀树,指向 Term Dictionary
├── Term Dictionary (.tim) # 词条字典,存储所有 Term
└── Posting List (.doc) # 倒排列表
├── DocId List # 文档 ID 列表(差值压缩)
├── Term Frequency # 词频(用于评分)
└── Position (.pos) # 位置信息(用于短语查询)
实际案例:用户表的倒排索引
假设有一个用户索引 users,包含以下 3 条文档:
| _id | name | city | bio |
|---|
| 1 | 张三 | 北京 | 资深Java开发工程师 |
| 2 | 李四 | 上海 | Java架构师,擅长分布式系统 |
| 3 | 王五 | 北京 | Go语言开发,熟悉Java |
ES 会为每个字段建立独立的倒排索引:
@startuml
skinparam backgroundColor transparent
skinparam defaultFontSize 12
rectangle "原始文档 (Stored Fields)" as Docs #LightBlue {
rectangle "Doc1: {name:张三, city:北京, bio:资深Java开发工程师}" as D1
rectangle "Doc2: {name:李四, city:上海, bio:Java架构师,擅长分布式系统}" as D2
rectangle "Doc3: {name:王五, city:北京, bio:Go语言开发,熟悉Java}" as D3
}
rectangle "name 字段倒排索引" as NameIndex #LightYellow {
rectangle "张三 → [Doc1]" as N1
rectangle "李四 → [Doc2]" as N2
rectangle "王五 → [Doc3]" as N3
}
rectangle "city 字段倒排索引" as CityIndex #LightGreen {
rectangle "北京 → [Doc1, Doc3]" as C1
rectangle "上海 → [Doc2]" as C2
}
rectangle "bio 字段倒排索引 (分词后)" as BioIndex #LightPink {
rectangle "java → [Doc1, Doc2, Doc3]" as B1
rectangle "开发 → [Doc1, Doc3]" as B2
rectangle "工程师 → [Doc1]" as B3
rectangle "架构师 → [Doc2]" as B4
rectangle "分布式 → [Doc2]" as B5
rectangle "系统 → [Doc2]" as B6
rectangle "go → [Doc3]" as B7
rectangle "语言 → [Doc3]" as B8
}
Docs -down[hidden]-> NameIndex
NameIndex -right[hidden]-> CityIndex
CityIndex -down[hidden]-> BioIndex
@enduml正在加载图表...
![PlantUML Diagram]()
查询示例:
// 查询:city = "北京" AND bio 包含 "Java"
{
"query": {
"bool": {
"must": [
{ "term": { "city": "北京" } },
{ "match": { "bio": "Java" } }
]
}
}
}
查询执行过程:
- city 字段查找:
北京 → [Doc1, Doc3] - bio 字段查找:
java → [Doc1, Doc2, Doc3] - 求交集:
[Doc1, Doc3] ∩ [Doc1, Doc2, Doc3] = [Doc1, Doc3] - 返回结果:张三、王五
倒排索引文件存储结构:
词条到文档的映射关系通过 三个文件协同工作 完成:
@startuml
skinparam backgroundColor transparent
skinparam defaultFontSize 11
rectangle ".tip 文件 (Term Index)" as TIP #LightBlue {
rectangle "FST 前缀树\n快速定位词条位置" as FST
rectangle "j → offset:1024\ng → offset:2048\n..." as TIPContent
}
rectangle ".tim 文件 (Term Dictionary)" as TIM #LightYellow {
rectangle "存储所有词条 + 指向 Posting 的指针" as TIMDesc
rectangle "offset:1024 → java → postingPtr:100\noffset:1025 → 架构师 → postingPtr:200\noffset:2048 → go → postingPtr:300\n..." as TIMContent
}
rectangle ".doc 文件 (Posting List) ⭐核心映射" as DOC #LightGreen {
rectangle "存储 Term → DocId 的映射关系" as DOCDesc
rectangle "postingPtr:100 → java → [Doc1, Doc2, Doc3]\npostingPtr:200 → 架构师 → [Doc2]\npostingPtr:300 → go → [Doc3]\n..." as DOCContent
}
TIP -down-> TIM : "1. 通过前缀\n定位词条位置"
TIM -down-> DOC : "2. 通过指针\n找到文档列表"
note right of DOC #Pink
**核心答案**
词条 → 文档ID 的映射
存储在 .doc 文件中
(Posting List)
end note
@enduml正在加载图表...
![PlantUML Diagram]()
查询 “java” 的完整过程:
1. 查 .tip 文件:前缀 "j" → 定位到 .tim 文件 offset:1024
2. 查 .tim 文件:offset:1024 找到词条 "java" → postingPtr:100
3. 查 .doc 文件:postingPtr:100 → DocId 列表 [1, 2, 3] ⭐ 这就是映射!
4. 返回结果:包含 "java" 的文档是 Doc1, Doc2, Doc3
各文件职责总结:
| 文件 | 后缀 | 存储内容 | 作用 |
|---|
| Term Index | .tip | 词条前缀的 FST 索引 | 快速定位词条在 .tim 中的位置 |
| Term Dictionary | .tim | 所有词条 + Posting 指针 | 存储词条,指向 .doc |
| Posting List | .doc | Term → DocId 列表 | ⭐ 核心:存储映射关系 |
| Positions | .pos | 词条在文档中的位置 | 支持短语查询 |
| Payloads | .pay | 词频等额外信息 | 用于相关性评分 |
💡 简单记忆:.tip 找词条位置 → .tim 找词条 → .doc 找文档列表
Doc Values(列式存储):
用于 keyword 类型字段的排序和聚合,按列存储:
city 字段 Doc Values:
Doc1 → 北京
Doc2 → 上海
Doc3 → 北京
# 聚合查询 "按城市统计用户数" 时直接遍历此列
# 结果:北京=2, 上海=1
4.2 数据写入流程
@startuml
skinparam backgroundColor transparent
skinparam sequenceArrowThickness 2
skinparam roundcorner 20
participant "Client" as Client
participant "Primary Shard" as Primary
participant "Index Buffer\n内存缓冲区" as Buffer
participant "Translog\n事务日志" as Translog
participant "OS Cache\n文件系统缓存" as Cache
participant "Disk\n磁盘" as Disk
Client -> Primary: 写入文档
activate Primary
Primary -> Buffer: 1. 写入内存 Buffer
activate Buffer
Buffer -> Buffer: 分词、构建倒排索引
deactivate Buffer
Primary -> Translog: 2. 追加 Translog
activate Translog
Translog -> Disk: fsync(默认每请求)
note right
**数据安全保障**
Translog 保证数据不丢失
即使宕机也可恢复
end note
deactivate Translog
Primary --> Client: 返回成功
deactivate Primary
== Refresh(默认每秒) ==
Buffer -> Cache: 生成新 Segment
activate Cache
Cache -> Cache: 写入文件系统缓存
note right
**近实时可搜索**
数据进入 OS Cache
即可被搜索到
end note
deactivate Cache
== Flush(默认 30 分钟或 Translog 达到 512MB) ==
Cache -> Disk: fsync 持久化
activate Disk
Disk -> Disk: Segment 文件落盘
deactivate Disk
Translog -> Translog: 清空 Translog
note right
**Commit Point**
记录已持久化的 Segment
Translog 可安全删除
end note
@enduml
正在加载图表...
![PlantUML Diagram]()
写入流程详解
写入内存 Buffer:
- 文档经过分词器(Analyzer)处理
- 构建内存中的倒排索引
- 此时数据不可搜索
追加 Translog:
- 记录写入操作日志
- 默认每个请求都 fsync 到磁盘
- 保证宕机后数据可恢复
Refresh(默认 1 秒):
- 将内存 Buffer 写入文件系统缓存
- 生成新的 Segment
- 数据变为可搜索状态
- 这是"近实时"的来源
Flush(默认 30 分钟):
- 将文件系统缓存的 Segment 持久化到磁盘
- 清空 Translog
- 触发条件:时间间隔或 Translog 大小超过阈值
4.3 数据查询流程
@startuml
skinparam backgroundColor transparent
skinparam sequenceArrowThickness 2
skinparam roundcorner 20
actor "Client" as Client
participant "Coordinating Node\n协调节点" as Coord
participant "Shard 0" as S0
participant "Shard 1" as S1
participant "Shard 2" as S2
Client -> Coord: 查询: {"query": {"match": {"content": "北京"}}, "from": 0, "size": 10}
activate Coord
== Query 阶段 ==
Coord -> S0: 查询请求
Coord -> S1: 查询请求
Coord -> S2: 查询请求
activate S0
activate S1
activate S2
S0 -> S0: 1. 查询倒排索引\n2. 计算相关性评分\n3. 返回 Top(from+size) 文档
S1 -> S1: 同上
S2 -> S2: 同上
S0 --> Coord: [(doc_id, score), ...]
S1 --> Coord: [(doc_id, score), ...]
S2 --> Coord: [(doc_id, score), ...]
deactivate S0
deactivate S1
deactivate S2
Coord -> Coord: 合并所有分片结果\n全局排序\n取 Top 10
note right of Coord
**Query 阶段结果**
只返回 doc_id 和 score
不返回文档内容
end note
== Fetch 阶段 ==
Coord -> S0: 获取 Doc1, Doc5 的内容
Coord -> S2: 获取 Doc8 的内容
activate S0
activate S2
S0 --> Coord: 返回文档内容
S2 --> Coord: 返回文档内容
deactivate S0
deactivate S2
Coord -> Coord: 组装最终结果
Coord --> Client: 返回搜索结果
deactivate Coord
@enduml正在加载图表...
![PlantUML Diagram]()
查询流程详解
Query 阶段:
- 协调节点将查询广播到所有相关分片
- 每个分片在本地执行查询,返回
from + size 个文档的 ID 和评分 - 协调节点合并所有结果,全局排序,确定最终的 Top N
Fetch 阶段:
- 协调节点根据 Query 阶段的结果,向相关分片请求文档内容
- 只请求最终需要返回的文档(Top N)
- 分片返回完整文档,协调节点组装后返回给客户端
相关性评分:
- 默认使用 BM25 算法(5.x 之前使用 TF-IDF)
- 考虑词频(TF)、逆文档频率(IDF)、文档长度等因素
4.4 Segment Merge 机制
@startuml
skinparam backgroundColor transparent
skinparam sequenceArrowThickness 2
skinparam roundcorner 20
participant "写入操作" as Write
participant "Refresh" as Refresh
participant "Segment 列表" as Segments
participant "Merge 调度器" as Scheduler
participant "Merge 任务" as Task
== 持续写入产生多个 Segment ==
Write -> Refresh: 每秒 Refresh
activate Refresh
Refresh -> Segments: 生成 Segment 1 (10 docs)
Refresh -> Segments: 生成 Segment 2 (10 docs)
Refresh -> Segments: 生成 Segment 3 (10 docs)
Refresh -> Segments: 生成 Segment 4 (10 docs)
deactivate Refresh
note right of Segments
**问题**
Segment 过多会影响查询性能
每次查询需要搜索所有 Segment
end note
== Merge 过程 ==
Scheduler -> Scheduler: 检测到 Segment 数量过多
activate Scheduler
Scheduler -> Task: 创建 Merge 任务
deactivate Scheduler
activate Task
Task -> Segments: 读取 Segment 1, 2, 3, 4
Task -> Task: 1. 合并倒排索引\n2. 移除已删除文档\n3. 重新排序
Task -> Segments: 生成新 Segment (40 docs)
Task -> Segments: 删除旧 Segments
note right of Task
**Merge 效果**
- 减少 Segment 数量
- 物理删除已标记删除的文档
- 优化存储空间
- 提升查询性能
end note
deactivate Task
@enduml
正在加载图表...
![PlantUML Diagram]()
Merge 机制详解
为什么需要 Merge:
- 每次 Refresh 都会生成新 Segment
- Segment 数量过多影响查询性能(需要搜索所有 Segment)
- 删除操作只是标记,不会真正释放空间
Merge 策略:
- Tiered Merge Policy(默认):按大小分层合并
- 小 Segment 优先合并
- 控制单次 Merge 的 Segment 数量和大小
Merge 过程:
- 选择多个小 Segment
- 合并倒排索引和存储的文档
- 物理删除已标记删除的文档
- 生成新的大 Segment
- 原子性替换旧 Segment
Force Merge:
- 手动触发:
POST /index/_forcemerge?max_num_segments=1 - 强制合并为指定数量的 Segment
- 注意:会消耗大量 I/O,建议在低峰期执行
5. ElasticSearch 存在的问题与挑战
5.1 近实时搜索的延迟
问题描述:
- 数据写入后默认需要 1 秒才能被搜索到
- 某些场景下 1 秒的延迟无法接受
影响场景:
可视化展示:
@startuml
skinparam backgroundColor transparent
participant "Client" as Client
participant "ES" as ES
database "Segment" as Segment
Client -> ES: 写入文档 doc_id=1
activate ES
ES -> ES: 写入 Buffer
ES --> Client: 写入成功
deactivate ES
Client -> ES: 查询 doc_id=1
activate ES
ES -> Segment: 搜索所有 Segment
Segment --> ES: 未找到
note right #Pink
**问题**
文档在 Buffer 中
尚未 Refresh 到 Segment
查询不到!
end note
ES --> Client: 返回空结果
deactivate ES
... 1秒后 Refresh ...
Client -> ES: 再次查询 doc_id=1
activate ES
ES -> Segment: 搜索所有 Segment
Segment --> ES: 找到文档
ES --> Client: 返回文档
deactivate ES
@enduml
正在加载图表...
![PlantUML Diagram]()
缓解措施:
- 写入后手动 Refresh:
POST /index/_refresh(影响性能,慎用) - 使用
?refresh=wait_for 参数等待 Refresh 完成 - 调整
index.refresh_interval 参数(权衡实时性和性能) - 对于需要立即可见的场景,使用
GET /index/_doc/{id} 直接读取
5.2 深度分页问题
问题描述:
from + size 分页方式在深度分页时性能急剧下降- 例如
from=10000, size=10,每个分片需要返回 10010 条数据
影响场景:
问题示意:
@startuml
skinparam backgroundColor transparent
rectangle "查询: from=10000, size=10" as Query
rectangle "Shard 0" as S0 {
rectangle "返回 10010 条" as S0R #Pink
}
rectangle "Shard 1" as S1 {
rectangle "返回 10010 条" as S1R #Pink
}
rectangle "Shard 2" as S2 {
rectangle "返回 10010 条" as S2R #Pink
}
rectangle "协调节点" as Coord {
rectangle "合并 30030 条\n排序后取 10 条" as Merge #Yellow
}
Query -down-> S0
Query -down-> S1
Query -down-> S2
S0 -down-> Coord
S1 -down-> Coord
S2 -down-> Coord
note bottom of Coord #Pink
**性能问题**
- 每个分片返回 from+size 条数据
- 协调节点需要合并大量数据
- 内存和网络开销巨大
- 默认限制 max_result_window=10000
end note
@enduml正在加载图表...
![PlantUML Diagram]()
缓解措施:
- Scroll API:适合大量数据导出,维护快照
- Search After:基于上一页最后一条的排序值,只支持向后翻页
- 避免深度分页:业务层限制最大页码
- 使用
index.max_result_window 调整限制(治标不治本)
5.3 更新和删除代价高
问题描述:
- Segment 不可变,更新 = 删除旧文档 + 写入新文档
- 删除只是标记(.liv 文件),不会立即释放空间
- 需要等待 Merge 才能真正删除
影响场景:
- 频繁更新的数据
- 需要立即删除的合规场景
- 空间敏感的场景
缓解措施:
- 减少更新频率,批量更新
- 定期执行 Force Merge
- 设计时避免频繁更新的字段
- 使用版本号或时间戳做逻辑删除
5.4 集群脑裂风险
问题描述:
- 网络分区可能导致集群分裂为多个独立集群
- 每个子集群选举出自己的 Master
- 数据写入不同子集群,导致数据不一致
影响场景:
缓解措施:
- 7.x+ 版本已大幅改进选举机制,风险降低
- 设置合理的 Master 节点数量(至少 3 个)
- 使用
discovery.zen.minimum_master_nodes(旧版本) - 网络规划避免分区
5.5 内存压力
问题描述:
- 倒排索引的 Term Dictionary 需要加载到内存
- 聚合查询需要大量内存
- Field Data 可能导致 OOM
影响场景:
- 高基数字段聚合(如用户 ID)
- 大量 text 字段
- 复杂的嵌套聚合
缓解措施:
- 使用 Doc Values 代替 Field Data
- 控制聚合的基数
- 合理设置 JVM 堆内存(不超过 32GB)
- 使用 Circuit Breaker 防止 OOM
6. 总结
6.1 ElasticSearch 的优势
- 高效的全文搜索:基于倒排索引,毫秒级搜索响应
- 灵活的分词:支持多种分词器,可定制化
- 分布式架构:原生支持分片和副本,易于扩展
- 近实时搜索:写入约 1 秒后可搜索
- 丰富的查询能力:全文搜索 + 结构化查询 + 聚合分析
6.2 需要注意的问题
- 近实时延迟:写入后不能立即搜索到
- 深度分页:避免
from + size 深度分页 - 更新代价高:减少不必要的更新操作
- 不适合 OLTP:不要用于事务场景
- 内存管理:注意 JVM 和系统内存分配
6.3 最佳实践建议
索引设计:
- 合理设置分片数(建议每个分片 10-50GB)
- 避免过多字段(控制在 1000 以内)
- 使用合适的字段类型(keyword vs text)
- 禁用不需要搜索的字段的索引
写入优化:
- 批量写入(Bulk API,每批 5-15MB)
- 写入时可临时增大
refresh_interval - 使用自动生成的 ID 或避免版本冲突
查询优化:
- 使用 Filter 代替 Query(可缓存)
- 避免
* 通配符开头的查询 - 使用 Search After 替代深度分页
- 合理使用
_source 过滤返回字段
集群规划:
- 分离 Master、Data、Coordinating 节点角色
- JVM 堆内存不超过 32GB,预留一半给文件系统缓存
- 使用 SSD 存储
- 监控关键指标:索引延迟、搜索延迟、GC 时间
运维管理:
- 定期监控分片大小和数量
- 低峰期执行 Force Merge
- 使用 ILM(Index Lifecycle Management)管理索引生命周期
- 定期备份(Snapshot)
ElasticSearch 在全文搜索和日志分析场景下具有显著优势,但需要理解其工作原理和限制,才能充分发挥其潜力。通过合理的索引设计、查询优化和集群规划,可以构建高效、稳定的搜索服务。