Skip to content

Commit cb03e1d

Browse files
Merge pull request #1209 from redis/DOC-4837-jedis-vec-example
DOC-4837 jedis vector query example
2 parents cf39a89 + 194273e commit cb03e1d

File tree

3 files changed

+291
-2
lines changed

3 files changed

+291
-2
lines changed

content/develop/clients/jedis/produsage.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ categories:
1212
description: Get your Jedis app ready for production
1313
linkTitle: Production usage
1414
title: Production usage
15-
weight: 3
15+
weight: 6
1616
---
1717

1818
The following sections explain how to handle situations that may occur

content/develop/clients/jedis/transpipe.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ categories:
1212
description: Learn how to use Redis pipelines and transactions
1313
linkTitle: Pipelines/transactions
1414
title: Pipelines and transactions
15-
weight: 2
15+
weight: 5
1616
---
1717

1818
Redis lets you send a sequence of commands to the server together in a batch.
Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
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/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 [HuggingFace](https://huggingface.co/) model
32+
[`all-mpnet-base-v2`](https://huggingface.co/sentence-transformers/all-mpnet-base-v2)
33+
to generate the vector embeddings to store and index with Redis Query Engine.
34+
35+
## Initialize
36+
37+
If you are using [Maven](https://maven.apache.org/), add the following
38+
dependencies to your `pom.xml` file:
39+
40+
```xml
41+
<dependency>
42+
<groupId>redis.clients</groupId>
43+
<artifactId>jedis</artifactId>
44+
<version>5.2.0</version>
45+
</dependency>
46+
<dependency>
47+
<groupId>ai.djl.huggingface</groupId>
48+
<artifactId>tokenizers</artifactId>
49+
<version>0.24.0</version>
50+
</dependency>
51+
```
52+
53+
If you are using [Gradle](https://gradle.org/), add the following
54+
dependencies to your `build.gradle` file:
55+
56+
```bash
57+
implementation 'redis.clients:jedis:5.2.0'
58+
implementation 'ai.djl.huggingface:tokenizers:0.24.0'
59+
```
60+
61+
## Import dependencies
62+
63+
Import the following classes in your source file:
64+
65+
```java
66+
// Jedis client and query engine classes.
67+
import redis.clients.jedis.UnifiedJedis;
68+
import redis.clients.jedis.search.*;
69+
import redis.clients.jedis.search.schemafields.*;
70+
import redis.clients.jedis.search.schemafields.VectorField.VectorAlgorithm;
71+
import redis.clients.jedis.exceptions.JedisDataException;
72+
73+
// Data manipulation.
74+
import java.nio.ByteBuffer;
75+
import java.nio.ByteOrder;
76+
import java.util.Map;
77+
import java.util.List;
78+
79+
// Tokenizer to generate the vector embeddings.
80+
import ai.djl.huggingface.tokenizers.HuggingFaceTokenizer;
81+
```
82+
83+
## Define a helper method
84+
85+
Our embedding model represents the vectors as an array of `long` integer values,
86+
but Redis Query Engine expects the vector components to be `float` values.
87+
Also, when you store vectors in a hash object, you must encode the vector
88+
array as a `byte` string. To simplify this situation, we declare a helper
89+
method `longsToFloatsByteString()` that takes the `long` array that the
90+
embedding model returns, converts it to an array of `float` values, and
91+
then encodes the `float` array as a `byte` string:
92+
93+
```java
94+
public static byte[] longsToFloatsByteString(long[] input) {
95+
float[] floats = new float[input.length];
96+
for (int i = 0; i < input.length; i++) {
97+
floats[i] = input[i];
98+
}
99+
100+
byte[] bytes = new byte[Float.BYTES * floats.length];
101+
ByteBuffer
102+
.wrap(bytes)
103+
.order(ByteOrder.LITTLE_ENDIAN)
104+
.asFloatBuffer()
105+
.put(floats);
106+
return bytes;
107+
}
108+
```
109+
110+
## Create a tokenizer instance
111+
112+
We will use the
113+
[`all-mpnet-base-v2`](https://huggingface.co/sentence-transformers/all-mpnet-base-v2)
114+
tokenizer to generate the embeddings. The vectors that represent the
115+
embeddings have 768 components, regardless of the length of the input
116+
text.
117+
118+
```java
119+
HuggingFaceTokenizer sentenceTokenizer = HuggingFaceTokenizer.newInstance(
120+
"sentence-transformers/all-mpnet-base-v2",
121+
Map.of("maxLength", "768", "modelMaxLength", "768")
122+
);
123+
```
124+
125+
## Create the index
126+
127+
Connect to Redis and delete any index previously created with the
128+
name `vector_idx`. (The `ftDropIndex()` call throws an exception if
129+
the index doesn't already exist, which is why you need the
130+
`try...catch` block.)
131+
132+
```java
133+
UnifiedJedis jedis = new UnifiedJedis("redis://localhost:6379");
134+
135+
try {jedis.ftDropIndex("vector_idx");} catch (JedisDataException j){}
136+
```
137+
138+
Next, we create the index.
139+
The schema in the example below includes three fields: the text content to index, a
140+
[tag]({{< relref "/develop/interact/search-and-query/advanced-concepts/tags" >}})
141+
field to represent the "genre" of the text, and the embedding vector generated from
142+
the original text content. The `embedding` field specifies
143+
[HNSW]({{< relref "/develop/interact/search-and-query/advanced-concepts/vectors#hnsw-index" >}})
144+
indexing, the
145+
[L2]({{< relref "/develop/interact/search-and-query/advanced-concepts/vectors#distance-metrics" >}})
146+
vector distance metric, `Float32` values to represent the vector's components,
147+
and 768 dimensions, as required by the `all-mpnet-base-v2` embedding model.
148+
149+
The `FTCreateParams` object specifies hash objects for storage and a
150+
prefix `doc:` that identifies the hash objects we want to index.
151+
152+
```java
153+
SchemaField[] schema = {
154+
TextField.of("content"),
155+
TagField.of("genre"),
156+
VectorField.builder()
157+
.fieldName("embedding")
158+
.algorithm(VectorAlgorithm.HNSW)
159+
.attributes(
160+
Map.of(
161+
"TYPE", "FLOAT32",
162+
"DIM", 768,
163+
"DISTANCE_METRIC", "L2"
164+
)
165+
)
166+
.build()
167+
};
168+
169+
jedis.ftCreate("vector_idx",
170+
FTCreateParams.createParams()
171+
.addPrefix("doc:")
172+
.on(IndexDataType.HASH),
173+
schema
174+
);
175+
```
176+
177+
## Add data
178+
179+
You can now supply the data objects, which will be indexed automatically
180+
when you add them with [`hset()`]({{< relref "/commands/hset" >}}), as long as
181+
you use the `doc:` prefix specified in the index definition.
182+
183+
Use the `encode()` method of the `sentenceTokenizer` object
184+
as shown below to create the embedding that represents the `content` field.
185+
The `getIds()` method that follows `encode()` obtains the vector
186+
of `long` values which we then convert to a `float` array stored as a `byte`
187+
string using our helper method. Use the `byte` string representation when you are
188+
indexing hash objects (as we are here), but use the default list of `float` for
189+
JSON objects. Note that when we set the `embedding` field, we must use an overload
190+
of `hset()` that requires `byte` arrays for each of the key, the field name, and
191+
the value, which is why we include the `getBytes()` calls on the strings.
192+
193+
```java
194+
String sentence1 = "That is a very happy person";
195+
jedis.hset("doc:1", Map.of("content", sentence1, "genre", "persons"));
196+
jedis.hset(
197+
"doc:1".getBytes(),
198+
"embedding".getBytes(),
199+
longsToFloatsByteString(sentenceTokenizer.encode(sentence1).getIds())
200+
);
201+
202+
String sentence2 = "That is a happy dog";
203+
jedis.hset("doc:2", Map.of("content", sentence2, "genre", "pets"));
204+
jedis.hset(
205+
"doc:2".getBytes(),
206+
"embedding".getBytes(),
207+
longsToFloatsByteString(sentenceTokenizer.encode(sentence2).getIds())
208+
);
209+
210+
String sentence3 = "Today is a sunny day";
211+
jedis.hset("doc:3", Map.of("content", sentence3, "genre", "weather"));
212+
jedis.hset(
213+
"doc:3".getBytes(),
214+
"embedding".getBytes(),
215+
longsToFloatsByteString(sentenceTokenizer.encode(sentence3).getIds())
216+
);
217+
```
218+
219+
## Run a query
220+
221+
After you have created the index and added the data, you are ready to run a query.
222+
To do this, you must create another embedding vector from your chosen query
223+
text. Redis calculates the vector distance between the query vector and each
224+
embedding vector in the index as it runs the query. We can request the results to be
225+
sorted to rank them in order of ascending distance.
226+
227+
The code below creates the query embedding using the `encode()` method, as with
228+
the indexing, and passes it as a parameter when the query executes (see
229+
[Vector search]({{< relref "/develop/interact/search-and-query/query/vector-search" >}})
230+
for more information about using query parameters with embeddings).
231+
The query is a
232+
[K nearest neighbors (KNN)]({{< relref "/develop/interact/search-and-query/advanced-concepts/vectors#knn-vector-search" >}})
233+
search that sorts the results in order of vector distance from the query vector.
234+
235+
```java
236+
String sentence = "That is a happy person";
237+
238+
int K = 3;
239+
Query q = new Query("*=>[KNN $K @embedding $BLOB AS distance]")
240+
.returnFields("content", "distance")
241+
.addParam("K", K)
242+
.addParam(
243+
"BLOB",
244+
longsToFloatsByteString(
245+
sentenceTokenizer.encode(sentence)..getIds()
246+
)
247+
)
248+
.setSortBy("distance", true)
249+
.dialect(2);
250+
251+
List<Document> docs = jedis.ftSearch("vector_idx", q).getDocuments();
252+
253+
for (Document doc: docs) {
254+
System.out.println(
255+
String.format(
256+
"ID: %s, Distance: %s, Content: %s",
257+
doc.getId(),
258+
doc.get("distance"),
259+
doc.get("content")
260+
)
261+
);
262+
}
263+
```
264+
265+
Assuming you have added the code from the steps above to your source file,
266+
it is now ready to run, but note that it may take a while to complete when
267+
you run it for the first time (which happens because the tokenizer must download the
268+
`all-mpnet-base-v2` model data before it can
269+
generate the embeddings). When you run the code, it outputs the following result text:
270+
271+
```
272+
Results:
273+
ID: doc:2, Distance: 1411344, Content: That is a happy dog
274+
ID: doc:1, Distance: 9301635, Content: That is a very happy person
275+
ID: doc:3, Distance: 67178800, Content: Today is a sunny day
276+
```
277+
278+
Note that the results are ordered according to the value of the `distance`
279+
field, with the lowest distance indicating the greatest similarity to the query.
280+
For this model, the text *"That is a happy dog"*
281+
is the result judged to be most similar in meaning to the query text
282+
*"That is a happy person"*.
283+
284+
## Learn more
285+
286+
See
287+
[Vector search]({{< relref "/develop/interact/search-and-query/query/vector-search" >}})
288+
for more information about the indexing options, distance metrics, and query format
289+
for vectors.

0 commit comments

Comments
 (0)