1use std::collections::HashSet;
10
11const DEFAULT_NUM_HASHES: usize = 128;
12const DEFAULT_SHINGLE_SIZE: usize = 3;
13
14#[derive(Clone, Debug)]
20pub struct MinHash {
21 pub signatures: Vec<u64>,
23 pub num_hashes: usize,
25}
26
27fn generate_hash_coeffs(num_hashes: usize) -> (Vec<u64>, Vec<u64>) {
32 let seed: u64 = 0x9e3779b97f4a7c15;
35 let mut a_coeffs = Vec::with_capacity(num_hashes);
36 let mut b_coeffs = Vec::with_capacity(num_hashes);
37
38 for i in 0..num_hashes {
39 let a = seed.wrapping_mul(i as u64 + 1).wrapping_add(0xABCD);
41 let b = seed.wrapping_mul(i as u64 + 1).wrapping_add(0x1234);
42 a_coeffs.push(a | 1); b_coeffs.push(b);
44 }
45
46 (a_coeffs, b_coeffs)
47}
48
49#[inline]
58pub fn signature(text: &str, shingle_size: usize, num_hashes: usize) -> MinHash {
59 let chars: Vec<char> = text.chars().collect();
61 let mut shingles = HashSet::new();
62
63 if chars.len() < shingle_size {
64 shingles.insert(text.to_string());
65 } else {
66 for w in chars.windows(shingle_size) {
67 shingles.insert(w.iter().collect::<String>());
68 }
69 }
70
71 let (a_coeffs, b_coeffs) = generate_hash_coeffs(num_hashes);
72 const P: u64 = (1 << 61) - 1;
74
75 let mut signatures = vec![u64::MAX; num_hashes];
76
77 for shingle in &shingles {
78 let shingle_hash = fxhash(shingle);
79
80 for i in 0..num_hashes {
81 let hash_val = a_coeffs[i]
82 .wrapping_mul(shingle_hash)
83 .wrapping_add(b_coeffs[i])
84 % P;
85 if hash_val < signatures[i] {
86 signatures[i] = hash_val;
87 }
88 }
89 }
90
91 MinHash {
92 signatures,
93 num_hashes,
94 }
95}
96
97fn fxhash(s: &str) -> u64 {
99 let mut hash: u64 = 0xcbf29ce484222325;
100 for b in s.bytes() {
101 hash ^= b as u64;
102 hash = hash.wrapping_mul(0x100000001b3);
103 }
104 hash
105}
106
107#[inline]
111pub fn similarity(a: &MinHash, b: &MinHash) -> f64 {
112 let min_len = std::cmp::min(a.signatures.len(), b.signatures.len());
113 let matches = a.signatures[..min_len]
114 .iter()
115 .zip(b.signatures[..min_len].iter())
116 .filter(|(sa, sb)| sa == sb)
117 .count();
118
119 matches as f64 / min_len as f64
120}
121
122#[inline]
124pub fn compare(a: &str, b: &str, shingle_size: usize, num_hashes: usize) -> f64 {
125 let sig_a = signature(a, shingle_size, num_hashes);
126 let sig_b = signature(b, shingle_size, num_hashes);
127 similarity(&sig_a, &sig_b)
128}
129
130#[inline]
132pub fn compare_default(a: &str, b: &str) -> f64 {
133 compare(a, b, DEFAULT_SHINGLE_SIZE, DEFAULT_NUM_HASHES)
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139
140 #[test]
141 fn identical_documents() {
142 let sig_a = signature("hello world this is a test", 3, 128);
143 let sig_b = signature("hello world this is a test", 3, 128);
144 let s = similarity(&sig_a, &sig_b);
145 assert!((s - 1.0).abs() < 0.01, "identical should be ~1.0, got {s}");
146 }
147
148 #[test]
149 fn similar_documents() {
150 let s = compare_default(
151 "the quick brown fox jumps over the lazy dog",
152 "the quick brown fox jumps over the lazy cat",
153 );
154 assert!(s > 0.5, "expected high similarity, got {s}");
155 }
156
157 #[test]
158 fn different_documents() {
159 let s = compare_default(
160 "hello world this is a test",
161 "completely unrelated content nothing similar",
162 );
163 assert!(s < 0.3, "expected low similarity, got {s}");
164 }
165
166 #[test]
167 fn symmetric() {
168 let a = compare_default("abc def ghi", "abc xyz");
169 let b = compare_default("abc xyz", "abc def ghi");
170 assert!((a - b).abs() < f64::EPSILON);
171 }
172
173 #[test]
174 fn consistent_signature_length() {
175 let sig = signature("test", 3, 128);
176 assert_eq!(sig.signatures.len(), 128);
177 assert_eq!(sig.num_hashes, 128);
178 }
179}