Design complete systems / CHAPTER 57
Design Twitter search
Find posts matching a word query, then check their current text, deletion state and audience in the main post database before display.
Builds on Design a microblogging service (Twitter), Database indexes, Data partitioning, Strong versus eventual consistency, Synchronous versus asynchronous communication.
- Explain how saved post changes become searchable and why that can take time.
- Combine results from index partitions and continue pages within a defined result view.
- Check current post versions and permission so stale search data cannot expose removed content.
01Functional requirements
A search index is a separate structure built from posts to find matching text quickly. A new post appears only after a background program copies its words into the index and the search engine exposes that update. An index hit is therefore only a possible result until the service checks the post’s current text, deleted state and audience. We will follow three short posts and a garden search through that path.
FR1: Match explicit text queries. Search existing post text using
AND,ORor quoted phrases. A phrase requires words in the requested order at adjacent positions. Use the same analyzer, meaning the same text-to-term rules, for stored posts and queries.FR2: Filter and order results. Apply author/time filters and offer recent-first or an explicitly requested relevance order with documented tie-breakers. A filter never grants that author's private permissions.
FR3: Page a query view. Return size-limited result pages with a continuation cursor tied to the chosen index view and order. Report when the cursor expires or a required index partition is unavailable. Return partial results only under an explicitly supported partial-results contract.
FR4: Reflect source changes. Process post creates, edits and deletes into a recoverable search index, while reporting that new posts can take time to become searchable.
FR5: Enforce current visibility. Return only bodies, snippets and metadata the caller may currently read. The source post/version/audience remains authoritative, including during reranking.
Out of scope
- Media-content search, typeahead, direct-message search and generated answers.
- Exact live counts/facets without an equivalent current-visibility mechanism.
- Meaning-based retrieval under the existing word-matching modes; introduce a semantic mode explicitly if related wording should also qualify.
Example: For p81 “Garden opens Saturday”, p82 “Garden sale Sunday” and p83 “Library opens Saturday”, AND garden saturday yields p81 before current-state filtering.
02Nonfunctional requirements
Planning load: ten million new posts and 200 million searches/day, with an illustrative tenfold query peak. These are proposed interview targets to validate under the stated conditions.
NFR1: Query latency. At least 95% of measured primary-region queries must finish within 300 ms at the agreed peak, including checks against current source records. Cap query terms, phrase length, page size and work sent to index partitions.
NFR2: Search freshness. An ordinary new post should become searchable within five seconds of being saved in the main post database. Include time waiting for the indexer and time before the search engine exposes indexed changes; a short final refresh cannot hide an upstream backlog.
NFR3: Current result correctness. Before returning text or snippets, verify the current source version, deletion state and audience. If the post changed since indexing, drop it or test the query again on the permitted current text. If a required private-permission check is unavailable, omit the result or fail the request rather than expose unchecked content.
NFR4: Recoverable indexing. A successfully saved post change must remain recoverable for indexing after a worker crash. Keep its pending or replayable change record until the agreed repair policy can recover it. Repeated delivery or delayed older work must not restore a newer deleted post.
NFR5: Stable traversal. While its saved index view is valid, a cursor continues one defined result order. Current permission can still remove a result between pages. If the saved view expires, tell the caller to restart rather than silently continue a different ordering.
NFR6: Honest completeness. If required shards are unavailable and partial results are unsupported, fail the query. Current filtering may shorten a page; it never permits unchecked filler hits or unfiltered exact totals.
NFR7: Bounded AI and cache use. A cached ID or optional model score cannot grant permission or change the requested word-matching rule. Check that returned posts still match
AND/OR/phrase requirements and check current visibility again after model scoring.
03Estimate ingestion and query fan-out
Use decimal units:1 KB is 1,000 bytes. Count external queries, their internal index work and retained data separately.
- New source posts. Ten million posts/day give
10,000,000 ÷86,400 ≈116 creates/second. Edits, deletes, retries and bursts add work. A successful source write needs retained indexing work even if the search program is temporarily down. - Queries. Two hundred million searches/day give about 2,315 queries/second. Assume a tenfold peak of 23,150/second. Benchmark the complete query, including current post/permission reads, at that peak.
- Source bytes. One year at ten million posts/day is 3.65 billion posts. At 1 KB each, source records occupy 3.65 TB before copies. Retention and source storage are separate from the structures used to search words.
- Index bytes. At a measured planning budget of 2 KB/indexed post,
3.65 billion ×2 KB =7.3 TB. One extra full copy gives 14.6 TB. Partitioning divides that dataset; replicas copy it for recovery and eligible reads. - Internal calls. If every query touches 20 partitions,
2,315 ×20 =46,300partition requests/second on average, or about 463,000 at peak. Each additional partition adds coordination. Time filters can avoid older partitions only when the query truly excludes their data. - Posting IDs. At 20 distinct terms/post and eight bytes per uncompressed posting ID, new IDs alone use
10 million ×20 ×8 =1.6 GB/day, or 11.2 GB over seven days. Word positions, term dictionaries, caches and copies add space; compression changes the actual result. - Candidate verification. If 20 partitions return 50 IDs each, the coordinator can receive 1,000 candidates for one query. Cap that work and read current metadata in batches. One network call per candidate would multiply latency and load.
The entire index need not fit in memory. Measure compressed disk size and the frequently accessed data kept hot by representative queries before choosing machine counts.
04Define search requests, results and retries
Ben’s client sends a search request to the query handler. The three example posts are p81 “Garden opens Saturday”, p82 “Garden sale Sunday” and p83 “Library opens Saturday”. The handler must find matching words and then check whether Ben may read each current post.
POST /search
Content-Type: application/json
{"terms":["garden","saturday"],"mode":"AND","authorId":null,"before":null,"order":"recent","limit":20,"cursor":null}
{"items":[{"postId":"p81","text":"Garden opens Saturday","version":1}],"cursor":"<index-view-and-sort-key>","partial":false,"indexedThrough":"2026-10-24T12:00:00Z"}
- Read the query fields.
termssupplies the words,modechooses the supportedAND/OR/phrase behavior, and author/time filters narrow the search.orderchooses recent-first or the documented relevance order. The example asks forgardenANDsaturday, returning up to 20 results. - Use the caller’s identity. The server obtains Ben’s identity from authentication.
authorIdis only a filter; typing Mira’s author ID does not grant her private access. - Return an honest result. Each item includes its source post ID, permitted text and version. The cursor continues a defined index view and sort position.
partial:falsemeans the required index partitions answered, not that every source post has already been indexed. - Handle retry and errors. A query is read-only and may be repeated, but results may change without a valid continued view. Return
400for syntax errors,429 when throttled and 503 when a required partition is unavailable and partial results are unsupported. Reject queries exceeding documented work limits; mark supported partial results explicitly. - Keep modes distinct. An optional model may reorder valid matches for an explicit relevance request. Meaning-based retrieval requires a separately introduced semantic mode; it must not silently turn an
ANDor phrase query into “related wording.”
05Choose source records and index access paths
The main post database accepts creates, edits, audience changes and deletes. Search keeps another, rebuildable representation for word lookup. It is not a second place to decide the latest body or permission.
| Record / key | Purpose and access |
|---|---|
Source post, postId |
Current text, author, version, visibility, deleted state |
Change event, (postId,version) |
Replayable creates/edits/deletes from source |
Posting list, normalizedTerm within an index shard |
Sorted document IDs and optional positions |
Search document, postId |
Stored fields and indexed source version |
Tombstone, postId |
Highest removed version for safe delayed-event handling |
Search view, viewId |
Index generation, snapshot lifetime and stable sort continuation |
- Source-change recording: the post handler saves a change record with the post in one transaction. This pending work table is an outbox. A relay reads it and notifies the indexer, the background program that updates search structures.
- Text analysis: the indexer converts text into stored terms. Lowercasing makes
Gardenmatchgarden; splitting text into words is tokenization. Optional stemming groups related word forms. The query handler must apply the same chosen rules. - Version checking: the search document stores the source version it represents. Before applying an event, the indexer compares that version with the highest version already accepted for the same post ID. Repeated or older versions do not replace newer state.
- Deletion evidence: after deleting a document, retain its highest deleted version, called a tombstone. Otherwise delayed older creation work could insert it again.
- Phrase support: postings retain word positions when phrase matching is required. A list of matching document IDs alone does not show whether two words were adjacent.
- Recovery: the post ID joins search candidates to the main record. The query handler uses it to verify current version, text eligibility and audience before returning bodies or snippets.
06Make the derived index recoverable
First search the three example posts on one logical index. An inverted index maps a word to the post IDs containing it. Each word’s ID list is a posting list.
Build searchable records
- The post handler commits
p81/version 1 and its pending search-change record in the main database. Acknowledging that source write does not yet make it searchable. - The relay reads the pending record and sends it to the indexer. If the send worked but the relay crashed before recording progress, it may send the same record again.
- The indexer looks up
p81’s accepted source version. For a new version, it replaces that post’s indexed terms and records the version together. An equal-version retry does not add another logical document; an older version is rejected. - The search engine performs a refresh, making newly indexed changes visible to new queries. Waiting for the relay, indexing and refresh contribute separately to the five-second freshness target.
Answer Ben’s query
- The query handler applies the same text rules to
gardenandsaturday.Garden’s list is[p81,p82];saturday’s is[p81,p83]. - For
AND, retain IDs present in both lists: onlyp81. ForOR, combine and remove duplicates to obtainp81,p82,p83. - For the phrase “
gardenopens”, also inspect the word positions inp81.Gardenfollowed immediately byopensqualifies; both words appearing somewhere would not prove the phrase. - Order candidates by the chosen rule, initially recency with a stable time/ID tie-break. Read the main post records and permission needed for the caller.
- Return
p81’s current permitted text only if its current version still qualifies. If it changed since indexing, omit it until catch-up or re-evaluate the query on the permitted current text.
One indexer can read outbox records directly and maintain one logical dataset with a replica. A separate log service is not required for this small baseline. Retain source work for replay: being visible to a search is not itself proof that a lost index can be reconstructed.
BUILD THE DESIGN, ONE CHANGE AT A TIME
Watch the architecture evolve.
Advance through the stages in this same diagram. Existing components keep their places; additions fill the available space, then add rows. Highlights show what changed from the previous stage, and numbered arrows match the connection key below.
Stage 1 of 3
Start here. First search one logical index while its measured size and load fit, retaining source work and checking current access.
Baseline: match in the index, authorize at the source
Follow the numbered arrows
- Post database → Search index + replicaReplay committed changes
- Searching user → Query serviceSearch request
- Query service → Search index + replicaFind matching candidates
- Query service → Post databaseVerify current state / access
Trace the diagram in words
- The post handler commits p81 and its pending search change in one transaction.
- The indexer applies that version and refresh makes it visible.
- Ben’s handler searches the term lists and checks current p81 state before returning text.
- The post handler saves each post change and pending indexing record together.
- One indexer applies newer source versions and refreshes the searchable copy.
- The query handler verifies main-source version and access before returning text.
The cost: One logical index has a storage and processing limit. A replica adds a copy for recovery or reads; it does not divide the dataset.
Why change it? The 7.3 TB logical dataset or measured query/indexing work now exceeds one partition’s useful capacity.
Growth: retained ingestion and document-shard query fan-out
Follow the numbered arrows
- Post database → Durable change logRelay committed outbox
- Durable change log → Search shards + replicasVersion-aware indexing
- Searching user → Query coordinatorSearch request
- Query coordinator → Search shards + replicasGather local top candidates
- Query coordinator → Post databaseVerify current state / access
Trace the diagram in words
- The relay forwards saved source changes into the retained log.
- Each indexer checks source versions before updating its own document set.
- The coordinator merges local matches and verifies current source text and access as before.
- The relay writes committed changes to a retained log so independent indexers can resume them.
- Divide document IDs among index owners. Each owner keeps local term lists; query replicas must meet the declared search-freshness limit.
- The coordinator searches required owners, merges local matches and performs the same source checks.
Inspect the connection changes
- Now: Post database → Durable change log: Relay committed outbox
- Now: Durable change log → Search shards + replicas: Version-aware indexing
- Now: Query coordinator → Search shards + replicas: Gather local top candidates
- Previously: Post database → Search shards + replicas: Replay committed changes
- Previously: Query coordinator → Search shards + replicas: Find matching candidates
All other connections are retained. Response paths are implicit unless drawn.
The cost: Partitioning adds disk and processing capacity but creates more internal requests, log recovery and replica operations. Twenty queried shards mean twenty calls before retries.
Why change it? Repeated popular queries now consume enough shard work to justify caching their candidate IDs.
Measured optimization: cache candidates, retain source authority
Follow the numbered arrows
- Post database → Durable change logRelay committed outbox
- Durable change log → Search shards + replicasVersion-aware indexing
- Searching user → Query coordinatorSearch request
- Query coordinator → Search shards + replicasMiss: gather local candidates
- Query coordinator → Post databaseVerify current state / access
- Query coordinator → Candidate cacheLook up candidate IDs
Trace the diagram in words
- Ben’s coordinator tries the cache for this exact query and index view.
- On a miss it queries the required shards and stores bounded candidates.
- Before return it checks current source versions and access for candidates from either route.
- The coordinator keys the cache by normalized query, filters, order, access context and index generation.
- A miss uses the same document-shard query and stores a bounded ID list.
- A hit still checks current source versions, deletion and permission before producing text or snippets.
Inspect the connection changes
- Now: Query coordinator → Search shards + replicas: Miss: gather local candidates
- Now: Query coordinator → Candidate cache: Look up candidate IDs
- Previously: Query coordinator → Search shards + replicas: Gather local top candidates
All other connections are retained. Response paths are implicit unless drawn.
The cost: Caching costs memory and needs expiry and generation handling. It saves repeated search work but cannot turn a saved ID into current permission.
07Compare term sharding with document sharding
A partition, also called a shard, owns only part of the index. Decide what is divided before adding machines.
| Layout | Data placed on each owner | Query work |
|---|---|---|
| By term | A owns garden’s list; B owns saturday’s |
Fetch lists from different owners and intersect; popular words can overload one owner |
| By document ID | A owns p81/p83; B owns p82, each with local term lists |
Evaluate matches locally, then combine shard results |
Follow document sharding for Ben’s AND query:
- The coordinator, the query program combining answers, sends
gardenANDsaturdaytoAandB. Alooks upgarden[p81]andsaturday[p81,p83], so its local intersection isp81.Blooks upgarden[p82]and an emptysaturdaylist, so its local result is empty.- A and
Breturn local matches; the coordinator merges them and performs the current-source checks before returning text.
Each shard still has an inverted index. Document sharding does not mean scanning all raw post bodies. More shards spread storage but add calls:20 queried shards at 2,315 external queries/second produce about 46,300 internal calls/second before retries. Read replicas are additional copies of a shard, not further division of its documents; the chosen copy must meet the freshness rule.
First test whether a larger search machine meets measured needs. Add document shards when storage, ingestion or query work no longer fits one logical partition. The next architecture stage also exposes a durable log between the source and indexers so separate workers can resume their own change streams.
Document shards intersect locally before the coordinator merges
Follow the numbered arrows
- AND query → Shard A: p81, p83Evaluate local intersection
- AND query → Shard B: p82Evaluate local intersection
- Shard A: p81, p83 → CoordinatorLocal result p81
- Shard B: p82 → CoordinatorNo local match
- Coordinator → Current source checkLoad current post; check version and audience
Trace the diagram in words
- Shard A intersects its garden and saturday lists and returns p81.
- Shard B has no saturday match and returns no AND candidate.
- The coordinator combines answers and checks the current main post record and caller permission.
08Distribute queries and state the ranking cost
Keep the same query and permission checks when requests span many machines:
- Place documents. Hash a post ID to its document owner. That owner’s indexer maintains term lists for its assigned posts. Replicas can share eligible query load.
- Request local matches. The coordinator sends Ben’s query to every relevant shard and asks for a bounded number of its best matching IDs. A required missing shard must fail the request unless the API explicitly supports partial results.
- Merge one order. Combine recent-first results using the same time/ID sort. For relevance, use scores with compatible meaning across shards; arbitrary local score scales cannot be compared just because both are numbers. Use the search engine’s supported distributed scoring behavior.
- Check current records. Read source metadata in batches, remove repeated IDs and reject deleted, forbidden or mismatched results. Fetch extra candidates only within a work limit; current filtering may leave a short page.
- Continue a defined view. A point-in-time view retains a fixed searchable index state for a limited lifetime. A search-after cursor records the last sort key within that view so the next page continues after it. If the view expires, tell the client to restart.
- Keep permission current. The view freezes index traversal, not source audience rules. Each page still checks present access; a post becoming private must disappear from later responses even if the old view lists it.
Time-bucket indexes group posts by age for retention and time-filtered queries. Include every bucket the query can match. Hashing inside a bucket can spread recent writes. These savings are useful only when their extra routing and maintenance cost fits the measured workload.
09Rebuild safely and choose the next optimization
Separate recovery from optional caching. A replica can replace a failed node, but a lost dataset or changed index schema requires reconstructing search data.
Rebuild while posts continue changing
- Take a consistent source snapshot and record which change-log position it includes, or the corresponding positions for partitioned logs. This saved boundary is the snapshot’s watermark.
- Copy that snapshot into a new index generation. Keep later creates, edits and deletes in the retained log while the copy runs.
- Replay changes after the saved watermark. If
p82changed during copying, this step updates its copied old version before the new index serves readers. - Compare expected record counts and run known queries that include a new post, an edit and a deletion. Confirm those cases reflect the replayed source versions. Only then switch the reader alias, the stable name pointing readers to the chosen index generation.
- Keep the old generation until rollback needs and existing cursor lifetimes are resolved. Retain deletion versions while older events may still be replayed; otherwise a rebuild can restore removed posts.
Cache only when repeated queries waste significant shard work
- The coordinator builds a key from normalized query, filters, order, access context and index generation.
- On a miss, it uses the shard path and stores a bounded candidate-ID list with its index-view context.
- On a hit, it saves that index work but still reads current source version and permission before producing text or snippets. A cache entry is not a lasting grant.
Measure source-to-search delay, oldest unapplied change, shard timeouts, removed stale candidates and expensive-query resource use. Cap phrase, wildcard and broad-prefix work where supported. More frequent refresh can shorten final visibility delay but adds indexing work; it cannot drain an upstream backlog by itself.
See stage 3: Candidate cache ↗10Trace a post change and a search
Use Ben’s query and Mira’s p81 deletion to test the complete path:
- Mira’s post handler saves
p81/version 1 with its pending search change. The indexer records its terms/positions and refresh makes it searchable. - Ben’s query handler normalizes
gardenandsaturday, then each required shard intersects its local lists. - The coordinator combines candidates and reads their current source metadata in batches. It checks Ben’s permission, deletion state and source version before returning
p81’s text or a snippet. - Mira deletes
p81at version 2. A new source read now sees deletion, so the query handler returns nop81body even while an old index view or cache contains its ID. - The indexer removes
p81and retains deletion version 2. If a delayed version 1 event arrives, its version comparison rejects it rather than inserting the document again. - If Mira instead edited out
saturday, an old index hit is no longer evidence of a current match. Drop it until reindexing or test the query against permitted current text.
Do not expose unchecked exact totals or facets either. If a count includes private hits without equivalent current checks, it can leak their existence even when bodies are omitted. Leave exact totals out unless the design can enforce the same visibility rules.
11Protect correctness and measure the pipeline
Name which program can continue and which result is no longer established:
- The indexer stops. Source posts can remain saved with their outbox work while search grows older. Measure oldest unapplied work and source-to-search delay; slow intake when finite backlog limits require it. A successful query response must not hide growing lag.
- Delivery repeats. The indexer compares
(postId,sourceVersion)with saved version state. Equal versions do not create another document; lower versions cannot overwrite a newer edit or deletion. - A document was removed but old work remains. Keep the deletion version until that work can no longer return. Physically removing only the searchable body forgets why an old create must be rejected.
- A required shard is missing. Fail rather than return an apparently complete answer from only the available shards. Supported partial responses must be explicitly marked.
- Private verification is unavailable. Omit unchecked content or fail under the agreed policy. Neither a cached snippet nor a snapshot supplies current permission.
- A query is too expensive. Bound terms, phrases, scans and per-shard work, rate-limit excessive callers and watch slowest responses. Adding shards can increase query calls and waiting as well as capacity.
Replicas, retention and frequent refresh trade storage and processing cost for availability and freshness. They do not replace the main-source checks protecting current access.
OPTIONAL DESIGN EXTENSIONWhere AI Fits in This Design
A model may optionally reorder posts that already satisfy the requested words and current access rules. It must not quietly change which posts qualify.
- Input and output. The coordinator sends the query and, for example, up to 50 currently permitted post texts. The model returns a score for each supplied ID, not a generated answer or invented post. A separate meaning-based search mode would need its own explicit API contract.
- Integration. Retrieve matching candidates, check current access, then call the model with a deadline. Before returning results, check source versions and permission again; Ben may lose access while inference runs.
- Fallback and budget. Assume 40 ms within the 300 ms response target, then benchmark it. On timeout or invalid scores, retain the ordinary word-match/recent order. Cap candidate count and text length so one query cannot create unlimited inference work.
- Evaluation and privacy. Compare judged relevance, empty results, slow calls and cost/search against that fallback. Send protected text only through approved inference infrastructure. Minimize query logs and define deletion/retention for those copies. A high score cannot make deleted or private
p81readable.
Matching, permission and result ordering remain different checks.
DRAW. CALCULATE. EXPLAIN.
Search through a delayed delete and an out-of-order event
75 minutes · guided practice
Use p81=garden opens saturday, p82=garden sale sunday and p83=library opens saturday. Put p81/p83 on shard A and p82 on B. Draw AND and OR garden saturday, then phrase garden opens. Delete p81 at version 2 before replaying version 1. Explain a generation rebuild while p82 changes, and calculate seven days of raw postings at 10 million posts/day, 20 IDs/post and 8 bytes/ID.
Compare the worked solution
- Evaluate local lists. On shard
A,garden[p81]intersectssaturday[p81,p83]atp81. OnB,garden[p82]intersects an emptysaturdaylist, producing noANDmatch. The coordinator therefore hasp81.ORcombines the distinct IDs and givesp81,p82,p83. - Check the phrase. For “
gardenopens”, inspectp81’s stored word positions and confirm the requested adjacent order. Two words appearing somewhere in a post are insufficient. - Delete safely. The main source records
p81deleted at version 2, so the query handler no longer returns its text. The indexer retains version 2 and rejects the late version 1 update rather than rebuilding a visible old document. - Rebuild without losing p82’s edit. Copy a consistent snapshot and record its change-log position. Retain later events, replay them into the new generation, validate and switch readers. This includes changes committed while copying ran.
- Calculate posting bytes.
10,000,000 ×20 ×8 ×7 =11.2 billion bytes, or 11.2 GB raw in seven days. Positions, dictionaries and replicas add space; compression changes physical storage. - Keep paging and permission separate. A retained index view stabilizes the cursor’s order, but every returned body/snippet still passes current source checks. Expired views require an explicit restart.
The checkboxes record your own assessment. The worked solution gives you a way to check the reasoning.
RETRIEVE BEFORE YOU REVEAL
Keep the important ideas.
Say the answer aloud, then turn the card over.
CARD 01How does multiword retrieval work?
The index maps each word to post IDs. AND keeps IDs in every required list; OR combines lists without repeats. A phrase also checks adjacent word positions. Document shards perform these checks locally, and the coordinator merges their answers.
CARD 02Why can indexed text differ from the current post?
The source database decides current text and access. Before returning an index hit, the query handler checks its current version, deletion and audience. A frozen index view or high model score cannot preserve revoked permission; check again after optional inference.
CARD 03How is a search index recovered?
Copy a consistent source snapshot and save the change-log position it includes. Replay changes after that position, test counts and known edit/delete queries, then switch the reader alias. Replicas help a failed node; retained source records and changes are still needed to rebuild a lost dataset.
CHECK THE REASONING
Can you explain the choice?
Answer every question, then finish for an immediate score and an explanation for each answer. Aim for 80%, then revisit any gaps.
CLOSE THE LOOP
Explain it without the page.
Summarise the mechanism, one trade-off, and one failure mode. Then mark this chapter complete.