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

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

### Where to use the Schema

We need the schema when creating or querying an index:

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