Skip to content

Aggregation, hybrid search and semantic routing fail on a client configured for RESP3 #834

Description

@vishal-bala

Summary

On a client created with protocol=3, every RedisVL feature built on FT.AGGREGATE or FT.HYBRID raises AttributeError. redis-py returns the RESP3 reply as a plain dict (or, for a cursor, a list) instead of an AggregateResult or HybridResult, and RedisVL reads .rows, .cursor or .results from it as if it were the parsed object. This affects AggregateHybridQuery, MultiVectorQuery, HybridQuery, SemanticRouter route matching, and the cursor that update_by_filter and drop_by_filter use to collect keys. The sync and async paths fail the same way.

This is the aggregation counterpart of #825, which covers FT.SEARCH. As there, clients that RedisVL creates itself are unaffected, because RedisConnectionFactory sets protocol=2 unless the caller asks otherwise. The failure needs a client passed in with protocol=3, or a URL with ?protocol=3.

Reproduction

Measured on main at 87d5c177 against Redis 8.4.8, with redis-py 6.4.0, 7.4.1 and 8.1.0, on a two-document hash index with a tag, a text and a three-dimensional FLAT vector field:

import redis
from redis.commands.search.aggregation import AggregateRequest
from redisvl.index import SearchIndex
from redisvl.query import AggregateHybridQuery

index = SearchIndex.from_dict(schema, redis_client=redis.Redis(protocol=3))
index.create(overwrite=True, drop=True)
index.load(docs)

index.aggregate(AggregateRequest("*").load("@t"))
# returns a dict, not an AggregateResult

index.query(AggregateHybridQuery("hello", "txt", [0.1, 0.2, 0.3], "v"))
# AttributeError: 'dict' object has no attribute 'rows'
Call protocol 2 protocol 3
index.aggregate(AggregateRequest(...)) AggregateResult raw dict
query(AggregateHybridQuery(...)) results 'dict' object has no attribute 'rows'
query(MultiVectorQuery(...)) results 'dict' object has no attribute 'rows'
query(HybridQuery(...)), redis-py 7.4.1 and 8.1.0 results 'dict' object has no attribute 'results'
SemanticRouter.__call__ and aroute RouteMatch 'dict' object has no attribute 'rows'
update_by_filter, drop_by_filter BulkResult 'dict' object has no attribute 'total'
_iter_keys_by_filter (the bulk key cursor) keys 'list' object has no attribute 'cursor'

Every row held on all three redis-py versions, for SearchIndex with redis.Redis and AsyncSearchIndex with redis.asyncio.Redis alike. HybridQuery needs redis-py 7.1 or later, so it was not measured on 6.4.0.

update_by_filter and drop_by_filter fail first on the CountQuery they run to count matches, which is #825. Once that is fixed they will fail on the aggregate cursor instead, as the last row shows when the cursor is called directly.

The migration executor's key enumeration is not affected: it reads the raw FT.AGGREGATE reply and already handles the RESP3 map (#714).

Cause

SearchIndex.aggregate (redisvl/index/index.py:2016) and AsyncSearchIndex.aggregate (index.py:3319) return whatever ft().aggregate() gives back, and are typed as returning AggregateResult. On RESP3 redis-py skips its RESP2 parser and returns the reply map unchanged. The callers then assume the parsed shape:

  • process_aggregate_results reads results.rows (index.py:590), which every AggregationQuery goes through.
  • SemanticRouter._route_matches_from_result reads aggregation_result.rows (redisvl/extensions/router/semantic.py:521), for both the sync and async routes.
  • _iter_keys_by_filter reads result.cursor and result.rows (index.py:1850 and index.py:3165). With a cursor, the RESP3 reply comes back as a [map, cursor_id] list.
  • _hybrid_search reads results.results from hybrid_search() (index.py:2187 and index.py:3497).

Expected

  • aggregate() returns an AggregateResult on a protocol=3 client, as it does on protocol=2, so that every caller sees one shape.
  • AggregateHybridQuery, MultiVectorQuery, HybridQuery, SemanticRouter route matching and the bulk filter operations return the same results on protocol=3 as on protocol=2, sync and async.
  • A regression test that fails before the fix, covering at least one AggregationQuery, a cursor read and HybridQuery on a protocol=3 client.

Activity

  1. added theissue type on Nov 9, 2026
  2. Bhavy-Ranka commented on Nov 9, 2026

    @Bhavy-Ranka
    Contributor

    I'd like to work on this

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions