use std::hash::{BuildHasher, Hash};
#[derive(Debug, Clone)]
pub struct BloomFilter<S = std::hash::RandomState> {
words: Vec<u64>,
bits: usize,
hashes: usize,
hasher_builder: S,
}
impl BloomFilter<std::hash::RandomState> {
pub fn with_capacity(expected_items: usize, fp_rate: f64) -> Self {
Self::with_capacity_and_hasher(expected_items, fp_rate, std::hash::RandomState::new())
}
}
impl<S: BuildHasher> BloomFilter<S> {
pub fn with_capacity_and_hasher(
expected_items: usize,
fp_rate: f64,
hasher_builder: S,
) -> Self {
let n = expected_items.max(1) as f64;
let p = fp_rate.clamp(f64::MIN_POSITIVE, 1.0 - f64::EPSILON);
let ln2 = std::f64::consts::LN_2;
let bits = (-n * p.ln() / (ln2 * ln2)).ceil() as usize;
let bits = bits.max(64);
let hashes = (((bits as f64 / n) * ln2).round() as usize).clamp(1, 64);
let words = bits.div_ceil(64);
Self {
words: vec![0u64; words],
bits,
hashes,
hasher_builder,
}
}
pub fn bit_len(&self) -> usize {
self.bits
}
pub fn hash_count(&self) -> usize {
self.hashes
}
fn h1_h2(&self, item: &impl Hash) -> (u32, u32) {
let h = self.hasher_builder.hash_one(item);
let h1 = (h >> 32) as u32;
let h2 = (h as u32) | 1;
(h1, h2)
}
pub fn insert(&mut self, item: impl Hash) -> bool {
let (h1, h2) = self.h1_h2(&item);
let mut newly = false;
for i in 0..self.hashes {
let bit = (h1.wrapping_add((i as u32).wrapping_mul(h2)) as usize) % self.bits;
let word = bit / 64;
let mask = 1u64 << (bit % 64);
if self.words[word] & mask == 0 {
newly = true;
self.words[word] |= mask;
}
}
newly
}
pub fn contains(&self, item: &impl Hash) -> bool {
let (h1, h2) = self.h1_h2(item);
for i in 0..self.hashes {
let bit = (h1.wrapping_add((i as u32).wrapping_mul(h2)) as usize) % self.bits;
let word = bit / 64;
let mask = 1u64 << (bit % 64);
if self.words[word] & mask == 0 {
return false;
}
}
true
}
pub fn clear(&mut self) {
for w in &mut self.words {
*w = 0;
}
}
pub fn is_empty(&self) -> bool {
self.words.iter().all(|&w| w == 0)
}
pub fn saturation(&self) -> f64 {
let set: u32 = self.words.iter().map(|w| w.count_ones()).sum();
set as f64 / self.bits as f64
}
}
impl<S: BuildHasher> crate::correlate::Mergeable for BloomFilter<S> {
fn merge(&mut self, other: Self) {
assert_eq!(
(self.bits, self.hashes),
(other.bits, other.hashes),
"BloomFilter::merge requires matching bit length and hash count",
);
for (a, b) in self.words.iter_mut().zip(other.words) {
*a |= b;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::correlate::Mergeable;
use std::hash::BuildHasherDefault;
type FixedBloom = BloomFilter<BuildHasherDefault<std::collections::hash_map::DefaultHasher>>;
fn new_fixed(n: usize, p: f64) -> FixedBloom {
BloomFilter::with_capacity_and_hasher(n, p, BuildHasherDefault::default())
}
#[test]
fn no_false_negatives() {
let mut b = new_fixed(10_000, 0.01);
for i in 0..10_000u64 {
b.insert(i);
}
for i in 0..10_000u64 {
assert!(b.contains(&i), "false negative for {i}");
}
}
#[test]
fn false_positive_rate_near_target() {
let mut b = new_fixed(10_000, 0.01);
for i in 0..10_000u64 {
b.insert(i);
}
let mut fp = 0;
let trials = 100_000u64;
for i in 1_000_000..1_000_000 + trials {
if b.contains(&i) {
fp += 1;
}
}
let rate = fp as f64 / trials as f64;
assert!(rate < 0.03, "fp rate {rate} too high (target 0.01)");
}
#[test]
fn insert_reports_newness() {
let mut b = new_fixed(1000, 0.01);
assert!(b.insert("first"));
assert!(!b.insert("first")); assert!(b.insert("second"));
}
#[test]
fn merge_is_union() {
let mut a = new_fixed(10_000, 0.01);
let mut b = new_fixed(10_000, 0.01);
for i in 0..5000u64 {
a.insert(i);
}
for i in 5000..10_000u64 {
b.insert(i);
}
a.merge(b);
for i in 0..10_000u64 {
assert!(a.contains(&i), "merged filter missing {i}");
}
}
#[test]
#[should_panic(expected = "matching bit length")]
fn merge_panics_on_mismatch() {
let mut a = new_fixed(1000, 0.01);
let b = new_fixed(5000, 0.01);
a.merge(b);
}
#[test]
fn clear_resets() {
let mut b = new_fixed(1000, 0.01);
b.insert("x");
assert!(!b.is_empty());
b.clear();
assert!(b.is_empty());
assert!(!b.contains(&"x"));
}
}