Reachy_Mini / src /workers /searchWorker.js
tfrere's picture
tfrere HF Staff
feat: highlight fuzzy search matches on app cards
c28008c
import Fuse from 'fuse.js';
let fuse = null;
const FUSE_OPTIONS = {
keys: [
{ name: 'name', weight: 3 },
{ name: 'id', weight: 2 },
{ name: '_searchAuthor', weight: 2 },
{ name: '_searchDescription', weight: 1.5 },
{ name: '_searchTags', weight: 1 },
],
threshold: 0.35,
ignoreLocation: true,
includeScore: true,
includeMatches: true,
};
/**
* Build a Fuse.js index from app data.
* Flattens searchable fields for better matching.
*/
function buildIndex(apps) {
const enriched = apps.map((app) => ({
id: app.id,
name: app.name,
_searchAuthor: app.extra?.author || app.id?.split('/')?.[0] || '',
_searchDescription:
app.extra?.cardData?.short_description || app.description || '',
_searchTags: [...(app.extra?.tags || []), ...(app.extra?.cardData?.tags || [])]
.filter(Boolean)
.join(' '),
}));
fuse = new Fuse(enriched, FUSE_OPTIONS);
}
/**
* Search and return ordered list of matching app IDs with scores.
*/
/**
* Search and return ordered list of matching app IDs with scores and match indices.
* Matches are keyed by field name for easy highlight rendering.
*/
function search(query) {
if (!fuse || !query.trim()) return [];
const results = fuse.search(query.trim());
return results.map((r) => {
// Convert Fuse matches array into a map: fieldName → [[start, end], ...]
const matches = {};
if (r.matches) {
for (const m of r.matches) {
matches[m.key] = m.indices;
}
}
return { id: r.item.id, score: r.score, matches };
});
}
// Handle messages from the main thread
self.onmessage = (e) => {
const { type, apps, query } = e.data;
if (type === 'INDEX') {
buildIndex(apps);
self.postMessage({ type: 'INDEXED' });
} else if (type === 'SEARCH') {
const results = search(query);
self.postMessage({ type: 'RESULTS', results, query });
}
};