fix(desktop): match people search on profile name

People search re-ranked relay results against the display name only, so
a person whose display name differs from their name (e.g. "S" vs
"Starfish Sol") was dropped even though the relay matched them.
Score `name` alongside `display_name` in both the Rust and TS rankers.

Co-authored-by: Otter-Opus <72c1a2fb33d700987f1e778749662d6fc95cf78e979f43e5039baa76dc2588aa@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Diem Nguyen <diem@squareup.com>
Signed-off-by: Fizz <6be7be23ec5bba868f3e1db5ec86013d76bd1a293dce5b435a192158fec13641@buzz.block.builderlab.xyz>
This commit is contained in:
Diem Nguyen
2026-10-16 13:03:07 -07:00
committed by Fizz
parent 60de1db0b8
commit 879d59f093
7 changed files with 33 additions and 3 deletions

View File

@@ -66,6 +66,8 @@ pub struct UsersBatchResponse {
pub struct UserSearchResultInfo {
pub pubkey: String,
pub display_name: Option<String>,
#[serde(default)]
pub name: Option<String>,
pub avatar_url: Option<String>,
pub nip05_handle: Option<String>,
pub owner_pubkey: Option<String>,

View File

@@ -18,6 +18,7 @@ pub fn user_search_result_from_event(ev: &Event) -> UserSearchResultInfo {
.and_then(Value::as_str)
.or_else(|| v.get("name").and_then(Value::as_str))
.map(str::to_string),
name: v.get("name").and_then(Value::as_str).map(str::to_string),
avatar_url: v.get("picture").and_then(Value::as_str).map(str::to_string),
nip05_handle: v.get("nip05").and_then(Value::as_str).map(str::to_string),
is_agent: owner_pubkey.is_some(),
@@ -126,10 +127,11 @@ pub fn rank_user_search_results(
let info = user_search_result_from_event(ev);
let display = info.display_name.as_deref().unwrap_or("").to_lowercase();
let name = info.name.as_deref().unwrap_or("").to_lowercase();
let nip05 = info.nip05_handle.as_deref().unwrap_or("").to_lowercase();
let pubkey = info.pubkey.to_lowercase();
let score = match_score(&q, &display, &nip05, &pubkey);
let score = match_score(&q, &display, &name, &nip05, &pubkey);
if score == 0 {
continue;
}
@@ -156,7 +158,7 @@ pub fn rank_user_search_results(
}
}
fn match_score(q: &str, display_name: &str, nip05: &str, pubkey_hex: &str) -> u32 {
fn match_score(q: &str, display_name: &str, name: &str, nip05: &str, pubkey_hex: &str) -> u32 {
const DISPLAY_EXACT: u32 = 1000;
const DISPLAY_PREFIX: u32 = 900;
const DISPLAY_CONTAINS: u32 = 800;
@@ -184,7 +186,13 @@ fn match_score(q: &str, display_name: &str, nip05: &str, pubkey_hex: &str) -> u3
DISPLAY_EXACT,
DISPLAY_PREFIX,
DISPLAY_CONTAINS,
);
)
.max(score_field(
name,
DISPLAY_EXACT,
DISPLAY_PREFIX,
DISPLAY_CONTAINS,
));
let nip05_score = score_field(nip05, NIP05_EXACT, NIP05_PREFIX, NIP05_CONTAINS);
let pubkey_score = if !pubkey_hex.is_empty() && pubkey_hex.starts_with(q) {
PUBKEY_PREFIX
@@ -406,4 +414,13 @@ mod tests {
assert_eq!(r.users.len(), 1);
assert_eq!(r.users[0].display_name.as_deref(), Some("alice"));
}
#[test]
fn rank_matches_name_when_display_name_differs() {
let e = ev(0, r#"{"name":"Starfish Sol","display_name":"S"}"#, vec![]);
let r = rank_user_search_results(&[e], "starfish", 10);
assert_eq!(r.users.len(), 1);
assert_eq!(r.users[0].display_name.as_deref(), Some("S"));
assert_eq!(r.users[0].name.as_deref(), Some("Starfish Sol"));
}
}

View File

@@ -309,6 +309,7 @@ export function MembersSidebar({
: current.isAgent
? currentName
: (currentName ?? candidateName),
name: current.name ?? candidate.name ?? null,
nip05Handle: current.nip05Handle ?? candidate.nip05Handle ?? null,
ownerPubkey: current.ownerPubkey ?? candidate.ownerPubkey ?? null,
isAgent: current.isAgent || candidate.isAgent,

View File

@@ -47,6 +47,13 @@ test("scoreUserCandidate ranks display labels before pubkeys", () => {
);
});
test("scoreUserCandidate matches a profile name that differs from the display name", () => {
const user = makeUser({ displayName: "S", name: "Starfish Sol" });
assert.equal(scoreUserCandidate({ label: "S", query: "starfish", user }), 0);
assert.equal(scoreUserCandidate({ label: "S", query: "sol", user }), 1);
});
test("scoreUserCandidate supports agent labels and empty-query defaults", () => {
const agent = makeUser({ isAgent: true });

View File

@@ -37,6 +37,7 @@ export function scoreUserCandidate({
const labels = [
label,
user.name?.trim() ?? "",
user.nip05Handle?.trim() ?? "",
user.isAgent ? "agent" : "",
];

View File

@@ -64,6 +64,7 @@ function fromRawUserSearchResult(user: RawUserSearchResult): UserSearchResult {
return {
pubkey: user.pubkey,
displayName: user.display_name,
name: user.name ?? null,
avatarUrl: user.avatar_url,
nip05Handle: user.nip05_handle,
ownerPubkey: user.owner_pubkey,

View File

@@ -126,6 +126,7 @@ export type UsersBatchResponse = {
export type UserSearchResult = {
pubkey: string;
displayName: string | null;
name?: string | null;
avatarUrl: string | null;
nip05Handle: string | null;
ownerPubkey: string | null;