This commit is contained in:
@@ -130,6 +130,13 @@
|
|||||||
- Убраны избыточные логи из `precache_topics_followers`
|
- Убраны избыточные логи из `precache_topics_followers`
|
||||||
- Более чистое и информативное логирование процесса кеширования
|
- Более чистое и информативное логирование процесса кеширования
|
||||||
|
|
||||||
|
### 🚨 Исправлено
|
||||||
|
- **Запуск приложения**: Исправлена блокировка при старте из-за SentenceTransformers
|
||||||
|
- Переведен импорт `sentence_transformers` на lazy loading
|
||||||
|
- Модель загружается только при первом использовании поиска
|
||||||
|
- Исправлена ошибка deprecated `TRANSFORMERS_CACHE` на `HF_HOME`
|
||||||
|
- Приложение теперь запускается мгновенно без ожидания загрузки ML моделей
|
||||||
|
|
||||||
## [0.9.13] - 2025-08-27
|
## [0.9.13] - 2025-08-27
|
||||||
|
|
||||||
### 🗑️ Удалено
|
### 🗑️ Удалено
|
||||||
|
|||||||
@@ -209,11 +209,6 @@ class MockInfo:
|
|||||||
}
|
}
|
||||||
self.field_nodes = [MockFieldNode(requested_fields or [])]
|
self.field_nodes = [MockFieldNode(requested_fields or [])]
|
||||||
|
|
||||||
# Патчинг зависимостей
|
|
||||||
@patch('storage.redis.aioredis')
|
|
||||||
def test_redis_connection(mock_aioredis):
|
|
||||||
# Тест логики
|
|
||||||
pass
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Асинхронные тесты
|
### Асинхронные тесты
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ dependencies = [
|
|||||||
# https://docs.astral.sh/uv/concepts/dependencies/#development-dependencies
|
# https://docs.astral.sh/uv/concepts/dependencies/#development-dependencies
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
dev = [
|
dev = [
|
||||||
"fakeredis[aioredis]",
|
"fakeredis",
|
||||||
"pytest",
|
"pytest",
|
||||||
"pytest-asyncio",
|
"pytest-asyncio",
|
||||||
"pytest-cov",
|
"pytest-cov",
|
||||||
@@ -68,7 +68,7 @@ dev = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
test = [
|
test = [
|
||||||
"fakeredis[aioredis]",
|
"fakeredis",
|
||||||
"pytest",
|
"pytest",
|
||||||
"pytest-asyncio",
|
"pytest-asyncio",
|
||||||
"pytest-cov",
|
"pytest-cov",
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ def get_models_cache_dir() -> str:
|
|||||||
"""Определяет лучшую папку для кеша моделей"""
|
"""Определяет лучшую папку для кеша моделей"""
|
||||||
# Пробуем /dump если доступен для записи
|
# Пробуем /dump если доступен для записи
|
||||||
dump_path = Path("/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):
|
if dump_path.exists() and os.access("/dump", os.W_OK):
|
||||||
cache_dir = "/dump/huggingface"
|
cache_dir = "/dump/huggingface"
|
||||||
|
|||||||
@@ -9,11 +9,12 @@ from typing import Any, Dict, List
|
|||||||
|
|
||||||
import muvera
|
import muvera
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from sentence_transformers import SentenceTransformer
|
|
||||||
|
|
||||||
from settings import MUVERA_INDEX_NAME, SEARCH_MAX_BATCH_SIZE, SEARCH_PREFETCH_SIZE
|
from settings import MUVERA_INDEX_NAME, SEARCH_MAX_BATCH_SIZE, SEARCH_PREFETCH_SIZE
|
||||||
from utils.logger import root_logger as logger
|
from utils.logger import root_logger as logger
|
||||||
|
|
||||||
|
# Отложенный импорт SentenceTransformer для избежания блокировки запуска
|
||||||
|
SentenceTransformer = None
|
||||||
primary_model = "paraphrase-multilingual-MiniLM-L12-v2"
|
primary_model = "paraphrase-multilingual-MiniLM-L12-v2"
|
||||||
|
|
||||||
|
|
||||||
@@ -22,7 +23,9 @@ def get_models_cache_dir() -> str:
|
|||||||
"""Определяет лучшую папку для кеша моделей"""
|
"""Определяет лучшую папку для кеша моделей"""
|
||||||
# Пробуем /dump если доступен для записи
|
# Пробуем /dump если доступен для записи
|
||||||
dump_path = Path("/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):
|
if dump_path.exists() and os.access("/dump", os.W_OK):
|
||||||
cache_dir = "/dump/huggingface"
|
cache_dir = "/dump/huggingface"
|
||||||
@@ -41,13 +44,28 @@ def get_models_cache_dir() -> str:
|
|||||||
|
|
||||||
|
|
||||||
MODELS_CACHE_DIR = get_models_cache_dir()
|
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)
|
os.environ.setdefault("HF_HOME", MODELS_CACHE_DIR)
|
||||||
|
|
||||||
# Global collection for background tasks
|
# Global collection for background tasks
|
||||||
background_tasks: List[asyncio.Task] = []
|
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:
|
class MuveraWrapper:
|
||||||
"""🔍 Real vector search with SentenceTransformers + FDE encoding"""
|
"""🔍 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.documents: Dict[str, Dict[str, Any]] = {} # Simple in-memory storage for demo
|
||||||
self.embeddings: Dict[str, np.ndarray | None] = {} # Store encoded embeddings
|
self.embeddings: Dict[str, np.ndarray | None] = {} # Store encoded embeddings
|
||||||
|
|
||||||
# 🚀 Инициализируем реальную модель эмбедингов с локальным кешом
|
# 🚀 Откладываем инициализацию модели до первого использования
|
||||||
try:
|
logger.info("🔄 MuveraWrapper инициализирован - модель будет загружена при первом использовании")
|
||||||
logger.info(f"💾 Using models cache directory: {MODELS_CACHE_DIR}")
|
self.encoder = None
|
||||||
|
self._model_loaded = False
|
||||||
# Проверяем наличие основной модели
|
|
||||||
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
|
|
||||||
|
|
||||||
def _is_model_cached(self, model_name: str) -> bool:
|
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}")
|
logger.debug(f"Error checking model cache for {model_name}: {e}")
|
||||||
return False
|
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:
|
async def async_init(self) -> None:
|
||||||
"""🔄 Асинхронная инициализация - восстановление индекса из файла"""
|
"""🔄 Асинхронная инициализация - восстановление индекса из файла"""
|
||||||
try:
|
try:
|
||||||
@@ -153,7 +193,12 @@ class MuveraWrapper:
|
|||||||
|
|
||||||
async def search(self, query: str, limit: int) -> List[Dict[str, Any]]:
|
async def search(self, query: str, limit: int) -> List[Dict[str, Any]]:
|
||||||
"""🔍 Real vector search using SentenceTransformers + FDE encoding"""
|
"""🔍 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 []
|
return []
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -194,7 +239,8 @@ class MuveraWrapper:
|
|||||||
|
|
||||||
async def index(self, documents: List[Dict[str, Any]], silent: bool = False) -> None:
|
async def index(self, documents: List[Dict[str, Any]], silent: bool = False) -> None:
|
||||||
"""🚀 Index documents using real SentenceTransformers + FDE encoding"""
|
"""🚀 Index documents using real SentenceTransformers + FDE encoding"""
|
||||||
if not self.encoder:
|
# Загружаем модель при первом использовании
|
||||||
|
if not self._ensure_model_loaded():
|
||||||
if not silent:
|
if not silent:
|
||||||
logger.warning("🔍 No encoder available for indexing")
|
logger.warning("🔍 No encoder available for indexing")
|
||||||
return
|
return
|
||||||
|
|||||||
6
uv.lock
generated
6
uv.lock
generated
@@ -400,7 +400,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "discours-core"
|
name = "discours-core"
|
||||||
version = "0.9.14"
|
version = "0.9.18"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "ariadne" },
|
{ name = "ariadne" },
|
||||||
@@ -492,7 +492,7 @@ requires-dist = [
|
|||||||
|
|
||||||
[package.metadata.requires-dev]
|
[package.metadata.requires-dev]
|
||||||
dev = [
|
dev = [
|
||||||
{ name = "fakeredis", extras = ["aioredis"] },
|
{ name = "fakeredis" },
|
||||||
{ name = "mypy" },
|
{ name = "mypy" },
|
||||||
{ name = "playwright" },
|
{ name = "playwright" },
|
||||||
{ name = "pytest" },
|
{ name = "pytest" },
|
||||||
@@ -506,7 +506,7 @@ lint = [
|
|||||||
{ name = "ruff" },
|
{ name = "ruff" },
|
||||||
]
|
]
|
||||||
test = [
|
test = [
|
||||||
{ name = "fakeredis", extras = ["aioredis"] },
|
{ name = "fakeredis" },
|
||||||
{ name = "playwright" },
|
{ name = "playwright" },
|
||||||
{ name = "pytest" },
|
{ name = "pytest" },
|
||||||
{ name = "pytest-asyncio" },
|
{ name = "pytest-asyncio" },
|
||||||
|
|||||||
Reference in New Issue
Block a user