diff --git a/content/develop/use-cases/feature-store/java-lettuce/FeatureStore.java b/content/develop/use-cases/feature-store/java-lettuce/FeatureStore.java
index 1d0731ae8c..7a37a406bc 100644
--- a/content/develop/use-cases/feature-store/java-lettuce/FeatureStore.java
+++ b/content/develop/use-cases/feature-store/java-lettuce/FeatureStore.java
@@ -39,7 +39,7 @@
*
{@code HEXPIRE} and {@code HTTL} require Redis 7.4 or later.
* Lettuce exposes them as {@code hexpire} / {@code httl} on
* {@code RedisAsyncCommands} from 6.4 onwards; the demo pins
- * 7.5.2.RELEASE.
+ * 7.9.0.RELEASE.
*
* Lettuce vs. Jedis
*
diff --git a/content/develop/use-cases/feature-store/java-lettuce/_index.md b/content/develop/use-cases/feature-store/java-lettuce/_index.md
index 0acbefba29..f701ce3ebf 100644
--- a/content/develop/use-cases/feature-store/java-lettuce/_index.md
+++ b/content/develop/use-cases/feature-store/java-lettuce/_index.md
@@ -273,7 +273,7 @@ for debugging and as a freshness signal in the model itself.
> **HEXPIRE requires Redis 7.4 or later.** `HEXPIRE` and the field-level TTL
> commands were added in Redis 7.4. Lettuce 6.4 was the first release with
-> the bindings; the demo's `pom.xml` pins 7.5.2.RELEASE.
+> the bindings; the demo's `pom.xml` pins 7.9.0.RELEASE.
### Inference reads with HMGET
@@ -448,7 +448,7 @@ Endpoints:
demo relies on per-field TTL for the mixed-staleness story.
* **Java 17 or later.** The demo uses switch expressions with arrow labels
(`case "..." -> ...`), records, and text blocks.
-* **Lettuce 6.4 or later.** The demo's `pom.xml` pins 7.5.2.RELEASE.
+* **Lettuce 6.4 or later.** The demo's `pom.xml` pins 7.9.0.RELEASE.
Field-level TTL bindings (`hexpire`, `httl`, `hpersist`) ship from
Lettuce 6.4.
diff --git a/content/develop/use-cases/feature-store/java-lettuce/pom.xml b/content/develop/use-cases/feature-store/java-lettuce/pom.xml
index 7b926a0096..f32ec15258 100644
--- a/content/develop/use-cases/feature-store/java-lettuce/pom.xml
+++ b/content/develop/use-cases/feature-store/java-lettuce/pom.xml
@@ -30,11 +30,11 @@
+ (hexpire / httl / hpersist); the demo pins 7.9.0.RELEASE. -->
io.lettuce
lettuce-core
- 7.5.2.RELEASE
+ 7.9.0.RELEASE
diff --git a/content/develop/use-cases/recommendation-engine/java-lettuce/Recommender.java b/content/develop/use-cases/recommendation-engine/java-lettuce/Recommender.java
index b18260e3d4..146ce85c5d 100644
--- a/content/develop/use-cases/recommendation-engine/java-lettuce/Recommender.java
+++ b/content/develop/use-cases/recommendation-engine/java-lettuce/Recommender.java
@@ -19,6 +19,7 @@
import io.lettuce.core.output.NestedMultiOutput;
import io.lettuce.core.protocol.CommandArgs;
import io.lettuce.core.protocol.ProtocolKeyword;
+import io.lettuce.core.search.FieldValue;
import io.lettuce.core.search.SearchReply;
import io.lettuce.core.search.arguments.CreateArgs;
import io.lettuce.core.search.arguments.FieldArgs;
@@ -161,19 +162,19 @@ public String userKey(String userId) {
* {@code FT.SEARCH} can pre-filter and then KNN-rank in one pass.
*/
public void createIndex() {
- List> schema = Arrays.asList(
- TextFieldArgs.builder().name("name").weight(1L).build(),
+ List schema = Arrays.asList(
+ TextFieldArgs.builder().name("name").weight(1L).build(),
// Lettuce's TextField weight is a long: 1 == default weight,
// we accept the loss of the 0.5 fractional weight on
// description because Lettuce 7 does not expose a double
// weight builder.
- TextFieldArgs.builder().name("description").build(),
- TagFieldArgs.builder().name("category").build(),
- TagFieldArgs.builder().name("brand").build(),
- TagFieldArgs.builder().name("in_stock").build(),
- NumericFieldArgs.builder().name("price").sortable().build(),
- NumericFieldArgs.builder().name("rating").sortable().build(),
- VectorFieldArgs.builder()
+ TextFieldArgs.builder().name("description").build(),
+ TagFieldArgs.builder().name("category").build(),
+ TagFieldArgs.builder().name("brand").build(),
+ TagFieldArgs.builder().name("in_stock").build(),
+ NumericFieldArgs.builder().name("price").sortable().build(),
+ NumericFieldArgs.builder().name("rating").sortable().build(),
+ VectorFieldArgs.builder()
.name("embedding")
.hnsw()
.type(VectorFieldArgs.VectorType.FLOAT32)
@@ -181,7 +182,7 @@ public void createIndex() {
.distanceMetric(VectorFieldArgs.DistanceMetric.COSINE)
.build());
- CreateArgs createArgs = CreateArgs.builder()
+ CreateArgs createArgs = CreateArgs.builder()
.on(CreateArgs.TargetType.HASH)
.withPrefix(keyPrefix)
.build();
@@ -205,15 +206,23 @@ public void dropIndex(boolean deleteDocuments) {
try {
connection.sync().ftDropindex(indexName, deleteDocuments);
} catch (RedisCommandExecutionException exc) {
- String msg = String.valueOf(exc.getMessage()).toLowerCase();
- // Different Redis Search versions phrase the missing-index
- // error differently; tolerate either.
- if (!msg.contains("no such index") && !msg.contains("unknown index name")) {
+ if (!isMissingIndex(exc)) {
throw exc;
}
}
}
+ /**
+ * Different Redis Search versions phrase the missing-index error
+ * differently ("Unknown index name", "no such index", and, from
+ * Redis 8.10, "SEARCH_INDEX_NOT_FOUND Index not found").
+ */
+ private static boolean isMissingIndex(RedisCommandExecutionException exc) {
+ String msg = String.valueOf(exc.getMessage()).toLowerCase();
+ return msg.contains("no such index") || msg.contains("unknown index name")
+ || msg.contains("index not found");
+ }
+
// ------------------------------------------------------------------
// Catalogue ingest
// ------------------------------------------------------------------
@@ -325,7 +334,7 @@ public List candidateRetrieve(float[] queryVec, RetrieveOptions opts)
// parameter goes through as raw bytes and the returned hash
// field values come back as byte[] (we UTF-8-decode the text
// fields ourselves below).
- SearchArgs args = SearchArgs.builder()
+ SearchArgs args = SearchArgs.builder()
.param("vec", floatsToBytes(effective))
.returnField("name")
.returnField("description")
@@ -335,39 +344,37 @@ public List candidateRetrieve(float[] queryVec, RetrieveOptions opts)
.returnField("rating")
.returnField("in_stock")
.returnField("vector_score")
- .sortBy(SortByArgs.builder().attribute("vector_score").build())
+ .sortBy(SortByArgs.builder().attribute("vector_score").build())
.limit(0, k)
.dialect(QueryDialects.DIALECT2)
.build();
- // ftSearch's V (value type) is byte[] on the binary connection,
- // so the query expression has to be supplied as UTF-8 bytes too.
- SearchReply reply = binConnection.sync()
- .ftSearch(indexName, utf8(queryStr), args);
- List> results =
+ SearchReply reply = binConnection.sync()
+ .ftSearch(indexName, queryStr, args);
+ List> results =
reply == null ? Collections.emptyList() : reply.getResults();
List out = new ArrayList<>(results.size());
- for (SearchReply.SearchResult doc : results) {
+ for (SearchReply.SearchResult doc : results) {
out.add(decodeCandidate(doc));
}
return out;
}
- private Candidate decodeCandidate(SearchReply.SearchResult doc) {
+ private Candidate decodeCandidate(SearchReply.SearchResult doc) {
String rawKey = doc.getId();
String bareId = rawKey.startsWith(keyPrefix)
? rawKey.substring(keyPrefix.length()) : rawKey;
- Map fields = doc.getFields();
- double dist = parseDouble(fromUtf8(fields.get("vector_score")), 0.0);
- double price = parseDouble(fromUtf8(fields.get("price")), 0.0);
- double rating = parseDouble(fromUtf8(fields.get("rating")), 0.0);
- boolean inStock = "true".equals(fromUtf8(fields.get("in_stock")));
+ Map fields = doc.getFields();
+ double dist = parseDouble(text(fields, "vector_score"), 0.0);
+ double price = parseDouble(text(fields, "price"), 0.0);
+ double rating = parseDouble(text(fields, "rating"), 0.0);
+ boolean inStock = "true".equals(text(fields, "in_stock"));
Candidate c = new Candidate();
c.id = bareId;
- c.name = fromUtf8(fields.get("name"));
- c.description = fromUtf8(fields.get("description"));
- c.category = fromUtf8(fields.get("category"));
- c.brand = fromUtf8(fields.get("brand"));
+ c.name = stringOrEmpty(text(fields, "name"));
+ c.description = stringOrEmpty(text(fields, "description"));
+ c.category = stringOrEmpty(text(fields, "category"));
+ c.brand = stringOrEmpty(text(fields, "brand"));
c.price = price;
c.rating = rating;
c.inStock = inStock;
@@ -695,8 +702,7 @@ public IndexStats indexInfo() {
Object vecMb = info.get("vector_index_sz_mb");
stats.vectorIndexSizeMb = vecMb == null ? 0.0 : parseDouble(vecMb.toString(), 0.0);
} catch (RedisCommandExecutionException exc) {
- String msg = String.valueOf(exc.getMessage()).toLowerCase();
- if (!msg.contains("no such index") && !msg.contains("unknown index name")) {
+ if (!isMissingIndex(exc)) {
throw exc;
}
}
@@ -709,32 +715,32 @@ public IndexStats indexInfo() {
*/
public List listProducts(int limit) {
if (limit <= 0) limit = 100;
- SearchArgs args = SearchArgs.builder()
+ SearchArgs args = SearchArgs.builder()
.returnField("name")
.returnField("category")
.returnField("brand")
.returnField("price")
.returnField("rating")
.returnField("in_stock")
- .sortBy(SortByArgs.builder().attribute("price").build())
+ .sortBy(SortByArgs.builder().attribute("price").build())
.limit(0, limit)
.dialect(QueryDialects.DIALECT2)
.build();
- SearchReply reply = connection.sync().ftSearch(indexName, "*", args);
- List> results =
+ SearchReply reply = connection.sync().ftSearch(indexName, "*", args);
+ List> results =
reply == null ? Collections.emptyList() : reply.getResults();
List out = new ArrayList<>(results.size());
- for (SearchReply.SearchResult doc : results) {
+ for (SearchReply.SearchResult doc : results) {
ProductSummary p = new ProductSummary();
String rawKey = doc.getId();
p.id = rawKey.startsWith(keyPrefix) ? rawKey.substring(keyPrefix.length()) : rawKey;
- Map fields = doc.getFields();
- p.name = stringOrEmpty(fields.get("name"));
- p.category = stringOrEmpty(fields.get("category"));
- p.brand = stringOrEmpty(fields.get("brand"));
- p.price = parseDouble(fields.get("price"), 0.0);
- p.rating = parseDouble(fields.get("rating"), 0.0);
- p.inStock = "true".equals(fields.get("in_stock"));
+ Map fields = doc.getFields();
+ p.name = stringOrEmpty(text(fields, "name"));
+ p.category = stringOrEmpty(text(fields, "category"));
+ p.brand = stringOrEmpty(text(fields, "brand"));
+ p.price = parseDouble(text(fields, "price"), 0.0);
+ p.rating = parseDouble(text(fields, "rating"), 0.0);
+ p.inStock = "true".equals(text(fields, "in_stock"));
out.add(p);
}
return out;
@@ -851,6 +857,12 @@ private static String stringOrEmpty(Object value) {
return value == null ? "" : value.toString();
}
+ /** The field's value as a string, or null if the document doesn't have it. */
+ private static String text(Map fields, String key) {
+ FieldValue value = fields.get(key);
+ return value == null ? null : value.asString();
+ }
+
private static byte[] utf8(String value) {
return value == null ? new byte[0] : value.getBytes(StandardCharsets.UTF_8);
}
diff --git a/content/develop/use-cases/recommendation-engine/java-lettuce/_index.md b/content/develop/use-cases/recommendation-engine/java-lettuce/_index.md
index 7c031bcc12..0cd4bcad2a 100644
--- a/content/develop/use-cases/recommendation-engine/java-lettuce/_index.md
+++ b/content/develop/use-cases/recommendation-engine/java-lettuce/_index.md
@@ -178,23 +178,22 @@ FT.SEARCH recommend:idx
When there's no filter, the pre-filter clause is just `(*)`. `vector_score` is the cosine *distance* (0 means identical, 2 means opposite), so the result is sorted ascending and the top row is the closest candidate to the query.
-The Lettuce equivalent uses the typed `ftSearch` method on the binary-codec connection's `RediSearchCommands`. The query expression is passed as the value type (`byte[]` on the binary connection, so UTF-8-encoded) and the binary `$vec` parameter goes through `SearchArgs.Builder.param(...)`:
+The Lettuce equivalent uses the typed `ftSearch` method on the binary-codec connection's `RediSearchCommands`. The query expression is an ordinary `String`, and the binary `$vec` parameter goes through `SearchArgs.Builder.param(...)`:
```java
String knn = filterClause + "=>[KNN " + k + " @embedding $vec AS vector_score]";
-SearchArgs args = SearchArgs.builder()
+SearchArgs args = SearchArgs.builder()
.param("vec", Recommender.floatsToBytes(queryVec))
.returnField("name").returnField("description")
.returnField("category").returnField("brand")
.returnField("price").returnField("rating")
.returnField("in_stock").returnField("vector_score")
- .sortBy(SortByArgs.builder().attribute("vector_score").build())
+ .sortBy(SortByArgs.builder().attribute("vector_score").build())
.limit(0, k)
.dialect(QueryDialects.DIALECT2)
.build();
-SearchReply reply = binConn.sync()
- .ftSearch("recommend:idx",
- knn.getBytes(StandardCharsets.UTF_8), args);
+SearchReply reply = binConn.sync()
+ .ftSearch("recommend:idx", knn, args);
```
## Lettuce specifics: binary fields and pipelining
diff --git a/content/develop/use-cases/recommendation-engine/java-lettuce/pom.xml b/content/develop/use-cases/recommendation-engine/java-lettuce/pom.xml
index 24837d2f8e..34795549d0 100644
--- a/content/develop/use-cases/recommendation-engine/java-lettuce/pom.xml
+++ b/content/develop/use-cases/recommendation-engine/java-lettuce/pom.xml
@@ -33,7 +33,7 @@
io.lettuce
lettuce-core
- 7.5.2.RELEASE
+ 7.9.0.RELEASE