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 158 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/23
Dimensionality Reduction for EmbeddingsCoT, ToT & Self-Consistency PromptingFunction Calling & Tool UseMCP & Tool Protocol StandardsPrompt 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&BMixed Precision TrainingModel 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: Production Agent
🧠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/16
Scaling Laws & Compute-Optimal TrainingPre-training Data at ScaleBuild GPT from Scratch LabContinued Pretraining for Domain ShiftSynthetic Data PipelinesSupervised Fine-Tuning PipelineDistributed Training: FSDP & ZeROLoRA & Parameter-Efficient TuningReward Modeling from Preference DataRLHF & DPO AlignmentConstitutional AI & Red TeamingRLVR & Verifiable RewardsKnowledge Distillation for LLMsModel Merging and Weight InterpolationPrompt Optimization with DSPyRecursive Language Models (RLM)
🤖Advanced Agents & Retrieval0/14
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 & RecoveryMulti-Agent Orchestration
⚡Inference & Production Scale0/20
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 ManagementContext EngineeringMixture 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 & Test-Time Compute
🎤AI Lab Interviewing0/4
AI Lab Coding Interview: Python SystemsAI Lab System Design InterviewAI Lab Behavioral InterviewAI Lab Technical Presentation
Back to Topics
LearnPortfolio CapstonesCapstone: Product Ranking
📊HardEvaluation & Benchmarks

Capstone: Product Ranking

Ship a marketplace ranking candidate with eligible retrieval, separate recall and NDCG gates, replayable exposure rows, and an A/B-ready rollback receipt.

15 min read
Learning path
Step 80 of 158 in the full curriculum
Capstone: Delivery ETA PredictionCapstone: Demand Forecasting

The ETA project produced one guarded prediction for one parcel. This project makes a decision that shapes its own future data: which products shoppers see near the top of search results.

Your task is to ship a ranking candidate for searches such as insulated delivery bag. It may reorder eligible, in-stock listings. It may not surface blocked listings or call clicks an unbiased truth signal.

Product-ranking release evidence for the label-printer query. Raw high-scoring listings P8 and P11 are rejected before retrieval because P8 is policy-blocked and out of stock while P11 is unavailable in the shopper region. Eligible products P5, P6, and P4 enter ranking. Control orders them P4, P5, P6 with NDCG 0.736; treatment orders P5, P6, P4 with NDCG 1.000. An immutable P5 position-one impression precedes click and purchase outcomes. The release receipt shows recall 1.000, ranker p95 latency 18 milliseconds, 12 of 12 gates, and candidate-for-A-B-test status. Product-ranking release evidence for the label-printer query. Raw high-scoring listings P8 and P11 are rejected before retrieval because P8 is policy-blocked and out of stock while P11 is unavailable in the shopper region. Eligible products P5, P6, and P4 enter ranking. Control orders them P4, P5, P6 with NDCG 0.736; treatment orders P5, P6, P4 with NDCG 1.000. An immutable P5 position-one impression precedes click and purchase outcomes. The release receipt shows recall 1.000, ranker p95 latency 18 milliseconds, 12 of 12 gates, and candidate-for-A-B-test status.
Eligibility removes the two strongest raw matches before ranking. Treatment then improves the displayed order, while the immutable P5 impression preserves the exposure that makes later click and purchase events interpretable.

Specify the Ranking Surface

Define the scope tightly:

ContractDecision
surfacemarketplace search results
query setfrozen judged search queries plus online experiment traffic
eligibilityin-stock, deliverable region, policy-approved listing
offline metriccandidate recall@100 and NDCG@10
online primary metricpurchase conversion per search
guardrailsreturns, latency, unsafe listings, seller concentration

Candidate generation and reranking are separate artifacts. The candidate component must retrieve relevant items reliably. The ranker can only adjust order inside that eligible set.

Learning-to-rank methods can learn pairwise preferences so a relevant product receives a higher score than a less relevant candidate.[1]Reference 1Learning to Rank using Gradient Descent.https://www.microsoft.com/en-us/research/publication/learning-to-rank-using-gradient-descent/ That modeling detail doesn't excuse missing policy: eligibility filtering must run before scoring, and its version belongs in every impression log.

Diagram showing Catalog snapshot inventory + policy, Eligible candidates recall@100, Ranker v1 NDCG@10, and Displayed slate + impression log position + assignment. Diagram showing Catalog snapshot inventory + policy, Eligible candidates recall@100, Ranker v1 NDCG@10, and Displayed slate + impression log position + assignment.
Catalog snapshot inventory + policy, Eligible candidates recall@100, Ranker v1 NDCG@10, and Displayed slate + impression log position + assignment.

Build offline evidence first

Create a judged fixture set. Each query has eligible candidates and graded relevance from human review:

text
1ranking-product/ 2 data/ 3 catalog_snapshot.jsonl 4 judged_queries.jsonl 5 eligibility_policy.json 6 retrieval/ 7 candidate_generator.py 8 ranking/ 9 ranker.py 10 evaluate_ndcg.py 11 experiments/ 12 impression_schema.json 13 ab_plan.md 14 tests/ 15 test_blocked_listing_never_surfaces.py 16 test_ndcg_regression_gate.py

Required offline checks:

CheckWhy it blocks release
eligible-only resultsa relevant prohibited listing still can't display
candidate recall@100the ranker can't repair missing products
NDCG@10 by query categoryoverall improvement may hide poor critical categories
scoring latencya slower list damages shopping experience
diversity/seller concentrationone seller with many feedback events shouldn't crowd out catalog

Filter policy before retrieval

Start with the boundary that a model can't learn safely: listing eligibility. A prohibited or sold-out product may have an excellent text match and a high learned score. It still can't reach the ranker.

The compact fixture below keeps two judged searches in memory. Production might retrieve 100 candidates per query; this local receipt uses 3 so each listing stays visible.

01-marketplace-eligibility-contract.py
1from collections import Counter 2from dataclasses import dataclass 3from datetime import datetime 4from math import ceil, log2 5import json 6 7@dataclass(frozen=True) 8class Listing: 9 product_id: str 10 query: str 11 relevance: int 12 retrieval_score: float 13 baseline_rank_score: float 14 candidate_rank_score: float 15 in_stock: bool 16 policy_approved: bool 17 deliverable_region: bool 18 seller_id: str 19 20CATALOG = [ 21 Listing("P1", "insulated-bag", 3, 0.96, 0.82, 0.97, True, True, True, "S1"), 22 Listing("P2", "insulated-bag", 1, 0.88, 0.75, 0.62, True, True, True, "S2"), 23 Listing("P3", "insulated-bag", 2, 0.84, 0.65, 0.90, True, True, True, "S3"), 24 Listing("P9", "insulated-bag", 3, 0.99, 0.99, 0.99, True, False, True, "S9"), 25 Listing("P10", "insulated-bag", 2, 0.95, 0.98, 0.96, False, True, True, "S1"), 26 Listing("P4", "label-printer", 1, 0.74, 0.90, 0.45, True, True, True, "S4"), 27 Listing("P5", "label-printer", 3, 0.93, 0.72, 0.96, True, True, True, "S5"), 28 Listing("P6", "label-printer", 2, 0.86, 0.65, 0.84, True, True, True, "S6"), 29 Listing("P8", "label-printer", 3, 0.98, 0.99, 0.99, False, False, True, "S8"), 30 Listing("P11", "label-printer", 2, 0.97, 0.97, 0.97, True, True, False, "S11"), 31] 32 33QUERIES = ("insulated-bag", "label-printer") 34 35def exclusion_reason(listing: Listing) -> str | None: 36 if not listing.policy_approved: 37 return "policy_block" 38 if not listing.in_stock: 39 return "out_of_stock" 40 if not listing.deliverable_region: 41 return "unavailable_region" 42 return None 43 44print("catalog listings:", len(CATALOG)) 45print("queries:", QUERIES)
Output
1catalog listings: 10 2queries: ('insulated-bag', 'label-printer')
02-filter-and-retrieve-candidates.py
1def retrieve(query: str, budget: int = 3) -> list[Listing]: 2 eligible = [ 3 listing for listing in CATALOG 4 if listing.query == query and exclusion_reason(listing) is None 5 ] 6 return sorted(eligible, key=lambda listing: (-listing.retrieval_score, listing.product_id))[:budget] 7 8def relevant_ids(query: str) -> set[str]: 9 return { 10 listing.product_id for listing in CATALOG 11 if listing.query == query and exclusion_reason(listing) is None and listing.relevance >= 2 12 } 13 14def candidate_recall(query: str, candidates: list[Listing]) -> float: 15 relevant = relevant_ids(query) 16 if not relevant: 17 raise ValueError(f"{query}: no relevant eligible judgments") 18 return len(relevant & {listing.product_id for listing in candidates}) / len(relevant) 19 20retrieved = {query: retrieve(query) for query in QUERIES} 21rejected = { 22 listing.product_id: exclusion_reason(listing) 23 for listing in CATALOG 24 if exclusion_reason(listing) is not None 25} 26 27for query, candidates in retrieved.items(): 28 print(query, [listing.product_id for listing in candidates], f"recall@3={candidate_recall(query, candidates):.3f}") 29print("rejected:", rejected)
Output
1insulated-bag ['P1', 'P2', 'P3'] recall@3=1.000 2label-printer ['P5', 'P6', 'P4'] recall@3=1.000 3rejected: {'P9': 'policy_block', 'P10': 'out_of_stock', 'P8': 'policy_block', 'P11': 'unavailable_region'}

Products P9 and P8 have the strongest scores for their searches, but policy excludes them. Product P10 is approved but out of stock. Product P11 can't ship to the shopper's region. Filtering first prevents the ranker from treating a business invariant as a preference it can trade away.

Measure Retrieval and Ordering Separately

Candidate recall asks whether relevant eligible products reached the ranker. Normalized discounted cumulative gain (NDCG) then asks whether stronger judgments appear near the top of the returned list. A ranker can't repair a relevant product that retrieval dropped.

The next cell reranks each eligible candidate set and compares baseline and candidate NDCG. It also rechecks the invariant after scoring.

03-ndcg-ranking-helpers.py
1def dcg(rows: list[Listing]) -> float: 2 return sum((2**listing.relevance - 1) / log2(rank + 2) for rank, listing in enumerate(rows)) 3 4def ndcg(rows: list[Listing]) -> float: 5 ideal = sorted(rows, key=lambda listing: (-listing.relevance, listing.product_id)) 6 ideal_dcg = dcg(ideal) 7 return dcg(rows) / ideal_dcg if ideal_dcg else 0.0 8 9def rerank(rows: list[Listing], score_field: str) -> list[Listing]: 10 return sorted(rows, key=lambda listing: (-getattr(listing, score_field), listing.product_id)) 11 12baseline_ranked = { 13 query: rerank(candidates, "baseline_rank_score") 14 for query, candidates in retrieved.items() 15} 16candidate_ranked = { 17 query: rerank(candidates, "candidate_rank_score") 18 for query, candidates in retrieved.items() 19} 20 21print("queries ranked:", list(candidate_ranked))
04-compare-retrieval-and-ranking.py
1for query in QUERIES: 2 print( 3 query, 4 f"baseline_ndcg@3={ndcg(baseline_ranked[query]):.3f}", 5 f"candidate_ndcg@3={ndcg(candidate_ranked[query]):.3f}", 6 ) 7 8blocked_hits = [ 9 listing.product_id 10 for rows in candidate_ranked.values() 11 for listing in rows 12 if exclusion_reason(listing) is not None 13] 14print("blocked hits:", blocked_hits)
Output
1insulated-bag baseline_ndcg@3=0.972 candidate_ndcg@3=1.000 2label-printer baseline_ndcg@3=0.736 candidate_ndcg@3=1.000 3blocked hits: []

The candidate improves ordering for both query fixtures. The metric helpers also make two edge policies explicit: a query with no relevant eligible judgments is a fixture error for this capstone, while NDCG returns 0.0 when a ranked set has no positive gain. This still isn't a launch decision. Offline judgments support a controlled experiment, and the displayed slate creates the future data used to evaluate or retrain the system.

Log exposure before learning from outcomes

Every displayed slate should log:

FieldWhy
request_id, query, served_atgroup one displayed decision
shopper_id, experiment_id, experiment_armreplay stable experiment assignment
catalog_snapshot, eligibility_versionshow available choices
candidate_version, ranker_versionidentify scoring path
product_id, positionpreserve exposure

Clicks alone aren't reliable targets: top-ranked items receive attention because they are top-ranked. Joachims, Swaminathan, and Schnabel show why position bias makes direct click training suboptimal and derive a counterfactual correction for biased implicit feedback.[2]Reference 2Unbiased Learning-to-Rank with Biased Feedbackhttps://arxiv.org/abs/1608.04468 Use judged offline sets for regression detection, then use a controlled A/B experiment to compare user outcomes. Kohavi et al. describe online experimentation as a disciplined way to measure product changes.[3]Reference 3Trustworthy Online Controlled Experiments: A Practical Guide to A/B Testing.https://experimentguide.com/

Clicks, purchases, and returns arrive later. Append them as outcome events with event_id, occurred_at, request_id, product_id, and outcome. Don't overwrite the served impression row. An A/B plan should state allocation, duration or sample-size rule, primary metric, latency and return-rate guardrails, prohibited-listing invariant, and stop conditions. A conversion lift that increases returns or policy violations isn't a successful ranking release.

The final cell publishes replayable impression rows, illustrative joined outcome events, and one candidate receipt. It doesn't invent live lift. It proves offline evidence, exposure schema, stable assignment, outcome join, and rollback pointer are ready before traffic begins.

05-experiment-arm-config.py
1CATALOG_SNAPSHOT = "catalog-2026-05-01" 2ELIGIBILITY_VERSION = "market-eligibility-v1" 3CANDIDATE_VERSION = "retrieval-v1" 4RANKER_VERSION = "market-ranker-v1" 5PREVIOUS_RANKER = "market-ranker-v0" 6EXPERIMENT_ID = "ranking-ab-2026-05" 7 8ARM_CONFIG = { 9 "control": (PREVIOUS_RANKER, baseline_ranked), 10 "treatment": (RANKER_VERSION, candidate_ranked), 11} 12 13def impression_rows( 14 request_id: str, 15 shopper_id: str, 16 served_at: str, 17 query: str, 18 arm: str, 19) -> list[dict[str, object]]: 20 ranker_version, ranked_by_query = ARM_CONFIG[arm] 21 return [ 22 { 23 "request_id": request_id, 24 "shopper_id": shopper_id, 25 "served_at": served_at, 26 "query": query, 27 "catalog_snapshot": CATALOG_SNAPSHOT, 28 "eligibility_version": ELIGIBILITY_VERSION, 29 "candidate_version": CANDIDATE_VERSION, 30 "ranker_version": ranker_version, 31 "experiment_id": EXPERIMENT_ID, 32 "experiment_arm": arm, 33 "product_id": listing.product_id, 34 "position": position, 35 } 36 for position, listing in enumerate(ranked_by_query[query], start=1) 37 ] 38 39print("experiment:", EXPERIMENT_ID, "arms:", list(ARM_CONFIG))
06-log-impression-rows.py
1control_impressions = impression_rows("REQ-500", "SHOP-100", "2026-05-01T12:00:00Z", "label-printer", "control") 2treatment_impressions = impression_rows("REQ-501", "SHOP-200", "2026-05-01T12:00:05Z", "label-printer", "treatment") 3impressions = control_impressions + treatment_impressions 4outcomes = [ 5 { 6 "event_id": "EVT-900", 7 "occurred_at": "2026-05-01T12:00:08Z", 8 "request_id": "REQ-501", 9 "product_id": "P5", 10 "outcome": "click", 11 }, 12 { 13 "event_id": "EVT-901", 14 "occurred_at": "2026-05-01T12:04:18Z", 15 "request_id": "REQ-501", 16 "product_id": "P5", 17 "outcome": "purchase", 18 }, 19] 20 21print("impressions:", len(impressions), "outcomes:", len(outcomes))
07-join-outcomes-to-impressions.py
1required_impression_fields = { 2 "request_id", "shopper_id", "served_at", "query", "catalog_snapshot", 3 "eligibility_version", "candidate_version", "ranker_version", 4 "experiment_id", "experiment_arm", "product_id", "position", 5} 6required_outcome_fields = {"event_id", "occurred_at", "request_id", "product_id", "outcome"} 7outcome_event_ids = [row["event_id"] for row in outcomes] 8latencies_ms = [11, 13, 12, 15, 14, 16, 13, 12, 17, 18] 9 10def nearest_rank(values: list[int], percentile: float) -> int: 11 if not values: 12 raise ValueError("latency sample must not be empty") 13 rank = max(1, ceil(len(values) * percentile)) 14 return sorted(values)[rank - 1] 15 16baseline_ndcg = sum(ndcg(rows) for rows in baseline_ranked.values()) / len(QUERIES) 17candidate_ndcg = sum(ndcg(rows) for rows in candidate_ranked.values()) / len(QUERIES) 18max_seller_count = max( 19 max(Counter(listing.seller_id for listing in rows).values()) 20 for rows in candidate_ranked.values() 21) 22impression_index = { 23 (row["request_id"], row["product_id"]): row 24 for row in impressions 25} 26shopper_arms: dict[str, set[str]] = {} 27for row in impressions: 28 shopper_arms.setdefault(row["shopper_id"], set()).add(row["experiment_arm"]) 29 30def parse_utc(value: str) -> datetime: 31 return datetime.fromisoformat(value.replace("Z", "+00:00")) 32 33def outcome_follows_impression(row: dict[str, object]) -> bool: 34 impression = impression_index.get((row["request_id"], row["product_id"])) 35 return impression is not None and parse_utc(impression["served_at"]) < parse_utc(row["occurred_at"]) 36 37print("baseline_ndcg:", round(baseline_ndcg, 3), "candidate_ndcg:", round(candidate_ndcg, 3))
08-offline-release-gates.py
1release_gates = { 2 "candidate_recall_complete": all(candidate_recall(query, retrieved[query]) == 1.0 for query in QUERIES), 3 "candidate_ndcg_improves": candidate_ndcg > baseline_ndcg, 4 "ndcg_slices_at_least_0_95": all(ndcg(rows) >= 0.95 for rows in candidate_ranked.values()), 5 "ranker_p95_latency_ms_at_most_20": nearest_rank(latencies_ms, 0.95) <= 20, 6 "blocked_items_absent": not blocked_hits, 7 "impression_schema": all(required_impression_fields <= row.keys() for row in impressions), 8 "outcome_schema": all(required_outcome_fields <= row.keys() for row in outcomes), 9 "outcome_event_ids_unique": len(outcome_event_ids) == len(set(outcome_event_ids)), 10 "outcomes_join_impressions": all((row["request_id"], row["product_id"]) in impression_index for row in outcomes), 11 "outcomes_follow_impressions": all(outcome_follows_impression(row) for row in outcomes), 12 "one_arm_per_shopper": all(len(arms) == 1 for arms in shopper_arms.values()), 13 "seller_concentration_at_most_2_per_slate": max_seller_count <= 2, 14} 15 16print("release_gates_pass:", all(release_gates.values()))
09-assemble-experiment-receipt.py
1receipt = { 2 "bundle_id": RANKER_VERSION, 3 "previous_ranker": PREVIOUS_RANKER, 4 "catalog_snapshot": CATALOG_SNAPSHOT, 5 "eligibility_version": ELIGIBILITY_VERSION, 6 "candidate_version": CANDIDATE_VERSION, 7 "offline": { 8 "baseline_ndcg_at_3": round(baseline_ndcg, 3), 9 "candidate_ndcg_at_3": round(candidate_ndcg, 3), 10 "ranker_p95_latency_ms": nearest_rank(latencies_ms, 0.95), 11 }, 12 "release_gates": release_gates, 13 "experiment": { 14 "experiment_id": EXPERIMENT_ID, 15 "assignment_unit": "shopper_id", 16 "control": PREVIOUS_RANKER, 17 "treatment": RANKER_VERSION, 18 "allocation": {"control": 0.5, "treatment": 0.5}, 19 "minimum_eligible_searches": 10000, 20 "primary_metric": "purchase_conversion_per_search", 21 "guardrails": ["returns_per_purchase", "search_p95_latency_ms", "blocked_listing_impressions", "seller_concentration_at_3"], 22 "stop_conditions": [ 23 "blocked_listing_impressions > 0", 24 "search_p95_latency_ms > 120", 25 "returns_per_purchase > control + 0.01", 26 ], 27 }, 28 "candidate_decision": "candidate_for_ab_test" if all(release_gates.values()) else "hold", 29} 30 31print("candidate_decision:", receipt["candidate_decision"])
10-publish-ranking-experiment-receipt.py
1print("control slate:", [row["product_id"] for row in control_impressions]) 2print("treatment slate:", [row["product_id"] for row in treatment_impressions]) 3print("first treatment impression:", json.dumps(treatment_impressions[0], sort_keys=True)) 4print("first treatment outcome:", json.dumps(outcomes[0], sort_keys=True)) 5print(json.dumps(receipt, indent=2))
Output
1control slate: ['P4', 'P5', 'P6'] 2treatment slate: ['P5', 'P6', 'P4'] 3first treatment impression: {"candidate_version": "retrieval-v1", "catalog_snapshot": "catalog-2026-05-01", "eligibility_version": "market-eligibility-v1", "experiment_arm": "treatment", "experiment_id": "ranking-ab-2026-05", "position": 1, "product_id": "P5", "query": "label-printer", "ranker_version": "market-ranker-v1", "request_id": "REQ-501", "served_at": "2026-05-01T12:00:05Z", "shopper_id": "SHOP-200"} 4first treatment outcome: {"event_id": "EVT-900", "occurred_at": "2026-05-01T12:00:08Z", "outcome": "click", "product_id": "P5", "request_id": "REQ-501"} 5{ 6 "bundle_id": "market-ranker-v1", 7 "previous_ranker": "market-ranker-v0", 8 "catalog_snapshot": "catalog-2026-05-01", 9 "eligibility_version": "market-eligibility-v1", 10 "candidate_version": "retrieval-v1", 11 "offline": { 12 "baseline_ndcg_at_3": 0.854, 13 "candidate_ndcg_at_3": 1.0, 14 "ranker_p95_latency_ms": 18 15 }, 16 "release_gates": { 17 "candidate_recall_complete": true, 18 "candidate_ndcg_improves": true, 19 "ndcg_slices_at_least_0_95": true, 20 "ranker_p95_latency_ms_at_most_20": true, 21 "blocked_items_absent": true, 22 "impression_schema": true, 23 "outcome_schema": true, 24 "outcome_event_ids_unique": true, 25 "outcomes_join_impressions": true, 26 "outcomes_follow_impressions": true, 27 "one_arm_per_shopper": true, 28 "seller_concentration_at_most_2_per_slate": true 29 }, 30 "experiment": { 31 "experiment_id": "ranking-ab-2026-05", 32 "assignment_unit": "shopper_id", 33 "control": "market-ranker-v0", 34 "treatment": "market-ranker-v1", 35 "allocation": { 36 "control": 0.5, 37 "treatment": 0.5 38 }, 39 "minimum_eligible_searches": 10000, 40 "primary_metric": "purchase_conversion_per_search", 41 "guardrails": [ 42 "returns_per_purchase", 43 "search_p95_latency_ms", 44 "blocked_listing_impressions", 45 "seller_concentration_at_3" 46 ], 47 "stop_conditions": [ 48 "blocked_listing_impressions > 0", 49 "search_p95_latency_ms > 120", 50 "returns_per_purchase > control + 0.01" 51 ] 52 }, 53 "candidate_decision": "candidate_for_ab_test" 54}

candidate_for_ab_test is intentionally narrower than launch approval. The local 20 millisecond gate measures ranker scoring only; the experiment's 120 millisecond stop applies to end-to-end search latency. The receipt says which ranker deserves controlled exposure, which immutable logs make later outcomes interpretable, and which previous alias remains available if guardrails fail.

Submission checklist

ArtifactEvidence
eligibility policyblocked items can't enter ranking
candidate evaluationrecall measured before reranking
ranker evaluationNDCG slices and latency recorded
impression schemapositions, versions, and assignment context logged before outcomes
outcome streamlater events append separately and join displayed products
experiment planstable assignment, metrics, guardrails, stop rules
rollbackstable ranker alias remains available

Practice: break the ranking contract

Use the runnable examples as a release harness. Change one condition at a time, predict the result, then rerun the examples.

  1. Change P3 retrieval score from 0.84 to 0.10, then change retrieve() default budget from 3 to 2. Which metric exposes missing relevant product?
  2. Set P9.policy_approved to True. Why isn't that a harmless relevance change?
  3. Change candidate score for P6 from 0.84 to 0.20. Which NDCG slice fails?
  4. Remove position from impression_rows(). Which receipt gate blocks experiment readiness?
  5. Change all three eligible insulated-bag listings to seller S1. Which marketplace guardrail fails?
  6. Use SHOP-100 for both control and treatment impressions. Which assignment gate fails?
  7. Change first outcome's product_id from P5 to P9. Which replay gate fails?
  8. Append a retry with the same event_id as EVT-900. Which deduplication gate fails?

Practice answer sketches

Mastery check

Evaluation rubric

ArtifactStrong submission demonstrates
ranking stackexplicit eligibility, candidate generation, reranking inputs, and replayable slate logs
evaluationNDCG-style offline result with policy guardrails and slice review
experiment planimmutable impressions, joined outcomes, stable assignment, guardrails, stopping rule, and rollback

Common failures

SymptomCauseFix
Great NDCG while prohibited item appearsranking runs before eligibilityfilter first and test invariant
Strong ranker metric while relevant product never appearscandidate retrieval dropped it before rerankinggate candidate recall separately
New model increasingly favors former winnersclicks used without exposure evidencelog impressions and experiment arms
Outcome history changes after retriesmutable impression row overwritten with later statusappend outcome events and join by request plus product
Same shopper appears in both A/B armsassignment identity wasn't preservedlog shopper and experiment IDs before exposure
Conversion increases while customer value fallsreturns absent from guardrailsgate promotion on downstream outcomes
Complete the lesson

Mastery Check

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

1.P9 has the highest scores for insulated-bag but is excluded by policy_block. What does flipping P9.policy_approved to True change?
2.A relevant eligible product, P3, is dropped when the retrieval budget for insulated-bag is reduced before reranking. Which release gate should fail?
3.In the label-printer fixture, if P5's candidate score falls from 0.96 to 0.20, the treatment order becomes P6, P4, P5 instead of ideal P5, P6, P4. Which release gate catches this regression even though the two-query treatment average can still beat the baseline?
4.An impression row for a displayed search slate omits position but keeps request, shopper, query, version, experiment, and product fields. Which release gate should fail?
5.A team wants to train the next ranker by labeling every clicked product as preferred and every unclicked product as irrelevant, using only outcome rows. Why is this unsafe?
6.An outcome event for request REQ-501 is changed from product P5 to product P9, and the impression rows for REQ-501 displayed only P5, P6, and P4. Which gate directly tests whether the appended outcome is usable as ranking feedback?
7.A treatment slate contains three eligible products, all from seller S1. The release receipt allows at most two products from one seller in a slate. Which gate should fail?
8.The experiment assignment unit is shopper_id, but SHOP-100 appears in both control and treatment impression rows. Which gate should fail?
9.An offline-qualified treatment enters its A/B test. Purchase conversion increases, but returns per purchase are 0.015 above control. The stop rule is returns per purchase greater than control plus 0.01. What should the team do?
10.A displayed product is clicked and later purchased. An ingestion retry then replays the click event. Which record design keeps the history auditable?

10 questions remaining.

Next Step
Continue to Capstone: Demand Forecasting

You can now ship a measured ranking surface whose outputs influence future data. Next you'll ship time-ordered forecasts and alerts for inventory and warehouse capacity.

PreviousCapstone: Delivery ETA Prediction
Share this article
XFacebookLinkedInBlueskyRedditHacker NewsEmail
References

Learning to Rank using Gradient Descent.

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

Unbiased Learning-to-Rank with Biased Feedback

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

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

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

Discussion

Questions and insights from fellow learners.

Discussion loads when you reach this section.