# Архитектура Rakhmanov Search

## Что это

SaaS-модуль интеллектуального товарного поиска: каталог индексируется один раз (или по расписанию), а каждый поисковый запрос пользователя обслуживается из **готового in-memory индекса** без SQL-сканов и без обращения к витрине.

Клиентский демо-стенд: [https://www.detmir.rakhmanov.studio](https://www.detmir.rakhmanov.studio)  
Интерактивный API (Swagger): [https://www.detmir.rakhmanov.studio/docs](https://www.detmir.rakhmanov.studio/docs)

---

## Слои системы

```
Покупатель / витрина
        │  HTTPS
        ▼
┌───────────────────────┐
│  Nginx gateway        │  TLS, маршрутизация, лимит тела фида
└──────────┬────────────┘
           │
     ┌─────┴──────┐
     ▼            ▼
┌─────────┐  ┌────────────────┐
│ Next.js │  │ FastAPI (/v1)  │  auth по X-API-Key, классификатор,
│ витрина │  │ search/suggest │  маппинг фида, постановка job
└─────────┘  └───────┬────────┘
                     │
          ┌──────────┼──────────┐
          ▼          ▼          ▼
     PostgreSQL   Meilisearch  Uploads
     tenants,     inverted     CSV/JSON
     feed_jobs    index RAM    фиды
```

| Компонент | Роль |
|-----------|------|
| **Next.js** | Клиентский UI поиска (не участвует в latency API) |
| **FastAPI** | Контракт API, auth, kids/zoo-классификатор, оркестрация индексации |
| **Meilisearch** | Поисковый движок: инвертированный индекс, фасеты, typo-tolerance |
| **PostgreSQL** | Тенанты, API-ключи, статусы задач индексации |
| **Nginx** | Единая точка входа, TLS, proxy к web/API |

Каждый клиент (tenant) получает **свой `index_uid`** и API-ключ. Данные каталогов логически изолированы.

---

## Путь одного поискового запроса

1. Витрина вызывает `GET /v1/search?q=…` с заголовком `X-API-Key`.
2. API валидирует ключ → находит tenant и его `index_uid`.
3. При необходимости срабатывает **классификатор** `kids|zoo` (словарные сигналы, O(длина запроса)).
4. Запрос нормализуется: опечатки/синонимы — на стороне Meili; бренды в латинице сохраняются; известные транслиты (`kolyaska` → `коляска`) раскрываются явно.
5. Meilisearch выполняет поиск по **предрассчитанному** индексу: токены → posting lists → ранжирование → фасеты.
6. API возвращает hits + facets + `response_time_ms` (полный round-trip API) и `processing_time_ms` (время внутри движка).

**Важно:** на горячем пути поиска **нет** полного скана CSV, **нет** JOIN’ов по 500k строк в Postgres и **нет** ML-инференса. Postgres используется только для auth/job metadata.

---

## Путь индексации (почему поиск быстрый)

Индексация **дорогая и редкая**; поиск **дешёвый и частый**.

1. `POST /v1/feeds/upload` принимает CSV/TSV/JSON (до ~512 МБ на gateway).
2. Файл пишется на диск **потоком** (chunk 1 МБ), не целиком в RAM процесса.
3. Создаётся `FeedJob` (`pending`) → background task.
4. Импортёр читает строки итератором, нормализует поля, шлёт в Meili **батчами по 2000** документов.
5. Перед загрузкой выполняется `delete_all_documents` — полная замена индекса (demo-семантика «новый фид = актуальный каталог»).
6. Повторный запуск: `POST /v1/feeds/{job_id}/reindex` без повторной загрузки файла.

После завершения индекса Meili держит структуры в памяти/на SSD. Дальше любой поиск — это lookup + merge posting lists, а не парсинг каталога.

Текущий демо-индекс: **500 000 документов**.

---

## Почему быстро (факторы latency)

| Фактор | Эффект |
|--------|--------|
| Inverted index в Meilisearch | Поиск по токенам, не full-table scan |
| Индекс уже «прогрет» в RAM | Нет холодного чтения 277 МБ CSV на каждый запрос |
| Узкий API-слой | Auth + опциональный classify + один RPC к Meili |
| Фильтры как `filterableAttributes` | Фасеты/brand/color/price без повторного полного скана |
| Typo + synonyms на движке | Не нужен отдельный NLP-сервис на каждый запрос |
| Suggest с малым `limit` | Короткий posting-list merge, целевой SLA ≤ 100 мс |

### Измерения на демо-стенде (500k SKU, прогретый индекс)

| Метрика | Search | Suggest |
|---------|--------|---------|
| Типичный ответ | ~10–20 мс | ~20–50 мс |
| P90 search (выборка 32 запросов) | ≈ **20 мс** | — |
| P95 / P99 search | ≈ **20 / 24 мс** | — |
| SLA из ТЗ | ≤ 400 / 500 / 1000 мс | ≤ 100 мс (P90) |

Запас относительно ТЗ — **более чем на порядок** на текущем железе демо.

Первый запрос после рестарта контейнера может быть «холодным» (сотни мс) — прогрев индекса/соединений. В проде это закрывается health-check warmup и keep-alive.

---

## На сколько одновременных поисков заточено

Оценки для **текущего демо-контура** (один узел API + один Meilisearch, каталог 500k, shared VPS ~15 ГБ RAM):

| Режим | Оценка | Комментарий |
|-------|--------|-------------|
| **Комфортный рабочий** | **200–500 RPS** поиска | При P50≈15 мс и утилизации CPU Meili < 50% |
| **Пиковый краткосрочный** | **800–1500 RPS** | До роста tail latency; зависит от сложности запроса и фасетов |
| **Одновременные in-flight** | **~50–150** | Исходя из latency×concurrency ≈ занятых worker/slots |
| **Suggest** | выше search RPS | Меньше документов в ответе, короче merge |

Это **инженерная оценка по архитектуре и замерам latency**, не официальный load-test отчёт для SLA-контракта. Для тендерного prod-контура проводится отдельный нагрузочный прогон с целевыми P90/P95/P99 и error_rate.

Узкие места демо сейчас:
- **один процесс uvicorn** (sync handlers) — упирается в GIL/один CPU на API-слое при очень высоком RPS;
- **один инстанс Meilisearch** — горизонталь чтения ещё не включена;
- shared host с другими проектами.

---

## Как масштабировать

### Вертикально (быстрый следующий шаг)
1. `uvicorn --workers N` (N ≈ число CPU) или gunicorn+uvicorn workers.
2. Выделить Meilisearch на отдельный хост с **≥ 4–8 ГБ RAM** под индекс 500–600k (+ запас).
3. Вынести Postgres на managed/отдельный инстанс.
4. Включить keep-alive и connection pool к Meili.

### Горизонтально (боевой контур под 3 витрины detmir.ru / .by / .kz)
1. **API**: несколько реплик за балансировщиком (stateless; jobs — через очередь).
2. **Meilisearch**: primary для записи индекса + read-replicas / отдельный search-tier на чтение.
3. **Очередь индексации**: Redis/Rabbit + worker’ы вместо BackgroundTasks FastAPI.
4. **Инкрементальные апдейты** вместо full-replace (delta feed по изменённым SKU).
5. **Шардирование по tenant** или по направлению kids/zoo при росте > 1–2M SKU.
6. **CDN/edge** только для статики витрины; search API — ближе к индексу (низкий RTT).

### Ориентиры capacity planning

| Каталог | RAM Meili (порядок) | Search RPS (1 узел, ориентир) |
|---------|---------------------|-------------------------------|
| 100k SKU | 0.3–0.7 ГБ | тысячи |
| **500k SKU** | **0.8–2 ГБ** | сотни–низкие тысячи |
| 1–2M SKU | 2–6 ГБ | сотни на узел; дальше — replicas |
| 5M+ SKU | шарды / несколько индексов | обязательно горизонталь |

Точные цифры зависят от длины title/description, числа filterable/facet полей и доли опечаточных запросов.

---

## Что уже есть / что в roadmap prod

**Есть на демо**
- 500k индекс, search/suggest/classify, фасеты, сортировки, API-key multi-tenant
- потоковый импорт, full reindex, измерение latency в каждом ответе
- без передачи ПДн покупателей в стандартной интеграции

**Roadmap для контрактного SLA 24×7**
- HA (active/standby), failover ≤ 5 мин
- формальный load-test отчёт (P90/P95/P99, error_rate)
- инкрементальные выгрузки, поведенческое ранжирование
- ML-классификатор вместо/поверх словарного
- geo-разнесение под .ru / .by / .kz при необходимости

---

## Безопасность контура

- Auth: `X-API-Key` на все `/v1/*` кроме `/health`
- Каталожные данные обезличены (SKU, атрибуты, тексты) — **не** персональные данные покупателей
- Логическая изоляция tenant → index
- TLS на edge (Nginx Proxy Manager / Let's Encrypt)

---

## Ключевые API для инженера интеграции

| Метод | Назначение |
|-------|------------|
| `POST /v1/feeds/upload` | Загрузить фид → автозапуск полной индексации |
| `GET /v1/feeds/{job_id}` | Статус: pending / processing / completed / failed |
| `POST /v1/feeds/{job_id}/reindex` | Переиндексировать уже загруженный файл |
| `GET /v1/search` | Поиск + фасеты + latency |
| `GET /v1/suggest` | Текстовый и товарный саджест |
| `GET /v1/classify` | kids / zoo / unknown |
| `GET /health` | API + Meili + Postgres |

Демо-ключ: `detmir-demo-key-2026` (кнопка **Authorize** в Swagger).
