Skip to content

Commit 6e7e920

Browse files
DOC-5557 added Lettuce vector query examples
1 parent dbdd33d commit 6e7e920

File tree

1 file changed

+267
-0
lines changed

1 file changed

+267
-0
lines changed
Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
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: 3
16+
---
17+
18+
[Redis Query Engine]({{< relref "/develop/ai/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/ai/search-and-query/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/ai/search-and-query/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+
The example below uses the [HuggingFace](https://huggingface.co/) model
32+
[`all-MiniLM-L6-v2`](https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2)
33+
to generate the vector embeddings to store and index with Redis Query Engine.
34+
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).
37+
38+
## Initialize
39+
40+
If you are using [Maven](https://maven.apache.org/), add the following
41+
dependencies to your `pom.xml` file:
42+
43+
```xml
44+
<dependency>
45+
<groupId>io.lettuce</groupId>
46+
<artifactId>lettuce-core</artifactId>
47+
<!-- Check for the latest version on Maven Central -->
48+
<version>6.7.1.RELEASE</version>
49+
</dependency>
50+
51+
<dependency>
52+
<groupId>ai.djl.huggingface</groupId>
53+
<artifactId>tokenizers</artifactId>
54+
<version>0.33.0</version>
55+
</dependency>
56+
57+
<dependency>
58+
<groupId>ai.djl.pytorch</groupId>
59+
<artifactId>pytorch-model-zoo</artifactId>
60+
<version>0.33.0</version>
61+
</dependency>
62+
63+
<dependency>
64+
<groupId>ai.djl</groupId>
65+
<artifactId>api</artifactId>
66+
<version>0.33.0</version>
67+
</dependency>
68+
```
69+
70+
If you are using [Gradle](https://gradle.org/), add the following
71+
dependencies to your `build.gradle` file:
72+
73+
```bash
74+
compileOnly 'io.lettuce:lettuce-core:6.7.1.RELEASE'
75+
compileOnly 'ai.djl.huggingface:tokenizers:0.33.0'
76+
compileOnly 'ai.djl.pytorch:pytorch-model-zoo:0.33.0'
77+
compileOnly 'ai.djl:api:0.33.0'
78+
```
79+
80+
## Import dependencies
81+
82+
Import the following classes in your source file:
83+
84+
{{< clients-example set="home_query_vec" step="import" lang_filter="Java-Async,Java-Reactive" >}}
85+
{{< /clients-example >}}
86+
87+
## Define a helper method
88+
89+
When you store vectors in a hash object, or pass them as query parameters,
90+
you must encode the `float` components of the vector
91+
array as a `byte` string. The helper method `floatArrayToByteBuffer()`
92+
shown below does this for you:
93+
94+
{{< clients-example set="home_query_vec" step="helper_method" lang_filter="Java-Async,Java-Reactive" >}}
95+
{{< /clients-example >}}
96+
97+
## Create a embedding model instance
98+
99+
The example below uses the
100+
[`all-MiniLM-L6-v2`](https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2)
101+
model to generate the embeddings. The vectors that represent the
102+
embeddings have 384 components, regardless of the length of the input
103+
text.
104+
105+
The [`Predictor`](https://javadoc.io/doc/ai.djl/api/latest/ai/djl/inference/Predictor.html)
106+
class implements the model to generate the embeddings. The code below
107+
creates an instance of `Predictor` that uses the `all-MiniLM-L6-v2` model:
108+
109+
{{< clients-example set="home_query_vec" step="model" lang_filter="Java-Async,Java-Reactive" >}}
110+
{{< /clients-example >}}
111+
112+
## Create the index
113+
114+
As noted in [Define a helper method](#define-a-helper-method) above, you must
115+
pass the embeddings to the hash and query commands as a binary string. Lettuce lets
116+
you specify a `ByteBufferCodec` for the connection to Redis, which lets you construct
117+
the binary strings for Redis keys and values conveniently using
118+
the standard
119+
[`ByteBuffer`](https://docs.oracle.com/javase/8/docs/api/java/nio/ByteBuffer.html)
120+
class (see [Codecs](https://redis.github.io/lettuce/integration-extension/#codecs)
121+
in the Lettuce documentation for more information). However, it is more convenient
122+
to use the default `StringCodec` for commands that don't require binary strings.
123+
The code below shows how to declare both connections in the try-with-resources
124+
block. You also need two separate instances of `RedisAsyncCommands` to
125+
use the two connections:
126+
127+
{{< clients-example set="home_query_vec" step="connect" lang_filter="Java-Async,Java-Reactive" >}}
128+
{{< /clients-example >}}
129+
130+
Next, create the index.
131+
The schema in the example below includes three fields:
132+
133+
- The text content to index
134+
- A [tag]({{< relref "/develop/ai/search-and-query/advanced-concepts/tags" >}})
135+
field to represent the "genre" of the text
136+
- The embedding vector generated from the original text content
137+
138+
The `embedding` field specifies
139+
[HNSW]({{< relref "/develop/ai/search-and-query/vectors#hnsw-index" >}})
140+
indexing, the
141+
[L2]({{< relref "/develop/ai/search-and-query/vectors#distance-metrics" >}})
142+
vector distance metric, `Float32` values to represent the vector's components,
143+
and 384 dimensions, as required by the `all-MiniLM-L6-v2` embedding model.
144+
145+
The `CreateArgs` object specifies hash objects for storage and a
146+
prefix `doc:` that identifies the hash objects we want to index.
147+
148+
{{< clients-example set="home_query_vec" step="create_index" lang_filter="Java-Async,Java-Reactive" >}}
149+
{{< /clients-example >}}
150+
151+
## Add data
152+
153+
You can now supply the data objects, which will be indexed automatically
154+
when you add them with [`hset()`]({{< relref "/commands/hset" >}}), as long as
155+
you use the `doc:` prefix specified in the index definition.
156+
157+
Use the `predict()` method of the `Predictor` object
158+
as shown below to create the embedding that represents the `content` field
159+
and use the `floatArrayToByteBuffer()` helper method to convert it to a binary string.
160+
Use the binary string representation when you are
161+
indexing hash objects (as we are here), but use an array of `float` for
162+
JSON objects (see [Differences with JSON objects](#differences-with-json-documents)
163+
below).
164+
165+
You must use instances of `Map<ByteBuffer, ByteBuffer>` to supply the data to `hset()`
166+
when using the `ByteBufferCodec` connection, which adds a little complexity. Note
167+
that the `predict()` method is in a `try`/`catch` block because it can throw
168+
exceptions if it can't download the embedding model (you should add code to handle
169+
the exceptions in production code).
170+
171+
{{< clients-example set="home_query_vec" step="add_data" lang_filter="Java-Async,Java-Reactive" >}}
172+
{{< /clients-example >}}
173+
174+
## Run a query
175+
176+
After you have created the index and added the data, you are ready to run a query.
177+
To do this, you must create another embedding vector from your chosen query
178+
text. Redis calculates the vector distance between the query vector and each
179+
embedding vector in the index as it runs the query. You can request the results to be
180+
sorted to rank them in order of ascending distance.
181+
182+
The code below creates the query embedding using the `predict()` method, as with
183+
the indexing, and passes it as a parameter when the query executes (see
184+
[Vector search]({{< relref "/develop/ai/search-and-query/query/vector-search" >}})
185+
for more information about using query parameters with embeddings).
186+
The query is a
187+
[K nearest neighbors (KNN)]({{< relref "/develop/ai/search-and-query/vectors#knn-vector-search" >}})
188+
search that sorts the results in order of vector distance from the query vector.
189+
190+
{{< clients-example set="home_query_vec" step="query" lang_filter="Java-Async,Java-Reactive" >}}
191+
{{< /clients-example >}}
192+
193+
Assuming you have added the code from the steps above to your source file,
194+
it is now ready to run, but note that it may take a while to complete when
195+
you run it for the first time (which happens because the model must download the
196+
`all-MiniLM-L6-v2` model data before it can
197+
generate the embeddings). When you run the code, it outputs the following result text:
198+
199+
```
200+
Results:
201+
ID: doc:1, Content: That is a very happy person, Distance: 0.114169836044
202+
ID: doc:2, Content: That is a happy dog, Distance: 0.610845506191
203+
ID: doc:3, Content: Today is a sunny day, Distance: 1.48624765873
204+
```
205+
206+
Note that the results are ordered according to the value of the `distance`
207+
field, with the lowest distance indicating the greatest similarity to the query.
208+
As you would expect, the result for `doc:1` with the content text
209+
*"That is a very happy person"*
210+
is the result that is most similar in meaning to the query text
211+
*"That is a happy person"*.
212+
213+
## Differences with JSON documents
214+
215+
Indexing JSON documents is similar to hash indexing, but there are some
216+
important differences. JSON allows much richer data modeling with nested fields, so
217+
you must supply a [path]({{< relref "/develop/data-types/json/path" >}}) in the schema
218+
to identify each field you want to index. However, you can declare a short alias for each
219+
of these paths (using the `as()` option) to avoid typing it in full for
220+
every query. Also, you must specify `CreateArgs.TargetType.JSON` when you create the index.
221+
222+
The code below shows these differences, but the index is otherwise very similar to
223+
the one created previously for hashes:
224+
225+
{{< clients-example set="home_query_vec" step="json_schema" lang_filter="Java-Async,Java-Reactive" >}}
226+
{{< /clients-example >}}
227+
228+
An important difference with JSON indexing is that the vectors are
229+
specified using arrays of `float` instead of binary strings. This means
230+
you don't need to use the `ByteBufferCodec` connection, and you can use
231+
`Arrays.toString()` to convert the `float` array to a suitable JSON string.
232+
233+
Use [`jsonSet()`]({{< relref "/commands/json.set" >}}) to add the data
234+
instead of [`hset()`]({{< relref "/commands/hset" >}}). Use instances
235+
of `JSONObject` to supply the data instead of `Map`, as you would for
236+
hash objects.
237+
238+
{{< clients-example set="home_query_vec" step="json_data" lang_filter="Java-Async,Java-Reactive" >}}
239+
{{< /clients-example >}}
240+
241+
The query is almost identical to the one for the hash documents. This
242+
demonstrates how the right choice of aliases for the JSON paths can
243+
save you having to write complex queries. An important thing to notice
244+
is that the vector parameter for the query is still specified as a
245+
binary string, even though the data for the `embedding` field of the JSON
246+
was specified as an array.
247+
248+
{{< clients-example set="home_query_vec" step="json_query" lang_filter="Java-Async,Java-Reactive" >}}
249+
{{< /clients-example >}}
250+
251+
The distance values are not identical to the hash query because the
252+
string representations of the vectors used here are stored with different
253+
precisions. However, the relative order of the results is the same:
254+
255+
```
256+
Results:
257+
ID: jdoc:1, Content: That is a very happy person, Distance: 0.628328084946
258+
ID: jdoc:2, Content: That is a happy dog, Distance: 0.895147025585
259+
ID: jdoc:3, Content: Today is a sunny day, Distance: 1.49569523335
260+
```
261+
262+
## Learn more
263+
264+
See
265+
[Vector search]({{< relref "/develop/ai/search-and-query/query/vector-search" >}})
266+
for more information about the indexing options, distance metrics, and query format
267+
for vectors.

0 commit comments

Comments
 (0)