Skip to main content
Every index requires a schema that defines the structure of searchable documents. The schema enforces type safety and enables query optimization.

Schema Builder Utility

The TypeScript SDK provides a convenient schema builder utility s that makes it easy to define schemas with type safety and better developer experience.

Importing the Schema Builder

import { Redis, s } from "@upstash/redis";

Basic Usage

The schema builder provides methods for each field type:
const schema = s.object({
  // Text fields
  name: s.string(),
  description: s.string(),
  
  // Numeric fields
  age: s.number("U64"),      // Unsigned 64-bit integer
  price: s.number("F64"),    // 64-bit floating point
  count: s.number("I64"),    // Signed 64-bit integer
  
  // Date fields
  createdAt: s.date(),       // RFC 3339 timestamp
  
  // Boolean fields
  active: s.boolean(),
});

Field Options

The schema builder supports chaining field options:
const schema = s.object({
  // Text field without tokenization
  sku: s.string().noTokenize(),
  
  // Text field without stemming
  brand: s.string().noStem(),
  
  // Numeric field with fast storage for sorting
  price: s.number("F64"),
  
  // Combining multiple options is not supported yet
});

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().noTokenize(),
  }),
  stats: s.object({
    views: s.number("U64"),
    likes: s.number("U64"),
  }),
});

Using Schema with Index Creation

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,
});

Schema Builder vs. Plain Objects

You can define schemas using either the schema builder or plain objects: The schema builder provides:
  • Better type safety
  • Autocomplete support
  • More readable and maintainable code
  • Easier refactoring

Field Types

TypeDescriptionExample Values
TEXTFull-text searchable string"hello world", "The quick brown fox"
U64Unsigned 64-bit integer0, 42, 18446744073709551615
I64Signed 64-bit integer-100, 0, 9223372036854775807
F6464-bit floating point3.14, -0.001, 1e10
BOOLBooleantrue, false
DATERFC 3339 timestamp"2024-01-15T09:30:00Z", "1985-04-12T23:20:50.52Z"

Field Options

Options modify field behavior and enable additional features.

Text Field Options

By default, text fields are tokenized and stemmed. Stemming reduces words to their root form, enabling searches for “running” to match “run,” “runs,” and “runner.” This is controlled per-field with NOSTEM and globally with the LANGUAGE option.
LanguageExample Stemming
english”running” → “run”, “studies” → “studi”
turkish”koşuyorum” → “koş”
All languages use the same tokenizer, which splits text into tokens of consecutive alphanumeric characters. This might change in the future when support for Asian languages is added. It is possible to configure this behavior using the following options:
OptionDescriptionUse Case
NOSTEMDisable word stemmingNames, proper nouns, technical terms
NOTOKENIZETreat entire value as single tokenURLs, UUIDs, email addresses, category codes
When using $regex, be aware of stemming behavior:
// With stemming enabled (default), "experiment" is stored as "experi"
// This regex won't match:
await products.query({
  filter: {
    description: {
      $regex: "experiment.*",
    },
  },
});

// This will match:
await products.query({
  filter: {
    description: {
      $regex: "experi.*",
    },
  },
});
To avoid stemming issues, use NOSTEM on fields where you need exact regex matching

Numeric, Boolean, and Date Field Options

OptionDescriptionUse Case
FASTEnable fast field storageSorting, fast range queries, field retrieval

Nested Fields

You can define fields at arbitrary nesting levels using the . character as a separator.

Non-Indexed Fields

Although the schema definition is strict, documents do not have to match with the schema exactly. There might be missing or extra fields in the documents. In that case, extra fields are not part of the index, and missing fields are not indexed for that document at all. So, documents with missing fields won’t be part of the result set, where there are required matches for the missing fields.

Schema Examples

E-commerce product schema
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({
    // Searchable product name with stemming
    name: s.string(),

    // Exact-match SKU codes
    sku: s.string().noTokenize(),

    // Brand names without stemming
    brand: s.string().noStem(),

    // Full-text description
    description: s.string(),

    // Sortable price
    price: s.number("F64"),

    // Sortable rating
    rating: s.number("F64"),

    // Non-sortable review count
    reviewCount: s.number("U64"),

    // Filterable stock status
    inStock: s.boolean(),
  }),
});
User directory schema
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({
    // Exact username matches
    username: s.string().noTokenize(),

    // Nested schema fields
    profile: s.object({
      // Name search without stemming
      displayName: s.string().noStem(),

      // Full-text bio search
      bio: s.string(),

      // Exact email matches
      email: s.string().noTokenize(),
    }),

    // Join date for sorting
    createdAt: s.date().fast(),

    // Filter by verification status
    verified: s.boolean(),
  }),
});