1use crate::error::{Error, Result};
4
5pub const MAX_SIGNATURE_SIZE: usize = 1 << 16;
25
26#[derive(Debug, Clone, Copy, PartialEq)]
28pub struct Config {
29 pub signature_size: usize,
31 pub num_bands: usize,
33 pub shingle_size: usize,
35 pub similarity_threshold: f64,
37 pub(crate) max_document_size: usize,
39 pub(crate) memory_limit_in_mb: usize,
41 pub(crate) seed: u64,
43 pub(crate) store_documents: bool,
45}
46
47impl Config {
48 pub fn new(
57 signature_size: usize,
58 num_bands: usize,
59 shingle_size: usize,
60 similarity_threshold: f64,
61 ) -> Result<Self> {
62 if signature_size == 0 {
63 return Err(Error::InvalidConfig {
64 reason: "signature_size must be at least 1".to_string(),
65 fix: "use signature_size >= 1".to_string(),
66 });
67 }
68 if signature_size > MAX_SIGNATURE_SIZE {
69 return Err(Error::InvalidConfig {
70 reason: format!(
71 "signature_size ({signature_size}) exceeds maximum {MAX_SIGNATURE_SIZE}"
72 ),
73 fix: format!(
74 "use signature_size <= {MAX_SIGNATURE_SIZE}; larger signatures add no accuracy and only exhaust memory"
75 ),
76 });
77 }
78 if num_bands == 0 {
79 return Err(Error::InvalidConfig {
80 reason: "num_bands must be at least 1".to_string(),
81 fix: "use num_bands >= 1".to_string(),
82 });
83 }
84 if signature_size % num_bands != 0 {
85 return Err(Error::InvalidConfig {
86 reason: format!(
87 "signature_size ({signature_size}) must be divisible by num_bands ({num_bands})"
88 ),
89 fix: "use signature_size = num_bands * rows_per_band".to_string(),
90 });
91 }
92 if shingle_size == 0 {
93 return Err(Error::InvalidConfig {
94 reason: "shingle_size must be at least 1".to_string(),
95 fix: "use shingle_size >= 1".to_string(),
96 });
97 }
98 if similarity_threshold <= 0.0 || similarity_threshold > 1.0 {
99 return Err(Error::InvalidConfig {
100 reason: format!(
101 "similarity_threshold ({similarity_threshold}) must be in (0.0, 1.0]"
102 ),
103 fix: "use 0.0 < similarity_threshold <= 1.0".to_string(),
104 });
105 }
106
107 Ok(Self {
108 signature_size,
109 num_bands,
110 shingle_size,
111 similarity_threshold,
112 max_document_size: 10 * 1024 * 1024, memory_limit_in_mb: 4096, seed: 0x9e37_79b9_7f4a_7c15, store_documents: false,
116 })
117 }
118
119 #[must_use]
121 pub fn with_similarity_threshold(mut self, threshold: f64) -> Self {
122 self.similarity_threshold = threshold.clamp(0.01, 1.0);
123 self
124 }
125
126 #[must_use]
137 pub fn with_num_bands(mut self, num_bands: usize) -> Self {
138 if num_bands > 0 {
139 self.num_bands = nearest_divisor(self.signature_size, num_bands);
140 }
141 self
142 }
143
144 #[must_use]
146 pub fn with_shingle_size(mut self, shingle_size: usize) -> Self {
147 if shingle_size > 0 {
148 self.shingle_size = shingle_size;
149 }
150 self
151 }
152
153 #[must_use]
162 pub fn with_signature_size(mut self, signature_size: usize) -> Self {
163 if signature_size > 0 {
164 let signature_size = signature_size.min(MAX_SIGNATURE_SIZE);
170 let bands = self.num_bands.max(1);
171 self.signature_size = signature_size.div_ceil(bands) * bands;
172 }
173 self
174 }
175
176 #[must_use]
178 pub fn with_max_document_size(mut self, max_bytes: usize) -> Self {
179 self.max_document_size = max_bytes;
180 self
181 }
182
183 #[must_use]
185 pub fn with_memory_limit(mut self, memory_limit_in_mb: usize) -> Self {
186 self.memory_limit_in_mb = memory_limit_in_mb;
187 self
188 }
189
190 #[must_use]
192 pub fn with_seed(mut self, seed: u64) -> Self {
193 self.seed = seed;
194 self
195 }
196
197 #[must_use]
199 pub fn with_store_documents(mut self, store: bool) -> Self {
200 self.store_documents = store;
201 self
202 }
203
204 #[must_use]
206 pub const fn rows_per_band(&self) -> usize {
207 self.signature_size / self.num_bands
208 }
209
210 #[must_use]
212 pub fn estimated_memory_per_document(&self) -> usize {
213 let signature_bytes = self.signature_size * 4;
216 let index_overhead = self.num_bands * 16; signature_bytes + index_overhead + 64 }
219
220 #[must_use]
222 pub fn max_documents_in_memory(&self) -> usize {
223 let memory_bytes = self.memory_limit_in_mb.saturating_mul(1024 * 1024);
224 let per_doc = self.estimated_memory_per_document();
225 memory_bytes / per_doc.max(1)
226 }
227}
228
229impl Default for Config {
230 fn default() -> Self {
231 Self {
235 signature_size: 128,
236 num_bands: 16,
237 shingle_size: 5,
238 similarity_threshold: 0.9,
239 max_document_size: 10 * 1024 * 1024,
240 memory_limit_in_mb: 4096,
241 seed: 0x9e37_79b9_7f4a_7c15,
242 store_documents: false,
243 }
244 }
245}
246
247fn nearest_divisor(n: usize, target: usize) -> usize {
256 if n == 0 {
257 return 1;
258 }
259 let limit = n.min(MAX_BAND_SEARCH);
260 let mut best = 1_usize;
261 let mut best_dist = target.abs_diff(1);
262 let mut d = 1_usize;
263 while d <= limit {
264 if n % d == 0 {
265 let dist = target.abs_diff(d);
266 if dist < best_dist {
267 best_dist = dist;
268 best = d;
269 }
270 }
271 d += 1;
272 }
273 best
274}
275
276const MAX_BAND_SEARCH: usize = 1 << 16;
280
281#[cfg(test)]
282mod tests {
283 use super::*;
284
285 #[test]
286 fn nearest_divisor_snaps_to_closest_divisor() {
287 assert_eq!(nearest_divisor(128, 20), 16); assert_eq!(nearest_divisor(128, 30), 32); assert_eq!(nearest_divisor(128, 8), 8); assert_eq!(nearest_divisor(100, 7), 5); assert_eq!(nearest_divisor(0, 9), 1); }
294
295 #[test]
296 fn with_num_bands_snaps_indivisible_request_to_valid_divisor() {
297 let config = Config::default().with_num_bands(20);
301 assert_eq!(config.num_bands, 16);
302 assert_eq!(config.signature_size % config.num_bands, 0);
303
304 let config = Config::default().with_num_bands(30);
306 assert_eq!(config.num_bands, 32);
307 assert_eq!(config.signature_size % config.num_bands, 0);
308 }
309
310 #[test]
311 fn with_signature_size_rounds_up_to_multiple_of_bands() {
312 let config = Config::default().with_signature_size(100);
315 assert_eq!(config.num_bands, 16);
316 assert_eq!(config.signature_size, 112);
317 assert_eq!(config.signature_size % config.num_bands, 0);
318
319 let config = Config::default().with_signature_size(256);
321 assert_eq!(config.signature_size, 256);
322 }
323
324 #[test]
325 fn default_config_valid() {
326 let config = Config::default();
327 assert_eq!(config.signature_size, 128);
328 assert_eq!(config.num_bands, 16);
329 assert_eq!(config.rows_per_band(), 8);
330 }
331
332 #[test]
333 fn new_validates_signature_size() {
334 let result = Config::new(100, 16, 5, 0.9);
335 assert!(result.is_err());
336 assert!(result.unwrap_err().to_string().contains("divisible"));
337 }
338
339 #[test]
340 fn new_validates_threshold() {
341 let result = Config::new(128, 16, 5, 0.0);
342 assert!(result.is_err());
343 let result = Config::new(128, 16, 5, 1.5);
344 assert!(result.is_err());
345 }
346
347 #[test]
348 fn builder_pattern_works() {
349 let config = Config::default()
350 .with_similarity_threshold(0.85)
351 .with_num_bands(8)
352 .with_shingle_size(4);
353
354 assert!((config.similarity_threshold - 0.85).abs() < f64::EPSILON);
355 assert_eq!(config.num_bands, 8);
356 assert_eq!(config.shingle_size, 4);
357 }
358
359 #[test]
360 fn rows_per_band_calculation() {
361 let config = Config::new(256, 32, 5, 0.9).unwrap();
362 assert_eq!(config.rows_per_band(), 8);
363 }
364
365 #[test]
366 fn memory_estimation() {
367 let config = Config::default();
368 let per_doc = config.estimated_memory_per_document();
369 assert!(per_doc > 0);
370
371 let max_docs = config.max_documents_in_memory();
372 assert!(max_docs > 0);
373 }
374
375 #[test]
376 fn invalid_shingle_size_rejected() {
377 let result = Config::new(128, 16, 0, 0.9);
378 assert!(result.is_err());
379 }
380
381 #[test]
382 fn valid_config_accepts() {
383 let config = Config::new(128, 16, 5, 0.9).unwrap();
384 assert_eq!(config.signature_size, 128);
385 assert_eq!(config.num_bands, 16);
386 }
387
388 #[test]
395 fn new_rejects_oversized_signature_size() {
396 let result = Config::new(MAX_SIGNATURE_SIZE + 16, 16, 5, 0.9);
397 let err = result.expect_err("oversized signature_size must be rejected");
398 let msg = err.to_string();
399 assert!(msg.contains("exceeds maximum"), "error names the bound: {msg}");
400 assert!(msg.contains("Fix:"), "error carries a fix: {msg}");
401
402 assert!(Config::new(MAX_SIGNATURE_SIZE, 16, 5, 0.9).is_ok());
404 }
405
406 #[test]
412 fn with_signature_size_near_usize_max_cannot_overflow() {
413 let config = Config::default().with_signature_size(usize::MAX);
414 assert_eq!(config.signature_size, MAX_SIGNATURE_SIZE);
415 assert_eq!(config.signature_size % config.num_bands, 0);
416 }
417}