docs: add exception reference and error handling guide - #654
Conversation
Add docs/api/exceptions.rst and link it from the API toctree, so the exception hierarchy is documented in one discoverable place. The page covers the five classes in redisvl.exceptions, a table mapping each to the operations that raise it, and try/except examples for the two cases called out in the issue: schema validation on load and query validation. Each example is drawn from an actual raise site, so validate_on_load=True is shown for SchemaValidationError and ef_runtime on a flat vector field for QueryValidationError. Also notes that redis-py exceptions are not part of the hierarchy and that RedisVL chains the original error, since that is the part most likely to trip someone writing an except clause. Fixes redis#496
There was a problem hiding this comment.
Hi! Thanks for contributing to RedisVL.
This looks good overall — the page is well-organized, and I verified the exception hierarchy, table, and examples against the source without finding inaccuracies.
One thing needs fixing before merge: .. currentmodule:: sits below the prose, so 12 of the 14 :class: references render as plain text instead of links — including all five names in the "When each error is raised" table. It's a one-line move; details inline.
Worth knowing why your Definition of Done didn't catch this: your build report is accurate (I reproduced 11 warnings, none referencing exceptions.rst), but Sphinx only reports unresolved Python cross-references under -n. So make docs-build passing can't detect this class of problem — sphinx -b html -n docs docs/_build/html surfaces it.
Two smaller notes inline, both optional.
| Exception classes | ||
| ================= | ||
|
|
||
| .. currentmodule:: redisvl.exceptions |
There was a problem hiding this comment.
This directive needs to be above the prose that uses it. Sphinx resolves bare :class:`RedisVLError` against the current module, and until this line there isn't one — so it looks for a top-level RedisVLError while autodoc registered redisvl.exceptions.RedisVLError. Result: 12 unresolved refs (lines 6, 25, 39, 43, 47, 51, 55, 70, 98, 122).
Moving it to just under the page title fixes all 12 at once, and matches how schema.rst orders it. The autoclass blocks below still work, since currentmodule applies to the rest of the file.
For comparison once fixed: the ~redisvl.index.SearchIndex ref on line 60 already links correctly, because it's fully qualified.
| *********** | ||
|
|
||
| RedisVL defines its custom exceptions in ``redisvl.exceptions``. Every one of them | ||
| inherits from :class:`RedisVLError`, so catching that single base class is enough to |
There was a problem hiding this comment.
"enough to handle any error the library raises on its own behalf" holds for the operations you document, but argument validation happens in constructors, outside that net. Concretely, with the parameter this page features: VectorQuery(..., ef_runtime=-1) raises ValueError: ef_runtime must be positive, not a RedisVLError — and it fires before the try block in the line 107 example opens. A sentence noting that constructor/argument validation raises standard ValueError would set expectations well.
| ├── RedisSearchError | ||
| ├── SchemaValidationError | ||
| ├── QueryValidationError | ||
| └── RedisModuleVersionError |
There was a problem hiding this comment.
Minor completeness note: redisvl/mcp/errors.py:22 defines RedisVLMCPError(Exception), which isn't a RedisVLError subclass. Since line 5 reads as exhaustive, either scoping that sentence to the core index/query APIs or adding a pointer would keep this accurate as MCP grows.
## What this fixes
`Timestamp.__ne__`'s docstring promises: *"For date objects (without
time), this excludes the entire day."* The implementation called
`self.between(start, end)` — the **same positive range construction as
`__eq__`** — so the filter matched the entire day instead:
```python
Timestamp("created_at") == date(2023, 3, 17) # @created_at:[1679029200.0 1679115599.999999]
Timestamp("created_at") != date(2023, 3, 17) # @created_at:[1679011200.0 1679097599.999999] ← positive range!
```
Silent inverted filtering: `!=` with a date returns exactly the rows the
caller asked to exclude, with no error or warning. The non-date path of
the same method already negates correctly via `FilterOperator.NE`
(`(-@created_at:[v v])`), as do the `NE` forms of `Tag`, `Text`, `Num`,
and `Geo` — the date branch was the one exception.
There was a second, smaller asymmetry hiding in the same branch:
`__eq__` computes its day bounds with `.astimezone(timezone.utc)` while
`__ne__` used bare `datetime.combine`, so the two methods disagreed
about where the day starts on machines whose local timezone isn't UTC.
## The fix
Compute the day bounds exactly as `__eq__` does (including the
`astimezone` conversion), convert with `_convert_to_timestamp` exactly
as `between()` does, and emit through the existing
`OPERATOR_MAP[FilterOperator.NE]` template. The invariant this
establishes: **`str(ts != d)` is byte-for-byte `f"(-{ts == d})"`** on
any machine, for both `date` objects and date-only ISO strings
(`"2023-03-17"`), which take the same branch. The datetime/unix path is
untouched.
## Tests
Added `test_timestamp_not_equal_date`, which asserts the negated-range
form, the exact-negation-of-`__eq__` invariant, and the ISO-string path.
The existing suite covered `!=` only with a `datetime` (the non-date
path), which is why this never failed. The new test fails on current
`main` and passes with the fix.
`tests/unit/test_filter.py`: 63/63 pass. Full `tests/unit`: 1168 passed,
1 skipped — with 6 errors identical on unmodified `main` in my
environment (Docker-dependent CLI fixtures; no daemon locally). `black`,
`isort`, and `mypy` clean.
## Notes
- Bug-fix, so this should carry the **`auto:patch`** label per
CONTRIBUTING — I can't set labels as an outside contributor.
- Found by reading `filter.py` while working on #654 (exception docs,
still open) — happy to rebase either PR if they land in either order;
they don't overlap.
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Medium Risk**
> Changes query semantics for a previously broken filter path; callers
relying on the old (incorrect) behavior would see different results, but
the fix aligns with documented behavior and other filter types.
>
> **Overview**
> Fixes **`Timestamp != date`** (and date-only ISO strings) so they
**exclude** the full day instead of matching it. The date branch no
longer calls `between()`; it builds the same UTC day bounds as **`==`**,
converts them with `_convert_to_timestamp`, and emits a negated Redis
range via **`FilterOperator.NE`**—so `str(ts != d)` matches `(-{ts ==
d})`.
>
> Adds **`test_timestamp_not_equal_date`** to lock in the negated query
shape, the `==` / `!=` pairing, and the ISO-string path.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
1157c08. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Move currentmodule above the prose so the bare :class: references resolve against redisvl.exceptions; all 12 previously-plain names now link, verified with sphinx -n. Note that constructor and argument validation raises standard Python exceptions before any try block opens, and scope the base-class claim to the core index and query APIs, pointing at the separate RedisVLMCPError.
|
Thanks for the thorough review, all three points are in the latest commit. The currentmodule directive now sits right under the page title, and I removed the duplicate above the autoclass section since it applies to the rest of the file. Verified with the sphinx -n build you suggested: zero unresolved reference warnings for exceptions.rst now. Good to know that make docs-build can't catch this class of problem, I'll use -n for docs changes going forward. Both optional notes are in too. The intro now scopes the base-class claim to the core index and query APIs with a pointer to redisvl.mcp.errors.RedisVLMCPError, and the note block mentions that constructor validation raises standard ValueError before any try block opens, using your ef_runtime example. |
Fixes #496
Adds
docs/api/exceptions.rstand links it from the API toctree, so the exception hierarchy has one discoverable home.What's on the page
redisvl.exceptions, and a note thatredis-pyexceptions such asredis.exceptions.ConnectionErrorare not part of it. RedisVL wraps those and chains the original withraise ... from e, so__cause__still holds it. That seemed like the detail most likely to trip someone writing anexceptclause.SchemaValidationError→load(),RedisSearchError→create()/delete()/search()/aggregate().try/exceptexamples, including the two the issue asks for.:members:/:show-inheritance:pattern used inschema.rst.Examples are drawn from real raise sites
Rather than writing plausible-looking snippets, I traced each exception to where it is actually raised so the examples match current behaviour:
SchemaValidationErrorstorage.py_preprocess_and_validate_objectsvalidate_on_load=True, since validation is off by default and the error cannot otherwise occurQueryValidationErrorindex.py_validate_queryef_runtimeon aflatvector field — the concrete case that method rejectsRedisSearchErrorcreate,delete,search,aggregateRedisModuleVersionError_check_svs_support, reached fromcreate()exceptclauses, since it subclassesRedisVLErrorand notRedisSearchErrorThe
from_yaml(..., validate_on_load=True)form matches the example already in theSearchIndexdocstring.Definition of Done
docs/_build/html/api/exceptions.htmlrenders, all five classes appear with their docstrings,RedisModuleVersionError.for_svs_vamanais documented, and the page is linked from the API index as "Exceptions".make docs-buildpasses — exit 0. The build reports 11 warnings; all 11 are pre-existing and point atquery.py/aggregate.pydocstrings inherited from redis-py,index.mdheading levels, and the MCP how-to cross-references. None referenceexceptions.rst.codespellis clean on both files. No Python changed, so no behaviour change.Notes
auto:documentationlabel per CONTRIBUTING. I can't set labels as an outside contributor — could a maintainer add it?uv.lockout. Runningmake docs-buildwith a recentuvrewrites it to lockfile revision 3 (a ~250 line diff) which has nothing to do with this change.Note
Low Risk
Documentation-only change with no runtime or API behavior impact.
Overview
Adds a dedicated Exceptions API page and links it from the API toctree so
redisvl.exceptionsis easy to find.The new
exceptions.rstdocuments the five-class hierarchy underRedisVLError, clarifies thatredis-pyerrors are wrapped asRedisSearchErrorwith chaining on__cause__, and includes a table mapping each exception to typical entry points (load(),query(),create(), etc.). It also provides fourtry/exceptpatterns (schema validation on load, query validation, version vs Redis failures, and catchingRedisVLError) plus autoclass reference blocks matching the style used elsewhere in the API docs.Reviewed by Cursor Bugbot for commit 891bfe9. Bugbot is set up for automated code reviews on this repo. Configure here.