Skip to main content

rust_zero_core/
bloom.rs

1use std::{
2    fmt,
3    sync::{
4        atomic::{AtomicUsize, Ordering},
5        Mutex,
6    },
7};
8
9/// A thread-safe in-memory Bloom filter.
10///
11/// Bloom filters never produce false negatives. A positive result means that an item may exist,
12/// while a negative result guarantees that it has not been inserted.
13#[derive(Debug)]
14pub struct BloomFilter {
15    bits: Mutex<Vec<u64>>,
16    bit_count: usize,
17    hash_functions: u32,
18    insertions: AtomicUsize,
19}
20
21impl BloomFilter {
22    /// Creates a filter with an explicit number of bits and hash functions.
23    pub fn new(bit_count: usize, hash_functions: u32) -> Result<Self, BloomError> {
24        if bit_count == 0 {
25            return Err(BloomError::ZeroBits);
26        }
27        if hash_functions == 0 {
28            return Err(BloomError::ZeroHashFunctions);
29        }
30
31        Ok(Self {
32            bits: Mutex::new(vec![0; bit_count.div_ceil(64)]),
33            bit_count,
34            hash_functions,
35            insertions: AtomicUsize::new(0),
36        })
37    }
38
39    /// Sizes a filter for the expected number of items and desired false-positive rate.
40    pub fn with_rate(expected_items: usize, false_positive_rate: f64) -> Result<Self, BloomError> {
41        if expected_items == 0 {
42            return Err(BloomError::ZeroExpectedItems);
43        }
44        if !(0.0..1.0).contains(&false_positive_rate) {
45            return Err(BloomError::InvalidFalsePositiveRate(false_positive_rate));
46        }
47
48        let item_count = expected_items as f64;
49        let logarithm = std::f64::consts::LN_2;
50        let bit_count =
51            (-(item_count * false_positive_rate.ln()) / logarithm.powi(2)).ceil() as usize;
52        let hash_functions = ((bit_count as f64 / item_count) * logarithm)
53            .round()
54            .max(1.0) as u32;
55        Self::new(bit_count, hash_functions)
56    }
57
58    /// Inserts an item and reports whether at least one bit changed.
59    pub fn insert(&self, value: impl AsRef<[u8]>) -> bool {
60        let (first, second) = hashes(value.as_ref());
61        let mut bits = self.bits.lock().expect("Bloom filter mutex poisoned");
62        let mut changed = false;
63
64        for index in self.indices(first, second) {
65            let word = index / 64;
66            let mask = 1_u64 << (index % 64);
67            changed |= bits[word] & mask == 0;
68            bits[word] |= mask;
69        }
70
71        if changed {
72            self.insertions.fetch_add(1, Ordering::Relaxed);
73        }
74        changed
75    }
76
77    /// Returns `false` only when the item definitely has not been inserted.
78    pub fn contains(&self, value: impl AsRef<[u8]>) -> bool {
79        let (first, second) = hashes(value.as_ref());
80        let bits = self.bits.lock().expect("Bloom filter mutex poisoned");
81        self.indices(first, second).all(|index| {
82            let word = index / 64;
83            let mask = 1_u64 << (index % 64);
84            bits[word] & mask != 0
85        })
86    }
87
88    pub fn bit_count(&self) -> usize {
89        self.bit_count
90    }
91
92    pub fn hash_functions(&self) -> u32 {
93        self.hash_functions
94    }
95
96    /// Returns the number of insert calls that changed the filter.
97    pub fn insertions(&self) -> usize {
98        self.insertions.load(Ordering::Relaxed)
99    }
100
101    pub fn clear(&self) {
102        self.bits
103            .lock()
104            .expect("Bloom filter mutex poisoned")
105            .fill(0);
106        self.insertions.store(0, Ordering::Relaxed);
107    }
108
109    fn indices(&self, first: u64, second: u64) -> impl Iterator<Item = usize> + '_ {
110        (0..self.hash_functions).map(move |iteration| {
111            first
112                .wrapping_add(u64::from(iteration).wrapping_mul(second))
113                .wrapping_rem(self.bit_count as u64) as usize
114        })
115    }
116}
117
118fn hashes(bytes: &[u8]) -> (u64, u64) {
119    const OFFSET_BASIS: u64 = 0xcbf29ce484222325;
120    const PRIME: u64 = 0x100000001b3;
121
122    let first = bytes.iter().fold(OFFSET_BASIS, |hash, byte| {
123        (hash ^ u64::from(*byte)).wrapping_mul(PRIME)
124    });
125    let second = bytes
126        .iter()
127        .rev()
128        .fold(OFFSET_BASIS ^ 0x9e3779b97f4a7c15, |hash, byte| {
129            (hash ^ u64::from(*byte)).wrapping_mul(PRIME)
130        })
131        | 1;
132    (first, second)
133}
134
135#[derive(Debug, Clone, PartialEq)]
136pub enum BloomError {
137    ZeroBits,
138    ZeroHashFunctions,
139    ZeroExpectedItems,
140    InvalidFalsePositiveRate(f64),
141}
142
143impl fmt::Display for BloomError {
144    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
145        match self {
146            Self::ZeroBits => {
147                formatter.write_str("Bloom filter bit count must be greater than zero")
148            }
149            Self::ZeroHashFunctions => {
150                formatter.write_str("Bloom filter hash count must be greater than zero")
151            }
152            Self::ZeroExpectedItems => {
153                formatter.write_str("Bloom filter expected item count must be greater than zero")
154            }
155            Self::InvalidFalsePositiveRate(rate) => write!(
156                formatter,
157                "Bloom filter false-positive rate must be between zero and one: {rate}"
158            ),
159        }
160    }
161}
162
163impl std::error::Error for BloomError {}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168
169    #[test]
170    fn inserted_items_are_never_missing() {
171        let filter = BloomFilter::with_rate(1_000, 0.001).unwrap();
172        for item in 0..1_000 {
173            filter.insert(item.to_string());
174        }
175
176        for item in 0..1_000 {
177            assert!(filter.contains(item.to_string()));
178        }
179        assert_eq!(filter.insertions(), 1_000);
180    }
181
182    #[test]
183    fn reports_absent_items_and_can_be_cleared() {
184        let filter = BloomFilter::new(1_024, 4).unwrap();
185        filter.insert("known");
186
187        assert!(filter.contains("known"));
188        assert!(!filter.contains("definitely-absent"));
189        filter.clear();
190        assert!(!filter.contains("known"));
191        assert_eq!(filter.insertions(), 0);
192    }
193
194    #[test]
195    fn validates_filter_dimensions() {
196        assert_eq!(BloomFilter::new(0, 1).unwrap_err(), BloomError::ZeroBits);
197        assert_eq!(
198            BloomFilter::new(1, 0).unwrap_err(),
199            BloomError::ZeroHashFunctions
200        );
201        assert!(matches!(
202            BloomFilter::with_rate(100, 1.0),
203            Err(BloomError::InvalidFalsePositiveRate(1.0))
204        ));
205    }
206}