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; }