Skip to content

Commit bbda6d9

Browse files
committed
feat(tuple): add tuple intersection backed by a shared raw state machine
Extract RawThetaIntersection into thetacommon (mirroring RawThetaUnion) and rebuild both ThetaIntersection and the new TupleIntersection on it.
1 parent 6e3efa3 commit bbda6d9

9 files changed

Lines changed: 1126 additions & 170 deletions

File tree

datasketches/src/theta/hash_table.rs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -77,14 +77,10 @@ impl ThetaHashTable {
7777
}
7878

7979
/// Get iterator over entries.
80+
#[cfg(test)]
8081
pub fn iter(&self) -> impl Iterator<Item = u64> + '_ {
8182
self.iter_entries().map(RawHashTableEntry::hash)
8283
}
83-
84-
/// Returns true if the given hash exists in the table.
85-
pub fn contains_hash(&self, hash: u64) -> bool {
86-
self.get_entry(hash).is_some()
87-
}
8884
}
8985

9086
#[cfg(test)]

datasketches/src/theta/intersection.rs

Lines changed: 25 additions & 160 deletions
Original file line numberDiff line numberDiff line change
@@ -15,39 +15,35 @@
1515
// specific language governing permissions and limitations
1616
// under the License.
1717

18-
use crate::common::ResizeFactor;
1918
use crate::error::Error;
2019
use crate::hash::DEFAULT_UPDATE_SEED;
2120
use crate::theta::CompactThetaSketch;
2221
use crate::theta::ThetaSketchView;
23-
use crate::theta::hash_table::ThetaHashTable;
24-
use crate::thetacommon::constants::HASH_TABLE_REBUILD_THRESHOLD;
25-
use crate::thetacommon::constants::MAX_THETA;
22+
use crate::theta::hash_table::ThetaEntry;
23+
use crate::thetacommon::intersection::RawThetaIntersection;
24+
use crate::thetacommon::intersection::RawThetaIntersectionPolicy;
2625

2726
/// Stateful intersection operator for Theta sketches.
2827
///
2928
/// Before the first [`update`](Self::update), the result is undefined; use
3029
/// [`has_result`](Self::has_result) to check.
3130
#[derive(Debug)]
3231
pub struct ThetaIntersection {
33-
is_valid: bool,
34-
table: ThetaHashTable,
32+
raw: RawThetaIntersection<ThetaEntry, NoopIntersectionPolicy>,
33+
}
34+
35+
#[derive(Debug)]
36+
struct NoopIntersectionPolicy;
37+
38+
impl RawThetaIntersectionPolicy<ThetaEntry> for NoopIntersectionPolicy {
39+
fn merge(&self, _existing: &mut ThetaEntry, _incoming: ThetaEntry) {}
3540
}
3641

3742
impl ThetaIntersection {
3843
/// Creates a new intersection operator for the given `seed`.
3944
pub fn new(seed: u64) -> Self {
4045
Self {
41-
is_valid: false,
42-
table: ThetaHashTable::from_raw_parts(
43-
0,
44-
0,
45-
ResizeFactor::X1,
46-
1.0,
47-
MAX_THETA,
48-
seed,
49-
false,
50-
),
46+
raw: RawThetaIntersection::new(seed, NoopIntersectionPolicy),
5147
}
5248
}
5349

@@ -62,144 +58,12 @@ impl ThetaIntersection {
6258
/// and every update can reduce the current set to leave the overlapping
6359
/// subset only.
6460
pub fn update<S: ThetaSketchView>(&mut self, sketch: &S) -> Result<(), Error> {
65-
let new_default_table = |table: &ThetaHashTable| {
66-
ThetaHashTable::from_raw_parts(
67-
0,
68-
0,
69-
ResizeFactor::X1,
70-
1.0,
71-
table.theta(),
72-
table.hash_seed(),
73-
table.is_empty(),
74-
)
75-
};
76-
77-
if self.table.is_empty() {
78-
return Ok(());
79-
}
80-
81-
if !sketch.is_empty() && sketch.seed_hash() != self.table.seed_hash() {
82-
return Err(Error::invalid_argument(format!(
83-
"incompatible seed hash: expected {}, got {}",
84-
self.table.seed_hash(),
85-
sketch.seed_hash()
86-
)));
87-
}
88-
89-
if sketch.is_empty() {
90-
self.table.set_empty(true);
91-
}
92-
93-
self.table.set_theta(if self.table.is_empty() {
94-
MAX_THETA
95-
} else {
96-
self.table.theta().min(sketch.theta())
97-
});
98-
99-
if self.is_valid && self.table.num_retained() == 0 {
100-
return Ok(());
101-
}
102-
103-
if sketch.num_retained() == 0 {
104-
self.is_valid = true;
105-
self.table = new_default_table(&self.table);
106-
return Ok(());
107-
}
108-
109-
// first update, copy or move incoming sketch
110-
if !self.is_valid {
111-
self.is_valid = true;
112-
let lg_size = ThetaHashTable::lg_size_from_count_for_rebuild(
113-
sketch.num_retained(),
114-
HASH_TABLE_REBUILD_THRESHOLD,
115-
);
116-
self.table = ThetaHashTable::from_raw_parts(
117-
lg_size,
118-
lg_size - 1,
119-
ResizeFactor::X1,
120-
1.0,
121-
self.table.theta(),
122-
self.table.hash_seed(),
123-
self.table.is_empty(),
124-
);
125-
for entry in sketch.iter() {
126-
let hash = entry.hash();
127-
if !self.table.try_insert_hash(hash) {
128-
return Err(Error::invalid_argument(
129-
"Insert entries from sketch fail, possibly corrupted input sketch",
130-
));
131-
}
132-
}
133-
// Safety check.
134-
if self.table.num_retained() != sketch.num_retained() {
135-
return Err(Error::invalid_argument(
136-
"num entries mismatch, possibly corrupted input sketch",
137-
));
138-
}
139-
} else {
140-
let max_matches = self.table.num_retained().min(sketch.num_retained());
141-
let mut matched_entries = Vec::with_capacity(max_matches);
142-
let mut count = 0;
143-
for entry in sketch.iter() {
144-
let hash = entry.hash();
145-
if hash < self.table.theta() {
146-
if self.table.contains_hash(hash) {
147-
if matched_entries.len() == max_matches {
148-
return Err(Error::invalid_argument(
149-
"max matches exceeded, possibly corrupted input sketch",
150-
));
151-
}
152-
matched_entries.push(hash);
153-
}
154-
} else if sketch.is_ordered() {
155-
break; // early stop for ordered sketches
156-
}
157-
count += 1;
158-
}
159-
// Safety check.
160-
if count > sketch.num_retained() {
161-
return Err(Error::invalid_argument(
162-
"more keys than expected, possibly corrupted input sketch",
163-
));
164-
} else if !sketch.is_ordered() && count < sketch.num_retained() {
165-
return Err(Error::invalid_argument(
166-
"fewer keys than expected, possibly corrupted input sketch",
167-
));
168-
}
169-
if matched_entries.is_empty() {
170-
self.table = new_default_table(&self.table);
171-
if self.table.theta() == MAX_THETA {
172-
self.table.set_empty(true);
173-
}
174-
} else {
175-
let lg_size = ThetaHashTable::lg_size_from_count_for_rebuild(
176-
matched_entries.len(),
177-
HASH_TABLE_REBUILD_THRESHOLD,
178-
);
179-
self.table = ThetaHashTable::from_raw_parts(
180-
lg_size,
181-
lg_size - 1,
182-
ResizeFactor::X1,
183-
1.0,
184-
self.table.theta(),
185-
self.table.hash_seed(),
186-
self.table.is_empty(),
187-
);
188-
for hash in matched_entries {
189-
if !self.table.try_insert_hash(hash) {
190-
return Err(Error::invalid_argument(
191-
"duplicate key, possibly corrupted input sketch",
192-
));
193-
}
194-
}
195-
}
196-
}
197-
Ok(())
61+
self.raw.update(sketch)
19862
}
19963

20064
/// Returns whether this operator has received at least one update.
20165
pub fn has_result(&self) -> bool {
202-
self.is_valid
66+
self.raw.has_result()
20367
}
20468

20569
/// Returns the intersection result as a compact theta sketch.
@@ -209,19 +73,20 @@ impl ThetaIntersection {
20973
/// Panics if called before the first [`update`](Self::update).
21074
pub fn to_sketch(&self, ordered: bool) -> CompactThetaSketch {
21175
assert!(
212-
self.is_valid,
76+
self.raw.has_result(),
21377
"ThetaIntersection::to_sketch() called before first update()"
21478
);
215-
let mut hashes: Vec<u64> = self.table.iter().collect();
216-
if ordered {
217-
hashes.sort_unstable();
218-
}
79+
let parts = self.raw.result(ordered);
21980
CompactThetaSketch::from_parts(
220-
hashes,
221-
self.table.theta(),
222-
self.table.seed_hash(),
223-
ordered,
224-
self.table.is_empty(),
81+
parts
82+
.entries
83+
.into_iter()
84+
.map(|entry| entry.hash())
85+
.collect(),
86+
parts.theta,
87+
parts.seed_hash,
88+
parts.ordered,
89+
parts.empty,
22590
)
22691
}
22792
}

datasketches/src/thetacommon/hash_table.rs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,6 @@ where
169169
}
170170

171171
/// Returns a reference to the entry stored for `hash`, or `None` if the hash is not retained.
172-
#[allow(dead_code)] // used by the set operations; tuple set operations arrive in follow-ups
173172
pub fn get_entry(&self, hash: u64) -> Option<&E> {
174173
if hash == 0 {
175174
return None;
@@ -291,13 +290,11 @@ where
291290
}
292291

293292
/// Get the hash seed used by this table.
294-
#[allow(dead_code)] // used by the set operations; tuple set operations arrive in follow-ups
295293
pub fn hash_seed(&self) -> u64 {
296294
self.hash_seed
297295
}
298296

299297
/// Sets theta value.
300-
#[allow(dead_code)] // used by the set operations and tests; tuple set operations arrive in follow-ups
301298
pub fn set_theta(&mut self, theta: u64) {
302299
assert!(
303300
(1..=MAX_THETA).contains(&theta),
@@ -307,7 +304,6 @@ where
307304
}
308305

309306
/// Returns minimal lg_size where rebuild-capacity can hold `count`.
310-
#[allow(dead_code)] // used by the set operations; tuple set operations arrive in follow-ups
311307
pub fn lg_size_from_count_for_rebuild(count: usize, load_factor: f64) -> u8 {
312308
let log2 = |n: usize| {
313309
if n == 0 { 0_u8 } else { n.ilog2() as u8 }

0 commit comments

Comments
 (0)