Content Based Recommendations in Go

Introduction to Content-Based Recommendation Systems

Welcome to the beginning of our journey into content-based recommendation systems. In the grand scope of recommendation technologies, these systems play a crucial role. They allow applications to suggest relevant items to users based on various content features, enhancing the user experience through personalization. Imagine a music app recommending songs based on the characteristics of songs that a user has liked or listened to in the past. That's the power of a content-based system!

In this lesson, we will delve into how content features are extracted to create efficient recommendations, setting a solid foundation for more advanced techniques.

Dataset Overview and Setup

Let's start by revisiting the datasets we will be working with: tracks.json and authors.json. These JSON files contain essential information about music tracks and artists, respectively. Here is an example of how this can work:

JSON
// tracks.json
[
    {
        "track_id": "001",
        "title": "Song A",
        "likes": 150,
        "clicks": 300,
        "full_listens": 120,
        "author_id": "A1"
    }
    // ... more tracks
]
JSON
// authors.json
[
    {
        "author_id": "A1",
        "name": "Artist X",
        "author_listeners": 5000,
        "genre": "Rock"
    }
    // ... more authors
]

Note that we link a track to its author using the author_id field.

Reading Data

In Go, we can read JSON files and unmarshal their contents into slices of structs. This allows us to work with the data in a structured way.

First, let's define the structs that match the structure of our JSON data:

Go
type Track struct {
    TrackID      string `json:"track_id"`
    Title        string `json:"title"`
    Likes        int    `json:"likes"`
    Clicks       int    `json:"clicks"`
    FullListens  int    `json:"full_listens"`
    AuthorID     string `json:"author_id"`
}

type Author struct {
    AuthorID        string `json:"author_id"`
    Name            string `json:"name"`
    AuthorListeners int    `json:"author_listeners"`
    Genre           string `json:"genre"`
}

Now, let's read the JSON files and unmarshal them into slices of these structs:

Go
import (
    "encoding/json"
    "fmt"
    "io"
    "log"
    "os"
)

func main() {
    // Read tracks.json
    tracksFile, err := os.Open("tracks.json")
    if err != nil {
        log.Fatal(err)
    }
    defer tracksFile.Close()
    tracksBytes, err := io.ReadAll(tracksFile)
    if err != nil {
        log.Fatal(err)
    }

    var tracks []Track
    if err := json.Unmarshal(tracksBytes, &tracks); err != nil {
        log.Fatal(err)
    }

    // Read authors.json
    authorsFile, err := os.Open("authors.json")
    if err != nil {
        log.Fatal(err)
    }
    defer authorsFile.Close()
    authorsBytes, err := io.ReadAll(authorsFile)
    if err != nil {
        log.Fatal(err)
    }

    var authors []Author
    if err := json.Unmarshal(authorsBytes, &authors); err != nil {
        log.Fatal(err)
    }

    // Now tracks and authors are slices of structs containing our data
    fmt.Println(tracks)
    fmt.Println(authors)
}

After loading, the tracks and authors slices in Go represent tabular data structures, similar to spreadsheets. Each element in the slice is like a row, and each field in the struct is like a column. For example, the data looks like this:

tracks:

text
{TrackID:001 Title:Song A Likes:150 Clicks:300 FullListens:120 AuthorID:A1}
{TrackID:002 Title:Song B Likes:200 Clicks:400 FullListens:180 AuthorID:A2}
{TrackID:003 Title:Song C Likes:100 Clicks:250 FullListens:95 AuthorID:A3}

authors:

text
{AuthorID:A1 Name:Artist X AuthorListeners:5000 Genre:Rock}
{AuthorID:A2 Name:Artist Y AuthorListeners:8000 Genre:Pop}
{AuthorID:A3 Name:Artist Z AuthorListeners:3000 Genre:Jazz}

These slices make it easy to process and analyze your data, with each struct field corresponding to a column and each slice element representing a row.

Merging Data

To make meaningful recommendations, we need to combine information about tracks and authors. In Go, we can do this by matching the author_id field in both slices. One efficient way is to build a map from author ID to author struct, and then create a new slice that combines the information.

Let's define a new struct to hold the merged data:

Go
type MergedTrack struct {
    TrackID         string
    Title           string
    Likes           int
    Clicks          int
    FullListens     int
    AuthorID        string
    AuthorName      string
    AuthorListeners int
    Genre           string
}

Now, let's merge the data:

Go
// Build a map from author_id to Author
authorMap := make(map[string]Author)
for _, author := range authors {
    authorMap[author.AuthorID] = author
}

// Merge tracks with their corresponding author information
var mergedTracks []MergedTrack
for _, track := range tracks {
    author, ok := authorMap[track.AuthorID]
    if ok {
        merged := MergedTrack{
            TrackID:         track.TrackID,
            Title:           track.Title,
            Likes:           track.Likes,
            Clicks:          track.Clicks,
            FullListens:     track.FullListens,
            AuthorID:        track.AuthorID,
            AuthorName:      author.Name,
            AuthorListeners: author.AuthorListeners,
            Genre:           author.Genre,
        }
        mergedTracks = append(mergedTracks, merged)
    }
}

// Print merged tracks
for _, mt := range mergedTracks {
    fmt.Printf("%+v\n", mt)
}

After merging, the combined data will look like this:

text
{TrackID:001 Title:Song A Likes:150 Clicks:300 FullListens:120 AuthorID:A1 AuthorName:Artist X AuthorListeners:5000 Genre:Rock}
{TrackID:002 Title:Song B Likes:200 Clicks:400 FullListens:180 AuthorID:A2 AuthorName:Artist Y AuthorListeners:8000 Genre:Pop}
{TrackID:003 Title:Song C Likes:100 Clicks:250 FullListens:95 AuthorID:A3 AuthorName:Artist Z AuthorListeners:3000 Genre:Jazz}

This merged structure ensures that each track is paired with the corresponding author information. Only records with matching author_id values in both datasets are included, similar to an inner join in tabular data processing.

Extracting Relevant Content Features

Content features are specific attributes of data that can be used to calculate recommendations. They provide the basis for comparing items and identifying similarities.

In our example, we’re interested in features such as the number of likes, clicks, full_listens, the number of author_listeners, and the genre. Let's define a new struct to hold only these relevant features:

Go
type ContentFeatures struct {
    Likes           int
    Clicks          int
    FullListens     int
    AuthorListeners int
    Genre           string
}

Now, let's extract these features from the merged data:

Go
var featuresList []ContentFeatures
for _, mt := range mergedTracks {
    features := ContentFeatures{
        Likes:           mt.Likes,
        Clicks:          mt.Clicks,
        FullListens:     mt.FullListens,
        AuthorListeners: mt.AuthorListeners,
        Genre:           mt.Genre,
    }
    featuresList = append(featuresList, features)
}

// Print the content features
for _, f := range featuresList {
    fmt.Printf("%+v\n", f)
}

This results in a slice of ContentFeatures structs, each containing only the essential features that drive our recommendation logic. By isolating these features, we prepare a tidy dataset that is easy to use for content-based algorithms.

Output:

text
{Likes:150 Clicks:300 FullListens:120 AuthorListeners:5000 Genre:Rock}
{Likes:200 Clicks:400 FullListens:180 AuthorListeners:8000 Genre:Pop}
{Likes:100 Clicks:250 FullListens:95 AuthorListeners:3000 Genre:Jazz}

This output shows a clean list with only the essential features that drive our recommendation logic.

Note:
While features like Likes, Clicks, and AuthorListeners are already numeric and can be used directly in similarity calculations or machine learning models, categorical features such as Genre (and, if used, AuthorName) are represented as strings. Most downstream similarity algorithms and models require these categorical fields to be converted into numeric representations—such as one-hot encoding, label encoding, or learned embeddings—before they can be used effectively. We will address how to handle these categorical features in later units.

Review and Next Steps

In this lesson, we've covered the initial steps in building a content-based recommendation system. Starting from loading the data, merging datasets, and extracting relevant content features, you've gained skills crucial for moving forward with more comprehensive recommendations.

The next step for you is to apply this knowledge in practice exercises on CodeSignal, where you will put into practice what you've just learned. Remember, the skills acquired here are foundational, paving the way for more sophisticated and personalized recommendation systems. Keep exploring, and enjoy the process of crafting tailored experiences for your future users!

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