Настроить TTL для semantic cache

ТЕХНИЧЕСКОЕ ЗАДАНИЕ: Настроить TTL для semantic cache

1. Цель задачи

Научиться проектировать и реализовывать механизм Time-To-Live (TTL) для semantic cache в production-системе. Необходимо настроить разное время жизни кэша для «горячих» (часто запрашиваемых) тем и «редких» (низкочастотных) запросов, чтобы балансировать между latency и актуальностью ответов.

Ключевой результат В semantic cache реализован динамический TTL: для запросов, попадающих в hot topics (частота > 100 запросов/час), TTL = 1 минута; для всех остальных — 1 час. Система корректно применяет TTL при записи и чтении кэша.


2. Исходные данные

Что нужноОткуда взять
Работающая RAG-система с семантическим кэшемPet-проект (например, из задачи по сборке RAG)
Векторная база данных для семантического поискаQdrant / Milvus / FAISS (свой инстанс)
База данных для хранения метаданных кэша (TTL, frequency)Redis / PostgreSQL / SQLite
Нагрузочный тест или дашборд частоты запросовPrometheus + Grafana или смоделированные логи
Библиотека для вычисления семантической близостиsentence-transformers / OpenAI embeddings

Если нет реального semantic cache — симулируем:

  1. Разверните минимальный semantic cache на базе Qdrant (или простого in-memory dict с эмбеддингами).
  2. Создайте Python-скрипт, который эмулирует разные типы запросов (100 уникальных hot topics, 1000 редких).
  3. Запишите в лог частоту каждого запроса (можно генерировать экспоненциальное распределение).
  4. Настройте таймеры TTL в коде: при записи в кэш сохраняйте timestamp создания + TTL, при чтении сверяйте текущее время.

3. Технологический стек

КомпонентИнструментыНазначение
Semantic cache storeQdrant / Weaviate / MilvusХранение векторов запросов и ответов
Метаданные (TTL, частота)Redis / PostgreSQLХранение: key: hash, value: {timestamp, ttl, frequency}
Эмбеддинг-модельsentence-transformers (all-MiniLM-L6-v2) / OpenAI APIПреобразование запроса в вектор
Определение hot topicsRedis + счётчики (INCR) / скользящее окноПодсчёт частоты запросов за последние N минут
OrchestrationPython 3.10+ (FastAPI / aiohttp)Слой между RAG и кэшем
МониторингPrometheus + Grafana / просто printСбор метрик hit rate, miss rate, expired rate

4. Этапы выполнения

Этап 1: Архитектура и проектирование механизма TTL (оценка: 30 минут)

Действия

  1. Определите, как хранить TTL.

    • Вариант A: В метаполе вектора в Qdrant (поле payload).
    • Вариант B: Отдельный Redis hash с теми же ключами.
    • Рекомендуется Вариант B (Redis) для быстрых операций INCR и TTL.
  2. Разработайте схему Redis

    Key: cache:ttl:{hash}  →  hash field: 'created_at' (unix timestamp)
    Key: cache:count:{bucket}  →  sorted set (для скользящего окна)
    Key: cache:frequency:{hash}  →  count (общее за последние 5 минут)
    
  3. Определите логику классификации hot/rare

    • Hot: frequency > THRESHOLD за последние 5 минут (например, >100).
    • Rare: иначе.
    • TTL_hot = 60 секунд, TTL_rare = 3600 секунд.
  4. Нарисуйте flow диаграмму

    Запрос → Эмбеддинг → Поиск в Qdrant (cosine > epsilon) → 
      если найден → проверка TTL в Redis → если просрочен → удалить из Qdrant, miss.
      если актуален → вернуть ответ, увеличить frequency.
      если не найден → вычислить ответ через LLM, записать в Qdrant, сохранить в Redis с TTL (определить частоту запроса).
    

Ожидаемый результат этапа Markdown- или draw.io-диаграмма, описание структуры Redis, константы TTL.


Этап 2: Реализация метаданных TTL и частоты (оценка: 1 час)

Действия

  1. Напишите класс CacheTTLManager на Python

    import redis
    import time
    from collections import defaultdict
    
    class CacheTTLManager:
        def __init__(self, redis_client: redis.Redis, hot_threshold: int = 100, window_seconds: int = 300):
            self.r = redis_client
            self.threshold = hot_threshold
            self.window = window_seconds
        
        def get_frequency(self, query_hash: str) -> int:
            """Возвращает количество запросов за последние window_seconds."""
            now = int(time.time())
            self.r.zremrangebyscore(f'cache:freq:{query_hash}', 0, now - self.window)
            return self.r.zcard(f'cache:freq:{query_hash}')
        
        def record_request(self, query_hash: str):
            """Записывает факт запроса (текущее время)."""
            now = int(time.time())
            self.r.zadd(f'cache:freq:{query_hash}', {now: now})
        
        def get_ttl(self, query_hash: str) -> int:
            freq = self.get_frequency(query_hash)
            return 60 if freq >= self.threshold else 3600
        
        def is_expired(self, query_hash: str, created_at: int) -> bool:
            ttl = self.get_ttl(query_hash)
            return time.time() - created_at > ttl
    
  2. Реализуйте логику записи в кэш

    • При cache miss: вычислить query_hash, записать эмбеддинг + ответ в Qdrant.
    • В Redis установить HSET cache:meta:{hash} created_at <now>.
    • EXPIRE cache:meta:{hash} <max_possible_ttl> (например, 3600 + запас).
  3. Реализуйте логику чтения

    • При cache hit: получить created_at из Redis, проверить is_expired().
    • Если expired: удалить из Qdrant (client.delete(collection_name, point_ids=[...])), вернуть miss.
    • Если не expired: вернуть ответ, вызвать record_request().

Ожидаемый результат этапа Класс CacheTTLManager с методами get_ttl/is_expired/record_request, интеграционная обвязка с Qdrant.


Этап 3: Интеграция с semantic cache и эмуляция запросов (оценка: 1.5 часа)

Действия

  1. Создайте эмулятор запросов (или используйте существующий дашборд).

    import random
    import time
    from sentence_transformers import SentenceTransformer
    from qdrant_client import QdrantClient
    
    model = SentenceTransformer('all-MiniLM-L6-v2')
    hot_queries = ["погода в Москве", "курс доллара", "новости сегодня", ...] * 10
    rare_queries = ["история 3D-печати", "рецепт панакоты", ...] * 100
    # Цикл эмуляции: 80% запросов — hot, 20% — rare
    
  2. Реализуйте функцию get_or_compute(query) с кэшем и TTL.

    • Получить эмбеддинг.
    • Поиск в Qdrant (threshold 0.85).
    • Если найден → проверить TTL, вернуть кэшированный ответ.
    • Если не найден или expired → имитировать вычисление (time.sleep(0.5)) → записать в кэш.
  3. Запустите эмуляцию на 10 минут, собирайте логи:

    stats = {'cache_hit': 0, 'cache_miss': 0, 'expired': 0}
    for i in range(500):
        query = random.choice(hot_queries if random.random() < 0.8 else rare_queries)
        result = get_or_compute(query)
        # логируем hit/miss/expired
    
  4. Выведите метрики

    • Hit rate для hot vs rare запросов.
    • Средняя задержка.
    • Количество просроченных записей.

Ожидаемый результат этапа Скрипт эмуляции, который выводит таблицу метрик, показывающую, что hot-запросы кэшируются лишь на 1 минуту, а редкие — на 1 час.


Этап 4: Тестирование граничных случаев и мониторинг (оценка: 30 минут)

Действия

  1. Проверьте сценарии

    • Запрос становится hot после периода редких запросов (TTL должен пересчитаться).
    • Холодный запрос не должен перезаписывать hot TTL.
    • Удаление истекших записей из Qdrant не должно ломать поиск.
    • Обработка отсутствия Redis (graceful degradation — отключать кэш, а не падать).
  2. Настройте метрики Prometheus (или просто print-лог):

    • cache_ttl_seconds{type="hot"} = 60
    • cache_ttl_seconds{type="rare"} = 3600
    • cache_hit_count{type="hot"}, cache_expired_count
  3. Напишите минимальный дашборд в Grafana (или текстовый отчёт с графиками matplotlib).

Ожидаемый результат этапа Лог всех граничных случаев, подтверждающий корректное поведение, описание ошибок и их обработки.


Этап 5: Документирование и ревью (оценка: 30 минут)

Действия

  1. Опишите архитектуру в README

    • Как TTL определяется (формула частоты).
    • Схема Redis и Qdrant.
    • Пример конфигурации (TTL thresholds, window).
  2. Зафиксируйте результаты тестов

    • График hit rate по времени.
    • Средняя задержка: без кэша, с кэшем, с TTL.
  3. Оцените влияние TTL на cache hit rate:

    • Какой процент запросов становится устаревшим до использования.
    • Комментарий: стоит ли уменьшать TTL для hot-запросов до 30 секунд.

Ожидаемый результат этапа README-файл с архитектурой и результатами.


5. Критерии приемки (Definition of Done)

  • Механизм TTL реализован: для hot topics TTL = 60 сек, для rare = 3600 сек.
  • Классификация горячих/редких запросов основана на частоте за последние 5 минут (порог >100).
  • При кэш-хите проверяется время создания: если превышен TTL, запись удаляется и возвращается miss.
  • Удаление истекших записей из векторной базы данных происходит (или запланировано TTL в Qdrant).
  • Эмуляция подтверждает, что hot-запросы обновляются чаще, чем rare.
  • Обработаны ошибки: отключение Redis → кэш отключается, система продолжает работу без него.
  • Метрики TTL, hit rate, expired count выведены в лог или дашборд.
  • README содержит описание архитектуры, пример конфигурации и результаты тестов.

6. Ожидаемый результат

Основной артефакт Python-пакет с модулем cache_ttl_manager.py, содержащим класс CacheTTLManager, и скрипт эмуляции emulate_ttl.py.

Содержание

  • Код реализации TTL.
  • Лог выполнения с метриками.
  • README с архитектурой.

Опциональные результаты


7. Возможные сложности и их решение

СложностьРешение
Частота запросов считается неверно (плавающие hot topics)Использовать скользящее окно (zrangebyscore) вместо абсолютного счётчика.
Redis не поддерживает HSET с TTL для отдельных полейИспользовать EXPIRE на весь ключ, или записывать created_at в отдельный ключ с TTL.
Удаление из Qdrant при истечении TTL нагружает BDИспользовать таймер (celery beat) для фоновой чистки раз в минуту, или TTL-индекс в Qdrant (если поддерживается).
High contention на счётчиках частотыИспользовать Redis Pipeline или INCR с истечением через EXPIRE.
Hot topic порог неправильно выбранСделать конфигурируемым через переменные окружения, выставить дефолты.

8. Бюджет времени (оценка)

ЭтапВремя
Этап 1: Архитектура и проектирование30 мин
Этап 2: Реализация Redis и классификации1 ч
Этап 3: Интеграция и эмуляция1.5 ч
Этап 4: Тестирование граничных случаев30 мин
Этап 5: Документирование30 мин
Итого4 часа

Примечание Для первого раза время может увеличиться до 6 часов из-за отладки.


9. Связанные вопросы из базы знаний

ВопросТема
112Semantic cache — архитектура и метрики
145TTL и инвалидация кэша
203Redis как store для метаданных кэша
278Определение hot topics с помощью скользящего окна
321Qdrant: операция удаления по payload
407Graceful degradation при отказе Redis
456Эмбеддинги и метрики семантической близости
512Мониторинг cache hit rate в Prometheus
623Настройка TTL в Qdrant (долгоживущие точки)
701Паттерны кэширования: Cache-Aside, Write-Through

10. Чек-лист самопроверки

  • Я реализовал CacheTTLManager с методами get_ttl и is_expired.
  • Я проверил, что hot-запросы (частота >100/5min) получают TTL 60 секунд, а остальные — 3600.
  • Я протестировал сценарий: запрос сначала редкий, затем становится горячим — TTL пересчитывается.
  • Я убедился, что при отказе Redis кэш отключается, и система не падает.
  • Я вывел метрики (hit rate, expired count) в удобном виде (таблица/график).
  • Я написал README с описанием архитектуры и инструкцией по запуску эмуляции.
  • Я проверил, что удаление истекших записей из Qdrant происходит (вручную или автоматически).