Restaurant Recommendation Engine
A dual-algorithm recommender that combines sentence-transformer embeddings with LLM-style semantic search — every result ships with a specific, human-readable explanation of why it was surfaced.
Discovery is stuck at keyword search and star ratings.
Yelp and Google Maps tell you what is popular near you. They rarely tell you why a specific place is right for you, and they hide the mechanics of ranking entirely. Two problems compound this: cold-start (a new user has no history, so personalization degrades to averages) and explainability (people don't trust a black box telling them where to eat).
I wanted to see how far I could push a personal recommender that took both problems seriously — one that understood natural language intent, learned from interaction, and defended every recommendation with the actual attributes that drove it.
Two algorithms, not one — because accuracy and discovery pull in opposite directions.
Most recommender demos ship a single algorithm and call it a day. That flattens a real tension: content-based methods are precise when history exists but blind on day one, while semantic retrieval is great from a cold start but drifts away from established taste. I built both, and blended them.
Content-Based Filtering encodes each restaurant with all-MiniLM-L6-v2 into a 384-dimensional vector, then builds a weighted preference vector from a user's liked and rated items. Rankings are cosine similarity, computed as a single batched dot product across all 300 restaurants.
LLM-Semantic Search embeds a natural-language query in the same space — "cozy Italian date night under $40" — and combines dense retrieval with a keyword-map parser that pulls out hard constraints (cuisine, price, dietary, features) and applies calibrated score boosts. Semantic similarity dominates; structured matches re-rank within similar clusters.
A hybrid mode linearly blends both scores with a configurable α, which is the mode that matters most in practice — mid-session, when a user has some history and also wants to steer with a query.
A full evaluation harness, not a notebook.
The deliverable is a five-tab Streamlit app: Discover (recommendations), Browse (filter the full dataset), My List (interaction history), Metrics (live evaluation dashboard), and About. Behind it sits a clean separation between recommender.py (the two algorithms plus explanation generators) and evaluation.py (a simulated-user harness that scores both algorithms across Precision@K, Recall@K, NDCG@K, Intra-List Diversity, and Category Coverage).
Every card carries an explanation that cites real attributes — "Same Italian cuisine as 'La Golden Bistro' which you enjoyed; matching $$ price point; cozy atmosphere; known for Margherita Pizza" — not a generic "based on your preferences." Explanations are template-filled from the actual matched fields, so they're specific and traceable by construction.
Numbers that make the trade-off visible.
Evaluated across 50 simulated user profiles at K=10, the two algorithms produced a clean, defensible split. Content-Based Filtering hit Precision@10 = 0.848, Recall@10 = 0.771, and NDCG@10 = 0.891. LLM-Semantic Search scored lower on all three (0.700 / 0.636 / 0.801) — but nearly doubled category coverage from 11.9% to 19.3% and lifted average recommended rating from 3.92★ to 4.04★.
That's the tension the system is designed around: precision within known taste versus discovery beyond it. The hybrid mode is the honest answer — high accuracy from the content model plus the exploratory reach of semantic retrieval, weighted by an α the user can tune.
What I'd do next.
The biggest thing this project taught me is that the evaluation harness matters more than the algorithm. Anyone can wire up cosine similarity; very few will write the Precision / Recall / NDCG / ILD / Coverage plumbing that lets you actually see whether a change helped. That harness turned a black-box demo into a system I could reason about.
If I picked it back up, I'd add collaborative filtering on top of real interaction logs, replace the rule-based query parser with a fine-tuned NER model so "something bougie" resolves to fine dining, and persist user sessions in a real backend. And I'd A/B test hybrid α values against live users, because simulated ground truth only takes you so far.