Chapter 09 · Redis Search: Indexes, Text/Numeric/Tag/Geo Fields, Querying, and Aggregation
Create Search Indexes over Hash/JSON Data and Understand Schema/Prefix Scope
Create bounded Redis Search indexes over JSON and Hash data, prove prefix/filter scope, and separate source keys from secondary index state.
Learning outcomes
AtlasMart can already fetch a product when its Redis key is known, but catalog pages need queries such as “outdoor products under a price ceiling near this store.” Redis Search solves that access-path problem by maintaining a secondary index over selected HASH or JSON fields. The index is derived state: source keys remain the application records, while the Search schema defines which attributes become searchable, filterable, sortable, or geospatial.
Distinguish direct Redis key access from a Search secondary index and explain why both can coexist.
Create bounded Search indexes over JSON and Hash sources with explicit PREFIX and FILTER scope.
Map source fields or JSONPath expressions to TEXT, TAG, NUMERIC, and GEO index attributes.
Use FT.INFO and deliberate inside/outside fixtures to prove what is and is not indexed.
Reason about index memory, write amplification, ACL boundaries, initial scans, and schema evolution before production use.
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:*. Search commands are
version-sensitive and belong to the
@search command category in current Redis. No
lesson changes host firewall rules, managed-service
configuration, or unrelated data.
1. Primary key access and secondary Search access answer different questions
Primary key access means the application
already knows a Redis key such as
atlasmart:ch09:product:1001 and retrieves that
record directly. A secondary index derives
searchable structures from source fields so the application can
locate keys from predicates it did not know in advance. Search
does not replace the source record; it provides another access
path.
| Question | Direct key access | Redis Search |
|---|---|---|
| “Give me product 1001.” | Use the known Redis key; no secondary predicate is needed. | Usually unnecessary overhead. |
| “Find outdoor products under 130 AZN.” | Application would need some other index or a scan. | TAG + NUMERIC predicates express this directly. |
| “Find products whose description contains runner.” | A key lookup cannot discover unknown keys. | TEXT analysis and the inverted index provide discovery. |
| Source of business truth | The HASH/JSON key plus application contract. | Derived index state; verify readiness and indexing errors. |
2. Prove the server feature surface before freezing commands
Redis 8 integrates Search and JSON into Redis Open Source, but a lesson should still prove the target server actually exposes the commands. This catches wrong endpoints, old images, compatible-but-different services, ACL denials, and accidental connections to a Redis instance that does not match the course baseline.
docker exec -e REDISCLI_AUTH=AtlasMart-Admin-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user academy-admin INFO serverdocker exec -e REDISCLI_AUTH=AtlasMart-Admin-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user academy-admin COMMAND INFO FT.CREATEdocker exec -e REDISCLI_AUTH=AtlasMart-Admin-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user academy-admin COMMAND INFO FT.SEARCHdocker exec -e REDISCLI_AUTH=AtlasMart-Admin-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user academy-admin COMMAND INFO JSON.GETdocker exec -e REDISCLI_AUTH=AtlasMart-Admin-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user academy-admin ACL WHOAMI
Expected evidence: redis_version:8.10.1 appears in
the server section, each command lookup returns metadata rather
than nil, and ACL WHOAMI identifies
academy-admin. This proves command availability on
this endpoint; it does not prove that a particular index exists
or is populated.
3. Build deterministic JSON source fixtures and a deliberate out-of-scope key
The prefix is part of the data contract. The first three keys
share atlasmart:ch09:product:; the fourth
deliberately does not. All values are bounded synthetic JSON
documents. Monetary values stay as integer cents so this Search
lesson does not quietly reintroduce floating-point money.
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"}'
Verify source truth directly before creating any index.
JSON.GET against all four keys should succeed. At
this point no Search result has been proven.
4. FT.CREATE chooses one source type and an explicit prefix
FT.CREATE selects a source type with
ON HASH or ON JSON. One index does not
silently mix the two storage types. For JSON, the schema
identifiers are JSONPath expressions; AS gives them
concise query attributes. PREFIX limits which Redis
keys are candidates for indexing. Omitting it defaults to all
keys, which is rarely a good production default in a shared
database.
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
The schema makes name and
description full-text fields,
category/brand/sku
exact-match tags, prices and stock numeric fields, and
location a point-valued GEO field.
SORTABLE is added only to attributes we plan to
sort or aggregate efficiently because sortable values consume
additional index memory.
5. FT.INFO proves index scope and readiness better than assumptions
After index creation, inspect the index rather than immediately
assuming all source documents are searchable.
FT.INFO exposes schema/configuration plus metrics
such as num_docs, indexing,
percent_indexed,
hash_indexing_failures, term/record counts, and
index-memory components. Field names can evolve, so inspect the
actual 8.10.1 reply rather than parsing an undocumented array
position.
docker exec -e REDISCLI_AUTH=AtlasMart-Admin-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user academy-admin FT.INFO atlasmart-ch09-products-json-idxdocker 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 0 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 '@sku:{SKU-9001}' NOCONTENT DIALECT 2docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.GET atlasmart:ch09:outside:product:9001 '$.sku'
With the deterministic fixture, the Search count should be three
once indexing is ready, the outside-prefix SKU should return
zero Search results, and direct JSON access still proves the
fourth source key exists. That difference is the evidence for
PREFIX scope—not a missing source record.
6. Wrong approach: create the right schema over the wrong prefix
A syntactically valid index can be operationally useless if its
prefix does not match the application namespace. That failure is
dangerous because FT.CREATE still returns
OK.
docker exec -e REDISCLI_AUTH=AtlasMart-Admin-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user academy-admin FT.CREATE atlasmart-ch09-wrong-prefix-idx ON JSON PREFIX 1 atlasmart:ch09:wrong: SCHEMA '$.name' AS name TEXTdocker exec -e REDISCLI_AUTH=AtlasMart-Admin-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user academy-admin FT.SEARCH atlasmart-ch09-wrong-prefix-idx '*' NOCONTENT LIMIT 0 0 DIALECT 2docker exec -e REDISCLI_AUTH=AtlasMart-Admin-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user academy-admin FT.INFO atlasmart-ch09-wrong-prefix-idxdocker exec -e REDISCLI_AUTH=AtlasMart-Admin-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user academy-admin FT.DROPINDEX atlasmart-ch09-wrong-prefix-idx
The zero-document result is expected. Diagnose by comparing index prefix configuration with real key names, then drop the disposable wrong index. Never “repair” this by broadening to the default all-key scope without first designing namespaces and cardinality.
7. FILTER narrows candidates after source-type/prefix selection
FILTER is an indexing-time predicate using the
Search aggregation expression language. It is distinct from a
query-time filter: documents that do not satisfy the index
FILTER are not represented in that index. The following HASH
fixture makes the distinction concrete and uses a documented
field predicate.
docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app DEL atlasmart:ch09:offer:1 atlasmart:ch09:offer:2docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app HSET atlasmart:ch09:offer:1 name "Trail Pack Offer" status active priceCents 11990docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app HSET atlasmart:ch09:offer:2 name "Draft Pack Offer" status draft priceCents 10990docker exec -e REDISCLI_AUTH=AtlasMart-Admin-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user academy-admin FT.CREATE atlasmart-ch09-active-offers-idx ON HASH PREFIX 1 atlasmart:ch09:offer: FILTER '@status=="active"' SCHEMA name TEXT status TAG priceCents NUMERICdocker exec -e REDISCLI_AUTH=AtlasMart-Admin-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user academy-admin FT.SEARCH atlasmart-ch09-active-offers-idx '*' NOCONTENT LIMIT 0 0 DIALECT 2docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app HGETALL atlasmart:ch09:offer:2
The Search index should contain only the active offer while the draft hash remains directly readable. FILTER logic, source schema, and later mutations therefore belong in migration tests; a field rename can remove documents from index scope without deleting them from Redis.
8. Existing keys, future writes, and initial-scan choices are different phases
Creating an index normally scans matching existing keys and then
maintains the index as matching source documents change.
SKIPINITIALSCAN deliberately skips the first phase;
it is not a performance flag to copy casually because existing
records will not be retroactively included merely because the
index exists. During initial population or bulk changes, observe
indexing, percent_indexed,
num_docs, and failures before shifting user
traffic.
A successful FT.CREATE reply proves that the
index definition was accepted. It does not by itself
prove that all expected existing documents have been indexed,
that zero documents failed schema conversion, or that the
query result set matches the application contract.
9. Field types are query contracts, not display formatting
Declaring a field TEXT, TAG,
NUMERIC, or GEO determines indexing
and query semantics. A product category is not “basically text”
when the requirement is exact membership; a price string is not
a numeric range merely because humans can read digits; and a
longitude/latitude string is not useful until it is indexed as
GEO with the expected coordinate order.
| Type | Index intent | Example question | Common mistake |
|---|---|---|---|
| TEXT | tokenized full-text retrieval | description contains stemmed running terms | expect exact categorical equality |
| TAG | exact categorical filtering | category contains outdoor | expect stemming or relevance ranking |
| NUMERIC | range/filter/sort | priceCents between 5000 and 15000 | store inconsistent string/non-number JSON values |
| GEO | radius around lon/lat | within 5 km of store | reverse latitude/longitude or mix units |
10. Source memory and index memory are both part of capacity
The JSON key still consumes Redis memory after it is indexed. Search then adds document tables, inverted indexes, term dictionaries, offset vectors, sortable values, key tables, and other structures depending on schema/features. Indexing a field also adds write work when that source field changes. Measure the before/after state rather than claiming a universal memory multiplier.
docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app MEMORY USAGE atlasmart:ch09:product:1001docker exec -e REDISCLI_AUTH=AtlasMart-Admin-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user academy-admin INFO memorydocker exec -e REDISCLI_AUTH=AtlasMart-Admin-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user academy-admin FT.INFO atlasmart-ch09-products-json-idx
For this three-document fixture the absolute values are too small for capacity extrapolation. The production exercise is to repeat the measurement with representative document sizes, term distributions, sortable fields, write rates, persistence settings, and concurrent queries.
11. Search ACLs do not replace source-key or application authorization
Search commands have their own ACL category and index names are
not an application authorization boundary. A query that can
discover a document may expose key identifiers or selected
content depending on command permissions and result options.
Production design therefore separates network/TLS, Redis ACL
command/key permissions, index administration, and
application-level tenant/business authorization. The disposable
course uses academy-admin for Search so Chapter 09
does not silently broaden the application user.
12. Reproducible lesson lab
Blast radius: only the four bounded JSON keys,
two bounded Hash keys, and named Chapter 09 indexes above. No
FLUSH*, host changes, or production endpoints. Run
the JSON fixture and index creation blocks, then verify the
following checklist.
| Verification | Expected evidence |
|---|---|
| Source keys exist | Direct JSON.GET/HGETALL succeeds for all intended fixtures. |
| JSON index scope | Three product keys indexed; outside-prefix key remains searchable only by direct key access. |
| Wrong prefix failure | Wrong index accepts definition but has zero docs; FT.INFO shows its prefix. |
| HASH FILTER scope | Active offer is indexed; draft offer remains source-only. |
| No hidden cleanup | Dropping a Search index without DD leaves source keys intact unless DD is explicitly requested. |
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-Admin-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user academy-admin FT.DROPINDEX atlasmart-ch09-active-offers-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 atlasmart:ch09:offer:1 atlasmart:ch09:offer:2
13. Production judgment
Use Redis Search when applications need secondary predicates, full-text retrieval, structured filters, geospatial queries, aggregation, or later vector retrieval over data already modeled appropriately in Redis. Prefer direct key access when the key is already known and no secondary discovery is needed. Every indexed field carries memory and write-amplification cost; every broad prefix increases candidate cardinality; sortable attributes cost additional memory; index builds and migrations need readiness checks; ACLs and tenant authorization remain separate; persistence/replication must account for both source data and index rebuild/recovery behavior; and Cluster/managed-service semantics must be verified against the exact target. Do not treat an index as backup or schema enforcement.
14. Summary and next step
You now have the correct foundation: Search is a derived secondary access path over explicitly scoped HASH or JSON source documents. Prefixes and filters decide which documents participate; schema types decide how attributes can be queried; and FT.INFO proves index state. Lesson 2 moves inside those field types to show why TEXT analysis, TAG equality, numeric ranges, and GEO queries produce intentionally different results.
Check your understanding
- Does FT.CREATE replace the underlying JSON or Hash keys?
- What does PREFIX prove?
- Why can FT.CREATE returning OK still be insufficient readiness evidence?
- Can one Search index silently mix ON HASH and ON JSON?
- Why not index every field?
Review the answers
No. The Search index is derived from source keys; direct key access remains a separate path.
It limits candidate Redis keys by key-name prefix. An out-of-prefix source key can exist while returning no Search result.
Initial indexing, field conversion failures, and document counts still need to be observed with FT.INFO and sample queries.
No. The source type is chosen when the index is created.
Each field increases schema surface and can add memory and update work even if applications never query it.
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.