Redis Vector Search in Java: Vector Sets, the Query Engine, and What to Use When

Last updated
August 28, 2026

Redis vector search finds records by proximity in embedding space rather than by exact match — not "which records contain this term," but "which records are closest to this point." Since Redis 8 folded the Query Engine into the core server, you no longer need Redis Stack or a separate vector database to do it, and Valkey offers the same capability through the valkey-search module. Both are reachable from Java.

There are three ways to run it, and picking the wrong one costs a rewrite. This guide covers all three end to end with Redisson: which to choose, the query types each supports, server-side filtering, hybrid ranking, the memory arithmetic that decides whether a workload fits in RAM, and the byte-encoding detail that silently breaks most first Java implementations.

The examples that circulate for this are almost entirely Python and Node. Java teams end up translating redis-py snippets by hand, usually getting the encoding wrong on the first attempt and finding out only when the results look like noise. All code here is against Redisson 4.7.0 and has been checked against the current API.

Three Ways to Run Vector Search on Redis and Valkey

Valkey and Redis expose vector similarity through two server-side mechanisms, plus one framework-level abstraction on top.

AspectVector sets (RVectorSet)Query Engine (RSearch)Spring AI store
ModelNative data typeSecondary index over Hash or JSONDocument abstraction
SetupAdd vectors directlyDeclare an index schema firstConfiguration only
Query typesNearest neighborsKNN, range, hybridKNN with threshold
FilteringExpression over JSON attributesFull text, tag, numeric, geoPortable filter expressions
QuantizationQ8, BIN, NOQUANTNot availableNot exposed
AvailabilityRedis 8+ onlyRedis and ValkeyRedis and Valkey

The rule of thumb: if the query is "find things like this," use a vector set. If it's "find things like this, in this category, under this price, updated since last week," you want the Query Engine. If you are already building RAG inside a Spring application and would rather work at the document level than the vector level, use the Spring AI store.

And if your stack is Valkey rather than Redis, the choice is partly made for you — see Valkey and Redis: what actually differs below.

Vector Sets: Similarity Search Without an Index

A vector set behaves like a sorted set in which each element carries a high-dimensional vector instead of a scalar score. There is no index to declare — you add vectors and query them. At the protocol level it is a small command family: VADD to insert, VSIM to search, VCARD and VDIM for cardinality and dimensionality, VEMB to read a stored vector back. Redisson wraps all of them; the command-level detail is covered in our Redis vector set command reference for Java.

Note the package. The vector API lives in org.redisson.api.vector, not org.redisson.api.search — an easy mistake to make, and one that produces an unresolved-import error rather than anything informative.

import org.redisson.api.RVectorSet;
import org.redisson.api.vector.VectorAddArgs;
import org.redisson.api.vector.VectorSimilarArgs;
import org.redisson.api.vector.QuantizationType;
import org.redisson.client.protocol.ScoredEntry;
import org.redisson.client.protocol.ScoreAttributesEntry;

RVectorSet products = redisson.getVectorSet("product-embeddings");

products.add(VectorAddArgs.element("sku-501")
    .vector(0.90, 0.10, 0.80, 0.20)
    .quantization(QuantizationType.Q8));

The builder starts with the element name, then requires a vector — either as Double... or as a byte[] blob of 32-bit floats. Real embedding models return the latter, and the encoding is covered below.

Getting distances back, not just names

The most common early mistake is reaching for getSimilar() and then discovering you have no way to threshold the results. It returns element names only:

List<String> names = products.getSimilar(
    VectorSimilarArgs.element("sku-501").count(10));

Most applications need the score. Two other methods return it:

// names plus similarity scores
List<ScoredEntry<String>> scored = products.getSimilarEntries(
    VectorSimilarArgs.element("sku-501").count(10));

for (ScoredEntry<String> entry : scored) {
    String sku = entry.getValue();
    Double score = entry.getScore();
}

// names, scores, and the stored JSON attributes in one round trip
List<ScoreAttributesEntry<String>> full = products.getSimilarEntriesWithAttributes(
    VectorSimilarArgs.element("sku-501").count(10));

The third form matters more than it looks. Without it, a result list of ten SKUs becomes ten follow-up lookups to fetch the metadata you need to render them. With it, the whole result set arrives in one call — which, on a page that has a few milliseconds of budget, is the difference between one round trip and eleven.

Searching by an existing element's name rather than by a raw vector is worth preferring wherever the query object is already stored. A 1,536-dimension query vector is roughly 6 KB pushed over the network on every request; an element name is a few bytes.

Filtering happens on the server

Attributes attached to an element are not merely payload to filter in application code after the fact. VectorSimilarArgs.filter() takes an expression that Valkey or Redis evaluates during the search itself, so filtered-out candidates never cross the network.

Note the signature: attributes() takes an object and a JsonCodec. There is no single-string overload, so passing a hand-built JSON string on its own will not compile.

import org.redisson.codec.JacksonCodec;

products.add(VectorAddArgs.element("sku-777")
    .vector(0.88, 0.14, 0.76, 0.25)
    .attributes(Map.of("category", "footwear", "price", 129.99),
                new JacksonCodec<>(Map.class)));

List<String> matches = products.getSimilar(
    VectorSimilarArgs.element("sku-501")
        .count(5)
        .filter(".category == \"footwear\" && .price < 100"));

This is the single most valuable feature of vector sets and the one most often missed, because the filter is easy to mistake for a client-side convenience. Applying a predicate after retrieval is not equivalent: filter ten nearest neighbours down to three and you have three results, not the ten you asked for, and no way to recover the rest without querying again at a higher k.

HNSW, quantization, and the memory arithmetic

Two mechanisms decide what a vector workload costs, and both are worth understanding before you size an instance.

HNSW (Hierarchical Navigable Small World) organises vectors into a multi-layered graph so that a search can skip most of the dataset instead of scanning it. It is what keeps nearest-neighbour lookups sub-millisecond as the collection grows into the millions. It is also approximate: HNSW trades a small amount of recall for a large amount of speed, which is the right trade almost always and the wrong one when you need provable exactness.

Quantization is the memory lever, and on an in-memory store it is usually the constraint that decides whether a workload fits at all:

ModeMemoryRecallUse it when
Q8 (default)~4× reductionHighAlmost always — the default is the right default
BINUp to ~32× reductionNoticeably lowerCoarse first-pass filtering, re-ranked afterwards
NOQUANTFull 32-bit floatsExact storageRecall must be provably maximal

The arithmetic is worth doing on paper first. A million 1,536-dimension vectors at full 32-bit precision is roughly 6 GB of raw float data before graph overhead; at Q8 that drops to something closer to 1.5 GB. Since Valkey and Redis hold everything in RAM, that difference is often the difference between one node and four. Our guide to Redis as a vector database works through the sizing in more detail, including when a dedicated vector store starts to make sense instead.

Tuning recall and cost

Three parameters control the accuracy/latency trade, and all three have sensible defaults you should only override deliberately:

  • explorationFactor(int) — how widely the HNSW graph is traversed. Higher values raise recall and latency together. Available on both VectorAddArgs (at insert time) and VectorSimilarArgs (at query time).

  • epsilon(double) — widens the accepted distance boundary, letting marginally more distant candidates into the result set.

  • filterEffort(int) — how hard the engine works to satisfy a filter before giving up. Raise it when a selective filter is returning fewer results than you asked for.

For correctness checks, useLinearScan() forces an exhaustive comparison against every vector. It is far too slow for production but is the only way to establish ground truth when you suspect approximate search is returning the wrong neighbours — measure your recall against it once, before you start tuning blind.

The Query Engine: Vector Search over Hashes and JSON

When vectors are one attribute of records that also carry business metadata, declare a vector field in an RSearch index alongside ordinary fields. Redisson supports three index types through the same FieldIndex builder:

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

search.createIndex("product_idx", IndexOptions.defaults()
        .on(IndexType.JSON)
        .prefix(Arrays.asList("product:")),
        FieldIndex.text("$.description").as("description"),
        FieldIndex.tag("$.category").as("category"),
        FieldIndex.numeric("$.price").as("price"),
        FieldIndex.hnswVector("$.embedding").as("embedding")
                  .type(VectorTypeParam.Type.FLOAT32)
                  .dim(1536)
                  .distance(VectorDistParam.DistanceMetric.COSINE)
                  .m(16)
                  .efConstruction(200)
                  .efRuntime(10));

FieldIndex.flatVector() swaps in a brute-force index — exact results, linear query time, the right choice for small collections and for benchmarking. FieldIndex.svsVamanaVector() provides a third option. The dimension must match your embedding model's output exactly and cannot be changed without rebuilding the index.

Distance metric is not a tuning knob either. COSINE suits text embeddings, where direction carries meaning and magnitude does not; L2 suits image features and spatial data; IP ranks identically to cosine on vectors already normalized to unit length. Pairing a model with the wrong metric produces plausible-looking results that are quietly wrong, which is considerably harder to debug than an outright error.

For non-vector querying against the same indexes — text, tag and numeric fields, aggregation, spellcheck — see searching data stored in Redis from Java and the Redis Query Engine reference.

Encoding the query vector

This is where most Java implementations fail first. Query vectors travel as raw bytes — 32-bit IEEE 754 floats in little-endian order — not as a list of numbers. Java's ByteBuffer defaults to big-endian, so the byte order has to be set explicitly:

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

Get this wrong and nothing throws. The query succeeds and returns results that are simply meaningless, ranked by distance in a space your vectors don't occupy. If your similarity scores look like noise, check the byte order before anything else. The same encoding applies to Valkey, so this helper is portable across both engines.

Hybrid search: combining lexical and semantic ranking

Vector search alone loses exact-match precision — product codes, names, and specific terminology are exactly what lexical search is good at. Hybrid search runs both and fuses the rankings. Redisson exposes this through hybridSearch():

HybridSearchResult result = search.hybridSearch("product_idx",
    HybridQueryArgs.query("@description:running @category:{footwear}")
        .scoreAlias("text_score")
        .vectorSimilarity(
            VectorSimilarity.of("embedding", "query_vec")
                .nearestNeighbors(20)
                .yieldDistanceAs("vec_distance"))
        .params(Map.of("query_vec", toBytes(queryEmbedding)))
        .combine(Combine.reciprocalRankFusion().window(40))
        .load("description", "price", "vec_distance")
        .limit(0, 10));

long total = result.getTotal();
for (Map<String, String> doc : result.getResults()) {
    String description = doc.get("description");
    String distance = doc.get("vec_distance");
}

Two fusion strategies are available. Combine.reciprocalRankFusion() merges by rank position rather than raw score, which sidesteps the problem that a BM25 text score and a cosine distance aren't on comparable scales; tune it with window() and constant(). Combine.linear() blends the scores directly with alpha() and beta() weights, useful when you want explicit control over how much each side contributes.

Reciprocal rank fusion is the safer default. Linear combination requires that both score distributions be normalized and stable, and embedding model changes will shift them underneath you.

Three modifiers refine a KNN clause: yieldDistanceAs() exposes the raw distance as a named field you can load and sort on, efRuntime() widens the graph traversal for higher recall, and shardKRatio() controls how many candidates each shard returns relative to the requested k in a cluster. Note that each returns the base VectorSimilarity type, so exactly one can be applied per clause — pick the one your query actually needs rather than chaining them.

Range Queries: Searching by Distance Instead of Count

KNN always returns k results, even when nothing in the index is genuinely similar. For semantic caching and deduplication that's the wrong shape — you want "everything within this distance, possibly nothing":

HybridSearchResult nearby = search.hybridSearch("product_idx",
    HybridQueryArgs.query("*")
        .vectorSimilarity(
            VectorSimilarity.of("embedding", "query_vec")
                .range(0.15)
                .epsilon(0.01))
        .params(Map.of("query_vec", toBytes(queryEmbedding)))
        .limit(0, 50));

This is the query behind semantic caching for LLM applications: embed an incoming prompt, look for any previously answered prompt within a tight distance threshold, and return the cached response on a hit. An empty result is the correct answer when no prior prompt was close enough — a KNN query would have returned the nearest one regardless and quietly served the wrong cached answer.

Using Spring AI with Redisson

Teams building on Spring AI often assume adopting its Redis vector store means introducing a second Valkey or Redis client alongside Redisson. That is no longer the case. Redisson ships its own Spring AI VectorStore implementation, so RAG and semantic search run through the connection pool, cluster configuration, and codecs you already have.

<dependency>
    <groupId>pro.redisson</groupId>
    <artifactId>redisson-spring-ai-store-starter-10</artifactId>
    <version>4.7.0</version>
</dependency>
spring:
  ai:
    vectorstore:
      redisson:
        index-name: my-index
        prefix: "doc:"
        initialize-schema: true
        vector-algorithm: HNSW
        distance-metric: COSINE
        metadata-fields:
          - name: category
            type: TAG
          - name: year
            type: NUMERIC
@Autowired
VectorStore vectorStore;

vectorStore.add(List.of(
    new Document("Trail running shoe with rock plate",
                 Map.of("category", "footwear", "year", 2026))));

List<Document> results = vectorStore.similaritySearch(
    SearchRequest.builder()
        .query("shoes for rocky terrain")
        .topK(5)
        .similarityThreshold(0.7)
        .filterExpression("category == 'footwear' && year >= 2025")
        .build());

Portable filter expressions are translated into engine query syntax automatically, so category == 'footwear' && year >= 2025 becomes @category:{footwear} @year:[2025 inf]. Note that initialize-schema defaults to false and must be enabled for the index to be created for you. This integration is a Redisson PRO feature.

What You Build With It: Serving Recommendations

Most writing about recommendation systems is about training — collaborative filtering, matrix factorisation, how the embeddings get made. Comparatively little is about serving, which is the half that has a latency budget and the half Redis is for.

The split matters architecturally. Your offline pipeline produces embeddings on whatever cadence it runs at. The online path has perhaps 10–50 milliseconds to turn a user action into a ranked list of items, and it gets none of the batch system's luxuries. Vector search is the candidate-generation step in that online path: narrow a million-item catalogue to a few dozen plausible items fast enough that a re-ranking model still has budget left to score them.

RVectorSet catalog = redisson.getVectorSet("product-embeddings");

// Candidate generation: the item the user is looking at, in-stock only.
// Ask for more than you need — re-ranking will discard some.
List<ScoreAttributesEntry<String>> candidates =
    catalog.getSimilarEntriesWithAttributes(
        VectorSimilarArgs.element(currentSku)
            .count(50)
            .filter(".in_stock == true && .price < 200")
            .filterEffort(200));

Four things in that call are the whole design, and each is a decision people get wrong:

  • Query by element, not by vector. The item being viewed is already in the set. Sending its 6 KB embedding back to the server on every page view is bandwidth you do not need to spend.

  • Filter on the server. Business rules — in stock, region, price band, not-already-purchased — belong in the filter() expression. Applying them in Java after retrieval means over-fetching, and it means a request that legitimately has fifty candidates can come back with six.

  • Raise filterEffort when the filter is selective. This is the parameter people discover last. A narrow filter over a large catalogue will silently under-return unless the engine is told to work harder.

  • Fetch attributes in the same call. Fifty candidates with a follow-up lookup each is fifty round trips inside a budget that allows roughly one.

Two things vector search does not solve, and it is worth being clear about both. It has nothing to say about the cold-start problem — a new user has no history and a new item has no interactions, and the usual answer is a popularity fallback, which on Redis is an ordinary sorted-set leaderboard rather than anything vector-shaped. And it does not do business ranking: similarity is not margin, not inventory position, and not what you are contractually obliged to promote. Vector search gets you the candidate set; something else decides the order.

The same shape — narrow fast, then re-rank — recurs across most of what teams build on this. For the broader catalogue of patterns, see Redis use cases and how to build them in Java.

Valkey and Redis: What Actually Differs

The two engines reached vector search by different routes, and the difference is not cosmetic.

Redis 8 introduced vector sets as a native data type and folded the Query Engine — formerly the RediSearch module, and formerly a Redis Stack feature — into the core server. Both mechanisms are available from a stock Redis 8 install with no modules to add.

Valkey forked from Redis at 7.4, before vector sets existed, so it does not have them and will not gain them by inheritance. What it has instead is the official valkey-search module, available from Valkey 8.1.1, which provides approximate nearest-neighbour indexing over hash and JSON records. Redisson drives it through the same RSearch API used for the Redis Query Engine.

In practice: if you are on Valkey, the vector-set sections above do not apply to you and the Query Engine sections do. Code written against RSearch is portable between the two engines; code written against RVectorSet is not. If you expect to move between them, or you are not yet sure which you will run in production, that alone is a reason to choose RSearch. The wider engine comparison is in Valkey vs Redis.

Choosing an Approach

Reach for RVectorSet when similarity is the entire requirement and you're on Redis 8 or later — it needs no schema, supports quantization, and filters on the server through a compact expression syntax. Reach for RSearch when vectors are one field among several, when you need range queries or hybrid ranking, or when you're running Valkey. Reach for the Spring AI store when you're building RAG inside a Spring application and would rather work at the document level than the vector level.

All three run against Standalone, Sentinel, and Cluster deployments with only configuration changing, which matters when a prototype on one node has to become a clustered production service.

Redis Vector Search: Frequently Asked Questions

Do I Need Redis Stack for Vector Search?

No, not since Redis 8. The Query Engine that provides vector indexing — previously the RediSearch module, previously packaged in Redis Stack — is part of the core server from Redis 8 onward, and vector sets were added as a native data type in the same release. A stock Redis 8 install can do both. Older deployments on Redis 7 still need Redis Stack for the Query Engine, and cannot use vector sets at all.

Does Valkey Support Vector Search?

Yes, through the valkey-search module from Valkey 8.1.1 onward, which provides approximate nearest-neighbour search over hash and JSON records. Valkey does not have vector sets — it forked from Redis at 7.4, before that data type existed. From Java, valkey-search is driven through Redisson's RSearch API, the same one used for the Redis Query Engine, so that code is portable across both engines.

What Is the Difference Between Vector Sets and the Query Engine?

A vector set is a native data type holding elements and their vectors with no schema to declare; the Query Engine is a secondary index over Hash or JSON records where the vector is one indexed field among many. Vector sets support quantization and are simpler to operate. The Query Engine supports range queries, hybrid lexical-plus-semantic ranking, and filtering on full-text, tag, numeric and geo fields. Vector sets are Redis 8 only; the Query Engine works on both Redis and Valkey.

How Do I Encode a Query Vector in Java?

As raw bytes: 32-bit IEEE 754 floats in little-endian order. Java's ByteBuffer defaults to big-endian, so you must call .order(ByteOrder.LITTLE_ENDIAN) explicitly. Getting this wrong throws no exception — the query succeeds and returns results ranked in a space your vectors do not occupy. If similarity scores look like noise, check the byte order first.

How Much Memory Do Vectors Need in Redis?

Raw float storage is dimensions × 4 bytes per vector, before graph overhead — so a million 1,536-dimension vectors is roughly 6 GB at full precision. The default Q8 quantization cuts that by about 4×, and BIN by up to 32× at a real cost to recall. Because Redis and Valkey hold everything in RAM, quantization is usually the setting that decides whether a workload fits on one node.

Is Redis Fast Enough to Serve Recommendations?

For the candidate-generation step, yes — that is the workload HNSW indexing exists for, and it is why an in-memory store is used for the online path at all. Keep the design honest about what it covers: vector search narrows a large catalogue to a small candidate set within a few milliseconds, and something else re-ranks by business rules. It does not solve cold start, and similarity is not the same thing as ranking by margin or inventory.

Can I Use Redis Vector Search With Spring AI?

Yes. Redisson ships a Spring AI VectorStore implementation, so a Spring application can use Spring AI's document-level API without introducing a second Redis client alongside Redisson — the same connection pool, cluster configuration and codecs are reused. Set initialize-schema: true if you want the index created for you; it defaults to false. The integration is a Redisson PRO feature.

Should I Use a Dedicated Vector Database Instead?

It depends on whether vectors are the workload or part of it. If you already run Redis or Valkey for caching, sessions or queues, and your vector collection fits comfortably in memory, adding a second system buys you little. A dedicated vector database earns its place when the collection is large enough that RAM cost dominates, when you need index types or filtering semantics that are not available here, or when the vector workload is isolated enough that it does not benefit from sharing infrastructure. If you are weighing Elasticsearch specifically, Redis vs. Elasticsearch covers text relevance and hybrid retrieval alongside vectors.

Similar articles