← WorkSIDE PROJECT · AI
Beerolog — a taste-based recommender
A self-directed AI project: learn a drinker's taste from a 30-second non-beer quiz, then recommend beers with embeddings + pgvector retrieval, a tunable two-stage match engine, and an evaluation harness that gates quality in CI.
Context
Beerolog helps someone learn their taste and get beer recommendations that sharpen over time. It’s a full product I built and run on my own — TanStack Start (React) on the web, a Python FastAPI backend, Neon Postgres, and OpenAI, deployed on Vercel. Live at beerolog.com.
I treated it as an AI-engineering project rather than a demo: the interesting problems were retrieval quality, cross-lingual matching, explainable ranking, and being able to prove the recommender actually works.
Problem
- Cold start without beer jargon. Most people can’t describe beer in beer terms. Onboarding asks about coffee, sparkling water, snacks, and sour / smoky / citrus food instead — never about beer styles.
- Cross-lingual catalog. Catalog and queries mix Hebrew and English; matching has to work across both.
- Explainability. A ranked list isn’t enough — each pick needs a reason a human trusts.
- Proof. “It feels good” is not a quality bar. I wanted measurable retrieval quality.
Architecture
Onboarding answers compose into a synthetic preference text that is embedded (OpenAI text-embedding-3-small, 1536-D) into a persisted BaselineTaste vector. The beer catalog is embedded the same way. Both live in Neon Postgres via pgvector.
Matching runs in two stages, both tunable:
score = α · cos(baseline, beer) + (1−α) · cos(session, beer) # stage 1: retrieval score += β · (noveltyAffinity − 0.5) · beer.adventurousness # stage 2: novelty re-rank
- Stage 1 — retrieval: an α-weighted cosine blend of the durable BaselineTaste vector and an ephemeral SessionIntent vector (“what I want right now”), so the same person gets different picks on different nights.
- Stage 2 — novelty re-rank: nudges results toward or away from unusual beers based on how adventurous the user is.
- Learning loop: the per-user embedding evolves from 1–5 star ratings and refreshes on a staleness window, so the profile improves as the user drinks.
- Explanations:
gpt-4owrites a one-line “why this beer,” grounded in which contributor (baseline, session, or novelty) drove the score.
Because Hebrew and English land in the same embedding space, a Hebrew query retrieves against an English-keyed catalog without translation.
Evaluation
The part I’m most deliberate about — a validation harness gates the matcher:
- Persona relevance: hand-authored personas run end-to-end through the real composers and match engine, reporting P@5 and MRR; the aggregate must clear a P@5 ≥ 0.4 floor or CI fails.
- Cross-lingual probe: Hebrew queries must hit ≥ 70% of the English baseline against the same catalog.
- Semantic-signal probe: confirms hop names carry real semantic signal versus a random-pair baseline.
- Parameter sweeps: α and β are swept from the CLI, including a β = 0 null comparison, so tuning is measured rather than guessed.
Stack
TanStack Start (React), FastAPI (Python), Drizzle ORM + Neon Postgres (pgvector), OpenAI (text-embedding-3-small, gpt-4o), Clerk, Vercel.
Where it stands
Live and evolving at beerolog.com. It’s my hands-on track into AI engineering — retrieval, evals, and LLM integration — built on top of production full-stack work.