1use std::hash::{Hash, Hasher};
24use std::sync::Arc;
25
26#[derive(Default)]
32struct FxBloomHasher(u64);
33
34const FX_SEED: u64 = 0xCBF2_9CE4_8422_2325;
35const FX_MULT: u64 = 0x517C_C1B7_2722_0A95;
36
37impl FxBloomHasher {
38 fn with_seed(seed: u64) -> Self {
39 Self(seed ^ FX_SEED)
40 }
41}
42
43impl Hasher for FxBloomHasher {
44 #[inline]
45 fn write(&mut self, bytes: &[u8]) {
46 let mut chunks = bytes.chunks_exact(8);
47 for c in &mut chunks {
48 let n = u64::from_le_bytes([
49 c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7],
50 ]);
51 self.0 = (self.0.rotate_left(5) ^ n).wrapping_mul(FX_MULT);
52 }
53 for &b in chunks.remainder() {
54 self.0 = (self.0.rotate_left(5) ^ b as u64).wrapping_mul(FX_MULT);
55 }
56 }
57 #[inline]
58 fn write_u64(&mut self, n: u64) {
59 self.0 = (self.0.rotate_left(5) ^ n).wrapping_mul(FX_MULT);
60 }
61 #[inline]
62 fn write_u32(&mut self, n: u32) { self.write_u64(n as u64); }
63 #[inline]
64 fn write_u16(&mut self, n: u16) { self.write_u64(n as u64); }
65 #[inline]
66 fn write_u8(&mut self, n: u8) { self.write_u64(n as u64); }
67 #[inline]
68 fn write_i64(&mut self, n: i64) { self.write_u64(n as u64); }
69 #[inline]
70 fn write_i32(&mut self, n: i32) { self.write_u64(n as u64); }
71 #[inline]
72 fn write_isize(&mut self, n: isize) { self.write_u64(n as u64); }
73 #[inline]
74 fn write_usize(&mut self, n: usize) { self.write_u64(n as u64); }
75 #[inline]
76 fn finish(&self) -> u64 { self.0 }
77}
78
79#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
84pub struct Bloom64(pub u64);
85
86impl Bloom64 {
87 pub const ZERO: Self = Self(0);
88 pub const SUGGESTED_CAPACITY: usize = 8;
92
93 #[inline]
94 pub(crate) fn fast_hash<K: Hash + ?Sized>(key: &K, seed: u64) -> u64 {
95 let mut h = FxBloomHasher::with_seed(seed);
96 key.hash(&mut h);
97 h.finish()
98 }
99
100 #[inline]
104 fn indices<K: Hash + ?Sized>(key: &K) -> [u8; 4] {
105 let h = Self::fast_hash(key, 0x9E37_79B9_7F4A_7C15);
106 [
107 (h & 0x3F) as u8,
108 ((h >> 16) & 0x3F) as u8,
109 ((h >> 32) & 0x3F) as u8,
110 ((h >> 48) & 0x3F) as u8,
111 ]
112 }
113
114 pub fn insert<K: Hash + ?Sized>(&mut self, key: &K) {
116 for bit in Self::indices(key) {
117 self.0 |= 1u64 << bit;
118 }
119 }
120
121 pub fn might_contain<K: Hash + ?Sized>(&self, key: &K) -> bool {
124 let bits = Self::indices(key);
125 for bit in bits {
126 if (self.0 >> bit) & 1 == 0 {
127 return false;
128 }
129 }
130 true
131 }
132
133 pub fn from_keys<'a, K, I>(keys: I) -> Self
135 where K: Hash + 'a, I: IntoIterator<Item = &'a K>,
136 {
137 let mut b = Self::ZERO;
138 for k in keys { b.insert(k); }
139 b
140 }
141
142 pub fn popcount(&self) -> u32 { self.0.count_ones() }
145
146 pub fn estimated_fpr(n: usize) -> f64 {
148 let m = 64.0;
150 let k = 4.0;
151 let p_zero = (-k * n as f64 / m).exp();
152 (1.0 - p_zero).powf(k)
153 }
154}
155
156#[derive(Debug, Clone)]
158pub struct BloomPointer<T> {
159 bloom: Bloom64,
160 target: Arc<T>,
161}
162
163impl<T> BloomPointer<T> {
164 pub const SIGNATURE: subetha_core::AxisMask = subetha_core::AxisMask::from_axes(
169 &[subetha_core::Axis::ContentPrefix],
170 );
171
172 pub fn new(target: Arc<T>, bloom: Bloom64) -> Self {
173 Self { bloom, target }
174 }
175
176 pub fn from_keys<K, I>(target: Arc<T>, keys: I) -> Self
180 where K: Hash, I: IntoIterator<Item = K>,
181 {
182 let mut b = Bloom64::ZERO;
183 for k in keys { b.insert(&k); }
184 Self { bloom: b, target }
185 }
186
187 #[inline]
188 pub fn bloom(&self) -> Bloom64 { self.bloom }
189
190 #[inline]
191 pub fn target(&self) -> &Arc<T> { &self.target }
192
193 #[inline]
196 pub fn might_contain<K: Hash + ?Sized>(&self, key: &K) -> bool {
197 self.bloom.might_contain(key)
198 }
199
200 pub fn set_bloom(&mut self, b: Bloom64) { self.bloom = b; }
203}
204
205#[derive(Debug, Clone)]
218pub struct BloomCascade<T> {
219 coarse: Bloom64,
220 fine: BloomFine,
221 target: Arc<T>,
222}
223
224#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
226pub struct BloomFine {
227 bits: [u64; 4],
228}
229
230impl BloomFine {
231 pub const ZERO: Self = Self { bits: [0; 4] };
232 pub const SUGGESTED_CAPACITY: usize = 64;
234
235 #[inline]
242 fn indices<K: Hash + ?Sized>(key: &K) -> [u8; 8] {
243 let h1 = Bloom64::fast_hash(key, 0x9E37_79B9_7F4A_7C15);
244 let h2 = Bloom64::fast_hash(key, 0xBB67_AE85_84CA_A73B);
245 [
246 (h1 & 0xFF) as u8,
247 ((h1 >> 16) & 0xFF) as u8,
248 ((h1 >> 32) & 0xFF) as u8,
249 ((h1 >> 48) & 0xFF) as u8,
250 (h2 & 0xFF) as u8,
251 ((h2 >> 16) & 0xFF) as u8,
252 ((h2 >> 32) & 0xFF) as u8,
253 ((h2 >> 48) & 0xFF) as u8,
254 ]
255 }
256
257 pub fn insert<K: Hash + ?Sized>(&mut self, key: &K) {
258 for bit in Self::indices(key) {
259 self.bits[(bit / 64) as usize] |= 1u64 << (bit % 64);
260 }
261 }
262
263 pub fn might_contain<K: Hash + ?Sized>(&self, key: &K) -> bool {
264 for bit in Self::indices(key) {
265 let word = self.bits[(bit / 64) as usize];
266 if (word >> (bit % 64)) & 1 == 0 { return false; }
267 }
268 true
269 }
270
271 pub fn from_keys<'a, K, I>(keys: I) -> Self
272 where K: Hash + 'a, I: IntoIterator<Item = &'a K>,
273 {
274 let mut b = Self::ZERO;
275 for k in keys { b.insert(k); }
276 b
277 }
278
279 pub fn popcount(&self) -> u32 {
280 self.bits.iter().map(|w| w.count_ones()).sum()
281 }
282}
283
284impl<T> BloomCascade<T> {
285 pub fn new(target: Arc<T>, coarse: Bloom64, fine: BloomFine) -> Self {
286 Self { coarse, fine, target }
287 }
288
289 pub fn from_keys<K, I>(target: Arc<T>, keys: I) -> Self
291 where K: Hash, I: IntoIterator<Item = K>,
292 {
293 let mut coarse = Bloom64::ZERO;
294 let mut fine = BloomFine::ZERO;
295 for k in keys {
296 coarse.insert(&k);
297 fine.insert(&k);
298 }
299 Self { coarse, fine, target }
300 }
301
302 pub fn target(&self) -> &Arc<T> { &self.target }
303 pub fn coarse(&self) -> Bloom64 { self.coarse }
304 pub fn fine(&self) -> &BloomFine { &self.fine }
305
306 pub fn cascade_check<K: Hash + ?Sized>(&self, key: &K) -> CascadeOutcome {
311 if !self.coarse.might_contain(key) {
312 return CascadeOutcome::RejectedAtCoarse;
313 }
314 if !self.fine.might_contain(key) {
315 return CascadeOutcome::RejectedAtFine;
316 }
317 CascadeOutcome::MightContain
318 }
319}
320
321#[derive(Debug, Clone, Copy, PartialEq, Eq)]
323pub enum CascadeOutcome {
324 RejectedAtCoarse,
325 RejectedAtFine,
326 MightContain,
327}
328
329impl CascadeOutcome {
330 pub fn might_contain(self) -> bool { matches!(self, Self::MightContain) }
331 pub fn rejected(self) -> bool { !self.might_contain() }
332}
333
334#[cfg(test)]
335mod tests {
336 use super::*;
337
338 #[test]
339 fn bloom64_insert_then_query() {
340 let mut b = Bloom64::ZERO;
341 b.insert(&42u64);
342 b.insert(&"hello");
343 assert!(b.might_contain(&42u64));
344 assert!(b.might_contain(&"hello"));
345 let mut rejects = 0;
348 for k in 1000..1100u64 {
349 if !b.might_contain(&k) { rejects += 1; }
350 }
351 assert!(rejects > 80,
352 "fresh Bloom64 with 2 entries should reject most random keys; got {rejects}/100");
353 }
354
355 #[test]
356 fn bloom64_no_false_negative() {
357 let mut b = Bloom64::ZERO;
359 for k in 0..16u64 { b.insert(&k); }
360 for k in 0..16u64 {
361 assert!(b.might_contain(&k),
362 "Bloom must never give false negative; missed {k}");
363 }
364 }
365
366 #[test]
367 fn bloom_pointer_basic_usage() {
368 let target = Arc::new(vec![1u64, 2, 3, 4, 5]);
369 let keys: Vec<u64> = target.iter().copied().collect();
370 let bp = BloomPointer::from_keys(target.clone(), keys);
371 for k in 1..=5u64 {
372 assert!(bp.might_contain(&k));
373 }
374 let mut rejects = 0;
376 for k in 100..200u64 {
377 if !bp.might_contain(&k) { rejects += 1; }
378 }
379 assert!(rejects > 80,
380 "BloomPointer with 5 entries should reject most random; got {rejects}/100");
381 }
382
383 #[test]
384 fn bloom_pointer_size_is_16_bytes() {
385 assert_eq!(std::mem::size_of::<BloomPointer<u64>>(), 16);
386 }
387
388 #[test]
389 fn bloom_fine_holds_more_keys_than_coarse() {
390 let mut coarse = Bloom64::ZERO;
393 let mut fine = BloomFine::ZERO;
394 for k in 0..32u64 {
395 coarse.insert(&k);
396 fine.insert(&k);
397 }
398 let mut coarse_rejects = 0;
399 let mut fine_rejects = 0;
400 for k in 1000..1100u64 {
401 if !coarse.might_contain(&k) { coarse_rejects += 1; }
402 if !fine.might_contain(&k) { fine_rejects += 1; }
403 }
404 assert!(fine_rejects >= coarse_rejects,
407 "fine filter must reject at least as much as coarse: \
408 coarse={coarse_rejects} fine={fine_rejects}");
409 }
410
411 #[test]
412 fn bloom_cascade_layered_rejection() {
413 let target: Arc<Vec<u64>> = Arc::new((0..32u64).collect());
414 let keys: Vec<u64> = target.iter().copied().collect();
415 let bc = BloomCascade::from_keys(target.clone(), keys);
416
417 for k in 0..32u64 {
419 assert!(bc.cascade_check(&k).might_contain(),
420 "inserted key {k} must not be rejected");
421 }
422
423 let mut coarse_rej = 0;
425 let mut fine_rej = 0;
426 let mut survive = 0;
427 for k in 1000..1100u64 {
428 match bc.cascade_check(&k) {
429 CascadeOutcome::RejectedAtCoarse => coarse_rej += 1,
430 CascadeOutcome::RejectedAtFine => fine_rej += 1,
431 CascadeOutcome::MightContain => survive += 1,
432 }
433 }
434 assert!(coarse_rej + fine_rej >= 90,
436 "cascade should reject most random queries; \
437 coarse_rej={coarse_rej} fine_rej={fine_rej} survive={survive}");
438 }
439
440 #[test]
441 fn estimated_fpr_grows_with_load() {
442 let fpr1 = Bloom64::estimated_fpr(1);
443 let fpr8 = Bloom64::estimated_fpr(8);
444 let fpr16 = Bloom64::estimated_fpr(16);
445 let fpr32 = Bloom64::estimated_fpr(32);
446 assert!(fpr1 < fpr8);
447 assert!(fpr8 < fpr16);
448 assert!(fpr16 < fpr32);
449 let fpr8_actual = Bloom64::estimated_fpr(8);
451 assert!(fpr8_actual < 0.05,
452 "8-key FPR should be < 5%, got {fpr8_actual}");
453 assert!(fpr16 > 0.10 && fpr16 < 0.25,
456 "16-key FPR should be in [10%, 25%], got {fpr16}");
457 }
458
459 #[test]
460 fn bloom_cascade_outer_inner_information() {
461 let mut coarse = Bloom64::ZERO;
465 let mut fine = BloomFine::ZERO;
466 for k in 0..10u64 {
468 coarse.insert(&k);
469 fine.insert(&k);
470 }
471 for k in 100..200u64 {
474 coarse.insert(&k);
475 }
476 let bc = BloomCascade {
478 coarse, fine,
479 target: Arc::new(()),
480 };
481 let outcome = bc.cascade_check(&150u64);
484 assert_ne!(outcome, CascadeOutcome::RejectedAtCoarse,
485 "150 was inserted into coarse so must pass coarse");
486 }
490}