From 7b7364b60d0d76532debb6d4476b4154234e0220 Mon Sep 17 00:00:00 2001 From: Metin Dumandag <29387993+mdumandag@users.noreply.github.com> Date: Mon, 12 Jan 2026 16:54:00 +0300 Subject: [PATCH 01/12] WIP: Search docs --- docs.json | 52 ++ redis/search/counting.mdx | 41 ++ redis/search/getting-started.mdx | 112 ++++ redis/search/index-management.mdx | 331 +++++++++++ redis/search/introduction.mdx | 19 + .../boolean-operators/boost.mdx | 90 +++ .../boolean-operators/must-not.mdx | 71 +++ .../boolean-operators/must.mdx | 76 +++ .../boolean-operators/overview.mdx | 106 ++++ .../boolean-operators/should.mdx | 110 ++++ .../query-operators/field-operators/boost.mdx | 79 +++ .../field-operators/contains.mdx | 49 ++ .../query-operators/field-operators/eq.mdx | 76 +++ .../query-operators/field-operators/fuzzy.mdx | 117 ++++ .../query-operators/field-operators/in.mdx | 44 ++ .../query-operators/field-operators/ne.mdx | 46 ++ .../field-operators/overview.mdx | 6 + .../field-operators/phrase.mdx | 108 ++++ .../field-operators/range-operators.mdx | 72 +++ .../query-operators/field-operators/regex.mdx | 90 +++ .../field-operators/smart-matching.mdx | 69 +++ redis/search/query-operators/overview.mdx | 5 + redis/search/querying.mdx | 297 ++++++++++ redis/search/recipes/blog-search.mdx | 461 ++++++++++++++++ redis/search/recipes/e-commerce-search.mdx | 457 +++++++++++++++ redis/search/recipes/overview.mdx | 24 + redis/search/recipes/user-directory.mdx | 520 ++++++++++++++++++ redis/search/schema-definition.mdx | 219 ++++++++ 28 files changed, 3747 insertions(+) create mode 100644 redis/search/counting.mdx create mode 100644 redis/search/getting-started.mdx create mode 100644 redis/search/index-management.mdx create mode 100644 redis/search/introduction.mdx create mode 100644 redis/search/query-operators/boolean-operators/boost.mdx create mode 100644 redis/search/query-operators/boolean-operators/must-not.mdx create mode 100644 redis/search/query-operators/boolean-operators/must.mdx create mode 100644 redis/search/query-operators/boolean-operators/overview.mdx create mode 100644 redis/search/query-operators/boolean-operators/should.mdx create mode 100644 redis/search/query-operators/field-operators/boost.mdx create mode 100644 redis/search/query-operators/field-operators/contains.mdx create mode 100644 redis/search/query-operators/field-operators/eq.mdx create mode 100644 redis/search/query-operators/field-operators/fuzzy.mdx create mode 100644 redis/search/query-operators/field-operators/in.mdx create mode 100644 redis/search/query-operators/field-operators/ne.mdx create mode 100644 redis/search/query-operators/field-operators/overview.mdx create mode 100644 redis/search/query-operators/field-operators/phrase.mdx create mode 100644 redis/search/query-operators/field-operators/range-operators.mdx create mode 100644 redis/search/query-operators/field-operators/regex.mdx create mode 100644 redis/search/query-operators/field-operators/smart-matching.mdx create mode 100644 redis/search/query-operators/overview.mdx create mode 100644 redis/search/querying.mdx create mode 100644 redis/search/recipes/blog-search.mdx create mode 100644 redis/search/recipes/e-commerce-search.mdx create mode 100644 redis/search/recipes/overview.mdx create mode 100644 redis/search/recipes/user-directory.mdx create mode 100644 redis/search/schema-definition.mdx 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..e77d5f4e --- /dev/null +++ b/redis/search/getting-started.mdx @@ -0,0 +1,112 @@ +--- +title: Getting Started +--- + +This section demonstrates a complete workflow: creating an index, adding data, and searching. + + + + +```ts +import { Redis } 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: { + name: "TEXT", + description: "TEXT", + category: { + type: "TEXT", + noTokenize: true, + }, + price: { + type: "F64", + fast: true, + }, + inStock: "BOOL", + }, +}); + +// 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..ceda0e96 --- /dev/null +++ b/redis/search/index-management.mdx @@ -0,0 +1,331 @@ +--- +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 +// Basic index on JSON data +const users = await redis.search.createIndex({ + name: "users", + dataType: "json", + prefix: "user:", + schema: { + name: "TEXT", + email: "TEXT", + age: "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 +// String index with nested schema fields +const comments = await redis.search.createIndex({ + name: "comments", + dataType: "string", + prefix: "comment:", + schema: { + user: { + name: "TEXT", + email: { + type: "TEXT", + noTokenize: true, + }, + }, + comment: "TEXT", + upvotes: { + type: "U64", + fast: true, + }, + commentedAt: { + type: "DATE", + fast: true, + }, + }, +}); +``` + + + +```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 +// JSON index with multiple prefixes +const articles = await redis.search.createIndex({ + name: "articles", + dataType: "json", + prefix: ["article:", "blog:", "news:"], + schema: { + title: "TEXT", + body: "TEXT", + author: { + type: "TEXT", + noStem: true, + }, + publishedAt: { + type: "DATE", + fast: true, + }, + viewCount: { + type: "U64", + fast: true, + }, + }, +}); +``` + + + +```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 +// Turkish language index +const addresses = await redis.search.createIndex({ + name: "addresses", + dataType: "json", + prefix: "address:", + language: "turkish", + schema: { + address: { + type: "TEXT", + noStem: true, + }, + description: "TEXT", + }, +}); +``` + + + +```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. + +### 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..645cef07 --- /dev/null +++ b/redis/search/query-operators/field-operators/fuzzy.mdx @@ -0,0 +1,117 @@ +--- +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 2 edits (one deletion + one insertion). +Setting `transpositionCostOne: true` counts transpositions as a single edit instead. + +For example, searching for "haedphone" to find "headphone": +- Without `transpositionCostOne`: Distance is 2 (swap "ae" → "ea" costs 2) +- With `transpositionCostOne: true`: Distance is 1 (swap counts as 1) + +This is useful when users commonly transpose characters while typing quickly. + +### Stemming Behavior + +On fields with stemming enabled (the default), fuzzy matching operates on the stemmed form of terms. +For example, "running" is stored as "run", so searching for "runing" (one typo) would match. + +For predictable fuzzy matching, consider using `NOSTEM` on fields where you need to match the original word forms. +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 +// Simple fuzzy (distance 1, handles single typos) +// Matches "headphones" even with the typo "headphons" +await products.query({ + filter: { + name: { $fuzzy: "headphons" }, + }, +}); + +// Custom distance for more tolerance +// "hedphone" is 2 edits away from "headphone" +await products.query({ + filter: { + name: { + $fuzzy: { + value: "hedphone", + distance: 2, + }, + }, + }, +}); + +// 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, + }, + }, + }, +}); +``` + + + +```bash +# Simple fuzzy (distance 1, handles single typos) +SEARCH.QUERY products '{"name": {"$fuzzy": "headphons"}}' + +# Custom distance for more tolerance +SEARCH.QUERY products '{"name": {"$fuzzy": {"value": "hedphone", "distance": 2}}}' + +# Handle character transpositions efficiently +SEARCH.QUERY products '{"name": {"$fuzzy": {"value": "haedphone", "distance": 1, "transpositionCostOne": true}}}' +``` + + + \ No newline at end of file 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..73e07265 --- /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..241e38cf --- /dev/null +++ b/redis/search/query-operators/field-operators/smart-matching.mdx @@ -0,0 +1,69 @@ +--- +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 performs a straightforward term search. +The word is matched against individual tokens in the field. + +``` +{ name: "headphones" } +``` + +This finds all documents where the `name` field contains the token "headphones". +Due to stemming (enabled by default), this also matches variations like "headphone". + +### 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. **All terms match** (medium boost): Documents containing all the search terms, regardless of + position or order, receive a moderate score boost. + +3. **Fuzzy matching** (lower boost): Documents containing terms similar to the search terms + (accounting for typos) are included with a lower score. + +``` +{ description: "wireless headphones" } +``` + +This query returns results in roughly this order: +- "Premium **wireless headphones** with noise cancellation" (exact phrase) +- "**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..1c6c4268 --- /dev/null +++ b/redis/search/querying.mdx @@ -0,0 +1,297 @@ +--- +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"}' SORTBY price ASC + +# Sort by date, newest first +SEARCH.QUERY articles '{"author": "john"}' SORTBY publishedAt DESC + +# Sort by rating, highest first, which can be combined with LIMIT and OFFSET +SEARCH.QUERY products '{"inStock": true}' SORTBY 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"}' RETURN 2 name price +``` + + + + +#### 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. diff --git a/redis/search/recipes/blog-search.mdx b/redis/search/recipes/blog-search.mdx new file mode 100644 index 00000000..f042a139 --- /dev/null +++ b/redis/search/recipes/blog-search.mdx @@ -0,0 +1,461 @@ +--- +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 +const articles = await redis.search.createIndex({ + name: "articles", + dataType: "hash", + prefix: "article:", + schema: { + // Full-text searchable content + title: "TEXT", + body: "TEXT", + summary: "TEXT", + + // Author name without stemming + author: { + type: "TEXT", + noStem: true, + }, + + // Date fields for filtering and sorting + publishedAt: { + type: "DATE", + fast: true, + }, + updatedAt: { + type: "DATE", + fast: true, + }, + + // Status for draft/published filtering + published: "BOOL", + + // View count for popularity sorting + viewCount: { + type: "U64", + fast: true, + }, + }, +}); +``` + + + +```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}' SORTBY 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"}}' SORTBY 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}' SORTBY viewCount DESC LIMIT 10 + +# Popular articles about a topic +SEARCH.QUERY articles '{"$must": {"body": "redis", "published": true}}' SORTBY 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..8af7e3bc --- /dev/null +++ b/redis/search/recipes/e-commerce-search.mdx @@ -0,0 +1,457 @@ +--- +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 +const products = await redis.search.createIndex({ + name: "products", + dataType: "json", + prefix: "product:", + schema: { + // Full-text searchable fields + name: "TEXT", + description: "TEXT", + brand: { + type: "TEXT", + noStem: true, // "Nike" shouldn't stem to "Nik" + }, + + // Exact-match category for filtering + category: { + type: "TEXT", + noTokenize: true, // "Electronics > Audio" as single token + }, + + // Numeric fields for filtering and sorting + price: { + type: "F64", + fast: true, // Enable sorting by price + }, + rating: { + type: "F64", + fast: true, // Enable sorting by rating + }, + reviewCount: "U64", + + // Boolean for stock filtering + inStock: "BOOL", + + // Date for "new arrivals" queries + createdAt: { + type: "DATE", + fast: true, + }, + }, +}); +``` + + + +```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"}' SORTBY rating DESC LIMIT 20 + +# Page 2 +SEARCH.QUERY products '{"category": "Electronics > Audio > Headphones"}' SORTBY rating DESC LIMIT 20 OFFSET 20 + +# Sort by price, cheapest first +SEARCH.QUERY products '{"name": "headphones", "inStock": true}' SORTBY 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}' SORTBY 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..5f948796 --- /dev/null +++ b/redis/search/recipes/user-directory.mdx @@ -0,0 +1,520 @@ +--- +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 +const users = await redis.search.createIndex({ + name: "users", + dataType: "json", + prefix: "user:", + schema: { + // Exact username matching (no tokenization) + username: { + type: "TEXT", + noTokenize: true, + }, + + // Nested profile fields + profile: { + // Name search without stemming (proper nouns) + firstName: { + type: "TEXT", + noStem: true, + }, + lastName: { + type: "TEXT", + noStem: true, + }, + displayName: { + type: "TEXT", + noStem: true, + }, + + // Email as exact match (contains special characters) + email: { + type: "TEXT", + noTokenize: true, + }, + + // Searchable bio/about text + bio: "TEXT", + }, + + // Department and role for filtering + department: { + type: "TEXT", + noTokenize: true, + }, + role: { + type: "TEXT", + noTokenize: true, + }, + title: "TEXT", + + // Boolean flags + isActive: "BOOL", + isAdmin: "BOOL", + + // Dates for filtering + hiredAt: { + type: "DATE", + fast: true, + }, + lastActiveAt: { + type: "DATE", + fast: true, + }, + }, +}); +``` + + + +```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 `$contains` for search-as-you-type functionality: + + + + +```ts +// As user types "ja" in the search box +const suggestions = await users.query({ + filter: { + "profile.displayName": { + $contains: "ja", + }, + }, + limit: 5, +}); +// Matches "Jane Smith", "James Wilson", etc. + +// As user types "jane sm" +const refinedSuggestions = await users.query({ + filter: { + "profile.displayName": { + $contains: "jane sm", + }, + }, + limit: 5, +}); +// Matches "Jane Smith", "Jane Smithson", etc. +``` + + + +```bash +# As user types "ja" +SEARCH.QUERY users '{"profile.displayName": {"$contains": "ja"}}' LIMIT 5 + +# As user types "jane sm" +SEARCH.QUERY users '{"profile.displayName": {"$contains": "jane sm"}}' 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 `$contains` for search-as-you-type autocomplete functionality +- 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..4e4f17d4 --- /dev/null +++ b/redis/search/schema-definition.mdx @@ -0,0 +1,219 @@ +--- +title: Schema Definition +--- + +Every index requires a schema that defines the structure of searchable documents. +The schema enforces type safety and enables query optimization. + +| 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 [`$fuzzy`](./query-operators/field-operators/fuzzy) or [`$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/fuzzy 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. + +### 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 +const products = await redis.search.createIndex({ + name: "products", + dataType: "hash", + prefix: "product:", + schema: { + // Searchable product name with stemming + name: "TEXT", + + // Exact-match SKU codes + sku: { + type: "TEXT", + noTokenize: true, + }, + + // Brand names without stemming + brand: { + type: "TEXT", + noStem: true, + }, + + // Full-text description + description: "TEXT", + + // Sortable price + price: { + type: "F64", + fast: true, + }, + + // Sortable rating + rating: { + type: "F64", + fast: true, + }, + + // Non-sortable review count + reviewCount: "U64", + + // Filterable stock status + inStock: "BOOL", + }, +}); +``` + + + +```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 +const users = await redis.search.createIndex({ + name: "users", + dataType: "json", + prefix: "user:", + schema: { + // Exact username matches + username: { + type: "TEXT", + noTokenize: true, + }, + + // Nested schema fields + profile: { + // Name search without stemming + displayName: { + type: "TEXT", + noStem: true, + }, + + // Full-text bio search + bio: "TEXT", + + // Exact email matches + email: { + type: "TEXT", + noTokenize: true, + }, + }, + + // Join date for sorting + createdAt: { + type: "DATE", + fast: true, + }, + + // Filter by verification status + verified: "BOOL", + }, +}); +``` + + + +```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 +``` + + + From 6b10472619cbaef675fcc123180e85ca4ca1fe2e Mon Sep 17 00:00:00 2001 From: CahidArda Date: Mon, 12 Jan 2026 19:47:29 +0300 Subject: [PATCH 02/12] fix: use s utility for schema and add redis.search.index --- redis/search/getting-started.mdx | 22 +-- redis/search/index-management.mdx | 137 ++++++++----- redis/search/recipes/blog-search.mdx | 36 ++-- redis/search/recipes/e-commerce-search.mdx | 41 ++-- redis/search/recipes/user-directory.mdx | 65 ++----- redis/search/schema-definition.mdx | 211 ++++++++++++++++----- 6 files changed, 316 insertions(+), 196 deletions(-) diff --git a/redis/search/getting-started.mdx b/redis/search/getting-started.mdx index e77d5f4e..b0e2354d 100644 --- a/redis/search/getting-started.mdx +++ b/redis/search/getting-started.mdx @@ -8,7 +8,7 @@ This section demonstrates a complete workflow: creating an index, adding data, a ```ts -import { Redis } from "@upstash/redis"; +import { Redis, s } from "@upstash/redis"; const redis = Redis.fromEnv(); @@ -17,19 +17,13 @@ const index = await redis.search.createIndex({ name: "products", dataType: "json", prefix: "product:", - schema: { - name: "TEXT", - description: "TEXT", - category: { - type: "TEXT", - noTokenize: true, - }, - price: { - type: "F64", - fast: true, - }, - inStock: "BOOL", - }, + 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) diff --git a/redis/search/index-management.mdx b/redis/search/index-management.mdx index ceda0e96..5dea7886 100644 --- a/redis/search/index-management.mdx +++ b/redis/search/index-management.mdx @@ -14,16 +14,20 @@ 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: { - name: "TEXT", - email: "TEXT", - age: "U64", - }, + schema: s.object({ + name: s.string(), + email: s.string(), + age: s.number("U64"), + }), }); ``` @@ -49,29 +53,23 @@ 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: { - user: { - name: "TEXT", - email: { - type: "TEXT", - noTokenize: true, - }, - }, - comment: "TEXT", - upvotes: { - type: "U64", - fast: true, - }, - commentedAt: { - type: "DATE", - fast: true, - }, - }, + schema: s.object({ + user: s.object({ + name: s.string(), + email: s.string().noTokenize(), + }), + comment: s.string(), + upvotes: s.number("U64"), + commentedAt: s.date().fast(), + }), }); ``` @@ -96,27 +94,21 @@ multiple prefixes: ```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: { - title: "TEXT", - body: "TEXT", - author: { - type: "TEXT", - noStem: true, - }, - publishedAt: { - type: "DATE", - fast: true, - }, - viewCount: { - type: "U64", - fast: true, - }, - }, + schema: s.object({ + title: s.string(), + body: s.string(), + author: s.string().noStem(), + publishedAt: s.date().fast(), + viewCount: s.number("U64"), + }), }); ``` @@ -183,19 +175,20 @@ Currently, the following languages are supported: ```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: { - address: { - type: "TEXT", - noStem: true, - }, - description: "TEXT", - }, + schema: s.object({ + address: s.string().noStem(), + description: s.string(), + }), }); ``` @@ -232,6 +225,58 @@ 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. diff --git a/redis/search/recipes/blog-search.mdx b/redis/search/recipes/blog-search.mdx index f042a139..889f6f88 100644 --- a/redis/search/recipes/blog-search.mdx +++ b/redis/search/recipes/blog-search.mdx @@ -13,41 +13,33 @@ 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: { + schema: s.object({ // Full-text searchable content - title: "TEXT", - body: "TEXT", - summary: "TEXT", + title: s.string(), + body: s.string(), + summary: s.string(), // Author name without stemming - author: { - type: "TEXT", - noStem: true, - }, + author: s.string().noStem(), // Date fields for filtering and sorting - publishedAt: { - type: "DATE", - fast: true, - }, - updatedAt: { - type: "DATE", - fast: true, - }, + publishedAt: s.date().fast(), + updatedAt: s.date().fast(), // Status for draft/published filtering - published: "BOOL", + published: s.boolean(), // View count for popularity sorting - viewCount: { - type: "U64", - fast: true, - }, - }, + viewCount: s.number("U64"), + }), }); ``` diff --git a/redis/search/recipes/e-commerce-search.mdx b/redis/search/recipes/e-commerce-search.mdx index 8af7e3bc..30cba23e 100644 --- a/redis/search/recipes/e-commerce-search.mdx +++ b/redis/search/recipes/e-commerce-search.mdx @@ -13,45 +13,34 @@ 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: { + schema: s.object({ // Full-text searchable fields - name: "TEXT", - description: "TEXT", - brand: { - type: "TEXT", - noStem: true, // "Nike" shouldn't stem to "Nik" - }, + name: s.string(), + description: s.string(), + brand: s.string().noStem(), // "Nike" shouldn't stem to "Nik" // Exact-match category for filtering - category: { - type: "TEXT", - noTokenize: true, // "Electronics > Audio" as single token - }, + category: s.string().noTokenize(), // "Electronics > Audio" as single token // Numeric fields for filtering and sorting - price: { - type: "F64", - fast: true, // Enable sorting by price - }, - rating: { - type: "F64", - fast: true, // Enable sorting by rating - }, - reviewCount: "U64", + 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: "BOOL", + inStock: s.boolean(), // Date for "new arrivals" queries - createdAt: { - type: "DATE", - fast: true, - }, - }, + createdAt: s.date().fast(), + }), }); ``` diff --git a/redis/search/recipes/user-directory.mdx b/redis/search/recipes/user-directory.mdx index 5f948796..50d1822a 100644 --- a/redis/search/recipes/user-directory.mdx +++ b/redis/search/recipes/user-directory.mdx @@ -13,68 +13,45 @@ The schema uses nested fields for profile data and exact matching for identifier ```ts +import { Redis, s } from "@upstash/redis"; + +const redis = Redis.fromEnv(); + const users = await redis.search.createIndex({ name: "users", dataType: "json", prefix: "user:", - schema: { + schema: s.object({ // Exact username matching (no tokenization) - username: { - type: "TEXT", - noTokenize: true, - }, + username: s.string().noTokenize(), // Nested profile fields - profile: { + profile: s.object({ // Name search without stemming (proper nouns) - firstName: { - type: "TEXT", - noStem: true, - }, - lastName: { - type: "TEXT", - noStem: true, - }, - displayName: { - type: "TEXT", - noStem: true, - }, + firstName: s.string().noStem(), + lastName: s.string().noStem(), + displayName: s.string().noStem(), // Email as exact match (contains special characters) - email: { - type: "TEXT", - noTokenize: true, - }, + email: s.string().noTokenize(), // Searchable bio/about text - bio: "TEXT", - }, + bio: s.string(), + }), // Department and role for filtering - department: { - type: "TEXT", - noTokenize: true, - }, - role: { - type: "TEXT", - noTokenize: true, - }, - title: "TEXT", + department: s.string().noTokenize(), + role: s.string().noTokenize(), + title: s.string(), // Boolean flags - isActive: "BOOL", - isAdmin: "BOOL", + isActive: s.boolean(), + isAdmin: s.boolean(), // Dates for filtering - hiredAt: { - type: "DATE", - fast: true, - }, - lastActiveAt: { - type: "DATE", - fast: true, - }, - }, + hiredAt: s.date().fast(), + lastActiveAt: s.date().fast(), + }), }); ``` diff --git a/redis/search/schema-definition.mdx b/redis/search/schema-definition.mdx index 4e4f17d4..d24aae56 100644 --- a/redis/search/schema-definition.mdx +++ b/redis/search/schema-definition.mdx @@ -5,6 +5,145 @@ 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"` | @@ -109,47 +248,39 @@ matches for the missing fields. ```ts +import { Redis, s } from "@upstash/redis"; + +const redis = Redis.fromEnv(); + const products = await redis.search.createIndex({ name: "products", dataType: "hash", prefix: "product:", - schema: { + schema: s.object({ // Searchable product name with stemming - name: "TEXT", + name: s.string(), // Exact-match SKU codes - sku: { - type: "TEXT", - noTokenize: true, - }, + sku: s.string().noTokenize(), // Brand names without stemming - brand: { - type: "TEXT", - noStem: true, - }, + brand: s.string().noStem(), // Full-text description - description: "TEXT", + description: s.string(), // Sortable price - price: { - type: "F64", - fast: true, - }, + price: s.number("F64"), // Sortable rating - rating: { - type: "F64", - fast: true, - }, + rating: s.number("F64"), // Non-sortable review count - reviewCount: "U64", + reviewCount: s.number("U64"), // Filterable stock status - inStock: "BOOL", - }, + inStock: s.boolean(), + }), }); ``` @@ -168,44 +299,36 @@ SEARCH.CREATE products ON HASH PREFIX 1 product: SCHEMA name TEXT sku TEXT NOTOK ```ts +import { Redis, s } from "@upstash/redis"; + +const redis = Redis.fromEnv(); + const users = await redis.search.createIndex({ name: "users", dataType: "json", prefix: "user:", - schema: { + schema: s.object({ // Exact username matches - username: { - type: "TEXT", - noTokenize: true, - }, + username: s.string().noTokenize(), // Nested schema fields - profile: { + profile: s.object({ // Name search without stemming - displayName: { - type: "TEXT", - noStem: true, - }, + displayName: s.string().noStem(), // Full-text bio search - bio: "TEXT", + bio: s.string(), // Exact email matches - email: { - type: "TEXT", - noTokenize: true, - }, - }, + email: s.string().noTokenize(), + }), // Join date for sorting - createdAt: { - type: "DATE", - fast: true, - }, + createdAt: s.date().fast(), // Filter by verification status - verified: "BOOL", - }, + verified: s.boolean(), + }), }); ``` From d42f5d26b20fedfd999ecf50056cff417bf3a939 Mon Sep 17 00:00:00 2001 From: Metin Dumandag <29387993+mdumandag@users.noreply.github.com> Date: Tue, 13 Jan 2026 10:14:30 +0300 Subject: [PATCH 03/12] fix operator link --- redis/search/query-operators/field-operators/ne.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redis/search/query-operators/field-operators/ne.mdx b/redis/search/query-operators/field-operators/ne.mdx index 73e07265..33ff5293 100644 --- a/redis/search/query-operators/field-operators/ne.mdx +++ b/redis/search/query-operators/field-operators/ne.mdx @@ -8,7 +8,7 @@ This operator works as a filter, removing matching documents from the result set 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, +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 From a4b4877ffb293f2d6b780dcdd2b47d471f173f58 Mon Sep 17 00:00:00 2001 From: Metin Dumandag <29387993+mdumandag@users.noreply.github.com> Date: Wed, 14 Jan 2026 15:15:31 +0300 Subject: [PATCH 04/12] add docs for the fuzzy with prefix --- .../query-operators/field-operators/fuzzy.mdx | 47 +++++++++++++++++++ .../field-operators/smart-matching.mdx | 35 ++++++++++---- redis/search/recipes/user-directory.mdx | 39 +++++++++++---- 3 files changed, 103 insertions(+), 18 deletions(-) diff --git a/redis/search/query-operators/field-operators/fuzzy.mdx b/redis/search/query-operators/field-operators/fuzzy.mdx index 645cef07..2949cf73 100644 --- a/redis/search/query-operators/field-operators/fuzzy.mdx +++ b/redis/search/query-operators/field-operators/fuzzy.mdx @@ -41,6 +41,20 @@ For example, searching for "haedphone" to find "headphone": 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. + ### Stemming Behavior On fields with stemming enabled (the default), fuzzy matching operates on the stemmed form of terms. @@ -98,6 +112,33 @@ await products.query({ }, }, }); + +// Fuzzy prefix for autocomplete with typo tolerance +// Matches "headphones", "headphone", and handles typos in incomplete words +await products.query({ + filter: { + name: { + $fuzzy: { + value: "headpho", + prefix: 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, + transpositionCostOne: true, + }, + }, + }, +}); ``` @@ -111,6 +152,12 @@ SEARCH.QUERY products '{"name": {"$fuzzy": {"value": "hedphone", "distance": 2}} # Handle character transpositions efficiently SEARCH.QUERY products '{"name": {"$fuzzy": {"value": "haedphone", "distance": 1, "transpositionCostOne": true}}}' + +# Fuzzy prefix for autocomplete with typo tolerance +SEARCH.QUERY products '{"name": {"$fuzzy": {"value": "headpho", "prefix": true}}}' + +# Combine prefix with transposition for robust autocomplete +SEARCH.QUERY products '{"name": {"$fuzzy": {"value": "haedpho", "prefix": true, "transpositionCostOne": true}}}' ``` diff --git a/redis/search/query-operators/field-operators/smart-matching.mdx b/redis/search/query-operators/field-operators/smart-matching.mdx index 241e38cf..dce42544 100644 --- a/redis/search/query-operators/field-operators/smart-matching.mdx +++ b/redis/search/query-operators/field-operators/smart-matching.mdx @@ -8,15 +8,24 @@ This behavior varies based on the input format. ### Single-Word Values -For single-word searches, the engine performs a straightforward term search. -The word is matched against individual tokens in the field. +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: "headphones" } +{ name: "tabletop" } ``` -This finds all documents where the `name` field contains the token "headphones". -Due to stemming (enabled by default), this also matches variations like "headphone". +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 @@ -27,18 +36,26 @@ their scores to surface the most relevant results: score highest. Searching for `wireless headphones` ranks documents containing "wireless headphones" as a phrase above those with the words scattered. -2. **All terms match** (medium boost): Documents containing all the search terms, regardless of +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** (lower boost): Documents containing terms similar to the search terms +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 headphones" } +{ description: "wireless headphon" } ``` This query returns results in roughly this order: -- "Premium **wireless headphones** with noise cancellation" (exact phrase) +- "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) diff --git a/redis/search/recipes/user-directory.mdx b/redis/search/recipes/user-directory.mdx index 50d1822a..a4fdffe2 100644 --- a/redis/search/recipes/user-directory.mdx +++ b/redis/search/recipes/user-directory.mdx @@ -181,7 +181,8 @@ SEARCH.WAITINDEXING users ### Autocomplete Search -Use `$contains` for search-as-you-type functionality: +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: @@ -191,22 +192,39 @@ Use `$contains` for search-as-you-type functionality: const suggestions = await users.query({ filter: { "profile.displayName": { - $contains: "ja", + $fuzzy: { + value: "ja", + prefix: true, + }, }, }, limit: 5, }); // Matches "Jane Smith", "James Wilson", etc. -// As user types "jane sm" -const refinedSuggestions = await users.query({ +// As user types "jn" (typo for "ja") +const typoSuggestions = await users.query({ filter: { "profile.displayName": { - $contains: "jane sm", + $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. ``` @@ -214,10 +232,13 @@ const refinedSuggestions = await users.query({ ```bash # As user types "ja" -SEARCH.QUERY users '{"profile.displayName": {"$contains": "ja"}}' LIMIT 5 +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 sm" -SEARCH.QUERY users '{"profile.displayName": {"$contains": "jane sm"}}' LIMIT 5 +# As user types "jane smi" (smart matching handles fuzzy prefix on last word) +SEARCH.QUERY users '{"profile.displayName": "jane smi"}' LIMIT 5 ``` @@ -492,6 +513,6 @@ SEARCH.QUERY users '{"$must": {"department": "Engineering", "isActive": true}, " - Use `NOTOKENIZE` for usernames, emails, and exact-match identifiers - Use `NOSTEM` for proper nouns like names to prevent incorrect stemming -- Use `$contains` for search-as-you-type autocomplete functionality +- 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 From 51982fd7458c67a36bd8df6a1f8430f313f3961b Mon Sep 17 00:00:00 2001 From: Metin Dumandag <29387993+mdumandag@users.noreply.github.com> Date: Wed, 14 Jan 2026 16:39:20 +0300 Subject: [PATCH 05/12] make transposition cost one true by default for fuzzy --- .../query-operators/field-operators/fuzzy.mdx | 38 ++++++------------- 1 file changed, 11 insertions(+), 27 deletions(-) diff --git a/redis/search/query-operators/field-operators/fuzzy.mdx b/redis/search/query-operators/field-operators/fuzzy.mdx index 2949cf73..7c765dd8 100644 --- a/redis/search/query-operators/field-operators/fuzzy.mdx +++ b/redis/search/query-operators/field-operators/fuzzy.mdx @@ -32,12 +32,12 @@ Higher distances match more terms but may include unintended matches, so use dis ### Transposition Cost A transposition swaps two adjacent characters (e.g., "teh" → "the"). -By default, a transposition counts as 2 edits (one deletion + one insertion). -Setting `transpositionCostOne: true` counts transpositions as a single edit instead. +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": -- Without `transpositionCostOne`: Distance is 2 (swap "ae" → "ea" costs 2) - 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. @@ -79,21 +79,22 @@ See [Text Field Options](../../schema-definition#text-field-options) for more de ```ts // Simple fuzzy (distance 1, handles single typos) -// Matches "headphones" even with the typo "headphons" +// Matches "headphone" even with the typo "headphon" await products.query({ filter: { - name: { $fuzzy: "headphons" }, + name: { $fuzzy: "headphon" }, }, }); // Custom distance for more tolerance -// "hedphone" is 2 edits away from "headphone" +// "haedphone" is 2 edits away from "headphone" without taking transposition into account await products.query({ filter: { name: { $fuzzy: { - value: "hedphone", + value: "haedphone", distance: 2, + transpositionCostOne: false, }, }, }, @@ -113,19 +114,6 @@ await products.query({ }, }); -// Fuzzy prefix for autocomplete with typo tolerance -// Matches "headphones", "headphone", and handles typos in incomplete words -await products.query({ - filter: { - name: { - $fuzzy: { - value: "headpho", - prefix: true, - }, - }, - }, -}); - // Combine prefix with transposition for robust autocomplete // Handles both typos and incomplete input like "haedpho" → "headphones" await products.query({ @@ -134,7 +122,6 @@ await products.query({ $fuzzy: { value: "haedpho", prefix: true, - transpositionCostOne: true, }, }, }, @@ -145,20 +132,17 @@ await products.query({ ```bash # Simple fuzzy (distance 1, handles single typos) -SEARCH.QUERY products '{"name": {"$fuzzy": "headphons"}}' +SEARCH.QUERY products '{"name": {"$fuzzy": "headphon"}}' # Custom distance for more tolerance -SEARCH.QUERY products '{"name": {"$fuzzy": {"value": "hedphone", "distance": 2}}}' +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}}}' -# Fuzzy prefix for autocomplete with typo tolerance -SEARCH.QUERY products '{"name": {"$fuzzy": {"value": "headpho", "prefix": true}}}' - # Combine prefix with transposition for robust autocomplete SEARCH.QUERY products '{"name": {"$fuzzy": {"value": "haedpho", "prefix": true, "transpositionCostOne": true}}}' ``` - \ No newline at end of file + From 4f57ecd9f6142a38eb6275826e6d5a81de48f20a Mon Sep 17 00:00:00 2001 From: Metin Dumandag <29387993+mdumandag@users.noreply.github.com> Date: Wed, 14 Jan 2026 16:46:59 +0300 Subject: [PATCH 06/12] talk about multiple words support for fuzzy operator --- .../query-operators/field-operators/fuzzy.mdx | 21 ++++++++++++++----- redis/search/schema-definition.mdx | 5 ++--- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/redis/search/query-operators/field-operators/fuzzy.mdx b/redis/search/query-operators/field-operators/fuzzy.mdx index 7c765dd8..b8612eb1 100644 --- a/redis/search/query-operators/field-operators/fuzzy.mdx +++ b/redis/search/query-operators/field-operators/fuzzy.mdx @@ -55,13 +55,13 @@ For example, searching for "headpho" with `prefix: true`: This is particularly useful for search-as-you-type autocomplete where users may have typos in partially typed words. -### Stemming Behavior +### Multiple Words -On fields with stemming enabled (the default), fuzzy matching operates on the stemmed form of terms. -For example, "running" is stored as "run", so searching for "runing" (one typo) would match. +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 predictable fuzzy matching, consider using `NOSTEM` on fields where you need to match the original word forms. -See [Text Field Options](../../schema-definition#text-field-options) for more details. +For example, searching for "wireles headphons" will match documents containing both "wireless" and "headphones", even with the typos. ### Compatibility @@ -126,6 +126,14 @@ await products.query({ }, }, }); + +// 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" }, + }, +}); ``` @@ -142,6 +150,9 @@ SEARCH.QUERY products '{"name": {"$fuzzy": {"value": "haedphone", "distance": 1, # 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/schema-definition.mdx b/redis/search/schema-definition.mdx index d24aae56..aac8f678 100644 --- a/redis/search/schema-definition.mdx +++ b/redis/search/schema-definition.mdx @@ -179,8 +179,7 @@ It is possible to configure this behavior using the following options: | `NOSTEM` | Disable word stemming | Names, proper nouns, technical terms | | `NOTOKENIZE` | Treat entire value as single token | URLs, UUIDs, email addresses, category codes | -When using [`$fuzzy`](./query-operators/field-operators/fuzzy) or [`$regex`](./query-operators/field-operators/regex), -be aware of stemming behavior: +When using [`$regex`](./query-operators/field-operators/regex), be aware of stemming behavior: @@ -220,7 +219,7 @@ SEARCH.QUERY articles '{"description": {"$regex": "experi.*"}}' -To avoid stemming issues, use `NOSTEM` on fields where you need exact regex/fuzzy matching +To avoid stemming issues, use `NOSTEM` on fields where you need exact regex matching #### Numeric, Boolean, and Date Field Options From cbf83e89f58e319ea8043de0a76f00e7c84bb4cb Mon Sep 17 00:00:00 2001 From: Metin Dumandag <29387993+mdumandag@users.noreply.github.com> Date: Mon, 19 Jan 2026 11:58:44 +0300 Subject: [PATCH 07/12] add support for aliased fields --- redis/search/querying.mdx | 12 +++++++ redis/search/schema-definition.mdx | 53 ++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/redis/search/querying.mdx b/redis/search/querying.mdx index 1c6c4268..d04936f4 100644 --- a/redis/search/querying.mdx +++ b/redis/search/querying.mdx @@ -247,6 +247,12 @@ SEARCH.QUERY products '{"name": "headphones"}' RETURN 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. @@ -295,3 +301,9 @@ SEARCH.QUERY products '{"description": "wireless"}' HIGHLIGHT FIELDS 1 descripti 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/schema-definition.mdx b/redis/search/schema-definition.mdx index aac8f678..78f65c47 100644 --- a/redis/search/schema-definition.mdx +++ b/redis/search/schema-definition.mdx @@ -231,6 +231,59 @@ To avoid stemming issues, use `NOSTEM` on fields where you need exact regex matc 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().as("description"), + + // Create a short alias for a deeply nested field + authorName: s.string().as("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 NOSTEM AS description 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 From c3483be83e7be3a88e01bf45e27cc53e56844ba5 Mon Sep 17 00:00:00 2001 From: Metin Dumandag <29387993+mdumandag@users.noreply.github.com> Date: Tue, 20 Jan 2026 16:43:42 +0300 Subject: [PATCH 08/12] make sdk and the protocol similar for parameter names --- redis/search/querying.mdx | 8 ++++---- redis/search/recipes/blog-search.mdx | 8 ++++---- redis/search/recipes/e-commerce-search.mdx | 8 ++++---- redis/search/schema-definition.mdx | 6 +++--- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/redis/search/querying.mdx b/redis/search/querying.mdx index d04936f4..ed2a088f 100644 --- a/redis/search/querying.mdx +++ b/redis/search/querying.mdx @@ -174,13 +174,13 @@ await products.query({ ```bash # Sort by price, cheapest first -SEARCH.QUERY products '{"category": "electronics"}' SORTBY price ASC +SEARCH.QUERY products '{"category": "electronics"}' ORDERBY price ASC # Sort by date, newest first -SEARCH.QUERY articles '{"author": "john"}' SORTBY publishedAt DESC +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}' SORTBY rating DESC LIMIT 5 +SEARCH.QUERY products '{"inStock": true}' ORDERBY rating DESC LIMIT 5 ``` @@ -241,7 +241,7 @@ await products.query({ ```bash # Return specific fields only -SEARCH.QUERY products '{"name": "headphones"}' RETURN 2 name price +SEARCH.QUERY products '{"name": "headphones"}' SELECT 2 name price ``` diff --git a/redis/search/recipes/blog-search.mdx b/redis/search/recipes/blog-search.mdx index 889f6f88..7b343dcd 100644 --- a/redis/search/recipes/blog-search.mdx +++ b/redis/search/recipes/blog-search.mdx @@ -325,7 +325,7 @@ const authorSearch = await articles.query({ ```bash # All articles by Jane Smith -SEARCH.QUERY articles '{"author": "Jane Smith", "published": true}' SORTBY publishedAt DESC +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"}}' @@ -360,7 +360,7 @@ const marchArticles = await articles.query({ ```bash # Articles from a specific month -SEARCH.QUERY articles '{"publishedAt": {"$gte": "2026-01-01T00:00:00Z", "$lt": "2026-02-01T00:00:00Z"}}' SORTBY publishedAt DESC +SEARCH.QUERY articles '{"publishedAt": {"$gte": "2026-01-01T00:00:00Z", "$lt": "2026-02-01T00:00:00Z"}}' ORDERBY publishedAt DESC ``` @@ -404,10 +404,10 @@ const popularRedis = await articles.query({ ```bash # Most popular articles -SEARCH.QUERY articles '{"published": true}' SORTBY viewCount DESC LIMIT 10 +SEARCH.QUERY articles '{"published": true}' ORDERBY viewCount DESC LIMIT 10 # Popular articles about a topic -SEARCH.QUERY articles '{"$must": {"body": "redis", "published": true}}' SORTBY viewCount DESC LIMIT 5 +SEARCH.QUERY articles '{"$must": {"body": "redis", "published": true}}' ORDERBY viewCount DESC LIMIT 5 ``` diff --git a/redis/search/recipes/e-commerce-search.mdx b/redis/search/recipes/e-commerce-search.mdx index 30cba23e..d627af33 100644 --- a/redis/search/recipes/e-commerce-search.mdx +++ b/redis/search/recipes/e-commerce-search.mdx @@ -302,13 +302,13 @@ const cheapest = await products.query({ ```bash # Page 1: Top-rated headphones -SEARCH.QUERY products '{"category": "Electronics > Audio > Headphones"}' SORTBY rating DESC LIMIT 20 +SEARCH.QUERY products '{"category": "Electronics > Audio > Headphones"}' ORDERBY rating DESC LIMIT 20 # Page 2 -SEARCH.QUERY products '{"category": "Electronics > Audio > Headphones"}' SORTBY rating DESC LIMIT 20 OFFSET 20 +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}' SORTBY price ASC LIMIT 10 +SEARCH.QUERY products '{"name": "headphones", "inStock": true}' ORDERBY price ASC LIMIT 10 ``` @@ -339,7 +339,7 @@ const newArrivals = await products.query({ ```bash # Products added after a specific date -SEARCH.QUERY products '{"createdAt": {"$gte": "2026-01-01T00:00:00Z"}, "inStock": true}' SORTBY createdAt DESC LIMIT 10 +SEARCH.QUERY products '{"createdAt": {"$gte": "2026-01-01T00:00:00Z"}, "inStock": true}' ORDERBY createdAt DESC LIMIT 10 ``` diff --git a/redis/search/schema-definition.mdx b/redis/search/schema-definition.mdx index 78f65c47..e38ff0f0 100644 --- a/redis/search/schema-definition.mdx +++ b/redis/search/schema-definition.mdx @@ -252,10 +252,10 @@ const products = await redis.search.createIndex({ schema: s.object({ // Index 'description' twice: once with stemming (default), once without description: s.string(), - descriptionExact: s.string().noStem().as("description"), + descriptionExact: s.string().noStem().from("description"), // Create a short alias for a deeply nested field - authorName: s.string().as("metadata.author.displayName"), + authorName: s.string().from("metadata.author.displayName"), }), }); ``` @@ -265,7 +265,7 @@ const products = await redis.search.createIndex({ ```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 NOSTEM AS description authorName TEXT AS metadata.author.displayName +SEARCH.CREATE products ON JSON PREFIX 1 product: SCHEMA description TEXT descriptionExact TEXT FROM description NOSTEM authorName TEXT AS metadata.author.displayName ``` From 27ea466cbb99e41ab03ce9790ae8c92bd91dc082 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mustafa=20Taha=20S=C3=B6ylemez?= <97186995+mtahasylmz@users.noreply.github.com> Date: Thu, 22 Jan 2026 16:27:01 +0300 Subject: [PATCH 09/12] cloud-4061 (#611) --- redis/search/index-management.mdx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/redis/search/index-management.mdx b/redis/search/index-management.mdx index 5dea7886..8219428a 100644 --- a/redis/search/index-management.mdx +++ b/redis/search/index-management.mdx @@ -238,7 +238,7 @@ import { Redis, s } from "@upstash/redis"; const redis = Redis.fromEnv(); // Get a client for an existing index -const users = redis.search.index("users"); +const users = redis.search.index({ name: "users" }); // Query the index const results = await users.query({ @@ -246,13 +246,16 @@ const results = await users.query({ }); // With schema for type safety -const schema = s.object({ +const userSchema = s.object({ name: s.string(), email: s.string(), age: s.number("U64"), }); -const typedUsers = redis.search.index("users", schema); +// Note: The schema parameter provides TypeScript type safety +// for queries and results. It does not validate against the +// server-side index schema. +const typedUsers = redis.search.index({ name: "users", schema: userSchema }); // Now queries are type-safe const typedResults = await typedUsers.query({ From 8d4b2ac9c70fb4c9c1a19a2f408abaf05107200e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mustafa=20Taha=20S=C3=B6ylemez?= <97186995+mtahasylmz@users.noreply.github.com> Date: Fri, 23 Jan 2026 09:31:52 +0300 Subject: [PATCH 10/12] replace as keyword with from (#612) --- redis/search/schema-definition.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/redis/search/schema-definition.mdx b/redis/search/schema-definition.mdx index e38ff0f0..df38502e 100644 --- a/redis/search/schema-definition.mdx +++ b/redis/search/schema-definition.mdx @@ -235,7 +235,7 @@ You can define fields at arbitrary nesting levels using the `.` character as a s 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. +Use the `FROM` keyword to specify which document field the alias points to. @@ -265,7 +265,7 @@ const products = await redis.search.createIndex({ ```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 +SEARCH.CREATE products ON JSON PREFIX 1 product: SCHEMA description TEXT descriptionExact TEXT FROM description NOSTEM authorName TEXT FROM metadata.author.displayName ``` From d80bada1cd54a8aa671b25cb035fe64ea03a0149 Mon Sep 17 00:00:00 2001 From: Metin Dumandag <29387993+mdumandag@users.noreply.github.com> Date: Fri, 23 Jan 2026 09:38:37 +0300 Subject: [PATCH 11/12] replace sortby with orderby --- redis/search/querying.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redis/search/querying.mdx b/redis/search/querying.mdx index ed2a088f..635fbb4a 100644 --- a/redis/search/querying.mdx +++ b/redis/search/querying.mdx @@ -132,7 +132,7 @@ 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. +When using `ORDERBY`, the score in results reflects the sort field's value rather than relevance. From 886cec2af2307e2328bbd9f9ae54c17948cef508 Mon Sep 17 00:00:00 2001 From: josh Date: Tue, 27 Jan 2026 15:34:30 +0100 Subject: [PATCH 12/12] chore: refactor docs --- docs.json | 1 - img/redis-search/redis-search-cover.png | Bin 0 -> 240231 bytes redis/search/getting-started.mdx | 89 ++++--- redis/search/index-management.mdx | 66 +++-- redis/search/introduction.mdx | 20 +- redis/search/query-operators/overview.mdx | 5 - redis/search/querying.mdx | 136 ++++------ redis/search/schema-definition.mdx | 291 +++++++--------------- 8 files changed, 226 insertions(+), 382 deletions(-) create mode 100644 img/redis-search/redis-search-cover.png delete mode 100644 redis/search/query-operators/overview.mdx diff --git a/docs.json b/docs.json index 7bd84527..e7a3c5fd 100644 --- a/docs.json +++ b/docs.json @@ -710,7 +710,6 @@ { "group": "Query Operators", "pages": [ - "redis/search/query-operators/overview", { "group": "Boolean Operators", "pages": [ diff --git a/img/redis-search/redis-search-cover.png b/img/redis-search/redis-search-cover.png new file mode 100644 index 0000000000000000000000000000000000000000..1b182ec23689a3a6a96a07359b85988c4b169821 GIT binary patch literal 240231 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%0DMhH!4t^f9BOb_#I2TI8C^j-g+5`n7rz1> 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�ob7%%gKGm8YpY!ULhxt(71BnxbqVL+PL0h%*N< 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 zⅇp$?rb3H1Sg{;R_byHL6HWV?U2#jF>c4Y2h%dDlSYSM>6GL;?KVd9WJ5jI`#Oc z>M@-fC4=L1nUJS(nx;&xzp%I{)Lk~R1D#j|Wz|Bdnd+&{k+F&EVV!dv#Slga@*hrS zAh&;m!i6zvFU}(;b%AD_v}$_S0ybZA$r>pvBGRPp-zcg(0AZHX;N^ka_nN0GK;;Dr zO?l)3eI__vuI}9|FRAgQc%RpNcY7{zfr{4Ay=NE8PK>PFE*+UYig@&gWo8~q0V;dt{j}@wtMcwW5UE{ z``t)E;^|c%b0UTgoM%&A6d_fZb)I=z=gT06I{$c{a)9WWS1!L4g3z;>g39g9e)cdI z0ckbG;CLEmwe{9H1Z=-ir+Ta|mU@Hi+0~A3UdX-0gY%!=n*~=OzMN!J?A=#+QiB5} z)^Zmr|@ug2y+7Yq&_YCQ7n}CL8}xVFU7%+ZdJGr z)pQG=kWg^~L5pET3FtWZU{nr4K=w=9kb=@|JFS^^zlb3VSA%d$HIX+9=_th&{<8wm zS+-O?$gVb>x}MPM(6{~k_ng4Jf=iJzMITWsk!az^^VR9Ru|%cH`H}^MJRsZ~zds5* zvH8|!7MyE8xE(~j`OBfG9es4!z$hB3R8sc%CZ7LwE?(iqePoLhFu%UPs~IYJl|wDH zdRn6tb+5~^t1Dcm8QBg8Jk`>*Jx%2pI#xIR70qCLHy?;@ac#JVwREvn^4hdO?M#@? z@2AhqkVQATxnu$1f*UD3#!fur$Ma#zALXruY%6mGLhbl1d{;;kc29AEjcwP`V{9-t zv1so{03WBXo)S=gEJ<%@O!N>_^S--63lt;OLJnwLn1H<_;+5GZu{o*BtaXE3)zG<62aACttt$QM3C0f~9IU?hi zj#TxbpxwKzThSI~`S>U-I82`4$Mow10l%eqL!R5~Au@mTgXear>HU;{#nM)!8c}q` z;-)xC64@aCgzy~=v z;hrzo*BbfNQzdmadRhjNMVkHLZbvJow?oSk46#?eeCJCNBk_jtTPUAtr4k+dcz3`r zqUuYI#psfGgQFbPd~KuTd|F@zP7IvN+`>Y1mBD21&oAnFnCp&QZvhd{Fpb2FFEwPi zI%!Zp(SO0ArN|%!>vnE$E&PtCRJ4_yXxosDm7Dr~jxF*29#!}?Wa*3AOW6;EIKaVP zHlSn^&q{}E_B%)Q6Ep3tpA)1a9vAxd%JoE{^NBmM=wir6C;&LtF&Afs0S59jWEGyHyolmXD-3*TiKSZ?Ni$X1#c-gabQ=2Pq={89>%2ZH=DI#dKc7{E56q# zk1#em<;f-p|3ujFN);j0NXVZOv1p9YRdybeB=`O5Kx{dpYK3Q_#w&t=jT-@8?-b3R z)_VZ40TPU|!AOPAfzNYHuEZ&BmPD9=og-6`JkzrlUgJ3TDV>d~<9t=_iNl)Ue~8{~ zu?gyWd1$awkZtEU@_`wE8|3m#0KeDmh*Lmba|g`!!8H@$j2He}?|NRp+HUHLw+kH~ zITn$yW+?m7pKMJh-m*ZBZB>8L%r#6mIk{hgu`W#@!KLmSnsaa$fQ!g!W{ zVUjsqI0FO2EijXXWju0b;VC`)>%4!(&9w&=!G%0SJo9SO?n#B{p2l$`gVvzJ!?@n z9a1iHpH5v4=TrAC=G5VPH&@azlUoR7&M0{w%`6}|hmU0TD+p=jA_bgQ`RssQO#Ri0uSHrtM$J3_Lp_ z5dyYaija3L@YISLGzGm_l12uS{hZ0LK_$+9(r;6Ua_Jfm3r>A=@LGss?7>;gZKmdV z@xtvc6c@-xCy)-nhYx_+R=95mqJ_7Zl}HMFikVZgeS_^l-avNAtK>H1=D>Q8A;(Sjh`G4 zWhiJFJ4x%^#ewvW3zwhD%K{X6oywup%v@@VE8vVf_W%rCB}=0ho-EF2Vq6ZQL?yD0 zq^mTTmot=*(*gi=V;Cuf+EE+0n%@6N7LvH+&+8XOje2iJ-j4QVj(lbWZYA#AB!UVlE#yB*;6`pg%FUY_ zd9WfQ{mC&5T0S5CHZh!t6)6YlE$R_eAi+jBgc3yv5~=}0&z)Tpj_EMM4Dgr{sI*=E!<5kx+%t9w#2ft+k7q|R2BxnU&a&M ztAZUh@){hPv6U9sN7QXMOELJoE7E4Aa|R(;8T6TN>-ObQFWH?Y;|W;o=mdfZFn=;+ z4v?8^tL+r`qn0FK7cwCTpth68%Gp>tAUT9|3IAah=4GKieE@0$2%DB2ycw@65H+*~ z!jANfximl9bINE^r(j~f+O3NTIy4()4oFPc+c7iV)tr1L~h1{CO z6h$;oy2XjGXrGO>`NcIYMHr!_>yt&x{_=t-*@-H@z*oo|{u+4KvH7VyAm>P^MK5_o zo}vzjP!v`HT9ndM-dNC$&?C?qQzA?B75U*v0zsI?3l(ttix4USfQEkpG7OXMZQ3}P zAFRV>*-004@qWS%niBbOa$0j=g{RdMTl%A97$c_tDZ_2&F7IpFRc}<%U4qX>wwSpO z%MF&fl1Dq2$PQ@08cFy4sbWrE^1k6M1*$K&l(QI+lP%G`WR-L?q^aB#zFiM?l_aR6 z*?)k!L^W8`NAX4kXAlMiBFifF1-{q64y>=@^SFc9~Fj4Tu?M zi7dY5(N=JKXtwiUthRFrWi;p zNy^Cj#M;d;=JqRvhO?Be;HddIAFmT@T4Pzf6RE7DtFe?Lr168uiT-=*SB8I}jKx)R ztS}1q-=h~=f6SZP=vwoYS=@;w>sT)?v7z6l)GTDK8K6|S8)G2lr2T*>36}U2O}j=d zca^$_(M>~gSQ8*nY_(mFzegxVgIKVW`w2N->wtA=L%oq@Jl`92)eI7Idw=Rr_2;#; zt5nOrefWM&PSMYOD%IA#!5le<7o=2e1908a2)s)q%ME z(f^8vt&iX1&I@7ma&55w;ne>$zBvz$cNo>+TOc6Dj-Qaay_Nf@R4XT!HShF1++1gv zj%#P4@?y8>w1F)NQfvFp;O1%y@25U|wCgORm`>A~jSq4EpPT%4Lw)}F1dZ`njp|HC z}NWyVeueFa&%P*Zim7nKZZ6kROaLa z-awAFG*%(6ke_=xgW})yjLXTw9h}e=f9gUnr8OOPEcCJgkZaij(ZV-g)RGU9O!ak4 zcUw0FoR})2p8oM@>@RlPRcda!L!Z&q`}oMz`MJiIrA-7cnVGTZ5kkPA9R5Rnl|mR5 zs6R1~Z}ZO+;B59A+J;2la;#KmvDFCd3;fG3_<&QqNeC}k4EXI>ai7t-CK4RdC~0k@ zxnT^69qvhwL!wU}x~C*?%4G`hw99;1WRnG_yy zwTuKf^XCCG?Rhors<}b${wNG>ia*mSx71Cs&L;Pn{=r=)Smt43%XruqNPo{OhQG-# z!PrE}lluwzwfoM+gJtksY^cEAVrvyx7WA_HpNuZXIi`oE=duQFfA+yXm*LhAyd30D z`D!(7Ljp~9tq#{m9b*S@=Fjte+{>?&mO{*Wc7%WY^5d+=1TI1?Jt|pT>H|6-rP%JN^~s zpdJ*#`t_&buAJh{rG#{yzt8=OlY+4CqD=^RwVqa3a4%WtY>ao+#U}ZGLV(=D@fCg8 z{PjYwrV_OYJZ0RIHkH8_FX}6P;&OX{_0N5%f0(OBsN;6xyN|G}Mt6pr=H}5-E!*;o z!H71+47srD&nzkdFjjqq0LBfReWy*ZS)jS?nP<^PxxjegLXdQxxb+7Gp&#F0zmJ1$ zO@n_TqN%O@!%h8^vmPUVBkXBBqPWm@7iIGYaE)SRWr^@6qlMAomZjX#V&CW>FS&AZ z8<(ERg=1d&Wy_sS=MKQRHGtvQ! z)6C*A>^1h4kC=@C{kBJsiO=uR?I>BGTU7C>4Ia6qzzU|P;PJ;i+Jfg^_>iXMZ$r{j z{uFn)1Pj2f&X`7=;BFV&_lcG5^eb?D{xTtuhrPy@k1|8YKkCYZSGruiNA59?tdOm? zmx2wl&J4j+z2Y2q+wQ2?shzPqM~u71?3cgs`(s>fl}<-R@c2=ta7Xu;a*usy=%L`w z3bunnle&XvdsAMiPmKa|sHkhG=^Hne+o;z+ZZ&4xZ52F2h9Bo%8#y?x9h$(Z=?-S? z1?xaa3(8NK@>`q(xsPc(I9aMg+B(C7?1r1~1>OC>rZ6h&7+ZA>%B;?NMr9vN@zu%qewjm)a|U{C$7^k3_K zaA}KL-$;j+JYtNM2ufx;_ZPUtd|~?U^GyDo&AEAEkmebV4jwjkO3KOQMD11M+^o69 zaE;xYn@h|*_{`N)KLx1vT5q#co15GcawzEzgYV=&Qo-9WDpa+K9S{au?JU|QbJgSK z%Sgdf=2;to0n@dT&5^Kni0az9CB)6zLl#+9>t_>)FFKBgN`PBWi2ee6FI z!#@?c=BVrUky3~2ER9k`@*LT1HZ@!_#xg;NH(uAZY7u*9xn_WKbdq-^Kg_$M>gcHB z@83d zTh)C!gA80sgePu^-M*|m5bF8 zQWpCXrD&XtZIY5uNs@w;3L6@I!COc|_RKZ7iSG2+j!{rAo5d0m5c8-y6LAx5mfIf~ zHLuG=BK8IgUFQJnOJ$0LUY|;y{w%$u+qQVBg^qXkG)`F20M;_E)n#1GtQDX4BgCU+PQ6DaLOoamYdr6o5!8|Dm#yg%G255P3 z6f=u}y}8(-DNf=!phGpyJy2~`_&kN(e?p^ss^QgvI+m&_+E+vY;{ZPFij0-FfpV8!^YtA?(FG@bfCUMzvdC_BgX$Sm~x=R}19rC5K@|F2!nG5BP<#*=o0E!|6vA7}p2P zN}q;I%*N(cIVw_>i{7E!XZb;P%M|*K=^PB#hFA2}=!hN$v#f=Por}!h zEz*1nl)6A$vwienloAwrH2vanT_i_P#+hgtr|sA?@Lhl%0-;+>1HyXE?0d;WKi1W1 z($#`+2fm!&>g*uYywzwq0~rGJUhM3OVIn)?-PM@svXIkcy+?*us7ckUY9x9Bq>-HI z0YO~Zp#7By#pC#3UTJ_zjSCYY?+YU~NST$F*xU`2jkOPPM>_!S{t>sT=NRe>qNuS` zSSmFeHC}qju~0_w>hfDNWRyCtDF}Phxu$2eHrw zTwWbJ1xHMU=yhLbCrybj=)|9g#2G1*jUxNo@5W9|KNer@N*Kf%7_7l`AUgdHBn1`iD*SZGvh~9x!&T~oDD(<%=iN>_!i1oZ zqWA)&hA=-5MLDG=b-yfRvpL4GUR&$OU*I%H>jI=I;H0ag>Q?1zPv@VSltX{M9;6Wb zEG}+hZ(5qm}+jw6$gL&45-P;MC`ncw0nvCNM~siO&?*nOTwsoDKywSVO_*(moM>)rqB zSunIY+50>91_JkZ0LqZ0chqNl6C0QRwl@*8&OsGFuI*t*$5g6uZf&9X=x)8b0Q|$&xSty*WT>V82^@Y01b#Db!_QN~ ztKKyq@c}ohK72G~J<@Mpc6?#zv`?z-rQn4e7N@J8yntYZw^8%^|J2Cg+Z~Gf6^RDe ze!H|X-@QD^AbxQ-QWK&VC<*A?fG5$W`avA~6B2=iQg6@mu2taHS;l>ZE{gL}_xTaE ztWWMpPT|i!2R|M|h`r%O9x{LE2l6LTp`K~QX9mhf4wcL4<5@=8zQwclIQ`F;U3H<_2B$+7G`?oq7(Df+l61n?u-FkOH`m!u6@o z*)RJj!?7~|%murlk&t28-})%qU>j*{qvxj>MyJ+~*tC$$w{Fcn;3BQb#N zRJ8l;rMCOoG#V%=RJ>r8G0@N8hm%L(B6X=k4mW3iURpH1B%sxP?NkG=@KgM&_yi+t zc!C5Op#<;rySzW8arp^+;69sw?eH}ZxUNwCctZ(`1UF2sz_=KsQ?oq8lQ$i&QSD8| zrYFw!@;~fe#XC3T5j-4*YWZB9np8*m=XLwQZFW?@6b;Waotx>?`P1 zTt|WxuG}ARsHM#*W#JmX-$l#vN9{Iyepzke{2q zTmZw_-pBZ=p04u&q5#Y_7kG1cvbV1>3Bq;L9Tf(vp0)ec91VIJ|H@EQ|0uEggA+o_ zeLZ>Z`G0J{Tj9D-DdNe2D*v#j;V!Qg_p0)r1eBA52Asmh2TjzI$(>ul3Hfp0Bj39h zL10e`b$kEkVE5opdqww+kSF&!?wihcJySFt&w29Q z=H2gcbZxset8~zoWYP2z`^893z+*=yW!z4t)w0BK7Fe-5gw5NU4`mMl`gN-F@gr9N z(2JB`9JCeatTRKymj+^}>(NZ0J*%g?Gn#klMcyudelT=}D{ejpeSC>PYU7i6HMuVb z_pcVVWdi>At35hB_ZfZ4U((GxPi4hK7d+O)auYAibn*1BNrol!K9u{W(^Vl(eJkn^ zB$0cgH`!t7$@;&acV5j*ZM3p-RFLcp#HBhw!6XS8xM;T)%$(f6X3*$L`($A3_#+q1 zV8PSVOdraVG%lT6yZ7w9q-U)0wP#R9jMTx2NzARHopLABzA!T5*uZ55)F+MZRk2yA z9}Uq5_7Xg`3^$1 z&=6EkOv0H@&hxWVO2CO*Ha6mcLaVH%bY7o(of{PIAlx|;IA2QC$UMBT4E1M;rWZ|F z>qhGoAWp2EG*Tb#Nzb6AWq0p8DmyA&)9bIxH5l*fU345T6XGZn2RJh{cOGq$IXw^9 zm}lV~`zT!JT3P&klc}3Recg`72Z8edco@MXSJ2dml*#@6!44^h6F%o-(Z7Gwdm!x{ z?)|914KM;v+2hL1ySlDVCz1vdenj91#krN1f%e*9E0ovvm!SXv=I0wY%&}m~_r$JG z;AOH^1Fv+4$X+M#N{=TFR&$yIUiarKnhk$D7z{z@PM$XtdGA4US1#Zb*uoBgXnsn zGZWWdxRo2&yh$&uuWcyTB_0{gSfGy+4Rqq)hs`1h)*Y^K(TF+okSf=UOZE5^eogsu z*|3Hg&gKF8*-dt$GT=*3`Y?nIERB%(Y|eEEj9j0P)U+!vOZl24xZGjL3@eO6QW5>9 z^V%#WLL0|3YXWv)ksX#Zj;FEZYpCrC71ZQg?0^GIaH1E2pLyTIxIDd=5Z#;!) zorE5_Ex|)a?j>F4Qh<6uSA8yHz(+;~2qJ$n9qY8eF<*}YFJ@nLHaek^kmdFDajvzr zHOEWADhm8e4;8?`h;k;TV#cK6g~AWGWz{LPZg7X(aYnPbPgqLMMw`B3XDl1=Yx-p^ zhCh1Q=QCpz$>5tKh`a+R;nHGGD|OQ!f+jYG1MqF|7Qva7Sy%d^WQLP=uEFq2cWBYO zx7b5!zvry|f`}Zj5#|@NC8W2Wj)TN^wAN5~8_}K% zEepq$X7yx6mp)qxVPkIaIi+lr72`h+ZldKXpRREZu=tSmMIC}CdGE!!ZTNTG3`>_A z%dU3x!JAi8zjl7eRs$iWLH3X8RajAgaPB1tXYI4n3+4t-5Nn{rvB&~|v7lE=XBC*7 zsHNFPcqiI~wgT0*<90Vr=ndluMrwThRXi`CeGN$1v)14(AXRjO&xzdM%R>&1GzK57 ze?%g`_AdhY#@DY3%D*s|jbc5Q^W<9Zipl!rR_1)0-@~TntAU~PKKrUU*#V-$`d!W8 zci0H4FI!z_X!h;`ob=9j`8XFU~6LWw_7IMjHDHoRpPjcWh3wkVFp z3)ulsIbN+TTZJGF1dbos^R{M_Bo4O79K%FGuZs(gGFoq#4LSUF(nR}Zg5~KCTSvew zS*~yit0Ltp!S8)}E(_q7I$$=Oy7Mqg0-}GX&K{tQ?GKbPwoIruNaylv_;2k=X)~OI z4;UmLYT|jbrUw}k(30jUie6@6MpMC2(EBFwoBWI8OLGF-C7{@AGUix`qLI?o@DAuU z@Lt3&5_`jSL7t9eEbS=moLTw!uSvfj1-i@MFZ0wG?Kj|7zrTHDPIak7v8fM%KcGS@tY%&tP%8uW z!jZ_0f#e`gkliG2efe+H;g3vk6Tl9K2-@EP-8NuOFITvNK~hLL8oYDVvmWR}oI`d( z`i|mia459xW8knKIQ6SrqrKW*7aJj7R8*fwk`GRqoC4AP>q8b5H*C7>3GNwbF1V9`9 z9!0W2#w$FrEhv7}`@nO+RGd3set4~c)wi4`W?EET1kR)x-pm{D){qT?;+xvQPsn?SAo0do1O|mTF1@^PyAeiQj33&zU?8pQbD}1_pjzFl!$6?q?$t zTC#B+1){&mANaEEM?wIooMi)xmk`0fg$-Z;I7oIecz~+Ol$!x|z z!bPP7-I4qQNI)KIz60g1HdX13-THXfCg~8yn1=M)sT&31f}LdLIO$|b^r-CBzW_~< zK2(|Ix&YMsfs6KA%X^jb9kEQ7OrYxIjRHn@62avmI_|R=6c2l1j(ffS$W?orle_bb zcF-G*&_CtNVxx=_^K;E2fq%Uz8(9e^)6Y(EVul6drPKElvMttx(&*4dY)00s4FgMF zM)Ur9Y{76hnYux6=DymO$C6%iJmBdV9OR0vMu9iF!p1Kn82hZfuhbg*Dk1PR(@JOc z+MM%LLaczO*fJNAIOq;~mlDKTbv%RSE@SjO{}~tfvZ|(aIWm`w&gX2yH*$lVmBbKJ zWrk^N-JfOLOHA&3zH>I7rrX!Xg}kQl56NQuk+j<)<&;=Spf=8)5TjK8IUMS)vvHOZ zAF|TTB(}E=;O^o646!I+K|6Oso66Wm8c9G_QjnaAF9|V+4DF zIJWyGiWbd5l5k8~hkAqk3!g=-i*rt>c7Z}L+aeO@N~i!}@h}-vJN+UkAByJmPZ#CrdEe!!jLdFzAVS#!MPj5o{3Km>4ZZ~tJqdCJjW*vnX zpebLhh@n*D6^Et=;5xeo!PEq%5z86g4u65Nxbg$C^k@T&(F03X1%5OJAnFT_j-LHS zMMhH%S^aH$1r5dqkTkx(0$jCANQGdW-C1(J2g#X!imFsY`NHVEG%nvrsZf$siCh=& z;z*P1F*YzmEQ=t(Ys3;-OQHOn>M^IXUgoGraPxa((~IVU8p|;Wr^*;S1(e!C(*C9g z(rBOi)pwo_e3zZrTI2c2V1U)BG*87Woh1jA9`~H*5kihgF|SET*~)Y8Rmp#)@QO{d z9X-vUdhRu1G}aJS9Rv)7#s}y11d_3?wr;F}plzK^uDt%q@ZruGf5#9UDfM3q!JhAB zPIgURCs-F_TMF1$#F>ZS%DbLeHf|$z8 zbc8sC2j2!Wq}|gb81BXw*K@~{Ko<`d3?w&K5joeK5F@Yv!MmxyyaB2TaIWd$8vIqh zb7-(=2Gj)R5uSa`Nn%K#AK={^j?$!7u{!H=-+p1d&s7*4{oF*L9)g*ov5JE4cxO?y z=D|?oK--`aMa5kZ5|Q3PU=iZ<69yQpte#s+pGL#LL~e|Vau=p_?+ww%D*-{>giQqh zQnK{SU7oKy(_Jes|CwO;?~=BJk!vB?nJEN>E%HE#rYK37tgqv4V3cAc83$0FtMDUM zP#%y@S`lG)l5e5#^yA>}h{%1wIb;{_yoF9SF+-pXE0R5;fPPzMII5o4<8oT1&!>&q z26j~3d%p&_Zo>l{oWGrTM*iv6Ck$zF?%~1N&ju=j7s{@^Xl{wgZb<9^Z{mJ|nNMF- z8vTt-sc|E4mBr=Xj}kU208H8zk(3~){(d$#u&%^!Lp6VwFu)D>nE)a(JjFx@@SeB) zOsu6umC-k&b%~ZE{Gmt&*(LN4!jyyLh)4`cJzVLa@G<$XMRQsy2R`(Z7A`29&@wPk z`sIzY_zzhIX2=u!f1-(Cf#$s z!?q+Hc%LW zhpr?($(+4ZNlt-+8K&-pXzu9;5c83xdiA<^62an5BLOHqtzRdk@V7P|UY7^1? zW1!7tMK;;Vq$Lxpk+0Uec$y;3wc-mC*tK5X;|>2A6kd>O@ezv#zu58<@d`IQxyQ^e z?&9@VeW~&>ntm&Mpr}Yem7GfSAwsXlQgCz^Udo@M&w^X)_4l{Zz71T*w@-m+b8%4& z+`pHf>@G>axiBSPi_P95Wy~6IXqr`ixbZ$wHe@q_^wvJNwPH)b4@#{1&ctXGe;Q9-b%9&a8aFNDAqF0~XZqz$qlf1>{Ri&C3MO4~7zo zM?K}&)Ekw$_nk=y6o51I?~_c4Bv6m6cwf?!vwOJvrZ0ia{*Nng7jq)4Zga--aw}`Nac% zS5rLaCONYP8^}b&y#&|?dU6$ilsgF?3)E){Z_EO6Dwp}CI%3>S0F)9uIL#0rYj9QJ z=C4w<=hyr-%r`nXb|d#RxSP6T+`*9mhX&;)G`qFxPeTb`^{H!gicbpG#q*5z2ikg8 z)R_TO$g|KHR~?4;ktut&h?fM|*Txyns&7XE#!i4G7axb|feyp|BH(G_z_3-}*l&GB zn+K$7i(DL^*aD}IKH)h*X$5Av+>%WDkx;=Yg3Cd0`h+>z@iIu~{V#CCu>+NB+Kh5P zXS9MHRwE~DO-fG5dE&MlP-+pWa3WQ3`nYg#XwNTHiQ2e_JuQ@Pc2PdTbH*uS?cR;+ z$SMv>O~8b9<<`gdAh%sJoYO|y17;leU?*j8AJTNjR*)(Dq9#(f<;M2~RU88VT~;%^)7-N?dDfHSTmykB^rG5p!O)*?3FfTrpwyq$D1b5cz;#MzJAi ztO0?gO?Yqnd1LOtQQP65yUZJBA9Sm9uhJoluoR#5NRfi`on*vWJ1FCy|3b$ zvv_UX&*S}~(cvY9cZSU!DX@YLpPF?A$YS)pVWvA;&0F6GxIe9E)QboTQHh({Q$&;l z7vmj5f42xROc(8!8J8ai+<~rfJen5rEr;@{-j*5MexfhyxNS@8Cf#{V6=Juc_;L}z zd3cW+$W>Kuy02>gzS-*4M$tLB<4X+;&Sa)jACcvC;;LT8vbo4|UH1YAtk%Gme{;)+ zjaE)IV1?v+Iz*V)<%Bi2_tFD?(7yw{UL#>moN)o=g25P1bsTM zA!rnEd&A#p4ipX~fNFfI8!>jwoC@3&+Usw+W7)|H#Ut>+b?YF2{!s$L=428kb)G-zpsy0`z&+mS`KBFedzYCm=P|& zWxz83fP88>A1HPlm7qaX`~X{G%t7g3!reDALHbTs5V|X_GCCc8R>$4|EOki8EBM z>SE^;BUHtLm>J)ZYHSOw4E6}Z%(rw`T;R32jSgcF9s4Tl3-g{rHFf_$4c~WMCEy-o z&FX+-$Rwc~-4UV#kIus-GS1{bE~EVld3mf!G0JiMjV7fV5n0?n(01OOH87Z8-2YG? zq=fmGbSx_|?H%n6VR>VNmZpaWqK=5Vm+BA$suk=aaY%OU*4=! zMRFI2@g1{g;o!B0wHxi;sdMvn`}WZNyNp`Kw(^mh`$ei~qaYL>GIGgK`%HGF&g(mm zYH2D#+G@5JALPFLfJ9Q>%3-R1xVngKgYP}(R@U2XuTVT$631n|XrtWFPOvsZs6U+a z8c7G-i%dM-Qah`enajIG8_LU0;V&;0=~itMkA%=C?HbmLldKx@^)HS^&_Un#5X}#= zoOl|h_V){MylUEGSyL{r-`ToOScrfb{+V?O?IEuf6V8ahGps9(9Dj74Xgs7fJ&tSj z(Z=U|J9lG)I+i#1RXjtO?a@zkIqMW7$Hj+hjheL8_TH7TWQjDkf1h-5vzcxZ)!VvK zR;!&b9752<0p%7gW0G!?{JhVPEYQhq;uum+zC@FwvOZEn-|oUT8lj+yG5@O9VW|ME zK|6+RAcf3ISN~s)^%*p3O5>)BmO!AijT6p}IbGCjr7E0c?8FHo$O;xavJYPA7qZ{* z?dFhz8yLr;XCMu~+V#y}OO_PnAiICn^Je5rk(dZgY${oC=-N*QYw)<$VvbEk;NYn` zdICN~apundFDL{Zd!jlaPjJpp$NF#prJs();Sop0eWu~x(7!}Iv5t0nB(lDM*jh`m z4yx6}2o9cjK>Tp6?-lV{!ogzIgW~`_%u`+RLR4rxadsW~L^X%xp**Gk7;i4PKY&%} zV&*TRq2d=#^jE`HO^`*8zWz(#V=EI8G4Vj1So1yBz;|6n*Z(5k?{{FKWi7-V1t2+q z#Q;%pU}Wx*Gc%ST*HodNyg;HLfF!LczppQnIzJ8N18DN+hez?`q`x^QX>0rhCV*(W*^97v5W?DUy6yz&AvkDU zcN#?`JP?MjIXG<~>ME=L9qa*zir)Ug$&g18W^P+1M6-c(7ZM}w!YpU-iR11D90Y(+ z9Z44T+XTFWI5=Vp(#0Uqy@*kz`0^xV6Gk?(?G~B573Wxqv>|%Brl>>vT$CmqBHBxO zflLgPL&Re=z$No1VY)s25>ZZ@gIFP-R*$<(H&>pD105xRkjjvgrdr1t#`06F(!w6- z&_R-dn?Dj^On^m0lYV<^;esvbv|&aI90TtKUi-ZW79C{ zL9x2{J16}FvC;tb{hA@ z+a}BFIrd3?f+KZz2}zTXx~W7}gPV81K8I%Z_JAs_ChW9hSg)6+j1XDO1D7 zgzh4|rgy;z47xOV7MU0Jz{SNEb8EdR@XSk}$(bou6P!cB$-AaQYKPRxQpQ$}PaO

vSLe_Jwoa~~y!Wjm+E4D9>*jMKv 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 L9a}Rt(?b6T$TKak literal 0 HcmV?d00001 diff --git a/redis/search/getting-started.mdx b/redis/search/getting-started.mdx index b0e2354d..853ae0c7 100644 --- a/redis/search/getting-started.mdx +++ b/redis/search/getting-started.mdx @@ -1,8 +1,8 @@ --- -title: Getting Started +title: Quickstart --- -This section demonstrates a complete workflow: creating an index, adding data, and searching. +## 1. Create Index @@ -12,7 +12,6 @@ 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", @@ -21,12 +20,29 @@ const index = await redis.search.createIndex({ name: s.string(), description: s.string(), category: s.string().noTokenize(), - price: s.number("F64"), + price: s.number(), inStock: s.boolean(), }), }); +``` + + + +```bash +SEARCH.CREATE products ON JSON PREFIX 1 product: SCHEMA name TEXT description TEXT category TEXT NOTOKENIZE price F64 FAST inStock BOOL +``` + + + + +## 2. Add Data + +Add data using standard Redis JSON commands. Any key matching the index prefix will be automatically indexed. -// Add some products (standard Redis JSON commands) + + + +```ts await redis.json.set("product:1", "$", { name: "Wireless Headphones", description: @@ -43,62 +59,41 @@ await redis.json.set("product:2", "$", { 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, -}); + +```bash +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}' -// Wait for indexing to complete (optional, for immediate queries) -await index.waitIndexing(); +SEARCH.WAITINDEXING products +``` + -// Search for products -const wirelessProducts = await index.query({ - filter: { description: "wireless" }, -}); + -for (const product of wirelessProducts) { - console.log(product); -} +## 3. Search Data -// Search with more filters -const runningProducts = await index.query({ - filter: { description: "running", inStock: true }, -}); -for (const product of runningProducts) { - console.log(product); -} + + + +```ts +const results = await index.query({ + filter: { description: "wireless" }, +}); -// Count matching documents -const count = await index.count({ filter: { price: { $lt: 150 } } }); -console.log(count); +const count = await index.count({ + filter: { price: { $lt: 150 } }, +}); ``` ```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 index 8219428a..411bce3f 100644 --- a/redis/search/index-management.mdx +++ b/redis/search/index-management.mdx @@ -1,14 +1,22 @@ --- -title: Index Management +title: Indices --- -### Creating an Index +An index is a data structure that enables **fast full-text search and filtering across your Redis data**. Without an index, searching requires scanning every key in your database: an operation that becomes very slow as your data grows. + +When you create a search index, Upstash Redis builds an optimized lookup structure for the schema you specify. This allows queries to find matching documents in milliseconds, regardless of dataset size. The index automatically stays in sync with your data: any keys matching the index prefix are indexed when created, updated when modified, and removed from the index when deleted. + +You define an index once by specifying: + +- A **name** to identify the index +- A **prefix** pattern to determine which keys to track (e.g., `user:`) +- A **schema** describing which fields to index and their types + +--- -The `SEARCH.CREATE` command creates a new search index that automatically tracks keys matching specified prefixes. +### Creating an Index -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. +An index is identified by its name, which must be a unique key in Redis. Each index works with a single key type (JSON, hash, or string). @@ -26,9 +34,10 @@ const users = await redis.search.createIndex({ schema: s.object({ name: s.string(), email: s.string(), - age: s.number("U64"), + age: s.number(), }), }); + ``` @@ -37,17 +46,20 @@ const users = await redis.search.createIndex({ # 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. +**We only create an index once**, for example inside of a script we run once. We do not recommend creating an index at runtime, for example inside of a serverless function. + +- For **JSON** indices, 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 **hash** indices, an index field can be specified for fields. As hash fields cannot have + nesting on their own, for this kind of indices, 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. +- For **string** indices, indexed keys must be valid JSON strings. A field on any nesting level + can be indexed, similar to JSON indices. @@ -67,10 +79,11 @@ const comments = await redis.search.createIndex({ email: s.string().noTokenize(), }), comment: s.string(), - upvotes: s.number("U64"), + upvotes: s.number(), commentedAt: s.date().fast(), }), }); + ``` @@ -79,6 +92,7 @@ const comments = await redis.search.createIndex({ # 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 ``` + @@ -107,9 +121,10 @@ const articles = await redis.search.createIndex({ body: s.string(), author: s.string().noStem(), publishedAt: s.date().fast(), - viewCount: s.number("U64"), + viewCount: s.number(), }), }); + ``` @@ -118,6 +133,7 @@ const articles = await redis.search.createIndex({ # 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 ``` + @@ -190,6 +206,7 @@ const addresses = await redis.search.createIndex({ description: s.string(), }), }); + ``` @@ -198,6 +215,7 @@ const addresses = await redis.search.createIndex({ # Turkish language index SEARCH.CREATE addresses ON JSON PREFIX 1 address: LANGUAGE turkish SCHEMA address TEXT NOSTEM description TEXT ``` + @@ -249,7 +267,7 @@ const results = await users.query({ const userSchema = s.object({ name: s.string(), email: s.string(), - age: s.number("U64"), + age: s.number(), }); // Note: The schema parameter provides TypeScript type safety @@ -261,6 +279,7 @@ const typedUsers = redis.search.index({ name: "users", schema: userSchema }); const typedResults = await typedUsers.query({ filter: { name: "John" }, }); + ``` @@ -291,6 +310,7 @@ The `SEARCH.DESCRIBE` command returns detailed information about an index. let description = await index.describe(); console.log(description); ``` + @@ -303,13 +323,13 @@ 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 | +| 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 @@ -360,6 +380,7 @@ const products = await index.query({ for (const product of products) { console.log(product); } + ``` @@ -374,6 +395,7 @@ 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 index 0a12a436..b1b7d8a5 100644 --- a/redis/search/introduction.mdx +++ b/redis/search/introduction.mdx @@ -2,18 +2,12 @@ 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: +Upstash Redis Search is our first extension beyond the official Redis spec. Using the Rust-based Tantivy under the hood, we provide an extremely fast way to search through Redis data. -- **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. +- **Easy Integration**: Works with JSON, Hashes and Strings out of the box +- **Auto-Synchronization**: Once you created an index, all write operations are automatically tracked +- **Intuitive Query Language**: A type-safe 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 diff --git a/redis/search/query-operators/overview.mdx b/redis/search/query-operators/overview.mdx deleted file mode 100644 index d7c4d463..00000000 --- a/redis/search/query-operators/overview.mdx +++ /dev/null @@ -1,5 +0,0 @@ ---- -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 index 635fbb4a..9ff610d4 100644 --- a/redis/search/querying.mdx +++ b/redis/search/querying.mdx @@ -1,37 +1,28 @@ --- -title: Querying +title: Queries --- -Queries are JSON strings that describe what documents to find. The simplest form specifies field-value pairs: +Queries are JSON strings that describe which documents to return. -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. +We recommend searching by field values directly because we automatically provide intelligent matching behavior out of the box: ```ts -// Search for a term in a specific field +// Basic search await index.query({ - filter: { - name: "headphones", - }, + filter: { name: "headphones" }, }); // Search across multiple fields (implicit AND) await index.query({ - filter: { - name: "wireless", - category: "electronics", - }, + filter: { name: "wireless", category: "electronics" }, }); // Search with exact values for non-text fields await index.query({ - filter: { - inStock: true, - price: 199.99, - }, + filter: { inStock: true, price: 199.99 }, }); ``` @@ -51,10 +42,11 @@ 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): +When you provide a value directly to a text field (without explicit operators), we automatically apply [smart matching](./query-operators/field-operators/smart-matching). Under the hood, it works like this: - **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 @@ -65,16 +57,13 @@ the search engine applies [smart matching](./query-operators/field-operators/sma 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 +## Query Options -Limit controls how many results to return. -Offset controls how many results to skip. +### 1. Pagination with Limit and Offset -Used together, these options provide a way to do pagination. +Limit controls how many results to return. Offset controls how many results to skip. Together, they provide a way to paginate results. @@ -82,26 +71,20 @@ 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", - }, + filter: { description: "wireless" }, limit: 10, }); // Page 2: results 11-20 const page2 = await index.query({ - filter: { - description: "wireless", - }, + filter: { description: "wireless" }, limit: 10, offset: 10, }); // Page 3: results 21-30 const page3 = await index.query({ - filter: { - description: "wireless", - }, + filter: { description: "wireless" }, limit: 10, offset: 20, }); @@ -123,16 +106,16 @@ SEARCH.QUERY products '{"description": "wireless"}' LIMIT 10 OFFSET 20 -#### Sorting Results +### 2. 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. +Only fields defined as `.fast()` in the schema can be used as the sort field (enabled by default). -When using `ORDERBY`, the score in results reflects the sort field's value rather than relevance. +When using `orderBy`, the score in results reflects the sort field's value rather than relevance. @@ -140,32 +123,20 @@ When using `ORDERBY`, the score in results reflects the sort field's value rathe ```ts // Sort by price, cheapest first await products.query({ - filter: { - category: "electronics", - }, - orderBy: { - price: "ASC", - }, + filter: { category: "electronics" }, + orderBy: { price: "ASC" }, }); // Sort by date, newest first await articles.query({ - filter: { - author: "john", - }, - orderBy: { - publishedAt: "DESC", - }, + 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", - }, + filter: { inStock: true }, + orderBy: { rating: "DESC" }, limit: 5, }); ``` @@ -186,26 +157,21 @@ SEARCH.QUERY products '{"inStock": true}' ORDERBY rating DESC LIMIT 5 -#### Controlling Output +### 3. 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`. +For JSON and string indexes, that means the stored JSON objects as whole. For hash indexes, it means all fields and values. -```ts -// Return only keys and scores +```ts {3} +// Example: Return documents without content await products.query({ - filter: { - name: "headphones", - }, select: {}, + filter: { name: "headphones" }, }); ``` @@ -219,21 +185,14 @@ 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 +```ts {3} +// Example: Return only `name` and `price` await products.query({ - filter: { - name: "headphones", - }, - select: { - name: true, - price: true, - }, + select: { name: true, price: true }, + filter: { name: "headphones" }, }); ``` @@ -253,7 +212,9 @@ use the **actual document field name** (not the alias) when selecting fields to This is because aliasing happens at the index level and does not modify the underlying documents. -#### Highlighting +--- + +### 4. Highlighting Highlighting allows you to see why a document matched the query by marking the matching portions of the document's fields. @@ -265,24 +226,14 @@ By default, `` and `` are used as the highlight tags. ```ts // Highlight matching terms await products.query({ - filter: { - description: "wireless noise cancelling", - }, - highlight: { - fields: ["description"], - }, + 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: "**", - }, + filter: { description: "wireless" }, + highlight: { fields: ["description"], preTag: "!!", postTag: "**" }, }); ``` @@ -299,8 +250,7 @@ SEARCH.QUERY products '{"description": "wireless"}' HIGHLIGHT FIELDS 1 descripti -Note that, highlighting only works for operators that resolve to terms, such as term or -phrase queries. +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), diff --git a/redis/search/schema-definition.mdx b/redis/search/schema-definition.mdx index df38502e..65c722cf 100644 --- a/redis/search/schema-definition.mdx +++ b/redis/search/schema-definition.mdx @@ -1,18 +1,13 @@ --- -title: Schema Definition +title: Schemas --- -Every index requires a schema that defines the structure of searchable documents. -The schema enforces type safety and enables query optimization. +Every search index requires a schema that defines the structure of searchable documents. The schema allows for type-safety and allows us to optimize your data for very fast queries. -## 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 +We provide a schema builder utility called `s` that makes it easy to define a schema. ```ts -import { Redis, s } from "@upstash/redis"; +import { Redis, s } from "@upstash/redis" ``` ### Basic Usage @@ -21,40 +16,21 @@ 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 + age: s.number(), + createdAt: s.date(), active: s.boolean(), -}); +}) ``` -### Field Options - -The schema builder supports chaining field options: +The schema builder also supports chaining field options. We'll see what `noTokenize()` and `noStem()` are used for in the section below. -```ts +```ts {2,3} 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 -}); + price: s.number(), +}) ``` ### Nested Objects @@ -66,21 +42,23 @@ const schema = s.object({ title: s.string(), author: s.object({ name: s.string(), - email: s.string().noTokenize(), + email: s.string(), }), stats: s.object({ - views: s.number("U64"), - likes: s.number("U64"), + views: s.number(), + likes: s.number(), }), -}); +}) ``` -### Using Schema with Index Creation +### Where to use the Schema + +We need the schema when creating or querying an index: ```ts -import { Redis, s } from "@upstash/redis"; +import { Redis, s } from "@upstash/redis" -const redis = Redis.fromEnv(); +const redis = Redis.fromEnv() const schema = s.object({ name: s.string(), @@ -88,150 +66,82 @@ const schema = s.object({ 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, - }, -}; -``` - +## Tokenization & Stemming - +When you store text in a search index, it goes through two transformations: **Tokenization** and **Stemming**. By default, text fields are both tokenized and stemmed. Understanding these helps you configure fields correctly. -The schema builder provides: -- Better type safety -- Autocomplete support -- More readable and maintainable code -- Easier refactoring +### Tokenization -## Field Types +Tokenization splits text into individual searchable words (tokens) by breaking on spaces and punctuation. -| 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"` | +| Original Text | Tokens | +| -------------------- | ---------------------------- | +| `"hello world"` | `["hello", "world"]` | +| `"user@example.com"` | `["user", "example", "com"]` | +| `"SKU-12345-BLK"` | `["SKU", "12345", "BLK"]` | -### Field Options +This is great for natural language because searching for "world" will match "hello world". But it breaks values that should stay together. -Options modify field behavior and enable additional features. +**When to disable tokenization** with `.noTokenize()`: -#### Text Field Options +- Email addresses (`user@example.com`) +- URLs (`https://example.com/page`) +- Product codes and SKUs (`SKU-12345-BLK`) +- UUIDs (`550e8400-e29b-41d4-a716-446655440000`) +- Category slugs (`electronics/phones/android`) -By default, text fields are tokenized and stemmed. +```ts +const schema = s.object({ + title: s.string(), + email: s.string().noTokenize(), + sku: s.string().noTokenize(), +}) +``` -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ş" | +### Stemming -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. +Stemming reduces words to their root form so different variations match the same search. -It is possible to configure this behavior using the following options: +| Word | Stemmed Form | +| -------------------------------------- | ------------ | +| `"running"`, `"runs"`, `"runner"` | `"run"` | +| `"studies"`, `"studying"`, `"studied"` | `"studi"` | +| `"experiments"`, `"experimenting"` | `"experi"` | -| 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 | +This way, a user searching for "running shoes" will also find "run shoes" and "runner shoes". -When using [`$regex`](./query-operators/field-operators/regex), be aware of stemming behavior: +**When to disable stemming** with `.noStem()`: - +- Brand names (`Nike` shouldn't match `Nik`) +- Proper nouns and names (`Johnson` shouldn't become `John`) +- Technical terms (`React` shouldn't match `Reac`) +- When using regex patterns (stemmed text won't match your expected patterns) - ```ts -// 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.*"}}' +const schema = s.object({ + description: s.string(), + brand: s.string().noStem(), + authorName: s.string().noStem(), +}) ``` - - - -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 Aliased fields allow you to index the same document field multiple times with different settings, or to create shorter names for complex nested paths. @@ -250,15 +160,13 @@ const products = await redis.search.createIndex({ 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"), }), }); ``` + @@ -267,6 +175,7 @@ const products = await redis.search.createIndex({ # Create a short alias for a deeply nested field SEARCH.CREATE products ON JSON PREFIX 1 product: SCHEMA description TEXT descriptionExact TEXT FROM description NOSTEM authorName TEXT FROM metadata.author.displayName ``` + @@ -282,16 +191,21 @@ When using aliased fields: - 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 +--- + +## Non-Indexed Fields + +Documents don't need to match the schema exactly: -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. +- **Extra fields**: Fields in your document that aren't defined in the schema are simply ignored. They won't be indexed or searchable. +- **Missing fields**: If a document is missing a field defined in the schema, that document won't appear in search results that filter on the missing field. -### Schema Examples +--- + +## Schema Examples **E-commerce product schema** @@ -300,41 +214,27 @@ matches for the missing fields. ```ts -import { Redis, s } from "@upstash/redis"; +import { Redis, s } from "@upstash/redis" -const redis = Redis.fromEnv(); +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 + sku: s.string().noTokenize(), // Exact-match SKU codes + brand: s.string().noStem(), // Brand names without stemming 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 + price: s.number("F64"), // Sortable (F64) price + rating: s.number("F64"), // Sortable (F64) rating + reviewCount: s.number("U64"), // Non-sortable (U64) review count inStock: s.boolean(), }), -}); +}) ``` + @@ -360,29 +260,18 @@ const users = await redis.search.createIndex({ 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(), }), }); ``` +