Repository navigation
Description
Summary
Every SearchIndex.query() and AsyncSearchIndex.query() on a client created with protocol=3 raises AttributeError, for standalone and cluster clients alike. redis-py parses an FT.SEARCH reply into a dict on RESP3, and process_results reads .docs and .total from it as if it were a Result. clear(), and delete(drop=True) through it, fail the same way because they run a CountQuery.
Clients that RedisVL creates are unaffected, because RedisConnectionFactory sets protocol=2 unless the caller asks otherwise. A plain redis.Redis() or RedisCluster.from_url() with the default protocol is also unaffected, on redis-py 6.3 through 8.1. The failure needs a client passed in with protocol=3, or a URL with ?protocol=3.
Reproduction
Measured on main at 506b961b against Redis 8.4, with redis-py 6.3.0 and 8.1.0:
import redis
from redisvl.index import SearchIndex
from redisvl.query import CountQuery, FilterQuery
from redisvl.query.filter import FilterExpression, Tag
index = SearchIndex.from_dict(
{"index": {"name": "r3", "prefix": "r3"}, "fields": [{"name": "t", "type": "tag"}]},
redis_client=redis.Redis(protocol=3),
)
index.create(overwrite=True, drop=True)
index.load([{"t": "a"}])
index.query(FilterQuery(Tag("t") == "a"))
# AttributeError: 'dict' object has no attribute 'docs' (index.py:544)
index.query(CountQuery(FilterExpression("*")))
# AttributeError: 'dict' object has no attribute 'total' (index.py:483)| Client | TextQuery / FilterQuery / VectorQuery |
CountQuery, clear() |
|---|---|---|
redis.Redis(), default protocol |
works | works |
redis.Redis(protocol=3) |
fails | fails |
RedisCluster.from_url(url), default protocol |
works | works |
RedisCluster(..., protocol=3) |
fails | fails |
SearchIndex(..., redis_url=url) |
works | works |
The async clients (redis.asyncio.Redis and redis.asyncio.cluster.RedisCluster) behave the same way with AsyncSearchIndex: they work on the default protocol and fail with the same two errors on protocol=3.
On a cluster client, create() and exists() also fail on RESP3 before any query runs, because of the raw FT.INFO reply; #795 fixes that part.
Cause
_parse_batch_search_result (redisvl/index/index.py:100) already handles this for pipelined searches: when _parse_results returns a dict, it converts it with redis-py's _parse_search_resp3 where that exists, and by hand on redis-py 6.x. The single-query path through _query and cluster_search hands the dict straight to process_results.
Acceptance
query()returns the same results on aprotocol=3client as onprotocol=2, for at leastFilterQuery,VectorQueryandCountQuery, sync and async, standalone and cluster.clear()anddelete(drop=True)work on aprotocol=3client.- A regression test that fails before the fix. The cluster case needs a hermetic unit test, since cluster tests don't run on pull requests.
Activity
hyeonsang010716 commentedon Nov 6, 2026
I'm taking this one and will open a PR shortly. It converts the reply inside
search(), soquery(),paginate(),clear()and anything readingsearch().totalwork on RESP3, standalone and cluster, sync and async.While reproducing it on
930c14ewith redis-py 6.4.0, 7.4.1 and 8.1.0, I saw thatAggregateHybridQuery('dict' object has no attribute 'rows') andHybridQuery(... 'results') fail the same way on aprotocol=3client. Their replies have different shapes, so I've left them out of this PR; happy to follow up on them separately.