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 acceptbyte[]throughparam(String, byte[]), and result field values areFieldValues 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);
Basic Search¶
// 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.
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¶
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.
Vector Similarity Search¶
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);
Geospatial Search¶
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¶
Multi-Index Search¶
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¶
- Field Selection: Only index fields you actually search on
- Text Field Optimization: Use
NOOFFSETS,NOHL,NOFREQSfor memory savings - Numeric Fields: Use
NOINDEXfor sort-only fields - 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:
- Query Dialect: Use
DIALECT 2for new features - Vector Fields: Available in RediSearch 2.4+
- JSON Support: Requires RedisJSON module for versions of Redis before 8.0
- Aggregation Cursors: Available in RediSearch 2.0+