Design search over a catalogue of fifty million products. How is the index built, how are results ranked, and how does the index stay fresh?
An inverted index maps terms to posting lists, built by an analysis chain that must run identically over documents and queries, ranked by BM25 plus business signals, and fed by a change stream of denormalised documents with reindexing behind an alias treated as routine.
What the interviewer is scoring
- Whether the analysis chain is described as a chain, and applied symmetrically to documents and queries
- Does the candidate treat the index as a derived view rather than a system of record
- That relevance is separated into textual scoring and business signals instead of collapsed into one score
- Can they explain why the index is near-real-time and what the freshness lag is bought with
- Whether a mapping or analyser change is planned for, given that it cannot be applied to existing documents in place
Answer
The data structure everything else follows from
Search is fast because the index is stored the wrong way round. Instead of documents pointing at their words, an inverted index maps each term to a posting list of the documents containing it, with per-document term frequency and often the positions where it occurred. Answering wireless keyboard becomes: fetch two posting lists, intersect them, score the survivors. Positions are what let you distinguish a phrase query from a bag of words, and they are also why enabling them roughly doubles index size.
Posting lists are sorted by document id and compressed with delta encoding, so a list of ids becomes a list of small gaps that pack into few bits, and skip pointers let an intersection jump forward instead of walking. This matters for the interview only insofar as it explains a design constraint you will hit: the index is optimised for reading term-ordered data and is hostile to updating a single document in place. Everything awkward about keeping search fresh descends from that.
Fifty million products is not a large index by modern standards — it is tens of gigabytes, which one machine can hold — so shard for query concurrency, indexing throughput and failure isolation rather than because the data will not fit. Each shard is a self-contained index, a query fans out to all of them and the coordinating node merges the top results.
Analysis is where searches silently fail
An analyser is a pipeline: character filters normalise the raw text, a tokeniser splits it into tokens, then token filters lowercase, fold accents, strip or keep stopwords, apply stemming or lemmatisation, and expand synonyms. Wireless Keyboards (US) becomes something like wireless, keyboard, us. Only the output is stored, and the original string is unrecoverable from the index — the searchable representation and the displayed representation are different artefacts.
The consequence is the single sharpest thing to say about search design: the same chain must run over the query. If documents were stemmed and the query is not, a user searching keyboards will not match the indexed token keyboard, and the failure is total and silent — no error, no warning, an empty result page for a product that is definitely in the catalogue. Deliberate asymmetry is legitimate in exactly one direction, expanding synonyms at query time rather than index time so that changing the synonym list does not require rebuilding the index, and if you do that you must reason about how the expansion interacts with scoring.
Product catalogues need more than one analysis of the same field, which is why serious designs index a field several ways: a lowercased analysed field for recall, an unanalysed keyword field for exact matching, faceting and sorting, an edge-n-gram field for as-you-type completion. SKUs and part numbers are the case that catches people, because the standard tokeniser will happily split KB-2100/XT into fragments and a customer typing the full code exactly will not find it.
Ranking, in two layers
Textual relevance in Lucene-based engines is BM25, which has been the default similarity since Lucene 6. It scores a document higher when the query term appears often in it, higher when the term is rare across the corpus, and lower when the document field is long — the last of these being what stops a thousand-word description outranking a precise title. Its two parameters are worth naming because they are the tuning surface: k1 controls how quickly repeated occurrences stop adding score, and b controls how strongly length normalisation applies. Explaining BM25 as "TF-IDF with saturation and length normalisation" is an accurate and interview-sized summary.
BM25 alone ranks a catalogue badly, and saying so is what separates a search design from a search tutorial. It has no opinion about whether a product is in stock, well reviewed, profitable or bought by anyone. So the second layer combines the text score with business signals: field boosts so a title match beats a description match, availability as a hard filter rather than a score, popularity and conversion rate as multiplicative boosts, recency for new arrivals. Keep the layers separate — retrieve and score a candidate set with the text model, then rescore the top few hundred with the expensive business model. This is also the structure a learned ranker slots into later, as a rescorer over a cheap first pass, and it is why you should not tangle merchandising rules into the scoring formula.
Keeping the index fresh
Segments are immutable. New documents accumulate in an in-memory buffer, a refresh flushes them into a new searchable segment, and background merges combine small segments into larger ones. An update is an add plus a tombstone marking the old document deleted, and the space is only reclaimed at merge. Two things follow: search is near real-time, with the refresh interval — one second by default in Elasticsearch — being exactly the lag you are choosing; and a high-churn update pattern costs you merge throughput and index bloat rather than being free.
The ingestion path should be a change stream, not a nightly job and not a write from application code alongside the database write. Products live in a relational store; a search document is a denormalised join of product, category, brand, price, inventory and computed signals. Emit changes to those tables through the outbox pattern or change data capture, join and enrich in a stream processor, and write to the index in bulk batches. Bulk is not an optimisation here, it is the supported mode — per-document requests at catalogue scale will bottleneck on request overhead long before the index does.
Two ordering hazards are worth pre-empting. Concurrent updates to the same product can arrive out of order, so carry a monotonic version from the source and let the engine reject a stale write rather than letting last-writer-wins corrupt a row. And a price change that must be visible in seconds and a description change that can wait an hour do not belong in the same priority queue.
You cannot change the analyser on an existing index
This is the operational fact that catches teams, and naming it demonstrates you have run search rather than read about it. Because the index stores analyser output, changing the analyser, adding a stemmer or altering a field's type does not apply to documents already written. The engine will refuse the mapping change or, worse, accept it for new documents only, leaving you with an index where half the documents were tokenised one way and half another.
The answer is to make reindexing routine. Index into a versioned name, point an alias at it, and have clients only ever talk to the alias. To change analysis, build a new index from the source of truth, run it in parallel, compare relevance on a sample of real queries, then swap the alias atomically and keep the old index until you are confident. This is only possible if the search index is genuinely derived — if any field exists only in the index and cannot be regenerated from upstream, you have made the index a system of record by accident and reindexing has become a data migration.
The index is a derived, disposable view whose contents are analyser output rather than your text, so the two things you must be able to do on demand are rebuild it from the source of truth and prove that the same analysis chain ran over the query.
Likely follow-ups
- A merchandiser wants a specific product pinned first for one query. Where does that rule live so it does not corrupt the scoring model?
- How would you evaluate whether a relevance change is an improvement before shipping it?
- The catalogue has thirty facets and users filter on four at once. What does that do to your query and to caching?
- How do you handle a query with a typo, and where does that fit relative to synonyms and stemming?
Related questions
- Site search converts poorly. How would you improve relevance when conversion is the metric you are judged on?hardAlso on search and relevance6 min
- Where would you start if you were handed a retailer's search and told to improve it?mediumAlso on search and relevance4 min
- The operations team says there is no rule for this, they just use judgement. How do you model that?hardSame kind of round: design6 min
- Marketing want to raise the price of a plan a million subscribers are already on, and change what it includes. What has to happen in the catalogue?hardSame kind of round: design5 min
- You have been handed a scope and a budget, and the budget cannot buy the scope. What do you put in front of the customer?hardSame kind of round: design5 min
- You have six teams building one product and they keep blocking each other. How do you coordinate the dependencies, and what do you make of frameworks like SAFe?hardSame kind of round: design6 min
- How would you aggregate exposure across a portfolio so that the number can still be defended a year later?hardSame kind of round: design6 min
- How do you decide whether to build a capability in-house or buy it?mediumSame kind of round: design4 min