feat: instant local search results while full pipeline runs

Added SearchLocal() — queries only the FTS5 index with no network
calls. The frontend now calls SearchLocal first, renders those
results immediately (clearing the loading spinner), then fires the
full Search() pipeline in the background. When full results arrive,
they replace the local hits seamlessly.

For indexed queries this means sub-100ms first results regardless
of how slow MB/LB are. Unindexed queries still show the loading
spinner until the full pipeline completes.
This commit is contained in:
2026-03-29 16:45:25 -04:00
parent a93a83c4d2
commit 4138fe593d
4 changed files with 47 additions and 1 deletions
+17
View File
@@ -112,6 +112,23 @@ func (e *Service) SearchRecordings(query string) ([]MBRecording, error) {
return e.mb.SearchRecordings(e.ctx, query, mbSearchLimit)
}
// SearchLocal queries only the local FTS5 index and returns results
// instantly with no network calls. Returns nil if the index isn't
// ready. The frontend calls this in parallel with Search() to show
// instant results while the full pipeline runs.
func (e *Service) SearchLocal(query string) *MBSearchResult {
indexHits := e.index.Search(query, 30) //nolint:mnd
if len(indexHits) == 0 {
return nil
}
var result MBSearchResult
mergeIndexHits(&result, indexHits)
filterAndCap(&result)
return &result
}
// ---------------------------------------------------------------------------
// MusicBrainz lookup
// ---------------------------------------------------------------------------
@@ -1,7 +1,7 @@
import { LitElement, html, css, nothing } from 'lit';
import { customElement, state, query as litQuery } from 'lit/decorators.js';
import { designTokens } from '../../styles/tokens.css';
import { Search, GetThumbnails, GetArtistImageURL, CheckLibraryMBIDs } from '@go/explore/Service';
import { Search, SearchLocal, GetThumbnails, GetArtistImageURL, CheckLibraryMBIDs } from '@go/explore/Service';
import type { ThumbnailRequest } from '@go/explore/Service';
import type {
MBSearchResult,
@@ -520,6 +520,29 @@ export class ExploreView extends LitElement {
const startTime = performance.now();
console.log(`[explore] search started: "${query}"`);
// Phase 1: show local index hits instantly (no network).
try {
const local = await SearchLocal(query);
if (version !== this.searchVersion) return;
if (local && (local.artists?.length || local.releaseGroups?.length || local.recordings?.length)) {
this.results = local;
this.loading = false;
this.loadThumbnails();
this.loadArtistImages();
this.checkLibrary();
const elapsed = (performance.now() - startTime).toFixed(0);
console.log(
`[explore] local results: "${query}" in ${elapsed}ms — ` +
`artists=${local.artists?.length ?? 0}, ` +
`albums=${local.releaseGroups?.length ?? 0}, ` +
`tracks=${local.recordings?.length ?? 0}`,
);
}
} catch {
// Local search failed — continue to full search.
}
// Phase 2: full pipeline (MB + LB + reranking).
try {
const result = await Search(query);
+2
View File
@@ -35,6 +35,8 @@ export function Search(arg1:string):Promise<explore.MBSearchResult>;
export function SearchArtists(arg1:string):Promise<Array<explore.MBArtist>>;
export function SearchLocal(arg1:string):Promise<explore.MBSearchResult>;
export function SearchRecordings(arg1:string):Promise<Array<explore.MBRecording>>;
export function SearchReleaseGroups(arg1:string):Promise<Array<explore.MBReleaseGroup>>;
+4
View File
@@ -66,6 +66,10 @@ export function SearchArtists(arg1) {
return window['go']['explore']['Service']['SearchArtists'](arg1);
}
export function SearchLocal(arg1) {
return window['go']['explore']['Service']['SearchLocal'](arg1);
}
export function SearchRecordings(arg1) {
return window['go']['explore']['Service']['SearchRecordings'](arg1);
}