Every fast search engine, from web giants to a chatbot knowledge base running in a tab, relies on the same data structure: the inverted index. It maps each term to the documents containing it, turning a full scan of every document into a handful of dictionary lookups.
The problem it solves
Suppose your knowledge base holds 500 fragments and a user asks about 'gradient descent'. Without an index, you scan all 500 fragments for both words on every query. With an inverted index, you look up 'gradient' and 'descent' directly and score only the fragments in those two posting lists — often a dozen instead of 500.
Structure of a posting list
The index is a map from normalized term to a list of postings. Each posting records a document id and whatever the ranker needs:
// term -> Map(docId -> term count in that document)
const index = new Map([
['gradient', new Map([[3, 4], [17, 2], [42, 1]])],
['descent', new Map([[3, 3], [42, 2], [101, 5]])],
]);
For TF-IDF and BM25 scoring you need the term count per document, the document frequency (posting list length), and the total document count. Store document lengths in a side array for length normalization.
Building the index
Construction is a single pass over the corpus. Normalize each document with stop-word removal and stemming, count term occurrences, and append postings:
function buildIndex(docs) {
const index = new Map();
const docLengths = [];
for (const doc of docs) {
const terms = normalize(doc.text);
docLengths[doc.id] = terms.length;
const counts = new Map();
for (const t of terms) counts.set(t, (counts.get(t) || 0) + 1);
for (const [t, c] of counts) {
if (!index.has(t)) index.set(t, new Map());
index.get(t).set(doc.id, c);
}
}
return { index, docLengths, docCount: docs.length };
}
A few hundred documents index in milliseconds at page load. For larger collections, build once and persist the serialized index in IndexedDB so repeat visits skip the work.
Query processing
To answer a query, normalize it with the same pipeline, fetch one posting list per term, and accumulate scores:
- Look up each query term in the index.
- Union the document ids across posting lists.
- Score each candidate with BM25 or TF-IDF.
- Sort and return the top results.
Because only documents containing at least one query term are scored, query time grows with the match set, not the corpus. Field-weighted variants keep one posting list per field so title matches can outrank body matches.
Memory footprint
An inverted index for a small knowledge base is tiny. A 500-fragment corpus with a 5,000-term vocabulary typically serializes to tens of kilobytes — far smaller than a single embedding matrix. That is why keyword retrieval is the ideal first stage on device: it runs instantly, works offline per your offline-first architecture, and narrows candidates before any expensive semantic scoring.
When to move beyond it
Inverted indexes match exact terms only. Synonyms, paraphrases, and typos need help from query expansion, typo tolerance, or dense retrieval. Treat the inverted index as the fast foundation, then layer semantic signals on top for the queries keywords miss.