Skip to content

Commit 1b4fab6

Browse files
committed
(fix) improve documentation and keep only XXH3_64 as hash fn
So, this commit is mostly around documentation and leaving a single function for hashing. By adding more documentation, I saw the opportunity to rename some variables to match what the documentation explains, for example, using "splits" rather than "lanes." Both are okay, but "splits" matches the documentation. On the other side, by having a single hash function, it was no longer necessary to keep the match for hashes, the implementations for each one, or the clarification about seed usage. Signed-off-by: Joaquin Colacci <joaquincolacci@gmail.com>
1 parent 6820e51 commit 1b4fab6

3 files changed

Lines changed: 180 additions & 104 deletions

File tree

-8 Bytes
Binary file not shown.

vortex-layout/src/layouts/zoned/aggregates/bloom_filter/mod.rs

Lines changed: 116 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
111110
impl 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.
136137
impl 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.
143147
impl 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

Comments
 (0)