0.5.1 Feature Overview

Overview

This notebook provides an overview of what's new with the 0.5.1 release of redisvl. It also highlights changes and potential enhancements for existing usage.

What's new?

- Hybrid query and text query classes
- Threshold optimizer classes
- Schema validation
- Timestamp filters
- Batched queries
- Vector normalization
- Hybrid policy on knn with filters

Define and load index for examples

[code example]

    12:44:52 redisvl.index.index INFO   Index already exists, overwriting.

    ['jobs:01JR0V1SA29RVD9AAVSTBV9P5H',
     'jobs:01JR0V1SA209KMVHMD7G54P3H5',
     'jobs:01JR0V1SA23ZE7BRERXTZWC33Z']

# HybridQuery class

Perform hybrid lexical (BM25) and vector search where results are ranked by: `hybrid_score = (1-alpha)*lexical_Score + alpha*vector_similarity`.

[code example]

    [{'vector_distance': '0.61871612072',
      'job_title': 'Software Engineer',
      'vector_similarity': '0.69064193964',
      'text_score': '49.6242910712',
      'hybrid_score': '15.3707366791'},
     {'vector_distance': '0.937997639179',
      'job_title': 'Marketing Manager',
      'vector_similarity': '0.53100118041',
      'text_score': '49.6242910712',
      'hybrid_score': '15.2589881476'},
     {'vector_distance': '0.859166145325',
      'job_title': 'Data Analyst',
      'vector_similarity': '0.570416927338',
      'text_score': '0',
      'hybrid_score': '0.399291849136'}]

# TextQueries

TextQueries make it easy to perform pure lexical search with redisvl.

[code example]

    [{'id': 'jobs:01JR0V1SA29RVD9AAVSTBV9P5H',
      'score': 49.62429107116745,
      'job_title': 'Software Engineer'},
     {'id': 'jobs:01JR0V1SA23ZE7BRERXTZWC33Z',
      'score': 49.62429107116745,
      'job_title': 'Marketing Manager'}]

# Threshold optimization

In redis 0.5.0 we added the ability to quickly configure either your semantic cache or semantic router with test data examples.

For a step by step guide see: [09_threshold_optimization.ipynb](../09_threshold_optimization.ipynb).

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).

[code example]

    Distance threshold before: 0.5

    Distance threshold after: 0.13050847457627118

# Schema validation

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`

[code example]

    16:20:25 redisvl.index.index ERROR   Schema validation error while loading data
    Traceback (most recent call last):
      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
        processed_obj = self._validate(processed_obj)
                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
      File "/Users/robert.shelton/.pyenv/versions/3.11.9/lib/python3.11/site-packages/redisvl/index/storage.py", line 160, in _validate
        return validate_object(self.index_schema, obj)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
      File "/Users/robert.shelton/.pyenv/versions/3.11.9/lib/python3.11/site-packages/redisvl/schema/validation.py", line 276, in validate_object
        validated = model_class.model_validate(flat_obj)
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
      File "/Users/robert.shelton/.pyenv/versions/3.11.9/lib/python3.11/site-packages/pydantic/main.py", line 627, in model_validate
        return cls.__pydantic_validator__.validate_python(
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    pydantic_core._pydantic_core.ValidationError: 2 validation errors for cars__PydanticModel
    mpg.int
      Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='twenty-two', input_type=str]
        For further information visit https://errors.pydantic.dev/2.10/v/int_parsing
    mpg.float
      Input should be a valid number, unable to parse string as a number [type=float_parsing, input_value='twenty-two', input_type=str]
        For further information visit https://errors.pydantic.dev/2.10/v/float_parsing

    The above exception was the direct cause of the following exception:

    Traceback (most recent call last):
      File "/Users/robert.shelton/.pyenv/versions/3.11.9/lib/python3.11/site-packages/redisvl/index/index.py", line 615, in load
        return self._storage.write(
               ^^^^^^^^^^^^^^^^^^^^
      File "/Users/robert.shelton/.pyenv/versions/3.11.9/lib/python3.11/site-packages/redisvl/index/storage.py", line 265, in write
        prepared_objects = self._preprocess_and_validate_objects(
                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
      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
        raise SchemaValidationError(str(e), index=i) from e
    redisvl.exceptions.SchemaValidationError: Validation failed for object at index 1: 2 validation errors for cars__PydanticModel
    mpg.int
      Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='twenty-two', input_type=str]
        For further information visit https://errors.pydantic.dev/2.10/v/int_parsing
    mpg.float
      Input should be a valid number, unable to parse string as a number [type=float_parsing, input_value='twenty-two', input_type=str]
        For further information visit https://errors.pydantic.dev/2.10/v/float_parsing
    Error loading data: Validation failed for object at index 1: 2 validation errors for cars__PydanticModel
    mpg.int
      Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='twenty-two', input_type=str]
        For further information visit https://errors.pydantic.dev/2.10/v/int_parsing
    mpg.float
      Input should be a valid number, unable to parse string as a number [type=float_parsing, input_value='twenty-two', input_type=str]
        For further information visit https://errors.pydantic.dev/2.10/v/float_parsing

# Timestamp filters

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.

[code example]

    [{'id': 'jobs:01JQYMYZBA6NM6DX9YW35MCHJZ',
      'job_title': 'Software Engineer',
      'job_description': 'Develop and maintain web applications using JavaScript, React, and Node.js.',
      'posted': '1743625199.9'},
     {'id': 'jobs:01JQYMYZBABXYR96H96SQ99ZPS',
      'job_title': 'Data Analyst',
      'job_description': 'Analyze large datasets to provide business insights and create data visualizations.',
      'posted': '1743106799.9'},
     {'id': 'jobs:01JQYMYZBAGEBDS270EZADQ1TM',
      'job_title': 'Marketing Manager',
      'job_description': 'Develop and implement marketing strategies to drive brand awareness and customer engagement.',
      'posted': '1741123199.9'}]

[code example]

    [{'id': 'jobs:01JQYMYZBA6NM6DX9YW35MCHJZ',
      'job_title': 'Software Engineer',
      'job_description': 'Develop and maintain web applications using JavaScript, React, and Node.js.',
      'posted': '1743625199.9'}]

[code example]

    [{'id': 'jobs:01JQYMYZBABXYR96H96SQ99ZPS',
      'job_title': 'Data Analyst',
      'job_description': 'Analyze large datasets to provide business insights and create data visualizations.',
      'posted': '1743106799.9'}]

# Batch search

This enhancement allows you to speed up the execution of queries by reducing the impact of network latency.

[code example]

    Time taken for 200 queries: 0.11 seconds

[code example]

    Time taken for 200 batched queries: 0.03 seconds

# Vector normalization

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.

[code example]

    [{'id': 'jobs:01JQYMYZBA6NM6DX9YW35MCHJZ',
      'vector_distance': '0.7090711295605',
      'job_title': 'Software Engineer',
      'job_description': 'Develop and maintain web applications using JavaScript, React, and Node.js.',
      'posted': '1743625199.9'},
     {'id': 'jobs:01JQYMYZBABXYR96H96SQ99ZPS',
      'vector_distance': '0.6049451231955',
      'job_title': 'Data Analyst',
      'job_description': 'Analyze large datasets to provide business insights and create data visualizations.',
      'posted': '1743106799.9'},
     {'id': 'jobs:01JQYMYZBAGEBDS270EZADQ1TM',
      'vector_distance': '0.553376108408',
      'job_title': 'Marketing Manager',
      'job_description': 'Develop and implement marketing strategies to drive brand awareness and customer engagement.',
      'posted': '1741123199.9'}]

# Hybrid policy on knn with filters

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.

[code example]

    [{'id': 'jobs:01JQYMYZBA6NM6DX9YW35MCHJZ',
      'vector_distance': '0.581857740879',
      'job_title': 'Software Engineer',
      'job_description': 'Develop and maintain web applications using JavaScript, React, and Node.js.',
      'posted': '1743625199.9'},
     {'id': 'jobs:01JQYMYZBAGEBDS270EZADQ1TM',
      'vector_distance': '0.893247783184',
      'job_title': 'Marketing Manager',
      'job_description': 'Develop and implement marketing strategies to drive brand awareness and customer engagement.',
      'posted': '1741123199.9'}]

from redisvl.utils.vectorize import HFTextVectorizer
from redisvl.index import SearchIndex
import datetime as dt

import warnings
warnings.filterwarnings("ignore", category=UserWarning, module="redis")

# Embedding model
emb_model = HFTextVectorizer()

REDIS_URL = "redis://localhost:6379/0"
NOW = dt.datetime.now()

job_data = [
  {
    "job_title": "Software Engineer",
    "job_description": "Develop and maintain web applications using JavaScript, React, and Node.js.",
    "posted": (NOW - dt.timedelta(days=1)).timestamp() # day ago
  },
  {
    "job_title": "Data Analyst",
    "job_description": "Analyze large datasets to provide business insights and create data visualizations.",
    "posted": (NOW - dt.timedelta(days=7)).timestamp() # week ago
  },
  {
    "job_title": "Marketing Manager",
    "job_description": "Develop and implement marketing strategies to drive brand awareness and customer engagement.",
    "posted": (NOW - dt.timedelta(days=30)).timestamp() # month ago
  }
]

job_data = [{**job, "job_embedding": emb_model.embed(job["job_description"], as_buffer=True)} for job in job_data]

job_schema = {
    "index": {
        "name": "jobs",
        "prefix": "jobs",
        "storage_type": "hash",
    },
    "fields": [
        {"name": "job_title", "type": "text"},
        {"name": "job_description", "type": "text"},
        {"name": "posted", "type": "numeric"},
        {
            "name": "job_embedding",
            "type": "vector",
            "attrs": {
                "dims": 768,
                "distance_metric": "cosine",
                "algorithm": "flat",
                "datatype": "float32"
            }

        }
    ],
}

index = SearchIndex.from_dict(job_schema, redis_url=REDIS_URL)
index.create(overwrite=True, drop=True)
index.load(job_data)

from redisvl.query import HybridQuery

text = "Find a job as a where you develop software"
vec = emb_model.embed(text, as_buffer=True)

query = HybridQuery(
    text=text,
    text_field_name="job_description",
    vector=vec,
    vector_field_name="job_embedding",
    alpha=0.7,
    num_results=10,
    return_fields=["job_title"],
)

results = index.query(query)
results

from redisvl.query import TextQuery

text = "Find where you develop software"

query = TextQuery(
    text=text,
    text_field_name="job_description",
    return_fields=["job_title"],
    num_results=10,
)

results = index.query(query)
results

from redisvl.utils.optimize import CacheThresholdOptimizer
from redisvl.extensions.cache.llm import SemanticCache

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
    },
]

print(f"\nDistance threshold before: {sem_cache.distance_threshold} \n")
optimizer = CacheThresholdOptimizer(sem_cache, test_data)
optimizer.optimize()
print(f"\nDistance threshold after: {sem_cache.distance_threshold} \n")

# NBVAL_SKIP
from redisvl.index import SearchIndex

# sample schema
car_schema = {
    "index": {
        "name": "cars",
        "prefix": "cars",
        "storage_type": "json",
    },
    "fields": [
        {"name": "make", "type": "text"},
        {"name": "model", "type": "text"},
        {"name": "description", "type": "text"},
        {"name": "mpg", "type": "numeric"},
        {
            "name": "car_embedding",
            "type": "vector",
            "attrs": {
                "dims": 3,
                "distance_metric": "cosine",
                "algorithm": "flat",
                "datatype": "float32"
            }

        }
    ],
}

sample_data_bad = [
    {
        "make": "Toyota",
        "model": "Camry",
        "description": "A reliable sedan with great fuel economy.",
        "mpg": 28,
        "car_embedding": [0.1, 0.2, 0.3]
    },
    {
        "make": "Honda",
        "model": "CR-V",
        "description": "A practical SUV with advanced technology.",
        # incorrect type will throw an error
        "mpg": "twenty-two",
        "car_embedding": [0.4, 0.5, 0.6]
    }
]

# this should now throw an error
car_index = SearchIndex.from_dict(car_schema, redis_url=REDIS_URL, validate_on_load=True)
car_index.create(overwrite=True)

try:
    car_index.load(sample_data_bad)
except Exception as e:
    print(f"Error loading data: {e}")

from redisvl.query import FilterQuery
from redisvl.query.filter import Timestamp

# find all jobs
ts = Timestamp("posted") < NOW # now datetime created above

filter_query = FilterQuery(
    return_fields=["job_title", "job_description", "posted"],
    filter_expression=ts,
    num_results=10,
)
res = index.query(filter_query)
res

# jobs posted in the last 3 days => 1 job
ts = Timestamp("posted") > NOW - dt.timedelta(days=3)

filter_query = FilterQuery(
    return_fields=["job_title", "job_description", "posted"],
    filter_expression=ts,
    num_results=10,
)
res = index.query(filter_query)
res

# more than 3 days ago but less than 14 days ago => 1 job
ts = Timestamp("posted").between(
    NOW - dt.timedelta(days=14),
    NOW - dt.timedelta(days=3),
)

filter_query = FilterQuery(
    return_fields=["job_title", "job_description", "posted"],
    filter_expression=ts,
    num_results=10,
)

res = index.query(filter_query)
res

import time
num_queries = 200

start = time.time()
for i in range(num_queries):
    # run the same filter query
    res = index.query(filter_query)
end = time.time()
print(f"Time taken for {num_queries} queries: {end - start:.2f} seconds")

batched_queries = [filter_query] * num_queries

start = time.time()

index.batch_search(batched_queries, batch_size=10)

end = time.time()
print(f"Time taken for {num_queries} batched queries: {end - start:.2f} seconds")

from redisvl.query import VectorQuery

query = VectorQuery(
    vector=emb_model.embed("Software Engineer", as_buffer=True),
    vector_field_name="job_embedding",
    return_fields=["job_title", "job_description", "posted"],
    normalize_vector_distance=True,
)

res = index.query(query)
res

from redisvl.query.filter import Text

filter = Text("job_description") % "Develop"

query = VectorQuery(
    vector=emb_model.embed("Software Engineer", as_buffer=True),
    vector_field_name="job_embedding",
    return_fields=["job_title", "job_description", "posted"],
    hybrid_policy="BATCHES"
)

query.set_filter(filter)

res = index.query(query)
res


Release Guides

0.5.1 Feature Overview

Content

This section contains guidelines and information for RedisVL releases.

* [0.5.1 Feature Overview](0_5_0_release/)
  * [What’s new?](0_5_0_release/#what-s-new)
  * [Define and load index for examples](0_5_0_release/#define-and-load-index-for-examples)
* [HybridQuery class](0_5_0_release/#hybridquery-class)
* [TextQueries](0_5_0_release/#textqueries)
* [Threshold optimization](0_5_0_release/#threshold-optimization)
* [Schema validation](0_5_0_release/#schema-validation)
* [Timestamp filters](0_5_0_release/#timestamp-filters)
* [Batch search](0_5_0_release/#batch-search)
* [Vector normalization](0_5_0_release/#vector-normalization)
* [Hybrid policy on knn with filters](0_5_0_release/#hybrid-policy-on-knn-with-filters)


Rerankers

Overview

In this notebook, we will show how to use RedisVL to rerank search results
(documents or chunks or records) based on the input query. Today RedisVL
supports reranking through:

- A re-ranker that uses pre-trained [Cross-Encoders](https://sbert.net/examples/applications/cross-encoder/README.html) which can use models from [Hugging Face cross encoder models](https://huggingface.co/cross-encoder) or Hugging Face models that implement a cross encoder function ([example: BAAI/bge-reranker-base](https://huggingface.co/BAAI/bge-reranker-base)).
- The [Cohere /rerank API](https://docs.cohere.com/docs/rerank-2).
- The [VoyageAI /rerank API](https://docs.voyageai.com/docs/reranker).

Before running this notebook, be sure to:
1. Have installed ``redisvl`` and have that environment active for this notebook.
2. Have a running Redis Stack instance with RediSearch > 2.4 active.

For example, you can run Redis Stack locally with Docker:

[code example]

This will run Redis on port 6379 and RedisInsight at http://localhost:8001.

[code example]

Simple Reranking

Reranking provides a relevance boost to search results generated by
traditional (lexical) or semantic search strategies.

As a simple demonstration, take the passages and user query below:

[code example]

The goal of reranking is to provide a more fine-grained quality improvement to
initial search results. With RedisVL, this would likely be results coming back
from a search operation like full text or vector.

Using the Cross-Encoder Reranker

To use the cross-encoder reranker we initialize an instance of `HFCrossEncoderReranker` passing a suitable model (if no model is provided, the `cross-encoder/ms-marco-MiniLM-L-6-v2` model is used):

[code example]

Rerank documents with HFCrossEncoderReranker

With the obtained reranker instance we can rerank and truncate the list of
documents based on relevance to the initial query.

[code example]

[code example]

    0.07461125403642654  --  {'content': 'Washington, D.C. (also known as simply Washington or D.C., and officially as the District of Columbia) is the capital of the United States. It is a federal district. The President of the USA and many major national government offices are in the territory. This makes it the political center of the United States of America.'}
    0.05220315232872963  --  {'content': 'Charlotte Amalie is the capital and largest city of the United States Virgin Islands. It has about 20,000 people. The city is on the island of Saint Thomas.'}
    0.3802368640899658  --  {'content': 'Carson City is the capital city of the American state of Nevada. At the 2010 United States Census, Carson City had a population of 55,274.'}

Using the Cohere Reranker

To initialize the Cohere reranker you'll need to install the cohere library and provide the right Cohere API Key.

[code example]

[code example]

[code example]

Rerank documents with CohereReranker

Below we will use the `CohereReranker` to rerank and truncate the list of
documents above based on relevance to the initial query.

[code example]

[code example]

    0.9990564  --  Washington, D.C. (also known as simply Washington or D.C., and officially as the District of Columbia) is the capital of the United States. It is a federal district. The President of the USA and many major national government offices are in the territory. This makes it the political center of the United States of America.
    0.7516481  --  Capital punishment (the death penalty) has existed in the United States since before the United States was a country. As of 2017, capital punishment is legal in 30 of the 50 states. The federal government (including the United States military) also uses capital punishment.
    0.08882029  --  The Commonwealth of the Northern Mariana Islands is a group of islands in the Pacific Ocean that are a political division controlled by the United States. Its capital is Saipan.

Working with semi-structured documents

Often times the initial result set includes other metadata and components that could be used to steer the reranking relevancy. To accomplish this, we can set the `rank_by` argument and provide documents with those additional fields.

[code example]

[code example]

[code example]

    0.9988121  --  {'source': 'textbook', 'passage': 'Washington, D.C. (also known as simply Washington or D.C., and officially as the District of Columbia) is the capital of the United States. It is a federal district. The President of the USA and many major national government offices are in the territory. This makes it the political center of the United States of America.'}
    0.5974905  --  {'source': 'wiki', 'passage': 'Capital punishment (the death penalty) has existed in the United States since before the United States was a country. As of 2017, capital punishment is legal in 30 of the 50 states. The federal government (including the United States military) also uses capital punishment.'}
    0.059101548  --  {'source': 'encyclopedia', 'passage': 'The Commonwealth of the Northern Mariana Islands is a group of islands in the Pacific Ocean that are a political division controlled by the United States. Its capital is Saipan.'}

Using the VoyageAI Reranker

To initialize the VoyageAI reranker you'll need to install the voyaeai library and provide the right VoyageAI API Key.

[code example]

[code example]

[code example]

Rerank documents with VoyageAIReranker

Below we will use the `VoyageAIReranker` to rerank and also truncate the list of
documents above based on relevance to the initial query.

[code example]

[code example]

    0.796875  --  Washington, D.C. (also known as simply Washington or D.C., and officially as the District of Columbia) is the capital of the United States. It is a federal district. The President of the USA and many major national government offices are in the territory. This makes it the political center of the United States of America.
    0.578125  --  Charlotte Amalie is the capital and largest city of the United States Virgin Islands. It has about 20,000 people. The city is on the island of Saint Thomas.
    0.5625  --  Carson City is the capital city of the American state of Nevada. At the 2010 United States Census, Carson City had a population of 55,274.

docker run -d -p 6379:6379 -p 8001:8001 redis/redis-stack:latest

query = "What is the capital of the United States?"
docs = [
    "Carson City is the capital city of the American state of Nevada. At the 2010 United States Census, Carson City had a population of 55,274.",
    "The Commonwealth of the Northern Mariana Islands is a group of islands in the Pacific Ocean that are a political division controlled by the United States. Its capital is Saipan.",
    "Charlotte Amalie is the capital and largest city of the United States Virgin Islands. It has about 20,000 people. The city is on the island of Saint Thomas.",
    "Washington, D.C. (also known as simply Washington or D.C., and officially as the District of Columbia) is the capital of the United States. It is a federal district. The President of the USA and many major national government offices are in the territory. This makes it the political center of the United States of America.",
    "Capital punishment (the death penalty) has existed in the United States since before the United States was a country. As of 2017, capital punishment is legal in 30 of the 50 states. The federal government (including the United States military) also uses capital punishment."
]

from redisvl.utils.rerank import HFCrossEncoderReranker

cross_encoder_reranker = HFCrossEncoderReranker("BAAI/bge-reranker-base")

results, scores = cross_encoder_reranker.rank(query=query, docs=docs)

for result, score in zip(results, scores):
    print(score, " -- ", result)

import getpass

# setup the API Key
api_key = os.environ.get("COHERE_API_KEY") or getpass.getpass("Enter your Cohere API key: ")

from redisvl.utils.rerank import CohereReranker

cohere_reranker = CohereReranker(limit=3, api_config={"api_key": api_key})

results, scores = cohere_reranker.rank(query=query, docs=docs)

docs = [
    {
        "source": "wiki",
        "passage": "Carson City is the capital city of the American state of Nevada. At the 2010 United States Census, Carson City had a population of 55,274."
    },
    {
        "source": "encyclopedia",
        "passage": "The Commonwealth of the Northern Mariana Islands is a group of islands in the Pacific Ocean that are a political division controlled by the United States. Its capital is Saipan."
    },
    {
        "source": "textbook",
        "passage": "Charlotte Amalie is the capital and largest city of the United States Virgin Islands. It has about 20,000 people. The city is on the island of Saint Thomas."
    },
    {
        "source": "textbook",
        "passage": "Washington, D.C. (also known as simply Washington or D.C., and officially as the District of Columbia) is the capital of the United States. It is a federal district. The President of the USA and many major national government offices are in the territory. This makes it the political center of the United States of America."
    },
    {
        "source": "wiki",
        "passage": "Capital punishment (the death penalty) has existed in the United States since before the United States was a country. As of 2017, capital punishment is legal in 30 of the 50 states. The federal government (including the United States military) also uses capital punishment."
    }
]

results, scores = cohere_reranker.rank(query=query, docs=docs, rank_by=["passage", "source"])

import getpass

# setup the API Key
api_key = os.environ.get("VOYAGE_API_KEY") or getpass.getpass("Enter your VoyageAI API key: ")

from redisvl.utils.rerank import VoyageAIReranker

reranker = VoyageAIReranker(model="rerank-lite-1", limit=3, api_config={"api_key": api_key})# Please check the available models at https://docs.voyageai.com/docs/reranker

results, scores = reranker.rank(query=query, docs=docs)


Semantic Routing

Overview

RedisVL provides a `SemanticRouter` interface to utilize Redis' built-in search & aggregation in order to perform
KNN-style classification over a set of `Route` references to determine the best match.

This notebook will go over how to use Redis as a Semantic Router for your applications

Define the Routes

Below we define 3 different routes. One for `technology`, one for `sports`, and
another for `entertainment`. Now for this example, the goal here is
surely topic "classification". But you can create routes and references for
almost anything.

Each route has a set of references that cover the "semantic surface area" of the
route. The incoming query from a user needs to be semantically similar to one or
more of the references in order to "match" on the route.

Additionally, each route has a `distance_threshold` which determines the maximum distance between the query and the reference for the query to be routed to the route. This value is unique to each route.

[code example]

Initialize the SemanticRouter

``SemanticRouter`` will automatically create an index within Redis upon initialization for the route references. By default, it uses the `HFTextVectorizer` to
generate embeddings for each route reference.

[code example]

    /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
      from .autonotebook import tqdm as notebook_tqdm

    13:03:49 sentence_transformers.SentenceTransformer INFO   Use pytorch device_name: mps
    13:03:49 sentence_transformers.SentenceTransformer INFO   Load pretrained SentenceTransformer: sentence-transformers/all-mpnet-base-v2

    Batches: 100%|██████████| 1/1 [00:00<00:00,  6.31it/s]
    Batches: 100%|██████████| 1/1 [00:00<00:00,  7.02it/s]
    Batches: 100%|██████████| 1/1 [00:00<00:00,  8.21it/s]
    Batches: 100%|██████████| 1/1 [00:00<00:00, 54.33it/s]

[code example]

    Index Information:
    ╭──────────────────┬──────────────────┬──────────────────┬──────────────────┬──────────────────╮
    │ Index Name       │ Storage Type     │ Prefixes         │ Index Options    │ Indexing         │
    ├──────────────────┼──────────────────┼──────────────────┼──────────────────┼──────────────────┤
    | topic-router     | HASH             | ['topic-router'] | []               | 0                |
    ╰──────────────────┴──────────────────┴──────────────────┴──────────────────┴──────────────────╯
    Index Fields:
    ╭─────────────────┬─────────────────┬─────────────────┬─────────────────┬─────────────────┬─────────────────┬─────────────────┬─────────────────┬─────────────────┬─────────────────┬─────────────────╮
    │ Name            │ Attribute       │ Type            │ Field Option    │ Option Value    │ Field Option    │ Option Value    │ Field Option    │ Option Value    │ Field Option    │ Option Value    │
    ├─────────────────┼─────────────────┼─────────────────┼─────────────────┼─────────────────┼─────────────────┼─────────────────┼─────────────────┼─────────────────┼─────────────────┼─────────────────┤
    │ reference_id    │ reference_id    │ TAG             │ SEPARATOR       │ ,               │                 │                 │                 │                 │                 │                 │
    │ route_name      │ route_name      │ TAG             │ SEPARATOR       │ ,               │                 │                 │                 │                 │                 │                 │
    │ reference       │ reference       │ TEXT            │ WEIGHT          │ 1               │                 │                 │                 │                 │                 │                 │
    │ vector          │ vector          │ VECTOR          │ algorithm       │ FLAT            │ data_type       │ FLOAT32         │ dim             │ 768             │ distance_metric │ COSINE          │
    ╰─────────────────┴─────────────────┴─────────────────┴─────────────────┴─────────────────┴─────────────────┴─────────────────┴─────────────────┴─────────────────┴─────────────────┴─────────────────╯

[code example]

    11

Simple routing

[code example]

    Batches: 100%|██████████| 1/1 [00:00<00:00,  8.63it/s]

    RouteMatch(name='technology', distance=0.419145941734)

[code example]

    Batches: 100%|██████████| 1/1 [00:00<00:00, 11.71it/s]

    RouteMatch(name=None, distance=None)

We can also route a statement to many routes and order them by distance:

[code example]

    Batches: 100%|██████████| 1/1 [00:00<00:00, 12.12it/s]

    [RouteMatch(name='technology', distance=0.556493639946),
     RouteMatch(name='sports', distance=0.671060085297)]

[code example]

    Batches: 100%|██████████| 1/1 [00:00<00:00, 56.69it/s]

    [RouteMatch(name='technology', distance=0.556493639946),
     RouteMatch(name='sports', distance=0.629264354706)]

Note the different route match distances. This is because we used the `min` aggregation method instead of the default `avg` approach.

Update the routing config

[code example]

[code example]

    Batches: 100%|██████████| 1/1 [00:00<00:00, 13.20it/s]

    [RouteMatch(name='sports', distance=0.663253903389)]

Router serialization

[code example]

    {'name': 'topic-router',
     'routes': [{'name': 'technology',
       'references': ['what are the latest advancements in AI?',
        'tell me about the newest gadgets',
        "what's trending in tech?"],
       'metadata': {'category': 'tech', 'priority': 1},
       'distance_threshold': 0.71},
      {'name': 'sports',
       'references': ['who won the game last night?',
        'tell me about the upcoming sports events',
        "what's the latest in the world of sports?",
        'sports',
        'basketball and football'],
       'metadata': {'category': 'sports', 'priority': 2},
       'distance_threshold': 0.72},
      {'name': 'entertainment',
       'references': ['what are the top movies right now?',
        'who won the best actor award?',
        "what's new in the entertainment industry?"],
       'metadata': {'category': 'entertainment', 'priority': 3},
       'distance_threshold': 0.7}],
     'vectorizer': {'type': 'hf',
      'model': 'sentence-transformers/all-mpnet-base-v2'},
     'routing_config': {'max_k': 3, 'aggregation_method': 'min'}}

[code example]

    13:03:54 sentence_transformers.SentenceTransformer INFO   Use pytorch device_name: mps
    13:03:54 sentence_transformers.SentenceTransformer INFO   Load pretrained SentenceTransformer: sentence-transformers/all-mpnet-base-v2

    Batches: 100%|██████████| 1/1 [00:00<00:00, 53.91it/s]

    13:03:54 redisvl.index.index INFO   Index already exists, not overwriting.

[code example]

[code example]

    13:03:54 sentence_transformers.SentenceTransformer INFO   Use pytorch device_name: mps
    13:03:54 sentence_transformers.SentenceTransformer INFO   Load pretrained SentenceTransformer: sentence-transformers/all-mpnet-base-v2

    Batches: 100%|██████████| 1/1 [00:00<00:00, 51.94it/s]

    13:03:55 redisvl.index.index INFO   Index already exists, not overwriting.

Add route references

[code example]

    Batches: 100%|██████████| 1/1 [00:00<00:00,  8.12it/s]

    ['topic-router:technology:f243fb2d073774e81c7815247cb3013794e6225df3cbe3769cee8c6cefaca777',
     'topic-router:technology:7e4bca5853c1c3298b4d001de13c3c7a79a6e0f134f81acc2e7cddbd6845961f']

Get route references

[code example]

    [{'id': 'topic-router:technology:85cc73a1437df27caa2f075a29c497e5a2e532023fbb75378aedbae80779ab37',
      'reference_id': '85cc73a1437df27caa2f075a29c497e5a2e532023fbb75378aedbae80779ab37',
      'route_name': 'technology',
      'reference': 'tell me about the newest gadgets'},
     {'id': 'topic-router:technology:851f51cce5a9ccfbbcb66993908be6b7871479af3e3a4b139ad292a1bf7e0676',
      'reference_id': '851f51cce5a9ccfbbcb66993908be6b7871479af3e3a4b139ad292a1bf7e0676',
      'route_name': 'technology',
      'reference': 'what are the latest advancements in AI?'},
     {'id': 'topic-router:technology:f243fb2d073774e81c7815247cb3013794e6225df3cbe3769cee8c6cefaca777',
      'reference_id': 'f243fb2d073774e81c7815247cb3013794e6225df3cbe3769cee8c6cefaca777',
      'route_name': 'technology',
      'reference': 'latest AI trends'},
     {'id': 'topic-router:technology:7e4bca5853c1c3298b4d001de13c3c7a79a6e0f134f81acc2e7cddbd6845961f',
      'reference_id': '7e4bca5853c1c3298b4d001de13c3c7a79a6e0f134f81acc2e7cddbd6845961f',
      'route_name': 'technology',
      'reference': 'new tech gadgets'},
     {'id': 'topic-router:technology:149a9c9919c58534aa0f369e85ad95ba7f00aa0513e0f81e2aff2ea4a717b0e0',
      'reference_id': '149a9c9919c58534aa0f369e85ad95ba7f00aa0513e0f81e2aff2ea4a717b0e0',
      'route_name': 'technology',
      'reference': "what's trending in tech?"}]

[code example]

    [{'id': 'topic-router:technology:85cc73a1437df27caa2f075a29c497e5a2e532023fbb75378aedbae80779ab37',
      'reference_id': '85cc73a1437df27caa2f075a29c497e5a2e532023fbb75378aedbae80779ab37',
      'route_name': 'technology',
      'reference': 'tell me about the newest gadgets'}]

Delete route references

[code example]

    5

[code example]

    1

Clean up the router

[code example]

[code example]

from redisvl.extensions.router import Route

# Define routes for the semantic router
technology = Route(
    name="technology",
    references=[
        "what are the latest advancements in AI?",
        "tell me about the newest gadgets",
        "what's trending in tech?"
    ],
    metadata={"category": "tech", "priority": 1},
    distance_threshold=0.71
)

sports = Route(
    name="sports",
    references=[
        "who won the game last night?",
        "tell me about the upcoming sports events",
        "what's the latest in the world of sports?",
        "sports",
        "basketball and football"
    ],
    metadata={"category": "sports", "priority": 2},
    distance_threshold=0.72
)

entertainment = Route(
    name="entertainment",
    references=[
        "what are the top movies right now?",
        "who won the best actor award?",
        "what's new in the entertainment industry?"
    ],
    metadata={"category": "entertainment", "priority": 3},
    distance_threshold=0.7
)

import os
from redisvl.extensions.router import SemanticRouter
from redisvl.utils.vectorize import HFTextVectorizer

os.environ["TOKENIZERS_PARALLELISM"] = "false"

# Initialize the SemanticRouter
router = SemanticRouter(
    name="topic-router",
    vectorizer=HFTextVectorizer(),
    routes=[technology, sports, entertainment],
    redis_url="redis://localhost:6379",
    overwrite=True # Blow away any other routing index with this name
)

# look at the index specification created for the semantic router
!rvl index info -i topic-router

# Query the router with a statement
route_match = router("Can you tell me about the latest in artificial intelligence?")
route_match

# Query the router with a statement and return a miss
route_match = router("are aliens real?")
route_match

# Perform multi-class classification with route_many() -- toggle the max_k and the distance_threshold
route_matches = router.route_many("How is AI used in basketball?", max_k=3)
route_matches

# Toggle the aggregation method -- note the different distances in the result
from redisvl.extensions.router.schema import DistanceAggregationMethod

route_matches = router.route_many("How is AI used in basketball?", aggregation_method=DistanceAggregationMethod.min, max_k=3)
route_matches

from redisvl.extensions.router import RoutingConfig

router.update_routing_config(
    RoutingConfig(aggregation_method=DistanceAggregationMethod.min, max_k=3)
)

route_matches = router.route_many("Lebron James")
route_matches

router2 = SemanticRouter.from_dict(router.to_dict(), redis_url="redis://localhost:6379")

assert router2.to_dict() == router.to_dict()

router3 = SemanticRouter.from_yaml("router.yaml", redis_url="redis://localhost:6379")

assert router3.to_dict() == router2.to_dict() == router.to_dict()

router.add_route_references(route_name="technology", references=["latest AI trends", "new tech gadgets"])

# by route name
refs = router.get_route_references(route_name="technology")
refs

# by reference id
refs = router.get_route_references(reference_ids=[refs[0]["reference_id"]])
refs

# by route name
deleted_count = router.delete_route_references(route_name="sports")
deleted_count

# by id
deleted_count = router.delete_route_references(reference_ids=[refs[0]["reference_id"]])
deleted_count

# Use clear to flush all routes from the index
router.clear()

# Use delete to clear the index and remove it completely
router.delete()


LLM Session Memory

Overview

Large Language Models are inherently stateless and have no knowledge of previous interactions with a user, or even of previous parts of the current conversation. While this may not be noticable when asking simple questions, it becomes a hinderance when engaging in long running conversations that rely on conversational context.

The solution to this problem is to append the previous conversation history to each subsequent call to the LLM.

This notebook will show how to use Redis to structure and store and retrieve this conversational session memory.

[code example]

    12:24:11 redisvl.index.index INFO   Index already exists, not overwriting.

To align with common LLM APIs, Redis stores messages with `role` and `content` fields.
The supported roles are "system", "user" and "llm".

You can store messages one at a time or all at once.

[code example]

At any point we can retrieve the recent history of the conversation. It will be ordered by entry time.

[code example]

    {'role': 'llm', 'content': 'The capital is Paris.'}
    {'role': 'user', 'content': 'And what is the capital of Spain?'}
    {'role': 'llm', 'content': 'The capital is Madrid.'}
    {'role': 'user', 'content': 'What is the population of Great Britain?'}
    {'role': 'llm', 'content': 'As of 2023 the population of Great Britain is approximately 67 million people.'}

In many LLM flows the conversation progresses in a series of prompt and response pairs. session managers offer a convienience function `store()` to add these simply.

[code example]

    {'role': 'user', 'content': 'And what is the capital of Spain?'}
    {'role': 'llm', 'content': 'The capital is Madrid.'}
    {'role': 'user', 'content': 'What is the population of Great Britain?'}
    {'role': 'llm', 'content': 'As of 2023 the population of Great Britain is approximately 67 million people.'}
    {'role': 'user', 'content': 'what is the size of England compared to Portugal?'}
    {'role': 'llm', 'content': 'England is larger in land area than Portal by about 15000 square miles.'}

Managing multiple users and conversations

For applications that need to handle multiple conversations concurrently, Redis supports tagging messages to keep conversations separated.

[code example]

    {'role': 'system', 'content': 'You are a helpful algebra tutor, giving simple answers to math problems.'}
    {'role': 'user', 'content': 'What is the value of x in the equation 2x + 3 = 7?'}
    {'role': 'llm', 'content': 'The value of x is 2.'}
    {'role': 'user', 'content': 'What is the value of y in the equation 3y - 5 = 7?'}
    {'role': 'llm', 'content': 'The value of y is 4.'}

Semantic conversation memory

For longer conversations our list of messages keeps growing. Since LLMs are stateless we have to continue to pass this conversation history on each subsequent call to ensure the LLM has the correct context.

A typical flow looks like this:
[code example]

This works, but as context keeps growing so too does our LLM token count, which increases latency and cost.

Conversation histories can be truncated, but that can lead to losing relevant information that appeared early on.

A better solution is to pass only the relevant conversational context on each subsequent call.

For this, RedisVL has the `SemanticSessionManager`, which uses vector similarity search to return only semantically relevant sections of the conversation.

[code example]

    12:24:15 redisvl.index.index INFO   Index already exists, not overwriting.

[code example]

    {'role': 'user', 'content': 'what is the size of England compared to Portugal?'}
    {'role': 'llm', 'content': 'England is larger in land area than Portal by about 15000 square miles.'}

You can adjust the degree of semantic similarity needed to be included in your context.

Setting a distance threshold close to 0.0 will require an exact semantic match, while a distance threshold of 1.0 will include everthing.

[code example]

    {'role': 'user', 'content': 'what is the size of England compared to Portugal?'}
    {'role': 'llm', 'content': 'England is larger in land area than Portal by about 15000 square miles.'}
    {'role': 'user', 'content': 'What is the population of Great Britain?'}
    {'role': 'llm', 'content': 'As of 2023 the population of Great Britain is approximately 67 million people.'}

Conversation control

LLMs can hallucinate on occasion and when this happens it can be useful to prune incorrect information from conversational histories so this incorrect information doesn't continue to be passed as context.

[code example]

    {'role': 'user', 'content': 'What is the population of Great Britain?'}
    {'role': 'llm', 'content': 'As of 2023 the population of Great Britain is approximately 67 million people.'}
    {'role': 'user', 'content': 'what is the size of England compared to Portugal?'}
    {'role': 'llm', 'content': 'England is larger in land area than Portal by about 15000 square miles.'}
    {'role': 'user', 'content': 'what is the smallest country in Europe?'}

[code example]

from redisvl.extensions.session_manager import StandardSessionManager
chat_session = StandardSessionManager(name='student tutor')

chat_session.add_message({"role":"system", "content":"You are a helpful geography tutor, giving simple and short answers to questions about Europen countries."})
chat_session.add_messages([
    {"role":"user", "content":"What is the capital of France?"},
    {"role":"llm", "content":"The capital is Paris."},
    {"role":"user", "content":"And what is the capital of Spain?"},
    {"role":"llm", "content":"The capital is Madrid."},
    {"role":"user", "content":"What is the population of Great Britain?"},
    {"role":"llm", "content":"As of 2023 the population of Great Britain is approximately 67 million people."},]
    )

context = chat_session.get_recent()
for message in context:
    print(message)

prompt = "what is the size of England compared to Portugal?"
response = "England is larger in land area than Portal by about 15000 square miles."
chat_session.store(prompt, response)

context = chat_session.get_recent(top_k=6)
for message in context:
    print(message)

chat_session.add_message({"role":"system", "content":"You are a helpful algebra tutor, giving simple answers to math problems."}, session_tag='student two')
chat_session.add_messages([
    {"role":"user", "content":"What is the value of x in the equation 2x + 3 = 7?"},
    {"role":"llm", "content":"The value of x is 2."},
    {"role":"user", "content":"What is the value of y in the equation 3y - 5 = 7?"},
    {"role":"llm", "content":"The value of y is 4."}],
    session_tag='student two'
    )

for math_message in chat_session.get_recent(session_tag='student two'):
    print(math_message)

while True:
    prompt = input('enter your next question')
    context = chat_session.get_recent()
    response = LLM_api_call(prompt=prompt, context=context)
    chat_session.store(prompt, response)

from redisvl.extensions.session_manager import SemanticSessionManager
semantic_session = SemanticSessionManager(name='tutor')

semantic_session.add_messages(chat_session.get_recent(top_k=8))

prompt = "what have I learned about the size of England?"
semantic_session.set_distance_threshold(0.35)
context = semantic_session.get_relevant(prompt)
for message in context:
    print(message)

semantic_session.set_distance_threshold(0.7)

larger_context = semantic_session.get_relevant(prompt)
for message in larger_context:
    print(message)

semantic_session.store(
    prompt="what is the smallest country in Europe?",
    response="Monaco is the smallest country in Europe at 0.78 square miles." # Incorrect. Vatican City is the smallest country in Europe
    )

# get the key of the incorrect message
context = semantic_session.get_recent(top_k=1, raw=True)
bad_key = context[0]['entry_id']
semantic_session.drop(bad_key)

corrected_context = semantic_session.get_recent()
for message in corrected_context:
    print(message)


SVS-VAMANA Vector Search

Overview

In this notebook, we will explore SVS-VAMANA (Scalable Vector Search with VAMANA graph algorithm), a graph-based vector search algorithm that is optimized to work with compression methods to reduce memory usage. It combines the Vamana graph algorithm with advanced compression techniques (LVQ and LeanVec) and is optimized for Intel hardware.

**How it works**

Vamana builds a single-layer proximity graph and prunes edges during construction based on tunable parameters, similar to HNSW but with a simpler structure. The compression methods apply per-vector normalization and scalar quantization, learning parameters directly from the data to enable fast, on-the-fly distance computations with SIMD-optimized layout Vector quantization and compression.

**SVS-VAMANA offers:**
- **Fast approximate nearest neighbor search** using graph-based algorithms
- **Vector compression** (LVQ, LeanVec) with up to 87.5% memory savings
- **Dimensionality reduction** (optional, with LeanVec)
- **Automatic performance optimization** through CompressionAdvisor

**Use SVS-VAMANA when:**
- Large datasets where memory is expensive
- Cloud deployments with memory-based pricing
- When 90-95% recall is acceptable
- High-dimensional vectors (>1024 dims) with LeanVec compression

**Table of Contents**

1. [Prerequisites](#Prerequisites)
2. [Quick Start with CompressionAdvisor](#Quick-Start-with-CompressionAdvisor)
3. [Creating an SVS-VAMANA Index](#Creating-an-SVS-VAMANA-Index)
4. [Loading Sample Data](#Loading-Sample-Data)
5. [Performing Vector Searches](#Performing-Vector-Searches)
6. [Understanding Compression Types](#Understanding-Compression-Types)
7. [Hybrid Queries with SVS-VAMANA](#Hybrid-Queries-with-SVS-VAMANA)
8. [Performance Monitoring](#Performance-Monitoring)
9. [Manual Configuration (Advanced)](#Manual-Configuration-(Advanced))
10. [Best Practices and Tips](#Best-Practices-and-Tips)
11. [Cleanup](#Cleanup)

---

Prerequisites

Before running this notebook, ensure you have:
1. Installed `redisvl` and have that environment active for this notebook
2. A running Redis Stack instance with:
   - Redis >= 8.2.0
   - RediSearch >= 2.8.10

For example, you can run Redis Stack locally with Docker:

[code example]

**Note:** SVS-VAMANA only supports FLOAT16 and FLOAT32 datatypes.

[code example]

[code example]

Quick Start with CompressionAdvisor

The easiest way to get started with SVS-VAMANA is using the `CompressionAdvisor` utility, which automatically recommends optimal configuration based on your vector dimensions and performance priorities.

[code example]

    Recommended Configuration:
      algorithm: svs-vamana
      datatype: float16
      graph_max_degree: 64
      construction_window_size: 300
      compression: LeanVec4x8
      reduce: 512
      search_window_size: 30

    Estimated Memory Savings: 81.2%

Creating an SVS-VAMANA Index

Let's create an index using the recommended configuration. We'll use a simple schema with text content and vector embeddings.

[code example]

    ✅ Created SVS-VAMANA index: svs_demo
       Algorithm: svs-vamana
       Compression: LeanVec4x8
       Dimensions: 1024
       Reduced to: 512 dimensions

Loading Sample Data

Let's create some sample documents with embeddings to demonstrate SVS-VAMANA search capabilities.

[code example]

    Creating vectors with 512 dimensions (reduced from 1024 if applicable)
    ✅ Loaded 10 documents into the index
       Index now contains 0 documents

Performing Vector Searches

Now let's perform some vector similarity searches using our SVS-VAMANA index.

[code example]

    🔍 Vector Search Results:
    ==================================================

Runtime Parameters for Performance Tuning

SVS-VAMANA supports runtime parameters that can be adjusted at query time without rebuilding the index. These parameters allow you to fine-tune the trade-off between search speed and accuracy.

**Available Runtime Parameters:**

- **`search_window_size`**: Controls the size of the search window during KNN search (higher = better recall, slower search)
- **`epsilon`**: Approximation factor for range queries (default: 0.01)
- **`use_search_history`**: Whether to use search buffer (OFF/ON/AUTO, default: AUTO)
- **`search_buffer_capacity`**: Tuning parameter for 2-level compression (default: search_window_size)

Let's see how these parameters affect search performance:

[code example]

Runtime Parameters with Range Queries

Runtime parameters are also useful for range queries, where you want to find all vectors within a certain distance threshold:

[code example]

Understanding Compression Types

SVS-VAMANA supports different compression algorithms that trade off between memory usage and search quality. Let's explore the available options.

[code example]

    Compression Recommendations for Different Priorities:
    ============================================================

    MEMORY Priority:
      Compression: LeanVec4x8
      Datatype: float16
      Dimensionality reduction: 1024 → 512
      Search window size: 20
      Memory savings: 81.2%

    SPEED Priority:
      Compression: LeanVec4x8
      Datatype: float16
      Dimensionality reduction: 1024 → 256
      Search window size: 40
      Memory savings: 90.6%

    BALANCED Priority:
      Compression: LeanVec4x8
      Datatype: float16
      Dimensionality reduction: 1024 → 512
      Search window size: 30
      Memory savings: 81.2%

Compression Types Explained

SVS-VAMANA offers several compression algorithms:

LVQ (Learned Vector Quantization)

- **LVQ4**: 4 bits per dimension (87.5% memory savings)
- **LVQ4x4**: 8 bits per dimension (75% memory savings)
- **LVQ4x8**: 12 bits per dimension (62.5% memory savings)
- **LVQ8**: 8 bits per dimension (75% memory savings)

LeanVec (Compression + Dimensionality Reduction)

- **LeanVec4x8**: 12 bits per dimension + dimensionality reduction
- **LeanVec8x8**: 16 bits per dimension + dimensionality reduction

The CompressionAdvisor automatically chooses the best compression type based on your vector dimensions and priority.

[code example]

    Memory Savings by Vector Dimension:
    ==================================================
    Dims   Compression  Savings  Strategy
    --------------------------------------------------
    384    LVQ4x4         75.0% LVQ
    768    LVQ4x4         75.0% LVQ
    1024   LeanVec4x8     81.2% LeanVec
    1536   LeanVec4x8     81.2% LeanVec
    3072   LeanVec4x8     81.2% LeanVec

Hybrid Queries with SVS-VAMANA

SVS-VAMANA can be combined with other Redis search capabilities for powerful hybrid queries that filter by metadata while performing vector similarity search.

[code example]

    🔍 Hybrid Search Results (Technology category only):
    =======================================================

Performance Monitoring

Let's examine the index statistics to understand the performance characteristics of our SVS-VAMANA index.

[code example]

    📊 Index Statistics:
    ==============================
    Documents: 0
    Vector index size: 0.00 MB
    Total indexing time: 1.58 seconds
    Memory efficiency calculation requires documents and vector index size > 0

Manual Configuration (Advanced)

For advanced users who want full control over SVS-VAMANA parameters, you can manually configure the algorithm instead of using CompressionAdvisor.

[code example]

    Manual SVS-VAMANA Configuration:
    ========================================
      algorithm: svs-vamana
      datatype: float32
      distance_metric: cosine
      graph_max_degree: 64
      construction_window_size: 300
      search_window_size: 40
      compression: LVQ4x4
      training_threshold: 10000

    Estimated memory savings: 75.0%

Best Practices and Tips

When to Use SVS-VAMANA

- **Large datasets** (>10K vectors) where memory efficiency matters
- **High-dimensional vectors** (>512 dimensions) that benefit from compression
- **Applications** that can tolerate slight recall trade-offs for speed and memory savings

Parameter Tuning Guidelines

**Index-time parameters** (set during index creation):
- **Start with CompressionAdvisor** recommendations for compression and datatype
- **Use LeanVec** for high-dimensional vectors (≥1024 dims)
- **Use LVQ** for lower-dimensional vectors (<1024 dims)
- **graph_max_degree**: Higher values improve recall but increase memory usage
- **construction_window_size**: Higher values improve index quality but slow down build time

**Runtime parameters** (adjustable at query time without rebuilding index):
- **search_window_size**: Start with 20, increase to 40-100 for higher recall
- **epsilon**: Use 0.01-0.05 for range queries (higher = faster but less accurate)
- **use_search_history**: Use 'AUTO' (default) or 'ON' for better recall
- **search_buffer_capacity**: Usually set equal to search_window_size

Performance Considerations

- **Index build time** increases with higher construction_window_size
- **Search latency** increases with higher search_window_size (tunable at query time!)
- **Memory usage** decreases with more aggressive compression
- **Recall quality** may decrease with more aggressive compression or lower search_window_size

Cleanup

Clean up the indices created in this demo.

[code example]

    Cleaned up svs_demo index

docker run -d -p 6379:6379 -p 8001:8001 redis/redis-stack:latest

# Import necessary modules
import numpy as np
from redisvl.index import SearchIndex
from redisvl.query import VectorQuery
from redisvl.utils import CompressionAdvisor
from redisvl.redis.utils import array_to_buffer

# Set random seed for reproducible results
np.random.seed(42)

# Redis connection
REDIS_URL = "redis://localhost:6379"

# Get recommended configuration for common embedding dimensions
dims = 1024  # Common embedding dimensions (works reliably with SVS-VAMANA)

config = CompressionAdvisor.recommend(
    dims=dims,
    priority="balanced"  # Options: "memory", "speed", "balanced"
)

print("Recommended Configuration:")
for key, value in config.items():
    print(f"  {key}: {value}")

# Estimate memory savings
savings = CompressionAdvisor.estimate_memory_savings(
    config["compression"],
    dims,
    config.get("reduce")
)
print(f"\nEstimated Memory Savings: {savings}%")

# Create index schema with recommended SVS-VAMANA configuration
schema = {
    "index": {
        "name": "svs_demo",
        "prefix": "doc",
    },
    "fields": [
        {"name": "content", "type": "text"},
        {"name": "category", "type": "tag"},
        {
            "name": "embedding",
            "type": "vector",
            "attrs": {
                "dims": dims,
                **config,  # Use the recommended configuration
                "distance_metric": "cosine"
            }
        }
    ]
}

# Create the index
index = SearchIndex.from_dict(schema, redis_url=REDIS_URL)
index.create(overwrite=True)

print(f"✅ Created SVS-VAMANA index: {index.name}")
print(f"   Algorithm: {config['algorithm']}")
print(f"   Compression: {config['compression']}")
print(f"   Dimensions: {dims}")
if 'reduce' in config:
    print(f"   Reduced to: {config['reduce']} dimensions")

# Generate sample data
sample_documents = [
    {"content": "Machine learning algorithms for data analysis", "category": "technology"},
    {"content": "Natural language processing and text understanding", "category": "technology"},
    {"content": "Computer vision and image recognition systems", "category": "technology"},
    {"content": "Delicious pasta recipes from Italy", "category": "food"},
    {"content": "Traditional French cooking techniques", "category": "food"},
    {"content": "Healthy meal planning and nutrition", "category": "food"},
    {"content": "Travel guide to European destinations", "category": "travel"},
    {"content": "Adventure hiking in mountain regions", "category": "travel"},
    {"content": "Cultural experiences in Asian cities", "category": "travel"},
    {"content": "Financial planning for retirement", "category": "finance"},
]

# Generate random embeddings for demonstration
# In practice, you would use a real embedding model
data_to_load = []

# Use reduced dimensions if LeanVec compression is applied
vector_dims = config.get("reduce", dims)
print(f"Creating vectors with {vector_dims} dimensions (reduced from {dims} if applicable)")

for i, doc in enumerate(sample_documents):
    # Create a random vector with some category-based clustering
    base_vector = np.random.random(vector_dims).astype(np.float32)

    # Add some category-based similarity (optional, for demo purposes)
    category_offset = hash(doc["category"]) % 100 / 1000.0
    base_vector[0] += category_offset

    # Convert to the datatype specified in config
    if config["datatype"] == "float16":
        base_vector = base_vector.astype(np.float16)

    data_to_load.append({
        "content": doc["content"],
        "category": doc["category"],
        "embedding": array_to_buffer(base_vector, dtype=config["datatype"])
    })

# Load data into the index
index.load(data_to_load)
print(f"✅ Loaded {len(data_to_load)} documents into the index")

# Wait a moment for indexing to complete
import time
time.sleep(2)

# Verify the data was loaded
info = index.info()
print(f"   Index now contains {info.get('num_docs', 0)} documents")

# Create a query vector (in practice, this would be an embedding of your query text)
# Important: Query vector must match the index datatype and dimensions
vector_dims = config.get("reduce", dims)
if config["datatype"] == "float16":
    query_vector = np.random.random(vector_dims).astype(np.float16)
else:
    query_vector = np.random.random(vector_dims).astype(np.float32)

# Perform a vector similarity search
query = VectorQuery(
    vector=query_vector.tolist(),
    vector_field_name="embedding",
    return_fields=["content", "category"],
    num_results=5
)

results = index.query(query)

print("🔍 Vector Search Results:")
print("=" * 50)
for i, result in enumerate(results, 1):
    distance = result.get('vector_distance', 'N/A')
    print(f"{i}. [{result['category']}] {result['content']}")
    print(f"   Distance: {distance:.4f}" if isinstance(distance, (int, float)) else f"   Distance: {distance}")
    print()

# Example 1: Basic query with default parameters
basic_query = VectorQuery(
    vector=query_vector.tolist(),
    vector_field_name="embedding",
    return_fields=["content", "category"],
    num_results=5
)

print("🔍 Basic Query (default parameters):")
results = index.query(basic_query)
print(f"Found {len(results)} results\n")

# Example 2: Query with tuned runtime parameters for higher recall
tuned_query = VectorQuery(
    vector=query_vector.tolist(),
    vector_field_name="embedding",
    return_fields=["content", "category"],
    num_results=5,
    search_window_size=40,      # Larger window for better recall
    use_search_history='ON',    # Use search history
    search_buffer_capacity=50   # Larger buffer capacity
)

print("🎯 Tuned Query (higher recall parameters):")
results = index.query(tuned_query)
print(f"Found {len(results)} results")
print("\nNote: Higher search_window_size improves recall but may increase latency")

from redisvl.query import VectorRangeQuery

# Range query with runtime parameters
range_query = VectorRangeQuery(
    vector=query_vector.tolist(),
    vector_field_name="embedding",
    return_fields=["content", "category"],
    distance_threshold=0.3,
    epsilon=0.05,               # Approximation factor
    search_window_size=30,      # Search window size
    use_search_history='AUTO'   # Automatic history management
)

results = index.query(range_query)
print(f"🎯 Range Query Results: Found {len(results)} vectors within distance threshold 0.3")
for i, result in enumerate(results[:3], 1):
    distance = result.get('vector_distance', 'N/A')
    print(f"{i}. {result['content'][:50]}... (distance: {distance})")

# Compare different compression priorities
print("Compression Recommendations for Different Priorities:")
print("=" * 60)

priorities = ["memory", "speed", "balanced"]
for priority in priorities:
    config = CompressionAdvisor.recommend(dims=dims, priority=priority)
    savings = CompressionAdvisor.estimate_memory_savings(
        config["compression"],
        dims,
        config.get("reduce")
    )

    print(f"\n{priority.upper()} Priority:")
    print(f"  Compression: {config['compression']}")
    print(f"  Datatype: {config['datatype']}")
    if "reduce" in config:
        print(f"  Dimensionality reduction: {dims} → {config['reduce']}")
    print(f"  Search window size: {config['search_window_size']}")
    print(f"  Memory savings: {savings}%")

# Demonstrate compression savings for different vector dimensions
test_dimensions = [384, 768, 1024, 1536, 3072]

print("Memory Savings by Vector Dimension:")
print("=" * 50)
print(f"{'Dims':<6} {'Compression':<12} {'Savings':<8} {'Strategy'}")
print("-" * 50)

for dims in test_dimensions:
    config = CompressionAdvisor.recommend(dims=dims, priority="balanced")
    savings = CompressionAdvisor.estimate_memory_savings(
        config["compression"],
        dims,
        config.get("reduce")
    )

    strategy = "LeanVec" if dims >= 1024 else "LVQ"
    print(f"{dims:<6} {config['compression']:<12} {savings:>6.1f}% {strategy}")

# Perform a hybrid search: vector similarity + category filter
hybrid_query = VectorQuery(
    vector=query_vector.tolist(),
    vector_field_name="embedding",
    return_fields=["content", "category"],
    num_results=3
)

# Add a filter to only search within "technology" category
hybrid_query.set_filter("@category:{technology}")

filtered_results = index.query(hybrid_query)

print("🔍 Hybrid Search Results (Technology category only):")
print("=" * 55)
for i, result in enumerate(filtered_results, 1):
    distance = result.get('vector_distance', 'N/A')
    print(f"{i}. [{result['category']}] {result['content']}")
    print(f"   Distance: {distance:.4f}" if isinstance(distance, (int, float)) else f"   Distance: {distance}")
    print()

# Get detailed index information
info = index.info()

print("📊 Index Statistics:")
print("=" * 30)
print(f"Documents: {info.get('num_docs', 0)}")

# Handle vector_index_sz_mb which might be a string
vector_size = info.get('vector_index_sz_mb', 0)
if isinstance(vector_size, str):
    try:
        vector_size = float(vector_size)
    except ValueError:
        vector_size = 0.0
print(f"Vector index size: {vector_size:.2f} MB")

# Handle total_indexing_time which might also be a string
indexing_time = info.get('total_indexing_time', 0)
if isinstance(indexing_time, str):
    try:
        indexing_time = float(indexing_time)
    except ValueError:
        indexing_time = 0.0
print(f"Total indexing time: {indexing_time:.2f} seconds")

# Calculate memory efficiency
if info.get('num_docs', 0) > 0 and vector_size > 0:
    mb_per_doc = vector_size / info.get('num_docs', 1)
    print(f"Memory per document: {mb_per_doc:.4f} MB")

    # Estimate for larger datasets
    for scale in [1000, 10000, 100000]:
        estimated_mb = mb_per_doc * scale
        print(f"Estimated size for {scale:,} docs: {estimated_mb:.1f} MB")
else:
    print("Memory efficiency calculation requires documents and vector index size > 0")

# Example of manual SVS-VAMANA configuration
manual_schema = {
    "index": {
        "name": "svs_manual",
        "prefix": "manual",
    },
    "fields": [
        {"name": "content", "type": "text"},
        {
            "name": "embedding",
            "type": "vector",
            "attrs": {
                "dims": 768,
                "algorithm": "svs-vamana",
                "datatype": "float32",
                "distance_metric": "cosine",

                # Graph construction parameters
                "graph_max_degree": 64,           # Higher = better recall, more memory
                "construction_window_size": 300,  # Higher = better quality, slower build

                # Search parameters
                "search_window_size": 40,         # Higher = better recall, slower search

                # Compression settings
                "compression": "LVQ4x4",          # Choose compression type
                "training_threshold": 10000,      # Min vectors before compression training
            }
        }
    ]
}

print("Manual SVS-VAMANA Configuration:")
print("=" * 40)
vector_attrs = manual_schema["fields"][1]["attrs"]
for key, value in vector_attrs.items():
    if key != "dims":  # Skip dims as it's obvious
        print(f"  {key}: {value}")

# Calculate memory savings for this configuration
manual_savings = CompressionAdvisor.estimate_memory_savings(
    "LVQ4x4", 768, None
)
print(f"\nEstimated memory savings: {manual_savings}%")

# Clean up demo indices
try:
    index.delete()
    print("Cleaned up svs_demo index")
except:
    print("- svs_demo index was already deleted or doesn't exist")