Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)!;
}
Loading