Files
tolaria/src/utils/fuzzyMatch.ts
lucaronin 4649662f3e fix: ensure exact title match always ranks first in search
Title exact match gets exclusive tier 0 — alias exact match is capped
at tier 1, so a note titled "Refactoring" always appears above notes
with "Refactoring" as an alias or prefix. The 5-tier ranking is:
0=title exact, 1=alias exact, 2=title prefix, 3=alias prefix, 4=fuzzy.

Also adds ranking to editor wikilink autocomplete (enrichSuggestionItems)
and trims whitespace in searchRank comparisons.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 22:03:54 +01:00

59 lines
1.8 KiB
TypeScript

/** Search rank tier: 0 = exact match, 1 = prefix match, 2 = fuzzy only. Lower is better. */
export function searchRank(query: string, target: string): number {
const q = query.trim().toLowerCase()
const t = target.trim().toLowerCase()
if (t === q) return 0
if (t.startsWith(q)) return 1
return 2
}
/**
* Rank a note by how well its title/aliases match the query.
* Tiers: 0 = title exact, 1 = alias exact, 2 = title prefix,
* 3 = alias prefix, 4 = fuzzy only. Lower is better.
*
* Title exact match (tier 0) is exclusive to the title — an alias
* exact match only reaches tier 1, ensuring the note whose title
* matches exactly always sorts first.
*/
export function bestSearchRank(query: string, title: string, aliases: string[]): number {
const q = query.trim().toLowerCase()
const t = title.trim().toLowerCase()
if (t === q) return 0
let aliasExact = false
let aliasPrefix = false
for (const alias of aliases) {
const a = alias.trim().toLowerCase()
if (a === q) { aliasExact = true; break }
if (a.startsWith(q)) aliasPrefix = true
}
if (aliasExact) return 1
if (t.startsWith(q)) return 2
if (aliasPrefix) return 3
return 4
}
/** Fuzzy match: all query chars must appear in order in the target. */
export function fuzzyMatch(query: string, target: string): { match: boolean; score: number } {
const q = query.toLowerCase()
const t = target.toLowerCase()
let qi = 0
let score = 0
let lastMatchIndex = -1
for (let ti = 0; ti < t.length && qi < q.length; ti++) {
if (t[ti] === q[qi]) {
if (ti === lastMatchIndex + 1) score += 2
if (ti === 0 || t[ti - 1] === ' ' || t[ti - 1] === '-') score += 3
score += 1
lastMatchIndex = ti
qi++
}
}
return { match: qi === q.length, score }
}