# Стратегия кэширования и предварительной обработки

## Принципы

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% |
