diff --git a/CHANGELOG.md b/CHANGELOG.md index ad846755..c35b916c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -130,6 +130,13 @@ - Убраны избыточные логи из `precache_topics_followers` - Более чистое и информативное логирование процесса кеширования +### 🚨 Исправлено +- **Запуск приложения**: Исправлена блокировка при старте из-за SentenceTransformers + - Переведен импорт `sentence_transformers` на lazy loading + - Модель загружается только при первом использовании поиска + - Исправлена ошибка deprecated `TRANSFORMERS_CACHE` на `HF_HOME` + - Приложение теперь запускается мгновенно без ожидания загрузки ML моделей + ## [0.9.13] - 2025-08-27 ### 🗑️ Удалено diff --git a/docs/testing.md b/docs/testing.md index 6f62a54c..5b1a83ae 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -209,11 +209,6 @@ class MockInfo: } self.field_nodes = [MockFieldNode(requested_fields or [])] -# Патчинг зависимостей -@patch('storage.redis.aioredis') -def test_redis_connection(mock_aioredis): - # Тест логики - pass ``` ### Асинхронные тесты diff --git a/pyproject.toml b/pyproject.toml index b1aa363b..38303796 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,7 +57,7 @@ dependencies = [ # https://docs.astral.sh/uv/concepts/dependencies/#development-dependencies [dependency-groups] dev = [ - "fakeredis[aioredis]", + "fakeredis", "pytest", "pytest-asyncio", "pytest-cov", @@ -68,7 +68,7 @@ dev = [ ] test = [ - "fakeredis[aioredis]", + "fakeredis", "pytest", "pytest-asyncio", "pytest-cov", diff --git a/scripts/preload_models.py b/scripts/preload_models.py index 627e9b7b..e9b46d88 100755 --- a/scripts/preload_models.py +++ b/scripts/preload_models.py @@ -15,8 +15,10 @@ def get_models_cache_dir() -> str: """Определяет лучшую папку для кеша моделей""" # Пробуем /dump если доступен для записи dump_path = Path("/dump") - print(f"🔍 Checking /dump - exists: {dump_path.exists()}, writable: {os.access('/dump', os.W_OK) if dump_path.exists() else 'N/A'}") - + print( + f"🔍 Checking /dump - exists: {dump_path.exists()}, writable: {os.access('/dump', os.W_OK) if dump_path.exists() else 'N/A'}" + ) + if dump_path.exists() and os.access("/dump", os.W_OK): cache_dir = "/dump/huggingface" try: diff --git a/services/search.py b/services/search.py index 04e208d3..b41449ca 100644 --- a/services/search.py +++ b/services/search.py @@ -9,11 +9,12 @@ from typing import Any, Dict, List import muvera import numpy as np -from sentence_transformers import SentenceTransformer from settings import MUVERA_INDEX_NAME, SEARCH_MAX_BATCH_SIZE, SEARCH_PREFETCH_SIZE from utils.logger import root_logger as logger +# Отложенный импорт SentenceTransformer для избежания блокировки запуска +SentenceTransformer = None primary_model = "paraphrase-multilingual-MiniLM-L12-v2" @@ -22,7 +23,9 @@ def get_models_cache_dir() -> str: """Определяет лучшую папку для кеша моделей""" # Пробуем /dump если доступен для записи dump_path = Path("/dump") - logger.info(f"🔍 Checking /dump - exists: {dump_path.exists()}, writable: {os.access('/dump', os.W_OK) if dump_path.exists() else 'N/A'}") + logger.info( + f"🔍 Checking /dump - exists: {dump_path.exists()}, writable: {os.access('/dump', os.W_OK) if dump_path.exists() else 'N/A'}" + ) if dump_path.exists() and os.access("/dump", os.W_OK): cache_dir = "/dump/huggingface" @@ -41,13 +44,28 @@ def get_models_cache_dir() -> str: MODELS_CACHE_DIR = get_models_cache_dir() -os.environ.setdefault("TRANSFORMERS_CACHE", MODELS_CACHE_DIR) +# Используем HF_HOME вместо устаревшего TRANSFORMERS_CACHE os.environ.setdefault("HF_HOME", MODELS_CACHE_DIR) # Global collection for background tasks background_tasks: List[asyncio.Task] = [] +def _lazy_import_sentence_transformers(): + """🔄 Lazy import SentenceTransformer для избежания блокировки старта приложения""" + global SentenceTransformer # noqa: PLW0603 + if SentenceTransformer is None: + try: + from sentence_transformers import SentenceTransformer as SentenceTransformerClass + + SentenceTransformer = SentenceTransformerClass + logger.info("✅ SentenceTransformer импортирован успешно") + except ImportError as e: + logger.error(f"❌ Не удалось импортировать SentenceTransformer: {e}") + SentenceTransformer = None + return SentenceTransformer + + class MuveraWrapper: """🔍 Real vector search with SentenceTransformers + FDE encoding""" @@ -60,42 +78,10 @@ class MuveraWrapper: self.documents: Dict[str, Dict[str, Any]] = {} # Simple in-memory storage for demo self.embeddings: Dict[str, np.ndarray | None] = {} # Store encoded embeddings - # 🚀 Инициализируем реальную модель эмбедингов с локальным кешом - try: - logger.info(f"💾 Using models cache directory: {MODELS_CACHE_DIR}") - - # Проверяем наличие основной модели - is_cached = self._is_model_cached(primary_model) - if is_cached: - logger.info(f"🔍 Found cached model: {primary_model}") - else: - logger.info(f"🔽 Downloading model: {primary_model}") - - # Используем многоязычную модель, хорошо работающую с русским - self.encoder = SentenceTransformer( - primary_model, - cache_folder=MODELS_CACHE_DIR, - local_files_only=is_cached, # Не скачиваем если уже есть в кеше - ) - logger.info("🔍 SentenceTransformer model loaded successfully") - except Exception as e: - logger.error(f"Failed to load primary SentenceTransformer: {e}") - # Fallback - простая модель - try: - fallback_model = "all-MiniLM-L6-v2" - is_fallback_cached = self._is_model_cached(fallback_model) - if is_fallback_cached: - logger.info(f"🔍 Found cached fallback model: {fallback_model}") - else: - logger.info(f"🔽 Downloading fallback model: {fallback_model}") - - self.encoder = SentenceTransformer( - fallback_model, cache_folder=MODELS_CACHE_DIR, local_files_only=is_fallback_cached - ) - logger.info("🔍 Fallback SentenceTransformer model loaded") - except Exception: - logger.error("Failed to load any SentenceTransformer model") - self.encoder = None + # 🚀 Откладываем инициализацию модели до первого использования + logger.info("🔄 MuveraWrapper инициализирован - модель будет загружена при первом использовании") + self.encoder = None + self._model_loaded = False def _is_model_cached(self, model_name: str) -> bool: """🔍 Проверяет наличие модели в кеше""" @@ -128,6 +114,60 @@ class MuveraWrapper: logger.debug(f"Error checking model cache for {model_name}: {e}") return False + def _ensure_model_loaded(self) -> bool: + """🔄 Убеждаемся что модель загружена (lazy loading)""" + if self._model_loaded: + return self.encoder is not None + + # Импортируем SentenceTransformer при первой необходимости + sentence_transformer_class = _lazy_import_sentence_transformers() + if sentence_transformer_class is None: + logger.error("❌ SentenceTransformer недоступен") + return False + + try: + logger.info(f"💾 Using models cache directory: {MODELS_CACHE_DIR}") + + # Проверяем наличие основной модели + is_cached = self._is_model_cached(primary_model) + if is_cached: + logger.info(f"🔍 Found cached model: {primary_model}") + else: + logger.info(f"🔽 Downloading model: {primary_model}") + + # Используем многоязычную модель, хорошо работающую с русским + self.encoder = sentence_transformer_class( + primary_model, + cache_folder=MODELS_CACHE_DIR, + local_files_only=is_cached, # Не скачиваем если уже есть в кеше + ) + logger.info("🔍 SentenceTransformer model loaded successfully") + self._model_loaded = True + return True + + except Exception as e: + logger.error(f"Failed to load primary SentenceTransformer: {e}") + # Fallback - простая модель + try: + fallback_model = "all-MiniLM-L6-v2" + is_fallback_cached = self._is_model_cached(fallback_model) + if is_fallback_cached: + logger.info(f"🔍 Found cached fallback model: {fallback_model}") + else: + logger.info(f"🔽 Downloading fallback model: {fallback_model}") + + self.encoder = sentence_transformer_class( + fallback_model, cache_folder=MODELS_CACHE_DIR, local_files_only=is_fallback_cached + ) + logger.info("🔍 Fallback SentenceTransformer model loaded") + self._model_loaded = True + return True + except Exception: + logger.error("Failed to load any SentenceTransformer model") + self.encoder = None + self._model_loaded = True # Помечаем как попытка завершена + return False + async def async_init(self) -> None: """🔄 Асинхронная инициализация - восстановление индекса из файла""" try: @@ -153,7 +193,12 @@ class MuveraWrapper: async def search(self, query: str, limit: int) -> List[Dict[str, Any]]: """🔍 Real vector search using SentenceTransformers + FDE encoding""" - if not query.strip() or not self.encoder: + if not query.strip(): + return [] + + # Загружаем модель при первом использовании + if not self._ensure_model_loaded(): + logger.warning("🔍 Search unavailable - model not loaded") return [] try: @@ -194,7 +239,8 @@ class MuveraWrapper: async def index(self, documents: List[Dict[str, Any]], silent: bool = False) -> None: """🚀 Index documents using real SentenceTransformers + FDE encoding""" - if not self.encoder: + # Загружаем модель при первом использовании + if not self._ensure_model_loaded(): if not silent: logger.warning("🔍 No encoder available for indexing") return diff --git a/uv.lock b/uv.lock index 86a53e63..1868c59c 100644 --- a/uv.lock +++ b/uv.lock @@ -400,7 +400,7 @@ wheels = [ [[package]] name = "discours-core" -version = "0.9.14" +version = "0.9.18" source = { editable = "." } dependencies = [ { name = "ariadne" }, @@ -492,7 +492,7 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ - { name = "fakeredis", extras = ["aioredis"] }, + { name = "fakeredis" }, { name = "mypy" }, { name = "playwright" }, { name = "pytest" }, @@ -506,7 +506,7 @@ lint = [ { name = "ruff" }, ] test = [ - { name = "fakeredis", extras = ["aioredis"] }, + { name = "fakeredis" }, { name = "playwright" }, { name = "pytest" }, { name = "pytest-asyncio" },