From 8a15f4913d10d6cdacd291ed3b2e883625df9339 Mon Sep 17 00:00:00 2001 From: Nitin Kanukolanu Date: Mon, 2 Mar 2026 13:33:19 -0500 Subject: [PATCH] feat: add SQLQuery support to AsyncSearchIndex (#487) - Add async SQL query execution using sql-redis AsyncSchemaRegistry and AsyncExecutor - Update sql-redis dependency to >=0.2.0 - Add integration tests for async SQL SELECT and AGGREGATE queries --- docs/user_guide/12_sql_to_redis_queries.ipynb | 2303 +++++++++-------- pyproject.toml | 4 +- redisvl/index/index.py | 57 +- tests/integration/test_sql_redis_json.py | 69 + uv.lock | 12 +- 5 files changed, 1282 insertions(+), 1163 deletions(-) diff --git a/docs/user_guide/12_sql_to_redis_queries.ipynb b/docs/user_guide/12_sql_to_redis_queries.ipynb index d70046c8..cb2aac47 100644 --- a/docs/user_guide/12_sql_to_redis_queries.ipynb +++ b/docs/user_guide/12_sql_to_redis_queries.ipynb @@ -1,1142 +1,1171 @@ { - "cells": [ - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Write SQL Queries for Redis\n", - "\n", - "While Redis does not natively support SQL, RedisVL provides a `SQLQuery` class that translates SQL-like queries into Redis queries.\n", - "\n", - "The `SQLQuery` class wraps the [`sql-redis`](https://pypi.org/project/sql-redis/) package. This package is not installed by default, so install it with:\n", - "\n", - "```bash\n", - "pip install redisvl[sql-redis]\n", - "```\n", - "\n", - "## Prerequisites\n", - "\n", - "Before you begin, ensure you have:\n", - "- Installed RedisVL with SQL support: `pip install redisvl[sql-redis]`\n", - "- A running Redis instance ([Redis 8+](https://redis.io/downloads/) or [Redis Cloud](https://redis.io/cloud))\n", - "\n", - "## What You'll Learn\n", - "\n", - "By the end of this guide, you will be able to:\n", - "- Write SQL-like queries for Redis using `SQLQuery`\n", - "- Translate SELECT, WHERE, and ORDER BY clauses to Redis queries\n", - "- Combine SQL queries with vector search\n", - "- Use aggregate functions and grouping" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Create an index to search" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": { - "execution": { - "iopub.execute_input": "2026-02-16T15:20:01.542482Z", - "iopub.status.busy": "2026-02-16T15:20:01.542270Z", - "iopub.status.idle": "2026-02-16T15:20:19.311130Z", - "shell.execute_reply": "2026-02-16T15:20:19.310567Z" - } - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/Users/tyler.hutcherson/Documents/AppliedAI/redis-vl-python/.venv/lib/python3.13/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n", - " from .autonotebook import tqdm as notebook_tqdm\n" - ] - } - ], - "source": [ - "from redisvl.utils.vectorize import HFTextVectorizer\n", - "\n", - "hf = HFTextVectorizer()\n", - "\n", - "schema = {\n", - " \"index\": {\n", - " \"name\": \"user_simple\",\n", - " \"prefix\": \"user_simple_docs\",\n", - " \"storage_type\": \"json\",\n", - " },\n", - " \"fields\": [\n", - " {\"name\": \"user\", \"type\": \"tag\"},\n", - " {\"name\": \"region\", \"type\": \"tag\"},\n", - " {\"name\": \"job\", \"type\": \"tag\"},\n", - " {\"name\": \"job_description\", \"type\": \"text\"},\n", - " {\"name\": \"age\", \"type\": \"numeric\"},\n", - " {\n", - " \"name\": \"job_embedding\",\n", - " \"type\": \"vector\",\n", - " \"attrs\": {\n", - " \"dims\": len(hf.embed(\"get embed length\")),\n", - " \"distance_metric\": \"cosine\",\n", - " \"algorithm\": \"flat\",\n", - " \"datatype\": \"float32\"\n", - " }\n", - " }\n", - " ]\n", - "}" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Create sample dataset" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": { - "execution": { - "iopub.execute_input": "2026-02-16T15:20:19.312894Z", - "iopub.status.busy": "2026-02-16T15:20:19.312512Z", - "iopub.status.idle": "2026-02-16T15:20:19.621061Z", - "shell.execute_reply": "2026-02-16T15:20:19.620563Z" - } - }, - "outputs": [], - "source": [ - "data = [\n", - " {\n", - " 'user': 'john',\n", - " 'age': 34,\n", - " 'job': 'software engineer',\n", - " 'region': 'us-west',\n", - " 'job_description': 'Designs, develops, and maintains software applications and systems.'\n", - " },\n", - " {\n", - " 'user': 'bill',\n", - " 'age': 54,\n", - " 'job': 'engineer',\n", - " 'region': 'us-central',\n", - " 'job_description': 'Applies scientific and mathematical principles to solve technical problems.'\n", - " },\n", - " {\n", - " 'user': 'mary',\n", - " 'age': 24,\n", - " 'job': 'doctor',\n", - " 'region': 'us-central',\n", - " 'job_description': 'Diagnoses and treats illnesses, injuries, and other medical conditions in the healthcare field.'\n", - " },\n", - " {\n", - " 'user': 'joe',\n", - " 'age': 27,\n", - " 'job': 'dentist',\n", - " 'region': 'us-east',\n", - " 'job_description': 'Provides oral healthcare including diagnosing and treating teeth and gum issues.'\n", - " },\n", - " {\n", - " 'user': 'stacy',\n", - " 'age': 61,\n", - " 'job': 'project manager',\n", - " 'region': 'us-west',\n", - " 'job_description': 'Plans, organizes, and oversees projects from inception to completion.'\n", - " }\n", - "]\n", - "\n", - "data = [\n", - " { \n", - " **d,\n", - " \"job_embedding\": hf.embed(f\"{d['job_description']=} {d['job']=}\"),\n", - " } \n", - " for d in data\n", - "]" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Create a `SearchIndex`\n", - "\n", - "With the schema and sample dataset ready, create a `SearchIndex`." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Bring your own Redis connection instance\n", - "\n", - "This is ideal in scenarios where you have custom settings on the connection instance or if your application will share a connection pool:" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": { - "execution": { - "iopub.execute_input": "2026-02-16T15:20:19.622451Z", - "iopub.status.busy": "2026-02-16T15:20:19.622366Z", - "iopub.status.idle": "2026-02-16T15:20:19.630721Z", - "shell.execute_reply": "2026-02-16T15:20:19.630403Z" - } - }, - "outputs": [], - "source": [ - "from redisvl.index import SearchIndex\n", - "from redis import Redis\n", - "\n", - "client = Redis.from_url(\"redis://localhost:6379\")\n", - "index = SearchIndex.from_dict(schema, redis_client=client, validate_on_load=True)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Let the index manage the connection instance\n", - "\n", - "This is ideal for simple cases:" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": { - "execution": { - "iopub.execute_input": "2026-02-16T15:20:19.632020Z", - "iopub.status.busy": "2026-02-16T15:20:19.631934Z", - "iopub.status.idle": "2026-02-16T15:20:19.633821Z", - "shell.execute_reply": "2026-02-16T15:20:19.633429Z" - } - }, - "outputs": [], - "source": [ - "index = SearchIndex.from_dict(schema, redis_url=\"redis://localhost:6379\", validate_on_load=True)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Create the index\n", - "\n", - "Now that we are connected to Redis, we need to run the create command." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": { - "execution": { - "iopub.execute_input": "2026-02-16T15:20:19.634818Z", - "iopub.status.busy": "2026-02-16T15:20:19.634741Z", - "iopub.status.idle": "2026-02-16T15:20:19.640648Z", - "shell.execute_reply": "2026-02-16T15:20:19.640244Z" - } - }, - "outputs": [], - "source": [ - "index.create(overwrite=True, drop=True)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Load Data to `SearchIndex`\n", - "\n", - "Load the sample dataset to Redis.\n", - "\n", - "### Validate data entries on load\n", - "RedisVL uses pydantic validation under the hood to ensure loaded data is valid and confirms to your schema. This setting is optional and can be configured in the `SearchIndex` class." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": { - "execution": { - "iopub.execute_input": "2026-02-16T15:20:19.641629Z", - "iopub.status.busy": "2026-02-16T15:20:19.641563Z", - "iopub.status.idle": "2026-02-16T15:20:19.751366Z", - "shell.execute_reply": "2026-02-16T15:20:19.750887Z" - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "['user_simple_docs:01KHKJGG26AR3VW2RJA381R8YK', 'user_simple_docs:01KHKJGG2R8EZP6H15MG1V4E53', 'user_simple_docs:01KHKJGG369F5R0R51PW2HP8MV', 'user_simple_docs:01KHKJGG3MGVPAZ6XEQVEWXZFC', 'user_simple_docs:01KHKJGG44ZEKJVRQJ0EF72PV7']\n" - ] - } - ], - "source": [ - "keys = index.load(data)\n", - "\n", - "print(keys)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Create a `SQLQuery` Object\n", - "\n", - "First, let's test a simple select statement such as the one below." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": { - "execution": { - "iopub.execute_input": "2026-02-16T15:20:19.752548Z", - "iopub.status.busy": "2026-02-16T15:20:19.752471Z", - "iopub.status.idle": "2026-02-16T15:20:19.754355Z", - "shell.execute_reply": "2026-02-16T15:20:19.753935Z" - } - }, - "outputs": [], - "source": [ - "from redisvl.query import SQLQuery\n", - "\n", - "sql_str = \"\"\"\n", - " SELECT user, region, job, age\n", - " FROM user_simple\n", - " WHERE age > 17\n", - " \"\"\"\n", - "\n", - "sql_query = SQLQuery(sql_str) " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Check the created query string" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": { - "execution": { - "iopub.execute_input": "2026-02-16T15:20:19.755445Z", - "iopub.status.busy": "2026-02-16T15:20:19.755366Z", - "iopub.status.idle": "2026-02-16T15:20:20.018643Z", - "shell.execute_reply": "2026-02-16T15:20:20.018223Z" - } - }, - "outputs": [ - { - "data": { - "text/plain": [ - "'FT.SEARCH user_simple \"@age:[(17 +inf]\" RETURN 4 user region job age'" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "sql_query.redis_query_string(redis_url=\"redis://localhost:6379\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Executing the query" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": { - "execution": { - "iopub.execute_input": "2026-02-16T15:20:20.019728Z", - "iopub.status.busy": "2026-02-16T15:20:20.019644Z", - "iopub.status.idle": "2026-02-16T15:20:20.026215Z", - "shell.execute_reply": "2026-02-16T15:20:20.025841Z" - } - }, - "outputs": [ - { - "data": { - "text/plain": [ - "[{'user': 'john',\n", - " 'region': 'us-west',\n", - " 'job': 'software engineer',\n", - " 'age': '34'},\n", - " {'user': 'bill', 'region': 'us-central', 'job': 'engineer', 'age': '54'},\n", - " {'user': 'mary', 'region': 'us-central', 'job': 'doctor', 'age': '24'},\n", - " {'user': 'joe', 'region': 'us-east', 'job': 'dentist', 'age': '27'},\n", - " {'user': 'stacy', 'region': 'us-west', 'job': 'project manager', 'age': '61'}]" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "results = index.query(sql_query)\n", - "results" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Additional query support\n", - "\n", - "### Conditional operators" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": { - "execution": { - "iopub.execute_input": "2026-02-16T15:20:20.027232Z", - "iopub.status.busy": "2026-02-16T15:20:20.027154Z", - "iopub.status.idle": "2026-02-16T15:20:20.036830Z", - "shell.execute_reply": "2026-02-16T15:20:20.036450Z" - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Resulting redis query: FT.SEARCH user_simple \"@age:[(17 +inf] @region:{us\\-west}\" RETURN 4 user region job age\n" - ] - }, - { - "data": { - "text/plain": [ - "[{'user': 'john',\n", - " 'region': 'us-west',\n", - " 'job': 'software engineer',\n", - " 'age': '34'},\n", - " {'user': 'stacy', 'region': 'us-west', 'job': 'project manager', 'age': '61'}]" - ] - }, - "execution_count": 10, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "sql_str = \"\"\"\n", - " SELECT user, region, job, age\n", - " FROM user_simple\n", - " WHERE age > 17 and region = 'us-west'\n", - "\"\"\"\n", - "\n", - "sql_query = SQLQuery(sql_str)\n", - "redis_query = sql_query.redis_query_string(redis_url=\"redis://localhost:6379\")\n", - "print(\"Resulting redis query: \", redis_query)\n", - "results = index.query(sql_query)\n", - "results" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": { - "execution": { - "iopub.execute_input": "2026-02-16T15:20:20.037744Z", - "iopub.status.busy": "2026-02-16T15:20:20.037670Z", - "iopub.status.idle": "2026-02-16T15:20:20.047532Z", - "shell.execute_reply": "2026-02-16T15:20:20.047144Z" - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Resulting redis query: FT.SEARCH user_simple \"((@region:{us\\-west})|(@region:{us\\-central}))\" RETURN 4 user region job age\n" - ] - }, - { - "data": { - "text/plain": [ - "[{'user': 'john',\n", - " 'region': 'us-west',\n", - " 'job': 'software engineer',\n", - " 'age': '34'},\n", - " {'user': 'bill', 'region': 'us-central', 'job': 'engineer', 'age': '54'},\n", - " {'user': 'stacy', 'region': 'us-west', 'job': 'project manager', 'age': '61'},\n", - " {'user': 'mary', 'region': 'us-central', 'job': 'doctor', 'age': '24'}]" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "sql_str = \"\"\"\n", - " SELECT user, region, job, age\n", - " FROM user_simple\n", - " WHERE region = 'us-west' or region = 'us-central'\n", - " \"\"\"\n", - "\n", - "sql_query = SQLQuery(sql_str)\n", - "redis_query = sql_query.redis_query_string(redis_url=\"redis://localhost:6379\")\n", - "print(\"Resulting redis query: \", redis_query)\n", - "results = index.query(sql_query)\n", - "results" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": { - "execution": { - "iopub.execute_input": "2026-02-16T15:20:20.048481Z", - "iopub.status.busy": "2026-02-16T15:20:20.048419Z", - "iopub.status.idle": "2026-02-16T15:20:20.057250Z", - "shell.execute_reply": "2026-02-16T15:20:20.056891Z" - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Resulting redis query: FT.SEARCH user_simple \"@job:{software engineer|engineer|pancake tester}\" RETURN 4 user region job age\n" - ] - }, - { - "data": { - "text/plain": [ - "[{'user': 'john',\n", - " 'region': 'us-west',\n", - " 'job': 'software engineer',\n", - " 'age': '34'},\n", - " {'user': 'bill', 'region': 'us-central', 'job': 'engineer', 'age': '54'}]" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# job is a tag field therefore this syntax works\n", - "sql_str = \"\"\"\n", - " SELECT user, region, job, age\n", - " FROM user_simple\n", - " WHERE job IN ('software engineer', 'engineer', 'pancake tester')\n", - " \"\"\"\n", - "\n", - "sql_query = SQLQuery(sql_str)\n", - "redis_query = sql_query.redis_query_string(redis_url=\"redis://localhost:6379\")\n", - "print(\"Resulting redis query: \", redis_query)\n", - "results = index.query(sql_query)\n", - "results" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Text based searches\n", - "\n", - "See [the docs](https://redis.io/docs/latest/develop/ai/search-and-query/query/full-text/) for available text queries in Redis.\n", - "\n", - "For more on exact matching see [here](https://redis.io/docs/latest/develop/ai/search-and-query/query/exact-match/)" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": { - "execution": { - "iopub.execute_input": "2026-02-16T15:20:20.058215Z", - "iopub.status.busy": "2026-02-16T15:20:20.058144Z", - "iopub.status.idle": "2026-02-16T15:20:20.067897Z", - "shell.execute_reply": "2026-02-16T15:20:20.067471Z" - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Resulting redis query: FT.SEARCH user_simple \"@job_description:sci*\" RETURN 5 user region job job_description age\n" - ] - }, - { - "data": { - "text/plain": [ - "[{'user': 'bill',\n", - " 'region': 'us-central',\n", - " 'job': 'engineer',\n", - " 'job_description': 'Applies scientific and mathematical principles to solve technical problems.',\n", - " 'age': '54'}]" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Prefix\n", - "sql_str = \"\"\"\n", - " SELECT user, region, job, job_description, age\n", - " FROM user_simple\n", - " WHERE job_description = 'sci*'\n", - "\"\"\"\n", - "\n", - "sql_query = SQLQuery(sql_str)\n", - "redis_query = sql_query.redis_query_string(redis_url=\"redis://localhost:6379\")\n", - "print(\"Resulting redis query: \", redis_query)\n", - "results = index.query(sql_query)\n", - "results" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": { - "execution": { - "iopub.execute_input": "2026-02-16T15:20:20.068791Z", - "iopub.status.busy": "2026-02-16T15:20:20.068717Z", - "iopub.status.idle": "2026-02-16T15:20:20.076915Z", - "shell.execute_reply": "2026-02-16T15:20:20.076527Z" - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Resulting redis query: FT.SEARCH user_simple \"@job_description:*care\" RETURN 5 user region job job_description age\n" - ] - }, - { - "data": { - "text/plain": [ - "[{'user': 'mary',\n", - " 'region': 'us-central',\n", - " 'job': 'doctor',\n", - " 'job_description': 'Diagnoses and treats illnesses, injuries, and other medical conditions in the healthcare field.',\n", - " 'age': '24'},\n", - " {'user': 'joe',\n", - " 'region': 'us-east',\n", - " 'job': 'dentist',\n", - " 'job_description': 'Provides oral healthcare including diagnosing and treating teeth and gum issues.',\n", - " 'age': '27'}]" - ] - }, - "execution_count": 14, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Suffix\n", - "sql_str = \"\"\"\n", - " SELECT user, region, job, job_description, age\n", - " FROM user_simple\n", - " WHERE job_description = '*care'\n", - "\"\"\"\n", - "\n", - "sql_query = SQLQuery(sql_str)\n", - "redis_query = sql_query.redis_query_string(redis_url=\"redis://localhost:6379\")\n", - "print(\"Resulting redis query: \", redis_query)\n", - "results = index.query(sql_query)\n", - "results" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": { - "execution": { - "iopub.execute_input": "2026-02-16T15:20:20.077848Z", - "iopub.status.busy": "2026-02-16T15:20:20.077786Z", - "iopub.status.idle": "2026-02-16T15:20:20.085797Z", - "shell.execute_reply": "2026-02-16T15:20:20.085426Z" - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Resulting redis query: FT.SEARCH user_simple \"@job_description:%diagnose%\" RETURN 5 user region job job_description age\n" - ] - }, - { - "data": { - "text/plain": [ - "[{'user': 'mary',\n", - " 'region': 'us-central',\n", - " 'job': 'doctor',\n", - " 'job_description': 'Diagnoses and treats illnesses, injuries, and other medical conditions in the healthcare field.',\n", - " 'age': '24'}]" - ] - }, - "execution_count": 15, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Fuzzy\n", - "sql_str = \"\"\"\n", - " SELECT user, region, job, job_description, age\n", - " FROM user_simple\n", - " WHERE job_description = '%diagnose%'\n", - "\"\"\"\n", - "\n", - "sql_query = SQLQuery(sql_str)\n", - "redis_query = sql_query.redis_query_string(redis_url=\"redis://localhost:6379\")\n", - "print(\"Resulting redis query: \", redis_query)\n", - "results = index.query(sql_query)\n", - "results" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "metadata": { - "execution": { - "iopub.execute_input": "2026-02-16T15:20:20.086696Z", - "iopub.status.busy": "2026-02-16T15:20:20.086635Z", - "iopub.status.idle": "2026-02-16T15:20:20.094743Z", - "shell.execute_reply": "2026-02-16T15:20:20.094356Z" - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Resulting redis query: FT.SEARCH user_simple \"@job_description:\"healthcare including\"\" RETURN 5 user region job job_description age\n" - ] - }, - { - "data": { - "text/plain": [ - "[{'user': 'joe',\n", - " 'region': 'us-east',\n", - " 'job': 'dentist',\n", - " 'job_description': 'Provides oral healthcare including diagnosing and treating teeth and gum issues.',\n", - " 'age': '27'}]" - ] - }, - "execution_count": 16, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Phrase no stop words\n", - "sql_str = \"\"\"\n", - " SELECT user, region, job, job_description, age\n", - " FROM user_simple\n", - " WHERE job_description = 'healthcare including'\n", - "\"\"\"\n", - "\n", - "sql_query = SQLQuery(sql_str)\n", - "redis_query = sql_query.redis_query_string(redis_url=\"redis://localhost:6379\")\n", - "print(\"Resulting redis query: \", redis_query)\n", - "results = index.query(sql_query)\n", - "results" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "metadata": { - "execution": { - "iopub.execute_input": "2026-02-16T15:20:20.095658Z", - "iopub.status.busy": "2026-02-16T15:20:20.095592Z", - "iopub.status.idle": "2026-02-16T15:20:20.103917Z", - "shell.execute_reply": "2026-02-16T15:20:20.103604Z" - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Resulting redis query: FT.SEARCH user_simple \"@job_description:\"diagnosing treating\"\" RETURN 5 user region job job_description age\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/Users/tyler.hutcherson/Documents/AppliedAI/redis-vl-python/.venv/lib/python3.13/site-packages/sql_redis/translator.py:136: UserWarning: Stopwords ['and'] were removed from phrase search 'diagnosing and treating'. By default, Redis does not index stopwords. To include stopwords in your index, create it with STOPWORDS 0.\n", - " return self._query_builder.build_text_condition(\n" - ] - }, - { - "data": { - "text/plain": [ - "[{'user': 'joe',\n", - " 'region': 'us-east',\n", - " 'job': 'dentist',\n", - " 'job_description': 'Provides oral healthcare including diagnosing and treating teeth and gum issues.',\n", - " 'age': '27'}]" - ] - }, - "execution_count": 17, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Phrase with stop words currently limitation of core Redis\n", - "sql_str = \"\"\"\n", - " SELECT user, region, job, job_description, age\n", - " FROM user_simple\n", - " WHERE job_description = 'diagnosing and treating'\n", - "\"\"\"\n", - "\n", - "sql_query = SQLQuery(sql_str)\n", - "redis_query = sql_query.redis_query_string(redis_url=\"redis://localhost:6379\")\n", - "print(\"Resulting redis query: \", redis_query)\n", - "results = index.query(sql_query)\n", - "results" - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "metadata": { - "execution": { - "iopub.execute_input": "2026-02-16T15:20:20.104811Z", - "iopub.status.busy": "2026-02-16T15:20:20.104747Z", - "iopub.status.idle": "2026-02-16T15:20:20.112841Z", - "shell.execute_reply": "2026-02-16T15:20:20.112495Z" - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Resulting redis query: FT.SEARCH user_simple \"@age:[40 60]\" RETURN 4 user region job age\n" - ] - }, - { - "data": { - "text/plain": [ - "[{'user': 'bill', 'region': 'us-central', 'job': 'engineer', 'age': '54'}]" - ] - }, - "execution_count": 18, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "sql_str = \"\"\"\n", - " SELECT user, region, job, age\n", - " FROM user_simple\n", - " WHERE age BETWEEN 40 and 60\n", - " \"\"\"\n", - "\n", - "sql_query = SQLQuery(sql_str)\n", - "redis_query = sql_query.redis_query_string(redis_url=\"redis://localhost:6379\")\n", - "print(\"Resulting redis query: \", redis_query)\n", - "results = index.query(sql_query)\n", - "results" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Aggregations\n", - "\n", - "See docs for redis supported reducer functions: [docs](https://redis.io/docs/latest/develop/ai/search-and-query/advanced-concepts/aggregations/#supported-groupby-reducers)." - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "metadata": { - "execution": { - "iopub.execute_input": "2026-02-16T15:20:20.113767Z", - "iopub.status.busy": "2026-02-16T15:20:20.113705Z", - "iopub.status.idle": "2026-02-16T15:20:20.123216Z", - "shell.execute_reply": "2026-02-16T15:20:20.122829Z" - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Resulting redis query: FT.AGGREGATE user_simple \"*\" LOAD 2 age region GROUPBY 1 @region REDUCE COUNT 0 AS count_age REDUCE COUNT_DISTINCT 1 @age AS count_distinct_age REDUCE MIN 1 @age AS min_age REDUCE MAX 1 @age AS max_age REDUCE AVG 1 @age AS avg_age REDUCE STDDEV 1 @age AS std_age REDUCE FIRST_VALUE 1 @age AS fist_value_age REDUCE TOLIST 1 @age AS to_list_age REDUCE QUANTILE 2 @age 0.99 AS quantile_age\n" - ] - }, - { - "data": { - "text/plain": [ - "[{'region': 'us-west',\n", - " 'count_age': '2',\n", - " 'count_distinct_age': '2',\n", - " 'min_age': '34',\n", - " 'max_age': '61',\n", - " 'avg_age': '47.5',\n", - " 'std_age': '19.091883092',\n", - " 'fist_value_age': '34',\n", - " 'to_list_age': [b'34', b'61'],\n", - " 'quantile_age': '61'},\n", - " {'region': 'us-central',\n", - " 'count_age': '2',\n", - " 'count_distinct_age': '2',\n", - " 'min_age': '24',\n", - " 'max_age': '54',\n", - " 'avg_age': '39',\n", - " 'std_age': '21.2132034356',\n", - " 'fist_value_age': '54',\n", - " 'to_list_age': [b'24', b'54'],\n", - " 'quantile_age': '54'},\n", - " {'region': 'us-east',\n", - " 'count_age': '1',\n", - " 'count_distinct_age': '1',\n", - " 'min_age': '27',\n", - " 'max_age': '27',\n", - " 'avg_age': '27',\n", - " 'std_age': '0',\n", - " 'fist_value_age': '27',\n", - " 'to_list_age': [b'27'],\n", - " 'quantile_age': '27'}]" - ] - }, - "execution_count": 19, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "sql_str = \"\"\"\n", - " SELECT\n", - " user,\n", - " COUNT(age) as count_age,\n", - " COUNT_DISTINCT(age) as count_distinct_age,\n", - " MIN(age) as min_age,\n", - " MAX(age) as max_age,\n", - " AVG(age) as avg_age,\n", - " STDEV(age) as std_age,\n", - " FIRST_VALUE(age) as fist_value_age,\n", - " ARRAY_AGG(age) as to_list_age,\n", - " QUANTILE(age, 0.99) as quantile_age\n", - " FROM user_simple\n", - " GROUP BY region\n", - " \"\"\"\n", - "\n", - "sql_query = SQLQuery(sql_str)\n", - "redis_query = sql_query.redis_query_string(redis_url=\"redis://localhost:6379\")\n", - "print(\"Resulting redis query: \", redis_query)\n", - "results = index.query(sql_query)\n", - "results" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Vector search" - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "metadata": { - "execution": { - "iopub.execute_input": "2026-02-16T15:20:20.124186Z", - "iopub.status.busy": "2026-02-16T15:20:20.124113Z", - "iopub.status.idle": "2026-02-16T15:20:20.211719Z", - "shell.execute_reply": "2026-02-16T15:20:20.211380Z" - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Resulting redis query: FT.SEARCH user_simple \"*=>[KNN 10 @job_embedding $vector AS vector_distance]\" PARAMS 2 vector $vector DIALECT 2 RETURN 4 user job job_description vector_distance SORTBY vector_distance ASC\n" - ] - }, - { - "data": { - "text/plain": [ - "[{'vector_distance': '0.82351064682',\n", - " 'user': 'bill',\n", - " 'job': 'engineer',\n", - " 'job_description': 'Applies scientific and mathematical principles to solve technical problems.'},\n", - " {'vector_distance': '0.965160369873',\n", - " 'user': 'john',\n", - " 'job': 'software engineer',\n", - " 'job_description': 'Designs, develops, and maintains software applications and systems.'},\n", - " {'vector_distance': '1.00401353836',\n", - " 'user': 'mary',\n", - " 'job': 'doctor',\n", - " 'job_description': 'Diagnoses and treats illnesses, injuries, and other medical conditions in the healthcare field.'},\n", - " {'vector_distance': '1.00626885891',\n", - " 'user': 'stacy',\n", - " 'job': 'project manager',\n", - " 'job_description': 'Plans, organizes, and oversees projects from inception to completion.'},\n", - " {'vector_distance': '1.01110625267',\n", - " 'user': 'joe',\n", - " 'job': 'dentist',\n", - " 'job_description': 'Provides oral healthcare including diagnosing and treating teeth and gum issues.'}]" - ] - }, - "execution_count": 20, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "sql_str = \"\"\"\n", - " SELECT user, job, job_description, cosine_distance(job_embedding, :vec) AS vector_distance\n", - " FROM user_simple\n", - " ORDER BY vector_distance ASC\n", - " \"\"\"\n", - "\n", - "vec = hf.embed(\"looking for someone to use base principles to solve problems\", as_buffer=True)\n", - "sql_query = SQLQuery(sql_str, params={\"vec\": vec})\n", - "\n", - "redis_query = sql_query.redis_query_string(redis_url=\"redis://localhost:6379\")\n", - "print(\"Resulting redis query: \", redis_query)\n", - "results = index.query(sql_query)\n", - "\n", - "results" - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "metadata": { - "execution": { - "iopub.execute_input": "2026-02-16T15:20:20.212904Z", - "iopub.status.busy": "2026-02-16T15:20:20.212823Z", - "iopub.status.idle": "2026-02-16T15:20:20.242415Z", - "shell.execute_reply": "2026-02-16T15:20:20.241988Z" - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Resulting redis query: FT.SEARCH user_simple \"(@region:{us\\-central})=>[KNN 10 @job_embedding $vector AS vector_distance]\" PARAMS 2 vector $vector DIALECT 2 RETURN 3 user region vector_distance SORTBY vector_distance ASC\n" - ] - }, - { - "data": { - "text/plain": [ - "[{'vector_distance': '0.82351064682', 'user': 'bill', 'region': 'us-central'},\n", - " {'vector_distance': '1.00401353836', 'user': 'mary', 'region': 'us-central'}]" - ] - }, - "execution_count": 21, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "sql_str = \"\"\"\n", - " SELECT user, region, cosine_distance(job_embedding, :vec) AS vector_distance\n", - " FROM user_simple\n", - " WHERE region = 'us-central'\n", - " ORDER BY vector_distance ASC\n", - " \"\"\"\n", - "\n", - "vec = hf.embed(\"looking for someone to use base principles to solve problems\", as_buffer=True)\n", - "sql_query = SQLQuery(sql_str, params={\"vec\": vec})\n", - "\n", - "redis_query = sql_query.redis_query_string(redis_url=\"redis://localhost:6379\")\n", - "print(\"Resulting redis query: \", redis_query)\n", - "results = index.query(sql_query)\n", - "\n", - "results" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Next Steps\n", - "\n", - "Now that you understand SQL queries for Redis, explore these related guides:\n", - "\n", - "- [Use Advanced Query Types](11_advanced_queries.ipynb) - Learn about TextQuery, HybridQuery, and MultiVectorQuery\n", - "- [Query and Filter Data](02_complex_filtering.ipynb) - Apply filters using native RedisVL query syntax\n", - "- [Getting Started](01_getting_started.ipynb) - Review the basics of RedisVL indexes" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Cleanup\n", - "\n", - "To remove all data from Redis associated with the index, use the `.clear()` method. This leaves the index in place for future insertions or updates.\n", - "\n", - "To remove everything including the index, use `.delete()` which removes both the index and the underlying data." - ] - }, - { - "cell_type": "code", - "execution_count": 22, - "metadata": { - "execution": { - "iopub.execute_input": "2026-02-16T15:20:20.243553Z", - "iopub.status.busy": "2026-02-16T15:20:20.243464Z", - "iopub.status.idle": "2026-02-16T15:20:20.245944Z", - "shell.execute_reply": "2026-02-16T15:20:20.245506Z" - } - }, - "outputs": [], - "source": [ - "index.delete()" - ] - } + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Write SQL Queries for Redis\n", + "\n", + "While Redis does not natively support SQL, RedisVL provides a `SQLQuery` class that translates SQL-like queries into Redis queries.\n", + "\n", + "The `SQLQuery` class wraps the [`sql-redis`](https://pypi.org/project/sql-redis/) package. This package is not installed by default, so install it with:\n", + "\n", + "```bash\n", + "pip install redisvl[sql-redis]\n", + "```\n", + "\n", + "## Prerequisites\n", + "\n", + "Before you begin, ensure you have:\n", + "- Installed RedisVL with SQL support: `pip install redisvl[sql-redis]`\n", + "- A running Redis instance ([Redis 8+](https://redis.io/downloads/) or [Redis Cloud](https://redis.io/cloud))\n", + "\n", + "## What You'll Learn\n", + "\n", + "By the end of this guide, you will be able to:\n", + "- Write SQL-like queries for Redis using `SQLQuery`\n", + "- Translate SELECT, WHERE, and ORDER BY clauses to Redis queries\n", + "- Combine SQL queries with vector search\n", + "- Use aggregate functions and grouping" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Create an index to search" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "execution": { + "iopub.execute_input": "2026-02-16T15:20:01.542482Z", + "iopub.status.busy": "2026-02-16T15:20:01.542270Z", + "iopub.status.idle": "2026-02-16T15:20:19.311130Z", + "shell.execute_reply": "2026-02-16T15:20:19.310567Z" + } + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/tyler.hutcherson/Documents/AppliedAI/redis-vl-python/.venv/lib/python3.13/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n", + " from .autonotebook import tqdm as notebook_tqdm\n" + ] + } + ], + "source": [ + "from redisvl.utils.vectorize import HFTextVectorizer\n", + "\n", + "hf = HFTextVectorizer()\n", + "\n", + "schema = {\n", + " \"index\": {\n", + " \"name\": \"user_simple\",\n", + " \"prefix\": \"user_simple_docs\",\n", + " \"storage_type\": \"json\",\n", + " },\n", + " \"fields\": [\n", + " {\"name\": \"user\", \"type\": \"tag\"},\n", + " {\"name\": \"region\", \"type\": \"tag\"},\n", + " {\"name\": \"job\", \"type\": \"tag\"},\n", + " {\"name\": \"job_description\", \"type\": \"text\"},\n", + " {\"name\": \"age\", \"type\": \"numeric\"},\n", + " {\n", + " \"name\": \"job_embedding\",\n", + " \"type\": \"vector\",\n", + " \"attrs\": {\n", + " \"dims\": len(hf.embed(\"get embed length\")),\n", + " \"distance_metric\": \"cosine\",\n", + " \"algorithm\": \"flat\",\n", + " \"datatype\": \"float32\"\n", + " }\n", + " }\n", + " ]\n", + "}" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Create sample dataset" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "execution": { + "iopub.execute_input": "2026-02-16T15:20:19.312894Z", + "iopub.status.busy": "2026-02-16T15:20:19.312512Z", + "iopub.status.idle": "2026-02-16T15:20:19.621061Z", + "shell.execute_reply": "2026-02-16T15:20:19.620563Z" + } + }, + "outputs": [], + "source": [ + "data = [\n", + " {\n", + " 'user': 'john',\n", + " 'age': 34,\n", + " 'job': 'software engineer',\n", + " 'region': 'us-west',\n", + " 'job_description': 'Designs, develops, and maintains software applications and systems.'\n", + " },\n", + " {\n", + " 'user': 'bill',\n", + " 'age': 54,\n", + " 'job': 'engineer',\n", + " 'region': 'us-central',\n", + " 'job_description': 'Applies scientific and mathematical principles to solve technical problems.'\n", + " },\n", + " {\n", + " 'user': 'mary',\n", + " 'age': 24,\n", + " 'job': 'doctor',\n", + " 'region': 'us-central',\n", + " 'job_description': 'Diagnoses and treats illnesses, injuries, and other medical conditions in the healthcare field.'\n", + " },\n", + " {\n", + " 'user': 'joe',\n", + " 'age': 27,\n", + " 'job': 'dentist',\n", + " 'region': 'us-east',\n", + " 'job_description': 'Provides oral healthcare including diagnosing and treating teeth and gum issues.'\n", + " },\n", + " {\n", + " 'user': 'stacy',\n", + " 'age': 61,\n", + " 'job': 'project manager',\n", + " 'region': 'us-west',\n", + " 'job_description': 'Plans, organizes, and oversees projects from inception to completion.'\n", + " }\n", + "]\n", + "\n", + "data = [\n", + " { \n", + " **d,\n", + " \"job_embedding\": hf.embed(f\"{d['job_description']=} {d['job']=}\"),\n", + " } \n", + " for d in data\n", + "]" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Create a `SearchIndex`\n", + "\n", + "With the schema and sample dataset ready, create a `SearchIndex`." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Bring your own Redis connection instance\n", + "\n", + "This is ideal in scenarios where you have custom settings on the connection instance or if your application will share a connection pool:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "execution": { + "iopub.execute_input": "2026-02-16T15:20:19.622451Z", + "iopub.status.busy": "2026-02-16T15:20:19.622366Z", + "iopub.status.idle": "2026-02-16T15:20:19.630721Z", + "shell.execute_reply": "2026-02-16T15:20:19.630403Z" + } + }, + "outputs": [], + "source": [ + "from redisvl.index import SearchIndex\n", + "from redis import Redis\n", + "\n", + "client = Redis.from_url(\"redis://localhost:6379\")\n", + "index = SearchIndex.from_dict(schema, redis_client=client, validate_on_load=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Let the index manage the connection instance\n", + "\n", + "This is ideal for simple cases:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "execution": { + "iopub.execute_input": "2026-02-16T15:20:19.632020Z", + "iopub.status.busy": "2026-02-16T15:20:19.631934Z", + "iopub.status.idle": "2026-02-16T15:20:19.633821Z", + "shell.execute_reply": "2026-02-16T15:20:19.633429Z" + } + }, + "outputs": [], + "source": [ + "index = SearchIndex.from_dict(schema, redis_url=\"redis://localhost:6379\", validate_on_load=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Create the index\n", + "\n", + "Now that we are connected to Redis, we need to run the create command." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "execution": { + "iopub.execute_input": "2026-02-16T15:20:19.634818Z", + "iopub.status.busy": "2026-02-16T15:20:19.634741Z", + "iopub.status.idle": "2026-02-16T15:20:19.640648Z", + "shell.execute_reply": "2026-02-16T15:20:19.640244Z" + } + }, + "outputs": [], + "source": [ + "index.create(overwrite=True, drop=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Load Data to `SearchIndex`\n", + "\n", + "Load the sample dataset to Redis.\n", + "\n", + "### Validate data entries on load\n", + "RedisVL uses pydantic validation under the hood to ensure loaded data is valid and confirms to your schema. This setting is optional and can be configured in the `SearchIndex` class." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "execution": { + "iopub.execute_input": "2026-02-16T15:20:19.641629Z", + "iopub.status.busy": "2026-02-16T15:20:19.641563Z", + "iopub.status.idle": "2026-02-16T15:20:19.751366Z", + "shell.execute_reply": "2026-02-16T15:20:19.750887Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "['user_simple_docs:01KHKJGG26AR3VW2RJA381R8YK', 'user_simple_docs:01KHKJGG2R8EZP6H15MG1V4E53', 'user_simple_docs:01KHKJGG369F5R0R51PW2HP8MV', 'user_simple_docs:01KHKJGG3MGVPAZ6XEQVEWXZFC', 'user_simple_docs:01KHKJGG44ZEKJVRQJ0EF72PV7']\n" + ] + } + ], + "source": [ + "keys = index.load(data)\n", + "\n", + "print(keys)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Create a `SQLQuery` Object\n", + "\n", + "First, let's test a simple select statement such as the one below." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "execution": { + "iopub.execute_input": "2026-02-16T15:20:19.752548Z", + "iopub.status.busy": "2026-02-16T15:20:19.752471Z", + "iopub.status.idle": "2026-02-16T15:20:19.754355Z", + "shell.execute_reply": "2026-02-16T15:20:19.753935Z" + } + }, + "outputs": [], + "source": [ + "from redisvl.query import SQLQuery\n", + "\n", + "sql_str = \"\"\"\n", + " SELECT user, region, job, age\n", + " FROM user_simple\n", + " WHERE age > 17\n", + " \"\"\"\n", + "\n", + "sql_query = SQLQuery(sql_str) " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Check the created query string" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "execution": { + "iopub.execute_input": "2026-02-16T15:20:19.755445Z", + "iopub.status.busy": "2026-02-16T15:20:19.755366Z", + "iopub.status.idle": "2026-02-16T15:20:20.018643Z", + "shell.execute_reply": "2026-02-16T15:20:20.018223Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'FT.SEARCH user_simple \"@age:[(17 +inf]\" RETURN 4 user region job age'" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sql_query.redis_query_string(redis_url=\"redis://localhost:6379\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Executing the query" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "execution": { + "iopub.execute_input": "2026-02-16T15:20:20.019728Z", + "iopub.status.busy": "2026-02-16T15:20:20.019644Z", + "iopub.status.idle": "2026-02-16T15:20:20.026215Z", + "shell.execute_reply": "2026-02-16T15:20:20.025841Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[{'user': 'john',\n", + " 'region': 'us-west',\n", + " 'job': 'software engineer',\n", + " 'age': '34'},\n", + " {'user': 'bill', 'region': 'us-central', 'job': 'engineer', 'age': '54'},\n", + " {'user': 'mary', 'region': 'us-central', 'job': 'doctor', 'age': '24'},\n", + " {'user': 'joe', 'region': 'us-east', 'job': 'dentist', 'age': '27'},\n", + " {'user': 'stacy', 'region': 'us-west', 'job': 'project manager', 'age': '61'}]" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "results = index.query(sql_query)\n", + "results" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Additional query support\n", + "\n", + "### Conditional operators" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "execution": { + "iopub.execute_input": "2026-02-16T15:20:20.027232Z", + "iopub.status.busy": "2026-02-16T15:20:20.027154Z", + "iopub.status.idle": "2026-02-16T15:20:20.036830Z", + "shell.execute_reply": "2026-02-16T15:20:20.036450Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Resulting redis query: FT.SEARCH user_simple \"@age:[(17 +inf] @region:{us\\-west}\" RETURN 4 user region job age\n" + ] + }, + { + "data": { + "text/plain": [ + "[{'user': 'john',\n", + " 'region': 'us-west',\n", + " 'job': 'software engineer',\n", + " 'age': '34'},\n", + " {'user': 'stacy', 'region': 'us-west', 'job': 'project manager', 'age': '61'}]" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sql_str = \"\"\"\n", + " SELECT user, region, job, age\n", + " FROM user_simple\n", + " WHERE age > 17 and region = 'us-west'\n", + "\"\"\"\n", + "\n", + "sql_query = SQLQuery(sql_str)\n", + "redis_query = sql_query.redis_query_string(redis_url=\"redis://localhost:6379\")\n", + "print(\"Resulting redis query: \", redis_query)\n", + "results = index.query(sql_query)\n", + "results" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "execution": { + "iopub.execute_input": "2026-02-16T15:20:20.037744Z", + "iopub.status.busy": "2026-02-16T15:20:20.037670Z", + "iopub.status.idle": "2026-02-16T15:20:20.047532Z", + "shell.execute_reply": "2026-02-16T15:20:20.047144Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Resulting redis query: FT.SEARCH user_simple \"((@region:{us\\-west})|(@region:{us\\-central}))\" RETURN 4 user region job age\n" + ] + }, + { + "data": { + "text/plain": [ + "[{'user': 'john',\n", + " 'region': 'us-west',\n", + " 'job': 'software engineer',\n", + " 'age': '34'},\n", + " {'user': 'bill', 'region': 'us-central', 'job': 'engineer', 'age': '54'},\n", + " {'user': 'stacy', 'region': 'us-west', 'job': 'project manager', 'age': '61'},\n", + " {'user': 'mary', 'region': 'us-central', 'job': 'doctor', 'age': '24'}]" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sql_str = \"\"\"\n", + " SELECT user, region, job, age\n", + " FROM user_simple\n", + " WHERE region = 'us-west' or region = 'us-central'\n", + " \"\"\"\n", + "\n", + "sql_query = SQLQuery(sql_str)\n", + "redis_query = sql_query.redis_query_string(redis_url=\"redis://localhost:6379\")\n", + "print(\"Resulting redis query: \", redis_query)\n", + "results = index.query(sql_query)\n", + "results" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "execution": { + "iopub.execute_input": "2026-02-16T15:20:20.048481Z", + "iopub.status.busy": "2026-02-16T15:20:20.048419Z", + "iopub.status.idle": "2026-02-16T15:20:20.057250Z", + "shell.execute_reply": "2026-02-16T15:20:20.056891Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Resulting redis query: FT.SEARCH user_simple \"@job:{software engineer|engineer|pancake tester}\" RETURN 4 user region job age\n" + ] + }, + { + "data": { + "text/plain": [ + "[{'user': 'john',\n", + " 'region': 'us-west',\n", + " 'job': 'software engineer',\n", + " 'age': '34'},\n", + " {'user': 'bill', 'region': 'us-central', 'job': 'engineer', 'age': '54'}]" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# job is a tag field therefore this syntax works\n", + "sql_str = \"\"\"\n", + " SELECT user, region, job, age\n", + " FROM user_simple\n", + " WHERE job IN ('software engineer', 'engineer', 'pancake tester')\n", + " \"\"\"\n", + "\n", + "sql_query = SQLQuery(sql_str)\n", + "redis_query = sql_query.redis_query_string(redis_url=\"redis://localhost:6379\")\n", + "print(\"Resulting redis query: \", redis_query)\n", + "results = index.query(sql_query)\n", + "results" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Text based searches\n", + "\n", + "See [the docs](https://redis.io/docs/latest/develop/ai/search-and-query/query/full-text/) for available text queries in Redis.\n", + "\n", + "For more on exact matching see [here](https://redis.io/docs/latest/develop/ai/search-and-query/query/exact-match/)" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "execution": { + "iopub.execute_input": "2026-02-16T15:20:20.058215Z", + "iopub.status.busy": "2026-02-16T15:20:20.058144Z", + "iopub.status.idle": "2026-02-16T15:20:20.067897Z", + "shell.execute_reply": "2026-02-16T15:20:20.067471Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Resulting redis query: FT.SEARCH user_simple \"@job_description:sci*\" RETURN 5 user region job job_description age\n" + ] + }, + { + "data": { + "text/plain": [ + "[{'user': 'bill',\n", + " 'region': 'us-central',\n", + " 'job': 'engineer',\n", + " 'job_description': 'Applies scientific and mathematical principles to solve technical problems.',\n", + " 'age': '54'}]" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } ], + "source": [ + "# Prefix\n", + "sql_str = \"\"\"\n", + " SELECT user, region, job, job_description, age\n", + " FROM user_simple\n", + " WHERE job_description = 'sci*'\n", + "\"\"\"\n", + "\n", + "sql_query = SQLQuery(sql_str)\n", + "redis_query = sql_query.redis_query_string(redis_url=\"redis://localhost:6379\")\n", + "print(\"Resulting redis query: \", redis_query)\n", + "results = index.query(sql_query)\n", + "results" + ] + }, + { + "cell_type": "code", + "execution_count": 14, "metadata": { - "kernelspec": { - "display_name": ".venv", - "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.13.2" - } + "execution": { + "iopub.execute_input": "2026-02-16T15:20:20.068791Z", + "iopub.status.busy": "2026-02-16T15:20:20.068717Z", + "iopub.status.idle": "2026-02-16T15:20:20.076915Z", + "shell.execute_reply": "2026-02-16T15:20:20.076527Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Resulting redis query: FT.SEARCH user_simple \"@job_description:*care\" RETURN 5 user region job job_description age\n" + ] + }, + { + "data": { + "text/plain": [ + "[{'user': 'mary',\n", + " 'region': 'us-central',\n", + " 'job': 'doctor',\n", + " 'job_description': 'Diagnoses and treats illnesses, injuries, and other medical conditions in the healthcare field.',\n", + " 'age': '24'},\n", + " {'user': 'joe',\n", + " 'region': 'us-east',\n", + " 'job': 'dentist',\n", + " 'job_description': 'Provides oral healthcare including diagnosing and treating teeth and gum issues.',\n", + " 'age': '27'}]" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Suffix\n", + "sql_str = \"\"\"\n", + " SELECT user, region, job, job_description, age\n", + " FROM user_simple\n", + " WHERE job_description = '*care'\n", + "\"\"\"\n", + "\n", + "sql_query = SQLQuery(sql_str)\n", + "redis_query = sql_query.redis_query_string(redis_url=\"redis://localhost:6379\")\n", + "print(\"Resulting redis query: \", redis_query)\n", + "results = index.query(sql_query)\n", + "results" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": { + "execution": { + "iopub.execute_input": "2026-02-16T15:20:20.077848Z", + "iopub.status.busy": "2026-02-16T15:20:20.077786Z", + "iopub.status.idle": "2026-02-16T15:20:20.085797Z", + "shell.execute_reply": "2026-02-16T15:20:20.085426Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Resulting redis query: FT.SEARCH user_simple \"@job_description:%diagnose%\" RETURN 5 user region job job_description age\n" + ] + }, + { + "data": { + "text/plain": [ + "[{'user': 'mary',\n", + " 'region': 'us-central',\n", + " 'job': 'doctor',\n", + " 'job_description': 'Diagnoses and treats illnesses, injuries, and other medical conditions in the healthcare field.',\n", + " 'age': '24'}]" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Fuzzy\n", + "sql_str = \"\"\"\n", + " SELECT user, region, job, job_description, age\n", + " FROM user_simple\n", + " WHERE job_description = '%diagnose%'\n", + "\"\"\"\n", + "\n", + "sql_query = SQLQuery(sql_str)\n", + "redis_query = sql_query.redis_query_string(redis_url=\"redis://localhost:6379\")\n", + "print(\"Resulting redis query: \", redis_query)\n", + "results = index.query(sql_query)\n", + "results" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": { + "execution": { + "iopub.execute_input": "2026-02-16T15:20:20.086696Z", + "iopub.status.busy": "2026-02-16T15:20:20.086635Z", + "iopub.status.idle": "2026-02-16T15:20:20.094743Z", + "shell.execute_reply": "2026-02-16T15:20:20.094356Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Resulting redis query: FT.SEARCH user_simple \"@job_description:\"healthcare including\"\" RETURN 5 user region job job_description age\n" + ] + }, + { + "data": { + "text/plain": [ + "[{'user': 'joe',\n", + " 'region': 'us-east',\n", + " 'job': 'dentist',\n", + " 'job_description': 'Provides oral healthcare including diagnosing and treating teeth and gum issues.',\n", + " 'age': '27'}]" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Phrase no stop words\n", + "sql_str = \"\"\"\n", + " SELECT user, region, job, job_description, age\n", + " FROM user_simple\n", + " WHERE job_description = 'healthcare including'\n", + "\"\"\"\n", + "\n", + "sql_query = SQLQuery(sql_str)\n", + "redis_query = sql_query.redis_query_string(redis_url=\"redis://localhost:6379\")\n", + "print(\"Resulting redis query: \", redis_query)\n", + "results = index.query(sql_query)\n", + "results" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": { + "execution": { + "iopub.execute_input": "2026-02-16T15:20:20.095658Z", + "iopub.status.busy": "2026-02-16T15:20:20.095592Z", + "iopub.status.idle": "2026-02-16T15:20:20.103917Z", + "shell.execute_reply": "2026-02-16T15:20:20.103604Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Resulting redis query: FT.SEARCH user_simple \"@job_description:\"diagnosing treating\"\" RETURN 5 user region job job_description age\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/tyler.hutcherson/Documents/AppliedAI/redis-vl-python/.venv/lib/python3.13/site-packages/sql_redis/translator.py:136: UserWarning: Stopwords ['and'] were removed from phrase search 'diagnosing and treating'. By default, Redis does not index stopwords. To include stopwords in your index, create it with STOPWORDS 0.\n", + " return self._query_builder.build_text_condition(\n" + ] + }, + { + "data": { + "text/plain": [ + "[{'user': 'joe',\n", + " 'region': 'us-east',\n", + " 'job': 'dentist',\n", + " 'job_description': 'Provides oral healthcare including diagnosing and treating teeth and gum issues.',\n", + " 'age': '27'}]" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Phrase with stop words currently limitation of core Redis\n", + "sql_str = \"\"\"\n", + " SELECT user, region, job, job_description, age\n", + " FROM user_simple\n", + " WHERE job_description = 'diagnosing and treating'\n", + "\"\"\"\n", + "\n", + "sql_query = SQLQuery(sql_str)\n", + "redis_query = sql_query.redis_query_string(redis_url=\"redis://localhost:6379\")\n", + "print(\"Resulting redis query: \", redis_query)\n", + "results = index.query(sql_query)\n", + "results" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": { + "execution": { + "iopub.execute_input": "2026-02-16T15:20:20.104811Z", + "iopub.status.busy": "2026-02-16T15:20:20.104747Z", + "iopub.status.idle": "2026-02-16T15:20:20.112841Z", + "shell.execute_reply": "2026-02-16T15:20:20.112495Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Resulting redis query: FT.SEARCH user_simple \"@age:[40 60]\" RETURN 4 user region job age\n" + ] + }, + { + "data": { + "text/plain": [ + "[{'user': 'bill', 'region': 'us-central', 'job': 'engineer', 'age': '54'}]" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sql_str = \"\"\"\n", + " SELECT user, region, job, age\n", + " FROM user_simple\n", + " WHERE age BETWEEN 40 and 60\n", + " \"\"\"\n", + "\n", + "sql_query = SQLQuery(sql_str)\n", + "redis_query = sql_query.redis_query_string(redis_url=\"redis://localhost:6379\")\n", + "print(\"Resulting redis query: \", redis_query)\n", + "results = index.query(sql_query)\n", + "results" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Aggregations\n", + "\n", + "See docs for redis supported reducer functions: [docs](https://redis.io/docs/latest/develop/ai/search-and-query/advanced-concepts/aggregations/#supported-groupby-reducers)." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": { + "execution": { + "iopub.execute_input": "2026-02-16T15:20:20.113767Z", + "iopub.status.busy": "2026-02-16T15:20:20.113705Z", + "iopub.status.idle": "2026-02-16T15:20:20.123216Z", + "shell.execute_reply": "2026-02-16T15:20:20.122829Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Resulting redis query: FT.AGGREGATE user_simple \"*\" LOAD 2 age region GROUPBY 1 @region REDUCE COUNT 0 AS count_age REDUCE COUNT_DISTINCT 1 @age AS count_distinct_age REDUCE MIN 1 @age AS min_age REDUCE MAX 1 @age AS max_age REDUCE AVG 1 @age AS avg_age REDUCE STDDEV 1 @age AS std_age REDUCE FIRST_VALUE 1 @age AS fist_value_age REDUCE TOLIST 1 @age AS to_list_age REDUCE QUANTILE 2 @age 0.99 AS quantile_age\n" + ] + }, + { + "data": { + "text/plain": [ + "[{'region': 'us-west',\n", + " 'count_age': '2',\n", + " 'count_distinct_age': '2',\n", + " 'min_age': '34',\n", + " 'max_age': '61',\n", + " 'avg_age': '47.5',\n", + " 'std_age': '19.091883092',\n", + " 'fist_value_age': '34',\n", + " 'to_list_age': [b'34', b'61'],\n", + " 'quantile_age': '61'},\n", + " {'region': 'us-central',\n", + " 'count_age': '2',\n", + " 'count_distinct_age': '2',\n", + " 'min_age': '24',\n", + " 'max_age': '54',\n", + " 'avg_age': '39',\n", + " 'std_age': '21.2132034356',\n", + " 'fist_value_age': '54',\n", + " 'to_list_age': [b'24', b'54'],\n", + " 'quantile_age': '54'},\n", + " {'region': 'us-east',\n", + " 'count_age': '1',\n", + " 'count_distinct_age': '1',\n", + " 'min_age': '27',\n", + " 'max_age': '27',\n", + " 'avg_age': '27',\n", + " 'std_age': '0',\n", + " 'fist_value_age': '27',\n", + " 'to_list_age': [b'27'],\n", + " 'quantile_age': '27'}]" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sql_str = \"\"\"\n", + " SELECT\n", + " user,\n", + " COUNT(age) as count_age,\n", + " COUNT_DISTINCT(age) as count_distinct_age,\n", + " MIN(age) as min_age,\n", + " MAX(age) as max_age,\n", + " AVG(age) as avg_age,\n", + " STDEV(age) as std_age,\n", + " FIRST_VALUE(age) as fist_value_age,\n", + " ARRAY_AGG(age) as to_list_age,\n", + " QUANTILE(age, 0.99) as quantile_age\n", + " FROM user_simple\n", + " GROUP BY region\n", + " \"\"\"\n", + "\n", + "sql_query = SQLQuery(sql_str)\n", + "redis_query = sql_query.redis_query_string(redis_url=\"redis://localhost:6379\")\n", + "print(\"Resulting redis query: \", redis_query)\n", + "results = index.query(sql_query)\n", + "results" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Vector search" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": { + "execution": { + "iopub.execute_input": "2026-02-16T15:20:20.124186Z", + "iopub.status.busy": "2026-02-16T15:20:20.124113Z", + "iopub.status.idle": "2026-02-16T15:20:20.211719Z", + "shell.execute_reply": "2026-02-16T15:20:20.211380Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Resulting redis query: FT.SEARCH user_simple \"*=>[KNN 10 @job_embedding $vector AS vector_distance]\" PARAMS 2 vector $vector DIALECT 2 RETURN 4 user job job_description vector_distance SORTBY vector_distance ASC\n" + ] + }, + { + "data": { + "text/plain": [ + "[{'vector_distance': '0.82351064682',\n", + " 'user': 'bill',\n", + " 'job': 'engineer',\n", + " 'job_description': 'Applies scientific and mathematical principles to solve technical problems.'},\n", + " {'vector_distance': '0.965160369873',\n", + " 'user': 'john',\n", + " 'job': 'software engineer',\n", + " 'job_description': 'Designs, develops, and maintains software applications and systems.'},\n", + " {'vector_distance': '1.00401353836',\n", + " 'user': 'mary',\n", + " 'job': 'doctor',\n", + " 'job_description': 'Diagnoses and treats illnesses, injuries, and other medical conditions in the healthcare field.'},\n", + " {'vector_distance': '1.00626885891',\n", + " 'user': 'stacy',\n", + " 'job': 'project manager',\n", + " 'job_description': 'Plans, organizes, and oversees projects from inception to completion.'},\n", + " {'vector_distance': '1.01110625267',\n", + " 'user': 'joe',\n", + " 'job': 'dentist',\n", + " 'job_description': 'Provides oral healthcare including diagnosing and treating teeth and gum issues.'}]" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sql_str = \"\"\"\n", + " SELECT user, job, job_description, cosine_distance(job_embedding, :vec) AS vector_distance\n", + " FROM user_simple\n", + " ORDER BY vector_distance ASC\n", + " \"\"\"\n", + "\n", + "vec = hf.embed(\"looking for someone to use base principles to solve problems\", as_buffer=True)\n", + "sql_query = SQLQuery(sql_str, params={\"vec\": vec})\n", + "\n", + "redis_query = sql_query.redis_query_string(redis_url=\"redis://localhost:6379\")\n", + "print(\"Resulting redis query: \", redis_query)\n", + "results = index.query(sql_query)\n", + "\n", + "results" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": { + "execution": { + "iopub.execute_input": "2026-02-16T15:20:20.212904Z", + "iopub.status.busy": "2026-02-16T15:20:20.212823Z", + "iopub.status.idle": "2026-02-16T15:20:20.242415Z", + "shell.execute_reply": "2026-02-16T15:20:20.241988Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Resulting redis query: FT.SEARCH user_simple \"(@region:{us\\-central})=>[KNN 10 @job_embedding $vector AS vector_distance]\" PARAMS 2 vector $vector DIALECT 2 RETURN 3 user region vector_distance SORTBY vector_distance ASC\n" + ] + }, + { + "data": { + "text/plain": [ + "[{'vector_distance': '0.82351064682', 'user': 'bill', 'region': 'us-central'},\n", + " {'vector_distance': '1.00401353836', 'user': 'mary', 'region': 'us-central'}]" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sql_str = \"\"\"\n", + " SELECT user, region, cosine_distance(job_embedding, :vec) AS vector_distance\n", + " FROM user_simple\n", + " WHERE region = 'us-central'\n", + " ORDER BY vector_distance ASC\n", + " \"\"\"\n", + "\n", + "vec = hf.embed(\"looking for someone to use base principles to solve problems\", as_buffer=True)\n", + "sql_query = SQLQuery(sql_str, params={\"vec\": vec})\n", + "\n", + "redis_query = sql_query.redis_query_string(redis_url=\"redis://localhost:6379\")\n", + "print(\"Resulting redis query: \", redis_query)\n", + "results = index.query(sql_query)\n", + "\n", + "results" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Async Support\n", + "\n", + "SQL queries also work with `AsyncSearchIndex` for async applications:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from redisvl.index import AsyncSearchIndex\n", + "from redisvl.query import SQLQuery\n", + "\n", + "# Create async index\n", + "async_index = AsyncSearchIndex.from_dict(schema, redis_url=\"redis://localhost:6379\")\n", + "\n", + "# Execute SQL query asynchronously\n", + "sql_query = SQLQuery(f\"SELECT user, age FROM {async_index.name} WHERE age > 30\")\n", + "results = await async_index.query(sql_query)\n", + "\n", + "# Cleanup\n", + "await async_index.disconnect()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Next Steps\n", + "\n", + "Now that you understand SQL queries for Redis, explore these related guides:\n", + "\n", + "- [Use Advanced Query Types](11_advanced_queries.ipynb) - Learn about TextQuery, HybridQuery, and MultiVectorQuery\n", + "- [Query and Filter Data](02_complex_filtering.ipynb) - Apply filters using native RedisVL query syntax\n", + "- [Getting Started](01_getting_started.ipynb) - Review the basics of RedisVL indexes" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Cleanup\n", + "\n", + "To remove all data from Redis associated with the index, use the `.clear()` method. This leaves the index in place for future insertions or updates.\n", + "\n", + "To remove everything including the index, use `.delete()` which removes both the index and the underlying data." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": { + "execution": { + "iopub.execute_input": "2026-02-16T15:20:20.243553Z", + "iopub.status.busy": "2026-02-16T15:20:20.243464Z", + "iopub.status.idle": "2026-02-16T15:20:20.245944Z", + "shell.execute_reply": "2026-02-16T15:20:20.245506Z" + } + }, + "outputs": [], + "source": [ + "index.delete()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 }, - "nbformat": 4, - "nbformat_minor": 4 + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.2" + } + }, + "nbformat": 4, + "nbformat_minor": 4 } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 7e35f19e..717e4bbb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,7 +54,7 @@ pillow = [ "pillow>=11.3.0", ] sql-redis = [ - "sql-redis>=0.1.2", + "sql-redis>=0.2.0", ] all = [ "mistralai>=1.0.0", @@ -69,7 +69,7 @@ all = [ "boto3>=1.36.0,<2", "urllib3<2.2.0", "pillow>=11.3.0", - "sql-redis>=0.1.1", + "sql-redis>=0.2.0", ] [project.urls] diff --git a/redisvl/index/index.py b/redisvl/index/index.py index c1abd011..a2837793 100644 --- a/redisvl/index/index.py +++ b/redisvl/index/index.py @@ -957,19 +957,7 @@ def _sql_query(self, sql_query: SQLQuery) -> List[Dict[str, Any]]: # Execute the query with any params result = executor.execute(sql_query.sql, params=sql_query.params) - # Decode bytes to strings in the results (Redis may return bytes) - decoded_rows = [] - for row in result.rows: - decoded_row = {} - for key, value in row.items(): - # Decode key if bytes - str_key = key.decode("utf-8") if isinstance(key, bytes) else key - # Decode value if bytes - str_value = value.decode("utf-8") if isinstance(value, bytes) else value - decoded_row[str_key] = str_value - decoded_rows.append(decoded_row) - - return decoded_rows + return [convert_bytes(row) for row in result.rows] def aggregate(self, *args, **kwargs) -> "AggregateResult": """Perform an aggregation operation against the index. @@ -1180,7 +1168,7 @@ def query( handles post-processing of the search. Args: - query (Union[BaseQuery, AggregateQuery, HybridQuery]): The query to run. + query (Union[BaseQuery, AggregationQuery, HybridQuery]): The query to run. Returns: List[Result]: A list of search results. @@ -2099,16 +2087,47 @@ async def _query(self, query: BaseQuery) -> List[Dict[str, Any]]: results = await self.search(query.query, query_params=query.params) return process_results(results, query=query, schema=self.schema) + async def _sql_query(self, sql_query: SQLQuery) -> List[Dict[str, Any]]: + """Asynchronously execute a SQL query and return results. + + Args: + sql_query: The SQLQuery object containing the SQL statement. + + Returns: + List of dictionaries containing the query results. + + Raises: + ImportError: If sql-redis package is not installed. + """ + try: + from sql_redis import AsyncExecutor, AsyncSchemaRegistry + except ImportError: + raise ImportError( + "sql-redis is required for SQL query support. " + "Install it with: pip install redisvl[sql-redis]" + ) + + client = await self._get_client() + registry = AsyncSchemaRegistry(client) + await registry.load_all() # Loads index schemas from Redis asynchronously + + executor = AsyncExecutor(client, registry) + + # Execute the query with any params asynchronously + result = await executor.execute(sql_query.sql, params=sql_query.params) + + return [convert_bytes(row) for row in result.rows] + async def query( - self, query: Union[BaseQuery, AggregationQuery, HybridQuery] + self, query: Union[BaseQuery, AggregationQuery, HybridQuery, SQLQuery] ) -> List[Dict[str, Any]]: """Asynchronously execute a query on the index. - This method takes a BaseQuery, AggregationQuery, or HybridQuery object directly, runs - the search, and handles post-processing of the search. + This method takes a BaseQuery, AggregationQuery, HybridQuery, or SQLQuery object + directly, runs the search, and handles post-processing of the search. Args: - query (Union[BaseQuery, AggregateQuery, HybridQuery]): The query to run. + query (Union[BaseQuery, AggregationQuery, HybridQuery, SQLQuery]): The query to run. Returns: List[Result]: A list of search results. @@ -2127,6 +2146,8 @@ async def query( """ if isinstance(query, AggregationQuery): return await self._aggregate(query) + elif isinstance(query, SQLQuery): + return await self._sql_query(query) elif isinstance(query, HybridQuery): return await self._hybrid_search(query) else: diff --git a/tests/integration/test_sql_redis_json.py b/tests/integration/test_sql_redis_json.py index 76191af0..c9df3014 100644 --- a/tests/integration/test_sql_redis_json.py +++ b/tests/integration/test_sql_redis_json.py @@ -1141,3 +1141,72 @@ def test_vector_search_with_prefilter_redis_query_string( assert "Science Fiction" in redis_cmd or "Science\\ Fiction" in redis_cmd assert "=>[KNN" in redis_cmd assert "KNN 3" in redis_cmd + + +# ============================================================================= +# Async SQLQuery Tests +# ============================================================================= + +from redisvl.index import AsyncSearchIndex + + +@pytest.fixture +async def async_sql_index(redis_url, worker_id): + """Create an async products index for SQL query testing.""" + unique_id = str(uuid.uuid4())[:8] + index_name = f"async_sql_products_{worker_id}_{unique_id}" + + index = AsyncSearchIndex.from_dict( + { + "index": { + "name": index_name, + "prefix": f"async_product_{worker_id}_{unique_id}", + "storage_type": "json", + }, + "fields": [ + {"name": "title", "type": "text", "attrs": {"sortable": True}}, + {"name": "price", "type": "numeric", "attrs": {"sortable": True}}, + {"name": "category", "type": "tag", "attrs": {"sortable": True}}, + ], + }, + redis_url=redis_url, + ) + + await index.create(overwrite=True) + + # Load test data + products = [ + {"title": "Gaming Laptop", "price": 899, "category": "electronics"}, + {"title": "Budget Laptop", "price": 499, "category": "electronics"}, + {"title": "Python Book", "price": 45, "category": "books"}, + ] + + await index.load(products) + + yield index + + # Cleanup + await index.delete(drop=True) + + +class TestAsyncSQLQuery: + """Tests for async SQLQuery support in AsyncSearchIndex.""" + + @pytest.mark.asyncio + async def test_async_sql_select(self, async_sql_index): + """Test async SELECT query (FT.SEARCH code path).""" + sql_query = SQLQuery(f"SELECT title, price FROM {async_sql_index.name}") + results = await async_sql_index.query(sql_query) + + assert len(results) == 3 + assert "title" in results[0] + assert "price" in results[0] + + @pytest.mark.asyncio + async def test_async_sql_aggregate(self, async_sql_index): + """Test async COUNT(*) aggregation (FT.AGGREGATE code path).""" + sql_query = SQLQuery(f"SELECT COUNT(*) as total FROM {async_sql_index.name}") + results = await async_sql_index.query(sql_query) + + assert len(results) == 1 + assert int(results[0]["total"]) == 3 diff --git a/uv.lock b/uv.lock index 34277f94..4d81a78b 100644 --- a/uv.lock +++ b/uv.lock @@ -4771,7 +4771,7 @@ wheels = [ [[package]] name = "redisvl" -version = "0.14.1" +version = "0.15.0" source = { editable = "." } dependencies = [ { name = "jsonpath-ng" }, @@ -4901,8 +4901,8 @@ requires-dist = [ { name = "redis", specifier = ">=5.0,<7.2" }, { name = "sentence-transformers", marker = "extra == 'all'", specifier = ">=3.4.0,<4" }, { name = "sentence-transformers", marker = "extra == 'sentence-transformers'", specifier = ">=3.4.0,<4" }, - { name = "sql-redis", marker = "extra == 'all'", specifier = ">=0.1.1" }, - { name = "sql-redis", marker = "extra == 'sql-redis'", specifier = ">=0.1.2" }, + { name = "sql-redis", marker = "extra == 'all'", specifier = ">=0.2.0" }, + { name = "sql-redis", marker = "extra == 'sql-redis'", specifier = ">=0.2.0" }, { name = "tenacity", specifier = ">=8.2.2" }, { name = "urllib3", marker = "extra == 'all'", specifier = "<2.2.0" }, { name = "urllib3", marker = "extra == 'bedrock'", specifier = "<2.2.0" }, @@ -5930,16 +5930,16 @@ wheels = [ [[package]] name = "sql-redis" -version = "0.1.2" +version = "0.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "redis", version = "7.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "redis", version = "7.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "sqlglot" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/86/d0/d16fccfa2c526f86fa14de2b24b000ff6f9e9d3eb50585326e8a12a74895/sql_redis-0.1.2.tar.gz", hash = "sha256:abdef256af90e4f2815c8983ec23563104cee7b8469234012fc1688df00bb499", size = 107321, upload-time = "2026-02-06T15:30:15.488Z" } +sdist = { url = "https://files.pythonhosted.org/packages/63/e8/a83da3199b388d8eff30e01ae6a5dbc3a0fc9e230b9f250409e7610a1a2a/sql_redis-0.2.0.tar.gz", hash = "sha256:63feabfb9d5dbe950a9ae51d9f66e600eba57291162c66bd15429e49ce58f1bb", size = 108976, upload-time = "2026-03-02T18:09:04.646Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/17/55/4fbdfa4fdceab9ba97a08e61c2294d41a3203cb90bcfc657fc83c7342d0b/sql_redis-0.1.2-py3-none-any.whl", hash = "sha256:2efae4b1e25cbc9e27d1e2f3e4dbc3bce277a534475167c6c821a2e037a7e516", size = 19905, upload-time = "2026-02-06T15:30:13.973Z" }, + { url = "https://files.pythonhosted.org/packages/6e/b0/70d46c36a8a3cc302d192e8fb4e55fb3bbc1d7078514ae475f0736a3a97a/sql_redis-0.2.0-py3-none-any.whl", hash = "sha256:a9098d211d335e4ac65d349ab6f45ea883034e4210ed137f2f428dad65d91234", size = 20693, upload-time = "2026-03-02T18:09:03.652Z" }, ] [[package]]