Skip to content

Redis Search support in Lettuce

Lettuce supports Redis Search starting from Lettuce 6.8.0.RELEASE.

Redis Search provides a rich query engine that enables full-text search, vector search, geospatial queries, and aggregations on Redis data. It transforms Redis into a powerful document database, vector database, secondary index, and search engine.

Info

Redis Search is available in Redis Open Source version 8.0, Redis Enterprise, and Redis Cloud. For older versions of Redis Open Source the functionality requires the RediSearch module to be loaded.

Warning

Redis Search commands are marked as @Experimental in Lettuce 6.8 and may undergo API changes in future releases. The underlying Redis Search functionality is stable and production-ready.

Core Concepts

Redis Search operates on indexes that define how your data should be searchable. An index specifies:

  • Data source: Which Redis keys to index (HASH or JSON documents)
  • Field definitions: Which fields are searchable and their types (TEXT, NUMERIC, TAG, GEO, VECTOR)
  • Search capabilities: Full-text search, exact matching, range queries, vector similarity

Codecs

Search operates on the bytes stored in Redis, so the connection's RedisCodec matters in two ways:

  • Keys go through the codec. Document keys, INKEYS, suggestion dictionary keys and the document ids in results are encoded and decoded with the key codec, exactly like every other Lettuce command.
  • Everything else is text. Index names, schema field identifiers and aliases, query text and the attribute references inside it, dictionary and synonym terms, and server-side tokens such as tag values are sent and read as String. Binary data has two dedicated paths: query parameters accept byte[] through param(String, byte[]), and result field values are FieldValues that expose their raw bytes.

Identity codecs (StringCodec, ByteArrayCodec) are fully supported. Codecs that transform values, such as CompressionCodec or CipherCodec, store bytes the server cannot search, so an index over such values matches nothing, for every client and not only Lettuce. Codecs that transform keys, such as a prefixing codec, are honoured only where a key is passed through the API: the index prefix and hash field identifiers are sent as written and must match the stored bytes, and nothing inside query text or a JSONPath is encoded. Note the asymmetry: INKEYS values and suggestion dictionary keys are keys and are encoded, while the PREFIX filter is not, so with such a codec you pass logical keys everywhere except the prefix. In all of these cases the failure mode is an empty result, not an error.

Getting Started

Basic Setup

RedisURI redisURI = RedisURI.Builder.redis("localhost").withPort(6379).build();
RedisClient redisClient = RedisClient.create(redisURI);
StatefulRedisConnection<String, String> connection = redisClient.connect();
RedisCommands<String, String> redis = connection.sync();
RediSearchCommands<String> search = redis;

Creating Your First Index

// Define searchable fields
List<FieldArgs> 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
String result = search.ftCreate("products-idx", fields);
// Returns: "OK"

Adding Data

// Add documents as Redis hashes
Map<String, String> 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<String, String> 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);
// Simple text search
SearchReply<String> results = search.ftSearch("products-idx", "wireless");

// Access results
System.out.println("Found " + results.getCount() + " documents");
for (SearchReply.SearchResult<String> result : results.getResults()) {
    System.out.println("Key: " + result.getId());
    System.out.println("Title: " + result.getFields().get("title").asString());
}

Field Types and Indexing

Text Fields

Full-text searchable fields with stemming, phonetic matching, and scoring.

TextFieldArgs titleField = TextFieldArgs.builder()
    .name("title")
    .weight(2)                                        // Boost importance in scoring
    .sortable()                                       // Enable sorting
    .noStem()                                         // Disable stemming
    .phonetic(TextFieldArgs.PhoneticMatcher.ENGLISH)  // Enable phonetic matching
    .build();

Numeric Fields

For range queries and sorting on numeric values.

NumericFieldArgs priceField = NumericFieldArgs.builder()
    .name("price")
    .sortable()         // Enable sorting
    .noIndex()          // Don't index for search, only for sorting
    .build();

Tag Fields

For exact matching and faceted search.

TagFieldArgs categoryField = TagFieldArgs.builder()
    .name("category")
    .separator(",")     // Custom separator for multiple tags
    .sortable()
    .build();

Geospatial Fields

For location-based queries.

GeoFieldArgs locationField = GeoFieldArgs.builder()
    .name("location")
    .build();

Vector Fields

For semantic search and similarity matching.

VectorFieldArgs embeddingField = VectorFieldArgs.builder()
    .name("embedding")
    .algorithm(VectorFieldArgs.Algorithm.FLAT)
    .type(VectorFieldArgs.VectorType.FLOAT32)
    .dimensions(768)
    .distanceMetric(VectorFieldArgs.DistanceMetric.COSINE)
    .build();

Advanced Index Configuration

Index with Custom Settings

CreateArgs createArgs = CreateArgs.builder()
    .on(CreateArgs.TargetType.HASH)            // Index HASH documents
    .withPrefix("product:")                    // Only index keys with this prefix
    .defaultLanguage(DocumentLanguage.ENGLISH) // Default language for text processing
    .languageField("lang")                     // Field containing document language
    .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
    .noFrequency()                             // Don't store term frequencies
    .stopWords(Arrays.asList("the", "a", "an")) // Custom stopwords
    .build();

String result = search.ftCreate("advanced-idx", createArgs, fields);

JSON Document Indexing

CreateArgs jsonArgs = CreateArgs.builder()
    .on(CreateArgs.TargetType.JSON)
    .withPrefix("user:")
    .build();

List<FieldArgs> 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);

Search Queries

Info

The Lettuce driver uses DIALECT 2 by default for all search queries, unless configured otherwise. This is the recommended approach for new applications as all other dialects are deprecated.

Query Syntax

Redis Search supports a rich query language:

// Simple term search
search.ftSearch("products-idx", "wireless");

// Phrase search
search.ftSearch("products-idx", "\"noise cancellation\"");

// Boolean operators
search.ftSearch("products-idx", "wireless AND headphones");
search.ftSearch("products-idx", "headphones OR earbuds");
search.ftSearch("products-idx", "audio -speakers");

// Field-specific search
search.ftSearch("products-idx", "@title:wireless @category:electronics");

// Wildcard and fuzzy search
search.ftSearch("products-idx", "wireles*");              // Prefix matching
search.ftSearch("products-idx", "%wireles%");             // Fuzzy matching

// Numeric range queries
search.ftSearch("products-idx", "@price:[100 200]");      // Inclusive range
search.ftSearch("products-idx", "@price:[(100 (200]");    // Exclusive bounds
search.ftSearch("products-idx", "@price:[100 +inf]");     // Open range

Advanced Search Options

SearchArgs<String> searchArgs = SearchArgs.<String>builder()
    .limit(0, 10)                                              // Pagination: offset 0, limit 10
    .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("<b>", "</b>")                              // Custom highlight tags
    .summarizeField("content")                                 // Summarize specific fields
    .summarizeFragments(3)                                     // Number of summary fragments
    .summarizeLen(50)                                          // Summary length
    .scorer(ScoringFunction.TF_IDF)                            // Scoring algorithm
    .withScores()                                              // Include document scores
    .noContent()                                               // Don't return document content
    .verbatim()                                                // Don't use stemming
    .withSortKeys()                                            // Include sort key values
    .inKey("product:1").inKey("product:2")                    // Search only specific keys
    .inField("title").inField("content")                      // Search only specific fields
    .slop(2)                                                   // Allow term reordering
    .timeout(Duration.ofSeconds(5))                             // Query timeout
    .param("category", "electronics")                          // Query parameter
    .dialect(QueryDialects.DIALECT2)                           // Query dialect version
    .build();

SearchReply<String> results = search.ftSearch("products-idx", "@title:$category", searchArgs);

Sort Keys

WITHSORTKEYS returns the value each document was sorted by, next to its id. This is mostly useful when merging sorted results from several indexes or shards. The server serializes the sort key as text: a $ prefix for textual attributes (normalized to lower case unless the attribute was declared UNF) and a # prefix for numeric attributes. A document without a value for the sorting attribute has no sort key, and without SORTBY every sort key is null.

SearchArgs<String> sortKeyArgs = SearchArgs.<String>builder()
    .sortBy(SortByArgs.builder().attribute("title").build())
    .withSortKeys()
    .build();

SearchReply<String> results = search.ftSearch("products-idx", "*", sortKeyArgs);
for (SearchReply.SearchResult<String> result : results.getResults()) {
    System.out.println(result.getId() + " sorted by " + result.getSortKey()); // product:2 sorted by $running shoes
}

Vector search enables semantic similarity matching using machine learning embeddings.

Creating a Vector Index

List<FieldArgs> vectorFields = Arrays.asList(
    TextFieldArgs.builder().name("title").build(),
    VectorFieldArgs.builder()
        .name("embedding")
        .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()
);

search.ftCreate("semantic-idx", vectorFields);

Adding Vector Data

A FLOAT32 vector field stores the raw binary form of the vector: exactly dimensions × 4 bytes, four little-endian bytes per dimension. The default String codec is UTF-8 and cannot carry such arbitrary bytes, so write the embedding through a binary-valued codec:

// Convert text to embeddings (using your ML model)
float[] embedding = textToEmbedding("wireless headphones");

// Pack the embedding as the blob the index expects: 4 little-endian bytes per FLOAT32 dimension
ByteBuffer embeddingBytes = ByteBuffer.allocate(embedding.length * Float.BYTES)
    .order(ByteOrder.LITTLE_ENDIAN);
for (float value : embedding) {
    embeddingBytes.putFloat(value);
}

// String keys and hash field names, binary values
RedisCommands<String, byte[]> binary = redisClient
    .connect(RedisCodec.of(StringCodec.UTF8, ByteArrayCodec.INSTANCE)).sync();

Map<String, byte[]> doc = new HashMap<>();
doc.put("title", "Wireless Headphones".getBytes(StandardCharsets.UTF_8));
doc.put("embedding", embeddingBytes.array());
binary.hmset("doc:1", doc);

Warning

Do not store the embedding as text (for example Arrays.toString(embedding)). The hash write succeeds, but the value is not a valid vector blob, so the indexer silently rejects the whole document — it never appears in any search result, and the only visible signal is the hash_indexing_failures counter in FT.INFO.

The query vector must be packed exactly like the stored vectors — same element type (FLOAT32) and little-endian byte order. The search itself can run on the regular String connection: param(String, byte[]) sends the vector bytes verbatim.

// 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<String> vectorArgs = SearchArgs.<String>builder()
    .param("query_vec", queryVectorBuffer.array())
    .sortBy(SortByArgs.builder().attribute("score").build())
    .returnField("title").returnField("score")
    .dialect(QueryDialects.DIALECT2)
    .build();

SearchReply<String> results = search.ftSearch("semantic-idx", vectorQuery, vectorArgs);

Search for documents based on geographic location.

Creating a Geo Index

List<FieldArgs> geoFields = Arrays.asList(
    TextFieldArgs.builder().name("name").build(),
    GeoFieldArgs.builder().name("location").build()
);

search.ftCreate("places-idx", geoFields);

Adding Geo Data

Map<String, String> 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

// Find places within a 5 km radius
SearchReply<String> results = search.ftSearch("places-idx",
    "@location:[40.7829 -73.9654 5 km]");

Aggregations

Aggregations provide powerful analytics capabilities for processing search results.

Basic Aggregation

// Simple aggregation without pipeline operations
AggregationReply<String> results = search.ftAggregate("products-idx", "*");

Advanced Aggregation Pipeline

AggregateArgs aggArgs = AggregateArgs.builder()
    // Load specific fields
    .load("title").load("price").load("category")

    // Apply transformations
    .apply("@price * 0.9", "discounted_price")

    // Filter results
    .filter("@price > 50")

    // Group by category with reducers
    .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", AggregateArgs.SortDirection.DESC)

    // Limit results
    .limit(0, 10)

    // Apply final transformations
    .apply("@total_value / @product_count", "calculated_avg")

    // Set query parameters
    .verbatim()
    .timeout(Duration.ofSeconds(5))
    .param("min_price", "50")
    .dialect(QueryDialects.DIALECT2)
    .build();

AggregationReply<String> aggResults = search.ftAggregate("products-idx", "*", aggArgs);

// Process aggregation results
for (SearchReply<String> reply : aggResults.getReplies()) {
    for (SearchReply.SearchResult<String> result : reply.getResults()) {
        System.out.println("Category: " + result.getFields().get("category").asString());
        System.out.println("Count: " + result.getFields().get("product_count").asString());
        System.out.println("Avg Price: " + result.getFields().get("avg_price").asString());
    }
}

Aggregation rows are not documents: SearchResult.getId() is null for every row of an AggregationReply, including rows read through a cursor. If you need the key of the underlying document, load it explicitly and read it from the fields:

AggregateArgs withKey = AggregateArgs.builder().load("__key").load("title").build();
AggregationReply<String> keyed = search.ftAggregate("products-idx", "*", withKey);
for (SearchReply.SearchResult<String> row : keyed.getReplies().get(0).getResults()) {
    System.out.println(row.getFields().get("__key").asString() + ": " + row.getFields().get("title").asString());
}

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:

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<String> 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:

for (SearchReply.SearchResult<String> group : collectResults.getReplies().get(0).getResults()) {
    for (FieldValue entry : group.getFields().get("top_products").asList()) {
        Map<String, FieldValue> 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:

AggregateArgs complexPipeline = AggregateArgs.builder()
    // First transformation
    .apply("@price * @quantity", "total_value")

    // First filter
    .filter("@total_value > 100")

    // First grouping
    .groupBy(AggregateArgs.GroupBy.of("category")
        .reduce(AggregateArgs.Reducer.sum("@total_value").as("category_revenue")))

    // First sort
    .sortBy("category_revenue", AggregateArgs.SortDirection.DESC)

    // Second transformation
    .apply("@category_revenue / 1000", "revenue_k")

    // Second filter
    .filter("@revenue_k > 5")

    // Second grouping (re-entrant)
    .groupBy(AggregateArgs.GroupBy.of("revenue_k")
        .reduce(AggregateArgs.Reducer.count().as("high_revenue_categories")))

    // Second sort (re-entrant)
    .sortBy("high_revenue_categories", AggregateArgs.SortDirection.DESC)

    .build();

Cursor-based Aggregation

For large result sets, use cursors to process data in batches:

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<String> firstBatch = search.ftAggregate("products-idx", "*", cursorArgs);
AggregationReply.Cursor cursor = firstBatch.getCursor().orElse(null);

// Read subsequent batches
while (cursor != null && cursor.getCursorId() != 0) {
    AggregationReply<String> nextBatch = search.ftCursorread("products-idx", cursor, 500);
    cursor = nextBatch.getCursor().orElse(null);

    // Process batch
    processResults(nextBatch);
}

Index Management

Index Information and Statistics

// Get index information
Map<String, Object> info = search.ftInfo("products-idx");
System.out.println("Index size: " + info.get("num_docs"));
System.out.println("Index memory: " + info.get("inverted_sz_mb") + " MB");

// List all indexes
List<String> indexes = search.ftList();

Index Aliases

// Create an alias for easier index management
search.ftAliasadd("products", "products-idx-v1");

// Update alias to point to new index version
search.ftAliasupdate("products", "products-idx-v2");

// Remove alias
search.ftAliasdel("products");

Modifying Indexes

// Add new fields to existing index
List<FieldArgs> newFields = Arrays.asList(
    TagFieldArgs.builder().name("brand").build(),
    NumericFieldArgs.builder().name("rating").build()
);

search.ftAlter("products-idx", false, newFields);  // false = scan existing docs
search.ftAlter("products-idx", true, newFields);   // true = skip initial scan

Index Cleanup

// Drop an index (keeps the data)
search.ftDropindex("products-idx");

// Drop an index and delete all associated documents
search.ftDropindex("products-idx", true);

Auto-completion and Suggestions

Redis Search provides auto-completion functionality for building search-as-you-type features.

Creating Suggestions

// Add suggestions to a dictionary
search.ftSugadd("autocomplete", "wireless headphones", 1.0);
search.ftSugadd("autocomplete", "bluetooth speakers", 0.8);
search.ftSugadd("autocomplete", "noise cancelling earbuds", 0.9);

// Add with additional options
SugAddArgs sugArgs = SugAddArgs.Builder.incr(); // Increment score if suggestion exists

search.ftSugadd("autocomplete", "gaming headset", 0.7, sugArgs);

Suggestion payloads (PAYLOAD on FT.SUGADD, WITHPAYLOADS on FT.SUGGET) are deprecated by Redis since RediSearch 2.0 and are deprecated in Lettuce as well; avoid them in new code.

Getting Suggestions

// Basic suggestion retrieval
List<Suggestion> suggestions = search.ftSugget("autocomplete", "head");

// Advanced suggestion options
SugGetArgs getArgs = SugGetArgs.Builder.fuzzy() // Enable fuzzy matching
    .max(5)         // Limit to 5 suggestions
    .withScores();  // Include scores

List<Suggestion> results = search.ftSugget("autocomplete", "head", getArgs);

for (Suggestion suggestion : results) {
    System.out.println("Suggestion: " + suggestion.getValue());
    System.out.println("Score: " + suggestion.getScore());
}

Managing Suggestions

// Get suggestion dictionary size
Long count = search.ftSuglen("autocomplete");

// Delete a suggestion
Boolean deleted = search.ftSugdel("autocomplete", "old suggestion");

Spell Checking

Redis Search can suggest corrections for misspelled queries.

// Basic spell check
SpellCheckResult corrections = search.ftSpellcheck("products-idx", "wireles hedphones");

// Advanced spell check with options
SpellCheckArgs spellArgs = SpellCheckArgs.Builder.distance(2) // Maximum Levenshtein distance
    .termsInclude("dictionary")                               // Include custom dictionary terms
    .termsExclude("stopwords")                                // Exclude stopword dictionary terms
    .dialect(2);

SpellCheckResult results = search.ftSpellcheck("products-idx", "wireles hedphones", spellArgs);

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() + ")");
    }
}

Dictionary Management

Manage custom dictionaries for spell checking and synonyms.

// Add terms to dictionary
search.ftDictadd("custom_dict", "smartphone", "tablet", "laptop");

// Remove terms from dictionary
search.ftDictdel("custom_dict", "outdated_term");

// Get all terms in dictionary
List<String> terms = search.ftDictdump("custom_dict");

Synonym Management

Create synonym groups for query expansion.

// Create synonym group
search.ftSynupdate("products-idx", "group1", "phone", "smartphone", "mobile");

// Update synonym group (replaces existing)
SynUpdateArgs synArgs = SynUpdateArgs.Builder.skipInitialScan(); // Don't reindex existing documents

search.ftSynupdate("products-idx", "group1", synArgs, "phone", "smartphone", "mobile", "cellphone");

// Get synonym groups
Map<String, List<String>> synonyms = search.ftSyndump("products-idx");

Query Profiling and Debugging

Query Explanation

Understand how Redis Search executes your queries:

// Basic query explanation
String plan = search.ftExplain("products-idx", "@title:wireless");

// Detailed explanation with dialect
ExplainArgs explainArgs = ExplainArgs.Builder.dialect(QueryDialects.DIALECT2);

String detailedPlan = search.ftExplain("products-idx", "@title:wireless", explainArgs);
System.out.println("Execution plan: " + detailedPlan);

Advanced Usage Patterns

Search across multiple indexes for federated queries:

// Create specialized indexes
search.ftCreate("products-idx", productFields);
search.ftCreate("reviews-idx", reviewFields);

// Search each index separately and combine results
SearchReply<String> productResults = search.ftSearch("products-idx", "wireless");
SearchReply<String> reviewResults = search.ftSearch("reviews-idx", "wireless");

// Combine and process results as needed

Index Versioning and Blue-Green Deployment

// Create new index version
search.ftCreate("products-idx-v2", newFields);

// Populate new index with updated data
// ... data migration logic ...

// Switch alias to new index
search.ftAliasupdate("products", "products-idx-v2");

// Clean up old index after verification
search.ftDropindex("products-idx-v1");

Conditional Indexing

// Index only documents matching certain criteria
CreateArgs conditionalArgs = CreateArgs.builder()
    .on(CreateArgs.TargetType.HASH)
    .withPrefix("product:")
    .filter("@status=='active'")  // Only index active products
    .build();

search.ftCreate("active-products-idx", conditionalArgs, fields);

Performance Optimization

Index Design Best Practices

  1. Field Selection: Only index fields you actually search on
  2. Text Field Optimization: Use NOOFFSETS, NOHL, NOFREQS for memory savings
  3. Numeric Fields: Use NOINDEX for sort-only fields
  4. Vector Fields: Choose appropriate algorithm (FLAT vs HNSW) based on use case
// 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")
    .build();

// Sort-only numeric field
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

// Use specific field searches instead of global search
search.ftSearch("idx", "@title:wireless");  // Better than "wireless"

// Use numeric ranges for better performance
search.ftSearch("idx", "@price:[100 200]"); // Better than "@price:>=100 @price:<=200"

// Limit result sets appropriately
SearchArgs<String> limitedArgs = SearchArgs.<String>builder()
    .limit(0, 20)   // Don't fetch more than needed
    .noContent()    // Skip content if only metadata needed
    .build();

Error Handling and Troubleshooting

Common Error Scenarios

try {
    search.ftCreate("existing-idx", fields);
} catch (RedisCommandExecutionException e) {
    if (e.getMessage().contains("Index already exists")) {
        // Handle index already exists
        System.out.println("Index already exists, skipping creation");
    } else {
        throw e;
    }
}

try {
    SearchReply<String> results = search.ftSearch("idx", "invalid:query[");
} catch (RedisCommandExecutionException e) {
    if (e.getMessage().contains("Syntax error")) {
        // Handle query syntax error
        System.out.println("Invalid query syntax: " + e.getMessage());
    }
}

Index Health Monitoring

// Monitor index statistics
Map<String, Object> info = search.ftInfo("products-idx");
long numDocs = (Long) info.get("num_docs");
double memoryMB = (Double) info.get("inverted_sz_mb");

if (memoryMB > 1000) {  // Alert if index uses > 1GB
    System.out.println("Warning: Index memory usage is high: " + memoryMB + " MB");
}

// Check for indexing errors
List<String> errors = (List<String>) info.get("hash_indexing_failures");
if (!errors.isEmpty()) {
    System.out.println("Indexing errors detected: " + errors);
}

Integration Examples

Spring Boot Integration

@Configuration
public class RedisSearchConfig {

    @Bean
    public RedisClient redisClient() {
        return RedisClient.create("redis://localhost:6379");
    }

    @Bean
    public RediSearchCommands<String> rediSearchCommands(RedisClient client) {
        return client.connect().sync();
    }
}

@Service
public class ProductSearchService {

    @Autowired
    private RediSearchCommands<String> search;

    public List<Product> searchProducts(String query, int page, int size) {
        SearchArgs<String> args = SearchArgs.<String>builder()
            .limit(page * size, size)
            .build();

        SearchReply<String> results = search.ftSearch("products-idx", query, args);
        return convertToProducts(results);
    }
}

Reactive Programming

// Using reactive commands
StatefulRedisConnection<String, String> connection = redisClient.connect();
RediSearchReactiveCommands<String> reactiveSearch = connection.reactive();

Mono<SearchReply<String>> searchMono = reactiveSearch.ftSearch("products-idx", "wireless");

searchMono.subscribe(results -> {
    System.out.println("Found " + results.getCount() + " results");
    results.getResults().forEach(result ->
        System.out.println("Product: " + result.getFields().get("title").asString())
    );
});

Migration and Compatibility

Upgrading from RediSearch 1.x

When migrating from older RediSearch versions:

  1. Query Dialect: Use DIALECT 2 for new features
  2. Vector Fields: Available in RediSearch 2.4+
  3. JSON Support: Requires RedisJSON module for versions of Redis before 8.0
  4. Aggregation Cursors: Available in RediSearch 2.0+
// Ensure compatibility with modern features
SearchArgs<String> modernArgs = SearchArgs.<String>builder()
    .dialect(QueryDialects.DIALECT2)  // Use latest dialect
    .build();