Introduction: Why Embedding-Based Endpoints Matter

Welcome back! In the last few lessons, you learned how to turn music tracks and user preferences into vectors, use cosine similarity for recommendations, and group tracks into clusters. Now, you are ready to see how all these pieces come together in a real-world application.

In this lesson, you will learn how to expose your recommendation logic through API endpoints. These endpoints allow your music app to deliver personalized track suggestions, show how tracks are grouped, and let users inspect their own listening profiles.

Before jumping into the code, it’s helpful to understand the purpose of each endpoint:

  • A recommendation endpoint helps deliver real-time track suggestions.
  • A cluster summary endpoint lets users or developers explore how the music library is organized.
  • A user profile endpoint exposes the internal representation (embedding) of a user’s taste — useful for debugging or building transparency features.

These endpoints don’t just return data — they act as bridges between the machine learning logic and a real frontend or client.

Quick Recap: App Structure and Data Loading

Before we dive into the new endpoints, let’s quickly remind ourselves how the app is set up. You have a Flask application that loads track data, computes embeddings, and prepares everything needed for recommendations. Here’s a summary of the setup:

from flask import Flask
from src.database import get_all_tracks
from src.user_model import get_track_embeddings
from src.clustering import assign_track_clusters

app = Flask(__name__)

# Initialize data and embeddings
get_all_tracks()            # Loads tracks into memory
get_track_embeddings()      # Computes and caches track embeddings
assign_track_clusters()     # Computes and caches track clusters

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5001, debug=True)

This code ensures that your app is ready to serve recommendations as soon as it starts. If you are using CodeSignal, these libraries and data will already be set up for you.

Embedding-Based Recommendation Endpoint

The main endpoint for delivering personalized track suggestions is:

GET /api/recommendations/embedding/<user_id>?top_n=5

Let’s break down how this works:

  • <user_id>: The ID of the user you want recommendations for.
  • top_n (optional): How many recommendations to return (the default is 5).

Here’s the relevant code:

@app.route('/api/recommendations/embedding/<user_id>', methods=['GET'])
def get_embedding_recommendations_endpoint(user_id: str):
    try:
        top_n_str = request.args.get('top_n', default='5')
        top_n = int(top_n_str)
        if top_n <= 0:
            return jsonify({"error": "top_n must be a positive integer"}), 400
    except ValueError:
        return jsonify({"error": "top_n must be a valid integer"}), 400

    recommended_track_ids = recommend_tracks_by_similarity(user_id, top_n=top_n)
    
    if not recommended_track_ids:
        return jsonify({
            "user_id": user_id,
            "recommendations": [],
            "message": "No recommendations available for this user (perhaps new user or no suitable tracks)."
        }), 200

    recommended_tracks_details = []
    for track_id in recommended_track_ids:
        track_details = get_track_by_id(track_id)
        if track_details:
            recommended_tracks_details.append(track_details)
            
    return jsonify({
        "user_id": user_id,
        "recommendations": recommended_tracks_details
    })

What happens here?

  1. The endpoint reads the top_n parameter and checks if it’s a valid positive integer.
  2. It calls recommend_tracks_by_similarity(user_id, top_n) to get the best track IDs for the user.
  3. If there are no recommendations (for example, if the user is new), it returns an empty list with a helpful message. This case is known as the cold start problem in recommendation systems. Since the system doesn't yet know the user’s preferences, it can't generate a profile vector. In a production app, you'd typically fall back to popular tracks or ask the user to rate a few songs first.
  4. Otherwise, it fetches the full details for each recommended track and returns them in a JSON response.

As you remember from the previous units, the endpoint relies on a function called recommend_tracks_by_similarity, which compares the user’s profile vector against all track embeddings using cosine similarity. This gives each track a similarity score — how close it is to the user’s taste — and returns the top matches.

Note that tracks the user has already listened to are excluded from the final results. This is handled inside the recommendation logic and ensures users only see new content.

Example output:

{
  "user_id": "user_123",
  "recommendations": [
    {"id": "track_7", "title": "Dreamscape", "genre": "Ambient"},
    {"id": "track_2", "title": "Night Drive", "genre": "Synthwave"},
    {"id": "track_5", "title": "Sunrise", "genre": "Pop"}
  ]
}

If the user has no listening history:

{
  "user_id": "user_123",
  "recommendations": [],
  "message": "No recommendations available for this user (perhaps new user or no suitable tracks)."
}

This endpoint is the main way your app delivers personalized music suggestions.

Track Clusters Summary Endpoint

Another useful endpoint is:

GET /api/clusters/summary?n_clusters=3
  • n_clusters (optional): How many clusters to group tracks into (the default is 3).

Here’s the code:

@app.route('/api/clusters/summary', methods=['GET'])
def get_clusters_summary():
    try:
        n_clusters = int(request.args.get('n_clusters', '3'))
    except ValueError:
        return jsonify({"error": "n_clusters must be a valid integer"}), 400

    clustered_df = assign_track_clusters(n_clusters)
    if clustered_df.empty or 'cluster' not in clustered_df.columns:
        return jsonify({"message": "No cluster data available"}), 200

    result = {}
    for cluster_id, group in clustered_df.groupby('cluster'):
        result[str(cluster_id)] = group[['id', 'title', 'genre']].to_dict(orient='records')

    return jsonify({
        "n_clusters": n_clusters,
        "clusters": result
    })

What does this do?

  1. Reads the n_clusters parameter and checks if it’s valid.
  2. Calls assign_track_clusters(n_clusters) to group tracks.
  3. For each cluster, collects the track IDs, titles, and genres.
  4. Returns a summary of all clusters.

The cluster summary endpoint is especially useful for debugging and UI exploration. It lets you answer questions like:

  • Which songs are grouped together?
  • What type of content does cluster 1 contain?
  • Are similar genres or moods appearing in the same cluster?

It’s also useful if you want to build features like “Browse by mood” or “Explore by cluster.”

Example output:

{
  "n_clusters": 3,
  "clusters": {
    "0": [
      {"id": "track_1", "title": "Chill Vibes", "genre": "Lo-fi"},
      {"id": "track_4", "title": "Soft Rain", "genre": "Ambient"}
    ],
    "1": [
      {"id": "track_2", "title": "Night Drive", "genre": "Synthwave"}
    ],
    "2": [
      {"id": "track_3", "title": "Upbeat Energy", "genre": "Pop"}
    ]
  }
}

This endpoint helps you see how tracks are grouped, which can be useful for browsing or for debugging your recommendation system.

User Profile Vector Endpoint

The last endpoint in this lesson is:

GET /api/user/profile-vector/<user_id>
  • <user_id>: The ID of the user whose profile you want to inspect.

Here’s the code:

@app.route('/api/user/profile-vector/<user_id>', methods=['GET'])
def get_user_profile_vector_endpoint(user_id: str):
    profile_vector = generate_user_profile_vector(user_id)
    if profile_vector is None:
        return jsonify({
            "user_id": user_id,
            "profile_vector": [],
            "message": "No profile available — user may have no listening history."
        }), 200

    return jsonify({
        "user_id": user_id,
        "embedding_dim": len(profile_vector),
        "profile_vector": profile_vector.tolist()
    })

What does this do?

  1. Calls generate_user_profile_vector(user_id) to get the user’s embedding.
  2. If the user has no listening history, it returns an empty vector and a message.
  3. Otherwise, it returns the user’s profile vector and its dimension.

Example output for a user with a profile:

{
  "user_id": "user_123",
  "embedding_dim": 8,
  "profile_vector": [0.12, 0.34, 0.56, 0.78, 0.11, 0.22, 0.33, 0.44]
}

Example output for a new user:

{
  "user_id": "user_456",
  "profile_vector": [],
  "message": "No profile available — user may have no listening history."
}

This endpoint is helpful for debugging and understanding how user preferences are represented in your system. The profile vector is the mean embedding of all tracks a user has listened to. You can think of it as a mathematical summary of their musical taste. While the numbers may not be human-readable, they power the similarity comparisons used for recommendations.

If you want to visualize or analyze a user’s taste shift over time, you could compare their profile vectors before and after a given period.

How Everything Connects

Let’s quickly recap the flow from data to endpoint:

  1. get_all_tracks() loads your track metadata (genre, mood, tempo, etc.).
  2. get_track_embeddings() transforms this metadata into vector format (using one-hot + scaled numerical features).
  3. generate_user_profile_vector(user_id) averages the vectors of the user’s listened tracks.
  4. recommend_tracks_by_similarity() compares that profile vector to all track vectors using cosine similarity.
  5. The Flask endpoint simply serves the results to external clients (like a web app or mobile frontend).

Each endpoint is therefore just an interface — the heavy lifting is already done in user_model.py and recommend.py.

Security Note: In this simplified environment, all API routes are public for demonstration purposes. In a real-world app, you would need to authenticate users and protect endpoints like /recommendations and /profile-vector to prevent unauthorized access to private user data.

Summary And Practice Preview

In this lesson, you learned how to expose your recommendation logic through three key API endpoints:

  • The embedding-based recommendation endpoint for personalized track suggestions
  • The clusters summary endpoint for viewing how tracks are grouped
  • The user profile vector endpoint for inspecting user preferences

These endpoints are the foundation for building interactive and personalized music experiences. In the next section, you will get hands-on practice using these endpoints, making requests, and interpreting the results. This will help you solidify your understanding and prepare you to build even more advanced features.

Sign up
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal