Редактировать embedding_service.py

This commit is contained in:
Markov Andrey
2026-06-30 21:28:48 +00:00
parent f77fb9d590
commit c02c7b5d1c

View File

@@ -1,18 +1,20 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
Сервис генерации эмбеддингов через GigaChat API. Сервис генерации эмбеддингов через GigaChat API с кэшированием в Redis.
Поддерживает одиночные запросы с кэшированием (lru_cache) и пакетную обработку. Поддерживает одиночные запросы с кэшированием (Redis + локальный кэш) и пакетную обработку.
Все параметры (модель, таймаут, размер кэша, verify_ssl) вынесены в конфиг.
""" """
import asyncio import asyncio
import logging import logging
import time # используется только в синхронной функции _embed_batch_sync import json
import hashlib
import time
from functools import lru_cache from functools import lru_cache
from typing import List, Optional, Any from typing import List, Optional, Any
from gigachat import GigaChat from gigachat import GigaChat
from gigachat.exceptions import AuthenticationError, RateLimitError from gigachat.exceptions import AuthenticationError, RateLimitError
import redis.asyncio as aioredis
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -23,6 +25,7 @@ except ImportError:
HAS_SPARSE = False HAS_SPARSE = False
logging.warning("fastembed не установлен, sparse-эмбеддинги отключены") logging.warning("fastembed не установлен, sparse-эмбеддинги отключены")
class EmbeddingService: class EmbeddingService:
def __init__( def __init__(
self, self,
@@ -30,7 +33,8 @@ class EmbeddingService:
model: str = "GigaChat-Embeddings", model: str = "GigaChat-Embeddings",
timeout: int = 30, timeout: int = 30,
cache_size: int = 4096, cache_size: int = 4096,
verify_ssl: bool = False verify_ssl: bool = False,
redis_url: Optional[str] = None
): ):
self.api_key = api_key self.api_key = api_key
self.model = model self.model = model
@@ -38,6 +42,18 @@ class EmbeddingService:
self.cache_size = cache_size self.cache_size = cache_size
self.verify_ssl = verify_ssl self.verify_ssl = verify_ssl
self._giga = None self._giga = None
# Локальный кэш (fallback, если Redis недоступен)
self._local_cache = {}
self._local_cache_max = cache_size
# Redis клиент
self.redis = None
if redis_url:
try:
self.redis = aioredis.from_url(redis_url, decode_responses=True)
logger.info("Redis подключён для кэширования эмбеддингов")
except Exception as e:
logger.warning(f"Не удалось подключиться к Redis: {e}, используем только локальный кэш")
# Синхронный кэш для fallback
self.embed_sync = lru_cache(maxsize=cache_size)(self._embed_sync_impl) self.embed_sync = lru_cache(maxsize=cache_size)(self._embed_sync_impl)
self._sparse_model = None self._sparse_model = None
@@ -93,11 +109,39 @@ class EmbeddingService:
if not isinstance(text, str): if not isinstance(text, str):
logger.error(f"embed получил не строку, а {type(text)}: {text!r}") logger.error(f"embed получил не строку, а {type(text)}: {text!r}")
return None return None
# 1. Проверка в Redis
if self.redis:
try:
key = f"emb:{hashlib.sha256(text.encode()).hexdigest()}"
cached = await self.redis.get(key)
if cached:
return json.loads(cached)
except Exception as e:
logger.warning(f"Redis недоступен, используем локальный кэш: {e}")
# 2. Локальный кэш
if text in self._local_cache:
return self._local_cache[text]
# 3. Вызов API
for attempt in range(retries): for attempt in range(retries):
try: try:
loop = asyncio.get_running_loop() loop = asyncio.get_running_loop()
result = await loop.run_in_executor(None, self.embed_sync, text) result = await loop.run_in_executor(None, self.embed_sync, text)
if result is not None: if result is not None:
# Сохраняем в Redis
if self.redis:
try:
key = f"emb:{hashlib.sha256(text.encode()).hexdigest()}"
await self.redis.setex(key, 3600, json.dumps(result))
except:
pass
# Сохраняем в локальный кэш
if len(self._local_cache) >= self._local_cache_max:
# Удаляем первый элемент (не самый эффективный, но простой)
self._local_cache.pop(next(iter(self._local_cache)))
self._local_cache[text] = result
return result return result
else: else:
raise ValueError("Эмбеддинг вернул None") raise ValueError("Эмбеддинг вернул None")
@@ -107,7 +151,7 @@ class EmbeddingService:
f"Попытка {attempt+1}/{retries} получить эмбеддинг не удалась: {e}. " f"Попытка {attempt+1}/{retries} получить эмбеддинг не удалась: {e}. "
f"Повтор через {wait:.1f} сек." f"Повтор через {wait:.1f} сек."
) )
await asyncio.sleep(wait) # <-- исправлено: await asyncio.sleep await asyncio.sleep(wait)
logger.error(f"Не удалось получить эмбеддинг после {retries} попыток") logger.error(f"Не удалось получить эмбеддинг после {retries} попыток")
return None return None
@@ -136,9 +180,9 @@ class EmbeddingService:
return None return None
except RateLimitError as e: except RateLimitError as e:
if attempt < 4: if attempt < 4:
wait = 2 ** attempt # 1,2,4,8 секунд wait = 2 ** attempt
logger.warning(f"Rate limit, повтор через {wait} секунд") logger.warning(f"Rate limit, повтор через {wait} секунд")
time.sleep(wait) # оставляем, так как функция синхронная и выполняется в отдельном потоке time.sleep(wait)
else: else:
logger.error("Превышено количество попыток при 429") logger.error("Превышено количество попыток при 429")
return None return None
@@ -159,14 +203,14 @@ class EmbeddingService:
if vectors is None: if vectors is None:
for attempt in range(retries): for attempt in range(retries):
wait = delay * (2 ** attempt) wait = delay * (2 ** attempt)
await asyncio.sleep(wait) # <-- исправлено: await asyncio.sleep await asyncio.sleep(wait)
vectors = await loop.run_in_executor(None, self._embed_batch_sync, batch) vectors = await loop.run_in_executor(None, self._embed_batch_sync, batch)
if vectors is not None: if vectors is not None:
break break
else: else:
return None return None
results.extend(vectors) results.extend(vectors)
await asyncio.sleep(0.5) # <-- исправлено: await asyncio.sleep await asyncio.sleep(0.5)
return results return results
async def embed_sparse_batch(self, texts: List[str]) -> List[Optional[Any]]: async def embed_sparse_batch(self, texts: List[str]) -> List[Optional[Any]]: