Chapter 09 · Redis Search: Indexes, Text/Numeric/Tag/Geo Fields, Querying, and Aggregation
FT.SEARCH Query Syntax, Boolean Logic, Phrase/Prefix/Fuzzy Search, Sorting, and Pagination
Compose FT.SEARCH safely with explicit dialect, boolean/phrase/prefix/fuzzy syntax, projections, sorting, parameters, and deterministic paging.
Learning outcomes
AtlasMart now has correctly typed index fields. The next risk is query composition: a syntactically small Search string can encode boolean logic, phrase constraints, prefix/fuzzy expansion, sorting, projections, and pagination, and each of those choices changes correctness or cost. The safest approach is to make the dialect and result-order contract explicit.
Compose scoped TEXT/TAG/NUMERIC/GEO predicates with explicit boolean logic and parentheses.
Use exact phrases, prefixes, fuzzy terms, negation, and optional clauses without confusing their retrieval semantics.
Use PARAMS and explicit DIALECT 2 where values or modern syntax require a stable parser contract.
Sort/project results deliberately and explain why LIMIT without deterministic sorting can duplicate or omit rows across pages.
Diagnose query parsing and cost with FT.EXPLAINCLI and bounded evidence instead of trial-and-error punctuation.
All Chapter 09 mandatory labs reuse the disposable Chapter 01
environment: Redis Open Source 8.10.1 from Docker
Official Image redis:8.10.1, container
atlasmart-redis-ch01, standalone topology, host
publication 127.0.0.1:6379, TLS disabled only
because traffic stays on loopback, default ACL user disabled,
named users atlasmart-app and
academy-admin, logical database 0, AOF with
appendfsync everysec plus RDB snapshots,
persistent /data, and no explicit
maxmemory limit or eviction policy. Redis 8
integrates Search and JSON into Redis Open Source; the
mandatory path needs no separate historical Redis Stack image
or paid service. Source writes use the restricted application
user where possible; Search/index administration and evidence
commands use the disposable academy-admin user
because the Chapter 01 application ACL was intentionally not
broadened for Search administration. Fixtures stay under
atlasmart:ch09:*. Redis 8 documentation still
lists Dialect 1 as the server default while marking Dialects
1, 3, and 4 deprecated. The course therefore requests
DIALECT 2 for modern query syntax and verifies
any newer optimization before production adoption.
1. Re-create the bounded Search fixture
docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app DEL atlasmart:ch09:product:1001 atlasmart:ch09:product:1002 atlasmart:ch09:product:1003 atlasmart:ch09:outside:product:9001docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.SET atlasmart:ch09:product:1001 '$' '{"schemaVersion":2,"sku":"SKU-1001","name":"Trail Running Backpack","description":"Lightweight running backpack for trail and city travel","category":["outdoor","travel"],"brand":"AtlasPeak","priceCents":12990,"stock":12,"location":"49.8671,40.4093"}'docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.SET atlasmart:ch09:product:1002 '$' '{"schemaVersion":2,"sku":"SKU-1002","name":"City Runner Pack","description":"Compact runner backpack for commuting and urban travel","category":["urban","travel"],"brand":"AtlasPeak","priceCents":9990,"stock":0,"location":"49.8920,40.3777"}'docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.SET atlasmart:ch09:product:1003 '$' '{"schemaVersion":2,"sku":"SKU-1003","name":"Trail Bottle","description":"Insulated bottle for hiking and trail running","category":["outdoor","hydration"],"brand":"NorthSpring","priceCents":3490,"stock":40,"location":"49.8500,40.4000"}'docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.SET atlasmart:ch09:outside:product:9001 '$' '{"schemaVersion":2,"sku":"SKU-9001","name":"Outside Prefix Backpack","description":"This key proves prefix scope","category":["outdoor"],"brand":"ScopeTest","priceCents":1,"stock":1,"location":"49.8671,40.4093"}'
docker exec -e REDISCLI_AUTH=AtlasMart-Admin-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user academy-admin FT.CREATE atlasmart-ch09-products-json-idx ON JSON PREFIX 1 atlasmart:ch09:product: SCHEMA '$.sku' AS sku TAG SORTABLE '$.name' AS name TEXT WEIGHT 2.0 '$.description' AS description TEXT '$.category[*]' AS category TAG SORTABLE '$.brand' AS brand TAG SORTABLE '$.priceCents' AS priceCents NUMERIC SORTABLE '$.stock' AS stock NUMERIC SORTABLE '$.location' AS location GEO '$.schemaVersion' AS schemaVersion NUMERIC
Wait until FT.INFO reports the expected documents
before comparing query result sets. Query syntax testing against
a half-built index confuses parser behavior with indexing
readiness.
2. Space means AND; pipe means OR; parentheses protect intent
In the Search query language, multiple terms/clauses generally
intersect, while | expresses union. Negation uses a
leading minus. Parentheses should be used whenever human readers
could disagree about precedence—especially as TAG, TEXT, and
numeric clauses mix.
docker exec -e REDISCLI_AUTH=AtlasMart-Admin-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user academy-admin FT.SEARCH atlasmart-ch09-products-json-idx '@category:{outdoor} @stock>0' RETURN 3 sku name stock DIALECT 2docker exec -e REDISCLI_AUTH=AtlasMart-Admin-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user academy-admin FT.SEARCH atlasmart-ch09-products-json-idx '(@category:{outdoor}|@category:{urban}) @priceCents<13000' RETURN 3 sku name priceCents DIALECT 2docker exec -e REDISCLI_AUTH=AtlasMart-Admin-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user academy-admin FT.SEARCH atlasmart-ch09-products-json-idx '@category:{travel} -@stock:[0 0]' RETURN 3 sku name stock DIALECT 2
Do not build authorization by string-concatenating a tenant clause into user-provided search text. Application authorization must be enforced independently and query values should be parameterized/escaped according to the field grammar.
3. Phrase search is not the same as ANDing terms
runner backpack means both analyzed terms are
required; "runner backpack" asks for the phrase
relationship. SLOP and INORDER can
relax/control distance and order when the use case genuinely
needs them. Phrase support depends on index offsets; schema
options that remove offsets can disable that capability.
docker exec -e REDISCLI_AUTH=AtlasMart-Admin-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user academy-admin FT.SEARCH atlasmart-ch09-products-json-idx '@description:(runner backpack)' RETURN 2 sku description DIALECT 2docker exec -e REDISCLI_AUTH=AtlasMart-Admin-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user academy-admin FT.SEARCH atlasmart-ch09-products-json-idx '@description:"runner backpack"' RETURN 2 sku description DIALECT 2docker exec -e REDISCLI_AUTH=AtlasMart-Admin-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user academy-admin FT.EXPLAINCLI atlasmart-ch09-products-json-idx '@description:"runner backpack"' DIALECT 2
4. Prefix and fuzzy terms expand the candidate vocabulary
A prefix such as Trail* matches indexed terms
beginning with the prefix. Fuzzy terms use percent markers to
permit edit distance. Both improve recall when spelling or
morphology is uncertain, but both can expand work. Evaluate them
with representative vocabulary size and tail latency.
docker exec -e REDISCLI_AUTH=AtlasMart-Admin-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user academy-admin FT.SEARCH atlasmart-ch09-products-json-idx '@name:(Trail*)' RETURN 2 sku name DIALECT 2docker exec -e REDISCLI_AUTH=AtlasMart-Admin-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user academy-admin FT.SEARCH atlasmart-ch09-products-json-idx '@name:(%Backpak%)' RETURN 2 sku name DIALECT 2docker exec -e REDISCLI_AUTH=AtlasMart-Admin-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user academy-admin FT.PROFILE atlasmart-ch09-products-json-idx SEARCH LIMITED QUERY '@name:(%Backpak%)' NOCONTENT DIALECT 2
The profile shape is version-sensitive. Use it to identify iterators and time spent; do not parse human-readable profiler text as a stable application API.
5. Scope terms to fields instead of searching “everything” by accident
Unscoped text can match any indexed TEXT attribute. Field
selectors such as @name: and
@description: constrain intent. This can improve
both relevance and cost by reducing unnecessary candidate
matches. Exact TAG/NUMERIC/GEO predicates are already
field-scoped by their grammar.
docker exec -e REDISCLI_AUTH=AtlasMart-Admin-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user academy-admin FT.SEARCH atlasmart-ch09-products-json-idx 'running' RETURN 2 sku name DIALECT 2docker exec -e REDISCLI_AUTH=AtlasMart-Admin-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user academy-admin FT.SEARCH atlasmart-ch09-products-json-idx '@name:(running)' RETURN 2 sku name DIALECT 2docker exec -e REDISCLI_AUTH=AtlasMart-Admin-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user academy-admin FT.SEARCH atlasmart-ch09-products-json-idx '@description:(running)' RETURN 2 sku description DIALECT 2
6. Query dialect is part of the client/server contract
Redis Search dialects exist so syntax can evolve without silently breaking old applications. Redis 8 documentation currently deprecates Dialects 1, 3, and 4 while still listing Dialect 1 as the server default. Maintained clients may explicitly select another dialect—for example redis-py 6.x chooses Dialect 2 for Search methods. Therefore record both server and client behavior and request a dialect explicitly when query semantics depend on it.
All modern Chapter 09 examples request
DIALECT 2 unless a lesson explicitly demonstrates
a dialect-dependent return shape. Do not change the
server-wide default merely to make one lab work.
7. PARAMS separates values from query grammar
PARAMS substitutes values where concrete values are
allowed and requires Dialect 2 or newer. It is useful for
numeric/geo/vector values and reduces ad-hoc query-string
construction. It does not let parameters replace field names or
arbitrary grammar, so application code still needs a safe query
builder for TAG/TEXT syntax.
docker exec -e REDISCLI_AUTH=AtlasMart-Admin-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user academy-admin FT.SEARCH atlasmart-ch09-products-json-idx '@location:[$lon $lat $radius km] @priceCents:[$min $max]' PARAMS 10 lon 49.8671 lat 40.4093 radius 5 min 3000 max 15000 RETURN 3 sku name priceCents DIALECT 2
The PARAMS 10 count is the number of name/value
tokens that follow: five names and five values. Count mistakes
are protocol errors, which is why client-library APIs are
preferable in application code.
8. RETURN is projection; NOCONTENT is ids only
Search can return full source content, selected fields, or just document IDs. Returning less data reduces network/serialization work, but it does not change the set of matching documents. Design the projection from the API contract rather than returning entire JSON documents by default.
docker exec -e REDISCLI_AUTH=AtlasMart-Admin-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user academy-admin FT.SEARCH atlasmart-ch09-products-json-idx '@category:{outdoor}' NOCONTENT DIALECT 2docker exec -e REDISCLI_AUTH=AtlasMart-Admin-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user academy-admin FT.SEARCH atlasmart-ch09-products-json-idx '@category:{outdoor}' RETURN 3 sku name priceCents DIALECT 2docker exec -e REDISCLI_AUTH=AtlasMart-Admin-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user academy-admin FT.SEARCH atlasmart-ch09-products-json-idx '@category:{outdoor}' RETURN 6 '$.sku' AS sourceSku '$.name' AS sourceName DIALECT 2
9. SORTBY needs a sortable contract and costs memory
Attributes used for low-latency sorting should be declared
SORTABLE in the schema. This keeps sortable values
in index structures and therefore consumes additional memory.
Relevance order and explicit field order are different
contracts; APIs should state which one callers receive.
docker exec -e REDISCLI_AUTH=AtlasMart-Admin-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user academy-admin FT.SEARCH atlasmart-ch09-products-json-idx '*' RETURN 3 sku name priceCents SORTBY priceCents ASC LIMIT 0 3 DIALECT 2docker exec -e REDISCLI_AUTH=AtlasMart-Admin-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user academy-admin FT.SEARCH atlasmart-ch09-products-json-idx '*' RETURN 2 sku name SORTBY sku ASC LIMIT 0 3 DIALECT 2
sku is unique in the fixture and therefore useful
for stable demonstration ordering. In real systems, choose a
sorting/paging contract whose uniqueness and mutation semantics
are explicit.
10. Wrong approach: paginate with LIMIT and no deterministic ordering
Redis documents that LIMIT without sorting is
non-deterministic: repeated pages can duplicate or miss values.
Deep offsets also require increasingly more work in common
plans. The repair is not “retry until pages look right.” Add a
deterministic sort where suitable, or use an aggregation cursor
for large streaming result sets.
docker exec -e REDISCLI_AUTH=AtlasMart-Admin-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user academy-admin FT.SEARCH atlasmart-ch09-products-json-idx '*' NOCONTENT LIMIT 0 2 DIALECT 2docker exec -e REDISCLI_AUTH=AtlasMart-Admin-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user academy-admin FT.SEARCH atlasmart-ch09-products-json-idx '*' NOCONTENT LIMIT 2 2 DIALECT 2docker exec -e REDISCLI_AUTH=AtlasMart-Admin-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user academy-admin FT.SEARCH atlasmart-ch09-products-json-idx '*' RETURN 1 sku SORTBY sku ASC LIMIT 0 2 DIALECT 2docker exec -e REDISCLI_AUTH=AtlasMart-Admin-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user academy-admin FT.SEARCH atlasmart-ch09-products-json-idx '*' RETURN 1 sku SORTBY sku ASC LIMIT 2 2 DIALECT 2
Because the fixture is tiny, you may not observe duplicates in the first two commands. The failure is the missing guarantee itself; production behavior under concurrent updates and larger result sets is what makes it unsafe.
11. Escaping belongs to the field grammar, not one universal backslash rule
TEXT, TAG, wildcard, phrase, numeric, and GEO clauses have different special characters and dialect rules. Dialect 2 allows unescaped spaces in TAG queries, but punctuation such as braces, pipes, and other metacharacters can still need escaping. Use the current query-syntax documentation or a maintained client query builder; never run raw end-user strings as a Search grammar fragment.
Search query escaping prevents parser mistakes/injection into the Search grammar. It does not implement authentication, tenant authorization, or permission to view a matched product.
12. Explain before tuning
FT.EXPLAIN/FT.EXPLAINCLI reveal how
the parser interpreted a query. They are especially useful when
boolean precedence, stemming, field scoping, or escaping
produces surprising matches. First make the logical plan match
the business question; only then use FT.PROFILE to
investigate runtime cost.
docker exec -e REDISCLI_AUTH=AtlasMart-Admin-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user academy-admin FT.EXPLAINCLI atlasmart-ch09-products-json-idx '(@category:{outdoor} @priceCents<15000) @description:(running)' DIALECT 2
13. Reproducible lesson lab and verification
Run the source/index setup, then capture result IDs for boolean, phrase, fuzzy, numeric/GEO, and sorted-page queries. Verify both parser intent and output order.
| Check | Pass condition |
|---|---|
| Boolean query | Parenthesized AND/OR result matches the three known fixtures. |
| Phrase vs AND | You can explain why their result sets may differ. |
| Prefix/fuzzy | Expanded retrieval is deliberate and profileable. |
| Pagination | Sorted SKU pages are deterministic for this static fixture. |
| Dialect | Every modern query explicitly records DIALECT 2. |
docker exec -e REDISCLI_AUTH=AtlasMart-Admin-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user academy-admin FT.DROPINDEX atlasmart-ch09-products-json-idxdocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app DEL atlasmart:ch09:product:1001 atlasmart:ch09:product:1002 atlasmart:ch09:product:1003 atlasmart:ch09:outside:product:9001
14. Production judgment
Keep query grammar small, explicit, and tested. Boolean/fuzzy/prefix complexity can increase candidate work and p99 latency; full content can inflate network replies; sortable fields trade memory for sort efficiency; unsorted pagination is non-deterministic; deep offsets are a poor default for large result sets; dialect/client defaults can change semantics; and source updates during a query can affect loaded values. Use timeout/retry policies appropriate to read queries, but do not hide malformed queries behind retries. Treat authorization, index readiness, and source-of-truth reconciliation separately.
15. Summary and next step
FT.SEARCH is a query language, not a string contains API. You now have explicit boolean, phrase, prefix/fuzzy, parameter, sorting, projection, paging, and dialect contracts. Lesson 4 moves from selecting documents to transforming result sets with FT.AGGREGATE pipelines, grouping, reducers, filters, APPLY, and result shaping.
Check your understanding
- What does a space between clauses normally mean?
- Why request DIALECT 2 explicitly?
- Is LIMIT without SORTBY deterministic paging?
- What is RETURN for?
- Does query escaping provide tenant authorization?
Review the answers
Intersection/AND; use parentheses when combining it with OR or negation.
So copied query semantics do not depend on a mutable server-wide or client default.
No. Redis documents that it can produce duplicate or missing values across pages.
Projection: limit which source/index attributes are returned without changing which documents match.
No. It only protects query grammar construction; authorization is a separate application/security control.
Authoritative references
These references were re-checked for the Redis 8.10.1 course snapshot. Command output and planner details can change between Redis releases, clients, RESP modes, and topologies; prefer the target-version reference when reproducing the lab.