Hybrid search that finds the exact symbol and what you meant
with weighted rank fusion with exact matching for symbols and IDs with an optional ColBERT reranker that skips the reranker when sources agree in browsers, Bun and Node.js measured in 20 languages
defuss-search runs a lexical and a vector retriever over the same document IDs and fuses their rankings. Exact promotion keeps a document whose symbol or ID equals the query on top, weighted rank fusion orders everything after them, and an optional ColBERT stage reorders the top 25. Every result carries the ranks and scores behind its position.
Search algorithms and engineering brought to you by Aron Homberg (kyr0) (opens in a new tab) · Open Source (MIT) · built on defuss-lexicalsearch (opens in a new tab) and defuss-vectorsearch (opens in a new tab)
Exact where it matters
Symbols, IDs and error codes need exact matches. With
exact: 'promote', a document whose exact field equals the query ranks
first, even when the query vector points at another one. Stemmed fields get an
unstemmed companion, so a word as written beats words that only share its stem.
Fusion or a cascade
Fuse independent lexical and vector top-25 lists with weighted reciprocal rank
fusion, or rescore one source's candidates with the other: fusion,
vector-first or lexical-first, chosen per query.
Neural only where it helps
searchPreset('reranked') lets ColBERT reorder the top 25 and skips
the model when the vector winner already ranks first by a clear margin and lexical
retrieval agrees. searchPreset('efficient') runs no reranker at all.
Every rank explained
Each hit reports its rank and raw score in every source, each fusion contribution and the reasons for a promotion. Each response says whether the gate ran or skipped the reranker, and why.
Small core, optional peers
The root entry has no runtime dependencies and ships as ESM and CommonJS with TypeScript types. Adapters for defuss-lexicalsearch, defuss-vectorsearch and ColBERT load only when you import them; your own retrievers and rerankers plug in the same way.
20 languages, measured
Per-language analyzers from defuss-lexicalsearch and multilingual Winzling vectors cover 20 languages. The benchmark below reports R@1 to R@25 for each, with the same ranks on Bun, Node.js and Chromium.
Retrieve.
Fuse.
Rerank.
Pick a factory, then search
The recall benchmark's 2,000 passages in 20 languages, indexed in your browser by the factory you choose. Pick one of the 1,000 benchmark questions with ⌘K and watch where it ranks the gold passage, or type your own question. In the tab you build a hybrid index of your own notes.
Choose a search factory
Each factory of defuss-search/strategies needs a different download.
The figures are R@1 / R@5 from bun run bench:strategies. The API docs
are defuss-shadcn's 8,043 documentation sections, indexed with unstemmed companion
fields, which took exact words from 71% to 98% R@1.
The benchmark corpus is loading meanwhile.
Runtime · Lexical-only
Downloading the benchmark corpus...
Corpus
...
passages
Indexed in
...
lexical index, 20 analyzers
Models
none yet
ONNX Runtime Web, 1 thread
Last query
none yet
the chosen factory
Ask the factory a question
A benchmark question shows where the factory ranks its gold passage, which the published figures count.
Measure all 1,000 questions here
Runs every benchmark question through the chosen factory in this browser and compares the means with the published figures. Each factory you measure adds a row.
| Configuration | R@1 | R@5 | R@10 | R@25 | ColBERT calls | Published |
|---|
Passages and questions: tiny-embedding-bench-v1, a short-passage subset of Belebele (Lucas Bandarkar et al., ACL 2024; Meta; underlying Wikimedia contributors) with binary question-to-passage labels, licensed CC BY-SA 4.0. This page serves the text unchanged, regrouped into gzipped JSON, with recorded Winzling vectors in a separate file.
Your notes
Write anything and pick its language: the lexical index then uses that language's analyzer and stopwords. A note joins it 500 ms after you stop typing; the semantic factories add its vector or ColBERT tokens once their model is loaded. An empty note leaves the index.
No notes yet
Add a note, or five random ones in German, English and Russian.
Search what you wrote
Lexical matching finds shared words as you type. The semantic factories also find notes by meaning, across languages, once their model has encoded them.
Retrieve twice, fuse once, rerank only when needed
The lexical retriever gets the query text, the vector retriever its embedding; both return their top 25 within the same eligible IDs. Pure functions fuse the two rankings and apply exact promotion and source coverage. The gate then decides whether ColBERT reorders the top 25, and the policies apply again before the page is cut.
- text
- vector
- ranks
- ranks
- top 25
- rerank
- reorder
- skip
Fusion default
Retrieve both sources independently, deduplicate the union and combine the ranks. A document that only one source finds can still win.
Vector first
Retrieve semantic candidates, then order that pool with the lexical index. Candidates without a lexical hit stay in the tail.
Lexical first
Retrieve lexical candidates, then rescore those IDs by vector similarity. The second stage cannot recover a candidate the first one missed.
The ranking contract, in order
Final rank is authoritative; scores from different stages are not comparable, so
each hit states its scoreKind.
- retrieve each source returns its top 25 among the eligible IDs
- fuse weighted reciprocal rank fusion, or the second stage of a cascade
-
exact
with
exact: 'promote', literal matches move to the front -
coverage
balancereserves early ranks for each source's best candidate - gate a clear vector winner that lexical retrieval agrees with skips the reranker
- rerank ColBERT reorders the top 25; exact promotion and coverage apply again
- page ranks are assigned before the page is cut
A measured frontier, an explicit choice
Fusion weights, RRF constants and reranker gates were chosen on 500 development questions and then scored once on 500 held-out questions in 20 languages. The table lists the configurations that span quality against inference work; the presets ship the chosen ones. Gated neural counts replay real ColBERT scores and are not latency measurements. Methodology and limits.
Held-out quality and work
500 questions; vector and neural columns count the questions that needed each model.
| Configuration | R@1 | R@5 | Candidates | Vector | Neural |
|---|
Measured, not promised*
Each of the 2,000 passages of tiny-embedding-bench-v1 (opens in a new tab) is indexed once, and 1,000 questions in 20 languages search them. R@k is the share of a language's 50 questions whose gold passage, in the question's language, ranks in the top k. Lexical search knows the question's language; vector search scans every passage, so a question also competes with the 19 translations of its gold passage. From make bench, version 0.4.0.
* Runtime comparison: Bun 1.4.3, Node.js 26.11.1 and Chromium 156.0.8078.4 returned the same rank for each of the 1,000 questions under both presets. Word segmentation for Japanese, Chinese and Thai follows the runtime's ICU data (Bun 78.1, Node.js 78.3).
Recall by query language
Share of a language's 50 questions whose gold passage ranks in the top k of 2,000; one question is 2 points. The badge names how the language's lexical analyzer works. Reranking reorders only the top 25, so R@25 is the same for both presets.
searchPreset('efficient'): lexical and vector top 25, weighted RRF with k=1 and weights 3:1, no neural stage; when BM25's first hit matches few or only common query words and trails the vector first hit, that vector hit leads. Mean over 20 languages: R@1 91.3% · R@5 97.3% · R@10 98.5% · R@25 99.0%.
searchPreset('reranked'): the same retrieval, then MoEdernColBERT 1.5M refines the top 25 unless both sources agree on a clear vector winner. Mean over 20 languages: R@1 91.3% · R@5 97.4% · R@10 98.5% · R@25 99.0%. ColBERT ran for 885 of 1,000 questions.
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 index yourself, or hand one prompt to your coding agent.
Package
Install
npm v0.4.0 (opens in a new tab)
with bun add defuss-search defuss-lexicalsearch defuss-vectorsearch.
Then, in Node.js, Bun or the browser:
import { apiSearchPreset } from 'defuss-lexicalsearch';import { searchPreset } from 'defuss-search';import { createHybridIndex } from 'defuss-search/indexing';const search = createHybridIndex({ documents: [ { id: 'push', fields: { symbol: 'Array.prototype.push()', description: 'Add elements to the end of an array' } }, { id: 'pop', fields: { symbol: 'Array.prototype.pop()', description: 'Remove the last element of an array' } }, ], vectors: [[1, 0], [0, 1]], defaults: searchPreset('efficient'), lexical: apiSearchPreset, lexicalSearch: { exact: { fields: ['symbol'] } },});const hits = await search.search({ text: 'Array.prototype.push()', vector: [0, 1] }, { strategy: 'fusion', exact: 'promote',});console.log(hits[0].id); // "push": the exact symbol match ranks first
The API reference covers real embedding models, ColBERT, adapters for existing indexes, filters, pagination and cancellation.
Vibe coding
Paste this prompt into your coding agent. It links the repository, so the agent works from the current README.
Integrate defuss-search into this app: https://github.com/kyr0/defuss-search
Install defuss-search with its peers defuss-lexicalsearch and defuss-vectorsearch, and read the README first. Build the index with createHybridIndex({ documents, vectors, defaults: searchPreset('efficient') }) from defuss-search/indexing, or connect existing indexes with createLexicalRetriever, createVectorRetriever and new HybridSearch({ lexical, vector }).
Embed documents and queries with the same model; defuss-vectorsearch's Winzling runs in the browser. Pass exact: 'promote' so a query that equals a symbol or ID field ranks that document first.
For neural reranking, wrap a ColBERT model with createColbertAdapter from defuss-search/colbert and use searchPreset('reranked'), which skips the model when both sources agree.
send
Exact and semantic search anywhere, for free
Lexical precision, vector recall and an optional ColBERT reranker run wherever JavaScript runs: no search server, no hosted index, no per-query bill.
MIT licensed · benchmark data credited below