Skip to main content

← 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-4o writes 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.

Next: Bottom sheet migration →