Elasticsearch与Lucene核心机制详解
一、基础概念:ES与Lucene的分工
1.1 核心比喻
- Elasticsearch = 图书管理员 + 图书馆管理系统
- Lucene = 图书索引卡片柜(最核心的检索技术)
1.2 具体职责分工
| 功能 | Lucene负责 | Elasticsearch负责 |
|---|---|---|
| 索引存储 | 单个索引文件格式 | 分布式分片管理 |
| 分词 | 分词算法实现 | 分词器配置管理 |
| 相关性 | TF-IDF/BM25计算 | 查询语句解析 |
| 存储 | 字段原始值存储 | 集群数据分布 |
| 查询 | 单个分片内检索 | 分布式查询协调 |
| 事务 | 单个索引段变更 | 集群级事务一致性 |
| API | 无 | RESTful API、客户端SDK |
1.3 关键理解点
-
Lucene是库,ES是服务
- Lucene:Java库,需要编程调用
- ES:开箱即用的搜索服务
-
Lucene不懂分布式
- Lucene只处理单个索引(分片)
- ES负责把数据分布到多个Lucene索引上
-
ES是Lucene的"集群管理器"
一个ES节点 = Lucene实例 + 集群协调模块 整个ES集群 = 多个Lucene实例 + 分布式协调系统
二、Lucene倒排索引详细结构
2.1 简化表示 vs 真实结构
简化表示(便于理解):
词项: "小米" -> 位置: [name字段, tags字段]
真实的多层结构:
Level 1: 词项字典(Term Dictionary)
├── "小米" → 指向倒排列表A
├── "手机" → 指向倒排列表B
├── "2999" → 指向倒排列表C
└── ... (按字典序排序,使用FST压缩存储)
Level 2: 倒排列表(Posting List)
倒排列表A(对应词项"小米"):
┌───────────────────────────────────────┐
│ Header: │
│ • DocFreq = 2 (文档频率) │
│ • TotalTermFreq = 3 (总词频) │
├───────────────────────────────────────┤
│ DocId List (使用FOR编码压缩): │
│ 文档1 → [位置信息指针] │
│ 文档4 → [位置信息指针] │
├───────────────────────────────────────┤
│ 位置信息 (存储在每个文档项中): │
│ 文档1: │
│ • 字段: name │
│ - 位置: 0 (在字段中的偏移) │
│ - 起始字符: 0 │
│ - 结束字符: 2 │
│ • 字段: brand │
│ - 位置: 0 │
│ - 起始字符: 0 │
│ - 结束字符: 2 │
└───────────────────────────────────────┘
2.2 倒排索引的核心组件
-
词项字典(Term Dictionary)
- 所有词项的排序列表
- 使用FST(有限状态转换器)压缩存储
- 支持前缀查询和模糊查询
-
倒排列表(Posting List)
- 存储包含该词项的文档列表
- 包含:文档ID、词频、位置信息
- 使用FOR(Frame Of Reference)编码压缩
-
位置信息(Positions)
- 词项在文档中的具体位置
- 包括:字段名、偏移量、起止字符
- 用于短语查询和高亮显示
三、索引段(Segment)机制
3.1 什么是段?
段是Lucene索引的最小独立单元,每个段都是一个完整的、不可变的倒排索引。
3.2 段的产生过程
graph TB
A[文档写入] --> B[内存Buffer]
B --> C{达到阈值?}
C --否--> D[等待Refresh]
C --是--> E[创建新段]
E --> F[Segment_1]
F --> G[Refresh后<br>段可搜索]
G --> H{后台Merge触发?}
H --是--> I[选择小段合并]
I --> J[创建更大段]
J --> K[删除旧段]
style F fill:#e1f5fe
style J fill:#f3e5f5
3.3 段生命周期示例
时间线发展:
T0: 索引为空
T1: 写入文档1-3 → 内存Buffer
T2: 写入文档4,触发refresh → 创建Segment_1[文档1-4]
T3: 写入文档5-7 → 内存Buffer
T4: 写入文档8,触发refresh → 创建Segment_2[文档5-8]
T5: 写入文档9-10 → 内存Buffer
T6: refresh → 创建Segment_3[文档9-10]
T7: 触发合并 → Segment_1+Segment_2合并为Segment_4[文档1-8]
最终:Segment_4 + Segment_3
3.4 段的物理文件结构
Segment_1/
├── .si (段信息文件)
├── 倒排索引相关:
│ ├── .tim (词项字典)
│ ├── .tip (词项索引)
│ ├── .doc (倒排列表)
│ └── .pos (位置信息)
├── 正排存储:
│ ├── .fdt (存储字段值)
│ ├── .fdx (存储字段索引)
│ └── .fdm (存储字段元数据)
├── 列式存储:
│ ├── .dvd (DocValues数据)
│ └── .dvm (DocValues元数据)
└── 其他元数据文件
3.5 段的关键特性
- 不可变性:一旦创建就不能修改
- 独立性:每个段都是完整索引
- 渐进合并:小段合并成大段
- 删除标记:删除文档时只标记,合并时才物理删除
3.6 为什么要有段机制?
- 写入性能:可以批量写入,减少磁盘IO
- 删除效率:通过标记删除,避免立即重写索引
- 缓存友好:小段可以完全载入内存
- 并发控制:读操作不影响写操作
- 故障恢复:translog + 段机制保证数据安全
四、10文档ES集群存储与查询实例
4.1 示例文档
文档1: {"id":1,"name":"小米手机","brand":"小米","price":2999,"category":"手机"}
文档2: {"id":2,"name":"苹果手机","brand":"苹果","price":8999,"category":"手机"}
文档3: {"id":3,"name":"华为平板","brand":"华为","price":3999,"category":"平板"}
文档4: {"id":4,"name":"小米电视","brand":"小米","price":4999,"category":"电视"}
文档5: {"id":5,"name":"联想笔记本","brand":"联想","price":5999,"category":"电脑"}
文档6: {"id":6,"name":"苹果笔记本","brand":"苹果","price":12999,"category":"电脑"}
文档7: {"id":7,"name":"华为手机","brand":"华为","price":3999,"category":"手机"}
文档8: {"id":8,"name":"小米笔记本","brand":"小米","price":6999,"category":"电脑"}
文档9: {"id":9,"name":"三星电视","brand":"三星","price":5999,"category":"电视"}
文档10: {"id":10,"name":"苹果平板","brand":"苹果","price":5999,"category":"平板"}
4.2 集群部署结构
3节点集群,3主分片+1副本:
节点1: 分片P0(主), 分片P1(副本), 分片P2(副本)
节点2: 分片P1(主), 分片P2(副本), 分片P0(副本)
节点3: 分片P2(主), 分片P0(副本), 分片P1(副本)
4.3 文档分布(按_id哈希路由)
文档1 → 分片1 文档6 → 分片0
文档2 → 分片2 文档7 → 分片1
文档3 → 分片0 文档8 → 分片2
文档4 → 分片1 文档9 → 分片0
文档5 → 分片2 文档10 → 分片1
最终分布:
- 分片0: 文档3,6,9
- 分片1: 文档1,4,7,10
- 分片2: 文档2,5,8
4.4 搜索流程详解
场景1:搜索"小米"
1. ES接收请求:GET /products/_search?q=小米
2. 转换为内部查询:match "_all": "小米"
3. 向所有主分片广播查询:
- 节点2查询分片1
- 节点3查询分片2
- 节点1查询分片0
4. 各分片Lucene并行查询:
- 分片1: [文档1:0.85, 文档4:0.85]
- 分片2: [文档8:0.70]
- 分片0: []
5. ES归并结果:[文档1, 文档4, 文档8]
6. 返回给用户
场景2:范围查询"price>5000"
各分片本地查询:
- 分片1: price DocValues遍历 → 文档10(5999)匹配
- 分片2: 文档2(8999)、5(5999)、8(6999)匹配
- 分片0: 文档6(12999)、9(5999)匹配
全局结果:文档2,5,6,8,9,10
场景3:聚合统计"按品牌分组"
分片本地聚合:
- 分片1: 小米×2, 华为×1, 苹果×1
- 分片2: 苹果×1, 联想×1, 小米×1
- 分片0: 华为×1, 苹果×1, 三星×1
全局归并:
苹果:3, 小米:3, 华为:2, 联想:1, 三星:1
4.5 写入过程数据流
1. 客户端 → ES协调节点
2. 路由计算:_id哈希 → 确定分片
3. 转发到主分片节点
4. 主分片写入:
a. 写入translog(保证持久化)
b. Lucene内存buffer添加文档
c. 返回成功给客户端
5. 异步复制到副本分片
6. refresh(默认1秒):
a. Lucene buffer生成新段
b. 索引可搜索
7. flush(每30分钟/512MB):
a. 将内存数据写入磁盘
b. 清理translog
五、关键配置参数
# 段相关配置
index.refresh_interval: "1s" # 刷新间隔
index.merge.policy.max_merged_segment: "5gb" # 最大段大小
index.merge.policy.segments_per_tier: 10 # 每层段数
index.merge.scheduler.max_thread_count: 1 # 合并线程数
# 索引配置
index.number_of_shards: 3 # 主分片数
index.number_of_replicas: 1 # 副本数
index.codec: "Lucene87" # 压缩编解码器