Skip to main content

subms_hyperloglog/features/
union_intersect.rs

1//! Set operations on HyperLogLog sketches.
2//!
3//! - `estimate_union(a, b)` is exact in the HLL sense: merge the two
4//!   sketches register-wise and estimate. Same operation as the base
5//!   `merge()` method, just non-destructive.
6//! - `estimate_intersect(a, b)` uses inclusion-exclusion:
7//!   `|A and B| ~= |A| + |B| - |A or B|`. This is the only practical HLL
8//!   intersection. Be aware: when A and B mostly overlap, the variance
9//!   of the subtraction is large relative to the result, so the
10//!   estimator gets noisy. The error bound is `~1.04/sqrt(m) * (|A| +
11//!   |B|)`, not `~1.04/sqrt(m) * |A and B|`. For nearly-disjoint or
12//!   nearly-identical sets, prefer Apache DataSketches' Theta sketches.
13
14use crate::{HllError, HyperLogLog};
15
16/// Distinct count of the union, exact in the HLL sense.
17pub fn estimate_union(a: &HyperLogLog, b: &HyperLogLog) -> Result<f64, HllError> {
18    if a.precision() != b.precision() {
19        return Err(HllError::PrecisionMismatch {
20            left: a.precision(),
21            right: b.precision(),
22        });
23    }
24    let mut merged = HyperLogLog::new(a.precision());
25    let ra = a.registers();
26    let rb = b.registers();
27    // Reach into the merged buffer; the base `merge()` would work
28    // too, but doing one pass keeps the cost obvious.
29    merged.apply_paired_max(ra, rb);
30    Ok(merged.estimate())
31}
32
33/// Distinct count of the intersection via inclusion-exclusion. Clamps to
34/// `>= 0` since a negative estimate is a hard signal of large relative error.
35pub fn estimate_intersect(a: &HyperLogLog, b: &HyperLogLog) -> Result<f64, HllError> {
36    let ea = a.estimate();
37    let eb = b.estimate();
38    let union = estimate_union(a, b)?;
39    let inter = ea + eb - union;
40    Ok(inter.max(0.0))
41}
42
43/// Absolute error the inclusion-exclusion estimate carries at one standard
44/// deviation. It scales with `|A| + |B|`, so a thin overlap between two large
45/// sets can come back with an error bar wider than the answer. Check it
46/// against the estimate before believing an intersection.
47pub fn intersect_error_bound(a: &HyperLogLog, b: &HyperLogLog) -> Result<f64, HllError> {
48    if a.precision() != b.precision() {
49        return Err(HllError::PrecisionMismatch {
50            left: a.precision(),
51            right: b.precision(),
52        });
53    }
54    Ok(a.standard_error() * (a.estimate() + b.estimate()))
55}
56
57// Tiny extension on the base so we don't expose register internals
58// to every caller. Lives here next to the only consumer.
59impl HyperLogLog {
60    pub(crate) fn apply_paired_max(&mut self, a: &[u8], b: &[u8]) {
61        debug_assert_eq!(a.len(), self.registers().len());
62        debug_assert_eq!(b.len(), self.registers().len());
63        for (i, (x, y)) in a.iter().zip(b.iter()).enumerate() {
64            let m = (*x).max(*y);
65            if m > self.registers[i] {
66                self.registers[i] = m;
67            }
68        }
69    }
70}
71
72#[cfg(test)]
73#[path = "union_intersect_tests.rs"]
74mod tests;