AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
This skill provides a centralized reference for Redis Search, covering the full lifecycle of index management and query execution. It details schema design using FT.CREATE, command selection between FT.SEARCH, FT.AGGREGATE, and FT.HYBRID, and advanced techniques like vector similarity search (HNSW/FLAT) and RAG pipeline integration. It also includes operational best practices for zero-downtime index updates and performance debugging using FT.PROFILE and FT.EXPLAIN.
What it does
- Guides schema design for Hash and JSON documents using various field types.
- Explains command selection for lexical, vector, and hybrid search requirements.
- Provides patterns for vector similarity search, including HNSW and FLAT algorithm tuning.
- Details the construction of declarative aggregation pipelines for analytics.
- Offers strategies for zero-downtime index management and schema evolution.
- Assists in troubleshooting performance and query results with profiling tools.
When to use it
- When defining or modifying a search index schema for Redis.
- When building retrieval-augmented generation (RAG) pipelines.
- When optimizing complex search queries or aggregation pipelines.
- When troubleshooting slow queries or unexpected search results.
- When implementing vector similarity search for machine learning applications.
Advantages
- Consolidates complex search and vector indexing patterns into a single reference.
- Provides clear guidance on choosing the most efficient query command.
- Includes practical examples for common search and aggregation scenarios.
- Offers actionable advice for performance tuning and debugging.
- Supports modern development workflows like RAG and zero-downtime updates.
Usage examples
- “How do I create a search index for JSON documents with a vector field?”
- The agent provides the FT.CREATE syntax, specifying the ON JSON option, the correct prefix, and the schema definition including the VECTOR field with appropriate DIM and DISTANCE_METRIC settings.
- “My vector search is slow. How can I optimize it?”
- The agent suggests using FT.PROFILE to identify bottlenecks, checking HNSW parameters like M and EF_RUNTIME, and ensuring the query uses appropriate pre-filters to narrow the search space.
- “How do I perform a hybrid search combining text and vector similarity?”
- The agent explains the use of FT.HYBRID (for Redis 8.4+) or the pre-filter pattern for earlier versions, detailing how to blend scores using RRF or linear fusion.
- “How can I update my index schema without taking the application offline?”
- The agent outlines the alias pattern: creating a new index version, populating it, and then using FT.ALIASUPDATE to point the application to the new index seamlessly.
Install
Download the skill as a ZIP:
https://mp-staging.autogineer.com/agent-skills/redis/redis-search/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip redis-search.zip -d .agents/skills
Documentation by Redis, Inc.
Redis Search
Single source of guidance for Redis Search — the retrieval surface that spans lexical, numeric, geo, JSON-path, and vector queries. Vector fields are part of the same FT.CREATE machinery as TEXT/TAG/NUMERIC fields, and FT.HYBRID blends lexical and vector ranking in one command, so this skill covers them together.
When to apply
Creating, modifying, or reviewing a Redis Search index (
FT.CREATE,FT.ALTER).Writing or optimizing
FT.SEARCH,FT.AGGREGATE, orFT.HYBRIDqueries.Picking between
TEXT,TAG,NUMERIC,GEO,GEOSHAPE,VECTOR, or JSON-path fields.Defining a
VECTORfield, choosing HNSW vs FLAT, tuning HNSW parameters.Building a retrieval-augmented generation (RAG) pipeline.
Rolling out a new index schema without downtime.
Troubleshooting empty results, slow queries, or tokenization issues with
FT.EXPLAIN,FT.PROFILE,FT.INFO.
1. Pick the right command
Three query commands. Reach for the narrowest one that fits.
| Command | When to use | Mental model | Minimum Redis |
|---|---|---|---|
| FT.SEARCH | Document retrieval, ranked or sorted. Best default. | Returns matching docs directly. | 2.0 (module) / 8.0 (built-in) |
| FT.AGGREGATE | Faceting, computed fields, custom output shape, analytics. | Declarative pipeline: LOAD, APPLY, GROUPBY, REDUCE, SORTBY. | 2.0 / 8.0 |
| FT.HYBRID | Blend lexical (BM25) with vector similarity, with configurable fusion. | Pipeline with explicit SEARCH + VSIM legs and a COMBINE fusion stage. | 8.4.0 |
# FT.SEARCH — most common
FT.SEARCH idx:products "@category:{electronics} @price:[100 500]" LIMIT 0 20 RETURN 3 name price category
# FT.AGGREGATE — top categories by avg price
FT.AGGREGATE idx:products "*" GROUPBY 1 @category REDUCE AVG 1 @price AS avg_price SORTBY 2 @avg_price DESC
# FT.HYBRID (Redis ≥ 8.4) — lexical + vector fusion
FT.HYBRID idx:docs
SEARCH "@title:transformers" SCORER BM25 YIELD_SCORE_AS lexscore
VSIM embedding $vec KNN count 1 K 50 YIELD_SCORE_AS vecscore
COMBINE RRF 2 CONSTANT 60
PARAMS 2 vec "..."
DIALECT 2For Redis < 8.4 the lexical+vector blend is approximated with FT.SEARCH pre-filter + =>[KNN ...]. See references/command-selection.md and references/hybrid-search.md.
2. Schema basics — FT.CREATE
FT.CREATE indexes Hash or JSON documents matching a PREFIX. Always set PREFIX. Use DIALECT 2 (the default since Redis 8; required for vector queries).
FT.CREATE idx:products ON HASH PREFIX 1 product:
SCHEMA
name TEXT WEIGHT 2.0
category TAG SORTABLE
price NUMERIC SORTABLE
location GEO
embedding VECTOR HNSW 6
TYPE FLOAT32
DIM 1536
DISTANCE_METRIC COSINEPick the narrowest field type that supports your access pattern:
| Field type | Use when | Notes |
|---|---|---|
TEXT | Full-text search | Tokenized + stemmed; not for exact match |
TAG | Exact match / filtering | Add SORTABLE UNF for fastest tag queries |
NUMERIC | Range queries, sorting | Prices, counts, timestamps |
GEO | Lat/long points | Stores, users |
GEOSHAPE | Polygon / area queries | Delivery zones, regions |
VECTOR | Similarity search | HNSW or FLAT; see §4 |
JSON $.path AS alias | Nested JSON fields | ON JSON; see references/json-indexing.md |
The classic mistake is TEXT for a category or status field "because it's a string" — TAG is roughly 10× faster for exact-match filtering.
See references/index-creation.md, references/field-types.md, references/dialect.md, references/ft-create-options.md, references/json-indexing.md.
3. Common queries
Narrow with filters; return only what you need.
# Tag filter + numeric range, sorted by price
FT.SEARCH idx:products "@category:{electronics} @price:[100 500]"
SORTBY price ASC
LIMIT 0 20
RETURN 3 name price category
# Text + tag filter
FT.SEARCH idx:products "wireless headphones @category:{audio}"
# Negation and OR
FT.SEARCH idx:products "@category:{audio} -@brand:{generic} (@price:[0 100] | @on_sale:{true})"Operators worth remembering: space = AND, | = OR, - = NOT, ~ = optional (scoring boost), =>{$weight: N} = boost. Escape hyphens and special characters inside TAG values (@sku:{ABC\\-123}). See references/query-syntax.md and references/search-syntax-primitives.md for the DSL vocabulary.
For tokenization gotchas (stemming, stopwords, language) see references/text-tokenization.md. For result shaping (SORTBY, RETURN, HIGHLIGHT, SUMMARIZE, NOCONTENT) see references/result-shaping.md. For performance levers (pre-filters, SORTABLE fields, tight RETURN, FT.PROFILE) see references/query-optimization.md.
4. Vector basics
Three vector settings have to match the embedding model exactly:
DIM— output dimensionality (e.g. 1536 for OpenAItext-embedding-3-small). Mismatch produces silent garbage.DISTANCE_METRIC—COSINEfor normalized text embeddings (common case),IPfor unnormalized inner-product,L2for raw Euclidean.TYPE— usuallyFLOAT32. UseFLOAT16or quantized variants only when memory is the binding constraint.
# Index
FT.CREATE idx:docs ON HASH PREFIX 1 doc:
SCHEMA
content TEXT
embedding VECTOR HNSW 6 TYPE FLOAT32 DIM 1536 DISTANCE_METRIC COSINE
# Pure KNN query (top 5 by cosine similarity)
FT.SEARCH idx:docs "*=>[KNN 5 @embedding $vec AS score]"
PARAMS 2 vec "..."
SORTBY score
DIALECT 2| Algorithm | Speed | Accuracy | Memory | Use for |
|---|---|---|---|---|
| HNSW | Fast (approximate) | ~95%+ recall (tunable) | Higher | Production: >10k vectors, latency-sensitive |
| FLAT | Slow (exact) | 100% | Lower | Small corpora (<10k), exact-match required |
HNSW tuning levers: M (16–64, connections per node), EF_CONSTRUCTION (100–500, build quality), EF_RUNTIME (query-time candidate list).
See references/vector-query.md, references/algorithm-choice.md.
5. Hybrid retrieval
Two distinct patterns get called "hybrid." Pick by intent.
Filter-then-vector (any Redis version) — apply attribute filters so the engine narrows the search space before the vector comparison.
FT.SEARCH idx:docs "(@category:{tech} @date:[2024 +inf])=>[KNN 10 @embedding $vec AS score]"
PARAMS 2 vec "..."
SORTBY score
DIALECT 2Lexical + vector fusion (Redis ≥ 8.4) — blend BM25 text scoring with vector similarity, fuse with RRF or LINEAR. Use FT.HYBRID (see §1).
Don't fetch a wide unfiltered result and filter client-side — slower and less accurate. See references/hybrid-search.md.
6. Aggregations and shaping
FT.AGGREGATE is the declarative result-shaping command. Build a pipeline of stages.
# Top 5 categories by total revenue
FT.AGGREGATE idx:orders "@status:{shipped}"
LOAD 2 @category @amount
GROUPBY 1 @category
REDUCE SUM 1 @amount AS revenue
SORTBY 2 @revenue DESC
LIMIT 0 5Common stages: LOAD, APPLY (computed fields), FILTER (post-query), GROUPBY + REDUCE (SUM, COUNT, AVG, FIRST_VALUE, TOLIST), SORTBY, LIMIT.
For long-running result sets use WITHCURSOR + FT.CURSOR READ to page server-side. See references/aggregate-pipeline.md and references/aggregate-cursors.md.
7. RAG pattern
Standard pipeline: embed the query, vector-search Redis, pass top-K context to the LLM.
Practical tips:
Match the metric to the embedding model (almost always
COSINEfor normalized text models).Chunk long documents (200–500-token chunks usually beat indexing whole pages).
Batch inserts rather than one call per record.
Pre-filter with attributes (tenant, recency, document type) before the vector search — see §5.
Re-rank at the top of the funnel if precision matters more than recall.
See references/rag-pattern.md.
8. Operations
Zero-downtime schema changes: keep app queries pointed at an alias and swap the underlying index.
FT.CREATE idx:products_v2 ON HASH PREFIX 1 product: SCHEMA ...
FT.ALIASUPDATE products idx:products_v2
# App queries are stable:
FT.SEARCH products "@category:{electronics}"Useful management commands: FT.INFO, FT.DROPINDEX, FT._LIST, FT.ALIASADD/UPDATE/DEL. See references/index-management.md.
Debug empty or slow queries with FT.EXPLAIN (shows how the query was parsed) and FT.PROFILE (shows execution stats). See references/debugging.md.
9. Client examples
Inline examples in this SKILL.md are CLI / RESP form — the wire protocol every client serializes to. For idiomatic snippets in a specific client:
redis-py (Python, raw client): references/clients/python-redis-py.md
Jedis (Java): references/clients/java-jedis.md
RedisVL (Python, higher-level SDK on top of redis-py): references/clients/python-redisvl.md
Other clients (Lettuce, node-redis, go-redis, NRedisStack, .NET) translate the same CLI form; coverage is tracked as a follow-up.
References
Files
- ATTRIBUTION.mdAdded by AutoGineer879 B
- LICENSELicence file added by AutoGineer1.0 KiB
- SKILL.md11.2 KiB
- references/aggregate-cursors.md3.9 KiB
- references/aggregate-pipeline.md15.0 KiB
- references/algorithm-choice.md4.0 KiB
- references/clients/java-jedis.md50.0 KiB
- references/clients/python-redis-py.md33.8 KiB
- references/clients/python-redisvl.md49.1 KiB
- references/command-selection.md9.2 KiB
- references/debugging.md4.6 KiB
- references/dialect.md2.6 KiB
- references/field-types.md3.7 KiB
- references/ft-create-options.md6.2 KiB
- references/hybrid-search.md5.0 KiB
- references/index-creation.md6.9 KiB
- references/index-management.md3.9 KiB
- references/json-indexing.md6.2 KiB
- references/query-optimization.md3.9 KiB
- references/query-syntax.md9.7 KiB
- references/rag-pattern.md5.1 KiB
- references/result-shaping.md5.9 KiB
- references/search-syntax-primitives.md6.6 KiB
- references/text-tokenization.md4.8 KiB
- references/vector-query.md4.7 KiB
references/clients/python-redis-py.md
redis-py — Redis Search quick reference
This reference covers the FT.* (Redis Search) surface of the raw redis-py client. It shows how redis-py expresses the canonical CLI form — it does not re-explain the query DSL. Read it after a reference that already states what to do.
Query DSL vocabulary (delimiters, operators, attributes):
../search-syntax-primitives.md. Do not duplicate that grammar here.Jedis (Java) equivalents for the same operations:
java-jedis.md.RedisVL is a different SDK (schema-first, semantic-cache, message-history). For RedisVL targets, read
python-redisvl.mdinstead; the two are not interchangeable.
Examples below trace to specific files in redis/redis-py/doctests/ and preserve the upstream STEP_START/STEP_END labels so you can pair-verify against the runnable source. The shared Bicycle dataset (bicycle:<n> JSON docs with brand, model, description, price, condition, type, store_location, description_embeddings) is used throughout.
Table of contents
Minimum supported versions
Connection setup
Schema imports
Create index — HASH
Create index — JSON
FT.SEARCH idioms
FT.AGGREGATE idioms
Cursors
Vector queries
FT.HYBRID
Debugging
Index management
Common errors & version gotchas
Upstream examples index
1. Minimum supported versions
Jedis equivalent: see java-jedis.md#1-minimum-supported-versions.
| Component | Minimum | Notes |
|---|---|---|
redis-py | 5.0 | Earlier 4.x releases predate the consolidated redis.commands.search.* import paths and lack IndexType.JSON ergonomics. |
redis-py (for HybridQuery) | 7.1.0 | The redis.commands.search.hybrid_query module ships from redis-py 7.1.0. Older releases (5.x–7.0.x) lack the HybridQuery builder and index.hybrid_search(). |
| Redis server (FT.SEARCH / FT.AGGREGATE) | 7.4 | Redis Search ships built-in from Redis 8.0; 7.4 still requires the RediSearch module. |
Redis server (FT.HYBRID) | 8.4.0 | Hard floor. Older Redis returns unknown command 'FT.HYBRID'. Fall back to pre-filter + =>[KNN ...] via FT.SEARCH. |
| Python | 3.8+ | Type hints in redis.commands.search.* assume typing from 3.8. |
DIALECT default: redis-py does not set DIALECT on your behalf. Every query in this reference passes DIALECT 2 explicitly (.dialect(2) or as a Query() argument) — required for vector attribute syntax (=>[KNN ...]) and the modern numeric/tag parser. Redis 8 changed the server default to DIALECT 2, but client-side absence still emits the server's compatibility default for older servers.
2. Connection setup
Jedis equivalent: see java-jedis.md#2-connection-setup.
The canonical connect, from doctests/search_quickstart.py — STEP_START connect:
import redis
r = redis.Redis(host="localhost", port=6379, db=0, decode_responses=True)decode_responses=True is the right default for FT.* work because Search returns field names and string values as bytes by default — every result tuple becomes b"..." keys/values otherwise. The case that justifies leaving it False:
Vector blobs you re-emit unchanged. Vector data is binary
FLOAT32bytes; withdecode_responses=TrueRedis Search still returns them correctly because the client only decodes RESP simple/bulk strings, but mixing decoded + raw bytes in the same result set is error-prone.
For FT.HYBRID results, field decoding differs — see §10.
Pool reuse (for any non-toy app):
pool = redis.ConnectionPool(host="localhost", port=6379, decode_responses=True, max_connections=32)
r = redis.Redis(connection_pool=pool)Reuse one Redis() instance across threads — it's thread-safe via the underlying pool.
3. Schema imports
Jedis equivalent: see java-jedis.md#3-schema-imports.
redis-py splits the Search API across submodules of redis.commands.search. The canonical import block, mirroring doctests/search_quickstart.py and search_vss.py:
from redis.commands.search.field import (
TextField,
TagField,
NumericField,
GeoField,
GeoShapeField,
VectorField,
)
from redis.commands.search.index_definition import IndexDefinition, IndexType
from redis.commands.search.query import Query, NumericFilter
from redis.commands.search.aggregation import AggregateRequest, Cursor
import redis.commands.search.reducers as reducersNotes:
index_definitionis the modern path; older code imports fromindexDefinition(camelCase). Both work in 5.x, but the underscore form is what current upstream doctests use.The two query builders are different classes:
QueryforFT.SEARCH,AggregateRequestforFT.AGGREGATE. They are not interchangeable and don't share methods. This is the single most common source of confusion when porting from another client.redis.commands.search.reducersis a module of factory functions (count(),sum(),avg(),tolist()), not a class — that's why upstream imports it withas reducers.
4. Create index — HASH
Jedis equivalent: see java-jedis.md#4-create-index--hash.
CLI form (from index-creation.md):
FT.CREATE idx:bicycle ON HASH PREFIX 1 bicycle:
SCHEMA
model TEXT WEIGHT 2.0
description TEXT
brand TAG
condition TAG
price NUMERIC SORTABLE
store_location GEOredis-py — mirrors doctests/home_json.py STEP_START make_hash_index (the upstream HASH index example; home_json.py itself is JSON-indexed elsewhere, but this specific step demonstrates the HASH variant):
# STEP_START create_index_hash
schema = (
TextField("model", weight=2.0),
TextField("description"),
TagField("brand"),
TagField("condition"),
NumericField("price", sortable=True),
GeoField("store_location"),
)
r.ft("idx:bicycle").create_index(
schema,
definition=IndexDefinition(prefix=["bicycle:"], index_type=IndexType.HASH),
)
# STEP_ENDHASH-specific notes:
Field names in the schema are the hash field names verbatim (no
$.path prefix; noas_name).Document keys must literally start with the declared prefix —
bicycle:1,bicycle:2, … An empty / missing prefix indexes every hash in the database.Use
r.hset("bicycle:1", mapping={...})to add documents; indexing happens synchronously on write.
5. Create index — JSON
Jedis equivalent: see java-jedis.md#5-create-index--json.
CLI form:
FT.CREATE idx:bicycle ON JSON PREFIX 1 bicycle:
SCHEMA
$.brand AS brand TEXT
$.model AS model TEXT
$.description AS description TEXT
$.price AS price NUMERIC
$.condition AS condition TAGredis-py — mirrors doctests/search_quickstart.py STEP_START create_index and home_json.py STEP_START make_index:
# STEP_START create_index_json
schema = (
TextField("$.brand", as_name="brand"),
TextField("$.model", as_name="model"),
TextField("$.description", as_name="description"),
NumericField("$.price", as_name="price"),
TagField("$.condition", as_name="condition"),
)
r.ft("idx:bicycle").create_index(
schema,
definition=IndexDefinition(prefix=["bicycle:"], index_type=IndexType.JSON),
)
# STEP_ENDJSON-specific notes:
The first positional argument is the JSONPath, not the alias. Always pair it with
as_name="<alias>"; the alias is what queries reference as@<alias>.Without
as_name, Redis auto-generates a field alias from the path — usable but brittle (renaming the JSON key silently breaks the index).Array projections use
[*]:TextField("$.tags[*]", as_name="tags"). Nested objects use the obvious$.address.city.Add documents with
r.json().set("bicycle:1", "$", {...})(seehome_json.pySTEP_STARTadd_data).
6. FT.SEARCH idioms
Jedis equivalent: see java-jedis.md#6-ftsearch-idioms.
For the query DSL itself (delimiters, operators, escaping), read ../search-syntax-primitives.md. This section shows only how redis-py binds a query to FT.SEARCH.
The Query builder
Query("<expression>") wraps the query expression. The fluent setters mirror FT.SEARCH flags:
Query method | CLI equivalent | Purpose |
|---|---|---|
.paging(offset, num) | LIMIT offset num | Result page slice. |
.sort_by(field, asc=True) | `SORTBY field ASC | DESC` |
.return_fields(*fields) | RETURN n f1 f2 ... | Project only listed fields. |
.return_field(path, as_field=...) | RETURN n path AS alias | JSON projection by path with alias. |
.no_content() | NOCONTENT | IDs only — saves bandwidth for LIMIT 0 0 count queries. |
.with_scores() | WITHSCORES | Append relevance score per hit. |
.verbatim() | VERBATIM | Disable stemming. |
.dialect(2) | DIALECT 2 | Always pass this. |
.add_filter(NumericFilter(...)) | FILTER field min max | Inline numeric range; alternative to @field:[min max] in the expression. |
Exact match (TAG / NUMERIC) — mirrors doctests/query_em.py
# STEP_START em1 — numeric exact match via range with equal bounds
r.ft("idx:bicycle").search(Query("@price:[270 270]").dialect(2))
# STEP_START em2 — tag exact match
r.ft("idx:bicycle").search(Query("@condition:{new}").dialect(2))
# STEP_START em4 — exact phrase in TEXT
r.ft("idx:bicycle").search(Query('@description:"rough terrain"').dialect(2))Numeric ranges — mirrors doctests/query_range.py
# STEP_START range1 — inclusive
r.ft("idx:bicycle").search(Query("@price:[500 1000]").dialect(2))
# STEP_START range3 — exclusive lower, unbounded upper, via NumericFilter
q = Query("*").add_filter(NumericFilter("price", "(1000", "+inf")).dialect(2)
r.ft("idx:bicycle").search(q)
# STEP_START range4 — sorted + paged
q = Query("@price:[-inf 2000]").sort_by("price").paging(0, 5).dialect(2)
r.ft("idx:bicycle").search(q)NumericFilter accepts numeric values or RESP-style strings ("(1000" for exclusive, "+inf" / "-inf").
Full-text idioms — mirrors doctests/query_ft.py
# STEP_START ft1 — field-scoped term
r.ft("idx:bicycle").search(Query("@description: kids").dialect(2))
# STEP_START ft2 — prefix
r.ft("idx:bicycle").search(Query("@model: ka*").dialect(2))
# STEP_START ft3 — suffix (requires WITHSUFFIXTRIE at index time for efficiency)
r.ft("idx:bicycle").search(Query("@brand: *bikes").dialect(2))
# STEP_START ft4 — fuzzy (Levenshtein distance 1)
r.ft("idx:bicycle").search(Query("%optamized%").dialect(2))Geo — mirrors doctests/query_geo.py
# STEP_START geo1 — radius query, parametrised
params = {"lon": -0.1778, "lat": 51.5524, "radius": 20, "units": "mi"}
q = Query("@store_location:[$lon $lat $radius $units]").dialect(2)
r.ft("idx:bicycle").search(q, query_params=params)
# STEP_START geo2 — GEOSHAPE CONTAINS (requires DIALECT 3)
# DIALECT 3 required for GEOSHAPE WITHIN/CONTAINS predicates (Redis 7.2+ with FT.CREATE GEOSHAPE field).
params = {"bike": "POINT(-0.1278 51.5074)"}
q = Query("@pickup_zone:[CONTAINS $bike]").dialect(3)
r.ft("idx:bicycle").search(q, query_params=params)query_params is the redis-py mechanism for binding $name placeholders in the query expression — use it for any user-supplied or binary value (vector blobs, geo points, range bounds).
Reading results
search() returns a Result with .total (server-reported match count) and .docs (list of Document objects). Each Document exposes id, payload, and one attribute per returned field:
res = r.ft("idx:bicycle").search(Query("@condition:{new}").return_fields("brand", "model", "price").dialect(2))
for doc in res.docs:
print(doc.id, doc.brand, doc.model, doc.price)When decode_responses=False, both attribute names and values come back as bytes — fix it at the connection level, not via per-result decoding.
7. FT.AGGREGATE idioms
Jedis equivalent: see java-jedis.md#7-ftaggregate-idioms.
For pipeline-stage ordering rules, see aggregate-pipeline.md. This section shows only the redis-py builder shape.
The AggregateRequest builder
AggregateRequest("<filter-expression>") is a separate class from Query. The fluent setters map directly to FT.AGGREGATE stages:
AggregateRequest method | CLI stage |
|---|---|
.load(*fields) | LOAD n f1 f2 ... |
.apply(alias="<expr>") | APPLY <expr> AS alias (keyword form: alias on left) |
.filter("<expr>") | FILTER <expr> |
.group_by(field_or_list, *reducers) | GROUPBY n f1 ... REDUCE ... |
| `.sort_by(("<field>", "ASC | DESC"))` |
.limit(offset, num) | LIMIT offset num |
.cursor(count=<n>, max_idle=<seconds>) | WITHCURSOR [COUNT n] [MAXIDLE ms] (see §8) |
.dialect(2) | DIALECT 2 |
Reducers live in redis.commands.search.reducers as factory functions. Common ones:
| Factory | CLI form |
|---|---|
reducers.count() | REDUCE COUNT 0 |
reducers.count_distinct("@f") | REDUCE COUNT_DISTINCT 1 @f |
reducers.sum("@f") | REDUCE SUM 1 @f |
reducers.avg("@f") | REDUCE AVG 1 @f |
reducers.min("@f") / reducers.max("@f") | REDUCE MIN 1 @f / MAX 1 @f |
reducers.quantile("@f", 0.95) | REDUCE QUANTILE 2 @f 0.95 |
reducers.tolist("@f") | REDUCE TOLIST 1 @f |
Every reducer factory takes .alias("<name>") to set the AS <alias> token.
Worked pipeline — mirrors doctests/query_agg.py
# STEP_START agg1 — LOAD + APPLY (no grouping)
req = (
AggregateRequest(query="@condition:{new}")
.load("__key", "price")
.apply(discounted="@price - (@price * 0.1)")
.dialect(2)
)
res = r.ft("idx:bicycle").aggregate(req)
# res.rows -> [['__key', 'bicycle:0', 'price', '270', 'discounted', '243'], ...]
# STEP_START agg2 — APPLY + GROUPBY + REDUCE
req = (
AggregateRequest(query="*")
.load("price")
.apply(price_category="@price<1000")
.group_by("@condition", reducers.sum("@price_category").alias("num_affordable"))
.dialect(2)
)
r.ft("idx:bicycle").aggregate(req)
# STEP_START agg3 — synthesised group key via APPLY (mirrors doctests/query_agg.py)
req = (
AggregateRequest(query="*")
.apply(type="'bicycle'")
.group_by("@type", reducers.count().alias("num_total"))
.dialect(2)
)
r.ft("idx:bicycle").aggregate(req)
# res.rows -> [['type', 'bicycle', 'num_total', '10']]
# STEP_START agg4 — GROUPBY + TOLIST
req = (
AggregateRequest(query="*")
.load("__key")
.group_by("@condition", reducers.tolist("__key").alias("bicycles"))
.dialect(2)
)
r.ft("idx:bicycle").aggregate(req)Result shape: AggregateResult with .rows (a list of flat [key, val, key, val, ...] lists, mirroring RESP2). Pair adjacent elements yourself or convert via the upstream pandas helper in search_vss.py.
.apply() uses keyword arguments where the keyword is the alias and the value is the expression — apply(discounted="@price * 0.9") emits APPLY "@price * 0.9" AS discounted.
8. Cursors
Jedis equivalent: see java-jedis.md#8-cursors.
For lifecycle rules and when to use cursors, see aggregate-cursors.md.
API note.
redis-py5.x does not expose standaloneft().cursor_read()orft().cursor_del()methods.FT.CURSOR READis invoked by passing aCursorinstance back toft().aggregate(cursor).FT.CURSOR DELrequires the rawr.execute_command("FT.CURSOR", "DEL", index, cursor_id)path shown below.
CLI form:
FT.AGGREGATE idx:bicycle "*"
GROUPBY 1 @brand REDUCE COUNT 0 AS n
WITHCURSOR COUNT 1000 MAXIDLE 30000
DIALECT 2
FT.CURSOR READ idx:bicycle <cursor_id> COUNT 1000
FT.CURSOR DEL idx:bicycle <cursor_id>redis-py — open a cursor:
# STEP_START aggregate_cursor_open
req = (
AggregateRequest(query="*")
.group_by("@brand", reducers.count().alias("n"))
.cursor(count=1000, max_idle=30.0) # max_idle is seconds; client converts to ms
.dialect(2)
)
result = r.ft("idx:bicycle").aggregate(req)
cursor = result.cursor # redis.commands.search.aggregation.Cursor
first_batch = result.rows
# STEP_ENDRead the next page by passing the Cursor back into aggregate():
# STEP_START aggregate_cursor_read
while cursor.cid != 0: # cid == 0 signals exhausted server-side cursor
cursor.count = 1000 # optional: override per-read batch size
page = r.ft("idx:bicycle").aggregate(cursor)
cursor = page.cursor
process(page.rows)
# STEP_ENDExplicit cleanup (release before MAXIDLE):
# STEP_START aggregate_cursor_del
r.execute_command("FT.CURSOR", "DEL", "idx:bicycle", cursor.cid)
# STEP_ENDSee the API note at the top of this section — FT.CURSOR DEL requires execute_command; FT.CURSOR READ is wrapped via aggregate(cursor).
9. Vector queries
Jedis equivalent: see java-jedis.md#9-vector-queries.
For query-attribute syntax (=>[KNN ...], [VECTOR_RANGE ...]) and pre-filter shape, read vector-query.md.
Index a vector field
CLI form:
FT.CREATE idx:bicycle ON JSON PREFIX 1 bicycle: SCHEMA
...
$.description_embeddings AS vector VECTOR FLAT 6
TYPE FLOAT32 DIM 1536 DISTANCE_METRIC COSINEredis-py — mirrors doctests/search_vss.py STEP_START create_index (dimension parametrised; use 1536 for OpenAI text-embedding-3-small / ada-002):
# STEP_START create_vector_index
VECTOR_DIMENSION = 1536 # match your embedding model
schema = (
TextField("$.model", no_stem=True, as_name="model"),
TextField("$.brand", no_stem=True, as_name="brand"),
NumericField("$.price", as_name="price"),
TagField("$.type", as_name="type"),
VectorField(
"$.description_embeddings",
"FLAT", # or "HNSW" for ANN
{
"TYPE": "FLOAT32",
"DIM": VECTOR_DIMENSION,
"DISTANCE_METRIC": "COSINE",
},
as_name="vector",
),
)
r.ft("idx:bicycle").create_index(
schema,
definition=IndexDefinition(prefix=["bicycle:"], index_type=IndexType.JSON),
)
# STEP_ENDEncode the query vector
The de facto pattern (used by every upstream doctest): numpy.array(...).astype(np.float32).tobytes(). Mirrors query_combined.py:
import numpy as np
def embed_to_bytes(model, text: str) -> bytes:
return np.array(model.encode(text)).astype(np.float32).tobytes()FLOAT32 little-endian is the only encoding redis-py ships with — match this on both index and query side, every time. A FLOAT64 array silently produces zero hits because the per-element byte offsets disagree with the index's TYPE FLOAT32.
KNN — mirrors doctests/search_vss.py STEP_START run_knn_query
# STEP_START vector_knn
query = (
Query("(*)=>[KNN 3 @vector $query_vector AS vector_score]")
.sort_by("vector_score")
.return_fields("vector_score", "id", "brand", "model", "description")
.dialect(2)
)
res = r.ft("idx:bicycle").search(
query,
query_params={"query_vector": embed_to_bytes(model, "Bike for small kids")},
)
# STEP_ENDPre-filtered KNN — mirrors doctests/query_combined.py STEP_START combined7
# STEP_START vector_prefilter
query = (
Query("(@price:[500 1000] -@condition:{new})=>[KNN 3 @vector $query_vector AS vector_score]")
.sort_by("vector_score")
.return_fields("vector_score", "brand", "model", "price")
.dialect(2)
)
r.ft("idx:bicycle").search(query, query_params={"query_vector": query_vec})
# STEP_ENDThe pre-filter (@price:[500 1000] -@condition:{new}) is applied before the KNN scan — it shrinks the candidate set HNSW/FLAT has to walk. Forgetting it is the most common cause of slow vector queries.
Range — mirrors doctests/search_vss.py STEP_START run_range_query
# STEP_START vector_range
range_query = (
Query(
"@vector:[VECTOR_RANGE $range $query_vector]=>"
"{$YIELD_DISTANCE_AS: vector_score}"
)
.sort_by("vector_score")
.return_fields("vector_score", "brand", "model", "description")
.paging(0, 4)
.dialect(2)
)
r.ft("idx:bicycle").search(
range_query,
query_params={"range": 0.55, "query_vector": query_vec},
)
# STEP_ENDAS <alias> (KNN form) and $YIELD_DISTANCE_AS (RANGE form) are not interchangeable — the upstream doctest demonstrates the difference.
HNSW tuning per-query
EF_RUNTIME is an in-query attribute:
Query("*=>[KNN 10 @vector $query_vector EF_RUNTIME 200 AS score]").dialect(2)Index-time EF_CONSTRUCTION is set in the VectorField algorithm dict and is independent.
10. FT.HYBRID
Jedis equivalent: see java-jedis.md#10-fthybrid.
Version gate: FT.HYBRID requires Redis ≥ 8.4.0 and redis-py ≥ 7.1.0 (the release that ships the hybrid_query module). On older Redis or older redis-py, use the pre-filter + KNN pattern in §9. See command-selection.md for the SEARCH vs AGGREGATE vs HYBRID decision.
High-level builder (recommended)
redis-py ≥ 7.1.0 ships an @experimental high-level HybridQuery builder under redis.commands.search.hybrid_query. The shape: build a HybridSearchQuery (text leg) + HybridVsimQuery (vector leg), combine with a CombineResultsMethod, call index.hybrid_search(...).
# STEP_START hybrid_query
from redis.commands.search.hybrid_query import (
HybridQuery,
HybridSearchQuery,
HybridVsimQuery,
VectorSearchMethods,
CombineResultsMethod,
CombinationMethods,
HybridPostProcessingConfig,
)
# Result types live in a separate module:
from redis.commands.search.hybrid_result import HybridResult, HybridCursorResult
search_leg = HybridSearchQuery(
query_string="laptop",
scorer="BM25",
yield_score_as="text_score",
)
vsim_leg = HybridVsimQuery(
vector_field_name="@description_vector",
vector_data="$query_vec", # bound via params_substitution below
vsim_search_method=VectorSearchMethods.KNN,
vsim_search_method_params={"K": 10, "EF_RUNTIME": 100},
yield_score_as="vec_score",
)
hybrid = HybridQuery(search_leg, vsim_leg)
combine = CombineResultsMethod(
CombinationMethods.RRF, # or CombinationMethods.LINEAR
WINDOW=100,
YIELD_SCORE_AS="final_score",
)
result = r.ft("idx:bicycle").hybrid_search(
hybrid,
combine_method=combine,
params_substitution={"query_vec": embed_to_bytes(model, "laptop")},
timeout=2000,
)
# STEP_ENDReturns a HybridResult (or HybridCursorResult when cursor=... is supplied).
Important behaviours
hybrid_searchis decorated@experimental_method(). API may shift; pinredis-pyif you depend on it in production.LOAD-returned field values come back as bytes by default, even withdecode_responses=True, to match the legacy RESP2 HYBRID contract. Opt into decoding per field viaHybridPostProcessingConfig.load("brand", "model", decode_field=True)and pass the config aspost_processing=.CombineResultsMethodkwargs are passed verbatim to the server —WINDOW,CONSTANT,YIELD_SCORE_ASfor RRF;ALPHA,BETA,YIELD_SCORE_ASfor LINEAR. The client does no validation.
Raw execute_command fallback
When you need a feature not yet wrapped (or are on a redis-py minor that pre-dates the high-level builder), drop to raw RESP:
r.execute_command(
"FT.HYBRID", "idx:bicycle",
"SEARCH", "laptop",
"VSIM", "@description_vector", "$query_vec",
"KNN", "2", "K", "10",
"COMBINE", "RRF", "2", "WINDOW", "100",
"PARAMS", "2", "query_vec", embed_to_bytes(model, "laptop"),
"DIALECT", "2",
)The raw shape mirrors the verified syntax in spec 0001 §5.0a.
Upstream: redis/redis-py master — redis/commands/search/hybrid_query.py (builder classes), redis/commands/search/hybrid_result.py (HybridResult, HybridCursorResult), and redis/commands/search/commands.py (the hybrid_search method). The API is decorated @experimental_method and may shift between minor releases — pin redis-py if you depend on it.
11. Debugging
Jedis equivalent: see java-jedis.md#11-debugging.
For interpreting FT.EXPLAIN and FT.PROFILE output, see debugging.md.
FT.EXPLAIN
# Pass either a Query or a raw string
plan = r.ft("idx:bicycle").explain(
Query("(@brand:{Velorim}) @price:[100 500]").dialect(2)
)
print(plan)
# INTERSECT {
# TAG:@brand {
# Velorim
# }
# NUMERIC {100.000000 <= @price <= 500.000000}
# }The output is a parse tree — useful for spotting unexpected stemming, tokenization, or operator-precedence surprises.
FT.PROFILE
result, profile_info = r.ft("idx:bicycle").profile(
Query("@brand:{Velorim}").dialect(2),
limited=False,
)
print(profile_info.iterators_profile)
print(profile_info.result_processors_profile)
print(profile_info.total_profile_time)profile() returns a (Result, ProfileInformation) tuple for Query input; for AggregateRequest it returns (AggregateResult, ProfileInformation). limited=True suppresses the per-iterator detail when you only care about totals.
FT.INFO
info = r.ft("idx:bicycle").info()
print(info["num_docs"], info["hash_indexing_failures"], info["inverted_sz_mb"])info is a dict-like with stringly-typed values (Redis returns them as strings; cast to int/float as needed). Key fields to monitor:
| Key | Why it matters |
|---|---|
num_docs | Docs successfully indexed. |
hash_indexing_failures | Non-zero means silent dropouts — usually schema/path mismatches. |
inverted_sz_mb | Inverted-index memory footprint. |
indexing | 1 while a background scan is running. |
percent_indexed | Progress of the background scan. |
12. Index management
Jedis equivalent: see java-jedis.md#12-index-management.
For semantics (FT.ALTER capacity, alias use cases), see index-management.md.
Add a field
r.ft("idx:bicycle").alter_schema_add(TagField("availability"))Subject to the MAXTEXTFIELDS capacity declared at FT.CREATE time. There is no FT.ALTER for removing or retyping a field — drop and recreate the index.
Aliases (for blue/green index swaps)
r.ft("idx:bicycle_v2").aliasadd("idx:bicycle:active")
r.ft("idx:bicycle_v2").aliasupdate("idx:bicycle:active") # repoint existing alias
r.ft("idx:bicycle_v2").aliasdel("idx:bicycle:active")All three are wrapped — they call FT.ALIASADD / FT.ALIASUPDATE / FT.ALIASDEL respectively. Aliases let application code query a stable name while you build a replacement index behind it.
Drop the index
# Keep documents, drop only the index
r.ft("idx:bicycle").dropindex()
# Drop index AND delete every indexed document (destructive)
r.ft("idx:bicycle").dropindex(delete_documents=True)delete_documents=True is the equivalent of FT.DROPINDEX ... DD — gone forever, no undo.
13. Common errors & version gotchas
Jedis equivalent: see java-jedis.md#13-common-errors--version-gotchas.
| Symptom | Likely cause | Fix |
|---|---|---|
unknown command 'FT.CREATE' (or any other FT.*) | Redis < 8.0 without the RediSearch module loaded. | Load the module (MODULE LOAD /path/to/redisearch.so or via loadmodule in redis.conf), or upgrade to Redis ≥ 8.0 where Redis Search is built-in. |
unknown command 'FT.HYBRID' | Server < 8.4.0. | Upgrade or fall back to pre-filter + KNN via FT.SEARCH (§9). |
ImportError / cannot import name 'HybridQuery' from redis.commands.search.hybrid_query | redis-py < 7.1.0 — the hybrid_query module ships from 7.1.0. | Upgrade redis-py to ≥ 7.1.0, or fall back to pre-filter + KNN via FT.SEARCH (§9). |
Syntax error at offset N near KNN | Missing DIALECT 2. | Always .dialect(2) on every Query and AggregateRequest. |
GEOSHAPE WITHIN/CONTAINS returns syntax error | Missing .dialect(3), or server lacks DIALECT 3 support. | Pass .dialect(3) explicitly; ensure Redis ≥ 7.2 with GEOSHAPE-capable RediSearch. |
Vector dimension mismatch | Query vector dim differs from index DIM. | Recompute embedding with the same model used at index time; assert len(arr) == DIM. |
| Vector query returns 0 hits despite obvious matches | Query vector encoded as FLOAT64 (default numpy dtype). | Always .astype(np.float32) before .tobytes(). |
Result fields come back as b"..." bytes | decode_responses=False. | Set decode_responses=True on the connection. Don't decode per-result. |
| Result fields come back as bytes inside an FT.HYBRID response | Expected: HybridResult LOAD values stay bytes by default. | Pass HybridPostProcessingConfig().load("brand", decode_field=True) as post_processing=. |
Index already exists from idempotent setup | create_index is not "create or replace". | Try/except ResponseError, or dropindex() first when bootstrapping. |
JSON paths not matching docs | Document set with JSON.SET but index defined ON HASH (or vice versa). | Match IndexType to write path; info()'s hash_indexing_failures > 0 is the signal. |
Empty .docs but non-zero .total | NOCONTENT (via .no_content()) was set. | Remove .no_content() or call .return_fields(...). |
'@price:[270]' syntax not yet supported | Single-value numeric-bracket form is a Redis 8 server feature, but Query builder validation may reject it pre-Redis-8 clients. | Use @price:[270 270] or NumericFilter("price", 270, 270) (mirrors query_em.py). |
DIALECT defaults: server default is DIALECT 2 from Redis 8; older servers default to 1 and reject the vector attribute form (=>[KNN ...]). redis-py itself never injects DIALECT — you must pass .dialect(2). This is the most common silent failure mode when porting code between Redis versions.
14. Upstream examples index
Jedis equivalent: see java-jedis.md#14-upstream-examples-index.
Curated index of STEP_START labels in redis/redis-py/doctests/ so you can fetch the runnable source by step name. Files live at https://github.com/redis/redis-py/blob/master/doctests/<file>.
| Step label | Operation | Upstream file |
|---|---|---|
connect | redis.Redis(host=..., decode_responses=True) | search_quickstart.py |
data_sample | Bicycle JSON document shape | search_quickstart.py |
create_index (bicycle JSON) | JSON schema with TEXT/TAG/NUMERIC + as_name aliases | search_quickstart.py |
make_index | JSON index for users (TextField/TagField/NumericField) | home_json.py |
make_hash_index | HASH index, same fields without $. paths | home_json.py |
add_data | r.json().set(key, "$", doc) | home_json.py |
query1 | Query("Paul @age:[30 40]") — combined TEXT + NUMERIC | home_json.py |
query2 | Query("Paul").return_field("$.city", as_field="city") | home_json.py |
query3 | AggregateRequest.group_by("@city", reducers.count().alias("count")) | home_json.py |
em1 | Numeric exact match @price:[270 270] + NumericFilter | query_em.py |
em2 | TAG exact match @condition:{new} | query_em.py |
em4 | Exact phrase in TEXT | query_em.py |
range1 | Inclusive numeric range | query_range.py |
range3 | Exclusive lower bound via NumericFilter("price", "(1000", "+inf") | query_range.py |
range4 | Range + sort_by("price").paging(0, 5) | query_range.py |
ft1–ft5 | Field-scoped term, prefix, suffix, fuzzy %term%, double-fuzzy %%term%% | query_ft.py |
geo1 | Geo radius with query_params substitution | query_geo.py |
geo2 | GEOSHAPE CONTAINS, requires .dialect(3) | query_geo.py |
geo3 | GEOSHAPE WITHIN polygon | query_geo.py |
combined1–combined7 | Mixed TAG / NUMERIC / TEXT / negation / KNN pre-filter | query_combined.py |
agg1 | LOAD + APPLY (no grouping) | query_agg.py |
agg2 | APPLY + GROUPBY + REDUCE SUM | query_agg.py |
agg3 | Synthesised group key via APPLY type="'bicycle'" | query_agg.py |
agg4 | GROUPBY + REDUCE TOLIST | query_agg.py |
imports | Canonical from redis.commands.search.* import block | search_vss.py |
create_index (vector) | JSON schema with VectorField("FLAT", {...}, as_name="vector") for VSS | search_vss.py |
run_knn_query | Query("(*)=>[KNN 3 @vector $query_vector AS vector_score]") | search_vss.py |
run_hybrid_query | Pre-filter + KNN: (@brand:Peaknetic)=>[KNN ...] | search_vss.py |
run_range_query | [VECTOR_RANGE $range $query_vector]=>{$YIELD_DISTANCE_AS: ...} | search_vss.py |
Footer: async
redis.asyncio.Redis mirrors the sync API for FT.* (same Query, AggregateRequest, schema imports). Out of scope for v1 of this reference — see redis-py's async tests under tests/test_asyncio/test_search.py for parallel examples. Sync semantics described above apply.
Requirements and compatibility
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Needs: Needs Python, Needs Node.js
Security
No issues found by our static scan· No issues found
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Created by
- Redis, Inc.
- Publisher
- Redis (Official)
- Source
- redis/agent-skills · skills/redis-search · commit 74708da
- Licence
- MIT License (redistribution allowed)
- LICENSE (added by AutoGineer from LICENSE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
- Also distributed in
- redis/agent-skills
AutoGineer is not affiliated with or endorsed by Redis, Inc. or Redis.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download redis-search.zip