Index and query vectors
Learn how to index and query vector embeddings with Redis
Redis Query Engine lets you index vector fields in hash or JSON objects (see the Vectors reference page for more information).
Vector fields can store text embeddings, which are AI-generated vector representations of text content. The vector distance between two embeddings measures their semantic similarity. When you compare the similarity of a query embedding with stored embeddings, Redis can retrieve documents that closely match the query's meaning.
In the example below, we use the
@xenova/transformers
library to generate vector embeddings to store and index with
Redis Query Engine. The code is first demonstrated for hash documents with a
separate section to explain the
differences with JSON documents.
Initialize
Install the required dependencies:
- Install
node-redis
if you haven't already. - Install
@xenova/transformers
:
npm install @xenova/transformers
In your JavaScript source file, import the required classes:
import * as transformers from '@xenova/transformers';
import {VectorAlgorithms, createClient, SchemaFieldTypes} from 'redis';
The @xenova/transformers
module handles embedding models. This example uses the
all-distilroberta-v1
model, which:
- Generates 768-dimensional vectors
- Truncates input to 128 tokens
- Uses word piece tokenization (see Word piece tokenization at the Hugging Face docs for details)
The pipe
function generates embeddings. The pipeOptions
object specifies how to generate sentence embeddings from token embeddings (see the
all-distilroberta-v1
documentation for details):
let pipe = await transformers.pipeline(
'feature-extraction', 'Xenova/all-distilroberta-v1'
);
const pipeOptions = {
pooling: 'mean',
normalize: true,
};
Create the index
First, connect to Redis and remove any existing index named vector_idx
:
const client = createClient({url: 'redis://localhost:6379'});
await client.connect();
try {
await client.ft.dropIndex('vector_idx');
} catch (e) {
// Index doesn't exist, which is fine
}
Next, create the index with the following schema:
content
: Text field for the content to indexgenre
: Tag field representing the text's genreembedding
: Vector field with:
await client.ft.create('vector_idx', {
'content': {
type: SchemaFieldTypes.TEXT,
},
'genre': {
type: SchemaFieldTypes.TAG,
},
'embedding': {
type: SchemaFieldTypes.VECTOR,
TYPE: 'FLOAT32',
ALGORITHM: VectorAlgorithms.HNSW,
DISTANCE_METRIC: 'L2',
DIM: 768,
}
}, {
ON: 'HASH',
PREFIX: 'doc:'
});
Add data
Add data objects to the index using hSet()
. The index automatically processes objects with the doc:
prefix.
For each document:
- Generate an embedding using the
pipe()
function andpipeOptions
- Convert the embedding to a binary string using
Buffer.from()
- Store the document with
hSet()
Use Promise.all()
to batch the commands and reduce network round trips:
const sentence1 = 'That is a very happy person';
const doc1 = {
'content': sentence1,
'genre': 'persons',
'embedding': Buffer.from(
(await pipe(sentence1, pipeOptions)).data.buffer
),
};
const sentence2 = 'That is a happy dog';
const doc2 = {
'content': sentence2,
'genre': 'pets',
'embedding': Buffer.from(
(await pipe(sentence2, pipeOptions)).data.buffer
)
};
const sentence3 = 'Today is a sunny day';
const doc3 = {
'content': sentence3,
'genre': 'weather',
'embedding': Buffer.from(
(await pipe(sentence3, pipeOptions)).data.buffer
)
};
await Promise.all([
client.hSet('doc:1', doc1),
client.hSet('doc:2', doc2),
client.hSet('doc:3', doc3)
]);
Run a query
To query the index:
- Generate an embedding for your query text
- Pass the embedding as a parameter to the search
- Redis calculates vector distances and ranks results
The query returns an array of document objects. Each object contains:
id
: The document's keyvalue
: An object with fields specified in theRETURN
option
const similar = await client.ft.search(
'vector_idx',
'*=>[KNN 3 @embedding $B AS score]',
{
'PARAMS': {
B: Buffer.from(
(await pipe('That is a happy person', pipeOptions)).data.buffer
),
},
'RETURN': ['score', 'content'],
'DIALECT': '2'
},
);
for (const doc of similar.documents) {
console.log(`${doc.id}: '${doc.value.content}', Score: ${doc.value.score}`);
}
await client.quit();
The first run may take longer as it downloads the model data. The output shows results ordered by score (vector distance), with lower scores indicating greater similarity:
doc:1: 'That is a very happy person', Score: 0.127055495977
doc:2: 'That is a happy dog', Score: 0.836842417717
doc:3: 'Today is a sunny day', Score: 1.50889515877
Differences with JSON documents
JSON documents support richer data modeling with nested fields. Key differences from hash documents:
- Use paths in the schema to identify fields
- Declare aliases for paths using the
AS
option - Set
ON
toJSON
when creating the index - Use arrays instead of binary strings for vectors
- Use
json.set()
instead ofhSet()
Create the index with path aliases:
await client.ft.create('vector_json_idx', {
'$.content': {
type: SchemaFieldTypes.TEXT,
AS: 'content',
},
'$.genre': {
type: SchemaFieldTypes.TAG,
AS: 'genre',
},
'$.embedding': {
type: SchemaFieldTypes.VECTOR,
TYPE: 'FLOAT32',
ALGORITHM: VectorAlgorithms.HNSW,
DISTANCE_METRIC: 'L2',
DIM: 768,
AS: 'embedding',
}
}, {
ON: 'JSON',
PREFIX: 'jdoc:'
});
Add data using json.set()
. Convert the Float32Array
to a standard JavaScript array using the spread operator:
const jSentence1 = 'That is a very happy person';
const jdoc1 = {
'content': jSentence1,
'genre': 'persons',
'embedding': [...(await pipe(jSentence1, pipeOptions)).data],
};
const jSentence2 = 'That is a happy dog';
const jdoc2 = {
'content': jSentence2,
'genre': 'pets',
'embedding': [...(await pipe(jSentence2, pipeOptions)).data],
};
const jSentence3 = 'Today is a sunny day';
const jdoc3 = {
'content': jSentence3,
'genre': 'weather',
'embedding': [...(await pipe(jSentence3, pipeOptions)).data],
};
await Promise.all([
client.json.set('jdoc:1', '$', jdoc1),
client.json.set('jdoc:2', '$', jdoc2),
client.json.set('jdoc:3', '$', jdoc3)
]);
Query JSON documents using the same syntax, but note that the vector parameter must still be a binary string:
const jsons = await client.ft.search(
'vector_json_idx',
'*=>[KNN 3 @embedding $B AS score]',
{
"PARAMS": {
B: Buffer.from(
(await pipe('That is a happy person', pipeOptions)).data.buffer
),
},
'RETURN': ['score', 'content'],
'DIALECT': '2'
},
);
The results are identical to the hash document query, except for the jdoc:
prefix:
jdoc:1: 'That is a very happy person', Score: 0.127055495977
jdoc:2: 'That is a happy dog', Score: 0.836842417717
jdoc:3: 'Today is a sunny day', Score: 1.50889515877
Learn more
See Vector search for more information about indexing options, distance metrics, and query format.