Skip to content

fix(index): classify a missing index by reply type, not wording - #818

Open
Yarmoluk wants to merge 1 commit into
redis:mainfrom
Yarmoluk:fix/exists-without-error-wording
Open

Yarmoluk wants to merge 1 commit into
redis:mainfrom
Yarmoluk:fix/exists-without-error-wording

Conversation

@Yarmoluk

@Yarmoluk Yarmoluk commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Closes #773.

Motivation

exists() decides that an FT.INFO error means "missing index" by matching the message against _MISSING_INDEX_ERROR_FRAGMENTS. A wording that isn't in the list makes exists() raise instead of returning False, and because create() calls exists() first, the index is never created. The list already has to track three Redis Search wordings across versions, so each future rewording would break exists() again until someone adds a line. This removes the list, as suggested in the issue thread.

Changes

  • _is_missing_index_error classifies by exception type: it returns True only when the underlying redis-py exception is exactly ResponseError. redis-py raises a more specific type for the replies that mean the check itself failed (NoPermissionError, ReadOnlyError, ClusterDownError, AuthenticationError, BusyLoadingError, and so on), and connection failures aren't ResponseError at all, so those still raise.
  • _MISSING_INDEX_ERROR_FRAGMENTS is gone. SearchIndex.exists, AsyncSearchIndex.exists and the MCP server's schema loader all go through the one function, so none of them change.
  • docs/api/exceptions.rst describes the type-based check instead of message matching.
  • tests/unit/test_index_exists.py: the permission case now raises NoPermissionError, which is what redis-py raises for a real NOPERM reply; ReadOnlyError and ClusterDownError join the must-raise cases; and a wording no Redis version sends joins the must-return-False cases.

Release Notes

SearchIndex.exists() and AsyncSearchIndex.exists() no longer depend on the wording of Redis Search's missing-index error, so create() keeps working if that wording changes again. On a server without Redis Search, exists() now returns False instead of raising; create() still raises the server's unknown command error.

Reviewer Brief

Start here: redisvl/exceptions.py, _is_missing_index_error. It is now one type check, and every missing-index decision in the library goes through it.

Where it could fail

  • redisvl/exceptions.py:107: any plain error reply to FT.INFO now reads as "missing". A misread can only give False, so create(overwrite=True) never drops on it; create() then fails on FT.CREATE with the server's real error. Check that no FT.INFO reply for an existing index comes back as a bare ResponseError.
  • redisvl/mcp/server.py:1001: on a server without Redis Search, the MCP server now reports "Configured Redis index ... does not exist" rather than the unknown command error.

Not verified: cluster mode (the cluster path re-raises the node's exception unchanged in redis-py, but no cluster was run); Redis 8.8's SEARCH_INDEX_NOT_FOUND reply live (covered by the unit test only).

Beyond CI: live on Redis 8.2.10 with redis-py 8.1.0: missing index → False, after create() → True, an ACL user with -@search → raises NoPermissionError, async missing index → False; on a server without Redis Search (Valkey 8), exists() → False and create() raises unknown command 'FT.CREATE'. Without the change, the two new must-return-False unit tests fail. redis-py 6.3.0 (the floor) and 8.1.0 map NOPERM, READONLY, CLUSTERDOWN, TRYAGAIN, MASTERDOWN, NOAUTH and LOADING to specific exception types.

Reviewed 87501fe · Claude Code (claude-opus-5-5) · lenses: correctness, tests, docs


Note

Medium Risk
Any future bare ResponseError from FT.INFO on an existing index would be misclassified as missing (safe for create() but could affect overwrite paths); behavior on servers without Search and MCP messaging also shifts slightly.

Overview
SearchIndex.exists() / AsyncSearchIndex.exists() no longer treat a missing index by matching Redis Search error strings. _is_missing_index_error now returns true only when the chained redis-py cause is exactly a plain ResponseError, so new Redis wordings (issue #773) still yield False instead of breaking create().

Permission, cluster, and connection failures stay distinct: redis-py’s typed errors (NoPermissionError, ReadOnlyError, ClusterDownError, etc.) and non-ResponseError failures are still re-raised, not misreported as absent. Servers without Redis Search (unknown command) also return False from exists(); create() still surfaces the real FT.CREATE error.

Docs in docs/api/exceptions.rst describe the type-based rule; unit tests add unseen missing-index wording, typed non-missing cases, and NoPermissionError for the “index named like an error phrase” scenario.

Reviewed by Cursor Bugbot for commit 87501fe. Bugbot is set up for automated code reviews on this repo. Configure here.

exists() decided an FT.INFO error meant "missing index" by matching the
message against a list of known phrasings. An unlisted phrasing made it
raise instead of returning False, and because create() checks exists()
first, the index was then never created (redis#773).

redis-py raises a bare ResponseError for an ordinary error reply and a
more specific exception for the replies that mean the check itself failed
(NoPermissionError, ReadOnlyError, ClusterDownError, ...), while
connection failures are not ResponseError at all. _is_missing_index_error
now checks for exactly a bare ResponseError, so no list of phrasings is
needed and a future rewording cannot break exists().

A misread can only produce False, never True, so create() never drops an
index because of it; on a server without Redis Search, exists() returns
False and create() raises the server's "unknown command" error.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

exists() depends on exact FT.INFO error wording; an unrecognised "missing index" message makes create() never create the index (regression from #672)

1 participant