和其他局部敏感哈希函数一样,如果两个文档的 SimHash 值的海明距离小于 k,我们就认为它们是相似的。举个例子,在 Google 的实现中,k 的取值为 3。这个时候,检索相似文章的问题变成了要找出海明距离在 3 之内的所有文档。如果是一个个文档比对的话,这就是一个遍历过程,效率很低。有没有更高效的检索方案呢?
一个直观的想法是,我们可以针对每一个比特位做索引。由于每个比特位只有 0 和 1 这 2 个值,一共有 64 个比特位,也就一共有 2*64 共 128 个不同的 Key。因此我们可以使用倒排索引,将所有的文档根据自己每个比特位的值,加入到对应的倒排索引的 posting list 中。这样,当要查询和一个文档相似的其他文档的时候,我们只需要通过 3 步就可以实现了,具体的步骤如下:
- 计算出待查询文档的 SimHash 值;
- 以该 SimHash 值中每个比特位的值作为 Key,去倒排索引中查询,将相同位置具有相同值的文档都召回;
- 合并这些文档,并一一判断它们和要查询的文档之间的海明距离是否在 3 之内,留下满足条件的。
我们发现,在这个过程中,只要有一个比特位的值相同,文档就会被召回。也就是说,这个方案和遍历所有文档相比,其实只能排除掉「比特位完全不同的文档」。因此,这种方法的检索效率并不高。
这又该怎么优化呢?Google 利用 抽屉原理 设计了一个更高效的检索方法。什么是抽屉原理呢?简单来说,如果我们有 3 个苹果要放入 4 个抽屉,就至少有一个抽屉会是空的。那应用到检索上,Google 会将哈希值平均切为 4 段,如果两个哈希值的比特位差异不超过 3 个,那这三个差异的比特位最多出现在 3 个段中,也就是说至少有一个段的比特位是完全相同的!因此,我们可以将前面的查询优化为「有一段比特位完全相同的文档会被召回」。
根据这个思路,我们可以将每一个文档都根据比特位划分为 4 段,以每一段的 16 个比特位的值作为 Key,建立 4 个倒排索引。检索的时候,我们会把要查询文档的 SimHash 值也分为 4 段,然后分别去对应的倒排索引中,查询和自己这一段比特位完全相同的文档。最后,将返回的四个 posting list 合并,并一一判断它们的的海明距离是否在 3 之内。
通过使用 SimHash 函数和分段检索(抽屉原理),使得 Google 能在百亿级别的网页中快速完成过滤相似网页的功能,从而保证了搜索结果的质量。