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

相关推荐
Dev_XH4 分钟前
【成品设计】基于单片机的工厂设备能耗监控与优化系统
stm32·单片机·嵌入式硬件·物联网
不脱发的程序猿33 分钟前
STM32制作单手鼠标键盘
stm32·嵌入式硬件·计算机外设·usb·qmk
其实防守也摸鱼13 小时前
个人学习 Pwn 思路:从基础到漏洞利用的进阶路径
大数据·前端·人工智能·chrome·算法·安全·网络安全
Nuanyt13 小时前
LeetCode Hot 100 链表专题(五)· 2 题(Java)
java·算法·leetcode·链表
w_zero_one16 小时前
递归算法(一)
java·开发语言·算法
路上有只喵17 小时前
认识元器件 · 单片机(MCU/MPU/SOC) · 实战篇 11
单片机·嵌入式硬件
不昀17 小时前
开关电源电流互感器怎么选:匝比、采样电阻与复位的计算笔记
单片机·嵌入式硬件·电子元器件
风影同志7832418 小时前
20 个比特能装下多少行情?——实测拆解 Kronos 的 K 线分词器
算法
写的都是BUG18 小时前
APK 里的钥匙,一直就放在门口:聊聊加固真正该做什么
前端·算法