# 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.

<!--
Guidance: How to use this template
- Decide first what "fresh" means for your corpus: searchable within one
  second (messages, logs, social posts) or within an hour (catalogue, docs).
  Freshness decides the indexing design more than any other requirement.
- Fill the corpus size, document size and query rate in section 3 before
  choosing shard counts. Shard counts are hard to change later.
- Write down how relevance will be judged (7.6) before tuning ranking.
  Without a judged query set, every ranking change is an opinion.
- If you use a managed engine (for example an Elasticsearch or OpenSearch
  service), keep sections 5 to 7 anyway: they record your mapping, sharding
  and ranking decisions, which the product does not make for you.
-->

## Document control

<!-- Guidance: A design is a proposal until it is approved; record who decided and when. -->

| Field | Value |
|---|---|
| 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 updated | YYYY-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

| Role | Name | Decision | Date |
|---|---|---|---|
| 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

<!-- Guidance: Note what changed in the design, not the wording. -->

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

## Contents

<!-- Guidance: Keep aligned with the headings. -->

1. [Overview](#1-overview)
2. [Requirements](#2-requirements)
3. [Capacity estimation](#3-capacity-estimation)
4. [API design](#4-api-design)
5. [Data model](#5-data-model)
6. [High-level design](#6-high-level-design)
7. [Detailed design](#7-detailed-design)
8. [Scalability and performance](#8-scalability-and-performance)
9. [Reliability, security and operations](#9-reliability-security-and-operations)
10. [Trade-offs and alternatives](#10-trade-offs-and-alternatives)
11. [Delivery plan and risks](#11-delivery-plan-and-risks)
12. [Appendices](#12-appendices)

## 1. Overview

<!-- Guidance: One page that a busy reviewer can read alone: problem, proposed solution in a paragraph, the decisions that need attention, and the short interview path. -->

### 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

| ID | Question | Recommended option | Section |
|---|---|---|---|
| Q-01 | *Example:* Partition by document or by term? | By document, with time-based indices for the message corpus | 7.3 |
| Q-02 | *Example:* Freshness target? | Searchable within 1 s for posts, 15 min for help articles | 7.2 |
| Q-03 | *Example:* Ranking? | BM25 first phase, learned reranker on top 200 | 7.4 |
| Q-04 | *Example:* Does this service also serve typing suggestions? | No, separate autocomplete service | 7.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.

| Minutes | Cover | Output on the whiteboard |
|---|---|---|
| 0 to 5 | Scope: keyword search with filters and ranking; freshness target; below-the-line items | Core flows, freshness number |
| 5 to 10 | Estimates: corpus, index size, write rate, query rate | Shard count and copies, each with a reason |
| 10 to 15 | API: search with filters and paging, index document, delete | Three calls |
| 15 to 25 | High-level design: ingestion pipeline, shards and replicas, broker, reranker | One diagram |
| 25 to 40 | One deep dive: inverted index and real-time ingestion (default), or sharding, or ranking | Options, choice, reason |
| 40 to 45 | Failures, relevance evaluation, 10x change | Failure 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

<!-- Guidance: Split core requirements from below-the-line items so the design stays focused. Non-functional targets need numbers and a way to measure them. -->

### 2.1 Core functional requirements

| ID | Requirement | Priority |
|---|---|---|
| FR-01 | *Example:* Keyword search over title and body with boolean operators and phrase queries | Must |
| FR-02 | *Example:* Filters on author, type, language and date range | Must |
| FR-03 | *Example:* Results ranked by relevance, with a sort-by-date option | Must |
| FR-04 | *Example:* New and edited documents searchable within 1 s; deletes hidden within 1 s | Must |
| FR-05 | *Example:* Results respect the viewer's access rights | Must |
| FR-06 | *Example:* Highlighted snippets showing matched terms | Should |
| FR-07 | *Example:* Facet counts for type and language | Should |
| [FR-nn] | [Requirement] | [Must / Should / Could] |

### 2.2 Non-functional requirements

| ID | Quality | Target | Measured as |
|---|---|---|---|
| NFR-01 | Query latency | *Example:* p99 at or below 200 ms at the broker | Server timing |
| NFR-02 | Freshness | *Example:* p99 at or below 1 s from commit to searchable | Synthetic canary documents |
| NFR-03 | Availability | *Example:* 99.95% of queries answered; partial results flagged | Probes and error budget |
| NFR-04 | Relevance | *Example:* nDCG@10 on the judged set does not drop by more than 1% per release | Offline evaluation |
| NFR-05 | Durability | *Example:* No acknowledged document lost; index rebuildable from source | Rebuild 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

<!-- Guidance: Every number must end in a consequence for the design. Show inputs, formulas and results; round generously and say which way you rounded. -->

### 3.1 Assumptions

| Input | Symbol | Value | Source |
|---|---|---|---|
| Documents | $N$ | *Example:* 2 billion | Source database |
| Average indexed text per document | $s$ | *Example:* 2 KB | Sample of 10 000 documents |
| Index size relative to raw text (postings, positions, doc values) | $r$ | *Example:* 0.5 | Measured on a test shard |
| New or changed documents per day | $w$ | *Example:* 50 million | Change stream metrics |
| Peak to average write ratio | $k$ | *Example:* 3 | Change stream metrics |
| Peak queries per second | $Q$ | *Example:* 2 000 | Product forecast |
| Target shard size | $z$ | *Example:* 50 GB | Recovery time and merge cost tests |
| Shard queries one copy serves per second | $c$ | *Example:* 1 000 | Load test on target hardware |

### 3.2 Index size and shard count

$$
I = N \times s \times r = 2 \times 10^{9} \times 2 \ \text{KB} \times 0.5 = 2 \ \text{TB}
$$

$$
P = \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

$$
W_{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 \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

| Quantity | Estimate | Design consequence |
|---|---|---|
| Index size | ~2 TB primary, ~6 TB with copies | 40 shards of ~50 GB |
| Writes | ~1 740 docs/s at peak | 1 s refresh, merge policy tuned for small segments |
| Shard queries | 80 000/s | 3 copies per shard, adaptive copy selection |
| Tail latency | Broker waits for the slowest of 40 shards | Per-shard timeout and partial results |

## 4. API design

<!-- Guidance: Define the contract first: resources, methods, request and response shapes, errors, idempotency, pagination, rate limits and versioning. Use RFC 9457 problem details for errors. -->

### 4.1 Endpoints

| Interface | Operation | Purpose |
|---|---|---|
| Public API | `POST /v1/search` | Query with filters, sort, page cursor |
| Internal | Change stream consumer | Index, update and delete documents from the source |
| Admin | `PUT /v1/indices/{name}/mapping` | Versioned field and analyser definitions |
| Admin | `POST /v1/indices/{name}/reindex` | Rebuild 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:

```json
{"q": "refund \"late delivery\"", "filters": {"type": ["post", "article"], "lang": "en", "created_after": "2026-01-01"}, "sort": "relevance", "page_size": 20, "cursor": null}
```

```json
{"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

| Case | Status | Body type |
|---|---|---|
| Query syntax error | 400 | `problems/invalid-query` with the position |
| Too many clauses or wildcard on a short prefix | 422 | `problems/query-too-expensive` |
| Some shards timed out | 200 with `"partial": true` | Not an error; the client may show a notice |
| Too many requests | 429 with `Retry-After` | `problems/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

<!-- Guidance: Choose storage by access pattern, not by habit. Show entities, keys, partitioning and the query each store must answer. -->

### 5.1 Entities

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

```mermaid
erDiagram
  DOCUMENT {
    string doc_id PK
    string type
    string author_id
    string lang
    datetime created_at
    int version
  }
  TERM {
    string field PK
    string term PK
    int doc_freq
  }
  POSTING {
    string term FK
    int local_doc_id
    int term_freq
    string positions
  }
  SEGMENT {
    string segment_id PK
    int shard
    int doc_count
    bool live
  }
  ACL {
    string doc_id FK
    string principal
  }
  DOCUMENT ||--o{ POSTING : "appears in"
  TERM ||--o{ POSTING : "lists"
  SEGMENT ||--o{ POSTING : "contains"
  DOCUMENT ||--o{ ACL : "visible to"
```

### 5.2 Index mapping

| Field | Type | Analysis | Used for |
|---|---|---|---|
| `title` | Text | Language analyser with stemming; also an exact sub-field | Matching, boosting |
| `body` | Text with positions | Language analyser | Matching, phrases, snippets |
| `type`, `lang`, `author_id` | Keyword | None | Filters, facets |
| `created_at` | Date | None | Filters, sort, time-based routing |
| `acl_principals` | Keyword, multi-valued | None | Access filter |
| `quality_score` | Numeric doc value | None | Ranking feature |

### 5.3 Access patterns

| Query | Key or index that serves it | Partition key and rationale |
|---|---|---|
| Keyword query on body | Term dictionary, then postings lists per shard | `doc_id` hash; spreads documents evenly, every shard answers every query |
| Filter by type or language | Keyword postings or doc values intersected with the text match | Same shard as the document, so filters are local |
| Recent messages only | Time-based index per week | `created_at` week; queries for "last 7 days" touch 1 or 2 indices |
| Get document for display | Source database by `doc_id` | Primary key in the source, not the index |
| Access-filtered results | `acl_principals` postings | Stored with the document so the filter runs on the shard |
| Delete or update | Mark old version deleted in its segment, add new version | Routing by `doc_id` finds the one shard holding it |

## 6. High-level design

<!-- Guidance: One diagram that shows every component on the request paths and where state lives. Keep ingestion and query paths visibly separate. -->

```mermaid
flowchart LR
  src[(Source database)] --> cdc[[Change stream]]
  cdc --> idx[Indexing workers: analyse, route by doc_id]
  idx --> s1[Shard 1 copies]
  idx --> s2[Shard 2 copies]
  idx --> sn[Shard N copies]
  user([Client]) --> api[Search API]
  api --> broker[Query broker]
  broker --> s1
  broker --> s2
  broker --> sn
  broker --> rr[Reranker, top candidates]
  rr --> api
  api --> src
  logs[[Query and click logs]] --> eval[Relevance evaluation]
  api --> logs
```

| Component | Responsibility | Scales by |
|---|---|---|
| Change stream | Ordered changes with document version | Partitions |
| Indexing workers | Analyse text, route to shard, retry on failure | Stateless instances |
| Shard copy | Hold segments, apply writes, answer shard queries | Copies per shard |
| Query broker | Parse, fan out, merge top results, enforce timeouts | Stateless instances |
| Reranker | Re-score top candidates with richer features | Stateless instances, model cache |
| Relevance evaluation | Judged sets, metrics, experiment analysis | Batch |

## 7. Detailed design

<!-- Guidance: Go deep on the parts that are new, risky or contested. For each deep dive list the options, the choice and the reason. -->

### 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.

| Option | How updates work | Cost |
|---|---|---|
| One mutable index | Update postings in place | Locks, fragmentation, hard to compress |
| Immutable segments | New documents go to a new small segment; deletes are bitmaps; background merges combine segments | Many small segments until merged |
| Rebuild in batch and swap | Build a full new index, switch atomically | Freshness 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.

| Option | Freshness | Effect on queries |
|---|---|---|
| Commit to disk per document | Immediate | Very slow, huge number of tiny segments |
| Periodic refresh: buffer in memory, open a new searchable segment every interval | Interval (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 readers | Sub-second | Careful 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).

```mermaid
sequenceDiagram
  participant W as Indexing worker
  participant P as Primary copy
  participant L as Transaction log
  participant R as Replica copies
  participant Q as Query
  W->>P: index doc_7c1e version 4
  P->>L: append (durable)
  P->>R: replicate operation
  P-->>W: acknowledged
  Note over P,R: refresh tick (1 s): buffer becomes a searchable segment
  Q->>R: shard query
  R-->>Q: includes doc_7c1e version 4
  Note over P: background merge combines small segments, drops deleted docs
```

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

| Option | Query cost | Write cost | Main risk |
|---|---|---|---|
| By document (hash of `doc_id`) | Every query hits every shard | One shard per document | Tail latency of the slowest shard |
| By term | A query hits only shards holding its terms | One document updates many shards | Hot terms, very costly updates |
| By time (one index per week) | Recent-only queries hit few indices | Writes go to the newest index | Old 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 $q_i$, document
$D$ with length $|D|$ and average length $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)}
$$

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

| Option | Quality | Cost |
|---|---|---|
| BM25 only, with field boosts | Good baseline | Cheapest |
| BM25 plus static features (quality, recency) in a function | Better for fresh content | Low |
| Two phase: BM25 top 200 per query, then a learned reranker | Best measured quality | Model 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

| Layer | Method | Metric |
|---|---|---|
| Offline | Judged set of *Example* 1 000 queries with graded labels per result | nDCG@10, recall@100 |
| Offline | Known-item queries (a user searched for a document they later opened) | MRR |
| Online | A/B test or interleaving of two rankers | Click-through on top 3, reformulation rate, abandonment |
| Guardrail | Latency and zero-result rate | p99, 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

<!-- Guidance: Show how the design meets the numbers in section 3 and where it will break first. -->

| Concern | Approach | Limit before redesign |
|---|---|---|
| Query throughput | More copies per shard | Network and broker merge cost |
| Corpus growth | More shards via new time indices or a split | *Example:* shards above 50 GB slow recovery |
| Tail latency | Timeouts, adaptive copy selection, partial results | Fan-out above about 100 shards per query |
| Expensive queries | Clause limits, no leading wildcards, query cost estimate | [Limit] |
| Merge pressure | Tiered merge policy, throttled I/O | Refresh interval below 1 s at high write rates |
| Caching | Filter cache per segment, result cache for head queries | Hit rate falls with personalised filters |

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

| Step | Budget |
|---|---|
| API, auth, query parse | 10 ms |
| Fan-out and shard evaluation (slowest shard) | 110 ms |
| Merge top results at the broker | 10 ms |
| Rerank 200 candidates | 40 ms |
| Load display fields and snippets | 20 ms |
| Headroom | 10 ms |

## 9. Reliability, security and operations

<!-- Guidance: Name the failure modes with their blast radius and recovery time, the threats, and the signals operators watch. -->

### 9.1 Failure modes

| Component | Failure | Detection | Blast radius | Mitigation | RTO |
|---|---|---|---|---|---|
| Shard copy | Node lost | Cluster health | Fewer copies for some shards | Promote replica, rebuild copy from a peer | *Example:* 10 min |
| All copies of a shard | Zone or data loss | Shard unassigned | Partial results for every query | Copies in 3 zones; restore snapshot and replay change stream | *Example:* 1 h |
| Change stream consumer | Lag | Canary document freshness | Stale results everywhere | Autoscale workers, alert on lag above 30 s | *Example:* 15 min |
| Bad mapping or analyser change | Recall drops | Evaluation gate, zero-result rate | Whole index | New index plus alias switch, keep the old one | *Example:* 5 min |
| Reranker | Timeout or error | Error rate | Ranking quality | Fall back to first-phase order | *Example:* immediate |
| Query of death | Node CPU or memory exhausted | Circuit breaker trips | Shards on that node | Query cost limits, per-request memory breaker | *Example:* 5 min |
| [Component] | [Failure] | [Signal] | [Scope] | [Mitigation] | [Time] |

### 9.2 Security and privacy

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

### 9.3 Observability

| Signal | Metric | Alert threshold |
|---|---|---|
| Freshness | Canary commit-to-searchable time | p99 above 1 s for 10 min |
| Latency | Broker p99, slowest shard p99 | Above 200 ms for 10 min |
| Partial results | Share of responses with `partial: true` | Above 0.5% |
| Zero results | Share of queries with no hit | Doubling vs. 7-day baseline |
| Relevance | Online click-through on top 3 | Drop of 5% after a release |

### 9.4 Cost estimate

| Item | Unit cost | Quantity | Monthly 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

<!-- Guidance: For each major decision list the options, what each costs and why you chose one. Include what changes at ten times and at one tenth of the load. -->

| Decision | Options | Chosen | Why | What we give up |
|---|---|---|---|---|
| Engine | Database full-text feature; Lucene-based engine; in-house | Lucene-based engine | Mature segments, analysers, BM25 | Another stateful cluster to run |
| Partitioning | Document; term; time | Document plus time indices | Local writes, simple scaling | Every query fans out |
| Freshness | Batch rebuild; refresh; custom real-time segments | 1 s refresh | Meets target with standard engine | Merge cost, small segments |
| Ranking | BM25; BM25 plus features; two phase | Two phase | Measured quality gain at bounded cost | Model pipeline to maintain |
| Lexical vs hybrid | Lexical only; add vector retrieval | Lexical now, hybrid later | Judged set shows lexical meets target | Weaker on paraphrased queries |
| What changes at 10x / 0.1x | 10x 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 enough | Review yearly | Size drives the build or buy line | [Note] |
| [Decision] | [Options] | [Chosen] | [Why] | [Cost] |

## 11. Delivery plan and risks

<!-- Guidance: How the design reaches production safely, and what could stop it. -->

### 11.1 Milestones

| Milestone | Scope | Exit criterion | Target date |
|---|---|---|---|
| M1 | Index help articles, BM25 only | Judged set baseline recorded | YYYY-MM-DD |
| M2 | Change stream ingestion for posts, 1 s refresh | Freshness within NFR-02 | YYYY-MM-DD |
| M3 | ACL filtering and security review | No leak in access tests | YYYY-MM-DD |
| M4 | Reranker behind an experiment | nDCG@10 gain and online win | YYYY-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

| ID | Risk | Likelihood | Impact | Mitigation | Owner |
|---|---|---|---|---|---|
| R-01 | *Example:* Shard count too low for corpus growth | Medium | High | Time indices; split procedure tested | [@name] |
| R-02 | *Example:* Stemming hurts precision in some languages | Medium | Medium | Per-language analysers checked on judged sets | [@name] |
| R-03 | *Example:* ACL changes lag behind source | Low | High | Load-time access check | [@name] |
| [R-nn] | [Risk] | [L / M / H] | [L / M / H] | [Mitigation] | [@name] |

## 12. Appendices

<!-- Guidance: Supporting detail, glossary, references and open questions. -->

### 12.1 Open questions

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

### 12.2 Glossary

| Term | Meaning |
|---|---|
| Inverted index | Map from each term to the documents that contain it |
| Postings list | The list of documents (and positions) for one term |
| Segment | An immutable piece of an index; a shard holds many |
| Refresh | Making buffered documents searchable by opening a new segment |
| Merge | Combining segments and dropping deleted documents |
| BM25 | Probabilistic text relevance score using term frequency, document frequency and length |
| nDCG | Ranking metric that rewards relevant results near the top, normalised to the ideal order |

### 12.3 References

| Reference | Use |
|---|---|
| Busch, Gade, Larson, Lok, Luckenbill, Lin, "Earlybird: Real-Time Search at Twitter", ICDE 2012 | 7.2 and 7.8 |
| Robertson and Zaragoza, "The Probabilistic Relevance Framework: BM25 and Beyond", Foundations and Trends in Information Retrieval 3(4), 2009 | 7.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.
