This is an automated email from the ASF dual-hosted git repository.

tisonkun pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/datasketches-rust.git


The following commit(s) were added to refs/heads/main by this push:
     new 9cf7ddd  feat(hll): expose composite cardinality estimates (#294)
9cf7ddd is described below

commit 9cf7ddd1d25f4435212a5c484f892eccb0360716
Author: edouardmulliez <[email protected]>
AuthorDate: Thu Oct 8 19:05:26 2026 +0200

    feat(hll): expose composite cardinality estimates (#294)
    
    Co-authored-by: tison <[email protected]>
---
 CHANGELOG.md                                 |   1 +
 datasketches/src/hll/array4.rs               |   5 ++
 datasketches/src/hll/array6.rs               |   5 ++
 datasketches/src/hll/array8.rs               |   5 ++
 datasketches/src/hll/estimator.rs            |   2 +-
 datasketches/src/hll/sketch.rs               |  28 ++++++
 datasketches/src/hll/union.rs                |  20 +++++
 tests-integration/tests/hll_test/estimate.rs | 130 +++++++++++++++++++++++++++
 tests-integration/tests/hll_test/main.rs     |   1 +
 9 files changed, 196 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4f366e1..5cfc3df 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,7 @@ All significant changes to this project will be documented in 
this file.
 
 ### New features
 
+* Add `HllSketch::composite_estimate` and `HllUnion::composite_estimate` to 
query cardinality without relying on HIP update history, matching Java's 
`getCompositeEstimate()`.
 * `KllSketch` is now available behind the `kll` feature, with rank, quantile, 
PMF, and CDF queries, merging, serialization, custom ordered item types, and a 
`KllFloat` adapter for non-NaN floating-point values.
 * Add `TDigestMut::quantiles` and `TDigest::quantiles` for querying several 
ranks in one centroid scan while preserving input order.
 * Add `FromIterator<TDigestMut>` as an alternative to incremental 
`TDigestMut::merge` calls for combining owned partial sketches in a single 
compression pass.
diff --git a/datasketches/src/hll/array4.rs b/datasketches/src/hll/array4.rs
index 1a13059..19c9632 100644
--- a/datasketches/src/hll/array4.rs
+++ b/datasketches/src/hll/array4.rs
@@ -241,6 +241,11 @@ impl Array4 {
             .estimate(self.lg_config_k, self.cur_min, self.num_at_cur_min)
     }
 
+    pub fn composite_estimate(&self) -> f64 {
+        self.estimator
+            .composite_estimate(self.lg_config_k, self.cur_min, 
self.num_at_cur_min)
+    }
+
     pub fn upper_bound(&self, num_std_dev: NumStdDev) -> f64 {
         self.estimator.upper_bound(
             self.lg_config_k,
diff --git a/datasketches/src/hll/array6.rs b/datasketches/src/hll/array6.rs
index a1b0032..c29a901 100644
--- a/datasketches/src/hll/array6.rs
+++ b/datasketches/src/hll/array6.rs
@@ -123,6 +123,11 @@ impl Array6 {
         self.estimator.estimate(self.lg_config_k, 0, self.num_zeros)
     }
 
+    pub fn composite_estimate(&self) -> f64 {
+        self.estimator
+            .composite_estimate(self.lg_config_k, 0, self.num_zeros)
+    }
+
     pub fn upper_bound(&self, num_std_dev: NumStdDev) -> f64 {
         self.estimator
             .upper_bound(self.lg_config_k, 0, self.num_zeros, num_std_dev)
diff --git a/datasketches/src/hll/array8.rs b/datasketches/src/hll/array8.rs
index a2f60aa..355bcc1 100644
--- a/datasketches/src/hll/array8.rs
+++ b/datasketches/src/hll/array8.rs
@@ -87,6 +87,11 @@ impl Array8 {
         self.estimator.estimate(self.lg_config_k, 0, self.num_zeros)
     }
 
+    pub fn composite_estimate(&self) -> f64 {
+        self.estimator
+            .composite_estimate(self.lg_config_k, 0, self.num_zeros)
+    }
+
     pub fn upper_bound(&self, num_std_dev: NumStdDev) -> f64 {
         self.estimator
             .upper_bound(self.lg_config_k, 0, self.num_zeros, num_std_dev)
diff --git a/datasketches/src/hll/estimator.rs 
b/datasketches/src/hll/estimator.rs
index 1a22809..457dff8 100644
--- a/datasketches/src/hll/estimator.rs
+++ b/datasketches/src/hll/estimator.rs
@@ -182,7 +182,7 @@ impl Estimator {
     }
 
     /// Selects between bias-corrected HLL and bitmap estimates using their 
empirical crossover.
-    fn composite_estimate(&self, lg_config_k: u8, cur_min: u8, num_at_cur_min: 
u32) -> f64 {
+    pub fn composite_estimate(&self, lg_config_k: u8, cur_min: u8, 
num_at_cur_min: u32) -> f64 {
         let raw_estimate = self.raw_estimate(lg_config_k);
 
         let x_arr = composite_interpolation::get_x_arr(lg_config_k);
diff --git a/datasketches/src/hll/sketch.rs b/datasketches/src/hll/sketch.rs
index 437062f..06dd823 100644
--- a/datasketches/src/hll/sketch.rs
+++ b/datasketches/src/hll/sketch.rs
@@ -232,6 +232,34 @@ impl HllSketch {
         }
     }
 
+    /// Returns the current cardinality estimate that never depends on
+    /// the order in which values were inserted.
+    ///
+    /// This can be less accurate than [`estimate`](Self::estimate).
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// use datasketches::hll::HllSketch;
+    /// use datasketches::hll::HllType;
+    ///
+    /// let mut sketch = HllSketch::new(12, HllType::Hll4).unwrap();
+    /// for value in 0..10_000 {
+    ///     sketch.update(value);
+    /// }
+    /// let estimate = sketch.composite_estimate();
+    /// assert!((estimate - 10_000.0).abs() < 500.0);
+    /// ```
+    pub fn composite_estimate(&self) -> f64 {
+        match &self.mode {
+            Mode::List { list, .. } => list.container().estimate(),
+            Mode::Set { set, .. } => set.container().estimate(),
+            Mode::Array4(arr) => arr.composite_estimate(),
+            Mode::Array6(arr) => arr.composite_estimate(),
+            Mode::Array8(arr) => arr.composite_estimate(),
+        }
+    }
+
     /// Returns the upper confidence bound for `num_std_dev` standard 
deviations.
     ///
     /// # Examples
diff --git a/datasketches/src/hll/union.rs b/datasketches/src/hll/union.rs
index f16cebc..0f6e20e 100644
--- a/datasketches/src/hll/union.rs
+++ b/datasketches/src/hll/union.rs
@@ -307,6 +307,26 @@ impl HllUnion {
         self.gadget.estimate()
     }
 
+    /// Returns the union's cardinality estimate using the composite estimator.
+    ///
+    /// See [`HllSketch::composite_estimate`].
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// use datasketches::hll::HllUnion;
+    ///
+    /// let mut union = HllUnion::new(12).unwrap();
+    /// for value in 0..10_000 {
+    ///     union.update_value(value);
+    /// }
+    /// let estimate = union.composite_estimate();
+    /// assert!((estimate - 10_000.0).abs() < 500.0);
+    /// ```
+    pub fn composite_estimate(&self) -> f64 {
+        self.gadget.composite_estimate()
+    }
+
     /// Returns the upper confidence bound for `num_std_dev` standard 
deviations.
     ///
     /// # Examples
diff --git a/tests-integration/tests/hll_test/estimate.rs 
b/tests-integration/tests/hll_test/estimate.rs
new file mode 100644
index 0000000..3724259
--- /dev/null
+++ b/tests-integration/tests/hll_test/estimate.rs
@@ -0,0 +1,130 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use datasketches::hll::HllSketch;
+use datasketches::hll::HllType;
+use datasketches::hll::HllUnion;
+use googletest::assert_that;
+use googletest::prelude::near;
+
+const HLL_TYPES: [HllType; 3] = [HllType::Hll4, HllType::Hll6, HllType::Hll8];
+
+fn make_sketch(hll_type: HllType, values: impl Iterator<Item = u64>) -> 
HllSketch {
+    let mut sketch = HllSketch::new(11, hll_type).unwrap();
+    for value in values {
+        sketch.update(value);
+    }
+    sketch
+}
+
+#[test]
+fn composite_estimate_matches_coupon_estimate_in_list_and_set_modes() {
+    for hll_type in HLL_TYPES {
+        for n in [0, 3, 100] {
+            let sketch = make_sketch(hll_type, 0..n);
+            assert_eq!(sketch.composite_estimate(), sketch.estimate());
+        }
+    }
+}
+
+#[test]
+fn composite_estimate_is_independent_of_insertion_order() {
+    for hll_type in HLL_TYPES {
+        for n in [1_000, 10_000, 100_000] {
+            let forward = make_sketch(hll_type, 0..n);
+            let reverse = make_sketch(hll_type, (0..n).rev());
+            assert_ne!(forward.estimate(), reverse.estimate());
+            assert_that!(
+                forward.composite_estimate(),
+                near(reverse.composite_estimate(), 1e-9),
+                "type={hll_type:?}, n={n}"
+            );
+        }
+    }
+}
+
+#[test]
+fn composite_estimate_matches_estimate_after_register_merge() {
+    for hll_type in HLL_TYPES {
+        for n in [1_000, 10_000, 100_000] {
+            let sketch = make_sketch(hll_type, 0..n);
+            let mut union = HllUnion::new(sketch.lg_config_k()).unwrap();
+            union.update(&sketch);
+            union.update(&sketch);
+            assert_that!(sketch.composite_estimate(), near(union.estimate(), 
1e-9));
+            assert_eq!(union.composite_estimate(), union.estimate());
+            for result_type in HLL_TYPES {
+                assert_that!(
+                    union.to_sketch(result_type).composite_estimate(),
+                    near(union.estimate(), 1e-9)
+                );
+            }
+        }
+    }
+}
+
+#[test]
+fn composite_estimate_preserves_hip_state_and_subsequent_updates() {
+    for hll_type in HLL_TYPES {
+        let mut sketch = make_sketch(hll_type, 0..10_000);
+        let mut control = sketch.clone();
+        let estimate = sketch.estimate();
+        let bytes = sketch.serialize();
+        assert_ne!(sketch.composite_estimate(), estimate);
+        assert_eq!(sketch.estimate(), estimate);
+        assert_eq!(sketch.serialize(), bytes);
+
+        let decoded = HllSketch::deserialize(&bytes).unwrap();
+        assert_eq!(decoded.composite_estimate(), sketch.composite_estimate());
+        assert_eq!(decoded.estimate(), estimate);
+
+        for value in 10_000..20_000_u64 {
+            sketch.update(value);
+            control.update(value);
+        }
+        assert_eq!(sketch.serialize(), control.serialize());
+    }
+}
+
+#[test]
+fn union_composite_estimate_works_with_direct_updates_and_single_sketch() {
+    for n in [0, 3, 100, 10_000] {
+        let sketch = make_sketch(HllType::Hll8, 0..n);
+        let mut direct = HllUnion::new(11).unwrap();
+        for value in 0..n {
+            direct.update_value(value);
+        }
+        let direct_estimate = direct.estimate();
+        assert_eq!(direct.composite_estimate(), sketch.composite_estimate());
+        assert_eq!(direct.estimate(), direct_estimate);
+
+        for hll_type in HLL_TYPES {
+            let sketch = make_sketch(hll_type, 0..n);
+            let mut union = HllUnion::new(11).unwrap();
+            union.update(&sketch);
+            let hip_estimate = union.estimate();
+            assert_eq!(hip_estimate, sketch.estimate());
+            assert_that!(
+                union.composite_estimate(),
+                near(sketch.composite_estimate(), 1e-9)
+            );
+            assert_eq!(union.estimate(), hip_estimate);
+            union.reset();
+            assert_eq!(union.composite_estimate(), 0.0);
+        }
+    }
+}
diff --git a/tests-integration/tests/hll_test/main.rs 
b/tests-integration/tests/hll_test/main.rs
index 30fb7dc..5a892ff 100644
--- a/tests-integration/tests/hll_test/main.rs
+++ b/tests-integration/tests/hll_test/main.rs
@@ -16,5 +16,6 @@
 // under the License.
 
 mod bounds;
+mod estimate;
 mod union;
 mod update;


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to