Skip to content

Commit 81ec1cb

Browse files
ariesdeviltisonkun
andauthored
feat: add tuple sketch implementation (#152)
Co-authored-by: tison <wander4096@gmail.com>
1 parent ee6e1cc commit 81ec1cb

20 files changed

Lines changed: 2301 additions & 69 deletions

datasketches/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ frequencies = []
4545
hll = []
4646
tdigest = []
4747
theta = []
48+
tuple = []
4849

4950
[dev-dependencies]
5051
googletest = { workspace = true }

datasketches/src/codec/decode.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,16 @@ impl SketchSlice<'_> {
3838
self.slice.set_position(pos + n);
3939
}
4040

41+
/// Returns the not-yet-read portion of the underlying slice.
42+
///
43+
/// Useful for handing the remaining bytes to a variable-length decoder that reports how many
44+
/// bytes it consumed; pair it with [`advance`](Self::advance).
45+
pub fn remaining(&self) -> &[u8] {
46+
let buf = self.slice.get_ref();
47+
let pos = (self.slice.position() as usize).min(buf.len());
48+
&buf[pos..]
49+
}
50+
4151
/// Reads exactly `buf.len()` bytes from the slice into `buf`.
4252
pub fn read_exact(&mut self, buf: &mut [u8]) -> io::Result<()> {
4353
self.slice.read_exact(buf)

datasketches/src/codec/family.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,15 @@ impl Family {
5353
max_pre_longs: 1,
5454
};
5555

56+
/// Tuple Sketch for cardinality estimation with per-key summaries.
57+
#[cfg(feature = "tuple")]
58+
pub const TUPLE: Family = Family {
59+
id: 9,
60+
name: "TUPLE",
61+
min_pre_longs: 1,
62+
max_pre_longs: 3,
63+
};
64+
5665
/// The Frequency family of sketches.
5766
#[cfg(feature = "frequencies")]
5867
pub const FREQUENCY: Family = Family {

datasketches/src/codec/mod.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ pub use self::encode::SketchBytes;
2929
feature = "frequencies",
3030
feature = "hll",
3131
feature = "tdigest",
32-
feature = "theta"
32+
feature = "theta",
33+
feature = "tuple",
3334
))]
3435
#[allow(dead_code)] // some utilities are only used for certain sketches
3536
pub(crate) mod assert;
@@ -41,6 +42,7 @@ pub(crate) mod assert;
4142
feature = "frequencies",
4243
feature = "hll",
4344
feature = "tdigest",
44-
feature = "theta"
45+
feature = "theta",
46+
feature = "tuple",
4547
))]
4648
pub(crate) mod family;

datasketches/src/error.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,7 @@ impl Error {
8989
}
9090
}
9191

92-
// pub(crate) convenient constructors
93-
#[allow(dead_code)] // some constructors are only used for certain sketches
92+
#[allow(dead_code)] // some convenient constructors are only used for certain sketches
9493
impl Error {
9594
pub(crate) fn invalid_argument(msg: impl Into<String>) -> Self {
9695
Self::new(ErrorKind::InvalidArgument, msg)

datasketches/src/hash/mod.rs

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,17 @@
2020
feature = "cpc",
2121
feature = "frequencies",
2222
feature = "hll",
23-
feature = "theta"
23+
feature = "theta",
24+
feature = "tuple",
2425
))]
2526
mod murmurhash;
2627
#[cfg(any(
2728
feature = "countmin",
2829
feature = "cpc",
2930
feature = "frequencies",
3031
feature = "hll",
31-
feature = "theta"
32+
feature = "theta",
33+
feature = "tuple",
3234
))]
3335
pub(crate) use self::murmurhash::MurmurHash3X64128;
3436

@@ -56,7 +58,8 @@ pub(crate) use self::xxhash::XxHash64;
5658
feature = "cpc",
5759
feature = "frequencies",
5860
feature = "hll",
59-
feature = "theta"
61+
feature = "theta",
62+
feature = "tuple",
6063
))]
6164
pub(crate) const DEFAULT_UPDATE_SEED: u64 = 9001;
6265

@@ -68,7 +71,12 @@ pub(crate) const DEFAULT_UPDATE_SEED: u64 = 9001;
6871
/// # Panics
6972
///
7073
/// Panics if the computed seed hash is zero.
71-
#[cfg(any(feature = "countmin", feature = "cpc", feature = "theta"))]
74+
#[cfg(any(
75+
feature = "countmin",
76+
feature = "cpc",
77+
feature = "theta",
78+
feature = "tuple",
79+
))]
7280
pub(crate) fn compute_seed_hash(seed: u64) -> u16 {
7381
use std::hash::Hasher;
7482

@@ -91,7 +99,8 @@ pub(crate) fn compute_seed_hash(seed: u64) -> u16 {
9199
feature = "cpc",
92100
feature = "frequencies",
93101
feature = "hll",
94-
feature = "theta"
102+
feature = "theta",
103+
feature = "tuple",
95104
))]
96105
fn read_u64_le(bytes: &[u8]) -> u64 {
97106
let mut buf = [0u8; 8];

datasketches/src/lib.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,11 @@ pub mod hll;
4545
pub mod tdigest;
4646
#[cfg(feature = "theta")]
4747
pub mod theta;
48-
#[cfg(feature = "theta")]
48+
#[cfg(any(feature = "theta", feature = "tuple"))]
49+
#[allow(dead_code)] // some utilities are only used for certain sketches
4950
pub mod thetacommon;
51+
#[cfg(feature = "tuple")]
52+
pub mod tuple;
5053

5154
// common modules
5255
pub mod codec;

datasketches/src/theta/serialization.rs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,3 @@ pub(super) const COMPRESSED_SERIAL_VERSION: u8 = 4;
2323
pub(super) const V2_PREAMBLE_EMPTY: u8 = 1;
2424
pub(super) const V2_PREAMBLE_PRECISE: u8 = 2;
2525
pub(super) const V2_PREAMBLE_ESTIMATE: u8 = 3;
26-
27-
pub(super) const FLAGS_IS_READ_ONLY: u8 = 1 << 1;
28-
pub(super) const FLAGS_IS_EMPTY: u8 = 1 << 2;
29-
pub(super) const FLAGS_IS_COMPACT: u8 = 1 << 3;
30-
pub(super) const FLAGS_IS_ORDERED: u8 = 1 << 4;

datasketches/src/theta/sketch.rs

Lines changed: 27 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ use crate::theta::serialization::V2_PREAMBLE_PRECISE;
4646
use crate::thetacommon::RawThetaSketchView;
4747
use crate::thetacommon::binomial_bounds;
4848
use crate::thetacommon::constants::DEFAULT_LG_K;
49+
use crate::thetacommon::constants::FLAGS_IS_COMPACT;
50+
use crate::thetacommon::constants::FLAGS_IS_EMPTY;
51+
use crate::thetacommon::constants::FLAGS_IS_ORDERED;
52+
use crate::thetacommon::constants::FLAGS_IS_READ_ONLY;
4953
use crate::thetacommon::constants::MAX_LG_K;
5054
use crate::thetacommon::constants::MAX_THETA;
5155
use crate::thetacommon::constants::MIN_LG_K;
@@ -220,25 +224,18 @@ impl ThetaSketch {
220224
/// assert_eq!(compact.num_retained(), 1);
221225
/// ```
222226
pub fn compact(&self, ordered: bool) -> CompactThetaSketch {
223-
let mut entries: Vec<u64> = self.iter().map(|entry| entry.hash()).collect();
224-
225-
let empty = self.is_empty();
226-
let theta = if empty {
227-
// Match Java's correctThetaOnCompact() behavior for never-updated sketches
228-
// initialized with p < 1.0.
229-
MAX_THETA
230-
} else {
231-
self.table.theta()
232-
};
233-
let is_single = entries.len() == 1 && theta == MAX_THETA;
234-
// Empty or Single-item sketches are always ordered (Java compatibility)
235-
let ordered = ordered || empty || is_single;
236-
237-
if ordered && entries.len() > 1 {
238-
entries.sort_unstable();
239-
}
240-
241-
CompactThetaSketch::from_parts(entries, theta, self.table.seed_hash(), ordered, empty)
227+
let parts = self.table.to_compact_parts(ordered);
228+
CompactThetaSketch::from_parts(
229+
parts
230+
.entries
231+
.into_iter()
232+
.map(|entry| entry.hash())
233+
.collect(),
234+
parts.theta,
235+
parts.seed_hash,
236+
parts.ordered,
237+
parts.empty,
238+
)
242239
}
243240

244241
/// Returns the approximate lower error bound given the specified number of Standard Deviations.
@@ -467,13 +464,13 @@ impl CompactThetaSketch {
467464
bytes.write_u16_be(0); // unused for compact
468465

469466
let mut flags = 0u8;
470-
flags |= serialization::FLAGS_IS_READ_ONLY;
471-
flags |= serialization::FLAGS_IS_COMPACT;
467+
flags |= FLAGS_IS_READ_ONLY;
468+
flags |= FLAGS_IS_COMPACT;
472469
if self.is_empty() {
473-
flags |= serialization::FLAGS_IS_EMPTY;
470+
flags |= FLAGS_IS_EMPTY;
474471
}
475472
if self.is_ordered() {
476-
flags |= serialization::FLAGS_IS_ORDERED;
473+
flags |= FLAGS_IS_ORDERED;
477474
}
478475
bytes.write_u8(flags);
479476

@@ -510,9 +507,9 @@ impl CompactThetaSketch {
510507
bytes.write_u8(num_entries_bytes);
511508

512509
let mut flags = 0u8;
513-
flags |= serialization::FLAGS_IS_READ_ONLY;
514-
flags |= serialization::FLAGS_IS_COMPACT;
515-
flags |= serialization::FLAGS_IS_ORDERED;
510+
flags |= FLAGS_IS_READ_ONLY;
511+
flags |= FLAGS_IS_COMPACT;
512+
flags |= FLAGS_IS_ORDERED;
516513
bytes.write_u8(flags);
517514

518515
bytes.write_u16_le(self.seed_hash);
@@ -748,7 +745,7 @@ impl CompactThetaSketch {
748745
.read_u16_le()
749746
.map_err(insufficient_data("seed_hash"))?;
750747

751-
let empty = (flags & serialization::FLAGS_IS_EMPTY) != 0;
748+
let empty = (flags & FLAGS_IS_EMPTY) != 0;
752749
let mut theta = MAX_THETA;
753750
let num_entries;
754751
let mut entries = vec![];
@@ -776,7 +773,7 @@ impl CompactThetaSketch {
776773
}
777774
entries = Self::read_entries(&mut cursor, num_entries as usize, theta)?;
778775
}
779-
let ordered = (flags & serialization::FLAGS_IS_ORDERED) != 0;
776+
let ordered = (flags & FLAGS_IS_ORDERED) != 0;
780777
Ok(Self {
781778
entries,
782779
theta,
@@ -797,7 +794,7 @@ impl CompactThetaSketch {
797794
let seed_hash = cursor
798795
.read_u16_le()
799796
.map_err(insufficient_data("seed_hash"))?;
800-
let empty = (flags & serialization::FLAGS_IS_EMPTY) != 0;
797+
let empty = (flags & FLAGS_IS_EMPTY) != 0;
801798
if !empty {
802799
let expected_seed_hash = compute_seed_hash(expected_seed);
803800
if seed_hash != expected_seed_hash {
@@ -861,7 +858,7 @@ impl CompactThetaSketch {
861858
}
862859
}
863860

864-
let ordered = (flags & serialization::FLAGS_IS_ORDERED) != 0;
861+
let ordered = (flags & FLAGS_IS_ORDERED) != 0;
865862

866863
Ok(Self {
867864
entries,

datasketches/src/theta/union.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -60,17 +60,17 @@ impl ThetaUnion {
6060

6161
/// Return this union as a compact sketch.
6262
pub fn to_sketch(&self, ordered: bool) -> CompactThetaSketch {
63-
let result = self.raw.result(ordered);
63+
let parts = self.raw.to_compact_parts(ordered);
6464
CompactThetaSketch::from_parts(
65-
result
65+
parts
6666
.entries
6767
.into_iter()
6868
.map(|entry| entry.hash())
6969
.collect(),
70-
result.theta,
71-
result.seed_hash,
72-
result.ordered,
73-
result.empty,
70+
parts.theta,
71+
parts.seed_hash,
72+
parts.ordered,
73+
parts.empty,
7474
)
7575
}
7676

0 commit comments

Comments
 (0)