Skip to content

Commit 6820e51

Browse files
committed
(fix) document salt order in implementation and add regression test.
The idea for the test is to detect changes when the salt changes, similar to the hash function test. Also adds more documentation about the order, and why it is in that way. Signed-off-by: Joaquin Colacci <joaquincolacci@gmail.com>
1 parent d586ecd commit 6820e51

1 file changed

Lines changed: 54 additions & 11 deletions

File tree

  • vortex-layout/src/layouts/zoned/aggregates/bloom_filter

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

Lines changed: 54 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@
33

44
//! Split block Bloom filters (SBBF) implementation for Vortex.
55
//!
6-
//! This implementation follows the original paper, renaming `bucket` to `block`,
7-
//! with small changes that help the Rust compiler generate optimized, vectorized
8-
//! code for `make_mask`, `add_hash`, and `find_hash`.
6+
//! This implementation follows the original paper but
7+
//! with the following noticeable changes:
8+
//! - Renaming `bucket` to `block`,
9+
//! - Small changes that help the Rust compiler generate optimized, vectorized
10+
//! code for `make_mask`, `add_hash`, and `find_hash`
11+
//! - A different salt order.
912
//!
1013
//! [Split block Bloom filters]: https://arxiv.org/pdf/2101.01719
1114
@@ -43,6 +46,20 @@ pub(super) const BLOCK_SIZE: usize = LANES_PER_BLOCK * BYTES_PER_LANE;
4346
/// See the [XXH specification](https://github.com/Cyan4973/xxHash/blob/v0.8.3/doc/xxhash_spec.md#step-1-initialize-internal-accumulators).
4447
const DEFAULT_SEED: u64 = 0;
4548

49+
/// Eight odd constants for multiply-shift hashing.
50+
///
51+
/// They fit in one 256-bit SIMD vector, and the order matches the one
52+
/// used by the Apache Parquet specification. The paper's example uses
53+
/// the same values but in a different order. This was not
54+
/// intentional for having compatibility with Apache Parquet, but remains
55+
/// as a common-order in implementations.
56+
///
57+
/// It is important to notice that while order doesn't affect validity,
58+
/// it changes the final bits set in each lane.
59+
const SALT: [u32; 8] = [
60+
0x47b6137b, 0x44974d91, 0x8824ad5b, 0xa2b7289d, 0x705495c7, 0x2df1424b, 0x9efc4947, 0x5c6bfb31,
61+
];
62+
4663
/// Hash function to use in a bloom filter.
4764
///
4865
/// The current options are fast, non-cryptographic xxHash variants.
@@ -94,7 +111,7 @@ impl TryFrom<u32> for HashFn {
94111
}
95112
}
96113

97-
/// Represents a Split block Bloom Filter filter for a single layout zone.
114+
/// Represents a Split block Bloom Filter for a single layout zone.
98115
pub struct BloomPartial {
99116
blocks: Vec<[u32; 8]>,
100117
hash_fn: HashFn,
@@ -195,16 +212,10 @@ impl BloomPartial {
195212
fn make_mask(&self, hash: u32) -> [u32; 8] {
196213
let mut out = [0u32; 8];
197214

198-
// Set eight odd constants for multiply-shift hashing
199-
let rehash: [u32; 8] = [
200-
0x47b6137b, 0x44974d91, 0x8824ad5b, 0xa2b7289d, 0x705495c7, 0x2df1424b, 0x9efc4947,
201-
0x5c6bfb31,
202-
];
203-
204215
for i in 0..8 {
205216
// Shift all data right, reducing the hash values from 32 bits to five bits.
206217
// Those five bits represent an index in [0, 31)
207-
let y = hash.wrapping_mul(rehash[i]) >> 27;
218+
let y = hash.wrapping_mul(SALT[i]) >> 27;
208219

209220
// Set a bit in each lane based on using the [0, 32) data as shift values.
210221
out[i] = 1u32 << y;
@@ -566,6 +577,38 @@ mod tests {
566577
assert!(invalid_filter.is_err(), "expect filter to be invalid");
567578
}
568579

580+
/// Another regression test for bloom serialization,
581+
/// but in this case to detect mask salt changes.
582+
/// It just verifies that a filter's serialized representation remains stable.
583+
#[test]
584+
fn serialized_bits_are_stable() {
585+
let options = BloomOptions::new(NonZeroU32::MIN, HashFn::XxHash3_64);
586+
let mut bloom_filter = BloomPartial::from(&options);
587+
588+
bloom_filter.insert(b"vortex");
589+
590+
// Because we have only one block, and this is the only value inserted,
591+
// these lanes equal its mask: `empty | mask == mask`.
592+
let expected_lanes: [u32; 8] = [
593+
0x0000_1000,
594+
0x0200_0000,
595+
0x0000_2000,
596+
0x0800_0000,
597+
0x0200_0000,
598+
0x0000_0040,
599+
0x0000_4000,
600+
0x0000_1000,
601+
];
602+
603+
let expected_bytes: Vec<u8> = expected_lanes
604+
.into_iter()
605+
.flat_map(u32::to_le_bytes)
606+
.collect();
607+
608+
let bytes: Vec<u8> = (&bloom_filter).into();
609+
assert_eq!(bytes, expected_bytes);
610+
}
611+
569612
// Similar to the goldenfile tests, but for hash functions.
570613
//
571614
// Useful compatibility test to catch an accidental hash-algorithm or seed change.

0 commit comments

Comments
 (0)