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]);

Reply via email to