diff --git a/package-lock.json b/package-lock.json index 42a28697..b7f17035 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,6 @@ "@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" }, @@ -23,7 +22,6 @@ "@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", @@ -31,6 +29,7 @@ "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", @@ -1185,12 +1184,6 @@ "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", @@ -2971,6 +2964,15 @@ "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", @@ -3796,11 +3798,6 @@ "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 18c331c6..fc3b92fd 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,6 @@ "@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", @@ -30,6 +29,7 @@ "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,7 +47,6 @@ "@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 50269115..9b9b8537 100644 --- a/src/lib/build_index.ts +++ b/src/lib/build_index.ts @@ -3,25 +3,19 @@ import fs from "fs" import glob from "glob" import { convert } from "html-to-text" import JSDOM from "jsdom" -import lunr from "lunr" +import Fuse from "fuse.js" const excludePaths = ["/search"] export async function buildSearchIndex() { - const docs = [] - const indexDocs: { - title: string - description: string - body: string - id: number - }[] = [] + const indexData = [] // 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 [i, file] of allFiles.entries()) { + for (const file of allFiles) { try { const webPath = file .replace(pagesPath, "") @@ -49,42 +43,23 @@ export async function buildSearchIndex() { dom.window.document .querySelector('meta[name="description"]') ?.getAttribute("content") || "" - docs.push({ - title, - description, - path: webPath, - }) - indexDocs.push({ + indexData.push({ title, description, body: plaintext, - id: i, + path: webPath, }) } catch (e) { console.log("Blog search indexing error", file, e) } } - 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(), - } + const index = Fuse.createIndex(["title", "description", "body"], indexData) + const jsonIndex = index.toJSON() + const data = { index: jsonIndex, indexData, buildTime: Date.now() } + return data } -// 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 6ef894f4..ae6a85de 100644 --- a/src/routes/(marketing)/search/+page.svelte +++ b/src/routes/(marketing)/search/+page.svelte @@ -2,19 +2,17 @@ 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" - type Result = { - title: string - description: string - path: string + const fuseOptions = { + keys: ["title", "description", "body"], + ignoreLocation: true, + threshold: 0.3, } - let results: Result[] = [] - let index: lunr.Index | undefined - let docs: Result[] = [] + let fuse: Fuse | undefined let loading = true let error = false @@ -25,11 +23,9 @@ throw new Error(`HTTP error! status: ${response.status}`) } const searchData = await response.json() - 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 + if (searchData && searchData.index && searchData.indexData) { + const index = Fuse.parseIndex(searchData.index) + fuse = new Fuse(searchData.indexData, fuseOptions, index) } } catch (e) { console.error("Failed to load search data", e) @@ -40,14 +36,21 @@ } }) + 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 (searchQuery.length == 0) { - results = [] - } else if (index) { - let indexResults = index.search(searchQuery) - results = indexResults.map((r) => docs[parseInt(r.ref)]) + if (fuse) { + results = fuse.search(searchQuery) } } // Update the URL hash when searchQuery changes so the browser can bookmark/share the search results @@ -120,8 +123,8 @@
No results found
{#if dev}
- Development mode message: if you're missing content, rebuild your local - search index with `npm run build` + Development mode only message: if you're missing content, rebuild your + local search index with `npm run build`
{/if} {/if} @@ -129,17 +132,17 @@
{#each results as result, i}
-
{result.title}
+
{result.item.title}
- {result.path} + {result.item.path}
-
{result.description}
+
{result.item.description}
{/each}