Динамические поля и расширенный поиск

Доступно только в PRO версии! Расширенные (динамические) поля [[search.extended_fields]] индексируются и участвуют в поиске только в PRO-сборке. В Free-режиме секция [[search.extended_fields]] игнорируется; произвольные атрибуты можно индексировать через enable_attrs_indexing (см. раздел «Произвольные поля (attrs)» ниже).
Для коммерческих сценариев: sku-search умеет искать товары без указания наименования — только по дополнительным реквизитам.

Представьте: вам нужно найти всех поставщиков, у которых есть товар с определенным кодом ТН ВЭД, или найти все позиции конкретного бренда. Вы не знаете точное название — знаете только характеристики. Сервис ищет по ним, даже если name пустой.

Как это работает: два режима

В конфиге есть два ключевых параметра, которые управляют расширенным поиском:

extended_search_enabled вкл/выкл false
Что делает: Включает или выключает поиск по динамическим полям (PRO only).
  • true — сервис индексирует и ищет по дополнительным полям (бренд, поставщик, цвет, размер и т.д.)
  • false — секция [[search.extended_fields]] игнорируется
Кодовый дефолт — false. Во встроенных production-конфигах (deployment/sku-pro/config.toml) параметр установлен в true.
require_name_for_fuzzy вкл/выкл true
Что делает: Определяет, можно ли искать нечетко по названию товара без явного `name`.
  • true (режим совместимости по умолчанию) — нечеткий поиск по name требует непустого name. Поиск ТОЛЬКО по динамическим полям при name = "" работает независимо от этого флага
  • false — разрешает нечеткий поиск по name даже с пустым name (полностью коммерческий режим)
Важно: Этот флаг влияет только на нечеткий поиск по наименованию. Динамический поиск по supplier, brand, tnved и другим расширенным полям работает при любом значении, если extended_search_enabled = true.

Таблица режимов работы

extended_search_enabledrequire_name_for_fuzzyЧто получаемКогда использовать
falsetrue или falseБазовые поля (name, code, barcode, art) + attrs при enable_attrs_indexing = true. Расширенные поля [[search.extended_fields]] не используютсяПростая база, не нужны доп. реквизиты
truetrueРасширенные поля работают как доп. фильтр; нечеткий поиск по name требует непустого name. Поиск только по динамическим полям при name = "" работаетНужны доп. характеристики, но название всегда известно
truefalseПОЛНЫЙ КОММЕРЧЕСКИЙ РЕЖИМ — можно искать ТОЛЬКО по динамическим полям; нечеткий поиск по name также доступен с пустым nameПоставщики, бренды, ТН ВЭД, характеристики — название не требуется

Что такое динамические поля и как их настроить

Динамические поля — это любые дополнительные реквизиты товара. Например:

  • supplier — поставщик ("ООО Ромашка")
  • brand — бренд ("Bosch", "Samsung")
  • tnved — код ТН ВЭД (для таможни)
  • color — цвет ("красный", "синий")
  • size — размер ("42", "XL")
  • diagonal — диагональ телевизора ("55")
  • material — материал ("нержавеющая сталь")

Главная особенность: разные товары могут иметь РАЗНЫЕ наборы полей. Обувь — цвет и размер. Холодильники — диагональ и бренд. Сервис индексирует только те поля, которые есть у конкретного товара. Поиск работает по любым полям, независимо от их наличия у других товаров.

Настройка в config.toml

# Включаем расширенный поиск
[search]
extended_search_enabled = true
require_name_for_fuzzy = false   # ← коммерческий режим

# Описываем все возможные поля (один раз для всей базы)
[[search.extended_fields]]
field = "supplier"      # Имя поля в JSON
weight = 0.7            # Сохраняется в конфиге; в текущей версии не влияет на FS-ранжирование
stemmed = true          # Приводить к основе слова ("поставщики" → "поставщик")
fuzzy = true            # Разрешить опечатки ("Ромашкаа" → "Ромашка")

[[search.extended_fields]]
field = "tnved"
weight = 0.9              # Сохраняется в конфиге; в текущей версии не влияет на FS-ранжирование
stemmed = false         # Код должен совпадать ТОЧНО
fuzzy = false

[[search.extended_fields]]
field = "size"
weight = 0.4              # Сохраняется в конфиге; в текущей версии не влияет на FS-ранжирование
stemmed = false
fuzzy = true

[[search.extended_fields]]
field = "color"
weight = 0.3              # Сохраняется в конфиге; в текущей версии не влияет на FS-ранжирование
stemmed = true
fuzzy = true

Параметры поля ([[search.extended_fields]])

field строка
Имя поля — как оно будет называться в JSON-запросах и ответах.
Совет: используйте английские названия (`supplier`, `brand`), они короче и удобнее для API.
weight число (0.0-2.0) 1.0
Вес поля в конфигурации. Сохраняется и возвращается в /api/search-config, но в текущей версии не используется при вычислении итогового скора в FS-поиске. Ранжирование по динамическим полям определяется глобальными настройками [search.weights] и фиксированными бустами.
  • Для кодов (tnved, артикулы производителя): 0.8-1.0 — точное совпадение критично
  • Для названий (бренд, поставщик): 0.5-0.7 — важно, но не фатально
  • Для характеристик (цвет, размер): 0.3-0.5 — уточняющие параметры
stemmed bool true
Приводить ли слова к основе (стемминг).
  • true: "поставщики", "поставщиком", "поставщиках" → все найдет "поставщик"
  • false: только точное совпадение (для кодов, артикулов)
Для текстовых полей (бренд, поставщик) включайте. Для кодов — выключайте.
fuzzy bool true
Разрешить нечеткий поиск (опечатки) по динамическому полю в FS-движке.
  • true: "Ромашкаа" найдет "Ромашка", "Samsng" найдет "Samsung"
  • false: только точное совпадение
Для кодов (tnved, артикулы) лучше выключить — опечатки в кодах обычно не нужны.

Сценарии использования с примерами

Сценарий 1: Поиск всех товаров поставщика

Найти все товары от "ООО Ромашка", даже не зная названий:

curl -X POST http://localhost:8080/find \
  -H "Content-Type: application/json" \
  -d '{
    "items": [{
      "name": "",
      "supplier": "ООО Ромашка"
    }],
    "limit": 50
  }'
# Тот же запрос через CLI
sku-cli search "" --field supplier="ООО Ромашка" --dynamic-only --limit 50
Обратите внимание: `name` пустой, но поиск работает, потому что `require_name_for_fuzzy = false`.

Сценарий 2: Фильтрация по характеристикам

Найти все телевизоры Samsung с диагональю 55 дюймов:

curl -X POST http://localhost:8080/find \
  -H "Content-Type: application/json" \
  -d '{
    "items": [{
      "name": "",
      "brand": "Samsung",
      "diagonal": "55"
    }],
    "limit": 20
  }'
# Тот же запрос через CLI
sku-cli search "" --field brand="Samsung" --field diagonal="55" --dynamic-only --limit 20

Сценарий 3: Поиск по коду ТН ВЭД

Точный поиск по классификатору (без опечаток, точное совпадение):

curl -X POST http://localhost:8080/find \
  -H "Content-Type: application/json" \
  -d '{
    "items": [{
      "name": "",
      "tnved": "7307 19 100 0"
    }]
  }'
# Тот же запрос через CLI
sku-cli search "" --field tnved="7307 19 100 0" --dynamic-only
Для этого поля в конфиге должны быть `stemmed = false` и `fuzzy = false`.

Сценарий 4: Смешанный поиск (название + характеристики)

Ищем красные кроссовки 42 размера:

curl -X POST http://localhost:8080/find \
  -H "Content-Type: application/json" \
  -d '{
    "items": [{
      "name": "кроссовки",
      "color": "красный",
      "size": "42"
    }],
    "limit": 10
  }'
# Тот же запрос через CLI
sku-cli search "кроссовки" --field color="красный" --field size="42" --limit 10

Сценарий 5: Пакетный поиск для сверки с прайсом поставщика

Загружаете прайс от поставщика и хотите найти, какие товары уже есть в вашей базе:

curl -X POST http://localhost:8080/find \
  -H "Content-Type: application/json" \
  -d '{
    "items": [
      {"name": "", "supplier_code": "ART-001"},
      {"name": "", "supplier_code": "ART-002"},
      {"name": "", "supplier_code": "ART-003"}
    ],
    "limit": 1
  }'

Как это работает внутри (для понимания)

  1. Настройка полей в конфиге — вы описываете все возможные поля один раз. Сервис запоминает их.
  2. Загрузка товара — вы отправляете товар с любым набором полей. Сервис индексирует ТОЛЬКО те поля, которые есть у этого товара.
  3. Поиск — вы отправляете запрос с любыми полями. Сервис ищет среди товаров, у которых эти поля есть. Если поля нет у товара — он просто не попадет в результаты.
Приоритеты поиска (оптимизация для скорости):
  1. Точные совпадения — id, barcode, code, art (1-2 мс)
  2. Точные совпадения по динамическим полям — supplier, tnved и т.д. (1-2 мс)
  3. Нечеткий поиск — по названию и динамическим полям (3-10 мс)
Логика поиска по нескольким динамическим полям — AND. Если в запросе указано несколько полей (например, color и brand), сервис вернет только товары, у которых совпали ВСЕ указанные поля. ExtendedField.fuzzy определяет, разрешен ли нечеткий поиск для конкретного поля.

Пример: разные товары — разные поля

Представьте, что у вас в базе есть и обувь, и холодильники. У них совершенно разные характеристики:

# Загружаем кроссовки (есть цвет, размер, бренд)
{
  "id": "SHOE-001",
  "name": "Кроссовки спортивные",
  "color": "красный",
  "size": "42",
  "brand": "Nike"
}

# Загружаем холодильник (есть бренд, диагональ, объем)
{
  "id": "FRIDGE-001",
  "name": "Холодильник Side-by-Side",
  "brand": "Samsung",
  "diagonal": "55",
  "volume": "600"
}

Что получится при поиске:

  • Запрос {"color": "красный"} → найдет ТОЛЬКО кроссовки
  • Запрос {"diagonal": "55"} → найдет ТОЛЬКО холодильник
  • Запрос {"brand": "Samsung"} → найдет холодильник (и ничего больше)
  • Запрос {"color": "красный", "brand": "Samsung"} → ничего не найдет (нет товара, у которого есть оба поля)

Частые ошибки и как их исправить

Ошибка 1: Передаю `"name": ""`, но поиск ничего не находит.

Решение: Проверьте, что extended_search_enabled = true и поля описаны в [[search.extended_fields]]. Поиск только по динамическим полям работает независимо от require_name_for_fuzzy. Если же нужен нечеткий поиск по пустому name — установите require_name_for_fuzzy = false.

Ошибка 2: Поле есть в запросе, но сервис его игнорирует.

Решение: Проверьте, описано ли поле в [[search.extended_fields]]. Если поле не описано — оно не индексируется и не участвует в поиске.

Ошибка 3: Поиск работает, но результаты нерелевантные.

Решение: Проверьте настройки стемминга и fuzzy, а также глобальные веса в секции [search.weights] (см. config.md). Индивидуальный параметр weight в [[search.extended_fields]] в текущей версии не влияет на ранжирование.

Ошибка 4: "Ромашкаа" не находит "Ромашка".

Решение: Включите fuzzy = true для этого поля. Без него только точные совпадения.

Рекомендации для коммерческого внедрения

Пошаговый план настройки:
  1. Включите extended_search_enabled = true
  2. Опишите ВСЕ поля, которые есть в вашей базе (даже если они есть не у всех товаров)
  3. Для коммерческого режима установите require_name_for_fuzzy = false
  4. Настройте стемминг и fuzzy-логику полей в зависимости от их важности:
    • Ключевые идентификаторы (ТН ВЭД, артикул производителя) → stemmed = false, fuzzy = false
    • Бренд, поставщик → stemmed = true, fuzzy = true
    • Характеристики (цвет, размер) → stemmed = false или true (по ситуации), fuzzy = true
  5. Протестируйте поиск: отправьте запросы, которые реально делают ваши операторы
  6. При необходимости скорректируйте глобальные веса в секции [search.weights] (см. config.md)
Мониторинг качества: Используйте `GET /admin/analytics/tuning` (PRO only), чтобы получить рекомендации по настройке весов на основе реальных запросов.

Произвольные поля (attrs) — Атрибуты и реквизиты 1С

Для разработчиков и аналитиков: В 1С у номенклатуры могут быть сотни произвольных реквизитов (цвет, материал, ГОСТ, гарантия, страна и т.д.). Сервис умеет автоматически индексировать и искать по ним, даже если они не описаны в конфиге явно.

Как это работает под капотом: API → Индекс

При загрузке товара через POST /add вы передаете произвольные ключи плоско, на одном уровне с name, id, code (они попадают в ItemQuery.extra благодаря #[serde(flatten)]). Не заворачивайте их во вложенный объект extra — это вызовет ошибку 422. Сервис автоматически разделяет их на два потока индексации:

  1. attrs — для точной фильтрации. Идеально для кодов, размеров, артикулов производителей, нормативных документов.
  2. attrs_flat — для нечеткого и стеммированного поиска. Сервис склеивает значения в строку, применяет стеммер и стоп-слова. Идеально для названий брендов, материалов, характеристик.

Важно: Механизм управляется параметром enable_attrs_indexing в секции [search]. При false произвольные поля игнорируются на уровне индексации.

Настройка в config.toml

ПараметрТипПо умолчаниюОписание
enable_attrs_indexingboolfalseВключить индексацию произвольных полей. true активирует двойную запись: attrs (точное) + attrs_flat (текстовое).
search.weights.attrs_exact_weightf321.0Множитель к скору при точном совпадении по JSON-атрибутам (динамическим extra-полям).
search.weights.attrs_flat_weightf320.7Вес текстового буста через плоские атрибуты. Влияет на скор при нечетком/тематическом поиске по характеристикам.

Пример загрузки товара с атрибутами

curl -X POST http://localhost:8080/add \
  -H "Content-Type: application/json" \
  -d '{
    "items": [{
      "name": "Болт М16х60",
      "id": "1C-GUID-0001",
      "code": "BOLT-16-60",
      "color": "оцинкованный",
      "material": "сталь 35",
      "gost": "7798-70",
      "warranty_months": "24",
      "manufacturer": "Уралметиз"
    }]
  }'

Что произойдет внутри сервиса:

  • Известные поля (name, id, code) пойдут в стандартные индексы.
  • Остальные поля (color, material, gost и др.) автоматически попадут в attrs (для точного поиска) и attrs_flat (для текстового поиска с учетом опечаток и словоформ).
  • Авто-фильтрация мусора: Поля с техническими значениями (только цифры, символы - . / пробел _ </code>, GUID, внутренние коды 1С) и значения короче 2 символов не участвуют в полнотекстовом поиске: они пропускаются из attrs_flat. Однако после версии с исправлением ATTR-001 они все равно сохраняются в JSON attrs и возвращаются в ответе — это позволяет использовать их как точный фильтр (например, "size": "42" или "internal_code": "000000042").

Как искать по атрибутам

Сервис автоматически ищет по attrs_flat, если вы передаете текст в запросе. Для точной фильтрации по конкретному атрибуту передавайте ключ в теле запроса:

# Поиск товаров из "стали 35" (с учетом опечаток: "сталь", "стл", "стали 35")
curl -X POST http://localhost:8080/find \
  -H "Content-Type: application/json" \
  -d '{"items": [{"name": "", "material": "сталь"}], "limit": 10}'

# Точный поиск по ГОСТу (через attrs JSON, без стемминга и fuzzy)
curl -X POST http://localhost:8080/find \
  -H "Content-Type: application/json" \
  -d '{"items": [{"name": "", "gost": "7798-70"}], "limit": 5}'

# Комбинированный запрос: название + произвольный атрибут
curl -X POST http://localhost:8080/find \
  -H "Content-Type: application/json" \
  -d '{"items": [{"name": "болт", "color": "оцинк"}], "limit": 20}'

Совет: При включенном enable_attrs_indexing атрибуты участвуют в поиске автоматически. Вам не нужно описывать их в [[search.extended_fields]], если не требуется индивидуальная настройка веса, стемминга или fuzzy-логики.

Ограничения и рекомендации для продакшена

  • Рост индекса: Индексация attrs и attrs_flat увеличивает размер индекса на ~10-15%. Для баз >1 млн позиций с редким поиском по реквизитам рекомендуется false.
  • Приоритет полей: Если поле явно описано в [[search.extended_fields]], оно будет индексироваться строго по правилам конфига и не попадет в автоматический attrs_flat.
  • Post-фильтрация: Результаты, найденные через attrs, проходят строгую проверку на совпадение ключ-значение. Если в запросе "gost": "7798-70", товар с "gost": "7798" не пройдет фильтр, даже если скор высокий.
  • Производительность: Поиск по attrs_flat использует тот же стеммер и стоп-слова, что и name. Убедитесь, что отраслевой шум (упак, кор, шт) добавлен в stopwords.txt.

Что класть в extra, а что нет

Произвольные атрибуты удобны для коротких характеристик товара. Но не все стоит туда складывать — важно понимать, что попадает в поисковый индекс, а что только хранится.

Подходит для extra:

  • Краткие характеристики: color, size, brand, material, gost, country, warranty.
  • Идентификаторы, которые нужны в ответе или как фильтр: supplier_code, internal_code, oem, serial.
  • Короткие текстовые метки и теги: series, line, segment.
  • Значения, по которым может искать оператор: diagonal, voltage, weight.

Не подходит для extra:

  • Большие тексты документов (текст договора, ТЗ, спецификации, инструкции). Они сохранятся, но и будут токенизироваться в индексе — индекс сильно разрастется, а поиск замедлится.
  • Бинарные данные в base64, JSON-объекты, HTML-разметка.
  • Длинные табличные данные (например, прайс-лист внутри одного поля).
  • Поля, которые нужно искать по диапазонам: даты, цены (если требуется «от/до»).

Правило: если значение больше пары строк или им не ищут как по обычному слову — храните его в 1С / файловом хранилище / S3, а в SKU Search передавайте только метаданные и ключевые слова.

Все значения хранятся как строки. Числа (42), даты (2026-09-29), булевы значения (true) передаются JSON-корректно, но внутри сервиса и при сравнении они являются строками. Это значит: нет числовых операций («цена от 1000 до 2000»), нет диапазонов дат, нет математики. Если нужно искать по диапазону — используйте 1С или нормальную базу данных.
Если вам нужно вернуть большой текст в ответе, но не искать по нему, сейчас стандартными средствами SKU Search это сделать нельзя: все значения extra при включенном enable_attrs_indexing индексируются. Для таких сценариев используйте внешнее хранилище и передавайте в сервис только ссылку или краткое содержание.

Режимы фильтрации по extra

По умолчанию параметры extra работают как жесткий фильтр: товар не попадает в результаты, если хотя бы одно указанное поле не совпадает. Это поведение можно изменить без переиндексации.

ПараметрЗначениеЭффект
search.extra_filter_mode = "hard"По умолчаниюТочный фильтр: только товары со всеми совпавшими extra-полями.
search.extra_filter_mode = "soft"Требует enable_attrs_indexing = trueСовпадение по extra увеличивает скор, но не отсекает остальные товары. Силу буста задает search.weights.extra_match_boost.
search.extra_filter_mode = "off"Требует enable_attrs_indexing = trueextra игнорируется при поиске; поля по-прежнему сохраняются и возвращаются в ответе.

Пример: мягкое бустирование по бренду

Допустим, оператор ищет "кроссовки" и хочет, чтобы товары Nike шли первыми, но не хочет терять Adidas:

# config.toml
[search]
enable_attrs_indexing = true
extra_filter_mode = "soft"

[search.weights]
extra_match_boost = 0.5
curl -X POST http://localhost:8080/find \
  -H "Content-Type: application/json" \
  -d '{"items": [{"name": "кроссовки", "brand": "Nike"}], "limit": 20}'

В ответе Nike будет выше, чем Adidas, но Adidas тоже останется в списке. При extra_filter_mode = "hard" в ответе остались бы только Nike.

Режимы "soft" и "off" работают только при enable_attrs_indexing = true. Если индексация атрибутов выключена, extra не влияет на поиск в любом режиме.

Кросс-коды (аналоги и заменители) — PRO only

Доступно только в PRO версии! Требует `pro.cross_codes.enabled = true`.

Что это такое: Когда один товар можно заменить другим. Например, у вас есть оригинальная деталь "Болт М16х60 оцинкованный", а в базе есть три аналога от разных производителей. При поиске по артикулу ЛЮБОГО аналога — сервис вернет ВСЮ группу, и оператор увидит все доступные варианты.

Жизненный пример: Покупатель хочет "Резинку багажника Bosch". В базе есть оригинал Bosch и два аналога (Febi, Lemförder). Оператор вводит артикул оригинала — видит ВСЕ варианты. Вводит артикул аналога — тоже видит ВСЕ. Не нужно искать каждый аналог отдельно!

Как это устроено в данных

При загрузке товаров вы указываете для каждого:

  • group_id — идентификатор группы аналогов (одинаковый для всех взаимозаменяемых товаров)
  • is_original — является ли этот товар оригиналом (true/false)
# Загружаем группу аналогов
    curl -X POST http://localhost:8080/add \
      -H "Content-Type: application/json" \
      -d '{
        "items": [
          {
            "id": "DETAL-001",
            "name": "Болт М16х60 оцинкованный оригинал",
            "group_id": "GROUP-BOLT-001",
            "is_original": true
          },
          {
            "id": "DETAL-002",
            "name": "Болт М16х60 оцинкованный аналог (Nord)",
            "group_id": "GROUP-BOLT-001",
            "is_original": false
          },
          {
            "id": "DETAL-003",
            "name": "Болт М16х60 оцинкованный аналог (DIN)",
            "group_id": "GROUP-BOLT-001",
            "is_original": false
          }
        ]
      }'
# Тот же запрос через CLI
# Создайте файл items.json:
    [
      {
        "id": "DETAL-001",
        "name": "Болт М16х60 оцинкованный оригинал",
        "group_id": "GROUP-BOLT-001",
        "is_original": true
      },
      {
        "id": "DETAL-002",
        "name": "Болт М16х60 оцинкованный аналог (Nord)",
        "group_id": "GROUP-BOLT-001",
        "is_original": false
      },
      {
        "id": "DETAL-003",
        "name": "Болт М16х60 оцинкованный аналог (DIN)",
        "group_id": "GROUP-BOLT-001",
        "is_original": false
      }
    ]

sku-cli add items.json

Как это выглядит в ответе поиска

При поиске по любому из товаров группы вы получите оригинал + список аналогов:

{
      "search_time_ms": 10,
      "results": [{
        "query": { "name": "болт м16" },
        "matches": [{
          "original": {
            "name_out": "Болт М16х60 оцинкованный оригинал",
            "id_out": "DETAL-001",
            "group_id": "GROUP-BOLT-001",
            "is_original": true,
            "score": 0.94
          },
          "cross_codes": [
            {
              "name_out": "Болт М16х60 оцинкованный аналог (Nord)",
              "id_out": "DETAL-002",
              "score": 0.89,
              "is_original": false
            },
            {
              "name_out": "Болт М16х60 оцинкованный аналог (DIN)",
              "id_out": "DETAL-003",
              "score": 0.85,
              "is_original": false
            }
          ]
        }]
      }]
    }

Настройка в config.toml

[pro.cross_codes]
    enabled = true                      # Включить поддержку кросс-кодов
    max_group_size = 30                 # Максимум аналогов в ответе
    include_in_response = true          # Возвращать кросс-коды в /find
    sort_within_group = true            # Сортировать аналоги по релевантности
    include_original_in_cross_list = false  # Не дублировать оригинал в массиве аналогов
max_group_size число 30
Максимальное количество аналогов в ответе.
  • 10 — для быстрых ответов (магазины)
  • 30 — стандарт (рекомендуется)
  • 50-100 — для автозапчастей (много аналогов)
Важно: При изменении `pro.cross_codes.enabled` требуется ПЕРЕИНДЕКСАЦИЯ! Поля `group_id` и `is_original` должны быть проиндексированы. Если включить после загрузки данных — нужно перезагрузить все товары.

Иерархия (BOM / Where-Used) — PRO only

Доступно только в PRO версии! Требует pro.plugins.hierarchy.enabled = true. Для построения нетривиальных деревьев (связей parent_id) необходимо также включить pro.cross_codes.enabled = true; иначе /hierarchy вернет вырожденное дерево (root без детей).

Что это такое: Возможность строить деревья спецификаций (BOM — Bill of Materials) и находить, где используется деталь (Where-Used).

Два направления обхода:
  • "Вниз" (down) — берем готовое изделие и смотрим, из каких деталей оно состоит (разузлование)
  • "Вверх" (up) — берем деталь и ищем, в каких изделиях она используется (где применяется)

Как это устроено в данных

При загрузке товаров вы указываете для каждого:

  • parent_id — ID родительского товара (для которого эта деталь является составной частью)
# Загружаем иерархию: Изделие → Деталь1 → Деталь2
    curl -X POST http://localhost:8080/add \
      -H "Content-Type: application/json" \
      -d '{
        "items": [
          {
            "id": "PROD-001",
            "name": "Изделие (корневая сборка)",
            "parent_id": null
          },
          {
            "id": "PART-001",
            "name": "Деталь А",
            "parent_id": "PROD-001"
          },
          {
            "id": "PART-002",
            "name": "Деталь Б",
            "parent_id": "PROD-001"
          },
          {
            "id": "SUB-001",
            "name": "Поддеталь А.1",
            "parent_id": "PART-001"
          }
        ]
      }'
# Тот же запрос через CLI
# Создайте файл items.json:
    [
      {
        "id": "PROD-001",
        "name": "Изделие (корневая сборка)",
        "parent_id": null
      },
      {
        "id": "PART-001",
        "name": "Деталь А",
        "parent_id": "PROD-001"
      },
      {
        "id": "PART-002",
        "name": "Деталь Б",
        "parent_id": "PROD-001"
      },
      {
        "id": "SUB-001",
        "name": "Поддеталь А.1",
        "parent_id": "PART-001"
      }
    ]

sku-cli add items.json

Пример 1: Разузлование (direction = "down")

Показать, из чего состоит изделие "PROD-001":

curl -X POST http://localhost:8080/hierarchy \
      -H "Content-Type: application/json" \
      -d '{
        "root_id": "PROD-001",
        "direction": "down",
        "max_depth": 5,
        "include_cross_codes": false,
        "limit": 100
      }'

Ответ (дерево):

{
      "root": {
        "name_out": "Изделие (корневая сборка)",
        "id_out": "PROD-001"
      },
      "tree": {
        "item": { "name_out": "Изделие (корневая сборка)", "id_out": "PROD-001" },
        "level": 0,
        "children": [
          {
            "item": { "name_out": "Деталь А", "id_out": "PART-001" },
            "level": 1,
            "children": [
              {
                "item": { "name_out": "Поддеталь А.1", "id_out": "SUB-001" },
                "level": 2,
                "children": []
              }
            ]
          },
          {
            "item": { "name_out": "Деталь Б", "id_out": "PART-002" },
            "level": 1,
            "children": []
          }
        ]
      },
      "stats": {
        "total_nodes": 4,
        "max_depth_reached": 2,
        "cross_codes_found": 0,
        "elapsed_ms": 8
      }
    }

Пример 2: Where-Used (direction = "up")

Найти, где используется деталь "PART-001":

curl -X POST http://localhost:8080/hierarchy \
      -H "Content-Type: application/json" \
      -d '{
        "root_id": "PART-001",
        "direction": "up",
        "max_depth": 5
      }'

Ответ (цепочка родителей):

{
      "root": {
        "name_out": "Изделие (корневая сборка)",
        "id_out": "PROD-001"
      },
      "tree": {
        "item": { "name_out": "Изделие (корневая сборка)", "id_out": "PROD-001" },
        "level": 0,
        "children": [
          {
            "item": { "name_out": "Деталь А", "id_out": "PART-001" },
            "level": 1,
            "children": []
          }
        ]
      },
      "stats": {
        "total_nodes": 2,
        "max_depth_reached": 1,
        "elapsed_ms": 5
      }
    }

Пример 3: С включением кросс-кодов

При разузловании можно также получить аналоги для каждой детали:

curl -X POST http://localhost:8080/hierarchy \
      -H "Content-Type: application/json" \
      -d '{
        "root_id": "PROD-001",
        "direction": "down",
        "include_cross_codes": true
      }'

Ответ (с аналогами):

{
      "tree": {
        "item": { ... },
        "cross_codes": [],  // у корневого изделия может не быть аналогов
        "children": [
          {
            "item": { "name_out": "Деталь А", "id_out": "PART-001" },
            "cross_codes": [
              { "name_out": "Деталь А аналог (Nord)", "id_out": "PART-001-ALT" }
            ],
            "children": [ ... ]
          }
        ]
      }
    }

Настройка в config.toml

[pro.plugins.hierarchy]
    enabled = true                      # Включить плагин иерархии
    default_max_depth = 5               # Максимальная глубина по умолчанию
    default_limit = 1000                # Лимит узлов в ответе
    include_cross_codes_by_default = false  # Включать кросс-коды по умолчанию
default_max_depth число 5
Максимальная глубина обхода дерева. Защита от бесконечных циклов и слишком глубоких деревьев. Для большинства производственных задач хватает 5-7 уровней.
include_cross_codes_by_default вкл/выкл false
Включать ли поиск кросс-кодов (аналогов) для каждого узла дерева автоматически. Если `true`, то для каждой детали в дереве будут подтянуты ее аналоги.
Внимание: Это увеличивает время ответа и размер JSON в 2-3 раза.

Когда это нужно

СценарийЧто делаемНаправление
Производство (спецификация изделия)Показать из каких деталей состоит продуктdown
Ремонт (запчасти)Найти, в каких изделиях используется детальup
ЗакупкиПри поиске аналога детали — показать все родительские изделияdown + include_cross_codes
Конструкторская документацияЭкспорт полного дерева в JSON или CSVdown
Совет: Для больших деревьев (тысячи узлов) используйте параметр `limit`, чтобы ограничить размер ответа. Для глубоких деревьев (более 10 уровней) увеличьте `max_depth`, но помните, что каждый уровень добавляет время ответа.