Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,15 @@ signals a backwards-compatible change.
`0x05 0x80` still decode, with the scalars dropped, and are never encoded.
The `sketchlib-golden-bytes` submodule gains the `ddsketch_buckets_*`
fixtures.
- **Breaking: `Bloom` no longer keeps an insert counter.** The state is the
bit grid alone, and `inserted()` is removed: no membership answer reads it
(`contains`, `fill_ratio` and `estimated_fpp` read the bits, and
`predicted_fpp` takes its distinct count from the caller). A caller that
needs the number of inserts keeps a separate exact count.
- **ASAPv1: Bloom encodes as `0x17 0x01`.** The payload is `[words]`. The
retired `0x17 0x00`, whose payload also carried `inserted`, still decodes,
with the counter dropped, and is never encoded. The `sketchlib-golden-bytes`
submodule gains the `bloom_bits_*` fixtures.
- **Count Sketch estimates no longer allocate.** The per-row estimates a
median is taken over are collected on the stack instead of in a fresh
`Vec`: up to 20 rows for `Vector2D` (the fast and regular paths),
Expand Down
2 changes: 1 addition & 1 deletion asapv1_golden
13 changes: 8 additions & 5 deletions docs/api/api_bloom.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ fn update_batch(&mut self, values: &[DataInput])
```

One bit per slice. Re-inserting a key sets nothing new, so the fill is a
function of the distinct keys alone while `inserted()` counts every call.
function of the distinct keys alone. The filter keeps no insert counter; a
caller that needs the number of inserts keeps a separate exact count.

## Query

Expand All @@ -75,7 +76,6 @@ False is exact: the key was never inserted. True is probabilistic.
fn predicted_fpp(&self, distinct_items: usize) -> f64
fn estimated_fpp(&self) -> f64
fn fill_ratio(&self) -> f64
fn inserted(&self) -> u64
fn rows(&self) -> usize
fn cols(&self) -> usize
fn bit_capacity(&self) -> usize
Expand Down Expand Up @@ -104,12 +104,15 @@ fn to_asapv1(&self) -> Result<Vec<u8>, RmpEncodeError>
fn from_asapv1(bytes: &[u8]) -> Result<Self, RmpDecodeError>
```

These produce/consume the **ASAPv1** wire envelope (kind `0x17 0x00`) — see the
These produce/consume the **ASAPv1** wire envelope (kind `0x17 0x01`) — see the
[ASAPv1 wire format spec](../asapv1_wire_format.md). The impl is bounded on
`Mode: BloomMode` and `H: SketchHasher + HashProfile`, so a hasher that declares
no profile cannot serialize at all. `rows`, `cols` and `mode` are carried in the
envelope metadata; the payload is just `[words, inserted]`, the packed bit words
row-major plus the insert counter, with the word stride derived from `cols`.
envelope metadata; the payload is just `[words]`, the packed bit words
row-major, with the word stride derived from `cols`. `from_asapv1` also reads
the retired kind `0x17 0x00`, whose payload `[words, inserted]` carried an
insert counter: the counter is dropped, and the filter re-encodes as
`0x17 0x01`. Nothing encodes `0x17 0x00`.

The wire covers the geometries `with_capacity` produces: at most
`BLOOM_MAX_SLICES` slices, a power-of-two `cols`, and at most `BLOOM_MAX_BITS`
Expand Down
24 changes: 14 additions & 10 deletions docs/asapv1_wire_format.md

Large diffs are not rendered by default.

7 changes: 4 additions & 3 deletions docs/message_pack_format.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,10 @@ Converted:
`src/sketches/kll_dynamic/wire.rs` dynamic → `0x06 0x01`) — both variants share
one payload `[levels, items, coin]` and differ only by `kind_id`. KLL never
hashes, so its metadata carries no hash-spec group.
- **Bloom** (`src/sketches/bloom/wire.rs`, kind `0x17 0x00`) — the payload is the
bit grid packed into `u64`s plus the insert count; the wire covers the
geometries `Bloom::with_capacity` produces.
- **Bloom** (`src/sketches/bloom/wire.rs`, kind `0x17 0x01`) — the payload is the
bit grid packed into `u64`s; the wire covers the geometries
`Bloom::with_capacity` produces. The retired `0x17 0x00` also carried an
insert count; it still decodes, with the count dropped, and is never encoded.
- **Space-Saving** (`src/sketches/space_saving/wire.rs`, kind `0x18 0x00`) — the
payload is the `(key, count, error)` triples plus `total` and `discarded_max`; the
Stream-Summary's links and arenas are rebuilt on decode.
Expand Down
37 changes: 20 additions & 17 deletions docs/tests.md

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion src/asapv1/kind.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,11 @@ pub(crate) const UNIVMON: &[u8] = &[0x10, 0x00];
pub(crate) const UNIVMON_TWO_TIER: &[u8] = &[0x11, 0x00];
pub(crate) const EXPONENTIAL_HISTOGRAM: &[u8] = &[0x13, 0x00];
pub(crate) const EH_BUCKET_SKETCH: &[u8] = &[0x14, 0x00];
pub(crate) const BLOOM: &[u8] = &[0x17, 0x00];
/// Bloom: the packed bit grid (spec §3.4).
pub(crate) const BLOOM: &[u8] = &[0x17, 0x01];
/// Retired Bloom kind that also carried the `inserted` counter: decoded, with
/// the counter dropped, and never encoded.
pub(crate) const BLOOM_WITH_INSERTED: &[u8] = &[0x17, 0x00];
pub(crate) const SPACE_SAVING: &[u8] = &[0x18, 0x00];
pub(crate) const COUNT_SKETCH_WITH_L2_NORM: &[u8] = &[0x19, 0x00];
pub(crate) const UNIVMON_Q: &[u8] = &[0x1a, 0x00];
Expand Down
64 changes: 12 additions & 52 deletions src/sketches/bloom.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@ impl BloomMode for FastPath {
#[derive(Clone, Debug)]
pub struct Bloom<Mode = RegularPath, H: SketchHasher = DefaultXxHasher> {
bits: BitMatrix,
inserted: u64,
_mode: PhantomData<Mode>,
_hasher: PhantomData<H>,
}
Expand All @@ -85,7 +84,6 @@ impl<Mode, H: SketchHasher> Bloom<Mode, H> {
);
Self {
bits: BitMatrix::new(rows, cols),
inserted: 0,
_mode: PhantomData,
_hasher: PhantomData,
}
Expand Down Expand Up @@ -166,12 +164,6 @@ impl<Mode, H: SketchHasher> Bloom<Mode, H> {
self.bits.size_in_bytes()
}

/// Number of `update` calls this filter has seen, duplicates included.
#[inline(always)]
pub fn inserted(&self) -> u64 {
self.inserted
}

/// Fraction of bits set.
pub fn fill_ratio(&self) -> f64 {
self.bits.fill_ratio()
Expand All @@ -182,10 +174,9 @@ impl<Mode, H: SketchHasher> Bloom<Mode, H> {
self.bits.count_ones() == 0
}

/// Clears every bit and the insert counter.
/// Clears every bit.
pub fn clear(&mut self) {
self.bits.clear();
self.inserted = 0;
}

/// Read access to the underlying bit grid.
Expand Down Expand Up @@ -215,7 +206,6 @@ impl<Mode, H: SketchHasher> Bloom<Mode, H> {
/// result is exactly the filter the concatenated streams would have built.
pub fn merge(&mut self, other: &Self) {
self.bits.union_from(&other.bits);
self.inserted = self.inserted.saturating_add(other.inserted);
}
}
// Regular-path membership: one seeded hash per slice.
Expand All @@ -230,7 +220,6 @@ impl<H: SketchHasher> Bloom<RegularPath, H> {
let col = ((hashed & LOWER_32_MASK) as usize) % cols;
self.bits.set(r, col);
}
self.inserted = self.inserted.saturating_add(1);
}

/// Records every value in `values`.
Expand Down Expand Up @@ -264,7 +253,6 @@ impl<H: SketchHasher> Bloom<FastPath, H> {
let hashed_val = <BitMatrix as FastPathHasher<H>>::hash_for_matrix(&self.bits, value);
self.bits
.fast_insert(|cell, _, _| *cell = true, (), &hashed_val);
self.inserted = self.inserted.saturating_add(1);
}

/// Records every value in `values`.
Expand Down Expand Up @@ -585,68 +573,40 @@ mod tests {
}
}

/// `inserted` counts calls, not distinct keys — it is the denominator a
/// caller compares against `predicted_fpp`, so silently deduplicating it
/// would make the filter look better sized than it is. `clear` has to take
/// it back down with the bits.
/// `clear` takes the filter back to the empty state: no bit set, a zero
/// fill ratio, and no former member reported present.
#[test]
fn the_insert_counter_counts_calls_and_clear_returns_it_to_zero() {
fn clear_returns_the_filter_to_empty() {
let mut filter = Bloom::<RegularPath>::with_dimensions(4, 256);
assert_eq!(filter.inserted(), 0, "a fresh filter has a non-zero count");
assert!(filter.is_empty(), "a fresh filter has bits set");

let repeated = DataInput::Str("same");
for _ in 0..5 {
filter.update(&repeated);
}
assert_eq!(
filter.inserted(),
5,
"five inserts of one key counted as {}",
filter.inserted()
);
assert!(!filter.is_empty(), "an inserted-into filter reports empty");

filter.update_batch(&keys(3));
assert_eq!(
filter.inserted(),
8,
"update_batch of three left the count at {}",
filter.inserted()
);
assert!(!filter.is_empty(), "an inserted-into filter reports empty");

filter.clear();
assert_eq!(
filter.inserted(),
0,
"clear left the count at {}",
filter.inserted()
);
assert!(filter.is_empty(), "clear left bits set");
assert_eq!(filter.fill_ratio(), 0.0, "clear left a non-zero fill ratio");
assert!(!filter.contains(&repeated), "clear left a member present");
}

/// A merge unions the bits, so the merged filter has seen both streams and
/// its count has to say so; a count that stayed put would understate the
/// load the filter is carrying.
/// A merge unions the bits, so the merged filter answers yes for every
/// member of either side and keeps its geometry.
#[test]
fn merging_identical_geometries_sums_the_insert_counts() {
fn merging_identical_geometries_unions_the_members() {
let mut left = Bloom::<RegularPath>::with_dimensions(5, 512);
let mut right = Bloom::<RegularPath>::with_dimensions(5, 512);
for key in keys(30) {
left.update(&key);
for value in 0..30u64 {
left.update(&DataInput::U64(value));
}
for value in 30..70u64 {
right.update(&DataInput::U64(value));
}
let expected = left.inserted() + right.inserted();

left.merge(&right);
assert_eq!(
left.inserted(),
expected,
"merged count is {} not {expected}",
left.inserted()
);
for value in 0..70u64 {
assert!(
left.contains(&DataInput::U64(value)),
Expand Down
Loading
Loading