Repository navigation
fix(countmin): use version 2 hashing for new sketches - #299
Conversation
|
1. Empty strings and byte arrays are counted. The doc comment asks callers to skip these, but the spec indicates it should be handled by the implementation. This fails today on #[test]
fn v2_ignores_empty_strings_and_byte_arrays() {
let mut sketch = CountMinSketch::<i64>::new(3, 1000).unwrap();
sketch.update_with_weight(raw_bytes::from_str(""), 5);
sketch.update_with_weight(raw_bytes::from_slice(&[]), 7);
assert!(sketch.is_empty());
sketch.update_with_weight(raw_bytes::from_str("apple"), 5);
assert_eq!(sketch.estimate(raw_bytes::from_str("")), 0);
assert_eq!(sketch.estimate(raw_bytes::from_slice(&[])), 0);
}2. Bare narrow integers are not widened. #[test]
fn v2_widens_narrow_integers_to_64_bits() {
let mut sketch = CountMinSketch::<i64>::new(3, 1000).unwrap();
sketch.update_with_weight(9i64, 10);
sketch.update_with_weight(-2i64, 20);
assert_eq!(sketch.estimate(9i8), 10);
assert_eq!(sketch.estimate(9u16), 10);
assert_eq!(sketch.estimate(9usize), 10);
assert_eq!(sketch.estimate(-2i32), 20);
assert_eq!(sketch.estimate(-2isize), 20);
}We could add something like this? fn write_u8(&mut self, v: u8) { self.write_u64(u64::from(v)) }
fn write_u16(&mut self, v: u16) { self.write_u64(u64::from(v)) }
fn write_u32(&mut self, v: u32) { self.write_u64(u64::from(v)) }
fn write_usize(&mut self, v: usize) { self.write_u64(v as u64) }
fn write_i8(&mut self, v: i8) { self.write_i64(i64::from(v)) }
fn write_i16(&mut self, v: i16) { self.write_i64(i64::from(v)) }
fn write_i32(&mut self, v: i32) { self.write_i64(i64::from(v)) }
fn write_isize(&mut self, v: isize) { self.write_i64(v as i64) } |
|
Maybe the doctests update can be a follow up PR?
|
|
Updated to describe the merged implementation in 6856e45:
The merged implementation uses The final PR head passed CI checks, benchmark smoke workloads, and tests on Linux, macOS, and Windows with the MSRV and stable Rust. Coverage includes published version 2 vectors, value wrappers, legacy fixtures, version preservation, mixed-version merge rejection, and version-specific row limits. |
`ItemHasher` overrode `Hasher::write_i8` through `write_usize` to widen narrow integers to 64 bits and tracked zero-byte writes to skip empty encodings. Because `std::hash::Hasher` integer methods are low-level stream operations used by standard `Hash` implementations (for example, `str::hash` writes a trailing `0xff` via `write_u8` and `[T]::hash` writes the slice length via `write_usize`), overriding them alters the encoding of ordinary Rust strings, slices, booleans, enums, and derived composite keys. Deferring weight validation until after the first row's hash check also allowed invalid weights such as `i64::MIN` to be silently ignored on zero-byte inputs. Hash version 2 items directly with `XxHash64`, matching `BloomFilter`, `XorFilter`, and the rest of the crate's `hash::value` design. Validate and accumulate the absolute update weight once before iterating over the sketch rows, document `natural_extend` for Count-Min sketches alongside Bloom filters, and fold the portable value-wrapper test into `countmin_test::sketch`.
`CountMinSketch` stored `serial_version` as a raw `u8`, requiring `unreachable!()` arms when deriving row seeds and hashing items, and duplicated table dimension validation across constructor and deserializer helpers. Meanwhile, `countmin::serialization` held only five private constants used solely by `sketch.rs`. Introduce a private `SerialVersion` enum (`V1 = 1`, `V2 = 2`) that owns version decoding, row limits, row seed generation, table entry validation, and item hashing. Inline the preamble and version constants into `datasketches/src/countmin/sketch.rs` and remove `datasketches/src/countmin/serialization.rs`.
…tructor docs Add a module-level cross-language compatibility note to `countmin`, separate the `CountMinSketch` overview from its serialization and `hash::value` encoding guidance, restore the bulleted `# Errors` list and `# Examples` doctest on `CountMinSketch::with_seed`, and format the `1..=127` row range as inline code in `CHANGELOG.md`.
…ity tests Format serialized preamble headers in hexadecimal `u64` words via a shared `words_to_bytes` helper, add `natural_extend` checks to the known-bucket vector test, and remove the redundant 51-word legacy fixture that overlapped existing version 1 round-trip, update, and merge coverage.
Count-Min derives row hashes differently across implementations, so serialized sketches can return incorrect estimates when read in another language. New sketches use serial version 2 hashing, while readers retain the legacy algorithm for version 1 images. The row seeds, bucket mapping, and version policy follow the proposed format in apache/datasketches-tck#18 (reply in thread).
CountMinSketch::newandwith_seedcreate version 2 sketches. Row seeds useXXH64(LE64(row_index), base_seed), items use XXH64, and buckets use(hash >> 1) % num_buckets.Hashcontract. For portable inputs, usei64/u64,natural_extendwrappers or explicit 64-bit widening for narrower integers, andraw_bytesfor strings and byte collections. The crate rejects big-endian targets at compile time.raw_bytesvalue directly still hashes it and can update counters or return a nonzero estimate.Compatibility
Version selection is internal to
CountMinSketch; construction uses the existing API. Version 1 data remains readable and updatable with its original Rust input encoding. A new sketch cannot be merged with version 1 data, and version-1-only libraries cannot read new images. Migrating an existing sketch to version 2 requires rebuilding it from source items because the two algorithms map items to different counters. New sketches support 1–127 rows; readers still accept legacy version 1 images with more rows.Cross-language version 2 interchange requires
i64counters, compatible item encodings, and caller-side empty-input handling. Bare strings, slices, narrow integers, and arbitrary customHashimplementations are not automatically portable. Version 1 cross-language queries and updates remain unreliable.Validation
The final PR head passed CI checks, benchmark smoke workloads, and tests on Linux, macOS, and Windows with both the MSRV and stable Rust.
Tests cover the published version 2 image and independent bucket vectors, including non-default seeds and value wrappers. Rust v0.5.0/v0.6.0 fixtures verify legacy behavior and serialization; existing C++ version 1 snapshots cover binary round-trips. Mixed-version merge rejection and the distinct constructor/legacy-reader row limits are also covered. Multi-implementation version 2 TCK snapshots remain a follow-up once available.
Refs: #297