Design Post Search
Design keyword search over a social network's posts, sorted by recency or likes: fast indexing, like-count tiers, time-sharded indexes and query caching.
On this page
Something happens in your city and you type "ferry strike" into the search box of a social app. You expect posts from the last few minutes, newest first, or the most-liked posts about it if you flip the sort. The kind of product this question means is Twitter search: keyword search over a huge, fast-moving stream of short posts.
The hard part is that the index is written far more often than it is searched, and one of the two sort orders depends on a number (likes) that changes 116,000 times a second. Search Systems already covers the inverted index, analysers and feeding an index from the database; this lesson builds on it. The steps follow A Framework for Any Design.
Clarify the problem
Design search for posts on a social network.
Users type keywords and get matching public posts. Two sort orders, newest first and most liked? Do we need phrase search, typo tolerance or personalised ranking?
Both sorts. All the words must match; no phrases, no typos, no personalisation.
Scale: posts and likes per day, and how many searches?
500 million posts a day, 10 billion likes a day, 200 million searches a day. Search covers all posts ever made.
Then the index takes far more writes than queries. I'll aim for a new post being searchable within 10 seconds, results in under 500 ms, and like counts that may lag by a minute in the sort order. A deleted post must vanish from results quickly. Private posts are out of scope.
- Functional: search public posts by keywords (all must match); sort by newest or by most liked, optionally within a time window; page through results.
- Non-functional: new posts searchable within 10 seconds; 500 ms or better at the 99th percentile; like-based order may lag about a minute; deleted posts disappear from results within seconds; search stays up if parts of the index are down, possibly with fewer old results.
- Out of scope: typo tolerance, phrase search, personalised ranking, private posts, autocomplete (see Design Typeahead Search).
Estimate the scale
A day is 86,400 seconds; assume peaks of 3x.
- New posts: 500 million ÷ 86,400 ≈ 5,800 a second, about 17,000 at peak.
- Likes: 10 billion ÷ 86,400 ≈ 116,000 a second, 20 per post on average.
- Searches: 200 million ÷ 86,400 ≈ 2,300 a second, about 7,000 at peak.
- Writes against reads: (5,800 + 116,000) ÷ 2,300 ≈ 53, so about 50 writes for every search.
How much does the inverted index grow each day?
- 500 million posts a day
- About 30 tokens per post after analysis
- About 5 bytes per posting, compressed, with overhead
Show the working
- Postings: 500 × 106 × 30 = 1.5 × 1010 a day
- 1.5 × 1010 × 5 B = 7.5 × 1010 B = 75 GB a day
- Last 7 days: 7 × 75 = 525 GB. One year: 75 GB × 365 ≈ 27 TB
Estimate 75 GB (anything from 25 GB to 250 GB is a good answer)
A week fits in memory: 7 days × 20 shards of about 3.75 GB, each with 3 replicas, about 1.6 TB of RAM. A year does not. That split is the hot and cold tiers.
If each like rewrote its post's document in the index, how many document rewrites per second would that be on average?
- 10 billion likes a day
- A day is 86,400 seconds
Show the working
- 10 × 109 ÷ 86,400 ≈ 116,000 a second
- That is 20 times the rate of new posts
Estimate 116 thousand/s (anything from 40 thousand/s to 400 thousand/s is a good answer)
An index update is a delete plus a re-insert of the whole document. 20 times more index churn than new content is the problem the second deep dive solves.
Write the search endpoint. How does the client ask for the next page, for each sort order?
Compare with a model answer
One GET with the query, the sort and an optional time window. For newest first, the cursor is "older than post id X", which stays correct as new posts arrive. For most liked, counts move under you, so the cursor points to a short-lived snapshot of the ranked list.
Define the API
GET /v1/search?q=ferry+strike&sort=recent&limit=20&cursor=...
GET /v1/search?q=ferry+strike&sort=likes&since=2026-10-15&limit=20
200 { "results": [ { "post_id": "1849...", "author": "...", "text": "...",
"likes": 1532, "created_at": "..." } ],
"next_cursor": "...", "partial": false }
400 Bad Request empty query, or more than 10 words
410 Gone cursor snapshot expired; start againThe decision worth arguing is pagination. For sort=recent, post ids are time-ordered (see Unique IDs), so the cursor is just "ids smaller than the last one shown": new posts never shift a page. For sort=likes, a post can gain 500 likes between page 1 and page 2, so the server ranks once, keeps the ordered ids for 10 minutes and the cursor names that snapshot. The partial flag tells the app that some shards timed out and the list may miss a few old posts: a degraded answer beats an error.
What does the index store for each post, and where do like counts live so that the likes sort works without rewriting the index?
Compare with a model answer
The index stores the post id, its tokens and a coarse like tier. Exact counts live in a separate key-value store keyed by post id, read at query time. The posts database stays the source of truth for text and deletion.
Model the data
posts DB (source of truth, partitioned by post_id)
post_id 64-bit, time-ordered text up to 1 KB
author_id 64-bit status live | deleted
index document (one per post, never stores the text)
post_id 64-bit tokens ["ferry", "strike", ...]
like_tier 0..6 -- 0: 0-9 likes, 1: 10-99, 2: 100-999 ...
index layout
one index per day -> split into N shards by hash(post_id)
like_counts (key-value, partitioned by post_id)
post_id -> count, last_offsetThree stores, three access patterns. The posts database is read by key to fetch the text of 20 results; a wide-column or key-value store fits. The search index answers "which ids contain all these words", and needs a search engine such as Elasticsearch or a purpose-built inverted index. The like count store answers "how many likes do these 2,000 ids have" in one batch; an in-memory key-value store such as Redis, backed by a durable table, fits.
Sketch how a new post and a like reach the index and count store, and how a search request is answered.
Compare with a model answer
Posts and likes flow as events from a stream. Indexers add posts to today's index; an aggregator batches likes into the count store and tells the indexers only when a post changes tier. A search service checks a query cache, fans out to the index shards, reads counts if sorting by likes, and fetches text from the posts database.
High-level design
request and replyasynchronous message
1Ana posts "Ferry strike again today". The post service writes the posts database; change data capture publishes the post to the event stream.
2An indexer analyses it into
ferry, strike, again, todayand adds it to the in-memory buffer of one of today's shards. The buffer becomes a searchable segment within a second.3The post takes off: 300 likes in one second, from 800 to 1,100. The aggregator sums them and writes
count += 300once. The index is not touched.4Passing 1,000 likes moved it from tier 2 to tier 3, so the aggregator publishes one tier change and an indexer updates one document. So far that has happened at 10, 100 and 1,000 likes, not 1,100 times.
5Leo searches "ferry strike", newest first. The service normalises the query and checks the cache: a miss.
6It asks every shard of today's index for its 20 newest matches. Each shard walks its lists from the newest id down and stops at 20, so the work does not grow with the number of old matches.
7One shard's replica is slow. After 50 ms with no answer, the service sends the same request to another replica and uses whichever answers first.
8It merges the shard lists into the newest 20 overall, fetches their text, and drops one post deleted a minute ago. The page never needed the older index.
9The ranked ids go into the cache for 15 seconds. During a breaking story, the next thousands of identical searches are cache hits.
10An indexer crashes. New posts wait in the stream and become searchable late; nothing is lost. On restart it resumes from its saved offset and skips any post version it already applied.
Deep dives
Interviewers push on four things: indexing at 5,800 posts a second, sorting by likes, sharding, and keeping popular queries cheap.
Indexing a firehose
An inverted index is cheap to read and expensive to change in place, because each postings list is sorted and compressed. So the indexer never edits lists. It buffers new posts in memory and writes them out as a small, immutable segment every second; a search reads all segments of a shard. A background job merges small segments into bigger ones, the same idea as the log-structured storage in a key-value store. Deletes are recorded as a list of dead ids and removed for real during merges.
Time-ordered post ids pay off twice here. New posts always have the largest ids, so they append to the end of every list. And a newest-first query can walk lists backwards and stop early. Step through it on one shard.
- ferry010213402512377749015950strike0881340261537774950
1Postings for both words, sorted by post id. Higher id means newer. We want the 2 newest posts that contain both.
- ferry010213402512377749015950istrike0881340261537774950j
- found
- 950
2Start at the end of both lists. 950 and 950: a match.
- ferry01021340251237774901i5950strike088134026153777j4950
- found
- 950
3901 against 777. The larger one cannot be in the other list from here on, so step past 901.
- ferry0102134025123777i49015950strike088134026153777j4950
- found
- 950, 777
- entries read
- 5
4777 and 777: a second match. We have 2, so we stop. The six older entries were never read.
Within a second of a post arriving, it is in a segment; the 10-second freshness goal leaves room for the stream's own delay. At 17,000 posts a second at peak and 30 tokens each, the fleet adds about 510,000 postings a second, spread over the shards of today's index.
Sorting by likes without rewriting the index
Rewriting a document per like means 116,000 index updates a second, 20 times the post rate. The fix is to separate "roughly how popular" from "exactly how many".
| Option | Good at | Bad at | Pick it when |
|---|---|---|---|
| Update the document on every like | The index always has the exact count; sorting is built in. | 116,000 delete-and-reinsert operations a second, mostly for posts nobody is searching for. | Counts change rarely, like product ratings. |
| Counts outside the index only | Likes never touch the index. Exact counts at query time. | "Most liked posts about weather" may match 10 million posts, and every one needs a count lookup before you can sort. | Queries match few documents. |
| Periodic bulk refresh | Rewrite counts for changed posts every 10 minutes in one batch. | Still rewrites every liked post, and a breaking post sits at its old count for up to 10 minutes. | Freshness of popularity barely matters. |
| Tiers in the index, exact counts outside Usual default | The index holds a tier (powers of 10) that changes only a few times in a post's life; exact counts sort within a tier. | Two stores to keep in step. A post that just crossed a tier is misplaced for a few seconds. | High like rates and broad queries, as here. |
With tiers, a post with 1 million likes has changed tier 6 times (at 10, 100, 1,000, 10,000, 100,000 and 1 million), not a million times. Most posts cross none or one boundary; even if every post crossed two, that is 2 × 5,800 = 11,600 updates a second, a tenth of the like rate.
A likes query searches the highest tier first, within the time window. Suppose "ferry strike" matches 4 posts in tier 4 and 150 in tier 3. Those 154 are the only possible top 20, since every lower-tier post has fewer likes. The service fetches their 154 exact counts in one batch, sorts, and returns 20. Lower tiers are never read. The order is exact except for a post whose tier update is a few seconds behind, which the requirements allow. A broad word over 30 days can put 50,000 matches in one tier; then each shard sends only the newest few hundred from that tier, which bounds the work and makes the order approximate. Finer tiers (half powers of ten) shrink that case for twice the tier updates.
The counts themselves are batched as in Design Photo Sharing: the aggregator sums likes per post each second and stores the last applied stream offset with the count, so a replay cannot add twice.
Sharding the index
Split by time first, then by document. Each day gets its own index, split across shards by hash of post id, as Search Systems recommends: every shard holds a fair slice, and a post touches one shard. Time first has three benefits. Writes all go to today's index, which can sit on more machines. Newest-first queries read only the latest days unless they need more. And an old day's index never receives new posts, so it can be merged into one large segment and moved to cheaper machines.
The cost is fan-out. A query that needs a year reads 365 day indexes times their shards. So older days are rolled into week and month indexes, and likes queries default to a window of 30 days; "all time" is allowed but slower.
With 20 shards per query, the slowest shard sets the latency. If one shard is slow 1% of the time, a query that waits for all 20 is slow 1 − 0.9920 ≈ 18% of the time. That is why the tracer sent a hedged request to another replica after 50 ms.
Hot and cold data, and caching
The recent 7 days, 525 GB of index, live in memory on fast machines with 3 replicas, because nearly every newest-first query ends there. Older data, about 27 TB a year, lives on SSDs with fewer replicas. If the cold tier is down, the service returns recent results with partial: true.
Queries are skewed: during a news event, thousands of people type the same words. Cache the ranked list of ids, keyed by normalised query, sort and window, for 15 seconds, short enough that new posts appear. Also cache per-shard results for days that are over: an old day gains no new posts, so its answer for "ferry strike" changes only through deletes and tier moves, and can be kept for an hour. A deep page then costs one live query on today's shards and cache reads for the rest. Deleted posts are filtered when the text is fetched, so a cached list can never show one.
Bottlenecks and failures
At 10x (5 billion posts a day), today's index takes 170,000 posts a second at peak, so it needs about 10 times the shards, and the recent 7 days grow to 5 TB of memory. The like stream grows to 1.2 million events a second; it is partitioned by post id so aggregators scale out. The first thing to hurt is usually fan-out latency, which you fix with fewer, larger rolled-up indexes for old data and narrower default windows.
| When this dies | What happens | What you do |
|---|---|---|
| An indexer | New posts become searchable late. | The stream keeps events; it resumes from its offset and skips versions already applied. |
| A recent-index replica | Its shard has fewer copies. | Queries go to the other replicas; rebuild from the stream and the posts database. |
| A whole shard | About 1 in 20 of that day's posts is missing. | Return partial: true rather than an error, and page someone. |
| The like count store | Likes sort cannot order within a tier. | Sort by tier and then recency; mark the result approximate. |
| The query cache | Every search reaches the shards: 7,000 a second at peak. | Size the shards for it; the cache is a saving, not a requirement. |
| The index as a whole | Search is down. | Rebuild from the posts database, newest days first, so useful results return within hours. |
What a strong answer sounds like
| Level | What the answer does |
|---|---|
| Junior | Puts posts into a search engine, sorts by date or likes in the query, and adds a cache. |
| Mid | Feeds the index from a change stream, shards by document, notices that likes change constantly and keeps counts outside the index. |
| Senior | Leads with the 50-to-1 write ratio, shards by time then document, uses early termination for newest-first, uses tiers for likes with the arithmetic, caches closed days, and explains tail latency from fan-out. |
How do you sort results by likes?
Store the like count in the index and update it when someone likes a post.
How do you sort results by likes?
Likes come in at 116,000 a second, so they can't rewrite the index. I'd index a tier, powers of ten, which changes a few times per post, and keep exact counts in a key-value store. A query takes the top tiers until it has at least 20 candidates, looks up those counts and sorts. The cost: a post that just crossed a tier is misplaced for a few seconds.
Follow-up questions
How do you add relevance ranking?
Fetch a few hundred candidates per shard by a cheap score (text match plus tier plus recency), then re-rank the merged list with a richer model in the search service. The two-stage shape keeps expensive scoring to a few hundred posts.
A post is deleted. How fast does it disappear?
Immediately for users: the service filters ids whose status is deleted when it fetches text. The index entry is marked dead within seconds and physically removed at the next merge.
How would you search hashtags or a single user's posts?
A hashtag is just another token. "From this user" adds an author_id field to the document and a filter in the query.
How do you change the analyser, say to add stemming?
Build new indexes alongside the old ones, newest days first, by replaying from the posts database, then switch reads one day at a time. Old and new can run side by side because each day is a separate index.
Why not use the database's full-text search?
At 75 GB of new index a day and 5,800 inserts a second, a single database's text index cannot keep up or be split by time cleanly. For a few million posts it would be the right first answer.
After your attempt, tick what you really did. Be strict: "I thought about it" is not "I said it out loud".