questro-model-api / RECOMMENDER_API_IMPROVED8_DOCS.md
zeyadgamal00's picture
Deploy model API as Docker Space (artifacts pulled from CL-EPIDTN at build)
7b5a4f7 verified
|
Raw
History Blame Contribute Delete
25.6 kB

Questro Recommender API β€” Integration Guide (improved_8)

Engine generation: improved_8 (architecture file cl_epidtn_recommender_improved_8.py, artifacts dir artifacts_improved8/, checkpoint improved_8epochs.pt) Runtime model_version: improved_8 (echoed in every response) Base URL: http://<ML_HOST>:7749 Protocol: REST / JSON (FastAPI) Auth: None (internal network only)


Quick Start

# Start the server
uvicorn recommender_api_improved8:app --host 0.0.0.0 --port 7749

# Health check
curl http://localhost:7749/health

# Get recommendations (star ratings)
curl -X POST http://localhost:7749/recommend \
  -H "Content-Type: application/json" \
  -d '{
    "user": {
      "ratings": [
        {"item_id": "game_271590", "title": "Grand Theft Auto V", "stars": 5.0},
        {"item_id": "movie_155", "title": "The Dark Knight", "stars": 4.5}
      ]
    },
    "k": 10,
    "domain": "game",
    "blocked_genres": ["Horror", "War"]
  }'

Endpoints

Method Path Purpose
GET /health Liveness + model-loaded status
GET /genres List blockable genres/tags (for the parental-controls UI)
POST /recommend Personalised recommendations with pagination + genre blocking
POST /recommend/rerank Re-rank a RAG-fetched candidate list (RAG tool)
POST /catalog/add Hot-add cold-start items at runtime
POST /admin/reload Reload artifacts from disk (clears hot-added items)

1. GET /health

Health check β€” verify the model is loaded before sending requests.

Response:

{
  "status": "ok",
  "model_loaded": true,
  "n_items": 138541,
  "n_genres_tracked": 138541,
  "text_index_loaded": true,
  "model_version": "improved_8",
  "hot_added_count": 0
}
Field Type Description
status string Always "ok" when the server responds
model_loaded bool false during startup β€” wait until true
n_items int Total items in catalog (movies + games)
n_genres_tracked int Items with genre/tag data for blocking
text_index_loaded bool Whether text embeddings are available (enhances quality)
model_version string Engine version string ("improved_8")
hot_added_count int Items added at runtime via /catalog/add since the last load

2. POST /recommend

The main endpoint. Send a user profile and get personalised, genre-filtered recommendations with pagination.

Request Body

{
  "user": {
    "age": 21,
    "gender": "Male",
    "profession": "Student",
    "country": "Egypt",
    "movie_genres_fav": "Action|Adventure|Comedy",
    "movie_genres_disliked": "Horror|War",
    "game_genres_fav": "Action|RPG|Shooter",
    "game_genres_disliked": "Card|Educational",
    "ratings": [
      {"item_id": "game_271590", "title": "Grand Theft Auto V", "type": "game", "rating": "5 Stars"},
      {"item_id": "movie_155", "title": "The Dark Knight", "stars": 4.5},
      {"item_id": "movie_680", "source": "wishlist"},
      {"item_id": "game_99999", "source": "ignore"}
    ]
  },
  "k": 10,
  "offset": 0,
  "domain": "movie",
  "blocked_genres": ["Horror", "Crime"]
}

User Profile Fields

Field Type Required Description
age int No User's age (1–120)
gender string No Gender
profession string No Profession / occupation
country string No Country
movie_genres_fav string No Pipe-separated favourite movie genres
movie_genres_disliked string No Pipe-separated disliked movie genres
game_genres_fav string No Pipe-separated favourite game genres
game_genres_disliked string No Pipe-separated disliked game genres
ratings RatingItem[] βœ… Yes At least 1 rating (see below)

Rating Item Fields

Each item in ratings supports three input modes β€” use whichever is convenient:

Field Type Description
item_id string Required. Format: "movie_{id}", "movie:{id}", "game_{id}", or "game:{id}"
title string Optional, for logging
type "movie" | "game" Optional domain hint (inferred from item_id prefix if omitted)
rating string Mode 1: Survey label (see table below)
stars float Mode 2: Numeric rating 1.0–5.0
source string Mode 3: "rating", "wishlist", or "ignore"

Rating Labels Reference

Label Numeric Equivalent Weight
"5 Stars" 5.0 +1.0
"4 Stars" 4.0 +0.5
"Didn't watch but would watch" 3.5 +0.25
"Didn't play but would play" 3.5 +0.25
"3 Stars" 3.0 0.0
"2 Stars" 2.0 βˆ’0.5
"Didn't watch and wouldn't watch" 1.5 βˆ’0.75
"Didn't play and wouldn't play" 1.5 βˆ’0.75
"1 Star" 1.0 βˆ’1.0

Numeric stars are mapped with weight = clamp((stars βˆ’ 3) / 2, βˆ’1, 1). An item with no rating, stars, or source defaults to a mild positive (+0.25).

Source Signals

source value Meaning Equivalent Label
"wishlist" User saved / wishlisted the item "Didn't watch but would watch" (+0.25)
"ignore" User blocked / ignored the item "Didn't watch and wouldn't watch" (βˆ’0.75)
"rating" or null Normal rating β€” uses rating or stars field β€”

Request Parameters

Field Type Required Default Description
k int No 10 Results per page (1–100)
offset int No 0 Pagination offset. Page 1 = 0, page 2 = k, etc.
domain string | null No null "movie", "game", or null for cross-domain
blocked_genres string[] | null No null Genres/tags to exclude (case-insensitive)

Response Body

{
  "count": 10,
  "total_available": 85,
  "domain": "movie",
  "offset": 0,
  "k": 10,
  "recommendations": [
    {
      "item_id": 157336,
      "item_key": "movie:157336",
      "title": "Interstellar (2014)",
      "domain": "movie",
      "score": 0.872451
    }
  ],
  "signals_used": 3,
  "blocked_genres": ["crime", "horror"],
  "model_version": "improved_8",
  "has_more": true
}
Field Type Description
count int Items in this page
total_available int Total results available (after genre filtering)
domain string | null Echoes the requested domain filter
offset int Current offset
k int Requested page size
has_more bool true if more pages exist beyond this one
recommendations[].item_id int | null Backend provider ID: TMDB ID for movies, RAWG ID for games. null if the catalog row has no provider ID.
recommendations[].item_key string Internal key: movie:{id} or game:{id}
recommendations[].title string Human-readable title
recommendations[].domain string "movie" or "game"
recommendations[].score float Relevance score, rounded to 6 dp (higher = better)
signals_used int How many submitted ratings mapped to the catalog
blocked_genres string[] Genres that were blocked (lowercased)
model_version string "improved_8"

Resolve display data from item_id (the TMDB/RAWG provider ID) against your own Movies/Games tables. Use item_key only when you need the model's internal identifier.

Pagination example:

  • Page 1: {"k": 10, "offset": 0} β†’ items 1–10
  • Page 2: {"k": 10, "offset": 10} β†’ items 11–20
  • Stop when has_more is false

3. POST /recommend/rerank β€” RAG Tool

Re-rank a pre-fetched candidate list using the recommender model. Use this as a tool in your RAG pipeline.

Workflow

1. RAG retrieves a broad list of candidate items (e.g. "top 50 sci-fi games")
2. POST them to /recommend/rerank with the user's profile
3. The recommender scores each candidate against the user's taste
4. Items are returned ranked by personalised relevance
5. Blocked genres are filtered out; the user's own history items are removed

Request Body

{
  "user": {
    "ratings": [
      {"item_id": "game_271590", "stars": 5.0},
      {"item_id": "movie_155", "stars": 4.0}
    ]
  },
  "candidate_items": [
    {"item_id": "game_1091500", "title": "Cyberpunk 2077"},
    {"item_id": "game_292030", "title": "The Witcher 3"},
    {"item_id": "game_374320", "title": "Dark Souls III"},
    {"item_id": "movie_27205", "title": "Inception"}
  ],
  "blocked_genres": ["Horror"],
  "k": 3
}
Field Type Required Default Description
user UserProfile βœ… Yes β€” Same user profile as /recommend
candidate_items CandidateItem[] βœ… Yes β€” Items to score (at least 1)
candidate_items[].item_id string βœ… Yes β€” "movie_{id}" / "movie:{id}" / "game_{id}" / "game:{id}"
candidate_items[].title string No β€” Optional title
blocked_genres string[] | null No null Genres/tags to block
k int | null No null Max results. null = return all ranked

Response Body

{
  "count": 3,
  "recommendations": [
    {"item_id": 1091500, "item_key": "game:1091500", "title": "Cyberpunk 2077", "domain": "game", "score": 0.91},
    {"item_id": 292030, "item_key": "game:292030", "title": "The Witcher 3", "domain": "game", "score": 0.87},
    {"item_id": 27205, "item_key": "movie:27205", "title": "Inception", "domain": "movie", "score": 0.83}
  ],
  "signals_used": 2,
  "candidates_submitted": 4,
  "candidates_matched": 4,
  "blocked_genres": ["horror"],
  "model_version": "improved_8"
}
Field Type Description
count int Final results after filtering
recommendations[] object[] Same shape as /recommend (item_id, item_key, title, domain, score)
signals_used int User ratings that mapped to the catalog
candidates_submitted int How many candidates you sent
candidates_matched int How many were found in the model catalog
blocked_genres string[] Genres that were blocked (lowercased)
model_version string "improved_8"

Graceful FAISS fallback. If none of the candidates can be scored by the model β€” e.g. they are all hot-added items whose indices fall outside the trained embedding range, or a transient CUDA error occurs during scoring β€” the endpoint returns count: 0 with an empty recommendations list instead of erroring. The RAG pipeline should treat an empty rerank result as "fall back to the original FAISS similarity order."


4. POST /catalog/add β€” Hot-Add Items

Register new items into the running catalog dynamically, so the recommender can score items it has never been trained on. Ideal for newly released titles or dynamic RAG pipelines.

Workflow

1. RAG retrieves items from an external DB the ML model wasn't trained on
2. POST the missing items to /catalog/add with metadata/text
3. The server expands its tensors and (if sentence-transformers is installed and a
   text index is loaded) computes text embeddings so the items can be scored (0-shot)
4. The items can now be returned by /recommend or /recommend/rerank

Request Body

{
  "items": [
    {
      "item_id": "game_1091500",
      "title": "Cyberpunk 2077",
      "domain": "game",
      "description": "An open-world, action-adventure story set in Night City...",
      "genres": "Action|RPG",
      "tags": "sci-fi|open world|cyberpunk",
      "provider_id": 1091500
    }
  ]
}
Field Type Required Default Description
items CatalogNewItem[] βœ… Yes β€” 1–500 items per request
items[].item_id string βœ… Yes β€” "movie_{id}" / "movie:{id}" / "game_{id}" / "game:{id}"
items[].title string βœ… Yes β€” Human-readable title
items[].domain "movie" | "game" | null No inferred from item_id Optional domain override
items[].description string No "" Synopsis (used for text embedding)
items[].genres string No "" Pipe- or comma-separated genres, e.g. "Action|RPG"
items[].tags string No "" Pipe- or comma-separated tags
items[].provider_id int | null No null TMDB ID for movies, RAWG ID for games β€” surfaced as item_id in later responses

Response Body

{
  "added": ["game:1091500"],
  "already_exists": [],
  "failed": {},
  "n_items": 138542,
  "text_index_updated": true
}
Field Type Description
added string[] Internal keys (domain:id) that were registered this call
already_exists string[] Keys that were already in the catalog (skipped)
failed object Map of item_id β†’ error reason for items that could not be added
n_items int Total catalog size after the operation
text_index_updated bool true if text embeddings were computed and written for the new items

Notes. Hot-added items are runtime-only β€” they are cleared on /admin/reload or a server restart. They start with zero learned collaborative embeddings and rank primarily through text similarity, which requires sentence-transformers to be installed and a text index to be loaded; otherwise text_index_updated is false. Items whose new index exceeds the model's trained embedding bounds are kept in the catalog (and still visible to FAISS/RAG) but are skipped by the model's scoring path in /recommend/rerank.


5. GET /genres

List all genres/tags available for blocking. Use this to populate the parental-controls UI.

Response:

{
  "genres": ["action", "adventure", "animation", "comedy", "crime", "documentary", "drama", "fantasy", "horror", "indie", "mystery", "rpg", "racing", "romance", "sci-fi", "shooter", "simulation", "sports", "strategy", "thriller", "war", "western"]
}

All genres are lowercased and limited to tokens longer than 2 characters. When sending blocked_genres, any casing works β€” the API normalises everything to lowercase.


6. POST /admin/reload

Reload all on-disk artifacts (model, indices, metadata). Runtime hot-added items are intentionally cleared so the server returns to the persisted artifact state.

Response:

{
  "status": "ok",
  "n_items": 138541,
  "text_index_loaded": true,
  "hot_added_count": 0
}
Field Type Description
status string "ok" on success
n_items int Catalog size after reload
text_index_loaded bool Whether text embeddings were reloaded
hot_added_count int Reset to 0 after reload

How Recommendations Are Scored

A few behaviours are worth knowing when integrating:

  • Over-fetch before filtering. When blocked_genres is set, the engine fetches OVERFETCH_MULTIPLIER Γ— more candidates (default 5Γ—) before applying genre filtering, so a full page of k results is still returned after blocked items are removed.
  • Title-family franchise boost. For profiles seeded with a clear franchise (e.g. Grand Theft Auto V), the engine adds extra same-franchise candidates and applies a score boost (TITLE_FAMILY_BOOST, default 0.40) so obvious franchise neighbours rank ahead of generic genre matches. Version/edition words ("Remastered", "GOTY", roman numerals, …) are stripped so sequels still match.
  • History exclusion. In /recommend/rerank, candidates already present in the user's rating history are dropped before scoring.
  • Cold-start text scoring. Hot-added items are embedded with a lightweight sentence encoder (all-MiniLM-L6-v2) so they can be scored by content similarity even though they have no learned collaborative vector.

Integration Guide

Mapping Your IDs to the API

Your Database API item_id format Example
Movie (TMDB ID) "movie_{TMDB_Id}" "movie_155"
Movie (MovieLens ID) "movie_{movieId}" "movie_1199"
Game (Steam App ID) "game_{app_id}" "game_271590"

Responses return the provider ID in the item_id field β€” TMDB ID for movies, RAWG ID for games β€” alongside the internal item_key.

Converting Your Signals to API Ratings

User Action in Your App API Rating
Rated 1–5 stars {"item_id": "...", "stars": 4.0}
Liked {"item_id": "...", "stars": 4.0}
Watched / Played {"item_id": "...", "stars": 4.0}
Wishlisted / Saved {"item_id": "...", "source": "wishlist"}
Ignored / Blocked {"item_id": "...", "source": "ignore"}
Disliked {"item_id": "...", "stars": 1.0}

Parental Controls

Send blocked_genres with every request for child accounts. Populate the picker from GET /genres.

Restriction Level blocked_genres
Child-safe ["horror", "crime", "war", "thriller"]
Teen-safe ["horror"]
No restrictions null or []

The API guarantees:

  • No item with a blocked genre/tag will appear in results.
  • The requested k items are returned after filtering (the model over-fetches internally).
  • Case-insensitive matching: "Horror", "HORROR", "horror" are all equivalent.

Typical Backend Flow

1. User opens "For You" page
2. Backend queries its own DB for user's ratings/likes/watches/wishlists/ignores
3. Backend maps each item to {item_id, stars/source}
4. Backend reads the user's parental control settings β†’ blocked_genres
5. Backend POSTs to /recommend with offset=0, k=20
6. Backend receives item_id values (TMDB/RAWG provider IDs)
7. Backend looks up those IDs in its own Movies/Games table
8. Backend returns rich item data (posters, descriptions) to frontend
9. On scroll / "Load More", backend POSTs again with offset=20

RAG Tool Integration

1. User asks chatbot: "Recommend me games like Skyrim"
2. RAG retrieves top 50 RPG games from your vector DB
3. (Optional) RAG hot-adds any items missing from the model via /catalog/add
4. RAG calls POST /recommend/rerank with the user profile + 50 candidates
5. API returns the items re-ranked by the user's personal taste
   (empty list β‡’ fall back to the original FAISS order)
6. RAG picks top 5 and formats the response

C# Backend Integration Example

public class RecommenderClient
{
    private readonly HttpClient _http;
    private readonly string _baseUrl;

    public RecommenderClient(string baseUrl)
    {
        _baseUrl = baseUrl.TrimEnd('/');
        _http = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
    }

    public async Task<RecommendResponse?> GetRecommendationsAsync(
        UserProfile user,
        int k = 10,
        int offset = 0,
        string? domain = null,
        List<string>? blockedGenres = null)
    {
        var request = new
        {
            user,
            k,
            offset,
            domain,
            blocked_genres = blockedGenres
        };

        var json = JsonSerializer.Serialize(request,
            new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower });

        var response = await _http.PostAsync(
            $"{_baseUrl}/recommend",
            new StringContent(json, Encoding.UTF8, "application/json"));

        response.EnsureSuccessStatusCode();
        return await response.Content.ReadFromJsonAsync<RecommendResponse>();
    }

    public async Task<RerankResponse?> RerankAsync(
        UserProfile user,
        List<CandidateItem> candidates,
        int? k = null,
        List<string>? blockedGenres = null)
    {
        var request = new
        {
            user,
            candidate_items = candidates,
            k,
            blocked_genres = blockedGenres
        };

        var json = JsonSerializer.Serialize(request,
            new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower });

        var response = await _http.PostAsync(
            $"{_baseUrl}/recommend/rerank",
            new StringContent(json, Encoding.UTF8, "application/json"));

        response.EnsureSuccessStatusCode();
        return await response.Content.ReadFromJsonAsync<RerankResponse>();
    }
}

// Each recommendation exposes item_id (TMDB/RAWG provider id), item_key, title, domain, score.
// Usage β€” recommendations with pagination:
var user = BuildUserProfile(userId);  // your helper
var page1 = await _recommender.GetRecommendationsAsync(
    user, k: 20, offset: 0,
    domain: "movie",
    blockedGenres: userSettings.BlockedGenres);

// Show page1.Recommendations to user...

if (page1.HasMore)
{
    var page2 = await _recommender.GetRecommendationsAsync(
        user, k: 20, offset: 20,
        domain: "movie",
        blockedGenres: userSettings.BlockedGenres);
}

// Usage β€” RAG reranking:
var ragResults = await _vectorDb.SearchAsync("sci-fi games", top: 50);
var candidates = ragResults.Select(r => new CandidateItem
{
    ItemId = $"game_{r.SteamAppId}",
    Title = r.Title
}).ToList();

var reranked = await _recommender.RerankAsync(
    user, candidates, k: 5,
    blockedGenres: userSettings.BlockedGenres);

Error Responses

Code When
422 No ratings provided, all items unmapped, all candidates already in history, invalid rating label, stars out of range
503 Model / catalog still loading (check /health first)

Note: an all-unscoreable /recommend/rerank request is not an error β€” it returns 200 with count: 0 so the caller can fall back to FAISS scores.


Deployment Notes

Item Value
Server start uvicorn recommender_api_improved8:app --host 0.0.0.0 --port 7749
Required files recommender_api_improved8.py, cl_epidtn_recommender_improved_8.py
Artifacts dir ./artifacts_improved8/ containing: improved_8epochs.pt, item_index.pt, item_to_idx.pkl, item_meta.pkl, title_lookup.pkl, improved_item_text_embeddings.pt
Dependencies pip install torch fastapi uvicorn pydantic pandas (plus sentence-transformers for cold-start text scoring)
Text encoder sentence-transformers/all-MiniLM-L6-v2 (cold-start hot-add embeddings)
Swagger docs http://<HOST>:7749/docs (auto-generated)
Cold start time ~15–30 seconds (model + text embeddings loading)
Avg response time ~50–150ms per request

Configurable Environment Variables

Variable Default Purpose
ARTIFACTS_DIR artifacts_improved8 Base directory for artifacts
MODEL_CHECKPOINT <dir>/improved_8epochs.pt Trained model checkpoint
TEXT_EMBEDDINGS_PATH <dir>/improved_item_text_embeddings.pt Optional content embeddings
TEXT_ENCODER_MODEL sentence-transformers/all-MiniLM-L6-v2 Cold-start text encoder
MAX_RECS 100 Hard cap on internal fetch size
OVERFETCH_MULTIPLIER 5 Extra candidates fetched before genre filtering
TITLE_FAMILY_BOOST 0.40 Score boost for same-franchise neighbours
TITLE_FAMILY_EXTRA_CANDIDATES 50 Max extra franchise candidates injected

FAQ

Q: What if a user has no ratings yet? A: The API requires at least 1 rating. For new users, show popular/trending items until they interact with something, then call /recommend.

Q: What if a submitted item_id isn't in the model catalog? A: It's silently skipped. Check signals_used in the response β€” if it's 0, none of the items were found (the endpoint returns 422).

Q: Can I mix stars and survey labels in the same request? A: Yes. Each rating item is independent. You can use stars for some, rating labels for others, and source: "wishlist"/"ignore" for others.

Q: How does pagination work? A: Set offset and k. Page 1 = offset: 0, k: 20, page 2 = offset: 20, k: 20. The has_more field tells you if another page exists. total_available gives the total count after filtering.

Q: Are blocked genres guaranteed to be excluded? A: Yes. The API over-fetches internally and filters BEFORE paginating, so you always get k results (or fewer only if the entire filtered catalog is exhausted).

Q: Why does /recommend/rerank sometimes return an empty list? A: When none of the candidates can be scored by the model (all hot-added/out-of-range, or a transient CUDA error). This is intentional β€” treat it as a signal to keep the RAG's original FAISS ordering.

Q: Is the API stateless? A: User data is stateless β€” no per-user data is stored server-side. The catalog is mutable, though: /catalog/add mutates in-memory state until /admin/reload or a restart.

Q: What's the rerank endpoint for? A: It's designed as a tool for your RAG/chatbot pipeline. Your RAG retrieves a broad candidate list (e.g. "top 50 RPG games"), and the recommender personalises the ranking for the specific user.