diff --git a/docs.json b/docs.json
index ca832723..7bd84527 100644
--- a/docs.json
+++ b/docs.json
@@ -698,6 +698,58 @@
"redis/help/managing-healthcare-data"
]
},
+ {
+ "group": "Search",
+ "pages": [
+ "redis/search/introduction",
+ "redis/search/getting-started",
+ "redis/search/index-management",
+ "redis/search/schema-definition",
+ "redis/search/querying",
+ "redis/search/counting",
+ {
+ "group": "Query Operators",
+ "pages": [
+ "redis/search/query-operators/overview",
+ {
+ "group": "Boolean Operators",
+ "pages": [
+ "redis/search/query-operators/boolean-operators/overview",
+ "redis/search/query-operators/boolean-operators/must",
+ "redis/search/query-operators/boolean-operators/should",
+ "redis/search/query-operators/boolean-operators/must-not",
+ "redis/search/query-operators/boolean-operators/boost"
+ ]
+ },
+ {
+ "group": "Field Operators",
+ "pages": [
+ "redis/search/query-operators/field-operators/overview",
+ "redis/search/query-operators/field-operators/smart-matching",
+ "redis/search/query-operators/field-operators/eq",
+ "redis/search/query-operators/field-operators/ne",
+ "redis/search/query-operators/field-operators/in",
+ "redis/search/query-operators/field-operators/range-operators",
+ "redis/search/query-operators/field-operators/phrase",
+ "redis/search/query-operators/field-operators/fuzzy",
+ "redis/search/query-operators/field-operators/regex",
+ "redis/search/query-operators/field-operators/contains",
+ "redis/search/query-operators/field-operators/boost"
+ ]
+ }
+ ]
+ },
+ {
+ "group": "Recipes",
+ "pages": [
+ "redis/search/recipes/overview",
+ "redis/search/recipes/e-commerce-search",
+ "redis/search/recipes/blog-search",
+ "redis/search/recipes/user-directory"
+ ]
+ }
+ ]
+ },
{
"group": "How To",
"pages": [
diff --git a/redis/search/counting.mdx b/redis/search/counting.mdx
new file mode 100644
index 00000000..03556aeb
--- /dev/null
+++ b/redis/search/counting.mdx
@@ -0,0 +1,41 @@
+---
+title: Counting
+---
+
+The `SEARCH.COUNT` command returns the number of documents matching a query without retrieving them.
+
+You can use `SEARCH.COUNT` for analytics, pagination UI (showing "X results found"),
+or validating queries before retrieving results.
+
+
+
+
+```ts
+// Count all electronics
+await products.count({
+ filter: {
+ category: "electronics",
+ },
+});
+
+// Count in-stock items under $100
+await products.count({
+ filter: {
+ inStock: true,
+ price: { $lt: 100 },
+ },
+});
+```
+
+
+
+```bash
+# Count all electronics
+SEARCH.COUNT products '{"category": "electronics"}'
+
+# Count in-stock items under $100
+SEARCH.COUNT products '{"inStock": true, "price": {"$lt": 100}}'
+```
+
+
+
diff --git a/redis/search/getting-started.mdx b/redis/search/getting-started.mdx
new file mode 100644
index 00000000..b0e2354d
--- /dev/null
+++ b/redis/search/getting-started.mdx
@@ -0,0 +1,106 @@
+---
+title: Getting Started
+---
+
+This section demonstrates a complete workflow: creating an index, adding data, and searching.
+
+
+
+
+```ts
+import { Redis, s } from "@upstash/redis";
+
+const redis = Redis.fromEnv();
+
+// Create an index for product data stored as JSON
+const index = await redis.search.createIndex({
+ name: "products",
+ dataType: "json",
+ prefix: "product:",
+ schema: s.object({
+ name: s.string(),
+ description: s.string(),
+ category: s.string().noTokenize(),
+ price: s.number("F64"),
+ inStock: s.boolean(),
+ }),
+});
+
+// Add some products (standard Redis JSON commands)
+await redis.json.set("product:1", "$", {
+ name: "Wireless Headphones",
+ description:
+ "Premium noise-cancelling wireless headphones with 30-hour battery life",
+ category: "electronics",
+ price: 199.99,
+ inStock: true,
+});
+
+await redis.json.set("product:2", "$", {
+ name: "Running Shoes",
+ description: "Lightweight running shoes with advanced cushioning technology",
+ category: "sports",
+ price: 129.99,
+ inStock: true,
+});
+
+await redis.json.set("product:3", "$", {
+ name: "Coffee Maker",
+ description: "Programmable coffee maker with built-in grinder",
+ category: "kitchen",
+ price: 89.99,
+ inStock: false,
+});
+
+// Wait for indexing to complete (optional, for immediate queries)
+await index.waitIndexing();
+
+// Search for products
+const wirelessProducts = await index.query({
+ filter: { description: "wireless" },
+});
+
+for (const product of wirelessProducts) {
+ console.log(product);
+}
+
+// Search with more filters
+const runningProducts = await index.query({
+ filter: { description: "running", inStock: true },
+});
+
+for (const product of runningProducts) {
+ console.log(product);
+}
+
+// Count matching documents
+const count = await index.count({ filter: { price: { $lt: 150 } } });
+console.log(count);
+```
+
+
+
+```bash
+# Create an index for product data stored as JSON
+SEARCH.CREATE products ON JSON PREFIX 1 product: SCHEMA name TEXT description TEXT category TEXT NOTOKENIZE price F64 FAST inStock BOOL
+
+# Add some products (standard Redis JSON commands)
+JSON.SET product:1 $ '{"name": "Wireless Headphones", "description": "Premium noise-cancelling wireless headphones with 30-hour battery life", "category": "electronics", "price": 199.99, "inStock": true}'
+JSON.SET product:2 $ '{"name": "Running Shoes", "description": "Lightweight running shoes with advanced cushioning technology", "category": "sports", "price": 129.99, "inStock": true}'
+JSON.SET product:3 $ '{"name": "Coffee Maker", "description": "Programmable coffee maker with built-in grinder", "category": "kitchen", "price": 89.99, "inStock": false}'
+
+# Wait for indexing to complete (optional, for immediate queries)
+SEARCH.WAITINDEXING products
+
+# Search for products
+SEARCH.QUERY products '{"description": "wireless"}'
+
+# Search with more filters
+SEARCH.QUERY products '{"description": "running", "inStock": true}'
+
+# Count matching documents
+SEARCH.COUNT products '{"price": {"$lt": 150}}'
+```
+
+
+
diff --git a/redis/search/index-management.mdx b/redis/search/index-management.mdx
new file mode 100644
index 00000000..5dea7886
--- /dev/null
+++ b/redis/search/index-management.mdx
@@ -0,0 +1,376 @@
+---
+title: Index Management
+---
+
+### Creating an Index
+
+The `SEARCH.CREATE` command creates a new search index that automatically tracks keys matching specified prefixes.
+
+An index is identified by its name, which must be a unique key in Redis.
+Each index works only with a specified key type (JSON, hash, or string)
+and tracks changes to keys matching the prefixes defined during index creation.
+
+
+
+
+```ts
+import { Redis, s } from "@upstash/redis";
+
+const redis = Redis.fromEnv();
+
+// Basic index on JSON data
+const users = await redis.search.createIndex({
+ name: "users",
+ dataType: "json",
+ prefix: "user:",
+ schema: s.object({
+ name: s.string(),
+ email: s.string(),
+ age: s.number("U64"),
+ }),
+});
+```
+
+
+
+```bash
+# Basic index on hash data
+SEARCH.CREATE users on JSON PREFIX 1 user: SCHEMA name TEXT email TEXT age u64
+```
+
+
+
+
+For JSON indexes, an index field can be specified for fields on various nested levels.
+
+For hash indexes, an index field can be specified for fields. As hash fields cannot have
+nesting on their own, for this kind of indexes, only top-level schema fields can be used.
+
+For string indexes, indexed keys must be valid JSON strings. A field on any nesting level
+can be indexed, similar to JSON indexes.
+
+
+
+
+```ts
+import { Redis, s } from "@upstash/redis";
+const redis = Redis.fromEnv();
+
+// String index with nested schema fields
+const comments = await redis.search.createIndex({
+ name: "comments",
+ dataType: "string",
+ prefix: "comment:",
+ schema: s.object({
+ user: s.object({
+ name: s.string(),
+ email: s.string().noTokenize(),
+ }),
+ comment: s.string(),
+ upvotes: s.number("U64"),
+ commentedAt: s.date().fast(),
+ }),
+});
+```
+
+
+
+```bash
+# String index with nested schema fields
+SEARCH.CREATE comments ON STRING PREFIX 1 comment: SCHEMA user.name TEXT user.email TEXT NOTOKENIZE comment TEXT upvotes U64 FAST commentedAt DATE FAST
+```
+
+
+
+
+It is possible to define an index for more than one prefix. However, there are some rules concerning the usage of
+multiple prefixes:
+
+- Prefixes must not contain duplicates.
+- No prefix should cover another prefix (e.g., `user:` and `user:admin:` are not allowed together).
+- Multiple distinct prefixes are allowed (e.g., `article:` and `blog:` are valid together).
+
+
+
+
+```ts
+import { Redis, s } from "@upstash/redis";
+const redis = Redis.fromEnv();
+
+// JSON index with multiple prefixes
+const articles = await redis.search.createIndex({
+ name: "articles",
+ dataType: "json",
+ prefix: ["article:", "blog:", "news:"],
+ schema: s.object({
+ title: s.string(),
+ body: s.string(),
+ author: s.string().noStem(),
+ publishedAt: s.date().fast(),
+ viewCount: s.number("U64"),
+ }),
+});
+```
+
+
+
+```bash
+# String index with nested schema fields
+SEARCH.CREATE comments ON STRING PREFIX 1 comment: SCHEMA user.name TEXT user.email TEXT NOTOKENIZE comment TEXT upvotes U64 FAST commentedAt DATE FAST
+```
+
+
+
+
+By default, when an index is created, all existing keys matching the specified type and prefixes are scanned and indexed.
+Use `SKIPINITIALSCAN` to defer indexing, which is useful for large datasets where you want to start fresh or handle
+existing data differently.
+
+
+
+
+```ts
+// Skipping initial scan and indexing of keys with SKIPINITIALSCAN
+// TODO: TS SDK does not support SKIPINITIALSCAN for now
+```
+
+
+
+```bash
+# Skipping initial scan and indexing of keys with SKIPINITIALSCAN
+SEARCH.CREATE profiles ON STRING PREFIX 1 profiles: SKIPINITIALSCAN SCHEMA name TEXT
+```
+
+
+
+
+It is possible to specify the language of the text fields, so that an appropriate tokenizer
+and stemmer can be used. For more on tokenization and stemming, see the
+[Text Field Options](./schema-definition#text-field-options) section.
+
+When not specified, language defaults to `english`.
+
+Currently, the following languages are supported:
+
+- `english`
+- `arabic`
+- `danish`
+- `dutch`
+- `finnish`
+- `french`
+- `german`
+- `greek`
+- `hungarian`
+- `italian`
+- `norwegian`
+- `portuguese`
+- `romanian`
+- `russian`
+- `spanish`
+- `swedish`
+- `tamil`
+- `turkish`
+
+
+
+
+```ts
+import { Redis, s } from "@upstash/redis";
+
+const redis = Redis.fromEnv();
+
+// Turkish language index
+const addresses = await redis.search.createIndex({
+ name: "addresses",
+ dataType: "json",
+ prefix: "address:",
+ language: "turkish",
+ schema: s.object({
+ address: s.string().noStem(),
+ description: s.string(),
+ }),
+});
+```
+
+
+
+```bash
+# Turkish language index
+SEARCH.CREATE addresses ON JSON PREFIX 1 address: LANGUAGE turkish SCHEMA address TEXT NOSTEM description TEXT
+```
+
+
+
+
+Finally, it is possible safely create an index only if it does not exist, using
+the `EXISTOK` option.
+
+
+
+
+```ts
+// Safe creation with EXISTOK
+// TODO: TS SDK does not support EXISTSOK for now
+```
+
+
+
+```bash
+# Safe creation with EXISTOK
+SEARCH.CREATE cache ON STRING PREFIX 1 cache: EXISTSOK SCHEMA content TEXT
+```
+
+
+
+
+For the schema definition of the index, see the [Schema Definition](./schema-definition) section.
+
+### Getting an Index Client
+
+The `redis.search.index()` method creates a client for an existing index without making a Redis call. This is useful when you want to query or manage an index that already exists, without the overhead of creating it.
+
+
+
+
+```ts
+import { Redis, s } from "@upstash/redis";
+
+const redis = Redis.fromEnv();
+
+// Get a client for an existing index
+const users = redis.search.index("users");
+
+// Query the index
+const results = await users.query({
+ filter: { name: "John" },
+});
+
+// With schema for type safety
+const schema = s.object({
+ name: s.string(),
+ email: s.string(),
+ age: s.number("U64"),
+});
+
+const typedUsers = redis.search.index("users", schema);
+
+// Now queries are type-safe
+const typedResults = await typedUsers.query({
+ filter: { name: "John" },
+});
+```
+
+
+
+
+This method is different from `redis.search.createIndex()` which:
+- Creates a new index if it doesn't exist
+- Makes a Redis call to create the index
+- Returns an error if the index already exists (unless `EXISTOK` is used)
+
+Use `redis.search.index()` when:
+- The index already exists
+- You want to avoid unnecessary Redis calls
+- You're querying or managing an existing index
+
+Use `redis.search.createIndex()` when:
+- You need to create a new index
+- You're setting up your application for the first time
+
+### Describing an Index
+
+The `SEARCH.DESCRIBE` command returns detailed information about an index.
+
+
+
+
+```ts
+let description = await index.describe();
+console.log(description);
+```
+
+
+
+```bash
+SEARCH.DESCRIBE index
+```
+
+
+
+
+On response, the following information is returned:
+
+| Field | Description |
+|-------|-------------|
+| `name` | Index name |
+| `type` | Data type (`STRING`, `HASH`, or `JSON`) |
+| `prefixes` | List of tracked key prefixes |
+| `language` | Stemming language |
+| `schema` | Field definitions with types and options |
+
+### Dropping an Index
+
+The `SEARCH.DROP` command removes an index and stops tracking associated keys.
+
+
+
+
+```ts
+await index.drop();
+```
+
+
+
+```bash
+SEARCH.DROP index
+```
+
+
+
+
+Note that, dropping an index only removes the search index. The underlying Redis keys are not affected.
+
+### Waiting for Indexing
+
+For adequate performance, index updates are batched and committed periodically. This means recent writes may not
+immediately appear in search results. Use `SEARCH.WAITINDEXING` when you need to ensure queries reflect recent changes.
+
+The `SEARCH.WAITINDEXING` command blocks until all pending index updates are processed and visible to queries.
+We recommend **not to** call this command each time you perform a write operation on the index. For optimal indexing and
+query performance, batch updates are necessary.
+
+
+
+
+```ts
+// Add new document
+await redis.json.set("product:new", "$", { name: "New Product", price: 49.99 });
+
+// Ensure it's searchable before querying
+await index.waitIndexing();
+
+// Now the query will include the new product
+const products = await index.query({
+ filter: { name: "new" },
+});
+
+for (const product of products) {
+ console.log(product);
+}
+```
+
+
+
+```bash
+# Add new document
+JSON.SET product:new $ '{"name": "New Product", "price": 49.99}'
+
+# Ensure it's searchable before querying
+SEARCH.WAITINDEXING products
+
+# Now the query will include the new product
+SEARCH.QUERY products '{"name": "new"}'
+```
+
+
+
diff --git a/redis/search/introduction.mdx b/redis/search/introduction.mdx
new file mode 100644
index 00000000..0a12a436
--- /dev/null
+++ b/redis/search/introduction.mdx
@@ -0,0 +1,19 @@
+---
+title: Introduction
+---
+
+Modern applications often need to search through large volumes of data stored in Redis.
+While Redis excels at key-value operations, it lacks native full-text search capabilities.
+This feature bridges that gap by providing:
+
+- **Seamless Integration**: Works directly with your existing Redis data structures (JSON, Hash, String) without
+ requiring data migration or duplication to external systems.
+- **Automatic Synchronization**: Once an index is created, all write operations to matching keys are automatically
+ tracked and reflected in the index—no manual indexing required.
+- **Powerful Query Language**: A JSON-based query syntax with boolean operators, fuzzy matching, phrase queries,
+ regex support, and more.
+- **Production-Ready Performance**: Built on Tantivy, a fast full-text search engine library written in Rust,
+ the same technology that powers search engines handling millions of queries.
+
+Whether you're building a product catalog search, a document management system, or a user directory,
+this feature allows you to add sophisticated search capabilities to your Redis-backed application with minimal effort.
diff --git a/redis/search/query-operators/boolean-operators/boost.mdx b/redis/search/query-operators/boolean-operators/boost.mdx
new file mode 100644
index 00000000..4c934a19
--- /dev/null
+++ b/redis/search/query-operators/boolean-operators/boost.mdx
@@ -0,0 +1,90 @@
+---
+title: $boost
+---
+
+The `$boost` operator adjusts the score contribution of a boolean clause.
+Use it to fine-tune how much weight specific conditions have in the overall relevance ranking.
+
+By default, all matching conditions contribute equally to a document's relevance score.
+The `$boost` operator multiplies a clause's score contribution by the specified factor:
+
+- **Values greater than 1** increase importance (e.g., `$boost: 5.0` makes the clause 5x more important)
+- **Values between 0 and 1** decrease importance
+- **Negative values** demote matches, pushing them lower in results
+
+The most common use case is boosting specific `$should` conditions to prioritize certain matches:
+
+```ts
+{
+ $must: { category: "electronics" },
+ $should: [
+ { description: "premium", $boost: 10.0 }, // High priority
+ { description: "featured", $boost: 5.0 }, // Medium priority
+ { description: "sale" } // Normal priority (default boost: 1.0)
+ ]
+}
+```
+
+Documents matching "premium" rank significantly higher than those matching only "sale".
+
+Negative boost values demote matches without excluding them entirely.
+This is useful when you want to push certain results lower without filtering them out:
+
+```ts
+{
+ $must: { category: "electronics" },
+ $should: {
+ description: "budget",
+ $boost: -2.0 // Push budget items lower in results
+ }
+}
+```
+
+Unlike `$mustNot`, which excludes documents entirely, negative boosting keeps documents in results but ranks them lower.
+
+### Examples
+
+
+
+
+```ts
+// Prioritize wireless and in-stock items
+await products.query({
+ filter: {
+ $must: {
+ name: "headphones",
+ },
+ $should: {
+ description: "wireless",
+ inStock: true,
+ $boost: 5.0,
+ },
+ },
+});
+
+// Demote budget items without excluding them
+await products.query({
+ filter: {
+ $must: {
+ category: "electronics",
+ },
+ $should: {
+ description: "budget",
+ $boost: -1.0,
+ },
+ },
+});
+```
+
+
+
+```bash
+# Prioritize wireless and in-stock items
+SEARCH.QUERY products '{"$must": {"name": "headphones"}, "$should": {"description": "wireless", "inStock": true, "$boost": 5.0}}'
+
+# Demote budget items without excluding them
+SEARCH.QUERY products '{"$must": {"category": "electronics"}, "$should": {"description": "budget", "$boost": -1.0}}'
+```
+
+
+
diff --git a/redis/search/query-operators/boolean-operators/must-not.mdx b/redis/search/query-operators/boolean-operators/must-not.mdx
new file mode 100644
index 00000000..eb1faaa8
--- /dev/null
+++ b/redis/search/query-operators/boolean-operators/must-not.mdx
@@ -0,0 +1,71 @@
+---
+title: $mustNot
+---
+
+The `$mustNot` operator excludes documents that match any of the specified conditions.
+It acts as a filter, removing matching documents from the result set.
+
+The `$mustNot` operator only filters results—it never adds documents to the result set.
+This means it must be combined with `$must` or `$should` to define which documents to search.
+
+A query with only `$mustNot` returns no results because there is no base set to filter:
+
+```ts
+// This returns NO results - nothing to filter
+{ $mustNot: { category: "electronics" } }
+
+// This works - $must provides the base set, $mustNot filters it
+{ $must: { inStock: true }, $mustNot: { category: "electronics" } }
+```
+
+### Excluding Multiple Conditions
+
+When `$mustNot` contains multiple conditions (via array or object), documents matching ANY of those conditions are excluded.
+This is effectively an OR within the exclusion:
+
+```ts
+// Exclude documents that match category="generic" OR price > 500
+{ $mustNot: [{ category: "generic" }, { price: { $gt: 500 } }] }
+```
+
+### Examples
+
+
+
+
+```ts
+// Exclude out-of-stock items
+await products.query({
+ filter: {
+ $must: {
+ category: "electronics",
+ },
+ $mustNot: {
+ inStock: false,
+ },
+ },
+});
+
+// Exclude multiple conditions
+await products.query({
+ filter: {
+ $must: {
+ name: "headphones",
+ },
+ $mustNot: [{ category: "generic" }, { price: { $gt: 500 } }],
+ },
+});
+```
+
+
+
+```bash
+# Exclude out-of-stock items
+SEARCH.QUERY products '{"$must": {"category": "electronics"}, "$mustNot": {"inStock": false}}'
+
+# Exclude multiple conditions
+SEARCH.QUERY products '{"$must": {"name": "headphones"}, "$mustNot": [{"category": "generic"}, {"price": {"$gt": 500}}]}'
+```
+
+
+
diff --git a/redis/search/query-operators/boolean-operators/must.mdx b/redis/search/query-operators/boolean-operators/must.mdx
new file mode 100644
index 00000000..6eea1267
--- /dev/null
+++ b/redis/search/query-operators/boolean-operators/must.mdx
@@ -0,0 +1,76 @@
+---
+title: $must
+---
+
+The `$must` operator requires all specified conditions to match.
+Documents are only included in results if they satisfy every condition within the `$must` clause.
+This implements logical AND behavior.
+
+When you specify multiple field conditions at the top level of a query without any boolean operator,
+they are implicitly wrapped in a `$must`. These queries are equivalent:
+
+```ts
+// Implicit must
+{ name: "headphones", inStock: true }
+
+// Explicit $must
+{ $must: { name: "headphones", inStock: true } }
+```
+
+### Syntax Options
+
+The `$must` operator accepts either an object or an array:
+
+- **Object syntax**: Each key-value pair is a condition that must match
+- **Array syntax**: Each element is a separate condition object that must match
+
+Array syntax is useful when you have multiple conditions on the same field
+or when building queries programmatically.
+
+### Examples
+
+
+
+
+```ts
+// Explicit $must
+await products.query({
+ filter: {
+ $must: [{ name: "headphones" }, { inStock: true }],
+ },
+});
+
+// Equivalent implicit form
+await products.query({
+ filter: {
+ name: "headphones",
+ inStock: true,
+ },
+});
+
+// $must with object syntax
+await products.query({
+ filter: {
+ $must: {
+ name: "headphones",
+ inStock: true,
+ },
+ },
+});
+```
+
+
+
+```bash
+# Explicit $must
+SEARCH.QUERY products '{"$must": [{"name": "headphones"}, {"inStock": true}]}'
+
+# Equivalent implicit form
+SEARCH.QUERY products '{"name": "headphones", "inStock": true}'
+
+# $must with object syntax
+SEARCH.QUERY products '{"$must": {"name": "headphones", "inStock": true}}'
+```
+
+
+
\ No newline at end of file
diff --git a/redis/search/query-operators/boolean-operators/overview.mdx b/redis/search/query-operators/boolean-operators/overview.mdx
new file mode 100644
index 00000000..9cd81648
--- /dev/null
+++ b/redis/search/query-operators/boolean-operators/overview.mdx
@@ -0,0 +1,106 @@
+---
+title: Overview
+---
+
+Boolean operators combine multiple conditions to build complex queries.
+They control how individual field conditions are logically combined to determine which documents match.
+
+### Available Operators
+
+| Operator | Description |
+|----------|-------------|
+| [`$must`](./must) | All conditions must match |
+| [`$should`](./should) | At least one condition should match, or acts as a score booster when combined with `$must` |
+| [`$mustNot`](./must-not) | Excludes documents matching any condition |
+| [`$boost`](./boost) | Adjusts the score contribution of a boolean clause |
+
+### How Boolean Operators Work Together
+
+Boolean operators can be combined in a single query to express complex logic.
+The operators work together as follows:
+
+1. **`$must`** defines the required conditions. Documents must match ALL of these.
+2. **`$should`** adds optional conditions:
+ - When used alone: documents must match at least one condition
+ - When combined with `$must`: conditions become optional score boosters
+3. **`$mustNot`** filters out unwanted documents from the result set.
+
+### Combining Operators
+
+Here's an example that uses all three operators together:
+
+
+
+
+```ts
+// Find in-stock electronics, preferring wireless products, excluding budget items
+await products.query({
+ filter: {
+ $must: {
+ category: "electronics",
+ inStock: true,
+ },
+ $should: [
+ { name: "wireless" },
+ { description: "bluetooth" },
+ ],
+ $mustNot: {
+ description: "budget",
+ },
+ },
+});
+```
+
+
+
+```bash
+# Find in-stock electronics, preferring wireless products, excluding budget items
+SEARCH.QUERY products '{"$must": {"category": "electronics", "inStock": true}, "$should": [{"name": "wireless"}, {"description": "bluetooth"}], "$mustNot": {"description": "budget"}}'
+```
+
+
+
+
+This query:
+1. **Requires** documents to be in the "electronics" category AND in stock
+2. **Boosts** documents that mention "wireless" or "bluetooth" (but doesn't require them)
+3. **Excludes** any documents containing "budget" in the description
+
+### Nesting Boolean Operators
+
+Boolean operators can be nested to create more complex logic:
+
+
+
+
+```ts
+// Find products that are either (premium electronics) OR (discounted sports items)
+await products.query({
+ filter: {
+ $should: [
+ {
+ $must: {
+ category: "electronics",
+ description: "premium",
+ },
+ },
+ {
+ $must: {
+ category: "sports",
+ price: { $lt: 50 },
+ },
+ },
+ ],
+ },
+});
+```
+
+
+
+```bash
+# Find products that are either (premium electronics) OR (discounted sports items)
+SEARCH.QUERY products '{"$should": [{"$must": {"category": "electronics", "description": "premium"}}, {"$must": {"category": "sports", "price": {"$lt": 50}}}]}'
+```
+
+
+
diff --git a/redis/search/query-operators/boolean-operators/should.mdx b/redis/search/query-operators/boolean-operators/should.mdx
new file mode 100644
index 00000000..2bed9d5d
--- /dev/null
+++ b/redis/search/query-operators/boolean-operators/should.mdx
@@ -0,0 +1,110 @@
+---
+title: $should
+---
+
+The `$should` operator specifies conditions where at least one should match.
+Its behavior changes depending on whether it's used alone or combined with `$must`.
+
+When `$should` is the only boolean operator in a query, it acts as a logical OR.
+Documents must match at least one of the conditions to be included in results:
+
+```ts
+// Match documents in electronics OR sports category
+{ $should: [{ category: "electronics" }, { category: "sports" }] }
+```
+
+Documents matching more conditions score higher than those matching fewer.
+
+When `$should` is combined with `$must`, the `$should` conditions become optional score boosters.
+Documents are not required to match the `$should` conditions, but those that do receive higher relevance scores:
+
+```ts
+{
+ $must: { category: "electronics" }, // Required: must be electronics
+ $should: { description: "premium" } // Optional: boosts score if present
+}
+```
+
+This is useful for influencing result ranking without restricting the result set.
+
+When multiple `$should` conditions are specified, each matching condition adds to the document's score.
+Documents matching more conditions rank higher:
+
+```ts
+{
+ $must: { category: "electronics" },
+ $should: [
+ { name: "wireless" }, // +score if matches
+ { description: "bluetooth" }, // +score if matches
+ { description: "premium" } // +score if matches
+ ]
+}
+```
+
+A document matching all three `$should` conditions scores higher than one matching only one.
+
+### Syntax Options
+
+The `$should` operator accepts either an object or an array:
+
+- **Object syntax**: Each key-value pair is a condition that should match
+- **Array syntax**: Each element is a separate condition object that should match
+
+Array syntax is useful when you have multiple conditions on the same field
+or when building queries programmatically.
+
+### Examples
+
+
+
+
+```ts
+// Match either condition
+await products.query({
+ filter: {
+ $should: [{ category: "electronics" }, { category: "sports" }],
+ },
+});
+
+// Optional boost: "wireless" is required, "premium" boosts score if present
+await products.query({
+ filter: {
+ $must: {
+ name: "wireless",
+ },
+ $should: {
+ description: "premium",
+ },
+ },
+});
+
+// Multiple optional boosters
+await products.query({
+ filter: {
+ $must: {
+ category: "electronics",
+ },
+ $should: [
+ { name: "wireless" },
+ { description: "bluetooth" },
+ { description: "premium" },
+ ],
+ },
+});
+```
+
+
+
+```bash
+# Match either condition
+SEARCH.QUERY products '{"$should": [{"category": "electronics"}, {"category": "sports"}]}'
+
+# Optional boost: "wireless" is required, "premium" boosts score if present
+SEARCH.QUERY products '{"$must": {"name": "wireless"}, "$should": {"description": "premium"}}'
+
+# Multiple optional boosters
+SEARCH.QUERY products '{"$must": {"category": "electronics"}, "$should": [{"name": "wireless"}, {"description": "bluetooth"}, {"description": "premium"}]}'
+```
+
+
+
diff --git a/redis/search/query-operators/field-operators/boost.mdx b/redis/search/query-operators/field-operators/boost.mdx
new file mode 100644
index 00000000..5a78cfda
--- /dev/null
+++ b/redis/search/query-operators/field-operators/boost.mdx
@@ -0,0 +1,79 @@
+---
+title: $boost
+---
+
+The `$boost` operator adjusts the relevance score contribution of a field match.
+This allows you to prioritize certain matches over others in the result ranking.
+
+### How Boosting Works
+
+Search results are ordered by a relevance score that reflects how well each document matches the query.
+By default, all matching conditions contribute equally to this score.
+The `$boost` operator multiplies a match's score contribution by the specified factor.
+
+- **Positive values greater than 1** increase the match's importance (e.g., `$boost: 2.0` doubles the contribution)
+- **Values between 0 and 1** decrease the match's importance
+- **Negative values** demote matches, pushing them lower in results
+
+### Use Cases
+
+- **Prioritize premium content:** Boost matches in title fields over body text
+- **Promote featured items:** Give higher scores to promoted products
+- **Demote less relevant matches:** Use negative boosts to push certain matches down
+
+### Compatibility
+
+The `$boost` operator can be used with any field type since it modifies the score rather than the matching behavior.
+
+| Field Type | Supported |
+|------------|-----------|
+| TEXT | Yes |
+| U64/I64/F64 | Yes |
+| DATE | Yes |
+| BOOL | Yes |
+
+### Examples
+
+
+
+
+```ts
+// Boost matches in $should clauses to prioritize certain terms
+await products.query({
+ filter: {
+ $must: {
+ inStock: true,
+ },
+ $should: [
+ { description: "premium", $boost: 10.0 },
+ { description: "quality", $boost: 5.0 },
+ ],
+ },
+});
+
+// Demote budget items with negative boost
+await products.query({
+ filter: {
+ $must: {
+ category: "electronics",
+ },
+ $should: [
+ { name: "featured", $boost: 5.0 },
+ { description: "budget", $boost: -2.0 },
+ ],
+ },
+});
+```
+
+
+
+```bash
+# Boost matches in $should clauses to prioritize certain terms
+SEARCH.QUERY products '{"$must": {"inStock": true}, "$should": [{"description": "premium", "$boost": 10.0}, {"description": "quality", "$boost": 5.0}]}'
+
+# Demote budget items with negative boost
+SEARCH.QUERY products '{"$must": {"category": "electronics"}, "$should": [{"name": "featured", "$boost": 5.0}, {"description": "budget", "$boost": -2.0}]}'
+```
+
+
+
diff --git a/redis/search/query-operators/field-operators/contains.mdx b/redis/search/query-operators/field-operators/contains.mdx
new file mode 100644
index 00000000..49e1f3a1
--- /dev/null
+++ b/redis/search/query-operators/field-operators/contains.mdx
@@ -0,0 +1,49 @@
+---
+title: $contains
+---
+
+The `$contains` operator is designed for search-as-you-type and autocomplete scenarios.
+It matches documents where the field contains all the specified terms, with the last term treated as a prefix.
+
+For example, searching for `"noise cancel"` matches documents containing:
+- "noise cancelling" (the "cancel" prefix matches "cancelling")
+- "noise cancellation"
+- "noise cancelled"
+
+This differs from a simple term search because complete words must match exactly,
+while only the last word can be a partial match.
+This makes it ideal for search boxes where users type incrementally.
+
+### Compatibility
+
+| Field Type | Supported |
+|------------|-----------|
+| TEXT | Yes |
+| U64/I64/F64 | No |
+| DATE | No |
+| BOOL | No |
+
+### Examples
+
+
+
+
+```ts
+// TODO: SDK does not support $contains operator
+```
+
+
+
+```bash
+# Matches "wireless", "wireless headphones", "wireless bluetooth headphones"
+SEARCH.QUERY products '{"name": {"$contains": "wireless"}}'
+
+# Partial last word, matches "noise cancelling", "noise cancellation"
+SEARCH.QUERY products '{"description": {"$contains": "noise cancel"}}'
+
+# Search bar autocomplete, matches "bluetooth", "blue", etc.
+SEARCH.QUERY products '{"name": {"$contains": "blu"}}'
+```
+
+
+
diff --git a/redis/search/query-operators/field-operators/eq.mdx b/redis/search/query-operators/field-operators/eq.mdx
new file mode 100644
index 00000000..fed92f86
--- /dev/null
+++ b/redis/search/query-operators/field-operators/eq.mdx
@@ -0,0 +1,76 @@
+---
+title: $eq
+---
+
+The `$eq` operator performs explicit equality matching on a field.
+While you can often omit `$eq` and pass values directly (e.g., `{ price: 199.99 }`),
+using `$eq` explicitly makes your intent clear and is required when combining with other operators.
+
+For **text fields**, `$eq` performs a term search for single words.
+For multi-word values, it behaves like a phrase query, requiring the words to appear adjacent and in order.
+For **numeric fields**, it matches the exact value.
+For **boolean fields**, it matches `true` or `false`.
+For **date fields**, it matches the exact timestamp.
+
+### Compatibility
+
+| Field Type | Supported |
+|------------|-----------|
+| TEXT | Yes |
+| U64/I64/F64 | Yes |
+| DATE | Yes |
+| BOOL | Yes |
+
+### Examples
+
+
+
+
+```ts
+// Text field
+await products.query({
+ filter: {
+ name: { $eq: "wireless headphones" },
+ },
+});
+
+// Numeric field
+await products.query({
+ filter: {
+ price: { $eq: 199.99 },
+ },
+});
+
+// Boolean field
+await products.query({
+ filter: {
+ inStock: { $eq: true },
+ },
+});
+
+// Date field
+await users.query({
+ filter: {
+ createdAt: { $eq: "2024-01-15T00:00:00Z" },
+ },
+});
+```
+
+
+
+```bash
+# Text field
+SEARCH.QUERY products '{"name": {"$eq": "wireless headphones"}}'
+
+# Numeric field
+SEARCH.QUERY products '{"price": {"$eq": 199.99}}'
+
+# Boolean field
+SEARCH.QUERY products '{"inStock": {"$eq": true}}'
+
+# Date field
+SEARCH.QUERY users '{"createdAt": {"$eq": "2024-01-15T00:00:00Z"}}'
+```
+
+
+
diff --git a/redis/search/query-operators/field-operators/fuzzy.mdx b/redis/search/query-operators/field-operators/fuzzy.mdx
new file mode 100644
index 00000000..b8612eb1
--- /dev/null
+++ b/redis/search/query-operators/field-operators/fuzzy.mdx
@@ -0,0 +1,159 @@
+---
+title: $fuzzy
+---
+
+The `$fuzzy` operator matches terms that are similar but not identical to the search term.
+This is useful for handling typos, misspellings, and minor variations in user input.
+
+### Levenshtein Distance
+
+Fuzzy matching uses Levenshtein distance (also called edit distance) to measure similarity between terms.
+The distance is the minimum number of single-character edits needed to transform one word into another.
+These edits include:
+
+- **Insertions:** Adding a character ("headphone" → "headphones")
+- **Deletions:** Removing a character ("headphones" → "headphone")
+- **Substitutions:** Replacing a character ("headphones" → "headphonez")
+
+For example, the Levenshtein distance between "headphons" and "headphones" is 1 (one insertion needed).
+
+### Distance Parameter
+
+The `distance` parameter sets the maximum Levenshtein distance allowed for a match.
+The default is 1, and the maximum allowed value is 2.
+
+| Distance | Matches |
+|----------|---------|
+| 1 | Words with 1 edit (handles most single typos) |
+| 2 | Words with up to 2 edits (handles more severe misspellings) |
+
+Higher distances match more terms but may include unintended matches, so use distance 2 only when needed.
+
+### Transposition Cost
+
+A transposition swaps two adjacent characters (e.g., "teh" → "the").
+By default, a transposition counts as 1 edit.
+Setting `transpositionCostOne: false` counts transpositions as two edits (one deletion + one insertion) instead.
+
+For example, searching for "haedphone" to find "headphone":
+- With `transpositionCostOne: true`: Distance is 1 (swap counts as 1)
+- Without `transpositionCostOne`: Distance is 2 (swap "ae" → "ea" costs 2)
+
+This is useful when users commonly transpose characters while typing quickly.
+
+### Prefix Matching
+
+Setting `prefix: true` enables fuzzy prefix matching, which matches terms that start with a fuzzy
+variation of the search term. This combines the typo tolerance of fuzzy matching with prefix
+matching for incomplete words.
+
+For example, searching for "headpho" with `prefix: true`:
+- Matches "headphones" (prefix match)
+- Matches "headphone" (prefix match)
+- Matches "haedphones" with `transpositionCostOne: true` (fuzzy prefix, handles typo + incomplete word)
+
+This is particularly useful for search-as-you-type autocomplete where users may have typos
+in partially typed words.
+
+### Multiple Words
+
+When you provide multiple words to `$fuzzy`, each word is matched with fuzzy tolerance and combined using
+a boolean [`$must`](../boolean-operators/must) query.
+This means all terms must match (with their respective fuzzy tolerance) for a document to be returned.
+
+For example, searching for "wireles headphons" will match documents containing both "wireless" and "headphones", even with the typos.
+
+### Compatibility
+
+| Field Type | Supported |
+|------------|-----------|
+| TEXT | Yes |
+| U64/I64/F64 | No |
+| DATE | No |
+| BOOL | No |
+
+### Examples
+
+
+
+
+```ts
+// Simple fuzzy (distance 1, handles single typos)
+// Matches "headphone" even with the typo "headphon"
+await products.query({
+ filter: {
+ name: { $fuzzy: "headphon" },
+ },
+});
+
+// Custom distance for more tolerance
+// "haedphone" is 2 edits away from "headphone" without taking transposition into account
+await products.query({
+ filter: {
+ name: {
+ $fuzzy: {
+ value: "haedphone",
+ distance: 2,
+ transpositionCostOne: false,
+ },
+ },
+ },
+});
+
+// Handle character transpositions efficiently
+// "haedphone" has swapped "ae", with transpositionCostOne this is 1 edit
+await products.query({
+ filter: {
+ name: {
+ $fuzzy: {
+ value: "haedphone",
+ distance: 1,
+ transpositionCostOne: true,
+ },
+ },
+ },
+});
+
+// Combine prefix with transposition for robust autocomplete
+// Handles both typos and incomplete input like "haedpho" → "headphones"
+await products.query({
+ filter: {
+ name: {
+ $fuzzy: {
+ value: "haedpho",
+ prefix: true,
+ },
+ },
+ },
+});
+
+// Multiple words - matches documents containing both terms (with fuzzy tolerance)
+// Matches "wireless headphones" even with typos in both words
+await products.query({
+ filter: {
+ name: { $fuzzy: "wireles headphons" },
+ },
+});
+```
+
+
+
+```bash
+# Simple fuzzy (distance 1, handles single typos)
+SEARCH.QUERY products '{"name": {"$fuzzy": "headphon"}}'
+
+# Custom distance for more tolerance
+SEARCH.QUERY products '{"name": {"$fuzzy": {"value": "haedphone", "distance": 2, "transpositionCostOne": false}}}'
+
+# Handle character transpositions efficiently
+SEARCH.QUERY products '{"name": {"$fuzzy": {"value": "haedphone", "distance": 1, "transpositionCostOne": true}}}'
+
+# Combine prefix with transposition for robust autocomplete
+SEARCH.QUERY products '{"name": {"$fuzzy": {"value": "haedpho", "prefix": true, "transpositionCostOne": true}}}'
+
+# Multiple words - matches documents containing both terms (with fuzzy tolerance)
+SEARCH.QUERY products '{"name": {"$fuzzy": "wireles headphons"}}'
+```
+
+
+
diff --git a/redis/search/query-operators/field-operators/in.mdx b/redis/search/query-operators/field-operators/in.mdx
new file mode 100644
index 00000000..346a35d3
--- /dev/null
+++ b/redis/search/query-operators/field-operators/in.mdx
@@ -0,0 +1,44 @@
+---
+title: $in
+---
+
+The `$in` operator matches documents where the field value equals any one of the specified values.
+This is useful when you want to filter by multiple acceptable values without writing separate conditions.
+
+The operator takes an array of values and returns documents where the field matches at least one value in the array.
+This is equivalent to combining multiple `$eq` conditions with a logical OR.
+
+### Compatibility
+
+| Field Type | Supported |
+|------------|-----------|
+| TEXT | Yes |
+| U64/I64/F64 | Yes |
+| DATE | Yes |
+| BOOL | Yes |
+
+### Examples
+
+
+
+
+```ts
+// Match any of several categories
+await products.query({
+ filter: {
+ category: {
+ $in: ["electronics", "accessories", "audio"],
+ },
+ },
+});
+```
+
+
+
+```bash
+# Match any of several categories
+SEARCH.QUERY products '{"category": {"$in": ["electronics", "accessories", "audio"]}}'
+```
+
+
+
diff --git a/redis/search/query-operators/field-operators/ne.mdx b/redis/search/query-operators/field-operators/ne.mdx
new file mode 100644
index 00000000..33ff5293
--- /dev/null
+++ b/redis/search/query-operators/field-operators/ne.mdx
@@ -0,0 +1,46 @@
+---
+title: $ne
+---
+
+The `$ne` (not equal) operator excludes documents where the field matches a specific value.
+This operator works as a filter, removing matching documents from the result set rather than adding to it.
+
+Because `$ne` only filters results, it must be combined with other conditions that produce a base result set.
+A query using only `$ne` conditions returns no results since there is nothing to filter.
+
+This behavior is similar to the [`$mustNot`](../boolean-operators/must-not) boolean operator,
+but `$ne` operates at the field level rather than combining multiple conditions.
+
+### Compatibility
+
+| Field Type | Supported |
+|------------|-----------|
+| TEXT | Yes |
+| U64/I64/F64 | Yes |
+| DATE | Yes |
+| BOOL | Yes |
+
+### Examples
+
+
+
+
+```ts
+// Exclude specific values
+await products.query({
+ filter: {
+ name: "headphones",
+ price: { $gt: 9.99, $ne: 99.99 },
+ },
+});
+```
+
+
+
+```bash
+# Exclude specific values
+SEARCH.QUERY products '{"name": "headphones", "price": {"$gt": 9.99, "$ne": 99.99}}'
+```
+
+
+
diff --git a/redis/search/query-operators/field-operators/overview.mdx b/redis/search/query-operators/field-operators/overview.mdx
new file mode 100644
index 00000000..75a6704d
--- /dev/null
+++ b/redis/search/query-operators/field-operators/overview.mdx
@@ -0,0 +1,6 @@
+---
+title: Overview
+---
+
+Field operators provide precise control over how individual fields are matched.
+Use these when simple value matching doesn't meet your needs.
diff --git a/redis/search/query-operators/field-operators/phrase.mdx b/redis/search/query-operators/field-operators/phrase.mdx
new file mode 100644
index 00000000..e8632c96
--- /dev/null
+++ b/redis/search/query-operators/field-operators/phrase.mdx
@@ -0,0 +1,108 @@
+---
+title: $phrase
+---
+
+The `$phrase` operator matches documents where terms appear in a specific sequence.
+Unlike a simple multi-term search that finds documents containing all terms anywhere,
+phrase matching requires the terms to appear in the exact order specified.
+
+### Simple Form
+
+In its simplest form, `$phrase` takes a string value and requires the terms to appear adjacent to each other in order:
+
+```
+{ $phrase: "wireless headphones" }
+```
+
+This matches "wireless headphones" but not "wireless bluetooth headphones" or "headphones wireless".
+
+### Slop Parameter
+
+The `slop` parameter allows flexibility in phrase matching by permitting other words to appear between your search terms.
+The slop value represents the maximum number of position moves allowed to match the phrase.
+
+For example, with `slop: 2`:
+- "wireless headphones" matches (0 moves needed)
+- "wireless bluetooth headphones" matches (1 move: "headphones" shifted 1 position)
+- "wireless bluetooth overhead headphones" matches (2 moves)
+- "wireless bluetooth noise-cancelling headphones" does NOT match (requires 3 moves)
+
+A slop of 0 requires exact adjacency (the default behavior).
+
+### Prefix Parameter
+
+The `prefix` parameter allows the last term to match as a prefix.
+This is useful for autocomplete scenarios where users might not complete the final word:
+
+```
+description: { $phrase: { value: "wireless head", prefix: true } }
+```
+
+This matches "wireless headphones", "wireless headset", etc.
+
+**Note:** You can use either `slop` or `prefix`, but not both in the same query.
+
+### Compatibility
+
+| Field Type | Supported |
+|------------|-----------|
+| TEXT | Yes |
+| U64/I64/F64 | No |
+| DATE | No |
+| BOOL | No |
+
+### Examples
+
+
+
+
+```ts
+// Simple phrase (terms must be adjacent)
+await products.query({
+ filter: {
+ description: {
+ $phrase: "noise cancelling",
+ },
+ },
+});
+
+// With slop (allow terms between, not exact adjacency)
+await products.query({
+ filter: {
+ description: {
+ $phrase: {
+ value: "wireless headphones",
+ slop: 2,
+ },
+ },
+ },
+});
+
+// Prefix matching (last term can be partial)
+await products.query({
+ filter: {
+ name: {
+ $phrase: {
+ value: "wireless head",
+ prefix: true,
+ },
+ },
+ },
+});
+```
+
+
+
+```bash
+# Simple phrase (terms must be adjacent)
+SEARCH.QUERY products '{"description": {"$phrase": "noise cancelling"}}'
+
+# With slop (allow terms between, not exact adjacency)
+SEARCH.QUERY products '{"description": {"$phrase": {"value": "wireless headphones", "slop": 2}}}'
+
+# Prefix matching (last term can be partial)
+SEARCH.QUERY products '{"name": {"$phrase": {"value": "wireless head", "prefix": true}}}'
+```
+
+
+
diff --git a/redis/search/query-operators/field-operators/range-operators.mdx b/redis/search/query-operators/field-operators/range-operators.mdx
new file mode 100644
index 00000000..305b0c2a
--- /dev/null
+++ b/redis/search/query-operators/field-operators/range-operators.mdx
@@ -0,0 +1,72 @@
+---
+title: Range Operators
+---
+
+Range operators filter documents based on numeric or date field boundaries.
+You can use them to find values within a range, above a threshold, or below a limit.
+
+| Operator | Description | Example |
+|----------|-------------|---------|
+| `$gt` | Greater than (exclusive) | `price > 100` |
+| `$gte` | Greater than or equal (inclusive) | `price >= 100` |
+| `$lt` | Less than (exclusive) | `price < 200` |
+| `$lte` | Less than or equal (inclusive) | `price <= 200` |
+
+You can combine multiple range operators on the same field to create bounded ranges.
+For example, `{ $gte: 100, $lte: 200 }` matches values from 100 to 200 inclusive.
+
+For open-ended ranges, use a single operator.
+For example, `{ $gt: 500 }` matches all values greater than 500 with no upper limit.
+
+### Compatibility
+
+| Field Type | Supported |
+|------------|-----------|
+| TEXT | No |
+| U64/I64/F64 | Yes |
+| DATE | Yes |
+| BOOL | No |
+
+### Examples
+
+
+
+
+```ts
+// Price range
+await products.query({
+ filter: {
+ price: { $gte: 100, $lte: 200 },
+ },
+});
+
+// Open-ended range (no upper bound)
+await products.query({
+ filter: {
+ price: { $gt: 500 },
+ },
+});
+
+// Date range (no lower bound)
+await users.query({
+ filter: {
+ createdAt: { $lt: "2024-02-01T00:00:00Z" },
+ },
+});
+```
+
+
+
+```bash
+# Price range
+SEARCH.QUERY products '{"price": {"$gte": 100, "$lte": 200}}'
+
+# Open-ended range (no upper bound)
+SEARCH.QUERY products '{"price": {"$gt": 500}}'
+
+# Date range (no lower bound)
+SEARCH.QUERY users '{"createdAt": {"$lt": "2024-02-01T00:00:00Z"}}'
+```
+
+
+
diff --git a/redis/search/query-operators/field-operators/regex.mdx b/redis/search/query-operators/field-operators/regex.mdx
new file mode 100644
index 00000000..f853ce6c
--- /dev/null
+++ b/redis/search/query-operators/field-operators/regex.mdx
@@ -0,0 +1,90 @@
+---
+title: $regex
+---
+
+The `$regex` operator matches documents where field terms match a regular expression pattern.
+This is useful for flexible pattern matching when exact values are unknown or when you need to match variations of a term.
+
+### How Regex Matching Works
+
+Regex patterns are matched against individual tokenized terms, not the entire field value.
+Text fields are tokenized (split into words) during indexing, so the regex is applied to each term separately.
+
+For example, if a document contains "hello world" in a text field:
+- `{ $regex: "hel.*" }` matches (matches the "hello" term)
+- `{ $regex: "hello world.*" }` does NOT match (regex cannot span multiple terms)
+- `{ $regex: "wor.*" }` matches (matches the "world" term)
+
+For exact phrase matching across multiple words, use the [$phrase](./phrase) operator instead.
+
+### Performance Considerations
+
+- **Avoid leading wildcards:** Patterns like `".*suffix"` require scanning all terms in the index and are slow.
+ Prefer patterns that start with literal characters.
+
+### Stemming Behavior
+
+On fields with stemming enabled (the default), regex operates on the stemmed form of terms.
+For example, "running" might be stored as "run", so a regex pattern `"running.*"` would not match.
+
+To use regex reliably, consider using `NOSTEM` on fields where you need exact pattern matching.
+See [Text Field Options](../../schema-definition#text-field-options) for more details.
+
+### Compatibility
+
+| Field Type | Supported |
+|------------|-----------|
+| TEXT | Yes |
+| U64/I64/F64 | No |
+| DATE | No |
+| BOOL | No |
+
+### Examples
+
+
+
+
+```ts
+// Match categories starting with "e"
+await products.query({
+ filter: {
+ category: {
+ $regex: "e.*",
+ },
+ },
+});
+
+// Match email domains (requires NOTOKENIZE field)
+await users.query({
+ filter: {
+ email: {
+ $regex: ".*@company\\.com",
+ },
+ },
+});
+
+// Match product codes with pattern
+await products.query({
+ filter: {
+ sku: {
+ $regex: "SKU-[0-9]{4}",
+ },
+ },
+});
+```
+
+
+
+```bash
+# Match categories starting with "e"
+SEARCH.QUERY products '{"category": {"$regex": "e.*"}}'
+
+# Match email domains (requires NOTOKENIZE field)
+SEARCH.QUERY users '{"email": {"$regex": ".*@company\\.com"}}'
+
+# Match product codes with pattern
+SEARCH.QUERY products '{"sku": {"$regex": "SKU-[0-9]{4}"}}'
+```
+
+
+
diff --git a/redis/search/query-operators/field-operators/smart-matching.mdx b/redis/search/query-operators/field-operators/smart-matching.mdx
new file mode 100644
index 00000000..dce42544
--- /dev/null
+++ b/redis/search/query-operators/field-operators/smart-matching.mdx
@@ -0,0 +1,86 @@
+---
+title: Smart Matching
+---
+
+When you provide a value directly to a text field (without explicit operators like `$phrase` or `$fuzzy`),
+the search engine applies smart matching to find the most relevant results.
+This behavior varies based on the input format.
+
+### Single-Word Values
+
+For single-word searches, the engine runs multiple matching strategies and combines their scores:
+
+1. **Exact term match** (high boost): Documents containing the exact token score highest.
+
+2. **Fuzzy match** (no boost): Documents containing terms within Levenshtein distance 1
+ (with transpositions counting as a single edit) are included.
+
+3. **Fuzzy prefix match** (no boost): Documents containing terms that start with a fuzzy
+ variation of the search term are included. This handles incomplete words during typing.
+
+```
+{ name: "tabletop" }
+```
+
+This query returns results in roughly this order:
+- "**tabletop**" (exact match)
+- "**tabeltop**" (fuzzy match, 1 edit away)
+- "**tabeltopping**" (fuzzy prefix match, incomplete word with typos matches full term)
+
+### Multi-Word Values
+
+For multi-word searches, the engine runs multiple matching strategies simultaneously and combines
+their scores to surface the most relevant results:
+
+1. **Exact phrase match** (highest boost): Documents where all words appear adjacent and in order
+ score highest. Searching for `wireless headphones` ranks documents containing
+ "wireless headphones" as a phrase above those with the words scattered.
+
+2. **Exact phrase match with slop** (medium boost): Documents where all the query terms appear in
+ order but not necessarily adjacent, allowing for a small number of intervening words—receive a boost.
+ Searching for wireless headphones would rank documents containing `wireless bluetooth headphones`
+ above those where the words are far apart, but below an exact phrase match (`wireless headphones`).
+
+3. **Terms match** (medium boost): Documents containing all or some of the search terms, regardless of
+ position or order, receive a moderate score boost.
+
+3. **Fuzzy matching** (no boost): Documents containing terms similar to the search terms
+ (accounting for typos) are included with a lower score.
+
+4. **Fuzzy prefix on last word** (no boost): The last word is also matched with fuzzy prefix,
+ handling incomplete words during search-as-you-type scenarios.
+
+```
+{ description: "wireless headphon" }
+```
+
+This query returns results in roughly this order:
+- "Premium **wireless headphones** with noise cancellation" (phrase match with fuzzy prefix on last word)
+- "**Headphones** with **wireless** connectivity" (all terms present, different order)
+- "**Wireles headphone** with long battery" (fuzzy match for typos)
+
+### Double-Quoted Phrases
+
+Wrapping your search value in double quotes forces exact phrase matching.
+The words must appear adjacent and in the exact order specified.
+
+```
+{ description: "\"noise cancelling\"" }
+```
+
+This matches only documents containing "noise cancelling" as an exact phrase.
+It will NOT match:
+- "noise and cancelling" (words not adjacent)
+- "cancelling noise" (wrong order)
+- "noise-cancelling" (hyphenated, tokenized differently)
+
+This is useful when you need precise matching without the fuzzy tolerance of smart matching.
+
+### When to Use Explicit Operators
+
+Smart matching works well for general search scenarios, but consider using explicit operators when you need:
+
+- **Typo tolerance only**: Use [`$fuzzy`](./fuzzy) with specific distance settings
+- **Phrase with gaps**: Use [`$phrase`](./phrase) with the `slop` parameter
+- **Autocomplete**: Use [`$contains`](./contains) for prefix matching on the last term
+- **Pattern matching**: Use [`$regex`](./regex) for regular expression patterns
diff --git a/redis/search/query-operators/overview.mdx b/redis/search/query-operators/overview.mdx
new file mode 100644
index 00000000..d7c4d463
--- /dev/null
+++ b/redis/search/query-operators/overview.mdx
@@ -0,0 +1,5 @@
+---
+title: Overview
+---
+
+While simple field-value queries handle most use cases, operators provide precise control when you need it.
diff --git a/redis/search/querying.mdx b/redis/search/querying.mdx
new file mode 100644
index 00000000..ed2a088f
--- /dev/null
+++ b/redis/search/querying.mdx
@@ -0,0 +1,309 @@
+---
+title: Querying
+---
+
+Queries are JSON strings that describe what documents to find. The simplest form specifies field-value pairs:
+
+The most common way to search is by providing field values directly.
+This approach is recommended for most use cases and provides intelligent matching behavior.
+
+
+
+
+```ts
+// Search for a term in a specific field
+await index.query({
+ filter: {
+ name: "headphones",
+ },
+});
+
+// Search across multiple fields (implicit AND)
+await index.query({
+ filter: {
+ name: "wireless",
+ category: "electronics",
+ },
+});
+
+// Search with exact values for non-text fields
+await index.query({
+ filter: {
+ inStock: true,
+ price: 199.99,
+ },
+});
+```
+
+
+
+```bash
+# Search for a term in a specific field
+SEARCH.QUERY products '{"name": "headphones"}'
+
+# Search across multiple fields (implicit AND)
+SEARCH.QUERY products '{"name": "wireless", "category": "electronics"}'
+
+# Search with exact values for non-text fields
+SEARCH.QUERY products '{"inStock": true, "price": 199.99}'
+```
+
+
+
+
+### Smart Matching for Text Fields
+
+When you provide a value directly to a text field (without explicit operators),
+the search engine applies [smart matching](./query-operators/field-operators/smart-matching):
+
+- **Single-word values**: Performs a term search, matching the word against tokens in the field.
+- **Multi-word values**: Combines phrase matching, term matching, and fuzzy matching with
+ different boost weights to rank exact phrases highest while still finding partial matches.
+- **Double-quoted phrases**: Forces exact phrase matching (e.g., `"\"noise cancelling\""` matches
+ only those words adjacent and in order).
+
+For more control, use explicit operators like [`$phrase`](./query-operators/field-operators/phrase),
+[`$fuzzy`](./query-operators/field-operators/fuzzy), or [`$contains`](./query-operators/field-operators/contains).
+
+### Query Options
+
+The `SEARCH.QUERY` command supports several options to control result format and ordering.
+
+#### Pagination with Limit and Offset
+
+Limit controls how many results to return.
+Offset controls how many results to skip.
+
+Used together, these options provide a way to do pagination.
+
+
+
+
+```ts
+// Page 1: first 10 results (with optional offset)
+const page1 = await index.query({
+ filter: {
+ description: "wireless",
+ },
+ limit: 10,
+});
+
+// Page 2: results 11-20
+const page2 = await index.query({
+ filter: {
+ description: "wireless",
+ },
+ limit: 10,
+ offset: 10,
+});
+
+// Page 3: results 21-30
+const page3 = await index.query({
+ filter: {
+ description: "wireless",
+ },
+ limit: 10,
+ offset: 20,
+});
+```
+
+
+
+```bash
+# Page 1: first 10 results (with optional offset)
+SEARCH.QUERY products '{"description": "wireless"}' LIMIT 10
+
+# Page 2: results 11-20
+SEARCH.QUERY products '{"description": "wireless"}' LIMIT 10 OFFSET 10
+
+# Page 3: results 21-30
+SEARCH.QUERY products '{"description": "wireless"}' LIMIT 10 OFFSET 20
+```
+
+
+
+
+#### Sorting Results
+
+Normally, search results are sorted in descending order of query relevance.
+
+It is possible to override this, and sort the results by a certain field
+in ascending or descending order.
+
+Only fields defined as `FAST` in the schema can be used as the sort field.
+
+When using `SORTBY`, the score in results reflects the sort field's value rather than relevance.
+
+
+
+
+```ts
+// Sort by price, cheapest first
+await products.query({
+ filter: {
+ category: "electronics",
+ },
+ orderBy: {
+ price: "ASC",
+ },
+});
+
+// Sort by date, newest first
+await articles.query({
+ filter: {
+ author: "john",
+ },
+ orderBy: {
+ publishedAt: "DESC",
+ },
+});
+
+// Sort by rating, highest first, which can be combined with LIMIT and OFFSET
+await products.query({
+ filter: {
+ inStock: true,
+ },
+ orderBy: {
+ rating: "DESC",
+ },
+ limit: 5,
+});
+```
+
+
+
+```bash
+# Sort by price, cheapest first
+SEARCH.QUERY products '{"category": "electronics"}' ORDERBY price ASC
+
+# Sort by date, newest first
+SEARCH.QUERY articles '{"author": "john"}' ORDERBY publishedAt DESC
+
+# Sort by rating, highest first, which can be combined with LIMIT and OFFSET
+SEARCH.QUERY products '{"inStock": true}' ORDERBY rating DESC LIMIT 5
+```
+
+
+
+
+#### Controlling Output
+
+By default, search results include document key, relevance score, and the contents of the document
+(including the non-indexed fields).
+
+For JSON and string indexes, that means the stored JSON objects as whole. For hash indexes, it means
+all fields and values.
+
+It is possible to get only document keys and relevance scores using `NOCONTENT`.
+
+
+
+
+```ts
+// Return only keys and scores
+await products.query({
+ filter: {
+ name: "headphones",
+ },
+ select: {},
+});
+```
+
+
+
+```bash
+# Return only keys and scores
+SEARCH.QUERY products '{"name": "headphones"}' NOCONTENT
+```
+
+
+
+
+It is also possible to select only the specified fields of the documents, whether they are indexed or not.
+
+
+
+
+```ts
+// Return specific fields only
+await products.query({
+ filter: {
+ name: "headphones",
+ },
+ select: {
+ name: true,
+ price: true,
+ },
+});
+```
+
+
+
+```bash
+# Return specific fields only
+SEARCH.QUERY products '{"name": "headphones"}' SELECT 2 name price
+```
+
+
+
+
+
+When using [aliased fields](/redis/search/schema-definition#aliased-fields),
+use the **actual document field name** (not the alias) when selecting fields to return.
+This is because aliasing happens at the index level and does not modify the underlying documents.
+
+
+#### Highlighting
+
+Highlighting allows you to see why a document matched the query by marking the matching portions of the document's fields.
+
+By default, `` and `` are used as the highlight tags.
+
+
+
+
+```ts
+// Highlight matching terms
+await products.query({
+ filter: {
+ description: "wireless noise cancelling",
+ },
+ highlight: {
+ fields: ["description"],
+ },
+});
+
+// Custom open and close highlight tags
+await products.query({
+ filter: {
+ description: "wireless",
+ },
+ highlight: {
+ fields: ["description"],
+ preTag: "!!",
+ postTag: "**",
+ },
+});
+```
+
+
+
+```bash
+# Highlight matching terms
+SEARCH.QUERY products '{"description": "wireless noise cancelling"}' HIGHLIGHT FIELDS 1 description
+
+# Custom open and close highlight tags
+SEARCH.QUERY products '{"description": "wireless"}' HIGHLIGHT FIELDS 1 description TAGS !! **
+```
+
+
+
+
+Note that, highlighting only works for operators that resolve to terms, such as term or
+phrase queries.
+
+
+When using [aliased fields](/redis/search/schema-definition#aliased-fields),
+use the **alias name** (not the actual document field name) when specifying fields to highlight.
+The highlighting feature works with indexed field names, which are the aliases.
+
diff --git a/redis/search/recipes/blog-search.mdx b/redis/search/recipes/blog-search.mdx
new file mode 100644
index 00000000..7b343dcd
--- /dev/null
+++ b/redis/search/recipes/blog-search.mdx
@@ -0,0 +1,453 @@
+---
+title: Blog Search
+---
+
+This recipe demonstrates building a full-text blog search with highlighted snippets,
+phrase matching, and date filtering.
+
+### Schema Design
+
+The schema prioritizes full-text search capabilities with date-based filtering:
+
+
+
+
+```ts
+import { Redis, s } from "@upstash/redis";
+
+const redis = Redis.fromEnv();
+
+const articles = await redis.search.createIndex({
+ name: "articles",
+ dataType: "hash",
+ prefix: "article:",
+ schema: s.object({
+ // Full-text searchable content
+ title: s.string(),
+ body: s.string(),
+ summary: s.string(),
+
+ // Author name without stemming
+ author: s.string().noStem(),
+
+ // Date fields for filtering and sorting
+ publishedAt: s.date().fast(),
+ updatedAt: s.date().fast(),
+
+ // Status for draft/published filtering
+ published: s.boolean(),
+
+ // View count for popularity sorting
+ viewCount: s.number("U64"),
+ }),
+});
+```
+
+
+
+```bash
+SEARCH.CREATE articles ON HASH PREFIX 1 article: SCHEMA title TEXT body TEXT summary TEXT author TEXT NOSTEM publishedAt DATE FAST updatedAt DATE FAST published BOOL viewCount U64 FAST
+```
+
+
+
+
+### Sample Data
+
+
+
+
+```ts
+await redis.hset("article:1", {
+ title: "Getting Started with Redis Search",
+ body: "Redis Search provides powerful full-text search capabilities directly in Redis. In this tutorial, we'll explore how to create indexes, define schemas, and write queries. Full-text search allows you to find documents based on their content rather than just their keys. This is essential for building search features in modern applications.",
+ summary: "Learn how to add full-text search to your Redis application with practical examples.",
+ author: "Jane Smith",
+ tags: "redis,search,tutorial",
+ publishedAt: "2024-03-15T10:00:00Z",
+ updatedAt: "2024-03-15T10:00:00Z",
+ published: "true",
+ viewCount: "1542",
+});
+
+await redis.hset("article:2", {
+ title: "Advanced Query Techniques for Search",
+ body: "Once you've mastered the basics, it's time to explore advanced query techniques. Boolean operators let you combine conditions with AND, OR, and NOT logic. Phrase matching ensures words appear in sequence. Fuzzy matching handles typos gracefully. Together, these features enable sophisticated search experiences.",
+ summary: "Master boolean operators, phrase matching, and fuzzy search for better results.",
+ author: "John Doe",
+ tags: "redis,search,advanced",
+ publishedAt: "2024-03-20T14:30:00Z",
+ updatedAt: "2024-03-22T09:15:00Z",
+ published: "true",
+ viewCount: "892",
+});
+
+await redis.hset("article:3", {
+ title: "Building Real-Time Search with Redis",
+ body: "Real-time search requires instant indexing and low-latency queries. Redis excels at both. When you write data to Redis, the search index updates automatically. Queries execute in milliseconds, even with millions of documents. This makes Redis ideal for applications where search results must reflect the latest data.",
+ summary: "Build search features that update instantly as data changes.",
+ author: "Jane Smith",
+ tags: "redis,real-time,performance",
+ publishedAt: "2024-03-25T08:00:00Z",
+ updatedAt: "2024-03-25T08:00:00Z",
+ published: "true",
+ viewCount: "2103",
+});
+```
+
+
+
+```bash
+HSET article:1 title "Getting Started with Redis Search" body "Redis Search provides powerful full-text search capabilities directly in Redis. In this tutorial, we will explore how to create indexes, define schemas, and write queries. Full-text search allows you to find documents based on their content rather than just their keys. This is essential for building search features in modern applications." summary "Learn how to add full-text search to your Redis application with practical examples." author "Jane Smith" tags "redis,search,tutorial" publishedAt "2024-03-15T10:00:00Z" updatedAt "2024-03-15T10:00:00Z" published "true" viewCount "1542"
+
+HSET article:2 title "Advanced Query Techniques for Search" body "Once you have mastered the basics, it is time to explore advanced query techniques. Boolean operators let you combine conditions with AND, OR, and NOT logic. Phrase matching ensures words appear in sequence. Fuzzy matching handles typos gracefully. Together, these features enable sophisticated search experiences." summary "Master boolean operators, phrase matching, and fuzzy search for better results." author "John Doe" tags "redis,search,advanced" publishedAt "2024-03-20T14:30:00Z" updatedAt "2024-03-22T09:15:00Z" published "true" viewCount "892"
+
+HSET article:3 title "Building Real-Time Search with Redis" body "Real-time search requires instant indexing and low-latency queries. Redis excels at both. When you write data to Redis, the search index updates automatically. Queries execute in milliseconds, even with millions of documents. This makes Redis ideal for applications where search results must reflect the latest data." summary "Build search features that update instantly as data changes." author "Jane Smith" tags "redis,real-time,performance" publishedAt "2024-03-25T08:00:00Z" updatedAt "2024-03-25T08:00:00Z" published "true" viewCount "2103"
+```
+
+
+
+
+### Waiting for Indexing
+
+Index updates are batched for performance, so newly added data may not appear in search results immediately.
+Use `SEARCH.WAITINDEXING` to ensure all pending updates are processed before querying:
+
+
+
+
+```ts
+await articles.waitIndexing();
+```
+
+
+
+```bash
+SEARCH.WAITINDEXING articles
+```
+
+
+
+
+### Basic Full-Text Search
+
+Smart matching handles natural language queries across title and body:
+
+
+
+
+```ts
+// Search across title and body
+const results = await articles.query({
+ filter: {
+ $should: [
+ { title: "redis search" },
+ { body: "redis search" },
+ ],
+ },
+});
+```
+
+
+
+```bash
+SEARCH.QUERY articles '{"$should": [{"title": "redis search"}, {"body": "redis search"}]}'
+```
+
+
+
+
+### Search with Highlighted Results
+
+Highlighting shows users why each result matched their query:
+
+
+
+
+```ts
+// Search with highlighted matches in title and body
+const results = await articles.query({
+ filter: {
+ $should: [
+ { title: "full-text search" },
+ { body: "full-text search" },
+ ],
+ },
+ highlight: {
+ fields: ["title", "body"],
+ },
+});
+
+// Results include highlighted text like:
+// "Redis Search provides powerful full-text search capabilities..."
+```
+
+
+
+```bash
+SEARCH.QUERY articles '{"$should": [{"title": "full-text search"}, {"body": "full-text search"}]}' HIGHLIGHT FIELDS 2 title body
+```
+
+
+
+
+### Custom Highlight Tags
+
+Use custom tags for different rendering contexts:
+
+
+
+
+```ts
+// Markdown-style highlighting
+const results = await articles.query({
+ filter: {
+ body: "redis",
+ },
+ highlight: {
+ fields: ["body"],
+ preTag: "**",
+ postTag: "**",
+ },
+});
+
+// HTML with custom class
+const htmlResults = await articles.query({
+ filter: {
+ body: "redis",
+ },
+ highlight: {
+ fields: ["body"],
+ preTag: "",
+ postTag: "",
+ },
+});
+```
+
+
+
+```bash
+# Markdown-style highlighting
+SEARCH.QUERY articles '{"body": "redis"}' HIGHLIGHT FIELDS 1 body TAGS ** **
+
+# HTML with custom class
+SEARCH.QUERY articles '{"body": "redis"}' HIGHLIGHT FIELDS 1 body TAGS "" ""
+```
+
+
+
+
+### Exact Phrase Search
+
+Find articles containing exact phrases using double quotes or the `$phrase` operator:
+
+
+
+
+```ts
+// Using double quotes for exact phrase
+const results = await articles.query({
+ filter: {
+ body: "\"full-text search\"",
+ },
+});
+
+// Using $phrase operator
+const phraseResults = await articles.query({
+ filter: {
+ body: {
+ $phrase: "boolean operators",
+ },
+ },
+});
+
+// Phrase with slop (allow words between)
+// Matches "search results" or "search the results" or "search for better results"
+const slopResults = await articles.query({
+ filter: {
+ body: {
+ $phrase: {
+ value: "search results",
+ slop: 3,
+ },
+ },
+ },
+});
+```
+
+
+
+```bash
+# Using double quotes for exact phrase
+SEARCH.QUERY articles '{"body": "\"full-text search\""}'
+
+# Using $phrase operator
+SEARCH.QUERY articles '{"body": {"$phrase": "boolean operators"}}'
+
+# Phrase with slop
+SEARCH.QUERY articles '{"body": {"$phrase": {"value": "search results", "slop": 3}}}'
+```
+
+
+
+
+### Filter by Author
+
+Find all articles by a specific author:
+
+
+
+
+```ts
+// All articles by Jane Smith
+const results = await articles.query({
+ filter: {
+ author: "Jane Smith",
+ published: true,
+ },
+ orderBy: {
+ publishedAt: "DESC",
+ },
+});
+
+// Search within a specific author's articles
+const authorSearch = await articles.query({
+ filter: {
+ $must: {
+ author: "Jane Smith",
+ body: "redis",
+ },
+ },
+});
+```
+
+
+
+```bash
+# All articles by Jane Smith
+SEARCH.QUERY articles '{"author": "Jane Smith", "published": true}' ORDERBY publishedAt DESC
+
+# Search within a specific author's articles
+SEARCH.QUERY articles '{"$must": {"author": "Jane Smith", "body": "redis"}}'
+```
+
+
+
+
+### Date Range Queries
+
+Find articles published within a specific time period:
+
+
+
+
+```ts
+// Articles from a specific month
+const marchArticles = await articles.query({
+ filter: {
+ publishedAt: {
+ $gte: "2026-01-01T00:00:00Z",
+ $lt: "2026-02-01T00:00:00Z",
+ },
+ },
+ orderBy: {
+ publishedAt: "DESC",
+ },
+});
+```
+
+
+
+```bash
+# Articles from a specific month
+SEARCH.QUERY articles '{"publishedAt": {"$gte": "2026-01-01T00:00:00Z", "$lt": "2026-02-01T00:00:00Z"}}' ORDERBY publishedAt DESC
+```
+
+
+
+
+### Popular Articles
+
+Sort by view count to find popular content:
+
+
+
+
+```ts
+// Most popular articles
+const popular = await articles.query({
+ filter: {
+ published: true,
+ },
+ orderBy: {
+ viewCount: "DESC",
+ },
+ limit: 10,
+});
+
+// Popular articles about a topic
+const popularRedis = await articles.query({
+ filter: {
+ $must: {
+ body: "redis",
+ published: true,
+ },
+ },
+ orderBy: {
+ viewCount: "DESC",
+ },
+ limit: 5,
+});
+```
+
+
+
+```bash
+# Most popular articles
+SEARCH.QUERY articles '{"published": true}' ORDERBY viewCount DESC LIMIT 10
+
+# Popular articles about a topic
+SEARCH.QUERY articles '{"$must": {"body": "redis", "published": true}}' ORDERBY viewCount DESC LIMIT 5
+```
+
+
+
+
+### Boosting Title Matches
+
+Prioritize matches in the title over body text:
+
+
+
+
+```ts
+// Boost title matches for better relevance
+const results = await articles.query({
+ filter: {
+ $should: [
+ { title: "redis search", $boost: 5.0 }, // Title matches score 5x higher
+ { body: "redis search" },
+ { summary: "redis search", $boost: 2.0 },
+ ],
+ },
+});
+```
+
+
+
+```bash
+SEARCH.QUERY articles '{"$should": [{"title": "redis search", "$boost": 5.0}, {"body": "redis search"}, {"summary": "redis search", "$boost": 2.0}]}'
+```
+
+
+
+
+### Key Takeaways
+
+- Hash storage works well for flat document structures like blog articles
+- Use highlighting to show users why results matched their query
+- Boost title matches over body text for better relevance
+- Use `$phrase` with `slop` for flexible phrase matching
+- Combine date ranges with text search for temporal filtering
+- Mark `viewCount` as `FAST` to enable popularity sorting
+- Filter drafts using `published: true` in `$must` conditions
diff --git a/redis/search/recipes/e-commerce-search.mdx b/redis/search/recipes/e-commerce-search.mdx
new file mode 100644
index 00000000..d627af33
--- /dev/null
+++ b/redis/search/recipes/e-commerce-search.mdx
@@ -0,0 +1,446 @@
+---
+title: E-commerce Search
+---
+
+This recipe demonstrates building a product catalog search with filtering, sorting,
+typo tolerance, and relevance boosting.
+
+### Schema Design
+
+The schema balances searchability with filtering and sorting capabilities:
+
+
+
+
+```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({
+ // Full-text searchable fields
+ name: s.string(),
+ description: s.string(),
+ brand: s.string().noStem(), // "Nike" shouldn't stem to "Nik"
+
+ // Exact-match category for filtering
+ category: s.string().noTokenize(), // "Electronics > Audio" as single token
+
+ // Numeric fields for filtering and sorting
+ price: s.number("F64"), // Enable sorting by price
+ rating: s.number("F64"), // Enable sorting by rating
+ reviewCount: s.number("U64"),
+
+ // Boolean for stock filtering
+ inStock: s.boolean(),
+
+ // Date for "new arrivals" queries
+ createdAt: s.date().fast(),
+ }),
+});
+```
+
+
+
+```bash
+SEARCH.CREATE products ON JSON PREFIX 1 product: SCHEMA name TEXT description TEXT brand TEXT NOSTEM category TEXT NOTOKENIZE price F64 FAST rating F64 FAST reviewCount U64 inStock BOOL createdAt DATE FAST
+```
+
+
+
+
+### Sample Data
+
+
+
+
+```ts
+await redis.json.set("product:1", "$", {
+ name: "Sony WH-1000XM5 Wireless Headphones",
+ description: "Industry-leading noise cancellation with premium sound quality. 30-hour battery life with quick charging.",
+ brand: "Sony",
+ category: "Electronics > Audio > Headphones",
+ price: 349.99,
+ rating: 4.8,
+ reviewCount: 2847,
+ inStock: true,
+ createdAt: "2024-01-15T00:00:00Z",
+});
+
+await redis.json.set("product:2", "$", {
+ name: "Apple AirPods Pro 2nd Generation",
+ description: "Active noise cancellation, transparency mode, and spatial audio. MagSafe charging case included.",
+ brand: "Apple",
+ category: "Electronics > Audio > Earbuds",
+ price: 249.99,
+ rating: 4.7,
+ reviewCount: 5621,
+ inStock: true,
+ createdAt: "2024-02-20T00:00:00Z",
+});
+
+await redis.json.set("product:3", "$", {
+ name: "Bose QuietComfort Ultra Headphones",
+ description: "World-class noise cancellation with immersive spatial audio. Luxurious comfort for all-day wear.",
+ brand: "Bose",
+ category: "Electronics > Audio > Headphones",
+ price: 429.99,
+ rating: 4.6,
+ reviewCount: 1253,
+ inStock: false,
+ createdAt: "2024-03-10T00:00:00Z",
+});
+```
+
+
+
+```bash
+JSON.SET product:1 $ '{"name": "Sony WH-1000XM5 Wireless Headphones", "description": "Industry-leading noise cancellation with premium sound quality. 30-hour battery life with quick charging.", "brand": "Sony", "category": "Electronics > Audio > Headphones", "price": 349.99, "rating": 4.8, "reviewCount": 2847, "inStock": true, "createdAt": "2024-01-15T00:00:00Z"}'
+
+JSON.SET product:2 $ '{"name": "Apple AirPods Pro 2nd Generation", "description": "Active noise cancellation, transparency mode, and spatial audio. MagSafe charging case included.", "brand": "Apple", "category": "Electronics > Audio > Earbuds", "price": 249.99, "rating": 4.7, "reviewCount": 5621, "inStock": true, "createdAt": "2024-02-20T00:00:00Z"}'
+
+JSON.SET product:3 $ '{"name": "Bose QuietComfort Ultra Headphones", "description": "World-class noise cancellation with immersive spatial audio. Luxurious comfort for all-day wear.", "brand": "Bose", "category": "Electronics > Audio > Headphones", "price": 429.99, "rating": 4.6, "reviewCount": 1253, "inStock": false, "createdAt": "2024-03-10T00:00:00Z"}'
+```
+
+
+
+
+### Waiting for Indexing
+
+Index updates are batched for performance, so newly added data may not appear in search results immediately.
+Use `SEARCH.WAITINDEXING` to ensure all pending updates are processed before querying:
+
+
+
+
+```ts
+// Wait for all pending index updates to complete
+await products.waitIndexing();
+```
+
+
+
+```bash
+SEARCH.WAITINDEXING products
+```
+
+
+
+
+This is especially useful in scripts or tests where you need to query immediately after inserting data.
+In production, the slight indexing delay is usually acceptable and calling this after every write is not recommended.
+
+### Basic Product Search
+
+The simplest search uses smart matching for natural language queries:
+
+
+
+
+```ts
+// User types "wireless headphones" in search box
+const results = await products.query({
+ filter: {
+ name: "wireless headphones",
+ },
+});
+```
+
+
+
+```bash
+SEARCH.QUERY products '{"name": "wireless headphones"}'
+```
+
+
+
+
+Smart matching automatically handles this by:
+1. Prioritizing exact phrase matches ("wireless headphones" adjacent)
+2. Including documents with both terms in any order
+3. Finding fuzzy matches for typos
+
+### Search with Typo Tolerance
+
+Users often misspell product names. Use fuzzy matching to handle typos:
+
+
+
+
+```ts
+// User types "wireles headphons" (two typos)
+const results = await products.query({
+ filter: {
+ $should: [
+ { name: { $fuzzy: "wireles" } },
+ { name: { $fuzzy: "headphons" } },
+ ],
+ },
+});
+```
+
+
+
+```bash
+SEARCH.QUERY products '{"$should": [{"name": {"$fuzzy": "wireles"}}, {"name": {"$fuzzy": "headphons"}}]}'
+```
+
+
+
+
+### Filtered Search
+
+Combine text search with filters for category, price, and availability:
+
+
+
+
+```ts
+// Search within a category, price range, and only in-stock items
+const results = await products.query({
+ filter: {
+ $must: {
+ description: "noise cancellation",
+ category: "Electronics > Audio > Headphones",
+ inStock: true,
+ price: { $gte: 200, $lte: 400 },
+ },
+ },
+});
+```
+
+
+
+```bash
+SEARCH.QUERY products '{"$must": {"description": "noise cancellation", "category": "Electronics > Audio > Headphones", "inStock": true, "price": {"$gte": 200, "$lte": 400}}}'
+```
+
+
+
+
+### Boosting Premium Results
+
+Promote featured products or preferred brands using score boosting:
+
+
+
+
+```ts
+// Search for headphones, boosting Sony and in-stock items
+const results = await products.query({
+ filter: {
+ $must: {
+ name: "headphones",
+ },
+ $should: [
+ { brand: "Sony", $boost: 5.0 }, // Preferred brand
+ { inStock: true, $boost: 10.0 }, // Strongly prefer in-stock
+ { description: "premium", $boost: 2.0 },
+ ],
+ },
+});
+```
+
+
+
+```bash
+SEARCH.QUERY products '{"$must": {"name": "headphones"}, "$should": [{"brand": "Sony", "$boost": 5.0}, {"inStock": true, "$boost": 10.0}, {"description": "premium", "$boost": 2.0}]}'
+```
+
+
+
+
+### Sorting and Pagination
+
+Sort results by price or rating, with pagination for large result sets:
+
+
+
+
+```ts
+// Page 1: Top-rated headphones, 20 per page
+const page1 = await products.query({
+ filter: {
+ category: "Electronics > Audio > Headphones",
+ },
+ orderBy: {
+ rating: "DESC",
+ },
+ limit: 20,
+});
+
+// Page 2
+const page2 = await products.query({
+ filter: {
+ category: "Electronics > Audio > Headphones",
+ },
+ orderBy: {
+ rating: "DESC",
+ },
+ limit: 20,
+ offset: 20,
+});
+
+// Sort by price, cheapest first
+const cheapest = await products.query({
+ filter: {
+ name: "headphones",
+ inStock: true,
+ },
+ orderBy: {
+ price: "ASC",
+ },
+ limit: 10,
+});
+```
+
+
+
+```bash
+# Page 1: Top-rated headphones
+SEARCH.QUERY products '{"category": "Electronics > Audio > Headphones"}' ORDERBY rating DESC LIMIT 20
+
+# Page 2
+SEARCH.QUERY products '{"category": "Electronics > Audio > Headphones"}' ORDERBY rating DESC LIMIT 20 OFFSET 20
+
+# Sort by price, cheapest first
+SEARCH.QUERY products '{"name": "headphones", "inStock": true}' ORDERBY price ASC LIMIT 10
+```
+
+
+
+
+### New Arrivals
+
+Find recently added products using date range queries:
+
+
+
+
+```ts
+// Products added after a specific date
+const newArrivals = await products.query({
+ filter: {
+ createdAt: { $gte: "2026-01-01T00:00:00Z" },
+ inStock: true,
+ },
+ orderBy: {
+ createdAt: "DESC",
+ },
+ limit: 10,
+});
+```
+
+
+
+```bash
+# Products added after a specific date
+SEARCH.QUERY products '{"createdAt": {"$gte": "2026-01-01T00:00:00Z"}, "inStock": true}' ORDERBY createdAt DESC LIMIT 10
+```
+
+
+
+
+### Excluding Out-of-Stock Items
+
+Use `$mustNot` to filter out unavailable products:
+
+
+
+
+```ts
+// Search results excluding out-of-stock items
+const results = await products.query({
+ filter: {
+ $must: {
+ name: "headphones",
+ },
+ $mustNot: {
+ inStock: false,
+ },
+ },
+});
+```
+
+
+
+```bash
+SEARCH.QUERY products '{"$must": {"name": "headphones"}, "$mustNot": {"inStock": false}}'
+```
+
+
+
+
+### Multi-Category Search
+
+Search across multiple categories using `$in`:
+
+
+
+
+```ts
+// Find products in either headphones or earbuds categories
+const results = await products.query({
+ filter: {
+ $must: {
+ description: "noise cancellation",
+ category: {
+ $in: [
+ "Electronics > Audio > Headphones",
+ "Electronics > Audio > Earbuds",
+ ],
+ },
+ },
+ },
+});
+```
+
+
+
+```bash
+SEARCH.QUERY products '{"$must": {"description": "noise cancellation", "category": {"$in": ["Electronics > Audio > Headphones", "Electronics > Audio > Earbuds"]}}}'
+```
+
+
+
+
+### Counting Results
+
+Use `SEARCH.COUNT` to get the number of matching documents without retrieving them.
+This is useful for pagination UI ("Showing 1-20 of 156 results") or analytics:
+
+
+
+
+```ts
+// Count all products in a category
+const totalHeadphones = await products.count({
+ filter: {
+ category: "Electronics > Audio > Headphones",
+ },
+});
+```
+
+
+
+```bash
+# Count all products in a category
+SEARCH.COUNT products '{"category": "Electronics > Audio > Headphones"}'
+```
+
+
+
+
+### Key Takeaways
+
+- Use `NOTOKENIZE` for categories and codes that should match exactly
+- Use `NOSTEM` for brand names to prevent unwanted stemming
+- Mark price, rating, and date fields as `FAST` for sorting
+- Combine `$must`, `$should`, and `$mustNot` for complex filtering
+- Use `$boost` to promote featured or preferred items
+- Use `SEARCH.COUNT` to get result counts for pagination UI
+- Smart matching handles most natural language queries automatically
diff --git a/redis/search/recipes/overview.mdx b/redis/search/recipes/overview.mdx
new file mode 100644
index 00000000..4f7dca2d
--- /dev/null
+++ b/redis/search/recipes/overview.mdx
@@ -0,0 +1,24 @@
+---
+title: Overview
+---
+
+This section provides complete, real-world examples demonstrating how to use Redis Search
+in common application scenarios. Each recipe includes schema design, sample data, and
+query patterns you can adapt for your own use cases.
+
+### Available Recipes
+
+| Recipe | Description | Key Features |
+|--------|-------------|--------------|
+| [E-commerce Search](./e-commerce-search) | Build a product catalog with filters, faceted search, and typo tolerance | Fuzzy matching, range filters, boosting, sorting |
+| [Blog Search](./blog-search) | Full-text article search with highlighted snippets | Phrase matching, date ranges, highlighting, smart matching |
+| [User Directory](./user-directory) | Searchable employee directory with autocomplete | Autocomplete, nested fields, exact matching, boolean filters |
+
+### Choosing the Right Approach
+
+These recipes demonstrate different patterns:
+
+- **E-commerce**: Optimized for filtering and faceted navigation with typo-tolerant search
+- **Blog Search**: Optimized for content discovery with relevance ranking and visual feedback
+- **User Directory**: Optimized for quick lookups with autocomplete and exact matching
+
diff --git a/redis/search/recipes/user-directory.mdx b/redis/search/recipes/user-directory.mdx
new file mode 100644
index 00000000..a4fdffe2
--- /dev/null
+++ b/redis/search/recipes/user-directory.mdx
@@ -0,0 +1,518 @@
+---
+title: User Directory
+---
+
+This recipe demonstrates building a searchable employee directory with autocomplete,
+fuzzy name matching, and department filtering.
+
+### Schema Design
+
+The schema uses nested fields for profile data and exact matching for identifiers:
+
+
+
+
+```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({
+ // Exact username matching (no tokenization)
+ username: s.string().noTokenize(),
+
+ // Nested profile fields
+ profile: s.object({
+ // Name search without stemming (proper nouns)
+ firstName: s.string().noStem(),
+ lastName: s.string().noStem(),
+ displayName: s.string().noStem(),
+
+ // Email as exact match (contains special characters)
+ email: s.string().noTokenize(),
+
+ // Searchable bio/about text
+ bio: s.string(),
+ }),
+
+ // Department and role for filtering
+ department: s.string().noTokenize(),
+ role: s.string().noTokenize(),
+ title: s.string(),
+
+ // Boolean flags
+ isActive: s.boolean(),
+ isAdmin: s.boolean(),
+
+ // Dates for filtering
+ hiredAt: s.date().fast(),
+ lastActiveAt: s.date().fast(),
+ }),
+});
+```
+
+
+
+```bash
+SEARCH.CREATE users ON JSON PREFIX 1 user: SCHEMA username TEXT NOTOKENIZE profile.firstName TEXT NOSTEM profile.lastName TEXT NOSTEM profile.displayName TEXT NOSTEM profile.email TEXT NOTOKENIZE profile.bio TEXT department TEXT NOTOKENIZE role TEXT NOTOKENIZE title TEXT isActive BOOL isAdmin BOOL hiredAt DATE FAST lastActiveAt DATE FAST
+```
+
+
+
+
+### Sample Data
+
+
+
+
+```ts
+await redis.json.set("user:1", "$", {
+ username: "jsmith",
+ profile: {
+ firstName: "Jane",
+ lastName: "Smith",
+ displayName: "Jane Smith",
+ email: "jane.smith@company.com",
+ bio: "Senior software engineer focused on backend systems and distributed computing.",
+ },
+ department: "Engineering",
+ role: "Individual Contributor",
+ title: "Senior Software Engineer",
+ isActive: true,
+ isAdmin: false,
+ hiredAt: "2021-03-15T00:00:00Z",
+ lastActiveAt: "2024-03-25T14:30:00Z",
+});
+
+await redis.json.set("user:2", "$", {
+ username: "mjohnson",
+ profile: {
+ firstName: "Michael",
+ lastName: "Johnson",
+ displayName: "Mike Johnson",
+ email: "michael.johnson@company.com",
+ bio: "Engineering manager leading the platform team. Passionate about developer experience.",
+ },
+ department: "Engineering",
+ role: "Manager",
+ title: "Engineering Manager",
+ isActive: true,
+ isAdmin: true,
+ hiredAt: "2019-06-01T00:00:00Z",
+ lastActiveAt: "2024-03-25T16:45:00Z",
+});
+
+await redis.json.set("user:3", "$", {
+ username: "swilliams",
+ profile: {
+ firstName: "Sarah",
+ lastName: "Williams",
+ displayName: "Sarah Williams",
+ email: "sarah.williams@company.com",
+ bio: "Product designer specializing in user research and interaction design.",
+ },
+ department: "Design",
+ role: "Individual Contributor",
+ title: "Senior Product Designer",
+ isActive: true,
+ isAdmin: false,
+ hiredAt: "2022-01-10T00:00:00Z",
+ lastActiveAt: "2024-03-24T11:20:00Z",
+});
+
+await redis.json.set("user:4", "$", {
+ username: "rbrown",
+ profile: {
+ firstName: "Robert",
+ lastName: "Brown",
+ displayName: "Rob Brown",
+ email: "robert.brown@company.com",
+ bio: "Former engineering lead, now focused on technical writing and documentation.",
+ },
+ department: "Engineering",
+ role: "Individual Contributor",
+ title: "Staff Engineer",
+ isActive: false,
+ isAdmin: false,
+ hiredAt: "2018-09-20T00:00:00Z",
+ lastActiveAt: "2023-12-15T09:00:00Z",
+});
+```
+
+
+
+```bash
+JSON.SET user:1 $ '{"username": "jsmith", "profile": {"firstName": "Jane", "lastName": "Smith", "displayName": "Jane Smith", "email": "jane.smith@company.com", "bio": "Senior software engineer focused on backend systems and distributed computing."}, "department": "Engineering", "role": "Individual Contributor", "title": "Senior Software Engineer", "isActive": true, "isAdmin": false, "hiredAt": "2021-03-15T00:00:00Z", "lastActiveAt": "2024-03-25T14:30:00Z"}'
+
+JSON.SET user:2 $ '{"username": "mjohnson", "profile": {"firstName": "Michael", "lastName": "Johnson", "displayName": "Mike Johnson", "email": "michael.johnson@company.com", "bio": "Engineering manager leading the platform team. Passionate about developer experience."}, "department": "Engineering", "role": "Manager", "title": "Engineering Manager", "isActive": true, "isAdmin": true, "hiredAt": "2019-06-01T00:00:00Z", "lastActiveAt": "2024-03-25T16:45:00Z"}'
+
+JSON.SET user:3 $ '{"username": "swilliams", "profile": {"firstName": "Sarah", "lastName": "Williams", "displayName": "Sarah Williams", "email": "sarah.williams@company.com", "bio": "Product designer specializing in user research and interaction design."}, "department": "Design", "role": "Individual Contributor", "title": "Senior Product Designer", "isActive": true, "isAdmin": false, "hiredAt": "2022-01-10T00:00:00Z", "lastActiveAt": "2024-03-24T11:20:00Z"}'
+
+JSON.SET user:4 $ '{"username": "rbrown", "profile": {"firstName": "Robert", "lastName": "Brown", "displayName": "Rob Brown", "email": "robert.brown@company.com", "bio": "Former engineering lead, now focused on technical writing and documentation."}, "department": "Engineering", "role": "Individual Contributor", "title": "Staff Engineer", "isActive": false, "isAdmin": false, "hiredAt": "2018-09-20T00:00:00Z", "lastActiveAt": "2023-12-15T09:00:00Z"}'
+```
+
+
+
+
+### Waiting for Indexing
+
+Index updates are batched for performance, so newly added data may not appear in search results immediately.
+Use `SEARCH.WAITINDEXING` to ensure all pending updates are processed before querying:
+
+
+
+
+```ts
+await users.waitIndexing();
+```
+
+
+
+```bash
+SEARCH.WAITINDEXING users
+```
+
+
+
+
+### Autocomplete Search
+
+Use `$fuzzy` with `prefix: true` for search-as-you-type functionality. This approach handles
+both incomplete words and typos, providing a more forgiving autocomplete experience:
+
+
+
+
+```ts
+// As user types "ja" in the search box
+const suggestions = await users.query({
+ filter: {
+ "profile.displayName": {
+ $fuzzy: {
+ value: "ja",
+ prefix: true,
+ },
+ },
+ },
+ limit: 5,
+});
+// Matches "Jane Smith", "James Wilson", etc.
+
+// As user types "jn" (typo for "ja")
+const typoSuggestions = await users.query({
+ filter: {
+ "profile.displayName": {
+ $fuzzy: {
+ value: "jn",
+ prefix: true,
+ transpositionCostOne: true,
+ },
+ },
+ },
+ limit: 5,
+});
+// Still matches "Jane Smith", "James Wilson", etc.
+
+// As user types "jane smi"
+const refinedSuggestions = await users.query({
+ filter: {
+ "profile.displayName": "jane smi",
+ },
+ limit: 5,
+});
+// Smart matching applies fuzzy prefix to last word automatically
+// Matches "Jane Smith", "Jane Smithson", etc.
+```
+
+
+
+```bash
+# As user types "ja"
+SEARCH.QUERY users '{"profile.displayName": {"$fuzzy": {"value": "ja", "prefix": true}}}' LIMIT 5
+
+# As user types "jn" (typo)
+SEARCH.QUERY users '{"profile.displayName": {"$fuzzy": {"value": "jn", "prefix": true, "transpositionCostOne": true}}}' LIMIT 5
+
+# As user types "jane smi" (smart matching handles fuzzy prefix on last word)
+SEARCH.QUERY users '{"profile.displayName": "jane smi"}' LIMIT 5
+```
+
+
+
+
+### Fuzzy Name Search
+
+Handle typos and misspellings in name searches:
+
+
+
+
+```ts
+// User types "Micheal" (common misspelling of "Michael")
+const results = await users.query({
+ filter: {
+ "profile.firstName": {
+ $fuzzy: "Micheal",
+ },
+ },
+});
+// Matches "Michael Johnson"
+
+// Search with more tolerance for longer names
+const fuzzyResults = await users.query({
+ filter: {
+ "profile.lastName": {
+ $fuzzy: {
+ value: "Willaims", // Typo in "Williams"
+ distance: 2,
+ },
+ },
+ },
+});
+
+// Search across first and last name with fuzzy matching
+const combinedFuzzy = await users.query({
+ filter: {
+ $should: [
+ { "profile.firstName": { $fuzzy: "Srah" } }, // Typo
+ { "profile.lastName": { $fuzzy: "Srah" } },
+ ],
+ },
+});
+```
+
+
+
+```bash
+# Fuzzy first name search
+SEARCH.QUERY users '{"profile.firstName": {"$fuzzy": "Micheal"}}'
+
+# Fuzzy with higher distance tolerance
+SEARCH.QUERY users '{"profile.lastName": {"$fuzzy": {"value": "willaims", "distance": 2}}}'
+
+# Search both first and last name
+SEARCH.QUERY users '{"$should": [{"profile.firstName": {"$fuzzy": "srah"}}, {"profile.lastName": {"$fuzzy": "srah"}}]}'
+```
+
+
+
+
+### Exact Username/Email Lookup
+
+Find users by exact username or email:
+
+
+
+
+```ts
+// Exact username lookup
+const user = await users.query({
+ filter: {
+ username: "jsmith",
+ },
+});
+
+// Exact email lookup
+const userByEmail = await users.query({
+ filter: {
+ "profile.email": "jane.smith@company.com",
+ },
+});
+
+// Find users with email at specific domain
+const companyUsers = await users.query({
+ filter: {
+ "profile.email": {
+ $regex: "jane.*@company\\.com",
+ },
+ },
+});
+```
+
+
+
+```bash
+# Exact username lookup
+SEARCH.QUERY users '{"username": "jsmith"}'
+
+# Exact email lookup
+SEARCH.QUERY users '{"profile.email": "jane.smith@company.com"}'
+
+# Users with specific email domain
+SEARCH.QUERY users '{"profile.email": {"$regex": "jane.*@company\\.com"}}'
+```
+
+
+
+
+### Department and Role Filtering
+
+Filter users by department, role, or both:
+
+
+
+
+```ts
+// All users in Engineering
+const engineers = await users.query({
+ filter: {
+ department: "Engineering",
+ isActive: true,
+ },
+});
+
+// All managers across departments
+const managers = await users.query({
+ filter: {
+ role: "Manager",
+ isActive: true,
+ },
+});
+
+// Engineers who are managers
+const engineeringManagers = await users.query({
+ filter: {
+ $must: {
+ department: "Engineering",
+ role: "Manager",
+ isActive: true,
+ },
+ },
+});
+
+// Users in Engineering or Design
+const productTeam = await users.query({
+ filter: {
+ department: {
+ $in: ["Engineering", "Design", "Product"],
+ },
+ isActive: true,
+ },
+});
+```
+
+
+
+```bash
+# All users in Engineering
+SEARCH.QUERY users '{"department": "Engineering", "isActive": true}'
+
+# All managers
+SEARCH.QUERY users '{"role": "Manager", "isActive": true}'
+
+# Engineering managers
+SEARCH.QUERY users '{"$must": {"department": "Engineering", "role": "Manager", "isActive": true}}'
+
+# Users in multiple departments
+SEARCH.QUERY users '{"department": {"$in": ["Engineering", "Design", "Product"]}, "isActive": true}'
+```
+
+
+
+
+### Search by Skills in Bio
+
+Find users with specific skills or expertise:
+
+
+
+
+```ts
+// Find users who mention "distributed" in their bio
+const distributedExperts = await users.query({
+ filter: {
+ "profile.bio": "distributed",
+ isActive: true,
+ },
+});
+
+// Find users with multiple skills
+const backendEngineers = await users.query({
+ filter: {
+ $must: {
+ "profile.bio": "backend",
+ department: "Engineering",
+ },
+ },
+});
+
+// Search for phrase in bio
+const uxResearchers = await users.query({
+ filter: {
+ "profile.bio": {
+ $phrase: "user research",
+ },
+ },
+});
+```
+
+
+
+```bash
+# Find users mentioning "distributed" in bio
+SEARCH.QUERY users '{"profile.bio": "distributed", "isActive": true}'
+
+# Backend engineers
+SEARCH.QUERY users '{"$must": {"profile.bio": "backend", "department": "Engineering"}}'
+
+# Phrase search in bio
+SEARCH.QUERY users '{"profile.bio": {"$phrase": "user research"}}'
+```
+
+
+
+
+### Admin User Search
+
+Find administrators or users with specific permissions:
+
+
+
+
+```ts
+// All admin users
+const admins = await users.query({
+ filter: {
+ isAdmin: true,
+ isActive: true,
+ },
+});
+
+// Active non-admin users in Engineering
+const regularEngineers = await users.query({
+ filter: {
+ $must: {
+ department: "Engineering",
+ isActive: true,
+ },
+ $mustNot: {
+ isAdmin: true,
+ },
+ },
+});
+```
+
+
+
+```bash
+# All admin users
+SEARCH.QUERY users '{"isAdmin": true, "isActive": true}'
+
+# Active non-admin engineers
+SEARCH.QUERY users '{"$must": {"department": "Engineering", "isActive": true}, "$mustNot": {"isAdmin": true}}'
+```
+
+
+
+
+### Key Takeaways
+
+- Use `NOTOKENIZE` for usernames, emails, and exact-match identifiers
+- Use `NOSTEM` for proper nouns like names to prevent incorrect stemming
+- Use `$fuzzy` with `prefix: true` for search-as-you-type autocomplete with typo tolerance
+- Use `$fuzzy` to handle typos in name searches
+- Combine nested field paths (e.g., `profile.firstName`) for structured data
diff --git a/redis/search/schema-definition.mdx b/redis/search/schema-definition.mdx
new file mode 100644
index 00000000..e38ff0f0
--- /dev/null
+++ b/redis/search/schema-definition.mdx
@@ -0,0 +1,394 @@
+---
+title: Schema Definition
+---
+
+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
+
+```ts
+import { Redis, s } from "@upstash/redis";
+```
+
+### Basic Usage
+
+The schema builder provides methods for each field type:
+
+```ts
+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:
+
+```ts
+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:
+
+```ts
+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
+
+```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,
+});
+```
+
+### Schema Builder vs. Plain Objects
+
+You can define schemas using either the schema builder or plain objects:
+
+
+
+
+```ts
+import { Redis, s } from "@upstash/redis";
+
+const redis = Redis.fromEnv();
+
+const schema = s.object({
+ name: s.string(),
+ price: s.number("F64"),
+ category: s.string().noTokenize(),
+});
+```
+
+
+
+```ts
+const schema = {
+ name: "TEXT",
+ price: {
+ type: "F64",
+ fast: true,
+ },
+ category: {
+ type: "TEXT",
+ noTokenize: true,
+ },
+};
+```
+
+
+
+
+The schema builder provides:
+- Better type safety
+- Autocomplete support
+- More readable and maintainable code
+- Easier refactoring
+
+## Field Types
+
+| Type | Description | Example Values |
+|------|-------------|----------------|
+| `TEXT` | Full-text searchable string | `"hello world"`, `"The quick brown fox"` |
+| `U64` | Unsigned 64-bit integer | `0`, `42`, `18446744073709551615` |
+| `I64` | Signed 64-bit integer | `-100`, `0`, `9223372036854775807` |
+| `F64` | 64-bit floating point | `3.14`, `-0.001`, `1e10` |
+| `BOOL` | Boolean | `true`, `false` |
+| `DATE` | RFC 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.
+
+| Language | Example 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:
+
+| Option | Description | Use Case |
+|--------|-------------|----------|
+| `NOSTEM` | Disable word stemming | Names, proper nouns, technical terms |
+| `NOTOKENIZE` | Treat entire value as single token | URLs, UUIDs, email addresses, category codes |
+
+When using [`$regex`](./query-operators/field-operators/regex), be aware of stemming behavior:
+
+
+
+
+```ts
+// 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.*",
+ },
+ },
+});
+```
+
+
+
+```bash
+# With stemming enabled (default), "experiment" is stored as "experi"
+# This regex won't match:
+SEARCH.QUERY products '{"description": {"$regex": "experiment.*"}}'
+
+# This will match:
+SEARCH.QUERY articles '{"description": {"$regex": "experi.*"}}'
+```
+
+
+
+
+To avoid stemming issues, use `NOSTEM` on fields where you need exact regex matching
+
+#### Numeric, Boolean, and Date Field Options
+
+| Option | Description | Use Case |
+|--------|-------------|----------|
+| `FAST` | Enable fast field storage | Sorting, fast range queries, field retrieval |
+
+### Nested Fields
+
+You can define fields at arbitrary nesting levels using the `.` character as a separator.
+
+### 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 `AS` keyword to specify which document field the alias points to.
+
+
+
+
+```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({
+ // Index 'description' twice: once with stemming (default), once without
+ description: s.string(),
+ descriptionExact: s.string().noStem().from("description"),
+
+ // Create a short alias for a deeply nested field
+ authorName: s.string().from("metadata.author.displayName"),
+ }),
+});
+```
+
+
+
+```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 AS 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
+
+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**
+
+
+
+
+
+```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({
+ // 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(),
+ }),
+});
+```
+
+
+
+```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
+```
+
+
+
+
+**User directory schema**
+
+
+
+
+```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({
+ // 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(),
+ }),
+});
+```
+
+
+
+```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
+```
+
+
+