use crate::common::hash::xxhash;
use crate::common::{Mergeable, Sketch, SketchError};
use std::hash::Hash;
#[derive(Clone, Debug)]
pub struct MinHash {
num_perm: usize,
hash_values: Vec<u64>,
hash_seeds: Vec<u64>,
}
impl MinHash {
const MIN_NUM_PERM: usize = 16;
pub fn new(num_perm: usize) -> Result<Self, SketchError> {
if num_perm == 0 {
return Err(SketchError::InvalidParameter {
param: "num_perm".to_string(),
value: num_perm.to_string(),
constraint: "must be greater than 0".to_string(),
});
}
if num_perm < Self::MIN_NUM_PERM {
return Err(SketchError::InvalidParameter {
param: "num_perm".to_string(),
value: num_perm.to_string(),
constraint: format!(
"must be at least {} for reasonable accuracy",
Self::MIN_NUM_PERM
),
});
}
let hash_values = vec![u64::MAX; num_perm];
let hash_seeds = (0..num_perm)
.map(|i| {
let mut seed = i as u64;
seed = seed.wrapping_mul(0x9e3779b97f4a7c15u64); seed ^= seed >> 30;
seed = seed.wrapping_mul(0xbf58476d1ce4e5b9u64);
seed ^= seed >> 27;
seed = seed.wrapping_mul(0x94d049bb133111ebu64);
seed ^= seed >> 31;
seed
})
.collect();
Ok(MinHash {
num_perm,
hash_values,
hash_seeds,
})
}
pub fn update<T: Hash>(&mut self, item: &T) {
for i in 0..self.num_perm {
let hash = self.hash_with_seed(item, self.hash_seeds[i]);
if hash < self.hash_values[i] {
self.hash_values[i] = hash;
}
}
}
pub fn jaccard_similarity(&self, other: &Self) -> Result<f64, SketchError> {
if self.num_perm != other.num_perm {
return Err(SketchError::IncompatibleSketches {
reason: format!(
"Cannot compare MinHash with different num_perm: {} vs {}",
self.num_perm, other.num_perm
),
});
}
let self_empty = self.hash_values.iter().all(|&v| v == u64::MAX);
let other_empty = other.hash_values.iter().all(|&v| v == u64::MAX);
if self_empty && other_empty {
return Ok(0.0);
}
if self_empty || other_empty {
return Ok(0.0);
}
let matches = self
.hash_values
.iter()
.zip(other.hash_values.iter())
.filter(|(&a, &b)| a == b)
.count();
let similarity = matches as f64 / self.num_perm as f64;
Ok(similarity)
}
fn hash_with_seed<T: Hash>(&self, item: &T, seed: u64) -> u64 {
use std::hash::Hasher as StdHasher;
struct ByteHasher {
bytes: Vec<u8>,
}
impl StdHasher for ByteHasher {
fn finish(&self) -> u64 {
0 }
fn write(&mut self, bytes: &[u8]) {
self.bytes.extend_from_slice(bytes);
}
}
let mut hasher = ByteHasher { bytes: Vec::new() };
item.hash(&mut hasher);
xxhash(&hasher.bytes, seed)
}
pub fn num_perm(&self) -> usize {
self.num_perm
}
}
impl Sketch for MinHash {
type Item = u64;
fn update(&mut self, item: &Self::Item) {
self.update(item);
}
fn estimate(&self) -> f64 {
0.0
}
fn is_empty(&self) -> bool {
self.hash_values.iter().all(|&v| v == u64::MAX)
}
fn serialize(&self) -> Vec<u8> {
let mut bytes = Vec::new();
bytes.extend_from_slice(&(self.num_perm as u64).to_le_bytes());
for &seed in &self.hash_seeds {
bytes.extend_from_slice(&seed.to_le_bytes());
}
for &value in &self.hash_values {
bytes.extend_from_slice(&value.to_le_bytes());
}
bytes
}
fn deserialize(bytes: &[u8]) -> Result<Self, SketchError> {
if bytes.len() < 8 {
return Err(SketchError::DeserializationError(
"Insufficient data for num_perm".to_string(),
));
}
let num_perm = u64::from_le_bytes(
bytes[0..8]
.try_into()
.map_err(|_| SketchError::DeserializationError("Invalid num_perm".to_string()))?,
) as usize;
let expected_len = 8 + (num_perm * 8 * 2); if bytes.len() != expected_len {
return Err(SketchError::DeserializationError(format!(
"Invalid data length: expected {}, got {}",
expected_len,
bytes.len()
)));
}
let mut hash_seeds = Vec::with_capacity(num_perm);
for i in 0..num_perm {
let offset = 8 + (i * 8);
let seed = u64::from_le_bytes(
bytes[offset..offset + 8]
.try_into()
.map_err(|_| SketchError::DeserializationError("Invalid seed".to_string()))?,
);
hash_seeds.push(seed);
}
let mut hash_values = Vec::with_capacity(num_perm);
for i in 0..num_perm {
let offset = 8 + (num_perm * 8) + (i * 8);
let value = u64::from_le_bytes(bytes[offset..offset + 8].try_into().map_err(|_| {
SketchError::DeserializationError("Invalid hash value".to_string())
})?);
hash_values.push(value);
}
Ok(MinHash {
num_perm,
hash_values,
hash_seeds,
})
}
}
impl Mergeable for MinHash {
fn merge(&mut self, other: &Self) -> Result<(), SketchError> {
if self.num_perm != other.num_perm {
return Err(SketchError::IncompatibleSketches {
reason: format!(
"Cannot merge MinHash with different num_perm: {} vs {}",
self.num_perm, other.num_perm
),
});
}
for i in 0..self.num_perm {
self.hash_values[i] = self.hash_values[i].min(other.hash_values[i]);
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_minhash() {
let mh = MinHash::new(128);
assert!(mh.is_ok());
let mh = mh.unwrap();
assert_eq!(mh.num_perm(), 128);
assert_eq!(mh.hash_values.len(), 128);
assert_eq!(mh.hash_seeds.len(), 128);
assert!(mh.hash_values.iter().all(|&v| v == u64::MAX));
}
#[test]
fn test_update() {
let mut mh = MinHash::new(128).unwrap();
mh.update(&"test");
assert!(mh.hash_values.iter().any(|&v| v != u64::MAX));
}
#[test]
fn test_identical_sets() {
let mut mh1 = MinHash::new(128).unwrap();
let mut mh2 = MinHash::new(128).unwrap();
for i in 0..100 {
mh1.update(&i);
mh2.update(&i);
}
let sim = mh1.jaccard_similarity(&mh2).unwrap();
assert!((sim - 1.0).abs() < 0.001);
}
#[test]
fn test_disjoint_sets() {
let mut mh1 = MinHash::new(128).unwrap();
let mut mh2 = MinHash::new(128).unwrap();
for i in 0..100 {
mh1.update(&i);
}
for i in 100..200 {
mh2.update(&i);
}
let sim = mh1.jaccard_similarity(&mh2).unwrap();
assert!(sim < 0.05);
}
#[test]
fn test_merge() {
let mut mh1 = MinHash::new(128).unwrap();
let mut mh2 = MinHash::new(128).unwrap();
for i in 0..50 {
mh1.update(&i);
}
for i in 50..100 {
mh2.update(&i);
}
mh1.merge(&mh2).unwrap();
let mut mh_union = MinHash::new(128).unwrap();
for i in 0..100 {
mh_union.update(&i);
}
let sim = mh1.jaccard_similarity(&mh_union).unwrap();
assert!(sim > 0.95);
}
#[test]
fn test_hash_seeds_are_different() {
let mh = MinHash::new(128).unwrap();
let unique_seeds: std::collections::HashSet<_> = mh.hash_seeds.iter().collect();
assert_eq!(unique_seeds.len(), 128, "Seeds should be unique");
}
}