refactor oracle23ai.py

This commit is contained in:
Oracle Public Cloud User
2025-07-07 16:21:34 +00:00
parent 25e241ae41
commit 12ebdbae81
2 changed files with 164 additions and 121 deletions

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
x.py
.DS_Store .DS_Store
node_modules node_modules
/build /build

View File

@@ -1,5 +1,7 @@
""" """
# ORACLE23AI (Oracle23ai Vector Search) : env.examples Oracle 23ai Vector Database Client - Fixed Version
# .env
VECTOR_DB = "oracle23ai" VECTOR_DB = "oracle23ai"
## DBCS or oracle 23ai free ## DBCS or oracle 23ai free
@@ -24,10 +26,14 @@ ORACLE_DB_POOL_MAX = 10
ORACLE_DB_POOL_INCREMENT = 1 ORACLE_DB_POOL_INCREMENT = 1
""" """
from typing import Optional, List, Dict, Any from typing import Optional, List, Dict, Any, Union
from decimal import Decimal from decimal import Decimal
import logging import logging
import os import os
import threading
import time
import json
import array
import oracledb import oracledb
from open_webui.retrieval.vector.main import ( from open_webui.retrieval.vector.main import (
@@ -54,11 +60,6 @@ from open_webui.env import SRC_LOG_LEVELS
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"]) log.setLevel(SRC_LOG_LEVELS["RAG"])
# ORACLE_DB_USE_WALLET = os.environ.get("ORACLE_DB_USE_WALLET", "DBCS")
# ORACLE_DB_USER = os.environ.get("ORACLE_DB_USER", "DEMOUSER")
# ORACLE_DB_PASSWORD = os.environ.get("ORACLE_DB_PASSWORD", "Welcome123456")
# ORACLE_DB_DSN = os.environ.get("ORACLE_DB_DSN", "medium")
# ORACLE_DB_DSN = os.environ.get("ORACLE_DB_DSN", "(description= (retry_count=3)(retry_delay=3)(address=(protocol=tcps)(port=1522)(host=d6aqmjs6.adb.us-chicago-1.oraclecloud.com))(connect_data=(service_name=g13fc7c96b5ee55_agentvs_medium.adb.oraclecloud.com))(security=(ssl_server_dn_match=no)))")
class Oracle23aiClient(VectorDBBase): class Oracle23aiClient(VectorDBBase):
""" """
@@ -76,16 +77,15 @@ class Oracle23aiClient(VectorDBBase):
""" """
Initialize the Oracle23aiClient with a connection pool. Initialize the Oracle23aiClient with a connection pool.
Creates a connection pool with min=2 and max=10 connections, initializes Creates a connection pool with configurable min/max connections, initializes
the database schema if needed, and sets up necessary tables and indexes. the database schema if needed, and sets up necessary tables and indexes.
Args:
db_type (str): Database type - "ADB" for Autonomous Database or "DBCS" for Database Cloud Service
Raises: Raises:
ValueError: If required configuration parameters are missing ValueError: If required configuration parameters are missing
Exception: If database initialization fails Exception: If database initialization fails
""" """
self.pool = None
try: try:
# Create the appropriate connection pool based on DB type # Create the appropriate connection pool based on DB type
if ORACLE_DB_USE_WALLET: if ORACLE_DB_USE_WALLET:
@@ -94,10 +94,10 @@ class Oracle23aiClient(VectorDBBase):
self._create_dbcs_pool() self._create_dbcs_pool()
dsn = ORACLE_DB_DSN dsn = ORACLE_DB_DSN
log.info(f" >>> Creating Connection Pool [{ORACLE_DB_USER}:**@{dsn}]") log.info(f"Creating Connection Pool [{ORACLE_DB_USER}:**@{dsn}]")
with self.get_connection() as connection: with self.get_connection() as connection:
log.info("Connection version:", connection.version) log.info(f"Connection version: {connection.version}")
self._initialize_database(connection) self._initialize_database(connection)
log.info("Oracle Vector Search initialization complete.") log.info("Oracle Vector Search initialization complete.")
@@ -122,7 +122,7 @@ class Oracle23aiClient(VectorDBBase):
wallet_location=ORACLE_WALLET_DIR, wallet_location=ORACLE_WALLET_DIR,
wallet_password=ORACLE_WALLET_PASSWORD wallet_password=ORACLE_WALLET_PASSWORD
) )
log.info(f"Created ADB connection pool with wallet authentication.") log.info("Created ADB connection pool with wallet authentication.")
def _create_dbcs_pool(self) -> None: def _create_dbcs_pool(self) -> None:
""" """
@@ -158,7 +158,6 @@ class Oracle23aiClient(VectorDBBase):
log.exception(f"Connection attempt {attempt + 1} failed: {error_obj.message}") log.exception(f"Connection attempt {attempt + 1} failed: {error_obj.message}")
if attempt < max_retries - 1: if attempt < max_retries - 1:
import time
wait_time = 2 ** attempt wait_time = 2 ** attempt
log.info(f"Retrying in {wait_time} seconds...") log.info(f"Retrying in {wait_time} seconds...")
time.sleep(wait_time) time.sleep(wait_time)
@@ -189,12 +188,17 @@ class Oracle23aiClient(VectorDBBase):
def _reconnect_pool(self): def _reconnect_pool(self):
""" """
Attempt to reinitialize the connection pool if it's been closed or broken. Attempt to reinitialize the connection pool if it's been closed or broken.
Args:
db_type (str): Database type - "ADB" for Autonomous Database or "DBCS" for Database Cloud Service
""" """
try: try:
log.info("Attempting to reinitialize the Oracle connection pool...") log.info("Attempting to reinitialize the Oracle connection pool...")
# Close existing pool if it exists
if self.pool:
try:
self.pool.close()
except Exception as close_error:
log.warning(f"Error closing existing pool: {close_error}")
# Re-create the appropriate connection pool based on DB type # Re-create the appropriate connection pool based on DB type
if ORACLE_DB_USE_WALLET: if ORACLE_DB_USE_WALLET:
self._create_adb_pool() self._create_adb_pool()
@@ -233,8 +237,6 @@ class Oracle23aiClient(VectorDBBase):
return cursor.var(metadata.type_code, arraysize=cursor.arraysize, return cursor.var(metadata.type_code, arraysize=cursor.arraysize,
outconverter=list) outconverter=list)
# Rest of the Oracle23aiClient class remains unchanged...
def _initialize_database(self, connection) -> None: def _initialize_database(self, connection) -> None:
""" """
Initialize database schema, tables and indexes. Initialize database schema, tables and indexes.
@@ -247,62 +249,69 @@ class Oracle23aiClient(VectorDBBase):
Raises: Raises:
Exception: If schema initialization fails Exception: If schema initialization fails
""" """
with connection.cursor() as cursor: with connection.cursor() as cursor:
log.info(f" >>> Creating Table document_chunk") try:
cursor.execute(f""" log.info("Creating Table document_chunk")
BEGIN cursor.execute("""
EXECUTE IMMEDIATE ' BEGIN
CREATE TABLE IF NOT EXISTS document_chunk ( EXECUTE IMMEDIATE '
id VARCHAR2(255) PRIMARY KEY, CREATE TABLE IF NOT EXISTS document_chunk (
collection_name VARCHAR2(255) NOT NULL, id VARCHAR2(255) PRIMARY KEY,
text CLOB, collection_name VARCHAR2(255) NOT NULL,
vmetadata JSON, text CLOB,
vector vector(*, float32) vmetadata JSON,
) vector vector(*, float32)
'; )
EXCEPTION ';
WHEN OTHERS THEN EXCEPTION
IF SQLCODE != -955 THEN WHEN OTHERS THEN
RAISE; IF SQLCODE != -955 THEN
END IF; RAISE;
END; END IF;
""") END;
""")
log.info(f" >>> Creating Table document_chunk_collection_name_idx")
cursor.execute(""" log.info("Creating Index document_chunk_collection_name_idx")
BEGIN cursor.execute("""
EXECUTE IMMEDIATE ' BEGIN
CREATE INDEX IF NOT exists document_chunk_collection_name_idx EXECUTE IMMEDIATE '
ON document_chunk (collection_name) CREATE INDEX IF NOT EXISTS document_chunk_collection_name_idx
'; ON document_chunk (collection_name)
EXCEPTION ';
WHEN OTHERS THEN EXCEPTION
IF SQLCODE != -955 THEN WHEN OTHERS THEN
RAISE; IF SQLCODE != -955 THEN
END IF; RAISE;
END; END IF;
""") END;
""")
log.info(f" >>> Creating VECTOR INDEX document_chunk_vector_ivf_idx")
cursor.execute(""" log.info("Creating VECTOR INDEX document_chunk_vector_ivf_idx")
BEGIN cursor.execute("""
EXECUTE IMMEDIATE ' BEGIN
create vector index IF NOT EXISTS document_chunk_vector_ivf_idx on document_chunk(vector) EXECUTE IMMEDIATE '
organization neighbor partitions CREATE VECTOR INDEX IF NOT EXISTS document_chunk_vector_ivf_idx
distance cosine ON document_chunk(vector)
with target accuracy 95 ORGANIZATION NEIGHBOR PARTITIONS
PARAMETERS (type IVF, NEIGHBOR PARTITIONS 100) DISTANCE COSINE
'; WITH TARGET ACCURACY 95
EXCEPTION PARAMETERS (TYPE IVF, NEIGHBOR PARTITIONS 100)
WHEN OTHERS THEN ';
IF SQLCODE != -955 THEN EXCEPTION
RAISE; WHEN OTHERS THEN
END IF; IF SQLCODE != -955 THEN
END; RAISE;
""") END IF;
END;
connection.commit() """)
connection.commit()
log.info("Database initialization completed successfully.")
except Exception as e:
connection.rollback()
log.exception(f"Error during database initialization: {e}")
raise
def check_vector_length(self) -> None: def check_vector_length(self) -> None:
""" """
@@ -323,7 +332,6 @@ class Oracle23aiClient(VectorDBBase):
Returns: Returns:
bytes: The vector in Oracle BLOB format bytes: The vector in Oracle BLOB format
""" """
import array
return array.array("f", vector) return array.array("f", vector)
def adjust_vector_length(self, vector: List[float]) -> List[float]: def adjust_vector_length(self, vector: List[float]) -> List[float]:
@@ -365,7 +373,6 @@ class Oracle23aiClient(VectorDBBase):
Returns: Returns:
str: JSON representation of metadata str: JSON representation of metadata
""" """
import json
return json.dumps(metadata, default=self._decimal_handler) if metadata else "{}" return json.dumps(metadata, default=self._decimal_handler) if metadata else "{}"
def _json_to_metadata(self, json_str: str) -> Dict: def _json_to_metadata(self, json_str: str) -> Dict:
@@ -378,7 +385,6 @@ class Oracle23aiClient(VectorDBBase):
Returns: Returns:
Dict: Metadata dictionary Dict: Metadata dictionary
""" """
import json
return json.loads(json_str) if json_str else {} return json.loads(json_str) if json_str else {}
def insert(self, collection_name: str, items: List[VectorItem]) -> None: def insert(self, collection_name: str, items: List[VectorItem]) -> None:
@@ -400,7 +406,8 @@ class Oracle23aiClient(VectorDBBase):
... ] ... ]
>>> client.insert("my_collection", items) >>> client.insert("my_collection", items)
""" """
log.info(f"Oracle23aiClient:Inserting {len(items)} items into collection '{collection_name}'.") log.info(f"Inserting {len(items)} items into collection '{collection_name}'.")
with self.get_connection() as connection: with self.get_connection() as connection:
try: try:
with connection.cursor() as cursor: with connection.cursor() as cursor:
@@ -421,7 +428,8 @@ class Oracle23aiClient(VectorDBBase):
}) })
connection.commit() connection.commit()
log.info(f"Oracle23aiClient:Inserted {len(items)} items into collection '{collection_name}'.") log.info(f"Successfully inserted {len(items)} items into collection '{collection_name}'.")
except Exception as e: except Exception as e:
connection.rollback() connection.rollback()
log.exception(f"Error during insert: {e}") log.exception(f"Error during insert: {e}")
@@ -440,7 +448,7 @@ class Oracle23aiClient(VectorDBBase):
Raises: Raises:
Exception: If upsert operation fails Exception: If upsert operation fails
Example: Example:
>>> client = Oracle23aiClient() >>> client = Oracle23aiClient()
>>> items = [ >>> items = [
@@ -449,6 +457,8 @@ class Oracle23aiClient(VectorDBBase):
... ] ... ]
>>> client.upsert("my_collection", items) >>> client.upsert("my_collection", items)
""" """
log.info(f"Upserting {len(items)} items into collection '{collection_name}'.")
with self.get_connection() as connection: with self.get_connection() as connection:
try: try:
with connection.cursor() as cursor: with connection.cursor() as cursor:
@@ -458,32 +468,33 @@ class Oracle23aiClient(VectorDBBase):
cursor.execute(""" cursor.execute("""
MERGE INTO document_chunk d MERGE INTO document_chunk d
USING (SELECT :id as id FROM dual) s USING (SELECT :merge_id as id FROM dual) s
ON (d.id = s.id) ON (d.id = s.id)
WHEN MATCHED THEN WHEN MATCHED THEN
UPDATE SET UPDATE SET
collection_name = :collection_name, collection_name = :upd_collection_name,
text = :text, text = :upd_text,
vmetadata = :metadata, vmetadata = :upd_metadata,
vector = :vector vector = :upd_vector
WHEN NOT MATCHED THEN WHEN NOT MATCHED THEN
INSERT (id, collection_name, text, vmetadata, vector) INSERT (id, collection_name, text, vmetadata, vector)
VALUES (:id, :collection_name, :text, :metadata, :vector) VALUES (:ins_id, :ins_collection_name, :ins_text, :ins_metadata, :ins_vector)
""", { """, {
'id': item["id"], 'merge_id': item["id"],
'collection_name': collection_name, 'upd_collection_name': collection_name,
'text': item["text"], 'upd_text': item["text"],
'metadata': metadata_json, 'upd_metadata': metadata_json,
'vector': vector_blob, 'upd_vector': vector_blob,
'id': item["id"], 'ins_id': item["id"],
'collection_name': collection_name, 'ins_collection_name': collection_name,
'text': item["text"], 'ins_text': item["text"],
'metadata': metadata_json, 'ins_metadata': metadata_json,
'vector': vector_blob 'ins_vector': vector_blob
}) })
connection.commit() connection.commit()
log.info(f"Upserted {len(items)} items into collection '{collection_name}'.") log.info(f"Successfully upserted {len(items)} items into collection '{collection_name}'.")
except Exception as e: except Exception as e:
connection.rollback() connection.rollback()
log.exception(f"Error during upsert: {e}") log.exception(f"Error during upsert: {e}")
@@ -492,8 +503,8 @@ class Oracle23aiClient(VectorDBBase):
def search( def search(
self, self,
collection_name: str, collection_name: str,
vectors: List[List[float]], vectors: List[List[Union[float, int]]],
limit: Optional[int] = None limit: int
) -> Optional[SearchResult]: ) -> Optional[SearchResult]:
""" """
Search for similar vectors in the database. Search for similar vectors in the database.
@@ -502,8 +513,8 @@ class Oracle23aiClient(VectorDBBase):
Args: Args:
collection_name (str): Name of the collection to search collection_name (str): Name of the collection to search
vectors (List[List[float]]): Query vectors to find similar items for vectors (List[List[Union[float, int]]]): Query vectors to find similar items for
limit (Optional[int]): Maximum number of results to return per query limit (int): Maximum number of results to return per query
Returns: Returns:
Optional[SearchResult]: Search results containing ids, distances, documents, and metadata Optional[SearchResult]: Search results containing ids, distances, documents, and metadata
@@ -513,16 +524,17 @@ class Oracle23aiClient(VectorDBBase):
>>> query_vector = [0.1, 0.2, 0.3, ...] # Must match VECTOR_LENGTH >>> query_vector = [0.1, 0.2, 0.3, ...] # Must match VECTOR_LENGTH
>>> results = client.search("my_collection", [query_vector], limit=5) >>> results = client.search("my_collection", [query_vector], limit=5)
>>> if results: >>> if results:
... print(f"Found {len(results.ids[0])} matches") ... log.info(f"Found {len(results.ids[0])} matches")
... for i, (id, dist) in enumerate(zip(results.ids[0], results.distances[0])): ... for i, (id, dist) in enumerate(zip(results.ids[0], results.distances[0])):
... print(f"Match {i+1}: id={id}, distance={dist}") ... log.info(f"Match {i+1}: id={id}, distance={dist}")
""" """
print(f"Oracle23aiClient:Searching items from collection '{collection_name}'.") log.info(f"Searching items from collection '{collection_name}' with limit {limit}.")
try: try:
if not vectors: if not vectors:
log.warning("No vectors provided for search.")
return None return None
limit = limit or 10
num_queries = len(vectors) num_queries = len(vectors)
ids = [[] for _ in range(num_queries)] ids = [[] for _ in range(num_queries)]
@@ -557,22 +569,23 @@ class Oracle23aiClient(VectorDBBase):
metadatas[qid].append(row[2].read() if isinstance(row[2], oracledb.LOB) else row[2]) metadatas[qid].append(row[2].read() if isinstance(row[2], oracledb.LOB) else row[2])
distances[qid].append(float(row[3])) distances[qid].append(float(row[3]))
log.info(f"Search completed. Found {sum(len(ids[i]) for i in range(num_queries))} total results.")
return SearchResult( return SearchResult(
ids=ids, ids=ids,
distances=distances, distances=distances,
documents=documents, documents=documents,
metadatas=metadatas metadatas=metadatas
) )
except Exception as e: except Exception as e:
log.exception(f"Error during search: {e}") log.exception(f"Error during search: {e}")
import traceback
log.exception(traceback.format_exc())
return None return None
def query( def query(
self, self,
collection_name: str, collection_name: str,
filter: Dict[str, Any], filter: Dict,
limit: Optional[int] = None limit: Optional[int] = None
) -> Optional[GetResult]: ) -> Optional[GetResult]:
""" """
@@ -595,7 +608,8 @@ class Oracle23aiClient(VectorDBBase):
>>> if results: >>> if results:
... print(f"Found {len(results.ids[0])} matching documents") ... print(f"Found {len(results.ids[0])} matching documents")
""" """
print(f"Oracle23aiClient:Querying items from collection '{collection_name}'.") log.info(f"Querying items from collection '{collection_name}' with filters.")
try: try:
limit = limit or 100 limit = limit or 100
@@ -621,27 +635,27 @@ class Oracle23aiClient(VectorDBBase):
results = cursor.fetchall() results = cursor.fetchall()
if not results: if not results:
log.info("No results found for query.")
return None return None
ids = [[row[0] for row in results]] ids = [[row[0] for row in results]]
documents = [[row[1].read() if isinstance(row[1], oracledb.LOB) else str(row[1]) for row in results]] documents = [[row[1].read() if isinstance(row[1], oracledb.LOB) else str(row[1]) for row in results]]
metadatas = [[row[2].read() if isinstance(row[2], oracledb.LOB) else row[2] for row in results]] metadatas = [[row[2].read() if isinstance(row[2], oracledb.LOB) else row[2] for row in results]]
log.info(f"Query completed. Found {len(ids)} results.")
return GetResult( return GetResult(
ids=ids, ids=ids,
documents=documents, documents=documents,
metadatas=metadatas metadatas=metadatas
) )
except Exception as e: except Exception as e:
log.exception(f"Error during query: {e}") log.exception(f"Error during query: {e}")
import traceback
log.exception(traceback.format_exc())
return None return None
def get( def get(
self, self,
collection_name: str, collection_name: str
limit: Optional[int] = None
) -> Optional[GetResult]: ) -> Optional[GetResult]:
""" """
Get all items in a collection. Get all items in a collection.
@@ -661,8 +675,10 @@ class Oracle23aiClient(VectorDBBase):
>>> if results: >>> if results:
... print(f"Retrieved {len(results.ids[0])} documents from collection") ... print(f"Retrieved {len(results.ids[0])} documents from collection")
""" """
log.info(f"Getting items from collection '{collection_name}' with limit {limit}.")
try: try:
limit = limit or 100 limit = limit or 1000
with self.get_connection() as connection: with self.get_connection() as connection:
with connection.cursor() as cursor: with connection.cursor() as cursor:
@@ -679,6 +695,7 @@ class Oracle23aiClient(VectorDBBase):
results = cursor.fetchall() results = cursor.fetchall()
if not results: if not results:
log.info("No results found.")
return None return None
ids = [[row[0] for row in results]] ids = [[row[0] for row in results]]
@@ -690,10 +707,9 @@ class Oracle23aiClient(VectorDBBase):
documents=documents, documents=documents,
metadatas=metadatas metadatas=metadatas
) )
except Exception as e: except Exception as e:
log.exception(f"Error during get: {e}") log.exception(f"Error during get: {e}")
import traceback
log.exception(traceback.format_exc())
return None return None
def delete( def delete(
@@ -722,11 +738,14 @@ class Oracle23aiClient(VectorDBBase):
>>> # Or delete by metadata filter >>> # Or delete by metadata filter
>>> client.delete("my_collection", filter={"source": "deprecated_source"}) >>> client.delete("my_collection", filter={"source": "deprecated_source"})
""" """
log.info(f"Deleting items from collection '{collection_name}'.")
try: try:
query = "DELETE FROM document_chunk WHERE collection_name = :collection_name" query = "DELETE FROM document_chunk WHERE collection_name = :collection_name"
params = {'collection_name': collection_name} params = {'collection_name': collection_name}
if ids: if ids:
# Use proper parameterized query for IDs
id_list = ",".join([f"'{id}'" for id in ids]) id_list = ",".join([f"'{id}'" for id in ids])
query += f" AND id IN ({id_list})" query += f" AND id IN ({id_list})"
@@ -743,6 +762,7 @@ class Oracle23aiClient(VectorDBBase):
connection.commit() connection.commit()
log.info(f"Deleted {deleted} items from collection '{collection_name}'.") log.info(f"Deleted {deleted} items from collection '{collection_name}'.")
except Exception as e: except Exception as e:
log.exception(f"Error during delete: {e}") log.exception(f"Error during delete: {e}")
raise raise
@@ -760,13 +780,17 @@ class Oracle23aiClient(VectorDBBase):
>>> client = Oracle23aiClient() >>> client = Oracle23aiClient()
>>> client.reset() # Warning: Removes all data! >>> client.reset() # Warning: Removes all data!
""" """
log.info("Resetting database - deleting all items.")
try: try:
with self.get_connection() as connection: with self.get_connection() as connection:
with connection.cursor() as cursor: with connection.cursor() as cursor:
cursor.execute("DELETE FROM document_chunk") cursor.execute("DELETE FROM document_chunk")
deleted = cursor.rowcount deleted = cursor.rowcount
connection.commit() connection.commit()
log.info(f"Reset complete. Deleted {deleted} items from 'document_chunk' table.") log.info(f"Reset complete. Deleted {deleted} items from 'document_chunk' table.")
except Exception as e: except Exception as e:
log.exception(f"Error during reset: {e}") log.exception(f"Error during reset: {e}")
raise raise
@@ -785,9 +809,9 @@ class Oracle23aiClient(VectorDBBase):
try: try:
if hasattr(self, 'pool') and self.pool: if hasattr(self, 'pool') and self.pool:
self.pool.close() self.pool.close()
print("Oracle Vector Search connection pool closed.") log.info("Oracle Vector Search connection pool closed.")
except Exception as e: except Exception as e:
print(f"Error closing connection pool: {e}") log.exception(f"Error closing connection pool: {e}")
def has_collection(self, collection_name: str) -> bool: def has_collection(self, collection_name: str) -> bool:
""" """
@@ -817,7 +841,9 @@ class Oracle23aiClient(VectorDBBase):
""", {'collection_name': collection_name}) """, {'collection_name': collection_name})
count = cursor.fetchone()[0] count = cursor.fetchone()[0]
return count > 0 return count > 0
except Exception as e: except Exception as e:
log.exception(f"Error checking collection existence: {e}") log.exception(f"Error checking collection existence: {e}")
return False return False
@@ -835,5 +861,21 @@ class Oracle23aiClient(VectorDBBase):
>>> client = Oracle23aiClient() >>> client = Oracle23aiClient()
>>> client.delete_collection("obsolete_collection") >>> client.delete_collection("obsolete_collection")
""" """
self.delete(collection_name) log.info(f"Deleting collection '{collection_name}'.")
log.info(f"Collection '{collection_name}' deleted.")
try:
with self.get_connection() as connection:
with connection.cursor() as cursor:
cursor.execute("""
DELETE FROM document_chunk
WHERE collection_name = :collection_name
""", {'collection_name': collection_name})
deleted = cursor.rowcount
connection.commit()
log.info(f"Collection '{collection_name}' deleted. Removed {deleted} items.")
except Exception as e:
log.exception(f"Error deleting collection '{collection_name}': {e}")
raise