# Schemas

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.

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

### Field Type Reference

| SDK Method    | Redis CLI Type                | Description                                               | Supports FAST | Multiple values | Range Operators                    | Text Operators                          |
| ------------- | ----------------------------- | --------------------------------------------------------- | ------------- | --------------- | ---------------------------------- | --------------------------------------- |
| `s.string()`  | `TEXT`                        | Full-text searchable field with tokenization and stemming | No            | Yes             | No                                 | `$smart`, `$phrase`, `$fuzzy`, `$regex` |
| `s.keyword()` | `KEYWORD`                     | Exact-match string (no tokenization)                      | Yes           | Yes             | Yes (`$gt`, `$gte`, `$lt`, `$lte`) | No                                      |
| `s.number()`  | `F64` (default), `U64`, `I64` | Numeric field                                             | Yes           | Yes             | Yes                                | No                                      |
| `s.date()`    | `DATE`                        | Date/time field                                           | Yes           | Yes             | Yes                                | No                                      |
| `s.boolean()` | `BOOL`                        | Boolean field                                             | Yes           | No              | No                                 | No                                      |
| `s.facet()`   | `FACET`                       | Hierarchical path-based field                             | No            | No              | No                                 | No (only `$eq`, `$in`)                  |

<Note>
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.
</Note>

### 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.

```bash
# 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:

```ts
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.

```ts {2,3}
const schema = s.object({
  sku: s.string().noTokenize(),
  brand: s.string().noStem(),
  price: s.number(),
})
```

### Nested Objects

The schema builder supports nested object structures:

```ts
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(),
  }),
})
```

## Multi-value fields

A document field can contain several values without a separate array type in the schema. Declare the
field as `TEXT`, `KEYWORD`, `U64`, `I64`, `F64`, or `DATE`, then store its value as a JSON array.
Search indexes every valid array element under the same field.

For example, this schema accepts several tags, descriptions, prices, and restock dates per product:

```ts
const schema = s.object({
  tags: s.keyword(),
  descriptions: s.string(),
  prices: s.number("F64"),
  restockedAt: s.date(),
});
```

The way you store the array depends on the index data type:

| Index data type | How to store multiple values                              | Example                                            |
| --------------- | --------------------------------------------------------- | -------------------------------------------------- |
| `JSON`          | A JSON array in the document                              | `JSON.SET product:1 $ '{"tags":["new","sale"]}'`   |
| `STRING`        | A JSON array inside the JSON object stored in the string  | `SET product:1 '{"tags":["new","sale"]}'`          |
| `HASH`          | A complete, JSON-encoded array in the hash field string   | `HSET product:1 tags '["new","sale"]'`             |
| `STREAM`        | A complete, JSON-encoded array in the stream field string | `XADD events * services '["checkout","payments"]'` |

There is no separator option, a value such as `new,sale` remains one value. Use a valid JSON array `["new","sale"]` to index two values.

Each array element must match the schema type. Search ignores an invalid element while still indexing
the valid elements in the same array. An empty array, or an array with no valid elements, acts like a
missing field. `BOOL` and `FACET` fields do not support array values.

Queries match a document when any indexed element satisfies the condition. For example, both of these
queries match a product whose `tags` value is `["new","sale"]` and whose `prices` value is
`[19.99,24.99]`:

```bash
SEARCH.QUERY products '{"tags":{"$eq":"sale"}}'
SEARCH.QUERY products '{"prices":{"$lt":20}}'
```

Each `TEXT` array element is analyzed separately. A phrase can match within one element, but cannot
start in one element and continue in another. See [Multi-value queries](/redis/search/querying#multi-value-fields)
for more examples and highlighting behavior.

### Where to use the Schema

We need the schema when creating or querying an index:

For the complete index creation syntax, see [`SEARCH.CREATE`](/redis/commands/search/search-create).

```ts
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`)

```ts
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)

```ts
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.

<Tabs>

<Tab title="TypeScript">
```ts
const schema = s.object({
  tag: s.keyword(),
})
```
</Tab>

<Tab title="Python">
```python
schema = {
    "tag": "KEYWORD",
}
```
</Tab>

<Tab title="Redis CLI">
```bash
SEARCH.CREATE idx ON JSON PREFIX 1 prefix: SCHEMA tag KEYWORD
```
</Tab>

</Tabs>

**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](/redis/search/aggregation-operators/bucket-aggregations/facet)
to build category trees and faceted navigation.

<Tabs>

<Tab title="TypeScript">
```ts
const schema = s.object({
  category: s.facet(),
})
```
</Tab>

<Tab title="Python">
```python
schema = {
    "category": "FACET",
}
```
</Tab>

<Tab title="Redis CLI">
```bash
SEARCH.CREATE idx ON JSON PREFIX 1 prefix: SCHEMA category FACET
```
</Tab>

</Tabs>

**Example — querying facet fields:**

<Tabs>

<Tab title="TypeScript">
```ts
await index.query({
  filter: { category: { $eq: "/category/books/fiction" } },
});

await index.query({
  filter: {
    category: { $in: ["/category/books", "/category/electronics"] },
  },
});
```
</Tab>

<Tab title="Python">
```python
index.query(filter={"category": {"$eq": "/category/books/fiction"}})

index.query(filter={"category": {"$in": ["/category/books", "/category/electronics"]}})
```
</Tab>

<Tab title="Redis CLI">
```bash
SEARCH.QUERY products '{"category": {"$eq": "/category/books/fiction"}}'

SEARCH.QUERY products '{"category": {"$in": ["/category/books", "/category/electronics"]}}'
```
</Tab>

</Tabs>

---

## 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.

<Tabs>

<Tab title="TypeScript">
```ts
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"),
  }),
});
```

</Tab>

<Tab title="Python">
```python
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"},
    },
)
```

</Tab>

<Tab title="Redis CLI">
```bash
# 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
```

</Tab>

</Tabs>

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.

<Note>
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.

</Note>

---

## 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**

<Tabs>

<Tab title="TypeScript">

```ts
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(),
  }),
})
```

</Tab>

<Tab title="Python">
```python
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",
    },
)
```

</Tab>

<Tab title="Redis CLI">
```bash
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
```
</Tab>

</Tabs>

**User directory schema**

<Tabs>

<Tab title="TypeScript">
```ts
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(),
  }),
});
```

</Tab>

<Tab title="Python">
```python
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",
    },
)
```

</Tab>

<Tab title="Redis CLI">
```bash
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
```
</Tab>

</Tabs>
