use crate::common::hash::xxhash;
use crate::common::{Mergeable, Sketch, SketchError};
use std::hash::Hash;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SimHash {
fingerprint: u64,
accumulator: Vec<i64>,
finalized: bool,
count: usize,
}
impl Default for SimHash {
fn default() -> Self {
Self::new()
}
}
impl SimHash {
pub const BITS: usize = 64;
pub fn new() -> Self {
SimHash {
fingerprint: 0,
accumulator: vec![0i64; Self::BITS],
finalized: false,
count: 0,
}
}
pub fn update<T: Hash + ?Sized>(&mut self, feature: &T) {
self.update_weighted(feature, 1);
}
pub fn update_weighted<T: Hash + ?Sized>(&mut self, feature: &T, weight: i64) {
if self.finalized {
self.finalized = false;
}
let hash = self.hash_feature(feature);
self.count += 1;
for i in 0..Self::BITS {
if (hash >> i) & 1 == 1 {
self.accumulator[i] += weight;
} else {
self.accumulator[i] -= weight;
}
}
}
pub fn fingerprint(&mut self) -> u64 {
if !self.finalized {
self.compute_fingerprint();
}
self.fingerprint
}
fn compute_fingerprint(&mut self) {
self.fingerprint = 0;
for i in 0..Self::BITS {
if self.accumulator[i] > 0 {
self.fingerprint |= 1u64 << i;
}
}
self.finalized = true;
}
pub fn hamming_distance(&mut self, other: &mut SimHash) -> u32 {
let fp1 = self.fingerprint();
let fp2 = other.fingerprint();
(fp1 ^ fp2).count_ones()
}
pub fn similarity(&mut self, other: &mut SimHash) -> f64 {
let distance = self.hamming_distance(other);
(Self::BITS as u32 - distance) as f64 / Self::BITS as f64
}
pub fn similarity_from_fingerprints(fp1: u64, fp2: u64) -> f64 {
let distance = (fp1 ^ fp2).count_ones();
(Self::BITS as u32 - distance) as f64 / Self::BITS as f64
}
pub fn hamming_distance_from_fingerprints(fp1: u64, fp2: u64) -> u32 {
(fp1 ^ fp2).count_ones()
}
pub fn len(&self) -> usize {
self.count
}
pub fn is_empty(&self) -> bool {
self.count == 0
}
fn hash_feature<T: Hash + ?Sized>(&self, feature: &T) -> 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() };
feature.hash(&mut hasher);
xxhash(&hasher.bytes, 0)
}
pub fn to_bytes(&mut self) -> Vec<u8> {
let fp = self.fingerprint();
let mut bytes = Vec::with_capacity(8 + 8 + 1 + Self::BITS * 8);
bytes.extend_from_slice(&fp.to_le_bytes());
bytes.extend_from_slice(&(self.count as u64).to_le_bytes());
bytes.push(self.finalized as u8);
for &val in &self.accumulator {
bytes.extend_from_slice(&val.to_le_bytes());
}
bytes
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, SketchError> {
let min_len = 8 + 8 + 1 + Self::BITS * 8;
if bytes.len() < min_len {
return Err(SketchError::DeserializationError(format!(
"Insufficient data: expected at least {} bytes, got {}",
min_len,
bytes.len()
)));
}
let fingerprint =
u64::from_le_bytes(bytes[0..8].try_into().map_err(|_| {
SketchError::DeserializationError("Invalid fingerprint".to_string())
})?);
let count = u64::from_le_bytes(
bytes[8..16]
.try_into()
.map_err(|_| SketchError::DeserializationError("Invalid count".to_string()))?,
) as usize;
let finalized = bytes[16] != 0;
let mut accumulator = Vec::with_capacity(Self::BITS);
for i in 0..Self::BITS {
let offset = 17 + i * 8;
let val = i64::from_le_bytes(bytes[offset..offset + 8].try_into().map_err(|_| {
SketchError::DeserializationError("Invalid accumulator".to_string())
})?);
accumulator.push(val);
}
Ok(SimHash {
fingerprint,
accumulator,
finalized,
count,
})
}
}
impl Sketch for SimHash {
type Item = String;
fn update(&mut self, item: &Self::Item) {
self.update(item);
}
fn estimate(&self) -> f64 {
self.fingerprint as f64
}
fn is_empty(&self) -> bool {
self.count == 0
}
fn serialize(&self) -> Vec<u8> {
let mut sh = self.clone();
sh.to_bytes()
}
fn deserialize(bytes: &[u8]) -> Result<Self, SketchError> {
Self::from_bytes(bytes)
}
}
impl Mergeable for SimHash {
fn merge(&mut self, other: &Self) -> Result<(), SketchError> {
for i in 0..Self::BITS {
self.accumulator[i] += other.accumulator[i];
}
self.count += other.count;
self.finalized = false; Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_simhash() {
let sh = SimHash::new();
assert!(sh.is_empty());
assert_eq!(sh.len(), 0);
}
#[test]
fn test_update() {
let mut sh = SimHash::new();
sh.update("hello");
assert!(!sh.is_empty());
assert_eq!(sh.len(), 1);
}
#[test]
fn test_identical_inputs() {
let mut sh1 = SimHash::new();
let mut sh2 = SimHash::new();
sh1.update("hello");
sh1.update("world");
sh2.update("hello");
sh2.update("world");
assert_eq!(sh1.hamming_distance(&mut sh2), 0);
assert!((sh1.similarity(&mut sh2) - 1.0).abs() < 0.001);
}
#[test]
fn test_different_inputs() {
let mut sh1 = SimHash::new();
let mut sh2 = SimHash::new();
sh1.update("hello");
sh1.update("world");
sh2.update("completely");
sh2.update("different");
let distance = sh1.hamming_distance(&mut sh2);
assert!(distance > 0);
}
#[test]
fn test_similar_inputs() {
let mut sh1 = SimHash::new();
let mut sh2 = SimHash::new();
for word in ["the", "quick", "brown", "fox", "jumps"] {
sh1.update(word);
sh2.update(word);
}
sh1.update("over");
sh2.update("under");
let similarity = sh1.similarity(&mut sh2);
assert!(similarity > 0.7);
}
#[test]
fn test_weighted_update() {
let mut sh = SimHash::new();
sh.update_weighted("important", 10);
sh.update_weighted("noise", 1);
assert_eq!(sh.len(), 2);
}
#[test]
fn test_merge() {
let mut sh1 = SimHash::new();
let mut sh2 = SimHash::new();
sh1.update("hello");
sh2.update("world");
let mut combined = SimHash::new();
combined.update("hello");
combined.update("world");
sh1.merge(&sh2).unwrap();
assert_eq!(sh1.fingerprint(), combined.fingerprint());
}
#[test]
fn test_serialization() {
let mut sh = SimHash::new();
sh.update("hello");
sh.update("world");
let bytes = sh.to_bytes();
let restored = SimHash::from_bytes(&bytes).unwrap();
assert_eq!(sh.fingerprint, restored.fingerprint);
assert_eq!(sh.count, restored.count);
}
#[test]
fn test_fingerprint_from_static() {
let fp1 = 0b1010101010101010u64;
let fp2 = 0b1010101010101011u64;
let distance = SimHash::hamming_distance_from_fingerprints(fp1, fp2);
assert_eq!(distance, 1);
let similarity = SimHash::similarity_from_fingerprints(fp1, fp2);
assert!((similarity - 63.0 / 64.0).abs() < 0.001);
}
}