搜索引擎能在毫秒级时间内从数十亿文档中找到包含特定关键词的内容,这背后依靠的核心数据结构就是倒排索引 (Inverted Index)。与数据库使用的 B+ 树索引不同,倒排索引专为全文检索设计,是实现高效文本搜索的关键技术。
一、搜索引擎的核心问题# 1.1 从图书馆检索说起# 假设你在图书馆查找所有包含”Elasticsearch”这个词的书籍:
flowchart LR
subgraph 传统方法
A1[遍历每本书] --> A2[逐页搜索]
A2 --> A3[记录匹配书籍]
end
subgraph 索引方法
B1[查阅索引卡片] --> B2[直接获取书架位置]
B2 --> B3[取书阅读]
end
Note over A1,A3: 时间复杂度 O(N×M)<br/>N=书籍数量,M=平均页数
Note over B1,B3: 时间复杂度 O(1)<br/>直接定位
传统方法 :逐本书翻阅,检查每一页是否包含目标词。
索引方法 :图书馆有索引卡片,列出每个关键词出现在哪些书中、哪些页。
1.2 全文检索的定义# 全文检索(Full-Text Search)是指:
从大量非结构化文本数据中,根据关键词快速找到包含这些词的文档,并按相关性排序返回结果。
flowchart TB
subgraph 搜索需求
Q[用户查询: "Elasticsearch 性能优化"]
end
subgraph 文档集合
D1[文档1: Elasticsearch 基础教程]
D2[文档2: 数据库性能优化指南]
D3[文档3: Elasticsearch 性能调优实践]
D4[文档4: Java 编程指南]
D5[文档5: 搜索引擎性能优化]
end
subgraph 期望结果
R1[1. 文档3 - 包含两个关键词]
R2[2. 文档1 - 包含 Elasticsearch]
R3[3. 文档5 - 包含 性能优化]
R4[4. 文档2 - 包含 性能优化]
end
Q --> D1 & D2 & D3 & D4 & D5
D1 & D2 & D3 & D4 & D5 --> R1 & R2 & R3 & R4
1.3 核心挑战# 全文检索面临的核心挑战:
挑战 描述 传统数据库的局限 数据规模 文档数量可达数十亿 逐行扫描不可接受 查询复杂度 多关键词组合、模糊匹配 LIKE 查询性能极差 相关性排序 结果需要按相关程度排序 数据库无此概念 实时性要求 毫秒级响应 全表扫描无法满足 语言特性处理 分词、词干提取、同义词等 数据库无相关支持
二、正排索引与倒排索引# 2.1 正排索引:文档到词的映射# 正排索引(Forward Index)是从文档视角出发,记录每个文档包含哪些词:
flowchart LR
subgraph 正排索引
D1[文档1] --> W1["elasticsearch, 搜索, 引擎"]
D2[文档2] --> W2["数据库, mysql, 索引"]
D3[文档3] --> W3["elasticsearch, 性能, 优化"]
end
Note over D1,W3: 文档ID → 词列表
正排索引的结构 :
"doc_1" : [ "elasticsearch" , "搜索" , "引擎" ],
"doc_2" : [ "数据库" , "mysql" , "索引" ],
"doc_3" : [ "elasticsearch" , "性能" , "优化" ]
查询方式 :要查找包含”elasticsearch”的文档,需要遍历所有文档的词列表。
def search_forward_index (query, forward_index):
for doc_id, words in forward_index.items():
2.2 倒排索引:词到文档的映射# 倒排索引(Inverted Index)反转了这个关系,从词出发,记录每个词出现在哪些文档中:
flowchart LR
subgraph 倒排索引
T1["elasticsearch"] --> DL1["doc_1, doc_3"]
T2["搜索"] --> DL2["doc_1"]
T3["引擎"] --> DL3["doc_1"]
T4["数据库"] --> DL4["doc_2"]
T5["mysql"] --> DL5["doc_2"]
T6["索引"] --> DL6["doc_2"]
T7["性能"] --> DL7["doc_3"]
T8["优化"] --> DL8["doc_3"]
end
Note over T1,DL8: 词 → 文档ID列表
倒排索引的结构 :
查询方式 :直接通过词定位文档列表。
def search_inverted_index (query, inverted_index):
return inverted_index.get(query, [])
2.3 两种索引的对比# flowchart TB
subgraph 正排索引查询
F1[查询: elasticsearch] --> F2[遍历文档1: 包含]
F2 --> F3[遍历文档2: 不包含]
F3 --> F4[遍历文档3: 包含]
F4 --> F5[返回 doc_1, doc_3]
end
subgraph 倒排索引查询
I1[查询: elasticsearch] --> I2[直接定位词典]
I2 --> I3[获取倒排表]
I3 --> I4[返回 doc_1, doc_3]
end
Note over F1,F5: 复杂度 O(N)
Note over I1,I4: 复杂度 O(1)
特性 正排索引 倒排索引 数据结构 文档 → 词列表 词 → 文档列表 查询复杂度 O(N),N 为文档数 O(1),直接定位 存储开销 较小,每文档存一份词列表 较大,每词存一份文档列表 更新效率 高,只更新单个文档 低,需更新多个词的倒排表 适用场景 获取文档内容、构建索引 全文检索、关键词查询 组合查询 困难,需多次遍历 高效,倒排表求交集
2.4 为什么叫”倒排”?# “倒排”(Inverted)这个名字源于关系反转:
flowchart LR
subgraph 正常思维
A[文档] -->|包含| B[词]
end
subgraph 倒排思维
C[词] -->|出现在| D[文档]
end
A -.->|关系反转| C
B -.->|关系反转| D
Note over A,B: 顺着文档找词
Note over C,D: 倒着用词找文档
理解方式 :
正排 :顺着文档的顺序,文档在前、词在后
倒排 :把关系倒过来,词在前、文档在后
三、倒排索引的详细结构# 3.1 核心组件# 倒排索引由两个核心部分组成:
flowchart TB
subgraph 倒排索引结构
subgraph 词典 Term Dictionary
T1["elasticsearch"] --> P1
T2["性能"] --> P2
T3["优化"] --> P3
T4["搜索引擎"] --> P4
end
subgraph 倒排表 Posting List
P1["doc_1, doc_3, doc_5"]
P2["doc_3, doc_7"]
P3["doc_3, doc_9"]
P4["doc_1, doc_5"]
end
end
Note over T1,P4: 词典有序存储<br/>倒排表存储文档ID
词典(Term Dictionary) :
存储所有不重复的词项(Term)
有序排列,支持二分查找
每个词项指向对应的倒排表
倒排表(Posting List) :
存储包含该词的所有文档 ID
有序排列,便于求交集
包含位置信息、词频等元数据
3.2 完整的倒排表结构# 实际应用中,倒排表不仅存储文档 ID,还包含丰富的元数据:
flowchart TB
subgraph 词项: "elasticsearch"
subgraph 倒排表
E1["DocID: 1<br/>TF: 3<br/>Positions: [1, 15, 42]<br/>Payload: [title, body, body]"]
E2["DocID: 3<br/>TF: 1<br/>Positions: [8]<br/>Payload: [body]"]
E3["DocID: 5<br/>TF: 2<br/>Positions: [3, 27]<br/>Payload: [title, body]"]
end
end
Note over E1,E3: TF = Term Frequency 词频<br/>Positions = 词在文档中的位置<br/>Payload = 附加信息
倒排表项的字段 :
字段 含义 用途 DocID 文档唯一标识 定位文档 TF 词频(Term Frequency) 计算相关性得分 Positions 词在文档中的位置 短语查询、高亮显示 Payload 附加信息 存储词的特殊属性 Offset 词的起止位置 高亮显示 Norm 归一化因子 调整文档长度对得分的影响
3.3 词典的组织结构# 词典需要支持高效的查找,Lucene(Elasticsearch 的底层)使用多层结构:
flowchart TB
subgraph 词典查找流程
Q[查询词: "search"] --> TI[Term Index<br/>内存中的前缀索引]
TI --> |快速定位| TD[Term Dictionary<br/>磁盘上的有序词典]
TD --> |二分查找| T[找到词项]
T --> PL[获取倒排表指针]
PL --> POST[读取倒排表]
end
Note over TI: 常驻内存,快速定位
Note over TD: 磁盘存储,节省内存
三层结构 :
Term Index :内存中的前缀索引,快速定位词典块
Term Dictionary :磁盘上的有序词典,存储完整词项
Posting List :倒排表,存储文档列表
3.4 为什么需要 Term Index?# 如果词典完全存储在内存中,内存开销巨大;如果完全在磁盘上,查找效率低下。
flowchart LR
subgraph 方案对比
subgraph 全内存
A1["内存占用: 大<br/>查找速度: 快"]
end
subgraph 全磁盘
A2["内存占用: 小<br/>查找速度: 慢"]
end
subgraph 折中方案
A3["Term Index: 内存<br/>Dictionary: 磁盘<br/>兼顾速度与空间"]
end
end
Term Index 的设计思想 :
只存储词项的前缀,大幅减少内存占用
通过前缀快速定位到词典中的数据块
然后在数据块中进行二分查找
四、FST 压缩技术# 4.1 词典存储的挑战# 假设词典有 100 万个词,平均每个词 10 字节,仅词典就需要约 10MB 内存。对于大规模应用,这个数字会更大。
flowchart TB
subgraph 存储挑战
W1["百万级词项"] --> M1["内存压力"]
W2["长尾词项"] --> M2["存储冗余"]
W3["频繁查询"] --> M3["访问效率"]
end
subgraph 解决方案
S1["压缩存储"] --> FST["FST 有序状态转换器"]
S2["前缀共享"] --> FST
S3["后缀共享"] --> FST
end
4.2 FST 的原理# FST(Finite State Transducer,有限状态转换器)是一种高效的有向无环图结构,用于压缩存储有序字符串集合:
flowchart LR
subgraph FST 示例
S((Start)) --> |"m"| M
M --> |"a"| MA
MA --> |"p"| MAP((Map:0))
MA --> |"t"| MAT((Mat:1))
M --> |"o"| MO
MO --> |"p"| MOP((Mop:2))
end
Note over S,MOP: 边表示字符<br/>终点数字表示输出值<br/>共享前缀'ma'和'm'
FST 的特点 :
前缀共享 :相同前缀只存储一次
后缀共享 :相同后缀也可共享
有序存储 :词项有序,支持二分查找
输出函数 :每个词项关联一个输出值
4.3 FST 与其他结构的对比# flowchart TB
subgraph 存储对比
subgraph 原始存储
O1["mop → 2"]
O2["map → 0"]
O3["mat → 1"]
O4["总计: 约 15 字节"]
end
subgraph Trie 树
T1["共享前缀 'm'"]
T2["共享前缀 'ma'"]
T3["总计: 约 12 字节"]
end
subgraph FST
F1["共享所有前缀"]
F2["共享所有后缀"]
F3["总计: 约 8 字节"]
end
end
特性 原始存储 Trie 树 FST 前缀共享 无 有 有 后缀共享 无 无 有 压缩率 0%(基准) 约 30-50% 约 60-80% 查找速度 O(1) hash O(L) L=词长 O(L) 有序遍历 不支持 支持 支持 内存效率 低 中 高
4.4 FST 在 Lucene 中的应用# flowchart TB
subgraph Lucene 词典实现
subgraph Term Index(内存)
FST["FST 结构<br/>存储词项前缀"]
end
subgraph Term Dictionary(磁盘)
BLK1["Block 1: apple..."]
BLK2["Block 2: banana..."]
BLK3["Block 3: cherry..."]
end
subgraph Posting Lists(磁盘)
PL1["词项倒排表"]
PL2["词项倒排表"]
PL3["词项倒排表"]
end
end
FST --> |定位| BLK1 & BLK2 & BLK3
BLK1 & BLK2 & BLK3 --> PL1 & PL2 & PL3
Note over FST: FST 指向磁盘块<br/>磁盘块包含完整词项
查找流程 :
def search_term_fst (term):
block = fst.search(term[:prefix_len])
block_data = disk.read(block)
term_info = binary_search(block_data, term)
posting_list = read_posting(term_info.pointer)
五、BM25 相关性排序算法# 5.1 为什么需要相关性排序?# 搜索”elasticsearch 性能优化”,可能返回数千个文档,用户需要最相关的排在前面。
flowchart TB
subgraph 搜索结果排序
Q[查询: elasticsearch 性能优化] --> R1[文档3: 包含两个关键词<br/>相关性: 高]
Q --> R2[文档1: 包含 elasticsearch<br/>相关性: 中]
Q --> R3[文档7: 包含 性能优化<br/>相关性: 中]
Q --> R4[文档9: 包含 性能<br/>相关性: 低]
end
5.2 TF-IDF 算法# 早期的相关性算法,基于两个概念:
词频(TF, Term Frequency) :词在文档中出现的次数越多,越相关。
逆文档频率(IDF, Inverse Document Frequency) :词在所有文档中出现得越少,区分度越高。
flowchart LR
subgraph TF-IDF 计算
TF["TF = 词在文档中出现次数 / 文档总词数"]
IDF["IDF = log(总文档数 / 包含该词的文档数)"]
SCORE["TF-IDF = TF × IDF"]
end
TF --> SCORE
IDF --> SCORE
公式 :
T F - I D F ( t , d ) = T F ( t , d ) × I D F ( t ) TF\text{-}IDF(t, d) = TF(t, d) \times IDF(t) T F - I D F ( t , d ) = T F ( t , d ) × I D F ( t )
其中:
T F ( t , d ) = 词 t 在文档 d 中的出现次数 文档 d 的总词数 TF(t, d) = \frac{\text{词}t\text{在文档}d\text{中的出现次数}}{\text{文档}d\text{的总词数}} T F ( t , d ) = 文档 d 的总词数 词 t 在文档 d 中的出现次数
I D F ( t ) = log 文档总数 包含词 t 的文档数 IDF(t) = \log\frac{\text{文档总数}}{\text{包含词}t\text{的文档数}} I D F ( t ) = log 包含词 t 的文档数 文档总数
5.3 BM25 算法# BM25 是 TF-IDF 的改进版本,解决了 TF-IDF 的一些问题:
flowchart TB
subgraph BM25 vs TF-IDF
subgraph TF-IDF 问题
P1["词频饱和问题:<br/>词出现100次不一定比10次相关10倍"]
P2["文档长度问题:<br/>长文档词数多,TF 偏高"]
end
subgraph BM25 改进
S1["饱和函数:<br/>词频增长有上限"]
S2["文档长度归一化:<br/>考虑文档平均长度"]
end
end
BM25 公式 :
B M 25 ( d , q ) = ∑ i = 1 n I D F ( q i ) × f ( q i , d ) × ( k 1 + 1 ) f ( q i , d ) + k 1 × ( 1 − b + b × ∣ d ∣ a v g d l ) BM25(d, q) = \sum_{i=1}^{n} IDF(q_i) \times \frac{f(q_i, d) \times (k_1 + 1)}{f(q_i, d) + k_1 \times (1 - b + b \times \frac{|d|}{avgdl})} B M 25 ( d , q ) = ∑ i = 1 n I D F ( q i ) × f ( q i , d ) + k 1 × ( 1 − b + b × a v g d l ∣ d ∣ ) f ( q i , d ) × ( k 1 + 1 )
参数说明:
| 参数 | 含义 | 默认值 |
| ----------- | ---------------------------- | ------ | --------------- | --- |
| f ( q i , d ) f(q_i, d) f ( q i , d ) | 词 q i q_i q i 在文档 d d d 中的频率 | - |
| ∣ d ∣ | d | ∣ d ∣ | 文档 d d d 的长度 | - |
| a v g d l avgdl a v g d l | 平均文档长度 | - |
| k 1 k_1 k 1 | 词频饱和参数,控制饱和速度 | 1.2 |
| b b b | 文档长度归一化参数 | 0.75 |
5.4 词频饱和曲线# xychart-beta
title "词频与得分关系曲线"
x-axis "词频" [1, 2, 3, 5, 10, 20]
y-axis "得分贡献" 0 --> 3
line "TF-IDF" [1, 2, 3, 5, 10, 20]
line "BM25" [0.9, 1.4, 1.7, 2.0, 2.3, 2.5]
关键差异 :
TF-IDF :得分随词频线性增长,可能出现过度匹配
BM25 :得分有饱和上限,避免长文档过度得分
5.5 BM25 参数调优# # Elasticsearch 中 BM25 参数配置
参数调优建议 :
场景 k1 建议 b 建议 原因 短文本(标题) 1.2-1.5 0.3-0.5 文档长度差异小,减少归一化 长文本(文章) 1.2-1.8 0.7-0.9 需要更强的长度归一化 专业领域 1.5-2.0 0.75 专业词权重更高
六、分词与规范化处理# 6.1 为什么需要分词?# 搜索”搜索引擎”,期望同时找到包含”搜索”、“引擎”、“搜索引擎”的文档:
flowchart TB
subgraph 无分词
T1["搜索引擎"] --> D1["仅匹配精确词"]
end
subgraph 有分词
T2["搜索引擎"] --> S1["搜索"]
T2 --> S2["引擎"]
S1 --> D2["匹配 '搜索' 的文档"]
S2 --> D3["匹配 '引擎' 的文档"]
end
6.2 分词器的工作流程# flowchart LR
subgraph 分词流程
T[原始文本] --> CH[Character Filters<br/>字符过滤]
CH --> TK[Tokenizer<br/>分词]
TK --> TF[Token Filters<br/>词项过滤]
TF --> R[词项列表]
end
subgraph 示例
E1["<p>Elasticsearch搜索引擎</p>"] --> E2["Elasticsearch搜索引擎"]
E2 --> E3["[elasticsearch, 搜索, 引擎]"]
E3 --> E4["[elasticsearch, 搜索, 引擎]"]
end
三个阶段 :
Character Filters :预处理,如去除 HTML 标签
Tokenizer :分词,将文本切分为词项
Token Filters :后处理,如小写转换、词干提取
6.3 常见分词器# flowchart TB
subgraph 英文分词器
EN1["Standard Analyzer<br/>按空格和标点分词"]
EN2["English Analyzer<br/>词干提取: running → run"]
EN3["Whitespace Analyzer<br/>仅按空格分词"]
end
subgraph 中文分词器
CN1["IK 分词器<br/>智能分词 / 最细粒度"]
CN2["jieba 分词器<br/>精确模式 / 全模式"]
CN3["HanLP 分词器<br/>多种分词算法"]
end
中文分词示例 :
smart_result = [ "中华人民共和国" ] # 作为一个整体
max_result = [ "中华人民共和国" , "中华人民" , "中华" , "华人" , "人民共和国" , "人民" , "共和国" , "共和" , "国" ]
6.4 规范化处理# 规范化处理确保不同形式的词能够匹配:
flowchart TB
subgraph 规范化处理
subgraph 大小写归一化
C1["Elasticsearch"] --> C2["elasticsearch"]
C3["ELASTICSEARCH"] --> C2
end
subgraph 词干提取
S1["running"] --> S2["run"]
S3["ran"] --> S2
end
subgraph 同义词扩展
Y1["搜索"] --> Y2["search"]
Y3["查找"] --> Y2
end
end
处理类型 示例 作用 大小写归一化 Elasticsearch → elasticsearch 大小写不敏感匹配 词干提取 running → run 不同词形归一化 停用词过滤 the, a, is → 删除 减少索引大小,提高精度 同义词扩展 搜索 → [搜索, search, 查找] 扩大召回范围
6.5 分词对索引的影响# flowchart TB
subgraph 建立索引
D[文档: "Elasticsearch搜索引擎"] --> A[分词]
A --> T1["elasticsearch"]
A --> T2["搜索"]
A --> T3["引擎"]
T1 --> I1[写入倒排索引]
T2 --> I2[写入倒排索引]
T3 --> I3[写入倒排索引]
end
subgraph 查询处理
Q[查询: "搜索"] --> QA[分词]
QA --> QT["搜索"]
QT --> S[查询倒排索引]
end
关键原则 :索引和查询必须使用相同的分词器,否则无法匹配。
七、倒排索引与 B+ 树的对比# 7.1 设计目标的差异# flowchart TB
subgraph B+ 树
B1["设计目标: 精确查找"]
B2["数据类型: 数值、字符串"]
B3["查询类型: 等值、范围"]
B4["典型场景: 数据库索引"]
end
subgraph 倒排索引
I1["设计目标: 全文检索"]
I2["数据类型: 文本"]
I3["查询类型: 关键词、短语"]
I4["典型场景: 搜索引擎"]
end
7.2 查询模式对比# sequenceDiagram
participant Q as 查询
participant B as B+ 树索引
participant I as 倒排索引
Note over Q,I: 精确查找
Q->>B: WHERE id = 100
B-->>Q: O(log N) 树遍历
Q->>I: TEXT MATCH "elasticsearch"
I-->>Q: O(1) 哈希查找
Note over Q,I: 范围查询
Q->>B: WHERE id > 100 AND id < 200
B-->>Q: 叶子节点顺序扫描
Q->>I: 不支持范围查询
I-->>Q: N/A
Note over Q,I: 多条件组合
Q->>B: WHERE id > 100 AND name = 'test'
B-->>Q: 多索引合并,效率一般
Q->>I: TEXT MATCH 'a' AND 'b'
I-->>Q: 倒排表求交集,效率高
7.3 结构对比# flowchart TB
subgraph B+ 树结构
B_ROOT["根节点<br/>(30, 60, 90)"]
B_L1["节点<br/>(10, 20)"]
B_L2["节点<br/>(30, 40, 50)"]
B_L3["节点<br/>(60, 70, 80)"]
B_L4["节点<br/>(90, 100)"]
B_ROOT --> B_L1 & B_L2 & B_L3 & B_L4
end
subgraph 倒排索引结构
I_DICT["词典<br/>有序数组或 FST"]
I_P1["倒排表: 10 → [1, 5, 8]"]
I_P2["倒排表: 20 → [2, 7]"]
I_P3["倒排表: 30 → [3, 4, 9]"]
I_DICT --> I_P1 & I_P2 & I_P3
end
7.4 性能特征对比#
特性 B+ 树索引 倒排索引 精确查找 O(log N) O(1) 范围查询 高效(叶子节点有序) 不支持 多条件组合 效率一般 高效(倒排表求交集) 模糊匹配 不支持(或前缀匹配) 支持(编辑距离、拼音等) 写入性能 高(顺序写入) 较低(更新多个倒排表) 空间效率 较高 较低(词项冗余) 相关性排序 不支持 原生支持 数据更新 增删改高效 增删改开销大
7.5 为什么数据库不适合全文检索?# SELECT * FROM articles WHERE content LIKE '%elasticsearch%' ;
flowchart TB
subgraph LIKE 查询问题
L1["前导通配符 % 导致索引失效"]
L2["无法利用 B+ 树的有序性"]
L3["必须全表扫描"]
L4["性能随数据量线性下降"]
end
subgraph 倒排索引优势
I1["直接通过词典定位"]
I2["无需遍历所有数据"]
I3["性能与数据量关系不大"]
I4["毫秒级响应"]
end
八、写入流程与段合并# 8.1 倒排索引的写入挑战# 倒排索引设计为读优化 结构,写入面临挑战:
flowchart TB
subgraph 写入挑战
W1["新增文档"] --> U1["更新多个词的倒排表"]
W2["删除文档"] --> U2["标记删除,产生碎片"]
W3["更新文档"] --> U3["删除+新增,开销大"]
end
subgraph 原因
R1["一个词可能在多个文档中"]
R2["一个文档包含多个词"]
R3["写入需要更新多处"]
end
8.2 Lucene 的段机制# Lucene 采用写时复制 策略,将索引分为多个段(Segment):
flowchart TB
subgraph 段机制
W[写入请求] --> M[内存缓冲区]
M --> |刷盘| S1[Segment 1]
M --> |刷盘| S2[Segment 2]
M --> |刷盘| S3[Segment 3]
S1 --> Q[查询时合并所有段]
S2 --> Q
S3 --> Q
end
Note over S1,S3: 段不可变<br/>新增=新建段
段的特点 :
不可变 :段一旦写入,不再修改
追加写入 :新增文档创建新段
查询合并 :查询时合并所有段的结果
后台合并 :定期合并小段为大段
8.3 写入流程详解# sequenceDiagram
participant C as 客户端
participant N as Node
participant M as Memory Buffer
participant T as Transaction Log
participant S as Segment
C->>N: 写入文档
N->>M: 写入内存缓冲区
N->>T: 写入事务日志
Note over M,T: 异步刷新
M->>S: 缓冲区满或定时刷新
M->>M: 清空缓冲区
Note over S: 新段生成,可被搜索
T->>T: 定期清除已持久化日志
写入步骤 :
文档写入内存缓冲区(Memory Buffer)
同时写入事务日志(Translog)保证可靠性
缓冲区满或定时刷新到磁盘生成新段
新段立即可被搜索
8.4 段合并(Segment Merge)# 随着写入增多,段数量增加,查询性能下降:
flowchart TB
subgraph 合并前
S1["Segment 1<br/>100 docs"]
S2["Segment 2<br/>50 docs"]
S3["Segment 3<br/>30 docs"]
S4["Segment 4<br/>20 docs"]
end
subgraph 合并过程
M[合并线程] --> |读取| S1 & S2 & S3 & S4
S1 & S2 & S3 & S4 --> |合并| N[新 Segment<br/>200 docs]
end
subgraph 合并后
N --> F1["更少的段"]
N --> F2["更快的查询"]
N --> F3["回收删除空间"]
end
合并策略 :
# Tiered Merge Policy(分层合并策略)
def find_merge_candidates (segments):
for segment in sorted (segments, key = size):
if segment.size < merge_threshold:
candidates.append(segment)
if len (candidates) >= max_merge_count:
8.5 删除与更新的实现# flowchart TB
subgraph 删除操作
D[删除请求] --> B[标记 .del 文件]
B --> Q[查询时过滤]
Q --> M[合并时真正删除]
end
subgraph 更新操作
U[更新请求] --> D2[标记旧文档删除]
D2 --> I[写入新文档]
I --> S[新段包含新版本]
end
关键设计 :
操作 实现 特点 新增 直接写入新段 高效 删除 标记删除,合并时真正删除 避免修改原段 更新 删除旧版本 + 写入新版本 实际是两次操作 查询 合并所有段结果,过滤已删除文档 需要额外过滤开销
8.6 近实时搜索# Lucene 通过近实时搜索 (Near Real-Time Search)实现写入后立即可搜索:
flowchart TB
subgraph 传统方案
T1["写入"] --> T2["刷盘到 Segment"]
T2 --> T3["可搜索"]
Note over T1,T3: 延迟秒级
end
subgraph 近实时方案
N1["写入"] --> N2["内存 Buffer"]
N2 --> N3["刷新到内存 Segment"]
N3 --> N4["可搜索"]
N3 --> N5["后台刷到磁盘"]
Note over N1,N4: 延迟毫秒级
end
关键参数 :
"refresh_interval" : "1s" , // 内存段刷新间隔
"durability" : "async" // 异步刷盘提高性能
九、Elasticsearch 的架构整合# 9.1 整体架构# flowchart TB
subgraph Elasticsearch 节点
subgraph 写入路径
W[写入请求] --> B[Buffer]
B --> T[Translog]
B --> R[Refresh<br/>生成 Segment]
end
subgraph 索引结构
R --> SEG[Segments]
SEG --> FST[FST 词典]
FST --> TD[Term Dictionary]
TD --> PL[Posting Lists]
end
subgraph 读取路径
Q[查询请求] --> C[协调节点]
C --> S[分片查询]
S --> FST
PL --> SC[评分排序]
SC --> RES[返回结果]
end
end
9.2 关键设计决策# mindmap
root((Elasticsearch<br/>设计决策))
倒排索引
快速全文检索
多关键词求交集
相关性排序
FST 压缩
内存效率
前缀后缀共享
有序遍历
段机制
写入吞吐
近实时搜索
不可变设计
分布式
分片分布
副本容错
路由策略
9.3 适用场景分析#
场景 推荐度 原因 日志分析 写入密集、文本搜索、聚合分析 全文搜索 核心场景、相关性排序 监控指标 时序数据、聚合查询 商品搜索 多条件筛选、分词搜索 精确查询 不如数据库、无事务支持 高频更新 更新开销大、段合并压力 关系型数据 无 Join、数据冗余
十、总结与最佳实践# 10.1 倒排索引的核心价值# flowchart TB
subgraph 核心优势
A1["O(1) 关键词查找"]
A2["高效多条件求交集"]
A3["相关性评分排序"]
A4["丰富的文本处理能力"]
end
subgraph 设计权衡
W1["读优化 vs 写开销"]
W2["空间换时间"]
W3["不可变段 vs 更新成本"]
end
A1 --> W2
A2 --> W2
A3 --> W1
A4 --> W3
10.2 最佳实践建议# 索引设计 :
合理设置分片数量,避免过多或过少
选择合适的分词器,匹配业务语言特性
禁用不需要的字段索引,节省空间
使用 nested 类型处理数组对象
查询优化 :
使用 filter 代替 query 利用缓存
避免深度分页,使用 scroll 或 search_after
合理使用聚合,避免内存溢出
监控慢查询,优化查询语句
写入优化 :
批量写入减少请求开销
调整 refresh_interval 平衡实时性和性能
合理设置段合并策略
避免高频更新同一文档
10.3 与其他技术的对比#
技术 数据结构 适用场景 特点 MySQL B+ 树 精确查询、事务 ACID、关系模型 Redis 哈希表、跳表 缓存、键值存储 高性能、数据结构丰富 Elasticsearch 倒排索引 全文检索、日志分析 分布式、文本处理 MongoDB B 树 文档存储 灵活 Schema ClickHouse 列式存储 OLAP 分析 高压缩、快速聚合
10.4 核心要点回顾# flowchart TB
subgraph 倒排索引本质
N1["词到文档的反向映射"]
N2["空间换时间的经典案例"]
N3["读优化的数据结构"]
end
subgraph 关键技术
K1["FST 压缩词典"]
K2["BM25 相关性算法"]
K3["段机制处理写入"]
K4["分词器处理文本"]
end
subgraph 权衡取舍
T1["查询速度 vs 写入开销"]
T2["索引大小 vs 搜索能力"]
T3["实时性 vs 吞吐量"]
end
倒排索引是搜索引擎的基石,它通过反转文档与词的关系,实现了毫秒级的关键词查找。理解倒排索引的设计原理,有助于我们更好地使用 Elasticsearch,并在合适的场景做出正确的技术选型。