Movie Recommendation
Notebook
Note
This tutorial is meant for Standard & Premium Workspaces. You can't run this with a Free Starter Workspace due to restrictions on Storage. Create a Workspace using +group in the left nav & select Standard for this notebook. Gallery notebooks tagged with "Starter" are suitable to run on a Free Starter Workspace
Source: Full MovieLens 25M Dataset - Appplication
This notebook demonstrates how SingleStoreDB helps you build a simple Movie Recommender System.
1. Install required libraries
Install the library for vectorizing the data (up to 2 minutes).
In [1]:
!pip install sentence-transformers --quiet
2. Create database and ingest data
Create the movie_recommender
database.
In [2]:
%%sqlDROP DATABASE IF EXISTS movie_recommender;CREATE DATABASE IF NOT EXISTS movie_recommender;
Action Required
Make sure to select the movie_recommender database from the drop-down menu at the top of this notebook. It updates the connection_url which is used by the %%sql magic command and SQLAlchemy to make connections to the selected database.
Create tags
table and start pipeline.
In [3]:
%%sqlCREATE TABLE IF NOT EXISTS tags (`userId` bigint(20) NULL,`movieId` bigint(20) NULL,`tag` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL,`timestamp` bigint(20) NULL);CREATE PIPELINE tagsAS LOAD DATA S3 'studiotutorials/movielens/tags.csv'CONFIG '{\"region\":\"us-east-1\", \"disable_gunzip\": false}'BATCH_INTERVAL 2500MAX_PARTITIONS_PER_BATCH 1DISABLE OUT_OF_ORDER OPTIMIZATIONDISABLE OFFSETS METADATA GCSKIP DUPLICATE KEY ERRORSINTO TABLE `tags`FIELDS TERMINATED BY ',' ENCLOSED BY '"' ESCAPED BY '\\'LINES TERMINATED BY '\r\n'NULL DEFINED BY ''IGNORE 1 LINES(userId, movieId, tag, timestamp);START PIPELINE tags;
Create ratings
table and start pipeline.
In [4]:
%%sqlCREATE TABLE IF NOT EXISTS ratings (userId bigint(20) DEFAULT NULL,movieId bigint(20) DEFAULT NULL,rating double DEFAULT NULL,timestamp bigint(20) DEFAULT NULL);CREATE PIPELINE ratingsAS LOAD DATA S3 'studiotutorials/movielens/ratings.csv'CONFIG '{\"region\":\"us-east-1\", \"disable_gunzip\": false}'BATCH_INTERVAL 2500MAX_PARTITIONS_PER_BATCH 1DISABLE OUT_OF_ORDER OPTIMIZATIONDISABLE OFFSETS METADATA GCSKIP DUPLICATE KEY ERRORSINTO TABLE `ratings`FIELDS TERMINATED BY ',' ENCLOSED BY '"' ESCAPED BY '\\'LINES TERMINATED BY '\r\n'NULL DEFINED BY ''IGNORE 1 LINES(userId, movieId, rating, timestamp);START PIPELINE ratings;
Create movies
table and start pipeline.
In [5]:
%%sqlCREATE TABLE movies (movieId bigint(20) DEFAULT NULL,title text CHARACTER SET utf8 COLLATE utf8_general_ci,genres text CHARACTER SET utf8 COLLATE utf8_general_ci,FULLTEXT(title));CREATE PIPELINE moviesAS LOAD DATA S3 'studiotutorials/movielens/movies.csv'CONFIG '{\"region\":\"us-east-1\", \"disable_gunzip\": false}'BATCH_INTERVAL 2500MAX_PARTITIONS_PER_BATCH 1DISABLE OUT_OF_ORDER OPTIMIZATIONDISABLE OFFSETS METADATA GCSKIP DUPLICATE KEY ERRORSINTO TABLE `movies`FIELDS TERMINATED BY ',' ENCLOSED BY '"' ESCAPED BY '\\'LINES TERMINATED BY '\r\n'NULL DEFINED BY ''IGNORE 1 LINES(movieId, title, genres);START PIPELINE movies;
Check that all the data has been loaded
There should be 25m rows for ratings, 62k for movies and 1m for tags. If the values are less than that, try the query again in a few seconds, the pipelines are still running.
In [6]:
%%sqlSELECT COUNT(*) AS count_rows FROM ratingsUNION ALLSELECT COUNT(*) AS count_rows FROM moviesUNION ALLSELECT COUNT(*) AS count_rows FROM tags
Concatenate tags
and movies
tables using all tags
In [7]:
%%sqlCREATE TABLE movies_with_tags ASSELECTm.movieId,m.title,m.genres,GROUP_CONCAT(t.tag SEPARATOR ',') AS allTagsFROM movies mLEFT JOIN tags t ON m.movieId = t.movieIdGROUP BY m.movieId, m.title, m.genres;
3. Vectorize data
Initialize sentence transformer.
In [8]:
from sentence_transformers import SentenceTransformermodel = SentenceTransformer('flax-sentence-embeddings/all_datasets_v3_mpnet-base')
Query the movies_with_tags
table and store the output in a variable named result
. The result <<
syntax in the
%%sql
line indicates that the output from the query should get stored under that variable name.
In [9]:
%%sql result <<SELECT * FROM movies_with_tags
Convert the result from the above SQL into a DataFrame and clean up quotes.
In [10]:
import pandas as pddf = pd.DataFrame(result)# Curate the special charactersdf['title'] = df['title'].str.replace('"', '')df['allTags'] = df['allTags'].str.replace('"', '').str.replace("'", '')data = df.to_dict(orient='records')
Check the first row of the list.
In [11]:
data[0]
Concatenate title and tags.
In [12]:
all_title_type_column = [f'{row["title"]}-{row["allTags"]}' if row["title"] is not None else row["title"] for row in data]
Create the embeddings for Title & Tag (~3 minutes).
In [13]:
# Remove [:3000] if you want to vectorize all rows (~60 minutes)all_embeddings = model.encode(all_title_type_column[:3000])all_embeddings.shape
Merge the original data with the vector data.
In [14]:
# Remember the list will be only 3,000 elementsfor row, embedding in zip(data, all_embeddings):row['embedding'] = embedding
In [15]:
data[0]
4. Create table for movie information and vectors
In [16]:
%%sqlDROP TABLE IF EXISTS movie_with_tags_with_vectors;CREATE TABLE movie_with_tags_with_vectors (movieId BIGINT(20) DEFAULT NULL,title text CHARACTER SET utf8 COLLATE utf8_general_ci,genres text CHARACTER SET utf8 COLLATE utf8_general_ci,allTags longtext CHARACTER SET utf8mb4,vector BLOB)
Create a database connection using SQLAlchemy. We are going to use an SQLAlchemy connection here because one
column of data is numpy arrays. The SingleStoreDB SQLAlchemy driver will automatically convert those to
the correct binary format when uploading, so it's a bit more convenient than doing the conversions and
formatting manually for the %sql
magic command.
In [17]:
from singlestoredb import create_engineconn = create_engine().connect()
Insert the data. Some rows might encounter errors due to unsupported characters.
In [18]:
import sqlalchemy as sasql_query = sa.text('''INSERT INTO movie_with_tags_with_vectors (movieId,title,genres,allTags,vector)VALUES (:movieId,:title,:genres,:allTags,:embedding)''')conn.execute(sql_query, data[:3000])
5. Marrying Search ❤️ Semantic Search ❤️ Analytics
Build autocomplete search
This is en experimentat we started with to render a full text search.
In [19]:
%%sqlWITH queryouter AS (SELECT DISTINCT(title), movieId, MATCH(title) AGAINST ('Pocahontas*') as relevanceFROM moviesWHERE MATCH(title) AGAINST ('Pocahontas*')ORDER BY relevance DESCLIMIT 10)SELECT title, movieId FROM queryouter;
Create user favorite movie tables
In [20]:
%%sqlCREATE ROWSTORE TABLE IF NOT EXISTS user_choice (userid text CHARACTER SET utf8 COLLATE utf8_general_ci,title text CHARACTER SET utf8 COLLATE utf8_general_ci,ts datetime DEFAULT NULL,KEY userid (userid))
Enter dummy data for testing purposes.
In [21]:
%%sqlINSERT INTO user_choice (userid, title, ts)VALUES ('user1', 'Zone 39 (1997)', '2022-01-01 00:00:00'),('user1', 'Star Trek II: The Wrath of Khan (1982)', '2022-01-01 00:00:00'),('user1', 'Giver, The (2014)', '2022-01-01 00:00:00');
Build semantic search for a movie recommendation
In [22]:
%%sqlWITHtable_match AS (SELECTm.title,m.movieId,m.vectorFROMuser_choice tINNER JOIN movie_with_tags_with_vectors m ON m.title = t.titleWHEREuserid = 'user1'),movie_pairs AS (SELECTm1.movieId AS movieId1,m1.title AS title1,m2.movieId AS movieId2,m2.title AS title2,DOT_PRODUCT(m1.vector, m2.vector) AS similarityFROMtable_match m1CROSS JOIN movie_with_tags_with_vectors m2WHEREm1.movieId != m2.movieIdAND NOT EXISTS (SELECT1FROMuser_choice ucWHEREuc.userid = 'user1'AND uc.title = m2.title)),movie_match AS (SELECTmovieId1,title1,movieId2,title2,similarityFROMmovie_pairsORDER BYsimilarity DESC),distinct_count AS (SELECT DISTINCTmovieId2,title2 AS Title,ROUND(AVG(similarity), 4) AS Rating_MatchFROMmovie_matchGROUP BYmovieId2,title2ORDER BYRating_Match DESC),average_ratings AS (SELECTmovieId,AVG(rating) AS Avg_RatingFROMratingsGROUP BYmovieId)SELECTdc.Title,dc.Rating_Match as 'Match Score',ROUND(ar.Avg_Rating, 1) AS 'Average User Rating'FROMdistinct_count dcJOIN average_ratings ar ON dc.movieId2 = ar.movieIdORDER BYdc.Rating_Match DESCLIMIT5;
6. What are you looking for?
In [23]:
search_embedding = model.encode("I want see a French comedy movie")
In [24]:
sql_query = sa.text('''SELECT title, genres, DOT_PRODUCT(vector, :vector) AS score FROM movie_with_tags_with_vectors tvORDER BY Score DESCLIMIT 10''')results = conn.execute(sql_query, dict(vector=search_embedding))for i, res in enumerate(results):print(f"{i + 1}: {res.title} {res.genres} Score: {res.score}")
Clean up
In [25]:
%%sqlDROP DATABASE IF EXISTS movie_recommender
Details
About this Template
Movie recommendation engine using vectors stored in SingleStore to find your next watch.
This Notebook can be run in Standard and Enterprise deployments.
Tags
License
This Notebook has been released under the Apache 2.0 open source license.