picker order by2

This commit is contained in:
Priec
2026-07-16 14:49:26 +02:00
parent f13db931a4
commit c0b9cfda9f

View File

@@ -25,6 +25,7 @@ const DEFAULT_RESULT_LIMIT: usize = 60;
const HARD_RESULT_LIMIT: usize = 200; const HARD_RESULT_LIMIT: usize = 200;
const DEFAULT_LIST_LIMIT: usize = 5; const DEFAULT_LIST_LIMIT: usize = 5;
const SEARCH_SCORE_RELATIVE_FLOOR: f32 = 0.25; const SEARCH_SCORE_RELATIVE_FLOOR: f32 = 0.25;
const SEARCH_SCORE_GROUP_WIDTH: f32 = 0.25;
pub struct SearcherService { pub struct SearcherService {
pub pool: PgPool, 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( async fn fetch_ordered_rows(
pool: &PgPool, pool: &PgPool,
profile_name: &str, 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 \ SELECT t.*, ROW_NUMBER() OVER (ORDER BY id ASC) AS picker_position \
FROM {} t WHERE deleted = FALSE\ FROM {} t WHERE deleted = FALSE\
) \ ) \
SELECT id, to_jsonb(positioned) - 'picker_position' AS data, picker_position \ SELECT positioned.id, to_jsonb(positioned) - 'picker_position' AS data, \
FROM positioned WHERE id = ANY($1) ORDER BY {} LIMIT $2 OFFSET $3", 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), qualify_profile_table(profile_name, table_name),
order_clause(&resolved_order, order.direction), ranked_order_clause(&resolved_order, order.direction),
); );
let ids = candidates let ids = candidates
.iter() .iter()
@@ -858,10 +891,21 @@ async fn fetch_ordered_candidate_rows(
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let scores = candidates let scores = candidates
.iter() .iter()
.map(|(score, id, _)| (*id, *score)) .map(|(score, _, _)| *score)
.collect::<HashMap<_, _>>(); .collect::<Vec<_>>();
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::<Vec<_>>();
let rows = sqlx::query(AssertSqlSafe(sql)) let rows = sqlx::query(AssertSqlSafe(sql))
.bind(&ids) .bind(&ids)
.bind(&scores)
.bind(&groups)
.bind(limit as i64) .bind(limit as i64)
.bind(offset as i64) .bind(offset as i64)
.fetch_all(pool) .fetch_all(pool)
@@ -874,11 +918,12 @@ async fn fetch_ordered_candidate_rows(
let id: i64 = row.try_get("id").unwrap_or_default(); let id: i64 = row.try_get("id").unwrap_or_default();
let json_data: serde_json::Value = row.try_get("data").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 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 json_data = remap_json_to_display_names(json_data, &physical_to_display);
let display_value = row_display_value(&json_data, &display_column); let display_value = row_display_value(&json_data, &display_column);
Hit { Hit {
id, id,
score: scores.get(&id).copied().unwrap_or_default(), score,
content_json: json_data.to_string(), content_json: json_data.to_string(),
table_name: table_name.to_string(), table_name: table_name.to_string(),
row_display_value: display_value, row_display_value: display_value,
@@ -955,4 +1000,26 @@ mod tests {
"\"1\" ASC NULLS LAST, id ASC" "\"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);
}
} }