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>
This commit is contained in:
282
docs/architecture/03-cache-and-precompute-strategy.md
Normal file
282
docs/architecture/03-cache-and-precompute-strategy.md
Normal file
@@ -0,0 +1,282 @@
|
||||
# Стратегия кэширования и предварительной обработки
|
||||
|
||||
## Принципы
|
||||
|
||||
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% |
|
||||
Reference in New Issue
Block a user