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 ad4e32bf66 [global-index] Optimize hybrid ranker top-k (#8348)
ad4e32bf66 is described below

commit ad4e32bf66b0e4e93f78381ef3124130f3ddde98
Author: QuakeWang <[email protected]>
AuthorDate: Thu Jun 25 10:31:06 2026 +0800

    [global-index] Optimize hybrid ranker top-k (#8348)
    
    Java hybrid ranker currently sorts all fused candidates before
    truncating to topK, making the final selection O(n log n). This PR
    replaces that full sort with a bounded heap, reducing it to O(n log
    limit) while preserving the existing score desc / rowId asc tie-breaking
    semantics.
    
    A regression test is added to cover boundary ties, ensuring smaller
    rowId candidates are retained when scores are equal.
---
 .../paimon/globalindex/HybridSearchRanker.java     | 38 +++++++++++++++++-----
 .../paimon/globalindex/HybridSearchRankerTest.java | 20 ++++++++++++
 2 files changed, 49 insertions(+), 9 deletions(-)

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 1b6504c0f9..675e4b1a38 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
@@ -22,9 +22,11 @@ import org.apache.paimon.utils.RoaringNavigableMap64;
 
 import java.io.Serializable;
 import java.util.ArrayList;
+import java.util.Comparator;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.PriorityQueue;
 
 /** Ranker utilities for hybrid search results. */
 public class HybridSearchRanker {
@@ -151,24 +153,42 @@ public class HybridSearchRanker {
     }
 
     private static ScoredGlobalIndexResult topK(Map<Long, Float> scores, int 
limit) {
-        if (scores.isEmpty()) {
+        if (scores.isEmpty() || limit <= 0) {
             return ScoredGlobalIndexResult.createEmpty();
         }
-        List<Map.Entry<Long, Float>> ranked = new 
ArrayList<>(scores.entrySet());
-        ranked.sort(
+        if (scores.size() <= limit) {
+            return toScoredResult(scores.entrySet());
+        }
+
+        // The heap head is the weakest kept row: lowest score, then largest 
rowId.
+        Comparator<Map.Entry<Long, Float>> weakestFirst =
                 (left, right) -> {
-                    int scoreCompare = Float.compare(right.getValue(), 
left.getValue());
+                    int scoreCompare = Float.compare(left.getValue(), 
right.getValue());
                     if (scoreCompare != 0) {
                         return scoreCompare;
                     }
-                    return Long.compare(left.getKey(), right.getKey());
-                });
+                    return Long.compare(right.getKey(), left.getKey());
+                };
+
+        PriorityQueue<Map.Entry<Long, Float>> topEntries =
+                new PriorityQueue<>(limit + 1, weakestFirst);
+        for (Map.Entry<Long, Float> entry : scores.entrySet()) {
+            if (topEntries.size() < limit) {
+                topEntries.offer(entry);
+            } else if (weakestFirst.compare(entry, topEntries.peek()) > 0) {
+                topEntries.poll();
+                topEntries.offer(entry);
+            }
+        }
+
+        return toScoredResult(topEntries);
+    }
 
-        int size = Math.min(limit, ranked.size());
+    private static ScoredGlobalIndexResult toScoredResult(
+            Iterable<Map.Entry<Long, Float>> entries) {
         RoaringNavigableMap64 bitmap = new RoaringNavigableMap64();
         Map<Long, Float> topScores = new HashMap<>();
-        for (int i = 0; i < size; i++) {
-            Map.Entry<Long, Float> entry = ranked.get(i);
+        for (Map.Entry<Long, Float> entry : entries) {
             bitmap.add(entry.getKey());
             topScores.put(entry.getKey(), entry.getValue());
         }
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 0d4b2c0824..0e3ad8cac5 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
@@ -127,6 +127,26 @@ public class HybridSearchRankerTest {
         assertThat(ranked.scoreGetter().score(3L)).isCloseTo(2.0f, 
within(0.000001f));
     }
 
+    @Test
+    public void testWeightedScoreTopKBreaksBoundaryTiesByRowId() {
+        ScoredGlobalIndexResult tied =
+                result(
+                        new long[] {4, 3, 2, 1},
+                        new float[] {5.0f, 5.0f, 5.0f, 5.0f},
+                        new long[] {4, 3, 2, 1});
+
+        ScoredGlobalIndexResult ranked =
+                HybridSearchRanker.weightedScore(
+                        Collections.singletonList(
+                                new HybridSearchRanker.WeightedResult(tied, 
2.0f)),
+                        2);
+
+        assertThat(ranked.results()).contains(1L, 2L);
+        assertThat(ranked.results()).doesNotContain(3L, 4L);
+        assertThat(ranked.scoreGetter().score(1L)).isCloseTo(2.0f, 
within(0.000001f));
+        assertThat(ranked.scoreGetter().score(2L)).isCloseTo(2.0f, 
within(0.000001f));
+    }
+
     @Test
     public void testRejectNonFiniteWeights() {
         ScoredGlobalIndexResult result = result(new long[] {1}, new float[] 
{1.0f});

Reply via email to