← Return to homepage

How it works

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 technical version

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.

A real catalogue, without hallucinations

~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.

Semantic retrieval that fits on a small box

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.

LLM query understanding, not LLM recommending

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.

Grounded explanations

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".

Evaluation-driven

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.

Resource-constrained deployment

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.

Tech stack

Frontend
Next.js (React 19)
Backend
Python / FastAPI (request orchestration) + a separate Python / FastAPI recommender service
Vector database
Qdrant (self-hosted, int8-quantized)
Metadata store
PostgreSQL
Embeddings
fastembed / ONNX Runtime (bge-small-en-v1.5)
LLM
OpenAI API (query understanding, descriptor enrichment, and grounded explanations)
Deployment
Vercel (frontend + backend) and a self-hosted Hetzner VPS (recommender, via Docker + nginx)