Part of the Santh security research ecosystem.
High-performance dataset deduplication for ML training data using MinHash + LSH.
- MinHash + LSH: Industry-standard near-duplicate detection with configurable similarity thresholds
- Fast Hashing: Efficient hash computation for high throughput
- Streaming: Process billions of documents without loading all into memory
- tenshift Integration: Plugs into data pipelines as a
Transform - Configurable: Tune signature size, band count, and thresholds for your data
- Robust: Fuzz-quality input handling, zero
unwrapin production code
use dedup::{Config, DedupTransformer};
use dedup::tenshift::Sample;
use tenshift_core::sample::Tensor;
let config = Config::default()
.with_similarity_threshold(0.9);
let mut dedup = DedupTransformer::new(config)?;
// Add documents
dedup.push(Sample::new().with("text", Tensor::bytes(b"the quick brown fox".to_vec())));
dedup.push(Sample::new().with("text", Tensor::bytes(b"the quick brown fox".to_vec())));
// Get deduplicated results: the exact duplicate collapses to one entry.
let unique = dedup.finish_batch();
assert_eq!(unique.len(), 1);
# Ok::<(), dedup::Error>(())┌─────────────┐ ┌──────────────┐ ┌─────────────┐ ┌─────────────┐
│ Shingle │────▶│ MinHash │────▶│ LSH Bands │────▶│ Cluster │
│ (k-grams) │ │ (fast hash) │ │ (buckets) │ │ Output │
└─────────────┘ └──────────────┘ └─────────────┘ └─────────────┘
- Shingling: Convert documents to sets of k-grams (overlapping subsequences)
- MinHash: Compress documents to small signatures preserving Jaccard similarity
- LSH: Band signatures such that similar documents collide in buckets
- Clustering: Group colliding documents and output unique representatives
let config = dedup::Config::new(
128, // signature size (hash functions)
16, // LSH bands
5, // shingle size
0.9, // similarity threshold
)?;
# Ok::<(), dedup::Error>(())| Parameter | Default | Description |
|---|---|---|
signature_size |
128 | Number of hash functions. Higher = more accurate but slower |
num_bands |
16 | LSH bands. Higher = more sensitive but more false positives |
shingle_size |
5 | k-gram length. 4-7 works well for text |
threshold |
0.9 | Similarity threshold. 0.85-0.95 recommended |
use dedup::{Config, StatefulDedupTransform};
use dedup::tenshift::Sample;
use tenshift_core::sample::Tensor;
use tenshift_core::transform::StatefulTransform;
let config = Config::default();
let mut dedup = StatefulDedupTransform::new(config)?;
// In your pipeline
let samples = vec![
Sample::new().with("text", Tensor::bytes(b"hello world".to_vec())),
Sample::new().with("text", Tensor::bytes(b"hello world".to_vec())),
];
for sample in samples {
let output = dedup.push(sample);
// output is empty until finish() is called
assert!(output.is_empty());
}
let unique = dedup.finish();
assert_eq!(unique.len(), 1);
# Ok::<(), dedup::Error>(())Benchmarked on AMD Ryzen 9 5950X:
| Operation | Throughput |
|---|---|
| MinHash (128 sig) | ~50,000 docs/sec |
| MinHash (256 sig) | ~25,000 docs/sec |
| LSH Insert | ~100,000 ops/sec |
| Batch (1000 docs) | ~30ms |
MinHash estimates Jaccard similarity between sets:
J(A,B) = |A ∩ B| / |A ∪ B|- MinHash approximates this by comparing hash signatures
- Expected error:
√(s(1-s)/k)wheresis similarity,kis signature size
LSH reduces O(n²) comparisons to O(n):
- Signature split into
bbands ofrrows each - Probability of collision for similarity
s:1 - (1 - s^r)^b - Threshold (S-curve inflection):
t ≈ (1/b)^(1/r)
cargo test
cargo test --release # For benchmarks
cargo clippy -- -D warningsMIT License - See LICENSE file for details.