From d5372cc023f567339ca1fc1b502241736d2ff9cf Mon Sep 17 00:00:00 2001 From: Robert Shelton Date: Thu, 3 Apr 2025 16:53:41 -0400 Subject: [PATCH 1/9] update guides and version --- docs/index.md | 1 + docs/user_guide/index.md | 1 + .../release_guide/0.5.0_release.ipynb | 630 ++++++++++++++++++ docs/user_guide/release_guide/index.md | 16 + pyproject.toml | 2 +- redisvl/utils/optimize/cache.py | 2 + redisvl/utils/optimize/router.py | 2 + redisvl/version.py | 2 +- 8 files changed, 654 insertions(+), 2 deletions(-) create mode 100644 docs/user_guide/release_guide/0.5.0_release.ipynb create mode 100644 docs/user_guide/release_guide/index.md diff --git a/docs/index.md b/docs/index.md index a9bbd094..8e544276 100644 --- a/docs/index.md +++ b/docs/index.md @@ -56,6 +56,7 @@ This will also spin up the [Redis Insight GUI](https://redis.io/insight/) at `ht Overview API User Guides +Release Guides Example Gallery ``` diff --git a/docs/user_guide/index.md b/docs/user_guide/index.md index b6592e3d..7471d6d0 100644 --- a/docs/user_guide/index.md +++ b/docs/user_guide/index.md @@ -20,4 +20,5 @@ User guides provide helpful resources for using RedisVL and its different compon 06_rerankers 07_session_manager 08_semantic_router +09_threshold_optimization ``` diff --git a/docs/user_guide/release_guide/0.5.0_release.ipynb b/docs/user_guide/release_guide/0.5.0_release.ipynb new file mode 100644 index 00000000..a8c8d096 --- /dev/null +++ b/docs/user_guide/release_guide/0.5.0_release.ipynb @@ -0,0 +1,630 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![Redis](https://redis.io/wp-content/uploads/2024/04/Logotype.svg?auto=webp&quality=85,75&width=120)\n", + "# RedisVL 0.5.0 - Release overview\n", + "\n", + "This notebook provides an overview of what's new with the 0.5.0 release of redisvl. It also highlights changes and potential enhancements for existing usage.\n", + "\n", + "## What's new?\n", + "\n", + "- Hybrid query and text query classes\n", + "- Threshold optimizer classes\n", + "- Schema validation\n", + "- Timestamp filters\n", + "- Batched queries\n", + "- Vector normalization\n", + "- Hybrid policy on knn with filters\n", + "\n", + "## Define and load index for examples" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "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" + ] + }, + { + "data": { + "text/plain": [ + "['jobs:01JQYMYZBA6NM6DX9YW35MCHJZ',\n", + " 'jobs:01JQYMYZBABXYR96H96SQ99ZPS',\n", + " 'jobs:01JQYMYZBAGEBDS270EZADQ1TM']" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from redisvl.utils.vectorize import HFTextVectorizer\n", + "from redisvl.index import SearchIndex\n", + "import datetime as dt\n", + "\n", + "import warnings\n", + "warnings.filterwarnings(\"ignore\", category=UserWarning, module=\"redis\")\n", + "\n", + "# Embedding model\n", + "emb_model = HFTextVectorizer()\n", + "\n", + "REDIS_URL = \"redis://localhost:6379/0\"\n", + "NOW = dt.datetime.now()\n", + "\n", + "job_data = [\n", + " {\n", + " \"job_title\": \"Software Engineer\",\n", + " \"job_description\": \"Develop and maintain web applications using JavaScript, React, and Node.js.\",\n", + " \"posted\": (NOW - dt.timedelta(days=1)).timestamp() # day ago\n", + " },\n", + " {\n", + " \"job_title\": \"Data Analyst\",\n", + " \"job_description\": \"Analyze large datasets to provide business insights and create data visualizations.\",\n", + " \"posted\": (NOW - dt.timedelta(days=7)).timestamp() # week ago\n", + " },\n", + " {\n", + " \"job_title\": \"Marketing Manager\",\n", + " \"job_description\": \"Develop and implement marketing strategies to drive brand awareness and customer engagement.\",\n", + " \"posted\": (NOW - dt.timedelta(days=30)).timestamp() # month ago\n", + " }\n", + "]\n", + "\n", + "job_data = [{**job, \"job_embedding\": emb_model.embed(job[\"job_description\"], as_buffer=True)} for job in job_data]\n", + "\n", + "\n", + "job_schema = {\n", + " \"index\": {\n", + " \"name\": \"jobs\",\n", + " \"prefix\": \"jobs\",\n", + " \"storage_type\": \"hash\",\n", + " },\n", + " \"fields\": [\n", + " {\"name\": \"job_title\", \"type\": \"text\"},\n", + " {\"name\": \"job_description\", \"type\": \"text\"},\n", + " {\"name\": \"posted\", \"type\": \"numeric\"},\n", + " {\n", + " \"name\": \"job_embedding\",\n", + " \"type\": \"vector\",\n", + " \"attrs\": {\n", + " \"dims\": 768,\n", + " \"distance_metric\": \"cosine\",\n", + " \"algorithm\": \"flat\",\n", + " \"datatype\": \"float32\"\n", + " }\n", + "\n", + " }\n", + " ],\n", + "}\n", + "\n", + "index = SearchIndex.from_dict(job_schema, redis_url=REDIS_URL)\n", + "index.create(overwrite=True, drop=True)\n", + "index.load(job_data)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Hybrid query and text query classes\n", + "\n", + "In 0.5.0 we introduced classes to make it easier to perform hybrid lexical (BM25) and vector searches.\n", + "\n", + "> TODO: update hybrid search notebook to use the class and make sure it works the same" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Threshold optimization\n", + "\n", + "In redis 0.5.0 we added the ability to quickly configure either your semantic cache or semantic router with test data examples.\n", + "\n", + "For a step by step guide see: [09_threshold_optimization.ipynb](../09_threshold_optimization.ipynb).\n", + "\n", + "For a more advanced routing example see: [this example](https://github.com/redis-developer/redis-ai-resources/blob/main/python-recipes/semantic-router/01_routing_optimization.ipynb). " + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Distance threshold before: 0.5 \n", + "\n", + "\n", + "Distance threshold after: 0.13050847457627118 \n", + "\n" + ] + } + ], + "source": [ + "from redisvl.utils.optimize import CacheThresholdOptimizer\n", + "from redisvl.extensions.llmcache import SemanticCache\n", + "\n", + "sem_cache = SemanticCache(\n", + " name=\"sem_cache\", # underlying search index name\n", + " redis_url=\"redis://localhost:6379\", # redis connection url string\n", + " distance_threshold=0.5 # semantic cache distance threshold\n", + ")\n", + "\n", + "paris_key = sem_cache.store(prompt=\"what is the capital of france?\", response=\"paris\")\n", + "rabat_key = sem_cache.store(prompt=\"what is the capital of morocco?\", response=\"rabat\")\n", + "\n", + "test_data = [\n", + " {\n", + " \"query\": \"What's the capital of Britain?\",\n", + " \"query_match\": \"\"\n", + " },\n", + " {\n", + " \"query\": \"What's the capital of France??\",\n", + " \"query_match\": paris_key\n", + " },\n", + " {\n", + " \"query\": \"What's the capital city of Morocco?\",\n", + " \"query_match\": rabat_key\n", + " },\n", + "]\n", + "\n", + "print(f\"\\nDistance threshold before: {sem_cache.distance_threshold} \\n\")\n", + "optimizer = CacheThresholdOptimizer(sem_cache, test_data)\n", + "optimizer.optimize()\n", + "print(f\"\\nDistance threshold after: {sem_cache.distance_threshold} \\n\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Schema validation\n", + "\n", + "This feature makes it easier to make sure your data is in the right format. To demo this we will create a new index with the `validate_on_load` flag set to `True`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[32m16:20:25\u001b[0m \u001b[34mredisvl.index.index\u001b[0m \u001b[1;30mERROR\u001b[0m \u001b[31mSchema validation error while loading data\u001b[0m\n", + "Traceback (most recent call last):\n", + " File \"/Users/robert.shelton/.pyenv/versions/3.11.9/lib/python3.11/site-packages/redisvl/index/storage.py\", line 204, in _preprocess_and_validate_objects\n", + " processed_obj = self._validate(processed_obj)\n", + " ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", + " File \"/Users/robert.shelton/.pyenv/versions/3.11.9/lib/python3.11/site-packages/redisvl/index/storage.py\", line 160, in _validate\n", + " return validate_object(self.index_schema, obj)\n", + " ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", + " File \"/Users/robert.shelton/.pyenv/versions/3.11.9/lib/python3.11/site-packages/redisvl/schema/validation.py\", line 276, in validate_object\n", + " validated = model_class.model_validate(flat_obj)\n", + " ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", + " File \"/Users/robert.shelton/.pyenv/versions/3.11.9/lib/python3.11/site-packages/pydantic/main.py\", line 627, in model_validate\n", + " return cls.__pydantic_validator__.validate_python(\n", + " ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", + "pydantic_core._pydantic_core.ValidationError: 2 validation errors for cars__PydanticModel\n", + "mpg.int\n", + " Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='twenty-two', input_type=str]\n", + " For further information visit https://errors.pydantic.dev/2.10/v/int_parsing\n", + "mpg.float\n", + " Input should be a valid number, unable to parse string as a number [type=float_parsing, input_value='twenty-two', input_type=str]\n", + " For further information visit https://errors.pydantic.dev/2.10/v/float_parsing\n", + "\n", + "The above exception was the direct cause of the following exception:\n", + "\n", + "Traceback (most recent call last):\n", + " File \"/Users/robert.shelton/.pyenv/versions/3.11.9/lib/python3.11/site-packages/redisvl/index/index.py\", line 615, in load\n", + " return self._storage.write(\n", + " ^^^^^^^^^^^^^^^^^^^^\n", + " File \"/Users/robert.shelton/.pyenv/versions/3.11.9/lib/python3.11/site-packages/redisvl/index/storage.py\", line 265, in write\n", + " prepared_objects = self._preprocess_and_validate_objects(\n", + " ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", + " File \"/Users/robert.shelton/.pyenv/versions/3.11.9/lib/python3.11/site-packages/redisvl/index/storage.py\", line 211, in _preprocess_and_validate_objects\n", + " raise SchemaValidationError(str(e), index=i) from e\n", + "redisvl.exceptions.SchemaValidationError: Validation failed for object at index 1: 2 validation errors for cars__PydanticModel\n", + "mpg.int\n", + " Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='twenty-two', input_type=str]\n", + " For further information visit https://errors.pydantic.dev/2.10/v/int_parsing\n", + "mpg.float\n", + " Input should be a valid number, unable to parse string as a number [type=float_parsing, input_value='twenty-two', input_type=str]\n", + " For further information visit https://errors.pydantic.dev/2.10/v/float_parsing\n", + "Error loading data: Validation failed for object at index 1: 2 validation errors for cars__PydanticModel\n", + "mpg.int\n", + " Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='twenty-two', input_type=str]\n", + " For further information visit https://errors.pydantic.dev/2.10/v/int_parsing\n", + "mpg.float\n", + " Input should be a valid number, unable to parse string as a number [type=float_parsing, input_value='twenty-two', input_type=str]\n", + " For further information visit https://errors.pydantic.dev/2.10/v/float_parsing\n" + ] + } + ], + "source": [ + "# NBVAL_SKIP\n", + "from redisvl.index import SearchIndex\n", + "\n", + "# sample schema\n", + "car_schema = {\n", + " \"index\": {\n", + " \"name\": \"cars\",\n", + " \"prefix\": \"cars\",\n", + " \"storage_type\": \"json\",\n", + " },\n", + " \"fields\": [\n", + " {\"name\": \"make\", \"type\": \"text\"},\n", + " {\"name\": \"model\", \"type\": \"text\"},\n", + " {\"name\": \"description\", \"type\": \"text\"},\n", + " {\"name\": \"mpg\", \"type\": \"numeric\"},\n", + " {\n", + " \"name\": \"car_embedding\",\n", + " \"type\": \"vector\",\n", + " \"attrs\": {\n", + " \"dims\": 3,\n", + " \"distance_metric\": \"cosine\",\n", + " \"algorithm\": \"flat\",\n", + " \"datatype\": \"float32\"\n", + " }\n", + "\n", + " }\n", + " ],\n", + "}\n", + "\n", + "sample_data_bad = [\n", + " {\n", + " \"make\": \"Toyota\",\n", + " \"model\": \"Camry\",\n", + " \"description\": \"A reliable sedan with great fuel economy.\",\n", + " \"mpg\": 28,\n", + " \"car_embedding\": [0.1, 0.2, 0.3]\n", + " },\n", + " {\n", + " \"make\": \"Honda\",\n", + " \"model\": \"CR-V\",\n", + " \"description\": \"A practical SUV with advanced technology.\",\n", + " # incorrect type will throw an error\n", + " \"mpg\": \"twenty-two\",\n", + " \"car_embedding\": [0.4, 0.5, 0.6]\n", + " }\n", + "]\n", + "\n", + "# this should now throw an error\n", + "car_index = SearchIndex.from_dict(car_schema, redis_url=REDIS_URL, validate_on_load=True)\n", + "car_index.create(overwrite=True)\n", + "\n", + "try:\n", + " car_index.load(sample_data_bad)\n", + "except Exception as e:\n", + " print(f\"Error loading data: {e}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Timestamp filters\n", + "\n", + "In Redis datetime objects are stored as numeric epoch times. Timestamp filter makes it easier to handle querying by these fields by handling conversion for you." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[{'id': 'jobs:01JQYMYZBA6NM6DX9YW35MCHJZ',\n", + " 'job_title': 'Software Engineer',\n", + " 'job_description': 'Develop and maintain web applications using JavaScript, React, and Node.js.',\n", + " 'posted': '1743625199.9'},\n", + " {'id': 'jobs:01JQYMYZBABXYR96H96SQ99ZPS',\n", + " 'job_title': 'Data Analyst',\n", + " 'job_description': 'Analyze large datasets to provide business insights and create data visualizations.',\n", + " 'posted': '1743106799.9'},\n", + " {'id': 'jobs:01JQYMYZBAGEBDS270EZADQ1TM',\n", + " 'job_title': 'Marketing Manager',\n", + " 'job_description': 'Develop and implement marketing strategies to drive brand awareness and customer engagement.',\n", + " 'posted': '1741123199.9'}]" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from redisvl.query import FilterQuery\n", + "from redisvl.query.filter import Timestamp\n", + "\n", + "# find all jobs\n", + "ts = Timestamp(\"posted\") < NOW # now datetime created above\n", + "\n", + "filter_query = FilterQuery(\n", + " return_fields=[\"job_title\", \"job_description\", \"posted\"], \n", + " filter_expression=ts,\n", + " num_results=10,\n", + ")\n", + "res = index.query(filter_query)\n", + "res" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[{'id': 'jobs:01JQYMYZBA6NM6DX9YW35MCHJZ',\n", + " 'job_title': 'Software Engineer',\n", + " 'job_description': 'Develop and maintain web applications using JavaScript, React, and Node.js.',\n", + " 'posted': '1743625199.9'}]" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# jobs posted in the last 3 days => 1 job\n", + "ts = Timestamp(\"posted\") > NOW - dt.timedelta(days=3)\n", + "\n", + "filter_query = FilterQuery(\n", + " return_fields=[\"job_title\", \"job_description\", \"posted\"], \n", + " filter_expression=ts,\n", + " num_results=10,\n", + ")\n", + "res = index.query(filter_query)\n", + "res" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[{'id': 'jobs:01JQYMYZBABXYR96H96SQ99ZPS',\n", + " 'job_title': 'Data Analyst',\n", + " 'job_description': 'Analyze large datasets to provide business insights and create data visualizations.',\n", + " 'posted': '1743106799.9'}]" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# more than 3 days ago but less than 14 days ago => 1 job\n", + "ts = Timestamp(\"posted\").between(\n", + " NOW - dt.timedelta(days=14),\n", + " NOW - dt.timedelta(days=3),\n", + ")\n", + "\n", + "filter_query = FilterQuery(\n", + " return_fields=[\"job_title\", \"job_description\", \"posted\"], \n", + " filter_expression=ts,\n", + " num_results=10,\n", + ")\n", + "\n", + "res = index.query(filter_query)\n", + "res" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Batch search\n", + "\n", + "This enhancement allows you to speed up the execution of queries by reducing the impact of network latency." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Time taken for 200 queries: 0.11 seconds\n" + ] + } + ], + "source": [ + "import time\n", + "num_queries = 200\n", + "\n", + "start = time.time()\n", + "for i in range(num_queries):\n", + " # run the same filter query \n", + " res = index.query(filter_query)\n", + "end = time.time()\n", + "print(f\"Time taken for {num_queries} queries: {end - start:.2f} seconds\")" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Time taken for 200 batched queries: 0.03 seconds\n" + ] + } + ], + "source": [ + "batched_queries = [filter_query] * num_queries\n", + "\n", + "start = time.time()\n", + "\n", + "index.batch_search(batched_queries, batch_size=10)\n", + "\n", + "end = time.time()\n", + "print(f\"Time taken for {num_queries} batched queries: {end - start:.2f} seconds\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Vector normalization\n", + "\n", + "By default, Redis returns the vector cosine distance when performing a search, which yields a value between 0 and 2, where 0 represents a perfect match. However, you may sometimes prefer a similarity score between 0 and 1, where 1 indicates a perfect match. When enabled, this flag performs the conversion for you. Additionally, if this flag is set to true for L2 distance, it normalizes the Euclidean distance to a value between 0 and 1 as well.\n", + " " + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[{'id': 'jobs:01JQYMYZBA6NM6DX9YW35MCHJZ',\n", + " 'vector_distance': '0.7090711295605',\n", + " 'job_title': 'Software Engineer',\n", + " 'job_description': 'Develop and maintain web applications using JavaScript, React, and Node.js.',\n", + " 'posted': '1743625199.9'},\n", + " {'id': 'jobs:01JQYMYZBABXYR96H96SQ99ZPS',\n", + " 'vector_distance': '0.6049451231955',\n", + " 'job_title': 'Data Analyst',\n", + " 'job_description': 'Analyze large datasets to provide business insights and create data visualizations.',\n", + " 'posted': '1743106799.9'},\n", + " {'id': 'jobs:01JQYMYZBAGEBDS270EZADQ1TM',\n", + " 'vector_distance': '0.553376108408',\n", + " 'job_title': 'Marketing Manager',\n", + " 'job_description': 'Develop and implement marketing strategies to drive brand awareness and customer engagement.',\n", + " 'posted': '1741123199.9'}]" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from redisvl.query import VectorQuery\n", + "\n", + "query = VectorQuery(\n", + " vector=emb_model.embed(\"Software Engineer\", as_buffer=True),\n", + " vector_field_name=\"job_embedding\",\n", + " return_fields=[\"job_title\", \"job_description\", \"posted\"],\n", + " normalize_vector_distance=True,\n", + ")\n", + "\n", + "res = index.query(query)\n", + "res" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Hybrid policy on knn with filters\n", + "\n", + "Within the default redis client you can set the `HYBRID_POLICY` which specifies the filter mode to use during vector search with filters. It can take values `BATCHES` or `ADHOC_BF`. Previously this option was not exposed by redisvl." + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[{'id': 'jobs:01JQYMYZBA6NM6DX9YW35MCHJZ',\n", + " 'vector_distance': '0.581857740879',\n", + " 'job_title': 'Software Engineer',\n", + " 'job_description': 'Develop and maintain web applications using JavaScript, React, and Node.js.',\n", + " 'posted': '1743625199.9'},\n", + " {'id': 'jobs:01JQYMYZBAGEBDS270EZADQ1TM',\n", + " 'vector_distance': '0.893247783184',\n", + " 'job_title': 'Marketing Manager',\n", + " 'job_description': 'Develop and implement marketing strategies to drive brand awareness and customer engagement.',\n", + " 'posted': '1741123199.9'}]" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from redisvl.query.filter import Text\n", + "\n", + "filter = Text(\"job_description\") % \"Develop\"\n", + "\n", + "query = VectorQuery(\n", + " vector=emb_model.embed(\"Software Engineer\", as_buffer=True),\n", + " vector_field_name=\"job_embedding\",\n", + " return_fields=[\"job_title\", \"job_description\", \"posted\"],\n", + " hybrid_policy=\"BATCHES\"\n", + ")\n", + "\n", + "query.set_filter(filter)\n", + "\n", + "res = index.query(query)\n", + "res" + ] + } + ], + "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": 2 +} diff --git a/docs/user_guide/release_guide/index.md b/docs/user_guide/release_guide/index.md new file mode 100644 index 00000000..c6484158 --- /dev/null +++ b/docs/user_guide/release_guide/index.md @@ -0,0 +1,16 @@ +--- +myst: + html_meta: + "description lang=en": | + Release Guides for RedisVL +--- + +# Release Guides +Release guides provide practical examples of what has been changed and/or added with a release. + +```{toctree} +:caption: Release Guides +:maxdepth: 2 + +0.5.0_release +``` diff --git a/pyproject.toml b/pyproject.toml index eed0e0dd..4e3934c2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "redisvl" -version = "0.4.1" +version = "0.5.0" description = "Python client library and CLI for using Redis as a vector database" authors = ["Redis Inc. "] license = "MIT" diff --git a/redisvl/utils/optimize/cache.py b/redisvl/utils/optimize/cache.py index f88c53ff..04bff662 100644 --- a/redisvl/utils/optimize/cache.py +++ b/redisvl/utils/optimize/cache.py @@ -71,6 +71,8 @@ def _grid_search_opt_cache( class CacheThresholdOptimizer(BaseThresholdOptimizer): + """Class for optimizing thresholds for a SemanticCache.""" + def __init__( self, cache: SemanticCache, diff --git a/redisvl/utils/optimize/router.py b/redisvl/utils/optimize/router.py index 40a7ea7d..5dc29793 100644 --- a/redisvl/utils/optimize/router.py +++ b/redisvl/utils/optimize/router.py @@ -90,6 +90,8 @@ def _random_search_opt_router( class RouterThresholdOptimizer(BaseThresholdOptimizer): + """Class for optimizing thresholds for a SemanticRouter.""" + def __init__( self, router: SemanticRouter, diff --git a/redisvl/version.py b/redisvl/version.py index 3d26edf7..3d187266 100644 --- a/redisvl/version.py +++ b/redisvl/version.py @@ -1 +1 @@ -__version__ = "0.4.1" +__version__ = "0.5.0" From b38ed9209524e09f1f7dd28715512cc8d43c82c9 Mon Sep 17 00:00:00 2001 From: Tyler Hutcherson Date: Fri, 4 Apr 2025 11:10:00 -0400 Subject: [PATCH 2/9] update release guide --- docs/index.md | 1 - docs/user_guide/index.md | 3 ++- .../{0.5.0_release.ipynb => 0_5_0_release.ipynb} | 2 +- docs/user_guide/release_guide/index.md | 7 ++++--- 4 files changed, 7 insertions(+), 6 deletions(-) rename docs/user_guide/release_guide/{0.5.0_release.ipynb => 0_5_0_release.ipynb} (99%) diff --git a/docs/index.md b/docs/index.md index 8e544276..a9bbd094 100644 --- a/docs/index.md +++ b/docs/index.md @@ -56,7 +56,6 @@ This will also spin up the [Redis Insight GUI](https://redis.io/insight/) at `ht Overview API User Guides -Release Guides Example Gallery ``` diff --git a/docs/user_guide/index.md b/docs/user_guide/index.md index 7471d6d0..30a51b8a 100644 --- a/docs/user_guide/index.md +++ b/docs/user_guide/index.md @@ -2,7 +2,7 @@ myst: html_meta: "description lang=en": | - User Guides for RedisVL + User guides for RedisVL --- # User Guides @@ -21,4 +21,5 @@ User guides provide helpful resources for using RedisVL and its different compon 07_session_manager 08_semantic_router 09_threshold_optimization +release_guide/index ``` diff --git a/docs/user_guide/release_guide/0.5.0_release.ipynb b/docs/user_guide/release_guide/0_5_0_release.ipynb similarity index 99% rename from docs/user_guide/release_guide/0.5.0_release.ipynb rename to docs/user_guide/release_guide/0_5_0_release.ipynb index a8c8d096..777055b4 100644 --- a/docs/user_guide/release_guide/0.5.0_release.ipynb +++ b/docs/user_guide/release_guide/0_5_0_release.ipynb @@ -5,7 +5,7 @@ "metadata": {}, "source": [ "![Redis](https://redis.io/wp-content/uploads/2024/04/Logotype.svg?auto=webp&quality=85,75&width=120)\n", - "# RedisVL 0.5.0 - Release overview\n", + "# RedisVL 0.5.0 Overview\n", "\n", "This notebook provides an overview of what's new with the 0.5.0 release of redisvl. It also highlights changes and potential enhancements for existing usage.\n", "\n", diff --git a/docs/user_guide/release_guide/index.md b/docs/user_guide/release_guide/index.md index c6484158..1eba08c9 100644 --- a/docs/user_guide/release_guide/index.md +++ b/docs/user_guide/release_guide/index.md @@ -2,15 +2,16 @@ myst: html_meta: "description lang=en": | - Release Guides for RedisVL + Release guides for RedisVL --- # Release Guides -Release guides provide practical examples of what has been changed and/or added with a release. + +This section contains guidelines and information for RedisVL releases. ```{toctree} :caption: Release Guides :maxdepth: 2 -0.5.0_release +0_5_0_release ``` From e71a35e39bee3d8b455bfd177ed0128de8e930f0 Mon Sep 17 00:00:00 2001 From: Tyler Hutcherson Date: Fri, 4 Apr 2025 11:29:53 -0400 Subject: [PATCH 3/9] prep optimizer docs --- docs/api/index.md | 1 + docs/api/session_manager.rst | 1 - docs/api/threshold_optimizer.rst | 26 +++++++++++++++++++ .../release_guide/0_5_0_release.ipynb | 3 +-- redisvl/utils/optimize/cache.py | 25 ++++++++++++++++-- redisvl/utils/optimize/router.py | 16 ++++++++++-- 6 files changed, 65 insertions(+), 7 deletions(-) create mode 100644 docs/api/threshold_optimizer.rst diff --git a/docs/api/index.md b/docs/api/index.md index 598d9dcd..e3dc486e 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -22,5 +22,6 @@ reranker cache session_manager router +threshold_optimizer ``` diff --git a/docs/api/session_manager.rst b/docs/api/session_manager.rst index 9b885a35..86289204 100644 --- a/docs/api/session_manager.rst +++ b/docs/api/session_manager.rst @@ -2,7 +2,6 @@ LLM Session Manager ******************* - SemanticSessionManager ====================== diff --git a/docs/api/threshold_optimizer.rst b/docs/api/threshold_optimizer.rst new file mode 100644 index 00000000..dc226cef --- /dev/null +++ b/docs/api/threshold_optimizer.rst @@ -0,0 +1,26 @@ +******************** +Threshold Optimizers +******************** + +CacheThresholdOptimizer +======================= + +.. _cachethresholdoptimizer_api: + +.. currentmodule:: redisvl.utils.optimize.cache + +.. autoclass:: CacheThresholdOptimizer + :show-inheritance: + :members: + + +RouterThresholdOptimizer +======================== + +.. _routerthresholdoptimizer_api: + +.. currentmodule:: redisvl.utils.optimize.router + +.. autoclass:: RouterThresholdOptimizer + :show-inheritance: + :members: diff --git a/docs/user_guide/release_guide/0_5_0_release.ipynb b/docs/user_guide/release_guide/0_5_0_release.ipynb index 777055b4..a5f10441 100644 --- a/docs/user_guide/release_guide/0_5_0_release.ipynb +++ b/docs/user_guide/release_guide/0_5_0_release.ipynb @@ -4,8 +4,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "![Redis](https://redis.io/wp-content/uploads/2024/04/Logotype.svg?auto=webp&quality=85,75&width=120)\n", - "# RedisVL 0.5.0 Overview\n", + "# 0.5.0 Feature Overview\n", "\n", "This notebook provides an overview of what's new with the 0.5.0 release of redisvl. It also highlights changes and potential enhancements for existing usage.\n", "\n", diff --git a/redisvl/utils/optimize/cache.py b/redisvl/utils/optimize/cache.py index 04bff662..8da8c6ca 100644 --- a/redisvl/utils/optimize/cache.py +++ b/redisvl/utils/optimize/cache.py @@ -76,12 +76,33 @@ class CacheThresholdOptimizer(BaseThresholdOptimizer): def __init__( self, cache: SemanticCache, - test_dict: List[Dict], + test_dict: List[Dict[str, Any]], opt_fn: Callable = _grid_search_opt_cache, eval_metric: str = "f1", ): + """Initialize the optimizer. + + Args: + cache (SemanticCache): The RedisVL SemanticCache instance to optimize. + test_dict (List[Dict[str, Any]]): List of test cases. + opt_fn (Callable): Function to perform optimization. Defaults to + grid search. + eval_metric (str): Evaluation metric for threshold optimization. + Defaults to "f1" score. + + .. code-block:: python + + # TODO + + """ super().__init__(cache, test_dict, opt_fn, eval_metric) def optimize(self, **kwargs: Any): - """Optimize thresholds using the provided optimization function for cache case.""" + """Optimize thresholds using the provided optimization function for cache case. + + .. code-block:: python + + # TODO + + """ self.opt_fn(self.optimizable, self.test_data, self.eval_metric, **kwargs) diff --git a/redisvl/utils/optimize/router.py b/redisvl/utils/optimize/router.py index 5dc29793..eaa4bc8a 100644 --- a/redisvl/utils/optimize/router.py +++ b/redisvl/utils/optimize/router.py @@ -95,13 +95,25 @@ class RouterThresholdOptimizer(BaseThresholdOptimizer): def __init__( self, router: SemanticRouter, - test_dict: List[Dict], + test_dict: List[Dict[str, Any]], opt_fn: Callable = _random_search_opt_router, eval_metric: str = "f1", ): + """ + # TODO + + .. code-block:: python + + # TODO + """ super().__init__(router, test_dict, opt_fn, eval_metric) def optimize(self, **kwargs: Any): - """Optimize thresholds using the provided optimization function for router case.""" + """Optimize thresholds using the provided optimization function for router case. + + .. code-block:: python + + # TODO + """ qrels = _format_qrels(self.test_data) self.opt_fn(self.optimizable, self.test_data, qrels, self.eval_metric, **kwargs) From 46f8672f9368fb3d00848760fd35af166db0ef59 Mon Sep 17 00:00:00 2001 From: Robert Shelton Date: Fri, 4 Apr 2025 12:50:08 -0400 Subject: [PATCH 4/9] docstrings and hybrid updates --- .../release_guide/0_5_0_release.ipynb | 111 +++++++++++++++--- redisvl/query/aggregate.py | 2 +- redisvl/utils/optimize/cache.py | 39 +++++- redisvl/utils/optimize/router.py | 50 +++++++- 4 files changed, 176 insertions(+), 26 deletions(-) diff --git a/docs/user_guide/release_guide/0_5_0_release.ipynb b/docs/user_guide/release_guide/0_5_0_release.ipynb index a5f10441..66edf3ca 100644 --- a/docs/user_guide/release_guide/0_5_0_release.ipynb +++ b/docs/user_guide/release_guide/0_5_0_release.ipynb @@ -23,26 +23,25 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 9, "metadata": {}, "outputs": [ { - "name": "stderr", + "name": "stdout", "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" + "\u001b[32m12:44:52\u001b[0m \u001b[34mredisvl.index.index\u001b[0m \u001b[1;30mINFO\u001b[0m Index already exists, overwriting.\n" ] }, { "data": { "text/plain": [ - "['jobs:01JQYMYZBA6NM6DX9YW35MCHJZ',\n", - " 'jobs:01JQYMYZBABXYR96H96SQ99ZPS',\n", - " 'jobs:01JQYMYZBAGEBDS270EZADQ1TM']" + "['jobs:01JR0V1SA29RVD9AAVSTBV9P5H',\n", + " 'jobs:01JR0V1SA209KMVHMD7G54P3H5',\n", + " 'jobs:01JR0V1SA23ZE7BRERXTZWC33Z']" ] }, - "execution_count": 7, + "execution_count": 9, "metadata": {}, "output_type": "execute_result" } @@ -115,19 +114,101 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Hybrid query and text query classes\n", + "# HybridQuery class\n", + "\n", + "Perform hybrid lexical (BM25) and vector search where results are ranked by: `hybrid_score = (1-alpha)*lexical_Score + alpha*vector_similarity`." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[{'vector_distance': '0.655903100967',\n", + " 'job_title': 'Software Engineer',\n", + " 'vector_similarity': '0.672048449516',\n", + " 'text_score': '0',\n", + " 'hybrid_score': '0.470433914661'},\n", + " {'vector_distance': '0.892600417137',\n", + " 'job_title': 'Data Analyst',\n", + " 'vector_similarity': '0.553699791431',\n", + " 'text_score': '0',\n", + " 'hybrid_score': '0.387589854002'},\n", + " {'vector_distance': '0.958741784096',\n", + " 'job_title': 'Marketing Manager',\n", + " 'vector_similarity': '0.520629107952',\n", + " 'text_score': '0',\n", + " 'hybrid_score': '0.364440375566'}]" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from redisvl.query import HybridQuery\n", + "\n", + "text = \"Find a job as a software engineer\"\n", + "vec = emb_model.embed(text, as_buffer=True)\n", + "\n", + "query = HybridQuery(\n", + " text=text,\n", + " text_field_name=\"job_description\",\n", + " vector=vec,\n", + " vector_field_name=\"job_embedding\",\n", + " alpha=0.7,\n", + " num_results=10,\n", + " return_fields=[\"job_title\"],\n", + ")\n", "\n", - "In 0.5.0 we introduced classes to make it easier to perform hybrid lexical (BM25) and vector searches.\n", + "results = index.query(query)\n", + "results" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# TextQueries\n", "\n", - "> TODO: update hybrid search notebook to use the class and make sure it works the same" + "TextQueries make it easy to perform pure lexical search with redisvl." ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 13, "metadata": {}, - "outputs": [], - "source": [] + "outputs": [ + { + "data": { + "text/plain": [ + "[]" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from redisvl.query import TextQuery\n", + "\n", + "text = \"Find a job as a software engineer\"\n", + "\n", + "query = TextQuery(\n", + " text=text,\n", + " text_field_name=\"job_description\",\n", + " return_fields=[\"job_title\"],\n", + " num_results=10,\n", + ")\n", + "\n", + "results = index.query(query)\n", + "results" + ] }, { "cell_type": "markdown", @@ -607,7 +688,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "redisvl-56gG2io_-py3.11", "language": "python", "name": "python3" }, diff --git a/redisvl/query/aggregate.py b/redisvl/query/aggregate.py index 1e5526d1..831b48e9 100644 --- a/redisvl/query/aggregate.py +++ b/redisvl/query/aggregate.py @@ -77,7 +77,7 @@ def __init__( from redisvl.query import HybridQuery from redisvl.index import SearchIndex - index = SearchIndex.from_yaml(index.yaml) + index = SearchIndex.from_yaml("path/to/index.yaml") query = HybridQuery( text="example text", diff --git a/redisvl/utils/optimize/cache.py b/redisvl/utils/optimize/cache.py index 8da8c6ca..8de0450a 100644 --- a/redisvl/utils/optimize/cache.py +++ b/redisvl/utils/optimize/cache.py @@ -80,7 +80,7 @@ def __init__( opt_fn: Callable = _grid_search_opt_cache, eval_metric: str = "f1", ): - """Initialize the optimizer. + """Initialize the cache optimizer. Args: cache (SemanticCache): The RedisVL SemanticCache instance to optimize. @@ -91,9 +91,35 @@ def __init__( Defaults to "f1" score. .. code-block:: python - - # TODO - + from redisvl.extensions.llmcache import SemanticCache + from redisvl.utils.optimize import CacheThresholdOptimizer + + sem_cache = SemanticCache( + name="sem_cache", # underlying search index name + redis_url="redis://localhost:6379", # redis connection url string + distance_threshold=0.5 # semantic cache distance threshold + ) + + paris_key = sem_cache.store(prompt="what is the capital of france?", response="paris") + rabat_key = sem_cache.store(prompt="what is the capital of morocco?", response="rabat") + + test_data = [ + { + "query": "What's the capital of Britain?", + "query_match": "" + }, + { + "query": "What's the capital of France??", + "query_match": paris_key + }, + { + "query": "What's the capital city of Morocco?", + "query_match": rabat_key + }, + ] + + optimizer = CacheThresholdOptimizer(sem_cache, test_data) + optimizer.optimize() """ super().__init__(cache, test_dict, opt_fn, eval_metric) @@ -101,8 +127,9 @@ def optimize(self, **kwargs: Any): """Optimize thresholds using the provided optimization function for cache case. .. code-block:: python + from redisvl.utils.optimize import CacheThresholdOptimizer - # TODO - + optimizer = CacheThresholdOptimizer(semantic_cache, test_data) + optimizer.optimize(*kwargs) """ self.opt_fn(self.optimizable, self.test_data, self.eval_metric, **kwargs) diff --git a/redisvl/utils/optimize/router.py b/redisvl/utils/optimize/router.py index eaa4bc8a..b5215d9b 100644 --- a/redisvl/utils/optimize/router.py +++ b/redisvl/utils/optimize/router.py @@ -99,12 +99,52 @@ def __init__( opt_fn: Callable = _random_search_opt_router, eval_metric: str = "f1", ): - """ - # TODO + """Initialize the router optimizer. + + Args: + router (SemanticRouter): The RedisVL SemanticRouter instance to optimize. + test_dict (List[Dict[str, Any]]): List of test cases. + opt_fn (Callable): Function to perform optimization. Defaults to + grid search. + eval_metric (str): Evaluation metric for threshold optimization. + Defaults to "f1" score. .. code-block:: python + from redisvl.extensions.router import Route, SemanticRouter + from redisvl.utils.vectorize import HFTextVectorizer + from redisvl.utils.optimize import RouterThresholdOptimizer + + routes = [ + Route( + name="greeting", + references=["hello", "hi"], + metadata={"type": "greeting"}, + distance_threshold=0.5, + ), + Route( + name="farewell", + references=["bye", "goodbye"], + metadata={"type": "farewell"}, + distance_threshold=0.5, + ), + ] + + router = SemanticRouter( + name="greeting-router", + vectorizer=HFTextVectorizer(), + routes=routes, + redis_url="redis://localhost:6379", + overwrite=True # Blow away any other routing index with this name + ) + + test_data = [ + {"query": "hello", "query_match": "greeting"}, + {"query": "goodbye", "query_match": "farewell"}, + ... + ] - # TODO + optimizer = RouterThresholdOptimizer(router, test_data) + optimizer.optimize() """ super().__init__(router, test_dict, opt_fn, eval_metric) @@ -112,8 +152,10 @@ def optimize(self, **kwargs: Any): """Optimize thresholds using the provided optimization function for router case. .. code-block:: python + from redisvl.utils.optimize import RouterThresholdOptimizer - # TODO + optimizer = RouterThresholdOptimizer(router, test_data) + optimizer.optimize(search_step=0.05, max_iterations=50) """ qrels = _format_qrels(self.test_data) self.opt_fn(self.optimizable, self.test_data, qrels, self.eval_metric, **kwargs) From 14b6749e89ab988ad138bac98b7cce245269d358 Mon Sep 17 00:00:00 2001 From: Robert Shelton Date: Fri, 4 Apr 2025 13:16:34 -0400 Subject: [PATCH 5/9] update text query --- .../release_guide/0_5_0_release.ipynb | 43 +++++++++++-------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/docs/user_guide/release_guide/0_5_0_release.ipynb b/docs/user_guide/release_guide/0_5_0_release.ipynb index 66edf3ca..ca539f34 100644 --- a/docs/user_guide/release_guide/0_5_0_release.ipynb +++ b/docs/user_guide/release_guide/0_5_0_release.ipynb @@ -121,30 +121,30 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 16, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "[{'vector_distance': '0.655903100967',\n", + "[{'vector_distance': '0.61871612072',\n", " 'job_title': 'Software Engineer',\n", - " 'vector_similarity': '0.672048449516',\n", - " 'text_score': '0',\n", - " 'hybrid_score': '0.470433914661'},\n", - " {'vector_distance': '0.892600417137',\n", - " 'job_title': 'Data Analyst',\n", - " 'vector_similarity': '0.553699791431',\n", - " 'text_score': '0',\n", - " 'hybrid_score': '0.387589854002'},\n", - " {'vector_distance': '0.958741784096',\n", + " 'vector_similarity': '0.69064193964',\n", + " 'text_score': '49.6242910712',\n", + " 'hybrid_score': '15.3707366791'},\n", + " {'vector_distance': '0.937997639179',\n", " 'job_title': 'Marketing Manager',\n", - " 'vector_similarity': '0.520629107952',\n", + " 'vector_similarity': '0.53100118041',\n", + " 'text_score': '49.6242910712',\n", + " 'hybrid_score': '15.2589881476'},\n", + " {'vector_distance': '0.859166145325',\n", + " 'job_title': 'Data Analyst',\n", + " 'vector_similarity': '0.570416927338',\n", " 'text_score': '0',\n", - " 'hybrid_score': '0.364440375566'}]" + " 'hybrid_score': '0.399291849136'}]" ] }, - "execution_count": 14, + "execution_count": 16, "metadata": {}, "output_type": "execute_result" } @@ -152,7 +152,7 @@ "source": [ "from redisvl.query import HybridQuery\n", "\n", - "text = \"Find a job as a software engineer\"\n", + "text = \"Find a job as a where you develop software\"\n", "vec = emb_model.embed(text, as_buffer=True)\n", "\n", "query = HybridQuery(\n", @@ -180,16 +180,21 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 17, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "[]" + "[{'id': 'jobs:01JR0V1SA29RVD9AAVSTBV9P5H',\n", + " 'score': 49.62429107116745,\n", + " 'job_title': 'Software Engineer'},\n", + " {'id': 'jobs:01JR0V1SA23ZE7BRERXTZWC33Z',\n", + " 'score': 49.62429107116745,\n", + " 'job_title': 'Marketing Manager'}]" ] }, - "execution_count": 13, + "execution_count": 17, "metadata": {}, "output_type": "execute_result" } @@ -197,7 +202,7 @@ "source": [ "from redisvl.query import TextQuery\n", "\n", - "text = \"Find a job as a software engineer\"\n", + "text = \"Find where you develop software\"\n", "\n", "query = TextQuery(\n", " text=text,\n", From d0c86ed5d1c8622f8a51a5cb158ce9e30da0e56b Mon Sep 17 00:00:00 2001 From: Robert Shelton Date: Fri, 4 Apr 2025 13:45:59 -0400 Subject: [PATCH 6/9] update doc strings to work --- redisvl/utils/optimize/cache.py | 77 ++++++++++++++------------- redisvl/utils/optimize/router.py | 90 ++++++++++++++++---------------- 2 files changed, 82 insertions(+), 85 deletions(-) diff --git a/redisvl/utils/optimize/cache.py b/redisvl/utils/optimize/cache.py index 8de0450a..05c99f76 100644 --- a/redisvl/utils/optimize/cache.py +++ b/redisvl/utils/optimize/cache.py @@ -71,7 +71,41 @@ def _grid_search_opt_cache( class CacheThresholdOptimizer(BaseThresholdOptimizer): - """Class for optimizing thresholds for a SemanticCache.""" + """ + Class for optimizing thresholds for a SemanticCache. + + .. code-block:: python + + from redisvl.extensions.llmcache import SemanticCache + from redisvl.utils.optimize import CacheThresholdOptimizer + + sem_cache = SemanticCache( + name="sem_cache", # underlying search index name + redis_url="redis://localhost:6379", # redis connection url string + distance_threshold=0.5 # semantic cache distance threshold + ) + + paris_key = sem_cache.store(prompt="what is the capital of france?", response="paris") + rabat_key = sem_cache.store(prompt="what is the capital of morocco?", response="rabat") + + test_data = [ + { + "query": "What's the capital of Britain?", + "query_match": "" + }, + { + "query": "What's the capital of France??", + "query_match": paris_key + }, + { + "query": "What's the capital city of Morocco?", + "query_match": rabat_key + }, + ] + + optimizer = CacheThresholdOptimizer(sem_cache, test_data) + optimizer.optimize() + """ def __init__( self, @@ -90,46 +124,11 @@ def __init__( eval_metric (str): Evaluation metric for threshold optimization. Defaults to "f1" score. - .. code-block:: python - from redisvl.extensions.llmcache import SemanticCache - from redisvl.utils.optimize import CacheThresholdOptimizer - - sem_cache = SemanticCache( - name="sem_cache", # underlying search index name - redis_url="redis://localhost:6379", # redis connection url string - distance_threshold=0.5 # semantic cache distance threshold - ) - - paris_key = sem_cache.store(prompt="what is the capital of france?", response="paris") - rabat_key = sem_cache.store(prompt="what is the capital of morocco?", response="rabat") - - test_data = [ - { - "query": "What's the capital of Britain?", - "query_match": "" - }, - { - "query": "What's the capital of France??", - "query_match": paris_key - }, - { - "query": "What's the capital city of Morocco?", - "query_match": rabat_key - }, - ] - - optimizer = CacheThresholdOptimizer(sem_cache, test_data) - optimizer.optimize() + Raises: + ValueError: If the test_dict not in LabeledData format. """ super().__init__(cache, test_dict, opt_fn, eval_metric) def optimize(self, **kwargs: Any): - """Optimize thresholds using the provided optimization function for cache case. - - .. code-block:: python - from redisvl.utils.optimize import CacheThresholdOptimizer - - optimizer = CacheThresholdOptimizer(semantic_cache, test_data) - optimizer.optimize(*kwargs) - """ + """Optimize thresholds using the provided optimization function for cache case.""" self.opt_fn(self.optimizable, self.test_data, self.eval_metric, **kwargs) diff --git a/redisvl/utils/optimize/router.py b/redisvl/utils/optimize/router.py index b5215d9b..8105dddd 100644 --- a/redisvl/utils/optimize/router.py +++ b/redisvl/utils/optimize/router.py @@ -90,7 +90,47 @@ def _random_search_opt_router( class RouterThresholdOptimizer(BaseThresholdOptimizer): - """Class for optimizing thresholds for a SemanticRouter.""" + """ + Class for optimizing thresholds for a SemanticRouter. + + .. code-block:: python + + from redisvl.extensions.router import Route, SemanticRouter + from redisvl.utils.vectorize import HFTextVectorizer + from redisvl.utils.optimize import RouterThresholdOptimizer + + routes = [ + Route( + name="greeting", + references=["hello", "hi"], + metadata={"type": "greeting"}, + distance_threshold=0.5, + ), + Route( + name="farewell", + references=["bye", "goodbye"], + metadata={"type": "farewell"}, + distance_threshold=0.5, + ), + ] + + router = SemanticRouter( + name="greeting-router", + vectorizer=HFTextVectorizer(), + routes=routes, + redis_url="redis://localhost:6379", + overwrite=True # Blow away any other routing index with this name + ) + + test_data = [ + {"query": "hello", "query_match": "greeting"}, + {"query": "goodbye", "query_match": "farewell"}, + ... + ] + + optimizer = RouterThresholdOptimizer(router, test_data) + optimizer.optimize() + """ def __init__( self, @@ -108,54 +148,12 @@ def __init__( grid search. eval_metric (str): Evaluation metric for threshold optimization. Defaults to "f1" score. - - .. code-block:: python - from redisvl.extensions.router import Route, SemanticRouter - from redisvl.utils.vectorize import HFTextVectorizer - from redisvl.utils.optimize import RouterThresholdOptimizer - - routes = [ - Route( - name="greeting", - references=["hello", "hi"], - metadata={"type": "greeting"}, - distance_threshold=0.5, - ), - Route( - name="farewell", - references=["bye", "goodbye"], - metadata={"type": "farewell"}, - distance_threshold=0.5, - ), - ] - - router = SemanticRouter( - name="greeting-router", - vectorizer=HFTextVectorizer(), - routes=routes, - redis_url="redis://localhost:6379", - overwrite=True # Blow away any other routing index with this name - ) - - test_data = [ - {"query": "hello", "query_match": "greeting"}, - {"query": "goodbye", "query_match": "farewell"}, - ... - ] - - optimizer = RouterThresholdOptimizer(router, test_data) - optimizer.optimize() + Raises: + ValueError: If the test_dict not in LabeledData format. """ super().__init__(router, test_dict, opt_fn, eval_metric) def optimize(self, **kwargs: Any): - """Optimize thresholds using the provided optimization function for router case. - - .. code-block:: python - from redisvl.utils.optimize import RouterThresholdOptimizer - - optimizer = RouterThresholdOptimizer(router, test_data) - optimizer.optimize(search_step=0.05, max_iterations=50) - """ + """Optimize kicks of the optimization process for router""" qrels = _format_qrels(self.test_data) self.opt_fn(self.optimizable, self.test_data, qrels, self.eval_metric, **kwargs) From fcd4cc3d76e7b34e1e8052829b20f23f7cd2bbce Mon Sep 17 00:00:00 2001 From: Robert Shelton Date: Fri, 4 Apr 2025 13:57:23 -0400 Subject: [PATCH 7/9] update doc strings for Hybrid and Text --- docs/api/query.rst | 28 ++++++++++++++++++++++ redisvl/query/aggregate.py | 48 ++++++++++++++++++++------------------ redisvl/query/query.py | 43 +++++++++++++++++++--------------- 3 files changed, 77 insertions(+), 42 deletions(-) diff --git a/docs/api/query.rst b/docs/api/query.rst index 8086f5ca..fa92230e 100644 --- a/docs/api/query.rst +++ b/docs/api/query.rst @@ -34,6 +34,34 @@ VectorRangeQuery :show-inheritance: :exclude-members: add_filter,get_args,highlight,return_field,summarize +HybridQuery +================ + + +.. currentmodule:: redisvl.query + + +.. autoclass:: HybridQuery + :members: + :inherited-members: + :show-inheritance: + :exclude-members: add_filter,get_args,highlight,return_field,summarize + + +TextQuery +================ + + +.. currentmodule:: redisvl.query + + +.. autoclass:: TextQuery + :members: + :inherited-members: + :show-inheritance: + :exclude-members: add_filter,get_args,highlight,return_field,summarize + + FilterQuery =========== diff --git a/redisvl/query/aggregate.py b/redisvl/query/aggregate.py index 831b48e9..4e6f4085 100644 --- a/redisvl/query/aggregate.py +++ b/redisvl/query/aggregate.py @@ -23,6 +23,31 @@ class HybridQuery(AggregationQuery): HybridQuery combines text and vector search in Redis. It allows you to perform a hybrid search using both text and vector similarity. It scores documents based on a weighted combination of text and vector similarity. + + .. code-block:: python + + from redisvl.query import HybridQuery + from redisvl.index import SearchIndex + + index = SearchIndex.from_yaml("path/to/index.yaml") + + query = HybridQuery( + text="example text", + text_field_name="text_field", + vector=[0.1, 0.2, 0.3], + vector_field_name="vector_field", + text_scorer="BM25STD", + filter_expression=None, + alpha=0.7, + dtype="float32", + num_results=10, + return_fields=["field1", "field2"], + stopwords="english", + dialect=2, + ) + + results = index.query(query) + """ DISTANCE_ID: str = "vector_distance" @@ -72,29 +97,6 @@ def __init__( ValueError: If the text string is empty, or if the text string becomes empty after stopwords are removed. TypeError: If the stopwords are not a set, list, or tuple of strings. - - .. code-block:: python - from redisvl.query import HybridQuery - from redisvl.index import SearchIndex - - index = SearchIndex.from_yaml("path/to/index.yaml") - - query = HybridQuery( - text="example text", - text_field_name="text_field", - vector=[0.1, 0.2, 0.3], - vector_field_name="vector_field", - text_scorer="BM25STD", - filter_expression=None, - alpha=0.7, - dtype="float32", - num_results=10, - return_fields=["field1", "field2"], - stopwords="english", - dialect=2, - ) - - results = index.query(query) """ if not text.strip(): diff --git a/redisvl/query/query.py b/redisvl/query/query.py index cc4d26f0..ea09bc19 100644 --- a/redisvl/query/query.py +++ b/redisvl/query/query.py @@ -690,6 +690,30 @@ class RangeQuery(VectorRangeQuery): class TextQuery(BaseQuery): + """ + TextQuery is a query for running a full text search, along with an optional filter expression. + + .. code-block:: python + + from redisvl.query import TextQuery + from redisvl.index import SearchIndex + + index = SearchIndex.from_yaml(index.yaml) + + query = TextQuery( + text="example text", + text_field_name="text_field", + text_scorer="BM25STD", + filter_expression=None, + num_results=10, + return_fields=["field1", "field2"], + stopwords="english", + dialect=2, + ) + + results = index.query(query) + """ + def __init__( self, text: str, @@ -739,25 +763,6 @@ def __init__( Raises: ValueError: if stopwords language string cannot be loaded. TypeError: If stopwords is not a valid iterable set of strings. - - .. code-block:: python - from redisvl.query import TextQuery - from redisvl.index import SearchIndex - - index = SearchIndex.from_yaml(index.yaml) - - query = TextQuery( - text="example text", - text_field_name="text_field", - text_scorer="BM25STD", - filter_expression=None, - num_results=10, - return_fields=["field1", "field2"], - stopwords="english", - dialect=2, - ) - - results = index.query(query) """ self._text = text self._text_field = text_field_name From 9320d21a9ef4d8ac0d2a5309bb67309d485a0312 Mon Sep 17 00:00:00 2001 From: Robert Shelton Date: Fri, 4 Apr 2025 14:25:27 -0400 Subject: [PATCH 8/9] fix test --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b2747769..2b95fe8a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -170,7 +170,7 @@ jobs: if [[ "${{ matrix.python-version }}" > "3.9" ]]; then make test-notebooks else - poetry run test-notebooks --ignore ./docs/user_guide/09_threshold_optimization.ipynb + poetry run test-notebooks --ignore ./docs/user_guide/09_threshold_optimization.ipynb --ignore ./docs/user_guide/release_guide/0.5.0_release.ipynb fi docs: From f4e060a56d84e3149dd6aa5e7c77ced3bc1242d1 Mon Sep 17 00:00:00 2001 From: Robert Shelton Date: Fri, 4 Apr 2025 14:42:28 -0400 Subject: [PATCH 9/9] fix test #2 --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2b95fe8a..25bc8d22 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -170,7 +170,7 @@ jobs: if [[ "${{ matrix.python-version }}" > "3.9" ]]; then make test-notebooks else - poetry run test-notebooks --ignore ./docs/user_guide/09_threshold_optimization.ipynb --ignore ./docs/user_guide/release_guide/0.5.0_release.ipynb + poetry run test-notebooks --ignore ./docs/user_guide/09_threshold_optimization.ipynb --ignore ./docs/user_guide/release_guide/0_5_0_release.ipynb fi docs: