用Python從零開始實現一個Bloomfilter

簡介

如果你不知道什麼是 Bloomfilter,可以在這裏找到詳盡的描述Bloomfilter 介紹。簡單來說Bloomfilter是一個概率數據結構,功能上類似於集合的一個子集,可以向裏面添加一個元素,或者判斷一個元素是否在其中。不過你只能準確判斷一個數據不在其中,對於那些Bloomfilter判定其中的元素,只能保證它有非常大的概率在其中(這個概率一般高達99.9%+)。

需要什麼樣的功能接口?

Bloomfilter需要存儲輸入數據的某種狀態,每當向其中添加一個元素,它的狀態就會發生變化,所以可以實現爲一個類,用字節數組來保存狀態。然後來考慮其初始化方法,一個Bloomfilter有三個參數,分別是輸入數據規模n,字節數組大小m以及可以接受的錯誤率k(即錯誤率上限)
Bloomfilter 有兩個主要的功能,添加一個元素的 add 和 測試一個元素是否在裏面的 test,但是這個方法可以利用Python關鍵字 in更好的實現。

# bloomfilter.py

class 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.py

import mmap


PAGE_SIZE = 4096
Byte_SIZE = 8


class 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.py

def double_hashing(delta, h1, h2):
    def new_hash(msg):
        return h1(msg) + delta * h2(msg)
    return new_hash

在生成一系列哈希函數時,由於對於給定的輸入,h1和h2的輸出值是確定的,每一個哈希函數值之間只是相差了一個delta權值。所以不需要每次單獨計算多個哈希函數,只需要計算兩個哈希值併產生多個哈希值即可。

# hash_tools.py

def 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.py

class 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.py

class 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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章