☰
28 搜索引擎索引更新:从 LSM Tree 到布隆过滤器的高效实现
2026/10/14 3:10:16 网站建设 项目流程

一、背景:压缩索引的更新难题

在搜索引擎中,为了节省存储空间,倒排索引通常会采用差值编码、前缀压缩等技术进行压缩。但这些压缩技术有一个致命问题:压缩依赖有序性。例如,差值编码需要文档 ID 按升序排列,前缀压缩需要词典保持有序。当新增网页时,插入新文档会打破原有有序性,导致所有后续编码需要重新计算,甚至需要重建整个索引,这在海量数据场景下几乎不可行。

二、核心问题:推倒重建还是增量插入?

传统的压缩索引更新面临两难选择:

  • 推倒重建:需要扫描全部网页,耗时数小时,完全无法应对搜索引擎每秒数万条的新页面涌入速度。
  • 增量插入:会导致压缩结构失效,修改代价极高,尤其是重度压缩的索引,修改一次就需要全部重算。

三、解决方案:LSM Tree 分层架构

LSM Tree(Log-Structured Merge Tree)通过将存储分为内存层和磁盘层,实现了不推倒重建就插入的高效索引更新。其核心架构如下:

1. 内存层:MemTable

  • 存储方式:新网页先写入内存中的 MemTable,不进行压缩,保证插入速度极快。
  • 数据结构:通常使用跳表(Skip List)或哈希表(Hash Table)。
    • 跳表:支持有序查询,O (log N) 的查询和插入时间复杂度。
    • 哈希表:支持精确查找,O (1) 的查找时间复杂度,但不支持范围查询。

2. 磁盘层:不可变段(SSTable)

  • 冻结机制:当 MemTable 写满后,将其冻结为只读状态,转换为有序结构(排序 + 编码),然后刷到磁盘生成不可变段(Segment File)。
  • 不可变特性:一旦写入磁盘,不可变段就不再修改,保证了数据的安全性和可并发性。

3. 后台归并:解决段膨胀问题

随着不断写入,磁盘上的不可变段会越来越多,导致查询需要遍历多个段,读放大加剧。因此需要后台归并线程,将多个小段合并成大段,合并时执行真正的删除和重新编码压缩,减少段数量和存储空间。

四、关键优化技术

1. 布隆过滤器:减少磁盘 I/O

布隆过滤器是一种概率型数据结构,用于快速判断一个元素是否在集合中。在 LSM Tree 中,每个不可变段都会附带一个布隆过滤器,查询时先通过布隆过滤器判断词是否在段中:

  • 如果布隆过滤器返回 “肯定不存在”,则直接跳过该段,避免不必要的磁盘读取。
  • 如果返回 “可能存在”,则再读取磁盘段进行精确查找。

2. 墓碑标记:软删除

删除文档时,不会直接从索引中移除,而是写入一条墓碑(Tombstone)标记。查询时遇到墓碑标记,会过滤掉该文档,不返回已删除的结果。在后台归并时,才会真正物理删除带有墓碑标记的文档。

五、代码实现示例

1. 布隆过滤器 Python 实现

import hashlib class BloomFilter: def __init__(self, size, num_hashes): self.size = size self.num_hashes = num_hashes self.bit_array = [False] * size def _hash(self, item, seed): return int(hashlib.md5((str(seed) + item).encode()).hexdigest(), 16) % self.size def add(self, item): for i in range(self.num_hashes): index = self._hash(item, i) self.bit_array[index] = True def contains(self, item): for i in range(self.num_hashes): index = self._hash(item, i) if not self.bit_array[index]: return False return True

2. MemTable(跳表)Python 实现

class SkipListNode: def __init__(self, key, value, level): self.key = key self.value = value self.forward = [None] * (level + 1) class SkipList: def __init__(self, max_level=16, p=0.5): self.max_level = max_level self.p = p self.head = SkipListNode(None, None, max_level) self.level = 0 def random_level(self): level = 0 while random.random() < self.p and level < self.max_level: level += 1 return level def insert(self, key, value): update = [None] * (self.max_level + 1) current = self.head for i in range(self.level, -1, -1): while current.forward[i] and current.forward[i].key < key: current = current.forward[i] update[i] = current current = current.forward[0] if current is None or current.key != key: new_level = self.random_level() if new_level > self.level: for i in range(self.level + 1, new_level + 1): update[i] = self.head self.level = new_level new_node = SkipListNode(key, value, new_level) for i in range(new_level + 1): new_node.forward[i] = update[i].forward[i] update[i].forward[i] = new_node else: current.value = value def search(self, key): current = self.head for i in range(self.level, -1, -1): while current.forward[i] and current.forward[i].key < key: current = current.forward[i] current = current.forward[0] if current and current.key == key: return current.value return None

六、总结

LSM Tree 通过分层架构,将随机写转换为顺序写,牺牲了部分读性能和空间性能,换取了极高的写入吞吐量。结合布隆过滤器、墓碑标记和后台归并等优化技术,实现了高效的索引更新,完美解决了搜索引擎中压缩索引的增量插入难题。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询