From 67d95e1f49efc9f5850be9feb045d9a1c31d5a43 Mon Sep 17 00:00:00 2001
From: "redisdocsapp[bot]"
<177626021+redisdocsapp[bot]@users.noreply.github.com>
Date: Thu, 20 Aug 2026 00:20:23 +0000
Subject: [PATCH] Update for redisvl 0.26.0
---
content/develop/ai/redisvl/api/cache.md | 33 +++++-
content/develop/ai/redisvl/api/exceptions.md | 4 +
.../develop/ai/redisvl/api/message_history.md | 24 +++-
content/develop/ai/redisvl/api/router.md | 27 ++++-
.../user_guide/how_to_guides/vectorizers.md | 9 +-
.../ai/redisvl/user_guide/installation.md | 108 +++++++++++++++---
6 files changed, 182 insertions(+), 23 deletions(-)
diff --git a/content/develop/ai/redisvl/api/cache.md b/content/develop/ai/redisvl/api/cache.md
index aa136c33c7..79838c8a50 100644
--- a/content/develop/ai/redisvl/api/cache.md
+++ b/content/develop/ai/redisvl/api/cache.md
@@ -10,7 +10,7 @@ aliases:
-### `class SemanticCache(name='llmcache', distance_threshold=0.1, ttl=None, vectorizer=None, filterable_fields=None, redis_client=None, redis_url='redis://localhost:6379', connection_kwargs={}, overwrite=False, **kwargs)`
+### `class SemanticCache(name='llmcache', distance_threshold=0.1, ttl=None, vectorizer=None, filterable_fields=None, redis_client=None, redis_url='redis://localhost:6379', connection_kwargs={}, overwrite=False, create_index=True, **kwargs)`
Bases: `BaseLLMCache`
@@ -37,11 +37,38 @@ Semantic Cache for Large Language Models.
for the redis client. Defaults to empty {}.
* **overwrite** (*bool*) – Whether or not to force overwrite the schema for
the semantic cache index. Defaults to false.
+ * **create_index** (*bool*) – Whether RedisVL creates and validates the index.
+ When True, the constructor runs `FT.INFO` to check whether the
+ index exists, compares the live schema against this one, and runs
+ `FT.CREATE` if it is absent. When False it does none of these
+ and issues no index command at all: the index must already exist
+ with a compatible schema. A live index whose prefix or storage
+ type differs from this schema is not detected and produces empty
+ results rather than an error. Use this when the index is managed
+ externally, or when the credential cannot run `FT.INFO`. See
+ [Install RedisVL]({{< relref "../user_guide/installation" >}}) for the ACL details. Defaults to
+ true.
* **Raises:**
* **TypeError** – If an invalid vectorizer is provided.
* **TypeError** – If the TTL value is not an int.
* **ValueError** – If the threshold is not between 0 and 2 (Redis COSINE distance).
* **ValueError** – If existing schema does not match new schema and overwrite is False.
+ * **ValueError** – If both create_index is False and overwrite is True.
+
+```python
+from redisvl.extensions.cache.llm import SemanticCache
+
+# RedisVL creates the index if it is missing
+cache = SemanticCache(name="llmcache", redis_url="redis://localhost:6379")
+
+# the index is managed externally, or this credential cannot run
+# FT.INFO -- assume the index exists and issue no index command
+cache = SemanticCache(
+ name="llmcache",
+ redis_url="redis://localhost:6379",
+ create_index=False,
+)
+```
#### `async acheck(prompt=None, vector=None, num_results=1, return_fields=None, filter_expression=None, distance_threshold=None)`
@@ -86,7 +113,7 @@ response = await cache.acheck(
#### `async aclear()`
-Async clear the cache of all keys.
+Async clear all cache keys when RedisVL manages the index lifecycle.
* **Return type:**
None
@@ -242,7 +269,7 @@ response = cache.check(
#### `clear()`
-Clear the cache of all keys.
+Clear all cache keys when RedisVL manages the index lifecycle.
* **Return type:**
None
diff --git a/content/develop/ai/redisvl/api/exceptions.md b/content/develop/ai/redisvl/api/exceptions.md
index a3b0fc02ff..f68485b602 100644
--- a/content/develop/ai/redisvl/api/exceptions.md
+++ b/content/develop/ai/redisvl/api/exceptions.md
@@ -133,6 +133,10 @@ there raises `redis.exceptions.NoPermissionError` itself rather than a wrapped
[RedisSearchError](#redissearcherror). See [Install RedisVL]({{< relref "../user_guide/installation" >}}) for the ACL categories
RedisVL needs.
+When the credential genuinely cannot run `FT.INFO`, this error is not something to
+handle: construct the extension with `create_index=False` instead, which skips the
+existence check entirely. See [Install RedisVL]({{< relref "../user_guide/installation" >}}).
+
### `Telling "the index is missing" apart from other failures`
Redis Search reports an absent index as an ordinary error reply rather than a distinct
diff --git a/content/develop/ai/redisvl/api/message_history.md b/content/develop/ai/redisvl/api/message_history.md
index 7795571e5c..4f8b5e2064 100644
--- a/content/develop/ai/redisvl/api/message_history.md
+++ b/content/develop/ai/redisvl/api/message_history.md
@@ -10,7 +10,7 @@ aliases:
-### `class SemanticMessageHistory(name, session_tag=None, prefix=None, vectorizer=None, distance_threshold=0.3, redis_client=None, redis_url='redis://localhost:6379', connection_kwargs={}, overwrite=False, **kwargs)`
+### `class SemanticMessageHistory(name, session_tag=None, prefix=None, vectorizer=None, distance_threshold=0.3, redis_client=None, redis_url='redis://localhost:6379', connection_kwargs={}, overwrite=False, create_index=True, **kwargs)`
Bases: `BaseMessageHistory`
@@ -37,6 +37,16 @@ responses.
for the redis client. Defaults to empty {}.
* **overwrite** (*bool*) – Whether or not to force overwrite the schema for
the semantic message index. Defaults to false.
+ * **create_index** (*bool*) – Whether RedisVL creates and validates the index.
+ When False the constructor issues no index command at all: the
+ index must already exist with a compatible schema, and a live
+ index whose prefix or storage type differs is not detected –
+ which produces empty results rather than an error. See
+ [`SemanticCache`]({{< relref "cache/#semanticcache" >}}) for a worked
+ example, and [Install RedisVL]({{< relref "../user_guide/installation" >}}) for the ACL details.
+ Defaults to True.
+* **Raises:**
+ **ValueError** – If both create_index is False and overwrite is True.
The proposed schema will support a single vector embedding constructed
from either the prompt or response in a single string.
@@ -180,7 +190,7 @@ Returns the full message history.
-### `class MessageHistory(name, session_tag=None, prefix=None, redis_client=None, redis_url='redis://localhost:6379', connection_kwargs={}, **kwargs)`
+### `class MessageHistory(name, session_tag=None, prefix=None, redis_client=None, redis_url='redis://localhost:6379', connection_kwargs={}, create_index=True, **kwargs)`
Bases: `BaseMessageHistory`
@@ -202,6 +212,16 @@ responses.
* **redis_url** (*str* *,* *optional*) – The redis url. Defaults to redis://localhost:6379.
* **connection_kwargs** (*Dict* *[* *str* *,* *Any* *]*) – The connection arguments
for the redis client. Defaults to empty {}.
+ * **create_index** (*bool*) – Whether RedisVL creates the index. When False
+ the constructor issues no index command at all: the index must
+ already exist over this name and prefix. This class never
+ validates an existing index’s schema, so nothing further is
+ verified either way – and as elsewhere, a live index whose
+ prefix or storage type differs from this one is not detected and
+ produces empty results rather than an error. See
+ [`SemanticCache`]({{< relref "cache/#semanticcache" >}}) for a worked
+ example, and [Install RedisVL]({{< relref "../user_guide/installation" >}}) for the ACL details.
+ Defaults to True.
#### `add_message(message, session_tag=None)`
diff --git a/content/develop/ai/redisvl/api/router.md b/content/develop/ai/redisvl/api/router.md
index 34607cd2eb..4db99ee997 100644
--- a/content/develop/ai/redisvl/api/router.md
+++ b/content/develop/ai/redisvl/api/router.md
@@ -10,7 +10,7 @@ aliases:
## Semantic Router
-### `class SemanticRouter(name, routes, vectorizer=None, routing_config=None, redis_client=None, redis_url='redis://localhost:6379', overwrite=False, connection_kwargs={})`
+### `class SemanticRouter(name, routes, vectorizer=None, routing_config=None, redis_client=None, redis_url='redis://localhost:6379', overwrite=False, connection_kwargs={}, create_index=True)`
Semantic Router for managing and querying route vectors.
@@ -26,11 +26,30 @@ Initialize the SemanticRouter.
* **overwrite** (*bool* *,* *optional*) – Whether to overwrite existing index. Defaults to False.
* **connection_kwargs** (*Dict* *[* *str* *,* *Any* *]*) – The connection arguments
for the redis client. Defaults to empty {}.
+ * **create_index** (*bool* *,* *optional*) – Whether RedisVL creates and validates
+ the index. When False the constructor issues no index command at
+ all and writes nothing: the index must already exist, already
+ hold the reference vectors for `routes`, and already have its
+ stored config, since none of that is written or verified.
+ `routes` must match what is indexed, because each route’s
+ distance threshold is applied from this local list – and
+ [add_route](#add_route) rewrites the stored config from that same list,
+ so attaching with a partial set and then adding a route
+ truncates the config every other client reads. See
+ [`SemanticCache`]({{< relref "cache/#semanticcache" >}}) for a worked
+ example of the flag, and [Install RedisVL]({{< relref "../user_guide/installation" >}}) for the
+ ACL details. Defaults to True.
+* **Raises:**
+ **ValueError** – If both create_index is False and overwrite is True.
#### `add_route(route)`
Add a new route to the SemanticRouter.
+Note that this replaces the router’s stored config with this instance’s
+route list, so a router constructed with a subset of the indexed routes
+will drop the rest from the config that [from_existing](#from_existing) reads.
+
Embeds the route’s references, writes them to the Redis index,
appends the route to `self.routes`, and persists the updated router
config so the route survives [from_existing](#from_existing).
@@ -116,6 +135,12 @@ router = SemanticRouter.from_dict(router_data)
Return SemanticRouter instance from existing index.
+Reads the stored route config with `JSON.GET`, so unlike
+`SearchIndex.from_existing()` this needs no `FT.INFO`. Pass
+`create_index=False` to keep it that way through construction, which
+makes this the way to attach to a router with a credential that cannot
+run index-metadata commands.
+
* **Parameters:**
* **name** (*str*)
* **redis_client** (*Redis* *|* *RedisCluster* *|* *None*)
diff --git a/content/develop/ai/redisvl/user_guide/how_to_guides/vectorizers.md b/content/develop/ai/redisvl/user_guide/how_to_guides/vectorizers.md
index 87b30c8dc5..19509da296 100644
--- a/content/develop/ai/redisvl/user_guide/how_to_guides/vectorizers.md
+++ b/content/develop/ai/redisvl/user_guide/how_to_guides/vectorizers.md
@@ -139,19 +139,22 @@ The only practical difference between OpenAI and Azure OpenAI is the variables r
```python
+# NBVAL_SKIP
# additionally to the API Key, setup the API endpoint and version
api_key = os.environ.get("AZURE_OPENAI_API_KEY") or getpass.getpass("Enter your AzureOpenAI API key: ")
api_version = os.environ.get("OPENAI_API_VERSION") or getpass.getpass("Enter your AzureOpenAI API version: ")
azure_endpoint = os.environ.get("AZURE_OPENAI_ENDPOINT") or getpass.getpass("Enter your AzureOpenAI API endpoint: ")
deployment_name = os.environ.get("AZURE_OPENAI_DEPLOYMENT_NAME", "text-embedding-ada-002")
-# Skip Azure examples when required values are missing (e.g. CI or Run All without Azure).
+# Skip Azure examples when required values are missing (e.g. Run All without Azure configured).
_azure_configured = bool(azure_endpoint and api_key and api_version)
```
```python
+# NBVAL_SKIP
+# Depends on the Azure OpenAI cell above, which is not executed in CI.
from redisvl.utils.vectorize import AzureOpenAITextVectorizer
if not _azure_configured:
@@ -175,6 +178,8 @@ else:
```python
+# NBVAL_SKIP
+# Depends on the Azure OpenAI cell above, which is not executed in CI.
# Just like OpenAI, AzureOpenAI supports batching embeddings and asynchronous requests.
sentences = [
"That is a happy dog",
@@ -302,6 +307,8 @@ GCP_LOCATION=
```python
+# NBVAL_SKIP
+# Deprecated vectorizer; not executed in CI so notebook validation makes no API calls.
from redisvl.utils.vectorize import VertexAIVectorizer
diff --git a/content/develop/ai/redisvl/user_guide/installation.md b/content/develop/ai/redisvl/user_guide/installation.md
index eff9443dbf..0d2d2ae313 100644
--- a/content/develop/ai/redisvl/user_guide/installation.md
+++ b/content/develop/ai/redisvl/user_guide/installation.md
@@ -185,32 +185,106 @@ The Sentinel URL format supports:
## Redis permissions (ACLs)
-RedisVL works through Redis Search commands, so a connecting credential needs the `@search` ACL category, or the individual `FT.*` commands. Reading an index additionally requires key permissions covering its prefix: the [ACL documentation](https://redis.io/docs/latest/operate/oss_and_stack/management/security/acl/#command-categories) describes this rule for creating, modifying, and reading an index, and in practice `FT.INFO`, `FT.SEARCH`, and `FT.AGGREGATE` are denied when the index prefix falls outside the allowed key patterns. `FT.CREATE` is not checked this way, so a credential can create an index it is then unable to read.
+RedisVL reaches Redis through Redis Search commands, but not all of them need the `@search` category. Querying and loading work under an ordinary `+@read +@write` role; it is the commands that inspect or manage an index — `FT.INFO`, `FT.CREATE`, `FT._LIST` — that need `@search` or an explicit grant.
-One command needs more than `@search`. Redis tags `FT._LIST` as `@admin` as well as `@search` and `@slow`, and ACL rules are applied left to right — so a rule that grants search access and then takes back administrative commands, such as `+@search -@admin` or `+@all -@admin`, denies it:
+The command-to-category mapping below was measured against live servers rather than quoted from published documentation, which does not list ACL categories per `FT.*` command. It was identical on Redis 8.0.6, 8.2.7, 8.4.5, 8.6.4, 8.8.0 and 8.8.1. Check your own deployment with `COMMAND INFO ft.info`, `ACL CAT search`, or `ACL DRYRUN FT.INFO `.
+
+### What each operation needs
+
+| Operation | Redis command | `+@all -@admin` | `+@read +@write` |
+|-----------------------------------------------------------------------------------------------------------------|-----------------------------------------|-------------------|--------------------|
+| `index.query()`, `index.search()`, `index.aggregate()` | `FT.SEARCH`, `FT.AGGREGATE` | Yes | Yes |
+| `index.load()` | `HSET` or `JSON.SET` (needs key access) | Yes | Yes |
+| `index.exists()`, `index.info()`, `index.clear()`, `SearchIndex.from_existing()`, `rvl index info`, `rvl stats` | `FT.INFO` | Yes | **No** |
+| `index.create()` | `FT.CREATE` | Yes | **No** |
+| `index.delete()`, `rvl index delete`, `rvl index destroy` | `FT.DROPINDEX` | Yes | Yes |
+| Enumerating indexes (see below) | `FT._LIST` | **No** | **No** |
+
+Except for `FT.CREATE`, every `Yes` above assumes key patterns that cover the index prefix — see [Key permissions](). `FT.CREATE` is not checked against those patterns, so a credential can create an index it cannot query. Adding `-@dangerous` to the second column additionally denies `FT.DROPINDEX`, so `index.delete()` becomes `No`. An SVS-VAMANA schema needs more than `FT.CREATE`: `index.create()` first probes capabilities with `INFO` (`@slow @dangerous`) and `MODULE LIST` (`@admin @slow @dangerous`), so both `-@admin` and `-@dangerous` policies break creation for those schemas.
+
+Two of the rows above deserve their own explanation.
+
+`FT._LIST` is tagged `@admin` as well as `@search` and `@slow`, and ACL rules are applied left to right — so a rule that grants search access and then takes back administrative commands, such as `+@search -@admin` or `+@all -@admin`, denies it:
```text
User has no permissions to run the 'FT._LIST' command
```
-Note that `FT._LIST` does not *require* `@admin`: granting `+@search` on its own permits it. Only rules that subtract `@admin` after granting search are affected. To keep such a policy and still enumerate indexes, grant the command back explicitly with `+ft._list`.
+`FT._LIST` does not *require* `@admin`: granting `+@search` on its own permits it. Only rules that subtract `@admin` after granting search are affected. To keep such a policy and still enumerate indexes, grant the command back explicitly with `+ft._list`. Enumeration is reached by `SearchIndex.listall()` and `AsyncSearchIndex.listall()`, by `rvl index listall`, and by the migration entry points that discover indexes for you: `rvl migrate helper`, `rvl migrate wizard` when no `-i/--index` is given, and `rvl migrate batch-plan --pattern`.
+
+`FT.DROPINDEX` is tagged `@dangerous` and `@write` as well as `@search`, so a policy that subtracts `@dangerous` denies `index.delete()`, `rvl index delete`, and `rvl index destroy`.
+
+### Roles built from `@read` and `@write`
+
+`FT.INFO` is in neither `@read` nor `@write` — its only category is `@search` — and `FT.CREATE` is the same. So an application role assembled from `+@read +@write -@dangerous` — a natural least-privilege shape for a runtime that must query and load but must not manage indexes — can query and load, but cannot ask whether an index exists and cannot create or drop one. Subtracting `@dangerous` is not what denies `FT.INFO` or `FT.CREATE`: `+@all -@dangerous` permits both. Those commands are simply never granted by `@read` or `@write`.
+
+Every extension constructor checks whether its index exists, so under such a credential all of them fail while being constructed:
-| Operation | Redis command | Permitted by `+@all -@admin` |
-|-----------------------------------------------------------|------------------------------------------------------|--------------------------------|
-| `index.create()`, `index.exists()` | `FT.CREATE`, `FT.INFO` | Yes |
-| `index.query()`, `index.search()`, `index.aggregate()` | `FT.SEARCH`, `FT.AGGREGATE` | Yes |
-| `index.info()`, `rvl index info`, `rvl stats` | `FT.INFO` | Yes |
-| `index.load()` | `HSET` or `JSON.SET` (needs `@write` and key access) | Yes |
-| `index.delete()`, `rvl index delete`, `rvl index destroy` | `FT.DROPINDEX` | Yes |
-| Enumerating indexes (see below) | `FT._LIST` | No |
+```text
+RedisSearchError: Error while fetching llmcache index info:
+User has no permissions to run the 'FT.INFO' command
+```
+
+### "no permissions to run the ‘FT.INFO’ command"
+
+RedisVL does not guess its way around this. A credential that cannot ask whether the index exists also cannot create one, so there is nothing useful to infer — instead, tell RedisVL that the index is already there:
+
+```python
+cache = SemanticCache(
+ name="llmcache",
+ redis_url="redis://localhost:6379",
+ create_index=False,
+)
+```
-`SemanticCache`, `SemanticMessageHistory`, `MessageHistory`, and `SemanticRouter` each call `index.create()` while being constructed, so they are covered by the first row.
+`create_index=False` is available on `SemanticCache`, `MessageHistory`, `SemanticMessageHistory` and `SemanticRouter`. It skips the existence check, the comparison of your schema against the live index, and index creation — the constructor issues no index command at all. Pass it when the index is managed externally, or when the credential cannot run `FT.INFO`. It cannot be combined with `overwrite=True`, which asks for the opposite.
-Index enumeration is the only thing an `-@admin` rule breaks. It is reached by `SearchIndex.listall()` and `AsyncSearchIndex.listall()`, by `rvl index listall`, and by the migration entry points that discover indexes for you: `rvl migrate helper`, `rvl migrate wizard` when no `-i/--index` is given, and `rvl migrate batch-plan --pattern`.
+A `SearchIndex` used directly needs nothing special: build it with `from_dict()` or `from_yaml()`, then load and query. Two of its methods stay unavailable, because both read index metadata: `from_existing()`, which reconstructs a schema out of Redis, and `clear()`, which starts by calling `info()`.
-Other categories gate different operations. `FT.DROPINDEX` is tagged `@dangerous` and `@write` as well as `@search`, so a policy that subtracts `@dangerous` denies `index.delete()`, `rvl index delete`, and `rvl index destroy`.
+The flag also skips the SVS-VAMANA capability probe described above, since that runs inside `create()`.
-Outside of Redis Search, RedisVL identifies itself on connect with `CLIENT SETINFO` and falls back to `ECHO`. A credential permitted to run neither currently fails when the connection is created, before any index operation.
+### Provisioning a router
+
+`SemanticRouter` with `create_index=False` writes nothing at all: not the reference vectors for its routes, and not the stored route config that `SemanticRouter.from_existing()` reads. Preparing a router for this mode therefore means constructing it once with a privileged credential — a hand-written `FT.CREATE` is not enough, because the reference vectors have to be embedded and written too. Without them the router matches nothing, which looks like a distance-threshold problem rather than an empty index.
+
+Afterwards, `SemanticRouter.from_existing(name, create_index=False)` is the way to attach to it: it recovers the routes and thresholds with `JSON.GET` and needs no `FT.INFO`. The stored config must contain the full route set. Each route’s distance threshold is applied from that recovered list, so an incomplete stored config silently narrows matching — and `add_route()` and `remove_route()` rewrite the stored config from the same list, so mutating an incomplete config permanently drops the omitted routes from the config every other client reads.
+
+### When the schema diverges
+
+With `create_index=False` nothing verifies that the live index matches the schema you described. Some mismatches are loud on first use, and several are silent:
+
+| Mismatch | What happens |
+|--------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| The index does not exist | `RedisSearchError` on the first query, naming the missing index |
+| Vector dimensions disagree | `Error parsing vector similarity query: query vector blob size (32) does not match index's expected size (16)` — but only once the index holds a document. On an empty index the same query returns nothing, so a freshly provisioned index hides this until the first write lands |
+| The prefix does not cover your keys | **Silent.** Documents are written but never indexed, so queries return nothing, forever |
+| The index is `ON JSON` and you write hashes (or the reverse) | **Silent**, the same way |
+| The datatype or distance metric differs | **Silent.** Neither is restated by a query, so nothing compares them — results come back ranked by the index’s metric, not yours |
+
+For the silent cases the tell is `FT.INFO`’s `key_type`, `prefixes` and `attributes` — not `hash_indexing_failures`, which stays `0` because those keys were never indexing candidates. Diagnosing it therefore needs a credential that can run `FT.INFO`.
+
+An extension constructed with `create_index=False` refuses index-wide `delete()` and `clear()` operations (and their async cache equivalents). This protects an externally managed index — including an index reached through an alias — from being destroyed through an attach-only instance. Targeted operations such as dropping a specific cache entry or message remain available. Perform lifecycle-wide destructive operations through the privileged provisioning path that owns the index.
+
+### Key permissions
+
+Command categories are only half of it. Redis also scopes the search commands by key pattern: [the ACL documentation](https://redis.io/docs/latest/operate/oss_and_stack/management/security/acl/#command-categories) states that only users with access to a *superset* of the prefixes defined at index creation can create, modify, or read an index.
+
+Measured on 8.4.5 against an index prefixed `doc:`, with the command categories held constant at `+@all`:
+
+| Key patterns | `FT.SEARCH`, `FT.INFO`, `FT.AGGREGATE` |
+|-----------------------------------|-------------------------------------------------------------------------|
+| `~doc:*` (superset) | Permitted |
+| `%R~doc:*` (read permission only) | Permitted |
+| `~doc:1` (partial overlap) | `NOPERM User does not have the required permissions to query the index` |
+| `~other:*` (no overlap) | The same denial |
+
+Partial overlap is worth emphasising: it fails exactly like no overlap at all, rather than returning the subset you can read. `FT.CREATE` is not checked this way, so a credential can create an index it is then unable to query.
+
+`create_index=False` does not help here — the very commands it lets you avoid are joined by the ones it cannot, so widen the key patterns instead.
+
+Outside of Redis Search, RedisVL identifies itself on connect with `CLIENT SETINFO`. That command is tagged `@connection` and `@slow`, and belongs to neither `@read` nor `@write`, so a rule built up from those categories never grants it. A credential that cannot run it still connects: identification only populates the `lib-name` field that `CLIENT LIST` and `CLIENT INFO` display, so a refusal is ignored (and logged, if you have configured logging at debug level). Grant `+client|setinfo` if you want RedisVL to appear as the connecting library there — note that this labels the connection RedisVL opens, while redis-py labels the rest of the pool as plain `redis-py`.
+
+Cluster deployments need one more grant. `RedisCluster` discovers the topology with `CLUSTER SLOTS`, which is tagged `@slow` only, so a credential assembled from `+@read +@write` cannot open a clustered connection at all — redis-py reports this as `Redis Cluster cannot be connected`, with the underlying permission error chained beneath it. Grant `+cluster|slots` alongside the rules above.
### Redis Cloud and Redis Software
@@ -219,4 +293,6 @@ Both manage ACLs through their own control plane rather than the `ACL SETUSER` c
- **Redis Cloud** provides three predefined ACL rules that cannot be edited — Full-Access, Read-Write ("read and write commands and excludes dangerous commands"), and Read-Only — which you assign to a data access role. See [Configure permissions with Redis ACLs](https://redis.io/docs/latest/operate/rc/security/access-control/data-access-control/configure-acls/). Custom rules use the same syntax as above.
- **Redis Software** ships one predefined ACL, Full Access, and you define others in the Cluster Manager UI or with a [`POST /v1/redis_acls`](https://redis.io/docs/latest/operate/rs/security/access-control/create-db-roles/) request. It [does not support every `ACL` command](https://redis.io/docs/latest/operate/rs/security/access-control/redis-acl-overview/#acl-command-support), nor nested selectors, nor `(` and `)` in key patterns.
-Because the predefined rules’ exact command sets are not published, confirm a credential against the database rather than inferring what its policy name implies. Redis Software’s documentation uses `+@read +FT.INFO +FT.SEARCH` as an example rule, which is a good illustration: it permits querying and `index.exists()`, but not `index.create()` or index enumeration. Grant `FT.CREATE` explicitly when the application creates its own index, which `SemanticCache`, `SemanticMessageHistory`, `MessageHistory`, and `SemanticRouter` all do.
+Because the predefined rules’ exact command sets are not published, confirm a credential against the database rather than inferring what its policy name implies — `ACL DRYRUN FT.INFO ` answers it directly. Read the descriptions carefully before assuming you are unaffected: Read-Only allows read commands, and Read-Write "allows read and write commands and excludes dangerous commands", so both read as `@read`/`@write`-shaped — the shape that denies `FT.INFO` and `FT.CREATE` and wants `create_index=False`. Only Full-Access is clearly unaffected.
+
+Redis Software’s documentation uses `+@read +FT.INFO +FT.SEARCH` as an example rule, which is a good illustration: it permits querying and `index.exists()`, but not `index.create()` or index enumeration. Grant `FT.CREATE` explicitly when the application creates its own index. When the index is provisioned for the application instead, leave it out and construct with `create_index=False`.