DEV Community

Cover image for I built a recommender that asks *why* you loved something — here's what broke 🍿 Pt. 1
Pushpendra Yadav
Pushpendra Yadav

Posted on

I built a recommender that asks *why* you loved something — here's what broke 🍿 Pt. 1

Every recommendation engine I've used asks the wrong question. "Because you watched Interstellar" gets you more space epics. But maybe the space was never the point. Maybe it was the loneliness, and an ending that hit your heart instead of your brain.

So I built Because I liked: you describe something you loved and why, in your own words, and it finds films, shows, books and games that match the why. Every pick explains how it connects to what you said.

Type "a lonely, beautiful sci-fi where the twist is emotional, not clever" and you get Moon, Her, Contact, The Midnight Sky, and Finch, a gentle film about a dying man building a robot to look after his dog. No space operas, no heist-in-orbit thrillers. It matched the feeling, not the genre shelf.

It's a solo project. This post covers how it works, and the parts that broke in instructive ways.

🧱 The stack

  • Next.js 16 (App Router) on Vercel
  • Supabase: Postgres + pgvector for similarity search
  • OpenAI text-embedding-3-small for vectors
  • Claude Haiku for reading queries, describing titles and writing explanations
  • About 12,000 titles from TMDB (films, TV), IGDB (games) and Open Library (books)

🧠 How a search works

  1. Read the request. An LLM splits what you typed into the part that describes a feeling and the parts that are constraints.
  2. Embed the feeling and find the nearest titles in pgvector.
  3. Apply the constraints the data can actually check.
  4. Explain the top picks against your own words.

Step 1 exists because of the first big lesson.

💥 Lesson 1: embeddings encode topic, not instruction

Early on, "feel-good beach-town shows, dubbed in Hindi" returned… Indian TV. Not beach towns. The words "Hindi" and "India" dominated the vector, and the thing the person actually wanted (sun, sea, warmth) ranked below 300.

An embedding doesn't know "dubbed in Hindi" is a filter. It just sees a topic. So now a model reads the request first:

// what gets embedded vs what becomes a filter
{
  texture: "warm, sunny, low-stakes shows set in a beach town",
  language: { code: "hi", intent: "audio" },
  medium: "tv",
}
Enter fullscreen mode Exit fullscreen mode

Only texture goes into the vector. Language, age suitability, availability and named people become filters or boosts, each checked against real data. And when we can't check something (dubs on a specific streaming service, say), the app says so instead of pretending.

Dubs are the honest gap. No catalog I can access says which shows are dubbed in Hindi on which service, so the app doesn't pretend. It says it can't check dubs yet, and offers titles originally made in Hindi instead, as a clearly labelled alternative rather than a quiet swap.

🎭 Lesson 2: names in a vector pull in look-alikes

Someone asked for comedies "which also star Kevin James" and got comedies… without Kevin James. The name was in the vector, and the vector found films that felt similar.

The fix was the same pattern again: pull the person out of the query, look them up in the stored cast and crew credits, and put every title crediting them first, ranked by how well each one fits the why. Searching "directed by David Fincher" now leads with all eleven Fincher films we have.

⚡ Lesson 3: my title pages took 10 seconds (it was geography)

Each title has its own page, which is the bulk of the SEO surface. They were taking 1–10 seconds. Three causes, three fixes:

  1. Distance. My Vercel functions ran in Washington DC (the default) while the database sits in Mumbai. Every query crossed the planet, about five times per page. Pinning functions to Mumbai with a one-line vercel.json cut the base cost from about 1.2 s to about 50 ms.
  2. Live vector search on every view. "More like this" ran a similarity search per page view. I precomputed the 12 nearest neighbours for every title into a table instead, which turned it into one indexed read.
  3. Per-visitor rendering. The page checked who was signed in, so it couldn't be cached. I moved the signed-in bits to the client, and now the page is cached at the edge.

Result: about 0.1 s for a cached title page, down from up to 10 s.

👀 Lesson 4: your first real user will find your blind spot

I launched quietly and watched (anonymous, cookieless) analytics. One of the first real visitors typed something like:

"a quiet film where ordinary rooms, window light and careful framing make everyday life feel interesting. Gentle mood, little violence."

That's a beautiful, very film person request. Two of the results were serial-killer thrillers. The visitor refined once and left.

The catalog had Paterson and Perfect Days, close to perfect answers, and neither surfaced. Why? My per-title descriptions (written by an LLM, then embedded) talked about story and character arcs, and mentioned the look only in passing. People don't search for plots. They mood-watch. They ask for how something looks, sounds and feels to sit with.

So I rewrote the description prompt to lead with the experience. Here's Paterson, before and after:

Before: "…The look is spare and patient…"

After: "Soft, unhurried daylight through kitchen windows and bus glass, with static camera setups that let ordinary rooms and street corners sit in frame… Intensity is very low: no violence, no gore."

Every title now states its aesthetic (light, palette, framing, sound), its vibe and the occasions it suits ("a rainy Sunday"), and its intensity in plain words, so "little violence" finally has something to match against.

Re-describing all 12,000 titles through Anthropic's Batch API cost about $2.30. A quality gate rejected descriptions that slipped into the first person ("a film I know well…") and retried them.

The same search now leads with Paterson, followed by quiet, carefully framed films like The Quiet Girl, Close and Umberto D., and the three thrillers are gone. One real visitor, one honest miss, one catalog-wide fix.

🔍 What's next: making "golden hour" findable

A single paragraph per title averages detail away. Ask for "a film where most scenes are shot in golden hour" and that detail is maybe 2% of a title's description, and 2% barely moves a vector. Next up:

  • Real keywords from TMDB and IGDB ("small town", "rain", "road trip"): facts the model can't be trusted to remember
  • Structured facets (visual motifs, intensity scores, runtime), so some asks become filters instead of hopes
  • Hybrid search: vectors for the vibe, facets for the specifics

💸 What it costs to run

  • Picks for one search: about $0.002 in LLM usage
  • Re-describing the whole catalog: about $2.30
  • Embedding it: about $0.10

Cheap enough to iterate on every time a real query exposes a gap, which is the whole game.

🙏 Try it, break it

It's free, with no sign-up needed to search, and no ads. becauseiliked.com

Describe something you genuinely loved and why, then tell me in the comments whether the picks got the why or just the genre. The misses are the most useful thing you can send me.

Top comments (0)