simhash核心步骤是分词、加权、降维、汉明距离比较;需用md5/sha1+权重累加生成64位指纹,中文依赖jieba等分词器,汉明距离用__builtin_popcountll()高效计算,海量场景须结合lsh分桶优化。

SimHash核心步骤:分词、加权、降维、汉明距离比较
SimHash不是直接比文本,而是把每篇文本映射成一个64位(或其它固定长度)的整数指纹,相似文本的指纹汉明距离小。关键在“怎么生成这个指纹”——不是哈希函数随便一算,而是要保留语义相似性。C++里没有现成的simhash库,得自己搭骨架。
典型流程是:分词 → 提取特征词(如TF-IDF前N个)→ 对每个词计算MD5/SHA1再取前64位 → 按权重对每一位累加(+1/-1)→ 符号位转0/1得最终指纹。注意:分词质量直接影响效果,中文必须先过jieba或THULAC;英文可直接空格+停用词过滤。
- 别用
std::hash<:string></:string>替代特征哈希——它不一致(不同编译器/进程结果不同),必须用std::md5(C++23前需第三方)或openssl/md5.h - 权重建议用
log(1 + tf) * idf,idf从样本集预估,不能全用tf - 降维时,对64维向量每位求和后,正数填1、负数填0,最后用
uint64_t打包——别用bitset转整数,移位顺序容易错
汉明距离计算:用__builtin_popcountll()加速
两个uint64_t指纹异或后,1的个数就是汉明距离。C++标准库没提供高效汉明计数,但GCC/Clang支持__builtin_popcountll(),单指令搞定,比循环移位快10倍以上。
示例:
uint64_t simhash_a = /* ... */; uint64_t simhash_b = /* ... */; int distance = __builtin_popcountll(simhash_a ^ simhash_b); if (distance
- MSVC用户得换
__popcnt64(),且需开启/arch:AVX2,否则回退到慢实现 - 别写
while (x) { cnt += x & 1; x >>= 1; }——64次循环太伤,尤其查重场景要批量比对 - 如果阈值固定(如只接受≤3),可用查表法:把异或结果拆成8个byte,查256项静态数组,但空间换时间,一般没必要
海量文本查重:不能两两暴力比,得用桶索引
100万文本两两比要 ~5×10¹¹ 次运算,完全不可行。SimHash真正能落地,靠的是“局部敏感哈希(LSH)”思想:把64位指纹按固定位置分段(如每4位一段,共16段),只要有一段完全相同,就进同一个桶。这样只需比同桶内文本。
例如分4段(每段16位),构造std::unordered_map<uint16_t std::vector>></uint16_t> 4次,每次用不同掩码提取段值:
uint16_t seg0 = (fingerprint >> 0) & 0xFFFF; uint16_t seg1 = (fingerprint >> 16) & 0xFFFF; // ...
- 段数越多,漏判越少但桶越多;段数越少,误判上升但内存可控。实测100万中文新闻,分4~6段较稳
- 别用
std::map——插入慢;也别把整个指纹当key(桶太多),必须切段 - 桶内仍需计算汉明距离,但数量从O(N²)降到平均O(K×M),K是桶大小,M是桶数量
实际部署要注意的三个坑
算法写对只是开始,线上跑起来才发现问题:
- 中文分词不统一:训练idf用jieba,线上用THULAC,特征词对不上——所有环节必须用同一套分词器+词典
- 指纹生成没归一化:同一篇文本因标点、空格、大小写差异生成不同指纹——预处理必须做
to_lower()、去多余空白、统一引号 - 64位指纹用
long long在Windows上是signed,右移是算术移位,导致高位补1出错——一律用uint64_t,移位前强转
最麻烦的其实是idf更新:新文本进来,idf变,老指纹就得重算。生产环境通常冻结idf,或定期全量重刷指纹。这点容易被忽略,直到发现召回率突然掉一半。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











