An end-to-end machine learning platform on the full 7-million-review Yelp dataset, powering two services, a business recommendation engine and a sentiment classifier, through one REST API, spanning large-scale data processing, model training, API serving, containerization and automated testing.
A model that scores well in a notebook is not a service, and the distance between the two is where most of the actual work hides. The Yelp Open Dataset is 5.3 GB of raw JSON across 6.99 million reviews, which is past the point where a single-machine dataframe stays comfortable and past the point where the training code and the serving code can honestly be the same code. The aim was to take the raw dumps all the way to a REST API answering two unrelated questions, which business to recommend and how a review actually feels, and find out what breaks in between.
Spark is the right tool for the first half of that and the wrong tool for the second. It converts 5.3 GB and factorises 4.39 million interactions without complaining, but the same session called from inside a web request spends roughly 290 ms on a single prediction, almost all of it planning a query and crossing into the JVM to score one row. The data pushes back as well: Yelp star ratings pile up at the top end, so a three-class sentiment model can post a healthy accuracy while barely handling the middle class, and recall on a recommender is a small number by nature (any honest figure looks like failure until a baseline sits next to it).
The shape follows from that split. Everything heavy happens once, offline, and the request path is allowed to know nothing about Spark.
Two implementations of the same maths is a genuine liability: the numpy path can drift from the trained model and nothing would visibly fail, it would just answer differently. The fix is to make parity a test rather than a claim, checking that the exported artifacts reproduce Spark's predictions on held-out reviews, which currently holds at 100 percent. The rest of the tax is ordinary but not free: MLflow to keep runs and model versions straight, Docker Compose so the API and its dependencies come up the same way twice, a Pytest suite in CI, and benchmark numbers written to provenance-stamped files that are committed rather than quoted from a terminal.
The serving side is the clean result: 0.11 ms at p99 against roughly 290 ms for the in-process Spark path, at full prediction parity. The models are more modest and are worth stating plainly. Recall@10 of 5.5 percent means nothing on its own, but it is 6.2 times the most-popular baseline it was measured against, and 86.3 percent sentiment accuracy sits beside a macro-F1 of 0.70 against a 0.67 baseline, which is the class imbalance showing through. Both are reported against those baselines below rather than in isolation.
In numbers
The headline number here is the parity test, not the latency gap: a fast wrong answer is worse than a slow right one, and this only ships the fast path because it is checked automatically against the trained model on every held-out review, not assumed to match it.