use rudb_common::{Error, Result};
pub const DEFAULT_K: usize = 4096;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Sketch {
k: usize,
hashes: Vec<u64>,
}
impl Sketch {
pub fn new(k: usize) -> Result<Self> {
if k == 0 {
return Err(Error::internal("a sketch that keeps no hashes estimates nothing"));
}
Ok(Self { k, hashes: Vec::new() })
}
#[must_use]
pub fn of(values: &[&[u8]]) -> Self {
let mut sketch = Self { k: DEFAULT_K, hashes: Vec::new() };
for value in values {
sketch.add(value);
}
sketch
}
pub fn add(&mut self, value: &[u8]) {
self.add_hash(hash64(value));
}
pub fn add_hash(&mut self, hash: u64) {
if self.hashes.len() == self.k {
match self.hashes.last() {
Some(largest) if hash >= *largest => return,
_ => {}
}
}
match self.hashes.binary_search(&hash) {
Ok(_) => {}
Err(at) => {
self.hashes.insert(at, hash);
self.hashes.truncate(self.k);
}
}
}
#[must_use]
pub fn len(&self) -> usize {
self.hashes.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.hashes.is_empty()
}
#[must_use]
pub fn is_exact(&self) -> bool {
self.hashes.len() < self.k
}
#[must_use]
pub fn distinct(&self) -> f64 {
if self.is_exact() {
return self.hashes.len() as f64;
}
let largest = self.hashes[self.hashes.len() - 1] as f64 / u64::MAX as f64;
if largest <= 0.0 {
return self.hashes.len() as f64;
}
(self.k as f64 - 1.0) / largest
}
pub fn union(&self, other: &Self) -> Result<Self> {
if self.k != other.k {
return Err(Error::internal(format!(
"sketches of {} and {} hashes cannot be combined",
self.k, other.k
)));
}
let mut merged = Self { k: self.k, hashes: Vec::with_capacity(self.k) };
let mut left = self.hashes.iter().peekable();
let mut right = other.hashes.iter().peekable();
while merged.hashes.len() < self.k {
let next = match (left.peek(), right.peek()) {
(Some(a), Some(b)) => {
if a <= b {
left.next()
} else {
right.next()
}
}
(Some(_), None) => left.next(),
(None, Some(_)) => right.next(),
(None, None) => break,
};
let Some(hash) = next else {
break;
};
if merged.hashes.last() != Some(hash) {
merged.hashes.push(*hash);
}
}
Ok(merged)
}
pub fn jaccard(&self, other: &Self) -> Result<f64> {
let union = self.union(other)?;
if union.is_empty() {
return Ok(0.0);
}
let both =
union.hashes.iter().filter(|hash| self.holds(**hash) && other.holds(**hash)).count();
Ok(both as f64 / union.hashes.len() as f64)
}
fn holds(&self, hash: u64) -> bool {
self.hashes.binary_search(&hash).is_ok()
}
}
pub fn dependence(left: &Sketch, pairs: &Sketch) -> Result<f64> {
if left.k != pairs.k {
return Err(Error::internal("a column and its pairs need sketches of the same size"));
}
let alone = left.distinct();
let together = pairs.distinct();
if alone <= 0.0 {
return Ok(1.0);
}
Ok((alone / together.max(alone)).min(1.0))
}
#[must_use]
pub fn pair_hash(left: &[u8], right: &[u8]) -> u64 {
mix(hash64(left) ^ SEEDS[3], hash64(right).wrapping_add(SEEDS[2]))
}
const SEEDS: [u64; 4] =
[0xa076_1d64_78bd_642f, 0xe703_7ed1_a0b4_28db, 0x8ebc_6af0_9c88_c6e3, 0x5899_65cc_7537_4cc3];
fn mix(left: u64, right: u64) -> u64 {
let wide = u128::from(left).wrapping_mul(u128::from(right));
(wide as u64) ^ ((wide >> 64) as u64)
}
#[must_use]
pub fn hash64(value: &[u8]) -> u64 {
let mut state = SEEDS[0] ^ mix(value.len() as u64, SEEDS[1]);
let mut chunks = value.chunks_exact(8);
let mut word = [0u8; 8];
for chunk in &mut chunks {
word.copy_from_slice(chunk);
state = mix(state ^ u64::from_le_bytes(word), SEEDS[2]);
}
let rest = chunks.remainder();
if !rest.is_empty() {
let mut last = [0u8; 8];
last[..rest.len()].copy_from_slice(rest);
state = mix(state ^ u64::from_le_bytes(last), SEEDS[3]);
}
mix(state, SEEDS[1])
}
#[cfg(test)]
mod tests {
use super::*;
fn values(count: usize, prefix: &str) -> Vec<Vec<u8>> {
(0..count).map(|index| format!("{prefix}{index}").into_bytes()).collect()
}
fn borrow(values: &[Vec<u8>]) -> Vec<&[u8]> {
values.iter().map(Vec::as_slice).collect()
}
fn within(estimate: f64, actual: f64, tolerance: f64) -> bool {
(estimate - actual).abs() / actual <= tolerance
}
#[test]
fn a_sketch_that_never_filled_up_is_exact() {
let column = values(1000, "value-");
let sketch = Sketch::of(&borrow(&column));
assert!(sketch.is_exact());
assert_eq!(sketch.distinct(), 1000.0);
}
#[test]
fn duplicates_do_not_count() {
let mut sketch = Sketch::new(64).unwrap();
for _ in 0..1000 {
sketch.add(b"the same value");
}
assert_eq!(sketch.distinct(), 1.0);
}
#[test]
fn the_distinct_count_is_within_two_percent_at_the_default_k() {
for count in [50_000usize, 250_000, 1_000_000] {
let mut sketch = Sketch::new(DEFAULT_K).unwrap();
for index in 0..count {
sketch.add(format!("http://example.com/page/{index}").as_bytes());
}
assert!(!sketch.is_exact());
let estimate = sketch.distinct();
assert!(
within(estimate, count as f64, 0.02),
"{estimate:.0} against {count} distinct values"
);
}
}
#[test]
fn the_sketch_does_not_depend_on_the_order_values_arrived_in() {
let column = values(100_000, "value-");
let forwards = Sketch::of(&borrow(&column));
let mut backwards = Sketch::new(DEFAULT_K).unwrap();
for value in column.iter().rev() {
backwards.add(value);
}
assert_eq!(forwards, backwards);
}
#[test]
fn two_columns_with_the_same_values_overlap_completely() {
let column = values(200_000, "http://example.com/");
let left = Sketch::of(&borrow(&column));
let right = Sketch::of(&borrow(&column));
assert_eq!(left.jaccard(&right).unwrap(), 1.0);
}
#[test]
fn two_columns_with_nothing_in_common_do_not_overlap() {
let left = Sketch::of(&borrow(&values(200_000, "left-")));
let right = Sketch::of(&borrow(&values(200_000, "right-")));
assert_eq!(left.jaccard(&right).unwrap(), 0.0);
}
#[test]
fn a_half_overlap_measures_as_a_third() {
let shared = values(50_000, "shared-");
let mut left = shared.clone();
left.extend(values(50_000, "left-"));
let mut right = shared;
right.extend(values(50_000, "right-"));
let overlap = Sketch::of(&borrow(&left)).jaccard(&Sketch::of(&borrow(&right))).unwrap();
assert!(within(overlap, 1.0 / 3.0, 0.05), "{overlap:.4}");
}
#[test]
fn the_union_of_two_sketches_counts_the_union_of_the_columns() {
let left = values(300_000, "left-");
let right = values(300_000, "right-");
let union = Sketch::of(&borrow(&left)).union(&Sketch::of(&borrow(&right))).unwrap();
assert!(within(union.distinct(), 600_000.0, 0.03), "{:.0}", union.distinct());
}
#[test]
fn sketches_of_different_sizes_do_not_combine() {
let small = Sketch::new(16).unwrap();
let large = Sketch::new(32).unwrap();
assert!(small.union(&large).is_err());
assert!(small.jaccard(&large).is_err());
}
#[test]
fn a_sketch_that_keeps_nothing_is_rejected() {
assert!(Sketch::new(0).is_err());
}
#[test]
fn a_functional_dependency_shows_up_as_a_dependence_of_one() {
let urls = values(200_000, "http://example.com/page/");
let mut left = Sketch::new(DEFAULT_K).unwrap();
let mut pairs = Sketch::new(DEFAULT_K).unwrap();
for url in &urls {
let derived = hash64(url).to_le_bytes();
left.add(url);
pairs.add_hash(pair_hash(url, &derived));
}
let score = dependence(&left, &pairs).unwrap();
assert!(score > 0.97, "{score:.4}");
}
#[test]
fn two_independent_columns_do_not_look_like_a_dependency() {
let left = values(1000, "left-");
let right = values(1000, "right-");
let mut alone = Sketch::new(DEFAULT_K).unwrap();
let mut pairs = Sketch::new(DEFAULT_K).unwrap();
for left_value in &left {
alone.add(left_value);
for right_value in &right {
pairs.add_hash(pair_hash(left_value, right_value));
}
}
let score = dependence(&alone, &pairs).unwrap();
assert!(score < 0.01, "{score:.4}");
}
#[test]
fn the_pair_hash_does_not_ignore_where_the_boundary_is() {
assert_ne!(pair_hash(b"ab", b"c"), pair_hash(b"a", b"bc"));
assert_ne!(pair_hash(b"a", b"b"), pair_hash(b"b", b"a"));
}
#[test]
fn the_hash_spreads_one_bit_changes_across_the_output() {
let mut total = 0u32;
let mut trials = 0u32;
for index in 0..2000u32 {
let value = index.to_le_bytes();
let base = hash64(&value);
for bit in 0..32 {
let mut flipped = value;
flipped[bit / 8] ^= 1 << (bit % 8);
total += (base ^ hash64(&flipped)).count_ones();
trials += 1;
}
}
let average = f64::from(total) / f64::from(trials);
assert!((average - 32.0).abs() < 1.0, "{average:.3} bits changed on average");
}
#[test]
fn the_hash_does_not_collide_on_values_that_differ_by_one_byte() {
let mut hashes: Vec<u64> = (0..200_000u32)
.map(|index| hash64(format!("http://a/{index:09}").as_bytes()))
.collect();
hashes.sort_unstable();
let before = hashes.len();
hashes.dedup();
assert_eq!(hashes.len(), before);
}
#[test]
fn a_long_value_and_its_prefix_hash_differently() {
assert_ne!(hash64(b""), hash64(b"\0"));
assert_ne!(hash64(b"abcdefgh"), hash64(b"abcdefgh\0"));
assert_ne!(hash64(&[0u8; 16]), hash64(&[0u8; 24]));
}
}