HASH 64a3e04ec7f2
DATE 2026-07-17
SUBJECT web: latin-only fonts, drop unused 500, replace fuse.js with a 40-line
scorer
FILES 6 CHANGED HASH 64a3e04ec7f2
DATE 2026-07-17
SUBJECT web: latin-only fonts, drop unused
500, replace fuse.js with a 40-line
scorer
FILES 6 CHANGED
┌─ ooknet-design/package-lock.json ──────────────────────────────────────────┐
│ diff --git a/ooknet-design/package-lock.json b/ooknet-design/package-lock.json │
│ index 3482424..d17bd9b 100644 │
│ --- a/ooknet-design/package-lock.json │
│ +++ b/ooknet-design/package-lock.json │
│ @@ -10,8 +10,7 @@ │
│ "dependencies": { │
│ "@fontsource/cascadia-mono": "^5.2.3", │
│ "@fontsource/ibm-plex-mono": "^5.2.7", │
│ - "astro": "^6.1.9", │
│ - "fuse.js": "^7.3.0" │
│ + "astro": "^6.1.9" │
│ }, │
│ "devDependencies": { │
│ "sass": "^1.99.0", │
│ @@ -2895,19 +2894,6 @@ │
│ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" │
│ } │
│ }, │
│ - "node_modules/fuse.js": { │
│ - "version": "7.3.0", │
│ - "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.3.0.tgz", │
│ - "integrity": "sha512-plz8RVjfcDedTGfVngWH1jmJvBvAwi1v2jecfDerbEnMcmOYUEEwKF │
│ THbNoCiYyzaK2Ws8lABkTCcRSqCY1q4w==", │
│ - "license": "Apache-2.0", │
│ - "engines": { │
│ - "node": ">=10" │
│ - }, │
│ - "funding": { │
│ - "type": "github", │
│ - "url": "https://github.com/sponsors/krisk" │
│ - } │
│ - }, │
│ "node_modules/github-slugger": { │
│ "version": "2.0.0", │
│ "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0 │
│ .0.tgz", │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ooknet-design/package-lock.json ────┐
│ diff --git a/ooknet-design/package-lock.json │
│ b/ooknet-design/package-lock.json │
│ index 3482424..d17bd9b 100644 │
│ --- a/ooknet-design/package-lock.json │
│ +++ b/ooknet-design/package-lock.json │
│ @@ -10,8 +10,7 @@ │
│ "dependencies": { │
│ "@fontsource/cascadia-mono": "^5.2. │
│ 3", │
│ "@fontsource/ibm-plex-mono": "^5.2. │
│ 7", │
│ - "astro": "^6.1.9", │
│ - "fuse.js": "^7.3.0" │
│ + "astro": "^6.1.9" │
│ }, │
│ "devDependencies": { │
│ "sass": "^1.99.0", │
│ @@ -2895,19 +2894,6 @@ │
│ "node": "^8.16.0 || ^10.6.0 || >=11 │
│ .0.0" │
│ } │
│ }, │
│ - "node_modules/fuse.js": { │
│ - "version": "7.3.0", │
│ - "resolved": "https://registry.npmjs.o │
│ rg/fuse.js/-/fuse.js-7.3.0.tgz", │
│ - "integrity": "sha512-plz8RVjfcDedTGfV │
│ ngWH1jmJvBvAwi1v2jecfDerbEnMcmOYUEEwKFTHbNoC │
│ iYyzaK2Ws8lABkTCcRSqCY1q4w==", │
│ - "license": "Apache-2.0", │
│ - "engines": { │
│ - "node": ">=10" │
│ - }, │
│ - "funding": { │
│ - "type": "github", │
│ - "url": "https://github.com/sponsors │
│ /krisk" │
│ - } │
│ - }, │
│ "node_modules/github-slugger": { │
│ "version": "2.0.0", │
│ "resolved": "https://registry.npmjs.o │
│ rg/github-slugger/-/github-slugger-2.0.0.tgz │
│ ", │
└──────────────────────────────────────────────┘
┌─ ooknet-design/package.json ───────────────────────────────────────────────┐
│ diff --git a/ooknet-design/package.json b/ooknet-design/package.json │
│ index 78b118d..31126d6 100644 │
│ --- a/ooknet-design/package.json │
│ +++ b/ooknet-design/package.json │
│ @@ -15,8 +15,7 @@ │
│ "dependencies": { │
│ "@fontsource/cascadia-mono": "^5.2.3", │
│ "@fontsource/ibm-plex-mono": "^5.2.7", │
│ - "astro": "^6.1.9", │
│ - "fuse.js": "^7.3.0" │
│ + "astro": "^6.1.9" │
│ }, │
│ "devDependencies": { │
│ "sass": "^1.99.0", │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ooknet-design/package.json ─────────┐
│ diff --git a/ooknet-design/package.json b/oo │
│ knet-design/package.json │
│ index 78b118d..31126d6 100644 │
│ --- a/ooknet-design/package.json │
│ +++ b/ooknet-design/package.json │
│ @@ -15,8 +15,7 @@ │
│ "dependencies": { │
│ "@fontsource/cascadia-mono": "^5.2.3", │
│ "@fontsource/ibm-plex-mono": "^5.2.7", │
│ - "astro": "^6.1.9", │
│ - "fuse.js": "^7.3.0" │
│ + "astro": "^6.1.9" │
│ }, │
│ "devDependencies": { │
│ "sass": "^1.99.0", │
└──────────────────────────────────────────────┘
┌─ ooknet-design/src/components/TopHeader/TopHeader.astro ───────────────────┐
│ diff --git a/ooknet-design/src/components/TopHeader/TopHeader.astro b/ooknet-desig │
│ n/src/components/TopHeader/TopHeader.astro │
│ index ae32ca8..6e565d6 100644 │
│ --- a/ooknet-design/src/components/TopHeader/TopHeader.astro │
│ +++ b/ooknet-design/src/components/TopHeader/TopHeader.astro │
│ @@ -39,26 +39,14 @@ const PLACEHOLDER_NARROW = "_".repeat(18); │
│ </div> │
│ │
│ <script> │
│ - import Fuse from "fuse.js"; │
│ + import { searchIndex, type SearchItem } from "../../lib/search"; │
│ │
│ - type Note = { note: string; title: string; category: string; tags: string[]; fi │
│ led: string; url: string }; │
│ - let fuse: Fuse<Note> | null = null; │
│ + let data: SearchItem[] | null = null; │
│ │
│ async function ensureLoaded() { │
│ - if (fuse) return; │
│ + if (data) return; │
│ const res = await fetch("/search.json"); │
│ - const data: Note[] = await res.json(); │
│ - fuse = new Fuse(data, { │
│ - keys: [ │
│ - { name: "title", weight: 2 }, │
│ - { name: "note", weight: 1.5 }, │
│ - { name: "category", weight: 1 }, │
│ - { name: "tags", weight: 1 }, │
│ - { name: "filed", weight: 1 }, │
│ - ], │
│ - threshold: 0.4, │
│ - ignoreLocation: true, │
│ - }); │
│ + data = await res.json(); │
│ } │
│ │
│ function getResultsList(input: HTMLInputElement) { │
│ @@ -81,14 +69,13 @@ const PLACEHOLDER_NARROW = "_".repeat(18); │
│ list.innerHTML = ""; │
│ } │
│ │
│ - function render(list: HTMLUListElement, hits: { item: Note }[]) { │
│ + function render(list: HTMLUListElement, hits: SearchItem[]) { │
│ if (!hits.length) { close(list); return; } │
│ list.hidden = false; │
│ list.innerHTML = hits │
│ - .slice(0, 8) │
│ - .map((h, i) => { │
│ + .map((item, i) => { │
│ const safe = (s: string) => s.replace(/&/g, "&").replace(/</g, "<" │
│ ).replace(/>/g, ">"); │
│ - return `<li${i === 0 ? ' class="active"' : ""}><a href="${safe(h.item.url │
│ )}">${safe(h.item.note)} - ${safe(h.item.title)}</a></li>`; │
│ + return `<li${i === 0 ? ' class="active"' : ""}><a href="${safe(item.url)} │
│ ">${safe(item.note)} - ${safe(item.title)}</a></li>`; │
│ }) │
│ .join(""); │
│ } │
│ @@ -101,7 +88,7 @@ const PLACEHOLDER_NARROW = "_".repeat(18); │
│ const q = input.value.trim(); │
│ if (!q) { close(list); return; } │
│ await ensureLoaded(); │
│ - render(list, fuse!.search(q)); │
│ + render(list, searchIndex(data!, q)); │
│ } │
│ │
│ function onKeydown(e: KeyboardEvent) { │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...nts/TopHeader/TopHeader.astro ───┐
│ diff --git a/ooknet-design/src/components/To │
│ pHeader/TopHeader.astro b/ooknet-design/src/ │
│ components/TopHeader/TopHeader.astro │
│ index ae32ca8..6e565d6 100644 │
│ --- a/ooknet-design/src/components/TopHeader │
│ /TopHeader.astro │
│ +++ b/ooknet-design/src/components/TopHeader │
│ /TopHeader.astro │
│ @@ -39,26 +39,14 @@ const PLACEHOLDER_NARROW │
│ = "_".repeat(18); │
│ </div> │
│ │
│ <script> │
│ - import Fuse from "fuse.js"; │
│ + import { searchIndex, type SearchItem } f │
│ rom "../../lib/search"; │
│ │
│ - type Note = { note: string; title: string │
│ ; category: string; tags: string[]; filed: s │
│ tring; url: string }; │
│ - let fuse: Fuse<Note> | null = null; │
│ + let data: SearchItem[] | null = null; │
│ │
│ async function ensureLoaded() { │
│ - if (fuse) return; │
│ + if (data) return; │
│ const res = await fetch("/search.json") │
│ ; │
│ - const data: Note[] = await res.json(); │
│ - fuse = new Fuse(data, { │
│ - keys: [ │
│ - { name: "title", weight: 2 }, │
│ - { name: "note", weight: 1.5 }, │
│ - { name: "category", weight: 1 }, │
│ - { name: "tags", weight: 1 }, │
│ - { name: "filed", weight: 1 }, │
│ - ], │
│ - threshold: 0.4, │
│ - ignoreLocation: true, │
│ - }); │
│ + data = await res.json(); │
│ } │
│ │
│ function getResultsList(input: HTMLInputE │
│ lement) { │
│ @@ -81,14 +69,13 @@ const PLACEHOLDER_NARROW │
│ = "_".repeat(18); │
│ list.innerHTML = ""; │
│ } │
│ │
│ - function render(list: HTMLUListElement, h │
│ its: { item: Note }[]) { │
│ + function render(list: HTMLUListElement, h │
│ its: SearchItem[]) { │
│ if (!hits.length) { close(list); return │
│ ; } │
│ list.hidden = false; │
│ list.innerHTML = hits │
│ - .slice(0, 8) │
│ - .map((h, i) => { │
│ + .map((item, i) => { │
│ const safe = (s: string) => s.repla │
│ ce(/&/g, "&").replace(/</g, "<").repl │
│ ace(/>/g, ">"); │
│ - return `<li${i === 0 ? ' class="act │
│ ive"' : ""}><a href="${safe(h.item.url)}">${ │
│ safe(h.item.note)} - ${safe(h.item.title)}</ │
│ a></li>`; │
│ + return `<li${i === 0 ? ' class="act │
│ ive"' : ""}><a href="${safe(item.url)}">${sa │
│ fe(item.note)} - ${safe(item.title)}</a></li │
│ >`; │
│ }) │
│ .join(""); │
│ } │
│ @@ -101,7 +88,7 @@ const PLACEHOLDER_NARROW │
│ = "_".repeat(18); │
│ const q = input.value.trim(); │
│ if (!q) { close(list); return; } │
│ await ensureLoaded(); │
│ - render(list, fuse!.search(q)); │
│ + render(list, searchIndex(data!, q)); │
│ } │
│ │
│ function onKeydown(e: KeyboardEvent) { │
└──────────────────────────────────────────────┘
┌─ ooknet-design/src/layouts/ArticleLayout/ArticleLayout.astro ──────────────┐
│ diff --git a/ooknet-design/src/layouts/ArticleLayout/ArticleLayout.astro b/ooknet- │
│ design/src/layouts/ArticleLayout/ArticleLayout.astro │
│ index 2314452..b91d047 100644 │
│ --- a/ooknet-design/src/layouts/ArticleLayout/ArticleLayout.astro │
│ +++ b/ooknet-design/src/layouts/ArticleLayout/ArticleLayout.astro │
│ @@ -1,11 +1,12 @@ │
│ --- │
│ -import "@fontsource/ibm-plex-mono/400.css"; │
│ -import "@fontsource/ibm-plex-mono/500.css"; │
│ -import "@fontsource/ibm-plex-mono/600.css"; │
│ -import "@fontsource/ibm-plex-mono/700.css"; │
│ -import "@fontsource/ibm-plex-mono/400-italic.css"; │
│ -import "@fontsource/cascadia-mono/400.css"; │
│ -import "@fontsource/cascadia-mono/700.css"; │
│ +// latin-only: the full css ships every script's files into dist (~1MB │
│ +// of fonts the site never uses). 500 dropped - nothing sets it. │
│ +import "@fontsource/ibm-plex-mono/latin-400.css"; │
│ +import "@fontsource/ibm-plex-mono/latin-600.css"; │
│ +import "@fontsource/ibm-plex-mono/latin-700.css"; │
│ +import "@fontsource/ibm-plex-mono/latin-400-italic.css"; │
│ +import "@fontsource/cascadia-mono/latin-400.css"; │
│ +import "@fontsource/cascadia-mono/latin-700.css"; │
│ import "../../styles/global.scss"; │
│ import "./ArticleLayout.scss"; │
│ import Toast from "../../components/Toast/Toast.astro"; │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...cleLayout/ArticleLayout.astro ───┐
│ diff --git a/ooknet-design/src/layouts/Artic │
│ leLayout/ArticleLayout.astro b/ooknet-design │
│ /src/layouts/ArticleLayout/ArticleLayout.ast │
│ ro │
│ index 2314452..b91d047 100644 │
│ --- a/ooknet-design/src/layouts/ArticleLayou │
│ t/ArticleLayout.astro │
│ +++ b/ooknet-design/src/layouts/ArticleLayou │
│ t/ArticleLayout.astro │
│ @@ -1,11 +1,12 @@ │
│ --- │
│ -import "@fontsource/ibm-plex-mono/400.css"; │
│ -import "@fontsource/ibm-plex-mono/500.css"; │
│ -import "@fontsource/ibm-plex-mono/600.css"; │
│ -import "@fontsource/ibm-plex-mono/700.css"; │
│ -import "@fontsource/ibm-plex-mono/400-itali │
│ c.css"; │
│ -import "@fontsource/cascadia-mono/400.css"; │
│ -import "@fontsource/cascadia-mono/700.css"; │
│ +// latin-only: the full css ships every scr │
│ ipt's files into dist (~1MB │
│ +// of fonts the site never uses). 500 dropp │
│ ed - nothing sets it. │
│ +import "@fontsource/ibm-plex-mono/latin-400 │
│ .css"; │
│ +import "@fontsource/ibm-plex-mono/latin-600 │
│ .css"; │
│ +import "@fontsource/ibm-plex-mono/latin-700 │
│ .css"; │
│ +import "@fontsource/ibm-plex-mono/latin-400 │
│ -italic.css"; │
│ +import "@fontsource/cascadia-mono/latin-400 │
│ .css"; │
│ +import "@fontsource/cascadia-mono/latin-700 │
│ .css"; │
│ import "../../styles/global.scss"; │
│ import "./ArticleLayout.scss"; │
│ import Toast from "../../components/Toast/T │
│ oast.astro"; │
└──────────────────────────────────────────────┘
┌─ ooknet-design/src/lib/search.test.ts ─────────────────────────────────────┐
│ diff --git a/ooknet-design/src/lib/search.test.ts b/ooknet-design/src/lib/search.t │
│ est.ts │
│ new file mode 100644 │
│ index 0000000..1275886 │
│ --- /dev/null │
│ +++ b/ooknet-design/src/lib/search.test.ts │
│ @@ -0,0 +1,40 @@ │
│ +import { describe, expect, it } from "vitest"; │
│ +import { searchIndex, type SearchItem } from "./search"; │
│ + │
│ +const ITEMS: SearchItem[] = [ │
│ + { note: "N217", title: "ENTROPY BOUNDS IN SPARSE GRAPHS", category: "graph-theo │
│ ry", tags: ["entropy", "sparse-graphs"], filed: "/INFO/GRAPH/ENTROPY/BOUND", url: │
│ "/notes/n217-entropy-bounds/" }, │
│ + { note: "N0", title: "STYLE SHEET", category: "design-system", tags: ["design-s │
│ ystem", "reference"], filed: "/META/STYLE/SHEET", url: "/notes/n0-style-sheet/" }, │
│ + { note: "DOC", title: "Wikilinks", category: "docs", tags: [], filed: "/DOCS/AU │
│ THORING/WIKILINKS", url: "/docs/authoring/wikilinks/" }, │
│ +]; │
│ + │
│ +describe("searchIndex", () => { │
│ + it("matches note numbers first", () => { │
│ + expect(searchIndex(ITEMS, "n217")[0].url).toBe("/notes/n217-entropy-bounds/") │
│ ; │
│ + }); │
│ + │
│ + it("matches title substrings", () => { │
│ + expect(searchIndex(ITEMS, "entropy")[0].note).toBe("N217"); │
│ + }); │
│ + │
│ + it("matches tags exactly, as tag badges search", () => { │
│ + expect(searchIndex(ITEMS, "reference")[0].note).toBe("N0"); │
│ + }); │
│ + │
│ + it("matches filed path segments, as breadcrumbs search", () => { │
│ + expect(searchIndex(ITEMS, "style")[0].note).toBe("N0"); │
│ + }); │
│ + │
│ + it("requires every term to match", () => { │
│ + expect(searchIndex(ITEMS, "entropy style")).toHaveLength(0); │
│ + expect(searchIndex(ITEMS, "sparse entropy")[0].note).toBe("N217"); │
│ + }); │
│ + │
│ + it("returns nothing for a blank or unmatched query", () => { │
│ + expect(searchIndex(ITEMS, " ")).toHaveLength(0); │
│ + expect(searchIndex(ITEMS, "zebra")).toHaveLength(0); │
│ + }); │
│ + │
│ + it("finds docs", () => { │
│ + expect(searchIndex(ITEMS, "wikilinks")[0].url).toBe("/docs/authoring/wikilink │
│ s/"); │
│ + }); │
│ +}); │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...design/src/lib/search.test.ts ───┐
│ diff --git a/ooknet-design/src/lib/search.te │
│ st.ts b/ooknet-design/src/lib/search.test.ts │
│ new file mode 100644 │
│ index 0000000..1275886 │
│ --- /dev/null │
│ +++ b/ooknet-design/src/lib/search.test.ts │
│ @@ -0,0 +1,40 @@ │
│ +import { describe, expect, it } from "vites │
│ t"; │
│ +import { searchIndex, type SearchItem } fro │
│ m "./search"; │
│ + │
│ +const ITEMS: SearchItem[] = [ │
│ + { note: "N217", title: "ENTROPY BOUNDS IN │
│ SPARSE GRAPHS", category: "graph-theory", t │
│ ags: ["entropy", "sparse-graphs"], filed: "/ │
│ INFO/GRAPH/ENTROPY/BOUND", url: "/notes/n217 │
│ -entropy-bounds/" }, │
│ + { note: "N0", title: "STYLE SHEET", categ │
│ ory: "design-system", tags: ["design-system" │
│ , "reference"], filed: "/META/STYLE/SHEET", │
│ url: "/notes/n0-style-sheet/" }, │
│ + { note: "DOC", title: "Wikilinks", catego │
│ ry: "docs", tags: [], filed: "/DOCS/AUTHORIN │
│ G/WIKILINKS", url: "/docs/authoring/wikilink │
│ s/" }, │
│ +]; │
│ + │
│ +describe("searchIndex", () => { │
│ + it("matches note numbers first", () => { │
│ + expect(searchIndex(ITEMS, "n217")[0].ur │
│ l).toBe("/notes/n217-entropy-bounds/"); │
│ + }); │
│ + │
│ + it("matches title substrings", () => { │
│ + expect(searchIndex(ITEMS, "entropy")[0] │
│ .note).toBe("N217"); │
│ + }); │
│ + │
│ + it("matches tags exactly, as tag badges s │
│ earch", () => { │
│ + expect(searchIndex(ITEMS, "reference")[ │
│ 0].note).toBe("N0"); │
│ + }); │
│ + │
│ + it("matches filed path segments, as bread │
│ crumbs search", () => { │
│ + expect(searchIndex(ITEMS, "style")[0].n │
│ ote).toBe("N0"); │
│ + }); │
│ + │
│ + it("requires every term to match", () => │
│ { │
│ + expect(searchIndex(ITEMS, "entropy styl │
│ e")).toHaveLength(0); │
│ + expect(searchIndex(ITEMS, "sparse entro │
│ py")[0].note).toBe("N217"); │
│ + }); │
│ + │
│ + it("returns nothing for a blank or unmatc │
│ hed query", () => { │
│ + expect(searchIndex(ITEMS, " ")).toHave │
│ Length(0); │
│ + expect(searchIndex(ITEMS, "zebra")).toH │
│ aveLength(0); │
│ + }); │
│ + │
│ + it("finds docs", () => { │
│ + expect(searchIndex(ITEMS, "wikilinks")[ │
│ 0].url).toBe("/docs/authoring/wikilinks/"); │
│ + }); │
│ +}); │
└──────────────────────────────────────────────┘
┌─ ooknet-design/src/lib/search.ts ──────────────────────────────────────────┐
│ diff --git a/ooknet-design/src/lib/search.ts b/ooknet-design/src/lib/search.ts │
│ new file mode 100644 │
│ index 0000000..a473965 │
│ --- /dev/null │
│ +++ b/ooknet-design/src/lib/search.ts │
│ @@ -0,0 +1,47 @@ │
│ +// Masthead search scorer. Replaces fuse.js - 9.5k of gzipped fuzzy │
│ +// matching is absurd for an index this size, and a plain scorer stays │
│ +// predictable as it grows. Runs in the browser. │
│ + │
│ +export interface SearchItem { │
│ + note: string; │
│ + title: string; │
│ + category: string; │
│ + tags: string[]; │
│ + filed: string; │
│ + url: string; │
│ +} │
│ + │
│ +function fieldScore(value: string, term: string, weight: number): number { │
│ + const v = value.toLowerCase(); │
│ + if (v === term) return weight * 3; │
│ + if (v.startsWith(term)) return weight * 2; │
│ + if (v.includes(term)) return weight; │
│ + return 0; │
│ +} │
│ + │
│ +function itemScore(item: SearchItem, terms: string[]): number { │
│ + let total = 0; │
│ + for (const t of terms) { │
│ + const s = Math.max( │
│ + fieldScore(item.note, t, 3), │
│ + fieldScore(item.title, t, 2), │
│ + ...item.tags.map((tag) => fieldScore(tag, t, 1.5)), │
│ + fieldScore(item.category, t, 1), │
│ + fieldScore(item.filed, t, 1), │
│ + ); │
│ + if (s === 0) return 0; // every term must land somewhere │
│ + total += s; │
│ + } │
│ + return total; │
│ +} │
│ + │
│ +export function searchIndex(items: SearchItem[], query: string, limit = 8): Searc │
│ hItem[] { │
│ + const terms = query.trim().toLowerCase().split(/\s+/).filter(Boolean); │
│ + if (!terms.length) return []; │
│ + return items │
│ + .map((item) => ({ item, score: itemScore(item, terms) })) │
│ + .filter((s) => s.score > 0) │
│ + .sort((a, b) => b.score - a.score) │
│ + .slice(0, limit) │
│ + .map((s) => s.item); │
│ +} │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ooknet-design/src/lib/search.ts ────┐
│ diff --git a/ooknet-design/src/lib/search.ts │
│ b/ooknet-design/src/lib/search.ts │
│ new file mode 100644 │
│ index 0000000..a473965 │
│ --- /dev/null │
│ +++ b/ooknet-design/src/lib/search.ts │
│ @@ -0,0 +1,47 @@ │
│ +// Masthead search scorer. Replaces fuse.js │
│ - 9.5k of gzipped fuzzy │
│ +// matching is absurd for an index this siz │
│ e, and a plain scorer stays │
│ +// predictable as it grows. Runs in the bro │
│ wser. │
│ + │
│ +export interface SearchItem { │
│ + note: string; │
│ + title: string; │
│ + category: string; │
│ + tags: string[]; │
│ + filed: string; │
│ + url: string; │
│ +} │
│ + │
│ +function fieldScore(value: string, term: st │
│ ring, weight: number): number { │
│ + const v = value.toLowerCase(); │
│ + if (v === term) return weight * 3; │
│ + if (v.startsWith(term)) return weight * 2 │
│ ; │
│ + if (v.includes(term)) return weight; │
│ + return 0; │
│ +} │
│ + │
│ +function itemScore(item: SearchItem, terms: │
│ string[]): number { │
│ + let total = 0; │
│ + for (const t of terms) { │
│ + const s = Math.max( │
│ + fieldScore(item.note, t, 3), │
│ + fieldScore(item.title, t, 2), │
│ + ...item.tags.map((tag) => fieldScore( │
│ tag, t, 1.5)), │
│ + fieldScore(item.category, t, 1), │
│ + fieldScore(item.filed, t, 1), │
│ + ); │
│ + if (s === 0) return 0; // every term mu │
│ st land somewhere │
│ + total += s; │
│ + } │
│ + return total; │
│ +} │
│ + │
│ +export function searchIndex(items: SearchIt │
│ em[], query: string, limit = 8): SearchItem[ │
│ ] { │
│ + const terms = query.trim().toLowerCase(). │
│ split(/\s+/).filter(Boolean); │
│ + if (!terms.length) return []; │
│ + return items │
│ + .map((item) => ({ item, score: itemScor │
│ e(item, terms) })) │
│ + .filter((s) => s.score > 0) │
│ + .sort((a, b) => b.score - a.score) │
│ + .slice(0, limit) │
│ + .map((s) => s.item); │
│ +} │
└──────────────────────────────────────────────┘
[ BACK TO LOG ]
──────────────────────────────────────────────────────────────────────────────────────
OOKNET
────────────────────────────────────────────────
OOKNET