Valkey: Vector search and RAG
Semantic search, recommendations, and retrieval-augmented generation with Valkey
👋 Welcome to the Stackhero documentation!
Stackhero offers a ready-to-use Valkey cloud solution that provides a host of benefits, including:
Valkey Adminweb UI included.- Unlimited message size and transfers.
- Effortless updates with just a click.
- Optimal performance and robust security powered by a private, dedicated infrastructure.
Save time and simplify your life: it only takes 5 minutes to try Stackhero's Valkey cloud hosting solution!
If you are building semantic search, a recommendation engine, or a retrieval-augmented generation (RAG) pipeline, you need to store embeddings and quickly find the closest matches for a query. Stackhero for Valkey handles this natively: the search module indexes vectors and answers similarity queries in milliseconds. You do not need to set up a separate vector database alongside the Valkey service you already use.
Two features make this approach practical and efficient:
- Hybrid queries. You can run vector similarity searches and apply regular filters (such as numeric ranges, tags, or text) in a single request. For example, you can retrieve "the 5 chunks closest to this question, but only from documents this user is allowed to read" with one query, not two.
- One less moving part. Your embeddings are stored alongside your cache and session data, using the same credentials, backups, and monitoring tools. This keeps your stack simpler and easier to manage.
Before you start
Enable the Search module (and JSON if you want to store documents as JSON) in the Modules section of your service configuration on the Stackhero dashboard. For details, see the search and JSON guide.
Creating a vector index
Declare a vector field by specifying its dimension and distance metric. The dimension should match your model: for example, 1536 for OpenAI text-embedding-3-small, or 768 for many open source models.
FT.CREATE chunksIndex
ON HASH
PREFIX 1 chunk:
SCHEMA
content TEXT
documentId TAG
embedding VECTOR HNSW 6
TYPE FLOAT32
DIM 1536
DISTANCE_METRIC COSINE
HNSW creates a graph index, providing fast approximate searches even as your collection grows large. This is recommended for collections larger than a few thousand vectors. For small collections, you can use FLAT for exact, brute-force search.
COSINE is usually the best distance metric for text embeddings. L2 and IP metrics are also supported.
Querying
A K-nearest neighbors (KNN) query passes the vector as a parameter, using raw little-endian float32 bytes:
FT.SEARCH chunksIndex "*=>[KNN 5 @embedding $queryVector]"
PARAMS 2 queryVector "<rawBytes>"
To combine vector search with filters, simply replace the * with your filter expression:
FT.SEARCH chunksIndex "@documentId:{doc42}=>[KNN 5 @embedding $queryVector]"
PARAMS 2 queryVector "<rawBytes>"
A RAG pipeline in Node.js
Valkey uses the same protocol as Redis, so you can use any Redis-compatible client.
import { createClient, SCHEMA_FIELD_TYPE, SCHEMA_VECTOR_FIELD_ALGORITHM } from 'redis';
const client = createClient({ url: process.env.STACKHERO_VALKEY_URL_TLS });
await client.connect();
const DIMENSIONS = 1536;
const toBytes = embedding => Buffer.from(new Float32Array(embedding).buffer);
// 1. Create the index once, at startup
try {
await client.ft.create(
'chunksIndex',
{
content: SCHEMA_FIELD_TYPE.TEXT,
documentId: SCHEMA_FIELD_TYPE.TAG,
embedding: {
type: SCHEMA_FIELD_TYPE.VECTOR,
ALGORITHM: SCHEMA_VECTOR_FIELD_ALGORITHM.HNSW,
TYPE: 'FLOAT32',
DIM: DIMENSIONS,
DISTANCE_METRIC: 'COSINE'
}
},
{ ON: 'HASH', PREFIX: 'chunk:' }
);
}
catch (error) {
if (!error.message.includes('Index already exists')) {
throw error;
}
}
// 2. Index your chunks (embedding comes from your model provider)
async function indexChunk({ id, documentId, content, embedding }) {
await client.hSet(`chunk:${id}`, {
content,
documentId,
embedding: toBytes(embedding)
});
}
// 3. Retrieve the chunks closest to a question, restricted to one document
async function retrieve({ questionEmbedding, documentId, count = 5 }) {
const results = await client.ft.search(
'chunksIndex',
`@documentId:{${documentId}}=>[KNN ${count} @embedding $queryVector]`,
{ PARAMS: { queryVector: toBytes(questionEmbedding) } }
);
return results.documents.map(({ value }) => value.content);
}
// 4. Feed them to your model as context
const context = await retrieve({ questionEmbedding, documentId: 'doc42' });
const prompt = `Answer using only this context:\n\n${context.join('\n\n')}\n\nQuestion: ${question}`;
The same in Python
import os
import numpy as np
import redis
from redis.commands.search.field import TextField, TagField, VectorField
from redis.commands.search.index_definition import IndexDefinition, IndexType
from redis.commands.search.query import Query
DIMENSIONS = 1536
r = redis.from_url(os.environ['STACKHERO_VALKEY_URL_TLS'])
# Create the index once, at startup
try:
r.ft('chunksIndex').create_index(
(
TextField('content'),
TagField('documentId'),
VectorField(
'embedding',
'HNSW',
{'TYPE': 'FLOAT32', 'DIM': DIMENSIONS, 'DISTANCE_METRIC': 'COSINE'},
),
),
definition=IndexDefinition(prefix=['chunk:'], index_type=IndexType.HASH),
)
except redis.ResponseError as error:
if 'Index already exists' not in str(error):
raise
def index_chunk(chunk_id, document_id, content, embedding):
r.hset(
f'chunk:{chunk_id}',
mapping={
'content': content,
'documentId': document_id,
'embedding': np.array(embedding, dtype=np.float32).tobytes(),
},
)
def retrieve(question_embedding, document_id, count=5):
query = Query(f'@documentId:{{{document_id}}}=>[KNN {count} @embedding $queryVector]')
results = r.ft('chunksIndex').search(
query,
query_params={'queryVector': np.array(question_embedding, dtype=np.float32).tobytes()},
)
return [document.content for document in results.docs]
Sizing your service
Vectors are stored in memory, so it is important to plan for their footprint. The rough estimate for float32 vectors is:
numberOfVectors x dimensions x 4 bytes, plus around 30 to 50 percent extra for the HNSW graph.
For example, one million 1536-dimension vectors require about 6 GB for the raw vectors. A 20 GB plan is a comfortable starting point. You can reduce memory usage by choosing a model with fewer dimensions: a 768-dimension model uses only half the memory of a 1536-dimension one.
You can check actual memory usage with FT.INFO chunksIndex and with the used_memory metric in your Prometheus monitoring. If you need more memory, you can upgrade your plan at any time from your dashboard.
Good to know
- Indexing existing keys runs in the background. You can track progress with
FT.INFO. Searches will return partial results until backfilling is complete. - The search module implements a focused set of
FT.*commands. For vector search, it covers all essentials: HNSW and exact KNN, hybrid filtering, andFT.AGGREGATE.
For a full reference, see the Valkey search documentation.