28 搜索引擎索引更新:从 LSM Tree 到布隆过滤器的高效实现

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

在搜索引擎中,为了节省存储空间,倒排索引通常会采用差值编码、前缀压缩等技术进行压缩。但这些压缩技术有一个致命问题:压缩依赖有序性。例如,差值编码需要文档 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
  1. 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 通过分层架构,将随机写转换为顺序写,牺牲了部分读性能和空间性能,换取了极高的写入吞吐量。结合布隆过滤器、墓碑标记和后台归并等优化技术,实现了高效的索引更新,完美解决了搜索引擎中压缩索引的增量插入难题。

相关推荐
霖霖7141 小时前
【ESP32笔记】freeRTOS使用教学
笔记·单片机·mcu·物联网
我命由我123451 小时前
LRU 缓存淘汰策略
数据结构·redis·学习·mysql·算法·缓存·学习方法
垆边人似月.1 小时前
华为题:合并区间
数据结构·c++·算法
垆边人似月.2 小时前
华为题:滑动窗口最大值
数据结构·c++·算法
2601_965742222 小时前
AI是怎么判断一家本地商家值得推荐的?云南老板看完这篇就懂了
大数据·人工智能·算法·ai·新媒体运营
小O的算法实验室2 小时前
IEEE TAES,基于DQN 辅助的集群无人机自组网约束多目标重构与优化
人工智能·算法
尼喃2 小时前
高压输入60V降压转5V,3.3V,12V电源芯片设计要点
嵌入式硬件
_不会dp不改名_2 小时前
leetcode_3727 最大平方交替和
linux·算法·leetcode
Zw-awa2 小时前
PID 的三个参数之外:位置式、增量式和反馈控制
stm32