Vector Similarity#
Vectors (also called “Embeddings”), represent an AI model’s impression (or understanding) of a piece of unstructured data like text, images, audio, videos, etc. Vector Similarity Search (VSS) is the process of finding vectors in the vector database that are similar to a given query vector. Popular VSS uses include recommendation systems, image and video search, document retrieval, and question answering.
Index Creation#
Before doing vector search, first define the schema and create an index.
[1]:
import valkey
from valkey.commands.search.field import TagField, VectorField
from valkey.commands.search.indexDefinition import IndexDefinition, IndexType
from valkey.commands.search.query import Query
r = valkey.Valkey(host="localhost", port=6379)
INDEX_NAME = "index" # Vector Index Name
DOC_PREFIX = "doc:" # RediSearch Key Prefix for the Index
def create_index(vector_dimensions: int):
try:
# check to see if index exists
r.ft(INDEX_NAME).info()
print("Index already exists!")
except:
# schema
schema = (
TagField("tag"), # Tag Field Name
VectorField("vector", # Vector Field Name
"FLAT", { # Vector Index Type: FLAT or HNSW
"TYPE": "FLOAT32", # FLOAT32 or FLOAT64
"DIM": vector_dimensions, # Number of Vector Dimensions
"DISTANCE_METRIC": "COSINE", # Vector Search Distance Metric
}
),
)
# index Definition
definition = IndexDefinition(prefix=[DOC_PREFIX], index_type=IndexType.HASH)
# create Index
r.ft(INDEX_NAME).create_index(fields=schema, definition=definition)
We’ll start by working with vectors that have 1536 dimensions.
[2]:
# define vector dimensions
VECTOR_DIMENSIONS = 1536
# create the index
create_index(vector_dimensions=VECTOR_DIMENSIONS)
Adding Vectors to Valkey#
Next, we add vectors (dummy data) to Valkey using hset
. The search index listens to keyspace notifications and will include any written HASH objects prefixed by DOC_PREFIX
.
[ ]:
%pip install numpy
[3]:
import numpy as np
[4]:
# instantiate a valkey pipeline
pipe = r.pipeline()
# define some dummy data
objects = [
{"name": "a", "tag": "foo"},
{"name": "b", "tag": "foo"},
{"name": "c", "tag": "bar"},
]
# write data
for obj in objects:
# define key
key = f"doc:{obj['name']}"
# create a random "dummy" vector
obj["vector"] = np.random.rand(VECTOR_DIMENSIONS).astype(np.float32).tobytes()
# HSET
pipe.hset(key, mapping=obj)
res = pipe.execute()
Searching#
You can use VSS queries with the .ft(...).search(...)
query command. To use a VSS query, you must specify the option .dialect(2)
.
There are two supported types of vector queries in Valkey: KNN
and Range
. Hybrid
queries can work in both settings and combine elements of traditional search and VSS.
KNN Queries#
KNN queries are for finding the topK most similar vectors given a query vector.
[5]:
query = (
Query("*=>[KNN 2 @vector $vec as score]")
.sort_by("score")
.return_fields("id", "score")
.paging(0, 2)
.dialect(2)
)
query_params = {
"vec": np.random.rand(VECTOR_DIMENSIONS).astype(np.float32).tobytes()
}
r.ft(INDEX_NAME).search(query, query_params).docs
[5]:
[Document {'id': 'doc:b', 'payload': None, 'score': '0.2376562953'},
Document {'id': 'doc:c', 'payload': None, 'score': '0.240063905716'}]
Range Queries#
Range queries provide a way to filter results by the distance between a vector field in Valkey and a query vector based on some pre-defined threshold (radius).
[6]:
query = (
Query("@vector:[VECTOR_RANGE $radius $vec]=>{$YIELD_DISTANCE_AS: score}")
.sort_by("score")
.return_fields("id", "score")
.paging(0, 3)
.dialect(2)
)
# Find all vectors within 0.8 of the query vector
query_params = {
"radius": 0.8,
"vec": np.random.rand(VECTOR_DIMENSIONS).astype(np.float32).tobytes()
}
r.ft(INDEX_NAME).search(query, query_params).docs
[6]:
[Document {'id': 'doc:a', 'payload': None, 'score': '0.243115246296'},
Document {'id': 'doc:c', 'payload': None, 'score': '0.24981123209'},
Document {'id': 'doc:b', 'payload': None, 'score': '0.251443207264'}]
See additional Range Query examples in this Jupyter notebook.
Hybrid Queries#
Hybrid queries contain both traditional filters (numeric, tags, text) and VSS in one single Valkey command.
[7]:
query = (
Query("(@tag:{ foo })=>[KNN 2 @vector $vec as score]")
.sort_by("score")
.return_fields("id", "tag", "score")
.paging(0, 2)
.dialect(2)
)
query_params = {
"vec": np.random.rand(VECTOR_DIMENSIONS).astype(np.float32).tobytes()
}
r.ft(INDEX_NAME).search(query, query_params).docs
[7]:
[Document {'id': 'doc:b', 'payload': None, 'score': '0.24422544241', 'tag': 'foo'},
Document {'id': 'doc:a', 'payload': None, 'score': '0.259926855564', 'tag': 'foo'}]
See additional Hybrid Query examples in this Jupyter notebook.
Vector Creation and Storage Examples#
The above examples use dummy data as vectors. However, in reality, most use cases leverage production-grade AI models for creating embeddings. Below we will take some sample text data, pass it to the OpenAI and Cohere API’s respectively, and then write them to Valkey.
[8]:
texts = [
"Today is a really great day!",
"The dog next door barks really loudly.",
"My cat escaped and got out before I could close the door.",
"It's supposed to rain and thunder tomorrow."
]
OpenAI Embeddings#
Before working with OpenAI Embeddings, we clean up our existing search index and create a new one.
[9]:
# delete index
r.ft(INDEX_NAME).dropindex(delete_documents=True)
# make a new one
create_index(vector_dimensions=VECTOR_DIMENSIONS)
[ ]:
%pip install openai
[10]:
import openai
# set your OpenAI API key - get one at https://platform.openai.com
openai.api_key = "YOUR OPENAI API KEY"
[11]:
# Create Embeddings with OpenAI text-embedding-ada-002
# https://openai.com/blog/new-and-improved-embedding-model
response = openai.Embedding.create(input=texts, engine="text-embedding-ada-002")
embeddings = np.array([r["embedding"] for r in response["data"]], dtype=np.float32)
# Write to Valkey
pipe = r.pipeline()
for i, embedding in enumerate(embeddings):
pipe.hset(f"doc:{i}", mapping = {
"vector": embedding.tobytes(),
"content": texts[i],
"tag": "openai"
})
res = pipe.execute()
[12]:
embeddings
[12]:
array([[ 0.00509819, 0.0010873 , -0.00228475, ..., -0.00457579,
0.01329307, -0.03167175],
[-0.00357223, -0.00550784, -0.01314328, ..., -0.02915693,
0.01470436, -0.01367203],
[-0.01284631, 0.0034875 , -0.01719686, ..., -0.01537451,
0.01953256, -0.05048691],
[-0.01145045, -0.00785481, 0.00206323, ..., -0.02070181,
-0.01629098, -0.00300795]], dtype=float32)
Search with OpenAI Embeddings#
Now that we’ve created embeddings with OpenAI, we can also perform a search to find relevant documents to some input text.
[13]:
text = "animals"
# create query embedding
response = openai.Embedding.create(input=[text], engine="text-embedding-ada-002")
query_embedding = np.array([r["embedding"] for r in response["data"]], dtype=np.float32)[0]
query_embedding
[13]:
array([ 0.00062901, -0.0070723 , -0.00148926, ..., -0.01904645,
-0.00436092, -0.01117944], dtype=float32)
[14]:
# query for similar documents that have the openai tag
query = (
Query("(@tag:{ openai })=>[KNN 2 @vector $vec as score]")
.sort_by("score")
.return_fields("content", "tag", "score")
.paging(0, 2)
.dialect(2)
)
query_params = {"vec": query_embedding.tobytes()}
r.ft(INDEX_NAME).search(query, query_params).docs
# the two pieces of content related to animals are returned
[14]:
[Document {'id': 'doc:1', 'payload': None, 'score': '0.214349985123', 'content': 'The dog next door barks really loudly.', 'tag': 'openai'},
Document {'id': 'doc:2', 'payload': None, 'score': '0.237052619457', 'content': 'My cat escaped and got out before I could close the door.', 'tag': 'openai'}]
Cohere Embeddings#
Before working with Cohere Embeddings, we clean up our existing search index and create a new one.
[15]:
# delete index
r.ft(INDEX_NAME).dropindex(delete_documents=True)
# make a new one for cohere embeddings (1024 dimensions)
VECTOR_DIMENSIONS = 1024
create_index(vector_dimensions=VECTOR_DIMENSIONS)
[ ]:
%pip install cohere
[16]:
import cohere
co = cohere.Client("YOUR COHERE API KEY")
[17]:
# Create Embeddings with Cohere
# https://docs.cohere.ai/docs/embeddings
response = co.embed(texts=texts, model="small")
embeddings = np.array(response.embeddings, dtype=np.float32)
# Write to Valkey
for i, embedding in enumerate(embeddings):
r.hset(f"doc:{i}", mapping = {
"vector": embedding.tobytes(),
"content": texts[i],
"tag": "cohere"
})
[18]:
embeddings
[18]:
array([[-0.3034668 , -0.71533203, -0.2836914 , ..., 0.81152344,
1.0253906 , -0.8095703 ],
[-0.02560425, -1.4912109 , 0.24267578, ..., -0.89746094,
0.15625 , -3.203125 ],
[ 0.10125732, 0.7246094 , -0.29516602, ..., -1.9638672 ,
1.6630859 , -0.23291016],
[-2.09375 , 0.8588867 , -0.23352051, ..., -0.01541138,
0.17053223, -3.4042969 ]], dtype=float32)
Search with Cohere Embeddings#
Now that we’ve created embeddings with Cohere, we can also perform a search to find relevant documents to some input text.
[19]:
text = "animals"
# create query embedding
response = co.embed(texts=[text], model="small")
query_embedding = np.array(response.embeddings[0], dtype=np.float32)
query_embedding
[19]:
array([-0.49682617, 1.7070312 , 0.3466797 , ..., 0.58984375,
0.1060791 , -2.9023438 ], dtype=float32)
[20]:
# query for similar documents that have the cohere tag
query = (
Query("(@tag:{ cohere })=>[KNN 2 @vector $vec as score]")
.sort_by("score")
.return_fields("content", "tag", "score")
.paging(0, 2)
.dialect(2)
)
query_params = {"vec": query_embedding.tobytes()}
r.ft(INDEX_NAME).search(query, query_params).docs
# the two pieces of content related to animals are returned
[20]:
[Document {'id': 'doc:1', 'payload': None, 'score': '0.658673524857', 'content': 'The dog next door barks really loudly.', 'tag': 'cohere'},
Document {'id': 'doc:2', 'payload': None, 'score': '0.662699103355', 'content': 'My cat escaped and got out before I could close the door.', 'tag': 'cohere'}]
Find more example apps, tutorials, and projects using Redis Vector Similarity Search in this GitHub organization.