@bitbaum/searchkit

中文文档
0.2.3发布于 2026/10/28MITTypeScriptES Module零依赖构建来源已验证近 30 天下载 4499

Search that finds what was meant: typo budgets by word length, prefix while typing, German spellings without umlauts, joined and split words, Algolia-order tie-break ranking, highlight ranges, query expansion for any database, and a generated typo regress

原始使用文档
文档目录12 节
  1. Where it lives, and how a site adopts it
  2. What it ships
  3. Matching — what counts as the same word
  4. Ranking — a tie-break, not a weighted sum
  5. For a search that runs in a database
  6. Your own spelling rule — normalize
  7. Rows a database already found — rerank
  8. Highlighting
  9. React ( @bitbaum/searchkit/react )
  10. Proving it — typoCases and evaluate
  11. What it does not do
  12. Sources

searchkit

Search that finds what was meant.

You start typing and you get where you need to go. One wrong letter in a name — the SoundCloud failure, where an artist is unfindable because you typed kayne — still finds it. Zero dependencies; React optional.

A survey of the fleet on 2026-10-12 found about 25 search boxes, and one with any typo tolerance (substrata's own engine). The rest were text.toLowerCase().includes(q) or an SQL ILIKE '%q%': no typos, no ranking, a few without escaping %. Every new site would have written a 26th.

Where it lives, and how a site adopts it

This repo, created by bitbaum/fleet's package-repos workflow from bitbaum/orangecat packages/searchkit (its history before 0.2.2 is there). A site installs a release from npm, which Dependabot moves forward on its own:

// package.json
"@bitbaum/searchkit": "^0.2.2"

It ships compiled JavaScript in dist/, committed beside the source the way @bitbaum/whenkit does, so nothing has to transpile it: Next.js, Vitest, and Node's own test runner (which refuses TypeScript inside node_modules) all load it as is. Its tests are in test/ and run with plain Node (npm test); one of them fails while dist/ is not the build of src/ (npm run build).

Every search box in the fleet should go through it. The first adopter, OrangeCat, runs it in three places: the ⌘K palette's own lists (pages, your things), /api/search (vocabulary corrections handed to Postgres, then ranking), and /discover.

What it ships

import { createSearch } from '@bitbaum/searchkit';

const people = createSearch(rows, {
  fields: [
    { name: 'name', get: p => p.name }, // most important first
    { name: 'handle', get: p => p.username },
    { name: 'bio', get: p => p.bio },
  ],
  rank: p => Math.log1p(p.followers), // last tie-break only
});

people.search('kayne wset'); // → Kanye West, with ranges to mark

Matching — what counts as the same word

typedfindshow
cqtoCatoone wrong letter (budget: 0 typos under 4 letters, 1 from 4, 2 from 8)
kayneKanyea swap is ONE typo (optimal string alignment)
kokiLokia wrong first letter still matches — ranked as two typos
loLokithe start of a word, while typing
bitconBitcoina typo in the start of a word
zurich, muellerZürich, Mülleraccents folded; umlauts also indexed spelled out
orangecat, orange catOrangeCat, Orange CatcamelCase and separators split; adjacent words also indexed joined (ranked as a typo when the text wrote them apart, so catto still finds Cato first)
cato xylophoneCatoa word that matches nothing is set aside, not fatal
jon smithJon Snow, John Smithwhen no item has every word, the most words any item has are kept — whichever words they are

The budget is the one Algolia and Typesense use. Meilisearch's (one typo from 5 letters, a first-letter typo costs two) cannot find a mistyped 4-letter name at all, and a 5–8-letter one whose first letter slipped.

Ranking — a tie-break, not a weighted sum

Each rule only decides between results the rules above it called equal: typos → words matched → field → exactness (the whole field typed, then whole words, then prefixes) → proximity → position in the field → your rank → shorter field. Sums need tuning and drift; a long description outscores a short exact title. This is Algolia's published order, adjusted for names.

For a search that runs in a database

The forgiving part does not need to live in the database. Keep the collection's words (createVocabulary), expand what was typed into the words it could mean, and send those along:

import { corrections, createVocabulary, didYouMean } from '@bitbaum/searchkit';

const vocab = createVocabulary(wordsWithCounts); // e.g. from a cached SQL view
corrections('cqto', vocab); // → ["cato"]: OR these into your tsquery / ILIKE
didYouMean('cqto', vocab); // → "cato", for the zero-result screen

Then rerank the rows the database returned with createSearch — one ranking for in-memory lists and database results alike.

Why not do it in Postgres: pg_trgm scores kanye/kayne at 0.2 and cqto/cato at 0.4, under any useful threshold, and fuzzystrmatch's levenshtein counts a swap as two edits. Use trigrams for recall at a low threshold if you like; decide typos here.

Your own spelling rule — normalize

SearchSpec.normalize(word) runs on every word, both sides, after the standard folding. Heidi reads Swiss German as people type it: "ie" as "i", doubled letters as one, so gruzi mittenand finds "Grüezi mitenand". Keep it to spelling; meaning belongs elsewhere. expandQuery/corrections take the same function so a vocabulary agrees with the index. The typo budget stays the WRITTEN word's: the rule turns "vill" into "vil", and a slip in "vill" is still forgiven as in any four-letter word.

Rows a database already found — rerank

rerank(rows, spec, query) ranks them by what was typed and keeps the rows it cannot place (matched on a field you did not pass, or by stemming) after the ranked ones, in their original order. OrangeCat's /api/search ranks Postgres candidates this way.

Highlighting

hit.matches[field] holds ranges into the original text (a typo match marks the whole word; a prefix only what was typed). highlightQuery(text, q) marks words that start with a typed word, for text that was not searched. Both return plain parts — never HTML. snippet(text, ranges, { maxLength }) cuts a long description to the window around its first match, at word boundaries, with "…" where it was cut, and re-bases the ranges onto it.

React (@bitbaum/searchkit/react)

  • useSearchAsYouType(text, (q, signal) => fetch(...), { debounceMs: 100 }) — each keystroke aborts the request before it; a late answer to an older query is dropped.
  • <Highlight text ranges | query markClassName /> — unstyled <mark>s.

Proving it — typoCases and evaluate

const cases = typoCases(names); // every one-slip misspelling of every name
const report = await evaluate(cases, q => search(q).map(h => h.id));
// { successAt1, successAt3, mrr, zeroResults, misses }

Slips are generated, not imagined: a neighbouring key (QWERTY and QWERTZ), a dropped letter, a doubled letter, a swapped pair. Put it in CI and "one slip still finds it" fails the day it stops being true. This package's own suite runs 571 such cases over eight names at success@1 = 1.

What it does not do

  • Meaning. "someone to fix my bike" finding "Velo repair" is embeddings' job (descriptive queries, never names — dense retrieval loses to lexical on names by tens of points). Fuse such results with Reciprocal Rank Fusion (k = 60) if you add them.
  • Security. Whether a row may be shown is decided before it reaches this package. An in-memory matcher has no business behind a visibility gate.
  • Phonetics. Schmidt/Schmitt, Meier/Mayer. Useful for person names only, harmful elsewhere; not yet built.

Sources

Algolia typo tolerance and ranking criteria; Typesense search parameters; Meilisearch typo internals; Damerau (1964), Pollock & Zamora (1983) on single edits; Schulz & Mihov (2002) on Levenshtein automata; SymSpell; Nielsen's response-time limits; Cormack, Clarke & Büttcher (SIGIR 2009) on RRF; Sciavolino et al. (EMNLP 2021) on dense retrieval and entity names.

版本与发布记录

@bitbaum/searchkit 共发布 3 个版本,首次发布于 2026/10/26;过去 12 个月发布了 3 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.2.3latest2026/10/2811 天前
0.2.22026/10/2613 天前
0.0.0-stage2026/10/2613 天前

关于 @bitbaum/searchkit 的常见问题

@bitbaum/searchkit 是什么?

Search that finds what was meant: typo budgets by word length, prefix while typing, German spellings without umlauts, joined and split words, Algolia-order tie-break ranking, highlight ranges, query expansion for any database, and a generated typo regress

如何安装 @bitbaum/searchkit?

在项目目录运行 npm install @bitbaum/searchkit,或使用 pnpm add / yarn add / bun add。

@bitbaum/searchkit 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@bitbaum/searchkit 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@bitbaum/searchkit 可以商用吗?

@bitbaum/searchkit 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@bitbaum/searchkit 的下载量有多少?

根据 npm 官方下载统计,@bitbaum/searchkit 在 2026/10/08 – 2026/11/06 这 30 天内被下载 4,499 次,日均约 150 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@bitbaum/searchkit 还在维护吗?

最新版本 0.2.3 发布于 2026/10/28(11 天前);至今共发布 3 个版本,其中过去 12 个月发布了 3 个;首次发布于 2026/10/26。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。