#![allow(
clippy::module_name_repetitions,
clippy::must_use_candidate,
clippy::missing_errors_doc,
)]
#![warn(missing_docs)]
#![warn(clippy::pedantic)]
#![forbid(unsafe_code)]
#![warn(missing_docs, clippy::pedantic)]
#![cfg_attr(
not(test),
deny(
clippy::unwrap_used,
clippy::expect_used,
clippy::todo,
clippy::unimplemented,
clippy::panic
)
)]
#![allow(
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
clippy::module_name_repetitions,
clippy::needless_pass_by_value,
clippy::must_use_candidate,
clippy::return_self_not_must_use,
clippy::unnecessary_literal_bound,
clippy::doc_markdown,
clippy::cast_precision_loss
)]
mod cluster;
mod config;
mod error;
mod lsh;
mod minhash;
pub mod shingle;
mod fast_hash;
mod transform;
pub use config::Config;
pub use error::{Error, Result};
pub use cluster::DuplicateCluster;
pub use lsh::LshIndex;
pub use minhash::{MinHashSignature, MinHasher};
pub use shingle::ShingleIterator;
pub use fast_hash::{hash_bytes, FastHasher};
pub use transform::{DedupTransformer, StatefulDedupTransform};
pub mod tenshift {
pub use tenshift_core::sample::Sample;
pub use tenshift_core::transform::{Transform, TransformResult};
}
pub const DEFAULT_SIGNATURE_SIZE: usize = 128;
pub const DEFAULT_SHINGLE_SIZE: usize = 5;
pub const DEFAULT_NUM_BANDS: usize = 16;
pub const DEFAULT_SIMILARITY_THRESHOLD: f64 = 0.9;
#[must_use]
pub const fn compute_rows_per_band(signature_size: usize, num_bands: usize) -> Option<usize> {
if num_bands == 0 || signature_size % num_bands != 0 {
return None;
}
Some(signature_size / num_bands)
}
#[must_use]
pub fn candidate_probability(similarity: f64, num_bands: usize, rows_per_band: usize) -> f64 {
if similarity <= 0.0 {
return 0.0;
}
if similarity >= 1.0 {
return 1.0;
}
let band_match_prob = similarity.powf(rows_per_band as f64);
1.0 - (1.0 - band_match_prob).powf(num_bands as f64)
}
#[must_use]
pub fn optimize_lsh_params(
signature_size: usize,
target_threshold: f64,
) -> (usize, usize) {
if signature_size == 0 {
return (1, 0);
}
const MAX_CANDIDATE_BANDS: usize = 1024;
let search_limit = signature_size.min(MAX_CANDIDATE_BANDS);
let mut best: Option<(f64, usize, usize)> = None;
for num_bands in 1..=search_limit {
if signature_size % num_bands != 0 {
continue;
}
let rows_per_band = signature_size / num_bands;
let p_at_threshold = candidate_probability(target_threshold, num_bands, rows_per_band);
let p_below = candidate_probability(target_threshold * 0.9, num_bands, rows_per_band);
let p_above =
candidate_probability((target_threshold * 1.1).min(1.0), num_bands, rows_per_band);
let score = p_above - p_below - (p_at_threshold - 0.5).abs() * 0.5;
if best.map_or(true, |(best_score, _, _)| score > best_score) {
best = Some((score, num_bands, rows_per_band));
}
}
let (_, bands, rows) = best.unwrap_or((0.0, 1, signature_size));
(bands, rows)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_compute_rows_per_band() {
assert_eq!(compute_rows_per_band(128, 16), Some(8));
assert_eq!(compute_rows_per_band(128, 8), Some(16));
assert_eq!(compute_rows_per_band(128, 0), None);
assert_eq!(compute_rows_per_band(128, 3), None);
}
#[test]
fn test_candidate_probability_bounds() {
assert_eq!(candidate_probability(0.0, 16, 8), 0.0);
assert_eq!(candidate_probability(1.0, 16, 8), 1.0);
}
#[test]
fn test_candidate_probability_increases_with_similarity() {
let p1 = candidate_probability(0.5, 16, 8);
let p2 = candidate_probability(0.8, 16, 8);
let p3 = candidate_probability(0.95, 16, 8);
assert!(p1 < p2, "probability should increase with similarity");
assert!(p2 < p3, "probability should increase with similarity");
}
#[test]
fn test_optimize_lsh_params_produces_valid_params() {
let (bands, rows) = optimize_lsh_params(128, 0.9);
assert!(bands > 0);
assert!(rows > 0);
assert_eq!(bands * rows, 128);
}
#[test]
fn test_optimize_lsh_params_valid_when_indivisible_by_hardcoded_bands() {
for size in [1_usize, 2, 3, 5, 6, 7, 10, 25, 30, 50, 100, 127, 200] {
let (bands, rows) = optimize_lsh_params(size, 0.85);
assert!(bands >= 1, "size {size}: bands must be >= 1, got {bands}");
assert!(rows >= 1, "size {size}: rows must be >= 1, got {rows}");
assert_eq!(
bands * rows,
size,
"size {size}: returned ({bands}, {rows}) must tile the signature exactly"
);
}
}
}
#[cfg(doctest)]
#[doc = include_str!("../README.md")]
struct ReadmeExamples;