{ "cells": [ { "cell_type": "markdown", "id": "cbba56a9", "metadata": {}, "source": [ "![Redis](https://redis.io/wp-content/uploads/2024/04/Logotype.svg?auto=webp&quality=85,75&width=120)\n", "# Vector Search with Redispy\n", "## Let's Begin!\n", "\"Open\n" ] }, { "cell_type": "markdown", "id": "0b80de6b", "metadata": {}, "source": [ "## Prepare data\n", "\n", "In this examples we will load a list of movie objects with the following attributes: `title`, `rating`, `description`, and `genre`. \n", "\n", "For the vector part of our vector search we will embed the description so that user's can search for movies that best match what they're looking for.\n", "\n", "**If you are running this notebook locally**, FYI you may not need to perform this step at all." ] }, { "cell_type": "code", "execution_count": 5, "id": "b966a9b5", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Cloning into 'temp_repo'...\n", "remote: Enumerating objects: 204, done.\u001b[K\n", "remote: Counting objects: 100% (52/52), done.\u001b[K\n", "remote: Compressing objects: 100% (28/28), done.\u001b[K\n", "remote: Total 204 (delta 37), reused 24 (delta 24), pack-reused 152\u001b[K\n", "Receiving objects: 100% (204/204), 9.47 MiB | 10.76 MiB/s, done.\n", "Resolving deltas: 100% (64/64), done.\n", "mv: temp_repo/python-recipes/vector-search/resources: No such file or directory\n" ] } ], "source": [ "# NBVAL_SKIP\n", "!git clone https://github.com/redis-developer/redis-ai-resources.git temp_repo\n", "!mv temp_repo/python-recipes/vector-search/resources .\n", "!rm -rf temp_repo" ] }, { "cell_type": "markdown", "id": "19bdc2a5-2192-4f5f-bd6e-7c956fd0e230", "metadata": {}, "source": [ "## Packages" ] }, { "cell_type": "code", "execution_count": 24, "id": "c620286e", "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "huggingface/tokenizers: The current process just got forked, after parallelism has already been used. Disabling parallelism to avoid deadlocks...\n", "To disable this warning, you can either:\n", "\t- Avoid using `tokenizers` before the fork if possible\n", "\t- Explicitly set the environment variable TOKENIZERS_PARALLELISM=(true | false)\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "\n", "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m A new release of pip is available: \u001b[0m\u001b[31;49m24.0\u001b[0m\u001b[39;49m -> \u001b[0m\u001b[32;49m25.1\u001b[0m\n", "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m To update, run: \u001b[0m\u001b[32;49mpip install --upgrade pip\u001b[0m\n", "Note: you may need to restart the kernel to use updated packages.\n" ] } ], "source": [ "%pip install -q \"redis>=5.0.5\" numpy sentence-transformers" ] }, { "cell_type": "markdown", "id": "323aec7f", "metadata": {}, "source": [ "## Install Redis Stack\n", "\n", "Later in this tutorial, Redis will be used to store, index, and query vector\n", "embeddings created from PDF document chunks. **We need to make sure we have a Redis\n", "instance available.\n", "\n", "#### For Colab\n", "Use the shell script below to download, extract, and install [Redis Stack](https://redis.io/docs/getting-started/install-stack/) directly from the Redis package archive." ] }, { "cell_type": "code", "execution_count": null, "id": "2cb85a99", "metadata": {}, "outputs": [], "source": [ "# NBVAL_SKIP\n", "%%sh\n", "curl -fsSL https://packages.redis.io/gpg | sudo gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpg\n", "echo \"deb [signed-by=/usr/share/keyrings/redis-archive-keyring.gpg] https://packages.redis.io/deb $(lsb_release -cs) main\" | sudo tee /etc/apt/sources.list.d/redis.list\n", "sudo apt-get update > /dev/null 2>&1\n", "sudo apt-get install redis-stack-server > /dev/null 2>&1\n", "redis-stack-server --daemonize yes" ] }, { "cell_type": "markdown", "id": "7c5dbaaf", "metadata": {}, "source": [ "#### For Alternative Environments\n", "There are many ways to get the necessary redis-stack instance running\n", "1. On cloud, deploy a [FREE instance of Redis in the cloud](https://redis.com/try-free/). Or, if you have your\n", "own version of Redis Enterprise running, that works too!\n", "2. Per OS, [see the docs](https://redis.io/docs/latest/operate/oss_and_stack/install/install-stack/)\n", "3. With docker: `docker run -d --name redis-stack-server -p 6379:6379 redis/redis-stack-server:latest`" ] }, { "cell_type": "markdown", "id": "1d4499ae", "metadata": {}, "source": [ "### Define the Redis Connection URL\n", "\n", "By default this notebook connects to the local instance of Redis Stack. **If you have your own Redis Enterprise instance** - replace REDIS_PASSWORD, REDIS_HOST and REDIS_PORT values with your own." ] }, { "cell_type": "code", "execution_count": 41, "id": "aefda1d1", "metadata": {}, "outputs": [], "source": [ "import os\n", "\n", "# Replace values below with your own if using Redis Cloud instance\n", "REDIS_HOST = os.getenv(\"REDIS_HOST\", \"localhost\") # ex: \"redis-18374.c253.us-central1-1.gce.cloud.redislabs.com\"\n", "REDIS_PORT = os.getenv(\"REDIS_PORT\", \"6379\") # ex: 18374\n", "REDIS_PASSWORD = os.getenv(\"REDIS_PASSWORD\", \"\") # ex: \"1TNxTEdYRDgIDKM2gDfasupCADXXXX\"\n", "\n", "# If SSL is enabled on the endpoint, use rediss:// as the URL prefix\n", "REDIS_URL = f\"redis://:{REDIS_PASSWORD}@{REDIS_HOST}:{REDIS_PORT}\"" ] }, { "cell_type": "markdown", "id": "f8c6ef53", "metadata": {}, "source": [ "### Create redis client" ] }, { "cell_type": "code", "execution_count": 42, "id": "370c1fcc", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "True" ] }, "execution_count": 42, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from redis import Redis\n", "client = Redis.from_url(REDIS_URL)\n", "client.ping()" ] }, { "cell_type": "code", "execution_count": 43, "id": "458fc773", "metadata": {}, "outputs": [], "source": [ "import json\n", "\n", "with open(\"resources/movies.json\", 'r') as file:\n", " movies = json.load(file)" ] }, { "cell_type": "code", "execution_count": 44, "id": "8d561462", "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "/Users/robert.shelton/.pyenv/versions/3.11.9/lib/python3.11/site-packages/huggingface_hub/file_download.py:1142: FutureWarning: `resume_download` is deprecated and will be removed in version 1.0.0. Downloads always resume when possible. If you want to force a new download, use `force_download=True`.\n", " warnings.warn(\n" ] } ], "source": [ "import numpy as np\n", "from sentence_transformers import SentenceTransformer\n", "\n", "# load model for embedding our movie descriptions\n", "model = SentenceTransformer('sentence-transformers/all-MiniLM-L6-v2')\n", "\n", "def embed_text(model, text):\n", " return np.array(model.encode(text)).astype(np.float32).tobytes()" ] }, { "cell_type": "code", "execution_count": 45, "id": "9946a382", "metadata": {}, "outputs": [], "source": [ "# Note: convert embedding array to bytes for storage in Redis Hash data type\n", "movie_data = [\n", " {\n", " **movie,\n", " \"vector\": embed_text(model, movie[\"description\"])\n", " } for movie in movies\n", "]" ] }, { "cell_type": "code", "execution_count": 46, "id": "8797fcc6", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'id': 1,\n", " 'title': 'Explosive Pursuit',\n", " 'genre': 'action',\n", " 'rating': 7,\n", " 'description': 'A daring cop chases a notorious criminal across the city in a high-stakes game of cat and mouse.',\n", " 'vector': b'\\x8bf|=\\xc3`\\n;\\xf2\\x91\\xb7;?\\xcb~\\xbd\\xdfd\\xce\\xbb\\xc7\\x16J=H\\xa7?=\\xdfv\\x95\\x17\\xbeA\\x1e\\x05\\xb9Hu\\xbfg3\\xbd$\\xcd\\xbd\\xbd\\xa1$\\xf7;\\x04\\xf5z=\\xfc\\xb4\\x8c=\\x89\\x0e\\xc6\\xbdhI\\x90\\xbd^\\x16\\xbd;z\\xe7\\x0c\\xbd\\x1b3\\xc9\\xbc\\x89\\xf8\\xbb\\xbc\\x18\\'u\\xbb>\\x8f\\xca<\\x02\\x80J=\\x0e\\xaf*=\\x8dOU\\xbd\\xcf\\xf0\\x95\\xbc \\x02\\x19=\\x19\\xf4K<\\xc5\\xc2\\t=J\\x83\\xac=\\x95\\xd7\\xb8\\xbd\\xf2\\xb5\\x9c\\xbd=\\x85\\x18=\\x94d&=03\\xf8<\\xee\\xf7\\x88<\\x80v\\xf2\\xbb9=[\\xbdG\\xac\\xee\\xbb<:A\\xbd\\xe1d\\x19\\xbd!d\\xf2\\xbb\\x1d\\xbax;\\xec;O<\\xd21,\\xbc\\xec\\xae\\xae=r\\x00-\\xbc\"\\x06\\xae\\xbdl\\xd6\\x1a=\\xc4\\xbf\\xcd=\\x19\\x150=\\xe3\\xf1\\x9d\\xbc\\xa6GK=\\xb2\\xb8 =\\xb2\\xf1I\\xbd-e\\x9e\\xbb\\xe9\\x8a\\xf7:\\x88\\xf8\\x1c=\\x7f\\xba\\xde<\\xd2n\\x16\\xbb\\xb4\\\\p\\xbb\\xd4\\xd5<<\\x89\\xa5\\xa3\\xb8\\xc79s<=4&<\\x84\\x1c\\x18<\\x18\\xd9-\\xbd\\xdf\\xe6\\x98<\\x15\\xa1N=\\xa2/\\xa5=\\x1d\\xf3\\xdd<\\x17L\\x13<\\x10\\x10\\xce\\xbac\\x9e\\xdc\\xbc\\xa68\\x05=+\\xa1\\xf5\\xbd\\x84\\x1bF\\xbd\\xa0?\\x14\\xbe\\xc4\\x8f(\\xbd\\xe6O\\x89\\xbd\\xf7\\xad\\xd4<\\xa7\\x12\\xc3=\\xaf\\x05O\\xbd\\x99\\x8ep\\xbc\\x18\\xb5\\xac\\xbc\\xc9\\x9ee\\xbdH\\x8es;$a\\xc1;\\xd9\\xfaB\\xbd\\xa8#\\xfe:\\x92\\xe6\\xf4=\\xcd\\x15*<\\x86\\xf8\\x1b=\\x01\\xfcV\\xbd\\xd3\\xd1\\r=9\\xee\\x06=\\x13u\\xba\\xbd\\xf7\\xa3\\xd6<\\x1a\\xec\\xd9;\\xb79/=\\xa4\\xc2\\x85=p\\x0b\"=\\xe1i\\xef<:\\xe8c=\\xfb2\\x08\\xbe\\xce\\x12;=OVW;V\\xa4b<\\xd0\\x9d\\xb7<\\x87r;\\xbdqz\\x91\\xbcV\\x00<\\xbd\\xfe\\x19\\xa3<\\xeaJ%\\xbc!\\xe7\\xbf\\xbb\\x7f\\x87\\x12=\\x94\\x1d\\x95=b|\\xfd\\xbc\\xf3\\xf1\\xd1\\xbd\\xf5y\\x84;\\xc9\\tu=]\\x8ai<3\\x91R\\xbd\\xec\\xf3m\\xbd\\x93\\xb83=V\\xedF=\\x1f\\xf3\\xd1\\x08yA\\xba<#\\xacO\\xbd\\x01\\x0f\\xc7;\\x7f\\xf4\\x04\\xbdP\\x82\\x92\\xbd\\x9b\\xddD=p\\xd8;\\xbc\\xd3;\\xf4\\xbc\\xb3\\x8f\\x97\\xbd1\\\\\\r\\xbd\\xea\\x8c\\xf5\\xbd\\x8c\\x13(=\\x9e\\xc8\\xc6=\\xa3\\xed\\x1a=\\x98\\xa8\\xf8=\\x84\\xc1\\xee\\xbc\\xcd-\\x18\\xbb\\xf5~;<\\xd6F\\t\\xbd\\x14\\x08\\x17=\\xa5\\xa5\\x1e=\\x14K\\xcb\\xbd.\\xf7\\x8c\\xbdyb\\xed\\xbb\\x86[\\x19\\xbc]\\x0c\\x13\\xbcgq\\x83=\\xf0wd\\xbd\\xe3\\xc7\\xd1\\xbb8lY\\xbc\\xa7|a=3\\xcf\\xfd\\xbc\\x1f\\xa5\\x83\\xbb\\x99O\\x19\\xbd6\\x02]\\xbd\\xbb\\xeaz=\\x036\\x9c=:^\\xa9\\xbd)^9\\xbcg\\xe4N\\xbcs\\x07x\\xbd\\x18{\\xa0=:\\x9f\\x96<\\xecq8\\xba\\x9e\\xbb=\\xbd\\xe4|(<\\x96\\xdf\\xb4\\xbbl\\xc9\\x0b\\xbd\\xc4\\x01\\x95\\xbd\\xf7\\xc6T=\\tp\\xd1\\x17A\\x1e\\x05Hug3$ͽ$;\\x04z==\\x0eƽhI^\\x16;z\\x0c\\x1b3ɼ\\x18\\'u><\\x02J=\\x0e*=OU \\x02\\x19=\\x19K<\\t=J=\\u05f8\\U000b573d=\\x18=d&=03<\\x1bF?\\x14ď(O<\\x12=\\x05Op\\x18ɞeHs;$a;B#:=\\x15*<\\x1b=\\x01V\\r=9\\x06=\\x13u<\\x1a;9/=\\x85=p\\x0b\"=i<:c=2\\x08\\x12;=OVW;Vb<Н<9,=\\x17ߺ\\x14:M9\\x08\\x0bV<_6=!Ub#=WX=u\\x11=?6=\\x06,<\\'\\x15t=;лwK-=H\\x11\\x036=\\x15<8xM\\x10=_\\x03D=\\x0b\\x08$G\\x0cr=m=<)$y\\x06=X=s%\\r\\x1dz\\x0e\\t<$\\tI=\\x01x\\x10;Y\\x0f<蓻bߺe c=>;\\x18u༎\\x10x~=ah<\\x070;#r=iD:?ئa2g\\x00=\\x1bą;g\\x12=OʃRF2=\\x11䛽%==^<̒\\x06=-@g<;ܼX\\x19=#b\\x0bb}xU;\\\\\\x08~=/&N(缸&\\x08ۆ=:p^<|僼½f\\x11=\\u05fdx<#;Ȼ=1I\\x0b\\x7f\\x0cR\\x11\\x14ʽuA<;\\rpr}\\x0f\\x18=Tp1gC<:\\x16{\\x19.<$5=AGl=-\\\\=hGEY>;2\\r==y{@\\x16Oƻ$o=\\x0b#j=~0> {\\x03kl/=ul\\x07ͼ\\x17>F1\\x1bYFؔ/\\x1d5M\\x07Jݏ=-\\x08xN>\\x7f;M\\x05u\\x19H@tC=<\\x0f\\x18Kz=\\x13=ጽ&=qZ\\x07=Mq=^ߣA*\\'\\x13\\x03=;A&s=u0ltn>\\t=bڼ@f\\'j\\x10\\x01=Ѽ\\x12C=6)vgi6\\x05\\x01l\\x15<\\x17m\\x15; =\\rL\\x13;ýC=\\x04лS\\x03_\\x02[=B/D>=5\\x19\\x03\\x13<\\r|K=\\'h<\\nB<>9T\\x1eh=ݨa=Ϳ-\\x00;=fK=t=}ظ;Ϧj<ݛ;\\x03}<-<\\x18;\\x1e.=\\x1en;\\x01G=L\\x10Q\\\\|\\x11Yo=u\\x0f\\x19%+=1P<:m;\\x07&==rQA\\x15ϼ\\x0b+<\\x02=h\\x0e9=I3K=5ͼ\\x04E7;ty|=\\x04Ӏ<\\x0c<\\x01\\x0e\\x18>\\x0f\\x14qiQ=yR:kX=\\x13|\\x0b=ǎI0s-Qߒ;{XB=\\r\\x046=y6EW=\\r<=X=\\x1a<\\x18U\\x15\\x02I\\x00Bg;~ =\\x7fv\\x16y\\'غ\\x19\\r\\x1b)(3\\x16Oj\\x0eA0=7L=dI\\r=A[=\\x02A;\\\\=o\\x00Ƽ)V¼Їh5\\x01=\\x06=h>ˠŐWxL=\\x04=v\\x7f)͓:\\x10;aZ\"\\x06/2\\x0b==\\t/&|f<ӽm\\x1fnx=+F\\n*<}w\\x07lI\\x00\\x02@=Bi\\x06=\\x1c\\x11v:/ٍ!\\x18(Ј`;<ᓟ\\x02R=\\x13>c{R=3Qٽ2ӄ\\x05<~<${Y=_i=Ib>=Y\\x1c<̠$>#=\\x01j|\\x19Q=6l=\\x15q-Sf<\\x1d\\x07$=\\x1f\\x0e=>\\x03$L~<\\x01\\x02a\\x1dI<\\x14=ZnS3m!~͈\\x05\\u07bb\\x1e=cHf\\x11h@<1ki$3=\\x14\\x08.\\x17w>=\\x03)=><\\x1d\\x10b괺Be=\\x1b\\x003=Y<\\x156e<1bL=D\\x03\\x0b]b\\x14<3 >\\x02\\n:2*=\"8,QʽQ=j\\x1d\\x16w!>\\x13<\\x1ey!\\x00U<\\x13u<\\x12<\\x14C[:c=5<@\\x0bM=\\x05\\x182;f<ӭ==\\x03b;\\x0bН<\\x1b=\\r\\tșL[{\\x16;!μU;\\nZ<\\x0f\\x17=ߑ=\\uec09dT=^Լ<\\x0c;\\x196(=\\x08\\x1b<\\x01=!\\x17<\\x0f.=yq=~\\x1a\\x1f:G0(H]X=d\\x10=Ge@[\\x06F<', 'genre': 'action', 'title': 'John Wick', 'description': 'A retired hitman seeks vengeance against those who wronged him, leaving a trail of destruction in his wake.'}, Document {'id': '6', 'payload': None, 'rating': '9', 'vector': ':A%=Em5\\x0eGh=\\x035%\\x01P\\x1eq\\x1d\\x1c=N==\\r\\x04\\\\8E= ,==4\\x01G==(<\\x02/)=PK6\\x04Y螉\\x0eྲྀ8:2jt|=,\\x0c6\\x17am<&=\\t\\x18>_\\x108<\\x0f!lQ^\\x0e>1K=<*F\\x01Q.hЌg\\r\\x01<Ԭ<@<0\\r\\x11=\\rq\\rT\\'P=y\\x17ml>DM}=rH5=\\x0f\\x13Ϋ=D\\x03;\\rR=a=4=\\x13q\\x07=ޭ\\x01=\\x17<.J\\x01=Gy\\t\\x13S=\\'6k\\x00\\x07;Oؽf\\x08<2ݼ<(}E=M{JֽY=Vj\\x18A=CT@]=Jj<18\\x1a=ぎ=\\r8P{<.4Ž\\x0f=\\\\g==+D=Vce=6xUǽ-\\r+=O=,ü0C\\x05R=\\nrSZ=[L\\x01\\nnV=Ѝ\\x19W=:w=4v=C\\x1b@<:y;\\nh=ڙ=C\\uef1e=@ے=9_S8;e<\\x1d\\x1b=rDK-S새;\\t\\x16=\\x1a\\x18~=r6\\x19=r=1?;\\x16\\r\\x18\\x1e;<\\x15j4ߜP<\\nAR\\x06=ߕ$\\x11=hgv\\x18>\\x14ѽO=Ѷ<@6=\\x03o=\\t\\x01|>A=\\x00!.c3\\\\7L\\x01==-I ];ջͣ<[\\x15\\x0cҩ=Q\\x19Rk$\\x17Oc\\x11B}j\\x02\\roy=~=4==\\r-=(=Zҹ<`@j\\x1c(\\x1b=\\x10=(x:h#pI=x\\u05fb\\\\<\\x1d\\x1cY|\\x0c=Aˡ?\\x18\\x0f\\x04n=?5d\\'=d3;\\x06`ܻ\\x101=^)\\x0c\\x13B<\">=s\\x06=\\x08=̽|\\x1f!{=i\\x19Dm\\x192;<-ſ?[R@=|\\x0f%<\\x01ؕ4\\x13$=Ӽ\\x1c-F!ﵼ\\x01Hf{\\ue17d=\\x12TB<\\x064Ἥ\\x13:\\x11\\x1b_:2;W\\x0c=Xx;m=\\x02= <+*x=-\"P=:\\\\=k\\x0c=\\x12L42\\x15\\x17!q>s=|;\\x14\\x1eKj;>v\\x1e=!=d&=s<<\\x16<4\"\\x16\"u\\x0e>\\x0b\\x05Ɍ.kY-pض\\x12\\x19<[*\\x13=\\x1ej\\t\\x7f=]p<.\\x1f=0f~;%\\n_=?;\\x1eC\\x19W\\x04=FF<_s<;=B=վȤ\\x14r\\x00\\x0c0<\\na.t=s\\x19P$\\'%\\x19\\x05===\\x7fWE}A\\r\\r*<`\\x05=TF= ^=\\x0c0=FA;\\x17G\\x01%\\x05U%=\\x0ck5\\x08Hżb[KNN 3 @vector $vec_param AS dist]\").sort_by(\"dist\").dialect(2)\n", "\n", "res = client.ft(index_name).search(query, query_params = {'vec_param': embedded_user_query})\n", "\n", "print_results(res)\n" ] }, { "cell_type": "markdown", "id": "ef5e1997", "metadata": {}, "source": [ "### Hybrid filter vector search\n", "\n", "Redis allows you to combine filter searches on fields within the index object allowing us to create more specific searches." ] }, { "cell_type": "code", "execution_count": 52, "id": "d499dcad", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Top 3 movies: [('Fast & Furious 9', 'action', '6'), ('Mad Max: Fury Road', 'action', '8'), ('Explosive Pursuit', 'action', '7')]\n" ] } ], "source": [ "# Search for top 3 movies specifically in the action genre\n", "\n", "user_query = \"High tech movies\"\n", "\n", "embedded_user_query = embed_text(model, user_query)\n", "\n", "# Note: genre is a tag field in our schema so the syntax is @:{ | | ...}\n", "query = Query(\"(@genre:{action})=>[KNN 3 @vector $vec_param AS dist]\").sort_by('dist').dialect(2)\n", "\n", "res = client.ft(index_name).search(query, query_params = {'vec_param': embedded_user_query})\n", "\n", "print_results(res)" ] }, { "cell_type": "code", "execution_count": 53, "id": "f59fff2c", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Top 3 movies: [('Mad Max: Fury Road', 'action', '8'), ('Explosive Pursuit', 'action', '7'), ('The Avengers', 'action', '8')]\n" ] } ], "source": [ "# Search for top 3 movies specifically in the action genre with ratings at or above a 7\n", "\n", "user_query = \"High tech movies\"\n", "\n", "embedded_user_query = embed_text(model, user_query)\n", "\n", "query = Query(\"(@genre:{action} & (@rating:[7 inf]))=>[KNN 3 @vector $vec_param AS dist]\").sort_by('dist').dialect(2)\n", "\n", "res = client.ft(index_name).search(query, query_params = {'vec_param': embedded_user_query})\n", "\n", "print_results(res)" ] }, { "cell_type": "code", "execution_count": 54, "id": "8a493ae0", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Top 2 movies: [('Despicable Me', 'comedy', '7'), ('The Dark Knight', 'action', '9')]\n" ] } ], "source": [ "# Search with full text search for movies that directly mention \"criminal mastermind\" in the description\n", "\n", "user_query = \"High tech movies\"\n", "\n", "embedded_user_query = embed_text(model, user_query)\n", "\n", "query = Query(\"(@description:(criminal mastermind))=>[KNN 3 @vector $vec_param AS dist]\").sort_by('dist').dialect(2)\n", "\n", "res = client.ft(index_name).search(query, query_params = {'vec_param': embedded_user_query})\n", "\n", "print_results(res)" ] }, { "cell_type": "code", "execution_count": 55, "id": "c0d7ab60", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Top 3 movies: [('Despicable Me', 'comedy', '7'), ('The Incredibles', 'comedy', '8'), ('Explosive Pursuit', 'action', '7')]\n" ] } ], "source": [ "# Vector search with wild card match\n", "\n", "user_query = \"High tech movies\"\n", "\n", "embedded_user_query = embed_text(model, user_query)\n", "\n", "query = Query(\"(@description:(crim*))=>[KNN 3 @vector $vec_param AS dist]\").sort_by('dist').dialect(2)\n", "\n", "res = client.ft(index_name).search(query, query_params = {'vec_param': embedded_user_query})\n", "\n", "print_results(res)" ] }, { "cell_type": "code", "execution_count": 56, "id": "748c15ae", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Top 3 movies: [('The Avengers', 'action', '8'), ('Black Widow', 'action', '7'), ('The Princess Diaries', 'comedy', '6')]\n" ] } ], "source": [ "# Vector search with fuzzy match\n", "\n", "user_query = \"High tech movies\"\n", "\n", "embedded_user_query = embed_text(model, user_query)\n", "\n", "# Note: fuzzy match is based on Levenshtein distance. Therefore, \"hero\" might return result for \"her\" as an example.\n", "# See docs for more info https://redis.io/docs/latest/develop/interact/search-and-query/advanced-concepts/query_syntax/\n", "query = Query(\"(@description:%hero%)=>[KNN 3 @vector $vec_param AS dist]\").sort_by('dist').dialect(2)\n", "\n", "res = client.ft(index_name).search(query, query_params = {'vec_param': embedded_user_query})\n", "\n", "print_results(res)" ] }, { "cell_type": "markdown", "id": "6bd27cb3", "metadata": {}, "source": [ "## Range queries\n", "\n", "Range queries allow you to set a pre defined \"threshold\" for which we want to return documents. This is helpful when you only want documents with a certain distance from the search query." ] }, { "cell_type": "code", "execution_count": 57, "id": "cafe1795", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Top 6 movies: [('The Incredibles', 'comedy', '8'), ('Black Widow', 'action', '7'), ('Despicable Me', 'comedy', '7'), ('Shrek', 'comedy', '8'), ('Monsters, Inc.', 'comedy', '8'), ('Aladdin', 'comedy', '8')]\n" ] } ], "source": [ "user_query = \"Family friendly fantasy movies\"\n", "\n", "embedded_user_query = embed_text(model, user_query)\n", "\n", "query = (\n", " Query(\"@vector:[VECTOR_RANGE $radius $vector]=>{$YIELD_DISTANCE_AS: vector_distance}\")\n", " .sort_by(\"vector_distance\")\n", " .return_fields(\"title\", \"rating\", \"genre\", \"vector_distance\")\n", " .dialect(2)\n", ")\n", "\n", "# Find all vectors within 0.8 of the query vector\n", "query_params = {\n", " \"radius\": 0.8,\n", " \"vector\": embedded_user_query\n", "}\n", "\n", "res = client.ft(index_name).search(query, query_params)\n", "print_results(res)\n" ] }, { "cell_type": "markdown", "id": "a1586ea7", "metadata": {}, "source": [ "Like the queries above, we can also chain additional filters and conditional operators with range queries. The following adds an `or` condition that returns vector search within the defined range or with a rating at or above 9." ] }, { "cell_type": "code", "execution_count": 58, "id": "d3110324", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Top 3 movies: [('The Incredibles', 'comedy', '8'), ('The Dark Knight', 'action', '9'), ('Inception', 'action', '9')]\n" ] } ], "source": [ "user_query = \"Family friendly fantasy movies\"\n", "\n", "embedded_user_query = embed_text(model, user_query)\n", "\n", "query = (\n", " Query(\"@rating:[9 +inf] | @vector:[VECTOR_RANGE $radius $vector]=>{$YIELD_DISTANCE_AS: vector_distance}\")\n", " .sort_by(\"vector_distance\")\n", " .return_fields(\"title\", \"rating\", \"genre\", \"vector_distance\")\n", " .dialect(2)\n", ")\n", "\n", "# Find all vectors within 0.8 of the query vector\n", "query_params = {\n", " \"radius\": 0.7,\n", " \"vector\": embedded_user_query\n", "}\n", "\n", "res = client.ft(index_name).search(query, query_params)\n", "print_results(res)" ] }, { "cell_type": "markdown", "id": "6e435ce5", "metadata": {}, "source": [ "### Additional queries\n", "\n", "In addition to the variety of vector queries shown above redis supports full-text search, aggregations, and various weighting strategies that can be mixed and matched for a wide range of search applications.\n", "\n", "### Full text search with BM25\n", "\n", "The following query does a pure token based BM25 search with redis." ] }, { "cell_type": "code", "execution_count": 59, "id": "3307ab80", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[Document {'id': '6', 'payload': None, 'score': 4.743066248010575, 'title': 'The Dark Knight', 'genre': 'action', 'rating': '9', 'description': 'Batman faces off against the Joker, a criminal mastermind who threatens to plunge Gotham into chaos.'},\n", " Document {'id': '17', 'payload': None, 'score': 4.560171658735046, 'title': 'Despicable Me', 'genre': 'comedy', 'rating': '7', 'description': 'When a criminal mastermind uses a trio of orphan girls as pawns for a grand scheme, he finds their love is profoundly changing him for the better.'},\n", " Document {'id': '0', 'payload': None, 'score': 2.8628170632759318, 'title': 'Explosive Pursuit', 'genre': 'action', 'rating': '7', 'description': 'A daring cop chases a notorious criminal across the city in a high-stakes game of cat and mouse.'}]" ] }, "execution_count": 59, "metadata": {}, "output_type": "execute_result" } ], "source": [ "input = \"Criminal mastermind\"\n", "\n", "# Redis breaks searches into key tokens\n", "def tokenize(query):\n", " return \" | \".join(query.split(\" \")).lower()\n", "\n", "user_query = Query(tokenize(input))\\\n", " .scorer(\"BM25STD\") \\\n", " .with_scores() \\\n", " .return_fields(\"title\", \"genre\", \"rating\", \"description\") \\\n", " .paging(0, 10) # limits the amount of results to 10\n", "\n", "res = client.ft(index_name).search(user_query)\n", "res.docs" ] }, { "cell_type": "markdown", "id": "1a70df15", "metadata": {}, "source": [ "# Aggregations\n", "\n", "Redis aggregate queries allow you to group, filter, and compute metrics (like counts, sums, and averages) over indexed data stored in Redis. For instance, the following returns the average rating per genre." ] }, { "cell_type": "code", "execution_count": 60, "id": "efb45202", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[[b'genre', b'action', b'avg_rating', b'7.8'],\n", " [b'genre', b'comedy', b'avg_rating', b'7.5']]" ] }, "execution_count": 60, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from redis.commands.search.aggregation import AggregateRequest\n", "import redis.commands.search.reducers as reducers\n", "\n", "req = (\n", " AggregateRequest(\"*\")\n", " .group_by([\"@genre\"], reducers.avg(\"rating\").alias(\"avg_rating\"))\n", " .dialect(2)\n", " )\n", "\n", "res = client.ft(index_name).aggregate(req)\n", "res.rows" ] }, { "cell_type": "markdown", "id": "ef001bef", "metadata": {}, "source": [ "### Weighting (boosting)\n", "\n", "Sometimes you might want a search to lean more heavily towards one condition over another and weight it higher in the result set.\n", "\n", "In this example, you can see that even though `The Incredibles` isn't an `action` movie it is still the top result because it ranks highly on the fuzzy search for `%superhero%`." ] }, { "cell_type": "code", "execution_count": 61, "id": "f11af548", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[Document {'id': '15', 'payload': None, 'title': 'The Incredibles', 'genre': 'comedy', 'rating': '8', 'description': \"A family of undercover superheroes, while trying to live the quiet suburban life, are forced into action to save the world. Bob Parr (Mr. Incredible) and his wife Helen (Elastigirl) were among the world's greatest crime fighters, but now they must assume civilian identities and retreat to the suburbs to live a 'normal' life with their three children. However, the family's desire to help the world pulls them back into action when they face a new and dangerous enemy.\"},\n", " Document {'id': '0', 'payload': None, 'title': 'Explosive Pursuit', 'genre': 'action', 'rating': '7', 'description': 'A daring cop chases a notorious criminal across the city in a high-stakes game of cat and mouse.'},\n", " Document {'id': '1', 'payload': None, 'title': 'Skyfall', 'genre': 'action', 'rating': '8', 'description': 'James Bond returns to track down a dangerous new enemy who threatens global security.'}]" ] }, "execution_count": 61, "metadata": {}, "output_type": "execute_result" } ], "source": [ "query = Query('((@genre:{action}=>{$weight: 1}) | (@description:(%superhero%)=>{$weight: 10}))') \\\n", " .return_fields(\"title\", \"genre\", \"rating\", \"description\") \\\n", " .paging(0, 3) \\\n", " .dialect(2)\n", "\n", "res = client.ft(index_name).search(query)\n", "res.docs" ] }, { "cell_type": "code", "execution_count": 62, "id": "1902b43b", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "True" ] }, "execution_count": 62, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# clean up!\n", "client.flushall()" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.11.9" } }, "nbformat": 4, "nbformat_minor": 5 }