Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion crates/buzz-search/src/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,14 @@ pub async fn search(pool: &PgPool, query: &SearchQuery) -> Result<SearchResult,
};
let page = query.page.clamp(1, PAGE_MAX);
let offset = ((page - 1) as i64) * (per_page_actual as i64);
// Profile typeahead uses broad prefix matching. For one- and two-character
// queries, a busy community can have enough newer prefix matches to push a
// short exact display name off the bounded first page. Keep the same result
// set and pagination contract, but put rows containing the whole lexeme
// first for this one narrow caller shape.
let prioritize_exact_profile_lexeme = query.mode == SearchMode::Prefix
&& query.kinds.as_deref() == Some(&[0][..])
&& search_text.chars().count() <= 2;

let mut qb: QueryBuilder<sqlx::Postgres> = QueryBuilder::new(
"SELECT id, kind, pubkey, channel_id, \
Expand Down Expand Up @@ -292,7 +300,13 @@ pub async fn search(pool: &PgPool, query: &SearchQuery) -> Result<SearchResult,
qb.push(")");
}

qb.push(" ORDER BY rank DESC, created_at DESC, id LIMIT ");
if prioritize_exact_profile_lexeme {
qb.push(" ORDER BY search_tsv @@ websearch_to_tsquery('simple', ");
qb.push_bind(&search_text);
qb.push(") DESC, rank DESC, created_at DESC, id LIMIT ");
} else {
qb.push(" ORDER BY rank DESC, created_at DESC, id LIMIT ");
}
qb.push_bind(per_page_actual as i64);
qb.push(" OFFSET ");
qb.push_bind(offset);
Expand Down
61 changes: 61 additions & 0 deletions crates/buzz-search/tests/fts_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,67 @@ async fn kind0_search_by_display_name_works_without_flattening() {
teardown(pool, &schema).await;
}

#[tokio::test]
#[ignore = "requires Postgres"]
async fn short_kind0_prefix_prioritizes_exact_lexeme_on_a_noisy_page() {
let (pool, schema) = setup().await;

let c = mk_community(&pool, "short-profile-prefix.example").await;
let exact_id = rand_bytes32();
insert_event(
&pool,
c,
exact_id,
rand_bytes32(),
0,
r#"{"display_name":"jm"}"#,
None,
1_700_000_000,
)
.await;

// These profiles all match jm:* and are newer than the exact name. Without
// exact-lexeme priority they consume the entire bounded first page.
for (i, display_name) in ["jma", "jmbravo", "jmcharlie", "jmdelta"]
.iter()
.enumerate()
{
insert_event(
&pool,
c,
rand_bytes32(),
rand_bytes32(),
0,
&format!(r#"{{"display_name":"{display_name}"}}"#),
None,
1_700_000_100 + i as i64,
)
.await;
}

let svc = SearchService::new(pool.clone());
let first_page = svc
.search(&SearchQuery {
community: c,
q: "jm".into(),
channel_scope: ChannelScope::Any,
kinds: Some(vec![0]),
authors: None,
since: None,
until: None,
page: 1,
per_page: 3,
mode: buzz_search::SearchMode::Prefix,
})
.await
.expect("short profile prefix search ok");

assert_eq!(first_page.hits.len(), 3);
assert_eq!(first_page.hits[0].event_id, exact_id);

teardown(pool, &schema).await;
}

#[tokio::test]
#[ignore = "requires Postgres"]
async fn prefix_mode_matches_final_token_prefix_without_changing_full_text() {
Expand Down
Loading