diff --git a/lib/search.ts b/lib/search.ts index 878b7be..d392cf8 100644 --- a/lib/search.ts +++ b/lib/search.ts @@ -2,36 +2,40 @@ import Fuse from "fuse.js" // Helper to get nested object values (e.g., "tags.name") function getNestedValue(obj: any, path: string): any { - return path.split('.').reduce((current, key) => current?.[key], obj) + return path.split(".").reduce((current, key) => current?.[key], obj) } // Post-filter results with word boundary matching // This ensures "pir" matches "(pir)", "pir.", "pirate" but NOT "inspired" -function filterByWordBoundary(results: T[], query: string, keys: string[]): T[] { +function filterByWordBoundary( + results: T[], + query: string, + keys: string[] +): T[] { if (!query.trim()) return results // Split query into individual words const words = query.trim().split(/\s+/) // Create regex patterns for each word (word boundary matching) - const patterns = words.map(word => { + const patterns = words.map((word) => { // Escape special regex characters - const escaped = word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + const escaped = word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") // Match at word boundaries (start of word) - return new RegExp(`\\b${escaped}`, 'i') + return new RegExp(`\\b${escaped}`, "i") }) - return results.filter(item => { + return results.filter((item) => { // Check if ALL query words match at word boundaries in ANY searchable field - return patterns.every(pattern => { - return keys.some(key => { + return patterns.every((pattern) => { + return keys.some((key) => { const value = getNestedValue(item, key) if (!value) return false // Handle arrays (like tags) if (Array.isArray(value)) { - return value.some(v => { - const str = typeof v === 'object' ? JSON.stringify(v) : String(v) + return value.some((v) => { + const str = typeof v === "object" ? JSON.stringify(v) : String(v) return pattern.test(str) }) } @@ -48,18 +52,22 @@ function preprocessQuery(query: string): string { const words = query.trim().split(/\s+/) // Use include-match (') for each word - const patterns = words.map(word => { + const patterns = words.map((word) => { // If word already has extended search operators, don't modify it - if (word.startsWith('^') || word.endsWith('$') || - word.startsWith('=') || word.startsWith("'") || - word.startsWith('!')) { + if ( + word.startsWith("^") || + word.endsWith("$") || + word.startsWith("=") || + word.startsWith("'") || + word.startsWith("!") + ) { return word } // Use include-match operator (fuzzy match anywhere) return `'${word}` }) - return patterns.join(' ') + return patterns.join(" ") } // Search articles with proper field weights @@ -115,9 +123,14 @@ export function searchProjects(projects: T[], query: string): T[] { // Post-filter with word boundary matching const searchKeys = [ - "name", "tags.themes", "tags.keywords", "tags.builtWith", - "tldr", "description", "projectStatus", "content" + "name", + "tags.themes", + "tags.keywords", + "tags.builtWith", + "tldr", + "description", + "projectStatus", + "content", ] return filterByWordBoundary(fuseResults, query, searchKeys) } -