Skip to main content

akar_storage/
hyperloglog.rs

1//! HyperLogLog cardinality estimation.
2//!
3//! Implements the HyperLogLog algorithm as described in:
4//!   Flajolet et al., "HyperLogLog: the analysis of a near-optimal
5//!   cardinality estimation algorithm", 2007.
6//!
7//! Uses precision P=6 (M=64 registers) with 8-bit registers,
8//! suitable for estimating up to ~2^32 distinct values.
9//! This matches LadybugDB's configuration.
10
11use std::collections::hash_map::DefaultHasher;
12use std::hash::{Hash, Hasher};
13
14/// Precision parameter — determines the number of registers (M = 2^P).
15const P: u32 = 6;
16/// Number of registers = 64.
17const M: usize = 1 << P;
18/// Q = 64 - P = 58 bits used for the hash prefix.
19const Q: u32 = 64 - P;
20/// Bias correction constant: α = 1 / (M * ∫₀^∞ (log₂((2+u)/(1+u)))^M du) ≈ 0.709
21/// The standard approximation for M=64 is m * α_m where α_64 ≈ 0.709.
22const ALPHA_MM: f64 = 0.709;
23
24/// Linear counting threshold — use LC when raw estimate < threshold.
25const LINEAR_COUNTING_THRESHOLD: f64 = M as f64 * 2.5;
26
27/// HyperLogLog cardinality estimator.
28#[derive(Debug, Clone)]
29pub struct HyperLogLog {
30    /// Register array — each entry is the maximum number of leading zeros + 1 seen.
31    registers: [u8; M],
32}
33
34impl Default for HyperLogLog {
35    fn default() -> Self {
36        Self::new()
37    }
38}
39
40impl HyperLogLog {
41    pub fn new() -> Self {
42        Self { registers: [0u8; M] }
43    }
44
45    /// Hash a value and update the HLL registers.
46    pub fn insert<T: Hash>(&mut self, value: &T) {
47        let hash = Self::hash_value(value);
48        let idx = (hash & ((1 << P) - 1)) as usize; // lower P bits = register index
49        let w = (hash >> P) | (1u64 << Q); // upper Q bits with implicit leading 1
50        let leading_zeros = (w.trailing_zeros() + 1) as u8; // ρ(w) = position of leftmost 1
51        if leading_zeros > self.registers[idx] {
52            self.registers[idx] = leading_zeros;
53        }
54    }
55
56    /// Insert a pre-computed hash value directly.
57    pub fn insert_hash(&mut self, hash: u64) {
58        let idx = (hash & ((1 << P) - 1)) as usize;
59        let w = (hash >> P) | (1u64 << Q);
60        let leading_zeros = (w.trailing_zeros() + 1) as u8;
61        if leading_zeros > self.registers[idx] {
62            self.registers[idx] = leading_zeros;
63        }
64    }
65
66    /// Merge another HLL into this one (union operation).
67    pub fn merge(&mut self, other: &HyperLogLog) {
68        for i in 0..M {
69            if other.registers[i] > self.registers[i] {
70                self.registers[i] = other.registers[i];
71            }
72        }
73    }
74
75    /// Estimate the cardinality (number of distinct values seen).
76    pub fn count(&self) -> u64 {
77        // Raw HyperLogLog estimate
78        let z_inv: f64 = self.registers.iter().map(|&r| 2.0f64.powi(-(r as i32))).sum();
79        let raw_estimate = ALPHA_MM * (M as f64).powi(2) / z_inv;
80
81        // Small range correction: use Linear Counting if estimate is small
82        if raw_estimate <= LINEAR_COUNTING_THRESHOLD {
83            let zero_regs = self.registers.iter().filter(|&&r| r == 0).count() as f64;
84            if zero_regs > 0.0 {
85                return (M as f64 * (M as f64 / zero_regs).ln()).round() as u64;
86            }
87        }
88
89        // Large range correction: no correction needed for M=64 (simple HLL)
90        raw_estimate.round() as u64
91    }
92
93    /// Number of non-zero registers.
94    pub fn non_zero_registers(&self) -> usize {
95        self.registers.iter().filter(|&&r| r > 0).count()
96    }
97
98    /// Reset all registers to zero.
99    pub fn clear(&mut self) {
100        self.registers = [0u8; M];
101    }
102
103    fn hash_value<T: Hash>(value: &T) -> u64 {
104        let mut hasher = DefaultHasher::new();
105        value.hash(&mut hasher);
106        hasher.finish()
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    #[test]
115    fn test_empty_hll() {
116        let hll = HyperLogLog::new();
117        assert_eq!(hll.count(), 0);
118    }
119
120    #[test]
121    fn test_single_element() {
122        let mut hll = HyperLogLog::new();
123        hll.insert(&42i64);
124        let count = hll.count();
125        assert!((1..=5).contains(&count), "Expected ~1, got {}", count);
126    }
127
128    #[test]
129    fn test_small_set() {
130        let mut hll = HyperLogLog::new();
131        for i in 0..100i64 {
132            hll.insert(&i);
133        }
134        let count = hll.count();
135        let error = ((count as f64 - 100.0) / 100.0).abs();
136        assert!(
137            error < 0.30,
138            "Expected ~100, got {} (error: {:.1}%)",
139            count,
140            error * 100.0
141        );
142    }
143
144    #[test]
145    fn test_large_set() {
146        let mut hll = HyperLogLog::new();
147        for i in 0..10_000i64 {
148            hll.insert(&i);
149        }
150        let count = hll.count();
151        let error = ((count as f64 - 10_000.0) / 10_000.0).abs();
152        assert!(
153            error < 0.15,
154            "Expected ~10000, got {} (error: {:.1}%)",
155            count,
156            error * 100.0
157        );
158    }
159
160    #[test]
161    fn test_merge() {
162        let mut hll1 = HyperLogLog::new();
163        let mut hll2 = HyperLogLog::new();
164        for i in 0..500i64 {
165            hll1.insert(&i);
166        }
167        for i in 250..750i64 {
168            hll2.insert(&i);
169        }
170        hll1.merge(&hll2);
171        let count = hll1.count();
172        let error = ((count as f64 - 750.0) / 750.0).abs();
173        assert!(
174            error < 0.35,
175            "Expected ~750, got {} (error: {:.1}%)",
176            count,
177            error * 100.0
178        );
179    }
180
181    #[test]
182    fn test_clear() {
183        let mut hll = HyperLogLog::new();
184        for i in 0..1_000i64 {
185            hll.insert(&i);
186        }
187        assert!(hll.count() > 0);
188        hll.clear();
189        assert_eq!(hll.count(), 0);
190    }
191}