- 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>
283 lines
14 KiB
Markdown
283 lines
14 KiB
Markdown
# Стратегия кэширования и предварительной обработки
|
||
|
||
## Принципы
|
||
|
||
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% |
|