pub struct BloomFilter { /* private fields */ }Implementations§
Source§impl BloomFilter
impl BloomFilter
Sourcepub fn new(expected_entries: usize) -> Self
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?
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}Sourcepub fn add(&mut self, key: &str)
pub fn add(&mut self, key: &str)
Examples found in repository?
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}Sourcepub fn might_contain(&self, key: &str) -> bool
pub fn might_contain(&self, key: &str) -> bool
Examples found in repository?
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}pub fn bit_count(&self) -> u32
pub fn k(&self) -> u32
Sourcepub fn set_bits(&self) -> u64
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.
Sourcepub fn approximate_element_count(&self) -> u64
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?
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}Sourcepub fn estimated_fpp(&self) -> f64
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?
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}Sourcepub fn is_compatible(&self, other: &BloomFilter) -> bool
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.
Sourcepub fn union(&mut self, other: &BloomFilter) -> Result<(), GeometryMismatch>
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?
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}Sourcepub fn clear(&mut self)
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?
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}