When to use this
Redis is often already in the stack for caching, so teams use it for semantic caches and small-to-medium RAG indexes. This sheet covers the day-to-day commands: create an index, load vectors, query, check that documents are actually indexed, and change an index without downtime.
Vector search needs the Redis Query Engine (RediSearch). It's built into Redis 8, included in Redis Stack, and available as a module on Redis Software, Redis Cloud and Azure Managed Redis (where it must be enabled when the cache is created). Check MODULE LIST or INFO modules on older builds. Managed Valkey and MemoryDB services have their own vector search implementations with a similar FT.* syntax but different options; check that service's documentation.
redis-cli -h $REDIS_HOST -p 6379 --tls -a "$REDIS_PASSWORD" INFO modules
redis-cli FT._LIST # list indexes
Create an index
HNSW on hashes
FT.CREATE idx:docs ON HASH PREFIX 1 doc: SCHEMA
title TEXT
tenant TAG
category TAG
updated NUMERIC SORTABLE
embedding VECTOR HNSW 10
TYPE FLOAT32 DIM 1536 DISTANCE_METRIC COSINE M 16 EF_CONSTRUCTION 200
The number after the algorithm (10) is the count of attribute tokens that follow: five name/value pairs here. Getting it wrong is the most common FT.CREATE syntax error.
FLAT (brute force) for small sets
FT.CREATE idx:cache ON HASH PREFIX 1 cache: SCHEMA
model TAG
embedding VECTOR FLAT 6 TYPE FLOAT32 DIM 768 DISTANCE_METRIC COSINE
FLAT gives exact results and is fine up to tens of thousands of vectors. Use HNSW above that.
On JSON documents
FT.CREATE idx:kb ON JSON PREFIX 1 kb: SCHEMA
$.tenant AS tenant TAG
$.text AS text TEXT
$.embedding AS embedding VECTOR HNSW 6 TYPE FLOAT32 DIM 1536 DISTANCE_METRIC COSINE
With JSON, the vector is stored as an array of numbers. With hashes, it's a raw binary blob.
Vector field options
| Option | Values | Notes |
|---|---|---|
| Algorithm | FLAT, HNSW | Newer releases add more (for example SVS-VAMANA in Redis 8.2+); check your version |
TYPE | FLOAT32, FLOAT64 (more types on newer builds) | Must match the bytes you write |
DIM | Embedding dimension | Must match the model exactly |
DISTANCE_METRIC | COSINE, IP, L2 | Fixed at creation |
M | HNSW links per node | Higher = better recall, more memory |
EF_CONSTRUCTION | HNSW build breadth | Higher = slower build, better graph |
EF_RUNTIME | Default query breadth | Can be overridden per query |
Warning
DIM,TYPEandDISTANCE_METRICcan't be changed withFT.ALTER. Changing the embedding model means a new index. Use an alias (below) so applications don't notice the switch.
Load vectors
Hash vectors must be little-endian float32 bytes. In Python with redis-py and NumPy:
import numpy as np, redis
r = redis.Redis(host="localhost", port=6379, decode_responses=False)
def put(doc_id, title, tenant, vec):
r.hset(f"doc:{doc_id}", mapping={
"title": title,
"tenant": tenant,
"category": "runbook",
"updated": 1762732800,
"embedding": np.asarray(vec, dtype=np.float32).tobytes(),
})
pipe = r.pipeline(transaction=False)
# for many docs, queue hset calls on pipe and pipe.execute() every 500-1000 docs
A vector with the wrong byte length doesn't raise an error on HSET. The document is written but not indexed and shows up only in hash_indexing_failures (see index health).
Query
All vector queries need query dialect 2 or higher. Pass DIALECT 2 on every command unless you've set the default dialect for the server (the config name varies by version; check your build's documentation).
Plain KNN
FT.SEARCH idx:docs "*=>[KNN 5 @embedding $vec AS score]"
PARAMS 2 vec "<binary blob>"
SORTBY score
RETURN 3 title tenant score
DIALECT 2
score is a distance: lower is closer. For COSINE it's 1 - cosine similarity.
KNN with filters (hybrid)
The part before => filters first, then KNN runs over the matching documents:
FT.SEARCH idx:docs "(@tenant:{acme} @category:{runbook|kb} @updated:[1735689600 +inf])=>[KNN 10 @embedding $vec AS score]"
PARAMS 2 vec "<blob>" SORTBY score RETURN 2 title score DIALECT 2
TAG values with spaces, dashes or dots need escaping: @tenant:{acme\-eu}.
Per-query HNSW breadth
FT.SEARCH idx:docs "*=>[KNN 10 @embedding $vec EF_RUNTIME $ef AS score]"
PARAMS 4 vec "<blob>" ef 200 SORTBY score DIALECT 2
Raise EF_RUNTIME when recall is poor; it costs latency.
Range query (semantic cache lookups)
Return everything within a distance threshold rather than a fixed count:
FT.SEARCH idx:cache "@embedding:[VECTOR_RANGE $radius $vec]=>{$YIELD_DISTANCE_AS: dist}"
PARAMS 4 radius 0.1 vec "<blob>" SORTBY dist LIMIT 0 1 DIALECT 2
The same from Python
q = np.asarray(query_vec, dtype=np.float32).tobytes()
res = r.execute_command(
"FT.SEARCH", "idx:docs",
"(@tenant:{acme})=>[KNN 5 @embedding $vec AS score]",
"PARAMS", 2, "vec", q,
"SORTBY", "score", "RETURN", 2, "title", "score",
"DIALECT", 2,
)
print(res[0], res[1:]) # total, then key/fields pairs
execute_command avoids differences between redis-py versions in the redis.commands.search helper modules.
Index health
FT.INFO idx:docs
Fields to read:
| Field | What it tells you |
|---|---|
num_docs | Documents in the index. Compare with your source count |
hash_indexing_failures | Documents that matched the prefix but failed to index (wrong vector size, bad numeric value) |
indexing / percent_indexed | 1 / below 1 while a background scan of existing keys is running |
vector_index_sz_mb or per-field memory stats | Vector index memory; field names vary by version |
Quick checks:
redis-cli FT.INFO idx:docs | grep -A1 -E "num_docs|hash_indexing_failures|percent_indexed"
redis-cli --scan --pattern 'doc:*' | wc -l # keys with the prefix, compare with num_docs
redis-cli HSTRLEN doc:123 embedding # expect DIM * 4 bytes for FLOAT32 (1536 -> 6144)
redis-cli FT.EXPLAIN idx:docs "(@tenant:{acme})=>[KNN 5 @embedding \$vec]" DIALECT 2
FT.PROFILE idx:docs SEARCH QUERY "..." PARAMS ... DIALECT 2 shows where query time goes.
Change an index without downtime
Applications should query an alias, never the index name:
FT.ALIASADD docs idx:docs_v1 # once
FT.CREATE idx:docs_v2 ON HASH PREFIX 1 docv2: SCHEMA ... DIM 3072 ...
# backfill docv2:* keys, wait until percent_indexed is 1, run recall checks
FT.ALIASUPDATE docs idx:docs_v2 # atomic switch
FT.ALIASUPDATE docs idx:docs_v1 # rollback if needed
Non-vector fields can be added in place:
FT.ALTER idx:docs SCHEMA ADD source TAG
Existing documents are re-scanned in the background for the new field.
Drop an index
FT.DROPINDEX idx:docs_v1 # removes the index, keeps the keys
FT.DROPINDEX idx:docs_v1 DD # removes the index AND deletes every indexed key
Caution
DDdeletes the documents themselves. If two indexes share a prefix,DDon one deletes data the other still uses. CheckFT.INFOforprefixesbefore dropping, and confirm you have an RDB/AOF backup.
Common errors
| Error or symptom | Cause | Fix |
|---|---|---|
Syntax error at offset ... on a KNN query | Dialect 1 in use | Add DIALECT 2 |
Unknown index name | Wrong name, alias missing, or wrong database/shard endpoint | FT._LIST, FT.INFO <alias> |
num_docs lower than key count | Vector length mismatch or wrong prefix | Check hash_indexing_failures and HSTRLEN |
| Query returns 0 results with a filter | TAG value not escaped, or case differs | Escape punctuation; TAGs are case-insensitive unless CASESENSITIVE is set |
| Good matches missing | EF_RUNTIME too low, or filter very selective | Raise EF_RUNTIME; for small filtered sets consider FLAT |
OOM command not allowed during load | Vector index counts against maxmemory | Size memory for vectors plus HNSW graph; don't use allkeys-* eviction on a vector database |
Prevent it next time
- Put index definitions in code or migration scripts, not only in someone's shell history.
- Alert when
hash_indexing_failuresis above zero ornum_docsdrifts from the source count. - Query through aliases so model changes become a create, backfill and
FT.ALIASUPDATE. - Keep vector indexes on a Redis instance with
noevictionand its own memory budget, separate from the general cache.