Rank search results instead of taking the first eight
Search scanned the index in construction order — 8750 stars, then 18 bodies, then 6319 exoplanets — collected substring matches, and stopped at eight. With no scoring, position in the index decided everything. Typing "Io" returned eight stars named "Iot Cas", "Iot Eri" and so on, and never reached the moon Io — despite Io being an *exact* match, because the scan had already filled up 8750 entries before the bodies begin. "Kepler" returned Kepler-939 b, Kepler-1292 b, Kepler-223 d and five more in whatever order the archive happened to list them. Everything is now scored before anything is taken, so where an entry sits in the index cannot hide a better match. Exact beats prefix beats word-start beats substring, with the gaps wide enough that a worse kind of match can never outrank a better one. Ties break on kind — the eighteen solar-system bodies first, then stars, then exoplanets, so "Proxima" offers the star before its own planets — then on name length, then alphabetically, so the order is fully determined rather than inherited from the input. Matching is punctuation insensitive as well as case insensitive, so "gl357" finds "Gl 357". "Io" now returns the moon first. "Kepler" returns Kepler-4 b through Kepler-9 d. The index is pre-normalised once on load rather than per keystroke. Lowercasing, stripping punctuation and splitting 15,000 names on every character typed costs about 11 ms, which is most of a frame, and search shares a thread with the render loop — so it stuttered the scene while typing. Precomputing takes a broad query down to 2.5 ms and a narrow one to 0.5 ms, for one 15 ms build during the existing data load. Adds the first tests this component has had, alongside the ranking's own. Tests: 237 passing, up from 206. Verified in a real browser. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaySiNst4HhDXBHnMy8p5G
This commit is contained in:
@@ -3,18 +3,7 @@ import { Router } from '@angular/router';
|
||||
|
||||
import { DataLoaderService } from '../../core/data/data-loader.service';
|
||||
import { NavigationStore } from '../../shared/state/navigation.store';
|
||||
|
||||
type SearchResultKind = 'star' | 'body' | 'exoplanet';
|
||||
|
||||
interface SearchEntry {
|
||||
kind: SearchResultKind;
|
||||
name: string;
|
||||
subtitle: string;
|
||||
/** HYG star id, for `kind: 'star'` results. */
|
||||
starId?: number;
|
||||
/** `bodies.json`/`exoplanets.json` id, for `kind: 'body' | 'exoplanet'` results. */
|
||||
bodyId?: string;
|
||||
}
|
||||
import { buildSearchIndex, IndexedSearchEntry, rankSearchResults, SearchEntry, SearchResultKind } from './search-ranking';
|
||||
|
||||
const MAX_RESULTS = 8;
|
||||
const MIN_QUERY_LENGTH = 2;
|
||||
@@ -70,23 +59,15 @@ const KIND_LABELS: Record<SearchResultKind, string> = {
|
||||
})
|
||||
export class SearchComponent {
|
||||
readonly query = signal('');
|
||||
private readonly index = signal<SearchEntry[]>([]);
|
||||
/** Pre-normalised once on load; re-deriving it per keystroke would stutter the render loop. */
|
||||
private readonly index = signal<IndexedSearchEntry[]>([]);
|
||||
|
||||
readonly results = computed(() => {
|
||||
const query = this.query().trim().toLowerCase();
|
||||
const query = this.query().trim();
|
||||
if (query.length < MIN_QUERY_LENGTH) {
|
||||
return [];
|
||||
}
|
||||
const matches: SearchEntry[] = [];
|
||||
for (const entry of this.index()) {
|
||||
if (entry.name.toLowerCase().includes(query)) {
|
||||
matches.push(entry);
|
||||
if (matches.length >= MAX_RESULTS) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return matches;
|
||||
return rankSearchResults(this.index(), query, MAX_RESULTS);
|
||||
});
|
||||
|
||||
constructor(
|
||||
@@ -133,7 +114,7 @@ export class SearchComponent {
|
||||
...exoplanets.map((exoplanet): SearchEntry => ({ kind: 'exoplanet', name: exoplanet.name, subtitle: exoplanet.hostStarName, bodyId: exoplanet.id }))
|
||||
];
|
||||
|
||||
this.index.set(entries);
|
||||
this.index.set(buildSearchIndex(entries));
|
||||
} catch (error) {
|
||||
console.error('Failed to build the search index.', error);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user