Skip to content

Add UpdatableThetaSketch.compactTrimmed() to compact and reduce to k in one pass - #763

Open
leerho wants to merge 1 commit into
mainfrom
theta-compact-trim
Open

Add UpdatableThetaSketch.compactTrimmed() to compact and reduce to k in one pass#763
leerho wants to merge 1 commit into
mainfrom
theta-compact-trim

Conversation

@leerho

@leerho leerho commented Sep 8, 2026

Copy link
Copy Markdown
Member

What changed

Two concrete methods on UpdatableThetaSketch:

public CompactThetaSketch compactTrimmed()
public CompactThetaSketch compactTrimmed(boolean dstOrdered, MemorySegment dstWSeg)

They produce a CompactThetaSketch reduced to at most the nominal size k, without mutating the source sketch. Today the only way to get a k-bounded result is rebuild() then compact(), and rebuild() 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 trim flag on compact()

The C++ side adds an optional trim parameter to the existing compact(). Java cannot do the same, for two independent reasons:

  1. ThetaSketch.compact(boolean dstOrdered, MemorySegment dstSeg) is abstract with seven overriders, so its signature cannot grow a parameter.
  2. Java has no default parameter values. A compact(boolean, MemorySegment, boolean trim) overload would only ever be called with trim = true — passing false is 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

compactTrimmed throws UnsupportedOperationException for Family.ALPHA. Alpha maintains theta by its own discipline and never needs reducing to k; its rebuild() 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 and selectExcludingZeros would mis-adjust its pivot.

Implementation

The valid entries are gathered into a dense array first — the method must not modify the sketch, and QuickSelect.select permutes whatever array it is given — then:

thetaLong = select(validArr, 0, n - 1, k);

Index k is 0-based, so this is the (k + 1)th smallest hash: the same value the (k + 1) 1-based pivot yields in HeapQuickSelectSketch.quickSelectAndRebuild(). The two cannot drift apart.

Because it never mutates, compactTrimmed also works on a read-only sketch, where rebuild() throws SketchesReadOnlyException.

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 default lg_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 about sqrt(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 k entries — nothing has been evicted, so theta is still 1.0. Trimming there discards real data and returns an estimating sketch, so getEstimate() 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:

  • the four ordered/trimmed combinations, asserting the trimmed result matches rebuild() + compact() on theta and retained set, that every retained hash is below the new theta, and that the source sketch is unmodified
  • serialization equality: compactTrimmed(true, null).toByteArray() is byte-for-byte identical to rebuild().compact(true, null).toByteArray(), and the no-arg form matches the explicit one
  • exact mode converting to estimation, with n still inside the 3-sigma bounds
  • bounds widening for a source already in estimation mode
  • empty and below-k sketches, where trimming is a no-op
  • Alpha rejecting the call while plain compact() still works on it
mvn test
Tests run: 2228, Failures: 0, Errors: 0   BUILD SUCCESS

mvn -Pcheck_java_files test
Tests run: 73, Failures: 0, Errors: 0     BUILD SUCCESS

🤖 Generated with Claude Code

https://claude.ai/code/session_017EDHa7UhfW4eSj5L82panJ

…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
@leerho
leerho requested review from proost and tisonkun September 8, 2026 02:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant