muryshev's picture
update
86c402d
raw
history blame
8.86 kB
import logging
from typing import Callable, Optional
from uuid import UUID
import numpy as np
from ntr_fileparser import ParsedDocument
from ntr_text_fragmentation import Destructurer, InjectionBuilder, LinkerEntity
from common.configuration import Configuration
from components.dbo.chunk_repository import ChunkRepository
from components.embedding_extraction import EmbeddingExtractor
from components.nmd.faiss_vector_search import FaissVectorSearch
logger = logging.getLogger(__name__)
class EntityService:
"""
Сервис для работы с сущностями.
Объединяет функциональность chunk_repository, destructurer, injection_builder и faiss_vector_search.
"""
def __init__(
self,
vectorizer: EmbeddingExtractor,
chunk_repository: ChunkRepository,
config: Configuration,
) -> None:
"""
Инициализация сервиса.
Args:
vectorizer: Модель для извлечения эмбеддингов
chunk_repository: Репозиторий для работы с чанками
config: Конфигурация приложения
"""
self.vectorizer = vectorizer
self.config = config
self.chunk_repository = chunk_repository
self.faiss_search = None # Инициализируется при необходимости
self.current_dataset_id = None # Текущий dataset_id
def _ensure_faiss_initialized(self, dataset_id: int) -> None:
"""
Проверяет и при необходимости инициализирует или обновляет FAISS индекс.
Args:
dataset_id: ID датасета для инициализации
"""
# Если индекс не инициализирован или датасет изменился
if self.faiss_search is None or self.current_dataset_id != dataset_id:
logger.info(f'Initializing FAISS for dataset {dataset_id}')
entities, embeddings = self.chunk_repository.get_searching_entities(dataset_id)
if entities:
# Создаем словарь только из не-None эмбеддингов
embeddings_dict = {
str(entity.id): embedding # Преобразуем UUID в строку для ключа
for entity, embedding in zip(entities, embeddings)
if embedding is not None
}
if embeddings_dict: # Проверяем, что есть хотя бы один эмбеддинг
self.faiss_search = FaissVectorSearch(
self.vectorizer,
embeddings_dict,
self.config.db_config,
)
self.current_dataset_id = dataset_id
logger.info(f'FAISS initialized for dataset {dataset_id} with {len(embeddings_dict)} embeddings')
else:
logger.warning(f'No valid embeddings found for dataset {dataset_id}')
self.faiss_search = None
self.current_dataset_id = None
else:
logger.warning(f'No entities found for dataset {dataset_id}')
self.faiss_search = None
self.current_dataset_id = None
def process_document(
self,
document: ParsedDocument,
dataset_id: int,
progress_callback: Optional[Callable] = None,
**destructurer_kwargs,
) -> None:
"""
Обработка документа: разбиение на чанки и сохранение в базу.
Args:
document: Документ для обработки
dataset_id: ID датасета
progress_callback: Функция для отслеживания прогресса
**destructurer_kwargs: Дополнительные параметры для Destructurer
"""
logger.info(f"Processing document {document.name} for dataset {dataset_id}")
# Создаем деструктуризатор с параметрами по умолчанию
destructurer = Destructurer(
document,
strategy_name="fixed_size",
process_tables=True,
**{
"words_per_chunk": 50,
"overlap_words": 25,
"respect_sentence_boundaries": True,
**destructurer_kwargs,
}
)
# Получаем сущности
entities = destructurer.destructure()
# Фильтруем сущности для поиска
filtering_entities = [entity for entity in entities if entity.in_search_text is not None]
filtering_texts = [entity.in_search_text for entity in filtering_entities]
# Получаем эмбеддинги с поддержкой callback
embeddings = self.vectorizer.vectorize(filtering_texts, progress_callback)
embeddings_dict = {
str(entity.id): embedding # Преобразуем UUID в строку для ключа
for entity, embedding in zip(filtering_entities, embeddings)
}
# Сохраняем в базу
self.chunk_repository.add_entities(entities, dataset_id, embeddings_dict)
# Переинициализируем FAISS индекс, если это текущий датасет
if self.current_dataset_id == dataset_id:
self._ensure_faiss_initialized(dataset_id)
logger.info(f"Added {len(entities)} entities to dataset {dataset_id}")
def build_text(
self,
entities: list[LinkerEntity],
chunk_scores: Optional[list[float]] = None,
include_tables: bool = True,
max_documents: Optional[int] = None,
) -> str:
"""
Сборка текста из сущностей.
Args:
entities: Список сущностей
chunk_scores: Список весов чанков
include_tables: Флаг включения таблиц
max_documents: Максимальное количество документов
Returns:
Собранный текст
"""
logger.info(f"Building text for {len(entities)} entities")
if chunk_scores is not None:
chunk_scores = {entity.id: score for entity, score in zip(entities, chunk_scores)}
builder = InjectionBuilder(self.chunk_repository)
return builder.build(
[entity.id for entity in entities], # Передаем UUID напрямую
chunk_scores=chunk_scores,
include_tables=include_tables,
max_documents=max_documents,
)
def search_similar(
self,
query: str,
dataset_id: int,
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
"""
Поиск похожих сущностей.
Args:
query: Текст запроса
dataset_id: ID датасета
Returns:
tuple[np.ndarray, np.ndarray, np.ndarray]:
- Вектор запроса
- Оценки сходства
- Идентификаторы найденных сущностей
"""
# Убеждаемся, что FAISS инициализирован для текущего датасета
self._ensure_faiss_initialized(dataset_id)
if self.faiss_search is None:
return np.array([]), np.array([]), np.array([])
# Выполняем поиск
return self.faiss_search.search_vectors(query)
def add_neighboring_chunks(
self,
entities: list[LinkerEntity],
max_distance: int = 1,
) -> list[LinkerEntity]:
"""
Добавление соседних чанков.
Args:
entities: Список сущностей
max_distance: Максимальное расстояние для поиска соседей
Returns:
Расширенный список сущностей
"""
# Убедимся, что все ID представлены в UUID формате
for entity in entities:
if not isinstance(entity.id, UUID):
entity.id = UUID(str(entity.id))
builder = InjectionBuilder(self.chunk_repository)
return builder.add_neighboring_chunks(entities, max_distance)