From 3f5ba36622183e0cf4e8e8a813f409cf19546622 Mon Sep 17 00:00:00 2001 From: Rachit <145279448+v0id-X@users.noreply.github.com> Date: Thu, 3 Sep 2026 01:09:25 +0000 Subject: [PATCH 1/4] feat(bloom): introduce BloomFilterInvertedView for inverted queries --- datasketches/src/bloom/mod.rs | 11 +- datasketches/src/bloom/sketch.rs | 137 +++++++++++++++++-- tests-integration/tests/bloom_test/sketch.rs | 38 ++++- 3 files changed, 168 insertions(+), 18 deletions(-) diff --git a/datasketches/src/bloom/mod.rs b/datasketches/src/bloom/mod.rs index dabecdbd..7f93483d 100644 --- a/datasketches/src/bloom/mod.rs +++ b/datasketches/src/bloom/mod.rs @@ -28,8 +28,9 @@ //! * **Fixed size**: Unlike typical sketches, Bloom filters do not resize automatically //! * **Linear space**: Size is proportional to the expected number of distinct items //! -//! These guarantees describe normal operation. After [`invert()`](BloomFilter::invert) neither -//! the no-false-negative nor the false-positive guarantee holds; see its documentation. +//! These guarantees describe normal operation. When converted into a [`BloomFilterInvertedView`] +//! via [`invert()`](BloomFilter::invert), neither the no-false-negative nor the false-positive +//! guarantee holds; see its documentation. //! //! # Usage //! @@ -128,8 +129,9 @@ //! // Intersect: recognizes only items in both filters //! // filter1.intersect(&filter2).unwrap(); //! -//! // Invert: approximately inverts set membership -//! // filter1.invert(); +//! // Invert: returns a read-only inverted view +//! //let inverted = filter1.invert(); +//! //assert!(!inverted.contains(&"a")); //! ``` //! //! # Implementation Details @@ -149,3 +151,4 @@ mod sketch; pub use self::sketch::BloomFilter; pub use self::sketch::BloomFilterBuilder; +pub use self::sketch::BloomFilterInvertedView; diff --git a/datasketches/src/bloom/sketch.rs b/datasketches/src/bloom/sketch.rs index d0d4b6bb..7b471083 100644 --- a/datasketches/src/bloom/sketch.rs +++ b/datasketches/src/bloom/sketch.rs @@ -39,7 +39,7 @@ const EMPTY_FLAG_MASK: u8 = 1 << 2; /// * Tunable false positive rate /// * Constant space usage /// -/// These guarantees hold until [`invert()`](Self::invert) is called; see its documentation. +/// These guarantees hold unless inverted via [`invert()`](Self::invert). #[derive(Debug, Clone, PartialEq)] pub struct BloomFilter { /// Hash seed for all hash functions @@ -251,13 +251,17 @@ impl BloomFilter { Ok(()) } - /// Inverts all bits in the filter. + /// Consumes the filter and inverts all its bits, returning a read-only inverted view. /// /// This approximately inverts the notion of set membership. After inversion, neither the /// no-false-negative nor the false-positive guarantee holds: inserted items may return - /// `false` from [`contains()`](Self::contains), and [`is_empty()`](Self::is_empty), - /// [`bits_used()`](Self::bits_used), and [`load_factor()`](Self::load_factor) describe the - /// raw bit state rather than the inserted items. + /// `false` from [`contains()`](BloomFilterInvertedView::contains), and metadata methods + /// describe the raw inverted bit state. + /// + /// Updates are disallowed on an inverted view to prevent unsound filter states. An inverted + /// view can be converted back into an updatable [`BloomFilter`] via + /// [`invert()`](BloomFilterInvertedView::invert) or + /// [`into_filter()`](BloomFilterInvertedView::into_filter). /// /// # Examples /// @@ -269,20 +273,25 @@ impl BloomFilter { /// .unwrap(); /// filter.insert("apple"); /// - /// filter.invert(); - /// // Now "apple" probably returns false, and most other items return true + /// let inverted = filter.invert(); + /// // "apple" likely returns false in the inverted view: + /// assert!(!inverted.contains(&"apple")); + /// + /// // Inverting back restores the original filter state: + /// let restored = inverted.invert(); + /// assert!(restored.contains(&"apple")); /// ``` - pub fn invert(&mut self) { + pub fn invert(mut self) -> BloomFilterInvertedView { for word in &mut self.bit_array { *word = !*word; } self.num_bits_set = self.capacity() as u64 - self.num_bits_set; + BloomFilterInvertedView { inner: self } } /// Returns whether no bits are set in the filter. /// - /// In normal operation this means no items were inserted. After [`invert()`](Self::invert), - /// it reports the raw bit state instead. + /// Returns `true` if no bits are set in the filter. pub fn is_empty(&self) -> bool { self.num_bits_set == 0 } @@ -613,6 +622,114 @@ impl BloomFilter { } } +/// A read-only inverted view of a [`BloomFilter`]. +/// +/// An inverted view is created by calling [`BloomFilter::invert()`]. +/// Modifications (such as inserting new elements or merging) are disallowed +/// on an inverted view to avoid corrupting set membership invariants. +/// +/// Set membership queries can still be executed via [`contains()`](Self::contains), +/// and the view can be reinverted back into an updatable [`BloomFilter`]. +#[derive(Debug, Clone, PartialEq)] +pub struct BloomFilterInvertedView { + inner: BloomFilter, +} + +impl BloomFilterInvertedView { + /// Returns `true` if an item is possibly in the inverted set. + /// + /// # Examples + /// + /// ``` + /// use datasketches::bloom::BloomFilterBuilder; + /// + /// let mut filter = BloomFilterBuilder::with_accuracy(100, 0.01) + /// .build() + /// .unwrap(); + /// filter.insert("apple"); + /// + /// let inverted = filter.invert(); + /// assert!(!inverted.contains(&"apple")); + /// ``` + pub fn contains(&self, item: &T) -> bool { + self.inner.contains(item) + } + + /// Re-inverts the view back into an updatable [`BloomFilter`]. + /// + /// Inverting twice restores the original bit state and filter guarantees. + /// + /// # Examples + /// + /// ``` + /// use datasketches::bloom::BloomFilterBuilder; + /// + /// let mut filter = BloomFilterBuilder::with_accuracy(100, 0.01) + /// .build() + /// .unwrap(); + /// filter.insert("apple"); + /// + /// let inverted = filter.invert(); + /// let restored = inverted.invert(); + /// assert!(restored.contains(&"apple")); + /// ``` + pub fn invert(self) -> BloomFilter { + self.into_filter() + } + + /// Converts this inverted view back into an updatable [`BloomFilter`] by + /// inverting the bits again. + /// + /// Equivalent to [`invert()`](Self::invert). + pub fn into_filter(mut self) -> BloomFilter { + for word in &mut self.inner.bit_array { + *word = !*word; + } + self.inner.num_bits_set = self.inner.capacity() as u64 - self.inner.num_bits_set; + self.inner + } + + /// Returns a reference to the underlying [`BloomFilter`] representation. + pub fn as_filter(&self) -> &BloomFilter { + &self.inner + } + + /// Returns whether no bits are set in the inverted filter view. + pub fn is_empty(&self) -> bool { + self.inner.is_empty() + } + + /// Returns the number of bits set to 1 in the inverted filter. + pub fn bits_used(&self) -> u64 { + self.inner.bits_used() + } + + /// Returns the total bit capacity of the filter. + pub fn capacity(&self) -> usize { + self.inner.capacity() + } + + /// Returns the number of hash functions used. + pub fn num_hashes(&self) -> u16 { + self.inner.num_hashes() + } + + /// Returns the hash seed. + pub fn seed(&self) -> u64 { + self.inner.seed() + } + + /// Returns the current load factor of the inverted filter. + pub fn load_factor(&self) -> f64 { + self.inner.load_factor() + } + + /// Returns the estimated size of the filter view in bytes. + pub fn estimated_size(&self) -> usize { + self.inner.estimated_size() + } +} + /// Builder for creating [`BloomFilter`] instances. /// /// Provides two construction modes: diff --git a/tests-integration/tests/bloom_test/sketch.rs b/tests-integration/tests/bloom_test/sketch.rs index 3032fd99..be5c282f 100644 --- a/tests-integration/tests/bloom_test/sketch.rs +++ b/tests-integration/tests/bloom_test/sketch.rs @@ -91,11 +91,41 @@ fn test_invert_is_reversible() { let original = filter.clone(); let original_bits = filter.bits_used(); - filter.invert(); - assert_eq!(filter.bits_used(), filter.capacity() as u64 - original_bits); - filter.invert(); - assert_eq!(filter, original); + let inverted = filter.invert(); + assert_eq!( + inverted.bits_used(), + inverted.capacity() as u64 - original_bits + ); + assert_eq!(inverted.capacity(), original.capacity()); + assert_eq!(inverted.num_hashes(), original.num_hashes()); + assert_eq!(inverted.seed(), original.seed()); + + let restored = inverted.invert(); + assert_eq!(restored, original); +} + +#[test] +fn test_invert_into_filter() { + let mut filter = filter(); + filter.insert("apple"); + + let original = filter.clone(); + let inverted = filter.invert(); + let restored = inverted.into_filter(); + + assert_eq!(restored, original); +} + +#[test] +fn test_inverted_view_queries() { + let mut filter = filter(); + filter.insert("apple"); + + let inverted = filter.invert(); + assert!(!inverted.contains(&"apple")); + assert_that!(inverted.load_factor(), gt(0.0)); + assert_that!(inverted.estimated_size(), gt(0)); } #[test] From 19a5cb05ada45382ff30f640ed4bce1df622615a Mon Sep 17 00:00:00 2001 From: Rachit <145279448+v0id-X@users.noreply.github.com> Date: Thu, 3 Sep 2026 01:20:37 +0000 Subject: [PATCH 2/4] docs: add changelog entry for BloomFilterInvertedView --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e8c86b7..0be7920f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ All significant changes to this project will be documented in this file. ## Unreleased +- feat(bloom): make post-invert semantics observable via `BloomFilterInvertedView` (#270, #271) ### Breaking changes From a27015d8b468deb975a53d1171e12730f55b5222 Mon Sep 17 00:00:00 2001 From: Rachit <145279448+v0id-X@users.noreply.github.com> Date: Thu, 3 Sep 2026 02:05:04 +0000 Subject: [PATCH 3/4] refactor(bloom): address review comments on BloomFilterInvertedView --- CHANGELOG.md | 4 +- datasketches/src/bloom/sketch.rs | 49 ++++---------------- tests-integration/tests/bloom_test/sketch.rs | 12 ----- 3 files changed, 11 insertions(+), 54 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0be7920f..03fa869b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,14 +3,16 @@ All significant changes to this project will be documented in this file. ## Unreleased -- feat(bloom): make post-invert semantics observable via `BloomFilterInvertedView` (#270, #271) + ### Breaking changes +* `BloomFilter::invert` now consumes `self` and returns a read-only `BloomFilterInvertedView` instead of mutating in place. Call `invert()` on the view to restore the original updatable filter. * Move `SearchCriteria` from `req` to `common` and remove its `Default` implementation. Import `datasketches::common::SearchCriteria` and explicitly choose `Inclusive` or `Exclusive` for each query. ### New features +* Add `BloomFilterInvertedView` to represent inverted Bloom filters with read-only query semantics. * Add KLL sketches behind the `kll` feature, with rank, quantile, PMF, and CDF queries, merging, totally ordered custom item types, a `KllFloat` adapter for non-NaN floating-point values, and serialization. ### Improvements diff --git a/datasketches/src/bloom/sketch.rs b/datasketches/src/bloom/sketch.rs index 7b471083..7ef75ab8 100644 --- a/datasketches/src/bloom/sketch.rs +++ b/datasketches/src/bloom/sketch.rs @@ -38,8 +38,6 @@ const EMPTY_FLAG_MASK: u8 = 1 << 2; /// * No false negatives (inserted items always return `true`) /// * Tunable false positive rate /// * Constant space usage -/// -/// These guarantees hold unless inverted via [`invert()`](Self::invert). #[derive(Debug, Clone, PartialEq)] pub struct BloomFilter { /// Hash seed for all hash functions @@ -253,16 +251,6 @@ impl BloomFilter { /// Consumes the filter and inverts all its bits, returning a read-only inverted view. /// - /// This approximately inverts the notion of set membership. After inversion, neither the - /// no-false-negative nor the false-positive guarantee holds: inserted items may return - /// `false` from [`contains()`](BloomFilterInvertedView::contains), and metadata methods - /// describe the raw inverted bit state. - /// - /// Updates are disallowed on an inverted view to prevent unsound filter states. An inverted - /// view can be converted back into an updatable [`BloomFilter`] via - /// [`invert()`](BloomFilterInvertedView::invert) or - /// [`into_filter()`](BloomFilterInvertedView::into_filter). - /// /// # Examples /// /// ``` @@ -274,12 +262,7 @@ impl BloomFilter { /// filter.insert("apple"); /// /// let inverted = filter.invert(); - /// // "apple" likely returns false in the inverted view: /// assert!(!inverted.contains(&"apple")); - /// - /// // Inverting back restores the original filter state: - /// let restored = inverted.invert(); - /// assert!(restored.contains(&"apple")); /// ``` pub fn invert(mut self) -> BloomFilterInvertedView { for word in &mut self.bit_array { @@ -289,8 +272,6 @@ impl BloomFilter { BloomFilterInvertedView { inner: self } } - /// Returns whether no bits are set in the filter. - /// /// Returns `true` if no bits are set in the filter. pub fn is_empty(&self) -> bool { self.num_bits_set == 0 @@ -624,19 +605,19 @@ impl BloomFilter { /// A read-only inverted view of a [`BloomFilter`]. /// -/// An inverted view is created by calling [`BloomFilter::invert()`]. -/// Modifications (such as inserting new elements or merging) are disallowed -/// on an inverted view to avoid corrupting set membership invariants. +/// Created by calling [`BloomFilter::invert()`]. Set membership queries on this view are inverted: +/// a `true` result from [`contains()`](Self::contains) guarantees that the item was definitely not +/// inserted into the filter prior to inversion. /// -/// Set membership queries can still be executed via [`contains()`](Self::contains), -/// and the view can be reinverted back into an updatable [`BloomFilter`]. +/// Modifying operations are omitted to prevent unsound updates. Calling +/// [`invert()`](Self::invert) restores the original [`BloomFilter`]. #[derive(Debug, Clone, PartialEq)] pub struct BloomFilterInvertedView { inner: BloomFilter, } impl BloomFilterInvertedView { - /// Returns `true` if an item is possibly in the inverted set. + /// Returns `true` if an item was definitely not in the set prior to inversion. /// /// # Examples /// @@ -655,7 +636,7 @@ impl BloomFilterInvertedView { self.inner.contains(item) } - /// Re-inverts the view back into an updatable [`BloomFilter`]. + /// Inverts the view back into an updatable [`BloomFilter`]. /// /// Inverting twice restores the original bit state and filter guarantees. /// @@ -673,15 +654,7 @@ impl BloomFilterInvertedView { /// let restored = inverted.invert(); /// assert!(restored.contains(&"apple")); /// ``` - pub fn invert(self) -> BloomFilter { - self.into_filter() - } - - /// Converts this inverted view back into an updatable [`BloomFilter`] by - /// inverting the bits again. - /// - /// Equivalent to [`invert()`](Self::invert). - pub fn into_filter(mut self) -> BloomFilter { + pub fn invert(mut self) -> BloomFilter { for word in &mut self.inner.bit_array { *word = !*word; } @@ -689,11 +662,6 @@ impl BloomFilterInvertedView { self.inner } - /// Returns a reference to the underlying [`BloomFilter`] representation. - pub fn as_filter(&self) -> &BloomFilter { - &self.inner - } - /// Returns whether no bits are set in the inverted filter view. pub fn is_empty(&self) -> bool { self.inner.is_empty() @@ -729,7 +697,6 @@ impl BloomFilterInvertedView { self.inner.estimated_size() } } - /// Builder for creating [`BloomFilter`] instances. /// /// Provides two construction modes: diff --git a/tests-integration/tests/bloom_test/sketch.rs b/tests-integration/tests/bloom_test/sketch.rs index be5c282f..618549ca 100644 --- a/tests-integration/tests/bloom_test/sketch.rs +++ b/tests-integration/tests/bloom_test/sketch.rs @@ -105,18 +105,6 @@ fn test_invert_is_reversible() { assert_eq!(restored, original); } -#[test] -fn test_invert_into_filter() { - let mut filter = filter(); - filter.insert("apple"); - - let original = filter.clone(); - let inverted = filter.invert(); - let restored = inverted.into_filter(); - - assert_eq!(restored, original); -} - #[test] fn test_inverted_view_queries() { let mut filter = filter(); From c15b4f93cfb85d02c5dfd75bce70819c5c08f4b0 Mon Sep 17 00:00:00 2001 From: Rachit <145279448+v0id-X@users.noreply.github.com> Date: Thu, 3 Sep 2026 02:10:43 +0000 Subject: [PATCH 4/4] docs(bloom): uncomment inverted view doctest snippet --- datasketches/src/bloom/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/datasketches/src/bloom/mod.rs b/datasketches/src/bloom/mod.rs index 7f93483d..7f72d2b0 100644 --- a/datasketches/src/bloom/mod.rs +++ b/datasketches/src/bloom/mod.rs @@ -130,8 +130,8 @@ //! // filter1.intersect(&filter2).unwrap(); //! //! // Invert: returns a read-only inverted view -//! //let inverted = filter1.invert(); -//! //assert!(!inverted.contains(&"a")); +//! let inverted = filter1.invert(); +//! assert!(!inverted.contains(&"a")); //! ``` //! //! # Implementation Details