1use rayon::prelude::*;
5
6use crate::algo;
7use crate::router::{resolve_intent, Algo, Intent};
8use crate::SimiError;
9
10#[derive(Clone, Debug)]
12pub struct BatchResult {
13 pub index_a: usize,
15 pub index_b: usize,
17 pub score: f64,
19}
20
21#[derive(Clone, Debug)]
23pub struct BatchComparator {
24 algorithm: Algo,
25}
26
27impl BatchComparator {
28 #[inline]
30 pub fn new(algorithm: Algo) -> Self {
31 Self { algorithm }
32 }
33
34 #[inline]
49 pub fn for_intent(intent: Intent) -> Self {
50 let algo = resolve_intent(intent, "", "");
51 Self { algorithm: algo }
52 }
53
54 #[inline]
61 pub fn auto() -> Self {
62 Self::for_intent(Intent::Auto)
63 }
64
65 #[inline]
70 pub fn compare_pairs(&self, a: &[String], b: &[String]) -> Result<Vec<BatchResult>, SimiError> {
71 if a.len() != b.len() {
72 return Err(SimiError::BatchError(
73 "Input slices must have the same length".into(),
74 ));
75 }
76
77 let results: Vec<BatchResult> = a
78 .par_iter()
79 .zip(b.par_iter())
80 .enumerate()
81 .map(|(i, (sa, sb))| {
82 let score = match &self.algorithm {
83 Algo::Levenshtein => algo::levenshtein::similarity(sa, sb),
84 Algo::JaroWinkler => algo::jaro_winkler::similarity(sa, sb),
85 Algo::Hamming => algo::hamming::similarity(sa, sb).unwrap_or(0.0),
86 Algo::Jaccard(n) => algo::jaccard::similarity(sa, sb, *n),
87 Algo::JaccardBigram => algo::jaccard::bigram_similarity(sa, sb),
88 Algo::JaccardTrigram => algo::jaccard::trigram_similarity(sa, sb),
89 Algo::JaccardWord => algo::jaccard::word_similarity(sa, sb),
90 Algo::MinHash(sh, nh) => algo::minhash::compare(sa, sb, *sh, *nh),
91 Algo::MinHashDefault => algo::minhash::compare_default(sa, sb),
92 Algo::SimHash(sh) => algo::simhash::compare(sa, sb, *sh),
93 Algo::SimHashDefault => algo::simhash::compare_default(sa, sb),
94 Algo::Bm25 => algo::bm25::similarity(sa, sb),
95 Algo::TfIdf => algo::tfidf::similarity(sa, sb),
96 };
97 BatchResult {
98 index_a: i,
99 index_b: i,
100 score,
101 }
102 })
103 .collect();
104
105 Ok(results)
106 }
107
108 #[inline]
112 pub fn compare_one_to_many(
113 &self,
114 reference: &str,
115 candidates: &[String],
116 ) -> Result<Vec<BatchResult>, SimiError> {
117 let results: Vec<BatchResult> = candidates
118 .par_iter()
119 .enumerate()
120 .map(|(i, candidate)| {
121 let score = match &self.algorithm {
122 Algo::Levenshtein => algo::levenshtein::similarity(reference, candidate),
123 Algo::JaroWinkler => algo::jaro_winkler::similarity(reference, candidate),
124 Algo::Hamming => algo::hamming::similarity(reference, candidate).unwrap_or(0.0),
125 Algo::Jaccard(n) => algo::jaccard::similarity(reference, candidate, *n),
126 Algo::JaccardBigram => algo::jaccard::bigram_similarity(reference, candidate),
127 Algo::JaccardTrigram => algo::jaccard::trigram_similarity(reference, candidate),
128 Algo::JaccardWord => algo::jaccard::word_similarity(reference, candidate),
129 Algo::MinHash(sh, nh) => algo::minhash::compare(reference, candidate, *sh, *nh),
130 Algo::MinHashDefault => algo::minhash::compare_default(reference, candidate),
131 Algo::SimHash(sh) => algo::simhash::compare(reference, candidate, *sh),
132 Algo::SimHashDefault => algo::simhash::compare_default(reference, candidate),
133 Algo::Bm25 => algo::bm25::similarity(reference, candidate),
134 Algo::TfIdf => algo::tfidf::similarity(reference, candidate),
135 };
136 BatchResult {
137 index_a: 0,
138 index_b: i,
139 score,
140 }
141 })
142 .collect();
143
144 Ok(results)
145 }
146
147 #[inline]
152 pub fn compare_matrix(
153 &self,
154 a: &[String],
155 b: &[String],
156 ) -> Result<Vec<BatchResult>, SimiError> {
157 let results: Vec<BatchResult> = a
158 .par_iter()
159 .enumerate()
160 .flat_map(|(i, sa)| {
161 b.par_iter()
162 .enumerate()
163 .map(|(j, sb)| {
164 let score = match &self.algorithm {
165 Algo::Levenshtein => algo::levenshtein::similarity(sa, sb),
166 Algo::JaroWinkler => algo::jaro_winkler::similarity(sa, sb),
167 Algo::Hamming => algo::hamming::similarity(sa, sb).unwrap_or(0.0),
168 Algo::Jaccard(n) => algo::jaccard::similarity(sa, sb, *n),
169 Algo::JaccardBigram => algo::jaccard::bigram_similarity(sa, sb),
170 Algo::JaccardTrigram => algo::jaccard::trigram_similarity(sa, sb),
171 Algo::JaccardWord => algo::jaccard::word_similarity(sa, sb),
172 Algo::MinHash(sh, nh) => algo::minhash::compare(sa, sb, *sh, *nh),
173 Algo::MinHashDefault => algo::minhash::compare_default(sa, sb),
174 Algo::SimHash(sh) => algo::simhash::compare(sa, sb, *sh),
175 Algo::SimHashDefault => algo::simhash::compare_default(sa, sb),
176 Algo::Bm25 => algo::bm25::similarity(sa, sb),
177 Algo::TfIdf => algo::tfidf::similarity(sa, sb),
178 };
179 BatchResult {
180 index_a: i,
181 index_b: j,
182 score,
183 }
184 })
185 .collect::<Vec<_>>()
186 })
187 .collect();
188
189 Ok(results)
190 }
191}
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196
197 #[test]
198 fn compare_pairs_basic() {
199 let a = vec!["hello".into(), "world".into(), "rust".into()];
200 let b = vec!["hello".into(), "word".into(), "rusty".into()];
201
202 let comparator = BatchComparator::new(Algo::Levenshtein);
203 let results = comparator.compare_pairs(&a, &b).unwrap();
204
205 assert_eq!(results.len(), 3);
206 assert!((results[0].score - 1.0).abs() < f64::EPSILON);
208 assert!(results[0].score >= 0.0 && results[0].score <= 1.0);
210 assert!(results[1].score >= 0.0 && results[1].score <= 1.0);
211 assert!(results[2].score >= 0.0 && results[2].score <= 1.0);
212 }
213
214 #[test]
215 fn compare_one_to_many() {
216 let reference = "hello".to_string();
217 let candidates = vec!["hello".into(), "hallo".into(), "world".into()];
218
219 let comparator = BatchComparator::new(Algo::Levenshtein);
220 let results = comparator
221 .compare_one_to_many(&reference, &candidates)
222 .unwrap();
223
224 assert_eq!(results.len(), 3);
225 assert!((results[0].score - 1.0).abs() < f64::EPSILON);
226 }
227
228 #[test]
229 fn compare_matrix() {
230 let a = vec!["hello".into(), "world".into()];
231 let b = vec!["hello".into(), "word".into()];
232
233 let comparator = BatchComparator::new(Algo::Levenshtein);
234 let results = comparator.compare_matrix(&a, &b).unwrap();
235
236 assert_eq!(results.len(), 4);
238 }
239
240 #[test]
241 fn unequal_lengths_error() {
242 let a = vec!["hello".into()];
243 let b = vec!["hello".into(), "world".into()];
244
245 let comparator = BatchComparator::new(Algo::Levenshtein);
246 let result = comparator.compare_pairs(&a, &b);
247 assert!(result.is_err());
248 }
249
250 #[test]
251 fn large_batch() {
252 let size = 100;
253 let a: Vec<String> = (0..size).map(|i| format!("string {}", i)).collect();
254 let b: Vec<String> = (0..size).map(|i| format!("string {}", i + 1)).collect();
255
256 let comparator = BatchComparator::new(Algo::Levenshtein);
257 let results = comparator.compare_pairs(&a, &b).unwrap();
258
259 assert_eq!(results.len(), size);
260 for r in &results {
262 assert!(r.score >= 0.0 && r.score <= 1.0);
263 }
264 }
265}