Skip to main content

BloomFilter

Struct BloomFilter 

Source
pub struct BloomFilter { /* private fields */ }

Implementations§

Source§

impl BloomFilter

Source

pub fn new(expected_entries: usize) -> Self

Build an empty filter sized for expected_entries at ~1% FPR (10 bits/key, k=7). The 64-bit floor matters when expected_entries is small.

Examples found in repository?
examples/sample_app.rs (line 33)
31fn base_crawler_dedup() {
32    println!("== base: crawler URL dedup ==");
33    let mut seen = BloomFilter::new(10_000);
34    let frontier = [
35        "https://a.example/",
36        "https://b.example/",
37        "https://a.example/", // dup
38        "https://c.example/",
39        "https://b.example/", // dup
40    ];
41
42    let (mut fetched, mut skipped) = (0usize, 0usize);
43    for url in frontier {
44        if seen.might_contain(url) {
45            println!("  skip  {url}");
46            skipped += 1;
47        } else {
48            seen.add(url);
49            println!("  fetch {url}");
50            fetched += 1;
51        }
52    }
53    println!("  -> fetched {fetched}, skipped {skipped}");
54    for url in [
55        "https://a.example/",
56        "https://b.example/",
57        "https://c.example/",
58    ] {
59        assert!(seen.might_contain(url), "no false negatives");
60    }
61}
62
63/// Each shard builds its own filter over the symbols it saw, then the gateway
64/// ORs them into one membership set. `union` only accepts identical geometry,
65/// so every shard must be constructed with the same expected count.
66/// `estimated_fpp` reports occupancy against the design point, which is how you
67/// find out a filter has outgrown its sizing before the false positives do.
68fn shard_merge_and_occupancy() {
69    println!(
70        "
71== merge: per-shard filters unioned at the gateway =="
72    );
73    let capacity = 10_000;
74    let mut gateway = BloomFilter::new(capacity);
75    for shard in 0..4 {
76        let mut local = BloomFilter::new(capacity);
77        for i in 0..500 {
78            local.add(&format!("shard{shard}-sym{i}"));
79        }
80        gateway.union(&local).expect("shards share one geometry");
81    }
82    println!("  merged 4 shards x 500 symbols");
83    println!(
84        "  approx distinct keys: {}",
85        gateway.approximate_element_count()
86    );
87    println!(
88        "  occupancy fpp:        {:.4}%",
89        gateway.estimated_fpp() * 100.0
90    );
91
92    let mismatched = BloomFilter::new(capacity * 2);
93    match gateway.union(&mismatched) {
94        Err(e) => println!("  refused mismatched shard: {e}"),
95        Ok(()) => unreachable!("geometry check must reject this"),
96    }
97
98    gateway.clear();
99    println!(
100        "  after clear -> approx distinct keys: {}",
101        gateway.approximate_element_count()
102    );
103}
Source

pub fn add(&mut self, key: &str)

Examples found in repository?
examples/sample_app.rs (line 48)
31fn base_crawler_dedup() {
32    println!("== base: crawler URL dedup ==");
33    let mut seen = BloomFilter::new(10_000);
34    let frontier = [
35        "https://a.example/",
36        "https://b.example/",
37        "https://a.example/", // dup
38        "https://c.example/",
39        "https://b.example/", // dup
40    ];
41
42    let (mut fetched, mut skipped) = (0usize, 0usize);
43    for url in frontier {
44        if seen.might_contain(url) {
45            println!("  skip  {url}");
46            skipped += 1;
47        } else {
48            seen.add(url);
49            println!("  fetch {url}");
50            fetched += 1;
51        }
52    }
53    println!("  -> fetched {fetched}, skipped {skipped}");
54    for url in [
55        "https://a.example/",
56        "https://b.example/",
57        "https://c.example/",
58    ] {
59        assert!(seen.might_contain(url), "no false negatives");
60    }
61}
62
63/// Each shard builds its own filter over the symbols it saw, then the gateway
64/// ORs them into one membership set. `union` only accepts identical geometry,
65/// so every shard must be constructed with the same expected count.
66/// `estimated_fpp` reports occupancy against the design point, which is how you
67/// find out a filter has outgrown its sizing before the false positives do.
68fn shard_merge_and_occupancy() {
69    println!(
70        "
71== merge: per-shard filters unioned at the gateway =="
72    );
73    let capacity = 10_000;
74    let mut gateway = BloomFilter::new(capacity);
75    for shard in 0..4 {
76        let mut local = BloomFilter::new(capacity);
77        for i in 0..500 {
78            local.add(&format!("shard{shard}-sym{i}"));
79        }
80        gateway.union(&local).expect("shards share one geometry");
81    }
82    println!("  merged 4 shards x 500 symbols");
83    println!(
84        "  approx distinct keys: {}",
85        gateway.approximate_element_count()
86    );
87    println!(
88        "  occupancy fpp:        {:.4}%",
89        gateway.estimated_fpp() * 100.0
90    );
91
92    let mismatched = BloomFilter::new(capacity * 2);
93    match gateway.union(&mismatched) {
94        Err(e) => println!("  refused mismatched shard: {e}"),
95        Ok(()) => unreachable!("geometry check must reject this"),
96    }
97
98    gateway.clear();
99    println!(
100        "  after clear -> approx distinct keys: {}",
101        gateway.approximate_element_count()
102    );
103}
Source

pub fn might_contain(&self, key: &str) -> bool

Examples found in repository?
examples/sample_app.rs (line 44)
31fn base_crawler_dedup() {
32    println!("== base: crawler URL dedup ==");
33    let mut seen = BloomFilter::new(10_000);
34    let frontier = [
35        "https://a.example/",
36        "https://b.example/",
37        "https://a.example/", // dup
38        "https://c.example/",
39        "https://b.example/", // dup
40    ];
41
42    let (mut fetched, mut skipped) = (0usize, 0usize);
43    for url in frontier {
44        if seen.might_contain(url) {
45            println!("  skip  {url}");
46            skipped += 1;
47        } else {
48            seen.add(url);
49            println!("  fetch {url}");
50            fetched += 1;
51        }
52    }
53    println!("  -> fetched {fetched}, skipped {skipped}");
54    for url in [
55        "https://a.example/",
56        "https://b.example/",
57        "https://c.example/",
58    ] {
59        assert!(seen.might_contain(url), "no false negatives");
60    }
61}
Source

pub fn bit_count(&self) -> u32

Source

pub fn k(&self) -> u32

Source

pub fn set_bits(&self) -> u64

Population count of the bit array. Walks every word, so keep it off the hot path; it is the input to both saturation estimators below.

Source

pub fn approximate_element_count(&self) -> u64

Swamidass-Baldi estimate of how many distinct keys were added: -(m/k) * ln(1 - X/m) for X set bits. Diverges once the array saturates, so a fully set filter reports u64::MAX rather than a number that reads as real.

Examples found in repository?
examples/sample_app.rs (line 85)
68fn shard_merge_and_occupancy() {
69    println!(
70        "
71== merge: per-shard filters unioned at the gateway =="
72    );
73    let capacity = 10_000;
74    let mut gateway = BloomFilter::new(capacity);
75    for shard in 0..4 {
76        let mut local = BloomFilter::new(capacity);
77        for i in 0..500 {
78            local.add(&format!("shard{shard}-sym{i}"));
79        }
80        gateway.union(&local).expect("shards share one geometry");
81    }
82    println!("  merged 4 shards x 500 symbols");
83    println!(
84        "  approx distinct keys: {}",
85        gateway.approximate_element_count()
86    );
87    println!(
88        "  occupancy fpp:        {:.4}%",
89        gateway.estimated_fpp() * 100.0
90    );
91
92    let mismatched = BloomFilter::new(capacity * 2);
93    match gateway.union(&mismatched) {
94        Err(e) => println!("  refused mismatched shard: {e}"),
95        Ok(()) => unreachable!("geometry check must reject this"),
96    }
97
98    gateway.clear();
99    println!(
100        "  after clear -> approx distinct keys: {}",
101        gateway.approximate_element_count()
102    );
103}
Source

pub fn estimated_fpp(&self) -> f64

Current false-positive probability given actual occupancy: (X/m)^k. This is the measured rate, not the design-point ~1%, so it is what tells you the filter has outgrown its sizing.

Examples found in repository?
examples/sample_app.rs (line 89)
68fn shard_merge_and_occupancy() {
69    println!(
70        "
71== merge: per-shard filters unioned at the gateway =="
72    );
73    let capacity = 10_000;
74    let mut gateway = BloomFilter::new(capacity);
75    for shard in 0..4 {
76        let mut local = BloomFilter::new(capacity);
77        for i in 0..500 {
78            local.add(&format!("shard{shard}-sym{i}"));
79        }
80        gateway.union(&local).expect("shards share one geometry");
81    }
82    println!("  merged 4 shards x 500 symbols");
83    println!(
84        "  approx distinct keys: {}",
85        gateway.approximate_element_count()
86    );
87    println!(
88        "  occupancy fpp:        {:.4}%",
89        gateway.estimated_fpp() * 100.0
90    );
91
92    let mismatched = BloomFilter::new(capacity * 2);
93    match gateway.union(&mismatched) {
94        Err(e) => println!("  refused mismatched shard: {e}"),
95        Ok(()) => unreachable!("geometry check must reject this"),
96    }
97
98    gateway.clear();
99    println!(
100        "  after clear -> approx distinct keys: {}",
101        gateway.approximate_element_count()
102    );
103}
Source

pub fn is_compatible(&self, other: &BloomFilter) -> bool

Two filters can be unioned only if they agree on m and k - the bit positions mean nothing otherwise.

Source

pub fn union(&mut self, other: &BloomFilter) -> Result<(), GeometryMismatch>

OR another filter’s bits into this one. The result is the filter you would have built by adding both key sets to one array, which is what makes a shard-per-producer build mergeable at fan-in.

Examples found in repository?
examples/sample_app.rs (line 80)
68fn shard_merge_and_occupancy() {
69    println!(
70        "
71== merge: per-shard filters unioned at the gateway =="
72    );
73    let capacity = 10_000;
74    let mut gateway = BloomFilter::new(capacity);
75    for shard in 0..4 {
76        let mut local = BloomFilter::new(capacity);
77        for i in 0..500 {
78            local.add(&format!("shard{shard}-sym{i}"));
79        }
80        gateway.union(&local).expect("shards share one geometry");
81    }
82    println!("  merged 4 shards x 500 symbols");
83    println!(
84        "  approx distinct keys: {}",
85        gateway.approximate_element_count()
86    );
87    println!(
88        "  occupancy fpp:        {:.4}%",
89        gateway.estimated_fpp() * 100.0
90    );
91
92    let mismatched = BloomFilter::new(capacity * 2);
93    match gateway.union(&mismatched) {
94        Err(e) => println!("  refused mismatched shard: {e}"),
95        Ok(()) => unreachable!("geometry check must reject this"),
96    }
97
98    gateway.clear();
99    println!(
100        "  after clear -> approx distinct keys: {}",
101        gateway.approximate_element_count()
102    );
103}
Source

pub fn clear(&mut self)

Zero the bits, keeping the allocation. A generation boundary that rebuilds membership from a source of truth reuses the array instead of dropping and re-allocating it.

Examples found in repository?
examples/sample_app.rs (line 98)
68fn shard_merge_and_occupancy() {
69    println!(
70        "
71== merge: per-shard filters unioned at the gateway =="
72    );
73    let capacity = 10_000;
74    let mut gateway = BloomFilter::new(capacity);
75    for shard in 0..4 {
76        let mut local = BloomFilter::new(capacity);
77        for i in 0..500 {
78            local.add(&format!("shard{shard}-sym{i}"));
79        }
80        gateway.union(&local).expect("shards share one geometry");
81    }
82    println!("  merged 4 shards x 500 symbols");
83    println!(
84        "  approx distinct keys: {}",
85        gateway.approximate_element_count()
86    );
87    println!(
88        "  occupancy fpp:        {:.4}%",
89        gateway.estimated_fpp() * 100.0
90    );
91
92    let mismatched = BloomFilter::new(capacity * 2);
93    match gateway.union(&mismatched) {
94        Err(e) => println!("  refused mismatched shard: {e}"),
95        Ok(()) => unreachable!("geometry check must reject this"),
96    }
97
98    gateway.clear();
99    println!(
100        "  after clear -> approx distinct keys: {}",
101        gateway.approximate_element_count()
102    );
103}
Source

pub fn write_to<W: Write>(&self, out: &mut W) -> Result<()>

Source

pub fn parse(buf: &[u8]) -> Result<Self>

Parse a serialised bloom filter from buf. Errors if the buffer is shorter than the header or truncated mid-bits.

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.