Skip to content

fix(index): return a Result from search() on RESP3 clients - #830

Open
hyeonsang010716 wants to merge 1 commit into
redis:mainfrom
hyeonsang010716:fix/resp3-search-results
Open

hyeonsang010716 wants to merge 1 commit into
redis:mainfrom
hyeonsang010716:fix/resp3-search-results

Conversation

@hyeonsang010716

@hyeonsang010716 hyeonsang010716 commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

Motivation

Fixes #825. On a client created with protocol=3, redis-py returns the FT.SEARCH reply as a dict, and process_results reads it as a Result. query() raises AttributeError: 'dict' object has no attribute 'docs' for FilterQuery, VectorQuery, VectorRangeQuery and TextQuery, and 'total' for CountQuery. paginate(), clear() and delete(drop=True), which go through query(), fail the same way, sync and async. I reproduced it on redis-py 6.4.0, 7.4.1 and 8.1.0 against Redis 8.4.

Changes

Convert RESP3 replies in search()

SearchIndex.search() and AsyncSearchIndex.search() now pass a dict reply through the same conversion batch_search already uses for pipelined replies, so they return a Result on every protocol. Everything that reads search() gets the fix, including query(), paginate(), clear() and the migration validators that read .total. The conversion is moved out of _parse_batch_search_result into _resp3_search_to_result so both paths share it. The cluster branch goes through it too.

Notes

The conversion sits in search() rather than only in _query(), so that search() returns the Result its signature and docstring promise. FT.AGGREGATE (AggregateHybridQuery) and FT.HYBRID (HybridQuery) also fail on RESP3 clients, with 'rows' and 'results'; their replies have different shapes, so they're left for a follow-up.

Release Notes

query(), paginate(), search(), clear() and delete(drop=True) now work on SearchIndex and AsyncSearchIndex with a client created with protocol=3. On such a client, search() now returns a redis-py Result instead of the raw reply dict. AggregateHybridQuery and HybridQuery are still unsupported on RESP3 clients.

Reviewer Brief

Start here: _search_reply_to_result in redisvl/index/index.py, called from SearchIndex.search() and AsyncSearchIndex.search(). It decides when a reply is converted and rebuilds the query that the conversion needs.

Where it could fail

  • redisvl/index/index.py:2156 and :3469: code that read the raw dict from search() on a protocol=3 client, such as index.search("*")["total_results"], now gets a Result and raises TypeError.
  • redisvl/index/index.py:155: on redis-py 6.x and 7.x the reply is rebuilt by hand, and every TextQuery now goes through its WITHSCORES branch; check it against the scored TextQuery assertion in the new integration tests.

Not verified: a real Redis Cluster on RESP3 (only the mocked unit tests in tests/unit/test_search_resp3.py cover that branch, and cluster tests don't run in CI); Python versions other than 3.12; Redis Software and Redis Cloud.

Beyond CI: the new tests fail on the code before the fix (the three unit tests and both protocol-3 integration cases); removing the WITHSCORES branch of the hand-rebuilt reply fails both protocol-3 integration cases on redis-py 7.4.1.

Reviewed the working tree on 930c14e, fixes in cc03141 · Claude Code (claude-opus-5-5) · lenses: correctness, security, design, tests, docs, Redis behaviour


Note

Medium Risk
Changes the core FT.SEARCH return type on RESP3 clients and relies on redis-py private parsers; cluster RESP3 is only covered by mocks in CI.

Overview
Fixes broken query(), paginate(), clear(), and related paths when the Redis client uses RESP3 (protocol=3), where redis-py returned a raw FT.SEARCH dict instead of a Result.

SearchIndex.search() and AsyncSearchIndex.search() (standalone and cluster) now normalize dict replies through shared helpers _search_reply_to_result / _resp3_search_to_result, reusing the same logic batch_search already used via _parse_batch_search_result. On redis-py 6.x/7.x without _parse_search_resp3, the code still hand-builds a RESP2-shaped reply (including WITHSCORES for scored queries).

Integration tests cover user-provided clients across default, protocol=3, and redis-py 8 legacy_responses; unit tests mock cluster search with a RESP3-shaped reply.

Note: Callers that treated search() on RESP3 as a dict (e.g. ["total_results"]) now get a Result and may see TypeError. HybridQuery / aggregate paths on RESP3 remain out of scope.

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

A client created with protocol=3 gets FT.SEARCH replies as a dict, and
query(), paginate() and clear() read them as a Result, so they raised
AttributeError. search() now converts the dict the way batch_search
already does, for standalone and cluster clients, sync and async.

Fixes redis#825

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.

SearchIndex.query() fails on a client configured for RESP3

1 participant