The Assembler
In one line: this is the offline half, and it is a clean example of a pipeline where each stage uses a different store because the data changes shape at every step.
Why it exists
Trie creation and updates should not occur in the critical path of a user's query. Real-time updates are avoided because:
- Scale: millions of users enter queries every second. Updating in real time would significantly slow the suggestion service.
- Relevance: top suggestions do not change frequently enough to require immediate updates.
The second reason is the one that licenses the whole architecture
Scale explains why real-time updates are hard. Relevance explains why they are unnecessary — and that is the stronger argument.
If the top suggestions for "wea" changed every few seconds, no amount of batching would help; you would be forced into real-time updates and the 200 ms budget would be unmeetable.
They do not. "weather" has been the top completion of "wea" for years. The head of the distribution is extremely stable, and only the long tail churns.
A tight read latency is affordable only when the data is stable enough to precompute — Lesson 1's pairing, stated here as a design justification. And it is why a 15-minute staleness window costs nothing perceptible.
The three stages
| Stage | What it does | Store |
|---|---|---|
| Collection service | Logs the phrase, timestamp, and metadata for later processing | HDFS — massive volume of raw data |
| Aggregator | MapReduce job aggregates prefix frequencies over a time interval | Cassandra — tabular, regularly updated |
| Trie builder | Creates or updates tries from aggregated data, published via ZooKeeper | MongoDB — trie snapshots for persistence and recovery |
Four stores because the data changes shape four times
This looks like technology sprawl and it is not. Trace what the data is at each step:
| Stage | Shape | Access pattern | Store |
|---|---|---|---|
| Raw log | An append-only stream | Written once, read in bulk by batch jobs | HDFS |
| Aggregated | A table — phrase, frequency, interval | Written in bulk, read by key | Cassandra |
| Built trie | A tree — a nested document | Written whole, read whole | MongoDB |
| Serving | Top-10 lists per prefix | Read at 607,000/second | Redis |
Each store matches its stage's shape and access pattern, and none could reasonably do another's job:
HDFS for the raw log because it is enormous, append-only, and read by full scans — exactly what a distributed filesystem is for and exactly what a database is not.
Cassandra for aggregates because they are a wide table written in bulk on a schedule.
MongoDB for tries because a trie is a nested document, and serializing a tree into relational rows is painful.
Redis for serving because Lesson 1 requires memory.
A pipeline that transforms data should expect to change stores when the shape changes, and each transition here is justified. The failure mode to avoid is the opposite — forcing one store to serve four shapes because it is the one you already run.
MapReduce is the right tool, and the reason is the access pattern
The aggregation is: read a very large log, group by phrase, sum, emit. That is exactly MapReduce's canonical shape — the word-count example, at planetary scale.
Two properties make it fit:
It is embarrassingly parallel. Counting phrase frequencies partitions cleanly — map emits (phrase, 1), reduce sums per phrase, and no reducer needs another's data.
It reads everything and writes little. Lesson 2's 52.5 billion daily keystroke events collapse to a table of distinct phrases with counts. A full scan producing a small result is what batch processing is for.
Worth noting the interval choice: Fifteen minutes against Lesson 6's observation that top suggestions change slowly is comfortable.
When aggregation is associative and the output is far smaller than the input, batch processing is right and streaming is over-engineering.
Timestamps are collected and never used for decay
The collection service records "the phrase, timestamp, and metadata," and the design explains: "timestamps are recorded to track when to update a phrase's frequency."
So time is captured, and the aggregator uses it only to bucket into intervals — the output table is phrase, frequency, and time interval.
What it never does is weight by recency, and that is a missed opportunity Lesson 4 already anticipated when discussing counter overflow.
Consider two queries with equal total counts:
"world cup final" -> 10,000 searches, ALL from three years ago "world cup fixtures" -> 10,000 searches, ALL from this week
Ranked by raw frequency they tie. Ranked by time-decayed frequency, the current one wins, which is obviously right.
Since timestamps are already in the log and the aggregator already buckets by interval, exponential decay is nearly free to add — weight each interval's contribution by its age. It would also solve the overflow problem Lesson 4 raised, because decayed sums converge rather than growing without bound.
When you are already collecting time and already bucketing by it, decay costs one multiplication and buys both recency and boundedness.
A shared trie, which the evaluation later contradicts
The design's own question — per-user tries or one shared trie — is answered clearly:
With billions of users, maintaining a separate trie for each user would be impractical, and would create duplicate tries when users issue similar queries. Therefore our design assumes a common trie shared among users.
Correct, and it is what makes Lesson 3's observation hold: getSuggestions(prefix) takes no user ID, so the response is cacheable across everyone.
Hold onto this, because Lesson 8's evaluation says suggestions "should incorporate the user's search history, location, and language preferences" and "prioritizes personalized matches over global results."
Those two positions are in direct tension, and the next lesson works through the resolution.
The assembler exists so the query path can be trivial. Everything expensive — counting, filtering, ranking, building — happens here, on a clock measured in hours rather than milliseconds.
Key takeaway
The assembler exists for two reasons, and the second licenses the architecture: real-time updates are hard and unnecessary, because top suggestions are extremely stable — a tight read latency is affordable only when the data is stable enough to precompute. Its four stores are justified because the data changes shape four times — an append-only stream, a table, a nested document, a memory-resident lookup — and each transition matches a real change in access pattern. MapReduce fits because the aggregation is associative and the output is far smaller than the input. And timestamps are collected and never used for decay, which is a missed opportunity: since time is already bucketed, exponential decay would cost one multiplication and solve both recency and the counter-overflow problem Lesson 4 raised.
Next: the evaluation, the client, and the personalization contradiction.