feat: pure tag-ranked art library search

This commit is contained in:
2026-07-20 15:24:34 +01:00
parent f61eb48038
commit 6b88e0fa76
2 changed files with 45 additions and 0 deletions
+15
View File
@@ -0,0 +1,15 @@
export function searchArt(manifest, query) {
const terms = String(query ?? '').toLowerCase().split(/\s+/).filter(Boolean);
if (terms.length === 0) return [...manifest];
const scored = manifest.map((asset, index) => {
const hay = [asset.label, ...(asset.tags || [])].join(' ').toLowerCase();
const score = terms.reduce((s, term) => s + (hay.includes(term) ? 1 : 0), 0);
return { asset, index, score };
});
return scored
.filter((r) => r.score > 0)
.sort((a, b) => b.score - a.score || a.index - b.index)
.map((r) => r.asset);
}
+30
View File
@@ -0,0 +1,30 @@
import { describe, it, expect } from 'vitest';
import { searchArt } from '../../src/art/search.js';
const M = [
{ id: 'a', src: 'a.png', label: 'Detective in alley', tags: ['detective', 'alley', 'rain'] },
{ id: 'b', src: 'b.png', label: 'Neon street', tags: ['street', 'neon', 'rain'] },
{ id: 'c', src: 'c.png', label: 'Worn portrait', tags: ['portrait', 'face'] },
];
describe('searchArt', () => {
it('returns the whole manifest for a blank query, in order', () => {
expect(searchArt(M, '').map((x) => x.id)).toEqual(['a', 'b', 'c']);
expect(searchArt(M, ' ').map((x) => x.id)).toEqual(['a', 'b', 'c']);
});
it('ranks by number of matched terms', () => {
// "rain alley" -> a matches both (2), b matches rain (1), c matches none
expect(searchArt(M, 'rain alley').map((x) => x.id)).toEqual(['a', 'b']);
});
it('matches substrings in tags or label, case-insensitive', () => {
expect(searchArt(M, 'DETECT').map((x) => x.id)).toEqual(['a']);
expect(searchArt(M, 'portrait').map((x) => x.id)).toEqual(['c']);
});
it('breaks rank ties by original manifest order', () => {
// "rain" -> a and b both match once; a comes first in manifest
expect(searchArt(M, 'rain').map((x) => x.id)).toEqual(['a', 'b']);
});
});