Add UpdatableThetaSketch.compactTrimmed() to compact and reduce to k in one pass - #763
Open
leerho wants to merge 1 commit into
Open
Add UpdatableThetaSketch.compactTrimmed() to compact and reduce to k in one pass#763leerho wants to merge 1 commit into
leerho wants to merge 1 commit into
Conversation
…in one pass
An update sketch retains more than the nominal size k between rebuilds. Callers
who need a result bounded by k currently have to call rebuild(), which mutates
the sketch and rebuilds its hash table, before compact().
This adds two concrete methods on UpdatableThetaSketch:
public CompactThetaSketch compactTrimmed()
public CompactThetaSketch compactTrimmed(boolean dstOrdered, MemorySegment dstWSeg)
Both are concrete on the base, so none of the subclasses change. A separate
method rather than a trim flag on compact(): Java has no default parameter
values, so a flag would only ever be passed true, and compact(dstOrdered,
MemorySegment) is abstract with seven overriders, so its signature cannot grow
a parameter in any case.
The implementation gathers the valid entries into a dense array, since the
method must not modify the sketch and QuickSelect.select permutes whatever
array it is given, then selects 0-based index k. That is the same hash value
the (k + 1) 1-based pivot yields in rebuild(), so the two cannot drift apart,
and a test asserts the result serializes byte-for-byte identically to
rebuild() + compact().
Because it never mutates, this also works on a read-only sketch, where
rebuild() throws SketchesReadOnlyException.
The Alpha family is excluded and throws UnsupportedOperationException: it
maintains theta by its own discipline and never needs reducing to k. The guard
is a family denylist rather than a QuickSelect allowlist, because Theta's
family names are historically fragmented and an allowlist could wrongly reject
a legitimate member.
Trimming stays an explicit opt-in because it is lossy. Relative error scales
with 1 / sqrt(retained), so discarding entries always widens the confidence
bounds, and a sketch in exact mode that retains more than k loses exactness and
is returned in estimation mode. Both effects are documented and pinned by tests.
This is the Java counterpart of apache/datasketches-cpp#524.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017EDHa7UhfW4eSj5L82panJ
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changed
Two concrete methods on
UpdatableThetaSketch:They produce a
CompactThetaSketchreduced to at most the nominal sizek, without mutating the source sketch. Today the only way to get ak-bounded result isrebuild()thencompact(), andrebuild()mutates the sketch and rebuilds its hash table.Java counterpart of apache/datasketches-cpp#524, which came out of apache/datasketches-cpp#515 by @stojkomilos — thanks for raising the need there.
Why a separate method rather than a
trimflag oncompact()The C++ side adds an optional
trimparameter to the existingcompact(). Java cannot do the same, for two independent reasons:ThetaSketch.compact(boolean dstOrdered, MemorySegment dstSeg)is abstract with seven overriders, so its signature cannot grow a parameter.compact(boolean, MemorySegment, boolean trim)overload would only ever be called withtrim = true— passingfalseis strictly more typing than the existing two-arg call — so the parameter would be dead weight. A named method makes the opt-in explicit at the call site, which is the actual goal.Both methods are concrete on the base class, so no subclass changes.
Why the Alpha family is excluded
compactTrimmedthrowsUnsupportedOperationExceptionforFamily.ALPHA. Alpha maintains theta by its own discipline and never needs reducing tok; itsrebuild()only purges dirty values and does not trim.The guard is a denylist (
getFamily() == Family.ALPHA) rather than a QuickSelect allowlist on purpose. Theta's family names are historically fragmented in a way no other sketch's are, so an allowlist risks wrongly rejecting a legitimate member.This also matters for correctness, not just taste: a dirty Alpha sketch keeps non-zero entries at or above theta in its cache, so
getRetainedEntries(true)undercounts the array's non-zeros andselectExcludingZeroswould mis-adjust its pivot.Implementation
The valid entries are gathered into a dense array first — the method must not modify the sketch, and
QuickSelect.selectpermutes whatever array it is given — then:Index
kis 0-based, so this is the (k + 1)th smallest hash: the same value the(k + 1)1-based pivot yields inHeapQuickSelectSketch.quickSelectAndRebuild(). The two cannot drift apart.Because it never mutates,
compactTrimmedalso works on a read-only sketch, whererebuild()throwsSketchesReadOnlyException.Why trimming is an explicit opt-in
Relative error scales with
1 / sqrt(retained), so discarding entries always widens the confidence bounds. Measured on the C++ side at the defaultlg_k, comparing untrimmed against trimmed 2-sigma widths: 1.03x to 1.27x depending on where in the rebuild cycle the sketch is caught, and up to aboutsqrt(15/8)(~37%) worst case for a sketch grown to just under the rebuild threshold.Separately, a sketch in exact mode can retain more than
kentries — nothing has been evicted, so theta is still 1.0. Trimming there discards real data and returns an estimating sketch, sogetEstimate()carries error where it would have returned an exact count. Both effects are documented on the methods and pinned by tests.How tested
New
CompactTrimTest, 6 cases:rebuild()+compact()on theta and retained set, that every retained hash is below the new theta, and that the source sketch is unmodifiedcompactTrimmed(true, null).toByteArray()is byte-for-byte identical torebuild().compact(true, null).toByteArray(), and the no-arg form matches the explicit onecompact()still works on it🤖 Generated with Claude Code
https://claude.ai/code/session_017EDHa7UhfW4eSj5L82panJ