Repository navigation
feat(xor): add immutable filters with portable serialization - #309
Merged
Merged
Conversation
Member
Author
|
fyi @pmcgleenon @freakyzoidberg @notfilippo @ariesdevil You may mainly review:
|
`XorFilter::into_parts` and `from_parts` exchanged a four-element tuple, leaving the seed, segment length, and optional item count unnamed at call sites and making any additional field a breaking change. They now exchange `XorFilterParts`, created with `XorFilterParts::new(seed, segment_length, fingerprints)` and an optional `with_num_items`, while `from_parts` keeps validating the layout. `XorFilter::from_hashes` and `XorFilter::from_distinct_hashes` duplicated the builder methods. Construction now goes through `XorFilterBuilder` only, as Bloom filters do through `BloomFilterBuilder`. Duplicate input to `build_from_distinct_hashes` previously consumed all 100 construction attempts before failing, several dozen times the cost of a successful build. After a failed attempt, a slot that still holds two hashes whose XOR is zero now reports duplicate input immediately. Mixing is a bijection for a fixed seed, so only equal input hashes can cancel out. The Java fixture tests now also check that filters built from the same inputs and base seed reproduce Java's header: construction seed, segment length, and item count. Fingerprint slots may still differ because the peeling order differs. The README now lists 32-bit fingerprints in the standard format, and the module documentation no longer promises layout parity with a third-party crate.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds immutable XOR filters for complete input sets, with 8-, 16-, and 32-bit fingerprints behind the
xorfeature. Construction uses bounded retries and compact scratch storage without adding runtime dependencies.API
XorFilterBuilderfollows the library's consuming configuration pattern: choose a fingerprint type, optionally setseed(...), then consume the builder withbuild(values),build_from_hashes(hashes), orbuild_from_distinct_hashes(&hashes). It does not retain input or expose incremental updates. The first two paths deduplicate; the distinct path borrows without copying or sorting. Callers can retain borrowed input to retry with another seed. Construction goes through the builder only.Construction makes at most 100 attempts. Size violations, duplicate input, and exhausted attempts return
InvalidArgument. Duplicate hashes on the distinct-input path prevent peeling; when a failed attempt leaves two equal hashes in one slot, construction reports duplicate input immediately instead of consuming the remaining attempts. This detection is not guaranteed for every duplicate configuration. Valid input can exhaust the attempt budget, and reseeding does not guarantee success. With consistent hashing, a negative query proves absence and a positive result may be a false positive.Compatibility
Standard serialization supports 8/16/32-bit fingerprints using DataSketches family 22, version 1. The 8/16-bit images use the format of the merged Java implementation. For the same inputs and base seed, Rust and Java choose the same construction seed and layout, so headers match; fingerprint slots can differ because the peeling order differs. The 32-bit images use the same header with fingerprint width 32 and little-endian
u32slots; interoperability requires readers that support this width. Serialization requires a known item count and signed 32-bit format bounds. Application-defined storage usesXorFilter::into_parts/XorFilter::from_parts, which exchangeXorFilterPartsbuilt withXorFilterParts::new(seed, segment_length, fingerprints)and optionallywith_num_items(count).from_partsvalidates the three-segment layout. Tests check that raw parts round-trip with xorf 0.13 for all three widths.Imported parts can omit cardinality;
num_items,is_empty, andbits_per_itemthen returnNone. Known-empty filters reject all queries, while unknown counts preserve ordinary fingerprint comparisons.Ordinary Rust values use xxHash64 with seed 0, but Rust and Java value encodings can differ. The module documents integer and raw-byte encodings for interoperability. Externally computed hashes must use the precomputed-hash APIs throughout. Rust deserialization owns its data; independently constructed filters need not have identical fingerprint bytes.
Construction performance
The synthetic
xor::matched_buildworkloads use base seed 1 for both implementations and verify matching winning seeds, ensuring equal attempt counts. Input generation is outside timing; neither distinct-hash path includes deduplication. Mapping visits one segment at a time and reuses the peel stack for mixed hashes, improving locality without another scratch allocation.Xor8 results for eight million distinct synthetic hashes on an Apple M4 Max, arm64 macOS, rustc 1.101.0-nightly (2026-10-17), optimized bench profile:
These are medians of four round medians, alternating builder order, with 20 single-iteration samples per builder per round. Peak live heap allocation is approximately 263.3 MB versus 1.082 GB, excluding preexisting input and allocator overhead; it is not process RSS. Both fingerprint payloads occupy 9.84 MB. Results are specific to this workload and machine, not a universal speed guarantee.
The matched suite covers all three widths and a synthetic case requiring a retry. Automatic retry after a failed peel is checked against a fresh build starting at the next seed.
cargo bench --package benchmarks --bench benchmarks -- 'xor::matched_build::(datasketches_8|xorf_8).*8000000' --sample-count 20 --sample-size 1