Pick a depth. Each prompt opens in your AI pre-loaded with the lesson. Click a row to preview the prompt.
Every production recommender you'll ever touch — Netflix's homepage, Amazon's 'customers also bought,' a search engine's results page — is built as a funnel, not a single model. The reason is arithmetic: if you have 50 million items and 200ms to respond, you cannot run a heavyweight ranking model over all 50 million candidates for every request. So the system narrows in stages. Retrieval cheaply cuts millions down to a few hundred plausible candidates using simple, fast signals. Ranking scores those few hundred with an expensive, accurate model. Re-ranking makes final adjustments to the ranked list — diversity, business rules, freshness — before it's shown. Understanding this funnel shape first is what stops you from asking 'what's the recommender model' as if there's only one; there are at least three, each with a different job, latency budget, and evaluation metric.
The demo times a brute-force 'score everything' approach against a two-stage funnel on a toy catalog, scaled up to make the gap obvious. The retrieval stage uses a cheap heuristic (co-purchase counts); the ranking stage uses an 'expensive' scorer that we simulate with an artificial per-item cost.
ranked still contains the true top item from all_scored — this is the recall-vs-latency trade-off retrieval makes.re_rank(ranked) step that swaps the #1 and #2 items if they belong to the same simulated 'category', to see how a re-ranking rule changes the funnel's output without touching retrieval or ranking.expensive_score to count how many times it's called in each approach and print the ratio — that call count is the real-world cost driver, not wall-clock time on a toy example.import time, random
random.seed(0)
N_ITEMS = 200_000
TOP_K = 10
# A cheap signal: precomputed co-occurrence counts for a user's last item.
co_occurrence = {i: random.random() for i in range(N_ITEMS)}
def expensive_score(item_id: int) -> float:
# Simulates a heavy model call (a few microseconds of real work here,
# milliseconds in a real deep ranker) applied per candidate.
return co_occurrence[item_id] * 0.7 + random.random() * 0.3
# --- Brute force: score every item with the expensive model ---
t0 = time.perf_counter()
all_scored = sorted(range(N_ITEMS), key=expensive_score, reverse=True)[:TOP_K]
brute_force_ms = (time.perf_counter() - t0) * 1000
# --- Funnel: retrieve top 500 cheaply, only rank those 500 expensively ---
t0 = time.perf_counter()
candidates = sorted(co_occurrence, key=co_occurrence.get, reverse=True)[:500]
ranked = sorted(candidates, key=expensive_score, reverse=True)[:TOP_K]
funnel_ms = (time.perf_counter() - t0) * 1000
print(f"catalog size: {N_ITEMS:,}")
print(f"brute force: {brute_force_ms:.1f}ms (scored {N_ITEMS:,} items)")
print(f"retrieve+rank: {funnel_ms:.1f}ms (scored 500 items)")
print(f"speedup: {brute_force_ms / funnel_ms:.0f}x")python3 main.py