Redis Vector Search in Java: Vector Sets, the Query Engine, and What to Use When
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 four ways to run it from Java. Picking the wrong one of the two engine-level APIs costs a rewrite, which is why Redisson 4.8.0 added a vector store that runs the same code on either engine. This guide covers all four 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 targets Redisson 4.8.0.
Four Ways to Run Vector Search on Redis and Valkey
Valkey and Redis expose vector similarity through two server-side mechanisms. Redisson adds two abstractions on top: a vector store that works over either mechanism, and a Spring AI store for document-level RAG.
| Aspect | Vector sets (RVectorSet) | Query Engine (RSearch) | Vector store (RVectorStore) | Spring AI store module |
|---|---|---|---|---|
| Model | Native data type | Secondary index over Hash or JSON | Records: id, vector, filterable attributes, JSON document | Document abstraction |
| Setup | Add vectors directly | Declare an index schema first | An options object picks the engine; text is embedded for you | Configuration only |
| Query types | Nearest neighbors | KNN, range, hybrid | Nearest neighbors with a similarity cutoff, or an exact scan | KNN with threshold |
| Filtering | Expression over JSON attributes | Full text, tag, numeric, geo | Typed filter tree, compiled for either engine | Portable filter expressions |
| Quantization | Q8, BIN, NOQUANT | Not available | Q8, BIN, NOQUANT on vector sets | Not exposed |
| Availability | Redis 8+ only | Redis and Valkey | Redis; Valkey through the Query Engine | Redis 8+ or Redis Stack |
| Edition | Community | Community | PRO | PRO |
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 want text in and matches out without handling vectors yourself, or you are not ready to commit to an engine, use the vector store. If you are already building RAG inside a Spring application and would rather work at the document level than the vector level, use a 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:
| Mode | Memory | Recall | Use it when |
|---|---|---|---|
Q8 (default) | ~4× reduction | High | Almost always — the default is the right default |
BIN | Up to ~32× reduction | Noticeably lower | Coarse first-pass filtering, re-ranked afterwards |
NOQUANT | Full 32-bit floats | Exact storage | Recall 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 bothVectorAddArgs(at insert time) andVectorSimilarArgs(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. Redisson 4.8.0 also packages the whole pattern as RSemanticCache, built on the vector store below, which adds one thing a hand-built cache rarely has: it removes the cached answers built from a document when that document changes.
One API for Both Engines: RVectorStore
RVectorSet and RSearch stay close to their commands. That is their strength, and it is also why code written for one does not run on the other. Redisson 4.8.0 adds RVectorStore, part of the Redisson AI modules in Redisson PRO. It stores records (an id, the vector of a text, filterable attributes and an optional JSON document) and searches them by similarity. Text goes in, and the store embeds it through its embedding model, so one call writes and one call searches.
The options object you build picks the engine. Everything after that line is the same code:
REmbeddingModel model = OpenAiEmbeddingModel.create(
OpenAiEmbeddingOptions.model("text-embedding-3-small")
.apiKey(System.getenv("OPENAI_API_KEY"))
.nativeDimensions(1536),
RedissonAI.of(redisson).getMeterRegistry());
RVectorStore<Product> store = RedissonAI.of(redisson).getVectorStore(
VectorSetStoreOptions.name("products")
.embeddingModel(model)
.payloadType(Product.class)
.payloadCodec(new JacksonCodec<>(Product.class))
.filterableSchema(Map.of(
"category", FieldType.TAG,
"price", FieldType.NUMERIC)));
store.add(VectorRecord.of("sku-777", "Trail running shoe with rock plate")
.filterable("category", "footwear")
.filterable("price", 129.99)
.payload(product));
List<VectorMatch<Product>> hits = store.search(
SearchQuery.text("shoes for rocky terrain")
.k(5)
.minSimilarity(0.75)
.filter(VectorFilter.and(
VectorFilter.eq("category", "footwear"),
VectorFilter.lt("price", 200))));
To run the same store on the Query Engine, which is the only choice on Valkey, swap the options object. The add and search calls do not change:
RVectorStore<Product> store = RedissonAI.of(redisson).getVectorStore(
RediSearchStoreOptions.hnsw("products")
.embeddingModel(model)
.payloadType(Product.class)
.payloadCodec(new JacksonCodec<>(Product.class))
.filterableSchema(Map.of(
"category", FieldType.TAG,
"price", FieldType.NUMERIC)));
Imports are omitted. The artifacts are redisson-ai-core plus an embedding module such as redisson-embedding-openai, all under the pro.redisson group. Four details are worth knowing before you build on it:
Scores mean the same thing on both engines. Vector sets report a similarity and the Query Engine reports a cosine distance, so a raw threshold would mean opposite things on the two. On a cosine store, every match carries a normalized similarity,
(1 + cos) / 2, andminSimilarityapplies on that scale. The0.75above keeps matches with a cosine similarity of 0.5 or more. Convert any threshold you bring from a tool that uses raw cosine.The engine is fixed when the store is created. There is no automatic selection and no fallback. Moving a store to the other engine means building a new store and adding the records again, and the store keeps the vector, not the text. Keep the text in the document if you may ever need to re-embed it.
A selective filter can return fewer than k results on vector sets. This is the
filterEffortbehaviour described above. In the Redisson documentation's measurement, a filter matching 20 of 20,000 records returned two results at the default effort and ten withfilterEffort(200000). The Query Engine applies the filter before ranking, so it returns k results whenever k matching records exist.What a server cannot do is declared, not guessed.
capabilities()reports whether the connected server supports range queries, text and geo filters, aliases and enumeration. Onvalkey-searchall four are absent, and a call that needs one throwsUnsupportedByBackendExceptioninstead of returning a partial answer.
For a multi-tenant store, a filterPolicy adds a predicate to every filtered operation that a caller cannot remove, which is safer than trusting every query to carry the right filter. It is enforced by Redisson, not by the server, so anything that talks to Redis directly bypasses it.
The vector store does not replace the two engine-level APIs. This release has no hybrid search in it, so for hybrid ranking stay with RSearch, or reach the underlying object through getNativeClient(). And vector sets shipped in Redis 8.0 labelled as a preview: test failover against your own workload before relying on them in a primary-replica topology.
Using Spring AI or LangChain4j with Redisson
Teams building on Spring AI often assume adopting its Redis vector store means introducing a second Valkey or Redis client alongside Redisson. It doesn't. Redisson ships two Spring AI integrations, both in Redisson PRO, and both run RAG and semantic search through the connection pool, cluster configuration, and codecs you already have:
The Spring AI vector store module (
redisson-spring-ai-store-*) is a Spring Boot starter configured fromapplication.yaml. It runs on the Query Engine and needs Redis 8 or Redis Stack. Use the-20artifacts for Spring AI 2.0 and the-10artifacts for Spring AI 1.0.The Redisson AI adapter (
redisson-store-spring-ai, Spring AI 2.0) wraps anRVectorStore. It has no starter, but it runs on vector sets or the Query Engine, Valkey included, and the same store can also be used from plain Java or LangChain4j.
The rule: if you want Spring Boot auto-configuration, use the module. If you are on Valkey, or want the store outside Spring AI too, use the adapter. The module looks like this:
<dependency>
<groupId>pro.redisson</groupId>
<artifactId>redisson-spring-ai-store-starter-20</artifactId>
<version>4.8.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. Both integrations are Redisson PRO features.
The Redisson AI adapter
Both integrations ship a class named RedissonVectorStore, so check the import before copying code between them. The adapter's class is org.redisson.spring.ai.RedissonVectorStore, built over an RVectorStore<TextPayload>:
RVectorStore<TextPayload> backing = RedissonAI.of(redisson).getVectorStore(
RediSearchStoreOptions.hnsw("docs")
.payloadType(TextPayload.class)
.payloadCodec(new JacksonCodec<>(TextPayload.class))
.embeddingModel(model)
.filterableSchema(Map.of(
"category", FieldType.TAG,
"year", FieldType.NUMERIC)));
VectorStore vectorStore = new org.redisson.spring.ai.RedissonVectorStore(
backing, false, RedissonAI.of(redisson).getMeterRegistry());
Three differences matter when you move to it. Embedding is done by the Redisson store's own model, not by a Spring AI EmbeddingModel bean; if your model is already configured in Spring AI, the redisson-embedding-springai bridge wraps it. Document.getScore() is the normalized similarity, so similarityThreshold(0.7) keeps matches with a cosine similarity of 0.4 or more, which is looser than the same number in Spring AI's own SimpleVectorStore. And leave the second constructor argument, allowUnboundedDelete, at false: Spring AI has no notion of a bounded filter, and a stray != in a delete would otherwise empty the store.
LangChain4j
The LangChain4j counterpart is RedissonEmbeddingStore in redisson-store-langchain4j, compiled for LangChain4j 1.18.x. It implements EmbeddingStore<TextSegment> over the same kind of RVectorStore<TextPayload>. In LangChain4j the caller embeds, so every write and search arrives with a vector already computed, and the store's own model is never called on that path. Use the same embedding model on both sides: the adapter checks the vector width, but it cannot detect a different model of the same width, and that produces plausible, wrong rankings. Scores are the same normalized similarity, which matches LangChain4j's RelevanceScore.fromCosineSimilarity. Both adapters are covered in the Redisson AI documentation.
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
filterEffortwhen 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, and RVectorStore runs on it through its Query Engine backend, with capabilities() reporting what valkey-search lacks.
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. Code written against RVectorStore is portable except for the line that creates the store, and on Valkey that line must choose the Query Engine. If you expect to move between engines, or you are not yet sure which you will run in production, that alone is a reason to avoid RVectorSet. On the Spring side, the Spring AI vector store module documents Redis 8 and Redis Stack only; on Valkey, use the Redisson AI adapter. 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 RVectorStore when you want text in and matches out, one filter syntax for both engines, and thresholds that survive a change of engine. Reach for a Spring AI store when you're building RAG inside a Spring application and would rather work at the document level than the vector level: the module for Spring Boot auto-configuration, the Redisson AI adapter for Valkey or for a store you also use outside Spring AI.
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. Redisson's RVectorStore also runs on it, through its Query Engine backend.
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.
Can I Switch Between Vector Sets and the Query Engine Later?
Not in place. With Redisson's RVectorStore, the add, search and filter code is identical on both engines, and scores are normalized, so a similarity threshold survives the move. But the engine is fixed when the store is created, so switching means building a new store and adding the records again. The store keeps vectors, not text, so keep the text in each record's document if you may need to re-embed it.
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, in two ways, and neither needs a second Redis client alongside Redisson. The Spring AI vector store module is a Spring Boot starter on the Query Engine for Redis 8 or Redis Stack; set initialize-schema: true if you want the index created for you, since it defaults to false. The Redisson AI adapter wraps an RVectorStore for Spring AI 2.0, runs on vector sets or the Query Engine, Valkey included, and has no starter. Both reuse Redisson's connection pool, cluster configuration and codecs, and both are Redisson PRO features.
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
- Redis Vector Database: Vector Search and RAG in Java
- Redis Vector Set Commands in Java: VADD, VSIM and RVectorSet
- How to Search Data Stored in Redis on Java
- Redis Use Cases: 10 Patterns and How to Build Them in Java
- How to Build a Real-Time Leaderboard in Java with Valkey and Redis
- What Is a Redis Search?