How Khoji works

Traditional site search sends every query to a server that holds a copy of your content. Khoji inverts this: the index is built and queried inside the visitor's browser. Here is the full pipeline.

1. Discovery

On first use, Khoji fetches your sitemap.xml(following sitemap-index files up to two levels deep) and collects same-origin URLs up to your plan's page limit. Without a sitemap it falls back to harvesting links from the current page.

2. Extraction and chunking

Pages are fetched four at a time from your own origin — the visitor is effectively pre-loading your site. Khoji strips navigation, scripts, headers and footers, prefers <main> or <article> content, and splits the text into sentence-aligned chunks of roughly 480 characters.

3. Instant keyword index (BM25)

Each chunk goes into an in-memory BM25 index — the same ranking family used by Elasticsearch. This is available within seconds, so search works immediately even before any machine learning happens. Partial typing gets prefix-match credit, which makes search-as-you-type feel natural.

4. Semantic embeddings (WASM)

In a background Web Worker, Khoji loads a quantized all-MiniLM-L6-v2 embedding model (~23 MB, cached by the browser after first load) through transformers.js and ONNX Runtime WebAssembly. Every chunk is converted into a normalized 384-dimensional vector. Your visitor's CPU does this once, then the vectors are stored.

5. Hybrid ranking

A query is scored two ways: BM25 for lexical matches and cosine similarity against chunk vectors for meaning. Scores are blended (45% lexical, 55% semantic), deduplicated per page, and the best chunk becomes the snippet. This is why searching "refund my order" finds a page titled "Returns policy" even though the words don't overlap.

6. Caching

Pages, hashes and vectors persist in IndexedDB. On the next visit, Khoji re-checks page hashes and re-embeds only what changed. A repeat visitor gets a warm, fully semantic index in milliseconds.

Performance characteristics

Site sizeKeyword readySemantic ready (first visit)
30 pages (blog)~2 s~15–30 s
200 pages (docs site)~5 s~1–3 min in background
2,000 pages (Pro)~20 sbackground, cached forever after

All of this happens off the main thread — your site's Core Web Vitals are unaffected, and the widget itself is a 14 KB script that loads async.