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://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/java-jedis.md
Jedis — Redis Search quick reference
This reference covers the FT.* (Redis Search) surface of the Jedis client. It shows how Jedis 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, escaping):
../search-syntax-primitives.md. Do not duplicate that grammar here.redis-py (Python) equivalents for the same operations:
python-redis-py.md.RedisVL is a Python SDK only; there is no Java equivalent. For Java targets, this is the reference.
Examples below trace to specific files in redis/jedis/src/test/java/io/redis/examples/ and the broader Jedis test suite, preserving the upstream STEP_START/STEP_END labels so you can pair-verify against the runnable Java source — and against the matching Python steps in python-redis-py.md. The shared Bicycle dataset (bicycle:<n> JSON docs with brand, model, description, price, condition, type, pickup_zone, store_location, description_embeddings) is used throughout.
Async / reactive: Jedis is sync-only by design. For non-blocking I/O on Redis Search, use Lettuce — out of scope for v1 of this reference.
Table of contents
Minimum supported versions
Client class choice
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
redis-py equivalent: see python-redis-py.md#1-minimum-supported-versions.
| Component | Minimum | Notes |
|---|---|---|
| Jedis | 5.0 | 4.x predates the redis.clients.jedis.search.schemafields.* package and the fluent SchemaField[] API. Examples in this reference will not compile against 4.x. |
| Jedis (FT.HYBRID high-level API) | 6.0 | ftHybrid / FTHybridParams are not in Jedis 5.x — fall back to sendCommand(SearchCommand.HYBRID, ...) (see §11). |
| Redis server (FT.SEARCH / FT.AGGREGATE) | 7.4 | Redis Search ships built-in from Redis 8.0; on 7.4 the RediSearch module must be loaded. |
Redis server (FT.HYBRID) | 8.4.0 | Hard floor. ftHybrid returns JedisDataException: unknown command 'FT.HYBRID' on older Redis. Fall back to pre-filter + =>[KNN ...] via FT.SEARCH. |
| Java | 8+ | Jedis 5.x targets Java 8 baseline; 6.x targets Java 11+. |
DIALECT default: Jedis does not set DIALECT on your behalf. Every query in this reference passes DIALECT 2 explicitly via FTSearchParams.searchParams().dialect(2) or AggregationBuilder.dialect(2). GEOSHAPE WITHIN/CONTAINS predicates require DIALECT 3.
2. Client class choice
redis-py has a single client class (redis.Redis); no equivalent client-choice section — see python-redis-py.md#2-connection-setup for how the single class is constructed.
Jedis has accumulated several entry points. Pick one per project and stay consistent — mixing them in the same codebase forces conversions and confuses readers.
| Class | Use when… | Threading | Notes |
|---|---|---|---|
RedisClient | Current upstream default. All examples under redis/jedis/src/test/java/io/redis/examples use this. Constructed via RedisClient.create("redis://localhost:6379"). | Internal pool; safe to share across threads. | Use this for new code. |
UnifiedJedis | Single sync client without a pool wrapper — useful for tests, scripts, or single-threaded callers. | Single connection; not thread-safe. | Parent class of RedisClient and JedisPooled; appears in internal test base classes. |
JedisPooled | Pre-RedisClient recommended pooled client. Still widely used in existing apps. | Internal JedisPool; thread-safe. | Functionally equivalent to RedisClient for FT.* calls. Don't rewrite working JedisPooled code just to swap names. |
Jedis (legacy) | A single raw connection, the original 4.x-era API. | One connection; not thread-safe. Must be returned to a pool or close()'d per use. | Avoid in new code. Many community blog posts still show this pattern. |
JedisCluster | Redis Cluster. | Cluster-aware pool. | FT.* indexes are not sharded across cluster slots — see Redis Search cluster docs before using. |
Migration path: Jedis → JedisPooled (same API + connection management) → RedisClient (same API, current upstream name). All three accept the same ftSearch, ftCreate, ftAggregate etc. methods, so migration is mostly a constructor swap.
Divergence to flag: upstream Jedis examples (SearchQuickstartExample.java) use RedisClient.create("localhost", 6379) while most public-internet blog posts and older Redis docs still show UnifiedJedis or JedisPooled. If you're porting code from those sources, the FT.* method names are identical — only the construction line differs.
3. Connection setup
redis-py equivalent: see python-redis-py.md#2-connection-setup.
The canonical connect, from SearchQuickstartExample.java — STEP_START connect:
import redis.clients.jedis.RedisClient;
RedisClient jedis = RedisClient.create("localhost", 6379);
// or, URI form (used in QueryFtExample.java, QueryEmExample.java, etc.):
RedisClient jedis2 = RedisClient.create("redis://localhost:6379");RedisClient carries an internal pool and is safe to share across threads. Use it as a long-lived field; do not create one per request. Always close() it at application shutdown (or use try-with-resources for short-lived scripts):
try (RedisClient jedis = RedisClient.create("redis://localhost:6379")) {
// FT.* calls here
}For TLS / auth, prefer the URI form: redis://user:password@host:6379 or rediss://... for TLS. Configuration of pool size, timeouts, and SSL contexts goes through DefaultJedisClientConfig.builder() — out of scope here, see Jedis docs.
4. Schema imports
redis-py equivalent: see python-redis-py.md#3-schema-imports.
The canonical Jedis import block for FT.* code, mirroring SearchQuickstartExample.java and HomeJsonExample.java STEP_START import:
import redis.clients.jedis.RedisClient;
import redis.clients.jedis.exceptions.JedisDataException;
import redis.clients.jedis.json.Path2;
import redis.clients.jedis.search.*; // Query, SearchResult, Document,
// FTCreateParams, FTSearchParams,
// IndexDataType, RediSearchUtil
import redis.clients.jedis.search.schemafields.*; // TextField, TagField, NumericField,
// GeoField, GeoShapeField, VectorField
import redis.clients.jedis.search.aggr.*; // AggregationBuilder, AggregationResult,
// Reducers, SortedField, Row, Group
import redis.clients.jedis.args.SortingOrder; // ASC / DESCFor FT.HYBRID (Redis ≥ 8.4.0, Jedis ≥ 6.0):
import redis.clients.jedis.search.Combiners;
import redis.clients.jedis.search.Scorers;
import redis.clients.jedis.search.hybrid.FTHybridParams;
import redis.clients.jedis.search.hybrid.FTHybridSearchParams;
import redis.clients.jedis.search.hybrid.FTHybridVectorParams;
import redis.clients.jedis.search.hybrid.FTHybridPostProcessingParams;
import redis.clients.jedis.search.hybrid.HybridResult;Notes:
The schema field classes live under
redis.clients.jedis.search.schemafields.*— notredis.clients.jedis.search.*. Star-importing onlyredis.clients.jedis.search.*will miss them and produce confusing "cannot find symbolTextField" errors.Use
SchemaField[](the modern API) — not the deprecatedSchemaclass.Schema sc = new Schema().addTextField(...)still appears in older test bases and pre-5.0 docs; treat it as legacy. See §14 for the migration.Use
Path2— notPath. Both exist;Path2is the current one forFT.*JSON paths andjsonSetcalls. All upstream examples usePath2.
5. Create index — HASH
redis-py equivalent: see python-redis-py.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 GEOJedis — mirrors HomeJsonExample.java STEP_START make_hash_index (the upstream HASH-index example; the JSON variant is in §6):
// STEP_START create_index_hash
SchemaField[] schema = {
TextField.of("model").weight(2.0),
TextField.of("description"),
TagField.of("brand"),
TagField.of("condition"),
NumericField.of("price").sortable(),
GeoField.of("store_location")
};
jedis.ftCreate("idx:bicycle",
FTCreateParams.createParams()
.on(IndexDataType.HASH)
.addPrefix("bicycle:"),
schema
);
// STEP_ENDHASH-specific notes:
Field names in the schema are the hash field names verbatim — no
$.path, no.as("alias")call. The schema field's name is the alias.Document keys must literally start with the declared prefix (
bicycle:1,bicycle:2, …). An empty / missing prefix indexes every hash in the database.Write documents with
jedis.hset("bicycle:1", Map.of(...))— indexing happens synchronously on the write.
6. Create index — JSON
redis-py equivalent: see python-redis-py.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 TAGJedis — mirrors SearchQuickstartExample.java STEP_START create_index:
// STEP_START create_index_json
SchemaField[] schema = {
TextField.of("$.brand").as("brand"),
TextField.of("$.model").as("model"),
TextField.of("$.description").as("description"),
NumericField.of("$.price").as("price"),
TagField.of("$.condition").as("condition")
};
jedis.ftCreate("idx:bicycle",
FTCreateParams.createParams()
.on(IndexDataType.JSON)
.addPrefix("bicycle:"),
schema
);
// STEP_ENDJSON-specific notes:
TextField.of("$.brand")takes the JSONPath, not the alias. Always pair it with.as("brand"); the alias is what queries reference as@brand.Without
.as(...), Redis auto-generates an alias from the path — usable but brittle (renaming the JSON key silently breaks the index).Array projections:
TextField.of("$.tags[*]").as("tags"). Nested objects:TextField.of("$.address.city").as("city").Add documents with
jedis.jsonSet("bicycle:1", Path2.ROOT_PATH, bicycleJson)orjsonSetWithEscape(...)for a POJO that needs JSON-string-value escaping (used inSearchQuickstartExample.javaSTEP_STARTadd_documents).
7. FT.SEARCH idioms
redis-py equivalent: see python-redis-py.md#6-ftsearch-idioms.
For the query DSL itself (delimiters, operators, escaping), read ../search-syntax-primitives.md. This section shows only how Jedis binds a query to FT.SEARCH.
Two call shapes
Jedis exposes two overloads:
// String-only — convenient for simple queries.
SearchResult res = jedis.ftSearch("idx:bicycle", "@condition:{new}");
// String + FTSearchParams — the full surface (filters, dialect, sort, return fields, paging).
SearchResult res2 = jedis.ftSearch("idx:bicycle",
"@condition:{new}",
FTSearchParams.searchParams()
.returnFields("brand", "model", "price")
.sortBy("price", SortingOrder.ASC)
.limit(0, 10)
.dialect(2)
);
// Legacy: Query object (still supported; FTSearchParams is the modern path).
Query q = new Query("@condition:{new}").returnFields("brand").dialect(2);
SearchResult res3 = jedis.ftSearch("idx:bicycle", q);FTSearchParams.searchParams() is the modern fluent path used across all current upstream examples (QueryRangeExample.java, QueryGeoExample.java, QueryEmExample.java). Use it for new code; the Query class still works and is preserved for backward compatibility.
FTSearchParams method | CLI equivalent | Purpose |
|---|---|---|
.limit(offset, num) | LIMIT offset num | Result page slice. |
.sortBy(field, SortingOrder.ASC) | SORTBY field ASC|DESC | Override score-based ranking. Requires the field declared .sortable() at index time. |
.returnFields(f1, f2, ...) | RETURN n f1 f2 ... | Project only listed fields. |
.noContent() | NOCONTENT | IDs only — pair with LIMIT 0 0 for count-only queries. |
.withScores() | WITHSCORES | Append per-doc relevance score. |
.verbatim() | VERBATIM | Disable stemming. |
.dialect(2) | DIALECT 2 | Always pass this. |
.filter("field", min, max) | FILTER field min max | Inline numeric range; alternative to @field:[min max] in the expression. |
.addParam("name", value) | PARAMS n name value … | Bind $name placeholders in the expression. |
Exact match (TAG / NUMERIC) — mirrors QueryEmExample.java
// STEP_START em1 — numeric exact match via range with equal bounds
SearchResult res1 = jedis.ftSearch("idx:bicycle", "@price:[270 270]");
// Equivalent via FILTER (no inline range):
SearchResult res2 = jedis.ftSearch("idx:bicycle", "*",
FTSearchParams.searchParams().filter("price", 270, 270));
// STEP_START em2 — tag exact match
SearchResult res3 = jedis.ftSearch("idx:bicycle", "@condition:{new}");
// STEP_START em4 — exact phrase in TEXT
SearchResult res5 = jedis.ftSearch("idx:bicycle", "@description:\"rough terrain\"");Numeric ranges — mirrors QueryRangeExample.java
// STEP_START range1 — inclusive
SearchResult res1 = jedis.ftSearch("idx:bicycle", "@price:[500 1000]",
FTSearchParams.searchParams().returnFields("price").dialect(2));
// STEP_START range3 — exclusive lower, unbounded upper, via FTSearchParams.filter
SearchResult res3 = jedis.ftSearch("idx:bicycle", "*",
FTSearchParams.searchParams()
.returnFields("price")
.filter("price", 1000, true, Double.POSITIVE_INFINITY, false)
.dialect(2));
// STEP_START range4 — sorted + paged
SearchResult res4 = jedis.ftSearch("idx:bicycle", "@price:[-inf 2000]",
FTSearchParams.searchParams()
.returnFields("price")
.sortBy("price", SortingOrder.ASC)
.limit(0, 5)
.dialect(2));.filter(field, min, minExclusive, max, maxExclusive) is Jedis's typed equivalent of "@price:[(1000 +inf]". Pass Double.POSITIVE_INFINITY / Double.NEGATIVE_INFINITY for unbounded ends.
Full-text idioms — mirrors QueryFtExample.java
// STEP_START ft1 — field-scoped term
SearchResult res1 = jedis.ftSearch("idx:bicycle", "@description: kids");
// STEP_START ft2 — prefix
SearchResult res2 = jedis.ftSearch("idx:bicycle", "@model: ka*");
// STEP_START ft3 — suffix (requires WITHSUFFIXTRIE at index time for efficiency)
SearchResult res3 = jedis.ftSearch("idx:bicycle", "@brand: *bikes");
// STEP_START ft4 — fuzzy (Levenshtein distance 1)
SearchResult res4 = jedis.ftSearch("idx:bicycle", "%optamized%");
// STEP_START ft5 — fuzzy distance 2 (double % per side)
SearchResult res5 = jedis.ftSearch("idx:bicycle", "%%optamised%%");Geo — mirrors QueryGeoExample.java
// STEP_START geo1 — radius query, parameterised
SearchResult res1 = jedis.ftSearch("idx:bicycle",
"@store_location:[$lon $lat $radius $units]",
FTSearchParams.searchParams()
.addParam("lon", -0.1778)
.addParam("lat", 51.5524)
.addParam("radius", 20)
.addParam("units", "mi")
.dialect(2));
// STEP_START geo2 — GEOSHAPE CONTAINS (requires DIALECT 3)
SearchResult res2 = jedis.ftSearch("idx:bicycle",
"@pickup_zone:[CONTAINS $bike]",
FTSearchParams.searchParams()
.addParam("bike", "POINT(-0.1278 51.5074)")
.dialect(3));
// STEP_START geo3 — GEOSHAPE WITHIN polygon
SearchResult res3 = jedis.ftSearch("idx:bicycle",
"@pickup_zone:[WITHIN $europe]",
FTSearchParams.searchParams()
.addParam("europe", "POLYGON((-25 35, 40 35, 40 70, -25 70, -25 35))")
.dialect(3));Note that GEOSHAPE fields need GeoShapeField.of("$.pickup_zone", GeoShapeField.CoordinateSystem.FLAT).as("pickup_zone") in the schema (see QueryGeoExample.java).
Reading results
SearchResult exposes:
res.getTotalResults(); // server-reported match count (long)
res.getDocuments(); // List<Document>
for (Document doc : res.getDocuments()) {
doc.getId(); // "bicycle:0"
doc.getScore(); // double, when .withScores() was set
doc.getString("brand"); // typed field access
doc.get("price"); // raw Object value
doc.hasProperty("price"); // existence check
}For HASH indexes, field values come back as String. For JSON indexes without .returnFields(...), Jedis returns the whole JSON document under the $ property (see SearchQuickstartExample.java STEP_START query_single_term output comments).
8. FT.AGGREGATE idioms
redis-py equivalent: see python-redis-py.md#7-ftaggregate-idioms.
For pipeline-stage ordering rules, see aggregate-pipeline.md. This section shows only the Jedis builder shape.
The AggregationBuilder
AggregationBuilder("<filter-expression>") is Jedis's FT.AGGREGATE shape, separate from Query. Fluent setters map directly to pipeline stages:
AggregationBuilder method | CLI stage |
|---|---|
.load(field1, field2, ...) | LOAD n f1 f2 ... |
.apply("<expr>", "alias") | APPLY <expr> AS alias (note: expression first, alias second — opposite of redis-py's keyword form) |
.filter("<expr>") | FILTER <expr> |
.groupBy("@field", Reducers.X.as("alias")) | GROUPBY n field REDUCE ... |
.sortBy(SortedField.asc("@field")) / .sortBy(n, SortedField.desc("@field")) | SORTBY n <field> ASC|DESC |
.limit(offset, num) | LIMIT offset num |
.cursor(count, maxIdleMs) | WITHCURSOR [COUNT n] [MAXIDLE ms] (see §9) |
.dialect(2) | DIALECT 2 |
Reducers live in redis.clients.jedis.search.aggr.Reducers as static factory methods. 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.to_list("@f") | REDUCE TOLIST 1 @f |
Every reducer takes .as("alias") to set the AS <alias> token.
Worked pipeline — mirrors QueryAggExample.java
// STEP_START agg1 — LOAD + APPLY (no grouping)
AggregationResult res1 = jedis.ftAggregate("idx:bicycle",
new AggregationBuilder("@condition:{new}")
.load("__key", "price")
.apply("@price - (@price * 0.1)", "discounted")
.dialect(2));
// Rows: {__key=bicycle:0, discounted=243, price=270}, ...
// STEP_START agg2 — APPLY + GROUPBY + REDUCE
AggregationResult res2 = jedis.ftAggregate("idx:bicycle",
new AggregationBuilder("*")
.load("price")
.apply("@price<1000", "price_category")
.groupBy("@condition", Reducers.sum("@price_category").as("num_affordable"))
.dialect(2));
// STEP_START agg3 — synthesised group key via APPLY
AggregationResult res3 = jedis.ftAggregate("idx:bicycle",
new AggregationBuilder("*")
.apply("'bicycle'", "type")
.groupBy("@type", Reducers.count().as("num_total"))
.dialect(2));
// Rows: {type=bicycle, num_total=10}
// STEP_START agg4 — GROUPBY + TOLIST
AggregationResult res4 = jedis.ftAggregate("idx:bicycle",
new AggregationBuilder("*")
.load("__key")
.groupBy("@condition", Reducers.to_list("__key").as("bicycles"))
.dialect(2));Result shape: AggregationResult.getRows() returns List<Row>. Per row:
Row r = res2.getRows().get(0);
r.getString("condition"); // "new"
r.getLong("num_affordable"); // 3
r.getDouble("avg_price"); // when applicable
r.get("bicycles"); // raw value for TOLIST (ArrayList<String>)Argument order gotcha: .apply(expression, alias) puts the expression first. redis-py uses the opposite order via keyword: apply(discounted="@price * 0.9") — keyword is the alias. When porting from Python, swap.
9. Cursors
redis-py equivalent: see python-redis-py.md#8-cursors.
For lifecycle rules and when to use cursors, see aggregate-cursors.md.
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>Jedis — open a cursor (AggregationCommandsTestBase.java STEP_START cursor):
// STEP_START aggregate_cursor_open
AggregationBuilder ab = new AggregationBuilder("*")
.groupBy("@brand", Reducers.count().as("n"))
.sortBy(10, SortedField.desc("@n"))
.cursor(1000, 30000) // COUNT 1000, MAXIDLE 30000 ms
.dialect(2);
AggregationResult page = jedis.ftAggregate("idx:bicycle", ab);
long cursorId = page.getCursorId();
// STEP_ENDRead subsequent pages:
// STEP_START aggregate_cursor_read
while (cursorId != 0) { // 0 signals exhausted server-side cursor
page = jedis.ftCursorRead("idx:bicycle", cursorId, 1000);
cursorId = page.getCursorId();
process(page.getRows());
}
// STEP_ENDExplicit cleanup (release before MAXIDLE):
// STEP_START aggregate_cursor_del
jedis.ftCursorDel("idx:bicycle", cursorId);
// STEP_ENDHigher-level helper. Jedis also exposes ftAggregateIteration(...) which encapsulates the cursor loop and exposes nextBatch() / collect(...), mirroring AggregationCommandsTestBase.java aggregateIteration test:
FtAggregateIteration it = jedis.ftAggregateIteration("idx:bicycle", ab);
while (!it.isIterationCompleted()) {
AggregationResult batch = it.nextBatch();
process(batch.getRows());
}Use the helper for straightforward "drain to the end" cases; fall back to manual ftCursorRead / ftCursorDel when you need per-batch flow control or explicit cleanup on cancellation.
10. Vector queries
redis-py equivalent: see python-redis-py.md#9-vector-queries.
For query-attribute syntax (=>[KNN ...], [VECTOR_RANGE ...]) and pre-filter shape, read vector-query.md.
Note on upstream sourcing. VectorSetExample.java in redis/jedis/src/test/java/io/redis/examples demonstrates the Redis Vector Set data type (VADD, VSIM) — a separate feature, not FT.* vector indexing. The canonical FT.* vector tests live in redis/jedis/src/test/java/redis/clients/jedis/commands/unified/search/SearchWithParamsCommandsTestBase.java (methods testHNSWVectorSimilarity, testFlatVectorSimilarity, vectorSearchProfile). Examples below mirror those.
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 COSINEJedis — mirrors SearchWithParamsCommandsTestBase.java testHNSWVectorSimilarity adapted to the bicycle schema (dim 1536 matches OpenAI text-embedding-3-small / ada-002):
// STEP_START create_vector_index
import redis.clients.jedis.search.schemafields.VectorField;
import redis.clients.jedis.search.schemafields.VectorField.VectorAlgorithm;
int VECTOR_DIMENSION = 1536; // match your embedding model
Map<String, Object> vectorAttrs = new HashMap<>();
vectorAttrs.put("TYPE", "FLOAT32");
vectorAttrs.put("DIM", VECTOR_DIMENSION);
vectorAttrs.put("DISTANCE_METRIC", "COSINE");
SchemaField[] schema = {
TextField.of("$.model").noStem().as("model"),
TextField.of("$.brand").noStem().as("brand"),
NumericField.of("$.price").as("price"),
TagField.of("$.type").as("type"),
VectorField.builder()
.fieldName("$.description_embeddings")
.algorithm(VectorAlgorithm.FLAT) // or HNSW for ANN
.attributes(vectorAttrs)
.build()
.as("vector")
};
jedis.ftCreate("idx:bicycle",
FTCreateParams.createParams()
.on(IndexDataType.JSON)
.addPrefix("bicycle:"),
schema);
// STEP_ENDEncode the query vector
The de facto pattern — float[] → little-endian byte[]. Jedis ships a helper: RediSearchUtil.toByteArray(float[]):
import redis.clients.jedis.search.RediSearchUtil;
byte[] queryBytes = RediSearchUtil.toByteArray(embedding); // dim 1536 float[]Equivalent explicit form (mirrors FTHybridCommandsTestBase.java floatArrayToByteArray and the redis-py convention):
static byte[] floatArrayToByteArray(float[] floats) {
ByteBuffer buf = ByteBuffer.allocate(floats.length * 4).order(ByteOrder.LITTLE_ENDIAN);
for (float f : floats) buf.putFloat(f);
return buf.array();
}FLOAT32 little-endian is the only encoding redis-py and Jedis ship with — match this on both index and query side, every time. A double[] (or big-endian buffer) silently produces zero hits because per-element byte offsets disagree with the index's TYPE FLOAT32.
KNN — mirrors SearchWithParamsCommandsTestBase.java testHNSWVectorSimilarity
// STEP_START vector_knn
FTSearchParams searchParams = FTSearchParams.searchParams()
.addParam("query_vector", queryBytes)
.sortBy("vector_score", SortingOrder.ASC)
.returnFields("vector_score", "brand", "model", "description")
.dialect(2);
SearchResult res = jedis.ftSearch("idx:bicycle",
"(*)=>[KNN 3 @vector $query_vector AS vector_score]",
searchParams);
// STEP_ENDAS vector_score aliases the distance field — sort by it and return it just like any other field.
Pre-filtered KNN — mirrors the query_combined.py shape from redis-py
// STEP_START vector_prefilter (pre-Redis-8.4 hybrid pattern — for native blended ranking see §11)
SearchResult res = jedis.ftSearch("idx:bicycle",
"(@price:[500 1000] -@condition:{new})=>[KNN 3 @vector $query_vector AS vector_score]",
FTSearchParams.searchParams()
.addParam("query_vector", queryBytes)
.sortBy("vector_score", SortingOrder.ASC)
.returnFields("vector_score", "brand", "model", "price")
.dialect(2));
// 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 SearchWithParamsCommandsTestBase.java vector range pattern
// STEP_START vector_range
SearchResult res = jedis.ftSearch("idx:bicycle",
"@vector:[VECTOR_RANGE $range $query_vector]=>{$YIELD_DISTANCE_AS: vector_score}",
FTSearchParams.searchParams()
.addParam("range", 0.55)
.addParam("query_vector", queryBytes)
.sortBy("vector_score", SortingOrder.ASC)
.returnFields("vector_score", "brand", "model", "description")
.limit(0, 4)
.dialect(2));
// STEP_ENDAS <alias> (KNN form) and $YIELD_DISTANCE_AS: <alias> (RANGE form) are not interchangeable — the syntax differs by query type.
HNSW tuning per-query
EF_RUNTIME is an in-query attribute on the KNN tail:
jedis.ftSearch("idx:bicycle",
"*=>[KNN 10 @vector $query_vector EF_RUNTIME 200 AS score]",
FTSearchParams.searchParams().addParam("query_vector", queryBytes).dialect(2));Index-time EF_CONSTRUCTION lives in the VectorField attributes map and is independent of EF_RUNTIME.
11. FT.HYBRID
redis-py equivalent: see python-redis-py.md#10-fthybrid.
Version gate: FT.HYBRID requires Redis ≥ 8.4.0. On older Redis use the pre-filter + KNN pattern in §10. See command-selection.md for the SEARCH vs AGGREGATE vs HYBRID decision.
Jedis client gate: the high-level ftHybrid method requires Jedis 6.x. Jedis 5.x users must use the sendCommand fallback shown at the bottom of this section.
High-level builder
Jedis 6.x ships a high-level ftHybrid method backed by FTHybridParams.builder(). Pattern: build a FTHybridSearchParams (text leg) + FTHybridVectorParams (vector leg), combine with a Combiners.rrf() or Combiners.linear(), optionally add a FTHybridPostProcessingParams for LOAD / GROUPBY / APPLY / SORTBY / FILTER / LIMIT stages. Mirrors FTHybridCommandsTestBase.java testComprehensiveFtHybridWithAllFeatures:
// STEP_START run_hybrid_query_native
import redis.clients.jedis.search.Combiners;
import redis.clients.jedis.search.Scorers;
import redis.clients.jedis.search.hybrid.*;
import redis.clients.jedis.search.aggr.Group;
import redis.clients.jedis.search.aggr.Reducers;
import redis.clients.jedis.search.aggr.SortedField;
import redis.clients.jedis.search.Apply;
import redis.clients.jedis.search.Filter;
import redis.clients.jedis.search.Limit;
FTHybridPostProcessingParams postProcessing = FTHybridPostProcessingParams.builder()
.load("price", "brand", "@category")
.groupBy(new Group("@brand")
.reduce(Reducers.sum("@price").as("sum"))
.reduce(Reducers.count().as("count")))
.apply(Apply.of("@sum * 0.9", "discounted_price"))
.sortBy(SortedField.asc("@sum"), SortedField.desc("@count"))
.filter(Filter.of("@sum > 700"))
.limit(Limit.of(0, 20))
.build();
FTHybridParams hybridArgs = FTHybridParams.builder()
.search(FTHybridSearchParams.builder()
.query("@category:{electronics} smartphone camera")
.scorer(Scorers.bm25std())
.scoreAlias("text_score")
.build())
.vectorSearch(FTHybridVectorParams.builder()
.field("@image_embedding")
.vector("vector") // param name, bound below
.method(FTHybridVectorParams.Knn.of(20).efRuntime(150))
.filter("(@brand:{apple|samsung|google}) (@price:[500 1500])")
.scoreAlias("vector_score")
.build())
.combine(Combiners.linear().alpha(0.7).beta(0.3).window(25)) // or Combiners.rrf().window(60)
.postProcessing(postProcessing)
.param("vector", queryBytes)
.build();
HybridResult reply = jedis.ftHybrid("idx:products", hybridArgs);
reply.getTotalResults();
reply.getDocuments(); // List<Document>
reply.getExecutionTime(); // server-side timing (double, ms)
reply.getWarnings();
// STEP_ENDCombine methods
| Factory | CLI emitted | When to use |
|---|---|---|
Combiners.rrf() | COMBINE RRF count [CONSTANT c] [WINDOW w] | Default for blended ranking. Reciprocal Rank Fusion — rank-based, robust without weight tuning. Knobs: .window(int), .constant(double) (typically 60). |
Combiners.linear() | COMBINE LINEAR count [ALPHA a] [BETA b] [WINDOW w] | Weighted score blend. Needs .alpha(double) / .beta(double) tuned to your scorer scales. |
Both expose .as("alias") to alias the final combined score.
Important behaviours
ftHybridis annotated@Experimentalin Jedis. Pin your Jedis minor version if you depend on it in production; the builder API may shift between minors.FTHybridSearchParamsandFTHybridVectorParamsuse different builders — the search leg owns the text query and scorer; the vector leg owns the vector field, KNN/range method, optional internal.filter(...)(applied before the vector scan), and per-leg.scoreAlias(...).FTHybridVectorParams.Knn.of(int)setsK; chain.efRuntime(int)to tune HNSW per query.Vector blob is bound by name via the top-level
.param("vector", byte[])— samePARAMS-binding mechanism as FT.SEARCH.FTHybridPostProcessingParams.load(...)-returned field values may come back asbyte[]rather thanStringdepending on protocol and field type — defensive callers should check withinstanceofbefore casting (mirrors theredis-pyHYBRID gotcha).
Raw sendCommand fallback (Jedis 5.x or features missing from the builder)
For Jedis 5.x callers — or features that have not yet landed in the high-level builder — drop to the binary sendCommand overload. Encoding the vector through new String(bytes, ISO_8859_1) is lossy on RESP3 and corrupts certain byte values; pass the raw byte[] to sendCommand(ProtocolCommand, byte[]...) instead:
import redis.clients.jedis.search.SearchProtocol.SearchCommand;
import redis.clients.jedis.util.SafeEncoder;
byte[] queryBytes = RediSearchUtil.toByteArray(embedding); // FLOAT32 little-endian
// UnifiedJedis (parent of RedisClient / JedisPooled) exposes sendCommand(ProtocolCommand, byte[]...).
Object raw = ((UnifiedJedis) jedis).sendCommand(
SearchCommand.HYBRID,
SafeEncoder.encode("idx:products"),
SafeEncoder.encode("SEARCH"), SafeEncoder.encode("laptop"),
SafeEncoder.encode("VSIM"), SafeEncoder.encode("@description_vector"),
SafeEncoder.encode("$query_vec"),
SafeEncoder.encode("KNN"), SafeEncoder.encode("2"),
SafeEncoder.encode("K"), SafeEncoder.encode("10"),
SafeEncoder.encode("COMBINE"), SafeEncoder.encode("RRF"),
SafeEncoder.encode("2"),
SafeEncoder.encode("WINDOW"), SafeEncoder.encode("100"),
SafeEncoder.encode("PARAMS"), SafeEncoder.encode("2"),
SafeEncoder.encode("query_vec"),
queryBytes, // raw vector — DO NOT round-trip through String
SafeEncoder.encode("DIALECT"), SafeEncoder.encode("2")
);Key points:
Use
SearchCommand.HYBRID(redis.clients.jedis.search.SearchProtocol.SearchCommand) rather than an ad-hocProtocolCommandanonymous class — it's the canonical enum and survives upstream renames.Use
((UnifiedJedis) jedis).sendCommand(ProtocolCommand, byte[]...)(the binary varargs form, defined onUnifiedJedis). TheString...overload silently UTF-8-encodes its arguments and mangles vector bytes on the wire.SafeEncoder.encode(String)is Jedis's canonical UTF-8 string→byte[]helper — use it for every text argument so the wire bytes match what the high-level API would emit.The vector
byte[]is passed in directly; nonew String(queryBytes, ISO_8859_1)round-trip.
The raw shape mirrors the verified syntax in spec 0001 §5.0a. Use it only when the high-level ftHybrid builder lacks a flag you need — and consider opening an issue upstream once you confirm the gap.
Upstream sources: src/main/java/redis/clients/jedis/search/hybrid/FTHybridParams.java, src/main/java/redis/clients/jedis/search/Combiners.java, src/test/java/redis/clients/jedis/commands/unified/search/FTHybridCommandsTestBase.java.
12. Debugging
redis-py equivalent: see python-redis-py.md#11-debugging.
For interpreting FT.EXPLAIN and FT.PROFILE output, see debugging.md.
FT.EXPLAIN
// Pass either a Query object or a raw query string.
String plan = jedis.ftExplain("idx:bicycle",
new Query("(@brand:{Velorim}) @price:[100 500]").dialect(2));
System.out.println(plan);
// INTERSECT {
// TAG:@brand { Velorim }
// NUMERIC {100.000000 <= @price <= 500.000000}
// }The output is the server's parse tree — useful for spotting unexpected stemming, tokenization, or operator-precedence surprises.
FT.PROFILE
import redis.clients.jedis.search.FTProfileParams;
import redis.clients.jedis.search.ProfilingInfo;
Map.Entry<SearchResult, ProfilingInfo> reply = jedis.ftProfileSearch("idx:bicycle",
FTProfileParams.profileParams(),
"@brand:{Velorim}",
FTSearchParams.searchParams().dialect(2));
SearchResult result = reply.getKey();
Object profile = reply.getValue().getProfilingInfo(); // shape depends on protocol (RESP2/RESP3)For aggregations:
Map.Entry<AggregationResult, ProfilingInfo> aggReply = jedis.ftProfileAggregate("idx:bicycle",
FTProfileParams.profileParams(),
new AggregationBuilder("*").groupBy("@brand", Reducers.count().as("n")).dialect(2));The ProfilingInfo payload is protocol-shaped: on RESP3 it's a Map<String, Object> with Shards / Coordinator top-level keys (Redis 8+); on RESP2 it's a nested List. Cast accordingly — see SearchWithParamsCommandsTestBase.java vectorSearchProfile for the pattern.
FT.INFO
Map<String, Object> info = jedis.ftInfo("idx:bicycle");
info.get("index_name"); // "idx:bicycle"
info.get("num_docs"); // server-stringified counts; cast as needed
info.get("hash_indexing_failures"); // non-zero = silent dropouts (schema mismatch)
info.get("attributes"); // List of per-field detail maps
info.get("inverted_sz_mb"); // memory footprint
info.get("indexing"); // 1 while background scan runs
info.get("percent_indexed"); // 0.0 – 1.0ftInfo returns a Map<String, Object> because the server's reply mixes scalars, lists, and maps. Treat any numeric you read from it as protocol-dependent: RESP2 typically gives strings, RESP3 typed values. Cast defensively.
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. |
13. Index management
redis-py equivalent: see python-redis-py.md#12-index-management.
For semantics (FT.ALTER capacity, alias use cases), see index-management.md.
Add fields
jedis.ftAlter("idx:bicycle",
TagField.of("availability"),
TextField.of("name").weight(0.5));ftAlter accepts a varargs of SchemaField. Mirrors SearchWithParamsCommandsTestBase.java alter test. Subject to 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)
jedis.ftAliasAdd("idx:bicycle:active", "idx:bicycle_v2");
jedis.ftAliasUpdate("idx:bicycle:active", "idx:bicycle_v2"); // repoint
jedis.ftAliasDel("idx:bicycle:active");Argument order is (alias, indexName) — Jedis aliases come first, the underlying index second. Mirrors SearchWithParamsCommandsTestBase.java alias test. 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
jedis.ftDropIndex("idx:bicycle");
// Drop index AND delete every indexed document (destructive)
jedis.ftDropIndexDD("idx:bicycle");ftDropIndexDD is the equivalent of FT.DROPINDEX ... DD — gone forever, no undo. The double-D in the name signals "drop the docs too."
List indexes
Set<String> all = jedis.ftList();14. Common errors & version gotchas
redis-py equivalent: see python-redis-py.md#13-common-errors--version-gotchas.
| Symptom | Likely cause | Fix |
|---|---|---|
JedisDataException: 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. |
JedisDataException: unknown command 'FT.HYBRID' | Server < 8.4.0. | Upgrade or fall back to pre-filter + KNN via FT.SEARCH (§10). |
Syntax error at offset N near KNN | Missing DIALECT 2. | FTSearchParams.searchParams().dialect(2) on every vector query and every modern parser feature. |
| 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. |
JedisDataException: Vector dimension mismatch | Query vector dim differs from index DIM. | Recompute embedding with the same model used at index time; assert embedding.length == DIM. |
| Vector query returns 0 hits despite obvious matches | Query vector encoded big-endian or as double[]. | Use RediSearchUtil.toByteArray(float[]) or ByteBuffer.allocate(n*4).order(LITTLE_ENDIAN).putFloat(...). |
cannot find symbol: class TextField | Missing import redis.clients.jedis.search.schemafields.*; — redis.clients.jedis.search.* does not pull in field types. | Add the schemafields.* import explicitly. See §4. |
JedisDataException: Index already exists | ftCreate is not "create or replace." | Wrap idempotent setup in try/catch on JedisDataException, or ftDropIndex first when bootstrapping. |
| JSON paths not matching docs | Document set with jedis.jsonSet(...) but index defined ON HASH (or vice versa). | Match IndexDataType to write path. ftInfo's hash_indexing_failures > 0 is the signal. |
Cannot resolve method 'addTextField' after upgrade to Jedis 5.x | Code uses deprecated Schema class. | Migrate to SchemaField[] (see migration below). |
Cannot resolve symbol 'Path' after upgrade | Code uses redis.clients.jedis.json.Path. | Switch to redis.clients.jedis.json.Path2 — current path API for FT.* and JSON commands. |
Empty getDocuments() but non-zero getTotalResults() | .noContent() was set. | Remove .noContent() or call .returnFields(...). |
Schema → SchemaField[] migration
Older Jedis code (pre-5.x, or community blog posts) uses the Schema class:
// LEGACY — Schema class, do not use in new code.
Schema sc = new Schema()
.addSortableTextField("name", 1.0)
.addSortableNumericField("count")
.addTagField("tags");
jedis.ftCreate(INDEX, IndexOptions.defaultOptions(), sc);Current API:
// MODERN — SchemaField[] + FTCreateParams.
SchemaField[] schema = {
TextField.of("name").weight(1.0).sortable(),
NumericField.of("count").sortable(),
TagField.of("tags")
};
jedis.ftCreate(INDEX, FTCreateParams.createParams(), schema);Translation rules:
addTextField(name, weight)→TextField.of(name).weight(weight)addSortableTextField(...)→.sortable()chainedaddNumericField(name)→NumericField.of(name)addTagField(name)→TagField.of(name)addVectorField(name, algo, attrs)→VectorField.builder().fieldName(name).algorithm(algo).attributes(attrs).build()IndexOptions.defaultOptions()→FTCreateParams.createParams()(then chain.on(IndexDataType.JSON),.addPrefix(...), etc.)
Path vs Path2
| Class | Status | Use it |
|---|---|---|
redis.clients.jedis.json.Path | Legacy | No |
redis.clients.jedis.json.Path2 | Current | Yes — for jsonSet, jsonGet, jsonDel, and any $.* paths the FT.* schema references. All upstream examples use Path2. |
15. Upstream examples index
redis-py equivalent: see python-redis-py.md#14-upstream-examples-index.
Curated index of STEP_START labels in redis/jedis/src/test/java/io/redis/examples/ and the broader Jedis FT.* test suite, so you can fetch the runnable Java source by step name. Step labels match the redis-py reference where the two clients cover the same operation — pair them up to verify cross-language behaviour.
| Step label | Operation | Upstream file |
|---|---|---|
connect | RedisClient.create("localhost", 6379) | SearchQuickstartExample.java |
create_index (bicycle JSON) | JSON schema with TEXT/TAG/NUMERIC + .as(alias) | SearchQuickstartExample.java |
add_documents | jsonSetWithEscape(key, bicyclePojo) | SearchQuickstartExample.java |
wildcard_query | Query("*") | SearchQuickstartExample.java |
query_single_term | Query("@model:Jigger") | SearchQuickstartExample.java |
query_single_term_limit_fields | Query("@model:Jigger").returnFields("price") | SearchQuickstartExample.java |
query_single_term_and_num_range | Query("basic @price:[500 1000]") | SearchQuickstartExample.java |
query_exact_matching | Query("@brand:\"Noka Bikes\"") | SearchQuickstartExample.java |
simple_aggregation | AggregationBuilder("*").groupBy(...).count() | SearchQuickstartExample.java |
import | Canonical import block | HomeJsonExample.java |
make_index | JSON index for users | HomeJsonExample.java |
make_hash_index | HASH index, same fields without $. paths | HomeJsonExample.java |
add_data | jedis.jsonSet(key, Path2.ROOT_PATH, doc) | HomeJsonExample.java |
query1 | ftSearch("idx:users", "Paul @age:[30 40]") | HomeJsonExample.java |
query2 | FTSearchParams.searchParams().returnFields("city") | HomeJsonExample.java |
query3 | AggregationBuilder("*").groupBy("@city", Reducers.count().as("count")) | HomeJsonExample.java |
em1 | Numeric exact match @price:[270 270] + .filter("price", 270, 270) | QueryEmExample.java |
em2 | TAG exact match @condition:{new} | QueryEmExample.java |
em3 | Escaping @email via RediSearchUtil.escapeQuery | QueryEmExample.java |
em4 | Exact phrase @description:"rough terrain" | QueryEmExample.java |
ft1–ft5 | Field-scoped term, prefix, suffix, fuzzy %term%, double-fuzzy %%term%% | QueryFtExample.java |
range1 | Inclusive @price:[500 1000] | QueryRangeExample.java |
range2 | .filter("price", 500, 1000) form | QueryRangeExample.java |
range3 | .filter("price", 1000, true, +inf, false) (exclusive lower) | QueryRangeExample.java |
range4 | Range + .sortBy(..., ASC).limit(0, 5) | QueryRangeExample.java |
geo1 | Geo radius parameterised via .addParam | QueryGeoExample.java |
geo2 | GEOSHAPE CONTAINS with .dialect(3) | QueryGeoExample.java |
geo3 | GEOSHAPE WITHIN polygon | QueryGeoExample.java |
agg1 | LOAD + APPLY (no grouping) | QueryAggExample.java |
agg2 | APPLY + GROUPBY + Reducers.sum | QueryAggExample.java |
agg3 | Synthesised group key via .apply("'bicycle'", "type") | QueryAggExample.java |
agg4 | GROUPBY + Reducers.to_list("__key") | QueryAggExample.java |
aggregate_cursor_open (in-doc; pairs with redis-py aggregate_cursor_open) | .cursor(count, maxIdle) opens the cursor; getCursorId() reads it | AggregationCommandsTestBase.java (cursor() test) |
aggregate_cursor_read (in-doc; pairs with redis-py aggregate_cursor_read) | ftCursorRead(index, cursorId, count) page loop | AggregationCommandsTestBase.java (cursor() test) |
aggregate_cursor_del (in-doc; pairs with redis-py aggregate_cursor_del) | ftCursorDel(index, cursorId) explicit release | AggregationCommandsTestBase.java (cursor() test) |
aggregateIteration (upstream method name) | ftAggregateIteration(...) higher-level loop helper | AggregationCommandsTestBase.java |
vector_knn (in-doc; pairs with redis-py vector_knn) | (*)=>[KNN 3 @vector $query_vector AS vector_score] | SearchWithParamsCommandsTestBase.java (testHNSWVectorSimilarity) |
vector_prefilter (in-doc; pairs with redis-py vector_prefilter) | (@price:[…] -@condition:{new})=>[KNN 3 @vector $query_vector …] | SearchWithParamsCommandsTestBase.java (testHNSWVectorSimilarity) |
vector_range (in-doc; pairs with redis-py vector_range) | @vector:[VECTOR_RANGE $range $query_vector]=>{$YIELD_DISTANCE_AS: …} | SearchWithParamsCommandsTestBase.java |
testHNSWVectorSimilarity | VectorField.builder().algorithm(HNSW) + *=>[KNN 2 @v $vec] | SearchWithParamsCommandsTestBase.java |
testFlatVectorSimilarity | VectorField.builder().algorithm(FLAT) + *=>[KNN 2 @v $vec] | SearchWithParamsCommandsTestBase.java |
vectorSearchProfile | KNN inside ftProfileSearch | SearchWithParamsCommandsTestBase.java |
testComprehensiveFtHybridWithAllFeatures | Full ftHybrid with linear() combiner + post-processing pipeline | FTHybridCommandsTestBase.java |
alter | ftAlter(index, TagField.of(...), TextField.of(...).weight(0.5)) | SearchWithParamsCommandsTestBase.java |
alias | ftAliasAdd / ftAliasUpdate / ftAliasDel | SearchWithParamsCommandsTestBase.java |
ftExplain | ftExplain(index, new Query(...).dialect(2)) | SearchWithParamsCommandsTestBase.java |
info | ftInfo(index) returns Map<String, Object> | SearchWithParamsCommandsTestBase.java |
Examples files live under https://github.com/redis/jedis/tree/master/src/test/java/io/redis/examples/. The FT.* test bases (SearchWithParamsCommandsTestBase, AggregationCommandsTestBase, FTHybridCommandsTestBase) live under https://github.com/redis/jedis/tree/master/src/test/java/redis/clients/jedis/commands/unified/search/.
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