1#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
12pub struct SimHashFingerprint(pub u64);
13
14const DEFAULT_SHINGLE_SIZE: usize = 4;
15
16#[inline]
22pub fn fingerprint(text: &str, shingle_size: usize) -> SimHashFingerprint {
23 let chars: Vec<char> = text.chars().collect();
24 let mut shingles = Vec::new();
25
26 if chars.len() < shingle_size {
27 shingles.push(text.to_string());
28 } else {
29 for w in chars.windows(shingle_size) {
31 shingles.push(w.iter().collect::<String>());
32 }
33 }
34
35 let mut v = [0i64; 64];
37
38 for shingle in &shingles {
39 let h = fxhash64(shingle);
40 for (i, val) in v.iter_mut().enumerate() {
41 if (h >> i) & 1 == 1 {
42 *val += 1;
43 } else {
44 *val -= 1;
45 }
46 }
47 }
48
49 let mut fp: u64 = 0;
51 for (i, val) in v.iter().enumerate() {
52 if *val > 0 {
53 fp |= 1 << i;
54 }
55 }
56
57 SimHashFingerprint(fp)
58}
59
60#[inline]
62pub fn fingerprint_default(text: &str) -> SimHashFingerprint {
63 fingerprint(text, DEFAULT_SHINGLE_SIZE)
64}
65
66#[inline]
70pub fn hamming_distance(a: SimHashFingerprint, b: SimHashFingerprint) -> u32 {
71 (a.0 ^ b.0).count_ones()
72}
73
74#[inline]
78pub fn similarity(a: SimHashFingerprint, b: SimHashFingerprint) -> f64 {
79 1.0 - hamming_distance(a, b) as f64 / 64.0
80}
81
82#[inline]
84pub fn compare(a: &str, b: &str, shingle_size: usize) -> f64 {
85 let fp_a = fingerprint(a, shingle_size);
86 let fp_b = fingerprint(b, shingle_size);
87 similarity(fp_a, fp_b)
88}
89
90#[inline]
92pub fn compare_default(a: &str, b: &str) -> f64 {
93 compare(a, b, DEFAULT_SHINGLE_SIZE)
94}
95
96fn fxhash64(s: &str) -> u64 {
98 let mut hash: u64 = 0xcbf29ce484222325;
99 for b in s.bytes() {
100 hash ^= b as u64;
101 hash = hash.wrapping_mul(0x100000001b3);
102 }
103 hash
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109
110 #[test]
111 fn identical_documents() {
112 let s = compare_default("hello world this is a test", "hello world this is a test");
113 assert!(
114 (s - 1.0).abs() < f64::EPSILON,
115 "identical should be 1.0, got {s}"
116 );
117 }
118
119 #[test]
120 fn similar_documents() {
121 let s = compare_default(
122 "the quick brown fox jumps over the lazy dog",
123 "the quick brown fox jumps over the lazy cat",
124 );
125 assert!(s > 0.75, "expected high similarity, got {s}");
126 }
127
128 #[test]
129 fn different_documents() {
130 let fp_a = fingerprint_default("completely unrelated content one");
131 let fp_b = fingerprint_default("totally different text here two");
132 let d = hamming_distance(fp_a, fp_b);
133 assert!(d > 20, "expected at least 20 bits different, got {d}");
134 }
135
136 #[test]
137 fn symmetric() {
138 let a = compare_default("abc def", "abc xyz");
139 let b = compare_default("abc xyz", "abc def");
140 assert!((a - b).abs() < f64::EPSILON);
141 }
142
143 #[test]
144 fn small_text() {
145 let fp = fingerprint("hi", 4);
146 assert_ne!(fp.0, 0);
148 }
149}