Files
gooseek/docs/architecture/03-cache-and-precompute-strategy.md
home cd6b7857ba feat: default locale Russian, geo determines language for other countries
- localization-svc: defaultLocale ru, resolveLocale only by geo
- web-svc: DEFAULT_LOCALE ru, layout lang=ru, embeddedTranslations fallback ru
- countryToLocale: default ru when no country or unknown country

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-23 15:10:38 +03:00

14 KiB
Raw Permalink Blame History

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

Принципы

  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 каждые 46 ч) — 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

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

cache-worker --task=discover   # только discover
cache-worker --task=finance    # только finance
cache-worker --task=travel     # trending + Inspiration Cards
cache-worker --task=all        # все задачи

8. Метрики кэширования

Область Снижение LLM вызовов
Discover ~6080%
Finance ~6080%
Search ~3050%