LeetLLM
My PlanLearnGlossaryTracksPracticeBlog
LeetLLM

Your go-to resource for mastering AI & LLM systems.

Product

  • Learn
  • Glossary
  • Tracks
  • Practice
  • Blog
  • RSS

Legal

  • Terms of Service
  • Privacy Policy

© 2026 LeetLLM. All rights reserved.

All Topics
Your Progress
0%

0 of 178 articles completed

🛠️Computing Foundations0/9
Git, Shell, Linux for AIDocker for Reproducible AIPython for AI EngineeringNumPy and Tensor ShapesCUDA for ML TrainingMPS & Metal for ML on MacData Structures for AISQL and Data ModelingAlgorithms for ML Engineers
📊Math & Statistics0/8
Gradients and BackpropVectors, Matrices & TensorsLinear Algebra for MLAdam, Momentum, SchedulersProbability for Machine LearningStatistics and UncertaintyDistributions and SamplingHypothesis Tests, Intervals, and pass@k
📚Preparation & Prerequisites0/13
Neural Networks from ScratchCNNs from ScratchTraining & BackpropagationSoftmax, Cross-Entropy & OptimizationRNNs, LSTMs, GRUs, and Sequence ModelingAutoencoders and VAEsThe Transformer Architecture End-to-EndLanguage Modeling & Next TokensFrom GPT to Modern LLMsPrompt Engineering FundamentalsCalling LLM APIs in ProductionFirst AI App End-to-EndThe LLM Lifecycle
🧮ML Algorithms & Evaluation0/11
Linear Regression from ScratchLogistic Regression and MetricsDecision Trees, Forests, and BoostingReinforcement Learning BasicsValidation and LeakageClustering and PCACore Retrieval AlgorithmsDecoding AlgorithmsExperiment Design and A/B TestingPyTorch Training LoopsDataset Pipelines and Data Quality
📦Production ML Systems0/6
Feature Engineering for Production MLBatch and Streaming Feature PipelinesGradient Boosted Trees in ProductionRanking and Recommendation SystemsForecasting and Anomaly DetectionMonitoring Predictive Models
🧪Core LLM Foundations0/8
The Bitter Lesson & ComputeBPE, WordPiece, and SentencePieceStatic to Contextual EmbeddingsPerplexity & Model EvaluationFile Ingestion for AIChunking StrategiesLLM Benchmarks & LimitationsInstruction Tuning & Chat Templates
🧰Applied LLM Engineering0/24
Dimensionality Reduction for EmbeddingsCoT, ToT & Self-Consistency PromptingFunction Calling & Tool UseMCP & Tool Protocol StandardsContext EngineeringPrompt Injection DefenseResponsible AI GovernanceData Labeling and Human FeedbackEvaluating AI AgentsProduction RAG PipelinesHybrid Search: Dense + SparseReranking and Cross-Encoders for RAGRAG Evaluation for Reliable AnswersLLM-as-a-Judge EvaluationBias & Fairness in LLMsHallucination Detection & MitigationLLM Observability & MonitoringExperiment Tracking with MLflow and W&BPrompt Optimization with DSPyModel Versioning & DeploymentSemantic Caching & Cost OptimizationLLM Cost Engineering & Token EconomicsModel Gateways, Routing, and FallbacksDesign an Automated Support Agent
🎓Portfolio Capstones0/9
Capstone: Delivery ETA PredictionCapstone: Product RankingCapstone: Demand ForecastingCapstone: Image Damage ClassifierCapstone: Production ML PipelineCapstone: Document QACapstone: Eval DashboardCapstone: Fine-Tuned ClassifierCapstone: Reproducible ML Study
🧠Transformer Deep Dives0/8
Sentence Embeddings & Contrastive LossEmbedding Similarity & QuantizationScaled Dot-Product AttentionVision Transformers and Image EncodersPositional Encoding: RoPE & ALiBiLayer Normalization: Pre-LN vs Post-LNMechanistic InterpretabilityDecoding Strategies: Greedy to Nucleus
🧬Advanced Training & Adaptation0/17
Scaling Laws & Compute-Optimal TrainingPre-training Data at ScaleBuild GPT from Scratch LabJAX for PyTorch ResearchersContinued Pretraining for Domain ShiftSynthetic Data PipelinesSupervised Fine-Tuning PipelineMixed Precision TrainingDistributed Training: FSDP & ZeROLoRA & Parameter-Efficient TuningTraining Run OperationsReward Modeling from Preference DataRLHF & DPO AlignmentConstitutional AI & Red TeamingRLVR & Verifiable RewardsKnowledge Distillation for LLMsModel Merging and Weight Interpolation
🤖Advanced Agents & Retrieval0/16
Vector DB Internals: HNSW & IVFAdvanced RAG: HyDE & Self-RAGGraphRAG & Knowledge GraphsRAG Security & Access ControlStructured Output GenerationReAct & Plan-and-ExecuteGuardrails & Safety FiltersCode Generation & SandboxingComputer-Use / GUI / Browser AgentsHuman-in-the-Loop Agent ArchitectureAI Coding Workflow with AgentsAgent Memory & PersistenceAgent Failure & RecoveryRecursive Language Models (RLM)Multi-Agent OrchestrationCapstone: Production Agent
⚡Inference & Production Scale0/19
Inference: TTFT, TPS & KV CacheMulti-Query & Grouped-Query AttentionKV Cache & PagedAttentionPrefix Caching and Prompt CachingFlashAttention & Memory EfficiencyContinuous Batching & SchedulingScaling LLM InferenceModel Parallelism for LLM InferenceModel Quantization: GPTQ, AWQ & GGUFLocal LLM DeploymentSLM Specialization & Edge DeploymentSpeculative DecodingLong Context Window ManagementMixture of Experts ArchitectureMamba & State Space ModelsReasoning & Test-Time ComputeAdvanced MLOps & DevOps for AIGPU Serving & AutoscalingA/B Testing for LLMs
🏗️System Design Capstones0/9
Content Moderation SystemCode Completion SystemMulti-Tenant LLM PlatformLLM-Powered Search EngineVision-Language Models & CLIPMultimodal LLM ArchitectureDiffusion Models: Images & TextReal-Time Voice AI AgentReasoning Agent System Design
🎤AI Lab Interviewing0/4
AI Lab Coding Interview: Python SystemsAI Lab System Design InterviewAI Lab Behavioral InterviewAI Lab Technical Presentation
🔬Project Deep Dives0/17
Deep Dive - vLLMDeep Dive - SkyRLDeep Dive - FlashAttentionDeep Dive - FlashInferDeep Dive - DeepGEMMDeep Dive - NCCLDeep Dive - MegatronDeep Dive - DeepSpeedDeep Dive - RayDeep Dive - MLflowDeep Dive - PyTorchDeep Dive - TransformersDeep Dive - SGLangDeep Dive - slimeDeep Dive - DeepEPDeep Dive - TinkerDeep Dive - Light-PEFT
Back to Topics
LearnProduction ML SystemsRanking and Recommendation Systems
📊MediumEvaluation & Benchmarks

Ranking and Recommendation Systems

Rank documents for a developer using candidate retrieval, relevance metrics, and feedback-loop safeguards.

18 min read
Learning path
Step 45 of 178 in the full curriculum
Gradient Boosted Trees in ProductionForecasting and Anomaly Detection

Personalize this lesson

Adapt explanations and teaching visuals to your background and preferred voice.

A developer searches for retry idempotency key. A search ranker doesn't emit one yes-or-no action. Given that query, which documents should appear first, and in what order?

Gradient boosted trees scored one job at a time: intervene or wait. Ranked order also changes the training data. Classifier labels are usually collected independently of the last prediction. Yesterday's ranking becomes today's clicks. A recommendation feed uses the same funnel without a typed query: developer and session context replace query features.

Separate eligibility, retrieval, and ranking

The corpus for that retry idempotency key query has one million documents. A feature-heavy model shouldn't score every document on every request.

StageJobExample guard
eligibility filterremove forbidden or stale documentscurrent, permitted source
candidate generationrecover perhaps 200 plausible documentstext or embedding retrieval
rankingorder candidates preciselyrelevance, freshness, source trust
business policyenforce final constraintssponsored labels, diversity, safety

A document that never enters the candidate set can't be rescued by a perfect ranker. A forbidden document shouldn't enter the candidate set at all. Candidate recall, policy filtering, and ranking quality are separate obligations.

Build a seven-document fixture. The relevance value is an offline human judgment for one frozen query. It belongs in evaluation data, not in online model features.

build-doc-search-fixture.py
1from hashlib import sha256 2from json import dumps 3from math import log2 4 5query = "retry idempotency key" 6corpus = [ 7 {"id": "api-guide", "title": "retry idempotency API guide", "current": True, "permitted": True, "retrieval": 0.96, "query_fit": 0.98, "reliability": 0.88, "relevance": 3}, 8 {"id": "blog-post", "title": "general release blog", "current": True, "permitted": True, "retrieval": 0.91, "query_fit": 0.35, "reliability": 0.97, "relevance": 0}, 9 {"id": "legacy-changelog", "title": "legacy changelog", "current": True, "permitted": True, "retrieval": 0.90, "query_fit": 0.15, "reliability": 0.99, "relevance": 0}, 10 {"id": "retry-runbook", "title": "retry policy runbook", "current": True, "permitted": True, "retrieval": 0.84, "query_fit": 0.92, "reliability": 0.98, "relevance": 2}, 11 {"id": "sdk-example", "title": "idempotency SDK example", "current": True, "permitted": True, "retrieval": 0.80, "query_fit": 0.86, "reliability": 0.96, "relevance": 2}, 12 {"id": "private-draft", "title": "private retry incident draft", "current": True, "permitted": False, "retrieval": 0.99, "query_fit": 0.99, "reliability": 0.99, "relevance": 3}, 13 {"id": "stale-guide", "title": "stale retry migration guide", "current": False, "permitted": True, "retrieval": 0.95, "query_fit": 0.94, "reliability": 0.70, "relevance": 2}, 14] 15 16print("corpus rows:", len(corpus)) 17print("query:", query)
Output
1corpus rows: 7 2query: retry idempotency key

The corpus includes a private draft and a stale guide. Remove both before retrieval. A learned score can't override access policy.

filter-eligible-docs.py
1eligible = [ 2 item 3 for item in corpus 4 if item["current"] and item["permitted"] 5] 6blocked = sorted(item["id"] for item in corpus if item not in eligible) 7 8print("eligible:", [item["id"] for item in eligible]) 9print("blocked:", blocked)
Output
1eligible: ['api-guide', 'blog-post', 'legacy-changelog', 'retry-runbook', 'sdk-example'] 2blocked: ['private-draft', 'stale-guide']

Measure candidate recall before ordering quality

Candidate generation runs a cheap retrieval stage. This lab uses a precomputed retrieval score so you can focus on the contract between stages. In Core Retrieval Algorithms that cheap stage was BM25, dense similarity, or a fused shortlist. Here the same contract shows up in a production ranker: recover the right documents first, then spend compute ordering them.

The judged relevant eligible documents are api-guide, retry-runbook, and sdk-example. Measure how many survive candidate generation.

measure-candidate-recall.py
1relevant_ids = { 2 item["id"] 3 for item in eligible 4 if item["relevance"] > 0 5} 6 7def candidates_at(limit): 8 return sorted( 9 eligible, 10 key=lambda item: (-item["retrieval"], item["id"]), 11 )[:limit] 12 13def candidate_recall(items): 14 returned_ids = {item["id"] for item in items} 15 return len(returned_ids & relevant_ids) / len(relevant_ids) 16 17for limit in (3, 5): 18 items = candidates_at(limit) 19 print( 20 f"k={limit} ids={[item['id'] for item in items]} " 21 f"recall={candidate_recall(items):.3f}" 22 )
Output
1k=3 ids=['api-guide', 'blog-post', 'legacy-changelog'] recall=0.333 2k=5 ids=['api-guide', 'blog-post', 'legacy-changelog', 'retry-runbook', 'sdk-example'] recall=1.000

At k=3, retrieval keeps only one of three relevant eligible documents. No downstream ranker can recover the missing runbook or SDK example. Raising this tiny lab's candidate budget to 5 restores candidate recall before the expensive scorer runs.

The relevant item never enters the top-100 candidate set. Can a more accurate reranker place it first?

Answer

No. Ranking can reorder only retrieved candidates. Fix candidate recall before interpreting downstream ordering metrics.

Rerank the surviving candidates

Keep five candidates. The first-stage retrieval order is fast but imprecise: blog-post and legacy-changelog sit near the top on retrieval alone, yet their query_fit stays low for the idempotency intent.

The scorer below is hand-written so each feature remains visible. query_fit represents a richer query-document match feature. reliability represents source trust. Neither uses the offline relevance label.

score-and-rerank.py
1candidates = candidates_at(5) 2 3def rank_score(item): 4 return 2.0 * item["query_fit"] + 0.6 * item["reliability"] 5 6ranked = sorted( 7 candidates, 8 key=lambda item: (-rank_score(item), item["id"]), 9) 10 11print("retrieval order:", [item["id"] for item in candidates]) 12print("ranked order:", [item["id"] for item in ranked]) 13print("rank scores:", [(item["id"], round(rank_score(item), 3)) for item in ranked])
Output
1retrieval order: ['api-guide', 'blog-post', 'legacy-changelog', 'retry-runbook', 'sdk-example'] 2ranked order: ['api-guide', 'retry-runbook', 'sdk-example', 'blog-post', 'legacy-changelog'] 3rank scores: [('api-guide', 2.488), ('retry-runbook', 2.428), ('sdk-example', 2.296), ('blog-post', 1.282), ('legacy-changelog', 0.894)]

A production scorer learns weights or nonlinear interactions from labeled examples. The request path stays the same: retrieve cheaply, score the bounded candidate set, then apply policy. Those two orders of the same five documents are ready to compare. Which one is actually better?

A metric that values the top slots

A classifier's accuracy doesn't care which mistakes you made first. A ranked list does. Putting the best document first is a bigger win than sliding it from position 40 to position 39. Discounted Cumulative Gain (DCG) gives graded relevance near the top more weight:

DCG@k=∑i=1k2reli−1log⁡2(i+1)\text{DCG@k} = \sum_{i=1}^{k} \frac{2^{rel_i} - 1}{\log_2(i+1)}DCG@k=i=1∑k​log2​(i+1)2reli​−1​

Here relirel_ireli​ is the judged relevance at position iii, counting from 1. Position 1 has denominator log⁡2(2)=1\log_2(2)=1log2​(2)=1, so it keeps full weight. Lower positions receive a logarithmic discount. This is the exponential-gain form Manning uses for NDCG.[1]Reference 1Introduction to Information Retrieval.https://nlp.stanford.edu/IR-book/ Common library helpers often use linear gain reli/log⁡2(i+1)rel_i / \log_2(i+1)reli​/log2​(i+1) instead. The two agree on binary labels and disagree on grades such as 0, 2, 3, so pin the gain function in the evaluation suite.

Normalized DCG (NDCG) divides by the score of the ideal ordering, producing a value between zero and one for a query.

Work the retrieval order [3, 0, 0, 2, 2] by hand before looking at code. Gain is 2rel−12^{rel}-12rel−1, so a grade-3 document contributes 7 and a grade-2 document contributes 3:

DCG@5=7log⁡22+0+0+3log⁡25+3log⁡26≈7+1.292+1.161=9.453\begin{align*} \text{DCG@5} &= \frac{7}{\log_2 2} + 0 + 0 + \frac{3}{\log_2 5} + \frac{3}{\log_2 6} \\ &\approx 7 + 1.292 + 1.161 = 9.453 \end{align*}DCG@5​=log2​27​+0+0+log2​53​+log2​63​≈7+1.292+1.161=9.453​

The ideal order of these five grades is [3, 2, 2, 0, 0]:

IDCG@5=7+3log⁡23+3log⁡24≈7+1.893+1.500=10.393\text{IDCG@5} = 7 + \frac{3}{\log_2 3} + \frac{3}{\log_2 4} \approx 7 + 1.893 + 1.500 = 10.393IDCG@5=7+log2​33​+log2​43​≈7+1.893+1.500=10.393

So retrieval-order NDCG@5 is 9.453/10.393≈0.9109.453 / 10.393 \approx 0.9109.453/10.393≈0.910. Build that ideal list from all candidate relevances before truncating. Otherwise a relevant candidate below the evaluated slate could disappear from the denominator. The guard for best == 0 handles a query with no judged relevant documents instead of dividing by zero.

This lab computes reranker NDCG over documents that survived retrieval. That boundary is deliberate: candidate recall measures misses before ranking, while reranker NDCG measures ordering quality inside the bounded candidate set. A whole-system evaluation can also compute NDCG against the eligible judged corpus so retrieval misses reduce the final score.

compare-ndcg.py
1def dcg(relevances): 2 return sum( 3 (2**relevance - 1) / log2(rank + 2) 4 for rank, relevance in enumerate(relevances) 5 ) 6 7def reranker_ndcg(items, limit): 8 relevances = [item["relevance"] for item in items[:limit]] 9 ideal = sorted( 10 (item["relevance"] for item in items), 11 reverse=True, 12 )[:limit] 13 best = dcg(ideal) 14 return dcg(relevances) / best if best else 0.0 15 16print("retrieval-order reranker ndcg@5:", f"{reranker_ndcg(candidates, 5):.3f}") 17print("ranked-order reranker ndcg@5:", f"{reranker_ndcg(ranked, 5):.3f}")
Output
1retrieval-order reranker ndcg@5: 0.910 2ranked-order reranker ndcg@5: 1.000

The scorer lifts reranker NDCG@5 from 0.910 to 1.000. That doesn't prove the model will improve a live docs portal. It only proves this frozen judged query improved after reranking.

Reranker NDCG can still look perfect when retrieval drops relevant documents. Compare it with whole-system NDCG, which builds the ideal list from the eligible judged corpus so missing relevants lower the score:

compare-system-and-reranker-ndcg.py
1def system_ndcg(items, eligible_items, limit): 2 relevances = [item["relevance"] for item in items[:limit]] 3 ideal = sorted( 4 (item["relevance"] for item in eligible_items), 5 reverse=True, 6 )[:limit] 7 best = dcg(ideal) 8 return dcg(relevances) / best if best else 0.0 9 10tight = candidates_at(3) 11tight_ranked = sorted( 12 tight, 13 key=lambda item: (-rank_score(item), item["id"]), 14) 15 16print("k=3 survivors:", [item["id"] for item in tight_ranked]) 17print("k=3 grades:", [item["relevance"] for item in tight_ranked]) 18print("k=3 reranker ndcg:", f"{reranker_ndcg(tight_ranked, 3):.3f}") 19print("k=3 system ndcg:", f"{system_ndcg(tight_ranked, eligible, 3):.3f}")
Output
1k=3 survivors: ['api-guide', 'blog-post', 'legacy-changelog'] 2k=3 grades: [3, 0, 0] 3k=3 reranker ndcg: 1.000 4k=3 system ndcg: 0.674

At k=3, the perfect reranker order among survivors still yields reranker NDCG 1.000, because the ideal list is built only from those three documents. System NDCG falls to 0.674 because the ideal top three from the eligible corpus is [3, 2, 2] and two relevant documents never entered the slate. Report both when you care about end-to-end quality; keep candidate recall as the explicit retrieval diagnostic.

Two candidate windows for retry idempotency key. At k=3 the retrieval order is api-guide (grade 3), blog-post (0), legacy-changelog (0), with retry-runbook (2) and sdk-example (2) left outside; reranker NDCG is 1.000 while system NDCG is 0.674. At k=5 reranking packs grades 3, 2, 2, 0, 0 and both metrics reach 1.000.
The k=3 window leaves retry-runbook and sdk-example outside ranking. Reranker NDCG on the survivors is still 1.000, while system NDCG falls to 0.674 because the missing grade-2 documents never compete.

One query isn't an evaluation set. Average reranker NDCG over a frozen query set, report slices such as new documents and languages, and inspect failed queries. Keep candidate recall separate so a strong reranker score can't hide retrieval loss.

Why does normalized discounted cumulative gain (NDCG) penalize moving a relevant item from rank 1 to rank 10?

Answer

Discounted cumulative gain gives earlier positions more weight through a logarithmic discount. NDCG then divides by the ideal ordering, making the score comparable across queries with different relevance sets.

Two-tower retrieval and the precomputation constraint

The lab just ranked five rows. A live corpus has closer to a million. Candidate retrieval often uses a two-tower architecture so that million-row scan never happens on the request path. One encoder maps the live query or session context to a vector u(q)∈Rdu(q) \in \mathbb{R}^du(q)∈Rd. A separate encoder maps each document or item to a vector v(i)∈Rdv(i) \in \mathbb{R}^dv(i)∈Rd. The search system compares those vectors with a dot product or cosine similarity. Huang et al. trained deep structured semantic models that map queries and documents into one shared space and score them with cosine similarity.[2]Reference 2Learning Deep Structured Semantic Models for Web Search Using Clickthrough Data.https://www.microsoft.com/en-us/research/publication/learning-deep-structured-semantic-models-for-web-search-using-clickthrough-data/

The separation changes serving work. Because the item tower doesn't depend on the current query, item vectors can be computed offline and loaded into an approximate nearest neighbor (ANN) index. A request runs the query tower once, retrieves a small candidate set, and reserves expensive pairwise scoring for those candidates. Exact complexity and latency depend on the index, recall target, hardware, and corpus, so ANN search shouldn't be described with one universal O(log⁡N)O(\log N)O(logN) bound.

The cross-feature bottleneck

An early cross-feature mixes query and item information before each side has produced its independent vector. Query-document cross-attention is one example. The resulting item representation changes with every query, so it can't be precomputed.

Using such a model as first-stage retrieval requires pairwise model work across the corpus. Using it after retrieval requires pairwise work only across the bounded candidate set. That makes cross-features useful for reranking even when they're too expensive for candidate generation.

The miniature example uses exact dot products so every step stays visible. A production ANN index changes how candidates are found, but it preserves the same separation between precomputed item vectors and live query work.

two-tower-vs-cross-feature.py
1from random import Random 2 3rng = Random(42) 4n_items = 1_000 5dim = 16 6candidate_budget = 5 7 8def random_vector(): 9 return [rng.gauss(0.0, 1.0) for _ in range(dim)] 10 11def dot(left, right): 12 return sum(a * b for a, b in zip(left, right)) 13 14# Item tower output is computed before any live query arrives. 15doc_embeddings = [random_vector() for _ in range(n_items)] 16query_embedding = random_vector() 17 18# Exact search stands in for an ANN lookup in this small lab. 19retrieval_scores = [dot(doc, query_embedding) for doc in doc_embeddings] 20candidate_ids = sorted( 21 range(n_items), 22 key=lambda item_id: retrieval_scores[item_id], 23)[-candidate_budget:] 24 25# A pairwise reranker now computes cross-features for five rows, not 1,000. 26candidate_quality = [rng.gauss(0.0, 1.0) for _ in range(candidate_budget)] 27rerank_scores = [ 28 0.8 * retrieval_scores[item_id] + 0.2 * quality 29 for item_id, quality in zip(candidate_ids, candidate_quality) 30] 31 32print("offline item vectors:", len(doc_embeddings)) 33print("live query vectors:", 1) 34print("pairwise rows before retrieval:", n_items) 35print("pairwise rows after retrieval:", len(rerank_scores)) 36 37assert len(doc_embeddings) == n_items 38assert len(doc_embeddings[0]) == dim 39assert len(set(candidate_ids)) == candidate_budget 40assert len(rerank_scores) == candidate_budget
Output
1offline item vectors: 1000 2live query vectors: 1 3pairwise rows before retrieval: 1000 4pairwise rows after retrieval: 5

Precomputation removes item-encoder work from the request path. Candidate retrieval narrows the pairwise work by a factor of 200 in this fixture, from 1,000 query-item rows to 5. Measure ANN recall and latency on your own data because the index adds an approximation that this exact-search lab doesn't model.

Turn judgments into pair preferences

The hand-written scorer is pointwise: each document gets an independent score. Pairwise learning-to-rank methods such as RankNet train from preferences so a relevant item scores above a less relevant one.[3]Reference 3Learning to Rank using Gradient Descent.https://www.microsoft.com/en-us/research/publication/learning-to-rank-using-gradient-descent/ Listwise methods try to optimize a list metric directly. This lab stays with a transparent pointwise score and pairwise labels built from the same frozen query.

FamilyWhat it learnsThis lab
pointwisea score or probability per documentrank_score
pairwisewhich of two documents should rank higherpreference pairs below
listwisethe whole ordered list, often toward NDCGnot implemented here
build-pair-preferences.py
1pairs = [ 2 (preferred["id"], other["id"]) 3 for preferred in ranked 4 for other in ranked 5 if preferred["relevance"] > other["relevance"] 6] 7 8print("pair count:", len(pairs)) 9print("first pairs:", pairs[:5])
Output
1pair count: 8 2first pairs: [('api-guide', 'retry-runbook'), ('api-guide', 'sdk-example'), ('api-guide', 'blog-post'), ('api-guide', 'legacy-changelog'), ('retry-runbook', 'blog-post')]

The pair ("api-guide", "blog-post") says the API guide should score higher for this query. Equal grades don't form a pair, so retry-runbook vs sdk-example is absent. A learned pairwise model uses features to reduce preference mistakes across many queries. The hand-written scorer stays useful as a transparent baseline.

Log exposure before reading clicks

The ranked slate is now ready to display. Persist one immutable impression row per exposed document before reading clicks. Later click and resolved events join back through request and document IDs. Without the slate, position, eligibility-policy version, candidate-set version, and ranker version, you can't reproduce what the developer saw.

write-impression-log.py
1slate = ranked[:3] 2impressions = [ 3 { 4 "request_id": "req-1042", 5 "query": query, 6 "eligibility_policy": "docs-access-policy-v4", 7 "candidate_set": "retrieval-v3", 8 "ranker": "docs-ranker-v7", 9 "document_id": item["id"], 10 "position": position, 11 } 12 for position, item in enumerate(slate, start=1) 13] 14outcomes = [ 15 { 16 "event_id": "evt-click-1042", 17 "occurred_at": "2026-05-01T12:00:03Z", 18 "request_id": "req-1042", 19 "document_id": "api-guide", 20 "event": "click", 21 }, 22 { 23 "event_id": "evt-resolved-1042", 24 "occurred_at": "2026-05-01T12:04:18Z", 25 "request_id": "req-1042", 26 "document_id": "api-guide", 27 "event": "resolved", 28 }, 29] 30 31print("slate:", [item["id"] for item in slate]) 32print("logged positions:", [ 33 (row["document_id"], row["position"]) 34 for row in impressions 35]) 36print("later outcomes:", [ 37 (row["document_id"], row["event"]) 38 for row in outcomes 39])
Output
1slate: ['api-guide', 'retry-runbook', 'sdk-example'] 2logged positions: [('api-guide', 1), ('retry-runbook', 2), ('sdk-example', 3)] 3later outcomes: [('api-guide', 'click'), ('api-guide', 'resolved')]

The first row doesn't mean api-guide is universally best. It means this request exposed it at position 1, and one developer clicked it.

Log enough context to interpret later outcomes:

Logged fieldReason
request and query IDgroup displayed slate and join later outcomes
eligibility-policy versionknow which documents were allowed
candidate set versionknow what could have ranked
final positionsmeasure exposure
ranker versionattribute outcomes
timestamped click, save, resolved, bad-result eventsdistinguish curiosity from value
freshness and permission snapshotreproduce context

Clicks are biased labels

When a model places a document first, it receives more attention and therefore more clicks. Training directly on clicks rewards past ranking position as if it were document relevance. This is a feedback loop.

Diagram showing Eligible corpus, Candidates, Ranker, and Top results.
Eligible corpus, Candidates, Ranker, and Top results.

Take two documents with equal underlying appeal. Historical ranker always places same-quality-a first and same-quality-b second. Slot one is examined more often.

reproduce-position-bias.py
1historical = [ 2 {"document": "same-quality-a", "position": 1, "impressions": 100, "clicks": 40}, 3 {"document": "same-quality-b", "position": 2, "impressions": 100, "clicks": 20}, 4] 5 6for row in historical: 7 raw_ctr = row["clicks"] / row["impressions"] 8 print( 9 f"{row['document']}: position={row['position']} " 10 f"raw_ctr={raw_ctr:.2f}" 11 )
Output
1same-quality-a: position=1 raw_ctr=0.40 2same-quality-b: position=2 raw_ctr=0.20

Naive training labels say document a is twice as attractive. The log doesn't justify that conclusion because document and position never vary independently.

A simple counterfactual correction weights each click by the inverse of its position's examination probability. This example fixes slot-one propensity at 1.0 and slot-two propensity at 0.5.

correct-toy-position-bias.py
1examination_propensity = {1: 1.0, 2: 0.5} 2 3for row in historical: 4 corrected_signal = ( 5 row["clicks"] 6 / examination_propensity[row["position"]] 7 / row["impressions"] 8 ) 9 print(f"{row['document']}: corrected_signal={corrected_signal:.2f}")
Output
1same-quality-a: corrected_signal=0.40 2same-quality-b: corrected_signal=0.40

Both corrected signals become 0.40. The arithmetic exposes the idea, not a production estimator. Real propensities need careful estimation, often through randomized interventions or a validated click model. Very small propensities also create large, noisy weights, so a production estimator needs variance controls and evaluation. Inverse-propensity learning-to-rank treats examination as the missing piece: you can only supervise a click on documents the user actually saw.[4]Reference 4Unbiased Learning-to-Rank with Biased Feedbackhttps://arxiv.org/abs/1608.04468

Why can't raw click rate serve as an unbiased relevance label?

Answer

Users can click only items they were shown, and higher-ranked items receive more attention. Log exposure and position, then use controlled exploration or bias-aware estimation before treating clicks as preference evidence.

Protect users and the dataset

Ranking can narrow what developers see. Eligibility constraints for stale documents, private drafts, and source policy run before retrieval because those items must never compete. Diversity among otherwise eligible items belongs after scoring: if product policy permits at most two results from one source in the top three, adjust the final slate deterministically and gate that invariant in release checks. Monitor slices such as new documents, small doc sets, languages, and stale categories rather than accepting one portal-wide NDCG.

Policy must stay outside the learned score. Reproduce a bad integration that ranks the raw corpus without applying eligibility first.

reproduce-policy-bypass.py
1unsafe_order = sorted( 2 corpus, 3 key=lambda item: (-rank_score(item), item["id"]), 4) 5 6print("unsafe first result:", unsafe_order[0]["id"]) 7print("permitted:", unsafe_order[0]["permitted"])
Output
1unsafe first result: private-draft 2permitted: False

The private draft ranks first because its query match is strong. Better model training won't fix this bug. Eligibility filtering must run before retrieval and remain covered by release checks.

Gate an offline candidate

A useful release packet includes offline query judgments, candidate recall, reranker NDCG, p95 scoring latency, diversity checks, impression and outcome schemas, and an A/B stopping rule. A list that looks good offline isn't allowed to silently write its own future training labels.

Use concrete gates for this candidate:

  • candidate recall at 5 must be 1.0
  • reranker NDCG@5 must be at least 0.95
  • p95 scorer latency must be at most 20 milliseconds
  • blocked documents must stay absent
  • impression rows must include fields needed for replay
  • outcome events must join a logged impression
check-offline-ranking-gates.py
1latencies_ms = [11, 13, 12, 15, 14, 16, 13, 12, 17, 18] 2 3def percentile_nearest_rank(values, percentile): 4 rank = max(1, int(len(values) * percentile + 0.999999)) 5 return sorted(values)[rank - 1] 6 7required_impression_fields = { 8 "request_id", 9 "query", 10 "eligibility_policy", 11 "candidate_set", 12 "ranker", 13 "document_id", 14 "position", 15} 16required_outcome_fields = { 17 "event_id", 18 "occurred_at", 19 "request_id", 20 "document_id", 21 "event", 22} 23exposed_keys = { 24 (row["request_id"], row["document_id"]) 25 for row in impressions 26} 27release_checks = { 28 "candidate_recall_at_5": candidate_recall(candidates) >= 1.0, 29 "reranker_ndcg_at_5": reranker_ndcg(ranked, 5) >= 0.95, 30 "p95_latency_ms": percentile_nearest_rank(latencies_ms, 0.95) <= 20, 31 "blocked_items_absent": not ( 32 {"private-draft", "stale-guide"} 33 & {item["id"] for item in ranked} 34 ), 35 "impression_schema": all( 36 required_impression_fields <= row.keys() 37 for row in impressions 38 ), 39 "outcome_schema": all( 40 required_outcome_fields <= row.keys() 41 for row in outcomes 42 ), 43 "outcomes_join_impressions": all( 44 (row["request_id"], row["document_id"]) in exposed_keys 45 for row in outcomes 46 ), 47} 48 49for name, passed in release_checks.items(): 50 print(f"{name}: {passed}") 51print("release gate:", all(release_checks.values()))
Output
1candidate_recall_at_5: True 2reranker_ndcg_at_5: True 3p95_latency_ms: True 4blocked_items_absent: True 5impression_schema: True 6outcome_schema: True 7outcomes_join_impressions: True 8release gate: True

Passing offline checks earns a controlled online experiment, not an immediate global rollout. Engagement changes can reflect latency, layout, freshness, source trust, and ranking behavior. Randomized A/B assignment isolates the candidate change more reliably than comparing two time windows, which is the same discipline as Experiment Design and A/B Testing.[5]Reference 5Trustworthy Online Controlled Experiments: A Practical Guide to A/B Testing.https://experimentguide.com/ Predeclare minimum sample size, minimum duration, and safety stops before exposure starts so the team doesn't stop when a noisy result happens to look favorable. The miniature receipt below uses illustrative commitments; real values come from traffic, power analysis, and risk policy.

Publish a receipt before exposure begins.

publish-ranking-candidate-receipt.py
1receipt = { 2 "artifact": "docs-ranker-v7", 3 "eligibility_policy": "docs-access-policy-v4", 4 "candidate_set": "retrieval-v3", 5 "offline": { 6 "candidate_recall_at_5": round(candidate_recall(candidates), 3), 7 "reranker_ndcg_at_5": round(reranker_ndcg(ranked, 5), 3), 8 "p95_latency_ms": percentile_nearest_rank(latencies_ms, 0.95), 9 }, 10 "release_checks": release_checks, 11 "required_checks_pass": all(release_checks.values()), 12 "experiment": { 13 "assignment_unit": "developer_id", 14 "control": "docs-ranker-v6", 15 "treatment": "docs-ranker-v7", 16 "primary_metric": "successful_resolutions_per_session", 17 "minimum_duration_days": 14, 18 "minimum_requests_per_arm": 5000, 19 "guardrails": [ 20 "bad_result_reports_per_session", 21 "p95_latency_ms", 22 "blocked_document_impressions", 23 ], 24 "safety_stops": { 25 "blocked_document_impressions": 0, 26 "p95_latency_ms": 20, 27 }, 28 }, 29 "status": "candidate_for_ab_test" if all(release_checks.values()) else "blocked", 30} 31payload = dumps(receipt, sort_keys=True) 32print("status:", receipt["status"]) 33print("offline:", receipt["offline"]) 34print("receipt sha256:", sha256(payload.encode()).hexdigest()[:12])
Output
1status: candidate_for_ab_test 2offline: {'candidate_recall_at_5': 1.0, 'reranker_ndcg_at_5': 1.0, 'p95_latency_ms': 18} 3receipt sha256: d0fe7347e924

The receipt binds eligibility policy, candidate generator, ranker, offline evidence, release checks, assignment unit, primary metric, minimum exposure, and guardrails. Your later analysis must keep these versions pinned.

When ranking breaks

SymptomCauseFix
NDCG looks strong but desired document never appearscandidates lost recallmeasure retrieval recall and system NDCG separately from reranker NDCG
Prohibited document appears firstpolicy filter ran after scoring or not at allfilter eligibility before retrieval and gate blocked impressions
Popular documents dominate foreverclick-position looplog exposure and use controlled evaluation
Corrected click score still looks suspiciouspropensity estimate is wrongvalidate click model or collect randomized intervention data
Corrected click score becomes unstabletiny propensities create large inverse weightsadd variance controls and evaluate estimator behavior
Relevance improves while bad-result reports risewrong online objectivepair engagement with success and bad-result guardrails
Complete the lesson

Mastery Check

Answer every question, then check your score. Score 75% or higher to mark this lesson complete.

1.A candidate generator for the query returns, in order, ['api-guide', 'blog-post', 'legacy-changelog']. The judged relevant eligible documents are {'api-guide', 'retry-runbook', 'sdk-example'}. What does this imply before any reranker runs?

Correct answer: Candidate recall is 1/3; retry-runbook and sdk-example can't appear because they never reach the reranker.

Candidate recall counts judged relevant eligible documents that survive candidate generation. Only api-guide is returned, so recall is 1 out of 3. A reranker can reorder only its input set, so missing relevant documents must be exposed by recall rather than hidden inside a ranking metric on survivors.

2.An engineer proposes using each document's offline human relevance grade as an online ranker feature. What is the correct design?

Correct answer: Reserve relevance for labels and evaluation; score online with available signals such as query fit and reliability.

The relevance grade is an offline judgment for a frozen query. Treating it as a serving feature leaks the target and assumes a judgment exists for future requests. The ranker should instead use available query, document, freshness, and source-trust signals, while relevance supplies training labels and evaluation grades.

3.For a fixed candidate set, the retrieval order has relevance grades [3, 0, 0, 2, 2], while the reranked order has [3, 2, 2, 0, 0]. Why is the reranked slate's NDCG@5 equal to 1.0?

Correct answer: It matches the ideal candidate order [3, 2, 2, 0, 0], so DCG equals IDCG here.

NDCG@5 divides the slate's DCG@5 by the DCG of the best five grades selected from all candidates and sorted by relevance. Here the candidate set has exactly five items, and [3, 2, 2, 0, 0] is already ideal, so the normalized value is 1.0. This is a reranker metric over surviving candidates, not proof of whole-corpus recall.

4.A developer accidentally sorts the raw corpus with rank_score before eligibility filtering. The top result is 'private-draft' with permitted=False because it has strong query_fit and reliability. What change fixes the required policy invariant?

Correct answer: Filter freshness and access policy before retrieval, then gate that blocked documents stay absent.

Policy eligibility is a hard constraint, not a preference for the ranker to learn. If forbidden or stale documents enter retrieval or scoring, a strong relevance score can put them at the top. Filter them before candidate generation and verify that blocked documents remain absent in release checks.

5.A ranked top-three slate contains three permitted, current documents from one source. A hard rule allows at most two documents from one source in the top three. What should the system do?

Correct answer: Apply the diversity constraint after scoring, adjust the slate to at most two, and gate releases on that invariant.

The documents are eligible, so the issue isn't access permission or freshness filtering. It's a final-slate constraint. A hard diversity rule should deterministically adjust the ranked slate and be covered by release checks rather than left for a learned score to trade away.

6.Which logging design preserves the evidence needed to replay a slate, join outcomes, and analyze position bias?

Correct answer: Log every exposed document before outcomes, with request, position, snapshots, and component versions.

Clicks alone omit the documents that were shown but ignored, and aggregate logs lose request-level exposure. An immutable impression row written before outcomes records the actual slate, position, serving context, and deployed versions. Later events can then join by request and document.

7.A team adds query-document cross-attention inside the item encoder used for first-stage retrieval. What serving property does this break?

Correct answer: Item vectors can no longer be precomputed independently because each representation now depends on the live query.

Two-tower retrieval scales by encoding items independently of the live query. Query-document cross-attention couples both sides, so the item representation changes per request. Keep that pairwise work behind candidate retrieval, where it runs over a bounded set.

8.Document A at slot 1 has 40 clicks from 100 impressions with propensity 1.0. Document B at slot 2 has 20 clicks from 100 impressions with assumed propensity 0.25. Using clicks / propensity / impressions, what follows?

Correct answer: A=0.40 and B=0.80, but the gap depends on the propensity assumption and doesn't prove B is twice as appealing.

A's corrected signal is 40 / 1.0 / 100 = 0.40. B's is 20 / 0.25 / 100 = 0.80. Changing the assumed slot-two propensity changes the conclusion even though the clicks didn't change. Propensities therefore require careful estimation, and small values create large, noisy inverse weights.

9.A reranker achieves NDCG@5 of 1.0 on one frozen query. What evidence is still needed before treating its offline ranking quality as established?

Correct answer: Average NDCG over a frozen query set, inspect failed queries and slices, and report candidate recall separately.

A perfect score on one query establishes only that one judged slate. Ranking quality must be aggregated over a frozen query set and examined on slices such as languages and new documents. Candidate recall remains separate because reranker NDCG over survivors can hide retrieval losses.

10.A candidate passes these offline gates: candidate_recall_at_5=1.0, reranker_ndcg_at_5=1.0, nearest-rank p95 latency=18 ms with a 20 ms limit, blocked documents absent, valid impression and outcome schemas, and outcomes join impressions. What status is justified?

Correct answer: It's a candidate for a controlled A/B test with pinned versions, predeclared metrics, minimum exposure, guardrails, and safety stops.

Passing offline checks is necessary but not enough for a global rollout. Live engagement can change because of latency, layout, freshness, source trust, and ranking behavior. Randomized assignment, pinned versions, minimum exposure, guardrails, and safety stops reduce the chance of accepting a noisy or unsafe change.

10 questions remaining.

Next Step
Continue to Forecasting and Anomaly Detection

Ranking scores a slate for one request. Next you'll predict values that unfold over time and tell a repeating weekly pattern apart from a real operational surprise.

PreviousGradient Boosted Trees in Production
Share this article
XFacebookLinkedInBlueskyRedditHacker NewsEmail
References

Introduction to Information Retrieval.

Manning, C. D., Raghavan, P., Schutze, H. · 2008 · Cambridge University Press

https://nlp.stanford.edu/IR-book/

Learning Deep Structured Semantic Models for Web Search Using Clickthrough Data.

Huang, P.-S., He, X., Gao, J., Deng, L., Acero, A., & Heck, L. · 2013 · CIKM

https://www.microsoft.com/en-us/research/publication/learning-deep-structured-semantic-models-for-web-search-using-clickthrough-data/

Learning to Rank using Gradient Descent.

Burges, C. J. C., et al. · 2005 · ICML 2005

https://www.microsoft.com/en-us/research/publication/learning-to-rank-using-gradient-descent/

Unbiased Learning-to-Rank with Biased Feedback

Joachims, T., Swaminathan, A., & Schnabel, T. · 2017 · WSDM 2017

https://arxiv.org/abs/1608.04468

Trustworthy Online Controlled Experiments: A Practical Guide to A/B Testing.

Kohavi, R., Tang, D., Xu, Y. · 2020

https://experimentguide.com/

Discussion

Questions and insights from fellow learners.

Discussion loads when you reach this section.