Chapter 09 · Redis Search: Indexes, Text/Numeric/Tag/Geo Fields, Querying, and Aggregation
Text Analysis, Stemming, Stop Words, Exact Tags, Numeric Ranges, and Geospatial Fields
Choose TEXT, TAG, NUMERIC, and GEO semantics deliberately, then verify analyzers, exact matches, ranges, coordinates, relevance, and latency.
Learning outcomes
AtlasMart search quality depends on choosing the correct field semantics before tuning scores or adding more data. A customer typing “running backpack” expects language-aware full-text behavior; a category filter expects exact membership; price filters require numeric comparisons; and store-nearby queries require longitude/latitude plus a distance unit. Redis deliberately indexes those cases differently.
Explain TEXT tokenization, normalization, stemming, language, and stop-word behavior without treating it as exact equality.
Use TAG fields for exact categorical matching and distinguish JSON arrays from delimited Hash strings.
Apply NUMERIC ranges and modern Dialect 2 comparison syntax to correctly typed values.
Index/query GEO points with longitude-first coordinates and explicit units.
Measure result quality and latency from deterministic fixtures instead of assuming analyzers or randomness behave as desired.
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:*. This lesson re-creates its own
bounded product index so it can run independently. It uses
DIALECT 2 explicitly for modern syntax because
Redis 8 keeps dialect behavior version-sensitive.
1. Re-create a field-rich Search fixture
The same AtlasMart documents deliberately contain words with related stems, exact category arrays, integer prices, stock, and GEO points. Re-create the source and index so every field comparison comes from one controlled dataset.
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
Once FT.INFO reports three indexed documents and no
indexing failures, the examples below have a deterministic
starting point.
2. TEXT is analyzed language, not a string-equality operator
A TEXT attribute is lowercased/tokenized and
normally stemmed according to the selected language. The default
language is English unless the index/document/query specifies
another supported language. Search therefore works over indexed
terms and expansions rather than byte-for-byte string equality.
WEIGHT can influence relevance;
NOSTEM disables stemming for a field.
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:(running)' RETURN 3 sku name 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)' RETURN 3 sku name 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:(running)' DIALECT 2
The exact expansion shown by FT.EXPLAINCLI is
implementation/version evidence, not an API contract to
hard-code. The stable lesson is that TEXT is analyzed and that
analyzer choices affect recall and ranking.
3. VERBATIM and NOSTEM solve different problems
VERBATIM is a query option that stops query-time
stemming expansion for that request. NOSTEM is a
schema option that changes how a TEXT field is indexed. They are
not interchangeable knobs. If an identifier, SKU, status, or
category requires equality semantics, a TAG field is usually a
better model than trying to turn TEXT analysis off everywhere.
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:(running)' 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 '@description:(running)' NOCONTENT VERBATIM DIALECT 2
Compare counts on your exact server; do not promise a particular stem expansion from memory. If exact term identity is a business invariant, model it explicitly instead of hoping a full-text analyzer happens to preserve it.
4. Stop words reduce common full-text noise
Search indexes can define stop words. Common words may be
ignored in full-text analysis so they do not dominate term
structures and queries. The exact default list and analyzer
behavior are configuration/version details.
STOPWORDS 0 can disable the stop-word list for an
index, while the old NOSTOPWORDS query option is
deprecated in Redis 8.0. Observe the query plan rather than
teaching “the” as a guaranteed universal example forever.
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 '@description:(the trail)' 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:(the trail)' RETURN 2 sku description DIALECT 2
5. TAG means exact categorical membership
TAG fields are designed for exact-match filters and are
typically more space-efficient than TEXT for short categorical
values. The query uses braces: @category:{outdoor}.
TAG matching is case-insensitive by default unless
CASESENSITIVE is declared. There is no stemming or
relevance interpretation of “outdoor.”
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}' RETURN 3 sku name category 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 '@brand:{AtlasPeak}' RETURN 3 sku name brand 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|hydration}' NOCONTENT DIALECT 2
For JSON, arrays such as ["outdoor","travel"] are
the preferred multi-tag representation. A JSON string containing
commas is one tag unless a separator is explicitly configured;
Hash TAG fields, by contrast, default to comma splitting. That
storage difference is easy to miss during migrations.
6. Wrong approach: expect a TAG field to behave like TEXT
Searching @category:(outdoor) uses TEXT-style
syntax against a field that was indexed as TAG. Depending on
parser context, this is invalid or simply not the intended
predicate. The repair is not “try random punctuation”; use the
schema contract and brace 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 '@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 '@description:(outdoor)' NOCONTENT DIALECT 2
The first asks for exact category membership. The second asks whether the analyzed description contains the token outdoor. They are semantically different even if a particular fixture happens to make their counts equal.
7. NUMERIC fields preserve range/comparison semantics, not money arithmetic
The source stores priceCents as an integer. Search
indexes it as NUMERIC so a query can filter a range. Redis
Search numeric values are for filtering/sorting, not a
replacement for financial ledger arithmetic or application
validation.
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 '@priceCents:[3000 10000]' RETURN 3 sku name priceCents SORTBY priceCents ASC 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 '@stock>0' RETURN 3 sku name stock SORTBY stock DESC 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 '@priceCents<10000' NOCONTENT DIALECT 2
Range endpoints and comparison operators have exact dialect
semantics. The course uses DIALECT 2 explicitly so
copied queries do not silently depend on a server-wide default.
8. GEO fields are longitude, latitude plus a unit-aware radius
A Search GEO point is stored as longitude and latitude. Query
syntax follows the same order:
@location:[lon lat radius unit]. Allowed radius
units include meters, kilometers, miles, and feet. Redis uses a
spherical approximation appropriate for many proximity searches
but not precision geodesy or safety-critical navigation.
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:[49.8671 40.4093 3 km]' RETURN 3 sku name location 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 '@location:[$lon $lat 5 km]' PARAMS 4 lon 49.8671 lat 40.4093 RETURN 2 sku name DIALECT 2
The parameterized form avoids constructing numeric query strings in application code. A reversed latitude/longitude pair can still be syntactically numeric while pointing somewhere completely different, so verify known-coordinate fixtures at boundaries.
9. Full-text phrase, prefix, and fuzzy matching trade precision for recall
TEXT fields support exact phrases, prefix terms, and fuzzy terms in the query language. These features increase candidate expansion and should be evaluated against false positives and latency, not enabled because they look convenient.
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:"trail running"' 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 '@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 2
Fuzzy syntax uses percent markers and increases allowed Levenshtein distance as more pairs are added, up to the documented limit. Prefix/fuzzy expansion can become expensive with broad vocabularies, so profile representative terms.
10. Analyzer quality is a data-and-language decision
Redis supports multiple stemmer languages. AtlasMart should not assume an English index correctly analyzes Turkish, Arabic, or other catalog text. If documents carry different languages, design explicit language metadata and test stemming/tokenization per language. Search analysis is not translation, semantic understanding, or locale-aware business logic.
Build a labeled relevance set: query, expected relevant SKUs, known non-relevant SKUs, and business importance. Measure precision/recall-style outcomes alongside latency. A fast query with poor retrieval quality is not a successful Search design.
11. Measure p50/p95/p99 instead of quoting one latency
The prompt requires measured query latency, but this environment cannot run Docker. The lab therefore supplies a deterministic standard-library Python harness for your machine. It invokes the exact pinned Docker/redis-cli command repeatedly, separates warmup, and reports percentile observations. Keep dataset size, query, server version, persistence, topology, and concurrent load beside the numbers.
import subprocess, time, statisticscmd = ["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","NOCONTENT","DIALECT","2"]for _ in range(5): subprocess.run(cmd, check=True, capture_output=True)samples = []for _ in range(50): t0 = time.perf_counter_ns() subprocess.run(cmd, check=True, capture_output=True) samples.append((time.perf_counter_ns()-t0)/1_000_000)samples.sort()def pct(p): return samples[min(len(samples)-1, max(0, int((p/100)*len(samples))-1))]print({"p50_ms":pct(50),"p95_ms":pct(95),"p99_ms":pct(99),"n":len(samples)})
This measures end-to-end Docker CLI + redis-cli invocation, not pure server execution time. For serious benchmarking use a persistent client and later Chapter 24 methodology; here the point is to record distributions and environment honestly.
12. Reproducible lesson lab and verification
Run the source/index setup, then execute one TEXT, TAG, NUMERIC, and GEO query plus the percentile harness. Record exact counts and any analyzer plan differences on Redis 8.10.1.
| Evidence | What it should prove |
|---|---|
| FT.EXPLAINCLI | How the current parser/analyzer interpreted a TEXT query. |
| TAG result set | Exact membership semantics over JSON arrays. |
| NUMERIC range | Correct typed range/comparison behavior. |
| GEO known-point fixture | Longitude/latitude order and unit assumptions. |
| p50/p95/p99 harness | Observed local distribution, not a universal Redis latency claim. |
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
13. Production judgment
Use TEXT only for human-language retrieval that benefits from tokenization/stemming; use TAG for bounded exact categories/identifiers; use NUMERIC for typed ranges/sorts; use GEO for proximity filters with validated coordinates and units. Analyzer choices alter relevance and index memory, sortable values add memory, broad fuzzy/prefix queries can increase tail latency, malformed source types can create indexing failures, client libraries may choose their own dialect defaults, and mixed-language catalogs need explicit tests. Search still does not provide application authorization or guarantee that source and secondary access paths are interchangeable during migrations.
14. Summary and next step
Field types encode semantics. TEXT analyzes language, TAG matches categories, NUMERIC filters typed values, and GEO filters point distance. Lesson 3 now composes those fields through the full FT.SEARCH query language—boolean logic, phrase/prefix/fuzzy terms, sorting, projection, and deterministic pagination.
Check your understanding
- Why is category usually TAG instead of TEXT?
- What does VERBATIM change?
- What coordinate order does GEO use?
- Why store priceCents as integer source data?
- What should accompany a p95 latency number?
Review the answers
Because categories require exact membership rather than tokenization, stemming, and relevance scoring.
It disables query-time stemming expansion for that query; it does not redesign how the field was indexed.
Longitude first, latitude second, with an explicit radius unit in radius queries.
It keeps the business representation exact; Search NUMERIC is then used for filtering/sorting, not ledger arithmetic.
Dataset/cardinality, query, server/client version, persistence/topology, concurrency, warmup, and measurement method.
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.