Features and the Feature Store
In one line: the ranker is mostly its features, and the two hard parts are getting them fast enough at serving time and getting them correct at training time.
Four families
| Family | Examples | Changes | Where it lives |
|---|---|---|---|
| User | Lifetime purchases, preferred categories, price band | Slowly | Precomputed, fetched per request |
| Item | Price, category, quality score, age, seller rating | Slowly to moderately | Precomputed, cached hot |
| Context | Time of day, device, locale, referrer | Per request | Computed inline — free |
| Cross | Times this user bought from this seller | Per pair | The expensive one — computed or looked up per candidate |
The cross row is where the cost is. User features are fetched once per request; cross features are needed for every candidate, so a hundred candidates means a hundred lookups unless they can be computed from what you already have.
That constraint shapes the design: prefer cross features derivable from a user vector and an item vector already in hand — a dot product, a category membership test — over ones needing a per-pair lookup. It is the difference between one fetch and a hundred.
Freshness is per feature, not global
Treating freshness as one setting is the standard mistake. It is a per-feature decision with a real cost curve.
The closing point is worth dwelling on. What a user did in the last few minutes is enormously predictive of what they want next, and it is precisely the feature a daily batch pipeline cannot provide. A team that computes everything nightly has excluded the strongest signal available and will attribute the resulting mediocrity to the model.
The counterpart is that a user's lifetime purchase count being a day stale costs nothing. Paying streaming costs for it is waste.
Point-in-time correctness, in the ranking context
The foundations module established the requirement: every feature in a training row must be reconstructed as of the decision moment. Here is what it means concretely for a ranker.
You are training on a logged impression from three weeks ago. The user's "lifetime purchase count" today is 47. At the moment of that impression it was 31. Training on 47 gives the model information that did not exist — and worse, that number partly reflects the outcome you are predicting, because if they bought this item the count went up.
The model learns to read the future. Offline accuracy is excellent. Production accuracy is not.
Log and wait, rather than reconstruct
The stronger pattern, and the one to propose.
Reconstructing historical feature values is possible with a properly versioned offline store, and it is fiddly and easy to get subtly wrong. The alternative removes the problem:
Log the feature values you actually used, at the moment you used them.
Point-in-time correctness stops being a reconstruction problem and becomes a logging one, which is much easier to get right. The training row is by construction exactly what the model saw.
The costs are real and worth naming: logging a full feature vector per impression is a lot of data — at a hundred candidates per request, you are logging a hundred vectors or sampling them — and the label arrives later, so a join and a waiting period are needed before a row is usable. For delayed conversions that wait can be days, which sets a floor on how quickly you can retrain.
Feature fetch dominates the budget
From the estimation chapter's latency table, and it surprises people:
Feature fetch ~20 ms Candidate generation ~10 ms Pre-ranking ~5 ms Ranking ~20 ms
Feature fetch is comparable to ranking itself, which makes the online store latency-critical infrastructure rather than a database that happens to hold features.
Four levers, roughly in order of value:
Batch the fetch. One multi-get for all candidates, never one call per candidate. This is the single biggest win and the most commonly missed.
Fetch in parallel with candidate generation. User and context features do not depend on which candidates come back, so start them at request arrival.
Cache hot item features. Item features are shared across users and change slowly — the best cache-hit profile in the system.
Cut the tail. A feature that arrives late is worse than a feature that is missing, if the model tolerates absence. Which requires the model to explicitly handle a missing feature rather than imputing a zero — the silent degradation from the monitoring lesson.
Key takeaway
Cross features cost the most because they are needed per candidate rather than per request, so prefer ones derivable from vectors already in hand. Freshness is a per-feature decision, and session-scoped features are usually the strongest signal in the model and exactly what a nightly batch cannot provide. Prefer logging the feature vector you actually served over reconstructing history, which turns point-in-time correctness from a reconstruction problem into a logging one. And batch the feature fetch — it is comparable in cost to ranking itself, which makes the online store latency-critical infrastructure.
Next: what counts as a negative when all you have is what users clicked.