# Changelog ## [2.2.0] - 2024-12-20 - Model Upgrade & Critical Fixes ### 🔥 Критические исправления 1. **Новая модель: `ai-forever/ru-en-RoSBERTa`** - Оптимизирована для русского языка - Размерность: 768 (вместо 384) - Лучшее качество для semantic matching 2. **Нормализация эмбеддингов** ```python model.encode( texts, batch_size=32, normalize_embeddings=True, # КРИТИЧНО для cosine similarity! ... ) ``` - pgvector + cosine (`<=>`) ожидает нормализованные векторы - Без нормализации similarity "плывёт" и хуже ранжирование 3. **Унифицированная кэш-логика** - Новая функция `encode_single_async_with_flag()` возвращает `(embedding, cached)` - Исправлены двойные `CACHE_MISSES` - Корректный флаг `cached` во всех ответах 4. **MAX_CONCURRENT_REQUESTS = 6** (было 4) - Оптимально для 8-16 vCPU ### ⚠️ Breaking Changes - **Размерность эмбеддингов изменилась: 384 → 768** - Необходимо переиндексировать все объекты! - SQL миграция: ```sql ALTER TABLE leads DROP COLUMN embedding; ALTER TABLE leads ADD COLUMN embedding vector(768); -- Пересоздать индекс CREATE INDEX ON leads USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100); ``` --- ## [2.1.0] - 2024-12-19 - Production-Ready Release ### 🚀 Основные улучшения Полная переработка сервиса для production-ready статуса по рекомендациям экспертов. --- ## Что было (v2.0.0) vs Что стало (v2.1.0) ### 1. Асинхронность и CPU/IO разграничение **Было:** ```python # Синхронный вызов, блокирует event loop FastAPI embedding = model.encode(request.text, convert_to_numpy=True) ``` **Стало:** ```python # Асинхронный вызов через ThreadPoolExecutor async def encode_async(texts: List[str]) -> np.ndarray: loop = asyncio.get_event_loop() result = await asyncio.wait_for( loop.run_in_executor( executor, lambda: model.encode(texts, convert_to_numpy=True, show_progress_bar=False) ), timeout=ENCODE_TIMEOUT_SECONDS ) return result ``` **Влияние на индексацию:** - ✅ Сервис остаётся отзывчивым при параллельных запросах - ✅ Таймаут 30 секунд предотвращает "зависание" запросов - ✅ До 4 параллельных encode операций (настраивается через `MAX_CONCURRENT_REQUESTS`) --- ### 2. Валидация входных данных **Было:** - Нет ограничений на размер текста - Нет ограничений на размер батча - Возможность DoS-атаки через огромные запросы **Стало:** ```python MAX_BATCH_SIZE = 128 # Максимум элементов в батче MAX_TEXT_LENGTH = 10000 # Максимум символов в тексте MAX_CONCURRENT_REQUESTS = 4 # Параллельные encode операции ENCODE_TIMEOUT_SECONDS = 30 # Таймаут на encode class EmbedRequest(BaseModel): text: str = Field(..., min_length=1, max_length=MAX_TEXT_LENGTH) class BatchRequest(BaseModel): items: List[BatchItem] = Field(..., max_length=MAX_BATCH_SIZE) ``` **Влияние на индексацию:** - ✅ Защита от перегрузки сервиса большими запросами - ✅ Понятные 400 ошибки при превышении лимитов - ✅ Предсказуемое время ответа --- ### 3. Prometheus метрики **Было:** - Нет метрик - Невозможно отследить производительность - "Слепой полёт" в production **Стало:** ```python # Endpoint /metrics возвращает: embedding_requests_total{endpoint="/embed", status="success"} 150 embedding_request_latency_seconds_bucket{endpoint="/embed", le="0.1"} 120 embedding_batch_size_bucket{le="10"} 45 embedding_encode_failures_total{reason="timeout"} 2 embedding_model_loaded 1 embedding_cache_hits_total 89 embedding_cache_misses_total 61 embedding_active_requests 3 ``` **Влияние на индексацию:** - ✅ Мониторинг в Grafana: requests/s, latency, batch sizes - ✅ Алерты на encode_failures и model_loaded - ✅ Отслеживание cache hit rate для оптимизации --- ### 4. Rate Limiting **Было:** - Нет ограничений на частоту запросов - Возможность перегрузки сервиса одним клиентом **Стало:** ```python RATE_LIMIT = "100/minute" # Для одиночных запросов RATE_LIMIT_BATCH = "20/minute" # Для батчей @app.post("/embed") @limiter.limit(RATE_LIMIT) async def embed_text(request: Request, body: EmbedRequest): ... ``` **Влияние на индексацию:** - ✅ Защита от перегрузки - ✅ Справедливое распределение ресурсов между клиентами - ✅ HTTP 429 при превышении лимита --- ### 5. In-Memory кэширование **Было:** - Каждый запрос генерирует эмбеддинг заново - Повторные запросы тратят CPU **Стало:** ```python CACHE_ENABLED = True CACHE_TTL_SECONDS = 3600 # 1 час CACHE_MAX_SIZE = 10000 # 10k эмбеддингов # Автоматическое кэширование: cache_key = hashlib.sha256(text.encode()).hexdigest() if cache_key in embedding_cache: return embedding_cache[cache_key] # Мгновенно! ``` **Влияние на индексацию:** - ✅ **До 100x ускорение** для повторных запросов (0.1-0.5s → <1ms) - ✅ Экономия CPU для часто запрашиваемых объектов - ✅ TTL автоматически инвалидирует устаревший кэш - ✅ Статистика: `GET /cache/stats`, очистка: `POST /cache/clear` --- ### 6. Версионирование модели **Было:** - Невозможно отследить какая модель использовалась - Проблемы при обновлении модели **Стало:** ```python # Каждый ответ содержит: { "embedding": [...], "model_version": "2.1.0", "model_checksum": "a1b2c3d4e5f6" # MD5 от model_name:dimensions } ``` **Влияние на индексацию:** - ✅ Go Backend может хранить model_checksum вместе с эмбеддингом - ✅ При обновлении модели можно переиндексировать только устаревшие записи - ✅ `/model-info` показывает время загрузки модели --- ### 7. Structured Logging (JSON) **Было:** ``` Loading embedding model: sentence-transformers/... Model loaded. Dimensions: 384 ``` **Стало:** ```json {"event": "model_loading", "model": "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2", "timestamp": "2024-12-19T10:30:00Z"} {"event": "model_loaded", "dimensions": 384, "checksum": "a1b2c3d4e5f6", "load_time_seconds": 12.5, "timestamp": "2024-12-19T10:30:12Z"} {"event": "batch_process", "total": 50, "successful": 50, "cached": 12, "timestamp": "2024-12-19T10:35:00Z"} ``` **Влияние на индексацию:** - ✅ Интеграция с ELK/Loki/CloudWatch - ✅ Поиск и анализ логов - ✅ Трейсинг запросов --- ### 8. Улучшенная батч-обработка **Было:** ```python # Все тексты генер��руются заново embeddings = model.encode(texts, convert_to_numpy=True) ``` **Стало:** ```python # Сначала проверяем кэш for item in body.items: cache_key = get_cache_key(prepared) if cache_key in embedding_cache: # Мгновенно из кэша! cached_count += 1 continue texts_to_encode.append(prepared) # Только некэшированные идут в model.encode if texts_to_encode: embeddings = await encode_async(texts_to_encode) ``` **Влияние на индексацию:** - ✅ Смешанный батч (кэш + compute) обрабатывается оптимально - ✅ Ответ содержит `cached_count` для аналитики - ✅ Каждый `BatchResultItem` имеет флаг `cached: true/false` --- ### 9. Graceful Error Handling **Было:** - 500 при любой ошибке - Нет информации о причине **Стало:** ```python # Таймаут except asyncio.TimeoutError: ENCODE_FAILURES.labels(reason="timeout").inc() raise HTTPException(status_code=503, detail=f"Encoding timeout after {ENCODE_TIMEOUT_SECONDS}s") # Ошибка модели except Exception as e: ENCODE_FAILURES.labels(reason="error").inc() raise HTTPException(status_code=500, detail=f"Encoding error: {str(e)}") ``` **Влияние на индексацию:** - ✅ 503 для временных проблем (клиент может повторить) - ✅ 400 для ошибок валидации (клиент должен исправить запрос) - ✅ Метрики для алертов на ошибки --- ### 10. Новые endpoints | Endpoint | Описание | |----------|----------| | `GET /metrics` | Prometheus метрики | | `GET /cache/stats` | Статистика кэша | | `POST /cache/clear` | Очистка кэша | --- ## Конфигурация (переменные окружения) | Переменная | По умолчанию | Описание | |------------|--------------|----------| | `EMBEDDING_MODEL` | `paraphrase-multilingual-MiniLM-L12-v2` | Модель эмбеддингов | | `MAX_BATCH_SIZE` | `128` | Максимум элементов в батче | | `MAX_TEXT_LENGTH` | `10000` | Максимум символов в тексте | | `MAX_CONCURRENT_REQUESTS` | `4` | Параллельные encode | | `ENCODE_TIMEOUT_SECONDS` | `30` | Таймаут на encode | | `RATE_LIMIT` | `100/minute` | Rate limit для одиночных | | `RATE_LIMIT_BATCH` | `20/minute` | Rate limit для батчей | | `CACHE_ENABLED` | `true` | Включить кэш | | `CACHE_TTL_SECONDS` | `3600` | TTL кэша (1 час) | | `CACHE_MAX_SIZE` | `10000` | Максимум записей в кэше | | `ALLOWED_ORIGINS` | `*` | CORS origins | --- ## Оценка готовности к production | Критерий | v2.0.0 | v2.1.0 | |----------|--------|--------| | Асинхронность | ❌ Блокирует event loop | ✅ ThreadPoolExecutor | | Валидация | ❌ Нет лимитов | ✅ Batch/text limits | | Метрики | ❌ Нет | ✅ Prometheus | | Rate limiting | ❌ Нет | ✅ slowapi | | Кэширование | ❌ Нет | ✅ TTLCache | | Версионирование | ❌ Нет | ✅ checksum в ответах | | Логирование | ❌ print() | ✅ structlog JSON | | Таймауты | ❌ Нет | ✅ 30s timeout | | Error handling | ❌ Базовый | ✅ Graceful 503/400 | **Рейтинг:** 5/10 → **8/10** ✅ --- ## Следующие шаги (roadmap для 9/10) 1. **Redis кэширование** — для распределённого кэша 2. **OpenTelemetry tracing** — trace_id propagation 3. **API Key авторизация** — уже подготовлено (`API_KEY` env) 4. **Background workers** — для длинных reindex-batch (Celery/RQ) 5. **ONNX Runtime** — для ускорения инференса 6. **Health check с warmup** — pre-load model weights --- ## Миграция с v2.0.0 1. Обновить `requirements.txt` 2. Обновить `main.py` 3. Обновить `Dockerfile` (опционально) 4. Настроить Prometheus scraping на `/metrics` 5. Добавить переменные окружения (опционально) **Breaking changes:** Нет. Все endpoints совместимы.