Skip to content

Commit d980aa1

Browse files
committed
Add "Best match" group to search results for title matches
Prioritizes results whose title starts with or contains the search query, showing them in a "Best match" group above regular results.
1 parent 00c31da commit d980aa1

1 file changed

Lines changed: 44 additions & 20 deletions

File tree

docs/src/routes/docs/search/Search.svelte

Lines changed: 44 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -114,48 +114,72 @@
114114
}
115115
];
116116
117+
/** Rank a result for "Best match": 0 = title starts with query, 1 = title contains query, -1 = no match */
118+
function bestMatchRank(result: SearchEntry, query: string): number {
119+
if (result.type === 'heading') return -1;
120+
const plainTitle = result.title.replace(/<[^>]*>/g, '').toLowerCase();
121+
const q = query.toLowerCase().trim();
122+
if (plainTitle.startsWith(q)) return 0;
123+
if (plainTitle.includes(q)) return 1;
124+
return -1;
125+
}
126+
117127
// Convert search results to MenuOption format with grouping
118128
const options = $derived.by((): SearchOption[] => {
119129
// Show default options when no search query
120130
if (!searchQuery) return defaultOptions;
121131
122132
if (!searchResults.length) return [];
123133
124-
// Sort by group order, then by parent slug (so headings appear after their parent page)
125-
const sorted = [...searchResults].sort((a, b) => {
126-
const aGroupType = getGroupType(a);
127-
const bGroupType = getGroupType(b);
134+
// Split results into best matches (starts with, then contains) and the rest
135+
const startsWithMatches: SearchEntry[] = [];
136+
const containsMatches: SearchEntry[] = [];
137+
const rest: SearchEntry[] = [];
138+
for (const result of searchResults) {
139+
const rank = bestMatchRank(result, searchQuery);
140+
if (rank === 0) startsWithMatches.push(result);
141+
else if (rank === 1) containsMatches.push(result);
142+
else rest.push(result);
143+
}
128144
129-
// First sort by group
130-
if (groupOrder[aGroupType] !== groupOrder[bGroupType]) {
131-
return groupOrder[aGroupType] - groupOrder[bGroupType];
132-
}
145+
// Sort helper: by group order, then parent slug, then headings after parent
146+
function sortEntries(entries: SearchEntry[]) {
147+
return [...entries].sort((a, b) => {
148+
const aGroupType = getGroupType(a);
149+
const bGroupType = getGroupType(b);
133150
134-
// Within the same group, sort by parent slug so headings follow their parent
135-
const aParentSlug = a.type === 'heading' ? a.parentSlug : a.slug;
136-
const bParentSlug = b.type === 'heading' ? b.parentSlug : b.slug;
151+
if (groupOrder[aGroupType] !== groupOrder[bGroupType]) {
152+
return groupOrder[aGroupType] - groupOrder[bGroupType];
153+
}
137154
138-
if (aParentSlug !== bParentSlug) {
139-
return (aParentSlug ?? '').localeCompare(bParentSlug ?? '');
140-
}
155+
const aParentSlug = a.type === 'heading' ? a.parentSlug : a.slug;
156+
const bParentSlug = b.type === 'heading' ? b.parentSlug : b.slug;
141157
142-
// Parent pages come before their headings
143-
if (a.type !== 'heading' && b.type === 'heading') return -1;
144-
if (a.type === 'heading' && b.type !== 'heading') return 1;
158+
if (aParentSlug !== bParentSlug) {
159+
return (aParentSlug ?? '').localeCompare(bParentSlug ?? '');
160+
}
161+
162+
if (a.type !== 'heading' && b.type === 'heading') return -1;
163+
if (a.type === 'heading' && b.type !== 'heading') return 1;
164+
165+
return 0;
166+
});
167+
}
145168
146-
return 0;
147-
});
169+
const bestMatches = [...sortEntries(startsWithMatches), ...sortEntries(containsMatches)];
170+
const sorted = [...bestMatches, ...sortEntries(rest)];
148171
149172
// Convert to MenuOption format, deduplicating by slug
150173
const seen = new Set<string>();
151174
const opts: SearchOption[] = [];
152175
for (const result of sorted) {
153176
if (seen.has(result.slug)) continue;
154177
seen.add(result.slug);
178+
const isBest = bestMatches.includes(result);
155179
opts.push({
156180
label: result.title,
157181
value: result.slug,
158-
group: groupLabels[getGroupType(result)],
182+
group: isBest ? 'Best match' : groupLabels[getGroupType(result)],
159183
result
160184
});
161185
}

0 commit comments

Comments
 (0)