Skip to content

Commit da432db

Browse files
authored
Better search (#695)
* prioritise current section when searching * use weighting system * lint * sort blocks within groups
1 parent 2712586 commit da432db

File tree

4 files changed

+73
-30
lines changed

4 files changed

+73
-30
lines changed

apps/svelte.dev/src/routes/search/+page.server.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export async function load({ url, fetch }) {
1111

1212
const query = url.searchParams.get('q') ?? '';
1313

14-
const results = query ? search(query) : [];
14+
const results = query ? search(query, '') : [];
1515

1616
return {
1717
query,

packages/site-kit/src/lib/search/SearchBox.svelte

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ It appears when the user clicks on the `Search` component or presses the corresp
1010
import Icon from '../components/Icon.svelte';
1111
import SearchResults from './SearchResults.svelte';
1212
import SearchWorker from './search-worker.js?worker';
13+
import { page } from '$app/stores';
1314
1415
interface Props {
1516
placeholder?: string;
@@ -94,7 +95,14 @@ It appears when the user clicks on the `Search` component or presses the corresp
9495
const id = uid++;
9596
pending.add(id);
9697
97-
worker.postMessage({ type: 'query', id, payload: $search_query });
98+
worker.postMessage({
99+
type: 'query',
100+
id,
101+
payload: {
102+
query: $search_query,
103+
path: $page.url.pathname
104+
}
105+
});
98106
}
99107
});
100108

packages/site-kit/src/lib/search/search-worker.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ addEventListener('message', async (event) => {
1212
}
1313

1414
if (type === 'query') {
15-
const query = payload;
16-
const results = search(query);
15+
const { query, path } = payload;
16+
const results = search(query, path);
1717

1818
postMessage({ type: 'results', payload: { results, query } });
1919
}

packages/site-kit/src/lib/search/search.ts

Lines changed: 61 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -44,56 +44,91 @@ export function init(blocks: Block[]) {
4444
inited = true;
4545
}
4646

47+
const CURRENT_SECTION_BOOST = 2;
48+
const EXACT_MATCH_BOOST = 10;
49+
const WORD_MATCH_BOOST = 4;
50+
const NEAR_MATCH_BOOST = 2;
51+
const BREADCRUMB_LENGTH_BOOST = 0.2;
52+
53+
interface Entry {
54+
block: Block;
55+
score: number;
56+
rank: number;
57+
}
58+
4759
/**
4860
* Search for a given query in the existing index
4961
*/
50-
export function search(query: string): BlockGroup[] {
62+
export function search(query: string, path: string): BlockGroup[] {
5163
const escaped = query.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
52-
const regex = new RegExp(`(^|\\b)${escaped}`, 'i');
64+
const exact_match = new RegExp(`^${escaped}$`, 'i');
65+
const word_match = new RegExp(`(^|\\b)${escaped}($|\\b)`, 'i');
66+
const near_match = new RegExp(`(^|\\b)${escaped}`, 'i');
67+
68+
const parts = path.split('/');
5369

5470
const blocks = indexes
5571
.flatMap((index) => index.search(query))
5672
// @ts-expect-error flexsearch types are wrong i think?
5773
.map(lookup)
58-
.map((block, rank) => ({ block: block as Block, rank }))
59-
.sort((a, b) => {
60-
// If rank is way lower, give that priority
61-
if (Math.abs(a.rank - b.rank) > 3) {
62-
return a.rank - b.rank;
74+
.map((block, rank) => {
75+
const block_parts = block.href.split('/');
76+
77+
// prioritise current section
78+
let score = block_parts.findIndex((part, i) => part !== parts[i]);
79+
if (score === -1) score = block_parts.length;
80+
score *= CURRENT_SECTION_BOOST;
81+
82+
if (block.breadcrumbs.some((text) => exact_match.test(text))) {
83+
console.log('EXACT MATCH', block.breadcrumbs);
84+
score += EXACT_MATCH_BOOST;
85+
} else if (block.breadcrumbs.some((text) => word_match.test(text))) {
86+
score += WORD_MATCH_BOOST;
87+
} else if (block.breadcrumbs.some((text) => near_match.test(text))) {
88+
score += NEAR_MATCH_BOOST;
6389
}
6490

65-
const a_title_matches = regex.test(a.block.breadcrumbs.at(-1)!);
66-
const b_title_matches = regex.test(b.block.breadcrumbs.at(-1)!);
91+
// prioritise branches over leaves
92+
score -= block.breadcrumbs.length * BREADCRUMB_LENGTH_BOOST;
6793

68-
// massage the order a bit, so that title matches
69-
// are given higher priority
70-
if (a_title_matches !== b_title_matches) {
71-
return a_title_matches ? -1 : 1;
72-
}
94+
const entry: Entry = { block, score, rank };
7395

74-
return a.block.breadcrumbs.length - b.block.breadcrumbs.length || a.rank - b.rank;
75-
})
76-
.map(({ block }) => block);
96+
return entry;
97+
});
7798

78-
const groups: Record<string, BlockGroup> = {};
99+
const grouped: Record<string, { breadcrumbs: string[]; entries: Entry[] }> = {};
79100

80-
for (const block of blocks) {
81-
const breadcrumbs = block.breadcrumbs.slice(0, 2);
82-
83-
const group = (groups[breadcrumbs.join('::')] ??= {
101+
for (const entry of blocks) {
102+
const breadcrumbs = entry.block.breadcrumbs.slice(0, 2);
103+
const group = (grouped[breadcrumbs.join('::')] ??= {
84104
breadcrumbs,
85-
blocks: []
105+
entries: []
86106
});
87107

88-
group.blocks.push(block);
108+
group.entries.push(entry);
89109
}
90110

91-
return Object.values(groups);
111+
const sorted = Object.values(grouped);
112+
113+
// sort blocks within groups...
114+
for (const group of sorted) {
115+
group.entries.sort((a, b) => b.score - a.score || a.rank - b.rank);
116+
}
117+
118+
// ...then sort groups
119+
sorted.sort((a, b) => b.entries[0].score - a.entries[0].score);
120+
121+
return sorted.map((group) => {
122+
return {
123+
breadcrumbs: group.breadcrumbs,
124+
blocks: group.entries.map((entry) => entry.block)
125+
};
126+
});
92127
}
93128

94129
/**
95130
* Get a block with details by its href
96131
*/
97132
export function lookup(href: string) {
98-
return map.get(href);
133+
return map.get(href)!;
99134
}

0 commit comments

Comments
 (0)