Skip to content

Commit

Permalink
Better search (#695)
Browse files Browse the repository at this point in the history
* prioritise current section when searching

* use weighting system

* lint

* sort blocks within groups
  • Loading branch information
Rich-Harris authored Oct 29, 2024
1 parent 2712586 commit da432db
Show file tree
Hide file tree
Showing 4 changed files with 73 additions and 30 deletions.
2 changes: 1 addition & 1 deletion apps/svelte.dev/src/routes/search/+page.server.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export async function load({ url, fetch }) {

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

const results = query ? search(query) : [];
const results = query ? search(query, '') : [];

return {
query,
Expand Down
10 changes: 9 additions & 1 deletion packages/site-kit/src/lib/search/SearchBox.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ It appears when the user clicks on the `Search` component or presses the corresp
import Icon from '../components/Icon.svelte';
import SearchResults from './SearchResults.svelte';
import SearchWorker from './search-worker.js?worker';
import { page } from '$app/stores';
interface Props {
placeholder?: string;
Expand Down Expand Up @@ -94,7 +95,14 @@ It appears when the user clicks on the `Search` component or presses the corresp
const id = uid++;
pending.add(id);
worker.postMessage({ type: 'query', id, payload: $search_query });
worker.postMessage({
type: 'query',
id,
payload: {
query: $search_query,
path: $page.url.pathname
}
});
}
});
Expand Down
4 changes: 2 additions & 2 deletions packages/site-kit/src/lib/search/search-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ addEventListener('message', async (event) => {
}

if (type === 'query') {
const query = payload;
const results = search(query);
const { query, path } = payload;
const results = search(query, path);

postMessage({ type: 'results', payload: { results, query } });
}
Expand Down
87 changes: 61 additions & 26 deletions packages/site-kit/src/lib/search/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,56 +44,91 @@ export function init(blocks: Block[]) {
inited = true;
}

const CURRENT_SECTION_BOOST = 2;
const EXACT_MATCH_BOOST = 10;
const WORD_MATCH_BOOST = 4;
const NEAR_MATCH_BOOST = 2;
const BREADCRUMB_LENGTH_BOOST = 0.2;

interface Entry {
block: Block;
score: number;
rank: number;
}

/**
* Search for a given query in the existing index
*/
export function search(query: string): BlockGroup[] {
export function search(query: string, path: string): BlockGroup[] {
const escaped = query.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
const regex = new RegExp(`(^|\\b)${escaped}`, 'i');
const exact_match = new RegExp(`^${escaped}$`, 'i');
const word_match = new RegExp(`(^|\\b)${escaped}($|\\b)`, 'i');
const near_match = new RegExp(`(^|\\b)${escaped}`, 'i');

const parts = path.split('/');

const blocks = indexes
.flatMap((index) => index.search(query))
// @ts-expect-error flexsearch types are wrong i think?
.map(lookup)
.map((block, rank) => ({ block: block as Block, rank }))
.sort((a, b) => {
// If rank is way lower, give that priority
if (Math.abs(a.rank - b.rank) > 3) {
return a.rank - b.rank;
.map((block, rank) => {
const block_parts = block.href.split('/');

// prioritise current section
let score = block_parts.findIndex((part, i) => part !== parts[i]);
if (score === -1) score = block_parts.length;
score *= CURRENT_SECTION_BOOST;

if (block.breadcrumbs.some((text) => exact_match.test(text))) {
console.log('EXACT MATCH', block.breadcrumbs);
score += EXACT_MATCH_BOOST;
} else if (block.breadcrumbs.some((text) => word_match.test(text))) {
score += WORD_MATCH_BOOST;
} else if (block.breadcrumbs.some((text) => near_match.test(text))) {
score += NEAR_MATCH_BOOST;
}

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

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

return a.block.breadcrumbs.length - b.block.breadcrumbs.length || a.rank - b.rank;
})
.map(({ block }) => block);
return entry;
});

const groups: Record<string, BlockGroup> = {};
const grouped: Record<string, { breadcrumbs: string[]; entries: Entry[] }> = {};

for (const block of blocks) {
const breadcrumbs = block.breadcrumbs.slice(0, 2);

const group = (groups[breadcrumbs.join('::')] ??= {
for (const entry of blocks) {
const breadcrumbs = entry.block.breadcrumbs.slice(0, 2);
const group = (grouped[breadcrumbs.join('::')] ??= {
breadcrumbs,
blocks: []
entries: []
});

group.blocks.push(block);
group.entries.push(entry);
}

return Object.values(groups);
const sorted = Object.values(grouped);

// sort blocks within groups...
for (const group of sorted) {
group.entries.sort((a, b) => b.score - a.score || a.rank - b.rank);
}

// ...then sort groups
sorted.sort((a, b) => b.entries[0].score - a.entries[0].score);

return sorted.map((group) => {
return {
breadcrumbs: group.breadcrumbs,
blocks: group.entries.map((entry) => entry.block)
};
});
}

/**
* Get a block with details by its href
*/
export function lookup(href: string) {
return map.get(href);
return map.get(href)!;
}

0 comments on commit da432db

Please sign in to comment.