Skip to content

Instantly share code, notes, and snippets.

@toolittlecakes
Created April 22, 2026 16:40
Show Gist options
  • Select an option

  • Save toolittlecakes/4cbe99dc9f713fc31e9d67e3874d9212 to your computer and use it in GitHub Desktop.

Select an option

Save toolittlecakes/4cbe99dc9f713fc31e9d67e3874d9212 to your computer and use it in GitHub Desktop.

Предложение по multi-source модели данных

Контекст

Сейчас система спроектирована как single-source, tender-centric:

  • одна основная сущность: tender
  • канонический ключ вида <source>:<source_tender_id>
  • procurement-поля вроде дедлайна, организатора, НМЦ, лотов, документов

Это хорошо работает для:

  • Контур
  • Seldon

Но новые источники уже другого типа:

  • ЕГРЗ
  • investment push
  • LMG financing

Проблема не только в том, что у них “другие колонки”.
Проблема в том, что это разные типы сущностей.

Поэтому нам не стоит пытаться класть всё в одну таблицу tenders.


Какие данные приходят из источников

1. Procurement sources

Примеры:

  • Контур
  • Seldon

Типичные данные:

  • search hit
  • tender detail
  • results / protocols
  • документы
  • поля закупки:
    • название
    • заказчик
    • организатор
    • статус
    • дедлайн
    • НМЦ
    • лоты
    • объекты

Это естественно ложится в tender.

2. Registry / project signals

Пример:

  • ЕГРЗ

Типичные данные:

  • номер заключения экспертизы
  • тип и результат экспертизы
  • объект и адрес
  • регион
  • застройщик / техзаказчик / проектировщик
  • сведения о ТПД / типовых решениях
  • сведения об эффективности
  • часто нет procurement-атрибутов:
    • нет дедлайна закупки
    • нет организатора закупки
    • нет НМЦ
    • нет тендерных документов

Это не tender.
Это скорее egrz_record или project_signal.

3. Business leads / push integrations

Пример:

  • investment projects JSON push

Типичные данные:

  • входящий JSON-лид
  • данные по проекту / объекту
  • организация
  • ответ о дубле
  • метаданные исходящего ERP payload

Это ближе к investment_project, чем к tender.

4. Financing / program signals

Пример:

  • LMG financing

Типичные данные:

  • строка сводного файла
  • программа / объект / бюджет / финансирование
  • часто вообще нет procurement identity

Это отдельный тип, например financing_item.


Принципы модели

  • Raw-данные источника должны сохраняться как есть.
  • Для реестра и аналитики нужен общий слой.
  • Source-specific поля не надо насильно унифицировать.
  • Cross-source совпадения на первом этапе лучше хранить как links, а не как жёсткий merge.
  • Документы и extraction должны быть привязаны к snapshot, но быть опциональными.

Предлагаемая минимальная схема БД

1. Raw intake layer

source_batches

Одна загрузка / один batch / один run / один file import.

Примеры:

  • один импорт CSV ЕГРЗ
  • один batch webhook-запросов
  • один scheduled run Контур
  • одна ручная загрузка файла ЛМГ

Поля:

  • id
  • source_system
  • ingest_mode (api, file, webhook, manual)
  • file_name
  • storage_key
  • metadata
  • created_at

source_records

Одна сырая запись из источника.

Поля:

  • id
  • batch_id
  • source_system
  • record_kind
  • external_id
  • source_url
  • payload_json
  • payload_hash
  • received_at

Примеры record_kind:

  • search_hit
  • detail_payload
  • results_payload
  • registry_row
  • file_row
  • webhook_payload

2. Common entity layer

entities

Главная сущность для реестра, фильтров и аналитики.

Поля:

  • id
  • canonical_key
  • entity_kind
    • tender
    • egrz_record
    • investment_project
    • financing_item
  • lead_channel
    • kontur
    • seldon
    • egrz
    • investment
    • lmg_financing
  • source_system
  • external_id
  • current_published_snapshot_id
  • current_title
  • current_source_url
  • current_org_name
  • current_org_inn
  • current_region_code
  • current_industry_code
  • current_amount_rub
  • current_event_date
  • current_deadline_at
  • source_status
  • processing_status
  • relevance_status
  • first_discovered_at
  • last_discovered_at

Смысл: это не “универсальный тендер”, а общая оболочка сущности.

entity_snapshots

Версионированное состояние сущности.

Поля:

  • id
  • entity_id
  • version
  • common_data_json
  • material_change_hash
  • publication_state
  • snapshot_at
  • created_at

3. Typed source-specific layer

tender_snapshots

Только procurement-поля:

  • notification number
  • дедлайн
  • дата итогов
  • НМЦ
  • заказчик
  • организатор
  • лоты
  • объекты
  • tender-specific status

egrz_snapshots

Только EGRZ-поля:

  • expertise_number
  • expertise_result_id
  • expertise_type
  • expertise_document_type
  • expertise_result_type
  • object_name
  • object_address
  • developer_info
  • technical_customer_info
  • planner_info
  • economy_efficiency_info
  • efficiency_order_number
  • efficiency_order_date
  • tpr
  • tpr_list
  • is_tpr
  • work_type

investment_snapshots

Только investment-specific поля:

  • inbound JSON lead fields
  • duplicate-check state
  • ERP payload metadata

financing_snapshots

Только financing-specific поля:

  • source program
  • funding attributes
  • row-specific workbook fields

4. Documents / extraction layer

documents

Документы, если они есть у конкретного snapshot.

document_artifacts

Результаты парсинга документов.

extracted_facts

Извлечённые факты.

evidences

Evidence-ссылки.

score_breakdowns

Скоринг и объяснение.

Важно: этот слой должен быть опциональным.

  • у tender он обычно есть
  • у public EGRZ его может не быть
  • у investment push сначала может не быть
  • модель при этом не ломается

5. Linking layer

entity_links

Мягкие связи между сущностями.

Поля:

  • id
  • from_entity_id
  • to_entity_id
  • link_type
    • possible_same_project
    • confirmed_same_project
    • same_counterparty
    • derived_from
  • confidence
  • evidence_json
  • created_at

Это минимальный и безопасный способ поддержать cross-source matching без тяжёлого merge.


Почему именно так

Почему не одна таблица tenders

Потому что ЕГРЗ, investment и financing не являются тендерами.
Если всё класть в tenders, мы быстро получим:

  • семантически ложные поля
  • много meaningless null
  • странные статусы
  • усложнение UI и аналитики

Почему не отдельный silo на каждый источник

Потому что нам всё равно нужен:

  • единый реестр
  • единая аналитика
  • единые фильтры
  • единая операционная поверхность

Почему этот средний вариант

Потому что он даёт:

  • сохранение raw-правды
  • одну общую registry/entity модель
  • source-specific детализацию
  • мягкий cross-source linkage
  • разумный минимализм

Сценарии обработки

Сценарий 1. Пришла запись только из procurement source

Пример: Контур нашёл новый тендер.

Поток:

  1. Сохраняем raw API ответы в source_records
  2. Создаём или обновляем entity
    • entity_kind = tender
    • lead_channel = kontur
  3. Создаём entity_snapshot
  4. Создаём tender_snapshot
  5. Загружаем документы
  6. Запускаем parsing / extraction / scoring
  7. Публикуем snapshot

Результат:

  • одна entity
  • один typed tender snapshot
  • документы и derived facts привязаны к snapshot

Сценарий 2. Пришла запись только из ЕГРЗ

Пример: нашли запись реестра, но в Контуре/Селдоне ничего нет.

Поток:

  1. Сохраняем raw row в source_records
  2. Создаём entity
    • entity_kind = egrz_record
    • lead_channel = egrz
  3. Создаём entity_snapshot
  4. Создаём egrz_snapshot
  5. Документный pipeline не запускаем, если документов нет
  6. Делаем только лёгкое enrichment:
    • регион
    • отрасль
    • нормализация организации
    • signal classification

Результат:

  • запись живёт в системе сама по себе
  • это не “тендер-заглушка”
  • позже её можно связать с tender, если он найдётся

Сценарий 3. Один и тот же проект пришёл из нескольких источников

Пример: сначала ЕГРЗ, потом тот же проект нашли в Контуре или Seldon.

Поток:

  1. egrz_record уже существует как отдельная entity
  2. Позже приходит tender как отдельная entity
  3. Matching logic находит возможную связь
  4. Создаём entity_link
  5. Обе сущности пока остаются отдельными

Результат:

  • нет premature merge
  • нет потери source-specific смысла
  • cross-source intelligence появляется через link

Это оптимальный минималистичный вариант для первого этапа.


Сценарий 4. Данные приходят в реальном времени

Пример: investment project приходит webhook’ом.

Поток:

  1. Принимаем webhook
  2. Сохраняем payload в source_records
  3. Создаём entity
    • entity_kind = investment_project
    • lead_channel = investment
  4. Создаём entity_snapshot
  5. Создаём investment_snapshot
  6. Запускаем duplicate check
  7. Сохраняем результат и метаданные downstream response

Результат:

  • real-time source ложится в ту же схему
  • не нужен отдельный special-case storage path

Сценарий 5. Ручная или batch-загрузка файла

Пример: LMG workbook или CSV ЕГРЗ.

Поток:

  1. Создаём source_batch
  2. Каждую строку сохраняем как source_record
  3. Строим entity + entity_snapshot + typed snapshot
  4. Если это full refresh, помечаем superseded / inactive старые сущности или snapshots
  5. Обновляем registry projection

Результат:

  • file imports используют ту же core-модель, что API и webhook
  • lineage batch-загрузки сохраняется

Рекомендованный путь внедрения

Этап 1

Оставить текущий Kontur pipeline почти как есть.

Этап 2

Добавить новые общие таблицы:

  • source_batches
  • source_records
  • entities
  • entity_snapshots
  • entity_links

Этап 3

Считать текущий procurement flow первым subtype:

  • либо временно оставить текущие tenders
  • либо постепенно перенести их в tender_snapshots

Этап 4

Добавить новые typed subtypes:

  • egrz_snapshots
  • investment_snapshots
  • financing_snapshots

Итоговое решение

Для нашей текущей стадии наиболее разумная схема такая:

  • raw source layer
  • common entity registry
  • typed source-specific snapshots
  • soft links between entities
  • optional document/extraction layer per snapshot

Это минимальная модель, которая:

  • поддерживает несколько разных семей источников
  • не ломает семантику данных
  • остаётся понятной для команды
  • не превращается в тяжёлую MDM-систему
  • позволяет постепенно расти от procurement-only модели к multi-source системе
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment