Идентичность товара

NOTE

Статус: Target design. Документ описывает целевую доменную модель. Соответствующий код реализован частично (см. backend/internal/core/) или пока не начат. Правила маркировки — в 50-processes/documentation-standard.md.

Один из двух центральных концептов системы (второй — модель характеристик). Определяет, что считается “одним и тем же товаром”.

Проблема

У разных поставщиков одинаковый товар может быть представлен по-разному:

  • Разные артикулы (supplier_sku).
  • Разные наборы характеристик (часть может отсутствовать).
  • Разное написание производителя (“BOSCH” / “Bosch GmbH” / “Бош”).
  • Разные описания и фото.

При этом:

  • Некоторые характеристики меняют товар кардинально (изменение напряжения у реле — другое изделие).
  • Другие меняются от поставщика к поставщику и не влияют на идентичность (страна производства, упаковка, цвет маркировочной этикетки).

Нужна формальная модель, определяющая границу “тот же товар / другой товар”.

Ключевой принцип: идентичность через Identity Profile

В системе нет жёстких категорий. Вместо них — Identity Profile: шаблон, описывающий для группы товаров, какие характеристики критические.

Структура Identity Profile

IdentityProfile
├── id
├── label                    — человеко-читаемое имя ("Кнопочные посты")
├── matcher                  — предикат по характеристикам:
│                             к каким товарам применим профиль
├── critical_attribute_keys  — список Characteristic.key
├── priority                 — для выбора при пересечении матчеров
├── source                   — manual | ml_inferred | imported_from_supplier
└── version

Matcher профиля

Предикат над характеристиками товара, определяющий применимость профиля. Примеры:

matcher_1 = object_type == "пост кнопочный"
matcher_2 = object_type == "реле" AND voltage_kind == "AC"
matcher_3 = object_family == "кабель" AND cable_section > 0

Матчеры — формальный DSL, хранимый структурированно (JSONB-дерево), исполняемый детерминированно.

Приоритет и разрешение пересечений

Один товар может подпадать под несколько профилей (например, общий профиль “реле” и специализированный “реле времени”). Выбирается профиль с максимальным priority, но при равенстве — более специфичный (число критических характеристик больше).

Если товар не подпадает ни под один профиль → попадает в orphan pool и ждёт либо классификации AI/модератором, либо появления нового профиля.

Identity Signature

identity_signature = stable_hash(
    manufacturer_canonical_id,
    mpn_normalized,                    -- нормализованный MPN или sentinel "∅" (см. ниже)
    identity_profile_id,
    canonical_critical_attrs            -- упорядоченный набор (key, normalized_value) по profile.critical_attribute_keys
)

Нормализация значений (для попадания в сигнатуру):

  • Числа — приводятся к канонической единице (например, всё давление к паскалям).
  • Строки — trim, lowercase, NFC, пробелы схлопнуты.
  • Enum — по ID значения из справочника.
  • Range, tolerance — к канонической форме записи.

Поведение при пустом MPN:

  • Если MPN отсутствует — в hash подставляется sentinel "∅" (символ Unicode U+2205, не пустая строка) — это гарантирует, что разные товары без MPN различаются по identity_profile + critical_attrs.
  • Identity-инвариант для таких товаров полагается исключительно на critical_attribute_keys профиля. Профиль для таких категорий ОБЯЗАН включать достаточный набор characteristic’ов, чтобы исключить collision (валидируется в admin UI при создании профиля).

Инвариант: identity_signature уникален в системе. Два товара с одинаковой сигнатурой — тот же Canonical Product.

Match Confidence

Не каждое сопоставление Supplier Offer → Canonical Product одинаково надёжное. Уровни:

УровеньОпределениеАвтоматическая смета
exactсовпадение по (manufacturer, mpn) + все critical attributes совпадаютда
strongсовпадение по (manufacturer, mpn), есть расхождения в некритических атрибутахда
probableкритические атрибуты совпадают, MPN отсутствует или отличаетсятолько если нет exact/strong кандидата и подтверждено вторым сигналом (см. ниже)
weakчастичное совпадение по описанию и части характеристикнет, только с ручной верификацией
unmatchedкандидат не найден или не прошёл порогинет

Матчинг не идемпотентен по времени: сегодняшний probable может стать завтрашним exact, когда система узнала новые алиасы атрибутов. Поэтому предусматривается фоновый rematching периодически и по событиям.

Второй сигнал для probable

probable match попадает в автоматическую смету (без модератора), только если выполнено хотя бы одно из:

  1. Embedding similarity между описанием offer и описанием canonical ≥ 0.85 (cosine).
  2. Совпадение классификационных тегов: classification_tag поставщика мапится в наш тег, ассоциированный с identity_profile канонического товара.
  3. Совпадение packaging fingerprint: совпадает производитель + габариты упаковки + единица отгрузки (для метизов / расходников без MPN).
  4. Multi-supplier consensus: не менее 2 поставщиков прислали offers с одинаковыми critical attributes на этот canonical (один и тот же набор → одинаковый identity_signature → автоматически).
  5. Manual confirmation: модератор подтвердил аналогичный probable match на этом identity_profile последние 30 дней.

Если ни одно условие не выполнено — probable match НЕ попадает в смету автоматически, ставится в moderation_queue (тип match_review).

SLA по orphan offers

Orphan offers (без identity_profile) обрабатываются по приоритетам:

КатегорияТриггерSLA на резолюцию
Hot orphanoffer цитировался в клиентском поиске последние 30 дней24 часа
Warm orphanoffer от топ-5 поставщиков (по объёму)7 дней
Cold orphanпрочее30 дней

Резолюция = принят к существующему/новому identity_profile или явно помечен unclassifiable (не показывается в каталоге, но хранится для аудита).

Owner: catalog operations team. Эскалация в catalog-quality при пробое SLA на > 5% orphans в категории.

Метрики: matching_orphan_pool_size{category}, matching_orphan_resolution_time_seconds{category}.

Equivalence Class

Identity = “это тот же товар”. Equivalence = “эти товары решают ту же задачу”.

Эквивалентность — шире, чем идентичность. Формируется:

  • Вручную: модератор связывает группу canonical products.
  • По правилам: “все canonical products с identity profile X и critical attributes в заданных диапазонах”.
  • По запросу пользователя: при поиске аналогов с критериями “не хуже чем” — эквивалентность вычисляется на лету.

Lifecycle и замены

  • active — нормально предлагается.
  • deprecated — показывается, но не предлагается в первом слое рекомендаций.
  • discontinued — снят с производства. Показывается только в явных запросах, связан с преемником через replaced_by.
  • Поставщик может отметить свой offer как discontinued, не затрагивая canonical. Canonical меняет статус только когда все активные поставщики так его пометили ИЛИ модератор принял решение вручную.

Правила изменения

  • Новый Identity Profile создаётся через модерацию (см. ../50-processes/governance.md (TBD)).
  • Добавление характеристики к critical_attribute_keys существующего профиля = breaking change: все существующие canonical products этого профиля пересчитывают identity_signature, возможно разделение или слияние. Только через ADR и явное разрешение.
  • Удаление характеристики из critical_attribute_keys — обратный кейс, тоже breaking, но менее критичный (возможно слияние canonical products).

Примеры

Пример 1. Простой товар с MPN

Товар: кнопочный пост ПКУ 15-21.

Profile matcher: object_type == "пост кнопочный". Critical: series, voltage, contact_count_no, contact_count_nc, protection_class.

Два предложения:

  • ETM offer: mnf=Электротехник, mpn=ET054487, voltage=660V, …
  • Supplier B offer: mnf=Электротехник, mpn=ET054487, voltage=660В, …

Identity signature совпадает ⇒ одна Canonical Product. Confidence exact благодаря совпадению (manufacturer, mpn).

Пример 2. Товар без стабильного MPN

Товар: метиз (болт M10×50 DIN 933, оцинкованный).

У разных поставщиков артикулы разные. MPN отсутствует.

Profile matcher: object_type == "болт". Critical: thread_diameter, length, strength_class, standard, coating.

Сигнатуры совпадают по critical attributes ⇒ одна Canonical Product. Confidence probable, т.к. нет MPN. После подтверждения модератором — strong.

Пример 3. Orphan-позиция

Поставщик прислал offer без object_type и без достаточного набора характеристик, подпадающего под matcher любого профиля.

Результат: match_confidence = unmatched, попадает в orphan pool. Ожидает либо AI-классификации, либо ручной.