use std::collections::hash_map::RandomState;
use std::hash::{BuildHasher, Hash, Hasher};
const TARGET_FPR: f64 = 0.01;
pub const INITIAL_CAPACITY: usize = 4096;
#[derive(Clone)]
pub struct BloomFilter {
bits: Vec<u64>,
num_bits: usize,
num_hashes: u32,
capacity: usize,
count: usize,
seed: RandomState,
}
impl std::fmt::Debug for BloomFilter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("BloomFilter")
.field("num_bits", &self.num_bits)
.field("num_hashes", &self.num_hashes)
.field("capacity", &self.capacity)
.field("count", &self.count)
.finish_non_exhaustive()
}
}
impl Default for BloomFilter {
fn default() -> Self {
Self::with_capacity(INITIAL_CAPACITY)
}
}
impl BloomFilter {
pub fn with_capacity(capacity: usize) -> Self {
let capacity = capacity.max(1);
let m = (-(capacity as f64) * TARGET_FPR.ln() / std::f64::consts::LN_2.powi(2)).ceil() as usize;
let num_words = m.div_ceil(64).max(1);
let num_bits = num_words * 64;
let num_hashes = (((num_bits as f64 / capacity as f64) * std::f64::consts::LN_2).round() as u32).clamp(1, 30);
BloomFilter { bits: vec![0u64; num_words], num_bits, num_hashes, capacity, count: 0, seed: RandomState::new() }
}
pub fn new() -> Self {
Self::default()
}
pub fn capacity(&self) -> usize {
self.capacity
}
pub fn is_saturated(&self) -> bool {
self.count >= self.capacity
}
fn hash_pair(&self, item: &[u8]) -> (u64, u64) {
let mut h1 = self.seed.build_hasher();
item.hash(&mut h1);
let combined = h1.finish();
(combined >> 32, combined & 0xFFFF_FFFF)
}
fn positions(h1: u64, h2: u64, num_bits: usize, num_hashes: u32) -> impl Iterator<Item = usize> {
(0..num_hashes).map(move |i| (h1.wrapping_add((i as u64).wrapping_mul(h2)) as usize) % num_bits)
}
pub fn insert(&mut self, item: &[u8]) {
let (h1, h2) = self.hash_pair(item);
for pos in Self::positions(h1, h2, self.num_bits, self.num_hashes) {
self.bits[pos / 64] |= 1 << (pos % 64);
}
self.count += 1;
}
pub fn might_contain(&self, item: &[u8]) -> bool {
let (h1, h2) = self.hash_pair(item);
Self::positions(h1, h2, self.num_bits, self.num_hashes).all(|pos| self.bits[pos / 64] & (1 << (pos % 64)) != 0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn never_false_negative_for_inserted_items() {
let mut filter = BloomFilter::with_capacity(10_000);
let items: Vec<Vec<u8>> = (0..10_000).map(|i| format!("doc-{i}").into_bytes()).collect();
for item in &items {
filter.insert(item);
}
for item in &items {
assert!(filter.might_contain(item), "inserted item must never be reported as absent");
}
}
#[test]
fn false_positive_rate_is_low_at_designed_capacity() {
let mut filter = BloomFilter::with_capacity(200_000);
let inserted: Vec<Vec<u8>> = (0..200_000).map(|i| format!("doc-{i}").into_bytes()).collect();
for item in &inserted {
filter.insert(item);
}
let mut false_positives = 0;
let probes = 10_000;
for i in 200_000..200_000 + probes {
let candidate = format!("doc-{i}").into_bytes();
if filter.might_contain(&candidate) {
false_positives += 1;
}
}
let fpr = false_positives as f64 / probes as f64;
assert!(fpr < 0.05, "false positive rate {fpr} should be well under 5% at designed capacity (target ~1%)");
}
#[test]
fn false_positive_rate_stays_low_even_far_past_a_small_capacity_if_rebuilt_larger() {
let mut capacity = 1_000;
let mut filter = BloomFilter::with_capacity(capacity);
let mut inserted: Vec<Vec<u8>> = Vec::new();
for i in 0..50_000usize {
if filter.is_saturated() {
capacity *= 2;
let mut bigger = BloomFilter::with_capacity(capacity);
for item in &inserted {
bigger.insert(item);
}
filter = bigger;
}
let item = format!("doc-{i}").into_bytes();
filter.insert(&item);
inserted.push(item);
}
for item in &inserted {
assert!(filter.might_contain(item));
}
let mut false_positives = 0;
let probes = 10_000;
for i in 50_000..50_000 + probes {
if filter.might_contain(format!("doc-{i}").into_bytes().as_slice()) {
false_positives += 1;
}
}
let fpr = false_positives as f64 / probes as f64;
assert!(fpr < 0.05, "false positive rate {fpr} should stay low after growing past the original small capacity");
}
#[test]
fn empty_filter_reports_nothing_present() {
let filter = BloomFilter::new();
assert!(!filter.might_contain(b"anything"));
}
#[test]
fn is_saturated_flips_once_capacity_is_reached() {
let mut filter = BloomFilter::with_capacity(10);
assert!(!filter.is_saturated());
for i in 0..10 {
filter.insert(format!("doc-{i}").as_bytes());
}
assert!(filter.is_saturated());
}
}