Примечание: Это первая статья из цикла, в которой я делюсь бизнес-смыслами и подходом к решению проблемы. Во второй статье планирую подробно разобрать техническую реализацию.
Я работаю в компании, которая занимается автозапчастями. Не буду называть бренд, но представьте любой крупный интернет-магазин запчастей — у нас всё примерно так же.
Десять лет всё работало. Поставщики присылали прайсы, менеджеры загружали. В 90% случаев клиенты искали товар по артикулу — просто вбивали номер и получали результат. Оставшиеся 10% запросов — это названия вроде «хомут бмв х5». И поиск как-то справлялся.
Да, в базе была каша: один и тот же товар мог называться «Хомут винт. BMW X5/E81» и «Хомут крепления топливного шланга 12мм для BMW». Но артикулы вывозили, а на остальное закрывали глаза.
А потом мы узнали про Честный знак.
В 2026 году в автозапчастях вводится обязательная маркировка. Звучит скучно, пока не начинаешь разбираться.
Чтобы маркировать товар, нужно точно знать, что это за товар. Какая категория? Какие характеристики? Подпадает ли он под маркировку?
И тут мы полезли в свою базу.
200 миллионов номенклатур. Это не шутка, это реальная цифра за 10 лет работы.
И только 5% из них привязаны к категориям. Остальные 95% — это просто строки. Иногда осмысленные («Хомут винтовой 12-15 мм»), иногда — полная каша («BMW E81 12MM хомут крепл 1 шт»).
Мы не знали, что продаём. Буквально. Мы как котята, которые тыкаются в миску — вроде что-то съедобное, но что именно — непонятно.
Первая мысль — нанять студентов. Пусть сидят и вручную проставляют категории.
Посчитали: один человек обрабатывает 500 позиций в день. 200 млн / 500 = 400 000 человеко-дней.
Переведём в более понятные единицы:
При 250 рабочих днях в году: ~1 600 лет работы одним человеком
С командой из 100 человек: ~16 лет
С командой из 1 000 человек: ~1,6 года
Тысяча человек — это целый завод. С зарплатой 50 тыс — 50 млн в месяц только на зарплату, без налогов, без оборудования, без соцпакета.
Отбой. Ищем другой путь.
Когда увидел эти цифры, засел за эксперименты в свободное время. Не потому что на работе не давали ресурсов — просто тема оказалась настолько интересной, что вечерами и выходными я ковырялся сам. Как хобби, только полезное.
Взял выборку из 10 тысяч наших «грязных» названий. Поставил задачу: научиться из строки:
понимать:
что это за предмет (хомут винтовой)
какие у него характеристики (12-15 мм)
для каких машин подходит (BMW 1, 3, 5, X5, X6...)
и главное — к каким категориям это относится
Первое, что приходит в голову — написать регулярные выражения. Ну правда, кажется же несложно: вырезать всё до цифр, потом взять цифры, потом найти модели машин...
Написал. Работало на 10 примерах. На 100 — начало сбоить. На 1000 — понял, что так не вывезу. Слишком много вариантов написания, слишком много исключений, слишком много ручной работы под каждого поставщика.
Пошёл дальше. Начал писать регулярные выражения под каждый тип данных. Отдельная регулярка для размеров, отдельная — для моделей BMW, отдельная — для брендов.
Регулярки работали, но их становилось всё больше. Сначала 10, потом 50, потом 200. Каждая новая регулярка замедляла обработку. Замерил: на 10 тысячах записей скорость упала в 3 раза. На 100 тысячах — в 10 раз.
Регулярки умерли. Они просто не масштабировались.
Тогда вспомнил про Aho-Corasick — это алгоритм для одновременного поиска множества подстрок за один проход текста. Сделал словари: «хомут» → «хомут», «хомутик» → «хомут», «clamp» → «хомут», «хомут винтовой» → «хомут винтовой».
Скорость вернулась. Мог искать 1000 паттернов за то же время, что раньше искал 1. Но качество упёрлось в потолок. Словари не покрывали всех вариантов написания, особенно когда в строке была каша из цифр, букв и служебных символов.
Когда понял, что правила и словари не покрывают всех случаев, решил попробовать нейросети. И тут ждал сюрприз.
Оказалось, что:
Дорогие модели (GPT-4, Claude) работают отлично, но стоят денег
Дешёвые модели работают так себе
Есть локальные модели, которые можно дообучить под нашу специфику через LoRA — и они работают почти как дорогие, но за копейки
А для 80% простых задач ИИ вообще не нужен — хватает тех же регулярных выражений или тривиальных преобразований
Разделил задачи по сложности и под каждую подобрал свой инструмент:
Базовые операции (trim, lowercase, collapse_whitespace и тд.) — для приведения строк к единому виду
Regex — для простых замен с чёткими паттернами (например, замена 5W-20 на 5w20 или удаление лишних символов)
Aho-Corasick — для массовых замен по словарям (синонимы, бренды, модели)
Локальные модели (LoRA) — для извлечения сущностей в типовых случаях
Тяжёлые модели (OpenAI, YandexGPT) — только для самых сложных случаев, где нужно реальное понимание контекста
И вот здесь родилась ключевая идея: пайплайны. Последовательность шагов, где каждый шаг делает что-то одно. Первый шаг — очистка мусора. Второй — извлечение характеристик. Третий — определение предмета. Четвёртый — маппинг на категории.
И чтобы это можно было настраивать без программистов — сделал визуальный конструктор.
В JSON-объекте товара есть десятки полей: артикул, бренд, название, описание, цена, остатки, характеристики. Мне нужно чистить только название и иногда описание. Остальные поля трогать нельзя — артикул должен остаться артикулом, бренд — брендом.
Пришлось придумать field selector'ы — механизм, который говорит: «примени это преобразование только к конкретному полю, а остальные поля оставь как есть». Без этого я бы поломал все данные.
Сначала пытался заставить ИИ сразу определять категорию. Это плохо работало, потому что категория — вещь условная. Один и тот же предмет может лежать в разных категориях в зависимости от магазина.
Сделал иначе: ИИ вытаскивает предмет (хомут винтовой, прокладка, болт). А уже потом, через матрицу соответствий «предмет → категории», товар автоматически маппится на те категории, где этот предмет чаще всего встречается. Если предмет может относиться к нескольким категориям — берём лучшее пересечение.
Когда начинал, думал: привяжусь к OpenAI, и всё будет хорошо. А потом пришли юристы с работы и сказали: «А если данные не должны покидать РФ?»
Пришлось делать архитектуру, где можно подключить любую модель в два клика:
OpenAI (и всё совместимое)
YandexGPT (через Yandex Cloud — данные остаются в РФ)
Локальные модели (через Ollama или свои сервера)
Самописные (если вдруг)
Пользователь в интерфейсе просто выбирает, какую модель на какой шаг пайплайна поставить. Дешёвые — на простые задачи, дорогие — на сложные. Оптимизация бюджета и качества.
Стандартных экшенов (trim, lowercase, regex, aho) хватает для 80% задач. Но всегда есть 20%, где нужно что-то своё: сходить в CRM, проверить товар по API поставщика, подтянуть цену из внешней системы.
Добавил в Rhai (безопасный скриптовый язык) возможность делать HTTP-запросы прямо из пайплайна:
let response = http_get("https://api.supplier.ru/check/" + value.sku); if response.status == 200 { value.verified = true; value.supplier_price = response.body.price; } else { value.verified = false; } value
Что это даёт:
Можно обогащать данные в реальном времени
Можно проверять товары по внешним справочникам
Можно интегрироваться с чем угодно без доработки платформы
И всё это безопасно — Rhai работает в песочнице, никаких системных вызовов, только HTTP наружу с явного разрешения.
200 млн записей — это много. А ещё есть ИИ-обработка, которая может длиться секунды на одну запись. Если запустить всё последовательно, буду чистить базу до пенсии.
Особенность архитектуры — Rust-бэкенд потребляет минимум ресурсов и достаточно шустрый. Поэтому его масштабирование обходится дёшево. А провайдеры ИИ-моделей обычно не выставляют жёстких rate limit'ов — можно слать очень много запросов параллельно.
Kubernetes позволяет масштабировать обработку горизонтально: хочу обрабатывать быстрее — добавляю поды. Сейчас один под обрабатывает ~150 000 батчей в 6 часов при лимите CPU всего 300m (30% одного ядра). Технически можно увеличить лимиты и обрабатывать больше на одном поде, но мне удобнее масштабироваться через добавление подов — это даёт лучшую отказоустойчивость и упрощает управление ресурсами. Десять подов — 1.5 млн батчей в 6 часов. И это на тяжеловесных моделях.
Главное, что это линейно масштабируется. Хоть 200 млн обработаю.
Через несколько месяцев вечерней работы получился работающий прототип. Прогнал через него 1 млн наших записей и получил:
|
Метрика |
Значение |
|---|---|
|
Точность обработки |
96% (проверяли ручной выборкой) |
|
Определение предмета |
>85% (с нуля) |
|
Категоризация |
78% товаров получили категорию через маппинг предметов |
|
Экономия vs ручная работа |
до 75% |
Было (одна строка из прайс-листа поставщика):
"OC222 Фильтр масл._A-R 156 2.4TDi 97-> / Volvo 340/440/460/480 1.4/1.7/1.6D 79-95"
После прохода через:
{ "article": "W9171", "brand": "MANN", "raw_name": "OC222 Фильтр масл._A-R 156 2.4TDi 97-> / Volvo 340/440/460/480 1.4/1.7/1.6D 79-95", "name": "Фильтр масляный", "subjects": [ "масляный фильтр", "фильтр" ], "features": [ "a-r", "двигатели 2.4 tdi", "двигатели 1.4", "двигатели 1.7", "двигатели 1.6d" ], "applicability": [ { "brand": "VOLVO", "model": "340", "year": "1979-1995", "brand_info": { "name": "Volvo", "cyrillic_name": "Вольво", "country": "Швеция" } }, { "brand": "VOLVO", "model": "440", "year": "1979-1995", "brand_info": { "name": "Volvo", "cyrillic_name": "Вольво", "country": "Швеция" }, "model_info": { "name": "440", "cyrillic_name": "440", "class": "C" } }, { "brand": "VOLVO", "model": "460", "year": "1979-1995", "brand_info": { "name": "Volvo", "cyrillic_name": "Вольво", "country": "Швеция" }, "model_info": { "name": "460", "cyrillic_name": "460", "class": "C" } }, { "brand": "VOLVO", "model": "480", "year": "1979-1995", "brand_info": { "name": "Volvo", "cyrillic_name": "Вольво", "country": "Швеция" }, "model_info": { "name": "480", "cyrillic_name": "480", "class": "C" } } ] }
И теперь, имея предметы, через матрицу соответствий автоматически отношу этот товар к категориям:
Фильтры (основная)
Масляная система (дополнительная)
Эта универсальная система категоризации теперь позволяет точно определить, какие товары подлежат маркировке по «Честному знаку». Правила маркировки привязаны к категориям товаров и их свойствам, и теперь, когда мы знаем категории всех 200 миллионов позиций, можем автоматически применять эти правила. Раньше мы не могли этого сделать — не знали, к каким категориям относятся 95% товаров. Теперь система проверяет каждую позицию по матрице соответствий «категория → необходимость маркировки» и формирует чёткий список.
Когда привёл 200 млн записей в порядок, осознал, что открыл новые возможности для монетизации, о которых раньше не подозревал.
Раньше: База была чёрным ящиком. Мы знали, что что-то продаётся, но что именно — могли понять только по артикулам и брендам, которые помнили менеджеры.
Теперь: У нас структурированные данные по каждому товару: предмет, категория, характеристики, применимость и какие то свойства товара. И мы можем отвечать на вопросы, о которых раньше даже не задумывались.
Раньше информация о бренде бралась из поля "brand" в прайсе. Мы знали, что "ABS" — это бренд запчастей, но не знали статистику: какая категория товаров, на какие машины, какого года выпуска и т.д. Теперь мы точно знаем распределение:
Ранг | Бренд | % от всех позиций ---------------------------------------- 1 | HYUNDAI | 9.0% 2 | TOYOTA | 8.2% 3 | VOLKSWAGEN | 6.6% 4 | BMW | 6.0% 5 | KIA | 5.4% 6 | AUDI | 4.9% 7 | MERCEDES-BENZ | 4.8% 8 | RENAULT | 4.5% 9 | NISSAN | 4.0% 10 | FORD | 4.0% 11 | MITSUBISHI | 3.4% 12 | CHEVROLET | 2.6% 13 | SKODA | 2.6% 14 | VOLVO | 2.6% 15 | OPEL | 2.2% 16 | LADA | 3.7% 17 | MAZDA | 1.9% 18 | CITROEN | 1.7% 19 | CHERY | 1.6% 20 | PEUGEOT | 1.6%
Что мы видим: Корейские бренды (Hyundai, Kia) в сумме дают почти 15% ассортимента — это характерная особенность нашего ассортимента. А Lada даёт 3.7% — не так много, как можно было подумать.
Теперь мы знаем, какие товары составляют основу нашего ассортимента:
Ранг | Предмет | % от всех позиций ---------------------------------------- 1 | фильтр | 6.7% 2 | прокладка | 4.1% 3 | колодки | 3.1% 4 | тормозные колодки | 2.9% 5 | масло | 2.4% 6 | подшипник | 2.4% 7 | моторное масло | 2.3% 8 | воздушный фильтр | 2.1% 9 | датчик | 2.1% 10 | фара | 2.0% 11 | опора | 1.9% 12 | кольцо | 1.8% 13 | сальник | 1.8% 14 | масляный фильтр | 1.8% 15 | кронштейн | 1.7% 16 | накладка | 1.6% 17 | ремень | 1.5% 18 | сайлентблок | 1.5% 19 | диск | 1.5% 20 | бампер | 1.4%
Что мы видим: Фильтры (всех типов) в сумме дают около 10% ассортимента. Это товары с высокой оборачиваемостью — их нужно держать в стоке всегда.
А вот "колодки" и "тормозные колодки" — это не одно и то же. В нашей системе классификации это разные уровни детализации:
Предмет первого ранга ("колодки") — общая категория товара
Предмет второго ранга ("тормозные колодки") — более конкретное определение с указанием назначения
Это работает по принципу иерархической классификации: система извлекает из названия ключевые сущности (subjects) разного уровня детализации. "Колодки" — это общий тип товара, а "тормозные колодки" — уже конкретизированный вариант с указанием системы автомобиля.
Нормализация помогла нам увидеть эту структуру и правильно классифицировать товары по уровням детализации, что критически важно для точной аналитики и управления ассортиментом.
Через применимость мы теперь знаем, под какие модели у нас больше всего запчастей:
Ранг | Модель | % от всех связок ---------------------------------------- 1 | SOLARIS | 1.0% 2 | LOGAN | 1.0% 3 | A3 | 1.0% 4 | CAMRY | 0.9% 5 | RIO | 0.8% 6 | OCTAVIA | 0.8% 7 | GOLF | 0.8% 8 | ELANTRA | 0.7% 9 | PASSAT | 0.7% 10 | ACCENT | 0.7% 11 | POLO | 0.7% 12 | A4 | 0.7% 13 | COROLLA | 0.7% 14 | SPORTAGE | 0.7% 15 | DUSTER | 0.6% 16 | 3 | 0.6% 17 | SANTA FE | 0.6% 18 | TUCSON | 0.6% 19 | AVEO | 0.6% 20 | FOCUS | 0.6%
Что мы видим: Ожидаемо лидируют бюджетные модели (Solaris, Logan, Rio) — они составляют основу нашего ассортимента.
Но есть две интересные находки:
Audi A3 на 3-м месте — неожиданно высоко
Toyota Camry на 4-м месте — подтверждает репутацию надёжного седана, который долго служит и требует обслуживания
Особенно интересен случай с Audi A3. Почему так высоко в рейтинге запчастей? Несколько причин:
Массовость в своём сегменте: A3 — самый доступный Audi, его продавали огромными тиражами. За 10+ лет выпуска накопился большой парк.
Возраст парка: Многие A3 первого и второго поколений (выпуска 1996-2012 годов) уже требуют серьёзного обслуживания. Владельцы готовы вкладываться в ремонт, так как покупка нового премиум-автомобиля обходится дороже.
Ремонтопригодность: Немецкие автомобили хорошо поддаются ремонту, а для A3 есть огромный выбор запчастей — как оригинальных, так и аналогов.
Что касается Camry — это классический случай "вечной" машины. Её ремонтируют десятилетиями, потому что конструкция проверена временем, а стоимость обслуживания разумная.
Мы проанализировали все связки "товар — модель авто" и посмотрели, для каких лет выпуска чаще всего подбирают запчасти:
Формат года | % от всех связок ---------------------------------------- 2006- | 5.1% 2010- | 4.1% 2007- | 4.0% 2004- | 3.9% 2005- | 3.5% 2011- | 3.4% 2008- | 3.2% 2012- | 3.2% 2003- | 2.9% 2009- | 2.7% 2002- | 2.2% 2015- | 2.0% 2013- | 1.9% 2000- | 1.8% 2014- | 1.7%
Что мы видим: Пик приходится на 2006-2010 годы — это машины, которые уже вышли из гарантии, но ещё не отправились на свалку. Для них нужны запчасти, и владельцы готовы платить. Машины младше 2015 года (2.0%) пока редко попадают в наш ассортимент — либо ещё на гарантии, либо ремонтируются у официальных дилеров. А вот машины 2000-2003 годов (ещё 2-3%) — это уже "олдтаймеры", для которых запчасти ищут коллекционеры и энтузиасты.
Мы видим, что "фильтр" может быть масляным, воздушным, топливным, салонным. "Колодки" бывают тормозными (передние/задние), а бывают — стояночного тормоза.
Это позволяет умнее строить навигацию по сайту и перекрёстные рекомендации. Например, если человек купил масляный фильтр, ему скорее всего нужно и масло (2.4% ассортимента) — и мы можем предложить это сразу.
Теперь мы понимаем, что для Hyundai Solaris (самая популярная модель) чаще всего покупают:
масляные фильтры (каждые 10-15 тыс км)
тормозные колодки (каждые 30-40 тыс км)
сайлентблоки (после 60-80 тыс км)
Значит, эти позиции должны быть в стоке всегда. А вот, скажем, фары для Audi A3 покупают реже, но когда покупают — готовы ждать под заказ.
Имея чистые данные за несколько лет, можно строить модели:
зимой растёт спрос на свечи накала (для дизелей) и антифризы
перед летом — на кондиционеры и радиаторы
при росте курса валют — на бюджетные аналоги (корейские бренды выходят на первый план)
|
Что измерили |
До нормализации |
После |
|---|---|---|
|
Товаров с известным брендом |
~0% |
>85% |
|
Товаров с известной моделью |
~0% |
>85% |
|
Товаров с известной категорией |
5% |
78% |
|
Возможность аналитики по спросу |
почти 0 |
есть по всем разрезам |
Когда я показал результат коллегам, они сказали: «Это работает. Надо внедрять». Внедрили. Система обрабатывает наши данные до сих пор.
А потом я посмотрел по сторонам и понял: такая проблема не только у нас. У любого, кто торгует сложными товарами (автозапчасти, стройматериалы, одежда, электроника) — та же беда. Поставщики присылают кто во что горазд, менеджеры правят руками, база превращается в свалку.
И Честный знак придёт ко всем.
Поэтому я решил систематизировать свой подход. Доработал интерфейс, добавил визуальный конструктор пайплайнов (чтобы любой аналитик мог настраивать без программистов), упаковал в Kubernetes.
Так сформировался подход к решению проблемы нормализации, который оказался полезным не только в моём случае.
Если коротко:
Ядро: Rust (безопасность, скорость, предсказуемость)
API: gRPC (строгая типизация, streaming)
Фронтенд: Nuxt + визуальный конструктор
Инфраструктура: Kubernetes (горизонтальное масштабирование, self-healing)
Хранилище: PostgreSQL (конфиги, история, задачи)
Скриптинг: Rhai (изолированная среда)
ИИ: Адаптеры под OpenAI, YandexGPT, локальные модели
Особенности:
Field selector'ы для точечной обработки полей
Aho-Corasick для быстрых множественных замен
JSON Schema для валидации
Регулярные выражения
Типовые операции смены регистров, удаления пробелов
Произвольный код RHAI с расширением функционала стандартной библиотеки
Полная поддержка работы в РФ (Yandex Cloud)
Этот подход начался как внутреннее решение конкретной проблемы. В процессе работы над ним родилось много инсайтов о том, как подходить к нормализации данных в современных условиях.
Если тема нормализации данных, подготовки к Честному знаку или работа с большими объёмами неструктурированной информации вам интересна — давайте обсудим в комментариях или личных сообщениях. Мне интересно узнать, с какими похожими проблемами сталкиваетесь вы и какие подходы находите эффективными.
Особенно было бы полезно услышать от тех, кто:
Работает с похожими объёмами данных
Уже сталкивался с необходимостью маркировки товаров
Имеет опыт внедрения ИИ-решений для обработки текстов
Пытался решать подобные задачи другими способами
Также планирую написать вторую, более техническую статью, где подробно разберу архитектуру решения, особенности реализации на Rust, работу с Kubernetes и нюансы интеграции разных ИИ-моделей.
P.S. Долго не решался написать статью — это моя первая попытка поделиться опытом. Писал, чтобы рассказать о решении сложной задачи нормализации данных и обсудить подходы, которые могут оказаться полезными другим разработчикам и аналитикам. Если что-то сделал не так или упустил — сори, буду рад конструктивной критике в комментариях.
Источник


