Идентичность товара
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 попадает в автоматическую смету (без модератора), только если выполнено хотя бы одно из:
- Embedding similarity между описанием offer и описанием canonical ≥ 0.85 (cosine).
- Совпадение классификационных тегов:
classification_tagпоставщика мапится в наш тег, ассоциированный с identity_profile канонического товара. - Совпадение packaging fingerprint: совпадает производитель + габариты упаковки + единица отгрузки (для метизов / расходников без MPN).
- Multi-supplier consensus: не менее 2 поставщиков прислали offers с одинаковыми critical attributes на этот canonical (один и тот же набор → одинаковый identity_signature → автоматически).
- Manual confirmation: модератор подтвердил аналогичный probable match на этом identity_profile последние 30 дней.
Если ни одно условие не выполнено — probable match НЕ попадает в смету автоматически, ставится в moderation_queue (тип match_review).
SLA по orphan offers
Orphan offers (без identity_profile) обрабатываются по приоритетам:
| Категория | Триггер | SLA на резолюцию |
|---|---|---|
| Hot orphan | offer цитировался в клиентском поиске последние 30 дней | 24 часа |
| Warm orphan | offer от топ-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-классификации, либо ручной.