This is an automated email from the ASF dual-hosted git repository.
JingsongLi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/paimon.git
The following commit(s) were added to refs/heads/master by this push:
new bf9333ea6e [core] Stabilize hybrid RRF tie-breaking (#8288)
bf9333ea6e is described below
commit bf9333ea6e652e360314103c05f29c516aab8d92
Author: QuakeWang <[email protected]>
AuthorDate: Fri Jun 19 18:11:07 2026 +0800
[core] Stabilize hybrid RRF tie-breaking (#8288)
Hybrid RRF ranked rows within each route only by score. When multiple
rows had the same route score, their RRF rank depended on the input
bitmap iteration order and Java sort stability, while the final `topK`
already used `score desc, rowId asc`.
This PR makes the route-level RRF ranking use the same deterministic
tie-break: `score desc, rowId asc`. It also adds regression coverage for
tied route scores and verifies the resulting RRF contributions.
---
.../paimon/globalindex/HybridSearchRanker.java | 9 ++++++-
.../paimon/globalindex/HybridSearchRankerTest.java | 31 ++++++++++++++++++++++
2 files changed, 39 insertions(+), 1 deletion(-)
diff --git
a/paimon-common/src/main/java/org/apache/paimon/globalindex/HybridSearchRanker.java
b/paimon-common/src/main/java/org/apache/paimon/globalindex/HybridSearchRanker.java
index 1ab156c6cc..34c8a09c20 100644
---
a/paimon-common/src/main/java/org/apache/paimon/globalindex/HybridSearchRanker.java
+++
b/paimon-common/src/main/java/org/apache/paimon/globalindex/HybridSearchRanker.java
@@ -122,7 +122,14 @@ public class HybridSearchRanker {
}
final ScoreGetter scoreGetter = result.scoreGetter();
rowIds.sort(
- (left, right) -> Float.compare(scoreGetter.score(right),
scoreGetter.score(left)));
+ (left, right) -> {
+ int scoreCompare =
+ Float.compare(scoreGetter.score(right),
scoreGetter.score(left));
+ if (scoreCompare != 0) {
+ return scoreCompare;
+ }
+ return Long.compare(left, right);
+ });
return rowIds;
}
diff --git
a/paimon-common/src/test/java/org/apache/paimon/globalindex/HybridSearchRankerTest.java
b/paimon-common/src/test/java/org/apache/paimon/globalindex/HybridSearchRankerTest.java
index 226be53dd2..02b835134c 100644
---
a/paimon-common/src/test/java/org/apache/paimon/globalindex/HybridSearchRankerTest.java
+++
b/paimon-common/src/test/java/org/apache/paimon/globalindex/HybridSearchRankerTest.java
@@ -25,6 +25,7 @@ import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
+import java.util.Iterator;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
@@ -46,6 +47,20 @@ public class HybridSearchRankerTest {
assertThat(ranked.scoreGetter().score(2L)).isGreaterThan(ranked.scoreGetter().score(1L));
}
+ @Test
+ public void testRrfBreaksRouteScoreTiesByRowId() {
+ ScoredGlobalIndexResult result =
+ result(new long[] {3, 1, 2}, new float[] {1.0f, 1.0f, 1.0f},
new long[] {3, 1, 2});
+
+ ScoredGlobalIndexResult ranked =
+ HybridSearchRanker.rrf(Collections.singletonList(result), new
float[] {1.0f}, 2);
+
+ assertThat(ranked.results()).contains(1L, 2L);
+ assertThat(ranked.results()).doesNotContain(3L);
+ assertThat(ranked.scoreGetter().score(1L)).isCloseTo(1.0f / 61.0f,
within(0.000001f));
+ assertThat(ranked.scoreGetter().score(2L)).isCloseTo(1.0f / 62.0f,
within(0.000001f));
+ }
+
@Test
public void testWeightedScoreUsesAlignedWeightsAfterEmptyRouteIsSkipped() {
ScoredGlobalIndexResult result = result(new long[] {1, 2}, new float[]
{0.3f, 0.2f});
@@ -61,6 +76,22 @@ public class HybridSearchRankerTest {
private ScoredGlobalIndexResult result(long[] rowIds, float[] scores) {
RoaringNavigableMap64 bitmap = new RoaringNavigableMap64();
+ return result(rowIds, scores, bitmap);
+ }
+
+ private ScoredGlobalIndexResult result(long[] rowIds, float[] scores,
long[] iterationOrder) {
+ RoaringNavigableMap64 bitmap =
+ new RoaringNavigableMap64() {
+ @Override
+ public Iterator<Long> iterator() {
+ return
Arrays.stream(iterationOrder).boxed().iterator();
+ }
+ };
+ return result(rowIds, scores, bitmap);
+ }
+
+ private ScoredGlobalIndexResult result(
+ long[] rowIds, float[] scores, RoaringNavigableMap64 bitmap) {
Map<Long, Float> scoreMap = new HashMap<>();
for (int i = 0; i < rowIds.length; i++) {
bitmap.add(rowIds[i]);