From 0ebe894806c9dc90e7ce85ec188be41089e90217 Mon Sep 17 00:00:00 2001 From: Max Freedom Pollard <272618364+MaxFreedomPollard@users.noreply.github.com> Date: Thu, 22 Oct 2026 15:16:58 -0700 Subject: [PATCH] Theta: clamp the union size in JaccardSimilarity.exactlyEqual exactlyEqual() sized its union with ceilingPowerOf2(countA + countB) and no bounds, so ThetaSetOperationBuilder.setNominalEntries() threw SketchesArgumentException whenever the two retained counts summed to 8 or less, for example two sketches holding one item each, or to more than 2^26. Clamp the union size to [2^4, 2^26], as jaccard() has done since #153 and as the C++ compute_union() does for both jaccard() and exactly_equal(). Equal sketches that retain more than 2^26 entries, which needs 2^26 nominal entries, now compare as not equal instead of throwing; the javadoc notes this. Co-Authored-By: Claude Opus 5.5 --- .../apache/datasketches/theta/JaccardSimilarity.java | 9 ++++++++- .../datasketches/theta/JaccardSimilarityTest.java | 11 +++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/apache/datasketches/theta/JaccardSimilarity.java b/src/main/java/org/apache/datasketches/theta/JaccardSimilarity.java index 2fc9d4fe5..7f967198d 100644 --- a/src/main/java/org/apache/datasketches/theta/JaccardSimilarity.java +++ b/src/main/java/org/apache/datasketches/theta/JaccardSimilarity.java @@ -100,6 +100,10 @@ public static double[] jaccard(final ThetaSketch sketchA, final ThetaSketch sket /** * Returns true if the two given sketches have exactly the same hash values and the same * theta values. Thus, they are equivalent. + * + *

Note: For very large pairs of sketches, where the configured nominal entries of the sketches + * are 2^26, this method may return false even if the two sketches are equal. + * * @param sketchA the given sketch A * @param sketchB the given sketch B * @return true if the two given sketches have exactly the same hash values and the same @@ -116,8 +120,11 @@ public static boolean exactlyEqual(final ThetaSketch sketchA, final ThetaSketch final int countB = sketchB.getRetainedEntries(true); //Create the ThetaUnion + final int minK = 1 << ThetaUtil.MIN_LG_NOM_LONGS; + final int maxK = 1 << ThetaUtil.MAX_LG_NOM_LONGS; + final int newK = max(min(ceilingPowerOf2(countA + countB), maxK), minK); final ThetaUnion union = - ThetaSetOperation.builder().setNominalEntries(ceilingPowerOf2(countA + countB)).buildUnion(); + ThetaSetOperation.builder().setNominalEntries(newK).buildUnion(); union.union(sketchA); union.union(sketchB); final ThetaSketch unionAB = union.getResult(); diff --git a/src/test/java/org/apache/datasketches/theta/JaccardSimilarityTest.java b/src/test/java/org/apache/datasketches/theta/JaccardSimilarityTest.java index 4f84c5382..17cadcc92 100644 --- a/src/test/java/org/apache/datasketches/theta/JaccardSimilarityTest.java +++ b/src/test/java/org/apache/datasketches/theta/JaccardSimilarityTest.java @@ -233,6 +233,17 @@ public void checkMinK() { println(result[0] + ", " + result[1] + ", " + result[2]); } + @Test + public void checkMinKExactlyEqual() { + UpdatableThetaSketch skA = UpdatableThetaSketch.builder().build(); //4096 + UpdatableThetaSketch skB = UpdatableThetaSketch.builder().build(); //4096 + skA.update(1); + skB.update(1); + assertTrue(exactlyEqual(skA, skB)); + skB.update(2); + assertFalse(exactlyEqual(skA, skB)); + } + @Test public void printlnTest() { println("PRINTING: "+this.getClass().getName());