When you type a query into a search box, how does the engine decide which document matches best? One of the oldest and most durable answers is TF-IDF: term frequency times inverse document frequency. It powers keyword search, chatbot retrieval, and document ranking, and it runs easily in a browser with no model download.

Term frequency: how often the word appears

Term frequency (TF) counts how many times a query term appears in a document. A page that mentions 'photosynthesis' six times is more likely about photosynthesis than a page that mentions it once. Raw counts are usually dampened with a logarithm or saturation curve so that the twentieth repetition counts for less than the first.

A common variant is tf = 1 + log(count) for terms that appear at least once, and zero otherwise. This keeps frequent terms influential without letting long documents dominate purely by length.

Inverse document frequency: how rare the word is

Inverse document frequency (IDF) measures how informative a term is across the whole collection. If 'the' appears in every document, it tells you nothing. If 'chlorophyll' appears in three documents out of ten thousand, a match is strong evidence.

The standard formula is idf(t) = log(N / df(t)), where N is the total number of documents and df(t) is the number of documents containing the term. Rare terms get large weights; ubiquitous terms get weights near zero.

A worked example

Suppose your knowledge base has 1,000 documents. The query is 'solar panel efficiency'.

  • 'solar' appears in 50 documents: idf = log(1000/50) = 3.0
  • 'panel' appears in 200 documents: idf = log(1000/200) = 1.6
  • 'efficiency' appears in 10 documents: idf = log(1000/10) = 4.6

A document matching 'efficiency' earns nearly triple the weight of one matching 'panel'. The final score for each document is the sum of tf * idf over the query terms. That single sum is the ranked list.

From TF-IDF to BM25

Pure TF-IDF has two weaknesses: it over-rewards very long documents, and term frequency grows without bound. BM25 fixes both with length normalization and TF saturation, which is why it remains the default keyword ranker in most retrieval stacks, including field-weighted browser retrieval. If you understand TF-IDF, BM25 is one short step further.

Implementing TF-IDF in the browser

A minimal implementation needs only an inverted index mapping each term to the documents that contain it, plus per-document term counts:

function tfidfScore(queryTerms, docId, index, docCount) {
  let score = 0;
  for (const term of queryTerms) {
    const posting = index.get(term);
    if (!posting || !posting.has(docId)) continue;
    const tf = 1 + Math.log(posting.get(docId));
    const idf = Math.log(docCount / posting.size);
    score += tf * idf;
  }
  return score;
}

For a few hundred documents this runs in well under a millisecond. Precompute the IDF values once at index time so queries only do lookups and additions.

Limitations worth knowing

TF-IDF matches exact terms, so 'car' never matches 'automobile'. It also ignores word order and meaning. Dense embeddings cover the synonym problem, and the strongest small systems combine both signals with query expansion. But as a fast, explainable, dependency-free baseline, TF-IDF is still the right place to start.

Try it yourself

ReLU.chat starts retrieval with keyword matching before any optional model loads. Ask a question in the live demo or read how the full pipeline works.