Markdown Editor

Full-Text Search Engine System Design Template

Inverted index, segments, real-time ingestion vs queries, document and time sharding, replicas, BM25 plus reranker, autocomplete boundary, relevance evaluation

How to use: Use it when designing keyword search over your own corpus or reviewing a search cluster. Fill the freshness target, corpus size and query rate first, because they fix shard count and refresh policy. The main pitfall is tuning ranking without a judged query set and offline metrics such as nDCG.

Preview

System Design: [Full-text search service name]

Reference design for a full-text search engine: inverted index built from immutable segments, near-real-time ingestion running concurrently with queries, sharding by document or by time, replicas for throughput and availability, BM25-style ranking with a second-phase reranker, the boundary with autocomplete, and an offline and online relevance evaluation loop. It follows the structure of the System Design Document template, adds a 45-minute interview path, access-pattern and failure-mode tables, and is pre-filled with a worked design to adapt. Replace every [bracketed] value and every row marked Example; values marked Example are assumptions, not benchmarks.

Document control

FieldValue
Document ID[SDD-FTS-001]
System / feature[Full-text search]
Version[0.1]
Status[Draft / In review / Approved / Implemented / Superseded]
Owner[@owner]
Author(s)[@author]
Reviewers[@reviewer-1], [@reviewer-2]
Last updatedYYYY-MM-DD
Classification[Internal / Confidential]
Corpus[What is indexed, languages, owner of the source of truth]
Engine[Lucene-based engine / in-house / managed service, with version]
Related[Search autocomplete design], [Relevance judging guide], [ADR: shard count]

Approvals

RoleNameDecisionDate
Tech lead[@name][Approved / Changes requested]YYYY-MM-DD
Staff / principal engineer[@name][Decision]YYYY-MM-DD
SRE / operations[@name][Decision]YYYY-MM-DD
Relevance / data science[@name][Decision]YYYY-MM-DD
Security (access control in results)[@name][Decision]YYYY-MM-DD

Revision history

VersionDateAuthorChange summary
0.1YYYY-MM-DD[@name]First draft for design review
0.2YYYY-MM-DD[@name]Example: Added time-based indices for the message corpus
0.3YYYY-MM-DD[@name]Example: Added second-phase reranker after judged-set evaluation
1.0YYYY-MM-DD[@name]Example: Approved

Contents

1. Overview

1.1 Problem statement

[Example: Users search 2 billion documents (posts, comments and help articles) through a relational database LIKE query on titles only. Queries time out above a few million rows, body text is not searchable, results are ordered by date only, and a new post takes up to an hour to appear because a nightly job copies data into a secondary table.]

1.2 Proposed solution

[Example: A change stream from the primary database feeds an indexing pipeline that analyses text (tokenise, normalise, stem per language) and writes into a document-partitioned inverted index. Each shard is a set of immutable segments plus one small in-memory segment that receives new documents and becomes searchable within one second. Queries go to a broker that fans out to one copy of every shard, gathers top results, and passes the best candidates to a reranking step. Each shard has three copies across zones. A judged query set and online experiments measure every ranking change.]

1.3 Decisions requested from reviewers

IDQuestionRecommended optionSection
Q-01Example: Partition by document or by term?By document, with time-based indices for the message corpus7.3
Q-02Example: Freshness target?Searchable within 1 s for posts, 15 min for help articles7.2
Q-03Example: Ranking?BM25 first phase, learned reranker on top 2007.4
Q-04Example: Does this service also serve typing suggestions?No, separate autocomplete service7.5
[Q-nn][Question][Option][Section]

1.4 45-minute interview path

Use this order when the document serves an interview or a short review.

MinutesCoverOutput on the whiteboard
0 to 5Scope: keyword search with filters and ranking; freshness target; below-the-line itemsCore flows, freshness number
5 to 10Estimates: corpus, index size, write rate, query rateShard count and copies, each with a reason
10 to 15API: search with filters and paging, index document, deleteThree calls
15 to 25High-level design: ingestion pipeline, shards and replicas, broker, rerankerOne diagram
25 to 40One deep dive: inverted index and real-time ingestion (default), or sharding, or rankingOptions, choice, reason
40 to 45Failures, relevance evaluation, 10x changeFailure rows, metric names

Draw the inverted index early (term to postings list) and explain why it makes keyword lookup cheap. Most weak answers stop at "use Elasticsearch"; strong answers explain segments, refresh and fan-out.

2. Requirements

2.1 Core functional requirements

IDRequirementPriority
FR-01Example: Keyword search over title and body with boolean operators and phrase queriesMust
FR-02Example: Filters on author, type, language and date rangeMust
FR-03Example: Results ranked by relevance, with a sort-by-date optionMust
FR-04Example: New and edited documents searchable within 1 s; deletes hidden within 1 sMust
FR-05Example: Results respect the viewer's access rightsMust
FR-06Example: Highlighted snippets showing matched termsShould
FR-07Example: Facet counts for type and languageShould
[FR-nn][Requirement][Must / Should / Could]

2.2 Non-functional requirements

IDQualityTargetMeasured as
NFR-01Query latencyExample: p99 at or below 200 ms at the brokerServer timing
NFR-02FreshnessExample: p99 at or below 1 s from commit to searchableSynthetic canary documents
NFR-03AvailabilityExample: 99.95% of queries answered; partial results flaggedProbes and error budget
NFR-04RelevanceExample: nDCG@10 on the judged set does not drop by more than 1% per releaseOffline evaluation
NFR-05DurabilityExample: No acknowledged document lost; index rebuildable from sourceRebuild drill
[NFR-nn][Quality][Target][Measurement]

2.3 Below the line (out of scope)

  • [Example: typing suggestions and query completion (see 7.5 and the autocomplete design)]
  • [Example: semantic or vector search (planned as a hybrid extension, see 10)]
  • [Example: web crawling; documents arrive from our own databases only]
  • [Example: cross-language retrieval]
  • [Item deferred, with the release that will take it]

3. Capacity estimation

3.1 Assumptions

InputSymbolValueSource
DocumentsNNExample: 2 billionSource database
Average indexed text per documentssExample: 2 KBSample of 10 000 documents
Index size relative to raw text (postings, positions, doc values)rrExample: 0.5Measured on a test shard
New or changed documents per daywwExample: 50 millionChange stream metrics
Peak to average write ratiokkExample: 3Change stream metrics
Peak queries per secondQQExample: 2 000Product forecast
Target shard sizezzExample: 50 GBRecovery time and merge cost tests
Shard queries one copy serves per secondccExample: 1 000Load test on target hardware

3.2 Index size and shard count

I=N×s×r=2×109×2 KB×0.5=2 TBI = N \times s \times r = 2 \times 10^{9} \times 2 \ \text{KB} \times 0.5 = 2 \ \text{TB}
P=Iz=2 TB50 GB=40 primary shardsP = \frac{I}{z} = \frac{2 \ \text{TB}}{50 \ \text{GB}} = 40 \ \text{primary shards}

Consequence: 40 shards, sized so a lost copy recovers in minutes, not hours. Plan for growth by choosing the shard count for the two-year corpus or by using time-based indices (7.3) so new data goes to new shards.

3.3 Write rate

Wavg=5×10786 400≈580 docs/s,Wpeak=k×Wavg≈3×580≈1 740 docs/sW_{avg} = \frac{5 \times 10^{7}}{86\,400} \approx 580 \ \text{docs/s}, \qquad W_{peak} = k \times W_{avg} \approx 3 \times 580 \approx 1\,740 \ \text{docs/s}

Consequence: the write rate is moderate, but every write must become visible within 1 s, so the cost is in frequent refreshes and in merging the many small segments they create, not in raw throughput.

3.4 Query fan-out and replicas

With document partitioning every query visits one copy of every shard:

F=Q×P=2 000×40=80 000 shard queries/s,Fc=80 0001 000=80 busy copiesF = Q \times P = 2\,000 \times 40 = 80\,000 \ \text{shard queries/s}, \qquad \frac{F}{c} = \frac{80\,000}{1\,000} = 80 \ \text{busy copies}

That is 2 copies per shard for throughput; a third copy covers a zone failure. Consequence: 3 copies per shard (120 shard copies, 6 TB of index). At 10 times the query rate we add replicas, not shards; at 10 times the corpus we add shards.

3.5 Summary

QuantityEstimateDesign consequence
Index size~2 TB primary, ~6 TB with copies40 shards of ~50 GB
Writes~1 740 docs/s at peak1 s refresh, merge policy tuned for small segments
Shard queries80 000/s3 copies per shard, adaptive copy selection
Tail latencyBroker waits for the slowest of 40 shardsPer-shard timeout and partial results

4. API design

4.1 Endpoints

InterfaceOperationPurpose
Public APIPOST /v1/searchQuery with filters, sort, page cursor
InternalChange stream consumerIndex, update and delete documents from the source
AdminPUT /v1/indices/{name}/mappingVersioned field and analyser definitions
AdminPOST /v1/indices/{name}/reindexRebuild into a new index, then switch an alias

4.2 Request and response examples

A search request with a filter and a cursor, and its response:

{"q": "refund \"late delivery\"", "filters": {"type": ["post", "article"], "lang": "en", "created_after": "2026-01-01"}, "sort": "relevance", "page_size": 20, "cursor": null}
{"total_estimate": 18420, "partial": false, "results": [{"id": "doc_7c1e", "score": 12.84, "title": "[Title]", "snippet": "our <em>refund</em> policy for <em>late delivery</em> orders", "created_at": "2026-09-14T10:02:00Z"}], "next_cursor": "[opaque cursor]", "index_version": "posts-v12"}

4.3 Errors and limits

CaseStatusBody type
Query syntax error400problems/invalid-query with the position
Too many clauses or wildcard on a short prefix422problems/query-too-expensive
Some shards timed out200 with "partial": trueNot an error; the client may show a notice
Too many requests429 with Retry-Afterproblems/rate-limited

Deep pagination uses a cursor (search after the last sort value), not offsets: an offset of 10 000 forces every shard to rank 10 020 results.

5. Data model

5.1 Entities

The source database owns documents; the index is a derived, rebuildable view.

5.2 Index mapping

FieldTypeAnalysisUsed for
titleTextLanguage analyser with stemming; also an exact sub-fieldMatching, boosting
bodyText with positionsLanguage analyserMatching, phrases, snippets
type, lang, author_idKeywordNoneFilters, facets
created_atDateNoneFilters, sort, time-based routing
acl_principalsKeyword, multi-valuedNoneAccess filter
quality_scoreNumeric doc valueNoneRanking feature

5.3 Access patterns

QueryKey or index that serves itPartition key and rationale
Keyword query on bodyTerm dictionary, then postings lists per sharddoc_id hash; spreads documents evenly, every shard answers every query
Filter by type or languageKeyword postings or doc values intersected with the text matchSame shard as the document, so filters are local
Recent messages onlyTime-based index per weekcreated_at week; queries for "last 7 days" touch 1 or 2 indices
Get document for displaySource database by doc_idPrimary key in the source, not the index
Access-filtered resultsacl_principals postingsStored with the document so the filter runs on the shard
Delete or updateMark old version deleted in its segment, add new versionRouting by doc_id finds the one shard holding it

6. High-level design

ComponentResponsibilityScales by
Change streamOrdered changes with document versionPartitions
Indexing workersAnalyse text, route to shard, retry on failureStateless instances
Shard copyHold segments, apply writes, answer shard queriesCopies per shard
Query brokerParse, fan out, merge top results, enforce timeoutsStateless instances
RerankerRe-score top candidates with richer featuresStateless instances, model cache
Relevance evaluationJudged sets, metrics, experiment analysisBatch

7. Detailed design

7.1 Deep dive: the inverted index and segments

An inverted index maps each term to a postings list: the documents that contain it, with term frequency and, for phrase queries, positions. Query evaluation intersects (AND) or unions (OR) postings lists, so cost follows the length of the lists touched, not the size of the corpus.

OptionHow updates workCost
One mutable indexUpdate postings in placeLocks, fragmentation, hard to compress
Immutable segmentsNew documents go to a new small segment; deletes are bitmaps; background merges combine segmentsMany small segments until merged
Rebuild in batch and swapBuild a full new index, switch atomicallyFreshness measured in hours

Choice: immutable segments, the model used by Lucene and its derivatives. Reason: a segment never changes after it is written, so it can be compressed, cached and read without locks; updates are a delete mark plus a new version in a newer segment.

7.2 Deep dive: real-time ingestion against concurrent queries

The hard part is making a document searchable within 1 s while thousands of queries read the same shard.

OptionFreshnessEffect on queries
Commit to disk per documentImmediateVery slow, huge number of tiny segments
Periodic refresh: buffer in memory, open a new searchable segment every intervalInterval (for example 1 s)Small segments to merge; each refresh is a short pause
In-memory writable segment with a single writer and many lock-free readersSub-secondCareful memory visibility rules; engine-specific

Choice: periodic refresh every 1 s for posts and 15 min for help articles, with a transaction log for durability between refreshes. Reason: it meets NFR-02 with a standard engine. A custom single-writer design is only worth building when sub-second freshness at very high write rates is the product (see 7.8).

Version numbers from the source make ingestion idempotent: an operation with a version lower than the indexed one is ignored, so replays after a failure are safe.

7.3 Deep dive: sharding by document, term or time

OptionQuery costWrite costMain risk
By document (hash of doc_id)Every query hits every shardOne shard per documentTail latency of the slowest shard
By termA query hits only shards holding its termsOne document updates many shardsHot terms, very costly updates
By time (one index per week)Recent-only queries hit few indicesWrites go to the newest indexOld indices still queried for "all time"

Choice: by document within each index, plus time-based indices for the message corpus. Reason: term partitioning makes every document write a distributed write; document partitioning keeps writes local and scales query throughput with replicas. Time indices let old weeks be merged to one segment, moved to cheaper nodes and dropped by retention without deletes.

Control the fan-out tail: per-shard timeout (Example 110 ms, the shard budget in section 8), return partial results with a flag, and pick the copy with the shortest queue (adaptive replica selection) rather than round robin.

7.4 Deep dive: ranking

The first phase scores every matching document cheaply on each shard; the second phase re-scores a few hundred candidates with richer signals.

BM25 is the standard first-phase text score. For query terms qiq_i, document DD with length ∣D∣|D| and average length avgdlavgdl:

score(D,Q)=∑iIDF(qi)⋅f(qi,D) (k1+1)f(qi,D)+k1(1−b+b∣D∣avgdl)\text{score}(D, Q) = \sum_{i} \text{IDF}(q_i) \cdot \frac{f(q_i, D)\,(k_1 + 1)}{f(q_i, D) + k_1 \left(1 - b + b \frac{|D|}{avgdl}\right)}

k1k_1 controls how quickly repeated terms stop adding score and bb controls length normalisation; Lucene's defaults are k1=1.2k_1 = 1.2 and b=0.75b = 0.75. Tune them only against the judged set.

OptionQualityCost
BM25 only, with field boostsGood baselineCheapest
BM25 plus static features (quality, recency) in a functionBetter for fresh contentLow
Two phase: BM25 top 200 per query, then a learned rerankerBest measured qualityModel serving, feature pipeline

Choice: two phase. Reason: the reranker sees only 200 candidates per query, so its cost is bounded and independent of corpus size. Note that IDF is computed per shard; with hash partitioning shard statistics are close to global, but small shards or time indices can skew them. Use global statistics if scores vary visibly between shards.

7.5 Autocomplete boundary

Typing suggestions and full search look similar but have different contracts: suggestions answer every keystroke in tens of milliseconds from a precomputed prefix structure built from popular queries, while full search evaluates the whole query against the corpus. Keep them separate services. Search contributes its query logs to the suggestion pipeline described in Search Autocomplete System Design (system-design/design-search-autocomplete), and suggestions submit a full query here when selected.

7.6 Relevance evaluation

LayerMethodMetric
OfflineJudged set of Example 1 000 queries with graded labels per resultnDCG@10, recall@100
OfflineKnown-item queries (a user searched for a document they later opened)MRR
OnlineA/B test or interleaving of two rankersClick-through on top 3, reformulation rate, abandonment
GuardrailLatency and zero-result ratep99, share of queries with no result

Sample judged queries from logs across head, torso and tail. Record the engine, mapping and model version with every evaluation run so results can be compared over time.

7.7 Deletes, access control and reindexing

  • Deletes mark the document in a per-segment bitmap and disappear from results at the next refresh; space is reclaimed by merges.
  • Access filters run on the shard using acl_principals; the API also checks access when it loads documents from the source, so a stale ACL in the index cannot leak a document. Total estimates, facet counts and snippets still come from the index, so ACL changes travel on the same 1 s path as document changes and snippets are shown only for documents that pass the load-time check.
  • Mapping changes that alter analysis need a full reindex: build a new index from the source, replay the change stream from the build start, then switch the alias. Keep the old index until the new one passes evaluation.

7.8 How real systems do it

How real systems do it (paraphrased, not copied). Busch, Gade, Larson, Lok, Luckenbill and Lin describe Earlybird, the retrieval engine behind Twitter's real-time search (ICDE 2012). It is built on Lucene but organises postings for traversal from newest to oldest, keeps several segments per server of which only one accepts new documents at a time, converts a full segment into a read-optimised form, and uses a single-writer, many-reader concurrency model with targeted memory barriers so indexing and queries run at once without locks. Front-end servers fan queries out to the partitions and merge results. The lesson for this design: when freshness is the product, make the write path single-threaded per segment and let readers proceed without coordination. Read the paper in 12.3; this box keeps only the general idea.

8. Scalability and performance

ConcernApproachLimit before redesign
Query throughputMore copies per shardNetwork and broker merge cost
Corpus growthMore shards via new time indices or a splitExample: shards above 50 GB slow recovery
Tail latencyTimeouts, adaptive copy selection, partial resultsFan-out above about 100 shards per query
Expensive queriesClause limits, no leading wildcards, query cost estimate[Limit]
Merge pressureTiered merge policy, throttled I/ORefresh interval below 1 s at high write rates
CachingFilter cache per segment, result cache for head queriesHit rate falls with personalised filters

Latency budget for a search (target p99 200 ms):

StepBudget
API, auth, query parse10 ms
Fan-out and shard evaluation (slowest shard)110 ms
Merge top results at the broker10 ms
Rerank 200 candidates40 ms
Load display fields and snippets20 ms
Headroom10 ms

9. Reliability, security and operations

9.1 Failure modes

ComponentFailureDetectionBlast radiusMitigationRTO
Shard copyNode lostCluster healthFewer copies for some shardsPromote replica, rebuild copy from a peerExample: 10 min
All copies of a shardZone or data lossShard unassignedPartial results for every queryCopies in 3 zones; restore snapshot and replay change streamExample: 1 h
Change stream consumerLagCanary document freshnessStale results everywhereAutoscale workers, alert on lag above 30 sExample: 15 min
Bad mapping or analyser changeRecall dropsEvaluation gate, zero-result rateWhole indexNew index plus alias switch, keep the old oneExample: 5 min
RerankerTimeout or errorError rateRanking qualityFall back to first-phase orderExample: immediate
Query of deathNode CPU or memory exhaustedCircuit breaker tripsShards on that nodeQuery cost limits, per-request memory breakerExample: 5 min
[Component][Failure][Signal][Scope][Mitigation][Time]

9.2 Security and privacy

ThreatControl
Results leak private documentsACL filter in the shard query plus check at load time (7.7)
Query injection into the engine syntaxParse user input with a restricted grammar; never pass raw strings to the engine query DSL
Resource exhaustion by crafted queriesClause limits, timeouts, rate limits per caller
Deleted personal data remains searchableDelete propagates within 1 s; merges and snapshot retention documented for erasure requests
Query logs contain personal dataRetention limit, pseudonymised user ids
[Threat][Control]

9.3 Observability

SignalMetricAlert threshold
FreshnessCanary commit-to-searchable timep99 above 1 s for 10 min
LatencyBroker p99, slowest shard p99Above 200 ms for 10 min
Partial resultsShare of responses with partial: trueAbove 0.5%
Zero resultsShare of queries with no hitDoubling vs. 7-day baseline
RelevanceOnline click-through on top 3Drop of 5% after a release

9.4 Cost estimate

ItemUnit costQuantityMonthly cost
Search nodes[USD per node-month]Example: 120 shard copies at 4 per node = 30 nodes[USD]
Broker and reranker[USD per node-month][Nodes][USD]
Snapshots[USD per TB-month][TB][USD]
Total[USD]

10. Trade-offs and alternatives

DecisionOptionsChosenWhyWhat we give up
EngineDatabase full-text feature; Lucene-based engine; in-houseLucene-based engineMature segments, analysers, BM25Another stateful cluster to run
PartitioningDocument; term; timeDocument plus time indicesLocal writes, simple scalingEvery query fans out
FreshnessBatch rebuild; refresh; custom real-time segments1 s refreshMeets target with standard engineMerge cost, small segments
RankingBM25; BM25 plus features; two phaseTwo phaseMeasured quality gain at bounded costModel pipeline to maintain
Lexical vs hybridLexical only; add vector retrievalLexical now, hybrid laterJudged set shows lexical meets targetWeaker on paraphrased queries
What changes at 10x / 0.1x10x queries: more copies and a result cache; 10x corpus: more shards and tiered storage for old time indices. 0.1x: a database full-text index may be enoughReview yearlySize drives the build or buy line[Note]
[Decision][Options][Chosen][Why][Cost]

11. Delivery plan and risks

11.1 Milestones

MilestoneScopeExit criterionTarget date
M1Index help articles, BM25 onlyJudged set baseline recordedYYYY-MM-DD
M2Change stream ingestion for posts, 1 s refreshFreshness within NFR-02YYYY-MM-DD
M3ACL filtering and security reviewNo leak in access testsYYYY-MM-DD
M4Reranker behind an experimentnDCG@10 gain and online winYYYY-MM-DD

11.2 Rollout and rollback

[Example: Search reads through an alias. New indices are built in parallel, evaluated offline, then shadow-queried with production traffic for 3 days. Switching the alias is the release; switching it back is the rollback.]

11.3 Risks

IDRiskLikelihoodImpactMitigationOwner
R-01Example: Shard count too low for corpus growthMediumHighTime indices; split procedure tested[@name]
R-02Example: Stemming hurts precision in some languagesMediumMediumPer-language analysers checked on judged sets[@name]
R-03Example: ACL changes lag behind sourceLowHighLoad-time access check[@name]
[R-nn][Risk][L / M / H][L / M / H][Mitigation][@name]

12. Appendices

12.1 Open questions

IDQuestionOwnerDue
OQ-01Example: Do we need global IDF statistics for time indices?[@name]YYYY-MM-DD
OQ-02Example: Which languages need custom analysers?[@name]YYYY-MM-DD
[OQ-nn][Question][@name]YYYY-MM-DD

12.2 Glossary

TermMeaning
Inverted indexMap from each term to the documents that contain it
Postings listThe list of documents (and positions) for one term
SegmentAn immutable piece of an index; a shard holds many
RefreshMaking buffered documents searchable by opening a new segment
MergeCombining segments and dropping deleted documents
BM25Probabilistic text relevance score using term frequency, document frequency and length
nDCGRanking metric that rewards relevant results near the top, normalised to the ideal order

12.3 References

ReferenceUse
Busch, Gade, Larson, Lok, Luckenbill, Lin, "Earlybird: Real-Time Search at Twitter", ICDE 20127.2 and 7.8
Robertson and Zaragoza, "The Probabilistic Relevance Framework: BM25 and Beyond", Foundations and Trends in Information Retrieval 3(4), 20097.4 BM25
Apache Lucene documentation (segments, similarity)7.1, 7.4 defaults
Search Autocomplete System Design (system-design/design-search-autocomplete)7.5 boundary
Distributed Key-Value Store System Design (system-design/design-distributed-kv-store)Replication and partitioning background
Back-of-Envelope Estimation and Capacity Model (system-design/back-of-envelope-estimation)Method for section 3

Standard and licence note

Based on common industry practice for inverted-index search engines and on the published research cited in 12.3. The Earlybird paper and other engineering write-ups are inspiration only; no text, diagrams or numbers were copied, and all figures here are illustrative examples. Check the current documentation of your search engine for defaults and limits. Original text written for this collection. Template licence: MIT.

Related templates