From 875345694ead380b8e2e55daa99ccf81c6df0766 Mon Sep 17 00:00:00 2001 From: Doondi-Ashlesh Date: Tue, 21 Apr 2026 03:22:13 +0000 Subject: [PATCH 1/2] fix(python/redis): handle redisvl 0.5 API change in process_results and guard vector buffer decode Two bugs prevent FT.SEARCH from working in the Python Redis connector: 1. redisvl 0.5.0 changed the signature of process_results() to accept an IndexSchema instead of StorageType. Detect the active API at import time via inspect.signature and pass the appropriate argument. When IndexSchema is required, construct a minimal schema from the collection name and storage type so no search-path code needs to know the redisvl version. 2. RedisHashsetCollection._deserialize_store_models_to_dicts calls buffer_to_array() unconditionally for every vector field. When include_vectors=False (the default for search), the vector field is absent from the result dict and a KeyError is raised. Add a guard so vector decoding is skipped for absent fields. Fixes #13896 Signed-off-by: Doondi-Ashlesh --- python/semantic_kernel/connectors/redis.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/python/semantic_kernel/connectors/redis.py b/python/semantic_kernel/connectors/redis.py index 575624895aca..fa63af4adeaa 100644 --- a/python/semantic_kernel/connectors/redis.py +++ b/python/semantic_kernel/connectors/redis.py @@ -3,6 +3,7 @@ import ast import asyncio import contextlib +import inspect import json import logging import sys @@ -23,6 +24,14 @@ from redisvl.redis.utils import array_to_buffer, buffer_to_array, convert_bytes from redisvl.schema import StorageType +try: + from redisvl.schema import IndexSchema as _RedisVLIndexSchema + + _PROCESS_RESULTS_USES_SCHEMA: bool = "schema" in inspect.signature(process_results).parameters +except ImportError: + _RedisVLIndexSchema = None # type: ignore[assignment] + _PROCESS_RESULTS_USES_SCHEMA = False + from semantic_kernel.connectors.ai.embedding_generator_base import EmbeddingGeneratorBase from semantic_kernel.data.vector import ( DistanceFunction, @@ -321,7 +330,13 @@ async def _inner_search( results = await self.redis_database.ft(self.collection_name).search( # type: ignore query=query.query, query_params=query.params ) - processed = process_results(results, query, STORAGE_TYPE_MAP[self.collection_type]) + if _PROCESS_RESULTS_USES_SCHEMA and _RedisVLIndexSchema is not None: + _schema = _RedisVLIndexSchema.from_dict( + {"index": {"name": self.collection_name, "storage_type": STORAGE_TYPE_MAP[self.collection_type].value}} + ) + processed = process_results(results, query, _schema) + else: + processed = process_results(results, query, STORAGE_TYPE_MAP[self.collection_type]) return KernelSearchResults( results=self._get_vector_search_results_from_results(desync_list(processed)), total_count=results.total, @@ -616,8 +631,9 @@ def _deserialize_store_models_to_dicts( case FieldTypes.KEY: rec[field.name] = self._unget_redis_key(rec[field.name]) case "vector": - dtype = DATATYPE_MAP_VECTOR[field.type_ or "default"] - rec[field.name] = buffer_to_array(rec[field.name], dtype) + if field.name in rec: + dtype = DATATYPE_MAP_VECTOR[field.type_ or "default"] + rec[field.name] = buffer_to_array(rec[field.name], dtype) results.append(rec) return results From d15ca479d161a14bfd7e034802b54bea9bdfbc9d Mon Sep 17 00:00:00 2001 From: Doondi-Ashlesh Date: Tue, 21 Apr 2026 03:44:37 +0000 Subject: [PATCH 2/2] fix(python/redis): migrate to redisvl >= 0.5 IndexSchema API and add tests Address review feedback on the earlier compatibility shim approach: - Drop the inspect.signature dual-path. redisvl ~= 0.4 allowed 0.5.x per PEP 440, but the right fix is to express the requirement explicitly. Update pyproject.toml to redisvl >= 0.5 and use only the IndexSchema API path in _inner_search. The redundant try/except ImportError guard is removed since StorageType is already imported from the same module. - Add test_deserialize_hashset_skips_missing_vector_field: calls _deserialize_store_models_to_dicts with a record that has no vector key (as returned by search with include_vectors=False) and asserts no KeyError is raised and other fields are intact. - Add test_inner_search_passes_index_schema_to_process_results: parametrized over hashset and json, patches process_results and AsyncSearch.search, calls _inner_search with a direct vector, and asserts that the third argument to process_results is an IndexSchema with the correct storage_type - not a bare StorageType enum. Fixes #13896 Signed-off-by: Doondi-Ashlesh --- python/pyproject.toml | 2 +- python/semantic_kernel/connectors/redis.py | 22 +++------- .../connectors/memory/test_redis_store.py | 43 +++++++++++++++++++ 3 files changed, 49 insertions(+), 18 deletions(-) diff --git a/python/pyproject.toml b/python/pyproject.toml index 1c2dd95d3038..6eee39288c10 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -138,7 +138,7 @@ qdrant = [ redis = [ "redis[hiredis] >= 6,< 8", "types-redis ~= 4.6.0.20240425", - "redisvl ~= 0.4" + "redisvl >= 0.5" ] realtime = [ "websockets >= 13, < 16", diff --git a/python/semantic_kernel/connectors/redis.py b/python/semantic_kernel/connectors/redis.py index fa63af4adeaa..bff6612a5a91 100644 --- a/python/semantic_kernel/connectors/redis.py +++ b/python/semantic_kernel/connectors/redis.py @@ -3,7 +3,6 @@ import ast import asyncio import contextlib -import inspect import json import logging import sys @@ -22,15 +21,7 @@ from redisvl.query.filter import FilterExpression, Num, Tag, Text from redisvl.query.query import BaseQuery, VectorQuery from redisvl.redis.utils import array_to_buffer, buffer_to_array, convert_bytes -from redisvl.schema import StorageType - -try: - from redisvl.schema import IndexSchema as _RedisVLIndexSchema - - _PROCESS_RESULTS_USES_SCHEMA: bool = "schema" in inspect.signature(process_results).parameters -except ImportError: - _RedisVLIndexSchema = None # type: ignore[assignment] - _PROCESS_RESULTS_USES_SCHEMA = False +from redisvl.schema import IndexSchema as _RedisVLIndexSchema, StorageType from semantic_kernel.connectors.ai.embedding_generator_base import EmbeddingGeneratorBase from semantic_kernel.data.vector import ( @@ -330,13 +321,10 @@ async def _inner_search( results = await self.redis_database.ft(self.collection_name).search( # type: ignore query=query.query, query_params=query.params ) - if _PROCESS_RESULTS_USES_SCHEMA and _RedisVLIndexSchema is not None: - _schema = _RedisVLIndexSchema.from_dict( - {"index": {"name": self.collection_name, "storage_type": STORAGE_TYPE_MAP[self.collection_type].value}} - ) - processed = process_results(results, query, _schema) - else: - processed = process_results(results, query, STORAGE_TYPE_MAP[self.collection_type]) + schema = _RedisVLIndexSchema.from_dict( + {"index": {"name": self.collection_name, "storage_type": STORAGE_TYPE_MAP[self.collection_type].value}} + ) + processed = process_results(results, query, schema) return KernelSearchResults( results=self._get_vector_search_results_from_results(desync_list(processed)), total_count=results.total, diff --git a/python/tests/unit/connectors/memory/test_redis_store.py b/python/tests/unit/connectors/memory/test_redis_store.py index e779ad945a97..d58ce11638dd 100644 --- a/python/tests/unit/connectors/memory/test_redis_store.py +++ b/python/tests/unit/connectors/memory/test_redis_store.py @@ -306,3 +306,46 @@ async def test_create_index_manual(collection_hash, mock_ensure_collection_exist async def test_create_index_fail(collection_hash, mock_ensure_collection_exists): with raises(VectorStoreOperationException, match="Invalid index type supplied."): await collection_hash.ensure_collection_exists(index_definition="index_definition", fields="fields") + + +def test_deserialize_hashset_skips_missing_vector_field(collection_hash): + # Simulate search results with include_vectors=False: vector key is absent. + records = [{"id": "id1", "content": "hello"}] + result = collection_hash._deserialize_store_models_to_dicts(records) + assert len(result) == 1 + assert result[0]["id"] == "id1" + assert result[0]["content"] == "hello" + assert "vector" not in result[0] + + +@mark.parametrize("type_", ["hashset", "json"]) +async def test_inner_search_passes_index_schema_to_process_results( + collection_hash, collection_json, type_ +): + from unittest.mock import MagicMock + + from redisvl.schema import IndexSchema, StorageType + + from semantic_kernel.data.vector import SearchType, VectorSearchOptions + + collection = collection_hash if type_ == "hashset" else collection_json + expected_storage = StorageType.HASH if type_ == "hashset" else StorageType.JSON + + mock_results = MagicMock() + mock_results.docs = [] + mock_results.total = 0 + + with patch("redis.commands.search.AsyncSearch.search", new=AsyncMock(return_value=mock_results)): + with patch( + "semantic_kernel.connectors.redis.process_results", return_value=[] + ) as mock_process: + await collection._inner_search( + search_type=SearchType.VECTOR, + options=VectorSearchOptions(vector_property_name="vector", top=3), + vector=[1.0, 2.0, 3.0, 4.0, 5.0], + ) + + mock_process.assert_called_once() + _results_arg, _query_arg, schema_arg = mock_process.call_args.args + assert isinstance(schema_arg, IndexSchema), "process_results must receive an IndexSchema, not a StorageType" + assert schema_arg.index.storage_type == expected_storage