What is a Redis Search?

Whether Redis is used as a cache, message broker, rate limiter, leaderboard system's backend, or traditional database, one feature is critical: search. The default Redis server features basic search options, but Redis Stack and Redis Enterprise include Redis Search (sometimes called RediSearch) and other helpful query tools. According to the official Redis documentation, Redis Search offers:

  • A rich query language
  • Incremental indexing on JSON and hash documents
  • Vector search
  • Full-text search
  • Geospatial queries
  • Aggregations

These search and query features make Redis an excellent choice for a wide range of apps, including a document database, a search engine, or a secondary index.

Redis Search features

Full-text searching is among Redis Search's features. It allows you to index and intelligently search text fields within Redis data, with support for stemming, stop words, and fuzzy matching. Full-text search supports ranking search results based on relevance. There's also a phrase search feature to find exact matches.

More advanced features include numeric range queries, which search and filter numeric fields within a given range. This is ideal for price ranges, rating systems, and other numerical data. There are also geospatial queries for indexing and searching geographic data (longitude and latitude values). With this feature, you can perform radius searches—finding locations within a specified radius of a starting point—and other location-based queries.

Redis Search also supports aggregations, which can group search results and perform statistical functions like count, sum, average, and min and max (minimum and maximum values).

Finally, vector search indexes embeddings—numeric representations of text, images, or audio generated by a machine learning model—and retrieves records by semantic similarity instead of exact matching. Rather than asking which documents contain a keyword, you ask which documents sit closest to a query vector in high-dimensional space. This is the capability behind semantic search, recommendation engines, and retrieval-augmented generation (RAG), and it's covered in detail below.

Getting started with Redis Search

To get started with Redis Search, you must first index your data. Indexes are most often defined over Redis JSON documents, whose fields can be addressed individually. Index schemas use JSON schemas defined by JSONPath expressions. The Redis syntax looks like this:

FT.CREATE {index_name} ON JSON SCHEMA {json_path} AS {attribute} {type}

Again, from the official Redis documentation, this command uses the above syntax to create an index that indexes the name, description, price, and image vector embedding of each JSON document that represents an inventory item:

FT.CREATE itemIdx ON JSON PREFIX 1 item: SCHEMA $.name AS name TEXT $.description as description TEXT $.price AS price NUMERIC $.embedding AS embedding VECTOR FLAT 6 DIM 4 DISTANCE_METRIC L2 TYPE FLOAT32

Here's a simple query that sorts the results by price:

FT.SEARCH myIndex "search terms" LIMIT 0 10 SORTBY price ASC

Working with Redis Search on Java

Although Redis doesn't natively support Java, Redisson simplifies the integration of Redis Search into Java applications. Redisson provides Java developer-friendly APIs that abstract the complexities of the Redis Search query language, making it simple to create indexes, build queries, handle results, and even map Java objects directly to Redis Search results.

Query execution

To create a RediSearch service with query execution for Redisson's RMap object:

RMap m = redisson.getMap("doc:1", new CompositeCodec(StringCodec.INSTANCE, redisson.getConfig().getCodec()));
m.put("v1", new SimpleObject("name1"));
m.put("v2", new SimpleObject("name2"));
RMap m2 = redisson.getMap("doc:2", new CompositeCodec(StringCodec.INSTANCE, redisson.getConfig().getCodec()));
m2.put("v1", new SimpleObject("name3"));
m2.put("v2", new SimpleObject("name4"));

RSearch s = redisson.getSearch();

// creates an index for documents with the prefix "doc."
s.createIndex("idx", IndexOptions.defaults()
                                  .on(IndexType.HASH)
                                  .prefix(Arrays.asList("doc:")),
                                  FieldIndex.text("v1"),
                                  FieldIndex.text("v2"));

SearchResult r = s.search("idx", "*", QueryOptions.defaults()
                                                  .returnAttributes(new ReturnAttribute("v1"), new ReturnAttribute("v2")));

SearchResult r = s.search("idx", "*", QueryOptions.defaults()
                                                  .filters(QueryFilter.geo("field")
                                                  .from(1, 1)
                                                  .radius(10, GeoUnit.FEET)));

And here's a code example for JSON objects using Redisson's RJsonBucket object:

public class TestClass {

     private List arr;
     private String value;

     public TestClass() {
     }

     public TestClass(List arr, String value) {
         this.arr = arr;
         this.value = value;
     }

     public List getArr() {
         return arr;
     }

     public TestClass setArr(List arr) {
         this.arr = arr;
         return this;
     }

     public String getValue() {
         return value;
     }

     public TestClass setValue(String value) {
         this.value = value;
         return this;
     }
}


RJsonBucket b = redisson.getJsonBucket("doc:1", new JacksonCodec<>(TestClass.class));
b.set(new TestClass(Arrays.asList(1, 2, 3), "hello"));

RSearch s = redisson.getSearch(StringCodec.INSTANCE);
// creates an index for documents with the prefix "doc"
s.createIndex("idx", IndexOptions.defaults()
                                 .on(IndexType.JSON)
                                 .prefix(Arrays.asList("doc:")),
                                 FieldIndex.numeric("$..arr").as("arr"),
                                 FieldIndex.text("$..value").as("val"));

SearchResult r = s.search("idx", "*", QueryOptions.defaults()
                                                  .returnAttributes(new ReturnAttribute("arr"), new ReturnAttribute("val")));
// total amount of found documents
long total = r.getTotal();
// found documents
List docs = r.getDocuments();

for (Document doc: docs) {
    String id = doc.getId();
    Map attrs = doc.getAttributes();
}

Vector search

Vector search is the feature that turns Redis Search from a text and numeric query engine into a semantic one. Instead of matching terms, it compares embeddings: fixed-length arrays of floating-point numbers produced by a machine learning model, where records with similar meaning end up close together in vector space. A query is converted into a vector by the same model, and Redis returns its nearest neighbors.

This is the capability that lets Redis serve as a vector database. To use it, you declare a VECTOR field in the index schema and tell Redis three things: which indexing algorithm to use, how many dimensions each vector has, and how to measure distance between two vectors.

FLAT vs. HNSW

Redis Search supports two vector indexing algorithms, and the choice is a straightforward trade between accuracy and latency:

  • FLAT performs an exhaustive, brute-force comparison against every vector in the index. It returns exact results with perfect recall, but query time grows linearly with the size of the dataset. Use it for smaller collections, for development and testing, and as the ground truth when benchmarking.
  • HNSW (Hierarchical Navigable Small World) builds a multi-layered graph that lets the engine skip most of the dataset while traversing toward the closest matches. Results are approximate rather than exact, but queries stay fast as the index grows into the millions of vectors. Use it in production at scale.

The distance metric matters just as much. COSINE measures the angle between two vectors and is the usual choice for text embeddings, where direction carries the meaning and magnitude does not. L2 measures straight-line Euclidean distance and suits image features or spatial data. IP (inner product) measures the dot product of two vectors; when embeddings are already normalized to unit length, it ranks results identically to cosine.

Defining a vector index in Java

Redisson's RSearch service exposes vector fields through the same FieldIndex builder used for text, numeric, and geospatial fields, so a vector index is declared alongside ordinary fields in a single schema. Here's a FLAT index over a JSON document that stores both a description and its embedding:

public class Product {
    private String description;
    private List<Float> embedding;
    // constructors, getters and setters omitted
}

RJsonBucket<Product> b = redisson.getJsonBucket("doc:1", new JacksonCodec<>(Product.class));
b.set(new Product("Trail running shoe", Arrays.asList(-0.019f, 0.017f, -0.028f, 0.041f)));

RSearch s = redisson.getSearch(StringCodec.INSTANCE);

s.createIndex("product_idx", IndexOptions.defaults()
                                 .on(IndexType.JSON)
                                 .prefix(Arrays.asList("doc:")),
                                 FieldIndex.text("$.description").as("description"),
                                 FieldIndex.flatVector("$.embedding")
                                           .type(VectorTypeParam.Type.FLOAT32)
                                           .dim(4)
                                           .distance(VectorDistParam.DistanceMetric.COSINE));

Switching to an approximate index means swapping a single builder method. FieldIndex.hnswVector() accepts the same type, dimension, and distance settings:

FieldIndex.hnswVector("$.embedding")
          .type(VectorTypeParam.Type.FLOAT32)
          .dim(4)
          .distance(VectorDistParam.DistanceMetric.COSINE)

Note that the dimension must exactly match the output size of whichever embedding model you're using—768 and 1536 are common—and it cannot be changed later without rebuilding the index.

Running a KNN query

Nearest-neighbor lookups use a dedicated query syntax. The expression before => is a pre-filter that narrows the candidate set, where * means every vector is considered, and the bracketed expression requests the k nearest neighbors of a parameterized query vector:

FT.SEARCH product_idx "*=>[KNN 10 @embedding $BLOB AS score]" PARAMS 2 BLOB "<binary_vector>" SORTBY score ASC LIMIT 0 10 DIALECT 2

Two details catch people out. Vector queries require query dialect 2 or higher, so DIALECT 2 is mandatory rather than optional. And because FT.SEARCH paginates to ten results by default, requesting more than ten neighbors also requires an explicit LIMIT 0 <k>, or you'll silently receive only the first ten.

The query vector must be sent as raw bytes rather than as a list of floats. In Java, that means packing the array in little-endian order before passing it as a query parameter:

public static byte[] toBytes(List<Float> vector) {
    ByteBuffer buffer = ByteBuffer.allocate(vector.size() * Float.BYTES)
                                  .order(ByteOrder.LITTLE_ENDIAN);
    for (float f : vector) {
        buffer.putFloat(f);
    }
    return buffer.array();
}

Pre-filters are what make vector search inside Redis genuinely useful, because they combine semantic similarity with the ordinary filtering you already index for. A query such as @category:{footwear}=>[KNN 5 @embedding $BLOB] restricts the search to one category before comparing vectors, which is difficult to do cleanly when embeddings live in a separate, dedicated vector store and your business data lives in Redis.

Vector sets: the alternative approach

Index-based vector search through RSearch isn't the only option. Redis 8 introduced vector sets, a native data type that stores embeddings directly and handles similarity search without a secondary index, exposed in Redisson through the RVectorSet API. Vector sets are simpler to work with when similarity is all you need; RSearch remains the better fit when vectors are one field among many in a richer query. Vector sets also support quantization, which cuts the memory cost of storing millions of embeddings substantially.

If your stack runs Valkey rather than Redis, note that Valkey forked before vector sets existed and doesn't support the data type. Valkey does provide approximate nearest neighbor search through its valkey-search module from version 8.1.1 onward.

Aggregation

The aggregation feature of Redis Search is also easily integrated into an RMap object:

RMap m = redisson.getMap("doc:1", new CompositeCodec(StringCodec.INSTANCE, redisson.getConfig().getCodec()));
m.put("v1", new SimpleObject("name1"));
m.put("v2", new SimpleObject("name2"));
RMap m2 = redisson.getMap("doc:2", new CompositeCodec(StringCodec.INSTANCE, redisson.getConfig().getCodec()));
m2.put("v1", new SimpleObject("name3"));
m2.put("v2", new SimpleObject("name4"));

RSearch s = redisson.getSearch();
// creates an index for documents with the prefix "doc"
s.createIndex("idx", IndexOptions.defaults()
                                 .on(IndexType.HASH)
                                 .prefix(Arrays.asList("doc:")),
FieldIndex.text("v1"),
FieldIndex.text("v2"));

AggregationResult r = s.aggregate("idx", "*", AggregationOptions.defaults().load("v1", "v2"));

// total amount of attributes
long total = r.getTotal();

// list of attributes mapped by attribute name
List> attrs = r.getAttributes();

Here's the corresponding code for the JSON object:

public class TestClass {

     private List arr;
     private String value;

     public TestClass() {
     }

     public TestClass(List arr, String value) {
         this.arr = arr;
         this.value = value;
     }

     public List getArr() {
         return arr;
     }

     public TestClass setArr(List arr) {
         this.arr = arr;
         return this;
     }

     public String getValue() {
         return value;
     }

     public TestClass setValue(String value) {
         this.value = value;
         return this;
     }
}

RJsonBucket b = redisson.getJsonBucket("doc:1", new JacksonCodec<>(TestClass.class));
// stores object in JSON format
b.set(new TestClass(Arrays.asList(1, 2, 3), "hello"));

RSearch s = redisson.getSearch(StringCodec.INSTANCE);
// creates an index for documents with the prefix "doc"
s.createIndex("idx", IndexOptions.defaults()
                                 .on(IndexType.JSON)
                                 .prefix(Arrays.asList("doc:")),
                                 FieldIndex.numeric("$..arr").as("arr"),
                                 FieldIndex.text("$..value").as("val"));

AggregationResult r = s.aggregate("idx", "*", AggregationOptions.defaults().load("arr", "val"));

// total amount of attributes
long total = r.getTotal();

// list of attributes mapped by attribute name
List> attrs = r.getAttributes();

Spellcheck

Redisson can also create a RediSearch service to implement spellcheck features. This RSearch object will check the spelling of three words (name, hockey, and stik), with the last word spelled wrong:

RSearch s = redisson.getSearch();

s.createIndex("idx", IndexOptions.defaults()
                                 .on(IndexType.HASH)
                                 .prefix(Arrays.asList("doc:")),
                                 FieldIndex.text("t1"),
                                 FieldIndex.text("t2"));

s.addDict("name", "hockey", "stik");

Map> res = s.spellcheck("idx", "Hocke sti", SpellcheckOptions.defaults().includedTerms("name"));

// returns misspelled terms and their score - "hockey", 0
Map m = res.get("hocke");

// returns misspelled terms and their score - "stik", 0
Map m = res.get("sti");
Similar terms