Skip to main content

reddb_server/storage/index/
bloom_segment.rs

1//! Reusable bloom filter header that any segment can embed.
2//!
3//! Segments (table pages, vector segments, timeseries chunks, graph
4//! partitions) all benefit from the same question: "is key X *possibly* in
5//! this segment?". Rather than reimplementing bloom wiring in every storage
6//! engine, wrap [`crate::storage::primitives::split_block_bloom::SplitBlockBloom`]
7//! here with a serialisation header and a small trait `HasBloom` for owners to
8//! plug in.
9//!
10//! Layout on disk / inside a segment header is:
11//!
12//! ```text
13//! [ u8  magic       ]
14//! [ u8  reserved    ]   // zero
15//! [ u32 inserted     ]   // monotonic counter, best-effort
16//! [ bytes...          ]   // SplitBlockBloom::to_bytes()
17//! ```
18
19use crate::storage::primitives::split_block_bloom::SplitBlockBloom;
20
21pub use reddb_file::BloomSegmentFrameError as BloomSegmentError;
22
23/// Trait implemented by owners of a segment-level bloom filter.
24///
25/// Segments ask their owner (table, vector collection, timeseries chunk)
26/// whether a key is definitely absent before walking their main structure.
27pub trait HasBloom {
28    /// Reference to the bloom filter attached to this segment, if any.
29    fn bloom_segment(&self) -> Option<&BloomSegment>;
30
31    /// Fast-path negative check. Returns `true` iff the bloom is present and
32    /// reports the key as absent.
33    fn definitely_absent(&self, key: &[u8]) -> bool {
34        self.bloom_segment()
35            .map(|b| b.definitely_absent(key))
36            .unwrap_or(false)
37    }
38}
39
40/// Owning bloom header that can be embedded in a segment.
41pub struct BloomSegment {
42    filter: SplitBlockBloom,
43    inserted: u32,
44}
45
46impl std::fmt::Debug for BloomSegment {
47    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48        f.debug_struct("BloomSegment")
49            .field("num_blocks", &self.filter.num_blocks())
50            .field("inserted", &self.inserted)
51            .finish()
52    }
53}
54
55impl BloomSegment {
56    /// Build a bloom sized for `expected` elements at the split-block
57    /// primitive's fixed ~1% false-positive rate.
58    pub fn with_capacity(expected: usize) -> Self {
59        Self {
60            filter: SplitBlockBloom::with_capacity(expected.max(16)),
61            inserted: 0,
62        }
63    }
64
65    /// Record `key` as possibly present.
66    pub fn insert(&mut self, key: &[u8]) {
67        self.filter.insert_bytes(key);
68        self.inserted = self.inserted.saturating_add(1);
69    }
70
71    /// Might `key` be present? (May return a false positive, never a false
72    /// negative.)
73    pub fn contains(&self, key: &[u8]) -> bool {
74        self.filter.probe_bytes(key)
75    }
76
77    /// Inverse of `contains` — the thing callers usually want.
78    pub fn definitely_absent(&self, key: &[u8]) -> bool {
79        !self.filter.probe_bytes(key)
80    }
81
82    /// Approximate current false-positive rate from word fill. Split-block
83    /// bloom probes require all eight salted words to match, so this is a
84    /// stats/debug estimate rather than a contract.
85    pub fn estimated_fp_rate(&self) -> f64 {
86        self.filter.fill_ratio().powi(8)
87    }
88
89    /// Number of elements recorded so far (best-effort).
90    pub fn inserted_count(&self) -> u32 {
91        self.inserted
92    }
93
94    /// Bytes used by the underlying split-block payload.
95    pub fn byte_size(&self) -> usize {
96        self.filter.byte_size()
97    }
98
99    /// Merge another bloom segment into this one. Both must have the same
100    /// size and hash count. Returns `false` on mismatch.
101    pub fn union_inplace(&mut self, other: &BloomSegment) -> bool {
102        if self.filter.union_inplace(&other.filter) {
103            self.inserted = self.inserted.saturating_add(other.inserted);
104            true
105        } else {
106            false
107        }
108    }
109
110    /// Serialise into the header layout documented at module level.
111    pub fn encode(&self) -> Vec<u8> {
112        reddb_file::encode_bloom_segment_frame(self.inserted, &self.filter.to_bytes())
113    }
114
115    /// Parse a previously encoded header. Returns a fresh `BloomSegment` and
116    /// the number of bytes consumed.
117    pub fn decode(bytes: &[u8]) -> Result<(Self, usize), BloomSegmentError> {
118        let (inserted, bloom_blob, consumed) = reddb_file::decode_bloom_segment_frame(bytes)?;
119        let filter =
120            SplitBlockBloom::from_bytes(&bloom_blob).ok_or(BloomSegmentError::LengthMismatch)?;
121        Ok((Self { filter, inserted }, consumed))
122    }
123}
124
125/// Fluent builder that produces a `BloomSegment`.
126pub struct BloomSegmentBuilder {
127    expected: usize,
128}
129
130impl BloomSegmentBuilder {
131    pub fn new() -> Self {
132        Self { expected: 1024 }
133    }
134
135    pub fn expected(mut self, n: usize) -> Self {
136        self.expected = n;
137        self
138    }
139
140    pub fn build(self) -> BloomSegment {
141        BloomSegment::with_capacity(self.expected)
142    }
143}
144
145impl Default for BloomSegmentBuilder {
146    fn default() -> Self {
147        Self::new()
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154
155    #[test]
156    fn insert_and_query() {
157        let mut seg = BloomSegment::with_capacity(1024);
158        seg.insert(b"alpha");
159        seg.insert(b"beta");
160
161        assert!(seg.contains(b"alpha"));
162        assert!(seg.contains(b"beta"));
163        assert!(seg.definitely_absent(b"gamma") || seg.contains(b"gamma"));
164        // No false negatives.
165        assert!(!seg.definitely_absent(b"alpha"));
166        assert_eq!(seg.inserted_count(), 2);
167    }
168
169    #[test]
170    fn encode_decode_roundtrip() {
171        let mut seg = BloomSegment::with_capacity(512);
172        for i in 0..100 {
173            seg.insert(format!("key{i}").as_bytes());
174        }
175
176        let bytes = seg.encode();
177        let (restored, consumed) = BloomSegment::decode(&bytes).unwrap();
178        assert_eq!(consumed, bytes.len());
179        assert_eq!(restored.inserted_count(), 100);
180        for i in 0..100 {
181            assert!(restored.contains(format!("key{i}").as_bytes()));
182        }
183    }
184
185    #[test]
186    fn decode_rejects_bad_magic() {
187        let mut bytes = BloomSegment::with_capacity(64).encode();
188        bytes[0] = 0x00;
189        assert_eq!(
190            BloomSegment::decode(&bytes).unwrap_err(),
191            BloomSegmentError::BadMagic
192        );
193    }
194
195    #[test]
196    fn decode_rejects_short_buffer() {
197        let bytes = [reddb_file::BLOOM_SEGMENT_V2_MAGIC, 0, 0, 0];
198        assert_eq!(
199            BloomSegment::decode(&bytes).unwrap_err(),
200            BloomSegmentError::TooShort
201        );
202    }
203
204    #[test]
205    fn decode_rejects_truncated_payload() {
206        let mut bytes = BloomSegment::with_capacity(64).encode();
207        bytes.truncate(bytes.len() - 1);
208        assert_eq!(
209            BloomSegment::decode(&bytes).unwrap_err(),
210            BloomSegmentError::LengthMismatch
211        );
212    }
213
214    #[test]
215    fn union_merges_populations() {
216        let mut a = BloomSegment::with_capacity(1024);
217        let mut b = BloomSegment::with_capacity(1024);
218        a.insert(b"one");
219        b.insert(b"two");
220        assert!(a.union_inplace(&b));
221        assert!(a.contains(b"one"));
222        assert!(a.contains(b"two"));
223        assert_eq!(a.inserted_count(), 2);
224    }
225
226    #[test]
227    fn union_rejects_incompatible() {
228        let mut a = BloomSegment::with_capacity(1024);
229        let b = BloomSegment::with_capacity(4096);
230        assert!(!a.union_inplace(&b));
231    }
232
233    #[test]
234    fn has_bloom_default_absent_when_none() {
235        struct NoBloom;
236        impl HasBloom for NoBloom {
237            fn bloom_segment(&self) -> Option<&BloomSegment> {
238                None
239            }
240        }
241        let x = NoBloom;
242        assert!(!x.definitely_absent(b"anything"));
243    }
244}