Audiobooks are a problem of too much choice. Audible’s catalogue runs into the tens of thousands of titles across self-help, business, fiction, fantasy, technology, history, and several more genres I have never personally explored. Searching manually is a worse experience than picking by hand at a bookstore. So when I wanted to learn how recommendation engines actually work, audiobooks were the test domain. The codebase was small, the data was public, and the deployment story was something I could ship on a Linux VPS in an afternoon.
This is the build log: an end-to-end hybrid recommender for audiobooks, written in Python (the popular general-purpose programming language), deployed with Streamlit (a Python framework that turns scripts into interactive web apps). The interesting parts are the parts that are not obvious from the API docs. TF-IDF (Term Frequency-Inverse Document Frequency, a way to turn text into weighted numbers that emphasize rare, meaningful words) does most of the semantic heavy lifting. KMeans clustering (an unsupervised algorithm that groups items into k similar buckets) gives you the discovery back. The hybrid logic is where the actual quality lives, not in any single model.
What I actually built
The goal was deliberately narrow: a user pastes in a book title, and the app returns a short list of similar audiobooks. No login, no history, no accounts. The session is stateless. The user picks a model flavor (content, cluster, or hybrid) and gets five recommendations.
Three libraries carried the entire build: Pandas for tabular manipulation, Scikit-learn for the vector math, and Streamlit for the UI. Everything else was stock Python and a CSV (comma-separated values file) reader. I deliberately stayed off heavy frameworks. PyTorch and TensorFlow would have been overkill for a system where the inference is a single cosine similarity (a measure of how close two vectors point in the same direction, scored from 0 to 1) computation against a few thousand rows.
Dataset choice mattered more than I expected. Audible publishes two CSVs: a basic one with name, author, genre, rating, review count, price, and runtime, and an “advanced features” version with description text and narrator metadata. The advanced file is what makes the recommendation quality work. Without the description text, you are stuck on genre bucketing, which collapses every fantasy novel into the same cluster. With it, you get the semantic difference between “high fantasy” and “urban fantasy” the moment TF-IDF weights the right words.
The four pieces in order
The build breaks into four sequential pieces. Each one is small enough to write in a single sitting, and each one has its own failure mode.
First is data cleaning. Real CSVs from public datasets are messy. Mine had missing ratings, duplicate titles with inconsistent author formatting, genre labels like “Self Help” and “Self-Help” that needed merging, and runtime values stored as “12 hrs 30 mins” strings rather than minutes. Pandas handled all of it in about thirty lines of preprocessing. The general pattern is: numeric columns get median imputation (filling missing values with the column’s middle value), categorical columns get “Unknown” as a placeholder, and text fields get lowercased and stripped of punctuation. None of this is novel, but skipping any of it produces visibly worse recommendations downstream.
Second is feature extraction. Book descriptions are paragraphs of natural language, and machine learning models need numbers. TF-IDF is the standard conversion. The intuition is simple: a word that appears in every book description (“the”, “and”, “story”) gets a low weight, while a word that appears in only a few descriptions (“neuroscience”, “dragon”, “entrepreneurship”) gets a high weight. After TF-IDF, every book is a long sparse vector (an array where most values are zero) of word-importance scores. The vectors become the input to the next step.
Third is clustering. With several thousand book vectors in a high-dimensional space, KMeans partitions them into thematic groups. Choosing k (the number of clusters) is the part people skip and get wrong. The elbow method (plotting the model’s distortion as k increases and looking for the bend where adding more clusters stops helping) is the standard answer. For an audiobook catalogue with broad genre spread, k in the 12-20 range worked well. Below that, fantasy and sci-fi collapsed together. Above that, sub-genres became their own clusters and the recommendations became too narrow. A useful sanity check is to pick a book from a popular genre and verify the top recommendations include books you would actually expect to see there.
Fourth is the recommendation function itself, which has three flavors. Content-based picks the top N books by cosine similarity to the input vector. Cluster-based picks the top-rated books in the same cluster. Hybrid is the simple combination: a few from cosine similarity, then a few more from the cluster, deduplicated.
What surprised me
Three observations from the build, in order of how much they shifted my approach:
- The content-based approach is the most precise and the most boring recommendation flavor. It recommends books the user already knows exist.
- Cluster-based is the broadest possible answer and gives discovery without relevance.
- The hybrid logic is what fixed both: top few from content similarity, fill the rest from the cluster.
- Skipping the cleanup step is the single most common reason a quick recommender build looks worse than it should.
The first surprise was that pure content similarity is precise but boring. Given a book about leadership, it returns five more books about leadership. The user already knows those exist. The point of a recommender is to surface things the user would not have searched for, and pure content similarity does not do that.
Cluster-based is the opposite problem. It is the broadest possible answer. Every book in the same cluster as “Atomic Habits” is a self-help book. The user gets zero signal about which self-help book. It is discovery without relevance.
The hybrid logic is what fixed both. Taking the top three from content similarity and filling the rest from the cluster gives precision on the first half and discovery on the second. The recommendation list reads like a thoughtful librarian made the picks: two obvious choices the user probably already considered, then three unexpected ones from the same neighborhood.
Evaluation on a small dataset is mostly vibes. I tried Precision@5 (the fraction of the top five recommendations that are genuinely relevant) and Recall@5 (the fraction of all relevant items that made it into the top five) as quantitative metrics. They are easy to compute and they tell you almost nothing about whether the system is good. The only honest evaluation is opening ten random books in the UI, looking at the recommendations, and asking whether a human reader would consider them reasonable. I would not publish a recommender based on metric scores alone.
Data cleaning mattered more than I expected. After the first TF-IDF pass, I had several near-duplicate books in the catalogue (same title, same author, slightly different runtime). The recommender was recommending them to each other. The deduplication step looked cosmetic until I actually ran the system and watched the duplicate recommendations pile up. Cleanup before modeling is not optional.
Deployment, and what it cost
Streamlit was the entire deployment story. One Python file, one streamlit run app.py (a command that starts the local web server Streamlit provides), and the app was accessible on localhost. For a real deployment, I packaged it into a small Docker container (a lightweight, portable way to bundle an application and its dependencies) and pushed it to a Linux VPS. The container is under 200 MB. The memory footprint at idle is around 150 MB. Five concurrent users on a $5 VPS is fine.
Streamlit is the framework I keep coming back to for small ML projects. It gets out of the way fast enough to let you focus on the model. You write a normal Python script, sprinkle a few st.write() and st.text_input() decorators on top, and you have a web app. No HTML, no JavaScript, no React (a popular JavaScript library for building user interfaces). For internal tools and prototypes, it is hard to beat.
The downside is that Streamlit is not built for high-traffic production use. Each user session is its own Python process, which means the per-user memory cost is real. For an internal demo or a low-traffic public site, this is fine. For something serving 10,000 concurrent users, you would want a more traditional API + frontend split.
Trade-offs
The hybrid model is the sensible default. Picking pure content similarity or pure cluster filtering is leaving quality on the table. But the hybrid logic is also the part most likely to feel arbitrary if you tweak the weights, so I would start with the simple version (top 3 from content, top 2 from cluster) and only adjust if your specific dataset shows a different balance working better.
TF-IDF is the right starting feature, but it is also a 20-year-old technique. Modern transformer embeddings (vector representations of text learned by neural networks trained on large amounts of data) would give meaningfully better semantic matches at the cost of more setup and slower inference. For a few thousand books, TF-IDF is fast enough and good enough. For a million, I would switch.
Clustering is necessary, but the k choice is genuinely hard to automate. The elbow method gives you a number, but it is often wrong for the use case. I ended up hand-tuning k by running the app, sampling ten books, and judging the cluster quality by eye. That is not a satisfying answer, but it is the honest one.
Streamlit deployment is the right call for anything you are showing to yourself or a small audience. It is the wrong call if you ever expect to monetize the system or run a real product business on top of it. Plan for the rewrite if the user base grows.
What I would tell past me
Three things, in order of importance.
Skip the metric-based evaluation. Open the app, click ten books, and judge the recommendations yourself. Metrics on a small dataset will lie to you about quality.
Dedup the dataset before TF-IDF, not after. Duplicates pollute the similarity scores in ways that are hard to spot in the metrics but obvious in the UI.
Default to the hybrid model. The content-only and cluster-only versions are useful for understanding the system, but they are not the version you ship.
Bottom line: A hybrid recommender for a few thousand audiobooks is a weekend project, not a research problem. TF-IDF, KMeans, and Streamlit get you 90% of the way there. The remaining 10% is data cleaning and the hybrid combination logic, and that is where the actual quality lives.