# Архитектура MedArchive

Документ описывает систему **как она собрана** (as-built): поток данных, карту модулей,
модель данных, ключевые инженерные решения и их обоснование.

Назначение системы: принять архив разнородных прайс-листов клиник, извлечь услуги и цены,
нормализовать их к единому справочнику и отдать поиск и сравнение «кто оказывает услугу и
по какой цене». Живая версия — `https://med.secondbrain.tools`.

---

## 1. Поток данных

```
архив (PDF/скан/DOCX/XLSX/XLS)
        │
        ▼
  извлечение по форматам ──(битый слой / 0 строк)──► Gemini Vision
        │
        ▼
   нормализация к справочнику   код → точное → эмбеддинги → нечёткое → очередь
        │
        ▼
      валидация (правила ТЗ §4.4)
        │
        ▼
        SQLite  ── версионирование цен (свежий прайс активен, старые в архив)
        │
        ▼
   FastAPI (поиск, сравнение, верификация, загрузка, экспорт)
        │
        ▼
   статический фронт (поиск и сравнение цен, админка оператора)
```

Конвейер линейный, каждый шаг — отдельный модуль. Два входа: `run()` собирает базу с нуля
по всему архиву; `ingest()` догружает новые прайсы в готовую базу (на нём держится приём
архива через интерфейс и авто-загрузка из папки).

---

## 2. Карта модулей

| Каталог | Роль |
|---|---|
| `contracts/` | Pydantic-модели (контракт между шагами): `RawRow`, `ParsedDocument`, `MatchResult`, выходные DTO API |
| `etl/dictionary.py` | Загрузка справочника услуг из xlsx, нормализация названий |
| `etl/extractors/` | `readers` — чтение форматов в «сетки»; `grid` — определение ролей колонок; `common` — эвристики прайсов; `vision` — добор через Gemini; `__init__` — диспетчер и триггер Vision |
| `etl/normalize/` | `embedding` — эмбеддинги Gemini; `matcher` — каскад сопоставления |
| `etl/validate/` | Правила валидации позиций (ТЗ §4.4) |
| `etl/pipeline.py` | Оркестрация: `run` (полная сборка), `ingest` (догрузка), версионирование, отчёт о качестве |
| `api/` | FastAPI: поиск, сравнение, верификация, загрузка, экспорт, авто-загрузка, алерты |
| `web/` | Одностраничный фронт (Tailwind): поиск и сравнение цен + админка оператора |
| `db/migrations/` | Боевая схема PostgreSQL + pgvector (наготове) |
| `infra/` | Dockerfile, docker-compose, Caddyfile — деплой за Caddy на Selectel KZ |
| `scripts/` | `run_pipeline` — сборка базы; `gen_fixtures` — синтетические прайсы; `proof/` — разведка форматов |

---

## 3. Модель данных (SQLite, MVP)

- **`service`** — справочник: `service_id`, `specialty`, `name_ru`, `name_norm`, `tarificator_code`.
- **`partner`** — клиника: `partner_id`, `name`, `name_norm` (дедуп), `city`.
- **`price_document`** — загруженный файл: `doc_id`, `partner_id`, `file_name`, `file_format`
  (`pdf`/`scan_pdf`/`docx`/`xlsx`/`xls`), `effective_date`, `parse_status`, `rows_count`.
- **`price_item`** — извлечённая позиция: `service_name_raw`, `service_code_source`, `service_id`
  (nullable — пусто, если в очереди), **`prices`** (JSON `тариф → сумма`: `resident`,
  `nonresident`, `cis`, `far`, `insurance`, `partner`), денормализованные `price_resident` и
  `price_nonresident`, `unit`, `effective_date`, `map_method`, `map_confidence`, `is_active`,
  `suggested_service_id`/`suggested_score` (кандидат оператору, даже ниже порога).
- **`learned_synonym`** (`name_norm → service_id`) — синонимы, выученные при верификации.
- **`skipped_item`**, **`settings`**, **`ingested_file`** — служебные (очередь, папка авто-загрузки,
  журнал обработанных файлов); создаются лениво на стороне API.

Боевая схема — PostgreSQL + pgvector (`db/migrations`): эмбеддинги в `vector`-колонке с
HNSW-индексом, в остальном модель та же.

---

## 4. Извлечение

Все табличные источники сводятся к «сетке» (список строк из строковых ячеек) и обрабатываются
единым кодом — так xlsx, xls, docx и таблицы из PDF проходят через одну логику.

- **XLSX/XLS/DOCX** — нативные парсеры (openpyxl, xlrd, python-docx). Для DOCX правки принимаются
  (работаем с финальным текстом).
- **PDF с текстовым слоем** — таблицы восстанавливаются **по координатам слов**: у казахстанских
  прайсов часто нет линий таблицы, поэтому `pdfplumber.extract_tables` ничего не находит. Слова
  группируются в строки по вертикали, внутри строки режутся на ячейки по горизонтальным разрывам —
  так колонки цен (резидент/нерезидент/…) не сливаются.
- **Определение ролей колонок** (`grid.py`): ценовые колонки — по заголовку и медиане значений
  (медуслуга дороже сотни тенге, что отсекает колонку «№» и мелочь); колонка названия — самая
  «кириллическая», но **колонки `Код`/`№`/`тарификатор` исключаются заранее** (иначе ключ «услуг»
  ловит заголовок «Код услуги» и колонка кодов ошибочно становится колонкой названий).
- **Gemini Vision** (`gemini-2.5-flash`) — запасной путь для сканов, битого текстового слоя и
  нестандартной вёрстки. Триггерится автоматически, когда нативный разбор пуст, помечен как
  `scan_pdf` или даёт подозрительно мало строк на страницу; большие документы пропускаются ради
  экономии (это фиксируется в логе). Если Vision полнее нативного разбора — заменяет его.

---

## 5. Нормализация

Каскад по убыванию надёжности (`matcher.py`):

1. **Код тарификатора** — точное совпадение по канонической части кода, если клиника его указала
   (бесплатно и точно).
2. **Точное совпадение** нормализованного названия со справочником.
3. **Эмбеддинги Gemini** (`gemini-embedding-001`, 768 измерений; справочник — `RETRIEVAL_DOCUMENT`,
   запрос — `RETRIEVAL_QUERY`) — семантическая близость названий. **Порог косинуса 0.70**: у Gemini
   эмбеддинги «в конусе» (несвязанные названия дают 0.6–0.7), ниже порога ловится мусор.
4. **Нечёткое сравнение** (RapidFuzz) — запасной сигнал.
5. Иначе — **очередь ручной разметки** (`unmatched`), но кандидат сохраняется оператору.

Эмбеддинги справочника кэшируются (`dict_emb.npy`): при догрузке нового прайса справочник не
пересчитывается, эмбеддится только новый файл. Подтверждение оператора пишет `learned_synonym` —
следующий прогон сопоставит синоним автоматически (система дообучается).

**Качество на тестовом архиве** (8 клиник, 10 файлов): 16 140 позиций извлечено, **73,7%
нормализовано автоматически** (код 4397, точное 291, эмбеддинги 7083, нечёткое 128), плюс 273 неуверенных
матча сняты guard'ом переобобщения в очередь.

---

## 6. Валидация и версионирование

Движок проверок (`validate/rules.py`, правила ТЗ §4.4): цена положительна и числовая; нерезидент
не дешевле резидента; название не пустое и не код; дата не в будущем. Нарушения помечают позицию
на ручную проверку.

Версионирование: если у клиники по той же услуге есть более свежий прайс, старые позиции
архивируются (`is_active = 0`), а не удаляются — история цен сохраняется и питает алерты об
изменении цен.

---

## 7. API (FastAPI)

`root_path="/api"` — за Caddy сервис живёт на `/api`, иначе Swagger ищет спеку в корне. Документация
на `/api/docs`.

| Эндпоинт | Назначение |
|---|---|
| `GET /services`, `GET /services/{id}/partners` | Справочник и **ядро WOW**: кто оказывает услугу и по какой цене (от выгодной) |
| `GET /partners`, `GET /partners/{id}/services` | Клиники и их прайсы |
| `GET /search` | Регистронезависимый поиск по услугам и клиникам (по `name_norm`) |
| `GET /unmatched`, `GET /unmatched/count`, `POST /match`, `POST /skip` | Очередь верификации, ручное сопоставление с дообучением, пропуск |
| `POST /ingest` | Приём ZIP-архива через интерфейс (§4.1) |
| `GET/POST /settings`, `POST /scan` | Папка авто-загрузки и её обработка (вызывается кроном) |
| `GET /price-changes` | Услуги, у которых цена изменилась между прайсами (детектор аномалий в интерфейсе) |
| `GET /export.xlsx` | Выгрузка каталога в Excel: лист «Каталог» (одна строка на услугу×клинику) + лист «Все позиции» |
| `GET /stats`, `GET /health` | Сводка качества (из базы) и проверка живости |

Тяжёлые зависимости (конвейер, Gemini) в `/ingest` и `/scan` импортируются лениво — read-only API
живёт и без них; эмбеддинги при загрузке — best-effort (при сбое Gemini обрабатываем без них).

---

## 8. Фронт

Один статический `web/index.html` (Tailwind CDN, vanilla JS), без сборки и Next.js:

- **Поиск → сравнение**: услуга → таблица клиник с ценами, подсветка самой выгодной и **переплаты
  к медиане**.
- **Админка оператора**: дашборд качества; загрузка ZIP; авто-загрузка из папки; алерты об
  изменении цен; экспорт в Excel; **очередь верификации** с предложенным кандидатом и дообучением.

---

## 9. Инфраструктура и деплой

Контейнер API (Docker-образ с зависимостями, код монтируется томом) за общим Caddy на VPS Selectel
(Казахстан). Caddy маршрутизирует `/api/*` → контейнер, `/` → фронт, `/pitch` `/day1` `/day2` →
презентации. Авто-загрузка: папка `/app/inbox` (том хоста) + крон каждые 10 минут дёргает `/scan`.

Read-only поиск работает на готовой SQLite-базе без обращений к Gemini; ключ Gemini нужен только
для `/ingest`/`/scan` (живая догрузка) и при сборке базы.

---

## 10. Ключевые решения и обоснование

- **SQLite для MVP** — разворачивается без Docker (на Mac его нет), быстрый старт. Боевая схема
  PostgreSQL + pgvector готова в `db/migrations`.
- **Эмбеддинги и Vision через Gemini API, не локальный torch** — контейнер на маленьком VPS не
  раздувается и не держит модель в RAM.
- **Реконструкция таблиц PDF по координатам слов** — реальные прайсы часто без линий таблицы,
  обычный парсер таблиц их не находит.
- **Порог эмбеддингов 0.70 + очередь unmatched** — у Gemini несвязанные названия лежат «в конусе»
  0.6–0.7; порог отсекает мусор, спорное уходит оператору (ТЗ §4.3), а не сопоставляется наугад.
- **Кэш эмбеддингов справочника** — догрузка нового прайса не пересчитывает тысячи услуг.
- **Гибкая модель цен `prices jsonb`** — у клиник не «резидент/нерезидент», а N тарифов (СНГ,
  дальнее зарубежье, страховые, партнёр); две главные колонки денормализованы под ТЗ и API.
- **Vision как добор, а не основной путь** — чистое берут быстрые парсеры, трудное добирает Vision;
  большие документы не отправляются в Vision ради стоимости и времени.

---

## 11. Что дальше

- Миграция на PostgreSQL + pgvector (схема готова) для продакшн-нагрузки.
- Извлечение БИН и города партнёра (модель есть, поля пока не заполняются).
- Экспорт каталога в биллинг/CRM по API (не только Excel).
- Мультиязычные синонимы справочника (KAZ/ENG) для роста автонормализации.
