Project: a reading recommendation and explanation service
Build a synthetic book catalog and rule-based ranking before grounded explanations, handling cold starts, preference access, empty results, and evaluation.
Implement an explainable ranking baseline and test empty preferences, ordering, evidence, and access.
Start with explicit reading preferences
This project returns a reading list with reasons that a user can inspect. The exercise uses synthetic book IDs, tags, and descriptions. It does not collect real reading behavior or treat one click as a lasting interest. Users choose categories such as AI, systems, and design; the service returns matches or an honest empty result with options to change the filters.
Build catalog browsing, filtering, ranking, and feedback first. A model may phrase the reasons, but the catalog determines which entries exist and what they say. Do not let the model invent authors, publication dates, or reading history. A rule-based baseline also provides a comparison for later model features: extra cost needs a measurable benefit.
Define observable requirements. The same catalog version and preferences should produce the same order. Users should see matching tags, understand the empty-preference path, and retain access when the explanation model is unavailable. Separate ranking from explanation so an explanation failure does not remove a valid list. Each user may read and modify only their own preferences.
Preserve the data behind each recommendation
A book record holds an ID, name, description, status, and catalog version. A controlled tag vocabulary connects to books through an association table. Preferences belong to an authenticated user and have a revision. A recommendation_run records the catalog version, preference snapshot, ranking-rule version, candidate IDs, and scores. Explanations belong to that run.
New runs should exclude a disabled entry. Historical results must still identify the data they used. Store the necessary snapshot or retain a versioned catalog according to the storage and retention policy. Reading today's description does not establish what an older recommendation relied on.
Derive preference ownership from authentication. An arbitrary request user_id must not authorize a write for someone else. Update preferences and their revision in one transaction, and check ownership when reading saved results. A real product also needs a way to clear history, withdraw preferences, and understand retention; the exercise uses fictional data.
Implement an inspectable ranking baseline
Jaccard similarity is the intersection size divided by the union size. Shared tags contribute to the numerator, while additional tags affect the denominator. The score measures set overlap; it does not measure book quality or reader satisfaction.
These three IDs are synthetic. B-1 exactly matches the preferences, while B-3 shares one tag. Sorting uses score first and ID second to make ties deterministic.
books = {"B-1": {"ai", "systems"}, "B-2": {"design"}, "B-3": {"ai", "data"}}
preferences = {"ai", "systems"}
def score(tags):
return len(tags & preferences)/len(tags | preferences)
ranked = sorted(books, key=lambda book: (-score(books[book]), book))
assert ranked[0] == "B-1"
assert score(books["B-2"]) == 0
print([(book, round(score(books[book]), 3)) for book in ranked])
The output is [('B-1', 1.0), ('B-3', 0.333), ('B-2', 0.0)]. The example retains zero-score entries for inspection. The product endpoint can filter them out and return an empty result when none remain. Last place with no matching tags does not support a claim that an entry suits the reader.
The example does not handle an empty union. Decide how empty preferences work before extending it: ask for categories or show explicitly labeled editorial picks. If empty tag sets are permitted, define the function's return value for an empty union. Consider indexing or precomputation when the catalog warrants it; three entries provide no throughput evidence.
Connect the path through Spring endpoints
Use the official Spring REST guide to establish a local service, then define this project's resources. A controller handles input and responses, the recommendation service owns filtering and ranking, and the data-access layer owns catalog and preference operations. HTTP mapping and JSON serialization still need authentication, resource authorization, and business validation around them.
One possible contract uses GET /books, PUT /me/preferences, POST /recommendations, and a read endpoint for a saved run. Bound page size, tag count, and text length on the server. Reject unknown tags rather than silently converting them into query conditions. Return run IDs and versions so the interface can recognize stale results.
Build a vertical slice: import a fixed synthetic catalog, select tags, return deterministic rankings, save runs, and finally add an explanation adapter. Distinguish loading, empty results, explanation failure, and changed preferences in the UI. A late response for old preferences must not overwrite a newer list.
Ground explanations in selected entries
Give the model only selected IDs, descriptions, and matching tags. Bind each explanation to its entry. Validate that returned IDs belong to the candidate set, referenced fields exist, and reasons agree with that run's snapshot. Correct formatting does not establish factual accuracy. A rule-based sentence such as “Matches the AI and systems tags” is a useful fallback.
Bound time and calls. On refusal or interruption, preserve the list and provide rule-based reasons. Catalog text is data, not a tool instruction. The explanation model should have no authority to modify preferences, delete history, or purchase items. External catalog search would add source, permission, and version requirements before longer explanations become useful.
Embeddings may help generate candidates; learned ranking requires suitable data and evaluation. Identify whether the problem is inadequate tag vocabulary, missing candidates, or poor ordering before adding a model. The demonstrated algorithm is rule-based and trains no personalized model.
Include catalog, preference, and rule revisions in cache identity, with the correct user scope. A catalog cache may be shared; personalized results must not leak through a generic cache key. Verify invalidation after catalog updates so a newly recorded revision does not still return stale candidates.
Test relevance, stability, and access separately
Keep test preferences outside the tuning process. Include one tag, several tags, empty sets, unknown tags, no matches, and tied scores. Check order, empty-state behavior, and explanation ownership. Then test disabled entries, preference changes, duplicate requests, model timeouts, and unauthorized reads. Version records make failures reproducible.
Assess relevance, diversity, and explanation grounding separately. Reviewers can use explicit criteria for preference fit, repetitive entries, and support from catalog fields. Passing synthetic tests establishes behavior for those cases; it does not establish a click-through improvement or generalization from examples already used in tuning.
After the first version passes, compare model and rule-based explanations on identical candidates. Record error types, latency, and call cost, then decide whether the difference addresses a real reader difficulty.
Sources & further reading
Sources checked on 2026-11-10. Original engineering explanation; examples are teaching data, not production measurements.