pub const SKETCH_WIDTH: usize = 16;
const SKETCH_SEED: u64 = 0x9E37_79B9_7F4A_7C15;
fn mix(value: u64) -> u64 {
let mut z = value.wrapping_add(SKETCH_SEED);
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^ (z >> 31)
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct SimilaritySketch {
values: [u64; SKETCH_WIDTH],
len: u8,
}
impl SimilaritySketch {
pub fn leaf(full_hash: u64) -> Self {
let mut values = [0u64; SKETCH_WIDTH];
values[0] = mix(full_hash);
Self { values, len: 1 }
}
pub fn merge(children: impl IntoIterator<Item = Self>) -> Self {
let mut pool: Vec<u64> = Vec::new();
for child in children {
pool.extend_from_slice(child.as_slice());
}
pool.sort_unstable();
pool.dedup();
pool.truncate(SKETCH_WIDTH);
let mut values = [0u64; SKETCH_WIDTH];
values[..pool.len()].copy_from_slice(&pool);
Self {
values,
len: pool.len() as u8,
}
}
fn as_slice(&self) -> &[u64] {
&self.values[..self.len as usize]
}
#[cfg(test)]
pub fn is_exact(&self) -> bool {
(self.len as usize) < SKETCH_WIDTH
}
pub fn jaccard(&self, other: &Self) -> f32 {
let (a, b) = (self.as_slice(), other.as_slice());
if a.is_empty() || b.is_empty() {
return 0.0;
}
let (mut i, mut j) = (0usize, 0usize);
let (mut union_size, mut shared) = (0usize, 0usize);
while union_size < SKETCH_WIDTH && (i < a.len() || j < b.len()) {
match (a.get(i), b.get(j)) {
(Some(&x), Some(&y)) if x == y => {
shared += 1;
i += 1;
j += 1;
}
(Some(&x), Some(&y)) if x < y => i += 1,
(Some(_), Some(_)) => j += 1,
(Some(_), None) => i += 1,
(None, Some(_)) => j += 1,
(None, None) => unreachable!("loop condition guarantees one side has values left"),
}
union_size += 1;
}
shared as f32 / union_size as f32
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sketch_of(leaf_hashes: &[u64]) -> SimilaritySketch {
SimilaritySketch::merge(leaf_hashes.iter().map(|&h| SimilaritySketch::leaf(h)))
}
#[test]
fn identical_sets_score_one_and_disjoint_sets_score_zero() {
let a = sketch_of(&[1, 2, 3, 4]);
assert_eq!(a.jaccard(&sketch_of(&[1, 2, 3, 4])), 1.0);
assert_eq!(a.jaccard(&sketch_of(&[90, 91, 92, 93])), 0.0);
}
#[test]
fn small_sets_are_exact_not_estimated() {
let a = sketch_of(&[1, 2, 3, 4]);
let b = sketch_of(&[1, 2, 3, 4, 5]);
assert!(a.is_exact() && b.is_exact());
assert_eq!(a.jaccard(&b), 0.8);
}
#[test]
fn one_changed_leaf_out_of_many_stays_near_one() {
let shared: Vec<u64> = (0..40).collect();
let mut changed = shared.clone();
changed[7] = 1_000;
let similarity = sketch_of(&shared).jaccard(&sketch_of(&changed));
assert!(
similarity > 0.8,
"one differing leaf in 40 scored {similarity}"
);
}
#[test]
fn merge_is_order_independent() {
let forward = sketch_of(&[5, 9, 1, 7, 3]);
let backward = sketch_of(&[3, 7, 1, 9, 5]);
assert_eq!(forward, backward);
}
#[test]
fn merging_is_associative_over_intermediate_nodes() {
let flat = sketch_of(&(0..60).collect::<Vec<_>>());
let nested = SimilaritySketch::merge([
sketch_of(&(0..20).collect::<Vec<_>>()),
SimilaritySketch::merge([
sketch_of(&(20..40).collect::<Vec<_>>()),
sketch_of(&(40..60).collect::<Vec<_>>()),
]),
]);
assert_eq!(flat, nested);
}
#[test]
fn saturated_sketches_estimate_large_set_similarity() {
let a: Vec<u64> = (0..1_000).collect();
let b: Vec<u64> = (500..1_500).collect(); let estimate = sketch_of(&a).jaccard(&sketch_of(&b));
assert!(
(0.1..=0.6).contains(&estimate),
"estimate {estimate} is nowhere near the true 0.333"
);
}
}