reddb_server/storage/primitives/
split_block_bloom.rs1const SALTS: [u32; 8] = [
27 0x47b6137b, 0x44974d91, 0x8824ad5b, 0xa2b7289d, 0x705495c7, 0x2df1424b, 0x9efc4947, 0x5c6bfb31,
28];
29
30#[repr(align(32))]
32#[derive(Clone, Default, PartialEq, Eq)]
33pub struct Block {
34 words: [u32; 8],
35}
36
37impl std::fmt::Debug for Block {
38 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39 write!(f, "Block({:08x?})", &self.words)
40 }
41}
42
43#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct SplitBlockBloom {
47 blocks: Vec<Block>,
48 mask: usize,
50}
51
52impl SplitBlockBloom {
53 pub fn with_capacity(n: usize) -> Self {
55 let bits_needed = (n * 10).max(256);
58 let blocks_needed = bits_needed.div_ceil(256);
59 let num_blocks = blocks_needed.next_power_of_two();
60 Self {
61 blocks: vec![Block::default(); num_blocks],
62 mask: num_blocks - 1,
63 }
64 }
65
66 #[inline]
68 pub fn insert(&mut self, key: u32) {
69 let block_idx = (key as usize) & self.mask;
70 let block = &mut self.blocks[block_idx];
71 for (i, &salt) in SALTS.iter().enumerate() {
72 let bit = key.wrapping_mul(salt) >> 27; block.words[i] |= 1u32 << bit;
74 }
75 }
76
77 #[inline]
79 pub fn insert_bytes(&mut self, key: &[u8]) {
80 let h = stable_hash64(key);
81 let block_idx = ((h >> 32) as usize) & self.mask;
82 let probe_word = h as u32;
83 let block = &mut self.blocks[block_idx];
84 for (i, &salt) in SALTS.iter().enumerate() {
85 let bit = probe_word.wrapping_mul(salt) >> 27;
86 block.words[i] |= 1u32 << bit;
87 }
88 }
89
90 #[inline]
93 pub fn probe(&self, key: u32) -> bool {
94 let block_idx = (key as usize) & self.mask;
95 let block = &self.blocks[block_idx];
96 for (i, &salt) in SALTS.iter().enumerate() {
97 let bit = key.wrapping_mul(salt) >> 27;
98 if block.words[i] & (1u32 << bit) == 0 {
99 return false;
100 }
101 }
102 true
103 }
104
105 #[inline]
108 pub fn probe_bytes(&self, key: &[u8]) -> bool {
109 let h = stable_hash64(key);
110 let block_idx = ((h >> 32) as usize) & self.mask;
111 let probe_word = h as u32;
112 let block = &self.blocks[block_idx];
113 for (i, &salt) in SALTS.iter().enumerate() {
114 let bit = probe_word.wrapping_mul(salt) >> 27;
115 if block.words[i] & (1u32 << bit) == 0 {
116 return false;
117 }
118 }
119 true
120 }
121
122 pub(crate) fn compact(&mut self) {
127 let original_len = self.blocks.len();
128 while self.blocks.len() > 1 {
129 let half = self.blocks.len() / 2;
130 let can_fold =
131 self.blocks[..half]
132 .iter()
133 .zip(&self.blocks[half..])
134 .all(|(left, right)| {
135 left.words
136 .iter()
137 .zip(right.words)
138 .all(|(left, right)| (left | right).count_ones() <= u32::BITS / 2)
139 });
140 if !can_fold {
141 break;
142 }
143 for index in 0..half {
144 for word in 0..8 {
145 self.blocks[index].words[word] |= self.blocks[index + half].words[word];
146 }
147 }
148 self.blocks.truncate(half);
149 self.mask = half - 1;
150 }
151 if self.blocks.len() != original_len {
152 self.blocks.shrink_to_fit();
153 }
154 }
155
156 pub fn num_blocks(&self) -> usize {
158 self.blocks.len()
159 }
160
161 pub fn byte_size(&self) -> usize {
164 self.blocks.len() * 32
165 }
166
167 pub fn union_inplace(&mut self, other: &SplitBlockBloom) -> bool {
171 if self.blocks.len() != other.blocks.len() {
172 return false;
173 }
174 for (left, right) in self.blocks.iter_mut().zip(&other.blocks) {
175 for (left_word, right_word) in left.words.iter_mut().zip(right.words) {
176 *left_word |= right_word;
177 }
178 }
179 true
180 }
181
182 pub fn fill_ratio(&self) -> f64 {
184 let total_words = self.blocks.len() * 8;
185 if total_words == 0 {
186 return 0.0;
187 }
188 let filled_words = self
189 .blocks
190 .iter()
191 .flat_map(|block| block.words)
192 .filter(|word| *word != 0)
193 .count();
194 filled_words as f64 / total_words as f64
195 }
196
197 pub fn to_bytes(&self) -> Vec<u8> {
203 let mut out = Vec::with_capacity(4 + self.blocks.len() * 32);
204 out.extend_from_slice(&(self.blocks.len() as u32).to_le_bytes());
205 for block in &self.blocks {
206 for &w in &block.words {
207 out.extend_from_slice(&w.to_le_bytes());
208 }
209 }
210 out
211 }
212
213 pub fn from_bytes(bytes: &[u8]) -> Option<Self> {
218 if bytes.len() < 4 {
219 return None;
220 }
221 let num_blocks = u32::from_le_bytes(bytes[0..4].try_into().ok()?) as usize;
222 if num_blocks == 0 || !num_blocks.is_power_of_two() {
223 return None;
224 }
225 if bytes.len() < 4 + num_blocks * 32 {
226 return None;
227 }
228 let mut blocks = Vec::with_capacity(num_blocks);
229 let mut cur = 4;
230 for _ in 0..num_blocks {
231 let mut words = [0u32; 8];
232 for w in &mut words {
233 *w = u32::from_le_bytes(bytes[cur..cur + 4].try_into().ok()?);
234 cur += 4;
235 }
236 blocks.push(Block { words });
237 }
238 Some(Self {
239 blocks,
240 mask: num_blocks - 1,
241 })
242 }
243}
244
245pub fn stable_hash64(bytes: &[u8]) -> u64 {
252 let mut hash = 0xcbf2_9ce4_8422_2325u64;
253 for &byte in bytes {
254 hash ^= u64::from(byte);
255 hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
256 }
257 hash ^= hash >> 33;
258 hash = hash.wrapping_mul(0xff51_afd7_ed55_8ccd);
259 hash ^= hash >> 33;
260 hash = hash.wrapping_mul(0xc4ce_b9fe_1a85_ec53);
261 hash ^ (hash >> 33)
262}
263
264pub fn hash_bytes_u32(bytes: &[u8]) -> u32 {
269 use std::hash::{Hash, Hasher};
270 let mut h = std::collections::hash_map::DefaultHasher::new();
271 bytes.hash(&mut h);
272 let bits = h.finish();
273 (bits ^ (bits >> 32)) as u32
274}
275
276pub fn hash_value_u32(v: &reddb_types::Value) -> u32 {
280 use std::hash::{Hash, Hasher};
281 let mut h = std::collections::hash_map::DefaultHasher::new();
282 v.hash(&mut h);
283 let bits = h.finish();
284 (bits ^ (bits >> 32)) as u32
285}
286
287#[cfg(test)]
288mod tests {
289 use super::*;
290
291 #[test]
292 fn test_insert_then_probe() {
293 let mut bloom = SplitBlockBloom::with_capacity(100);
294 for i in 0u32..100 {
295 bloom.insert(i);
296 }
297 for i in 0u32..100 {
298 assert!(bloom.probe(i), "false negative for key {i}");
299 }
300 }
301
302 #[test]
303 fn test_absent_key_may_return_false() {
304 let mut bloom = SplitBlockBloom::with_capacity(1000);
305 for i in 0u32..1000 {
306 bloom.insert(i * 2); }
308 for i in 0u32..1000 {
311 assert!(bloom.probe(i * 2), "false negative for key {}", i * 2);
312 }
313 }
314
315 #[test]
316 fn to_bytes_from_bytes_round_trips_and_keeps_no_false_negatives() {
317 let mut bloom = SplitBlockBloom::with_capacity(500);
318 for i in 0u32..500 {
319 bloom.insert(i.wrapping_mul(2_654_435_761));
320 }
321 let blob = bloom.to_bytes();
322 let restored = SplitBlockBloom::from_bytes(&blob).expect("round-trips");
323 assert_eq!(restored, bloom);
324 for i in 0u32..500 {
326 assert!(restored.probe(i.wrapping_mul(2_654_435_761)));
327 }
328 }
329
330 #[test]
331 fn compact_preserves_keys_and_serialized_routing() {
332 for distinct in [1u64, 4, 16, 64, 128, 200, 255, 256] {
333 let mut bloom = SplitBlockBloom::with_capacity(8192);
334 for value in 0u64..8192 {
335 bloom.insert(hash_bytes_u32(&(value % distinct).to_le_bytes()));
336 }
337 let original_size = bloom.byte_size();
338 bloom.compact();
339 assert!(bloom.byte_size() < original_size);
340 let restored = SplitBlockBloom::from_bytes(&bloom.to_bytes()).expect("compacted bloom");
341 for value in 0..distinct {
342 assert!(restored.probe(hash_bytes_u32(&value.to_le_bytes())));
343 }
344 let compacted = bloom.clone();
345 bloom.compact();
346 assert_eq!(bloom, compacted);
347 }
348 }
349
350 #[test]
351 fn compact_preserves_byte_key_routing_and_dense_filters() {
352 let mut sparse = SplitBlockBloom::with_capacity(8192);
353 for value in 0u64..16 {
354 sparse.insert_bytes(&value.to_le_bytes());
355 }
356 sparse.compact();
357 let restored =
358 SplitBlockBloom::from_bytes(&sparse.to_bytes()).expect("compacted byte bloom");
359 for value in 0u64..16 {
360 assert!(restored.probe_bytes(&value.to_le_bytes()));
361 }
362
363 let mut dense = SplitBlockBloom::with_capacity(128);
364 for value in 0u64..128 {
365 dense.insert(hash_bytes_u32(&value.to_le_bytes()));
366 }
367 let original = dense.clone();
368 dense.compact();
369 assert_eq!(
370 dense, original,
371 "dense filters should not pay for a rebuild"
372 );
373 }
374
375 #[test]
376 fn from_bytes_rejects_truncated_or_non_power_of_two() {
377 assert!(SplitBlockBloom::from_bytes(&[]).is_none());
378 assert!(SplitBlockBloom::from_bytes(&[1, 2, 3]).is_none());
379 assert!(SplitBlockBloom::from_bytes(&3u32.to_le_bytes()).is_none());
381 assert!(SplitBlockBloom::from_bytes(&2u32.to_le_bytes()).is_none());
383 }
384
385 #[test]
386 fn hash_bytes_u32_is_stable_across_calls() {
387 let a = hash_bytes_u32(&7u64.to_le_bytes());
388 let b = hash_bytes_u32(&7u64.to_le_bytes());
389 assert_eq!(a, b);
390 }
391
392 #[test]
393 fn test_false_positive_rate_approximately_one_percent() {
394 const N: usize = 10_000;
395 let mut bloom = SplitBlockBloom::with_capacity(N);
396 for i in 0u32..N as u32 {
397 bloom.insert(i);
398 }
399 let mut fp = 0usize;
400 let probes = 10_000usize;
401 for i in N as u32..(N as u32 + probes as u32) {
402 if bloom.probe(i) {
403 fp += 1;
404 }
405 }
406 let fpr = fp as f64 / probes as f64;
407 assert!(fpr < 0.05, "FPR too high: {fpr:.3}");
409 }
410
411 #[test]
412 fn insert_bytes_has_no_false_negatives() {
413 const N: usize = 10_000;
414 let mut bloom = SplitBlockBloom::with_capacity(N);
415 for i in 0..N as u64 {
416 bloom.insert_bytes(&byte_key(i));
417 }
418 for i in 0..N as u64 {
419 assert!(
420 bloom.probe_bytes(&byte_key(i)),
421 "false negative for key {i}"
422 );
423 }
424 }
425
426 #[test]
427 fn insert_bytes_false_positive_rate_stays_below_two_percent() {
428 const N: usize = 10_000;
429 let mut bloom = SplitBlockBloom::with_capacity(N);
430 for i in 0..N as u64 {
431 bloom.insert_bytes(&byte_key(i));
432 }
433 let mut fp = 0usize;
434 let probes = 10_000usize;
435 for i in N as u64..(N + probes) as u64 {
436 if bloom.probe_bytes(&byte_key(i)) {
437 fp += 1;
438 }
439 }
440 let fpr = fp as f64 / probes as f64;
441 assert!(fpr < 0.02, "FPR too high: {fpr:.3}");
442 }
443
444 #[test]
445 fn bytes_round_trip_keeps_no_false_negatives() {
446 const N: usize = 10_000;
447 let mut bloom = SplitBlockBloom::with_capacity(N);
448 for i in 0..N as u64 {
449 bloom.insert_bytes(&byte_key(i));
450 }
451 let blob = bloom.to_bytes();
452 let restored = SplitBlockBloom::from_bytes(&blob).expect("round-trips");
453 assert_eq!(restored, bloom);
454 for i in 0..N as u64 {
455 assert!(
456 restored.probe_bytes(&byte_key(i)),
457 "false negative for key {i}"
458 );
459 }
460 }
461
462 #[test]
463 fn bytes_hashing_agrees_across_filters() {
464 const N: usize = 10_000;
465 let mut a = SplitBlockBloom::with_capacity(N);
466 let mut b = SplitBlockBloom::with_capacity(N);
467 for i in 0..N as u64 {
468 let key = byte_key(i);
469 a.insert_bytes(&key);
470 b.insert_bytes(&key);
471 }
472 assert_eq!(a, b);
473 for i in 0..N as u64 {
474 let key = byte_key(i);
475 assert_eq!(a.probe_bytes(&key), b.probe_bytes(&key));
476 assert!(a.probe_bytes(&key), "false negative for key {i}");
477 }
478 }
479
480 #[test]
481 fn union_inplace_merges_matching_block_counts() {
482 let mut a = SplitBlockBloom::with_capacity(1024);
483 let mut b = SplitBlockBloom::with_capacity(1024);
484 a.insert_bytes(b"one");
485 b.insert_bytes(b"two");
486
487 assert!(a.union_inplace(&b));
488 assert!(a.probe_bytes(b"one"));
489 assert!(a.probe_bytes(b"two"));
490 }
491
492 #[test]
493 fn union_inplace_rejects_mismatched_block_counts() {
494 let mut a = SplitBlockBloom::with_capacity(1024);
495 let b = SplitBlockBloom::with_capacity(4096);
496
497 assert!(!a.union_inplace(&b));
498 }
499
500 fn byte_key(i: u64) -> [u8; 16] {
501 let mut key = [0u8; 16];
502 key[..8].copy_from_slice(&i.to_le_bytes());
503 key[8..].copy_from_slice(&i.wrapping_mul(0x9e37_79b9_7f4a_7c15).to_le_bytes());
504 key
505 }
506}