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