From 43d8e854ed606db96450264cd067ceeafd8228be Mon Sep 17 00:00:00 2001 From: ilterkavlak Date: Mon, 16 Mar 2026 23:23:44 +0300 Subject: [PATCH 1/4] CLOUD-4245 improve-redis-search-docs --- docs.json | 2 + .../bucket-aggregations/overview.mdx | 13 +- .../metric-aggregations/overview.mdx | 16 +- redis/search/aggregations.mdx | 62 ++++ redis/search/command-reference.mdx | 275 ++++++++++++++++++ redis/search/counting.mdx | 1 + redis/search/index-management.mdx | 6 +- .../field-operators/overview.mdx | 13 + redis/search/querying.mdx | 62 ++++ redis/search/schema-definition.mdx | 36 +++ redis/search/troubleshooting.mdx | 138 +++++++++ 11 files changed, 620 insertions(+), 4 deletions(-) create mode 100644 redis/search/command-reference.mdx create mode 100644 redis/search/troubleshooting.mdx diff --git a/docs.json b/docs.json index 6547e248..aec06e16 100644 --- a/docs.json +++ b/docs.json @@ -724,6 +724,8 @@ "redis/search/aggregations", "redis/search/counting", "redis/search/aliases", + "redis/search/command-reference", + "redis/search/troubleshooting", { "group": "Query Operators", "pages": [ diff --git a/redis/search/aggregation-operators/bucket-aggregations/overview.mdx b/redis/search/aggregation-operators/bucket-aggregations/overview.mdx index 4f864831..a665346d 100644 --- a/redis/search/aggregation-operators/bucket-aggregations/overview.mdx +++ b/redis/search/aggregation-operators/bucket-aggregations/overview.mdx @@ -16,8 +16,19 @@ Use bucket aggregations when you want segmented analytics (for example by catego | [`$dateHistogram`](./date-histogram) | Fixed date/time intervals | | [`$facet`](./facet) | Hierarchical FACET paths | +### Input Format + +Every bucket operator takes an object with a `field` property and operator-specific parameters: + +```json +{"by_category": {"$terms": {"field": "category", "size": 10}}} +{"price_ranges": {"$range": {"field": "price", "ranges": [{"to": 50}, {"from": 50, "to": 100}, {"from": 100}]}}} +{"by_month": {"$histogram": {"field": "price", "interval": 10}}} +{"by_date": {"$dateHistogram": {"field": "createdAt", "calendarInterval": "month"}}} +``` + ### Behavior Notes -- Bucket operators can contain nested `$aggs`. +- Bucket operators can contain nested `$aggs` for per-bucket metrics. - `$terms`, `$range`, `$histogram`, and `$dateHistogram` support nested `$aggs`. - `$facet` does not support nested `$aggs` and cannot be used as a sub-aggregation. diff --git a/redis/search/aggregation-operators/metric-aggregations/overview.mdx b/redis/search/aggregation-operators/metric-aggregations/overview.mdx index 3fb74175..b1e6ea23 100644 --- a/redis/search/aggregation-operators/metric-aggregations/overview.mdx +++ b/redis/search/aggregation-operators/metric-aggregations/overview.mdx @@ -20,9 +20,21 @@ Use metrics when you want one value (or stats object), not grouped buckets. | [`$extendedStats`](./extended-stats) | `$stats` + variance and std deviation metrics | | [`$percentiles`](./percentiles) | Distribution percent points | +### Input Format + +Every metric operator takes an object with at least a `field` property: + +```json +{"alias_name": {"$avg": {"field": "price"}}} +{"alias_name": {"$avg": {"field": "price", "missing": 0}}} +``` + +The `field` value must be a string pointing to a FAST field in your schema. +Do **not** pass a bare string — it must be an object with `{"field": "..."}`. + ### Behavior Notes -- Metric operators require a `field`. -- The field must be `FAST` in your schema. +- Metric operators require a `field` — you will get `missing required 'field' property` if it's omitted. +- The field must be `FAST` in your schema — otherwise you will get an error: `operator '$avg' requires field '' to be FAST`. See [FAST Fields](/redis/search/schema-definition#fast-fields). - Metric operators do not support nested `$aggs`. - For many metric operators, `missing` lets you provide a fallback value for documents where the field does not exist. diff --git a/redis/search/aggregations.mdx b/redis/search/aggregations.mdx index 4f5c658c..89fe9d24 100644 --- a/redis/search/aggregations.mdx +++ b/redis/search/aggregations.mdx @@ -47,6 +47,68 @@ Aggregation requests have two phases: Each aggregation is defined with an **alias** (the key you choose for the result) and an **operator** that specifies what to compute. +### Response Format + + + + +```ts +const result = await index.aggregate({ + aggregations: { + avg_price: { $avg: { field: "price" } }, + by_category: { $terms: { field: "category", size: 5 } }, + }, +}); + +// result is an object keyed by alias: +// { +// avg_price: 49.99, +// by_category: [ +// { key: "electronics", docCount: 42 }, +// { key: "clothing", docCount: 31 }, +// ] +// } +``` + + + +```python +result = index.aggregate( + aggregations={ + "avg_price": {"$avg": {"field": "price"}}, + "by_category": {"$terms": {"field": "category", "size": 5}}, + }, +) + +# result is a dict keyed by alias: +# { +# "avg_price": 49.99, +# "by_category": [ +# {"key": "electronics", "doc_count": 42}, +# {"key": "clothing", "doc_count": 31}, +# ] +# } +``` + + + +```bash +SEARCH.AGGREGATE products '{}' '{"avg_price": {"$avg": {"field": "price"}}, "by_category": {"$terms": {"field": "category", "size": 5}}}' + +# Response is a flat array of alternating alias-value pairs: +# ["avg_price", 49.99, "by_category", [["electronics", ["doc_count", 42]], ["clothing", ["doc_count", 31]]]] +``` + + + + + +All metric aggregation operators require the target field to be marked as `FAST` in your schema. +If the field is not FAST, you will get an error like: +`Aggregation '' operator '$avg' requires field '' to be FAST`. +See [FAST Fields](/redis/search/schema-definition#fast-fields) for details. + + ## Filtering Use `filter` to restrict which documents participate in the aggregation. diff --git a/redis/search/command-reference.mdx b/redis/search/command-reference.mdx new file mode 100644 index 00000000..a5627fec --- /dev/null +++ b/redis/search/command-reference.mdx @@ -0,0 +1,275 @@ +--- +title: Command Reference +--- + +A complete reference of all Upstash Redis Search commands, their syntax, and return values. + + +Upstash Redis Search uses `SEARCH.*` commands. These are **not** the same as the `FT.*` commands +from the open-source RediSearch module. The two are completely separate implementations and are +not compatible with each other. + + +## Index Commands + +### SEARCH.CREATE + +Creates a new search index. + +```bash +SEARCH.CREATE ON + PREFIX [ ...] + [LANGUAGE ] + [SKIPINITIALSCAN] + [EXISTSOK] + SCHEMA [FAST] [NOSTEM] [NOTOKENIZE] [FROM ] ... +``` + +**Returns:** `1` on success. Returns an error if the index already exists (unless `EXISTSOK` is used). + + + +```bash +SEARCH.CREATE products ON JSON PREFIX 1 product: SCHEMA name TEXT price F64 FAST inStock BOOL +``` + + +```bash +curl -X POST https://YOUR_ENDPOINT.upstash.io \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -d '["SEARCH.CREATE", "products", "ON", "JSON", "PREFIX", "1", "product:", "SCHEMA", "name", "TEXT", "price", "F64", "FAST", "inStock", "BOOL"]' +``` + + + +--- + +### SEARCH.DROP + +Removes an index. The underlying Redis keys are **not** deleted. + +```bash +SEARCH.DROP +``` + +**Returns:** `1` if dropped, `0` if the index was not found. + +--- + +### SEARCH.DESCRIBE + +Returns metadata about an index. + +```bash +SEARCH.DESCRIBE +``` + +**Returns:** Index metadata including name, data type, prefixes, language, and schema definition. Returns `null` if the index does not exist. + +--- + +### SEARCH.WAITINDEXING + +Blocks until all pending index updates are processed and visible to queries. + +```bash +SEARCH.WAITINDEXING +``` + +**Returns:** `1` when indexing is complete, `0` if the index was not found. + + +Do not call `SEARCH.WAITINDEXING` after every write. Batch updates are necessary for +optimal indexing performance. Use this command only when you need to ensure queries +reflect recent changes, such as in tests or CI pipelines. + + +--- + +## Query Commands + +### SEARCH.QUERY + +Searches for documents matching a JSON filter. + +```bash +SEARCH.QUERY '' + [LIMIT ] + [OFFSET ] + [ORDERBY ] + [SELECT [ ...]] + [NOCONTENT] + [HIGHLIGHT FIELDS [ ...] [TAGS ]] + [SCOREFUNC ...] +``` + +**Parameters:** + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `LIMIT` | Maximum number of results to return. Must be between 1 and 1000. | 10 | +| `OFFSET` | Number of results to skip (for pagination). | 0 | +| `ORDERBY` | Sort by a FAST field in `ASC` or `DESC` order. | Sort by relevance score (descending) | +| `SELECT` | Return only specific fields. Prefix with count of fields. | All fields | +| `NOCONTENT` | Return only keys and scores, no document content. | Disabled | +| `HIGHLIGHT` | Wrap matching terms in tags. Default tags: `` / ``. | Disabled | +| `SCOREFUNC` | Adjust relevance scores using numeric field values. | Disabled | + +**Response format (Redis CLI):** + +``` +[ + ["key1", "score1", [["$", "{\"name\": \"...\", ...}"]]], + ["key2", "score2", [["$", "{\"name\": \"...\", ...}"]]] +] +``` + +Each result is an array of `[key, score, content]` where: +- `key` — the Redis key of the matching document +- `score` — relevance score (float as string) +- `content` — array of field-value pairs (for JSON: `[["$", ""]]`) + +When `NOCONTENT` is used, the content element is omitted. +When `SELECT` is used, only the selected fields appear in the content. + + + +```bash +SEARCH.QUERY products '{"name": "wireless"}' LIMIT 10 OFFSET 0 +``` + + +```bash +curl -X POST https://YOUR_ENDPOINT.upstash.io \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -d '["SEARCH.QUERY", "products", "{\"name\": \"wireless\"}", "LIMIT", "10", "OFFSET", "0"]' +``` + + + +--- + +### SEARCH.COUNT + +Returns the number of documents matching a query without retrieving them. + +```bash +SEARCH.COUNT '' +``` + +**Returns:** An integer — the count of matching documents. + + + +```bash +SEARCH.COUNT products '{"inStock": true}' +``` + + +```bash +curl -X POST https://YOUR_ENDPOINT.upstash.io \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -d '["SEARCH.COUNT", "products", "{\"inStock\": true}"]' +``` + + + +--- + +### SEARCH.AGGREGATE + +Computes analytics (metrics and buckets) over matching documents. + +```bash +SEARCH.AGGREGATE '' '' +``` + +**Response format (Redis CLI):** + +The response is a flat array of alternating alias-value pairs: + +``` +["alias1", , "alias2", , ...] +``` + +Where each value depends on the aggregation type: +- **Metric operators** (`$avg`, `$sum`, etc.) — a single number or stats object +- **Bucket operators** (`$terms`, `$range`, etc.) — an array of bucket objects + + + +```bash +SEARCH.AGGREGATE products '{}' '{"avg_price": {"$avg": {"field": "price"}}}' +``` + + +```bash +curl -X POST https://YOUR_ENDPOINT.upstash.io \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -d '["SEARCH.AGGREGATE", "products", "{}", "{\"avg_price\": {\"$avg\": {\"field\": \"price\"}}}"]' +``` + + + +--- + +## Alias Commands + +### SEARCH.ALIASADD + +Creates or updates an alias pointing to an index. + +```bash +SEARCH.ALIASADD +``` + +**Returns:** `1` if a new alias was created, `2` if an existing alias was updated. + +--- + +### SEARCH.ALIASDEL + +Removes an alias. + +```bash +SEARCH.ALIASDEL +``` + +**Returns:** `1` if deleted, `0` if the alias was not found. + +--- + +### SEARCH.LISTALIASES + +Returns all aliases and the indices they point to. + +```bash +SEARCH.LISTALIASES +``` + +**Returns:** An array of `[alias, index_name]` pairs. + +--- + +## REST API Usage + +All search commands can be sent via the Upstash REST API using a JSON array POST body. +Each element of the array corresponds to a token in the command. + +```bash +curl -X POST https://YOUR_ENDPOINT.upstash.io \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -d '["COMMAND", "arg1", "arg2", ...]' +``` + +The JSON filter and aggregation arguments are passed as string values within the array (not as nested JSON objects): + +```bash +# Correct — filter is a string inside the array +["SEARCH.QUERY", "products", "{\"name\": \"wireless\"}", "LIMIT", "10"] + +# Incorrect — filter is a nested object +["SEARCH.QUERY", "products", {"name": "wireless"}, "LIMIT", "10"] +``` + +Search commands also work through the `/pipeline` endpoint for batching multiple commands in a single HTTP request. diff --git a/redis/search/counting.mdx b/redis/search/counting.mdx index add892cd..b9bf9e90 100644 --- a/redis/search/counting.mdx +++ b/redis/search/counting.mdx @@ -3,6 +3,7 @@ title: Counting --- The `SEARCH.COUNT` command returns the number of documents matching a query without retrieving them. +It returns a single integer. You can use `SEARCH.COUNT` for analytics, pagination UI (showing "X results found"), or validating queries before retrieving results. diff --git a/redis/search/index-management.mdx b/redis/search/index-management.mdx index 7b95674b..1060addc 100644 --- a/redis/search/index-management.mdx +++ b/redis/search/index-management.mdx @@ -496,8 +496,12 @@ For adequate performance, index updates are batched and committed periodically. 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. +This includes both initial scan indexing (when the index is first created) and incremental updates +from subsequent write operations. + 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. +query performance, batch updates are necessary. This command is primarily useful in tests and CI pipelines +where you need to guarantee that writes are reflected in subsequent queries. Returns `1` when indexing is complete, or `0` if the index was not found. diff --git a/redis/search/query-operators/field-operators/overview.mdx b/redis/search/query-operators/field-operators/overview.mdx index 75a6704d..cae236c5 100644 --- a/redis/search/query-operators/field-operators/overview.mdx +++ b/redis/search/query-operators/field-operators/overview.mdx @@ -4,3 +4,16 @@ title: Overview Field operators provide precise control over how individual fields are matched. Use these when simple value matching doesn't meet your needs. + +### Quick Reference + +| Operator | Supported Field Types | Description | Example | +|---|---|---|---| +| [`$smart`](./smart-matching) | all | Intelligent multi-stage matching (default behavior) | `{"name": {"$smart": "wireless"}}` | +| [`$eq`](./eq) | all | Exact equality | `{"status": {"$eq": "active"}}` | +| [`$in`](./in) | all | Match any of multiple values | `{"status": {"$in": ["active", "pending"]}}` | +| [`$gt`](./range-operators), `$gte`, `$lt`, `$lte` | numeric, date, keyword | Range comparison | `{"price": {"$gt": 10, "$lt": 100}}` | +| [`$phrase`](./phrase) | text | Phrase matching with optional slop and prefix | `{"text": {"$phrase": {"query": "hello world", "slop": 1}}}` | +| [`$fuzzy`](./fuzzy) | text | Typo-tolerant matching (Levenshtein distance) | `{"name": {"$fuzzy": {"term": "wireles", "distance": 1}}}` | +| [`$regex`](./regex) | text | Regular expression pattern matching | `{"name": {"$regex": "wire.*"}}` | +| [`$boost`](./boost) | modifier (on any operator) | Adjust relevance score weight | `{"name": {"$eq": "wireless", "$boost": 2.0}}` | diff --git a/redis/search/querying.mdx b/redis/search/querying.mdx index 055fc61c..a12a2133 100644 --- a/redis/search/querying.mdx +++ b/redis/search/querying.mdx @@ -57,6 +57,58 @@ SEARCH.QUERY products '{"inStock": true, "price": 199.99}' --- +### Response Format + +The query response is an array of matching documents. Each document includes the Redis key, a relevance score, and the document content. + + + + +```ts +const results = await index.query({ + filter: { name: "headphones" }, +}); + +// results is an array of objects: +// [ +// { key: "product:1", score: 5.23, data: { name: "Wireless Headphones", price: 99.99, ... } }, +// { key: "product:2", score: 3.11, data: { name: "Studio Headphones", price: 149.99, ... } }, +// ] +``` + + + +```python +results = index.query(filter={"name": "headphones"}) + +# results is a list of objects: +# [ +# QueryResult(key="product:1", score=5.23, data={"name": "Wireless Headphones", "price": 99.99, ...}), +# QueryResult(key="product:2", score=3.11, data={"name": "Studio Headphones", "price": 149.99, ...}), +# ] +``` + + + +```bash +SEARCH.QUERY products '{"name": "headphones"}' + +# Response: +# [ +# ["product:1", "5.23", [["$", "{\"name\": \"Wireless Headphones\", \"price\": 99.99}"]]], +# ["product:2", "3.11", [["$", "{\"name\": \"Studio Headphones\", \"price\": 149.99}"]]] +# ] +``` + + + + +When `select` / `NOCONTENT` is used, the response shape changes — see [Controlling Output](#3-controlling-output). + +If the index doesn't exist or no documents match, an empty array is returned. + +--- + ### Smart Matching When you provide a value directly to a field (without explicit operators), @@ -81,6 +133,16 @@ or [`$fuzzy`](./query-operators/field-operators/fuzzy). Limit controls how many results to return. Offset controls how many results to skip. Together, they provide a way to paginate results. +| Parameter | Description | Default | Constraints | +|-----------|-------------|---------|-------------| +| `LIMIT` | Number of results to return | 10 | Must be between 1 and 1000 | +| `OFFSET` | Number of results to skip | 0 | Must be non-negative | + + +`LIMIT` and `OFFSET` are separate keywords. This differs from RediSearch which uses the combined +`LIMIT ` syntax. Using `LIMIT 0 10` will return an error. + + diff --git a/redis/search/schema-definition.mdx b/redis/search/schema-definition.mdx index 6412c494..b5bf2fae 100644 --- a/redis/search/schema-definition.mdx +++ b/redis/search/schema-definition.mdx @@ -10,6 +10,42 @@ We provide a schema builder utility called `s` that makes it easy to define a sc import { Redis, s } from "@upstash/redis" ``` +### Field Type Reference + +| SDK Method | Redis CLI Type | Description | Supports FAST | Range Operators | Text Operators | +|---|---|---|---|---|---| +| `s.string()` | `TEXT` | Full-text searchable field with tokenization and stemming | No | No | `$smart`, `$phrase`, `$fuzzy`, `$regex` | +| `s.keyword()` | `KEYWORD` | Exact-match string (no tokenization) | Yes | Yes (`$gt`, `$gte`, `$lt`, `$lte`) | No | +| `s.number()` | `F64` (default), `U64`, `I64` | Numeric field | Yes | Yes | No | +| `s.date()` | `DATE` | Date/time field | Yes | Yes | No | +| `s.boolean()` | `BOOL` | Boolean field | Yes | No | No | +| `s.facet()` | `FACET` | Hierarchical path-based field | No | No | No (only `$eq`, `$in`) | + + +In the TypeScript SDK, `s.number()` defaults to `F64`. You can specify `s.number("U64")` or +`s.number("I64")` for unsigned or signed 64-bit integers. `F64` fields are FAST by default. + + +### FAST Fields + +The `FAST` flag creates a columnar store for a field, enabling: +- **Sorting** with `ORDERBY` in queries +- **Score functions** with `SCOREFUNC FIELDVALUE` +- **Metric aggregations** (`$avg`, `$sum`, `$min`, `$max`, `$count`, etc.) + +In the TypeScript SDK, numeric (`F64`), boolean, and date fields are FAST **by default**. You can +disable it with `.fast(false)`. In Redis CLI, you must explicitly add the `FAST` keyword after the +field type. + +```bash +# Redis CLI — FAST must be explicit +SEARCH.CREATE products ON JSON PREFIX 1 product: SCHEMA name TEXT price F64 FAST rating F64 FAST +``` + +If you attempt to use `ORDERBY`, `SCOREFUNC`, or metric aggregations on a non-FAST field, you will get an error. + +--- + ### Basic Usage The schema builder provides methods for each field type: diff --git a/redis/search/troubleshooting.mdx b/redis/search/troubleshooting.mdx new file mode 100644 index 00000000..8af556f3 --- /dev/null +++ b/redis/search/troubleshooting.mdx @@ -0,0 +1,138 @@ +--- +title: Troubleshooting +--- + +Common errors and how to resolve them when working with Upstash Redis Search. + +## Index Errors + +### `ERR Index already exists` + +You tried to create an index that already exists. + +**Fix:** Use the `existsOk` option (SDK) or `EXISTSOK` flag (CLI) to skip creation if the index already exists: + +```bash +SEARCH.CREATE products ON JSON PREFIX 1 product: EXISTSOK SCHEMA name TEXT +``` + +Or drop the existing index first with `SEARCH.DROP `. + +--- + +## Query Errors + +### `ERR limit should be a positive number less than 1000` + +The `LIMIT` value must be between 1 and 1000. + +**Common causes:** +- Using `LIMIT 0` — the minimum is 1 +- Using `LIMIT` greater than 1000 +- Using RediSearch-style `LIMIT ` syntax — Upstash uses separate `LIMIT` and `OFFSET` keywords + +**Fix:** +```bash +# Correct +SEARCH.QUERY products '{}' LIMIT 10 OFFSET 20 + +# Incorrect (RediSearch style) +SEARCH.QUERY products '{}' LIMIT 20 10 +``` + +### `ERR Unknown field operator: $not` + +There is no `$not` operator. Use `$mustNot` for exclusion filtering: + +```bash +# Correct +SEARCH.QUERY products '{"$mustNot": [{"status": "discontinued"}]}' + +# Incorrect +SEARCH.QUERY products '{"status": {"$not": "discontinued"}}' +``` + +--- + +## Aggregation Errors + +### Aggregation operator requires field to be FAST + +``` +Aggregation '' operator '$avg' requires field '' to be FAST +``` + +All metric aggregation operators (`$avg`, `$sum`, `$min`, `$max`, `$count`, `$cardinality`, `$stats`, `$extendedStats`, `$percentiles`) require the target field to be defined as `FAST` in the index schema. + +**Fix:** Recreate the index with the field marked as `FAST`: + +```bash +# Ensure 'price' has the FAST flag +SEARCH.CREATE products ON JSON PREFIX 1 product: SCHEMA name TEXT price F64 FAST +``` + +In the TypeScript SDK, numeric fields (`s.number("F64")`) and date fields (`s.date()`) are FAST by default. Boolean fields (`s.boolean()`) are also FAST by default. If you explicitly called `.fast(false)`, remove it. + +### Missing required 'field' property + +``` +Aggregation '' is missing required 'field' property +``` + +Every metric aggregation operator requires a `field` property pointing to the field to aggregate. + +**Fix:** Pass `field` as an object property, not a bare string: + +```json +// Correct +{"avg_price": {"$avg": {"field": "price"}}} + +// Incorrect +{"avg_price": {"$avg": "price"}} +``` + +### Type key value must be an object + +``` +Aggregation '' type key value must be an object +``` + +The aggregation operator value must be a JSON object, not a string or number. + +**Fix:** +```json +// Correct +{"avg_price": {"$avg": {"field": "price"}}} + +// Incorrect +{"avg_price": {"$avg": "price"}} +``` + +--- + +## Schema Errors + +### Field type mismatches + +If a document field's value doesn't match the schema type (e.g., a string value `"abc"` for a numeric field), that document is silently skipped during indexing for that field. It will not appear in queries that filter on the mismatched field. + +### Missing document fields + +If a document doesn't have a field defined in the schema, it simply won't match queries filtering on that field. No error is raised. + +--- + +## Upstash Redis Search vs RediSearch + +Upstash Redis Search uses `SEARCH.*` commands and is a separate implementation from the +open-source RediSearch module which uses `FT.*` commands. The two are **not** compatible. + +| | Upstash Redis Search | RediSearch | +|---|---|---| +| Command prefix | `SEARCH.*` | `FT.*` | +| Engine | Tantivy (Rust) | RediSearch (C) | +| Query syntax | JSON-based filters | RediSearch query syntax | +| Pagination | `LIMIT OFFSET ` (separate keywords) | `LIMIT ` (combined) | +| Hosting | Serverless (Upstash) | Self-hosted or Redis Cloud | + +If you are migrating from RediSearch, you will need to rewrite your queries to use the JSON filter syntax and `SEARCH.*` commands. From 45c531b484a9712a28c7a2a7cf7d3080956192e5 Mon Sep 17 00:00:00 2001 From: up-ilter <91122592+up-ilter@users.noreply.github.com> Date: Tue, 17 Mar 2026 10:55:22 +0300 Subject: [PATCH 2/4] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- redis/search/command-reference.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redis/search/command-reference.mdx b/redis/search/command-reference.mdx index a5627fec..81309956 100644 --- a/redis/search/command-reference.mdx +++ b/redis/search/command-reference.mdx @@ -95,7 +95,7 @@ Searches for documents matching a JSON filter. ```bash SEARCH.QUERY '' [LIMIT ] - [OFFSET ] + [OFFSET ] [ORDERBY ] [SELECT [ ...]] [NOCONTENT] From 9033424a21227576cd4aafc3a247adaa08584d74 Mon Sep 17 00:00:00 2001 From: ilterkavlak Date: Tue, 17 Mar 2026 11:13:25 +0300 Subject: [PATCH 3/4] CLOUD-4245 improve-redis-search-docs --- .../bucket-aggregations/overview.mdx | 17 +++++++++++++-- redis/search/aggregations.mdx | 6 ++++++ redis/search/command-reference.mdx | 14 ++++++------- redis/search/querying.mdx | 4 +++- redis/search/troubleshooting.mdx | 21 ++++++++++++------- 5 files changed, 44 insertions(+), 18 deletions(-) diff --git a/redis/search/aggregation-operators/bucket-aggregations/overview.mdx b/redis/search/aggregation-operators/bucket-aggregations/overview.mdx index a665346d..84449415 100644 --- a/redis/search/aggregation-operators/bucket-aggregations/overview.mdx +++ b/redis/search/aggregation-operators/bucket-aggregations/overview.mdx @@ -20,11 +20,24 @@ Use bucket aggregations when you want segmented analytics (for example by catego Every bucket operator takes an object with a `field` property and operator-specific parameters: +**`$terms`** — group by distinct values: ```json {"by_category": {"$terms": {"field": "category", "size": 10}}} +``` + +**`$range`** — custom range buckets: +```json {"price_ranges": {"$range": {"field": "price", "ranges": [{"to": 50}, {"from": 50, "to": 100}, {"from": 100}]}}} -{"by_month": {"$histogram": {"field": "price", "interval": 10}}} -{"by_date": {"$dateHistogram": {"field": "createdAt", "calendarInterval": "month"}}} +``` + +**`$histogram`** — fixed numeric intervals: +```json +{"price_buckets": {"$histogram": {"field": "price", "interval": 10}}} +``` + +**`$dateHistogram`** — fixed time intervals: +```json +{"by_month": {"$dateHistogram": {"field": "createdAt", "calendarInterval": "month"}}} ``` ### Behavior Notes diff --git a/redis/search/aggregations.mdx b/redis/search/aggregations.mdx index 89fe9d24..eadd825f 100644 --- a/redis/search/aggregations.mdx +++ b/redis/search/aggregations.mdx @@ -102,6 +102,12 @@ SEARCH.AGGREGATE products '{}' '{"avg_price": {"$avg": {"field": "price"}}, "by_ + +Each SDK normalizes response keys to match its language's conventions: camelCase for TypeScript +(e.g., `docCount`) and snake_case for Python (e.g., `doc_count`). The Redis CLI returns the raw +wire format with key-value arrays. + + All metric aggregation operators require the target field to be marked as `FAST` in your schema. If the field is not FAST, you will get an error like: diff --git a/redis/search/command-reference.mdx b/redis/search/command-reference.mdx index 81309956..a2d12627 100644 --- a/redis/search/command-reference.mdx +++ b/redis/search/command-reference.mdx @@ -36,7 +36,7 @@ SEARCH.CREATE products ON JSON PREFIX 1 product: SCHEMA name TEXT price F64 FAST ```bash curl -X POST https://YOUR_ENDPOINT.upstash.io \ - -H "Authorization: Bearer YOUR_TOKEN" \ + -H "Authorization: Bearer $UPSTASH_REDIS_REST_TOKEN" \ -d '["SEARCH.CREATE", "products", "ON", "JSON", "PREFIX", "1", "product:", "SCHEMA", "name", "TEXT", "price", "F64", "FAST", "inStock", "BOOL"]' ``` @@ -107,7 +107,7 @@ SEARCH.QUERY '' | Parameter | Description | Default | |-----------|-------------|---------| -| `LIMIT` | Maximum number of results to return. Must be between 1 and 1000. | 10 | +| `LIMIT` | Maximum number of results to return. Must be between 1 and 999. | 10 | | `OFFSET` | Number of results to skip (for pagination). | 0 | | `ORDERBY` | Sort by a FAST field in `ASC` or `DESC` order. | Sort by relevance score (descending) | | `SELECT` | Return only specific fields. Prefix with count of fields. | All fields | @@ -141,7 +141,7 @@ SEARCH.QUERY products '{"name": "wireless"}' LIMIT 10 OFFSET 0 ```bash curl -X POST https://YOUR_ENDPOINT.upstash.io \ - -H "Authorization: Bearer YOUR_TOKEN" \ + -H "Authorization: Bearer $UPSTASH_REDIS_REST_TOKEN" \ -d '["SEARCH.QUERY", "products", "{\"name\": \"wireless\"}", "LIMIT", "10", "OFFSET", "0"]' ``` @@ -168,7 +168,7 @@ SEARCH.COUNT products '{"inStock": true}' ```bash curl -X POST https://YOUR_ENDPOINT.upstash.io \ - -H "Authorization: Bearer YOUR_TOKEN" \ + -H "Authorization: Bearer $UPSTASH_REDIS_REST_TOKEN" \ -d '["SEARCH.COUNT", "products", "{\"inStock\": true}"]' ``` @@ -194,7 +194,7 @@ The response is a flat array of alternating alias-value pairs: Where each value depends on the aggregation type: - **Metric operators** (`$avg`, `$sum`, etc.) — a single number or stats object -- **Bucket operators** (`$terms`, `$range`, etc.) — an array of bucket objects +- **Bucket operators** (`$terms`, `$range`, etc.) — nested arrays of key-value pairs (SDKs parse these into objects) @@ -205,7 +205,7 @@ SEARCH.AGGREGATE products '{}' '{"avg_price": {"$avg": {"field": "price"}}}' ```bash curl -X POST https://YOUR_ENDPOINT.upstash.io \ - -H "Authorization: Bearer YOUR_TOKEN" \ + -H "Authorization: Bearer $UPSTASH_REDIS_REST_TOKEN" \ -d '["SEARCH.AGGREGATE", "products", "{}", "{\"avg_price\": {\"$avg\": {\"field\": \"price\"}}}"]' ``` @@ -258,7 +258,7 @@ Each element of the array corresponds to a token in the command. ```bash curl -X POST https://YOUR_ENDPOINT.upstash.io \ - -H "Authorization: Bearer YOUR_TOKEN" \ + -H "Authorization: Bearer $UPSTASH_REDIS_REST_TOKEN" \ -d '["COMMAND", "arg1", "arg2", ...]' ``` diff --git a/redis/search/querying.mdx b/redis/search/querying.mdx index a12a2133..885bb89a 100644 --- a/redis/search/querying.mdx +++ b/redis/search/querying.mdx @@ -107,6 +107,8 @@ When `select` / `NOCONTENT` is used, the response shape changes — see [Control If the index doesn't exist or no documents match, an empty array is returned. +For a detailed breakdown of the raw response structure, see the [Command Reference](/redis/search/command-reference#searchquery). + --- ### Smart Matching @@ -135,7 +137,7 @@ Limit controls how many results to return. Offset controls how many results to s | Parameter | Description | Default | Constraints | |-----------|-------------|---------|-------------| -| `LIMIT` | Number of results to return | 10 | Must be between 1 and 1000 | +| `LIMIT` | Number of results to return | 10 | Must be between 1 and 999 | | `OFFSET` | Number of results to skip | 0 | Must be non-negative | diff --git a/redis/search/troubleshooting.mdx b/redis/search/troubleshooting.mdx index 8af556f3..984eef37 100644 --- a/redis/search/troubleshooting.mdx +++ b/redis/search/troubleshooting.mdx @@ -24,11 +24,11 @@ Or drop the existing index first with `SEARCH.DROP `. ### `ERR limit should be a positive number less than 1000` -The `LIMIT` value must be between 1 and 1000. +The `LIMIT` value must be a positive integer up to (but not including) 1000. Valid range: 1–999. **Common causes:** - Using `LIMIT 0` — the minimum is 1 -- Using `LIMIT` greater than 1000 +- Using `LIMIT 1000` or greater — the maximum is 999 - Using RediSearch-style `LIMIT ` syntax — Upstash uses separate `LIMIT` and `OFFSET` keywords **Fix:** @@ -83,11 +83,13 @@ Every metric aggregation operator requires a `field` property pointing to the fi **Fix:** Pass `field` as an object property, not a bare string: +Correct: ```json -// Correct {"avg_price": {"$avg": {"field": "price"}}} +``` -// Incorrect +Incorrect — operator value is a bare string instead of an object: +```json {"avg_price": {"$avg": "price"}} ``` @@ -97,15 +99,18 @@ Every metric aggregation operator requires a `field` property pointing to the fi Aggregation '' type key value must be an object ``` -The aggregation operator value must be a JSON object, not a string or number. +The aggregation alias must map to an object containing an operator key (like `$avg`), not directly to a string or number. **Fix:** + +Correct: ```json -// Correct {"avg_price": {"$avg": {"field": "price"}}} +``` -// Incorrect -{"avg_price": {"$avg": "price"}} +Incorrect — alias maps directly to an operator string: +```json +{"avg_price": "$avg"} ``` --- From eddc802daa29cdf42346712f9615e52cdbab93cb Mon Sep 17 00:00:00 2001 From: ilterkavlak Date: Tue, 17 Mar 2026 12:33:49 +0300 Subject: [PATCH 4/4] CLOUD-4245 improve-redis-search-docs --- .../bucket-aggregations/overview.mdx | 2 +- redis/search/aggregations.mdx | 8 +++---- redis/search/command-reference.mdx | 21 ++++++++++--------- redis/search/querying.mdx | 6 +++--- redis/search/troubleshooting.mdx | 12 ++++++----- 5 files changed, 26 insertions(+), 23 deletions(-) diff --git a/redis/search/aggregation-operators/bucket-aggregations/overview.mdx b/redis/search/aggregation-operators/bucket-aggregations/overview.mdx index 84449415..09e1d894 100644 --- a/redis/search/aggregation-operators/bucket-aggregations/overview.mdx +++ b/redis/search/aggregation-operators/bucket-aggregations/overview.mdx @@ -37,7 +37,7 @@ Every bucket operator takes an object with a `field` property and operator-speci **`$dateHistogram`** — fixed time intervals: ```json -{"by_month": {"$dateHistogram": {"field": "createdAt", "calendarInterval": "month"}}} +{"by_month": {"$dateHistogram": {"field": "createdAt", "fixedInterval": "30d"}}} ``` ### Behavior Notes diff --git a/redis/search/aggregations.mdx b/redis/search/aggregations.mdx index eadd825f..7d5752af 100644 --- a/redis/search/aggregations.mdx +++ b/redis/search/aggregations.mdx @@ -95,8 +95,8 @@ result = index.aggregate( ```bash SEARCH.AGGREGATE products '{}' '{"avg_price": {"$avg": {"field": "price"}}, "by_category": {"$terms": {"field": "category", "size": 5}}}' -# Response is a flat array of alternating alias-value pairs: -# ["avg_price", 49.99, "by_category", [["electronics", ["doc_count", 42]], ["clothing", ["doc_count", 31]]]] +# Response (`redis-cli --json`) is an object keyed by alias: +# {"avg_price":{"value":49.99},"by_category":{"buckets":[{"key":"electronics","docCount":42},{"key":"clothing","docCount":31}]}} ``` @@ -104,8 +104,8 @@ SEARCH.AGGREGATE products '{}' '{"avg_price": {"$avg": {"field": "price"}}, "by_ Each SDK normalizes response keys to match its language's conventions: camelCase for TypeScript -(e.g., `docCount`) and snake_case for Python (e.g., `doc_count`). The Redis CLI returns the raw -wire format with key-value arrays. +(e.g., `docCount`) and snake_case for Python (e.g., `doc_count`). In `redis-cli --json`, Redis CLI +returns a JSON object keyed by aggregation alias. diff --git a/redis/search/command-reference.mdx b/redis/search/command-reference.mdx index a2d12627..af2d036c 100644 --- a/redis/search/command-reference.mdx +++ b/redis/search/command-reference.mdx @@ -25,7 +25,7 @@ SEARCH.CREATE ON SCHEMA [FAST] [NOSTEM] [NOTOKENIZE] [FROM ] ... ``` -**Returns:** `1` on success. Returns an error if the index already exists (unless `EXISTSOK` is used). +**Returns:** `1` on success. With `EXISTSOK`, returns `0` if an identical index already exists. Returns an error if the existing index has a different configuration. @@ -107,7 +107,7 @@ SEARCH.QUERY '' | Parameter | Description | Default | |-----------|-------------|---------| -| `LIMIT` | Maximum number of results to return. Must be between 1 and 999. | 10 | +| `LIMIT` | Maximum number of results to return. Must be between 1 and 1000. | 10 | | `OFFSET` | Number of results to skip (for pagination). | 0 | | `ORDERBY` | Sort by a FAST field in `ASC` or `DESC` order. | Sort by relevance score (descending) | | `SELECT` | Return only specific fields. Prefix with count of fields. | All fields | @@ -126,8 +126,8 @@ SEARCH.QUERY '' Each result is an array of `[key, score, content]` where: - `key` — the Redis key of the matching document -- `score` — relevance score (float as string) -- `content` — array of field-value pairs (for JSON: `[["$", ""]]`) +- `score` — relevance score (float) +- `content` — array of field-value pairs (for JSON indexes: `[["$", ""]]`; for HASH indexes: `[["field", "value"], ...]`) When `NOCONTENT` is used, the content element is omitted. When `SELECT` is used, only the selected fields appear in the content. @@ -186,15 +186,16 @@ SEARCH.AGGREGATE '' '' **Response format (Redis CLI):** -The response is a flat array of alternating alias-value pairs: +In `redis-cli --json`, the response is an object keyed by alias: -``` -["alias1", , "alias2", , ...] +```json +{ + "avg_price": { "value": 49.99 }, + "by_category": { "buckets": [{ "key": "electronics", "docCount": 42 }] } +} ``` -Where each value depends on the aggregation type: -- **Metric operators** (`$avg`, `$sum`, etc.) — a single number or stats object -- **Bucket operators** (`$terms`, `$range`, etc.) — nested arrays of key-value pairs (SDKs parse these into objects) +Raw RESP output may be rendered differently by client/protocol settings, but semantically each top-level alias maps to its aggregation result. diff --git a/redis/search/querying.mdx b/redis/search/querying.mdx index 885bb89a..7b0f923d 100644 --- a/redis/search/querying.mdx +++ b/redis/search/querying.mdx @@ -2,7 +2,7 @@ title: Queries --- -Queries are JSON strings that describe which documents to return. If the index doesn't exist, queries return an empty array. +Queries are JSON strings that describe which documents to return. If the index doesn't exist, queries return `null`. We recommend searching by field values directly because we automatically provide intelligent matching behavior out of the box: @@ -105,7 +105,7 @@ SEARCH.QUERY products '{"name": "headphones"}' When `select` / `NOCONTENT` is used, the response shape changes — see [Controlling Output](#3-controlling-output). -If the index doesn't exist or no documents match, an empty array is returned. +If no documents match, an empty array is returned. If the index doesn't exist, `null` is returned. For a detailed breakdown of the raw response structure, see the [Command Reference](/redis/search/command-reference#searchquery). @@ -137,7 +137,7 @@ Limit controls how many results to return. Offset controls how many results to s | Parameter | Description | Default | Constraints | |-----------|-------------|---------|-------------| -| `LIMIT` | Number of results to return | 10 | Must be between 1 and 999 | +| `LIMIT` | Number of results to return | 10 | Must be between 1 and 1000 | | `OFFSET` | Number of results to skip | 0 | Must be non-negative | diff --git a/redis/search/troubleshooting.mdx b/redis/search/troubleshooting.mdx index 984eef37..4f3512a8 100644 --- a/redis/search/troubleshooting.mdx +++ b/redis/search/troubleshooting.mdx @@ -10,7 +10,7 @@ Common errors and how to resolve them when working with Upstash Redis Search. You tried to create an index that already exists. -**Fix:** Use the `existsOk` option (SDK) or `EXISTSOK` flag (CLI) to skip creation if the index already exists: +**Fix:** Use the `existsOk` option (SDK) or `EXISTSOK` flag (CLI) to skip creation if an identical index already exists: ```bash SEARCH.CREATE products ON JSON PREFIX 1 product: EXISTSOK SCHEMA name TEXT @@ -18,17 +18,19 @@ SEARCH.CREATE products ON JSON PREFIX 1 product: EXISTSOK SCHEMA name TEXT Or drop the existing index first with `SEARCH.DROP `. +If the existing index has a different schema/configuration, `EXISTSOK` still returns an error. In that case, drop and recreate the index (or use a different index name). + --- ## Query Errors ### `ERR limit should be a positive number less than 1000` -The `LIMIT` value must be a positive integer up to (but not including) 1000. Valid range: 1–999. +The `LIMIT` value must be a positive integer. Valid range is `1` to `1000` (inclusive). **Common causes:** - Using `LIMIT 0` — the minimum is 1 -- Using `LIMIT 1000` or greater — the maximum is 999 +- Using `LIMIT 1001` or greater — the maximum is 1000 - Using RediSearch-style `LIMIT ` syntax — Upstash uses separate `LIMIT` and `OFFSET` keywords **Fix:** @@ -93,10 +95,10 @@ Incorrect — operator value is a bare string instead of an object: {"avg_price": {"$avg": "price"}} ``` -### Type key value must be an object +### Aggregation must be a JSON object ``` -Aggregation '' type key value must be an object +Aggregation '' must be a JSON object ``` The aggregation alias must map to an object containing an operator key (like `$avg`), not directly to a string or number.