diff --git a/.gitignore b/.gitignore index 46efce0db5..25dbdf105b 100644 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,5 @@ dependency-reduced-pom.xml *.java-version *.DS_Store prDescription.md -.vscode* \ No newline at end of file +.vscode* +.augment diff --git a/docs/user-guide/redis-search.md b/docs/user-guide/redis-search.md index b5ddabaa0d..59ee6adf06 100644 --- a/docs/user-guide/redis-search.md +++ b/docs/user-guide/redis-search.md @@ -26,18 +26,19 @@ Redis Search operates on **indexes** that define how your data should be searcha RedisURI redisURI = RedisURI.Builder.redis("localhost").withPort(6379).build(); RedisClient redisClient = RedisClient.create(redisURI); StatefulRedisConnection connection = redisClient.connect(); -RediSearchCommands search = connection.sync(); +RedisCommands redis = connection.sync(); +RediSearchCommands search = redis; ``` ### Creating Your First Index ```java // Define searchable fields -List> fields = Arrays.asList( - TextFieldArgs.builder().name("title").build(), - TextFieldArgs.builder().name("content").build(), - NumericFieldArgs.builder().name("price").sortable().build(), - TagFieldArgs.builder().name("category").sortable().build() +List fields = Arrays.asList( + TextFieldArgs.builder().name("title").build(), + TextFieldArgs.builder().name("content").build(), + NumericFieldArgs.builder().name("price").sortable().build(), + TagFieldArgs.builder().name("category").sortable().build() ); // Create the index @@ -49,20 +50,18 @@ String result = search.ftCreate("products-idx", fields); ```java // Add documents as Redis hashes -Map product1 = Map.of( - "title", "Wireless Headphones", - "content", "High-quality wireless headphones with noise cancellation", - "price", "199.99", - "category", "electronics" -); +Map product1 = new HashMap<>(); +product1.put("title", "Wireless Headphones"); +product1.put("content", "High-quality wireless headphones with noise cancellation"); +product1.put("price", "199.99"); +product1.put("category", "electronics"); redis.hmset("product:1", product1); -Map product2 = Map.of( - "title", "Running Shoes", - "content", "Comfortable running shoes for daily exercise", - "price", "89.99", - "category", "sports" -); +Map product2 = new HashMap<>(); +product2.put("title", "Running Shoes"); +product2.put("content", "Comfortable running shoes for daily exercise"); +product2.put("price", "89.99"); +product2.put("category", "sports"); redis.hmset("product:2", product2); ``` @@ -70,12 +69,12 @@ redis.hmset("product:2", product2); ```java // Simple text search -SearchReply results = search.ftSearch("products-idx", "wireless"); +SearchReply results = search.ftSearch("products-idx", "wireless"); // Access results System.out.println("Found " + results.getCount() + " documents"); -for (SearchReply.SearchResult result : results.getResults()) { - System.out.println("Key: " + result.getKey()); +for (SearchReply.SearchResult result : results.getResults()) { + System.out.println("Key: " + result.getId()); System.out.println("Title: " + result.getFields().get("title")); } ``` @@ -86,9 +85,9 @@ for (SearchReply.SearchResult result : results.getResults()) { Full-text searchable fields with stemming, phonetic matching, and scoring. ```java -TextFieldArgs titleField = TextFieldArgs.builder() +TextFieldArgs titleField = TextFieldArgs.builder() .name("title") - .weight(2.0) // Boost importance in scoring + .weight(2) // Boost importance in scoring .sortable() // Enable sorting .noStem() // Disable stemming .phonetic(TextFieldArgs.PhoneticMatcher.ENGLISH) // Enable phonetic matching @@ -99,7 +98,7 @@ TextFieldArgs titleField = TextFieldArgs.builder() For range queries and sorting on numeric values. ```java -NumericFieldArgs priceField = NumericFieldArgs.builder() +NumericFieldArgs priceField = NumericFieldArgs.builder() .name("price") .sortable() // Enable sorting .noIndex() // Don't index for search, only for sorting @@ -110,7 +109,7 @@ NumericFieldArgs priceField = NumericFieldArgs.builder() For exact matching and faceted search. ```java -TagFieldArgs categoryField = TagFieldArgs.builder() +TagFieldArgs categoryField = TagFieldArgs.builder() .name("category") .separator(",") // Custom separator for multiple tags .sortable() @@ -121,7 +120,7 @@ TagFieldArgs categoryField = TagFieldArgs.builder() For location-based queries. ```java -GeoFieldArgs locationField = GeoFieldArgs.builder() +GeoFieldArgs locationField = GeoFieldArgs.builder() .name("location") .build(); ``` @@ -130,12 +129,12 @@ GeoFieldArgs locationField = GeoFieldArgs.builder() For semantic search and similarity matching. ```java -VectorFieldArgs embeddingField = VectorFieldArgs.builder() +VectorFieldArgs embeddingField = VectorFieldArgs.builder() .name("embedding") - .algorithm(VectorAlgorithm.FLAT) - .type(VectorType.FLOAT32) - .dimension(768) - .distanceMetric(DistanceMetric.COSINE) + .algorithm(VectorFieldArgs.Algorithm.FLAT) + .type(VectorFieldArgs.VectorType.FLOAT32) + .dimensions(768) + .distanceMetric(VectorFieldArgs.DistanceMetric.COSINE) .build(); ``` @@ -144,20 +143,20 @@ VectorFieldArgs embeddingField = VectorFieldArgs.builder() ### Index with Custom Settings ```java -CreateArgs createArgs = CreateArgs.builder() +CreateArgs createArgs = CreateArgs.builder() .on(CreateArgs.TargetType.HASH) // Index HASH documents .withPrefix("product:") // Only index keys with this prefix - .language("english") // Default language for text processing + .defaultLanguage(DocumentLanguage.ENGLISH) // Default language for text processing .languageField("lang") // Field containing document language - .score(0.5) // Default document score + .defaultScore(0.5) // Default document score .scoreField("popularity") // Field containing document score .maxTextFields() // Allow unlimited text fields .temporary(3600) // Auto-expire index after 1 hour .noOffsets() // Disable term offset storage .noHighlighting() // Disable highlighting .noFields() // Don't store field contents - .noFreqs() // Don't store term frequencies - .stopwords("the", "a", "an") // Custom stopwords + .noFrequency() // Don't store term frequencies + .stopWords(Arrays.asList("the", "a", "an")) // Custom stopwords .build(); String result = search.ftCreate("advanced-idx", createArgs, fields); @@ -166,15 +165,15 @@ String result = search.ftCreate("advanced-idx", createArgs, fields); ### JSON Document Indexing ```java -CreateArgs jsonArgs = CreateArgs.builder() +CreateArgs jsonArgs = CreateArgs.builder() .on(CreateArgs.TargetType.JSON) - .prefix("user:") + .withPrefix("user:") .build(); -List> jsonFields = Arrays.asList( - TextFieldArgs.builder().name("$.name").as("name").build(), - NumericFieldArgs.builder().name("$.age").as("age").build(), - TagFieldArgs.builder().name("$.tags[*]").as("tags").build() +List jsonFields = Arrays.asList( + TextFieldArgs.builder().name("$.name").as("name").build(), + NumericFieldArgs.builder().name("$.age").as("age").build(), + TagFieldArgs.builder().name("$.tags[*]").as("tags").build() ); search.ftCreate("users-idx", jsonArgs, jsonFields); @@ -217,31 +216,29 @@ search.ftSearch("products-idx", "@price:[100 +inf]"); // Open range ### Advanced Search Options ```java -SearchArgs searchArgs = SearchArgs.builder() +SearchArgs searchArgs = SearchArgs.builder() .limit(0, 10) // Pagination: offset 0, limit 10 - .sortBy("price", SortDirection.ASC) // Sort by price ascending - .returnFields("title", "price") // Only return specific fields - .highlightFields("title", "content") // Highlight specific fields + .sortBy(SortByArgs.builder().attribute("price").build()) // Sort by price ascending + .returnField("title").returnField("price") // Only return specific fields + .highlightField("title").highlightField("content") // Highlight specific fields .highlightTags("", "") // Custom highlight tags - .summarizeFields("content") // Summarize specific fields - .summarizeFrags(3) // Number of summary fragments + .summarizeField("content") // Summarize specific fields + .summarizeFragments(3) // Number of summary fragments .summarizeLen(50) // Summary length .scorer(ScoringFunction.TF_IDF) // Scoring algorithm - .explainScore() // Include score explanation .withScores() // Include document scores .noContent() // Don't return document content .verbatim() // Don't use stemming - .noStopwords() // Don't filter stopwords .withSortKeys() // Include sort key values - .inKeys("product:1", "product:2") // Search only specific keys - .inFields("title", "content") // Search only specific fields + .inKey("product:1").inKey("product:2") // Search only specific keys + .inField("title").inField("content") // Search only specific fields .slop(2) // Allow term reordering - .timeout(5000) // Query timeout in milliseconds - .params("category", "electronics") // Query parameters + .timeout(Duration.ofSeconds(5)) // Query timeout + .param("category", "electronics") // Query parameter .dialect(QueryDialects.DIALECT2) // Query dialect version .build(); -SearchReply results = search.ftSearch("products-idx", "@title:$category", searchArgs); +SearchReply results = search.ftSearch("products-idx", "@title:$category", searchArgs); ``` ## Vector Search @@ -251,15 +248,15 @@ Vector search enables semantic similarity matching using machine learning embedd ### Creating a Vector Index ```java -List> vectorFields = Arrays.asList( - TextFieldArgs.builder().name("title").build(), - VectorFieldArgs.builder() +List vectorFields = Arrays.asList( + TextFieldArgs.builder().name("title").build(), + VectorFieldArgs.builder() .name("embedding") - .algorithm(VectorAlgorithm.FLAT) // or VectorAlgorithm.HNSW - .type(VectorType.FLOAT32) - .dimension(768) // Vector dimension - .distanceMetric(DistanceMetric.COSINE) // COSINE, L2, or IP - .initialCapacity(1000) // Initial vector capacity + .algorithm(VectorFieldArgs.Algorithm.FLAT) // or VectorFieldArgs.Algorithm.HNSW + .type(VectorFieldArgs.VectorType.FLOAT32) + .dimensions(768) // Vector dimension + .distanceMetric(VectorFieldArgs.DistanceMetric.COSINE) // COSINE, L2, or IP + .attribute("INITIAL_CAP", 1000) // Initial vector capacity .build() ); @@ -273,10 +270,9 @@ search.ftCreate("semantic-idx", vectorFields); float[] embedding = textToEmbedding("wireless headphones"); String embeddingStr = Arrays.toString(embedding); -Map doc = Map.of( - "title", "Wireless Headphones", - "embedding", embeddingStr -); +Map doc = new HashMap<>(); +doc.put("title", "Wireless Headphones"); +doc.put("embedding", embeddingStr); redis.hmset("doc:1", doc); ``` @@ -286,15 +282,20 @@ redis.hmset("doc:1", doc); // Find similar documents using vector search float[] queryVector = textToEmbedding("bluetooth audio device"); String vectorQuery = "*=>[KNN 10 @embedding $query_vec AS score]"; +ByteBuffer queryVectorBuffer = ByteBuffer.allocate(queryVector.length * Float.BYTES) + .order(ByteOrder.LITTLE_ENDIAN); +for (float value : queryVector) { + queryVectorBuffer.putFloat(value); +} -SearchArgs vectorArgs = SearchArgs.builder() - .params("query_vec", Arrays.toString(queryVector)) - .sortBy("score", SortDirection.ASC) - .returnFields("title", "score") +SearchArgs vectorArgs = SearchArgs.builder() + .param("query_vec", queryVectorBuffer.array()) + .sortBy(SortByArgs.builder().attribute("score").build()) + .returnField("title").returnField("score") .dialect(QueryDialects.DIALECT2) .build(); -SearchReply results = search.ftSearch("semantic-idx", vectorQuery, vectorArgs); +SearchReply results = search.ftSearch("semantic-idx", vectorQuery, vectorArgs); ``` ## Geospatial Search @@ -304,9 +305,9 @@ Search for documents based on geographic location. ### Creating a Geo Index ```java -List> geoFields = Arrays.asList( - TextFieldArgs.builder().name("name").build(), - GeoFieldArgs.builder().name("location").build() +List geoFields = Arrays.asList( + TextFieldArgs.builder().name("name").build(), + GeoFieldArgs.builder().name("location").build() ); search.ftCreate("places-idx", geoFields); @@ -315,25 +316,17 @@ search.ftCreate("places-idx", geoFields); ### Adding Geo Data ```java -Map place = Map.of( - "name", "Central Park", - "location", "40.7829,-73.9654" // lat,lon format -); +Map place = new HashMap<>(); +place.put("name", "Central Park"); +place.put("location", "40.7829,-73.9654"); // lat,lon format redis.hmset("place:1", place); ``` ### Geo Queries ```java -// Find places within radius -SearchArgs geoArgs = SearchArgs.builder() - .geoFilter("location", 40.7829, -73.9654, 5, GeoUnit.KM) - .build(); - -SearchReply nearbyPlaces = search.ftSearch("places-idx", "*", geoArgs); - -// Geo query in search string -SearchReply results = search.ftSearch("places-idx", +// Find places within a 5 km radius +SearchReply results = search.ftSearch("places-idx", "@location:[40.7829 -73.9654 5 km]"); ``` @@ -345,13 +338,13 @@ Aggregations provide powerful analytics capabilities for processing search resul ```java // Simple aggregation without pipeline operations -AggregationReply results = search.ftAggregate("products-idx", "*"); +AggregationReply results = search.ftAggregate("products-idx", "*"); ``` ### Advanced Aggregation Pipeline ```java -AggregateArgs aggArgs = AggregateArgs.builder() +AggregateArgs aggArgs = AggregateArgs.builder() // Load specific fields .load("title").load("price").load("category") @@ -362,15 +355,15 @@ AggregateArgs aggArgs = AggregateArgs.builder() .filter("@price > 50") // Group by category with reducers - .groupBy(GroupBy.of("category") - .reduce(Reducer.count().as("product_count")) - .reduce(Reducer.avg("@price").as("avg_price")) - .reduce(Reducer.sum("@price").as("total_value")) - .reduce(Reducer.min("@price").as("min_price")) - .reduce(Reducer.max("@price").as("max_price"))) + .groupBy(AggregateArgs.GroupBy.of("category") + .reduce(AggregateArgs.Reducer.count().as("product_count")) + .reduce(AggregateArgs.Reducer.avg("@price").as("avg_price")) + .reduce(AggregateArgs.Reducer.sum("@price").as("total_value")) + .reduce(AggregateArgs.Reducer.min("@price").as("min_price")) + .reduce(AggregateArgs.Reducer.max("@price").as("max_price"))) // Sort results - .sortBy("avg_price", SortDirection.DESC) + .sortBy("avg_price", AggregateArgs.SortDirection.DESC) // Limit results .limit(0, 10) @@ -380,16 +373,16 @@ AggregateArgs aggArgs = AggregateArgs.builder() // Set query parameters .verbatim() - .timeout(5000) - .params("min_price", "50") + .timeout(Duration.ofSeconds(5)) + .param("min_price", "50") .dialect(QueryDialects.DIALECT2) .build(); -AggregationReply aggResults = search.ftAggregate("products-idx", "*", aggArgs); +AggregationReply aggResults = search.ftAggregate("products-idx", "*", aggArgs); // Process aggregation results -for (SearchReply reply : aggResults.getReplies()) { - for (SearchReply.SearchResult result : reply.getResults()) { +for (SearchReply reply : aggResults.getReplies()) { + for (SearchReply.SearchResult result : reply.getResults()) { System.out.println("Category: " + result.getFields().get("category")); System.out.println("Count: " + result.getFields().get("product_count")); System.out.println("Avg Price: " + result.getFields().get("avg_price")); @@ -397,12 +390,50 @@ for (SearchReply reply : aggResults.getReplies()) { } ``` +### Collecting Group Entries (COLLECT) + +!!! WARNING + `COLLECT` is an experimental Redis Query Engine feature gated behind the + `search-enable-unstable-features` server configuration. Both the server feature + and the Lettuce API may change. + +The `COLLECT` reducer gathers per-row field projections within each `GROUPBY` group and +returns them as an array of entries under the reducer alias: + +```java +AggregateArgs collectArgs = AggregateArgs.builder() + .groupBy(AggregateArgs.GroupBy.of("category") + .reduce(AggregateArgs.Reducer.collect() + .fields("title", "price") + .sortBy(new AggregateArgs.SortProperty("price", AggregateArgs.SortDirection.DESC)) + .limit(0, 3) + .as("top_products"))) + .build(); + +AggregationReply collectResults = search.ftAggregate("products-idx", "*", collectArgs); +``` + +Unlike other reducers, a `COLLECT` column is not a scalar: its `FieldValue` in +`SearchResult#getFields()` is an array (`FieldValue.Kind.ARRAY`) with one element per +collected entry. Read the entries with `FieldValue#asList()` and each entry with +`FieldValue#asMap()`, which normalizes the protocol-specific entry shape (RESP3 returns +each entry as a map, RESP2 as a flat key/value array) to one map per collected entry: + +```java +for (SearchReply.SearchResult group : collectResults.getReplies().get(0).getResults()) { + for (FieldValue entry : group.getFields().get("top_products").asList()) { + Map product = entry.asMap(); + System.out.println(product.get("title").asString() + ": " + product.get("price").asString()); + } +} +``` + ### Dynamic and Re-entrant Pipelines Redis aggregations support dynamic pipelines where operations can be repeated and applied in any order: ```java -AggregateArgs complexPipeline = AggregateArgs.builder() +AggregateArgs complexPipeline = AggregateArgs.builder() // First transformation .apply("@price * @quantity", "total_value") @@ -410,11 +441,11 @@ AggregateArgs complexPipeline = AggregateArgs.bu .filter("@total_value > 100") // First grouping - .groupBy(GroupBy.of("category") - .reduce(Reducer.sum("@total_value").as("category_revenue"))) + .groupBy(AggregateArgs.GroupBy.of("category") + .reduce(AggregateArgs.Reducer.sum("@total_value").as("category_revenue"))) // First sort - .sortBy("category_revenue", SortDirection.DESC) + .sortBy("category_revenue", AggregateArgs.SortDirection.DESC) // Second transformation .apply("@category_revenue / 1000", "revenue_k") @@ -423,11 +454,11 @@ AggregateArgs complexPipeline = AggregateArgs.bu .filter("@revenue_k > 5") // Second grouping (re-entrant) - .groupBy(GroupBy.of("revenue_k") - .reduce(Reducer.count().as("high_revenue_categories"))) + .groupBy(AggregateArgs.GroupBy.of("revenue_k") + .reduce(AggregateArgs.Reducer.count().as("high_revenue_categories"))) // Second sort (re-entrant) - .sortBy("high_revenue_categories", SortDirection.DESC) + .sortBy("high_revenue_categories", AggregateArgs.SortDirection.DESC) .build(); ``` @@ -437,28 +468,24 @@ AggregateArgs complexPipeline = AggregateArgs.bu For large result sets, use cursors to process data in batches: ```java -AggregateArgs cursorArgs = AggregateArgs.builder() - .groupBy(GroupBy.of("category") - .reduce(Reducer.count().as("count"))) - .withCursor() - .withCursor(1000, 300000) // batch size: 1000, timeout: 5 minutes +AggregateArgs cursorArgs = AggregateArgs.builder() + .groupBy(AggregateArgs.GroupBy.of("category") + .reduce(AggregateArgs.Reducer.count().as("count"))) + .withCursor(AggregateArgs.WithCursor.of(1000L, Duration.ofMinutes(5))) .build(); // Initial aggregation with cursor -AggregationReply firstBatch = search.ftAggregate("products-idx", "*", cursorArgs); -long cursorId = firstBatch.getCursorId(); +AggregationReply firstBatch = search.ftAggregate("products-idx", "*", cursorArgs); +AggregationReply.Cursor cursor = firstBatch.getCursor().orElse(null); // Read subsequent batches -while (cursorId != 0) { - AggregationReply nextBatch = search.ftCursorread("products-idx", cursorId, 500); - cursorId = nextBatch.getCursorId(); +while (cursor != null && cursor.getCursorId() != 0) { + AggregationReply nextBatch = search.ftCursorread("products-idx", cursor, 500); + cursor = nextBatch.getCursor().orElse(null); // Process batch processResults(nextBatch); } - -// Clean up cursor when done -search.ftCursordel("products-idx", cursorId); ``` ## Index Management @@ -492,9 +519,9 @@ search.ftAliasdel("products"); ```java // Add new fields to existing index -List> newFields = Arrays.asList( - TagFieldArgs.builder().name("brand").build(), - NumericFieldArgs.builder().name("rating").build() +List newFields = Arrays.asList( + TagFieldArgs.builder().name("brand").build(), + NumericFieldArgs.builder().name("rating").build() ); search.ftAlter("products-idx", false, newFields); // false = scan existing docs @@ -524,10 +551,8 @@ search.ftSugadd("autocomplete", "bluetooth speakers", 0.8); search.ftSugadd("autocomplete", "noise cancelling earbuds", 0.9); // Add with additional options -SugAddArgs sugArgs = SugAddArgs.builder() - .increment() // Increment score if suggestion exists - .payload("category:electronics") // Additional metadata - .build(); +SugAddArgs sugArgs = SugAddArgs.Builder.incr() // Increment score if suggestion exists + .payload("category:electronics"); // Additional metadata search.ftSugadd("autocomplete", "gaming headset", 0.7, sugArgs); ``` @@ -536,19 +561,17 @@ search.ftSugadd("autocomplete", "gaming headset", 0.7, sugArgs); ```java // Basic suggestion retrieval -List> suggestions = search.ftSugget("autocomplete", "head"); +List suggestions = search.ftSugget("autocomplete", "head"); // Advanced suggestion options -SugGetArgs getArgs = SugGetArgs.builder() - .fuzzy() // Enable fuzzy matching +SugGetArgs getArgs = SugGetArgs.Builder.fuzzy() // Enable fuzzy matching .max(5) // Limit to 5 suggestions .withScores() // Include scores - .withPayloads() // Include payloads - .build(); + .withPayloads(); // Include payloads -List> results = search.ftSugget("autocomplete", "head", getArgs); +List results = search.ftSugget("autocomplete", "head", getArgs); -for (Suggestion suggestion : results) { +for (Suggestion suggestion : results) { System.out.println("Suggestion: " + suggestion.getValue()); System.out.println("Score: " + suggestion.getScore()); System.out.println("Payload: " + suggestion.getPayload()); @@ -571,22 +594,20 @@ Redis Search can suggest corrections for misspelled queries. ```java // Basic spell check -List> corrections = search.ftSpellcheck("products-idx", "wireles hedphones"); +SpellCheckResult corrections = search.ftSpellcheck("products-idx", "wireles hedphones"); // Advanced spell check with options -SpellCheckArgs spellArgs = SpellCheckArgs.builder() - .distance(2) // Maximum Levenshtein distance - .terms("include", "dictionary") // Include terms from dictionary - .terms("exclude", "stopwords") // Exclude stopwords - .dialect(QueryDialects.DIALECT2) - .build(); +SpellCheckArgs spellArgs = SpellCheckArgs.Builder.distance(2) // Maximum Levenshtein distance + .termsInclude("dictionary") // Include custom dictionary terms + .termsExclude("stopwords") // Exclude stopword dictionary terms + .dialect(2); -List> results = search.ftSpellcheck("products-idx", "wireles hedphones", spellArgs); +SpellCheckResult results = search.ftSpellcheck("products-idx", "wireles hedphones", spellArgs); -for (SpellCheckResult result : results) { - System.out.println("Original: " + result.getTerm()); - for (SpellCheckResult.Suggestion suggestion : result.getSuggestions()) { - System.out.println(" Suggestion: " + suggestion.getValue() + " (score: " + suggestion.getScore() + ")"); +for (SpellCheckResult.MisspelledTerm term : results.getMisspelledTerms()) { + System.out.println("Original: " + term.getTerm()); + for (SpellCheckResult.Suggestion suggestion : term.getSuggestions()) { + System.out.println(" Suggestion: " + suggestion.getSuggestion() + " (score: " + suggestion.getScore() + ")"); } } ``` @@ -615,9 +636,7 @@ Create synonym groups for query expansion. search.ftSynupdate("products-idx", "group1", "phone", "smartphone", "mobile"); // Update synonym group (replaces existing) -SynUpdateArgs synArgs = SynUpdateArgs.builder() - .skipInitialScan() // Don't reindex existing documents - .build(); +SynUpdateArgs synArgs = SynUpdateArgs.Builder.skipInitialScan(); // Don't reindex existing documents search.ftSynupdate("products-idx", "group1", synArgs, "phone", "smartphone", "mobile", "cellphone"); @@ -636,9 +655,7 @@ Understand how Redis Search executes your queries: String plan = search.ftExplain("products-idx", "@title:wireless"); // Detailed explanation with dialect -ExplainArgs explainArgs = ExplainArgs.builder() - .dialect(QueryDialects.DIALECT2) - .build(); +ExplainArgs explainArgs = ExplainArgs.Builder.dialect(QueryDialects.DIALECT2); String detailedPlan = search.ftExplain("products-idx", "@title:wireless", explainArgs); System.out.println("Execution plan: " + detailedPlan); @@ -656,8 +673,8 @@ search.ftCreate("products-idx", productFields); search.ftCreate("reviews-idx", reviewFields); // Search each index separately and combine results -SearchReply productResults = search.ftSearch("products-idx", "wireless"); -SearchReply reviewResults = search.ftSearch("reviews-idx", "wireless"); +SearchReply productResults = search.ftSearch("products-idx", "wireless"); +SearchReply reviewResults = search.ftSearch("reviews-idx", "wireless"); // Combine and process results as needed ``` @@ -682,9 +699,9 @@ search.ftDropindex("products-idx-v1"); ```java // Index only documents matching certain criteria -CreateArgs conditionalArgs = CreateArgs.builder() +CreateArgs conditionalArgs = CreateArgs.builder() .on(CreateArgs.TargetType.HASH) - .prefix("product:") + .withPrefix("product:") .filter("@status=='active'") // Only index active products .build(); @@ -701,20 +718,26 @@ search.ftCreate("active-products-idx", conditionalArgs, fields); 4. **Vector Fields**: Choose appropriate algorithm (FLAT vs HNSW) based on use case ```java -// Memory-optimized text field -TextFieldArgs optimizedField = TextFieldArgs.builder() +// Memory-optimized index options +CreateArgs memoryOptimizedArgs = CreateArgs.builder() + .noOffsets() // Disable position tracking + .noHighlighting() // Disable highlighting + .noFrequency() // Disable frequency tracking + .build(); + +TextFieldArgs optimizedField = TextFieldArgs.builder() .name("description") - .noOffsets() // Disable position tracking - .noHL() // Disable highlighting - .noFreqs() // Disable frequency tracking .build(); // Sort-only numeric field -NumericFieldArgs sortField = NumericFieldArgs.builder() +NumericFieldArgs sortField = NumericFieldArgs.builder() .name("timestamp") .sortable() .noIndex() // Don't index for search .build(); + +search.ftCreate("memory-optimized-idx", memoryOptimizedArgs, + Arrays.asList(optimizedField, sortField)); ``` ### Query Optimization @@ -727,7 +750,7 @@ search.ftSearch("idx", "@title:wireless"); // Better than "wireless" search.ftSearch("idx", "@price:[100 200]"); // Better than "@price:>=100 @price:<=200" // Limit result sets appropriately -SearchArgs limitedArgs = SearchArgs.builder() +SearchArgs limitedArgs = SearchArgs.builder() .limit(0, 20) // Don't fetch more than needed .noContent() // Skip content if only metadata needed .build(); @@ -750,7 +773,7 @@ try { } try { - SearchReply results = search.ftSearch("idx", "invalid:query["); + SearchReply results = search.ftSearch("idx", "invalid:query["); } catch (RedisCommandExecutionException e) { if (e.getMessage().contains("Syntax error")) { // Handle query syntax error @@ -792,7 +815,7 @@ public class RedisSearchConfig { } @Bean - public RediSearchCommands rediSearchCommands(RedisClient client) { + public RediSearchCommands rediSearchCommands(RedisClient client) { return client.connect().sync(); } } @@ -801,14 +824,14 @@ public class RedisSearchConfig { public class ProductSearchService { @Autowired - private RediSearchCommands search; + private RediSearchCommands search; public List searchProducts(String query, int page, int size) { - SearchArgs args = SearchArgs.builder() + SearchArgs args = SearchArgs.builder() .limit(page * size, size) .build(); - SearchReply results = search.ftSearch("products-idx", query, args); + SearchReply results = search.ftSearch("products-idx", query, args); return convertToProducts(results); } } @@ -819,9 +842,9 @@ public class ProductSearchService { ```java // Using reactive commands StatefulRedisConnection connection = redisClient.connect(); -RediSearchReactiveCommands reactiveSearch = connection.reactive(); +RediSearchReactiveCommands reactiveSearch = connection.reactive(); -Mono> searchMono = reactiveSearch.ftSearch("products-idx", "wireless"); +Mono> searchMono = reactiveSearch.ftSearch("products-idx", "wireless"); searchMono.subscribe(results -> { System.out.println("Found " + results.getCount() + " results"); @@ -844,7 +867,7 @@ When migrating from older RediSearch versions: ```java // Ensure compatibility with modern features -SearchArgs modernArgs = SearchArgs.builder() +SearchArgs modernArgs = SearchArgs.builder() .dialect(QueryDialects.DIALECT2) // Use latest dialect .build(); ``` diff --git a/src/main/java/io/lettuce/core/AbstractRedisAsyncCommands.java b/src/main/java/io/lettuce/core/AbstractRedisAsyncCommands.java index 167fa29268..0c0e61d77f 100644 --- a/src/main/java/io/lettuce/core/AbstractRedisAsyncCommands.java +++ b/src/main/java/io/lettuce/core/AbstractRedisAsyncCommands.java @@ -122,7 +122,7 @@ public abstract class AbstractRedisAsyncCommands implements RedisAclAsyncC RedisSortedSetAsyncCommands, RedisScriptingAsyncCommands, RedisServerAsyncCommands, RedisHLLAsyncCommands, BaseRedisAsyncCommands, RedisTransactionalAsyncCommands, RedisGeoAsyncCommands, RedisClusterAsyncCommands, RedisJsonAsyncCommands, - RedisVectorSetAsyncCommands, RediSearchAsyncCommands, RedisArrayAsyncCommands, + RedisVectorSetAsyncCommands, RediSearchAsyncCommands, RedisArrayAsyncCommands, RedisBloomFilterAsyncCommands, RedisCuckooFilterAsyncCommands, RedisTopKAsyncCommands { private final StatefulConnection connection; @@ -1684,12 +1684,12 @@ public RedisFuture info(String section) { } @Override - public RedisFuture ftCreate(String index, CreateArgs options, List> fieldArgs) { + public RedisFuture ftCreate(String index, CreateArgs options, List fieldArgs) { return dispatch(searchCommandBuilder.ftCreate(index, options, fieldArgs)); } @Override - public RedisFuture ftCreate(String index, List> fieldArgs) { + public RedisFuture ftCreate(String index, List fieldArgs) { return dispatch(searchCommandBuilder.ftCreate(index, null, fieldArgs)); } @@ -1709,92 +1709,92 @@ public RedisFuture ftAliasdel(String alias) { } @Override - public RedisFuture ftAlter(String index, boolean skipInitialScan, List> fieldArgs) { + public RedisFuture ftAlter(String index, boolean skipInitialScan, List fieldArgs) { return dispatch(searchCommandBuilder.ftAlter(index, skipInitialScan, fieldArgs)); } @Override - public RedisFuture> ftTagvals(String index, String fieldName) { + public RedisFuture> ftTagvals(String index, String fieldName) { return dispatch(searchCommandBuilder.ftTagvals(index, fieldName)); } @Override - public RedisFuture> ftSpellcheck(String index, V query) { + public RedisFuture ftSpellcheck(String index, String query) { return dispatch(searchCommandBuilder.ftSpellcheck(index, query)); } @Override - public RedisFuture> ftSpellcheck(String index, V query, SpellCheckArgs args) { + public RedisFuture ftSpellcheck(String index, String query, SpellCheckArgs args) { return dispatch(searchCommandBuilder.ftSpellcheck(index, query, args)); } @Override - public RedisFuture ftDictadd(String dict, V... terms) { + public RedisFuture ftDictadd(String dict, String... terms) { return dispatch(searchCommandBuilder.ftDictadd(dict, terms)); } @Override - public RedisFuture ftDictdel(String dict, V... terms) { + public RedisFuture ftDictdel(String dict, String... terms) { return dispatch(searchCommandBuilder.ftDictdel(dict, terms)); } @Override - public RedisFuture> ftDictdump(String dict) { + public RedisFuture> ftDictdump(String dict) { return dispatch(searchCommandBuilder.ftDictdump(dict)); } @Override - public RedisFuture ftExplain(String index, V query) { + public RedisFuture ftExplain(String index, String query) { return dispatch(searchCommandBuilder.ftExplain(index, query)); } @Override - public RedisFuture ftExplain(String index, V query, ExplainArgs args) { + public RedisFuture ftExplain(String index, String query, ExplainArgs args) { return dispatch(searchCommandBuilder.ftExplain(index, query, args)); } @Override - public RedisFuture> ftList() { + public RedisFuture> ftList() { return dispatch(searchCommandBuilder.ftList()); } @Override - public RedisFuture>> ftSyndump(String index) { + public RedisFuture>> ftSyndump(String index) { return dispatch(searchCommandBuilder.ftSyndump(index)); } @Override - public RedisFuture ftSynupdate(String index, V synonymGroupId, V... terms) { + public RedisFuture ftSynupdate(String index, String synonymGroupId, String... terms) { return dispatch(searchCommandBuilder.ftSynupdate(index, synonymGroupId, terms)); } @Override - public RedisFuture ftSynupdate(String index, V synonymGroupId, SynUpdateArgs args, V... terms) { + public RedisFuture ftSynupdate(String index, String synonymGroupId, SynUpdateArgs args, String... terms) { return dispatch(searchCommandBuilder.ftSynupdate(index, synonymGroupId, args, terms)); } @Override - public RedisFuture ftSugadd(K key, V string, double score) { - return dispatch(searchCommandBuilder.ftSugadd(key, string, score)); + public RedisFuture ftSugadd(K key, String suggestion, double score) { + return dispatch(searchCommandBuilder.ftSugadd(key, suggestion, score)); } @Override - public RedisFuture ftSugadd(K key, V string, double score, SugAddArgs args) { - return dispatch(searchCommandBuilder.ftSugadd(key, string, score, args)); + public RedisFuture ftSugadd(K key, String suggestion, double score, SugAddArgs args) { + return dispatch(searchCommandBuilder.ftSugadd(key, suggestion, score, args)); } @Override - public RedisFuture ftSugdel(K key, V string) { - return dispatch(searchCommandBuilder.ftSugdel(key, string)); + public RedisFuture ftSugdel(K key, String suggestion) { + return dispatch(searchCommandBuilder.ftSugdel(key, suggestion)); } @Override - public RedisFuture>> ftSugget(K key, V prefix) { + public RedisFuture> ftSugget(K key, String prefix) { return dispatch(searchCommandBuilder.ftSugget(key, prefix)); } @Override - public RedisFuture>> ftSugget(K key, V prefix, SugGetArgs args) { + public RedisFuture> ftSugget(K key, String prefix, SugGetArgs args) { return dispatch(searchCommandBuilder.ftSugget(key, prefix, args)); } @@ -1804,7 +1804,7 @@ public RedisFuture ftSuglen(K key) { } @Override - public RedisFuture ftAlter(String index, List> fieldArgs) { + public RedisFuture ftAlter(String index, List fieldArgs) { return dispatch(searchCommandBuilder.ftAlter(index, false, fieldArgs)); } @@ -1819,32 +1819,32 @@ public RedisFuture ftDropindex(String index) { } @Override - public RedisFuture> ftSearch(String index, V query, SearchArgs args) { + public RedisFuture> ftSearch(String index, String query, SearchArgs args) { return dispatch(searchCommandBuilder.ftSearch(index, query, args)); } @Override - public RedisFuture> ftSearch(String index, V query) { - return dispatch(searchCommandBuilder.ftSearch(index, query, SearchArgs. builder().build())); + public RedisFuture> ftSearch(String index, String query) { + return dispatch(searchCommandBuilder.ftSearch(index, query, SearchArgs. builder().build())); } @Override - public RedisFuture> ftHybrid(String index, HybridArgs args) { + public RedisFuture> ftHybrid(String index, HybridArgs args) { return dispatch(searchCommandBuilder.ftHybrid(index, args)); } @Override - public RedisFuture> ftAggregate(String index, V query, AggregateArgs args) { + public RedisFuture> ftAggregate(String index, String query, AggregateArgs args) { return dispatch(searchCommandBuilder.ftAggregate(index, query, args)); } @Override - public RedisFuture> ftAggregate(String index, V query) { + public RedisFuture> ftAggregate(String index, String query) { return dispatch(searchCommandBuilder.ftAggregate(index, query, null)); } @Override - public RedisFuture> ftCursorread(String index, Cursor cursor, int count) { + public RedisFuture> ftCursorread(String index, Cursor cursor, int count) { if (cursor == null) { throw new IllegalArgumentException("cursor must not be null"); } @@ -1853,7 +1853,7 @@ public RedisFuture> ftCursorread(String index, Cursor cur } @Override - public RedisFuture> ftCursorread(String index, Cursor cursor) { + public RedisFuture> ftCursorread(String index, Cursor cursor) { return ftCursorread(index, cursor, -1); } diff --git a/src/main/java/io/lettuce/core/AbstractRedisReactiveCommands.java b/src/main/java/io/lettuce/core/AbstractRedisReactiveCommands.java index 9c104e4bef..725ba2b3cc 100644 --- a/src/main/java/io/lettuce/core/AbstractRedisReactiveCommands.java +++ b/src/main/java/io/lettuce/core/AbstractRedisReactiveCommands.java @@ -129,7 +129,7 @@ public abstract class AbstractRedisReactiveCommands RedisSortedSetReactiveCommands, RedisScriptingReactiveCommands, RedisServerReactiveCommands, RedisHLLReactiveCommands, BaseRedisReactiveCommands, RedisTransactionalReactiveCommands, RedisGeoReactiveCommands, RedisClusterReactiveCommands, RedisJsonReactiveCommands, - RedisVectorSetReactiveCommands, RediSearchReactiveCommands, RedisArrayReactiveCommands, + RedisVectorSetReactiveCommands, RediSearchReactiveCommands, RedisArrayReactiveCommands, RedisBloomFilterReactiveCommands, RedisCuckooFilterReactiveCommands, RedisTopKReactiveCommands { private final StatefulConnection connection; @@ -1770,12 +1770,12 @@ public Mono info(String section) { } @Override - public Mono ftCreate(String index, CreateArgs options, List> fieldArgs) { + public Mono ftCreate(String index, CreateArgs options, List fieldArgs) { return createMono(() -> searchCommandBuilder.ftCreate(index, options, fieldArgs)); } @Override - public Mono ftCreate(String index, List> fieldArgs) { + public Mono ftCreate(String index, List fieldArgs) { return createMono(() -> searchCommandBuilder.ftCreate(index, null, fieldArgs)); } @@ -1795,92 +1795,92 @@ public Mono ftAliasdel(String alias) { } @Override - public Mono ftAlter(String index, boolean skipInitialScan, List> fieldArgs) { + public Mono ftAlter(String index, boolean skipInitialScan, List fieldArgs) { return createMono(() -> searchCommandBuilder.ftAlter(index, skipInitialScan, fieldArgs)); } @Override - public Flux ftTagvals(String index, String fieldName) { + public Flux ftTagvals(String index, String fieldName) { return createDissolvingFlux(() -> searchCommandBuilder.ftTagvals(index, fieldName)); } @Override - public Mono> ftSpellcheck(String index, V query) { + public Mono ftSpellcheck(String index, String query) { return createMono(() -> searchCommandBuilder.ftSpellcheck(index, query)); } @Override - public Mono> ftSpellcheck(String index, V query, SpellCheckArgs args) { + public Mono ftSpellcheck(String index, String query, SpellCheckArgs args) { return createMono(() -> searchCommandBuilder.ftSpellcheck(index, query, args)); } @Override - public Mono ftDictadd(String dict, V... terms) { + public Mono ftDictadd(String dict, String... terms) { return createMono(() -> searchCommandBuilder.ftDictadd(dict, terms)); } @Override - public Mono ftDictdel(String dict, V... terms) { + public Mono ftDictdel(String dict, String... terms) { return createMono(() -> searchCommandBuilder.ftDictdel(dict, terms)); } @Override - public Flux ftDictdump(String dict) { + public Flux ftDictdump(String dict) { return createDissolvingFlux(() -> searchCommandBuilder.ftDictdump(dict)); } @Override - public Mono ftExplain(String index, V query) { + public Mono ftExplain(String index, String query) { return createMono(() -> searchCommandBuilder.ftExplain(index, query)); } @Override - public Mono ftExplain(String index, V query, ExplainArgs args) { + public Mono ftExplain(String index, String query, ExplainArgs args) { return createMono(() -> searchCommandBuilder.ftExplain(index, query, args)); } @Override - public Flux ftList() { + public Flux ftList() { return createDissolvingFlux(() -> searchCommandBuilder.ftList()); } @Override - public Mono>> ftSyndump(String index) { + public Mono>> ftSyndump(String index) { return createMono(() -> searchCommandBuilder.ftSyndump(index)); } @Override - public Mono ftSynupdate(String index, V synonymGroupId, V... terms) { + public Mono ftSynupdate(String index, String synonymGroupId, String... terms) { return createMono(() -> searchCommandBuilder.ftSynupdate(index, synonymGroupId, terms)); } @Override - public Mono ftSynupdate(String index, V synonymGroupId, SynUpdateArgs args, V... terms) { + public Mono ftSynupdate(String index, String synonymGroupId, SynUpdateArgs args, String... terms) { return createMono(() -> searchCommandBuilder.ftSynupdate(index, synonymGroupId, args, terms)); } @Override - public Mono ftSugadd(K key, V string, double score) { - return createMono(() -> searchCommandBuilder.ftSugadd(key, string, score)); + public Mono ftSugadd(K key, String suggestion, double score) { + return createMono(() -> searchCommandBuilder.ftSugadd(key, suggestion, score)); } @Override - public Mono ftSugadd(K key, V string, double score, SugAddArgs args) { - return createMono(() -> searchCommandBuilder.ftSugadd(key, string, score, args)); + public Mono ftSugadd(K key, String suggestion, double score, SugAddArgs args) { + return createMono(() -> searchCommandBuilder.ftSugadd(key, suggestion, score, args)); } @Override - public Mono ftSugdel(K key, V string) { - return createMono(() -> searchCommandBuilder.ftSugdel(key, string)); + public Mono ftSugdel(K key, String suggestion) { + return createMono(() -> searchCommandBuilder.ftSugdel(key, suggestion)); } @Override - public Flux> ftSugget(K key, V prefix) { + public Flux ftSugget(K key, String prefix) { return createDissolvingFlux(() -> searchCommandBuilder.ftSugget(key, prefix)); } @Override - public Flux> ftSugget(K key, V prefix, SugGetArgs args) { + public Flux ftSugget(K key, String prefix, SugGetArgs args) { return createDissolvingFlux(() -> searchCommandBuilder.ftSugget(key, prefix, args)); } @@ -1890,7 +1890,7 @@ public Mono ftSuglen(K key) { } @Override - public Mono ftAlter(String index, List> fieldArgs) { + public Mono ftAlter(String index, List fieldArgs) { return createMono(() -> searchCommandBuilder.ftAlter(index, false, fieldArgs)); } @@ -1916,32 +1916,32 @@ public Mono ftDropindex(String index) { } @Override - public Mono> ftSearch(String index, V query, SearchArgs args) { + public Mono> ftSearch(String index, String query, SearchArgs args) { return createMono(() -> searchCommandBuilder.ftSearch(index, query, args)); } @Override - public Mono> ftSearch(String index, V query) { - return createMono(() -> searchCommandBuilder.ftSearch(index, query, SearchArgs. builder().build())); + public Mono> ftSearch(String index, String query) { + return createMono(() -> searchCommandBuilder.ftSearch(index, query, SearchArgs. builder().build())); } @Override - public Mono> ftHybrid(String index, HybridArgs args) { + public Mono> ftHybrid(String index, HybridArgs args) { return createMono(() -> searchCommandBuilder.ftHybrid(index, args)); } @Override - public Mono> ftAggregate(String index, V query, AggregateArgs args) { + public Mono> ftAggregate(String index, String query, AggregateArgs args) { return createMono(() -> searchCommandBuilder.ftAggregate(index, query, args)); } @Override - public Mono> ftAggregate(String index, V query) { + public Mono> ftAggregate(String index, String query) { return createMono(() -> searchCommandBuilder.ftAggregate(index, query, null)); } @Override - public Mono> ftCursorread(String index, Cursor cursor, int count) { + public Mono> ftCursorread(String index, Cursor cursor, int count) { return createMono(() -> { if (cursor == null) { throw new IllegalArgumentException("cursor must not be null"); @@ -1952,7 +1952,7 @@ public Mono> ftCursorread(String index, Cursor cursor, in } @Override - public Mono> ftCursorread(String index, Cursor cursor) { + public Mono> ftCursorread(String index, Cursor cursor) { return ftCursorread(index, cursor, -1); } diff --git a/src/main/java/io/lettuce/core/RediSearchCommandBuilder.java b/src/main/java/io/lettuce/core/RediSearchCommandBuilder.java index 1f62e01362..f2c1c76363 100644 --- a/src/main/java/io/lettuce/core/RediSearchCommandBuilder.java +++ b/src/main/java/io/lettuce/core/RediSearchCommandBuilder.java @@ -10,14 +10,16 @@ import java.util.Map; import io.lettuce.core.codec.RedisCodec; +import io.lettuce.core.codec.StringCodec; import io.lettuce.core.internal.LettuceAssert; import io.lettuce.core.output.BooleanOutput; +import io.lettuce.core.output.CommandOutput; import io.lettuce.core.output.ComplexOutput; import io.lettuce.core.output.EncodedComplexOutput; import io.lettuce.core.output.IntegerOutput; import io.lettuce.core.output.StatusOutput; -import io.lettuce.core.output.ValueListOutput; +import io.lettuce.core.output.StringListOutput; import io.lettuce.core.protocol.BaseRedisCommandBuilder; import io.lettuce.core.protocol.Command; import io.lettuce.core.protocol.CommandArgs; @@ -69,7 +71,7 @@ class RediSearchCommandBuilder extends BaseRedisCommandBuilder { * @param fieldArgs the fieldArgs * @return the result of the create command */ - public Command ftCreate(String index, CreateArgs createArgs, List> fieldArgs) { + public Command ftCreate(String index, CreateArgs createArgs, List fieldArgs) { LettuceAssert.notNull(index, "Index must not be null"); notEmpty(fieldArgs.toArray()); @@ -81,7 +83,7 @@ public Command ftCreate(String index, CreateArgs createArgs, args.add(CommandKeyword.SCHEMA); - for (FieldArgs arg : fieldArgs) { + for (FieldArgs arg : fieldArgs) { arg.build(args); } @@ -97,12 +99,12 @@ public Command ftCreate(String index, CreateArgs createArgs, * @param searchArgs the search arguments * @return the result of the search command */ - public Command> ftSearch(String index, V query, SearchArgs searchArgs) { + public Command> ftSearch(String index, String query, SearchArgs searchArgs) { LettuceAssert.notNull(index, "Index must not be null"); LettuceAssert.notNull(query, "Query must not be null"); CommandArgs args = new CommandArgs<>(codec).add(index); - args.addValue(query); + args.add(query); if (searchArgs != null) { searchArgs.build(args); @@ -124,7 +126,7 @@ public Command> ftSearch(String index, V query, SearchAr * @param hybridArgs the hybrid query arguments containing SEARCH and/or VSIM clauses * @return the command */ - public Command> ftHybrid(String index, HybridArgs hybridArgs) { + public Command> ftHybrid(String index, HybridArgs hybridArgs) { LettuceAssert.notNull(index, "Index must not be null"); LettuceAssert.notNull(hybridArgs, "HybridArgs must not be null"); @@ -142,12 +144,12 @@ public Command> ftHybrid(String index, HybridArgs * @param aggregateArgs the aggregate arguments * @return the result of the aggregate command */ - public Command> ftAggregate(String index, V query, AggregateArgs aggregateArgs) { + public Command> ftAggregate(String index, String query, AggregateArgs aggregateArgs) { LettuceAssert.notNull(index, "Index must not be null"); LettuceAssert.notNull(query, "Query must not be null"); CommandArgs args = new CommandArgs<>(codec).add(index); - args.addValue(query); + args.add(query); boolean withCursor = false; @@ -168,7 +170,7 @@ public Command> ftAggregate(String index, V query, * @param count the number of results to read * @return the result of the cursor read command */ - public Command> ftCursorread(String index, long cursorId, int count) { + public Command> ftCursorread(String index, long cursorId, int count) { LettuceAssert.notNull(index, "Index must not be null"); CommandArgs args = new CommandArgs<>(codec).add(CommandKeyword.READ).add(index); @@ -252,7 +254,7 @@ public Command ftAliasdel(String alias) { * @param fieldArgs the field arguments for the new attributes to add * @return the result of the alter command */ - public Command ftAlter(String index, boolean skipInitialScan, List> fieldArgs) { + public Command ftAlter(String index, boolean skipInitialScan, List fieldArgs) { LettuceAssert.notNull(index, "Index must not be null"); notEmpty(fieldArgs.toArray()); @@ -265,7 +267,7 @@ public Command ftAlter(String index, boolean skipInitialScan, List args.add(CommandKeyword.SCHEMA); args.add(CommandKeyword.ADD); - for (FieldArgs arg : fieldArgs) { + for (FieldArgs arg : fieldArgs) { arg.build(args); } @@ -279,13 +281,13 @@ public Command ftAlter(String index, boolean skipInitialScan, List * @param fieldName the name of a Tag field defined in the schema * @return the result of the tagvals command */ - public Command> ftTagvals(String index, String fieldName) { + public Command> ftTagvals(String index, String fieldName) { LettuceAssert.notNull(index, "Index must not be null"); LettuceAssert.notNull(fieldName, "Field name must not be null"); CommandArgs args = new CommandArgs<>(codec).add(index).add(fieldName); - return createCommand(FT_TAGVALS, new ValueListOutput<>(codec), args); + return createCommand(FT_TAGVALS, new StringListOutput<>(codec), args); } /** @@ -295,7 +297,7 @@ public Command> ftTagvals(String index, String fieldName) { * @param query the search query * @return the result of the spellcheck command */ - public Command> ftSpellcheck(String index, V query) { + public Command ftSpellcheck(String index, String query) { return ftSpellcheck(index, query, null); } @@ -307,17 +309,17 @@ public Command> ftSpellcheck(String index, V query) { * @param args the spellcheck arguments * @return the result of the spellcheck command */ - public Command> ftSpellcheck(String index, V query, SpellCheckArgs args) { + public Command ftSpellcheck(String index, String query, SpellCheckArgs args) { LettuceAssert.notNull(index, "Index must not be null"); LettuceAssert.notNull(query, "Query must not be null"); - CommandArgs commandArgs = new CommandArgs<>(codec).add(index).addValue(query); + CommandArgs commandArgs = new CommandArgs<>(codec).add(index).add(query); if (args != null) { args.build(commandArgs); } - SpellCheckResultParser parser = new SpellCheckResultParser<>(codec); + SpellCheckResultParser parser = new SpellCheckResultParser(); return createCommand(FT_SPELLCHECK, new EncodedComplexOutput<>(codec, parser), commandArgs); } @@ -328,16 +330,15 @@ public Command> ftSpellcheck(String index, V query, Sp * @param terms the terms to add to the dictionary * @return the result of the dictadd command */ - @SafeVarargs - public final Command ftDictadd(String dict, V... terms) { + public final Command ftDictadd(String dict, String... terms) { LettuceAssert.notNull(dict, "Dictionary must not be null"); LettuceAssert.notNull(terms, "Terms must not be null"); LettuceAssert.isTrue(terms.length > 0, "At least one term must be provided"); CommandArgs commandArgs = new CommandArgs<>(codec).add(dict); - for (V term : terms) { + for (String term : terms) { LettuceAssert.notNull(term, "Term must not be null"); - commandArgs.addValue(term); + commandArgs.add(term); } return createCommand(FT_DICTADD, new IntegerOutput<>(codec), commandArgs); @@ -350,16 +351,15 @@ public final Command ftDictadd(String dict, V... terms) { * @param terms the terms to delete from the dictionary * @return the result of the dictdel command */ - @SafeVarargs - public final Command ftDictdel(String dict, V... terms) { + public final Command ftDictdel(String dict, String... terms) { LettuceAssert.notNull(dict, "Dictionary must not be null"); LettuceAssert.notNull(terms, "Terms must not be null"); LettuceAssert.isTrue(terms.length > 0, "At least one term must be provided"); CommandArgs commandArgs = new CommandArgs<>(codec).add(dict); - for (V term : terms) { + for (String term : terms) { LettuceAssert.notNull(term, "Term must not be null"); - commandArgs.addValue(term); + commandArgs.add(term); } return createCommand(FT_DICTDEL, new IntegerOutput<>(codec), commandArgs); @@ -371,12 +371,12 @@ public final Command ftDictdel(String dict, V... terms) { * @param dict the dictionary name * @return the result of the dictdump command */ - public Command> ftDictdump(String dict) { + public Command> ftDictdump(String dict) { LettuceAssert.notNull(dict, "Dictionary name must not be null"); CommandArgs commandArgs = new CommandArgs<>(codec).add(dict); - return createCommand(FT_DICTDUMP, new ValueListOutput<>(codec), commandArgs); + return createCommand(FT_DICTDUMP, new StringListOutput<>(codec), commandArgs); } /** @@ -386,7 +386,7 @@ public Command> ftDictdump(String dict) { * @param query the search query * @return the execution plan as a string */ - public Command ftExplain(String index, V query) { + public Command ftExplain(String index, String query) { return ftExplain(index, query, null); } @@ -398,11 +398,11 @@ public Command ftExplain(String index, V query) { * @param args the explain arguments * @return the execution plan as a string */ - public Command ftExplain(String index, V query, ExplainArgs args) { + public Command ftExplain(String index, String query, ExplainArgs args) { LettuceAssert.notNull(index, "Index must not be null"); LettuceAssert.notNull(query, "Query must not be null"); - CommandArgs commandArgs = new CommandArgs<>(codec).add(index).addValue(query); + CommandArgs commandArgs = new CommandArgs<>(codec).add(index).add(query); if (args != null) { args.build(commandArgs); @@ -416,9 +416,9 @@ public Command ftExplain(String index, V query, ExplainArgs * * @return the list of index names */ - public Command> ftList() { + public Command> ftList() { CommandArgs commandArgs = new CommandArgs<>(codec); - return createCommand(FT_LIST, new ValueListOutput<>(codec), commandArgs); + return createCommand(FT_LIST, new StringListOutput<>(codec), commandArgs); } /** @@ -427,12 +427,12 @@ public Command> ftList() { * @param index the index name * @return a map where keys are synonym terms and values are lists of group IDs containing that synonym */ - public Command>> ftSyndump(String index) { + public Command>> ftSyndump(String index) { LettuceAssert.notNull(index, "Index must not be null"); CommandArgs commandArgs = new CommandArgs<>(codec).add(index); - return createCommand(FT_SYNDUMP, new EncodedComplexOutput<>(codec, new SynonymMapParser<>(codec)), commandArgs); + return createCommand(FT_SYNDUMP, new EncodedComplexOutput<>(codec, new SynonymMapParser()), commandArgs); } /** @@ -443,8 +443,7 @@ public Command>> ftSyndump(String index) { * @param terms the terms to add to the synonym group * @return the result of the synupdate command */ - @SafeVarargs - public final Command ftSynupdate(String index, V synonymGroupId, V... terms) { + public final Command ftSynupdate(String index, String synonymGroupId, String... terms) { return ftSynupdate(index, synonymGroupId, null, terms); } @@ -457,22 +456,21 @@ public final Command ftSynupdate(String index, V synonymGroupId, V * @param terms the terms to add to the synonym group * @return the result of the synupdate command */ - @SafeVarargs - public final Command ftSynupdate(String index, V synonymGroupId, SynUpdateArgs args, V... terms) { + public final Command ftSynupdate(String index, String synonymGroupId, SynUpdateArgs args, String... terms) { LettuceAssert.notNull(index, "Index must not be null"); LettuceAssert.notNull(synonymGroupId, "Synonym group ID must not be null"); LettuceAssert.notNull(terms, "Terms must not be null"); LettuceAssert.isTrue(terms.length > 0, "At least one term must be provided"); - CommandArgs commandArgs = new CommandArgs<>(codec).add(index).addValue(synonymGroupId); + CommandArgs commandArgs = new CommandArgs<>(codec).add(index).add(synonymGroupId); if (args != null) { args.build(commandArgs); } - for (V term : terms) { + for (String term : terms) { LettuceAssert.notNull(term, "Term must not be null"); - commandArgs.addValue(term); + commandArgs.add(term); } return createCommand(FT_SYNUPDATE, new StatusOutput<>(codec), commandArgs); @@ -482,28 +480,28 @@ public final Command ftSynupdate(String index, V synonymGroupId, S * Add a suggestion string to an auto-complete suggestion dictionary. * * @param key the suggestion dictionary key - * @param string the suggestion string to index + * @param suggestion the suggestion string to index * @param score the floating point number of the suggestion string's weight * @return the result of the sugadd command */ - public Command ftSugadd(K key, V string, double score) { - return ftSugadd(key, string, score, null); + public Command ftSugadd(K key, String suggestion, double score) { + return ftSugadd(key, suggestion, score, null); } /** * Add a suggestion string to an auto-complete suggestion dictionary. * * @param key the suggestion dictionary key - * @param string the suggestion string to index + * @param suggestion the suggestion string to index * @param score the floating point number of the suggestion string's weight * @param args the suggestion add arguments * @return the result of the sugadd command */ - public Command ftSugadd(K key, V string, double score, SugAddArgs args) { + public Command ftSugadd(K key, String suggestion, double score, SugAddArgs args) { notNullKey(key); - LettuceAssert.notNull(string, "String must not be null"); + LettuceAssert.notNull(suggestion, "Suggestion must not be null"); - CommandArgs commandArgs = new CommandArgs<>(codec).addKey(key).addValue(string).add(score); + CommandArgs commandArgs = new CommandArgs<>(codec).addKey(key).add(suggestion).add(score); if (args != null) { args.build(commandArgs); @@ -516,14 +514,14 @@ public Command ftSugadd(K key, V string, double score, SugAddArgs ftSugdel(K key, V string) { + public Command ftSugdel(K key, String suggestion) { notNullKey(key); - LettuceAssert.notNull(string, "String must not be null"); + LettuceAssert.notNull(suggestion, "Suggestion must not be null"); - CommandArgs commandArgs = new CommandArgs<>(codec).addKey(key).addValue(string); + CommandArgs commandArgs = new CommandArgs<>(codec).addKey(key).add(suggestion); return createCommand(FT_SUGDEL, new BooleanOutput<>(codec), commandArgs); } @@ -535,7 +533,7 @@ public Command ftSugdel(K key, V string) { * @param prefix the prefix to complete on * @return the result of the sugget command */ - public Command>> ftSugget(K key, V prefix) { + public Command> ftSugget(K key, String prefix) { return ftSugget(key, prefix, null); } @@ -547,11 +545,11 @@ public Command>> ftSugget(K key, V prefix) { * @param args the suggestion get arguments * @return the result of the sugget command */ - public Command>> ftSugget(K key, V prefix, SugGetArgs args) { + public Command> ftSugget(K key, String prefix, SugGetArgs args) { notNullKey(key); LettuceAssert.notNull(prefix, "Prefix must not be null"); - CommandArgs commandArgs = new CommandArgs<>(codec).addKey(key).addValue(prefix); + CommandArgs commandArgs = new CommandArgs<>(codec).addKey(key).add(prefix); boolean withScores = false; boolean withPayloads = false; @@ -562,8 +560,8 @@ public Command>> ftSugget(K key, V prefix, SugGetArgs parser = new SuggestionParser<>(withScores, withPayloads); - return createCommand(FT_SUGGET, new ComplexOutput<>(codec, parser), commandArgs); + SuggestionParser parser = new SuggestionParser(withScores, withPayloads); + return createCommand(FT_SUGGET, (CommandOutput) new ComplexOutput<>(StringCodec.UTF8, parser), commandArgs); } /** diff --git a/src/main/java/io/lettuce/core/api/async/RediSearchAsyncCommands.java b/src/main/java/io/lettuce/core/api/async/RediSearchAsyncCommands.java index 3baad417d6..200ea9388f 100644 --- a/src/main/java/io/lettuce/core/api/async/RediSearchAsyncCommands.java +++ b/src/main/java/io/lettuce/core/api/async/RediSearchAsyncCommands.java @@ -31,13 +31,12 @@ * Asynchronous executed commands for RediSearch functionality * * @param Key type. - * @param Value type. * @author Tihomir Mateev * @see RediSearch * @since 6.8 * @generated by io.lettuce.apigenerator.CreateAsyncApi */ -public interface RediSearchAsyncCommands { +public interface RediSearchAsyncCommands { /** * Create a new search index with the given name and field definitions using default settings. @@ -63,7 +62,7 @@ public interface RediSearchAsyncCommands { * @see #ftDropindex(String) */ @Experimental - RedisFuture ftCreate(String index, List> fieldArgs); + RedisFuture ftCreate(String index, List fieldArgs); /** * Create a new search index with the given name, custom configuration, and field definitions. @@ -102,7 +101,7 @@ public interface RediSearchAsyncCommands { * @see #ftDropindex(String) */ @Experimental - RedisFuture ftCreate(String index, CreateArgs arguments, List> fieldArgs); + RedisFuture ftCreate(String index, CreateArgs arguments, List fieldArgs); /** * Add an alias to a search index. @@ -279,7 +278,7 @@ public interface RediSearchAsyncCommands { * @see #ftCreate(String, CreateArgs, List) */ @Experimental - RedisFuture ftAlter(String index, boolean skipInitialScan, List> fieldArgs); + RedisFuture ftAlter(String index, boolean skipInitialScan, List fieldArgs); /** * Add new attributes to an existing search index. @@ -314,7 +313,7 @@ public interface RediSearchAsyncCommands { * @see #ftCreate(String, CreateArgs, List) */ @Experimental - RedisFuture ftAlter(String index, List> fieldArgs); + RedisFuture ftAlter(String index, List fieldArgs); /** * Return a distinct set of values indexed in a Tag field. @@ -370,7 +369,7 @@ public interface RediSearchAsyncCommands { * @see #ftCreate(String, CreateArgs, List) */ @Experimental - RedisFuture> ftTagvals(String index, String fieldName); + RedisFuture> ftTagvals(String index, String fieldName); /** * Perform spelling correction on a query, returning suggestions for misspelled terms. @@ -405,13 +404,13 @@ public interface RediSearchAsyncCommands { * @since 6.8 * @see FT.SPELLCHECK * @see Spellchecking - * @see #ftSpellcheck(String, Object, SpellCheckArgs) - * @see #ftDictadd(String, Object[]) - * @see #ftDictdel(String, Object[]) + * @see #ftSpellcheck(String, String, SpellCheckArgs) + * @see #ftDictadd(String, String[]) + * @see #ftDictdel(String, String[]) * @see #ftDictdump(String) */ @Experimental - RedisFuture> ftSpellcheck(String index, V query); + RedisFuture ftSpellcheck(String index, String query); /** * Perform spelling correction on a query with additional options. @@ -442,13 +441,13 @@ public interface RediSearchAsyncCommands { * @since 6.8 * @see FT.SPELLCHECK * @see Spellchecking - * @see #ftSpellcheck(String, Object) - * @see #ftDictadd(String, Object[]) - * @see #ftDictdel(String, Object[]) + * @see #ftSpellcheck(String, String) + * @see #ftDictadd(String, String[]) + * @see #ftDictdel(String, String[]) * @see #ftDictdump(String) */ @Experimental - RedisFuture> ftSpellcheck(String index, V query, SpellCheckArgs args); + RedisFuture ftSpellcheck(String index, String query, SpellCheckArgs args); /** * Add terms to a dictionary. @@ -478,11 +477,11 @@ public interface RediSearchAsyncCommands { * @since 6.8 * @see FT.DICTADD * @see Spellchecking - * @see #ftDictdel(String, Object[]) + * @see #ftDictdel(String, String[]) * @see #ftDictdump(String) */ @Experimental - RedisFuture ftDictadd(String dict, V... terms); + RedisFuture ftDictadd(String dict, String... terms); /** * Delete terms from a dictionary. @@ -501,11 +500,11 @@ public interface RediSearchAsyncCommands { * @return the number of terms that were deleted * @since 6.8 * @see FT.DICTDEL - * @see #ftDictadd(String, Object[]) + * @see #ftDictadd(String, String[]) * @see #ftDictdump(String) */ @Experimental - RedisFuture ftDictdel(String dict, V... terms); + RedisFuture ftDictdel(String dict, String... terms); /** * Dump all terms in a dictionary. @@ -522,11 +521,11 @@ public interface RediSearchAsyncCommands { * @return a list of all terms in the dictionary * @since 6.8 * @see FT.DICTDUMP - * @see #ftDictadd(String, Object[]) - * @see #ftDictdel(String, Object[]) + * @see #ftDictadd(String, String[]) + * @see #ftDictdel(String, String[]) */ @Experimental - RedisFuture> ftDictdump(String dict); + RedisFuture> ftDictdump(String dict); /** * Return the execution plan for a complex query. @@ -555,11 +554,11 @@ public interface RediSearchAsyncCommands { * @return the execution plan as a string * @since 6.8 * @see FT.EXPLAIN - * @see #ftExplain(String, Object, ExplainArgs) - * @see #ftSearch(String, Object) + * @see #ftExplain(String, String, ExplainArgs) + * @see #ftSearch(String, String) */ @Experimental - RedisFuture ftExplain(String index, V query); + RedisFuture ftExplain(String index, String query); /** * Return the execution plan for a complex query with additional options. @@ -586,11 +585,11 @@ public interface RediSearchAsyncCommands { * @return the execution plan as a string * @since 6.8 * @see FT.EXPLAIN - * @see #ftExplain(String, Object) - * @see #ftSearch(String, Object) + * @see #ftExplain(String, String) + * @see #ftSearch(String, String) */ @Experimental - RedisFuture ftExplain(String index, V query, ExplainArgs args); + RedisFuture ftExplain(String index, String query, ExplainArgs args); /** * Return a list of all existing indexes. @@ -622,11 +621,11 @@ public interface RediSearchAsyncCommands { * @return a list of index names * @since 6.8 * @see FT._LIST - * @see #ftCreate(String, CreateArgs, FieldArgs[]) + * @see #ftCreate(String, CreateArgs, List) * @see #ftDropindex(String) */ @Experimental - RedisFuture> ftList(); + RedisFuture> ftList(); /** * Dump synonym group contents. @@ -654,11 +653,11 @@ public interface RediSearchAsyncCommands { * @return a map where keys are synonym terms and values are lists of group IDs containing that synonym * @since 6.8 * @see FT.SYNDUMP - * @see #ftSynupdate(String, Object, Object[]) - * @see #ftSynupdate(String, Object, SynUpdateArgs, Object[]) + * @see #ftSynupdate(String, String, String[]) + * @see #ftSynupdate(String, String, SynUpdateArgs, String[]) */ @Experimental - RedisFuture>> ftSyndump(String index); + RedisFuture>> ftSyndump(String index); /** * Update a synonym group with additional terms. @@ -688,11 +687,11 @@ public interface RediSearchAsyncCommands { * @return OK if executed correctly * @since 6.8 * @see FT.SYNUPDATE - * @see #ftSynupdate(String, Object, SynUpdateArgs, Object[]) + * @see #ftSynupdate(String, String, SynUpdateArgs, String[]) * @see #ftSyndump(String) */ @Experimental - RedisFuture ftSynupdate(String index, V synonymGroupId, V... terms); + RedisFuture ftSynupdate(String index, String synonymGroupId, String... terms); /** * Update a synonym group with additional terms and options. @@ -720,11 +719,11 @@ public interface RediSearchAsyncCommands { * @return OK if executed correctly * @since 6.8 * @see FT.SYNUPDATE - * @see #ftSynupdate(String, Object, Object[]) + * @see #ftSynupdate(String, String, String[]) * @see #ftSyndump(String) */ @Experimental - RedisFuture ftSynupdate(String index, V synonymGroupId, SynUpdateArgs args, V... terms); + RedisFuture ftSynupdate(String index, String synonymGroupId, SynUpdateArgs args, String... terms); /** * Add a suggestion string to an auto-complete suggestion dictionary. @@ -755,13 +754,13 @@ public interface RediSearchAsyncCommands { * @return the current size of the suggestion dictionary after adding the suggestion * @since 6.8 * @see FT.SUGADD - * @see #ftSugadd(Object, Object, double, SugAddArgs) - * @see #ftSugget(Object, Object) - * @see #ftSugdel(Object, Object) - * @see #ftSuglen(Object) + * @see #ftSugadd(K, String, double, SugAddArgs) + * @see #ftSugget(K, String) + * @see #ftSugdel(K, String) + * @see #ftSuglen(K) */ @Experimental - RedisFuture ftSugadd(K key, V suggestion, double score); + RedisFuture ftSugadd(K key, String suggestion, double score); /** * Add a suggestion string to an auto-complete suggestion dictionary with additional options. @@ -782,13 +781,13 @@ public interface RediSearchAsyncCommands { * @return the current size of the suggestion dictionary after adding the suggestion * @since 6.8 * @see FT.SUGADD - * @see #ftSugadd(Object, Object, double) - * @see #ftSugget(Object, Object, SugGetArgs) - * @see #ftSugdel(Object, Object) - * @see #ftSuglen(Object) + * @see #ftSugadd(K, String, double) + * @see #ftSugget(K, String, SugGetArgs) + * @see #ftSugdel(K, String) + * @see #ftSuglen(K) */ @Experimental - RedisFuture ftSugadd(K key, V suggestion, double score, SugAddArgs args); + RedisFuture ftSugadd(K key, String suggestion, double score, SugAddArgs args); /** * Delete a string from a suggestion dictionary. @@ -807,12 +806,12 @@ public interface RediSearchAsyncCommands { * @return {@code true} if the string was found and deleted, {@code false} otherwise * @since 6.8 * @see FT.SUGDEL - * @see #ftSugadd(Object, Object, double) - * @see #ftSugget(Object, Object) - * @see #ftSuglen(Object) + * @see #ftSugadd(K, String, double) + * @see #ftSugget(K, String) + * @see #ftSuglen(K) */ @Experimental - RedisFuture ftSugdel(K key, V suggestion); + RedisFuture ftSugdel(K key, String suggestion); /** * Get completion suggestions for a prefix. @@ -831,13 +830,13 @@ public interface RediSearchAsyncCommands { * @return a list of suggestions matching the prefix * @since 6.8 * @see FT.SUGGET - * @see #ftSugget(Object, Object, SugGetArgs) - * @see #ftSugadd(Object, Object, double) - * @see #ftSugdel(Object, Object) - * @see #ftSuglen(Object) + * @see #ftSugget(K, String, SugGetArgs) + * @see #ftSugadd(K, String, double) + * @see #ftSugdel(K, String) + * @see #ftSuglen(K) */ @Experimental - RedisFuture>> ftSugget(K key, V prefix); + RedisFuture> ftSugget(K key, String prefix); /** * Get completion suggestions for a prefix with additional options. @@ -857,13 +856,13 @@ public interface RediSearchAsyncCommands { * @return a list of suggestions matching the prefix, optionally with scores and payloads * @since 6.8 * @see FT.SUGGET - * @see #ftSugget(Object, Object) - * @see #ftSugadd(Object, Object, double, SugAddArgs) - * @see #ftSugdel(Object, Object) - * @see #ftSuglen(Object) + * @see #ftSugget(K, String) + * @see #ftSugadd(K, String, double, SugAddArgs) + * @see #ftSugdel(K, String) + * @see #ftSuglen(K) */ @Experimental - RedisFuture>> ftSugget(K key, V prefix, SugGetArgs args); + RedisFuture> ftSugget(K key, String prefix, SugGetArgs args); /** * Get the size of an auto-complete suggestion dictionary. @@ -880,9 +879,9 @@ public interface RediSearchAsyncCommands { * @return the current size of the suggestion dictionary * @since 6.8 * @see FT.SUGLEN - * @see #ftSugadd(Object, Object, double) - * @see #ftSugget(Object, Object) - * @see #ftSugdel(Object, Object) + * @see #ftSugadd(K, String, double) + * @see #ftSugget(K, String) + * @see #ftSugdel(K, String) */ @Experimental RedisFuture ftSuglen(K key); @@ -974,10 +973,10 @@ public interface RediSearchAsyncCommands { * @see Query syntax * @see SearchReply * @see SearchArgs - * @see #ftSearch(String, Object, SearchArgs) + * @see #ftSearch(String, String, SearchArgs) */ @Experimental - RedisFuture> ftSearch(String index, V query); + RedisFuture> ftSearch(String index, String query); /** * Search the index with a textual query using advanced search options and filters. @@ -1025,23 +1024,23 @@ public interface RediSearchAsyncCommands { * @see Advanced concepts * @see SearchReply * @see SearchArgs - * @see #ftSearch(String, Object) + * @see #ftSearch(String, String) */ @Experimental - RedisFuture> ftSearch(String index, V query, SearchArgs args); + RedisFuture> ftSearch(String index, String query, SearchArgs args); /** * Run a search query on an index and perform basic aggregate transformations using default options. * *

* This command executes a search query and applies aggregation operations to transform and analyze the results. Unlike - * {@link #ftSearch(String, Object)}, which returns individual documents, FT.AGGREGATE processes the result set through a + * {@link #ftSearch(String, String)}, which returns individual documents, FT.AGGREGATE processes the result set through a * pipeline of transformations to produce analytical insights, summaries, and computed values. *

* *

* This basic variant uses default aggregation behavior without additional pipeline operations. For advanced aggregations - * with grouping, sorting, filtering, and custom transformations, use {@link #ftAggregate(String, Object, AggregateArgs)}. + * with grouping, sorting, filtering, and custom transformations, use {@link #ftAggregate(String, String, AggregateArgs)}. *

* *

@@ -1067,10 +1066,10 @@ public interface RediSearchAsyncCommands { * "https://redis.io/docs/latest/develop/interact/search-and-query/advanced-concepts/aggregations/">Aggregations * @see SearchReply * @see AggregateArgs - * @see #ftAggregate(String, Object, AggregateArgs) + * @see #ftAggregate(String, String, AggregateArgs) */ @Experimental - RedisFuture> ftAggregate(String index, V query); + RedisFuture> ftAggregate(String index, String query); /** * Run a search query on an index and perform advanced aggregate transformations with a processing pipeline. @@ -1122,18 +1121,18 @@ public interface RediSearchAsyncCommands { * API * @see SearchReply * @see AggregateArgs - * @see #ftAggregate(String, Object) + * @see #ftAggregate(String, String) * @see #ftCursorread(String, Cursor) */ @Experimental - RedisFuture> ftAggregate(String index, V query, AggregateArgs args); + RedisFuture> ftAggregate(String index, String query, AggregateArgs args); /** * Read next results from an existing cursor and optionally override the batch size. * *

* This command is used to read the next batch of results from a cursor that was created by - * {@link #ftAggregate(String, Object, AggregateArgs)} with the {@code WITHCURSOR} option. Cursors provide an efficient way + * {@link #ftAggregate(String, String, AggregateArgs)} with the {@code WITHCURSOR} option. Cursors provide an efficient way * to iterate through large result sets without loading all results into memory at once. *

* @@ -1156,17 +1155,17 @@ public interface RediSearchAsyncCommands { * "https://redis.io/docs/latest/develop/interact/search-and-query/advanced-concepts/aggregations/#cursor-api">Cursor * API * @see AggregationReply - * @see #ftAggregate(String, Object, AggregateArgs) + * @see #ftAggregate(String, String, AggregateArgs) */ @Experimental - RedisFuture> ftCursorread(String index, Cursor cursor, int count); + RedisFuture> ftCursorread(String index, Cursor cursor, int count); /** * Read next results from an existing cursor using the default batch size. * *

* This command is used to read the next batch of results from a cursor created by - * {@link #ftAggregate(String, Object, AggregateArgs)} with the {@code WITHCURSOR} option. This variant uses the default + * {@link #ftAggregate(String, String, AggregateArgs)} with the {@code WITHCURSOR} option. This variant uses the default * batch size that was specified in the original {@code FT.AGGREGATE} command's {@code WITHCURSOR} clause. *

* @@ -1188,16 +1187,16 @@ public interface RediSearchAsyncCommands { * "https://redis.io/docs/latest/develop/interact/search-and-query/advanced-concepts/aggregations/#cursor-api">Cursor * API * @see AggregationReply - * @see #ftAggregate(String, Object, AggregateArgs) + * @see #ftAggregate(String, String, AggregateArgs) */ @Experimental - RedisFuture> ftCursorread(String index, Cursor cursor); + RedisFuture> ftCursorread(String index, Cursor cursor); /** * Delete a cursor and free its associated resources. * *

- * This command is used to explicitly delete a cursor created by {@link #ftAggregate(String, Object, AggregateArgs)} with + * This command is used to explicitly delete a cursor created by {@link #ftAggregate(String, String, AggregateArgs)} with * the {@code WITHCURSOR} option. Deleting a cursor frees up server resources and should be done when you no longer need to * read more results from the cursor. *

@@ -1225,7 +1224,7 @@ public interface RediSearchAsyncCommands { * @see Cursor * API - * @see #ftAggregate(String, Object, AggregateArgs) + * @see #ftAggregate(String, String, AggregateArgs) * @see #ftCursorread(String, Cursor) * @see #ftCursorread(String, Cursor, int) */ @@ -1244,6 +1243,6 @@ public interface RediSearchAsyncCommands { * @since 7.2 */ @Experimental - RedisFuture> ftHybrid(String index, HybridArgs args); + RedisFuture> ftHybrid(String index, HybridArgs args); } diff --git a/src/main/java/io/lettuce/core/api/async/RedisAsyncCommands.java b/src/main/java/io/lettuce/core/api/async/RedisAsyncCommands.java index 835ea3ea83..de227c6a0a 100644 --- a/src/main/java/io/lettuce/core/api/async/RedisAsyncCommands.java +++ b/src/main/java/io/lettuce/core/api/async/RedisAsyncCommands.java @@ -41,7 +41,7 @@ public interface RedisAsyncCommands extends BaseRedisAsyncCommands, RedisScriptingAsyncCommands, RedisServerAsyncCommands, RedisSetAsyncCommands, RedisSortedSetAsyncCommands, RedisStreamAsyncCommands, RedisStringAsyncCommands, RedisTransactionalAsyncCommands, RedisJsonAsyncCommands, RedisVectorSetAsyncCommands, - RediSearchAsyncCommands, RedisArrayAsyncCommands, RedisBloomFilterAsyncCommands, + RediSearchAsyncCommands, RedisArrayAsyncCommands, RedisBloomFilterAsyncCommands, RedisCuckooFilterAsyncCommands, RedisTopKAsyncCommands { /** diff --git a/src/main/java/io/lettuce/core/api/reactive/RediSearchReactiveCommands.java b/src/main/java/io/lettuce/core/api/reactive/RediSearchReactiveCommands.java index abb06677c2..cbdf8e71cc 100644 --- a/src/main/java/io/lettuce/core/api/reactive/RediSearchReactiveCommands.java +++ b/src/main/java/io/lettuce/core/api/reactive/RediSearchReactiveCommands.java @@ -33,13 +33,12 @@ * Reactive executed commands for RediSearch functionality * * @param Key type. - * @param Value type. * @author Tihomir Mateev * @see RediSearch * @since 6.8 * @generated by io.lettuce.apigenerator.CreateReactiveApi */ -public interface RediSearchReactiveCommands { +public interface RediSearchReactiveCommands { /** * Create a new search index with the given name and field definitions using default settings. @@ -65,7 +64,7 @@ public interface RediSearchReactiveCommands { * @see #ftDropindex(String) */ @Experimental - Mono ftCreate(String index, List> fieldArgs); + Mono ftCreate(String index, List fieldArgs); /** * Create a new search index with the given name, custom configuration, and field definitions. @@ -104,7 +103,7 @@ public interface RediSearchReactiveCommands { * @see #ftDropindex(String) */ @Experimental - Mono ftCreate(String index, CreateArgs arguments, List> fieldArgs); + Mono ftCreate(String index, CreateArgs arguments, List fieldArgs); /** * Add an alias to a search index. @@ -281,7 +280,7 @@ public interface RediSearchReactiveCommands { * @see #ftCreate(String, CreateArgs, List) */ @Experimental - Mono ftAlter(String index, boolean skipInitialScan, List> fieldArgs); + Mono ftAlter(String index, boolean skipInitialScan, List fieldArgs); /** * Add new attributes to an existing search index. @@ -316,7 +315,7 @@ public interface RediSearchReactiveCommands { * @see #ftCreate(String, CreateArgs, List) */ @Experimental - Mono ftAlter(String index, List> fieldArgs); + Mono ftAlter(String index, List fieldArgs); /** * Return a distinct set of values indexed in a Tag field. @@ -372,7 +371,7 @@ public interface RediSearchReactiveCommands { * @see #ftCreate(String, CreateArgs, List) */ @Experimental - Flux ftTagvals(String index, String fieldName); + Flux ftTagvals(String index, String fieldName); /** * Perform spelling correction on a query, returning suggestions for misspelled terms. @@ -407,13 +406,13 @@ public interface RediSearchReactiveCommands { * @since 6.8 * @see FT.SPELLCHECK * @see Spellchecking - * @see #ftSpellcheck(String, Object, SpellCheckArgs) - * @see #ftDictadd(String, Object[]) - * @see #ftDictdel(String, Object[]) + * @see #ftSpellcheck(String, String, SpellCheckArgs) + * @see #ftDictadd(String, String[]) + * @see #ftDictdel(String, String[]) * @see #ftDictdump(String) */ @Experimental - Mono> ftSpellcheck(String index, V query); + Mono ftSpellcheck(String index, String query); /** * Perform spelling correction on a query with additional options. @@ -444,13 +443,13 @@ public interface RediSearchReactiveCommands { * @since 6.8 * @see FT.SPELLCHECK * @see Spellchecking - * @see #ftSpellcheck(String, Object) - * @see #ftDictadd(String, Object[]) - * @see #ftDictdel(String, Object[]) + * @see #ftSpellcheck(String, String) + * @see #ftDictadd(String, String[]) + * @see #ftDictdel(String, String[]) * @see #ftDictdump(String) */ @Experimental - Mono> ftSpellcheck(String index, V query, SpellCheckArgs args); + Mono ftSpellcheck(String index, String query, SpellCheckArgs args); /** * Add terms to a dictionary. @@ -480,11 +479,11 @@ public interface RediSearchReactiveCommands { * @since 6.8 * @see FT.DICTADD * @see Spellchecking - * @see #ftDictdel(String, Object[]) + * @see #ftDictdel(String, String[]) * @see #ftDictdump(String) */ @Experimental - Mono ftDictadd(String dict, V... terms); + Mono ftDictadd(String dict, String... terms); /** * Delete terms from a dictionary. @@ -503,11 +502,11 @@ public interface RediSearchReactiveCommands { * @return the number of terms that were deleted * @since 6.8 * @see FT.DICTDEL - * @see #ftDictadd(String, Object[]) + * @see #ftDictadd(String, String[]) * @see #ftDictdump(String) */ @Experimental - Mono ftDictdel(String dict, V... terms); + Mono ftDictdel(String dict, String... terms); /** * Dump all terms in a dictionary. @@ -524,11 +523,11 @@ public interface RediSearchReactiveCommands { * @return a list of all terms in the dictionary * @since 6.8 * @see FT.DICTDUMP - * @see #ftDictadd(String, Object[]) - * @see #ftDictdel(String, Object[]) + * @see #ftDictadd(String, String[]) + * @see #ftDictdel(String, String[]) */ @Experimental - Flux ftDictdump(String dict); + Flux ftDictdump(String dict); /** * Return the execution plan for a complex query. @@ -557,11 +556,11 @@ public interface RediSearchReactiveCommands { * @return the execution plan as a string * @since 6.8 * @see FT.EXPLAIN - * @see #ftExplain(String, Object, ExplainArgs) - * @see #ftSearch(String, Object) + * @see #ftExplain(String, String, ExplainArgs) + * @see #ftSearch(String, String) */ @Experimental - Mono ftExplain(String index, V query); + Mono ftExplain(String index, String query); /** * Return the execution plan for a complex query with additional options. @@ -588,11 +587,11 @@ public interface RediSearchReactiveCommands { * @return the execution plan as a string * @since 6.8 * @see FT.EXPLAIN - * @see #ftExplain(String, Object) - * @see #ftSearch(String, Object) + * @see #ftExplain(String, String) + * @see #ftSearch(String, String) */ @Experimental - Mono ftExplain(String index, V query, ExplainArgs args); + Mono ftExplain(String index, String query, ExplainArgs args); /** * Return a list of all existing indexes. @@ -624,11 +623,11 @@ public interface RediSearchReactiveCommands { * @return a list of index names * @since 6.8 * @see FT._LIST - * @see #ftCreate(String, CreateArgs, FieldArgs[]) + * @see #ftCreate(String, CreateArgs, List) * @see #ftDropindex(String) */ @Experimental - Flux ftList(); + Flux ftList(); /** * Dump synonym group contents. @@ -656,11 +655,11 @@ public interface RediSearchReactiveCommands { * @return a map where keys are synonym terms and values are lists of group IDs containing that synonym * @since 6.8 * @see FT.SYNDUMP - * @see #ftSynupdate(String, Object, Object[]) - * @see #ftSynupdate(String, Object, SynUpdateArgs, Object[]) + * @see #ftSynupdate(String, String, String[]) + * @see #ftSynupdate(String, String, SynUpdateArgs, String[]) */ @Experimental - Mono>> ftSyndump(String index); + Mono>> ftSyndump(String index); /** * Update a synonym group with additional terms. @@ -690,11 +689,11 @@ public interface RediSearchReactiveCommands { * @return OK if executed correctly * @since 6.8 * @see FT.SYNUPDATE - * @see #ftSynupdate(String, Object, SynUpdateArgs, Object[]) + * @see #ftSynupdate(String, String, SynUpdateArgs, String[]) * @see #ftSyndump(String) */ @Experimental - Mono ftSynupdate(String index, V synonymGroupId, V... terms); + Mono ftSynupdate(String index, String synonymGroupId, String... terms); /** * Update a synonym group with additional terms and options. @@ -722,11 +721,11 @@ public interface RediSearchReactiveCommands { * @return OK if executed correctly * @since 6.8 * @see FT.SYNUPDATE - * @see #ftSynupdate(String, Object, Object[]) + * @see #ftSynupdate(String, String, String[]) * @see #ftSyndump(String) */ @Experimental - Mono ftSynupdate(String index, V synonymGroupId, SynUpdateArgs args, V... terms); + Mono ftSynupdate(String index, String synonymGroupId, SynUpdateArgs args, String... terms); /** * Add a suggestion string to an auto-complete suggestion dictionary. @@ -757,13 +756,13 @@ public interface RediSearchReactiveCommands { * @return the current size of the suggestion dictionary after adding the suggestion * @since 6.8 * @see FT.SUGADD - * @see #ftSugadd(Object, Object, double, SugAddArgs) - * @see #ftSugget(Object, Object) - * @see #ftSugdel(Object, Object) - * @see #ftSuglen(Object) + * @see #ftSugadd(K, String, double, SugAddArgs) + * @see #ftSugget(K, String) + * @see #ftSugdel(K, String) + * @see #ftSuglen(K) */ @Experimental - Mono ftSugadd(K key, V suggestion, double score); + Mono ftSugadd(K key, String suggestion, double score); /** * Add a suggestion string to an auto-complete suggestion dictionary with additional options. @@ -784,13 +783,13 @@ public interface RediSearchReactiveCommands { * @return the current size of the suggestion dictionary after adding the suggestion * @since 6.8 * @see FT.SUGADD - * @see #ftSugadd(Object, Object, double) - * @see #ftSugget(Object, Object, SugGetArgs) - * @see #ftSugdel(Object, Object) - * @see #ftSuglen(Object) + * @see #ftSugadd(K, String, double) + * @see #ftSugget(K, String, SugGetArgs) + * @see #ftSugdel(K, String) + * @see #ftSuglen(K) */ @Experimental - Mono ftSugadd(K key, V suggestion, double score, SugAddArgs args); + Mono ftSugadd(K key, String suggestion, double score, SugAddArgs args); /** * Delete a string from a suggestion dictionary. @@ -809,12 +808,12 @@ public interface RediSearchReactiveCommands { * @return {@code true} if the string was found and deleted, {@code false} otherwise * @since 6.8 * @see FT.SUGDEL - * @see #ftSugadd(Object, Object, double) - * @see #ftSugget(Object, Object) - * @see #ftSuglen(Object) + * @see #ftSugadd(K, String, double) + * @see #ftSugget(K, String) + * @see #ftSuglen(K) */ @Experimental - Mono ftSugdel(K key, V suggestion); + Mono ftSugdel(K key, String suggestion); /** * Get completion suggestions for a prefix. @@ -833,13 +832,13 @@ public interface RediSearchReactiveCommands { * @return a list of suggestions matching the prefix * @since 6.8 * @see FT.SUGGET - * @see #ftSugget(Object, Object, SugGetArgs) - * @see #ftSugadd(Object, Object, double) - * @see #ftSugdel(Object, Object) - * @see #ftSuglen(Object) + * @see #ftSugget(K, String, SugGetArgs) + * @see #ftSugadd(K, String, double) + * @see #ftSugdel(K, String) + * @see #ftSuglen(K) */ @Experimental - Flux> ftSugget(K key, V prefix); + Flux ftSugget(K key, String prefix); /** * Get completion suggestions for a prefix with additional options. @@ -859,13 +858,13 @@ public interface RediSearchReactiveCommands { * @return a list of suggestions matching the prefix, optionally with scores and payloads * @since 6.8 * @see FT.SUGGET - * @see #ftSugget(Object, Object) - * @see #ftSugadd(Object, Object, double, SugAddArgs) - * @see #ftSugdel(Object, Object) - * @see #ftSuglen(Object) + * @see #ftSugget(K, String) + * @see #ftSugadd(K, String, double, SugAddArgs) + * @see #ftSugdel(K, String) + * @see #ftSuglen(K) */ @Experimental - Flux> ftSugget(K key, V prefix, SugGetArgs args); + Flux ftSugget(K key, String prefix, SugGetArgs args); /** * Get the size of an auto-complete suggestion dictionary. @@ -882,9 +881,9 @@ public interface RediSearchReactiveCommands { * @return the current size of the suggestion dictionary * @since 6.8 * @see FT.SUGLEN - * @see #ftSugadd(Object, Object, double) - * @see #ftSugget(Object, Object) - * @see #ftSugdel(Object, Object) + * @see #ftSugadd(K, String, double) + * @see #ftSugget(K, String) + * @see #ftSugdel(K, String) */ @Experimental Mono ftSuglen(K key); @@ -976,10 +975,10 @@ public interface RediSearchReactiveCommands { * @see Query syntax * @see SearchReply * @see SearchArgs - * @see #ftSearch(String, Object, SearchArgs) + * @see #ftSearch(String, String, SearchArgs) */ @Experimental - Mono> ftSearch(String index, V query); + Mono> ftSearch(String index, String query); /** * Search the index with a textual query using advanced search options and filters. @@ -1027,23 +1026,23 @@ public interface RediSearchReactiveCommands { * @see Advanced concepts * @see SearchReply * @see SearchArgs - * @see #ftSearch(String, Object) + * @see #ftSearch(String, String) */ @Experimental - Mono> ftSearch(String index, V query, SearchArgs args); + Mono> ftSearch(String index, String query, SearchArgs args); /** * Run a search query on an index and perform basic aggregate transformations using default options. * *

* This command executes a search query and applies aggregation operations to transform and analyze the results. Unlike - * {@link #ftSearch(String, Object)}, which returns individual documents, FT.AGGREGATE processes the result set through a + * {@link #ftSearch(String, String)}, which returns individual documents, FT.AGGREGATE processes the result set through a * pipeline of transformations to produce analytical insights, summaries, and computed values. *

* *

* This basic variant uses default aggregation behavior without additional pipeline operations. For advanced aggregations - * with grouping, sorting, filtering, and custom transformations, use {@link #ftAggregate(String, Object, AggregateArgs)}. + * with grouping, sorting, filtering, and custom transformations, use {@link #ftAggregate(String, String, AggregateArgs)}. *

* *

@@ -1069,10 +1068,10 @@ public interface RediSearchReactiveCommands { * "https://redis.io/docs/latest/develop/interact/search-and-query/advanced-concepts/aggregations/">Aggregations * @see SearchReply * @see AggregateArgs - * @see #ftAggregate(String, Object, AggregateArgs) + * @see #ftAggregate(String, String, AggregateArgs) */ @Experimental - Mono> ftAggregate(String index, V query); + Mono> ftAggregate(String index, String query); /** * Run a search query on an index and perform advanced aggregate transformations with a processing pipeline. @@ -1124,18 +1123,18 @@ public interface RediSearchReactiveCommands { * API * @see SearchReply * @see AggregateArgs - * @see #ftAggregate(String, Object) + * @see #ftAggregate(String, String) * @see #ftCursorread(String, Cursor) */ @Experimental - Mono> ftAggregate(String index, V query, AggregateArgs args); + Mono> ftAggregate(String index, String query, AggregateArgs args); /** * Read next results from an existing cursor and optionally override the batch size. * *

* This command is used to read the next batch of results from a cursor that was created by - * {@link #ftAggregate(String, Object, AggregateArgs)} with the {@code WITHCURSOR} option. Cursors provide an efficient way + * {@link #ftAggregate(String, String, AggregateArgs)} with the {@code WITHCURSOR} option. Cursors provide an efficient way * to iterate through large result sets without loading all results into memory at once. *

* @@ -1151,24 +1150,24 @@ public interface RediSearchReactiveCommands { * @param index the index name * @param cursor the cursor obtained from a previous {@code FT.AGGREGATE} or {@code FT.CURSOR READ} command * @param count the number of results to read; overrides the {@code COUNT} from {@code FT.AGGREGATE} - * @return a {@link Mono} emitting the next batch of results; see {@link AggregationReply} + * @return the next batch of results; see {@link AggregationReply} * @since 6.8 * @see FT.CURSOR READ * @see Cursor * API * @see AggregationReply - * @see #ftAggregate(String, Object, AggregateArgs) + * @see #ftAggregate(String, String, AggregateArgs) */ @Experimental - Mono> ftCursorread(String index, Cursor cursor, int count); + Mono> ftCursorread(String index, Cursor cursor, int count); /** * Read next results from an existing cursor using the default batch size. * *

* This command is used to read the next batch of results from a cursor created by - * {@link #ftAggregate(String, Object, AggregateArgs)} with the {@code WITHCURSOR} option. This variant uses the default + * {@link #ftAggregate(String, String, AggregateArgs)} with the {@code WITHCURSOR} option. This variant uses the default * batch size that was specified in the original {@code FT.AGGREGATE} command's {@code WITHCURSOR} clause. *

* @@ -1190,16 +1189,16 @@ public interface RediSearchReactiveCommands { * "https://redis.io/docs/latest/develop/interact/search-and-query/advanced-concepts/aggregations/#cursor-api">Cursor * API * @see AggregationReply - * @see #ftAggregate(String, Object, AggregateArgs) + * @see #ftAggregate(String, String, AggregateArgs) */ @Experimental - Mono> ftCursorread(String index, Cursor cursor); + Mono> ftCursorread(String index, Cursor cursor); /** * Delete a cursor and free its associated resources. * *

- * This command is used to explicitly delete a cursor created by {@link #ftAggregate(String, Object, AggregateArgs)} with + * This command is used to explicitly delete a cursor created by {@link #ftAggregate(String, String, AggregateArgs)} with * the {@code WITHCURSOR} option. Deleting a cursor frees up server resources and should be done when you no longer need to * read more results from the cursor. *

@@ -1221,13 +1220,13 @@ public interface RediSearchReactiveCommands { * * @param index the index name, as a key * @param cursor the cursor obtained from a previous {@code FT.AGGREGATE} or {@code FT.CURSOR READ} command - * @return a {@link Mono} emitting {@code "OK"} if the cursor was successfully deleted + * @return {@code "OK"} if the cursor was successfully deleted * @since 6.8 * @see FT.CURSOR DEL * @see Cursor * API - * @see #ftAggregate(String, Object, AggregateArgs) + * @see #ftAggregate(String, String, AggregateArgs) * @see #ftCursorread(String, Cursor) * @see #ftCursorread(String, Cursor, int) */ @@ -1246,6 +1245,6 @@ public interface RediSearchReactiveCommands { * @since 7.2 */ @Experimental - Mono> ftHybrid(String index, HybridArgs args); + Mono> ftHybrid(String index, HybridArgs args); } diff --git a/src/main/java/io/lettuce/core/api/reactive/RedisReactiveCommands.java b/src/main/java/io/lettuce/core/api/reactive/RedisReactiveCommands.java index 4198c95e12..1b89a98689 100644 --- a/src/main/java/io/lettuce/core/api/reactive/RedisReactiveCommands.java +++ b/src/main/java/io/lettuce/core/api/reactive/RedisReactiveCommands.java @@ -40,7 +40,7 @@ public interface RedisReactiveCommands extends BaseRedisReactiveCommands, RedisScriptingReactiveCommands, RedisServerReactiveCommands, RedisSetReactiveCommands, RedisSortedSetReactiveCommands, RedisStreamReactiveCommands, RedisStringReactiveCommands, RedisTransactionalReactiveCommands, RedisJsonReactiveCommands, - RedisVectorSetReactiveCommands, RediSearchReactiveCommands, RedisArrayReactiveCommands, + RedisVectorSetReactiveCommands, RediSearchReactiveCommands, RedisArrayReactiveCommands, RedisBloomFilterReactiveCommands, RedisCuckooFilterReactiveCommands, RedisTopKReactiveCommands { /** diff --git a/src/main/java/io/lettuce/core/api/sync/RediSearchCommands.java b/src/main/java/io/lettuce/core/api/sync/RediSearchCommands.java index d505f79f65..2c68081b9e 100644 --- a/src/main/java/io/lettuce/core/api/sync/RediSearchCommands.java +++ b/src/main/java/io/lettuce/core/api/sync/RediSearchCommands.java @@ -31,13 +31,12 @@ * Synchronous executed commands for RediSearch functionality * * @param Key type. - * @param Value type. * @author Tihomir Mateev * @see RediSearch * @since 6.8 * @generated by io.lettuce.apigenerator.CreateSyncApi */ -public interface RediSearchCommands { +public interface RediSearchCommands { /** * Create a new search index with the given name and field definitions using default settings. @@ -63,7 +62,7 @@ public interface RediSearchCommands { * @see #ftDropindex(String) */ @Experimental - String ftCreate(String index, List> fieldArgs); + String ftCreate(String index, List fieldArgs); /** * Create a new search index with the given name, custom configuration, and field definitions. @@ -102,7 +101,7 @@ public interface RediSearchCommands { * @see #ftDropindex(String) */ @Experimental - String ftCreate(String index, CreateArgs arguments, List> fieldArgs); + String ftCreate(String index, CreateArgs arguments, List fieldArgs); /** * Add an alias to a search index. @@ -279,7 +278,7 @@ public interface RediSearchCommands { * @see #ftCreate(String, CreateArgs, List) */ @Experimental - String ftAlter(String index, boolean skipInitialScan, List> fieldArgs); + String ftAlter(String index, boolean skipInitialScan, List fieldArgs); /** * Add new attributes to an existing search index. @@ -314,7 +313,7 @@ public interface RediSearchCommands { * @see #ftCreate(String, CreateArgs, List) */ @Experimental - String ftAlter(String index, List> fieldArgs); + String ftAlter(String index, List fieldArgs); /** * Return a distinct set of values indexed in a Tag field. @@ -370,7 +369,7 @@ public interface RediSearchCommands { * @see #ftCreate(String, CreateArgs, List) */ @Experimental - List ftTagvals(String index, String fieldName); + List ftTagvals(String index, String fieldName); /** * Perform spelling correction on a query, returning suggestions for misspelled terms. @@ -405,13 +404,13 @@ public interface RediSearchCommands { * @since 6.8 * @see FT.SPELLCHECK * @see Spellchecking - * @see #ftSpellcheck(String, Object, SpellCheckArgs) - * @see #ftDictadd(String, Object[]) - * @see #ftDictdel(String, Object[]) + * @see #ftSpellcheck(String, String, SpellCheckArgs) + * @see #ftDictadd(String, String[]) + * @see #ftDictdel(String, String[]) * @see #ftDictdump(String) */ @Experimental - SpellCheckResult ftSpellcheck(String index, V query); + SpellCheckResult ftSpellcheck(String index, String query); /** * Perform spelling correction on a query with additional options. @@ -442,13 +441,13 @@ public interface RediSearchCommands { * @since 6.8 * @see FT.SPELLCHECK * @see Spellchecking - * @see #ftSpellcheck(String, Object) - * @see #ftDictadd(String, Object[]) - * @see #ftDictdel(String, Object[]) + * @see #ftSpellcheck(String, String) + * @see #ftDictadd(String, String[]) + * @see #ftDictdel(String, String[]) * @see #ftDictdump(String) */ @Experimental - SpellCheckResult ftSpellcheck(String index, V query, SpellCheckArgs args); + SpellCheckResult ftSpellcheck(String index, String query, SpellCheckArgs args); /** * Add terms to a dictionary. @@ -478,11 +477,11 @@ public interface RediSearchCommands { * @since 6.8 * @see FT.DICTADD * @see Spellchecking - * @see #ftDictdel(String, Object[]) + * @see #ftDictdel(String, String[]) * @see #ftDictdump(String) */ @Experimental - Long ftDictadd(String dict, V... terms); + Long ftDictadd(String dict, String... terms); /** * Delete terms from a dictionary. @@ -501,11 +500,11 @@ public interface RediSearchCommands { * @return the number of terms that were deleted * @since 6.8 * @see FT.DICTDEL - * @see #ftDictadd(String, Object[]) + * @see #ftDictadd(String, String[]) * @see #ftDictdump(String) */ @Experimental - Long ftDictdel(String dict, V... terms); + Long ftDictdel(String dict, String... terms); /** * Dump all terms in a dictionary. @@ -522,11 +521,11 @@ public interface RediSearchCommands { * @return a list of all terms in the dictionary * @since 6.8 * @see FT.DICTDUMP - * @see #ftDictadd(String, Object[]) - * @see #ftDictdel(String, Object[]) + * @see #ftDictadd(String, String[]) + * @see #ftDictdel(String, String[]) */ @Experimental - List ftDictdump(String dict); + List ftDictdump(String dict); /** * Return the execution plan for a complex query. @@ -555,11 +554,11 @@ public interface RediSearchCommands { * @return the execution plan as a string * @since 6.8 * @see FT.EXPLAIN - * @see #ftExplain(String, Object, ExplainArgs) - * @see #ftSearch(String, Object) + * @see #ftExplain(String, String, ExplainArgs) + * @see #ftSearch(String, String) */ @Experimental - String ftExplain(String index, V query); + String ftExplain(String index, String query); /** * Return the execution plan for a complex query with additional options. @@ -586,11 +585,11 @@ public interface RediSearchCommands { * @return the execution plan as a string * @since 6.8 * @see FT.EXPLAIN - * @see #ftExplain(String, Object) - * @see #ftSearch(String, Object) + * @see #ftExplain(String, String) + * @see #ftSearch(String, String) */ @Experimental - String ftExplain(String index, V query, ExplainArgs args); + String ftExplain(String index, String query, ExplainArgs args); /** * Return a list of all existing indexes. @@ -622,11 +621,11 @@ public interface RediSearchCommands { * @return a list of index names * @since 6.8 * @see FT._LIST - * @see #ftCreate(String, CreateArgs, FieldArgs[]) + * @see #ftCreate(String, CreateArgs, List) * @see #ftDropindex(String) */ @Experimental - List ftList(); + List ftList(); /** * Dump synonym group contents. @@ -654,11 +653,11 @@ public interface RediSearchCommands { * @return a map where keys are synonym terms and values are lists of group IDs containing that synonym * @since 6.8 * @see FT.SYNDUMP - * @see #ftSynupdate(String, Object, Object[]) - * @see #ftSynupdate(String, Object, SynUpdateArgs, Object[]) + * @see #ftSynupdate(String, String, String[]) + * @see #ftSynupdate(String, String, SynUpdateArgs, String[]) */ @Experimental - Map> ftSyndump(String index); + Map> ftSyndump(String index); /** * Update a synonym group with additional terms. @@ -688,11 +687,11 @@ public interface RediSearchCommands { * @return OK if executed correctly * @since 6.8 * @see FT.SYNUPDATE - * @see #ftSynupdate(String, Object, SynUpdateArgs, Object[]) + * @see #ftSynupdate(String, String, SynUpdateArgs, String[]) * @see #ftSyndump(String) */ @Experimental - String ftSynupdate(String index, V synonymGroupId, V... terms); + String ftSynupdate(String index, String synonymGroupId, String... terms); /** * Update a synonym group with additional terms and options. @@ -720,11 +719,11 @@ public interface RediSearchCommands { * @return OK if executed correctly * @since 6.8 * @see FT.SYNUPDATE - * @see #ftSynupdate(String, Object, Object[]) + * @see #ftSynupdate(String, String, String[]) * @see #ftSyndump(String) */ @Experimental - String ftSynupdate(String index, V synonymGroupId, SynUpdateArgs args, V... terms); + String ftSynupdate(String index, String synonymGroupId, SynUpdateArgs args, String... terms); /** * Add a suggestion string to an auto-complete suggestion dictionary. @@ -755,13 +754,13 @@ public interface RediSearchCommands { * @return the current size of the suggestion dictionary after adding the suggestion * @since 6.8 * @see FT.SUGADD - * @see #ftSugadd(Object, Object, double, SugAddArgs) - * @see #ftSugget(Object, Object) - * @see #ftSugdel(Object, Object) - * @see #ftSuglen(Object) + * @see #ftSugadd(K, String, double, SugAddArgs) + * @see #ftSugget(K, String) + * @see #ftSugdel(K, String) + * @see #ftSuglen(K) */ @Experimental - Long ftSugadd(K key, V suggestion, double score); + Long ftSugadd(K key, String suggestion, double score); /** * Add a suggestion string to an auto-complete suggestion dictionary with additional options. @@ -782,13 +781,13 @@ public interface RediSearchCommands { * @return the current size of the suggestion dictionary after adding the suggestion * @since 6.8 * @see FT.SUGADD - * @see #ftSugadd(Object, Object, double) - * @see #ftSugget(Object, Object, SugGetArgs) - * @see #ftSugdel(Object, Object) - * @see #ftSuglen(Object) + * @see #ftSugadd(K, String, double) + * @see #ftSugget(K, String, SugGetArgs) + * @see #ftSugdel(K, String) + * @see #ftSuglen(K) */ @Experimental - Long ftSugadd(K key, V suggestion, double score, SugAddArgs args); + Long ftSugadd(K key, String suggestion, double score, SugAddArgs args); /** * Delete a string from a suggestion dictionary. @@ -807,12 +806,12 @@ public interface RediSearchCommands { * @return {@code true} if the string was found and deleted, {@code false} otherwise * @since 6.8 * @see FT.SUGDEL - * @see #ftSugadd(Object, Object, double) - * @see #ftSugget(Object, Object) - * @see #ftSuglen(Object) + * @see #ftSugadd(K, String, double) + * @see #ftSugget(K, String) + * @see #ftSuglen(K) */ @Experimental - Boolean ftSugdel(K key, V suggestion); + Boolean ftSugdel(K key, String suggestion); /** * Get completion suggestions for a prefix. @@ -831,13 +830,13 @@ public interface RediSearchCommands { * @return a list of suggestions matching the prefix * @since 6.8 * @see FT.SUGGET - * @see #ftSugget(Object, Object, SugGetArgs) - * @see #ftSugadd(Object, Object, double) - * @see #ftSugdel(Object, Object) - * @see #ftSuglen(Object) + * @see #ftSugget(K, String, SugGetArgs) + * @see #ftSugadd(K, String, double) + * @see #ftSugdel(K, String) + * @see #ftSuglen(K) */ @Experimental - List> ftSugget(K key, V prefix); + List ftSugget(K key, String prefix); /** * Get completion suggestions for a prefix with additional options. @@ -857,13 +856,13 @@ public interface RediSearchCommands { * @return a list of suggestions matching the prefix, optionally with scores and payloads * @since 6.8 * @see FT.SUGGET - * @see #ftSugget(Object, Object) - * @see #ftSugadd(Object, Object, double, SugAddArgs) - * @see #ftSugdel(Object, Object) - * @see #ftSuglen(Object) + * @see #ftSugget(K, String) + * @see #ftSugadd(K, String, double, SugAddArgs) + * @see #ftSugdel(K, String) + * @see #ftSuglen(K) */ @Experimental - List> ftSugget(K key, V prefix, SugGetArgs args); + List ftSugget(K key, String prefix, SugGetArgs args); /** * Get the size of an auto-complete suggestion dictionary. @@ -880,9 +879,9 @@ public interface RediSearchCommands { * @return the current size of the suggestion dictionary * @since 6.8 * @see FT.SUGLEN - * @see #ftSugadd(Object, Object, double) - * @see #ftSugget(Object, Object) - * @see #ftSugdel(Object, Object) + * @see #ftSugadd(K, String, double) + * @see #ftSugget(K, String) + * @see #ftSugdel(K, String) */ @Experimental Long ftSuglen(K key); @@ -974,10 +973,10 @@ public interface RediSearchCommands { * @see Query syntax * @see SearchReply * @see SearchArgs - * @see #ftSearch(String, Object, SearchArgs) + * @see #ftSearch(String, String, SearchArgs) */ @Experimental - SearchReply ftSearch(String index, V query); + SearchReply ftSearch(String index, String query); /** * Search the index with a textual query using advanced search options and filters. @@ -1025,23 +1024,23 @@ public interface RediSearchCommands { * @see Advanced concepts * @see SearchReply * @see SearchArgs - * @see #ftSearch(String, Object) + * @see #ftSearch(String, String) */ @Experimental - SearchReply ftSearch(String index, V query, SearchArgs args); + SearchReply ftSearch(String index, String query, SearchArgs args); /** * Run a search query on an index and perform basic aggregate transformations using default options. * *

* This command executes a search query and applies aggregation operations to transform and analyze the results. Unlike - * {@link #ftSearch(String, Object)}, which returns individual documents, FT.AGGREGATE processes the result set through a + * {@link #ftSearch(String, String)}, which returns individual documents, FT.AGGREGATE processes the result set through a * pipeline of transformations to produce analytical insights, summaries, and computed values. *

* *

* This basic variant uses default aggregation behavior without additional pipeline operations. For advanced aggregations - * with grouping, sorting, filtering, and custom transformations, use {@link #ftAggregate(String, Object, AggregateArgs)}. + * with grouping, sorting, filtering, and custom transformations, use {@link #ftAggregate(String, String, AggregateArgs)}. *

* *

@@ -1067,10 +1066,10 @@ public interface RediSearchCommands { * "https://redis.io/docs/latest/develop/interact/search-and-query/advanced-concepts/aggregations/">Aggregations * @see SearchReply * @see AggregateArgs - * @see #ftAggregate(String, Object, AggregateArgs) + * @see #ftAggregate(String, String, AggregateArgs) */ @Experimental - AggregationReply ftAggregate(String index, V query); + AggregationReply ftAggregate(String index, String query); /** * Run a search query on an index and perform advanced aggregate transformations with a processing pipeline. @@ -1122,18 +1121,18 @@ public interface RediSearchCommands { * API * @see SearchReply * @see AggregateArgs - * @see #ftAggregate(String, Object) + * @see #ftAggregate(String, String) * @see #ftCursorread(String, Cursor) */ @Experimental - AggregationReply ftAggregate(String index, V query, AggregateArgs args); + AggregationReply ftAggregate(String index, String query, AggregateArgs args); /** * Read next results from an existing cursor and optionally override the batch size. * *

* This command is used to read the next batch of results from a cursor that was created by - * {@link #ftAggregate(String, Object, AggregateArgs)} with the {@code WITHCURSOR} option. Cursors provide an efficient way + * {@link #ftAggregate(String, String, AggregateArgs)} with the {@code WITHCURSOR} option. Cursors provide an efficient way * to iterate through large result sets without loading all results into memory at once. *

* @@ -1156,17 +1155,17 @@ public interface RediSearchCommands { * "https://redis.io/docs/latest/develop/interact/search-and-query/advanced-concepts/aggregations/#cursor-api">Cursor * API * @see AggregationReply - * @see #ftAggregate(String, Object, AggregateArgs) + * @see #ftAggregate(String, String, AggregateArgs) */ @Experimental - AggregationReply ftCursorread(String index, Cursor cursor, int count); + AggregationReply ftCursorread(String index, Cursor cursor, int count); /** * Read next results from an existing cursor using the default batch size. * *

* This command is used to read the next batch of results from a cursor created by - * {@link #ftAggregate(String, Object, AggregateArgs)} with the {@code WITHCURSOR} option. This variant uses the default + * {@link #ftAggregate(String, String, AggregateArgs)} with the {@code WITHCURSOR} option. This variant uses the default * batch size that was specified in the original {@code FT.AGGREGATE} command's {@code WITHCURSOR} clause. *

* @@ -1188,16 +1187,16 @@ public interface RediSearchCommands { * "https://redis.io/docs/latest/develop/interact/search-and-query/advanced-concepts/aggregations/#cursor-api">Cursor * API * @see AggregationReply - * @see #ftAggregate(String, Object, AggregateArgs) + * @see #ftAggregate(String, String, AggregateArgs) */ @Experimental - AggregationReply ftCursorread(String index, Cursor cursor); + AggregationReply ftCursorread(String index, Cursor cursor); /** * Delete a cursor and free its associated resources. * *

- * This command is used to explicitly delete a cursor created by {@link #ftAggregate(String, Object, AggregateArgs)} with + * This command is used to explicitly delete a cursor created by {@link #ftAggregate(String, String, AggregateArgs)} with * the {@code WITHCURSOR} option. Deleting a cursor frees up server resources and should be done when you no longer need to * read more results from the cursor. *

@@ -1225,7 +1224,7 @@ public interface RediSearchCommands { * @see Cursor * API - * @see #ftAggregate(String, Object, AggregateArgs) + * @see #ftAggregate(String, String, AggregateArgs) * @see #ftCursorread(String, Cursor) * @see #ftCursorread(String, Cursor, int) */ @@ -1244,6 +1243,6 @@ public interface RediSearchCommands { * @since 7.2 */ @Experimental - HybridReply ftHybrid(String index, HybridArgs args); + HybridReply ftHybrid(String index, HybridArgs args); } diff --git a/src/main/java/io/lettuce/core/api/sync/RedisCommands.java b/src/main/java/io/lettuce/core/api/sync/RedisCommands.java index b7eaea9e7f..3374a6eed6 100644 --- a/src/main/java/io/lettuce/core/api/sync/RedisCommands.java +++ b/src/main/java/io/lettuce/core/api/sync/RedisCommands.java @@ -38,7 +38,7 @@ public interface RedisCommands extends BaseRedisCommands, RedisAclCo RedisFunctionCommands, RedisGeoCommands, RedisHashCommands, RedisHLLCommands, RedisKeyCommands, RedisListCommands, RedisScriptingCommands, RedisServerCommands, RedisSetCommands, RedisSortedSetCommands, RedisStreamCommands, RedisStringCommands, - RedisTransactionalCommands, RedisJsonCommands, RedisVectorSetCommands, RediSearchCommands, + RedisTransactionalCommands, RedisJsonCommands, RedisVectorSetCommands, RediSearchCommands, RedisArrayCommands, RedisBloomFilterCommands, RedisCuckooFilterCommands, RedisTopKCommands { /** diff --git a/src/main/java/io/lettuce/core/cluster/RedisAdvancedClusterAsyncCommandsImpl.java b/src/main/java/io/lettuce/core/cluster/RedisAdvancedClusterAsyncCommandsImpl.java index 1ab805b05a..c1355cd052 100644 --- a/src/main/java/io/lettuce/core/cluster/RedisAdvancedClusterAsyncCommandsImpl.java +++ b/src/main/java/io/lettuce/core/cluster/RedisAdvancedClusterAsyncCommandsImpl.java @@ -775,7 +775,7 @@ public RedisFuture scan(KeyStreamingChannel channel, ScanCu } @Override - public RedisFuture> ftAggregate(String index, V query, AggregateArgs args) { + public RedisFuture> ftAggregate(String index, String query, AggregateArgs args) { return routeKeyless(() -> super.ftAggregate(index, query, args), (nodeId, conn) -> conn.ftAggregate(index, query, args).thenApply(reply -> { if (reply != null) { @@ -786,68 +786,68 @@ public RedisFuture> ftAggregate(String index, V query, Ag } @Override - public RedisFuture> ftAggregate(String index, V query) { + public RedisFuture> ftAggregate(String index, String query) { return ftAggregate(index, query, null); } @Override - public RedisFuture> ftSearch(String index, V query, SearchArgs args) { + public RedisFuture> ftSearch(String index, String query, SearchArgs args) { return routeKeyless(() -> super.ftSearch(index, query, args), (conn) -> conn.ftSearch(index, query, args), CommandType.FT_SEARCH); } @Override - public RedisFuture> ftSearch(String index, V query) { - return ftSearch(index, query, SearchArgs. builder().build()); + public RedisFuture> ftSearch(String index, String query) { + return ftSearch(index, query, SearchArgs. builder().build()); } @Override - public RedisFuture> ftHybrid(String index, HybridArgs args) { + public RedisFuture> ftHybrid(String index, HybridArgs args) { return routeKeyless(() -> super.ftHybrid(index, args), (conn) -> conn.ftHybrid(index, args), CommandType.FT_HYBRID); } @Override - public RedisFuture ftExplain(String index, V query) { + public RedisFuture ftExplain(String index, String query) { return routeKeyless(() -> super.ftExplain(index, query), (conn) -> conn.ftExplain(index, query), CommandType.FT_EXPLAIN); } @Override - public RedisFuture ftExplain(String index, V query, ExplainArgs args) { + public RedisFuture ftExplain(String index, String query, ExplainArgs args) { return routeKeyless(() -> super.ftExplain(index, query, args), (conn) -> conn.ftExplain(index, query, args), CommandType.FT_EXPLAIN); } @Override - public RedisFuture> ftTagvals(String index, String fieldName) { + public RedisFuture> ftTagvals(String index, String fieldName) { return routeKeyless(() -> super.ftTagvals(index, fieldName), (conn) -> conn.ftTagvals(index, fieldName), CommandType.FT_TAGVALS); } @Override - public RedisFuture> ftSpellcheck(String index, V query) { + public RedisFuture ftSpellcheck(String index, String query) { return routeKeyless(() -> super.ftSpellcheck(index, query), (conn) -> conn.ftSpellcheck(index, query), CommandType.FT_SPELLCHECK); } @Override - public RedisFuture> ftSpellcheck(String index, V query, SpellCheckArgs args) { + public RedisFuture ftSpellcheck(String index, String query, SpellCheckArgs args) { return routeKeyless(() -> super.ftSpellcheck(index, query, args), (conn) -> conn.ftSpellcheck(index, query, args), CommandType.FT_SPELLCHECK); } @Override - public RedisFuture ftDictadd(String dict, V... terms) { + public RedisFuture ftDictadd(String dict, String... terms) { return routeKeyless(() -> super.ftDictadd(dict, terms), (conn) -> conn.ftDictadd(dict, terms), CommandType.FT_DICTADD); } @Override - public RedisFuture ftDictdel(String dict, V... terms) { + public RedisFuture ftDictdel(String dict, String... terms) { return routeKeyless(() -> super.ftDictdel(dict, terms), (conn) -> conn.ftDictdel(dict, terms), CommandType.FT_DICTDEL); } @Override - public RedisFuture> ftDictdump(String dict) { + public RedisFuture> ftDictdump(String dict) { return routeKeyless(() -> super.ftDictdump(dict), (conn) -> conn.ftDictdump(dict), CommandType.FT_DICTDUMP); } @@ -869,30 +869,30 @@ public RedisFuture ftAliasdel(String alias) { } @Override - public RedisFuture> ftList() { + public RedisFuture> ftList() { return routeKeyless(super::ftList, (conn) -> conn.ftList(), CommandType.FT_LIST); } @Override - public RedisFuture ftCreate(String index, List> fieldArgs) { + public RedisFuture ftCreate(String index, List fieldArgs) { return routeKeyless(() -> super.ftCreate(index, fieldArgs), (conn) -> conn.ftCreate(index, fieldArgs), CommandType.FT_CREATE); } @Override - public RedisFuture ftCreate(String index, CreateArgs arguments, List> fieldArgs) { + public RedisFuture ftCreate(String index, CreateArgs arguments, List fieldArgs) { return routeKeyless(() -> super.ftCreate(index, arguments, fieldArgs), (conn) -> conn.ftCreate(index, arguments, fieldArgs), CommandType.FT_CREATE); } @Override - public RedisFuture ftAlter(String index, boolean skipInitialScan, List> fieldArgs) { + public RedisFuture ftAlter(String index, boolean skipInitialScan, List fieldArgs) { return routeKeyless(() -> super.ftAlter(index, skipInitialScan, fieldArgs), (conn) -> conn.ftAlter(index, skipInitialScan, fieldArgs), CommandType.FT_ALTER); } @Override - public RedisFuture ftAlter(String index, List> fieldArgs) { + public RedisFuture ftAlter(String index, List fieldArgs) { return routeKeyless(() -> super.ftAlter(index, fieldArgs), (conn) -> conn.ftAlter(index, fieldArgs), CommandType.FT_ALTER); } @@ -909,26 +909,26 @@ public RedisFuture ftDropindex(String index) { } @Override - public RedisFuture>> ftSyndump(String index) { + public RedisFuture>> ftSyndump(String index) { return routeKeyless(() -> super.ftSyndump(index), (conn) -> conn.ftSyndump(index), CommandType.FT_SYNDUMP); } @Override - public RedisFuture ftSynupdate(String index, V synonymGroupId, V... terms) { + public RedisFuture ftSynupdate(String index, String synonymGroupId, String... terms) { return routeKeyless(() -> super.ftSynupdate(index, synonymGroupId, terms), (conn) -> conn.ftSynupdate(index, synonymGroupId, terms), CommandType.FT_SYNUPDATE); } @Override - public RedisFuture ftSynupdate(String index, V synonymGroupId, SynUpdateArgs args, V... terms) { + public RedisFuture ftSynupdate(String index, String synonymGroupId, SynUpdateArgs args, String... terms) { return routeKeyless(() -> super.ftSynupdate(index, synonymGroupId, args, terms), (conn) -> conn.ftSynupdate(index, synonymGroupId, args, terms), CommandType.FT_SYNUPDATE); } @Override - public RedisFuture> ftCursorread(String index, Cursor cursor, int count) { + public RedisFuture> ftCursorread(String index, Cursor cursor, int count) { if (cursor == null) { - CompletableFuture> failed = new CompletableFuture<>(); + CompletableFuture> failed = new CompletableFuture<>(); failed.completeExceptionally(new IllegalArgumentException("cursor must not be null")); return new PipelinedRedisFuture<>(failed); } @@ -938,15 +938,15 @@ public RedisFuture> ftCursorread(String index, Cursor cur } Optional nodeIdOpt = cursor.getNodeId(); if (!nodeIdOpt.isPresent()) { - CompletableFuture> failed = new CompletableFuture<>(); + CompletableFuture> failed = new CompletableFuture<>(); failed.completeExceptionally( new IllegalArgumentException("Cursor missing nodeId; cannot route cursor READ in cluster mode")); return new PipelinedRedisFuture<>(failed); } String nodeId = nodeIdOpt.get(); StatefulRedisConnection byNode = getStatefulConnection().getConnection(nodeId, ConnectionIntent.READ); - RedisFuture> f = byNode.async().ftCursorread(index, cursor, count); - CompletableFuture> mapped = new CompletableFuture<>(); + RedisFuture> f = byNode.async().ftCursorread(index, cursor, count); + CompletableFuture> mapped = new CompletableFuture<>(); f.whenComplete((reply, err) -> { if (err != null) { mapped.completeExceptionally(err); @@ -961,7 +961,7 @@ public RedisFuture> ftCursorread(String index, Cursor cur } @Override - public RedisFuture> ftCursorread(String index, Cursor cursor) { + public RedisFuture> ftCursorread(String index, Cursor cursor) { return ftCursorread(index, cursor, -1); } diff --git a/src/main/java/io/lettuce/core/cluster/RedisAdvancedClusterReactiveCommandsImpl.java b/src/main/java/io/lettuce/core/cluster/RedisAdvancedClusterReactiveCommandsImpl.java index 75f503145f..adb0a10daa 100644 --- a/src/main/java/io/lettuce/core/cluster/RedisAdvancedClusterReactiveCommandsImpl.java +++ b/src/main/java/io/lettuce/core/cluster/RedisAdvancedClusterReactiveCommandsImpl.java @@ -592,7 +592,7 @@ public Mono scan(KeyStreamingChannel channel, ScanCursor sc } @Override - public Mono> ftAggregate(String index, V query, AggregateArgs args) { + public Mono> ftAggregate(String index, String query, AggregateArgs args) { return routeKeyless(() -> super.ftAggregate(index, query, args), (nodeId, conn) -> conn.ftAggregate(index, query, args).mapNotNull(reply -> { if (reply != null) { @@ -603,67 +603,67 @@ public Mono> ftAggregate(String index, V query, Aggregate } @Override - public Mono> ftAggregate(String index, V query) { + public Mono> ftAggregate(String index, String query) { return ftAggregate(index, query, null); } @Override - public Mono> ftSearch(String index, V query, SearchArgs args) { + public Mono> ftSearch(String index, String query, SearchArgs args) { return routeKeyless(() -> super.ftSearch(index, query, args), conn -> conn.ftSearch(index, query, args), CommandType.FT_SEARCH); } @Override - public Mono> ftSearch(String index, V query) { - return ftSearch(index, query, SearchArgs. builder().build()); + public Mono> ftSearch(String index, String query) { + return ftSearch(index, query, SearchArgs. builder().build()); } @Override - public Mono> ftHybrid(String index, HybridArgs args) { + public Mono> ftHybrid(String index, HybridArgs args) { return routeKeyless(() -> super.ftHybrid(index, args), conn -> conn.ftHybrid(index, args), CommandType.FT_HYBRID); } @Override - public Mono ftExplain(String index, V query) { + public Mono ftExplain(String index, String query) { return routeKeyless(() -> super.ftExplain(index, query), conn -> conn.ftExplain(index, query), CommandType.FT_EXPLAIN); } @Override - public Mono ftExplain(String index, V query, ExplainArgs args) { + public Mono ftExplain(String index, String query, ExplainArgs args) { return routeKeyless(() -> super.ftExplain(index, query, args), conn -> conn.ftExplain(index, query, args), CommandType.FT_EXPLAIN); } @Override - public Flux ftTagvals(String index, String fieldName) { + public Flux ftTagvals(String index, String fieldName) { return routeKeylessMany(() -> super.ftTagvals(index, fieldName), conn -> conn.ftTagvals(index, fieldName), CommandType.FT_TAGVALS); } @Override - public Mono> ftSpellcheck(String index, V query) { + public Mono ftSpellcheck(String index, String query) { return routeKeyless(() -> super.ftSpellcheck(index, query), conn -> conn.ftSpellcheck(index, query), CommandType.FT_SPELLCHECK); } @Override - public Mono> ftSpellcheck(String index, V query, SpellCheckArgs args) { + public Mono ftSpellcheck(String index, String query, SpellCheckArgs args) { return routeKeyless(() -> super.ftSpellcheck(index, query, args), conn -> conn.ftSpellcheck(index, query, args), CommandType.FT_SPELLCHECK); } @Override - public Mono ftDictadd(String dict, V... terms) { + public Mono ftDictadd(String dict, String... terms) { return routeKeyless(() -> super.ftDictadd(dict, terms), conn -> conn.ftDictadd(dict, terms), CommandType.FT_DICTADD); } @Override - public Mono ftDictdel(String dict, V... terms) { + public Mono ftDictdel(String dict, String... terms) { return routeKeyless(() -> super.ftDictdel(dict, terms), conn -> conn.ftDictdel(dict, terms), CommandType.FT_DICTDEL); } @Override - public Flux ftDictdump(String dict) { + public Flux ftDictdump(String dict) { return routeKeylessMany(() -> super.ftDictdump(dict), conn -> conn.ftDictdump(dict), CommandType.FT_DICTDUMP); } @@ -685,25 +685,25 @@ public Mono ftAliasdel(String alias) { } @Override - public Mono ftCreate(String index, List> fieldArgs) { + public Mono ftCreate(String index, List fieldArgs) { return routeKeyless(() -> super.ftCreate(index, fieldArgs), conn -> conn.ftCreate(index, fieldArgs), CommandType.FT_CREATE); } @Override - public Mono ftCreate(String index, CreateArgs arguments, List> fieldArgs) { + public Mono ftCreate(String index, CreateArgs arguments, List fieldArgs) { return routeKeyless(() -> super.ftCreate(index, arguments, fieldArgs), conn -> conn.ftCreate(index, arguments, fieldArgs), CommandType.FT_CREATE); } @Override - public Mono ftAlter(String index, boolean skipInitialScan, List> fieldArgs) { + public Mono ftAlter(String index, boolean skipInitialScan, List fieldArgs) { return routeKeyless(() -> super.ftAlter(index, skipInitialScan, fieldArgs), conn -> conn.ftAlter(index, skipInitialScan, fieldArgs), CommandType.FT_ALTER); } @Override - public Mono ftAlter(String index, List> fieldArgs) { + public Mono ftAlter(String index, List fieldArgs) { return routeKeyless(() -> super.ftAlter(index, fieldArgs), conn -> conn.ftAlter(index, fieldArgs), CommandType.FT_ALTER); } @@ -720,29 +720,29 @@ public Mono ftDropindex(String index) { } @Override - public Mono>> ftSyndump(String index) { + public Mono>> ftSyndump(String index) { return routeKeyless(() -> super.ftSyndump(index), conn -> conn.ftSyndump(index), CommandType.FT_SYNDUMP); } @Override - public Mono ftSynupdate(String index, V synonymGroupId, V... terms) { + public Mono ftSynupdate(String index, String synonymGroupId, String... terms) { return routeKeyless(() -> super.ftSynupdate(index, synonymGroupId, terms), conn -> conn.ftSynupdate(index, synonymGroupId, terms), CommandType.FT_SYNUPDATE); } @Override - public Mono ftSynupdate(String index, V synonymGroupId, SynUpdateArgs args, V... terms) { + public Mono ftSynupdate(String index, String synonymGroupId, SynUpdateArgs args, String... terms) { return routeKeyless(() -> super.ftSynupdate(index, synonymGroupId, args, terms), conn -> conn.ftSynupdate(index, synonymGroupId, args, terms), CommandType.FT_SYNUPDATE); } @Override - public Flux ftList() { + public Flux ftList() { return routeKeylessMany(super::ftList, RediSearchReactiveCommands::ftList, CommandType.FT_LIST); } @Override - public Mono> ftCursorread(String index, Cursor cursor, int count) { + public Mono> ftCursorread(String index, Cursor cursor, int count) { if (cursor == null) { return Mono.error(new IllegalArgumentException("cursor must not be null")); } @@ -765,7 +765,7 @@ public Mono> ftCursorread(String index, Cursor cursor, in } @Override - public Mono> ftCursorread(String index, Cursor cursor) { + public Mono> ftCursorread(String index, Cursor cursor) { return ftCursorread(index, cursor, -1); } diff --git a/src/main/java/io/lettuce/core/cluster/api/async/NodeSelectionAsyncCommands.java b/src/main/java/io/lettuce/core/cluster/api/async/NodeSelectionAsyncCommands.java index 0f0f4ad0c4..ef00b50d57 100644 --- a/src/main/java/io/lettuce/core/cluster/api/async/NodeSelectionAsyncCommands.java +++ b/src/main/java/io/lettuce/core/cluster/api/async/NodeSelectionAsyncCommands.java @@ -15,7 +15,7 @@ public interface NodeSelectionAsyncCommands extends BaseNodeSelectionAsync NodeSelectionHLLAsyncCommands, NodeSelectionKeyAsyncCommands, NodeSelectionListAsyncCommands, NodeSelectionScriptingAsyncCommands, NodeSelectionServerAsyncCommands, NodeSelectionSetAsyncCommands, NodeSelectionSortedSetAsyncCommands, NodeSelectionStreamCommands, NodeSelectionStringAsyncCommands, - NodeSelectionJsonAsyncCommands, NodeSelectionVectorSetAsyncCommands, NodeSelectionSearchAsyncCommands, + NodeSelectionJsonAsyncCommands, NodeSelectionVectorSetAsyncCommands, NodeSelectionSearchAsyncCommands, NodeSelectionBloomFilterAsyncCommands, NodeSelectionCuckooFilterAsyncCommands, NodeSelectionTopKAsyncCommands { } diff --git a/src/main/java/io/lettuce/core/cluster/api/async/NodeSelectionSearchAsyncCommands.java b/src/main/java/io/lettuce/core/cluster/api/async/NodeSelectionSearchAsyncCommands.java index 47834b225e..c5423be547 100644 --- a/src/main/java/io/lettuce/core/cluster/api/async/NodeSelectionSearchAsyncCommands.java +++ b/src/main/java/io/lettuce/core/cluster/api/async/NodeSelectionSearchAsyncCommands.java @@ -31,13 +31,12 @@ * Asynchronous executed commands on a node selection for RediSearch functionality * * @param Key type. - * @param Value type. * @author Tihomir Mateev * @see RediSearch * @since 6.8 * @generated by io.lettuce.apigenerator.CreateAsyncNodeSelectionClusterApi */ -public interface NodeSelectionSearchAsyncCommands { +public interface NodeSelectionSearchAsyncCommands { /** * Create a new search index with the given name and field definitions using default settings. @@ -63,7 +62,7 @@ public interface NodeSelectionSearchAsyncCommands { * @see #ftDropindex(String) */ @Experimental - AsyncExecutions ftCreate(String index, List> fieldArgs); + AsyncExecutions ftCreate(String index, List fieldArgs); /** * Create a new search index with the given name, custom configuration, and field definitions. @@ -102,7 +101,7 @@ public interface NodeSelectionSearchAsyncCommands { * @see #ftDropindex(String) */ @Experimental - AsyncExecutions ftCreate(String index, CreateArgs arguments, List> fieldArgs); + AsyncExecutions ftCreate(String index, CreateArgs arguments, List fieldArgs); /** * Add an alias to a search index. @@ -279,7 +278,7 @@ public interface NodeSelectionSearchAsyncCommands { * @see #ftCreate(String, CreateArgs, List) */ @Experimental - AsyncExecutions ftAlter(String index, boolean skipInitialScan, List> fieldArgs); + AsyncExecutions ftAlter(String index, boolean skipInitialScan, List fieldArgs); /** * Add new attributes to an existing search index. @@ -314,7 +313,7 @@ public interface NodeSelectionSearchAsyncCommands { * @see #ftCreate(String, CreateArgs, List) */ @Experimental - AsyncExecutions ftAlter(String index, List> fieldArgs); + AsyncExecutions ftAlter(String index, List fieldArgs); /** * Return a distinct set of values indexed in a Tag field. @@ -370,7 +369,7 @@ public interface NodeSelectionSearchAsyncCommands { * @see #ftCreate(String, CreateArgs, List) */ @Experimental - AsyncExecutions> ftTagvals(String index, String fieldName); + AsyncExecutions> ftTagvals(String index, String fieldName); /** * Perform spelling correction on a query, returning suggestions for misspelled terms. @@ -405,13 +404,13 @@ public interface NodeSelectionSearchAsyncCommands { * @since 6.8 * @see FT.SPELLCHECK * @see Spellchecking - * @see #ftSpellcheck(String, Object, SpellCheckArgs) - * @see #ftDictadd(String, Object[]) - * @see #ftDictdel(String, Object[]) + * @see #ftSpellcheck(String, String, SpellCheckArgs) + * @see #ftDictadd(String, String[]) + * @see #ftDictdel(String, String[]) * @see #ftDictdump(String) */ @Experimental - AsyncExecutions> ftSpellcheck(String index, V query); + AsyncExecutions ftSpellcheck(String index, String query); /** * Perform spelling correction on a query with additional options. @@ -442,13 +441,13 @@ public interface NodeSelectionSearchAsyncCommands { * @since 6.8 * @see FT.SPELLCHECK * @see Spellchecking - * @see #ftSpellcheck(String, Object) - * @see #ftDictadd(String, Object[]) - * @see #ftDictdel(String, Object[]) + * @see #ftSpellcheck(String, String) + * @see #ftDictadd(String, String[]) + * @see #ftDictdel(String, String[]) * @see #ftDictdump(String) */ @Experimental - AsyncExecutions> ftSpellcheck(String index, V query, SpellCheckArgs args); + AsyncExecutions ftSpellcheck(String index, String query, SpellCheckArgs args); /** * Add terms to a dictionary. @@ -478,11 +477,11 @@ public interface NodeSelectionSearchAsyncCommands { * @since 6.8 * @see FT.DICTADD * @see Spellchecking - * @see #ftDictdel(String, Object[]) + * @see #ftDictdel(String, String[]) * @see #ftDictdump(String) */ @Experimental - AsyncExecutions ftDictadd(String dict, V... terms); + AsyncExecutions ftDictadd(String dict, String... terms); /** * Delete terms from a dictionary. @@ -501,11 +500,11 @@ public interface NodeSelectionSearchAsyncCommands { * @return the number of terms that were deleted * @since 6.8 * @see FT.DICTDEL - * @see #ftDictadd(String, Object[]) + * @see #ftDictadd(String, String[]) * @see #ftDictdump(String) */ @Experimental - AsyncExecutions ftDictdel(String dict, V... terms); + AsyncExecutions ftDictdel(String dict, String... terms); /** * Dump all terms in a dictionary. @@ -522,11 +521,11 @@ public interface NodeSelectionSearchAsyncCommands { * @return a list of all terms in the dictionary * @since 6.8 * @see FT.DICTDUMP - * @see #ftDictadd(String, Object[]) - * @see #ftDictdel(String, Object[]) + * @see #ftDictadd(String, String[]) + * @see #ftDictdel(String, String[]) */ @Experimental - AsyncExecutions> ftDictdump(String dict); + AsyncExecutions> ftDictdump(String dict); /** * Return the execution plan for a complex query. @@ -555,11 +554,11 @@ public interface NodeSelectionSearchAsyncCommands { * @return the execution plan as a string * @since 6.8 * @see FT.EXPLAIN - * @see #ftExplain(String, Object, ExplainArgs) - * @see #ftSearch(String, Object) + * @see #ftExplain(String, String, ExplainArgs) + * @see #ftSearch(String, String) */ @Experimental - AsyncExecutions ftExplain(String index, V query); + AsyncExecutions ftExplain(String index, String query); /** * Return the execution plan for a complex query with additional options. @@ -586,11 +585,11 @@ public interface NodeSelectionSearchAsyncCommands { * @return the execution plan as a string * @since 6.8 * @see FT.EXPLAIN - * @see #ftExplain(String, Object) - * @see #ftSearch(String, Object) + * @see #ftExplain(String, String) + * @see #ftSearch(String, String) */ @Experimental - AsyncExecutions ftExplain(String index, V query, ExplainArgs args); + AsyncExecutions ftExplain(String index, String query, ExplainArgs args); /** * Return a list of all existing indexes. @@ -622,11 +621,11 @@ public interface NodeSelectionSearchAsyncCommands { * @return a list of index names * @since 6.8 * @see FT._LIST - * @see #ftCreate(String, CreateArgs, FieldArgs[]) + * @see #ftCreate(String, CreateArgs, List) * @see #ftDropindex(String) */ @Experimental - AsyncExecutions> ftList(); + AsyncExecutions> ftList(); /** * Dump synonym group contents. @@ -654,11 +653,11 @@ public interface NodeSelectionSearchAsyncCommands { * @return a map where keys are synonym terms and values are lists of group IDs containing that synonym * @since 6.8 * @see FT.SYNDUMP - * @see #ftSynupdate(String, Object, Object[]) - * @see #ftSynupdate(String, Object, SynUpdateArgs, Object[]) + * @see #ftSynupdate(String, String, String[]) + * @see #ftSynupdate(String, String, SynUpdateArgs, String[]) */ @Experimental - AsyncExecutions>> ftSyndump(String index); + AsyncExecutions>> ftSyndump(String index); /** * Update a synonym group with additional terms. @@ -688,11 +687,11 @@ public interface NodeSelectionSearchAsyncCommands { * @return OK if executed correctly * @since 6.8 * @see FT.SYNUPDATE - * @see #ftSynupdate(String, Object, SynUpdateArgs, Object[]) + * @see #ftSynupdate(String, String, SynUpdateArgs, String[]) * @see #ftSyndump(String) */ @Experimental - AsyncExecutions ftSynupdate(String index, V synonymGroupId, V... terms); + AsyncExecutions ftSynupdate(String index, String synonymGroupId, String... terms); /** * Update a synonym group with additional terms and options. @@ -720,11 +719,11 @@ public interface NodeSelectionSearchAsyncCommands { * @return OK if executed correctly * @since 6.8 * @see FT.SYNUPDATE - * @see #ftSynupdate(String, Object, Object[]) + * @see #ftSynupdate(String, String, String[]) * @see #ftSyndump(String) */ @Experimental - AsyncExecutions ftSynupdate(String index, V synonymGroupId, SynUpdateArgs args, V... terms); + AsyncExecutions ftSynupdate(String index, String synonymGroupId, SynUpdateArgs args, String... terms); /** * Add a suggestion string to an auto-complete suggestion dictionary. @@ -755,13 +754,13 @@ public interface NodeSelectionSearchAsyncCommands { * @return the current size of the suggestion dictionary after adding the suggestion * @since 6.8 * @see FT.SUGADD - * @see #ftSugadd(Object, Object, double, SugAddArgs) - * @see #ftSugget(Object, Object) - * @see #ftSugdel(Object, Object) - * @see #ftSuglen(Object) + * @see #ftSugadd(K, String, double, SugAddArgs) + * @see #ftSugget(K, String) + * @see #ftSugdel(K, String) + * @see #ftSuglen(K) */ @Experimental - AsyncExecutions ftSugadd(K key, V suggestion, double score); + AsyncExecutions ftSugadd(K key, String suggestion, double score); /** * Add a suggestion string to an auto-complete suggestion dictionary with additional options. @@ -782,13 +781,13 @@ public interface NodeSelectionSearchAsyncCommands { * @return the current size of the suggestion dictionary after adding the suggestion * @since 6.8 * @see FT.SUGADD - * @see #ftSugadd(Object, Object, double) - * @see #ftSugget(Object, Object, SugGetArgs) - * @see #ftSugdel(Object, Object) - * @see #ftSuglen(Object) + * @see #ftSugadd(K, String, double) + * @see #ftSugget(K, String, SugGetArgs) + * @see #ftSugdel(K, String) + * @see #ftSuglen(K) */ @Experimental - AsyncExecutions ftSugadd(K key, V suggestion, double score, SugAddArgs args); + AsyncExecutions ftSugadd(K key, String suggestion, double score, SugAddArgs args); /** * Delete a string from a suggestion dictionary. @@ -807,12 +806,12 @@ public interface NodeSelectionSearchAsyncCommands { * @return {@code true} if the string was found and deleted, {@code false} otherwise * @since 6.8 * @see FT.SUGDEL - * @see #ftSugadd(Object, Object, double) - * @see #ftSugget(Object, Object) - * @see #ftSuglen(Object) + * @see #ftSugadd(K, String, double) + * @see #ftSugget(K, String) + * @see #ftSuglen(K) */ @Experimental - AsyncExecutions ftSugdel(K key, V suggestion); + AsyncExecutions ftSugdel(K key, String suggestion); /** * Get completion suggestions for a prefix. @@ -831,13 +830,13 @@ public interface NodeSelectionSearchAsyncCommands { * @return a list of suggestions matching the prefix * @since 6.8 * @see FT.SUGGET - * @see #ftSugget(Object, Object, SugGetArgs) - * @see #ftSugadd(Object, Object, double) - * @see #ftSugdel(Object, Object) - * @see #ftSuglen(Object) + * @see #ftSugget(K, String, SugGetArgs) + * @see #ftSugadd(K, String, double) + * @see #ftSugdel(K, String) + * @see #ftSuglen(K) */ @Experimental - AsyncExecutions>> ftSugget(K key, V prefix); + AsyncExecutions> ftSugget(K key, String prefix); /** * Get completion suggestions for a prefix with additional options. @@ -857,13 +856,13 @@ public interface NodeSelectionSearchAsyncCommands { * @return a list of suggestions matching the prefix, optionally with scores and payloads * @since 6.8 * @see FT.SUGGET - * @see #ftSugget(Object, Object) - * @see #ftSugadd(Object, Object, double, SugAddArgs) - * @see #ftSugdel(Object, Object) - * @see #ftSuglen(Object) + * @see #ftSugget(K, String) + * @see #ftSugadd(K, String, double, SugAddArgs) + * @see #ftSugdel(K, String) + * @see #ftSuglen(K) */ @Experimental - AsyncExecutions>> ftSugget(K key, V prefix, SugGetArgs args); + AsyncExecutions> ftSugget(K key, String prefix, SugGetArgs args); /** * Get the size of an auto-complete suggestion dictionary. @@ -880,9 +879,9 @@ public interface NodeSelectionSearchAsyncCommands { * @return the current size of the suggestion dictionary * @since 6.8 * @see FT.SUGLEN - * @see #ftSugadd(Object, Object, double) - * @see #ftSugget(Object, Object) - * @see #ftSugdel(Object, Object) + * @see #ftSugadd(K, String, double) + * @see #ftSugget(K, String) + * @see #ftSugdel(K, String) */ @Experimental AsyncExecutions ftSuglen(K key); @@ -974,10 +973,10 @@ public interface NodeSelectionSearchAsyncCommands { * @see Query syntax * @see SearchReply * @see SearchArgs - * @see #ftSearch(String, Object, SearchArgs) + * @see #ftSearch(String, String, SearchArgs) */ @Experimental - AsyncExecutions> ftSearch(String index, V query); + AsyncExecutions> ftSearch(String index, String query); /** * Search the index with a textual query using advanced search options and filters. @@ -1025,23 +1024,23 @@ public interface NodeSelectionSearchAsyncCommands { * @see Advanced concepts * @see SearchReply * @see SearchArgs - * @see #ftSearch(String, Object) + * @see #ftSearch(String, String) */ @Experimental - AsyncExecutions> ftSearch(String index, V query, SearchArgs args); + AsyncExecutions> ftSearch(String index, String query, SearchArgs args); /** * Run a search query on an index and perform basic aggregate transformations using default options. * *

* This command executes a search query and applies aggregation operations to transform and analyze the results. Unlike - * {@link #ftSearch(String, Object)}, which returns individual documents, FT.AGGREGATE processes the result set through a + * {@link #ftSearch(String, String)}, which returns individual documents, FT.AGGREGATE processes the result set through a * pipeline of transformations to produce analytical insights, summaries, and computed values. *

* *

* This basic variant uses default aggregation behavior without additional pipeline operations. For advanced aggregations - * with grouping, sorting, filtering, and custom transformations, use {@link #ftAggregate(String, Object, AggregateArgs)}. + * with grouping, sorting, filtering, and custom transformations, use {@link #ftAggregate(String, String, AggregateArgs)}. *

* *

@@ -1067,10 +1066,10 @@ public interface NodeSelectionSearchAsyncCommands { * "https://redis.io/docs/latest/develop/interact/search-and-query/advanced-concepts/aggregations/">Aggregations * @see SearchReply * @see AggregateArgs - * @see #ftAggregate(String, Object, AggregateArgs) + * @see #ftAggregate(String, String, AggregateArgs) */ @Experimental - AsyncExecutions> ftAggregate(String index, V query); + AsyncExecutions> ftAggregate(String index, String query); /** * Run a search query on an index and perform advanced aggregate transformations with a processing pipeline. @@ -1122,18 +1121,18 @@ public interface NodeSelectionSearchAsyncCommands { * API * @see SearchReply * @see AggregateArgs - * @see #ftAggregate(String, Object) + * @see #ftAggregate(String, String) * @see #ftCursorread(String, Cursor) */ @Experimental - AsyncExecutions> ftAggregate(String index, V query, AggregateArgs args); + AsyncExecutions> ftAggregate(String index, String query, AggregateArgs args); /** * Read next results from an existing cursor and optionally override the batch size. * *

* This command is used to read the next batch of results from a cursor that was created by - * {@link #ftAggregate(String, Object, AggregateArgs)} with the {@code WITHCURSOR} option. Cursors provide an efficient way + * {@link #ftAggregate(String, String, AggregateArgs)} with the {@code WITHCURSOR} option. Cursors provide an efficient way * to iterate through large result sets without loading all results into memory at once. *

* @@ -1156,17 +1155,17 @@ public interface NodeSelectionSearchAsyncCommands { * "https://redis.io/docs/latest/develop/interact/search-and-query/advanced-concepts/aggregations/#cursor-api">Cursor * API * @see AggregationReply - * @see #ftAggregate(String, Object, AggregateArgs) + * @see #ftAggregate(String, String, AggregateArgs) */ @Experimental - AsyncExecutions> ftCursorread(String index, Cursor cursor, int count); + AsyncExecutions> ftCursorread(String index, Cursor cursor, int count); /** * Read next results from an existing cursor using the default batch size. * *

* This command is used to read the next batch of results from a cursor created by - * {@link #ftAggregate(String, Object, AggregateArgs)} with the {@code WITHCURSOR} option. This variant uses the default + * {@link #ftAggregate(String, String, AggregateArgs)} with the {@code WITHCURSOR} option. This variant uses the default * batch size that was specified in the original {@code FT.AGGREGATE} command's {@code WITHCURSOR} clause. *

* @@ -1188,16 +1187,16 @@ public interface NodeSelectionSearchAsyncCommands { * "https://redis.io/docs/latest/develop/interact/search-and-query/advanced-concepts/aggregations/#cursor-api">Cursor * API * @see AggregationReply - * @see #ftAggregate(String, Object, AggregateArgs) + * @see #ftAggregate(String, String, AggregateArgs) */ @Experimental - AsyncExecutions> ftCursorread(String index, Cursor cursor); + AsyncExecutions> ftCursorread(String index, Cursor cursor); /** * Delete a cursor and free its associated resources. * *

- * This command is used to explicitly delete a cursor created by {@link #ftAggregate(String, Object, AggregateArgs)} with + * This command is used to explicitly delete a cursor created by {@link #ftAggregate(String, String, AggregateArgs)} with * the {@code WITHCURSOR} option. Deleting a cursor frees up server resources and should be done when you no longer need to * read more results from the cursor. *

@@ -1225,7 +1224,7 @@ public interface NodeSelectionSearchAsyncCommands { * @see Cursor * API - * @see #ftAggregate(String, Object, AggregateArgs) + * @see #ftAggregate(String, String, AggregateArgs) * @see #ftCursorread(String, Cursor) * @see #ftCursorread(String, Cursor, int) */ @@ -1244,6 +1243,6 @@ public interface NodeSelectionSearchAsyncCommands { * @since 7.2 */ @Experimental - AsyncExecutions> ftHybrid(String index, HybridArgs args); + AsyncExecutions> ftHybrid(String index, HybridArgs args); } diff --git a/src/main/java/io/lettuce/core/cluster/api/async/RedisClusterAsyncCommands.java b/src/main/java/io/lettuce/core/cluster/api/async/RedisClusterAsyncCommands.java index 4b129b9391..3d62927136 100644 --- a/src/main/java/io/lettuce/core/cluster/api/async/RedisClusterAsyncCommands.java +++ b/src/main/java/io/lettuce/core/cluster/api/async/RedisClusterAsyncCommands.java @@ -47,7 +47,7 @@ public interface RedisClusterAsyncCommands RedisListAsyncCommands, RedisScriptingAsyncCommands, RedisServerAsyncCommands, RedisSetAsyncCommands, RedisSortedSetAsyncCommands, RedisStreamAsyncCommands, RedisStringAsyncCommands, RedisJsonAsyncCommands, RedisVectorSetAsyncCommands, - RediSearchAsyncCommands, RedisArrayAsyncCommands, RedisBloomFilterAsyncCommands, + RediSearchAsyncCommands, RedisArrayAsyncCommands, RedisBloomFilterAsyncCommands, RedisCuckooFilterAsyncCommands, RedisTopKAsyncCommands { /** diff --git a/src/main/java/io/lettuce/core/cluster/api/reactive/RedisClusterReactiveCommands.java b/src/main/java/io/lettuce/core/cluster/api/reactive/RedisClusterReactiveCommands.java index ebe4a3c402..2f97238e40 100644 --- a/src/main/java/io/lettuce/core/cluster/api/reactive/RedisClusterReactiveCommands.java +++ b/src/main/java/io/lettuce/core/cluster/api/reactive/RedisClusterReactiveCommands.java @@ -27,7 +27,6 @@ import io.lettuce.core.HotkeysReply; import io.lettuce.core.MSetExArgs; import io.lettuce.core.Range; -import io.lettuce.core.SetArgs; import io.lettuce.core.api.reactive.*; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -48,7 +47,7 @@ public interface RedisClusterReactiveCommands RedisKeyReactiveCommands, RedisListReactiveCommands, RedisScriptingReactiveCommands, RedisServerReactiveCommands, RedisSetReactiveCommands, RedisSortedSetReactiveCommands, RedisStreamReactiveCommands, RedisStringReactiveCommands, RedisJsonReactiveCommands, - RedisVectorSetReactiveCommands, RediSearchReactiveCommands, RedisArrayReactiveCommands, + RedisVectorSetReactiveCommands, RediSearchReactiveCommands, RedisArrayReactiveCommands, RedisBloomFilterReactiveCommands, RedisCuckooFilterReactiveCommands, RedisTopKReactiveCommands { /** diff --git a/src/main/java/io/lettuce/core/cluster/api/sync/NodeSelectionCommands.java b/src/main/java/io/lettuce/core/cluster/api/sync/NodeSelectionCommands.java index c1d331ff06..ed6ea5056b 100644 --- a/src/main/java/io/lettuce/core/cluster/api/sync/NodeSelectionCommands.java +++ b/src/main/java/io/lettuce/core/cluster/api/sync/NodeSelectionCommands.java @@ -13,6 +13,6 @@ public interface NodeSelectionCommands extends BaseNodeSelectionCommands, NodeSelectionListCommands, NodeSelectionScriptingCommands, NodeSelectionServerCommands, NodeSelectionSetCommands, NodeSelectionSortedSetCommands, NodeSelectionStreamCommands, NodeSelectionStringCommands, NodeSelectionJsonCommands, - NodeSelectionVectorSetCommands, NodeSelectionSearchCommands, NodeSelectionBloomFilterCommands, + NodeSelectionVectorSetCommands, NodeSelectionSearchCommands, NodeSelectionBloomFilterCommands, NodeSelectionCuckooFilterCommands, NodeSelectionTopKCommands { } diff --git a/src/main/java/io/lettuce/core/cluster/api/sync/NodeSelectionSearchCommands.java b/src/main/java/io/lettuce/core/cluster/api/sync/NodeSelectionSearchCommands.java index 33e6259010..1b908f4952 100644 --- a/src/main/java/io/lettuce/core/cluster/api/sync/NodeSelectionSearchCommands.java +++ b/src/main/java/io/lettuce/core/cluster/api/sync/NodeSelectionSearchCommands.java @@ -31,13 +31,12 @@ * Synchronous executed commands on a node selection for RediSearch functionality * * @param Key type. - * @param Value type. * @author Tihomir Mateev * @see RediSearch * @since 6.8 * @generated by io.lettuce.apigenerator.CreateSyncNodeSelectionClusterApi */ -public interface NodeSelectionSearchCommands { +public interface NodeSelectionSearchCommands { /** * Create a new search index with the given name and field definitions using default settings. @@ -63,7 +62,7 @@ public interface NodeSelectionSearchCommands { * @see #ftDropindex(String) */ @Experimental - Executions ftCreate(String index, List> fieldArgs); + Executions ftCreate(String index, List fieldArgs); /** * Create a new search index with the given name, custom configuration, and field definitions. @@ -102,7 +101,7 @@ public interface NodeSelectionSearchCommands { * @see #ftDropindex(String) */ @Experimental - Executions ftCreate(String index, CreateArgs arguments, List> fieldArgs); + Executions ftCreate(String index, CreateArgs arguments, List fieldArgs); /** * Add an alias to a search index. @@ -279,7 +278,7 @@ public interface NodeSelectionSearchCommands { * @see #ftCreate(String, CreateArgs, List) */ @Experimental - Executions ftAlter(String index, boolean skipInitialScan, List> fieldArgs); + Executions ftAlter(String index, boolean skipInitialScan, List fieldArgs); /** * Add new attributes to an existing search index. @@ -314,7 +313,7 @@ public interface NodeSelectionSearchCommands { * @see #ftCreate(String, CreateArgs, List) */ @Experimental - Executions ftAlter(String index, List> fieldArgs); + Executions ftAlter(String index, List fieldArgs); /** * Return a distinct set of values indexed in a Tag field. @@ -370,7 +369,7 @@ public interface NodeSelectionSearchCommands { * @see #ftCreate(String, CreateArgs, List) */ @Experimental - Executions> ftTagvals(String index, String fieldName); + Executions> ftTagvals(String index, String fieldName); /** * Perform spelling correction on a query, returning suggestions for misspelled terms. @@ -405,13 +404,13 @@ public interface NodeSelectionSearchCommands { * @since 6.8 * @see FT.SPELLCHECK * @see Spellchecking - * @see #ftSpellcheck(String, Object, SpellCheckArgs) - * @see #ftDictadd(String, Object[]) - * @see #ftDictdel(String, Object[]) + * @see #ftSpellcheck(String, String, SpellCheckArgs) + * @see #ftDictadd(String, String[]) + * @see #ftDictdel(String, String[]) * @see #ftDictdump(String) */ @Experimental - Executions> ftSpellcheck(String index, V query); + Executions ftSpellcheck(String index, String query); /** * Perform spelling correction on a query with additional options. @@ -442,13 +441,13 @@ public interface NodeSelectionSearchCommands { * @since 6.8 * @see FT.SPELLCHECK * @see Spellchecking - * @see #ftSpellcheck(String, Object) - * @see #ftDictadd(String, Object[]) - * @see #ftDictdel(String, Object[]) + * @see #ftSpellcheck(String, String) + * @see #ftDictadd(String, String[]) + * @see #ftDictdel(String, String[]) * @see #ftDictdump(String) */ @Experimental - Executions> ftSpellcheck(String index, V query, SpellCheckArgs args); + Executions ftSpellcheck(String index, String query, SpellCheckArgs args); /** * Add terms to a dictionary. @@ -478,11 +477,11 @@ public interface NodeSelectionSearchCommands { * @since 6.8 * @see FT.DICTADD * @see Spellchecking - * @see #ftDictdel(String, Object[]) + * @see #ftDictdel(String, String[]) * @see #ftDictdump(String) */ @Experimental - Executions ftDictadd(String dict, V... terms); + Executions ftDictadd(String dict, String... terms); /** * Delete terms from a dictionary. @@ -501,11 +500,11 @@ public interface NodeSelectionSearchCommands { * @return the number of terms that were deleted * @since 6.8 * @see FT.DICTDEL - * @see #ftDictadd(String, Object[]) + * @see #ftDictadd(String, String[]) * @see #ftDictdump(String) */ @Experimental - Executions ftDictdel(String dict, V... terms); + Executions ftDictdel(String dict, String... terms); /** * Dump all terms in a dictionary. @@ -522,11 +521,11 @@ public interface NodeSelectionSearchCommands { * @return a list of all terms in the dictionary * @since 6.8 * @see FT.DICTDUMP - * @see #ftDictadd(String, Object[]) - * @see #ftDictdel(String, Object[]) + * @see #ftDictadd(String, String[]) + * @see #ftDictdel(String, String[]) */ @Experimental - Executions> ftDictdump(String dict); + Executions> ftDictdump(String dict); /** * Return the execution plan for a complex query. @@ -555,11 +554,11 @@ public interface NodeSelectionSearchCommands { * @return the execution plan as a string * @since 6.8 * @see FT.EXPLAIN - * @see #ftExplain(String, Object, ExplainArgs) - * @see #ftSearch(String, Object) + * @see #ftExplain(String, String, ExplainArgs) + * @see #ftSearch(String, String) */ @Experimental - Executions ftExplain(String index, V query); + Executions ftExplain(String index, String query); /** * Return the execution plan for a complex query with additional options. @@ -586,11 +585,11 @@ public interface NodeSelectionSearchCommands { * @return the execution plan as a string * @since 6.8 * @see FT.EXPLAIN - * @see #ftExplain(String, Object) - * @see #ftSearch(String, Object) + * @see #ftExplain(String, String) + * @see #ftSearch(String, String) */ @Experimental - Executions ftExplain(String index, V query, ExplainArgs args); + Executions ftExplain(String index, String query, ExplainArgs args); /** * Return a list of all existing indexes. @@ -622,11 +621,11 @@ public interface NodeSelectionSearchCommands { * @return a list of index names * @since 6.8 * @see FT._LIST - * @see #ftCreate(String, CreateArgs, FieldArgs[]) + * @see #ftCreate(String, CreateArgs, List) * @see #ftDropindex(String) */ @Experimental - Executions> ftList(); + Executions> ftList(); /** * Dump synonym group contents. @@ -654,11 +653,11 @@ public interface NodeSelectionSearchCommands { * @return a map where keys are synonym terms and values are lists of group IDs containing that synonym * @since 6.8 * @see FT.SYNDUMP - * @see #ftSynupdate(String, Object, Object[]) - * @see #ftSynupdate(String, Object, SynUpdateArgs, Object[]) + * @see #ftSynupdate(String, String, String[]) + * @see #ftSynupdate(String, String, SynUpdateArgs, String[]) */ @Experimental - Executions>> ftSyndump(String index); + Executions>> ftSyndump(String index); /** * Update a synonym group with additional terms. @@ -688,11 +687,11 @@ public interface NodeSelectionSearchCommands { * @return OK if executed correctly * @since 6.8 * @see FT.SYNUPDATE - * @see #ftSynupdate(String, Object, SynUpdateArgs, Object[]) + * @see #ftSynupdate(String, String, SynUpdateArgs, String[]) * @see #ftSyndump(String) */ @Experimental - Executions ftSynupdate(String index, V synonymGroupId, V... terms); + Executions ftSynupdate(String index, String synonymGroupId, String... terms); /** * Update a synonym group with additional terms and options. @@ -720,11 +719,11 @@ public interface NodeSelectionSearchCommands { * @return OK if executed correctly * @since 6.8 * @see FT.SYNUPDATE - * @see #ftSynupdate(String, Object, Object[]) + * @see #ftSynupdate(String, String, String[]) * @see #ftSyndump(String) */ @Experimental - Executions ftSynupdate(String index, V synonymGroupId, SynUpdateArgs args, V... terms); + Executions ftSynupdate(String index, String synonymGroupId, SynUpdateArgs args, String... terms); /** * Add a suggestion string to an auto-complete suggestion dictionary. @@ -755,13 +754,13 @@ public interface NodeSelectionSearchCommands { * @return the current size of the suggestion dictionary after adding the suggestion * @since 6.8 * @see FT.SUGADD - * @see #ftSugadd(Object, Object, double, SugAddArgs) - * @see #ftSugget(Object, Object) - * @see #ftSugdel(Object, Object) - * @see #ftSuglen(Object) + * @see #ftSugadd(K, String, double, SugAddArgs) + * @see #ftSugget(K, String) + * @see #ftSugdel(K, String) + * @see #ftSuglen(K) */ @Experimental - Executions ftSugadd(K key, V suggestion, double score); + Executions ftSugadd(K key, String suggestion, double score); /** * Add a suggestion string to an auto-complete suggestion dictionary with additional options. @@ -782,13 +781,13 @@ public interface NodeSelectionSearchCommands { * @return the current size of the suggestion dictionary after adding the suggestion * @since 6.8 * @see FT.SUGADD - * @see #ftSugadd(Object, Object, double) - * @see #ftSugget(Object, Object, SugGetArgs) - * @see #ftSugdel(Object, Object) - * @see #ftSuglen(Object) + * @see #ftSugadd(K, String, double) + * @see #ftSugget(K, String, SugGetArgs) + * @see #ftSugdel(K, String) + * @see #ftSuglen(K) */ @Experimental - Executions ftSugadd(K key, V suggestion, double score, SugAddArgs args); + Executions ftSugadd(K key, String suggestion, double score, SugAddArgs args); /** * Delete a string from a suggestion dictionary. @@ -807,12 +806,12 @@ public interface NodeSelectionSearchCommands { * @return {@code true} if the string was found and deleted, {@code false} otherwise * @since 6.8 * @see FT.SUGDEL - * @see #ftSugadd(Object, Object, double) - * @see #ftSugget(Object, Object) - * @see #ftSuglen(Object) + * @see #ftSugadd(K, String, double) + * @see #ftSugget(K, String) + * @see #ftSuglen(K) */ @Experimental - Executions ftSugdel(K key, V suggestion); + Executions ftSugdel(K key, String suggestion); /** * Get completion suggestions for a prefix. @@ -831,13 +830,13 @@ public interface NodeSelectionSearchCommands { * @return a list of suggestions matching the prefix * @since 6.8 * @see FT.SUGGET - * @see #ftSugget(Object, Object, SugGetArgs) - * @see #ftSugadd(Object, Object, double) - * @see #ftSugdel(Object, Object) - * @see #ftSuglen(Object) + * @see #ftSugget(K, String, SugGetArgs) + * @see #ftSugadd(K, String, double) + * @see #ftSugdel(K, String) + * @see #ftSuglen(K) */ @Experimental - Executions>> ftSugget(K key, V prefix); + Executions> ftSugget(K key, String prefix); /** * Get completion suggestions for a prefix with additional options. @@ -857,13 +856,13 @@ public interface NodeSelectionSearchCommands { * @return a list of suggestions matching the prefix, optionally with scores and payloads * @since 6.8 * @see FT.SUGGET - * @see #ftSugget(Object, Object) - * @see #ftSugadd(Object, Object, double, SugAddArgs) - * @see #ftSugdel(Object, Object) - * @see #ftSuglen(Object) + * @see #ftSugget(K, String) + * @see #ftSugadd(K, String, double, SugAddArgs) + * @see #ftSugdel(K, String) + * @see #ftSuglen(K) */ @Experimental - Executions>> ftSugget(K key, V prefix, SugGetArgs args); + Executions> ftSugget(K key, String prefix, SugGetArgs args); /** * Get the size of an auto-complete suggestion dictionary. @@ -880,9 +879,9 @@ public interface NodeSelectionSearchCommands { * @return the current size of the suggestion dictionary * @since 6.8 * @see FT.SUGLEN - * @see #ftSugadd(Object, Object, double) - * @see #ftSugget(Object, Object) - * @see #ftSugdel(Object, Object) + * @see #ftSugadd(K, String, double) + * @see #ftSugget(K, String) + * @see #ftSugdel(K, String) */ @Experimental Executions ftSuglen(K key); @@ -974,10 +973,10 @@ public interface NodeSelectionSearchCommands { * @see Query syntax * @see SearchReply * @see SearchArgs - * @see #ftSearch(String, Object, SearchArgs) + * @see #ftSearch(String, String, SearchArgs) */ @Experimental - Executions> ftSearch(String index, V query); + Executions> ftSearch(String index, String query); /** * Search the index with a textual query using advanced search options and filters. @@ -1025,23 +1024,23 @@ public interface NodeSelectionSearchCommands { * @see Advanced concepts * @see SearchReply * @see SearchArgs - * @see #ftSearch(String, Object) + * @see #ftSearch(String, String) */ @Experimental - Executions> ftSearch(String index, V query, SearchArgs args); + Executions> ftSearch(String index, String query, SearchArgs args); /** * Run a search query on an index and perform basic aggregate transformations using default options. * *

* This command executes a search query and applies aggregation operations to transform and analyze the results. Unlike - * {@link #ftSearch(String, Object)}, which returns individual documents, FT.AGGREGATE processes the result set through a + * {@link #ftSearch(String, String)}, which returns individual documents, FT.AGGREGATE processes the result set through a * pipeline of transformations to produce analytical insights, summaries, and computed values. *

* *

* This basic variant uses default aggregation behavior without additional pipeline operations. For advanced aggregations - * with grouping, sorting, filtering, and custom transformations, use {@link #ftAggregate(String, Object, AggregateArgs)}. + * with grouping, sorting, filtering, and custom transformations, use {@link #ftAggregate(String, String, AggregateArgs)}. *

* *

@@ -1067,10 +1066,10 @@ public interface NodeSelectionSearchCommands { * "https://redis.io/docs/latest/develop/interact/search-and-query/advanced-concepts/aggregations/">Aggregations * @see SearchReply * @see AggregateArgs - * @see #ftAggregate(String, Object, AggregateArgs) + * @see #ftAggregate(String, String, AggregateArgs) */ @Experimental - Executions> ftAggregate(String index, V query); + Executions> ftAggregate(String index, String query); /** * Run a search query on an index and perform advanced aggregate transformations with a processing pipeline. @@ -1122,18 +1121,18 @@ public interface NodeSelectionSearchCommands { * API * @see SearchReply * @see AggregateArgs - * @see #ftAggregate(String, Object) + * @see #ftAggregate(String, String) * @see #ftCursorread(String, Cursor) */ @Experimental - Executions> ftAggregate(String index, V query, AggregateArgs args); + Executions> ftAggregate(String index, String query, AggregateArgs args); /** * Read next results from an existing cursor and optionally override the batch size. * *

* This command is used to read the next batch of results from a cursor that was created by - * {@link #ftAggregate(String, Object, AggregateArgs)} with the {@code WITHCURSOR} option. Cursors provide an efficient way + * {@link #ftAggregate(String, String, AggregateArgs)} with the {@code WITHCURSOR} option. Cursors provide an efficient way * to iterate through large result sets without loading all results into memory at once. *

* @@ -1156,17 +1155,17 @@ public interface NodeSelectionSearchCommands { * "https://redis.io/docs/latest/develop/interact/search-and-query/advanced-concepts/aggregations/#cursor-api">Cursor * API * @see AggregationReply - * @see #ftAggregate(String, Object, AggregateArgs) + * @see #ftAggregate(String, String, AggregateArgs) */ @Experimental - Executions> ftCursorread(String index, Cursor cursor, int count); + Executions> ftCursorread(String index, Cursor cursor, int count); /** * Read next results from an existing cursor using the default batch size. * *

* This command is used to read the next batch of results from a cursor created by - * {@link #ftAggregate(String, Object, AggregateArgs)} with the {@code WITHCURSOR} option. This variant uses the default + * {@link #ftAggregate(String, String, AggregateArgs)} with the {@code WITHCURSOR} option. This variant uses the default * batch size that was specified in the original {@code FT.AGGREGATE} command's {@code WITHCURSOR} clause. *

* @@ -1188,16 +1187,16 @@ public interface NodeSelectionSearchCommands { * "https://redis.io/docs/latest/develop/interact/search-and-query/advanced-concepts/aggregations/#cursor-api">Cursor * API * @see AggregationReply - * @see #ftAggregate(String, Object, AggregateArgs) + * @see #ftAggregate(String, String, AggregateArgs) */ @Experimental - Executions> ftCursorread(String index, Cursor cursor); + Executions> ftCursorread(String index, Cursor cursor); /** * Delete a cursor and free its associated resources. * *

- * This command is used to explicitly delete a cursor created by {@link #ftAggregate(String, Object, AggregateArgs)} with + * This command is used to explicitly delete a cursor created by {@link #ftAggregate(String, String, AggregateArgs)} with * the {@code WITHCURSOR} option. Deleting a cursor frees up server resources and should be done when you no longer need to * read more results from the cursor. *

@@ -1225,7 +1224,7 @@ public interface NodeSelectionSearchCommands { * @see Cursor * API - * @see #ftAggregate(String, Object, AggregateArgs) + * @see #ftAggregate(String, String, AggregateArgs) * @see #ftCursorread(String, Cursor) * @see #ftCursorread(String, Cursor, int) */ @@ -1244,6 +1243,6 @@ public interface NodeSelectionSearchCommands { * @since 7.2 */ @Experimental - Executions> ftHybrid(String index, HybridArgs args); + Executions> ftHybrid(String index, HybridArgs args); } diff --git a/src/main/java/io/lettuce/core/cluster/api/sync/RedisClusterCommands.java b/src/main/java/io/lettuce/core/cluster/api/sync/RedisClusterCommands.java index 74b924d5c3..10cd2ecd8e 100644 --- a/src/main/java/io/lettuce/core/cluster/api/sync/RedisClusterCommands.java +++ b/src/main/java/io/lettuce/core/cluster/api/sync/RedisClusterCommands.java @@ -45,7 +45,7 @@ public interface RedisClusterCommands extends BaseRedisCommands, Red RedisFunctionCommands, RedisGeoCommands, RedisHashCommands, RedisHLLCommands, RedisKeyCommands, RedisListCommands, RedisScriptingCommands, RedisServerCommands, RedisSetCommands, RedisSortedSetCommands, RedisStreamCommands, RedisStringCommands, - RedisJsonCommands, RedisVectorSetCommands, RediSearchCommands, RedisArrayCommands, + RedisJsonCommands, RedisVectorSetCommands, RediSearchCommands, RedisArrayCommands, RedisBloomFilterCommands, RedisCuckooFilterCommands, RedisTopKCommands { /** diff --git a/src/main/java/io/lettuce/core/codec/RedisCodec.java b/src/main/java/io/lettuce/core/codec/RedisCodec.java index 9c0b3d5ae9..850586c1da 100644 --- a/src/main/java/io/lettuce/core/codec/RedisCodec.java +++ b/src/main/java/io/lettuce/core/codec/RedisCodec.java @@ -7,6 +7,18 @@ * * The methods are called by multiple threads and must be thread-safe. * + *

Type semantics

+ *

+ * {@code K} represents a Redis key (or a hash field of such a key) routed through {@link #encodeKey(Object)} / + * {@link #decodeKey(ByteBuffer)}. + *

+ *

+ * {@code V} represents a value that is stored under a Redis key (or under a hash field of such a key) and that the server + * returns as opaque bytes; values round-trip through {@link #encodeValue(Object)} / {@link #decodeValue(ByteBuffer)}. Protocol + * literals (command keywords, DSL expressions, index/dictionary/synonym names, module/expander identifiers, and other metadata + * interpreted by the server) are not values in this sense and should not be routed through {@code V}. + *

+ * * @param Key type. * @param Value type. * diff --git a/src/main/java/io/lettuce/core/search/AggregateReplyParser.java b/src/main/java/io/lettuce/core/search/AggregateReplyParser.java index 516a4260fc..4d489b7379 100644 --- a/src/main/java/io/lettuce/core/search/AggregateReplyParser.java +++ b/src/main/java/io/lettuce/core/search/AggregateReplyParser.java @@ -25,21 +25,20 @@ *

* * @param Key type. - * @param Value type. * @author Tihomir Mateev * @since 6.8 * @see SearchReplyParser * @see SearchReply */ -public class AggregateReplyParser implements ComplexDataParser> { +public class AggregateReplyParser implements ComplexDataParser> { private static final InternalLogger LOG = InternalLoggerFactory.getInstance(AggregateReplyParser.class); - private final SearchReplyParser searchReplyParser; + private final SearchReplyParser searchReplyParser; private final boolean withCursor; - public AggregateReplyParser(RedisCodec codec, boolean withCursor) { + public AggregateReplyParser(RedisCodec codec, boolean withCursor) { this.searchReplyParser = new SearchReplyParser<>(codec); this.withCursor = withCursor; } @@ -54,8 +53,8 @@ public AggregateReplyParser(RedisCodec codec, boolean withCursor) { * @return a list of SearchReply objects, one for each aggregation result */ @Override - public AggregationReply parse(ComplexData data) { - AggregationReply reply = new AggregationReply<>(); + public AggregationReply parse(ComplexData data) { + AggregationReply reply = new AggregationReply<>(); if (data == null) { return reply; @@ -63,7 +62,7 @@ public AggregationReply parse(ComplexData data) { try { if (!withCursor) { - SearchReply searchReply = searchReplyParser.parse(data); + SearchReply searchReply = searchReplyParser.parse(data); reply.addSearchReply(searchReply); return reply; } @@ -85,7 +84,7 @@ public AggregationReply parse(ComplexData data) { } } else if (aggregateResult instanceof ComplexData) { // Each element should be a ComplexData that can be parsed by SearchReplyParser - SearchReply searchReply = searchReplyParser.parse((ComplexData) aggregateResult); + SearchReply searchReply = searchReplyParser.parse((ComplexData) aggregateResult); reply.addSearchReply(searchReply); replyRead = true; } diff --git a/src/main/java/io/lettuce/core/search/AggregationReply.java b/src/main/java/io/lettuce/core/search/AggregationReply.java index 9323b3068c..46da8567a1 100644 --- a/src/main/java/io/lettuce/core/search/AggregationReply.java +++ b/src/main/java/io/lettuce/core/search/AggregationReply.java @@ -41,16 +41,15 @@ *

* * @param the type of keys used in the aggregation results - * @param the type of values used in the aggregation results * @author Redis Ltd. * @since 6.8 * @see SearchReply */ -public class AggregationReply { +public class AggregationReply { long aggregationGroups = 1; - List> replies = new ArrayList<>(); + List> replies = new ArrayList<>(); /** * Optional Cursor metadata of the shard that created/owns the cursor. Present only when running in cluster mode, WITHCURSOR @@ -117,7 +116,7 @@ public long getAggregationGroups() { * @return a mutable list of {@link SearchReply} objects containing the aggregation results. Never {@code null}, but may be * empty if no results were found. */ - public List> getReplies() { + public List> getReplies() { return replies; } @@ -150,7 +149,7 @@ void setGroupCount(long value) { this.aggregationGroups = value; } - void addSearchReply(SearchReply searchReply) { + void addSearchReply(SearchReply searchReply) { this.replies.add(searchReply); } diff --git a/src/main/java/io/lettuce/core/search/FieldValue.java b/src/main/java/io/lettuce/core/search/FieldValue.java new file mode 100644 index 0000000000..b83919565a --- /dev/null +++ b/src/main/java/io/lettuce/core/search/FieldValue.java @@ -0,0 +1,310 @@ +/* + * Copyright 2026-present, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ +package io.lettuce.core.search; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import io.lettuce.core.internal.LettuceAssert; + +/** + * A single field value from a Redis search reply, retained in the exact shape returned by the server. + *

+ * A search reply carries no per-field type information, so the client cannot know whether a value is textual (a + * {@code TEXT}/{@code NUMERIC} field or an aggregation expression) or binary (a vector embedding returned through + * {@code RETURN} or {@code LOAD}). {@code FieldValue} keeps the exact bytes and lets the caller decide how to read them: + * {@link #asString()} for text and numbers, {@link #asBytes()} for binary content where UTF-8 decoding would corrupt the value. + *

+ * Most field values are scalars, but aggregation reducers such as {@code FT.AGGREGATE REDUCE COLLECT} produce nested column + * values. A {@code FieldValue} therefore has a {@link Kind}: {@link Kind#SCALAR} for raw bytes, {@link Kind#ARRAY} for a list + * of nested values, {@link Kind#MAP} for named nested values, and {@link Kind#NULL} for a server-returned null. A collected + * column is an {@link Kind#ARRAY} with one element per collected entry; read it via {@link #asList()} and read each entry via + * {@link #asMap()}, which normalizes the protocol-specific entry shape (RESP3 returns each entry as a map, RESP2 as a flat + * key/value array). + *

+ * The server can also return a field with a null value (for example a JSON {@code null} loaded through {@code RETURN} or + * {@code LOAD}). Such a field is kept with its key present and reported by {@link #isNull()}; all accessors return {@code null} + * for it. The {@code FieldValue} itself is never {@code null} in {@link SearchReply.SearchResult#getFields()}; a field that was + * not returned at all is represented by the absence of its key. Reading a non-null value through an accessor of a different + * kind (for example {@link #asString()} on an {@link Kind#ARRAY}) throws {@link IllegalStateException}. + * + * @author Viktoriya Kutsarova + * @since 7.7 + */ +public final class FieldValue { + + /** + * The shape of a {@link FieldValue}. + * + * @since 7.7 + */ + public enum Kind { + + /** + * A scalar value, kept as the raw bytes returned by the server. + */ + SCALAR, + + /** + * An array of nested values, for example a collected aggregation column. + */ + ARRAY, + + /** + * A map of nested values keyed by name, for example a collected entry under RESP3. + */ + MAP, + + /** + * A null value returned by the server. + */ + NULL + + } + + /** + * Shared instance representing a field that the server returned with a null value. + */ + static final FieldValue NULL = new FieldValue(Kind.NULL, null); + + private final Kind kind; + + private final Object value; + + private FieldValue(Kind kind, Object value) { + this.kind = kind; + this.value = value; + } + + /** + * Wraps the raw bytes of a scalar field value. + * + * @param value the raw field value exactly as returned by the server. Must not be {@code null}. + * @return a {@link Kind#SCALAR} {@link FieldValue} view over the given bytes + */ + public static FieldValue of(byte[] value) { + LettuceAssert.notNull(value, "Field value must not be null"); + return new FieldValue(Kind.SCALAR, value); + } + + /** + * Creates an array field value from the given elements. + * + * @param elements the array elements. Must not be {@code null} and must not contain {@code null} elements (use + * {@link #nullValue()} for a null element). + * @return a {@link Kind#ARRAY} {@link FieldValue} holding a copy of the given elements + * @since 7.7 + */ + public static FieldValue array(List elements) { + LettuceAssert.notNull(elements, "Field value elements must not be null"); + LettuceAssert.noNullElements(elements, "Field value elements must not contain null elements"); + return new FieldValue(Kind.ARRAY, Collections.unmodifiableList(new ArrayList<>(elements))); + } + + /** + * Creates a map field value from the given entries, preserving their iteration order. + * + * @param entries the map entries. Must not be {@code null} and must not contain {@code null} keys or values (use + * {@link #nullValue()} for a null value). + * @return a {@link Kind#MAP} {@link FieldValue} holding a copy of the given entries + * @since 7.7 + */ + public static FieldValue map(Map entries) { + LettuceAssert.notNull(entries, "Field value entries must not be null"); + Map copy = new LinkedHashMap<>(entries.size()); + entries.forEach((key, value) -> { + LettuceAssert.notNull(key, "Field value entry keys must not be null"); + LettuceAssert.notNull(value, "Field value entry values must not be null"); + copy.put(key, value); + }); + return new FieldValue(Kind.MAP, Collections.unmodifiableMap(copy)); + } + + /** + * Returns the {@link FieldValue} representing a server-returned null value. + * + * @return the shared {@link Kind#NULL} instance + * @since 7.7 + */ + public static FieldValue nullValue() { + return NULL; + } + + /** + * Returns the {@link Kind} of this field value. + * + * @return the kind, never {@code null} + * @since 7.7 + */ + public Kind getKind() { + return kind; + } + + /** + * Gets the raw field bytes, exactly as returned by the server. Use this accessor for binary fields such as vector + * embeddings, where UTF-8 decoding would corrupt the value. + * + * @return the raw field bytes, or {@code null} if the server returned a null value (see {@link #isNull()}). This is the + * backing array and must not be modified. + * @throws IllegalStateException if this value is an {@link Kind#ARRAY} or a {@link Kind#MAP} + */ + public byte[] asBytes() { + if (kind == Kind.NULL) { + return null; + } + if (kind != Kind.SCALAR) { + throw wrongKind(Kind.SCALAR); + } + return (byte[]) value; + } + + /** + * Gets the field value decoded as UTF-8 text. This suits textual and numeric fields. Binary values (for example vector + * embeddings) are not valid UTF-8 and are corrupted by this view; read those via {@link #asBytes()}. + * + * @return the field value decoded as UTF-8, or {@code null} if the server returned a null value (see {@link #isNull()}) + * @throws IllegalStateException if this value is an {@link Kind#ARRAY} or a {@link Kind#MAP} + */ + public String asString() { + return asString(StandardCharsets.UTF_8); + } + + /** + * Gets the field value decoded as text using the given charset. + * + * @param charset the charset to decode with + * @return the decoded field value, or {@code null} if the server returned a null value (see {@link #isNull()}) + * @throws IllegalStateException if this value is an {@link Kind#ARRAY} or a {@link Kind#MAP} + */ + public String asString(Charset charset) { + byte[] bytes = asBytes(); + return bytes == null ? null : new String(bytes, charset); + } + + /** + * Gets the elements of an array field value, for example the entries of a collected aggregation column. + * + * @return an unmodifiable list of the array elements, or {@code null} if the server returned a null value (see + * {@link #isNull()}) + * @throws IllegalStateException if this value is a {@link Kind#SCALAR} or a {@link Kind#MAP} + * @since 7.7 + */ + @SuppressWarnings("unchecked") + public List asList() { + if (kind == Kind.NULL) { + return null; + } + if (kind != Kind.ARRAY) { + throw wrongKind(Kind.ARRAY); + } + return (List) value; + } + + /** + * Gets the field value as a map of named nested values. + *

+ * A {@link Kind#MAP} value is returned directly. An {@link Kind#ARRAY} value is interpreted as a flat list of key/value + * pairs — the shape RESP2 uses for a collected aggregation entry — where each key must be a scalar and is decoded as UTF-8; + * an array of odd length, or one whose key positions hold non-scalar values, does not represent key/value pairs and is + * rejected. This makes a collected entry readable the same way on RESP2 and RESP3. Note that any even-length array of + * scalars is accepted by this interpretation, including values (for example a {@code TOLIST} column) that the server did + * not produce as key/value pairs — it is the caller's responsibility to apply this view only to fields that hold pairs. + * + * @return an unmodifiable, ordered map of the named nested values, or {@code null} if the server returned a null value (see + * {@link #isNull()}) + * @throws IllegalStateException if this value is a {@link Kind#SCALAR}, or an {@link Kind#ARRAY} that does not represent + * key/value pairs + * @since 7.7 + */ + @SuppressWarnings("unchecked") + public Map asMap() { + if (kind == Kind.NULL) { + return null; + } + if (kind == Kind.MAP) { + return (Map) value; + } + if (kind == Kind.ARRAY) { + return pairsToMap((List) value); + } + throw wrongKind(Kind.MAP); + } + + /** + * Reports whether the server returned this field with a null value. When {@code true}, all accessors return {@code null}. + * + * @return {@code true} if this field value is null + */ + public boolean isNull() { + return kind == Kind.NULL; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof FieldValue)) { + return false; + } + FieldValue other = (FieldValue) o; + if (kind != other.kind) { + return false; + } + if (kind == Kind.SCALAR) { + return Arrays.equals((byte[]) value, (byte[]) other.value); + } + return Objects.equals(value, other.value); + } + + @Override + public int hashCode() { + int valueHash = kind == Kind.SCALAR ? Arrays.hashCode((byte[]) value) : Objects.hashCode(value); + return 31 * kind.hashCode() + valueHash; + } + + @Override + public String toString() { + switch (kind) { + case NULL: + return "null"; + case SCALAR: + return new String((byte[]) value, StandardCharsets.UTF_8); + default: + return value.toString(); + } + } + + private static Map pairsToMap(List pairs) { + if (pairs.size() % 2 != 0) { + throw new IllegalStateException( + "Array field value of size " + pairs.size() + " does not represent key/value pairs"); + } + Map map = new LinkedHashMap<>(pairs.size() / 2); + for (int i = 0; i < pairs.size(); i += 2) { + FieldValue key = pairs.get(i); + if (key.getKind() != Kind.SCALAR) { + throw new IllegalStateException( + "Array field value does not represent key/value pairs, key at index " + i + " is " + key.getKind()); + } + map.put(key.asString(), pairs.get(i + 1)); + } + return Collections.unmodifiableMap(map); + } + + private IllegalStateException wrongKind(Kind requested) { + return new IllegalStateException("Field value is " + kind + ", not " + requested); + } + +} diff --git a/src/main/java/io/lettuce/core/search/HybridReply.java b/src/main/java/io/lettuce/core/search/HybridReply.java index 2bb7265ca4..23bf6bbc37 100644 --- a/src/main/java/io/lettuce/core/search/HybridReply.java +++ b/src/main/java/io/lettuce/core/search/HybridReply.java @@ -10,33 +10,32 @@ import java.util.ArrayList; import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; /** * Represents the results of an {@code FT.HYBRID} command. Contains total result count, execution time, warnings, and a list of - * result field maps. + * results. *

- * Each result is a {@link Map} of field names to values. The document key is available under the reserved field name - * {@code __key} when returning individual documents. Score information (text score, vector distance, combined score) is - * included when using {@code YIELD_SCORE_AS} in the query. + * Each result carries its decoded document key and returned fields. Score information (text score, vector distance, combined + * score) is included when using {@code YIELD_SCORE_AS} in the query. *

* - * @param Key type. - * @param Value type. + * @param document key type * @author Aleksandar Todorov * @since 7.2 */ @Experimental -public class HybridReply { +public class HybridReply { private long totalResults; private double executionTime; - private final List> results; + private final List> results; - private final List warnings = new ArrayList<>(); + private final List warnings = new ArrayList<>(); /** * Creates a new empty HybridReply instance. @@ -80,25 +79,25 @@ public void setExecutionTime(double executionTime) { } /** - * @return an unmodifiable view of all results returned by the command. Each result is a map of field names to values. + * @return an unmodifiable view of all results returned by the command */ - public List> getResults() { + public List> getResults() { return Collections.unmodifiableList(results); } /** * Add a new result entry. * - * @param result the result map to add + * @param result the result to add */ - public void addResult(Map result) { + public void addResult(HybridResult result) { this.results.add(result); } /** * @return a read-only view of all warnings reported by the server */ - public List getWarnings() { + public List getWarnings() { return Collections.unmodifiableList(warnings); } @@ -107,7 +106,7 @@ public List getWarnings() { * * @param warning the warning to add */ - public void addWarning(V warning) { + public void addWarning(String warning) { this.warnings.add(warning); } @@ -125,4 +124,56 @@ public boolean isEmpty() { return results.isEmpty(); } + /** + * Represents a single {@code FT.HYBRID} result entry. + *

+ * {@link #getFields()} maps each field name to a {@link FieldValue}, which retains the exact bytes returned by the server + * and can be read as either text ({@link FieldValue#asString()}) or binary ({@link FieldValue#asBytes()}). This lets a + * single result mix textual/numeric fields with binary fields such as vector embeddings loaded via {@code LOAD}, where + * UTF-8 decoding would corrupt the value. + * + * @param document key type + */ + public static class HybridResult { + + private K id; + + private final Map fields = new LinkedHashMap<>(); + + /** + * Get the decoded document key. + * + * @return the document key, or {@code null} when the result does not represent an individual document + * @since 7.7 + */ + public K getId() { + return id; + } + + void setId(K id) { + this.id = id; + } + + /** + * Gets the result fields, mapping each field name to its {@link FieldValue}, in the order returned by the server. Read + * each value as text via {@link FieldValue#asString()} or as raw bytes via {@link FieldValue#asBytes()}. + * + * @return an unmodifiable, ordered map of field name to {@link FieldValue}, or an empty map if not available + */ + public Map getFields() { + return Collections.unmodifiableMap(fields); + } + + /** + * Adds a single result field. + * + * @param key the field name + * @param value the raw field value + */ + public void addField(String key, byte[] value) { + this.fields.put(key, value == null ? FieldValue.NULL : FieldValue.of(value)); + } + + } + } diff --git a/src/main/java/io/lettuce/core/search/HybridReplyParser.java b/src/main/java/io/lettuce/core/search/HybridReplyParser.java index da77a0cb73..63ded96288 100644 --- a/src/main/java/io/lettuce/core/search/HybridReplyParser.java +++ b/src/main/java/io/lettuce/core/search/HybridReplyParser.java @@ -9,30 +9,32 @@ import io.lettuce.core.annotations.Experimental; import io.lettuce.core.codec.RedisCodec; import io.lettuce.core.codec.StringCodec; +import io.lettuce.core.internal.LettuceAssert; import io.lettuce.core.output.ComplexData; import io.lettuce.core.output.ComplexDataParser; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; import java.nio.ByteBuffer; -import java.util.HashMap; import java.util.List; import java.util.Map; /** * Parser for {@code FT.HYBRID} responses. Handles both RESP2 and RESP3 protocol formats. + *

+ * Field names are schema identifiers and are decoded as raw UTF-8; field values are kept as raw bytes so that binary content + * (for example vector embeddings) survives the round-trip. * - * @param Key type. - * @param Value type. + * @param document key type * @author Aleksandar Todorov * @since 7.2 */ @Experimental -public class HybridReplyParser implements ComplexDataParser> { +public class HybridReplyParser implements ComplexDataParser> { private static final InternalLogger LOG = InternalLoggerFactory.getInstance(HybridReplyParser.class); - private final RedisCodec codec; + private final RedisCodec codec; private final ByteBuffer TOTAL_RESULTS_KEY = StringCodec.UTF8.encodeKey("total_results"); @@ -42,14 +44,23 @@ public class HybridReplyParser implements ComplexDataParser codec) { + private final ByteBuffer DOCUMENT_KEY = StringCodec.UTF8.encodeKey("__key"); + + /** + * Create a parser that decodes document keys through {@code codec}. + * + * @param codec connection codec, must not be {@code null}. + * @since 7.7 + */ + public HybridReplyParser(RedisCodec codec) { + LettuceAssert.notNull(codec, "RedisCodec must not be null"); this.codec = codec; } @Override - public HybridReply parse(ComplexData data) { + public HybridReply parse(ComplexData data) { try { - HybridReply hybridReply = new HybridReply<>(); + HybridReply hybridReply = new HybridReply<>(); if (data.isList()) { parseResp2(data, hybridReply); @@ -64,7 +75,13 @@ public HybridReply parse(ComplexData data) { } } - private void parseResp2(ComplexData data, HybridReply reply) { + private static byte[] toBytes(ByteBuffer buffer) { + byte[] bytes = new byte[buffer.remaining()]; + buffer.duplicate().get(bytes); + return bytes; + } + + private void parseResp2(ComplexData data, HybridReply reply) { List list = data.getDynamicList(); if (list == null || list.isEmpty()) { return; @@ -72,7 +89,7 @@ private void parseResp2(ComplexData data, HybridReply reply) { parseResp2(list, reply); } - private void parseResp2(List list, HybridReply reply) { + private void parseResp2(List list, HybridReply reply) { // RESP2 format: ["key1", value1, "key2", value2, ...] // Parse as key-value pairs for (int i = 0; i + 1 < list.size(); i += 2) { @@ -107,7 +124,7 @@ private void parseResp2(List list, HybridReply reply) { if (warnList != null) { for (Object o : warnList) { if (o instanceof ByteBuffer) { - reply.addWarning(codec.decodeValue((ByteBuffer) o)); + reply.addWarning(StringCodec.UTF8.decodeValue((ByteBuffer) o)); } } } @@ -119,7 +136,7 @@ private void parseResp2(List list, HybridReply reply) { if (resultsList != null) { for (Object resultObj : resultsList) { if (resultObj instanceof ComplexData) { - Map result = new HashMap<>(); + HybridReply.HybridResult result = new HybridReply.HybridResult<>(); addFieldsFromComplexData((ComplexData) resultObj, result); reply.addResult(result); } @@ -130,7 +147,7 @@ private void parseResp2(List list, HybridReply reply) { } } - private void parseResp3(ComplexData data, HybridReply reply) { + private void parseResp3(ComplexData data, HybridReply reply) { Map resultsMap = data.getDynamicMap(); if (resultsMap == null || resultsMap.isEmpty()) { return; @@ -160,7 +177,7 @@ private void parseResp3(ComplexData data, HybridReply reply) { if (warnList != null) { for (Object o : warnList) { if (o instanceof ByteBuffer) { - reply.addWarning(codec.decodeValue((ByteBuffer) o)); + reply.addWarning(StringCodec.UTF8.decodeValue((ByteBuffer) o)); } } } @@ -183,39 +200,13 @@ private void parseResp3(ComplexData data, HybridReply reply) { } ComplexData resultData = (ComplexData) raw; - Map result = parseResultEntry(resultData); - reply.addResult(result); - } - } - - private Map parseResultEntry(ComplexData resultData) { - Map entryMap; - try { - entryMap = resultData.getDynamicMap(); - } catch (UnsupportedOperationException e) { - entryMap = null; - } - - Map result = new HashMap<>(); - - if (entryMap != null && !entryMap.isEmpty()) { - entryMap.forEach((key, value) -> { - if (!(key instanceof ByteBuffer) || !(value instanceof ByteBuffer)) { - return; - } - - K fieldKey = codec.decodeKey((ByteBuffer) key); - V fieldValue = codec.decodeValue((ByteBuffer) value); - result.put(fieldKey, fieldValue); - }); - } else { + HybridReply.HybridResult result = new HybridReply.HybridResult<>(); addFieldsFromComplexData(resultData, result); + reply.addResult(result); } - - return result; } - private void addFieldsFromComplexData(ComplexData data, Map result) { + private void addFieldsFromComplexData(ComplexData data, HybridReply.HybridResult result) { Map map; try { map = data.getDynamicMap(); @@ -225,12 +216,16 @@ private void addFieldsFromComplexData(ComplexData data, Map result) { if (map != null && !map.isEmpty()) { map.forEach((k, v) -> { - if (!(k instanceof ByteBuffer) || !(v instanceof ByteBuffer)) { + if (!(k instanceof ByteBuffer) || (v != null && !(v instanceof ByteBuffer))) { return; } - K decodedKey = codec.decodeKey((ByteBuffer) k); - V decodedValue = codec.decodeValue((ByteBuffer) v); - result.put(decodedKey, decodedValue); + if (k.equals(DOCUMENT_KEY)) { + if (v != null) { + result.setId(codec.decodeKey((ByteBuffer) v)); + } + return; + } + result.addField(StringCodec.UTF8.decodeKey((ByteBuffer) k), v == null ? null : toBytes((ByteBuffer) v)); }); return; } @@ -243,12 +238,16 @@ private void addFieldsFromComplexData(ComplexData data, Map result) { for (int i = 0; i + 1 < list.size(); i += 2) { Object k = list.get(i); Object v = list.get(i + 1); - if (!(k instanceof ByteBuffer) || !(v instanceof ByteBuffer)) { + if (!(k instanceof ByteBuffer) || (v != null && !(v instanceof ByteBuffer))) { + continue; + } + if (k.equals(DOCUMENT_KEY)) { + if (v != null) { + result.setId(codec.decodeKey((ByteBuffer) v)); + } continue; } - K decodedKey = codec.decodeKey((ByteBuffer) k); - V decodedValue = codec.decodeValue((ByteBuffer) v); - result.put(decodedKey, decodedValue); + result.addField(StringCodec.UTF8.decodeKey((ByteBuffer) k), v == null ? null : toBytes((ByteBuffer) v)); } } diff --git a/src/main/java/io/lettuce/core/search/SearchReply.java b/src/main/java/io/lettuce/core/search/SearchReply.java index 7851bf3e08..35911ce336 100644 --- a/src/main/java/io/lettuce/core/search/SearchReply.java +++ b/src/main/java/io/lettuce/core/search/SearchReply.java @@ -9,7 +9,7 @@ import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -17,24 +17,23 @@ * Represents the results of a Redis FT.SEARCH command. *

* This class encapsulates the search results including the total count of matching documents and a list of individual search - * result documents. Each document contains the document ID and optionally the document fields, score, payload, and sort keys - * depending on the search arguments used. + * result documents. Each document contains the document ID and optionally the document fields and score depending on the search + * arguments used. * * @param Key type. - * @param Value type. * @author Tihomir Mateev * @since 6.8 * @see FT.SEARCH */ -public class SearchReply { +public class SearchReply { private long count; - private final List> results; + private final List> results; private Long cursorId; - private final List warnings = new ArrayList<>(); + private final List warnings = new ArrayList<>(); /** * Creates a new empty SearchReply instance. @@ -51,7 +50,7 @@ public SearchReply() { * @param count the total number of matching documents * @param results the list of search result documents */ - SearchReply(long count, List> results) { + SearchReply(long count, List> results) { this.count = count; this.results = new ArrayList<>(results); this.cursorId = null; @@ -81,12 +80,11 @@ void setCount(long count) { /** * Gets the list of search result documents. *

- * Each result contains the document ID and optionally the document fields, score, payload, and sort keys depending on the - * search arguments used. + * Each result contains the document ID and optionally the document fields and score depending on the search arguments used. * * @return an unmodifiable list of search result documents */ - public List> getResults() { + public List> getResults() { return Collections.unmodifiableList(results); } @@ -95,7 +93,7 @@ public List> getResults() { * * @param result the search result document to add */ - public void addResult(SearchResult result) { + public void addResult(SearchResult result) { this.results.add(result); } @@ -134,7 +132,7 @@ public Long getCursorId() { /** * @return a {@link List} of all the warnings generated during the execution of this search */ - public List getWarnings() { + public List getWarnings() { return this.warnings; } @@ -152,27 +150,27 @@ void setCursorId(Long cursorId) { * * @param v the warning to add */ - void addWarning(V v) { + void addWarning(String v) { this.warnings.add(v); } /** * Represents a single search result document. + *

+ * {@link #getFields()} maps each field name to a {@link FieldValue}, which retains the exact bytes returned by the server + * and can be read as either text ({@link FieldValue#asString()}) or binary ({@link FieldValue#asBytes()}). This lets a + * single document mix textual/numeric fields with binary fields such as vector embeddings, where UTF-8 decoding would + * corrupt the value. * - * @param Key type. - * @param Value type. + * @param Key type of the document id. */ - public static class SearchResult { + public static class SearchResult { private final K id; private Double score; - private V payload; - - private V sortKey; - - private final Map fields = new HashMap<>(); + private final Map fields = new LinkedHashMap<>(); /** * Creates a new SearchResult with the specified document ID. @@ -217,74 +215,50 @@ void setScore(Double score) { } /** - * Gets the document payload. + * Gets the document fields, mapping each field name to its {@link FieldValue}, in the order returned by the server. If + * NOCONTENT was used in the search, this will be empty. Read each value as text via {@link FieldValue#asString()} or as + * raw bytes via {@link FieldValue#asBytes()}. *

- * This is only available if WITHPAYLOADS was used in the search. + * Aggregation reducers that produce non-scalar columns (for example {@code FT.AGGREGATE REDUCE COLLECT}) are + * represented as {@link FieldValue.Kind#ARRAY} values with one element per collected entry; read the entries via + * {@link FieldValue#asList()} and each entry via {@link FieldValue#asMap()}, which normalizes the protocol-specific + * entry shape. Entry order within a collected column follows the server ({@code SORTBY} order); iteration order of + * {@link FieldValue.Kind#MAP} values is not guaranteed to match the server. * - * @return the document payload, or null if not available + * @return an unmodifiable, ordered map of field name to {@link FieldValue}, or an empty map if not available */ - public V getPayload() { - return payload; + public Map getFields() { + return Collections.unmodifiableMap(fields); } /** - * Sets the document payload. - * - * @param payload the document payload - */ - void setPayload(V payload) { - this.payload = payload; - } - - /** - * Gets the sort key. - *

- * This is only available if WITHSORTKEYS was used in the search. - * - * @return the sort key, or null if not available - */ - public V getSortKey() { - return sortKey; - } - - /** - * Sets the sort key. - * - * @param sortKey the sort key - */ - void setSortKey(V sortKey) { - this.sortKey = sortKey; - } - - /** - * Gets the document fields. - *

- * This contains the field names and values of the document. If NOCONTENT was used in the search, this will be null or - * empty. + * Adds all the provided fields * - * @return the document fields, or null if not available + * @param fields the document fields, keyed by name, with raw byte values */ - public Map getFields() { - return fields; + public void addFields(Map fields) { + fields.forEach(this::addField); } /** - * Adds all the provided fields + * Adds a single document field * - * @param fields the document fields + * @param key the field name + * @param value the raw field value */ - public void addFields(Map fields) { - this.fields.putAll(fields); + public void addField(String key, byte[] value) { + addField(key, value == null ? FieldValue.NULL : FieldValue.of(value)); } /** - * Adds a single document field + * Adds a single document field. * * @param key the field name - * @param value the field value + * @param value the field value; {@code null} is stored as a {@link FieldValue#isNull() null value} + * @since 7.7 */ - public void addFields(K key, V value) { - this.fields.put(key, value); + public void addField(String key, FieldValue value) { + this.fields.put(key, value == null ? FieldValue.NULL : value); } } diff --git a/src/main/java/io/lettuce/core/search/SearchReplyParser.java b/src/main/java/io/lettuce/core/search/SearchReplyParser.java index f67a3f63ea..e29d158c10 100644 --- a/src/main/java/io/lettuce/core/search/SearchReplyParser.java +++ b/src/main/java/io/lettuce/core/search/SearchReplyParser.java @@ -15,10 +15,11 @@ import io.netty.util.internal.logging.InternalLoggerFactory; import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.stream.Collectors; -import java.util.stream.IntStream; /** * Parser for Redis Search (RediSearch) command responses that converts raw Redis data into structured {@link SearchReply} @@ -37,16 +38,22 @@ *

  • Total result counts
  • * * - * @param the type of keys used in the search results - * @param the type of values used in the search results + *

    + * Document ids are decoded through the connection's key codec. Field names are schema identifiers (hash field names, JSONPath + * expressions or aliases) and are decoded as raw UTF-8; field values are kept as raw bytes so that binary content (for example + * vector embeddings) survives the round-trip. Nested field values produced by aggregation reducers (for example + * {@code REDUCE COLLECT}) are converted recursively into {@link FieldValue} arrays and maps. + *

    + * + * @param the type of the document id in the search results * @author Redis Ltd. * @since 6.8 */ -public class SearchReplyParser implements ComplexDataParser> { +public class SearchReplyParser implements ComplexDataParser> { private static final InternalLogger LOG = InternalLoggerFactory.getInstance(SearchReplyParser.class); - private final RedisCodec codec; + private final RedisCodec codec; private final boolean withScores; @@ -59,7 +66,7 @@ public class SearchReplyParser implements ComplexDataParser @@ -68,7 +75,7 @@ public class SearchReplyParser implements ComplexDataParserDocument IDs are always parsed when using this constructor * */ - public SearchReplyParser(RedisCodec codec, SearchArgs args) { + public SearchReplyParser(RedisCodec codec, SearchArgs args) { this.codec = codec; this.withScores = args != null && args.isWithScores(); this.withContent = args == null || !args.isNoContent(); @@ -88,9 +95,9 @@ public SearchReplyParser(RedisCodec codec, SearchArgs args) { *
  • IDs are not parsed ({@code withIds = false})
  • * * - * @param codec the Redis codec used for encoding/decoding keys and values. Must not be {@code null}. + * @param codec the Redis codec used for decoding document ids. Must not be {@code null}. */ - public SearchReplyParser(RedisCodec codec) { + public SearchReplyParser(RedisCodec codec) { this.codec = codec; this.withScores = false; this.withContent = true; @@ -106,7 +113,7 @@ public SearchReplyParser(RedisCodec codec) { * {@link SearchReply} if parsing fails. */ @Override - public SearchReply parse(ComplexData data) { + public SearchReply parse(ComplexData data) { try { if (data.isList()) { return new Resp2SearchResultsParser().parse(data); @@ -119,11 +126,51 @@ public SearchReply parse(ComplexData data) { } } - class Resp2SearchResultsParser implements ComplexDataParser> { + private static byte[] toBytes(ByteBuffer buffer) { + byte[] bytes = new byte[buffer.remaining()]; + buffer.duplicate().get(bytes); + return bytes; + } + + /** + * Converts a raw field value as produced by the RESP parser into a {@link FieldValue}. Scalar values keep their exact + * bytes. Aggregation reducers such as {@code COLLECT} and {@code TOLIST} produce nested values (arrays or, under RESP3, + * maps); these are converted recursively so the raw protocol shape stays readable through + * {@link FieldValue#asList()}/{@link FieldValue#asMap()} instead of failing to parse. + */ + private static FieldValue toFieldValue(Object value) { + if (value == null) { + return FieldValue.NULL; + } + if (value instanceof ByteBuffer) { + return FieldValue.of(toBytes((ByteBuffer) value)); + } + if (value instanceof ComplexData) { + ComplexData data = (ComplexData) value; + if (data.isMap()) { + Map map = new LinkedHashMap<>(); + data.getDynamicMap().forEach((key, nested) -> map.put(decodeNestedKey(key), toFieldValue(nested))); + return FieldValue.map(map); + } + List list = new ArrayList<>(); + for (Object element : data.getDynamicList()) { + list.add(toFieldValue(element)); + } + return FieldValue.array(list); + } + // Scalars the RESP parser has already materialized (Long, Double, Boolean): keep their textual form. + return FieldValue.of(String.valueOf(value).getBytes(StandardCharsets.US_ASCII)); + } + + private static String decodeNestedKey(Object key) { + return key instanceof ByteBuffer ? StringCodec.UTF8.decodeKey((ByteBuffer) key) : String.valueOf(key); + } + + class Resp2SearchResultsParser implements ComplexDataParser> { @Override - public SearchReply parse(ComplexData data) { - final SearchReply searchReply = new SearchReply<>(); + public SearchReply parse(ComplexData data) { + final SearchReply searchReply = new SearchReply<>(); final List resultsList = data.getDynamicList(); @@ -166,7 +213,7 @@ public SearchReply parse(ComplexData data) { return searchReply; } - private void parseResults(SearchReply searchReply, List resultsList) { + private void parseResults(SearchReply searchReply, List resultsList) { for (int i = 1; i < resultsList.size();) { K id = codec.decodeKey(StringCodec.UTF8.encodeKey("0")); @@ -175,7 +222,7 @@ private void parseResults(SearchReply searchReply, List resultsLis i++; } - final SearchReply.SearchResult searchResult = new SearchReply.SearchResult<>(id); + final SearchReply.SearchResult searchResult = new SearchReply.SearchResult<>(id); if (withScores) { searchResult.setScore(Double.parseDouble(StringCodec.UTF8.decodeKey((ByteBuffer) resultsList.get(i)))); @@ -187,10 +234,8 @@ private void parseResults(SearchReply searchReply, List resultsLis List resultEntries = resultData.getDynamicList(); for (int idx = 0; idx < resultEntries.size(); idx += 2) { - K decodedKey = codec.decodeKey((ByteBuffer) resultEntries.get(idx)); - Object value = resultEntries.get(idx + 1); - V decodedValue = value == null ? null : codec.decodeValue((ByteBuffer) value); - searchResult.addFields(decodedKey, decodedValue); + String fieldName = StringCodec.UTF8.decodeKey((ByteBuffer) resultEntries.get(idx)); + searchResult.addField(fieldName, toFieldValue(resultEntries.get(idx + 1))); } i++; @@ -202,7 +247,7 @@ private void parseResults(SearchReply searchReply, List resultsLis } - class Resp3SearchResultsParser implements ComplexDataParser> { + class Resp3SearchResultsParser implements ComplexDataParser> { private final ByteBuffer ATTRIBUTES_KEY = StringCodec.UTF8.encodeKey("attributes"); @@ -225,8 +270,8 @@ class Resp3SearchResultsParser implements ComplexDataParser> { private final ByteBuffer CURSOR_KEY = StringCodec.UTF8.encodeKey("cursor"); @Override - public SearchReply parse(ComplexData data) { - final SearchReply searchReply = new SearchReply<>(); + public SearchReply parse(ComplexData data) { + final SearchReply searchReply = new SearchReply<>(); final Map resultsMap = data.getDynamicMap(); @@ -245,7 +290,7 @@ public SearchReply parse(ComplexData data) { ComplexData resultData = (ComplexData) result; Map resultEntry = resultData.getDynamicMap(); - SearchReply.SearchResult searchResult; + SearchReply.SearchResult searchResult; if (resultEntry.containsKey(ID_KEY)) { final K id = codec.decodeKey((ByteBuffer) resultEntry.get(ID_KEY)); searchResult = new SearchReply.SearchResult<>(id); @@ -266,9 +311,8 @@ public SearchReply parse(ComplexData data) { if (resultEntry.containsKey(EXTRA_ATTRIBUTES_KEY)) { ComplexData extraAttributes = (ComplexData) resultEntry.get(EXTRA_ATTRIBUTES_KEY); extraAttributes.getDynamicMap().forEach((key, value) -> { - K decodedKey = codec.decodeKey((ByteBuffer) key); - V decodedValue = value == null ? null : codec.decodeValue((ByteBuffer) value); - searchResult.addFields(decodedKey, decodedValue); + String fieldName = StringCodec.UTF8.decodeKey((ByteBuffer) key); + searchResult.addField(fieldName, toFieldValue(value)); }); } searchReply.addResult(searchResult); @@ -286,7 +330,7 @@ public SearchReply parse(ComplexData data) { if (resultsMap.containsKey(WARNING_KEY)) { ComplexData warning = (ComplexData) resultsMap.get(WARNING_KEY); warning.getDynamicList().forEach(warningEntry -> { - searchReply.addWarning(codec.decodeValue((ByteBuffer) warningEntry)); + searchReply.addWarning(StringCodec.UTF8.decodeValue((ByteBuffer) warningEntry)); }); } diff --git a/src/main/java/io/lettuce/core/search/SpellCheckResult.java b/src/main/java/io/lettuce/core/search/SpellCheckResult.java index ac06a90d20..007a84a518 100644 --- a/src/main/java/io/lettuce/core/search/SpellCheckResult.java +++ b/src/main/java/io/lettuce/core/search/SpellCheckResult.java @@ -17,13 +17,12 @@ * their order of appearance in the query. *

    * - * @param Value type. * @author Tihomir Mateev * @since 6.8 */ -public class SpellCheckResult { +public class SpellCheckResult { - private final List> misspelledTerms = new ArrayList<>(); + private final List misspelledTerms = new ArrayList<>(); public SpellCheckResult() { } @@ -33,7 +32,7 @@ public SpellCheckResult() { * * @return the list of misspelled terms */ - public List> getMisspelledTerms() { + public List getMisspelledTerms() { return misspelledTerms; } @@ -61,7 +60,7 @@ public boolean equals(Object o) { return true; if (o == null || getClass() != o.getClass()) return false; - SpellCheckResult that = (SpellCheckResult) o; + SpellCheckResult that = (SpellCheckResult) o; return Objects.equals(misspelledTerms, that.misspelledTerms); } @@ -75,20 +74,18 @@ public String toString() { return "SpellCheckResult{" + "misspelledTerms=" + misspelledTerms + '}'; } - void addMisspelledTerm(MisspelledTerm vMisspelledTerm) { - misspelledTerms.add(vMisspelledTerm); + void addMisspelledTerm(MisspelledTerm misspelledTerm) { + misspelledTerms.add(misspelledTerm); } /** * Represents a misspelled term and its spelling suggestions. - * - * @param Value type. */ - public static class MisspelledTerm { + public static class MisspelledTerm { - private final V term; + private final String term; - private final List> suggestions; + private final List suggestions; /** * Create a new misspelled term. @@ -96,7 +93,7 @@ public static class MisspelledTerm { * @param term the misspelled term * @param suggestions the list of spelling suggestions */ - public MisspelledTerm(V term, List> suggestions) { + public MisspelledTerm(String term, List suggestions) { this.term = term; this.suggestions = suggestions; } @@ -106,7 +103,7 @@ public MisspelledTerm(V term, List> suggestions) { * * @return the misspelled term */ - public V getTerm() { + public String getTerm() { return term; } @@ -115,7 +112,7 @@ public V getTerm() { * * @return the list of suggestions */ - public List> getSuggestions() { + public List getSuggestions() { return suggestions; } @@ -143,7 +140,7 @@ public boolean equals(Object o) { return true; if (o == null || getClass() != o.getClass()) return false; - MisspelledTerm that = (MisspelledTerm) o; + MisspelledTerm that = (MisspelledTerm) o; return Objects.equals(term, that.term) && Objects.equals(suggestions, that.suggestions); } @@ -161,14 +158,12 @@ public String toString() { /** * Represents a spelling suggestion with its score. - * - * @param Value type. */ - public static class Suggestion { + public static class Suggestion { private final double score; - private final V suggestion; + private final String suggestion; /** * Create a new spelling suggestion. @@ -176,7 +171,7 @@ public static class Suggestion { * @param score the suggestion score * @param suggestion the suggested term */ - public Suggestion(double score, V suggestion) { + public Suggestion(double score, String suggestion) { this.score = score; this.suggestion = suggestion; } @@ -199,7 +194,7 @@ public double getScore() { * * @return the suggested term */ - public V getSuggestion() { + public String getSuggestion() { return suggestion; } @@ -209,7 +204,7 @@ public boolean equals(Object o) { return true; if (o == null || getClass() != o.getClass()) return false; - Suggestion that = (Suggestion) o; + Suggestion that = (Suggestion) o; return Double.compare(that.score, score) == 0 && Objects.equals(suggestion, that.suggestion); } diff --git a/src/main/java/io/lettuce/core/search/SpellCheckResultParser.java b/src/main/java/io/lettuce/core/search/SpellCheckResultParser.java index 69951bffea..35b9d14c43 100644 --- a/src/main/java/io/lettuce/core/search/SpellCheckResultParser.java +++ b/src/main/java/io/lettuce/core/search/SpellCheckResultParser.java @@ -6,7 +6,6 @@ */ package io.lettuce.core.search; -import io.lettuce.core.codec.RedisCodec; import io.lettuce.core.codec.StringCodec; import io.lettuce.core.output.ComplexData; import io.lettuce.core.output.ComplexDataParser; @@ -31,24 +30,20 @@ *
  • An array of suggestions, where each suggestion is a 2-element array of [score, suggestion]
  • * * - * @param Value type. * @author Tihomir Mateev * @since 6.8 */ -public class SpellCheckResultParser implements ComplexDataParser> { +public class SpellCheckResultParser implements ComplexDataParser { private static final InternalLogger LOG = InternalLoggerFactory.getInstance(SpellCheckResultParser.class); - private final RedisCodec codec; - - public SpellCheckResultParser(RedisCodec codec) { - this.codec = codec; + public SpellCheckResultParser() { } @Override - public SpellCheckResult parse(ComplexData data) { + public SpellCheckResult parse(ComplexData data) { if (data == null) { - return new SpellCheckResult<>(); + return new SpellCheckResult(); } if (data.isList()) { @@ -63,7 +58,7 @@ public SpellCheckResult parse(ComplexData data) { *

    * The parsing logic handles the nested array structure returned by FT.SPELLCHECK: *

    - * + * *
          * [
          *  "results" ->  "misspelled_term" -> [ "score1" => "suggestion1", ["score2", "suggestion2"],
    @@ -71,13 +66,13 @@ public SpellCheckResult parse(ComplexData data) {
          * ]
          * 
    */ - class SpellCheckResp3Parser implements ComplexDataParser> { + class SpellCheckResp3Parser implements ComplexDataParser { private final ByteBuffer resultsKeyword = StringCodec.UTF8.encodeKey("results"); @Override - public SpellCheckResult parse(ComplexData data) { - SpellCheckResult result = new SpellCheckResult<>(); + public SpellCheckResult parse(ComplexData data) { + SpellCheckResult result = new SpellCheckResult(); if (data == null) { return null; @@ -96,29 +91,29 @@ public SpellCheckResult parse(ComplexData data) { for (Object term : resultsMap.keySet()) { // Key of the inner map is the misspelled term - V misspelledTerm = codec.decodeValue((ByteBuffer) term); + String misspelledTerm = decodeString(term); // Value of the inner map is the suggestions array ComplexData termData = (ComplexData) resultsMap.get(term); List suggestionsArray = termData.getDynamicList(); - List> suggestions = parseSuggestions(suggestionsArray); - result.addMisspelledTerm(new SpellCheckResult.MisspelledTerm<>(misspelledTerm, suggestions)); + List suggestions = parseSuggestions(suggestionsArray); + result.addMisspelledTerm(new SpellCheckResult.MisspelledTerm(misspelledTerm, suggestions)); } return result; } - private List> parseSuggestions(List suggestionsArray) { - List> suggestions = new ArrayList<>(); + private List parseSuggestions(List suggestionsArray) { + List suggestions = new ArrayList<>(); for (Object suggestionObj : suggestionsArray) { Map suggestionMap = ((ComplexData) suggestionObj).getDynamicMap(); for (Object suggestion : suggestionMap.keySet()) { double score = (double) suggestionMap.get(suggestion); - V suggestionValue = codec.decodeValue((ByteBuffer) suggestion); - suggestions.add(new SpellCheckResult.Suggestion<>(score, suggestionValue)); + String suggestionValue = decodeString(suggestion); + suggestions.add(new SpellCheckResult.Suggestion(score, suggestionValue)); } } @@ -132,7 +127,7 @@ private List> parseSuggestions(List sugge *

    * The parsing logic handles the nested array structure returned by FT.SPELLCHECK: *

    - * + * *
          * [
          *   ["TERM", "misspelled_term", [["score1", "suggestion1"], ["score2", "suggestion2"]]],
    @@ -140,13 +135,13 @@ private List> parseSuggestions(List sugge
          * ]
          * 
          */
    -    class SpellCheckResp2Parser implements ComplexDataParser> {
    +    class SpellCheckResp2Parser implements ComplexDataParser {
     
             private final ByteBuffer termKeyword = StringCodec.UTF8.encodeKey("TERM");
     
             @Override
    -        public SpellCheckResult parse(ComplexData data) {
    -            SpellCheckResult result = new SpellCheckResult<>();
    +        public SpellCheckResult parse(ComplexData data) {
    +            SpellCheckResult result = new SpellCheckResult();
     
                 List elements = data.getDynamicList();
                 if (elements == null || elements.isEmpty()) {
    @@ -171,21 +166,21 @@ public SpellCheckResult parse(ComplexData data) {
                     }
     
                     // Second element is the misspelled term
    -                V misspelledTerm = decodeValue(termContents.get(1));
    +                String misspelledTerm = decodeString(termContents.get(1));
     
                     // Third element is the suggestions array
                     ComplexData suggestionsObj = (ComplexData) termContents.get(2);
                     List suggestionsArray = suggestionsObj.getDynamicList();
    -                List> suggestions = parseSuggestions(suggestionsArray);
    +                List suggestions = parseSuggestions(suggestionsArray);
     
    -                result.addMisspelledTerm(new SpellCheckResult.MisspelledTerm<>(misspelledTerm, suggestions));
    +                result.addMisspelledTerm(new SpellCheckResult.MisspelledTerm(misspelledTerm, suggestions));
                 }
     
                 return result;
             }
     
    -        private List> parseSuggestions(List suggestionsArray) {
    -            List> suggestions = new ArrayList<>();
    +        private List parseSuggestions(List suggestionsArray) {
    +            List suggestions = new ArrayList<>();
     
                 for (Object suggestionObj : suggestionsArray) {
                     List suggestionData = ((ComplexData) suggestionObj).getDynamicList();
    @@ -199,9 +194,9 @@ private List> parseSuggestions(List sugge
                     double score = parseScore(suggestionData.get(0));
     
                     // Second element is the suggestion
    -                V suggestion = decodeValue(suggestionData.get(1));
    +                String suggestion = decodeString(suggestionData.get(1));
     
    -                suggestions.add(new SpellCheckResult.Suggestion<>(score, suggestion));
    +                suggestions.add(new SpellCheckResult.Suggestion(score, suggestion));
                 }
     
                 return suggestions;
    @@ -212,16 +207,13 @@ private List> parseSuggestions(List sugge
         /**
          * Helper method to decode values that can be either ByteBuffer or String objects.
          */
    -    @SuppressWarnings("unchecked")
    -    private V decodeValue(Object value) {
    +    private String decodeString(Object value) {
             if (value instanceof ByteBuffer) {
    -            return codec.decodeValue((ByteBuffer) value);
    +            return StringCodec.UTF8.decodeValue((ByteBuffer) value);
             } else if (value instanceof String) {
    -            // For test scenarios where strings are passed directly
    -            return (V) value;
    +            return (String) value;
             } else {
    -            // Fallback - try to cast directly
    -            return (V) value;
    +            return value == null ? null : value.toString();
             }
         }
     
    diff --git a/src/main/java/io/lettuce/core/search/Suggestion.java b/src/main/java/io/lettuce/core/search/Suggestion.java
    index 1716a3799d..a07269c1a1 100644
    --- a/src/main/java/io/lettuce/core/search/Suggestion.java
    +++ b/src/main/java/io/lettuce/core/search/Suggestion.java
    @@ -15,24 +15,23 @@
      * with WITHSCORES and/or WITHPAYLOADS options.
      * 

    * - * @param Value type. * @author Tihomir Mateev * @since 6.8 */ -public class Suggestion { +public class Suggestion { - private final V value; + private final String value; private Double score; - private V payload; + private String payload; /** * Create a new suggestion with only the value. * * @param value the suggestion string */ - public Suggestion(V value) { + public Suggestion(String value) { this.value = value; } @@ -40,7 +39,7 @@ void setScore(Double score) { this.score = score; } - void setPayload(V payload) { + void setPayload(String payload) { this.payload = payload; } @@ -49,7 +48,7 @@ void setPayload(V payload) { * * @return the suggestion value */ - public V getValue() { + public String getValue() { return value; } @@ -67,7 +66,7 @@ public Double getScore() { * * @return the suggestion payload, or {@code null} if not available */ - public V getPayload() { + public String getPayload() { return payload; } @@ -95,7 +94,7 @@ public boolean equals(Object o) { return true; if (o == null || getClass() != o.getClass()) return false; - Suggestion that = (Suggestion) o; + Suggestion that = (Suggestion) o; return Objects.equals(value, that.value) && Objects.equals(score, that.score) && Objects.equals(payload, that.payload); } diff --git a/src/main/java/io/lettuce/core/search/SuggestionParser.java b/src/main/java/io/lettuce/core/search/SuggestionParser.java index 3c24fb758e..161da66892 100644 --- a/src/main/java/io/lettuce/core/search/SuggestionParser.java +++ b/src/main/java/io/lettuce/core/search/SuggestionParser.java @@ -27,11 +27,10 @@ *
  • With both WITHSCORES and WITHPAYLOADS: Suggestion strings, scores, and payloads in sequence
  • * * - * @param Value type. * @author Tihomir Mateev * @since 6.8 */ -public class SuggestionParser implements ComplexDataParser>> { +public class SuggestionParser implements ComplexDataParser> { private static final InternalLogger LOG = InternalLoggerFactory.getInstance(SuggestionParser.class); @@ -66,9 +65,8 @@ public SuggestionParser(boolean withScores, boolean withPayloads) { * @return a list of {@link Suggestion} objects */ @Override - @SuppressWarnings("unchecked") - public List> parse(ComplexData data) { - List> suggestions = new ArrayList<>(); + public List parse(ComplexData data) { + List suggestions = new ArrayList<>(); if (data == null) { return suggestions; @@ -89,8 +87,8 @@ public List> parse(ComplexData data) { for (int i = 0; i < elements.size();) { - V value = (V) elements.get(i++); - Suggestion suggestion = new Suggestion<>(value); + String value = (String) elements.get(i++); + Suggestion suggestion = new Suggestion(value); if (withScores && i + 1 <= elements.size()) { Double score = parseScore(elements.get(i++)); @@ -98,7 +96,7 @@ public List> parse(ComplexData data) { } if (withPayloads && i + 1 <= elements.size()) { - V payload = (V) elements.get(i++); + String payload = (String) elements.get(i++); suggestion.setPayload(payload); } @@ -123,7 +121,12 @@ private Double parseScore(Object scoreObj) { return (Double) scoreObj; } - return 0.0; + try { + return Double.parseDouble(scoreObj.toString()); + } catch (NumberFormatException e) { + LOG.warn("Failed while parsing FT.SUGGET score: {}", scoreObj); + return null; + } } } diff --git a/src/main/java/io/lettuce/core/search/SynonymMapParser.java b/src/main/java/io/lettuce/core/search/SynonymMapParser.java index d68b11ef23..4c391a34c1 100644 --- a/src/main/java/io/lettuce/core/search/SynonymMapParser.java +++ b/src/main/java/io/lettuce/core/search/SynonymMapParser.java @@ -12,13 +12,13 @@ import java.util.Map; import java.util.stream.Collectors; -import io.lettuce.core.codec.RedisCodec; +import io.lettuce.core.codec.StringCodec; import io.lettuce.core.output.ComplexData; import io.lettuce.core.output.ComplexDataParser; /** * Parser for FT.SYNDUMP command results that handles both RESP2 and RESP3 protocol responses. - * + * *

    * This parser automatically detects the Redis protocol version and switches between RESP2 and RESP3 parsing strategies. *

    @@ -28,17 +28,12 @@ * structure properly represents the synonym relationships returned by Redis Search. *

    * - * @param Key type. - * @param Value type. * @author Tihomir Mateev * @since 6.8 */ -public class SynonymMapParser implements ComplexDataParser>> { - - private final RedisCodec codec; +public class SynonymMapParser implements ComplexDataParser>> { - public SynonymMapParser(RedisCodec codec) { - this.codec = codec; + public SynonymMapParser() { } /** @@ -48,7 +43,7 @@ public SynonymMapParser(RedisCodec codec) { * @return a map where keys are terms and values are lists of synonyms for each term */ @Override - public Map> parse(ComplexData data) { + public Map> parse(ComplexData data) { if (data == null) { return new LinkedHashMap<>(); @@ -64,9 +59,9 @@ public Map> parse(ComplexData data) { /** * Parse FT.SYNDUMP response in RESP2 format (array-based with alternating key-value pairs). */ - private Map> parseResp2(ComplexData data) { + private Map> parseResp2(ComplexData data) { List synonymArray = data.getDynamicList(); - Map> synonymMap = new LinkedHashMap<>(); + Map> synonymMap = new LinkedHashMap<>(); // RESP2: Parse alternating key-value pairs // Structure: [term1, [synonym1, synonym2], term2, [synonym3, synonym4], ...] @@ -76,13 +71,13 @@ private Map> parseResp2(ComplexData data) { } // Decode the term (key) - V term = codec.decodeValue((ByteBuffer) synonymArray.get(i++)); + String term = StringCodec.UTF8.decodeValue((ByteBuffer) synonymArray.get(i++)); // Decode the synonyms (value - should be a list) ComplexData synonymsData = (ComplexData) synonymArray.get(i++); List synonims = synonymsData.getDynamicList(); - List decodedSynonyms = synonims.stream().map(synonym -> codec.decodeValue((ByteBuffer) synonym)) + List decodedSynonyms = synonims.stream().map(synonym -> StringCodec.UTF8.decodeValue((ByteBuffer) synonym)) .collect(Collectors.toList()); synonymMap.put(term, decodedSynonyms); } @@ -93,21 +88,21 @@ private Map> parseResp2(ComplexData data) { /** * Parse FT.SYNDUMP response in RESP3 format (map-based). */ - private Map> parseResp3(ComplexData data) { + private Map> parseResp3(ComplexData data) { Map synonymMapRaw = data.getDynamicMap(); - Map> synonymMap = new LinkedHashMap<>(); + Map> synonymMap = new LinkedHashMap<>(); // RESP3: Parse native map structure // Structure: {term1: [synonym1, synonym2], term2: [synonym3, synonym4], ...} for (Map.Entry entry : synonymMapRaw.entrySet()) { // Decode the term (key) - V term = codec.decodeValue((ByteBuffer) entry.getKey()); + String term = StringCodec.UTF8.decodeValue((ByteBuffer) entry.getKey()); // Decode the synonyms (value - should be a list) Object synonymsData = entry.getValue(); List synonymsList = ((ComplexData) synonymsData).getDynamicList(); - List synonyms = synonymsList.stream().map(synonym -> codec.decodeValue((ByteBuffer) synonym)) + List synonyms = synonymsList.stream().map(synonym -> StringCodec.UTF8.decodeValue((ByteBuffer) synonym)) .collect(Collectors.toList()); synonymMap.put(term, synonyms); diff --git a/src/main/java/io/lettuce/core/search/aggregateutils/Apply.java b/src/main/java/io/lettuce/core/search/aggregateutils/Apply.java index ef44511bcf..bbfff89fba 100644 --- a/src/main/java/io/lettuce/core/search/aggregateutils/Apply.java +++ b/src/main/java/io/lettuce/core/search/aggregateutils/Apply.java @@ -20,29 +20,27 @@ *

    Example Usage:

    * *
    - * 
    + *
      * {
      *     @code
      *     // Calculate total value from price and quantity
    - *     Apply totalValue = Apply.of("@price * @quantity", "total_value");
    + *     Apply totalValue = Apply.of("@price * @quantity", "total_value");
      *
      *     // Mathematical operations
    - *     Apply discount = Apply.of("@price * 0.9", "discounted_price");
    + *     Apply discount = Apply.of("@price * 0.9", "discounted_price");
      * }
      * 
    * - * @param Key type. - * @param Value type. * @author Aleksandar Todorov * @since 7.5 * @see PostProcessingOperation */ @Experimental -public class Apply implements PostProcessingOperation { +public class Apply implements PostProcessingOperation { - private final V expression; + private final String expression; - private final K name; + private final String name; /** * Creates a new APPLY operation. @@ -50,7 +48,7 @@ public class Apply implements PostProcessingOperation { * @param expression the expression to apply * @param name the result field name */ - public Apply(V expression, K name) { + public Apply(String expression, String name) { this.expression = expression; this.name = name; } @@ -60,20 +58,18 @@ public Apply(V expression, K name) { * * @param expression the expression to apply * @param name the name of the result field - * @param Key type - * @param Value type * @return new Apply instance */ - public static Apply of(V expression, K name) { - return new Apply<>(expression, name); + public static Apply of(String expression, String name) { + return new Apply(expression, name); } @Override - public void build(CommandArgs args) { + public void build(CommandArgs args) { args.add(CommandKeyword.APPLY); - args.addValue(expression); + args.add(expression); args.add(CommandKeyword.AS); - args.add(name.toString()); + args.add(name); } } diff --git a/src/main/java/io/lettuce/core/search/aggregateutils/Filter.java b/src/main/java/io/lettuce/core/search/aggregateutils/Filter.java index 49d505ff84..7da0c500e3 100644 --- a/src/main/java/io/lettuce/core/search/aggregateutils/Filter.java +++ b/src/main/java/io/lettuce/core/search/aggregateutils/Filter.java @@ -20,34 +20,32 @@ *

    Example Usage:

    * *
    - * 
    + *
      * {
      *     @code
      *     // Filter by numeric comparison
    - *     Filter priceFilter = Filter.of("@price > 100");
    + *     Filter priceFilter = Filter.of("@price > 100");
      *
      *     // Filter by computed field
    - *     Filter totalFilter = Filter.of("@total_value > 1000");
    + *     Filter totalFilter = Filter.of("@total_value > 1000");
      * }
      * 
    * - * @param Key type. - * @param Value type. * @author Aleksandar Todorov * @since 7.5 * @see PostProcessingOperation */ @Experimental -public class Filter implements PostProcessingOperation { +public class Filter implements PostProcessingOperation { - private final V expression; + private final String expression; /** * Creates a new FILTER operation. * * @param expression the filter expression (e.g., "@price > 100", "@category == 'electronics'") */ - public Filter(V expression) { + public Filter(String expression) { this.expression = expression; } @@ -55,18 +53,16 @@ public Filter(V expression) { * Static factory method to create a Filter instance. * * @param expression the filter expression - * @param Key type - * @param Value type * @return new Filter instance */ - public static Filter of(V expression) { - return new Filter<>(expression); + public static Filter of(String expression) { + return new Filter(expression); } @Override - public void build(CommandArgs args) { + public void build(CommandArgs args) { args.add(CommandKeyword.FILTER); - args.addValue(expression); + args.add(expression); } } diff --git a/src/main/java/io/lettuce/core/search/aggregateutils/GroupBy.java b/src/main/java/io/lettuce/core/search/aggregateutils/GroupBy.java index 325f92530c..c37d9ce37d 100644 --- a/src/main/java/io/lettuce/core/search/aggregateutils/GroupBy.java +++ b/src/main/java/io/lettuce/core/search/aggregateutils/GroupBy.java @@ -24,20 +24,19 @@ *

    Example Usage:

    * *
    - * 
    + *
      * {
      *     @code
      *     // Group by category and count items
    - *     GroupBy groupBy = GroupBy.of("category").reduce(Reducers.count().as("item_count"));
    + *     GroupBy groupBy = GroupBy.of("category").reduce(Reducers.count().as("item_count"));
      *
      *     // Group by multiple fields with multiple reducers
    - *     GroupBy complexGroup = GroupBy.of("category", "brand").reduce(Reducers.count().as("count"))
    + *     GroupBy complexGroup = GroupBy.of("category", "brand").reduce(Reducers.count().as("count"))
      *             .reduce(Reducers.avg("@price").as("avg_price"));
      * }
      * 
    * - * @param Key type. - * @param Value type. + * @param Key type. * @author Aleksandar Todorov * @since 7.5 * @see Reducer @@ -46,18 +45,18 @@ * @see PostProcessingOperation */ @Experimental -public class GroupBy implements PostProcessingOperation { +public class GroupBy implements PostProcessingOperation { - private final List properties; + private final List properties; - private final List> reducers; + private final List reducers; /** * Creates a new GROUPBY operation. * * @param properties the properties to group by */ - public GroupBy(List properties) { + public GroupBy(List properties) { this.properties = new ArrayList<>(properties); this.reducers = new ArrayList<>(); } @@ -66,13 +65,12 @@ public GroupBy(List properties) { * Static factory method to create a GroupBy instance. * * @param properties the properties to group by - * @param Key type - * @param Value type + * @param Key type * @return new GroupBy instance */ @SafeVarargs - public static GroupBy of(K... properties) { - return new GroupBy<>(Arrays.asList(properties)); + public static GroupBy of(String... properties) { + return new GroupBy(Arrays.asList(properties)); } /** @@ -81,16 +79,16 @@ public static GroupBy of(K... properties) { * @param reducer the reducer to add * @return this GroupBy instance */ - public GroupBy reduce(Reducer reducer) { + public GroupBy reduce(Reducer reducer) { this.reducers.add(reducer); return this; } @Override - public void build(CommandArgs args) { + public void build(CommandArgs args) { args.add(CommandKeyword.GROUPBY); args.add(properties.size()); - for (K property : properties) { + for (String property : properties) { // Add @ prefix if not already present String propertyStr = property.toString(); if (!propertyStr.startsWith("@")) { @@ -100,7 +98,7 @@ public void build(CommandArgs args) { } } - for (Reducer reducer : reducers) { + for (Reducer reducer : reducers) { reducer.build(args); } } diff --git a/src/main/java/io/lettuce/core/search/aggregateutils/Limit.java b/src/main/java/io/lettuce/core/search/aggregateutils/Limit.java index 1945f35df1..c11f033272 100644 --- a/src/main/java/io/lettuce/core/search/aggregateutils/Limit.java +++ b/src/main/java/io/lettuce/core/search/aggregateutils/Limit.java @@ -16,25 +16,23 @@ *

    Example Usage:

    * *
    - * 
    + *
      * {
      *     @code
      *     // Get first 10 results
    - *     Limit limit = Limit.of(0, 10);
    + *     Limit limit = Limit.of(0, 10);
      *
      *     // Get results 50-100
    - *     Limit paginated = Limit.of(50, 50);
    + *     Limit paginated = Limit.of(50, 50);
      * }
      * 
    * - * @param Key type. - * @param Value type. * @author Aleksandar Todorov * @since 7.5 * @see PostProcessingOperation */ @Experimental -public class Limit implements PostProcessingOperation { +public class Limit implements PostProcessingOperation { private final long offset; @@ -56,12 +54,10 @@ public Limit(long offset, long num) { * * @param offset the zero-based starting index * @param num the maximum number of results to return - * @param Key type - * @param Value type * @return new Limit instance */ - public static Limit of(long offset, long num) { - return new Limit<>(offset, num); + public static Limit of(long offset, long num) { + return new Limit(offset, num); } /** @@ -83,7 +79,7 @@ public long getNum() { } @Override - public void build(CommandArgs args) { + public void build(CommandArgs args) { args.add(CommandKeyword.LIMIT); args.add(offset); args.add(num); diff --git a/src/main/java/io/lettuce/core/search/aggregateutils/PostProcessingOperation.java b/src/main/java/io/lettuce/core/search/aggregateutils/PostProcessingOperation.java index 05dc155594..e62b1402cd 100644 --- a/src/main/java/io/lettuce/core/search/aggregateutils/PostProcessingOperation.java +++ b/src/main/java/io/lettuce/core/search/aggregateutils/PostProcessingOperation.java @@ -13,8 +13,6 @@ * Interface for post-processing operations applied in user-specified order. This includes GROUPBY, SORTBY, APPLY, FILTER, and * LIMIT operations used in FT.HYBRID and FT.AGGREGATE commands. * - * @param Key type. - * @param Value type. * @author Aleksandar Todorov * @since 7.5 * @see GroupBy @@ -24,13 +22,13 @@ * @see Limit */ @Experimental -public interface PostProcessingOperation { +public interface PostProcessingOperation { /** * Build the operation arguments into the command args. * * @param args the command args to build into */ - void build(CommandArgs args); + void build(CommandArgs args); } diff --git a/src/main/java/io/lettuce/core/search/aggregateutils/Reducer.java b/src/main/java/io/lettuce/core/search/aggregateutils/Reducer.java index 78a84efbd4..0c535bea67 100644 --- a/src/main/java/io/lettuce/core/search/aggregateutils/Reducer.java +++ b/src/main/java/io/lettuce/core/search/aggregateutils/Reducer.java @@ -44,7 +44,6 @@ * } * * - * @param Key type. * @author Aleksandar Todorov * @since 7.5 * @see Reducers @@ -54,11 +53,11 @@ * @see FT.HYBRID */ @Experimental -public abstract class Reducer { +public abstract class Reducer { private final String function; - private K alias; + private String alias; /** * Creates a new reducer with the specified function. @@ -105,7 +104,7 @@ public final String getFunction() { * @return this reducer */ @SuppressWarnings("unchecked") - public > T as(K alias) { + public T as(String alias) { LettuceAssert.notNull(alias, "Alias must not be null"); this.alias = alias; return (T) this; @@ -118,9 +117,8 @@ public > T as(K alias) { *

    * * @param args the command args to build into - * @param value type */ - public final void build(CommandArgs args) { + public final void build(CommandArgs args) { args.add(CommandKeyword.REDUCE); args.add(function); @@ -132,7 +130,7 @@ public final void build(CommandArgs args) { if (alias != null) { args.add(CommandKeyword.AS); - args.addKey(alias); + args.add(alias); } } diff --git a/src/main/java/io/lettuce/core/search/aggregateutils/Reducers.java b/src/main/java/io/lettuce/core/search/aggregateutils/Reducers.java index b8c2f36a6c..aa8c6a12ac 100644 --- a/src/main/java/io/lettuce/core/search/aggregateutils/Reducers.java +++ b/src/main/java/io/lettuce/core/search/aggregateutils/Reducers.java @@ -52,57 +52,57 @@ public final class Reducers { private Reducers() { } - public static Count count() { - return new Count<>(); + public static Count count() { + return new Count(); } - public static CountDistinct countDistinct(K field) { - return new CountDistinct<>(field); + public static CountDistinct countDistinct(String field) { + return new CountDistinct(field); } - public static CountDistinctish countDistinctish(K field) { - return new CountDistinctish<>(field); + public static CountDistinctish countDistinctish(String field) { + return new CountDistinctish(field); } - public static Sum sum(K field) { - return new Sum<>(field); + public static Sum sum(String field) { + return new Sum(field); } - public static Avg avg(K field) { - return new Avg<>(field); + public static Avg avg(String field) { + return new Avg(field); } - public static Min min(K field) { - return new Min<>(field); + public static Min min(String field) { + return new Min(field); } - public static Max max(K field) { - return new Max<>(field); + public static Max max(String field) { + return new Max(field); } - public static Stddev stddev(K field) { - return new Stddev<>(field); + public static Stddev stddev(String field) { + return new Stddev(field); } - public static Quantile quantile(K field, double quantile) { - return new Quantile<>(field, quantile); + public static Quantile quantile(String field, double quantile) { + return new Quantile(field, quantile); } - public static ToList toList(K field) { - return new ToList<>(field); + public static ToList toList(String field) { + return new ToList(field); } - public static FirstValue firstValue(K field) { - return new FirstValue<>(field); + public static FirstValue firstValue(String field) { + return new FirstValue(field); } - public static RandomSample randomSample(K field, int sampleSize) { - return new RandomSample<>(field, sampleSize); + public static RandomSample randomSample(String field, int sampleSize) { + return new RandomSample(field, sampleSize); } // ==================== Concrete Reducer Implementations ==================== - public static class Count extends Reducer { + public static class Count extends Reducer { Count() { super(ReduceFunction.COUNT); @@ -115,11 +115,11 @@ protected List getOwnArgs() { } - public static class CountDistinct extends Reducer { + public static class CountDistinct extends Reducer { - private final K field; + private final String field; - CountDistinct(K field) { + CountDistinct(String field) { super(ReduceFunction.COUNT_DISTINCT); LettuceAssert.notNull(field, "Field must not be null"); this.field = field; @@ -132,11 +132,11 @@ protected List getOwnArgs() { } - public static class CountDistinctish extends Reducer { + public static class CountDistinctish extends Reducer { - private final K field; + private final String field; - CountDistinctish(K field) { + CountDistinctish(String field) { super(ReduceFunction.COUNT_DISTINCTISH); LettuceAssert.notNull(field, "Field must not be null"); this.field = field; @@ -149,11 +149,11 @@ protected List getOwnArgs() { } - public static class Sum extends Reducer { + public static class Sum extends Reducer { - private final K field; + private final String field; - Sum(K field) { + Sum(String field) { super(ReduceFunction.SUM); LettuceAssert.notNull(field, "Field must not be null"); this.field = field; @@ -166,11 +166,11 @@ protected List getOwnArgs() { } - public static class Avg extends Reducer { + public static class Avg extends Reducer { - private final K field; + private final String field; - Avg(K field) { + Avg(String field) { super(ReduceFunction.AVG); LettuceAssert.notNull(field, "Field must not be null"); this.field = field; @@ -183,11 +183,11 @@ protected List getOwnArgs() { } - public static class Min extends Reducer { + public static class Min extends Reducer { - private final K field; + private final String field; - Min(K field) { + Min(String field) { super(ReduceFunction.MIN); LettuceAssert.notNull(field, "Field must not be null"); this.field = field; @@ -200,11 +200,11 @@ protected List getOwnArgs() { } - public static class Max extends Reducer { + public static class Max extends Reducer { - private final K field; + private final String field; - Max(K field) { + Max(String field) { super(ReduceFunction.MAX); LettuceAssert.notNull(field, "Field must not be null"); this.field = field; @@ -217,11 +217,11 @@ protected List getOwnArgs() { } - public static class Stddev extends Reducer { + public static class Stddev extends Reducer { - private final K field; + private final String field; - Stddev(K field) { + Stddev(String field) { super(ReduceFunction.STDDEV); LettuceAssert.notNull(field, "Field must not be null"); this.field = field; @@ -234,13 +234,13 @@ protected List getOwnArgs() { } - public static class Quantile extends Reducer { + public static class Quantile extends Reducer { - private final K field; + private final String field; private final double quantile; - Quantile(K field, double quantile) { + Quantile(String field, double quantile) { super(ReduceFunction.QUANTILE); LettuceAssert.notNull(field, "Field must not be null"); LettuceAssert.isTrue(quantile >= 0 && quantile <= 1, "Quantile must be between 0 and 1"); @@ -258,11 +258,11 @@ protected List getOwnArgs() { } - public static class ToList extends Reducer { + public static class ToList extends Reducer { - private final K field; + private final String field; - ToList(K field) { + ToList(String field) { super(ReduceFunction.TOLIST); LettuceAssert.notNull(field, "Field must not be null"); this.field = field; @@ -275,26 +275,26 @@ protected List getOwnArgs() { } - public static class FirstValue extends Reducer { + public static class FirstValue extends Reducer { - private final K field; + private final String field; - private K byField; + private String byField; private SortDirection byDirection; - FirstValue(K field) { + FirstValue(String field) { super(ReduceFunction.FIRST_VALUE); LettuceAssert.notNull(field, "Field must not be null"); this.field = field; } - public FirstValue by(K byField) { + public FirstValue by(String byField) { this.byField = byField; return this; } - public FirstValue by(K byField, SortDirection direction) { + public FirstValue by(String byField, SortDirection direction) { this.byField = byField; this.byDirection = direction; return this; @@ -316,13 +316,13 @@ protected List getOwnArgs() { } - public static class RandomSample extends Reducer { + public static class RandomSample extends Reducer { - private final K field; + private final String field; private final int sampleSize; - RandomSample(K field, int sampleSize) { + RandomSample(String field, int sampleSize) { super(ReduceFunction.RANDOM_SAMPLE); LettuceAssert.notNull(field, "Field must not be null"); LettuceAssert.isTrue(sampleSize > 0, "Sample size must be positive"); diff --git a/src/main/java/io/lettuce/core/search/aggregateutils/Scorer.java b/src/main/java/io/lettuce/core/search/aggregateutils/Scorer.java index 3003a6914c..c72b95761b 100644 --- a/src/main/java/io/lettuce/core/search/aggregateutils/Scorer.java +++ b/src/main/java/io/lettuce/core/search/aggregateutils/Scorer.java @@ -59,10 +59,8 @@ public final String getName() { *

    * * @param args the {@link CommandArgs} to append to - * @param key type - * @param value type */ - public final void build(CommandArgs args) { + public final void build(CommandArgs args) { args.add(CommandKeyword.SCORER); args.add(name); getOwnArgs().forEach(args::add); diff --git a/src/main/java/io/lettuce/core/search/aggregateutils/SortBy.java b/src/main/java/io/lettuce/core/search/aggregateutils/SortBy.java index 46baffb5a9..d2117c928d 100644 --- a/src/main/java/io/lettuce/core/search/aggregateutils/SortBy.java +++ b/src/main/java/io/lettuce/core/search/aggregateutils/SortBy.java @@ -29,15 +29,15 @@ * { * @code * // Simple sort by single field - * SortBy sortBy = SortBy.of("price", SortDirection.DESC); + * SortBy sortBy = SortBy.of("price", SortDirection.DESC); * * // Multiple sort criteria - * SortBy multiSort = SortBy.of(SortProperty.of("category", SortDirection.ASC), + * SortBy multiSort = SortBy.of(SortProperty.of("category", SortDirection.ASC), * SortProperty.of("price", SortDirection.DESC)); * } * * - * @param Key type. + * @param Key type. * @author Aleksandar Todorov * @since 7.5 * @see SortProperty @@ -45,16 +45,16 @@ * @see PostProcessingOperation */ @Experimental -public class SortBy implements PostProcessingOperation { +public class SortBy implements PostProcessingOperation { - private final List> properties; + private final List properties; /** * Creates a new SORTBY operation. * * @param properties the properties to sort by */ - public SortBy(List> properties) { + public SortBy(List properties) { this.properties = new ArrayList<>(properties); } @@ -63,31 +63,31 @@ public SortBy(List> properties) { * * @param property the property to sort by * @param direction the sort direction - * @param Key type + * @param Key type * @return new SortBy instance */ - public static SortBy of(K property, SortDirection direction) { - return new SortBy<>(Collections.singletonList(new SortProperty<>(property, direction))); + public static SortBy of(String property, SortDirection direction) { + return new SortBy(Collections.singletonList(new SortProperty(property, direction))); } /** * Static factory method to create a SortBy instance with multiple properties. * * @param properties the properties to sort by - * @param Key type + * @param Key type * @return new SortBy instance */ @SafeVarargs - public static SortBy of(SortProperty... properties) { - return new SortBy<>(Arrays.asList(properties)); + public static SortBy of(SortProperty... properties) { + return new SortBy(Arrays.asList(properties)); } @Override - public void build(CommandArgs args) { + public void build(CommandArgs args) { args.add(CommandKeyword.SORTBY); // Count includes property + direction pairs args.add(properties.size() * 2L); - for (SortProperty property : properties) { + for (SortProperty property : properties) { // Add @ prefix if not already present String propertyStr = property.getProperty().toString(); if (!propertyStr.startsWith("@")) { diff --git a/src/main/java/io/lettuce/core/search/aggregateutils/SortProperty.java b/src/main/java/io/lettuce/core/search/aggregateutils/SortProperty.java index d46d777f9a..40555d5658 100644 --- a/src/main/java/io/lettuce/core/search/aggregateutils/SortProperty.java +++ b/src/main/java/io/lettuce/core/search/aggregateutils/SortProperty.java @@ -11,16 +11,16 @@ /** * Represents a sort property with direction for SORTBY operations in FT.HYBRID and FT.AGGREGATE commands. * - * @param Key type. + * @param Key type. * @author Aleksandar Todorov * @since 7.5 * @see SortBy * @see SortDirection */ @Experimental -public class SortProperty { +public class SortProperty { - private final K property; + private final String property; private final SortDirection direction; @@ -30,7 +30,7 @@ public class SortProperty { * @param property the property to sort by * @param direction the sort direction */ - public SortProperty(K property, SortDirection direction) { + public SortProperty(String property, SortDirection direction) { this.property = property; this.direction = direction; } @@ -40,11 +40,11 @@ public SortProperty(K property, SortDirection direction) { * * @param property the property to sort by * @param direction the sort direction - * @param Key type + * @param Key type * @return new SortProperty instance */ - public static SortProperty of(K property, SortDirection direction) { - return new SortProperty<>(property, direction); + public static SortProperty of(String property, SortDirection direction) { + return new SortProperty(property, direction); } /** @@ -52,7 +52,7 @@ public static SortProperty of(K property, SortDirection direction) { * * @return the property */ - public K getProperty() { + public String getProperty() { return property; } diff --git a/src/main/java/io/lettuce/core/search/arguments/AggregateArgs.java b/src/main/java/io/lettuce/core/search/arguments/AggregateArgs.java index d9dbbd0352..8d141d5ea3 100644 --- a/src/main/java/io/lettuce/core/search/arguments/AggregateArgs.java +++ b/src/main/java/io/lettuce/core/search/arguments/AggregateArgs.java @@ -7,8 +7,10 @@ package io.lettuce.core.search.arguments; +import io.lettuce.core.annotations.Experimental; import io.lettuce.core.protocol.CommandArgs; import io.lettuce.core.protocol.CommandKeyword; +import io.lettuce.core.search.AggregationReply; import java.time.Duration; import java.util.*; @@ -29,9 +31,9 @@ * { * @code * // Simple aggregation with grouping and counting - * AggregateArgs args = AggregateArgs. builder().groupBy("category") - * .reduce(Reducer.count().as("count")).sortBy("count", SortDirection.DESC).build(); - * SearchReply result = redis.ftAggregate("myindex", "*", args); + * AggregateArgs args = AggregateArgs.builder().groupBy("category").reduce(Reducer.count().as("count")) + * .sortBy("count", SortDirection.DESC).build(); + * SearchReply result = redis.ftAggregate("myindex", "*", args); * } * * @@ -43,7 +45,7 @@ * { * @code * // Complex aggregation pipeline - * AggregateArgs args = AggregateArgs. builder().load("price", "quantity", "category") + * AggregateArgs args = AggregateArgs.builder().load("price", "quantity", "category") * .apply("@price * @quantity", "total_value").filter("@total_value > 100").groupBy("category") * .reduce(Reducer.sum("@total_value").as("category_total")).reduce(Reducer.avg("@price").as("avg_price")) * .sortBy("category_total", SortDirection.DESC).limit(0, 10).dialect(QueryDialects.DIALECT2).build(); @@ -69,8 +71,6 @@ *
  • Consider using WITHCURSOR for large result sets to avoid memory issues
  • * * - * @param Key type. - * @param Value type. * @since 6.8 * @author Tihomir Mateev * @see FT.AGGREGATE @@ -78,11 +78,11 @@ * Aggregations Guide */ @SuppressWarnings("OptionalUsedAsFieldOrParameterType") -public class AggregateArgs { +public class AggregateArgs { private Optional verbatim = Optional.empty(); - private final List> loadFields = new ArrayList<>(); + private final List loadFields = new ArrayList<>(); private Optional timeout = Optional.empty(); @@ -90,13 +90,13 @@ public class AggregateArgs { * Ordered list of pipeline operations (GROUPBY, SORTBY, APPLY, FILTER). These operations must be applied in the order * specified by the user. */ - private final List> pipelineOperations = new ArrayList<>(); + private final List pipelineOperations = new ArrayList<>(); private Optional withCursor = Optional.empty(); - private final Map params = new HashMap<>(); + private final Map params = new HashMap<>(); - private Optional scorer = Optional.empty(); + private Optional scorer = Optional.empty(); private Optional addScores = Optional.empty(); @@ -105,23 +105,19 @@ public class AggregateArgs { /** * Creates a new {@link AggregateArgs} instance. * - * @param Key type. - * @param Value type. * @return new instance of {@link AggregateArgs}. */ - public static Builder builder() { - return new Builder<>(); + public static Builder builder() { + return new Builder(); } /** * Builder for {@link AggregateArgs}. * - * @param Key type. - * @param Value type. */ - public static class Builder { + public static class Builder { - private final AggregateArgs args = new AggregateArgs<>(); + private final AggregateArgs args = new AggregateArgs(); /** * Set VERBATIM flag - do not try to use stemming for query expansion. @@ -133,7 +129,7 @@ public static class Builder { * * @return the builder. */ - public Builder verbatim() { + public Builder verbatim() { args.verbatim = Optional.of(true); return this; } @@ -155,8 +151,8 @@ public Builder verbatim() { * @param field the field identifier (field name for hashes, JSONPath for JSON) * @return the builder. */ - public Builder load(K field) { - args.loadFields.add(new LoadField<>(field, null)); + public Builder load(String field) { + args.loadFields.add(new LoadField(field, null)); return this; } @@ -172,8 +168,8 @@ public Builder load(K field) { * @param alias the alias name to use in the result * @return the builder. */ - public Builder load(K field, K alias) { - args.loadFields.add(new LoadField<>(field, alias)); + public Builder load(String field, String alias) { + args.loadFields.add(new LoadField(field, alias)); return this; } @@ -187,8 +183,8 @@ public Builder load(K field, K alias) { * * @return the builder. */ - public Builder loadAll() { - args.loadFields.add(new LoadField<>(null, null)); // Special case for * + public Builder loadAll() { + args.loadFields.add(new LoadField(null, null)); // Special case for * return this; } @@ -198,7 +194,7 @@ public Builder loadAll() { * @param timeout the timeout duration * @return the builder. */ - public Builder timeout(Duration timeout) { + public Builder timeout(Duration timeout) { args.timeout = Optional.of(timeout); return this; } @@ -209,7 +205,7 @@ public Builder timeout(Duration timeout) { * @param groupBy the group by specification * @return the builder. */ - public Builder groupBy(GroupBy groupBy) { + public Builder groupBy(GroupBy groupBy) { args.pipelineOperations.add(groupBy); return this; } @@ -220,7 +216,7 @@ public Builder groupBy(GroupBy groupBy) { * @param sortBy the sort by specification * @return the builder. */ - public Builder sortBy(SortBy sortBy) { + public Builder sortBy(SortBy sortBy) { args.pipelineOperations.add(sortBy); return this; } @@ -231,7 +227,7 @@ public Builder sortBy(SortBy sortBy) { * @param apply the apply specification * @return the builder. */ - public Builder apply(Apply apply) { + public Builder apply(Apply apply) { args.pipelineOperations.add(apply); return this; } @@ -263,8 +259,8 @@ public Builder apply(Apply apply) { * @param num the maximum number of results to return * @return the builder. */ - public Builder limit(long offset, long num) { - args.pipelineOperations.add(new Limit<>(offset, num)); + public Builder limit(long offset, long num) { + args.pipelineOperations.add(new Limit(offset, num)); return this; } @@ -297,8 +293,8 @@ public Builder limit(long offset, long num) { * @param filter the filter expression (e.g., "@price > 100", "@category == 'electronics'") * @return the builder. */ - public Builder filter(V filter) { - args.pipelineOperations.add(new Filter<>(filter)); + public Builder filter(String filter) { + args.pipelineOperations.add(new Filter(filter)); return this; } @@ -323,14 +319,15 @@ public Builder filter(V filter) { * * *

    - * Use {@link io.lettuce.core.api.RediSearchCommands#ftCursorread(Object, long)} and - * {@link io.lettuce.core.api.RediSearchCommands#ftCursordel(Object, long)} to iterate through and manage the cursor. + * Use {@link io.lettuce.core.api.sync.RediSearchCommands#ftCursorread(String, AggregationReply.Cursor, int)} and + * {@link io.lettuce.core.api.sync.RediSearchCommands#ftCursordel(String, AggregationReply.Cursor)} to iterate through + * and manage the cursor. *

    * * @param withCursor the cursor specification with count and optional idle timeout * @return the builder. */ - public Builder withCursor(WithCursor withCursor) { + public Builder withCursor(WithCursor withCursor) { args.withCursor = Optional.of(withCursor); return this; } @@ -367,7 +364,29 @@ public Builder withCursor(WithCursor withCursor) { * @param value the parameter value * @return the builder. */ - public Builder param(K name, V value) { + public Builder param(String name, String value) { + args.params.put(name, value); + return this; + } + + /** + * Add a binary parameter for parameterized queries. + * + *

    + * Defines a binary value parameter that can be referenced in the query using {@code $name}. The value bypasses the + * connection's value codec, which is useful for passing vector blobs (e.g. KNN {@code $BLOB}) over a non-binary + * connection. + *

    + * + *

    + * Note: To use PARAMS, set DIALECT to 2 or greater. + *

    + * + * @param name the parameter name (referenced as $name in query) + * @param value the binary parameter value (e.g., vector data) + * @return the builder. + */ + public Builder param(String name, byte[] value) { args.params.put(name, value); return this; } @@ -378,7 +397,7 @@ public Builder param(K name, V value) { * @param scorer the scorer function * @return the builder. */ - public Builder scorer(V scorer) { + public Builder scorer(String scorer) { args.scorer = Optional.of(scorer); return this; } @@ -411,7 +430,7 @@ public Builder scorer(V scorer) { * * @return the builder. */ - public Builder addScores() { + public Builder addScores() { args.addScores = Optional.of(true); return this; } @@ -422,7 +441,7 @@ public Builder addScores() { * @param dialect the query dialect * @return the builder. */ - public Builder dialect(QueryDialects dialect) { + public Builder dialect(QueryDialects dialect) { args.dialect = dialect; return this; } @@ -434,8 +453,8 @@ public Builder dialect(QueryDialects dialect) { * @return the builder. */ @SafeVarargs - public final Builder groupBy(K... properties) { - return groupBy(new GroupBy<>(Arrays.asList(properties))); + public final Builder groupBy(String... properties) { + return groupBy(new GroupBy(Arrays.asList(properties))); } /** @@ -445,8 +464,8 @@ public final Builder groupBy(K... properties) { * @param direction the sort direction * @return the builder. */ - public Builder sortBy(K property, SortDirection direction) { - return sortBy(new SortBy<>(Collections.singletonList(new SortProperty<>(property, direction)))); + public Builder sortBy(String property, SortDirection direction) { + return sortBy(new SortBy(Collections.singletonList(new SortProperty(property, direction)))); } /** @@ -456,8 +475,8 @@ public Builder sortBy(K property, SortDirection direction) { * @param name the result field name * @return the builder. */ - public Builder apply(V expression, K name) { - return apply(new Apply<>(expression, name)); + public Builder apply(String expression, String name) { + return apply(new Apply(expression, name)); } /** @@ -465,7 +484,7 @@ public Builder apply(V expression, K name) { * * @return the built {@link AggregateArgs}. */ - public AggregateArgs build() { + public AggregateArgs build() { return args; } @@ -476,7 +495,7 @@ public AggregateArgs build() { * * @param args the {@link CommandArgs} object */ - public void build(CommandArgs args) { + public void build(CommandArgs args) { verbatim.ifPresent(v -> args.add(CommandKeyword.VERBATIM)); // ADDSCORES is a query-level option and must be emitted before the result-processing pipeline @@ -491,18 +510,18 @@ public void build(CommandArgs args) { } else { // Count the total number of arguments (field + optional AS + alias) int argCount = 0; - for (LoadField loadField : loadFields) { + for (LoadField loadField : loadFields) { argCount++; // field if (loadField.alias != null) { argCount += 2; // AS + alias } } args.add(argCount); - for (LoadField loadField : loadFields) { - args.add(loadField.field.toString()); + for (LoadField loadField : loadFields) { + args.add(loadField.field); if (loadField.alias != null) { args.add(CommandKeyword.AS); - args.add(loadField.alias.toString()); + args.add(loadField.alias); } } } @@ -514,11 +533,8 @@ public void build(CommandArgs args) { }); // Add pipeline operations in user-specified order - for (PipelineOperation operation : pipelineOperations) { - // Cast is safe because all operations can build with CommandArgs - @SuppressWarnings("unchecked") - PipelineOperation typedOperation = (PipelineOperation) operation; - typedOperation.build(args); + for (PipelineOperation operation : pipelineOperations) { + operation.build(args); } // Add WITHCURSOR clause @@ -538,14 +554,18 @@ public void build(CommandArgs args) { args.add(CommandKeyword.PARAMS); args.add(params.size() * 2L); params.forEach((key, value) -> { - args.add(key.toString()); - args.addValue(value); + args.add(key); + if (value instanceof byte[]) { + args.add((byte[]) value); + } else { + args.add((String) value); + } }); } scorer.ifPresent(s -> { args.add(CommandKeyword.SCORER); - args.addValue(s); + args.add(s); }); args.add(CommandKeyword.DIALECT); @@ -560,32 +580,32 @@ public Optional getWithCursor() { * Interface for pipeline operations that need to be applied in user-specified order. This includes GROUPBY, SORTBY, APPLY, * and FILTER operations. */ - public interface PipelineOperation { + public interface PipelineOperation { /** * Build the operation arguments into the command args. - * + * * @param args the command args to build into */ - void build(CommandArgs args); + void build(CommandArgs args); } // Helper classes - public static class LoadField { + public static class LoadField { - final K field; + final String field; - final K alias; + final String alias; - LoadField(K field, K alias) { + LoadField(String field, String alias) { this.field = field; this.alias = alias; } } - public static class Limit implements PipelineOperation { + public static class Limit implements PipelineOperation { final long offset; @@ -597,7 +617,7 @@ public static class Limit implements PipelineOperation { } @Override - public void build(CommandArgs args) { + public void build(CommandArgs args) { args.add(CommandKeyword.LIMIT); args.add(offset); args.add(num); @@ -654,10 +674,10 @@ public static WithCursor of(Long count) { * { * @code * // Group by category and count items - * GroupBy groupBy = GroupBy.of("category").reduce(Reducer.count().as("item_count")); + * GroupBy groupBy = GroupBy.of("category").reduce(Reducer.count().as("item_count")); * * // Group by multiple fields with multiple reducers - * GroupBy complexGroup = GroupBy.of("category", "brand").reduce(Reducer.count().as("count")) + * GroupBy complexGroup = GroupBy.of("category", "brand").reduce(Reducer.count().as("count")) * .reduce(Reducer.avg("@price").as("avg_price")).reduce(Reducer.sum("@quantity").as("total_quantity")); * } * @@ -669,6 +689,8 @@ public static WithCursor of(Long count) { *
  • AVG - Calculate average of numeric values
  • *
  • MIN/MAX - Find minimum/maximum values
  • *
  • COUNT_DISTINCT - Count distinct values
  • + *
  • COLLECT - Collect per-row field projections into an array of entries per group (experimental, see + * {@link Reducer#collect()})
  • * * *

    @@ -676,18 +698,18 @@ public static WithCursor of(Long count) { * performance. *

    */ - public static class GroupBy implements PipelineOperation { + public static class GroupBy implements PipelineOperation { - private final List properties; + private final List properties; - private final List> reducers; + private final List reducers; - public GroupBy(List properties) { + public GroupBy(List properties) { this.properties = new ArrayList<>(properties); this.reducers = new ArrayList<>(); } - public GroupBy reduce(Reducer reducer) { + public GroupBy reduce(Reducer reducer) { this.reducers.add(reducer); return this; } @@ -696,20 +718,18 @@ public GroupBy reduce(Reducer reducer) { * Static factory method to create a GroupBy instance. * * @param properties the properties to group by - * @param Key type - * @param Value type * @return new GroupBy instance */ @SafeVarargs - public static GroupBy of(K... properties) { - return new GroupBy<>(Arrays.asList(properties)); + public static GroupBy of(String... properties) { + return new GroupBy(Arrays.asList(properties)); } @Override - public void build(CommandArgs args) { + public void build(CommandArgs args) { args.add(CommandKeyword.GROUPBY); args.add(properties.size()); - for (K property : properties) { + for (String property : properties) { // Add @ prefix if not already present String propertyStr = property.toString(); if (!propertyStr.startsWith("@")) { @@ -719,7 +739,7 @@ public void build(CommandArgs args) { } } - for (Reducer reducer : reducers) { + for (Reducer reducer : reducers) { reducer.build(args); } } @@ -741,15 +761,15 @@ public void build(CommandArgs args) { * { * @code * // Simple sort by single field - * SortBy sortBy = SortBy.of("price", SortDirection.DESC); + * SortBy sortBy = SortBy.of("price", SortDirection.DESC); * * // Sort with MAX optimization for top-N queries - * SortBy topN = SortBy.of("score", SortDirection.DESC).max(100) // Only sort top 100 results + * SortBy topN = SortBy.of("score", SortDirection.DESC).max(100) // Only sort top 100 results * .withCount(); // Include accurate count * * // Multiple sort criteria - * SortBy multiSort = SortBy.of(new SortProperty<>("category", SortDirection.ASC), - * new SortProperty<>("price", SortDirection.DESC)); + * SortBy multiSort = SortBy.of(new SortProperty("category", SortDirection.ASC), + * new SortProperty("price", SortDirection.DESC)); * } * * @@ -765,24 +785,24 @@ public void build(CommandArgs args) { * using LIMIT. *

    */ - public static class SortBy implements PipelineOperation { + public static class SortBy implements PipelineOperation { - private final List> properties; + private final List properties; private Optional max = Optional.empty(); private boolean withCount = false; - public SortBy(List> properties) { + public SortBy(List properties) { this.properties = new ArrayList<>(properties); } - public SortBy max(long max) { + public SortBy max(long max) { this.max = Optional.of(max); return this; } - public SortBy withCount() { + public SortBy withCount() { this.withCount = true; return this; } @@ -792,31 +812,29 @@ public SortBy withCount() { * * @param property the property to sort by * @param direction the sort direction - * @param Key type * @return new SortBy instance */ - public static SortBy of(K property, SortDirection direction) { - return new SortBy<>(Collections.singletonList(new SortProperty<>(property, direction))); + public static SortBy of(String property, SortDirection direction) { + return new SortBy(Collections.singletonList(new SortProperty(property, direction))); } /** * Static factory method to create a SortBy instance with multiple properties. * * @param properties the properties to sort by - * @param Key type * @return new SortBy instance */ @SafeVarargs - public static SortBy of(SortProperty... properties) { - return new SortBy<>(Arrays.asList(properties)); + public static SortBy of(SortProperty... properties) { + return new SortBy(Arrays.asList(properties)); } @Override - public void build(CommandArgs args) { + public void build(CommandArgs args) { args.add(CommandKeyword.SORTBY); // Count includes property + direction pairs args.add(properties.size() * 2L); - for (SortProperty property : properties) { + for (SortProperty property : properties) { // Add @ prefix if not already present String propertyStr = property.property.toString(); if (!propertyStr.startsWith("@")) { @@ -855,16 +873,16 @@ public void build(CommandArgs args) { * { * @code * // Calculate total value from price and quantity - * Apply totalValue = new Apply<>("@price * @quantity", "total_value"); + * Apply totalValue = new Apply("@price * @quantity", "total_value"); * * // Mathematical operations - * Apply discount = new Apply<>("@price * 0.9", "discounted_price"); + * Apply discount = new Apply("@price * 0.9", "discounted_price"); * * // String operations - * Apply fullName = new Apply<>("@first_name + ' ' + @last_name", "full_name"); + * Apply fullName = new Apply("@first_name + ' ' + @last_name", "full_name"); * * // Date operations - * Apply dayOfWeek = new Apply<>("day(@timestamp)", "day"); + * Apply dayOfWeek = new Apply("day(@timestamp)", "day"); * } * * @@ -882,23 +900,23 @@ public void build(CommandArgs args) { * can be referenced by further operations. *

    */ - public static class Apply implements PipelineOperation { + public static class Apply implements PipelineOperation { - private final V expression; + private final String expression; - private final K name; + private final String name; - public Apply(V expression, K name) { + public Apply(String expression, String name) { this.expression = expression; this.name = name; } @Override - public void build(CommandArgs args) { + public void build(CommandArgs args) { args.add(CommandKeyword.APPLY); - args.addValue(expression); + args.add(expression); args.add(CommandKeyword.AS); - args.add(name.toString()); + args.add(name); } /** @@ -906,12 +924,10 @@ public void build(CommandArgs args) { * * @param name the name of the expression * @param expression the expression to apply - * @param Key type - * @param Value type * @return new Apply instance */ - public static Apply of(V expression, K name) { - return new Apply<>(expression, name); + public static Apply of(String expression, String name) { + return new Apply(expression, name); } } @@ -931,20 +947,20 @@ public static Apply of(V expression, K name) { * { * @code * // Count items in each group - * Reducer count = Reducer.count().as("item_count"); + * Reducer count = Reducer.count().as("item_count"); * * // Sum numeric values - * Reducer totalSales = Reducer.sum("@sales").as("total_sales"); + * Reducer totalSales = Reducer.sum("@sales").as("total_sales"); * * // Calculate average - * Reducer avgPrice = Reducer.avg("@price").as("average_price"); + * Reducer avgPrice = Reducer.avg("@price").as("average_price"); * * // Find extremes - * Reducer maxScore = Reducer.max("@score").as("highest_score"); - * Reducer minPrice = Reducer.min("@price").as("lowest_price"); + * Reducer maxScore = Reducer.max("@score").as("highest_score"); + * Reducer minPrice = Reducer.min("@price").as("lowest_price"); * * // Count distinct values - * Reducer uniqueUsers = Reducer.countDistinct("@user_id").as("unique_users"); + * Reducer uniqueUsers = Reducer.countDistinct("@user_id").as("unique_users"); * } * * @@ -963,20 +979,20 @@ public static Apply of(V expression, K name) { * name (e.g., "count_distinct(@user_id)"). *

    */ - public static class Reducer { + public static class Reducer { private final String function; - private final List args; + private final List args; - private Optional alias = Optional.empty(); + private Optional alias = Optional.empty(); - public Reducer(String function, List args) { + public Reducer(String function, List args) { this.function = function; this.args = new ArrayList<>(args); } - public Reducer as(K alias) { + public Reducer as(String alias) { this.alias = Optional.of(alias); return this; } @@ -984,88 +1000,351 @@ public Reducer as(K alias) { /** * Static factory method to create a COUNT reducer. * - * @param Key type - * @param Value type * @return new COUNT Reducer instance */ - public static Reducer count() { - return new Reducer<>("COUNT", Collections.emptyList()); + public static Reducer count() { + return new Reducer("COUNT", Collections.emptyList()); } /** * Static factory method to create a SUM reducer. * * @param field the field to sum - * @param Key type - * @param Value type * @return new SUM Reducer instance */ - public static Reducer sum(V field) { - return new Reducer<>("SUM", Collections.singletonList(field)); + public static Reducer sum(String field) { + return new Reducer("SUM", Collections.singletonList(field)); } /** * Static factory method to create an AVG reducer. * * @param field the field to average - * @param Key type - * @param Value type * @return new AVG Reducer instance */ - public static Reducer avg(V field) { - return new Reducer<>("AVG", Collections.singletonList(field)); + public static Reducer avg(String field) { + return new Reducer("AVG", Collections.singletonList(field)); } /** * Static factory method to create a MIN reducer. * * @param field the field to find minimum - * @param Key type - * @param Value type * @return new MIN Reducer instance */ - public static Reducer min(V field) { - return new Reducer<>("MIN", Collections.singletonList(field)); + public static Reducer min(String field) { + return new Reducer("MIN", Collections.singletonList(field)); } /** * Static factory method to create a MAX reducer. * * @param field the field to find maximum - * @param Key type - * @param Value type * @return new MAX Reducer instance */ - public static Reducer max(V field) { - return new Reducer<>("MAX", Collections.singletonList(field)); + public static Reducer max(String field) { + return new Reducer("MAX", Collections.singletonList(field)); } /** * Static factory method to create a COUNT_DISTINCT reducer. * * @param field the field to count distinct values - * @param Key type - * @param Value type * @return new COUNT_DISTINCT Reducer instance */ - public static Reducer countDistinct(V field) { - return new Reducer<>("COUNT_DISTINCT", Collections.singletonList(field)); + public static Reducer countDistinct(String field) { + return new Reducer("COUNT_DISTINCT", Collections.singletonList(field)); } - public void build(CommandArgs args) { + /** + * Static factory method to create a {@code COLLECT} reducer. + * + *

    + * {@code COLLECT} gathers per-document projections within a {@code GROUPBY} group and returns them as an array of + * per-entry maps under the reducer alias, optionally sorted and bounded. Configure the projected fields via + * {@link CollectReducer#fields(String[]) fields(...)} or {@link CollectReducer#fieldsAll() fieldsAll()}, then + * optionally chain {@link CollectReducer#sortBy(SortProperty[]) sortBy(...)} and + * {@link CollectReducer#limit(long, long) limit(...)} before calling {@link Reducer#as(String) as(...)}. + *

    + * + *

    + * The collected column is a {@link io.lettuce.core.search.FieldValue} of kind + * {@link io.lettuce.core.search.FieldValue.Kind#ARRAY} with one element per collected entry; read the entries via + * {@link io.lettuce.core.search.FieldValue#asList() asList()} and each entry via + * {@link io.lettuce.core.search.FieldValue#asMap() asMap()}, which normalizes the protocol-specific entry shape. + *

    + * + *

    + * Experimental. Both the underlying Redis Search feature and this API may change. {@code COLLECT} is + * gated behind {@code search-enable-unstable-features}; enable it on the server (for example via + * {@code CONFIG SET search-enable-unstable-features yes}) before issuing aggregations that use this reducer, otherwise + * the server replies with an error. + *

    + * + * @return new {@link CollectReducer} instance + * @see CollectReducer + */ + @Experimental + public static CollectReducer collect() { + return new CollectReducer(); + } + + public void build(CommandArgs args) { args.add(CommandKeyword.REDUCE); args.add(function); args.add(this.args.size()); - for (V arg : this.args) { - args.addValue(arg); + for (String arg : this.args) { + args.add(arg.toString()); + } + + alias.ifPresent(a -> { + args.add(CommandKeyword.AS); + args.add(a); + }); + } + + } + + /** + * Represents a {@code REDUCE COLLECT} clause in an aggregation pipeline. + * + *

    + * Within each {@code GROUPBY} group, {@code COLLECT} projects a chosen set of fields from every row and returns them as an + * array of per-entry maps under the reducer alias, optionally sorted and bounded. It targets grouped-reporting workflows + * where the caller needs the representative rows of each group in a single aggregation query. The grammar produced by this + * builder is: + *

    + * + *
    +     * {@code
    +     * REDUCE COLLECT 
    +     *     FIELDS ( * |  <@field> [<@field> ...] )
    +     *     [SORTBY  <@field> [ASC|DESC] [<@field> [ASC|DESC] ...]]
    +     *     [LIMIT  ]
    +     *   [AS ]
    +     * }
    +     * 
    + * + *

    + * Field and sort-key names are referenced with an {@code @} prefix on the wire (the builder adds it automatically when it + * is missing). The output map keys are the bare names. {@code FIELDS *} projects whatever the pipeline has materialized at + * the {@code COLLECT} stage; it does not implicitly fetch the full document. + *

    + * + *

    + * The collected column is a {@link io.lettuce.core.search.FieldValue} of kind + * {@link io.lettuce.core.search.FieldValue.Kind#ARRAY} with one element per collected entry; read the entries via + * {@link io.lettuce.core.search.FieldValue#asList() asList()} and each entry via + * {@link io.lettuce.core.search.FieldValue#asMap() asMap()}, which normalizes the protocol-specific entry shape (RESP3 + * returns each entry as a map, RESP2 as a flat key/value array). + *

    + * + *

    + * The number of collected entries per group is always bounded by the server: {@code SORTBY} without an explicit + * {@code LIMIT} returns at most 10 entries per group, and without either clause collection is capped by the + * {@code search-max-aggregate-results} configuration. Supply an explicit {@link #limit(long, long) limit(...)} to control + * the bound. + *

    + * + *

    + * Experimental. Both the underlying Redis Search feature and this API may change. {@code COLLECT} is gated + * behind {@code search-enable-unstable-features}; enable it on the server before issuing aggregations that use this + * reducer. + *

    + * + * @see Reducer#collect() + * @since 7.7 + */ + @Experimental + public static class CollectReducer extends Reducer { + + private boolean allFields = false; + + private final List fields = new ArrayList<>(); + + private final List sortProperties = new ArrayList<>(); + + private Optional limitOffset = Optional.empty(); + + private Optional limitCount = Optional.empty(); + + private Optional alias = Optional.empty(); + + CollectReducer() { + super("COLLECT", Collections.emptyList()); + } + + /** + * Project the named fields for every document in the group. Names may be supplied with or without a leading {@code @}; + * the builder normalizes each to a single {@code @} on the wire. Use {@code @__key} or ordinary document field + * names. + * + *

    + * Mutually exclusive with {@link #fieldsAll()}. May be called multiple times to append further fields. + *

    + * + * @param fields the fields to project + * @return {@code this} for chaining + */ + public CollectReducer fields(String... fields) { + if (this.allFields) { + throw new IllegalStateException("REDUCE COLLECT cannot mix FIELDS * with explicit field names"); + } + Collections.addAll(this.fields, fields); + return this; + } + + /** + * Project every field present in the pipeline at the {@code COLLECT} stage ({@code FIELDS *}). + * + *

    + * Per the COLLECT specification, {@code *} does not trigger an implicit load — fields must already be in the pipeline + * (typically via {@code LOAD *} or because they are grouping keys / reducer aliases). Mutually exclusive with + * {@link #fields(String[])}. + *

    + * + * @return {@code this} for chaining + */ + public CollectReducer fieldsAll() { + if (!this.fields.isEmpty()) { + throw new IllegalStateException("REDUCE COLLECT cannot mix FIELDS * with explicit field names"); + } + this.allFields = true; + return this; + } + + /** + * In-group sort by one or more properties. May be called multiple times to append further sort keys. + * + *

    + * Note: when {@code SORTBY} is supplied without an explicit {@link #limit(long, long) limit(...)}, the + * server applies a default limit of 10 entries per group. Supply an explicit limit to collect more sorted entries. + * Without {@code SORTBY}, entry order is unspecified and collection is capped by the server's + * {@code search-max-aggregate-results} configuration. + *

    + * + * @param properties the sort properties + * @return {@code this} for chaining + */ + public CollectReducer sortBy(SortProperty... properties) { + Collections.addAll(this.sortProperties, properties); + return this; + } + + /** + * Convenience for {@code sortBy(new SortProperty(field, SortDirection.ASC))}. + * + * @param field the field to sort by ascending + * @return {@code this} for chaining + */ + public CollectReducer sortByAsc(String field) { + this.sortProperties.add(new SortProperty(field, SortDirection.ASC)); + return this; + } + + /** + * Convenience for {@code sortBy(new SortProperty(field, SortDirection.DESC))}. + * + * @param field the field to sort by descending + * @return {@code this} for chaining + */ + public CollectReducer sortByDesc(String field) { + this.sortProperties.add(new SortProperty(field, SortDirection.DESC)); + return this; + } + + /** + * Bound the output per group to the first {@code count} entries (offset 0). + * + * @param count the maximum number of entries per group + * @return {@code this} for chaining + */ + public CollectReducer limit(long count) { + return limit(0, count); + } + + /** + * Bound the output per group to {@code count} entries starting at {@code offset}. + * + * @param offset the number of entries to skip + * @param count the maximum number of entries to return + * @return {@code this} for chaining + */ + public CollectReducer limit(long offset, long count) { + if (offset < 0 || count < 0) { + throw new IllegalArgumentException("LIMIT offset and count must be non-negative"); + } + this.limitOffset = Optional.of(offset); + this.limitCount = Optional.of(count); + return this; + } + + @Override + public CollectReducer as(String alias) { + this.alias = Optional.of(alias); + return this; + } + + @Override + public void build(CommandArgs args) { + if (!allFields && fields.isEmpty()) { + throw new IllegalStateException("REDUCE COLLECT requires either fields(...) or fieldsAll() to be configured"); + } + + args.add(CommandKeyword.REDUCE); + args.add("COLLECT"); + args.add(argCount()); + + args.add(CommandKeyword.FIELDS); + if (allFields) { + args.add("*"); + } else { + args.add(fields.size()); + for (String field : fields) { + args.add(withAtPrefix(field)); + } + } + + if (!sortProperties.isEmpty()) { + args.add(CommandKeyword.SORTBY); + args.add(sortProperties.size() * 2L); + for (SortProperty property : sortProperties) { + args.add(withAtPrefix(property.property)); + args.add(property.direction.name()); + } + } + + if (limitOffset.isPresent()) { + args.add(CommandKeyword.LIMIT); + args.add(limitOffset.get()); + args.add(limitCount.get()); } alias.ifPresent(a -> { args.add(CommandKeyword.AS); - args.add(a.toString()); + args.add(a); }); } + /** + * Computes {@code } as the number of reducer argument tokens (the {@code FIELDS}, {@code SORTBY}, and + * {@code LIMIT} clauses), excluding the trailing {@code AS }. + */ + private long argCount() { + long count = allFields ? 2 : 2 + fields.size(); + if (!sortProperties.isEmpty()) { + count += 2 + sortProperties.size() * 2L; + } + if (limitOffset.isPresent()) { + count += 3; + } + return count; + } + + private static String withAtPrefix(String name) { + return name.startsWith("@") ? name : "@" + name; + } + } /** @@ -1077,18 +1356,18 @@ public void build(CommandArgs args) { * reducer results. *

    */ - public static class Filter implements PipelineOperation { + public static class Filter implements PipelineOperation { - private final V expression; + private final String expression; - public Filter(V expression) { + public Filter(String expression) { this.expression = expression; } @Override - public void build(CommandArgs args) { + public void build(CommandArgs args) { args.add(CommandKeyword.FILTER); - args.addValue(expression); + args.add(expression); } } @@ -1096,13 +1375,13 @@ public void build(CommandArgs args) { /** * Represents a sort property with direction. */ - public static class SortProperty { + public static class SortProperty { - final K property; + final String property; final SortDirection direction; - public SortProperty(K property, SortDirection direction) { + public SortProperty(String property, SortDirection direction) { this.property = property; this.direction = direction; } diff --git a/src/main/java/io/lettuce/core/search/arguments/CreateArgs.java b/src/main/java/io/lettuce/core/search/arguments/CreateArgs.java index 1fbeb0a1fa..28c24f0226 100644 --- a/src/main/java/io/lettuce/core/search/arguments/CreateArgs.java +++ b/src/main/java/io/lettuce/core/search/arguments/CreateArgs.java @@ -20,14 +20,12 @@ /** * Argument list builder for {@code FT.CREATE}. * - * @param Key type. - * @param Value type. * @see FT.CREATE * @since 6.8 * @author Tihomir Mateev */ @SuppressWarnings("OptionalUsedAsFieldOrParameterType") -public class CreateArgs { +public class CreateArgs { /** * Possible target types for the index. @@ -38,19 +36,19 @@ public enum TargetType { private Optional on = Optional.of(TargetType.HASH); - private final List prefixes = new ArrayList<>(); + private final List prefixes = new ArrayList<>(); - private Optional filter = Optional.empty(); + private Optional filter = Optional.empty(); private Optional defaultLanguage = Optional.empty(); - private Optional languageField = Optional.empty(); + private Optional languageField = Optional.empty(); private OptionalDouble defaultScore = OptionalDouble.empty(); - private Optional scoreField = Optional.empty(); + private Optional scoreField = Optional.empty(); - private Optional payloadField = Optional.empty(); + private Optional payloadField = Optional.empty(); private boolean maxTextFields = false; @@ -66,61 +64,57 @@ public enum TargetType { private boolean skipInitialScan = false; - private Optional> stopWords = Optional.empty(); + private Optional> stopWords = Optional.empty(); /** * Used to build a new instance of the {@link CreateArgs}. * * @return a {@link Builder} that provides the option to build up a new instance of the {@link CreateArgs} - * @param the key type - * @param the value type */ - public static Builder builder() { - return new Builder<>(); + public static Builder builder() { + return new Builder(); } /** * Builder for {@link CreateArgs}. *

    * As a final step the {@link Builder#build()} method needs to be executed to create the final {@link CreateArgs} instance. - * - * @param the key type - * @param the value type + * * @see FT.CREATE */ - public static class Builder { + public static class Builder { - private final CreateArgs instance = new CreateArgs<>(); + private final CreateArgs instance = new CreateArgs(); /** * Set the {@link TargetType} type for the index. Defaults to {@link TargetType#HASH}. - * + * * @param targetType the target type * @return the instance of the current {@link Builder} for the purpose of method chaining */ - public Builder on(TargetType targetType) { + public Builder on(TargetType targetType) { instance.on = Optional.of(targetType); return this; } /** * Add a prefix to the index. You can add several prefixes to index. Default setting is * (all keys). - * + * * @param prefix the prefix * @return the instance of the current {@link Builder} for the purpose of method chaining */ - public Builder withPrefix(K prefix) { + public Builder withPrefix(String prefix) { instance.prefixes.add(prefix); return this; } /** * Add a list of prefixes to the index. You can add several prefixes to index. Default setting is * (all keys). - * + * * @param prefixes a {@link List} of prefixes * @return the instance of the current {@link Builder} for the purpose of method chaining */ - public Builder withPrefixes(List prefixes) { + public Builder withPrefixes(List prefixes) { instance.prefixes.addAll(prefixes); return this; } @@ -130,23 +124,23 @@ public Builder withPrefixes(List prefixes) { *

    * It is possible to use @__key to access the key that was just added/changed. A field can be used to set field name by * passing 'FILTER @indexName=="myindexname"'. - * + * * @param filter a filter expression with the full RediSearch aggregation expression language * @return the instance of the current {@link Builder} for the purpose of method chaining * @see RediSearch Query */ - public Builder filter(V filter) { + public Builder filter(String filter) { instance.filter = Optional.of(filter); return this; } /** * Set the default language for the documents in the index. The default setting is English. - * + * * @param language the default language * @return the instance of the current {@link Builder} for the purpose of method chaining */ - public Builder defaultLanguage(DocumentLanguage language) { + public Builder defaultLanguage(DocumentLanguage language) { instance.defaultLanguage = Optional.of(language); return this; } @@ -154,37 +148,37 @@ public Builder defaultLanguage(DocumentLanguage language) { /** * Set the field that contains the language setting for the documents in the index. The default setting is to have no * language field. - * + * * @param field the language field * @return the instance of the current {@link Builder} for the purpose of method chaining * @see Stemming */ - public Builder languageField(K field) { + public Builder languageField(String field) { instance.languageField = Optional.of(field); return this; } /** * Set the default score for the documents in the index. The default setting is 1.0. - * + * * @param score the default score * @return the instance of the current {@link Builder} for the purpose of method chaining * @see Scoring */ - public Builder defaultScore(double score) { + public Builder defaultScore(double score) { instance.defaultScore = OptionalDouble.of(score); return this; } /** * Set the field that contains the score setting for the documents in the index. The default setting is a score of 1.0. - * + * * @param field the score field * @return the instance of the current {@link Builder} for the purpose of method chaining * @see Scoring */ - public Builder scoreField(K field) { + public Builder scoreField(String field) { instance.scoreField = Optional.of(field); return this; } @@ -195,12 +189,12 @@ public Builder scoreField(K field) { *

    * This should be a document attribute that you use as a binary safe payload string to the document that can be * evaluated at query time by a custom scoring function or retrieved to the client - * + * * @param field the payload field * @return the instance of the current {@link Builder} for the purpose of method chaining * @see Scoring */ - public Builder payloadField(K field) { + public Builder payloadField(String field) { instance.payloadField = Optional.of(field); return this; } @@ -214,7 +208,7 @@ public Builder payloadField(K field) { * * @return the instance of the current {@link Builder} for the purpose of method chaining */ - public Builder maxTextFields() { + public Builder maxTextFields() { instance.maxTextFields = true; return this; } @@ -232,11 +226,11 @@ public Builder maxTextFields() { * are deleted along with the index. Historically, RediSearch used an FT.ADD command, which made a connection between * the document and the index. Then, FT.DROP, also a hystoric command, deleted documents by default. In version 2.x, * RediSearch indexes hashes and JSONs, and the dependency between the index and documents no longer exists. - * + * * @param seconds the temporary index expiration time in seconds * @return the instance of the current {@link Builder} for the purpose of method chaining */ - public Builder temporary(long seconds) { + public Builder temporary(long seconds) { instance.temporary = OptionalLong.of(seconds); return this; } @@ -249,7 +243,7 @@ public Builder temporary(long seconds) { * * @return the instance of the current {@link Builder} for the purpose of method chaining */ - public Builder noOffsets() { + public Builder noOffsets() { instance.noOffsets = true; return this; } @@ -262,7 +256,7 @@ public Builder noOffsets() { * * @return the instance of the current {@link Builder} for the purpose of method chaining */ - public Builder noHighlighting() { + public Builder noHighlighting() { instance.noHighlight = true; return this; } @@ -274,7 +268,7 @@ public Builder noHighlighting() { * * @return the instance of the current {@link Builder} for the purpose of method chaining */ - public Builder noFields() { + public Builder noFields() { instance.noFields = true; return this; } @@ -287,7 +281,7 @@ public Builder noFields() { * * @return the instance of the current {@link Builder} for the purpose of method chaining */ - public Builder noFrequency() { + public Builder noFrequency() { instance.noFrequency = true; return this; } @@ -297,7 +291,7 @@ public Builder noFrequency() { * * @return the instance of the current {@link Builder} for the purpose of method chaining */ - public Builder skipInitialScan() { + public Builder skipInitialScan() { instance.skipInitialScan = true; return this; } @@ -312,12 +306,12 @@ public Builder skipInitialScan() { * @see Stop * words */ - public Builder stopWords(List stopWords) { + public Builder stopWords(List stopWords) { instance.stopWords = Optional.of(stopWords); return this; } - public CreateArgs build() { + public CreateArgs build() { return instance; } @@ -338,10 +332,10 @@ public Optional getOn() { * Get the prefixes for the index. * * @return the prefixes - * @see Builder#withPrefix(Object) + * @see Builder#withPrefix(String) * @see Builder#withPrefixes(List) */ - public List getPrefixes() { + public List getPrefixes() { return prefixes; } @@ -349,9 +343,9 @@ public List getPrefixes() { * Get the filter for the index. * * @return the filter - * @see Builder#filter(Object) + * @see Builder#filter(String) */ - public Optional getFilter() { + public Optional getFilter() { return filter; } @@ -369,9 +363,9 @@ public Optional getDefaultLanguage() { * Get the field that contains the language setting for the documents in the index. * * @return the language field - * @see Builder#languageField(Object) + * @see Builder#languageField(String) */ - public Optional getLanguageField() { + public Optional getLanguageField() { return languageField; } @@ -389,9 +383,9 @@ public OptionalDouble getDefaultScore() { * Get the field that contains the score setting for the documents in the index. * * @return the score field - * @see Builder#scoreField(Object) + * @see Builder#scoreField(String) */ - public Optional getScoreField() { + public Optional getScoreField() { return scoreField; } @@ -399,9 +393,9 @@ public Optional getScoreField() { * Get the field that contains the payload setting for the documents in the index. * * @return the payload field - * @see Builder#payloadField(Object) + * @see Builder#payloadField(String) */ - public Optional getPayloadField() { + public Optional getPayloadField() { return payloadField; } @@ -481,7 +475,7 @@ public boolean isSkipInitialScan() { * @return the stop words * @see Builder#stopWords(List) */ - public Optional> getStopWords() { + public Optional> getStopWords() { return stopWords; } @@ -490,18 +484,18 @@ public Optional> getStopWords() { * * @param args the {@link CommandArgs} object */ - public void build(CommandArgs args) { + public void build(CommandArgs args) { on.ifPresent(targetType -> args.add(ON).add(targetType.name())); if (!prefixes.isEmpty()) { args.add(PREFIX).add(prefixes.size()); - prefixes.forEach(p -> args.add(p.toString())); + prefixes.forEach(args::add); } - filter.ifPresent(filter -> args.add(FILTER).addValue(filter)); + filter.ifPresent(filter -> args.add(FILTER).add(filter)); defaultLanguage.ifPresent(language -> args.add(LANGUAGE).add(language.toString())); - languageField.ifPresent(field -> args.add(LANGUAGE_FIELD).addKey(field)); + languageField.ifPresent(field -> args.add(LANGUAGE_FIELD).add(field)); defaultScore.ifPresent(score -> args.add(SCORE).add(score)); - scoreField.ifPresent(field -> args.add(SCORE_FIELD).addKey(field)); - payloadField.ifPresent(field -> args.add(PAYLOAD_FIELD).addKey(field)); + scoreField.ifPresent(field -> args.add(SCORE_FIELD).add(field)); + payloadField.ifPresent(field -> args.add(PAYLOAD_FIELD).add(field)); if (maxTextFields) { args.add(MAXTEXTFIELDS); } @@ -523,7 +517,7 @@ public void build(CommandArgs args) { } stopWords.ifPresent(words -> { args.add(STOPWORDS).add(words.size()); - words.forEach(args::addValue); + words.forEach(args::add); }); } diff --git a/src/main/java/io/lettuce/core/search/arguments/ExplainArgs.java b/src/main/java/io/lettuce/core/search/arguments/ExplainArgs.java index e9ceec1ff2..fe8eae929e 100644 --- a/src/main/java/io/lettuce/core/search/arguments/ExplainArgs.java +++ b/src/main/java/io/lettuce/core/search/arguments/ExplainArgs.java @@ -14,12 +14,10 @@ *

    * {@link ExplainArgs} is a mutable object and instances should be used only once to avoid shared mutable state. * - * @param Key type. - * @param Value type. * @author Tihomir Mateev * @since 6.8 */ -public class ExplainArgs { +public class ExplainArgs { private QueryDialects dialect = QueryDialects.DIALECT2; @@ -40,8 +38,8 @@ private Builder() { * @return new {@link ExplainArgs} with {@literal DIALECT} set. * @see ExplainArgs#dialect(QueryDialects) */ - public static ExplainArgs dialect(QueryDialects dialect) { - return new ExplainArgs().dialect(dialect); + public static ExplainArgs dialect(QueryDialects dialect) { + return new ExplainArgs().dialect(dialect); } } @@ -53,7 +51,7 @@ public static ExplainArgs dialect(QueryDialects dialect) { * @param dialect the dialect version. * @return {@code this} {@link ExplainArgs}. */ - public ExplainArgs dialect(QueryDialects dialect) { + public ExplainArgs dialect(QueryDialects dialect) { this.dialect = dialect; return this; } @@ -63,7 +61,7 @@ public ExplainArgs dialect(QueryDialects dialect) { * * @param args the command arguments to append to. */ - public void build(CommandArgs args) { + public void build(CommandArgs args) { if (dialect != null) { args.add("DIALECT").add(dialect.toString()); } diff --git a/src/main/java/io/lettuce/core/search/arguments/FieldArgs.java b/src/main/java/io/lettuce/core/search/arguments/FieldArgs.java index 3e339d9069..1abe1319e9 100644 --- a/src/main/java/io/lettuce/core/search/arguments/FieldArgs.java +++ b/src/main/java/io/lettuce/core/search/arguments/FieldArgs.java @@ -19,19 +19,18 @@ * This class contains common options shared by all field types. Specific field types should extend this class and add their * type-specific options. * - * @param Key type * @see Field * and type options * @since 6.8 * @author Tihomir Mateev */ @SuppressWarnings("OptionalUsedAsFieldOrParameterType") -public abstract class FieldArgs { +public abstract class FieldArgs { - // Common field properties - protected K name; + // The indexed field: a hash field name (HASH documents) or a JSONPath expression (JSON documents) + protected String name; - protected Optional as = Optional.empty(); + protected Optional as = Optional.empty(); protected boolean sortable; @@ -51,11 +50,11 @@ public abstract class FieldArgs { public abstract String getFieldType(); /** - * Get the field name. + * Get the indexed field: a hash field name or a JSONPath expression. * - * @return the field name + * @return the hash field name or JSONPath expression */ - public K getName() { + public String getName() { return name; } @@ -64,7 +63,7 @@ public K getName() { * * @return the field alias */ - public Optional getAs() { + public Optional getAs() { return as; } @@ -118,9 +117,9 @@ public boolean isIndexMissing() { * * @param args the command arguments to modify */ - public final void build(CommandArgs args) { - args.add(name.toString()); - as.ifPresent(a -> args.add(AS).add(a.toString())); + public final void build(CommandArgs args) { + args.add(name); + as.ifPresent(a -> args.add(AS).add(a)); args.add(getFieldType()); // Add type-specific arguments @@ -149,16 +148,15 @@ public final void build(CommandArgs args) { * * @param args the command arguments to modify */ - protected abstract void buildTypeSpecificArgs(CommandArgs args); + protected abstract void buildTypeSpecificArgs(CommandArgs args); /** * Base builder for field arguments. * - * @param Key type * @param The concrete field args type * @param The concrete builder type */ - public abstract static class Builder, B extends Builder> { + public abstract static class Builder> { protected final T instance; @@ -182,12 +180,13 @@ protected B self() { } /** - * The name of the field in a hash the index is going to be based on. + * The field to index: a hash field name when indexing HASH documents, or a JSONPath expression when indexing JSON + * documents. * - * @param name the name of the field + * @param name the hash field name or JSONPath expression * @return the instance of the {@link Builder} for the purpose of method chaining */ - public B name(K name) { + public B name(String name) { instance.name = name; return self(); } @@ -199,7 +198,7 @@ public B name(K name) { * @param as the field name to be used in queries * @return the instance of the {@link Builder} for the purpose of method chaining */ - public B as(K as) { + public B as(String as) { instance.as = Optional.of(as); return self(); } diff --git a/src/main/java/io/lettuce/core/search/arguments/GeoFieldArgs.java b/src/main/java/io/lettuce/core/search/arguments/GeoFieldArgs.java index 2740e05165..f7da7d86f6 100644 --- a/src/main/java/io/lettuce/core/search/arguments/GeoFieldArgs.java +++ b/src/main/java/io/lettuce/core/search/arguments/GeoFieldArgs.java @@ -16,23 +16,21 @@ * which allow you to implement location-based search functionality in your applications such as finding nearby restaurants, * stores, or any other points of interest. * - * @param Key type * @see Geo * Fields * @since 6.8 * @author Tihomir Mateev */ -public class GeoFieldArgs extends FieldArgs { +public class GeoFieldArgs extends FieldArgs { /** * Create a new {@link GeoFieldArgs} using the builder pattern. - * - * @param Key type + * * @return a new {@link Builder} */ - public static Builder builder() { - return new Builder<>(); + public static Builder builder() { + return new Builder(); } @Override @@ -41,19 +39,17 @@ public String getFieldType() { } @Override - protected void buildTypeSpecificArgs(CommandArgs args) { + protected void buildTypeSpecificArgs(CommandArgs args) { // Geo fields have no type-specific arguments beyond the common ones } /** * Builder for {@link GeoFieldArgs}. - * - * @param Key type */ - public static class Builder extends FieldArgs.Builder, Builder> { + public static class Builder extends FieldArgs.Builder { public Builder() { - super(new GeoFieldArgs<>()); + super(new GeoFieldArgs()); } } diff --git a/src/main/java/io/lettuce/core/search/arguments/GeoshapeFieldArgs.java b/src/main/java/io/lettuce/core/search/arguments/GeoshapeFieldArgs.java index 04a55437a7..bdabf7d0ef 100644 --- a/src/main/java/io/lettuce/core/search/arguments/GeoshapeFieldArgs.java +++ b/src/main/java/io/lettuce/core/search/arguments/GeoshapeFieldArgs.java @@ -21,7 +21,6 @@ * contained within an enclosing shape). You can also choose between geographical coordinates (on the surface of a sphere) or * standard Cartesian coordinates. * - * @param Key type * @see Geoshape * Fields @@ -29,7 +28,7 @@ * @author Tihomir Mateev */ @SuppressWarnings("OptionalUsedAsFieldOrParameterType") -public class GeoshapeFieldArgs extends FieldArgs { +public class GeoshapeFieldArgs extends FieldArgs { /** * Coordinate system for geoshape fields. @@ -49,12 +48,11 @@ public enum CoordinateSystem { /** * Create a new {@link GeoshapeFieldArgs} using the builder pattern. - * - * @param Key type + * * @return a new {@link Builder} */ - public static Builder builder() { - return new Builder<>(); + public static Builder builder() { + return new Builder(); } @Override @@ -72,7 +70,7 @@ public Optional getCoordinateSystem() { } @Override - protected void buildTypeSpecificArgs(CommandArgs args) { + protected void buildTypeSpecificArgs(CommandArgs args) { coordinateSystem.ifPresent(cs -> { switch (cs) { case FLAT: @@ -87,13 +85,11 @@ protected void buildTypeSpecificArgs(CommandArgs args) { /** * Builder for {@link GeoshapeFieldArgs}. - * - * @param Key type */ - public static class Builder extends FieldArgs.Builder, Builder> { + public static class Builder extends FieldArgs.Builder { public Builder() { - super(new GeoshapeFieldArgs<>()); + super(new GeoshapeFieldArgs()); } /** @@ -102,7 +98,7 @@ public Builder() { * @param coordinateSystem the coordinate system * @return the instance of the {@link Builder} for the purpose of method chaining */ - public Builder coordinateSystem(CoordinateSystem coordinateSystem) { + public Builder coordinateSystem(CoordinateSystem coordinateSystem) { instance.coordinateSystem = Optional.of(coordinateSystem); return self(); } @@ -112,7 +108,7 @@ public Builder coordinateSystem(CoordinateSystem coordinateSystem) { * * @return the instance of the {@link Builder} for the purpose of method chaining */ - public Builder flat() { + public Builder flat() { return coordinateSystem(CoordinateSystem.FLAT); } @@ -121,7 +117,7 @@ public Builder flat() { * * @return the instance of the {@link Builder} for the purpose of method chaining */ - public Builder spherical() { + public Builder spherical() { return coordinateSystem(CoordinateSystem.SPHERICAL); } diff --git a/src/main/java/io/lettuce/core/search/arguments/HighlightArgs.java b/src/main/java/io/lettuce/core/search/arguments/HighlightArgs.java index 49eb3a45f1..c1e500bf04 100644 --- a/src/main/java/io/lettuce/core/search/arguments/HighlightArgs.java +++ b/src/main/java/io/lettuce/core/search/arguments/HighlightArgs.java @@ -17,28 +17,25 @@ /** * Argument list builder for {@code HIGHLIGHT} clause. * - * @param Key type. - * @param Value type. * @see Highlighting * @since 6.8 * @author Tihomir Mateev */ @SuppressWarnings("OptionalUsedAsFieldOrParameterType") -public class HighlightArgs { +public class HighlightArgs { - private final List fields = new ArrayList<>(); + private final List fields = new ArrayList<>(); - private Optional> tags = Optional.empty(); + private Optional tags = Optional.empty(); /** * Used to build a new instance of the {@link HighlightArgs}. * * @return a {@link HighlightArgs.Builder} that provides the option to build up a new instance of the {@link SearchArgs} - * @param the key type */ - public static HighlightArgs.Builder builder() { - return new HighlightArgs.Builder<>(); + public static HighlightArgs.Builder builder() { + return new HighlightArgs.Builder(); } /** @@ -47,12 +44,11 @@ public static HighlightArgs.Builder builder() { * As a final step the {@link HighlightArgs.Builder#build()} method needs to be executed to create the final * {@link SortByArgs} instance. * - * @param the key type * @see FT.CREATE */ - public static class Builder { + public static class Builder { - private final HighlightArgs highlightArgs = new HighlightArgs<>(); + private final HighlightArgs highlightArgs = new HighlightArgs(); /** * Add a field to highlight. If no FIELDS directive is passed, then all returned fields are highlighted. @@ -60,7 +56,7 @@ public static class Builder { * @param field the field to summarize * @return the instance of the current {@link HighlightArgs.Builder} for the purpose of method chaining */ - public HighlightArgs.Builder field(K field) { + public HighlightArgs.Builder field(String field) { highlightArgs.fields.add(field); return this; } @@ -73,8 +69,8 @@ public HighlightArgs.Builder field(K field) { * @param endTag the string is appended to each matched term * @return the instance of the current {@link HighlightArgs.Builder} for the purpose of method chaining */ - public HighlightArgs.Builder tags(V startTag, V endTag) { - highlightArgs.tags = Optional.of(new Tags<>(startTag, endTag)); + public HighlightArgs.Builder tags(String startTag, String endTag) { + highlightArgs.tags = Optional.of(new Tags(startTag, endTag)); return this; } @@ -83,7 +79,7 @@ public HighlightArgs.Builder tags(V startTag, V endTag) { * * @return the {@link HighlightArgs} */ - public HighlightArgs build() { + public HighlightArgs build() { return highlightArgs; } @@ -94,30 +90,30 @@ public HighlightArgs build() { * * @param args the {@link CommandArgs} object */ - public void build(CommandArgs args) { + public void build(CommandArgs args) { args.add(CommandKeyword.HIGHLIGHT); if (!fields.isEmpty()) { args.add(CommandKeyword.FIELDS); args.add(fields.size()); - args.addKeys(fields); + fields.forEach(args::add); } tags.ifPresent(tags -> { args.add(CommandKeyword.TAGS); - args.addValue(tags.startTag); - args.addValue(tags.endTag); + args.add(tags.startTag); + args.add(tags.endTag); }); } - static class Tags { + static class Tags { - private final V startTag; + private final String startTag; - private final V endTag; + private final String endTag; - Tags(V startTag, V endTag) { + Tags(String startTag, String endTag) { this.startTag = startTag; this.endTag = endTag; } diff --git a/src/main/java/io/lettuce/core/search/arguments/NumericFieldArgs.java b/src/main/java/io/lettuce/core/search/arguments/NumericFieldArgs.java index 5b5ee2d347..e20ba32948 100644 --- a/src/main/java/io/lettuce/core/search/arguments/NumericFieldArgs.java +++ b/src/main/java/io/lettuce/core/search/arguments/NumericFieldArgs.java @@ -17,23 +17,21 @@ * For example, you can search for documents with a price between a certain range or retrieve documents with a specific rating * value. * - * @param Key type * @see Numeric * Fields * @since 6.8 * @author Tihomir Mateev */ -public class NumericFieldArgs extends FieldArgs { +public class NumericFieldArgs extends FieldArgs { /** * Create a new {@link NumericFieldArgs} using the builder pattern. - * - * @param Key type + * * @return a new {@link Builder} */ - public static Builder builder() { - return new Builder<>(); + public static Builder builder() { + return new Builder(); } @Override @@ -42,19 +40,17 @@ public String getFieldType() { } @Override - protected void buildTypeSpecificArgs(CommandArgs args) { + protected void buildTypeSpecificArgs(CommandArgs args) { // Numeric fields have no type-specific arguments beyond the common ones } /** * Builder for {@link NumericFieldArgs}. - * - * @param Key type */ - public static class Builder extends FieldArgs.Builder, Builder> { + public static class Builder extends FieldArgs.Builder { public Builder() { - super(new NumericFieldArgs<>()); + super(new NumericFieldArgs()); } } diff --git a/src/main/java/io/lettuce/core/search/arguments/QueryDialects.java b/src/main/java/io/lettuce/core/search/arguments/QueryDialects.java index 89ff7d00af..b77b345c2e 100644 --- a/src/main/java/io/lettuce/core/search/arguments/QueryDialects.java +++ b/src/main/java/io/lettuce/core/search/arguments/QueryDialects.java @@ -35,7 +35,7 @@ * * { * @code - * SearchArgs args = SearchArgs. builder().dialect(QueryDialects.DIALECT2).build(); + * SearchArgs args = SearchArgs. builder().dialect(QueryDialects.DIALECT2).build(); * } * * diff --git a/src/main/java/io/lettuce/core/search/arguments/SearchArgs.java b/src/main/java/io/lettuce/core/search/arguments/SearchArgs.java index 5bedf3c9e1..be433d6286 100644 --- a/src/main/java/io/lettuce/core/search/arguments/SearchArgs.java +++ b/src/main/java/io/lettuce/core/search/arguments/SearchArgs.java @@ -21,13 +21,12 @@ * Argument list builder for {@code FT.SEARCH}. * * @param Key type. - * @param Value type. * @since 6.8 * @author Tihomir Mateev * @see FT.SEARCH */ @SuppressWarnings("OptionalUsedAsFieldOrParameterType") -public class SearchArgs { +public class SearchArgs { private boolean noContent = false; @@ -39,13 +38,13 @@ public class SearchArgs { private final List inKeys = new ArrayList<>(); - private final List inFields = new ArrayList<>(); + private final List inFields = new ArrayList<>(); - private final Map> returnFields = new HashMap<>(); + private final Map> returnFields = new HashMap<>(); - private Optional> summarize = Optional.empty(); + private Optional summarize = Optional.empty(); - private Optional> highlight = Optional.empty(); + private Optional highlight = Optional.empty(); private Long slop; @@ -53,17 +52,17 @@ public class SearchArgs { private Optional language = Optional.empty(); - private Optional expander = Optional.empty(); + private Optional expander = Optional.empty(); private Optional scorer = Optional.empty(); - private Optional> sortBy = Optional.empty(); + private Optional sortBy = Optional.empty(); private Optional limit = Optional.empty(); private Optional timeout = Optional.empty(); - private final Map params = new HashMap<>(); + private final Map params = new HashMap<>(); private QueryDialects dialect = QueryDialects.DIALECT2; @@ -72,9 +71,8 @@ public class SearchArgs { * * @return a {@link SearchArgs.Builder} that provides the option to build up a new instance of the {@link SearchArgs} * @param the key type - * @param the value type */ - public static SearchArgs.Builder builder() { + public static SearchArgs.Builder builder() { return new SearchArgs.Builder<>(); } @@ -85,23 +83,22 @@ public static SearchArgs.Builder builder() { * instance. * * @param the key type - * @param the value type * @see FT.CREATE */ - public static class Builder { + public static class Builder { - private final SearchArgs instance = new SearchArgs<>(); + private final SearchArgs instance = new SearchArgs<>(); - private SummarizeArgs.Builder summarizeArgs; + private SummarizeArgs.Builder summarizeArgs; - private HighlightArgs.Builder highlightArgs; + private HighlightArgs.Builder highlightArgs; /** * Build a new instance of the {@link SearchArgs}. * * @return a new instance of the {@link SearchArgs} */ - public SearchArgs build() { + public SearchArgs build() { if (!instance.summarize.isPresent() && summarizeArgs != null) { instance.summarize = Optional.of(summarizeArgs.build()); } @@ -119,7 +116,7 @@ public SearchArgs build() { * * @return the instance of the current {@link SearchArgs.Builder} for the purpose of method chaining */ - public SearchArgs.Builder noContent() { + public SearchArgs.Builder noContent() { instance.noContent = true; return this; } @@ -129,7 +126,7 @@ public SearchArgs.Builder noContent() { * * @return the instance of the current {@link SearchArgs.Builder} for the purpose of method chaining */ - public SearchArgs.Builder verbatim() { + public SearchArgs.Builder verbatim() { instance.verbatim = true; return this; } @@ -140,7 +137,7 @@ public SearchArgs.Builder verbatim() { * * @return the instance of the current {@link SearchArgs.Builder} for the purpose of method chaining */ - public SearchArgs.Builder withScores() { + public SearchArgs.Builder withScores() { instance.withScores = true; return this; } @@ -152,7 +149,7 @@ public SearchArgs.Builder withScores() { * * @return the instance of the current {@link SearchArgs.Builder} for the purpose of method chaining */ - public SearchArgs.Builder withSortKeys() { + public SearchArgs.Builder withSortKeys() { instance.withSortKeys = true; return this; } @@ -164,7 +161,7 @@ public SearchArgs.Builder withSortKeys() { * @param key the key to search in * @return the instance of the current {@link SearchArgs.Builder} for the purpose of method chaining */ - public SearchArgs.Builder inKey(K key) { + public SearchArgs.Builder inKey(K key) { instance.inKeys.add(key); return this; } @@ -175,7 +172,7 @@ public SearchArgs.Builder inKey(K key) { * @param field the field to search in * @return the instance of the current {@link SearchArgs.Builder} for the purpose of method chaining */ - public SearchArgs.Builder inField(K field) { + public SearchArgs.Builder inField(String field) { instance.inFields.add(field); return this; } @@ -188,7 +185,7 @@ public SearchArgs.Builder inField(K field) { * @param as the alias to use for this field in the result * @return the instance of the current {@link SearchArgs.Builder} for the purpose of method chaining */ - public SearchArgs.Builder returnField(K field, K as) { + public SearchArgs.Builder returnField(String field, String as) { instance.returnFields.put(field, Optional.ofNullable(as)); return this; } @@ -200,7 +197,7 @@ public SearchArgs.Builder returnField(K field, K as) { * @param field the field to return * @return the instance of the current {@link SearchArgs.Builder} for the purpose of method chaining */ - public SearchArgs.Builder returnField(K field) { + public SearchArgs.Builder returnField(String field) { instance.returnFields.put(field, Optional.empty()); return this; } @@ -213,7 +210,7 @@ public SearchArgs.Builder returnField(K field) { * @see Highlighting */ - public SearchArgs.Builder summarizeArgs(SummarizeArgs summarizeFilter) { + public SearchArgs.Builder summarizeArgs(SummarizeArgs summarizeFilter) { instance.summarize = Optional.ofNullable(summarizeFilter); return this; } @@ -229,9 +226,9 @@ public SearchArgs.Builder summarizeArgs(SummarizeArgs summarizeFilte * @see Highlighting */ - public SearchArgs.Builder summarizeField(K field) { + public SearchArgs.Builder summarizeField(String field) { if (summarizeArgs == null) { - summarizeArgs = new SummarizeArgs.Builder<>(); + summarizeArgs = new SummarizeArgs.Builder(); } summarizeArgs.field(field); @@ -250,9 +247,9 @@ public SearchArgs.Builder summarizeField(K field) { * @see Highlighting */ - public SearchArgs.Builder summarizeLen(long len) { + public SearchArgs.Builder summarizeLen(long len) { if (summarizeArgs == null) { - summarizeArgs = new SummarizeArgs.Builder<>(); + summarizeArgs = new SummarizeArgs.Builder(); } summarizeArgs.len(len); @@ -272,9 +269,9 @@ public SearchArgs.Builder summarizeLen(long len) { * @see Highlighting */ - public SearchArgs.Builder summarizeSeparator(V separator) { + public SearchArgs.Builder summarizeSeparator(String separator) { if (summarizeArgs == null) { - summarizeArgs = new SummarizeArgs.Builder<>(); + summarizeArgs = new SummarizeArgs.Builder(); } summarizeArgs.separator(separator); @@ -292,9 +289,9 @@ public SearchArgs.Builder summarizeSeparator(V separator) { * @see Highlighting */ - public SearchArgs.Builder summarizeFragments(long fragments) { + public SearchArgs.Builder summarizeFragments(long fragments) { if (summarizeArgs == null) { - summarizeArgs = new SummarizeArgs.Builder<>(); + summarizeArgs = new SummarizeArgs.Builder(); } summarizeArgs.fragments(fragments); @@ -310,7 +307,7 @@ public SearchArgs.Builder summarizeFragments(long fragments) { * @see Highlighting */ - public SearchArgs.Builder highlightArgs(HighlightArgs highlightFilter) { + public SearchArgs.Builder highlightArgs(HighlightArgs highlightFilter) { instance.highlight = Optional.ofNullable(highlightFilter); return this; } @@ -325,9 +322,9 @@ public SearchArgs.Builder highlightArgs(HighlightArgs highlightFilte * @see Highlighting */ - public SearchArgs.Builder highlightField(K field) { + public SearchArgs.Builder highlightField(String field) { if (highlightArgs == null) { - highlightArgs = new HighlightArgs.Builder<>(); + highlightArgs = new HighlightArgs.Builder(); } highlightArgs.field(field); @@ -347,9 +344,9 @@ public SearchArgs.Builder highlightField(K field) { * @see Highlighting */ - public SearchArgs.Builder highlightTags(V startTag, V endTag) { + public SearchArgs.Builder highlightTags(String startTag, String endTag) { if (highlightArgs == null) { - highlightArgs = new HighlightArgs.Builder<>(); + highlightArgs = new HighlightArgs.Builder(); } highlightArgs.tags(startTag, endTag); @@ -366,7 +363,7 @@ public SearchArgs.Builder highlightTags(V startTag, V endTag) { * @param slop the slop value how many intermediate terms are allowed * @return the instance of the current {@link SearchArgs.Builder} for the purpose of method chaining */ - public SearchArgs.Builder slop(long slop) { + public SearchArgs.Builder slop(long slop) { instance.slop = slop; return this; } @@ -377,7 +374,7 @@ public SearchArgs.Builder slop(long slop) { * * @return the instance of the current {@link SearchArgs.Builder} for the purpose of method chaining */ - public SearchArgs.Builder inOrder() { + public SearchArgs.Builder inOrder() { instance.inOrder = true; return this; } @@ -391,7 +388,7 @@ public SearchArgs.Builder inOrder() { * @param language the language of the query * @return the instance of the current {@link SearchArgs.Builder} for the purpose of method chaining */ - public SearchArgs.Builder language(DocumentLanguage language) { + public SearchArgs.Builder language(DocumentLanguage language) { instance.language = Optional.ofNullable(language); return this; } @@ -404,7 +401,7 @@ public SearchArgs.Builder language(DocumentLanguage language) { * @see Extensions */ - public SearchArgs.Builder expander(V expander) { + public SearchArgs.Builder expander(String expander) { instance.expander = Optional.ofNullable(expander); return this; } @@ -418,7 +415,7 @@ public SearchArgs.Builder expander(V expander) { * "https://redis.io/docs/latest/develop/interact/search-and-query/administration/extensions/">Extensions * @see Scoring */ - public SearchArgs.Builder scorer(ScoringFunction scorer) { + public SearchArgs.Builder scorer(ScoringFunction scorer) { instance.scorer = Optional.ofNullable(scorer); return this; } @@ -432,7 +429,7 @@ public SearchArgs.Builder scorer(ScoringFunction scorer) { * @param sortBy the {@link SortByArgs} to use * @return the instance of the current {@link SearchArgs.Builder} for the purpose of method chaining */ - public SearchArgs.Builder sortBy(SortByArgs sortBy) { + public SearchArgs.Builder sortBy(SortByArgs sortBy) { instance.sortBy = Optional.ofNullable(sortBy); return this; } @@ -450,7 +447,7 @@ public SearchArgs.Builder sortBy(SortByArgs sortBy) { * @param number the limit to use * @return the instance of the current {@link SearchArgs.Builder} for the purpose of method chaining */ - public SearchArgs.Builder limit(long offset, long number) { + public SearchArgs.Builder limit(long offset, long number) { instance.limit = Optional.of(new Limit(offset, number)); return this; } @@ -461,7 +458,7 @@ public SearchArgs.Builder limit(long offset, long number) { * @param timeout the timeout to use (with millisecond resolution) * @return the instance of the current {@link SearchArgs.Builder} for the purpose of method chaining */ - public SearchArgs.Builder timeout(Duration timeout) { + public SearchArgs.Builder timeout(Duration timeout) { instance.timeout = Optional.ofNullable(timeout); return this; } @@ -475,7 +472,21 @@ public SearchArgs.Builder timeout(Duration timeout) { * @param value the value of the parameter * @return the instance of the current {@link SearchArgs.Builder} for the purpose of method chaining */ - public SearchArgs.Builder param(K name, V value) { + public SearchArgs.Builder param(String name, String value) { + instance.params.put(name, value); + return this; + } + + /** + * Add a binary value parameter, for example a vector blob for a KNN query ({@code $BLOB}). + *

    + * Requires {@link QueryDialects#DIALECT2} or higher. + * + * @param name the name of the parameter + * @param value the binary value of the parameter + * @return the instance of the current {@link SearchArgs.Builder} for the purpose of method chaining + */ + public SearchArgs.Builder param(String name, byte[] value) { instance.params.put(name, value); return this; } @@ -487,7 +498,7 @@ public SearchArgs.Builder param(K name, V value) { * @return the instance of the current {@link SearchArgs.Builder} for the purpose of method chaining * @see QueryDialects */ - public SearchArgs.Builder dialect(QueryDialects dialect) { + public SearchArgs.Builder dialect(QueryDialects dialect) { instance.dialect = dialect; return this; } @@ -526,7 +537,7 @@ public boolean isWithSortKeys() { * * @param args the {@link CommandArgs} object */ - public void build(CommandArgs args) { + public void build(CommandArgs args) { if (noContent) { args.add(CommandKeyword.NOCONTENT); @@ -553,7 +564,7 @@ public void build(CommandArgs args) { if (!inFields.isEmpty()) { args.add(CommandKeyword.INFIELDS); args.add(inFields.size()); - args.addKeys(inFields); + inFields.forEach(args::add); } if (!returnFields.isEmpty()) { @@ -566,10 +577,10 @@ public void build(CommandArgs args) { args.add(count); returnFields.forEach((field, as) -> { - args.addKey(field); + args.add(field); if (as.isPresent()) { args.add(CommandKeyword.AS); - args.addKey(as.get()); + args.add(as.get()); } }); } @@ -598,7 +609,7 @@ public void build(CommandArgs args) { expander.ifPresent(v -> { args.add(CommandKeyword.EXPANDER); - args.addValue(v); + args.add(v); }); scorer.ifPresent(scoringFunction -> { @@ -618,8 +629,12 @@ public void build(CommandArgs args) { args.add(CommandKeyword.PARAMS); args.add(params.size() * 2L); params.forEach((name, value) -> { - args.addKey(name); - args.addValue(value); + args.add(name); + if (value instanceof byte[]) { + args.add((byte[]) value); + } else { + args.add((String) value); + } }); } diff --git a/src/main/java/io/lettuce/core/search/arguments/SortByArgs.java b/src/main/java/io/lettuce/core/search/arguments/SortByArgs.java index 23605e1a3f..8ef9c59dcb 100644 --- a/src/main/java/io/lettuce/core/search/arguments/SortByArgs.java +++ b/src/main/java/io/lettuce/core/search/arguments/SortByArgs.java @@ -13,14 +13,13 @@ /** * Argument list builder for {@code SORTBY} clause. * - * @param Key type. * @see Sorting * @since 6.8 * @author Tihomir Mateev */ -public class SortByArgs { +public class SortByArgs { - private K attribute; + private String attribute; private boolean isDescending; @@ -30,10 +29,9 @@ public class SortByArgs { * Used to build a new instance of the {@link SortByArgs}. * * @return a {@link SortByArgs.Builder} that provides the option to build up a new instance of the {@link SearchArgs} - * @param the key type */ - public static SortByArgs.Builder builder() { - return new SortByArgs.Builder<>(); + public static SortByArgs.Builder builder() { + return new SortByArgs.Builder(); } /** @@ -42,12 +40,11 @@ public static SortByArgs.Builder builder() { * As a final step the {@link SortByArgs.Builder#build()} method needs to be executed to create the final {@link SortByArgs} * instance. * - * @param the key type * @see FT.CREATE */ - public static class Builder { + public static class Builder { - private final SortByArgs sortByArgs = new SortByArgs<>(); + private final SortByArgs sortByArgs = new SortByArgs(); /** * Add an attribute to sort by. @@ -55,7 +52,7 @@ public static class Builder { * @param attribute the attribute to sort by * @return the instance of the current {@link SortByArgs.Builder} for the purpose of method chaining */ - public SortByArgs.Builder attribute(K attribute) { + public SortByArgs.Builder attribute(String attribute) { sortByArgs.attribute = attribute; return this; } @@ -65,7 +62,7 @@ public SortByArgs.Builder attribute(K attribute) { * * @return the instance of the current {@link SortByArgs.Builder} for the purpose of method chaining */ - public SortByArgs.Builder descending() { + public SortByArgs.Builder descending() { sortByArgs.isDescending = true; return this; } @@ -75,7 +72,7 @@ public SortByArgs.Builder descending() { * * @return the instance of the current {@link SortByArgs.Builder} for the purpose of method chaining */ - public SortByArgs.Builder withCount() { + public SortByArgs.Builder withCount() { sortByArgs.withCount = true; return this; } @@ -85,7 +82,7 @@ public SortByArgs.Builder withCount() { * * @return the {@link SortByArgs} */ - public SortByArgs build() { + public SortByArgs build() { return sortByArgs; } @@ -96,8 +93,8 @@ public SortByArgs build() { * * @param args the {@link CommandArgs} object */ - public void build(CommandArgs args) { - args.add(CommandKeyword.SORTBY).addKey(attribute); + public void build(CommandArgs args) { + args.add(CommandKeyword.SORTBY).add(attribute); if (this.isDescending) { args.add(CommandKeyword.DESC); diff --git a/src/main/java/io/lettuce/core/search/arguments/SpellCheckArgs.java b/src/main/java/io/lettuce/core/search/arguments/SpellCheckArgs.java index 3619285fb8..41a260b3ef 100644 --- a/src/main/java/io/lettuce/core/search/arguments/SpellCheckArgs.java +++ b/src/main/java/io/lettuce/core/search/arguments/SpellCheckArgs.java @@ -18,18 +18,16 @@ *

    * {@link SpellCheckArgs} is a mutable object and instances should be used only once to avoid shared mutable state. * - * @param Key type. - * @param Value type. * @author Tihomir Mateev * @since 6.8 */ -public class SpellCheckArgs { +public class SpellCheckArgs { private Long distance; private Long dialect; - private final List> termsClauses = new ArrayList<>(); + private final List termsClauses = new ArrayList<>(); /** * Builder entry points for {@link SpellCheckArgs}. @@ -48,8 +46,8 @@ private Builder() { * @return new {@link SpellCheckArgs} with {@literal DISTANCE} set. * @see SpellCheckArgs#distance(long) */ - public static SpellCheckArgs distance(long distance) { - return new SpellCheckArgs().distance(distance); + public static SpellCheckArgs distance(long distance) { + return new SpellCheckArgs().distance(distance); } /** @@ -58,30 +56,28 @@ public static SpellCheckArgs distance(long distance) { * @return new {@link SpellCheckArgs} with {@literal DIALECT} set. * @see SpellCheckArgs#dialect(long) */ - public static SpellCheckArgs dialect(long dialect) { - return new SpellCheckArgs().dialect(dialect); + public static SpellCheckArgs dialect(long dialect) { + return new SpellCheckArgs().dialect(dialect); } /** * Creates new {@link SpellCheckArgs} setting {@literal TERMS INCLUDE}. * * @return new {@link SpellCheckArgs} with {@literal TERMS INCLUDE} set. - * @see SpellCheckArgs#termsInclude(Object, Object[]) + * @see SpellCheckArgs#termsInclude(String, String...) */ - @SafeVarargs - public static SpellCheckArgs termsInclude(K dictionary, V... terms) { - return new SpellCheckArgs().termsInclude(dictionary, terms); + public static SpellCheckArgs termsInclude(String dictionary, String... terms) { + return new SpellCheckArgs().termsInclude(dictionary, terms); } /** * Creates new {@link SpellCheckArgs} setting {@literal TERMS EXCLUDE}. * * @return new {@link SpellCheckArgs} with {@literal TERMS EXCLUDE} set. - * @see SpellCheckArgs#termsExclude(Object, Object[]) + * @see SpellCheckArgs#termsExclude(String, String...) */ - @SafeVarargs - public static SpellCheckArgs termsExclude(K dictionary, V... terms) { - return new SpellCheckArgs().termsExclude(dictionary, terms); + public static SpellCheckArgs termsExclude(String dictionary, String... terms) { + return new SpellCheckArgs().termsExclude(dictionary, terms); } } @@ -92,7 +88,7 @@ public static SpellCheckArgs termsExclude(K dictionary, V... terms) * @param distance the maximum distance. * @return {@code this} {@link SpellCheckArgs}. */ - public SpellCheckArgs distance(long distance) { + public SpellCheckArgs distance(long distance) { this.distance = distance; return this; } @@ -103,7 +99,7 @@ public SpellCheckArgs distance(long distance) { * @param dialect the dialect version. * @return {@code this} {@link SpellCheckArgs}. */ - public SpellCheckArgs dialect(long dialect) { + public SpellCheckArgs dialect(long dialect) { this.dialect = dialect; return this; } @@ -115,9 +111,8 @@ public SpellCheckArgs dialect(long dialect) { * @param terms optional terms to include from the dictionary. * @return {@code this} {@link SpellCheckArgs}. */ - @SafeVarargs - public final SpellCheckArgs termsInclude(K dictionary, V... terms) { - this.termsClauses.add(new TermsClause<>(TermsClause.Type.INCLUDE, dictionary, terms)); + public SpellCheckArgs termsInclude(String dictionary, String... terms) { + this.termsClauses.add(new TermsClause(TermsClause.Type.INCLUDE, dictionary, terms)); return this; } @@ -128,9 +123,8 @@ public final SpellCheckArgs termsInclude(K dictionary, V... terms) { * @param terms optional terms to exclude from the dictionary. * @return {@code this} {@link SpellCheckArgs}. */ - @SafeVarargs - public final SpellCheckArgs termsExclude(K dictionary, V... terms) { - this.termsClauses.add(new TermsClause<>(TermsClause.Type.EXCLUDE, dictionary, terms)); + public SpellCheckArgs termsExclude(String dictionary, String... terms) { + this.termsClauses.add(new TermsClause(TermsClause.Type.EXCLUDE, dictionary, terms)); return this; } @@ -139,12 +133,12 @@ public final SpellCheckArgs termsExclude(K dictionary, V... terms) { * * @param args the command arguments to append to. */ - public void build(CommandArgs args) { + public void build(CommandArgs args) { if (distance != null) { args.add(CommandKeyword.DISTANCE).add(distance); } - for (TermsClause clause : termsClauses) { + for (TermsClause clause : termsClauses) { clause.build(args); } @@ -156,7 +150,7 @@ public void build(CommandArgs args) { /** * Represents a TERMS clause (INCLUDE or EXCLUDE). */ - private static class TermsClause { + private static class TermsClause { enum Type { INCLUDE, EXCLUDE @@ -164,22 +158,21 @@ enum Type { private final Type type; - private final K dictionary; + private final String dictionary; - private final V[] terms; + private final String[] terms; - @SafeVarargs - TermsClause(Type type, K dictionary, V... terms) { + TermsClause(Type type, String dictionary, String... terms) { this.type = type; this.dictionary = dictionary; this.terms = terms; } - void build(CommandArgs args) { - args.add(CommandKeyword.TERMS).add(type.name()).addKey(dictionary); + void build(CommandArgs args) { + args.add(CommandKeyword.TERMS).add(type.name()).add(dictionary); if (terms != null) { - for (V term : terms) { - args.addValue(term); + for (String term : terms) { + args.add(term); } } } diff --git a/src/main/java/io/lettuce/core/search/arguments/SugAddArgs.java b/src/main/java/io/lettuce/core/search/arguments/SugAddArgs.java index 7ef59aa16e..29c68eed55 100644 --- a/src/main/java/io/lettuce/core/search/arguments/SugAddArgs.java +++ b/src/main/java/io/lettuce/core/search/arguments/SugAddArgs.java @@ -15,16 +15,14 @@ * FT.SUGADD command adds a suggestion string to an auto-complete suggestion dictionary with a specified score. *

    * - * @param Key type. - * @param Value type. * @author Tihomir Mateev * @since 6.8 */ -public class SugAddArgs { +public class SugAddArgs { private boolean incr; - private V payload; + private String payload; /** * Builder entry points for {@link SugAddArgs}. @@ -43,8 +41,8 @@ private Builder() { * @return new {@link SugAddArgs} with {@literal INCR} set. * @see SugAddArgs#incr() */ - public static SugAddArgs incr() { - return new SugAddArgs().incr(); + public static SugAddArgs incr() { + return new SugAddArgs().incr(); } /** @@ -52,10 +50,10 @@ public static SugAddArgs incr() { * * @param payload the payload to save with the suggestion. * @return new {@link SugAddArgs} with {@literal PAYLOAD} set. - * @see SugAddArgs#payload(Object) + * @see SugAddArgs#payload(String) */ - public static SugAddArgs payload(V payload) { - return new SugAddArgs().payload(payload); + public static SugAddArgs payload(String payload) { + return new SugAddArgs().payload(payload); } } @@ -66,7 +64,7 @@ public static SugAddArgs payload(V payload) { * * @return {@code this} {@link SugAddArgs}. */ - public SugAddArgs incr() { + public SugAddArgs incr() { this.incr = true; return this; } @@ -77,7 +75,7 @@ public SugAddArgs incr() { * @param payload the payload to save with the suggestion. * @return {@code this} {@link SugAddArgs}. */ - public SugAddArgs payload(V payload) { + public SugAddArgs payload(String payload) { this.payload = payload; return this; } @@ -87,13 +85,13 @@ public SugAddArgs payload(V payload) { * * @param args the command arguments to append to. */ - public void build(CommandArgs args) { + public void build(CommandArgs args) { if (incr) { args.add("INCR"); } if (payload != null) { - args.add("PAYLOAD").addValue(payload); + args.add("PAYLOAD").add(payload); } } diff --git a/src/main/java/io/lettuce/core/search/arguments/SugGetArgs.java b/src/main/java/io/lettuce/core/search/arguments/SugGetArgs.java index a4e08ef2f4..7647b627d3 100644 --- a/src/main/java/io/lettuce/core/search/arguments/SugGetArgs.java +++ b/src/main/java/io/lettuce/core/search/arguments/SugGetArgs.java @@ -15,12 +15,10 @@ * dictionary. The FT.SUGGET command retrieves completion suggestions for a prefix from an auto-complete suggestion dictionary. *

    * - * @param Key type. - * @param Value type. * @author Tihomir Mateev * @since 6.8 */ -public class SugGetArgs { +public class SugGetArgs { private boolean fuzzy; @@ -47,8 +45,8 @@ private Builder() { * @return new {@link SugGetArgs} with {@literal FUZZY} set. * @see SugGetArgs#fuzzy() */ - public static SugGetArgs fuzzy() { - return new SugGetArgs().fuzzy(); + public static SugGetArgs fuzzy() { + return new SugGetArgs().fuzzy(); } /** @@ -57,8 +55,8 @@ public static SugGetArgs fuzzy() { * @return new {@link SugGetArgs} with {@literal WITHSCORES} set. * @see SugGetArgs#withScores() */ - public static SugGetArgs withScores() { - return new SugGetArgs().withScores(); + public static SugGetArgs withScores() { + return new SugGetArgs().withScores(); } /** @@ -67,8 +65,8 @@ public static SugGetArgs withScores() { * @return new {@link SugGetArgs} with {@literal WITHPAYLOADS} set. * @see SugGetArgs#withPayloads() */ - public static SugGetArgs withPayloads() { - return new SugGetArgs().withPayloads(); + public static SugGetArgs withPayloads() { + return new SugGetArgs().withPayloads(); } /** @@ -78,8 +76,8 @@ public static SugGetArgs withPayloads() { * @return new {@link SugGetArgs} with {@literal MAX} set. * @see SugGetArgs#max(long) */ - public static SugGetArgs max(long max) { - return new SugGetArgs().max(max); + public static SugGetArgs max(long max) { + return new SugGetArgs().max(max); } } @@ -89,7 +87,7 @@ public static SugGetArgs max(long max) { * * @return {@code this} {@link SugGetArgs}. */ - public SugGetArgs fuzzy() { + public SugGetArgs fuzzy() { this.fuzzy = true; return this; } @@ -99,7 +97,7 @@ public SugGetArgs fuzzy() { * * @return {@code this} {@link SugGetArgs}. */ - public SugGetArgs withScores() { + public SugGetArgs withScores() { this.withScores = true; return this; } @@ -110,7 +108,7 @@ public SugGetArgs withScores() { * * @return {@code this} {@link SugGetArgs}. */ - public SugGetArgs withPayloads() { + public SugGetArgs withPayloads() { this.withPayloads = true; return this; } @@ -121,7 +119,7 @@ public SugGetArgs withPayloads() { * @param max the maximum number of suggestions to return. * @return {@code this} {@link SugGetArgs}. */ - public SugGetArgs max(long max) { + public SugGetArgs max(long max) { this.max = max; return this; } @@ -149,7 +147,7 @@ public boolean isWithPayloads() { * * @param args the command arguments to append to. */ - public void build(CommandArgs args) { + public void build(CommandArgs args) { if (fuzzy) { args.add("FUZZY"); } diff --git a/src/main/java/io/lettuce/core/search/arguments/SummarizeArgs.java b/src/main/java/io/lettuce/core/search/arguments/SummarizeArgs.java index 09632f68db..b948aa1dc1 100644 --- a/src/main/java/io/lettuce/core/search/arguments/SummarizeArgs.java +++ b/src/main/java/io/lettuce/core/search/arguments/SummarizeArgs.java @@ -18,31 +18,29 @@ /** * Argument list builder for {@code SUMMARIZE} clause. * - * @param Key type. - * @param Value type. - * @see Highlighing + * @see Summarization * @since 6.8 * @author Tihomir Mateev */ @SuppressWarnings("OptionalUsedAsFieldOrParameterType") -public class SummarizeArgs { +public class SummarizeArgs { - private final List fields = new ArrayList<>(); + private final List fields = new ArrayList<>(); private Optional frags = Optional.empty(); private Optional len = Optional.empty(); - private Optional separator = Optional.empty(); + private Optional separator = Optional.empty(); /** * Used to build a new instance of the {@link SummarizeArgs}. * * @return a {@link SummarizeArgs.Builder} that provides the option to build up a new instance of the {@link SearchArgs} - * @param the key type */ - public static SummarizeArgs.Builder builder() { - return new SummarizeArgs.Builder<>(); + public static SummarizeArgs.Builder builder() { + return new SummarizeArgs.Builder(); } /** @@ -51,12 +49,11 @@ public static SummarizeArgs.Builder builder() { * As a final step the {@link SummarizeArgs.Builder#build()} method needs to be executed to create the final * {@link SortByArgs} instance. * - * @param the key type * @see FT.CREATE */ - public static class Builder { + public static class Builder { - private final SummarizeArgs summarizeArgs = new SummarizeArgs<>(); + private final SummarizeArgs summarizeArgs = new SummarizeArgs(); /** * Add a field to summarize. Each field is summarized. If no FIELDS directive is passed, then all returned fields are @@ -65,7 +62,7 @@ public static class Builder { * @param field the field to summarize * @return the instance of the current {@link SummarizeArgs.Builder} for the purpose of method chaining */ - public SummarizeArgs.Builder field(K field) { + public SummarizeArgs.Builder field(String field) { summarizeArgs.fields.add(field); return this; } @@ -76,7 +73,7 @@ public SummarizeArgs.Builder field(K field) { * @param frags the number of fragments to return * @return the instance of the current {@link SummarizeArgs.Builder} for the purpose of method chaining */ - public SummarizeArgs.Builder fragments(long frags) { + public SummarizeArgs.Builder fragments(long frags) { summarizeArgs.frags = Optional.of(frags); return this; } @@ -89,7 +86,7 @@ public SummarizeArgs.Builder fragments(long frags) { * @return the instance of the current {@link SummarizeArgs.Builder} for the purpose of method chaining */ - public SummarizeArgs.Builder len(long len) { + public SummarizeArgs.Builder len(long len) { summarizeArgs.len = Optional.of(len); return this; } @@ -102,7 +99,7 @@ public SummarizeArgs.Builder len(long len) { * @param separator the separator between fragments * @return the instance of the current {@link SummarizeArgs.Builder} for the purpose of method chaining */ - public SummarizeArgs.Builder separator(V separator) { + public SummarizeArgs.Builder separator(String separator) { summarizeArgs.separator = Optional.of(separator); return this; } @@ -112,7 +109,7 @@ public SummarizeArgs.Builder separator(V separator) { * * @return the {@link SummarizeArgs} */ - public SummarizeArgs build() { + public SummarizeArgs build() { return summarizeArgs; } @@ -123,13 +120,13 @@ public SummarizeArgs build() { * * @param args the {@link CommandArgs} object */ - public void build(CommandArgs args) { + public void build(CommandArgs args) { args.add(CommandKeyword.SUMMARIZE); if (!fields.isEmpty()) { args.add(CommandKeyword.FIELDS); args.add(fields.size()); - args.addKeys(fields); + fields.forEach(args::add); } frags.ifPresent(f -> { @@ -144,7 +141,7 @@ public void build(CommandArgs args) { separator.ifPresent(s -> { args.add(CommandKeyword.SEPARATOR); - args.addValue(s); + args.add(s); }); } diff --git a/src/main/java/io/lettuce/core/search/arguments/SynUpdateArgs.java b/src/main/java/io/lettuce/core/search/arguments/SynUpdateArgs.java index 1dd569efda..959d1bb2cd 100644 --- a/src/main/java/io/lettuce/core/search/arguments/SynUpdateArgs.java +++ b/src/main/java/io/lettuce/core/search/arguments/SynUpdateArgs.java @@ -14,12 +14,10 @@ *

    * {@link SynUpdateArgs} is a mutable object and instances should be used only once to avoid shared mutable state. * - * @param Key type. - * @param Value type. * @author Tihomir Mateev * @since 6.8 */ -public class SynUpdateArgs { +public class SynUpdateArgs { private boolean skipInitialScan = false; @@ -40,8 +38,8 @@ private Builder() { * @return new {@link SynUpdateArgs} with {@literal SKIPINITIALSCAN} set. * @see SynUpdateArgs#skipInitialScan() */ - public static SynUpdateArgs skipInitialScan() { - return new SynUpdateArgs().skipInitialScan(); + public static SynUpdateArgs skipInitialScan() { + return new SynUpdateArgs().skipInitialScan(); } } @@ -52,7 +50,7 @@ public static SynUpdateArgs skipInitialScan() { * * @return {@code this} {@link SynUpdateArgs}. */ - public SynUpdateArgs skipInitialScan() { + public SynUpdateArgs skipInitialScan() { this.skipInitialScan = true; return this; } @@ -62,7 +60,7 @@ public SynUpdateArgs skipInitialScan() { * * @param args the command arguments to append to. */ - public void build(CommandArgs args) { + public void build(CommandArgs args) { if (skipInitialScan) { args.add("SKIPINITIALSCAN"); } diff --git a/src/main/java/io/lettuce/core/search/arguments/TagFieldArgs.java b/src/main/java/io/lettuce/core/search/arguments/TagFieldArgs.java index 6c499b5a46..be923651aa 100644 --- a/src/main/java/io/lettuce/core/search/arguments/TagFieldArgs.java +++ b/src/main/java/io/lettuce/core/search/arguments/TagFieldArgs.java @@ -21,7 +21,6 @@ * stored as-is without tokenization or stemming. They are useful for organizing and categorizing data, making it easier to * filter and retrieve documents based on specific tags. * - * @param Key type * @see Tag * Fields @@ -29,7 +28,7 @@ * @author Tihomir Mateev */ @SuppressWarnings("OptionalUsedAsFieldOrParameterType") -public class TagFieldArgs extends FieldArgs { +public class TagFieldArgs extends FieldArgs { private Optional separator = Optional.empty(); @@ -40,11 +39,10 @@ public class TagFieldArgs extends FieldArgs { /** * Create a new {@link TagFieldArgs} using the builder pattern. * - * @param Key type * @return a new {@link Builder} */ - public static Builder builder() { - return new Builder<>(); + public static Builder builder() { + return new Builder(); } @Override @@ -80,7 +78,7 @@ public boolean isWithSuffixTrie() { } @Override - protected void buildTypeSpecificArgs(CommandArgs args) { + protected void buildTypeSpecificArgs(CommandArgs args) { separator.ifPresent(s -> args.add(SEPARATOR).add(s)); if (caseSensitive) { args.add(CASESENSITIVE); @@ -93,12 +91,11 @@ protected void buildTypeSpecificArgs(CommandArgs args) { /** * Builder for {@link TagFieldArgs}. * - * @param Key type */ - public static class Builder extends FieldArgs.Builder, Builder> { + public static class Builder extends FieldArgs.Builder { public Builder() { - super(new TagFieldArgs<>()); + super(new TagFieldArgs()); } /** @@ -107,7 +104,7 @@ public Builder() { * @param separator the separator for tag fields * @return the instance of the {@link Builder} for the purpose of method chaining */ - public Builder separator(String separator) { + public Builder separator(String separator) { instance.separator = Optional.of(separator); return self(); } @@ -118,7 +115,7 @@ public Builder separator(String separator) { * * @return the instance of the {@link Builder} for the purpose of method chaining */ - public Builder caseSensitive() { + public Builder caseSensitive() { instance.caseSensitive = true; return self(); } @@ -130,7 +127,7 @@ public Builder caseSensitive() { * * @return the instance of the {@link Builder} for the purpose of method chaining */ - public Builder withSuffixTrie() { + public Builder withSuffixTrie() { instance.withSuffixTrie = true; return self(); } diff --git a/src/main/java/io/lettuce/core/search/arguments/TextFieldArgs.java b/src/main/java/io/lettuce/core/search/arguments/TextFieldArgs.java index bbf0c6fbed..5490a299ab 100644 --- a/src/main/java/io/lettuce/core/search/arguments/TextFieldArgs.java +++ b/src/main/java/io/lettuce/core/search/arguments/TextFieldArgs.java @@ -21,7 +21,6 @@ * The data is tokenized, meaning it is split into individual words or tokens, which enables efficient full-text search * functionality. * - * @param Key type * @see Text * Fields @@ -29,7 +28,7 @@ * @author Tihomir Mateev */ @SuppressWarnings("OptionalUsedAsFieldOrParameterType") -public class TextFieldArgs extends FieldArgs { +public class TextFieldArgs extends FieldArgs { /** * Phonetic matchers for text fields. @@ -61,11 +60,10 @@ public String getMatcher() { /** * Create a new {@link TextFieldArgs} using the builder pattern. * - * @param Key type * @return a new {@link Builder} */ - public static Builder builder() { - return new Builder<>(); + public static Builder builder() { + return new Builder(); } @Override @@ -110,7 +108,7 @@ public boolean isWithSuffixTrie() { } @Override - protected void buildTypeSpecificArgs(CommandArgs args) { + protected void buildTypeSpecificArgs(CommandArgs args) { weight.ifPresent(w -> args.add(WEIGHT).add(w)); if (noStem) { args.add(NOSTEM); @@ -124,12 +122,11 @@ protected void buildTypeSpecificArgs(CommandArgs args) { /** * Builder for {@link TextFieldArgs}. * - * @param Key type */ - public static class Builder extends FieldArgs.Builder, Builder> { + public static class Builder extends FieldArgs.Builder { public Builder() { - super(new TextFieldArgs<>()); + super(new TextFieldArgs()); } /** @@ -139,7 +136,7 @@ public Builder() { * @param weight the weight of the field * @return the instance of the {@link Builder} for the purpose of method chaining */ - public Builder weight(long weight) { + public Builder weight(long weight) { instance.weight = Optional.of(weight); return self(); } @@ -150,7 +147,7 @@ public Builder weight(long weight) { * * @return the instance of the {@link Builder} for the purpose of method chaining */ - public Builder noStem() { + public Builder noStem() { instance.noStem = true; return self(); } @@ -174,7 +171,7 @@ public Builder noStem() { * @param matcher the phonetic matcher * @return the instance of the {@link Builder} for the purpose of method chaining */ - public Builder phonetic(PhoneticMatcher matcher) { + public Builder phonetic(PhoneticMatcher matcher) { instance.phonetic = Optional.of(matcher); return self(); } @@ -186,7 +183,7 @@ public Builder phonetic(PhoneticMatcher matcher) { * * @return the instance of the {@link Builder} for the purpose of method chaining */ - public Builder withSuffixTrie() { + public Builder withSuffixTrie() { instance.withSuffixTrie = true; return self(); } diff --git a/src/main/java/io/lettuce/core/search/arguments/VectorFieldArgs.java b/src/main/java/io/lettuce/core/search/arguments/VectorFieldArgs.java index 3c9391e40c..f39954fec1 100644 --- a/src/main/java/io/lettuce/core/search/arguments/VectorFieldArgs.java +++ b/src/main/java/io/lettuce/core/search/arguments/VectorFieldArgs.java @@ -22,7 +22,6 @@ * represent unstructured data such as text, images, or other complex features. Redis allows you to search for similar vectors * using vector search algorithms like cosine similarity, Euclidean distance, and inner product. * - * @param Key type * @see Vector * Fields @@ -30,7 +29,7 @@ * @author Tihomir Mateev */ @SuppressWarnings("OptionalUsedAsFieldOrParameterType") -public class VectorFieldArgs extends FieldArgs { +public class VectorFieldArgs extends FieldArgs { /** * Vector similarity index algorithms. @@ -138,11 +137,10 @@ public enum DistanceMetric { /** * Create a new {@link VectorFieldArgs} using the builder pattern. * - * @param Key type * @return a new {@link Builder} */ - public static Builder builder() { - return new Builder<>(); + public static Builder builder() { + return new Builder(); } @Override @@ -169,7 +167,7 @@ public Map getAttributes() { } @Override - protected void buildTypeSpecificArgs(CommandArgs args) { + protected void buildTypeSpecificArgs(CommandArgs args) { algorithm.ifPresent(alg -> args.add(alg.toString())); if (!attributes.isEmpty()) { @@ -184,12 +182,11 @@ protected void buildTypeSpecificArgs(CommandArgs args) { /** * Builder for {@link VectorFieldArgs}. * - * @param Key type */ - public static class Builder extends FieldArgs.Builder, Builder> { + public static class Builder extends FieldArgs.Builder { public Builder() { - super(new VectorFieldArgs<>()); + super(new VectorFieldArgs()); } /** @@ -198,7 +195,7 @@ public Builder() { * @param algorithm the algorithm * @return the instance of the {@link Builder} for the purpose of method chaining */ - public Builder algorithm(Algorithm algorithm) { + public Builder algorithm(Algorithm algorithm) { instance.algorithm = Optional.of(algorithm); return self(); } @@ -208,7 +205,7 @@ public Builder algorithm(Algorithm algorithm) { * * @return the instance of the {@link Builder} for the purpose of method chaining */ - public Builder flat() { + public Builder flat() { return algorithm(Algorithm.FLAT); } @@ -217,7 +214,7 @@ public Builder flat() { * * @return the instance of the {@link Builder} for the purpose of method chaining */ - public Builder hnsw() { + public Builder hnsw() { return algorithm(Algorithm.HNSW); } @@ -236,7 +233,7 @@ public Builder hnsw() { * @return the instance of the {@link Builder} for the purpose of method chaining * @since Redis 8.2 */ - public Builder svsVamana() { + public Builder svsVamana() { return algorithm(Algorithm.SVS_VAMANA); } @@ -246,7 +243,7 @@ public Builder svsVamana() { * @param type the vector data type * @return the instance of the {@link Builder} for the purpose of method chaining */ - public Builder type(VectorType type) { + public Builder type(VectorType type) { instance.attributes.put(TYPE.toString(), type.toString()); return self(); } @@ -257,7 +254,7 @@ public Builder type(VectorType type) { * @param dimensions the number of dimensions * @return the instance of the {@link Builder} for the purpose of method chaining */ - public Builder dimensions(int dimensions) { + public Builder dimensions(int dimensions) { instance.attributes.put(DIM.toString(), dimensions); return self(); } @@ -268,7 +265,7 @@ public Builder dimensions(int dimensions) { * @param metric the distance metric * @return the instance of the {@link Builder} for the purpose of method chaining */ - public Builder distanceMetric(DistanceMetric metric) { + public Builder distanceMetric(DistanceMetric metric) { instance.attributes.put(DISTANCE_METRIC.toString(), metric.toString()); return self(); } @@ -280,7 +277,7 @@ public Builder distanceMetric(DistanceMetric metric) { * @param value the attribute value * @return the instance of the {@link Builder} for the purpose of method chaining */ - public Builder attribute(String name, Object value) { + public Builder attribute(String name, Object value) { instance.attributes.put(name, value); return self(); } diff --git a/src/main/java/io/lettuce/core/search/arguments/hybrid/Combiner.java b/src/main/java/io/lettuce/core/search/arguments/hybrid/Combiner.java index f32c04b7ba..64726ca41c 100644 --- a/src/main/java/io/lettuce/core/search/arguments/hybrid/Combiner.java +++ b/src/main/java/io/lettuce/core/search/arguments/hybrid/Combiner.java @@ -37,7 +37,6 @@ * } * * - * @param Key type * @author Aleksandar Todorov * @author apoorva-01 * @since 7.5 @@ -45,11 +44,11 @@ * @see FT.HYBRID */ @Experimental -public abstract class Combiner { +public abstract class Combiner { private final String name; - private K scoreAlias; + private String scoreAlias; /** * Creates a new combiner with the specified name. @@ -75,8 +74,7 @@ public final String getName() { * @param alias the field name to use for the combined score * @return this instance */ - @SuppressWarnings("unchecked") - public final > T as(K alias) { + public final T as(String alias) { this.scoreAlias = alias; return (T) this; } @@ -92,9 +90,8 @@ public final > T as(K alias) { * Build the combiner arguments into the command. * * @param args the {@link CommandArgs} to append to - * @param value type */ - public final void build(CommandArgs args) { + public final void build(CommandArgs args) { args.add(name); List ownArgs = getOwnArgs(); @@ -106,7 +103,7 @@ public final void build(CommandArgs args) { if (scoreAlias != null) { args.add(CommandKeyword.YIELD_SCORE_AS); - args.addKey(scoreAlias); + args.add(scoreAlias); } } diff --git a/src/main/java/io/lettuce/core/search/arguments/hybrid/Combiners.java b/src/main/java/io/lettuce/core/search/arguments/hybrid/Combiners.java index 02349eff75..ae341ee824 100644 --- a/src/main/java/io/lettuce/core/search/arguments/hybrid/Combiners.java +++ b/src/main/java/io/lettuce/core/search/arguments/hybrid/Combiners.java @@ -49,21 +49,19 @@ private Combiners() { /** * Create an RRF (Reciprocal Rank Fusion) combiner. * - * @param Key type * @return a new RRF combiner */ - public static RRF rrf() { - return new RRF<>(); + public static RRF rrf() { + return new RRF(); } /** * Create a Linear combination combiner. * - * @param Key type * @return a new Linear combiner */ - public static Linear linear() { - return new Linear<>(); + public static Linear linear() { + return new Linear(); } /** @@ -73,9 +71,8 @@ public static Linear linear() { * rank_in_window) *

    * - * @param Key type */ - public static class RRF extends Combiner { + public static class RRF extends Combiner { private Integer window; @@ -91,7 +88,7 @@ public static class RRF extends Combiner { * @param window number of top results * @return this RRF instance */ - public RRF window(int window) { + public RRF window(int window) { LettuceAssert.isTrue(window > 0, "Window must be positive"); this.window = window; return this; @@ -103,7 +100,7 @@ public RRF window(int window) { * @param constant constant value (typically 60) * @return this RRF instance */ - public RRF constant(double constant) { + public RRF constant(double constant) { LettuceAssert.isTrue(constant > 0, "Constant must be positive"); this.constant = constant; return this; @@ -135,9 +132,8 @@ protected List getOwnArgs() { * vector_score *

    * - * @param Key type */ - public static class Linear extends Combiner { + public static class Linear extends Combiner { private Integer window; @@ -155,7 +151,7 @@ public static class Linear extends Combiner { * @param window number of top results * @return this Linear instance */ - public Linear window(int window) { + public Linear window(int window) { LettuceAssert.isTrue(window > 0, "Window must be positive"); this.window = window; return this; @@ -167,7 +163,7 @@ public Linear window(int window) { * @param alpha weight for text score (0.0 to 1.0) * @return this Linear instance */ - public Linear alpha(double alpha) { + public Linear alpha(double alpha) { LettuceAssert.isTrue(alpha >= 0, "Alpha must be non-negative"); this.alpha = alpha; return this; @@ -179,7 +175,7 @@ public Linear alpha(double alpha) { * @param beta weight for vector score (0.0 to 1.0) * @return this Linear instance */ - public Linear beta(double beta) { + public Linear beta(double beta) { LettuceAssert.isTrue(beta >= 0, "Beta must be non-negative"); this.beta = beta; return this; diff --git a/src/main/java/io/lettuce/core/search/arguments/hybrid/HybridArgs.java b/src/main/java/io/lettuce/core/search/arguments/hybrid/HybridArgs.java index 0227736f37..14f0f8c27d 100644 --- a/src/main/java/io/lettuce/core/search/arguments/hybrid/HybridArgs.java +++ b/src/main/java/io/lettuce/core/search/arguments/hybrid/HybridArgs.java @@ -28,16 +28,13 @@ * * { * @code - * HybridArgs args = HybridArgs. builder() - * .search(HybridSearchArgs. builder().query("comfortable shoes").build()) - * .vectorSearch(HybridVectorArgs. builder().field("@embedding").vector(vectorBlob) - * .method(HybridVectorArgs.Knn.of(10)).build()) + * HybridArgs args = HybridArgs + * .builder().search(HybridSearchArgs.builder().query("comfortable shoes").build()).vectorSearch(HybridVectorArgs + * .builder().field("@embedding").vector(vectorBlob).method(HybridVectorArgs.Knn.of(10)).build()) * .combine(Combiners.rrf().window(20).constant(60)).build(); * } * * - * @param Key type. - * @param Value type. * @author Aleksandar Todorov * @since 7.2 * @see FT.HYBRID @@ -48,40 +45,40 @@ * @see PostProcessingArgs */ @SuppressWarnings("OptionalUsedAsFieldOrParameterType") -public class HybridArgs { +public class HybridArgs { - private final List> searchArgs = new ArrayList<>(); + private final List searchArgs = new ArrayList<>(); - private final List> vectorArgs = new ArrayList<>(); + private final List vectorArgs = new ArrayList<>(); - private Optional> combiner = Optional.empty(); + private Optional combiner = Optional.empty(); - private Optional> postProcessingArgs = Optional.empty(); + private Optional postProcessingArgs = Optional.empty(); - private final Map params = new HashMap<>(); + private final Map params = new HashMap<>(); private Optional timeout = Optional.empty(); /** * @return a new {@link Builder} for {@link HybridArgs}. */ - public static Builder builder() { - return new Builder<>(); + public static Builder builder() { + return new Builder(); } /** * Builder for {@link HybridArgs}. */ - public static class Builder { + public static class Builder { - private final HybridArgs instance = new HybridArgs<>(); + private final HybridArgs instance = new HybridArgs(); /** * Build the {@link HybridArgs} instance. * * @return the configured arguments */ - public HybridArgs build() { + public HybridArgs build() { return instance; } @@ -91,7 +88,7 @@ public HybridArgs build() { * @param searchArgs the search arguments * @return this builder */ - public Builder search(HybridSearchArgs searchArgs) { + public Builder search(HybridSearchArgs searchArgs) { LettuceAssert.notNull(searchArgs, "Search args must not be null"); instance.searchArgs.add(searchArgs); return this; @@ -103,7 +100,7 @@ public Builder search(HybridSearchArgs searchArgs) { * @param vectorArgs the vector search arguments * @return this builder */ - public Builder vectorSearch(HybridVectorArgs vectorArgs) { + public Builder vectorSearch(HybridVectorArgs vectorArgs) { LettuceAssert.notNull(vectorArgs, "Vector args must not be null"); instance.vectorArgs.add(vectorArgs); return this; @@ -119,7 +116,7 @@ public Builder vectorSearch(HybridVectorArgs vectorArgs) { * @return this builder * @see Combiners */ - public Builder combine(Combiner combiner) { + public Builder combine(Combiner combiner) { LettuceAssert.notNull(combiner, "Combiner must not be null"); instance.combiner = Optional.of(combiner); return this; @@ -131,7 +128,7 @@ public Builder combine(Combiner combiner) { * @param postProcessingArgs the post-processing configuration * @return this builder */ - public Builder postProcessing(PostProcessingArgs postProcessingArgs) { + public Builder postProcessing(PostProcessingArgs postProcessingArgs) { LettuceAssert.notNull(postProcessingArgs, "PostProcessingArgs must not be null"); instance.postProcessingArgs = Optional.of(postProcessingArgs); return this; @@ -147,7 +144,7 @@ public Builder postProcessing(PostProcessingArgs postProcessingArgs) * @param value the parameter value * @return this builder */ - public Builder param(K name, V value) { + public Builder param(String name, String value) { LettuceAssert.notNull(name, "Parameter name must not be null"); LettuceAssert.notNull(value, "Parameter value must not be null"); instance.params.put(name, value); @@ -165,7 +162,7 @@ public Builder param(K name, V value) { * @param value the binary parameter value (e.g., vector data) * @return this builder */ - public Builder param(K name, byte[] value) { + public Builder param(String name, byte[] value) { LettuceAssert.notNull(name, "Parameter name must not be null"); LettuceAssert.notNull(value, "Parameter value must not be null"); instance.params.put(name, value); @@ -178,7 +175,7 @@ public Builder param(K name, byte[] value) { * @param timeout the timeout duration (with millisecond resolution) * @return this builder */ - public Builder timeout(Duration timeout) { + public Builder timeout(Duration timeout) { LettuceAssert.notNull(timeout, "Timeout must not be null"); instance.timeout = Optional.of(timeout); return this; @@ -195,8 +192,7 @@ public Builder timeout(Duration timeout) { * * @param args the {@link CommandArgs} to append to */ - @SuppressWarnings("unchecked") - public void build(CommandArgs args) { + public void build(CommandArgs args) { // Both SEARCH and VSIM must be configured (per PRD) LettuceAssert.notNull(searchArgs, "SEARCH clause is required - use search() or search(HybridSearchArgs)"); LettuceAssert.notNull(vectorArgs, "VSIM clause is required - use vectorSearch() or vectorSearch(HybridVectorArgs)"); @@ -221,11 +217,11 @@ public void build(CommandArgs args) { args.add(CommandKeyword.PARAMS); args.add(params.size() * 2L); params.forEach((name, value) -> { - args.addKey(name); + args.add(name); if (value instanceof byte[]) { args.add((byte[]) value); } else { - args.addValue((V) value); + args.add((String) value); } }); } diff --git a/src/main/java/io/lettuce/core/search/arguments/hybrid/HybridSearchArgs.java b/src/main/java/io/lettuce/core/search/arguments/hybrid/HybridSearchArgs.java index e231b4ef8f..061c5957e2 100644 --- a/src/main/java/io/lettuce/core/search/arguments/hybrid/HybridSearchArgs.java +++ b/src/main/java/io/lettuce/core/search/arguments/hybrid/HybridSearchArgs.java @@ -18,33 +18,31 @@ /** * Arguments for the SEARCH clause in FT.HYBRID command. Configures text search query, scoring function, and score aliasing. * - * @param Key type - * @param Value type * @author Aleksandar Todorov * @since 7.5 * @see ScoringFunction * @see Scorer */ @Experimental -public class HybridSearchArgs { +public class HybridSearchArgs { - private final V query; + private final String query; private final Scorer scorer; - private final K scoreAlias; + private final String scoreAlias; - private HybridSearchArgs(Builder builder) { + private HybridSearchArgs(Builder builder) { this.query = builder.query; this.scorer = builder.scorer; this.scoreAlias = builder.scoreAlias; } - public static Builder builder() { - return new Builder<>(); + public static Builder builder() { + return new Builder(); } - public V getQuery() { + public String getQuery() { return query; } @@ -52,17 +50,17 @@ public Optional getScorer() { return Optional.ofNullable(scorer); } - public Optional getScoreAlias() { + public Optional getScoreAlias() { return Optional.ofNullable(scoreAlias); } - public static class Builder { + public static class Builder { - private V query; + private String query; private Scorer scorer; - private K scoreAlias; + private String scoreAlias; /** * Set the text search query. @@ -70,7 +68,7 @@ public static class Builder { * @param query the search query * @return this builder */ - public Builder query(V query) { + public Builder query(String query) { LettuceAssert.notNull(query, "Query must not be null"); this.query = query; return this; @@ -82,7 +80,7 @@ public Builder query(V query) { * @param scorer the scorer to use * @return this builder */ - public Builder scorer(Scorer scorer) { + public Builder scorer(Scorer scorer) { LettuceAssert.notNull(scorer, "Scorer must not be null"); this.scorer = scorer; return this; @@ -94,7 +92,7 @@ public Builder scorer(Scorer scorer) { * @param alias the field name to use for the search score * @return this builder */ - public Builder scoreAlias(K alias) { + public Builder scoreAlias(String alias) { LettuceAssert.notNull(alias, "Score alias must not be null"); this.scoreAlias = alias; return this; @@ -105,9 +103,9 @@ public Builder scoreAlias(K alias) { * * @return the configured arguments */ - public HybridSearchArgs build() { + public HybridSearchArgs build() { LettuceAssert.notNull(query, "Query must not be null"); - return new HybridSearchArgs<>(this); + return new HybridSearchArgs(this); } } @@ -117,9 +115,9 @@ public HybridSearchArgs build() { * * @param args the {@link CommandArgs} to append to */ - public void build(CommandArgs args) { + public void build(CommandArgs args) { args.add(CommandKeyword.SEARCH); - args.addValue(query); + args.add(query); // SCORER inside SEARCH if (scorer != null) { @@ -129,7 +127,7 @@ public void build(CommandArgs args) { // YIELD_SCORE_AS for SEARCH if (scoreAlias != null) { args.add(CommandKeyword.YIELD_SCORE_AS); - args.addKey(scoreAlias); + args.add(scoreAlias); } } diff --git a/src/main/java/io/lettuce/core/search/arguments/hybrid/HybridVectorArgs.java b/src/main/java/io/lettuce/core/search/arguments/hybrid/HybridVectorArgs.java index 5fc1523dcc..b5261c4908 100644 --- a/src/main/java/io/lettuce/core/search/arguments/hybrid/HybridVectorArgs.java +++ b/src/main/java/io/lettuce/core/search/arguments/hybrid/HybridVectorArgs.java @@ -20,8 +20,6 @@ * Arguments for the VSIM clause in FT.HYBRID command. Configures vector similarity search including field, vector data, search * method (KNN or RANGE), filters, and score aliasing. * - * @param Key type - * @param Value type * @author Aleksandar Todorov * @since 7.2 * @see VectorSearchMethod @@ -29,19 +27,19 @@ * @see Range */ @Experimental -public class HybridVectorArgs { +public class HybridVectorArgs { - private final K fieldName; + private final String fieldName; - private final V vectorData; + private final String vectorData; private final VectorSearchMethod method; private final List filters; - private final K scoreAlias; + private final String scoreAlias; - private HybridVectorArgs(Builder builder) { + private HybridVectorArgs(Builder builder) { this.fieldName = builder.fieldName; this.vectorData = builder.vectorData; this.method = builder.method; @@ -49,15 +47,15 @@ private HybridVectorArgs(Builder builder) { this.scoreAlias = builder.scoreAlias; } - public static Builder builder() { - return new Builder<>(); + public static Builder builder() { + return new Builder(); } - public K getFieldName() { + public String getFieldName() { return fieldName; } - public V getVectorData() { + public String getVectorData() { return vectorData; } @@ -69,21 +67,21 @@ public List getFilters() { return filters; } - public Optional getScoreAlias() { + public Optional getScoreAlias() { return Optional.ofNullable(scoreAlias); } - public static class Builder { + public static class Builder { - private K fieldName; + private String fieldName; - private V vectorData; + private String vectorData; private VectorSearchMethod method; private final List filters = new ArrayList<>(); - private K scoreAlias; + private String scoreAlias; /** * Set the vector field name. @@ -91,7 +89,7 @@ public static class Builder { * @param fieldName the field name (typically prefixed with '@') * @return this builder */ - public Builder field(K fieldName) { + public Builder field(String fieldName) { LettuceAssert.notNull(fieldName, "Field name must not be null"); this.fieldName = fieldName; return this; @@ -107,7 +105,7 @@ public Builder field(K fieldName) { * @param vectorData the parameter reference (e.g., "$vec") * @return this builder */ - public Builder vector(V vectorData) { + public Builder vector(String vectorData) { LettuceAssert.notNull(vectorData, "Vector data must not be null"); this.vectorData = vectorData; return this; @@ -121,7 +119,7 @@ public Builder vector(V vectorData) { * @see Knn * @see Range */ - public Builder method(VectorSearchMethod method) { + public Builder method(VectorSearchMethod method) { LettuceAssert.notNull(method, "Vector search method must not be null"); this.method = method; return this; @@ -133,7 +131,7 @@ public Builder method(VectorSearchMethod method) { * @param expression the filter expression (e.g., "@brand:{apple|samsung}") * @return this builder */ - public Builder filter(String expression) { + public Builder filter(String expression) { LettuceAssert.notNull(expression, "Filter expression must not be null"); this.filters.add(expression); return this; @@ -145,7 +143,7 @@ public Builder filter(String expression) { * @param alias the field name to use for the normalized vector distance * @return this builder */ - public Builder scoreAlias(K alias) { + public Builder scoreAlias(String alias) { LettuceAssert.notNull(alias, "Score alias must not be null"); this.scoreAlias = alias; return this; @@ -156,10 +154,10 @@ public Builder scoreAlias(K alias) { * * @return the configured arguments */ - public HybridVectorArgs build() { + public HybridVectorArgs build() { LettuceAssert.notNull(fieldName, "Field name must not be null"); LettuceAssert.notNull(vectorData, "Vector data must not be null"); - return new HybridVectorArgs<>(this); + return new HybridVectorArgs(this); } } @@ -169,10 +167,10 @@ public HybridVectorArgs build() { * * @param args the {@link CommandArgs} to append to */ - public void build(CommandArgs args) { + public void build(CommandArgs args) { args.add(CommandType.VSIM); - args.addKey(fieldName); - args.addValue(vectorData); + args.add(fieldName); + args.add(vectorData); // Vector search method (KNN or RANGE) - optional if (method != null) { @@ -188,7 +186,7 @@ public void build(CommandArgs args) { // YIELD_SCORE_AS for VSIM (normalized vector distance) if (scoreAlias != null) { args.add(CommandKeyword.YIELD_SCORE_AS); - args.addKey(scoreAlias); + args.add(scoreAlias); } } @@ -205,10 +203,8 @@ public interface VectorSearchMethod { * Build the method arguments into the command. * * @param args command arguments - * @param key type - * @param value type */ - void build(CommandArgs args); + void build(CommandArgs args); } @@ -261,7 +257,7 @@ public Knn efRuntime(int efRuntime) { } @Override - public void build(CommandArgs args) { + public void build(CommandArgs args) { args.add(CommandKeyword.KNN); // Count of total items: K + value, optionally EF_RUNTIME + value int itemCount = efRuntime != null ? 4 : 2; @@ -324,7 +320,7 @@ public Range epsilon(double epsilon) { } @Override - public void build(CommandArgs args) { + public void build(CommandArgs args) { args.add(CommandKeyword.RANGE); // Count of key-value pairs: 1 for RADIUS, +1 if EPSILON is present int pairCount = epsilon != null ? 4 : 2; diff --git a/src/main/java/io/lettuce/core/search/arguments/hybrid/PostProcessingArgs.java b/src/main/java/io/lettuce/core/search/arguments/hybrid/PostProcessingArgs.java index 011f44d58c..fabef4163e 100644 --- a/src/main/java/io/lettuce/core/search/arguments/hybrid/PostProcessingArgs.java +++ b/src/main/java/io/lettuce/core/search/arguments/hybrid/PostProcessingArgs.java @@ -41,15 +41,13 @@ * * { * @code - * PostProcessingArgs args = PostProcessingArgs. builder().load("@price", "@category") + * PostProcessingArgs args = PostProcessingArgs.builder().load("@price", "@category") * .groupBy(GroupBy.of("@category").reduce(Reducers.count().as("total"))) * .apply(Apply.of("@price * 0.9", "discounted_price")).sortBy(SortBy.of("@discounted_price", SortDirection.DESC)) * .filter(Filter.of("@discounted_price > 100")).limit(Limit.of(0, 10)).build(); * } * * - * @param Key type. - * @param Value type. * @author Aleksandar Todorov * @since 7.2 * @see GroupBy @@ -59,9 +57,9 @@ * @see Limit */ @Experimental -public class PostProcessingArgs { +public class PostProcessingArgs { - private final List loadFields = new ArrayList<>(); + private final List loadFields = new ArrayList<>(); private boolean loadAll = false; @@ -69,7 +67,7 @@ public class PostProcessingArgs { * Ordered list of pipeline operations (GROUPBY, SORTBY, APPLY, FILTER, LIMIT). These operations are applied in the order * specified by the user. */ - private final List> postProcessingOperations = new ArrayList<>(); + private final List postProcessingOperations = new ArrayList<>(); // Tracking flags for single-use operations private boolean hasGroupBy = false; @@ -83,19 +81,17 @@ public class PostProcessingArgs { /** * @return a new {@link Builder} for {@link PostProcessingArgs}. */ - public static Builder builder() { - return new Builder<>(); + public static Builder builder() { + return new Builder(); } /** * Builder for {@link PostProcessingArgs}. * - * @param Key type. - * @param Value type. */ - public static class Builder { + public static class Builder { - private final PostProcessingArgs instance = new PostProcessingArgs<>(); + private final PostProcessingArgs instance = new PostProcessingArgs(); /** * Request loading of document attributes. @@ -109,9 +105,9 @@ public static class Builder { * @throws IllegalArgumentException if {@code "*"} is passed as a field name */ @SafeVarargs - public final Builder load(K... fields) { + public final Builder load(String... fields) { LettuceAssert.notNull(fields, "Fields must not be null"); - for (K field : fields) { + for (String field : fields) { LettuceAssert.notNull(field, "Field must not be null"); if ("*".equals(field)) { throw new IllegalArgumentException("Use loadAll() instead of load(\"*\") to load all document attributes"); @@ -134,7 +130,7 @@ public final Builder load(K... fields) { * @return this builder * @since Redis OSS 8.6 */ - public Builder loadAll() { + public Builder loadAll() { instance.loadAll = true; return this; } @@ -150,7 +146,7 @@ public Builder loadAll() { * @return this builder * @throws IllegalStateException if a GROUPBY operation has already been added */ - public Builder groupBy(GroupBy groupBy) { + public Builder groupBy(GroupBy groupBy) { LettuceAssert.notNull(groupBy, "GroupBy must not be null"); if (instance.hasGroupBy) { throw new IllegalStateException("GROUPBY operation has already been added. Only one GROUPBY is allowed."); @@ -170,7 +166,7 @@ public Builder groupBy(GroupBy groupBy) { * @return this builder * @throws IllegalStateException if a SORTBY operation has already been added */ - public Builder sortBy(SortBy sortBy) { + public Builder sortBy(SortBy sortBy) { LettuceAssert.notNull(sortBy, "SortBy must not be null"); if (instance.hasSortBy) { throw new IllegalStateException("SORTBY operation has already been added. Only one SORTBY is allowed."); @@ -190,7 +186,7 @@ public Builder sortBy(SortBy sortBy) { * @return this builder * @throws IllegalStateException if a LIMIT operation has already been added */ - public Builder limit(Limit limit) { + public Builder limit(Limit limit) { LettuceAssert.notNull(limit, "Limit must not be null"); if (instance.hasLimit) { throw new IllegalStateException("LIMIT operation has already been added. Only one LIMIT is allowed."); @@ -209,7 +205,7 @@ public Builder limit(Limit limit) { * @param apply the APPLY operation * @return this builder */ - public Builder apply(Apply apply) { + public Builder apply(Apply apply) { LettuceAssert.notNull(apply, "Apply must not be null"); instance.postProcessingOperations.add(apply); return this; @@ -225,7 +221,7 @@ public Builder apply(Apply apply) { * @return this builder * @throws IllegalStateException if a FILTER operation has already been added */ - public Builder filter(Filter filter) { + public Builder filter(Filter filter) { LettuceAssert.notNull(filter, "Filter must not be null"); if (instance.hasFilter) { throw new IllegalStateException("FILTER operation has already been added. Only one FILTER is allowed."); @@ -240,7 +236,7 @@ public Builder filter(Filter filter) { * * @return the built {@link PostProcessingArgs} */ - public PostProcessingArgs build() { + public PostProcessingArgs build() { return instance; } @@ -254,7 +250,7 @@ public PostProcessingArgs build() { * * @param args the {@link CommandArgs} to append to */ - public void build(CommandArgs args) { + public void build(CommandArgs args) { // LOAD clause - only emit if loadAll or loadFields is specified if (loadAll) { // LOAD * (no count prefix for wildcard) @@ -264,15 +260,12 @@ public void build(CommandArgs args) { // LOAD count field [field ...] args.add(CommandKeyword.LOAD); args.add(loadFields.size()); - loadFields.forEach(args::addKey); + loadFields.forEach(args::add); } // No LOAD emitted if neither loadAll nor loadFields specified - for (PostProcessingOperation operation : postProcessingOperations) { - // Cast is safe because all operations can build with CommandArgs - @SuppressWarnings("unchecked") - PostProcessingOperation typedOperation = (PostProcessingOperation) operation; - typedOperation.build(args); + for (PostProcessingOperation operation : postProcessingOperations) { + operation.build(args); } } diff --git a/src/main/kotlin/io/lettuce/core/api/coroutines/RediSearchCoroutinesCommands.kt b/src/main/kotlin/io/lettuce/core/api/coroutines/RediSearchCoroutinesCommands.kt index 921eb29b90..f6388af68a 100644 --- a/src/main/kotlin/io/lettuce/core/api/coroutines/RediSearchCoroutinesCommands.kt +++ b/src/main/kotlin/io/lettuce/core/api/coroutines/RediSearchCoroutinesCommands.kt @@ -31,14 +31,13 @@ import io.lettuce.core.search.arguments.hybrid.HybridArgs * Coroutine executed commands for RediSearch functionality * * @param Key type. - * @param Value type. * @author Tihomir Mateev * @see RediSearch * @since 6.8 * @generated by io.lettuce.apigenerator.CreateKotlinCoroutinesApi */ @ExperimentalLettuceCoroutinesApi -interface RediSearchCoroutinesCommands { +interface RediSearchCoroutinesCommands { /** * Create a new search index with the given name and field definitions using default settings. @@ -64,7 +63,7 @@ interface RediSearchCoroutinesCommands { * @see #ftDropindex(String) */ @Experimental - suspend fun ftCreate(index: String, fieldArgs: List>): String? + suspend fun ftCreate(index: String, fieldArgs: List): String? /** * Create a new search index with the given name, custom configuration, and field definitions. @@ -103,7 +102,7 @@ interface RediSearchCoroutinesCommands { * @see #ftDropindex(String) */ @Experimental - suspend fun ftCreate(index: String, arguments: CreateArgs, fieldArgs: List>): String? + suspend fun ftCreate(index: String, arguments: CreateArgs, fieldArgs: List): String? /** * Add an alias to a search index. @@ -280,7 +279,7 @@ interface RediSearchCoroutinesCommands { * @see #ftCreate(String, CreateArgs, List) */ @Experimental - suspend fun ftAlter(index: String, skipInitialScan: Boolean, fieldArgs: List>): String? + suspend fun ftAlter(index: String, skipInitialScan: Boolean, fieldArgs: List): String? /** * Add new attributes to an existing search index. @@ -315,7 +314,7 @@ interface RediSearchCoroutinesCommands { * @see #ftCreate(String, CreateArgs, List) */ @Experimental - suspend fun ftAlter(index: String, fieldArgs: List>): String? + suspend fun ftAlter(index: String, fieldArgs: List): String? /** * Return a distinct set of values indexed in a Tag field. @@ -371,7 +370,7 @@ interface RediSearchCoroutinesCommands { * @see #ftCreate(String, CreateArgs, List) */ @Experimental - suspend fun ftTagvals(index: String, fieldName: String): List + suspend fun ftTagvals(index: String, fieldName: String): List /** * Perform spelling correction on a query, returning suggestions for misspelled terms. @@ -406,13 +405,13 @@ interface RediSearchCoroutinesCommands { * @since 6.8 * @see FT.SPELLCHECK * @see Spellchecking - * @see #ftSpellcheck(String, Any, SpellCheckArgs) - * @see #ftDictadd(String, Any[]) - * @see #ftDictdel(String, Any[]) + * @see #ftSpellcheck(String, String, SpellCheckArgs) + * @see #ftDictadd(String, String[]) + * @see #ftDictdel(String, String[]) * @see #ftDictdump(String) */ @Experimental - suspend fun ftSpellcheck(index: String, query: V): SpellCheckResult? + suspend fun ftSpellcheck(index: String, query: String): SpellCheckResult? /** * Perform spelling correction on a query with additional options. @@ -443,13 +442,13 @@ interface RediSearchCoroutinesCommands { * @since 6.8 * @see FT.SPELLCHECK * @see Spellchecking - * @see #ftSpellcheck(String, Any) - * @see #ftDictadd(String, Any[]) - * @see #ftDictdel(String, Any[]) + * @see #ftSpellcheck(String, String) + * @see #ftDictadd(String, String[]) + * @see #ftDictdel(String, String[]) * @see #ftDictdump(String) */ @Experimental - suspend fun ftSpellcheck(index: String, query: V, args: SpellCheckArgs): SpellCheckResult? + suspend fun ftSpellcheck(index: String, query: String, args: SpellCheckArgs): SpellCheckResult? /** * Add terms to a dictionary. @@ -479,11 +478,11 @@ interface RediSearchCoroutinesCommands { * @since 6.8 * @see FT.DICTADD * @see Spellchecking - * @see #ftDictdel(String, Any[]) + * @see #ftDictdel(String, String[]) * @see #ftDictdump(String) */ @Experimental - suspend fun ftDictadd(dict: String, vararg terms: V): Long? + suspend fun ftDictadd(dict: String, vararg terms: String): Long? /** * Delete terms from a dictionary. @@ -502,11 +501,11 @@ interface RediSearchCoroutinesCommands { * @return the number of terms that were deleted * @since 6.8 * @see FT.DICTDEL - * @see #ftDictadd(String, Any[]) + * @see #ftDictadd(String, String[]) * @see #ftDictdump(String) */ @Experimental - suspend fun ftDictdel(dict: String, vararg terms: V): Long? + suspend fun ftDictdel(dict: String, vararg terms: String): Long? /** * Dump all terms in a dictionary. @@ -523,11 +522,11 @@ interface RediSearchCoroutinesCommands { * @return a list of all terms in the dictionary * @since 6.8 * @see FT.DICTDUMP - * @see #ftDictadd(String, Any[]) - * @see #ftDictdel(String, Any[]) + * @see #ftDictadd(String, String[]) + * @see #ftDictdel(String, String[]) */ @Experimental - suspend fun ftDictdump(dict: String): List + suspend fun ftDictdump(dict: String): List /** * Return the execution plan for a complex query. @@ -556,11 +555,11 @@ interface RediSearchCoroutinesCommands { * @return the execution plan as a string * @since 6.8 * @see FT.EXPLAIN - * @see #ftExplain(String, Any, ExplainArgs) - * @see #ftSearch(String, Any) + * @see #ftExplain(String, String, ExplainArgs) + * @see #ftSearch(String, String) */ @Experimental - suspend fun ftExplain(index: String, query: V): String? + suspend fun ftExplain(index: String, query: String): String? /** * Return the execution plan for a complex query with additional options. @@ -587,11 +586,11 @@ interface RediSearchCoroutinesCommands { * @return the execution plan as a string * @since 6.8 * @see FT.EXPLAIN - * @see #ftExplain(String, Any) - * @see #ftSearch(String, Any) + * @see #ftExplain(String, String) + * @see #ftSearch(String, String) */ @Experimental - suspend fun ftExplain(index: String, query: V, args: ExplainArgs): String? + suspend fun ftExplain(index: String, query: String, args: ExplainArgs): String? /** * Return a list of all existing indexes. @@ -623,11 +622,11 @@ interface RediSearchCoroutinesCommands { * @return a list of index names * @since 6.8 * @see FT._LIST - * @see #ftCreate(String, CreateArgs, FieldArgs[]) + * @see #ftCreate(String, CreateArgs, List) * @see #ftDropindex(String) */ @Experimental - suspend fun ftList(): List + suspend fun ftList(): List /** * Dump synonym group contents. @@ -655,11 +654,11 @@ interface RediSearchCoroutinesCommands { * @return a map where keys are synonym terms and values are lists of group IDs containing that synonym * @since 6.8 * @see FT.SYNDUMP - * @see #ftSynupdate(String, Any, Any[]) - * @see #ftSynupdate(String, Any, SynUpdateArgs, Any[]) + * @see #ftSynupdate(String, String, String[]) + * @see #ftSynupdate(String, String, SynUpdateArgs, String[]) */ @Experimental - suspend fun ftSyndump(index: String): Map>? + suspend fun ftSyndump(index: String): Map>? /** * Update a synonym group with additional terms. @@ -689,11 +688,11 @@ interface RediSearchCoroutinesCommands { * @return OK if executed correctly * @since 6.8 * @see FT.SYNUPDATE - * @see #ftSynupdate(String, Any, SynUpdateArgs, Any[]) + * @see #ftSynupdate(String, String, SynUpdateArgs, String[]) * @see #ftSyndump(String) */ @Experimental - suspend fun ftSynupdate(index: String, synonymGroupId: V, vararg terms: V): String? + suspend fun ftSynupdate(index: String, synonymGroupId: String, vararg terms: String): String? /** * Update a synonym group with additional terms and options. @@ -721,11 +720,11 @@ interface RediSearchCoroutinesCommands { * @return OK if executed correctly * @since 6.8 * @see FT.SYNUPDATE - * @see #ftSynupdate(String, Any, Any[]) + * @see #ftSynupdate(String, String, String[]) * @see #ftSyndump(String) */ @Experimental - suspend fun ftSynupdate(index: String, synonymGroupId: V, args: SynUpdateArgs, vararg terms: V): String? + suspend fun ftSynupdate(index: String, synonymGroupId: String, args: SynUpdateArgs, vararg terms: String): String? /** * Add a suggestion string to an auto-complete suggestion dictionary. @@ -756,13 +755,13 @@ interface RediSearchCoroutinesCommands { * @return the current size of the suggestion dictionary after adding the suggestion * @since 6.8 * @see FT.SUGADD - * @see #ftSugadd(Any, Any, Double, SugAddArgs) - * @see #ftSugget(Any, Any) - * @see #ftSugdel(Any, Any) - * @see #ftSuglen(Any) + * @see #ftSugadd(K, String, Double, SugAddArgs) + * @see #ftSugget(K, String) + * @see #ftSugdel(K, String) + * @see #ftSuglen(K) */ @Experimental - suspend fun ftSugadd(key: K, suggestion: V, score: Double): Long? + suspend fun ftSugadd(key: K, suggestion: String, score: Double): Long? /** * Add a suggestion string to an auto-complete suggestion dictionary with additional options. @@ -783,13 +782,13 @@ interface RediSearchCoroutinesCommands { * @return the current size of the suggestion dictionary after adding the suggestion * @since 6.8 * @see FT.SUGADD - * @see #ftSugadd(Any, Any, Double) - * @see #ftSugget(Any, Any, SugGetArgs) - * @see #ftSugdel(Any, Any) - * @see #ftSuglen(Any) + * @see #ftSugadd(K, String, Double) + * @see #ftSugget(K, String, SugGetArgs) + * @see #ftSugdel(K, String) + * @see #ftSuglen(K) */ @Experimental - suspend fun ftSugadd(key: K, suggestion: V, score: Double, args: SugAddArgs): Long? + suspend fun ftSugadd(key: K, suggestion: String, score: Double, args: SugAddArgs): Long? /** * Delete a string from a suggestion dictionary. @@ -808,12 +807,12 @@ interface RediSearchCoroutinesCommands { * @return @code true} if the string was found and deleted, `false` otherwise * @since 6.8 * @see FT.SUGDEL - * @see #ftSugadd(Any, Any, Double) - * @see #ftSugget(Any, Any) - * @see #ftSuglen(Any) + * @see #ftSugadd(K, String, Double) + * @see #ftSugget(K, String) + * @see #ftSuglen(K) */ @Experimental - suspend fun ftSugdel(key: K, suggestion: V): Boolean? + suspend fun ftSugdel(key: K, suggestion: String): Boolean? /** * Get completion suggestions for a prefix. @@ -832,13 +831,13 @@ interface RediSearchCoroutinesCommands { * @return a list of suggestions matching the prefix * @since 6.8 * @see FT.SUGGET - * @see #ftSugget(Any, Any, SugGetArgs) - * @see #ftSugadd(Any, Any, Double) - * @see #ftSugdel(Any, Any) - * @see #ftSuglen(Any) + * @see #ftSugget(K, String, SugGetArgs) + * @see #ftSugadd(K, String, Double) + * @see #ftSugdel(K, String) + * @see #ftSuglen(K) */ @Experimental - suspend fun ftSugget(key: K, prefix: V): List> + suspend fun ftSugget(key: K, prefix: String): List /** * Get completion suggestions for a prefix with additional options. @@ -858,13 +857,13 @@ interface RediSearchCoroutinesCommands { * @return a list of suggestions matching the prefix, optionally with scores and payloads * @since 6.8 * @see FT.SUGGET - * @see #ftSugget(Any, Any) - * @see #ftSugadd(Any, Any, Double, SugAddArgs) - * @see #ftSugdel(Any, Any) - * @see #ftSuglen(Any) + * @see #ftSugget(K, String) + * @see #ftSugadd(K, String, Double, SugAddArgs) + * @see #ftSugdel(K, String) + * @see #ftSuglen(K) */ @Experimental - suspend fun ftSugget(key: K, prefix: V, args: SugGetArgs): List> + suspend fun ftSugget(key: K, prefix: String, args: SugGetArgs): List /** * Get the size of an auto-complete suggestion dictionary. @@ -881,9 +880,9 @@ interface RediSearchCoroutinesCommands { * @return the current size of the suggestion dictionary * @since 6.8 * @see FT.SUGLEN - * @see #ftSugadd(Any, Any, Double) - * @see #ftSugget(Any, Any) - * @see #ftSugdel(Any, Any) + * @see #ftSugadd(K, String, Double) + * @see #ftSugget(K, String) + * @see #ftSugdel(K, String) */ @Experimental suspend fun ftSuglen(key: K): Long? @@ -975,10 +974,10 @@ interface RediSearchCoroutinesCommands { * @see Query syntax * @see SearchReply * @see SearchArgs - * @see #ftSearch(String, Any, SearchArgs) + * @see #ftSearch(String, String, SearchArgs) */ @Experimental - suspend fun ftSearch(index: String, query: V): SearchReply? + suspend fun ftSearch(index: String, query: String): SearchReply? /** * Search the index with a textual query using advanced search options and filters. @@ -1026,23 +1025,23 @@ interface RediSearchCoroutinesCommands { * @see Advanced concepts * @see SearchReply * @see SearchArgs - * @see #ftSearch(String, Any) + * @see #ftSearch(String, String) */ @Experimental - suspend fun ftSearch(index: String, query: V, args: SearchArgs): SearchReply? + suspend fun ftSearch(index: String, query: String, args: SearchArgs): SearchReply? /** * Run a search query on an index and perform basic aggregate transformations using default options. * *

    * This command executes a search query and applies aggregation operations to transform and analyze the results. Unlike - * [ftSearch(String, Any)], which returns individual documents, FT.AGGREGATE processes the result set through a + * [ftSearch(String, String)], which returns individual documents, FT.AGGREGATE processes the result set through a * pipeline of transformations to produce analytical insights, summaries, and computed values. *

    * *

    * This basic variant uses default aggregation behavior without additional pipeline operations. For advanced aggregations - * with grouping, sorting, filtering, and custom transformations, use [ftAggregate(String, Any, AggregateArgs)]. + * with grouping, sorting, filtering, and custom transformations, use [ftAggregate(String, String, AggregateArgs)]. *

    * *

    @@ -1068,10 +1067,10 @@ interface RediSearchCoroutinesCommands { * "https://redis.io/docs/latest/develop/interact/search-and-query/advanced-concepts/aggregations/">Aggregations * @see SearchReply * @see AggregateArgs - * @see #ftAggregate(String, Any, AggregateArgs) + * @see #ftAggregate(String, String, AggregateArgs) */ @Experimental - suspend fun ftAggregate(index: String, query: V): AggregationReply? + suspend fun ftAggregate(index: String, query: String): AggregationReply? /** * Run a search query on an index and perform advanced aggregate transformations with a processing pipeline. @@ -1123,18 +1122,18 @@ interface RediSearchCoroutinesCommands { * API * @see SearchReply * @see AggregateArgs - * @see #ftAggregate(String, Any) + * @see #ftAggregate(String, String) * @see #ftCursorread(String, Cursor) */ @Experimental - suspend fun ftAggregate(index: String, query: V, args: AggregateArgs): AggregationReply? + suspend fun ftAggregate(index: String, query: String, args: AggregateArgs): AggregationReply? /** * Read next results from an existing cursor and optionally override the batch size. * *

    * This command is used to read the next batch of results from a cursor that was created by - * [ftAggregate(String, Any, AggregateArgs)] with the `WITHCURSOR` option. Cursors provide an efficient way + * [ftAggregate(String, String, AggregateArgs)] with the `WITHCURSOR` option. Cursors provide an efficient way * to iterate through large result sets without loading all results into memory at once. *

    * @@ -1157,17 +1156,17 @@ interface RediSearchCoroutinesCommands { * "https://redis.io/docs/latest/develop/interact/search-and-query/advanced-concepts/aggregations/#cursor-api">Cursor * API * @see AggregationReply - * @see #ftAggregate(String, Any, AggregateArgs) + * @see #ftAggregate(String, String, AggregateArgs) */ @Experimental - suspend fun ftCursorread(index: String, cursor: Cursor, count: Int): AggregationReply? + suspend fun ftCursorread(index: String, cursor: Cursor, count: Int): AggregationReply? /** * Read next results from an existing cursor using the default batch size. * *

    * This command is used to read the next batch of results from a cursor created by - * [ftAggregate(String, Any, AggregateArgs)] with the `WITHCURSOR` option. This variant uses the default + * [ftAggregate(String, String, AggregateArgs)] with the `WITHCURSOR` option. This variant uses the default * batch size that was specified in the original `FT.AGGREGATE` command's `WITHCURSOR` clause. *

    * @@ -1189,16 +1188,16 @@ interface RediSearchCoroutinesCommands { * "https://redis.io/docs/latest/develop/interact/search-and-query/advanced-concepts/aggregations/#cursor-api">Cursor * API * @see AggregationReply - * @see #ftAggregate(String, Any, AggregateArgs) + * @see #ftAggregate(String, String, AggregateArgs) */ @Experimental - suspend fun ftCursorread(index: String, cursor: Cursor): AggregationReply? + suspend fun ftCursorread(index: String, cursor: Cursor): AggregationReply? /** * Delete a cursor and free its associated resources. * *

    - * This command is used to explicitly delete a cursor created by [ftAggregate(String, Any, AggregateArgs)] with + * This command is used to explicitly delete a cursor created by [ftAggregate(String, String, AggregateArgs)] with * the `WITHCURSOR` option. Deleting a cursor frees up server resources and should be done when you no longer need to * read more results from the cursor. *

    @@ -1226,7 +1225,7 @@ interface RediSearchCoroutinesCommands { * @see Cursor * API - * @see #ftAggregate(String, Any, AggregateArgs) + * @see #ftAggregate(String, String, AggregateArgs) * @see #ftCursorread(String, Cursor) * @see #ftCursorread(String, Cursor, Integer) */ @@ -1245,7 +1244,7 @@ interface RediSearchCoroutinesCommands { * @since 7.2 */ @Experimental - suspend fun ftHybrid(index: String, args: HybridArgs): HybridReply? + suspend fun ftHybrid(index: String, args: HybridArgs): HybridReply? } diff --git a/src/main/kotlin/io/lettuce/core/api/coroutines/RediSearchCoroutinesCommandsImpl.kt b/src/main/kotlin/io/lettuce/core/api/coroutines/RediSearchCoroutinesCommandsImpl.kt index 2b39fbc258..f0b36255d8 100644 --- a/src/main/kotlin/io/lettuce/core/api/coroutines/RediSearchCoroutinesCommandsImpl.kt +++ b/src/main/kotlin/io/lettuce/core/api/coroutines/RediSearchCoroutinesCommandsImpl.kt @@ -34,18 +34,17 @@ import kotlinx.coroutines.reactive.awaitFirstOrNull * Coroutine executed commands (based on reactive commands) for RediSearch. * * @param Key type. - * @param Value type. * @author Tihomir Mateev * @since 6.8 */ @ExperimentalLettuceCoroutinesApi -open class RediSearchCoroutinesCommandsImpl(internal val ops: RediSearchReactiveCommands) : - RediSearchCoroutinesCommands { +open class RediSearchCoroutinesCommandsImpl(internal val ops: RediSearchReactiveCommands) : + RediSearchCoroutinesCommands { - override suspend fun ftCreate(index: String, arguments: CreateArgs, fieldArgs: List>): String? = + override suspend fun ftCreate(index: String, arguments: CreateArgs, fieldArgs: List): String? = ops.ftCreate(index, arguments, fieldArgs).awaitFirstOrNull() - override suspend fun ftCreate(index: String, fieldArgs: List>): String? = + override suspend fun ftCreate(index: String, fieldArgs: List): String? = ops.ftCreate(index, fieldArgs).awaitFirstOrNull() override suspend fun ftAliasadd(alias: String, index: String): String? = @@ -57,13 +56,13 @@ open class RediSearchCoroutinesCommandsImpl(internal val ops: override suspend fun ftAliasdel(alias: String): String? = ops.ftAliasdel(alias).awaitFirstOrNull() - override suspend fun ftAlter(index: String, skipInitialScan: Boolean, fieldArgs: List>): String? = + override suspend fun ftAlter(index: String, skipInitialScan: Boolean, fieldArgs: List): String? = ops.ftAlter(index, skipInitialScan, fieldArgs).awaitFirstOrNull() - override suspend fun ftTagvals(index: String, fieldName: String): List = + override suspend fun ftTagvals(index: String, fieldName: String): List = ops.ftTagvals(index, fieldName).asFlow().toList() - override suspend fun ftAlter(index: String, fieldArgs: List>): String? = + override suspend fun ftAlter(index: String, fieldArgs: List): String? = ops.ftAlter(index, fieldArgs).awaitFirstOrNull() override suspend fun ftDropindex(index: String, deleteDocuments: Boolean): String? = @@ -72,79 +71,79 @@ open class RediSearchCoroutinesCommandsImpl(internal val ops: override suspend fun ftDropindex(index: String): String? = ops.ftDropindex(index).awaitFirstOrNull() - override suspend fun ftSearch(index: String, query: V): SearchReply? = + override suspend fun ftSearch(index: String, query: String): SearchReply? = ops.ftSearch(index, query).awaitFirstOrNull() - override suspend fun ftSearch(index: String, query: V, args: SearchArgs): SearchReply? = + override suspend fun ftSearch(index: String, query: String, args: SearchArgs): SearchReply? = ops.ftSearch(index, query, args).awaitFirstOrNull() - override suspend fun ftAggregate(index: String, query: V, args: AggregateArgs): AggregationReply? = + override suspend fun ftAggregate(index: String, query: String, args: AggregateArgs): AggregationReply? = ops.ftAggregate(index, query, args).awaitFirstOrNull() - override suspend fun ftAggregate(index: String, query: V): AggregationReply? = + override suspend fun ftAggregate(index: String, query: String): AggregationReply? = ops.ftAggregate(index, query).awaitFirstOrNull() - override suspend fun ftCursorread(index: String, cursor: Cursor, count: Int): AggregationReply? = + override suspend fun ftCursorread(index: String, cursor: Cursor, count: Int): AggregationReply? = ops.ftCursorread(index, cursor, count).awaitFirstOrNull() - override suspend fun ftCursorread(index: String, cursor: Cursor): AggregationReply? = + override suspend fun ftCursorread(index: String, cursor: Cursor): AggregationReply? = ops.ftCursorread(index, cursor).awaitFirstOrNull() override suspend fun ftCursordel(index: String, cursor: Cursor): String? = ops.ftCursordel(index, cursor).awaitFirstOrNull() - override suspend fun ftHybrid(index: String, args: HybridArgs): HybridReply? = + override suspend fun ftHybrid(index: String, args: HybridArgs): HybridReply? = ops.ftHybrid(index, args).awaitFirstOrNull() - override suspend fun ftDictadd(dict: String, vararg terms: V): Long? = + override suspend fun ftDictadd(dict: String, vararg terms: String): Long? = ops.ftDictadd(dict, *terms).awaitFirstOrNull() - override suspend fun ftDictdel(dict: String, vararg terms: V): Long? = + override suspend fun ftDictdel(dict: String, vararg terms: String): Long? = ops.ftDictdel(dict, *terms).awaitFirstOrNull() - override suspend fun ftDictdump(dict: String): List = + override suspend fun ftDictdump(dict: String): List = ops.ftDictdump(dict).asFlow().toList() - override suspend fun ftSpellcheck(index: String, query: V): SpellCheckResult? = + override suspend fun ftSpellcheck(index: String, query: String): SpellCheckResult? = ops.ftSpellcheck(index, query).awaitFirstOrNull() - override suspend fun ftSpellcheck(index: String, query: V, args: SpellCheckArgs): SpellCheckResult? = + override suspend fun ftSpellcheck(index: String, query: String, args: SpellCheckArgs): SpellCheckResult? = ops.ftSpellcheck(index, query, args).awaitFirstOrNull() - override suspend fun ftSugadd(key: K, suggestion: V, score: Double): Long? = + override suspend fun ftSugadd(key: K, suggestion: String, score: Double): Long? = ops.ftSugadd(key, suggestion, score).awaitFirstOrNull() - override suspend fun ftSugadd(key: K, suggestion: V, score: Double, args: SugAddArgs): Long? = + override suspend fun ftSugadd(key: K, suggestion: String, score: Double, args: SugAddArgs): Long? = ops.ftSugadd(key, suggestion, score, args).awaitFirstOrNull() - override suspend fun ftSugdel(key: K, suggestion: V): Boolean? = + override suspend fun ftSugdel(key: K, suggestion: String): Boolean? = ops.ftSugdel(key, suggestion).awaitFirstOrNull() - override suspend fun ftSugget(key: K, prefix: V): List> = + override suspend fun ftSugget(key: K, prefix: String): List = ops.ftSugget(key, prefix).asFlow().toList() - override suspend fun ftSugget(key: K, prefix: V, args: SugGetArgs): List> = + override suspend fun ftSugget(key: K, prefix: String, args: SugGetArgs): List = ops.ftSugget(key, prefix, args).asFlow().toList() override suspend fun ftSuglen(key: K): Long? = ops.ftSuglen(key).awaitFirstOrNull() - override suspend fun ftSynupdate(index: String, synonymGroupId: V, vararg terms: V): String? = + override suspend fun ftSynupdate(index: String, synonymGroupId: String, vararg terms: String): String? = ops.ftSynupdate(index, synonymGroupId, *terms).awaitFirstOrNull() - override suspend fun ftSynupdate(index: String, synonymGroupId: V, args: SynUpdateArgs, vararg terms: V): String? = + override suspend fun ftSynupdate(index: String, synonymGroupId: String, args: SynUpdateArgs, vararg terms: String): String? = ops.ftSynupdate(index, synonymGroupId, args, *terms).awaitFirstOrNull() - override suspend fun ftSyndump(index: String): Map>? = + override suspend fun ftSyndump(index: String): Map>? = ops.ftSyndump(index).awaitFirstOrNull() - override suspend fun ftExplain(index: String, query: V): String? = + override suspend fun ftExplain(index: String, query: String): String? = ops.ftExplain(index, query).awaitFirstOrNull() - override suspend fun ftExplain(index: String, query: V, args: ExplainArgs): String? = + override suspend fun ftExplain(index: String, query: String, args: ExplainArgs): String? = ops.ftExplain(index, query, args).awaitFirstOrNull() - override suspend fun ftList(): List = + override suspend fun ftList(): List = ops.ftList().asFlow().toList() diff --git a/src/main/kotlin/io/lettuce/core/api/coroutines/RedisCoroutinesCommandsImpl.kt b/src/main/kotlin/io/lettuce/core/api/coroutines/RedisCoroutinesCommandsImpl.kt index f27dbab341..cf1dc3c00b 100644 --- a/src/main/kotlin/io/lettuce/core/api/coroutines/RedisCoroutinesCommandsImpl.kt +++ b/src/main/kotlin/io/lettuce/core/api/coroutines/RedisCoroutinesCommandsImpl.kt @@ -56,7 +56,7 @@ open class RedisCoroutinesCommandsImpl( RedisTransactionalCoroutinesCommands by RedisTransactionalCoroutinesCommandsImpl(ops), RedisJsonCoroutinesCommands by RedisJsonCoroutinesCommandsImpl(ops), RedisVectorSetCoroutinesCommands by RedisVectorSetCoroutinesCommandsImpl(ops), - RediSearchCoroutinesCommands by RediSearchCoroutinesCommandsImpl(ops), + RediSearchCoroutinesCommands by RediSearchCoroutinesCommandsImpl(ops), RedisArrayCoroutinesCommands by RedisArrayCoroutinesCommandsImpl(ops), RedisBloomFilterCoroutinesCommands by RedisBloomFilterCoroutinesCommandsImpl(ops), RedisCuckooFilterCoroutinesCommands by RedisCuckooFilterCoroutinesCommandsImpl(ops), diff --git a/src/main/templates/io/lettuce/core/api/RediSearchCommands.java b/src/main/templates/io/lettuce/core/api/RediSearchCommands.java index 0ec3cf91b2..428b08eb9b 100644 --- a/src/main/templates/io/lettuce/core/api/RediSearchCommands.java +++ b/src/main/templates/io/lettuce/core/api/RediSearchCommands.java @@ -28,12 +28,11 @@ * ${intent} for RediSearch functionality * * @param Key type. - * @param Value type. * @author Tihomir Mateev * @see RediSearch * @since 6.8 */ -public interface RediSearchCommands { +public interface RediSearchCommands { /** * Create a new search index with the given name and field definitions using default settings. @@ -59,7 +58,7 @@ public interface RediSearchCommands { * @see #ftDropindex(String) */ @Experimental - String ftCreate(String index, List> fieldArgs); + String ftCreate(String index, List fieldArgs); /** * Create a new search index with the given name, custom configuration, and field definitions. @@ -98,7 +97,7 @@ public interface RediSearchCommands { * @see #ftDropindex(String) */ @Experimental - String ftCreate(String index, CreateArgs arguments, List> fieldArgs); + String ftCreate(String index, CreateArgs arguments, List fieldArgs); /** * Add an alias to a search index. @@ -275,7 +274,7 @@ public interface RediSearchCommands { * @see #ftCreate(String, CreateArgs, List) */ @Experimental - String ftAlter(String index, boolean skipInitialScan, List> fieldArgs); + String ftAlter(String index, boolean skipInitialScan, List fieldArgs); /** * Add new attributes to an existing search index. @@ -310,7 +309,7 @@ public interface RediSearchCommands { * @see #ftCreate(String, CreateArgs, List) */ @Experimental - String ftAlter(String index, List> fieldArgs); + String ftAlter(String index, List fieldArgs); /** * Return a distinct set of values indexed in a Tag field. @@ -366,7 +365,7 @@ public interface RediSearchCommands { * @see #ftCreate(String, CreateArgs, List) */ @Experimental - List ftTagvals(String index, String fieldName); + List ftTagvals(String index, String fieldName); /** * Perform spelling correction on a query, returning suggestions for misspelled terms. @@ -401,13 +400,13 @@ public interface RediSearchCommands { * @since 6.8 * @see FT.SPELLCHECK * @see Spellchecking - * @see #ftSpellcheck(String, Object, SpellCheckArgs) - * @see #ftDictadd(String, Object[]) - * @see #ftDictdel(String, Object[]) + * @see #ftSpellcheck(String, String, SpellCheckArgs) + * @see #ftDictadd(String, String[]) + * @see #ftDictdel(String, String[]) * @see #ftDictdump(String) */ @Experimental - SpellCheckResult ftSpellcheck(String index, V query); + SpellCheckResult ftSpellcheck(String index, String query); /** * Perform spelling correction on a query with additional options. @@ -438,13 +437,13 @@ public interface RediSearchCommands { * @since 6.8 * @see FT.SPELLCHECK * @see Spellchecking - * @see #ftSpellcheck(String, Object) - * @see #ftDictadd(String, Object[]) - * @see #ftDictdel(String, Object[]) + * @see #ftSpellcheck(String, String) + * @see #ftDictadd(String, String[]) + * @see #ftDictdel(String, String[]) * @see #ftDictdump(String) */ @Experimental - SpellCheckResult ftSpellcheck(String index, V query, SpellCheckArgs args); + SpellCheckResult ftSpellcheck(String index, String query, SpellCheckArgs args); /** * Add terms to a dictionary. @@ -474,11 +473,11 @@ public interface RediSearchCommands { * @since 6.8 * @see FT.DICTADD * @see Spellchecking - * @see #ftDictdel(String, Object[]) + * @see #ftDictdel(String, String[]) * @see #ftDictdump(String) */ @Experimental - Long ftDictadd(String dict, V... terms); + Long ftDictadd(String dict, String... terms); /** * Delete terms from a dictionary. @@ -497,11 +496,11 @@ public interface RediSearchCommands { * @return the number of terms that were deleted * @since 6.8 * @see FT.DICTDEL - * @see #ftDictadd(String, Object[]) + * @see #ftDictadd(String, String[]) * @see #ftDictdump(String) */ @Experimental - Long ftDictdel(String dict, V... terms); + Long ftDictdel(String dict, String... terms); /** * Dump all terms in a dictionary. @@ -518,11 +517,11 @@ public interface RediSearchCommands { * @return a list of all terms in the dictionary * @since 6.8 * @see FT.DICTDUMP - * @see #ftDictadd(String, Object[]) - * @see #ftDictdel(String, Object[]) + * @see #ftDictadd(String, String[]) + * @see #ftDictdel(String, String[]) */ @Experimental - List ftDictdump(String dict); + List ftDictdump(String dict); /** * Return the execution plan for a complex query. @@ -551,11 +550,11 @@ public interface RediSearchCommands { * @return the execution plan as a string * @since 6.8 * @see FT.EXPLAIN - * @see #ftExplain(String, Object, ExplainArgs) - * @see #ftSearch(String, Object) + * @see #ftExplain(String, String, ExplainArgs) + * @see #ftSearch(String, String) */ @Experimental - String ftExplain(String index, V query); + String ftExplain(String index, String query); /** * Return the execution plan for a complex query with additional options. @@ -582,11 +581,11 @@ public interface RediSearchCommands { * @return the execution plan as a string * @since 6.8 * @see FT.EXPLAIN - * @see #ftExplain(String, Object) - * @see #ftSearch(String, Object) + * @see #ftExplain(String, String) + * @see #ftSearch(String, String) */ @Experimental - String ftExplain(String index, V query, ExplainArgs args); + String ftExplain(String index, String query, ExplainArgs args); /** * Return a list of all existing indexes. @@ -618,11 +617,11 @@ public interface RediSearchCommands { * @return a list of index names * @since 6.8 * @see FT._LIST - * @see #ftCreate(String, CreateArgs, FieldArgs[]) + * @see #ftCreate(String, CreateArgs, List) * @see #ftDropindex(String) */ @Experimental - List ftList(); + List ftList(); /** * Dump synonym group contents. @@ -650,11 +649,11 @@ public interface RediSearchCommands { * @return a map where keys are synonym terms and values are lists of group IDs containing that synonym * @since 6.8 * @see FT.SYNDUMP - * @see #ftSynupdate(String, Object, Object[]) - * @see #ftSynupdate(String, Object, SynUpdateArgs, Object[]) + * @see #ftSynupdate(String, String, String[]) + * @see #ftSynupdate(String, String, SynUpdateArgs, String[]) */ @Experimental - Map> ftSyndump(String index); + Map> ftSyndump(String index); /** * Update a synonym group with additional terms. @@ -684,11 +683,11 @@ public interface RediSearchCommands { * @return OK if executed correctly * @since 6.8 * @see FT.SYNUPDATE - * @see #ftSynupdate(String, Object, SynUpdateArgs, Object[]) + * @see #ftSynupdate(String, String, SynUpdateArgs, String[]) * @see #ftSyndump(String) */ @Experimental - String ftSynupdate(String index, V synonymGroupId, V... terms); + String ftSynupdate(String index, String synonymGroupId, String... terms); /** * Update a synonym group with additional terms and options. @@ -716,11 +715,11 @@ public interface RediSearchCommands { * @return OK if executed correctly * @since 6.8 * @see FT.SYNUPDATE - * @see #ftSynupdate(String, Object, Object[]) + * @see #ftSynupdate(String, String, String[]) * @see #ftSyndump(String) */ @Experimental - String ftSynupdate(String index, V synonymGroupId, SynUpdateArgs args, V... terms); + String ftSynupdate(String index, String synonymGroupId, SynUpdateArgs args, String... terms); /** * Add a suggestion string to an auto-complete suggestion dictionary. @@ -751,13 +750,13 @@ public interface RediSearchCommands { * @return the current size of the suggestion dictionary after adding the suggestion * @since 6.8 * @see FT.SUGADD - * @see #ftSugadd(Object, Object, double, SugAddArgs) - * @see #ftSugget(Object, Object) - * @see #ftSugdel(Object, Object) - * @see #ftSuglen(Object) + * @see #ftSugadd(K, String, double, SugAddArgs) + * @see #ftSugget(K, String) + * @see #ftSugdel(K, String) + * @see #ftSuglen(K) */ @Experimental - Long ftSugadd(K key, V suggestion, double score); + Long ftSugadd(K key, String suggestion, double score); /** * Add a suggestion string to an auto-complete suggestion dictionary with additional options. @@ -778,13 +777,13 @@ public interface RediSearchCommands { * @return the current size of the suggestion dictionary after adding the suggestion * @since 6.8 * @see FT.SUGADD - * @see #ftSugadd(Object, Object, double) - * @see #ftSugget(Object, Object, SugGetArgs) - * @see #ftSugdel(Object, Object) - * @see #ftSuglen(Object) + * @see #ftSugadd(K, String, double) + * @see #ftSugget(K, String, SugGetArgs) + * @see #ftSugdel(K, String) + * @see #ftSuglen(K) */ @Experimental - Long ftSugadd(K key, V suggestion, double score, SugAddArgs args); + Long ftSugadd(K key, String suggestion, double score, SugAddArgs args); /** * Delete a string from a suggestion dictionary. @@ -803,12 +802,12 @@ public interface RediSearchCommands { * @return {@code true} if the string was found and deleted, {@code false} otherwise * @since 6.8 * @see FT.SUGDEL - * @see #ftSugadd(Object, Object, double) - * @see #ftSugget(Object, Object) - * @see #ftSuglen(Object) + * @see #ftSugadd(K, String, double) + * @see #ftSugget(K, String) + * @see #ftSuglen(K) */ @Experimental - Boolean ftSugdel(K key, V suggestion); + Boolean ftSugdel(K key, String suggestion); /** * Get completion suggestions for a prefix. @@ -827,13 +826,13 @@ public interface RediSearchCommands { * @return a list of suggestions matching the prefix * @since 6.8 * @see FT.SUGGET - * @see #ftSugget(Object, Object, SugGetArgs) - * @see #ftSugadd(Object, Object, double) - * @see #ftSugdel(Object, Object) - * @see #ftSuglen(Object) + * @see #ftSugget(K, String, SugGetArgs) + * @see #ftSugadd(K, String, double) + * @see #ftSugdel(K, String) + * @see #ftSuglen(K) */ @Experimental - List> ftSugget(K key, V prefix); + List ftSugget(K key, String prefix); /** * Get completion suggestions for a prefix with additional options. @@ -853,13 +852,13 @@ public interface RediSearchCommands { * @return a list of suggestions matching the prefix, optionally with scores and payloads * @since 6.8 * @see FT.SUGGET - * @see #ftSugget(Object, Object) - * @see #ftSugadd(Object, Object, double, SugAddArgs) - * @see #ftSugdel(Object, Object) - * @see #ftSuglen(Object) + * @see #ftSugget(K, String) + * @see #ftSugadd(K, String, double, SugAddArgs) + * @see #ftSugdel(K, String) + * @see #ftSuglen(K) */ @Experimental - List> ftSugget(K key, V prefix, SugGetArgs args); + List ftSugget(K key, String prefix, SugGetArgs args); /** * Get the size of an auto-complete suggestion dictionary. @@ -876,9 +875,9 @@ public interface RediSearchCommands { * @return the current size of the suggestion dictionary * @since 6.8 * @see FT.SUGLEN - * @see #ftSugadd(Object, Object, double) - * @see #ftSugget(Object, Object) - * @see #ftSugdel(Object, Object) + * @see #ftSugadd(K, String, double) + * @see #ftSugget(K, String) + * @see #ftSugdel(K, String) */ @Experimental Long ftSuglen(K key); @@ -970,10 +969,10 @@ public interface RediSearchCommands { * @see Query syntax * @see SearchReply * @see SearchArgs - * @see #ftSearch(String, Object, SearchArgs) + * @see #ftSearch(String, String, SearchArgs) */ @Experimental - SearchReply ftSearch(String index, V query); + SearchReply ftSearch(String index, String query); /** * Search the index with a textual query using advanced search options and filters. @@ -1021,23 +1020,23 @@ public interface RediSearchCommands { * @see Advanced concepts * @see SearchReply * @see SearchArgs - * @see #ftSearch(String, Object) + * @see #ftSearch(String, String) */ @Experimental - SearchReply ftSearch(String index, V query, SearchArgs args); + SearchReply ftSearch(String index, String query, SearchArgs args); /** * Run a search query on an index and perform basic aggregate transformations using default options. * *

    * This command executes a search query and applies aggregation operations to transform and analyze the results. Unlike - * {@link #ftSearch(String, Object)}, which returns individual documents, FT.AGGREGATE processes the result set through a + * {@link #ftSearch(String, String)}, which returns individual documents, FT.AGGREGATE processes the result set through a * pipeline of transformations to produce analytical insights, summaries, and computed values. *

    * *

    * This basic variant uses default aggregation behavior without additional pipeline operations. For advanced aggregations - * with grouping, sorting, filtering, and custom transformations, use {@link #ftAggregate(String, Object, AggregateArgs)}. + * with grouping, sorting, filtering, and custom transformations, use {@link #ftAggregate(String, String, AggregateArgs)}. *

    * *

    @@ -1063,10 +1062,10 @@ public interface RediSearchCommands { * "https://redis.io/docs/latest/develop/interact/search-and-query/advanced-concepts/aggregations/">Aggregations * @see SearchReply * @see AggregateArgs - * @see #ftAggregate(String, Object, AggregateArgs) + * @see #ftAggregate(String, String, AggregateArgs) */ @Experimental - AggregationReply ftAggregate(String index, V query); + AggregationReply ftAggregate(String index, String query); /** * Run a search query on an index and perform advanced aggregate transformations with a processing pipeline. @@ -1118,18 +1117,18 @@ public interface RediSearchCommands { * API * @see SearchReply * @see AggregateArgs - * @see #ftAggregate(String, Object) + * @see #ftAggregate(String, String) * @see #ftCursorread(String, Cursor) */ @Experimental - AggregationReply ftAggregate(String index, V query, AggregateArgs args); + AggregationReply ftAggregate(String index, String query, AggregateArgs args); /** * Read next results from an existing cursor and optionally override the batch size. * *

    * This command is used to read the next batch of results from a cursor that was created by - * {@link #ftAggregate(String, Object, AggregateArgs)} with the {@code WITHCURSOR} option. Cursors provide an efficient way + * {@link #ftAggregate(String, String, AggregateArgs)} with the {@code WITHCURSOR} option. Cursors provide an efficient way * to iterate through large result sets without loading all results into memory at once. *

    * @@ -1152,17 +1151,17 @@ public interface RediSearchCommands { * "https://redis.io/docs/latest/develop/interact/search-and-query/advanced-concepts/aggregations/#cursor-api">Cursor * API * @see AggregationReply - * @see #ftAggregate(String, Object, AggregateArgs) + * @see #ftAggregate(String, String, AggregateArgs) */ @Experimental - AggregationReply ftCursorread(String index, Cursor cursor, int count); + AggregationReply ftCursorread(String index, Cursor cursor, int count); /** * Read next results from an existing cursor using the default batch size. * *

    * This command is used to read the next batch of results from a cursor created by - * {@link #ftAggregate(String, Object, AggregateArgs)} with the {@code WITHCURSOR} option. This variant uses the default + * {@link #ftAggregate(String, String, AggregateArgs)} with the {@code WITHCURSOR} option. This variant uses the default * batch size that was specified in the original {@code FT.AGGREGATE} command's {@code WITHCURSOR} clause. *

    * @@ -1184,16 +1183,16 @@ public interface RediSearchCommands { * "https://redis.io/docs/latest/develop/interact/search-and-query/advanced-concepts/aggregations/#cursor-api">Cursor * API * @see AggregationReply - * @see #ftAggregate(String, Object, AggregateArgs) + * @see #ftAggregate(String, String, AggregateArgs) */ @Experimental - AggregationReply ftCursorread(String index, Cursor cursor); + AggregationReply ftCursorread(String index, Cursor cursor); /** * Delete a cursor and free its associated resources. * *

    - * This command is used to explicitly delete a cursor created by {@link #ftAggregate(String, Object, AggregateArgs)} with + * This command is used to explicitly delete a cursor created by {@link #ftAggregate(String, String, AggregateArgs)} with * the {@code WITHCURSOR} option. Deleting a cursor frees up server resources and should be done when you no longer need to * read more results from the cursor. *

    @@ -1221,7 +1220,7 @@ public interface RediSearchCommands { * @see Cursor * API - * @see #ftAggregate(String, Object, AggregateArgs) + * @see #ftAggregate(String, String, AggregateArgs) * @see #ftCursorread(String, Cursor) * @see #ftCursorread(String, Cursor, int) */ @@ -1240,6 +1239,6 @@ public interface RediSearchCommands { * @since 7.2 */ @Experimental - HybridReply ftHybrid(String index, HybridArgs args); + HybridReply ftHybrid(String index, HybridArgs args); } diff --git a/src/test/java/io/lettuce/core/RediSearchCommandBuilderUnitTests.java b/src/test/java/io/lettuce/core/RediSearchCommandBuilderUnitTests.java index e650580072..4df2373e14 100644 --- a/src/test/java/io/lettuce/core/RediSearchCommandBuilderUnitTests.java +++ b/src/test/java/io/lettuce/core/RediSearchCommandBuilderUnitTests.java @@ -92,12 +92,11 @@ class RediSearchCommandBuilderUnitTests { // FT.CREATE idx ON HASH PREFIX 1 blog:post: SCHEMA title TEXT SORTABLE published_at NUMERIC SORTABLE category TAG SORTABLE @Test void shouldCorrectlyConstructFtCreateCommandScenario1() { - FieldArgs fieldArgs1 = TextFieldArgs. builder().name(FIELD1_NAME).sortable().build(); - FieldArgs fieldArgs2 = NumericFieldArgs. builder().name(FIELD2_NAME).sortable().build(); - FieldArgs fieldArgs3 = TagFieldArgs. builder().name(FIELD3_NAME).sortable().build(); + FieldArgs fieldArgs1 = TextFieldArgs.builder().name(FIELD1_NAME).sortable().build(); + FieldArgs fieldArgs2 = NumericFieldArgs.builder().name(FIELD2_NAME).sortable().build(); + FieldArgs fieldArgs3 = TagFieldArgs.builder().name(FIELD3_NAME).sortable().build(); - CreateArgs createArgs = CreateArgs. builder().withPrefix(PREFIX) - .on(CreateArgs.TargetType.HASH).build(); + CreateArgs createArgs = CreateArgs.builder().withPrefix(PREFIX).on(CreateArgs.TargetType.HASH).build(); Command command = builder.ftCreate(MY_KEY, createArgs, Arrays.asList(fieldArgs1, fieldArgs2, fieldArgs3)); ByteBuf buf = Unpooled.directBuffer(); @@ -128,11 +127,10 @@ void shouldCorrectlyConstructFtCreateCommandScenario1() { // FT.CREATE idx ON HASH PREFIX 1 blog:post: SCHEMA sku AS sku_text TEXT sku AS sku_tag TAG SORTABLE @Test void shouldCorrectlyConstructFtCreateCommandScenario2() { - FieldArgs fieldArgs1 = TextFieldArgs. builder().name(FIELD4_NAME).as(FIELD4_ALIAS1).build(); - FieldArgs fieldArgs2 = TagFieldArgs. builder().name(FIELD4_NAME).as(FIELD4_ALIAS2).sortable().build(); + FieldArgs fieldArgs1 = TextFieldArgs.builder().name(FIELD4_NAME).as(FIELD4_ALIAS1).build(); + FieldArgs fieldArgs2 = TagFieldArgs.builder().name(FIELD4_NAME).as(FIELD4_ALIAS2).sortable().build(); - CreateArgs createArgs = CreateArgs. builder().withPrefix(PREFIX) - .on(CreateArgs.TargetType.HASH).build(); + CreateArgs createArgs = CreateArgs.builder().withPrefix(PREFIX).on(CreateArgs.TargetType.HASH).build(); Command command = builder.ftCreate(MY_KEY, createArgs, Arrays.asList(fieldArgs1, fieldArgs2)); ByteBuf buf = Unpooled.directBuffer(); command.encode(buf); @@ -248,7 +246,7 @@ void shouldCorrectlyConstructFtTagvalsCommand() { // FT.SPELLCHECK index query @Test void shouldCorrectlyConstructFtSpellcheckCommand() { - Command> command = builder.ftSpellcheck(MY_KEY, "hello wrold"); + Command command = builder.ftSpellcheck(MY_KEY, "hello wrold"); ByteBuf buf = Unpooled.directBuffer(); command.encode(buf); @@ -263,9 +261,8 @@ void shouldCorrectlyConstructFtSpellcheckCommand() { // FT.SPELLCHECK index query DISTANCE 2 TERMS INCLUDE dict term1 term2 DIALECT 1 @Test void shouldCorrectlyConstructFtSpellcheckCommandWithArgs() { - SpellCheckArgs args = SpellCheckArgs.Builder. distance(2) - .termsInclude("dict", "term1", "term2").dialect(1); - Command> command = builder.ftSpellcheck(MY_KEY, "hello wrold", args); + SpellCheckArgs args = SpellCheckArgs.Builder.distance(2).termsInclude("dict", "term1", "term2").dialect(1); + Command command = builder.ftSpellcheck(MY_KEY, "hello wrold", args); ByteBuf buf = Unpooled.directBuffer(); command.encode(buf); @@ -350,7 +347,7 @@ void shouldCorrectlyConstructFtExplainCommand() { // FT.EXPLAIN index query DIALECT 1 @Test void shouldCorrectlyConstructFtExplainCommandWithArgs() { - ExplainArgs args = ExplainArgs.Builder.dialect(QueryDialects.DIALECT1); + ExplainArgs args = ExplainArgs.Builder.dialect(QueryDialects.DIALECT1); Command command = builder.ftExplain(MY_KEY, "hello world", args); ByteBuf buf = Unpooled.directBuffer(); command.encode(buf); @@ -412,7 +409,7 @@ void shouldCorrectlyConstructFtSynupdateCommand() { // FT.SYNUPDATE index synonymGroupId SKIPINITIALSCAN term1 term2 @Test void shouldCorrectlyConstructFtSynupdateCommandWithArgs() { - SynUpdateArgs args = SynUpdateArgs.Builder.skipInitialScan(); + SynUpdateArgs args = SynUpdateArgs.Builder.skipInitialScan(); Command command = builder.ftSynupdate(MY_KEY, "group1", args, "term1", "term2"); ByteBuf buf = Unpooled.directBuffer(); command.encode(buf); @@ -447,7 +444,7 @@ void shouldCorrectlyConstructFtSugaddCommand() { // FT.SUGADD key string score INCR PAYLOAD payload @Test void shouldCorrectlyConstructFtSugaddCommandWithArgs() { - SugAddArgs args = SugAddArgs.Builder. incr().payload("test-payload"); + SugAddArgs args = SugAddArgs.Builder.incr().payload("test-payload"); Command command = builder.ftSugadd(MY_KEY, "suggestion", 1.0, args); ByteBuf buf = Unpooled.directBuffer(); command.encode(buf); @@ -482,7 +479,7 @@ void shouldCorrectlyConstructFtSugdelCommand() { // FT.SUGGET key prefix @Test void shouldCorrectlyConstructFtSuggetCommand() { - Command>> command = builder.ftSugget(MY_KEY, "pre"); + Command> command = builder.ftSugget(MY_KEY, "pre"); ByteBuf buf = Unpooled.directBuffer(); command.encode(buf); @@ -497,8 +494,8 @@ void shouldCorrectlyConstructFtSuggetCommand() { // FT.SUGGET key prefix FUZZY WITHSCORES WITHPAYLOADS MAX 10 @Test void shouldCorrectlyConstructFtSuggetCommandWithArgs() { - SugGetArgs args = SugGetArgs.Builder. fuzzy().withScores().withPayloads().max(10); - Command>> command = builder.ftSugget(MY_KEY, "pre", args); + SugGetArgs args = SugGetArgs.Builder.fuzzy().withScores().withPayloads().max(10); + Command> command = builder.ftSugget(MY_KEY, "pre", args); ByteBuf buf = Unpooled.directBuffer(); command.encode(buf); @@ -532,7 +529,7 @@ void shouldCorrectlyConstructFtSuglenCommand() { // FT.ALTER idx SCHEMA ADD title TEXT @Test void shouldCorrectlyConstructFtAlterCommand() { - FieldArgs fieldArgs = TextFieldArgs. builder().name(FIELD1_NAME).build(); + FieldArgs fieldArgs = TextFieldArgs.builder().name(FIELD1_NAME).build(); Command command = builder.ftAlter(MY_KEY, false, Collections.singletonList(fieldArgs)); ByteBuf buf = Unpooled.directBuffer(); @@ -552,8 +549,8 @@ void shouldCorrectlyConstructFtAlterCommand() { // FT.ALTER idx SKIPINITIALSCAN SCHEMA ADD title TEXT published_at NUMERIC SORTABLE @Test void shouldCorrectlyConstructFtAlterCommandWithSkipInitialScan() { - FieldArgs fieldArgs1 = TextFieldArgs. builder().name(FIELD1_NAME).build(); - FieldArgs fieldArgs2 = NumericFieldArgs. builder().name(FIELD2_NAME).sortable().build(); + FieldArgs fieldArgs1 = TextFieldArgs.builder().name(FIELD1_NAME).build(); + FieldArgs fieldArgs2 = NumericFieldArgs.builder().name(FIELD2_NAME).sortable().build(); Command command = builder.ftAlter(MY_KEY, true, Arrays.asList(fieldArgs1, fieldArgs2)); ByteBuf buf = Unpooled.directBuffer(); @@ -576,8 +573,8 @@ void shouldCorrectlyConstructFtAlterCommandWithSkipInitialScan() { @Test void shouldCorrectlyConstructFtSearchCommandNoSearchArgs() { - Command> command = builder.ftSearch(MY_KEY, MY_QUERY, - SearchArgs. builder().build()); + Command> command = builder.ftSearch(MY_KEY, MY_QUERY, + SearchArgs. builder().build()); ByteBuf buf = Unpooled.directBuffer(); command.encode(buf); @@ -593,10 +590,9 @@ void shouldCorrectlyConstructFtSearchCommandNoSearchArgs() { @Test void shouldCorrectlyConstructFtSearchCommandLimit() { - SearchArgs searchArgs = SearchArgs. builder().limit(10, 10).returnField("title") - .build(); + SearchArgs searchArgs = SearchArgs. builder().limit(10, 10).returnField("title").build(); - Command> command = builder.ftSearch(MY_KEY, MY_QUERY, searchArgs); + Command> command = builder.ftSearch(MY_KEY, MY_QUERY, searchArgs); ByteBuf buf = Unpooled.directBuffer(); command.encode(buf); @@ -618,10 +614,10 @@ void shouldCorrectlyConstructFtSearchCommandLimit() { @Test void shouldCorrectlyConstructFtSearchCommandParams() { - SearchArgs searchArgs = SearchArgs. builder() - .param("poly", "POLYGON((2 2, 2 50, 50 50, 50 2, 2 2))").build(); + SearchArgs searchArgs = SearchArgs. builder().param("poly", "POLYGON((2 2, 2 50, 50 50, 50 2, 2 2))") + .build(); - Command> command = builder.ftSearch(MY_KEY, MY_QUERY, searchArgs); + Command> command = builder.ftSearch(MY_KEY, MY_QUERY, searchArgs); ByteBuf buf = Unpooled.directBuffer(); command.encode(buf); @@ -641,7 +637,7 @@ void shouldCorrectlyConstructFtSearchCommandParams() { @Test void shouldCorrectlyConstructFtAggregateCommandBasic() { - Command> command = builder.ftAggregate(MY_KEY, MY_QUERY, null); + Command> command = builder.ftAggregate(MY_KEY, MY_QUERY, null); ByteBuf buf = Unpooled.directBuffer(); command.encode(buf); @@ -657,18 +653,16 @@ void shouldCorrectlyConstructFtAggregateCommandBasic() { void shouldMaintainPipelineOperationOrder() { // Test that pipeline operations (GROUPBY, SORTBY, APPLY, FILTER, LIMIT) // are output in the order specified by the user, not in a fixed order - AggregateArgs aggregateArgs = AggregateArgs. builder()// + AggregateArgs aggregateArgs = AggregateArgs.builder()// .apply("@price * @quantity", "total_value")// First operation .filter("@total_value > 100")// Second operation - .groupBy(AggregateArgs.GroupBy. of("category") - .reduce(AggregateArgs.Reducer. count().as("count")))// Third + .groupBy(AggregateArgs.GroupBy.of("category").reduce(AggregateArgs.Reducer.count().as("count")))// Third // operation .limit(0, 5)// Fourth operation .sortBy(AggregateArgs.SortBy.of("count", AggregateArgs.SortDirection.DESC))// Fifth operation .build(); - Command> command = builder.ftAggregate(MY_KEY, MY_QUERY, - aggregateArgs); + Command> command = builder.ftAggregate(MY_KEY, MY_QUERY, aggregateArgs); ByteBuf buf = Unpooled.directBuffer(); command.encode(buf); @@ -687,13 +681,80 @@ void shouldMaintainPipelineOperationOrder() { assertThat(buf.toString(StandardCharsets.UTF_8)).isEqualTo(result); } + @Test + void shouldCorrectlyConstructFtAggregateCollectReducerWithExplicitFields() { + AggregateArgs aggregateArgs = AggregateArgs.builder()// + .groupBy(AggregateArgs.GroupBy.of("color").reduce(AggregateArgs.Reducer.collect()// + .fields("fruit", "sweetness")// + .sortBy(new AggregateArgs.SortProperty("sweetness", AggregateArgs.SortDirection.DESC))// + .limit(0, 2)// + .as("top")))// + .build(); + + Command> command = builder.ftAggregate(MY_KEY, MY_QUERY, aggregateArgs); + ByteBuf buf = Unpooled.directBuffer(); + command.encode(buf); + + // REDUCE COLLECT 11 FIELDS 2 @fruit @sweetness SORTBY 2 @sweetness DESC LIMIT 0 2 AS top + String result = "*24\r\n" + "$12\r\n" + "FT.AGGREGATE\r\n" + "$3\r\n" + "idx\r\n" + "$1\r\n" + "*\r\n"// + + "$7\r\n" + "GROUPBY\r\n" + "$1\r\n" + "1\r\n" + "$6\r\n" + "@color\r\n"// + + "$6\r\n" + "REDUCE\r\n" + "$7\r\n" + "COLLECT\r\n" + "$2\r\n" + "11\r\n"// + + "$6\r\n" + "FIELDS\r\n" + "$1\r\n" + "2\r\n" + "$6\r\n" + "@fruit\r\n" + "$10\r\n" + "@sweetness\r\n"// + + "$6\r\n" + "SORTBY\r\n" + "$1\r\n" + "2\r\n" + "$10\r\n" + "@sweetness\r\n" + "$4\r\n" + "DESC\r\n"// + + "$5\r\n" + "LIMIT\r\n" + "$1\r\n" + "0\r\n" + "$1\r\n" + "2\r\n"// + + "$2\r\n" + "AS\r\n" + "$3\r\n" + "top\r\n"// + + "$7\r\n" + "DIALECT\r\n" + "$1\r\n2\r\n";// + + assertThat(buf.toString(StandardCharsets.UTF_8)).isEqualTo(result); + } + + @Test + void shouldCorrectlyConstructFtAggregateCollectReducerWithFieldsAll() { + AggregateArgs aggregateArgs = AggregateArgs.builder()// + .loadAll()// + .groupBy(AggregateArgs.GroupBy.of("color").reduce(AggregateArgs.Reducer.collect()// + .fieldsAll()// + .sortByDesc("sweetness")// + .limit(2)))// + .build(); + + Command> command = builder.ftAggregate(MY_KEY, MY_QUERY, aggregateArgs); + ByteBuf buf = Unpooled.directBuffer(); + command.encode(buf); + + // LOAD * GROUPBY 1 @color REDUCE COLLECT 9 FIELDS * SORTBY 2 @sweetness DESC LIMIT 0 2 + String result = "*22\r\n" + "$12\r\n" + "FT.AGGREGATE\r\n" + "$3\r\n" + "idx\r\n" + "$1\r\n" + "*\r\n"// + + "$4\r\n" + "LOAD\r\n" + "$1\r\n" + "*\r\n"// + + "$7\r\n" + "GROUPBY\r\n" + "$1\r\n" + "1\r\n" + "$6\r\n" + "@color\r\n"// + + "$6\r\n" + "REDUCE\r\n" + "$7\r\n" + "COLLECT\r\n" + "$1\r\n" + "9\r\n"// + + "$6\r\n" + "FIELDS\r\n" + "$1\r\n" + "*\r\n"// + + "$6\r\n" + "SORTBY\r\n" + "$1\r\n" + "2\r\n" + "$10\r\n" + "@sweetness\r\n" + "$4\r\n" + "DESC\r\n"// + + "$5\r\n" + "LIMIT\r\n" + "$1\r\n" + "0\r\n" + "$1\r\n" + "2\r\n"// + + "$7\r\n" + "DIALECT\r\n" + "$1\r\n2\r\n";// + + assertThat(buf.toString(StandardCharsets.UTF_8)).isEqualTo(result); + } + + @Test + void ftAggregateCollectReducerShouldValidateUsage() { + assertThatThrownBy(() -> AggregateArgs.Reducer.collect().fields("a").fieldsAll()) + .isInstanceOf(IllegalStateException.class); + assertThatThrownBy(() -> AggregateArgs.Reducer.collect().fieldsAll().fields("a")) + .isInstanceOf(IllegalStateException.class); + assertThatThrownBy(() -> AggregateArgs.Reducer.collect().limit(-1, 5)).isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> { + AggregateArgs args = AggregateArgs.builder() + .groupBy(AggregateArgs.GroupBy.of("color").reduce(AggregateArgs.Reducer.collect().as("top"))).build(); + builder.ftAggregate(MY_KEY, MY_QUERY, args).encode(Unpooled.directBuffer()); + }).isInstanceOf(IllegalStateException.class); + } + @Test void shouldCorrectlyConstructFtAggregateCommandWithArgs() { - AggregateArgs aggregateArgs = AggregateArgs. builder()// + AggregateArgs aggregateArgs = AggregateArgs.builder()// .verbatim()// .load("title")// - .groupBy(AggregateArgs.GroupBy. of("category") - .reduce(AggregateArgs.Reducer. count().as("count")))// + .groupBy(AggregateArgs.GroupBy.of("category").reduce(AggregateArgs.Reducer.count().as("count")))// .sortBy(AggregateArgs.SortBy.of("count", AggregateArgs.SortDirection.DESC))// .apply(AggregateArgs.Apply.of("@title", "title_upper"))// .limit(0, 10)// @@ -705,8 +766,7 @@ void shouldCorrectlyConstructFtAggregateCommandWithArgs() { .dialect(QueryDialects.DIALECT2) // .build(); - Command> command = builder.ftAggregate(MY_KEY, MY_QUERY, - aggregateArgs); + Command> command = builder.ftAggregate(MY_KEY, MY_QUERY, aggregateArgs); ByteBuf buf = Unpooled.directBuffer(); command.encode(buf); @@ -732,7 +792,7 @@ void shouldCorrectlyConstructFtAggregateCommandWithArgs() { @Test void shouldCorrectlyConstructFtCursorreadCommandWithCount() { - Command> command = builder.ftCursorread("idx", 123L, 10); + Command> command = builder.ftCursorread("idx", 123L, 10); ByteBuf buf = Unpooled.directBuffer(); command.encode(buf); @@ -749,7 +809,7 @@ void shouldCorrectlyConstructFtCursorreadCommandWithCount() { @Test void shouldCorrectlyConstructFtCursorreadCommandWithoutCount() { - Command> command = builder.ftCursorread("idx", 456L, -1); + Command> command = builder.ftCursorread("idx", 456L, -1); ByteBuf buf = Unpooled.directBuffer(); command.encode(buf); @@ -779,14 +839,13 @@ void shouldCorrectlyConstructFtCursordelCommand() { @Test void returnFieldsWithAlias() { - SearchArgs options = SearchArgs. builder().returnField("as_is") - .returnField("$.field", "alias").build(); + SearchArgs options = SearchArgs. builder().returnField("as_is").returnField("$.field", "alias").build(); CommandArgs args = new CommandArgs<>(new StringCodec()); options.build(args); - // buggy implementation returns "RETURN 2 key key<$.field> key DIALECT " - assertThat("RETURN 4 key key<$.field> AS key DIALECT 2").isEqualTo(args.toCommandString()); + // Both the RETURN field name and the AS alias are schema identifiers, sent as raw Strings (not codec-encoded). + assertThat("RETURN 4 as_is $.field AS alias DIALECT 2").isEqualTo(args.toCommandString()); } @Test @@ -794,49 +853,48 @@ void shouldCorrectlyConstructFtHybridCommand() { byte[] queryVector = floatArrayToByteArray(new float[] { 0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f, 0.7f, 0.8f, 0.9f, 1.0f }); - HybridArgs hybridArgs = HybridArgs. builder() - .search(HybridSearchArgs. builder().query("@category:{electronics} smartphone camera") + HybridArgs hybridArgs = HybridArgs.builder() + .search(HybridSearchArgs.builder().query("@category:{electronics} smartphone camera") .scorer(Scorers.tfidfDocNorm()).scoreAlias("text_score").build()) - .vectorSearch(HybridVectorArgs. builder().field("@image_embedding").vector("$vec") - .method(HybridVectorArgs.Knn.of(20).efRuntime(150)).filter("@brand:{apple|samsung|google}") - .scoreAlias("vector_score").build()) - .combine(Combiners. linear().alpha(0.7).beta(0.3).window(26)) - .postProcessing(PostProcessingArgs. builder().load("@price", "@brand", "@category") - .groupBy(GroupBy. of("@brand").reduce(Reducers. sum("@price").as("sum")) - .reduce(Reducers. count().as("count"))) - .sortBy(SortBy.of(new SortProperty<>("@sum", SortDirection.ASC))) + .vectorSearch(HybridVectorArgs + .builder().field("@image_embedding").vector("$vec").method(HybridVectorArgs.Knn.of(20).efRuntime(150)) + .filter("@brand:{apple|samsung|google}").scoreAlias("vector_score").build()) + .combine(Combiners.linear().alpha(0.7).beta(0.3).window(26)) + .postProcessing(PostProcessingArgs.builder().load("@price", "@brand", "@category") + .groupBy(GroupBy.of("@brand").reduce(Reducers.sum("@price").as("sum")) + .reduce(Reducers.count().as("count"))) + .sortBy(SortBy.of(new SortProperty("@sum", SortDirection.ASC))) .apply(Apply.of("@sum * 0.9", "discounted_price")).filter(Filter.of("@sum > 700")) .limit(Limit.of(0, 20)).build()) .param("vec", queryVector).param("discount_rate", "0.9").build(); - Command> command = builder.ftHybrid("idx:ecommerce", hybridArgs); + Command> command = builder.ftHybrid("idx:ecommerce", hybridArgs); String args = command.getArgs().toCommandString(); // Vector data is passed via PARAMS, referenced as $vec in VSIM - assertThat(args).contains("VSIM key<@image_embedding> value<$vec>"); + assertThat(args).contains("VSIM @image_embedding $vec"); assertThat(args).contains("PARAMS 4"); - assertThat(args).contains("key value<0.9>"); + assertThat(args).contains("discount_rate 0.9"); // Verify LOAD is emitted with count prefix (LOAD 3 @price @brand @category) - assertThat(args).contains("LOAD 3 key<@price> key<@brand> key<@category>"); + assertThat(args).contains("LOAD 3 @price @brand @category"); } @Test void postProcessingArgsWithLoadFieldsShouldEmitLoadWithCount() { - PostProcessingArgs postProcessingArgs = PostProcessingArgs. builder() - .load("@price", "@brand", "@category").build(); + PostProcessingArgs postProcessingArgs = PostProcessingArgs.builder().load("@price", "@brand", "@category").build(); CommandArgs args = new CommandArgs<>(new StringCodec()); postProcessingArgs.build(args); // Should emit: LOAD 3 @price @brand @category - assertThat(args.toCommandString()).isEqualTo("LOAD 3 key<@price> key<@brand> key<@category>"); + assertThat(args.toCommandString()).isEqualTo("LOAD 3 @price @brand @category"); } @Test void postProcessingArgsWithLoadAllShouldEmitLoadStar() { - PostProcessingArgs postProcessingArgs = PostProcessingArgs. builder().loadAll().build(); + PostProcessingArgs postProcessingArgs = PostProcessingArgs.builder().loadAll().build(); CommandArgs args = new CommandArgs<>(new StringCodec()); postProcessingArgs.build(args); @@ -847,8 +905,7 @@ void postProcessingArgsWithLoadAllShouldEmitLoadStar() { @Test void postProcessingArgsWithoutLoadShouldNotEmitLoad() { - PostProcessingArgs postProcessingArgs = PostProcessingArgs. builder() - .filter(Filter.of("@price > 100")).build(); + PostProcessingArgs postProcessingArgs = PostProcessingArgs.builder().filter(Filter.of("@price > 100")).build(); CommandArgs args = new CommandArgs<>(new StringCodec()); postProcessingArgs.build(args); @@ -860,9 +917,9 @@ void postProcessingArgsWithoutLoadShouldNotEmitLoad() { @Test void postProcessingArgsWithOperationsOnlyShouldNotEmitLoad() { - PostProcessingArgs postProcessingArgs = PostProcessingArgs. builder() - .groupBy(GroupBy. of("@category").reduce(Reducers. count().as("count"))) - .sortBy(SortBy.of(new SortProperty<>("@count", SortDirection.DESC))).limit(Limit.of(0, 10)).build(); + PostProcessingArgs postProcessingArgs = PostProcessingArgs.builder() + .groupBy(GroupBy.of("@category").reduce(Reducers.count().as("count"))) + .sortBy(SortBy.of(new SortProperty("@count", SortDirection.DESC))).limit(Limit.of(0, 10)).build(); CommandArgs args = new CommandArgs<>(new StringCodec()); postProcessingArgs.build(args); @@ -878,8 +935,8 @@ void postProcessingArgsWithOperationsOnlyShouldNotEmitLoad() { @Test void loadWithAsteriskShouldThrowException() { // Passing "*" to load() should throw an exception directing users to use loadAll() instead - assertThatThrownBy(() -> PostProcessingArgs. builder().load("*")) - .isInstanceOf(IllegalArgumentException.class).hasMessageContaining("loadAll()"); + assertThatThrownBy(() -> PostProcessingArgs.builder().load("*")).isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("loadAll()"); } @Test @@ -888,7 +945,7 @@ void combinerWithScoreAliasShouldCountYieldScoreAsTokens() { Combiners. rrf().window(20).constant(60).as("score").build(args); // The count after RRF must cover the trailing YIELD_SCORE_AS pair, otherwise Redis rejects the alias - assertThat(args.toCommandString()).isEqualTo("RRF 6 WINDOW 20 CONSTANT 60.0 YIELD_SCORE_AS key"); + assertThat(args.toCommandString()).isEqualTo("RRF 6 WINDOW 20 CONSTANT 60.0 YIELD_SCORE_AS score"); } @Test @@ -896,7 +953,7 @@ void linearCombinerWithScoreAliasShouldCountYieldScoreAsTokens() { CommandArgs args = new CommandArgs<>(new StringCodec()); Combiners. linear().alpha(0.7).beta(0.3).as("combined_score").build(args); - assertThat(args.toCommandString()).isEqualTo("LINEAR 6 ALPHA 0.7 BETA 0.3 YIELD_SCORE_AS key"); + assertThat(args.toCommandString()).isEqualTo("LINEAR 6 ALPHA 0.7 BETA 0.3 YIELD_SCORE_AS combined_score"); } @Test @@ -905,7 +962,7 @@ void defaultCombinerWithScoreAliasShouldCountYieldScoreAsTokens() { Combiners. rrf().as("score").build(args); // No combiner parameters, so the count reflects only the YIELD_SCORE_AS pair - assertThat(args.toCommandString()).isEqualTo("RRF 2 YIELD_SCORE_AS key"); + assertThat(args.toCommandString()).isEqualTo("RRF 2 YIELD_SCORE_AS score"); } @Test diff --git a/src/test/java/io/lettuce/core/cluster/RedisAdvancedClusterAsyncCommandsImplTest.java b/src/test/java/io/lettuce/core/cluster/RedisAdvancedClusterAsyncCommandsImplTest.java index 7aebaf53e4..0478a68d1d 100644 --- a/src/test/java/io/lettuce/core/cluster/RedisAdvancedClusterAsyncCommandsImplTest.java +++ b/src/test/java/io/lettuce/core/cluster/RedisAdvancedClusterAsyncCommandsImplTest.java @@ -105,20 +105,19 @@ void setup() { @Test void ftAggregate_stampsNodeId_whenCursorCreated() { - AggregateArgs args = AggregateArgs. builder() - .withCursor(AggregateArgs.WithCursor.of(1L)).build(); + AggregateArgs args = AggregateArgs.builder().withCursor(AggregateArgs.WithCursor.of(1L)).build(); - AggregationReply replyWithCursor = new AggregationReply<>(); + AggregationReply replyWithCursor = new AggregationReply<>(); replyWithCursor.setCursor(AggregationReply.Cursor.of(42L, null)); - CompletableFuture> cf = new CompletableFuture<>(); + CompletableFuture> cf = new CompletableFuture<>(); cf.complete(replyWithCursor); - RedisFuture> nodeFuture = new PipelinedRedisFuture<>(cf); + RedisFuture> nodeFuture = new PipelinedRedisFuture<>(cf); when(nodeAsync.ftAggregate(anyString(), anyString(), any())).thenReturn(nodeFuture); when(nodeAsync.clusterMyId()).thenReturn(new PipelinedRedisFuture<>(CompletableFuture.completedFuture("node-1"))); - AggregationReply out = async.ftAggregate("idx", "*", args).toCompletableFuture().join(); + AggregationReply out = async.ftAggregate("idx", "*", args).toCompletableFuture().join(); assertThat(out.getCursor()).isPresent(); assertThat(out.getCursor().get().getCursorId()).isEqualTo(42L); @@ -136,13 +135,13 @@ void ftCursordel_throwsWhenMissingNodeId() { void ftCursorread_routesToNodeIdWithREAD_andStampsNodeId() { AggregationReply.Cursor cursor = AggregationReply.Cursor.of(7L, "node-1"); - AggregationReply reply = new AggregationReply<>(); + AggregationReply reply = new AggregationReply<>(); reply.setCursor(AggregationReply.Cursor.of(7L, null)); - CompletableFuture> cf = CompletableFuture.completedFuture(reply); + CompletableFuture> cf = CompletableFuture.completedFuture(reply); when(nodeAsync.ftCursorread(anyString(), any(), anyInt())).thenReturn(new PipelinedRedisFuture<>(cf)); - AggregationReply out = async.ftCursorread("idx", cursor, 100).toCompletableFuture().join(); + AggregationReply out = async.ftCursorread("idx", cursor, 100).toCompletableFuture().join(); assertThat(out.getCursor()).isPresent(); assertThat(out.getCursor().get().getNodeId()).contains("node-1"); @@ -163,13 +162,13 @@ void ftCursordel_routesToNodeIdWithWRITE() { @Test void ftAggregate_withoutCursor_returnsReplyWithoutNodeId() { - AggregationReply replyNoCursor = new AggregationReply<>(); - CompletableFuture> cf = CompletableFuture.completedFuture(replyNoCursor); + AggregationReply replyNoCursor = new AggregationReply<>(); + CompletableFuture> cf = CompletableFuture.completedFuture(replyNoCursor); when(nodeAsync.ftAggregate(anyString(), anyString(), any())).thenReturn(new PipelinedRedisFuture<>(cf)); when(nodeAsync.clusterMyId()).thenReturn(new PipelinedRedisFuture<>(CompletableFuture.completedFuture("node-1"))); - AggregationReply out = async.ftAggregate("idx", "*", AggregateArgs. builder().build()) - .toCompletableFuture().join(); + AggregationReply out = async.ftAggregate("idx", "*", AggregateArgs.builder().build()).toCompletableFuture() + .join(); assertThat(out.getCursor()).isEmpty(); } diff --git a/src/test/java/io/lettuce/core/cluster/RedisAdvancedClusterReactiveCommandsImplTest.java b/src/test/java/io/lettuce/core/cluster/RedisAdvancedClusterReactiveCommandsImplTest.java index daee134f55..4d2394c515 100644 --- a/src/test/java/io/lettuce/core/cluster/RedisAdvancedClusterReactiveCommandsImplTest.java +++ b/src/test/java/io/lettuce/core/cluster/RedisAdvancedClusterReactiveCommandsImplTest.java @@ -102,16 +102,15 @@ void setup() { @Test void ftAggregate_stampsNodeId_whenCursorCreated() { - AggregateArgs args = AggregateArgs. builder() - .withCursor(AggregateArgs.WithCursor.of(1L)).build(); + AggregateArgs args = AggregateArgs.builder().withCursor(AggregateArgs.WithCursor.of(1L)).build(); - AggregationReply replyWithCursor = new AggregationReply<>(); + AggregationReply replyWithCursor = new AggregationReply<>(); replyWithCursor.setCursor(AggregationReply.Cursor.of(42L, null)); when(nodeReactive.ftAggregate(anyString(), anyString(), any())).thenReturn(Mono.just(replyWithCursor)); when(nodeReactive.clusterMyId()).thenReturn(Mono.just("node-1")); - AggregationReply out = reactive.ftAggregate("idx", "*", args).block(); + AggregationReply out = reactive.ftAggregate("idx", "*", args).block(); assertThat(out).isNotNull(); assertThat(out.getCursor()).isPresent(); @@ -121,12 +120,11 @@ void ftAggregate_stampsNodeId_whenCursorCreated() { @Test void ftAggregate_withoutCursor_returnsReplyWithoutNodeId() { - AggregationReply replyNoCursor = new AggregationReply<>(); + AggregationReply replyNoCursor = new AggregationReply<>(); when(nodeReactive.ftAggregate(anyString(), anyString(), any())).thenReturn(Mono.just(replyNoCursor)); when(nodeReactive.clusterMyId()).thenReturn(Mono.just("node-1")); - AggregationReply out = reactive - .ftAggregate("idx", "*", AggregateArgs. builder().build()).block(); + AggregationReply out = reactive.ftAggregate("idx", "*", AggregateArgs.builder().build()).block(); assertThat(out).isNotNull(); assertThat(out.getCursor()).isEmpty(); @@ -136,12 +134,12 @@ void ftAggregate_withoutCursor_returnsReplyWithoutNodeId() { void ftCursorread_routesToNodeId_andStampsNodeId() { AggregationReply.Cursor cursor = AggregationReply.Cursor.of(7L, "node-1"); - AggregationReply reply = new AggregationReply<>(); + AggregationReply reply = new AggregationReply<>(); reply.setCursor(AggregationReply.Cursor.of(7L, null)); when(nodeReactive.ftCursorread(anyString(), any(), anyInt())).thenReturn(Mono.just(reply)); - AggregationReply out = reactive.ftCursorread("idx", cursor, 100).block(); + AggregationReply out = reactive.ftCursorread("idx", cursor, 100).block(); assertThat(out).isNotNull(); assertThat(out.getCursor()).isPresent(); diff --git a/src/test/java/io/lettuce/core/output/AggregateReplyParserUnitTests.java b/src/test/java/io/lettuce/core/output/AggregateReplyParserUnitTests.java new file mode 100644 index 0000000000..3b85f4932f --- /dev/null +++ b/src/test/java/io/lettuce/core/output/AggregateReplyParserUnitTests.java @@ -0,0 +1,162 @@ +/* + * Copyright 2026-present, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ +package io.lettuce.core.output; + +import static io.lettuce.TestTags.UNIT_TEST; +import static org.assertj.core.api.Assertions.assertThat; + +import io.lettuce.core.codec.StringCodec; +import io.lettuce.core.search.AggregateReplyParser; +import io.lettuce.core.search.AggregationReply; +import io.lettuce.core.search.SearchReply; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for {@link AggregateReplyParser}. + * + * @author Viktoriya Kutsarova + */ +@Tag(UNIT_TEST) +class AggregateReplyParserUnitTests { + + private static final StringCodec CODEC = StringCodec.UTF8; + + @Test + void shouldReturnEmptyReplyForNullData() { + AggregateReplyParser parser = new AggregateReplyParser<>(CODEC, false); + + AggregationReply reply = parser.parse(null); + + assertThat(reply).isNotNull(); + assertThat(reply.getReplies()).isEmpty(); + } + + @Test + void shouldParseResp2DataWithoutCursor() { + // Without cursor: data is passed directly to SearchReplyParser (no-ID mode). + // Format: [count, fields_complexData, ...] + AggregateReplyParser parser = new AggregateReplyParser<>(CODEC, false); + + ArrayComplexData fields = new ArrayComplexData(4); + fields.storeObject(CODEC.encodeKey("category")); + fields.storeObject(CODEC.encodeValue("electronics")); + fields.storeObject(CODEC.encodeKey("count")); + fields.storeObject(CODEC.encodeValue("5")); + + ArrayComplexData data = new ArrayComplexData(2); + data.storeObject(1L); + data.storeObject(fields); + + AggregationReply reply = parser.parse(data); + + assertThat(reply.getReplies()).hasSize(1); + SearchReply searchReply = reply.getReplies().get(0); + assertThat(searchReply.getResults()).hasSize(1); + assertThat(searchReply.getResults().get(0).getFields().get("category").asString()).isEqualTo("electronics"); + assertThat(searchReply.getResults().get(0).getFields().get("count").asString()).isEqualTo("5"); + } + + @Test + void shouldPreserveFieldWithNullValue() { + // A field loaded from a JSON null (e.g. FT.AGGREGATE ... LOAD) comes back with a null value. + AggregateReplyParser parser = new AggregateReplyParser<>(CODEC, false); + + ArrayComplexData fields = new ArrayComplexData(4); + fields.storeObject(CODEC.encodeKey("country")); + fields.storeObject(CODEC.encodeValue("SE")); + fields.storeObject(CODEC.encodeKey("city")); + fields.storeObject(null); // JSON null loaded field + + ArrayComplexData data = new ArrayComplexData(2); + data.storeObject(1L); + data.storeObject(fields); + + AggregationReply reply = parser.parse(data); + + assertThat(reply.getReplies()).hasSize(1); + SearchReply searchReply = reply.getReplies().get(0); + assertThat(searchReply.getResults()).hasSize(1); + SearchReply.SearchResult result = searchReply.getResults().get(0); + assertThat(result.getFields().get("country").asString()).isEqualTo("SE"); + assertThat(result.getFields().containsKey("city")).isTrue(); + assertThat(result.getFields().get("city").isNull()).isTrue(); + assertThat(result.getFields().get("city").asString()).isNull(); + } + + @Test + void shouldParseResp2DataWithCursor() { + // With cursor: format is [groupCount, resultsComplexData, cursorId]. + AggregateReplyParser parser = new AggregateReplyParser<>(CODEC, true); + + ArrayComplexData fields = new ArrayComplexData(2); + fields.storeObject(CODEC.encodeKey("brand")); + fields.storeObject(CODEC.encodeValue("apple")); + + // Inner results in SearchReplyParser (no-ID) format: [count, fields_complexData] + ArrayComplexData innerResults = new ArrayComplexData(2); + innerResults.storeObject(1L); + innerResults.storeObject(fields); + + ArrayComplexData data = new ArrayComplexData(3); + data.storeObject(3L); // groupCount + data.storeObject(innerResults); + data.storeObject(77L); // cursorId + + AggregationReply reply = parser.parse(data); + + assertThat(reply.getAggregationGroups()).isEqualTo(3); + assertThat(reply.getReplies()).hasSize(1); + assertThat(reply.getCursor()).isPresent(); + assertThat(reply.getCursor().get().getCursorId()).isEqualTo(77L); + SearchReply searchReply = reply.getReplies().get(0); + assertThat(searchReply.getResults()).hasSize(1); + assertThat(searchReply.getResults().get(0).getFields().get("brand").asString()).isEqualTo("apple"); + } + + @Test + void shouldReturnEmptyReplyForEmptyListWithCursor() { + AggregateReplyParser parser = new AggregateReplyParser<>(CODEC, true); + ArrayComplexData data = new ArrayComplexData(0); + + AggregationReply reply = parser.parse(data); + + assertThat(reply).isNotNull(); + assertThat(reply.getReplies()).isEmpty(); + } + + @Test + void shouldParseResp3DataWithoutCursor() { + // Without cursor: RESP3 map is passed directly to SearchReplyParser. + AggregateReplyParser parser = new AggregateReplyParser<>(CODEC, false); + + MapComplexData extraAttributes = new MapComplexData(1); + extraAttributes.storeObject(CODEC.encodeKey("category")); + extraAttributes.storeObject(CODEC.encodeValue("computers")); + + MapComplexData resultEntry = new MapComplexData(1); + resultEntry.storeObject(CODEC.encodeKey("extra_attributes")); + resultEntry.storeObject(extraAttributes); + + ArrayComplexData resultsList = new ArrayComplexData(1); + resultsList.storeObject(resultEntry); + + MapComplexData data = new MapComplexData(2); + data.storeObject(CODEC.encodeKey("total_results")); + data.storeObject(1L); + data.storeObject(CODEC.encodeKey("results")); + data.storeObject(resultsList); + + AggregationReply reply = parser.parse(data); + + assertThat(reply.getReplies()).hasSize(1); + SearchReply searchReply = reply.getReplies().get(0); + assertThat(searchReply.getResults()).hasSize(1); + assertThat(searchReply.getResults().get(0).getFields().get("category").asString()).isEqualTo("computers"); + } + +} diff --git a/src/test/java/io/lettuce/core/output/HybridReplyParserUnitTests.java b/src/test/java/io/lettuce/core/output/HybridReplyParserUnitTests.java new file mode 100644 index 0000000000..417ebe84eb --- /dev/null +++ b/src/test/java/io/lettuce/core/output/HybridReplyParserUnitTests.java @@ -0,0 +1,199 @@ +/* + * Copyright 2026-present, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ +package io.lettuce.core.output; + +import static io.lettuce.TestTags.UNIT_TEST; +import static org.assertj.core.api.Assertions.assertThat; + +import io.lettuce.core.codec.RedisCodec; +import io.lettuce.core.codec.StringCodec; +import io.lettuce.core.search.FieldValue; +import io.lettuce.core.search.HybridReply; +import io.lettuce.core.search.HybridReplyParser; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; +import java.util.Map; + +/** + * Unit tests for {@link HybridReplyParser}. + * + * @author Viktoriya Kutsarova + */ +@Tag(UNIT_TEST) +class HybridReplyParserUnitTests { + + private static final StringCodec CODEC = StringCodec.UTF8; + + @Test + void shouldParseListReply() { + byte[] vector = new byte[] { 0, -1, 1, 2 }; + HybridReplyParser parser = new HybridReplyParser<>(CODEC); + ArrayComplexData resultData = array(buffer("title"), buffer("Redis Search"), buffer("embedding"), + ByteBuffer.wrap(vector), buffer("city"), null, buffer("__key"), buffer("doc:1")); + ArrayComplexData data = array(buffer("total_results"), 1L, buffer("execution_time"), buffer("0.5"), buffer("results"), + array(resultData), buffer("warnings"), array(buffer("Timeout limit was reached"))); + + HybridReply reply = parser.parse(data); + + assertThat(reply.getTotalResults()).isEqualTo(1); + assertThat(reply.getExecutionTime()).isEqualTo(0.5); + assertThat(reply.getWarnings()).containsExactly("Timeout limit was reached"); + assertThat(reply.getResults()).singleElement().satisfies(result -> { + Map fields = result.getFields(); + assertThat(result.getId()).isEqualTo("doc:1"); + assertThat(fields).containsOnlyKeys("title", "embedding", "city"); + assertThat(fields.get("title").asString()).isEqualTo("Redis Search"); + assertThat(fields.get("embedding").asBytes()).isEqualTo(vector); + assertThat(fields.get("city").isNull()).isTrue(); + }); + } + + @Test + void shouldParseMapReply() { + byte[] vector = new byte[] { 0, -1, 1, 2 }; + HybridReplyParser parser = new HybridReplyParser<>(CODEC); + MapComplexData resultData = map(buffer("title"), buffer("Redis Search"), buffer("embedding"), ByteBuffer.wrap(vector), + buffer("city"), null, buffer("__key"), buffer("doc:1")); + MapComplexData data = map(buffer("total_results"), 1L, buffer("execution_time"), 0.75, buffer("results"), + array(resultData), buffer("warnings"), + array(buffer("Timeout limit was reached"), buffer("Results may be incomplete"))); + + HybridReply reply = parser.parse(data); + + assertThat(reply.getTotalResults()).isEqualTo(1); + assertThat(reply.getExecutionTime()).isEqualTo(0.75); + assertThat(reply.getWarnings()).containsExactly("Timeout limit was reached", "Results may be incomplete"); + assertThat(reply.getResults()).singleElement().satisfies(result -> { + Map fields = result.getFields(); + assertThat(result.getId()).isEqualTo("doc:1"); + assertThat(fields).containsOnlyKeys("title", "embedding", "city"); + assertThat(fields.get("title").asString()).isEqualTo("Redis Search"); + assertThat(fields.get("embedding").asBytes()).isEqualTo(vector); + assertThat(fields.get("city").isNull()).isTrue(); + }); + } + + @Test + void shouldDecodeListDocumentKeyWithConnectionKeyCodec() { + PrefixingStringCodec codec = new PrefixingStringCodec("tenant:"); + EncodedComplexOutput> output = new EncodedComplexOutput<>(codec, + new HybridReplyParser<>(codec)); + + output.multiArray(4); + output.set(buffer("total_results")); + output.set(1L); + output.set(buffer("results")); + output.multiArray(1); + output.multiArray(4); + output.set(buffer("__key")); + output.set(codec.encodeKey("doc:1")); + output.set(buffer("title")); + output.set(buffer("tenant:guide")); + output.complete(2); + output.complete(1); + output.complete(0); + + Map fields = output.get().getResults().get(0).getFields(); + + assertThat(fields.get("title").asString()).isEqualTo("tenant:guide"); + assertThat(output.get().getResults().get(0).getId()).isEqualTo("doc:1"); + assertThat(fields).doesNotContainKey("__key"); + } + + @Test + void shouldDecodeMapDocumentKeyWithConnectionKeyCodec() { + PrefixingStringCodec codec = new PrefixingStringCodec("tenant:"); + EncodedComplexOutput> output = new EncodedComplexOutput<>(codec, + new HybridReplyParser<>(codec)); + + output.multiMap(2); + output.set(buffer("total_results")); + output.set(1L); + output.set(buffer("results")); + output.multiArray(1); + output.multiMap(2); + output.set(buffer("__key")); + output.set(codec.encodeKey("doc:1")); + output.set(buffer("title")); + output.set(buffer("tenant:guide")); + output.complete(2); + output.complete(1); + output.complete(0); + + Map fields = output.get().getResults().get(0).getFields(); + + assertThat(fields.get("title").asString()).isEqualTo("tenant:guide"); + assertThat(output.get().getResults().get(0).getId()).isEqualTo("doc:1"); + assertThat(fields).doesNotContainKey("__key"); + } + + @Test + void shouldReturnEmptyReplyWhenInputCannotBeParsed() { + HybridReplyParser parser = new HybridReplyParser<>(CODEC); + + HybridReply reply = parser.parse(new SetComplexData(0)); + + assertThat(reply.getTotalResults()).isZero(); + assertThat(reply.getExecutionTime()).isZero(); + assertThat(reply.getResults()).isEmpty(); + assertThat(reply.getWarnings()).isEmpty(); + } + + private static ByteBuffer buffer(String value) { + return CODEC.encodeValue(value); + } + + private static ArrayComplexData array(Object... values) { + ArrayComplexData data = new ArrayComplexData(values.length); + for (Object value : values) { + data.storeObject(value); + } + return data; + } + + private static MapComplexData map(Object... entries) { + MapComplexData data = new MapComplexData(entries.length / 2); + for (Object entry : entries) { + data.storeObject(entry); + } + return data; + } + + private static final class PrefixingStringCodec implements RedisCodec { + + private final String prefix; + + private PrefixingStringCodec(String prefix) { + this.prefix = prefix; + } + + @Override + public String decodeKey(ByteBuffer bytes) { + String key = StringCodec.UTF8.decodeKey(bytes); + return key.startsWith(prefix) ? key.substring(prefix.length()) : key; + } + + @Override + public String decodeValue(ByteBuffer bytes) { + return StringCodec.UTF8.decodeValue(bytes); + } + + @Override + public ByteBuffer encodeKey(String key) { + return StringCodec.UTF8.encodeKey(prefix + key); + } + + @Override + public ByteBuffer encodeValue(String value) { + return StringCodec.UTF8.encodeValue(value); + } + + } + +} diff --git a/src/test/java/io/lettuce/core/output/SearchReplyCollectParserUnitTests.java b/src/test/java/io/lettuce/core/output/SearchReplyCollectParserUnitTests.java new file mode 100644 index 0000000000..807e354d9f --- /dev/null +++ b/src/test/java/io/lettuce/core/output/SearchReplyCollectParserUnitTests.java @@ -0,0 +1,252 @@ +// Copyright (c) 2026-Present, Redis Ltd. All rights reserved. +// SPDX-License-Identifier: MIT +package io.lettuce.core.output; + +import static io.lettuce.TestTags.UNIT_TEST; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.assertj.core.api.Assertions.entry; + +import java.nio.ByteBuffer; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import io.lettuce.core.codec.StringCodec; +import io.lettuce.core.search.FieldValue; +import io.lettuce.core.search.SearchReply; +import io.lettuce.core.search.SearchReplyParser; + +/** + * Unit tests for {@link SearchReplyParser} covering the nested value shape produced by the {@code FT.AGGREGATE REDUCE COLLECT} + * reducer. The reducer contributes one column per group whose value is an array of per-entry maps; the parser must decode this + * in both RESP2 (each entry is a flat key/value array) and RESP3 (each entry is a map) into {@link FieldValue} structures, and + * {@link FieldValue#asMap()} must normalize both raw entry shapes to the same map. + */ +@Tag(UNIT_TEST) +class SearchReplyCollectParserUnitTests { + + private static ByteBuffer v(String s) { + return StringCodec.UTF8.encodeValue(s); + } + + private static ByteBuffer k(String s) { + return StringCodec.UTF8.encodeKey(s); + } + + /** + * Builds the RESP2 reply for the aggregation row {@code color=red, items=[ [fruit,apple,sweetness,7], [fruit,cherry] ]}. + */ + private static ComplexData resp2Reply() { + ArrayComplexData entry1 = new ArrayComplexData(4); + entry1.storeObject(v("fruit")); + entry1.storeObject(v("apple")); + entry1.storeObject(v("sweetness")); + entry1.storeObject(v("7")); + + ArrayComplexData entry2 = new ArrayComplexData(2); // sparse entry, no sweetness + entry2.storeObject(v("fruit")); + entry2.storeObject(v("cherry")); + + ArrayComplexData items = new ArrayComplexData(2); + items.storeObject(entry1); + items.storeObject(entry2); + + ArrayComplexData row = new ArrayComplexData(4); + row.storeObject(v("color")); + row.storeObject(v("red")); + row.storeObject(v("items")); + row.storeObject(items); + + ArrayComplexData reply = new ArrayComplexData(2); + reply.store(1L); // total results + reply.storeObject(row); + return reply; + } + + /** + * Builds the RESP3 reply for {@code extra_attributes: { color: red, items: [ {fruit:apple, sweetness:7}, {fruit:cherry} ] + * }}. + */ + private static ComplexData resp3Reply() { + MapComplexData entry1 = new MapComplexData(2); + entry1.storeObject(k("fruit")); + entry1.storeObject(v("apple")); + entry1.storeObject(k("sweetness")); + entry1.storeObject(v("7")); + + MapComplexData entry2 = new MapComplexData(1); // sparse entry + entry2.storeObject(k("fruit")); + entry2.storeObject(v("cherry")); + + ArrayComplexData items = new ArrayComplexData(2); + items.storeObject(entry1); + items.storeObject(entry2); + + MapComplexData attributes = new MapComplexData(2); + attributes.storeObject(k("color")); + attributes.storeObject(v("red")); + attributes.storeObject(k("items")); + attributes.storeObject(items); + + MapComplexData resultEntry = new MapComplexData(1); + resultEntry.storeObject(k("extra_attributes")); + resultEntry.storeObject(attributes); + + ArrayComplexData results = new ArrayComplexData(1); + results.storeObject(resultEntry); + + MapComplexData reply = new MapComplexData(2); + reply.storeObject(k("results")); + reply.storeObject(results); + reply.storeObject(k("total_results")); + reply.store(1L); + return reply; + } + + private static List> collectedEntries(SearchReply reply, String field) { + return reply.getResults().get(0).getFields().get(field).asList().stream().map(FieldValue::asMap) + .collect(Collectors.toList()); + } + + @Test + void shouldParseCollectColumnFromResp2FlatArrays() { + SearchReply parsed = new SearchReplyParser<>(StringCodec.UTF8).parse(resp2Reply()); + + assertThat(parsed.getResults()).hasSize(1); + Map fields = parsed.getResults().get(0).getFields(); + assertThat(fields.get("color").asString()).isEqualTo("red"); + + FieldValue collected = fields.get("items"); + assertThat(collected.getKind()).isEqualTo(FieldValue.Kind.ARRAY); + List entries = collected.asList(); + assertThat(entries).hasSize(2); + + // RESP2 keeps the raw entry shape: a flat key/value array. + assertThat(entries.get(0).getKind()).isEqualTo(FieldValue.Kind.ARRAY); + assertThat(entries.get(0).asList().stream().map(FieldValue::asString)).containsExactly("fruit", "apple", "sweetness", + "7"); + // Sparse entry keeps only the fields that were present on the row. + assertThat(entries.get(1).asList().stream().map(FieldValue::asString)).containsExactly("fruit", "cherry"); + } + + @Test + void shouldParseCollectColumnFromResp3Maps() { + SearchReply parsed = new SearchReplyParser<>(StringCodec.UTF8).parse(resp3Reply()); + + assertThat(parsed.getResults()).hasSize(1); + Map fields = parsed.getResults().get(0).getFields(); + assertThat(fields.get("color").asString()).isEqualTo("red"); + + FieldValue collected = fields.get("items"); + assertThat(collected.getKind()).isEqualTo(FieldValue.Kind.ARRAY); + List entries = collected.asList(); + assertThat(entries).hasSize(2); + + // RESP3 delivers each entry as a map. + assertThat(entries.get(0).getKind()).isEqualTo(FieldValue.Kind.MAP); + Map first = entries.get(0).asMap(); + assertThat(first.get("fruit").asString()).isEqualTo("apple"); + assertThat(first.get("sweetness").asString()).isEqualTo("7"); + // Sparse entry omits the missing key rather than emitting a null placeholder. + Map second = entries.get(1).asMap(); + assertThat(second.get("fruit").asString()).isEqualTo("cherry"); + assertThat(second).doesNotContainKey("sweetness"); + } + + @Test + void asMapShouldNormalizeResp2AndResp3EntriesToTheSameShape() { + SearchReply resp2 = new SearchReplyParser<>(StringCodec.UTF8).parse(resp2Reply()); + SearchReply resp3 = new SearchReplyParser<>(StringCodec.UTF8).parse(resp3Reply()); + + List> resp2Entries = collectedEntries(resp2, "items"); + List> resp3Entries = collectedEntries(resp3, "items"); + + assertThat(resp2Entries).hasSize(2); + assertThat(resp2Entries.get(0)).containsExactly(entry("fruit", FieldValue.of("apple".getBytes())), + entry("sweetness", FieldValue.of("7".getBytes()))); + assertThat(resp2Entries.get(1)).containsExactly(entry("fruit", FieldValue.of("cherry".getBytes()))); + + assertThat(resp3Entries).isEqualTo(resp2Entries); + } + + @Test + void shouldPreserveNullValuesInsideCollectedEntries() { + MapComplexData entry = new MapComplexData(2); + entry.storeObject(k("fruit")); + entry.storeObject(v("apple")); + entry.storeObject(k("sweetness")); + entry.storeObject(null); + + ArrayComplexData items = new ArrayComplexData(1); + items.storeObject(entry); + + MapComplexData attributes = new MapComplexData(1); + attributes.storeObject(k("items")); + attributes.storeObject(items); + + MapComplexData resultEntry = new MapComplexData(1); + resultEntry.storeObject(k("extra_attributes")); + resultEntry.storeObject(attributes); + + ArrayComplexData results = new ArrayComplexData(1); + results.storeObject(resultEntry); + + MapComplexData reply = new MapComplexData(2); + reply.storeObject(k("results")); + reply.storeObject(results); + reply.storeObject(k("total_results")); + reply.store(1L); + + SearchReply parsed = new SearchReplyParser<>(StringCodec.UTF8).parse(reply); + + Map first = parsed.getResults().get(0).getFields().get("items").asList().get(0).asMap(); + assertThat(first.get("fruit").asString()).isEqualTo("apple"); + assertThat(first.get("sweetness").isNull()).isTrue(); + assertThat(first.get("sweetness")).isSameAs(FieldValue.nullValue()); + } + + @Test + void shouldParseToListStyleScalarArrayColumn() { + ArrayComplexData tolist = new ArrayComplexData(3); + tolist.storeObject(v("apple")); + tolist.storeObject(v("cherry")); + tolist.storeObject(v("plum")); + + ArrayComplexData row = new ArrayComplexData(2); + row.storeObject(v("fruits")); + row.storeObject(tolist); + + ArrayComplexData reply = new ArrayComplexData(2); + reply.store(1L); + reply.storeObject(row); + + SearchReply parsed = new SearchReplyParser<>(StringCodec.UTF8).parse(reply); + + FieldValue fruits = parsed.getResults().get(0).getFields().get("fruits"); + assertThat(fruits.getKind()).isEqualTo(FieldValue.Kind.ARRAY); + assertThat(fruits.asList().stream().map(FieldValue::asString)).containsExactly("apple", "cherry", "plum"); + // An odd-length scalar array does not represent key/value pairs. + assertThatIllegalStateException().isThrownBy(fruits::asMap); + } + + @Test + void scalarColumnsShouldRejectComplexAccessors() { + SearchReply parsed = new SearchReplyParser<>(StringCodec.UTF8).parse(resp3Reply()); + + FieldValue color = parsed.getResults().get(0).getFields().get("color"); + assertThatIllegalStateException().isThrownBy(color::asList).withMessageContaining("SCALAR"); + assertThatIllegalStateException().isThrownBy(color::asMap).withMessageContaining("SCALAR"); + } + + @Test + void absentFieldIsRepresentedByMissingKey() { + SearchReply parsed = new SearchReplyParser<>(StringCodec.UTF8).parse(resp3Reply()); + + assertThat(parsed.getResults().get(0).getFields()).doesNotContainKey("missing"); + } + +} diff --git a/src/test/java/io/lettuce/core/output/SearchReplyParserUnitTests.java b/src/test/java/io/lettuce/core/output/SearchReplyParserUnitTests.java new file mode 100644 index 0000000000..b71e609eaa --- /dev/null +++ b/src/test/java/io/lettuce/core/output/SearchReplyParserUnitTests.java @@ -0,0 +1,279 @@ +/* + * Copyright 2026-present, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ +package io.lettuce.core.output; + +import static io.lettuce.TestTags.UNIT_TEST; +import static org.assertj.core.api.Assertions.assertThat; + +import io.lettuce.core.codec.RedisCodec; +import io.lettuce.core.codec.StringCodec; +import io.lettuce.core.search.FieldValue; +import io.lettuce.core.search.SearchReply; +import io.lettuce.core.search.SearchReplyParser; +import io.lettuce.core.search.arguments.SearchArgs; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; +import java.util.Map; + +/** + * Unit tests for {@link SearchReplyParser}. + * + * @author Viktoriya Kutsarova + */ +@Tag(UNIT_TEST) +class SearchReplyParserUnitTests { + + private static final StringCodec CODEC = StringCodec.UTF8; + + @Test + void shouldParseListReply() { + SearchReplyParser parser = new SearchReplyParser<>(CODEC, null); + ArrayComplexData data = array(2L, buffer("doc:1"), + array(buffer("title"), buffer("Redis Search"), buffer("views"), buffer("100")), buffer("doc:2"), + array(buffer("title"), buffer("Advanced Techniques"))); + + SearchReply reply = parser.parse(data); + + assertThat(reply.getCount()).isEqualTo(2); + assertThat(reply.getResults()).hasSize(2); + assertThat(reply.getResults().get(0).getId()).isEqualTo("doc:1"); + assertThat(reply.getResults().get(0).getFields().get("title").asString()).isEqualTo("Redis Search"); + assertThat(reply.getResults().get(0).getFields().get("views").asString()).isEqualTo("100"); + assertThat(reply.getResults().get(1).getId()).isEqualTo("doc:2"); + assertThat(reply.getResults().get(1).getFields().get("title").asString()).isEqualTo("Advanced Techniques"); + } + + @Test + void shouldPreserveListFieldValues() { + byte[] vector = new byte[] { 0, -1, 1, 2 }; + SearchReplyParser parser = new SearchReplyParser<>(CODEC, null); + ArrayComplexData fieldsData = array(buffer("title"), buffer("Redis Search"), buffer("embedding"), + ByteBuffer.wrap(vector), buffer("city"), null); + ArrayComplexData data = array(1L, buffer("doc:1"), fieldsData); + + SearchReply reply = parser.parse(data); + + Map fields = reply.getResults().get(0).getFields(); + assertThat(fields).containsOnlyKeys("title", "embedding", "city"); + assertThat(fields.get("title").asString()).isEqualTo("Redis Search"); + assertThat(fields.get("embedding").asBytes()).isEqualTo(vector); + assertThat(fields.get("city").isNull()).isTrue(); + } + + @Test + void shouldParseScoresWhenRequested() { + SearchArgs args = SearchArgs. builder().withScores().build(); + SearchReplyParser parser = new SearchReplyParser<>(CODEC, args); + ArrayComplexData data = array(1L, buffer("doc:1"), buffer("0.95"), array(buffer("title"), buffer("Redis Search"))); + + SearchReply reply = parser.parse(data); + + assertThat(reply.getResults()).singleElement().satisfies(result -> { + assertThat(result.getId()).isEqualTo("doc:1"); + assertThat(result.getScore()).isEqualTo(0.95); + assertThat(result.getFields().get("title").asString()).isEqualTo("Redis Search"); + }); + } + + @Test + void shouldOmitContentWhenRequested() { + SearchArgs args = SearchArgs. builder().noContent().build(); + SearchReplyParser parser = new SearchReplyParser<>(CODEC, args); + ArrayComplexData data = array(2L, buffer("doc:1"), buffer("doc:2")); + + SearchReply reply = parser.parse(data); + + assertThat(reply.getCount()).isEqualTo(2); + assertThat(reply.getResults()).extracting(SearchReply.SearchResult::getId).containsExactly("doc:1", "doc:2"); + assertThat(reply.getResults()).allSatisfy(result -> assertThat(result.getFields()).isEmpty()); + } + + @Test + void shouldParseListCursorReply() { + SearchReplyParser parser = new SearchReplyParser<>(CODEC, null); + ArrayComplexData results = array(1L, buffer("doc:1"), array(buffer("title"), buffer("Redis Search"))); + ArrayComplexData data = array(results, 42L); + + SearchReply reply = parser.parse(data); + + assertThat(reply.getCursorId()).isEqualTo(42L); + assertThat(reply.getCount()).isEqualTo(1); + assertThat(reply.getResults()).singleElement().satisfies(result -> assertThat(result.getId()).isEqualTo("doc:1")); + } + + @Test + void shouldParseRowsWithoutDocumentIds() { + SearchReplyParser parser = new SearchReplyParser<>(CODEC); + ArrayComplexData data = array(2L, array(buffer("category"), buffer("books")), + array(buffer("category"), buffer("electronics"))); + + SearchReply reply = parser.parse(data); + + assertThat(reply.getResults()).hasSize(2); + assertThat(reply.getResults().get(0).getFields().get("category").asString()).isEqualTo("books"); + assertThat(reply.getResults().get(1).getFields().get("category").asString()).isEqualTo("electronics"); + } + + @Test + void shouldDecodeListDocumentIdWithKeyCodec() { + PrefixingStringCodec codec = new PrefixingStringCodec("tenant:"); + SearchReplyParser parser = new SearchReplyParser<>(codec, null); + ArrayComplexData data = array(1L, codec.encodeKey("doc:1"), array(buffer("tenant:title"), buffer("tenant:guide"))); + + SearchReply reply = parser.parse(data); + + SearchReply.SearchResult result = reply.getResults().get(0); + assertThat(result.getId()).isEqualTo("doc:1"); + assertThat(result.getFields()).containsKey("tenant:title"); + assertThat(result.getFields().get("tenant:title").asString()).isEqualTo("tenant:guide"); + } + + @Test + void shouldParseMapReply() { + byte[] vector = new byte[] { 0, -1, 1, 2 }; + SearchReplyParser parser = new SearchReplyParser<>(CODEC, null); + MapComplexData fieldsData = map(buffer("title"), buffer("Redis Search"), buffer("embedding"), ByteBuffer.wrap(vector), + buffer("city"), null); + MapComplexData resultData = map(buffer("id"), buffer("doc:1"), buffer("score"), 1.0, buffer("extra_attributes"), + fieldsData); + MapComplexData data = map(buffer("total_results"), 1L, buffer("results"), array(resultData)); + + SearchReply reply = parser.parse(data); + + assertThat(reply.getCount()).isEqualTo(1); + assertThat(reply.getResults()).singleElement().satisfies(result -> { + assertThat(result.getId()).isEqualTo("doc:1"); + assertThat(result.getScore()).isEqualTo(1.0); + assertThat(result.getFields()).containsOnlyKeys("title", "embedding", "city"); + assertThat(result.getFields().get("title").asString()).isEqualTo("Redis Search"); + assertThat(result.getFields().get("embedding").asBytes()).isEqualTo(vector); + assertThat(result.getFields().get("city").isNull()).isTrue(); + }); + } + + @Test + void shouldParseScoreArrayFromMapReply() { + SearchReplyParser parser = new SearchReplyParser<>(CODEC, null); + MapComplexData resultData = map(buffer("id"), buffer("doc:1"), buffer("score"), array(0.75)); + MapComplexData data = map(buffer("total_results"), 1L, buffer("results"), array(resultData)); + + SearchReply reply = parser.parse(data); + + assertThat(reply.getResults()).singleElement().satisfies(result -> assertThat(result.getScore()).isEqualTo(0.75)); + } + + @Test + void shouldKeepMapResultWithoutDocumentId() { + SearchReplyParser parser = new SearchReplyParser<>(CODEC); + MapComplexData fieldsData = map(buffer("category"), buffer("books")); + MapComplexData resultData = map(buffer("extra_attributes"), fieldsData); + MapComplexData data = map(buffer("total_results"), 1L, buffer("results"), array(resultData)); + + SearchReply reply = parser.parse(data); + + assertThat(reply.getResults()).singleElement().satisfies(result -> { + assertThat(result.getId()).isNull(); + assertThat(result.getFields().get("category").asString()).isEqualTo("books"); + }); + } + + @Test + void shouldParseWarningsAndCursorFromMapReply() { + SearchReplyParser parser = new SearchReplyParser<>(CODEC, null); + MapComplexData data = map(buffer("total_results"), 0L, buffer("results"), array(), buffer("warning"), + array(buffer("Timeout limit was reached")), buffer("cursor"), 99L); + + SearchReply reply = parser.parse(data); + + assertThat(reply.getCount()).isZero(); + assertThat(reply.getResults()).isEmpty(); + assertThat(reply.getWarnings()).containsExactly("Timeout limit was reached"); + assertThat(reply.getCursorId()).isEqualTo(99L); + } + + @Test + void shouldDecodeMapDocumentIdWithKeyCodec() { + PrefixingStringCodec codec = new PrefixingStringCodec("tenant:"); + SearchReplyParser parser = new SearchReplyParser<>(codec, null); + MapComplexData fieldsData = map(buffer("tenant:title"), buffer("tenant:guide")); + MapComplexData resultData = map(buffer("id"), codec.encodeKey("doc:1"), buffer("extra_attributes"), fieldsData); + MapComplexData data = map(buffer("total_results"), 1L, buffer("results"), array(resultData)); + + SearchReply reply = parser.parse(data); + + SearchReply.SearchResult result = reply.getResults().get(0); + assertThat(result.getId()).isEqualTo("doc:1"); + assertThat(result.getFields()).containsKey("tenant:title"); + assertThat(result.getFields().get("tenant:title").asString()).isEqualTo("tenant:guide"); + } + + @Test + void shouldReturnEmptyReplyWhenInputCannotBeParsed() { + SearchReplyParser parser = new SearchReplyParser<>(CODEC, null); + + SearchReply reply = parser.parse(array("not-a-count")); + + assertThat(reply.getCount()).isZero(); + assertThat(reply.getResults()).isEmpty(); + assertThat(reply.getCursorId()).isNull(); + assertThat(reply.getWarnings()).isEmpty(); + } + + private static ByteBuffer buffer(String value) { + return CODEC.encodeValue(value); + } + + private static ArrayComplexData array(Object... values) { + ArrayComplexData data = new ArrayComplexData(values.length); + for (Object value : values) { + data.storeObject(value); + } + return data; + } + + private static MapComplexData map(Object... entries) { + MapComplexData data = new MapComplexData(entries.length / 2); + for (Object entry : entries) { + data.storeObject(entry); + } + return data; + } + + private static final class PrefixingStringCodec implements RedisCodec { + + private final String prefix; + + private PrefixingStringCodec(String prefix) { + this.prefix = prefix; + } + + @Override + public String decodeKey(ByteBuffer bytes) { + String key = StringCodec.UTF8.decodeKey(bytes); + return key.startsWith(prefix) ? key.substring(prefix.length()) : key; + } + + @Override + public String decodeValue(ByteBuffer bytes) { + return StringCodec.UTF8.decodeValue(bytes); + } + + @Override + public ByteBuffer encodeKey(String key) { + return StringCodec.UTF8.encodeKey(prefix + key); + } + + @Override + public ByteBuffer encodeValue(String value) { + return StringCodec.UTF8.encodeValue(value); + } + + } + +} diff --git a/src/test/java/io/lettuce/core/output/SpellCheckResultParserUnitTests.java b/src/test/java/io/lettuce/core/output/SpellCheckResultParserUnitTests.java index 5b07b20251..f7fe0850e7 100644 --- a/src/test/java/io/lettuce/core/output/SpellCheckResultParserUnitTests.java +++ b/src/test/java/io/lettuce/core/output/SpellCheckResultParserUnitTests.java @@ -9,7 +9,6 @@ import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.assertThat; -import io.lettuce.core.codec.StringCodec; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; @@ -26,10 +25,10 @@ class SpellCheckResultParserUnitTests { @Test void shouldParseEmptySpellCheckResult() { - SpellCheckResultParser parser = new SpellCheckResultParser<>(StringCodec.UTF8); + SpellCheckResultParser parser = new SpellCheckResultParser(); ArrayComplexData data = new ArrayComplexData(0); - SpellCheckResult result = parser.parse(data); + SpellCheckResult result = parser.parse(data); assertThat(result.hasMisspelledTerms()).isFalse(); assertThat(result.getMisspelledTermCount()).isEqualTo(0); @@ -38,7 +37,7 @@ void shouldParseEmptySpellCheckResult() { @Test void shouldParseSingleMisspelledTermWithOneSuggestion() { - SpellCheckResultParser parser = new SpellCheckResultParser<>(StringCodec.UTF8); + SpellCheckResultParser parser = new SpellCheckResultParser(); ArrayComplexData data = new ArrayComplexData(1); // Create the nested structure for a single misspelled term @@ -56,24 +55,24 @@ void shouldParseSingleMisspelledTermWithOneSuggestion() { termArray.storeObject(suggestionsArray); data.storeObject(termArray); - SpellCheckResult result = parser.parse(data); + SpellCheckResult result = parser.parse(data); assertThat(result.hasMisspelledTerms()).isTrue(); assertThat(result.getMisspelledTermCount()).isEqualTo(1); - SpellCheckResult.MisspelledTerm misspelledTerm = result.getMisspelledTerms().get(0); + SpellCheckResult.MisspelledTerm misspelledTerm = result.getMisspelledTerms().get(0); assertThat(misspelledTerm.getTerm()).isEqualTo("reids"); assertThat(misspelledTerm.hasSuggestions()).isTrue(); assertThat(misspelledTerm.getSuggestionCount()).isEqualTo(1); - SpellCheckResult.Suggestion suggestionResult = misspelledTerm.getSuggestions().get(0); + SpellCheckResult.Suggestion suggestionResult = misspelledTerm.getSuggestions().get(0); assertThat(suggestionResult.getScore()).isEqualTo(0.7); assertThat(suggestionResult.getSuggestion()).isEqualTo("redis"); } @Test void shouldParseMultipleMisspelledTermsWithMultipleSuggestions() { - SpellCheckResultParser parser = new SpellCheckResultParser<>(StringCodec.UTF8); + SpellCheckResultParser parser = new SpellCheckResultParser(); ArrayComplexData data = new ArrayComplexData(2); // First misspelled term @@ -116,45 +115,45 @@ void shouldParseMultipleMisspelledTermsWithMultipleSuggestions() { term2Array.storeObject(suggestions2Array); data.storeObject(term2Array); - SpellCheckResult result = parser.parse(data); + SpellCheckResult result = parser.parse(data); assertThat(result.hasMisspelledTerms()).isTrue(); assertThat(result.getMisspelledTermCount()).isEqualTo(2); // Check first misspelled term - SpellCheckResult.MisspelledTerm misspelledTerm1 = result.getMisspelledTerms().get(0); + SpellCheckResult.MisspelledTerm misspelledTerm1 = result.getMisspelledTerms().get(0); assertThat(misspelledTerm1.getTerm()).isEqualTo("reids"); assertThat(misspelledTerm1.hasSuggestions()).isTrue(); assertThat(misspelledTerm1.getSuggestionCount()).isEqualTo(2); - SpellCheckResult.Suggestion suggestion1_1Result = misspelledTerm1.getSuggestions().get(0); + SpellCheckResult.Suggestion suggestion1_1Result = misspelledTerm1.getSuggestions().get(0); assertThat(suggestion1_1Result.getScore()).isEqualTo(0.7); assertThat(suggestion1_1Result.getSuggestion()).isEqualTo("redis"); - SpellCheckResult.Suggestion suggestion1_2Result = misspelledTerm1.getSuggestions().get(1); + SpellCheckResult.Suggestion suggestion1_2Result = misspelledTerm1.getSuggestions().get(1); assertThat(suggestion1_2Result.getScore()).isEqualTo(0.5); assertThat(suggestion1_2Result.getSuggestion()).isEqualTo("reads"); // Check second misspelled term - SpellCheckResult.MisspelledTerm misspelledTerm2 = result.getMisspelledTerms().get(1); + SpellCheckResult.MisspelledTerm misspelledTerm2 = result.getMisspelledTerms().get(1); assertThat(misspelledTerm2.getTerm()).isEqualTo("serch"); assertThat(misspelledTerm2.hasSuggestions()).isTrue(); assertThat(misspelledTerm2.getSuggestionCount()).isEqualTo(2); - SpellCheckResult.Suggestion suggestion2_1Result = misspelledTerm2.getSuggestions().get(0); + SpellCheckResult.Suggestion suggestion2_1Result = misspelledTerm2.getSuggestions().get(0); assertThat(suggestion2_1Result.getScore()).isEqualTo(0.8); assertThat(suggestion2_1Result.getSuggestion()).isEqualTo("search"); - SpellCheckResult.Suggestion suggestion2_2Result = misspelledTerm2.getSuggestions().get(1); + SpellCheckResult.Suggestion suggestion2_2Result = misspelledTerm2.getSuggestions().get(1); assertThat(suggestion2_2Result.getScore()).isEqualTo(0.6); assertThat(suggestion2_2Result.getSuggestion()).isEqualTo("serve"); } @Test void shouldThrowExceptionForNullData() { - SpellCheckResultParser parser = new SpellCheckResultParser<>(StringCodec.UTF8); + SpellCheckResultParser parser = new SpellCheckResultParser(); - SpellCheckResult result = parser.parse(null); + SpellCheckResult result = parser.parse(null); assertThat(result.hasMisspelledTerms()).isFalse(); assertThat(result.getMisspelledTermCount()).isEqualTo(0); assertThat(result.getMisspelledTerms()).isEmpty(); @@ -162,7 +161,7 @@ void shouldThrowExceptionForNullData() { @Test void shouldThrowExceptionForInvalidTermFormat() { - SpellCheckResultParser parser = new SpellCheckResultParser<>(StringCodec.UTF8); + SpellCheckResultParser parser = new SpellCheckResultParser(); ArrayComplexData data = new ArrayComplexData(1); // Create an invalid term array with only 2 elements (missing suggestions) @@ -171,7 +170,7 @@ void shouldThrowExceptionForInvalidTermFormat() { termArray.store("reids"); data.storeObject(termArray); - SpellCheckResult result = parser.parse(data); + SpellCheckResult result = parser.parse(data); assertThat(result.hasMisspelledTerms()).isFalse(); assertThat(result.getMisspelledTermCount()).isEqualTo(0); @@ -180,7 +179,7 @@ void shouldThrowExceptionForInvalidTermFormat() { @Test void shouldThrowExceptionForInvalidTermMarker() { - SpellCheckResultParser parser = new SpellCheckResultParser<>(StringCodec.UTF8); + SpellCheckResultParser parser = new SpellCheckResultParser(); ArrayComplexData data = new ArrayComplexData(1); // Create a term array with invalid marker (not "TERM") @@ -190,7 +189,7 @@ void shouldThrowExceptionForInvalidTermMarker() { termArray.storeObject(new ArrayComplexData(0)); data.storeObject(termArray); - SpellCheckResult result = parser.parse(data); + SpellCheckResult result = parser.parse(data); assertThat(result.hasMisspelledTerms()).isFalse(); assertThat(result.getMisspelledTermCount()).isEqualTo(0); @@ -199,7 +198,7 @@ void shouldThrowExceptionForInvalidTermMarker() { @Test void shouldThrowExceptionForInvalidSuggestionFormat() { - SpellCheckResultParser parser = new SpellCheckResultParser<>(StringCodec.UTF8); + SpellCheckResultParser parser = new SpellCheckResultParser(); ArrayComplexData data = new ArrayComplexData(1); // Create a term array with invalid suggestion (only 1 element instead of 2) diff --git a/src/test/java/io/lettuce/core/output/SuggestionParserUnitTests.java b/src/test/java/io/lettuce/core/output/SuggestionParserUnitTests.java index 813080cdcd..518d52f62e 100644 --- a/src/test/java/io/lettuce/core/output/SuggestionParserUnitTests.java +++ b/src/test/java/io/lettuce/core/output/SuggestionParserUnitTests.java @@ -8,7 +8,6 @@ import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.util.List; @@ -28,13 +27,13 @@ class SuggestionParserUnitTests { @Test void shouldParseBasicSuggestions() { - SuggestionParser parser = new SuggestionParser<>(false, false); + SuggestionParser parser = new SuggestionParser(false, false); ArrayComplexData data = new ArrayComplexData(3); data.store("suggestion1"); data.store("suggestion2"); data.store("suggestion3"); - List> suggestions = parser.parse(data); + List suggestions = parser.parse(data); assertThat(suggestions).hasSize(3); assertThat(suggestions.get(0).getValue()).isEqualTo("suggestion1"); @@ -46,14 +45,14 @@ void shouldParseBasicSuggestions() { @Test void shouldParseSuggestionsWithScores() { - SuggestionParser parser = new SuggestionParser<>(true, false); + SuggestionParser parser = new SuggestionParser(true, false); ArrayComplexData data = new ArrayComplexData(4); data.store("suggestion1"); data.store(1.5); data.store("suggestion2"); data.store(2.0); - List> suggestions = parser.parse(data); + List suggestions = parser.parse(data); assertThat(suggestions).hasSize(2); assertThat(suggestions.get(0).getValue()).isEqualTo("suggestion1"); @@ -66,14 +65,14 @@ void shouldParseSuggestionsWithScores() { @Test void shouldParseSuggestionsWithPayloads() { - SuggestionParser parser = new SuggestionParser<>(false, true); + SuggestionParser parser = new SuggestionParser(false, true); ArrayComplexData data = new ArrayComplexData(4); data.store("suggestion1"); data.store("payload1"); data.store("suggestion2"); data.store("payload2"); - List> suggestions = parser.parse(data); + List suggestions = parser.parse(data); assertThat(suggestions).hasSize(2); assertThat(suggestions.get(0).getValue()).isEqualTo("suggestion1"); @@ -86,7 +85,7 @@ void shouldParseSuggestionsWithPayloads() { @Test void shouldParseSuggestionsWithScoresAndPayloads() { - SuggestionParser parser = new SuggestionParser<>(true, true); + SuggestionParser parser = new SuggestionParser(true, true); ArrayComplexData data = new ArrayComplexData(6); data.store("suggestion1"); data.store(1.5); @@ -95,7 +94,7 @@ void shouldParseSuggestionsWithScoresAndPayloads() { data.store(2.0); data.store("payload2"); - List> suggestions = parser.parse(data); + List suggestions = parser.parse(data); assertThat(suggestions).hasSize(2); assertThat(suggestions.get(0).getValue()).isEqualTo("suggestion1"); @@ -108,50 +107,90 @@ void shouldParseSuggestionsWithScoresAndPayloads() { assertThat(suggestions.get(1).getPayload()).isEqualTo("payload2"); } + @Test + void shouldParseSuggestionsWithStringScores() { + // a string-encoded score must be parsed into its numeric value + SuggestionParser parser = new SuggestionParser(true, false); + ArrayComplexData data = new ArrayComplexData(4); + data.store("hell"); + data.store("2147483648"); + data.store("hello"); + data.store("0.70710676908493042"); + + List suggestions = parser.parse(data); + + assertThat(suggestions).hasSize(2); + assertThat(suggestions.get(0).getValue()).isEqualTo("hell"); + assertThat(suggestions.get(0).getScore()).isEqualTo(2147483648.0); + assertThat(suggestions.get(1).getValue()).isEqualTo("hello"); + assertThat(suggestions.get(1).getScore()).isEqualTo(0.70710676908493042); + } + + @Test + void shouldParseSuggestionsWithStringScoresAndPayloads() { + // a string-encoded score must be parsed into its numeric value, alongside the payload + SuggestionParser parser = new SuggestionParser(true, true); + ArrayComplexData data = new ArrayComplexData(6); + data.store("hell"); + data.store("1.5"); + data.store("payload1"); + data.store("hello"); + data.store("2"); + data.store("payload2"); + + List suggestions = parser.parse(data); + + assertThat(suggestions).hasSize(2); + assertThat(suggestions.get(0).getScore()).isEqualTo(1.5); + assertThat(suggestions.get(0).getPayload()).isEqualTo("payload1"); + assertThat(suggestions.get(1).getScore()).isEqualTo(2.0); + assertThat(suggestions.get(1).getPayload()).isEqualTo("payload2"); + } + @Test void shouldHandleEmptyList() { - SuggestionParser parser = new SuggestionParser<>(false, false); + SuggestionParser parser = new SuggestionParser(false, false); ArrayComplexData data = new ArrayComplexData(0); - List> suggestions = parser.parse(data); + List suggestions = parser.parse(data); assertThat(suggestions).isEmpty(); } @Test void shouldThrowExceptionForNullData() { - SuggestionParser parser = new SuggestionParser<>(false, false); + SuggestionParser parser = new SuggestionParser(false, false); - List> suggestions = parser.parse(null); + List suggestions = parser.parse(null); assertThat(suggestions).isEmpty(); } @Test void shouldThrowExceptionForInvalidScoreFormat() { - SuggestionParser parser = new SuggestionParser<>(true, false); + SuggestionParser parser = new SuggestionParser(true, false); ArrayComplexData data = new ArrayComplexData(3); data.store("suggestion1"); data.store("suggestion2"); data.store("suggestion3"); - List> suggestions = parser.parse(data); + List suggestions = parser.parse(data); assertThat(suggestions).hasSize(0); } @Test void shouldThrowExceptionForInvalidPayloadFormat() { - SuggestionParser parser = new SuggestionParser<>(false, true); + SuggestionParser parser = new SuggestionParser(false, true); ArrayComplexData data = new ArrayComplexData(3); data.store("suggestion1"); data.store("payload1"); data.store("suggestion2"); - List> suggestions = parser.parse(data); + List suggestions = parser.parse(data); assertThat(suggestions).hasSize(0); } @Test void shouldThrowExceptionForInvalidScoreAndPayloadFormat() { - SuggestionParser parser = new SuggestionParser<>(true, true); + SuggestionParser parser = new SuggestionParser(true, true); ArrayComplexData data = new ArrayComplexData(5); data.store("suggestion1"); data.store(1.5); @@ -159,7 +198,7 @@ void shouldThrowExceptionForInvalidScoreAndPayloadFormat() { data.store("suggestion2"); data.store(2.0); - List> suggestions = parser.parse(data); + List suggestions = parser.parse(data); assertThat(suggestions).hasSize(0); } diff --git a/src/test/java/io/lettuce/core/output/SynonymMapParserUnitTests.java b/src/test/java/io/lettuce/core/output/SynonymMapParserUnitTests.java index f0b926b7a0..aa2505d1f6 100644 --- a/src/test/java/io/lettuce/core/output/SynonymMapParserUnitTests.java +++ b/src/test/java/io/lettuce/core/output/SynonymMapParserUnitTests.java @@ -25,9 +25,7 @@ @Tag(UNIT_TEST) class SynonymMapParserUnitTests { - private final StringCodec codec = StringCodec.UTF8; - - private final SynonymMapParser parser = new SynonymMapParser<>(codec); + private final SynonymMapParser parser = new SynonymMapParser(); @Test void shouldParseResp2Format() { diff --git a/src/test/java/io/lettuce/core/search/FieldValueUnitTests.java b/src/test/java/io/lettuce/core/search/FieldValueUnitTests.java new file mode 100644 index 0000000000..0b0df697f7 --- /dev/null +++ b/src/test/java/io/lettuce/core/search/FieldValueUnitTests.java @@ -0,0 +1,163 @@ +// Copyright (c) 2026-Present, Redis Ltd. All rights reserved. +// SPDX-License-Identifier: MIT +package io.lettuce.core.search; + +import static io.lettuce.TestTags.UNIT_TEST; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.entry; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for {@link FieldValue}. + */ +@Tag(UNIT_TEST) +class FieldValueUnitTests { + + private static FieldValue scalar(String value) { + return FieldValue.of(value.getBytes(StandardCharsets.UTF_8)); + } + + @Test + void shouldDiscriminateKinds() { + assertThat(scalar("a").getKind()).isEqualTo(FieldValue.Kind.SCALAR); + assertThat(FieldValue.array(Collections.emptyList()).getKind()).isEqualTo(FieldValue.Kind.ARRAY); + assertThat(FieldValue.map(Collections.emptyMap()).getKind()).isEqualTo(FieldValue.Kind.MAP); + assertThat(FieldValue.nullValue().getKind()).isEqualTo(FieldValue.Kind.NULL); + + assertThat(scalar("a").isNull()).isFalse(); + assertThat(FieldValue.nullValue().isNull()).isTrue(); + } + + @Test + void scalarShouldExposeBytesAndText() { + byte[] bytes = "héllo".getBytes(StandardCharsets.UTF_8); + FieldValue value = FieldValue.of(bytes); + + assertThat(value.asBytes()).isSameAs(bytes); + assertThat(value.asString()).isEqualTo("héllo"); + assertThat(value.asString(StandardCharsets.ISO_8859_1)).isEqualTo(new String(bytes, StandardCharsets.ISO_8859_1)); + } + + @Test + void binaryScalarShouldSurviveByteAccess() { + byte[] binary = new byte[] { (byte) 0x91, (byte) 0xC3, 0x28, 0x00, (byte) 0xFF }; + assertThat(FieldValue.of(binary).asBytes()).isSameAs(binary); + } + + @Test + void nullValueShouldReturnNullFromAllAccessors() { + FieldValue value = FieldValue.nullValue(); + + assertThat(value.asBytes()).isNull(); + assertThat(value.asString()).isNull(); + assertThat(value.asString(StandardCharsets.US_ASCII)).isNull(); + assertThat(value.asList()).isNull(); + assertThat(value.asMap()).isNull(); + } + + @Test + void mismatchedAccessorsShouldThrow() { + FieldValue array = FieldValue.array(Collections.singletonList(scalar("a"))); + FieldValue map = FieldValue.map(Collections.singletonMap("k", scalar("v"))); + + assertThatIllegalStateException().isThrownBy(() -> scalar("a").asList()).withMessageContaining("SCALAR"); + assertThatIllegalStateException().isThrownBy(() -> scalar("a").asMap()).withMessageContaining("SCALAR"); + assertThatIllegalStateException().isThrownBy(array::asBytes).withMessageContaining("ARRAY"); + assertThatIllegalStateException().isThrownBy(array::asString).withMessageContaining("ARRAY"); + assertThatIllegalStateException().isThrownBy(map::asBytes).withMessageContaining("MAP"); + assertThatIllegalStateException().isThrownBy(map::asString).withMessageContaining("MAP"); + assertThatIllegalStateException().isThrownBy(map::asList).withMessageContaining("MAP"); + } + + @Test + void asMapShouldInterpretFlatPairArray() { + FieldValue pairs = FieldValue.array(Arrays.asList(scalar("fruit"), scalar("apple"), scalar("sweetness"), scalar("7"))); + + Map map = pairs.asMap(); + assertThat(map).containsExactly(entry("fruit", scalar("apple")), entry("sweetness", scalar("7"))); + } + + @Test + void asMapShouldRejectArraysThatAreNotPairs() { + FieldValue oddLength = FieldValue.array(Arrays.asList(scalar("a"), scalar("b"), scalar("c"))); + assertThatIllegalStateException().isThrownBy(oddLength::asMap).withMessageContaining("key/value pairs"); + + FieldValue complexKey = FieldValue.array(Arrays.asList(FieldValue.array(Collections.emptyList()), scalar("value"))); + assertThatIllegalStateException().isThrownBy(complexKey::asMap).withMessageContaining("key/value pairs"); + } + + @Test + void asMapOnEmptyArrayShouldReturnEmptyMap() { + assertThat(FieldValue.array(Collections.emptyList()).asMap()).isEmpty(); + } + + @Test + void factoriesShouldRejectNullInput() { + assertThatIllegalArgumentException().isThrownBy(() -> FieldValue.of(null)); + assertThatIllegalArgumentException().isThrownBy(() -> FieldValue.array(null)); + assertThatIllegalArgumentException().isThrownBy(() -> FieldValue.array(Collections.singletonList(null))); + assertThatIllegalArgumentException().isThrownBy(() -> FieldValue.map(null)); + assertThatIllegalArgumentException().isThrownBy(() -> FieldValue.map(Collections.singletonMap("k", null))); + } + + @Test + void complexValuesShouldBeImmutable() { + List elements = new ArrayList<>(Collections.singletonList(scalar("a"))); + FieldValue array = FieldValue.array(elements); + elements.add(scalar("b")); + assertThat(array.asList()).hasSize(1); + assertThatThrownBy(() -> array.asList().add(scalar("c"))).isInstanceOf(UnsupportedOperationException.class); + + Map entries = new LinkedHashMap<>(Collections.singletonMap("k", scalar("v"))); + FieldValue map = FieldValue.map(entries); + entries.put("k2", scalar("v2")); + assertThat(map.asMap()).hasSize(1); + assertThatThrownBy(() -> map.asMap().put("k3", scalar("v3"))).isInstanceOf(UnsupportedOperationException.class); + } + + @Test + void mapShouldPreserveEntryOrder() { + Map entries = new LinkedHashMap<>(); + entries.put("z", scalar("1")); + entries.put("a", scalar("2")); + entries.put("m", scalar("3")); + + assertThat(FieldValue.map(entries).asMap().keySet()).containsExactly("z", "a", "m"); + } + + @Test + void shouldImplementStructuralEquality() { + assertThat(scalar("a")).isEqualTo(scalar("a")).hasSameHashCodeAs(scalar("a")); + assertThat(scalar("a")).isNotEqualTo(scalar("b")); + assertThat(FieldValue.nullValue()).isEqualTo(FieldValue.nullValue()); + assertThat(scalar("a")).isNotEqualTo(FieldValue.array(Collections.singletonList(scalar("a")))); + + FieldValue nested1 = FieldValue + .array(Arrays.asList(FieldValue.map(Collections.singletonMap("k", scalar("v"))), FieldValue.nullValue())); + FieldValue nested2 = FieldValue + .array(Arrays.asList(FieldValue.map(Collections.singletonMap("k", scalar("v"))), FieldValue.nullValue())); + assertThat(nested1).isEqualTo(nested2).hasSameHashCodeAs(nested2); + } + + @Test + void toStringShouldBeReadable() { + assertThat(FieldValue.nullValue().toString()).isEqualTo("null"); + assertThat(scalar("abc").toString()).isEqualTo("abc"); + assertThat(FieldValue.array(Arrays.asList(scalar("a"), scalar("b"))).toString()).isEqualTo("[a, b]"); + assertThat(FieldValue.map(Collections.singletonMap("k", scalar("v"))).toString()).isEqualTo("{k=v}"); + } + +} diff --git a/src/test/java/io/lettuce/core/search/FtHybridIntegrationTests.java b/src/test/java/io/lettuce/core/search/FtHybridIntegrationTests.java index 4f636392a1..9427970409 100644 --- a/src/test/java/io/lettuce/core/search/FtHybridIntegrationTests.java +++ b/src/test/java/io/lettuce/core/search/FtHybridIntegrationTests.java @@ -78,17 +78,15 @@ static void setupOnce() { redis.flushall(); // Create index with all needed fields - FieldArgs titleField = TextFieldArgs. builder().name("title").build(); - FieldArgs categoryField = TagFieldArgs. builder().name("category").build(); - FieldArgs brandField = TagFieldArgs. builder().name("brand").build(); - FieldArgs priceField = NumericFieldArgs. builder().name("price").sortable().build(); - FieldArgs ratingField = NumericFieldArgs. builder().name("rating").sortable().build(); - FieldArgs vectorField = VectorFieldArgs. builder().name("embedding").hnsw() - .type(VectorFieldArgs.VectorType.FLOAT32).dimensions(8).distanceMetric(VectorFieldArgs.DistanceMetric.COSINE) - .build(); + FieldArgs titleField = TextFieldArgs.builder().name("title").build(); + FieldArgs categoryField = TagFieldArgs.builder().name("category").build(); + FieldArgs brandField = TagFieldArgs.builder().name("brand").build(); + FieldArgs priceField = NumericFieldArgs.builder().name("price").sortable().build(); + FieldArgs ratingField = NumericFieldArgs.builder().name("rating").sortable().build(); + FieldArgs vectorField = VectorFieldArgs.builder().name("embedding").hnsw().type(VectorFieldArgs.VectorType.FLOAT32) + .dimensions(8).distanceMetric(VectorFieldArgs.DistanceMetric.COSINE).build(); - CreateArgs createArgs = CreateArgs. builder().withPrefix(PREFIX) - .on(CreateArgs.TargetType.HASH).build(); + CreateArgs createArgs = CreateArgs.builder().withPrefix(PREFIX).on(CreateArgs.TargetType.HASH).build(); assertThat(redis.ftCreate(INDEX, createArgs, Arrays.asList(titleField, categoryField, brandField, priceField, ratingField, vectorField))).isEqualTo("OK"); @@ -165,23 +163,22 @@ private static byte[] floatArrayToByteArray(float[] vector) { @Test @Order(1) void hybridWithRrfCombiner() { - HybridArgs args = HybridArgs. builder() - .search(HybridSearchArgs. builder().query("smartphone camera").build()) - .vectorSearch(HybridVectorArgs. builder().field("@embedding").vector("$vec") - .method(HybridVectorArgs.Knn.of(10)).build()) - .combine(Combiners. rrf().window(20).constant(60)) - .postProcessing(PostProcessingArgs. builder().load("@title", "@brand").build()) - .param("vec", queryVectorClose).build(); + HybridArgs args = HybridArgs.builder().search(HybridSearchArgs.builder().query("smartphone camera").build()) + .vectorSearch(HybridVectorArgs.builder().field("@embedding").vector("$vec").method(HybridVectorArgs.Knn.of(10)) + .build()) + .combine(Combiners.rrf().window(20).constant(60)) + .postProcessing(PostProcessingArgs.builder().load("@title", "@brand").build()).param("vec", queryVectorClose) + .build(); - HybridReply reply = redis.ftHybrid(INDEX, args); + HybridReply reply = redis.ftHybrid(INDEX, args); assertThat(reply).isNotNull(); assertThat(reply.getResults()).isNotEmpty(); assertThat(reply.getTotalResults()).isGreaterThan(0); // Verify we get electronics products with "smartphone camera" in title - boolean hasSmartphone = reply.getResults().stream() - .anyMatch(r -> r.get("title") != null && r.get("title").toLowerCase().contains("smartphone")); + boolean hasSmartphone = reply.getResults().stream().anyMatch(r -> r.getFields().get("title").asString() != null + && r.getFields().get("title").asString().toLowerCase().contains("smartphone")); assertThat(hasSmartphone).isTrue(); } @@ -190,15 +187,13 @@ void hybridWithRrfCombiner() { @Test @Order(2) void hybridWithRangeVectorSearch() { - HybridArgs args = HybridArgs. builder() - .search(HybridSearchArgs. builder().query("*").build()) - .vectorSearch(HybridVectorArgs. builder().field("@embedding").vector("$vec") + HybridArgs args = HybridArgs.builder().search(HybridSearchArgs.builder().query("*").build()) + .vectorSearch(HybridVectorArgs.builder().field("@embedding").vector("$vec") .method(HybridVectorArgs.Range.of(0.5)).build()) - .combine(Combiners. rrf().window(20)) - .postProcessing(PostProcessingArgs. builder().load("@title").build()) + .combine(Combiners.rrf().window(20)).postProcessing(PostProcessingArgs.builder().load("@title").build()) .param("vec", queryVectorClose).build(); - HybridReply reply = redis.ftHybrid(INDEX, args); + HybridReply reply = redis.ftHybrid(INDEX, args); assertThat(reply).isNotNull(); // RANGE returns all vectors within radius 0.5 - should include close vectors @@ -210,21 +205,21 @@ void hybridWithRangeVectorSearch() { @Test @Order(3) void hybridWithExplicitScorer() { - HybridArgs args = HybridArgs. builder() - .search(HybridSearchArgs. builder().query("smartphone camera").scorer(Scorers.bm25()) - .scoreAlias("bm25_score").build()) - .vectorSearch(HybridVectorArgs. builder().field("@embedding").vector("$vec") - .method(HybridVectorArgs.Knn.of(10)).build()) - .combine(Combiners. linear().alpha(0.5).beta(0.5)) - .postProcessing(PostProcessingArgs. builder().load("@title", "@brand").build()) - .param("vec", queryVectorClose).build(); + HybridArgs args = HybridArgs.builder() + .search(HybridSearchArgs.builder().query("smartphone camera").scorer(Scorers.bm25()).scoreAlias("bm25_score") + .build()) + .vectorSearch(HybridVectorArgs.builder().field("@embedding").vector("$vec").method(HybridVectorArgs.Knn.of(10)) + .build()) + .combine(Combiners.linear().alpha(0.5).beta(0.5)) + .postProcessing(PostProcessingArgs.builder().load("@title", "@brand").build()).param("vec", queryVectorClose) + .build(); - HybridReply reply = redis.ftHybrid(INDEX, args); + HybridReply reply = redis.ftHybrid(INDEX, args); assertThat(reply).isNotNull(); assertThat(reply.getResults()).isNotEmpty(); // Products with "smartphone camera" should rank higher with BM25 - String firstTitle = reply.getResults().get(0).get("title"); + String firstTitle = reply.getResults().get(0).getFields().get("title").asString(); assertThat(firstTitle).isNotNull(); assertThat(firstTitle.toLowerCase()).containsAnyOf("smartphone", "camera"); } @@ -243,21 +238,19 @@ void hybridWithExplicitScorer() { @Order(4) void hybridWithLoadAll() { assumeTrue(RedisConditions.of(redis).hasVersionGreaterOrEqualsTo("8.6")); - HybridArgs args = HybridArgs. builder() - .search(HybridSearchArgs. builder().query("@category:{electronics}").build()) - .vectorSearch(HybridVectorArgs. builder().field("@embedding").vector("$vec") - .method(HybridVectorArgs.Knn.of(5)).build()) - .combine(Combiners. rrf().window(20)) - .postProcessing(PostProcessingArgs. builder().loadAll().build()).param("vec", queryVectorClose) - .build(); + HybridArgs args = HybridArgs.builder().search(HybridSearchArgs.builder().query("@category:{electronics}").build()) + .vectorSearch(HybridVectorArgs.builder().field("@embedding").vector("$vec").method(HybridVectorArgs.Knn.of(5)) + .build()) + .combine(Combiners.rrf().window(20)).postProcessing(PostProcessingArgs.builder().loadAll().build()) + .param("vec", queryVectorClose).build(); - HybridReply reply = redis.ftHybrid(INDEX, args); + HybridReply reply = redis.ftHybrid(INDEX, args); assertThat(reply).isNotNull(); assertThat(reply.getResults()).isNotEmpty(); // With LOAD *, all fields should be present - Map firstResult = reply.getResults().get(0); + Map firstResult = reply.getResults().get(0).getFields(); assertThat(firstResult).containsKeys("title", "category", "brand", "price", "rating"); } @@ -266,15 +259,13 @@ void hybridWithLoadAll() { @Test @Order(5) void hybridWithTimeout() { - HybridArgs args = HybridArgs. builder() - .search(HybridSearchArgs. builder().query("smartphone").build()) - .vectorSearch(HybridVectorArgs. builder().field("@embedding").vector("$vec") - .method(HybridVectorArgs.Knn.of(10)).build()) - .combine(Combiners. rrf().window(20)).timeout(Duration.ofSeconds(30)) - .postProcessing(PostProcessingArgs. builder().load("@title").build()) - .param("vec", queryVectorClose).build(); + HybridArgs args = HybridArgs.builder().search(HybridSearchArgs.builder().query("smartphone").build()) + .vectorSearch(HybridVectorArgs.builder().field("@embedding").vector("$vec").method(HybridVectorArgs.Knn.of(10)) + .build()) + .combine(Combiners.rrf().window(20)).timeout(Duration.ofSeconds(30)) + .postProcessing(PostProcessingArgs.builder().load("@title").build()).param("vec", queryVectorClose).build(); - HybridReply reply = redis.ftHybrid(INDEX, args); + HybridReply reply = redis.ftHybrid(INDEX, args); assertThat(reply).isNotNull(); assertThat(reply.getResults()).isNotEmpty(); @@ -288,22 +279,22 @@ void hybridWithTimeout() { @Order(6) void hybridWithScoreAliases() { // Test YIELD_SCORE_AS for SEARCH, VSIM and the COMBINE combiner - HybridArgs args = HybridArgs. builder() - .search(HybridSearchArgs. builder().query("smartphone").scoreAlias("text_score").build()) - .vectorSearch(HybridVectorArgs. builder().field("@embedding").vector("$vec") - .method(HybridVectorArgs.Knn.of(10)).scoreAlias("vector_score").build()) - .combine(Combiners. rrf().window(20).constant(60).as("combined_score")) - .postProcessing(PostProcessingArgs. builder().load("@title", "@brand").build()) - .param("vec", queryVectorClose).build(); + HybridArgs args = HybridArgs.builder() + .search(HybridSearchArgs.builder().query("smartphone").scoreAlias("text_score").build()) + .vectorSearch(HybridVectorArgs.builder().field("@embedding").vector("$vec").method(HybridVectorArgs.Knn.of(10)) + .scoreAlias("vector_score").build()) + .combine(Combiners.rrf().window(20).constant(60).as("combined_score")) + .postProcessing(PostProcessingArgs.builder().load("@title", "@brand").build()).param("vec", queryVectorClose) + .build(); - HybridReply reply = redis.ftHybrid(INDEX, args); + HybridReply reply = redis.ftHybrid(INDEX, args); assertThat(reply).isNotNull(); assertThat(reply.getResults()).isNotEmpty(); assertThat(reply.getTotalResults()).isGreaterThan(0); - assertThat(reply.getResults()).allSatisfy(result -> assertThat(result).containsKey("combined_score")); - assertThat(reply.getResults()).anySatisfy(result -> assertThat(result).containsKey("text_score")); - assertThat(reply.getResults()).anySatisfy(result -> assertThat(result).containsKey("vector_score")); + assertThat(reply.getResults()).allSatisfy(result -> assertThat(result.getFields()).containsKey("combined_score")); + assertThat(reply.getResults()).anySatisfy(result -> assertThat(result.getFields()).containsKey("text_score")); + assertThat(reply.getResults()).anySatisfy(result -> assertThat(result.getFields()).containsKey("vector_score")); } // ==================== TEST 7: Reducers AVG, MIN, MAX ==================== @@ -311,34 +302,34 @@ void hybridWithScoreAliases() { @Test @Order(7) void hybridWithReducerAvgMinMax() { - HybridArgs args = HybridArgs. builder() - .search(HybridSearchArgs. builder().query("@category:{electronics}").build()) - .vectorSearch(HybridVectorArgs. builder().field("@embedding").vector("$vec") - .method(HybridVectorArgs.Knn.of(10)).build()) - .combine(Combiners. rrf().window(20)) - .postProcessing(PostProcessingArgs. builder() - .groupBy(GroupBy. of("@brand").reduce(Reducers. avg("@price").as("avg_price")) + HybridArgs args = HybridArgs.builder().search(HybridSearchArgs.builder().query("@category:{electronics}").build()) + .vectorSearch(HybridVectorArgs + .builder().field("@embedding").vector("$vec").method(HybridVectorArgs.Knn.of(10)).build()) + .combine(Combiners.rrf().window(20)) + .postProcessing(PostProcessingArgs.builder() + .groupBy(GroupBy.of("@brand").reduce(Reducers.avg("@price").as("avg_price")) .reduce(Reducers.min("@price").as("min_price")).reduce(Reducers.max("@price").as("max_price"))) .build()) .param("vec", queryVectorClose).build(); - HybridReply reply = redis.ftHybrid(INDEX, args); + HybridReply reply = redis.ftHybrid(INDEX, args); assertThat(reply).isNotNull(); assertThat(reply.getResults()).isNotEmpty(); // Find Apple result and verify aggregations // Apple has products at $999 and $2499, so avg=1749, min=999, max=2499 - for (Map result : reply.getResults()) { + for (HybridReply.HybridResult hybridResult : reply.getResults()) { + Map result = hybridResult.getFields(); if ("apple".equals(result.get("brand"))) { - assertThat(result.get("avg_price")).isEqualTo("1749"); - assertThat(result.get("min_price")).isEqualTo("999"); - assertThat(result.get("max_price")).isEqualTo("2499"); + assertThat(result.get("avg_price").asString()).isEqualTo("1749"); + assertThat(result.get("min_price").asString()).isEqualTo("999"); + assertThat(result.get("max_price").asString()).isEqualTo("2499"); } else if ("samsung".equals(result.get("brand"))) { // Samsung: $799 and $1299, avg=1049, min=799, max=1299 - assertThat(result.get("avg_price")).isEqualTo("1049"); - assertThat(result.get("min_price")).isEqualTo("799"); - assertThat(result.get("max_price")).isEqualTo("1299"); + assertThat(result.get("avg_price").asString()).isEqualTo("1049"); + assertThat(result.get("min_price").asString()).isEqualTo("799"); + assertThat(result.get("max_price").asString()).isEqualTo("1299"); } } } @@ -348,35 +339,159 @@ void hybridWithReducerAvgMinMax() { @Test @Order(8) void hybridWithReducerQuantile() { - HybridArgs args = HybridArgs. builder() - .search(HybridSearchArgs. builder().query("@category:{electronics}").build()) - .vectorSearch(HybridVectorArgs. builder().field("@embedding").vector("$vec") - .method(HybridVectorArgs.Knn.of(10)).build()) - .combine(Combiners. rrf().window(20)) - .postProcessing(PostProcessingArgs. builder() - .groupBy(GroupBy. of("@category") - .reduce(Reducers.quantile("@price", 0.5).as("median_price")) - .reduce(Reducers. count().as("count"))) + HybridArgs args = HybridArgs.builder().search(HybridSearchArgs.builder().query("@category:{electronics}").build()) + .vectorSearch(HybridVectorArgs + .builder().field("@embedding").vector("$vec").method(HybridVectorArgs.Knn.of(10)).build()) + .combine(Combiners.rrf().window(20)) + .postProcessing(PostProcessingArgs.builder().groupBy(GroupBy.of("@category") + .reduce(Reducers.quantile("@price", 0.5).as("median_price")).reduce(Reducers.count().as("count"))) .build()) .param("vec", queryVectorClose).build(); - HybridReply reply = redis.ftHybrid(INDEX, args); + HybridReply reply = redis.ftHybrid(INDEX, args); assertThat(reply).isNotNull(); assertThat(reply.getResults()).isNotEmpty(); // Find electronics category and verify quantile was computed boolean foundElectronics = false; - for (Map result : reply.getResults()) { - if ("electronics".equals(result.get("category"))) { + for (HybridReply.HybridResult hybridResult : reply.getResults()) { + Map result = hybridResult.getFields(); + if ("electronics".equals(result.get("category").asString())) { foundElectronics = true; assertThat(result.get("median_price")).isNotNull(); - assertThat(result.get("count")).isEqualTo("5"); // 5 electronics products + assertThat(result.get("count").asString()).isEqualTo("5"); // 5 electronics products } } assertThat(foundElectronics).isTrue(); } + // ==================== TEST 10: Reducer TOLIST ==================== + + @Test + @Order(10) + void hybridWithReducerToList() { + HybridArgs args = HybridArgs.builder().search(HybridSearchArgs.builder().query("*").build()) + .vectorSearch(HybridVectorArgs + .builder().field("@embedding").vector("$vec").method(HybridVectorArgs.Knn.of(10)).build()) + .combine(Combiners.rrf().window(20)) + .postProcessing(PostProcessingArgs.builder().groupBy(GroupBy.of("@category") + .reduce(Reducers.toList("@brand").as("brands")).reduce(Reducers.count().as("count"))).build()) + .param("vec", queryVectorClose).build(); + + HybridReply reply = redis.ftHybrid(INDEX, args); + + assertThat(reply).isNotNull(); + assertThat(reply.getResults()).isNotEmpty(); + // TOLIST reducer returns a Redis array, which HybridReply cannot store as a plain Map + // value — addFieldsFromComplexData skips non-ByteBuffer values. We verify the count reducer (a scalar) + // is present and that the server accepted the TOLIST clause without error. + for (HybridReply.HybridResult result : reply.getResults()) { + assertThat(result.getFields().get("count")).isNotNull(); + } + } + + // ==================== TEST 11: Reducer FIRST_VALUE ==================== + + @Test + @Order(11) + void hybridWithReducerFirstValue() { + HybridArgs args = HybridArgs.builder().search(HybridSearchArgs.builder().query("*").build()) + .vectorSearch(HybridVectorArgs + .builder().field("@embedding").vector("$vec").method(HybridVectorArgs.Knn.of(10)).build()) + .combine(Combiners.rrf().window(20)) + .postProcessing(PostProcessingArgs.builder().groupBy(GroupBy.of("@category") + .reduce(Reducers.firstValue("@brand").as("first_brand")).reduce(Reducers.count().as("count"))).build()) + .param("vec", queryVectorClose).build(); + + HybridReply reply = redis.ftHybrid(INDEX, args); + + assertThat(reply).isNotNull(); + assertThat(reply.getResults()).isNotEmpty(); + for (HybridReply.HybridResult result : reply.getResults()) { + assertThat(result.getFields().get("first_brand")).isNotNull(); + assertThat(result.getFields().get("count")).isNotNull(); + } + } + + // ==================== TEST 12: Reducer RANDOM_SAMPLE ==================== + + @Test + @Order(12) + void hybridWithReducerRandomSample() { + HybridArgs args = HybridArgs.builder().search(HybridSearchArgs.builder().query("*").build()) + .vectorSearch(HybridVectorArgs + .builder().field("@embedding").vector("$vec").method(HybridVectorArgs.Knn.of(10)).build()) + .combine(Combiners.rrf().window(20)) + .postProcessing(PostProcessingArgs.builder().groupBy(GroupBy.of("@category") + .reduce(Reducers.randomSample("@brand", 2).as("sample_brands")).reduce(Reducers.count().as("count"))) + .build()) + .param("vec", queryVectorClose).build(); + + HybridReply reply = redis.ftHybrid(INDEX, args); + + assertThat(reply).isNotNull(); + assertThat(reply.getResults()).isNotEmpty(); + // RANDOM_SAMPLE returns a Redis array, which cannot be stored in Map. + // addFieldsFromComplexData skips non-ByteBuffer values, so "sample_brands" will not appear + // in the result map. Verify the count reducer (a scalar) is present without error. + for (HybridReply.HybridResult result : reply.getResults()) { + assertThat(result.getFields().get("count")).isNotNull(); + } + } + + // ==================== TEST 13: Reducer STDDEV ==================== + + @Test + @Order(13) + void hybridWithReducerStddev() { + HybridArgs args = HybridArgs.builder().search(HybridSearchArgs.builder().query("@category:{electronics}").build()) + .vectorSearch(HybridVectorArgs + .builder().field("@embedding").vector("$vec").method(HybridVectorArgs.Knn.of(10)).build()) + .combine(Combiners.rrf().window(20)) + .postProcessing(PostProcessingArgs.builder().groupBy(GroupBy.of("@category") + .reduce(Reducers.stddev("@price").as("price_stddev")).reduce(Reducers.count().as("count"))).build()) + .param("vec", queryVectorClose).build(); + + HybridReply reply = redis.ftHybrid(INDEX, args); + + assertThat(reply).isNotNull(); + assertThat(reply.getResults()).isNotEmpty(); + Map electronicsGroup = reply.getResults().get(0).getFields(); + assertThat(electronicsGroup.get("price_stddev")).isNotNull(); + // STDDEV is a scalar string. KNN+RRF may return only one electronics item per group, + // giving stddev=0. Just verify the value is a parseable non-negative double. + double stddev = Double.parseDouble(electronicsGroup.get("price_stddev").asString()); + assertThat(stddev).isGreaterThanOrEqualTo(0.0); + } + + // ==================== TEST 14: Reducer COUNT_DISTINCTISH ==================== + + @Test + @Order(14) + void hybridWithReducerCountDistinctish() { + HybridArgs args = HybridArgs.builder().search(HybridSearchArgs.builder().query("*").build()) + .vectorSearch(HybridVectorArgs + .builder().field("@embedding").vector("$vec").method(HybridVectorArgs.Knn.of(10)).build()) + .combine(Combiners.rrf().window(20)) + .postProcessing(PostProcessingArgs.builder() + .groupBy(GroupBy.of("@category").reduce(Reducers.countDistinctish("@brand").as("approx_brand_count")) + .reduce(Reducers.count().as("count"))) + .build()) + .param("vec", queryVectorClose).build(); + + HybridReply reply = redis.ftHybrid(INDEX, args); + + assertThat(reply).isNotNull(); + assertThat(reply.getResults()).isNotEmpty(); + for (HybridReply.HybridResult result : reply.getResults()) { + assertThat(result.getFields().get("approx_brand_count")).isNotNull(); + // HyperLogLog approx count should be a positive integer + long approxCount = Long.parseLong(result.getFields().get("approx_brand_count").asString()); + assertThat(approxCount).isGreaterThan(0); + } + } + // ==================== TEST 9: Mid-distance Vector with Text Dominance ==================== @Test @@ -385,22 +500,20 @@ void hybridWithMidDistanceVectorTextDominates() { // Using queryVectorMid (equidistant from all products), text search should dominate ranking // With LINEAR combiner alpha=0.8 (text weight) and beta=0.2 (vector weight), // the text match "Pro" should determine the ranking - HybridArgs args = HybridArgs. builder() - .search(HybridSearchArgs. builder().query("Pro").build()) - .vectorSearch(HybridVectorArgs. builder().field("@embedding").vector("$vec") - .method(HybridVectorArgs.Knn.of(10)).build()) - .combine(Combiners. linear().alpha(0.8).beta(0.2)) - .postProcessing(PostProcessingArgs. builder().load("@title").build()) - .param("vec", queryVectorMid).build(); + HybridArgs args = HybridArgs.builder().search(HybridSearchArgs.builder().query("Pro").build()) + .vectorSearch(HybridVectorArgs.builder().field("@embedding").vector("$vec").method(HybridVectorArgs.Knn.of(10)) + .build()) + .combine(Combiners.linear().alpha(0.8).beta(0.2)) + .postProcessing(PostProcessingArgs.builder().load("@title").build()).param("vec", queryVectorMid).build(); - HybridReply reply = redis.ftHybrid(INDEX, args); + HybridReply reply = redis.ftHybrid(INDEX, args); assertThat(reply).isNotNull(); assertThat(reply.getResults()).isNotEmpty(); // With mid-distance vector and high text weight, products with "Pro" in title should rank high // Our dataset has: "iPhone 15 Pro", "MacBook Pro" - String firstTitle = reply.getResults().get(0).get("title"); + String firstTitle = reply.getResults().get(0).getFields().get("title").asString(); assertThat(firstTitle).containsIgnoringCase("Pro"); } diff --git a/src/test/java/io/lettuce/core/search/HybridReplyTest.java b/src/test/java/io/lettuce/core/search/HybridReplyTest.java new file mode 100644 index 0000000000..51ad063025 --- /dev/null +++ b/src/test/java/io/lettuce/core/search/HybridReplyTest.java @@ -0,0 +1,130 @@ +/* + * Copyright 2026-present, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ +package io.lettuce.core.search; + +import static io.lettuce.TestTags.UNIT_TEST; +import static org.assertj.core.api.Assertions.assertThat; + +import java.nio.charset.StandardCharsets; +import java.util.Map; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for {@link HybridReply}. + * + * @author Viktoriya Kutsarova + */ +@Tag(UNIT_TEST) +class HybridReplyTest { + + @Test + void testEmptyHybridReply() { + HybridReply reply = new HybridReply<>(); + + assertThat(reply.getTotalResults()).isEqualTo(0); + assertThat(reply.getExecutionTime()).isEqualTo(0.0); + assertThat(reply.getResults()).isEmpty(); + assertThat(reply.getWarnings()).isEmpty(); + assertThat(reply.size()).isEqualTo(0); + assertThat(reply.isEmpty()).isTrue(); + } + + @Test + void testSetAndGetTotalResults() { + HybridReply reply = new HybridReply<>(); + reply.setTotalResults(42L); + + assertThat(reply.getTotalResults()).isEqualTo(42L); + } + + @Test + void testSetAndGetExecutionTime() { + HybridReply reply = new HybridReply<>(); + reply.setExecutionTime(1.23); + + assertThat(reply.getExecutionTime()).isEqualTo(1.23); + } + + @Test + void testAddAndGetResults() { + HybridReply reply = new HybridReply<>(); + + HybridReply.HybridResult result1 = new HybridReply.HybridResult<>(); + result1.setId("doc:1"); + result1.addField("title", "Redis Search".getBytes(StandardCharsets.UTF_8)); + + HybridReply.HybridResult result2 = new HybridReply.HybridResult<>(); + result2.addField("title", "Advanced Techniques".getBytes(StandardCharsets.UTF_8)); + + reply.addResult(result1); + reply.addResult(result2); + + assertThat(reply.size()).isEqualTo(2); + assertThat(reply.isEmpty()).isFalse(); + assertThat(reply.getResults()).hasSize(2); + assertThat(reply.getResults().get(0).getId()).isEqualTo("doc:1"); + assertThat(reply.getResults().get(0).getFields().get("title").asString()).isEqualTo("Redis Search"); + assertThat(reply.getResults().get(1).getFields().get("title").asString()).isEqualTo("Advanced Techniques"); + } + + @Test + void testFieldValuesExposeTextAndBinary() { + HybridReply.HybridResult result = new HybridReply.HybridResult<>(); + + // a binary value that is not valid UTF-8 (e.g. a little-endian float32 vector) + byte[] vector = new byte[] { -51, -52, -52, 61, -51, -52, 76, 62 }; + result.addField("embedding", vector); + result.addField("title", "Lettuce".getBytes(StandardCharsets.UTF_8)); + + Map fields = result.getFields(); + + assertThat(fields.get("title").asString()).isEqualTo("Lettuce"); + assertThat(fields.get("embedding").asBytes()).isEqualTo(vector); + assertThat(fields.get("missing")).isNull(); + assertThat(fields).containsOnlyKeys("embedding", "title"); + } + + @Test + void testAddAndGetWarnings() { + HybridReply reply = new HybridReply<>(); + reply.addWarning("Timeout limit was reached"); + reply.addWarning("Partial results returned"); + + assertThat(reply.getWarnings()).containsExactly("Timeout limit was reached", "Partial results returned"); + } + + @Test + void testGetResultsIsUnmodifiable() { + HybridReply reply = new HybridReply<>(); + reply.addResult(new HybridReply.HybridResult<>()); + + assertThat(reply.getResults()).hasSize(1); + try { + reply.getResults().clear(); + assertThat(false).as("Expected UnsupportedOperationException").isTrue(); + } catch (UnsupportedOperationException e) { + assertThat(reply.getResults()).hasSize(1); + } + } + + @Test + void testGetWarningsIsUnmodifiable() { + HybridReply reply = new HybridReply<>(); + reply.addWarning("warn"); + + assertThat(reply.getWarnings()).hasSize(1); + try { + reply.getWarnings().clear(); + assertThat(false).as("Expected UnsupportedOperationException").isTrue(); + } catch (UnsupportedOperationException e) { + assertThat(reply.getWarnings()).hasSize(1); + } + } + +} diff --git a/src/test/java/io/lettuce/core/search/RediSearchAdvancedConceptsIntegrationTests.java b/src/test/java/io/lettuce/core/search/RediSearchAdvancedConceptsIntegrationTests.java index d0fdb1c0a1..f5cd1e7464 100644 --- a/src/test/java/io/lettuce/core/search/RediSearchAdvancedConceptsIntegrationTests.java +++ b/src/test/java/io/lettuce/core/search/RediSearchAdvancedConceptsIntegrationTests.java @@ -107,11 +107,11 @@ static void teardown() { @Test void testStopWordsManagement() { // Test 1: Create index with custom stop words - FieldArgs titleField = TextFieldArgs. builder().name("title").build(); - FieldArgs contentField = TextFieldArgs. builder().name("content").build(); + FieldArgs titleField = TextFieldArgs.builder().name("title").build(); + FieldArgs contentField = TextFieldArgs.builder().name("content").build(); - CreateArgs customStopWordsArgs = CreateArgs. builder().withPrefix(ARTICLE_PREFIX) - .on(CreateArgs.TargetType.HASH).stopWords(Arrays.asList("foo", "bar", "baz")).build(); + CreateArgs customStopWordsArgs = CreateArgs.builder().withPrefix(ARTICLE_PREFIX).on(CreateArgs.TargetType.HASH) + .stopWords(Arrays.asList("foo", "bar", "baz")).build(); redis.ftCreate(STOPWORDS_INDEX, customStopWordsArgs, Arrays.asList(titleField, contentField)); @@ -127,7 +127,7 @@ void testStopWordsManagement() { redis.hmset("article:2", article2); // Test that custom stop words are ignored in search - SearchReply results = redis.ftSearch(STOPWORDS_INDEX, "foo"); + SearchReply results = redis.ftSearch(STOPWORDS_INDEX, "foo"); assertThat(results.getCount()).isEqualTo(0); // "foo" should be ignored as stop word results = redis.ftSearch(STOPWORDS_INDEX, "guide"); @@ -140,7 +140,7 @@ void testStopWordsManagement() { // FIXME DISABLED - not working on the server - // SearchArgs noStopWordsArgs = SearchArgs.builder().noStopWords().build(); + // SearchArgs noStopWordsArgs = SearchArgs.builder().noStopWords().build(); // results = redis.ftSearch(STOPWORDS_INDEX, "foo", noStopWordsArgs); // assertThat(results.getCount()).isEqualTo(1); // "foo" should be found when stop words are disabled @@ -156,10 +156,9 @@ void testStopWordsManagement() { @Test void testTokenizationAndEscaping() { // Create index for testing tokenization - FieldArgs textField = TextFieldArgs. builder().name("text").build(); + FieldArgs textField = TextFieldArgs.builder().name("text").build(); - CreateArgs createArgs = CreateArgs. builder().withPrefix(DOCUMENT_PREFIX) - .on(CreateArgs.TargetType.HASH).build(); + CreateArgs createArgs = CreateArgs.builder().withPrefix(DOCUMENT_PREFIX).on(CreateArgs.TargetType.HASH).build(); redis.ftCreate(TOKENIZATION_INDEX, createArgs, Collections.singletonList(textField)); @@ -177,7 +176,7 @@ void testTokenizationAndEscaping() { redis.hmset("doc:3", doc3); // Test 1: Punctuation marks separate tokens - SearchReply results = redis.ftSearch(TOKENIZATION_INDEX, "hello"); + SearchReply results = redis.ftSearch(TOKENIZATION_INDEX, "hello"); // FIXME seems that doc:2 is created with hello\\-world instead of hello\-world assertThat(results.getCount()).isEqualTo(1); // Both "hello-world" and "hello\\-world" @@ -214,12 +213,11 @@ void testTokenizationAndEscaping() { @Test void testSortingByIndexedFields() { // Create index with sortable fields - FieldArgs firstNameField = TextFieldArgs. builder().name("first_name").sortable().build(); - FieldArgs lastNameField = TextFieldArgs. builder().name("last_name").sortable().build(); - FieldArgs ageField = NumericFieldArgs. builder().name("age").sortable().build(); + FieldArgs firstNameField = TextFieldArgs.builder().name("first_name").sortable().build(); + FieldArgs lastNameField = TextFieldArgs.builder().name("last_name").sortable().build(); + FieldArgs ageField = NumericFieldArgs.builder().name("age").sortable().build(); - CreateArgs createArgs = CreateArgs. builder().withPrefix(USER_PREFIX) - .on(CreateArgs.TargetType.HASH).build(); + CreateArgs createArgs = CreateArgs.builder().withPrefix(USER_PREFIX).on(CreateArgs.TargetType.HASH).build(); redis.ftCreate(SORTING_INDEX, createArgs, Arrays.asList(firstNameField, lastNameField, ageField)); @@ -243,27 +241,27 @@ void testSortingByIndexedFields() { redis.hmset("user:3", user3); // Test 1: Sort by first name descending - SortByArgs sortByFirstName = SortByArgs. builder().attribute("first_name").descending().build(); - SearchArgs sortArgs = SearchArgs. builder().sortBy(sortByFirstName).build(); - SearchReply results = redis.ftSearch(SORTING_INDEX, "@last_name:jones", sortArgs); + SortByArgs sortByFirstName = SortByArgs.builder().attribute("first_name").descending().build(); + SearchArgs sortArgs = SearchArgs. builder().sortBy(sortByFirstName).build(); + SearchReply results = redis.ftSearch(SORTING_INDEX, "@last_name:jones", sortArgs); assertThat(results.getCount()).isEqualTo(2); assertThat(results.getResults()).hasSize(2); // Due to normalization, "bob" comes before "alice" in descending order - assertThat(results.getResults().get(0).getFields().get("first_name")).isEqualTo("bob"); - assertThat(results.getResults().get(1).getFields().get("first_name")).isEqualTo("alice"); + assertThat(results.getResults().get(0).getFields().get("first_name").asString()).isEqualTo("bob"); + assertThat(results.getResults().get(1).getFields().get("first_name").asString()).isEqualTo("alice"); // Test 2: Sort by age ascending - SortByArgs sortByAge = SortByArgs. builder().attribute("age").build(); - SearchArgs ageSort = SearchArgs. builder().sortBy(sortByAge).build(); + SortByArgs sortByAge = SortByArgs.builder().attribute("age").build(); + SearchArgs ageSort = SearchArgs. builder().sortBy(sortByAge).build(); results = redis.ftSearch(SORTING_INDEX, "*", ageSort); assertThat(results.getCount()).isEqualTo(3); assertThat(results.getResults()).hasSize(3); // Verify age sorting: 28, 35, 36 - assertThat(results.getResults().get(0).getFields().get("age")).isEqualTo("28"); - assertThat(results.getResults().get(1).getFields().get("age")).isEqualTo("35"); - assertThat(results.getResults().get(2).getFields().get("age")).isEqualTo("36"); + assertThat(results.getResults().get(0).getFields().get("age").asString()).isEqualTo("28"); + assertThat(results.getResults().get(1).getFields().get("age").asString()).isEqualTo("35"); + assertThat(results.getResults().get(2).getFields().get("age").asString()).isEqualTo("36"); // Cleanup redis.ftDropindex(SORTING_INDEX); @@ -276,12 +274,11 @@ void testSortingByIndexedFields() { @Test void testTagFieldOperations() { // Create index with tag fields using custom separator and case sensitivity - FieldArgs titleField = TextFieldArgs. builder().name("title").build(); - FieldArgs categoriesField = TagFieldArgs. builder().name("categories").separator(";").build(); - FieldArgs tagsField = TagFieldArgs. builder().name("tags").caseSensitive().build(); + FieldArgs titleField = TextFieldArgs.builder().name("title").build(); + FieldArgs categoriesField = TagFieldArgs. builder().name("categories").separator(";").build(); + FieldArgs tagsField = TagFieldArgs. builder().name("tags").caseSensitive().build(); - CreateArgs createArgs = CreateArgs. builder().withPrefix(PRODUCT_PREFIX) - .on(CreateArgs.TargetType.HASH).build(); + CreateArgs createArgs = CreateArgs.builder().withPrefix(PRODUCT_PREFIX).on(CreateArgs.TargetType.HASH).build(); redis.ftCreate(TAGS_INDEX, createArgs, Arrays.asList(titleField, categoriesField, tagsField)); @@ -305,7 +302,7 @@ void testTagFieldOperations() { redis.hmset("product:3", product3); // Test 1: Search by category with custom separator - SearchReply results = redis.ftSearch(TAGS_INDEX, "@categories:{gaming}"); + SearchReply results = redis.ftSearch(TAGS_INDEX, "@categories:{gaming}"); assertThat(results.getCount()).isEqualTo(2); // Gaming laptop and mouse results = redis.ftSearch(TAGS_INDEX, "@categories:{computers}"); @@ -349,12 +346,11 @@ void testTagFieldOperations() { @Test void testHighlightingAndSummarization() { // Create index for highlighting tests - FieldArgs titleField = TextFieldArgs. builder().name("title").build(); - FieldArgs contentField = TextFieldArgs. builder().name("content").build(); - FieldArgs authorField = TextFieldArgs. builder().name("author").build(); + FieldArgs titleField = TextFieldArgs.builder().name("title").build(); + FieldArgs contentField = TextFieldArgs.builder().name("content").build(); + FieldArgs authorField = TextFieldArgs.builder().name("author").build(); - CreateArgs createArgs = CreateArgs. builder().withPrefix(BOOK_PREFIX) - .on(CreateArgs.TargetType.HASH).build(); + CreateArgs createArgs = CreateArgs.builder().withPrefix(BOOK_PREFIX).on(CreateArgs.TargetType.HASH).build(); redis.ftCreate(HIGHLIGHT_INDEX, createArgs, Arrays.asList(titleField, contentField, authorField)); @@ -381,54 +377,52 @@ void testHighlightingAndSummarization() { redis.hmset("book:2", book2); // Test 1: Basic highlighting with default tags - HighlightArgs basicHighlight = HighlightArgs. builder().build(); - SearchArgs highlightArgs = SearchArgs. builder().highlightArgs(basicHighlight).build(); + HighlightArgs basicHighlight = HighlightArgs.builder().build(); + SearchArgs highlightArgs = SearchArgs. builder().highlightArgs(basicHighlight).build(); - SearchReply results = redis.ftSearch(HIGHLIGHT_INDEX, "Redis", highlightArgs); + SearchReply results = redis.ftSearch(HIGHLIGHT_INDEX, "Redis", highlightArgs); assertThat(results.getCount()).isEqualTo(1); // Check that highlighting tags are present in the content - String highlightedContent = results.getResults().get(0).getFields().get("content"); + String highlightedContent = results.getResults().get(0).getFields().get("content").asString(); assertThat(highlightedContent).contains("Redis"); // Default highlighting tags // Test 2: Custom highlighting tags - SearchArgs customHighlightArgs = SearchArgs. builder().highlightField("title") - .highlightField("content").highlightTags("", "").build(); + SearchArgs customHighlightArgs = SearchArgs. builder().highlightField("title").highlightField("content") + .highlightTags("", "").build(); results = redis.ftSearch(HIGHLIGHT_INDEX, "database", customHighlightArgs); assertThat(results.getCount()).isEqualTo(2); // Check custom highlighting tags - for (SearchReply.SearchResult result : results.getResults()) { - String content = result.getFields().get("content"); + for (SearchReply.SearchResult result : results.getResults()) { + String content = result.getFields().get("content").asString(); if (content.contains("database")) { assertThat(content).contains("database"); } } // Test 3: Summarization with custom parameters - SummarizeArgs summarize = SummarizeArgs. builder().field("content").fragments(2).len(25) - .separator(" ... ").build(); - SearchArgs summarizeArgs = SearchArgs. builder().summarizeArgs(summarize).build(); + SummarizeArgs summarize = SummarizeArgs.builder().field("content").fragments(2).len(25).separator(" ... ").build(); + SearchArgs summarizeArgs = SearchArgs. builder().summarizeArgs(summarize).build(); results = redis.ftSearch(HIGHLIGHT_INDEX, "patterns", summarizeArgs); assertThat(results.getCount()).isEqualTo(1); // Check that content is summarized - String summarizedContent = results.getResults().get(0).getFields().get("content"); + String summarizedContent = results.getResults().get(0).getFields().get("content").asString(); assertThat(summarizedContent).contains(" ... "); // Custom separator assertThat(summarizedContent.length()).isLessThan(book2.get("content").length()); // Should be shorter // Test 4: Combined highlighting and summarization - HighlightArgs combineHighlight = HighlightArgs. builder().field("content") - .tags("**", "**").build(); - SearchArgs combinedArgs = SearchArgs. builder().highlightArgs(combineHighlight) + HighlightArgs combineHighlight = HighlightArgs.builder().field("content").tags("**", "**").build(); + SearchArgs combinedArgs = SearchArgs. builder().highlightArgs(combineHighlight) .summarizeField("content").summarizeFragments(1).summarizeLen(30).build(); results = redis.ftSearch(HIGHLIGHT_INDEX, "Redis data", combinedArgs); assertThat(results.getCount()).isEqualTo(1); - String combinedContent = results.getResults().get(0).getFields().get("content"); + String combinedContent = results.getResults().get(0).getFields().get("content").asString(); assertThat(combinedContent).contains("**"); // Highlighting markers assertThat(combinedContent).contains("..."); // Default summarization separator @@ -444,12 +438,11 @@ void testHighlightingAndSummarization() { @Test void testDocumentScoring() { // Create index for scoring tests - TextFieldArgs titleField = TextFieldArgs. builder().name("title").weight(2).build(); - TextFieldArgs contentField = TextFieldArgs. builder().name("content").build(); - NumericFieldArgs ratingField = NumericFieldArgs. builder().name("rating").build(); + TextFieldArgs titleField = TextFieldArgs.builder().name("title").weight(2).build(); + TextFieldArgs contentField = TextFieldArgs.builder().name("content").build(); + NumericFieldArgs ratingField = NumericFieldArgs.builder().name("rating").build(); - CreateArgs createArgs = CreateArgs. builder().withPrefix(REVIEW_PREFIX) - .on(CreateArgs.TargetType.HASH).build(); + CreateArgs createArgs = CreateArgs.builder().withPrefix(REVIEW_PREFIX).on(CreateArgs.TargetType.HASH).build(); redis.ftCreate(SCORING_INDEX, createArgs, Arrays.asList(titleField, contentField, ratingField)); @@ -474,23 +467,22 @@ void testDocumentScoring() { redis.hmset("review:3", review3); // Test 1: Default BM25 scoring with scores - SearchArgs withScores = SearchArgs. builder().withScores().build(); - SearchReply results = redis.ftSearch(SCORING_INDEX, "Redis", withScores); + SearchArgs withScores = SearchArgs. builder().withScores().build(); + SearchReply results = redis.ftSearch(SCORING_INDEX, "Redis", withScores); assertThat(results.getCount()).isEqualTo(3); assertThat(results.getResults()).hasSize(3); // Verify scores are present and ordered (higher scores first) double previousScore = Double.MAX_VALUE; - for (SearchReply.SearchResult result : results.getResults()) { + for (SearchReply.SearchResult result : results.getResults()) { assertThat(result.getScore()).isNotNull(); assertThat(result.getScore()).isLessThanOrEqualTo(previousScore); previousScore = result.getScore(); } // Test 2: TFIDF scoring - SearchArgs tfidfScoring = SearchArgs. builder().withScores() - .scorer(ScoringFunction.TF_IDF).build(); + SearchArgs tfidfScoring = SearchArgs. builder().withScores().scorer(ScoringFunction.TF_IDF).build(); results = redis.ftSearch(SCORING_INDEX, "Redis guide", tfidfScoring); assertThat(results.getCount()).isEqualTo(2); @@ -498,8 +490,7 @@ void testDocumentScoring() { assertThat(results.getResults().get(0).getId()).isEqualTo("review:3"); // Test 3: DISMAX scoring - SearchArgs dismaxScoring = SearchArgs. builder().withScores() - .scorer(ScoringFunction.DIS_MAX).build(); + SearchArgs dismaxScoring = SearchArgs. builder().withScores().scorer(ScoringFunction.DIS_MAX).build(); results = redis.ftSearch(SCORING_INDEX, "Redis guide", dismaxScoring); assertThat(results.getCount()).isEqualTo(2); @@ -507,8 +498,8 @@ void testDocumentScoring() { assertThat(results.getResults().get(0).getId()).isEqualTo("review:2"); // Test 4: DOCSCORE scoring (uses document's inherent score) - SearchArgs docScoring = SearchArgs. builder().withScores() - .scorer(ScoringFunction.DOCUMENT_SCORE).build(); + SearchArgs docScoring = SearchArgs. builder().withScores().scorer(ScoringFunction.DOCUMENT_SCORE) + .build(); results = redis.ftSearch(SCORING_INDEX, "*", docScoring); assertThat(results.getCount()).isEqualTo(3); @@ -526,10 +517,10 @@ void testDocumentScoring() { @Test void testStemmingAndLanguageSupport() { // Test 1: English stemming - FieldArgs englishWordField = TextFieldArgs. builder().name("word").build(); + FieldArgs englishWordField = TextFieldArgs.builder().name("word").build(); - CreateArgs englishArgs = CreateArgs. builder().withPrefix(WORD_PREFIX) - .on(CreateArgs.TargetType.HASH).defaultLanguage(DocumentLanguage.ENGLISH).build(); + CreateArgs englishArgs = CreateArgs.builder().withPrefix(WORD_PREFIX).on(CreateArgs.TargetType.HASH) + .defaultLanguage(DocumentLanguage.ENGLISH).build(); redis.ftCreate(STEMMING_INDEX, englishArgs, Collections.singletonList(englishWordField)); @@ -552,7 +543,7 @@ void testStemmingAndLanguageSupport() { // Test stemming: searching for "run" should find all variations // FIXME Seems like a bug in the server, "runner" needs to also be stemmed, but it is not - SearchReply results = redis.ftSearch(STEMMING_INDEX, "run"); + SearchReply results = redis.ftSearch(STEMMING_INDEX, "run"); assertThat(results.getCount()).isEqualTo(3); // All forms should be found due to stemming // Test stemming: searching for "running" should also find all variations @@ -561,7 +552,7 @@ void testStemmingAndLanguageSupport() { assertThat(results.getCount()).isEqualTo(3); // Test VERBATIM search (disable stemming) - SearchArgs verbatimArgs = SearchArgs. builder().verbatim().build(); + SearchArgs verbatimArgs = SearchArgs. builder().verbatim().build(); results = redis.ftSearch(STEMMING_INDEX, "run", verbatimArgs); assertThat(results.getCount()).isEqualTo(1); // Only exact match @@ -569,8 +560,7 @@ void testStemmingAndLanguageSupport() { assertThat(results.getCount()).isEqualTo(1); // Only exact match // Test with language parameter in search (should override index language) - SearchArgs languageArgs = SearchArgs. builder().language(DocumentLanguage.GERMAN) - .build(); + SearchArgs languageArgs = SearchArgs. builder().language(DocumentLanguage.GERMAN).build(); results = redis.ftSearch(STEMMING_INDEX, "run", languageArgs); // German stemming rules would be different, but for this test we just verify it works assertThat(results.getCount()).isGreaterThanOrEqualTo(1); @@ -579,10 +569,10 @@ void testStemmingAndLanguageSupport() { redis.ftDropindex(STEMMING_INDEX); // Test 2: German stemming example from documentation - FieldArgs germanWordField = TextFieldArgs. builder().name("wort").build(); + FieldArgs germanWordField = TextFieldArgs.builder().name("wort").build(); - CreateArgs germanArgs = CreateArgs. builder().withPrefix("wort:") - .on(CreateArgs.TargetType.HASH).defaultLanguage(DocumentLanguage.GERMAN).build(); + CreateArgs germanArgs = CreateArgs.builder().withPrefix("wort:").on(CreateArgs.TargetType.HASH) + .defaultLanguage(DocumentLanguage.GERMAN).build(); redis.ftCreate("idx:german", germanArgs, Collections.singletonList(germanWordField)); @@ -607,11 +597,10 @@ void testStemmingAndLanguageSupport() { @Test void testPhoneticMatchers() { // Test 1: English phonetic matching - FieldArgs englishNameField = TextFieldArgs. builder().name("name") - .phonetic(TextFieldArgs.PhoneticMatcher.ENGLISH).build(); + FieldArgs englishNameField = TextFieldArgs.builder().name("name").phonetic(TextFieldArgs.PhoneticMatcher.ENGLISH) + .build(); - CreateArgs englishArgs = CreateArgs. builder().withPrefix("person:") - .on(CreateArgs.TargetType.HASH).build(); + CreateArgs englishArgs = CreateArgs.builder().withPrefix("person:").on(CreateArgs.TargetType.HASH).build(); redis.ftCreate("phonetic-en-idx", englishArgs, Collections.singletonList(englishNameField)); @@ -623,7 +612,7 @@ void testPhoneticMatchers() { redis.hset("person:5", "name", "Jonson"); // Search for "Smith" should find phonetically similar names - SearchReply results = redis.ftSearch("phonetic-en-idx", "@name:Smith"); + SearchReply results = redis.ftSearch("phonetic-en-idx", "@name:Smith"); assertThat(results.getCount()).isGreaterThanOrEqualTo(2); // Should find Smith and Smyth at minimum // Search for "Johnson" should find phonetically similar names @@ -633,11 +622,9 @@ void testPhoneticMatchers() { redis.ftDropindex("phonetic-en-idx"); // Test 2: French phonetic matching - FieldArgs frenchNameField = TextFieldArgs. builder().name("nom") - .phonetic(TextFieldArgs.PhoneticMatcher.FRENCH).build(); + FieldArgs frenchNameField = TextFieldArgs.builder().name("nom").phonetic(TextFieldArgs.PhoneticMatcher.FRENCH).build(); - CreateArgs frenchArgs = CreateArgs. builder().withPrefix("personne:") - .on(CreateArgs.TargetType.HASH).build(); + CreateArgs frenchArgs = CreateArgs.builder().withPrefix("personne:").on(CreateArgs.TargetType.HASH).build(); redis.ftCreate("phonetic-fr-idx", frenchArgs, Collections.singletonList(frenchNameField)); @@ -657,11 +644,10 @@ void testPhoneticMatchers() { redis.ftDropindex("phonetic-fr-idx"); // Test 3: Spanish phonetic matching - FieldArgs spanishNameField = TextFieldArgs. builder().name("nombre") - .phonetic(TextFieldArgs.PhoneticMatcher.SPANISH).build(); + FieldArgs spanishNameField = TextFieldArgs.builder().name("nombre").phonetic(TextFieldArgs.PhoneticMatcher.SPANISH) + .build(); - CreateArgs spanishArgs = CreateArgs. builder().withPrefix("persona:") - .on(CreateArgs.TargetType.HASH).build(); + CreateArgs spanishArgs = CreateArgs.builder().withPrefix("persona:").on(CreateArgs.TargetType.HASH).build(); redis.ftCreate("phonetic-es-idx", spanishArgs, Collections.singletonList(spanishNameField)); @@ -678,11 +664,10 @@ void testPhoneticMatchers() { redis.ftDropindex("phonetic-es-idx"); // Test 4: Portuguese phonetic matching - FieldArgs portugueseNameField = TextFieldArgs. builder().name("nome") - .phonetic(TextFieldArgs.PhoneticMatcher.PORTUGUESE).build(); + FieldArgs portugueseNameField = TextFieldArgs.builder().name("nome").phonetic(TextFieldArgs.PhoneticMatcher.PORTUGUESE) + .build(); - CreateArgs portugueseArgs = CreateArgs. builder().withPrefix("pessoa:") - .on(CreateArgs.TargetType.HASH).build(); + CreateArgs portugueseArgs = CreateArgs.builder().withPrefix("pessoa:").on(CreateArgs.TargetType.HASH).build(); redis.ftCreate("phonetic-pt-idx", portugueseArgs, Collections.singletonList(portugueseNameField)); @@ -706,10 +691,10 @@ void testPhoneticMatchers() { @Test void testNoStemmingOption() { // Test 1: Field with stemming enabled (default) - FieldArgs stemmingField = TextFieldArgs. builder().name("content_stemmed").build(); + FieldArgs stemmingField = TextFieldArgs.builder().name("content_stemmed").build(); - CreateArgs stemmingArgs = CreateArgs. builder().withPrefix("stem:") - .on(CreateArgs.TargetType.HASH).defaultLanguage(DocumentLanguage.ENGLISH).build(); + CreateArgs stemmingArgs = CreateArgs.builder().withPrefix("stem:").on(CreateArgs.TargetType.HASH) + .defaultLanguage(DocumentLanguage.ENGLISH).build(); redis.ftCreate("stemming-idx", stemmingArgs, Collections.singletonList(stemmingField)); @@ -725,10 +710,10 @@ void testNoStemmingOption() { redis.ftDropindex("stemming-idx"); // Test 2: Field with stemming disabled - FieldArgs noStemmingField = TextFieldArgs. builder().name("content_exact").noStem().build(); + FieldArgs noStemmingField = TextFieldArgs.builder().name("content_exact").noStem().build(); - CreateArgs noStemmingArgs = CreateArgs. builder().withPrefix("nostem:") - .on(CreateArgs.TargetType.HASH).defaultLanguage(DocumentLanguage.ENGLISH).build(); + CreateArgs noStemmingArgs = CreateArgs.builder().withPrefix("nostem:").on(CreateArgs.TargetType.HASH) + .defaultLanguage(DocumentLanguage.ENGLISH).build(); redis.ftCreate("nostemming-idx", noStemmingArgs, Collections.singletonList(noStemmingField)); @@ -743,7 +728,7 @@ void testNoStemmingOption() { Wait.untilEquals(1L, () -> redis.ftSearch("nostemming-idx", "@content_exact:run").getCount()).waitOrTimeout(); // Search for "running" should only find exact matches - SearchReply results = redis.ftSearch("nostemming-idx", "@content_exact:running"); + SearchReply results = redis.ftSearch("nostemming-idx", "@content_exact:running"); assertThat(results.getCount()).isEqualTo(1); // Only "running quickly" // Search for "runs" should only find exact matches @@ -753,11 +738,11 @@ void testNoStemmingOption() { redis.ftDropindex("nostemming-idx"); // Test 3: Mixed fields - one with stemming, one without - FieldArgs mixedStemField = TextFieldArgs. builder().name("stemmed_content").build(); - FieldArgs mixedNoStemField = TextFieldArgs. builder().name("exact_content").noStem().build(); + FieldArgs mixedStemField = TextFieldArgs.builder().name("stemmed_content").build(); + FieldArgs mixedNoStemField = TextFieldArgs.builder().name("exact_content").noStem().build(); - CreateArgs mixedArgs = CreateArgs. builder().withPrefix("mixed:") - .on(CreateArgs.TargetType.HASH).defaultLanguage(DocumentLanguage.ENGLISH).build(); + CreateArgs mixedArgs = CreateArgs.builder().withPrefix("mixed:").on(CreateArgs.TargetType.HASH) + .defaultLanguage(DocumentLanguage.ENGLISH).build(); redis.ftCreate("mixed-idx", mixedArgs, Arrays.asList(mixedStemField, mixedNoStemField)); @@ -789,10 +774,9 @@ void testNoStemmingOption() { @Test void testWithSuffixTrieOption() { // Test 1: Field without suffix trie (default) - FieldArgs normalField = TextFieldArgs. builder().name("title").build(); + FieldArgs normalField = TextFieldArgs.builder().name("title").build(); - CreateArgs normalArgs = CreateArgs. builder().withPrefix("normal:") - .on(CreateArgs.TargetType.HASH).build(); + CreateArgs normalArgs = CreateArgs.builder().withPrefix("normal:").on(CreateArgs.TargetType.HASH).build(); redis.ftCreate("normal-idx", normalArgs, Collections.singletonList(normalField)); @@ -803,16 +787,15 @@ void testWithSuffixTrieOption() { redis.hset("normal:4", "title", "Programming Languages"); // Basic search should work - SearchReply results = redis.ftSearch("normal-idx", "@title:Java*"); + SearchReply results = redis.ftSearch("normal-idx", "@title:Java*"); assertThat(results.getCount()).isEqualTo(2); // JavaScript and Java redis.ftDropindex("normal-idx"); // Test 2: Field with suffix trie enabled - FieldArgs suffixTrieField = TextFieldArgs. builder().name("title").withSuffixTrie().build(); + FieldArgs suffixTrieField = TextFieldArgs.builder().name("title").withSuffixTrie().build(); - CreateArgs suffixTrieArgs = CreateArgs. builder().withPrefix("suffix:") - .on(CreateArgs.TargetType.HASH).build(); + CreateArgs suffixTrieArgs = CreateArgs.builder().withPrefix("suffix:").on(CreateArgs.TargetType.HASH).build(); redis.ftCreate("suffix-idx", suffixTrieArgs, Collections.singletonList(suffixTrieField)); @@ -843,10 +826,9 @@ void testWithSuffixTrieOption() { redis.ftDropindex("suffix-idx"); // Test 3: Autocomplete-style functionality with suffix trie - FieldArgs autocompleteField = TextFieldArgs. builder().name("product_name").withSuffixTrie().build(); + FieldArgs autocompleteField = TextFieldArgs.builder().name("product_name").withSuffixTrie().build(); - CreateArgs autocompleteArgs = CreateArgs. builder().withPrefix("product:") - .on(CreateArgs.TargetType.HASH).build(); + CreateArgs autocompleteArgs = CreateArgs.builder().withPrefix("product:").on(CreateArgs.TargetType.HASH).build(); redis.ftCreate("autocomplete-idx", autocompleteArgs, Collections.singletonList(autocompleteField)); @@ -881,10 +863,9 @@ void testWithSuffixTrieOption() { redis.ftDropindex("autocomplete-idx"); // Test 4: Performance comparison - complex wildcard queries - FieldArgs performanceField = TextFieldArgs. builder().name("description").withSuffixTrie().build(); + FieldArgs performanceField = TextFieldArgs.builder().name("description").withSuffixTrie().build(); - CreateArgs performanceArgs = CreateArgs. builder().withPrefix("perf:") - .on(CreateArgs.TargetType.HASH).build(); + CreateArgs performanceArgs = CreateArgs.builder().withPrefix("perf:").on(CreateArgs.TargetType.HASH).build(); redis.ftCreate("performance-idx", performanceArgs, Collections.singletonList(performanceField)); diff --git a/src/test/java/io/lettuce/core/search/RediSearchAggregateIntegrationTests.java b/src/test/java/io/lettuce/core/search/RediSearchAggregateIntegrationTests.java index 1e7a708017..c5cd93291c 100644 --- a/src/test/java/io/lettuce/core/search/RediSearchAggregateIntegrationTests.java +++ b/src/test/java/io/lettuce/core/search/RediSearchAggregateIntegrationTests.java @@ -8,6 +8,7 @@ package io.lettuce.core.search; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assumptions.assumeTrue; import java.time.Duration; import java.util.ArrayList; @@ -22,6 +23,7 @@ import io.lettuce.TestTags; import io.lettuce.core.ClientOptions; import io.lettuce.core.RedisClient; +import io.lettuce.core.RedisCommandExecutionException; import io.lettuce.core.RedisURI; import io.lettuce.core.TestSupport; import org.junit.jupiter.api.BeforeEach; @@ -77,11 +79,10 @@ void setUp() { @Test void shouldPerformBasicAggregation() { // Create an index with prefix - List> fields = Arrays.asList(TextFieldArgs. builder().name("title").build(), - TextFieldArgs. builder().name("category").build()); + List fields = Arrays.asList(TextFieldArgs.builder().name("title").build(), + TextFieldArgs.builder().name("category").build()); - CreateArgs createArgs = CreateArgs. builder().withPrefix("doc:") - .on(CreateArgs.TargetType.HASH).build(); + CreateArgs createArgs = CreateArgs.builder().withPrefix("doc:").on(CreateArgs.TargetType.HASH).build(); assertThat(redis.ftCreate("basic-test-idx", createArgs, fields)).isEqualTo("OK"); @@ -107,11 +108,11 @@ void shouldPerformBasicAggregation() { assertThat(redis.hmset("doc:4", doc4)).isEqualTo("OK"); // First, let's verify the documents are indexed by doing a search - SearchReply searchResult = redis.ftSearch("basic-test-idx", "*"); + SearchReply searchResult = redis.ftSearch("basic-test-idx", "*"); assertThat(searchResult.getCount()).isEqualTo(4); // Verify documents are indexed // Perform basic aggregation without LOAD - should return empty field maps - AggregationReply result = redis.ftAggregate("basic-test-idx", "*"); + AggregationReply result = redis.ftAggregate("basic-test-idx", "*"); assertThat(result).isNotNull(); // If documents are indexed, we should have 1 aggregation group (no grouping) @@ -125,7 +126,7 @@ void shouldPerformBasicAggregation() { // the single reply // Each result should be empty since no LOAD was specified - for (SearchReply.SearchResult aggregateResult : result.getReplies().get(0).getResults()) { + for (SearchReply.SearchResult aggregateResult : result.getReplies().get(0).getResults()) { assertThat(aggregateResult.getFields()).isEmpty(); } } else { @@ -136,11 +137,74 @@ void shouldPerformBasicAggregation() { assertThat(redis.ftDropindex("basic-test-idx")).isEqualTo("OK"); } + @Test + void shouldPerformCollectAggregation() { + // COLLECT is gated behind search-enable-unstable-features; enable it and skip the test on builds where the + // reducer (or the config flag) is not available yet. + try { + redis.configSet("search-enable-unstable-features", "yes"); + } catch (RedisCommandExecutionException e) { + assumeTrue(false, "search-enable-unstable-features is not configurable on this Redis build: " + e.getMessage()); + } + + List fields = Arrays.asList(TagFieldArgs.builder().name("fruit").build(), + TagFieldArgs.builder().name("color").build(), NumericFieldArgs.builder().name("sweetness").sortable().build()); + CreateArgs createArgs = CreateArgs.builder().withPrefix("fruit:").on(CreateArgs.TargetType.HASH).build(); + assertThat(redis.ftCreate("collect-test-idx", createArgs, fields)).isEqualTo("OK"); + + redis.hmset("fruit:1", mapOf("fruit", "apple", "color", "yellow", "sweetness", "6")); + redis.hmset("fruit:2", mapOf("fruit", "banana", "color", "yellow", "sweetness", "5")); + redis.hmset("fruit:3", mapOf("fruit", "lemon", "color", "yellow", "sweetness", "2")); + redis.hmset("fruit:4", mapOf("fruit", "cherry", "color", "red", "sweetness", "7")); + + AggregateArgs args = AggregateArgs.builder() + .groupBy(GroupBy.of("color") + .reduce(Reducer.collect().fields("fruit", "sweetness") + .sortBy(new AggregateArgs.SortProperty("sweetness", SortDirection.DESC)).limit(0, 2).as("top"))) + .build(); + + AggregationReply result; + try { + result = redis.ftAggregate("collect-test-idx", "*", args); + } catch (RedisCommandExecutionException e) { + assumeTrue(false, "FT.AGGREGATE REDUCE COLLECT not supported by this Redis Search build: " + e.getMessage()); + return; + } + + assertThat(result.getReplies()).hasSize(1); + SearchReply reply = result.getReplies().get(0); + + SearchReply.SearchResult yellow = reply.getResults().stream() + .filter(r -> "yellow".equals(r.getFields().get("color").asString())).findFirst() + .orElseThrow(() -> new AssertionError("no yellow group in " + reply.getResults())); + + // The raw shape of a collected entry differs between RESP2 and RESP3; FieldValue#asMap() normalizes both + // to one map per collected entry. + List> collected = yellow.getFields().get("top").asList().stream().map(FieldValue::asMap) + .collect(Collectors.toList()); + // LIMIT 0 2 caps the group at 2 entries, SORTBY @sweetness DESC keeps the two sweetest (apple=6, banana=5). + assertThat(collected).hasSize(2); + assertThat(collected.get(0).get("fruit").asString()).isEqualTo("apple"); + assertThat(collected.get(0).get("sweetness").asString()).isEqualTo("6"); + assertThat(collected.get(1).get("fruit").asString()).isEqualTo("banana"); + assertThat(collected.get(1).get("sweetness").asString()).isEqualTo("5"); + + assertThat(redis.ftDropindex("collect-test-idx")).isEqualTo("OK"); + } + + private static Map mapOf(String... kv) { + Map map = new HashMap<>(); + for (int i = 0; i < kv.length; i += 2) { + map.put(kv[i], kv[i + 1]); + } + return map; + } + @Test void shouldPerformAggregationWithArgs() { // Create an index - List> fields = Arrays.asList(TextFieldArgs. builder().name("title").build(), - TextFieldArgs. builder().name("category").build()); + List fields = Arrays.asList(TextFieldArgs.builder().name("title").build(), + TextFieldArgs.builder().name("category").build()); assertThat(redis.ftCreate("args-test-idx", fields)).isEqualTo("OK"); @@ -161,19 +225,18 @@ void shouldPerformAggregationWithArgs() { assertThat(redis.hmset("doc:3", doc3)).isEqualTo("OK"); // Perform aggregation with arguments - LOAD fields - AggregateArgs args = AggregateArgs. builder().verbatim().load("title").load("category") - .build(); + AggregateArgs args = AggregateArgs.builder().verbatim().load("title").load("category").build(); - AggregationReply result = redis.ftAggregate("args-test-idx", "*", args); + AggregationReply result = redis.ftAggregate("args-test-idx", "*", args); assertThat(result).isNotNull(); assertThat(result.getAggregationGroups()).isEqualTo(1); // Should have 1 aggregation group (no grouping) assertThat(result.getReplies()).hasSize(1); // Should have 1 SearchReply containing all documents - SearchReply searchReply = result.getReplies().get(0); + SearchReply searchReply = result.getReplies().get(0); assertThat(searchReply.getResults()).hasSize(3); // Should have 3 documents (doc:1, doc:2, doc:3) // Check that loaded fields are present in results - for (SearchReply.SearchResult aggregateResult : searchReply.getResults()) { + for (SearchReply.SearchResult aggregateResult : searchReply.getResults()) { assertThat(aggregateResult.getFields().containsKey("title")).isTrue(); assertThat(aggregateResult.getFields().containsKey("category")).isTrue(); assertThat(aggregateResult.getFields().get("title")).isNotNull(); @@ -186,8 +249,8 @@ void shouldPerformAggregationWithArgs() { @Test void shouldPerformAggregationWithParams() { // Create an index - List> fields = Arrays.asList(TextFieldArgs. builder().name("title").build(), - TextFieldArgs. builder().name("category").build()); + List fields = Arrays.asList(TextFieldArgs.builder().name("title").build(), + TextFieldArgs.builder().name("category").build()); assertThat(redis.ftCreate("params-test-idx", fields)).isEqualTo("OK"); @@ -208,22 +271,21 @@ void shouldPerformAggregationWithParams() { assertThat(redis.hmset("doc:3", doc3)).isEqualTo("OK"); // Perform aggregation with parameters - requires DIALECT 2 - AggregateArgs args = AggregateArgs. builder().load("title").load("category") - .param("cat", "electronics").build(); + AggregateArgs args = AggregateArgs.builder().load("title").load("category").param("cat", "electronics").build(); - AggregationReply result = redis.ftAggregate("params-test-idx", "@category:$cat", args); + AggregationReply result = redis.ftAggregate("params-test-idx", "@category:$cat", args); assertThat(result).isNotNull(); assertThat(result.getAggregationGroups()).isEqualTo(1); // Should have 1 aggregation group (no grouping) assertThat(result.getReplies()).hasSize(1); // Should have 1 SearchReply containing all documents - SearchReply searchReply = result.getReplies().get(0); + SearchReply searchReply = result.getReplies().get(0); assertThat(searchReply.getResults()).hasSize(2); // Should have 2 electronics documents // All results should be electronics - for (SearchReply.SearchResult aggregateResult : searchReply.getResults()) { + for (SearchReply.SearchResult aggregateResult : searchReply.getResults()) { assertThat(aggregateResult.getFields().containsKey("title")).isTrue(); assertThat(aggregateResult.getFields().containsKey("category")).isTrue(); - assertThat(aggregateResult.getFields().get("category")).isEqualTo("electronics"); + assertThat(aggregateResult.getFields().get("category").asString()).isEqualTo("electronics"); } assertThat(redis.ftDropindex("params-test-idx")).isEqualTo("OK"); @@ -232,8 +294,8 @@ void shouldPerformAggregationWithParams() { @Test void shouldPerformAggregationWithLoadAll() { // Create an index - List> fields = Arrays.asList(TextFieldArgs. builder().name("title").build(), - TextFieldArgs. builder().name("category").build()); + List fields = Arrays.asList(TextFieldArgs.builder().name("title").build(), + TextFieldArgs.builder().name("category").build()); assertThat(redis.ftCreate("loadall-test-idx", fields)).isEqualTo("OK"); @@ -249,19 +311,19 @@ void shouldPerformAggregationWithLoadAll() { assertThat(redis.hmset("doc:2", doc2)).isEqualTo("OK"); // Perform aggregation with LOAD * (load all fields) - AggregateArgs args = AggregateArgs. builder().loadAll().build(); + AggregateArgs args = AggregateArgs.builder().loadAll().build(); - AggregationReply result = redis.ftAggregate("loadall-test-idx", "*", args); + AggregationReply result = redis.ftAggregate("loadall-test-idx", "*", args); assertThat(result).isNotNull(); assertThat(result.getAggregationGroups()).isEqualTo(1); // Should have 1 aggregation group (no grouping) assertThat(result.getReplies()).hasSize(1); // Should have 1 SearchReply containing all documents - SearchReply searchReply = result.getReplies().get(0); + SearchReply searchReply = result.getReplies().get(0); assertThat(searchReply.getResults()).hasSize(2); // Should have 2 documents (only doc:1 and doc:2 added // in this test) // Check that all fields are loaded - for (SearchReply.SearchResult aggregateResult : searchReply.getResults()) { + for (SearchReply.SearchResult aggregateResult : searchReply.getResults()) { assertThat(aggregateResult.getFields().containsKey("title")).isTrue(); assertThat(aggregateResult.getFields().containsKey("category")).isTrue(); assertThat(aggregateResult.getFields().get("title")).isNotNull(); @@ -274,15 +336,15 @@ void shouldPerformAggregationWithLoadAll() { @Test void shouldHandleEmptyResults() { // Create an index - List> fields = Arrays.asList(TextFieldArgs. builder().name("title").build(), - TextFieldArgs. builder().name("category").build()); + List fields = Arrays.asList(TextFieldArgs.builder().name("title").build(), + TextFieldArgs.builder().name("category").build()); assertThat(redis.ftCreate("empty-test-idx", fields)).isEqualTo("OK"); // Don't add any documents // Perform aggregation on empty index - AggregationReply result = redis.ftAggregate("empty-test-idx", "*"); + AggregationReply result = redis.ftAggregate("empty-test-idx", "*"); assertThat(result).isNotNull(); assertThat(result.getAggregationGroups()).isEqualTo(1); // Should have 0 aggregation groups for empty @@ -296,12 +358,12 @@ void shouldHandleEmptyResults() { @Test void shouldDemonstrateAdvancedAggregationScenarios() { // Create an index for e-commerce data similar to Redis documentation examples - List> fields = Arrays.asList(TextFieldArgs. builder().name("title").build(), - TextFieldArgs. builder().name("brand").sortable().build(), - TextFieldArgs. builder().name("category").sortable().build(), - NumericFieldArgs. builder().name("price").sortable().build(), - NumericFieldArgs. builder().name("rating").sortable().build(), - NumericFieldArgs. builder().name("stock").sortable().build()); + List fields = Arrays.asList(TextFieldArgs.builder().name("title").build(), + TextFieldArgs.builder().name("brand").sortable().build(), + TextFieldArgs.builder().name("category").sortable().build(), + NumericFieldArgs.builder().name("price").sortable().build(), + NumericFieldArgs.builder().name("rating").sortable().build(), + NumericFieldArgs.builder().name("stock").sortable().build()); assertThat(redis.ftCreate("products-idx", fields)).isEqualTo("OK"); @@ -343,137 +405,135 @@ NumericFieldArgs. builder().name("rating").sortable().build(), assertThat(redis.hmset("product:4", product4)).isEqualTo("OK"); // Test basic aggregation with all fields loaded - AggregateArgs args = AggregateArgs. builder().loadAll().build(); + AggregateArgs args = AggregateArgs.builder().loadAll().build(); - AggregationReply result = redis.ftAggregate("products-idx", "*", args); + AggregationReply result = redis.ftAggregate("products-idx", "*", args); assertThat(result).isNotNull(); assertThat(result.getAggregationGroups()).isEqualTo(1); // Should have 1 aggregation group (no grouping) assertThat(result.getReplies()).hasSize(1); // Should have 1 SearchReply containing all documents - SearchReply searchReply = result.getReplies().get(0); + SearchReply searchReply = result.getReplies().get(0); assertThat(searchReply.getResults()).hasSize(4); // Verify data structure for future aggregation operations - Set brands = searchReply.getResults().stream().map(r -> r.getFields().get("brand")).collect(Collectors.toSet()); + Set brands = searchReply.getResults().stream().map(r -> r.getFields().get("brand").asString()) + .collect(Collectors.toSet()); assertThat(brands).containsExactlyInAnyOrder("Apple", "Samsung", "Dell"); - Set categories = searchReply.getResults().stream().map(r -> r.getFields().get("category")) + Set categories = searchReply.getResults().stream().map(r -> r.getFields().get("category").asString()) .collect(Collectors.toSet()); assertThat(categories).containsExactlyInAnyOrder("smartphones", "laptops"); // 1. Group by category with statistics - AggregateArgs statsArgs = AggregateArgs. builder() - .groupBy(GroupBy. of("category").reduce(Reducer. count().as("count")) - .reduce(Reducer. avg("@price").as("avg_price")) - .reduce(Reducer. min("@price").as("min_price")) - .reduce(Reducer. max("@price").as("max_price"))) + AggregateArgs statsArgs = AggregateArgs.builder() + .groupBy( + GroupBy.of("category").reduce(Reducer.count().as("count")).reduce(Reducer.avg("@price").as("avg_price")) + .reduce(Reducer.min("@price").as("min_price")).reduce(Reducer.max("@price").as("max_price"))) .build(); - AggregationReply statsResult = redis.ftAggregate("products-idx", "*", statsArgs); + AggregationReply statsResult = redis.ftAggregate("products-idx", "*", statsArgs); assertThat(statsResult).isNotNull(); assertThat(statsResult.getAggregationGroups()).isEqualTo(1); // smartphones and laptops assertThat(statsResult.getReplies()).hasSize(1); - SearchReply statsReply = statsResult.getReplies().get(0); + SearchReply statsReply = statsResult.getReplies().get(0); assertThat(statsReply.getResults()).hasSize(2); // Verify each category group has the expected statistics fields - for (SearchReply.SearchResult group : statsReply.getResults()) { + for (SearchReply.SearchResult group : statsReply.getResults()) { assertThat(group.getFields()).containsKeys("category", "count", "avg_price", "min_price", "max_price"); // Verify the values make sense (e.g., min_price <= avg_price <= max_price) - double minPrice = Double.parseDouble(group.getFields().get("min_price")); - double avgPrice = Double.parseDouble(group.getFields().get("avg_price")); - double maxPrice = Double.parseDouble(group.getFields().get("max_price")); + double minPrice = Double.parseDouble(group.getFields().get("min_price").asString()); + double avgPrice = Double.parseDouble(group.getFields().get("avg_price").asString()); + double maxPrice = Double.parseDouble(group.getFields().get("max_price").asString()); assertThat(minPrice).isLessThanOrEqualTo(avgPrice); assertThat(avgPrice).isLessThanOrEqualTo(maxPrice); } // 2. Apply mathematical expressions - AggregateArgs mathArgs = AggregateArgs. builder().load("title").load("price") - .load("stock").load("rating").apply("@price * @stock", "inventory_value") - .apply("ceil(@rating)", "rating_rounded").build(); + AggregateArgs mathArgs = AggregateArgs.builder().load("title").load("price").load("stock").load("rating") + .apply("@price * @stock", "inventory_value").apply("ceil(@rating)", "rating_rounded").build(); - AggregationReply mathResult = redis.ftAggregate("products-idx", "*", mathArgs); + AggregationReply mathResult = redis.ftAggregate("products-idx", "*", mathArgs); assertThat(mathResult).isNotNull(); assertThat(mathResult.getAggregationGroups()).isEqualTo(1); assertThat(mathResult.getReplies()).hasSize(1); - SearchReply mathReply = mathResult.getReplies().get(0); + SearchReply mathReply = mathResult.getReplies().get(0); assertThat(mathReply.getResults()).hasSize(4); // Verify computed fields exist and have correct values - for (SearchReply.SearchResult item : mathReply.getResults()) { + for (SearchReply.SearchResult item : mathReply.getResults()) { assertThat(item.getFields()).containsKeys("title", "price", "stock", "rating", "inventory_value", "rating_rounded"); // Verify inventory_value = price * stock - double price = Double.parseDouble(item.getFields().get("price")); - double stock = Double.parseDouble(item.getFields().get("stock")); - double inventoryValue = Double.parseDouble(item.getFields().get("inventory_value")); + double price = Double.parseDouble(item.getFields().get("price").asString()); + double stock = Double.parseDouble(item.getFields().get("stock").asString()); + double inventoryValue = Double.parseDouble(item.getFields().get("inventory_value").asString()); assertThat(inventoryValue).isEqualTo(price * stock); // Verify rating_rounded is ceiling of rating - double rating = Double.parseDouble(item.getFields().get("rating")); - double ratingRounded = Double.parseDouble(item.getFields().get("rating_rounded")); + double rating = Double.parseDouble(item.getFields().get("rating").asString()); + double ratingRounded = Double.parseDouble(item.getFields().get("rating_rounded").asString()); assertThat(ratingRounded).isEqualTo(Math.ceil(rating)); } // 3. Filter and sort results - AggregateArgs filterArgs = AggregateArgs. builder().load("title").load("price") - .load("rating").filter("@price > 1000").sortBy("rating", SortDirection.DESC).build(); + AggregateArgs filterArgs = AggregateArgs.builder().load("title").load("price").load("rating").filter("@price > 1000") + .sortBy("rating", SortDirection.DESC).build(); - AggregationReply filterResult = redis.ftAggregate("products-idx", "*", filterArgs); + AggregationReply filterResult = redis.ftAggregate("products-idx", "*", filterArgs); assertThat(filterResult).isNotNull(); assertThat(filterResult.getReplies()).hasSize(1); - SearchReply filterReply = filterResult.getReplies().get(0); + SearchReply filterReply = filterResult.getReplies().get(0); // Verify all returned items have price > 1000 - for (SearchReply.SearchResult item : filterReply.getResults()) { - double price = Double.parseDouble(item.getFields().get("price")); + for (SearchReply.SearchResult item : filterReply.getResults()) { + double price = Double.parseDouble(item.getFields().get("price").asString()); assertThat(price).isGreaterThan(1000); } // Verify results are sorted by rating in descending order if (filterReply.getResults().size() >= 2) { - List> results = filterReply.getResults(); + List> results = filterReply.getResults(); for (int i = 0; i < results.size() - 1; i++) { - double rating1 = Double.parseDouble(results.get(i).getFields().get("rating")); - double rating2 = Double.parseDouble(results.get(i + 1).getFields().get("rating")); + double rating1 = Double.parseDouble(results.get(i).getFields().get("rating").asString()); + double rating2 = Double.parseDouble(results.get(i + 1).getFields().get("rating").asString()); assertThat(rating1).isGreaterThanOrEqualTo(rating2); } } // 4. Complex pipeline with multiple operations - AggregateArgs complexArgs = AggregateArgs. builder() - .groupBy(GroupBy. of("brand").reduce(Reducer. count().as("product_count")) - .reduce(Reducer. avg("@rating").as("avg_rating")) - .reduce(Reducer. sum("@stock").as("total_stock"))) + AggregateArgs complexArgs = AggregateArgs.builder() + .groupBy(GroupBy.of("brand").reduce(Reducer.count().as("product_count")) + .reduce(Reducer.avg("@rating").as("avg_rating")).reduce(Reducer.sum("@stock").as("total_stock"))) .sortBy("avg_rating", SortDirection.DESC).limit(0, 3) // Skip 0, take 3 .build(); - AggregationReply complexResult = redis.ftAggregate("products-idx", "*", complexArgs); + AggregationReply complexResult = redis.ftAggregate("products-idx", "*", complexArgs); assertThat(complexResult).isNotNull(); assertThat(complexResult.getReplies()).hasSize(1); - SearchReply complexReply = complexResult.getReplies().get(0); + SearchReply complexReply = complexResult.getReplies().get(0); // Verify each brand group has the expected fields - for (SearchReply.SearchResult group : complexReply.getResults()) { + for (SearchReply.SearchResult group : complexReply.getResults()) { assertThat(group.getFields()).containsKeys("brand", "product_count", "avg_rating", "total_stock"); } // Verify results are sorted by avg_rating in descending order if (complexReply.getResults().size() >= 2) { - List> results = complexReply.getResults(); + List> results = complexReply.getResults(); for (int i = 0; i < results.size() - 1; i++) { - double rating1 = Double.parseDouble(results.get(i).getFields().get("avg_rating")); - double rating2 = Double.parseDouble(results.get(i + 1).getFields().get("avg_rating")); + double rating1 = Double.parseDouble(results.get(i).getFields().get("avg_rating").asString()); + double rating2 = Double.parseDouble(results.get(i + 1).getFields().get("avg_rating").asString()); assertThat(rating1).isGreaterThanOrEqualTo(rating2); } } @@ -482,28 +542,28 @@ NumericFieldArgs. builder().name("rating").sortable().build(), assertThat(complexReply.getResults().size()).isLessThanOrEqualTo(3); // 5. String operations and functions - AggregateArgs stringArgs = AggregateArgs. builder().load("title").load("brand") - .apply("upper(@brand)", "brand_upper").apply("substr(@title, 0, 10)", "title_short").build(); + AggregateArgs stringArgs = AggregateArgs.builder().load("title").load("brand").apply("upper(@brand)", "brand_upper") + .apply("substr(@title, 0, 10)", "title_short").build(); - AggregationReply stringResult = redis.ftAggregate("products-idx", "*", stringArgs); + AggregationReply stringResult = redis.ftAggregate("products-idx", "*", stringArgs); assertThat(stringResult).isNotNull(); assertThat(stringResult.getReplies()).hasSize(1); - SearchReply stringReply = stringResult.getReplies().get(0); + SearchReply stringReply = stringResult.getReplies().get(0); // Verify string operations are applied correctly - for (SearchReply.SearchResult item : stringReply.getResults()) { + for (SearchReply.SearchResult item : stringReply.getResults()) { assertThat(item.getFields()).containsKeys("title", "brand", "brand_upper", "title_short"); // Verify brand_upper is uppercase of brand - String brand = item.getFields().get("brand"); - String brandUpper = item.getFields().get("brand_upper"); + String brand = item.getFields().get("brand").asString(); + String brandUpper = item.getFields().get("brand_upper").asString(); assertThat(brandUpper).isEqualTo(brand.toUpperCase()); // Verify title_short is substring of title (first 10 chars or less) - String title = item.getFields().get("title"); - String titleShort = item.getFields().get("title_short"); + String title = item.getFields().get("title").asString(); + String titleShort = item.getFields().get("title_short").asString(); assertThat(titleShort).isEqualTo(title.substring(0, Math.min(10, title.length()))); } @@ -513,14 +573,12 @@ NumericFieldArgs. builder().name("rating").sortable().build(), @Test void shouldHandleNestedGroupByOperations() { // Create an index for hierarchical grouping scenarios - List> fields = Arrays.asList(TextFieldArgs. builder().name("department").sortable().build(), - TextFieldArgs. builder().name("category").sortable().build(), - TextFieldArgs. builder().name("product").build(), - NumericFieldArgs. builder().name("sales").sortable().build(), - NumericFieldArgs. builder().name("profit").sortable().build()); + List fields = Arrays.asList(TextFieldArgs.builder().name("department").sortable().build(), + TextFieldArgs.builder().name("category").sortable().build(), TextFieldArgs.builder().name("product").build(), + NumericFieldArgs.builder().name("sales").sortable().build(), + NumericFieldArgs.builder().name("profit").sortable().build()); - CreateArgs createArgs = CreateArgs. builder().withPrefix("sales:") - .on(CreateArgs.TargetType.HASH).build(); + CreateArgs createArgs = CreateArgs.builder().withPrefix("sales:").on(CreateArgs.TargetType.HASH).build(); assertThat(redis.ftCreate("sales-idx", createArgs, fields)).isEqualTo("OK"); @@ -555,32 +613,30 @@ NumericFieldArgs. builder().name("sales").sortable().build(), redis.hmset("sales:4", salesData); // Test nested grouping by department and category - AggregateArgs nestedArgs = AggregateArgs. builder() - .groupBy(GroupBy. of("department", "category") - .reduce(Reducer. count().as("product_count")) - .reduce(Reducer. sum("@sales").as("total_sales")) - .reduce(Reducer. sum("@profit").as("total_profit"))) + AggregateArgs nestedArgs = AggregateArgs.builder() + .groupBy(GroupBy.of("department", "category").reduce(Reducer.count().as("product_count")) + .reduce(Reducer.sum("@sales").as("total_sales")).reduce(Reducer.sum("@profit").as("total_profit"))) .sortBy("total_sales", SortDirection.DESC).build(); - AggregationReply nestedResult = redis.ftAggregate("sales-idx", "*", nestedArgs); + AggregationReply nestedResult = redis.ftAggregate("sales-idx", "*", nestedArgs); assertThat(nestedResult).isNotNull(); assertThat(nestedResult.getReplies()).hasSize(1); - SearchReply nestedReply = nestedResult.getReplies().get(0); + SearchReply nestedReply = nestedResult.getReplies().get(0); // Verify each group has the expected fields - for (SearchReply.SearchResult group : nestedReply.getResults()) { + for (SearchReply.SearchResult group : nestedReply.getResults()) { assertThat(group.getFields()).containsKeys("department", "category", "product_count", "total_sales", "total_profit"); } // Verify results are sorted by total_sales in descending order if (nestedReply.getResults().size() >= 2) { - List> results = nestedReply.getResults(); + List> results = nestedReply.getResults(); for (int i = 0; i < results.size() - 1; i++) { - double sales1 = Double.parseDouble(results.get(i).getFields().get("total_sales")); - double sales2 = Double.parseDouble(results.get(i + 1).getFields().get("total_sales")); + double sales1 = Double.parseDouble(results.get(i).getFields().get("total_sales").asString()); + double sales2 = Double.parseDouble(results.get(i + 1).getFields().get("total_sales").asString()); assertThat(sales1).isGreaterThanOrEqualTo(sales2); } } @@ -591,13 +647,12 @@ NumericFieldArgs. builder().name("sales").sortable().build(), @Test void shouldHandleAdvancedFilteringAndConditionals() { // Create an index for advanced filtering scenarios - List> fields = Arrays.asList(TextFieldArgs. builder().name("status").sortable().build(), - TextFieldArgs. builder().name("priority").sortable().build(), - NumericFieldArgs. builder().name("score").sortable().build(), - NumericFieldArgs. builder().name("age").sortable().build()); + List fields = Arrays.asList(TextFieldArgs.builder().name("status").sortable().build(), + TextFieldArgs.builder().name("priority").sortable().build(), + NumericFieldArgs.builder().name("score").sortable().build(), + NumericFieldArgs.builder().name("age").sortable().build()); - CreateArgs createArgs = CreateArgs. builder().withPrefix("task:") - .on(CreateArgs.TargetType.HASH).build(); + CreateArgs createArgs = CreateArgs.builder().withPrefix("task:").on(CreateArgs.TargetType.HASH).build(); assertThat(redis.ftCreate("tasks-idx", createArgs, fields)).isEqualTo("OK"); @@ -628,21 +683,20 @@ NumericFieldArgs. builder().name("score").sortable().build(), redis.hmset("task:4", taskData); // Test complex filtering with multiple conditions - AggregateArgs filterArgs = AggregateArgs. builder().loadAll() - .filter("@score > 80 && @age < 12").apply("@score * 0.1", "normalized_score") - .sortBy("score", SortDirection.DESC).build(); + AggregateArgs filterArgs = AggregateArgs.builder().loadAll().filter("@score > 80 && @age < 12") + .apply("@score * 0.1", "normalized_score").sortBy("score", SortDirection.DESC).build(); - AggregationReply filterResult = redis.ftAggregate("tasks-idx", "*", filterArgs); + AggregationReply filterResult = redis.ftAggregate("tasks-idx", "*", filterArgs); assertThat(filterResult).isNotNull(); assertThat(filterResult.getReplies()).hasSize(1); - SearchReply filterReply = filterResult.getReplies().get(0); + SearchReply filterReply = filterResult.getReplies().get(0); // Verify all returned items meet the filter criteria - for (SearchReply.SearchResult item : filterReply.getResults()) { - double score = Double.parseDouble(item.getFields().get("score")); - double age = Double.parseDouble(item.getFields().get("age")); + for (SearchReply.SearchResult item : filterReply.getResults()) { + double score = Double.parseDouble(item.getFields().get("score").asString()); + double age = Double.parseDouble(item.getFields().get("age").asString()); assertThat(score).isGreaterThan(80); assertThat(age).isLessThan(12); @@ -650,7 +704,7 @@ NumericFieldArgs. builder().name("score").sortable().build(), // Verify computed fields assertThat(item.getFields()).containsKeys("normalized_score"); - double normalizedScore = Double.parseDouble(item.getFields().get("normalized_score")); + double normalizedScore = Double.parseDouble(item.getFields().get("normalized_score").asString()); assertThat(normalizedScore).isEqualTo(score * 0.1); } @@ -661,13 +715,12 @@ NumericFieldArgs. builder().name("score").sortable().build(), @Test void shouldHandleAdvancedStatisticalFunctions() { // Create an index for statistical analysis - List> fields = Arrays.asList(TextFieldArgs. builder().name("region").sortable().build(), - NumericFieldArgs. builder().name("temperature").sortable().build(), - NumericFieldArgs. builder().name("humidity").sortable().build(), - NumericFieldArgs. builder().name("pressure").sortable().build()); + List fields = Arrays.asList(TextFieldArgs.builder().name("region").sortable().build(), + NumericFieldArgs.builder().name("temperature").sortable().build(), + NumericFieldArgs.builder().name("humidity").sortable().build(), + NumericFieldArgs.builder().name("pressure").sortable().build()); - CreateArgs createArgs = CreateArgs. builder().withPrefix("weather:") - .on(CreateArgs.TargetType.HASH).build(); + CreateArgs createArgs = CreateArgs.builder().withPrefix("weather:").on(CreateArgs.TargetType.HASH).build(); assertThat(redis.ftCreate("weather-idx", createArgs, fields)).isEqualTo("OK"); @@ -682,36 +735,35 @@ NumericFieldArgs. builder().name("humidity").sortable().build(), } // Test advanced statistical functions - AggregateArgs statsArgs = AggregateArgs. builder() - .groupBy(GroupBy. of("region").reduce(Reducer. count().as("count")) - .reduce(Reducer. avg("@temperature").as("avg_temp")) - .reduce(Reducer. min("@temperature").as("min_temp")) - .reduce(Reducer. max("@temperature").as("max_temp"))) + AggregateArgs statsArgs = AggregateArgs.builder() + .groupBy(GroupBy.of("region").reduce(Reducer.count().as("count")) + .reduce(Reducer.avg("@temperature").as("avg_temp")).reduce(Reducer.min("@temperature").as("min_temp")) + .reduce(Reducer.max("@temperature").as("max_temp"))) .build(); - AggregationReply statsResult = redis.ftAggregate("weather-idx", "*", statsArgs); + AggregationReply statsResult = redis.ftAggregate("weather-idx", "*", statsArgs); assertThat(statsResult).isNotNull(); assertThat(statsResult.getReplies()).hasSize(1); - SearchReply statsReply = statsResult.getReplies().get(0); + SearchReply statsReply = statsResult.getReplies().get(0); assertThat(statsReply.getResults()).hasSize(2); // north and south regions // Verify each region has the expected statistical fields - for (SearchReply.SearchResult region : statsReply.getResults()) { + for (SearchReply.SearchResult region : statsReply.getResults()) { assertThat(region.getFields()).containsKeys("region", "count", "avg_temp", "min_temp", "max_temp"); // Verify statistical relationships - double minTemp = Double.parseDouble(region.getFields().get("min_temp")); - double avgTemp = Double.parseDouble(region.getFields().get("avg_temp")); - double maxTemp = Double.parseDouble(region.getFields().get("max_temp")); + double minTemp = Double.parseDouble(region.getFields().get("min_temp").asString()); + double avgTemp = Double.parseDouble(region.getFields().get("avg_temp").asString()); + double maxTemp = Double.parseDouble(region.getFields().get("max_temp").asString()); // Statistical invariants that should hold assertThat(minTemp).isLessThanOrEqualTo(avgTemp); assertThat(avgTemp).isLessThanOrEqualTo(maxTemp); // Count should be positive - int count = Integer.parseInt(region.getFields().get("count")); + int count = Integer.parseInt(region.getFields().get("count").asString()); assertThat(count).isGreaterThan(0); } @@ -721,7 +773,7 @@ NumericFieldArgs. builder().name("humidity").sortable().build(), @Test void shouldHandleTimeoutParameter() { // Create a simple index - List> fields = Collections.singletonList(TextFieldArgs. builder().name("title").build()); + List fields = Collections.singletonList(TextFieldArgs.builder().name("title").build()); assertThat(redis.ftCreate("timeout-test-idx", fields)).isEqualTo("OK"); @@ -731,17 +783,16 @@ void shouldHandleTimeoutParameter() { assertThat(redis.hmset("doc:1", doc)).isEqualTo("OK"); // Test with timeout parameter - AggregateArgs args = AggregateArgs. builder().load("title") - .timeout(Duration.ofSeconds(5)).build(); + AggregateArgs args = AggregateArgs.builder().load("title").timeout(Duration.ofSeconds(5)).build(); - AggregationReply result = redis.ftAggregate("timeout-test-idx", "*", args); + AggregationReply result = redis.ftAggregate("timeout-test-idx", "*", args); assertThat(result).isNotNull(); assertThat(result.getAggregationGroups()).isEqualTo(1); // Should have 1 aggregation group (no grouping) assertThat(result.getReplies()).hasSize(1); // Should have 1 SearchReply containing all documents - SearchReply searchReply = result.getReplies().get(0); + SearchReply searchReply = result.getReplies().get(0); assertThat(searchReply.getResults()).hasSize(1); - assertThat(searchReply.getResults().get(0).getFields().get("title")).isEqualTo("Test Document"); + assertThat(searchReply.getResults().get(0).getFields().get("title").asString()).isEqualTo("Test Document"); assertThat(redis.ftDropindex("timeout-test-idx")).isEqualTo("OK"); } @@ -749,10 +800,9 @@ void shouldHandleTimeoutParameter() { @Test void shouldPerformAggregationWithGroupBy() { // Create an index with numeric fields for aggregation - List> fields = Arrays.asList(TextFieldArgs. builder().name("title").build(), - TextFieldArgs. builder().name("category").build(), - NumericFieldArgs. builder().name("price").build(), - NumericFieldArgs. builder().name("rating").build()); + List fields = Arrays.asList(TextFieldArgs.builder().name("title").build(), + TextFieldArgs.builder().name("category").build(), NumericFieldArgs.builder().name("price").build(), + NumericFieldArgs.builder().name("rating").build()); assertThat(redis.ftCreate("groupby-agg-test-idx", fields)).isEqualTo("OK"); @@ -786,22 +836,22 @@ NumericFieldArgs. builder().name("price").build(), assertThat(redis.hmset("product:4", product4)).isEqualTo("OK"); // Perform aggregation with GROUPBY and COUNT reducer - AggregateArgs args = AggregateArgs. builder() - .groupBy(GroupBy. of("category").reduce(Reducer. count().as("count"))).build(); + AggregateArgs args = AggregateArgs.builder().groupBy(GroupBy.of("category").reduce(Reducer.count().as("count"))) + .build(); - AggregationReply result = redis.ftAggregate("groupby-agg-test-idx", "*", args); + AggregationReply result = redis.ftAggregate("groupby-agg-test-idx", "*", args); assertThat(result).isNotNull(); assertThat(result.getReplies()).hasSize(1); // Should have 1 SearchReply containing all groups - SearchReply searchReply = result.getReplies().get(0); + SearchReply searchReply = result.getReplies().get(0); assertThat(searchReply.getResults()).hasSize(2); // Should have 2 group results // Verify group results contain category and count fields // Based on redis-cli testing: electronics=2, computers=2 - for (SearchReply.SearchResult group : searchReply.getResults()) { + for (SearchReply.SearchResult group : searchReply.getResults()) { assertThat(group.getFields()).containsKey("category"); assertThat(group.getFields()).containsKey("count"); - assertThat(group.getFields().get("count")).isIn("1", "2"); // computers=2, electronics=2 + assertThat(group.getFields().get("count").asString()).isIn("1", "2"); // computers=2, electronics=2 } assertThat(redis.ftDropindex("groupby-agg-test-idx")).isEqualTo("OK"); @@ -810,10 +860,9 @@ NumericFieldArgs. builder().name("price").build(), @Test void shouldPerformAggregationWithGroupByAndMultipleReducers() { // Create an index with numeric fields - List> fields = Arrays.asList(TextFieldArgs. builder().name("title").build(), - TextFieldArgs. builder().name("category").build(), - NumericFieldArgs. builder().name("price").build(), - NumericFieldArgs. builder().name("stock").build()); + List fields = Arrays.asList(TextFieldArgs.builder().name("title").build(), + TextFieldArgs.builder().name("category").build(), NumericFieldArgs.builder().name("price").build(), + NumericFieldArgs.builder().name("stock").build()); assertThat(redis.ftCreate("multi-reducer-test-idx", fields)).isEqualTo("OK"); @@ -847,21 +896,18 @@ NumericFieldArgs. builder().name("price").build(), assertThat(redis.hmset("item:4", item4)).isEqualTo("OK"); // Perform aggregation with multiple reducers - AggregateArgs args = AggregateArgs. builder() - .groupBy(GroupBy. of("category").reduce(Reducer. count().as("count")) - .reduce(Reducer. avg("@price").as("avg_price")) - .reduce(Reducer. sum("@stock").as("total_stock"))) - .build(); + AggregateArgs args = AggregateArgs.builder().groupBy(GroupBy.of("category").reduce(Reducer.count().as("count")) + .reduce(Reducer.avg("@price").as("avg_price")).reduce(Reducer.sum("@stock").as("total_stock"))).build(); - AggregationReply result = redis.ftAggregate("multi-reducer-test-idx", "*", args); + AggregationReply result = redis.ftAggregate("multi-reducer-test-idx", "*", args); assertThat(result).isNotNull(); assertThat(result.getReplies()).hasSize(1); // Should have 1 SearchReply containing all groups - SearchReply searchReply = result.getReplies().get(0); + SearchReply searchReply = result.getReplies().get(0); assertThat(searchReply.getResults()).hasSize(2); // Verify each group has all reducer results - for (SearchReply.SearchResult group : searchReply.getResults()) { + for (SearchReply.SearchResult group : searchReply.getResults()) { assertThat(group.getFields()).containsKey("category"); assertThat(group.getFields()).containsKey("count"); assertThat(group.getFields()).containsKey("avg_price"); @@ -874,9 +920,9 @@ NumericFieldArgs. builder().name("price").build(), @Test void shouldPerformAggregationWithSortBy() { // Create an index with sortable fields - List> fields = Arrays.asList(TextFieldArgs. builder().name("title").build(), - NumericFieldArgs. builder().name("price").sortable().build(), - NumericFieldArgs. builder().name("rating").sortable().build()); + List fields = Arrays.asList(TextFieldArgs.builder().name("title").build(), + NumericFieldArgs.builder().name("price").sortable().build(), + NumericFieldArgs.builder().name("rating").sortable().build()); assertThat(redis.ftCreate("sortby-test-idx", fields)).isEqualTo("OK"); @@ -900,22 +946,21 @@ NumericFieldArgs. builder().name("price").sortable().build(), assertThat(redis.hmset("prod:3", prod3)).isEqualTo("OK"); // Perform aggregation with SORTBY price DESC - AggregateArgs args = AggregateArgs. builder().loadAll() - .sortBy("price", SortDirection.DESC).build(); + AggregateArgs args = AggregateArgs.builder().loadAll().sortBy("price", SortDirection.DESC).build(); - AggregationReply result = redis.ftAggregate("sortby-test-idx", "*", args); + AggregationReply result = redis.ftAggregate("sortby-test-idx", "*", args); assertThat(result).isNotNull(); assertThat(result.getAggregationGroups()).isEqualTo(1); // Should have 1 aggregation group (no grouping) assertThat(result.getReplies()).hasSize(1); // Should have 1 SearchReply containing all documents - SearchReply searchReply = result.getReplies().get(0); + SearchReply searchReply = result.getReplies().get(0); assertThat(searchReply.getResults()).hasSize(3); // Verify results are sorted by price in descending order - List> results = searchReply.getResults(); - assertThat(results.get(0).getFields().get("price")).isEqualTo("300"); // Highest price first - assertThat(results.get(1).getFields().get("price")).isEqualTo("200"); - assertThat(results.get(2).getFields().get("price")).isEqualTo("100"); // Lowest price last + List> results = searchReply.getResults(); + assertThat(results.get(0).getFields().get("price").asString()).isEqualTo("300"); // Highest price first + assertThat(results.get(1).getFields().get("price").asString()).isEqualTo("200"); + assertThat(results.get(2).getFields().get("price").asString()).isEqualTo("100"); // Lowest price last assertThat(redis.ftDropindex("sortby-test-idx")).isEqualTo("OK"); } @@ -923,9 +968,8 @@ NumericFieldArgs. builder().name("price").sortable().build(), @Test void shouldPerformAggregationWithApply() { // Create an index with numeric fields - List> fields = Arrays.asList(TextFieldArgs. builder().name("title").build(), - NumericFieldArgs. builder().name("price").build(), - NumericFieldArgs. builder().name("quantity").build()); + List fields = Arrays.asList(TextFieldArgs.builder().name("title").build(), + NumericFieldArgs.builder().name("price").build(), NumericFieldArgs.builder().name("quantity").build()); assertThat(redis.ftCreate("apply-agg-test-idx", fields)).isEqualTo("OK"); @@ -943,19 +987,19 @@ NumericFieldArgs. builder().name("price").build(), assertThat(redis.hmset("order:2", order2)).isEqualTo("OK"); // Perform aggregation with APPLY to calculate total value - AggregateArgs args = AggregateArgs. builder().load("title").load("price") - .load("quantity").apply("@price * @quantity", "total_value").build(); + AggregateArgs args = AggregateArgs.builder().load("title").load("price").load("quantity") + .apply("@price * @quantity", "total_value").build(); - AggregationReply result = redis.ftAggregate("apply-agg-test-idx", "*", args); + AggregationReply result = redis.ftAggregate("apply-agg-test-idx", "*", args); assertThat(result).isNotNull(); assertThat(result.getAggregationGroups()).isEqualTo(1); // Should have 1 aggregation group (no grouping) assertThat(result.getReplies()).hasSize(1); // Should have 1 SearchReply containing all documents - SearchReply searchReply = result.getReplies().get(0); + SearchReply searchReply = result.getReplies().get(0); assertThat(searchReply.getResults()).hasSize(2); // Verify computed field exists - for (SearchReply.SearchResult item : searchReply.getResults()) { + for (SearchReply.SearchResult item : searchReply.getResults()) { assertThat(item.getFields()).containsKey("total_value"); assertThat(item.getFields()).containsKey("title"); assertThat(item.getFields()).containsKey("price"); @@ -968,8 +1012,8 @@ NumericFieldArgs. builder().name("price").build(), @Test void shouldPerformAggregationWithLimit() { // Create an index - List> fields = Arrays.asList(TextFieldArgs. builder().name("title").build(), - NumericFieldArgs. builder().name("score").sortable().build()); + List fields = Arrays.asList(TextFieldArgs.builder().name("title").build(), + NumericFieldArgs.builder().name("score").sortable().build()); assertThat(redis.ftCreate("limit-test-idx", fields)).isEqualTo("OK"); @@ -982,27 +1026,26 @@ void shouldPerformAggregationWithLimit() { } // Perform aggregation with LIMIT - AggregateArgs args = AggregateArgs. builder().loadAll() - .sortBy("score", SortDirection.DESC).limit(2, 3) // Skip 2, take 3 + AggregateArgs args = AggregateArgs.builder().loadAll().sortBy("score", SortDirection.DESC).limit(2, 3) // Skip 2, take 3 .build(); - AggregationReply result = redis.ftAggregate("limit-test-idx", "*", args); + AggregationReply result = redis.ftAggregate("limit-test-idx", "*", args); assertThat(result).isNotNull(); assertThat(result.getAggregationGroups()).isEqualTo(1); // Should have 1 aggregation group (no grouping) assertThat(result.getReplies()).hasSize(1); // Should have 1 SearchReply containing all documents - SearchReply searchReply = result.getReplies().get(0); + SearchReply searchReply = result.getReplies().get(0); assertThat(searchReply.getResults()).hasSize(3); // Should return exactly 3 results // Verify we got the correct subset - let's check what we actually get - List> results = searchReply.getResults(); + List> results = searchReply.getResults(); // The results should be sorted in descending order and limited to 3 items // starting from offset 2 // So we should get items with scores: 80, 70, 60 (3rd, 4th, 5th highest) // But let's verify what we actually get and adjust accordingly - assertThat(results.get(0).getFields().get("score")).isIn("80", "70"); // Could be 3rd or 4th highest - assertThat(results.get(1).getFields().get("score")).isIn("70", "60"); // Could be 4th or 5th highest - assertThat(results.get(2).getFields().get("score")).isIn("60", "50"); // Could be 5th or 6th highest + assertThat(results.get(0).getFields().get("score").asString()).isIn("80", "70"); // Could be 3rd or 4th highest + assertThat(results.get(1).getFields().get("score").asString()).isIn("70", "60"); // Could be 4th or 5th highest + assertThat(results.get(2).getFields().get("score").asString()).isIn("60", "50"); // Could be 5th or 6th highest assertThat(redis.ftDropindex("limit-test-idx")).isEqualTo("OK"); } @@ -1010,9 +1053,8 @@ void shouldPerformAggregationWithLimit() { @Test void shouldPerformAggregationWithFilter() { // Create an index with numeric fields - List> fields = Arrays.asList(TextFieldArgs. builder().name("title").build(), - NumericFieldArgs. builder().name("price").build(), - NumericFieldArgs. builder().name("rating").build()); + List fields = Arrays.asList(TextFieldArgs.builder().name("title").build(), + NumericFieldArgs.builder().name("price").build(), NumericFieldArgs.builder().name("rating").build()); assertThat(redis.ftCreate("filter-test-idx", fields)).isEqualTo("OK"); @@ -1036,20 +1078,19 @@ NumericFieldArgs. builder().name("price").build(), assertThat(redis.hmset("item:3", item3)).isEqualTo("OK"); // Perform aggregation with FILTER for high-rated items - AggregateArgs args = AggregateArgs. builder().loadAll().filter("@rating >= 4.0") - .build(); + AggregateArgs args = AggregateArgs.builder().loadAll().filter("@rating >= 4.0").build(); - AggregationReply result = redis.ftAggregate("filter-test-idx", "*", args); + AggregationReply result = redis.ftAggregate("filter-test-idx", "*", args); assertThat(result).isNotNull(); assertThat(result.getAggregationGroups()).isEqualTo(1); // Should have 1 aggregation group (no grouping) assertThat(result.getReplies()).hasSize(1); // Should have 1 SearchReply containing all documents - SearchReply searchReply = result.getReplies().get(0); + SearchReply searchReply = result.getReplies().get(0); assertThat(searchReply.getResults()).hasSize(2); // Should filter to 2 items with rating >= 4.0 // Verify all returned items have rating >= 4.0 - for (SearchReply.SearchResult item : searchReply.getResults()) { - double rating = Double.parseDouble(item.getFields().get("rating")); + for (SearchReply.SearchResult item : searchReply.getResults()) { + double rating = Double.parseDouble(item.getFields().get("rating").asString()); assertThat(rating).isGreaterThanOrEqualTo(4.0); } @@ -1059,8 +1100,8 @@ NumericFieldArgs. builder().name("price").build(), @Test void shouldPerformAggregationWithBasicCursor() { // Create an index - List> fields = Arrays.asList(TextFieldArgs. builder().name("title").build(), - TextFieldArgs. builder().name("category").build()); + List fields = Arrays.asList(TextFieldArgs.builder().name("title").build(), + TextFieldArgs.builder().name("category").build()); assertThat(redis.ftCreate("cursor-basic-test-idx", fields)).isEqualTo("OK"); @@ -1081,25 +1122,24 @@ void shouldPerformAggregationWithBasicCursor() { assertThat(redis.hmset("doc:3", doc3)).isEqualTo("OK"); // Perform aggregation with cursor - AggregateArgs args = AggregateArgs. builder().loadAll() - .withCursor(AggregateArgs.WithCursor.of(2L)).build(); + AggregateArgs args = AggregateArgs.builder().loadAll().withCursor(AggregateArgs.WithCursor.of(2L)).build(); - AggregationReply result = redis.ftAggregate("cursor-basic-test-idx", "*", args); + AggregationReply result = redis.ftAggregate("cursor-basic-test-idx", "*", args); assertThat(result).isNotNull(); assertThat(result.getAggregationGroups()).isEqualTo(1); // Should have 1 aggregation group (no grouping) assertThat(result.getCursor()).isPresent(); assertThat(result.getCursor().get().getCursorId()).isNotEqualTo(0L); // Should have a valid cursor ID assertThat(result.getReplies()).hasSize(1); // Should have 1 SearchReply - SearchReply searchReply = result.getReplies().get(0); + SearchReply searchReply = result.getReplies().get(0); assertThat(searchReply.getResults()).hasSize(2); // Should return 2 results per page // Read next page from cursor - AggregationReply nextResult = redis.ftCursorread("cursor-basic-test-idx", result.getCursor().get()); + AggregationReply nextResult = redis.ftCursorread("cursor-basic-test-idx", result.getCursor().get()); assertThat(nextResult).isNotNull(); assertThat(nextResult.getReplies()).hasSize(1); // Should have 1 SearchReply - SearchReply nextSearchReply = nextResult.getReplies().get(0); + SearchReply nextSearchReply = nextResult.getReplies().get(0); assertThat(nextSearchReply.getResults()).hasSize(1); // Should return remaining 1 result assertThat(nextResult.getCursor()).isPresent(); assertThat(nextResult.getCursor().get().getCursorId()).isEqualTo(0L); // Should indicate end of results @@ -1110,8 +1150,8 @@ void shouldPerformAggregationWithBasicCursor() { @Test void shouldPerformAggregationWithCursorAndCount() { // Create an index - List> fields = Arrays.asList(TextFieldArgs. builder().name("title").build(), - NumericFieldArgs. builder().name("score").build()); + List fields = Arrays.asList(TextFieldArgs.builder().name("title").build(), + NumericFieldArgs.builder().name("score").build()); assertThat(redis.ftCreate("cursor-count-test-idx", fields)).isEqualTo("OK"); @@ -1124,37 +1164,35 @@ void shouldPerformAggregationWithCursorAndCount() { } // Perform aggregation with cursor and custom count - AggregateArgs args = AggregateArgs. builder().loadAll() - .withCursor(AggregateArgs.WithCursor.of(3L)).build(); + AggregateArgs args = AggregateArgs.builder().loadAll().withCursor(AggregateArgs.WithCursor.of(3L)).build(); - AggregationReply result = redis.ftAggregate("cursor-count-test-idx", "*", args); + AggregationReply result = redis.ftAggregate("cursor-count-test-idx", "*", args); assertThat(result).isNotNull(); assertThat(result.getAggregationGroups()).isEqualTo(1); // Should have 1 aggregation group (no grouping) assertThat(result.getCursor()).isPresent(); assertThat(result.getCursor().get().getCursorId()).isNotEqualTo(0L); assertThat(result.getReplies()).hasSize(1); // Should have 1 SearchReply - SearchReply searchReply = result.getReplies().get(0); + SearchReply searchReply = result.getReplies().get(0); assertThat(searchReply.getResults()).hasSize(3); // Should return 3 results per page // Read next page with different count - AggregationReply nextResult = redis.ftCursorread("cursor-count-test-idx", result.getCursor().get(), 5); + AggregationReply nextResult = redis.ftCursorread("cursor-count-test-idx", result.getCursor().get(), 5); assertThat(nextResult).isNotNull(); assertThat(nextResult.getReplies()).hasSize(1); // Should have 1 SearchReply - SearchReply nextSearchReply = nextResult.getReplies().get(0); + SearchReply nextSearchReply = nextResult.getReplies().get(0); assertThat(nextSearchReply.getResults()).hasSize(5); // Should return 5 results as specified assertThat(nextResult.getCursor()).isPresent(); assertThat(nextResult.getCursor().get().getCursorId()).isNotEqualTo(0L); // Should still have more // results // Read final page - AggregationReply finalResult = redis.ftCursorread("cursor-count-test-idx", - nextResult.getCursor().get()); + AggregationReply finalResult = redis.ftCursorread("cursor-count-test-idx", nextResult.getCursor().get()); assertThat(finalResult).isNotNull(); assertThat(finalResult.getReplies()).hasSize(1); // Should have 1 SearchReply - SearchReply finalSearchReply = finalResult.getReplies().get(0); + SearchReply finalSearchReply = finalResult.getReplies().get(0); assertThat(finalSearchReply.getResults()).hasSize(2); // Should return remaining 2 results assertThat(finalResult.getCursor()).isPresent(); assertThat(finalResult.getCursor().get().getCursorId()).isEqualTo(0L); // Should indicate end of results @@ -1165,7 +1203,7 @@ void shouldPerformAggregationWithCursorAndCount() { @Test void shouldPerformAggregationWithCursorAndMaxIdle() { // Create an index - List> fields = Collections.singletonList(TextFieldArgs. builder().name("title").build()); + List fields = Collections.singletonList(TextFieldArgs.builder().name("title").build()); assertThat(redis.ftCreate("cursor-maxidle-test-idx", fields)).isEqualTo("OK"); @@ -1177,21 +1215,21 @@ void shouldPerformAggregationWithCursorAndMaxIdle() { } // Perform aggregation with cursor and custom max idle timeout - AggregateArgs args = AggregateArgs. builder().loadAll() + AggregateArgs args = AggregateArgs.builder().loadAll() .withCursor(AggregateArgs.WithCursor.of(2L, Duration.ofSeconds(10))).build(); - AggregationReply result = redis.ftAggregate("cursor-maxidle-test-idx", "*", args); + AggregationReply result = redis.ftAggregate("cursor-maxidle-test-idx", "*", args); assertThat(result).isNotNull(); assertThat(result.getAggregationGroups()).isEqualTo(1); // Should have 1 aggregation group (no grouping) assertThat(result.getCursor()).isPresent(); assertThat(result.getCursor().get().getCursorId()).isNotEqualTo(0L); assertThat(result.getReplies()).hasSize(1); // Should have 1 SearchReply - SearchReply searchReply = result.getReplies().get(0); + SearchReply searchReply = result.getReplies().get(0); assertThat(searchReply.getResults()).hasSize(2); // Read from cursor should work within timeout - AggregationReply nextResult = redis.ftCursorread("cursor-maxidle-test-idx", result.getCursor().get()); + AggregationReply nextResult = redis.ftCursorread("cursor-maxidle-test-idx", result.getCursor().get()); assertThat(nextResult).isNotNull(); assertThat(nextResult.getReplies()).hasSize(1); // Should have 1 SearchReply @@ -1203,7 +1241,7 @@ void shouldPerformAggregationWithCursorAndMaxIdle() { @Test void shouldDeleteCursorExplicitly() { // Create an index - List> fields = Collections.singletonList(TextFieldArgs. builder().name("title").build()); + List fields = Collections.singletonList(TextFieldArgs.builder().name("title").build()); assertThat(redis.ftCreate("cursor-delete-test-idx", fields)).isEqualTo("OK"); @@ -1215,10 +1253,9 @@ void shouldDeleteCursorExplicitly() { } // Perform aggregation with cursor - AggregateArgs args = AggregateArgs. builder().loadAll() - .withCursor(AggregateArgs.WithCursor.of(2L)).build(); + AggregateArgs args = AggregateArgs.builder().loadAll().withCursor(AggregateArgs.WithCursor.of(2L)).build(); - AggregationReply result = redis.ftAggregate("cursor-delete-test-idx", "*", args); + AggregationReply result = redis.ftAggregate("cursor-delete-test-idx", "*", args); assertThat(result).isNotNull(); assertThat(result.getAggregationGroups()).isEqualTo(1); @@ -1237,8 +1274,8 @@ void shouldDeleteCursorExplicitly() { @Test void shouldHandleCursorPaginationCompletely() { // Create an index - List> fields = Arrays.asList(TextFieldArgs. builder().name("title").build(), - NumericFieldArgs. builder().name("id").sortable().build()); + List fields = Arrays.asList(TextFieldArgs.builder().name("title").build(), + NumericFieldArgs.builder().name("id").sortable().build()); assertThat(redis.ftCreate("cursor-pagination-test-idx", fields)).isEqualTo("OK"); // Add test documents @@ -1250,28 +1287,27 @@ void shouldHandleCursorPaginationCompletely() { } // Perform aggregation with cursor and sorting - AggregateArgs args = AggregateArgs. builder().loadAll() - .sortBy("id", AggregateArgs.SortDirection.ASC).withCursor(AggregateArgs.WithCursor.of(4L)).build(); + AggregateArgs args = AggregateArgs.builder().loadAll().sortBy("id", AggregateArgs.SortDirection.ASC) + .withCursor(AggregateArgs.WithCursor.of(4L)).build(); - AggregationReply result = redis.ftAggregate("cursor-pagination-test-idx", "*", args); + AggregationReply result = redis.ftAggregate("cursor-pagination-test-idx", "*", args); assertThat(result).isNotNull(); assertThat(result.getAggregationGroups()).isEqualTo(1); // Should have 1 aggregation group (no grouping) assertThat(result.getCursor()).isPresent(); assertThat(result.getCursor().get().getCursorId()).isNotEqualTo(0L); assertThat(result.getReplies()).hasSize(1); // Should have 1 SearchReply - SearchReply searchReply = result.getReplies().get(0); + SearchReply searchReply = result.getReplies().get(0); assertThat(searchReply.getResults()).hasSize(4); // Collect all results by paginating through cursor - List> allResults = new ArrayList<>(searchReply.getResults()); - AggregationReply current = result; + List> allResults = new ArrayList<>(searchReply.getResults()); + AggregationReply current = result; while (current.getCursor().isPresent() && current.getCursor().get().getCursorId() != 0L) { - AggregationReply nextResult = redis.ftCursorread("cursor-pagination-test-idx", - current.getCursor().get()); + AggregationReply nextResult = redis.ftCursorread("cursor-pagination-test-idx", current.getCursor().get()); assertThat(nextResult).isNotNull(); assertThat(nextResult.getReplies()).hasSize(1); // Should have 1 SearchReply - SearchReply nextSearchReply = nextResult.getReplies().get(0); + SearchReply nextSearchReply = nextResult.getReplies().get(0); allResults.addAll(nextSearchReply.getResults()); current = nextResult; @@ -1283,7 +1319,7 @@ void shouldHandleCursorPaginationCompletely() { // Verify results are sorted by id for (int i = 0; i < allResults.size(); i++) { String expectedId = String.valueOf(i + 1); - assertThat(allResults.get(i).getFields().get("id")).isEqualTo(expectedId); + assertThat(allResults.get(i).getFields().get("id").asString()).isEqualTo(expectedId); } assertThat(redis.ftDropindex("cursor-pagination-test-idx")).isEqualTo("OK"); @@ -1292,10 +1328,9 @@ void shouldHandleCursorPaginationCompletely() { @Test void shouldPerformCursorWithComplexAggregation() { // Create an index with multiple field types - List> fields = Arrays.asList(TextFieldArgs. builder().name("title").build(), - TextFieldArgs. builder().name("category").build(), - NumericFieldArgs. builder().name("price").build(), - NumericFieldArgs. builder().name("rating").build()); + List fields = Arrays.asList(TextFieldArgs.builder().name("title").build(), + TextFieldArgs.builder().name("category").build(), NumericFieldArgs.builder().name("price").build(), + NumericFieldArgs.builder().name("rating").build()); assertThat(redis.ftCreate("cursor-complex-test-idx", fields)).isEqualTo("OK"); @@ -1336,41 +1371,39 @@ NumericFieldArgs. builder().name("price").build(), assertThat(redis.hmset("product:5", product5)).isEqualTo("OK"); // Perform complex aggregation with groupby, reducers, and cursor - AggregateArgs args = AggregateArgs. builder() - .groupBy(AggregateArgs.GroupBy. of("category") - .reduce(AggregateArgs.Reducer. count().as("count")) - .reduce(AggregateArgs.Reducer. avg("@price").as("avg_price"))) + AggregateArgs args = AggregateArgs.builder() + .groupBy(AggregateArgs.GroupBy.of("category").reduce(AggregateArgs.Reducer.count().as("count")) + .reduce(AggregateArgs.Reducer.avg("@price").as("avg_price"))) .withCursor(AggregateArgs.WithCursor.of(1L)).build(); - AggregationReply result = redis.ftAggregate("cursor-complex-test-idx", "*", args); + AggregationReply result = redis.ftAggregate("cursor-complex-test-idx", "*", args); assertThat(result).isNotNull(); assertThat(result.getCursor()).isPresent(); assertThat(result.getCursor().get().getCursorId()).isNotEqualTo(0L); assertThat(result.getReplies()).hasSize(1); // Should have 1 SearchReply - SearchReply searchReply = result.getReplies().get(0); + SearchReply searchReply = result.getReplies().get(0); assertThat(searchReply.getResults()).hasSize(1); // Should return 1 group per page // Verify first group has expected fields - SearchReply.SearchResult firstGroup = searchReply.getResults().get(0); + SearchReply.SearchResult firstGroup = searchReply.getResults().get(0); assertThat(firstGroup.getFields()).containsKey("category"); assertThat(firstGroup.getFields()).containsKey("count"); assertThat(firstGroup.getFields()).containsKey("avg_price"); // Read next group from cursor - AggregationReply nextResult = redis.ftCursorread("cursor-complex-test-idx", result.getCursor().get()); + AggregationReply nextResult = redis.ftCursorread("cursor-complex-test-idx", result.getCursor().get()); assertThat(nextResult).isNotNull(); assertThat(nextResult.getReplies()).hasSize(1); // Should have 1 SearchReply - SearchReply nextSearchReply = nextResult.getReplies().get(0); + SearchReply nextSearchReply = nextResult.getReplies().get(0); assertThat(nextSearchReply.getResults()).hasSize(1); // Should return second group // RediSearch may either omit the cursor on the final page, or return a non-zero // cursor // that requires one more empty READ to return 0. Be tolerant across versions. long effective = nextResult.getCursor().map(AggregationReply.Cursor::getCursorId).orElse(0L); if (effective != 0L) { - AggregationReply finalPage = redis.ftCursorread("cursor-complex-test-idx", - nextResult.getCursor().get()); + AggregationReply finalPage = redis.ftCursorread("cursor-complex-test-idx", nextResult.getCursor().get()); assertThat(finalPage).isNotNull(); assertThat(finalPage.getReplies()).hasSize(1); assertThat(finalPage.getReplies().get(0).getResults()).isEmpty(); @@ -1378,7 +1411,7 @@ NumericFieldArgs. builder().name("price").build(), } // Verify second group has expected fields - SearchReply.SearchResult secondGroup = nextSearchReply.getResults().get(0); + SearchReply.SearchResult secondGroup = nextSearchReply.getResults().get(0); assertThat(secondGroup.getFields()).containsKey("category"); assertThat(secondGroup.getFields()).containsKey("count"); assertThat(secondGroup.getFields()).containsKey("avg_price"); @@ -1389,17 +1422,16 @@ NumericFieldArgs. builder().name("price").build(), @Test void shouldHandleEmptyResultsWithCursor() { // Create an index - List> fields = Collections.singletonList(TextFieldArgs. builder().name("title").build()); + List fields = Collections.singletonList(TextFieldArgs.builder().name("title").build()); assertThat(redis.ftCreate("cursor-empty-test-idx", fields)).isEqualTo("OK"); // Don't add any documents // Perform aggregation with cursor on empty index - AggregateArgs args = AggregateArgs. builder().loadAll() - .withCursor(AggregateArgs.WithCursor.of(5L)).build(); + AggregateArgs args = AggregateArgs.builder().loadAll().withCursor(AggregateArgs.WithCursor.of(5L)).build(); - AggregationReply result = redis.ftAggregate("cursor-empty-test-idx", "*", args); + AggregationReply result = redis.ftAggregate("cursor-empty-test-idx", "*", args); assertThat(result).isNotNull(); assertThat(result.getAggregationGroups()).isEqualTo(1); // Should have 0 aggregation groups for empty @@ -1415,11 +1447,11 @@ void shouldHandleEmptyResultsWithCursor() { @Test void shouldPerformAggregationWithGroupByAndAdvancedReducers() { // Create an index with multiple field types for comprehensive grouping tests - List> fields = Arrays.asList(TextFieldArgs. builder().name("department").sortable().build(), - TextFieldArgs. builder().name("role").sortable().build(), - NumericFieldArgs. builder().name("salary").sortable().build(), - NumericFieldArgs. builder().name("experience").sortable().build(), - NumericFieldArgs. builder().name("performance_score").sortable().build()); + List fields = Arrays.asList(TextFieldArgs.builder().name("department").sortable().build(), + TextFieldArgs.builder().name("role").sortable().build(), + NumericFieldArgs.builder().name("salary").sortable().build(), + NumericFieldArgs.builder().name("experience").sortable().build(), + NumericFieldArgs.builder().name("performance_score").sortable().build()); assertThat(redis.ftCreate("groupby-advanced-test-idx", fields)).isEqualTo("OK"); @@ -1465,66 +1497,64 @@ NumericFieldArgs. builder().name("experience").sortable().build(), assertThat(redis.hmset("emp:5", emp5)).isEqualTo("OK"); // Test 1: Group by department with comprehensive statistics - AggregateArgs deptStatsArgs = AggregateArgs. builder() - .groupBy(GroupBy. of("department").reduce(Reducer. count().as("employee_count")) - .reduce(Reducer. sum("@salary").as("total_salary")) - .reduce(Reducer. avg("@salary").as("avg_salary")) - .reduce(Reducer. min("@salary").as("min_salary")) - .reduce(Reducer. max("@salary").as("max_salary")) - .reduce(Reducer. avg("@performance_score").as("avg_performance")) - .reduce(Reducer. countDistinct("@role").as("role_diversity"))) + AggregateArgs deptStatsArgs = AggregateArgs.builder() + .groupBy(GroupBy.of("department").reduce(Reducer.count().as("employee_count")) + .reduce(Reducer.sum("@salary").as("total_salary")).reduce(Reducer.avg("@salary").as("avg_salary")) + .reduce(Reducer.min("@salary").as("min_salary")).reduce(Reducer.max("@salary").as("max_salary")) + .reduce(Reducer.avg("@performance_score").as("avg_performance")) + .reduce(Reducer.countDistinct("@role").as("role_diversity"))) .sortBy("avg_salary", SortDirection.DESC).build(); - AggregationReply deptStatsResult = redis.ftAggregate("groupby-advanced-test-idx", "*", deptStatsArgs); + AggregationReply deptStatsResult = redis.ftAggregate("groupby-advanced-test-idx", "*", deptStatsArgs); assertThat(deptStatsResult).isNotNull(); assertThat(deptStatsResult.getReplies()).hasSize(1); - SearchReply deptStatsReply = deptStatsResult.getReplies().get(0); + SearchReply deptStatsReply = deptStatsResult.getReplies().get(0); assertThat(deptStatsReply.getResults()).hasSize(2); // Engineering and Marketing departments // Verify each department group has all expected statistical fields - for (SearchReply.SearchResult deptGroup : deptStatsReply.getResults()) { + for (SearchReply.SearchResult deptGroup : deptStatsReply.getResults()) { assertThat(deptGroup.getFields()).containsKeys("department", "employee_count", "total_salary", "avg_salary", "min_salary", "max_salary", "avg_performance", "role_diversity"); // Verify statistical relationships - double minSalary = Double.parseDouble(deptGroup.getFields().get("min_salary")); - double avgSalary = Double.parseDouble(deptGroup.getFields().get("avg_salary")); - double maxSalary = Double.parseDouble(deptGroup.getFields().get("max_salary")); + double minSalary = Double.parseDouble(deptGroup.getFields().get("min_salary").asString()); + double avgSalary = Double.parseDouble(deptGroup.getFields().get("avg_salary").asString()); + double maxSalary = Double.parseDouble(deptGroup.getFields().get("max_salary").asString()); assertThat(minSalary).isLessThanOrEqualTo(avgSalary); assertThat(avgSalary).isLessThanOrEqualTo(maxSalary); // Verify count is positive - int empCount = Integer.parseInt(deptGroup.getFields().get("employee_count")); + int empCount = Integer.parseInt(deptGroup.getFields().get("employee_count").asString()); assertThat(empCount).isGreaterThan(0); } // Test 2: Multi-level grouping by department and role - AggregateArgs multiGroupArgs = AggregateArgs. builder() - .groupBy(GroupBy. of("department", "role").reduce(Reducer. count().as("count")) - .reduce(Reducer. avg("@salary").as("avg_salary")) - .reduce(Reducer. avg("@performance_score").as("avg_performance"))) + AggregateArgs multiGroupArgs = AggregateArgs.builder() + .groupBy(GroupBy.of("department", "role").reduce(Reducer.count().as("count")) + .reduce(Reducer.avg("@salary").as("avg_salary")) + .reduce(Reducer.avg("@performance_score").as("avg_performance"))) .sortBy("avg_salary", SortDirection.DESC).build(); - AggregationReply multiGroupResult = redis.ftAggregate("groupby-advanced-test-idx", "*", multiGroupArgs); + AggregationReply multiGroupResult = redis.ftAggregate("groupby-advanced-test-idx", "*", multiGroupArgs); assertThat(multiGroupResult).isNotNull(); assertThat(multiGroupResult.getReplies()).hasSize(1); - SearchReply multiGroupReply = multiGroupResult.getReplies().get(0); + SearchReply multiGroupReply = multiGroupResult.getReplies().get(0); // Should have 4 groups: Engineering-Senior, Engineering-Junior, // Marketing-Senior, Marketing-Junior assertThat(multiGroupReply.getResults()).hasSize(4); // Verify each group has the expected fields - for (SearchReply.SearchResult group : multiGroupReply.getResults()) { + for (SearchReply.SearchResult group : multiGroupReply.getResults()) { assertThat(group.getFields()).containsKeys("department", "role", "count", "avg_salary", "avg_performance"); // Verify department and role combinations are valid (Redis may normalize to // lowercase) - String dept = group.getFields().get("department"); - String role = group.getFields().get("role"); + String dept = group.getFields().get("department").asString(); + String role = group.getFields().get("role").asString(); assertThat(dept.toLowerCase()).isIn("engineering", "marketing"); assertThat(role.toLowerCase()).isIn("senior", "junior"); } @@ -1536,11 +1566,11 @@ NumericFieldArgs. builder().name("experience").sortable().build(), void shouldPerformAggregationWithSortByAndMaxOptimization() { // Create an index with sortable numeric fields for testing sorting // functionality - List> fields = Arrays.asList(TextFieldArgs. builder().name("product_name").build(), - TextFieldArgs. builder().name("category").sortable().build(), - NumericFieldArgs. builder().name("price").sortable().build(), - NumericFieldArgs. builder().name("rating").sortable().build(), - NumericFieldArgs. builder().name("sales_count").sortable().build()); + List fields = Arrays.asList(TextFieldArgs.builder().name("product_name").build(), + TextFieldArgs.builder().name("category").sortable().build(), + NumericFieldArgs.builder().name("price").sortable().build(), + NumericFieldArgs.builder().name("rating").sortable().build(), + NumericFieldArgs.builder().name("sales_count").sortable().build()); assertThat(redis.ftCreate("sortby-max-test-idx", fields)).isEqualTo("OK"); @@ -1556,20 +1586,20 @@ NumericFieldArgs. builder().name("rating").sortable().build(), } // Test 1: Basic sorting (should return results in correct order) - AggregateArgs basicSortArgs = AggregateArgs. builder().loadAll() + AggregateArgs basicSortArgs = AggregateArgs.builder().loadAll() .sortBy(AggregateArgs.SortBy.of("price", SortDirection.ASC)).limit(0, 5) // Only get top // 5 results .build(); - AggregationReply basicSortResult = redis.ftAggregate("sortby-max-test-idx", "*", basicSortArgs); + AggregationReply basicSortResult = redis.ftAggregate("sortby-max-test-idx", "*", basicSortArgs); assertThat(basicSortResult).isNotNull(); assertThat(basicSortResult.getReplies()).hasSize(1); - SearchReply basicSortReply = basicSortResult.getReplies().get(0); + SearchReply basicSortReply = basicSortResult.getReplies().get(0); assertThat(basicSortReply.getResults()).hasSize(5); // Limited to 5 results // Verify results are sorted by price in descending order - List> sortedResults = basicSortReply.getResults(); + List> sortedResults = basicSortReply.getResults(); assertThat(sortedResults).isNotEmpty(); // Check that we have the expected number of results @@ -1577,33 +1607,33 @@ NumericFieldArgs. builder().name("rating").sortable().build(), // Verify sorting: first result should have highest price, last should have // lowest - double firstPrice = Double.parseDouble(sortedResults.get(0).getFields().get("price")); - double lastPrice = Double.parseDouble(sortedResults.get(sortedResults.size() - 1).getFields().get("price")); + double firstPrice = Double.parseDouble(sortedResults.get(0).getFields().get("price").asString()); + double lastPrice = Double.parseDouble(sortedResults.get(sortedResults.size() - 1).getFields().get("price").asString()); assertThat(firstPrice).isLessThanOrEqualTo(lastPrice); // Verify each consecutive pair is in descending order for (int i = 0; i < sortedResults.size() - 1; i++) { - double price1 = Double.parseDouble(sortedResults.get(i).getFields().get("price")); - double price2 = Double.parseDouble(sortedResults.get(i + 1).getFields().get("price")); + double price1 = Double.parseDouble(sortedResults.get(i).getFields().get("price").asString()); + double price2 = Double.parseDouble(sortedResults.get(i + 1).getFields().get("price").asString()); assertThat(price1).isLessThanOrEqualTo(price2); } // Test 2: Sorting with MAX optimization - AggregateArgs maxSortArgs = AggregateArgs. builder().loadAll() + AggregateArgs maxSortArgs = AggregateArgs.builder().loadAll() .sortBy(AggregateArgs.SortBy.of("rating", SortDirection.DESC).max(10)).build(); - AggregationReply maxSortResult = redis.ftAggregate("sortby-max-test-idx", "*", maxSortArgs); + AggregationReply maxSortResult = redis.ftAggregate("sortby-max-test-idx", "*", maxSortArgs); assertThat(maxSortResult).isNotNull(); assertThat(maxSortResult.getReplies()).hasSize(1); - SearchReply maxSortReply = maxSortResult.getReplies().get(0); + SearchReply maxSortReply = maxSortResult.getReplies().get(0); assertThat(maxSortReply.getResults()).hasSize(10); // Limited by MAX to 10 results // Verify results are sorted by rating in descending order - List> maxSortedResults = maxSortReply.getResults(); + List> maxSortedResults = maxSortReply.getResults(); for (int i = 0; i < maxSortedResults.size() - 1; i++) { - double rating1 = Double.parseDouble(maxSortedResults.get(i).getFields().get("rating")); - double rating2 = Double.parseDouble(maxSortedResults.get(i + 1).getFields().get("rating")); + double rating1 = Double.parseDouble(maxSortedResults.get(i).getFields().get("rating").asString()); + double rating2 = Double.parseDouble(maxSortedResults.get(i + 1).getFields().get("rating").asString()); assertThat(rating1).isGreaterThanOrEqualTo(rating2); } @@ -1613,11 +1643,11 @@ NumericFieldArgs. builder().name("rating").sortable().build(), @Test void shouldPerformAggregationWithGroupByAndComplexReducers() { // Create an index for testing advanced reducer functions with grouping - List> fields = Arrays.asList(TextFieldArgs. builder().name("region").sortable().build(), - TextFieldArgs. builder().name("product_type").sortable().build(), - NumericFieldArgs. builder().name("revenue").sortable().build(), - NumericFieldArgs. builder().name("units_sold").sortable().build(), - NumericFieldArgs. builder().name("profit_margin").sortable().build()); + List fields = Arrays.asList(TextFieldArgs.builder().name("region").sortable().build(), + TextFieldArgs.builder().name("product_type").sortable().build(), + NumericFieldArgs.builder().name("revenue").sortable().build(), + NumericFieldArgs.builder().name("units_sold").sortable().build(), + NumericFieldArgs.builder().name("profit_margin").sortable().build()); assertThat(redis.ftCreate("groupby-complex-test-idx", fields)).isEqualTo("OK"); @@ -1644,75 +1674,70 @@ NumericFieldArgs. builder().name("units_sold").sortable().build(), } // Test 1: Group by region with comprehensive statistical reducers - AggregateArgs regionStatsArgs = AggregateArgs. builder() - .groupBy(GroupBy. of("region").reduce(Reducer. count().as("total_records")) - .reduce(Reducer. sum("@revenue").as("total_revenue")) - .reduce(Reducer. avg("@revenue").as("avg_revenue")) - .reduce(Reducer. min("@revenue").as("min_revenue")) - .reduce(Reducer. max("@revenue").as("max_revenue")) - .reduce(Reducer. sum("@units_sold").as("total_units")) - .reduce(Reducer. avg("@profit_margin").as("avg_profit_margin")) - .reduce(Reducer. countDistinct("@product_type").as("product_diversity"))) + AggregateArgs regionStatsArgs = AggregateArgs.builder() + .groupBy(GroupBy.of("region").reduce(Reducer.count().as("total_records")) + .reduce(Reducer.sum("@revenue").as("total_revenue")).reduce(Reducer.avg("@revenue").as("avg_revenue")) + .reduce(Reducer.min("@revenue").as("min_revenue")).reduce(Reducer.max("@revenue").as("max_revenue")) + .reduce(Reducer.sum("@units_sold").as("total_units")) + .reduce(Reducer.avg("@profit_margin").as("avg_profit_margin")) + .reduce(Reducer.countDistinct("@product_type").as("product_diversity"))) .sortBy("total_revenue", SortDirection.DESC).build(); - AggregationReply regionStatsResult = redis.ftAggregate("groupby-complex-test-idx", "*", - regionStatsArgs); + AggregationReply regionStatsResult = redis.ftAggregate("groupby-complex-test-idx", "*", regionStatsArgs); assertThat(regionStatsResult).isNotNull(); assertThat(regionStatsResult.getReplies()).hasSize(1); - SearchReply regionStatsReply = regionStatsResult.getReplies().get(0); + SearchReply regionStatsReply = regionStatsResult.getReplies().get(0); assertThat(regionStatsReply.getResults()).hasSize(4); // 4 regions // Verify each region group has all expected fields and valid statistics - for (SearchReply.SearchResult regionGroup : regionStatsReply.getResults()) { + for (SearchReply.SearchResult regionGroup : regionStatsReply.getResults()) { assertThat(regionGroup.getFields()).containsKeys("region", "total_records", "total_revenue", "avg_revenue", "min_revenue", "max_revenue", "total_units", "avg_profit_margin", "product_diversity"); // Verify statistical relationships - double minRevenue = Double.parseDouble(regionGroup.getFields().get("min_revenue")); - double avgRevenue = Double.parseDouble(regionGroup.getFields().get("avg_revenue")); - double maxRevenue = Double.parseDouble(regionGroup.getFields().get("max_revenue")); + double minRevenue = Double.parseDouble(regionGroup.getFields().get("min_revenue").asString()); + double avgRevenue = Double.parseDouble(regionGroup.getFields().get("avg_revenue").asString()); + double maxRevenue = Double.parseDouble(regionGroup.getFields().get("max_revenue").asString()); assertThat(minRevenue).isLessThanOrEqualTo(avgRevenue); assertThat(avgRevenue).isLessThanOrEqualTo(maxRevenue); // Each region should have 6 records (2 product types × 3 records each) - int totalRecords = Integer.parseInt(regionGroup.getFields().get("total_records")); + int totalRecords = Integer.parseInt(regionGroup.getFields().get("total_records").asString()); assertThat(totalRecords).isEqualTo(6); // Verify region name is valid (Redis may normalize to lowercase) - String region = regionGroup.getFields().get("region"); + String region = regionGroup.getFields().get("region").asString(); assertThat(region.toLowerCase()).isIn("north", "south", "east", "west"); } // Test 2: Multi-dimensional grouping by region and product_type - AggregateArgs multiDimArgs = AggregateArgs. builder() - .groupBy(GroupBy. of("region", "product_type") - .reduce(Reducer. count().as("record_count")) - .reduce(Reducer. avg("@revenue").as("avg_revenue")) - .reduce(Reducer. avg("@units_sold").as("avg_units")) - .reduce(Reducer. avg("@profit_margin").as("avg_margin"))) + AggregateArgs multiDimArgs = AggregateArgs.builder() + .groupBy(GroupBy.of("region", "product_type").reduce(Reducer.count().as("record_count")) + .reduce(Reducer.avg("@revenue").as("avg_revenue")).reduce(Reducer.avg("@units_sold").as("avg_units")) + .reduce(Reducer.avg("@profit_margin").as("avg_margin"))) .sortBy("avg_revenue", SortDirection.DESC).build(); - AggregationReply multiDimResult = redis.ftAggregate("groupby-complex-test-idx", "*", multiDimArgs); + AggregationReply multiDimResult = redis.ftAggregate("groupby-complex-test-idx", "*", multiDimArgs); assertThat(multiDimResult).isNotNull(); assertThat(multiDimResult.getReplies()).hasSize(1); - SearchReply multiDimReply = multiDimResult.getReplies().get(0); + SearchReply multiDimReply = multiDimResult.getReplies().get(0); assertThat(multiDimReply.getResults()).hasSize(8); // 4 regions × 2 product types = 8 combinations // Verify each combination group has expected fields - for (SearchReply.SearchResult comboGroup : multiDimReply.getResults()) { + for (SearchReply.SearchResult comboGroup : multiDimReply.getResults()) { assertThat(comboGroup.getFields()).containsKeys("region", "product_type", "record_count", "avg_revenue", "avg_units", "avg_margin"); // Each combination should have exactly 3 records - int recordCount = Integer.parseInt(comboGroup.getFields().get("record_count")); + int recordCount = Integer.parseInt(comboGroup.getFields().get("record_count").asString()); assertThat(recordCount).isEqualTo(3); // Verify valid combinations (Redis may normalize to lowercase) - String region = comboGroup.getFields().get("region"); - String productType = comboGroup.getFields().get("product_type"); + String region = comboGroup.getFields().get("region").asString(); + String productType = comboGroup.getFields().get("product_type").asString(); assertThat(region.toLowerCase()).isIn("north", "south", "east", "west"); assertThat(productType.toLowerCase()).isIn("premium", "standard"); } @@ -1723,11 +1748,10 @@ NumericFieldArgs. builder().name("units_sold").sortable().build(), @Test void shouldPerformAggregationWithSortByMultipleFields() { // Create an index for testing multi-field sorting with withCount - List> fields = Arrays.asList(TextFieldArgs. builder().name("team").sortable().build(), - TextFieldArgs. builder().name("player").build(), - NumericFieldArgs. builder().name("score").sortable().build(), - NumericFieldArgs. builder().name("assists").sortable().build(), - NumericFieldArgs. builder().name("rebounds").sortable().build()); + List fields = Arrays.asList(TextFieldArgs.builder().name("team").sortable().build(), + TextFieldArgs.builder().name("player").build(), NumericFieldArgs.builder().name("score").sortable().build(), + NumericFieldArgs.builder().name("assists").sortable().build(), + NumericFieldArgs.builder().name("rebounds").sortable().build()); assertThat(redis.ftCreate("sortby-multi-test-idx", fields)).isEqualTo("OK"); @@ -1750,26 +1774,26 @@ NumericFieldArgs. builder().name("assists").sortable().build(), } // Test: Sort by multiple fields (score DESC, then assists DESC) - AggregateArgs multiSortArgs = AggregateArgs. builder().loadAll() - .sortBy(AggregateArgs.SortBy.of(new AggregateArgs.SortProperty<>("score", SortDirection.DESC), - new AggregateArgs.SortProperty<>("assists", SortDirection.DESC))) + AggregateArgs multiSortArgs = AggregateArgs.builder().loadAll() + .sortBy(AggregateArgs.SortBy.of(new AggregateArgs.SortProperty("score", SortDirection.DESC), + new AggregateArgs.SortProperty("assists", SortDirection.DESC))) .limit(0, 8) // Get top 8 players .build(); - AggregationReply multiSortResult = redis.ftAggregate("sortby-multi-test-idx", "*", multiSortArgs); + AggregationReply multiSortResult = redis.ftAggregate("sortby-multi-test-idx", "*", multiSortArgs); assertThat(multiSortResult).isNotNull(); assertThat(multiSortResult.getReplies()).hasSize(1); - SearchReply multiSortReply = multiSortResult.getReplies().get(0); + SearchReply multiSortReply = multiSortResult.getReplies().get(0); assertThat(multiSortReply.getResults()).hasSize(8); // Limited to 8 results // Verify results are sorted correctly by score DESC, then assists DESC - List> sortedPlayers = multiSortReply.getResults(); + List> sortedPlayers = multiSortReply.getResults(); for (int i = 0; i < sortedPlayers.size() - 1; i++) { - int score1 = Integer.parseInt(sortedPlayers.get(i).getFields().get("score")); - int score2 = Integer.parseInt(sortedPlayers.get(i + 1).getFields().get("score")); - int assists1 = Integer.parseInt(sortedPlayers.get(i).getFields().get("assists")); - int assists2 = Integer.parseInt(sortedPlayers.get(i + 1).getFields().get("assists")); + int score1 = Integer.parseInt(sortedPlayers.get(i).getFields().get("score").asString()); + int score2 = Integer.parseInt(sortedPlayers.get(i + 1).getFields().get("score").asString()); + int assists1 = Integer.parseInt(sortedPlayers.get(i).getFields().get("assists").asString()); + int assists2 = Integer.parseInt(sortedPlayers.get(i + 1).getFields().get("assists").asString()); // Primary sort: score DESC if (score1 != score2) { @@ -1781,9 +1805,9 @@ NumericFieldArgs. builder().name("assists").sortable().build(), } // Verify all results have the expected fields - for (SearchReply.SearchResult player : sortedPlayers) { + for (SearchReply.SearchResult player : sortedPlayers) { assertThat(player.getFields()).containsKeys("team", "player", "score", "assists", "rebounds"); - String team = player.getFields().get("team"); + String team = player.getFields().get("team").asString(); assertThat(team.toLowerCase()).isIn("lakers", "warriors", "celtics"); } @@ -1793,10 +1817,10 @@ NumericFieldArgs. builder().name("assists").sortable().build(), @Test void shouldRespectUserSpecifiedPipelineOperationOrder() { // Create an index for testing pipeline operation order - List> fields = Arrays.asList(TextFieldArgs. builder().name("title").build(), - NumericFieldArgs. builder().name("price").sortable().build(), - NumericFieldArgs. builder().name("quantity").sortable().build(), - TagFieldArgs. builder().name("category").sortable().build()); + List fields = Arrays.asList(TextFieldArgs.builder().name("title").build(), + NumericFieldArgs.builder().name("price").sortable().build(), + NumericFieldArgs.builder().name("quantity").sortable().build(), + TagFieldArgs.builder().name("category").sortable().build()); assertThat(redis.ftCreate("pipeline-order-test-idx", fields)).isEqualTo("OK"); @@ -1825,33 +1849,33 @@ NumericFieldArgs. builder().name("quantity").sortable().build(), // Test that operations are applied in user-specified order // This specific order: APPLY -> FILTER -> GROUPBY -> LIMIT -> SORTBY // should work correctly and produce meaningful results - AggregateArgs args = AggregateArgs. builder().load("title").load("price") - .load("quantity").load("category").apply("@price * @quantity", "total_value") // Calculate - // total + AggregateArgs args = AggregateArgs.builder().load("title").load("price").load("quantity").load("category") + .apply("@price * @quantity", "total_value") // Calculate + // total // value first .filter("@total_value > 550") // Filter by total value (should keep only products 1 and // 2, both electronics) - .groupBy(GroupBy. of("category").reduce(Reducer. count().as("product_count")) - .reduce(Reducer. sum("@total_value").as("category_total"))) + .groupBy(GroupBy.of("category").reduce(Reducer.count().as("product_count")) + .reduce(Reducer.sum("@total_value").as("category_total"))) .limit(0, 10) // Limit results .sortBy("category_total", SortDirection.DESC) // Sort by category total .build(); - AggregationReply result = redis.ftAggregate("pipeline-order-test-idx", "*", args); + AggregationReply result = redis.ftAggregate("pipeline-order-test-idx", "*", args); assertThat(result).isNotNull(); assertThat(result.getReplies()).hasSize(1); - SearchReply searchReply = result.getReplies().get(0); + SearchReply searchReply = result.getReplies().get(0); // Should have only electronics category since books total_value (50*10=500) < // 550 // but electronics products (100*5=500, 200*3=600) both > 550 assertThat(searchReply.getResults()).hasSize(1); - SearchReply.SearchResult electronicsGroup = searchReply.getResults().get(0); - assertThat(electronicsGroup.getFields().get("category")).isEqualTo("electronics"); - assertThat(electronicsGroup.getFields().get("product_count")).isEqualTo("1"); - assertThat(electronicsGroup.getFields().get("category_total")).isEqualTo("600"); + SearchReply.SearchResult electronicsGroup = searchReply.getResults().get(0); + assertThat(electronicsGroup.getFields().get("category").asString()).isEqualTo("electronics"); + assertThat(electronicsGroup.getFields().get("product_count").asString()).isEqualTo("1"); + assertThat(electronicsGroup.getFields().get("category_total").asString()).isEqualTo("600"); } @Test @@ -1860,12 +1884,12 @@ void shouldSupportDynamicReentrantPipeline() { // Example from Redis docs: group by property X, sort top 100 by group size, // then group by property Y and sort by some other property - List> fields = Arrays.asList(TextFieldArgs. builder().name("product_name").build(), - TagFieldArgs. builder().name("category").sortable().build(), - TagFieldArgs. builder().name("brand").sortable().build(), - NumericFieldArgs. builder().name("price").sortable().build(), - NumericFieldArgs. builder().name("rating").sortable().build(), - NumericFieldArgs. builder().name("sales_count").sortable().build()); + List fields = Arrays.asList(TextFieldArgs.builder().name("product_name").build(), + TagFieldArgs.builder().name("category").sortable().build(), + TagFieldArgs.builder().name("brand").sortable().build(), + NumericFieldArgs.builder().name("price").sortable().build(), + NumericFieldArgs.builder().name("rating").sortable().build(), + NumericFieldArgs.builder().name("sales_count").sortable().build()); assertThat(redis.ftCreate("reentrant-pipeline-idx", fields)).isEqualTo("OK"); @@ -1898,13 +1922,12 @@ NumericFieldArgs. builder().name("rating").sortable().build(), // 5. Sort by different criteria // 6. Apply another transformation // 7. Final filtering and limiting - AggregateArgs complexArgs = AggregateArgs. builder().load("category").load("brand") - .load("price").load("rating").load("sales_count") + AggregateArgs complexArgs = AggregateArgs.builder().load("category").load("brand").load("price").load("rating") + .load("sales_count") // First aggregation: group by category - .groupBy(GroupBy. of("category").reduce(Reducer. count().as("product_count")) - .reduce(Reducer. avg("@price").as("avg_price")) - .reduce(Reducer. sum("@sales_count").as("total_sales")) - .reduce(Reducer. avg("@rating").as("avg_rating"))) + .groupBy(GroupBy.of("category").reduce(Reducer.count().as("product_count")) + .reduce(Reducer.avg("@price").as("avg_price")).reduce(Reducer.sum("@sales_count").as("total_sales")) + .reduce(Reducer.avg("@rating").as("avg_rating"))) // Apply transformation to create performance score .apply("@avg_rating * @total_sales / 100", "performance_score") // Filter categories with good performance @@ -1916,23 +1939,23 @@ NumericFieldArgs. builder().name("rating").sortable().build(), // Apply another transformation for price tier calculation .apply("@avg_price / 100", "price_tier").build(); - AggregationReply result = redis.ftAggregate("reentrant-pipeline-idx", "*", complexArgs); + AggregationReply result = redis.ftAggregate("reentrant-pipeline-idx", "*", complexArgs); assertThat(result).isNotNull(); assertThat(result.getReplies()).hasSize(1); - SearchReply searchReply = result.getReplies().get(0); + SearchReply searchReply = result.getReplies().get(0); // Should have results (electronics should be top performer) assertThat(searchReply.getResults()).isNotEmpty(); // Verify the pipeline operations were applied in correct order - SearchReply.SearchResult topCategory = searchReply.getResults().get(0); + SearchReply.SearchResult topCategory = searchReply.getResults().get(0); assertThat(topCategory.getFields()).containsKey("category"); assertThat(topCategory.getFields()).containsKey("performance_score"); assertThat(topCategory.getFields()).containsKey("price_tier"); // Electronics should be the top performer - assertThat(topCategory.getFields().get("category")).isEqualTo("electronics"); + assertThat(topCategory.getFields().get("category").asString()).isEqualTo("electronics"); } @Test @@ -1941,12 +1964,12 @@ void shouldSupportMultipleRepeatedOperations() { // This demonstrates the re-entrant nature where each operation can appear // multiple times - List> fields = Arrays.asList(TextFieldArgs. builder().name("employee_name").build(), - TagFieldArgs. builder().name("department").sortable().build(), - TagFieldArgs. builder().name("level").sortable().build(), - NumericFieldArgs. builder().name("salary").sortable().build(), - NumericFieldArgs. builder().name("experience").sortable().build(), - NumericFieldArgs. builder().name("performance_score").sortable().build()); + List fields = Arrays.asList(TextFieldArgs.builder().name("employee_name").build(), + TagFieldArgs.builder().name("department").sortable().build(), + TagFieldArgs.builder().name("level").sortable().build(), + NumericFieldArgs.builder().name("salary").sortable().build(), + NumericFieldArgs.builder().name("experience").sortable().build(), + NumericFieldArgs.builder().name("performance_score").sortable().build()); assertThat(redis.ftCreate("repeated-ops-idx", fields)).isEqualTo("OK"); @@ -1973,8 +1996,8 @@ NumericFieldArgs. builder().name("experience").sortable().build(), // Pipeline with repeated operations demonstrating re-entrant nature: // Multiple APPLY operations, multiple FILTER operations, multiple GROUPBY // operations - AggregateArgs repeatedOpsArgs = AggregateArgs. builder().load("department") - .load("level").load("salary").load("experience").load("performance_score") + AggregateArgs repeatedOpsArgs = AggregateArgs.builder().load("department").load("level").load("salary") + .load("experience").load("performance_score") // First APPLY: Calculate salary per experience year .apply("@salary / @experience", "salary_per_year") // First FILTER: Filter experienced employees @@ -1982,9 +2005,9 @@ NumericFieldArgs. builder().name("experience").sortable().build(), // Second APPLY: Calculate performance bonus .apply("@performance_score * 1000", "performance_bonus") // First GROUPBY: Group by department - .groupBy(GroupBy. of("department").reduce(Reducer. count().as("employee_count")) - .reduce(Reducer. avg("@salary").as("avg_salary")) - .reduce(Reducer. avg("@performance_score").as("avg_performance"))) + .groupBy(GroupBy.of("department").reduce(Reducer.count().as("employee_count")) + .reduce(Reducer.avg("@salary").as("avg_salary")) + .reduce(Reducer.avg("@performance_score").as("avg_performance"))) // Third APPLY: Calculate department efficiency .apply("@avg_performance / (@avg_salary / 1000)", "efficiency_ratio") // Second FILTER: Filter efficient departments @@ -1994,31 +2017,30 @@ NumericFieldArgs. builder().name("experience").sortable().build(), // Fourth APPLY: Calculate performance score .apply("@efficiency_ratio * 100", "performance_score") // Second GROUPBY: Re-group by efficiency level (using rounded efficiency ratio) - .groupBy(GroupBy. of("efficiency_ratio") - .reduce(Reducer. count().as("dept_count")) - .reduce(Reducer. avg("@avg_salary").as("class_avg_salary"))) + .groupBy(GroupBy.of("efficiency_ratio").reduce(Reducer.count().as("dept_count")) + .reduce(Reducer.avg("@avg_salary").as("class_avg_salary"))) // Second SORTBY: Sort by class average salary .sortBy("class_avg_salary", SortDirection.DESC) // Third FILTER: Final filter .filter("@dept_count > 0").build(); - AggregationReply result = redis.ftAggregate("repeated-ops-idx", "*", repeatedOpsArgs); + AggregationReply result = redis.ftAggregate("repeated-ops-idx", "*", repeatedOpsArgs); assertThat(result).isNotNull(); assertThat(result.getReplies()).hasSize(1); - SearchReply searchReply = result.getReplies().get(0); + SearchReply searchReply = result.getReplies().get(0); // Should have results showing performance classes assertThat(searchReply.getResults()).isNotEmpty(); // Verify the repeated operations worked correctly - for (SearchReply.SearchResult efficiencyGroup : searchReply.getResults()) { + for (SearchReply.SearchResult efficiencyGroup : searchReply.getResults()) { assertThat(efficiencyGroup.getFields()).containsKey("efficiency_ratio"); assertThat(efficiencyGroup.getFields()).containsKey("dept_count"); assertThat(efficiencyGroup.getFields()).containsKey("class_avg_salary"); // Verify efficiency ratio is a positive number - double efficiencyRatio = Double.parseDouble(efficiencyGroup.getFields().get("efficiency_ratio")); + double efficiencyRatio = Double.parseDouble(efficiencyGroup.getFields().get("efficiency_ratio").asString()); assertThat(efficiencyRatio).isGreaterThan(0.0); } } @@ -2029,13 +2051,13 @@ void shouldSupportComplexPipelineWithInterleavedOperations() { // "group by property X, sort the top 100 results by group size, // then group by property Y and sort the results by some other property" - List> fields = Arrays.asList(TextFieldArgs. builder().name("transaction_id").build(), - TagFieldArgs. builder().name("customer_segment").sortable().build(), - TagFieldArgs. builder().name("product_category").sortable().build(), - TagFieldArgs. builder().name("region").sortable().build(), - NumericFieldArgs. builder().name("amount").sortable().build(), - NumericFieldArgs. builder().name("quantity").sortable().build(), - NumericFieldArgs. builder().name("discount").sortable().build()); + List fields = Arrays.asList(TextFieldArgs.builder().name("transaction_id").build(), + TagFieldArgs.builder().name("customer_segment").sortable().build(), + TagFieldArgs.builder().name("product_category").sortable().build(), + TagFieldArgs.builder().name("region").sortable().build(), + NumericFieldArgs.builder().name("amount").sortable().build(), + NumericFieldArgs.builder().name("quantity").sortable().build(), + NumericFieldArgs.builder().name("discount").sortable().build()); assertThat(redis.ftCreate("interleaved-ops-idx", fields)).isEqualTo("OK"); @@ -2065,15 +2087,14 @@ NumericFieldArgs. builder().name("quantity").sortable().build(), } // Complex interleaved pipeline demonstrating the Redis docs example: - AggregateArgs interleavedArgs = AggregateArgs. builder().load("customer_segment") - .load("product_category").load("region").load("amount").load("quantity").load("discount") + AggregateArgs interleavedArgs = AggregateArgs.builder().load("customer_segment").load("product_category").load("region") + .load("amount").load("quantity").load("discount") // Calculate net amount after discount .apply("@amount * (100 - @discount) / 100", "net_amount") // First grouping: Group by customer_segment (property X) - .groupBy(GroupBy. of("customer_segment") - .reduce(Reducer. count().as("segment_transactions")) - .reduce(Reducer. sum("@net_amount").as("segment_revenue")) - .reduce(Reducer. avg("@quantity").as("avg_quantity"))) + .groupBy(GroupBy.of("customer_segment").reduce(Reducer.count().as("segment_transactions")) + .reduce(Reducer.sum("@net_amount").as("segment_revenue")) + .reduce(Reducer.avg("@quantity").as("avg_quantity"))) // Apply transformation to calculate revenue per transaction .apply("@segment_revenue / @segment_transactions", "revenue_per_transaction") // Sort by group size (segment_transactions) and limit to top results @@ -2084,32 +2105,32 @@ NumericFieldArgs. builder().name("quantity").sortable().build(), // Apply value score calculation .apply("@revenue_per_transaction / 100", "value_score") // Second grouping: Group by value_score (property Y) - .groupBy(GroupBy. of("value_score").reduce(Reducer. count().as("tier_count")) - .reduce(Reducer. sum("@segment_revenue").as("tier_total_revenue")) - .reduce(Reducer. avg("@revenue_per_transaction").as("tier_avg_revenue"))) + .groupBy(GroupBy.of("value_score").reduce(Reducer.count().as("tier_count")) + .reduce(Reducer.sum("@segment_revenue").as("tier_total_revenue")) + .reduce(Reducer.avg("@revenue_per_transaction").as("tier_avg_revenue"))) // Sort by different property (tier_total_revenue) .sortBy("tier_total_revenue", SortDirection.DESC) // Final transformation and filtering .apply("@tier_total_revenue / @tier_count", "revenue_efficiency").filter("@tier_count > 0").build(); - AggregationReply result = redis.ftAggregate("interleaved-ops-idx", "*", interleavedArgs); + AggregationReply result = redis.ftAggregate("interleaved-ops-idx", "*", interleavedArgs); assertThat(result).isNotNull(); assertThat(result.getReplies()).hasSize(1); - SearchReply searchReply = result.getReplies().get(0); + SearchReply searchReply = result.getReplies().get(0); // Should have results showing value tiers assertThat(searchReply.getResults()).isNotEmpty(); // Verify the complex interleaved operations worked correctly - for (SearchReply.SearchResult valueGroup : searchReply.getResults()) { + for (SearchReply.SearchResult valueGroup : searchReply.getResults()) { assertThat(valueGroup.getFields()).containsKey("value_score"); assertThat(valueGroup.getFields()).containsKey("tier_count"); assertThat(valueGroup.getFields()).containsKey("tier_total_revenue"); assertThat(valueGroup.getFields()).containsKey("revenue_efficiency"); // Verify value score is a positive number - double valueScore = Double.parseDouble(valueGroup.getFields().get("value_score")); + double valueScore = Double.parseDouble(valueGroup.getFields().get("value_score").asString()); assertThat(valueScore).isGreaterThan(0.0); } } @@ -2120,13 +2141,13 @@ void shouldSupportPipelineWithMultipleFiltersAndSorts() { // This demonstrates that operations can be repeated and applied at various // pipeline stages - List> fields = Arrays.asList(TextFieldArgs. builder().name("product_id").build(), - TagFieldArgs. builder().name("category").sortable().build(), - TagFieldArgs. builder().name("brand").sortable().build(), - NumericFieldArgs. builder().name("price").sortable().build(), - NumericFieldArgs. builder().name("stock").sortable().build(), - NumericFieldArgs. builder().name("rating").sortable().build(), - NumericFieldArgs. builder().name("reviews_count").sortable().build()); + List fields = Arrays.asList(TextFieldArgs.builder().name("product_id").build(), + TagFieldArgs.builder().name("category").sortable().build(), + TagFieldArgs.builder().name("brand").sortable().build(), + NumericFieldArgs.builder().name("price").sortable().build(), + NumericFieldArgs.builder().name("stock").sortable().build(), + NumericFieldArgs.builder().name("rating").sortable().build(), + NumericFieldArgs.builder().name("reviews_count").sortable().build()); assertThat(redis.ftCreate("multi-filter-sort-idx", fields)).isEqualTo("OK"); @@ -2155,8 +2176,8 @@ NumericFieldArgs. builder().name("rating").sortable().build(), } // Pipeline with multiple filters and sorts at different stages: - AggregateArgs multiFilterSortArgs = AggregateArgs. builder().load("category") - .load("brand").load("price").load("stock").load("rating").load("reviews_count") + AggregateArgs multiFilterSortArgs = AggregateArgs.builder().load("category").load("brand").load("price").load("stock") + .load("rating").load("reviews_count") // First filter: Only products with decent ratings .filter("@rating >= 4.0") // Calculate popularity score @@ -2168,10 +2189,10 @@ NumericFieldArgs. builder().name("rating").sortable().build(), // Calculate inventory value .apply("@price * @stock", "inventory_value") // Group by category to analyze category performance - .groupBy(GroupBy. of("category").reduce(Reducer. count().as("product_count")) - .reduce(Reducer. sum("@inventory_value").as("total_inventory_value")) - .reduce(Reducer. avg("@popularity_score").as("avg_popularity")) - .reduce(Reducer. max("@price").as("max_price"))) + .groupBy(GroupBy.of("category").reduce(Reducer.count().as("product_count")) + .reduce(Reducer.sum("@inventory_value").as("total_inventory_value")) + .reduce(Reducer.avg("@popularity_score").as("avg_popularity")) + .reduce(Reducer.max("@price").as("max_price"))) // Third filter: Categories with significant inventory .filter("@total_inventory_value > 5000") // Calculate value density @@ -2183,37 +2204,36 @@ NumericFieldArgs. builder().name("rating").sortable().build(), // Apply final score calculation .apply("@avg_popularity / 100", "category_score") // Group by score for final analysis - .groupBy(GroupBy. of("category_score") - .reduce(Reducer. count().as("tier_category_count")) - .reduce(Reducer. sum("@total_inventory_value").as("tier_inventory_value")) - .reduce(Reducer. avg("@max_price").as("tier_avg_max_price"))) + .groupBy(GroupBy.of("category_score").reduce(Reducer.count().as("tier_category_count")) + .reduce(Reducer.sum("@total_inventory_value").as("tier_inventory_value")) + .reduce(Reducer.avg("@max_price").as("tier_avg_max_price"))) // Third sort: Final sort by tier inventory value .sortBy("tier_inventory_value", SortDirection.DESC) // Fifth filter: Final filter for meaningful tiers .filter("@tier_category_count > 0").limit(0, 5).build(); - AggregationReply result = redis.ftAggregate("multi-filter-sort-idx", "*", multiFilterSortArgs); + AggregationReply result = redis.ftAggregate("multi-filter-sort-idx", "*", multiFilterSortArgs); assertThat(result).isNotNull(); assertThat(result.getReplies()).hasSize(1); - SearchReply searchReply = result.getReplies().get(0); + SearchReply searchReply = result.getReplies().get(0); // Should have results showing category tiers assertThat(searchReply.getResults()).isNotEmpty(); // Verify the multiple filters and sorts worked correctly - for (SearchReply.SearchResult categoryGroup : searchReply.getResults()) { + for (SearchReply.SearchResult categoryGroup : searchReply.getResults()) { assertThat(categoryGroup.getFields()).containsKey("category_score"); assertThat(categoryGroup.getFields()).containsKey("tier_category_count"); assertThat(categoryGroup.getFields()).containsKey("tier_inventory_value"); assertThat(categoryGroup.getFields()).containsKey("tier_avg_max_price"); // Verify category score is a positive number - double categoryScore = Double.parseDouble(categoryGroup.getFields().get("category_score")); + double categoryScore = Double.parseDouble(categoryGroup.getFields().get("category_score").asString()); assertThat(categoryScore).isGreaterThan(0.0); // Verify that filters were applied correctly (positive values) - int categoryCount = Integer.parseInt(categoryGroup.getFields().get("tier_category_count")); + int categoryCount = Integer.parseInt(categoryGroup.getFields().get("tier_category_count").asString()); assertThat(categoryCount).isGreaterThan(0); } } @@ -2225,15 +2245,15 @@ void shouldSupportAdvancedDynamicPipelineWithConditionalLogic() { // each other // This represents a real-world business intelligence scenario - List> fields = Arrays.asList(TextFieldArgs. builder().name("order_id").build(), - TagFieldArgs. builder().name("customer_type").sortable().build(), - TagFieldArgs. builder().name("product_line").sortable().build(), - TagFieldArgs. builder().name("sales_channel").sortable().build(), - TagFieldArgs. builder().name("season").sortable().build(), - NumericFieldArgs. builder().name("order_value").sortable().build(), - NumericFieldArgs. builder().name("cost").sortable().build(), - NumericFieldArgs. builder().name("shipping_cost").sortable().build(), - NumericFieldArgs. builder().name("customer_satisfaction").sortable().build()); + List fields = Arrays.asList(TextFieldArgs.builder().name("order_id").build(), + TagFieldArgs.builder().name("customer_type").sortable().build(), + TagFieldArgs.builder().name("product_line").sortable().build(), + TagFieldArgs.builder().name("sales_channel").sortable().build(), + TagFieldArgs.builder().name("season").sortable().build(), + NumericFieldArgs.builder().name("order_value").sortable().build(), + NumericFieldArgs.builder().name("cost").sortable().build(), + NumericFieldArgs.builder().name("shipping_cost").sortable().build(), + NumericFieldArgs.builder().name("customer_satisfaction").sortable().build()); assertThat(redis.ftCreate("advanced-pipeline-idx", fields)).isEqualTo("OK"); @@ -2265,9 +2285,8 @@ NumericFieldArgs. builder().name("shipping_cost").sortable().build(), // Advanced dynamic pipeline with conditional logic and multiple re-entrant // operations: - AggregateArgs advancedArgs = AggregateArgs. builder().load("customer_type") - .load("product_line").load("sales_channel").load("season").load("order_value").load("cost") - .load("shipping_cost").load("customer_satisfaction") + AggregateArgs advancedArgs = AggregateArgs.builder().load("customer_type").load("product_line").load("sales_channel") + .load("season").load("order_value").load("cost").load("shipping_cost").load("customer_satisfaction") // Stage 1: Calculate basic business metrics .apply("@order_value - @cost - @shipping_cost", "profit").apply("@profit / @order_value * 100", "profit_margin") @@ -2279,11 +2298,10 @@ NumericFieldArgs. builder().name("shipping_cost").sortable().build(), .apply("@order_value / 1000", "customer_value_score") // Stage 4: First aggregation - group by customer type - .groupBy(GroupBy. of("customer_type") - .reduce(Reducer. count().as("segment_orders")) - .reduce(Reducer. sum("@profit").as("segment_profit")) - .reduce(Reducer. avg("@profit_margin").as("avg_margin")) - .reduce(Reducer. avg("@customer_satisfaction").as("avg_satisfaction"))) + .groupBy(GroupBy.of("customer_type").reduce(Reducer.count().as("segment_orders")) + .reduce(Reducer.sum("@profit").as("segment_profit")) + .reduce(Reducer.avg("@profit_margin").as("avg_margin")) + .reduce(Reducer.avg("@customer_satisfaction").as("avg_satisfaction"))) // Stage 5: Calculate segment performance score .apply("(@avg_satisfaction * @avg_margin * @segment_orders) / 100", "performance_score") @@ -2302,11 +2320,10 @@ NumericFieldArgs. builder().name("shipping_cost").sortable().build(), .apply("@profit_per_order / 1000", "business_impact_score") // Stage 10: Second aggregation - re-group by business impact score - .groupBy(GroupBy. of("business_impact_score") - .reduce(Reducer. count().as("impact_segment_count")) - .reduce(Reducer. sum("@segment_profit").as("total_impact_profit")) - .reduce(Reducer. avg("@performance_score").as("avg_impact_performance")) - .reduce(Reducer. max("@avg_satisfaction").as("max_satisfaction"))) + .groupBy(GroupBy.of("business_impact_score").reduce(Reducer.count().as("impact_segment_count")) + .reduce(Reducer.sum("@segment_profit").as("total_impact_profit")) + .reduce(Reducer.avg("@performance_score").as("avg_impact_performance")) + .reduce(Reducer.max("@avg_satisfaction").as("max_satisfaction"))) // Stage 11: Calculate final business metrics .apply("@total_impact_profit / @impact_segment_count", "profit_efficiency") @@ -2320,17 +2337,17 @@ NumericFieldArgs. builder().name("shipping_cost").sortable().build(), .build(); - AggregationReply result = redis.ftAggregate("advanced-pipeline-idx", "*", advancedArgs); + AggregationReply result = redis.ftAggregate("advanced-pipeline-idx", "*", advancedArgs); assertThat(result).isNotNull(); assertThat(result.getReplies()).hasSize(1); - SearchReply searchReply = result.getReplies().get(0); + SearchReply searchReply = result.getReplies().get(0); // Should have results showing business impact analysis assertThat(searchReply.getResults()).isNotEmpty(); // Verify the advanced dynamic pipeline worked correctly - for (SearchReply.SearchResult impactGroup : searchReply.getResults()) { + for (SearchReply.SearchResult impactGroup : searchReply.getResults()) { // Verify all computed fields are present assertThat(impactGroup.getFields()).containsKey("business_impact_score"); assertThat(impactGroup.getFields()).containsKey("impact_segment_count"); @@ -2340,18 +2357,18 @@ NumericFieldArgs. builder().name("shipping_cost").sortable().build(), assertThat(impactGroup.getFields()).containsKey("strategic_score"); // Verify business impact score is a positive number - double impactScore = Double.parseDouble(impactGroup.getFields().get("business_impact_score")); + double impactScore = Double.parseDouble(impactGroup.getFields().get("business_impact_score").asString()); assertThat(impactScore).isGreaterThan(0.0); // Verify strategic score is a positive number - double strategicScore = Double.parseDouble(impactGroup.getFields().get("strategic_score")); + double strategicScore = Double.parseDouble(impactGroup.getFields().get("strategic_score").asString()); assertThat(strategicScore).isGreaterThan(0.0); // Verify that all metrics are positive (filters worked correctly) - double compositeScore = Double.parseDouble(impactGroup.getFields().get("composite_score")); + double compositeScore = Double.parseDouble(impactGroup.getFields().get("composite_score").asString()); assertThat(compositeScore).isGreaterThan(0.0); - int segmentCount = Integer.parseInt(impactGroup.getFields().get("impact_segment_count")); + int segmentCount = Integer.parseInt(impactGroup.getFields().get("impact_segment_count").asString()); assertThat(segmentCount).isGreaterThan(0); } } @@ -2359,12 +2376,11 @@ NumericFieldArgs. builder().name("shipping_cost").sortable().build(), @Test void shouldPerformAggregationOnJson() { // Create an index - List> fields = Arrays.asList(TextFieldArgs. builder().name("$.country").as("country").build(), - TextFieldArgs. builder().name("$.city").as("city").build(), - TextFieldArgs. builder().name("$.office").as("office").build(), - TextFieldArgs. builder().name("$.code").as("code").build()); - CreateArgs args = CreateArgs. builder().on(CreateArgs.TargetType.JSON) - .withPrefix("doc:").build(); + List fields = Arrays.asList(TextFieldArgs.builder().name("$.country").as("country").build(), + TextFieldArgs.builder().name("$.city").as("city").build(), + TextFieldArgs.builder().name("$.office").as("office").build(), + TextFieldArgs.builder().name("$.code").as("code").build()); + CreateArgs args = CreateArgs.builder().on(CreateArgs.TargetType.JSON).withPrefix("doc:").build(); assertThat(redis.ftCreate("args-test-idx", args, fields)).isEqualTo("OK"); @@ -2386,23 +2402,21 @@ TextFieldArgs. builder().name("$.office").as("office").build(), assertThat(redis.jsonSet("doc:2", JsonPath.ROOT_PATH, doc2)).isEqualTo("OK"); // Perform aggregation with arguments - LOAD fields - AggregateArgs.GroupBy groupBy = AggregateArgs.GroupBy - . of("country", "city", "office", "code") - .reduce(AggregateArgs.Reducer. count().as("__count")); + AggregateArgs.GroupBy groupBy = AggregateArgs.GroupBy.of("country", "city", "office", "code") + .reduce(AggregateArgs.Reducer.count().as("__count")); - AggregateArgs aggargs = AggregateArgs. builder().loadAll().groupBy(groupBy) - .dialect(QueryDialects.DIALECT2).build(); + AggregateArgs aggargs = AggregateArgs.builder().loadAll().groupBy(groupBy).dialect(QueryDialects.DIALECT2).build(); - AggregationReply result = redis.ftAggregate("args-test-idx", "*", aggargs); + AggregationReply result = redis.ftAggregate("args-test-idx", "*", aggargs); assertThat(result).isNotNull(); assertThat(result.getAggregationGroups()).isEqualTo(1); // Should have 1 aggregation group (no grouping) assertThat(result.getReplies()).hasSize(1); // Should have 1 SearchReply containing all documents - SearchReply searchReply = result.getReplies().get(0); + SearchReply searchReply = result.getReplies().get(0); assertThat(searchReply.getResults()).hasSize(2); // Should have 2 documents (doc:1, doc:2) // Check that loaded fields are present in results - for (SearchReply.SearchResult aggregateResult : searchReply.getResults()) { + for (SearchReply.SearchResult aggregateResult : searchReply.getResults()) { assertThat(aggregateResult.getFields().containsKey("country")).isTrue(); assertThat(aggregateResult.getFields().containsKey("city")).isTrue(); assertThat(aggregateResult.getFields().containsKey("office")).isTrue(); @@ -2419,12 +2433,11 @@ TextFieldArgs. builder().name("$.office").as("office").build(), @Test void shouldPerformAggregationOnJsonWithNulls() { // Create an index - List> fields = Arrays.asList(TextFieldArgs. builder().name("$.country").as("country").build(), - TextFieldArgs. builder().name("$.city").as("city").build(), - TextFieldArgs. builder().name("$.office").as("office").build(), - TextFieldArgs. builder().name("$.code").as("code").build()); - CreateArgs args = CreateArgs. builder().on(CreateArgs.TargetType.JSON) - .withPrefix("doc:").build(); + List fields = Arrays.asList(TextFieldArgs.builder().name("$.country").as("country").build(), + TextFieldArgs.builder().name("$.city").as("city").build(), + TextFieldArgs.builder().name("$.office").as("office").build(), + TextFieldArgs.builder().name("$.code").as("code").build()); + CreateArgs args = CreateArgs.builder().on(CreateArgs.TargetType.JSON).withPrefix("doc:").build(); assertThat(redis.ftCreate("args-test-idx", args, fields)).isEqualTo("OK"); @@ -2439,31 +2452,29 @@ TextFieldArgs. builder().name("$.office").as("office").build(), assertThat(redis.jsonSet("doc:1", JsonPath.ROOT_PATH, doc1)).isEqualTo("OK"); // Perform aggregation with arguments - LOAD fields - AggregateArgs.GroupBy groupBy = AggregateArgs.GroupBy - . of("country", "city", "office", "code") - .reduce(AggregateArgs.Reducer. count().as("__count")); + AggregateArgs.GroupBy groupBy = AggregateArgs.GroupBy.of("country", "city", "office", "code") + .reduce(AggregateArgs.Reducer.count().as("__count")); - AggregateArgs aggArgs = AggregateArgs. builder().loadAll().groupBy(groupBy) - .dialect(QueryDialects.DIALECT2).build(); + AggregateArgs aggArgs = AggregateArgs.builder().loadAll().groupBy(groupBy).dialect(QueryDialects.DIALECT2).build(); - AggregationReply result = redis.ftAggregate("args-test-idx", "*", aggArgs); + AggregationReply result = redis.ftAggregate("args-test-idx", "*", aggArgs); assertThat(result).isNotNull(); assertThat(result.getAggregationGroups()).isEqualTo(1); // Should have 1 aggregation group (no grouping) assertThat(result.getReplies()).hasSize(1); // Should have 1 SearchReply containing all documents - SearchReply searchReply = result.getReplies().get(0); + SearchReply searchReply = result.getReplies().get(0); assertThat(searchReply.getResults()).hasSize(1); // Should have 1 documents (doc:1) // Check that loaded fields are present in results - SearchReply.SearchResult aggregateResult = searchReply.getResults().get(0); + SearchReply.SearchResult aggregateResult = searchReply.getResults().get(0); assertThat(aggregateResult.getFields().containsKey("country")).isTrue(); assertThat(aggregateResult.getFields().containsKey("city")).isTrue(); assertThat(aggregateResult.getFields().containsKey("office")).isTrue(); assertThat(aggregateResult.getFields().containsKey("code")).isTrue(); - assertThat(aggregateResult.getFields().get("country")).isEqualTo("SE"); - assertThat(aggregateResult.getFields().get("city")).isNull(); - assertThat(aggregateResult.getFields().get("office")).isEqualTo("HQ"); - assertThat(aggregateResult.getFields().get("code")).isEqualTo("S1"); + assertThat(aggregateResult.getFields().get("country").asString()).isEqualTo("SE"); + assertThat(aggregateResult.getFields().get("city").asString()).isNull(); + assertThat(aggregateResult.getFields().get("office").asString()).isEqualTo("HQ"); + assertThat(aggregateResult.getFields().get("code").asString()).isEqualTo("S1"); assertThat(redis.ftDropindex("args-test-idx")).isEqualTo("OK"); } diff --git a/src/test/java/io/lettuce/core/search/RediSearchClusterCursorIntegrationTests.java b/src/test/java/io/lettuce/core/search/RediSearchClusterCursorIntegrationTests.java index 1914040bd7..87919ea19e 100644 --- a/src/test/java/io/lettuce/core/search/RediSearchClusterCursorIntegrationTests.java +++ b/src/test/java/io/lettuce/core/search/RediSearchClusterCursorIntegrationTests.java @@ -84,13 +84,12 @@ void setUp() { sync.flushall(); // Create schema - FieldArgs title = TextFieldArgs. builder().name("title").build(); - FieldArgs author = TagFieldArgs. builder().name("author").build(); - FieldArgs year = NumericFieldArgs. builder().name("year").sortable().build(); - FieldArgs rating = NumericFieldArgs. builder().name("rating").sortable().build(); + FieldArgs title = TextFieldArgs.builder().name("title").build(); + FieldArgs author = TagFieldArgs.builder().name("author").build(); + FieldArgs year = NumericFieldArgs.builder().name("year").sortable().build(); + FieldArgs rating = NumericFieldArgs.builder().name("rating").sortable().build(); - CreateArgs createArgs = CreateArgs. builder().withPrefix(PREFIX) - .on(CreateArgs.TargetType.HASH).build(); + CreateArgs createArgs = CreateArgs.builder().withPrefix(PREFIX).on(CreateArgs.TargetType.HASH).build(); assertThat(sync.ftCreate(INDEX, createArgs, Arrays.asList(title, author, year, rating))).isEqualTo("OK"); @@ -120,26 +119,25 @@ void tearDown() { @Test void sync_cursorLifecycle_and_stickiness() { - AggregateArgs args = AggregateArgs. builder() - .groupBy(AggregateArgs.GroupBy. of("author") - .reduce(AggregateArgs.Reducer. avg("@rating").as("avg_rating"))) + AggregateArgs args = AggregateArgs.builder() + .groupBy(AggregateArgs.GroupBy.of("author").reduce(AggregateArgs.Reducer.avg("@rating").as("avg_rating"))) .withCursor(AggregateArgs.WithCursor.of(2L)).build(); - AggregationReply first = sync.ftAggregate(INDEX, "*", args); + AggregationReply first = sync.ftAggregate(INDEX, "*", args); assertThat(first.getCursor().get().getCursorId()).isGreaterThan(0); assertThat(first.getCursor().get().getNodeId()).isPresent(); assertThat(first.getReplies()).isNotEmpty(); String nodeId = first.getCursor().get().getNodeId().get(); // Stickiness: reads route to the same node and pages advance - AggregationReply page2 = sync.ftCursorread(INDEX, first.getCursor().get()); + AggregationReply page2 = sync.ftCursorread(INDEX, first.getCursor().get()); assertThat(page2).isNotNull(); assertThat(page2.getCursor().get().getNodeId()).isPresent(); assertThat(page2.getCursor().get().getNodeId().get()).isEqualTo(nodeId); assertThat(page2.getReplies()).isNotEmpty(); assertThat(page2.getReplies()).isNotEqualTo(first.getReplies()); - AggregationReply page3 = sync.ftCursorread(INDEX, page2.getCursor().get()); + AggregationReply page3 = sync.ftCursorread(INDEX, page2.getCursor().get()); assertThat(page3.getCursor().get().getNodeId()).isPresent(); assertThat(page3.getCursor().get().getNodeId().get()).isEqualTo(nodeId); assertThat(page3.getReplies()).isNotEmpty(); @@ -152,26 +150,23 @@ void sync_cursorLifecycle_and_stickiness() { @Test void async_cursorLifecycle_and_stickiness() { - AggregateArgs args = AggregateArgs. builder() - .groupBy(AggregateArgs.GroupBy. of("author") - .reduce(AggregateArgs.Reducer. avg("@rating").as("avg_rating"))) + AggregateArgs args = AggregateArgs.builder() + .groupBy(AggregateArgs.GroupBy.of("author").reduce(AggregateArgs.Reducer.avg("@rating").as("avg_rating"))) .withCursor(AggregateArgs.WithCursor.of(2L)).build(); - AggregationReply first = async.ftAggregate(INDEX, "*", args).toCompletableFuture().join(); + AggregationReply first = async.ftAggregate(INDEX, "*", args).toCompletableFuture().join(); assertThat(first.getCursor().get().getCursorId()).isGreaterThan(0); assertThat(first.getCursor().get().getNodeId()).isPresent(); assertThat(first.getReplies()).isNotEmpty(); String nodeId = first.getCursor().get().getNodeId().get(); - AggregationReply page2 = async.ftCursorread(INDEX, first.getCursor().get()).toCompletableFuture() - .join(); + AggregationReply page2 = async.ftCursorread(INDEX, first.getCursor().get()).toCompletableFuture().join(); assertThat(page2.getCursor().get().getNodeId()).isPresent(); assertThat(page2.getCursor().get().getNodeId().get()).isEqualTo(nodeId); assertThat(page2.getReplies()).isNotEmpty(); assertThat(page2.getReplies()).isNotEqualTo(first.getReplies()); - AggregationReply page3 = async.ftCursorread(INDEX, page2.getCursor().get()).toCompletableFuture() - .join(); + AggregationReply page3 = async.ftCursorread(INDEX, page2.getCursor().get()).toCompletableFuture().join(); assertThat(page3.getCursor().get().getNodeId()).isPresent(); assertThat(page3.getCursor().get().getNodeId().get()).isEqualTo(nodeId); assertThat(page3.getReplies()).isNotEmpty(); @@ -183,26 +178,25 @@ void async_cursorLifecycle_and_stickiness() { @Test void reactive_cursorLifecycle_and_stickiness() { - AggregateArgs args = AggregateArgs. builder() - .groupBy(AggregateArgs.GroupBy. of("author") - .reduce(AggregateArgs.Reducer. avg("@rating").as("avg_rating"))) + AggregateArgs args = AggregateArgs.builder() + .groupBy(AggregateArgs.GroupBy.of("author").reduce(AggregateArgs.Reducer.avg("@rating").as("avg_rating"))) .withCursor(AggregateArgs.WithCursor.of(2L)).build(); - AggregationReply first = reactive.ftAggregate(INDEX, "*", args).block(); + AggregationReply first = reactive.ftAggregate(INDEX, "*", args).block(); assertThat(first).isNotNull(); assertThat(first.getCursor().get().getCursorId()).isGreaterThan(0); assertThat(first.getCursor().get().getNodeId()).isPresent(); assertThat(first.getReplies()).isNotEmpty(); String nodeId = first.getCursor().get().getNodeId().get(); - AggregationReply page2 = reactive.ftCursorread(INDEX, first.getCursor().get()).block(); + AggregationReply page2 = reactive.ftCursorread(INDEX, first.getCursor().get()).block(); assertThat(page2).isNotNull(); assertThat(page2.getCursor().get().getNodeId()).isPresent(); assertThat(page2.getCursor().get().getNodeId().get()).isEqualTo(nodeId); assertThat(page2.getReplies()).isNotEmpty(); assertThat(page2.getReplies()).isNotEqualTo(first.getReplies()); - AggregationReply page3 = reactive.ftCursorread(INDEX, page2.getCursor().get()).block(); + AggregationReply page3 = reactive.ftCursorread(INDEX, page2.getCursor().get()).block(); assertThat(page3.getCursor().get().getNodeId()).isPresent(); assertThat(page3.getCursor().get().getNodeId().get()).isEqualTo(nodeId); assertThat(page3.getReplies()).isNotEmpty(); @@ -249,15 +243,14 @@ void async_firstIteration_rotatesAcrossUpstreamNodes() { long upstreams = connection.getPartitions().stream().filter(n -> n.is(RedisClusterNode.NodeFlag.UPSTREAM)).count(); assumeTrue(upstreams >= 2, "requires >= 2 upstream nodes"); - AggregateArgs args = AggregateArgs. builder() - .groupBy(AggregateArgs.GroupBy. of("author") - .reduce(AggregateArgs.Reducer. avg("@rating").as("avg_rating"))) + AggregateArgs args = AggregateArgs.builder() + .groupBy(AggregateArgs.GroupBy.of("author").reduce(AggregateArgs.Reducer.avg("@rating").as("avg_rating"))) .withCursor(AggregateArgs.WithCursor.of(1L)).build(); Set nodeIds = new HashSet<>(); int observedCursors = 0; for (int i = 0; i < 30 && nodeIds.size() < upstreams; i++) { - AggregationReply first = async.ftAggregate(INDEX, "*", args).toCompletableFuture().join(); + AggregationReply first = async.ftAggregate(INDEX, "*", args).toCompletableFuture().join(); assertThat(first).isNotNull(); if (first.getCursor().isPresent() && first.getCursor().get().getCursorId() > 0) { observedCursors++; diff --git a/src/test/java/io/lettuce/core/search/RediSearchClusterIntegrationTests.java b/src/test/java/io/lettuce/core/search/RediSearchClusterIntegrationTests.java index b4192225ee..88672a842f 100644 --- a/src/test/java/io/lettuce/core/search/RediSearchClusterIntegrationTests.java +++ b/src/test/java/io/lettuce/core/search/RediSearchClusterIntegrationTests.java @@ -87,12 +87,11 @@ static void teardown() { @Test void testFtSearchAcrossMultipleShards() { // Create field definitions - FieldArgs nameField = TextFieldArgs. builder().name("name").build(); - FieldArgs categoryField = TagFieldArgs. builder().name("category").build(); - FieldArgs priceField = NumericFieldArgs. builder().name("price").sortable().build(); + FieldArgs nameField = TextFieldArgs.builder().name("name").build(); + FieldArgs categoryField = TagFieldArgs.builder().name("category").build(); + FieldArgs priceField = NumericFieldArgs.builder().name("price").sortable().build(); - CreateArgs createArgs = CreateArgs. builder().withPrefix(PRODUCT_PREFIX) - .on(CreateArgs.TargetType.HASH).build(); + CreateArgs createArgs = CreateArgs.builder().withPrefix(PRODUCT_PREFIX).on(CreateArgs.TargetType.HASH).build(); // Create index on all cluster nodes assertThat(redis.ftCreate(PRODUCTS_INDEX, createArgs, Arrays.asList(nameField, categoryField, priceField))) @@ -152,25 +151,25 @@ void testFtSearchAcrossMultipleShards() { redis.hmset(productKeys[5], phone); // Test 1: Search for all electronics across cluster - SearchReply searchResults = redis.ftSearch(PRODUCTS_INDEX, "@category:{electronics}"); + SearchReply searchResults = redis.ftSearch(PRODUCTS_INDEX, "@category:{electronics}"); // Verify we get results - should find laptop, mouse, keyboard, monitor assertThat(searchResults.getCount()).isEqualTo(4); assertThat(searchResults.getResults()).hasSize(4); // Test 2: Search with price range across cluster - SearchArgs priceSearchArgs = SearchArgs. builder().build(); - SearchReply priceResults = redis.ftSearch(PRODUCTS_INDEX, "@price:[100 500]", priceSearchArgs); + SearchArgs priceSearchArgs = SearchArgs. builder().build(); + SearchReply priceResults = redis.ftSearch(PRODUCTS_INDEX, "@price:[100 500]", priceSearchArgs); // Should find keyboard, monitor, tablet (prices 149.99, 399.99, 299.99) assertThat(priceResults.getCount()).isEqualTo(3); // Test 3: Text search across cluster - SearchReply textResults = redis.ftSearch(PRODUCTS_INDEX, "@name:Gaming"); + SearchReply textResults = redis.ftSearch(PRODUCTS_INDEX, "@name:Gaming"); // Should find only the Gaming Laptop assertThat(textResults.getCount()).isEqualTo(1); - assertThat(textResults.getResults().get(0).getFields().get("name")).isEqualTo("Gaming Laptop"); + assertThat(textResults.getResults().get(0).getFields().get("name").asString()).isEqualTo("Gaming Laptop"); // Cleanup redis.ftDropindex(PRODUCTS_INDEX); @@ -183,13 +182,12 @@ void testFtSearchAcrossMultipleShards() { @Test void testFtCursorAcrossMultipleShards() { // Create field definitions for books - FieldArgs titleField = TextFieldArgs. builder().name("title").build(); - FieldArgs authorField = TagFieldArgs. builder().name("author").build(); - FieldArgs yearField = NumericFieldArgs. builder().name("year").sortable().build(); - FieldArgs ratingField = NumericFieldArgs. builder().name("rating").sortable().build(); + FieldArgs titleField = TextFieldArgs.builder().name("title").build(); + FieldArgs authorField = TagFieldArgs.builder().name("author").build(); + FieldArgs yearField = NumericFieldArgs.builder().name("year").sortable().build(); + FieldArgs ratingField = NumericFieldArgs.builder().name("rating").sortable().build(); - CreateArgs createArgs = CreateArgs. builder().withPrefix(BOOK_PREFIX) - .on(CreateArgs.TargetType.HASH).build(); + CreateArgs createArgs = CreateArgs.builder().withPrefix(BOOK_PREFIX).on(CreateArgs.TargetType.HASH).build(); // Create index on cluster String createResult = redis.ftCreate(BOOKS_INDEX, createArgs, @@ -218,14 +216,13 @@ void testFtCursorAcrossMultipleShards() { } // Test aggregation with cursor - group by author and get average rating - AggregateArgs aggregateArgs = AggregateArgs. builder() - .groupBy(AggregateArgs.GroupBy. of("author") - .reduce(AggregateArgs.Reducer. avg("@rating").as("avg_rating"))) + AggregateArgs aggregateArgs = AggregateArgs.builder() + .groupBy(AggregateArgs.GroupBy.of("author").reduce(AggregateArgs.Reducer.avg("@rating").as("avg_rating"))) .withCursor(AggregateArgs.WithCursor.of(2L)) // Small batch size to test cursor functionality .build(); // Execute aggregation with cursor - AggregationReply aggregateResults = redis.ftAggregate(BOOKS_INDEX, "*", aggregateArgs); + AggregationReply aggregateResults = redis.ftAggregate(BOOKS_INDEX, "*", aggregateArgs); // Verify we get results with cursor assertThat(aggregateResults).isNotNull(); @@ -234,8 +231,7 @@ void testFtCursorAcrossMultipleShards() { // Test cursor read functionality if cursor is available if (aggregateResults.getCursor().isPresent() && aggregateResults.getCursor().get().getCursorId() > 0) { // Read next batch using cursor - AggregationReply cursorResults = redis.ftCursorread(BOOKS_INDEX, - aggregateResults.getCursor().get()); + AggregationReply cursorResults = redis.ftCursorread(BOOKS_INDEX, aggregateResults.getCursor().get()); // Verify cursor read works assertThat(cursorResults).isNotNull(); diff --git a/src/test/java/io/lettuce/core/search/RediSearchGeospatialIntegrationTests.java b/src/test/java/io/lettuce/core/search/RediSearchGeospatialIntegrationTests.java index 1f870bb651..74af4a7845 100644 --- a/src/test/java/io/lettuce/core/search/RediSearchGeospatialIntegrationTests.java +++ b/src/test/java/io/lettuce/core/search/RediSearchGeospatialIntegrationTests.java @@ -86,12 +86,11 @@ public void prepare() { @Test void testGeoFieldBasicFunctionality() { // Create index with GEO field for location data - FieldArgs locationField = GeoFieldArgs. builder().name("location").build(); - FieldArgs nameField = TextFieldArgs. builder().name("name").build(); - FieldArgs cityField = TextFieldArgs. builder().name("city").build(); + FieldArgs locationField = GeoFieldArgs.builder().name("location").build(); + FieldArgs nameField = TextFieldArgs.builder().name("name").build(); + FieldArgs cityField = TextFieldArgs.builder().name("city").build(); - CreateArgs createArgs = CreateArgs. builder().withPrefix("store:") - .on(CreateArgs.TargetType.HASH).build(); + CreateArgs createArgs = CreateArgs.builder().withPrefix("store:").on(CreateArgs.TargetType.HASH).build(); String result = redis.ftCreate(GEO_INDEX, createArgs, Arrays.asList(locationField, nameField, cityField)); assertThat(result).isEqualTo("OK"); @@ -116,7 +115,7 @@ void testGeoFieldBasicFunctionality() { redis.hmset("store:3", store3); // Test 1: Find stores within 50 miles of Denver - SearchReply results = redis.ftSearch(GEO_INDEX, "@location:[-104.991531 39.742043 50 mi]"); + SearchReply results = redis.ftSearch(GEO_INDEX, "@location:[-104.991531 39.742043 50 mi]"); assertThat(results.getCount()).isEqualTo(2); // Denver and Boulder stores assertThat(results.getResults()).hasSize(2); @@ -132,7 +131,7 @@ void testGeoFieldBasicFunctionality() { assertThat(results.getCount()).isEqualTo(1); // Only Denver store assertThat(results.getResults()).hasSize(1); - assertThat(results.getResults().get(0).getFields().get("name")).isEqualTo("Downtown Electronics"); + assertThat(results.getResults().get(0).getFields().get("name").asString()).isEqualTo("Downtown Electronics"); // Cleanup redis.ftDropindex(GEO_INDEX); @@ -145,11 +144,10 @@ void testGeoFieldBasicFunctionality() { @Test void testGeoFieldMultipleLocations() { // Create index for products with multiple store locations - FieldArgs locationField = GeoFieldArgs. builder().name("locations").build(); - FieldArgs productField = TextFieldArgs. builder().name("product").build(); + FieldArgs locationField = GeoFieldArgs.builder().name("locations").build(); + FieldArgs productField = TextFieldArgs.builder().name("product").build(); - CreateArgs createArgs = CreateArgs. builder().withPrefix("product:") - .on(CreateArgs.TargetType.HASH).build(); + CreateArgs createArgs = CreateArgs.builder().withPrefix("product:").on(CreateArgs.TargetType.HASH).build(); redis.ftCreate(GEO_INDEX, createArgs, Arrays.asList(locationField, productField)); @@ -166,10 +164,10 @@ void testGeoFieldMultipleLocations() { redis.hmset("product:2", product2); // Test search for products available near Denver (use smaller radius to be more specific) - SearchReply results = redis.ftSearch(GEO_INDEX, "@locations:[-104.991531 39.742043 10 mi]"); + SearchReply results = redis.ftSearch(GEO_INDEX, "@locations:[-104.991531 39.742043 10 mi]"); assertThat(results.getCount()).isEqualTo(1); - assertThat(results.getResults().get(0).getFields().get("product")).isEqualTo("Laptop Pro"); + assertThat(results.getResults().get(0).getFields().get("product").asString()).isEqualTo("Laptop Pro"); // Cleanup redis.ftDropindex(GEO_INDEX); @@ -182,11 +180,10 @@ void testGeoFieldMultipleLocations() { @Test void testGeoshapePointSphericalCoordinates() { // Create index with GEOSHAPE field using spherical coordinates (default) - FieldArgs geomField = GeoshapeFieldArgs. builder().name("geom").spherical().build(); - FieldArgs nameField = TextFieldArgs. builder().name("name").build(); + FieldArgs geomField = GeoshapeFieldArgs.builder().name("geom").spherical().build(); + FieldArgs nameField = TextFieldArgs.builder().name("name").build(); - CreateArgs createArgs = CreateArgs. builder().withPrefix("location:") - .on(CreateArgs.TargetType.HASH).build(); + CreateArgs createArgs = CreateArgs.builder().withPrefix("location:").on(CreateArgs.TargetType.HASH).build(); redis.ftCreate(GEOSHAPE_INDEX, createArgs, Arrays.asList(geomField, nameField)); @@ -208,9 +205,9 @@ void testGeoshapePointSphericalCoordinates() { // Test 1: Find points within Manhattan area (rough polygon) String manhattanPolygon = "POLYGON ((-74.047 40.680, -74.047 40.820, -73.910 40.820, -73.910 40.680, -74.047 40.680))"; - SearchArgs withinArgs = SearchArgs. builder().param("area", manhattanPolygon).build(); + SearchArgs withinArgs = SearchArgs. builder().param("area", manhattanPolygon).build(); - SearchReply results = redis.ftSearch(GEOSHAPE_INDEX, "@geom:[WITHIN $area]", withinArgs); + SearchReply results = redis.ftSearch(GEOSHAPE_INDEX, "@geom:[WITHIN $area]", withinArgs); assertThat(results.getCount()).isEqualTo(3); // All locations are in Manhattan assertThat(results.getResults()).hasSize(3); @@ -229,11 +226,10 @@ void testGeoshapePolygonSpatialRelationships() { assumeTrue(RedisConditions.of(redis).hasVersionGreaterOrEqualsTo("7.4")); // Create index with GEOSHAPE field using Cartesian coordinates for easier testing - FieldArgs geomField = GeoshapeFieldArgs. builder().name("geom").flat().build(); - FieldArgs nameField = TextFieldArgs. builder().name("name").build(); + FieldArgs geomField = GeoshapeFieldArgs.builder().name("geom").flat().build(); + FieldArgs nameField = TextFieldArgs.builder().name("name").build(); - CreateArgs createArgs = CreateArgs. builder().withPrefix("shape:") - .on(CreateArgs.TargetType.HASH).build(); + CreateArgs createArgs = CreateArgs.builder().withPrefix("shape:").on(CreateArgs.TargetType.HASH).build(); redis.ftCreate(CARTESIAN_INDEX, createArgs, Arrays.asList(geomField, nameField)); @@ -266,16 +262,16 @@ void testGeoshapePolygonSpatialRelationships() { // Test 1: WITHIN - Find shapes within the large square String largeSquare = "POLYGON ((0 0, 0 4, 4 4, 4 0, 0 0))"; - SearchArgs withinArgs = SearchArgs. builder().param("container", largeSquare).build(); + SearchArgs withinArgs = SearchArgs. builder().param("container", largeSquare).build(); - SearchReply results = redis.ftSearch(CARTESIAN_INDEX, "@geom:[WITHIN $container]", withinArgs); + SearchReply results = redis.ftSearch(CARTESIAN_INDEX, "@geom:[WITHIN $container]", withinArgs); // Should find small square and center point (both entirely within large square) assertThat(results.getCount()).isGreaterThanOrEqualTo(2); // Test 2: CONTAINS - Find shapes that contain a specific point String testPoint = "POINT (1.5 1.5)"; - SearchArgs containsArgs = SearchArgs. builder().param("point", testPoint).build(); + SearchArgs containsArgs = SearchArgs. builder().param("point", testPoint).build(); results = redis.ftSearch(CARTESIAN_INDEX, "@geom:[CONTAINS $point]", containsArgs); @@ -284,7 +280,7 @@ void testGeoshapePolygonSpatialRelationships() { // Test 3: INTERSECTS - Find shapes that intersect with a test area String testArea = "POLYGON ((2 0, 2 2, 4 2, 4 0, 2 0))"; - SearchArgs intersectsArgs = SearchArgs. builder().param("area", testArea).build(); + SearchArgs intersectsArgs = SearchArgs. builder().param("area", testArea).build(); results = redis.ftSearch(CARTESIAN_INDEX, "@geom:[INTERSECTS $area]", intersectsArgs); @@ -292,7 +288,7 @@ void testGeoshapePolygonSpatialRelationships() { assertThat(results.getCount()).isGreaterThanOrEqualTo(2); // Test 4: DISJOINT - Find shapes that don't overlap with a test area - SearchArgs disjointArgs = SearchArgs. builder().param("area", testArea).build(); + SearchArgs disjointArgs = SearchArgs. builder().param("area", testArea).build(); results = redis.ftSearch(CARTESIAN_INDEX, "@geom:[DISJOINT $area]", disjointArgs); @@ -313,14 +309,13 @@ void testComplexGeospatialQueries() { assumeTrue(RedisConditions.of(redis).hasVersionGreaterOrEqualsTo("7.4")); // Create index with mixed field types including geospatial - FieldArgs locationField = GeoFieldArgs. builder().name("location").build(); - FieldArgs serviceAreaField = GeoshapeFieldArgs. builder().name("service_area").spherical().build(); - FieldArgs nameField = TextFieldArgs. builder().name("name").build(); - FieldArgs categoryField = TextFieldArgs. builder().name("category").build(); - FieldArgs ratingField = TextFieldArgs. builder().name("rating").build(); + FieldArgs locationField = GeoFieldArgs.builder().name("location").build(); + FieldArgs serviceAreaField = GeoshapeFieldArgs.builder().name("service_area").spherical().build(); + FieldArgs nameField = TextFieldArgs.builder().name("name").build(); + FieldArgs categoryField = TextFieldArgs.builder().name("category").build(); + FieldArgs ratingField = TextFieldArgs.builder().name("rating").build(); - CreateArgs createArgs = CreateArgs. builder().withPrefix("business:") - .on(CreateArgs.TargetType.HASH).build(); + CreateArgs createArgs = CreateArgs.builder().withPrefix("business:").on(CreateArgs.TargetType.HASH).build(); redis.ftCreate(GEO_INDEX, createArgs, Arrays.asList(locationField, serviceAreaField, nameField, categoryField, ratingField)); @@ -343,16 +338,15 @@ void testComplexGeospatialQueries() { redis.hmset("business:2", business2); // Test 1: Find restaurants within 30 miles of a location - SearchReply results = redis.ftSearch(GEO_INDEX, + SearchReply results = redis.ftSearch(GEO_INDEX, "(@category:restaurant) (@location:[-104.991531 39.742043 30 mi])"); assertThat(results.getCount()).isEqualTo(1); - assertThat(results.getResults().get(0).getFields().get("name")).isEqualTo("Downtown Pizza"); + assertThat(results.getResults().get(0).getFields().get("name").asString()).isEqualTo("Downtown Pizza"); // Test 2: Find businesses whose service area contains a specific point String customerLocation = "POINT (-105.0 39.8)"; - SearchArgs serviceArgs = SearchArgs. builder().param("customer", customerLocation) - .build(); + SearchArgs serviceArgs = SearchArgs. builder().param("customer", customerLocation).build(); results = redis.ftSearch(GEO_INDEX, "@service_area:[CONTAINS $customer]", serviceArgs); @@ -360,7 +354,7 @@ void testComplexGeospatialQueries() { // Test 3: Find high-rated cafes with service areas intersecting a region String searchRegion = "POLYGON ((-105.3 40.0, -105.3 40.1, -105.2 40.1, -105.2 40.0, -105.3 40.0))"; - SearchArgs complexArgs = SearchArgs. builder().param("region", searchRegion).build(); + SearchArgs complexArgs = SearchArgs. builder().param("region", searchRegion).build(); results = redis.ftSearch(GEO_INDEX, "(@category:cafe) (@service_area:[INTERSECTS $region])", complexArgs); @@ -377,11 +371,10 @@ void testComplexGeospatialQueries() { @Test void testGeospatialUnitsAndCoordinateSystems() { // Create index for testing different units - FieldArgs locationField = GeoFieldArgs. builder().name("location").build(); - FieldArgs nameField = TextFieldArgs. builder().name("name").build(); + FieldArgs locationField = GeoFieldArgs.builder().name("location").build(); + FieldArgs nameField = TextFieldArgs.builder().name("name").build(); - CreateArgs createArgs = CreateArgs. builder().withPrefix("poi:") - .on(CreateArgs.TargetType.HASH).build(); + CreateArgs createArgs = CreateArgs.builder().withPrefix("poi:").on(CreateArgs.TargetType.HASH).build(); redis.ftCreate(GEO_INDEX, createArgs, Arrays.asList(locationField, nameField)); @@ -402,7 +395,7 @@ void testGeospatialUnitsAndCoordinateSystems() { redis.hmset("poi:3", poi3); // Test 1: Search with kilometers - SearchReply results = redis.ftSearch(GEO_INDEX, "@location:[0.0 0.0 2 km]"); + SearchReply results = redis.ftSearch(GEO_INDEX, "@location:[0.0 0.0 2 km]"); assertThat(results.getCount()).isEqualTo(3); // All points within 2 km // Test 2: Search with miles @@ -424,12 +417,11 @@ void testGeospatialUnitsAndCoordinateSystems() { @Test void testGeospatialErrorHandling() { // Create index for error testing - FieldArgs locationField = GeoFieldArgs. builder().name("location").build(); - FieldArgs geomField = GeoshapeFieldArgs. builder().name("geom").build(); - FieldArgs nameField = TextFieldArgs. builder().name("name").build(); + FieldArgs locationField = GeoFieldArgs.builder().name("location").build(); + FieldArgs geomField = GeoshapeFieldArgs.builder().name("geom").build(); + FieldArgs nameField = TextFieldArgs.builder().name("name").build(); - CreateArgs createArgs = CreateArgs. builder().withPrefix("test:") - .on(CreateArgs.TargetType.HASH).build(); + CreateArgs createArgs = CreateArgs.builder().withPrefix("test:").on(CreateArgs.TargetType.HASH).build(); redis.ftCreate(GEO_INDEX, createArgs, Arrays.asList(locationField, geomField, nameField)); @@ -441,7 +433,7 @@ void testGeospatialErrorHandling() { redis.hmset("test:1", validData); // Test 1: Valid query should work - SearchReply results = redis.ftSearch(GEO_INDEX, "@location:[-104.991531 39.742043 10 mi]"); + SearchReply results = redis.ftSearch(GEO_INDEX, "@location:[-104.991531 39.742043 10 mi]"); assertThat(results.getCount()).isEqualTo(1); // Test 2: Query with no results should return empty @@ -450,7 +442,7 @@ void testGeospatialErrorHandling() { // Test 3: Valid GEOSHAPE query String validPolygon = "POLYGON ((-105 39, -105 40, -104 40, -104 39, -105 39))"; - SearchArgs validArgs = SearchArgs. builder().param("area", validPolygon).build(); + SearchArgs validArgs = SearchArgs. builder().param("area", validPolygon).build(); results = redis.ftSearch(GEO_INDEX, "@geom:[WITHIN $area]", validArgs); assertThat(results.getCount()).isEqualTo(1); diff --git a/src/test/java/io/lettuce/core/search/RediSearchIntegrationTests.java b/src/test/java/io/lettuce/core/search/RediSearchIntegrationTests.java index ba51c6f012..3d85819efa 100644 --- a/src/test/java/io/lettuce/core/search/RediSearchIntegrationTests.java +++ b/src/test/java/io/lettuce/core/search/RediSearchIntegrationTests.java @@ -44,7 +44,6 @@ import io.lettuce.core.search.arguments.TextFieldArgs; import io.lettuce.core.search.arguments.VectorFieldArgs; import io.lettuce.test.condition.EnabledOnCommand; -import io.lettuce.test.condition.RedisConditions; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Tag; @@ -63,7 +62,6 @@ import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assumptions.assumeTrue; /** * Integration tests for Redis Search functionality using FT.SEARCH command. @@ -134,14 +132,13 @@ void testBasicTextSearchWithBlogPosts() { // Create index based on Redis documentation example: // FT.CREATE idx ON HASH PREFIX 1 blog:post: SCHEMA title TEXT WEIGHT 5.0 content TEXT author TAG created_date NUMERIC // SORTABLE views NUMERIC - FieldArgs titleField = TextFieldArgs. builder().name("title").weight(5).build(); - FieldArgs contentField = TextFieldArgs. builder().name("content").build(); - FieldArgs authorField = TagFieldArgs. builder().name("author").build(); - FieldArgs createdDateField = NumericFieldArgs. builder().name("created_date").sortable().build(); - FieldArgs viewsField = NumericFieldArgs. builder().name("views").build(); + FieldArgs titleField = TextFieldArgs.builder().name("title").weight(5).build(); + FieldArgs contentField = TextFieldArgs.builder().name("content").build(); + FieldArgs authorField = TagFieldArgs.builder().name("author").build(); + FieldArgs createdDateField = NumericFieldArgs.builder().name("created_date").sortable().build(); + FieldArgs viewsField = NumericFieldArgs.builder().name("views").build(); - CreateArgs createArgs = CreateArgs. builder().withPrefix(BLOG_PREFIX) - .on(CreateArgs.TargetType.HASH).build(); + CreateArgs createArgs = CreateArgs.builder().withPrefix(BLOG_PREFIX).on(CreateArgs.TargetType.HASH).build(); String result = redis.ftCreate(BLOG_INDEX, createArgs, Arrays.asList(titleField, contentField, authorField, createdDateField, viewsField)); @@ -173,16 +170,16 @@ void testBasicTextSearchWithBlogPosts() { assertThat(redis.hmset("blog:post:3", post3)).isEqualTo("OK"); // Test 1: Basic text search - SearchReply searchReply = redis.ftSearch(BLOG_INDEX, "@title:(Redis)"); + SearchReply searchReply = redis.ftSearch(BLOG_INDEX, "@title:(Redis)"); assertThat(searchReply.getCount()).isEqualTo(2); assertThat(searchReply.getResults()).hasSize(2); - assertThat(searchReply.getResults().get(1).getFields().get("title")).isEqualTo("Redis Search Tutorial"); - assertThat(searchReply.getResults().get(0).getFields().get("title")).isEqualTo("Advanced Redis Techniques"); - assertThat(searchReply.getResults().get(1).getFields().get("author")).isEqualTo("john_doe"); - assertThat(searchReply.getResults().get(0).getFields().get("author")).isEqualTo("jane_smith"); + assertThat(searchReply.getResults().get(1).getFields().get("title").asString()).isEqualTo("Redis Search Tutorial"); + assertThat(searchReply.getResults().get(0).getFields().get("title").asString()).isEqualTo("Advanced Redis Techniques"); + assertThat(searchReply.getResults().get(1).getFields().get("author").asString()).isEqualTo("john_doe"); + assertThat(searchReply.getResults().get(0).getFields().get("author").asString()).isEqualTo("jane_smith"); // Test 2: Search with field-specific query - SearchArgs titleSearchArgs = SearchArgs. builder().build(); + SearchArgs titleSearchArgs = SearchArgs. builder().build(); searchReply = redis.ftSearch(BLOG_INDEX, "@title:Redis", titleSearchArgs); assertThat(searchReply.getCount()).isEqualTo(2); @@ -204,11 +201,10 @@ void testBasicTextSearchWithBlogPosts() { @Test void testSearchOptionsAndModifiers() { // Create a simple index for testing search options - FieldArgs titleField = TextFieldArgs. builder().name("title").sortable().build(); - FieldArgs ratingField = NumericFieldArgs. builder().name("rating").sortable().build(); + FieldArgs titleField = TextFieldArgs.builder().name("title").sortable().build(); + FieldArgs ratingField = NumericFieldArgs.builder().name("rating").sortable().build(); - CreateArgs createArgs = CreateArgs. builder().withPrefix(MOVIE_PREFIX) - .on(CreateArgs.TargetType.HASH).build(); + CreateArgs createArgs = CreateArgs.builder().withPrefix(MOVIE_PREFIX).on(CreateArgs.TargetType.HASH).build(); redis.ftCreate(MOVIES_INDEX, createArgs, Arrays.asList(titleField, ratingField)); @@ -229,51 +225,51 @@ void testSearchOptionsAndModifiers() { redis.hmset("movie:3", movie3); // Test 1: Search with WITHSCORES - SearchArgs withScoresArgs = SearchArgs. builder().withScores().build(); - SearchReply results = redis.ftSearch(MOVIES_INDEX, "Matrix", withScoresArgs); + SearchArgs withScoresArgs = SearchArgs. builder().withScores().build(); + SearchReply results = redis.ftSearch(MOVIES_INDEX, "Matrix", withScoresArgs); assertThat(results.getCount()).isEqualTo(3); assertThat(results.getResults()).hasSize(3); // Verify that scores are present - for (SearchReply.SearchResult result : results.getResults()) { + for (SearchReply.SearchResult result : results.getResults()) { assertThat(result.getScore()).isNotNull(); assertThat(result.getScore()).isGreaterThan(0.0); } // Test 2: Search with NOCONTENT - SearchArgs noContentArgs = SearchArgs. builder().noContent().build(); + SearchArgs noContentArgs = SearchArgs. builder().noContent().build(); results = redis.ftSearch(MOVIES_INDEX, "Matrix", noContentArgs); assertThat(results.getCount()).isEqualTo(3); assertThat(results.getResults()).hasSize(3); // Verify that fields are not present - for (SearchReply.SearchResult result : results.getResults()) { + for (SearchReply.SearchResult result : results.getResults()) { assertThat(result.getFields()).isEmpty(); } // Test 3: Search with LIMIT - SearchArgs limitArgs = SearchArgs. builder().limit(0, 2).build(); + SearchArgs limitArgs = SearchArgs. builder().limit(0, 2).build(); results = redis.ftSearch(MOVIES_INDEX, "Matrix", limitArgs); assertThat(results.getCount()).isEqualTo(3); // Total count should still be 3 assertThat(results.getResults()).hasSize(2); // But only 2 results returned // Test 4: Search with SORTBY - SortByArgs sortByArgs = SortByArgs. builder().attribute("rating").descending().build(); - SearchArgs sortArgs = SearchArgs. builder().sortBy(sortByArgs).build(); + SortByArgs sortByArgs = SortByArgs.builder().attribute("rating").descending().build(); + SearchArgs sortArgs = SearchArgs. builder().sortBy(sortByArgs).build(); results = redis.ftSearch(MOVIES_INDEX, "Matrix", sortArgs); assertThat(results.getCount()).isEqualTo(3); assertThat(results.getResults()).hasSize(3); // Verify sorting order (highest rating first) double previousRating = Double.MAX_VALUE; - for (SearchReply.SearchResult result : results.getResults()) { - double currentRating = Double.parseDouble(result.getFields().get("rating")); + for (SearchReply.SearchResult result : results.getResults()) { + double currentRating = Double.parseDouble(result.getFields().get("rating").asString()); assertThat(currentRating).isLessThanOrEqualTo(previousRating); previousRating = currentRating; } // Test 5: Search with RETURN fields - SearchArgs returnArgs = SearchArgs. builder().returnField("title").build(); + SearchArgs returnArgs = SearchArgs. builder().returnField("title").build(); results = redis.ftSearch(MOVIES_INDEX, "Matrix", returnArgs); assertThat(results.getCount()).isEqualTo(3); - for (SearchReply.SearchResult result : results.getResults()) { + for (SearchReply.SearchResult result : results.getResults()) { assertThat(result.getFields()).containsKey("title"); assertThat(result.getFields()).doesNotContainKey("rating"); } @@ -290,11 +286,10 @@ void testSearchOptionsAndModifiers() { void testTagFieldsWithCustomSeparator() { // Create index with TAG field using custom separator // FT.CREATE books-idx ON HASH PREFIX 1 book:details SCHEMA title TEXT categories TAG SEPARATOR ";" - FieldArgs titleField = TextFieldArgs. builder().name("title").build(); - FieldArgs categoriesField = TagFieldArgs. builder().name("categories").separator(";").build(); + FieldArgs titleField = TextFieldArgs.builder().name("title").build(); + FieldArgs categoriesField = TagFieldArgs.builder().name("categories").separator(";").build(); - CreateArgs createArgs = CreateArgs. builder().withPrefix(BOOK_PREFIX) - .on(CreateArgs.TargetType.HASH).build(); + CreateArgs createArgs = CreateArgs.builder().withPrefix(BOOK_PREFIX).on(CreateArgs.TargetType.HASH).build(); redis.ftCreate(BOOKS_INDEX, createArgs, Arrays.asList(titleField, categoriesField)); @@ -315,7 +310,7 @@ void testTagFieldsWithCustomSeparator() { redis.hmset("book:details:3", book3); // Test 1: Search for books with "databases" category - SearchReply results = redis.ftSearch(BOOKS_INDEX, "@categories:{databases}"); + SearchReply results = redis.ftSearch(BOOKS_INDEX, "@categories:{databases}"); assertThat(results.getCount()).isEqualTo(3); // Test 2: Search for books with "nosql" category @@ -325,7 +320,7 @@ void testTagFieldsWithCustomSeparator() { // Test 3: Search for books with "programming" category results = redis.ftSearch(BOOKS_INDEX, "@categories:{programming}"); assertThat(results.getCount()).isEqualTo(1); - assertThat(results.getResults().get(0).getFields().get("title")).isEqualTo("Redis in Action"); + assertThat(results.getResults().get(0).getFields().get("title").asString()).isEqualTo("Redis in Action"); // Test 4: Search for books with multiple categories (OR) results = redis.ftSearch(BOOKS_INDEX, "@categories:{programming|design}"); @@ -341,12 +336,11 @@ void testTagFieldsWithCustomSeparator() { @Test void testNumericFieldOperations() { // Create index with numeric fields for testing range queries - FieldArgs nameField = TextFieldArgs. builder().name("name").build(); - FieldArgs priceField = NumericFieldArgs. builder().name("price").sortable().build(); - FieldArgs stockField = NumericFieldArgs. builder().name("stock").build(); + FieldArgs nameField = TextFieldArgs.builder().name("name").build(); + FieldArgs priceField = NumericFieldArgs.builder().name("price").sortable().build(); + FieldArgs stockField = NumericFieldArgs.builder().name("stock").build(); - CreateArgs createArgs = CreateArgs. builder().withPrefix(PRODUCT_PREFIX) - .on(CreateArgs.TargetType.HASH).build(); + CreateArgs createArgs = CreateArgs.builder().withPrefix(PRODUCT_PREFIX).on(CreateArgs.TargetType.HASH).build(); redis.ftCreate(PRODUCTS_INDEX, createArgs, Arrays.asList(nameField, priceField, stockField)); @@ -376,7 +370,7 @@ void testNumericFieldOperations() { redis.hmset("product:4", product4); // Test 1: Range query - products between $50 and $500 - SearchReply results = redis.ftSearch(PRODUCTS_INDEX, "@price:[50 500]"); + SearchReply results = redis.ftSearch(PRODUCTS_INDEX, "@price:[50 500]"); assertThat(results.getCount()).isEqualTo(2); // Keyboard and Monitor // Test 2: Open range query - products over $100 @@ -390,7 +384,7 @@ void testNumericFieldOperations() { // Test 4: Exact numeric value results = redis.ftSearch(PRODUCTS_INDEX, "@price:[29.99 29.99]"); assertThat(results.getCount()).isEqualTo(1); - assertThat(results.getResults().get(0).getFields().get("name")).isEqualTo("Mouse"); + assertThat(results.getResults().get(0).getFields().get("name").asString()).isEqualTo("Mouse"); // Test 5: Stock range query results = redis.ftSearch(PRODUCTS_INDEX, "@stock:[20 60]"); @@ -410,12 +404,11 @@ void testNumericFieldOperations() { @Test void testAdvancedSearchFeatures() { // Create a simple index for testing advanced features - FieldArgs titleField = TextFieldArgs. builder().name("title").build(); - FieldArgs contentField = TextFieldArgs. builder().name("content").build(); - FieldArgs categoryField = TagFieldArgs. builder().name("category").build(); + FieldArgs titleField = TextFieldArgs.builder().name("title").build(); + FieldArgs contentField = TextFieldArgs.builder().name("content").build(); + FieldArgs categoryField = TagFieldArgs.builder().name("category").build(); - CreateArgs createArgs = CreateArgs. builder().withPrefix(BLOG_PREFIX) - .on(CreateArgs.TargetType.HASH).build(); + CreateArgs createArgs = CreateArgs.builder().withPrefix(BLOG_PREFIX).on(CreateArgs.TargetType.HASH).build(); redis.ftCreate(BLOG_INDEX, createArgs, Arrays.asList(titleField, contentField, categoryField)); @@ -439,24 +432,22 @@ void testAdvancedSearchFeatures() { redis.hmset("blog:post:3", post3); // Test 1: Search with INKEYS (limit search to specific keys) - SearchArgs inKeysArgs = SearchArgs. builder().inKey("blog:post:1").inKey("blog:post:2") - .build(); - SearchReply results = redis.ftSearch(BLOG_INDEX, "Redis", inKeysArgs); + SearchArgs inKeysArgs = SearchArgs. builder().inKey("blog:post:1").inKey("blog:post:2").build(); + SearchReply results = redis.ftSearch(BLOG_INDEX, "Redis", inKeysArgs); assertThat(results.getCount()).isEqualTo(2); // Only posts 1 and 2 // Test 2: Search with INFIELDS (limit search to specific fields) - SearchArgs inFieldsArgs = SearchArgs. builder().inField("title").build(); + SearchArgs inFieldsArgs = SearchArgs. builder().inField("title").build(); results = redis.ftSearch(BLOG_INDEX, "Redis", inFieldsArgs); assertThat(results.getCount()).isEqualTo(2); // Only matches in title field // Test 3: Search with TIMEOUT - SearchArgs timeoutArgs = SearchArgs. builder().timeout(Duration.ofSeconds(5)).build(); + SearchArgs timeoutArgs = SearchArgs. builder().timeout(Duration.ofSeconds(5)).build(); results = redis.ftSearch(BLOG_INDEX, "Redis", timeoutArgs); assertThat(results.getCount()).isEqualTo(2); // Test 4: Search with PARAMS (parameterized query) - SearchArgs paramsArgs = SearchArgs. builder().param("category_param", "tutorial") - .build(); + SearchArgs paramsArgs = SearchArgs. builder().param("category_param", "tutorial").build(); results = redis.ftSearch(BLOG_INDEX, "@category:{$category_param}", paramsArgs); assertThat(results.getCount()).isEqualTo(2); // Posts with tutorial category @@ -470,13 +461,12 @@ void testAdvancedSearchFeatures() { @Test void testComplexQueriesAndBooleanOperations() { // Create index for testing complex queries - FieldArgs titleField = TextFieldArgs. builder().name("title").build(); - FieldArgs descriptionField = TextFieldArgs. builder().name("description").build(); - FieldArgs tagsField = TagFieldArgs. builder().name("tags").build(); - FieldArgs ratingField = NumericFieldArgs. builder().name("rating").build(); + FieldArgs titleField = TextFieldArgs.builder().name("title").build(); + FieldArgs descriptionField = TextFieldArgs.builder().name("description").build(); + FieldArgs tagsField = TagFieldArgs.builder().name("tags").build(); + FieldArgs ratingField = NumericFieldArgs.builder().name("rating").build(); - CreateArgs createArgs = CreateArgs. builder().withPrefix(MOVIE_PREFIX) - .on(CreateArgs.TargetType.HASH).build(); + CreateArgs createArgs = CreateArgs.builder().withPrefix(MOVIE_PREFIX).on(CreateArgs.TargetType.HASH).build(); redis.ftCreate(MOVIES_INDEX, createArgs, Arrays.asList(titleField, descriptionField, tagsField, ratingField)); @@ -510,9 +500,9 @@ void testComplexQueriesAndBooleanOperations() { redis.hmset("movie:4", movie4); // Test 1: Boolean AND operation - SearchReply results = redis.ftSearch(MOVIES_INDEX, "((@tags:{thriller}) (@tags:{action}))"); + SearchReply results = redis.ftSearch(MOVIES_INDEX, "((@tags:{thriller}) (@tags:{action}))"); assertThat(results.getCount()).isEqualTo(1); // The Matrix - assertThat(results.getResults().get(0).getFields().get("title")).isEqualTo("The Matrix"); + assertThat(results.getResults().get(0).getFields().get("title").asString()).isEqualTo("The Matrix"); // Test 2: Boolean OR operation results = redis.ftSearch(MOVIES_INDEX, "((@tags:{thriller}) | (@tags:{crime}))"); @@ -526,7 +516,7 @@ void testComplexQueriesAndBooleanOperations() { results = redis.ftSearch(MOVIES_INDEX, "@title:\"Inception\""); assertThat(results.getCount()).isEqualTo(1); - assertThat(results.getResults().get(0).getFields().get("title")).isEqualTo("Inception"); + assertThat(results.getResults().get(0).getFields().get("title").asString()).isEqualTo("Inception"); // Test 5: Wildcard search results = redis.ftSearch(MOVIES_INDEX, "Matrix*"); @@ -550,10 +540,9 @@ void testComplexQueriesAndBooleanOperations() { @Test void testEmptyResultsAndEdgeCases() { // Create a simple index - FieldArgs titleField = TextFieldArgs. builder().name("title").build(); + FieldArgs titleField = TextFieldArgs.builder().name("title").build(); - CreateArgs createArgs = CreateArgs. builder().withPrefix(BLOG_PREFIX) - .on(CreateArgs.TargetType.HASH).build(); + CreateArgs createArgs = CreateArgs.builder().withPrefix(BLOG_PREFIX).on(CreateArgs.TargetType.HASH).build(); redis.ftCreate(BLOG_INDEX, createArgs, Collections.singletonList(titleField)); @@ -563,18 +552,18 @@ void testEmptyResultsAndEdgeCases() { redis.hmset("blog:post:1", post1); // Test 1: Search for non-existent term - SearchReply results = redis.ftSearch(BLOG_INDEX, "nonexistent"); + SearchReply results = redis.ftSearch(BLOG_INDEX, "nonexistent"); assertThat(results.getCount()).isEqualTo(0); assertThat(results.getResults()).isEmpty(); // Test 2: Search with LIMIT beyond available results - SearchArgs limitArgs = SearchArgs. builder().limit(10, 20).build(); + SearchArgs limitArgs = SearchArgs. builder().limit(10, 20).build(); results = redis.ftSearch(BLOG_INDEX, "Redis", limitArgs); assertThat(results.getCount()).isEqualTo(1); assertThat(results.getResults()).isEmpty(); // No results in range 10-20 // Test 3: Search with NOCONTENT and WITHSCORES - SearchArgs combinedArgs = SearchArgs. builder().noContent().withScores().build(); + SearchArgs combinedArgs = SearchArgs. builder().noContent().withScores().build(); results = redis.ftSearch(BLOG_INDEX, "Redis", combinedArgs); assertThat(results.getCount()).isEqualTo(1); assertThat(results.getResults()).hasSize(1); @@ -593,8 +582,7 @@ void testFtAlterAddingNewFields() { String testIndex = "alter-test-idx"; // Create initial index with one field - List> initialFields = Collections - .singletonList(TextFieldArgs. builder().name("title").build()); + List initialFields = Collections.singletonList(TextFieldArgs.builder().name("title").build()); assertThat(redis.ftCreate(testIndex, initialFields)).isEqualTo("OK"); @@ -604,13 +592,12 @@ void testFtAlterAddingNewFields() { redis.hset("doc:1", doc1); // Verify initial search works - SearchReply initialSearch = redis.ftSearch(testIndex, "Test"); + SearchReply initialSearch = redis.ftSearch(testIndex, "Test"); assertThat(initialSearch.getCount()).isEqualTo(1); // Add new fields to the index - List> newFields = Arrays.asList( - NumericFieldArgs. builder().name("published_at").sortable().build(), - TextFieldArgs. builder().name("author").build()); + List newFields = Arrays.asList(NumericFieldArgs.builder().name("published_at").sortable().build(), + TextFieldArgs.builder().name("author").build()); assertThat(redis.ftAlter(testIndex, false, newFields)).isEqualTo("OK"); @@ -628,11 +615,11 @@ NumericFieldArgs. builder().name("published_at").sortable().build(), redis.hset("doc:2", doc2); // Verify search still works and new fields are indexed - SearchReply searchAfterAlter = redis.ftSearch(testIndex, "Document"); + SearchReply searchAfterAlter = redis.ftSearch(testIndex, "Document"); assertThat(searchAfterAlter.getCount()).isEqualTo(2); // Search by new field - SearchReply authorSearch = redis.ftSearch(testIndex, "@author:John"); + SearchReply authorSearch = redis.ftSearch(testIndex, "@author:John"); assertThat(authorSearch.getCount()).isEqualTo(1); assertThat(authorSearch.getResults().get(0).getId()).isEqualTo("doc:1"); @@ -647,8 +634,7 @@ void testFtAlterWithSkipInitialScan() { String testIndex = "alter-skip-test-idx"; // Create initial index - List> initialFields = Collections - .singletonList(TextFieldArgs. builder().name("title").build()); + List initialFields = Collections.singletonList(TextFieldArgs.builder().name("title").build()); assertThat(redis.ftCreate(testIndex, initialFields)).isEqualTo("OK"); @@ -659,13 +645,12 @@ void testFtAlterWithSkipInitialScan() { redis.hset("doc:1", doc1); // Add new field with SKIPINITIALSCAN - List> newFields = Collections - .singletonList(TextFieldArgs. builder().name("category").build()); + List newFields = Collections.singletonList(TextFieldArgs.builder().name("category").build()); assertThat(redis.ftAlter(testIndex, true, newFields)).isEqualTo("OK"); // The existing document should not be indexed for the new field due to SKIPINITIALSCAN - SearchReply categorySearch = redis.ftSearch(testIndex, "@category:Technology"); + SearchReply categorySearch = redis.ftSearch(testIndex, "@category:Technology"); assertThat(categorySearch.getCount()).isEqualTo(0); // But new documents should be indexed for the new field @@ -674,7 +659,7 @@ void testFtAlterWithSkipInitialScan() { doc2.put("category", "Science"); redis.hset("doc:2", doc2); - SearchReply newCategorySearch = redis.ftSearch(testIndex, "@category:Science"); + SearchReply newCategorySearch = redis.ftSearch(testIndex, "@category:Science"); assertThat(newCategorySearch.getCount()).isEqualTo(1); assertThat(newCategorySearch.getResults().get(0).getId()).isEqualTo("doc:2"); @@ -691,7 +676,7 @@ void testFtAliasCommands() { String alias = "test-alias"; // Create test indexes - List> fields = Collections.singletonList(TextFieldArgs. builder().name("title").build()); + List fields = Collections.singletonList(TextFieldArgs.builder().name("title").build()); assertThat(redis.ftCreate(testIndex, fields)).isEqualTo("OK"); assertThat(redis.ftCreate(testIndex2, fields)).isEqualTo("OK"); @@ -705,7 +690,7 @@ void testFtAliasCommands() { redis.hset("doc:1", doc); // Search using alias should work - SearchReply aliasSearch = redis.ftSearch(alias, "Test"); + SearchReply aliasSearch = redis.ftSearch(alias, "Test"); assertThat(aliasSearch.getCount()).isEqualTo(1); // Test FT.ALIASUPDATE - switch alias to different index @@ -717,7 +702,7 @@ void testFtAliasCommands() { redis.hset("doc:2", doc2); // Search using alias should now return results from second index - SearchReply updatedAliasSearch = redis.ftSearch(alias, "Different"); + SearchReply updatedAliasSearch = redis.ftSearch(alias, "Different"); assertThat(updatedAliasSearch.getCount()).isEqualTo(1); assertThat(updatedAliasSearch.getResults().get(0).getId()).isEqualTo("doc:2"); @@ -737,8 +722,8 @@ void testFtTagvals() { String testIndex = "tagvals-test-idx"; // Create index with a tag field - List> fields = Arrays.asList(TextFieldArgs. builder().name("title").build(), - TagFieldArgs. builder().name("category").build()); + List fields = Arrays.asList(TextFieldArgs.builder().name("title").build(), + TagFieldArgs.builder().name("category").build()); assertThat(redis.ftCreate(testIndex, fields)).isEqualTo("OK"); @@ -795,19 +780,19 @@ void testFtSuggestionCommands() { assertThat(redis.ftSuglen(suggestionKey)).isEqualTo(5L); // Test FT.SUGGET - Get suggestions for prefix - List> suggestions = redis.ftSugget(suggestionKey, "New"); + List suggestions = redis.ftSugget(suggestionKey, "New"); assertThat(suggestions).hasSize(3); assertThat(suggestions.stream().map(Suggestion::getValue)).containsExactlyInAnyOrder("New York", "New Orleans", "Newark"); // Test FT.SUGGET with MAX limit - SugGetArgs maxArgs = SugGetArgs.Builder.max(2); - List> limitedSuggestions = redis.ftSugget(suggestionKey, "New", maxArgs); + SugGetArgs maxArgs = SugGetArgs.Builder.max(2); + List limitedSuggestions = redis.ftSugget(suggestionKey, "New", maxArgs); assertThat(limitedSuggestions).hasSize(2); // Test FT.SUGGET with FUZZY matching - SugGetArgs fuzzyArgs = SugGetArgs.Builder.fuzzy(); - List> fuzzySuggestions = redis.ftSugget(suggestionKey, "Bost", fuzzyArgs); + SugGetArgs fuzzyArgs = SugGetArgs.Builder.fuzzy(); + List fuzzySuggestions = redis.ftSugget(suggestionKey, "Bost", fuzzyArgs); assertThat(fuzzySuggestions.stream().map(Suggestion::getValue)).contains("Boston"); // Test FT.SUGDEL - Delete a suggestion @@ -815,7 +800,7 @@ void testFtSuggestionCommands() { assertThat(redis.ftSuglen(suggestionKey)).isEqualTo(4L); // Verify deletion - List> afterDeletion = redis.ftSugget(suggestionKey, "New"); + List afterDeletion = redis.ftSugget(suggestionKey, "New"); assertThat(afterDeletion).hasSize(2); assertThat(afterDeletion.stream().map(Suggestion::getValue)).containsExactlyInAnyOrder("New York", "New Orleans"); @@ -823,19 +808,21 @@ void testFtSuggestionCommands() { assertThat(redis.ftSugdel(suggestionKey, "NonExistent")).isFalse(); // Test FT.SUGADD with INCR and PAYLOAD - SugAddArgs incrArgs = SugAddArgs.Builder. incr().payload("US-East"); + SugAddArgs incrArgs = SugAddArgs.Builder.incr().payload("US-East"); assertThat(redis.ftSugadd(suggestionKey, "New York", 0.5, incrArgs)).isEqualTo(4L); // Test FT.SUGGET with WITHSCORES and WITHPAYLOADS - SugGetArgs withExtrasArgs = SugGetArgs.Builder. withScores().withPayloads(); - List> detailedSuggestions = redis.ftSugget(suggestionKey, "New", withExtrasArgs); + SugGetArgs withExtrasArgs = SugGetArgs.Builder.withScores().withPayloads(); + List detailedSuggestions = redis.ftSugget(suggestionKey, "New", withExtrasArgs); assertThat(detailedSuggestions).isNotEmpty(); // Verify that suggestions with scores and payloads are properly parsed - for (Suggestion suggestion : detailedSuggestions) { + for (Suggestion suggestion : detailedSuggestions) { assertThat(suggestion.getValue()).isNotNull(); if ("New York".equals(suggestion.getValue())) { assertThat(suggestion.hasScore()).isTrue(); + // the score is returned as a string on the wire and must be parsed to its numeric value, not 0.0 + assertThat(suggestion.getScore()).isGreaterThan(0.0); assertThat(suggestion.hasPayload()).isTrue(); assertThat(suggestion.getPayload()).isEqualTo("US-East"); } @@ -904,12 +891,11 @@ void testFtSpellcheckCommand() { String testIndex = "spellcheck-idx"; // Create field definitions - FieldArgs titleField = TextFieldArgs. builder().name("title").build(); - FieldArgs contentField = TextFieldArgs. builder().name("content").build(); + FieldArgs titleField = TextFieldArgs.builder().name("title").build(); + FieldArgs contentField = TextFieldArgs.builder().name("content").build(); // Create an index with some documents - CreateArgs createArgs = CreateArgs. builder().withPrefix("doc:") - .on(CreateArgs.TargetType.HASH).build(); + CreateArgs createArgs = CreateArgs.builder().withPrefix("doc:").on(CreateArgs.TargetType.HASH).build(); assertThat(redis.ftCreate(testIndex, createArgs, Arrays.asList(titleField, contentField))).isEqualTo("OK"); @@ -935,17 +921,17 @@ void testFtSpellcheckCommand() { redis.hmset("doc:4", doc4); // Test basic spellcheck with misspelled words - SpellCheckResult result = redis.ftSpellcheck(testIndex, "reids serch"); + SpellCheckResult result = redis.ftSpellcheck(testIndex, "reids serch"); assertThat(result.hasMisspelledTerms()).isTrue(); assertThat(result.getMisspelledTermCount()).isEqualTo(2); // Check first misspelled term "reids" - SpellCheckResult.MisspelledTerm firstTerm = result.getMisspelledTerms().get(0); + SpellCheckResult.MisspelledTerm firstTerm = result.getMisspelledTerms().get(0); assertThat(firstTerm.getTerm()).isEqualTo("reids"); assertThat(firstTerm.hasSuggestions()).isFalse(); // Check second misspelled term "serch" - SpellCheckResult.MisspelledTerm secondTerm = result.getMisspelledTerms().get(1); + SpellCheckResult.MisspelledTerm secondTerm = result.getMisspelledTerms().get(1); assertThat(secondTerm.getTerm()).isEqualTo("serch"); assertThat(secondTerm.hasSuggestions()).isTrue(); @@ -955,25 +941,25 @@ void testFtSpellcheckCommand() { assertThat(hasSearchSuggestion).isTrue(); // Test spellcheck with distance parameter - SpellCheckArgs distanceArgs = SpellCheckArgs.Builder.distance(2); - SpellCheckResult distanceResult = redis.ftSpellcheck(testIndex, "databse", distanceArgs); + SpellCheckArgs distanceArgs = SpellCheckArgs.Builder.distance(2); + SpellCheckResult distanceResult = redis.ftSpellcheck(testIndex, "databse", distanceArgs); assertThat(distanceResult.hasMisspelledTerms()).isTrue(); // Test spellcheck with custom dictionary String dictKey = "custom-dict"; redis.ftDictadd(dictKey, "elasticsearch", "solr", "lucene"); - SpellCheckArgs includeArgs = SpellCheckArgs.Builder.termsInclude(dictKey); - SpellCheckResult includeResult = redis.ftSpellcheck(testIndex, "elasticsearh", includeArgs); + SpellCheckArgs includeArgs = SpellCheckArgs.Builder.termsInclude(dictKey); + SpellCheckResult includeResult = redis.ftSpellcheck(testIndex, "elasticsearh", includeArgs); assertThat(includeResult.hasMisspelledTerms()).isTrue(); // Test spellcheck with exclude dictionary - SpellCheckArgs excludeArgs = SpellCheckArgs.Builder.termsExclude(dictKey); - SpellCheckResult excludeResult = redis.ftSpellcheck(testIndex, "elasticsearh", excludeArgs); + SpellCheckArgs excludeArgs = SpellCheckArgs.Builder.termsExclude(dictKey); + SpellCheckResult excludeResult = redis.ftSpellcheck(testIndex, "elasticsearh", excludeArgs); assertThat(excludeResult.hasMisspelledTerms()).isTrue(); // Test spellcheck with correct words (should return no misspelled terms) - SpellCheckResult correctResult = redis.ftSpellcheck(testIndex, "redis search"); + SpellCheckResult correctResult = redis.ftSpellcheck(testIndex, "redis search"); assertThat(correctResult.hasMisspelledTerms()).isFalse(); assertThat(correctResult.getMisspelledTermCount()).isEqualTo(0); @@ -990,12 +976,11 @@ void testFtExplainCommand() { String testIndex = "explain-idx"; // Create field definitions - FieldArgs titleField = TextFieldArgs. builder().name("title").build(); - FieldArgs contentField = TextFieldArgs. builder().name("content").build(); + FieldArgs titleField = TextFieldArgs.builder().name("title").build(); + FieldArgs contentField = TextFieldArgs.builder().name("content").build(); // Create an index - CreateArgs createArgs = CreateArgs. builder().withPrefix("doc:") - .on(CreateArgs.TargetType.HASH).build(); + CreateArgs createArgs = CreateArgs.builder().withPrefix("doc:").on(CreateArgs.TargetType.HASH).build(); assertThat(redis.ftCreate(testIndex, createArgs, Arrays.asList(titleField, contentField))).isEqualTo("OK"); @@ -1006,7 +991,7 @@ void testFtExplainCommand() { assertThat(basicExplain).contains("INTERSECT", "UNION", "hello", "world"); // Test explain with dialect - ExplainArgs dialectArgs = ExplainArgs.Builder.dialect(QueryDialects.DIALECT1); + ExplainArgs dialectArgs = ExplainArgs.Builder.dialect(QueryDialects.DIALECT1); String dialectExplain = redis.ftExplain(testIndex, "hello world", dialectArgs); assertThat(dialectExplain).isNotNull(); assertThat(dialectExplain).isNotEmpty(); @@ -1034,16 +1019,14 @@ void testFtListCommand() { List initialIndexes = redis.ftList(); // Create field definitions - FieldArgs titleField = TextFieldArgs. builder().name("title").build(); + FieldArgs titleField = TextFieldArgs.builder().name("title").build(); // Create first index - CreateArgs createArgs1 = CreateArgs. builder().withPrefix("doc1:") - .on(CreateArgs.TargetType.HASH).build(); + CreateArgs createArgs1 = CreateArgs.builder().withPrefix("doc1:").on(CreateArgs.TargetType.HASH).build(); assertThat(redis.ftCreate(testIndex1, createArgs1, Collections.singletonList(titleField))).isEqualTo("OK"); // Create second index - CreateArgs createArgs2 = CreateArgs. builder().withPrefix("doc2:") - .on(CreateArgs.TargetType.HASH).build(); + CreateArgs createArgs2 = CreateArgs.builder().withPrefix("doc2:").on(CreateArgs.TargetType.HASH).build(); assertThat(redis.ftCreate(testIndex2, createArgs2, Collections.singletonList(titleField))).isEqualTo("OK"); // Get updated list of indexes @@ -1071,12 +1054,11 @@ void testSearchWithFieldAliases() { String testIndex = "alias-field-idx"; // Create index with multiple fields - FieldArgs titleField = TextFieldArgs. builder().name("title").build(); - FieldArgs authorField = TextFieldArgs. builder().name("author").build(); - FieldArgs priceField = NumericFieldArgs. builder().name("price").build(); + FieldArgs titleField = TextFieldArgs.builder().name("title").build(); + FieldArgs authorField = TextFieldArgs.builder().name("author").build(); + FieldArgs priceField = NumericFieldArgs.builder().name("price").build(); - CreateArgs createArgs = CreateArgs. builder().withPrefix("book:") - .on(CreateArgs.TargetType.HASH).build(); + CreateArgs createArgs = CreateArgs.builder().withPrefix("book:").on(CreateArgs.TargetType.HASH).build(); assertThat(redis.ftCreate(testIndex, createArgs, Arrays.asList(titleField, authorField, priceField))).isEqualTo("OK"); @@ -1094,26 +1076,26 @@ void testSearchWithFieldAliases() { redis.hmset("book:2", book2); // Test 1: Search with field alias - rename single field - SearchArgs aliasArgs = SearchArgs. builder().returnField("title", "book_title").build(); - SearchReply results = redis.ftSearch(testIndex, "Redis", aliasArgs); + SearchArgs aliasArgs = SearchArgs. builder().returnField("title", "book_title").build(); + SearchReply results = redis.ftSearch(testIndex, "Redis", aliasArgs); assertThat(results.getCount()).isEqualTo(2); assertThat(results.getResults()).hasSize(2); // Verify that the field is returned with the alias name - for (SearchReply.SearchResult result : results.getResults()) { + for (SearchReply.SearchResult result : results.getResults()) { assertThat(result.getFields()).containsKey("book_title"); assertThat(result.getFields()).doesNotContainKey("title"); - assertThat(result.getFields().get("book_title")).contains("Redis"); + assertThat(result.getFields().get("book_title").asString()).contains("Redis"); } // Test 2: Search with multiple field aliases - SearchArgs multiAliasArgs = SearchArgs. builder().returnField("title", "book_title") + SearchArgs multiAliasArgs = SearchArgs. builder().returnField("title", "book_title") .returnField("author", "writer").returnField("price", "cost").build(); results = redis.ftSearch(testIndex, "Redis", multiAliasArgs); assertThat(results.getCount()).isEqualTo(2); - for (SearchReply.SearchResult result : results.getResults()) { + for (SearchReply.SearchResult result : results.getResults()) { // Verify aliased fields are present assertThat(result.getFields()).containsKey("book_title"); assertThat(result.getFields()).containsKey("writer"); @@ -1126,12 +1108,12 @@ void testSearchWithFieldAliases() { } // Test 3: Mix of aliased and non-aliased fields - SearchArgs mixedArgs = SearchArgs. builder().returnField("title", "book_title") - .returnField("author").build(); + SearchArgs mixedArgs = SearchArgs. builder().returnField("title", "book_title").returnField("author") + .build(); results = redis.ftSearch(testIndex, "Redis", mixedArgs); assertThat(results.getCount()).isEqualTo(2); - for (SearchReply.SearchResult result : results.getResults()) { + for (SearchReply.SearchResult result : results.getResults()) { // Verify aliased field assertThat(result.getFields()).containsKey("book_title"); assertThat(result.getFields()).doesNotContainKey("title"); @@ -1155,12 +1137,11 @@ void testFtSynonymCommands() { String testIndex = "synonym-idx"; // Create field definitions - FieldArgs titleField = TextFieldArgs. builder().name("title").build(); - FieldArgs contentField = TextFieldArgs. builder().name("content").build(); + FieldArgs titleField = TextFieldArgs.builder().name("title").build(); + FieldArgs contentField = TextFieldArgs.builder().name("content").build(); // Create an index - CreateArgs createArgs = CreateArgs. builder().withPrefix("doc:") - .on(CreateArgs.TargetType.HASH).build(); + CreateArgs createArgs = CreateArgs.builder().withPrefix("doc:").on(CreateArgs.TargetType.HASH).build(); assertThat(redis.ftCreate(testIndex, createArgs, Arrays.asList(titleField, contentField))).isEqualTo("OK"); @@ -1185,7 +1166,7 @@ void testFtSynonymCommands() { assertThat(synonymsAfterUpdate.get("vehicle")).containsExactly("group1"); // Test synonym update with SKIPINITIALSCAN - SynUpdateArgs skipArgs = SynUpdateArgs.Builder.skipInitialScan(); + SynUpdateArgs skipArgs = SynUpdateArgs.Builder.skipInitialScan(); String result2 = redis.ftSynupdate(testIndex, "group2", skipArgs, "fast", "quick", "rapid"); assertThat(result2).isEqualTo("OK"); @@ -1218,18 +1199,17 @@ void testFtSynonymCommands() { void ftHybridAdvancedMultiQueryWithPostProcessing() { String indexName = "idx:ecommerce"; - FieldArgs titleField = TextFieldArgs. builder().name("title").build(); - FieldArgs categoryField = TagFieldArgs. builder().name("category").build(); - FieldArgs brandField = TagFieldArgs. builder().name("brand").build(); - FieldArgs priceField = NumericFieldArgs. builder().name("price").build(); - FieldArgs ratingField = NumericFieldArgs. builder().name("rating").build(); + FieldArgs titleField = TextFieldArgs.builder().name("title").build(); + FieldArgs categoryField = TagFieldArgs.builder().name("category").build(); + FieldArgs brandField = TagFieldArgs.builder().name("brand").build(); + FieldArgs priceField = NumericFieldArgs.builder().name("price").build(); + FieldArgs ratingField = NumericFieldArgs.builder().name("rating").build(); - FieldArgs vectorField = VectorFieldArgs. builder().name("image_embedding").hnsw() + FieldArgs vectorField = VectorFieldArgs. builder().name("image_embedding").hnsw() .type(VectorFieldArgs.VectorType.FLOAT32).dimensions(10).distanceMetric(VectorFieldArgs.DistanceMetric.COSINE) .build(); - CreateArgs createArgs = CreateArgs. builder().withPrefix("product:") - .on(CreateArgs.TargetType.HASH).build(); + CreateArgs createArgs = CreateArgs.builder().withPrefix("product:").on(CreateArgs.TargetType.HASH).build(); assertThat(redis.ftCreate(indexName, createArgs, Arrays.asList(titleField, categoryField, brandField, priceField, ratingField, vectorField))).isEqualTo("OK"); @@ -1267,23 +1247,23 @@ void ftHybridAdvancedMultiQueryWithPostProcessing() { byte[] queryVector = floatArrayToByteArray(new float[] { 0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f, 0.7f, 0.8f, 0.9f, 1.0f }); - HybridArgs hybridArgs = HybridArgs. builder() - .search(HybridSearchArgs. builder().query("@category:{electronics} smartphone camera") - .scoreAlias("text_score").build()) - .vectorSearch(HybridVectorArgs. builder().field("@image_embedding").vector("$vec") - .method(HybridVectorArgs.Knn.of(20).efRuntime(150)).filter("@brand:{apple|samsung|google}") - .scoreAlias("vector_score").build()) - .combine(Combiners. linear().alpha(0.7).beta(0.3).window(26)) - .postProcessing(PostProcessingArgs. builder().load("@price", "@brand", "@category") - .groupBy(GroupBy. of("@brand").reduce(Reducers.sum("@price").as("sum")) - .reduce(Reducers. count().as("count"))) - .sortBy(SortBy.of(new SortProperty<>("@sum", SortDirection.ASC), - new SortProperty<>("@count", SortDirection.DESC))) + HybridArgs hybridArgs = HybridArgs.builder() + .search(HybridSearchArgs.builder().query("@category:{electronics} smartphone camera").scoreAlias("text_score") + .build()) + .vectorSearch(HybridVectorArgs + .builder().field("@image_embedding").vector("$vec").method(HybridVectorArgs.Knn.of(20).efRuntime(150)) + .filter("@brand:{apple|samsung|google}").scoreAlias("vector_score").build()) + .combine(Combiners.linear().alpha(0.7).beta(0.3).window(26)) + .postProcessing(PostProcessingArgs.builder().load("@price", "@brand", "@category") + .groupBy(GroupBy.of("@brand").reduce(Reducers.sum("@price").as("sum")) + .reduce(Reducers.count().as("count"))) + .sortBy(SortBy.of(new SortProperty("@sum", SortDirection.ASC), + new SortProperty("@count", SortDirection.DESC))) .apply(Apply.of("@sum * 0.9", "discounted_price")).filter(Filter.of("@sum > 700")) .limit(Limit.of(0, 20)).build()) .param("vec", queryVector).param("discount_rate", "0.9").build(); - HybridReply reply = redis.ftHybrid(indexName, hybridArgs); + HybridReply reply = redis.ftHybrid(indexName, hybridArgs); // Verify results assertThat(reply).isNotNull(); @@ -1294,25 +1274,25 @@ void ftHybridAdvancedMultiQueryWithPostProcessing() { assertThat(reply.getExecutionTime()).isGreaterThan(0L); // Verify first result (google) - Map r1 = reply.getResults().get(0); - assertThat(r1.get("brand")).isEqualTo("google"); - assertThat(r1.get("count")).isEqualTo("2"); - assertThat(r1.get("sum")).isEqualTo("1398"); - assertThat(r1.get("discounted_price")).isEqualTo("1258.2"); + Map r1 = reply.getResults().get(0).getFields(); + assertThat(r1.get("brand").asString()).isEqualTo("google"); + assertThat(r1.get("count").asString()).isEqualTo("2"); + assertThat(r1.get("sum").asString()).isEqualTo("1398"); + assertThat(r1.get("discounted_price").asString()).isEqualTo("1258.2"); // Verify second result (samsung) - Map r2 = reply.getResults().get(1); - assertThat(r2.get("brand")).isEqualTo("samsung"); - assertThat(r2.get("count")).isEqualTo("2"); - assertThat(r2.get("sum")).isEqualTo("1598"); - assertThat(r2.get("discounted_price")).isEqualTo("1438.2"); + Map r2 = reply.getResults().get(1).getFields(); + assertThat(r2.get("brand").asString()).isEqualTo("samsung"); + assertThat(r2.get("count").asString()).isEqualTo("2"); + assertThat(r2.get("sum").asString()).isEqualTo("1598"); + assertThat(r2.get("discounted_price").asString()).isEqualTo("1438.2"); // Verify third result (apple) - Map r3 = reply.getResults().get(2); - assertThat(r3.get("brand")).isEqualTo("apple"); - assertThat(r3.get("count")).isEqualTo("3"); - assertThat(r3.get("sum")).isEqualTo("2997"); - assertThat(r3.get("discounted_price")).isEqualTo("2697.3"); + Map r3 = reply.getResults().get(2).getFields(); + assertThat(r3.get("brand").asString()).isEqualTo("apple"); + assertThat(r3.get("count").asString()).isEqualTo("3"); + assertThat(r3.get("sum").asString()).isEqualTo("2997"); + assertThat(r3.get("discounted_price").asString()).isEqualTo("2697.3"); redis.ftDropindex(indexName); } @@ -1333,13 +1313,12 @@ void testSearchWithLargeJsonPayloads() { try (StatefulRedisConnection connection = testClient.connect()) { RedisCommands testRedis = connection.sync(); - testRedis.ftCreate(testIndex, - CreateArgs. builder().on(CreateArgs.TargetType.JSON).withPrefix(prefix).build(), - Collections.singletonList(NumericFieldArgs. builder().name("$.pos").as("pos").build())); + testRedis.ftCreate(testIndex, CreateArgs.builder().on(CreateArgs.TargetType.JSON).withPrefix(prefix).build(), + Collections.singletonList(NumericFieldArgs.builder().name("$.pos").as("pos").build())); // Add sorting by pos to ensure deterministic order - SearchArgs searchArgs = SearchArgs. builder() - .sortBy(SortByArgs. builder().attribute("pos").build()).limit(0, 10_000).build(); + SearchArgs searchArgs = SearchArgs. builder().sortBy(SortByArgs.builder().attribute("pos").build()) + .limit(0, 10_000).build(); // Store expected values for exact comparison ArrayList expected = new ArrayList<>(); @@ -1355,12 +1334,12 @@ CreateArgs. builder().on(CreateArgs.TargetType.JSON).withPrefix( // Start checking at iteration 924 like the reproducer - this is where ~200KB threshold is reached if (i >= 924) { - SearchReply reply = testRedis.ftSearch(testIndex, "*", searchArgs); + SearchReply reply = testRedis.ftSearch(testIndex, "*", searchArgs); assertThat(reply.getCount()).isEqualTo(i); // Exact value comparison at each position for (int t = 0; t < expected.size(); t++) { - String actualBody = reply.getResults().get(t).getFields().get("$"); + String actualBody = reply.getResults().get(t).getFields().get("$").asString(); assertThat(actualBody).as("Mismatch at position %d on loop %d", t, i).isEqualTo(expected.get(t)); } } diff --git a/src/test/java/io/lettuce/core/search/RediSearchKeylessRoutingIntegrationTests.java b/src/test/java/io/lettuce/core/search/RediSearchKeylessRoutingIntegrationTests.java index 99241db352..523c96e6b8 100644 --- a/src/test/java/io/lettuce/core/search/RediSearchKeylessRoutingIntegrationTests.java +++ b/src/test/java/io/lettuce/core/search/RediSearchKeylessRoutingIntegrationTests.java @@ -100,13 +100,12 @@ void setUp() { connection.sync().flushall(); // Schema for text search tests - FieldArgs title = TextFieldArgs. builder().name("title").build(); - FieldArgs author = TagFieldArgs. builder().name("author").build(); - FieldArgs year = NumericFieldArgs. builder().name("year").sortable().build(); - FieldArgs rating = NumericFieldArgs. builder().name("rating").sortable().build(); + FieldArgs title = TextFieldArgs.builder().name("title").build(); + FieldArgs author = TagFieldArgs.builder().name("author").build(); + FieldArgs year = NumericFieldArgs.builder().name("year").sortable().build(); + FieldArgs rating = NumericFieldArgs.builder().name("rating").sortable().build(); - CreateArgs createArgs = CreateArgs. builder().withPrefix(PREFIX) - .on(CreateArgs.TargetType.HASH).build(); + CreateArgs createArgs = CreateArgs.builder().withPrefix(PREFIX).on(CreateArgs.TargetType.HASH).build(); assertThat(connection.sync().ftCreate(INDEX, createArgs, Arrays.asList(title, author, year, rating))).isEqualTo("OK"); // Data spread across slots @@ -137,10 +136,9 @@ void tearDown() { connection.sync().flushall(); } - private AggregateArgs aggWithCursor(long count) { - return AggregateArgs. builder() - .groupBy(AggregateArgs.GroupBy. of("author") - .reduce(AggregateArgs.Reducer. avg("@rating").as("avg_rating"))) + private AggregateArgs aggWithCursor(long count) { + return AggregateArgs.builder() + .groupBy(AggregateArgs.GroupBy.of("author").reduce(AggregateArgs.Reducer.avg("@rating").as("avg_rating"))) .withCursor(AggregateArgs.WithCursor.of(count)).build(); } @@ -153,9 +151,9 @@ void keylessAggregate_routesRandomly_acrossUpstreams_whenReadFromUpstream() { Set nodeIds = new HashSet<>(); int observedCursors = 0; - AggregateArgs args = aggWithCursor(1L); + AggregateArgs args = aggWithCursor(1L); for (int i = 0; i < 40 && nodeIds.size() < upstreams; i++) { - AggregationReply first = async.ftAggregate(INDEX, "*", args).toCompletableFuture().join(); + AggregationReply first = async.ftAggregate(INDEX, "*", args).toCompletableFuture().join(); assertThat(first).isNotNull(); if (first.getCursor().isPresent() && first.getCursor().get().getCursorId() > 0) { observedCursors++; @@ -182,9 +180,9 @@ void keylessAggregate_routesRandomly_acrossReplicas_whenReadFromAnyReplica() { Set nodeIds = new HashSet<>(); int observedCursors = 0; - AggregateArgs args = aggWithCursor(1L); + AggregateArgs args = aggWithCursor(1L); for (int i = 0; i < 60 && nodeIds.size() < replicas; i++) { - AggregationReply first = async.ftAggregate(INDEX, "*", args).toCompletableFuture().join(); + AggregationReply first = async.ftAggregate(INDEX, "*", args).toCompletableFuture().join(); assertThat(first).isNotNull(); if (first.getCursor().isPresent() && first.getCursor().get().getCursorId() > 0) { observedCursors++; @@ -218,9 +216,8 @@ void ftCreate_routesToUpstream_evenWhenReadFromReplica() { clearLatencyMetrics(); String tmpIndex = INDEX + ":create:" + UUID.randomUUID(); - FieldArgs title = TextFieldArgs. builder().name("title").build(); - CreateArgs createArgs = CreateArgs. builder().withPrefix(PREFIX) - .on(CreateArgs.TargetType.HASH).build(); + FieldArgs title = TextFieldArgs.builder().name("title").build(); + CreateArgs createArgs = CreateArgs.builder().withPrefix(PREFIX).on(CreateArgs.TargetType.HASH).build(); assertThat(connection.sync().ftCreate(tmpIndex, createArgs, Arrays.asList(title))).isEqualTo("OK"); Set nodes = observedNodeIdsFor(CommandType.FT_CREATE); @@ -356,25 +353,21 @@ private byte[] floatArrayToByteArray(float[] floats) { return buffer.array(); } - private HybridArgs hybridArgs() { + private HybridArgs hybridArgs() { float[] queryVector = { 0.15f, 0.25f, 0.35f, 0.45f }; - return HybridArgs. builder() - .search(HybridSearchArgs. builder().query("@category:{electronics}").build()) - .vectorSearch(HybridVectorArgs. builder().field("@embedding").vector("$vec") - .method(HybridVectorArgs.Knn.of(5)).build()) + return HybridArgs.builder().search(HybridSearchArgs.builder().query("@category:{electronics}").build()).vectorSearch( + HybridVectorArgs.builder().field("@embedding").vector("$vec").method(HybridVectorArgs.Knn.of(5)).build()) .param("vec", floatArrayToByteArray(queryVector)).build(); } private void prepareHybrid() { // Schema for hybrid search tests - FieldArgs category = TagFieldArgs. builder().name("category").build(); - FieldArgs price = NumericFieldArgs. builder().name("price").sortable().build(); - FieldArgs embedding = VectorFieldArgs. builder().name("embedding").hnsw() - .type(VectorFieldArgs.VectorType.FLOAT32).dimensions(4).distanceMetric(VectorFieldArgs.DistanceMetric.COSINE) - .build(); - - CreateArgs hybridCreateArgs = CreateArgs. builder().withPrefix(HYBRID_PREFIX) - .on(CreateArgs.TargetType.HASH).build(); + FieldArgs category = TagFieldArgs.builder().name("category").build(); + FieldArgs price = NumericFieldArgs.builder().name("price").sortable().build(); + FieldArgs embedding = VectorFieldArgs.builder().name("embedding").hnsw().type(VectorFieldArgs.VectorType.FLOAT32) + .dimensions(4).distanceMetric(VectorFieldArgs.DistanceMetric.COSINE).build(); + + CreateArgs hybridCreateArgs = CreateArgs.builder().withPrefix(HYBRID_PREFIX).on(CreateArgs.TargetType.HASH).build(); assertThat(connection.sync().ftCreate(HYBRID_INDEX, hybridCreateArgs, Arrays.asList(category, price, embedding))) .isEqualTo("OK"); diff --git a/src/test/java/io/lettuce/core/search/RediSearchPrefixingStringCodecSafetyIntegrationTests.java b/src/test/java/io/lettuce/core/search/RediSearchPrefixingStringCodecSafetyIntegrationTests.java new file mode 100644 index 0000000000..0f92a15902 --- /dev/null +++ b/src/test/java/io/lettuce/core/search/RediSearchPrefixingStringCodecSafetyIntegrationTests.java @@ -0,0 +1,585 @@ +/* + * Copyright 2026-present + * All rights reserved. + * + * Licensed under the MIT License. + */ + +package io.lettuce.core.search; + +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.codec.RedisCodec; +import io.lettuce.core.json.JsonPath; +import io.lettuce.core.search.arguments.AggregateArgs; +import io.lettuce.core.search.arguments.CreateArgs; +import io.lettuce.core.search.arguments.FieldArgs; +import io.lettuce.core.search.arguments.NumericFieldArgs; +import io.lettuce.core.search.arguments.SearchArgs; +import io.lettuce.core.search.arguments.SortByArgs; +import io.lettuce.core.search.arguments.TagFieldArgs; +import io.lettuce.core.search.arguments.TextFieldArgs; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import static io.lettuce.TestTags.INTEGRATION_TEST; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Codec-safety invariants for RediSearch when the connection key codec is non-trivial. Covers both {@code ON HASH} and + * {@code ON JSON} indexes. {@code HSET}/{@code HMSET} route hash field names and values through the connection's + * {@link RedisCodec} ({@link io.lettuce.core.protocol.CommandArgs#add(java.util.Map)} encodes each map entry as + * {@code addKey(field).addValue(value)}); {@code JSON.SET} sends its payload verbatim via + * {@link io.lettuce.core.protocol.CommandArgs#add(String)} (or {@code add(byte[])} for {@link io.lettuce.core.json.JsonValue}), + * bypassing the value codec entirely. With a prefixing key codec the stored hash field names become {@code tenant1:}. + * Schema field names and the read-side field clauses ({@code INFIELDS}, {@code RETURN}, {@code SORTBY}, {@code SUMMARIZE}, + * {@code HIGHLIGHT}) are all sent raw, so each test reproduces the prefix on every field reference to match the codec-encoded + * stored fields. Index prefixes are also sent raw, so callers must supply the physical prefix when a codec transforms key + * bytes. {@code INKEYS} is the one legitimately {@code K}-typed surface and must route through {@code encodeKey}. + * + * @author Viktoriya Kutsarova + */ +@Tag(INTEGRATION_TEST) +public class RediSearchPrefixingStringCodecSafetyIntegrationTests { + + private static final String HASH_INDEX = "codec-safety-idx"; + + private static final String JSON_INDEX = "codec-safety-json-idx"; + + private static final String HASH_DOC_KEY = "1"; + + private static final String JSON_DOC_KEY = "json:1"; + + private static final String BODY_TEXT = "A long introduction to Redis that mentions search only deep into the body text. " + + "Much preamble about indices and storage, only later do we finally cover search. " + + "More content about search follows with many examples. " + + "Finally more content after the search occurrences ends the description."; + + private static final String CODEC_PREFIX = "tenant1:"; + + private static RedisClient client; + + private static StatefulRedisConnection connection; + + private static RedisCommands redis; + + public RediSearchPrefixingStringCodecSafetyIntegrationTests() { + RedisURI uri = RedisURI.Builder.redis("127.0.0.1").withPort(16379).build(); + client = RedisClient.create(uri); + connection = client.connect(new PrefixingStringCodec(CODEC_PREFIX)); + redis = connection.sync(); + } + + @BeforeEach + void prepare() { + redis.flushall(); + + CreateArgs hashCreate = CreateArgs.builder().on(CreateArgs.TargetType.HASH).build(); + // FieldArgs.name(String) is now sent raw (no codec). The hash field names in the document below are written + // through hmset, which routes each map key via addKey and stores them codec-encoded as "tenant1:". To make + // the schema match the stored fields, the caller has to reproduce the codec's encodeKey transformation on the + // schema name manually (encodedFieldRef -> "tenant1:title", etc.). + FieldArgs hashTitle = TextFieldArgs.builder().name(encodedFieldRef("title")).sortable().build(); + FieldArgs hashBody = TextFieldArgs.builder().name(encodedFieldRef("body")).build(); + FieldArgs hashCategory = TagFieldArgs.builder().name(encodedFieldRef("category")).build(); + FieldArgs hashPrice = NumericFieldArgs.builder().name(encodedFieldRef("price")).sortable().build(); + redis.ftCreate(HASH_INDEX, hashCreate, Arrays.asList(hashTitle, hashBody, hashCategory, hashPrice)); + + Map doc = new HashMap<>(); + doc.put("title", "Redis search guide"); + doc.put("body", BODY_TEXT); + doc.put("category", "tutorial"); + doc.put("price", "50"); + redis.hmset(HASH_DOC_KEY, doc); + + CreateArgs jsonCreate = CreateArgs.builder().on(CreateArgs.TargetType.JSON).build(); + // For JSON the field name is a JSONPath sent raw (a codec prefix would corrupt the path, e.g. "tenant1:$.title"). + // JSON.SET sends its payload verbatim, so the document fields are NOT codec-encoded the way hmset field names are. + // The schema identifier that queries resolve against is the alias (AS), sent raw. Read-side clauses are also raw and + // pre-encoded by the caller, so the alias is encoded to match. Net effect: both HASH and JSON expose their fields as + // "tenant1:" to the query layer. + FieldArgs jsonTitle = TextFieldArgs.builder().name("$.title").as(encodedFieldRef("title")).sortable().build(); + FieldArgs jsonBody = TextFieldArgs.builder().name("$.body").as(encodedFieldRef("body")).build(); + FieldArgs jsonCategory = TagFieldArgs.builder().name("$.category").as(encodedFieldRef("category")).build(); + FieldArgs jsonPrice = NumericFieldArgs.builder().name("$.price").as(encodedFieldRef("price")).sortable().build(); + redis.ftCreate(JSON_INDEX, jsonCreate, Arrays.asList(jsonTitle, jsonBody, jsonCategory, jsonPrice)); + + String jsonDoc = "{\"title\":\"Redis search guide\",\"body\":\"" + BODY_TEXT + "\",\"category\":\"tutorial\"," + + "\"price\":50}"; + redis.jsonSet(JSON_DOC_KEY, JsonPath.ROOT_PATH, redis.getJsonParser().createJsonValue(jsonDoc)); + } + + @AfterAll + static void teardown() { + if (connection != null) { + connection.close(); + } + if (client != null) { + client.shutdown(); + } + } + + /** + * Sanity check. The query string is written raw via {@code args.add(query)} so this must work even when a prefixing codec + * is installed on the connection. + */ + @ParameterizedTest + @ValueSource(strings = { HASH_INDEX, JSON_INDEX }) + void baselineSearchWorksThroughCodec(String indexName) { + SearchReply result = redis.ftSearch(indexName, "search"); + assertThat(result.getCount()).isEqualTo(1L); + } + + /** + * Field-scoped query syntax ({@code @field:value}) is sent verbatim via {@code args.add(query)}. The schema field is stored + * under its encoded name {@code tenant1:title}, so the query must reference that encoded name (with {@code ':'} escaped) to + * resolve. + */ + @ParameterizedTest + @ValueSource(strings = { HASH_INDEX, JSON_INDEX }) + void fieldScopedQueryMustResolveAgainstSchemaThroughCodec(String indexName) { + // Manual workaround: the user has to (a) know the codec exists, (b) reproduce its prefix, and (c) escape any + // RediSearch-reserved characters that the prefix introduces (here, ':' must be escaped as '\:' inside the field + // reference because ':' is the field/value separator). On the wire this becomes "@tenant1\:title:Redis", which the + // server unescapes to look up the schema field "tenant1:title". + String query = "@" + escapedFieldRef("title") + ":Redis"; + + SearchReply result = redis.ftSearch(indexName, query); + + assertThat(result.getCount()).as("@field:value must resolve against the schema field name on the server side") + .isEqualTo(1L); + } + + /** + * Reproduces what the codec applies to a key, then escapes RediSearch-reserved characters introduced by the prefix so the + * result is safe to splice into a query/filter expression as {@code @}. Every call site that references a schema field + * inside a raw expression has to pay this cost. + */ + private static String escapedFieldRef(String field) { + String encoded = StandardCharsets.UTF_8.decode(new PrefixingStringCodec(CODEC_PREFIX).encodeKey(field)).toString(); + return encoded.replace(":", "\\:"); + } + + /** + * Reproduces what the codec applies to a key without escaping. Used where the field name is sent as a standalone command + * argument (e.g. {@code GROUPBY }) rather than embedded in a query/filter expression with delimiters. + */ + private static String encodedFieldRef(String field) { + return StandardCharsets.UTF_8.decode(new PrefixingStringCodec(CODEC_PREFIX).encodeKey(field)).toString(); + } + + /** + * Numeric range query ({@code @price:[40 60]}) is sent verbatim. The schema field is stored as {@code tenant1:price}, so + * the query must reference the encoded name (with {@code ':'} escaped) to resolve. NUMERIC variant of + * {@link #fieldScopedQueryMustResolveAgainstSchemaThroughCodec}. + */ + @ParameterizedTest + @ValueSource(strings = { HASH_INDEX, JSON_INDEX }) + void numericRangeQueryMustResolveAgainstSchemaThroughCodec(String indexName) { + // Manual workaround: schema field "price" is codec-encoded to "tenant1:price"; the ':' must be escaped inside the + // @field reference so the RediSearch query parser resolves the encoded schema name. + String query = "@" + escapedFieldRef("price") + ":[40 60]"; + + SearchReply result = redis.ftSearch(indexName, query); + + assertThat(result.getCount()).as("@price:[40 60] must resolve against the schema field name on the server side") + .isEqualTo(1L); + } + + /** + * Tag query ({@code @category:{tutorial}}) is sent verbatim. The schema field is stored as {@code tenant1:category}, so the + * query must reference the encoded name (with {@code ':'} escaped) to resolve. TAG variant of + * {@link #fieldScopedQueryMustResolveAgainstSchemaThroughCodec}. + */ + @ParameterizedTest + @ValueSource(strings = { HASH_INDEX, JSON_INDEX }) + void tagQueryMustResolveAgainstSchemaThroughCodec(String indexName) { + // Manual workaround: schema field "category" is codec-encoded to "tenant1:category"; ':' must be escaped inside the + // @field reference so the RediSearch query parser resolves the encoded schema name. + String query = "@" + escapedFieldRef("category") + ":{tutorial}"; + + SearchReply result = redis.ftSearch(indexName, query); + + assertThat(result.getCount()).as("@category:{tutorial} must resolve against the schema field name on the server side") + .isEqualTo(1L); + } + + /** + * Aggregate {@code GROUPBY} renders properties via {@code args.add("@" + property)} (raw, no codec). The schema field is + * stored as {@code tenant1:category}, so the property must be pre-encoded to match. + */ + @ParameterizedTest + @ValueSource(strings = { HASH_INDEX, JSON_INDEX }) + void aggregateGroupByMustResolveAgainstSchemaThroughCodec(String indexName) { + // Manual workaround: GroupBy.build emits "@" + property as a standalone command argument (not embedded in an + // expression with delimiters), so we pre-encode the property to "tenant1:category" without escaping the ':'. + AggregateArgs args = AggregateArgs.builder() + .groupBy(AggregateArgs.GroupBy.of(encodedFieldRef("category")).reduce(AggregateArgs.Reducer.count().as("cnt"))) + .build(); + + AggregationReply result = redis.ftAggregate(indexName, "*", args); + + assertThat(result.getReplies()).isNotEmpty(); + SearchReply reply = result.getReplies().get(0); + assertThat(reply.getResults()).as("GROUPBY @category must resolve against the schema field on the server side") + .isNotEmpty(); + assertThat(reply.getResults().get(0).getFields()).containsKey("cnt"); + } + + /** + * {@code FT.CREATE ... FILTER } is sent raw via {@code args.add(filter)} and evaluated at indexing time, so the + * filter must reference the schema field by its encoded name ({@code tenant1:price}, {@code ':'} escaped). The predicate + * ({@code @price>1000}) must drop the seeded {@code price=50} document, yielding count {@code 0}. + */ + @Test + void createArgsFilterExpressionMustResolveAgainstSchemaThroughCodec() { + String filteredIndex = "filter-expr-idx"; + + // Seed the hash BEFORE creating the index so the FILTER predicate is exercised during the initial scan. + Map doc = new HashMap<>(); + doc.put("title", "Filtered Redis search guide"); + doc.put("price", "50"); + redis.hmset("filtered:1", doc); + + // Manual workaround: schema field "price" is codec-encoded to "tenant1:price"; ':' must be escaped inside the + // @field reference so the FT.CREATE FILTER expression parser resolves the encoded schema name. Use a NUMERIC + // predicate that excludes the seeded document (price=50) so the filter fires unambiguously. + CreateArgs create = CreateArgs.builder().on(CreateArgs.TargetType.HASH).withPrefix(CODEC_PREFIX + "filtered:") + .filter("@" + escapedFieldRef("price") + ">1000").build(); + FieldArgs titleField = TextFieldArgs.builder().name("title").build(); + FieldArgs priceField = NumericFieldArgs.builder().name("price").build(); + redis.ftCreate(filteredIndex, create, Arrays.asList(titleField, priceField)); + + try { + Thread.sleep(500); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + SearchReply result = redis.ftSearch(filteredIndex, "*"); + + assertThat(result.getCount()) + .as("FILTER @price>1000 must resolve against the schema field; if this fails with 1, the filter expression " + + "references the field raw while the schema codec-encoded it, and the predicate is silently treated " + + "as match-all") + .isEqualTo(0L); + } + + /** + * Positive control for the document-key channel under {@code NOCONTENT}. The server returns only document IDs and the + * parser runs them back through {@code decodeKey}; the round-trip must yield the user-facing key the caller passed at write + * time ({@code HASH_DOC_KEY}), not the on-the-wire prefixed form. If this regresses, the key channel has stopped routing + * through the connection codec on the read path. + */ + @Test + void noContentDocumentIdRoundTripsThroughCodec() { + SearchArgs args = SearchArgs. builder().noContent().build(); + + SearchReply result = redis.ftSearch(HASH_INDEX, "search", args); + + assertThat(result.getCount()).isEqualTo(1L); + assertThat(result.getResults()).hasSize(1); + assertThat(result.getResults().get(0).getId()) + .as("NOCONTENT document id must be decoded through the codec back to the user-facing key") + .isEqualTo(HASH_DOC_KEY); + assertThat(result.getResults().get(0).getFields()).as("NOCONTENT must suppress field payloads").isNullOrEmpty(); + } + + /** + * Positive control for {@code FT.DROPINDEX ... DD}. With {@code DD}, the server deletes every document the index pointed to + * — those keys are stored under their codec-encoded form ({@code tenant1:1}). After the drop the underlying key must be + * gone from the database when looked up through the same codec ({@code EXISTS} routes through {@code encodeKey}). + */ + @Test + void dropIndexWithDeleteDocumentsRemovesCodecEncodedKeys() { + assertThat(redis.exists(HASH_DOC_KEY)).as("document must exist before DROPINDEX DD").isEqualTo(1L); + + redis.ftDropindex(HASH_INDEX, true); + + assertThat(redis.exists(HASH_DOC_KEY)).as("DROPINDEX DD must delete the underlying document at its codec-encoded key") + .isEqualTo(0L); + } + + /** + * {@code INFIELDS} names a schema field and is sent raw, so the caller passes the encoded name ({@code tenant1:title}) to + * match the codec-encoded schema field. + */ + @ParameterizedTest + @ValueSource(strings = { HASH_INDEX, JSON_INDEX }) + void inFieldMustNotBeMangledByCodec(String indexName) { + SearchArgs args = SearchArgs. builder().inField(encodedFieldRef("title")).build(); + + SearchReply result = redis.ftSearch(indexName, "search", args); + + assertThat(result.getCount()).as("INFIELDS must resolve against the encoded schema field").isEqualTo(1L); + } + + /** + * {@code RETURN} names a schema field, sent raw; the caller passes the encoded name ({@code tenant1:title}) to match the + * schema. Result-map field names are returned raw too, so the key appears in its encoded form. + */ + @ParameterizedTest + @ValueSource(strings = { HASH_INDEX, JSON_INDEX }) + void returnFieldMustNotBeMangledByCodec(String indexName) { + SearchArgs args = SearchArgs. builder().returnField(encodedFieldRef("title")).build(); + + SearchReply result = redis.ftSearch(indexName, "search", args); + + assertThat(result.getCount()).isEqualTo(1L); + assertThat(result.getResults().get(0).getFields()).as("RETURN schema field must appear as a key in the result map") + .containsKey(encodedFieldRef("title")); + } + + /** + * {@code RETURN ... AS alias} — the field is the encoded schema name; the alias is a raw logical identifier and appears + * verbatim in the result map. + */ + @ParameterizedTest + @ValueSource(strings = { HASH_INDEX, JSON_INDEX }) + void returnFieldAliasMustNotBeMangledByCodec(String indexName) { + SearchArgs args = SearchArgs. builder().returnField(encodedFieldRef("title"), "t").build(); + + SearchReply result = redis.ftSearch(indexName, "search", args); + + assertThat(result.getCount()).isEqualTo(1L); + assertThat(result.getResults().get(0).getFields()).as("RETURN alias must appear verbatim as a key in the result map") + .containsKey("t"); + } + + /** + * {@code SUMMARIZE FIELDS} names a schema field, sent raw; the caller passes the encoded name ({@code tenant1:body}) to + * match the schema. Redis then abbreviates the content and terminates it with the separator (default {@code ...}). + */ + // HASH only: SUMMARIZE/HIGHLIGHT are not supported on JSON indexes (server rejects with SEARCH_QUERY_BAD), so + // JSON_INDEX cannot be added to the @ValueSource here. + @Test + void summarizeFieldMustNotBeMangledByCodec() { + SearchArgs args = SearchArgs. builder().summarizeField(encodedFieldRef("body")).build(); + + SearchReply result = redis.ftSearch(HASH_INDEX, "search", args); + + assertThat(result.getCount()).isEqualTo(1L); + String body = result.getResults().get(0).getFields().get(encodedFieldRef("body")).asString(); + assertThat(body).as("SUMMARIZE must be applied to the 'body' field").contains("..."); + } + + /** + * {@code HIGHLIGHT FIELDS} names a schema field, sent raw; the caller passes the encoded name ({@code tenant1:body}) to + * match the schema. Matching terms are then wrapped with the configured tags. + */ + // HASH only: SUMMARIZE/HIGHLIGHT are not supported on JSON indexes (server rejects with SEARCH_QUERY_BAD), so + // JSON_INDEX cannot be added to the @ValueSource here. + @Test + void highlightFieldMustNotBeMangledByCodec() { + SearchArgs args = SearchArgs. builder().highlightField(encodedFieldRef("body")) + .highlightTags("", "").build(); + + SearchReply result = redis.ftSearch(HASH_INDEX, "search", args); + + assertThat(result.getCount()).isEqualTo(1L); + String body = result.getResults().get(0).getFields().get(encodedFieldRef("body")).asString(); + assertThat(body).as("HIGHLIGHT must wrap 'search' occurrences with the configured tags").contains("search"); + } + + /** + * {@code FT.AGGREGATE ... LOAD} names a schema field, sent raw; the caller passes the encoded name ({@code tenant1:title}) + * to match the schema, and the result-map key comes back in the same encoded form. + */ + @ParameterizedTest + @ValueSource(strings = { HASH_INDEX, JSON_INDEX }) + void aggregateLoadFieldMustNotBeMangledByCodec(String indexName) { + AggregateArgs args = AggregateArgs.builder().load(encodedFieldRef("title")).build(); + + AggregationReply result = redis.ftAggregate(indexName, "*", args); + + assertThat(result.getReplies()).isNotEmpty(); + SearchReply reply = result.getReplies().get(0); + assertThat(reply.getResults()).isNotEmpty(); + assertThat(reply.getResults().get(0).getFields()).as("LOAD schema field must be fetched verbatim") + .containsKey(encodedFieldRef("title")); + } + + /** + * {@code SORTBY} names a {@code SORTABLE} schema attribute, sent raw; the caller passes the encoded name + * ({@code tenant1:title}) to match the schema. + */ + @ParameterizedTest + @ValueSource(strings = { HASH_INDEX, JSON_INDEX }) + void sortByMustNotBeMangledByCodec(String indexName) { + SearchArgs args = SearchArgs. builder() + .sortBy(SortByArgs.builder().attribute(encodedFieldRef("title")).build()).build(); + + SearchReply result = redis.ftSearch(indexName, "search", args); + + assertThat(result.getCount()).as("SORTBY schema field must be sent raw, not codec-encoded").isEqualTo(1L); + } + + /** + * {@code PARAMS} substitution names are referenced as {@code $name} from the verbatim query. + * {@link SearchArgs.Builder#param} sends the name raw, so {@code $term} resolves on the server. Bare {@code $term} (no + * {@code @field:}) isolates this from schema-field-name routing. + */ + @ParameterizedTest + @ValueSource(strings = { HASH_INDEX, JSON_INDEX }) + void searchParamNameMustNotBeMangledByCodec(String indexName) { + SearchArgs args = SearchArgs. builder().param("term", "search").build(); + + SearchReply result = redis.ftSearch(indexName, "$term", args); + + assertThat(result.getCount()).as("PARAMS substitution name must be sent raw so $term resolves on the server side") + .isEqualTo(1L); + } + + /** + * Aggregate variant of {@link #searchParamNameMustNotBeMangledByCodec}; {@link AggregateArgs.Builder#param} sends the name + * raw too. + */ + @ParameterizedTest + @ValueSource(strings = { HASH_INDEX, JSON_INDEX }) + void aggregateParamNameMustNotBeMangledByCodec(String indexName) { + AggregateArgs args = AggregateArgs.builder().param("term", "search").build(); + + AggregationReply result = redis.ftAggregate(indexName, "$term", args); + + assertThat(result.getReplies()).isNotEmpty(); + SearchReply reply = result.getReplies().get(0); + assertThat(reply.getResults()).as("PARAMS substitution name must resolve on the server side").isNotEmpty(); + } + + /** + * {@code INKEYS} restricts the search to a list of actual document keys. Unlike the schema-identifier clauses above, this + * one is legitimately {@code K}-typed and {@link io.lettuce.core.protocol.CommandArgs#addKeys} must route through + * {@code encodeKey} so the prefix matches what was applied at write-time. Acts as a positive control: if the key channel + * stops routing through the codec, this assertion stops finding the document. + */ + @Test + void inKeyMustBeRoutedThroughCodecOnHash() { + SearchArgs args = SearchArgs. builder().inKey(HASH_DOC_KEY).build(); + + SearchReply result = redis.ftSearch(HASH_INDEX, "search", args); + + assertThat(result.getCount()).as("INKEYS must route the document key through encodeKey to match the stored prefix") + .isEqualTo(1L); + } + + /** + * JSON variant of {@link #inKeyMustBeRoutedThroughCodecOnHash}. The JSON document is stored via {@code jsonSet} under a + * codec-encoded key too, so {@code INKEYS} must route through {@code encodeKey} to match it. + */ + @Test + void inKeyMustBeRoutedThroughCodecOnJson() { + SearchArgs args = SearchArgs. builder().inKey(JSON_DOC_KEY).build(); + + SearchReply result = redis.ftSearch(JSON_INDEX, "search", args); + + assertThat(result.getCount()).as("INKEYS must route the document key through encodeKey to match the stored prefix") + .isEqualTo(1L); + } + + /** + * {@code FT.CREATE ... PREFIX 1 tenant1:doc:} restricts the index to keys whose Redis key starts with the given prefix. + * Prefixes are sent as raw strings, so the caller supplies the physical prefix needed to match the stored HASH key + * {@code "tenant1:doc:1"}. If the prefix were routed through {@code encodeKey}, it would become + * {@code "tenant1:tenant1:doc:"} and the document would not be indexed. + */ + @Test + void prefixInCreateArgsMustBeSentRawForHashKeys() { + redis.flushall(); + + CreateArgs prefixCreate = CreateArgs.builder().on(CreateArgs.TargetType.HASH).withPrefix(CODEC_PREFIX + "doc:").build(); + FieldArgs titleField = TextFieldArgs.builder().name(encodedFieldRef("title")).build(); + redis.ftCreate("prefix-test-idx", prefixCreate, Arrays.asList(titleField)); + + Map doc = new HashMap<>(); + doc.put("title", "Redis search guide"); + redis.hmset("doc:1", doc); + + // Wait for indexing + try { + Thread.sleep(500); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + SearchReply result = redis.ftSearch("prefix-test-idx", "search"); + + assertThat(result.getCount()).as("PREFIX must be sent raw to match the stored HASH key without double-prefixing") + .isEqualTo(1L); + } + + /** + * JSON variant of {@link #prefixInCreateArgsMustBeSentRawForHashKeys}. {@code JSON.SET} stores the document under a + * codec-encoded key too, so the caller supplies the matching physical prefix. + */ + @Test + void prefixInCreateArgsMustBeSentRawForJsonKeys() { + redis.flushall(); + + CreateArgs prefixCreate = CreateArgs.builder().on(CreateArgs.TargetType.JSON).withPrefix(CODEC_PREFIX + "doc:").build(); + FieldArgs titleField = TextFieldArgs.builder().name("$.title").as(encodedFieldRef("title")).build(); + redis.ftCreate("prefix-test-json-idx", prefixCreate, Arrays.asList(titleField)); + + redis.jsonSet("doc:1", JsonPath.ROOT_PATH, redis.getJsonParser().createJsonValue("{\"title\":\"Redis search guide\"}")); + + // Wait for indexing + try { + Thread.sleep(500); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + SearchReply result = redis.ftSearch("prefix-test-json-idx", "search"); + + assertThat(result.getCount()).as("PREFIX must be sent raw to match the stored JSON key without double-prefixing") + .isEqualTo(1L); + } + + /** + * Minimal codec that prefixes every encoded key with a fixed prefix and strips that prefix on decode when present. Values + * are passed through unchanged. Modelled after a tenant-scoping key transformation that real applications use to partition + * a Redis database by tenant id. + */ + static class PrefixingStringCodec implements RedisCodec { + + private final String prefix; + + PrefixingStringCodec(String prefix) { + this.prefix = prefix; + } + + @Override + public String decodeKey(ByteBuffer bytes) { + String s = StandardCharsets.UTF_8.decode(bytes).toString(); + return s.startsWith(prefix) ? s.substring(prefix.length()) : s; + } + + @Override + public String decodeValue(ByteBuffer bytes) { + return StandardCharsets.UTF_8.decode(bytes).toString(); + } + + @Override + public ByteBuffer encodeKey(String key) { + return StandardCharsets.UTF_8.encode(prefix + key); + } + + @Override + public ByteBuffer encodeValue(String value) { + return StandardCharsets.UTF_8.encode(value); + } + + } + +} diff --git a/src/test/java/io/lettuce/core/search/RediSearchStructuredKeyCodecSafetyIntegrationTests.java b/src/test/java/io/lettuce/core/search/RediSearchStructuredKeyCodecSafetyIntegrationTests.java new file mode 100644 index 0000000000..0afc6cf344 --- /dev/null +++ b/src/test/java/io/lettuce/core/search/RediSearchStructuredKeyCodecSafetyIntegrationTests.java @@ -0,0 +1,371 @@ +/* + * Copyright 2026-present + * All rights reserved. + * + * Licensed under the MIT License. + */ + +package io.lettuce.core.search; + +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.codec.RedisCodec; +import io.lettuce.core.json.JsonPath; +import io.lettuce.core.search.arguments.AggregateArgs; +import io.lettuce.core.search.arguments.CreateArgs; +import io.lettuce.core.search.arguments.FieldArgs; +import io.lettuce.core.search.arguments.SearchArgs; +import io.lettuce.core.search.arguments.SortByArgs; +import io.lettuce.core.search.arguments.TagFieldArgs; +import io.lettuce.core.search.arguments.TextFieldArgs; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import static io.lettuce.TestTags.INTEGRATION_TEST; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Mirror of {@link RediSearchPrefixingStringCodecSafetyIntegrationTests} using a connection codec whose key type is a + * structured POJO instead of {@link String}. {@link FieldArgs} sends schema field names raw at {@code FT.CREATE}; the read-side + * identifiers ({@code INFIELDS}, {@code RETURN}, {@code SORTBY}, {@code LOAD}) are {@code K}-typed and routed through + * {@code codec.encodeKey} at {@code FT.SEARCH}/{@code FT.AGGREGATE} time. They are passed as "bare" {@link RedisKey} instances + * built via {@link #field(String)}; {@link RedisKeyCodec} encodes a bare key as its {@code id} bytes only (no tenant/entity + * prefix), so the read-side bytes match the raw create-side field name. If a read clause skips the codec (or mangles a bare + * key), the bytes diverge, the schema lookup fails and the assertions below break. Covers both {@code ON HASH} and + * {@code ON JSON} indexes: {@code HSET}/{@code HMSET} route hash field names and values through the connection's + * {@link RedisCodec}; {@code JSON.SET} sends its payload verbatim via {@link io.lettuce.core.protocol.CommandArgs#add(String)} + * (or {@code add(byte[])} for {@link io.lettuce.core.json.JsonValue}), bypassing the value codec entirely. The JSON schema uses + * raw JSONPath names (e.g. {@code $.title}) aliased back to plain field names so the same read-side identifiers exercise both + * indexes. + * + * @author Viktoriya Kutsarova + */ +@Tag(INTEGRATION_TEST) +public class RediSearchStructuredKeyCodecSafetyIntegrationTests { + + private static final String HASH_INDEX = "codec-safety-rk-idx"; + + private static final String JSON_INDEX = "codec-safety-rk-json-idx"; + + private static final String HASH_DOC_ID = "1"; + + private static final String JSON_DOC_ID = "json-1"; + + private static final String TENANT = "tenant1"; + + private static final String ENTITY = "doc"; + + private static final String BODY_TEXT = "A long introduction to Redis that mentions search only deep into the body text. " + + "Much preamble about indices and storage, only later do we finally cover search. " + + "More content about search follows with many examples. " + + "Finally more content after the search occurrences ends the description."; + + private static RedisClient client; + + private static StatefulRedisConnection connection; + + private static RedisCommands redis; + + public RediSearchStructuredKeyCodecSafetyIntegrationTests() { + RedisURI uri = RedisURI.Builder.redis("127.0.0.1").withPort(16379).build(); + client = RedisClient.create(uri); + connection = client.connect(new RedisKeyCodec()); + redis = connection.sync(); + } + + @BeforeEach + void prepare() { + redis.flushall(); + + CreateArgs hashCreate = CreateArgs.builder().on(CreateArgs.TargetType.HASH).build(); + FieldArgs hashTitle = TextFieldArgs.builder().name("title").sortable().build(); + FieldArgs hashBody = TextFieldArgs.builder().name("body").build(); + FieldArgs hashCategory = TagFieldArgs.builder().name("category").build(); + redis.ftCreate(HASH_INDEX, hashCreate, Arrays.asList(hashTitle, hashBody, hashCategory)); + + Map doc = new HashMap<>(); + doc.put(field("title"), "Redis search guide"); + doc.put(field("body"), BODY_TEXT); + doc.put(field("category"), "tutorial"); + redis.hmset(new RedisKey(TENANT, ENTITY, HASH_DOC_ID), doc); + + CreateArgs jsonCreate = CreateArgs.builder().on(CreateArgs.TargetType.JSON).build(); + FieldArgs jsonTitle = TextFieldArgs.builder().name("$.title").as("title").sortable().build(); + FieldArgs jsonBody = TextFieldArgs.builder().name("$.body").as("body").build(); + FieldArgs jsonCategory = TagFieldArgs.builder().name("$.category").as("category").build(); + redis.ftCreate(JSON_INDEX, jsonCreate, Arrays.asList(jsonTitle, jsonBody, jsonCategory)); + + String jsonDoc = "{\"title\":\"Redis search guide\",\"body\":\"" + BODY_TEXT + "\",\"category\":\"tutorial\"}"; + redis.jsonSet(field(JSON_DOC_ID), JsonPath.ROOT_PATH, jsonDoc); + } + + @AfterAll + static void teardown() { + if (connection != null) { + connection.close(); + } + if (client != null) { + client.shutdown(); + } + } + + /** + * Build a "bare" {@link RedisKey} that round-trips through {@link RedisKeyCodec} as its {@code id} bytes only (no + * tenant/entity prefix). Used for hash field names written via {@code hmset} so they retain their literal byte shape. + */ + private static RedisKey field(String name) { + return new RedisKey("", "", name); + } + + /** + * Sanity check. The query string is written raw via {@code args.add(query)} so this must work even when a structured-key + * codec is installed on the connection. + */ + @ParameterizedTest + @ValueSource(strings = { HASH_INDEX, JSON_INDEX }) + void baselineSearchWorksThroughCodec(String indexName) { + SearchReply result = redis.ftSearch(indexName, "search"); + assertThat(result.getCount()).isEqualTo(1L); + } + + /** + * {@code INFIELDS} names schema fields declared at {@code FT.CREATE} time — they must be written raw, regardless of the + * connection's key type. + */ + @ParameterizedTest + @ValueSource(strings = { HASH_INDEX, JSON_INDEX }) + void inFieldMustNotBeMangledByCodec(String indexName) { + SearchArgs args = SearchArgs. builder().inField("title").build(); + + SearchReply result = redis.ftSearch(indexName, "search", args); + + assertThat(result.getCount()).as("INFIELDS schema field must be sent raw, not codec-encoded").isEqualTo(1L); + } + + /** + * {@code RETURN} field names are schema identifiers and must appear verbatim in the result map. + */ + @ParameterizedTest + @ValueSource(strings = { HASH_INDEX, JSON_INDEX }) + void returnFieldMustNotBeMangledByCodec(String indexName) { + SearchArgs args = SearchArgs. builder().returnField("title").build(); + + SearchReply result = redis.ftSearch(indexName, "search", args); + + assertThat(result.getCount()).isEqualTo(1L); + assertThat(result.getResults().get(0).getFields()) + .as("RETURN schema field must appear verbatim as a key in the result map").containsKey("title"); + } + + /** + * {@code RETURN ... AS alias} — the alias is also a schema-level identifier and must not be codec-encoded. + */ + @ParameterizedTest + @ValueSource(strings = { HASH_INDEX, JSON_INDEX }) + void returnFieldAliasMustNotBeMangledByCodec(String indexName) { + SearchArgs args = SearchArgs. builder().returnField("title", "t").build(); + + SearchReply result = redis.ftSearch(indexName, "search", args); + + assertThat(result.getCount()).isEqualTo(1L); + assertThat(result.getResults().get(0).getFields()).as("RETURN alias must appear verbatim as a key in the result map") + .containsKey("t"); + } + + /** + * {@code SUMMARIZE FIELDS} names schema fields. When applied correctly Redis abbreviates the field content and terminates + * it with the configured separator (default {@code ...}). + */ + // HASH only: SUMMARIZE/HIGHLIGHT are not supported on JSON indexes (server rejects with SEARCH_QUERY_BAD). + @Test + void summarizeFieldMustNotBeMangledByCodec() { + SearchArgs args = SearchArgs. builder().summarizeField("body").build(); + + SearchReply result = redis.ftSearch(HASH_INDEX, "search", args); + + assertThat(result.getCount()).isEqualTo(1L); + String body = result.getResults().get(0).getFields().get("body").asString(); + assertThat(body).as("SUMMARIZE must be applied to the 'body' field").contains("..."); + } + + /** + * {@code HIGHLIGHT FIELDS} names schema fields. When applied, matching terms are wrapped with the configured tags. + */ + // HASH only: SUMMARIZE/HIGHLIGHT are not supported on JSON indexes (server rejects with SEARCH_QUERY_BAD). + @Test + void highlightFieldMustNotBeMangledByCodec() { + SearchArgs args = SearchArgs. builder().highlightField("body").highlightTags("", "").build(); + + SearchReply result = redis.ftSearch(HASH_INDEX, "search", args); + + assertThat(result.getCount()).isEqualTo(1L); + String body = result.getResults().get(0).getFields().get("body").asString(); + assertThat(body).as("HIGHLIGHT must wrap 'search' occurrences with the configured tags").contains("search"); + } + + /** + * {@code FT.AGGREGATE ... LOAD} names schema fields. Same protocol clause as {@code PostProcessingArgs.load} — must be sent + * verbatim. + */ + @ParameterizedTest + @ValueSource(strings = { HASH_INDEX, JSON_INDEX }) + void aggregateLoadFieldMustNotBeMangledByCodec(String indexName) { + AggregateArgs args = AggregateArgs.builder().load("title").build(); + + AggregationReply result = redis.ftAggregate(indexName, "*", args); + + assertThat(result.getReplies()).isNotEmpty(); + SearchReply reply = result.getReplies().get(0); + assertThat(reply.getResults()).isNotEmpty(); + assertThat(reply.getResults().get(0).getFields()).as("LOAD schema field must be fetched verbatim").containsKey("title"); + } + + /** + * {@code SORTBY} names a {@code SORTABLE} schema field. {@link SortByArgs} routes the attribute through {@code addKey}; a + * bare {@link RedisKey} round-trips as plain {@code "title"} bytes and matches the schema attribute on both target types. + */ + @ParameterizedTest + @ValueSource(strings = { HASH_INDEX, JSON_INDEX }) + void sortByMustNotBeMangledByCodec(String indexName) { + SearchArgs args = SearchArgs. builder().sortBy(SortByArgs.builder().attribute("title").build()) + .build(); + + SearchReply result = redis.ftSearch(indexName, "search", args); + + assertThat(result.getCount()).as("SORTBY schema field must be sent raw, not codec-encoded").isEqualTo(1L); + } + + /** + * {@code PARAMS} substitution names are referenced as {@code $name} from the verbatim query. The name is a {@link String} + * sent raw (not {@code K}-typed), so {@code $term} resolves on the server regardless of the connection codec. + */ + @ParameterizedTest + @ValueSource(strings = { HASH_INDEX, JSON_INDEX }) + void searchParamNameMustNotBeMangledByCodec(String indexName) { + SearchArgs args = SearchArgs. builder().param("term", "search").build(); + + SearchReply result = redis.ftSearch(indexName, "@body:$term", args); + + assertThat(result.getCount()).as("PARAMS substitution name must be sent raw so $term resolves on the server side") + .isEqualTo(1L); + } + + /** + * Aggregate variant of {@link #searchParamNameMustNotBeMangledByCodec}; the parameter name is a {@link String} sent raw. + */ + @ParameterizedTest + @ValueSource(strings = { HASH_INDEX, JSON_INDEX }) + void aggregateParamNameMustNotBeMangledByCodec(String indexName) { + AggregateArgs args = AggregateArgs.builder().param("term", "search").build(); + + AggregationReply result = redis.ftAggregate(indexName, "@body:$term", args); + + assertThat(result.getReplies()).isNotEmpty(); + SearchReply reply = result.getReplies().get(0); + assertThat(reply.getResults()).as("PARAMS substitution name must resolve on the server side").isNotEmpty(); + } + + /** + * {@code INKEYS} restricts the search to a list of actual document keys. Unlike the schema-identifier clauses above, this + * one is legitimately {@code K}-typed: the codec must encode the {@link RedisKey} to {@code "tenant1:doc:"} so the + * entry matches what was written by {@code HMSET}. Positive control for the codec routing on the legitimate key surface. + */ + @Test + void inKeyMustBeRoutedThroughCodec() { + SearchArgs args = SearchArgs. builder().inKey(new RedisKey(TENANT, ENTITY, HASH_DOC_ID)).build(); + + SearchReply result = redis.ftSearch(HASH_INDEX, "search", args); + + assertThat(result.getCount()).as("INKEYS must route the document key through encodeKey to match the stored key") + .isEqualTo(1L); + } + + /** + * Value object used as the key type by {@link RedisKeyCodec}. Carries a tenant, an entity type and an id so a single Redis + * database can be partitioned along more than one dimension. A "bare" key — empty {@code tenant} and {@code entity} — + * round-trips identically to its {@code id} bytes; this lets hash field names like {@code "title"} retain their literal + * shape and stay compatible with the FT.CREATE schema. + */ + static class RedisKey { + + private final String tenant; + + private final String entity; + + private final String id; + + RedisKey(String tenant, String entity, String id) { + this.tenant = tenant; + this.entity = entity; + this.id = id; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof RedisKey)) { + return false; + } + RedisKey k = (RedisKey) o; + return tenant.equals(k.tenant) && entity.equals(k.entity) && id.equals(k.id); + } + + @Override + public int hashCode() { + return Objects.hash(tenant, entity, id); + } + + @Override + public String toString() { + return tenant.isEmpty() && entity.isEmpty() ? id : tenant + ":" + entity + ":" + id; + } + + } + + /** + * Encodes a {@link RedisKey} as {@code tenant:entity:id} (UTF-8) and parses the same shape back on decode. Bare keys (empty + * tenant and entity) round-trip as plain id bytes so that hash field names and other identity-shaped keys are not disturbed + * by the tenant/entity prefix. + */ + static class RedisKeyCodec implements RedisCodec { + + @Override + public RedisKey decodeKey(ByteBuffer bytes) { + String s = StandardCharsets.UTF_8.decode(bytes).toString(); + String[] parts = s.split(":", 3); + if (parts.length < 3) { + return new RedisKey("", "", s); + } + return new RedisKey(parts[0], parts[1], parts[2]); + } + + @Override + public String decodeValue(ByteBuffer bytes) { + return StandardCharsets.UTF_8.decode(bytes).toString(); + } + + @Override + public ByteBuffer encodeKey(RedisKey key) { + return StandardCharsets.UTF_8.encode(key.toString()); + } + + @Override + public ByteBuffer encodeValue(String value) { + return StandardCharsets.UTF_8.encode(value); + } + + } + +} diff --git a/src/test/java/io/lettuce/core/search/RediSearchVectorIntegrationTests.java b/src/test/java/io/lettuce/core/search/RediSearchVectorIntegrationTests.java index cd13763e65..31f58bbc11 100644 --- a/src/test/java/io/lettuce/core/search/RediSearchVectorIntegrationTests.java +++ b/src/test/java/io/lettuce/core/search/RediSearchVectorIntegrationTests.java @@ -13,13 +13,11 @@ import io.lettuce.core.RedisCommandExecutionException; import io.lettuce.core.RedisURI; import io.lettuce.core.api.sync.RedisCommands; -import io.lettuce.core.codec.RedisCodec; import io.lettuce.core.search.arguments.AggregateArgs; import io.lettuce.core.search.arguments.CreateArgs; import io.lettuce.core.search.arguments.FieldArgs; import io.lettuce.core.search.arguments.NumericFieldArgs; import io.lettuce.core.search.arguments.SearchArgs; -import io.lettuce.core.search.arguments.SortByArgs; import io.lettuce.core.search.arguments.TagFieldArgs; import io.lettuce.core.search.SearchReply.SearchResult; import io.lettuce.core.search.arguments.TextFieldArgs; @@ -30,7 +28,6 @@ import io.lettuce.test.condition.RedisConditions; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -121,14 +118,12 @@ void testSvsVamanaBasicVectorSearch() { String indexName = "svs-vamana-basic-idx"; // Create SVS-VAMANA vector field - FieldArgs vectorField = VectorFieldArgs. builder().name("embedding").svsVamana() - .type(VectorFieldArgs.VectorType.FLOAT32).dimensions(4).distanceMetric(VectorFieldArgs.DistanceMetric.COSINE) - .build(); + FieldArgs vectorField = VectorFieldArgs.builder().name("embedding").svsVamana().type(VectorFieldArgs.VectorType.FLOAT32) + .dimensions(4).distanceMetric(VectorFieldArgs.DistanceMetric.COSINE).build(); - FieldArgs nameField = TextFieldArgs. builder().name("name").build(); + FieldArgs nameField = TextFieldArgs.builder().name("name").build(); - CreateArgs createArgs = CreateArgs. builder().withPrefix("svs:") - .on(CreateArgs.TargetType.HASH).build(); + CreateArgs createArgs = CreateArgs.builder().withPrefix("svs:").on(CreateArgs.TargetType.HASH).build(); // Create index String result = redis.ftCreate(indexName, createArgs, Arrays.asList(vectorField, nameField)); @@ -152,20 +147,18 @@ void testSvsVamanaBasicVectorSearch() { // Perform vector search using binary query vector ByteBuffer queryVector = floatArrayToByteBuffer(new float[] { 1.0f, 0.0f, 0.0f, 0.0f }); - ByteBuffer blobKey = ByteBuffer.wrap("query_vec".getBytes()); - SearchArgs searchArgs = SearchArgs. builder() - .param(blobKey, queryVector).build(); + String blobKey = "query_vec"; + SearchArgs searchArgs = SearchArgs. builder().param(blobKey, queryVector.array()).build(); - ByteBuffer queryString = ByteBuffer.wrap("*=>[KNN 2 @embedding $query_vec]".getBytes()); - SearchReply searchResult = redisBinary.ftSearch(indexName, queryString, searchArgs); + String queryString = "*=>[KNN 2 @embedding $query_vec]"; + SearchReply searchResult = redisBinary.ftSearch(indexName, queryString, searchArgs); // Verify results assertThat(searchResult.getCount()).isEqualTo(2); assertThat(searchResult.getResults()).hasSize(2); // First result should be doc1 (exact match) - ByteBuffer nameKey = ByteBuffer.wrap("name".getBytes()); - String firstName = new String(searchResult.getResults().get(0).getFields().get(nameKey).array()); + String firstName = searchResult.getResults().get(0).getFields().get("name").asString(); assertThat(firstName).isEqualTo("Document 1"); // Cleanup @@ -185,15 +178,13 @@ void testSvsVamanaWithAdvancedParameters() { String indexName = "svs-vamana-advanced-idx"; // Create SVS-VAMANA vector field with advanced parameters (no compression) - FieldArgs vectorField = VectorFieldArgs. builder().name("embedding").svsVamana() - .type(VectorFieldArgs.VectorType.FLOAT32).dimensions(8).distanceMetric(VectorFieldArgs.DistanceMetric.L2) - .attribute("CONSTRUCTION_WINDOW_SIZE", 128).attribute("GRAPH_MAX_DEGREE", 32) - .attribute("SEARCH_WINDOW_SIZE", 64).build(); + FieldArgs vectorField = VectorFieldArgs.builder().name("embedding").svsVamana().type(VectorFieldArgs.VectorType.FLOAT32) + .dimensions(8).distanceMetric(VectorFieldArgs.DistanceMetric.L2).attribute("CONSTRUCTION_WINDOW_SIZE", 128) + .attribute("GRAPH_MAX_DEGREE", 32).attribute("SEARCH_WINDOW_SIZE", 64).build(); - FieldArgs categoryField = TagFieldArgs. builder().name("category").build(); + FieldArgs categoryField = TagFieldArgs.builder().name("category").build(); - CreateArgs createArgs = CreateArgs. builder().withPrefix("advanced:") - .on(CreateArgs.TargetType.HASH).build(); + CreateArgs createArgs = CreateArgs.builder().withPrefix("advanced:").on(CreateArgs.TargetType.HASH).build(); // Create index String result = redis.ftCreate(indexName, createArgs, Arrays.asList(vectorField, categoryField)); @@ -220,21 +211,19 @@ void testSvsVamanaWithAdvancedParameters() { // Perform vector search with category filter ByteBuffer queryVector = floatArrayToByteBuffer(new float[] { 1.0f, 0.5f, 0.2f, 0.8f, 0.3f, 0.9f, 0.1f, 0.6f }); - ByteBuffer blobKey = ByteBuffer.wrap("query_vec".getBytes()); - SearchArgs searchArgs = SearchArgs. builder() - .param(blobKey, queryVector).build(); + String blobKey = "query_vec"; + SearchArgs searchArgs = SearchArgs. builder().param(blobKey, queryVector.array()).build(); - ByteBuffer queryString = ByteBuffer.wrap("(@category:{electronics})=>[KNN 2 @embedding $query_vec]".getBytes()); - SearchReply searchResult = redisBinary.ftSearch(indexName, queryString, searchArgs); + String queryString = "(@category:{electronics})=>[KNN 2 @embedding $query_vec]"; + SearchReply searchResult = redisBinary.ftSearch(indexName, queryString, searchArgs); // Verify results - should find electronics products only assertThat(searchResult.getCount()).isEqualTo(2); assertThat(searchResult.getResults()).hasSize(2); // All results should be electronics - ByteBuffer categoryKey = ByteBuffer.wrap("category".getBytes()); - for (SearchReply.SearchResult searchResultItem : searchResult.getResults()) { - String category = new String(searchResultItem.getFields().get(categoryKey).array()); + for (SearchReply.SearchResult searchResultItem : searchResult.getResults()) { + String category = searchResultItem.getFields().get("category").asString(); assertThat(category).isEqualTo("electronics"); } @@ -255,15 +244,14 @@ void testSvsVamanaWithAggregation() { String indexName = "svs-vamana-agg-idx"; // Create SVS-VAMANA vector field optimized for aggregation (no compression) - FieldArgs vectorField = VectorFieldArgs. builder().name("embedding").svsVamana() - .type(VectorFieldArgs.VectorType.FLOAT32).dimensions(4).distanceMetric(VectorFieldArgs.DistanceMetric.COSINE) - .attribute("SEARCH_WINDOW_SIZE", 64).build(); + FieldArgs vectorField = VectorFieldArgs.builder().name("embedding").svsVamana().type(VectorFieldArgs.VectorType.FLOAT32) + .dimensions(4).distanceMetric(VectorFieldArgs.DistanceMetric.COSINE).attribute("SEARCH_WINDOW_SIZE", 64) + .build(); - FieldArgs categoryField = TagFieldArgs. builder().name("category").sortable().build(); - FieldArgs priceField = NumericFieldArgs. builder().name("price").sortable().build(); + FieldArgs categoryField = TagFieldArgs.builder().name("category").sortable().build(); + FieldArgs priceField = NumericFieldArgs.builder().name("price").sortable().build(); - CreateArgs createArgs = CreateArgs. builder().withPrefix("agg:") - .on(CreateArgs.TargetType.HASH).build(); + CreateArgs createArgs = CreateArgs.builder().withPrefix("agg:").on(CreateArgs.TargetType.HASH).build(); // Create index String result = redis.ftCreate(indexName, createArgs, Arrays.asList(vectorField, categoryField, priceField)); @@ -284,11 +272,10 @@ void testSvsVamanaWithAggregation() { } // Perform aggregation: group by category and calculate average price - AggregationReply aggregationResult = redis.ftAggregate(indexName, "*", - AggregateArgs. builder() - .groupBy(AggregateArgs.GroupBy. of("category") - .reduce(AggregateArgs.Reducer. count().as("count")) - .reduce(AggregateArgs.Reducer. avg("@price").as("avg_price"))) + AggregationReply aggregationResult = redis.ftAggregate(indexName, "*", + AggregateArgs.builder() + .groupBy(AggregateArgs.GroupBy.of("category").reduce(AggregateArgs.Reducer.count().as("count")) + .reduce(AggregateArgs.Reducer.avg("@price").as("avg_price"))) .sortBy(AggregateArgs.SortBy.of("avg_price", AggregateArgs.SortDirection.DESC)).build()); // Verify aggregation results @@ -296,14 +283,14 @@ AggregateArgs. builder() assertThat(aggregationResult.getAggregationGroups()).isEqualTo(1); // 1 aggregation operation assertThat(aggregationResult.getReplies()).hasSize(1); // One reply containing all groups - SearchReply reply = aggregationResult.getReplies().get(0); + SearchReply reply = aggregationResult.getReplies().get(0); assertThat(reply.getResults()).hasSize(2); // 2 category groups // Verify we have both categories represented - List> aggregationResults = reply.getResults(); + List> aggregationResults = reply.getResults(); Set foundCategories = new HashSet<>(); - for (SearchResult groupResult : aggregationResults) { - foundCategories.add(groupResult.getFields().get("category")); + for (SearchResult groupResult : aggregationResults) { + foundCategories.add(groupResult.getFields().get("category").asString()); } assertThat(foundCategories).containsExactlyInAnyOrder("electronics", "books"); @@ -327,15 +314,15 @@ void testSvsVamanaWithDifferentDistanceMetrics() { for (String metric : metrics) { String indexName = "svs-vamana-" + metric.toLowerCase() + "-idx"; - FieldArgs vectorField = VectorFieldArgs. builder().name("embedding").svsVamana() + FieldArgs vectorField = VectorFieldArgs.builder().name("embedding").svsVamana() .type(VectorFieldArgs.VectorType.FLOAT32).dimensions(3) .distanceMetric(VectorFieldArgs.DistanceMetric.valueOf(metric)).attribute("CONSTRUCTION_WINDOW_SIZE", 64) .build(); - FieldArgs nameField = TextFieldArgs. builder().name("name").build(); + FieldArgs nameField = TextFieldArgs.builder().name("name").build(); - CreateArgs createArgs = CreateArgs. builder().withPrefix(metric.toLowerCase() + ":") - .on(CreateArgs.TargetType.HASH).build(); + CreateArgs createArgs = CreateArgs.builder().withPrefix(metric.toLowerCase() + ":").on(CreateArgs.TargetType.HASH) + .build(); // Create index String result = redis.ftCreate(indexName, createArgs, Arrays.asList(vectorField, nameField)); @@ -359,12 +346,11 @@ void testSvsVamanaWithDifferentDistanceMetrics() { // Query with vector similar to vec1 ByteBuffer queryVector = floatArrayToByteBuffer(new float[] { 0.9f, 0.1f, 0.0f }); - ByteBuffer blobKey = ByteBuffer.wrap("query_vec".getBytes()); - SearchArgs searchArgs = SearchArgs. builder() - .param(blobKey, queryVector).build(); + String blobKey = "query_vec"; + SearchArgs searchArgs = SearchArgs. builder().param(blobKey, queryVector.array()).build(); - ByteBuffer queryString = ByteBuffer.wrap("*=>[KNN 3 @embedding $query_vec]".getBytes()); - SearchReply searchResult = redisBinary.ftSearch(indexName, queryString, searchArgs); + String queryString = "*=>[KNN 3 @embedding $query_vec]"; + SearchReply searchResult = redisBinary.ftSearch(indexName, queryString, searchArgs); // Verify we get results assertThat(searchResult.getCount()).isEqualTo(3); @@ -372,11 +358,10 @@ void testSvsVamanaWithDifferentDistanceMetrics() { // For all metrics, the most similar should be found // The exact ranking may vary by metric, but we should get valid results - List> results = searchResult.getResults(); - ByteBuffer nameKey = ByteBuffer.wrap("name".getBytes()); - assertThat(results.get(0).getFields().get(nameKey)).isNotNull(); - assertThat(results.get(1).getFields().get(nameKey)).isNotNull(); - assertThat(results.get(2).getFields().get(nameKey)).isNotNull(); + List> results = searchResult.getResults(); + assertThat(results.get(0).getFields().get("name")).isNotNull(); + assertThat(results.get(1).getFields().get("name")).isNotNull(); + assertThat(results.get(2).getFields().get("name")).isNotNull(); // Cleanup redis.ftDropindex(indexName); @@ -423,14 +408,13 @@ void testFlatVectorIndexWithKnnSearch() { // Create FLAT vector index based on Redis documentation: // FT.CREATE documents ON HASH PREFIX 1 docs: SCHEMA doc_embedding VECTOR FLAT 6 TYPE FLOAT32 DIM 1536 DISTANCE_METRIC // COSINE - FieldArgs vectorField = VectorFieldArgs. builder().name("doc_embedding").flat() - .type(VectorFieldArgs.VectorType.FLOAT32).dimensions(4) // Using smaller dimensions for testing + FieldArgs vectorField = VectorFieldArgs.builder().name("doc_embedding").flat().type(VectorFieldArgs.VectorType.FLOAT32) + .dimensions(4) // Using smaller dimensions for testing .distanceMetric(VectorFieldArgs.DistanceMetric.COSINE).build(); - FieldArgs titleField = TextFieldArgs. builder().name("title").build(); - FieldArgs categoryField = TagFieldArgs. builder().name("category").build(); + FieldArgs titleField = TextFieldArgs.builder().name("title").build(); + FieldArgs categoryField = TagFieldArgs.builder().name("category").build(); - CreateArgs createArgs = CreateArgs. builder().withPrefix(DOCS_PREFIX) - .on(CreateArgs.TargetType.HASH).build(); + CreateArgs createArgs = CreateArgs.builder().withPrefix(DOCS_PREFIX).on(CreateArgs.TargetType.HASH).build(); String result = redis.ftCreate(DOCUMENTS_INDEX, createArgs, Arrays.asList(vectorField, titleField, categoryField)); assertThat(result).isEqualTo("OK"); @@ -465,31 +449,34 @@ void testFlatVectorIndexWithKnnSearch() { ByteBuffer queryVectorBuffer = floatArrayToByteBuffer(queryVector); // Use binary connection for search to handle binary vector data properly - ByteBuffer blobKey = ByteBuffer.wrap("BLOB".getBytes(StandardCharsets.UTF_8)); - SearchArgs knnArgs = SearchArgs. builder() - .param(blobKey, queryVectorBuffer).limit(0, 2).build(); + String blobKey = "BLOB"; + SearchArgs knnArgs = SearchArgs. builder().param(blobKey, queryVectorBuffer.array()).limit(0, 2) + .build(); - ByteBuffer queryString = ByteBuffer - .wrap("*=>[KNN 2 @doc_embedding $BLOB AS vector_score]".getBytes(StandardCharsets.UTF_8)); + String queryString = "*=>[KNN 2 @doc_embedding $BLOB AS vector_score]"; - SearchReply results = redisBinary.ftSearch(DOCUMENTS_INDEX, queryString, knnArgs); + SearchReply results = redisBinary.ftSearch(DOCUMENTS_INDEX, queryString, knnArgs); assertThat(results.getCount()).isEqualTo(2); assertThat(results.getResults()).hasSize(2); // The results should be sorted by vector similarity (closest first) // vector1 and vector2 should be more similar to queryVector than vector3 - SearchReply.SearchResult firstResult = results.getResults().get(0); - SearchReply.SearchResult secondResult = results.getResults().get(1); + SearchReply.SearchResult firstResult = results.getResults().get(0); + SearchReply.SearchResult secondResult = results.getResults().get(1); - // Convert ByteBuffer results back to strings for assertions - ByteBuffer titleFieldKey = ByteBuffer.wrap("title".getBytes(StandardCharsets.UTF_8)); - String firstTitle = new String(firstResult.getFields().get(titleFieldKey).array(), StandardCharsets.UTF_8); - String secondTitle = new String(secondResult.getFields().get(titleFieldKey).array(), StandardCharsets.UTF_8); + String firstTitle = firstResult.getFields().get("title").asString(); + String secondTitle = secondResult.getFields().get("title").asString(); assertThat(firstTitle).isIn("Redis Vector Search Tutorial", "Advanced Vector Techniques"); assertThat(secondTitle).isIn("Redis Vector Search Tutorial", "Advanced Vector Techniques"); + // the binary vector field comes back byte-exact via FieldValue.asBytes(); the two closest documents are vector1/vector2 + assertThat(firstResult.getFields().get("doc_embedding").asBytes()).isIn(floatArrayToByteBuffer(vector1).array(), + floatArrayToByteBuffer(vector2).array()); + assertThat(secondResult.getFields().get("doc_embedding").asBytes()).isIn(floatArrayToByteBuffer(vector1).array(), + floatArrayToByteBuffer(vector2).array()); + // Cleanup redis.ftDropindex(DOCUMENTS_INDEX); } @@ -500,17 +487,16 @@ void testFlatVectorIndexWithKnnSearch() { @Test void testHnswVectorIndexWithFiltering() { // Create HNSW vector index with custom parameters - FieldArgs vectorField = VectorFieldArgs. builder().name("movie_embedding").hnsw() + FieldArgs vectorField = VectorFieldArgs.builder().name("movie_embedding").hnsw() .type(VectorFieldArgs.VectorType.FLOAT32).dimensions(3).distanceMetric(VectorFieldArgs.DistanceMetric.L2) .attribute("M", 40).attribute("EF_CONSTRUCTION", 250).build(); - FieldArgs titleField = TextFieldArgs. builder().name("title").build(); - FieldArgs genreField = TagFieldArgs. builder().name("genre").build(); - FieldArgs yearField = NumericFieldArgs. builder().name("year").sortable().build(); - FieldArgs ratingField = NumericFieldArgs. builder().name("rating").sortable().build(); + FieldArgs titleField = TextFieldArgs.builder().name("title").build(); + FieldArgs genreField = TagFieldArgs.builder().name("genre").build(); + FieldArgs yearField = NumericFieldArgs.builder().name("year").sortable().build(); + FieldArgs ratingField = NumericFieldArgs.builder().name("rating").sortable().build(); - CreateArgs createArgs = CreateArgs. builder().withPrefix(MOVIE_PREFIX) - .on(CreateArgs.TargetType.HASH).build(); + CreateArgs createArgs = CreateArgs.builder().withPrefix(MOVIE_PREFIX).on(CreateArgs.TargetType.HASH).build(); redis.ftCreate(MOVIES_INDEX, createArgs, Arrays.asList(vectorField, titleField, genreField, yearField, ratingField)); @@ -559,41 +545,37 @@ void testHnswVectorIndexWithFiltering() { float[] queryVector = { 0.8f, 0.6f, 0.2f }; // Similar to action-drama ByteBuffer queryVectorBuffer = floatArrayToByteBuffer(queryVector); - ByteBuffer blobKey = ByteBuffer.wrap("BLOB".getBytes(StandardCharsets.UTF_8)); - SearchArgs filterArgs = SearchArgs. builder() - .param(blobKey, queryVectorBuffer).limit(0, 10).build(); + String blobKey = "BLOB"; + SearchArgs filterArgs = SearchArgs. builder().param(blobKey, queryVectorBuffer.array()) + .limit(0, 10).build(); - ByteBuffer queryString = ByteBuffer - .wrap("(@genre:{action})=>[KNN 3 @movie_embedding $BLOB AS movie_distance]".getBytes(StandardCharsets.UTF_8)); + String queryString = "(@genre:{action})=>[KNN 3 @movie_embedding $BLOB AS movie_distance]"; // Search for action movies with vector similarity - SearchReply results = redisBinary.ftSearch(MOVIES_INDEX, queryString, filterArgs); + SearchReply results = redisBinary.ftSearch(MOVIES_INDEX, queryString, filterArgs); assertThat(results.getCount()).isEqualTo(2); // The Matrix and Heat have action genre - ByteBuffer genreFieldKey = ByteBuffer.wrap("genre".getBytes(StandardCharsets.UTF_8)); - for (SearchReply.SearchResult result : results.getResults()) { - String genre = new String(result.getFields().get(genreFieldKey).array(), StandardCharsets.UTF_8); + for (SearchReply.SearchResult result : results.getResults()) { + String genre = result.getFields().get("genre").asString(); assertThat(genre).contains("action"); } // Test 2: KNN search with year range filter - SearchArgs yearFilterArgs = SearchArgs. builder() - .param(blobKey, queryVectorBuffer).limit(0, 10).build(); + SearchArgs yearFilterArgs = SearchArgs. builder().param(blobKey, queryVectorBuffer.array()) + .limit(0, 10).build(); - ByteBuffer yearQueryString = ByteBuffer - .wrap("(@year:[1990 2000])=>[KNN 2 @movie_embedding $BLOB AS movie_distance]".getBytes(StandardCharsets.UTF_8)); + String yearQueryString = "(@year:[1990 2000])=>[KNN 2 @movie_embedding $BLOB AS movie_distance]"; results = redisBinary.ftSearch(MOVIES_INDEX, yearQueryString, yearFilterArgs); assertThat(results.getCount()).isEqualTo(2); // The Matrix (1999) and Heat (1995) // Test 3: KNN search with runtime EF parameter - ByteBuffer efKey = ByteBuffer.wrap("EF".getBytes(StandardCharsets.UTF_8)); + String efKey = "EF"; ByteBuffer efValue = ByteBuffer.wrap("150".getBytes(StandardCharsets.UTF_8)); - SearchArgs efArgs = SearchArgs. builder() - .param(blobKey, queryVectorBuffer).param(efKey, efValue).limit(0, 10).build(); + SearchArgs efArgs = SearchArgs. builder().param(blobKey, queryVectorBuffer.array()) + .param(efKey, efValue.array()).limit(0, 10).build(); - ByteBuffer efQueryString = ByteBuffer - .wrap("*=>[KNN 3 @movie_embedding $BLOB EF_RUNTIME $EF AS movie_distance]".getBytes(StandardCharsets.UTF_8)); + String efQueryString = "*=>[KNN 3 @movie_embedding $BLOB EF_RUNTIME $EF AS movie_distance]"; results = redisBinary.ftSearch(MOVIES_INDEX, efQueryString, efArgs); assertThat(results.getCount()).isEqualTo(3); @@ -609,16 +591,15 @@ void testHnswVectorIndexWithFiltering() { @Test void testVectorRangeQueries() { // Create vector index for range query testing - FieldArgs vectorField = VectorFieldArgs. builder().name("description_vector").flat() + FieldArgs vectorField = VectorFieldArgs.builder().name("description_vector").flat() .type(VectorFieldArgs.VectorType.FLOAT32).dimensions(3).distanceMetric(VectorFieldArgs.DistanceMetric.COSINE) .build(); - FieldArgs nameField = TextFieldArgs. builder().name("name").build(); - FieldArgs typeField = TagFieldArgs. builder().name("type").build(); - FieldArgs priceField = NumericFieldArgs. builder().name("price").sortable().build(); + FieldArgs nameField = TextFieldArgs.builder().name("name").build(); + FieldArgs typeField = TagFieldArgs.builder().name("type").build(); + FieldArgs priceField = NumericFieldArgs.builder().name("price").sortable().build(); - CreateArgs createArgs = CreateArgs. builder().withPrefix(PRODUCT_PREFIX) - .on(CreateArgs.TargetType.HASH).build(); + CreateArgs createArgs = CreateArgs.builder().withPrefix(PRODUCT_PREFIX).on(CreateArgs.TargetType.HASH).build(); redis.ftCreate(PRODUCTS_INDEX, createArgs, Arrays.asList(vectorField, nameField, typeField, priceField)); @@ -661,39 +642,34 @@ void testVectorRangeQueries() { float[] queryVector = { 0.9f, 0.1f, 0.0f }; // Close to electronics ByteBuffer queryVectorBuffer = floatArrayToByteBuffer(queryVector); - ByteBuffer blobKey = ByteBuffer.wrap("BLOB".getBytes(StandardCharsets.UTF_8)); - SearchArgs rangeArgs = SearchArgs. builder() - .param(blobKey, queryVectorBuffer).limit(0, 100).build(); + String blobKey = "BLOB"; + SearchArgs rangeArgs = SearchArgs. builder().param(blobKey, queryVectorBuffer.array()) + .limit(0, 100).build(); - ByteBuffer queryString = ByteBuffer - .wrap("@description_vector:[VECTOR_RANGE 0.5 $BLOB]".getBytes(StandardCharsets.UTF_8)); - SearchReply results = redisBinary.ftSearch(PRODUCTS_INDEX, queryString, rangeArgs); + String queryString = "@description_vector:[VECTOR_RANGE 0.5 $BLOB]"; + SearchReply results = redisBinary.ftSearch(PRODUCTS_INDEX, queryString, rangeArgs); // Should find electronics products and smart watch (mixed vector) assertThat(results.getCount()).isGreaterThanOrEqualTo(1); - ByteBuffer typeKey = ByteBuffer.wrap("type".getBytes(StandardCharsets.UTF_8)); - for (SearchReply.SearchResult result : results.getResults()) { - String productType = new String(result.getFields().get(typeKey).array(), StandardCharsets.UTF_8); + for (SearchReply.SearchResult result : results.getResults()) { + String productType = result.getFields().get("type").asString(); assertThat(productType).isIn("electronics"); // Electronics should be within range } // Test 2: Vector range query with distance field and sorting - SearchArgs sortedRangeArgs = SearchArgs. builder() - .param(blobKey, queryVectorBuffer).limit(0, 100).build(); + SearchArgs sortedRangeArgs = SearchArgs. builder().param(blobKey, queryVectorBuffer.array()) + .limit(0, 100).build(); - ByteBuffer sortedQueryString = ByteBuffer - .wrap("@description_vector:[VECTOR_RANGE 1.0 $BLOB]=>{$YIELD_DISTANCE_AS: vector_distance}" - .getBytes(StandardCharsets.UTF_8)); + String sortedQueryString = "@description_vector:[VECTOR_RANGE 1.0 $BLOB]=>{$YIELD_DISTANCE_AS: vector_distance}"; results = redisBinary.ftSearch(PRODUCTS_INDEX, sortedQueryString, sortedRangeArgs); assertThat(results.getCount()).isGreaterThanOrEqualTo(2); // Test 3: Combined filter - vector range + price filter - SearchArgs combinedArgs = SearchArgs. builder() - .param(blobKey, queryVectorBuffer).limit(0, 100).build(); + SearchArgs combinedArgs = SearchArgs. builder().param(blobKey, queryVectorBuffer.array()) + .limit(0, 100).build(); - ByteBuffer combinedQueryString = ByteBuffer - .wrap("(@price:[200 1000]) | @description_vector:[VECTOR_RANGE 0.8 $BLOB]".getBytes(StandardCharsets.UTF_8)); + String combinedQueryString = "(@price:[200 1000]) | @description_vector:[VECTOR_RANGE 0.8 $BLOB]"; results = redisBinary.ftSearch(PRODUCTS_INDEX, combinedQueryString, combinedArgs); assertThat(results.getCount()).isGreaterThanOrEqualTo(1); @@ -713,14 +689,12 @@ void testDistanceMetricsAndVectorTypes() { for (String metric : metrics) { String indexName = "test-" + metric.toLowerCase() + "-idx"; - FieldArgs vectorField = VectorFieldArgs. builder().name("embedding").flat() - .type(VectorFieldArgs.VectorType.FLOAT32).dimensions(2) - .distanceMetric(VectorFieldArgs.DistanceMetric.valueOf(metric)).build(); + FieldArgs vectorField = VectorFieldArgs.builder().name("embedding").flat().type(VectorFieldArgs.VectorType.FLOAT32) + .dimensions(2).distanceMetric(VectorFieldArgs.DistanceMetric.valueOf(metric)).build(); - FieldArgs nameField = TextFieldArgs. builder().name("name").build(); + FieldArgs nameField = TextFieldArgs.builder().name("name").build(); - CreateArgs createArgs = CreateArgs. builder().withPrefix("test:") - .on(CreateArgs.TargetType.HASH).build(); + CreateArgs createArgs = CreateArgs.builder().withPrefix("test:").on(CreateArgs.TargetType.HASH).build(); redis.ftCreate(indexName, createArgs, Arrays.asList(vectorField, nameField)); @@ -742,13 +716,12 @@ void testDistanceMetricsAndVectorTypes() { float[] queryVector = { 0.7f, 0.3f }; ByteBuffer queryVectorBuffer = floatArrayToByteBuffer(queryVector); - ByteBuffer blobKey = ByteBuffer.wrap("BLOB".getBytes(StandardCharsets.UTF_8)); - SearchArgs searchArgs = SearchArgs. builder() - .param(blobKey, queryVectorBuffer).limit(0, 2).build(); + String blobKey = "BLOB"; + SearchArgs searchArgs = SearchArgs. builder().param(blobKey, queryVectorBuffer.array()) + .limit(0, 2).build(); - ByteBuffer queryString = ByteBuffer - .wrap("*=>[KNN 2 @embedding $BLOB AS distance]".getBytes(StandardCharsets.UTF_8)); - SearchReply results = redisBinary.ftSearch(indexName, queryString, searchArgs); + String queryString = "*=>[KNN 2 @embedding $BLOB AS distance]"; + SearchReply results = redisBinary.ftSearch(indexName, queryString, searchArgs); assertThat(results.getCount()).isEqualTo(2); assertThat(results.getResults()).hasSize(2); @@ -765,15 +738,14 @@ void testDistanceMetricsAndVectorTypes() { @Test void testJsonVectorStorage() { // Create vector index for JSON documents with field aliases (key for proper search syntax) - FieldArgs vectorField = VectorFieldArgs. builder().name("$.vector").as("vector").flat() + FieldArgs vectorField = VectorFieldArgs.builder().name("$.vector").as("vector").flat() .type(VectorFieldArgs.VectorType.FLOAT32).dimensions(3).distanceMetric(VectorFieldArgs.DistanceMetric.COSINE) .build(); - FieldArgs titleField = TextFieldArgs. builder().name("$.title").as("title").build(); - FieldArgs categoryField = TagFieldArgs. builder().name("$.category").as("category").build(); + FieldArgs titleField = TextFieldArgs.builder().name("$.title").as("title").build(); + FieldArgs categoryField = TagFieldArgs.builder().name("$.category").as("category").build(); - CreateArgs createArgs = CreateArgs. builder().withPrefix("json:") - .on(CreateArgs.TargetType.JSON).build(); + CreateArgs createArgs = CreateArgs.builder().withPrefix("json:").on(CreateArgs.TargetType.JSON).build(); redis.ftCreate("json-vector-idx", createArgs, Arrays.asList(vectorField, titleField, categoryField)); @@ -798,22 +770,21 @@ void testJsonVectorStorage() { ByteBuffer queryVectorBuffer = floatArrayToByteBuffer(queryVector); // Test 1: KNN search with ADHOC_BF hybrid policy using binary codec - ByteBuffer blobKey = ByteBuffer.wrap("BLOB".getBytes(StandardCharsets.UTF_8)); - SearchArgs adhocArgs = SearchArgs. builder() - .param(blobKey, queryVectorBuffer).limit(0, 3).build(); + String blobKey = "BLOB"; + SearchArgs adhocArgs = SearchArgs. builder().param(blobKey, queryVectorBuffer.array()) + .limit(0, 3).build(); - ByteBuffer queryString = ByteBuffer.wrap("*=>[KNN 3 @vector $BLOB]".getBytes(StandardCharsets.UTF_8)); - SearchReply results = redisBinary.ftSearch("json-vector-idx", queryString, adhocArgs); + String queryString = "*=>[KNN 3 @vector $BLOB]"; + SearchReply results = redisBinary.ftSearch("json-vector-idx", queryString, adhocArgs); assertThat(results.getCount()).isEqualTo(3); assertThat(results.getResults()).hasSize(3); // Test filtering with JSON vectors - SearchArgs filterArgs = SearchArgs. builder() - .param(blobKey, queryVectorBuffer).limit(0, 10).build(); + SearchArgs filterArgs = SearchArgs. builder().param(blobKey, queryVectorBuffer.array()) + .limit(0, 10).build(); - ByteBuffer filterQueryString = ByteBuffer - .wrap("(@category:{tech})=>[KNN 2 @vector $BLOB]".getBytes(StandardCharsets.UTF_8)); + String filterQueryString = "(@category:{tech})=>[KNN 2 @vector $BLOB]"; results = redisBinary.ftSearch("json-vector-idx", filterQueryString, filterArgs); assertThat(results.getCount()).isEqualTo(2); // Only tech category documents @@ -830,16 +801,15 @@ void testJsonVectorStorage() { @Test void testAdvancedVectorSearchFeatures() { // Create HNSW index for advanced testing - VectorFieldArgs vectorField = VectorFieldArgs. builder().name("content_vector").hnsw() + VectorFieldArgs vectorField = VectorFieldArgs.builder().name("content_vector").hnsw() .type(VectorFieldArgs.VectorType.FLOAT32).dimensions(4).distanceMetric(VectorFieldArgs.DistanceMetric.COSINE) .attribute("M", 16).attribute("EF_CONSTRUCTION", 200).build(); - FieldArgs titleField = TextFieldArgs. builder().name("title").build(); - FieldArgs statusField = TagFieldArgs. builder().name("status").build(); - FieldArgs priorityField = NumericFieldArgs. builder().name("priority").sortable().build(); + FieldArgs titleField = TextFieldArgs.builder().name("title").build(); + FieldArgs statusField = TagFieldArgs.builder().name("status").build(); + FieldArgs priorityField = NumericFieldArgs.builder().name("priority").sortable().build(); - CreateArgs createArgs = CreateArgs. builder().withPrefix("task:") - .on(CreateArgs.TargetType.HASH).build(); + CreateArgs createArgs = CreateArgs.builder().withPrefix("task:").on(CreateArgs.TargetType.HASH).build(); redis.ftCreate("tasks-idx", createArgs, Arrays.asList(vectorField, titleField, statusField, priorityField)); @@ -859,68 +829,58 @@ void testAdvancedVectorSearchFeatures() { ByteBuffer queryVectorBuffer = floatArrayToByteBuffer(queryVector); // Test 1: KNN search with ADHOC_BF hybrid policy using binary codec - ByteBuffer blobKey = ByteBuffer.wrap("BLOB".getBytes(StandardCharsets.UTF_8)); - SearchArgs adhocArgs = SearchArgs. builder() - .param(blobKey, queryVectorBuffer).limit(0, 5).build(); + String blobKey = "BLOB"; + SearchArgs adhocArgs = SearchArgs. builder().param(blobKey, queryVectorBuffer.array()) + .limit(0, 5).build(); - ByteBuffer queryString = ByteBuffer - .wrap("(@status:{active})=>[KNN 5 @content_vector $BLOB HYBRID_POLICY ADHOC_BF AS task_score]" - .getBytes(StandardCharsets.UTF_8)); - SearchReply results = redisBinary.ftSearch("tasks-idx", queryString, adhocArgs); + String queryString = "(@status:{active})=>[KNN 5 @content_vector $BLOB HYBRID_POLICY ADHOC_BF AS task_score]"; + SearchReply results = redisBinary.ftSearch("tasks-idx", queryString, adhocArgs); assertThat(results.getCount()).isGreaterThanOrEqualTo(1); // Test 2: KNN search with BATCHES hybrid policy and custom batch size - ByteBuffer batchSizeKey = ByteBuffer.wrap("BATCH_SIZE".getBytes(StandardCharsets.UTF_8)); + String batchSizeKey = "BATCH_SIZE"; ByteBuffer batchSizeValue = ByteBuffer.wrap("3".getBytes(StandardCharsets.UTF_8)); - SearchArgs batchArgs = SearchArgs. builder() - .param(blobKey, queryVectorBuffer).param(batchSizeKey, batchSizeValue).limit(0, 5).build(); + SearchArgs batchArgs = SearchArgs. builder().param(blobKey, queryVectorBuffer.array()) + .param(batchSizeKey, batchSizeValue.array()).limit(0, 5).build(); - ByteBuffer batchQueryString = ByteBuffer.wrap( - "(@status:{active})=>[KNN 5 @content_vector $BLOB HYBRID_POLICY BATCHES BATCH_SIZE $BATCH_SIZE AS task_score]" - .getBytes(StandardCharsets.UTF_8)); + String batchQueryString = "(@status:{active})=>[KNN 5 @content_vector $BLOB HYBRID_POLICY BATCHES BATCH_SIZE $BATCH_SIZE AS task_score]"; results = redisBinary.ftSearch("tasks-idx", batchQueryString, batchArgs); assertThat(results.getCount()).isGreaterThanOrEqualTo(1); // Test 3: Vector search with custom EF_RUNTIME parameter - ByteBuffer efKey = ByteBuffer.wrap("EF".getBytes(StandardCharsets.UTF_8)); + String efKey = "EF"; ByteBuffer efValue = ByteBuffer.wrap("50".getBytes(StandardCharsets.UTF_8)); - SearchArgs efArgs = SearchArgs. builder() - .param(blobKey, queryVectorBuffer).param(efKey, efValue).limit(0, 3).build(); + SearchArgs efArgs = SearchArgs. builder().param(blobKey, queryVectorBuffer.array()) + .param(efKey, efValue.array()).limit(0, 3).build(); - ByteBuffer efQueryString = ByteBuffer - .wrap("*=>[KNN 3 @content_vector $BLOB EF_RUNTIME $EF AS task_score]".getBytes(StandardCharsets.UTF_8)); + String efQueryString = "*=>[KNN 3 @content_vector $BLOB EF_RUNTIME $EF AS task_score]"; results = redisBinary.ftSearch("tasks-idx", efQueryString, efArgs); assertThat(results.getCount()).isEqualTo(3); // Test 4: Complex query with multiple filters and vector search - SearchArgs complexArgs = SearchArgs. builder() - .param(blobKey, queryVectorBuffer).limit(0, 10).build(); + SearchArgs complexArgs = SearchArgs. builder().param(blobKey, queryVectorBuffer.array()) + .limit(0, 10).build(); - ByteBuffer complexQueryString = ByteBuffer - .wrap("((@status:{active}) (@priority:[3 5]))=>[KNN 5 @content_vector $BLOB AS task_score]" - .getBytes(StandardCharsets.UTF_8)); + String complexQueryString = "((@status:{active}) (@priority:[3 5]))=>[KNN 5 @content_vector $BLOB AS task_score]"; results = redisBinary.ftSearch("tasks-idx", complexQueryString, complexArgs); // Verify all results match the filter criteria - ByteBuffer statusKey = ByteBuffer.wrap("status".getBytes(StandardCharsets.UTF_8)); - ByteBuffer priorityKey = ByteBuffer.wrap("priority".getBytes(StandardCharsets.UTF_8)); - for (SearchReply.SearchResult result : results.getResults()) { - String status = new String(result.getFields().get(statusKey).array(), StandardCharsets.UTF_8); - String priorityStr = new String(result.getFields().get(priorityKey).array(), StandardCharsets.UTF_8); + for (SearchReply.SearchResult result : results.getResults()) { + String status = result.getFields().get("status").asString(); + String priorityStr = result.getFields().get("priority").asString(); assertThat(status).isEqualTo("active"); int priority = Integer.parseInt(priorityStr); assertThat(priority).isBetween(3, 5); } // Test 5: Vector search with timeout - SearchArgs timeoutArgs = SearchArgs. builder() - .param(blobKey, queryVectorBuffer).timeout(Duration.ofSeconds(5)).limit(0, 5).build(); + SearchArgs timeoutArgs = SearchArgs. builder().param(blobKey, queryVectorBuffer.array()) + .timeout(Duration.ofSeconds(5)).limit(0, 5).build(); - ByteBuffer timeoutQueryString = ByteBuffer - .wrap("*=>[KNN 5 @content_vector $BLOB AS task_score]".getBytes(StandardCharsets.UTF_8)); + String timeoutQueryString = "*=>[KNN 5 @content_vector $BLOB AS task_score]"; results = redisBinary.ftSearch("tasks-idx", timeoutQueryString, timeoutArgs); assertThat(results.getCount()).isEqualTo(5); @@ -936,14 +896,12 @@ void testAdvancedVectorSearchFeatures() { @Test void testVectorTypesAndPrecision() { // Test FLOAT64 vectors - FieldArgs float64Field = VectorFieldArgs. builder().name("embedding_f64").flat() - .type(VectorFieldArgs.VectorType.FLOAT64).dimensions(2).distanceMetric(VectorFieldArgs.DistanceMetric.L2) - .build(); + FieldArgs float64Field = VectorFieldArgs.builder().name("embedding_f64").flat().type(VectorFieldArgs.VectorType.FLOAT64) + .dimensions(2).distanceMetric(VectorFieldArgs.DistanceMetric.L2).build(); - FieldArgs nameField = TextFieldArgs. builder().name("name").build(); + FieldArgs nameField = TextFieldArgs.builder().name("name").build(); - CreateArgs createArgs = CreateArgs. builder().withPrefix("precision:") - .on(CreateArgs.TargetType.HASH).build(); + CreateArgs createArgs = CreateArgs.builder().withPrefix("precision:").on(CreateArgs.TargetType.HASH).build(); redis.ftCreate("precision-idx", createArgs, Arrays.asList(float64Field, nameField)); @@ -981,21 +939,19 @@ void testVectorTypesAndPrecision() { } queryBuffer.flip(); - ByteBuffer blobKey = ByteBuffer.wrap("BLOB".getBytes(StandardCharsets.UTF_8)); - SearchArgs precisionArgs = SearchArgs. builder() - .param(blobKey, queryBuffer).limit(0, 2).build(); + String blobKey = "BLOB"; + SearchArgs precisionArgs = SearchArgs. builder().param(blobKey, queryBuffer.array()).limit(0, 2) + .build(); - ByteBuffer queryString = ByteBuffer - .wrap("*=>[KNN 2 @embedding_f64 $BLOB AS distance]".getBytes(StandardCharsets.UTF_8)); - SearchReply results = redisBinary.ftSearch("precision-idx", queryString, precisionArgs); + String queryString = "*=>[KNN 2 @embedding_f64 $BLOB AS distance]"; + SearchReply results = redisBinary.ftSearch("precision-idx", queryString, precisionArgs); assertThat(results.getCount()).isEqualTo(2); assertThat(results.getResults()).hasSize(2); // Verify that the search worked with high precision vectors - ByteBuffer nameKey = ByteBuffer.wrap("name".getBytes(StandardCharsets.UTF_8)); - for (SearchReply.SearchResult result : results.getResults()) { - String name = new String(result.getFields().get(nameKey).array(), StandardCharsets.UTF_8); + for (SearchReply.SearchResult result : results.getResults()) { + String name = result.getFields().get("name").asString(); assertThat(name).contains("High Precision Vector"); } @@ -1012,12 +968,10 @@ void testVectorSearchErrorHandling() { assumeTrue(RedisConditions.of(redis).hasVersionGreaterOrEqualsTo("8.0")); // Create a simple vector index - FieldArgs vectorField = VectorFieldArgs. builder().name("test_vector").flat() - .type(VectorFieldArgs.VectorType.FLOAT32).dimensions(3).distanceMetric(VectorFieldArgs.DistanceMetric.COSINE) - .build(); + FieldArgs vectorField = VectorFieldArgs.builder().name("test_vector").flat().type(VectorFieldArgs.VectorType.FLOAT32) + .dimensions(3).distanceMetric(VectorFieldArgs.DistanceMetric.COSINE).build(); - CreateArgs createArgs = CreateArgs. builder().withPrefix("error:") - .on(CreateArgs.TargetType.HASH).build(); + CreateArgs createArgs = CreateArgs.builder().withPrefix("error:").on(CreateArgs.TargetType.HASH).build(); redis.ftCreate("error-test-idx", createArgs, Collections.singletonList(vectorField)); @@ -1031,21 +985,20 @@ void testVectorSearchErrorHandling() { float[] queryVector = { 0.9f, 0.1f, 0.0f }; ByteBuffer queryVectorBuffer = floatArrayToByteBuffer(queryVector); - ByteBuffer blobKey = ByteBuffer.wrap("BLOB".getBytes(StandardCharsets.UTF_8)); - SearchArgs validArgs = SearchArgs. builder() - .param(blobKey, queryVectorBuffer).limit(0, 1).build(); + String blobKey = "BLOB"; + SearchArgs validArgs = SearchArgs. builder().param(blobKey, queryVectorBuffer.array()) + .limit(0, 1).build(); - ByteBuffer queryString = ByteBuffer.wrap("*=>[KNN 1 @test_vector $BLOB]".getBytes(StandardCharsets.UTF_8)); - SearchReply results = redisBinary.ftSearch("error-test-idx", queryString, validArgs); + String queryString = "*=>[KNN 1 @test_vector $BLOB]"; + SearchReply results = redisBinary.ftSearch("error-test-idx", queryString, validArgs); assertThat(results.getCount()).isEqualTo(1); // Test 2: Search with invalid field should throw exception - SearchArgs noResultsArgs = SearchArgs. builder() - .param(blobKey, queryVectorBuffer).limit(0, 10).build(); + SearchArgs noResultsArgs = SearchArgs. builder().param(blobKey, queryVectorBuffer.array()) + .limit(0, 10).build(); - ByteBuffer noResultsQueryString = ByteBuffer - .wrap("(@nonexistent_field:value)=>[KNN 5 @test_vector $BLOB]".getBytes(StandardCharsets.UTF_8)); + String noResultsQueryString = "(@nonexistent_field:value)=>[KNN 5 @test_vector $BLOB]"; // This should throw an exception because the field doesn't exist assertThatThrownBy(() -> redisBinary.ftSearch("error-test-idx", noResultsQueryString, noResultsArgs)) @@ -1056,136 +1009,48 @@ void testVectorSearchErrorHandling() { } /** - * Test vector search with mixed binary and text fields, following the Python example. This test demonstrates handling both - * binary vector data and text data in the same hash, with proper decoding of each field type. + * A mixed text+vector response on a single {@code String} connection. A typical Lettuce app uses one shared {@code String} + * connection; its documents, however, often hold a mix of text and binary vector fields, where the vectors were written + * binary-safe by an ingestion pipeline (another service, a Python job, etc.). {@link SearchReply} keeps the returned field + * values as raw bytes, exposed per field as a {@link FieldValue}: the text field is read through + * {@link FieldValue#asString()} while the vector's exact bytes are read through {@link FieldValue#asBytes()}, so both + * survive the round-trip. */ @Test - @Disabled("Test is being very flaky on the pipeline") - void testVectorSearchBinaryAndTextFields() { - // Create a custom codec that can handle both strings and byte arrays - RedisCodec mixedCodec = new RedisCodec() { - - @Override - public String decodeKey(ByteBuffer bytes) { - return StandardCharsets.UTF_8.decode(bytes).toString(); - } + void mixedTextAndVectorResponseRoundTripsBothOnSingleConnection() { + String indexName = "mixed-response-idx"; + String key = indexName + ":1"; - @Override - public Object decodeValue(ByteBuffer bytes) { - // Try to decode as UTF-8 string first - try { - String str = StandardCharsets.UTF_8.decode(bytes.duplicate()).toString(); - // Check if it's a valid UTF-8 string (no replacement characters) - if (!str.contains("\uFFFD")) { - return str; - } - } catch (Exception e) { - // Fall through to return raw bytes - } - // Return raw bytes for binary data - byte[] result = new byte[bytes.remaining()]; - bytes.get(result); - return result; - } + float[] vec = { 0.1f, 0.2f, 0.3f, 0.4f }; + byte[] vecBytes = floatArrayToByteBuffer(vec).array(); - @Override - public ByteBuffer encodeKey(String key) { - return ByteBuffer.wrap(key.getBytes(StandardCharsets.UTF_8)); - } + FieldArgs nameField = TextFieldArgs.builder().name("name").build(); + FieldArgs vectorField = VectorFieldArgs.builder().name("embedding").hnsw().type(VectorFieldArgs.VectorType.FLOAT32) + .dimensions(4).distanceMetric(VectorFieldArgs.DistanceMetric.COSINE).build(); + CreateArgs createArgs = CreateArgs.builder().withPrefix(indexName + ":").on(CreateArgs.TargetType.HASH).build(); + redis.ftCreate(indexName, createArgs, Arrays.asList(nameField, vectorField)); - @Override - public ByteBuffer encodeValue(Object value) { - if (value instanceof String) { - return ByteBuffer.wrap(((String) value).getBytes(StandardCharsets.UTF_8)); - } else if (value instanceof byte[]) { - return ByteBuffer.wrap((byte[]) value); - } else if (value instanceof float[]) { - float[] floats = (float[]) value; - ByteBuffer buffer = ByteBuffer.allocate(floats.length * 4).order(ByteOrder.LITTLE_ENDIAN); - for (float f : floats) { - buffer.putFloat(f); - } - return (ByteBuffer) buffer.flip(); - } else { - return ByteBuffer.wrap(value.toString().getBytes(StandardCharsets.UTF_8)); - } - } - - }; - - // Create connection with mixed codec - RedisCommands redisMixed = client.connect(mixedCodec).sync(); + // The application's single connection is the String one below. The binary write via redisBinary is only a fixture + // standing in for the ingestion pipeline that stored the vector binary-safe (same key/field bytes on the wire). + redis.hset(key, "name", "Lettuce"); + redisBinary.hset(ByteBuffer.wrap(key.getBytes(StandardCharsets.UTF_8)), + ByteBuffer.wrap("embedding".getBytes(StandardCharsets.UTF_8)), ByteBuffer.wrap(vecBytes)); try { - // Create fake vector similar to Python example - float[] fakeVec = { 0.1f, 0.2f, 0.3f, 0.4f }; - byte[] fakeVecBytes = floatArrayToByteBuffer(fakeVec).array(); - - String indexName = "mixed_index"; - String keyName = indexName + ":1"; - - // Store mixed data: text field and binary vector field - redisMixed.hset(keyName, "first_name", "🥬 Lettuce"); - redisMixed.hset(keyName, "vector_emb", fakeVecBytes); + SearchReply result = redis.ftSearch(indexName, "*", + SearchArgs. builder().returnField("name").returnField("embedding").build()); - // Create index with both text and vector fields - FieldArgs textField = TagFieldArgs. builder().name("first_name").build(); + assertThat(result.getCount()).isEqualTo(1L); + SearchReply.SearchResult searchResult = result.getResults().get(0); - FieldArgs vectorField = VectorFieldArgs. builder().name("embeddings_bio").hnsw() - .type(VectorFieldArgs.VectorType.FLOAT32).dimensions(4) - .distanceMetric(VectorFieldArgs.DistanceMetric.COSINE).build(); - - CreateArgs createArgs = CreateArgs. builder().withPrefix(indexName + ":") - .on(CreateArgs.TargetType.HASH).build(); - - redis.ftCreate(indexName, createArgs, Arrays.asList(textField, vectorField)); - - // Search with specific field returns - equivalent to Python's return_field with decode_field=False - SearchArgs searchArgs = SearchArgs. builder().returnField("vector_emb") // This - // should - // return - // raw - // binary - // data - .returnField("first_name") // This should return decoded text - .build(); - - SearchReply results = redisMixed.ftSearch(indexName, "*", searchArgs); - - assertThat(results.getCount()).isEqualTo(1); - assertThat(results.getResults()).hasSize(1); - - SearchReply.SearchResult result = results.getResults().get(0); - Map fields = result.getFields(); - - // Verify text field is properly decoded - Object firstNameValue = fields.get("first_name"); - assertThat(firstNameValue).isInstanceOf(String.class); - assertThat((String) firstNameValue).isEqualTo("🥬 Lettuce"); - - // Verify vector field returns binary data - Object vectorValue = fields.get("vector_emb"); - assertThat(vectorValue).isInstanceOf(byte[].class); - - // Convert retrieved binary data back to float array and compare - byte[] retrievedVecBytes = (byte[]) vectorValue; - ByteBuffer buffer = ByteBuffer.wrap(retrievedVecBytes).order(ByteOrder.LITTLE_ENDIAN); - float[] retrievedVec = new float[4]; - for (int i = 0; i < 4; i++) { - retrievedVec[i] = buffer.getFloat(); - } - - // Assert that the vectors are equal (equivalent to Python's np.array_equal) - assertThat(retrievedVec).containsExactly(fakeVec); - - // Cleanup - redis.ftDropindex(indexName); + // the text field is read through the UTF-8 view + assertThat(searchResult.getFields().get("name").asString()).isEqualTo("Lettuce"); + // the vector field's raw bytes survive the round-trip via getFieldBytes + assertThat(searchResult.getFields().get("embedding").asBytes()) + .as("vector bytes must be preserved in a mixed text+vector response").isEqualTo(vecBytes); } finally { - // Close the mixed codec connection - if (redisMixed != null) { - redisMixed.getStatefulConnection().close(); - } + redis.ftDropindex(indexName); } } @@ -1204,22 +1069,21 @@ void testQuantizedVectorTypes(VectorFieldArgs.VectorType vectorType) { String fieldName = "embedding_" + typeName.toLowerCase(); // Create vector field with appropriate algorithm based on type - FieldArgs vectorField; + FieldArgs vectorField; if (vectorType == VectorFieldArgs.VectorType.INT8) { // INT8 with FLAT algorithm and L2 distance - vectorField = VectorFieldArgs. builder().name(fieldName).flat().type(vectorType).dimensions(4) + vectorField = VectorFieldArgs.builder().name(fieldName).flat().type(vectorType).dimensions(4) .distanceMetric(VectorFieldArgs.DistanceMetric.L2).build(); } else { // UINT8 with HNSW algorithm and COSINE distance - vectorField = VectorFieldArgs. builder().name(fieldName).hnsw().type(vectorType).dimensions(4) + vectorField = VectorFieldArgs.builder().name(fieldName).hnsw().type(vectorType).dimensions(4) .distanceMetric(VectorFieldArgs.DistanceMetric.COSINE).attribute("M", 16).attribute("EF_CONSTRUCTION", 200) .build(); } - FieldArgs nameField = TextFieldArgs. builder().name("name").build(); + FieldArgs nameField = TextFieldArgs.builder().name("name").build(); - CreateArgs createArgs = CreateArgs. builder().withPrefix(prefix) - .on(CreateArgs.TargetType.HASH).build(); + CreateArgs createArgs = CreateArgs.builder().withPrefix(prefix).on(CreateArgs.TargetType.HASH).build(); redis.ftCreate(indexName, createArgs, Arrays.asList(vectorField, nameField)); @@ -1257,21 +1121,19 @@ void testQuantizedVectorTypes(VectorFieldArgs.VectorType vectorType) { // Test KNN search ByteBuffer queryBuffer = ByteBuffer.wrap(queryVector); - ByteBuffer blobKey = ByteBuffer.wrap("BLOB".getBytes(StandardCharsets.UTF_8)); - SearchArgs searchArgs = SearchArgs. builder() - .param(blobKey, queryBuffer).limit(0, 2).build(); + String blobKey = "BLOB"; + SearchArgs searchArgs = SearchArgs. builder().param(blobKey, queryBuffer.array()).limit(0, 2) + .build(); - ByteBuffer queryString = ByteBuffer - .wrap(("*=>[KNN 2 @" + fieldName + " $BLOB AS distance]").getBytes(StandardCharsets.UTF_8)); - SearchReply results = redisBinary.ftSearch(indexName, queryString, searchArgs); + String queryString = "*=>[KNN 2 @" + fieldName + " $BLOB AS distance]"; + SearchReply results = redisBinary.ftSearch(indexName, queryString, searchArgs); assertThat(results.getCount()).isEqualTo(2); assertThat(results.getResults()).hasSize(2); // Verify that the search worked with the quantized vectors - ByteBuffer nameKey = ByteBuffer.wrap("name".getBytes(StandardCharsets.UTF_8)); - for (SearchReply.SearchResult result : results.getResults()) { - String name = new String(result.getFields().get(nameKey).array(), StandardCharsets.UTF_8); + for (SearchReply.SearchResult result : results.getResults()) { + String name = result.getFields().get("name").asString(); assertThat(name).contains(typeName + " Vector"); } diff --git a/src/test/java/io/lettuce/core/search/RedisJsonIndexingIntegrationTests.java b/src/test/java/io/lettuce/core/search/RedisJsonIndexingIntegrationTests.java index d79417a471..5c0d80e7d9 100644 --- a/src/test/java/io/lettuce/core/search/RedisJsonIndexingIntegrationTests.java +++ b/src/test/java/io/lettuce/core/search/RedisJsonIndexingIntegrationTests.java @@ -92,12 +92,11 @@ void testBasicJsonIndexingAndSearch() { // Create index based on Redis documentation example: // FT.CREATE itemIdx ON JSON PREFIX 1 item: SCHEMA $.name AS name TEXT $.description as description TEXT $.price AS // price NUMERIC - FieldArgs nameField = TextFieldArgs. builder().name("$.name").as("name").build(); - FieldArgs descriptionField = TextFieldArgs. builder().name("$.description").as("description").build(); - FieldArgs priceField = NumericFieldArgs. builder().name("$.price").as("price").build(); + FieldArgs nameField = TextFieldArgs.builder().name("$.name").as("name").build(); + FieldArgs descriptionField = TextFieldArgs.builder().name("$.description").as("description").build(); + FieldArgs priceField = NumericFieldArgs.builder().name("$.price").as("price").build(); - CreateArgs createArgs = CreateArgs. builder().withPrefix(ITEM_PREFIX) - .on(CreateArgs.TargetType.JSON).build(); + CreateArgs createArgs = CreateArgs.builder().withPrefix(ITEM_PREFIX).on(CreateArgs.TargetType.JSON).build(); String result = redis.ftCreate(ITEM_INDEX, createArgs, Arrays.asList(nameField, descriptionField, priceField)); assertThat(result).isEqualTo("OK"); @@ -118,7 +117,7 @@ void testBasicJsonIndexingAndSearch() { assertThat(redis.jsonSet("item:2", JsonPath.ROOT_PATH, item2)).isEqualTo("OK"); // Test 1: Search for items with "earbuds" in the name - SearchReply searchReply = redis.ftSearch(ITEM_INDEX, "@name:(earbuds)", null); + SearchReply searchReply = redis.ftSearch(ITEM_INDEX, "@name:(earbuds)", null); assertThat(searchReply.getCount()).isEqualTo(1); assertThat(searchReply.getResults()).hasSize(1); assertThat(searchReply.getResults().get(0).getId()).isEqualTo("item:2"); @@ -145,12 +144,11 @@ void testBasicJsonIndexingAndSearch() { void testJsonArraysAsTagFields() { // Create index with TAG field for colors using wildcard JSONPath // FT.CREATE itemIdx2 ON JSON PREFIX 1 item: SCHEMA $.colors.* AS colors TAG $.name AS name TEXT - FieldArgs colorsField = TagFieldArgs. builder().name("$.colors.*").as("colors").build(); - FieldArgs nameField = TextFieldArgs. builder().name("$.name").as("name").build(); - FieldArgs descriptionField = TextFieldArgs. builder().name("$.description").as("description").build(); + FieldArgs colorsField = TagFieldArgs.builder().name("$.colors.*").as("colors").build(); + FieldArgs nameField = TextFieldArgs.builder().name("$.name").as("name").build(); + FieldArgs descriptionField = TextFieldArgs.builder().name("$.description").as("description").build(); - CreateArgs createArgs = CreateArgs. builder().withPrefix(ITEM_PREFIX) - .on(CreateArgs.TargetType.JSON).build(); + CreateArgs createArgs = CreateArgs.builder().withPrefix(ITEM_PREFIX).on(CreateArgs.TargetType.JSON).build(); redis.ftCreate(ITEM_INDEX_2, createArgs, Arrays.asList(colorsField, nameField, descriptionField)); @@ -171,7 +169,7 @@ void testJsonArraysAsTagFields() { redis.jsonSet("item:3", JsonPath.ROOT_PATH, item3); // Test 1: Search for silver headphones - SearchReply results = redis.ftSearch(ITEM_INDEX_2, + SearchReply results = redis.ftSearch(ITEM_INDEX_2, "@colors:{silver} (@name:(headphones)|@description:(headphones))", null); assertThat(results.getCount()).isEqualTo(1); assertThat(results.getResults().get(0).getId()).isEqualTo("item:1"); @@ -197,12 +195,11 @@ void testJsonArraysAsTagFields() { void testJsonArraysAsTextFields() { // Create index with TEXT field for colors array // FT.CREATE itemIdx3 ON JSON PREFIX 1 item: SCHEMA $.colors AS colors TEXT $.name AS name TEXT - FieldArgs colorsField = TextFieldArgs. builder().name("$.colors").as("colors").build(); - FieldArgs nameField = TextFieldArgs. builder().name("$.name").as("name").build(); - FieldArgs descriptionField = TextFieldArgs. builder().name("$.description").as("description").build(); + FieldArgs colorsField = TextFieldArgs.builder().name("$.colors").as("colors").build(); + FieldArgs nameField = TextFieldArgs.builder().name("$.name").as("name").build(); + FieldArgs descriptionField = TextFieldArgs.builder().name("$.description").as("description").build(); - CreateArgs createArgs = CreateArgs. builder().withPrefix(ITEM_PREFIX) - .on(CreateArgs.TargetType.JSON).build(); + CreateArgs createArgs = CreateArgs.builder().withPrefix(ITEM_PREFIX).on(CreateArgs.TargetType.JSON).build(); redis.ftCreate(ITEM_INDEX_3, createArgs, Arrays.asList(colorsField, nameField, descriptionField)); @@ -217,9 +214,9 @@ void testJsonArraysAsTextFields() { redis.jsonSet("item:3", JsonPath.ROOT_PATH, item3); // Test full text search for light colored headphones - SearchArgs returnArgs = SearchArgs. builder().returnField("$.colors").build(); - SearchReply results = redis.ftSearch(ITEM_INDEX_3, - "@colors:(white|light) (@name|description:(headphones))", returnArgs); + SearchArgs returnArgs = SearchArgs. builder().returnField("$.colors").build(); + SearchReply results = redis.ftSearch(ITEM_INDEX_3, "@colors:(white|light) (@name|description:(headphones))", + returnArgs); assertThat(results.getCount()).isEqualTo(2); assertThat(results.getResults()).hasSize(2); @@ -235,10 +232,9 @@ void testJsonArraysAsTextFields() { void testJsonArraysAsNumericFields() { // Create index with NUMERIC field for max_level array // FT.CREATE itemIdx4 ON JSON PREFIX 1 item: SCHEMA $.max_level AS dB NUMERIC - FieldArgs dbField = NumericFieldArgs. builder().name("$.max_level").as("dB").build(); + FieldArgs dbField = NumericFieldArgs.builder().name("$.max_level").as("dB").build(); - CreateArgs createArgs = CreateArgs. builder().withPrefix(ITEM_PREFIX) - .on(CreateArgs.TargetType.JSON).build(); + CreateArgs createArgs = CreateArgs.builder().withPrefix(ITEM_PREFIX).on(CreateArgs.TargetType.JSON).build(); redis.ftCreate(ITEM_INDEX_4, createArgs, Collections.singletonList(dbField)); @@ -257,7 +253,7 @@ void testJsonArraysAsNumericFields() { redis.jsonSet("item:3", JsonPath.ROOT_PATH, item3); // Test 1: Search for headphones with max volume between 70 and 80 (inclusive) - SearchReply results = redis.ftSearch(ITEM_INDEX_4, "@dB:[70 80]", null); + SearchReply results = redis.ftSearch(ITEM_INDEX_4, "@dB:[70 80]", null); assertThat(results.getCount()).isEqualTo(2); // item:1 and item:2 // Test 2: Search for items with all values in range [90, 120] @@ -275,12 +271,11 @@ void testJsonArraysAsNumericFields() { @Test void testFieldProjectionWithJsonPath() { // Create basic index - FieldArgs nameField = TextFieldArgs. builder().name("$.name").as("name").build(); - FieldArgs descriptionField = TextFieldArgs. builder().name("$.description").as("description").build(); - FieldArgs priceField = NumericFieldArgs. builder().name("$.price").as("price").build(); + FieldArgs nameField = TextFieldArgs.builder().name("$.name").as("name").build(); + FieldArgs descriptionField = TextFieldArgs.builder().name("$.description").as("description").build(); + FieldArgs priceField = NumericFieldArgs.builder().name("$.price").as("price").build(); - CreateArgs createArgs = CreateArgs. builder().withPrefix(ITEM_PREFIX) - .on(CreateArgs.TargetType.JSON).build(); + CreateArgs createArgs = CreateArgs.builder().withPrefix(ITEM_PREFIX).on(CreateArgs.TargetType.JSON).build(); redis.ftCreate(ITEM_INDEX, createArgs, Arrays.asList(nameField, descriptionField, priceField)); @@ -297,23 +292,22 @@ void testFieldProjectionWithJsonPath() { redis.jsonSet("item:2", JsonPath.ROOT_PATH, item2); // Test 1: Return specific attributes (name and price) - SearchArgs returnArgs = SearchArgs. builder().returnField("name").returnField("price") - .build(); - SearchReply results = redis.ftSearch(ITEM_INDEX, "@description:(headphones)", returnArgs); + SearchArgs returnArgs = SearchArgs. builder().returnField("name").returnField("price").build(); + SearchReply results = redis.ftSearch(ITEM_INDEX, "@description:(headphones)", returnArgs); assertThat(results.getCount()).isEqualTo(2); - for (SearchReply.SearchResult result : results.getResults()) { + for (SearchReply.SearchResult result : results.getResults()) { assertThat(result.getFields()).containsKey("name"); assertThat(result.getFields()).containsKey("price"); assertThat(result.getFields()).doesNotContainKey("description"); } // Test 2: Project with JSONPath expression (including non-indexed field) - SearchArgs jsonPathArgs = SearchArgs. builder().returnField("name").returnField("price") + SearchArgs jsonPathArgs = SearchArgs. builder().returnField("name").returnField("price") .returnField("$.stock") // JSONPath without alias .build(); results = redis.ftSearch(ITEM_INDEX, "@description:(headphones)", jsonPathArgs); assertThat(results.getCount()).isEqualTo(2); - for (SearchReply.SearchResult result : results.getResults()) { + for (SearchReply.SearchResult result : results.getResults()) { assertThat(result.getFields()).containsKey("name"); assertThat(result.getFields()).containsKey("price"); assertThat(result.getFields()).containsKey("$.stock"); @@ -330,12 +324,10 @@ void testFieldProjectionWithJsonPath() { void testJsonObjectIndexing() { // Create index for individual object elements // FT.CREATE itemIdx ON JSON SCHEMA $.connection.wireless AS wireless TAG $.connection.type AS connectionType TEXT - FieldArgs wirelessField = TagFieldArgs. builder().name("$.connection.wireless").as("wireless").build(); - FieldArgs connectionTypeField = TextFieldArgs. builder().name("$.connection.type").as("connectionType") - .build(); + FieldArgs wirelessField = TagFieldArgs.builder().name("$.connection.wireless").as("wireless").build(); + FieldArgs connectionTypeField = TextFieldArgs.builder().name("$.connection.type").as("connectionType").build(); - CreateArgs createArgs = CreateArgs. builder().withPrefix(ITEM_PREFIX) - .on(CreateArgs.TargetType.JSON).build(); + CreateArgs createArgs = CreateArgs.builder().withPrefix(ITEM_PREFIX).on(CreateArgs.TargetType.JSON).build(); redis.ftCreate(ITEM_INDEX, createArgs, Arrays.asList(wirelessField, connectionTypeField)); @@ -350,7 +342,7 @@ void testJsonObjectIndexing() { redis.jsonSet("item:2", JsonPath.ROOT_PATH, item2); // Test 1: Search for wireless items - SearchReply results = redis.ftSearch(ITEM_INDEX, "@wireless:{true}", null); + SearchReply results = redis.ftSearch(ITEM_INDEX, "@wireless:{true}", null); assertThat(results.getCount()).isEqualTo(1); assertThat(results.getResults().get(0).getId()).isEqualTo("item:1"); diff --git a/src/test/java/io/lettuce/core/search/SearchResultsTest.java b/src/test/java/io/lettuce/core/search/SearchResultsTest.java index 9c6e0d0fd9..aac0691b52 100644 --- a/src/test/java/io/lettuce/core/search/SearchResultsTest.java +++ b/src/test/java/io/lettuce/core/search/SearchResultsTest.java @@ -8,7 +8,9 @@ package io.lettuce.core.search; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; @@ -27,7 +29,7 @@ class SearchResultsTest { @Test void testEmptySearchResults() { - SearchReply results = new SearchReply<>(); + SearchReply results = new SearchReply<>(); assertThat(results.getCount()).isEqualTo(0); assertThat(results.getResults()).isEmpty(); @@ -37,30 +39,26 @@ void testEmptySearchResults() { @Test void testSearchResultsWithData() { - SearchReply results = new SearchReply<>(); + SearchReply results = new SearchReply<>(); results.setCount(10); // Create a search result - SearchReply.SearchResult result1 = new SearchReply.SearchResult<>("doc1"); + SearchReply.SearchResult result1 = new SearchReply.SearchResult<>("doc1"); result1.setScore(0.95); - result1.setPayload("payload1"); - result1.setSortKey("sortkey1"); - Map fields1 = new HashMap<>(); - fields1.put("title", "Test Document 1"); - fields1.put("content", "This is test content"); + Map fields1 = new HashMap<>(); + fields1.put("title", "Test Document 1".getBytes(StandardCharsets.UTF_8)); + fields1.put("content", "This is test content".getBytes(StandardCharsets.UTF_8)); result1.addFields(fields1); results.addResult(result1); // Create another search result - SearchReply.SearchResult result2 = new SearchReply.SearchResult<>("doc2"); + SearchReply.SearchResult result2 = new SearchReply.SearchResult<>("doc2"); result2.setScore(0.87); - Map fields2 = new HashMap<>(); - fields2.put("title", "Test Document 2"); - fields2.put("content", "This is more test content"); - result2.addFields(fields2); + result2.addField("title", "Test Document 2".getBytes(StandardCharsets.UTF_8)); + result2.addField("content", "This is more test content".getBytes(StandardCharsets.UTF_8)); results.addResult(result2); @@ -71,29 +69,66 @@ void testSearchResultsWithData() { assertThat(results.getResults()).hasSize(2); - SearchReply.SearchResult firstResult = results.getResults().get(0); + SearchReply.SearchResult firstResult = results.getResults().get(0); assertThat(firstResult.getId()).isEqualTo("doc1"); assertThat(firstResult.getScore()).isEqualTo(0.95); - assertThat(firstResult.getPayload()).isEqualTo("payload1"); - assertThat(firstResult.getSortKey()).isEqualTo("sortkey1"); - assertThat(firstResult.getFields()).containsEntry("title", "Test Document 1"); - assertThat(firstResult.getFields()).containsEntry("content", "This is test content"); + assertThat(firstResult.getFields().get("title").asString()).isEqualTo("Test Document 1"); + assertThat(firstResult.getFields().get("content").asString()).isEqualTo("This is test content"); - SearchReply.SearchResult secondResult = results.getResults().get(1); + SearchReply.SearchResult secondResult = results.getResults().get(1); assertThat(secondResult.getId()).isEqualTo("doc2"); assertThat(secondResult.getScore()).isEqualTo(0.87); - assertThat(secondResult.getPayload()).isNull(); - assertThat(secondResult.getSortKey()).isNull(); - assertThat(secondResult.getFields()).containsEntry("title", "Test Document 2"); - assertThat(secondResult.getFields()).containsEntry("content", "This is more test content"); + assertThat(secondResult.getFields().get("title").asString()).isEqualTo("Test Document 2"); + assertThat(secondResult.getFields().get("content").asString()).isEqualTo("This is more test content"); + } + + @Test + void testFieldValuesExposeTextAndBinary() { + SearchReply.SearchResult result = new SearchReply.SearchResult<>("doc1"); + + // a binary value that is not valid UTF-8 (e.g. a little-endian float32 vector) + byte[] vector = new byte[] { -51, -52, -52, 61, -51, -52, 76, 62 }; + result.addField("embedding", vector); + result.addField("title", "Lettuce".getBytes(StandardCharsets.UTF_8)); + + Map fields = result.getFields(); + + // a text field reads back as a String + assertThat(fields.get("title").asString()).isEqualTo("Lettuce"); + + // the binary field survives byte-exact, independent of the lossy String view + assertThat(fields.get("embedding").asBytes()).isEqualTo(vector); + + // absent fields are simply not present in the map + assertThat(fields.get("missing")).isNull(); + assertThat(fields).containsOnlyKeys("embedding", "title"); + + // fields added later are visible + result.addField("category", "greens".getBytes(StandardCharsets.UTF_8)); + assertThat(result.getFields().get("category").asString()).isEqualTo("greens"); + } + + @Test + void testFieldValueDecodesWithGivenCharset() { + SearchReply.SearchResult result = new SearchReply.SearchResult<>("doc1"); + result.addField("title", "café".getBytes(StandardCharsets.ISO_8859_1)); + + FieldValue title = result.getFields().get("title"); + assertThat(title.asString(StandardCharsets.ISO_8859_1)).isEqualTo("café"); + } + + @Test + void testFieldValueRejectsNull() { + // a FieldValue always wraps real bytes; a missing field is a missing map key, not a null/empty value + assertThatThrownBy(() -> FieldValue.of(null)).isInstanceOf(IllegalArgumentException.class); } @Test void testSearchResultsConstructorWithData() { - SearchReply.SearchResult result = new SearchReply.SearchResult<>("doc1"); + SearchReply.SearchResult result = new SearchReply.SearchResult<>("doc1"); result.setScore(0.95); - SearchReply results = new SearchReply<>(5, java.util.Arrays.asList(result)); + SearchReply results = new SearchReply<>(5, java.util.Arrays.asList(result)); assertThat(results.getCount()).isEqualTo(5); assertThat(results.size()).isEqualTo(1); @@ -103,8 +138,8 @@ void testSearchResultsConstructorWithData() { @Test void testSearchResultImmutability() { - SearchReply results = new SearchReply<>(); - SearchReply.SearchResult result = new SearchReply.SearchResult<>("doc1"); + SearchReply results = new SearchReply<>(); + SearchReply.SearchResult result = new SearchReply.SearchResult<>("doc1"); results.addResult(result); // The returned list should be unmodifiable diff --git a/src/test/java/io/lettuce/core/search/arguments/AggregateArgsTest.java b/src/test/java/io/lettuce/core/search/arguments/AggregateArgsTest.java new file mode 100644 index 0000000000..88e6be9158 --- /dev/null +++ b/src/test/java/io/lettuce/core/search/arguments/AggregateArgsTest.java @@ -0,0 +1,104 @@ +/* + * Copyright 2026-present, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ + +package io.lettuce.core.search.arguments; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +import io.lettuce.core.codec.StringCodec; +import io.lettuce.core.protocol.CommandArgs; +import io.lettuce.core.search.arguments.AggregateArgs.GroupBy; +import io.lettuce.core.search.arguments.AggregateArgs.Reducer; +import io.lettuce.core.search.arguments.AggregateArgs.SortBy; +import io.lettuce.core.search.arguments.AggregateArgs.SortDirection; + +/** + * Unit tests for {@link AggregateArgs} focusing on {@code @} prefix normalization in {@link GroupBy}, {@link SortBy}, and + * {@link Reducer}. + * + * @author Viktoriya Kutsarova + */ +class AggregateArgsTest { + + // ------------------------------------------------------------------------- + // GroupBy + // ------------------------------------------------------------------------- + + @Test + void groupByFieldWithoutAtPrefixShouldAddPrefix() { + CommandArgs args = new CommandArgs<>(StringCodec.UTF8); + + GroupBy.of("category").build(args); + + assertThat(args.toString()).contains("@category"); + } + + @Test + void groupByFieldWithAtPrefixShouldNotDoublePrefix() { + CommandArgs args = new CommandArgs<>(StringCodec.UTF8); + + GroupBy.of("@category").build(args); + + String output = args.toString(); + assertThat(output).contains("@category"); + assertThat(output).doesNotContain("@@category"); + } + + @Test + void groupByMultipleFieldsMixedPrefixShouldNormalise() { + CommandArgs args = new CommandArgs<>(StringCodec.UTF8); + + GroupBy.of("brand", "@price").build(args); + + String output = args.toString(); + assertThat(output).contains("@brand"); + assertThat(output).contains("@price"); + assertThat(output).doesNotContain("@@price"); + } + + // ------------------------------------------------------------------------- + // SortBy + // ------------------------------------------------------------------------- + + @Test + @SuppressWarnings({ "unchecked", "rawtypes" }) + void sortByFieldWithoutAtPrefixShouldAddPrefix() { + CommandArgs args = new CommandArgs(StringCodec.UTF8); + + SortBy.of("price", SortDirection.ASC).build(args); + + assertThat(args.toString()).contains("@price"); + } + + @Test + @SuppressWarnings({ "unchecked", "rawtypes" }) + void sortByFieldWithAtPrefixShouldNotDoublePrefix() { + CommandArgs args = new CommandArgs(StringCodec.UTF8); + + SortBy.of("@price", SortDirection.DESC).build(args); + + String output = args.toString(); + assertThat(output).contains("@price"); + assertThat(output).doesNotContain("@@price"); + } + + // ------------------------------------------------------------------------- + // Reducer + // ------------------------------------------------------------------------- + + @Test + void reducerAvgWithAtPrefixShouldWork() { + CommandArgs args = new CommandArgs<>(StringCodec.UTF8); + + Reducer.avg("@price").as("avg_price").build(args); + + assertThat(args.toString()).contains("@price"); + } + +} diff --git a/src/test/java/io/lettuce/core/search/arguments/CreateArgsTest.java b/src/test/java/io/lettuce/core/search/arguments/CreateArgsTest.java index 5adfe8f31e..a836d6951f 100644 --- a/src/test/java/io/lettuce/core/search/arguments/CreateArgsTest.java +++ b/src/test/java/io/lettuce/core/search/arguments/CreateArgsTest.java @@ -29,7 +29,7 @@ class CreateArgsTest { @Test void testDefaultCreateArgs() { - CreateArgs args = CreateArgs. builder().build(); + CreateArgs args = CreateArgs.builder().build(); assertThat(args.getOn()).hasValue(CreateArgs.TargetType.HASH); assertThat(args.getPrefixes()).isEmpty(); @@ -51,32 +51,30 @@ void testDefaultCreateArgs() { @Test void testCreateArgsWithTargetType() { - CreateArgs hashArgs = CreateArgs. builder().on(CreateArgs.TargetType.HASH).build(); + CreateArgs hashArgs = CreateArgs.builder().on(CreateArgs.TargetType.HASH).build(); assertThat(hashArgs.getOn()).hasValue(CreateArgs.TargetType.HASH); - CreateArgs jsonArgs = CreateArgs. builder().on(CreateArgs.TargetType.JSON).build(); + CreateArgs jsonArgs = CreateArgs.builder().on(CreateArgs.TargetType.JSON).build(); assertThat(jsonArgs.getOn()).hasValue(CreateArgs.TargetType.JSON); } @Test void testCreateArgsWithPrefixes() { - CreateArgs args = CreateArgs. builder().withPrefix("blog:").withPrefix("post:") - .withPrefix("article:").build(); + CreateArgs args = CreateArgs.builder().withPrefix("blog:").withPrefixes(Arrays.asList("post:", "article:")).build(); assertThat(args.getPrefixes()).containsExactly("blog:", "post:", "article:"); } @Test void testCreateArgsWithFilter() { - CreateArgs args = CreateArgs. builder().filter("@status:published").build(); + CreateArgs args = CreateArgs.builder().filter("@status:published").build(); assertThat(args.getFilter()).hasValue("@status:published"); } @Test void testCreateArgsWithLanguageSettings() { - CreateArgs args = CreateArgs. builder().defaultLanguage(DocumentLanguage.ENGLISH) - .languageField("lang").build(); + CreateArgs args = CreateArgs.builder().defaultLanguage(DocumentLanguage.ENGLISH).languageField("lang").build(); assertThat(args.getDefaultLanguage()).hasValue(DocumentLanguage.ENGLISH); assertThat(args.getLanguageField()).hasValue("lang"); @@ -84,7 +82,7 @@ void testCreateArgsWithLanguageSettings() { @Test void testCreateArgsWithScoreSettings() { - CreateArgs args = CreateArgs. builder().defaultScore(0.5).scoreField("score").build(); + CreateArgs args = CreateArgs.builder().defaultScore(0.5).scoreField("score").build(); assertThat(args.getDefaultScore()).hasValue(0.5); assertThat(args.getScoreField()).hasValue("score"); @@ -92,15 +90,15 @@ void testCreateArgsWithScoreSettings() { @Test void testCreateArgsWithPayloadField() { - CreateArgs args = CreateArgs. builder().payloadField("payload").build(); + CreateArgs args = CreateArgs.builder().payloadField("payload").build(); assertThat(args.getPayloadField()).hasValue("payload"); } @Test void testCreateArgsWithFlags() { - CreateArgs args = CreateArgs. builder().maxTextFields().noOffsets().noHighlighting() - .noFields().noFrequency().skipInitialScan().build(); + CreateArgs args = CreateArgs.builder().maxTextFields().noOffsets().noHighlighting().noFields().noFrequency() + .skipInitialScan().build(); assertThat(args.isMaxTextFields()).isTrue(); assertThat(args.isNoOffsets()).isTrue(); @@ -112,7 +110,7 @@ void testCreateArgsWithFlags() { @Test void testCreateArgsWithTemporary() { - CreateArgs args = CreateArgs. builder().temporary(3600).build(); + CreateArgs args = CreateArgs.builder().temporary(3600).build(); assertThat(args.getTemporary()).hasValue(3600L); } @@ -120,25 +118,24 @@ void testCreateArgsWithTemporary() { @Test void testCreateArgsWithStopWords() { List stopWords = Arrays.asList("the", "and", "or", "but"); - CreateArgs args = CreateArgs. builder().stopWords(stopWords).build(); + CreateArgs args = CreateArgs.builder().stopWords(stopWords).build(); assertThat(args.getStopWords()).hasValue(stopWords); } @Test void testCreateArgsWithEmptyStopWords() { - CreateArgs args = CreateArgs. builder().stopWords(Arrays.asList()).build(); + CreateArgs args = CreateArgs.builder().stopWords(Arrays.asList()).build(); assertThat(args.getStopWords()).hasValue(Arrays.asList()); } @Test void testCreateArgsBuild() { - CreateArgs args = CreateArgs. builder().on(CreateArgs.TargetType.JSON) - .withPrefix("blog:").withPrefix("post:").filter("@status:published").defaultLanguage(DocumentLanguage.FRENCH) - .languageField("lang").defaultScore(0.8).scoreField("score").payloadField("payload").maxTextFields() - .temporary(7200).noOffsets().noHighlighting().noFields().noFrequency().skipInitialScan() - .stopWords(Arrays.asList("le", "la", "et")).build(); + CreateArgs args = CreateArgs.builder().on(CreateArgs.TargetType.JSON).withPrefix("blog:").withPrefix("post:") + .filter("@status:published").defaultLanguage(DocumentLanguage.FRENCH).languageField("lang").defaultScore(0.8) + .scoreField("score").payloadField("payload").maxTextFields().temporary(7200).noOffsets().noHighlighting() + .noFields().noFrequency().skipInitialScan().stopWords(Arrays.asList("le", "la", "et")).build(); CommandArgs commandArgs = new CommandArgs<>(StringCodec.UTF8); args.build(commandArgs); @@ -173,7 +170,7 @@ void testCreateArgsBuild() { @Test void testCreateArgsMinimalBuild() { - CreateArgs args = CreateArgs. builder().withPrefix("test:").build(); + CreateArgs args = CreateArgs.builder().withPrefix("test:").build(); CommandArgs commandArgs = new CommandArgs<>(StringCodec.UTF8); args.build(commandArgs); diff --git a/src/test/java/io/lettuce/core/search/arguments/FieldArgsTest.java b/src/test/java/io/lettuce/core/search/arguments/FieldArgsTest.java index 0d50a0977f..529cf366e1 100644 --- a/src/test/java/io/lettuce/core/search/arguments/FieldArgsTest.java +++ b/src/test/java/io/lettuce/core/search/arguments/FieldArgsTest.java @@ -27,7 +27,7 @@ class FieldArgsTest { /** * Concrete implementation of FieldArgs for testing purposes. */ - private static class TestFieldArgs extends FieldArgs { + private static class TestFieldArgs extends FieldArgs { @Override public String getFieldType() { @@ -35,18 +35,18 @@ public String getFieldType() { } @Override - protected void buildTypeSpecificArgs(CommandArgs args) { + protected void buildTypeSpecificArgs(CommandArgs args) { // No type-specific arguments for test field } - public static Builder builder() { - return new Builder<>(); + public static Builder builder() { + return new Builder(); } - public static class Builder extends FieldArgs.Builder, Builder> { + public static class Builder extends FieldArgs.Builder { public Builder() { - super(new TestFieldArgs<>()); + super(new TestFieldArgs()); } } @@ -55,7 +55,7 @@ public Builder() { @Test void testDefaultFieldArgs() { - TestFieldArgs field = TestFieldArgs. builder().name("test_field").build(); + TestFieldArgs field = TestFieldArgs.builder().name("test_field").build(); assertThat(field.getName()).isEqualTo("test_field"); assertThat(field.getAs()).isEmpty(); @@ -69,7 +69,7 @@ void testDefaultFieldArgs() { @Test void testFieldArgsWithAlias() { - TestFieldArgs field = TestFieldArgs. builder().name("complex_field_name").as("simple_alias").build(); + TestFieldArgs field = TestFieldArgs.builder().name("complex_field_name").as("simple_alias").build(); assertThat(field.getName()).isEqualTo("complex_field_name"); assertThat(field.getAs()).hasValue("simple_alias"); @@ -77,7 +77,7 @@ void testFieldArgsWithAlias() { @Test void testFieldArgsWithSortable() { - TestFieldArgs field = TestFieldArgs. builder().name("sortable_field").sortable().build(); + TestFieldArgs field = TestFieldArgs.builder().name("sortable_field").sortable().build(); assertThat(field.isSortable()).isTrue(); assertThat(field.isUnNormalizedForm()).isFalse(); @@ -85,8 +85,7 @@ void testFieldArgsWithSortable() { @Test void testFieldArgsWithSortableAndUnnormalized() { - TestFieldArgs field = TestFieldArgs. builder().name("sortable_field").sortable().unNormalizedForm() - .build(); + TestFieldArgs field = TestFieldArgs.builder().name("sortable_field").sortable().unNormalizedForm().build(); assertThat(field.isSortable()).isTrue(); assertThat(field.isUnNormalizedForm()).isTrue(); @@ -94,29 +93,29 @@ void testFieldArgsWithSortableAndUnnormalized() { @Test void testFieldArgsWithNoIndex() { - TestFieldArgs field = TestFieldArgs. builder().name("no_index_field").noIndex().build(); + TestFieldArgs field = TestFieldArgs.builder().name("no_index_field").noIndex().build(); assertThat(field.isNoIndex()).isTrue(); } @Test void testFieldArgsWithIndexEmpty() { - TestFieldArgs field = TestFieldArgs. builder().name("index_empty_field").indexEmpty().build(); + TestFieldArgs field = TestFieldArgs.builder().name("index_empty_field").indexEmpty().build(); assertThat(field.isIndexEmpty()).isTrue(); } @Test void testFieldArgsWithIndexMissing() { - TestFieldArgs field = TestFieldArgs. builder().name("index_missing_field").indexMissing().build(); + TestFieldArgs field = TestFieldArgs.builder().name("index_missing_field").indexMissing().build(); assertThat(field.isIndexMissing()).isTrue(); } @Test void testFieldArgsWithAllOptions() { - TestFieldArgs field = TestFieldArgs. builder().name("full_field").as("alias").sortable() - .unNormalizedForm().noIndex().indexEmpty().indexMissing().build(); + TestFieldArgs field = TestFieldArgs.builder().name("full_field").as("alias").sortable().unNormalizedForm().noIndex() + .indexEmpty().indexMissing().build(); assertThat(field.getName()).isEqualTo("full_field"); assertThat(field.getAs()).hasValue("alias"); @@ -129,8 +128,8 @@ void testFieldArgsWithAllOptions() { @Test void testFieldArgsBuild() { - TestFieldArgs field = TestFieldArgs. builder().name("test_field").as("alias").sortable() - .unNormalizedForm().noIndex().indexEmpty().indexMissing().build(); + TestFieldArgs field = TestFieldArgs.builder().name("test_field").as("alias").sortable().unNormalizedForm().noIndex() + .indexEmpty().indexMissing().build(); CommandArgs commandArgs = new CommandArgs<>(StringCodec.UTF8); field.build(commandArgs); @@ -149,7 +148,7 @@ void testFieldArgsBuild() { @Test void testFieldArgsMinimalBuild() { - TestFieldArgs field = TestFieldArgs. builder().name("simple_field").build(); + TestFieldArgs field = TestFieldArgs.builder().name("simple_field").build(); CommandArgs commandArgs = new CommandArgs<>(StringCodec.UTF8); field.build(commandArgs); @@ -167,7 +166,7 @@ void testFieldArgsMinimalBuild() { @Test void testFieldArgsSortableWithoutUnnormalized() { - TestFieldArgs field = TestFieldArgs. builder().name("sortable_field").sortable().build(); + TestFieldArgs field = TestFieldArgs.builder().name("sortable_field").sortable().build(); CommandArgs commandArgs = new CommandArgs<>(StringCodec.UTF8); field.build(commandArgs); @@ -180,8 +179,8 @@ void testFieldArgsSortableWithoutUnnormalized() { @Test void testBuilderMethodChaining() { // Test that builder methods return the correct type for method chaining - TestFieldArgs field = TestFieldArgs. builder().name("chained_field").as("chained_alias").sortable() - .unNormalizedForm().noIndex().indexEmpty().indexMissing().build(); + TestFieldArgs field = TestFieldArgs.builder().name("chained_field").as("chained_alias").sortable().unNormalizedForm() + .noIndex().indexEmpty().indexMissing().build(); assertThat(field.getName()).isEqualTo("chained_field"); assertThat(field.getAs()).hasValue("chained_alias"); diff --git a/src/test/java/io/lettuce/core/search/arguments/GeoFieldArgsTest.java b/src/test/java/io/lettuce/core/search/arguments/GeoFieldArgsTest.java index eec3e5331f..f128772a88 100644 --- a/src/test/java/io/lettuce/core/search/arguments/GeoFieldArgsTest.java +++ b/src/test/java/io/lettuce/core/search/arguments/GeoFieldArgsTest.java @@ -26,7 +26,7 @@ class GeoFieldArgsTest { @Test void testDefaultGeoFieldArgs() { - GeoFieldArgs field = GeoFieldArgs. builder().name("location").build(); + GeoFieldArgs field = GeoFieldArgs.builder().name("location").build(); assertThat(field.getName()).isEqualTo("location"); assertThat(field.getFieldType()).isEqualTo("GEO"); @@ -40,7 +40,7 @@ void testDefaultGeoFieldArgs() { @Test void testGeoFieldArgsWithAlias() { - GeoFieldArgs field = GeoFieldArgs. builder().name("coordinates").as("location").build(); + GeoFieldArgs field = GeoFieldArgs.builder().name("coordinates").as("location").build(); assertThat(field.getName()).isEqualTo("coordinates"); assertThat(field.getAs()).hasValue("location"); @@ -49,7 +49,7 @@ void testGeoFieldArgsWithAlias() { @Test void testGeoFieldArgsWithSortable() { - GeoFieldArgs field = GeoFieldArgs. builder().name("position").sortable().build(); + GeoFieldArgs field = GeoFieldArgs.builder().name("position").sortable().build(); assertThat(field.getName()).isEqualTo("position"); assertThat(field.isSortable()).isTrue(); @@ -58,7 +58,7 @@ void testGeoFieldArgsWithSortable() { @Test void testGeoFieldArgsWithSortableAndUnnormalized() { - GeoFieldArgs field = GeoFieldArgs. builder().name("geo_point").sortable().unNormalizedForm().build(); + GeoFieldArgs field = GeoFieldArgs.builder().name("geo_point").sortable().unNormalizedForm().build(); assertThat(field.getName()).isEqualTo("geo_point"); assertThat(field.isSortable()).isTrue(); @@ -67,7 +67,7 @@ void testGeoFieldArgsWithSortableAndUnnormalized() { @Test void testGeoFieldArgsWithNoIndex() { - GeoFieldArgs field = GeoFieldArgs. builder().name("internal_location").noIndex().build(); + GeoFieldArgs field = GeoFieldArgs.builder().name("internal_location").noIndex().build(); assertThat(field.getName()).isEqualTo("internal_location"); assertThat(field.isNoIndex()).isTrue(); @@ -75,7 +75,7 @@ void testGeoFieldArgsWithNoIndex() { @Test void testGeoFieldArgsWithIndexEmpty() { - GeoFieldArgs field = GeoFieldArgs. builder().name("optional_location").indexEmpty().build(); + GeoFieldArgs field = GeoFieldArgs.builder().name("optional_location").indexEmpty().build(); assertThat(field.getName()).isEqualTo("optional_location"); assertThat(field.isIndexEmpty()).isTrue(); @@ -83,7 +83,7 @@ void testGeoFieldArgsWithIndexEmpty() { @Test void testGeoFieldArgsWithIndexMissing() { - GeoFieldArgs field = GeoFieldArgs. builder().name("nullable_location").indexMissing().build(); + GeoFieldArgs field = GeoFieldArgs.builder().name("nullable_location").indexMissing().build(); assertThat(field.getName()).isEqualTo("nullable_location"); assertThat(field.isIndexMissing()).isTrue(); @@ -91,8 +91,8 @@ void testGeoFieldArgsWithIndexMissing() { @Test void testGeoFieldArgsWithAllOptions() { - GeoFieldArgs field = GeoFieldArgs. builder().name("comprehensive_geo").as("geo").sortable() - .unNormalizedForm().noIndex().indexEmpty().indexMissing().build(); + GeoFieldArgs field = GeoFieldArgs.builder().name("comprehensive_geo").as("geo").sortable().unNormalizedForm().noIndex() + .indexEmpty().indexMissing().build(); assertThat(field.getName()).isEqualTo("comprehensive_geo"); assertThat(field.getAs()).hasValue("geo"); @@ -105,8 +105,8 @@ void testGeoFieldArgsWithAllOptions() { @Test void testGeoFieldArgsBuild() { - GeoFieldArgs field = GeoFieldArgs. builder().name("store_location").as("location").sortable() - .unNormalizedForm().indexEmpty().indexMissing().build(); + GeoFieldArgs field = GeoFieldArgs.builder().name("store_location").as("location").sortable().unNormalizedForm() + .indexEmpty().indexMissing().build(); CommandArgs commandArgs = new CommandArgs<>(StringCodec.UTF8); field.build(commandArgs); @@ -124,7 +124,7 @@ void testGeoFieldArgsBuild() { @Test void testGeoFieldArgsMinimalBuild() { - GeoFieldArgs field = GeoFieldArgs. builder().name("simple_geo").build(); + GeoFieldArgs field = GeoFieldArgs.builder().name("simple_geo").build(); CommandArgs commandArgs = new CommandArgs<>(StringCodec.UTF8); field.build(commandArgs); @@ -142,7 +142,7 @@ void testGeoFieldArgsMinimalBuild() { @Test void testGeoFieldArgsSortableWithoutUnnormalized() { - GeoFieldArgs field = GeoFieldArgs. builder().name("sortable_geo").sortable().build(); + GeoFieldArgs field = GeoFieldArgs.builder().name("sortable_geo").sortable().build(); CommandArgs commandArgs = new CommandArgs<>(StringCodec.UTF8); field.build(commandArgs); @@ -154,7 +154,7 @@ void testGeoFieldArgsSortableWithoutUnnormalized() { @Test void testGeoFieldArgsWithNoIndexOnly() { - GeoFieldArgs field = GeoFieldArgs. builder().name("no_index_geo").noIndex().build(); + GeoFieldArgs field = GeoFieldArgs.builder().name("no_index_geo").noIndex().build(); CommandArgs commandArgs = new CommandArgs<>(StringCodec.UTF8); field.build(commandArgs); @@ -169,8 +169,8 @@ void testGeoFieldArgsWithNoIndexOnly() { @Test void testBuilderMethodChaining() { // Test that builder methods return the correct type for method chaining - GeoFieldArgs field = GeoFieldArgs. builder().name("chained_geo").as("chained_alias").sortable() - .unNormalizedForm().noIndex().indexEmpty().indexMissing().build(); + GeoFieldArgs field = GeoFieldArgs.builder().name("chained_geo").as("chained_alias").sortable().unNormalizedForm() + .noIndex().indexEmpty().indexMissing().build(); assertThat(field.getName()).isEqualTo("chained_geo"); assertThat(field.getAs()).hasValue("chained_alias"); @@ -184,7 +184,7 @@ void testBuilderMethodChaining() { @Test void testGeoFieldArgsTypeSpecificBehavior() { // Test that geo fields don't have type-specific arguments beyond common ones - GeoFieldArgs field = GeoFieldArgs. builder().name("geo_field").build(); + GeoFieldArgs field = GeoFieldArgs.builder().name("geo_field").build(); CommandArgs commandArgs = new CommandArgs<>(StringCodec.UTF8); field.build(commandArgs); @@ -205,8 +205,7 @@ void testGeoFieldArgsTypeSpecificBehavior() { @Test void testGeoFieldArgsInheritedMethods() { // Test that inherited methods from FieldArgs work correctly - GeoFieldArgs field = GeoFieldArgs. builder().name("inherited_geo").noIndex().indexEmpty().indexMissing() - .build(); + GeoFieldArgs field = GeoFieldArgs.builder().name("inherited_geo").noIndex().indexEmpty().indexMissing().build(); assertThat(field.isNoIndex()).isTrue(); assertThat(field.isIndexEmpty()).isTrue(); diff --git a/src/test/java/io/lettuce/core/search/arguments/GeoshapeFieldArgsTest.java b/src/test/java/io/lettuce/core/search/arguments/GeoshapeFieldArgsTest.java index ebe2498849..86ecfb4b52 100644 --- a/src/test/java/io/lettuce/core/search/arguments/GeoshapeFieldArgsTest.java +++ b/src/test/java/io/lettuce/core/search/arguments/GeoshapeFieldArgsTest.java @@ -26,7 +26,7 @@ class GeoshapeFieldArgsTest { @Test void testDefaultGeoshapeFieldArgs() { - GeoshapeFieldArgs field = GeoshapeFieldArgs. builder().name("geometry").build(); + GeoshapeFieldArgs field = GeoshapeFieldArgs.builder().name("geometry").build(); assertThat(field.getName()).isEqualTo("geometry"); assertThat(field.getFieldType()).isEqualTo("GEOSHAPE"); @@ -41,7 +41,7 @@ void testDefaultGeoshapeFieldArgs() { @Test void testGeoshapeFieldArgsWithSpherical() { - GeoshapeFieldArgs field = GeoshapeFieldArgs. builder().name("shape").spherical().build(); + GeoshapeFieldArgs field = GeoshapeFieldArgs.builder().name("shape").spherical().build(); assertThat(field.getName()).isEqualTo("shape"); assertThat(field.getCoordinateSystem()).hasValue(GeoshapeFieldArgs.CoordinateSystem.SPHERICAL); @@ -49,7 +49,7 @@ void testGeoshapeFieldArgsWithSpherical() { @Test void testGeoshapeFieldArgsWithFlat() { - GeoshapeFieldArgs field = GeoshapeFieldArgs. builder().name("polygon").flat().build(); + GeoshapeFieldArgs field = GeoshapeFieldArgs.builder().name("polygon").flat().build(); assertThat(field.getName()).isEqualTo("polygon"); assertThat(field.getCoordinateSystem()).hasValue(GeoshapeFieldArgs.CoordinateSystem.FLAT); @@ -57,7 +57,7 @@ void testGeoshapeFieldArgsWithFlat() { @Test void testGeoshapeFieldArgsWithAlias() { - GeoshapeFieldArgs field = GeoshapeFieldArgs. builder().name("complex_geometry").as("geom").build(); + GeoshapeFieldArgs field = GeoshapeFieldArgs.builder().name("complex_geometry").as("geom").build(); assertThat(field.getName()).isEqualTo("complex_geometry"); assertThat(field.getAs()).hasValue("geom"); @@ -66,7 +66,7 @@ void testGeoshapeFieldArgsWithAlias() { @Test void testGeoshapeFieldArgsWithSortable() { - GeoshapeFieldArgs field = GeoshapeFieldArgs. builder().name("sortable_shape").sortable().build(); + GeoshapeFieldArgs field = GeoshapeFieldArgs.builder().name("sortable_shape").sortable().build(); assertThat(field.getName()).isEqualTo("sortable_shape"); assertThat(field.isSortable()).isTrue(); @@ -75,8 +75,8 @@ void testGeoshapeFieldArgsWithSortable() { @Test void testGeoshapeFieldArgsWithAllOptions() { - GeoshapeFieldArgs field = GeoshapeFieldArgs. builder().name("comprehensive_geoshape").as("shape").flat() - .sortable().unNormalizedForm().noIndex().indexEmpty().indexMissing().build(); + GeoshapeFieldArgs field = GeoshapeFieldArgs.builder().name("comprehensive_geoshape").as("shape").flat().sortable() + .unNormalizedForm().noIndex().indexEmpty().indexMissing().build(); assertThat(field.getName()).isEqualTo("comprehensive_geoshape"); assertThat(field.getAs()).hasValue("shape"); @@ -96,8 +96,8 @@ void testCoordinateSystemEnum() { @Test void testGeoshapeFieldArgsBuildWithSpherical() { - GeoshapeFieldArgs field = GeoshapeFieldArgs. builder().name("spherical_shape").as("shape").spherical() - .sortable().indexEmpty().build(); + GeoshapeFieldArgs field = GeoshapeFieldArgs.builder().name("spherical_shape").as("shape").spherical().sortable() + .indexEmpty().build(); CommandArgs commandArgs = new CommandArgs<>(StringCodec.UTF8); field.build(commandArgs); @@ -114,8 +114,8 @@ void testGeoshapeFieldArgsBuildWithSpherical() { @Test void testGeoshapeFieldArgsBuildWithFlat() { - GeoshapeFieldArgs field = GeoshapeFieldArgs. builder().name("flat_shape").as("cartesian").flat() - .sortable().unNormalizedForm().build(); + GeoshapeFieldArgs field = GeoshapeFieldArgs.builder().name("flat_shape").as("cartesian").flat().sortable() + .unNormalizedForm().build(); CommandArgs commandArgs = new CommandArgs<>(StringCodec.UTF8); field.build(commandArgs); @@ -132,7 +132,7 @@ void testGeoshapeFieldArgsBuildWithFlat() { @Test void testGeoshapeFieldArgsMinimalBuild() { - GeoshapeFieldArgs field = GeoshapeFieldArgs. builder().name("simple_geoshape").build(); + GeoshapeFieldArgs field = GeoshapeFieldArgs.builder().name("simple_geoshape").build(); CommandArgs commandArgs = new CommandArgs<>(StringCodec.UTF8); field.build(commandArgs); @@ -152,7 +152,7 @@ void testGeoshapeFieldArgsMinimalBuild() { @Test void testGeoshapeFieldArgsWithNoIndex() { - GeoshapeFieldArgs field = GeoshapeFieldArgs. builder().name("no_index_shape").noIndex().build(); + GeoshapeFieldArgs field = GeoshapeFieldArgs.builder().name("no_index_shape").noIndex().build(); assertThat(field.getName()).isEqualTo("no_index_shape"); assertThat(field.isNoIndex()).isTrue(); @@ -169,7 +169,7 @@ void testGeoshapeFieldArgsWithNoIndex() { @Test void testGeoshapeFieldArgsWithIndexEmpty() { - GeoshapeFieldArgs field = GeoshapeFieldArgs. builder().name("index_empty_shape").indexEmpty().build(); + GeoshapeFieldArgs field = GeoshapeFieldArgs.builder().name("index_empty_shape").indexEmpty().build(); assertThat(field.getName()).isEqualTo("index_empty_shape"); assertThat(field.isIndexEmpty()).isTrue(); @@ -177,8 +177,7 @@ void testGeoshapeFieldArgsWithIndexEmpty() { @Test void testGeoshapeFieldArgsWithIndexMissing() { - GeoshapeFieldArgs field = GeoshapeFieldArgs. builder().name("index_missing_shape").indexMissing() - .build(); + GeoshapeFieldArgs field = GeoshapeFieldArgs.builder().name("index_missing_shape").indexMissing().build(); assertThat(field.getName()).isEqualTo("index_missing_shape"); assertThat(field.isIndexMissing()).isTrue(); @@ -187,8 +186,8 @@ void testGeoshapeFieldArgsWithIndexMissing() { @Test void testBuilderMethodChaining() { // Test that builder methods return the correct type for method chaining - GeoshapeFieldArgs field = GeoshapeFieldArgs. builder().name("chained_geoshape").as("chained_alias") - .spherical().sortable().unNormalizedForm().noIndex().indexEmpty().indexMissing().build(); + GeoshapeFieldArgs field = GeoshapeFieldArgs.builder().name("chained_geoshape").as("chained_alias").spherical() + .sortable().unNormalizedForm().noIndex().indexEmpty().indexMissing().build(); assertThat(field.getName()).isEqualTo("chained_geoshape"); assertThat(field.getAs()).hasValue("chained_alias"); @@ -203,7 +202,7 @@ void testBuilderMethodChaining() { @Test void testGeoshapeFieldArgsTypeSpecificBehavior() { // Test that geoshape fields have their specific arguments and not others - GeoshapeFieldArgs field = GeoshapeFieldArgs. builder().name("geoshape_field").flat().build(); + GeoshapeFieldArgs field = GeoshapeFieldArgs.builder().name("geoshape_field").flat().build(); CommandArgs commandArgs = new CommandArgs<>(StringCodec.UTF8); field.build(commandArgs); @@ -224,8 +223,8 @@ void testGeoshapeFieldArgsTypeSpecificBehavior() { @Test void testGeoshapeFieldArgsInheritedMethods() { // Test that inherited methods from FieldArgs work correctly - GeoshapeFieldArgs field = GeoshapeFieldArgs. builder().name("inherited_geoshape").noIndex().indexEmpty() - .indexMissing().build(); + GeoshapeFieldArgs field = GeoshapeFieldArgs.builder().name("inherited_geoshape").noIndex().indexEmpty().indexMissing() + .build(); assertThat(field.isNoIndex()).isTrue(); assertThat(field.isIndexEmpty()).isTrue(); diff --git a/src/test/java/io/lettuce/core/search/arguments/NumericFieldArgsTest.java b/src/test/java/io/lettuce/core/search/arguments/NumericFieldArgsTest.java index a28c839a4d..e0e58b0fb3 100644 --- a/src/test/java/io/lettuce/core/search/arguments/NumericFieldArgsTest.java +++ b/src/test/java/io/lettuce/core/search/arguments/NumericFieldArgsTest.java @@ -26,7 +26,7 @@ class NumericFieldArgsTest { @Test void testDefaultNumericFieldArgs() { - NumericFieldArgs field = NumericFieldArgs. builder().name("price").build(); + NumericFieldArgs field = NumericFieldArgs.builder().name("price").build(); assertThat(field.getName()).isEqualTo("price"); assertThat(field.getFieldType()).isEqualTo("NUMERIC"); @@ -40,7 +40,7 @@ void testDefaultNumericFieldArgs() { @Test void testNumericFieldArgsWithAlias() { - NumericFieldArgs field = NumericFieldArgs. builder().name("product_price").as("price").build(); + NumericFieldArgs field = NumericFieldArgs.builder().name("product_price").as("price").build(); assertThat(field.getName()).isEqualTo("product_price"); assertThat(field.getAs()).hasValue("price"); @@ -49,7 +49,7 @@ void testNumericFieldArgsWithAlias() { @Test void testNumericFieldArgsWithSortable() { - NumericFieldArgs field = NumericFieldArgs. builder().name("rating").sortable().build(); + NumericFieldArgs field = NumericFieldArgs.builder().name("rating").sortable().build(); assertThat(field.getName()).isEqualTo("rating"); assertThat(field.isSortable()).isTrue(); @@ -58,8 +58,7 @@ void testNumericFieldArgsWithSortable() { @Test void testNumericFieldArgsWithSortableAndUnnormalized() { - NumericFieldArgs field = NumericFieldArgs. builder().name("score").sortable().unNormalizedForm() - .build(); + NumericFieldArgs field = NumericFieldArgs.builder().name("score").sortable().unNormalizedForm().build(); assertThat(field.getName()).isEqualTo("score"); assertThat(field.isSortable()).isTrue(); @@ -68,7 +67,7 @@ void testNumericFieldArgsWithSortableAndUnnormalized() { @Test void testNumericFieldArgsWithNoIndex() { - NumericFieldArgs field = NumericFieldArgs. builder().name("internal_id").noIndex().build(); + NumericFieldArgs field = NumericFieldArgs.builder().name("internal_id").noIndex().build(); assertThat(field.getName()).isEqualTo("internal_id"); assertThat(field.isNoIndex()).isTrue(); @@ -76,7 +75,7 @@ void testNumericFieldArgsWithNoIndex() { @Test void testNumericFieldArgsWithIndexEmpty() { - NumericFieldArgs field = NumericFieldArgs. builder().name("optional_value").indexEmpty().build(); + NumericFieldArgs field = NumericFieldArgs.builder().name("optional_value").indexEmpty().build(); assertThat(field.getName()).isEqualTo("optional_value"); assertThat(field.isIndexEmpty()).isTrue(); @@ -84,7 +83,7 @@ void testNumericFieldArgsWithIndexEmpty() { @Test void testNumericFieldArgsWithIndexMissing() { - NumericFieldArgs field = NumericFieldArgs. builder().name("nullable_field").indexMissing().build(); + NumericFieldArgs field = NumericFieldArgs.builder().name("nullable_field").indexMissing().build(); assertThat(field.getName()).isEqualTo("nullable_field"); assertThat(field.isIndexMissing()).isTrue(); @@ -92,7 +91,7 @@ void testNumericFieldArgsWithIndexMissing() { @Test void testNumericFieldArgsWithAllOptions() { - NumericFieldArgs field = NumericFieldArgs. builder().name("comprehensive_numeric").as("num").sortable() + NumericFieldArgs field = NumericFieldArgs.builder().name("comprehensive_numeric").as("num").sortable() .unNormalizedForm().noIndex().indexEmpty().indexMissing().build(); assertThat(field.getName()).isEqualTo("comprehensive_numeric"); @@ -106,8 +105,8 @@ void testNumericFieldArgsWithAllOptions() { @Test void testNumericFieldArgsBuild() { - NumericFieldArgs field = NumericFieldArgs. builder().name("amount").as("total_amount").sortable() - .unNormalizedForm().indexEmpty().indexMissing().build(); + NumericFieldArgs field = NumericFieldArgs.builder().name("amount").as("total_amount").sortable().unNormalizedForm() + .indexEmpty().indexMissing().build(); CommandArgs commandArgs = new CommandArgs<>(StringCodec.UTF8); field.build(commandArgs); @@ -125,7 +124,7 @@ void testNumericFieldArgsBuild() { @Test void testNumericFieldArgsMinimalBuild() { - NumericFieldArgs field = NumericFieldArgs. builder().name("simple_number").build(); + NumericFieldArgs field = NumericFieldArgs.builder().name("simple_number").build(); CommandArgs commandArgs = new CommandArgs<>(StringCodec.UTF8); field.build(commandArgs); @@ -143,7 +142,7 @@ void testNumericFieldArgsMinimalBuild() { @Test void testNumericFieldArgsSortableWithoutUnnormalized() { - NumericFieldArgs field = NumericFieldArgs. builder().name("sortable_number").sortable().build(); + NumericFieldArgs field = NumericFieldArgs.builder().name("sortable_number").sortable().build(); CommandArgs commandArgs = new CommandArgs<>(StringCodec.UTF8); field.build(commandArgs); @@ -155,7 +154,7 @@ void testNumericFieldArgsSortableWithoutUnnormalized() { @Test void testNumericFieldArgsWithNoIndexOnly() { - NumericFieldArgs field = NumericFieldArgs. builder().name("no_index_number").noIndex().build(); + NumericFieldArgs field = NumericFieldArgs.builder().name("no_index_number").noIndex().build(); CommandArgs commandArgs = new CommandArgs<>(StringCodec.UTF8); field.build(commandArgs); @@ -170,8 +169,8 @@ void testNumericFieldArgsWithNoIndexOnly() { @Test void testBuilderMethodChaining() { // Test that builder methods return the correct type for method chaining - NumericFieldArgs field = NumericFieldArgs. builder().name("chained_numeric").as("chained_alias") - .sortable().unNormalizedForm().noIndex().indexEmpty().indexMissing().build(); + NumericFieldArgs field = NumericFieldArgs.builder().name("chained_numeric").as("chained_alias").sortable() + .unNormalizedForm().noIndex().indexEmpty().indexMissing().build(); assertThat(field.getName()).isEqualTo("chained_numeric"); assertThat(field.getAs()).hasValue("chained_alias"); @@ -185,7 +184,7 @@ void testBuilderMethodChaining() { @Test void testNumericFieldArgsTypeSpecificBehavior() { // Test that numeric fields don't have type-specific arguments beyond common ones - NumericFieldArgs field = NumericFieldArgs. builder().name("numeric_field").build(); + NumericFieldArgs field = NumericFieldArgs.builder().name("numeric_field").build(); CommandArgs commandArgs = new CommandArgs<>(StringCodec.UTF8); field.build(commandArgs); diff --git a/src/test/java/io/lettuce/core/search/arguments/SearchArgsTest.java b/src/test/java/io/lettuce/core/search/arguments/SearchArgsTest.java index 1a2e87049b..3a16c2a3d8 100644 --- a/src/test/java/io/lettuce/core/search/arguments/SearchArgsTest.java +++ b/src/test/java/io/lettuce/core/search/arguments/SearchArgsTest.java @@ -28,7 +28,7 @@ class SearchArgsTest { @Test void testDefaultSearchArgs() { - SearchArgs args = SearchArgs. builder().build(); + SearchArgs args = SearchArgs. builder().build(); assertThat(args.isNoContent()).isFalse(); assertThat(args.isWithScores()).isFalse(); @@ -37,8 +37,7 @@ void testDefaultSearchArgs() { @Test void testSearchArgsWithOptions() { - SearchArgs args = SearchArgs. builder().noContent().withScores().withSortKeys() - .verbatim().build(); + SearchArgs args = SearchArgs. builder().noContent().withScores().withSortKeys().verbatim().build(); assertThat(args.isNoContent()).isTrue(); assertThat(args.isWithScores()).isTrue(); @@ -47,8 +46,8 @@ void testSearchArgsWithOptions() { @Test void testSearchArgsWithFields() { - SearchArgs args = SearchArgs. builder().inKey("key1").inKey("key2").inField("field1") - .inField("field2").returnField("title").returnField("content", "text").build(); + SearchArgs args = SearchArgs. builder().inKey("key1").inKey("key2").inField("field1").inField("field2") + .returnField("title").returnField("content", "text").build(); // Test that the args can be built without errors CommandArgs commandArgs = new CommandArgs<>(StringCodec.UTF8); @@ -63,8 +62,8 @@ void testSearchArgsWithFields() { @Test void testSearchArgsWithLimitAndTimeout() { - SearchArgs args = SearchArgs. builder().limit(10, 20).timeout(Duration.ofSeconds(5)) - .slop(2).inOrder().build(); + SearchArgs args = SearchArgs. builder().limit(10, 20).timeout(Duration.ofSeconds(5)).slop(2).inOrder() + .build(); CommandArgs commandArgs = new CommandArgs<>(StringCodec.UTF8); args.build(commandArgs); @@ -78,7 +77,7 @@ void testSearchArgsWithLimitAndTimeout() { @Test void testSearchArgsWithLanguageAndScoring() { - SearchArgs args = SearchArgs. builder().language(DocumentLanguage.ENGLISH) + SearchArgs args = SearchArgs. builder().language(DocumentLanguage.ENGLISH) .scorer(ScoringFunction.TF_IDF).build(); CommandArgs commandArgs = new CommandArgs<>(StringCodec.UTF8); @@ -91,8 +90,8 @@ void testSearchArgsWithLanguageAndScoring() { @Test void testSearchArgsWithParams() { - SearchArgs args = SearchArgs. builder().param("param1", "value1") - .param("param2", "value2").dialect(QueryDialects.DIALECT3).build(); + SearchArgs args = SearchArgs. builder().param("param1", "value1").param("param2", "value2") + .dialect(QueryDialects.DIALECT3).build(); CommandArgs commandArgs = new CommandArgs<>(StringCodec.UTF8); args.build(commandArgs); @@ -105,9 +104,9 @@ void testSearchArgsWithParams() { @Test void testSearchArgsWithSortBy() { - SortByArgs sortBy = SortByArgs. builder().attribute("score").descending().build(); + SortByArgs sortBy = SortByArgs.builder().attribute("score").descending().build(); - SearchArgs args = SearchArgs. builder().sortBy(sortBy).build(); + SearchArgs args = SearchArgs. builder().sortBy(sortBy).build(); CommandArgs commandArgs = new CommandArgs<>(StringCodec.UTF8); args.build(commandArgs); @@ -118,11 +117,10 @@ void testSearchArgsWithSortBy() { @Test void testSearchArgsWithHighlightAndSummarize() { - HighlightArgs highlight = HighlightArgs. builder().field("title").tags("", "") - .build(); + HighlightArgs highlight = HighlightArgs.builder().field("title").tags("", "").build(); - SearchArgs args = SearchArgs. builder().highlightArgs(highlight) - .summarizeField("content").summarizeFragments(3).summarizeLen(100).summarizeSeparator("...").build(); + SearchArgs args = SearchArgs. builder().highlightArgs(highlight).summarizeField("content") + .summarizeFragments(3).summarizeLen(100).summarizeSeparator("...").build(); CommandArgs commandArgs = new CommandArgs<>(StringCodec.UTF8); args.build(commandArgs); diff --git a/src/test/java/io/lettuce/core/search/arguments/TagFieldArgsTest.java b/src/test/java/io/lettuce/core/search/arguments/TagFieldArgsTest.java index 73487b3d85..a95e104383 100644 --- a/src/test/java/io/lettuce/core/search/arguments/TagFieldArgsTest.java +++ b/src/test/java/io/lettuce/core/search/arguments/TagFieldArgsTest.java @@ -26,7 +26,7 @@ class TagFieldArgsTest { @Test void testDefaultTagFieldArgs() { - TagFieldArgs field = TagFieldArgs. builder().name("category").build(); + TagFieldArgs field = TagFieldArgs.builder().name("category").build(); assertThat(field.getName()).isEqualTo("category"); assertThat(field.getFieldType()).isEqualTo("TAG"); @@ -37,7 +37,7 @@ void testDefaultTagFieldArgs() { @Test void testTagFieldArgsWithSeparator() { - TagFieldArgs field = TagFieldArgs. builder().name("tags").separator("|").build(); + TagFieldArgs field = TagFieldArgs.builder().name("tags").separator("|").build(); assertThat(field.getName()).isEqualTo("tags"); assertThat(field.getSeparator()).hasValue("|"); @@ -45,7 +45,7 @@ void testTagFieldArgsWithSeparator() { @Test void testTagFieldArgsWithCaseSensitive() { - TagFieldArgs field = TagFieldArgs. builder().name("status").caseSensitive().build(); + TagFieldArgs field = TagFieldArgs.builder().name("status").caseSensitive().build(); assertThat(field.getName()).isEqualTo("status"); assertThat(field.isCaseSensitive()).isTrue(); @@ -53,7 +53,7 @@ void testTagFieldArgsWithCaseSensitive() { @Test void testTagFieldArgsWithSuffixTrie() { - TagFieldArgs field = TagFieldArgs. builder().name("keywords").withSuffixTrie().build(); + TagFieldArgs field = TagFieldArgs.builder().name("keywords").withSuffixTrie().build(); assertThat(field.getName()).isEqualTo("keywords"); assertThat(field.isWithSuffixTrie()).isTrue(); @@ -61,8 +61,8 @@ void testTagFieldArgsWithSuffixTrie() { @Test void testTagFieldArgsWithAllOptions() { - TagFieldArgs field = TagFieldArgs. builder().name("complex_tags").as("tags").separator(";") - .caseSensitive().withSuffixTrie().sortable().unNormalizedForm().build(); + TagFieldArgs field = TagFieldArgs.builder().name("complex_tags").as("tags").separator(";").caseSensitive() + .withSuffixTrie().sortable().unNormalizedForm().build(); assertThat(field.getName()).isEqualTo("complex_tags"); assertThat(field.getAs()).hasValue("tags"); @@ -75,8 +75,8 @@ void testTagFieldArgsWithAllOptions() { @Test void testTagFieldArgsBuild() { - TagFieldArgs field = TagFieldArgs. builder().name("labels").as("tag_labels").separator(",") - .caseSensitive().withSuffixTrie().sortable().indexEmpty().build(); + TagFieldArgs field = TagFieldArgs.builder().name("labels").as("tag_labels").separator(",").caseSensitive() + .withSuffixTrie().sortable().indexEmpty().build(); CommandArgs commandArgs = new CommandArgs<>(StringCodec.UTF8); field.build(commandArgs); @@ -96,7 +96,7 @@ void testTagFieldArgsBuild() { @Test void testTagFieldArgsMinimalBuild() { - TagFieldArgs field = TagFieldArgs. builder().name("simple_tag").build(); + TagFieldArgs field = TagFieldArgs.builder().name("simple_tag").build(); CommandArgs commandArgs = new CommandArgs<>(StringCodec.UTF8); field.build(commandArgs); @@ -112,7 +112,7 @@ void testTagFieldArgsMinimalBuild() { @Test void testTagFieldArgsWithSeparatorOnly() { - TagFieldArgs field = TagFieldArgs. builder().name("pipe_separated").separator("|").build(); + TagFieldArgs field = TagFieldArgs.builder().name("pipe_separated").separator("|").build(); CommandArgs commandArgs = new CommandArgs<>(StringCodec.UTF8); field.build(commandArgs); @@ -126,7 +126,7 @@ void testTagFieldArgsWithSeparatorOnly() { @Test void testTagFieldArgsWithCaseSensitiveOnly() { - TagFieldArgs field = TagFieldArgs. builder().name("case_sensitive_tag").caseSensitive().build(); + TagFieldArgs field = TagFieldArgs.builder().name("case_sensitive_tag").caseSensitive().build(); CommandArgs commandArgs = new CommandArgs<>(StringCodec.UTF8); field.build(commandArgs); @@ -139,7 +139,7 @@ void testTagFieldArgsWithCaseSensitiveOnly() { @Test void testTagFieldArgsWithSuffixTrieOnly() { - TagFieldArgs field = TagFieldArgs. builder().name("suffix_trie_tag").withSuffixTrie().build(); + TagFieldArgs field = TagFieldArgs.builder().name("suffix_trie_tag").withSuffixTrie().build(); CommandArgs commandArgs = new CommandArgs<>(StringCodec.UTF8); field.build(commandArgs); @@ -153,10 +153,10 @@ void testTagFieldArgsWithSuffixTrieOnly() { @Test void testTagFieldArgsWithCustomSeparators() { // Test various separator characters - TagFieldArgs commaField = TagFieldArgs. builder().name("comma_tags").separator(",").build(); - TagFieldArgs pipeField = TagFieldArgs. builder().name("pipe_tags").separator("|").build(); - TagFieldArgs semicolonField = TagFieldArgs. builder().name("semicolon_tags").separator(";").build(); - TagFieldArgs spaceField = TagFieldArgs. builder().name("space_tags").separator(" ").build(); + TagFieldArgs commaField = TagFieldArgs.builder().name("comma_tags").separator(",").build(); + TagFieldArgs pipeField = TagFieldArgs.builder().name("pipe_tags").separator("|").build(); + TagFieldArgs semicolonField = TagFieldArgs.builder().name("semicolon_tags").separator(";").build(); + TagFieldArgs spaceField = TagFieldArgs.builder().name("space_tags").separator(" ").build(); assertThat(commaField.getSeparator()).hasValue(","); assertThat(pipeField.getSeparator()).hasValue("|"); @@ -167,8 +167,8 @@ void testTagFieldArgsWithCustomSeparators() { @Test void testBuilderMethodChaining() { // Test that builder methods return the correct type for method chaining - TagFieldArgs field = TagFieldArgs. builder().name("chained_tag").as("chained_alias").separator(":") - .caseSensitive().withSuffixTrie().sortable().noIndex().indexMissing().build(); + TagFieldArgs field = TagFieldArgs.builder().name("chained_tag").as("chained_alias").separator(":").caseSensitive() + .withSuffixTrie().sortable().noIndex().indexMissing().build(); assertThat(field.getName()).isEqualTo("chained_tag"); assertThat(field.getAs()).hasValue("chained_alias"); @@ -183,8 +183,7 @@ void testBuilderMethodChaining() { @Test void testTagFieldArgsInheritedMethods() { // Test that inherited methods from FieldArgs work correctly - TagFieldArgs field = TagFieldArgs. builder().name("inherited_tag").noIndex().indexEmpty().indexMissing() - .build(); + TagFieldArgs field = TagFieldArgs.builder().name("inherited_tag").noIndex().indexEmpty().indexMissing().build(); assertThat(field.isNoIndex()).isTrue(); assertThat(field.isIndexEmpty()).isTrue(); @@ -202,8 +201,7 @@ void testTagFieldArgsInheritedMethods() { @Test void testTagFieldArgsTypeSpecificBehavior() { // Test that tag fields have their specific arguments and not others - TagFieldArgs field = TagFieldArgs. builder().name("tag_field").separator(",").caseSensitive() - .withSuffixTrie().build(); + TagFieldArgs field = TagFieldArgs.builder().name("tag_field").separator(",").caseSensitive().withSuffixTrie().build(); CommandArgs commandArgs = new CommandArgs<>(StringCodec.UTF8); field.build(commandArgs); diff --git a/src/test/java/io/lettuce/core/search/arguments/TextFieldArgsTest.java b/src/test/java/io/lettuce/core/search/arguments/TextFieldArgsTest.java index 1fac43859d..4ffec69b8b 100644 --- a/src/test/java/io/lettuce/core/search/arguments/TextFieldArgsTest.java +++ b/src/test/java/io/lettuce/core/search/arguments/TextFieldArgsTest.java @@ -26,7 +26,7 @@ class TextFieldArgsTest { @Test void testDefaultTextFieldArgs() { - TextFieldArgs field = TextFieldArgs. builder().name("title").build(); + TextFieldArgs field = TextFieldArgs.builder().name("title").build(); assertThat(field.getName()).isEqualTo("title"); assertThat(field.getFieldType()).isEqualTo("TEXT"); @@ -38,36 +38,35 @@ void testDefaultTextFieldArgs() { @Test void testTextFieldArgsWithWeight() { - TextFieldArgs field = TextFieldArgs. builder().name("title").weight(2L).build(); + TextFieldArgs field = TextFieldArgs.builder().name("title").weight(2L).build(); assertThat(field.getWeight()).hasValue(2L); } @Test void testTextFieldArgsWithNoStem() { - TextFieldArgs field = TextFieldArgs. builder().name("title").noStem().build(); + TextFieldArgs field = TextFieldArgs.builder().name("title").noStem().build(); assertThat(field.isNoStem()).isTrue(); } @Test void testTextFieldArgsWithPhonetic() { - TextFieldArgs field = TextFieldArgs. builder().name("title") - .phonetic(TextFieldArgs.PhoneticMatcher.ENGLISH).build(); + TextFieldArgs field = TextFieldArgs.builder().name("title").phonetic(TextFieldArgs.PhoneticMatcher.ENGLISH).build(); assertThat(field.getPhonetic()).hasValue(TextFieldArgs.PhoneticMatcher.ENGLISH); } @Test void testTextFieldArgsWithSuffixTrie() { - TextFieldArgs field = TextFieldArgs. builder().name("title").withSuffixTrie().build(); + TextFieldArgs field = TextFieldArgs.builder().name("title").withSuffixTrie().build(); assertThat(field.isWithSuffixTrie()).isTrue(); } @Test void testTextFieldArgsWithAllOptions() { - TextFieldArgs field = TextFieldArgs. builder().name("content").as("text_content").weight(2L).noStem() + TextFieldArgs field = TextFieldArgs.builder().name("content").as("text_content").weight(2L).noStem() .phonetic(TextFieldArgs.PhoneticMatcher.FRENCH).withSuffixTrie().sortable().build(); assertThat(field.getName()).isEqualTo("content"); @@ -89,7 +88,7 @@ void testPhoneticMatcherValues() { @Test void testTextFieldArgsBuild() { - TextFieldArgs field = TextFieldArgs. builder().name("description").as("desc").weight(3L).noStem() + TextFieldArgs field = TextFieldArgs.builder().name("description").as("desc").weight(3L).noStem() .phonetic(TextFieldArgs.PhoneticMatcher.SPANISH).withSuffixTrie().sortable().unNormalizedForm().build(); CommandArgs commandArgs = new CommandArgs<>(StringCodec.UTF8); @@ -112,7 +111,7 @@ void testTextFieldArgsBuild() { @Test void testTextFieldArgsMinimalBuild() { - TextFieldArgs field = TextFieldArgs. builder().name("simple_text").build(); + TextFieldArgs field = TextFieldArgs.builder().name("simple_text").build(); CommandArgs commandArgs = new CommandArgs<>(StringCodec.UTF8); field.build(commandArgs); @@ -129,7 +128,7 @@ void testTextFieldArgsMinimalBuild() { @Test void testTextFieldArgsWithWeightOnly() { - TextFieldArgs field = TextFieldArgs. builder().name("weighted_field").weight(1L).build(); + TextFieldArgs field = TextFieldArgs.builder().name("weighted_field").weight(1L).build(); CommandArgs commandArgs = new CommandArgs<>(StringCodec.UTF8); field.build(commandArgs); @@ -144,8 +143,8 @@ void testTextFieldArgsWithWeightOnly() { @Test void testTextFieldArgsWithPhoneticOnly() { - TextFieldArgs field = TextFieldArgs. builder().name("phonetic_field") - .phonetic(TextFieldArgs.PhoneticMatcher.PORTUGUESE).build(); + TextFieldArgs field = TextFieldArgs.builder().name("phonetic_field").phonetic(TextFieldArgs.PhoneticMatcher.PORTUGUESE) + .build(); CommandArgs commandArgs = new CommandArgs<>(StringCodec.UTF8); field.build(commandArgs); @@ -161,7 +160,7 @@ void testTextFieldArgsWithPhoneticOnly() { @Test void testBuilderMethodChaining() { // Test that builder methods return the correct type for method chaining - TextFieldArgs field = TextFieldArgs. builder().name("chained_field").weight(2L).noStem() + TextFieldArgs field = TextFieldArgs.builder().name("chained_field").weight(2L).noStem() .phonetic(TextFieldArgs.PhoneticMatcher.ENGLISH).withSuffixTrie().sortable().as("alias").build(); assertThat(field.getName()).isEqualTo("chained_field"); @@ -176,8 +175,7 @@ void testBuilderMethodChaining() { @Test void testTextFieldArgsInheritedMethods() { // Test that inherited methods from FieldArgs work correctly - TextFieldArgs field = TextFieldArgs. builder().name("inherited_field").noIndex().indexEmpty() - .indexMissing().build(); + TextFieldArgs field = TextFieldArgs.builder().name("inherited_field").noIndex().indexEmpty().indexMissing().build(); assertThat(field.isNoIndex()).isTrue(); assertThat(field.isIndexEmpty()).isTrue(); diff --git a/src/test/java/io/lettuce/core/search/arguments/VectorFieldArgsTest.java b/src/test/java/io/lettuce/core/search/arguments/VectorFieldArgsTest.java index 75b0818754..9f08e33d43 100644 --- a/src/test/java/io/lettuce/core/search/arguments/VectorFieldArgsTest.java +++ b/src/test/java/io/lettuce/core/search/arguments/VectorFieldArgsTest.java @@ -26,7 +26,7 @@ class VectorFieldArgsTest { @Test void testDefaultVectorFieldArgs() { - VectorFieldArgs field = VectorFieldArgs. builder().name("embedding").build(); + VectorFieldArgs field = VectorFieldArgs.builder().name("embedding").build(); assertThat(field.getName()).isEqualTo("embedding"); assertThat(field.getFieldType()).isEqualTo("VECTOR"); @@ -42,7 +42,7 @@ void testDefaultVectorFieldArgs() { @Test void testVectorFieldArgsWithFlat() { - VectorFieldArgs field = VectorFieldArgs. builder().name("vector").flat().build(); + VectorFieldArgs field = VectorFieldArgs.builder().name("vector").flat().build(); assertThat(field.getName()).isEqualTo("vector"); assertThat(field.getAlgorithm()).hasValue(VectorFieldArgs.Algorithm.FLAT); @@ -50,7 +50,7 @@ void testVectorFieldArgsWithFlat() { @Test void testVectorFieldArgsWithHnsw() { - VectorFieldArgs field = VectorFieldArgs. builder().name("vector").hnsw().build(); + VectorFieldArgs field = VectorFieldArgs.builder().name("vector").hnsw().build(); assertThat(field.getName()).isEqualTo("vector"); assertThat(field.getAlgorithm()).hasValue(VectorFieldArgs.Algorithm.HNSW); @@ -58,39 +58,37 @@ void testVectorFieldArgsWithHnsw() { @Test void testVectorFieldArgsWithType() { - VectorFieldArgs field = VectorFieldArgs. builder().name("vector") - .type(VectorFieldArgs.VectorType.FLOAT32).build(); + VectorFieldArgs field = VectorFieldArgs.builder().name("vector").type(VectorFieldArgs.VectorType.FLOAT32).build(); assertThat(field.getAttributes()).containsEntry("TYPE", "FLOAT32"); } @Test void testVectorFieldArgsWithDimensions() { - VectorFieldArgs field = VectorFieldArgs. builder().name("vector").dimensions(128).build(); + VectorFieldArgs field = VectorFieldArgs.builder().name("vector").dimensions(128).build(); assertThat(field.getAttributes()).containsEntry("DIM", 128); } @Test void testVectorFieldArgsWithDistanceMetric() { - VectorFieldArgs field = VectorFieldArgs. builder().name("vector") - .distanceMetric(VectorFieldArgs.DistanceMetric.COSINE).build(); + VectorFieldArgs field = VectorFieldArgs.builder().name("vector").distanceMetric(VectorFieldArgs.DistanceMetric.COSINE) + .build(); assertThat(field.getAttributes()).containsEntry("DISTANCE_METRIC", "COSINE"); } @Test void testVectorFieldArgsWithCustomAttribute() { - VectorFieldArgs field = VectorFieldArgs. builder().name("vector").attribute("INITIAL_CAP", 1000) - .build(); + VectorFieldArgs field = VectorFieldArgs.builder().name("vector").attribute("INITIAL_CAP", 1000).build(); assertThat(field.getAttributes()).containsEntry("INITIAL_CAP", 1000); } @Test void testVectorFieldArgsWithMultipleAttributes() { - VectorFieldArgs field = VectorFieldArgs. builder().name("vector").attribute("BLOCK_SIZE", 512) - .attribute("M", 16).attribute("EF_CONSTRUCTION", 200).build(); + VectorFieldArgs field = VectorFieldArgs.builder().name("vector").attribute("BLOCK_SIZE", 512).attribute("M", 16) + .attribute("EF_CONSTRUCTION", 200).build(); assertThat(field.getAttributes()).containsEntry("BLOCK_SIZE", 512); assertThat(field.getAttributes()).containsEntry("M", 16); @@ -99,7 +97,7 @@ void testVectorFieldArgsWithMultipleAttributes() { @Test void testVectorFieldArgsWithAllFlatOptions() { - VectorFieldArgs field = VectorFieldArgs. builder().name("flat_vector").as("vector").flat() + VectorFieldArgs field = VectorFieldArgs.builder().name("flat_vector").as("vector").flat() .type(VectorFieldArgs.VectorType.FLOAT32).dimensions(256).distanceMetric(VectorFieldArgs.DistanceMetric.L2) .attribute("INITIAL_CAP", 2000).attribute("BLOCK_SIZE", 1024).sortable().build(); @@ -116,7 +114,7 @@ void testVectorFieldArgsWithAllFlatOptions() { @Test void testVectorFieldArgsWithAllHnswOptions() { - VectorFieldArgs field = VectorFieldArgs. builder().name("hnsw_vector").as("vector").hnsw() + VectorFieldArgs field = VectorFieldArgs.builder().name("hnsw_vector").as("vector").hnsw() .type(VectorFieldArgs.VectorType.FLOAT64).dimensions(512).distanceMetric(VectorFieldArgs.DistanceMetric.IP) .attribute("INITIAL_CAP", 5000).attribute("M", 32).attribute("EF_CONSTRUCTION", 400).attribute("EF_RUNTIME", 20) .attribute("EPSILON", 0.005).sortable().build(); @@ -159,7 +157,7 @@ void testAlgorithmEnum() { @Test void testVectorFieldArgsBuildFlat() { - VectorFieldArgs field = VectorFieldArgs. builder().name("test_vector").as("vector").flat() + VectorFieldArgs field = VectorFieldArgs.builder().name("test_vector").as("vector").flat() .type(VectorFieldArgs.VectorType.FLOAT32).dimensions(128).distanceMetric(VectorFieldArgs.DistanceMetric.COSINE) .attribute("INITIAL_CAP", 1000).attribute("BLOCK_SIZE", 512).sortable().build(); @@ -187,10 +185,9 @@ void testVectorFieldArgsBuildFlat() { @Test void testVectorFieldArgsBuildHnsw() { - VectorFieldArgs field = VectorFieldArgs. builder().name("hnsw_test").hnsw() - .type(VectorFieldArgs.VectorType.FLOAT64).dimensions(256).distanceMetric(VectorFieldArgs.DistanceMetric.L2) - .attribute("M", 16).attribute("EF_CONSTRUCTION", 200).attribute("EF_RUNTIME", 10).attribute("EPSILON", 0.01) - .build(); + VectorFieldArgs field = VectorFieldArgs.builder().name("hnsw_test").hnsw().type(VectorFieldArgs.VectorType.FLOAT64) + .dimensions(256).distanceMetric(VectorFieldArgs.DistanceMetric.L2).attribute("M", 16) + .attribute("EF_CONSTRUCTION", 200).attribute("EF_RUNTIME", 10).attribute("EPSILON", 0.01).build(); CommandArgs commandArgs = new CommandArgs<>(StringCodec.UTF8); field.build(commandArgs); @@ -217,7 +214,7 @@ void testVectorFieldArgsBuildHnsw() { @Test void testVectorFieldArgsMinimalBuild() { - VectorFieldArgs field = VectorFieldArgs. builder().name("simple_vector").build(); + VectorFieldArgs field = VectorFieldArgs.builder().name("simple_vector").build(); CommandArgs commandArgs = new CommandArgs<>(StringCodec.UTF8); field.build(commandArgs); @@ -237,7 +234,7 @@ void testVectorFieldArgsMinimalBuild() { @Test void testBuilderMethodChaining() { // Test that builder methods return the correct type for method chaining - VectorFieldArgs field = VectorFieldArgs. builder().name("chained_vector").as("chained_alias").flat() + VectorFieldArgs field = VectorFieldArgs.builder().name("chained_vector").as("chained_alias").flat() .type(VectorFieldArgs.VectorType.FLOAT32).dimensions(64).distanceMetric(VectorFieldArgs.DistanceMetric.IP) .attribute("INITIAL_CAP", 500).attribute("BLOCK_SIZE", 256).sortable().noIndex().build(); @@ -255,7 +252,7 @@ void testBuilderMethodChaining() { @Test void testVectorFieldArgsWithSvsVamana() { - VectorFieldArgs field = VectorFieldArgs. builder().name("vector").svsVamana().build(); + VectorFieldArgs field = VectorFieldArgs.builder().name("vector").svsVamana().build(); assertThat(field.getName()).isEqualTo("vector"); assertThat(field.getAlgorithm()).hasValue(VectorFieldArgs.Algorithm.SVS_VAMANA); @@ -263,8 +260,8 @@ void testVectorFieldArgsWithSvsVamana() { @Test void testSvsVamanaWithCompression() { - VectorFieldArgs field = VectorFieldArgs. builder().name("compressed_vector").svsVamana() - .attribute("COMPRESSION", "LVQ").build(); + VectorFieldArgs field = VectorFieldArgs.builder().name("compressed_vector").svsVamana().attribute("COMPRESSION", "LVQ") + .build(); assertThat(field.getAlgorithm()).hasValue(VectorFieldArgs.Algorithm.SVS_VAMANA); assertThat(field.getAttributes()).containsEntry("COMPRESSION", "LVQ"); @@ -272,8 +269,8 @@ void testSvsVamanaWithCompression() { @Test void testSvsVamanaWithLeanVecCompression() { - VectorFieldArgs field = VectorFieldArgs. builder().name("leanvec_vector").svsVamana() - .attribute("COMPRESSION", "LEANVEC").build(); + VectorFieldArgs field = VectorFieldArgs.builder().name("leanvec_vector").svsVamana().attribute("COMPRESSION", "LEANVEC") + .build(); assertThat(field.getAlgorithm()).hasValue(VectorFieldArgs.Algorithm.SVS_VAMANA); assertThat(field.getAttributes()).containsEntry("COMPRESSION", "LEANVEC"); @@ -281,31 +278,30 @@ void testSvsVamanaWithLeanVecCompression() { @Test void testSvsVamanaWithConstructionWindowSize() { - VectorFieldArgs field = VectorFieldArgs. builder().name("vector").svsVamana() - .attribute("CONSTRUCTION_WINDOW_SIZE", 128).build(); + VectorFieldArgs field = VectorFieldArgs.builder().name("vector").svsVamana().attribute("CONSTRUCTION_WINDOW_SIZE", 128) + .build(); assertThat(field.getAttributes()).containsEntry("CONSTRUCTION_WINDOW_SIZE", 128); } @Test void testSvsVamanaWithGraphMaxDegree() { - VectorFieldArgs field = VectorFieldArgs. builder().name("vector").svsVamana() - .attribute("GRAPH_MAX_DEGREE", 64).build(); + VectorFieldArgs field = VectorFieldArgs.builder().name("vector").svsVamana().attribute("GRAPH_MAX_DEGREE", 64).build(); assertThat(field.getAttributes()).containsEntry("GRAPH_MAX_DEGREE", 64); } @Test void testSvsVamanaWithSearchWindowSize() { - VectorFieldArgs field = VectorFieldArgs. builder().name("vector").svsVamana() - .attribute("SEARCH_WINDOW_SIZE", 100).build(); + VectorFieldArgs field = VectorFieldArgs.builder().name("vector").svsVamana().attribute("SEARCH_WINDOW_SIZE", 100) + .build(); assertThat(field.getAttributes()).containsEntry("SEARCH_WINDOW_SIZE", 100); } @Test void testSvsVamanaWithAllOptions() { - VectorFieldArgs field = VectorFieldArgs. builder().name("svs_vector").as("vector").svsVamana() + VectorFieldArgs field = VectorFieldArgs.builder().name("svs_vector").as("vector").svsVamana() .type(VectorFieldArgs.VectorType.FLOAT32).dimensions(384).distanceMetric(VectorFieldArgs.DistanceMetric.COSINE) .attribute("COMPRESSION", "LVQ").attribute("CONSTRUCTION_WINDOW_SIZE", 256).attribute("GRAPH_MAX_DEGREE", 64) .attribute("SEARCH_WINDOW_SIZE", 128).sortable().build(); @@ -325,9 +321,9 @@ void testSvsVamanaWithAllOptions() { @Test void testVectorFieldArgsBuildSvsVamana() { - VectorFieldArgs field = VectorFieldArgs. builder().name("svs_test").svsVamana() - .type(VectorFieldArgs.VectorType.FLOAT32).dimensions(128).distanceMetric(VectorFieldArgs.DistanceMetric.L2) - .attribute("COMPRESSION", "LVQ").attribute("CONSTRUCTION_WINDOW_SIZE", 256).attribute("GRAPH_MAX_DEGREE", 64) + VectorFieldArgs field = VectorFieldArgs.builder().name("svs_test").svsVamana().type(VectorFieldArgs.VectorType.FLOAT32) + .dimensions(128).distanceMetric(VectorFieldArgs.DistanceMetric.L2).attribute("COMPRESSION", "LVQ") + .attribute("CONSTRUCTION_WINDOW_SIZE", 256).attribute("GRAPH_MAX_DEGREE", 64) .attribute("SEARCH_WINDOW_SIZE", 128).build(); CommandArgs commandArgs = new CommandArgs<>(StringCodec.UTF8);