diff --git a/content/develop/clients/jedis/vecsearch.md b/content/develop/clients/jedis/vecsearch.md index 53881dbd36..f7b26892c4 100644 --- a/content/develop/clients/jedis/vecsearch.md +++ b/content/develop/clients/jedis/vecsearch.md @@ -28,8 +28,8 @@ similarity of an embedding generated from some query text with embeddings stored or JSON fields, Redis can retrieve documents that closely match the query in terms of their meaning. -In the example below, we use the [HuggingFace](https://huggingface.co/) model -[`all-mpnet-base-v2`](https://huggingface.co/sentence-transformers/all-mpnet-base-v2) +The example below uses the [HuggingFace](https://huggingface.co/) model +[`all-MiniLM-L6-v2`](https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2) to generate the 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 @@ -53,12 +53,22 @@ dependencies to your `pom.xml` file: redis.clients jedis - 5.2.0 + 6.1.0 ai.djl.huggingface tokenizers - 0.24.0 + 0.33.0 + + + ai.djl.pytorch + pytorch-model-zoo + 0.33.0 + + + ai.djl + api + 0.33.0 ``` @@ -66,8 +76,10 @@ If you are using [Gradle](https://gradle.org/), add the following dependencies to your `build.gradle` file: ```bash -implementation 'redis.clients:jedis:5.2.0' -implementation 'ai.djl.huggingface:tokenizers:0.24.0' +implementation 'redis.clients:jedis:6.1.0' +implementation 'ai.djl.huggingface:tokenizers:0.33.0' +implementation 'ai.djl.pytorch:pytorch-model-zoo:0.33.0' +implementation 'ai.djl:api:0.33.0' ``` ## Import dependencies @@ -79,24 +91,25 @@ Import the following classes in your source file: ## Define a helper method -Our embedding model represents the vectors as an array of `long` integer values, -but Redis Query Engine expects the vector components to be `float` values. +The embedding model represents the vectors as an array of `float` values, +which is the format required by Redis Query Engine. Also, when you store vectors in a hash object, you must encode the vector -array as a `byte` string. To simplify this situation, we declare a helper -method `longsToFloatsByteString()` that takes the `long` array that the -embedding model returns, converts it to an array of `float` values, and -then encodes the `float` array as a `byte` string: +array as a `byte` string. To simplify this situation, you can declare a helper +method `floatsToByteString()` that takes the `float` array that the +embedding model returns and encodes it as a `byte` string: {{< clients-example set="HomeQueryVec" step="helper_method" lang_filter="Java-Sync" >}} {{< /clients-example >}} ## Create a tokenizer instance -We will use the -[`all-mpnet-base-v2`](https://huggingface.co/sentence-transformers/all-mpnet-base-v2) -tokenizer to generate the embeddings. The vectors that represent the -embeddings have 768 components, regardless of the length of the input -text. +The next step is to generate the embeddings using the +[`all-MiniLM-L6-v2`](https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2) +model. The vectors that represent the +embeddings have 384 components, regardless of the length of the input +text, but note that the input is truncated to 256 +tokens (see +[Word piece tokenization](https://huggingface.co/learn/nlp-course/en/chapter6/6) {{< clients-example set="HomeQueryVec" step="tokenizer" lang_filter="Java-Sync" >}} {{< /clients-example >}} @@ -111,7 +124,7 @@ the index doesn't already exist, which is why you need the {{< clients-example set="HomeQueryVec" step="connect" lang_filter="Java-Sync" >}} {{< /clients-example >}} -Next, we create the index. +Next, create the index. The schema in the example below includes three fields: the text content to index, a [tag]({{< relref "/develop/ai/search-and-query/advanced-concepts/tags" >}}) field to represent the "genre" of the text, and the embedding vector generated from @@ -120,10 +133,10 @@ the original text content. The `embedding` field specifies indexing, the [L2]({{< relref "/develop/ai/search-and-query/vectors#distance-metrics" >}}) vector distance metric, `Float32` values to represent the vector's components, -and 768 dimensions, as required by the `all-mpnet-base-v2` embedding model. +and 384 dimensions, as required by the `all-MiniLM-L6-v2` embedding model. The `FTCreateParams` object specifies hash objects for storage and a -prefix `doc:` that identifies the hash objects we want to index. +prefix `doc:` that identifies the hash objects to index. {{< clients-example set="HomeQueryVec" step="create_index" lang_filter="Java-Sync" >}} {{< /clients-example >}} @@ -134,16 +147,15 @@ You can now supply the data objects, which will be indexed automatically when you add them with [`hset()`]({{< relref "/commands/hset" >}}), as long as you use the `doc:` prefix specified in the index definition. -Use the `encode()` method of the `sentenceTokenizer` object +Use the `predict()` method of the `Predictor` object as shown below to create the embedding that represents the `content` field. -The `getIds()` method that follows `encode()` obtains the vector -of `long` values which we then convert to a `float` array stored as a `byte` -string using our helper method. Use the `byte` string representation when you are -indexing hash objects (as we are here), but use an array of `float` for +The `predict()` method returns a `float[]` array which is then converted to a `byte` +string using the helper method. Use the `byte` string representation when you are +indexing hash objects (as in this example), but use the array of `float` directly for JSON objects (see [Differences with JSON objects](#differences-with-json-documents) -below). Note that when we set the `embedding` field, we must use an overload +below). Note that when you set the `embedding` field, you must use an overload of `hset()` that requires `byte` arrays for each of the key, the field name, and -the value, which is why we include the `getBytes()` calls on the strings. +the value, which is why you must include the `getBytes()` calls on the strings. {{< clients-example set="HomeQueryVec" step="add_data" lang_filter="Java-Sync" >}} {{< /clients-example >}} @@ -153,10 +165,10 @@ the value, which is why we include the `getBytes()` calls on the strings. After you have created the index and added the data, you are ready to run a query. To do this, you must create another embedding vector from your chosen query text. Redis calculates the vector distance between the query vector and each -embedding vector in the index as it runs the query. We can request the results to be +embedding vector in the index as it runs the query. You can request the results to be sorted to rank them in order of ascending distance. -The code below creates the query embedding using the `encode()` method, as with +The code below creates the query embedding using the `predict()` method, as with the indexing, and passes it as a parameter when the query executes (see [Vector search]({{< relref "/develop/ai/search-and-query/query/vector-search" >}}) for more information about using query parameters with embeddings). @@ -170,19 +182,19 @@ search that sorts the results in order of vector distance from the query vector. Assuming you have added the code from the steps above to your source file, it is now ready to run, but note that it may take a while to complete when you run it for the first time (which happens because the tokenizer must download the -`all-mpnet-base-v2` model data before it can +`all-MiniLM-L6-v2` model data before it can generate the embeddings). When you run the code, it outputs the following result text: ``` Results: -ID: doc:2, Distance: 1411344, Content: That is a happy dog -ID: doc:1, Distance: 9301635, Content: That is a very happy person -ID: doc:3, Distance: 67178800, Content: Today is a sunny day +ID: doc:1, Distance: 0.114169836044, Content: That is a very happy person +ID: doc:2, Distance: 0.610845506191, Content: That is a happy dog +ID: doc:3, Distance: 1.48624765873, Content: Today is a sunny day ``` Note that the results are ordered according to the value of the `distance` field, with the lowest distance indicating the greatest similarity to the query. -For this model, the text *"That is a happy dog"* +As expected, the text *"That is a very happy person"* is the result judged to be most similar in meaning to the query text *"That is a happy person"*. @@ -202,12 +214,8 @@ the one created previously for hashes: {{< /clients-example >}} An important difference with JSON indexing is that the vectors are -specified using arrays of `float` instead of binary strings. This requires -a modified version of the `longsToFloatsByteString()` method -used previously: - -{{< clients-example set="HomeQueryVec" step="json_helper_method" lang_filter="Java-Sync" >}} -{{< /clients-example >}} +specified using arrays of `float` instead of binary strings, so you don't need +a helper method to convert the array to a binary string. Use [`jsonSet()`]({{< relref "/commands/json.set" >}}) to add the data instead of [`hset()`]({{< relref "/commands/hset" >}}). Use instances @@ -221,7 +229,7 @@ The query is almost identical to the one for the hash documents. This demonstrates how the right choice of aliases for the JSON paths can save you having to write complex queries. An important thing to notice is that the vector parameter for the query is still specified as a -binary string (using the `longsToFloatsByteString()` method), even though +binary string (created using the `floatsToByteString()` method), even though the data for the `embedding` field of the JSON was specified as an array. {{< clients-example set="HomeQueryVec" step="json_query" lang_filter="Java-Sync" >}} @@ -232,9 +240,9 @@ query is the same as for hash: ``` Results: -ID: jdoc:2, Distance: 1411344, Content: That is a happy dog -ID: jdoc:1, Distance: 9301635, Content: That is a very happy person -ID: jdoc:3, Distance: 67178800, Content: Today is a sunny day +ID: jdoc:1, Distance: 0.114169836044, Content: That is a very happy person +ID: jdoc:2, Distance: 0.610845506191, Content: That is a happy dog +ID: jdoc:3, Distance: 1.48624765873, Content: Today is a sunny day ``` ## Learn more diff --git a/content/develop/clients/jedis/vecsets.md b/content/develop/clients/jedis/vecsets.md new file mode 100644 index 0000000000..31ee385345 --- /dev/null +++ b/content/develop/clients/jedis/vecsets.md @@ -0,0 +1,198 @@ +--- +categories: +- docs +- develop +- stack +- oss +- rs +- rc +- oss +- kubernetes +- clients +description: Index and query embeddings with Redis vector sets +linkTitle: Vector set embeddings +title: Vector set embeddings +weight: 4 +bannerText: Vector set is a new data type that is currently in preview and may be subject to change. +bannerChildren: true +--- + +A Redis [vector set]({{< relref "/develop/data-types/vector-sets" >}}) lets +you store a set of unique keys, each with its own associated vector. +You can then retrieve keys from the set according to the similarity between +their stored vectors and a query vector that you specify. + +You can use vector sets to store any type of numeric vector but they are +particularly optimized to work with text embedding vectors (see +[Redis for AI]({{< relref "/develop/ai" >}}) to learn more about text +embeddings). The example below shows how to generate vector embeddings and then +store and retrieve them using a vector set with `Jedis`. + +## Initialize + +If you are using [Maven](https://maven.apache.org/), add the following +dependencies to your `pom.xml` file +(note that you need `Jedis` v6.2.0 or later to use vector sets): + +```xml + + redis.clients + jedis + 6.2.0 + + + + ai.djl.huggingface + tokenizers + 0.33.0 + + + + ai.djl.pytorch + pytorch-model-zoo + 0.33.0 + + + + ai.djl + api + 0.33.0 + +``` + +If you are using [Gradle](https://gradle.org/), add the following +dependencies to your `build.gradle` file: + +```bash +compileOnly 'redis.clients:jedis:6.2.0' +compileOnly 'ai.djl.huggingface:tokenizers:0.33.0' +compileOnly 'ai.djl.pytorch:pytorch-model-zoo:0.33.0' +compileOnly 'ai.djl:api:0.33.0' +``` + +In a new Java file, import the required classes: + +{{< clients-example set="home_vecsets" step="import" lang_filter="Java-Sync" >}} +{{< /clients-example >}} + +The imports include the classes required to generate embeddings from text. +This example uses an instance of the `Predictor` class with the +[`all-MiniLM-L6-v2`](https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2) +model for the embeddings. This model generates vectors with 384 dimensions, regardless +of the length of the input text, but note that the input is truncated to 256 +tokens (see +[Word piece tokenization](https://huggingface.co/learn/nlp-course/en/chapter6/6) +at the [Hugging Face](https://huggingface.co/) docs to learn more about the way tokens +are related to the original text). + +{{< clients-example set="home_vecsets" step="model" lang_filter="Java-Sync" >}} +{{< /clients-example >}} + +## Create the data + +The example data is contained in a `List` object with some brief +descriptions of famous people. + +{{< clients-example set="home_vecsets" step="data" lang_filter="Java-Sync" >}} +{{< /clients-example >}} + +## Add the data to a vector set + +The code below connects to Redis, then iterates through all the items in the `people` list, +generates embeddings for each person's description, and then +adds the appropriate elements to a vector set called `famousPeople`. +Note that the `predict()` call is in a `try`/`catch` block because it can throw +exceptions if it can't download the embedding model (you should add code to handle +the exceptions for production). + +The call to `vadd()` also adds the `born` and `died` values from the +original `people` list as attribute data. You can access this during a query +or by using the [`vgetattr()`]({{< relref "/commands/vgetattr" >}}) method. + +{{< clients-example set="home_vecsets" step="add_data" lang_filter="Java-Sync" >}} +{{< /clients-example >}} + +## Query the vector set + +You can now query the data in the set. The basic approach is to use the +`predict()` method to generate another embedding vector for the query text. +(This is the same method used to add the elements to the set.) Then, pass +the query vector to [`vsim()`]({{< relref "/commands/vsim" >}}) to return elements +of the set, ranked in order of similarity to the query. + +Start with a simple query for "actors": + +{{< clients-example set="home_vecsets" step="basic_query" lang_filter="Java-Sync" >}} +{{< /clients-example >}} + +This returns the following list of elements (formatted slightly for clarity): + +``` +['Masako Natsume', 'Chaim Topol', 'Linus Pauling', +'Marie Fredriksson', 'Maryam Mirzakhani', 'Marie Curie', +'Freddie Mercury', 'Paul Erdos'] +``` + +The first two people in the list are the two actors, as expected, but none of the +people from Linus Pauling onward was especially well-known for acting (and there certainly +isn't any information about that in the short description text). +As it stands, the search attempts to rank all the elements in the set, based +on the information contained in the embedding model. +You can use the `count` parameter of `vsim()` to limit the list of elements +to just the most relevant few items: + +{{< clients-example set="home_vecsets" step="limited_query" lang_filter="Java-Sync" >}} +{{< /clients-example >}} + +The reason for using text embeddings rather than simple text search +is that the embeddings represent semantic information. This allows a query +to find elements with a similar meaning even if the text is +different. For example, the word "entertainer" doesn't appear in any of the +descriptions, but if you use it as a query, the actors and musicians are ranked +highest in the results list: + +{{< clients-example set="home_vecsets" step="entertainer_query" lang_filter="Java-Async,Java-Reactive" >}} +{{< /clients-example >}} + +Similarly, if you use "science" as a query, you get the following results: + +``` +['Marie Curie', 'Linus Pauling', 'Maryam Mirzakhani', +'Paul Erdos', 'Marie Fredriksson', 'Freddie Mercury', 'Masako Natsume', +'Chaim Topol'] +``` + +The scientists are ranked highest, followed by the +mathematicians. This ranking seems reasonable given the connection between mathematics and science. + +You can also use +[filter expressions]({{< relref "/develop/data-types/vector-sets/filtered-search" >}}) +with `vsim()` to restrict the search further. For example, +repeat the "science" query, but this time limit the results to people +who died before the year 2000: + +{{< clients-example set="home_vecsets" step="filtered_query" lang_filter="Java-Sync" >}} +{{< /clients-example >}} + +Note that the boolean filter expression is applied to items in the list +before the vector distance calculation is performed. Items that don't +pass the filter test are removed from the results completely, rather +than just reduced in rank. This can help to improve the performance of the +search because there is no need to calculate the vector distance for +elements that have already been filtered out of the search. + +## More information + +See the [vector sets]({{< relref "/develop/data-types/vector-sets" >}}) +docs for more information and code examples. See the +[Redis for AI]({{< relref "/develop/ai" >}}) section for more details +about text embeddings and other AI techniques you can use with Redis. + +You may also be interested in +[vector search]({{< relref "/develop/clients/jedis/vecsearch" >}}). +This is a feature of the +[Redis query engine]({{< relref "/develop/ai/search-and-query" >}}) +that lets you retrieve +[JSON]({{< relref "/develop/data-types/json" >}}) and +[hash]({{< relref "/develop/data-types/hashes" >}}) documents based on +vector data stored in their fields. diff --git a/local_examples/client-specific/jedis/HomeQueryVec.java b/local_examples/client-specific/jedis/HomeQueryVec.java index 197a1b6059..b52a9a39c9 100644 --- a/local_examples/client-specific/jedis/HomeQueryVec.java +++ b/local_examples/client-specific/jedis/HomeQueryVec.java @@ -1,32 +1,32 @@ // EXAMPLE: HomeQueryVec +// REMOVE_START +package com.redis.app; +// REMOVE_END // STEP_START import -// Jedis client and query engine classes. import redis.clients.jedis.UnifiedJedis; import redis.clients.jedis.search.*; import redis.clients.jedis.search.schemafields.*; import redis.clients.jedis.search.schemafields.VectorField.VectorAlgorithm; import redis.clients.jedis.exceptions.JedisDataException; +import redis.clients.jedis.json.Path2; + +import org.json.JSONObject; -// Data manipulation. import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.Map; import java.util.List; -import org.json.JSONObject; -import redis.clients.jedis.json.Path2; -// Tokenizer to generate the vector embeddings. -import ai.djl.huggingface.tokenizers.HuggingFaceTokenizer; +// DJL classes for model loading and inference. +import ai.djl.huggingface.translator.TextEmbeddingTranslatorFactory; +import ai.djl.inference.Predictor; +import ai.djl.repository.zoo.Criteria; +import ai.djl.training.util.ProgressBar; // STEP_END public class HomeQueryVec { // STEP_START helper_method - public static byte[] longsToFloatsByteString(long[] input) { - float[] floats = new float[input.length]; - for (int i = 0; i < input.length; i++) { - floats[i] = input[i]; - } - + public static byte[] floatsToByteString(float[] floats) { byte[] bytes = new byte[Float.BYTES * floats.length]; ByteBuffer .wrap(bytes) @@ -37,27 +37,31 @@ public static byte[] longsToFloatsByteString(long[] input) { } // STEP_END - // STEP_START json_helper_method - public static float[] longArrayToFloatArray(long[] input) { - float[] floats = new float[input.length]; - for (int i = 0; i < input.length; i++) { - floats[i] = input[i]; - } - return floats; - } - // STEP_END - - public static void main(String[] args) throws Exception { + public static void main(String[] args) { // STEP_START tokenizer - HuggingFaceTokenizer sentenceTokenizer = HuggingFaceTokenizer.newInstance( - "sentence-transformers/all-mpnet-base-v2", - Map.of("maxLength", "768", "modelMaxLength", "768") - ); + Predictor predictor = null; + + try { + Criteria criteria = Criteria.builder().setTypes(String.class, float[].class) + .optModelUrls("djl://ai.djl.huggingface.pytorch/sentence-transformers/all-MiniLM-L6-v2") + .optEngine("PyTorch").optTranslatorFactory(new TextEmbeddingTranslatorFactory()) + .optProgress(new ProgressBar()).build(); + + predictor = criteria.loadModel().newPredictor(); + } catch (Exception e) { + // ... + } // STEP_END // STEP_START connect UnifiedJedis jedis = new UnifiedJedis("redis://localhost:6379"); - + // REMOVE_START + jedis.del( + "doc:1", "doc:2", "doc:3", + "jdoc:1", "jdoc:2", "jdoc:3" + ); + // REMOVE_END + try {jedis.ftDropIndex("vector_idx");} catch (JedisDataException j){} // STEP_END @@ -71,65 +75,85 @@ public static void main(String[] args) throws Exception { .attributes( Map.of( "TYPE", "FLOAT32", - "DIM", 768, + "DIM", 384, "DISTANCE_METRIC", "L2" ) ) .build() }; - + jedis.ftCreate("vector_idx", FTCreateParams.createParams() .addPrefix("doc:") .on(IndexDataType.HASH), schema ); + // STEP_END - + // STEP_START add_data String sentence1 = "That is a very happy person"; - jedis.hset("doc:1", Map.of("content", sentence1, "genre", "persons")); - jedis.hset( - "doc:1".getBytes(), - "embedding".getBytes(), - longsToFloatsByteString(sentenceTokenizer.encode(sentence1).getIds()) - ); + byte[] embedding1; + + try { + embedding1 = floatsToByteString(predictor.predict(sentence1)); + } catch (Exception e) { + // This just allows the code to compile without errors. + // In a real-world scenario, you would handle the exception properly. + embedding1 = new byte[384 * Float.BYTES]; + } + jedis.hset("doc:1", Map.of( "content", sentence1, "genre", "persons")); + jedis.hset("doc:1".getBytes(), "embedding".getBytes(), embedding1); + String sentence2 = "That is a happy dog"; - jedis.hset("doc:2", Map.of("content", sentence2, "genre", "pets")); - jedis.hset( - "doc:2".getBytes(), - "embedding".getBytes(), - longsToFloatsByteString(sentenceTokenizer.encode(sentence2).getIds()) - ); + byte[] embedding2; + + try { + embedding2 = floatsToByteString(predictor.predict(sentence2)); + } catch (Exception e) { + embedding2 = new byte[384 * Float.BYTES]; + } + + jedis.hset("doc:2", Map.of( "content", sentence2, "genre", "pets")); + jedis.hset("doc:2".getBytes(), "embedding".getBytes(), embedding2); String sentence3 = "Today is a sunny day"; - jedis.hset("doc:3", Map.of("content", sentence3, "genre", "weather")); - jedis.hset( - "doc:3".getBytes(), - "embedding".getBytes(), - longsToFloatsByteString(sentenceTokenizer.encode(sentence3).getIds()) - ); - // STEP_END + byte[] embedding3; + try { + embedding3 = floatsToByteString(predictor.predict(sentence3)); + } catch (Exception e) { + embedding3 = new byte[384 * Float.BYTES]; + } + + Map doc3 = Map.of( "content", sentence3, "genre", "weather"); + jedis.hset("doc:3", doc3); + jedis.hset("doc:3".getBytes(), "embedding".getBytes(), embedding3); + // STEP_END + // STEP_START query String sentence = "That is a happy person"; + byte[] embedding; + try { + embedding = floatsToByteString(predictor.predict(sentence)); + } catch (Exception e) { + embedding = new byte[384 * Float.BYTES]; + } + int K = 3; - Query q = new Query("*=>[KNN $K @embedding $BLOB AS distance]") - .returnFields("content", "distance") - .addParam("K", K) - .addParam( - "BLOB", - longsToFloatsByteString( - sentenceTokenizer.encode(sentence).getIds() - ) - ) - .setSortBy("distance", true) - .dialect(2); + Query q = new Query("*=>[KNN $K @embedding $BLOB AS distance]"). + returnFields("content", "distance"). + addParam("K", K). + addParam("BLOB", embedding) + .setSortBy("distance", true) + .dialect(2); + // Execute the query List docs = jedis.ftSearch("vector_idx", q).getDocuments(); - + System.out.println("Results:"); + for (Document doc: docs) { System.out.println( String.format( @@ -142,6 +166,8 @@ public static void main(String[] args) throws Exception { } // STEP_END + try {jedis.ftDropIndex("vector_json_idx");} catch (JedisDataException j){} + // STEP_START json_schema SchemaField[] jsonSchema = { TextField.of("$.content").as("content"), @@ -152,13 +178,13 @@ public static void main(String[] args) throws Exception { .attributes( Map.of( "TYPE", "FLOAT32", - "DIM", 768, + "DIM", 384, "DISTANCE_METRIC", "L2" ) ) .build() }; - + jedis.ftCreate("vector_json_idx", FTCreateParams.createParams() .addPrefix("jdoc:") @@ -170,42 +196,62 @@ public static void main(String[] args) throws Exception { // STEP_START json_data String jSentence1 = "That is a very happy person"; + float[] jEmbedding1; + + try { + jEmbedding1 = predictor.predict(jSentence1); + } catch (Exception e) { + // This just allows the code to compile without errors. + // In a real-world scenario, you would handle the exception properly. + jEmbedding1 = new float[384]; + } + JSONObject jdoc1 = new JSONObject() .put("content", jSentence1) .put("genre", "persons") .put( "embedding", - longArrayToFloatArray( - sentenceTokenizer.encode(jSentence1).getIds() - ) + jEmbedding1 ); jedis.jsonSet("jdoc:1", Path2.ROOT_PATH, jdoc1); String jSentence2 = "That is a happy dog"; + float[] jEmbedding2; + + try { + jEmbedding2 = predictor.predict(jSentence2); + } catch (Exception e) { + jEmbedding2 = new float[384]; + } + JSONObject jdoc2 = new JSONObject() .put("content", jSentence2) .put("genre", "pets") .put( "embedding", - longArrayToFloatArray( - sentenceTokenizer.encode(jSentence2).getIds() - ) + jEmbedding2 ); - + jedis.jsonSet("jdoc:2", Path2.ROOT_PATH, jdoc2); String jSentence3 = "Today is a sunny day"; + float[] jEmbedding3; + + try { + jEmbedding3 = predictor.predict(jSentence3); + } catch (Exception e) { + jEmbedding3 = new float[384]; + } + JSONObject jdoc3 = new JSONObject() .put("content", jSentence3) .put("genre", "weather") .put( "embedding", - longArrayToFloatArray( - sentenceTokenizer.encode(jSentence3).getIds() - ) + jEmbedding3 ); jedis.jsonSet("jdoc:3", Path2.ROOT_PATH, jdoc3); @@ -213,25 +259,32 @@ public static void main(String[] args) throws Exception { // STEP_START json_query String jSentence = "That is a happy person"; + byte[] jEmbedding; + + try { + jEmbedding = floatsToByteString(predictor.predict(jSentence)); + } catch (Exception e) { + jEmbedding = new byte[384 * Float.BYTES]; + } int jK = 3; - Query jq = new Query("*=>[KNN $K @embedding $BLOB AS distance]") - .returnFields("content", "distance") - .addParam("K", jK) - .addParam( - "BLOB", - longsToFloatsByteString( - sentenceTokenizer.encode(jSentence).getIds() + Query jq = new Query("*=>[KNN $K @embedding $BLOB AS distance]"). + returnFields("content", "distance"). + addParam("K", jK). + addParam( + "BLOB", + jEmbedding ) - ) - .setSortBy("distance", true) - .dialect(2); + .setSortBy("distance", true) + .dialect(2); // Execute the query List jDocs = jedis .ftSearch("vector_json_idx", jq) .getDocuments(); + System.out.println("Results:"); + for (Document doc: jDocs) { System.out.println( String.format( @@ -243,7 +296,5 @@ public static void main(String[] args) throws Exception { ); } // STEP_END - - jedis.close(); } } diff --git a/local_examples/client-specific/jedis/HomeVecSets.java b/local_examples/client-specific/jedis/HomeVecSets.java index 38daa104a2..4a8f6727e5 100644 --- a/local_examples/client-specific/jedis/HomeVecSets.java +++ b/local_examples/client-specific/jedis/HomeVecSets.java @@ -1,184 +1,187 @@ // EXAMPLE: home_vecsets +// REMOVE_START +package com.redis.app; +// REMOVE_END // STEP_START import -// Redis client (Lettuce) and vector set APIs -import io.lettuce.core.RedisClient; -import io.lettuce.core.RedisURI; -import io.lettuce.core.api.StatefulRedisConnection; -import io.lettuce.core.api.sync.RedisCommands; -import io.lettuce.core.VAddArgs; -import io.lettuce.core.VSimArgs; - -// Tokenizer to generate vectors (kept consistent with HomeQueryVec.java) -import ai.djl.huggingface.tokenizers.HuggingFaceTokenizer; - -// Data & utils +import redis.clients.jedis.UnifiedJedis; +import redis.clients.jedis.params.VAddParams; +import redis.clients.jedis.params.VSimParams; + import java.util.*; + +// DJL classes for model loading and inference. +import ai.djl.huggingface.translator.TextEmbeddingTranslatorFactory; +import ai.djl.inference.Predictor; +import ai.djl.repository.zoo.Criteria; +import ai.djl.training.util.ProgressBar; // STEP_END public class HomeVecSets { - // Keep the same tokenizer model style as HomeQueryVec.java - private static final int DIM = 768; // fixed dimension to pad/truncate token ids - - // Helper: convert tokenizer ids to a fixed-length Double[] of size DIM - private static Double[] idsToDoubleVector(long[] ids) { - Double[] out = new Double[DIM]; - int n = Math.min(ids.length, DIM); - for (int i = 0; i < n; i++) out[i] = (double) ids[i]; - for (int i = n; i < DIM; i++) out[i] = 0.0d; // pad - return out; - } - // Simple container for people data - private static class Person { - final int born; - final int died; - final String description; - Person(int born, int died, String description) { - this.born = born; this.died = died; this.description = description; + // REMOVE_START + public static void main(String[] args) { + // REMOVE_END + // STEP_START model + Predictor predictor = null; + + try { + Criteria criteria = Criteria.builder().setTypes(String.class, float[].class) + .optModelUrls("djl://ai.djl.huggingface.pytorch/sentence-transformers/all-MiniLM-L6-v2") + .optEngine("PyTorch").optTranslatorFactory(new TextEmbeddingTranslatorFactory()) + .optProgress(new ProgressBar()).build(); + + predictor = criteria.loadModel().newPredictor(); + } catch (Exception e) { + // ... + } + // STEP_END + + // STEP_START data + final class Person { + final String name; + final int born; + final int died; + final String description; + Person(String name, int born, int died, String description) { + this.name = name; this.born = born; this.died = died; this.description = description; + } } - } - public static void main(String[] args) throws Exception { - // STEP_START model - // Tokenizer configured like HomeQueryVec.java (acts as a simple, deterministic vectorizer here) - HuggingFaceTokenizer tokenizer = HuggingFaceTokenizer.newInstance( - "sentence-transformers/all-mpnet-base-v2", - Map.of("maxLength", String.valueOf(DIM), "modelMaxLength", String.valueOf(DIM)) + List people = Arrays.asList( + new Person( + "Marie Curie", + 1867, 1934, + "Polish-French chemist and physicist. The only person ever to win" + + " two Nobel prizes for two different sciences." + ), + new Person( + "Linus Pauling", + 1901, 1994, + "American chemist and peace activist. One of only two people to" + + " win two Nobel prizes in different fields (chemistry and peace)." + ), + new Person( + "Freddie Mercury", + 1946, 1991, + "British musician, best known as the lead singer of the rock band Queen." + ), + new Person( + "Marie Fredriksson", + 1958, 2019, + "Swedish multi-instrumentalist, mainly known as the lead singer and" + + " keyboardist of the band Roxette." + ), + new Person( + "Paul Erdos", + 1913, 1996, + "Hungarian mathematician, known for his eccentric personality almost" + + " as much as his contributions to many different fields of mathematics." + ), + new Person( + "Maryam Mirzakhani", + 1977, 2017, + "Iranian mathematician. The first woman ever to win the Fields medal" + + " for her contributions to mathematics." + ), + new Person( + "Masako Natsume", + 1957, 1985, + "Japanese actress. She was very famous in Japan but was primarily" + + " known elsewhere in the world for her portrayal of Tripitaka in the" + + " TV series Monkey." + ), + new Person( + "Chaim Topol", + 1935, 2023, + "Israeli actor and singer, usually credited simply as 'Topol'. He was" + + " best known for his many appearances as Tevye in the musical Fiddler" + + " on the Roof." + ) ); - // STEP_END - - // STEP_START data - Map peopleData = new LinkedHashMap<>(); - peopleData.put("Marie Curie", new Person( - 1867, 1934, - """ - Polish-French chemist and physicist. The only person ever to win - two Nobel prizes for two different sciences. - """.trim() - )); - peopleData.put("Linus Pauling", new Person( - 1901, 1994, - """ - American chemist and peace activist. One of only two people to win two - Nobel prizes in different fields (chemistry and peace). - """.trim() - )); - peopleData.put("Freddie Mercury", new Person( - 1946, 1991, - """ - British musician, best known as the lead singer of the rock band - Queen. - """.trim() - )); - peopleData.put("Marie Fredriksson", new Person( - 1958, 2019, - """ - Swedish multi-instrumentalist, mainly known as the lead singer and - keyboardist of the band Roxette. - """.trim() - )); - peopleData.put("Paul Erdos", new Person( - 1913, 1996, - """ - Hungarian mathematician, known for his eccentric personality almost - as much as his contributions to many different fields of mathematics. - """.trim() - )); - peopleData.put("Maryam Mirzakhani", new Person( - 1977, 2017, - """ - Iranian mathematician. The first woman ever to win the Fields medal - for her contributions to mathematics. - """.trim() - )); - peopleData.put("Masako Natsume", new Person( - 1957, 1985, - """ - Japanese actress. She was very famous in Japan but was primarily - known elsewhere in the world for her portrayal of Tripitaka in the - TV series Monkey. - """.trim() - )); - peopleData.put("Chaim Topol", new Person( - 1935, 2023, - """ - Israeli actor and singer, usually credited simply as 'Topol'. He was - best known for his many appearances as Tevye in the musical Fiddler - on the Roof. - """.trim() - )); - // STEP_END - - // STEP_START add_data - RedisClient client = RedisClient.create(RedisURI.Builder.redis("localhost", 6379).build()); - StatefulRedisConnection conn = null; - try { - conn = client.connect(); - RedisCommands cmd = conn.sync(); - - for (Map.Entry e : peopleData.entrySet()) { - String name = e.getKey(); - Person p = e.getValue(); - - // Vector from description - Double[] vec = idsToDoubleVector(tokenizer.encode(p.description).getIds()); - - // Add with attributes using VADD (vector sets API) - VAddArgs addArgs = new VAddArgs() - .attributes(String.format("{\"born\": %d, \"died\": %d}", p.born, p.died)); - - // Create set and add element + vector in one call - Boolean added = cmd.vadd("famousPeople", name, addArgs, vec); - if (Boolean.FALSE.equals(added)) { - // If element exists, you could update attributes via vsetattr - cmd.vsetattr("famousPeople", name, String.format("{\"born\": %d, \"died\": %d}", p.born, p.died)); - } + // STEP_END + + // STEP_START add_data + UnifiedJedis jedis = new UnifiedJedis("redis://localhost:6379"); + // REMOVE_START + jedis.del("famousPeople"); + // REMOVE_END + + for (Person person : people) { + float[] embedding; + try { + embedding = predictor.predict(person.description); + } catch (Exception e) { + // This just allows the code to compile without errors. + // In a real-world scenario, you would handle the exception properly. + embedding = new float[384]; } - } finally { - if (conn != null) conn.close(); - client.shutdown(); + + // Add element with attributes using VAddParams + String attrs = String.format("{\"born\": %d, \"died\": %d}", person.born, person.died); + boolean added = jedis.vadd("famousPeople", embedding, person.name, new VAddParams().setAttr(attrs)); + System.out.println(added); // >>> true + } + // STEP_END + + // STEP_START basic_query + float[] actorsEmbedding; + try { + actorsEmbedding = predictor.predict("actors"); + } catch (Exception e) { + actorsEmbedding = new float[384]; + } + + List basicResults = jedis.vsim("famousPeople", actorsEmbedding); + System.out.println(basicResults); + // >>> [Masako Natsume, Chaim Topol, Linus Pauling, Marie Fredriksson, + // >>> Maryam Mirzakhani, Marie Curie, Freddie Mercury, Paul Erdos] + // STEP_END + + // STEP_START limited_query + VSimParams limitParams = new VSimParams().count(2); + List limitedResults = jedis.vsim("famousPeople", actorsEmbedding, limitParams); + System.out.println(limitedResults); + // >>> [Masako Natsume, Chaim Topol] + // STEP_END + + // STEP_START entertainer_query + float[] entertainerEmbedding; + try { + entertainerEmbedding = predictor.predict("entertainers"); + } catch (Exception e) { + entertainerEmbedding = new float[384]; + } + + List entertainerResults = jedis.vsim("famousPeople", entertainerEmbedding); + System.out.println(entertainerResults); + // >>> [Freddie Mercury, Chaim Topol, Linus Pauling, Marie Fredriksson, + // >>> Masako Natsume, Paul Erdos, Maryam Mirzakhani, Marie Curie] + // STEP_END + + float[] scienceEmbedding; + try { + scienceEmbedding = predictor.predict("science"); + } catch (Exception e) { + scienceEmbedding = new float[384]; } - // STEP_END - - // Reconnect for queries (explicitly, to mirror example flow) - client = RedisClient.create(RedisURI.Builder.redis("localhost", 6379).build()); - try (StatefulRedisConnection qconn = client.connect()) { - RedisCommands q = qconn.sync(); - - // STEP_START basic_query - String queryValue = "actors"; - List actors = q.vsim("famousPeople", idsToDoubleVector(tokenizer.encode(queryValue).getIds())); - System.out.println("'actors': " + String.join(", ", actors)); - // STEP_END - - // STEP_START limited_query - queryValue = "actors"; - VSimArgs twoCount = new VSimArgs().count(2L); - List twoActors = q.vsim("famousPeople", twoCount, idsToDoubleVector(tokenizer.encode(queryValue).getIds())); - System.out.println("'actors (2)': " + String.join(", ", twoActors)); - // >>> 'actors (2)': Masako Natsume, Chaim Topol - // STEP_END - - // STEP_START entertainer_query - queryValue = "entertainer"; - List entertainers = q.vsim("famousPeople", idsToDoubleVector(tokenizer.encode(queryValue).getIds())); - System.out.println("'entertainer': " + String.join(", ", entertainers)); - // >>> 'entertainer': Chaim Topol, Freddie Mercury, Marie Fredriksson, ... - // STEP_END - - queryValue = "science"; - List science = q.vsim("famousPeople", idsToDoubleVector(tokenizer.encode(queryValue).getIds())); - System.out.println("'science': " + String.join(", ", science)); - - // STEP_START filtered_query - queryValue = "science"; - VSimArgs filtered = new VSimArgs().filter(".died < 2000"); - List science2000 = q.vsim("famousPeople", filtered, idsToDoubleVector(tokenizer.encode(queryValue).getIds())); - System.out.println("'science2000': " + String.join(", ", science2000)); - // STEP_END - } finally { - client.shutdown(); + + List scienceResults = jedis.vsim("famousPeople", scienceEmbedding); + System.out.println(scienceResults); + // >>> [Marie Curie, Linus Pauling, Maryam Mirzakhani, Paul Erdos, Marie Fredriksson, Freddie Mercury, Masako Natsume, Chaim Topol] + + // STEP_START filtered_query + float[] science2000Embedding; + try { + science2000Embedding = predictor.predict("science"); + } catch (Exception e) { + science2000Embedding = new float[384]; } + + VSimParams filterParams = new VSimParams().filter(".died < 2000"); + List filteredResults = jedis.vsim("famousPeople", science2000Embedding, filterParams); + System.out.println(filteredResults); + // >>> [Marie Curie, Linus Pauling, Paul Erdos, Freddie Mercury, Masako Natsume] + // STEP_END } }