Skip to content

Python: do not report an unreachable Redis as a missing index - #14474

Open
Eric Del Orefice (ericdelorefice) wants to merge 4 commits into
microsoft:mainfrom
ericdelorefice:fix/redis-collection-exists-specific-exception
Open

Eric Del Orefice (ericdelorefice) wants to merge 4 commits into
microsoft:mainfrom
ericdelorefice:fix/redis-collection-exists-specific-exception

Conversation

@ericdelorefice

Copy link
Copy Markdown
Contributor

Motivation and Context

RedisCollection.collection_exists caught every exception and returned False, and ensure_collection_deleted gates the drop on that answer:

async def ensure_collection_deleted(self, **kwargs) -> None:
    exists = await self.collection_exists()
    if exists:
        await self.redis_database.ft(self.collection_name).dropindex(**kwargs)
    else:
        logger.debug("Collection does not exist, skipping deletion.")

collection_exists() reaches Redis to answer, so a timeout, an authentication failure or an unreachable node all return False. ensure_collection_deleted then logs at debug level and returns normally, and the caller is told the collection is gone when it is still there.

The ChromaCollection connector already draws this distinction - it handles a genuinely absent collection quietly and raises VectorStoreOperationException for anything else. This brings the Redis connector in line with it.

Fixes #14473

Description

collection_exists now catches ResponseError, which is what redis-py raises when FT.INFO reports an unknown index. ConnectionError, TimeoutError and AuthenticationError are siblings of ResponseError rather than subclasses, so they propagate instead of being reported as an absent index.

On the test change: test_collection_exists_false used a bare Exception as the side effect. Redis does not raise that, and it matched a timeout as readily as a missing index, so the test could not tell the two cases apart. It now raises ResponseError, which is the real behaviour, and a new test asserts that a connection error reaches the caller.

I checked that the new test fails without the source change, so it is testing what it claims to.

Contribution Checklist

  • The code builds clean without any errors or warnings
  • I didn't break anyone 😄
  • I have added tests that prove my fix is effective
  • All unit tests pass locally: pytest tests/unit/connectors/memory/test_redis_store.py passes 27/27

RedisCollection.collection_exists caught every exception and returned
False. ensure_collection_deleted gates the drop on that answer, so a
timeout, an authentication failure or an unreachable node made it log at
debug level and return successfully having deleted nothing. The caller
is told the collection is gone when it is not.

Catch ResponseError, which is what redis-py raises when FT.INFO reports
an unknown index. ConnectionError, TimeoutError and AuthenticationError
are siblings of ResponseError rather than subclasses, so they now
propagate.

The ChromaCollection connector already distinguishes these two cases:
it handles a genuinely absent collection quietly and raises
VectorStoreOperationException for anything else.

The existing test used a bare Exception, which is not what Redis raises
and matched a timeout just as readily. It now uses ResponseError, and a
new test asserts a connection error reaches the caller.

Fixes microsoft#14473
@ericdelorefice

Copy link
Copy Markdown
Contributor Author

@microsoft-github-policy-service agree

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Redis imports are incorrectly ordered in both reviewed files and will fail configured Ruff checks.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 2 High severity

Open (2)
What changed in this PR

This pull request ensures Redis connection failures are not reported as missing indexes.

Changes:

  • Handles ResponseError for missing indexes.
  • Adds tests for connection-error propagation.
File Summary
python/​semantic_kernel/​connectors/​redis.py Updates Redis index existence error handling.
python/​tests/​unit/​connectors/​memory/​test_redis_store.py Adds targeted exception tests.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines 17 to 20
from redis.exceptions import ResponseError
from redis.commands.search.field import Field as RedisField
from redis.commands.search.field import NumericField, TagField, TextField, VectorField
from redis.commands.search.index_definition import IndexDefinition, IndexType
Comment on lines +15 to +16
from redis.exceptions import ConnectionError as RedisConnectionError
from redis.exceptions import ResponseError
redis.exceptions sorts after redis.commands, and in the test file the
new imports split the third-party block from the semantic_kernel one.
Ruff's isort gate is enabled in pyproject.toml, so both would have
failed lint.

ruff check and ruff format now pass on both files, and the unit tests
still pass 27/27.
@ericdelorefice

Copy link
Copy Markdown
Contributor Author

Good catch on both, and you are right. Fixed and pushed.

redis.exceptions sorts after redis.commands, so the connector import was in the wrong place within the third-party group, and in the test file the two new imports split the third-party block from the semantic_kernel one. Ruff's isort gate is enabled in python/pyproject.toml, so both would have failed lint rather than merely looking untidy.

ruff check and ruff format --check now pass on both files, and pytest tests/unit/connectors/memory/test_redis_store.py still passes 27/27.

One correction on the report itself: the second instance is at line 20 of the test file, not line 291 of the connector. Line 291 is the collection_exists change, which has no import in it.

Mangesh Raut (mangeshraut712) pushed a commit to mangeshraut712/semantic-kernel that referenced this pull request Oct 28, 2026
…deleted too little (microsoft#14510)

### Motivation and Context

`ChromaCollection` has two handlers for the same condition, and the
error type they were written against changed underneath them.

`pyproject.toml` allows `chromadb >= 0.5, < 1.6`. Across that range the
error for a missing collection is not the same:

```
chromadb 0.5.0   errors.py defines NotFoundError: False
chromadb 1.5.9   client.get_collection("no_such")   -> chromadb.errors.NotFoundError
                 isinstance(e, ValueError)          -> False
```

Both symptoms follow from that one fact, and they point in opposite
directions.

**`collection_exists` catches too much.** `except Exception: return
False`, so an unreachable server, a rejected request or a timeout all
report that the collection is not there. Three places in this repo
already say that is not the rule. `_get_collection`, nine lines above,
raises `RuntimeError` from the identical `client.get_collection(...)`
call. `ensure_collection_deleted`, in the same class, raises
`VectorStoreOperationException` for anything that is not a missing
collection. And `VectorStore.collection_exists` in `data/vector.py`
wraps this method and narrows to `except VectorStoreOperationException`,
so the layer above already assumes only a recognised failure becomes
`False`.

**`ensure_collection_deleted` catches too little.** Its `except
ValueError` is the 0.5-era signal. On chromadb 1.x a genuinely absent
collection raises `NotFoundError`, which is not a `ValueError`, so it
falls through to `except Exception` and is re-raised as
`VectorStoreOperationException`. The "could not be deleted because it
doesn't exist" path is unreachable on any chromadb 1.x, which is most of
the supported range.

### Description

One module-level tuple resolves the version difference once:

```python
try:  # chromadb >= 1.0 raises NotFoundError for a missing collection
    from chromadb.errors import NotFoundError as _ChromaNotFoundError

    COLLECTION_NOT_FOUND: tuple[type[Exception], ...] = (ValueError, _ChromaNotFoundError)
except ImportError:  # chromadb < 1.0 raised ValueError
    COLLECTION_NOT_FOUND = (ValueError,)
```

`collection_exists` narrows from `Exception` to that tuple.
`ensure_collection_deleted` widens from `ValueError` to the same tuple.
Nothing else changes, and the guarded import keeps `chromadb 0.5`
working, where `NotFoundError` does not exist.

### Contribution Checklist

Four unit tests, no Chroma server. They resolve the not-found error
themselves rather than importing the new constant, so the same file runs
against the code before and after this change.

Against unmodified `main`, two fail - one per symptom - and two pass:

```
FAILED test_collection_exists_raises_when_the_server_cannot_be_reached
FAILED test_ensure_collection_deleted_tolerates_an_absent_collection
```

The two that pass on `main` pin what must not move: a not-found still
returns `False`, and a real failure during delete still raises.

```
17 passed                   # tests/unit/connectors/memory/test_chroma.py
All checks passed!          # ruff check
2 files already formatted   # ruff format --check
```

### Note

This is the connector I named as the correct one in microsoft#14473, and it is
correct in `ensure_collection_deleted`, which was the method that issue
was about. `collection_exists` underneath it is not, which I only saw
when I went back to check the claim.

`RedisCollection.collection_exists` has the broad-catch half of this and
is already covered by microsoft#14474. `CosmosNoSqlCollection.collection_exists`
catches `CosmosHttpResponseError`, which includes 401, 403 and 5xx, so
it may want the same narrowing - left out here because I have not read
it closely enough to say.

This branch was successfully deployed

1 active deployment
github-app-auth — 44d07ace Deployed Oct 29, 2026 by ericdelorefice via add_label #29478
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

python Pull requests for the Python Semantic Kernel

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Python: RedisCollection.ensure_collection_deleted can return successfully without deleting the index

3 participants