Skip to content

Commit 46848ad

Browse files
Merge pull request #1217 from redis/DOC-4838-node-vec-example
DOC-4838 added node-redis vector query example
2 parents 4e4542b + 5604b6d commit 46848ad

File tree

1 file changed

+255
-0
lines changed

1 file changed

+255
-0
lines changed
Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
---
2+
categories:
3+
- docs
4+
- develop
5+
- stack
6+
- oss
7+
- rs
8+
- rc
9+
- oss
10+
- kubernetes
11+
- clients
12+
description: Learn how to index and query vector embeddings with Redis
13+
linkTitle: Index and query vectors
14+
title: Index and query vectors
15+
weight: 4
16+
---
17+
18+
[Redis Query Engine]({{< relref "/develop/interact/search-and-query" >}})
19+
lets you index vector fields in [hash]({{< relref "/develop/data-types/hashes" >}})
20+
or [JSON]({{< relref "/develop/data-types/json" >}}) objects (see the
21+
[Vectors]({{< relref "/develop/interact/search-and-query/advanced-concepts/vectors" >}})
22+
reference page for more information).
23+
Among other things, vector fields can store *text embeddings*, which are AI-generated vector
24+
representations of the semantic information in pieces of text. The
25+
[vector distance]({{< relref "/develop/interact/search-and-query/advanced-concepts/vectors#distance-metrics" >}})
26+
between two embeddings indicates how similar they are semantically. By comparing the
27+
similarity of an embedding generated from some query text with embeddings stored in hash
28+
or JSON fields, Redis can retrieve documents that closely match the query in terms
29+
of their meaning.
30+
31+
In the example below, we use the
32+
[`@xenova/transformers`](https://www.npmjs.com/package/@xenova/transformers)
33+
library to generate vector embeddings to store and index with
34+
Redis Query Engine.
35+
36+
## Initialize
37+
38+
Install [`node-redis`]({{< relref "/develop/clients/nodejs" >}}) if you
39+
have not already done so. Also, install `@xenova/transformers` with the
40+
following command:
41+
42+
```bash
43+
npm install @xenova/transformers
44+
```
45+
46+
In a new JavaScript source file, start by importing the required classes:
47+
48+
```js
49+
import * as transformers from '@xenova/transformers';
50+
import {VectorAlgorithms, createClient, SchemaFieldTypes} from 'redis';
51+
```
52+
53+
The first of these imports is the `@xenova/transformers` module, which handles
54+
the embedding models.
55+
Here, we use an instance of the
56+
[`all-distilroberta-v1`](https://huggingface.co/sentence-transformers/all-distilroberta-v1)
57+
model for the embeddings. This model generates vectors with 768 dimensions, regardless
58+
of the length of the input text, but note that the input is truncated to 128
59+
tokens (see
60+
[Word piece tokenization](https://huggingface.co/learn/nlp-course/en/chapter6/6)
61+
at the [Hugging Face](https://huggingface.co/) docs to learn more about the way tokens
62+
are related to the original text).
63+
64+
The `pipe` value obtained here is a function that we can call to generate the
65+
embeddings. We also need an object to pass some options for the `pipe()` function
66+
call. These specify the way the sentence embedding is generated from individual
67+
token embeddings (see the
68+
[`all-distilroberta-v1`](https://huggingface.co/sentence-transformers/all-distilroberta-v1)
69+
docs for more information).
70+
71+
```js
72+
let pipe = await transformers.pipeline(
73+
'feature-extraction', 'Xenova/all-distilroberta-v1'
74+
);
75+
76+
const pipeOptions = {
77+
pooling: 'mean',
78+
normalize: true,
79+
};
80+
```
81+
82+
## Create the index
83+
84+
Connect to Redis and delete any index previously created with the
85+
name `vector_idx`. (The `dropIndex()` call throws an exception if
86+
the index doesn't already exist, which is why you need the
87+
`try...catch` block.)
88+
89+
```js
90+
const client = createClient({url: 'redis://localhost:6379'});
91+
92+
await client.connect();
93+
94+
try { await client.ft.dropIndex('vector_idx'); } catch {}
95+
```
96+
97+
Next, create the index.
98+
The schema in the example below specifies hash objects for storage and includes
99+
three fields: the text content to index, a
100+
[tag]({{< relref "/develop/interact/search-and-query/advanced-concepts/tags" >}})
101+
field to represent the "genre" of the text, and the embedding vector generated from
102+
the original text content. The `embedding` field specifies
103+
[HNSW]({{< relref "/develop/interact/search-and-query/advanced-concepts/vectors#hnsw-index" >}})
104+
indexing, the
105+
[L2]({{< relref "/develop/interact/search-and-query/advanced-concepts/vectors#distance-metrics" >}})
106+
vector distance metric, `Float32` values to represent the vector's components,
107+
and 768 dimensions, as required by the `all-distilroberta-v1` embedding model.
108+
109+
```js
110+
await client.ft.create('vector_idx', {
111+
'content': {
112+
type: SchemaFieldTypes.TEXT,
113+
},
114+
'genre': {
115+
type:SchemaFieldTypes.TAG,
116+
},
117+
'embedding': {
118+
type: SchemaFieldTypes.VECTOR,
119+
TYPE: 'FLOAT32',
120+
ALGORITHM: VectorAlgorithms.HNSW,
121+
DISTANCE_METRIC: 'L2',
122+
DIM: 768,
123+
}
124+
},{
125+
ON: 'HASH',
126+
PREFIX: 'doc:'
127+
});
128+
```
129+
130+
## Add data
131+
132+
You can now supply the data objects, which will be indexed automatically
133+
when you add them with [`hSet()`]({{< relref "/commands/hset" >}}), as long as
134+
you use the `doc:` prefix specified in the index definition.
135+
136+
Use the `pipe()` method and the `pipeOptions` object that we created earlier to
137+
generate the embedding that represents the `content` field.
138+
The object returned by `pipe()` includes a `data` attribute, which is a
139+
[`Float32Array`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Float32Array)
140+
that contains the embedding data. If you are indexing hash objects, as
141+
we are here, then you must also call
142+
[`Buffer.from()`](https://nodejs.org/api/buffer.html#static-method-bufferfromarraybuffer-byteoffset-length)
143+
on this array's `buffer` value to convert the `Float32Array`
144+
to a binary string. If you are indexing JSON objects, you can just
145+
use the `Float32Array` directly to represent the embedding.
146+
147+
Make the `hSet()` calls within a
148+
[`Promise.all()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all)
149+
call to create a Redis [pipeline]({{< relref "/develop/use/pipelining" >}})
150+
(not to be confused with the `@xenova/transformers` pipeline).
151+
This combines the commands together into a batch to reduce network
152+
round trip time.
153+
154+
```js
155+
const sentence1 = 'That is a very happy person';
156+
const doc1 = {
157+
'content': sentence1,
158+
'genre':'persons',
159+
'embedding':Buffer.from(
160+
(await pipe(sentence1, pipeOptions)).data.buffer
161+
),
162+
};
163+
164+
const sentence2 = 'That is a happy dog';
165+
const doc2 = {
166+
'content': sentence2,
167+
'genre':'pets',
168+
'embedding': Buffer.from(
169+
(await pipe(sentence2, pipeOptions)).data.buffer
170+
)
171+
};
172+
173+
const sentence3 = 'Today is a sunny day';
174+
const doc3 = {
175+
'content': sentence3,
176+
'genre':'weather',
177+
'embedding': Buffer.from(
178+
(await pipe(sentence3, pipeOptions)).data.buffer
179+
)
180+
};
181+
182+
await Promise.all([
183+
client.hSet('doc:1', doc1),
184+
client.hSet('doc:2', doc2),
185+
client.hSet('doc:3', doc3)
186+
]);
187+
```
188+
189+
## Run a query
190+
191+
After you have created the index and added the data, you are ready to run a query.
192+
To do this, you must create another embedding vector from your chosen query
193+
text. Redis calculates the vector distance between the query vector and each
194+
embedding vector in the index and then ranks the results in order of this
195+
distance value.
196+
197+
The code below creates the query embedding using `pipe()`, as with
198+
the indexing, and passes it as a parameter during execution
199+
(see
200+
[Vector search]({{< relref "/develop/interact/search-and-query/query/vector-search" >}})
201+
for more information about using query parameters with embeddings).
202+
203+
The query returns an array of objects representing the documents
204+
that were found (which are hash objects here). The `id` attribute
205+
contains the document's key. The `value` attribute contains an object
206+
with a key-value entry corresponding to each index field specified in the
207+
`RETURN` option of the query.
208+
209+
210+
```js
211+
const similar = await client.ft.search(
212+
'vector_idx',
213+
'*=>[KNN 3 @embedding $B AS score]',
214+
{
215+
'PARAMS': {
216+
B: Buffer.from(
217+
(await pipe('That is a happy person', pipeOptions)).data.buffer
218+
),
219+
},
220+
'RETURN': ['score', 'content'],
221+
'DIALECT': '2'
222+
},
223+
);
224+
225+
for (const doc of similar.documents) {
226+
console.log(`${doc.id}: '${doc.value.content}', Score: ${doc.value.score}`);
227+
}
228+
229+
await client.quit();
230+
```
231+
232+
The code is now ready to run, but note that it may take a while to download the
233+
`all-distilroberta-v1` model data the first time you run it. The
234+
code outputs the following results:
235+
236+
```
237+
doc:1: 'That is a very happy person', Score: 0.127055495977
238+
doc:2: 'That is a happy dog', Score: 0.836842417717
239+
doc:3: 'Today is a sunny day', Score: 1.50889515877
240+
```
241+
242+
The results are ordered according to the value of the `score`
243+
field, which represents the vector distance here. The lowest distance indicates
244+
the greatest similarity to the query.
245+
As you would expect, the result for `doc:1` with the content text
246+
*"That is a very happy person"*
247+
is the result that is most similar in meaning to the query text
248+
*"That is a happy person"*.
249+
250+
## Learn more
251+
252+
See
253+
[Vector search]({{< relref "/develop/interact/search-and-query/query/vector-search" >}})
254+
for more information about the indexing options, distance metrics, and query format
255+
for vectors.

0 commit comments

Comments
 (0)