Individual project for the Recommender Systems course (Esade, Prof. Marc Torrens). A progressive, agile build of a movie-recommender prototype on the MovieLens Latest Small dataset — multiple algorithms behind one clean architecture, with a polished React UI.
Next.js (React + TS + Tailwind) → FastAPI REST → Python recommender core
backend/src/— the engine; every algorithm implements oneRecommenderinterface (fit/recommend).backend/api/— FastAPI exposing every model over REST (/api/home,/api/recommend,/api/similar,/api/metrics, …).frontend/— Next.js UI ("CineMatch", Netflix-style redesign with a cinematic launch intro, Anton/Archivo/Hanken type and true Netflix red):/"Who's watching?" landing with generated line-art viewer avatars;/u/[id]home — hero, Tonight's Arc, rails, grounded "why this" hover cards, the Safe↔Bold discovery slider, a Viewer-DNA auto-profile (Explorer/Comfort, derived from the viewer's own ratings), and a model selector that pins any algorithm to the Top-picks rail;/u/[id]/m/[movieId]movie detail (trailer, clickable cast/director, "For you" + "More like this" rails);/u/[id]/person/[name]person spotlight ·/u/[id]/mapthe Taste Map constellation ·/u/[id]/chatthe conversational AI guide ·/usersall viewers ·/evaluationthe metrics lab (click a model row to drive it live).
The design is registry-driven: add a model in src/, register it in api/registry.py, and it
appears automatically in the API, the evaluation harness, and the UI — no other changes needed.
backend/
├── src/ # the recommender CORE (models · data · evaluation · features)
│ ├── base.py # shared Recommender interface (fit / recommend)
│ ├── data_loading.py # load · validate · EDA stats · train/test split
│ ├── baselines.py · collaborative_filtering.py · content_based.py
│ ├── matrix_factorization.py · learning_to_rank.py · reranking.py # the models
│ ├── evaluation.py # OFFLINE EVALUATION (P@K · NDCG · diversity · novelty · …)
│ ├── insider.py · explain.py # feature engineering + grounded explanations
│ └── tmdb.py · gemini.py # external enrichment (metadata) + LLM intent parsing
├── main.py # MODEL TRAINING + EVALUATION pipeline -> results/metrics.csv
├── api/ # APP LOGIC — FastAPI serving the core over REST
└── notebooks/eda.py # exploratory data analysis -> results/figures/
frontend/ # APP LOGIC — Next.js UI (decoupled from the core)
- Model training → the model classes in
src/*.py, orchestrated bymain.py. - Offline evaluation →
src/evaluation.py, run viamain.py. - App logic →
api/(REST) +frontend/(UI), fully decoupled from the recommender core.
| Family | Models |
|---|---|
| Non-personalised | Most Popular · Highest Average · Bayesian Average · Random |
| Collaborative filtering | Item-Item CF · User-User CF |
| Content-based | TF-IDF over genres + tags + TMDB (overview/keywords/cast) · similar_items |
| Matrix factorization | Truncated-SVD latent factors |
| Hybrid (Learning-to-Rank) | LightGBM LambdaRank over all generators + insider studio-strategy features · MMR re-ranker (diversity/novelty/trust) |
| Conversational (LLM) | Gemini parses a free-text vibe → structured filters → our recommender ranks → grounded reply |
| Model | P@10 | NDCG@10 | Coverage |
|---|---|---|---|
| ltr_hybrid | 0.172 | 0.219 | 0.051 |
| user_user_cf | 0.168 | 0.217 | 0.030 |
| ltr_reranked | 0.164 | 0.209 | 0.059 |
| item_item_cf | 0.137 | 0.177 | 0.148 |
| most_popular | 0.128 | 0.158 | 0.006 |
| bayesian_avg | 0.079 | 0.096 | 0.004 |
| content_based | 0.043 | 0.065 | 0.179 |
| random | 0.001 | 0.002 | 0.490 |
The spread illustrates the project's thesis — accuracy is not everything: the LTR hybrid leads on accuracy, but content-based / random reach the long tail (coverage), and the re-ranker trades a little precision for the best diversity & serendipity.
| Model | Purpose | Key limitation |
|---|---|---|
| Most Popular | Cold-start floor; needs zero personal history | One list for everyone; severe popularity bias |
| Highest Average | Surfaces top-rated films | Hard min-vote cutoff discards the long tail |
| Bayesian Average | Shrinks ratings toward the global mean by vote count | Still non-personalised |
| Random | Honest lower bound + maximal coverage | ≈ 0 relevance |
| Item-Item CF | "More like the films you rated" | Fragile on 98.3% sparsity (cold items) |
| User-User CF | "What similar viewers loved" | Strong accuracy, narrow coverage; O(users²) |
| Content-based | Reaches cold items via genres + TMDB text | Coarse features → low precision; no collaborative signal |
| Matrix Factorization | Latent factors generalise through sparsity | Vanilla SVD; cold users need a fallback |
| LTR Hybrid | Learns to combine every generator (LambdaRank) | Generators see only 75% of train (for leakage-free labels) |
| Re-ranker | Injects diversity / novelty / trust (greedy MMR) | Deliberately trades a little precision |
| AI Guide (Gemini) | Natural language → intent; conversational steering | LLM only parses & explains — never ranks; needs API quota |
Backend
cd backend
pip install -r requirements.txt
py main.py # offline evaluation -> results/metrics.csv
py -m uvicorn api.main:app --port 8000 # REST API (http://127.0.0.1:8000/docs)Frontend
cd frontend
npm install
npm run dev # http://localhost:3000Open http://localhost:3000 → "Who's watching?" → pick a viewer.
Optional — external enrichment (the app runs without these and falls back gracefully):
create backend/.env with TMDB_API_KEY=… (posters + metadata) and GEMINI_API_KEY=… (the AI guide).
The frontend (Next.js) deploys cleanly to Vercel. The backend (FastAPI) loads every model into memory at startup (~1–2 min), so host it on an always-on Python service (Render / Railway / Fly.io) rather than serverless functions.
Frontend → Vercel
- vercel.com → New Project → import
jadzoghaib/Movie-RecSys. - Set Root Directory =
frontend(Next.js preset is auto-detected). - Add env var
NEXT_PUBLIC_API_BASE= your deployed backend URL (e.g.https://cinematch-api.onrender.com). Without it the client falls back tohttp://127.0.0.1:8000. - Deploy.
Backend → Render (example)
- New Web Service from the repo, Root Directory =
backend. - Build:
pip install -r requirements.txt· Start:uvicorn api.main:app --host 0.0.0.0 --port $PORT. - Env vars:
TMDB_API_KEY,GEMINI_API_KEY(optional), andFRONTEND_ORIGIN= your Vercel URL (added to CORS; comma-separate multiple). - Needs enough RAM to hold the fitted models; the bundled
tmdb_cache.jsonkeeps it offline-capable for posters.
MovieLens Latest Small (GroupLens), under backend/data/raw/.
Cite: F. M. Harper & J. A. Konstan (2015), The MovieLens Datasets: History and Context, ACM TiiS.
Movie posters, overviews, keywords and cast are enriched via TMDB. This product uses the TMDB API but is not endorsed or certified by TMDB.
Built sprint by sprint with Agile methodology — see WORKPLAN.md for the backlog,
sprint plan, and design decisions.