用Python从零开始实现一个Bloomfilter

来源:互联网 发布:android 仿照淘宝排序 编辑:程序博客网 时间:2024/06/07 08:17

简介

如果你不知道什么是 Bloomfilter,可以在这里找到详尽的描述Bloomfilter 介绍。简单来说Bloomfilter是一个概率数据结构,功能上类似于集合的一个子集,可以向里面添加一个元素,或者判断一个元素是否在其中。不过你只能准确判断一个数据不在其中,对于那些Bloomfilter判定其中的元素,只能保证它有非常大的概率在其中(这个概率一般高达99.9%+)。

需要什么样的功能接口?

Bloomfilter需要存储输入数据的某种状态,每当向其中添加一个元素,它的状态就会发生变化,所以可以实现为一个类,用字节数组来保存状态。然后来考虑其初始化方法,一个Bloomfilter有三个参数,分别是输入数据规模n,字节数组大小m以及可以接受的错误率k(即错误率上限)
Bloomfilter 有两个主要的功能,添加一个元素的 add 和 测试一个元素是否在里面的 test,但是这个方法可以利用Python关键字 in更好的实现。

# bloomfilter.pyclass Bloomfilter(object):    def __init__(self, m, n, k):        pass    def add(self, element):        pass    def __contains__self, element):        pass

用起来大概是这样

>>> from fastbloom import BloomFilter>>> bf = BloomFilter() # 创建>>> bf.add('http://www.github.com') # 添加元素>>> 'http://www.github.com' in bf # 测试一个元素是否在其中>>> True

需要什么样的底层支撑?

Bloomfilter 最大的优点就是内存占用小,带来的额外开销就是运行时间变长,所以其实从通用的角度讲只有一条标准,那就是尽可能的快
而Bloomfilter实际上由两部分组成:一个是作为实际存储空间的字节数组,因为实际的数组会非常大,所以需要能够快速的插入和查询;而另一个就是对输入元素进行映射的哈希函数,由于每一次插入和查询操作都需要用到这个函数,所以它的性能至关重要。

字节数组

这里用mmap作为底层存储,关于mmap你可以看这篇博客『认真分析mmap:是什么 为什么 怎么用』。它一大的好处就是对于内存的高效访问,同时在Python里的mmap模块实际实现使用C写的,所以可以大幅减少运行时间。但是mmap本身提供的接口太原始,所以需要对其进行封装。
实际上,我们所需要的就是一个比特数组,然后可以在这个比特数组上随机的进行访问和修改,所以实现了一个基于mmap的bitset。主要是实现了两个方法,一个是写入一个指定比特,另一个是测试一个指定比特是否为1。

# bitset.pyimport mmapPAGE_SIZE = 4096Byte_SIZE = 8class MmapBitSet(object):    def __init__(self, size):        byte_size = ((size / 8) / PAGE_SIZE + 1) * PAGE_SIZE        self._data_store = mmap.mmap(-1, byte_size)        self._size = byte_size * 8    def _write_byte(self, pos, byte):        self._data_store.seek(pos)        self._data_store.write_byte(byte)    def _read_byte(self, pos):        self._data_store.seek(pos)        return self._data_store.read_byte()    def set(self, pos, val=True):        assert isinstance(pos, int)        if pos < 0 or pos > self._size:            raise ValueError('Invalid value bit {bit}, '                             'should between {start} - {end}'.format(bit=pos,                                                                     start=0,                                                                     end=self._size))        byte_no = pos / Byte_SIZE        inside_byte_no = pos % Byte_SIZE        raw_byte = ord(self._read_byte(byte_no))        if val:  # set to 1            set_byte = raw_byte | (2 ** inside_byte_no)        else:  # set to 0            set_byte = raw_byte & (2 ** Byte_SIZE - 1 - 2 ** inside_byte_no)        if set_byte == raw_byte:            return        set_byte_char = chr(set_byte)        self._write_byte(byte_no, set_byte_char)    def test(self, pos):        byte_no = pos / Byte_SIZE        inside_byte_no = pos % Byte_SIZE        raw_byte = ord(self._read_byte(byte_no))        bit = raw_byte & 2 ** inside_byte_no        return True if bit else False

哈希函数

在哈希函数的选取上,由于Bloomfilter的特性需要快速,所以所有基于密钥的哈希算法都被排除在外,这里选择的是Murmur哈希和Spooky哈希,这两个是目前性能最好的字符串哈希函数之一,这里直接使用的pyhash的实现,因为它使用boost写的所以性能比较好。同时,本次实现的Bloomfilter支持对哈希函数的替换,只需要满足如下规则:
- 一个函数,接收字符串为参数,返回一个128 bit 的数字;
- 一个类,实现了call方法,其余同上。
除此之外,由于在实际的Bloomfilter中会用到多个哈希函数,而它们的数量又是不确定的,这里我们使用一个叫做 double hashing 的方法来产生互不相关的hash函数,可以得到和多个完全不同的哈希函数同样的性能。即new_hash = h1 + i * h2其中 i 为正整数。实现如下,非常简单:

# hash_tools.pydef double_hashing(delta, h1, h2):    def new_hash(msg):        return h1(msg) + delta * h2(msg)    return new_hash

在生成一系列哈希函数时,由于对于给定的输入,h1和h2的输出值是确定的,每一个哈希函数值之间只是相差了一个delta权值。所以不需要每次单独计算多个哈希函数,只需要计算两个哈希值并产生多个哈希值即可。

# hash_tools.pydef hashes(msg, h1, h2, number):    h1_value, h2_value = h1(msg), h2(msg)    result = []    for i in xrange(number):        yield ( h1_value + i*h2_value )

回到Bloomfilter

参数的确定

到目前为止还有一个悬而未决的问题,那就是Bloomfilter有三个参数,需要如何确定呢?其实这取决于你对运行速度,内存占用以及错误率的综合考量,在不同情况下可以采用不同的方法。实际上错误率为(1-e-kn/m)k,可以通过这个式子,对参数进行确定。
而本次实现中采用的是官方推荐的方法,通过确定n和m来求最优的k=(m/n)ln(2),然后不断迭代使得错误率达到要求,这样可以使得占用空间m最小,具体实现如下:

# bloomfilter.pyclass Bloomfilter(object):    def _adjust_param(self, bits_size, expected_error_rate):        n, estimated_m, estimated_k, error_rate = self.capacity, int(bits_size / 2), None, 1        weight, e = math.log(2), math.exp(1)        while error_rate > expected_error_rate:            estimated_m *= 2            estimated_k = int((float(estimated_m) / n) * weight) + 1            error_rate = (1 - math.exp(- (estimated_k * n) / estimated_m)) ** estimated_k        return estimated_m, estimated_k

实现Bloomfilter的接口

最后剩下的就只有一开始设计的几个接口了,有了前面的基础,其实已经比较简单了,只是简单的插入和查询操作。

# bloomfilter.pyclass Bloomfilter(Object):    def add(self, msg):        if not isinstance(msg, str):            msg = str(msg)        positions = []        for _hash_value in self._hashes(msg):            positions.append(_hash_value % self.num_of_bits)        for pos in sorted(positions):            self._data_store.set(int(pos))    def __contains__(self, msg):        if not isinstance(msg, str):            msg = str(msg)        positions = []        for _hash_value in self._hashes(msg):            positions.append(_hash_value % self.num_of_bits)        for position in sorted(positions):            if not self._data_store.test(position):                return False        return True

完整代码 Github pybloomfilter