Skip to content

Commit d5ab17b

Browse files
DOC-5150 added JSON examples to vector index/query page for JavaScript
1 parent 05fc72b commit d5ab17b

File tree

1 file changed

+153
-100
lines changed

1 file changed

+153
-100
lines changed

content/develop/clients/nodejs/vecsearch.md

Lines changed: 153 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -16,57 +16,54 @@ weight: 3
1616
---
1717

1818
[Redis Query Engine]({{< relref "/develop/interact/search-and-query" >}})
19-
lets you index vector fields in [hash]({{< relref "/develop/data-types/hashes" >}})
19+
enables you to index vector fields in [hash]({{< relref "/develop/data-types/hashes" >}})
2020
or [JSON]({{< relref "/develop/data-types/json" >}}) objects (see the
2121
[Vectors]({{< relref "/develop/interact/search-and-query/advanced-concepts/vectors" >}})
2222
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
23+
24+
Vector fields can store *text embeddings*, which are AI-generated vector
25+
representations of text content. The
2526
[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.
27+
between two embeddings measures their semantic similarity. When you compare the
28+
similarity of a query embedding with stored embeddings, Redis can retrieve documents
29+
that closely match the query's meaning.
3030

3131
In the example below, we use the
3232
[`@xenova/transformers`](https://www.npmjs.com/package/@xenova/transformers)
3333
library to generate vector embeddings to store and index with
34-
Redis Query Engine.
34+
Redis Query Engine. The code is first demonstrated for hash documents with a
35+
separate section to explain the
36+
[differences with JSON documents](#differences-with-json-documents).
3537

3638
## Initialize
3739

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:
40+
Install the required dependencies:
41+
42+
1. Install [`node-redis`]({{< relref "/develop/clients/nodejs" >}}) if you haven't already.
43+
2. Install `@xenova/transformers`:
4144

4245
```bash
4346
npm install @xenova/transformers
4447
```
4548

46-
In a new JavaScript source file, start by importing the required classes:
49+
In your JavaScript source file, import the required classes:
4750

4851
```js
4952
import * as transformers from '@xenova/transformers';
5053
import {VectorAlgorithms, createClient, SchemaFieldTypes} from 'redis';
5154
```
5255

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+
The `@xenova/transformers` module handles embedding models. This example uses the
5657
[`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
58+
model, which:
59+
- Generates 768-dimensional vectors
60+
- Truncates input to 128 tokens
61+
- Uses word piece tokenization (see [Word piece tokenization](https://huggingface.co/learn/nlp-course/en/chapter6/6)
62+
at the [Hugging Face](https://huggingface.co/) docs for details)
63+
64+
The `pipe` function generates embeddings. The `pipeOptions` object specifies how to generate sentence embeddings from token embeddings (see the
6865
[`all-distilroberta-v1`](https://huggingface.co/sentence-transformers/all-distilroberta-v1)
69-
docs for more information).
66+
documentation for details):
7067

7168
```js
7269
let pipe = await transformers.pipeline(
@@ -81,38 +78,35 @@ const pipeOptions = {
8178

8279
## Create the index
8380

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.)
81+
First, connect to Redis and remove any existing index named `vector_idx`:
8882

8983
```js
9084
const client = createClient({url: 'redis://localhost:6379'});
91-
9285
await client.connect();
9386

94-
try { await client.ft.dropIndex('vector_idx'); } catch {}
87+
try {
88+
await client.ft.dropIndex('vector_idx');
89+
} catch (e) {
90+
// Index doesn't exist, which is fine
91+
}
9592
```
9693

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.
94+
Next, create the index with the following schema:
95+
- `content`: Text field for the content to index
96+
- `genre`: Tag field representing the text's genre
97+
- `embedding`: Vector field with:
98+
- HNSW indexing
99+
- L2 distance metric
100+
- Float32 values
101+
- 768 dimensions (matching the embedding model)
108102

109103
```js
110104
await client.ft.create('vector_idx', {
111105
'content': {
112106
type: SchemaFieldTypes.TEXT,
113107
},
114108
'genre': {
115-
type:SchemaFieldTypes.TAG,
109+
type: SchemaFieldTypes.TAG,
116110
},
117111
'embedding': {
118112
type: SchemaFieldTypes.VECTOR,
@@ -121,50 +115,37 @@ await client.ft.create('vector_idx', {
121115
DISTANCE_METRIC: 'L2',
122116
DIM: 768,
123117
}
124-
},{
118+
}, {
125119
ON: 'HASH',
126120
PREFIX: 'doc:'
127121
});
128122
```
129123

130124
## Add data
131125

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.
126+
Add data objects to the index using `hSet()`. The index automatically processes objects with the `doc:` prefix.
127+
128+
For each document:
129+
1. Generate an embedding using the `pipe()` function and `pipeOptions`
130+
2. Convert the embedding to a binary string using `Buffer.from()`
131+
3. Store the document with `hSet()`
132+
133+
Use `Promise.all()` to batch the commands and reduce network round trips:
153134

154135
```js
155136
const sentence1 = 'That is a very happy person';
156137
const doc1 = {
157138
'content': sentence1,
158-
'genre':'persons',
159-
'embedding':Buffer.from(
139+
'genre': 'persons',
140+
'embedding': Buffer.from(
160141
(await pipe(sentence1, pipeOptions)).data.buffer
161142
),
162143
};
163144

164145
const sentence2 = 'That is a happy dog';
165146
const doc2 = {
166147
'content': sentence2,
167-
'genre':'pets',
148+
'genre': 'pets',
168149
'embedding': Buffer.from(
169150
(await pipe(sentence2, pipeOptions)).data.buffer
170151
)
@@ -173,7 +154,7 @@ const doc2 = {
173154
const sentence3 = 'Today is a sunny day';
174155
const doc3 = {
175156
'content': sentence3,
176-
'genre':'weather',
157+
'genre': 'weather',
177158
'embedding': Buffer.from(
178159
(await pipe(sentence3, pipeOptions)).data.buffer
179160
)
@@ -188,24 +169,14 @@ await Promise.all([
188169

189170
## Run a query
190171

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.
172+
To query the index:
173+
1. Generate an embedding for your query text
174+
2. Pass the embedding as a parameter to the search
175+
3. Redis calculates vector distances and ranks results
208176

177+
The query returns an array of document objects. Each object contains:
178+
- `id`: The document's key
179+
- `value`: An object with fields specified in the `RETURN` option
209180

210181
```js
211182
const similar = await client.ft.search(
@@ -229,27 +200,109 @@ for (const doc of similar.documents) {
229200
await client.quit();
230201
```
231202

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:
203+
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:
235204

236205
```
237206
doc:1: 'That is a very happy person', Score: 0.127055495977
238207
doc:2: 'That is a happy dog', Score: 0.836842417717
239208
doc:3: 'Today is a sunny day', Score: 1.50889515877
240209
```
241210

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"*.
211+
## Differences with JSON documents
212+
213+
JSON documents support richer data modeling with nested fields. Key differences from hash documents:
214+
215+
1. Use paths in the schema to identify fields
216+
2. Declare aliases for paths using the `AS` option
217+
3. Set `ON` to `JSON` when creating the index
218+
4. Use arrays instead of binary strings for vectors
219+
5. Use `json.set()` instead of `hSet()`
220+
221+
Create the index with path aliases:
222+
223+
```js
224+
await client.ft.create('vector_json_idx', {
225+
'$.content': {
226+
type: SchemaFieldTypes.TEXT,
227+
AS: 'content',
228+
},
229+
'$.genre': {
230+
type: SchemaFieldTypes.TAG,
231+
AS: 'genre',
232+
},
233+
'$.embedding': {
234+
type: SchemaFieldTypes.VECTOR,
235+
TYPE: 'FLOAT32',
236+
ALGORITHM: VectorAlgorithms.HNSW,
237+
DISTANCE_METRIC: 'L2',
238+
DIM: 768,
239+
AS: 'embedding',
240+
}
241+
}, {
242+
ON: 'JSON',
243+
PREFIX: 'jdoc:'
244+
});
245+
```
246+
247+
Add data using `json.set()`. Convert the `Float32Array` to a standard JavaScript array using the spread operator:
248+
249+
```js
250+
const jSentence1 = 'That is a very happy person';
251+
const jdoc1 = {
252+
'content': jSentence1,
253+
'genre': 'persons',
254+
'embedding': [...(await pipe(jSentence1, pipeOptions)).data],
255+
};
256+
257+
const jSentence2 = 'That is a happy dog';
258+
const jdoc2 = {
259+
'content': jSentence2,
260+
'genre': 'pets',
261+
'embedding': [...(await pipe(jSentence2, pipeOptions)).data],
262+
};
263+
264+
const jSentence3 = 'Today is a sunny day';
265+
const jdoc3 = {
266+
'content': jSentence3,
267+
'genre': 'weather',
268+
'embedding': [...(await pipe(jSentence3, pipeOptions)).data],
269+
};
270+
271+
await Promise.all([
272+
client.json.set('jdoc:1', '$', jdoc1),
273+
client.json.set('jdoc:2', '$', jdoc2),
274+
client.json.set('jdoc:3', '$', jdoc3)
275+
]);
276+
```
277+
278+
Query JSON documents using the same syntax, but note that the vector parameter must still be a binary string:
279+
280+
```js
281+
const jsons = await client.ft.search(
282+
'vector_json_idx',
283+
'*=>[KNN 3 @embedding $B AS score]',
284+
{
285+
"PARAMS": {
286+
B: Buffer.from(
287+
(await pipe('That is a happy person', pipeOptions)).data.buffer
288+
),
289+
},
290+
'RETURN': ['score', 'content'],
291+
'DIALECT': '2'
292+
},
293+
);
294+
```
295+
296+
The results are identical to the hash document query, except for the `jdoc:` prefix:
297+
298+
```
299+
jdoc:1: 'That is a very happy person', Score: 0.127055495977
300+
jdoc:2: 'That is a happy dog', Score: 0.836842417717
301+
jdoc:3: 'Today is a sunny day', Score: 1.50889515877
302+
```
249303

250304
## Learn more
251305

252306
See
253307
[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.
308+
for more information about indexing options, distance metrics, and query format.

0 commit comments

Comments
 (0)