- 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>
14 KiB
Стратегия кэширования и предварительной обработки
Принципы
- Не делать по запросу то, что можно сделать заранее
- Кэшировать по обезличенному ключу (query hash, topic, ticker)
- Фоновые воркеры обновляют кэш по расписанию
- Первый запрос — холодный, заполняет кэш для последующих
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 — в кэше нет:
- Выполнить запрос к API + суммаризацию
- Сохранить в Redis
- Отдать пользователю
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
// 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 | ~60–80% |
| Finance | ~60–80% |
| Search | ~30–50% |