←All posts

How Search Engines Work — Understanding Inverted Indexes and TF-IDF with Interactive Diagrams

A search engine splits text into tokens, uses an inverted index to find candidate documents without scanning every document, and ranks them with TF-IDF or BM25. Interactive diagrams walk through each step.

Search EngineInverted IndexTF-IDFInformation RetrievalData Structures

Introduction

Google Search, code search in an IDE, and product search on a shopping site all have to find the documents that match a query in a large collection quickly and put them in order. They are all built on the same basic search engine techniques.

Three of these techniques are central, and this article shows each one with interactive diagrams:

  1. Inverted Index — a data structure that maps each word to the documents containing it
  2. Boolean Search — processing queries as set operations
  3. TF-IDF / BM25 — scores used to rank search results
Watch the video explainer4:06Synthesized narration · burned-in captions
Show transcript

Why does a search box return useful results almost instantly? Let's look at the inverted index, Boolean search, TF-IDF, and BM25 behind it.

A search engine runs through roughly six stages, from collecting documents to returning results. This article focuses on text analysis, index building, query processing, and ranking. The engine turns raw text into searchable structures ahead of time, then reads only the parts it needs when a query arrives.

The first step in processing a document is text analysis: breaking it into searchable units called tokens. Character normalization aligns Unicode and letter case; in English, whitespace often gives word boundaries. For languages like Japanese, which lack spaces between words, tools such as MeCab or Kuromoji perform morphological analysis.

Next, high-frequency, low-information stopwords are removed. Then stemming or lemmatization maps words like running to run, and cats to cat, absorbing surface variations. At that point, a raw string has become a sequence of terms the engine can search.

A naive full-text search scans every document from start to finish. For N documents with average length L, that is O(N × L), which breaks down as the corpus grows. Instead of searching documents for a term, an inverted index goes from each term directly to the documents that contain it.

For example, cat points to D1 and D2, while sat points to D1 and D3; each list is a posting list. A hash-based term dictionary finds a term's posting list in expected O(1); a search tree over T terms takes O(log T). Real posting lists may also store term frequency, positions, and offsets, not just document IDs.

Once the query is tokenized, Boolean search treats posting lists as sets. For cat AND sat, the intersection of the two lists leaves only D1. Because lists are sorted by document ID, a two-pointer merge processes them in O(n + m).

For cat OR sat, the union returns every document that contains either term. When several terms are joined by AND, the engine starts with the shortest posting list. Keeping intermediate results small is the basic idea of query optimization.

Boolean search says only whether a document matches. Real search also needs an order. TF, term frequency, counts how many times a term appears in a document. IDF divides N, the total number of documents, by df, the number containing the term, then takes the base-10 log. TF-IDF multiplies frequency by rarity, and sums the scores across query terms.

But in TF-IDF, 100 occurrences score 100 times higher than a single one, though relevance isn't necessarily 100 times higher. BM25 makes the effect of TF saturate, and also corrects the advantage that long documents can have. The saturation parameter k1 is commonly 1.2 to 2.0, Lucene's default is 1.2, and b, for length normalization, is commonly 0.75.

In practice, posting lists are sorted document IDs, so delta encoding and variable byte encoding compress them well. Large systems shard the index, scatter the query to every shard, and gather the results. Internally, Lucene optimizes index reads and writes with segmentation, skip lists, and block encoding.

Instant search results come from combining inverted indexes, set operations, and ranking. In the article, you can step through building and searching an inverted index, and a TF-IDF calculation.

Search Engine Architecture

CollectDocumentsAnalyzeTokenizeIndexBuildQueryProcessRankScoreReturnResults

From collecting documents to returning results, the work splits into roughly 6 stages. This article covers text analysis, index building, query processing, and ranking.

Text Analysis — Breaking Documents into Tokens

When a search engine receives a document, it first runs text analysis, turning the raw text into searchable units called tokens.

Raw Text"The Cats Were Running!"Lowercasing"the cats were running!"Tokenization["the", "cats", "were", "running"]Stopword Removal["cats", "running"]Stemming["cat", "run"]

The pipeline has four steps:

1. Character Normalization

Apply Unicode normalization and case conversion, so that "The" and "the" become the same token.

2. Tokenization

Split text into individual words (tokens). In English this is usually done on whitespace. Languages like Japanese, which don't put spaces between words, need another method, such as morphological analysis with tools like MeCab or Kuromoji.

3. Stopword Removal

Remove words like "the", "is", and "on" that appear very often and carry little information for telling documents apart. This shrinks the index and can improve search precision.

4. Stemming / Lemmatization

Reduce words to their root form: "running" → "run", "cats" → "cat". Different forms of the same word then match each other, which improves search recall.

Inverted Index — From Words to Documents

Why We Need an Inverted Index

A naive full-text search scans each document from start to finish looking for the keywords. For NN documents that takes O(N×L)O(N \times L) time (LL being average document length), which stops being practical as the document count grows.

An inverted index removes the need for this full scan.

Forward Index vs Inverted Index

A forward index maps "document → contained terms":

Forward Index:
  D1 → [cat, sat, mat]
  D2 → [cat, dog]
  D3 → [dog, sat, log]

An inverted index reverses this mapping, storing "term → documents containing that term":

Inverted Index:
  cat → [D1, D2]
  dog → [D2, D3]
  log → [D3]
  mat → [D1]
  sat → [D1, D3]

The list of document IDs attached to each term is called a posting list. With a hash-based term dictionary, finding the posting list for a keyword takes expected O(1)O(1) (or O(log⁡T)O(\log T) with a tree/FST-style dictionary over TT terms). Most of the actual query time goes into reading and combining the posting lists once they are found.

Interactive Demo: Building and Searching an Inverted Index

The demo below builds an inverted index from 3 documents and then runs a search query against it, one step at a time.

Building and Searching an Inverted Index

Step through building an inverted index from 3 documents, then using it to find the documents that contain both "cat" and "sat".

Documents

D1"the cat sat on the mat"
D2"the cat and the dog"
D3"the dog sat on a log"

Start with 3 documents, D1–D3, and build an inverted index from them.

1 / 15

Posting List Structure

In real search engines, posting lists often store more than document IDs:

FieldPurpose
Document IDBasic document identification
Term Frequency (TF)Used for ranking
PositionPhrase search, proximity search
OffsetSnippet generation, highlighting

An index that includes position information is called a positional index. Because it knows where each word occurs, it can answer phrase searches like "cat sat".

Query Processing — Boolean Search Model

The query is tokenized the same way as the documents, and the inverted index is then used to find matching documents. The most basic model is Boolean search.

AND (Intersection)cat →D1D2sat →D1D3∩D1OR (Union)cat →D1D2sat →D1D3∪D1D2D3Merging lists sorted by IDO(n + m)n, m: lengths of the two listsNOT (Complement)e.g. "cat NOT dog" → {D1,D2} \ {D2,D3} = {D1}

AND (Intersection)

"cat AND sat" computes the intersection of the posting lists for cat and sat. Since both lists are sorted by document ID, a merge that walks both lists with two pointers processes them in O(n+m)O(n + m) time, where nn and mm are the list lengths.

def intersect(p1, p2):
    result = []
    i, j = 0, 0
    while i < len(p1) and j < len(p2):
        if p1[i] == p2[j]:
            result.append(p1[i])
            i += 1
            j += 1
        elif p1[i] < p2[j]:
            i += 1
        else:
            j += 1
    return result

OR (Union)

"cat OR sat" returns the union of both posting lists — all documents containing either term.

Query Optimization

With several AND conditions, processing the term with the shortest posting list first keeps intermediate results small and makes the query faster.

Ranking — TF-IDF and BM25

Boolean search only says whether a document matches or not. In practice we want to order documents by how relevant they are to the query, and TF-IDF and BM25 are scores for doing that.

TF-IDF(t, d) = TF(t, d) × IDF(t)TF(t, d)Term Frequencycount(t in d)How often does term t appear in document d?×IDF(t)Inverse Document Frequencylog₁₀(N / df(t))How rare is t across all documents?Reading the scoreHigh TF-IDF = frequent in this document but rare overall"the" → low IDF (common everywhere) vs "algorithm" → high IDF (rare, specific)

TF: Term Frequency

TF(t,d)=number of times term t appears in document d\text{TF}(t, d) = \text{number of times term } t \text{ appears in document } d

The more frequently a term appears in a document, the more likely that document is relevant to that term.

IDF: Inverse Document Frequency

IDF(t)=log⁡10 ⁣(Ndf(t))\text{IDF}(t) = \log_{10}\!\left(\frac{N}{\text{df}(t)}\right)

NN is the total number of documents, and df(t)\text{df}(t) is the number of documents containing term tt. Words like "the" that appear everywhere have low IDF, while words like "algorithm" that appear in few documents have high IDF. A word that occurs in only a few documents is a better clue for telling documents apart, and IDF puts that idea into a formula.

TF-IDF Score

TF-IDF(t,d)=TF(t,d)×IDF(t)\text{TF-IDF}(t, d) = \text{TF}(t, d) \times \text{IDF}(t)

For multi-term queries, TF-IDF scores for each term are summed to produce the document's final score.

Calculation Demo

The demo below calculates TF-IDF scores and shows how they decide the ranking.

TF-IDF Score Calculator

Step through computing TF-IDF scores of 3 documents for the query "cat mat" and ranking them.

Query:"cat mat"N=3
D1[cat, sat, mat]
D2[cat, dog]
D3[dog, sat, log]

Calculate TF-IDF scores for the query "cat mat" across N=3 documents (after stopword removal).

1 / 7

BM25: Improving on TF-IDF

TF-IDF has weaknesses. For example, a document where a term appears 100 times gets 100× the score of one where it appears once, but it isn't necessarily 100× as relevant.

BM25 (Best Matching 25) is a refinement of TF-IDF used by many search engines (it has been Lucene's default since 6.0).

IDFBM25(t)=log⁡ ⁣(1+N−df(t)+0.5df(t)+0.5)\text{IDF}_{\text{BM25}}(t) = \log\!\left(1 + \frac{N - \text{df}(t) + 0.5}{\text{df}(t) + 0.5}\right) BM25(t,d)=IDFBM25(t)⋅TF(t,d)⋅(k1+1)TF(t,d)+k1⋅(1−b+b⋅∣d∣avgdl)\text{BM25}(t, d) = \text{IDF}_{\text{BM25}}(t) \cdot \frac{\text{TF}(t,d) \cdot (k_1 + 1)}{\text{TF}(t,d) + k_1 \cdot \left(1 - b + b \cdot \frac{|d|}{\text{avgdl}}\right)}

Here ∣d∣|d| is the length of document dd in terms, and avgdl\text{avgdl} is the average document length. It has two parameters:

  • k1k_1 (commonly 1.2–2.0; Lucene's default is 1.2): Controls TF saturation. If K=k1⋅(1−b+b⋅∣d∣avgdl)K = k_1 \cdot \left(1 - b + b \cdot \frac{|d|}{\text{avgdl}}\right), then the saturation factor TF⋅(k1+1)TF+K\frac{\text{TF} \cdot (k_1+1)}{\text{TF}+K} asymptotically approaches (k1+1)(k_1 + 1) rather than growing without bound
  • bb (commonly 0.75): Document length normalization. Longer documents tend to have higher TF simply because they contain more words; bb corrects for this bias

Practical Topics

Index Compression

Posting lists are sorted integer sequences of document IDs, so they can be efficiently compressed using delta encoding combined with Variable Byte Encoding:

Original list:    [101, 105, 108, 120]
Delta encoding:   [101,   4,   3,  12]  ← smaller numbers
Variable bytes:   efficiently packed into bytes

Distributed Search

Large-scale search engines shard (partition) the index across multiple nodes. Queries are sent to all shards (scatter), and the results from each shard are merged (gather) into the final ranking. Elasticsearch and similar systems work this way.

Apache Lucene and Elasticsearch

Apache Lucene is a full-text search library written in Java and built on inverted indexes; it is the de facto standard in this area. Internally it uses optimizations such as:

  • Segmentation: Split the index into immutable segments, parallelizing reads and writes
  • Skip lists: Jump ahead within a posting list, which speeds up operations like intersection
  • Block encoding: Encode posting lists in blocks so they can be processed quickly with SIMD

Elasticsearch is a distributed search engine built on Lucene, providing REST APIs, clustering, and near-real-time indexing.

Summary

An inverted index stores a "term → posting list" mapping, and looking up a term in the dictionary takes expected O(1)O(1) with a hash or O(log⁡T)O(\log T) with a tree/FST. Boolean search answers queries with set operations on sorted posting lists. TF-IDF and BM25 combine how often a term appears in a document with how few documents contain it, and use the result to rank matches.

When you type a few words into a search box and the results come back right away, this is the kind of work happening underneath: inverted index lookups followed by score calculation.

References