我把 becauseiliked.com 的搜尋弄掛了:一次故障復盤
I took down my own search 🔥
Pushpendra Yadav 回顧自己維護的 becauseiliked.com 搜尋故障,指出重算 14,000 個標題相似項目的背景工作令共用資料庫負載過高,導致搜尋請求逾時。
Because I liked asks what you loved and why, then finds films, shows, books and games that match the why. Part 1 was how it works; part 2 was measuring it.
This one is shorter on cleverness and longer on mistakes. In one week I broke search for real users, found the cause was an experiment I'd forgotten was still running, taught it that "animated" doesn't always mean cartoon, and added 1,810 films for 38 cents.
🔥 Lesson 1: background jobs share your database with your users
Every title page shows "More like this": its 12 nearest neighbours, precomputed into a table so a page view is one indexed read instead of a vector search. After adding a batch of films, I started a job to recompute those lists for all 14,000 titles. Six titles at a time, each one a vector search.
Then I tried the site:
Search is having trouble right now. Please try again.
The vector search behind every query was hitting Postgres's statement timeout; with retries, a search took 26 seconds to fail. The database is small, and my batch job was hammering it with the same kind of query users were waiting on. Stopping the job brought search back.
Two things went wrong at once, and the second one hid behind the first.
The job could stall forever. Before the outage, the job had already gone quiet: the process was alive, but nothing had been written for six minutes. One request to the database had never come back, and nothing told it to give up. Every call now gets a deadline:
// a hung request otherwise stalls the whole run
return await Promise.race([fn(), timeout(30_000)])
It couldn't resume. A full recompute rewrote everything, so killing it meant starting over. It now takes a --before <time> flag and skips titles already done since then, plus --concurrency so it can run gently. The rest runs overnight, when nobody's searching.
👻 Lesson 2: your experiment is in the same index as production
Even with the job stopped, "films only" searches took over 5 seconds. Unfiltered ones took 140 ms. The difference was in the table.
In part 2 I described giving every film and show a second vector, describing only its lead character, to fix searches like "the main character is a sarcastic stoic". It didn't pass the test set, so I parked it. The vectors stayed:
Vectors in item_embeddings
|
Count |
|---|---|
Search (v3) |
13,826 |
Parked experiment (lead-v1) |
7,971 |
One HNSW index covered both. A filtered search walks the graph until it has collected enough rows that pass every filter (right version, right medium), and over a third of what it walked past could never match. Books, about 6% of the catalog, were worse.
The fix was to delete the parked vectors. The lines themselves are still stored on each title, so re-embedding them later costs about a cent. When the experiment comes back, its vectors go in a table and index of their own. Filtered searches dropped to 0.9–3.8 seconds cold, and about 0.3 seconds warm.
The lesson is boring and I keep relearning it: an experiment is only parked once it can't touch the production path. Keeping its data in the shared table wasn't parking it.
🎨 Lesson 3: "an animated girl" isn't a cartoon
Search for "an animated film that makes adults cry" and the results included Close, Wonder and Bridge to Terabithia. All good films, all live-action. "Animated" was just one more word in the mood being matched, and the mood ("makes adults cry") is what matters most.
The catalog already knows: TMDB tags films and shows with an Animation genre. So the model that reads each search got a new field, format, which search turns into a hard filter.
Then the obvious problem. My first prompt's rule was "animated → animated films only". I tested it on six phrasings and it got three wrong:
| Search | First prompt | Right answer |
|---|---|---|
| An animated movie that can make a grown man cry | animated | animated |
| A chirpy young animated girl living her life to the fullest | animated | lively, no filter |
| A film about an animated, talkative old man | animated | lively, no filter |
| I loved Up because the old man slowly opens up | animated | no filter: describing a loved film, not asking |
The fix was to say what the word is attached to: "animated" describing the work (film, series, anime, cartoon) is a format; describing a person means lively and stays part of the mood. The tightened prompt got all nine phrasings in the next round right, including "more animated films like Up" (yes) and "a cartoonish, over-the-top comedy" (no: that's a style).
When the filter is off, the word still shapes the search, so "a chirpy animated girl" still leans animated without shutting out live-action. When the meaning is unclear, that's the right default.
🦈 Lesson 4: your catalog is biased toward whatever you seeded it from
I've been answering recommendation threads on Reddit, and one asked for lighthearted horror with a retro vibe, like Fright Night and The Lost Boys. The crowd's answers were good. Then I checked them against my catalog: 9 of the 16 suggestions I checked weren't in it, including the top-voted one, The Frighteners.
The catalog was seeded from TMDB's "popular" lists, which skew to what's popular now. So I measured the gap against TMDB's full list, sorted by vote count:
| TMDB films with… | Missing from my catalog |
|---|---|
| 2,000+ votes | 2 of 2,837 |
| 1,000+ votes | 1,831 of 5,021 |
Nearly every big film was there; over a third of the next tier wasn't. That's where cult and retro films live: The Frighteners, Slither, Arachnophobia, Trick 'r Treat, Bonnie and Clyde.
I added 24 first and read their descriptions before running the rest, the habit from part 2. The full run, through Anthropic's Batch API:
| Step | Cost |
|---|---|
| Seed 1,810 films (TMDB) | free |
| Describe them | $0.32 |
| Embed them | $0.009 |
| Lead-character lines | $0.05 |
| Total | ≈ $0.38 |
32 films still fail the description quality check, mostly by slipping into "a film I know well…". They stay out of search rather than in with a bad description.
🖼️ Lesson 5: small things that make a page feel finished
Colour before the image. Grids of grey loading tiles look broken on a slow connection, which is most connections in India. Each poster and still now carries its average colour, worked out by shrinking a thumbnail to a single pixel:
const { data } = await sharp(thumbnail).resize(1, 1, { fit: 'fill' }).raw().toBuffer({ resolveWithObject: true })
// → '#81b1d2' for Up's sky
That ran across 13,850 titles for nothing but bandwidth. Then I found my own bug: a poster that took over 2.5 seconds swapped its colour for a designed fallback cover, so on exactly the slow connections this was for, people saw the fallback. Now the colour stays until the image arrives, and the fallback is only for images that fail.
Not looking like a film database. A visitor clicked a poster on the home page and left two seconds later. People clicked "12,000+ titles, browse it all" and took the product for a film database. That link is gone. So is "Browse" in the footer, and "← The library" on title pages is now "← Back to search". The product is the question, why did you love it?, so the page now leads with the question.
🔎 What about Google?
Part 2 promised the answer to whether the "why each pick fits" text on title pages gets them indexed. Honest update: too early to tell. Search Console still shows 2 pages indexed and 8 not, and that report runs days behind. A brand-new domain gets crawled cautiously until other sites link to it. I'll report when there's data, not before.
🙏 Try it, break it
Free, no sign-up to search, no ads: becauseiliked.com
Try "an animated movie that can make a grown man cry", then "a chirpy animated girl living her life to the fullest", and tell me if it read them right. And if a film you love is missing, put it in the comments: the catalog has a cult-classic gap, and you're better at finding it than any charts or trends.
來源:DEV Community · dev.to