How a Search Engine Works: Building a Tiny Inverted Index
Follow a movie search through text processing, an inverted index, and TF-IDF ranking.
- Author
- Faisal Husain
- Published
- Reading time
- 6 min read
- Share

You open a movie app and search for space rescue. How does it find the right movies, and why does one appear above another? I built mini-search to understand that path. It has three main steps:
- Process the text: Turn movie descriptions and search queries into consistent words.
- Build an inverted index: Store which movies contain each word, then use it to find possible matches.
- Score the matches: Put the strongest matches first with TF-IDF.
We will follow four made-up movies through all three steps. The real project gets movie data from TMDB; it does not crawl the web. In this example, we search descriptions only.
| ID | Movie | Description |
|---|---|---|
| D1 | Lost in Orbit | Space rescue finds a lost astronaut in space. |
| D2 | Home from Mars | Space rescue team brings an astronaut home. |
| D3 | New Worlds | Space explorers discover a new planet. |
| D4 | Blue Notes | A jazz drummer prepares for a concert. |
A basic search could read every description and check for the exact phrase space rescue. That finds D1 and D2, but misses D3, which mentions space without rescue. It also reads all four descriptions again for every new search. We can prepare the text once and keep a word lookup instead.
Step 1: Process the text
The Analyzer prepares text both when a movie is added and when someone searches. Both sides must follow the same rules. Otherwise, a query like SPACE, RESCUE! might miss descriptions containing space rescue.
The full pipeline has five stages, in this order:
- Lowercase (
LowercaseFilter.js):Spacebecomesspace. Capitalization no longer changes whether two words match. - Remove punctuation (
PunctuationFilter.js):rescue!becomesrescue. This project removes punctuation rather than replacing it with spaces, somovie-starbecomesmoviestar. - Tokenize (
Tokenizer.js): Split the cleaned text into words.space rescuebecomes["space", "rescue"]. We need individual words to look them up separately. - Remove stop words (
StopWordFilter.js): Drop very common words such asthe. They tend to create large posting lists without helping much to tell movies apart. This is a tradeoff: removing a word can also make some searches less precise. - Stem (
Stemmer.js): Trim word endings so different forms can match:running→runandruns→run. We do this to movie descriptions and search queries, so a search forruncan find either form. The project uses the Porter stemmer fromnatural. It follows suffix rules, so it does not turn every related word into the same stem;ranis an example.
For D1, the first three stages give us:
"Space rescue finds a lost astronaut in space."
→ ["space", "rescue", "finds", "a", "lost", "astronaut", "in", "space"]The output of the full pipeline is also a list of words, after stop-word removal and stemming. The interactive examples in this article deliberately stop after the first three stages. That keeps the words and counts visible: D1 has eight tokens, including a and in. The real project runs all five stages, so its token counts and scores may differ from the examples below.
Step 2: Build the inverted index
After text processing, we could store D1 → space, rescue, finds, …, space. But a person searching starts with a word, not a movie ID. We reverse the direction:
space → D1 (2), D2 (1), D3 (1)
rescue → D1 (1), D2 (1)Each row is a posting list. It says which movies contain the word. The number in parentheses is the raw count in that description: D1 says space twice. The InvertedIndex keeps that count and the field’s token length, which the scorer uses to calculate term frequency. The real index keeps postings by field, so a word in a title can be treated differently from one in a description.
When a movie is added, its fields go through the Analyzer, then each resulting word updates its posting list. At search time, space rescue goes through the same Analyzer. The engine looks up space and rescue and combines their lists:
spacefinds D1, D2, and D3.rescuefinds D1 and D2.- Together they give D1, D2, and D3 as possible results. D4 is not a match.
This is an OR search: a movie can contain either word. An AND search would keep only D1 and D2. mini-search’s SearchEngine uses OR. Looking up a word avoids scanning every description for every query, although we still have to read its posting list and score the movies it names.
Movie descriptions → words → index
Select a word to see which movies contain it.
Movies and words
D1Lost in Orbit — Space rescue finds a lost astronaut in space.
space · rescue · finds · a · lost · astronaut · in · space
D2Home from Mars — Space rescue team brings an astronaut home.
space · rescue · team · brings · an · astronaut · home
D3New Worlds — Space explorers discover a new planet.
space · explorers · discover · a · new · planet
D4Blue Notes — A jazz drummer prepares for a concert.
a · jazz · drummer · prepares · for · a · concert
Word → movie ID (count)
space · posting list
D1 (2) · D2 (1) · D3 (1)
Each entry points to a movie; the count tells us how often the word appears in its description.
Step 3: Score the matches with TF-IDF
Finding D1, D2, and D3 does not tell us their order. D1 says space twice and includes rescue. D2 includes both words once. D3 only includes space. We want the result list to reflect that.
Term frequency (TF) measures how much of one movie’s text is the query word. Here we divide the word’s count by the description’s token count. D1 has space twice in eight tokens, so its TF for space is 2/8. Dividing by length keeps a long description from winning just because it has more room for words.
Inverse document frequency (IDF) measures how much a word helps distinguish movies. space appears in three of our four descriptions; rescue appears in two. A rescue match carries more weight because it narrows the search further. The project uses Math.log(total documents / documents containing the word). A word in every movie gets an IDF of zero; an unseen word has no posting list and contributes nothing.
For each query word, the TFIDFScorer multiplies TF × IDF × field weight, then adds the contributions. Field weights let the real project give a title match more importance than a description match. Our four-movie example searches only descriptions, so every match has the same field weight.
| Movie | Score for space rescue | Why |
|---|---|---|
| D1 · Lost in Orbit | 0.1586 | Both words; space appears twice in eight tokens. |
| D2 · Home from Mars | 0.1401 | Both words appear once in seven tokens. |
| D3 · New Worlds | 0.0479 | Only space appears. |
The app shows Lost in Orbit → Home from Mars → New Worlds. Blue Notes is not a candidate. Try other words in the lab below to see the posting lists, scores, and final order change.
Try a query
A movie containing either word becomes a candidate. Scores decide the order.
Query tokens
space · rescue
Candidates from posting lists
D1 · D2 · D3
1. D1Lost in Orbit — Space rescue finds a lost astronaut in space.
0.1586space: 2/8 × ln(4/3) = 0.0719
rescue: 1/8 × ln(4/2) = 0.0866
2. D2Home from Mars — Space rescue team brings an astronaut home.
0.1401space: 1/7 × ln(4/3) = 0.0411
rescue: 1/7 × ln(4/2) = 0.0990
3. D3New Worlds — Space explorers discover a new planet.
0.0479space: 1/6 × ln(4/3) = 0.0479
rescue: 0/6 × ln(4/2) = 0.0000
A word absent from the index adds nothing. A word in every description has a rarity weight of zero, so some candidates can still score zero.
That is the core of this search engine: analyze text the same way on both sides, use an inverted index to find candidates, and score those candidates to choose an order. The full code and API are in mini-search. This small TF-IDF example explains the idea; Elasticsearch uses BM25 by default.
References
Let's work together
Have a product problem worth solving?
I help teams build fast, dependable products with thoughtful interfaces and strong engineering foundations.