SKU Search

Поиск и сопоставление номенклатуры для 1С и других учетных систем.

SKU Search — это self-hosted сервис, который умеет:

  • искать товары по названию, коду, артикулу, штрихкоду и произвольным полям;
  • понимать опечатки, транслитерацию и неправильную раскладку клавиатуры;
  • учиться на обратной связи операторов и предлагать синонимы;
  • (PRO) искать по смыслу через векторные эмбеддинги;
  • (PRO) работать с иерархиями (BOM / Where-Used) и кросс-кодами.

Документация разбита на разделы — выбирайте нужный в оглавлении слева или используйте поиск.

С чего начать

Редакции

  • Free — полнотекстовый поиск, бесплатен для любого использования.
  • PRO — добавляет эмбеддинги, иерархию, кросс-коды, аналитику и требует лицензии.

Что это и зачем? (для 1С-специалистов)

Знакомая ситуация?

Вы работаете в 1С. Приходит заказ на "Болт М16 оцинк.". Вы начинаете создавать новую позицию, а в базе уже есть "Болт М16х60 оцинкованный ГОСТ 7798-70". Если вы не заметите — появится дубль, остатки разъедутся, бухгалтерия будет ругаться.

sku-search — это сервис, который автоматически находит похожие товары за 2-10 миллисекунд и подсказывает: "Возможно, вы имеете в виду вот это?"

Что умеет сервис

  • Ищет даже с опечатками — "блот" найдет "болт", "гака" найдет "гайка"
  • Понимает неправильную раскладку — "ljvbr d lthtdyt" найдет "Домик в деревне"
  • Понимает формы слов — "болты", "болтом", "болтов" → все найдет "болт"
  • Работает с синонимами — "оцинк" = "оцинкованный", "цинк" = "оцинкованный"
  • Учится на ваших действиях — анализирует, когда вы нажали "неверно", и предлагает синонимы
  • Очень быстрый — ответ за 2-10 мс. 1С не тормозит, оператор не ждет
  • Выдерживает большие объемы — бенчмарк проведен до 100 000 товаров

Когда это нужно (реальные сценарии)

  1. Перед созданием нового товара — оператор вводит название, сервис проверяет — нет ли уже такого в базе. Не создаем дубли!
  2. При загрузке прайса поставщика — автоматически сопоставить позиции прайса с вашей номенклатурой
  3. При очистке справочника — найти и объединить дубли (например, "Болт М6" и "Болт М6х30")
  4. При подборе аналогов — клиент хочет деталь, которой нет в наличии. Сервис показывает аналоги из той же группы.
  5. При сборке спецификаций (BOM) — показать из каких деталей состоит изделие (PRO only)
Ключевое преимущество: Сервис ничего не знает про вашу 1С. Он просто получает JSON — и возвращает JSON. Подключить можно к любой системе. Не нужно менять 1С — только добавить несколько HTTP-запросов.

Цифры, которые вас заинтересуют

ПоказательЗначение
Скорость точного поиска по ID/штрихкоду1-2 мс — быстрее, чем вы моргаете
Скорость нечеткого поиска по названию2-5 мс (p50) — даже с опечатками
Объем индекса на 100 000 товаров~37 МБ (PRO-режим)
Потребление RAMзависит от объема данных и модели; Free-режим занимает сотни МБ, PRO — см. раздел бенчмарков
Как это работает "под капотом" (очень кратко):
  1. Вы загружаете свои товары через POST /add (один раз или при каждом изменении)
  2. Сервис строит индекс — специальную структуру для быстрого поиска
  3. При поиске (POST /find) сервис за миллисекунды находит самые похожие товары
  4. Индекс сохраняется на диск — при перезапуске сервиса данные не теряются

Как работает поиск? (объясняем "на пальцах")

Главная идея: Сервис не ищет точное совпадение слов. Он ищет СМЫСЛ и ПОХОЖЕСТЬ. Поэтому "блот" находит "болт", а "цинк" находит "оцинкованный".

Пошагово: что происходит, когда вы отправляете запрос

  1. Вы отправляете запрос в 1С
    Например, оператор вводит "блот м16 цынк" (с двумя опечатками).
  2. 1С вызывает сервис
    POST /find {"items":[{"name":"блот м16 цынк"}]}
  3. Сервис исправляет опечатки (если включено)
    "блот" → "болт" (расстояние Левенштейна)
    "цынк" → "цинк"
  4. Сервис приводит слова к нормальной форме
    "болты", "болтом", "болтов" → "болт". Это называется стемминг.
  5. Применяются синонимы
    Если в конфиге есть синоним "цинк" → "оцинкованный", сервис ищет оба варианта.
  6. Поиск в индексе
    Сервис быстро находит все товары, которые содержат "болт", "м16", "оцинкованный".
  7. Ранжирование (сортировка по релевантности)
    Сервис считает для каждого товара score — число от 0 до 1, где 1 — идеальное совпадение. Товары, совпавшие с запросом по большему числу слов, получают более высокий score и поднимаются выше в списке. Это настраивается параметром search.weights.coverage_boost_per_token.
  8. Фильтрация по порогам
    Сервис находит максимальный score среди кандидатов и отбрасывает товары, у которых score ниже max_score × auto_match_threshold (по умолчанию 0.85). Также учитывается абсолютный минимум absolute_min_score. Порог работает одинаково для BM25, эмбеддинг и гибридных стратегий.
  9. Ответ сервиса
    Сервис возвращает JSON с найденными товарами, их score и способом совпадения.
  10. 1С показывает результат оператору
    "Возможно, вы имели в виду Болт М16х60 оцинкованный?"

Что такое score и как его понимать

Score — это число от 0 до 1, которое показывает, насколько найденный товар похож на ваш запрос.

Значение scoreЧто значитЧто делать в 1С
1.0Идеальное совпадение (тот же GUID или штрихкод)Автоматически использовать — это точно тот же товар
0.90 - 0.99Почти точно тот же товар (отличается только незначительно)Показать оператору: "Скорее всего, это он"
0.75 - 0.89Похоже, но не увереныПоказать список вариантов — пусть оператор выберет
< 0.75Слабое совпадениеНе показывать — создать новый товар
Совет по настройке порога:
  • Если сервис часто предлагает неверные товары → увеличьте auto_match_threshold до 0.90-0.92
  • Если сервис часто ничего не находит (хотя товар есть) → уменьшите auto_match_threshold до 0.75-0.80
  • Начните с 0.85 — это баланс для большинства баз

Три уровня поиска (от простого к сложному)

  1. Точные совпадения (мгновенно, 1-2 мс)
    Если вы ищете по id, barcode, code или art — сервис сначала проверяет их. Нашел → сразу возвращает результат. Дальше даже не ищет.
  2. N-gram поиск (для коротких запросов, 2-5 мс)
    Если вы ищете "перф", а в базе "Перфоратор". Обычный поиск не находит, потому что "перф" слишком короткий. N-gram разбивает "Перфоратор" на части: "Пер", "Перф", "Перфо", "Перфор" — и находит по "Перф". Настраивается в search.edge_ngram.
  3. Нечеткий поиск (fuzzy, 3-10 мс)
    Если точного совпадения нет, сервис ищет с учетом опечаток, разных форм слов и синонимов. Использует расстояние Дамерау-Левенштейна — алгоритм, который считает, сколько операций (замена, вставка, удаление или перестановка соседних символов) нужно, чтобы слово совпало. "блот" → "болт" (перестановка соседних букв) — найдет. "блооот" → "болт" (2 операции) — тоже найдет, если разрешено.

Как синонимы улучшают поиск

Синонимы — это слова, которые считаются одинаковыми при поиске. Например:

  • "цинк" = "оцинкованный" = "zn" = "zinc"
  • "нерж" = "нержавеющий" = "stainless" = "a2"
# Добавляем синонимы через API
        curl -X POST http://localhost:8080/synonyms \
          -H "Content-Type: application/json" \
          -d '{"items":[
            {"word":"цинк","expansions":["оцинкованный","zn","zinc"]},
            {"word":"нерж","expansions":["нержавеющий","stainless","a2"]}
          ]}'
# Тот же запрос через CLI
sku-cli synonyms add --word "цинк" --expansions "оцинкованный,zn,zinc"

После добавления синонимов запрос "болт м16 цинк" найдет "Болт М16х60 оцинкованный" с высоким score.

Где брать синонимы?
  • Сервис сам анализирует ваши действия и предлагает синонимы через GET /admin/learn (PRO only)
  • Вы можете добавлять вручную через API или CLI
  • Синонимы применяются мгновенно, перезапуск сервиса не нужен

Пример из реальной жизни

Запрос оператора: "блот м16 цынк" (с двумя опечатками)

Что делает сервис:

  1. Исправляет опечатки: "блот" → "болт", "цынк" → "цинк"
  2. Применяет синонимы: "цинк" → "оцинкованный"
  3. Ищет в индексе: товары, содержащие "болт", "м16", "оцинкованный"
  4. Находит "Болт М16х60 оцинкованный"
  5. Считает score: 0.94 (очень похоже)
  6. Возвращает результат
{
          "matches": [{
            "name_out": "Болт М16х60 оцинкованный ГОСТ 7798-70",
            "score": 0.94,
            "matched_by": "fuzzy_text"
          }]
        }

Оператор в 1С видит: "Возможно, вы имели в виду Болт М16х60 оцинкованный? Нажмите 'Да' чтобы использовать существующий товар."

Поля type и ref: фильтр по типу и прямая ссылка на объект 1С

Помимо id, сервис поддерживает два поля, которые упрощают интеграцию с 1С:

  • ref — сериализованная ссылка 1С (например, результат ЗначениеВСтрокуВнутр()). Сервис хранит ее как есть и возвращает в ответе, а поиск по ней — точный.
  • type — тип объекта (например, Справочник.Номенклатура). Позволяет ограничить поиск одним классом объектов или комбинировать с art, barcode, code.

Оба поля участвуют в exact-поиске и работают даже с пустым name.

# Поиск по ref
    curl -X POST http://localhost:8080/find \
      -H "Content-Type: application/json" \
      -d '{"items":[{"name":"","ref":"e1cib/data/Справочник.Номенклатура?ref=a1b2c3d4e5f6..."}]}'
# Тот же запрос через CLI
sku-cli search --ref "e1cib/data/Справочник.Номенклатура?ref=a1b2c3d4e5f6..."
# Поиск с фильтром по type
    curl -X POST http://localhost:8080/find \
      -H "Content-Type: application/json" \
      -d '{"items":[{"name":"болт","type":"Справочник.Номенклатура"}]}'
# Тот же запрос через CLI
sku-cli search "болт" --type "Справочник.Номенклатура"

Транслитерация и неправильная раскладка

Если в конфиге включено search.transliteration_enabled = true, сервис автоматически понимает запросы, набранные не в той раскладке или латиницей.

# «Домик в деревне», набрано на русской раскладке как латиница
    curl -X POST http://localhost:8080/find \
      -H "Content-Type: application/json" \
      -d '{"items":[{"name":"ljvbr d lthtdyt"}]}'

# Та же фраза латиницей
    curl -X POST http://localhost:8080/find \
      -H "Content-Type: application/json" \
      -d '{"items":[{"name":"domik v derevne"}]}'
# Тот же запрос через CLI
sku-cli search "ljvbr d lthtdyt"
sku-cli search "domik v derevne"

Сервис размножает каждый токен запроса на возможные варианты (исходный, исправленная раскладка, транслитерация) и ищет по всем сразу. Работает для кириллицы и латиницы.

Настройка. Точность распознавания контролируется параметром search.transliteration_threshold (по умолчанию 0.35). Уменьшите порог, если сервис слишком редко срабатывает на транслит; увеличьте, если появляются ложные срабатывания.
PRO: Семантический поиск (эмбеддинги)

Если включены эмбеддинги (PRO only), сервис может искать товары по смыслу, а не только по словам.

Например: "шуруповерт" найдет "дрель-шуруповерт" и "винтоверт", даже если этих слов нет в запросе. Векторы "понимают", что эти слова означают похожие инструменты.

Как настроить эмбеддинги →

Быстрый старт за 5 минут

Хотите попробовать прямо сейчас? Вот пошаговая инструкция:

1

Скачайте и запустите сервис

# Скачайте архив для вашей ОС (ссылка в релизах) и распакуйте его
# Выполните скрипт первого запуска:
./setup.sh        # Linux / macOS
# setup.cmd       # Windows

# Или вручную скопируйте пример конфигурации:
# cp config.toml.default config.toml

# PRO-версия: CPU-рантайм ONNX Runtime уже включен в архив.
# Для GPU скачайте и положите рантайм в папку runtime/ (см. ONNX Runtime в PRO).

# Запустите сервис.
# После setup.sh в корне появится ссылка/батник sku-service, которую удобно запускать:
./sku-service
# Можно также запускать исполняемый файл напрямую из папки bin/ — сервис найдет config.toml:
# ./bin/linux/sku-service
# ./bin/windows/sku-service.exe
2

Добавьте первый товар

curl -X POST http://localhost:8080/add \
  -H "Content-Type: application/json" \
  -d '{"items":[{"name":"Болт М16х60 оцинкованный","id":"TEST-001"}]}'

# Или тот же запрос через CLI:
sku-cli add item.json
# где item.json содержит:
# [
#   { "id": "TEST-001", "name": "Болт М16х60 оцинкованный" }
# ]

# Или загрузите демо-каталог из архива (размер батча не более 500):
./bin/linux/sku-cli import --csv data/demo-data.csv --batch-size 500
3

Найдите его

curl -X POST http://localhost:8080/find \
  -H "Content-Type: application/json" \
  -d '{"items":[{"name":"болт м16"}]}'

# Или через CLI:
sku-cli search "болт м16"
4

Посмотрите результат

{
  "request_id": "550e8400-e29b-41d4-a716-446655440000",
  "search_time_ms": 5,
  "results": [
    {
      "query": { "name": "болт м16" },
      "matches": [
        {
          "original": {
            "name_out": "Болт М16х60 оцинкованный",
            "id_out": "TEST-001",
            "score": 1.0,
            "matched_by": "fuzzy_text"
          },
          "cross_codes": []
        }
      ]
    }
  ]
}
Готово! Сервис работает. Теперь можно подключать его к 1С через HTTP-запросы.

Первоначальная загрузка данных

Важно понимать: Сервис НЕ хранит ваши данные в своей базе. При первом запуске индекс ПУСТ. Вы должны сами загрузить в него свои товары.

Как загрузить данные в сервис

  1. Подготовьте данные — каждый товар должен содержать уникальный идентификатор id (ваш внутренний GUID из 1С) и название name.
  2. Загрузите данные через API — отправьте POST /add с массивом товаров. Размер пакета регулируется параметром server.max_batch_size и лимитом HTTP-тела server.max_request_body_size_mb.
  3. Проверьте результат — GET /health покажет количество загруженных записей.
Про поле id — важно:
  • id — ваш внутренний уникальный идентификатор товара (обычно GUID из 1С). Он обязателен при загрузке через /add.
  • Если вы повторно загрузите запись с тем же id, но другим набором полей, старая запись будет полностью перезаписана новыми данными. Сервис сначала удаляет старую версию, затем добавляет новую.
  • Пустой или отсутствующий id вызовет ошибку: товар не будет добавлен.

Способы загрузки

Способ 1: Загрузка из CSV файла (через CLI)

# Формат CSV: id,name,code,barcode,art
    # Пример: 1C-GUID-001,Болт М16х60 оцинкованный,BOLT-001,4607000010015,78454

    sku-cli import --csv items.csv

Если ваш CSV использует разделитель ; вместо ,, задайте его в config.toml:

[csv]
default_delimiter = ";"

sku-cli import при импорте учтет этот параметр.

Способ 2: Загрузка из JSON (через curl)

curl -X POST http://localhost:8080/add \
    -H "Content-Type: application/json" \
    -d '{
        "items": [
        {
            "id": "1C-GUID-001",
            "name": "Болт М16х60 оцинкованный",
            "code": "BOLT-001",
            "barcode": "4607000010015",
            "art": "78454"
        },
        {
            "id": "1C-GUID-002", 
            "name": "Гайка М16 нержавеющая",
            "code": "NUT-001"
        }
        ]
    }'
# Тот же запрос через CLI
# Создайте файл items.json:
    [
      {
        "id": "1C-GUID-001",
        "name": "Болт М16х60 оцинкованный",
        "code": "BOLT-001",
        "barcode": "4607000010015",
        "art": "78454"
      },
      {
        "id": "1C-GUID-002",
        "name": "Гайка М16 нержавеющая",
        "code": "NUT-001"
      }
    ]

sku-cli add items.json

Способ 3: Прямо из 1С (через HTTP-запрос)

// Псевдокод 1С
    Запрос = Новый HTTPЗапрос();
    Запрос.Заголовки.Вставить("Content-Type", "application/json");

    Данные = Новый Структура();
    Данные.Вставить("items", СписокТоваров); // Массив структур с полями id, name, code...

    Запрос.УстановитьТелоИзСтроки(JSON(Данные));

    Соединение = Новый HTTPСоединение("localhost", 8080);
    Ответ = Соединение.ОтправитьДляОбработки(Запрос, "/add");

Пример для тестовых данных

Если у вас нет своих данных, вот минимальный пример для тестирования:

curl -X POST http://localhost:8080/add \
    -H "Content-Type: application/json" \
    -d '{
        "items": [
        {"id": "TEST-001", "name": "Болт М6х30 оцинкованный"},
        {"id": "TEST-002", "name": "Болт М8х40 оцинкованный"},
        {"id": "TEST-003", "name": "Болт М10х50 нержавеющий"},
        {"id": "TEST-004", "name": "Болт М12х60 нержавеющий"},
        {"id": "TEST-005", "name": "Болт М16х80 оцинкованный"},
        {"id": "TOOL-001", "name": "Перфоратор Bosch GBH 2-26"},
        {"id": "FOOD-001", "name": "Молоко пастеризованное 3.2% 1л"}
        ]
    }'
# Тот же запрос через CLI
# Создайте файл items.json:
    [
      {
        "id": "TEST-001",
        "name": "Болт М6х30 оцинкованный"
      },
      {
        "id": "TEST-002",
        "name": "Болт М8х40 оцинкованный"
      },
      {
        "id": "TEST-003",
        "name": "Болт М10х50 нержавеющий"
      },
      {
        "id": "TEST-004",
        "name": "Болт М12х60 нержавеющий"
      },
      {
        "id": "TEST-005",
        "name": "Болт М16х80 оцинкованный"
      },
      {
        "id": "TOOL-001",
        "name": "Перфоратор Bosch GBH 2-26"
      },
      {
        "id": "FOOD-001",
        "name": "Молоко пастеризованное 3.2% 1л"
      }
    ]

sku-cli add items.json

Как проверить, что данные загружены

# Проверить количество записей
    curl http://localhost:8080/health | jq .db_record_count
    # Ожидаем: 7 (или сколько вы загрузили)

    # Попробовать найти что-нибудь
    curl -X POST http://localhost:8080/find \
    -H "Content-Type: application/json" \
    -d '{"items":[{"name":"болт"}]}' | jq .
# Тот же запрос через CLI
sku-cli health
Частые ошибки при загрузке:
  • Пустой id — сервис вернет ошибку. Каждый товар должен иметь уникальный идентификатор.
  • 413 Request Entity Too Large — тело запроса превысило server.max_request_body_size_mb (по умолчанию 20 МБ). Уменьшите batch или увеличьте лимит в config.toml и nginx.conf.
  • 400 Bad Request — batch превысил server.max_batch_size (по умолчанию 500). Разбейте на части или увеличьте max_batch_size.
  • Rate limiting — при большой загрузке установите в конфиге rate_limit_per_minute = 0.
  • Данные не сохранились после перезапуска — проверьте путь к индексу в конфиге. Индекс сохраняется на диск.

Производительность массовой загрузки

Размер batch и пропускная способность зависят от:

  • server.max_batch_size — сколько товаров принимается в одном POST /add;
  • server.max_request_body_size_mb — максимальный размер HTTP-тела, который готов принять сервис (и reverse proxy, например nginx);
  • длины полей товаров (name, supplier, описания и т.п.);
  • включен ли семантический поиск (PRO) и успевают ли фоновые воркеры строить векторы.
Конфигурация по умолчанию: `max_batch_size = 500`, `max_request_body_size_mb = 20`. Это безопасный выбор для большинства production-установок.

Результаты внутреннего бенчмарка (PRO-версия, семантический поиск отключен, 100 000 товаров):

batchitems/s100k заПримечание
500866~115 сБазовый режим
1 0001 703~58 с
2 0003 380~29 с
5 0008 016~12.5 с
10 00014 121~7.1 с
30 00037 602~2.7 сПри лимите тела 20 МБ — это максимальный работающий batch
50 00049 354~2.0 сТребует поднять max_request_body_size_mb и client_max_body_size до ~64 МБ
100 00052 476~1.9 сОдин запрос на всю пачку; предел CPU/RAM и дисковой подсистемы сервера
Рекомендации для production:
  1. Начните с max_batch_size = 500 и max_request_body_size_mb = 20 (по умолчанию).
  2. Если 1С формирует пакеты крупнее 500 и CPU/SSD сервера позволяют — поднимайте max_batch_size до 1 000–5 000.
  3. Batch больше 5 000 дает выигрыш только при отключенных эмбеддингах и большом лимите тела. При включенных эмбеддингах (PRO) скорость будет ограничена фоновой индексацией векторов, а не размером HTTP-пакета.
  4. При увеличении max_request_body_size_mb не забудьте поднять client_max_body_size в nginx (или другом reverse proxy).
  5. На массовой загрузке отключайте rate limiting: rate_limit_per_minute = 0, а после загрузки возвращайте обратно.
  6. Абсолютный потолок без семантического поиска — ~50–52k items/s. Дальше рост batch не ускоряет загрузку.
Совет для 1С:

При первоначальной загрузке большого справочника (100 000+ записей):

  1. Временно отключите rate limiting: rate_limit_per_minute = 0
  2. Разбейте данные на пакеты по 300–1 000 записей (можно больше, если позволяют настройки и железо).
  3. Добавьте небольшую паузу между пакетами (0.1–0.3 сек), чтобы не перегружать CPU.
  4. После загрузки верните настройки rate limiting обратно.

API

Что нужно знать 1С-разработчику:
  • id — ваш уникальный идентификатор товара (обычно GUID из 1С). Без него сервис не сможет добавить или обновить товар. Повторная загрузка с тем же id полностью перезаписывает запись
  • name — наименование товара. Обязательно только для нечеткого поиска
  • Сервис ищет по полям: name, code, barcode, art, а также динамическим полям в PRO-режиме (см. Расширенный поиск)
  • Ответ приходит за 2-10 мс — ваша 1С не зависнет
  • Если сервис недоступен — ставьте таймаут 1-2 секунды и создавайте новый товар без проверки
В примерах ниже используется порт `8080` как условный локальный порт. В production-контурах по умолчанию Free работает на `8081`, PRO — на `8082` (см. `deployment/docker-compose.yml`).
Аутентификация и rate limit. Если в конфиге задан security.api_key, все запросы должны содержать заголовок X-API-Key: <ключ>. Без него сервис вернет 401 Unauthorized. При превышении лимита запросов сервис вернет 429 Too Many Requests.
Режим "только для сайта" (site.enabled = true). Все мутации (/add, /delete, /admin/*, /feedback, /synonyms POST/DELETE и др.) возвращают 403 Forbidden. Доступны только чтение: /find, /export, /health, /api/search-config, /api/config и статика.

Поля в запросах и ответах

ПолеГде используетсяЧто значитТипОбязательность
nameв запросе /find, /addНаименование товараstringдля нечеткого поиска, для точного (можно пустым)
idв запросе /add, /delete, /findВаш GUID из 1Сstringобязательно для /add
id_outв ответе /findGUID найденного товараstring—
name_outв ответе /findНаименование найденного товараstring—
scoreв ответе /findУверенность (0-1). 1.0 = идеальное совпадениеfloat—
matched_byв ответе /findСпособ поиска (см. таблицу ниже)string—
search_time_msв ответе /findОбщее время выполнения поиска в миллисекундахinteger—
codeв запросе/ответеКод товара / производителяstringопционально
barcodeв запросе/ответеШтрихкод (строка или массив строк)string или arrayопционально
artв запросе/ответеДополнительный артикулstringопционально
fieldsв запросе /findКакие дополнительные поля вернуть в ответеobjectопционально
(динамические поля)в запросе/ответеЛюбые дополнительные реквизиты передаются плоско, на одном уровне с name, id, code (например: material, color, supplier, brand). Все значения хранятся и сравниваются как строки — числа и даты тоже становятся строками.stringопционально (PRO only)
strategyв запросе /findСтратегия поиска: "fs_only", "embedding_only", "hybrid", "parallel"stringопционально; принимается в обеих редакциях, эффект только в PRO
parent_idв запросе /addID родительского элемента (для иерархии)stringопционально (PRO only)
group_idв запросе /addID группы аналогов (кросс-коды)stringопционально (PRO only)
is_originalв запросе /addЯвляется ли товар оригиналом в группеbooleanопционально (PRO only)
limitв запросе /findМаксимальное количество результатов (не более 1000)integerопционально (по умолчанию из конфига, максимум 1000)
type/find, /add, ответТип номенклатуры (1С)stringопционально
desc/find, /add, ответОписание/реквизиты (1С)stringопционально
ref/find, /add, ответСсылка/UUID 1С (точный поиск)stringопционально
fs_only/add (body)Пропустить эмбеддинги (экономия CPU)boolпо умолчанию false
successв ответе /add, /deleteУспешно ли выполнена операцияboolean—
error / reasonв ответе /add, /deleteПричина ошибкиstring—

Значения поля matched_by

ЗначениеЧто значит
exact_idТочное совпадение по id
exact_id_from_nameUUID из поля name распознан как id
exact_id_fallbackТочное совпадение по id после fallback-поиска
exact_barcode / exact_barcode_from_nameТочное совпадение по штрихкоду (в том числе из name)
exact_code / exact_code_from_nameТочное совпадение по коду товара (в том числе из name)
exact_art / exact_art_from_nameТочное совпадение по артикулу (в том числе из name)
exact_typeТочное совпадение по типу 1С
exact_refТочное совпадение по ссылке/UUID 1С
exact_name / exact_name_rawТочное совпадение по наименованию (стеммированное / исходное)
exact_fieldsСовпадение по комбинации полей запроса (code, art, type, ref, barcode)
exact_dynamicТочное совпадение по динамическим полям (PRO only)
exact_identifierТочное совпадение по идентификатору 1С или внешнему коду
exact_parent_idНайден по parent_id (иерархия)
exact_1c_full_matchПолное совпадение по 1С-идентификаторам
ngramНайдено через Edge N-gram (по части слова)
phrase / phrase_fallbackНайдено по точной фразе
fuzzy_textНайдено через нечеткий поиск (с опечатками)
fuzzy_booleanНайдено через булевый нечеткий поиск
dynamic_onlyНайдено только по динамическим полям (без name, PRO only)
embeddingНайдено через семантический поиск (PRO only)
fs_onlyНайдено через полнотекстовый поиск
fs+embeddingГибридный поиск (PRO only)
hierarchy_childНайден как потомок в иерархии (PRO only)
hierarchy_defaultЗначение по умолчанию при иерархическом поиске (PRO only)

POST /find — поиск

POST/find

Поиск по названию (основной сценарий):

curl -X POST http://localhost:8080/find \
  -H "Content-Type: application/json" \
  -d '{
    "items": [
      {
        "name": "Болт М16 цинк"
      }
    ],
    "limit": 3
  }'
# Тот же запрос через CLI
sku-cli search "Болт М16 цинк" --limit 3

Точный поиск по ID (name можно передать пустым):

curl -X POST http://localhost:8080/find \
  -H "Content-Type: application/json" \
  -d '{
    "items": [
      {
        "name": "",
        "id": "1C-GUID-0001"
      }
    ]
  }'
# Тот же запрос через CLI
sku-cli search --id "1C-GUID-0001"

Поиск по артикулу (name можно передать пустым):

curl -X POST http://localhost:8080/find \
  -H "Content-Type: application/json" \
  -d '{
    "items": [
      {
        "name": "",
        "code": "BOLT-M16-60-ZN"
      }
    ]
  }'
# Тот же запрос через CLI
sku-cli search --code "BOLT-M16-60-ZN"
Про регистр: Если поле добавлено в `case_sensitive_fields`, то прямой поиск по нему (например, `code="BOLT-M16-60-ZN"`) учитывает регистр. При этом поиск через общее поле `name="bolt-m16-60-zn"` остается регистронезависимым — система найдет товар в любом регистре.

Поиск по динамическим полям (PRO only):

curl -X POST http://localhost:8080/find \
  -H "Content-Type: application/json" \
  -d '{
    "items": [
      {
        "name": "",
        "supplier": "ООО Ромашка",
        "brand": "Bosch"
      }
    ],
    "limit": 20
  }'
# Тот же запрос через CLI
sku-cli search "" --field supplier="ООО Ромашка" --field brand="Bosch" --dynamic-only --limit 20
PRO only. Для работы этого режима необходим PRO-режим и в конфиге должны быть включены extended_search_enabled = true и require_name_for_fuzzy = false.

Смешанный поиск (название + артикул + динамические поля):

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

С указанием стратегии поиска:

curl -X POST http://localhost:8080/find \
  -H "Content-Type: application/json" \
  -d '{
    "items": [
      {
        "name": "шуруповерт аккумуляторный"
      }
    ],
    "strategy": "hybrid",
    "limit": 5
  }'
# Тот же запрос через CLI
sku-cli search "шуруповерт аккумуляторный" --limit 5 --strategy hybrid

Поиск по type (фильтр по типу объекта 1С):

curl -X POST http://localhost:8080/find \
  -H "Content-Type: application/json" \
  -d '{
    "items": [
      {
        "name": "болт",
        "type": "Справочник.Номенклатура"
      }
    ]
  }'
# Тот же запрос через CLI
sku-cli search "болт" --type "Справочник.Номенклатура"

Поиск по ref (прямая ссылка на объект 1С):

curl -X POST http://localhost:8080/find \
  -H "Content-Type: application/json" \
  -d '{
    "items": [
      {
        "name": "",
        "ref": "e1cib/data/Справочник.Номенклатура?ref=a1b2c3d4e5f6..."
      }
    ]
  }'
# Тот же запрос через CLI
sku-cli search --ref "e1cib/data/Справочник.Номенклатура?ref=a1b2c3d4e5f6..."

Транслитерация и неправильная раскладка:

# Русская фраза, набранная латиницей на русской раскладке
    curl -X POST http://localhost:8080/find \
      -H "Content-Type: application/json" \
      -d '{"items":[{"name":"ljvbr d lthtdyt"}]}'

# Латинская транслитерация
    curl -X POST http://localhost:8080/find \
      -H "Content-Type: application/json" \
      -d '{"items":[{"name":"domik v derevne"}]}'
# Тот же запрос через CLI
sku-cli search "ljvbr d lthtdyt"
sku-cli search "domik v derevne"
Транслитерация работает, если в config.toml включен параметр search.transliteration_enabled = true.

Ответ (когда товар найден):

{
  "request_id": "550e8400-e29b-41d4-a716-446655440000",
  "search_time_ms": 12,
  "results": [
    {
      "query": {
        "name": "Болт М16 цинк",
        "id": null,
        "code": null,
        "art": null,
        "barcode": [],
        "fields": {},
        "parent_id": null,
        "group_id": null,
        "is_original": false,
        "type": null,
        "desc": null,
        "ref": null
      },
      "matches": [
        {
          "original": {
            "name_out": "Болт М16х60 оцинкованный ГОСТ 7798-70",
            "id_out": "1C-GUID-0001",
            "code": "BOLT-M16-60-ZN",
            "barcode": ["4607000010015"],
            "art": "78454",
            "score": 0.94,
            "matched_by": "fuzzy_text",
            "is_original": false,
            "matched_tokens": ["болт", "м16", "оцинкованный"]
          }
        }
      ]
    }
  ]
}
Поле cross_codes возвращается только при непустом списке кросс-кодов; если аналогов нет, ключ в JSON отсутствует.

Ответ с кросс-кодами (PRO only):

Параметр include_cross_codes по умолчанию true. Кросс-коды возвращаются только если в конфигурации включены pro.enabled и pro.cross_codes.enabled.

{
  "request_id": "550e8400-e29b-41d4-a716-446655440000",
  "search_time_ms": 8,
  "results": [
    {
      "query": { ... },
      "matches": [
        {
          "original": {
            "name_out": "Болт М16х60 оцинкованный",
            "id_out": "DETAL-BOSCH-001",
            "group_id": "GROUP-BOSCH-001",
            "is_original": true,
            "score": 0.94
          },
          "cross_codes": [
            {
              "name_out": "Болт М16х60 оцинкованный аналог",
              "id_out": "DETAL-BOSCH-002",
              "score": 0.89,
              "is_original": false
            }
          ]
        }
      ]
    }
  ]
}

Ответ (когда ничего не найдено):

{
  "request_id": "550e8400-e29b-41d4-a716-446655440000",
  "search_time_ms": 3,
  "results": [
    {
      "query": { "name": "несуществующий товар", ... },
      "matches": []
    }
  ]
}
Как возвращаются динамические поля: Значения произвольных полей приходят плоско внутри объекта `original`, наравне с `name_out`, `id_out` и т.д. (не во вложенном `extra`).
  • Обычные extra-поля (не описанные в <code>[[search.extended_fields]]</code>) сохраняются в f_attrs и возвращаются автоматически, если они проиндексированы.
  • Расширенные поля (<code>[[search.extended_fields]]</code>, PRO) сохраняются в отдельные поля схемы поискового индекса. Они возвращаются в ответе, только когда поиск выполняется именно по этому полю (обычно с пустым name). Если товар найден по названию или эмбеддингам, расширенное поле может отсутствовать.
  • «Технические» 1С-значения (GUID, строки только из цифр/дефисов/точек/слешей) не попадают в f_attrs и не возвращаются как extra-атрибуты.
Важно: Для нечеткого поиска (fuzzy) поле `name` обязательно и не должно быть пустым, если `require_name_for_fuzzy = true`. Для точного поиска по `id`, `barcode`, `code` или `art` поле `name` можно передать пустой строкой.

POST /add — добавить или обновить

POST/add

Запрос:

curl -X POST http://localhost:8080/add \
  -H "Content-Type: application/json" \
  -d '{
    "items": [{
      "name": "Гайка М16 нержавеющая",
      "id": "1C-GUID-0002",
      "code": "NUT-M16-A2",
      "barcode": ["4607000010022"],
      "art": "GN-16",
      "supplier": "ООО Ромашка",
      "brand": "Nord"
    }]
  }'
# Тот же запрос через CLI
# Создайте файл item.json:
    [
      {
        "name": "Гайка М16 нержавеющая",
        "id": "1C-GUID-0002",
        "code": "NUT-M16-A2",
        "barcode": [
          "4607000010022"
        ],
        "art": "GN-16",
        "supplier": "ООО Ромашка",
        "brand": "Nord"
      }
    ]

sku-cli add item.json
Важно: Поле `id` обязательно для индексации. Без него сервис не сможет обновить товар. Динамические поля (`supplier`, `brand` и др.) добавляются прямо в корень объекта.
Перезапись по id: Если в индексе уже есть товар с таким id, сервис удалит старую запись и запишет новую. Это позволяет обновлять товары, но требует, чтобы id был уникальным и не менялся между загрузками.

Ответ (успех):

{
  "request_id": "550e8400-e29b-41d4-a716-446655440000",
  "success": true,
  "error": null,
  "results": [
    {
      "id": "1C-GUID-0002",
      "success": true,
      "error": null
    }
  ]
}

Запрос с ошибкой (пустой id):

curl -X POST http://localhost:8080/add \
  -H "Content-Type: application/json" \
  -d '{
    "items": [{
      "name": "Товар без GUID",
      "id": ""
    }]
  }'
# Тот же запрос через CLI
# Создайте файл item.json:
    [
      {
        "name": "Товар без GUID",
        "id": ""
      }
    ]

sku-cli add item.json

Ответ (ошибка):

{
  "request_id": "550e8400-e29b-41d4-a716-446655440000",
  "success": false,
  "error": "Batch validation failed",
  "results": [
    {
      "id": "",
      "success": false,
      "error": "id missing or empty"
    }
  ]
}

POST /add?dry_run=true — проверить без записи

POST/add?dry_run=true
curl -X POST "http://localhost:8080/add?dry_run=true" \
  -H "Content-Type: application/json" \
  -d '{"items":[{"name":"Новый товар","id":"NEW-001"}]}'
# Тот же запрос через CLI
# Создайте файл item.json:
    [
      {
        "name": "Новый товар",
        "id": "NEW-001"
      }
    ]

sku-cli add item.json --dry-run

Ответ:

{
  "request_id": "550e8400-e29b-41d4-a716-446655440000",
  "success": true,
  "error": null,
  "results": [
    {
      "id": "NEW-001",
      "success": true,
      "error": "would_insert"
    }
  ]
}
`error` содержит `"would_insert"` — новый товар, `"would_update"` — обновит существующий.

POST /delete — удалить

POST/delete
curl -X POST http://localhost:8080/delete \
  -H "Content-Type: application/json" \
  -d '{"items":[{"id":"1C-GUID-OLD-001"}]}'
# Тот же запрос через CLI
sku-cli delete "1C-GUID-OLD-001"

Ответ:

{
  "request_id": "550e8400-e29b-41d4-a716-446655440000",
  "success": true,
  "error": null,
  "results": [
    {
      "id": "1C-GUID-OLD-001",
      "success": true,
      "error": null
    }
  ]
}

POST /synonyms — управление синонимами

POST/synonyms
curl -X POST http://localhost:8080/synonyms \
  -H "Content-Type: application/json" \
  -d '{"items":[{"word":"цинк","expansions":["оцинкованный","zn","zinc"]}]}'
# Тот же запрос через CLI
sku-cli synonyms add --word "цинк" --expansions "оцинкованный,zn,zinc"

Ответ:

{
  "results": [
    {
      "word": "цинк",
      "success": true,
      "reason": null
    }
  ]
}

GET /synonyms — получить все синонимы

GET/synonyms
curl http://localhost:8080/synonyms
# Тот же запрос через CLI
sku-cli synonyms list

Ответ:

{
  "synonyms": {
    "цинк": ["оцинкованный", "zn", "zinc"],
    "болт": ["болтик", "винт"]
  }
}

POST /feedback — обратная связь для обучения

POST/feedback
curl -X POST http://localhost:8080/feedback \
  -H "Content-Type: application/json" \
  -d '{
    "query_name": "болт м16 цинк",
    "matched_id_out": "1C-GUID-0001",
    "action": "reject"
  }'
# Тот же запрос через CLI
sku-cli feedback --query "болт м16 цинк" --matched-id "1C-GUID-0001" --action reject

Ответ:

{
  "status": "logged"
}
`action` может быть `"accept"` (подтвердить), `"reject"` (отклонить) или `"zero_result"` (по запросу ничего не нашлось).
Помимо обязательных полей query_name, matched_id_out и action, запрос может содержать опциональные поля: product_name, product_art, product_barcode, matched_by, position, session_id, expected_id.

GET /health — проверить состояние сервиса

GET/health
curl http://localhost:8080/health
# Тот же запрос через CLI
sku-cli health

Ответ:

{
  "status": "ok",
  "version": "0.2.9",
  "edition": "pro",
  "uptime_seconds": 3600,
  "config_path": "/app/config.toml",
  "config_hash": "a2476ef4e23468b5",
  "db_record_count": 20001,
  "db_size_mb": 142.5,
  "disk_free_gb": 180.2,
  "synonyms_count": 42,
  "cpu_usage_percent": 2.1,
  "ram_used_mb": 11912,
  "backups_count": 3,
  "cache_hits": 1234,
  "cache_misses": 567,
  "cache_size": 1000,
  "qps": 12.5,
  "index_status": {
    "optimized": true,
    "segments_count": 1
  },
  "orchestrator": {
    "strategy": "hybrid",
    "fs_enabled": true,
    "embedding_enabled": true,
    "total_searches": 0,
    "avg_time_ms": 0.0
  },
  "embedding": {
    "enabled": true,
    "model_loaded": true,
    "vector_count": 20399,
    "vector_dimension": 1024,
    "avg_search_time_ms": 0.0
  },
  "embedding_queue": {
    "enabled": true,
    "pending": 0,
    "processing": 0,
    "completed": 20399,
    "failed": 0,
    "dead": 0,
    "total": 20399
  },
  "background_workers": {
    "worker_count": 2,
    "active_workers": 2,
    "total_restarts": 0,
    "restarts_last_minute": 0,
    "max_restarts_per_minute": 8,
    "monitor_interval_secs": 2
  },
  "license": {
    "enabled": true,
    "active": true,
    "fingerprint": "sha256:..."
  },
  "last_error": null
}
В Free-режиме блоки `embedding`, `embedding_queue` и `background_workers` возвращают {"enabled": false, "model_loaded": false, "vector_count": 0} (для embedding) или {"enabled": false} (для остальных). Поле last_error всегда присутствует и равно null, если ошибок не было.

GET /export — выгрузить все данные

GET/export?format=csv&limit=1000&offset=0
curl "http://localhost:8080/export?format=csv&limit=1000&offset=0" -o export.csv

Параметры:

  • format — "json" (по умолчанию) или "csv"
  • limit — количество строк (макс. из конфига, по умолчанию 1000)
  • offset — смещение (для пагинации)

Ответ (CSV):

id_out,name,code,barcode,art
1C-GUID-0001,Болт М16х60 оцинкованный ГОСТ 7798-70,BOLT-M16-60-ZN,"[""4607000010015""]",78454
1C-GUID-0002,Гайка М16 нержавеющая,NUT-M16-A2,"[""4607000010022""]",GN-16

GET /api/search-config — получить конфигурацию поиска (для UI)

GET/api/search-config
curl http://localhost:8080/api/search-config
# Тот же запрос через CLI
sku-cli config

Ответ:

{
  "extended_fields": [
    {"field": "supplier", "weight": 0.7, "stemmed": true, "fuzzy": true},
    {"field": "brand", "weight": 0.6, "stemmed": true, "fuzzy": true}
  ],
  "extended_search_enabled": true,
  "require_name_for_fuzzy": false,
  "pro_enabled": true,
  "embedding_enabled": true,
  "search_strategy": "parallel",
  "pro_info_url": "https://example.com/pro",
  "purchase_url": "https://example.com/purchase"
}
В Free-режиме extended_fields возвращается пустым массивом, pro_enabled и embedding_enabled — false. Поля pro_info_url и purchase_url присутствуют в обеих редакциях, если заданы в конфигурации.

PRO API — Аналитика и тюнинг (только PRO версия)

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

GET /admin/analytics/stats — статистика поиска

GET/admin/analytics/stats
curl http://localhost:8080/admin/analytics/stats
# Тот же запрос через CLI
sku-cli analytics stats

POST /admin/analytics/reset — сброс статистики

POST/admin/analytics/reset

GET /admin/analytics/tuning — рекомендации по настройке

GET/admin/analytics/tuning

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

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

POST /hierarchy — построение дерева спецификаций

POST/hierarchy

Запрос (разузлование вниз):

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

Параметры:

  • root_id — ID корневого элемента (обязательно). Для direction=down это начало дерева; для direction=up — деталь, для которой ищутся родители.
  • direction — только строчные значения: "down" (разузлование, от изделия к деталям) или "up" (Where-Used, от детали к изделиям). Значения с заглавной буквы ("Down", "UP") вернут ошибку 422.
  • max_depth — максимальное количество уровней от корня. Корень имеет уровень 0, его прямые потомки/родители — уровень 1 и т.д. По умолчанию 5 (из конфига pro.plugins.hierarchy.default_max_depth).
  • include_cross_codes — включать аналоги для каждого узла. Сам узел в свой список аналогов не попадает. По умолчанию false (из конфига pro.plugins.hierarchy.include_cross_codes_by_default).
  • limit — общий лимит узлов во всем дереве или цепочке, включая корень. Не «на уровень». По умолчанию 1000 (из конфига pro.plugins.hierarchy.default_limit).

PRO API — Управление эмбеддингами

GET /admin/embedding/stats — статистика эмбеддингов

GET/admin/embedding/stats

POST /admin/embedding/reindex — переиндексация векторов

POST/admin/embedding/reindex

POST /admin/embedding/reindex/background — фоновая переиндексация

POST/admin/embedding/reindex/background

POST /admin/embedding/clear — очистить векторное хранилище

POST/admin/embedding/clear

POST /admin/embedding/optimize — оптимизировать HNSW индекс

POST/admin/embedding/optimize

POST /admin/embedding/cache/clear — очистить кэш эмбеддингов

POST/admin/embedding/cache/clear

Административные эндпоинты

PRO only: очистка индекса, оптимизация, аналитика, управление эмбеддингами, иерархия и кросс-коды доступны только в PRO.\ Free + PRO: `/admin/config` и `/admin/config/update` работают в обеих редакциях (при включенных мутациях).

POST /admin/clear — очистить весь индекс (PRO only)

POST/admin/clear
Необратимая операция! Удаляет все документы из индекса.

GET /admin/learn — анализ фидбека и предложение синонимов

GET/admin/learn

GET /admin/index/stats — статистика индекса

GET/admin/index/stats

POST /admin/index/optimize — оптимизация индекса (мерж сегментов)

POST/admin/index/optimize

GET /admin/queries/top — топ запросов

GET/admin/queries/top?limit=10

POST /admin/cache/clear — очистить кэш запросов

POST/admin/cache/clear

POST /admin/config/update — обновить конфигурацию на лету

POST/admin/config/update
curl -X POST http://localhost:8080/admin/config/update \\
  -H "Content-Type: application/json" \\
  -d '{"search":{"auto_match_threshold":0.9}}'
Принимает JSON с partial-обновлением секций конфига. Изменения применяются без перезапуска сервиса.

GET /admin/config — текущая конфигурация

GET/admin/config
GET /admin/config и GET /api/config возвращают одинаковую редактируемую конфигурацию (секции search, pro, csv, export). Различий между «admin» и «public» представлением нет.

GET /api/config — текущая конфигурация

GET/api/config

GET /swagger-ui — интерактивная документация OpenAPI

GET/swagger-ui

GET / — embedded GUI

GET/

Возвращает HTML-страницу встроенного интерфейса (Free/PRO GUI или страница активации), а не JSON-статус.

Полный список эндпоинтов

Мутации (/add, /delete, /feedback, /synonyms, /admin/config/update, /admin/clear, /admin/index/optimize, /admin/cache/clear, /admin/analytics/reset, /admin/embedding/*, /hierarchy и др.) регистрируются только при allow_mutations = true. В режиме «только для сайта» (site.enabled = true) мутации возвращают 403 Forbidden.
МетодПутьОписаниеFreePRO
GET/Embedded GUI (Free/PRO/активация)✓✓
GET/sample_queriesПримеры поисковых запросов✓✓
GET/docs/*Встроенная документация (mdbook)✓✓
GET/static/*Статические файлы GUI✓✓
GET/swagger-uiИнтерактивная документация OpenAPI✓✓
GET/api-docs/openapi.jsonOpenAPI-спецификация✓✓
GET/api/update-checkПроверка обновлений✓✓
POST/findПоиск номенклатуры✓✓
POST/addДобавить/обновить товары✓✓
POST/deleteУдалить товары✓✓
POST/synonymsУправление синонимами✓✓
DELETE/synonyms/:wordУдалить синоним✓✓
GET/synonymsПолучить синонимы✓✓
POST/feedbackОбратная связь для обучения✓✓
GET/healthСостояние сервиса✓✓
GET/exportЭкспорт данных✓✓
GET/api/search-configКонфигурация для UI✓✓
GET/api/configТекущая конфигурация✓✓
POST/api/activateАктивация PRO-лицензии—✓
GET/api/license/statusСтатус лицензии—✓
GET/api/license/fingerprintFingerprint сервера—✓
POST/api/license/uploadЗагрузка файла лицензии—✓
GET/api/license/heartbeatHeartbeat лицензии—✓
GET/api/license/crlСписок отозванных лицензий—✓
POST/api/license/recoverВосстановление лицензии—✓
GET/api/subscription-statusСтатус подписки—✓
GET/adminAdmin GUI✓✓
POST/admin/clearОчистить индекс—✓
GET/admin/learnАнализ фидбека—✓
GET/admin/index/statsСтатистика индекса—✓
POST/admin/index/optimizeОптимизация индекса—✓
GET/admin/queries/topТоп-10 запросов—✓
POST/admin/cache/clearОчистить кэш—✓
POST/admin/config/updateОбновить конфигурацию на лету✓✓
GET/admin/configТекущая конфигурация✓✓
POST/hierarchyИерархия (BOM)—✓
GET/admin/analytics/statsСтатистика аналитики—✓
POST/admin/analytics/resetСброс аналитики—✓
GET/admin/analytics/tuningРекомендации по настройке—✓
GET/admin/embedding/statsСтатистика эмбеддингов—✓
POST/admin/embedding/reindexПереиндексация векторов—✓
POST/admin/embedding/reindex/backgroundФоновая переиндексация—✓
POST/admin/embedding/clearОчистить векторное хранилище—✓
POST/admin/embedding/optimizeОптимизировать HNSW—✓
POST/admin/embedding/cache/clearОчистить кэш эмбеддингов—✓
GET/admin/background/statsСтатистика фоновой очереди—✓

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

Доступно только в 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`, но помните, что каждый уровень добавляет время ответа.

⌨ CLI-утилита

Для администраторов: CLI-утилита `sku-cli` позволяет управлять сервисом из командной строки — от поиска до управления моделями. Справка встроена: `sku-cli --help` и `sku-cli <команда> --help`.

Общие параметры

# Указать сервер явно
    sku-cli -s http://localhost:8081 health

    # Или через переменную окружения
    export SKU_CLI_SERVER=http://localhost:8082
    sku-cli health

    # Тихий режим (только JSON) — только флаг `-q`, env не поддерживается
    sku-cli -q search "болт" --format json

    # Авторизация по API-ключу (если включена) — только флаг `-a`, env не поддерживается
    sku-cli -a secret-key search "болт"

    # Загрузить config.toml (нужен для csv.default_delimiter и др.)
    sku-cli --config /etc/sku-search/config.toml health
Глобальные флаги: -s / --server, -a / --api-key, -q / --quiet, -c / --config. Переменные окружения поддерживаются только для SKU_CLI_SERVER.

1. Поиск

Команда search принимает позиционный запрос (название) и дополнительные фильтры. UUID длиной 36 символов автоматически интерпретируется как id.

# Обычный поиск
    sku-cli search "болт м16" --limit 5

    # Поиск по коду, артикулу, штрихкоду
    sku-cli search --code CSV-001 --art ART-42
    sku-cli search --barcode 4601234567890 --barcode 4609876543210
    sku-cli search --id 550e8400-e29b-41d4-a716-446655440000

    # Поиск с указанием стратегии (принимается везде, эффект только в PRO)
    sku-cli search "шуруповерт" --strategy hybrid --limit 10

    # Поиск только по динамическим/extra-полям (PRO only)
    sku-cli search "" --field supplier="ООО Ромашка" --dynamic-only
    sku-cli search "болт" --field material=сталь --field color=черный

    # Поиск с дополнительными 1С-полями
    sku-cli search "болт" --type "крепеж" --desc "оцинкованный" --ref "uuid-123"

    # Поиск с детальной статистикой
    sku-cli search "болт" --show-stats --format json

Стратегии поиска

Флаг --strategy принимается в обеих редакциях, но эффект имеет только в PRO (во Free любое значение сводится к fs_only).

  • fs_only — только полнотекстовый поиск (default).
  • embedding_only — только векторный (семантический) поиск (PRO).
  • hybrid — полнотекстовый поиск + эмбеддинги, взвешенный скор (PRO).
  • parallel — полнотекстовый поиск и эмбеддинги параллельно, быстрее, но выше нагрузка (PRO).

2. Добавление товаров

Команда add загружает JSON-файл с массивом объектов ItemQuery.

# Прямая загрузка
    sku-cli add items.json

    # Проверка без записи
    sku-cli add items.json --dry-run

    # Только полнотекстовый индекс (без эмбеддингов, быстрее)
    sku-cli add items.json --fs-only

Пример файла items.json:

[
      {
        "id": "ID-001",
        "name": "Болт М6x20",
        "code": "CSV-001",
        "art": "ART-001",
        "barcode": ["4601234567890"],
        "type": "крепеж",
        "material": "сталь",
        "color": "оцинкованный"
      }
    ]
Поля `id` и `name` обязательны. Поля `code`, `art`, `barcode` — основные идентификаторы. Все остальные поля сохраняются как extra-атрибуты и доступны для поиска через `-F`.

3. Импорт из CSV

Команда import преобразует CSV в батчи /add.

# Базовый импорт (батчи по 500 строк — лимит сервера)
    sku-cli import --csv items.csv

    # Импорт демо-данных из архива
    sku-cli import --csv data/demo-data.csv --batch-size 500

    # Сухой запуск
    sku-cli import --csv items.csv --dry-run

    # Пропуск ошибок и свой размер батча
    sku-cli import --csv items.csv --skip-errors --batch-size 500

    # Нестандартный разделитель через флаг
    sku-cli import --csv items.csv --delimiter ';'

    # Разделитель по умолчанию из config.toml
    sku-cli --config config.toml import --csv items.csv

    # Без эмбеддингов
    sku-cli import --csv items.csv --fs-only

CSV должен содержать колонки id и name. Рекомендуемые колонки: code, art, barcode, type. Остальные колонки импортируются как extra-атрибуты. Сервер принимает батчи максимум 500 строк; CLI по умолчанию использует 500.

4. Экспорт

# JSON в stdout
    sku-cli export --format json --limit 1000 > backup.json

    # CSV в файл
    sku-cli export --format csv --limit 10000 --output export.csv

    # Постранично
    sku-cli export --format json --offset 1000 --limit 1000 > page2.json

5. Удаление

# По одному ID
    sku-cli delete "ID-001"

    # Несколько ID
    sku-cli delete "ID-001" "ID-002" "ID-003"

    # Из файла (один ID на строку)
    sku-cli delete --file ids.txt

6. Обратная связь (feedback)

Помогает обучению поиска. Регистрирует, какой результат был выбран для запроса.

# Подтвердить релевантность (для обучения синонимов важно передать product_name)
    sku-cli feedback --query "болт м10" --matched-id "ID-001" --action accept --product-name "Болт М10х40 оцинкованный"

    # Отметить нерелевантность
    sku-cli feedback --query "болт м10" --matched-id "ID-002" --action reject

    # Запрос без результатов
    sku-cli feedback --query "перфоратор бош" --action zero_result

    # Дополнительные опции: --product-art, --product-barcode, --matched-by, --position, --session-id, --expected-id

7. Синонимы

# Добавить синонимы
    sku-cli synonyms add --word "цинк" --expansions "оцинкованный,zn,zinc"

    # Список всех синонимов
    sku-cli synonyms list

    # Фильтр по слову
    sku-cli synonyms list --word "цинк"

    # Удалить синоним
    sku-cli synonyms remove --word "цинк"

8. Администрирование индекса (PRO only)

# Статистика индекса
    sku-cli index-stats

    # Оптимизация индекса (мерж сегментов)
    sku-cli optimize

    # Очистка кэша запросов
    sku-cli cache-clear

    # Полная очистка индекса (ОСТОРОЖНО!)
    sku-cli clear --force
Все команды этого раздела работают только при подключении к PRO-серверу. Во Free-сборке соответствующие HTTP-роуты отсутствуют, и CLI получит 404.

9. Аналитика и обучение (PRO)

# Топ запросов
    sku-cli top-queries --limit 20

    # Статистика аналитики
    sku-cli analytics stats

    # Рекомендации по тюнингу
    sku-cli analytics tuning

    # Сброс накопленной аналитики
    sku-cli analytics reset

    # Предложения синонимов на основе фидбека
    sku-cli learn

10. Управление эмбеддингами (PRO)

# Статистика эмбеддинг-движка
    sku-cli embedding stats

    # Статус фоновой очереди
    sku-cli background

    # Переиндексация векторов (синхронно)
    sku-cli embedding reindex

    # Переиндексация векторов в фоне
    sku-cli embedding reindex --background

    # Оптимизация векторного индекса
    sku-cli embedding optimize

    # Очистка кэша модели
    sku-cli embedding cache-clear

    # Удалить все векторы (ОСТОРОЖНО!)
    sku-cli embedding clear

11. Иерархия (PRO)

# Разузлование (дети)
    sku-cli hierarchy "ID-001" --direction down --depth 3

    # Where-used (родители)
    sku-cli hierarchy "ID-001" --direction up --depth 3

    # С кросс-кодами и JSON-выводом
    sku-cli hierarchy "ID-001" --include-cross --format json

12. Утилиты

# Проверка здоровья сервера
    sku-cli health
    sku-cli health | jq .db_record_count

    # Текущая конфигурация поиска
    sku-cli config

    # Версия CLI
    sku-cli version

    # Автодополнение для bash
    sku-cli completion -s bash > /etc/bash_completion.d/sku-cli

    # Автодополнение для zsh
    sku-cli completion -s zsh > ~/.zsh/completions/_sku-cli

    # Man-страница
    sku-cli man > /tmp/sku-cli.1

Быстрый справочник по переменным окружения

SKU_CLI_SERVER http://localhost:8080
Базовый URL сервиса. Для Free используйте `http://localhost:8081`, для PRO — `http://localhost:8082`.
Поддерживается только SKU_CLI_SERVER. API-ключ (-a) и тихий режим (-q) задаются только флагами, переменных окружения для них нет.
Пример использования в скриптах:
#!/bin/bash
    # Автоматическая синхронизация с 1С

    # Выгружаем все товары в CSV
    sku-cli export --format csv --limit 50000 --output /tmp/export.csv

    # Импортируем новые товары из прайса
    sku-cli import --csv /tmp/price.csv --skip-errors

    # Оптимизируем индекс
    sku-cli optimize

    # Получаем статистику
    sku-cli health | jq .db_record_count

Архитектура и продвинутые концепции

Для кого этот раздел: Для технических специалистов, разработчиков и всех, кто хочет понять, как сервис работает «под капотом». Здесь мы разбираем три уровня поиска, семантический поиск (PRO), гибридные стратегии и подготовку моделей.

Три уровня поиска — от простого к сложному

Сервис ищет товары на трех уровнях:

1
Точный поиск — по id, barcode, code, art. Скорость: 1-2 мс. Нашел → сразу возвращает результат.
2
N-gram + Fuzzy поиск — по названию и динамическим полям. N-gram находит по короткому началу: "перф" → "Перфоратор". Fuzzy находит с опечатками: "блот" → "болт". Скорость: 3-10 мс.
3
Семантический поиск (PRO only) — ищет по смыслу. "шурик" → "шуруповерт", "дрель-шуруповерт" → "шуруповерт". Скорость: 15-50 мс (с INT8-квантованием).
Уровни не являются строгим каскадом «предыдущий не дал результат → включаем следующий». Точный поиск действительно имеет ранний выход при совпадении по id/code/art/barcode. Однако в стратегиях hybrid и parallel семантический поиск запускается параллельно с полнотекстовым всегда, а их результаты объединяются по весам.
Когда эмбеддинги (семантический поиск) реально нужны?
  • Пользователи используют сленг ("шурик", "макака", "холодильник самсунг")
  • В названиях товаров нет точных совпадений со словами пользователей
  • Вы хотите поднять точность поиска с 85% до 92-95%
  • Если все товары ищутся по артикулу/штрихкоду — эмбеддинги не дадут выигрыша

Принципиальная схема поиска

┌─────────────────────────────────────────────────────────────┐
│                        sku-search                            │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  Запрос пользователя: "шурик бош"                            │
│         │                                                    │
│         ▼                                                    │
│  ┌──────────────────────────────────────────────────────┐   │
│  │         Маршрутизатор запросов                        │   │
│  │  ┌──────────────┐  ┌──────────────┐  ┌────────────┐  │   │
│  │  │ Точный поиск │  │ N-gram/Fuzzy │  │ Векторный  │  │   │
│  │  │ (ID/артикул) │  │  (текст)     │  │  поиск     │  │   │
│  │  └──────┬───────┘  └──────┬───────┘  └─────┬──────┘  │   │
│  │         │                 │                 │         │   │
│  │         └─────────────────┼─────────────────┘         │   │
│  │                           ▼                           │   │
│  │              ┌─────────────────────┐                  │   │
│  │              │  Слияние и ранжирование               │   │
│  │              └─────────────────────┘                  │   │
│  └──────────────────────────────────────────────────────┘   │
│                           │                                  │
│                           ▼                                  │
│              Результат: "шуруповерт бош" (score: 0.94)       │
└─────────────────────────────────────────────────────────────┘

Семантический поиск (эмбеддинги) — как это работает

Доступно только в PRO версии! Требует включенных эмбеддингов в секции [pro.search] и подготовленной ONNX-модели.

Простая аналогия: Представьте, что каждый товар и каждый поисковый запрос получают свой «отпечаток пальца» — набор из 1024 чисел (для модели bge-m3; e5-small дает 384 числа). Похожие по смыслу товары имеют похожие отпечатки. Сервис умеет находить «отпечатки», которые ближе всего к отпечатку запроса, за 5-10 мс.

Как это работает на практике

1
Подготовка модели (один раз) — конвертируем модель (например, multilingual-e5-small) в формат ONNX с INT8-квантованием. Получаем файл model.onnx (30-40 МБ).
2
При добавлении товара — берем супер-текст (название + бренд + категория + артикул), ставим построение вектора в фоновую очередь и сразу возвращаем ответ. Фоновые воркеры асинхронно превращают текст в вектор и сохраняют его в векторное хранилище вместе с ID товара.
3
При поиске — пользовательский запрос превращаем в вектор, ищем в векторном хранилище ближайшие векторы, получаем ID товаров.
4
Объединение — результаты полнотекстового поиска (с весом) + результаты векторного поиска (с весом) → финальная сортировка.
Важно: Эмбеддинги не заменяют полнотекстовый поиск, а дополняют. Полнотекстовый поиск отвечает за точные совпадения (артикул, код). Эмбеддинги — за смысл (сленг, синонимы).

Гибридный поиск — как объединяются результаты

Когда включены оба движка (полнотекстовый + семантический поиск), сервис запускает их параллельно и объединяет результаты с весами:

Итоговый score = normalize(текстовый_score) × weight_tantivy + normalize(семантический_score) × weight_embedding

Перед объединением скоры обоих источников нормализуются (`min_max`, `robust`, `z_score`, `softmax` или `none`). Метод задается в `pro.search.orchestrator.score_normalization` (default: `robust`).

Упрощенный пример (значения до нормализации):
  Полнотекстовый поиск нашел товар с score = 0.70
  Семантический поиск нашел тот же товар с score = 0.92
  Веса: weight_tantivy = 0.6, weight_embedding = 0.4

  Итоговый score ≈ 0.70 × 0.6 + 0.92 × 0.4 = 0.42 + 0.368 = 0.788

Рекомендуемые веса

Режимtantivy_weightembedding_weightКогда использовать
Преимущество полнотекстового поиска (баланс)0.60.4Большинство кейсов, рекомендуется по умолчанию
Равные права0.50.5Текст и семантика одинаково важны
Преимущество семантики0.40.6Сленг и семантика важнее точных кодов
Только полнотекстовый поиск1.00.0Эмбеддинги выключены
Только семантический поиск0.01.0Экспериментальный режим

Как подготовить ONNX-модель с INT8-квантованием

Это нужно сделать один раз на любом Linux-компьютере. Готовую модель вы кладете рядом с исполняемым файлом сервиса.
# 1. Установка зависимостей (одна команда)
pip install optimum[exporters] onnxruntime transformers

# 2. Конвертация E5-small в ONNX с INT8-квантованием
optimum-cli export onnx --model intfloat/multilingual-e5-small --task feature-extraction --optimize O2 ./models/e5

optimum-cli onnxruntime quantize --onnx_model ./models/e5 -o ./models/e5-int8 --avx512

# Готово! В папке ./models/e5-int8 вы найдете model.onnx (30-40 МБ)

Доступные модели для конвертации

МодельРазмерСкорость инференсаТочность
multilingual-e5-small30-40 МББыстро (10-30 мс)Хорошая
multilingual-e5-base~280 МБСредне (20-60 мс)Очень хорошая
multilingual-e5-large~560 МБМедленно (50-150 мс)Отличная
Рекомендация: начните с multilingual-e5-small — баланс скорости и качества для большинства задач.

Векторное хранилище — что хранит и как ищет

Векторы товаров хранятся во встраиваемой векторной базе данных. Она не требует отдельного сервера — это просто файлы на диске.

  • Хранит пары: (ID товара, вектор)
  • Тип индекса задается параметром pro.search.embedding.vector_index_type (default: "auto"). Рекомендуется оставить "auto" — хранилище само выберет подходящий индекс. При смене типа нужно выполнить sku-cli embedding clear и переиндексировать товары, иначе останется старый индекс.
  • Поиск: 1-10 мс для 1 млн векторов
  • Не требует держать все векторы в RAM — данные на диске
# Что хранится в векторном хранилище (упрощенно)
┌─────────────────────────────────────┐
│  id: "DETAL-001"                    │
│  vector: [0.123, -0.456, 0.789...]  │  ← 1024 числа (bge-m3)
├─────────────────────────────────────┤
│  id: "DETAL-002"                    │
│  vector: [0.111, -0.444, 0.777...]  │
└─────────────────────────────────────┘

Как эмбеддинги работают с дополнительными полями

Простой принцип: перед отправкой в модель вы склеиваете ВСЕ значимые поля в один «супер-текст». Модель сама «поймет», где бренд, где название, где артикул.

Пример супер-текста для эмбеддинга:
  "Шуруповерт аккумуляторный Bosch GSR 12V-15 электроинструмент"

Что дает:
  • Запрос "шурик" → найдет этот товар (сленг)
  • Запрос "bosch gsr" → найдет (артикул)
  • Запрос "аккумуляторный инструмент" → найдет (категория)

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

Скорость работы: честные цифры

ОперацияВремяПримечание
Точный поиск (ID/штрихкод)1-2 мсВсегда работает быстро
N-gram / Fuzzy поиск3-10 мсОсновной режим
Эмбеддинг (вектор запроса)10-30 мсС INT8-квантованием
Поиск в векторном хранилище (1 млн векторов)5-10 мсАвтоматически выбранный индекс
Гибридный поиск (текст + векторы)20-50 мсПараллельный запуск
Индексация 10 000 товаров (с эмбеддингами)~2 минутыФоновая пакетная обработка
Зависит от железа: Все цифры приблизительные и сильно зависят от мощности вашего сервера (CPU, RAM, диск), размера базы и сложности запроса. На слабом железе время может быть в 2-3 раза выше, на мощном — ниже.
Что это значит для пользователя: Даже самый сложный гибридный поиск занимает меньше времени, чем вы моргаете (100-150 мс). Пользователь не заметит разницы.

Сравнение подходов (для принятия решений)

ФункцияТолько полнотекстовый поискТекст + эмбеддинги
Точный поиск (ID/штрихкод)✓✓
Fuzzy / опечатки✓✓
N-gram (короткие запросы)✓✓
Семантика / сленг ("шурик" → "шуруповерт")—✓
Синонимы без ручного ввода—✓
Скорость поиска2-10 мс15-50 мс
Память (RAM)200-500 МБ500-1000 МБ
Размер дистрибутива10-20 МБ60-80 МБ
Вердикт: Начинайте с классического полнотекстового поиска. Эмбеддинги подключайте, когда:
  1. Основная логика стабильна
  2. Есть реальные запросы пользователей, которые текущий поиск не покрывает
  3. Вы готовы добавить «AI-усиление» как PRO-фичу

Частые вопросы по архитектуре и эмбеддингам

1. Нужно ли переиндексировать все товары при смене модели?

Да. У каждой модели свое «понимание» смысла — векторы будут разными. Если вы меняете модель, векторное хранилище нужно перезаполнить.

2. А если я хочу попробовать несколько моделей?

Сервис хранит векторы только в одной таблице на инстанс (задается через lancedb.path + collection_name). Переход на другую модель делается в четыре шага:

  1. Удалите старые векторы: sku-cli embedding clear (или вручную удалите файл векторного хранилища).
  2. В config.toml измените active_backend, model_path и vector_dimension на новую модель.
  3. Перезапустите сервис.
  4. Заново загрузите все товары — сервис пересчитает эмбеддинги для каждого добавленного товара. Старые векторы при этом будут перезаписаны.

3. Как эмбеддинги влияют на время индексации?

Добавляет ~20-30 мс на товар (вычисление вектора). Для массовой загрузки используйте пакетную обработку (batch-инференс) — это ускорит процесс в 5-10 раз.

4. Мой сервер слабый — потянет ли он эмбеддинги?

Минимальные требования для E5-small с INT8:

  • RAM: 500 МБ свободной (модель 30 МБ + векторное хранилище)
  • CPU: любой с поддержкой AVX2 (Intel Core i3 / AMD Ryzen 3 и выше)
  • Диск: SSD (для векторного хранилища)

Если сервер очень слабый — используйте только полнотекстовый поиск (эмбеддинги отключите).

ONNX Runtime в PRO-редакции

Только для PRO. ONNX Runtime нужен только в PRO-редакции, если включен семантический поиск (эмбеддинги). В Free-редакции он не используется.
CPU-рантайм уже внутри архива PRO. Если вы скачали sku-search-pro.zip, распаковали его и не планируете использовать GPU, дополнительно ничего скачивать не нужно — нужные .so/.dll уже лежат в папке runtime/.

PRO-версия загружает ONNX Runtime из папки runtime/ рядом с исполняемым файлом. Это позволяет:

  • поставлять один исполняемый файл для всех конфигураций;
  • использовать разные рантаймы: CPU, CUDA, DirectML, ROCm;
  • не качать лишние сотни мегабайт GPU-зависимостей, если GPU не нужен.

Что входит в поставку

В архиве PRO уже есть CPU-рантайм, а также созданы пустые папки для GPU-рантаймов. После распаковки структура выглядит так:

sku-search-pro/
├── sku-service
├── config.toml
└── runtime/
    ├── linux-cpu/
    │   ├── libonnxruntime.so.1.23.2
    │   ├── libonnxruntime.so.1 -> libonnxruntime.so.1.23.2
    │   ├── libonnxruntime.so -> libonnxruntime.so.1
    │   └── libonnxruntime_providers_shared.so
    ├── linux-cuda/          # пустая, сюда кладется CUDA-рантайм
    ├── linux-rocm/          # пустая, сюда кладется ROCm-рантайм
    ├── win-cpu/             # уже содержит onnxruntime.dll с CPU + DirectML
    └── win-cuda/            # пустая, сюда кладется CUDA-рантайм

Если вы не используете GPU, ничего докачивать не нужно — сервис сразу загрузит CPU-рантайм из папки runtime/linux-cpu/ (или runtime/win-cpu/ на Windows). В win-cpu/onnxruntime.dll уже встроен DirectML execution provider; отдельно докачивать DirectML не требуется. Для CUDA нужно положить файлы в runtime/win-cuda/ или runtime/linux-cuda/.

Как подключить GPU-рантайм

  1. Скачайте нужный архив с официальной страницы ONNX Runtime 1.23.2:
ПлатформаВариантСсылкаРазмер архива
LinuxCPU (уже в поставке)onnxruntime-linux-x64-1.23.2.tgz~8 МБ
LinuxGPU (CUDA)onnxruntime-linux-x64-gpu-1.23.2.tgz~230 МБ
WindowsCPU / DirectML (уже в поставке)onnxruntime-win-x64-1.23.2.zip~78 МБ
WindowsGPU (CUDA)onnxruntime-win-x64-gpu-1.23.2.zip~326 МБ
macOSCPUonnxruntime-osx-universal2-1.23.2.tgz—
ROCm нет в готовых сборках Microsoft. Для запуска на AMD ROCm вам потребуется самостоятельно получить совместимую библиотеку libonnxruntime.so и положить ее в runtime/linux-rocm/.
  1. Распакуйте архив и перенесите библиотеки в папку runtime/ рядом с sku-service:

Linux CUDA:

tar -xzf onnxruntime-linux-x64-gpu-1.23.2.tgz
mkdir -p runtime/linux-cuda
cp onnxruntime-linux-x64-gpu-1.23.2/lib/libonnxruntime.so* runtime/linux-cuda/
cp onnxruntime-linux-x64-gpu-1.23.2/lib/libonnxruntime_providers_* runtime/linux-cuda/

Windows CUDA:

unzip onnxruntime-win-x64-gpu-1.23.2.zip
mkdir runtime\win-cuda
xcopy onnxruntime-win-x64-gpu-1.23.2\lib\*.dll runtime\win-cuda\
Microsoft Visual C++ Redistributable. PRO-сборка для Windows собирается с динамической линковкой CRT. Для ее запуска на целевой машине должен быть установлен MSVC Redistributable x64. Если его нет, скачайте с официального сайта Microsoft.

Execution providers (провайдеры инференса)

PRO-сервис пытается зарегистрировать execution providers в следующем порядке:

  1. CUDA (Linux), DirectML / CUDA (Windows) — если в конфиге use_gpu = true.
  2. CPU (всегда доступен).
GPU опционален. Если CUDA/DirectML не найдены в системе, ONNX Runtime автоматически переключается на CPU. Это нормальное поведение, а не ошибка. Чтобы принудительно использовать только CPU, установите use_gpu = false в секции [pro.search.embedding].

Как сервис выбирает рантайм

Сервис ищет папку runtime/ в нескольких местах:

  1. В директории, заданной переменной окружения SKU_ORT_RUNTIME_DIR.
  2. Рядом с исполняемым файлом sku-service.
  3. В текущей рабочей директории.

Внутри runtime/ проверяются подпапки в порядке приоритета:

  • Linux: linux-cuda → linux-rocm → linux-cpu.
  • Windows: win-cuda → win-cpu (в win-cpu уже есть DirectML).
  • macOS: macos-cpu.

Если найден GPU-рантайм и он успешно загрузился — используется GPU. Если нет — сервис пытается загрузить CPU-рантайм. Если ни один не загрузился, embedding-движок отключается, но текстовый поиск продолжает работать.

Настройка производительности embedding-бэкенда

PRO-версия позволяет управлять тем, сколько ресурсов CPU/GPU тратить на вычисление эмбеддингов. Основные параметры — в секциях [pro.search.embedding] и [pro.search.embedding.<backend>] (где <backend> = e5 или bge_m3):

ПараметрГде задаетсяЗначениеЭффект
use_gpu[pro.search.embedding]true / falseПри true сервис пытается использовать CUDA/DirectML, при false — только CPU.
intra_op_threads[pro.search.embedding] и бэкенд0 или числоЧисло потоков внутри одного сеанса ONNX Runtime. 0 означает «авто» — рантайм сам выберет под доступное железо.
inter_op_threads[pro.search.embedding] и бэкенд0 или числоПотоки между операциями графа. Для embedding-моделей обычно достаточно 0 или 1.
session_pool_sizeбэкенд1, 2, 4…Число независимых сеансов ONNX Runtime. Увеличение ускоряет параллельные запросы, но умножает потребление RAM.
background.batch_size[pro.background]100, 200, 500…Сколько задач воркер забирает из очереди за один инференс. Больший батч снижает накладные расходы, но увеличивает задержку первого результата.
background.worker_count[pro.background]1, 2, 4…Число фоновых воркеров. Каждый воркер держит свою ONNX-сессию и потоки intra_op_threads.

Рекомендации по CPU

  • Хотите максимальную скорость индексации: установите intra_op_threads = 0 (авто), background.worker_count = 2 и background.batch_size = 500 для e5-small (или 100 для bge-m3). На 16-ядерном процессоре (AMD Ryzen 9 7945HX) фоновая индексация 10 000 коротких товаров через e5-small-int8 занимает ~51 с, то есть ~195 vec/s. Точные цифры для разных моделей и настроек приведены в разделе Производительность.
  • Нужно оставить ресурсы поиску/сайту: задайте фиксированное intra_op_threads = 4 и background.worker_count = 2. Скорость индексации упадет, но поиск останется отзывчивым.
  • Слабый CPU / Colab / VPS с 2 vCPU: оставьте intra_op_threads = 0 и background.worker_count = 1, иначе воркеры будут бороться за ядра. В Colab CPU-индексация 10 000 товаров занимает ~12–13 минут.
Значения потоков не переносятся «как есть» между машинами. Тот же intra_op_threads = 4 на 16-ядерном сервере и на 2-vCPU VPS даст совершенно разную скорость. Поэтому по умолчанию в dev-конфиге используется 0 — сервис сам подстроится под железо.

Рекомендации по GPU

  • В Colab / на сервере с CUDA установите use_gpu = true и intra_op_threads = 0. GPU берет основную нагрузку инференса, а CPU-потоки остаются на токенизацию и пост-обработку.
  • Если CUDA/DirectML не зарегистрируется, сервис автоматически fallback'ится на CPU — индексация замедлится, но не сломается.

Проверка

После запуска сервиса посмотрите ответ /health:

curl -s http://localhost:8080/health | jq .ort_runtime

# Или через CLI:
sku-cli health

Пример с загруженным CPU-рантаймом:

{
  "loaded": true,
  "path": "/opt/sku-search-pro/runtime/linux-cpu/libonnxruntime.so",
  "build_info": "ORT Build Info: git-branch=HEAD, git-commit-id=a83fc4d58c, build type=Release"
}

Если рантайм не найден:

{
  "loaded": false
}

Совместимость версий

Используйте ONNX Runtime 1.23.2. Именно с этой версией протестирован PRO-сервис.

Если установить другую версию (например, 1.18 или 1.30), возможны следующие сценарии:

  • Рантайм не загрузится совсем — в логах будет ошибка, /health покажет "loaded": false, embedding отключится, но текстовый поиск останется работать.
  • Рантайм загрузится, но модель или execution provider будут вести себя непредсказуемо — возможны сбои при индексации эмбеддингов или при поиске.

Если вы хотите использовать версию, отличную от 1.23.2, проверьте работу эмбеддингов на копии данных перед переводом в production.

Частые причины, почему рантайм не загружается

  • Папка runtime/ не найдена. Убедитесь, что она лежит рядом с sku-service или в текущей директории.
  • Неправильная структура подпапок. Для Linux CPU должно быть runtime/linux-cpu/libonnxruntime.so, для Windows — runtime/win-cpu/onnxruntime.dll.
  • Потеряны symlink'и. Если архив распаковывался на Windows/FAT, libonnxruntime.so может оказаться битой ссылкой. Запустите setup.sh — он восстановит symlink'и.
  • Несовпадение версий. Установлен ONNX Runtime не версии 1.23.2.
  • Отсутствует MSVC Redistributable на Windows. PRO-версия требует установленного Visual C++ Redistributable x64.
  • На Linux не найдены зависимости CUDA. Убедитесь, что в системе установлены совместимые CUDA/cuDNN-библиотеки, либо добавьте их в переменную окружения LD_LIBRARY_PATH.
  • На Windows не найдены зависимости CUDA. Убедитесь, что DLL CUDA (cudart64_*.dll, cublas64_*.dll и др.) доступны либо рядом с onnxruntime.dll, либо в системной переменной PATH.
  • Проблемы с правами. Библиотека должна быть доступна для чтения пользователю, от имени которого запущен сервис.

Пользовательская директория для рантайма

Если по какой-то причине рантайм нельзя положить рядом с исполняемым файлом, можно задать собственный путь через переменную окружения:

export SKU_ORT_RUNTIME_DIR=/opt/onnxruntime
./sku-service

В этом случае сервис будет искать подпапки linux-cpu, linux-cuda и т.д. именно в /opt/onnxruntime.

Подготовка ONNX-моделей для семантического поиска

Доступно только в PRO версии! Требует включенных эмбеддингов в секции [pro.search], подготовленной ONNX-модели и загруженного ONNX Runtime.
CPU-рантайм уже в архиве PRO. Если нужен GPU (CUDA/DirectML) или вы настраиваете рантайм вручную, см. раздел ONNX Runtime в PRO.

Семантический поиск (эмбеддинги) требует предварительно подготовленную ONNX-модель. Сервис поддерживает модели семейства E5 (e5-small, e5-base, e5-large) и BGE-M3. CLI-утилита может автоматически скачать, экспортировать и квантовать модель.

Что такое квантование? Это процесс уменьшения размера модели. FP32 → INT8 уменьшает размер в 4 раза (с 440 MB до 110 MB) с минимальной потерей точности (1-3%). Рекомендуется для production на CPU.

Быстрый старт: подготовка E5-small модели

# 1. Установка зависимостей (один раз)
    pip install optimum[onnxruntime] transformers torch onnx huggingface-hub --break-system-packages
    
    # 2. Подготовка модели (скачать, экспортировать, квантовать в INT8)
    # CLI-утилита sku-cli (входит в поставку) поддерживает команду model prepare.
    sku-cli model prepare \
        --model intfloat/e5-small-v2 \
        --output ./models/e5 \
        --quantize int8 \
        --optimize
    
    # 3. Проверка, что модель готова
    ls -la ./models/e5/int8/
    # Должны быть: model.onnx, tokenizer.json, config.json
    
    # 4. Настройка config.toml:
    # model_path = "./models/e5/int8/model.onnx"
    # vector_dimension = 384

Доступные модели

МодельРазмер (INT8)РазмерностьСкоростьТочностьДля каких задач
intfloat/e5-small-v2~120 MB384очень быстросреднеОбщие задачи, быстрый поиск
intfloat/e5-base-v2~440 MB768средневысокаяТочный поиск, сложные запросы
intfloat/e5-large-v2~1.3 GB1024медленномакс. точностьНаучные тексты, медицинские базы
BAAI/bge-m3~650 MB1024среднемакс. точностьМультиязычный поиск, редкие слова

Подготовка BGE-M3 (особый случай)

BGE-M3 — большая модель (1.1B параметров). Прямая конвертация через optimum-cli может занять часы и потребовать 32+ GB RAM. Рекомендуется скачать готовую ONNX-модель.
# Скачать INT8 модель (рекомендуется)
    huggingface-cli download skatzR/USER-BGE-M3-ONNX-INT8 --local-dir ./models/bge-m3/int8
    
    # Или FP16 модель
    huggingface-cli download hotchpotch/vespa-onnx-BAAI-bge-m3-only-dense --local-dir ./models/bge-m3/fp16
    
    # Настройка config.toml:
    # [pro.search.embedding]
    # active_backend = "bge_m3"
    # vector_db_path = "./data/vectors.lance"
    # query_cache_enabled = true
    # session_pool_size задается только в секции бэкенда (.bge_m3 / .e5)
    #
    # [pro.search.embedding.bge_m3]
    # enabled = true
    # model_path = "./models/bge-m3/int8/model.onnx"
    # vector_dimension = 1024
    # enable_sparse = false
    # pooling = "mean"
    # intra_op_threads = 4
    # inter_op_threads = 1
    # session_pool_size = 1

Параметры команды model prepare

\--model обязательный
Идентификатор модели на Hugging Face. Например: `intfloat/e5-small-v2`, `BAAI/bge-m3`.
\--output обязательный
Папка для сохранения модели. Внутри будут созданы подпапки: `onnx/` (FP32), `int8/` или `fp16/`.
\--quantize none \| int8 \| fp16 none
Тип квантования.
  • none — сохранить FP32 (оригинальный размер, максимальная точность)
  • int8 — 8-битное целочисленное (рекомендуется для CPU, размер в 4 раза меньше)
  • fp16 — 16-битное с плавающей точкой (баланс, размер в 2 раза меньше)
\--optimize флаг
Применить оптимизацию O2 при экспорте (ускорение инференса на 20-30%).
\--task строка "feature-extraction"
Задача ONNX-экспорта. Для embedding-моделей используется `feature-extraction`.

Проверка модели

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

# Запускаем сервис
    ./sku-service --config config.toml
    
    # В другом терминале проверяем статус эмбеддингов
    curl http://localhost:8080/health | jq .embedding
    
    # Ожидаемый ответ:
    # {
    #   "enabled": true,
    #   "model_loaded": true,
    #   "vector_count": 50000,
    #   "vector_dimension": 384,
    #   "avg_search_time_ms": 12.3
    # }
# Тот же запрос через CLI
sku-cli health
Частые проблемы и их решение:
  • Embedding включен, но /health показывает embedding.loaded = false — проверьте, что папка runtime/ с нужным рантаймом лежит рядом с sku-service. Подробности в разделе ONNX Runtime в PRO.
  • "No module named 'optimum'" — установите зависимости: pip install optimum[onnxruntime] transformers torch onnx huggingface-hub
  • "File too large" / protobuf limit — используйте use_external_data_format=True (скрипт делает это автоматически)
  • Модель не загружается в сервис — проверьте путь в config.toml. Должен быть абсолютный или относительно корня проекта.
  • Медленный инференс — включите квантование (--quantize int8) и настройте intra_op_threads = 4 в конфиге.
  • Out of memory — уменьшите batch_size в конфиге (например, с 64 до 32).
Рекомендации по выбору модели:
  • Для большинства задач — intfloat/e5-small-v2 + INT8 квантование. Баланс скорости и качества.
  • Для высокой точности — intfloat/e5-base-v2 + INT8. Требует больше RAM, но точнее.
  • Для мультиязычного поиска (русский, английский, немецкий) — BAAI/bge-m3 (готовая INT8 модель с Hugging Face).
  • Для слабых серверов (2-4 GB RAM) — только полнотекстовый поиск, эмбеддинги отключить.

Пример полной настройки config.toml для E5-small

[pro]
    enabled = true
    
    [pro.search.orchestrator]
    fs_enabled = true
    embedding_enabled = true
    strategy = "hybrid"
    tantivy_weight = 0.6
    embedding_weight = 0.4
    
    [pro.search.embedding]
    active_backend = "bge_m3"  # или "e5"
    vector_db_path = "./data/vectors.lance"
    query_cache_enabled = true
    # session_pool_size задается только в секции бэкенда (.bge_m3 / .e5)
    
    [pro.search.embedding.bge_m3]
    enabled = true
    model_path = "./models/bge-m3-int8/model.onnx"
    vector_dimension = 1024
    enable_sparse = false
    pooling = "mean"
    intra_op_threads = 4
    inter_op_threads = 1
    session_pool_size = 1

    # Для E5-small раскомментируйте и выключите bge_m3 выше:
    # [pro.search.embedding.e5]
    # enabled = true
    # model_path = "./models/e5-small-int8/model.onnx"
    # model_type = "e5-small"
    # vector_dimension = 384
    # pooling = "mean"
    # intra_op_threads = 4
    # inter_op_threads = 1
    # session_pool_size = 1

Конфигурация

Поставляется с настройками по умолчанию. Сервис идет с готовым файлом config.toml, в котором большинство параметров уже заполнены разумными значениями. Вы можете запустить сервис «из коробки» или изменить конфигурацию под свои задачи.
Как читать эту страницу: у каждого параметра указан тип. Параметры, отмеченные как обязательные, не имеют встроенного значения по умолчанию: без них сервис не запустится. В поставляемом service/config.toml все обязательные поля уже заполнены. Для остальных параметров, если в конфиге значение не задано, используется встроенное значение по умолчанию, указанное на этой странице.
PRO-режим определяется поставляемым исполняемым файлом. Free-версия не поддерживает секцию [pro]; PRO-версия включает ее принудительно. Значение pro.enabled в config.toml игнорируется.
Важно: Все настройки находятся в файле config.toml. Параметры без встроенного значения по умолчанию обязательны и должны быть указаны в конфиге.

[csv] — Параметры CSV-импорта

default_delimiter Option\ ","
Разделитель CSV по умолчанию, используемый командой sku-cli import при запуске с флагом --config config.toml, если не переопределен явно через --delimiter.
  • Обычно "," или ";"
  • Применяется только в CLI; сам сервис CSV не парсит

[server] — Сетевые параметры

bind String "0.0.0.0:8080"
Адрес и порт, на котором сервис слушает запросы.
  • Локальная разработка: "127.0.0.1:8080"
  • Production / Docker: "0.0.0.0:8080"
  • IPv6: "[::1]:8080"
request_timeout_ms u64 обязательный
Максимальное время обработки одного HTTP-запроса (мс), используемое для диагностики. При превышении половины этого значения в лог выводится предупреждение "Slow request". Полноценный HTTP-таймаут с возвратом 504 Gateway Timeout в текущей версии не реализован.
  • Пакетная загрузка (до 500 элементов): 3000-5000 мс
  • Интерактивный поиск: 1000-2000 мс
max_batch_size usize 500
Максимальное количество элементов в одном запросе `/add`, `/find`, `/delete`.
  • Рекомендуется: 100-300 для 1С, до 500 для фоновой синхронизации, 1 000-5 000 для массовой первичной загрузки при мощном CPU/SSD.
  • При превышении вернется 400 Bad Request.
  • Защита от перегрузки CPU и RAM.
  • Большой batch требует большего max_request_body_size_mb и соответствующего client_max_body_size в nginx. Подробные замеры и рекомендации — в разделе Производительность массовой загрузки.
max_request_body_size_mb usize 20
Максимальный размер HTTP-тела запроса (в мегабайтах).
  • Влияет на лимит тела, который накладывает Axum до разбора JSON
  • Должен быть достаточен для max_batch_size с самыми длинными полями (name, supplier, описания и т.п.)
  • При превышении вернется 413 Request Entity Too Large
  • nginx (или другой reverse proxy) должен иметь не меньший client_max_body_size
tokio_worker_threads Option\ автодетект по ядрам CPU
Число worker-потоков Tokio runtime. Задавайте только в окружениях с жесткими лимитами CPU; обычно лучше оставить `null`.
tokio_max_blocking_threads Option\ автодетект
Максимальное число blocking-потоков Tokio. Увеличьте, если /add с большими батчами сталкивается с "blocking pool full".

[site] — Режим работы "Только для сайта" (API Gateway / Frontend Proxy)

enabled bool false
Включить режим read-only "только для сайта". Мутационные эндпоинты отключаются только при комбинации enabled = true и readonly = true. Остаются только чтение: /find, /export, /health, /api/search-config и статика.
readonly bool false
Работает в паре с enabled. Когда enabled = true и readonly = true, все мутации отключены. При enabled = false значение readonly не влияет на доступность мутаций.
Важно: `/find` остается доступен, потому что это поисковый запрос с телом (HTTP POST). Все остальные POST/PUT/DELETE/PAТCH-блокируются.

[license] — Лицензирование PRO

Параметры лицензирования настраиваются при сборке PRO-версии и зависят от вашего лицензионного сервера. Детали конфигурации публичной документации не раскрываются.

[index] — Полнотекстовый индекс

path String обязательный
Путь к директории поискового индекса.
  • Создается автоматически при старте
  • Размещайте на быстром SSD (NVMe предпочтительно)
  • Не используйте NFS, SMB — резкое падение производительности
writer_ram_budget_mb usize обязательный
Объем RAM для буфера записи индекса (МБ).
  • < 100 000 записей: 50-100 МБ
  • 100 000-500 000: 150-250 МБ
  • > 500 000: 300-500 МБ
  • Больше = реже диск I/O, быстрее массовая загрузка
В PRO-сборке можно переопределить это значение через [pro.index].writer_ram_budget_mb. Если [pro.index].writer_ram_budget_mb = 0 (default), используется верхнеуровневый параметр [index].writer_ram_budget_mb.
fuzzy_max_distance u8 обязательный
Максимальное расстояние Левенштейна для нечеткого поиска. Задается явно в конфиге.
  • 0: только точные совпадения (макс. скорость)
  • 1: одна опечатка (рекомендуется для production)
  • 2: две опечатки (+10-15% recall, но в 1.5-2 раза медленнее)
Морфология и падежи. Сервис использует русский стеммер, но пост-фильтрация результатов опирается на fuzzy_max_distance. С типовым значением 1 поиск хорошо ловит отдельные опечатки («блот» → «болт»), но может пропускать разные падежные формы («болтом», «болты»), потому что после стемминга они дают один и тот же корень, а расстояние Левенштейна между исходными формами бывает равно 2.

Если в вашей предметной области важен поиск в разных падежах (например, пользователи вводят «стальным болтом» вместо «стальной болт»), установите fuzzy_max_distance = 2. В наших замерах скорость при этом остается в диапазоне 15–20 мс на 50 000 товаров.
Поисковый индекс не гарантирует стабильную работу при значениях \>2. Не используйте \>2 в production.
auto_recreate_on_schema_mismatch bool false
Автоматически пересоздавать индекс, если его схема не совпадает с ожидаемой (например, после обновления кода или изменения конфигурации).
  • false (по умолчанию): сервис не запустится и сообщит об ошибке SchemaError. Используйте, если потеря индекса недопустима.
  • true: старый индекс переименовывается в ./data/index.bak.<timestamp>, создается новый пустой индекс. Сервис продолжит работу, но данные нужно будет загрузить заново.
Включайте `true` только если у вас есть надежный источник данных для переиндексации (CSV, 1С, API). В противном случае сервис запустится с пустым индексом.

[search] — Алгоритмы предобработки и фильтрации

[search.field_weights] — Веса базовых полей

name f32 1.0
Вес совпадений по названию товара.
code f32 0.8
Вес совпадений по коду товара.
barcode f32 1.0
Вес совпадений по штрихкоду.
art f32 0.6
Вес совпадений по артикулу.
min_token_length usize обязательный
Минимальная длина токена для участия в поиске.
  • 1: включает однобуквенные коды ("М", "А", "Б"), но добавляет шум
  • 2: оптимально для номенклатуры (отсекает предлоги, частицы)
  • 3: максимальная скорость, если нет значимых коротких кодов
max_results usize обязательный
Количество лучших совпадений по умолчанию (если клиент не передал `limit`).
use_stopwords bool обязательный
Включить фильтрацию стоп-слов при индексации и поиске.
  • Критично для точности BM25 и скорости
  • Отсекает высокочастотные слова: "упак", "кор", "ящ", "комплект", "набор"
  • Рекомендуется: true
stopwords_path String обязательный
Путь к файлу стоп-слов (одно слово на строку, без кавычек).
  • Загружается при старте. Если не найден — используется встроенный набор
  • Добавляйте отраслевой шум: "упак", "кор", "ящ", "комплект", "набор"
auto_match_threshold f32 обязательный
Относительный порог "надежности" для автоматического сопоставления (0.0-1.0).
  • Финальный порог вычисляется как max(relative_threshold, max_score × auto_match_threshold), т.е. результат должен быть не хуже доли от максимального скора
  • Рекомендуется: 0.85-0.90 для баланса точности/полноты
  • Много ложных срабатываний → увеличьте до 0.92
  • Ничего не находится → снизьте до 0.75-0.80
use_unified_search bool обязательный
Включить унифицированный поиск по `name + code + barcode + art`.
  • true: оператор может ввести "болт м16 78454" в одно поле — найдется
  • false: поиск только по полю name (быстрее, но менее гибко)
  • Рекомендуется: true для интеграции с 1С/Excel
transliteration_enabled bool false
Исправление опечаток при вводе на неправильной раскладке клавиатуры.
  • "ghbdtn" → "привет"
  • Экспериментальная функция, может давать ложные срабатывания
  • Включайте, если пользователи часто забывают переключить раскладку
transliteration_threshold f32 0.4
Порог уверенности при транслитерации (встроенный default 0.4). Снизьте, если мало срабатывает, повысьте при ложных совпадениях.
relative_score_threshold f32 0.5
Относительный порог скоринга (0.0-1.0).

Результаты с score < (max_score × relative_score_threshold) отфильтровываются.

  • 0.50: мягкая фильтрация (default, оставляет 50% от лучшего)
  • 0.55: баланс точности/полноты (рекомендуется для production)
  • 0.60-0.70: агрессивно, только очень релевантные
Пример: Найдено 3 кандидата: score 2.0, 1.5, 0.8. При пороге 0.55: 2.0×0.55=1.1 → пройдут 2.0 и 1.5, отсеется 0.8.
Взаимодействие с `auto_match_threshold`: Итоговый порог равен max(relative_threshold, max_score × auto_match_threshold). Результат должен превышать оба значения.
enable_critical_token_filtering bool false
Пост-фильтрация по критичным токенам.

Критичные токены — короткие (≤ critical_token_max_length) ИЛИ содержащие цифры.

Захватывает: М16, 42р, XL, 220V, IP67, DIN125 и т.д.

  • Включите для "грязных" баз, где важно точное совпадение по коротким кодам
  • Отключите, если короткие коды встречаются в каждом товаре (теряете полноту)
Взаимодействие: Работает ТОЛЬКО если `enable_critical_token_filtering = true`. Буст через Aho-Corasick включается отдельно через `enable_aho_corasick`.
critical_token_max_length usize 3
Максимальная длина токена, чтобы считаться "критичным".
  • М16 (3 символа) — критичный при значении 4
  • DIN125 (5 символов) — НЕ критичный при значении 4
  • Увеличьте до 5, если у вас есть значимые 5-символьные коды (DIN125, ГОСТ7798)
min_token_match_ratio f32 обязательный
Минимальное покрытие токенами (0.0-1.0).

Доля токенов запроса, которые должны совпасть в результате.

  • 0.5: минимум 50% токенов должны совпасть (рекомендуется)
  • 0.7: строже, 70% токенов (меньше ложных срабатываний)
  • 0.0: отключить фильтрацию (старое поведение)
Пример: Запрос "болт м16 оцинкованный" (3 токена). При пороге 0.5 → минимум 2 токена должны совпасть. "Болт М16 черный" (совпали "болт", "м16") пройдет. "Гайка м16" (совпал только "м16") отсеется.
Взаимодействие: Применяется ДО `relative_score_threshold`. Товары, не набравшие нужное количество совпавших токенов, даже не попадают в ранжирование.
candidate_multiplier usize обязательный
Множитель количества кандидатов для поиска. `limit × candidate_multiplier` — сколько документов будет извлечено из индекса перед финальной сортировкой.
  • Больше → выше точность, но медленнее
  • Меньше → быстрее, но можно пропустить релевантные результаты
  • Рекомендуемый диапазон: 2-5
extended_search_enabled bool false
Включить расширенный поиск по динамическим полям (PRO-only).
  • true: можно искать по supplier, brand, color и т.д.
  • false: динамические поля игнорируются
Взаимодействие: Если `false`, все `[[search.extended_fields]]` игнорируются, даже если они описаны в конфиге.
require_name_for_fuzzy bool true
Ключевой параметр для коммерческого режима!
  • true: для нечеткого поиска обязательно наличие name (режим совместимости, default)
  • false: можно искать ТОЛЬКО по динамическим полям, без указания name (коммерческий режим)
Когда менять: Установите `false`, если хотите искать товары по поставщику, бренду или ТН ВЭД, даже не зная названия. Также включите `enable_attrs_indexing = true` и задайте `[[search.extended_fields]]` в PRO.
absolute_min_score f32 0.01
Абсолютный минимальный скор для фильтрации результатов.

Защита от ситуации, когда все результаты имеют очень низкий score и относительный порог отбрасывает все.

  • Рекомендуемое значение: 0.01-0.05
  • Не меняйте без крайней необходимости
max_boolean_clauses usize 500
Максимальное количество условий в одном поисковом запросе.

Защита от экспоненциального роста запросов при сложных поисках.

  • Рекомендуемое: 300-1000
  • 500 — баланс безопасности и точности
  • При превышении — лишние клаузулы отбрасываются с предупреждением
allow_empty_name bool false
PRO-only. Режим "Справочник/Ключи".
  • Если true и name пустой — позволяет добавить/найти товар без названия, отключая стемминг/эмбеддинги для пустого имени.
  • Документ индексируется только по ID, code, art и т.п.
  • Рекомендация: true для справочников, где название неизвестно при загрузке; false для стандартного каталога.
enable_attrs_indexing bool false
PRO-only. Индексировать ли JSON-атрибуты 1С (attrs) для быстрых фильтров.
  • Включает точный поиск по реквизитам. Требует больше места на диске.
extra_filter_mode string "hard"
Как обрабатывать произвольные поля extra при поиске. Работает только при enable_attrs_indexing = true.
  • "hard" — точный фильтр: в результаты попадают только товары, у которых ВСЕ указанные в запросе extra-поля совпадают точно. Это поведение по умолчанию.
  • "soft" — бустирование: совпадение по extra увеличивает скор товара, но не отсекает товары без совпадения. Регулируется весом search.weights.extra_match_boost.
  • "off" — игнорировать extra при поиске: поля возвращаются в ответе, но не влияют на выборку и ранжирование.
При переходе между режимами переиндексация не требуется: логика применяется на этапе пост-обработки результатов.
enable_deep_1c_parsing bool false
Глубокий парсинг 1С-ссылок (ref, GUID, URI).
  • Автоматически извлекает ID из строк вида "e1cib/data/...?ref=..." или "{GUID} Название".
  • Рекомендация: включайте, если в name часто приходят 1С-ссылки; иначе оставьте false для экономии CPU.
case_sensitive_fields array [\["id"\]]{.param-default}
Поля, точный поиск по которым будет РЕГИСТРОЗАВИСИМЫМ (учет больших/маленьких букв).
  • Обычно используется для артикулов, штрихкодов, внутренних кодов.
  • Влияет только на прямой поиск по полю: query.code, query.art, query.barcode. Например, при case_sensitive_fields = ["code"] запрос code="ABC-123" найдет только ABC-123, а code="abc-123" — нет.
  • Поиск через общее поле name (unified search) остается регистронезависимым: запрос name="abc-123" найдет товар с code="ABC-123".
  • Чтобы регистрозависимость exact-поиска заработала для уже загруженных данных, выполните переиндексацию после изменения этого параметра.
case_sensitive_extra_fields array [\[\]]{.param-default}
PRO-only. Дополнительные поля для регистрозависимого exact-поиска.
  • Поиск по каким дополнительным полям будет РЕГИСТРОЗАВИСИМЫМ при прямом указании в запросе.
  • Как и case_sensitive_fields, не влияет на поиск через общее поле name.
merge_min_segments usize 8
Минимальное число сегментов поискового индекса для автоматического слияния. Меньшее значение дает более компактный индекс, но чаще тратит CPU на слияние.
merge_max_size_bytes u64 100000000 (100 МБ)
Максимальный размер сегмента (в байтах), который еще участвует в merge. `0` = ограничения нет.
search_timeout_ms u64 5000
Не реализовано. Парсится, но на поиск не влияет. Ошибка 408 Request Timeout не возвращается.
index_timeout_ms u64 30000
Не реализовано. Парсится, но на операции записи в индекс не влияет.
embedding_timeout_ms u64 10000
Не реализовано. Парсится, но на работу эмбеддингов не влияет.

[search.weights] — Веса разных типов совпадений

Для чего это нужно: Сервис может найти товар разными способами: точное совпадение, нечеткое (опечатки), совпадение в "сыром" поле. Эти веса говорят сервису, какой способ важнее.
exact_match_weight f32 1.0
Вес точных совпадений. Обычно не меняется — точные совпадения самые надежные.
  • Увеличить (1.2-1.5) → точные совпадения выше в результатах
  • Уменьшить (0.8-0.9) → точные совпадения менее важны
fuzzy_match_weight f32 0.7
Вес нечетких совпадений (с опечатками).
  • Много опечаток → увеличьте до 0.8-0.85
  • Нечеткий поиск часто ошибается → уменьшите до 0.5-0.6
Взаимодействие с `fuzzy_max_distance`: Если `fuzzy_max_distance = 0`, этот вес не применяется (нечеткий поиск отключен).
raw_match_weight f32 0.8
Вес совпадений в "сыром" поле (без приведения к основе).

Пример: запрос "болты" ищет "болты" (точное совпадение), а не "болт" (основа).

Обычно не требует изменения. Стоит между точным (1.0) и нечетким (0.7).
coverage_boost_per_token f32 0.2
Бонус за каждый совпавший токен.

Формула: score × (1.0 + matched_tokens × coverage_boost_per_token)

  • Запрос "болт м16 оцинкованный" (3 слова). Товар содержит все 3 → бонус 3×0.2=+60%
  • 0.1 — слабый бонус, 0.2 — баланс, 0.3 — сильный
Взаимодействие: Применяется ДО `relative_score_threshold`. Высокий бонус может "протащить" товар с низким исходным скором через порог отсечения.
prefix_boost f32 1.15
Бонус, если название товара начинается с токена запроса.
  • Запрос "болт" → "Болт м16" получает бонус
  • "Гайка болт" — не получает (начинается с "гайка")
  • 1.0 — без бонуса, 1.15 — +15%, 1.3 — +30%
type_weight f32 1.0
Множитель к скору при точном совпадении по полю type.
desc_weight f32 0.85
Дополнительные веса для совпадения по полям
  • Поле desc (точное совпадение)
  • Точное совпадение по полю desc получает дополнительный бонус
ref_weight f32 1.0
Множитель к скору при точном совпадении по полю ref.
attrs_exact_weight f32 1.0
Множитель к скору при точном совпадении по JSON-атрибутам (динамическим extra-полям), найденным через search_by_attrs.
attrs_flat_weight f32 0.7
Вес текстового буста через плоские атрибуты
  • Текстовый буст через плоские атрибуты
extra_match_boost f32 0.3
Множитель буста при совпадении по произвольным полям extra в режиме extra_filter_mode = "soft".
  • 0.0 — совпадение по extra не влияет на скор.
  • 0.3-0.5 — мягкое приоритезирование: подходящие товары поднимаются выше, но не вытесняют релевантные по названию.
  • 1.0-2.0 — сильный буст: товары с совпавшими extra могут оказаться на первых местах даже при менее точном совпадении по name.
Этот вес используется только в режиме "soft". В режиме "hard" совпадение по extra является обязательным фильтром, а в режиме "off" поля игнорируются при поиске.

\[search.fuzzy\] — Тонкая настройка нечеткого поиска

short_query_fuzzy_distance u8 3
Расстояние Левенштейна для однотокенных коротких запросов. Применяется только к запросам из одного токена; эффективное расстояние ограничено fuzzy_max_distance + 1.
  • 1: минимальное расширение
  • 2: стандартное (рекомендуется)
  • 3: максимальное (может давать ложные срабатывания)
Побочный эффект: при стандартных значениях fuzzy_max_distance = 1 эффективное расстояние будет 2, т.е. пример "перф" → "перфоратор" (расстояние 5) недостижим. Если много ложных срабатываний — уменьшите до 2.
short_token_max_len usize 4
Максимальная длина токена, считающегося "коротким".
  • 3: "пер" (короткий), "перф" (обычный)
  • 4: "перф" (короткий), "перфо" (обычный)
  • 5: "перфо" (короткий)
Взаимодействие: Токены короче этого порога получают увеличенное расстояние Левенштейна (`short_query_fuzzy_distance`). Длинные токены используют стандартное `fuzzy_max_distance`.
phrase_min_len usize 2
Не реализовано. Порог phrase search захардкожен: точная фраза ищется при наличии двух и более токенов. Значение парсится, но не влияет на поведение.
phrase_boost f32 4.0
Множитель скора для точных фразовых совпадений.
  • 1.0: без буста
  • 2.0: умеренный буст
  • 4.0: сильный буст (рекомендуется)
  • 8.0: экстремальный (только для точных совпадений)
Слишком высокий буст может "задавить" другие релевантные результаты. Рекомендуется 3.0-5.0.

[search.strategy] — Как сервис ищет: быстро или точно

Для чего это нужно: Последовательный поиск — точнее, булевый — быстрее. Эти настройки говорят сервису, когда использовать какой способ.
auto_select_strategy bool true
Автоматически выбирать стратегию поиска.
  • true: короткие запросы (1-3 слова) → последовательный (точнее), длинные (4+) → булевый (быстрее)
  • false: всегда последовательный (максимальная точность)
Рекомендуется `true` для production.
sequential_max_tokens usize 3
Максимальное количество токенов для последовательного поиска.
  • 2: быстрый переход на булевый (меньше точности)
  • 3: сбалансированный (рекомендуется)
  • 5: максимальная точность для длинных запросов (медленнее)

[search.edge_ngram] — Префиксный поиск для коротких запросов

Для чего это нужно: Пользователь ищет "перф", а в базе есть "Перфоратор". Обычный поиск не находит, потому что "перф" сильно короче. N-gram разбивает слова на части и находит даже по короткому началу.
enabled bool true
Включить Edge N-gram поиск.
Важно: При включении размер индекса увеличивается на 30-40%.
min_gram usize 3
Минимальная длина префикса.
  • 2: найдет даже по 2 буквам ("пе" → "перфоратор"), но больше шума
  • 3: оптимально для русского языка (рекомендуется)
  • 4: только по 4 буквам ("перф" → "перфоратор")
max_gram usize 15
Максимальная длина префикса.
  • 5-6: оптимально (баланс)
  • 8-10: точнее, но индекс больше
  • 15: индекс в 2 раза больше
min_query_len usize 2
Запросы короче этого числа ищутся обычным способом. Защита от мусора по 1 букве.
max_query_len usize 6
Только запросы короче этого числа идут через N-gram. Длинные запросы хорошо ищутся обычным поиском.
boost f32 1.5
Буст для результатов, найденных через N-gram. `1.2` — небольшое преимущество, `1.5` — баланс, `2.0` — сильное.
prefix_boost f32 1.2
Дополнительный буст для слов, которые начинаются с искомого токена. "перф" → "Перфоратор" получает буст, "Электроперфоратор" — нет.

[search.extended_fields] — Динамические поля

Для чего это нужно: Разные товары могут иметь разные наборы полей. Обувь — цвет, размер. Холодильники — диагональ, бренд. Вы описываете ВСЕ поля один раз, сервис индексирует только те, которые есть у конкретного товара.
field String
Имя поля (как будет называться в JSON).
weight f32 обязательный
Важность поля при ранжировании. Используется как множитель при точном совпадении по динамическому полю.
  • Для кодов (tnved, артикулы): 0.8-1.0
  • Для названий (бренд, поставщик): 0.5-0.7
  • Для характеристик (цвет, размер): 0.3-0.5
stemmed bool true
Приводить ли слова к основе (стемминг).
  • true: "поставщики" найдет "поставщик"
  • false: только точное совпадение (для кодов и артикулов)
fuzzy bool true
Разрешить нечеткий поиск (опечатки) для этого поля.
  • true: "Ромашка" найдет "Ромашкаа"
  • false: только точное совпадение

[pro] — Профессиональные фичи (Enterprise)

ВСЕ настройки в этой секции работают ТОЛЬКО в PRO-версии. PRO-режим определяется поставляемым исполняемым файлом; параметр pro.enabled в конфиге игнорируется.
enabled bool false
Показывает, активен ли Pro-режим. Значение определяется поставляемым исполняемым файлом и не может быть изменено через конфиг.
cache_enabled bool false
Кэширование результатов поиска. Повторные одинаковые запросы возвращаются мгновенно. Включайте при высокой нагрузке и стабильном запросном потоке.
cache_size usize 1000
Максимум записей в кэше (LRU).
cache_ttl_seconds u64 300
Время жизни кэша (секунды). 300 = 5 минут.
analytics_enabled bool false
Включить аналитику популярных запросов. Данные доступны через `GET /admin/analytics/stats` (PRO).
analytics_max_queries usize 1000
Максимальное количество уникальных запросов в аналитике.
adaptive_pool_enabled bool false
Включить адаптивный пул потоков для ONNX Runtime. Если `false`, используется фиксированный пул на `pool_min_threads`..`pool_max_threads` потоков.
pool_min_threads usize 2
Минимальное число потоков в пуле для ONNX/эмбеддингов.
pool_max_threads usize 16
Максимальное число потоков в пуле. Не устанавливайте значительно выше числа физических ядер.

[pro.index] — Продвинутые настройки индексации (PRO)

writer_ram_budget_mb usize 0
Бюджет RAM для писателя индекса в PRO-режиме (МБ). Если 0 — используется значение из [index].writer_ram_budget_mb. Положительное значение переопределяет верхнеуровневый параметр только в PRO-сборке. При возникновении OOM на индексации установите 0, чтобы вернуться к общему значению.
commit_mode, commit_docs_threshold и commit_timeout_secs пока не реализованы; коммиты идут по стандартной политике поискового индекса.

[pro.cross_codes] — Кросс-коды и аналоги

enabled bool false
Включить поддержку кросс-кодов (группировка аналогов). Требует переиндексации при изменении.
max_group_size usize 30
Максимальное количество аналогов в ответе из одной группы.
  • 10: для быстрых ответов
  • 30: стандарт (рекомендуется)
  • 50-100: для автозапчастей (много аналогов)
include_in_response bool true
Возвращать ли поле `cross_codes` в ответе поиска.
sort_within_group bool true
Сортировать аналоги внутри группы по score.
include_original_in_cross_list bool false
Включать ли исходный документ в список кросс-кодов в ответе.

[pro.search.embedding] — Семантический поиск (векторные эмбеддинги)

Для чего это нужно: Эмбеддинги позволяют искать товары по смыслу, а не по точному совпадению слов. "шуруповерт" найдет "дрель-шуруповерт" и "винтоверт". Требует ONNX модель и достаточно RAM.
active_backend string "bge_m3"
Активный бэкенд эмбеддингов. Возможные значения: `"bge_m3"`, `"e5"`. Бэкенд `"gemini"` не реализован. По умолчанию `"bge_m3"`.
vector_db_path string "./data/vectors.lance"
Путь к векторному хранилищу.
query_cache_enabled bool true
Кэшировать векторы запросов (снижает latency при повторяющихся фразах).

[pro.search.embedding] — Общие настройки эмбеддингов

active_backend — активный бэкенд: "bge_m3" или "e5" (default: "bge_m3"). "gemini" не реализован.
vector_db_path — путь к векторной БД, например "./data/vectors.lance"
vector_store_type — тип хранилища: "lancedb" (default: "lancedb"). Параметр vector_db_type — устаревший синоним, игнорируется.
vector_index_type — тип векторного индекса (default: "auto"). Если не уверены — оставьте "auto".
Доступные значения:
ЗначениеКогда использовать
"auto"По умолчанию. Хранилище само выберет индекс.
"ivf_pq"Большой каталог и ограниченная память.
"ivf_hnsw_sq"Нужна высокая скорость поиска, ресурсов достаточно.
"ivf_hnsw_pq"Компромисс между скоростью и размером индекса.
"ivf_hnsw_flat"Максимальная точность при поиске, но больше места на диске.
"hnsw"Для Qdrant — обычный HNSW-граф. Для встроенного векторного хранилища работает как "ivf_hnsw_sq".

При смене значения старый индекс не перестраивается автоматически. Чтобы применить новый тип, выполните sku-cli embedding clear и снова проиндексируйте товары.

weight_tantivy / weight_embedding — веса полнотекстового поиска и эмбеддингов при гибридном поиске (default: 0.6 / 0.4)
batch_size — размер батча для индексации (default: 64; для bge-m3 рекомендуется 64-100, для e5-small — до 500)
search_limit_multiplier — во сколько раз расширять выборку кандидатов у векторного поиска перед merge (default: 2; рекомендуется 2-5)
indexing_parallelism — параллелизм при индексации векторов (default: 4)
query_cache_enabled — кэшировать векторы запросов (default: true). Параметры query_cache_size и query_cache_ttl_seconds не реализованы; размер и TTL общего кэша задаются через cache_max_size / cache_ttl_seconds.
cache_max_size / cache_ttl_seconds — размер и TTL общего кэша эмбеддингов (default: 1000 / 300)
vector_quantization — квантование векторов (валидация): "f32", "f16", "i8" (default: "f16"). В текущей версии векторное хранилище всегда хранит векторы как Float32; само значение к хранению не применяется.
enable_sparse / sparse_threshold / sparse_rerank_weight — экспериментальная sparse-ветка bge-m3 (default: false / 0.01 / 0.0). В hybrid-режиме прироста точности не дает, по умолчанию выключена.
max_concurrent_inferences — максимальное число параллельных ONNX-сессий (default: 4; для bge-m3 в бенчмарках использовалось 8)
filter_technical_codes — фильтровать артикулы/штрихкоды из текста для эмбеддинга (default: false)
embedding_fuzzy_fallback_enabled — включать fuzzy-fallback для эмбеддингов (default: true). Параметр fs_fuzzy_fallback_enabled не реализован.
lancedb_retry_* / qdrant_retry_* — настройки retry при записи в vector store (default: 3 попытки, задержка 100-2000 мс)
include_fields / exclude_fields — явный белый/черный список полей, включаемых в эмбеддинг
include_extended_fields — включать ли динамические поля в эмбеддинг (default: true)
enable_aho_corasick — использовать Aho-Corasick для быстрой фильтрации технических кодов (default: true). В Free-сборке всегда выключен.
intra_op_threads / inter_op_threads — не реализовано на верхнем уровне. Потоки ONNX Runtime задаются в секции активного бэкенда (bge_m3 или e5).

[pro.search.embedding.bge_m3] — Бэкенд BGE-M3

enabled — включить режим (default: false). Если active_backend = "bge_m3", должен быть true, иначе сервис не запустится.
model_path — путь к ONNX модели, например "./models/bge-m3-int8/model.onnx" (обязательный)
vector_dimension — размерность вектора: 384 / 768 / 1024 (обязательный)
enable_sparse / sparse_threshold / pooling — sparse-ветка и pooling (default: false / 0.01 / ""). Пустая строка в pooling молча преобразуется в "mean".
Pooling — способ получения одного вектора предложения из выхода трансформера:
  • "mean": усреднение векторов всех токенов. Самый стабильный вариант, рекомендуется для E5 и bge-m3.
  • "cls": вектор специального [CLS]-токена. Иногда чуть лучше для классификации, но может быть нестабилен для поиска.
  • "last": вектор последнего токена. Редко используется; может подойти для декодер-подобных моделей.
intra_op_threads / inter_op_threads — потоки ONNX Runtime (default: 0 / 0). Значение 0 означает автовыбор эвристикой ONNX Runtime.
session_pool_size — пул ONNX-сессий (default: 1; увеличьте до 2–4 при высоком concurrency)

[pro.search.embedding.e5] — Бэкенд E5

enabled — включить режим (default: false)
model_path — путь к ONNX модели, например "./models/e5-small-int8/model.onnx" (обязательный)
model_type — "e5-small", "e5-base", "e5-large" (default: "e5-small")
vector_dimension — размерность вектора: 384 / 768 / 1024 (обязательный)
pooling — "mean", "cls", "last" (default: "", молча преобразуется в "mean")
intra_op_threads / inter_op_threads — потоки ONNX Runtime (default: 0 / 0)
session_pool_size — пул ONNX-сессий (default: 1)

[pro.search.embedding.gemini] — Бэкенд Google Gemini

Не реализовано. Бэкенд Gemini отключен в коде. При active_backend = "gemini" сервис молча переключается на bge_m3. Секция сохранена для обратной совместимости, но не влияет на поведение.

[pro.search.embedding.hnsw] — Настройки HNSW индекса

m — количество соседей в HNSW-графе (default: 16). Используется для Qdrant и встроенного векторного хранилища с IVF-HNSW (num_edges).
ef_construction — размер кандидатов при построении графа (default: 200).
ef_search — размер кандидатов на поиске (default: 100). Пока применяется только для Qdrant; встроенное векторное хранилище управляется автоматически.

[pro.search.embedding.lancedb] — Встроенное векторное хранилище

path — путь к векторному хранилищу (default: "./data/vectors.lance").
collection_name — имя таблицы внутри векторного хранилища (default: "embeddings").
quantization — квантование: "f32", "f16", "i8" (default: "f16").

[pro.search.embedding.qdrant] — Qdrant

url / api_key / collection — адрес, ключ и имя коллекции Qdrant
timeout_seconds — таймаут Qdrant (default: 30). Параметр use_grpc не реализован.

[pro.search.embedding.text_config] — Состав текста для эмбеддинга

include_name / name_weight — включать ли name и его вес (default: true / 2)
include_code / include_barcode / include_art — включать ли code, barcode, art (default: false / false / true)
include_extended_fields / include_extended_field_names / exclude_extended_field_names — какие динамические поля включать/исключать
separator — разделитель между частями супер-текста (default: " ")
filter_technical_codes / apply_stopwords — фильтровать технические коды и стоп-слова (default: true / false)
# Пример конфигурации PRO с BGE-M3
[pro.search.embedding]
active_backend = "bge_m3"
vector_db_path = "./data/vectors.lance"
query_cache_enabled = true

[pro.search.embedding.bge_m3]
enabled = true
model_path = "./models/bge-m3-int8/model.onnx"
vector_dimension = 1024
enable_sparse = false
pooling = ""
intra_op_threads = 0
inter_op_threads = 0
session_pool_size = 1

# Пример переключения на E5-small
# [pro.search.embedding.e5]
# enabled = true
# model_path = "./models/e5-small-int8/model.onnx"
# model_type = "e5-small"
# vector_dimension = 384
# pooling = ""
# intra_op_threads = 0
# inter_op_threads = 0
# session_pool_size = 1
Legacy-режим (active_mode = "legacy") удален. Используйте active_backend и секции [pro.search.embedding.bge_m3] или [pro.search.embedding.e5]. Бэкенд Gemini не реализован.

[pro.search.orchestrator] — Оркестратор поиска

fs_enabled bool true
Включить полнотекстовый поиск.
embedding_enabled bool false
Включить семантический поиск (эмбеддинги). Требует валидной ONNX-модели и инициализированного vector DB.
strategy String "fs_only"
Стратегия поиска по умолчанию.
  • "fs_only": только полнотекстовый поиск (быстро, точно, default)
  • "embedding_only": только эмбеддинги (семантический)
  • "hybrid": последовательный гибрид
  • "parallel": параллельный гибрид (максимальная скорость)
Начинайте с `fs_only`; переходите на `hybrid` после проверки качества эмбеддингов.
tantivy_weight / embedding_weight f32 0.6 / 0.4
Веса для гибридного поиска. `score = tantivy_weight × fs_score + embedding_weight × emb_score`.
  • Преимущество полнотекстового поиска: 0.8 / 0.2
  • Сбалансированный (по умолчанию): 0.6 / 0.4
  • Преимущество эмбеддингов: 0.4 / 0.6
score_normalization String "robust"
Метод нормализации скоров перед объединением полнотекстового поиска и эмбеддингов.
  • "min_max": масштабирование на [0,1] по максимуму.
  • "robust": нормализация на 95-й перцентиль, менее чувствительна к выбросам (default).
  • "z_score": центрирование на среднем и деление на std, затем масштабирование z-значений на [0,1].
  • "softmax": экспоненциальное преобразование, сумма скоров после нормализации = 1.
  • "none": не нормализовать, использовать сырые скоры источника.

Режим влияет только на гибридный/параллельный поиск в PRO, когда скоры полнотекстового поиска и эмбеддингов объединяются.

parallel_batch_threshold usize 8
Минимальный размер batch-запроса, при котором оркестратор обрабатывает запросы параллельно. При batch \< threshold работа идет последовательно.

[pro.background] — Фоновая очередь эмбеддингов

Для чего это нужно: Фоновая индексация эмбеддингов ускоряет ответ API в 10-50 раз. Синхронный режим удален; фоновая очередь всегда включена в PRO.
db_path String "./data/embedding_queue.db"
Путь к SQLite базе для очереди.
worker_count usize 2
Количество фоновых воркеров (рекомендуется 2).
batch_size usize 100
Размер батча для чтения задач из очереди перед отправкой в ONNX-модель.
flush_interval_ms u64 5000
Интервал сброса накопленных задач из очереди в worker\'ы (мс).
max_retries u8 3
Сколько раз повторно пытаться выполнить задачу перед перемещением в failed.
lance_write_buffer_size usize 1000
Сколько векторов накапливать перед одной записью в векторное хранилище. Увеличение снижает число версий хранилища, но при медленном потоке worker будет ждать таймаута.
lance_write_flush_timeout_ms u64 5000
Максимальное время хранения векторов в памяти перед принудительным сбросом.
worker_monitor_interval_secs u64 5
Интервал проверки состояния фоновых worker\'ов (сек). 0 = отключить мониторинг.
worker_max_restarts_per_minute usize 10
Максимальное количество автоперезапусков одного worker\'а в минуту. 0 = безлимит.
cleanup_interval_secs u64 3600
Интервал очистки завершенных записей (сек). По умолчанию раз в час.
retention_completed_secs i64 86400
Сколько секунд хранить завершенные задачи. 0 = удалять сразу после обработки. По умолчанию 24 часа.

[pro.plugins.hierarchy] — Иерархические группы (PRO)

enabled bool true
Включить API иерархических деревьев (POST /hierarchy).
default_max_depth usize 5
Максимальная глубина дерева, используемая в API, если не указана явно.
default_limit usize 1000
Лимит узлов в ответе по умолчанию.
include_cross_codes_by_default bool false
Включать кросс-коды в ответы иерархии по умолчанию.

[security] — Безопасность и лимиты

api_keyStringобязательный
API-ключ для аутентификации. Пусто = отключено. Все запросы должны содержать `X-API-Key: значение`.
rate_limit_per_minuteusizeобязательный
Среднее число запросов с одного IP в минуту. 0 = отключено.
rate_limit_burstusizeобязательный
Максимальный burst-размер. 0 = отключено.
rate_limit_window_secsu6460
Окно rate limiter (сек).

[admin] — Администрирование

backup_dirStringобязательный
Каталог для бэкапов индекса.
max_backupsusizeобязательный
Сколько последних бэкапов хранить. `0` — не удалять старые.
optimize_on_startupboolобязательный
Оптимизировать индекс при старте. Может замедлить запуск на больших индексах.

[feedback] — Обратная связь

enabledboolобязательный
Включить сбор обратной связи через `/feedback`.
log_pathStringобязательный
Путь к файлу с записями feedback.
learn_min_token_lenusize3
Минимальная длина токена, который попадает в обучение синонимов.
learn_max_suggestionsusize20
Максимальное число рекомендаций за один вызов `/admin/learn`.
Формат POST /feedback
Endpoint принимает обязательные поля:
  • query_name — исходный запрос пользователя.
  • matched_id_out — ID найденного товара.
  • action — accept, reject или zero_result.

И опциональные поля для улучшенного анализа:

  • product_name, product_art, product_barcode — данные найденного товара.
  • matched_by — поле, по которому произошло совпадение (name, code, art, barcode, attrs).
  • position — позиция товара в выдаче.
  • session_id — ID сессии для анализа реформулировок.
  • expected_id — ID товара, который ожидал увидеть пользователь.

Пример:

{
  "query_name": "болт м10",
  "matched_id_out": "ID-001",
  "action": "accept",
  "product_name": "Болт М10х50 DIN 933",
  "matched_by": "name",
  "position": 1
}
GET /admin/learn — рекомендации
PRO-only. Анализирует `feedback.log` и возвращает массив рекомендаций. Поддерживает фильтр по типу:
curl 'http://localhost:8080/admin/learn?type=synonym'

Типы рекомендаций:

  • synonym — предлагаемые синонимы на основе accept-записей.
  • missing_product — популярные запросы без результатов (zero_result).
  • check_product — товары с высоким reject rate.
  • typo — возможные опечатки и альтернативные написания.

Каждая рекомендация содержит recommendation_type, title, description, confidence (0.0–1.0) и data.

[export] — Экспорт данных

max_rowsusizeобязательный
Максимальное число строк за один запрос экспорта.
default_limitusizeобязательный
Лимит строк по умолчанию, если не задан явно.

[logging] — Логирование

levelStringобязательный
Уровень логирования: `error`, `warn`, `info`, `debug`, `trace`.
console_enabledboolобязательный
Выводить логи в stdout/stderr.
console_formatStringобязательный
Формат консольных логов: `pretty` или `json`. Значение `compact` молча заменяется на `pretty`.
file_enabledboolобязательный
Писать логи в файл.
file_directoryStringобязательный
Каталог для файлов логов.
file_prefixStringобязательный
Префикс имени файла лога.
file_rotate_max_size_mbusizeобязательный
Максимальный размер файла лога в мегабайтах. Если file_rotate_max_size_mb > 0, ротация выполняется по размеру (имеет приоритет над file_rotate_by_age). Если 0 — ротация только по времени.
file_rotate_by_ageStringобязательный
Период ротации, если ротация по размеру отключена (file_rotate_max_size_mb = 0): hourly, daily, weekly.
file_keep_last_nusizeобязательный
Сколько последних ротированных файлов лога оставлять. При 0 старые файлы не удаляются.
file_json_formatboolобязательный
Писать файловые логи в JSON.

[files] — Внешние словари

synonyms_pathStringобязательный
Путь к JSON-файлу синонимов. Файл должен существовать, если используется обучение.

Обучение на обратной связи

Сервис умеет учиться на ваших действиях. Не ML, а просто и понятно: вы отправляете обратную связь, сервис анализирует ее и предлагает улучшения.

Доступно только в PRO версии. Эндпоинты /admin/learn, /feedback и управление синонимами через API работают только в PRO-сборке. В Free-редакции /admin/learn вернет 404.
Текущий алгоритм анализирует действия accept и zero_result. Записи с reject сохраняются, но пока не используются для построения синонимов. При accept сервис учит, что искали одно, а выбрали другое, и предлагает синонимы; при zero_result — частые запросы, по которым ничего не нашлось.

Как это работает — по шагам

1
Оператор находит нужный товар или не находит ничего
В 1С после поиска сервис показал товары. Оператор либо выбирает подходящий и подтверждает (accept), либо сообщает, что ничего не нашлось (zero_result).
2
1С отправляет feedback в сервис
1С формирует запрос: что искали, какой товар выбрали, и действие оператора.
3
Сервис сохраняет этот случай в feedback.log
Каждая запись выглядит так:
{"timestamp":"2024-01-15T10:30:00Z","query_name":"болт м16 цинк","matched_1c_id_out":"1C-GUID-0001","product_name":"Болт М16х60 оцинкованный ГОСТ 7798-70","action":"accept"}
4
Администратор запускает анализ
Раз в неделю (или чаще) администратор выполняет команду:
# Через CLI:
sku-cli learn

# Или через API:
curl http://localhost:8080/admin/learn

Доступно только в PRO.

5
Сервис анализирует accept-записи и предлагает улучшения
Сервис ищет закономерности: если оператор искал слово "цинк", а в выбранном товаре встречается "оцинкованный", значит, это хорошая пара для синонима. Анализ строится по полю product_name, поэтому без него предложения не генерируются.
6
Администратор проверяет предложения
Сервис возвращает JSON с рекомендациями:
{
  "recommendations": [
    {
      "recommendation_type": "synonym",
      "title": "Синоним: цинк → оцинкованный",
      "description": "В 82% accept-запросов со словом 'цинк' выбран товар, содержащий 'оцинкованный'",
      "confidence": 0.82,
      "data": {
        "query_token": "цинк",
        "product_token": "оцинкованный"
      }
    },
    {
      "recommendation_type": "missing_product",
      "title": "Возможно отсутствующий товар: болт м16 din933",
      "description": "15 zero_result запросов за период",
      "confidence": 0.95,
      "data": {
        "query": "болт м16 din933",
        "count": 15
      }
    },
    {
      "recommendation_type": "check_product",
      "title": "Проверьте товар 1C-GUID-0001",
      "description": "Высокий reject rate (45%)",
      "confidence": 0.45,
      "data": {
        "id": "1C-GUID-0001",
        "reason": "Высокий reject rate (45%)"
      }
    },
    {
      "recommendation_type": "typo",
      "title": "Опечатка: перфоротар → перфоратор",
      "description": "Похоже на опечатку, найден канонический вариант",
      "confidence": 0.78,
      "data": {
        "query": "перфоротар",
        "canonical": "перфоратор"
      }
    }
  ],
  "processed_entries": 42
}
7
Администратор добавляет синонимы
Если предложение хорошее — добавляем его вручную через API:
# Через CLI:
sku-cli synonyms add --word "цинк" --expansions "оцинкованный,zn,zinc"

# Или через API:
curl -X POST http://localhost:8080/synonyms \
  -H "Content-Type: application/json" \
  -d '{"items":[{"word":"цинк","expansions":["оцинкованный","zn","zinc"]}]}'
8
Синоним начинает работать мгновенно
С этого момента все новые поиски "цинк" будут находить "оцинкованный". Перезапуск сервиса не нужен.

Как настроить 1С для отправки feedback

В 1С, после того как сервис вернул результаты поиска, добавьте кнопки "Выбрать этот товар" (accept) и "Ничего не нашлось" (zero_result). При нажатии вызывайте:

// Псевдокод 1С
Процедура ВыбранТовар(НайденныйТовар, ПоисковыйЗапрос)
    Данные = Новый Структура;
    Данные.Вставить("query_name", ПоисковыйЗапрос.Наименование);
    Данные.Вставить("matched_id_out", НайденныйТовар.Идентификатор);
    Данные.Вставить("product_name", НайденныйТовар.Наименование);
    Данные.Вставить("action", "accept");  // "accept" — выбрали этот товар

    // Отправка POST /feedback
    Ответ = ОтправитьHTTPЗапрос("/feedback", Данные);
КонецПроцедуры

Процедура НичегоНеНашлось(ПоисковыйЗапрос)
    Данные = Новый Структура;
    Данные.Вставить("query_name", ПоисковыйЗапрос.Наименование);
    Данные.Вставить("action", "zero_result");  // ничего не нашлось

    Ответ = ОтправитьHTTPЗапрос("/feedback", Данные);
КонецПроцедуры

Автоматизация обучения

Можно настроить cron для регулярного анализа:

# Запускать каждую неделю в понедельник в 9 утра
0 9 * * 1 cd /opt/sku-search && ./sku-cli learn >> /var/log/learn.log 2>&1

Полный цикл обучения на примере

  1. Оператор ищет "болт м16 цинк", сервис предлагает "Болт М16 черный"
  2. Оператор выбирает "Болт М16х60 оцинкованный ГОСТ 7798-70" и нажимает "Это тот товар", 1С отправляет feedback с action: "accept"
  3. В лог попадает запись: искали "цинк", выбрали товар с "оцинкованный"
  4. Администратор через неделю запускает sku-cli learn
  5. Сервис анализирует: в нескольких accept-запросах со словом "цинк" выбран товар, содержащий "оцинкованный"
  6. Сервис предлагает синоним: "цинк" → "оцинкованный"
  7. Администратор добавляет синоним через API
  8. Теперь "болт м16 цинк" находит "Болт М16х60 оцинкованный" с высоким score

Что происходит в feedback.log

Файл data/feedback.log накапливает все действия операторов. Пример содержимого:

{"timestamp":"2024-01-15T10:30:00.123Z","query_name":"болт м16 цинк","matched_1c_id_out":"1C-GUID-0001","product_name":"Болт М16х60 оцинкованный ГОСТ 7798-70","action":"accept"}
{"timestamp":"2024-01-15T10:32:15.456Z","query_name":"гайка нерж м16","matched_1c_id_out":"1C-GUID-0002","product_name":"Гайка М16 нержавеющая","action":"accept"}
{"timestamp":"2024-01-15T10:35:22.789Z","query_name":"шайба м8","matched_1c_id_out":"1C-GUID-0003","action":"zero_result"}

Вы можете анализировать его вручную:

# Сколько раз отклонили
grep '"action":"reject"' data/feedback.log | wc -l

# Сколько подтверждений
grep '"action":"accept"' data/feedback.log | wc -l

# Какие запросы чаще всего отклоняют
grep '"action":"reject"' data/feedback.log | jq -r '.query_name' | sort | uniq -c | sort -rn | head -10

Частые вопросы про обучение на фидбеке

1. Как часто нужно запускать sku-cli learn?

Зависит от объема данных. Если у вас 1000 новых feedback-записей в день — запускайте раз в неделю. Если 100 — раз в месяц. Сервис анализирует все записи с момента последнего запуска (он не хранит состояние, просто читает весь файл каждый раз).

2. Почему сервис предлагает синоним, который уже есть?

Алгоритм ищет пересечения токенов в query_name и названии выбранного товара (product_name). Если синоним уже есть в synonyms.json, он все равно может предложить его снова. Просто игнорируйте такие предложения.

3. Как удалить синоним, который добавили по ошибке?

Через API:

curl -X DELETE http://localhost:8080/synonyms/цинк
# или через CLI:
sku-cli synonyms remove --word цинк

Также можно отредактировать data/synonyms.json вручную и перезапустить сервис, но это не требуется.

4. Что означает confidence в предложениях?

Число от 0 до 1, которое показывает долю accept-запросов, содержащих query-токен, в которых выбран товар с product-токеном. 0.7 = в 70% таких accept-запросов выбран товар с предложенным токеном. Можете использовать как фильтр: добавлять только те, у кого confidence > 0.5. Внутренний порог генерации синонимов — 0.3.

5. А сервис сам добавит синоним, если я не проверю?

Нет. Сервис только предлагает. Решение всегда за администратором. Это сделано специально — чтобы вы случайно не испортили поиск автоматическими правками.

6. Почему после добавления синонима поиск не изменился?

Проверьте:

  • Синоним добавился (проверьте GET /health — там есть synonyms_count)
  • Ваш запрос содержит слово, которое совпадает с ключом синонима (регистр не важен, все приводится к нижнему)
  • Синоним не конфликтует со стоп-словом (стоп-слова фильтруются ДО применения синонимов)

7. Можно ли автоматизировать добавление синонимов?

Можно, но осторожно. Напишите скрипт, который раз в неделю:

  1. Вызывает GET /admin/learn
  2. Фильтрует предложения с confidence > 0.8
  3. Отправляет их через POST /synonyms

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

8. Что делать, если feedback.log слишком большой?

Сервис сам не ротирует этот файл. Добавьте в crontab:

# Каждую неделю архивировать и очищать
0 2 * * 0 mv /path/to/data/feedback.log /path/to/data/feedback.log.$(date +\%Y\%m\%d) && touch /path/to/data/feedback.log

9. Почему GET /admin/learn возвращает пустые предложения?

Причины:

  • Нет accept-записей с product_name в feedback.log (для синонимов)
  • Нет zero_result-записей (для missing_product)
  • Нет повторяющихся accept/reject по одному товару (для check_product)
  • Включен параметр enabled = false в конфиге (фидбек не пишется)
  • В запросах все токены короче learn_min_token_len (по умолчанию 3 символа). "М6", "шт", "уп" — игнорируются

10. Можно ли обучить сервис на "хороших" совпадениях (action: "accept")?

Да. Алгоритм синонимов как раз работает на accept-записях с заполненным product_name. Хорошие совпадения показывают, что искали одно, а выбрали другое — и это самый надежный источник для предложений синонимов.

11. Как проверить, что обучение реально помогло?

Возьмите 100 запросов, на которых раньше были ошибки. Запустите поиск до добавления синонимов и после. Сравните результаты. Эффект зависит от качества и полноты feedback.

12. Что делать, если операторы нажимают "неверно" случайно?

Алгоритм ищет повторяющиеся паттерны. Для синонима нужно минимум 2 accept-записи с одним query-токеном и confidence ≥ 0.3. Случайные нажатия не наберут статистику.

Продвинутая настройка обучения

В config.toml есть параметры:

[feedback]
enabled = true                      # Включить сбор фидбека
log_path = "./data/feedback.log"    # Путь к логу
learn_min_token_len = 3             # Игнорировать слова короче 3 букв
learn_max_suggestions = 20          # Максимум предложений за раз

Что изменить, если...

СитуацияРешение
Слишком много мусорных предложенийУвеличьте learn_min_token_len до 4-5
Сервис не видит короткие важные слова ("М6", "din")Уменьшите learn_min_token_len до 2
Приходит 100+ предложений, трудно читатьУменьшите learn_max_suggestions до 10
Нет предложений, хотя ошибки естьПроверьте, что длина токенов >= learn_min_token_len, и что в accept-записях передано product_name
Совет: Запускайте `sku-cli learn` раз в неделю и просматривайте предложения. Добавляйте синонимы, которые кажутся логичными. Через месяц точность поиска заметно вырастет.

Производительность

Ниже — реальные замеры на двух разных стендах. Free-версия тестировалась на скромном железе 2014 года, PRO-версия — на современном сервере с эмбеддингами. Все цифры приблизительные и зависят от CPU, RAM, диска и загрузки системы.

Free: поиск без эмбеддингов

Стенд: Mac mini 2014, Linux, 4 CPU, 8 ГБ RAM. Загружено 50 000 тестовых позиций. Цифры взяты из замеров для статьи на Infostart.

ПоказательЗначениеЧто это значит
Точный поиск по id1.85 мс / p95 3.40 мсМгновенный ответ, 1С не заметит задержки
Точный поиск по штрихкоду1.35 мс / p95 1.65 мсЕще быстрее — обычно по индексу
Fuzzy по названию2.35 мс / p95 2.86 мсС учетом опечаток и форм слов
Загрузка 50 000 позиций~39 с~1275 записей/сек
Размер индекса~3.4 МБОчень компактно

Free: QPS поиска по названию на 10 000 товаров

Замер через wrk на стенде AMD Ryzen 7 5700X, 64 ГБ RAM, NVMe SSD. 1000 случайных поисковых фраз из демо-каталога, limit=10, нагрузка: 2 потока, 10 соединений, 30 с.

КонфигурацияQPSСредняя latencyp50p99
Production-конфиг (по умолчанию)~242~41 мс~40 мс~83 мс
Tuned-конфиг (отключены дорогие фичи поиска)~664~15 мс~14 мс~30 мс

Tuned-конфиг дает примерно в 2,7 раза больше QPS за счет отключения дорогих search-time фич. Это максимум throughput; production-конфиг сохраняет более высокое качество поиска.

Что именно изменено в tuned-конфиге и зачем
ПараметрProductionTunedПочему влияет на скорость
max_candidates_for_fuzzy500100Меньше fuzzy-кандидатов — быстрее пост-фильтрация
use_unified_searchtruefalseНет лишнего Boolean OR по exact-полям (code, art, barcode)
transliteration_enabledtruefalseНе генерируются транслитерированные варианты токенов
enable_critical_token_filteringtruefalseОтключен пост-фильтр по «критичным» токенам
extended_search_enabledtruefalseНе ищем по extra-полям (color, material и т.д.)
enable_deep_1c_parsingtruefalseНет препроцессинга 1С-запросов
min_token_match_ratio0.50.0Не отсекаем результаты по минимальному покрытию токенов
candidate_multiplier21Меньше кандидатов на ранжирование
logging.levelinfowarnМеньше логов на каждый запрос
console_enabled / file_enabledtruefalseНет pretty-вывода и записи логов на диск

Побочный эффект: может снизиться recall на опечатках, разных падежах и поиске по атрибутам. Используйте tuned-конфиг только как потолок производительности или для нагрузочных тестов, где важен максимум RPS.

PRO: гибридный поиск с эмбеддингами

20 000 товаров, bge-m3-int8

Стенд: AMD Ryzen 9 7945HX, 16 cores / 32 threads, 92 ГБ RAM. Модель bge-m3-int8 (1024 dim).

ПоказательЗначениеПримечание
Вставка 20 000 товаров3.75 с~5339 записей/сек. Эмбеддинги строятся асинхронно.
Фоновая индексация 20 000 векторов~4 минДва фоновых воркера, batch 100. Скорость вычисления эмбеддингов — главный лимит.
Среднее время поиска3.86 мсГибридный поиск: текст + эмбеддинги
p502.28 мсБольшинство запросов мгновенны
p956.39 мсТяжелые запросы остаются быстрыми
p9962.20 мсХолодный старт / прогрев кэша первых запросов
QPS~2541000 тестовых запросов за 3.94 с

100 000 товаров, bge-m3-int8

Стенд: тот же AMD Ryzen 9 7945HX. Batch 500.

ПоказательЗначение
Вставка 100 000 товаров20.77 с (~4813 записей/сек)
Поиск (warm cache)QPS 1603, avg 0.60 мс, p95 0.74 мс
Поиск (cold cache)QPS 460, avg 2.14 мс, p99 75.5 мс
Размер data/506.87 МБ
Размер полнотекстового индекса37.05 МБ
Размер векторного хранилища415.31 МБ

Ресурсы PRO

Ресурс20k товаров100k товаров
RAM после загрузки + модель~9.2 ГБ~20.2 ГБ
Рост RAM относительно старта~0.5 ГБ~2.3 ГБ
Диск (полнотекстовый индекс + векторное хранилище + очередь)~119 МБ~507 МБ
CPU в поиске~13%~5%

Рекомендуемые настройки фоновой индексации

Параметрbge-m3 (по умолчанию)E5-small (быстрее)
pro.background.worker_count22
pro.background.batch_size100500
pro.background.lance_write_buffer_size5121000
Ожидаемая скорость индексации~69 items/s~195 items/s
Для небольших каталогов (\<1000 товаров): уменьшите `pro.background.lance_write_flush_timeout_ms` до 1000–2000 мс, иначе большой буфер будет ждать таймаута.

Какую модель выбрать?

В PRO-режиме семантический поиск работает через ONNX Runtime. По умолчанию активна модель bge-m3, также из коробки настроена e5. Оба варианта конфигурируются через config.toml: вы указываете путь к model.onnx, размерность вектора и стратегию объединения токенов.

Скорость и размер (100k товаров)

МодельВставка 100kQPSAvg latencyДиск data/Полнотекстовый индексВекторное хранилище
bge-m316.11 с (~6208/s)4562.17 мс506.52 МБ37.05 МБ415.23 МБ
e5-small25.02 с (~3997/s)8391.16 мс241.98 МБ37.04 МБ152.99 МБ

Точность (synthetic dataset, 1500 товаров, 120 запросов)

МодельstrategyHit Rate@1Hit Rate@5MRR
bge-m3embedding_only65.83%77.50%0.7156
e5-smallembedding_only36.67%58.33%0.4699
bge-m3hybrid74.17%88.33%0.7994
e5-smallhybrid61.67%80.83%0.7079
Выбирайте bge-m3, если:
  • важна высокая точность семантического поиска;
  • в каталоге много синонимов и опечаток;
  • нужна многоязычность (русский + английский + артикулы);
  • ресурсы сервера позволяют — 20+ ГБ RAM и SSD.
Выбирайте e5-small, если:
  • основной режим — hybrid с сильным полнотекстовым сигналом;
  • критичны скорость индексации и размер диска;
  • запросы в основном точные или содержат артикулы/штрихкоды;
  • сервер ограничен по RAM.
Про разреженные векторы: для `bge-m3` есть поддержка sparse-векторов (параметр `enable_sparse`). В наших тестах подключение sparse-ветки не дало прироста точности в hybrid-режиме, поэтому по умолчанию она выключена.
Как это читать: HTTP-ответ на `/add` остается мгновенным (\~0.1 мс на товар), потому что эмбеддинги строятся в фоновой очереди. Основное время загрузки большого каталога — именно фоновая индексация векторов.

Часто задаваемые вопросы

Что делать, если поиск не работает?

Пошаговый чек-лист:

1
Проверьте, что сервис запущен
curl http://localhost:8080/health | jq .status — ожидаем "ok".
Через CLI: sku-cli health | jq .status
2
Проверьте, что в индексе есть данные
curl http://localhost:8080/health | jq .db_record_count — ожидаем > 0. Если 0 — загрузите данные через POST /add.
Через CLI: sku-cli health | jq .db_record_count
3
Проверьте API-ключ (если включен)
В config.toml: api_key = "" — если не пустой, нужен заголовок X-API-Key. Ошибка 401 → проверьте ключ и заголовки.
4
Проверьте rate limiting
В config.toml: rate_limit_per_minute = 0 — временно отключите для теста. Ошибка 429 → увеличьте лимит.

Поиск не находит то, что должен

Запрос без name возвращает пустой результат

Причина: require_name_for_fuzzy = true.

[search]
require_name_for_fuzzy = false   # ← измените на false

При false можно искать только по динамическим полям (поставщик, бренд, ТН ВЭД).

Поиск по динамическим полям (supplier, brand) не работает

Поиск по динамическим полям доступен только в PRO-редакции. В Free-сборке расширенные поля не регистрируются в схеме индекса и не могут быть включены ни через какую настройку.

Причина 1 (PRO): extended_search_enabled = false.

[search]
extended_search_enabled = true    # ← включите

Причина 2 (PRO): поле не описано в [[search.extended_fields]].

[[search.extended_fields]]
field = "supplier"   # ← добавьте поле
weight = 0.7
stemmed = true
fuzzy = true

Сервис находит только идеальные совпадения, ничего "похожего"

Причина: слишком высокий порог auto_match_threshold.

[search]
auto_match_threshold = 0.85      # ← попробуйте снизить до 0.75-0.80
relative_score_threshold = 0.55

Слишком высокий порог (0.95+) — находим только идеальные совпадения. Слишком низкий (0.5) — много мусора.

Короткие запросы ("перф", "цинк") ничего не находят

Причина: edge_ngram.enabled = false или слишком высокий min_token_length.

[search]
min_token_length = 2           # ← должно быть 2 или меньше

[search.edge_ngram]
enabled = true                 # ← включите N-gram поиск
min_gram = 3
max_gram = 6
Включение N-gram увеличивает размер индекса на 30-40%.

Опечатки не исправляются ("блот" не находит "болт")

Причина: fuzzy_max_distance = 0.

[index]
fuzzy_max_distance = 2         # ← разрешить до 2 опечаток

Расстояние 1 уже находит "блот" → "болт" (перестановка соседних букв). Расстояние 2 дополнительно находит "блоот" → "болт", но делает поиск в 1.5-2 раза медленнее.

Поиск в другом падеже не работает ("болтом" не находит "болт")

Причина: при fuzzy_max_distance = 1 сервис не успевает «согласовать» стеммированный корень с исходной формой в документе. Русский стеммер приводит «болтом» и «болт» к одному корню, но пост-фильтр ориентируется на расстояние Дамерау-Левенштейна (Damerau-Levenshtein), которое между этими формами равно 2.

[index]
fuzzy_max_distance = 2         # ← позволяет находить разные падежи и числа

Рекомендация:

  • Используйте 1, если основная задача — исправлять опечатки и искать по точным артикулам/штрихкодам.
  • Используйте 2, если пользователи часто вводят названия в разных падежах или числах ("болтом", "болты", "краской" → "краска"), а также при работе с кириллическими названиями, где встречаются опечатки в два символа: "краскаа" → "краска", "крска" → "краска", "телефоон" → "телефон".
  • Значения >2 не рекомендуются — значительно растет задержка (+50–100% latency) и количество ложных срабатываний.

Товар с 2 совпавшими словами из 3 не показывается

Причина: слишком высокий min_token_match_ratio.

[search]
min_token_match_ratio = 0.5    # ← 50% совпадений достаточно
# Если 0.7 — нужно 70% (3 из 3 или 2 из 3 не хватит)

Сервис падает при старте с ошибкой SchemaError

Ошибка: SchemaError("An index exists but the schema does not match.")

Причина: поисковый индекс был создан другой версией сервиса или с другой схемой полей.

Решение: удалите старый индекс и перезапустите сервис. Данные в индексе будут потеряны — перезагрузите их через POST /add.

# Остановите сервис
rm -rf ./data/index
# Перезапустите сервис — индекс создастся заново
В production не удаляйте индекс без бэкапа. Данные в ./data/index можно восстановить только из исходного источника.
Чтобы избежать ошибки после обновления исполняемого файла: используйте одну и ту же версию сервиса для создания и открытия индекса, или настройте автоматическое пересоздание индекса при несовпадении схемы.

PRO: почему не работает PRO-версия?

В /health показывает edition: "free" или license.active: false.

Возможные причины:

  1. Используется Free-версия вместо PRO. Редакция определяется поставляемым исполняемым файлом и не переключается через config.toml. Проверьте:

    curl http://localhost:8080/health | jq .edition
    # Ожидаем: "pro"
    
    # Или через CLI:
    sku-cli health | jq .edition
    

    Если "free" — вы запустили Free-версию вместо PRO.

  2. Нет файла лицензии. PRO требует валидный файл ./data/license.lic. Получите его через сервер активации или админ-панель.

  3. PRO-файл поврежден или не соответствует вашей платформе. Скачайте архив PRO-версии заново и убедитесь, что исполняемый файл запускается без ошибок.

PRO: Как активировать PRO-версию?

  1. Получите пин-код PRO-лицензии (после оплаты или от администратора).

  2. Откройте в браузере страницу активации:

    • в сервисе — корень /, если PRO-версия еще не активирована;
    • на сайте — /license/activate/pro.
  3. Введите полученный пин-код и нажмите кнопку активации.

  4. Сервис проверит пин-код и сам установит лицензию по пути license_path (обычно ./data/license.lic). Ручное скачивание или копирование файла не требуется.

  5. Проверьте статус:

    curl http://localhost:8080/health | jq .license.active
    # Ожидаем: true
    
    # Или через CLI:
    sku-cli health | jq .license.active
    
Пин-код привязывается к вашей установке (fingerprint). Если вы переустановите сервис на другой машине — понадобится новая активация или пин с достаточным лимитом активаций.
Если после активации license.active: false — проверьте, что папка data/ (параметр license_path в config.toml) доступна сервису на запись и примонтирована как постоянный volume в Docker.

PRO: Кросс-коды и аналоги не показываются

Проверьте настройки кросс-кодов:

[pro.cross_codes]
enabled = true                  # ← должно быть true
include_in_response = true      # ← должно быть true
max_group_size = 30
Кросс-коды используют поле group_id. Если в ваших данных оно не заполнено — заполните его и перезагрузите товары. Переиндексация только из-за включения флага enabled не требуется.

PRO: Иерархия (/hierarchy) не работает

Проверьте настройки плагина:

[pro.plugins.hierarchy]
enabled = true                  # ← должно быть true
default_max_depth = 5

Также проверьте, что у товаров заполнено поле parent_id.

PRO: Эмбеддинги (семантический поиск) не работают

Чек-лист для эмбеддингов:

  1. Включен PRO-режим:

    [pro]
    enabled = true
    
  2. Включен оркестратор:

    [pro.search.orchestrator]
    embedding_enabled = true
    
  3. Выбран активный бэкенд и указан путь к модели:

    [pro.search.embedding]
    active_backend = "bge_m3"
    
    [pro.search.embedding.bge_m3]
    enabled = true
    model_path = "./models/bge-m3-int8/model.onnx"
    vector_dimension = 1024
    
  4. Модель существует и валидна:

    ls -la ./models/bge-m3-int8/model.onnx
    # Должен быть файл размером ~343 МБ (INT8)
    
  5. Проверьте health-статус:

    curl http://localhost:8080/health | jq .embedding
    # Ожидаем: "enabled": true, "model_loaded": true
    
    # Или через CLI:
    sku-cli health | jq .embedding
    
  6. Проверьте, что векторы построены:

    curl http://localhost:8080/health | jq .embedding.vector_count
    # Должно совпадать с .db_record_count
    
    # Или через CLI:
    sku-cli health | jq .embedding.vector_count
    
Если model_loaded = false, но файл модели есть — проверьте логи сервиса: возможна несовместимость версии ONNX-модели или ошибка инициализации runtime.

Частые ошибки при загрузке данных

Ошибка: id missing or empty

Причина: поле id обязательно для каждого товара.

{
  "name": "Болт М16",
  "id": "1C-GUID-001"   // ← обязательно!
}

Ошибка: rate limit exceeded (429)

Причина: слишком много запросов с одного IP.

[security]
rate_limit_per_minute = 0      # ← временно отключите для загрузки
Для пакетной загрузки отключайте rate limiting, после загрузки возвращайте обратно.

Данные не сохраняются после перезапуска

Причина: неправильный путь к индексу или volume не примонтирован.

[index]
path = "./data/index"          # ← относительный путь
В Docker используйте named volume или bind-mount для ./data. В production используйте абсолютный путь: path = "/var/lib/sku/index".

Фоновая индексация эмбеддингов идет слишком долго

Причина: маленький batch_size или мало worker_count.

[pro.background]
worker_count = 2
batch_size = 500               # ← для e5-small можно увеличить
lance_write_buffer_size = 1000
Для bge-m3 оставьте batch_size = 100 — он медленнее, но точнее. Для e5-small можно использовать batch_size = 500.

Как работают синонимы?

Синонимы хранятся в файле data/synonyms.json и загружаются при старте.

# Добавить синоним
sku-cli synonyms add --word "цинк" --expansions "оцинкованный,zn,zinc"

# Посмотреть список
sku-cli synonyms list

# Удалить синоним
sku-cli synonyms remove --word "цинк"
После изменения синонимов через CLI перезагрузка не требуется — изменения применяются сразу.

Общие рекомендации по настройке

Если сервис ничего не находит — снижайте пороги:

[search]
auto_match_threshold = 0.7
min_token_match_ratio = 0.4
relative_score_threshold = 0.4

Если сервис находит много мусора — повышайте пороги:

[search]
auto_match_threshold = 0.92
min_token_match_ratio = 0.7
relative_score_threshold = 0.6

Для массовой загрузки (100 000+ товаров):

[security]
rate_limit_per_minute = 0
rate_limit_burst = 0

[server]
max_batch_size = 500

[search]

Как получить поддержку

Если не нашли ответа на свой вопрос:

  • Email: kadr78job@gmail.com
  • Telegram: @kadr78job
  • Документация: полный перечень эндпоинтов и параметров — в этом документе
Перед обращением подготовьте:
  • Версию сервиса: sku-cli version
  • Логи: из папки ./data/logs/
  • Пример запроса, который не работает
  • Конфиг (без sensitive данных)

Глоссарий

Латентность
Время от отправки запроса до получения ответа. У нас 2-10 мс — это очень быстро. Для сравнения: вы моргаете за 100-150 мс.
Score
Число от 0 до 1, показывающее, насколько найденный товар похож на ваш запрос. 1.0 = идеально, 0 = совсем не похоже.
Fuzzy-поиск
Поиск с учетом опечаток. "блот" найдет "болт", "гайка" найдет "гайка".
Стемминг
Приведение слов к основе. "болты", "болтом", "болтов" → "болт". Повышает точность, особенно при поиске с разными формами слов.
Синонимы
Слова, которые считаются одинаковыми при поиске. "цинк" = "оцинкованный". Можно добавлять свои через CLI или API.
BM25
Алгоритм ранжирования, который решает, насколько товар похож на запрос. Умнее простого "посчитал одинаковые слова".
Токенизация
Разбиение текста на отдельные слова. "Болт М16 оцинкованный" → ["болт", "м16", "оцинкованный"].
id
Ваш внутренний идентификатор товара из 1С (GUID). Нужен, чтобы сервис мог обновлять товар. Должен быть уникальным: повторная загрузка с тем же id полностью перезаписывает запись. Пустой или отсутствующий id вызывает ошибку при добавлении.
N-gram
Поиск по коротким началам слов. "перф" найдет "перфоратор". Увеличивает размер индекса, но помогает при коротких запросах.
Эмбеддинг
Векторное представление текста товара или запроса. Позволяет искать по смыслу: "шурик" → "шуруповерт". Доступно только в PRO-режиме.

Лицензия и условия использования

sku-search распространяется в двух редакциях:
  • Free Edition — полнотекстовый поиск + динамические поля
  • PRO Edition — эмбеддинги, кросс-коды, иерархия, аналитика, фоновая очередь

Free Edition (бесплатно для всех)

Free Edition можно использовать БЕЗ ОГРАНИЧЕНИЙ:
  • В коммерческих и некоммерческих организациях
  • Внутри компании для своих нужд
  • В составе своего продукта (CRM, ERP, интернет-магазин)
  • Для тестирования и разработки
  • Распространять бинарные версии с сохранением уведомления об авторских правах
Что входит в Free Edition:
  • Полнотекстовый поиск — точный, fuzzy, n-gram
  • Синонимы и стоп-слова
  • Обратная связь (/feedback)
  • Экспорт / импорт данных
  • Базовое администрирование (создание папки backup_dir, ротация, бэкап конфига при обновлении)

PRO Edition (требует покупки лицензии)

PRO Edition включает все, что в Free, плюс:
  • Динамические поля (расширенный поиск по любым реквизитам)
  • Семантический поиск (эмбеддинги)
  • Кросс-коды (аналоги и заменители)
  • Иерархия (BOM / Where-Used)
  • Аналитика и тюнинг (/admin/analytics/*)
  • Фоновая очередь эмбеддингов
  • Кэширование результатов
  • Очистка и оптимизация индекса (/admin/clear, /admin/index/optimize)
Когда нужно покупать PRO-лицензию:
  • Вы используете PRO-функции (эмбеддинги, кросс-коды, иерархия) в коммерческих целях
  • Вы продаете продукт, в который встроен PRO-модуль
  • Вы предоставляете SaaS / API с PRO-функциями
  • Вы создаете конкурирующий продукт на основе PRO-модуля
Примеры:
СценарийFree или PRO?Нужна лицензия?
1С-франчайзи, ищете товары в своей базе (Free-функции)FreeНет
1С-франчайзи, используете эмбеддинги для поиска ("шурик" → "шуруповерт")PROДа
Делаете CRM и встраиваете Free-поиск для поиска контактовFreeНет
Делаете CRM и встраиваете PRO-эмбеддингиPROДа
Продаете SaaS "умный поиск для 1С" на PRO-функцияхPROДа

|

Пин-коды активации и подписка на обновления

Для активации PRO-версии используются пин-коды. Существует два типа пинов:

N — активация приложения
  • Дает право вечно использовать PRO-приложение на ограниченном числе ПК (по умолчанию до 3).
  • Включает стартовую подписку на обновления (по умолчанию 6 месяцев).
  • Привязывается к конкретному ПК через fingerprint.
S — продление подписки на обновления
  • Не активирует приложение сам по себе, но продлевает подписку на обновления.
  • Можно активировать на нескольких ПК в рамках лимита активаций.
  • Привязывается к email пользователя и fingerprint ПК.
Что происходит после истечения подписки?
  • Приложение продолжает работать — лицензия на использование вечная.
  • Вы больше не можете скачивать обновления PRO-версии через личный кабинет{.text-primary}.
  • Go UI показывает предупреждение «Срок подписки на обновления истек».
  • Для возобновления доступа к обновлениям активируйте пин продления подписки в форме активации{.text-primary} или в приложении.
Где активировать пин или скачать обновления?
  • Активировать PRO — получить .lic-файл для ручной установки на сервер.
  • Скачать обновления PRO — проверить подписку и скачать последнюю версию.
  • Веб-интерфейс PRO-сервиса (http://<адрес-сервера>:8080/activation) — встроенная форма активации, работает даже если лицензия еще не установлена.

Стоимость PRO-лицензии

  • Embedded (встраивание в свой продукт): от 19 900 ₽ / проект
  • SaaS / API (предоставление как услуги): от 49 900 ₽ / год
  • OEM (массовое распространение): договорная
Как получить PRO-лицензию:

В письме укажите: название компании, сценарий использования, какие PRO-функции планируете использовать.

Что запрещено в любом случае

  • Удалять уведомления об авторских правах из документации и исходников
  • Перепродавать Free-версию как "свой продукт без изменений"
  • Создавать прямого конкурента (продукт, чьей основной функцией является поиск номенклатуры)

Юридическая информация

Данный продукт использует следующие библиотеки с открытым исходным кодом:

  • Поисковый движок (MIT License)
  • Веб-фреймворк (MIT License)
  • Асинхронный runtime (MIT License)
  • Векторная база данных (Apache 2.0 License)
  • ONNX Runtime — инференс моделей (MIT License)

Полный список зависимостей и их лицензий доступен в файле LICENSES.md в составе дистрибутива.


© 2026 sku-search Team. Все права защищены.
Free Edition распространяется под лицензией MIT. PRO Edition — по коммерческой лицензии.
Сервис распространяется "как есть" (AS IS), без каких-либо гарантий.

SKU Search

Документация для разработчиков и 1С-специалистов • MIT License