Every search index requires a schema that defines the structure of searchable documents. The schema allows for type-safety and allows us to optimize your data for very fast queries.
We provide a schema builder utility called s that makes it easy to define a schema.
import { Redis, s } from "@upstash/redis"
Field Type Reference
| SDK Method | Redis CLI Type | Description | Supports FAST | Range Operators | Text Operators |
|---|
s.string() | TEXT | Full-text searchable field with tokenization and stemming | No | No | $smart, $phrase, $fuzzy, $regex |
s.keyword() | KEYWORD | Exact-match string (no tokenization) | Yes | Yes ($gt, $gte, $lt, $lte) | No |
s.number() | F64 (default), U64, I64 | Numeric field | Yes | Yes | No |
s.date() | DATE | Date/time field | Yes | Yes | No |
s.boolean() | BOOL | Boolean field | Yes | No | No |
s.facet() | FACET | Hierarchical path-based field | No | No | No (only $eq, $in) |
In the TypeScript SDK, s.number() defaults to F64. You can specify s.number("U64") or
s.number("I64") for unsigned or signed 64-bit integers. F64 fields are FAST by default.
FAST Fields
The FAST flag creates a columnar store for a field, enabling:
- Sorting with
ORDERBY in queries
- Score functions with
SCOREFUNC FIELDVALUE
- Metric aggregations (
$avg, $sum, $min, $max, $count, etc.)
In the TypeScript SDK, numeric (F64), boolean, and date fields are FAST by default. You can
disable it with .fast(false). In Redis CLI, you must explicitly add the FAST keyword after the
field type.
# Redis CLI — FAST must be explicit
SEARCH.CREATE products ON JSON PREFIX 1 product: SCHEMA name TEXT price F64 FAST rating F64 FAST
If you attempt to use ORDERBY, SCOREFUNC, or metric aggregations on a non-FAST field, you will get an error.
Basic Usage
The schema builder provides methods for each field type:
const schema = s.object({
name: s.string(),
age: s.number(),
createdAt: s.date(),
active: s.boolean(),
tag: s.keyword(),
category: s.facet(),
})
The schema builder also supports chaining field options. We’ll see what noTokenize() and noStem() are used for in the section below.
const schema = s.object({
sku: s.string().noTokenize(),
brand: s.string().noStem(),
price: s.number(),
})
Nested Objects
The schema builder supports nested object structures:
const schema = s.object({
title: s.string(),
author: s.object({
name: s.string(),
email: s.string(),
}),
stats: s.object({
views: s.number(),
likes: s.number(),
}),
})
Where to use the Schema
We need the schema when creating or querying an index:
import { Redis, s } from "@upstash/redis"
const redis = Redis.fromEnv()
const schema = s.object({
name: s.string(),
description: s.string(),
category: s.string().noTokenize(),
price: s.number("F64"),
inStock: s.boolean(),
})
const products = await redis.search.createIndex({
name: "products",
dataType: "json",
prefix: "product:",
schema,
})
Tokenization & Stemming
When you store text in a search index, it goes through two transformations: Tokenization and Stemming. By default, text fields are both tokenized and stemmed. Understanding these helps you configure fields correctly.
Tokenization
Tokenization splits text into individual searchable words (tokens) by breaking on spaces and punctuation.
| Original Text | Tokens |
|---|
"hello world" | ["hello", "world"] |
"user@example.com" | ["user", "example", "com"] |
"SKU-12345-BLK" | ["SKU", "12345", "BLK"] |
This is great for natural language because searching for “world” will match “hello world”. But it breaks values that should stay together.
When to disable tokenization with .noTokenize():
- Email addresses (
user@example.com)
- URLs (
https://example.com/page)
- Product codes and SKUs (
SKU-12345-BLK)
- UUIDs (
550e8400-e29b-41d4-a716-446655440000)
- Category slugs (
electronics/phones/android)
const schema = s.object({
title: s.string(),
email: s.string().noTokenize(),
sku: s.string().noTokenize(),
})
Stemming
Stemming reduces words to their root form so different variations match the same search.
| Word | Stemmed Form |
|---|
"running", "runs", "runner" | "run" |
"studies", "studying", "studied" | "studi" |
"experiments", "experimenting" | "experi" |
This way, a user searching for “running shoes” will also find “run shoes” and “runner shoes”.
When to disable stemming with .noStem():
- Brand names (
Nike shouldn’t match Nik)
- Proper nouns and names (
Johnson shouldn’t become John)
- Technical terms (
React shouldn’t match Reac)
- When using regex patterns (stemmed text won’t match your expected patterns)
const schema = s.object({
description: s.string(),
brand: s.string().noStem(),
authorName: s.string().noStem(),
})
Keyword Fields
The KEYWORD field type is for exact-match strings. Unlike TEXT fields, keywords are not tokenized or stemmed — the entire value is treated as a single token.
KEYWORD fields support numeric query operators ($eq, $in, $gt, $gte, $lt, $lte), which are not available on TEXT fields.
TypeScript
Python
Redis CLI
const schema = s.object({
tag: s.keyword(),
})
schema = {
"tag": "KEYWORD",
}
SEARCH.CREATE idx ON JSON PREFIX 1 prefix: SCHEMA tag KEYWORD
When to use KEYWORD instead of TEXT:
- When you need range operators (
$gt, $gte, $lt, $lte) on string values
- When the entire string should be treated as a single unit (no word splitting)
- For tags, labels, status codes, or any string that should match exactly
Facet Fields
The FACET field type is for hierarchical path-based faceted search. Values must be /-delimited paths starting with /.
FACET fields only support $eq and $in operators.
They are primarily used with the $facet aggregation
to build category trees and faceted navigation.
TypeScript
Python
Redis CLI
const schema = s.object({
category: s.facet(),
})
schema = {
"category": "FACET",
}
SEARCH.CREATE idx ON JSON PREFIX 1 prefix: SCHEMA category FACET
Example — querying facet fields:
TypeScript
Python
Redis CLI
await index.query({
filter: { category: { $eq: "/category/books/fiction" } },
});
await index.query({
filter: {
category: { $in: ["/category/books", "/category/electronics"] },
},
});
index.query(filter={"category": {"$eq": "/category/books/fiction"}})
index.query(filter={"category": {"$in": ["/category/books", "/category/electronics"]}})
SEARCH.QUERY products '{"category": {"$eq": "/category/books/fiction"}}'
SEARCH.QUERY products '{"category": {"$in": ["/category/books", "/category/electronics"]}}'
Aliased Fields
Aliased fields allow you to index the same document field multiple times with different settings,
or to create shorter names for complex nested paths.
Use the FROM keyword to specify which document field the alias points to.
TypeScript
Python
Redis CLI
import { Redis, s } from "@upstash/redis";
const redis = Redis.fromEnv();
const products = await redis.search.createIndex({
name: "products",
dataType: "json",
prefix: "product:",
schema: s.object({
description: s.string(),
descriptionExact: s.string().noStem().from("description"),
authorName: s.string().from("metadata.author.displayName"),
}),
});
from upstash_redis import Redis
redis = Redis.from_env()
products = redis.search.create_index(
name="products",
data_type="json",
prefix="product:",
schema={
"description": "TEXT",
"descriptionExact": {"type": "TEXT", "nostem": True, "from": "description"},
"authorName": {"type": "TEXT", "from": "metadata.author.displayName"},
},
)
# Index 'description' twice with different settings
# Create a short alias for a deeply nested field
SEARCH.CREATE products ON JSON PREFIX 1 product: SCHEMA description TEXT descriptionExact TEXT FROM description NOSTEM authorName TEXT FROM metadata.author.displayName
Common use cases for aliased fields:
- Same field with different settings: Index a text field both with and without stemming. Use the stemmed version for general searches and the non-stemmed version for exact matching or regex queries.
- Shorter query paths: Create concise aliases for deeply nested fields like
metadata.author.displayName to simplify queries.
When using aliased fields:
- Use the alias name in queries and highlighting (e.g.,
descriptionExact, authorName)
- Use the actual field name when selecting fields to return (e.g.,
description, metadata.author.displayName)
This is because aliasing happens at the index level and does not modify the underlying documents.
Non-Indexed Fields
Documents don’t need to match the schema exactly:
- Extra fields: Fields in your document that aren’t defined in the schema are simply ignored. They won’t be indexed or searchable.
- Missing fields: If a document is missing a field defined in the schema, that document won’t appear in search results that filter on the missing field.
Schema Examples
E-commerce product schema
TypeScript
Python
Redis CLI
import { Redis, s } from "@upstash/redis"
const redis = Redis.fromEnv()
const products = await redis.search.createIndex({
name: "products",
dataType: "hash",
prefix: "product:",
schema: s.object({
name: s.string(),
sku: s.string().noTokenize(), // Exact-match SKU codes
brand: s.string().noStem(), // Brand names without stemming
description: s.string(),
price: s.number("F64"), // Sortable (F64) price
rating: s.number("F64"), // Sortable (F64) rating
reviewCount: s.number("U64"), // Non-sortable (U64) review count
inStock: s.boolean(),
}),
})
from upstash_redis import Redis
redis = Redis.from_env()
products = redis.search.create_index(
name="products",
data_type="hash",
prefix="product:",
schema={
"name": "TEXT",
"sku": {"type": "TEXT", "notokenize": True},
"brand": {"type": "TEXT", "nostem": True},
"description": "TEXT",
"price": "F64",
"rating": "F64",
"reviewCount": "U64",
"inStock": "BOOL",
},
)
SEARCH.CREATE products ON HASH PREFIX 1 product: SCHEMA name TEXT sku TEXT NOTOKENIZE brand TEXT NOSTEM description TEXT price F64 FAST rating F64 FAST reviewCount U64 inStock BOOL FAST
User directory schema
TypeScript
Python
Redis CLI
import { Redis, s } from "@upstash/redis";
const redis = Redis.fromEnv();
const users = await redis.search.createIndex({
name: "users",
dataType: "json",
prefix: "user:",
schema: s.object({
username: s.string().noTokenize(),
profile: s.object({
displayName: s.string().noStem(),
bio: s.string(),
email: s.string().noTokenize(),
}),
createdAt: s.date().fast(),
verified: s.boolean(),
}),
});
from upstash_redis import Redis
redis = Redis.from_env()
users = redis.search.create_index(
name="users",
data_type="json",
prefix="user:",
schema={
"username": {"type": "TEXT", "notokenize": True},
"profile.displayName": {"type": "TEXT", "nostem": True},
"profile.bio": "TEXT",
"profile.email": {"type": "TEXT", "notokenize": True},
"createdAt": {"type": "DATE", "fast": True},
"verified": "BOOL",
},
)
SEARCH.CREATE users ON JSON PREFIX 1 users: SCHEMA username TEXT NOTOKENIZE profile.displayName TEXT NOSTEM profile.bio TEXT contact.email TEXT NOTOKENIZE createdAt DATE FAST verified BOOL