Skip to content

Commit 3cb51e8

Browse files
fix(index): sort search top-k results by relevance when not truncating (#614)
1 parent 27dea79 commit 3cb51e8

2 files changed

Lines changed: 43 additions & 18 deletions

File tree

crates/paimon/src/full_text.rs

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -126,14 +126,16 @@ impl SearchResult {
126126

127127
/// Return top-k results by score (descending).
128128
pub fn top_k(&self, k: usize) -> Self {
129-
if self.row_ids.len() <= k {
130-
return self.clone();
131-
}
129+
// Always sort best-first, even when no truncation is needed (len <= k):
130+
// downstream consumers rely on relevance order, not input/shard order.
132131
let mut indices: Vec<usize> = (0..self.row_ids.len()).collect();
132+
// Best-first by score (total_cmp is NaN-safe), then smaller row id. This
133+
// matches vector_search's top_k and keeps the order deterministic regardless
134+
// of the input/shard order.
133135
indices.sort_by(|&a, &b| {
134136
self.scores[b]
135-
.partial_cmp(&self.scores[a])
136-
.unwrap_or(std::cmp::Ordering::Equal)
137+
.total_cmp(&self.scores[a])
138+
.then_with(|| self.row_ids[a].cmp(&self.row_ids[b]))
137139
});
138140
indices.truncate(k);
139141
let row_ids = indices.iter().map(|&i| self.row_ids[i]).collect();
@@ -258,8 +260,30 @@ mod tests {
258260
.without_deleted_row_ranges(Some(&deleted))
259261
.unwrap()
260262
.top_k(10);
261-
assert_eq!(filtered.row_ids, vec![1, 4]);
262-
assert_eq!(filtered.scores, vec![0.1, 0.2]);
263+
// Rows 2..3 are deleted; the remaining rows come back best-first by score.
264+
assert_eq!(filtered.row_ids, vec![4, 1]);
265+
assert_eq!(filtered.scores, vec![0.2, 0.1]);
266+
}
267+
268+
#[test]
269+
fn test_search_result_top_k_sorts_best_first_without_truncation() {
270+
// k >= candidate count: results must still be best-first by score, not in
271+
// input/shard order.
272+
let result = SearchResult::new(vec![3, 1, 2], vec![0.1, 0.9, 0.5]);
273+
274+
let top = result.top_k(3);
275+
assert_eq!(top.row_ids, vec![1, 2, 3]);
276+
assert_eq!(top.scores, vec![0.9, 0.5, 0.1]);
277+
}
278+
279+
#[test]
280+
fn test_search_result_top_k_tie_breaks_by_smaller_row_id() {
281+
// Equal scores: order by smaller row id, independent of input order.
282+
let result = SearchResult::new(vec![30, 10, 20], vec![0.9, 0.9, 0.9]);
283+
284+
let top = result.top_k(3);
285+
assert_eq!(top.row_ids, vec![10, 20, 30]);
286+
assert_eq!(top.scores, vec![0.9, 0.9, 0.9]);
263287
}
264288

265289
#[test]

crates/paimon/src/vector_search.rs

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -218,12 +218,10 @@ impl SearchResult {
218218
}
219219

220220
if best_by_row_id.len() <= k {
221-
// Keep the original row order when no truncation is needed.
222-
let rows = self
223-
.row_ids
224-
.iter()
225-
.filter_map(|row_id| best_by_row_id.remove(row_id))
226-
.collect();
221+
// Still sort best-first: the scored map is unordered, and consumers rely
222+
// on relevance rank.
223+
let mut rows: Vec<ScoredRow> = best_by_row_id.into_values().collect();
224+
sort_scored_rows_by_rank(&mut rows);
227225
return Self::from_scored_rows(rows);
228226
}
229227

@@ -382,12 +380,14 @@ mod tests {
382380
}
383381

384382
#[test]
385-
fn test_search_result_top_k_preserves_order_without_truncation() {
383+
fn test_search_result_top_k_sorts_best_first_without_truncation() {
384+
// Even when k >= candidate count (no truncation), results must be returned
385+
// best-first by score, not in the input/insertion order.
386386
let result = SearchResult::new(vec![3, 1, 2], vec![0.1, 0.9, 0.5]);
387387

388388
let top = result.top_k(3);
389-
assert_eq!(top.row_ids, result.row_ids);
390-
assert_eq!(top.scores, result.scores);
389+
assert_eq!(top.row_ids, vec![1, 2, 3]);
390+
assert_eq!(top.scores, vec![0.9, 0.5, 0.1]);
391391
}
392392

393393
#[test]
@@ -409,8 +409,9 @@ mod tests {
409409
.without_deleted_row_ranges(Some(&deleted))
410410
.unwrap()
411411
.top_k(10);
412-
assert_eq!(filtered.row_ids, vec![1, 4]);
413-
assert_eq!(filtered.scores, vec![0.1, 0.2]);
412+
// Rows 2..3 are deleted; the remaining rows come back best-first by score.
413+
assert_eq!(filtered.row_ids, vec![4, 1]);
414+
assert_eq!(filtered.scores, vec![0.2, 0.1]);
414415
}
415416

416417
#[test]

0 commit comments

Comments
 (0)