feat: ranked library search with match-quality tiers
Library cache search was returning results in alphabetical order with no ranking. 'massive' showed Blanck Mass before Massive Attack because B comes before M. Now all matches are collected, scored by match quality, and sorted: Artists: exact match = 100, starts-with = 90, substring = 70, fuzzy = 50 Albums (same tiers as remote rgMatchTier): artist-exact = 100, artist-contains = 85, title-exact = 80, title-starts-with = 75, title-contains = 60, fuzzy = 40 Results are sorted by score descending, then alphabetically as tiebreaker. Cap increased from 5 to 10 per entity type to show more library content.
This commit is contained in:
@@ -637,47 +637,89 @@ export class ExploreView extends LitElement {
|
||||
private searchLibraryCache(query: string): MBSearchResult | null {
|
||||
const q = query.toLowerCase();
|
||||
|
||||
const artists: MBArtist[] = [];
|
||||
// Collect all matching artists with match-quality scores.
|
||||
const artistMatches: Array<{ artist: any; score: number }> = [];
|
||||
const cachedArtists = libraryStore.cachedArtists;
|
||||
if (cachedArtists) {
|
||||
for (const a of cachedArtists) {
|
||||
if (fuzzyMatch(q, a.Name.toLowerCase())) {
|
||||
artists.push({
|
||||
mbid: a.MBID || '',
|
||||
name: a.Name,
|
||||
sortName: '',
|
||||
type: '',
|
||||
country: '',
|
||||
disambiguation: '',
|
||||
score: 100,
|
||||
_imageSmall: a.ImageSmall || '',
|
||||
_imageMedium: a.ImageMedium || '',
|
||||
_inLibrary: true,
|
||||
} as MBArtist & { _imageSmall: string; _imageMedium: string; _inLibrary: boolean });
|
||||
if (artists.length >= 5) break;
|
||||
const name = a.Name.toLowerCase();
|
||||
if (!fuzzyMatch(q, name)) continue;
|
||||
|
||||
// Score by match quality (same tiers as remote search).
|
||||
let score: number;
|
||||
if (name === q) {
|
||||
score = 100; // exact
|
||||
} else if (name.startsWith(q)) {
|
||||
score = 90; // starts with
|
||||
} else if (name.includes(q)) {
|
||||
score = 70; // substring
|
||||
} else {
|
||||
score = 50; // fuzzy/word match
|
||||
}
|
||||
|
||||
artistMatches.push({ artist: a, score });
|
||||
}
|
||||
}
|
||||
|
||||
const releaseGroups: MBReleaseGroup[] = [];
|
||||
// Sort by score descending, then alphabetically.
|
||||
artistMatches.sort((a, b) => b.score - a.score || a.artist.Name.localeCompare(b.artist.Name));
|
||||
|
||||
const artists: MBArtist[] = artistMatches.slice(0, 10).map((m) => ({
|
||||
mbid: m.artist.MBID || '',
|
||||
name: m.artist.Name,
|
||||
sortName: '',
|
||||
type: '',
|
||||
country: '',
|
||||
disambiguation: '',
|
||||
score: m.score,
|
||||
_imageSmall: m.artist.ImageSmall || '',
|
||||
_imageMedium: m.artist.ImageMedium || '',
|
||||
_inLibrary: true,
|
||||
} as MBArtist & { _imageSmall: string; _imageMedium: string; _inLibrary: boolean }));
|
||||
|
||||
// Collect all matching albums with match-quality scores.
|
||||
const albumMatches: Array<{ album: any; score: number }> = [];
|
||||
const cachedAlbums = libraryStore.cachedAlbums;
|
||||
if (cachedAlbums) {
|
||||
for (const a of cachedAlbums) {
|
||||
if (fuzzyMatch(q, a.Name.toLowerCase()) || fuzzyMatch(q, a.ArtistName.toLowerCase())) {
|
||||
releaseGroups.push({
|
||||
mbid: a.MBID || '',
|
||||
title: a.Name,
|
||||
primaryType: 'Album',
|
||||
artistCredit: a.ArtistName,
|
||||
firstReleaseDate: a.Year ? String(a.Year) : '',
|
||||
_coverArt: a.CoverArtMedium || a.CoverArtSmall || '',
|
||||
_inLibrary: true,
|
||||
} as MBReleaseGroup & { _coverArt: string; _inLibrary: boolean });
|
||||
if (releaseGroups.length >= 5) break;
|
||||
const name = a.Name.toLowerCase();
|
||||
const artist = a.ArtistName.toLowerCase();
|
||||
const matchesName = fuzzyMatch(q, name);
|
||||
const matchesArtist = fuzzyMatch(q, artist);
|
||||
if (!matchesName && !matchesArtist) continue;
|
||||
|
||||
let score: number;
|
||||
// Artist name match is strongest (same as remote rgMatchTier).
|
||||
if (artist === q) {
|
||||
score = 100;
|
||||
} else if (artist.startsWith(q) || artist.includes(q)) {
|
||||
score = 85;
|
||||
} else if (name === q) {
|
||||
score = 80;
|
||||
} else if (name.startsWith(q)) {
|
||||
score = 75;
|
||||
} else if (name.includes(q)) {
|
||||
score = 60;
|
||||
} else {
|
||||
score = 40;
|
||||
}
|
||||
|
||||
albumMatches.push({ album: a, score });
|
||||
}
|
||||
}
|
||||
|
||||
albumMatches.sort((a, b) => b.score - a.score || a.album.Name.localeCompare(b.album.Name));
|
||||
|
||||
const releaseGroups: MBReleaseGroup[] = albumMatches.slice(0, 10).map((m) => ({
|
||||
mbid: m.album.MBID || '',
|
||||
title: m.album.Name,
|
||||
primaryType: 'Album',
|
||||
artistCredit: m.album.ArtistName,
|
||||
firstReleaseDate: m.album.Year ? String(m.album.Year) : '',
|
||||
_coverArt: m.album.CoverArtMedium || m.album.CoverArtSmall || '',
|
||||
_inLibrary: true,
|
||||
} as MBReleaseGroup & { _coverArt: string; _inLibrary: boolean }));
|
||||
|
||||
if (artists.length === 0 && releaseGroups.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user