Files
microsoft--semantic-kernel/python/semantic_kernel/connectors/redis.py
T
wehub-resource-sync b957a53def
CodeQL / Analyze (csharp) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 13:21:23 +08:00

846 lines
34 KiB
Python

# 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