|
26 | 26 | "recommender and use the movies dataset as our example data." |
27 | 27 | ] |
28 | 28 | }, |
| 29 | + { |
| 30 | + "cell_type": "markdown", |
| 31 | + "metadata": {}, |
| 32 | + "source": [ |
| 33 | + "Content Filtering recommender systems are built on the premise that a person will want to be recommended things that are similar to things they already like.\n", |
| 34 | + "\n", |
| 35 | + "In the case of movies, if a person watches and enjoys a nature documentary we should recommend other nature documentaries. Or if they like classic black & white horror films we should recommend more of those.\n", |
| 36 | + "\n", |
| 37 | + "The question we need to answer is, 'what does it mean for movies to be similar?'. There are exact matching strategies, like using a movie's labelled genre like 'Horror', or 'Sci Fi', but that can lock people in to only a few genres. Or what if it's not the genre that a person likes, but certain story arcs that are common among many genres?\n", |
| 38 | + "\n", |
| 39 | + "For our content filtering recommender we'll measure similarity between movies as semantic similarity of their descriptions and keywords." |
| 40 | + ] |
| 41 | + }, |
29 | 42 | { |
30 | 43 | "cell_type": "code", |
31 | | - "execution_count": 63, |
| 44 | + "execution_count": 1, |
32 | 45 | "metadata": {}, |
33 | 46 | "outputs": [], |
34 | 47 | "source": [ |
|
56 | 69 | "cell_type": "markdown", |
57 | 70 | "metadata": {}, |
58 | 71 | "source": [ |
59 | | - "Start by loading the movies data and doing a quick inspection of it." |
| 72 | + "Start by downloading the movies data and doing a quick inspection of it." |
60 | 73 | ] |
61 | 74 | }, |
62 | 75 | { |
63 | 76 | "cell_type": "code", |
64 | | - "execution_count": 64, |
| 77 | + "execution_count": 2, |
65 | 78 | "metadata": {}, |
66 | 79 | "outputs": [ |
67 | 80 | { |
|
223 | 236 | "4 1914 /title/tt0004457/ " |
224 | 237 | ] |
225 | 238 | }, |
226 | | - "execution_count": 64, |
| 239 | + "execution_count": 2, |
227 | 240 | "metadata": {}, |
228 | 241 | "output_type": "execute_result" |
229 | 242 | } |
230 | 243 | ], |
231 | 244 | "source": [ |
| 245 | + "try:\n", |
| 246 | + " df = pd.read_csv(\"datasets/content_filtering/25k_imdb_movie_dataset.csv\")\n", |
| 247 | + "except:\n", |
| 248 | + " import requests\n", |
| 249 | + " # download the file\n", |
| 250 | + " url = 'https://redis-ai-resources.s3.us-east-2.amazonaws.com/recommenders/datasets/content-filtering/25k_imdb_movie_dataset.csv'\n", |
| 251 | + " r = requests.get(url)\n", |
| 252 | + "\n", |
| 253 | + " #save the file as a csv\n", |
| 254 | + " os.mkdir('./datasets/content_filtering')\n", |
| 255 | + " with open('./datasets/content_filtering/25k_imdb_movie_dataset.csv', 'wb') as f:\n", |
| 256 | + " f.write(r.content)\n", |
| 257 | + " df = pd.read_csv(\"datasets/content_filtering/25k_imdb_movie_dataset.csv\")\n", |
232 | 258 | "\n", |
233 | | - "# modified from https://www.kaggle.com/datasets/utsh0dey/25k-movie-dataset\n", |
234 | | - "df = pd.read_csv(\"datasets/imdb_movies/25k_imdb_movie_dataset.csv\")\n", |
235 | 259 | "df.head()" |
236 | 260 | ] |
237 | 261 | }, |
|
248 | 272 | }, |
249 | 273 | { |
250 | 274 | "cell_type": "code", |
251 | | - "execution_count": 65, |
| 275 | + "execution_count": 3, |
252 | 276 | "metadata": {}, |
253 | 277 | "outputs": [ |
254 | 278 | { |
|
266 | 290 | "dtype: int64" |
267 | 291 | ] |
268 | 292 | }, |
269 | | - "execution_count": 65, |
| 293 | + "execution_count": 3, |
270 | 294 | "metadata": {}, |
271 | 295 | "output_type": "execute_result" |
272 | 296 | } |
|
303 | 327 | "cell_type": "markdown", |
304 | 328 | "metadata": {}, |
305 | 329 | "source": [ |
306 | | - "RedisVL supports complex query logic, beyond just vector similarity. To showcase this we'll generate an embedding from each movies' `overview` text and list of `plot keywords`,\n", |
307 | | - "and use the remaining fields like, `genres`, `year`, and `rating` as filterable fields to target our vector queries to.\n", |
| 330 | + "Since we movie similarity as semantic similarity of movie descriptions we need a way to generate semantic vector embeddings of these descriptions.\n", |
308 | 331 | "\n", |
309 | | - "#There are many choices for text vectorization, but here we'll use a pretrained model from HuggingFace's transformers library.\n", |
| 332 | + "RedisVL supports many different embedding generators. For this example we'll use a HuggingFace model that is rated well for semantic similarity use cases.\n", |
310 | 333 | "\n", |
311 | | - "RedisVL supports many different embedding generators. For this example we'll use a HuggingFace model that is rated well for semantic similarity use cases." |
| 334 | + "RedisVL also supports complex query logic, beyond just vector similarity. To showcase this we'll generate an embedding from each movies' `overview` text and list of `plot keywords`,\n", |
| 335 | + "and use the remaining fields like, `genres`, `year`, and `rating` as filterable fields to target our vector queries to.\n" |
312 | 336 | ] |
313 | 337 | }, |
314 | 338 | { |
315 | 339 | "cell_type": "code", |
316 | | - "execution_count": 66, |
| 340 | + "execution_count": 4, |
317 | 341 | "metadata": {}, |
318 | 342 | "outputs": [ |
319 | 343 | { |
|
322 | 346 | "'The Story of the Kelly Gang. Story of Ned Kelly, an infamous 19th-century Australian outlaw. ned kelly, australia, historic figure, australian western, first of its kind, directorial debut, australian history, 19th century, victoria australia, australian'" |
323 | 347 | ] |
324 | 348 | }, |
325 | | - "execution_count": 66, |
| 349 | + "execution_count": 4, |
326 | 350 | "metadata": {}, |
327 | 351 | "output_type": "execute_result" |
328 | 352 | } |
|
335 | 359 | }, |
336 | 360 | { |
337 | 361 | "cell_type": "code", |
338 | | - "execution_count": 67, |
| 362 | + "execution_count": null, |
339 | 363 | "metadata": {}, |
340 | 364 | "outputs": [], |
341 | 365 | "source": [ |
342 | 366 | "# this step will take a while, but only needs to be done once for your entire dataset\n", |
343 | | - "# currently taking 10 minutes to run, so we've gone ahead and saved the vectors to a file\n", |
| 367 | + "# currently taking 10 minutes to run, so we've gone ahead and saved the vectors to a file for you\n", |
| 368 | + "# if you don't want to wait, you can skip the cell and load the vectors from the file in the next cell\n", |
344 | 369 | "import pickle\n", |
345 | 370 | "from redisvl.utils.vectorize import HFTextVectorizer\n", |
346 | 371 | "\n", |
347 | 372 | "vectorizer = HFTextVectorizer(model = 'sentence-transformers/paraphrase-MiniLM-L6-v2')\n", |
348 | 373 | "\n", |
| 374 | + "df['embedding'] = df['full_text'].apply(lambda x: vectorizer.embed(x, as_buffer=False))\n", |
| 375 | + "pickle.dump(df['embedding'], open('datasets/content_filtering/text_embeddings.pkl', 'wb'))" |
| 376 | + ] |
| 377 | + }, |
| 378 | + { |
| 379 | + "cell_type": "code", |
| 380 | + "execution_count": 5, |
| 381 | + "metadata": {}, |
| 382 | + "outputs": [], |
| 383 | + "source": [ |
| 384 | + "import pickle\n", |
| 385 | + "\n", |
349 | 386 | "try:\n", |
350 | | - " with open('datasets/imdb_movies/text_embeddings.pkl', 'rb') as vector_file:\n", |
| 387 | + " with open('datasets/content_filtering/text_embeddings.pkl', 'rb') as vector_file:\n", |
351 | 388 | " df['embedding'] = pickle.load(vector_file)\n", |
352 | 389 | "except:\n", |
353 | | - " df['embedding'] = df['full_text'].apply(lambda x: vectorizer.embed(x, as_buffer=False))\n", |
354 | | - " pickle.dump(df['embedding'], open('datasets/imdb_movies/text_embeddings.pkl', 'wb'))" |
| 390 | + " embeddings_url = 'https://redis-ai-resources.s3.us-east-2.amazonaws.com/recommenders/datasets/content-filtering/text_embeddings.pkl'\n", |
| 391 | + " r = requests.get(embeddings_url)\n", |
| 392 | + " with open('./datasets/content_filtering/text_embeddings.pkl', 'wb') as f:\n", |
| 393 | + " f.write(r.content)\n", |
| 394 | + " with open('datasets/content_filtering/text_embeddings.pkl', 'rb') as vector_file:\n", |
| 395 | + " df['embedding'] = pickle.load(vector_file)" |
355 | 396 | ] |
356 | 397 | }, |
357 | 398 | { |
|
364 | 405 | "We'll load this from the accompanying `content_filtering_schema.yaml` file." |
365 | 406 | ] |
366 | 407 | }, |
| 408 | + { |
| 409 | + "cell_type": "markdown", |
| 410 | + "metadata": {}, |
| 411 | + "source": [ |
| 412 | + "This schema defines what each entry will look like within Redis. It will need to specify the name of each field, like `title`, `rating`, and `rating-count`, as well as the type of each field, like `text` or `numeric`.\n", |
| 413 | + "\n", |
| 414 | + "The vector component of each entry similarly needs its dimension (dims), distance metric, algorithm and datatype (dtype) attributes specified." |
| 415 | + ] |
| 416 | + }, |
367 | 417 | { |
368 | 418 | "cell_type": "code", |
369 | | - "execution_count": 68, |
| 419 | + "execution_count": 6, |
370 | 420 | "metadata": {}, |
371 | 421 | "outputs": [ |
372 | 422 | { |
373 | 423 | "name": "stdout", |
374 | 424 | "output_type": "stream", |
375 | 425 | "text": [ |
376 | | - "16:54:56 redisvl.index.index INFO Index already exists, overwriting.\n" |
| 426 | + "15:15:43 redisvl.index.index INFO Index already exists, overwriting.\n" |
377 | 427 | ] |
378 | 428 | } |
379 | 429 | ], |
|
402 | 452 | }, |
403 | 453 | { |
404 | 454 | "cell_type": "code", |
405 | | - "execution_count": 69, |
| 455 | + "execution_count": 7, |
406 | 456 | "metadata": {}, |
407 | 457 | "outputs": [], |
408 | 458 | "source": [ |
|
423 | 473 | }, |
424 | 474 | { |
425 | 475 | "cell_type": "code", |
426 | | - "execution_count": 70, |
| 476 | + "execution_count": 8, |
427 | 477 | "metadata": {}, |
428 | 478 | "outputs": [ |
429 | 479 | { |
430 | 480 | "name": "stdout", |
431 | 481 | "output_type": "stream", |
432 | 482 | "text": [ |
433 | | - "{'id': 'movie:b60e3c2f9e0d43dd8ed33f2f835fb4e0', 'vector_distance': '0.584870040417', 'title': 'The Odyssey', 'overview': 'The aquatic adventure of the highly influential and fearlessly ambitious pioneer, innovator, filmmaker, researcher, and conservationist, Jacques-Yves Cousteau, covers roughly thirty years of an inarguably rich in achievements life.'}\n", |
434 | | - "{'id': 'movie:82808ccd86c44864814c67d8c88ca0d1', 'vector_distance': '0.63329231739', 'title': 'The Inventor', 'overview': 'Inventing flying contraptions, war machines and studying cadavers, Leonardo da Vinci tackles the meaning of life itself with the help of French princess Marguerite de Nevarre.'}\n", |
435 | | - "{'id': 'movie:32ca15de9f6f4054b01fddd93e24eba6', 'vector_distance': '0.658123672009', 'title': 'Ruin', 'overview': 'The film follows a nameless ex-Nazi captain who navigates the ruins of post-WWII Germany determined to atone for his crimes during the war by hunting down the surviving members of his former SS Death Squad.'}\n", |
436 | | - "{'id': 'movie:d2c917e916cc47f3af335f0ec0e1bb50', 'vector_distance': '0.688094437122', 'title': 'The Raven', 'overview': 'A man with incredible powers is sought by the government and military.'}\n", |
437 | | - "{'id': 'movie:fbc21874295d479292eff1486bc49c20', 'vector_distance': '0.694671392441', 'title': 'Get the Girl', 'overview': 'Sebastain \"Bash\" Danye, a legendary gun for hire hangs up his weapon to retire peacefully with his \\'it\\'s complicated\\' partner Renee. Their quiet lives are soon interrupted when they find an unconscious woman on their property, Maddie. While nursing her back to health, some bad me... Read all'}\n" |
| 483 | + "{'id': 'movie:be648c0ed83b460d9e01f03940c7c7cf', 'vector_distance': '0.584870040417', 'title': 'The Odyssey', 'overview': 'The aquatic adventure of the highly influential and fearlessly ambitious pioneer, innovator, filmmaker, researcher, and conservationist, Jacques-Yves Cousteau, covers roughly thirty years of an inarguably rich in achievements life.'}\n", |
| 484 | + "{'id': 'movie:bc1375b4d7dd47e2a117c94bebdffa28', 'vector_distance': '0.63329231739', 'title': 'The Inventor', 'overview': 'Inventing flying contraptions, war machines and studying cadavers, Leonardo da Vinci tackles the meaning of life itself with the help of French princess Marguerite de Nevarre.'}\n", |
| 485 | + "{'id': 'movie:469d553ae60846b9b8c7128fc57fe079', 'vector_distance': '0.658123672009', 'title': 'Ruin', 'overview': 'The film follows a nameless ex-Nazi captain who navigates the ruins of post-WWII Germany determined to atone for his crimes during the war by hunting down the surviving members of his former SS Death Squad.'}\n", |
| 486 | + "{'id': 'movie:493527b640bc48a2941ee46df71a4018', 'vector_distance': '0.688094437122', 'title': 'The Raven', 'overview': 'A man with incredible powers is sought by the government and military.'}\n", |
| 487 | + "{'id': 'movie:bd123271d6a04128acb93dc48b8e5847', 'vector_distance': '0.694671392441', 'title': 'Get the Girl', 'overview': 'Sebastain \"Bash\" Danye, a legendary gun for hire hangs up his weapon to retire peacefully with his \\'it\\'s complicated\\' partner Renee. Their quiet lives are soon interrupted when they find an unconscious woman on their property, Maddie. While nursing her back to health, some bad me... Read all'}\n" |
438 | 488 | ] |
439 | 489 | } |
440 | 490 | ], |
|
468 | 518 | }, |
469 | 519 | { |
470 | 520 | "cell_type": "code", |
471 | | - "execution_count": 71, |
| 521 | + "execution_count": 9, |
472 | 522 | "metadata": {}, |
473 | 523 | "outputs": [], |
474 | 524 | "source": [ |
|
478 | 528 | " flexible_filter = (\n", |
479 | 529 | " (Num(\"year\") > release_year) & # only show movies released after this year\n", |
480 | 530 | " (Tag(\"genres\") == genres) & # only show movies that match at least one in list of genres\n", |
481 | | - " (Text(\"full_text\") % keywords) # only show movies that contain at least one of the keywords\n", |
| 531 | + " (Text(\"full_text\") % keywords) # only show movies that contain at least one of the keywords\n", |
482 | 532 | " )\n", |
483 | 533 | " return flexible_filter\n", |
484 | 534 | "\n", |
|
495 | 545 | " return recommendations" |
496 | 546 | ] |
497 | 547 | }, |
| 548 | + { |
| 549 | + "cell_type": "markdown", |
| 550 | + "metadata": {}, |
| 551 | + "source": [ |
| 552 | + "As a final demonstration we'll find movies similar to the classic horror film 'Nosferatu'.\n", |
| 553 | + "The process has 3 steps:\n", |
| 554 | + "- fetch the vector embedding of our film Nosferatu\n", |
| 555 | + "- optionally define any hard filters we want. Here we'll specify we want horror movies made on or after 1990\n", |
| 556 | + "- perform the vector range query to find similar movies that meet our filter criteria" |
| 557 | + ] |
| 558 | + }, |
498 | 559 | { |
499 | 560 | "cell_type": "code", |
500 | | - "execution_count": 72, |
| 561 | + "execution_count": 10, |
501 | 562 | "metadata": {}, |
502 | 563 | "outputs": [ |
503 | 564 | { |
|
542 | 603 | }, |
543 | 604 | { |
544 | 605 | "cell_type": "code", |
545 | | - "execution_count": 73, |
| 606 | + "execution_count": null, |
546 | 607 | "metadata": {}, |
547 | | - "outputs": [ |
548 | | - { |
549 | | - "name": "stdout", |
550 | | - "output_type": "stream", |
551 | | - "text": [ |
552 | | - "Deleted 143 keys\n" |
553 | | - ] |
554 | | - } |
555 | | - ], |
| 608 | + "outputs": [], |
556 | 609 | "source": [ |
557 | 610 | "# clean up your index\n", |
558 | 611 | "while remaining := index.clear():\n", |
|
0 commit comments