docs: ARCHITECTURE.md — as-built устройство системы + ссылка из README
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -36,6 +36,8 @@ XLSX/XLS), извлекает услуги и цены, нормализует
|
||||
| Фронт | Одностраничный (Tailwind): поиск и сравнение цен, экран верификации оператора, дашборд обработки |
|
||||
| Деплой | Контейнер API за Caddy на Selectel KZ |
|
||||
|
||||
Подробно об устройстве системы — **[docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)** (поток данных, карта модулей, модель данных, обоснование решений).
|
||||
|
||||
## Запуск локально
|
||||
|
||||
```bash
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
# Архитектура 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 файлов): 15 086 позиций извлечено, **76,1%
|
||||
нормализовано автоматически** (код 4397, точное 254, эмбеддинги 6717, нечёткое 110), остальное —
|
||||
в очередь.
|
||||
|
||||
---
|
||||
|
||||
## 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) для роста автонормализации.
|
||||
Reference in New Issue
Block a user