Primera version de chunkeo completo crud
This commit is contained in:
442
backend/app/services/vector_service.py
Normal file
442
backend/app/services/vector_service.py
Normal file
@@ -0,0 +1,442 @@
|
||||
"""
|
||||
Servicio de lógica de negocio para operaciones con bases de datos vectoriales.
|
||||
|
||||
Este módulo contiene toda la lógica de negocio relacionada con la gestión
|
||||
de colecciones y chunks en bases de datos vectoriales.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import List, Dict, Any, Optional
|
||||
|
||||
from app.vector_db import get_vector_db
|
||||
from app.models.vector_models import (
|
||||
CollectionCreateRequest,
|
||||
CollectionCreateResponse,
|
||||
CollectionDeleteResponse,
|
||||
CollectionExistsResponse,
|
||||
CollectionInfoResponse,
|
||||
FileExistsInCollectionResponse,
|
||||
GetChunksByFileResponse,
|
||||
DeleteFileFromCollectionResponse,
|
||||
AddChunksResponse,
|
||||
VectorDBHealthResponse,
|
||||
VectorDBErrorResponse
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class VectorService:
|
||||
"""
|
||||
Servicio para gestionar operaciones con bases de datos vectoriales.
|
||||
|
||||
Este servicio actúa como una capa intermedia entre los routers y
|
||||
la implementación de la base de datos vectorial.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Inicializa el servicio con la instancia de la base de datos vectorial."""
|
||||
self.vector_db = get_vector_db()
|
||||
|
||||
async def check_collection_exists(self, collection_name: str) -> CollectionExistsResponse:
|
||||
"""
|
||||
Verifica si una colección existe.
|
||||
|
||||
Args:
|
||||
collection_name: Nombre de la colección
|
||||
|
||||
Returns:
|
||||
CollectionExistsResponse: Response con el resultado
|
||||
"""
|
||||
try:
|
||||
exists = await self.vector_db.collection_exists(collection_name)
|
||||
logger.info(f"Verificación de colección '{collection_name}': {exists}")
|
||||
|
||||
return CollectionExistsResponse(
|
||||
exists=exists,
|
||||
collection_name=collection_name
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error al verificar colección '{collection_name}': {e}")
|
||||
raise
|
||||
|
||||
async def create_collection(
|
||||
self,
|
||||
request: CollectionCreateRequest
|
||||
) -> CollectionCreateResponse:
|
||||
"""
|
||||
Crea una nueva colección.
|
||||
|
||||
Args:
|
||||
request: Request con los datos de la colección
|
||||
|
||||
Returns:
|
||||
CollectionCreateResponse: Response con el resultado
|
||||
|
||||
Raises:
|
||||
ValueError: Si la colección ya existe
|
||||
"""
|
||||
try:
|
||||
# Verificar si ya existe
|
||||
exists = await self.vector_db.collection_exists(request.collection_name)
|
||||
|
||||
if exists:
|
||||
logger.warning(f"Intento de crear colección existente: '{request.collection_name}'")
|
||||
raise ValueError(f"La colección '{request.collection_name}' ya existe")
|
||||
|
||||
# Crear la colección
|
||||
success = await self.vector_db.create_collection(
|
||||
collection_name=request.collection_name,
|
||||
vector_size=request.vector_size,
|
||||
distance=request.distance
|
||||
)
|
||||
|
||||
if success:
|
||||
logger.info(f"Colección '{request.collection_name}' creada exitosamente")
|
||||
return CollectionCreateResponse(
|
||||
success=True,
|
||||
collection_name=request.collection_name,
|
||||
message=f"Colección '{request.collection_name}' creada exitosamente"
|
||||
)
|
||||
else:
|
||||
logger.error(f"Fallo al crear colección '{request.collection_name}'")
|
||||
raise Exception(f"No se pudo crear la colección '{request.collection_name}'")
|
||||
|
||||
except ValueError:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error al crear colección '{request.collection_name}': {e}")
|
||||
raise
|
||||
|
||||
async def delete_collection(self, collection_name: str) -> CollectionDeleteResponse:
|
||||
"""
|
||||
Elimina una colección completa.
|
||||
|
||||
Args:
|
||||
collection_name: Nombre de la colección
|
||||
|
||||
Returns:
|
||||
CollectionDeleteResponse: Response con el resultado
|
||||
|
||||
Raises:
|
||||
ValueError: Si la colección no existe
|
||||
"""
|
||||
try:
|
||||
# Verificar que existe
|
||||
exists = await self.vector_db.collection_exists(collection_name)
|
||||
|
||||
if not exists:
|
||||
logger.warning(f"Intento de eliminar colección inexistente: '{collection_name}'")
|
||||
raise ValueError(f"La colección '{collection_name}' no existe")
|
||||
|
||||
# Eliminar la colección
|
||||
success = await self.vector_db.delete_collection(collection_name)
|
||||
|
||||
if success:
|
||||
logger.info(f"Colección '{collection_name}' eliminada exitosamente")
|
||||
return CollectionDeleteResponse(
|
||||
success=True,
|
||||
collection_name=collection_name,
|
||||
message=f"Colección '{collection_name}' eliminada exitosamente"
|
||||
)
|
||||
else:
|
||||
logger.error(f"Fallo al eliminar colección '{collection_name}'")
|
||||
raise Exception(f"No se pudo eliminar la colección '{collection_name}'")
|
||||
|
||||
except ValueError:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error al eliminar colección '{collection_name}': {e}")
|
||||
raise
|
||||
|
||||
async def get_collection_info(self, collection_name: str) -> Optional[CollectionInfoResponse]:
|
||||
"""
|
||||
Obtiene información de una colección.
|
||||
|
||||
Args:
|
||||
collection_name: Nombre de la colección
|
||||
|
||||
Returns:
|
||||
Optional[CollectionInfoResponse]: Información de la colección o None
|
||||
"""
|
||||
try:
|
||||
info = await self.vector_db.get_collection_info(collection_name)
|
||||
|
||||
if info is None:
|
||||
logger.warning(f"Colección '{collection_name}' no encontrada")
|
||||
return None
|
||||
|
||||
return CollectionInfoResponse(**info)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error al obtener info de colección '{collection_name}': {e}")
|
||||
raise
|
||||
|
||||
async def check_file_exists_in_collection(
|
||||
self,
|
||||
collection_name: str,
|
||||
file_name: str
|
||||
) -> FileExistsInCollectionResponse:
|
||||
"""
|
||||
Verifica si un archivo existe en una colección.
|
||||
|
||||
Args:
|
||||
collection_name: Nombre de la colección
|
||||
file_name: Nombre del archivo
|
||||
|
||||
Returns:
|
||||
FileExistsInCollectionResponse: Response con el resultado
|
||||
"""
|
||||
try:
|
||||
# Primero verificar que la colección existe
|
||||
collection_exists = await self.vector_db.collection_exists(collection_name)
|
||||
|
||||
if not collection_exists:
|
||||
logger.warning(f"Colección '{collection_name}' no existe")
|
||||
return FileExistsInCollectionResponse(
|
||||
exists=False,
|
||||
collection_name=collection_name,
|
||||
file_name=file_name,
|
||||
chunk_count=0
|
||||
)
|
||||
|
||||
# Verificar si el archivo existe
|
||||
file_exists = await self.vector_db.file_exists_in_collection(
|
||||
collection_name,
|
||||
file_name
|
||||
)
|
||||
|
||||
chunk_count = None
|
||||
if file_exists:
|
||||
chunk_count = await self.vector_db.count_chunks_in_file(
|
||||
collection_name,
|
||||
file_name
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Archivo '{file_name}' en colección '{collection_name}': "
|
||||
f"existe={file_exists}, chunks={chunk_count}"
|
||||
)
|
||||
|
||||
return FileExistsInCollectionResponse(
|
||||
exists=file_exists,
|
||||
collection_name=collection_name,
|
||||
file_name=file_name,
|
||||
chunk_count=chunk_count
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Error al verificar archivo '{file_name}' "
|
||||
f"en colección '{collection_name}': {e}"
|
||||
)
|
||||
raise
|
||||
|
||||
async def get_chunks_by_file(
|
||||
self,
|
||||
collection_name: str,
|
||||
file_name: str,
|
||||
limit: Optional[int] = None
|
||||
) -> GetChunksByFileResponse:
|
||||
"""
|
||||
Obtiene todos los chunks de un archivo.
|
||||
|
||||
Args:
|
||||
collection_name: Nombre de la colección
|
||||
file_name: Nombre del archivo
|
||||
limit: Límite opcional de chunks
|
||||
|
||||
Returns:
|
||||
GetChunksByFileResponse: Response con los chunks
|
||||
|
||||
Raises:
|
||||
ValueError: Si la colección no existe
|
||||
"""
|
||||
try:
|
||||
# Verificar que la colección existe
|
||||
exists = await self.vector_db.collection_exists(collection_name)
|
||||
|
||||
if not exists:
|
||||
logger.warning(f"Colección '{collection_name}' no existe")
|
||||
raise ValueError(f"La colección '{collection_name}' no existe")
|
||||
|
||||
# Obtener chunks
|
||||
chunks = await self.vector_db.get_chunks_by_file(
|
||||
collection_name,
|
||||
file_name,
|
||||
limit
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Obtenidos {len(chunks)} chunks del archivo '{file_name}' "
|
||||
f"de la colección '{collection_name}'"
|
||||
)
|
||||
|
||||
return GetChunksByFileResponse(
|
||||
collection_name=collection_name,
|
||||
file_name=file_name,
|
||||
chunks=chunks,
|
||||
total_chunks=len(chunks)
|
||||
)
|
||||
|
||||
except ValueError:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Error al obtener chunks del archivo '{file_name}' "
|
||||
f"de la colección '{collection_name}': {e}"
|
||||
)
|
||||
raise
|
||||
|
||||
async def delete_file_from_collection(
|
||||
self,
|
||||
collection_name: str,
|
||||
file_name: str
|
||||
) -> DeleteFileFromCollectionResponse:
|
||||
"""
|
||||
Elimina todos los chunks de un archivo de una colección.
|
||||
|
||||
Args:
|
||||
collection_name: Nombre de la colección
|
||||
file_name: Nombre del archivo
|
||||
|
||||
Returns:
|
||||
DeleteFileFromCollectionResponse: Response con el resultado
|
||||
|
||||
Raises:
|
||||
ValueError: Si la colección no existe o el archivo no está en la colección
|
||||
"""
|
||||
try:
|
||||
# Verificar que la colección existe
|
||||
collection_exists = await self.vector_db.collection_exists(collection_name)
|
||||
|
||||
if not collection_exists:
|
||||
logger.warning(f"Colección '{collection_name}' no existe")
|
||||
raise ValueError(f"La colección '{collection_name}' no existe")
|
||||
|
||||
# Verificar que el archivo existe en la colección
|
||||
file_exists = await self.vector_db.file_exists_in_collection(
|
||||
collection_name,
|
||||
file_name
|
||||
)
|
||||
|
||||
if not file_exists:
|
||||
logger.warning(
|
||||
f"Archivo '{file_name}' no existe en colección '{collection_name}'"
|
||||
)
|
||||
raise ValueError(
|
||||
f"El archivo '{file_name}' no existe en la colección '{collection_name}'"
|
||||
)
|
||||
|
||||
# Eliminar el archivo
|
||||
chunks_deleted = await self.vector_db.delete_file_from_collection(
|
||||
collection_name,
|
||||
file_name
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Eliminados {chunks_deleted} chunks del archivo '{file_name}' "
|
||||
f"de la colección '{collection_name}'"
|
||||
)
|
||||
|
||||
return DeleteFileFromCollectionResponse(
|
||||
success=True,
|
||||
collection_name=collection_name,
|
||||
file_name=file_name,
|
||||
chunks_deleted=chunks_deleted,
|
||||
message=f"Archivo '{file_name}' eliminado exitosamente ({chunks_deleted} chunks)"
|
||||
)
|
||||
|
||||
except ValueError:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Error al eliminar archivo '{file_name}' "
|
||||
f"de la colección '{collection_name}': {e}"
|
||||
)
|
||||
raise
|
||||
|
||||
async def add_chunks(
|
||||
self,
|
||||
collection_name: str,
|
||||
chunks: List[Dict[str, Any]]
|
||||
) -> AddChunksResponse:
|
||||
"""
|
||||
Agrega chunks a una colección.
|
||||
|
||||
Args:
|
||||
collection_name: Nombre de la colección
|
||||
chunks: Lista de chunks a agregar
|
||||
|
||||
Returns:
|
||||
AddChunksResponse: Response con el resultado
|
||||
|
||||
Raises:
|
||||
ValueError: Si la colección no existe
|
||||
"""
|
||||
try:
|
||||
# Verificar que la colección existe
|
||||
exists = await self.vector_db.collection_exists(collection_name)
|
||||
|
||||
if not exists:
|
||||
logger.warning(f"Colección '{collection_name}' no existe")
|
||||
raise ValueError(f"La colección '{collection_name}' no existe")
|
||||
|
||||
# Agregar chunks
|
||||
success = await self.vector_db.add_chunks(collection_name, chunks)
|
||||
|
||||
if success:
|
||||
logger.info(
|
||||
f"Agregados {len(chunks)} chunks a la colección '{collection_name}'"
|
||||
)
|
||||
return AddChunksResponse(
|
||||
success=True,
|
||||
collection_name=collection_name,
|
||||
chunks_added=len(chunks),
|
||||
message=f"Se agregaron {len(chunks)} chunks exitosamente"
|
||||
)
|
||||
else:
|
||||
logger.error(f"Fallo al agregar chunks a '{collection_name}'")
|
||||
raise Exception(f"No se pudieron agregar los chunks a '{collection_name}'")
|
||||
|
||||
except ValueError:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error al agregar chunks a '{collection_name}': {e}")
|
||||
raise
|
||||
|
||||
async def health_check(self) -> VectorDBHealthResponse:
|
||||
"""
|
||||
Verifica el estado de la conexión con la base de datos vectorial.
|
||||
|
||||
Returns:
|
||||
VectorDBHealthResponse: Response con el estado
|
||||
"""
|
||||
try:
|
||||
is_healthy = await self.vector_db.health_check()
|
||||
|
||||
if is_healthy:
|
||||
return VectorDBHealthResponse(
|
||||
status="healthy",
|
||||
db_type="qdrant",
|
||||
message="Conexión exitosa con la base de datos vectorial"
|
||||
)
|
||||
else:
|
||||
return VectorDBHealthResponse(
|
||||
status="unhealthy",
|
||||
db_type="qdrant",
|
||||
message="No se pudo conectar con la base de datos vectorial"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error en health check: {e}")
|
||||
return VectorDBHealthResponse(
|
||||
status="error",
|
||||
db_type="qdrant",
|
||||
message=f"Error al verificar conexión: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
# Instancia global del servicio
|
||||
vector_service = VectorService()
|
||||
Reference in New Issue
Block a user