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.

Intermediate160–190 minutesAnalyzer and field-type labRedis Open Source 8.10.1Free/local-firstLast reviewed: September 6, 2026

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.

01

Explain TEXT tokenization, normalization, stemming, language, and stop-word behavior without treating it as exact equality.

02

Use TAG fields for exact categorical matching and distinguish JSON arrays from delimited Hash strings.

03

Apply NUMERIC ranges and modern Dialect 2 comparison syntax to correctly typed values.

04

Index/query GEO points with longitude-first coordinates and explicit units.

05

Measure result quality and latency from deterministic fixtures instead of assuming analyzers or randomness behave as desired.

Exact lab baseline

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.

redis-cli · source documents
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"}'
redis-cli · Search schema
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.

redis-cli · full-text retrieval and parser evidence
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.

redis-cli · compare ordinary and VERBATIM query behavior
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.

redis-cli · inspect a query containing a common word
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.”

redis-cli · exact JSON-array tags
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.

redis-cli · compare field semantics
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.

redis-cli · numeric range and Dialect 2 comparisons
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.

redis-cli · Baku-area radius fixtures
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.

redis-cli · three recall mechanisms
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.

Quality evidence

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.

Python · local percentile harness
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.
redis-cli · bounded cleanup
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

  1. Why is category usually TAG instead of TEXT?
  2. What does VERBATIM change?
  3. What coordinate order does GEO use?
  4. Why store priceCents as integer source data?
  5. 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.

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.