Getting Started with RedisVL

Overview

`redisvl` is a versatile Python library with an integrated CLI, designed to enhance AI applications using Redis. This guide will walk you through the following steps:

1. Defining an `IndexSchema`
2. Preparing a sample dataset
3. Creating a `SearchIndex` object
4. Testing `rvl` CLI functionality
5. Loading the sample data
6. Building `VectorQuery` objects and executing searches
7. Updating a `SearchIndex` object

...and more!

Prerequisites:
- Ensure `redisvl` is installed in your Python environment.
- Have a running instance of [Redis Stack](https://redis.io/docs/install/install-stack/) or [Redis Cloud](https://redis.io/cloud).

_____

Define an `IndexSchema`

The `IndexSchema` maintains crucial **index configuration** and **field definitions** to
enable search with Redis. For ease of use, the schema can be constructed from a
python dictionary or yaml file.

Example Schema Creation

Consider a dataset with user information, including `job`, `age`, `credit_score`,
and a 3-dimensional `user_embedding` vector.

You must also decide on a Redis index name and key prefix to use for this
dataset. Below are example schema definitions in both YAML and Dict format.

**YAML Definition:**

[code example]
Store this in a local file, such as `schema.yaml`, for RedisVL usage.

**Python Dictionary:**

[code example]

Sample Dataset Preparation

Below, create a mock dataset with `user`, `job`, `age`, `credit_score`, and
`user_embedding` fields. The `user_embedding` vectors are synthetic examples
for demonstration purposes.

For more information on creating real-world embeddings, refer to this
[article](https://mlops.community/vector-similarity-search-from-basics-to-production/).

[code example]

As seen above, the sample `user_embedding` vectors are converted into bytes. Using the `NumPy`, this is fairly trivial.

Create a `SearchIndex`

With the schema and sample dataset ready, create a `SearchIndex`.

Bring your own Redis connection instance

This is ideal in scenarios where you have custom settings on the connection instance or if your application will share a connection pool:

[code example]

Let the index manage the connection instance

This is ideal for simple cases:

[code example]

Create the index

Now that we are connected to Redis, we need to run the create command.

[code example]

    13:00:22 redisvl.index.index INFO   Index already exists, overwriting.

Note that at this point, the index has no entries. Data loading follows.

Inspect with the `rvl` CLI

Use the `rvl` CLI to inspect the created index and its fields:

[code example]

    13:00:24 [RedisVL] INFO   Indices:
    13:00:24 [RedisVL] INFO   1. user_simple

[code example]

    Index Information:
    ╭──────────────────────┬──────────────────────┬──────────────────────┬──────────────────────┬──────────────────────╮
    │ Index Name           │ Storage Type         │ Prefixes             │ Index Options        │ Indexing             │
    ├──────────────────────┼──────────────────────┼──────────────────────┼──────────────────────┼──────────────────────┤
    | user_simple          | HASH                 | ['user_simple_docs'] | []                   | 0                    |
    ╰──────────────────────┴──────────────────────┴──────────────────────┴──────────────────────┴──────────────────────╯
    Index Fields:
    ╭─────────────────┬─────────────────┬─────────────────┬─────────────────┬─────────────────┬─────────────────┬─────────────────┬─────────────────┬─────────────────┬─────────────────┬─────────────────╮
    │ Name            │ Attribute       │ Type            │ Field Option    │ Option Value    │ Field Option    │ Option Value    │ Field Option    │ Option Value    │ Field Option    │ Option Value    │
    ├─────────────────┼─────────────────┼─────────────────┼─────────────────┼─────────────────┼─────────────────┼─────────────────┼─────────────────┼─────────────────┼─────────────────┼─────────────────┤
    │ user            │ user            │ TAG             │ SEPARATOR       │ ,               │                 │                 │                 │                 │                 │                 │
    │ credit_score    │ credit_score    │ TAG             │ SEPARATOR       │ ,               │                 │                 │                 │                 │                 │                 │
    │ job             │ job             │ TEXT            │ WEIGHT          │ 1               │                 │                 │                 │                 │                 │                 │
    │ age             │ age             │ NUMERIC         │                 │                 │                 │                 │                 │                 │                 │                 │
    │ user_embedding  │ user_embedding  │ VECTOR          │ algorithm       │ FLAT            │ data_type       │ FLOAT32         │ dim             │ 3               │ distance_metric │ COSINE          │
    ╰─────────────────┴─────────────────┴─────────────────┴─────────────────┴─────────────────┴─────────────────┴─────────────────┴─────────────────┴─────────────────┴─────────────────┴─────────────────╯

Load Data to `SearchIndex`

Load the sample dataset to Redis.

Validate data entries on load

RedisVL uses pydantic validation under the hood to ensure loaded data is valid and confirms to your schema. This setting is optional and can be configured in the `SearchIndex` class.

[code example]

    ['user_simple_docs:01JY4J4Y08GFY10VMB9D4YDMZQ', 'user_simple_docs:01JY4J4Y0AY2MKJ24QXQS2Q2YS', 'user_simple_docs:01JY4J4Y0A9GFF2XG1R81EFD4Z']

By default, `load` will create a unique Redis key as a combination of the index key `prefix` and a random ULID. You can also customize the key by providing direct keys or pointing to a specified `id_field` on load.

Load INVALID data

This will raise a `SchemaValidationError` if `validate_on_load` is set to true in the `SearchIndex` class.

[code example]

    13:00:27 redisvl.index.index ERROR   Data validation failed during load operation
    Schema validation failed for object at index 0. Field 'user_embedding' expects bytes (vector data), but got boolean value 'True'. If this should be a vector field, provide a list of numbers or bytes. If this should be a different field type, check your schema definition.
    Object data: {
      "user_embedding": true
    }
    Hint: Check that your data types match the schema field definitions. Use index.schema.fields to view expected field types.

Upsert the index with new data

Upsert data by using the `load` method again:

[code example]

    ['user_simple_docs:01JY4J4Y0N4CNR9Y6R67MMVG7Q']

Creating `VectorQuery` Objects

Next we will create a vector query object for our newly populated index. This example will use a simple vector to demonstrate how vector similarity works. Vectors in production will likely be much larger than 3 floats and often require Machine Learning models (i.e. Huggingface sentence transformers) or an embeddings API (Cohere, OpenAI). `redisvl` provides a set of [Vectorizers]() to assist in vector creation.

[code example]

**Note:** For HNSW and SVS-VAMANA indexes, you can tune search performance using runtime parameters:

[code example]

See the [SVS-VAMANA guide](09_svs_vamana.ipynb) and [Advanced Queries guide](11_advanced_queries.ipynb) for more details on runtime parameters.

Executing queries

With our `VectorQuery` object defined above, we can execute the query over the `SearchIndex` using the `query` method.

[code example]

vector_distance	user	age	job	credit_score

0	john	1	engineer	high

0	mary	2	doctor	low

0	john	1	engineer	high

Using an Asynchronous Redis Client

The `AsyncSearchIndex` class along with an async Redis python client allows for queries, index creation, and data loading to be done asynchronously. This is the
recommended route for working with `redisvl` in production-like settings.

[code example]

    {'index': {'name': 'user_simple', 'prefix': 'user_simple_docs'},
     'fields': [{'name': 'user', 'type': 'tag'},
      {'name': 'credit_score', 'type': 'tag'},
      {'name': 'job', 'type': 'text'},
      {'name': 'age', 'type': 'numeric'},
      {'name': 'user_embedding',
       'type': 'vector',
       'attrs': {'dims': 3,
        'distance_metric': 'cosine',
        'algorithm': 'flat',
        'datatype': 'float32'}}]}

[code example]

[code example]

vector_distance	user	age	job	credit_score

0	john	1	engineer	high

0	mary	2	doctor	low

0	john	1	engineer	high

Updating a schema

In some scenarios, it makes sense to update the index schema. With Redis and `redisvl`, this is easy because Redis can keep the underlying data in place while you change or make updates to the index configuration.

So for our scenario, let's imagine we want to reindex this data in 2 ways:
- by using a `Tag` type for `job` field instead of `Text`
- by using an `hnsw` vector index for the `user_embedding` field instead of a `flat` vector index

[code example]

[code example]

    13:00:27 redisvl.index.index INFO   Index already exists, overwriting.

[code example]

vector_distance	user	age	job	credit_score

0	mary	2	doctor	low

0	john	1	engineer	high

0	john	1	engineer	high

Check Index Stats

Use the `rvl` CLI to check the stats for the index:

[code example]

    Statistics:
    ╭─────────────────────────────┬────────────╮
    │ Stat Key                    │ Value      │
    ├─────────────────────────────┼────────────┤
    │ num_docs                    │ 10         │
    │ num_terms                   │ 0          │
    │ max_doc_id                  │ 10         │
    │ num_records                 │ 50         │
    │ percent_indexed             │ 1          │
    │ hash_indexing_failures      │ 0          │
    │ number_of_uses              │ 2          │
    │ bytes_per_record_avg        │ 19.5200004 │
    │ doc_table_size_mb           │ 0.00105857 │
    │ inverted_sz_mb              │ 9.30786132 │
    │ key_table_size_mb           │ 4.70161437 │
    │ offset_bits_per_record_avg  │ nan        │
    │ offset_vectors_sz_mb        │ 0          │
    │ offsets_per_term_avg        │ 0          │
    │ records_per_doc_avg         │ 5          │
    │ sortable_values_size_mb     │ 0          │
    │ total_indexing_time         │ 0.16899999 │
    │ total_inverted_index_blocks │ 11         │
    │ vector_index_sz_mb          │ 0.23619842 │
    ╰─────────────────────────────┴────────────╯

Cleanup

Below we will clean up after our work. First, you can flush all data from Redis associated with the index by
using the `.clear()` method. This will leave the secondary index in place for future insertions or updates.

But if you want to clean up everything, including the index, just use `.delete()`
which will by default remove the index AND the underlying data.

[code example]

    10

[code example]

    True

[code example]

version: '0.1.0'

index:
  name: user_simple
  prefix: user_simple_docs

fields:
    - name: user
      type: tag
    - name: credit_score
      type: tag
    - name: job
      type: text
    - name: age
      type: numeric
    - name: user_embedding
      type: vector
      attrs:
        algorithm: flat
        dims: 3
        distance_metric: cosine
        datatype: float32

schema = {
    "index": {
        "name": "user_simple",
        "prefix": "user_simple_docs",
    },
    "fields": [
        {"name": "user", "type": "tag"},
        {"name": "credit_score", "type": "tag"},
        {"name": "job", "type": "text"},
        {"name": "age", "type": "numeric"},
        {
            "name": "user_embedding",
            "type": "vector",
            "attrs": {
                "dims": 3,
                "distance_metric": "cosine",
                "algorithm": "flat",
                "datatype": "float32"
            }
        }
    ]
}

import numpy as np

data = [
    {
        'user': 'john',
        'age': 1,
        'job': 'engineer',
        'credit_score': 'high',
        'user_embedding': np.array([0.1, 0.1, 0.5], dtype=np.float32).tobytes()
    },
    {
        'user': 'mary',
        'age': 2,
        'job': 'doctor',
        'credit_score': 'low',
        'user_embedding': np.array([0.1, 0.1, 0.5], dtype=np.float32).tobytes()
    },
    {
        'user': 'joe',
        'age': 3,
        'job': 'dentist',
        'credit_score': 'medium',
        'user_embedding': np.array([0.9, 0.9, 0.1], dtype=np.float32).tobytes()
    }
]

from redisvl.index import SearchIndex
from redis import Redis

client = Redis.from_url("redis://localhost:6379")
index = SearchIndex.from_dict(schema, redis_client=client, validate_on_load=True)

index = SearchIndex.from_dict(schema, redis_url="redis://localhost:6379", validate_on_load=True)

# If you don't specify a client or Redis URL, the index will attempt to
# connect to Redis at the default address "redis://localhost:6379".

# NBVAL_SKIP

try:
    keys = index.load([{"user_embedding": True}])
except Exception as e:
    print(str(e))

# Add more data
new_data = [{
    'user': 'tyler',
    'age': 9,
    'job': 'engineer',
    'credit_score': 'high',
    'user_embedding': np.array([0.1, 0.3, 0.5], dtype=np.float32).tobytes()
}]
keys = index.load(new_data)

print(keys)

from redisvl.query import VectorQuery
from jupyterutils import result_print

query = VectorQuery(
    vector=[0.1, 0.1, 0.5],
    vector_field_name="user_embedding",
    return_fields=["user", "age", "job", "credit_score", "vector_distance"],
    num_results=3
)

# Example with HNSW runtime parameters
query = VectorQuery(
    vector=[0.1, 0.1, 0.5],
    vector_field_name="user_embedding",
    return_fields=["user", "age", "job"],
    num_results=3,
    ef_runtime=50  # Higher for better recall (HNSW only)
)

results = index.query(query)
result_print(results)

from redisvl.index import AsyncSearchIndex
from redis.asyncio import Redis

client = Redis.from_url("redis://localhost:6379")
index = AsyncSearchIndex.from_dict(schema, redis_client=client)

# execute the vector query async
results = await index.query(query)
result_print(results)

# Modify this schema to have what we want

index.schema.remove_field("job")
index.schema.remove_field("user_embedding")
index.schema.add_fields([
    {"name": "job", "type": "tag"},
    {
        "name": "user_embedding",
        "type": "vector",
        "attrs": {
            "dims": 3,
            "distance_metric": "cosine",
            "algorithm": "hnsw",
            "datatype": "float32"
        }
    }
])

# Run the index update but keep underlying data in place
await index.create(overwrite=True, drop=False)

# Execute the vector query async
results = await index.query(query)
result_print(results)

# Clear all data from Redis associated with the index
await index.clear()

# Butm the index is still in place
await index.exists()

# Remove / delete the index in its entirety
await index.delete()


Hash vs JSON Storage

Overview

Out of the box, Redis provides a [variety of data structures](https://redis.com/redis-enterprise/data-structures/) that can adapt to your domain specific applications and use cases.
In this notebook, we will demonstrate how to use RedisVL with both [Hash](https://redis.io/docs/latest/develop/data-types/#hashes) and [JSON](https://redis.io/docs/latest/develop/data-types/json/) data.

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 or Redis Software instance with RediSearch > 2.4 activated.

For example, you can run [Redis Stack](https://redis.io/docs/install/install-stack/) locally with Docker:

[code example]

Or create a [FREE Redis Cloud](https://redis.io/cloud).

[code example]

[code example]

user	age	job	credit_score	office_location	user_embedding	last_updated

john	18	engineer	high	-122.4194,37.7749	b'\xcd\xcc\xcc=\xcd\xcc\xcc=\x00\x00\x00?'	1741627789

derrick	14	doctor	low	-122.4194,37.7749	b'\xcd\xcc\xcc=\xcd\xcc\xcc=\x00\x00\x00?'	1741627789

nancy	94	doctor	high	-122.4194,37.7749	b'333?\xcd\xcc\xcc=\x00\x00\x00?'	1710696589

tyler	100	engineer	high	-122.0839,37.3861	b'\xcd\xcc\xcc=\xcd\xcc\xcc>\x00\x00\x00?'	1742232589

tim	12	dermatologist	high	-122.0839,37.3861	b'\xcd\xcc\xcc>\xcd\xcc\xcc>\x00\x00\x00?'	1739644189

taimur	15	CEO	low	-122.0839,37.3861	b'\x9a\x99\x19?\xcd\xcc\xcc=\x00\x00\x00?'	1742232589

joe	35	dentist	medium	-122.0839,37.3861	b'fff?fff?\xcd\xcc\xcc='	1742232589

Hash or JSON -- how to choose?

Both storage options offer a variety of features and tradeoffs. Below we will work through a dummy dataset to learn when and how to use both.

Working with Hashes

Hashes in Redis are simple collections of field-value pairs. Think of it like a mutable single-level dictionary contains multiple "rows":

[code example]

Hashes are best suited for use cases with the following characteristics:
- Performance (speed) and storage space (memory consumption) are top concerns
- Data can be easily normalized and modeled as a single-level dict

Hashes are typically the default recommendation.

[code example]

[code example]

[code example]

    <StorageType.HASH: 'hash'>

#### Vectors as byte strings
One nuance when working with Hashes in Redis, is that all vectorized data must be passed as a byte string (for efficient storage, indexing, and processing). An example of that can be seen below:

[code example]

    {'user': 'john',
     'age': 18,
     'job': 'engineer',
     'credit_score': 'high',
     'office_location': '-122.4194,37.7749',
     'user_embedding': b'\xcd\xcc\xcc=\xcd\xcc\xcc=\x00\x00\x00?',
     'last_updated': 1741627789}

[code example]

[code example]

    Statistics:
    ╭─────────────────────────────┬────────────╮
    │ Stat Key                    │ Value      │
    ├─────────────────────────────┼────────────┤
    │ num_docs                    │ 7          │
    │ num_terms                   │ 6          │
    │ max_doc_id                  │ 7          │
    │ num_records                 │ 44         │
    │ percent_indexed             │ 1          │
    │ hash_indexing_failures      │ 0          │
    │ number_of_uses              │ 1          │
    │ bytes_per_record_avg        │ 40.2954559 │
    │ doc_table_size_mb           │ 7.27653503 │
    │ inverted_sz_mb              │ 0.00169086 │
    │ key_table_size_mb           │ 2.48908996 │
    │ offset_bits_per_record_avg  │ 8          │
    │ offset_vectors_sz_mb        │ 8.58306884 │
    │ offsets_per_term_avg        │ 0.20454545 │
    │ records_per_doc_avg         │ 6.28571414 │
    │ sortable_values_size_mb     │ 0          │
    │ total_indexing_time         │ 0.25799998 │
    │ total_inverted_index_blocks │ 18         │
    │ vector_index_sz_mb          │ 0.02023315 │
    ╰─────────────────────────────┴────────────╯

#### Performing Queries
Once our index is created and data is loaded into the right format, we can run queries against the index with RedisVL:

[code example]

vector_distance	user	credit_score	age	job	office_location

0	john	high	18	engineer	-122.4194,37.7749

0.109129190445	tyler	high	100	engineer	-122.0839,37.3861

[code example]

Working with JSON

JSON is best suited for use cases with the following characteristics:
- Ease of use and data model flexibility are top concerns
- Application data is already native JSON
- Replacing another document storage/db solution

[code example]

[code example]

[code example]

    13:02:56 [RedisVL] INFO   Indices:
    13:02:56 [RedisVL] INFO   1. user-json

#### Vectors as float arrays
Vectorized data stored in JSON must be stored as a pure array (python list) of floats. We will modify our sample data to account for this below:

[code example]

[code example]

    {'user': 'john',
     'age': 18,
     'job': 'engineer',
     'credit_score': 'high',
     'office_location': '-122.4194,37.7749',
     'user_embedding': [0.10000000149011612, 0.10000000149011612, 0.5],
     'last_updated': 1741627789}

[code example]

[code example]

vector_distance	user	credit_score	age	job	office_location

0	john	high	18	engineer	-122.4194,37.7749

0.109129190445	tyler	high	100	engineer	-122.0839,37.3861

Cleanup

[code example]

# Working with nested data in JSON

Redis also supports native **JSON** objects. These can be multi-level (nested) objects, with full JSONPath support for updating/retrieving sub elements:

[code example]

#### Full JSON Path support
Because Redis enables full JSON path support, when creating an index schema, elements need to be indexed and selected by their path with the desired `name` AND `path` that points to where the data is located within the objects.

By default, RedisVL will assume the path as `$.{name}` if not provided in JSON fields schema. If nested provide path as `$.object.attribute`

As an example:

[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:02:58 sentence_transformers.SentenceTransformer INFO   Use pytorch device_name: mps
    13:02:58 sentence_transformers.SentenceTransformer INFO   Load pretrained SentenceTransformer: sentence-transformers/all-mpnet-base-v2

    Batches: 100%|██████████| 1/1 [00:00<00:00,  7.23it/s]
    Batches: 100%|██████████| 1/1 [00:00<00:00, 12.93it/s]
    Batches: 100%|██████████| 1/1 [00:00<00:00, 14.10it/s]

[code example]

[code example]

    ['bike-json:01JY4J9M48CXF7F4Y6HRGEMT9B',
     'bike-json:01JY4J9M48RRY6F80HR82CVZ5G']

[code example]

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

**Note:** As shown in the example if you want to retrieve a field from json object that was not indexed you will also need to supply the full path as with `$.metadata.type`.

[code example]

    [{'id': 'bike-json:01JY4J9M48RRY6F80HR82CVZ5G',
      'vector_distance': '0.519989132881',
      'brand': 'Trek',
      '$.metadata.type': 'Enduro bikes'},
     {'id': 'bike-json:01JY4J9M48CXF7F4Y6HRGEMT9B',
      'vector_distance': '0.657624304295',
      'brand': 'Specialized',
      '$.metadata.type': 'Enduro bikes'}]

[code example]

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

# import necessary modules
import pickle

from redisvl.redis.utils import buffer_to_array
from redisvl.index import SearchIndex

# load in the example data and printing utils
data = pickle.load(open("hybrid_example_data.pkl", "rb"))

from jupyterutils import result_print, table_print

table_print(data)

{
    "model": "Deimos",
    "brand": "Ergonom",
    "type": "Enduro bikes",
    "price": 4972,
}

# define the hash index schema
hash_schema = {
    "index": {
        "name": "user-hash",
        "prefix": "user-hash-docs",
        "storage_type": "hash", # default setting -- HASH
    },
    "fields": [
        {"name": "user", "type": "tag"},
        {"name": "credit_score", "type": "tag"},
        {"name": "job", "type": "text"},
        {"name": "age", "type": "numeric"},
        {"name": "office_location", "type": "geo"},
        {
            "name": "user_embedding",
            "type": "vector",
            "attrs": {
                "dims": 3,
                "distance_metric": "cosine",
                "algorithm": "flat",
                "datatype": "float32"
            }

        }
    ],
}

# construct a search index from the hash schema
hindex = SearchIndex.from_dict(hash_schema, redis_url="redis://localhost:6379")

# create the index (no data yet)
hindex.create(overwrite=True)

# show the underlying storage type
hindex.storage_type

# show a single entry from the data that will be loaded
data[0]

# load hash data
keys = hindex.load(data)

from redisvl.query import VectorQuery
from redisvl.query.filter import Tag, Text, Num

t = (Tag("credit_score") == "high") & (Text("job") % "enginee*") & (Num("age") > 17)  # codespell:ignore enginee

v = VectorQuery(
    vector=[0.1, 0.1, 0.5],
    vector_field_name="user_embedding",
    return_fields=["user", "credit_score", "age", "job", "office_location"],
    filter_expression=t
)

results = hindex.query(v)
result_print(results)

# define the json index schema
json_schema = {
    "index": {
        "name": "user-json",
        "prefix": "user-json-docs",
        "storage_type": "json", # JSON storage type
    },
    "fields": [
        {"name": "user", "type": "tag"},
        {"name": "credit_score", "type": "tag"},
        {"name": "job", "type": "text"},
        {"name": "age", "type": "numeric"},
        {"name": "office_location", "type": "geo"},
        {
            "name": "user_embedding",
            "type": "vector",
            "attrs": {
                "dims": 3,
                "distance_metric": "cosine",
                "algorithm": "flat",
                "datatype": "float32"
            }

        }
    ],
}

# construct a search index from the json schema
jindex = SearchIndex.from_dict(json_schema, redis_url="redis://localhost:6379")

# create the index (no data yet)
jindex.create(overwrite=True)

# note the multiple indices in the same database
!rvl index listall

json_data = data.copy()

for d in json_data:
    d['user_embedding'] = buffer_to_array(d['user_embedding'], dtype='float32')

# inspect a single JSON record
json_data[0]

# we can now run the exact same query as above
result_print(jindex.query(v))

{
    "name": "Specialized Stump jumper",
    "metadata": {
        "model": "Stumpjumper",
        "brand": "Specialized",
        "type": "Enduro bikes",
        "price": 3000
    },
}

from redisvl.utils.vectorize import HFTextVectorizer

emb_model = HFTextVectorizer()

bike_data = [
    {
        "name": "Specialized Stump jumper",
        "metadata": {
            "model": "Stumpjumper",
            "brand": "Specialized",
            "type": "Enduro bikes",
            "price": 3000
        },
        "description": "The Specialized Stumpjumper is a versatile enduro bike that dominates both climbs and descents. Features a FACT 11m carbon fiber frame, FOX FLOAT suspension with 160mm travel, and SRAM X01 Eagle drivetrain. The asymmetric frame design and internal storage compartment make it a practical choice for all-day adventures."
    },
    {
        "name": "bike_2",
        "metadata": {
            "model": "Slash",
            "brand": "Trek",
            "type": "Enduro bikes",
            "price": 5000
        },
        "description": "Trek's Slash is built for aggressive enduro riding and racing. Featuring Trek's Alpha Aluminum frame with RE:aktiv suspension technology, 160mm travel, and Knock Block frame protection. Equipped with Bontrager components and a Shimano XT drivetrain, this bike excels on technical trails and enduro race courses."
    }
]

bike_data = [{**d, "bike_embedding": emb_model.embed(d["description"])} for d in bike_data]

bike_schema = {
    "index": {
        "name": "bike-json",
        "prefix": "bike-json",
        "storage_type": "json", # JSON storage type
    },
    "fields": [
        {
            "name": "model",
            "type": "tag",
            "path": "$.metadata.model" # note the '$'
        },
        {
            "name": "brand",
            "type": "tag",
            "path": "$.metadata.brand"
        },
        {
            "name": "price",
            "type": "numeric",
            "path": "$.metadata.price"
        },
        {
            "name": "bike_embedding",
            "type": "vector",
            "attrs": {
                "dims": len(bike_data[0]["bike_embedding"]),
                "distance_metric": "cosine",
                "algorithm": "flat",
                "datatype": "float32"
            }

        }
    ],
}

# construct a search index from the json schema
bike_index = SearchIndex.from_dict(bike_schema, redis_url="redis://localhost:6379")

# create the index (no data yet)
bike_index.create(overwrite=True)

from redisvl.query import VectorQuery

vec = emb_model.embed("I'd like a bike for aggressive riding")

v = VectorQuery(
    vector=vec,
    vector_field_name="bike_embedding",
    return_fields=[
        "brand",
        "name",
        "$.metadata.type"
    ]
)

results = bike_index.query(v)


Querying with RedisVL

Overview

In this notebook, we will explore more complex queries that can be performed with ``redisvl``

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

[code example]

user	age	job	credit_score	office_location	user_embedding	last_updated

john	18	engineer	high	-122.4194,37.7749	b'\xcd\xcc\xcc=\xcd\xcc\xcc=\x00\x00\x00?'	1741627789

derrick	14	doctor	low	-122.4194,37.7749	b'\xcd\xcc\xcc=\xcd\xcc\xcc=\x00\x00\x00?'	1741627789

nancy	94	doctor	high	-122.4194,37.7749	b'333?\xcd\xcc\xcc=\x00\x00\x00?'	1710696589

tyler	100	engineer	high	-122.0839,37.3861	b'\xcd\xcc\xcc=\xcd\xcc\xcc>\x00\x00\x00?'	1742232589

tim	12	dermatologist	high	-122.0839,37.3861	b'\xcd\xcc\xcc>\xcd\xcc\xcc>\x00\x00\x00?'	1739644189

taimur	15	CEO	low	-122.0839,37.3861	b'\x9a\x99\x19?\xcd\xcc\xcc=\x00\x00\x00?'	1742232589

joe	35	dentist	medium	-122.0839,37.3861	b'fff?fff?\xcd\xcc\xcc='	1742232589

[code example]

[code example]

[code example]

    13:00:56 [RedisVL] INFO   Indices:
    13:00:56 [RedisVL] INFO   1. user_queries

[code example]

[code example]

    7

Hybrid Queries

Hybrid queries are queries that combine multiple types of filters. For example, you may want to search for a user that is a certain age, has a certain job, and is within a certain distance of a location. This is a hybrid query that combines numeric, tag, and geographic filters.

Tag Filters

Tag filters are filters that are applied to tag fields. These are fields that are not tokenized and are used to store a single categorical value.

[code example]

vector_distance	user	credit_score	age	job	office_location	last_updated

0	john	high	18	engineer	-122.4194,37.7749	1741627789

0.109129190445	tyler	high	100	engineer	-122.0839,37.3861	1742232589

0.158808946609	tim	high	12	dermatologist	-122.0839,37.3861	1739644189

0.266666650772	nancy	high	94	doctor	-122.4194,37.7749	1710696589

[code example]

vector_distance	user	credit_score	age	job	office_location	last_updated

0	derrick	low	14	doctor	-122.4194,37.7749	1741627789

0.217882037163	taimur	low	15	CEO	-122.0839,37.3861	1742232589

0.653301358223	joe	medium	35	dentist	-122.0839,37.3861	1742232589

**Performance Tip:** For HNSW and SVS-VAMANA indexes, you can add runtime parameters to tune search performance:

[code example]

These parameters can be adjusted at query time without rebuilding the index. See the [Advanced Queries guide](11_advanced_queries.ipynb) for more details.

[code example]

vector_distance	user	credit_score	age	job	office_location	last_updated

0	john	high	18	engineer	-122.4194,37.7749	1741627789

0.109129190445	tyler	high	100	engineer	-122.0839,37.3861	1742232589

0.158808946609	tim	high	12	dermatologist	-122.0839,37.3861	1739644189

0.266666650772	nancy	high	94	doctor	-122.4194,37.7749	1710696589

0.653301358223	joe	medium	35	dentist	-122.0839,37.3861	1742232589

[code example]

vector_distance	user	credit_score	age	job	office_location	last_updated

0	john	high	18	engineer	-122.4194,37.7749	1741627789

0.109129190445	tyler	high	100	engineer	-122.0839,37.3861	1742232589

0.158808946609	tim	high	12	dermatologist	-122.0839,37.3861	1739644189

0.266666650772	nancy	high	94	doctor	-122.4194,37.7749	1710696589

0.653301358223	joe	medium	35	dentist	-122.0839,37.3861	1742232589

What about scenarios where you might want to dynamically generate a list of tags? Have no fear. RedisVL allows you to do this gracefully without having to check for the **empty case**. The **empty case** is when you attempt to run a Tag filter on a field with no defined values to match:

`Tag("credit_score") == []`

An empty filter like the one above will yield a `*` Redis query filter which implies the base case -- there is no filter here to use.

[code example]

vector_distance	user	credit_score	age	job	office_location	last_updated

0	john	high	18	engineer	-122.4194,37.7749	1741627789

0	derrick	low	14	doctor	-122.4194,37.7749	1741627789

0.109129190445	tyler	high	100	engineer	-122.0839,37.3861	1742232589

0.158808946609	tim	high	12	dermatologist	-122.0839,37.3861	1739644189

0.217882037163	taimur	low	15	CEO	-122.0839,37.3861	1742232589

0.266666650772	nancy	high	94	doctor	-122.4194,37.7749	1710696589

0.653301358223	joe	medium	35	dentist	-122.0839,37.3861	1742232589

Numeric Filters

Numeric filters are filters that are applied to numeric fields and can be used to isolate a range of values for a given field.

[code example]

vector_distance	user	credit_score	age	job	office_location	last_updated

0	john	high	18	engineer	-122.4194,37.7749	1741627789

0.217882037163	taimur	low	15	CEO	-122.0839,37.3861	1742232589

0.653301358223	joe	medium	35	dentist	-122.0839,37.3861	1742232589

[code example]

vector_distance	user	credit_score	age	job	office_location	last_updated

0	derrick	low	14	doctor	-122.4194,37.7749	1741627789

[code example]

vector_distance	user	credit_score	age	job	office_location	last_updated

0	john	high	18	engineer	-122.4194,37.7749	1741627789

0.109129190445	tyler	high	100	engineer	-122.0839,37.3861	1742232589

0.158808946609	tim	high	12	dermatologist	-122.0839,37.3861	1739644189

0.217882037163	taimur	low	15	CEO	-122.0839,37.3861	1742232589

0.266666650772	nancy	high	94	doctor	-122.4194,37.7749	1710696589

0.653301358223	joe	medium	35	dentist	-122.0839,37.3861	1742232589

Timestamp Filters

In redis all times are stored as an epoch time numeric however, this class allows you to filter with python datetime for ease of use.

[code example]

    Epoch comparison: 1742147139.132589

vector_distance	user	credit_score	age	job	office_location	last_updated

0.109129190445	tyler	high	100	engineer	-122.0839,37.3861	1742232589

0.217882037163	taimur	low	15	CEO	-122.0839,37.3861	1742232589

0.653301358223	joe	medium	35	dentist	-122.0839,37.3861	1742232589

[code example]

    Epoch comparison: 1742147139.132589

vector_distance	user	credit_score	age	job	office_location	last_updated

0	john	high	18	engineer	-122.4194,37.7749	1741627789

0	derrick	low	14	doctor	-122.4194,37.7749	1741627789

0.158808946609	tim	high	12	dermatologist	-122.0839,37.3861	1739644189

0.266666650772	nancy	high	94	doctor	-122.4194,37.7749	1710696589

[code example]

    Epoch between: 1736880339.132589 - 1742147139.132589

vector_distance	user	credit_score	age	job	office_location	last_updated

0	john	high	18	engineer	-122.4194,37.7749	1741627789

0	derrick	low	14	doctor	-122.4194,37.7749	1741627789

0.158808946609	tim	high	12	dermatologist	-122.0839,37.3861	1739644189

Text Filters

Text filters are filters that are applied to text fields. These filters are applied to the entire text field. For example, if you have a text field that contains the text "The quick brown fox jumps over the lazy dog", a text filter of "quick" will match this text field.

[code example]

vector_distance	user	credit_score	age	job	office_location	last_updated

0	derrick	low	14	doctor	-122.4194,37.7749	1741627789

0.266666650772	nancy	high	94	doctor	-122.4194,37.7749	1710696589

[code example]

vector_distance	user	credit_score	age	job	office_location	last_updated

0	john	high	18	engineer	-122.4194,37.7749	1741627789

0.109129190445	tyler	high	100	engineer	-122.0839,37.3861	1742232589

0.158808946609	tim	high	12	dermatologist	-122.0839,37.3861	1739644189

0.217882037163	taimur	low	15	CEO	-122.0839,37.3861	1742232589

0.653301358223	joe	medium	35	dentist	-122.0839,37.3861	1742232589

[code example]

vector_distance	user	credit_score	age	job	office_location	last_updated

0	derrick	low	14	doctor	-122.4194,37.7749	1741627789

0.266666650772	nancy	high	94	doctor	-122.4194,37.7749	1710696589

[code example]

vector_distance	user	credit_score	age	job	office_location	last_updated

0	john	high	18	engineer	-122.4194,37.7749	1741627789

0.109129190445	tyler	high	100	engineer	-122.0839,37.3861	1742232589

[code example]

vector_distance	user	credit_score	age	job	office_location	last_updated

0	john	high	18	engineer	-122.4194,37.7749	1741627789

0	derrick	low	14	doctor	-122.4194,37.7749	1741627789

0.109129190445	tyler	high	100	engineer	-122.0839,37.3861	1742232589

0.266666650772	nancy	high	94	doctor	-122.4194,37.7749	1710696589

[code example]

vector_distance	user	credit_score	age	job	office_location	last_updated

0	john	high	18	engineer	-122.4194,37.7749	1741627789

0	derrick	low	14	doctor	-122.4194,37.7749	1741627789

0.109129190445	tyler	high	100	engineer	-122.0839,37.3861	1742232589

0.158808946609	tim	high	12	dermatologist	-122.0839,37.3861	1739644189

0.217882037163	taimur	low	15	CEO	-122.0839,37.3861	1742232589

0.266666650772	nancy	high	94	doctor	-122.4194,37.7749	1710696589

0.653301358223	joe	medium	35	dentist	-122.0839,37.3861	1742232589

Use raw query strings as input. Below we use the `~` flag to indicate that the full text query is optional. We also choose the BM25 scorer and return document scores along with the result.

[code example]

    [{'id': 'user_queries_docs:01JY4J5VC91SV4C91BM4D0FCV2',
      'score': 0.9090908893868948,
      'vector_distance': '0',
      'user': 'john',
      'credit_score': 'high',
      'age': '18',
      'job': 'engineer',
      'office_location': '-122.4194,37.7749',
      'last_updated': '1741627789'},
     {'id': 'user_queries_docs:01JY4J5VC90DRSFJ0WKXXN49JT',
      'score': 0.0,
      'vector_distance': '0',
      'user': 'derrick',
      'credit_score': 'low',
      'age': '14',
      'job': 'doctor',
      'office_location': '-122.4194,37.7749',
      'last_updated': '1741627789'},
     {'id': 'user_queries_docs:01JY4J5VC9QTPMCD60YP40Q6PW',
      'score': 0.9090908893868948,
      'vector_distance': '0.109129190445',
      'user': 'tyler',
      'credit_score': 'high',
      'age': '100',
      'job': 'engineer',
      'office_location': '-122.0839,37.3861',
      'last_updated': '1742232589'},
     {'id': 'user_queries_docs:01JY4J5VC9FW7QQNJKDJ4Z7PRG',
      'score': 0.0,
      'vector_distance': '0.158808946609',
      'user': 'tim',
      'credit_score': 'high',
      'age': '12',
      'job': 'dermatologist',
      'office_location': '-122.0839,37.3861',
      'last_updated': '1739644189'},
     {'id': 'user_queries_docs:01JY4J5VC940DJ9F47EJ6KN2MH',
      'score': 0.0,
      'vector_distance': '0.217882037163',
      'user': 'taimur',
      'credit_score': 'low',
      'age': '15',
      'job': 'CEO',
      'office_location': '-122.0839,37.3861',
      'last_updated': '1742232589'},
     {'id': 'user_queries_docs:01JY4J5VC9D53KQD7ZTRP14KCE',
      'score': 0.0,
      'vector_distance': '0.266666650772',
      'user': 'nancy',
      'credit_score': 'high',
      'age': '94',
      'job': 'doctor',
      'office_location': '-122.4194,37.7749',
      'last_updated': '1710696589'},
     {'id': 'user_queries_docs:01JY4J5VC9806MD90GBZNP0MNY',
      'score': 0.0,
      'vector_distance': '0.653301358223',
      'user': 'joe',
      'credit_score': 'medium',
      'age': '35',
      'job': 'dentist',
      'office_location': '-122.0839,37.3861',
      'last_updated': '1742232589'}]

Geographic Filters

Geographic filters are filters that are applied to geographic fields. These filters are used to find results that are within a certain distance of a given point. The distance is specified in kilometers, miles, meters, or feet. A radius can also be specified to find results within a certain radius of a given point.

[code example]

score	vector_distance	user	credit_score	age	job	office_location	last_updated

0.4545454446934474	0	john	high	18	engineer	-122.4194,37.7749	1741627789

0.4545454446934474	0	derrick	low	14	doctor	-122.4194,37.7749	1741627789

0.4545454446934474	0.266666650772	nancy	high	94	doctor	-122.4194,37.7749	1710696589

[code example]

score	vector_distance	user	credit_score	age	job	office_location	last_updated

0.4545454446934474	0	john	high	18	engineer	-122.4194,37.7749	1741627789

0.4545454446934474	0	derrick	low	14	doctor	-122.4194,37.7749	1741627789

0.4545454446934474	0.109129190445	tyler	high	100	engineer	-122.0839,37.3861	1742232589

0.4545454446934474	0.158808946609	tim	high	12	dermatologist	-122.0839,37.3861	1739644189

0.4545454446934474	0.217882037163	taimur	low	15	CEO	-122.0839,37.3861	1742232589

0.4545454446934474	0.266666650772	nancy	high	94	doctor	-122.4194,37.7749	1710696589

0.4545454446934474	0.653301358223	joe	medium	35	dentist	-122.0839,37.3861	1742232589

[code example]

score	vector_distance	user	credit_score	age	job	office_location	last_updated

0.0	0.109129190445	tyler	high	100	engineer	-122.0839,37.3861	1742232589

0.0	0.158808946609	tim	high	12	dermatologist	-122.0839,37.3861	1739644189

0.0	0.217882037163	taimur	low	15	CEO	-122.0839,37.3861	1742232589

0.0	0.653301358223	joe	medium	35	dentist	-122.0839,37.3861	1742232589

Combining Filters

In this example, we will combine a numeric filter with a tag filter. We will search for users that are between the ages of 20 and 30 and have a job of "engineer".

Intersection ("and")

Union ("or")

The union of two queries is the set of all results that are returned by either of the two queries. The union of two queries is performed using the `|` operator.

[code example]

vector_distance	user	credit_score	age	job	office_location

0	derrick	low	14	doctor	-122.4194,37.7749

0.109129190445	tyler	high	100	engineer	-122.0839,37.3861

0.158808946609	tim	high	12	dermatologist	-122.0839,37.3861

0.217882037163	taimur	low	15	CEO	-122.0839,37.3861

0.266666650772	nancy	high	94	doctor	-122.4194,37.7749

Dynamic Combination

There are often situations where you may or may not want to use a filter in a
given query. As shown above, filters will except the ``None`` type and revert
to a wildcard filter essentially returning all results.

The same goes for filter combinations which enables rapid reuse of filters in
requests with different parameters as shown below. This removes the need for
a number of "if-then" conditionals to test for the empty case.

[code example]

[code example]

vector_distance	user	credit_score	age	job	office_location

0.109129190445	tyler	high	100	engineer	-122.0839,37.3861

[code example]

vector_distance	user	credit_score	age	job	office_location

0.109129190445	tyler	high	100	engineer	-122.0839,37.3861

0.266666650772	nancy	high	94	doctor	-122.4194,37.7749

[code example]

vector_distance	user	credit_score	age	job	office_location

0.109129190445	tyler	high	100	engineer	-122.0839,37.3861

0.266666650772	nancy	high	94	doctor	-122.4194,37.7749

0.653301358223	joe	medium	35	dentist	-122.0839,37.3861

[code example]

vector_distance	user	credit_score	age	job	office_location

0	john	high	18	engineer	-122.4194,37.7749

0	derrick	low	14	doctor	-122.4194,37.7749

0.109129190445	tyler	high	100	engineer	-122.0839,37.3861

0.158808946609	tim	high	12	dermatologist	-122.0839,37.3861

0.217882037163	taimur	low	15	CEO	-122.0839,37.3861

0.266666650772	nancy	high	94	doctor	-122.4194,37.7749

0.653301358223	joe	medium	35	dentist	-122.0839,37.3861

Non-vector Queries

In some cases, you may not want to run a vector query, but just use a ``FilterExpression`` similar to a SQL query. The ``FilterQuery`` class enable this functionality. It is similar to the ``VectorQuery`` class but solely takes a ``FilterExpression``.

[code example]

user	credit_score	age	job

derrick	low	14	doctor

taimur	low	15	CEO

Count Queries

In some cases, you may need to use a ``FilterExpression`` to execute a ``CountQuery`` that simply returns the count of the number of entities in the pertaining set. It is similar to the ``FilterQuery`` class but does not return the values of the underlying data.

[code example]

    2 records match the filter expression @credit_score:{low} for the given index.

Range Queries

Range Queries are a useful method to perform a vector search where only results within a vector ``distance_threshold`` are returned. This enables the user to find all records within their dataset that are similar to a query vector where "similar" is defined by a quantitative value.

[code example]

vector_distance	user	credit_score	age	job

0	john	high	18	engineer

0	derrick	low	14	doctor

0.109129190445	tyler	high	100	engineer

0.158808946609	tim	high	12	dermatologist

We can also change the distance threshold of the query object between uses if we like. Here we will set ``distance_threshold==0.1``. This means that the query object will return all matches that are within 0.1 of the query object. This is a small distance, so we expect to get fewer matches than before.

[code example]

vector_distance	user	credit_score	age	job

0	john	high	18	engineer

0	derrick	low	14	doctor

Range queries can also be used with filters like any other query type. The following limits the results to only include records with a ``job`` of ``engineer`` while also being within the vector range (aka distance).

[code example]

vector_distance	user	credit_score	age	job

0	john	high	18	engineer

Advanced Query Modifiers

See all modifier options available on the query API docs: https://redis.io/docs/latest/develop/ai/redisvl/api/query

[code example]

vector_distance	age	user	credit_score	job	office_location

0.109129190445	100	tyler	high	engineer	-122.0839,37.3861

0	18	john	high	engineer	-122.4194,37.7749

Raw Redis Query String

Sometimes it's helpful to convert these classes into their raw Redis query strings.

[code example]

    '@job:("engineer")=>[KNN 5 @user_embedding $vector AS vector_distance] RETURN 6 user credit_score age job office_location vector_distance SORTBY age DESC DIALECT 3 LIMIT 0 5'

[code example]

    '@credit_score:{high}'

[code example]

    '((@credit_score:{high} @age:[18 +inf]) @age:[-inf 100])'

The RedisVL `SearchIndex` class exposes a `search()` method which is a simple wrapper around the `FT.SEARCH` API.
Provide any valid Redis query string.

[code example]

    {'id': 'user_queries_docs:01JY4J5VC91SV4C91BM4D0FCV2', 'payload': None, 'user': 'john', 'age': '18', 'job': 'engineer', 'credit_score': 'high', 'office_location': '-122.4194,37.7749', 'user_embedding': '==\x00\x00\x00?', 'last_updated': '1741627789'}
    {'id': 'user_queries_docs:01JY4J5VC9D53KQD7ZTRP14KCE', 'payload': None, 'user': 'nancy', 'age': '94', 'job': 'doctor', 'credit_score': 'high', 'office_location': '-122.4194,37.7749', 'user_embedding': '333?=\x00\x00\x00?', 'last_updated': '1710696589'}
    {'id': 'user_queries_docs:01JY4J5VC9QTPMCD60YP40Q6PW', 'payload': None, 'user': 'tyler', 'age': '100', 'job': 'engineer', 'credit_score': 'high', 'office_location': '-122.0839,37.3861', 'user_embedding': '=>\x00\x00\x00?', 'last_updated': '1742232589'}
    {'id': 'user_queries_docs:01JY4J5VC9FW7QQNJKDJ4Z7PRG', 'payload': None, 'user': 'tim', 'age': '12', 'job': 'dermatologist', 'credit_score': 'high', 'office_location': '-122.0839,37.3861', 'user_embedding': '>>\x00\x00\x00?', 'last_updated': '1739644189'}

[code example]

import pickle
from jupyterutils import table_print, result_print

# load in the example data and printing utils
data = pickle.load(open("hybrid_example_data.pkl", "rb"))
table_print(data)

schema = {
    "index": {
        "name": "user_queries",
        "prefix": "user_queries_docs",
        "storage_type": "hash", # default setting -- HASH
    },
    "fields": [
        {"name": "user", "type": "tag"},
        {"name": "credit_score", "type": "tag"},
        {"name": "job", "type": "text"},
        {"name": "age", "type": "numeric"},
        {"name": "last_updated", "type": "numeric"},
        {"name": "office_location", "type": "geo"},
        {
            "name": "user_embedding",
            "type": "vector",
            "attrs": {
                "dims": 3,
                "distance_metric": "cosine",
                "algorithm": "flat",
                "datatype": "float32"
            }

        }
    ],
}

from redisvl.index import SearchIndex

# construct a search index from the schema
index = SearchIndex.from_dict(schema, redis_url="redis://localhost:6379")

# create the index (no data yet)
index.create(overwrite=True)

# use the CLI to see the created index
!rvl index listall

# load data to redis
keys = index.load(data)

from redisvl.query import VectorQuery
from redisvl.query.filter import Tag

t = Tag("credit_score") == "high"

v = VectorQuery(
    vector=[0.1, 0.1, 0.5],
    vector_field_name="user_embedding",
    return_fields=["user", "credit_score", "age", "job", "office_location", "last_updated"],
    filter_expression=t
)

results = index.query(v)
result_print(results)

# negation
t = Tag("credit_score") != "high"

v.set_filter(t)
result_print(index.query(v))

# Example with runtime parameters for better recall
v = VectorQuery(
    vector=[0.1, 0.1, 0.5],
    vector_field_name="user_embedding",
    return_fields=["user", "credit_score", "age"],
    filter_expression=t,
    ef_runtime=100,  # HNSW: higher for better recall
    search_window_size=40  # SVS-VAMANA: larger window for better recall
)

# use multiple tags as a list
t = Tag("credit_score") == ["high", "medium"]

v.set_filter(t)
result_print(index.query(v))

# use multiple tags as a set (to enforce uniqueness)
t = Tag("credit_score") == set(["high", "high", "medium"])

v.set_filter(t)
result_print(index.query(v))

# gracefully fallback to "*" filter if empty case
empty_case = Tag("credit_score") == []

v.set_filter(empty_case)
result_print(index.query(v))

from redisvl.query.filter import Num

numeric_filter = Num("age").between(15, 35)

v.set_filter(numeric_filter)
result_print(index.query(v))

# exact match query
numeric_filter = Num("age") == 14

v.set_filter(numeric_filter)
result_print(index.query(v))

# negation
numeric_filter = Num("age") != 14

v.set_filter(numeric_filter)
result_print(index.query(v))

from redisvl.query.filter import Timestamp
from datetime import datetime

dt = datetime(2025, 3, 16, 13, 45, 39, 132589)
print(f'Epoch comparison: {dt.timestamp()}')

timestamp_filter = Timestamp("last_updated") > dt

v.set_filter(timestamp_filter)
result_print(index.query(v))

from redisvl.query.filter import Timestamp
from datetime import datetime

dt = datetime(2025, 3, 16, 13, 45, 39, 132589)

print(f'Epoch comparison: {dt.timestamp()}')

timestamp_filter = Timestamp("last_updated") < dt

v.set_filter(timestamp_filter)
result_print(index.query(v))

from redisvl.query.filter import Timestamp
from datetime import datetime

dt_1 = datetime(2025, 1, 14, 13, 45, 39, 132589)
dt_2 = datetime(2025, 3, 16, 13, 45, 39, 132589)

print(f'Epoch between: {dt_1.timestamp()} - {dt_2.timestamp()}')

timestamp_filter = Timestamp("last_updated").between(dt_1, dt_2)

v.set_filter(timestamp_filter)
result_print(index.query(v))

from redisvl.query.filter import Text

# exact match filter -- document must contain the exact word doctor
text_filter = Text("job") == "doctor"

v.set_filter(text_filter)
result_print(index.query(v))

# negation -- document must not contain the exact word doctor
negate_text_filter = Text("job") != "doctor"

v.set_filter(negate_text_filter)
result_print(index.query(v))

# wildcard match filter
wildcard_filter = Text("job") % "doct*"

v.set_filter(wildcard_filter)
result_print(index.query(v))

# fuzzy match filter
fuzzy_match = Text("job") % "%%engine%%"

v.set_filter(fuzzy_match)
result_print(index.query(v))

# conditional -- match documents with job field containing engineer OR doctor
conditional = Text("job") % "engineer|doctor"

v.set_filter(conditional)
result_print(index.query(v))

# gracefully fallback to "*" filter if empty case
empty_case = Text("job") % ""

v.set_filter(empty_case)
result_print(index.query(v))

v.set_filter("(~(@job:engineer))")
v.scorer("BM25").with_scores()

index.query(v)

from redisvl.query.filter import Geo, GeoRadius

# within 10 km of San Francisco office
geo_filter = Geo("office_location") == GeoRadius(-122.4194, 37.7749, 10, "km")

v.set_filter(geo_filter)
result_print(index.query(v))

# within 100 km Radius of San Francisco office
geo_filter = Geo("office_location") == GeoRadius(-122.4194, 37.7749, 100, "km")

v.set_filter(geo_filter)
result_print(index.query(v))

# not within 10 km Radius of San Francisco office
geo_filter = Geo("office_location") != GeoRadius(-122.4194, 37.7749, 10, "km")

v.set_filter(geo_filter)
result_print(index.query(v))

t = Tag("credit_score") == "high"
low = Num("age") >= 18
high = Num("age") <= 100
ts = Timestamp("last_updated") > datetime(2025, 3, 16, 13, 45, 39, 132589)

combined = t & low & high & ts

v = VectorQuery([0.1, 0.1, 0.5],
                "user_embedding",
                return_fields=["user", "credit_score", "age", "job",  "office_location"],
                filter_expression=combined)

result_print(index.query(v))

low = Num("age") < 18
high = Num("age") > 93

combined = low | high

v.set_filter(combined)
result_print(index.query(v))

def make_filter(age=None, credit=None, job=None):
    flexible_filter = (
        (Num("age") > age) &
        (Tag("credit_score") == credit) &
        (Text("job") % job)
    )
    return flexible_filter

# all parameters
combined = make_filter(age=18, credit="high", job="engineer")
v.set_filter(combined)
result_print(index.query(v))

# just age and credit_score
combined = make_filter(age=18, credit="high")
v.set_filter(combined)
result_print(index.query(v))

# just age
combined = make_filter(age=18)
v.set_filter(combined)
result_print(index.query(v))

# no filters
combined = make_filter()
v.set_filter(combined)
result_print(index.query(v))

from redisvl.query import FilterQuery

has_low_credit = Tag("credit_score") == "low"

filter_query = FilterQuery(
    return_fields=["user", "credit_score", "age", "job", "location"],
    filter_expression=has_low_credit
)

results = index.query(filter_query)

result_print(results)

from redisvl.query import CountQuery

has_low_credit = Tag("credit_score") == "low"

filter_query = CountQuery(filter_expression=has_low_credit)

count = index.query(filter_query)

print(f"{count} records match the filter expression {str(has_low_credit)} for the given index.")

from redisvl.query import RangeQuery

range_query = RangeQuery(
    vector=[0.1, 0.1, 0.5],
    vector_field_name="user_embedding",
    return_fields=["user", "credit_score", "age", "job", "location"],
    distance_threshold=0.2
)

# same as the vector query or filter query
results = index.query(range_query)

result_print(results)

is_engineer = Text("job") == "engineer"

range_query.set_filter(is_engineer)

result_print(index.query(range_query))

# Sort by a different field and change dialect
v = VectorQuery(
    vector=[0.1, 0.1, 0.5],
    vector_field_name="user_embedding",
    return_fields=["user", "credit_score", "age", "job",  "office_location"],
    num_results=5,
    filter_expression=is_engineer
).sort_by("age", asc=False).dialect(3)

result = index.query(v)
result_print(result)

# check out the complex query from above
str(v)

t = Tag("credit_score") == "high"

str(t)

t = Tag("credit_score") == "high"
low = Num("age") >= 18
high = Num("age") <= 100

combined = t & low & high

str(combined)

results = index.search(str(t))
for r in results.docs:
    print(r.__dict__)


User Guides

Getting Started with RedisVL

Querying with RedisVL

LLM Caching

Vectorizers

Hash vs JSON Storage

Rerankers

LLM Message History

LLM Session Memory

Semantic Routing

SVS-VAMANA Vector Search

Threshold Optimization

Caching Embeddings

Advanced Query Types

Release Guides

Content

User guides provide helpful resources for using RedisVL and its different components.

* [Getting Started with RedisVL](getting_started/)
  * [Define an `IndexSchema`](getting_started/#define-an-indexschema)
  * [Sample Dataset Preparation](getting_started/#sample-dataset-preparation)
  * [Create a `SearchIndex`](getting_started/#create-a-searchindex)
  * [Inspect with the `rvl` CLI](getting_started/#inspect-with-the-rvl-cli)
  * [Load Data to `SearchIndex`](getting_started/#load-data-to-searchindex)
  * [Creating `VectorQuery` Objects](getting_started/#creating-vectorquery-objects)
  * [Using an Asynchronous Redis Client](getting_started/#using-an-asynchronous-redis-client)
  * [Updating a schema](getting_started/#updating-a-schema)
  * [Check Index Stats](getting_started/#check-index-stats)
  * [Cleanup](getting_started/#cleanup)
* [Querying with RedisVL](hybrid_queries/)
  * [Hybrid Queries](hybrid_queries/#hybrid-queries)
  * [Combining Filters](hybrid_queries/#combining-filters)
  * [Non-vector Queries](hybrid_queries/#non-vector-queries)
  * [Count Queries](hybrid_queries/#count-queries)
  * [Range Queries](hybrid_queries/#range-queries)
  * [Advanced Query Modifiers](hybrid_queries/#advanced-query-modifiers)
* [LLM Caching](llmcache/)
  * [Initializing `SemanticCache`](llmcache/#initializing-semanticcache)
  * [Basic Cache Usage](llmcache/#basic-cache-usage)
  * [Customize the Distance Threshold](llmcache/#customize-the-distance-threshold)
  * [Utilize TTL](llmcache/#utilize-ttl)
  * [Simple Performance Testing](llmcache/#simple-performance-testing)
  * [Cache Access Controls, Tags & Filters](llmcache/#cache-access-controls-tags-filters)
* [Vectorizers](vectorizers/)
  * [Creating Text Embeddings](vectorizers/#creating-text-embeddings)
  * [Search with Provider Embeddings](vectorizers/#search-with-provider-embeddings)
  * [Selecting your float data type](vectorizers/#selecting-your-float-data-type)
* [Hash vs JSON Storage](hash_vs_json/)
  * [Hash or JSON – how to choose?](hash_vs_json/#hash-or-json-how-to-choose)
  * [Cleanup](hash_vs_json/#cleanup)
* [Working with nested data in JSON](hash_vs_json/#working-with-nested-data-in-json)
  * [Full JSON Path support](hash_vs_json/#full-json-path-support)
  * [As an example:](hash_vs_json/#as-an-example)
* [Rerankers](rerankers/)
  * [Simple Reranking](rerankers/#simple-reranking)
* [LLM Message History](message_history/)
  * [Managing multiple users and conversations](message_history/#managing-multiple-users-and-conversations)
  * [Semantic message history](message_history/#semantic-message-history)
  * [Conversation control](message_history/#conversation-control)
* [Semantic Routing](semantic_router/)
  * [Define the Routes](semantic_router/#define-the-routes)
  * [Initialize the SemanticRouter](semantic_router/#initialize-the-semanticrouter)
  * [Simple routing](semantic_router/#simple-routing)
  * [Update the routing config](semantic_router/#update-the-routing-config)
  * [Router serialization](semantic_router/#router-serialization)
  * [Add route references](semantic_router/#add-route-references)
  * [Get route references](semantic_router/#get-route-references)
  * [Delete route references](semantic_router/#delete-route-references)
  * [Clean up the router](semantic_router/#clean-up-the-router)
* [SVS-VAMANA Vector Search](svs_vamana/)
  * [Prerequisites](svs_vamana/#prerequisites)
  * [Quick Start with CompressionAdvisor](svs_vamana/#quick-start-with-compressionadvisor)
  * [Creating an SVS-VAMANA Index](svs_vamana/#creating-an-svs-vamana-index)
  * [Loading Sample Data](svs_vamana/#loading-sample-data)
  * [Performing Vector Searches](svs_vamana/#performing-vector-searches)
  * [Runtime Parameters for Performance Tuning](svs_vamana/#runtime-parameters-for-performance-tuning)
  * [Understanding Compression Types](svs_vamana/#understanding-compression-types)
  * [Compression Types Explained](svs_vamana/#compression-types-explained)
  * [Hybrid Queries with SVS-VAMANA](svs_vamana/#hybrid-queries-with-svs-vamana)
  * [Performance Monitoring](svs_vamana/#performance-monitoring)
  * [Manual Configuration (Advanced)](svs_vamana/#manual-configuration-advanced)
  * [Best Practices and Tips](svs_vamana/#best-practices-and-tips)
  * [Cleanup](svs_vamana/#cleanup)
* [Caching Embeddings](embeddings_cache/)
  * [Setup](embeddings_cache/#setup)
  * [Initializing the EmbeddingsCache](embeddings_cache/#initializing-the-embeddingscache)
  * [Basic Usage](embeddings_cache/#basic-usage)
  * [Advanced Usage](embeddings_cache/#advanced-usage)
  * [Async Support](embeddings_cache/#async-support)
  * [Real-World Example](embeddings_cache/#real-world-example)
  * [Performance Benchmark](embeddings_cache/#performance-benchmark)
  * [Common Use Cases for Embedding Caching](embeddings_cache/#common-use-cases-for-embedding-caching)
  * [Cleanup](embeddings_cache/#cleanup)
  * [Summary](embeddings_cache/#summary)
* [Advanced Query Types](advanced_queries/)
  * [Setup and Data Preparation](advanced_queries/#setup-and-data-preparation)
  * [Define the Schema](advanced_queries/#define-the-schema)
  * [Create Index and Load Data](advanced_queries/#create-index-and-load-data)
  * [1. TextQuery: Full Text Search](advanced_queries/#textquery-full-text-search)
  * [2. AggregateHybridQuery: Combining Text and Vector Search](advanced_queries/#aggregatehybridquery-combining-text-and-vector-search)
  * [3. MultiVectorQuery: Multi-Vector Search](advanced_queries/#multivectorquery-multi-vector-search)
  * [Comparing Query Types](advanced_queries/#comparing-query-types)
  * [Best Practices](advanced_queries/#best-practices)


LLM Caching

Overview

This notebook demonstrates how to use RedisVL's `SemanticCache` to cache LLM responses based on semantic similarity. Semantic caching can significantly reduce API costs and latency by retrieving cached responses for semantically similar prompts instead of making redundant API calls.

Key features covered:
- Basic cache operations (store, check, clear)
- Customizing semantic similarity thresholds
- TTL policies for cache expiration
- Performance benchmarking
- Access controls with tags and filters for multi-user scenarios

Prerequisites:
- Ensure `redisvl` is installed in your Python environment
- Have a running instance of [Redis Stack](https://redis.io/docs/install/install-stack/) or [Redis Cloud](https://redis.io/cloud)
- OpenAI API key for the examples

First, we will import [OpenAI](https://platform.openai.com) to use their API for responding to user prompts. We will also create a simple `ask_openai` helper method to assist.

[code example]

[code example]

    The capital of France is Paris.

Initializing ``SemanticCache``

``SemanticCache`` will automatically create an index within Redis upon initialization for the semantic cache content.

[code example]

    13:02:02 sentence_transformers.SentenceTransformer INFO   Use pytorch device_name: mps
    13:02:02 sentence_transformers.SentenceTransformer INFO   Load pretrained SentenceTransformer: redis/langcache-embed-v1
    13:02:02 sentence_transformers.SentenceTransformer WARNING   You try to use a model that was created with version 4.1.0, however, your version is 3.4.1. This might cause unexpected behavior or errors. In that case, try to update to the latest version.

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

[code example]

    Index Information:
    ╭───────────────┬───────────────┬───────────────┬───────────────┬───────────────╮
    │ Index Name    │ Storage Type  │ Prefixes      │ Index Options │ Indexing      │
    ├───────────────┼───────────────┼───────────────┼───────────────┼───────────────┤
    | llmcache      | HASH          | ['llmcache']  | []            | 0             |
    ╰───────────────┴───────────────┴───────────────┴───────────────┴───────────────╯
    Index Fields:
    ╭─────────────────┬─────────────────┬─────────────────┬─────────────────┬─────────────────┬─────────────────┬─────────────────┬─────────────────┬─────────────────┬─────────────────┬─────────────────╮
    │ Name            │ Attribute       │ Type            │ Field Option    │ Option Value    │ Field Option    │ Option Value    │ Field Option    │ Option Value    │ Field Option    │ Option Value    │
    ├─────────────────┼─────────────────┼─────────────────┼─────────────────┼─────────────────┼─────────────────┼─────────────────┼─────────────────┼─────────────────┼─────────────────┼─────────────────┤
    │ prompt          │ prompt          │ TEXT            │ WEIGHT          │ 1               │                 │                 │                 │                 │                 │                 │
    │ response        │ response        │ TEXT            │ WEIGHT          │ 1               │                 │                 │                 │                 │                 │                 │
    │ inserted_at     │ inserted_at     │ NUMERIC         │                 │                 │                 │                 │                 │                 │                 │                 │
    │ updated_at      │ updated_at      │ NUMERIC         │                 │                 │                 │                 │                 │                 │                 │                 │
    │ prompt_vector   │ prompt_vector   │ VECTOR          │ algorithm       │ FLAT            │ data_type       │ FLOAT32         │ dim             │ 768             │ distance_metric │ COSINE          │
    ╰─────────────────┴─────────────────┴─────────────────┴─────────────────┴─────────────────┴─────────────────┴─────────────────┴─────────────────┴─────────────────┴─────────────────┴─────────────────╯

Basic Cache Usage

[code example]

[code example]

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

    Empty cache

Our initial cache check should be empty since we have not yet stored anything in the cache. Below, store the `question`,
proper `response`, and any arbitrary `metadata` (as a python dictionary object) in the cache.

[code example]

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

    'llmcache:115049a298532be2f181edb03f766770c0db84c22aff39003fec340deaec7545'

Now we will check the cache again with the same question and with a semantically similar question:

[code example]

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

    [{'prompt': 'What is the capital of France?', 'response': 'Paris', 'metadata': {'city': 'Paris', 'country': 'france'}, 'key': 'llmcache:115049a298532be2f181edb03f766770c0db84c22aff39003fec340deaec7545'}]

[code example]

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

    'Paris'

Customize the Distance Threshold

For most use cases, the right semantic similarity threshold is not a fixed quantity. Depending on the choice of embedding model,
the properties of the input query, and even business use case -- the threshold might need to change.

Fortunately, you can seamlessly adjust the threshold at any point like below:

[code example]

[code example]

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

    'Paris'

[code example]

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

    []

Utilize TTL

Redis uses TTL policies (optional) to expire individual keys at points in time in the future.
This allows you to focus on your data flow and business logic without bothering with complex cleanup tasks.

A TTL policy set on the `SemanticCache` allows you to temporarily hold onto cache entries. Below, we will set the TTL policy to 5 seconds.

[code example]

[code example]

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

[code example]

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

    []

[code example]

Simple Performance Testing

Next, we will measure the speedup obtained by using ``SemanticCache``. We will use the ``time`` module to measure the time taken to generate responses with and without ``SemanticCache``.

[code example]

[code example]

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

    13:02:17 httpx INFO   HTTP Request: POST https://api.openai.com/v1/completions "HTTP/1.1 200 OK"
    Without caching, a call to openAI to answer this simple question took 1.7948627471923828 seconds.

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

    'llmcache:67e0f6e28fe2a61c0022fd42bf734bb8ffe49d3e375fd69d692574295a20fc1a'

[code example]

    Batches: 100%|██████████| 1/1 [00:00<00:00, 20.90it/s]
    Batches: 100%|██████████| 1/1 [00:00<00:00, 23.24it/s]
    Batches: 100%|██████████| 1/1 [00:00<00:00, 22.85it/s]
    Batches: 100%|██████████| 1/1 [00:00<00:00, 21.98it/s]
    Batches: 100%|██████████| 1/1 [00:00<00:00, 22.65it/s]
    Batches: 100%|██████████| 1/1 [00:00<00:00, 22.65it/s]
    Batches: 100%|██████████| 1/1 [00:00<00:00, 21.84it/s]
    Batches: 100%|██████████| 1/1 [00:00<00:00, 20.67it/s]
    Batches: 100%|██████████| 1/1 [00:00<00:00, 22.08it/s]
    Batches: 100%|██████████| 1/1 [00:00<00:00, 21.14it/s]

    Avg time taken with LLM cache enabled: 0.049193501472473145
    Percentage of time saved: 97.26%

[code example]

    Statistics:
    ╭─────────────────────────────┬────────────╮
    │ Stat Key                    │ Value      │
    ├─────────────────────────────┼────────────┤
    │ num_docs                    │ 1          │
    │ num_terms                   │ 19         │
    │ max_doc_id                  │ 3          │
    │ num_records                 │ 29         │
    │ percent_indexed             │ 1          │
    │ hash_indexing_failures      │ 0          │
    │ number_of_uses              │ 19         │
    │ bytes_per_record_avg        │ 75.9655151 │
    │ doc_table_size_mb           │ 1.34468078 │
    │ inverted_sz_mb              │ 0.00210094 │
    │ key_table_size_mb           │ 2.76565551 │
    │ offset_bits_per_record_avg  │ 8          │
    │ offset_vectors_sz_mb        │ 2.09808349 │
    │ offsets_per_term_avg        │ 0.75862067 │
    │ records_per_doc_avg         │ 29         │
    │ sortable_values_size_mb     │ 0          │
    │ total_indexing_time         │ 14.3260002 │
    │ total_inverted_index_blocks │ 21         │
    │ vector_index_sz_mb          │ 3.01609802 │
    ╰─────────────────────────────┴────────────╯

[code example]

Cache Access Controls, Tags & Filters

When running complex workflows with similar applications, or handling multiple users it's important to keep data segregated. Building on top of RedisVL's support for complex and hybrid queries we can tag and filter cache entries using custom-defined `filterable_fields`.

Let's store multiple users' data in our cache with similar prompts and ensure we return only the correct user information:

[code example]

    13:02:20 sentence_transformers.SentenceTransformer INFO   Use pytorch device_name: mps
    13:02:20 sentence_transformers.SentenceTransformer INFO   Load pretrained SentenceTransformer: redis/langcache-embed-v1
    13:02:20 sentence_transformers.SentenceTransformer WARNING   You try to use a model that was created with version 4.1.0, however, your version is 3.4.1. This might cause unexpected behavior or errors. In that case, try to update to the latest version.

    Batches: 100%|██████████| 1/1 [00:00<00:00, 17.15it/s]
    Batches: 100%|██████████| 1/1 [00:00<00:00, 21.23it/s]
    Batches: 100%|██████████| 1/1 [00:00<00:00, 21.71it/s]

    'private_cache:2831a0659fb888e203cd9fedb9f65681bfa55e4977c092ed1bf87d42d2655081'

[code example]

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

    found 1 entry
    The number on file is 123-555-0000

[code example]

Multiple `filterable_fields` can be defined on a cache, and complex filter expressions can be constructed to filter on these fields, as well as the default fields already present.

[code example]

    13:02:21 sentence_transformers.SentenceTransformer INFO   Use pytorch device_name: mps
    13:02:21 sentence_transformers.SentenceTransformer INFO   Load pretrained SentenceTransformer: redis/langcache-embed-v1
    13:02:21 sentence_transformers.SentenceTransformer WARNING   You try to use a model that was created with version 4.1.0, however, your version is 3.4.1. This might cause unexpected behavior or errors. In that case, try to update to the latest version.

    Batches: 100%|██████████| 1/1 [00:00<00:00, 21.08it/s]
    Batches: 100%|██████████| 1/1 [00:00<00:00,  8.74it/s]
    Batches: 100%|██████████| 1/1 [00:00<00:00,  8.01it/s]
    Batches: 100%|██████████| 1/1 [00:00<00:00, 21.70it/s]
    Batches: 100%|██████████| 1/1 [00:00<00:00, 16.74it/s]

    'account_data:944f89729b09ca46b99923d223db45e0bccf584cfd53fcaf87d2a58f072582d3'

[code example]

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

    found 1 entry
    Your most recent transaction was for $350

[code example]

import os
import getpass
import time
import numpy as np

from openai import OpenAI

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

api_key = os.getenv("OPENAI_API_KEY") or getpass.getpass("Enter your OpenAI API key: ")

client = OpenAI(api_key=api_key)

def ask_openai(question: str) -> str:
    response = client.completions.create(
      model="gpt-3.5-turbo-instruct",
      prompt=question,
      max_tokens=200
    )
    return response.choices[0].text.strip()

# Test
print(ask_openai("What is the capital of France?"))

import warnings
warnings.filterwarnings('ignore')

from redisvl.extensions.cache.llm import SemanticCache
from redisvl.utils.vectorize import HFTextVectorizer

llmcache = SemanticCache(
    name="llmcache",                                          # underlying search index name
    redis_url="redis://localhost:6379",                       # redis connection url string
    distance_threshold=0.1,                                   # semantic cache distance threshold
    vectorizer=HFTextVectorizer("redis/langcache-embed-v1"),  # embedding model
)

# look at the index specification created for the semantic cache lookup
!rvl index info -i llmcache

question = "What is the capital of France?"

# Check the semantic cache -- should be empty
if response := llmcache.check(prompt=question):
    print(response)
else:
    print("Empty cache")

# Cache the question, answer, and arbitrary metadata
llmcache.store(
    prompt=question,
    response="Paris",
    metadata={"city": "Paris", "country": "france"}
)

# Check the cache again
if response := llmcache.check(prompt=question, return_fields=["prompt", "response", "metadata"]):
    print(response)
else:
    print("Empty cache")

# Check for a semantically similar result
question = "What actually is the capital of France?"
llmcache.check(prompt=question)[0]['response']

# Widen the semantic distance threshold
llmcache.set_threshold(0.5)

# Really try to trick it by asking around the point
# But is able to slip just under our new threshold
question = "What is the capital city of the country in Europe that also has a city named Nice?"
llmcache.check(prompt=question)[0]['response']

# Invalidate the cache completely by clearing it out
llmcache.clear()

# Should be empty now
llmcache.check(prompt=question)

llmcache.store("This is a TTL test", "This is a TTL test response")

time.sleep(6)

# confirm that the cache has cleared by now on it's own
result = llmcache.check("This is a TTL test")

print(result)

# Reset the TTL to null (long lived data)
llmcache.set_ttl()

def answer_question(question: str) -> str:
    """Helper function to answer a simple question using OpenAI with a wrapper
    check for the answer in the semantic cache first.

    Args:
        question (str): User input question.

    Returns:
        str: Response.
    """
    results = llmcache.check(prompt=question)
    if results:
        return results[0]["response"]
    else:
        answer = ask_openai(question)
        return answer

start = time.time()
# asking a question -- openai response time
question = "What was the name of the first US President?"
answer = answer_question(question)
end = time.time()

print(f"Without caching, a call to openAI to answer this simple question took {end-start} seconds.")

# add the entry to our LLM cache
llmcache.store(prompt=question, response="George Washington")

# Calculate the avg latency for caching over LLM usage
times = []

for _ in range(10):
    cached_start = time.time()
    cached_answer = answer_question(question)
    cached_end = time.time()
    times.append(cached_end-cached_start)

avg_time_with_cache = np.mean(times)
print(f"Avg time taken with LLM cache enabled: {avg_time_with_cache}")
print(f"Percentage of time saved: {round(((end - start) - avg_time_with_cache) / (end - start) * 100, 2)}%")

# check the stats of the index
!rvl stats -i llmcache

# Clear the cache AND delete the underlying index
llmcache.delete()

private_cache = SemanticCache(
    name="private_cache",
    filterable_fields=[{"name": "user_id", "type": "tag"}]
)

private_cache.store(
    prompt="What is the phone number linked to my account?",
    response="The number on file is 123-555-0000",
    filters={"user_id": "abc"},
)

private_cache.store(
    prompt="What's the phone number linked in my account?",
    response="The number on file is 123-555-1111",
    filters={"user_id": "def"},
)

from redisvl.query.filter import Tag

# define user id filter
user_id_filter = Tag("user_id") == "abc"

response = private_cache.check(
    prompt="What is the phone number linked to my account?",
    filter_expression=user_id_filter,
    num_results=2
)

print(f"found {len(response)} entry \n{response[0]['response']}")

complex_cache = SemanticCache(
    name='account_data',
    filterable_fields=[
        {"name": "user_id", "type": "tag"},
        {"name": "account_type", "type": "tag"},
        {"name": "account_balance", "type": "numeric"},
        {"name": "transaction_amount", "type": "numeric"}
    ]
)
complex_cache.store(
    prompt="what is my most recent checking account transaction under $100?",
    response="Your most recent transaction was for $75",
    filters={"user_id": "abc", "account_type": "checking", "transaction_amount": 75},
)
complex_cache.store(
    prompt="what is my most recent savings account transaction?",
    response="Your most recent deposit was for $300",
    filters={"user_id": "abc", "account_type": "savings", "transaction_amount": 300},
)
complex_cache.store(
    prompt="what is my most recent checking account transaction over $200?",
    response="Your most recent transaction was for $350",
    filters={"user_id": "abc", "account_type": "checking", "transaction_amount": 350},
)
complex_cache.store(
    prompt="what is my checking account balance?",
    response="Your current checking account is $1850",
    filters={"user_id": "abc", "account_type": "checking"},
)

from redisvl.query.filter import Num

value_filter = Num("transaction_amount") > 100
account_filter = Tag("account_type") == "checking"
complex_filter = value_filter & account_filter

# check for checking account transactions over $100
complex_cache.set_threshold(0.3)
response = complex_cache.check(
    prompt="what is my most recent checking account transaction?",
    filter_expression=complex_filter,
    num_results=5
)
print(f'found {len(response)} entry')
print(response[0]["response"])


LLM Message History

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 noticeable when asking simple questions, it becomes a hindrance 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 message history.

[code example]

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. Message history offer a convenience 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 message history

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 `SemanticMessageHistory`, which uses vector similarity search to return only semantically relevant sections of the conversation.

[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:39 sentence_transformers.SentenceTransformer INFO   Use pytorch device_name: mps
    13:03:39 sentence_transformers.SentenceTransformer INFO   Load pretrained SentenceTransformer: sentence-transformers/all-mpnet-base-v2

    Batches: 100%|██████████| 1/1 [00:00<00:00,  6.59it/s]
    Batches: 100%|██████████| 1/1 [00:00<00:00, 10.33it/s]
    Batches: 100%|██████████| 1/1 [00:00<00:00,  9.91it/s]
    Batches: 100%|██████████| 1/1 [00:00<00:00, 12.52it/s]
    Batches: 100%|██████████| 1/1 [00:00<00:00, 57.92it/s]
    Batches: 100%|██████████| 1/1 [00:00<00:00, 60.45it/s]
    Batches: 100%|██████████| 1/1 [00:00<00:00, 13.38it/s]
    Batches: 100%|██████████| 1/1 [00:00<00:00, 13.65it/s]
    Batches: 100%|██████████| 1/1 [00:00<00:00, 62.33it/s]

[code example]

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

    {'role': 'user', 'content': 'what is the size of England compared to Portugal?'}

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

[code example]

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

    {'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.'}
    {'role': 'user', 'content': 'And what is the capital of Spain?'}

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]

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

    {'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.message_history import MessageHistory

chat_history = MessageHistory(name='student tutor')

chat_history.add_message({"role":"system", "content":"You are a helpful geography tutor, giving simple and short answers to questions about European countries."})
chat_history.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_history.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_history.store(prompt, response)

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

chat_history.add_message({"role":"system", "content":"You are a helpful algebra tutor, giving simple answers to math problems."}, session_tag='student two')
chat_history.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_history.get_recent(session_tag='student two'):
    print(math_message)

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

from redisvl.extensions.message_history import SemanticMessageHistory
semantic_history = SemanticMessageHistory(name='tutor')

semantic_history.add_messages(chat_history.get_recent(top_k=8))

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

semantic_history.set_distance_threshold(0.7)

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

semantic_history.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_history.get_recent(top_k=1, raw=True)
bad_key = context[0]['entry_id']
semantic_history.drop(bad_key)

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