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