mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
3259 字
9 分钟
为什么 Elasticsearch 使用倒排索引
2024-03-25

搜索引擎能在毫秒级时间内从数十亿文档中找到包含特定关键词的内容,这背后依靠的核心数据结构就是倒排索引(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):
"""正排索引搜索:O(N×M)"""
results = []
for doc_id, words in forward_index.items():
if query in words:
results.append(doc_id)
return results

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列表

倒排索引的结构

{
"elasticsearch": [1, 3],
"搜索": [1],
"引擎": [1],
"数据库": [2],
"mysql": [2],
"索引": [2],
"性能": [3],
"优化": [3]
}

查询方式:直接通过词定位文档列表。

def search_inverted_index(query, inverted_index):
"""倒排索引搜索:O(1)"""
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: 倒着用词找文档

理解方式

  1. 正排:顺着文档的顺序,文档在前、词在后
  2. 倒排:把关系倒过来,词在前、文档在后

三、倒排索引的详细结构#

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: 磁盘存储,节省内存

三层结构

  1. Term Index:内存中的前缀索引,快速定位词典块
  2. Term Dictionary:磁盘上的有序词典,存储完整词项
  3. 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 的特点

  1. 前缀共享:相同前缀只存储一次
  2. 后缀共享:相同后缀也可共享
  3. 有序存储:词项有序,支持二分查找
  4. 输出函数:每个词项关联一个输出值

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) hashO(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):
# 1. 在 FST 中查找前缀
block = fst.search(term[:prefix_len])
# 2. 定位到磁盘块
block_data = disk.read(block)
# 3. 在块中二分查找
term_info = binary_search(block_data, term)
# 4. 获取倒排表指针
posting_list = read_posting(term_info.pointer)
return posting_list

五、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

公式

TF-IDF(t,d)=TF(t,d)×IDF(t)TF\text{-}IDF(t, d) = TF(t, d) \times IDF(t)

其中:

  • TF(t,d)=t在文档d中的出现次数文档d的总词数TF(t, d) = \frac{\text{词}t\text{在文档}d\text{中的出现次数}}{\text{文档}d\text{的总词数}}
  • IDF(t)=log文档总数包含词t的文档数IDF(t) = \log\frac{\text{文档总数}}{\text{包含词}t\text{的文档数}}

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 公式

BM25(d,q)=i=1nIDF(qi)×f(qi,d)×(k1+1)f(qi,d)+k1×(1b+b×davgdl)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})}

参数说明:

| 参数 | 含义 | 默认值 | | ----------- | ---------------------------- | ------ | --------------- | --- | | f(qi,d)f(q_i, d) | 词 qiq_i 在文档 dd 中的频率 | - | | d | d | | 文档 dd 的长度 | - | | avgdlavgdl | 平均文档长度 | - | | k1k_1 | 词频饱和参数,控制饱和速度 | 1.2 | | bb | 文档长度归一化参数 | 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 参数配置
PUT /my_index
{
"settings": {
"index": {
"similarity": {
"my_bm25": {
"type": "BM25",
"k1": 1.2, # 词频饱和参数
"b": 0.75 # 文档长度归一化参数
}
}
}
}
}

参数调优建议

场景k1 建议b 建议原因
短文本(标题)1.2-1.50.3-0.5文档长度差异小,减少归一化
长文本(文章)1.2-1.80.7-0.9需要更强的长度归一化
专业领域1.5-2.00.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

三个阶段

  1. Character Filters:预处理,如去除 HTML 标签
  2. Tokenizer:分词,将文本切分为词项
  3. 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

中文分词示例

# IK 分词器两种模式
text = "中华人民共和国"
# smart 模式(智能分词)
smart_result = ["中华人民共和国"] # 作为一个整体
# max_word 模式(最细粒度)
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 为什么数据库不适合全文检索?#

-- 数据库的 LIKE 查询
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/>新增=新建段

段的特点

  1. 不可变:段一旦写入,不再修改
  2. 追加写入:新增文档创建新段
  3. 查询合并:查询时合并所有段的结果
  4. 后台合并:定期合并小段为大段

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: 定期清除已持久化日志

写入步骤

  1. 文档写入内存缓冲区(Memory Buffer)
  2. 同时写入事务日志(Translog)保证可靠性
  3. 缓冲区满或定时刷新到磁盘生成新段
  4. 新段立即可被搜索

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):
"""
选择合并候选:
1. 段数量超过阈值触发合并
2. 优先合并小段
3. 避免合并大段(开销大)
"""
candidates = []
for segment in sorted(segments, key=size):
if segment.size < merge_threshold:
candidates.append(segment)
if len(candidates) >= max_merge_count:
break
return candidates

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", // 内存段刷新间隔
"translog": {
"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 最佳实践建议#

索引设计

  1. 合理设置分片数量,避免过多或过少
  2. 选择合适的分词器,匹配业务语言特性
  3. 禁用不需要的字段索引,节省空间
  4. 使用 nested 类型处理数组对象

查询优化

  1. 使用 filter 代替 query 利用缓存
  2. 避免深度分页,使用 scroll 或 search_after
  3. 合理使用聚合,避免内存溢出
  4. 监控慢查询,优化查询语句

写入优化

  1. 批量写入减少请求开销
  2. 调整 refresh_interval 平衡实时性和性能
  3. 合理设置段合并策略
  4. 避免高频更新同一文档

10.3 与其他技术的对比#

技术数据结构适用场景特点
MySQLB+ 树精确查询、事务ACID、关系模型
Redis哈希表、跳表缓存、键值存储高性能、数据结构丰富
Elasticsearch倒排索引全文检索、日志分析分布式、文本处理
MongoDBB 树文档存储灵活 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,并在合适的场景做出正确的技术选型。

参考引用#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

为什么 Elasticsearch 使用倒排索引
https://blog.souloss.com/posts/why-the-design/why-elasticsearch-uses-inverted-index/
作者
Souloss
发布于
2024-03-25
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时