在企业级搜索引擎中,常用一个称为布隆过滤器(Bloom Filter)的算法来实现对已经抓取过的URL进行过滤。
布隆过滤器算法
我们经常要判断一个元素是否在一个集合里面,最直接的方法是将集合中的全部元素存储在计算机中,遇到一个新元素时,将它和集合中的元素直接比较。一般来讲,计算机中的集合是用哈希表(Hash Table)来存储的。它的好处是快速而准确,缺点是浪费存储空间。当集合比较小的时候,这个问题不显著,但是当集合巨大时,哈希表存储效率低的问题就显现出来了。
例如一个公众电子邮件的提供商总是需要过滤来自发件人的垃圾邮件。一个办法是记录下那些发垃圾的Email的地址。由于那些发件人不停地在注册新的地址,全世界至少 也有几十亿个发垃圾邮件的地址,将他们都存储起来需要大量的网路服务器。如果用哈希表(将每一个email地址对应成一个8字节的信息指纹,然后存入哈希表。由于哈希表存户效率一般只有50%,因此一个email地址需要占用16个字节,一亿个地址大约需要1.6GB )。因此存储几十亿个邮件地址可能需要几百个GB放入内存。
但是布隆过滤器只需要哈希表的1/8到1/4的大小就能够解决同样的问题。
算法描述
布隆过滤器是有巴顿·布隆于1970年提出来的,它实际上是一个很长的二进制向量和一系列随机映射函数。
建立过滤器
假设存储一亿个电子邮件的地址,需建立一个16亿二进制常量,即2亿字节的向量,然后将这个16亿个二进制位全部设置为0,对于每一个电子邮件地址X,用8个不同的随机数产生器(F1,F2,……F8)产生8个信息指纹(f1,f2,…….f8)。再用一个随机数产生器G把这8个信息指纹映射到1到16亿中的8个自然数g1,g2….g8。现在把这个8个位置的二进制位全部设为1。当我们对这个1亿个email地址都进行这样的处理后,一个针对这些email地址的布隆过滤器就建成了。
过滤器工作
对于电子邮件地址Y,我们用8个随机数产生器(F1,F2,……F8)对这个地址信息产生8个信息指纹S1,S2,….S8。然后将这8个指纹对应到布隆过滤器的8个二进制位,分别是T1,T2,…T8。如果Y在黑名单中,显然T1,T2,…..T8对用的8个二进制位一定是1,,这样在遇到任何黑名单中的电子邮件地址时,我们都能狗进行准确的判断。
不足之处
布隆过滤器会识别出所有在黑名单中的可疑地址,但是他也有极小的可能将一个不再黑名单中的邮件地址判定为黑名单,这种可能性很小。我们将它称之为误识概率。常见的补救办法是建立一个小的白名单,存储那些可能误判的邮件地址。
布隆过滤器Java实现
书上没有说所实现的接口visitedFrontier里面包含那几个函数,我也不能具体吧这个接口写出来,反正,不用继承接口,我们认为这是内部函数这也是一样的。
然后我将原来main函数里面的内容重新写了一个函数,然后在MAIN.main()里面调用,这样便于修改。
public class SimpleBloomFilter {
private static final int DEFAULT_SIZE = 2 << 24;
private static final int[] seeds = new int[]{7, 11, 13, 31, 37, 61};
private BitSet bits = new BitSet(DEFAULT_SIZE);
private SimpleHash[] func = new SimpleHash[seeds.length];
public SimpleBloomFilter() {
for (int i = 0; i < seeds.length; i++) {
func[i] = new SimpleHash(DEFAULT_SIZE, seeds[i]);
}
}
public void start(){
String value = "stone2083@yahoo.cn";
SimpleBloomFilter filter = new SimpleBloomFilter();
System.out.println(filter.contains(value));
filter.add(value);
System.out.println(filter.contains(value));
}
//覆盖方法,把URL添加进来
public void add(CrawlUrl value) {
if (value != null) add(value.getOriUrl());
}
private void add(String value) {
for (SimpleHash f : func) {
bits.set(f.hash(value), true);
}
}
public boolean contains(CrawlUrl value) {
return contains(value.getOriUrl());
}
private boolean contains(String value) {
if (value == null) return false;
boolean ret = true;
for (SimpleHash f : func) {
ret = ret && bits.get(f.hash(value));
}
return ret;
}
public static class SimpleHash {
private int cap;
private int seed;
public SimpleHash(int cap, int seed) {
this.cap = cap;
this.seed = seed;
}
public int hash(String value) {
int result = 0;
int len = value.length();
for (int i = 0; i < len; i++) {
result = seed * result + value.charAt(i);
}
return (cap - 1) & result;
}
}
}
输出结果
布隆过滤器误判率表
比率(items:bits) | 误判率(False-positive) |
---|---|
1:1 | 0.63212055882856 |
1:2 | 0.39957640089373 |
1:4 | 0.14689159766038 |
1:8 | 0.02157714146322 |
1:16 | 0.00046557303372 |
1:32 | 0.00000021167340 |
1:64 | 0.00000000000004 |
比较保守的实现是为每个URL分配4个字节,项目和位数的比是1:32,误判率是0.00000021167340。对于5000万数量级的URL,布隆过滤器只占用200MB,并且排重速度超快,一遍下来不到两分钟。