agno 2.1.2__py3-none-any.whl → 2.3.13__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agno/agent/agent.py +5540 -2273
- agno/api/api.py +2 -0
- agno/api/os.py +1 -1
- agno/compression/__init__.py +3 -0
- agno/compression/manager.py +247 -0
- agno/culture/__init__.py +3 -0
- agno/culture/manager.py +956 -0
- agno/db/async_postgres/__init__.py +3 -0
- agno/db/base.py +689 -6
- agno/db/dynamo/dynamo.py +933 -37
- agno/db/dynamo/schemas.py +174 -10
- agno/db/dynamo/utils.py +63 -4
- agno/db/firestore/firestore.py +831 -9
- agno/db/firestore/schemas.py +51 -0
- agno/db/firestore/utils.py +102 -4
- agno/db/gcs_json/gcs_json_db.py +660 -12
- agno/db/gcs_json/utils.py +60 -26
- agno/db/in_memory/in_memory_db.py +287 -14
- agno/db/in_memory/utils.py +60 -2
- agno/db/json/json_db.py +590 -14
- agno/db/json/utils.py +60 -26
- agno/db/migrations/manager.py +199 -0
- agno/db/migrations/v1_to_v2.py +43 -13
- agno/db/migrations/versions/__init__.py +0 -0
- agno/db/migrations/versions/v2_3_0.py +938 -0
- agno/db/mongo/__init__.py +15 -1
- agno/db/mongo/async_mongo.py +2760 -0
- agno/db/mongo/mongo.py +879 -11
- agno/db/mongo/schemas.py +42 -0
- agno/db/mongo/utils.py +80 -8
- agno/db/mysql/__init__.py +2 -1
- agno/db/mysql/async_mysql.py +2912 -0
- agno/db/mysql/mysql.py +946 -68
- agno/db/mysql/schemas.py +72 -10
- agno/db/mysql/utils.py +198 -7
- agno/db/postgres/__init__.py +2 -1
- agno/db/postgres/async_postgres.py +2579 -0
- agno/db/postgres/postgres.py +942 -57
- agno/db/postgres/schemas.py +81 -18
- agno/db/postgres/utils.py +164 -2
- agno/db/redis/redis.py +671 -7
- agno/db/redis/schemas.py +50 -0
- agno/db/redis/utils.py +65 -7
- agno/db/schemas/__init__.py +2 -1
- agno/db/schemas/culture.py +120 -0
- agno/db/schemas/evals.py +1 -0
- agno/db/schemas/memory.py +17 -2
- agno/db/singlestore/schemas.py +63 -0
- agno/db/singlestore/singlestore.py +949 -83
- agno/db/singlestore/utils.py +60 -2
- agno/db/sqlite/__init__.py +2 -1
- agno/db/sqlite/async_sqlite.py +2911 -0
- agno/db/sqlite/schemas.py +62 -0
- agno/db/sqlite/sqlite.py +965 -46
- agno/db/sqlite/utils.py +169 -8
- agno/db/surrealdb/__init__.py +3 -0
- agno/db/surrealdb/metrics.py +292 -0
- agno/db/surrealdb/models.py +334 -0
- agno/db/surrealdb/queries.py +71 -0
- agno/db/surrealdb/surrealdb.py +1908 -0
- agno/db/surrealdb/utils.py +147 -0
- agno/db/utils.py +2 -0
- agno/eval/__init__.py +10 -0
- agno/eval/accuracy.py +75 -55
- agno/eval/agent_as_judge.py +861 -0
- agno/eval/base.py +29 -0
- agno/eval/performance.py +16 -7
- agno/eval/reliability.py +28 -16
- agno/eval/utils.py +35 -17
- agno/exceptions.py +27 -2
- agno/filters.py +354 -0
- agno/guardrails/prompt_injection.py +1 -0
- agno/hooks/__init__.py +3 -0
- agno/hooks/decorator.py +164 -0
- agno/integrations/discord/client.py +1 -1
- agno/knowledge/chunking/agentic.py +13 -10
- agno/knowledge/chunking/fixed.py +4 -1
- agno/knowledge/chunking/semantic.py +9 -4
- agno/knowledge/chunking/strategy.py +59 -15
- agno/knowledge/embedder/fastembed.py +1 -1
- agno/knowledge/embedder/nebius.py +1 -1
- agno/knowledge/embedder/ollama.py +8 -0
- agno/knowledge/embedder/openai.py +8 -8
- agno/knowledge/embedder/sentence_transformer.py +6 -2
- agno/knowledge/embedder/vllm.py +262 -0
- agno/knowledge/knowledge.py +1618 -318
- agno/knowledge/reader/base.py +6 -2
- agno/knowledge/reader/csv_reader.py +8 -10
- agno/knowledge/reader/docx_reader.py +5 -6
- agno/knowledge/reader/field_labeled_csv_reader.py +16 -20
- agno/knowledge/reader/json_reader.py +5 -4
- agno/knowledge/reader/markdown_reader.py +8 -8
- agno/knowledge/reader/pdf_reader.py +17 -19
- agno/knowledge/reader/pptx_reader.py +101 -0
- agno/knowledge/reader/reader_factory.py +32 -3
- agno/knowledge/reader/s3_reader.py +3 -3
- agno/knowledge/reader/tavily_reader.py +193 -0
- agno/knowledge/reader/text_reader.py +22 -10
- agno/knowledge/reader/web_search_reader.py +1 -48
- agno/knowledge/reader/website_reader.py +10 -10
- agno/knowledge/reader/wikipedia_reader.py +33 -1
- agno/knowledge/types.py +1 -0
- agno/knowledge/utils.py +72 -7
- agno/media.py +22 -6
- agno/memory/__init__.py +14 -1
- agno/memory/manager.py +544 -83
- agno/memory/strategies/__init__.py +15 -0
- agno/memory/strategies/base.py +66 -0
- agno/memory/strategies/summarize.py +196 -0
- agno/memory/strategies/types.py +37 -0
- agno/models/aimlapi/aimlapi.py +17 -0
- agno/models/anthropic/claude.py +515 -40
- agno/models/aws/bedrock.py +102 -21
- agno/models/aws/claude.py +131 -274
- agno/models/azure/ai_foundry.py +41 -19
- agno/models/azure/openai_chat.py +39 -8
- agno/models/base.py +1249 -525
- agno/models/cerebras/cerebras.py +91 -21
- agno/models/cerebras/cerebras_openai.py +21 -2
- agno/models/cohere/chat.py +40 -6
- agno/models/cometapi/cometapi.py +18 -1
- agno/models/dashscope/dashscope.py +2 -3
- agno/models/deepinfra/deepinfra.py +18 -1
- agno/models/deepseek/deepseek.py +69 -3
- agno/models/fireworks/fireworks.py +18 -1
- agno/models/google/gemini.py +877 -80
- agno/models/google/utils.py +22 -0
- agno/models/groq/groq.py +51 -18
- agno/models/huggingface/huggingface.py +17 -6
- agno/models/ibm/watsonx.py +16 -6
- agno/models/internlm/internlm.py +18 -1
- agno/models/langdb/langdb.py +13 -1
- agno/models/litellm/chat.py +44 -9
- agno/models/litellm/litellm_openai.py +18 -1
- agno/models/message.py +28 -5
- agno/models/meta/llama.py +47 -14
- agno/models/meta/llama_openai.py +22 -17
- agno/models/mistral/mistral.py +8 -4
- agno/models/nebius/nebius.py +6 -7
- agno/models/nvidia/nvidia.py +20 -3
- agno/models/ollama/chat.py +24 -8
- agno/models/openai/chat.py +104 -29
- agno/models/openai/responses.py +101 -81
- agno/models/openrouter/openrouter.py +60 -3
- agno/models/perplexity/perplexity.py +17 -1
- agno/models/portkey/portkey.py +7 -6
- agno/models/requesty/requesty.py +24 -4
- agno/models/response.py +73 -2
- agno/models/sambanova/sambanova.py +20 -3
- agno/models/siliconflow/siliconflow.py +19 -2
- agno/models/together/together.py +20 -3
- agno/models/utils.py +254 -8
- agno/models/vercel/v0.py +20 -3
- agno/models/vertexai/__init__.py +0 -0
- agno/models/vertexai/claude.py +190 -0
- agno/models/vllm/vllm.py +19 -14
- agno/models/xai/xai.py +19 -2
- agno/os/app.py +549 -152
- agno/os/auth.py +190 -3
- agno/os/config.py +23 -0
- agno/os/interfaces/a2a/router.py +8 -11
- agno/os/interfaces/a2a/utils.py +1 -1
- agno/os/interfaces/agui/router.py +18 -3
- agno/os/interfaces/agui/utils.py +152 -39
- agno/os/interfaces/slack/router.py +55 -37
- agno/os/interfaces/slack/slack.py +9 -1
- agno/os/interfaces/whatsapp/router.py +0 -1
- agno/os/interfaces/whatsapp/security.py +3 -1
- agno/os/mcp.py +110 -52
- agno/os/middleware/__init__.py +2 -0
- agno/os/middleware/jwt.py +676 -112
- agno/os/router.py +40 -1478
- agno/os/routers/agents/__init__.py +3 -0
- agno/os/routers/agents/router.py +599 -0
- agno/os/routers/agents/schema.py +261 -0
- agno/os/routers/evals/evals.py +96 -39
- agno/os/routers/evals/schemas.py +65 -33
- agno/os/routers/evals/utils.py +80 -10
- agno/os/routers/health.py +10 -4
- agno/os/routers/knowledge/knowledge.py +196 -38
- agno/os/routers/knowledge/schemas.py +82 -22
- agno/os/routers/memory/memory.py +279 -52
- agno/os/routers/memory/schemas.py +46 -17
- agno/os/routers/metrics/metrics.py +20 -8
- agno/os/routers/metrics/schemas.py +16 -16
- agno/os/routers/session/session.py +462 -34
- agno/os/routers/teams/__init__.py +3 -0
- agno/os/routers/teams/router.py +512 -0
- agno/os/routers/teams/schema.py +257 -0
- agno/os/routers/traces/__init__.py +3 -0
- agno/os/routers/traces/schemas.py +414 -0
- agno/os/routers/traces/traces.py +499 -0
- agno/os/routers/workflows/__init__.py +3 -0
- agno/os/routers/workflows/router.py +624 -0
- agno/os/routers/workflows/schema.py +75 -0
- agno/os/schema.py +256 -693
- agno/os/scopes.py +469 -0
- agno/os/utils.py +514 -36
- agno/reasoning/anthropic.py +80 -0
- agno/reasoning/gemini.py +73 -0
- agno/reasoning/openai.py +5 -0
- agno/reasoning/vertexai.py +76 -0
- agno/run/__init__.py +6 -0
- agno/run/agent.py +155 -32
- agno/run/base.py +55 -3
- agno/run/requirement.py +181 -0
- agno/run/team.py +125 -38
- agno/run/workflow.py +72 -18
- agno/session/agent.py +102 -89
- agno/session/summary.py +56 -15
- agno/session/team.py +164 -90
- agno/session/workflow.py +405 -40
- agno/table.py +10 -0
- agno/team/team.py +3974 -1903
- agno/tools/dalle.py +2 -4
- agno/tools/eleven_labs.py +23 -25
- agno/tools/exa.py +21 -16
- agno/tools/file.py +153 -23
- agno/tools/file_generation.py +16 -10
- agno/tools/firecrawl.py +15 -7
- agno/tools/function.py +193 -38
- agno/tools/gmail.py +238 -14
- agno/tools/google_drive.py +271 -0
- agno/tools/googlecalendar.py +36 -8
- agno/tools/googlesheets.py +20 -5
- agno/tools/jira.py +20 -0
- agno/tools/mcp/__init__.py +10 -0
- agno/tools/mcp/mcp.py +331 -0
- agno/tools/mcp/multi_mcp.py +347 -0
- agno/tools/mcp/params.py +24 -0
- agno/tools/mcp_toolbox.py +3 -3
- agno/tools/models/nebius.py +5 -5
- agno/tools/models_labs.py +20 -10
- agno/tools/nano_banana.py +151 -0
- agno/tools/notion.py +204 -0
- agno/tools/parallel.py +314 -0
- agno/tools/postgres.py +76 -36
- agno/tools/redshift.py +406 -0
- agno/tools/scrapegraph.py +1 -1
- agno/tools/shopify.py +1519 -0
- agno/tools/slack.py +18 -3
- agno/tools/spotify.py +919 -0
- agno/tools/tavily.py +146 -0
- agno/tools/toolkit.py +25 -0
- agno/tools/workflow.py +8 -1
- agno/tools/yfinance.py +12 -11
- agno/tracing/__init__.py +12 -0
- agno/tracing/exporter.py +157 -0
- agno/tracing/schemas.py +276 -0
- agno/tracing/setup.py +111 -0
- agno/utils/agent.py +938 -0
- agno/utils/cryptography.py +22 -0
- agno/utils/dttm.py +33 -0
- agno/utils/events.py +151 -3
- agno/utils/gemini.py +15 -5
- agno/utils/hooks.py +118 -4
- agno/utils/http.py +113 -2
- agno/utils/knowledge.py +12 -5
- agno/utils/log.py +1 -0
- agno/utils/mcp.py +92 -2
- agno/utils/media.py +187 -1
- agno/utils/merge_dict.py +3 -3
- agno/utils/message.py +60 -0
- agno/utils/models/ai_foundry.py +9 -2
- agno/utils/models/claude.py +49 -14
- agno/utils/models/cohere.py +9 -2
- agno/utils/models/llama.py +9 -2
- agno/utils/models/mistral.py +4 -2
- agno/utils/print_response/agent.py +109 -16
- agno/utils/print_response/team.py +223 -30
- agno/utils/print_response/workflow.py +251 -34
- agno/utils/streamlit.py +1 -1
- agno/utils/team.py +98 -9
- agno/utils/tokens.py +657 -0
- agno/vectordb/base.py +39 -7
- agno/vectordb/cassandra/cassandra.py +21 -5
- agno/vectordb/chroma/chromadb.py +43 -12
- agno/vectordb/clickhouse/clickhousedb.py +21 -5
- agno/vectordb/couchbase/couchbase.py +29 -5
- agno/vectordb/lancedb/lance_db.py +92 -181
- agno/vectordb/langchaindb/langchaindb.py +24 -4
- agno/vectordb/lightrag/lightrag.py +17 -3
- agno/vectordb/llamaindex/llamaindexdb.py +25 -5
- agno/vectordb/milvus/milvus.py +50 -37
- agno/vectordb/mongodb/__init__.py +7 -1
- agno/vectordb/mongodb/mongodb.py +36 -30
- agno/vectordb/pgvector/pgvector.py +201 -77
- agno/vectordb/pineconedb/pineconedb.py +41 -23
- agno/vectordb/qdrant/qdrant.py +67 -54
- agno/vectordb/redis/__init__.py +9 -0
- agno/vectordb/redis/redisdb.py +682 -0
- agno/vectordb/singlestore/singlestore.py +50 -29
- agno/vectordb/surrealdb/surrealdb.py +31 -41
- agno/vectordb/upstashdb/upstashdb.py +34 -6
- agno/vectordb/weaviate/weaviate.py +53 -14
- agno/workflow/__init__.py +2 -0
- agno/workflow/agent.py +299 -0
- agno/workflow/condition.py +120 -18
- agno/workflow/loop.py +77 -10
- agno/workflow/parallel.py +231 -143
- agno/workflow/router.py +118 -17
- agno/workflow/step.py +609 -170
- agno/workflow/steps.py +73 -6
- agno/workflow/types.py +96 -21
- agno/workflow/workflow.py +2039 -262
- {agno-2.1.2.dist-info → agno-2.3.13.dist-info}/METADATA +201 -66
- agno-2.3.13.dist-info/RECORD +613 -0
- agno/tools/googlesearch.py +0 -98
- agno/tools/mcp.py +0 -679
- agno/tools/memori.py +0 -339
- agno-2.1.2.dist-info/RECORD +0 -543
- {agno-2.1.2.dist-info → agno-2.3.13.dist-info}/WHEEL +0 -0
- {agno-2.1.2.dist-info → agno-2.3.13.dist-info}/licenses/LICENSE +0 -0
- {agno-2.1.2.dist-info → agno-2.3.13.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,682 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from typing import Any, Dict, List, Optional, Union
|
|
3
|
+
|
|
4
|
+
try:
|
|
5
|
+
from redis import Redis
|
|
6
|
+
from redis.asyncio import Redis as AsyncRedis
|
|
7
|
+
from redisvl.index import AsyncSearchIndex, SearchIndex
|
|
8
|
+
from redisvl.query import FilterQuery, HybridQuery, TextQuery, VectorQuery
|
|
9
|
+
from redisvl.query.filter import Tag
|
|
10
|
+
from redisvl.redis.utils import array_to_buffer, convert_bytes
|
|
11
|
+
from redisvl.schema import IndexSchema
|
|
12
|
+
except ImportError:
|
|
13
|
+
raise ImportError("`redis` and `redisvl` not installed. Please install using `pip install redis redisvl`")
|
|
14
|
+
|
|
15
|
+
from agno.filters import FilterExpr
|
|
16
|
+
from agno.knowledge.document import Document
|
|
17
|
+
from agno.knowledge.embedder import Embedder
|
|
18
|
+
from agno.knowledge.reranker.base import Reranker
|
|
19
|
+
from agno.utils.log import log_debug, log_error, log_info, log_warning
|
|
20
|
+
from agno.utils.string import hash_string_sha256
|
|
21
|
+
from agno.vectordb.base import VectorDb
|
|
22
|
+
from agno.vectordb.distance import Distance
|
|
23
|
+
from agno.vectordb.search import SearchType
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class RedisDB(VectorDb):
|
|
27
|
+
"""
|
|
28
|
+
Redis class for managing vector operations with Redis and RedisVL.
|
|
29
|
+
|
|
30
|
+
This class provides methods for creating, inserting, searching, and managing
|
|
31
|
+
vector data in a Redis database using the RedisVL library.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
index_name: str,
|
|
37
|
+
redis_url: Optional[str] = None,
|
|
38
|
+
redis_client: Optional[Redis] = None,
|
|
39
|
+
embedder: Optional[Embedder] = None,
|
|
40
|
+
search_type: SearchType = SearchType.vector,
|
|
41
|
+
distance: Distance = Distance.cosine,
|
|
42
|
+
vector_score_weight: float = 0.7,
|
|
43
|
+
reranker: Optional[Reranker] = None,
|
|
44
|
+
**redis_kwargs,
|
|
45
|
+
):
|
|
46
|
+
"""
|
|
47
|
+
Initialize the Redis instance.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
index_name (str): Name of the Redis index to store vector data.
|
|
51
|
+
redis_url (Optional[str]): Redis connection URL.
|
|
52
|
+
redis_client (Optional[redis.Redis]): Redis client instance.
|
|
53
|
+
embedder (Optional[Embedder]): Embedder instance for creating embeddings.
|
|
54
|
+
search_type (SearchType): Type of search to perform.
|
|
55
|
+
distance (Distance): Distance metric for vector comparisons.
|
|
56
|
+
vector_score_weight (float): Weight for vector similarity in hybrid search.
|
|
57
|
+
reranker (Optional[Reranker]): Reranker instance.
|
|
58
|
+
**redis_kwargs: Additional Redis connection parameters.
|
|
59
|
+
"""
|
|
60
|
+
if not index_name:
|
|
61
|
+
raise ValueError("Index name must be provided.")
|
|
62
|
+
|
|
63
|
+
if redis_client is None and redis_url is None:
|
|
64
|
+
raise ValueError("Either 'redis_url' or 'redis_client' must be provided.")
|
|
65
|
+
|
|
66
|
+
self.redis_url = redis_url
|
|
67
|
+
|
|
68
|
+
# Initialize Redis client
|
|
69
|
+
if redis_client is None:
|
|
70
|
+
assert redis_url is not None
|
|
71
|
+
self.redis_client = Redis.from_url(redis_url, **redis_kwargs)
|
|
72
|
+
else:
|
|
73
|
+
self.redis_client = redis_client
|
|
74
|
+
|
|
75
|
+
# Index settings
|
|
76
|
+
self.index_name: str = index_name
|
|
77
|
+
|
|
78
|
+
# Embedder for embedding the document contents
|
|
79
|
+
if embedder is None:
|
|
80
|
+
from agno.knowledge.embedder.openai import OpenAIEmbedder
|
|
81
|
+
|
|
82
|
+
embedder = OpenAIEmbedder()
|
|
83
|
+
log_info("Embedder not provided, using OpenAIEmbedder as default.")
|
|
84
|
+
|
|
85
|
+
self.embedder: Embedder = embedder
|
|
86
|
+
self.dimensions: Optional[int] = self.embedder.dimensions
|
|
87
|
+
|
|
88
|
+
if self.dimensions is None:
|
|
89
|
+
raise ValueError("Embedder.dimensions must be set.")
|
|
90
|
+
|
|
91
|
+
# Search type and distance metric
|
|
92
|
+
self.search_type: SearchType = search_type
|
|
93
|
+
self.distance: Distance = distance
|
|
94
|
+
self.vector_score_weight: float = vector_score_weight
|
|
95
|
+
|
|
96
|
+
# Reranker instance
|
|
97
|
+
self.reranker: Optional[Reranker] = reranker
|
|
98
|
+
|
|
99
|
+
# Create index schema
|
|
100
|
+
self.schema = self._get_schema()
|
|
101
|
+
self.index = self._create_index()
|
|
102
|
+
self.meta_data_fields: set[str] = set()
|
|
103
|
+
|
|
104
|
+
# Async components - created lazily when needed
|
|
105
|
+
self._async_redis_client: Optional[AsyncRedis] = None
|
|
106
|
+
self._async_index: Optional[AsyncSearchIndex] = None
|
|
107
|
+
|
|
108
|
+
log_debug(f"Initialized Redis with index '{self.index_name}'")
|
|
109
|
+
|
|
110
|
+
async def _get_async_index(self) -> AsyncSearchIndex:
|
|
111
|
+
"""Get or create the async index and client."""
|
|
112
|
+
if self._async_index is None:
|
|
113
|
+
if self.redis_url is None:
|
|
114
|
+
raise ValueError("redis_url must be provided for async operations")
|
|
115
|
+
url: str = self.redis_url
|
|
116
|
+
self._async_redis_client = AsyncRedis.from_url(url)
|
|
117
|
+
self._async_index = AsyncSearchIndex(schema=self.schema, redis_client=self._async_redis_client)
|
|
118
|
+
return self._async_index
|
|
119
|
+
|
|
120
|
+
def _get_schema(self):
|
|
121
|
+
"""Get default redis schema"""
|
|
122
|
+
distance_mapping = {
|
|
123
|
+
Distance.cosine: "cosine",
|
|
124
|
+
Distance.l2: "l2",
|
|
125
|
+
Distance.max_inner_product: "ip",
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return IndexSchema.from_dict(
|
|
129
|
+
{
|
|
130
|
+
"index": {
|
|
131
|
+
"name": self.index_name,
|
|
132
|
+
"prefix": f"{self.index_name}:",
|
|
133
|
+
"storage_type": "hash",
|
|
134
|
+
},
|
|
135
|
+
"fields": [
|
|
136
|
+
{"name": "id", "type": "tag"},
|
|
137
|
+
{"name": "name", "type": "tag"},
|
|
138
|
+
{"name": "content", "type": "text"},
|
|
139
|
+
{"name": "content_hash", "type": "tag"},
|
|
140
|
+
{"name": "content_id", "type": "tag"},
|
|
141
|
+
# Common metadata fields used in operations/tests
|
|
142
|
+
{"name": "status", "type": "tag"},
|
|
143
|
+
{"name": "category", "type": "tag"},
|
|
144
|
+
{"name": "tag", "type": "tag"},
|
|
145
|
+
{"name": "source", "type": "tag"},
|
|
146
|
+
{"name": "mode", "type": "tag"},
|
|
147
|
+
{
|
|
148
|
+
"name": "embedding",
|
|
149
|
+
"type": "vector",
|
|
150
|
+
"attrs": {
|
|
151
|
+
"dims": self.dimensions,
|
|
152
|
+
"distance_metric": distance_mapping[self.distance],
|
|
153
|
+
"algorithm": "flat",
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
],
|
|
157
|
+
}
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
def _create_index(self) -> SearchIndex:
|
|
161
|
+
"""Create the RedisVL index object for this schema."""
|
|
162
|
+
return SearchIndex(self.schema, redis_url=self.redis_url)
|
|
163
|
+
|
|
164
|
+
def create(self) -> None:
|
|
165
|
+
"""Create the Redis index if it does not exist."""
|
|
166
|
+
try:
|
|
167
|
+
if not self.exists():
|
|
168
|
+
self.index.create()
|
|
169
|
+
log_debug(f"Created Redis index: {self.index_name}")
|
|
170
|
+
else:
|
|
171
|
+
log_debug(f"Redis index already exists: {self.index_name}")
|
|
172
|
+
except Exception as e:
|
|
173
|
+
log_error(f"Error creating Redis index: {e}")
|
|
174
|
+
raise
|
|
175
|
+
|
|
176
|
+
async def async_create(self) -> None:
|
|
177
|
+
"""Async version of create method."""
|
|
178
|
+
try:
|
|
179
|
+
async_index = await self._get_async_index()
|
|
180
|
+
await async_index.create(overwrite=False, drop=False)
|
|
181
|
+
log_debug(f"Created Redis index: {self.index_name}")
|
|
182
|
+
except Exception as e:
|
|
183
|
+
if "already exists" in str(e).lower():
|
|
184
|
+
log_debug(f"Redis index already exists: {self.index_name}")
|
|
185
|
+
else:
|
|
186
|
+
log_error(f"Error creating Redis index: {e}")
|
|
187
|
+
raise
|
|
188
|
+
|
|
189
|
+
def name_exists(self, name: str) -> bool:
|
|
190
|
+
"""Check if a document with the given name exists."""
|
|
191
|
+
try:
|
|
192
|
+
name_filter = Tag("name") == name
|
|
193
|
+
query = FilterQuery(
|
|
194
|
+
filter_expression=name_filter,
|
|
195
|
+
return_fields=["id"],
|
|
196
|
+
num_results=1,
|
|
197
|
+
)
|
|
198
|
+
results = self.index.query(query)
|
|
199
|
+
return len(results) > 0
|
|
200
|
+
except Exception as e:
|
|
201
|
+
log_error(f"Error checking if name exists: {e}")
|
|
202
|
+
return False
|
|
203
|
+
|
|
204
|
+
async def async_name_exists(self, name: str) -> bool: # type: ignore[override]
|
|
205
|
+
"""Async version of name_exists method."""
|
|
206
|
+
try:
|
|
207
|
+
async_index = await self._get_async_index()
|
|
208
|
+
name_filter = Tag("name") == name
|
|
209
|
+
query = FilterQuery(
|
|
210
|
+
filter_expression=name_filter,
|
|
211
|
+
return_fields=["id"],
|
|
212
|
+
num_results=1,
|
|
213
|
+
)
|
|
214
|
+
results = await async_index.query(query)
|
|
215
|
+
return len(results) > 0
|
|
216
|
+
except Exception as e:
|
|
217
|
+
log_error(f"Error checking if name exists: {e}")
|
|
218
|
+
return False
|
|
219
|
+
|
|
220
|
+
def id_exists(self, id: str) -> bool:
|
|
221
|
+
"""Check if a document with the given ID exists."""
|
|
222
|
+
try:
|
|
223
|
+
id_filter = Tag("id") == id
|
|
224
|
+
query = FilterQuery(
|
|
225
|
+
filter_expression=id_filter,
|
|
226
|
+
return_fields=["id"],
|
|
227
|
+
num_results=1,
|
|
228
|
+
)
|
|
229
|
+
results = self.index.query(query)
|
|
230
|
+
return len(results) > 0
|
|
231
|
+
except Exception as e:
|
|
232
|
+
log_error(f"Error checking if ID exists: {e}")
|
|
233
|
+
return False
|
|
234
|
+
|
|
235
|
+
def content_hash_exists(self, content_hash: str) -> bool:
|
|
236
|
+
"""Check if a document with the given content hash exists."""
|
|
237
|
+
try:
|
|
238
|
+
content_hash_filter = Tag("content_hash") == content_hash
|
|
239
|
+
query = FilterQuery(
|
|
240
|
+
filter_expression=content_hash_filter,
|
|
241
|
+
return_fields=["id"],
|
|
242
|
+
num_results=1,
|
|
243
|
+
)
|
|
244
|
+
results = self.index.query(query)
|
|
245
|
+
return len(results) > 0
|
|
246
|
+
except Exception as e:
|
|
247
|
+
log_error(f"Error checking if content hash exists: {e}")
|
|
248
|
+
return False
|
|
249
|
+
|
|
250
|
+
def _parse_redis_hash(self, doc: Document):
|
|
251
|
+
"""
|
|
252
|
+
Create object serializable into Redis HASH structure
|
|
253
|
+
"""
|
|
254
|
+
doc_dict = doc.to_dict()
|
|
255
|
+
# Ensure an ID is present; derive a deterministic one from content when missing
|
|
256
|
+
doc_id = doc.id or hash_string_sha256(doc.content)
|
|
257
|
+
doc_dict["id"] = doc_id
|
|
258
|
+
if not doc.embedding:
|
|
259
|
+
doc.embed(self.embedder)
|
|
260
|
+
|
|
261
|
+
# TODO: determine how we want to handle dtypes
|
|
262
|
+
doc_dict["embedding"] = array_to_buffer(doc.embedding, "float32")
|
|
263
|
+
|
|
264
|
+
# Add content_id if available
|
|
265
|
+
if hasattr(doc, "content_id") and doc.content_id:
|
|
266
|
+
doc_dict["content_id"] = doc.content_id
|
|
267
|
+
|
|
268
|
+
if "meta_data" in doc_dict:
|
|
269
|
+
meta_data = doc_dict.pop("meta_data", {})
|
|
270
|
+
for md in meta_data:
|
|
271
|
+
self.meta_data_fields.add(md)
|
|
272
|
+
doc_dict.update(meta_data)
|
|
273
|
+
|
|
274
|
+
return doc_dict
|
|
275
|
+
|
|
276
|
+
def insert(
|
|
277
|
+
self,
|
|
278
|
+
content_hash: str,
|
|
279
|
+
documents: List[Document],
|
|
280
|
+
filters: Optional[Dict[str, Any]] = None,
|
|
281
|
+
) -> None:
|
|
282
|
+
"""Insert documents into the Redis index."""
|
|
283
|
+
try:
|
|
284
|
+
# Store content hash for tracking
|
|
285
|
+
parsed_documents = []
|
|
286
|
+
for doc in documents:
|
|
287
|
+
parsed_doc = self._parse_redis_hash(doc)
|
|
288
|
+
parsed_doc["content_hash"] = content_hash
|
|
289
|
+
parsed_documents.append(parsed_doc)
|
|
290
|
+
|
|
291
|
+
self.index.load(parsed_documents, id_field="id")
|
|
292
|
+
log_debug(f"Inserted {len(documents)} documents with content_hash: {content_hash}")
|
|
293
|
+
except Exception as e:
|
|
294
|
+
log_error(f"Error inserting documents: {e}")
|
|
295
|
+
raise
|
|
296
|
+
|
|
297
|
+
async def async_insert(
|
|
298
|
+
self,
|
|
299
|
+
content_hash: str,
|
|
300
|
+
documents: List[Document],
|
|
301
|
+
filters: Optional[Dict[str, Any]] = None,
|
|
302
|
+
) -> None:
|
|
303
|
+
"""Async version of insert method."""
|
|
304
|
+
try:
|
|
305
|
+
async_index = await self._get_async_index()
|
|
306
|
+
parsed_documents = []
|
|
307
|
+
for doc in documents:
|
|
308
|
+
parsed_doc = self._parse_redis_hash(doc)
|
|
309
|
+
parsed_doc["content_hash"] = content_hash
|
|
310
|
+
parsed_documents.append(parsed_doc)
|
|
311
|
+
await async_index.load(parsed_documents, id_field="id")
|
|
312
|
+
log_debug(f"Inserted {len(documents)} documents with content_hash: {content_hash}")
|
|
313
|
+
except Exception as e:
|
|
314
|
+
log_error(f"Error inserting documents: {e}")
|
|
315
|
+
raise
|
|
316
|
+
|
|
317
|
+
def upsert_available(self) -> bool:
|
|
318
|
+
"""Check if upsert is available (always True for Redis)."""
|
|
319
|
+
return True
|
|
320
|
+
|
|
321
|
+
def upsert(
|
|
322
|
+
self,
|
|
323
|
+
content_hash: str,
|
|
324
|
+
documents: List[Document],
|
|
325
|
+
filters: Optional[Dict[str, Any]] = None,
|
|
326
|
+
) -> None:
|
|
327
|
+
"""Upsert documents into the Redis index.
|
|
328
|
+
Strategy: delete existing docs with the same content_hash, then insert new docs.
|
|
329
|
+
"""
|
|
330
|
+
try:
|
|
331
|
+
# Find existing docs for this content_hash and delete them
|
|
332
|
+
ch_filter = Tag("content_hash") == content_hash
|
|
333
|
+
query = FilterQuery(
|
|
334
|
+
filter_expression=ch_filter,
|
|
335
|
+
return_fields=["id"],
|
|
336
|
+
num_results=1000,
|
|
337
|
+
)
|
|
338
|
+
existing = self.index.query(query)
|
|
339
|
+
parsed = convert_bytes(existing)
|
|
340
|
+
for r in parsed:
|
|
341
|
+
key = r.get("id")
|
|
342
|
+
if key:
|
|
343
|
+
self.index.drop_keys(key)
|
|
344
|
+
|
|
345
|
+
# Insert new docs
|
|
346
|
+
self.insert(content_hash, documents, filters)
|
|
347
|
+
except Exception as e:
|
|
348
|
+
log_error(f"Error upserting documents: {e}")
|
|
349
|
+
raise
|
|
350
|
+
|
|
351
|
+
async def async_upsert(
|
|
352
|
+
self,
|
|
353
|
+
content_hash: str,
|
|
354
|
+
documents: List[Document],
|
|
355
|
+
filters: Optional[Dict[str, Any]] = None,
|
|
356
|
+
) -> None:
|
|
357
|
+
"""Async version of upsert method.
|
|
358
|
+
Strategy: delete existing docs with the same content_hash, then insert new docs.
|
|
359
|
+
"""
|
|
360
|
+
try:
|
|
361
|
+
async_index = await self._get_async_index()
|
|
362
|
+
|
|
363
|
+
# Find existing docs for this content_hash and delete them
|
|
364
|
+
ch_filter = Tag("content_hash") == content_hash
|
|
365
|
+
query = FilterQuery(
|
|
366
|
+
filter_expression=ch_filter,
|
|
367
|
+
return_fields=["id"],
|
|
368
|
+
num_results=1000,
|
|
369
|
+
)
|
|
370
|
+
existing = await async_index.query(query)
|
|
371
|
+
parsed = convert_bytes(existing)
|
|
372
|
+
for r in parsed:
|
|
373
|
+
key = r.get("id")
|
|
374
|
+
if key:
|
|
375
|
+
await async_index.drop_keys(key)
|
|
376
|
+
|
|
377
|
+
# Insert new docs
|
|
378
|
+
await self.async_insert(content_hash, documents, filters)
|
|
379
|
+
except Exception as e:
|
|
380
|
+
log_error(f"Error upserting documents: {e}")
|
|
381
|
+
raise
|
|
382
|
+
|
|
383
|
+
def search(
|
|
384
|
+
self, query: str, limit: int = 5, filters: Optional[Union[Dict[str, Any], List[FilterExpr]]] = None
|
|
385
|
+
) -> List[Document]:
|
|
386
|
+
"""Search for documents using the specified search type."""
|
|
387
|
+
|
|
388
|
+
if filters and isinstance(filters, List):
|
|
389
|
+
log_warning("Filters Expressions are not supported in Redis. No filters will be applied.")
|
|
390
|
+
filters = None
|
|
391
|
+
try:
|
|
392
|
+
if self.search_type == SearchType.vector:
|
|
393
|
+
return self.vector_search(query, limit)
|
|
394
|
+
elif self.search_type == SearchType.keyword:
|
|
395
|
+
return self.keyword_search(query, limit)
|
|
396
|
+
elif self.search_type == SearchType.hybrid:
|
|
397
|
+
return self.hybrid_search(query, limit)
|
|
398
|
+
else:
|
|
399
|
+
raise ValueError(f"Unsupported search type: {self.search_type}")
|
|
400
|
+
except Exception as e:
|
|
401
|
+
log_error(f"Error in search: {e}")
|
|
402
|
+
return []
|
|
403
|
+
|
|
404
|
+
async def async_search(
|
|
405
|
+
self, query: str, limit: int = 5, filters: Optional[Union[Dict[str, Any], List[FilterExpr]]] = None
|
|
406
|
+
) -> List[Document]:
|
|
407
|
+
"""Async version of search method."""
|
|
408
|
+
return await asyncio.to_thread(self.search, query, limit, filters)
|
|
409
|
+
|
|
410
|
+
def vector_search(self, query: str, limit: int = 5) -> List[Document]:
|
|
411
|
+
"""Perform vector similarity search."""
|
|
412
|
+
try:
|
|
413
|
+
# Get query embedding
|
|
414
|
+
query_embedding = array_to_buffer(self.embedder.get_embedding(query), "float32")
|
|
415
|
+
|
|
416
|
+
# TODO: do we want to pass back the embedding?
|
|
417
|
+
# Create vector query
|
|
418
|
+
vector_query = VectorQuery(
|
|
419
|
+
vector=query_embedding,
|
|
420
|
+
vector_field_name="embedding",
|
|
421
|
+
return_fields=["id", "name", "content"],
|
|
422
|
+
return_score=False,
|
|
423
|
+
num_results=limit,
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
# Execute search
|
|
427
|
+
results = self.index.query(vector_query)
|
|
428
|
+
|
|
429
|
+
# Convert results to documents
|
|
430
|
+
documents = [Document.from_dict(r) for r in results]
|
|
431
|
+
|
|
432
|
+
# Apply reranking if reranker is available
|
|
433
|
+
if self.reranker:
|
|
434
|
+
documents = self.reranker.rerank(query=query, documents=documents)
|
|
435
|
+
|
|
436
|
+
return documents
|
|
437
|
+
except Exception as e:
|
|
438
|
+
log_error(f"Error in vector search: {e}")
|
|
439
|
+
return []
|
|
440
|
+
|
|
441
|
+
def keyword_search(self, query: str, limit: int = 5) -> List[Document]:
|
|
442
|
+
"""Perform keyword search using Redis text search."""
|
|
443
|
+
try:
|
|
444
|
+
# Create text query
|
|
445
|
+
text_query = TextQuery(
|
|
446
|
+
text=query,
|
|
447
|
+
text_field_name="content",
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
# Execute search
|
|
451
|
+
results = self.index.query(text_query)
|
|
452
|
+
|
|
453
|
+
# Convert results to documents
|
|
454
|
+
parsed = convert_bytes(results)
|
|
455
|
+
|
|
456
|
+
# Convert results to documents
|
|
457
|
+
documents = [Document.from_dict(p) for p in parsed]
|
|
458
|
+
|
|
459
|
+
# Apply reranking if reranker is available
|
|
460
|
+
if self.reranker:
|
|
461
|
+
documents = self.reranker.rerank(query=query, documents=documents)
|
|
462
|
+
|
|
463
|
+
return documents
|
|
464
|
+
except Exception as e:
|
|
465
|
+
log_error(f"Error in keyword search: {e}")
|
|
466
|
+
return []
|
|
467
|
+
|
|
468
|
+
def hybrid_search(self, query: str, limit: int = 5) -> List[Document]:
|
|
469
|
+
"""Perform hybrid search combining vector and keyword search."""
|
|
470
|
+
try:
|
|
471
|
+
# Get query embedding
|
|
472
|
+
query_embedding = array_to_buffer(self.embedder.get_embedding(query), "float32")
|
|
473
|
+
|
|
474
|
+
# Create vector query
|
|
475
|
+
vector_query = HybridQuery(
|
|
476
|
+
vector=query_embedding,
|
|
477
|
+
vector_field_name="embedding",
|
|
478
|
+
text=query,
|
|
479
|
+
text_field_name="content",
|
|
480
|
+
alpha=self.vector_score_weight,
|
|
481
|
+
return_fields=["id", "name", "content"],
|
|
482
|
+
num_results=limit,
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
# Execute search
|
|
486
|
+
results = self.index.query(vector_query)
|
|
487
|
+
parsed = convert_bytes(results)
|
|
488
|
+
|
|
489
|
+
# Convert results to documents
|
|
490
|
+
documents = [Document.from_dict(p) for p in parsed]
|
|
491
|
+
|
|
492
|
+
# Apply reranking if reranker is available
|
|
493
|
+
if self.reranker:
|
|
494
|
+
documents = self.reranker.rerank(query=query, documents=documents)
|
|
495
|
+
|
|
496
|
+
return documents
|
|
497
|
+
except Exception as e:
|
|
498
|
+
log_error(f"Error in hybrid search: {e}")
|
|
499
|
+
return []
|
|
500
|
+
|
|
501
|
+
def drop(self) -> bool: # type: ignore[override]
|
|
502
|
+
"""Drop the Redis index."""
|
|
503
|
+
try:
|
|
504
|
+
self.index.delete(drop=True)
|
|
505
|
+
log_debug(f"Deleted Redis index: {self.index_name}")
|
|
506
|
+
return True
|
|
507
|
+
except Exception as e:
|
|
508
|
+
log_error(f"Error dropping Redis index: {e}")
|
|
509
|
+
return False
|
|
510
|
+
|
|
511
|
+
async def async_drop(self) -> None:
|
|
512
|
+
"""Async version of drop method."""
|
|
513
|
+
try:
|
|
514
|
+
async_index = await self._get_async_index()
|
|
515
|
+
await async_index.delete(drop=True)
|
|
516
|
+
log_debug(f"Deleted Redis index: {self.index_name}")
|
|
517
|
+
except Exception as e:
|
|
518
|
+
log_error(f"Error dropping Redis index: {e}")
|
|
519
|
+
raise
|
|
520
|
+
|
|
521
|
+
def exists(self) -> bool:
|
|
522
|
+
"""Check if the Redis index exists."""
|
|
523
|
+
try:
|
|
524
|
+
return self.index.exists()
|
|
525
|
+
except Exception as e:
|
|
526
|
+
log_error(f"Error checking if index exists: {e}")
|
|
527
|
+
return False
|
|
528
|
+
|
|
529
|
+
async def async_exists(self) -> bool:
|
|
530
|
+
"""Async version of exists method."""
|
|
531
|
+
try:
|
|
532
|
+
async_index = await self._get_async_index()
|
|
533
|
+
return await async_index.exists()
|
|
534
|
+
except Exception as e:
|
|
535
|
+
log_error(f"Error checking if index exists: {e}")
|
|
536
|
+
return False
|
|
537
|
+
|
|
538
|
+
def optimize(self) -> None:
|
|
539
|
+
"""Optimize the Redis index (no-op for Redis)."""
|
|
540
|
+
log_debug("Redis optimization not required")
|
|
541
|
+
pass
|
|
542
|
+
|
|
543
|
+
def delete(self) -> bool:
|
|
544
|
+
"""Delete the Redis index (same as drop)."""
|
|
545
|
+
try:
|
|
546
|
+
self.index.clear()
|
|
547
|
+
return True
|
|
548
|
+
except Exception as e:
|
|
549
|
+
log_error(f"Error deleting Redis index: {e}")
|
|
550
|
+
return False
|
|
551
|
+
|
|
552
|
+
def delete_by_id(self, id: str) -> bool:
|
|
553
|
+
"""Delete documents by ID."""
|
|
554
|
+
try:
|
|
555
|
+
# Use RedisVL to drop documents by document ID
|
|
556
|
+
result = self.index.drop_documents(id)
|
|
557
|
+
log_debug(f"Deleted document with id '{id}' from Redis index")
|
|
558
|
+
return result > 0
|
|
559
|
+
except Exception as e:
|
|
560
|
+
log_error(f"Error deleting document by ID: {e}")
|
|
561
|
+
return False
|
|
562
|
+
|
|
563
|
+
def delete_by_name(self, name: str) -> bool:
|
|
564
|
+
"""Delete documents by name."""
|
|
565
|
+
try:
|
|
566
|
+
# First find documents with the given name
|
|
567
|
+
name_filter = Tag("name") == name
|
|
568
|
+
query = FilterQuery(
|
|
569
|
+
filter_expression=name_filter,
|
|
570
|
+
return_fields=["id"],
|
|
571
|
+
num_results=1000, # Get all matching documents
|
|
572
|
+
)
|
|
573
|
+
results = self.index.query(query)
|
|
574
|
+
parsed = convert_bytes(results)
|
|
575
|
+
|
|
576
|
+
# Delete each found document by key (result['id'] is the Redis key)
|
|
577
|
+
deleted_count = 0
|
|
578
|
+
for result in parsed:
|
|
579
|
+
key = result.get("id")
|
|
580
|
+
if key:
|
|
581
|
+
deleted_count += self.index.drop_keys(key)
|
|
582
|
+
|
|
583
|
+
log_debug(f"Deleted {deleted_count} documents with name '{name}'")
|
|
584
|
+
return deleted_count > 0
|
|
585
|
+
except Exception as e:
|
|
586
|
+
log_error(f"Error deleting documents by name: {e}")
|
|
587
|
+
return False
|
|
588
|
+
|
|
589
|
+
def delete_by_metadata(self, metadata: Dict[str, Any]) -> bool:
|
|
590
|
+
"""Delete documents by metadata."""
|
|
591
|
+
try:
|
|
592
|
+
# Build filter expression for metadata using Tag filters
|
|
593
|
+
filters = []
|
|
594
|
+
for key, value in metadata.items():
|
|
595
|
+
filters.append(Tag(key) == str(value))
|
|
596
|
+
|
|
597
|
+
# Combine filters with AND logic
|
|
598
|
+
if len(filters) == 1:
|
|
599
|
+
combined_filter = filters[0]
|
|
600
|
+
else:
|
|
601
|
+
combined_filter = filters[0]
|
|
602
|
+
for f in filters[1:]:
|
|
603
|
+
combined_filter = combined_filter & f
|
|
604
|
+
|
|
605
|
+
# Find documents with the given metadata
|
|
606
|
+
query = FilterQuery(
|
|
607
|
+
filter_expression=combined_filter,
|
|
608
|
+
return_fields=["id"],
|
|
609
|
+
num_results=1000, # Get all matching documents
|
|
610
|
+
)
|
|
611
|
+
results = self.index.query(query)
|
|
612
|
+
parsed = convert_bytes(results)
|
|
613
|
+
|
|
614
|
+
# Delete each found document by key (result['id'] is the Redis key)
|
|
615
|
+
deleted_count = 0
|
|
616
|
+
for result in parsed:
|
|
617
|
+
key = result.get("id")
|
|
618
|
+
if key:
|
|
619
|
+
deleted_count += self.index.drop_keys(key)
|
|
620
|
+
|
|
621
|
+
log_debug(f"Deleted {deleted_count} documents with metadata {metadata}")
|
|
622
|
+
return deleted_count > 0
|
|
623
|
+
except Exception as e:
|
|
624
|
+
log_error(f"Error deleting documents by metadata: {e}")
|
|
625
|
+
return False
|
|
626
|
+
|
|
627
|
+
def delete_by_content_id(self, content_id: str) -> bool:
|
|
628
|
+
"""Delete documents by content ID."""
|
|
629
|
+
try:
|
|
630
|
+
# Find documents with the given content_id
|
|
631
|
+
content_id_filter = Tag("content_id") == content_id
|
|
632
|
+
query = FilterQuery(
|
|
633
|
+
filter_expression=content_id_filter,
|
|
634
|
+
return_fields=["id"],
|
|
635
|
+
num_results=1000, # Get all matching documents
|
|
636
|
+
)
|
|
637
|
+
results = self.index.query(query)
|
|
638
|
+
parsed = convert_bytes(results)
|
|
639
|
+
|
|
640
|
+
# Delete each found document by key (result['id'] is the Redis key)
|
|
641
|
+
deleted_count = 0
|
|
642
|
+
for result in parsed:
|
|
643
|
+
key = result.get("id")
|
|
644
|
+
if key:
|
|
645
|
+
deleted_count += self.index.drop_keys(key)
|
|
646
|
+
|
|
647
|
+
log_debug(f"Deleted {deleted_count} documents with content_id '{content_id}'")
|
|
648
|
+
return deleted_count > 0
|
|
649
|
+
except Exception as e:
|
|
650
|
+
log_error(f"Error deleting documents by content_id: {e}")
|
|
651
|
+
return False
|
|
652
|
+
|
|
653
|
+
def update_metadata(self, content_id: str, metadata: Dict[str, Any]) -> None:
|
|
654
|
+
"""Update metadata for documents with the given content ID."""
|
|
655
|
+
try:
|
|
656
|
+
# Find documents with the given content_id
|
|
657
|
+
content_id_filter = Tag("content_id") == content_id
|
|
658
|
+
query = FilterQuery(
|
|
659
|
+
filter_expression=content_id_filter,
|
|
660
|
+
return_fields=["id"],
|
|
661
|
+
num_results=1000, # Get all matching documents
|
|
662
|
+
)
|
|
663
|
+
results = self.index.query(query)
|
|
664
|
+
|
|
665
|
+
# Update metadata for each found document
|
|
666
|
+
for result in results:
|
|
667
|
+
doc_id = result.get("id")
|
|
668
|
+
if doc_id:
|
|
669
|
+
# result['id'] is the Redis key
|
|
670
|
+
key = result.get("id")
|
|
671
|
+
# Update the hash with new metadata
|
|
672
|
+
if key:
|
|
673
|
+
self.redis_client.hset(key, mapping=metadata)
|
|
674
|
+
|
|
675
|
+
log_debug(f"Updated metadata for documents with content_id '{content_id}'")
|
|
676
|
+
except Exception as e:
|
|
677
|
+
log_error(f"Error updating metadata: {e}")
|
|
678
|
+
raise
|
|
679
|
+
|
|
680
|
+
def get_supported_search_types(self) -> List[str]:
|
|
681
|
+
"""Get list of supported search types."""
|
|
682
|
+
return ["vector", "keyword", "hybrid"]
|