Lexical search that finds the exact word, symbol or phrase
with BM25 relevance ranking that matches exactly where it matters that forgives typos and fragments in 12 kB of gzipped JavaScript with zero runtime dependencies in your browser and Node.js
defuss-lexicalsearch indexes your documents field by field and answers keyword, phrase, prefix, typo and literal queries while you type. Exact-first ranking puts the literal match on top, and field-weighted BM25 orders everything after it. Indexing and search never touch the network.
Search algorithms and engineering brought to you by Aron Homberg (kyr0) (opens in a new tab) · Open Source (MIT (opens in a new tab))
Exact where it matters
Literal modes compare the original strings: e.g. C++ never matches C#, and operator == never matches operator !=. Exact-first ranking keeps a whole-field match above every partial one, whatever the corpus statistics say.
Ranked like a search engine
Field-weighted BM25 with an analyzer per field and derived fields, so one symbol can be a raw keyword and a set of code words at once. With exact-first ranking, results come in strict tiers: literal, keyword, identifier-prefix, term, partial, fuzzy and incomplete.
Forgiving input
Prefix completion, suffix and inside-word matching, Levenshtein or transposition-aware typos, completion despite a typo (Kül finds Kühlschrank), quoted phrases, AND and OR, field restrictions and exclusions. Every expansion is exact: no cap silently drops a match.
Small enough to ship
The core is 12 kB of gzipped JavaScript with zero runtime dependencies, as ESM and CommonJS with TypeScript types. An index serializes to a JSON snapshot and restores with every structure rebuilt.
Private by design
Indexing and search run in your browser tab or Node.js process: documents and queries never leave the device, and no third party ever sees them. Only fromURL fetches, and only when you call it.
Many languages, many kinds of text
Unicode words, code identifiers, raw keywords and whitespace symbols, plus English, German, Spanish and French stemming, stopwords and phonetic codes, and Japanese word segmentation.
Analyze.
Index.
Rank.
Finding a needle in a haystack
The complete defuss-shadcn documentation, 410 MDX pages with 8.08 MB of text, downloaded and indexed right here in your browser. Type and the results follow every keystroke. Open any page with ⌘K, or let the demo hide a needle: a word that occurs on exactly one page. In the tab you build an index of your own.
Runtime
Downloading the documentation corpus...
Pages
indexed documents
Vocabulary
distinct terms
Indexed in
download, unpack, index
Last query
postings visited
for
Clear searchSearch 410 documentation pages as you type
Try one of these, each showing a different kind of match, or let the demo hide a needle and find it again.
Your notes
0 notes indexedWrite anything. Each keystroke updates the note in the index (upsert); an emptied note leaves it. Switching the analyzer rebuilds the index from your notes.
No notes yet
Add a note, or add the samples: they show stemming, compound words and code identifiers.
for
Clear searchSearch what you wrote
Notes are searched with completion, inside-word matching and typo tolerance: Kül
and ühl
find Kühlschrank
, http
finds XMLHttpRequest
. With the German analyzer, haus
finds Häuser
.
Analyze once, then match and rank every query
Each field runs its own analyzer into postings with word positions and a vocabulary trie. A query is parsed into words and quoted phrases, analyzed the same way, expanded where you asked for it, and ranked by BM25; with exact-first ranking, the tier decides first.
- text
- terms
- structures
- lookups
- text
- terms
- top k
Exact-first tiers, strongest first
With exact-first ranking, the weakest clause of a result decides its tier. A literal match always outranks a keyword match, however rare the words of the other result are; BM25 orders results within one tier.
- literal the whole original field equals the query, whitespace included
- keyword an exact analyzed keyword, such as a case-folded identifier
- identifier-prefix a keyword starts or ends with the query
- term the analyzed words of the query occur exactly
- partial a word starts with, ends with or contains the query
- fuzzy a word lies within the edit distance, or starts within it while you type
- incomplete under OR, at least one query word is missing
Measured on your device, just now
Your browser downloaded, unpacked and indexed the documentation above. The search worker then timed seven typical queries: after three warm-up runs, the median of 21 samples, each a batch of searches lasting about 2 ms, since browsers round timers to 0.1 ms. These figures come from this page load; for Node.js, make bench writes the same kind of report.
Median latency per query type
Top 12 of 410 pages. The first run of an inside-word search also builds its trigram index once.
| Query type | Query | Median | First run | Results |
|---|---|---|---|---|
| Runs once the index is ready... | ||||

Need search tuned to your data?
Aron Homberg, freelance and independent AI researcher: search and retrieval, embedding models and on-device AI, offers consulting services worldwide.
Add it to your own app
Write the five lines yourself, or hand one prompt to your coding agent.
Package
Install it from npm with npm install defuss-lexicalsearch, or bun add defuss-lexicalsearch. Then, in Node.js or the browser:
import { LexicalSearch } from "defuss-lexicalsearch";const index = new LexicalSearch({ fields: { title: { weight: 3 }, body: {} } });index.addAll([{ id: "dialog", fields: { title: "Dialog", body: "A modal window for focused tasks." } }]);const hits = index.search("dialgo", { fuzzy: true, transpositions: true });console.log(hits[0].id); // "dialog", despite the typo
Vibe coding
Paste this prompt into your coding agent. It links the repository, so the agent works from the current README.
Integrate defuss-lexicalsearch into this app: https://github.com/kyr0/defuss-lexicalsearch
Install the npm package defuss-lexicalsearch and read its README first. Index documents with new LexicalSearch({ fields }) and addAll(), then search with search(query, options): { prefix: true, substring: true, fuzzy: true, transpositions: true } for a search box, mode: "exact" for identifiers.
For code symbols, start from apiSearchPreset, which keeps programming languages apart; for German, English, Spanish or French text, import the language analyzer.
Index large collections in a Web Worker, and persist them with serialize() and LexicalSearch.fromJSON().
send
Exact and ranked search anywhere, for free
BM25 ranking, literal symbol matching, phrases and typo tolerance run wherever JavaScript runs: no search server, no hosted index, no per-query bill.
MIT licensed · demo corpus credited below