Skip to content

Commit

Permalink
Replace Algolia search with Flexsearch (FastEndpoints#10)
Browse files Browse the repository at this point in the history
* Remove the extra dot in the filename extension

Caused the generated links to have a dot in the url, e.g. /exception-handler.#unhandled-exception-handler.
Make it consistent with the rest of the files so that the logic for generating search hrefs remains consistent and simple.

* Ignore JetBrains .idea directory

* Add marked, svelte-spotlight and flexsearch dependencies

marked - markdown parsing/rendering
svelte-spotlight - search results in a modal component
flexsearch - client-side search indexing

* Add TS type definitions for flexsearch

Because both the type definitions that ship with flexsearch and the definitions in @types/flexsearch are incorrect

* Add build-time search file generation

Served via /search.json

* Replace Algolia search with custom flexsearch implementation

Fetching of the search file and the indexing/search logic happens in the background, on a SharedWorker to avoid blocking the main browser thread and allowing the page to load before the whole search file is fetched.
The SharedWorker is instantiated alongside the store because it makes it easy to manage the worker lifecycle.

* Replace yellow mark with light blue
  • Loading branch information
vzakanj authored Dec 5, 2022
1 parent d31c912 commit 9414ba4
Show file tree
Hide file tree
Showing 17 changed files with 991 additions and 14 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -114,3 +114,6 @@ node_modules

# Project specific
/docs

# JetBrains .idea directory
.idea/
54 changes: 54 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"@sveltejs/kit": "next",
"@svelteness/kit-docs": "^0.23.0",
"@tailwindcss/typography": "^0.5.4",
"@types/marked": "^4.0.7",
"@types/nprogress": "^0.2.0",
"@types/react": "^18.0.15",
"@typescript-eslint/eslint-plugin": "^5.27.0",
Expand All @@ -51,5 +52,10 @@
"unplugin-icons": "^0.13.0",
"vite": "^3.0.0"
},
"type": "module"
"type": "module",
"dependencies": {
"flexsearch": "^0.7.31",
"marked": "^4.2.2",
"svelte-spotlight": "^1.0.4"
}
}
30 changes: 30 additions & 0 deletions src/lib/search/client/components/SearchBox.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<div
class="cursor-pointer w-full items-center rounded-sm border border-gray-divider bg-gray-elevate py-2.5 px-3 text-[15px] shadow-sm flex gap-x-2"
on:click>
<div class="flex-1 flex items-center">
<svg width="20" height="20" class="search-icon" viewBox="0 0 20 20">
<path
d="M14.386 14.386l4.0877 4.0877-4.0877-4.0877c-2.9418 2.9419-7.7115 2.9419-10.6533 0-2.9419-2.9418-2.9419-7.7115 0-10.6533 2.9418-2.9419 7.7115-2.9419 10.6533 0 2.9419 2.9418 2.9419 7.7115 0 10.6533z"
stroke="currentColor"
fill="none"
fill-rule="evenodd"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
<span>Search</span>
</div>

<span class="flex space-x-0.5 font-semibold">
<span>⌘</span>
<span>K</span>
</span>
</div>

<style>
.search-icon {
margin-top: -0.25rem;
margin-right: 0.5rem;
stroke-width: 1.6;
}
</style>
88 changes: 88 additions & 0 deletions src/lib/search/client/components/SearchModal.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<script lang="ts">
import SvelteSpotlight from 'svelte-spotlight/SvelteSpotlight.svelte';
import { createEventDispatcher } from 'svelte';
import type { SearchResult } from '../../types';
export let query = "";
export let results : SearchResult[] = [];
export let isOpen = false;
const dispatch = createEventDispatcher();
$: onOpenChange(isOpen);
$: onQueryChange(query);
function onOpenChange(newIsOpen: boolean) {
dispatch('openchange', { newIsOpen })
}
function onQueryChange(newQuery: string) {
dispatch('querychange', { newQuery });
}
function escape(text: string) {
return text.replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
function excerpt(content: string, query: string) {
const index = content.toLowerCase().indexOf(query.toLowerCase());
if (index === -1) {
return content.slice(0, 100);
}
const prefix = index > 20
? `...${content.slice(index - 15, index)}`
: content.slice(0, index);
const suffix = content.slice(
index + query.length,
index + query.length + (80 - (prefix.length + query.length))
);
return (
escape(prefix) +
`<mark>${escape(content.slice(index, index + query.length))}</mark>` +
escape(suffix)
);
}
function onKeyboardSelect(event: CustomEvent<SearchResult>) {
dispatch('select', { href: event.detail.href });
}
</script>

<SvelteSpotlight
{results}
bind:query
bind:isOpen
modalClass={'w-[600px] max-w-[90%] bg-feDarkBlue-600 shadow-lg rounded-lg'}
headerClass={'py-3 px-10 border-b-2 border-gray-500 border-b-solid'}
inputClass="text-gray-50 focus:outline-none bg-transparent"
contentClass="py-3 [&_li]:py-2"
resultIdKey="href"
on:select={(event) => {
onKeyboardSelect(event);
}}
>
<div
slot="result"
let:selected
let:result
class={`[&_mark]:bg-feLightBlue-600 cursor-pointer hover:bg-feDarkBlue-800 text-sm w-full ${selected ? "bg-feDarkBlue-500" : ''} `}
>
<a href={result.href}
class="ml-10"
on:click={_ => dispatch('select', { href: result.href })}
>
<strong>{@html excerpt(result.title, query)}</strong>
{#if result.content}
<div class="mx-10 text-slate-500 text-sm truncate">{@html excerpt(result.content, query)}</div>
{/if}
</a>
</div>

<div slot="noResults" class="ml-10 text-slate-500 text-sm">
No results...
</div>
</SvelteSpotlight>
3 changes: 3 additions & 0 deletions src/lib/search/client/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { default as SearchModal } from './components/SearchModal.svelte';
export { default as SearchBox } from './components/SearchBox.svelte';
export { default as searchStore } from './store';
58 changes: 58 additions & 0 deletions src/lib/search/client/search.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import Index from "flexsearch";
import type { SearchBlocks, SearchResult } from "../types";



const index = new Index<string>({ tokenize: 'forward' });
const searchLookup = new Map<string, SearchResult>();

export let isInited = false;
export let isInitInProgess = false;


export async function init(origin: string) {
if (isInited) return;

isInitInProgess = true;

const rsp = await fetch(`${origin}/search.json`);
const searchData : SearchBlocks = await rsp.json();

for (const { breadcrumbs, content, href } of searchData.blocks) {
const title = breadcrumbs[breadcrumbs.length - 1];

searchLookup.set(href, {
title,
href,
breadcrumbs,
content
});

index.add(href, `${title} ${content}`);
}

isInitInProgess = false;
isInited = true;
}

export function search(query: string): SearchResult[] {
const resultHrefs = index.search(query);

return resultHrefs.map(lookup);
}

function lookup(href: string) {
const result = searchLookup.get(href);

if (result) {
return result;
}

// Should never reach this state,
// consider disabling @typescript-eslint/no-non-null-assertion
// eslint rule
throw Error("Invalid id/href");
}



Loading

0 comments on commit 9414ba4

Please sign in to comment.