Query Engine: Search & Aggregations
|
This content was generated with the assistance of AI and should be verified against the official Redis documentation before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
Examples on this page target Redis Open Source 8.10 (8.10.2) with the Jedis 8 and Lettuce 7.7 client
libraries, and every example runs against the idx:books index defined in
Query Engine: Indexing & Data Modeling — this page doesn’t redefine FT.CREATE
or the field list, only uses the resulting title, author, genres, price, year, and stock fields.
FT.SEARCH finds and ranks individual matching documents; FT.AGGREGATE reshapes the matching set itself — grouping, reducing, and computing over it — and the two share the same underlying query language for their
initial filter step.
Query syntax
All examples below use query dialect 2, the default for FT.SEARCH/FT.AGGREGATE since Redis 7.0 (set
explicitly with DIALECT 2 if a client or config has changed the default). A bare query without a field
modifier searches every TEXT field; @field: scopes a clause to one field.
-
Field modifiers and boolean combinators — terms separated by a space are implicitly ANDed,
\|(or the wordORbetween clauses) is a logical OR, and a leading-negates a clause. Parentheses group clauses. See Redis query syntax.
127.0.0.1:6379> FT.SEARCH idx:books "@title:redis @genres:{technology}"
127.0.0.1:6379> FT.SEARCH idx:books "@author:smith | @author:jones"
127.0.0.1:6379> FT.SEARCH idx:books "vectors -fiction"
-
Exact phrases — double-quoting a run of terms requires them adjacent and in order, rather than merely all present. See Exact match.
127.0.0.1:6379> FT.SEARCH idx:books "@title:\"streams in practice\""
-
Prefix, infix, suffix, and wildcard matching — a trailing
matches a prefix, a leadinga suffix, andterman infix;WILDCARDfields (or the query-time%fuzzy operator below) go further with full glob patterns. See Query syntax.
127.0.0.1:6379> FT.SEARCH idx:books "@title:vec*"
127.0.0.1:6379> FT.SEARCH idx:books "@title:*practice"
127.0.0.1:6379> FT.SEARCH idx:books "@title:*redis*"
-
Fuzzy matching — wrapping a term in
%…%matches it within Levenshtein distance 1 by default (%%…%%for distance 2,%%%…%%%for distance 3), useful for tolerating a typo in a user-typed search box. See Query syntax.
127.0.0.1:6379> FT.SEARCH idx:books "@title:%vektors%"
-
Numeric ranges —
@field:[min max]is inclusive on both ends;(before a bound excludes it, and+inf/-infleave a side open. See Numeric range queries.
127.0.0.1:6379> FT.SEARCH idx:books "@price:[10 30]"
127.0.0.1:6379> FT.SEARCH idx:books "@year:[(2020 +inf]"
-
Tag filters — a
TAGfield is matched with@field:{value}, and\{value1 | value2\}matches any of several exact tags in one clause (the braces are literal query syntax, not an AsciiDoc attribute reference — they need no escaping insideFT.SEARCHitself, only when this page writes them in prose). See Tag fields.
127.0.0.1:6379> FT.SEARCH idx:books "@genres:{technology}"
127.0.0.1:6379> FT.SEARCH idx:books "@genres:{fiction | technology}"
-
Geo filters —
@field:[lon lat radius unit]matches documents whoseGEOfield falls withinradiusof a point,unitone ofm/km/mi/ft. The Bookshop schema indexes each book’s warehouse as theGEOfieldwarehouse_location(see Query Engine: Indexing & Data Modeling), so the query below finds books stocked within 50 km of Madrid. See Geospatial queries.
127.0.0.1:6379> FT.SEARCH idx:books "@warehouse_location:[-3.7038 40.4168 50 km]"
-
SORTBY/LIMIT/RETURN/PARAMS—SORTBY field [ASC|DESC]orders the result set (requires the field to have been indexed withSORTABLE);LIMIT offset countpages through it;RETURN count field [field …]trims the reply to only the named fields instead of the whole document;PARAMS nargs name value [name value …]binds$nameplaceholders in the query string, so a parameterized query text can be prepared once and reused with different values without string concatenation. See FT.SEARCH.
127.0.0.1:6379> FT.SEARCH idx:books "@genres:{technology}" SORTBY price ASC LIMIT 0 10 RETURN 2 title price
127.0.0.1:6379> FT.SEARCH idx:books "@genres:{$g}" PARAMS 2 g technology DIALECT 2
Scoring, explaining, and highlighting
By default FT.SEARCH ranks matches with BM25 (SCORER BM25, the default scorer since Redis 8.0, replacing
the older BM25STD/TFIDF split), which favors term rarity and normalizes for document length; passing
SCORER TFIDF switches to plain term-frequency/inverse-document-frequency scoring instead, useful mainly for
comparing against a system built around the older model. WITHSCORES returns each result’s numeric score, and
EXPLAINSCORE (used together with WITHSCORES) additionally breaks down how that score was computed, term by
term — handy for understanding why one book outranked another rather than only that it did. See
Scoring.
127.0.0.1:6379> FT.SEARCH idx:books "@title:redis" SCORER BM25 WITHSCORES EXPLAINSCORE
HIGHLIGHT FIELDS count field [field …] [TAGS open close] wraps matched terms in the given tags (default
<b>/</b>) within the returned field values, and SUMMARIZE FIELDS count field [field …] [FRAGS num] [LEN
fragsize] [SEPARATOR sep] instead trims a long text field down to the fragments around each match — the two
combine for a typical "snippet with bolded terms" search-result listing. See
Highlighting.
127.0.0.1:6379> FT.SEARCH idx:books "@description:redis" HIGHLIGHT FIELDS 1 description TAGS "<em>" "</em>" SUMMARIZE FIELDS 1 description FRAGS 2 LEN 20
Stemming and stop words
A TEXT field is stemmed by default (using the index’s configured language, English unless FT.CREATE
specified otherwise), so a query for book also matches books, and one for running also matches run — $term in a query can be prefixed with + (e.g. +running) to force an exact, unstemmed match when that
matters. A small set of common English words (the, is, at, …) is dropped from indexing and queries
entirely as stop words unless the index was created with a custom STOPWORDS list (or STOPWORDS 0 to
disable filtering altogether); this keeps the index smaller and avoids near-universal terms dominating
relevance. See
Stemming and
Stop words.
Synonyms, spellcheck, and autocomplete
FT.SYNUPDATE index synonym-group-id [SKIPINITIALSCAN] term [term …] adds or updates a named synonym group,
so a query for any term in the group also matches documents containing any other term in the same group — useful for treating sci-fi and science fiction as interchangeable in the genres/title text without
duplicating data. See FT.SYNUPDATE.
127.0.0.1:6379> FT.SYNUPDATE idx:books scifi-group scifi "sci-fi" "science fiction"
OK
FT.SPELLCHECK index query [DISTANCE dist] [TERMS <INCLUDE|EXCLUDE> dictionary term [term …]] suggests
corrections for terms in query that don’t appear in the index, each with a normalized-score ranking of
candidate replacements — built for surfacing "did you mean" suggestions rather than issuing them as an
automatic rewrite. See FT.SPELLCHECK.
127.0.0.1:6379> FT.SPELLCHECK idx:books "vecktors"
1) 1) "TERM"
2) "vecktors"
3) 1) 1) "0.5"
2) "vectors"
Autocomplete uses a separate suggestion dictionary rather than the index itself: FT.SUGADD key string score
[INCR] [PAYLOAD payload] adds or reweights a suggestion string under a dictionary key, and FT.SUGGET key
prefix [FUZZY] [WITHSCORES] [WITHPAYLOADS] [MAX max] returns the highest-scoring suggestions matching a
typed prefix, optionally with fuzzy prefix matching for a typo-tolerant search box. See
FT.SUGADD and
FT.SUGGET.
127.0.0.1:6379> FT.SUGADD bookshop:titlesuggest "The Redis Way" 5
(integer) 1
127.0.0.1:6379> FT.SUGGET bookshop:titlesuggest "red" FUZZY WITHSCORES
1) "The Redis Way"
2) "5"
Inspecting a query: FT.EXPLAIN and FT.PROFILE
FT.EXPLAIN index query [DIALECT dialect] parses query and prints the execution tree the engine built from
it, without running it — useful for confirming how a query with mixed AND/OR/negation actually groups before
running it against real data. FT.PROFILE index <SEARCH|AGGREGATE> [LIMITED] QUERY query instead executes the
query and returns both its normal reply and a breakdown of time spent per execution stage, the tool of choice
when a query is slow and it isn’t obvious which clause or iterator is the cost. See
FT.EXPLAIN and
FT.PROFILE.
127.0.0.1:6379> FT.EXPLAIN idx:books "@genres:{technology} @price:[10 30]"
127.0.0.1:6379> FT.PROFILE idx:books SEARCH QUERY "@title:redis"
Searching from Jedis
package com.example.bookshop;
import redis.clients.jedis.RedisClient;
import redis.clients.jedis.search.Query;
import redis.clients.jedis.search.SearchResult;
public class BookSearchService {
private final RedisClient jedis;
public BookSearchService(RedisClient jedis) {
this.jedis = jedis;
}
public SearchResult searchTechnologyBooks(String term) {
Query query = new Query("@title:" + term + " @genres:{technology}")
.setSortBy("price", true)
.limit(0, 10)
.returnFields("title", "price");
return jedis.ftSearch("idx:books", query);
}
}
This derives from FT.SEARCH.
FT.AGGREGATE: reshaping the result set
Where FT.SEARCH returns matching documents, FT.AGGREGATE runs a matching set through a pipeline of
stages — loading extra fields, grouping and reducing, computing new values, sorting, and filtering — much
like a SQL GROUP BY query, and is the tool for faceted navigation, dashboards, and any "summarize, don’t
list" search.
-
FT.AGGREGATE index query [LOAD count field [field …] \| LOAD *] [GROUPBY nargs property [property …] REDUCE function nargs arg [arg …] [AS name] …] [APPLY expression AS name] [SORTBY nargs property [ASC \| DESC] … [MAX num]] [FILTER expression] [LIMIT offset count] [WITHCURSOR [COUNT count] [MAXIDLE ms]] [PARAMS nargs name value …] [DIALECT dialect]— stages run left to right in the order written, each consuming the previous stage’s output. See FT.AGGREGATE.
LOAD count field [field …] (or LOAD * for every field) pulls fields from the original document into the
pipeline — only fields actually indexed as SORTABLE, or explicitly `LOAD`ed, are available to later stages,
since aggregation otherwise only sees what the query already scored, not the full document.
GROUPBY nargs property [property …] REDUCE function nargs arg [arg …] [AS name] groups rows by one or
more properties and computes one or more reducers per group:
| Reducer | Computes |
|---|---|
|
Number of rows in the group. |
|
Sum of |
|
Mean of |
|
Smallest / largest value of |
|
Exact distinct-value count of |
|
Approximate distinct-value count via HyperLogLog — bounded memory, small error, for high-cardinality fields. |
|
Approximate value at quantile |
|
Every value of |
|
One representative value of |
|
Up to |
See Aggregations.
Run against the three Bookshop books loaded in JSON, after that page’s JSON.MERGE
raises "The Redis Way" to 21.99, fiction averages 21.99 and technology averages (34.50 + 29.00) / 2 = 31.75:
127.0.0.1:6379> FT.AGGREGATE idx:books "*" GROUPBY 1 @genres REDUCE COUNT 0 AS book_count REDUCE AVG 1 @price AS avg_price
1) (integer) 2
2) 1) "genres"
2) "fiction"
3) "book_count"
4) "1"
5) "avg_price"
6) "21.99"
3) 1) "genres"
2) "technology"
3) "book_count"
4) "2"
5) "avg_price"
6) "31.75"
APPLY expression AS name computes a new field from an expression over already-available fields (arithmetic,
string, and date functions), and FILTER expression drops rows where a boolean expression over such fields is
false — unlike a query clause, FILTER runs after LOAD/GROUPBY/APPLY, so it can filter on a computed
or reduced value the original query text has no way to reference:
127.0.0.1:6379> FT.AGGREGATE idx:books "*" GROUPBY 1 @genres REDUCE AVG 1 @price AS avg_price APPLY "avg_price * 1.21" AS avg_price_with_tax FILTER "@avg_price_with_tax > 15"
SORTBY nargs property [ASC \| DESC] … [MAX num] orders rows by one or more properties, and MAX caps how
many sorted rows are kept — a cheaper way to get a top-N than sorting the full set and then LIMIT`ing it,
since the engine only has to track the top `num rows as it scans. LIMIT offset count pages through the
final result the ordinary way.
For a result set too large to return in one reply, WITHCURSOR [COUNT count] [MAXIDLE ms] returns the first
count rows plus a cursor ID instead of the whole set, and FT.CURSOR READ index cursor-id [COUNT count]
fetches the next batch; the cursor is discarded automatically once exhausted, or after MAXIDLE milliseconds
of inactivity, or explicitly with FT.CURSOR DEL index cursor-id. See
FT.CURSOR READ.
127.0.0.1:6379> FT.AGGREGATE idx:books "*" GROUPBY 1 @genres REDUCE COUNT 0 AS book_count WITHCURSOR COUNT 100
127.0.0.1:6379> FT.CURSOR READ idx:books 342459320
Faceted navigation example
A typical faceted-search sidebar needs two independent facts about the current result set: how many books fall
into each genre, and how the average price trends per publication year. Both are single FT.AGGREGATE calls
over idx:books, run alongside (not instead of) the FT.SEARCH call that renders the actual result list:
127.0.0.1:6379> FT.AGGREGATE idx:books "*" GROUPBY 1 @genres REDUCE COUNT 0 AS book_count SORTBY 2 @book_count DESC
127.0.0.1:6379> FT.AGGREGATE idx:books "*" GROUPBY 1 @year REDUCE AVG 1 @price AS avg_price REDUCE COUNT 0 AS book_count SORTBY 2 @year ASC
The first powers a "Genre (count)" facet list; the second, a price-trend-by-year chart — both derived from FT.AGGREGATE with no extra round trip to the application tier to compute either.
Related pages
-
Query Engine: Indexing & Data Modeling —
FT.CREATEand theidx:booksschema every example on this page queries against.
References
-
Redis query syntax — field modifiers, boolean combinators, and general query structure.
-
Query syntax reference — prefix/infix/suffix, fuzzy, and wildcard matching.
-
Scoring — BM25, TF-IDF, and
EXPLAINSCORE. -
Highlighting —
HIGHLIGHTandSUMMARIZE. -
Aggregations —
GROUPBY/REDUCE,APPLY, and cursors.