From b3b16ccd4d32da979b6b36cd5670a2e564ec3240 Mon Sep 17 00:00:00 2001 From: JasonWildMe Date: Wed, 15 Apr 2026 22:12:45 -0700 Subject: [PATCH] Fix quicksearch relevance to show exact matches first The quicksearch was returning results in arbitrary order because wildcard queries return constant scores (1.0). When searching "CRC-6", the exact match individual might not appear in the top 10 results. Changes: - Add boosted queries: exact match (100) > prefix match (50) > wildcard (10/5/1) - Add sort=id&sortOrder=asc for deterministic ordering when scores are equal - Use case_insensitive wildcards for ID field (no normalizer in mapping) - Use term/prefix for names field (has normalizer, lowercase works) - Add minimum_should_match: 1 for explicit query behavior Fixes #1541 Co-Authored-By: Claude Opus 4.5 --- .../header/usePostHeaderQuickSearch.test.js | 2 +- .../src/models/usePostHeaderQuickSearch.js | 58 +++++++++++++++++-- 2 files changed, 54 insertions(+), 6 deletions(-) diff --git a/frontend/src/__tests__/components/header/usePostHeaderQuickSearch.test.js b/frontend/src/__tests__/components/header/usePostHeaderQuickSearch.test.js index 9eade9c750..b13c84ebef 100644 --- a/frontend/src/__tests__/components/header/usePostHeaderQuickSearch.test.js +++ b/frontend/src/__tests__/components/header/usePostHeaderQuickSearch.test.js @@ -27,7 +27,7 @@ describe("usePostHeaderQuickSearch", () => { }); expect(axios.post).toHaveBeenCalledWith( - "/api/v3/search/individual?size=10", + "/api/v3/search/individual?size=10&sort=id&sortOrder=asc", expect.any(Object), ); expect(result.current.searchResults).toEqual(mockData.data.hits); diff --git a/frontend/src/models/usePostHeaderQuickSearch.js b/frontend/src/models/usePostHeaderQuickSearch.js index cb5c697c98..037ef45199 100644 --- a/frontend/src/models/usePostHeaderQuickSearch.js +++ b/frontend/src/models/usePostHeaderQuickSearch.js @@ -14,32 +14,80 @@ export default function usePostHeaderQuickSearch(value) { const delayDebounce = setTimeout(() => { setLoading(true); + const searchValue = value.trim(); + const searchValueLower = searchValue.toLowerCase(); axios - .post("/api/v3/search/individual?size=10", { + .post("/api/v3/search/individual?size=10&sort=id&sortOrder=asc", { query: { bool: { + minimum_should_match: 1, should: [ + // Exact match on ID - highest priority (case-insensitive via wildcard) + // Note: id field is keyword without normalizer, so we use wildcard + // with no wildcards for case-insensitive exact match { wildcard: { - names: { - value: `*${value}*`, + id: { + value: searchValue, case_insensitive: true, + boost: 100, + }, + }, + }, + // Prefix match on ID - high priority + { + wildcard: { + id: { + value: `${searchValue}*`, + case_insensitive: true, + boost: 50, + }, + }, + }, + // Exact match on names - high priority + // names field has normalizer, so term query with lowercase works + { + term: { + names: { + value: searchValueLower, + boost: 80, }, }, }, + // Prefix match on names - medium priority + { + prefix: { + names: { + value: searchValueLower, + boost: 40, + }, + }, + }, + // Wildcard fallback for partial matches - lower priority { wildcard: { id: { - value: `*${value}*`, + value: `*${searchValue}*`, + case_insensitive: true, + boost: 10, + }, + }, + }, + { + wildcard: { + names: { + value: `*${searchValue}*`, case_insensitive: true, + boost: 5, }, }, }, { wildcard: { encounterIds: { - value: `*${value}*`, + value: `*${searchValue}*`, case_insensitive: true, + boost: 1, }, }, },