feat: exact-match-first ranking in search and wikilink autocomplete

Add searchRank/bestSearchRank utilities that compute a tier (0=exact,
1=prefix, 2=fuzzy-only). Both useNoteSearch and WikilinkChatInput now
sort by rank tier first, then by fuzzy score, ensuring notes with exact
title or alias matches always surface above partial/fuzzy matches.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
lucaronin
2026-03-08 21:00:58 +01:00
parent dd7f0f978a
commit 4ce9167e09
5 changed files with 110 additions and 11 deletions

View File

@@ -175,6 +175,38 @@ describe('useNoteSearch', () => {
expect(preventDefaultSpy).not.toHaveBeenCalled()
})
it('ranks exact title match first, prefix second, fuzzy third', () => {
const ranked: VaultEntry[] = [
makeEntry({ path: '/vault/ri.md', title: 'Refactoring Ideas', 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',
])
})
it('ranks case-insensitive exact match first', () => {
const ranked: VaultEntry[] = [
makeEntry({ path: '/vault/qi.md', title: 'Quarter Ideas', modifiedAt: 1700000002 }),
makeEntry({ path: '/vault/q.md', title: 'Quarter', modifiedAt: 1700000001 }),
]
const { result } = renderHook(() => useNoteSearch(ranked, 'quarter'))
expect(result.current.results[0].title).toBe('Quarter')
})
it('boosts note whose alias is an exact match', () => {
const ranked: VaultEntry[] = [
makeEntry({ path: '/vault/ri.md', title: 'Refactoring Ideas', modifiedAt: 1700000002 }),
makeEntry({ path: '/vault/rn.md', title: 'Refactoring Notes', aliases: ['ref'], modifiedAt: 1700000001 }),
]
const { result } = renderHook(() => useNoteSearch(ranked, 'ref'))
expect(result.current.results[0].title).toBe('Refactoring Notes')
})
it('resolves custom type color from Type entries', () => {
const withTypes: VaultEntry[] = [
makeEntry({ path: '/vault/t/recipe.md', title: 'Recipe', isA: 'Type', color: 'orange', icon: 'cooking-pot' }),