← Back to work
2025 · Machine Learning · Personal Project

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.

2025 ML Engineer Solo build
300
Restaurants · 20 cuisines
2 + 1
Algorithms + hybrid blend
5
Evaluation metrics tracked
5
Tab interactive UI
The problem

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.

Approach

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.

What I built

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.

Recommendation flow "cozy italian date night under $40" La Golden Bistro italian · $$ · cozy · downtown 94% Trattoria Bellini italian · $$ · romantic · midtown 89% Osteria Verde italian · $$$ · intimate · uptown 82% Casa Marino italian · $ · casual · riverside 76% Il Piccolo Forno mediterranean · $$ · cozy · arts district 71%
Natural-language query → embedded in shared semantic space → ranked with attribute boosts → each match ships with a cited explanation.
PythonStreamlit sentence-transformersscikit-learn pandasevaluation harness
Outcome

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.

A single algorithm is a demo. Two complementary algorithms plus a harness that lets you see the trade-off is a system.
Reflection

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.

See it in motion

The full app runs on Streamlit.

Browse the dataset, type a natural-language query, rate a few restaurants to build a taste profile, and open the Metrics tab to watch both algorithms benchmark themselves in real time.