# Copyright (c) Microsoft. All rights reserved. import ast import asyncio import contextlib import json import logging import sys from abc import abstractmethod from collections.abc import MutableSequence, Sequence from copy import copy from enum import Enum from typing import Any, ClassVar, Final, Generic, TypeVar from pydantic import SecretStr, ValidationError from redis.asyncio.client import Redis from redis.commands.search.field import Field as RedisField from redis.commands.search.field import NumericField, TagField, TextField, VectorField from redis.commands.search.index_definition import IndexDefinition, IndexType from redisvl.index.index import process_results 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 from semantic_kernel.connectors.ai.embedding_generator_base import EmbeddingGeneratorBase from semantic_kernel.data.vector import ( DistanceFunction, FieldTypes, GetFilteredRecordOptions, IndexKind, KernelSearchResults, SearchType, TModel, VectorSearch, VectorSearchOptions, VectorSearchResult, VectorStore, VectorStoreCollection, VectorStoreCollectionDefinition, VectorStoreField, ) from semantic_kernel.exceptions import ( VectorSearchExecutionException, VectorSearchOptionsException, VectorStoreInitializationException, VectorStoreOperationException, ) from semantic_kernel.kernel_pydantic import KernelBaseSettings from semantic_kernel.utils.feature_stage_decorator import release_candidate from semantic_kernel.utils.list_handler import desync_list if sys.version_info >= (3, 12): from typing import override # pragma: no cover else: from typing_extensions import override # pragma: no cover logger = logging.getLogger(__name__) TKey = TypeVar("TKey", bound=str) TQuery = TypeVar("TQuery", bound=BaseQuery) class RedisCollectionTypes(str, Enum): """Redis collection types.""" JSON = "json" HASHSET = "hashset" STORAGE_TYPE_MAP: Final[dict[RedisCollectionTypes, StorageType]] = { RedisCollectionTypes.JSON: StorageType.JSON, RedisCollectionTypes.HASHSET: StorageType.HASH, } DISTANCE_FUNCTION_MAP: Final[dict[DistanceFunction, str]] = { DistanceFunction.COSINE_SIMILARITY: "COSINE", DistanceFunction.DOT_PROD: "IP", DistanceFunction.EUCLIDEAN_DISTANCE: "L2", DistanceFunction.DEFAULT: "COSINE", } INDEX_KIND_MAP: Final[dict[IndexKind, str]] = { IndexKind.HNSW: "HNSW", IndexKind.FLAT: "FLAT", IndexKind.DEFAULT: "HNSW", } INDEX_TYPE_MAP: Final[dict[RedisCollectionTypes, IndexType]] = { RedisCollectionTypes.JSON: IndexType.JSON, RedisCollectionTypes.HASHSET: IndexType.HASH, } DATATYPE_MAP_VECTOR: Final[dict[str, str]] = { "float": "FLOAT32", "int": "FLOAT16", "binary": "FLOAT16", "ndarray": "FLOAT32", "default": "FLOAT32", } def _field_to_redis_field_hashset(name: str, field: VectorStoreField) -> RedisField: if field.field_type == FieldTypes.VECTOR: if field.distance_function not in DISTANCE_FUNCTION_MAP: raise VectorStoreOperationException( f"Distance function {field.distance_function} is not supported. " f"Supported functions are: {list(DISTANCE_FUNCTION_MAP.keys())}" ) if field.index_kind not in INDEX_KIND_MAP: raise VectorStoreOperationException( f"Index kind {field.index_kind} is not supported. Supported kinds are: {list(INDEX_KIND_MAP.keys())}" ) return VectorField( name=name, algorithm=INDEX_KIND_MAP[field.index_kind], attributes={ "type": DATATYPE_MAP_VECTOR[field.type_ or "default"], "dim": field.dimensions, "distance_metric": DISTANCE_FUNCTION_MAP[field.distance_function], }, ) if field.type_ in ["int", "float"]: return NumericField(name=name) if field.is_full_text_indexed: return TextField(name=name) return TagField(name=name) def _field_to_redis_field_json(name: str, field: VectorStoreField) -> RedisField: if field.field_type == FieldTypes.VECTOR: if field.distance_function not in DISTANCE_FUNCTION_MAP: raise VectorStoreOperationException( f"Distance function {field.distance_function} is not supported. " f"Supported functions are: {list(DISTANCE_FUNCTION_MAP.keys())}" ) if field.index_kind not in INDEX_KIND_MAP: raise VectorStoreOperationException( f"Index kind {field.index_kind} is not supported. Supported kinds are: {list(INDEX_KIND_MAP.keys())}" ) return VectorField( name=f"$.{name}", algorithm=INDEX_KIND_MAP[field.index_kind], attributes={ "type": DATATYPE_MAP_VECTOR[field.type_ or "default"], "dim": field.dimensions, "distance_metric": DISTANCE_FUNCTION_MAP[field.distance_function], }, as_name=name, ) if field.type_ in ["int", "float"]: return NumericField(name=f"$.{name}", as_name=name) if field.is_full_text_indexed: return TextField(name=f"$.{name}", as_name=name) return TagField(name=f"$.{name}", as_name=name) def _definition_to_redis_fields( definition: VectorStoreCollectionDefinition, collection_type: RedisCollectionTypes ) -> list[RedisField]: """Create a list of fields for Redis from a definition.""" fields: list[RedisField] = [] for field in definition.fields: if field.field_type == FieldTypes.KEY: continue if collection_type == RedisCollectionTypes.HASHSET: fields.append(_field_to_redis_field_hashset(field.storage_name or field.name, field)) # type: ignore elif collection_type == RedisCollectionTypes.JSON: fields.append(_field_to_redis_field_json(field.storage_name or field.name, field)) # type: ignore return fields @release_candidate class RedisSettings(KernelBaseSettings): """Redis model settings. Args: - connection_string (str | None): Redis connection string (Env var REDIS_CONNECTION_STRING) """ env_prefix: ClassVar[str] = "REDIS_" connection_string: SecretStr @release_candidate class RedisCollection( VectorStoreCollection[TKey, TModel], VectorSearch[TKey, TModel], Generic[TKey, TModel], ): """A vector store record collection implementation using Redis.""" redis_database: Redis prefix_collection_name_to_key_names: bool collection_type: RedisCollectionTypes supported_key_types: ClassVar[set[str] | None] = {"str"} supported_vector_types: ClassVar[set[str] | None] = {"float"} supported_search_types: ClassVar[set[SearchType]] = {SearchType.VECTOR} def __init__( self, record_type: type[TModel], definition: VectorStoreCollectionDefinition | None = None, collection_name: str | None = None, embedding_generator: EmbeddingGeneratorBase | None = None, redis_database: Redis | None = None, prefix_collection_name_to_key_names: bool = True, collection_type: RedisCollectionTypes = RedisCollectionTypes.HASHSET, connection_string: str | None = None, env_file_path: str | None = None, env_file_encoding: str | None = None, **kwargs: Any, ) -> None: """RedisMemoryStore is an abstracted interface to interact with a Redis node connection. See documentation about connections: https://redis-py.readthedocs.io/en/stable/connections.html See documentation about vector attributes: https://redis.io/docs/stack/search/reference/vectors. """ if redis_database: super().__init__( record_type=record_type, definition=definition, collection_name=collection_name, embedding_generator=embedding_generator, redis_database=redis_database, prefix_collection_name_to_key_names=prefix_collection_name_to_key_names, collection_type=collection_type, managed_client=False, **kwargs, ) return try: redis_settings = RedisSettings( connection_string=connection_string, env_file_path=env_file_path, env_file_encoding=env_file_encoding, ) except ValidationError as ex: raise VectorStoreInitializationException("Failed to create Redis settings.", ex) from ex super().__init__( record_type=record_type, definition=definition, collection_name=collection_name, embedding_generator=embedding_generator, redis_database=Redis.from_url(redis_settings.connection_string.get_secret_value()), prefix_collection_name_to_key_names=prefix_collection_name_to_key_names, collection_type=collection_type, **kwargs, ) def _get_redis_key(self, key: TKey) -> TKey: if self.prefix_collection_name_to_key_names: return f"{self.collection_name}:{key}" # type: ignore return key def _unget_redis_key(self, key: TKey) -> TKey: if self.prefix_collection_name_to_key_names and ":" in key: return key[len(self.collection_name) + 1 :] # type: ignore return key @override async def ensure_collection_exists(self, **kwargs) -> None: """Create a new index in Redis. Args: **kwargs: Additional keyword arguments. fields (list[Fields]): The fields to create the index with, when not supplied, these are created from the definition. index_definition (IndexDefinition): The search index to create, if this is supplied this is used instead of a index created based on the definition. other kwargs are passed to the create_index method. """ if (index_definition := kwargs.pop("index_definition", None)) and (fields := kwargs.pop("fields", None)): if isinstance(index_definition, IndexDefinition): await self.redis_database.ft(self.collection_name).create_index( fields, definition=index_definition, **kwargs ) return raise VectorStoreOperationException("Invalid index type supplied.") fields = _definition_to_redis_fields(self.definition, self.collection_type) index_definition = IndexDefinition( prefix=f"{self.collection_name}:", index_type=INDEX_TYPE_MAP[self.collection_type] ) await self.redis_database.ft(self.collection_name).create_index(fields, definition=index_definition, **kwargs) @override async def collection_exists(self, **kwargs) -> bool: try: await self.redis_database.ft(self.collection_name).info() return True except Exception: return False @override async def ensure_collection_deleted(self, **kwargs) -> None: exists = await self.collection_exists() if exists: await self.redis_database.ft(self.collection_name).dropindex(**kwargs) else: logger.debug("Collection does not exist, skipping deletion.") @override async def __aexit__(self, exc_type, exc_value, traceback) -> None: """Exit the context manager.""" if self.managed_client: await self.redis_database.aclose() # type: ignore @override async def _inner_search( self, search_type: SearchType, options: VectorSearchOptions, values: Any | None = None, vector: Sequence[float | int] | None = None, **kwargs: Any, ) -> KernelSearchResults[VectorSearchResult[TModel]]: if not vector: vector = await self._generate_vector_from_values(values, options) if not vector: raise VectorSearchExecutionException("No vector found.") query = self._construct_vector_query(vector, options, **kwargs) 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]) return KernelSearchResults( results=self._get_vector_search_results_from_results(desync_list(processed)), total_count=results.total, ) def _construct_vector_query( self, vector: Sequence[float | int], options: VectorSearchOptions, **kwargs: Any ) -> VectorQuery: vector_field = self.definition.try_get_vector_field(options.vector_property_name) if not vector_field: raise VectorSearchOptionsException("Vector field not found.") query = VectorQuery( vector=vector, # type: ignore vector_field_name=vector_field.storage_name or vector_field.name, # type: ignore num_results=options.top + options.skip, dialect=2, return_score=True, ) if filter := self._build_filter(options.filter): # type: ignore if isinstance(filter, list): expr = filter[0] for v in filter[1:]: expr = expr & v query.set_filter(expr) else: query.set_filter(filter) query.paging(offset=options.skip, num=options.top + options.skip) query.sort_by( query.DISTANCE_ID, asc=(vector_field.distance_function) in [ DistanceFunction.COSINE_SIMILARITY, DistanceFunction.DOT_PROD, DistanceFunction.DEFAULT, ], ) return self._add_return_fields(query, options.include_vectors) @override def _lambda_parser(self, node: ast.AST) -> FilterExpression: """Parse the lambda AST and return a RedisVL FilterExpression.""" def get_field_expr(field_name): # Find the field in the data model field = next( (f for f in self.definition.fields if (f.storage_name or f.name) == field_name), None, ) if field is None: raise VectorStoreOperationException(f"Field '{field_name}' not found in data model.") if field.field_type == FieldTypes.DATA: if field.is_full_text_indexed: return lambda: Text(field_name) if field.type_ in ("int", "float"): return lambda: Num(field_name) return lambda: Tag(field_name) if field.field_type == FieldTypes.VECTOR: raise VectorStoreOperationException(f"Cannot filter on vector field '{field_name}'.") return lambda: Tag(field_name) match node: case ast.Compare(): if len(node.ops) > 1: # Chain comparisons (e.g., 1 < x < 3) become & of each comparison expr = None for idx in range(len(node.ops)): left = node.left if idx == 0 else node.comparators[idx - 1] right = node.comparators[idx] op = node.ops[idx] sub = self._lambda_parser(ast.Compare(left=left, ops=[op], comparators=[right])) expr = expr & sub if expr else sub return expr left = node.left right = node.comparators[0] op = node.ops[0] # Only support field op value or value op field if isinstance(left, (ast.Attribute, ast.Name)): field_name = left.attr if isinstance(left, ast.Attribute) else left.id field_expr = get_field_expr(field_name)() value = self._lambda_parser(right) match op: case ast.Eq(): return field_expr == value case ast.NotEq(): return field_expr != value case ast.Gt(): return field_expr > value case ast.GtE(): return field_expr >= value case ast.Lt(): return field_expr < value case ast.LtE(): return field_expr <= value case ast.In(): return field_expr == value # Tag/Text/Num support list equality case ast.NotIn(): return ~(field_expr == value) raise NotImplementedError(f"Unsupported operator: {type(op)}") if isinstance(right, (ast.Attribute, ast.Name)): # Reverse: value op field field_name = right.attr if isinstance(right, ast.Attribute) else right.id field_expr = get_field_expr(field_name)() value = self._lambda_parser(left) match op: case ast.Eq(): return field_expr == value case ast.NotEq(): return field_expr != value case ast.Gt(): return field_expr < value case ast.GtE(): return field_expr <= value case ast.Lt(): return field_expr > value case ast.LtE(): return field_expr >= value case ast.In(): return field_expr == value case ast.NotIn(): return ~(field_expr == value) raise NotImplementedError(f"Unsupported operator: {type(op)}") raise NotImplementedError("Comparison must be between a field and a value.") case ast.BoolOp(): op = node.op # type: ignore values = [self._lambda_parser(v) for v in node.values] if isinstance(op, ast.And): expr = values[0] for v in values[1:]: expr = expr & v return expr if isinstance(op, ast.Or): expr = values[0] for v in values[1:]: expr = expr | v return expr raise NotImplementedError(f"Unsupported BoolOp: {type(op)}") case ast.UnaryOp(): match node.op: case ast.Not(): operand = self._lambda_parser(node.operand) return ~operand case ast.UAdd() | ast.USub() | ast.Invert(): raise NotImplementedError("Unary +, -, ~ are not supported in RedisVL filters.") case ast.Attribute(): # Only allow attributes that are in the data model if node.attr not in self.definition.storage_names: raise VectorStoreOperationException( f"Field '{node.attr}' not in data model (storage property names are used)." ) return node.attr case ast.Name(): # Only allow names that are in the data model if node.id not in self.definition.storage_names: raise VectorStoreOperationException( f"Field '{node.id}' not in data model (storage property names are used)." ) return node.id case ast.Constant(): return node.value raise NotImplementedError(f"Unsupported AST node: {type(node)}") @abstractmethod def _add_return_fields(self, query: TQuery, include_vectors: bool) -> TQuery: """Add the return fields to the query. There is a difference between the JSON and Hashset collections, this method should be overridden by the subclasses. """ pass @override def _get_record_from_result(self, result: dict[str, Any]) -> Any: """Get a record from a result.""" ret = result.copy() ret.pop("vector_distance", None) for key, value in ret.items(): with contextlib.suppress(json.JSONDecodeError): ret[key] = json.loads(value) if isinstance(value, str) else value return ret @override def _get_score_from_result(self, result: dict[str, Any]) -> float | None: return result.get("vector_distance") @release_candidate class RedisHashsetCollection(RedisCollection[TKey, TModel], Generic[TKey, TModel]): """A vector store record collection implementation using Redis Hashsets.""" def __init__( self, record_type: type[TModel], definition: VectorStoreCollectionDefinition | None = None, collection_name: str | None = None, embedding_generator: EmbeddingGeneratorBase | None = None, redis_database: Redis | None = None, prefix_collection_name_to_key_names: bool = False, connection_string: str | None = None, env_file_path: str | None = None, env_file_encoding: str | None = None, **kwargs: Any, ) -> None: """RedisMemoryStore is an abstracted interface to interact with a Redis node connection. See documentation about connections: https://redis-py.readthedocs.io/en/stable/connections.html See documentation about vector attributes: https://redis.io/docs/stack/search/reference/vectors. """ super().__init__( record_type=record_type, definition=definition, collection_name=collection_name, embedding_generator=embedding_generator, redis_database=redis_database, prefix_collection_name_to_key_names=prefix_collection_name_to_key_names, collection_type=RedisCollectionTypes.HASHSET, connection_string=connection_string, env_file_path=env_file_path, env_file_encoding=env_file_encoding, **kwargs, ) @override async def _inner_upsert(self, records: Sequence[Any], **kwargs: Any) -> Sequence[TKey]: return await asyncio.gather(*[self._single_upsert(record) for record in records]) async def _single_upsert(self, upsert_record: Any) -> TKey: await self.redis_database.hset(**upsert_record) return self._unget_redis_key(upsert_record["name"]) @override async def _inner_get( self, keys: Sequence[TKey] | None = None, options: GetFilteredRecordOptions | None = None, **kwargs, ) -> Sequence[dict[str, Any]] | None: if not keys: if options is not None: raise NotImplementedError("Get without keys is not yet implemented.") return None results = await asyncio.gather(*[self._single_get(self._get_redis_key(key)) for key in keys]) return [result for result in results if result] async def _single_get(self, key: str) -> dict[str, Any] | None: result = await self.redis_database.hgetall(key) if result: result = convert_bytes(result) result[self.definition.key_name] = key return result @override async def _inner_delete(self, keys: Sequence[TKey], **kwargs: Any) -> None: await self.redis_database.delete(*[self._get_redis_key(key) for key in keys]) @override def _serialize_dicts_to_store_models( self, records: Sequence[dict[str, Any]], **kwargs: Any, ) -> Sequence[dict[str, Any]]: """Serialize the dict to a Redis store model.""" results: MutableSequence[dict[str, Any]] = [] for record in records: result: dict[str, Any] = {"mapping": {}} for field in self.definition.fields: if field.field_type == FieldTypes.VECTOR: dtype = DATATYPE_MAP_VECTOR[field.type_ or "default"].lower() result["mapping"][field.storage_name or field.name] = array_to_buffer(record[field.name], dtype) continue if field.field_type == FieldTypes.KEY: result["name"] = self._get_redis_key(record[field.name]) continue result["mapping"][field.storage_name or field.name] = record[field.name] results.append(result) return results @override def _deserialize_store_models_to_dicts( self, records: Sequence[dict[str, Any]], **kwargs: Any, ) -> Sequence[dict[str, Any]]: results = [] for record in records: rec = record.copy() for field in self.definition.fields: match field.field_type: 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) results.append(rec) return results def _add_return_fields(self, query: TQuery, include_vectors: bool) -> TQuery: """Add the return fields to the query. For a Hashset index this should not be decoded, that is the only difference between this and the JSON collection. """ for field in self.definition.fields: match field.field_type: case "vector": if include_vectors: query.return_field(field.name, decode_field=False) case _: query.return_field(field.name) return query @release_candidate class RedisJsonCollection(RedisCollection[TKey, TModel], Generic[TKey, TModel]): """A vector store record collection implementation using Redis Json.""" def __init__( self, record_type: type[TModel], definition: VectorStoreCollectionDefinition | None = None, collection_name: str | None = None, embedding_generator: EmbeddingGeneratorBase | None = None, redis_database: Redis | None = None, prefix_collection_name_to_key_names: bool = False, connection_string: str | None = None, env_file_path: str | None = None, env_file_encoding: str | None = None, **kwargs: Any, ) -> None: """RedisMemoryStore is an abstracted interface to interact with a Redis node connection. See documentation about connections: https://redis-py.readthedocs.io/en/stable/connections.html See documentation about vector attributes: https://redis.io/docs/stack/search/reference/vectors. """ super().__init__( record_type=record_type, definition=definition, collection_name=collection_name, redis_database=redis_database, prefix_collection_name_to_key_names=prefix_collection_name_to_key_names, collection_type=RedisCollectionTypes.JSON, connection_string=connection_string, env_file_path=env_file_path, env_file_encoding=env_file_encoding, embedding_generator=embedding_generator, **kwargs, ) @override async def _inner_upsert(self, records: Sequence[Any], **kwargs: Any) -> Sequence[TKey]: return await asyncio.gather(*[self._single_upsert(record) for record in records]) async def _single_upsert(self, upsert_record: Any) -> TKey: await self.redis_database.json().set(upsert_record["name"], "$", upsert_record["value"]) return self._unget_redis_key(upsert_record["name"]) @override async def _inner_get( self, keys: Sequence[TKey] | None = None, options: GetFilteredRecordOptions | None = None, **kwargs, ) -> Sequence[dict[str, Any]] | None: if not keys: if options is not None: raise NotImplementedError("Get without keys is not yet implemented.") return None kwargs_copy = copy(kwargs) kwargs_copy.pop("include_vectors", None) redis_keys = [self._get_redis_key(key) for key in keys] results = await self.redis_database.json().mget(redis_keys, "$", **kwargs_copy) return [self._add_key(key, result[0]) for key, result in zip(redis_keys, results) if result] def _add_key(self, key: TKey, record: dict[str, Any]) -> dict[str, Any]: record[self.definition.key_name] = key return record @override async def _inner_delete(self, keys: Sequence[str], **kwargs: Any) -> None: await asyncio.gather(*[self.redis_database.json().delete(key, **kwargs) for key in keys]) @override def _serialize_dicts_to_store_models( self, records: Sequence[dict[str, Any]], **kwargs: Any, ) -> Sequence[dict[str, Any]]: """Serialize the dict to a Redis store model.""" results: MutableSequence[dict[str, Any]] = [] for record in records: result: dict[str, Any] = {"value": {}} for field in self.definition.fields: if field.field_type == FieldTypes.KEY: result["name"] = self._get_redis_key(record[field.name]) continue if field.field_type == "vector": result["value"][field.storage_name or field.name] = record[field.name] result["value"][field.storage_name or field.name] = record[field.name] results.append(result) return results @override def _deserialize_store_models_to_dicts( self, records: Sequence[dict[str, Any]], **kwargs: Any, ) -> Sequence[dict[str, Any]]: results = [] key_field_name = self.definition.key_name for record in records: rec = record.copy() rec[key_field_name] = self._unget_redis_key(record[key_field_name]) results.append(rec) return results def _add_return_fields(self, query: TQuery, include_vectors: bool) -> TQuery: """Add the return fields to the query.""" for field in self.definition.fields: match field.field_type: case FieldTypes.VECTOR: if include_vectors: query.return_field(field.name) case _: query.return_field(field.name) return query @release_candidate class RedisStore(VectorStore): """Create a Redis Vector Store.""" redis_database: Redis def __init__( self, connection_string: str | None = None, embedding_generator: EmbeddingGeneratorBase | None = None, env_file_path: str | None = None, env_file_encoding: str | None = None, redis_database: Redis | None = None, **kwargs: Any, ) -> None: """RedisMemoryStore is an abstracted interface to interact with a Redis instance.""" if redis_database: super().__init__( redis_database=redis_database, embedding_generator=embedding_generator, **kwargs, ) return try: redis_settings = RedisSettings( connection_string=connection_string, env_file_path=env_file_path, env_file_encoding=env_file_encoding, ) except ValidationError as ex: raise VectorStoreInitializationException("Failed to create Redis settings.", ex) from ex super().__init__( redis_database=Redis.from_url(redis_settings.connection_string.get_secret_value()), embedding_generator=embedding_generator, **kwargs, ) @override async def list_collection_names(self, **kwargs) -> Sequence[str]: return [name.decode() for name in await self.redis_database.execute_command("FT._LIST")] @override def get_collection( self, record_type: type[TModel], *, definition: VectorStoreCollectionDefinition | None = None, collection_name: str | None = None, embedding_generator: EmbeddingGeneratorBase | None = None, collection_type: RedisCollectionTypes = RedisCollectionTypes.HASHSET, **kwargs: Any, ) -> RedisCollection: """Get a RedisCollection instance. Args: record_type: The type of the data model. definition: The model fields, optional. collection_name: The name of the collection. embedding_generator: The embedding generator to use. collection_type: The type of the collection, can be JSON or HASHSET. **kwargs: Additional keyword arguments, passed to the collection constructor. """ if collection_type == RedisCollectionTypes.HASHSET: return RedisHashsetCollection( record_type=record_type, definition=definition, collection_name=collection_name, redis_database=self.redis_database, embedding_generator=embedding_generator or self.embedding_generator, **kwargs, ) if collection_type == RedisCollectionTypes.JSON: return RedisJsonCollection( record_type=record_type, definition=definition, collection_name=collection_name, redis_database=self.redis_database, embedding_generator=embedding_generator or self.embedding_generator, **kwargs, ) raise VectorStoreOperationException( f"Collection type {collection_type} is not supported. Supported types are: {RedisCollectionTypes}" ) @override async def __aexit__(self, exc_type, exc_value, traceback) -> None: """Exit the context manager.""" if self.managed_client: await self.redis_database.aclose() # type: ignore