use crate::common::SketchError;
use rand::rngs::SmallRng;
use rand::{Rng, SeedableRng};
use xxhash_rust::xxh64::xxh64;
const DEFAULT_COUNTER_BITS: u8 = 3;
#[derive(Clone, Debug)]
pub struct StableBloomFilter {
counters: Vec<u8>,
m: usize,
k: usize,
counter_bits: u8,
max_counter: u8,
p: usize,
rng: SmallRng,
}
impl StableBloomFilter {
pub fn new(expected_items: usize, fpr: f64) -> Result<Self, SketchError> {
Self::with_params(expected_items, fpr, DEFAULT_COUNTER_BITS, 0x12345678)
}
pub fn with_params(
expected_items: usize,
fpr: f64,
counter_bits: u8,
seed: u64,
) -> Result<Self, SketchError> {
if expected_items == 0 {
return Err(SketchError::InvalidParameter {
param: "expected_items".to_string(),
value: "0".to_string(),
constraint: "must be > 0".to_string(),
});
}
if fpr <= 0.0 || fpr >= 1.0 {
return Err(SketchError::InvalidParameter {
param: "fpr".to_string(),
value: fpr.to_string(),
constraint: "must be in (0, 1)".to_string(),
});
}
if counter_bits == 0 || counter_bits > 8 {
return Err(SketchError::InvalidParameter {
param: "counter_bits".to_string(),
value: counter_bits.to_string(),
constraint: "must be in [1, 8]".to_string(),
});
}
let m =
((-(expected_items as f64) * fpr.ln()) / (2.0_f64.ln().powi(2)) * 2.0).ceil() as usize;
let m = m.max(64);
let k = ((m as f64 / expected_items as f64) * 2.0_f64.ln()).ceil() as usize;
let k = k.clamp(1, 30);
let max_counter = (1u16 << counter_bits) - 1;
let p = ((m * k) as f64 / (expected_items as f64 * max_counter as f64)).ceil() as usize;
let p = p.clamp(1, m);
let counters_per_byte = 8 / counter_bits as usize;
let byte_count = m.div_ceil(counters_per_byte);
Ok(StableBloomFilter {
counters: vec![0; byte_count],
m,
k,
counter_bits,
max_counter: max_counter as u8,
p,
rng: SmallRng::seed_from_u64(seed),
})
}
pub fn num_counters(&self) -> usize {
self.m
}
pub fn num_hashes(&self) -> usize {
self.k
}
pub fn decrement_count(&self) -> usize {
self.p
}
pub fn insert(&mut self, key: &[u8]) {
for _ in 0..self.p {
let idx = self.rng.random_range(0..self.m);
self.decrement_counter(idx);
}
let indices = self.hash_indices(key);
for idx in indices {
self.set_counter(idx, self.max_counter);
}
}
pub fn contains(&self, key: &[u8]) -> bool {
let indices = self.hash_indices(key);
indices.into_iter().all(|idx| self.get_counter(idx) > 0)
}
pub fn get_count(&self, key: &[u8]) -> u8 {
let indices = self.hash_indices(key);
indices
.into_iter()
.map(|idx| self.get_counter(idx))
.min()
.unwrap_or(0)
}
#[inline]
fn hash_indices(&self, key: &[u8]) -> Vec<usize> {
let h1 = xxh64(key, 0);
let h2 = xxh64(key, h1);
(0..self.k)
.map(|i| {
let combined = h1.wrapping_add((i as u64).wrapping_mul(h2));
(combined as usize) % self.m
})
.collect()
}
#[inline]
fn get_counter(&self, idx: usize) -> u8 {
let counters_per_byte = 8 / self.counter_bits as usize;
let byte_idx = idx / counters_per_byte;
let offset = (idx % counters_per_byte) * self.counter_bits as usize;
let mask = self.max_counter;
(self.counters[byte_idx] >> offset) & mask
}
#[inline]
fn set_counter(&mut self, idx: usize, value: u8) {
let counters_per_byte = 8 / self.counter_bits as usize;
let byte_idx = idx / counters_per_byte;
let offset = (idx % counters_per_byte) * self.counter_bits as usize;
let mask = self.max_counter;
self.counters[byte_idx] &= !(mask << offset);
self.counters[byte_idx] |= (value & mask) << offset;
}
#[inline]
fn decrement_counter(&mut self, idx: usize) {
let current = self.get_counter(idx);
if current > 0 {
self.set_counter(idx, current - 1);
}
}
pub fn fill_ratio(&self) -> f64 {
let non_zero = (0..self.m).filter(|&i| self.get_counter(i) > 0).count();
non_zero as f64 / self.m as f64
}
pub fn clear(&mut self) {
self.counters.fill(0);
}
pub fn memory_usage(&self) -> usize {
self.counters.len()
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut bytes = Vec::new();
bytes.extend_from_slice(&(self.m as u64).to_le_bytes());
bytes.extend_from_slice(&(self.k as u64).to_le_bytes());
bytes.push(self.counter_bits);
bytes.push(self.max_counter);
bytes.extend_from_slice(&(self.p as u64).to_le_bytes());
bytes.extend_from_slice(&(self.counters.len() as u64).to_le_bytes());
bytes.extend_from_slice(&self.counters);
bytes
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, SketchError> {
if bytes.len() < 34 {
return Err(SketchError::DeserializationError(
"Insufficient data for StableBloomFilter header".to_string(),
));
}
let m = u64::from_le_bytes(bytes[0..8].try_into().unwrap()) as usize;
let k = u64::from_le_bytes(bytes[8..16].try_into().unwrap()) as usize;
let counter_bits = bytes[16];
let max_counter = bytes[17];
let p = u64::from_le_bytes(bytes[18..26].try_into().unwrap()) as usize;
let counter_len = u64::from_le_bytes(bytes[26..34].try_into().unwrap()) as usize;
if bytes.len() < 34 + counter_len {
return Err(SketchError::DeserializationError(format!(
"Expected {} bytes, got {}",
34 + counter_len,
bytes.len()
)));
}
let counters = bytes[34..34 + counter_len].to_vec();
Ok(StableBloomFilter {
counters,
m,
k,
counter_bits,
max_counter,
p,
rng: SmallRng::from_os_rng(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new() {
let filter = StableBloomFilter::new(1000, 0.01).unwrap();
assert!(filter.num_counters() > 0);
assert!(filter.num_hashes() > 0);
}
#[test]
fn test_invalid_params() {
assert!(StableBloomFilter::new(0, 0.01).is_err());
assert!(StableBloomFilter::new(1000, 0.0).is_err());
assert!(StableBloomFilter::new(1000, 1.0).is_err());
assert!(StableBloomFilter::with_params(1000, 0.01, 0, 0).is_err());
assert!(StableBloomFilter::with_params(1000, 0.01, 9, 0).is_err());
}
#[test]
fn test_insert_contains() {
let mut filter = StableBloomFilter::new(1000, 0.01).unwrap();
filter.insert(b"hello");
assert!(filter.contains(b"hello"));
}
#[test]
fn test_multiple_inserts() {
let mut filter = StableBloomFilter::new(1000, 0.01).unwrap();
filter.insert(b"key1");
filter.insert(b"key2");
filter.insert(b"key3");
assert!(filter.contains(b"key1"));
assert!(filter.contains(b"key2"));
assert!(filter.contains(b"key3"));
}
#[test]
fn test_not_contains() {
let filter = StableBloomFilter::new(1000, 0.01).unwrap();
assert!(!filter.contains(b"missing"));
}
#[test]
fn test_stability() {
let mut filter = StableBloomFilter::with_params(100, 0.1, 2, 42).unwrap();
for i in 0..10000u64 {
filter.insert(&i.to_le_bytes());
}
let fill = filter.fill_ratio();
assert!(fill < 1.0, "Filter should not saturate: fill={}", fill);
assert!(
fill > 0.5,
"Filter should maintain some data: fill={}",
fill
);
}
#[test]
fn test_get_count() {
let mut filter = StableBloomFilter::new(1000, 0.01).unwrap();
filter.insert(b"hello");
let count = filter.get_count(b"hello");
assert!(count > 0);
let count_missing = filter.get_count(b"missing");
assert_eq!(count_missing, 0);
}
#[test]
fn test_clear() {
let mut filter = StableBloomFilter::new(1000, 0.01).unwrap();
filter.insert(b"hello");
assert!(filter.contains(b"hello"));
filter.clear();
assert!(!filter.contains(b"hello"));
assert_eq!(filter.fill_ratio(), 0.0);
}
#[test]
fn test_serialization() {
let mut filter = StableBloomFilter::new(1000, 0.01).unwrap();
filter.insert(b"key1");
filter.insert(b"key2");
let bytes = filter.to_bytes();
let restored = StableBloomFilter::from_bytes(&bytes).unwrap();
assert!(restored.contains(b"key1"));
assert!(restored.contains(b"key2"));
assert_eq!(filter.num_counters(), restored.num_counters());
assert_eq!(filter.num_hashes(), restored.num_hashes());
}
#[test]
fn test_different_counter_bits() {
for bits in 1..=8 {
let filter = StableBloomFilter::with_params(100, 0.1, bits, 0).unwrap();
let expected_max = if bits == 8 { 255u8 } else { (1u8 << bits) - 1 };
assert_eq!(filter.max_counter, expected_max);
}
}
#[test]
fn test_counter_packing() {
let mut filter = StableBloomFilter::with_params(100, 0.1, 2, 42).unwrap();
filter.insert(b"test");
assert!(filter.contains(b"test"));
}
#[test]
fn test_memory_usage() {
let filter = StableBloomFilter::new(1000, 0.01).unwrap();
let memory = filter.memory_usage();
assert!(memory > 0);
}
}