This is an automated email from the ASF dual-hosted git repository.

JingsongLi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/paimon-rust.git


The following commit(s) were added to refs/heads/main by this push:
     new 16db7a54 fix(index): deduplicate vector search top-k results (#541)
16db7a54 is described below

commit 16db7a5404b5aaf615bba509ef4526de4780bea3
Author: QuakeWang <[email protected]>
AuthorDate: Mon Jul 20 11:07:15 2026 +0800

    fix(index): deduplicate vector search top-k results (#541)
---
 crates/paimon/src/vector_search.rs | 59 +++++++++++++++++++++++++++++++++++---
 1 file changed, 55 insertions(+), 4 deletions(-)

diff --git a/crates/paimon/src/vector_search.rs 
b/crates/paimon/src/vector_search.rs
index 2070d5e9..53205cbf 100644
--- a/crates/paimon/src/vector_search.rs
+++ b/crates/paimon/src/vector_search.rs
@@ -203,13 +203,32 @@ impl SearchResult {
         if k == 0 {
             return Self::empty();
         }
-        if self.row_ids.len() <= k {
-            return self.clone();
-        }
 
-        let mut heap = BinaryHeap::with_capacity(k + 1);
+        let mut best_by_row_id = HashMap::with_capacity(self.row_ids.len());
         for (&row_id, &score) in self.row_ids.iter().zip(&self.scores) {
             let entry = ScoredRow { row_id, score };
+            best_by_row_id
+                .entry(row_id)
+                .and_modify(|best| {
+                    if entry.is_stronger_than(best) {
+                        *best = entry;
+                    }
+                })
+                .or_insert(entry);
+        }
+
+        if best_by_row_id.len() <= k {
+            // Keep the original row order when no truncation is needed.
+            let rows = self
+                .row_ids
+                .iter()
+                .filter_map(|row_id| best_by_row_id.remove(row_id))
+                .collect();
+            return Self::from_scored_rows(rows);
+        }
+
+        let mut heap = BinaryHeap::with_capacity(k + 1);
+        for entry in best_by_row_id.into_values() {
             if heap.len() < k {
                 heap.push(entry);
             } else if heap
@@ -339,6 +358,38 @@ mod tests {
         assert!(top.row_ids.contains(&4));
     }
 
+    #[test]
+    fn test_search_result_top_k_deduplicates_overlapping_rows() {
+        let indexed = SearchResult::new(vec![1], vec![0.9]);
+        let fallback = SearchResult::new(vec![1, 2], vec![0.8, 0.7]);
+
+        let merged = indexed.or(&fallback);
+        assert_eq!(merged.row_ids, vec![1, 1, 2]);
+        assert_eq!(merged.scores, vec![0.9, 0.8, 0.7]);
+
+        let top = merged.top_k(2);
+        assert_eq!(top.row_ids, vec![1, 2]);
+        assert_eq!(top.scores, vec![0.9, 0.7]);
+    }
+
+    #[test]
+    fn test_search_result_top_k_keeps_highest_duplicate_score() {
+        let result = SearchResult::new(vec![1, 2, 1, 3], vec![0.5, 0.8, 0.9, 
0.7]);
+
+        let top = result.top_k(2);
+        assert_eq!(top.row_ids, vec![1, 2]);
+        assert_eq!(top.scores, vec![0.9, 0.8]);
+    }
+
+    #[test]
+    fn test_search_result_top_k_preserves_order_without_truncation() {
+        let result = SearchResult::new(vec![3, 1, 2], vec![0.1, 0.9, 0.5]);
+
+        let top = result.top_k(3);
+        assert_eq!(top.row_ids, result.row_ids);
+        assert_eq!(top.scores, result.scores);
+    }
+
     #[test]
     fn test_search_result_top_k_tie_breaks_by_smaller_row_id() {
         let result = SearchResult::new(vec![30, 10, 20], vec![0.9, 0.9, 0.9]);

Reply via email to