diff --git a/package-lock.json b/package-lock.json index b7f17035..42a28697 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@supabase/auth-helpers-sveltekit": "^0.11.0", "@supabase/auth-ui-svelte": "^0.2.9", "@supabase/supabase-js": "^2.33.0", + "lunr": "^2.3.9", "resend": "^3.5.0", "stripe": "^13.3.0" }, @@ -22,6 +23,7 @@ "@types/glob": "^8.1.0", "@types/html-to-text": "^9.0.4", "@types/jsdom": "^21.1.7", + "@types/lunr": "^2.3.7", "@typescript-eslint/eslint-plugin": "^6.20.0", "@typescript-eslint/parser": "^6.19.0", "autoprefixer": "^10.4.15", @@ -29,7 +31,6 @@ "eslint": "^8.28.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-svelte": "^2.30.0", - "fuse.js": "^7.0.0", "html-to-text": "^9.0.5", "jsdom": "^24.1.1", "postcss": "^8.4.31", @@ -1184,6 +1185,12 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, + "node_modules/@types/lunr": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/@types/lunr/-/lunr-2.3.7.tgz", + "integrity": "sha512-Tb/kUm38e8gmjahQzdCKhbdsvQ9/ppzHFfsJ0dMs3ckqQsRj+P5IkSAwFTBrBxdyr3E/LoMUUrZngjDYAjiE3A==", + "dev": true + }, "node_modules/@types/minimatch": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", @@ -2964,15 +2971,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/fuse.js": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.0.0.tgz", - "integrity": "sha512-14F4hBIxqKvD4Zz/XjDc3y94mNZN6pRv3U13Udo0lNLCWRBUsrMv2xwcF/y/Z5sV6+FQW+/ow68cHpm4sunt8Q==", - "dev": true, - "engines": { - "node": ">=10" - } - }, "node_modules/get-func-name": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", @@ -3798,6 +3796,11 @@ "node": "14 || >=16.14" } }, + "node_modules/lunr": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", + "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==" + }, "node_modules/magic-string": { "version": "0.30.10", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.10.tgz", diff --git a/package.json b/package.json index fc3b92fd..18c331c6 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "@types/glob": "^8.1.0", "@types/html-to-text": "^9.0.4", "@types/jsdom": "^21.1.7", + "@types/lunr": "^2.3.7", "@typescript-eslint/eslint-plugin": "^6.20.0", "@typescript-eslint/parser": "^6.19.0", "autoprefixer": "^10.4.15", @@ -29,7 +30,6 @@ "eslint": "^8.28.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-svelte": "^2.30.0", - "fuse.js": "^7.0.0", "html-to-text": "^9.0.5", "jsdom": "^24.1.1", "postcss": "^8.4.31", @@ -47,6 +47,7 @@ "@supabase/auth-helpers-sveltekit": "^0.11.0", "@supabase/auth-ui-svelte": "^0.2.9", "@supabase/supabase-js": "^2.33.0", + "lunr": "^2.3.9", "resend": "^3.5.0", "stripe": "^13.3.0" } diff --git a/src/lib/build_index.ts b/src/lib/build_index.ts index 9b9b8537..50269115 100644 --- a/src/lib/build_index.ts +++ b/src/lib/build_index.ts @@ -3,19 +3,25 @@ import fs from "fs" import glob from "glob" import { convert } from "html-to-text" import JSDOM from "jsdom" -import Fuse from "fuse.js" +import lunr from "lunr" const excludePaths = ["/search"] export async function buildSearchIndex() { - const indexData = [] + const docs = [] + const indexDocs: { + title: string + description: string + body: string + id: number + }[] = [] // iterate all files with html extension in ./svelte-kit/output/prerendered/pages const fileRoot = path.resolve(".") const pagesPath = path.join(fileRoot, ".svelte-kit/output/prerendered/pages") const allFiles = glob.sync(path.join(pagesPath, "**/*.html")) - for (const file of allFiles) { + for (const [i, file] of allFiles.entries()) { try { const webPath = file .replace(pagesPath, "") @@ -43,23 +49,42 @@ export async function buildSearchIndex() { dom.window.document .querySelector('meta[name="description"]') ?.getAttribute("content") || "" - indexData.push({ + docs.push({ title, description, - body: plaintext, path: webPath, }) + indexDocs.push({ + title, + description, + body: plaintext, + id: i, + }) } catch (e) { console.log("Blog search indexing error", file, e) } } - const index = Fuse.createIndex(["title", "description", "body"], indexData) - const jsonIndex = index.toJSON() - const data = { index: jsonIndex, indexData, buildTime: Date.now() } - return data + const index = lunr(function () { + this.field("title", { boost: 3 }) + this.field("description", { boost: 2 }) + this.field("body", { boost: 1 }) + this.ref("id") + + indexDocs.forEach((doc) => { + this.add(doc) + }, this) + }) + + return { + index: JSON.stringify(index), + docs, + buildTime: Date.now(), + } } +// Use this if you want to integrate intyou your build process manually. +// Default install achieves similar result by setting prerender=true fore /search/api route. export async function buildAndCacheSearchIndex() { const data = await buildSearchIndex() // write index data to file, overwriting static file on build diff --git a/src/routes/(marketing)/search/+page.svelte b/src/routes/(marketing)/search/+page.svelte index ae6a85de..6ef894f4 100644 --- a/src/routes/(marketing)/search/+page.svelte +++ b/src/routes/(marketing)/search/+page.svelte @@ -2,17 +2,19 @@ import { page } from "$app/stores" import { browser } from "$app/environment" import { onMount } from "svelte" - import Fuse from "fuse.js" import { goto } from "$app/navigation" import { dev } from "$app/environment" + import lunr from "lunr" - const fuseOptions = { - keys: ["title", "description", "body"], - ignoreLocation: true, - threshold: 0.3, + type Result = { + title: string + description: string + path: string } - let fuse: Fuse | undefined + let results: Result[] = [] + let index: lunr.Index | undefined + let docs: Result[] = [] let loading = true let error = false @@ -23,9 +25,11 @@ throw new Error(`HTTP error! status: ${response.status}`) } const searchData = await response.json() - if (searchData && searchData.index && searchData.indexData) { - const index = Fuse.parseIndex(searchData.index) - fuse = new Fuse(searchData.indexData, fuseOptions, index) + if (searchData && searchData.index && searchData.docs) { + //index = elasticlunr.Index.load(searchData.index) + let indexData = JSON.parse(searchData.index) + index = lunr.Index.load(indexData) + docs = searchData.docs } } catch (e) { console.error("Failed to load search data", e) @@ -36,21 +40,14 @@ } }) - type Result = { - item: { - title: string - description: string - body: string - path: string - } - } - let results: Result[] = [] - // searchQuery is $page.url.hash minus the "#" at the beginning if present let searchQuery = decodeURIComponent($page.url.hash.slice(1) ?? "") $: { - if (fuse) { - results = fuse.search(searchQuery) + if (searchQuery.length == 0) { + results = [] + } else if (index) { + let indexResults = index.search(searchQuery) + results = indexResults.map((r) => docs[parseInt(r.ref)]) } } // Update the URL hash when searchQuery changes so the browser can bookmark/share the search results @@ -123,8 +120,8 @@
No results found
{#if dev}
- Development mode only message: if you're missing content, rebuild your - local search index with `npm run build` + Development mode message: if you're missing content, rebuild your local + search index with `npm run build`
{/if} {/if} @@ -132,17 +129,17 @@
{#each results as result, i}
-
{result.item.title}
+
{result.title}
- {result.item.path} + {result.path}
-
{result.item.description}
+
{result.description}
{/each}