From e066b99d89341d7123aaea45c892f5826eaf1152 Mon Sep 17 00:00:00 2001 From: Giuseppe Zileni Date: Thu, 11 Jun 2026 09:00:37 +0200 Subject: [PATCH] fix(storage): pin Redis client to RESP2 so FT.SEARCH decodes correctly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Symptom: every /query call against a freshly-ingested index returned zero sources even when keyword and vector matches existed in the underlying Redis. Direct ``redis-cli FT.SEARCH ...`` from the host returned the expected docs; the kg-api Python path returned 0. Root cause: Redis Stack 7.2+ negotiates RESP3 by default. On RESP3 the FT.SEARCH reply is a dict: {b'total_results': N, b'results': [...], b'attributes': [...], ...} The ``ft().search()`` helper in redis-py still parses the legacy RESP2 positional list shape (``[count, doc_id, doc_data, ...]``). Faced with a dict it silently produces ``total=0, docs=[]`` — looks like "no matches" instead of a decode failure. Fix: pass ``protocol=2`` to ``aioredis.from_url`` in ``RedisVectorStore.__init__``. Forces the connection to RESP2 so the server replies with the positional list the helper expects. Verified end-to-end on the kairos market-news namespace: Q='mps' sources=5 Q='FTSE MIB' sources=7 Q='borsa italiana' sources=6 (Previously all three returned 0 with identical data in Redis.) --- knowledge-graph-api/storage/redis_vector.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/knowledge-graph-api/storage/redis_vector.py b/knowledge-graph-api/storage/redis_vector.py index 6332598..f2e90a6 100644 --- a/knowledge-graph-api/storage/redis_vector.py +++ b/knowledge-graph-api/storage/redis_vector.py @@ -17,9 +17,17 @@ class RedisVectorStore: """Async Redis vector store backed by RedisSearch.""" def __init__(self) -> None: + # Force RESP2 protocol. Redis Stack 7.2+ negotiates RESP3 by + # default, which makes the server reply to FT.SEARCH with a + # dict (``{b'total_results': N, b'results': [...]}``). The + # redis-py ``ft().search()`` helper still parses RESP2 list + # shape, so on RESP3 it silently returns ``total=0, docs=[]`` + # for queries that DO match. Pinning protocol=2 makes the + # response a positional list and search results decode correctly. self._client = aioredis.from_url( settings.REDIS_URL, decode_responses=False, + protocol=2, ) self._index_name = settings.REDIS_INDEX_NAME