@@ -74,8 +74,6 @@ pub struct BloomOptions {
7474 /// Hashing function to use.
7575 ///
7676 /// Defaults to: [`HashFn::XxHash3_64`].
77- ///
78- /// Hashing functions selection may impact in terms of performance.
7977 hash_fn : HashFn ,
8078}
8179
@@ -108,6 +106,7 @@ impl Display for BloomOptions {
108106 }
109107}
110108
109+ /// Deserializes [`BloomOptions`] from the layout binary representation.
111110impl TryFrom < & [ u8 ] > for BloomOptions {
112111 type Error = VortexError ;
113112
@@ -133,13 +132,18 @@ impl TryFrom<&[u8]> for BloomOptions {
133132 }
134133}
135134
135+ /// Useful implementation for serialization,
136+ /// to avoid the need to carry the serialized length.
136137impl From < & BloomOptions > for Vec < u8 > {
137138 fn from ( options : & BloomOptions ) -> Self {
138139 let bytes: [ u8 ; OPTIONS_BYTES_LEN ] = options. into ( ) ;
139140 bytes. to_vec ( )
140141 }
141142}
142143
144+ /// Serialization implementation for [BloomOptions]
145+ ///
146+ /// The following bytes are the ones stored in the zone layout metadata.
143147impl From < & BloomOptions > for [ u8 ; OPTIONS_BYTES_LEN ] {
144148 fn from ( options : & BloomOptions ) -> Self {
145149 let mut bytes = [ 0 ; OPTIONS_BYTES_LEN ] ;
@@ -151,18 +155,119 @@ impl From<&BloomOptions> for [u8; OPTIONS_BYTES_LEN] {
151155}
152156
153157/// A Bloom filter is an approximate membership query structure.
154- /// In Vortex layouts, it helps determine if a value is present in a zone or not.
158+ /// In Vortex layouts, it helps determine if a value is probably
159+ /// present in a single-column zone.
155160///
156- /// Because membership is approximate, the filter can produce false positives.
157- /// Their probability depends on the number of distinct values in the zone and
158- /// the filter configuration.
161+ /// Because membership is approximate, the filter can produce false positives
162+ /// but never false negatives. In other words, it can report that
163+ /// an absent value is present in a zone, but it never excludes a zone containing the value.
164+ /// The false-positive probability depends on the number of distinct values
165+ /// and the filter configuration.
159166///
160167/// ### Implementation
161168///
162- /// This implementation uses a Split block Bloom Filter (SBBF), a Bloom filter
163- /// variant designed to take advantage of SIMD instructions and parallelism.
169+ /// Implementation is based on the Split block Bloom Filter (SBBF),
170+ /// a Bloom filter variant that is cache-friendly and takes advantage of SIMD.
171+ /// As a tradeoff, it is less space-efficient than a traditional Bloom filter.
172+ ///
173+ /// The filter is made up of 256-bit blocks, where each block "splits" into eight sections.
174+ /// When a zone writer inserts a value, the value gets hashed and assigned to a block.
175+ /// And then a mask derived from the hash and applied to the assigned block.
176+ /// For more details about the process, see [Insertion](#insertion) for how a block
177+ /// is selected and updated. For the actual implementation code, refer to [BloomPartial].
178+ ///
179+ /// ### Representation
180+ ///
181+ /// The internal state is represented as `blocks: Vec<[u32; 8]>`, with each block
182+ /// containing its eight splits/sections.
183+ ///
184+ /// An empty filter looks as follows:
185+ ///
186+ /// ```text
187+ /// ┌──────────────────┬─────┬──────────────────────┐
188+ /// │ block 0 [u32; 8] │ ... │ block N -1 [u32; 8] │
189+ /// ├──────────────────┼─────┼──────────────────────┤
190+ /// │ 00000...00000 │ ... │ 00000...00000 │
191+ /// └──────────────────┴─────┴──────────────────────┘
192+ /// ```
193+ ///
194+ /// If we zoom in on a particular block it would look like this:
195+ ///
196+ /// ```text
197+ /// ┌────────────────┬─────┬────────────────┐
198+ /// │ split 0 [u32] │ ... │ split 7 [u32] │
199+ /// ├────────────────┼─────┼────────────────┤
200+ /// │ 00000...00000 │ ... │ 00000...00000 │
201+ /// └────────────────┴─────┴────────────────┘
202+ /// ```
203+ ///
204+ /// ### Insertion
205+ ///
206+ /// During insertion, the value to insert gets hashed into a 64-bit value.
207+ /// From the resulting hash, the upper 32 bits are used to select a block,
208+ /// while the lower 32 bits are used to create the mask.
209+ ///
210+ /// ```text
211+ /// hash [u64]
212+ /// 10100...11000_00011...11011
213+ /// │
214+ /// ▼
215+ /// ┌────────────────┬────────────────┐
216+ /// │ upper [u32] │ lower [u32] │
217+ /// ├────────────────┼────────────────┤
218+ /// │ 10100...11000 │ 00011...11011 │
219+ /// └───────┬────────┴───────┬────────┘
220+ /// │ │
221+ /// ▼ ▼
222+ /// block_index(upper) make_mask(lower)
223+ /// │ │
224+ /// ▼ ▼
225+ /// block_idx [usize] mask [u32; 8]
226+ /// ```
227+ ///
228+ /// The mask has the same structure as a block, but with one bit set for
229+ /// each of its eight splits.
230+ ///
231+ /// The following action is to OR the mask and block together, turning those bits on
232+ /// without changing any bits that were already set:
233+ ///
234+ /// `block = block OR mask`
235+ ///
236+ /// And this is how the updated block fits back into the filter:
237+ ///
238+ /// ```text
239+ /// ┌────────────────┬─────┬────────────────┐
240+ /// │ split 0 [u32] │ ... │ split 7 [u32] │
241+ /// ├────────────────┼─────┼────────────────┤
242+ /// │ 10000...00000 │ ... │ 00000...00100 │
243+ /// └────────────────┴─────┴────────────────┘
244+ /// ```
245+ ///
246+ /// Bloom filter block visualised (splits flattened):
247+ ///
248+ /// ```text
249+ /// ┌─────┬────────────────────────────────┬─────┐
250+ /// │ ... │ block 4 │ ... │
251+ /// ├─────┼────────────────────────────────┼─────┤
252+ /// │ ... │ 10000...00100 │ ... │
253+ /// └─────┴────────────────────────────────┴─────┘
254+ /// ```
164255///
165- /// Refer to [BloomPartial] for the implementation code.
256+ /// ### Serialization
257+ ///
258+ /// Serialization is simple, it is just flattening the blocks `Vec<[u32; 8]>`
259+ /// into `Vec<u8>`. So the only difference is that the blocks boundaries
260+ /// are now implicit, while the bits remain the same.
261+ ///
262+ /// To deserialize, it is just enough to split the byte sequence into 32-byte blocks.
263+ ///
264+ /// ```text
265+ /// ┌────────────────────────┬─────┬────────────────────────────────┐
266+ /// │ bytes 0..32 │ ... │ bytes (N - 1) * 32..N * 32 │
267+ /// ├────────────────────────┼─────┼────────────────────────────────┤
268+ /// │ block 0: 8 x u32 (LE) │ ... │ block N - 1 │
269+ /// └────────────────────────┴─────┴────────────────────────────────┘
270+ /// ```
166271///
167272/// ### Notice
168273///
@@ -241,6 +346,8 @@ impl AggregateFnVTable for BloomFilter {
241346 }
242347
243348 /// Returns true if all the blocks are full.
349+ ///
350+ /// When a bloom filter is saturated, it cannot rule out any values.
244351 fn is_saturated ( & self , partial : & Self :: Partial ) -> bool {
245352 partial. is_saturated ( )
246353 }
@@ -430,18 +537,6 @@ pub(in crate::layouts::zoned::aggregates::bloom_filter) mod test_utils {
430537 check_metadata ( "bloom_filter_sbbf_xxhash3_64.metadata" , & bytes) ;
431538 }
432539
433- #[ cfg_attr( miri, ignore) ]
434- #[ test]
435- fn test_bloom_metadata_variant ( ) {
436- let options = & BloomOptions :: new (
437- NonZeroU32 :: new ( 256 ) . vortex_expect ( "valid nonzero" ) ,
438- HashFn :: XxHash64 ,
439- ) ;
440- let bytes: [ u8 ; OPTIONS_BYTES_LEN ] = options. into ( ) ;
441-
442- check_metadata ( "bloom_filter_sbbf_xxhash64.metadata" , & bytes) ;
443- }
444-
445540 #[ test]
446541 fn bloom_options_equality_compares_all_fields ( ) {
447542 let default = BloomOptions :: default ( ) ;
@@ -453,13 +548,8 @@ pub(in crate::layouts::zoned::aggregates::bloom_filter) mod test_utils {
453548 NonZeroU32 :: new ( 4 ) . vortex_expect ( "valid nonzero" ) ,
454549 HashFn :: XxHash3_64 ,
455550 ) ;
456- let different_hash_fn = BloomOptions :: new (
457- NonZeroU32 :: new ( DEFAULT_BLOCKS_COUNT ) . vortex_expect ( "valid nonzero" ) ,
458- HashFn :: XxHash64 ,
459- ) ;
460551
461552 assert_eq ! ( default , same_as_default) ;
462553 assert_ne ! ( default , different_block_count) ;
463- assert_ne ! ( default , different_hash_fn) ;
464554 }
465555}
0 commit comments