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>
This commit is contained in:
Test
2026-03-08 22:03:54 +01:00
parent e40c09a2ef
commit 60f3139b3e
5 changed files with 95 additions and 25 deletions

View File

@@ -175,18 +175,31 @@ describe('useNoteSearch', () => {
expect(preventDefaultSpy).not.toHaveBeenCalled()
})
it('ranks exact title match first, prefix second, fuzzy third', () => {
it('ranks exact title match first even with many prefix competitors', () => {
const ranked: VaultEntry[] = [
makeEntry({ path: '/vault/ri.md', title: 'Refactoring Ideas', modifiedAt: 1700000003 }),
makeEntry({ path: '/vault/ri.md', title: 'Refactoring Ideas', modifiedAt: 1700000010 }),
makeEntry({ path: '/vault/rk.md', title: 'Refactoring Key Ideas', modifiedAt: 1700000009 }),
makeEntry({ path: '/vault/rp.md', title: 'Refactoring Patterns', modifiedAt: 1700000008 }),
makeEntry({ path: '/vault/rs.md', title: 'Refactoring Strategy', modifiedAt: 1700000007 }),
makeEntry({ path: '/vault/rt.md', title: 'Refactoring Techniques', modifiedAt: 1700000006 }),
makeEntry({ path: '/vault/rb.md', title: 'Refactoring Best Practices', modifiedAt: 1700000005 }),
makeEntry({ path: '/vault/rg.md', title: 'Refactoring Guide', modifiedAt: 1700000004 }),
makeEntry({ path: '/vault/rw.md', title: 'Refactoring Workflows', modifiedAt: 1700000003 }),
makeEntry({ path: '/vault/rc.md', title: 'Refactoring Checklist', modifiedAt: 1700000002 }),
makeEntry({ path: '/vault/r.md', title: 'Refactoring', isA: 'Area', modifiedAt: 1700000001 }),
]
const { result } = renderHook(() => useNoteSearch(ranked, 'Refactoring'))
expect(result.current.results[0].title).toBe('Refactoring')
})
it('ranks exact title match above note with alias exact match', () => {
const ranked: VaultEntry[] = [
makeEntry({ path: '/vault/ri.md', title: 'Refactoring Ideas', aliases: ['Refactoring'], modifiedAt: 1700000003 }),
makeEntry({ path: '/vault/rk.md', title: 'Refactoring Key Ideas', modifiedAt: 1700000002 }),
makeEntry({ path: '/vault/r.md', title: 'Refactoring', modifiedAt: 1700000001 }),
]
const { result } = renderHook(() => useNoteSearch(ranked, 'Refactoring'))
expect(result.current.results.map(r => r.title)).toEqual([
'Refactoring',
'Refactoring Ideas',
'Refactoring Key Ideas',
])
expect(result.current.results[0].title).toBe('Refactoring')
})
it('ranks case-insensitive exact match first', () => {