1use subms_bloom_filter::BloomFilter;
13
14fn main() {
15 base_crawler_dedup();
16 shard_merge_and_occupancy();
17
18 #[cfg(feature = "counting")]
19 counting_session_set();
20
21 #[cfg(feature = "scalable")]
22 scalable_growth();
23
24 #[cfg(feature = "partitioned")]
25 partitioned_variant();
26}
27
28fn 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/", "https://c.example/",
39 "https://b.example/", ];
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
63fn 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}
104
105#[cfg(feature = "counting")]
109fn counting_session_set() {
110 use subms_bloom_filter::CountingBloomFilter;
111 println!("\n== counting: active sessions with logout ==");
112 let mut sessions = CountingBloomFilter::new(1_000);
113 for s in ["sess-alice", "sess-bob", "sess-carol"] {
114 sessions.add(s);
115 }
116 println!(" bob active? {}", sessions.might_contain("sess-bob"));
117 sessions.remove("sess-bob"); println!(" bob after logout? {}", sessions.might_contain("sess-bob"));
119 assert!(
120 sessions.might_contain("sess-alice"),
121 "other sessions untouched"
122 );
123}
124
125#[cfg(feature = "scalable")]
129fn scalable_growth() {
130 use subms_bloom_filter::ScalableBloomFilter;
131 println!("\n== scalable: grows past its initial capacity ==");
132 let mut f = ScalableBloomFilter::new(64);
133 for i in 0..1_000 {
134 f.add(&format!("key-{i}"));
135 }
136 println!(
137 " added 1000 into a cap-64 filter -> {} layers",
138 f.layer_count()
139 );
140 assert!(f.layer_count() > 1, "it grew");
141 for i in 0..1_000 {
142 assert!(
143 f.might_contain(&format!("key-{i}")),
144 "no false negatives after growth"
145 );
146 }
147}
148
149#[cfg(feature = "partitioned")]
153fn partitioned_variant() {
154 use subms_bloom_filter::PartitionedBloomFilter;
155 println!("\n== partitioned: one slice per hash ==");
156 let mut f = PartitionedBloomFilter::new(1_000);
157 for tag in ["red", "green", "blue"] {
158 f.add(tag);
159 }
160 println!(" {} bits across {} slices", f.bit_count(), f.k());
161 println!(" green present? {}", f.might_contain("green"));
162 assert!(f.might_contain("red") && !f.might_contain("magenta-unseen"));
163}