# Стратегия кэширования и предварительной обработки ## Принципы 1. **Не делать по запросу то, что можно сделать заранее** 2. **Кэшировать по обезличенному ключу** (query hash, topic, ticker) 3. **Фоновые воркеры** обновляют кэш по расписанию 4. **Первый запрос** — холодный, заполняет кэш для последующих --- ## 1. Discover (Новости) ### Архитектура ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ cache-worker (CronJob каждые 15 мин) │ │ │ │ 1. Агрегировать новости по темам (tech, finance, travel, world, ...) │ │ 2. Для каждой новости: суммаризация через LLM (batch) │ │ 3. Сохранить в Redis: discover:{topic} = JSON │ │ TTL = 30 мин │ └─────────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────────┐ │ discover-svc (HTTP GET /discover?topic=tech) │ │ │ │ 1. Redis GET discover:tech │ │ 2. Если есть → отдать сразу │ │ 3. Если нет (cold) → синхронно выполнить агрегацию + суммаризацию, │ │ сохранить в Redis, отдать │ └─────────────────────────────────────────────────────────────────────────┘ ``` ### Redis схема | Ключ | Значение | TTL | |------|----------|-----| | `discover:tech` | `{ items: [{ title, summary, source, url, fetched_at }] }` | 30 min | | `discover:finance` | аналогично | 30 min | | `discover:travel` | аналогично | 30 min | ### UX при cold start - Skeleton карточек новостей; timeout 30 с; Retry при ошибке; предупреждение о задержке при cold ### Cron расписание ``` */15 * * * * cache-worker --task=discover ``` --- ## 2. Finance (Рынок, новости по тикерам) ### Архитектура ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ cache-worker (CronJob каждые 2 мин) │ │ │ │ 1. Market summary: индексы, futures, VIX │ │ 2. Heatmap S&P 500 │ │ 3. News для топ-20 тикеров (по популярности) → суммаризация │ │ 4. Redis: finance:summary, finance:heatmap, finance:news:AAPL, ... │ └─────────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────────┐ │ finance-svc │ │ │ │ GET /finance/summary → Redis finance:summary │ │ GET /finance/heatmap → Redis finance:heatmap │ │ GET /finance/news/AAPL → Redis finance:news:AAPL (или cold fill) │ └─────────────────────────────────────────────────────────────────────────┘ ``` ### Redis схема | Ключ | Значение | TTL | |------|----------|-----| | `finance:summary` | Market data JSON | 5 min | | `finance:heatmap` | Heatmap JSON | 5 min | | `finance:news:AAPL` | News + summary | 15 min | | `finance:news:TSLA` | News + summary | 15 min | ### Cold fill для редких тикеров Skeleton heatmap/карточки; timeout 20 с. Если запрос на `finance:news:XOM` — в кэше нет: 1. Выполнить запрос к API + суммаризацию 2. Сохранить в Redis 3. Отдать пользователю --- ## 3. Travel (Маршруты, тренды, Inspiration Cards) ### Архитектура ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ cache-worker (CronJob каждые 4–6 ч) — task=travel │ │ │ │ 1. Trending destinations → Redis travel:trending │ │ 2. Inspiration Cards (курируемые статьи) → Redis travel:inspiration │ │ Примеры: «Лучшие тропы в Словении», «Лесные отели в Швеции», │ │ «Скрытые пляжи Португалии» — LLM генерация/суммаризация batch │ │ 3. (Опционально) Pre-warm популярных маршрутов: Paris 5d, Tokyo 7d │ └─────────────────────────────────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────────────────────────────┐ │ travel-svc │ │ │ │ GET /travel/inspiration → Redis travel:inspiration (отдать сразу) │ │ GET /travel/trending → Redis travel:trending │ │ POST /travel/itinerary → hash → Redis или LLM │ └─────────────────────────────────────────────────────────────────────────┘ ``` ### Redis схема | Ключ | Значение | TTL | |------|----------|-----| | `travel:trending` | Список направлений с картинками | 6 h | | `travel:inspiration` | `[{ title, summary, image, url }]` — Inspiration Cards | 6 h | | `travel:itinerary:{hash}` | Сгенерированный маршрут | 24 h | ### UX при cold start - Skeleton trending/inspiration; itinerary: прогресс по этапам, 60 с timeout ### Реализация Inspiration Cards в cache-worker ```typescript // cache-worker/src/tasks/travel.ts export async function runTravelPrecompute(redis: Redis) { const trending = await fetchTrendingDestinations(); await redis.setex('travel:trending', 6 * 60 * 60, JSON.stringify(trending)); const inspirationTopics = [ 'best hiking trails Slovenia', 'forest hotels Sweden solitude', 'hidden beaches Portugal', 'weekend getaways Europe', // ... курированный список тем ]; const inspiration = await Promise.all( inspirationTopics.map(async (topic) => { const article = await generateOrFetchArticle(topic); // LLM или партнёрский API return { title: article.title, summary: article.summary, image: article.image, url: article.url }; }) ); await redis.setex('travel:inspiration', 6 * 60 * 60, JSON.stringify(inspiration)); } --- ## 4. Поиск (Search / Chat) ### Архитектура ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ chat-svc POST /api/v1/chat │ │ │ │ 1. query_normalized = normalize(query) // lowercase, trim, etc. │ │ 2. query_hash = sha256(query_normalized + mode + sources) │ │ 3. Redis GET search:result:{query_hash} │ │ 4. Если есть и не expired → отдать (stream из сохранённого или JSON) │ │ 5. Если нет → полный pipeline → сохранить в Redis → отдать │ └─────────────────────────────────────────────────────────────────────────┘ ``` ### Redis схема | Ключ | Значение | TTL | |------|----------|-----| | `search:result:{query_hash}` | `{ text, sources, widgets }` | 1 h (Quick) / 24 h (Pro, если low churn) | ### UX при Pro/Deep - AssistantSteps: отображение этапов (Classifier → Researcher → Writer); оценка времени ### Pre-warm популярных запросов cache-worker раз в час: 1. Анализ логов/метрик: топ-50 запросов за последние 24ч (без user_id, только query) 2. Для каждого: проверка кэша, если нет — выполнить pipeline, сохранить --- ## 5. Медиа (Images, Videos) ``` query_hash = sha256(search_query) Redis: media:images:{query_hash}, media:videos:{query_hash} TTL: 1 h ``` Одинаковые запросы на картинки/видео отдаются из кэша. ### UX - Skeleton grid; timeout 15 с; Retry при ошибке --- ## 6. Виджеты | Виджет | Ключ кэша | TTL | |--------|-----------|-----| | Погода | `widget:weather:{location}` | 15 min | | Акции | `widget:stock:{ticker}` | 5 min | | Калькулятор | **Не кэшируем** — на клиенте | --- ## 7. Реализация cache-worker ### Задачи (tasks) ```typescript // cache-worker/src/tasks/discover.ts export async function runDiscoverPrecompute(redis: Redis) { const topics = ['tech', 'finance', 'travel', 'world', 'science']; for (const topic of topics) { const items = await aggregateNews(topic); const summarized = await summarizeBatch(items); // batch LLM call await redis.setex( `discover:${topic}`, 30 * 60, // 30 min JSON.stringify({ items: summarized, updated_at: Date.now() }) ); } } // cache-worker/src/tasks/finance.ts export async function runFinancePrecompute(redis: Redis) { const summary = await fetchMarketSummary(); await redis.setex('finance:summary', 5 * 60, JSON.stringify(summary)); const heatmap = await fetchHeatmap(); await redis.setex('finance:heatmap', 5 * 60, JSON.stringify(heatmap)); const topTickers = ['AAPL', 'TSLA', 'GOOGL', 'MSFT', ...]; for (const ticker of topTickers) { const news = await fetchNewsForTicker(ticker); const summarized = await summarizeNews(news); await redis.setex( `finance:news:${ticker}`, 15 * 60, JSON.stringify(summarized) ); } } // cache-worker/src/tasks/travel.ts export async function runTravelPrecompute(redis: Redis) { const trending = await fetchTrendingDestinations(); await redis.setex('travel:trending', 6 * 60 * 60, JSON.stringify(trending)); } ``` ### CLI ```bash cache-worker --task=discover # только discover cache-worker --task=finance # только finance cache-worker --task=travel # trending + Inspiration Cards cache-worker --task=all # все задачи ``` --- ## 8. Метрики кэширования | Область | Снижение LLM вызовов | |---------|----------------------| | Discover | ~60–80% | | Finance | ~60–80% | | Search | ~30–50% |