The Production Loop#
A served model goes stale. CoreRec closes the loop from serving back to training: log what users saw and clicked, read online metrics, A/B test a new model, retrain on fresh data and swap the model without a restart, and get alerted when traffic drifts.
corerec serve --feedback-log -> POST /feedback -> GET /metrics
^ |
| POST /reload <- corerec retrain <----------+
1. Log impressions and feedback#
corerec train events.csv -o artifacts/m
corerec serve artifacts/m --feedback-log feedback.jsonl
Every /recommend response now carries a request_id, and the list shown is
logged as an impression. When the user acts on an item, send it back with that
id so the click is credited to the list (and A/B variant) that showed it:
curl -X POST localhost:8000/recommend -H 'Content-Type: application/json' \
-d '{"user_id": "u0042", "top_k": 5}'
# {"recommendations": ["i444", "i097", ...], "variant": "default", "request_id": "77e6...", ...}
curl -X POST localhost:8000/feedback -H 'Content-Type: application/json' \
-d '{"user_id": "u0042", "item_id": "i097", "request_id": "77e6...", "event": "click"}'
event is free text; click and purchase are used for retraining. The log
is one JSON object per line, easy to ship to a warehouse.
2. Online metrics#
curl localhost:8000/metrics
Per variant and overall:
Field |
Meaning |
|---|---|
|
clicks on shown items / items shown |
|
share of lists that got at least one click |
|
mean of 1 / rank of the first clicked item per list (0 if none) |
|
catalogue coverage |
|
share of requests answered with popular items (unknown users) |
These are what the offline NDCG from corerec train is a proxy for. The same
numbers are available offline with FeedbackLog("feedback.jsonl").metrics().
3. A/B test a new model#
corerec serve artifacts/m --challenger artifacts/m2 --challenger-share 0.1 \
--feedback-log feedback.jsonl
Users are assigned by a stable hash of their id, so each one keeps seeing the
same variant. /metrics then includes ab_test: both CTRs, the lift and a
two-proportion z-test p-value. In Python:
ModelServer({"control": m1, "treatment": m2}, traffic={"control": 0.9, "treatment": 0.1}, feedback_log="feedback.jsonl").
4. Retrain on a schedule#
corerec retrain artifacts/m --feedback feedback.jsonl
Retrain re-reads the training file (append new events to it, or pass
--data newer.csv) and adds clicks and purchases from the feedback log. Rows
newer than the model’s last training are new data, split in time:
the candidate trains on everything old plus the earlier half of the new rows
both the candidate and the current model are scored on the later half, which neither has seen, with the popular-item fallback the server uses
The candidate replaces the model only if its NDCG@10 is at least the current
one’s (--tolerance relaxes that). It is then retrained on all rows; the old
artifact stays in artifacts/m/previous/ for rollback. --dry-run compares
without changing anything. A random holdout can’t do this job: the deployed
model was trained on those rows, and its exclude-seen filter removes exactly
the items being tested.
Pick up the new model without a restart:
curl -X POST localhost:8000/reload
Nightly with cron:
0 3 * * * corerec retrain /srv/artifacts/m --feedback /srv/feedback.jsonl && curl -s -X POST localhost:8000/reload
Retrain needs a timestamp column in the data, to tell new rows from old.
5. Drift alerts#
/metrics also returns drift, comparing the last 1000 requests
(?recent=N) with everything before them. It alerts, and logs a warning,
when:
CTR fell by more than 30%
the fallback (unknown-user) share rose by more than 10 points
the distribution of clicked items moved (total variation distance > 0.2)
Thresholds are arguments of FeedbackLog.drift(). A rising fallback share
usually means many new users the model hasn’t seen: retrain.