Sistemas de recomendación
Generar candidatos relevantes para cada usuario, rankearlos rápido y aprender continuamente del feedback.
Recursos
Recommendation, Candidate, FeatureVector, Event, Experiment.
Operaciones
Generar, rankear, registrar feedback, A/B test.
Escala
Millones de items, millones de users, decisiones por segundo.
Riesgo
Cold start, sesgo de exposición, drift, feedback loop tóxico.
Requisitos
Aclara el contrato: qué responde el sistema y con qué garantías.
Funcionales
- Devolver lista rankeada según contexto.
- Logear feedback (impressions, clicks, conversiones).
- Explicabilidad opcional ("porque viste X").
- Asignar variantes A/B sticky por user.
- Filtros: locale, language, content policy.
No funcionales
- p99 < 100 ms en respuesta online.
- Train/serve consistency en features.
- Frescura modelo: re-train diario, hot features < min.
- Escala 100M+ items, 100M+ users.
- Exploración ≥ 5% del tráfico para evitar feedback loop.
Cómo aclarar requisitos en la entrevista
Casos de uso típicos. Home feed personalizado, "you might also like" en página de producto, email weekly picks, search re-ranking, push notifications de items nuevos relevantes, "out-of-stock alternatives" en e-commerce, sidebar de "trending in your network", continue-watching en streaming.
Preguntas que debes hacer:
• ¿Cuántos slots a recomendar? 5 vs 100 cambia el budget de cómputo.
• ¿Personalizado por user o cohort-based / popularidad?
• ¿Online (real-time) o async (precomputed batch)?
• ¿Cold start matters: usuarios nuevos cómo arrancan?
• ¿Métrica de éxito: CTR, conversión, watch time, retención D7/D30?
• ¿Diversidad importa? Si solo optimizas relevancia, eco chamber.
• ¿Explicabilidad obligatoria? Cambia los modelos válidos (interpretables vs deep).
• ¿Latencia: budget online (sub-100ms) o se puede precomputar?
• ¿Cuántos items y users? 1K vs 1B cambia ANN, sharding, etc.
Frase clave: "Asumo home feed online, ~10 slots, sub-100ms p99, 100M items y 100M users, métrica primaria CTR con guardrail de diversidad. Si me dices long-form watch time o conversion, ajusto el ranker."
Estimaciones de capacidad
Resoluciones año/día/seg. Define el budget online del ranker y el tamaño del feature store.
~30B sesiones/año.
~12K/s avg · ~120K/s pico.
~6M ranks/s en peak.
+ ~100 GB user features (1 KB × 100M).
Redis / Bigtable / DynamoDB.
Hard budget no negociable.
Impr + click + conv ratio típico 100:5:1.
~43 s/día permitidos.
Endpoints
| Método | Path | Descripción | Notas |
|---|---|---|---|
| GET | /api/recommendations?userId=&context=home | Lista rankeada para un slot. | Latencia objetivo <100ms. |
| POST | /api/events/impression | Vista del item en pantalla. | Imprescindible para CTR. |
| POST | /api/events/click | Click en una recomendación. | |
| POST | /api/events/conversion | Compra/like/save. | Señal positiva fuerte. |
| GET | /api/recommendations/explain?recId=... | Razón de la recomendación. | Útil para confianza. |
| POST | /api/experiments/assign | Devuelve variante A/B. | Ideal stickiness por user. |
Cómo defiendo estas APIs en la entrevista
Una API simple esconde un sistema complejo. GET /recommendations parece trivial — pero detrás hay candidate generation, feature lookup, ranker, diversity filter, A/B assignment. Mantengo la API estable; los detalles cambian con cada release del modelo.
Eventos separados por tipo. impression, click, conversion en endpoints distintos para que el cliente no envíe payloads enormes. Cada uno con event_id para dedup en el stream.
Explainability como first-class. GET /recommendations/explain?recId=... devuelve la razón ("similar a tu listening history" o "popular en tu cohort"). Esto sube confianza del usuario y ayuda a debuggear el sistema.
Frase clave: "La API es contract estable; el sistema atrás itera con A/B continuo. experiment_id en cada response permite atribuir métricas a la variante correcta."
Ejemplo
GET /api/recommendations?userId=u_44&context=home&limit=10
200 OK
{
"experiment": "ranker_v3.2",
"items": [
{ "id": "i_99", "score": 0.91, "reason": "similar_to_recent" },
{ "id": "i_42", "score": 0.88, "reason": "popular_in_cohort" }
]
}
Arquitectura
experiment routing"}} --> Recall["Candidate Generation
ANN + popular + cohort"] Recall --> Embed Reco --> Rank["Ranker Online
p99 ≈ 100 ms"] Rank --> FS Reco --> Filter["Policy / Diversity Filter"] Reco --> Exp["A/B Assignment
sticky hash"] Client -. impressions / clicks / conv .-> Stream Stream --> FS Stream --> Train["Training Pipeline
offline batch + incremental"] Train --> Rank Train --> Embed subgraph Embed["🧠 Embeddings ANN · HNSW / FAISS"] direction TB v1["item_vectors"] v2["user_vectors"] end subgraph FS["💾 Feature Store · Bigtable / Redis"] direction TB f1["user:id"] f2["item:id"] f3["cohort:id"] end subgraph Stream["📨 Kafka · events"] direction TB k1["impressions"] k2["clicks"] k3["conversions"] end classDef gateway fill:#fbbf24,stroke:#f59e0b,stroke-width:3px,color:#0f172a,font-weight:bold classDef service fill:#4c1d95,stroke:#a78bfa,stroke-width:2px,color:#ede9fe classDef client fill:#1f2937,stroke:#94a3b8,stroke-width:2px,color:#e5e7eb class Reco gateway class Recall,Rank,Filter,Exp,Train service class Client client
Cómo lo explico en la entrevista
Request entra. Cliente abre la home y hace GET /api/recommendations?context=home. El Reco API lee primero la asignación de experimento del user (sticky por hash(userId)), luego dispara dos cosas en paralelo: candidate generation y feature lookup.
Candidate generation (~30 ms). Tres queries: (1) ANN sobre el embedding del user devuelve ~200 items cercanos, (2) popularidad por cohort/región da ~100 items que están funcionando ahora, (3) collaborative filtering agrega ~200 más. Dedup → ~500 candidatos únicos.
Feature lookup paralelo. Bulk MGET al feature store: features del user (last 30 d activity, demographics, hour, device) más features de los 500 items. Esto lleva 20–30 ms con Redis o Bigtable.
Online ranker. El modelo (TF Serving u ONNX) toma los 500 × user_features × item_features y devuelve scores. Aplicamos policy filters (locale, content policy) y diversity filter. Top-K final con razón ("similar a lo que viste"). Total p99 ~100 ms.
Feedback loop. Cliente muestra. Cada item visible emite impression. Click → click. Compra → conversion. El stream alimenta features online en segundos y el training pipeline en batch (cada hora o día) para refrescar el modelo.
Frase clave: two-stage retrieval con feature store unificado. El ranker se entrena offline y se sirve online — esto evita train/serve skew y permite A/B sin caos.
Por qué cada componente
Cuellos de botella
- Feature store hot keys (mismos items leídos por miles de users).
- Ranker GPU/CPU saturado en peak — latencia se dispara.
- ANN index rebuild lento en uploads masivos de items nuevos.
- Cold start users e items sin historial.
- Train/serve skew si las features se computan distinto en cada lado.
Mejoras / cómo escala
- Cache de features por item con TTL corto (decay rápido pero ahorra hot reads).
- Multi-tenant ranker con cuotas por experimento.
- HNSW incremental updates para que items nuevos aparezcan en minutos.
- Bandit con epsilon-decay para cold start (más exploración al inicio).
- Feature Definition compartida (mismo código de transformación en train y serve).
- Modelo ligero en path crítico, modelo pesado para refresh batch.
Por qué estos servicios cloud (vs alternativas)
Por qué: top-K vecinos en embeddings de 100M+ items en sub-10 ms. HNSW soporta inserts incrementales para items nuevos. Recall > 95% con tuning correcto.
vs pgvector: insuficiente a esta escala. vs Elasticsearch dense_vector: aceptable, menos óptimo. vs Pinecone / Weaviate: managed equivalentes pero costosos.
Por qué: bulk MGET de features en <30 ms. Bigtable/Dynamo para volumen + Redis para hot keys. Feast como capa de abstracción para train/serve consistency.
vs Postgres: no escala a 6M req/s. vs DynamoDB sin Redis: aceptable pero hot keys saturan. vs custom in-memory: sin durabilidad ni replicación.
Por qué: stream con dedup por event_id, particionado por user_id, alimenta feature store online y warehouse para training. Múltiples consumers (training, attribution, fraud, BI).
vs Kinesis: AWS-only. vs SQS: point-to-point, no escala a múltiples consumers. vs llamadas síncronas: path crítico se rompe si downstream falla.
Por qué: inference con batching, model versioning, A/B sticky por user, GPU/CPU optimization, warm-up al deploy. Crítico para mantener p99 sub-100 ms.
vs SageMaker (managed): aceptable, más caro y menos custom batching. vs FastAPI con torch.load: sin batching ni warm-up serio, latencia inaceptable.
Por qué: orquestación de pipelines con dependencias (data ingestion → feature gen → train → eval → deploy), schedules, retries, observability del DAG.
vs cron + scripts: sin observability, retries inexistentes, debugging penoso. vs MLflow solo: tracking pero no orquesta el DAG.
Diseño de base de datos
Feature store online, embeddings en ANN store, eventos al stream para entrenamiento.
(type, created_at), idx (locale)idx HNSW para top-K ANN
Same code path en train y serve
event_idSink: feature store + warehouse
Cómo defiendo este diseño de base de datos
Cuatro storages, un solo dato lógico. Items en relacional para joins; embeddings en ANN para retrieval; features en KV low-latency para serving; events en stream para feedback. Cada uno representa una "vista" del mismo data lifecycle.
Train/serve consistency es no-negociable. El feature store online (serving) y el feature store offline (training) deben tener idéntico código de transformación. Si el modelo se entrena con user.click_rate_7d calculado de una manera y serving lo calcula distinto, el modelo miente sobre datos que nunca vió.
Embeddings actualizables. HNSW soporta inserts incrementales; cuando hay un item nuevo, generamos su embedding y lo agregamos sin rebuild completo. Re-train completo es nocturno; updates incrementales son sub-minuto.
Frase clave: "El sistema tiene un feature store que vive en el centro: training lee de él, serving lee de él, eventos lo updatean. Sin esa unificación, hay train/serve skew que destruye la accuracy."
Tipos de sistemas de recomendación
Lo que se pregunta en entrevistas: qué algoritmos existen, cuándo usar cada uno, y cuál proponer hoy.
Content-based filtering
Item features → recomienda items similares al historial del user.
Cómo: TF-IDF / embeddings sobre descripción, tags, atributos. User profile = vector promedio de items que le gustaron. Score = cosine similarity.
Cuándo: catálogo con metadata rica (artículos, productos), users nuevos sin historial colaborativo, cold-start friendly.
Pros: explicable ("porque te gustó X con tag Y"), no necesita historial de otros, items nuevos entran rápido.
Contras: filter bubble — solo te recomienda más de lo mismo. Depende de calidad de features.
Ejemplo: news apps por keyword, "más como este" en producto.
Collaborative filtering · user-based
User-User similarity → te gusta lo que les gustó a usuarios similares.
Cómo: matriz user × item con interacciones; cosine sim entre filas. Recomienda items que vieron tus k vecinos.
Cuándo: catálogos pequeños/medianos, prototipos rápidos. No para producción a escala.
Pros: descubre items lejos del historial, no necesita features de items.
Contras: no escala (k-NN sobre 100M users es brutal), cold start fuerte, sparsity.
Ejemplo: Amazon temprano, GroupLens, MovieLens académico.
Collaborative filtering · item-based
Item-Item similarity → items similares a los que ya usaste.
Cómo: cosine sim entre columnas (items co-vistos por mismos users). Score user×item = sum de similarities con items del historial.
Cuándo: muchos users, items relativamente estables. Workhorse clásico.
Pros: precomputable offline, escala mejor que user-based, explicable.
Contras: cold start de items nuevos, no captura tendencias temporales.
Ejemplo: Amazon "Customers who bought X also bought Y" (Linden et al. 2003).
Matrix Factorization (SVD / ALS)
Latent factors → comprime user-item en embeddings densos.
Cómo: descompone matriz user-item rala en U × Vᵀ con dimensiones latentes (~50–200). Score = U_user · V_item.
Cuándo: ratings explícitos o implícitos, base de muchos sistemas modernos.
Pros: descubre patrones latentes, ALS distribuido escala (Spark MLlib), pre-cálculo de embeddings.
Contras: cold start de users e items, factores poco interpretables.
Ejemplo: ganador del Netflix Prize, Spotify CF temprano.
Two-Tower / Dual Encoder
User tower + Item tower → embeddings que se matchean por dot product.
Cómo: dos NNs separados producen embeddings de user e item. Entrenar para que producto interno alto = relevancia. Indexar items con ANN (HNSW/FAISS/ScaNN).
Cuándo: retrieval de catálogos masivos con sub-10 ms latency. Estándar de oro hoy en producción.
Pros: escala a billones de items, captura semántica, train/serve consistency.
Contras: complejidad de infra, hay que regenerar embeddings de items nuevos.
Ejemplo: YouTube candidate generation, Pinterest, Etsy, Amazon.
Deep Ranking (DLRM / Wide & Deep / DCN)
Modelos densos para rankear pocos candidatos con muchas features.
Cómo: arquitecturas como DLRM (Meta), Wide & Deep (Google), DCN. Combinan features categóricas (embeddings) y dense (cross networks). Output: P(click), P(watch > 30 s), etc.
Cuándo: ranking stage en two-stage system, presupuesto computacional moderado.
Pros: SOTA accuracy, captura cross-features no obvias, multi-objetivo.
Contras: feature engineering caro, training pesado, latencia significativa por candidato.
Ejemplo: Facebook Newsfeed, Google Ads, YouTube ranker.
Sequence-based (BERT4Rec / SASRec / GRU4Rec)
Modela la secuencia de items vistos como un lenguaje.
Cómo: trata historia del user como secuencia. Predice next-item con self-attention (BERT4Rec / SASRec) o RNN (GRU4Rec). Mask de items para entrenar.
Cuándo: orden importa (música, video, e-commerce con sesiones), historiales largos.
Pros: captura patrones temporales y sesiones, SOTA en next-item.
Contras: caro de entrenar (transformers), latencia mayor, necesita historia suficiente.
Ejemplo: Spotify Discover Weekly mix, Netflix continue-watching, Amazon session-based.
Graph Neural Networks (PinSage / GraphSAGE)
Modela usuarios + items + interacciones como un grafo.
Cómo: aprende embeddings propagando info por aristas (followers, likes, co-views). Score por proximidad en el grafo.
Cuándo: dominio inherentemente de grafo (social, e-commerce con co-purchases, content sharing).
Pros: rico en estructura, items nuevos entran rápido vía vecinos, captura relaciones indirectas.
Contras: infra compleja, training pesado, debugging difícil.
Ejemplo: Pinterest PinSage, Uber Eats, LinkedIn People You May Know.
Contextual Bandits / RL
Decide explorar nuevos items vs explotar lo que sabes.
Cómo: cada slot es una decisión con reward. LinUCB, Thompson sampling. Slate optimization para múltiples slots correlacionados. RL con reward delayed (watch time, retention).
Cuándo: cold start estructural, exploración necesaria, métricas largo plazo.
Pros: maneja exploración formalmente, adapta rápido a items nuevos, marcos teóricos sólidos.
Contras: convergencia lenta sin priors, evaluación compleja, off-policy difícil.
Ejemplo: Yahoo News (LinUCB), Microsoft Decision Service, Uber promociones.
Hybrid (combinación)
Mezcla content + CF + signals contextuales.
Cómo: ensemble lineal sobre scores de modelos distintos, o cascada (content para cold start, CF cuando hay historia, deep ranker al final).
Cuándo: producción real. Casi nadie usa un solo modelo.
Pros: maneja cold start, combina fortalezas, robusto a fallas parciales.
Contras: complejidad operativa, debugging del ensemble, tuning de pesos.
Ejemplo: Netflix, Spotify, Amazon, YouTube — todos en producción son híbridos.
Qué proponer en una entrevista hoy
Para sistemas modernos a escala (YouTube, TikTok, Spotify, e-commerce), propongo un pipeline híbrido de dos etapas con bandits:
1. Retrieval (recall, ~500 candidatos). Combino tres fuentes en paralelo:
• Two-tower neural con ANN (HNSW/FAISS) para ~200 items semánticamente similares al user.
• Item-based CF precomputado para ~150 vecinos colaborativos.
• Popularidad por cohort/región para ~100 items que tienden ahora (cold start safety net).
Después dedup → ~500 candidatos en < 30 ms.
2. Ranking (precision, ~10–50). DLRM o Wide & Deep que toma features del user e item del feature store, predice probabilidad de la métrica primaria (CTR, watch time, conversión). Multi-objetivo si hay varios KPIs.
3. Re-ranking final. Diversity filter (no stack del mismo creator/topic), business rules (boost campañas, filter policy/locale), recent_seen dedup.
4. Cold start & exploration. Contextual bandit con epsilon-greedy ~5–10% del tráfico para items nuevos. Fallback a popularity para users sin historial.
5. Sequence model opcional. Si la entrevista es Spotify / Netflix continue-watching, agrego SASRec / BERT4Rec en candidate generation para capturar patrones temporales del user.
Por qué esta combinación: two-tower escala a billones de items con sub-10 ms; DLRM da SOTA en ranking; bandits resuelven cold start formalmente; popularidad y re-ranking son guardrails baratos. Es exactamente lo que YouTube, TikTok y Pinterest tienen en producción hoy.
Decisiones técnicas
| Tema | Decisión | Por qué |
|---|---|---|
| Two-stage | Recall (mucha cantidad) + ranking (pocos, finos). | Costo y latencia, patrón estándar. |
| Embeddings | ANN (HNSW, FAISS, ScaNN) para recall. | Subsegundo aún con millones de items. |
| Features | Online + offline en feature store unificado. | Train/serve consistency. |
| Modelo | Entrenado offline, servido online. | Estabilidad de latencia, A/B sano. |
| Eventos | Stream con dedup y latencia < segundos. | Aprende rápido del usuario. |
| Experimentos | Asignación stickyness por hash del userId. | Resultados consistentes en sesión. |
Cómo defiendo estas decisiones técnicas
Constraint principal: p99 < 100 ms con 6M req/s en peak. Esto fuerza two-stage retrieval (no puedes rankear millones), modelo offline (no entrenas en path crítico), feature store low-latency (no queries SQL), y model serving optimizado (no flask + torch.load).
Métricas que validan. Online: CTR, conversion, watch time. Offline: nDCG, recall@K. Operacional: p99, feature staleness, model staleness, A/B sample size. Guardrail: diversity, exploration rate, feature lookup error rate.
Cuándo cambio de opinión. Latencia presupuesto sube a 500 ms → modelo más complejo (sequence transformers, graph attention). Items super dinámicos → embeddings actualizables vs reindex completo. Cold start crítico → bandit prominent en el blend.
Frase clave: "Cada decisión es trade entre latencia, accuracy, freshness, complejidad operacional. El sistema mejor no es el con mejor modelo — es el que da el mejor número en producción con el budget que tienes."
Pitfalls
Si las features online no matchean con las del entrenamiento, el modelo miente.
Usuarios nuevos sin historial. Usa popularidad, geografía, onboarding.
El modelo se reinforza a sí mismo. Reserva tráfico para exploración.
Script de respuesta: “Diseño un pipeline de dos etapas: recall (con ANN sobre embeddings) y ranking (modelo offline servido online). Las features viven en un feature store compartido entre training y serving para evitar skew. Los eventos van por stream y alimentan el ciclo de re-entrenamiento. Manejo experimentos con asignación sticky por user, mido CTR, conversion, diversidad y exploro un porcentaje del tráfico.”
Tiers de almacenamiento
Feature store online es caro per-byte; events viejos pueden vivir muy frío sin perder valor.
Cómo defenderlo en la entrevista
BigQuery long-term automático. Particiones de tabla en BigQuery sin acceso por 90 días bajan automáticamente a long-term storage (50% off). No requires lifecycle manual — el warehouse lo hace solo. Hot queries siguen funcionando sin cambio.
Modelos viejos como artifacts. Cada modelo entrenado se versiona en S3 con tag model_version. Mantengo los últimos 2 en Standard (rollback rápido); > 6 meses van a IA. Si un modelo de hace 2 años falla auditoría, puedo retrievear (12 h) para investigar.
Frase clave: "Online serving está en RAM/Bigtable porque sub-30ms es no-negociable. Training data en S3 / BigQuery con tiering automático: hot recents, cold > 30 d, frozen para audit. Esto reduce el costo de storage del pipeline ML ~5×."
Versión alternativa: con Load Balancer (no API Gateway)
Para reco serving la latencia es product feature. LB es lo natural; Gateway no aporta valor.
region routing"}} --> Pod["Reco API Pods
+ JWT verifier (local)
+ Rate limit (token bucket)"] Pod --> Recall["Candidate Generation"] Recall --> Embed Pod --> Rank["Ranker Online"] Rank --> FS Pod --> Filter["Policy / Diversity Filter"] Pod --> Exp["A/B Assignment"] Client -. impressions/clicks/conv .-> Stream Stream --> FS Stream --> Train["Training Pipeline"] Train --> Rank Train --> Embed subgraph Embed["🧠 Embeddings ANN"] direction TB v1["item_vectors"] v2["user_vectors"] end subgraph FS["💾 Feature Store"] direction TB f1["user:id"] f2["item:id"] end subgraph Stream["📨 Kafka events"] direction TB k1["impressions"] k2["clicks"] k3["conversions"] end classDef lb fill:#34d399,stroke:#10b981,stroke-width:3px,color:#0f172a,font-weight:bold classDef service fill:#4c1d95,stroke:#a78bfa,stroke-width:2px,color:#ede9fe classDef client fill:#1f2937,stroke:#94a3b8,stroke-width:2px,color:#e5e7eb class LB lb class Pod,Recall,Rank,Filter,Exp,Train service class Client client
API Gateway vs Load Balancer: cómo defender cada uno
Reco serving es interno. El cliente típicamente es tu propio app o frontend, no un third-party. Auth con JWT verificado local en pod, rate limit con Redis. Gateway agrega latencia que el SLO p99 < 100 ms no aguanta.
Frase clave: "Reco serving usa LB porque es interno, latency-sensitive y high-QPS. Si expongo recos como producto (e.g., Reco-as-a-Service para terceros), agrego Gateway en el path externo manteniendo el LB para internal."
Cuándo elegir API Gateway
- Reco-as-a-Service ofrecido a clientes externos.
- Multi-tenant con quotas distintas por cliente.
- API monetization con plans por requests/mes.
- Auth via OAuth con scopes granulares.
Cuándo elegir Load Balancer
- Reco interno para tu propio frontend (caso típico).
- SLO p99 < 100 ms — gateway agrega latencia inaceptable.
- High QPS (12K+/s) — costo gateway prohibitivo.
- Eventos ingest: 10K/s, fire-and-forget, no necesita gateway features.
- Service mesh con sidecar para mTLS y observability.