matching / CHANGELOG.md
Calcifer0323's picture
v2.2.0: ai-forever/ru-en-RoSBERTa + normalize_embeddings + cache fix
8b656e5

Changelog

[2.2.0] - 2024-12-20 - Model Upgrade & Critical Fixes

🔥 Критические исправления

  1. Новая модель: ai-forever/ru-en-RoSBERTa

    • Оптимизирована для русского языка
    • Размерность: 768 (вместо 384)
    • Лучшее качество для semantic matching
  2. Нормализация эмбеддингов

    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 миграция:
    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 разграничение

Было:

# Синхронный вызов, блокирует event loop FastAPI
embedding = model.encode(request.text, convert_to_numpy=True)

Стало:

# Асинхронный вызов через 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-атаки через огромные запросы

Стало:

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

Стало:

# 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

Было:

  • Нет ограничений на частоту запросов
  • Возможность перегрузки сервиса одним клиентом

Стало:

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

Стало:

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. Версионирование модели

Было:

  • Невозможно отследить какая модель использовалась
  • Проблемы при обновлении модели

Стало:

# Каждый ответ содержит:
{
    "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

Стало:

{"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. Улучшенная батч-обработка

Было:

# Все тексты генер��руются заново
embeddings = model.encode(texts, convert_to_numpy=True)

Стало:

# Сначала проверяем кэш
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 при любой ошибке
  • Нет информации о причине

Стало:

# Таймаут
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 совместимы.