Перейти к содержанию

Обзор архитектуры

TenderLens — минимально распределённое событийное приложение: три процесса разделяют один домен и одну БД, а медленная индексация отделена durable-очередью.

Контекст системы

flowchart TB
    OP["Оператор / разработчик"] --> UI["Web UI"]
    CLIENT["API-клиент"] --> API["FastAPI"]
    UI --> API
    API --> PG[("PostgreSQL + pgvector")]
    API --> OLLAMA["Ollama"]
    TED["TED"] --> CRAWLER["Crawler"]
    CF["Contracts Finder"] --> CRAWLER
    CRAWLER --> PG
    CRAWLER --> FILES[("Attachment volume")]
    CRAWLER --> NATS["NATS JetStream"]
    NATS --> INDEXER["Indexer"]
    INDEXER --> FILES
    INDEXER --> OLLAMA
    INDEXER --> PG

Почему три роли, а не три проекта

Пакет tender_lens содержит общие модели, контракты, настройки и интеграции. Один Docker image запускается разными командами. Это исключает копирование типов и упрощает тестовое задание, но оставляет процессную изоляцию там, где она действительно нужна.

Роль Вход Выход Может перезапускаться независимо
crawler внешние HTTP API, cursor rows, files, NATS event да
indexer durable NATS event chunks, embeddings, index status да
API HTTP request JSON/UI response да

Crawler не ждёт embeddings. API не извлекает документы. Indexer не обращается к внешним реестрам.

Владение данными

flowchart LR
    CRAWLER["Crawler"] -->|"создаёт/обновляет"| SOURCES["sources"]
    CRAWLER -->|"создаёт/обновляет"| TENDERS["tenders"]
    CRAWLER -->|"скачивает"| ATT["attachments"]
    INDEXER["Indexer"] -->|"заменяет атомарно"| CHUNKS["chunks"]
    INDEXER -->|"меняет index_*"| TENDERS
    API["API"] -->|"читает"| TENDERS
    API -->|"читает/vector search"| CHUNKS
    API -->|"блокирует счётчик"| KEYS["api_keys"]

Владение здесь означает «кто имеет право менять основное состояние», а не отдельную физическую БД. Ограничение поддерживается слоями сервиса и тестами.

Слои Python-пакета

entrypoints     crawler/__main__.py · indexer/__main__.py · api/main.py · cli.py
orchestration   crawler/service.py  · indexer/service.py  · search.py
adapters        crawler/ted.py      · crawler/contracts_finder.py · ai.py
domain          schemas.py          · hashing.py           · errors.py
persistence     models.py           · db.py                · migrations/
infrastructure  nats.py             · storage.py           · logging.py
presentation    api/routes.py       · web/

Зависимости направлены преимущественно сверху вниз. Доменные схемы не открывают соединения, импорт модулей не должен требовать PostgreSQL/NATS/Ollama, а connection lifecycle создаётся в entrypoint или FastAPI lifespan.

Синхронные и асинхронные границы

  • Сетевой I/O и SQLAlchemy работают через asyncio.
  • Ограниченная параллельность — asyncio.BoundedSemaphore, а не OS threads.
  • Извлечение PDF сейчас синхронное и выполняется внутри indexer; для больших документов это известная граница MVP.
  • NATS отделяет ingestion от embeddings и обеспечивает at-least-once delivery.
  • HTTP Search/Ask синхронны с точки зрения клиента и не проходят через NATS.

Инварианты

Инвариант — условие, которое должно оставаться истинным при повторе, сбое или конкуренции.

Инвариант Механизм Реализация
закупка уникальна внутри источника unique (source_id, external_id) Tender
событие относится к версии данных content_hash в row и event TenderChangedV1
старое событие не побеждает новое повторная hash-проверка под row lock IndexerService.process()
незавершённый publish восстанавливается index_status=pending republish_pending()
файлы не видны частично .part + fsync + os.replace download_attachment()
лимит атомарен между API-процессами PostgreSQL FOR UPDATE consume_rate_limit()

Технологический словарь

  • FastAPI — HTTP routing, dependency injection и OpenAPI.
  • SQLAlchemy asyncio — async ORM/session layer.
  • PostgreSQL — транзакционное состояние.
  • pgvector — тип VECTOR и cosine distance operator.
  • NATS JetStream — durable stream и explicit ACK.
  • Ollama API — локальные embeddings и generation.
  • httpx — асинхронный HTTP-клиент.
  • Pydantic — runtime-валидация контрактов.

Конкретное значение каждого термина в проекте раскрыто в глоссарии.