feat: persist image captions and managed tags with fuzzy search

This commit is contained in:
smileBeda committed 2026-09-30 16:24:21 -03:00
1 parent 1385efec70
commit 082f715c39
4 files changed
+348 -125

No files matched your search

+61
View File
@@ -0,0 +1,61 @@
import { HttpError } from './errors.js';
import { normalizeTag } from './metadata.js';
export function searchOptions(params) {
const q = (params.get('q') || '').trim();
if (q.length > 200) throw new HttpError(400, 'Search query must be at most 200 characters.');
const tags = params.getAll('tag');
if (tags.length > 20) throw new HttpError(400, 'Use at most 20 tag filters.');
return { q, tags: [...new Set(tags.map(normalizeTag))] };
}
export function rankMemes(entries, { q = '', tags = [], missingCaption = false } = {}) {
const query = fold(q);
const terms = [...new Set(words(query))];
return entries.filter((entry) => tags.every((tag) => entry.tags.includes(tag)) && (!missingCaption || !entry.caption))
.map((entry) => ({ entry, score: relevance(entry, query, terms) }))
.filter(({ score }) => score > 0)
.sort((a, b) => b.score - a.score || b.entry.createdAt.localeCompare(a.entry.createdAt) || a.entry.id.localeCompare(b.entry.id))
.map(({ entry }) => entry);
}
function relevance(entry, query, terms) {
if (!query) return 1;
if (!terms.length) return 0;
const text = fold([entry.caption, ...entry.tags].join(' '));
const candidates = [...new Set(words(text))];
let score = text.includes(query) ? 20 : 0;
for (const term of terms) {
let best = 0;
for (const candidate of candidates) {
if (candidate === term) best = Math.max(best, 10);
else if (candidate.startsWith(term)) best = Math.max(best, 6);
else if (term.length >= 4 && near(term, candidate, term.length >= 7 ? 2 : 1)) best = Math.max(best, 3);
}
if (!best) return 0; // Every query term must match; results remain useful for multiword searches.
score += best;
}
return score;
}
function fold(value) {
return value.normalize('NFKD').replace(/\p{M}/gu, '').toLowerCase();
}
function words(value) {
return value.match(/[\p{L}\p{N}]+/gu) || [];
}
function near(a, b, limit) {
if (Math.abs(a.length - b.length) > limit) return false;
let previous = Array.from({ length: b.length + 1 }, (_, i) => i);
for (let i = 1; i <= a.length; i += 1) {
const row = [i];
for (let j = 1; j <= b.length; j += 1) {
row[j] = Math.min(row[j - 1] + 1, previous[j] + 1, previous[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
}
if (Math.min(...row) > limit) return false;
previous = row;
}
return previous[b.length] <= limit;
}