System Design (High-Level Design) Module 23 – Case studies: social, feeds and search
Design top-K trending topics
Design trending topics: count mentions in sliding windows, score growth against each topic’s baseline, merge regional top-k lists and keep bots out.
What you will learn
- Estimate the mention rate, counter memory and read load of a trending service
- Define trending as growth against a topic's own baseline, with a floor on volume
- Count heavy hitters over sliding windows with sketches and an exact recount
- Merge regional and partition top-k lists correctly, and filter coordinated manipulation
Before you start
What you’ll learn · Before you start
What you will learn
- Estimate the mention rate, counter memory and read load of a trending service
- Define trending as growth against a topic's own baseline, with a floor on volume
- Count heavy hitters over sliding windows with sketches and an exact recount
- Merge regional and partition top-k lists correctly, and filter coordinated manipulation
Before you start
On this page
At 500 million posts a day, a trending service counts about 11,574 topic mentions a second, and five times as many during a big event. Its list shows what people are suddenly talking about, not what they always talk about: every minute it finds the topics mentioned most in the last few minutes, and which of those are growing faster than usual.
The pipeline counts each topic in one-minute panes, compares each busy topic with its own baseline, discounts manipulation, and merges regional and world lists that a cache serves to everyone. Exact counts cost too much at the peak, so the counters keep sketch (data summary): A small, fixed-size summary of a stream that answers one question, such as how many distinct items it held or how often an item occurred, with a stated error instead of an exact answer. More in “HyperLogLog, Count-Min and heavy hitters”, approximate counts in fixed memory, corrected by an exact recount of a few candidates. This case study sizes the service, follows its paths, and shows why popular and trending are different lists.
A trend is a topic growing faster than usual, listed every minute
Questions first, because “trending” means different things to different products:
- What is a topic? Hashtags and named entities extracted from posts, one mention per topic per post.
- What is a trend? A topic mentioned far more often than its own usual rate, and often enough to be real.
- Which lists? The top 50 trending topics for each of 100 regions and for the whole world.
- Over what time? The last 15 minutes and the last hour, refreshed every minute.
- How fast? A real surge should appear within two minutes of starting.
- How exact? Approximate counts are fine; a list that is slightly out of order is fine; a list that a bot network controls is not.
| Aspect | Popular | Trending |
|---|---|---|
| Measures | mentions in the window | the window against the topic’s usual rate |
| Led by | the same big topics every day | whatever is suddenly growing |
| Needs | a count per topic | a count, a baseline and a floor on volume |
The functional requirements: count mentions by region over sliding windows, score topics by growth against their usual rate, publish regional and global top-k lists every minute, and let operators remove a topic. The non-functional ones: absorb five times the average mention rate during an event, serve the lists with a p99 (99th percentile): The latency within which 99 % of requests finish, so the slowest 1 % take longer. Tail latency is read from such high percentiles, because an average hides the slowest requests. More in “Latency, throughput and percentiles” of 50 ms, tolerate late and repeated mentions, and stay correct when one topic carries a large share of the stream.
Mentions peak near 58,000 a second, and no read reaches a counter
| Quantity | Assumption | Result |
|---|---|---|
| Mentions | 500 million posts a day, 2 topics each | 11,574 a second on average; about 58,000 at a 5 × event peak |
| One minute at the peak | 58,000 a second × 60 | about 3.5 million mentions a minute |
| Exact counts for a minute | 2 million distinct topics × about 50 bytes | about 100 MB a minute, more in an event |
| A sketch for a minute | 4 rows × 1,048,576 counters × 4 bytes | 16.8 MB, whatever the number of topics |
| An hour of panes | 60 one-minute sketches | about 1 GB in total, across all partitions |
| Reads of the lists | 100 million people × 3 views a day | 3,472 a second, served from a cache |
Counting is a stream problem and reading is a caching problem: the lists change once a minute and are the same for everyone in a region, so the 3,472 reads a second never reach the counters.
Checkpoint
Check the scope and the estimate
Four records carry the stream: mentions, panes, baselines and lists
GET /v1/trends?region=IN-MH&window=1h&limit=20returns the current list: topic, score, the count in the window and its usual rate, and a few example post ids.GET /v1/trends/globalreturns the merged world list.POST /v1/trends/blockslets operators remove a topic from every list.
| Data | Key | Holds |
|---|---|---|
| Mention event | topic | the topic, region, account id, post id and event time |
| Window pane | partition, minute | one sketch of that minute’s counts, and the candidate topics |
| Baseline | topic, region | the topic’s usual mentions a minute, for each hour of the week |
| Published list | region, window | the top 50 with scores, rebuilt every minute |
Panes add up by minute into any window, and lists are read whole by region.
Topics are counted once, scored every minute and served from a cache
From mentions to a trends list, refreshed every minute
Text description of the diagram
The post service extracts the topics of each post and (1) appends one mention per topic, with its region, account and time, to the mention log, which is partitioned by topic.
The window counters (2) count mentions in one-minute panes for each partition, and (3) pass the candidate topics and their counts to the trend scorer. The scorer compares each candidate's count in the window with its usual rate, read from the baseline store, and (4) passes the scores to the abuse filter, which weighs distinct accounts and removes known bots.
The merger (5) combines the clean lists into regional and global top-k lists and (6) publishes them to the trends cache, refreshed every minute. Apps read trends through the trends API, which reads only the cache.
Count. The post service writes one mention event per topic to a log partitioned by topic, so all mentions of a topic meet in one partition. Each partition keeps one-minute panes: a sketch of every topic’s count and a short list of the topics with the highest counts. Score. Every minute, the scorer sums the panes of the window for each candidate and compares the total with the topic’s usual rate for that hour, read from the baseline store; a candidate must also pass a minimum volume. Merge. The abuse filter discounts topics carried by few accounts, and the merger combines the partitions’ lists into each region’s top 50 and the global list. Serve. The lists go to a cache, and the API reads only the cache. Follow one mention through those steps:
The drawing shows the whole scene; its 8 steps are listed below.
Every step as text
- A post names a topic. A post about a solar storm is published. The post service extracts its topics and writes one mention event for each, with the region, the account and the time.
- One partition per topic. The mention log is partitioned by topic, so every mention of solar-storm, from every post server, reaches one partition and is counted in one place.
- Counted into its minute. The partition adds the mention to the pane of its minute. A pane holds one minute of counts, and a 5-minute window is the sum of the last five panes.
- Scored against its usual rate. Every minute the scorer compares each busy topic's window with the topic's usual rate. Solar-storm has 2,983 mentions where 212 were expected.
- Small topics are not listed. A topic needs 200 mentions in the window to be listed at all. A local quiz went from about 5 mentions to 52, eleven times its usual rate, and stays off the list.
- Many people, not a few accounts. The abuse filter discounts topics carried by a few accounts posting again and again. Thousands of different accounts mention solar-storm, so its score stands.
- Regional and world lists. The merger combines the partitions' candidates into the top 50 of each of 100 regions and into the world list.
- Every read from the cache. The lists go to the trends cache once a minute, the same for everyone in a region, so about 3,472 reads a second never reach the counters.
Checkpoint
Check: The four records and their keys · Count, score, merge and serve
Surprise against a baseline separates trending from popular
The program below shows why the score compares growth and not volume. For ten topics it takes the mentions in the 20 minutes before the window and in the 5-minute window itself: some topics always busy, one surging, one tiny topic that jumps. Surprise is the window’s count above the expected count, in rough standard deviations.
Python 3.14 · main.py
from math import sqrt
FLOOR = 200 # mentions a topic needs in the window to be eligible at all
TOPICS = [ # topic, mentions in the 20 minutes before the window, mentions in the 5-minute window
("weather", 18015, 4504),
("football", 15897, 4154),
("elections", 13084, 3261),
("movies", 10397, 2774),
("music", 9631, 2323),
("recipes", 6111, 1439),
("solar-storm", 847, 2983),
("chess-final", 2461, 1609),
("new-phone", 3974, 1280),
("local-quiz", 19, 52),
]
def expected(baseline: int) -> float:
return baseline / 20 * 5 # the window's 5 minutes at the topic's usual rate
def surprise(baseline: int, window: int) -> float:
"""Rough standard deviations above the usual rate, smoothed for small counts."""
return (window - expected(baseline)) / sqrt(expected(baseline) + 50)
by_volume = sorted(TOPICS, key=lambda t: -t[2])
by_surprise = sorted(TOPICS, key=lambda t: -surprise(t[1], t[2]))
floored = [t for t in by_surprise if t[2] >= FLOOR]
for title, ranked in [
("volume", by_volume),
("surprise", by_surprise),
("floor", floored),
]:
print(f"top 5 by {title:<9} {' '.join(t[0] for t in ranked[:5])}")
print("\ntopic window expected surprise")
for topic, base, window in by_surprise[:5]:
print(
f"{topic:<12} {window:>6} {expected(base):>8.0f} {surprise(base, window):>8.1f}"
) In Python: sorted() with a key of -surprise sorts from the highest, and a list comprehension keeps the topics above the floor.
Recorded with CPython 3.14.8 on macOS 26 arm64. It runs in your browser with Pyodide 314.0.7 (CPython 3.14.2). To run it yourself: python3 main.py
JavaScript Node 24 · main.mjs
const FLOOR = 200; // mentions a topic needs in the window to be eligible at all
const TOPICS = [
// topic, mentions in the 20 minutes before the window, mentions in the 5-minute window
["weather", 18015, 4504],
["football", 15897, 4154],
["elections", 13084, 3261],
["movies", 10397, 2774],
["music", 9631, 2323],
["recipes", 6111, 1439],
["solar-storm", 847, 2983],
["chess-final", 2461, 1609],
["new-phone", 3974, 1280],
["local-quiz", 19, 52],
];
// the window's 5 minutes at the topic's usual rate
const expected = (baseline) => (baseline / 20) * 5;
/** Rough standard deviations above the usual rate, smoothed for small counts. */
const surprise = (baseline, window) =>
(window - expected(baseline)) / Math.sqrt(expected(baseline) + 50);
const byVolume = [...TOPICS].sort((a, b) => b[2] - a[2]);
const bySurprise = [...TOPICS].sort((a, b) => surprise(b[1], b[2]) - surprise(a[1], a[2]));
const floored = bySurprise.filter((t) => t[2] >= FLOOR);
for (const [title, ranked] of [
["volume", byVolume],
["surprise", bySurprise],
["floor", floored],
]) {
console.log(
`top 5 by ${title.padEnd(9)} ${ranked
.slice(0, 5)
.map((t) => t[0])
.join(" ")}`,
);
}
console.log("\ntopic window expected surprise");
for (const [topic, base, window] of bySurprise.slice(0, 5)) {
const numbers = [
expected(base).toFixed(0).padStart(8),
surprise(base, window).toFixed(1).padStart(8),
];
console.log(`${topic.padEnd(12)} ${String(window).padStart(6)} ${numbers.join(" ")}`);
} In JavaScript: sort takes a comparator, so b minus a sorts from the highest, and [...TOPICS] sorts a copy and keeps the original order.
Recorded with Node.js 24.21.0 LTS (V8 13.6) on macOS 26 arm64. It runs in your browser with QuickJS (quickjs-emscripten 0.32.0). To run it yourself: node main.mjs
Java 25 LTS · Main.java
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.stream.Collectors;
public class Main {
static final int FLOOR = 200; // mentions a topic needs in the window to be eligible at all
// topic, mentions in the 20 minutes before the window, mentions in the 5-minute window
record Topic(String name, int baseline, int window) {
double expected() {
return baseline / 20.0 * 5; // the window's 5 minutes at the topic's usual rate
}
/** Rough standard deviations above the usual rate, smoothed for small counts. */
double surprise() {
return (window - expected()) / Math.sqrt(expected() + 50);
}
}
static String names(List<Topic> ranked) {
return ranked.stream().limit(5).map(Topic::name).collect(Collectors.joining(" "));
}
public static void main(String[] args) {
Locale.setDefault(Locale.ROOT);
List<Topic> topics =
List.of(
new Topic("weather", 18015, 4504),
new Topic("football", 15897, 4154),
new Topic("elections", 13084, 3261),
new Topic("movies", 10397, 2774),
new Topic("music", 9631, 2323),
new Topic("recipes", 6111, 1439),
new Topic("solar-storm", 847, 2983),
new Topic("chess-final", 2461, 1609),
new Topic("new-phone", 3974, 1280),
new Topic("local-quiz", 19, 52));
List<Topic> byVolume =
topics.stream().sorted(Comparator.comparingInt(Topic::window).reversed()).toList();
List<Topic> bySurprise =
topics.stream().sorted(Comparator.comparingDouble(Topic::surprise).reversed()).toList();
List<Topic> floored = bySurprise.stream().filter(t -> t.window() >= FLOOR).toList();
System.out.println("top 5 by volume " + names(byVolume));
System.out.println("top 5 by surprise " + names(bySurprise));
System.out.println("top 5 by floor " + names(floored));
System.out.println("\ntopic window expected surprise");
for (Topic t : bySurprise.subList(0, 5)) {
System.out.printf(
"%-12s %6d %8.0f %8.1f%n", t.name(), t.window(), t.expected(), t.surprise());
}
}
} In Java: The record computes expected() and surprise() as methods, and Comparator.comparingDouble(Topic::surprise).reversed() sorts from the highest.
Recorded with OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. It runs in your browser with a TeaVM 0.16.0 build in QuickJS (quickjs-emscripten 0.32.0). To run it yourself: java Main.java
C++23 · main.cpp
#include <algorithm>
#include <cmath>
#include <print>
#include <string>
#include <vector>
// mentions a topic needs in the window to be eligible at all
constexpr int FLOOR = 200;
// topic, mentions in the 20 minutes before the window, mentions in the 5-minute
// window
struct Topic {
std::string name;
int baseline, window;
// the window's 5 minutes at the topic's usual rate
double expected() const { return baseline / 20.0 * 5; }
// Rough standard deviations above the usual rate, smoothed for small counts.
double surprise() const {
return (window - expected()) / std::sqrt(expected() + 50);
}
};
void print_top(const char *title, const std::vector<Topic> &ranked) {
std::print("top 5 by {:<9}", title);
for (size_t i = 0; i < 5; i++)
std::print(" {}", ranked[i].name);
std::println();
}
int main() {
const std::vector<Topic> topics = {
{"weather", 18015, 4504}, {"football", 15897, 4154},
{"elections", 13084, 3261}, {"movies", 10397, 2774},
{"music", 9631, 2323}, {"recipes", 6111, 1439},
{"solar-storm", 847, 2983}, {"chess-final", 2461, 1609},
{"new-phone", 3974, 1280}, {"local-quiz", 19, 52}};
auto by_volume = topics, by_surprise = topics;
std::ranges::sort(by_volume, std::greater{}, &Topic::window);
std::ranges::sort(by_surprise, std::greater{}, &Topic::surprise);
std::vector<Topic> floored;
std::ranges::copy_if(by_surprise, std::back_inserter(floored),
[](const Topic &t) { return t.window >= FLOOR; });
print_top("volume", by_volume);
print_top("surprise", by_surprise);
print_top("floor", floored);
std::println("\ntopic window expected surprise");
for (size_t i = 0; i < 5; i++) {
const auto &t = by_surprise[i];
std::println("{:<12} {:>6} {:>8.0f} {:>8.1f}", t.name, t.window,
t.expected(), t.surprise());
}
} In C++: std::ranges::sort takes a projection, &Topic::surprise, so std::greater{} sorts from the highest, and copy_if keeps the topics above the floor.
Recorded with Apple clang 21.0.0 (C++23) and GCC 16.2.0 on macOS 26 arm64. It runs in your browser with Clang in WebAssembly (YoWASP) 22.0.0-git20542-10. To run it yourself: clang++ -std=c++23 -O2 -Wall -Wextra -Werror -pedantic -fsanitize=address,undefined main.cpp -o main && ./main
C# 14 · .NET 10 · app.cs
using static System.FormattableString;
const int Floor = 200; // mentions a topic needs in the window to be eligible at all
Topic[] topics =
[
new("weather", 18015, 4504),
new("football", 15897, 4154),
new("elections", 13084, 3261),
new("movies", 10397, 2774),
new("music", 9631, 2323),
new("recipes", 6111, 1439),
new("solar-storm", 847, 2983),
new("chess-final", 2461, 1609),
new("new-phone", 3974, 1280),
new("local-quiz", 19, 52),
];
var byVolume = topics.OrderByDescending(t => t.Window).ToList();
var bySurprise = topics.OrderByDescending(t => t.Surprise).ToList();
var floored = bySurprise.Where(t => t.Window >= Floor).ToList();
(string Title, List<Topic> Ranked)[] rankings =
[("volume", byVolume), ("surprise", bySurprise), ("floor", floored)];
foreach (var (title, ranked) in rankings)
{
string names = string.Join(" ", ranked.Take(5).Select(t => t.Name));
Console.WriteLine($"top 5 by {title,-9} {names}");
}
Console.WriteLine("\ntopic window expected surprise");
foreach (var t in bySurprise.Take(5))
{
Console.WriteLine(Invariant($"{t.Name,-12} {t.Window,6} {t.Expected,8:F0} {t.Surprise,8:F1}"));
}
// topic, mentions in the 20 minutes before the window, mentions in the 5-minute window
record Topic(string Name, int Baseline, int Window)
{
// the window's 5 minutes at the topic's usual rate
public double Expected => Baseline / 20.0 * 5;
// Rough standard deviations above the usual rate, smoothed for small counts.
public double Surprise => (Window - Expected) / Math.Sqrt(Expected + 50);
} In C#: Expected and Surprise are computed properties of the record, OrderByDescending sorts by them, and Invariant formats the numbers the same way in every culture.
Recorded with C# 14 on .NET 10.0.12 (SDK 10.0.401) on macOS 26 arm64. It runs in your browser with the .NET runtime for WebAssembly 10.0.12. To run it yourself: dotnet run app.cs
C23 · main.c
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
// FLOOR: mentions a topic needs in the window to be eligible at all
enum { FLOOR = 200, N = 10 };
// topic, mentions in the 20 minutes before the window, mentions in the 5-minute
// window
typedef struct {
const char *name;
int baseline, window;
} Topic;
// the window's 5 minutes at the topic's usual rate
static double expected(const Topic *t) { return t->baseline / 20.0 * 5; }
// Rough standard deviations above the usual rate, smoothed for small counts.
static double surprise(const Topic *t) {
return (t->window - expected(t)) / sqrt(expected(t) + 50);
}
static int by_volume(const void *x, const void *y) {
return ((const Topic *)y)->window - ((const Topic *)x)->window;
}
static int by_surprise(const void *x, const void *y) {
const double a = surprise(x), b = surprise(y);
return (a < b) - (a > b);
}
int main(void) {
Topic t[N] = {{"weather", 18015, 4504}, {"football", 15897, 4154},
{"elections", 13084, 3261}, {"movies", 10397, 2774},
{"music", 9631, 2323}, {"recipes", 6111, 1439},
{"solar-storm", 847, 2983}, {"chess-final", 2461, 1609},
{"new-phone", 3974, 1280}, {"local-quiz", 19, 52}};
qsort(t, N, sizeof t[0], by_volume);
printf("top 5 by volume ");
for (int i = 0; i < 5; i++)
printf(" %s", t[i].name);
qsort(t, N, sizeof t[0], by_surprise);
printf("\ntop 5 by surprise ");
for (int i = 0; i < 5; i++)
printf(" %s", t[i].name);
printf("\ntop 5 by floor ");
for (int i = 0, shown = 0; i < N && shown < 5; i++)
if (t[i].window >= FLOOR) {
printf(" %s", t[i].name);
shown++;
}
printf("\n\ntopic window expected surprise\n");
for (int i = 0; i < 5; i++)
printf("%-12s %6d %8.0f %8.1f\n", t[i].name, t[i].window, expected(&t[i]),
surprise(&t[i]));
return 0;
} In C: qsort sorts in place, so the volume ranking is printed before the array is sorted again by surprise; the comparator turns two doubles into -1, 0 or 1.
Recorded with Apple clang 21.0.0 (C23) and GCC 16.2.0 on macOS 26 arm64. It runs in your browser with Clang in WebAssembly (YoWASP) 22.0.0-git20542-10. To run it yourself: clang -std=c23 -O2 -Wall -Wextra -Werror -pedantic -fsanitize=address,undefined main.c -o main && ./main
PHP 8.5 · main.php
<?php
declare(strict_types=1);
const FLOOR = 200; // mentions a topic needs in the window to be eligible at all
// topic, mentions in the 20 minutes before the window, mentions in the 5-minute window
const TOPICS = [
['weather', 18015, 4504],
['football', 15897, 4154],
['elections', 13084, 3261],
['movies', 10397, 2774],
['music', 9631, 2323],
['recipes', 6111, 1439],
['solar-storm', 847, 2983],
['chess-final', 2461, 1609],
['new-phone', 3974, 1280],
['local-quiz', 19, 52],
];
function expected(int $baseline): float
{
return $baseline / 20 * 5; // the window's 5 minutes at the topic's usual rate
}
/** Rough standard deviations above the usual rate, smoothed for small counts. */
function surprise(int $baseline, int $window): float
{
return ($window - expected($baseline)) / sqrt(expected($baseline) + 50);
}
$byVolume = TOPICS;
usort($byVolume, fn(array $a, array $b): int => $b[2] <=> $a[2]);
$bySurprise = TOPICS;
$scoreOf = fn(array $t): float => surprise($t[1], $t[2]);
usort($bySurprise, fn(array $a, array $b): int => $scoreOf($b) <=> $scoreOf($a));
$floored = array_values(array_filter($bySurprise, fn(array $t): bool => $t[2] >= FLOOR));
$rankings = ['volume' => $byVolume, 'surprise' => $bySurprise, 'floor' => $floored];
foreach ($rankings as $title => $ranked) {
printf("top 5 by %-9s %s\n", $title, implode(' ', array_column(array_slice($ranked, 0, 5), 0)));
}
echo "\ntopic window expected surprise\n";
foreach (array_slice($bySurprise, 0, 5) as [$topic, $base, $window]) {
printf("%-12s %6d %8.0f %8.1f\n", $topic, $window, expected($base), surprise($base, $window));
} In PHP: usort sorts a copy of the constant array, because arrays are values, and <=> compares two surprises from the highest when its operands are swapped.
Recorded with PHP 8.5.11 (CLI) on macOS 26 arm64. It runs in your browser with php-wasm 0.2.0 (PHP 8.5.2). To run it yourself: php main.php
Rust 1.99 · main.rs
const FLOOR: u32 = 200; // mentions a topic needs in the window to be eligible at all
// topic, mentions in the 20 minutes before the window, mentions in the 5-minute window
const TOPICS: [(&str, u32, u32); 10] = [
("weather", 18015, 4504),
("football", 15897, 4154),
("elections", 13084, 3261),
("movies", 10397, 2774),
("music", 9631, 2323),
("recipes", 6111, 1439),
("solar-storm", 847, 2983),
("chess-final", 2461, 1609),
("new-phone", 3974, 1280),
("local-quiz", 19, 52),
];
/// The window's 5 minutes at the topic's usual rate.
fn expected(baseline: u32) -> f64 {
f64::from(baseline) / 20.0 * 5.0
}
/// Rough standard deviations above the usual rate, smoothed for small counts.
fn surprise(baseline: u32, window: u32) -> f64 {
(f64::from(window) - expected(baseline)) / (expected(baseline) + 50.0).sqrt()
}
fn main() {
let mut by_volume = TOPICS.to_vec();
by_volume.sort_by_key(|t| std::cmp::Reverse(t.2));
let mut by_surprise = TOPICS.to_vec();
by_surprise.sort_by(|a, b| surprise(b.1, b.2).total_cmp(&surprise(a.1, a.2)));
let floored: Vec<_> = by_surprise
.iter()
.copied()
.filter(|t| t.2 >= FLOOR)
.collect();
for (title, ranked) in [
("volume", &by_volume),
("surprise", &by_surprise),
("floor", &floored),
] {
let names: Vec<&str> = ranked.iter().take(5).map(|t| t.0).collect();
println!("top 5 by {title:<9} {}", names.join(" "));
}
println!("\ntopic window expected surprise");
for &(topic, base, window) in &by_surprise[..5] {
let (e, s) = (expected(base), surprise(base, window));
println!("{topic:<12} {window:>6} {e:>8.0} {s:>8.1}");
}
} In Rust: f64 has no total order of its own, so total_cmp sorts the surprises, and std::cmp::Reverse sorts the volumes from the highest.
Recorded with rustc 1.99.0 (edition 2024) on macOS 26 arm64. It runs in your browser with a WebAssembly build on the WASI runner (browser_wasi_shim 0.4.2). To run it yourself: rustc --edition 2024 -O main.rs && ./main
TypeScript 6.0 · main.ts
type Topic = [name: string, baseline: number, window: number];
const FLOOR = 200; // mentions a topic needs in the window to be eligible at all
const TOPICS: Topic[] = [
// topic, mentions in the 20 minutes before the window, mentions in the 5-minute window
["weather", 18015, 4504],
["football", 15897, 4154],
["elections", 13084, 3261],
["movies", 10397, 2774],
["music", 9631, 2323],
["recipes", 6111, 1439],
["solar-storm", 847, 2983],
["chess-final", 2461, 1609],
["new-phone", 3974, 1280],
["local-quiz", 19, 52],
];
// the window's 5 minutes at the topic's usual rate
const expected = (baseline: number): number => (baseline / 20) * 5;
/** Rough standard deviations above the usual rate, smoothed for small counts. */
const surprise = (baseline: number, window: number): number =>
(window - expected(baseline)) / Math.sqrt(expected(baseline) + 50);
const byVolume = [...TOPICS].sort((a, b) => b[2] - a[2]);
const bySurprise = [...TOPICS].sort((a, b) => surprise(b[1], b[2]) - surprise(a[1], a[2]));
const floored = bySurprise.filter((t) => t[2] >= FLOOR);
const rankings: [string, Topic[]][] = [
["volume", byVolume],
["surprise", bySurprise],
["floor", floored],
];
for (const [title, ranked] of rankings) {
console.log(
`top 5 by ${title.padEnd(9)} ${ranked
.slice(0, 5)
.map((t) => t[0])
.join(" ")}`,
);
}
console.log("\ntopic window expected surprise");
for (const [topic, base, window] of bySurprise.slice(0, 5)) {
const numbers = [
expected(base).toFixed(0).padStart(8),
surprise(base, window).toFixed(1).padStart(8),
];
console.log(`${topic.padEnd(12)} ${String(window).padStart(6)} ${numbers.join(" ")}`);
} In TypeScript: The labelled tuple type Topic names the three fields, and rankings is typed as pairs of a title and a ranked list.
Recorded with TypeScript 6.0.3 (strict), run by Node.js 24.21.0 on macOS 26 arm64. It runs in your browser with QuickJS (quickjs-emscripten 0.32.0) after Sucrase 3.35.1. To run it yourself: npx -p typescript@6.0.3 tsc --noEmit --strict main.ts && node main.ts
Go 1.27 · main.go
package main
import (
"cmp"
"fmt"
"math"
"slices"
"strings"
)
const floor = 200 // mentions a topic needs in the window to be eligible at all
// topic, mentions in the 20 minutes before the window, mentions in the 5-minute window
type topic struct {
name string
baseline, window int
}
// expected is the window's 5 minutes at the topic's usual rate.
func (t topic) expected() float64 { return float64(t.baseline) / 20 * 5 }
// surprise is roughly how many standard deviations above the usual rate, smoothed for small counts.
func (t topic) surprise() float64 {
return (float64(t.window) - t.expected()) / math.Sqrt(t.expected()+50)
}
func top5(ranked []topic) string {
names := []string{}
for _, t := range ranked[:5] {
names = append(names, t.name)
}
return strings.Join(names, " ")
}
func main() {
topics := []topic{
{"weather", 18015, 4504}, {"football", 15897, 4154}, {"elections", 13084, 3261},
{"movies", 10397, 2774}, {"music", 9631, 2323}, {"recipes", 6111, 1439},
{"solar-storm", 847, 2983}, {"chess-final", 2461, 1609}, {"new-phone", 3974, 1280},
{"local-quiz", 19, 52},
}
byVolume, bySurprise := slices.Clone(topics), slices.Clone(topics)
slices.SortFunc(byVolume, func(a, b topic) int { return cmp.Compare(b.window, a.window) })
slices.SortFunc(bySurprise, func(a, b topic) int {
return cmp.Compare(b.surprise(), a.surprise())
})
var floored []topic
for _, t := range bySurprise {
if t.window >= floor {
floored = append(floored, t)
}
}
fmt.Println("top 5 by volume " + top5(byVolume))
fmt.Println("top 5 by surprise " + top5(bySurprise))
fmt.Println("top 5 by floor " + top5(floored))
fmt.Println("\ntopic window expected surprise")
for _, t := range bySurprise[:5] {
fmt.Printf("%-12s %6d %8.0f %8.1f\n", t.name, t.window, t.expected(), t.surprise())
}
} In Go: expected and surprise are methods of the topic struct, and cmp.Compare with its arguments swapped sorts from the highest.
Recorded with Go 1.27.1 on macOS 26 arm64. It runs in your browser with the Go compiler in WebAssembly 1.27.1. To run it yourself: go run main.go
Swift 6.4 · main.swift
let floor = 200 // mentions a topic needs in the window to be eligible at all
// topic, mentions in the 20 minutes before the window, mentions in the 5-minute window
struct Topic {
let name: String
let baseline: Int
let window: Int
/// The window's 5 minutes at the topic's usual rate.
var expected: Double { Double(baseline) / 20 * 5 }
/// Rough standard deviations above the usual rate, smoothed for small counts.
var surprise: Double { (Double(window) - expected) / (expected + 50).squareRoot() }
}
let topics = [
("weather", 18015, 4504), ("football", 15897, 4154), ("elections", 13084, 3261),
("movies", 10397, 2774), ("music", 9631, 2323), ("recipes", 6111, 1439),
("solar-storm", 847, 2983), ("chess-final", 2461, 1609), ("new-phone", 3974, 1280),
("local-quiz", 19, 52),
].map { Topic(name: $0.0, baseline: $0.1, window: $0.2) }
func pad(_ text: String, _ width: Int, left: Bool = false) -> String {
let spaces = String(repeating: " ", count: max(0, width - text.count))
return left ? text + spaces : spaces + text
}
/// x rounded to one digit after the point
func oneDecimal(_ x: Double) -> String {
let n = Int((x * 10).rounded())
return "\(n / 10).\(n % 10)"
}
let byVolume = topics.sorted { $0.window > $1.window }
let bySurprise = topics.sorted { $0.surprise > $1.surprise }
let floored = bySurprise.filter { $0.window >= floor }
for (title, ranked) in [("volume", byVolume), ("surprise", bySurprise), ("floor", floored)] {
print(
"top 5 by " + pad(title, 9, left: true) + " "
+ ranked.prefix(5).map(\.name).joined(separator: " "))
}
print("\ntopic window expected surprise")
for t in bySurprise.prefix(5) {
let numbers = pad(String(t.window), 6) + pad(String(Int(t.expected.rounded())), 9)
print(pad(t.name, 12, left: true) + " " + numbers + pad(oneDecimal(t.surprise), 9))
} In Swift: expected and surprise are computed properties of the struct; without Foundation the standard library has no number format, so oneDecimal and pad lay out the table.
Recorded with Apple Swift 6.4 on macOS 26 arm64. It runs in your browser with a WebAssembly build on the WASI runner (browser_wasi_shim 0.4.2). To run it yourself: swiftc -O main.swift -o main && ./main
Ruby 4.0 · main.rb
# frozen_string_literal: true
FLOOR = 200 # mentions a topic needs in the window to be eligible at all
# topic, mentions in the 20 minutes before the window, mentions in the 5-minute window
Topic = Data.define(:name, :baseline, :window) do
# the window's 5 minutes at the topic's usual rate
def expected = baseline / 20.0 * 5
# Rough standard deviations above the usual rate, smoothed for small counts.
def surprise = (window - expected) / Math.sqrt(expected + 50)
end
TOPICS = [
['weather', 18_015, 4504], ['football', 15_897, 4154], ['elections', 13_084, 3261],
['movies', 10_397, 2774], ['music', 9631, 2323], ['recipes', 6111, 1439],
['solar-storm', 847, 2983], ['chess-final', 2461, 1609], ['new-phone', 3974, 1280],
['local-quiz', 19, 52]
].map { |name, baseline, window| Topic.new(name, baseline, window) }.freeze
by_surprise = TOPICS.sort_by { |t| -t.surprise }
rankings = { 'volume' => TOPICS.sort_by { |t| -t.window }, 'surprise' => by_surprise,
'floor' => by_surprise.select { |t| t.window >= FLOOR } }
rankings.each do |title, ranked|
puts "top 5 by #{title.ljust(9)} #{ranked.first(5).map(&:name).join(' ')}"
end
puts "\ntopic window expected surprise"
by_surprise.first(5).each do |t|
numbers = "#{t.window.to_s.rjust(6)} #{format('%.0f', t.expected).rjust(8)}"
puts "#{t.name.ljust(12)} #{numbers} #{format('%.1f', t.surprise).rjust(8)}"
end In Ruby: Data.define gives each topic its fields and its two methods, and sort_by with a negated surprise sorts from the highest.
Recorded with CRuby 4.0.7 on macOS 26 arm64. It runs in your browser with ruby.wasm 2.10.1 (Ruby 4.0.0). To run it yourself: ruby main.rb
Kotlin 2.4 · Main.kt
import kotlin.math.roundToLong
import kotlin.math.sqrt
const val FLOOR = 200 // mentions a topic needs in the window to be eligible at all
// topic, mentions in the 20 minutes before the window, mentions in the 5-minute window
data class Topic(
val name: String,
val baseline: Int,
val window: Int,
) {
val expected get() = baseline / 20.0 * 5 // the window's 5 minutes at the topic's usual rate
/** Rough standard deviations above the usual rate, smoothed for small counts. */
val surprise get() = (window - expected) / sqrt(expected + 50)
}
// the common library has no number format: one digit after the point
fun Double.oneDecimal(): String {
val n = (this * 10).roundToLong()
return "${n / 10}.${n % 10}"
}
fun main() {
val topics =
listOf(
Topic("weather", 18015, 4504),
Topic("football", 15897, 4154),
Topic("elections", 13084, 3261),
Topic("movies", 10397, 2774),
Topic("music", 9631, 2323),
Topic("recipes", 6111, 1439),
Topic("solar-storm", 847, 2983),
Topic("chess-final", 2461, 1609),
Topic("new-phone", 3974, 1280),
Topic("local-quiz", 19, 52),
)
val bySurprise = topics.sortedByDescending { it.surprise }
val rankings =
listOf(
"volume" to topics.sortedByDescending { it.window },
"surprise" to bySurprise,
"floor" to bySurprise.filter { it.window >= FLOOR },
)
for ((title, ranked) in rankings) {
println("top 5 by ${title.padEnd(9)} " + ranked.take(5).joinToString(" ") { it.name })
}
println("\ntopic window expected surprise")
for (t in bySurprise.take(5)) {
val numbers =
t.window.toString().padStart(6) +
t.expected
.roundToLong()
.toString()
.padStart(9)
println(t.name.padEnd(12) + " " + numbers + t.surprise.oneDecimal().padStart(9))
}
} In Kotlin: expected and surprise are properties of the data class, sortedByDescending sorts a copy, and oneDecimal formats a number, which the common library cannot.
Recorded with Kotlin 2.4.20 on Java 25.0.4.1 on macOS 26 arm64. It runs in your browser with a WebAssembly build on the WASI runner (browser_wasi_shim 0.4.2). To run it yourself: kotlinc Main.kt -include-runtime -d main.jar && java -jar main.jar
Dart 3.13 · main.dart
import 'dart:math';
const floor = 200; // mentions a topic needs in the window to be eligible at all
// topic, mentions in the 20 minutes before the window, mentions in the 5-minute window
const topics = [
('weather', 18015, 4504),
('football', 15897, 4154),
('elections', 13084, 3261),
('movies', 10397, 2774),
('music', 9631, 2323),
('recipes', 6111, 1439),
('solar-storm', 847, 2983),
('chess-final', 2461, 1609),
('new-phone', 3974, 1280),
('local-quiz', 19, 52),
];
/// The window's 5 minutes at the topic's usual rate.
double expected(int baseline) => baseline / 20 * 5;
/// Rough standard deviations above the usual rate, smoothed for small counts.
double surprise(int baseline, int window) =>
(window - expected(baseline)) / sqrt(expected(baseline) + 50);
void main() {
final byVolume = [...topics]..sort((a, b) => b.$3.compareTo(a.$3));
final bySurprise = [...topics]
..sort((a, b) => surprise(b.$2, b.$3).compareTo(surprise(a.$2, a.$3)));
final floored = bySurprise.where((t) => t.$3 >= floor).toList();
for (final (title, ranked) in [
('volume', byVolume),
('surprise', bySurprise),
('floor', floored),
]) {
print(
'top 5 by ${title.padRight(9)} ${ranked.take(5).map((t) => t.$1).join(' ')}',
);
}
print('\ntopic window expected surprise');
for (final (topic, base, window) in bySurprise.take(5)) {
final numbers =
'${'$window'.padLeft(6)} ${expected(base).toStringAsFixed(0).padLeft(8)}';
print(
'${topic.padRight(12)} $numbers ${surprise(base, window).toStringAsFixed(1).padLeft(8)}',
);
}
} In Dart: Each topic is a record, read as t.$1 to t.$3, and the cascade ..sort sorts a copy of the constant list in place.
Recorded with Dart 3.13.5 on macOS 26 arm64. It runs in your browser with a WebAssembly build by dart2wasm 3.13.5. To run it yourself: dart run main.dart
What every language printed
top 5 by volume weather football elections solar-storm movies top 5 by surprise solar-storm chess-final new-phone local-quiz movies top 5 by floor solar-storm chess-final new-phone movies football topic window expected surprise solar-storm 2983 212 171.3 chess-final 1609 615 38.5 new-phone 1280 994 8.9 local-quiz 52 5 6.4 movies 2774 2599 3.4
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs. Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. Runs on this device: a JavaScript build of this program (about 70 KB), made when the page was built. The first run also downloads the JavaScript runner (about 0.6 MB), which is kept for the next runs. To change the code, copy it and run it with the command below. Runs on this device: a WebAssembly build of this program (about 158 KB), made when the page was built. Edit runs your changes in the browser: the first edited run downloads the C and C++ compiler (about 29.1 MB), which is kept for the next runs. It is best on a computer. Runs on this device, in your browser. The first run downloads the C and C++ compiler (about 29.1 MB), which is kept for the next runs. It is best on a computer. Runs on this device: a .NET build of this program (about 5 KB), made when the page was built. The first run also downloads .NET (about 3.6 MB), which is kept for the next runs. Edit runs your changes in the browser: the first edited run downloads .NET (about 8.9 MB), which is kept for the next runs. Runs on this device, in your browser. The first run downloads .NET (about 8.9 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 10 KB), made when the page was built. Edit runs your changes in the browser: the first edited run downloads the C and C++ compiler (about 29.1 MB), which is kept for the next runs. It is best on a computer. Runs on this device, in your browser. The first run downloads the C and C++ compiler (about 29.1 MB), which is kept for the next runs. It is best on a computer. Runs on this device, in your browser. The first run downloads PHP (about 4.3 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 31 KB), made when the page was built. To change the code, copy it and run it with the command below. Runs on this device, in your browser. The first run downloads the JavaScript runner (about 0.9 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 729 KB), made when the page was built. Edit runs your changes in the browser: the first edited run downloads the Go compiler (about 18.7 MB), which is kept for the next runs. Runs on this device, in your browser. The first run downloads the Go compiler (about 18.7 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 1.7 MB), made when the page was built. To change the code, copy it and run it with the command below. Runs on this device, in your browser. The first run downloads Ruby (about 10.2 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 186 KB), made when the page was built. To change the code, copy it and run it with the command below. Runs on this device: a WebAssembly build of this program (about 18 KB), made when the page was built. To change the code, copy it and run it with the command below.Your run, in this browser
By volume, the last 5 minutes belong to weather, football and elections, which lead every day. By surprise, solar-storm leads with 2,983 mentions against 212 expected, followed by chess-final at 2.6 times its usual rate. The tiny topic grew about elevenfold, from an expected 4.75 to 52, and would rank fourth without the floor of 200 mentions: growth alone rewards noise.
Key idea
Popular is volume; trending is volume far above a topic’s own usual rate, with a floor that keeps tiny topics out.
Checkpoint
Check: Surprise with a floor
Practise: score topics against their baseline
Write the score and the floor that a trend scorer applies every minute, and run the sample tests:
Exercise · Easy · Python
Rank trending topics by surprise, with a floor
Write two functions in trending.py for a trend scorer. A topic is a tuple (name, baseline, window): baseline is its mentions in the 20 minutes before the window and window its mentions in the 5-minute window. The file already has expected(baseline), the count the window would have at the topic's usual rate.
surprise(baseline, window) returns how far the window is above the expected count, in rough standard deviations: the window's count minus the expected count, divided by the square root of the expected count plus 50, as the program above computes it.
trending(topics, floor, k) returns the names of the k topics with the highest surprise, highest first, among the topics with at least floor mentions in the window.
- When two topics have the same surprise, the one with more mentions in the window comes first, then the name in alphabetical order.
- When fewer than
ktopics reach the floor, return all of them.
Starter code · trending.py
import math
def expected(baseline):
"""The window's 5 minutes at the usual rate of the 20 minutes before it."""
return baseline / 20 * 5
def surprise(baseline, window):
"""Rough standard deviations above the usual rate, smoothed for small counts."""
# Replace this line with your code.
return 0.0
def trending(topics, floor, k):
"""The names of the k topics with the highest surprise among those with at least floor mentions."""
# Replace this line with your code.
return [name for name, baseline, window in topics][:k] The sample tests · test_trending.py
from trending import expected, surprise, trending
TOPICS = [
("weather", 18015, 4504),
("football", 15897, 4154),
("elections", 13084, 3261),
("movies", 10397, 2774),
("music", 9631, 2323),
("recipes", 6111, 1439),
("solar-storm", 847, 2983),
("chess-final", 2461, 1609),
("new-phone", 3974, 1280),
("local-quiz", 19, 52),
]
def test_expected_is_given():
"""the window's 5 minutes at the usual rate: a quarter of the 20 minutes before it"""
assert expected(800) == 200.0
assert round(expected(847), 2) == 211.75
def test_surprise_against_the_baseline():
"""2,983 mentions where 212 were expected is about 171 rough standard deviations"""
assert round(surprise(847, 2983), 1) == 171.3
assert round(surprise(2461, 1609), 1) == 38.5
assert round(surprise(19, 52), 1) == 6.4
def test_surprise_at_or_below_the_usual_rate():
"""a topic at its usual rate scores about zero, and one below it scores below zero"""
assert round(surprise(18015, 4504), 2) == 0.0
assert round(surprise(400, 50), 2) == -4.08
def test_trending_with_the_floor():
"""the lesson's ten topics: the tiny local-quiz stays off a list with a floor of 200"""
assert trending(TOPICS, 200, 5) == ["solar-storm", "chess-final", "new-phone", "movies", "football"]
def test_without_a_floor_growth_rewards_noise():
"""with no floor the tiny topic ranks fourth"""
assert trending(TOPICS, 0, 4) == ["solar-storm", "chess-final", "new-phone", "local-quiz"]
def test_the_floor_is_inclusive():
"""exactly floor mentions is enough; one fewer is not"""
assert trending([("a", 0, 200)], 200, 5) == ["a"]
assert trending([("a", 0, 199)], 200, 5) == []
def test_ties_more_mentions_then_name():
"""the same surprise: more mentions in the window first, then the name"""
tied = [("p", 200, 80), ("q", 1400, 410), ("b", 0, 0), ("a", 0, 0)]
assert trending(tied, 0, 4) == ["q", "p", "a", "b"]
def test_k_and_the_input_order():
"""at most k names, all of them when fewer reach the floor, whatever the order of the input"""
assert trending(TOPICS, 3000, 5) == ["football", "weather", "elections"]
assert trending(TOPICS, 200, 2) == ["solar-storm", "chess-final"]
assert trending(list(reversed(TOPICS)), 200, 5) == trending(TOPICS, 200, 5) A hint
Compute e = expected(baseline) once, then (window - e) / math.sqrt(e + 50). For trending, keep the topics whose window is at least floor, sort them with the key (-surprise(baseline, window), -window, name), which puts the highest surprise first, and return the first k names.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Then practise, weakest idea first:
Practice that adapts to you
Practise this lesson
12 questions about 12 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
The deep dives this design must answer
A round then probes these questions:
- Counting in fixed memory: a Count-Min sketch keeps approximate counts for every topic in a few rows of counters.
- Finding the heavy hitters: a heap of candidates keeps the topics with the highest estimates.
- Correcting the sketch: an exact recount of the candidates removes the sketch’s overestimates.
- Sliding windows: one-minute panes are added together to form any window.
- Late events: a watermark decides when a window is complete enough to publish.
- Merging lists: partitions that split a topic’s mentions must each send more than k candidates.
- Hot topics: pre-aggregation at the producers keeps one topic from swamping its partition.
- Manipulation: HyperLogLog counts distinct accounts per topic, so a few accounts cannot fake a trend.
Each of them changes a number in the estimates, which is what an interviewer listens for.
Counting heavy hitters in windows, measured Pro: opens with a Pro, Premium or Ultimate pass
The counters decide both the cost and the accuracy of the service. This part runs a sketch with a candidate list against exact counts, checks the result against the sketch’s error bound, and builds sliding windows that tolerate late events.
In this part:
- A sketch picks the candidates and an exact recount ranks them
- The Count-Min error, with numbers
- Panes form every window, and late mentions correct them
1 diagram · 1 runnable example (Python) · about 9 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Merging top-k lists and splitting hot topics, tested Pro: opens with a Pro, Premium or Ultimate pass
A list computed in many places must be merged, and the merge is only as good as what each place sends. This part shows a merge that is exact whatever the data, tests three merges against the exact answer at scale, and then handles the topic that swamps its partition.
In this part:
- Three rounds find the exact top K from partial counts
- Three ways to merge, tested against the exact answer
- A hot topic swamps its partition, with numbers
- Baselines by hour of the week keep weekly rhythms from trending
1 diagram · 2 runnable examples (JavaScript) · about 18 minutes
Opening this part with your pass…Pro part. It opens with the same pass as the part above.
Manipulation, failure drills, bottlenecks and the rubric Pro: opens with a Pro, Premium or Ultimate pass
A trending list is a target, so the last part defends it, drills its failures, scales it and turns it into a scored interview answer.
In this part:
- Weigh distinct accounts, not mentions
- Each failure has a fallback that keeps the list honest
- Bottlenecks at 10× and 100×
- What each level is expected to cover
- A 45-minute self-mock
about 5 minutes
Opening this part with your pass…Pro part. It opens with the same pass as the part above.
Interview questions
Choose your opening move for each brief:
Checkpoint
Your first decision
-
Fresher to mid · Warm-up
What is the difference between a popular topic and a trending one?
A popular topic has many mentions; a trending topic has many more mentions than usual for it. Weather is mentioned every minute of every day, so it is popular, but it trends only when a storm arrives. A trending score therefore compares a topic's count in a recent window with its own baseline, the rate expected at that hour, instead of ranking raw counts. Two guards keep the list honest: a minimum volume, because a topic that grows from 5 mentions to 50 has grown tenfold without mattering, and a count of distinct accounts, because a few accounts posting the same tag many times can imitate growth.
-
Mid to senior
During a cup final, one topic receives 40 % of all mentions, and the partition that counts it falls minutes behind while the others are idle. Fix the counting without losing or double-counting mentions.
-
Senior to staff
64 regional servers each send their top 10 topics to a global merger every minute. Explain why the global top 10 can be wrong, and design a merge that is exact.
-
Senior to staff
5,000 accounts created last week each post the same hashtag every few seconds, and it reaches the top of the trends. Keep such campaigns out without hiding a real surge such as breaking news.
Key takeaways
- Trending means growth against a topic’s own baseline, with a floor on volume; popular means volume.
- Partition the mention log by topic so that each topic is counted in one place, and serve the lists from a cache.
- Count in one-minute panes and add panes to form each window.
- Size the counters for an event peak, about five times the average.
References
- Count-min sketch (Redis documentation) (Redis)
- Top-K (Redis documentation) (Redis)
- HyperLogLog (Redis documentation) (Redis)
- An improved data stream summary: the count-min sketch and its applications (Cormode and Muthukrishnan) (Journal of Algorithms (Elsevier))
- Windows (Apache Flink documentation) (Apache Software Foundation)
- The Dataflow Model: A Practical Approach to Balancing Correctness, Latency, and Cost in Massive-Scale, Unbounded, Out-of-Order Data Processing (Google Research (VLDB))
- Efficient top-K query calculation in distributed networks (Cao and Wang) (ACM Symposium on Principles of Distributed Computing (PODC))
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress