Building a RecSys #
Phase 0: The Cold Start (Day 0 - Minimal Data) #
Scenario: You just launched a new app (newsletter platform or niche e-commerce). You have users and items, but no historical click/purchase data.
At this stage, collaborative filtering is not reliable. Design for robustness with content-based filtering, rules, and careful logging.
How to design Phase 0 #
- Define candidate pools first: popular items, category-matched items, and new arrivals.
- Add strict eligibility filters before ranking (stock, delivery zone, policy/safety).
- Use onboarding answers to build a lightweight user preference profile.
- Rank candidates with simple heuristics: content similarity + popularity + business constraints.
- Reserve a small fixed slot budget for exploration of new items (for example, a few positions per page).
- Log every impression with rank position and context so Phase 2 training data is clean.
How do we know if the user liked a recommended product? #
In practice, we use implicit feedback from logged events after an impression:
- Positive: click, add-to-cart, purchase, long dwell.
- Negative/weak: skip or hide.
- Important: items the user never saw are unknown, not negative.
This is enough to start collecting useful preference labels without explicit star ratings.
Logistics shopping example (Phase 0) #
- New user in Graz opens the app; ask 2-3 onboarding questions (household size, preferred categories, delivery window).
- Build the first feed from three pools: in-stock local bestsellers, category-matched products, and new nearby merchants.
- Enforce hard constraints early: serviceable address, inventory availability, and delivery SLA.
- Keep a few exploration slots for newly onboarded restaurants/products to gather signal.
- Log impression -> click -> cart -> purchase events to prepare nightly ALS/BPR in Phase 2.
Phase 2: The MVP (Months 1-3 - Batch Processing) #
Scenario: You now have hundreds of thousands of interactions (clicks, likes, purchases). Personalization becomes feasible. Yes, in this phase it is often better to pre-compute recommendations at night instead of scoring on every request.
Practical architecture in this phase #
- Build an implicit User-Item Matrix from clicks/add-to-cart/purchases.
- Train a nightly collaborative filtering model (ALS or BPR).
- Precompute top- \(N\) recommendations per active user and store them in cache/DB.
- Serve directly from precomputed lists with lightweight business filters.
- Optionally use XGBoost as a practical ranking baseline before moving to full deep ranking.
Matrix factorization (ALS-style) #
\[\min_{P,Q}\sum_{u,i} c_{ui}\left(r_{ui}-p_u^\top q_i\right)^2+\lambda\left(\|p_u\|^2+\|q_i\|^2\right)\]This captures patterns like: users who bought X also bought Y.
Pairwise alternative (BPR) #
\[\mathcal{L}_{BPR}=-\sum_{(u,i,j)}\log \sigma\left(p_u^\top q_i-p_u^\top q_j\right)+\lambda\|\Theta\|^2\]In practice, Phase 2 is often hybrid:
\[s_{mvp}(u,i)=\alpha\cdot p_u^\top q_i + (1-\alpha)\cdot s_{content}(u,i)\]This keeps collaborative filtering as the main engine while content-based filtering handles new/sparse items.
Apply time decay in batch features so recent behavior matters more:
\[\tilde{r}_{ui}=\sum_{e\in \mathcal{E}_{ui}}\exp\left(-\lambda(T-t_e)\right)\cdot r_e\]Logistics shopping example (Phase 2) #
- Build matrix from orders, carts, and repeat purchases.
- Nightly ALS/BPR precompute user lists such as: rice -> cooking oil -> detergent.
- Blend with content-based scores for newly onboarded SKUs with no purchase history.
Phase 3: Scalable Architecture (The Industry Standard) #
Scenario: You now have millions of users and millions of items. Nightly full precomputation is too slow and wasteful. You must generate recommendations on request.
Practical solution: Multi-Stage Funnel Architecture #
You cannot score 10M items with a heavy model in 100ms. Use a funnel:
Retrieval -> Ranking -> Re-ranking
- Retrieval (Two-Tower + ANN)
- Fetch top- \(K_r\) candidates fast from a vector index.
- Collaborative signal: interaction-trained user/item towers.
- Content-based signal: metadata/content tower for cold or newly launched items.
- Ranking (DCN v2)
- Apply heavier personalized scoring on the reduced candidate set.
- Two-Tower is optimized for fast recall, while DCN v2 captures richer feature interactions for precise final ordering.
- Re-ranking
- Enforce diversity, freshness, business rules, and safety constraints.
Typical online flow:
\[10^7\ \text{items} \rightarrow 10^3\ \text{candidates} \rightarrow 10^2\ \text{ranked} \rightarrow K\ \text{served}\]How are the deep networks trained? (and sampling) #
For Two-Tower and DCN v2, training data comes from logged impressions and outcomes.
- Build tuples from logs:
- positives \((u,i^+)\) from clicked/purchased impressions
- negatives \((u,i^-)\) from shown-but-not-clicked items
- unseen items are unknown, not negative
- train mostly on exposed set \(E_u\) : \[(u,i)\in E_u \quad \text{with sampled negatives}\]
- Train in mini-batches with mixed negatives:
- hard negatives: exposed but skipped
- in-batch negatives: other positives in the same batch
- random negatives: sampled catalog items for coverage
Two-Tower retrieval loss (sampled softmax):
\[\mathcal{L}_{retr}=-\log\frac{\exp(s(u,i^+)/\tau)}{\exp(s(u,i^+)/\tau)+\sum_{j\in N(u)}\exp(s(u,j)/\tau)}\]DCN v2 ranking loss (pointwise):
\[\mathcal{L}_{rank}=-\sum_n\left[y_n\log\hat{y}_n+(1-y_n)\log(1-\hat{y}_n)\right]\]Because positives are sparse, handle class imbalance with weighted loss:
\[\mathcal{L}_{wbce}=-\sum_n\left[w^+y_n\log\hat{y}_n+w^-(1-y_n)\log(1-\hat{y}_n)\right],\quad w^+>w^-\]Optional focal variant for rare positives:
\[\mathcal{L}_{focal}=-\sum_n\left[y_n(1-\hat{y}_n)^\gamma\log\hat{y}_n+(1-y_n)\hat{y}_n^\gamma\log(1-\hat{y}_n)\right]\]In the logistics case, sample negatives from available items in the same delivery zone so training matches real serving constraints.
Freshness and new nearby items #
When a new restaurant or new product appears nearby, inject it through content/geolocation retrieval and boost freshness in ranking/re-ranking.
\[b_{fresh}(i)=\exp(-\lambda\cdot \text{age}_i),\qquad s'_{rank}(u,i)=s_{rank}(u,i)+\eta b_{fresh}(i)\]Use time decay both in features (Phase 2) and online re-ranking (Phase 3) to avoid stale recommendations.
Evaluation and rollout in Phase 3 #
Use these metrics and release steps before full rollout:
- Offline retrieval: Recall
- Offline ranking: AUC, PR-AUC
- Guardrails: latency, complaint/cancel rate, fairness/diversity.
Why this scales #
- Retrieval remains sublinear with ANN search.
- Ranking compute is focused only where it matters.
- Re-ranking protects user experience and policy constraints.
Logistics shopping example (Phase 3) #
- Retrieval: Two-Tower ANN over millions of catalog items across warehouses.
- Ranking: DCN v2 with stock, ETA, shipping cost and margin features.
- Re-ranking: enforce serviceability, delivery SLA, basket diversity and policy constraints.
Example logistics-aware final score:
\[s_{final}(u,i)=s_{rank}(u,i)+\alpha\cdot \text{availability}_i-\beta\cdot \text{ETA}_i-\gamma\cdot \text{shipping\_cost}_i\]