Elasticsearch 索引原理:倒排索引与 Segment 管理
作者:技术博客作者 | 标签:Elasticsearch、索引、倒排索引、Segment、源码解析
目录
- 引言
- 倒排索引核心原理
- [Lucene 倒排索引实现机制](#Lucene 倒排索引实现机制)
- [Segment 管理机制](#Segment 管理机制)
- 数据写入流程与持久化
- 性能优化实践
- 总结
1. 引言
Elasticsearch 作为基于 Lucene 的分布式搜索引擎,其核心能力源于倒排索引(Inverted Index)这一精妙的数据结构。与传统关系型数据库的 B+ 树索引不同,倒排索引通过建立"词项到文档"的映射关系,实现了毫秒级的全文检索性能。本文将从源码层面深入剖析 Elasticsearch 8.x 的索引原理,涵盖倒排索引的数据结构、Segment 管理机制以及数据持久化流程。
核心问题导向:
- 为什么 Elasticsearch 能够在亿级文档中实现秒级检索?
- Segment 如何平衡写入性能与查询效率?
- refresh、flush、translog 三者如何协同保障数据安全?
2. 倒排索引核心原理
2.1 倒排索引 vs 正排索引
| 特性 | 正排索引(Forward Index) | 倒排索引(Inverted Index) |
|---|---|---|
| 映射关系 | 文档 → 词项 | 词项 → 文档列表 |
| 查询场景 | 按文档ID获取内容 | 按词项检索相关文档 |
| 典型应用 | MongoDB、MySQL InnoDB | Elasticsearch、Lucene、Solr |
| 检索复杂度 | O(n) 需遍历所有文档 | O(1) 直接定位词项 |
| 写入性能 | 高(追加式写入) | 中等(需更新索引结构) |
2.2 倒排索引的逻辑结构
倒排索引由两个核心组件构成:
plaintext
┌─────────────────────────────────────────────────────────────┐
│ 倒排索引逻辑结构 │
├─────────────────────────────────────────────────────────────┤
│ Term Dictionary (词项字典) │
│ ├─ "elasticsearch" → Pointer(0x01) │
│ ├─ "index" → Pointer(0x02) │
│ └─ "segment" → Pointer(0x03) │
├─────────────────────────────────────────────────────────────┤
│ Posting List (倒排表) │
│ ├─ 0x01: [Doc1, Doc3, Doc7] │
│ ├─ 0x02: [Doc1, Doc2, Doc5, Doc7] │
│ └─ 0x03: [Doc2, Doc7] │
└─────────────────────────────────────────────────────────────┘
示例数据:
| 文档ID | 文档内容 |
|---|---|
| Doc1 | "Elasticsearch 是基于 Lucene 的搜索引擎" |
| Doc2 | "Segment 是索引的最小存储单元" |
| Doc3 | "Lucene 使用倒排索引实现快速检索" |
| Doc7 | "Elasticsearch 管理多个 Segment 提供索引能力" |
对应的倒排索引:
| 词项(Term) | 倒排表(Posting List) |
|---|---|
| elasticsearch | [Doc1, Doc7] |
| index | [Doc1, Doc2, Doc7] |
| segment | [Doc2, Doc7] |
| lucene | [Doc1, Doc3] |
| 搜索引擎 | [Doc1, Doc3] |
3. Lucene 倒排索引实现机制
3.1 FST: Finite State Transducer
Lucene 使用 有限状态传感器(FST) 来实现 Term Dictionary,相比传统的 B+ 树或 Hash 表,FST 具有更小的内存占用和更快的前缀查询性能。
FST 核心优势:
- 内存压缩:共享词项前缀,如 "apple" 和 "application" 共享 "app" 前缀
- 前缀查找:O(prefix_length) 时间复杂度完成范围查询和通配符查询
- 映射能力:FST 的输出值可以指向 Posting List 的磁盘位置
FST 结构示意图:
e
l
a
s
t
i
c
⟂
s
e
g
m
e
n
t
开始
B
C
D
Node: PostingListPtr1
F
G
H
Node: PostingListPtr2
开始
K
L
M
N
O
P
Node: PostingListPtr3
3.2 Posting List 的压缩存储
为了减少磁盘占用和内存压力,Lucene 对 Posting List 采用了多种压缩技术:
| 压缩技术 | 适用场景 | 压缩比 | 解压开销 |
|---|---|---|---|
| Frame of Reference (FOR) | 有序文档ID列表 | 5-10x | 低 |
| Delta Encoding | 连续文档ID | 10-100x | 极低 |
| Bit-packing | 小整数列表 | 8-32x | 低 |
| Roaring Bitmaps | 集合运算(AND/OR) | 2-5x | 中等 |
Delta Encoding 示例:
原始 Posting List:[100, 101, 105, 110, 150]
Delta 编码后:[100, 1, 4, 5, 40]
关键源码片段(Lucene 9.7.0):
java
// org.apache.lucene.codecs.lucene90.Lucene90PostingsWriter
// 增量编码文档ID
void writeDoc(int docID) throws IOException {
// 计算与上一个文档ID的差值
final int delta = docID - lastDocID;
// 使用 Bit-packing 编码差值
if (delta == 0) {
// 连续文档(同一词项在同一文档中出现多次)
writeVInt(0); // 写入0表示连续
} else if (delta == 1) {
writeVInt(1); // 紧邻文档
} else {
writeVInt(delta); // 写入差值
}
lastDocID = docID;
}
3.3 查询流程:Term Query 执行路径
Disk PostingList FST ES Client Disk PostingList FST ES Client GET /_search?q=title:"lucene" 查找 Term "lucene" 返回 PostingList 指针 (0x02A4) 读取倒排表数据 读取磁盘块 返回压缩数据 [Doc1, Doc3, Doc7] 解压后返回文档ID列表 加载 Doc1, Doc3, Doc7 的 _source 返回匹配文档列表
4. Segment 管理机制
4.1 Segment 的生命周期
Segment 是 Elasticsearch 中不可变的索引文件单元,其生命周期包含以下阶段:
refresh
flush
merge
del
Index Buffer
内存缓冲区
New Segment
文件系统缓存
Committed Segment
磁盘持久化
Merged Segment
合并后的段
Deleted Segment
标记删除
关键特性:
- 不可变性 :已提交的 Segment 永不修改,删除操作通过
.del文件标记 - 原子性 :flush 时生成
segments_N文件,包含所有活跃 Segment 的元数据 - 合并策略:后台定期合并小 Segment 为大 Segment,减少文件句柄数
4.2 Segment Merge 策略
Elasticsearch 8.x 默认使用 TieredMergePolicy (分层合并策略),其核心思想是:限制 Segment 数量,控制 Segment 大小梯度。
合并决策因子:
| 参数 | 默认值 | 作用 |
|---|---|---|
index.merge.policy.max_merged_segment |
5GB | 合并后的单个 Segment 最大大小 |
index.merge.policy.segments_per_tier |
10 | 每层允许的 Segment 数量 |
index.merge.policy.max_merge_at_once |
10 | 单次合并最多涉及的 Segment 数量 |
TieredMergePolicy 工作原理:
java
// org.apache.lucene.index.TieredMergePolicy (Lucene 9.7.0)
// 简化的合并选择逻辑
public MergeSpecification findMerges(MergeTrigger mergeTrigger) {
// 1. 按 Segment 大小排序
final List<SegmentSize> sorted = sortBySize(segments);
// 2. 分层处理(大 Segment 单独一层,小 Segment 一层)
final List<List<SegmentSize>> tiers = groupIntoTiers(sorted);
// 3. 对每一层检查是否需要合并
for (List<SegmentSize> tier : tiers) {
if (tier.size() > maxSegmentsPerTier) {
// 选择候选 Segment 进行合并
final List<Segment> candidates = selectMergeCandidates(tier);
// 计算合并后的总大小
final long totalSize = sumSize(candidates);
if (totalSize < maxMergedSegmentSize) {
return new MergeSpecification(candidates);
}
}
}
return null; // 无需合并
}
4.3 Merge 对性能的影响
| 影响维度 | 正面效应 | 负面效应 |
|---|---|---|
| 查询性能 | 减少 Segment 数量,降低查询时合并开销 | Merge 时占用 CPU 和磁盘 I/O |
| 存储空间 | 删除已合并的旧 Segment,回收磁盘空间 | 临时需要 2x 磁盘空间(新旧 Segment 共存) |
| 写入吞吐 | 释放内存缓冲区,降低 refresh 频率 | 大批量写入时 Merge 可能成为瓶颈 |
优化建议:
json
PUT /my_index/_settings
{
"index.merge.policy.max_merged_segment": "10gb",
"index.merge.scheduler.max_thread_count": 1,
"index.merge.policy.max_merge_at_once": "5"
}
5. 数据写入流程与持久化
5.1 写入流程全景图
ReplicaShard Translog IndexBuffer PrimaryShard CoordinatingNode Client ReplicaShard Translog IndexBuffer PrimaryShard CoordinatingNode Client 后台线程每 1s 执行 refresh POST /my_index/_doc { "title": "..." } 转发写入请求 写入内存缓冲区 追加事务日志(WAL) 写入成功 同步数据到副本 确认写入 返回成功 201 Created 生成新 Segment(可搜索)
5.2 三大核心机制对比
| 机制 | 触发条件 | 作用 | 数据位置 |
|---|---|---|---|
| refresh | 默认 1秒(index.refresh_interval) |
使数据可被搜索 | 文件系统缓存 |
| flush | translog 大小阈值或 30分钟 | 持久化到磁盘 | 磁盘(.si, .cfe, .cfm) |
| translog fsync | 每次 index.translog.durability=request |
强制刷盘保证数据安全 | 磁盘(.tlog 文件) |
5.3 Translog 的作用与优化
Translog 的双重职责:
- 数据安全:防止宕机导致内存数据丢失
- 副本同步:主分片通过 Translog 顺序同步到副本
Durability 模式对比:
| 模式 | 性能 | 数据安全性 | 适用场景 |
|---|---|---|---|
request(默认) |
低 | 高(每次写入 fsync) | 金融、交易等关键数据 |
async |
高 | 中(每 5秒 fsync) | 日志、时序数据 |
test |
极高 | 低(不 fsync) | 单元测试环境 |
优化配置示例:
json
PUT /my_index/_settings
{
"index.refresh_interval": "30s", // 降低 refresh 频率提升写入吞吐
"index.translog.durability": "async", // 异步刷盘
"index.translog.sync_interval": "10s", // 每 10秒刷一次
"index.translog.flush_threshold_size": "1gb" // translog 达到 1GB 时触发 flush
}
5.4 源码解析:IndexWriter 写入逻辑
关键类: org.apache.lucene.index.IndexWriter(Lucene 9.7.0)
java
// 添加文档的核心流程
public long addDocument(Document doc) throws IOException {
// 1. 获取写入锁(保证并发安全)
try (Lock lock = writeLock.obtain()) {
// 2. 将文档写入内存缓冲区(DocumentsWriterPerThread)
final DocumentsWriterPerThread dwpt = flushControl.obtainAnyDWPT();
final long seqNo = dwpt.addDocument(doc);
// 3. 同时写入 translog(Elasticsearch 扩展点)
translog.add(new Translog.Index(doc, seqNo));
// 4. 检查是否需要触发 refresh
if (ramBytesUsed() > indexBufferSize) {
scheduleRefresh(); // 异步触发 refresh
}
return seqNo;
}
}
// refresh 操作:将内存数据转为 Segment
public void refresh() throws IOException {
// 1. 获取当前所有活跃的 DWPT(DocumentsWriterPerThread)
final List<DocumentsWriterPerThread> dwpts = flushControl.getAllDWPTs();
// 2. 每个 DWPT 生成一个 Segment(内存中)
final List<Segment> newSegments = new ArrayList<>();
for (DocumentsWriterPerThread dwpt : dwpts) {
// 2.1 flush 内部缓冲区到新 Segment
final Segment segment = dwpt.flush();
if (segment != null) {
newSegments.add(segment);
}
}
// 3. 将新 Segment 注册到 IndexWriter(使其可被搜索)
for (Segment segment : newSegments) {
segmentInfos.add(segment); // 更新 segments_N 文件
}
// 4. 清空已 flush 的 DWPT
flushControl.releaseAfterFlush(dwpts);
}
6. 性能优化实践
6.1 写入性能优化 Top 5
| 优化项 | 默认值 | 优化值 | 预期提升 |
|---|---|---|---|
| 批量写入 | 单条文档 | 1000-5000 条/批次 | 5-10x |
| refresh_interval | 1s | 30s-60s | 2-3x |
| translog.durability | request | async | 1.5-2x |
| 副本数 | 1 | 0(导入完成后恢复) | 2x |
| 线程数 | 1 | 4-8(根据 CPU 核数) | 3-4x |
批量写入代码示例(Java API):
java
import org.elasticsearch.action.bulk.BulkRequest;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.xcontent.XContentType;
import java.util.List;
public class EsBulkWriter {
private final RestHighLevelClient client;
private static final int BATCH_SIZE = 3000;
public void bulkIndex(String indexName, List<MyDocument> documents) throws Exception {
BulkRequest bulkRequest = new BulkRequest();
for (int i = 0; i < documents.size(); i++) {
MyDocument doc = documents.get(i);
// 构建索引请求
IndexRequest indexRequest = new IndexRequest(indexName)
.id(doc.getId()) // 显式指定 ID 避免 version lookup
.source(doc.toJson(), XContentType.JSON);
bulkRequest.add(indexRequest);
// 每 3000 条执行一次 bulk
if (i % BATCH_SIZE == 0) {
client.bulk(bulkRequest, RequestOptions.DEFAULT);
bulkRequest = new BulkRequest(); // 重置请求
}
}
// 写入剩余数据
if (bulkRequest.numberOfActions() > 0) {
client.bulk(bulkRequest, RequestOptions.DEFAULT);
}
}
}
6.2 查询性能优化
关键配置项:
json
PUT /my_index/_settings
{
"index.query.default_field": ["title^2", "content"], // 字段权重
"index.max_result_window": 10000, // 深分页限制
"index.routing.allocation.include._tier_preference": "data_hot,data_warm"
}
使用 File Dat a Cache 加速字段数据查询:
bash
# 查询缓存使用情况
GET /_nodes/stats/indices/query_cache
GET /_nodes/stats/indices/request_cache
# 清理缓存
POST /my_index/_cache/clear
6.3 Segment 数量监控
bash
# 查看各分片的 Segment 数量
GET /_cat/segments/my_index?v&h=index,shard,segment,size,size.memory
# 输出示例:
# index shard segment size size.memory
# my_index 0 0 1.2gb 45.2mb
# my_index 0 1 800mb 32.1mb
# my_index 1 0 1.5gb 51.3mb
健康阈值:
- 每个分片的 Segment 数量 < 50:健康
- 每个分片的 Segment 数量 50-100:关注
- 每个分片的 Segment 数量 > 100:需要强制合并
强制合并命令(慎用):
bash
# 将 Segment 合并为最多 1 个
POST /my_index/_forcemerge?max_num_segments=1
# 仅合并超过 100MB 的 Segment
POST /my_index/_forcemerge?max_num_segments=5&only_expunge_deletes=true
7. 总结
本文深入剖析了 Elasticsearch 的索引原理,核心要点如下:
技术要点回顾
- 倒排索引:通过 FST + Posting List 实现 O(1) 级别的词项查找
- Segment 不可变性:简化并发控制,通过标记删除实现更新
- TieredMergePolicy:平衡写入放大与查询性能
- translog + refresh + flush:三管齐下保障数据安全与可搜索性
最佳实践建议
| 场景 | 关键配置 | 预期效果 |
|---|---|---|
| 大数据量导入 | refresh_interval=30s, replicas=0, bulk_size=5000 |
写入吞吐提升 5-10 倍 |
| 实时搜索 | refresh_interval=1s, translog.durability=request |
搜索延迟 < 1秒 |
| 日志存储 | codec=best_compression, number_of_shards=3-5 |
存储空间减少 30-50% |
进阶学习路径
- 深入 Lucene 源码 :研究
BlockTreeTermsReader、Lucene90PostingsReader - 分布式一致性:理解主分片与副本的同步机制(Sequence Number、Checkpoint)
- 冷热分层:结合 ILM(Index Lifecycle Management)实现数据生命周期管理
参考资料:
- Elasticsearch 官方文档 - Mapping
- Apache Lucene 9.7.0 源码
- Elasticsearch: The Definitive Guide
- Lucene in Action
关于作者:
专注于分布式搜索引擎与大数据存储技术,曾主导日均 10亿+ 文档的 Elasticsearch 集群优化实践。
版权声明:
本文为原创技术文章,转载请注明出处。
推荐阅读: