Seek A Story is a book recommendation engine. Describe what you're in the mood for — a theme, a feeling, "something like X but angrier" — and it searches a catalogue of about 900,000 real books, then hands back a short, varied shortlist, each pick with its cover and one sentence on why it fits.
The recommendation core is a content-based retrieval system: books are drawn from an actual catalogue, not from a language model's memory of what a book called 1984 is probably about.
~900k English-language books ingested from Open Library's monthly dumps, filtered for a real description and at least one subject, and normalized into Postgres. Every book the app can recommend actually exists, so titles can't be invented.
Books are embedded (bge-small-en-v1.5 via ONNX/fastembed, no PyTorch) into Qdrant with int8 scalar quantization, so ~900k vectors' worth of HNSW index takes up a fraction of a gigabyte of RAM. The catalogue is further enriched with LLM-generated descriptors to sharpen retrieval on vague, mood-based queries that plain jacket-copy embeddings tend to miss.
A query-understanding call turns free text into an embedding-ready description plus structured filters and anchor terms. The model's job stops at interpreting the query; it never gets to name a book itself. Retrieved candidates are then blended via reciprocal rank fusion across the vector and descriptor signals, with an author-exclusion prior and MMR-style diversification so the shortlist isn't five near-identical picks by the same author.
A final batched LLM call writes one sentence per pick, using only the book's matched subjects and the parsed query signals. It doesn't get to invent facts about a book it hasn't "read".
Every ranking and model decision — embedding model, hybrid weighting, reranking — came out of running a ~100-query eval set through an LLM-judge harness and comparing scores, rather than eyeballing a handful of examples and calling it good.
The recommender runs as its own FastAPI microservice on a small shared VPS, self-hosted behind nginx with TLS and a bearer token. The quantization, the single-threaded ONNX runtime, the cgroup memory and CPU caps: all of it is tuned so the service survives alongside other unrelated containers on about 3GB of RAM.
Built and run by one person. More of my work lives at arno.sh.