首先介绍simHash
simhash算法分为5个步骤:分词、hash、加权、合并、降维
直接给例子,理解的更加生动些吧:https://blog.csdn.net/chinawangfei/article/details/82385842
-
1:分词。首先,判断文本分词,形成这个文章的特征单词。然后,形成去掉噪音词的单词序列。最后,为每个分词加上权重。我们假设权重分为5个级别(1~5),比如:“ 美国“51区”雇员称内部有9架飞碟,曾看见灰色外星人 ” ==> 分词后为 “ 美国(4) 51区(5) 雇员(3) 称(1) 内部(2) 有(1) 9架(3) 飞碟(5) 曾(1) 看见(3) 灰色(4) 外星人(5)”,括号里是代表单词在整个句子里重要程度,数字越大越重要。
-
2:hash。通过hash算法把每个词变成hash值,比如“美国”通过hash算法计算为 100101,“51区”通过hash算法计算为 101011。这样,我们的字符串就变成了一串串数字,还记得文章开头说过的吗?要把文章变为数字计算,才能提高相似度计算性能,现在是降维过程进行时。
-
3:加权。在第2步骤hash生成结果后,需要按照单词的权重形成加权数字串,比如“美国”的hash值为“100101”,通过加权计算为“4 -4 -4 4 -4 4”;“51区”的hash值为“101011”,通过加权计算为 “ 5 -5 5 -5 5 5”。
-
4:合并。把上面各个单词算出来的序列值累加,变成只有一个序列串。比如 “美国”的 “4 -4 -4 4 -4 4”,“51区”的 “ 5 -5 5 -5 5 5”, 把每一位进行累加, “4+5 -4+-5 -4+5 4+-5 -4+5 4+5” ==》 “9 -9 1 -1 1 9”。这里作为示例只算了两个单词的,真实的计算需要把所有单词的序列串累加。
-
5:降维。把第4步算出来的 “9 -9 1 -1 1 9” 变成 0 1 串,形成我们最终的simhash签名。 如果每一位大于0 记为 1,小于或等于0 则记为 0。最后算出结果为:“1 0 1 0 1 1”。
这个例子完了之后其中对于hash部分还是很懵逼,我很是想知道这个hash算法咋给计算的,为啥子要这么计算,因为在代码里就长这样了:
private BigInteger hash(String source) {
if (source == null || source.length() == 0) {
return new BigInteger("0");
} else {
char[] sourceArray = source.toCharArray();
BigInteger x = BigInteger.valueOf(((long) sourceArray[0]) << 7);
BigInteger m = new BigInteger("1000003");
BigInteger mask = new BigInteger("2").pow(this.hashBits).subtract(
new BigInteger("1"));
for (char item : sourceArray) {
BigInteger temp = BigInteger.valueOf((long) item);
x = x.multiply(m).xor(temp).and(mask);
}
x = x.xor(new BigInteger(String.valueOf(source.length())));
if (x.equals(new BigInteger("-1"))) {
x = new BigInteger("-2");
}
return x;
}
}
public BigInteger simHash() {
int[] v = new int[this.hashBits];
List<String> words = cutSentenceToWords(sentence);
for (String word : words) {
BigInteger t = this.hash(word);
for (int i = 0; i < this.hashBits; i++) {
BigInteger bitmask = new BigInteger("1").shiftLeft(i);
if (t.and(bitmask).signum() != 0) {
v[i] += 1;
} else {
v[i] -= 1;
}
}
}
BigInteger fingerprint = new BigInteger("0");
for (int i = 0; i < this.hashBits; i++) {
if (v[i] >= 0) {
fingerprint = fingerprint.add(new BigInteger("1").shiftLeft(i));
}
}
return fingerprint;
}
接下来介绍大数据量下存储和查询
到这里相似度问题基本解决,但是按这个思路,在海量数据几百亿的数量下,效率问题还是没有解决的,因为数据是不断添加进来的,不可能每来一条数据,都要和全库的数据做一次比较,按照这种思路,处理速度会越来越慢,线性增长。
可以参考这个:http://blog.itpub.net/69901774/viewspace-2675029/
抽屉原理,也称鸽巢原理。下面我们简单举例说一下: 桌子上有四个苹果,但只有三个抽屉,如果要将四个苹果放入三个抽屉里,那么必然有一个抽屉中放入了两个苹果。如果每个抽屉代表一个集合,每一个苹果就可以代表一个元素,假如有n+1个元素放到n个集合中去,其中必定有一个集合里至少有两个元素。 抽屉原理就是这么简单,那如果用它来解决我们海量数据的遍历问题呢? 针对海量数据的去重效率,我们可以将64位指纹,切分为4份16位的数据块,根据抽屉原理在海明距离为3的情况,如果两个文档相似,那么它必有一个块的数据是相等的。 那也就是说,我们可以以某文本的 SimHash 的每个16位截断指纹为 Key,Value 为 Key 相等时文本的 SimHash 集合存入 K-V 数据库即可,查询时候,精确匹配这个指纹的4个16位截断指纹所对应的4个 SimHash 集合即可。 如此,假设样本库,有2^37 条数据(1375亿数据),假设数据均匀分布,则每个16位(16个01数字随机组成的组合为2^16 个)倒排返回的最大数量为 |
数据压缩
如果再加上 Snappy 压缩呢?
如果再加上 Fast-Diff 编码呢?
如果再开启 Mob 对象存储呢? 每个 Set 是不是可以存10万个键值对?每行只需90个 Set 集合。
这些只是存储空间上面的,但是还有更加合适的方式,就是要结合业务
例如:
1.我球所处的业务是股票的社区讨论,股票的实时性很重要的,这个也是有人在公众号上问过的,为什么抖音上面没有关于股票介绍的(原因就是根据抖音的实时热点分发,当她把内容投递到用户的时候,没准这个观点已经不再适应当时的语义环境)
2.那么实时性结合严重的话,我们就只存储近7天的文章进行相似性的比对
3.文章的属性和simhash的key-value相互的反向存储根本也就占用不了多少资源
然后来介绍下工程实现
代码:https://github.com/singgel/simple-simhash
参考资料
1.https://blog.csdn.net/u010454030/article/details/49102565
2.http://www.lanceyan.com/tech/arch/simhash_hamming_distance_similarity2-html.html
3.https://cloud.tencent.com/developer/news/218062
4.https://blog.csdn.net/qq_36142114/article/details/80540303
5.https://blog.csdn.net/u011467621/article/details/49685107
6.http://blog.itpub.net/69901774/viewspace-2675029/
7.https://blog.csdn.net/u011467621/article/details/49685107