From c0b9cfda9fe32b8fe6ef7a5b40c352029a2a9258 Mon Sep 17 00:00:00 2001 From: Priec Date: Thu, 16 Jul 2026 14:49:26 +0200 Subject: [PATCH] picker order by2 --- search/src/lib.rs | 79 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 73 insertions(+), 6 deletions(-) diff --git a/search/src/lib.rs b/search/src/lib.rs index 6307824..200e91c 100644 --- a/search/src/lib.rs +++ b/search/src/lib.rs @@ -25,6 +25,7 @@ const DEFAULT_RESULT_LIMIT: usize = 60; const HARD_RESULT_LIMIT: usize = 200; const DEFAULT_LIST_LIMIT: usize = 5; const SEARCH_SCORE_RELATIVE_FLOOR: f32 = 0.25; +const SEARCH_SCORE_GROUP_WIDTH: f32 = 0.25; pub struct SearcherService { pub pool: PgPool, @@ -624,6 +625,33 @@ fn order_clause(column: &ResolvedOrderColumn, direction: SearchOrderDirection) - } } +fn ranked_order_clause( + column: &ResolvedOrderColumn, + direction: SearchOrderDirection, +) -> String { + let direction = order_direction_sql(direction); + match column { + ResolvedOrderColumn::Position => { + format!("candidate.candidate_group DESC, positioned.picker_position {}", direction) + } + ResolvedOrderColumn::Column(column) => { + let quoted_column = format!("\"{}\"", column.replace('"', "\"\"")); + format!( + "candidate.candidate_group DESC, positioned.{} {} NULLS LAST, positioned.id ASC", + quoted_column, direction + ) + } + } +} + +fn relevance_group(score: f32, best_score: f32) -> i32 { + if best_score <= 0.0 || !best_score.is_finite() || !score.is_finite() { + return 0; + } + + ((score.max(0.0) / best_score / SEARCH_SCORE_GROUP_WIDTH).floor() as i32).clamp(0, 3) +} + async fn fetch_ordered_rows( pool: &PgPool, profile_name: &str, @@ -847,10 +875,15 @@ async fn fetch_ordered_candidate_rows( SELECT t.*, ROW_NUMBER() OVER (ORDER BY id ASC) AS picker_position \ FROM {} t WHERE deleted = FALSE\ ) \ - SELECT id, to_jsonb(positioned) - 'picker_position' AS data, picker_position \ - FROM positioned WHERE id = ANY($1) ORDER BY {} LIMIT $2 OFFSET $3", + SELECT positioned.id, to_jsonb(positioned) - 'picker_position' AS data, \ + picker_position, candidate_score \ + FROM positioned \ + JOIN UNNEST($1::BIGINT[], $2::REAL[], $3::INTEGER[]) \ + AS candidate(candidate_id, candidate_score, candidate_group) \ + ON candidate_id = positioned.id \ + ORDER BY {} LIMIT $4 OFFSET $5", qualify_profile_table(profile_name, table_name), - order_clause(&resolved_order, order.direction), + ranked_order_clause(&resolved_order, order.direction), ); let ids = candidates .iter() @@ -858,10 +891,21 @@ async fn fetch_ordered_candidate_rows( .collect::>(); let scores = candidates .iter() - .map(|(score, id, _)| (*id, *score)) - .collect::>(); + .map(|(score, _, _)| *score) + .collect::>(); + let best_score = scores + .iter() + .copied() + .max_by(f32::total_cmp) + .unwrap_or_default(); + let groups = scores + .iter() + .map(|score| relevance_group(*score, best_score)) + .collect::>(); let rows = sqlx::query(AssertSqlSafe(sql)) .bind(&ids) + .bind(&scores) + .bind(&groups) .bind(limit as i64) .bind(offset as i64) .fetch_all(pool) @@ -874,11 +918,12 @@ async fn fetch_ordered_candidate_rows( let id: i64 = row.try_get("id").unwrap_or_default(); let json_data: serde_json::Value = row.try_get("data").unwrap_or_default(); let position: i64 = row.try_get("picker_position").unwrap_or_default(); + let score: f32 = row.try_get("candidate_score").unwrap_or_default(); let json_data = remap_json_to_display_names(json_data, &physical_to_display); let display_value = row_display_value(&json_data, &display_column); Hit { id, - score: scores.get(&id).copied().unwrap_or_default(), + score, content_json: json_data.to_string(), table_name: table_name.to_string(), row_display_value: display_value, @@ -955,4 +1000,26 @@ mod tests { "\"1\" ASC NULLS LAST, id ASC" ); } + + #[test] + fn ranked_order_preserves_relevance_before_column_order() { + assert_eq!( + ranked_order_clause( + &ResolvedOrderColumn::Column("created_at".to_string()), + SearchOrderDirection::Desc, + ), + "candidate.candidate_group DESC, positioned.\"created_at\" DESC NULLS LAST, positioned.id ASC" + ); + } + + #[test] + fn relevance_groups_nearby_high_scores_and_separates_low_scores() { + let best_score = 8.7; + + assert_eq!(relevance_group(8.7, best_score), 3); + assert_eq!(relevance_group(8.6, best_score), 3); + assert_eq!(relevance_group(8.4, best_score), 3); + assert_eq!(relevance_group(7.0, best_score), 3); + assert_eq!(relevance_group(3.0, best_score), 1); + } }