Skip to main content

sample_app/
sample_app.rs

1//! Sample app: a tour of `subms-bloom-filter`, base API first, then each
2//! optional feature. Run the base with `cargo run --example sample_app`; add
3//! `--all-features` (or a subset like `--features counting`) to see the feature
4//! sections light up.
5//!
6//! * base        - URL-seen dedup for a web crawler's frontier
7//! * merge       - per-shard filters unioned at fan-in, with occupancy readout
8//! * counting    - an active-session set that supports removal (logout)
9//! * scalable    - a filter that grows in layers as it fills, keeping FPR bounded
10//! * partitioned - the independent-slice variant
11
12use 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
28/// Base API: a crawler skips URLs it has already fetched. The bloom filter
29/// answers "seen this?" in a fixed footprint; the only risk is a rare false
30/// positive (a genuinely-new URL wrongly skipped), never a false negative.
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}
104
105/// `counting` feature: a plain bloom filter can never remove a key. A counting
106/// bloom filter keeps a small counter per cell, so a key can be removed - here,
107/// an active-session set where a logout deletes the session.
108#[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"); // logout
118    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/// `scalable` feature: sized for a small capacity, it adds a fresh, larger layer
126/// each time a layer fills - so the false-positive rate stays bounded no matter
127/// how many keys arrive, without knowing the count up front.
128#[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/// `partitioned` feature: instead of `k` hashes into one bit array, each hash
150/// owns its own equal slice. The per-slice fill is uniform, which makes the
151/// false-positive rate easier to reason about analytically.
152#[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}