Хабрахабр

Как мы автоматизировали большой интернет-магазин и стали сопоставлять товары автоматически

image

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

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

  • Работа с поставщиками. Чтобы продать что-то ненужное, нужно сначала купить что-то ненужное.
  • Управление каталогом. У кого-то узкая специализация, а кто-то продает сотни тысяч разных товаров.
  • Управление розничными ценами. Тут придется учесть и цены поставщиков, и цены конкурентов, и доступные финансовые инструменты.
  • Работа со складом. В принципе, можно и не иметь собственного склада, а забирать товар со складов партнеров, но так или иначе вопрос стоит.
  • Маркетинг. Тут наполнение сайта контентом, размещение на площадках, реклама (онлайн и офлайн), акции и много чего еще.
  • Прием и обработка заказов. Колл-центр, корзина на сайте, заказы через мессенджеры, заказы через площадки и маркетплейсы.
  • Доставка.
  • Бухгалтерия и прочие внутренние системы.

Магазин, о котором мы будем говорить, не имеет узкой специализации, а предлагает кучу всего от косметики до мини-трактора. Я расскажу, как у нас устроена работа с поставщиками, мониторинг конкурентов, управление каталогом и формирование цен (оптовых и розничных), работа с оптовыми клиентами. Немного затронем тему склада.

И, возможно, после нескольких попыток выйдет развить новый бизнес. Чтобы лучше понимать некоторые технические решения, будет не лишним знать, что в
какой-то момент мы решили, что технологические вещи, если это возможно, будем делать не для себя, а универсальными. Получается, условно, стартап внутри компании.

Так что рассматриваем отдельную систему, более-менее универсальную, с которой интегрирована остальная инфраструктура компании.

В чем проблема работы с поставщиками

А их много, на самом деле. Просто приведу некоторые:

  • Поставщиков самих по себе много. У нас — порядка 400. Каждому нужно уделить какое-то время.
  • Нет единого способа получить предложения поставщиков. Кто-то шлет на почту по расписанию, кто-то по запросу, кто-то загружает в файлообменники, кто-то размещает на сайте. Способов много, вплоть до пересылки файла по скайпу.
  • Нет единого формата данных. Я даже картинку нарисовал на эту тему (она ниже, таблицы символизируют разные форматы).
  • Существует понятие минимальных розничных и минимальных оптовых цен, которые нужно соблюдать, чтобы продолжать работать с поставщиком. Часто они предоставляются в своем собственном формате.
  • Номенклатура у каждого поставщика своя. В итоге один и тот же товар называется по-разному, и нет уникального ключа, по которому их можно достаточно просто сопоставить. Поэтому мы сопоставляем сложно.
  • Не автоматизирована система размещения заказа у поставщика. У кого-то заказываем по скайпу, у кого-то в личном кабинете, кому-то каждый вечер посылаем эксель-файл со списком заказов.

image

Кроме последней, над последней работа в процессе. С этими проблемами мы научились справляться. Сейчас будут технические подробности, а потом рассмотрим следующий список.

Собираем данные

Как было

Файлы поставщиков вручную собирались из разных источников и подготавливались. Подготовка включала в себя переименование по определенному шаблону и редактирование содержимого. В зависимости от файла, нужно было удалить некондицию, товары, которых нет в наличии, переименовать столбцы или пересчитать валюту, собрать данные из разных вкладок на одной.

Как стало

В первую очередь, мы научились проверять почту и забирать оттуда письма с вложениями. Потом автоматизировали работу с прямыми ссылками и ссылками на яндекс и гугл диски. Это решило вопрос с получением предложений примерно 75% наших поставщиков. Еще мы заметили, что именно по этим каналам предложения чаще обновляются, так что реальный процент автоматизации получился больше. Какие-то прайсы мы по-прежнему получаем в мессенджерах.

Для этого мы завели профили поставщиков, где можно указать, что в каком столбце и на какой вкладке брать, как определять валюту и наличие, время доставки, график работы поставщика. Во-вторых, мы больше не обрабатываем файлы вручную.

Естественно, не с первого раза все учли, но сейчас гибкости хватает чтобы настроить обработку всех 400 поставщиков с учетом того, что у всех разное форматирование файлов. Получилось гибко.

В нашем случае этого оказалось достаточно. Что касается форматов файлов, то понимаем xls, xlsx, csv, xml (yml).

Завели список стоп-слов, и если предложение поставщика его содержит, то мы его не обрабатываем. Также придумали, как фильтровать записи. С ним поэкспериментировали и оставили все как есть, потому что выигрыш ощущается на списке на порядок большем нашего. Технические подробности такие: на небольшом списке можно и даже лучше “в лоб”, на больших списках быстрее фильтр Блума.

Наши поставщики работают по разным графикам, к тому же находятся в разных странах, выходные дни в которых не совпадают. Еще одна важная вещь — график работы поставщика. Когда мы будем формировать розничные и оптовые цены, нам придется как-то оценить время, когда мы сможем доставить товар клиенту. А время доставки обычно указано в виде числа или диапазона чисел в рабочих днях. Для этого мы завели конфигурируемые календари, и в настройках каждого поставщика можно указать, по какому из календарей он работает.

Бывает так, что у поставщика общий файл для всех партнеров, но есть договоренности по скидкам с некоторыми партнерами. Пришлось сделать конфигурацию скидок и наценок в зависимости от категории и производителя. Благодаря этому еще оказалось возможно добавить или отнять НДС при необходимости.

Ведь перед их применением нужно узнать, что это за товар. Кстати, конфигурация правил скидок и наценок приводит нас к следующей теме.

Как работает сопоставление

Небольшой пример того, как один и тот же товар может быть назван у разных поставщиков, для понимания того, с чем придется работать:

5" LG 22MP48D-P Black (16:9, 1920x1080, IPS, 60 Гц, интерфейсы DVI+D-Sub (VGA))
COMP — Компьютерная периферия — Мониторы LG 22MP48D-P
до 22" включительно LG Монитор LG 22MP48D-P (21. Монитор LG LCD 22MP48D-P
21. 5" LG 22MP48D-P gl. 5", черный, IPS LED 5ms 16:9 DVI матовая 250cd 1920x1080 D-Sub FHD) 22MP48D-P
Мониторы LG 22" LG 22MP48D-P Glossy-Black (IPS, LED, 1920x1080, 5 ms, 178°/178°, 250 cd/m, 100M:1 ,+DVI) Монитор
Мониторы LCD LG Монитор LCD 22" IPS 22MP48D-P LG 22MP48D-P
LG Монитор 21. ARUZ
LG Монитор LG 22MP48D-P Black 22MP48D-P. Black IPS, 1920x1080, 5ms, 250 cd/m2, 1000:1 (Mega DCR), D-Sub, DVI-D (HDCP), vesa 22MP48D-P. 5" LG Flatron 22MP48D-P gl. ARUZ
Монитор LG 22MP48D-P 22MP48D-P
Мониторы LG 22MP48D-P Glossy-Black 22MP48D-P
Монитор 21. 5" 22MP48D-P IPS LED, 1920x1080, 5ms, 250cd/m2, 5Mln:1, 178°/178°, D-Sub, DVI, Tilt, VESA, Glossy Black 22MP48D-P
LG 21. Black (IPS, 1920x1080, 16:9, 178/178, 250cd/m2, 1000:1, 5ms, D-Sub, DVI-D) (22MP48D-P) 22MP48D-P
Монитор 22" LG 22MP48D-P
LG 22MP48D-P IPS DVI
LG LG 21. 5`` LG 22MP48D-P Black
LG МОНИТОР 21. 5" 22MP48D-P (16:9, IPS, VGA, DVI) 22MP48D-P
Монитор 21. 5'' [16:9] 1920х1080(FHD) IPS, nonGLARE, 250cd/m2, H178°/V178°, 1000:1, 16. 5" LG 22MP48D-P Glossy-Black (IPS, LED, 1920x1080, 5 ms, 178°/178°, 250 cd/m, 100M:1 ,+DVI) 22MP48D-P
LG Монитор LCD 21. 5" 22MP48D-P черный 22MP48D-P. 7M Color, 5ms, VGA, DVI, Tilt, 2Y, Black OK 22MP48D-P
LCD LG 21. 5" 16x9 LG Монитор LG 21. ARUZ
IDS_Monitors LG LG 22" LCD 22MP48D 22MP48D-P
21. 7кг 22MP48D-P. 5" 22MP48D-P черный IPS LED 5ms 16:9 DVI матовая 250cd 1920x1080 D-Sub FHD 2. 5" LG 22MP48D-P [Black]; 5ms; 1920x1080, DVI, IPS ARUZ
Монитор 21.

Как было

Сопоставлением занималась 1С (сторонний платный модуль). Что касается удобства / скорости / точности, то такая система позволяла поддерживать каталог с 60 тысячами товаров в наличии на этом уровне силами 6 человек. То есть, каждый день устаревало и исчезало из предложений поставщиков столько сопоставленных товаров, сколько создавалось новых. Очень примерно — 0,5% от размеров каталога, т.е. 300 товаров.

Как стало: общее описание подхода

Чуть выше я привел пример того, что нам нужно сопоставлять. Исследуя тему сопоставления, я был немного удивлен, что ElasticSearch пользуется популярностью для задачи сопоставления, по-моему, у него есть концептуальные ограничения. Что касается нашего стека технологий, то для хранения данных мы используем MS SQL Server, но сопоставление работает на собственной инфраструктуре, а поскольку данных много и нужно обрабатывать их быстро, применяем оптимизированные под конкретную задачу структуры данных и стараемся без необходимости не обращаться к диску, базе и прочим медленным системам.

Поэтому основная идея состоит в том, чтобы попытаться комбинировать эти способы, ранжировать их по точности и быстроте и применять по убыванию точности с учетом скорости. Очевидно, задачу сопоставления можно решать многими способами и очевидно, ни один из них не даст стопроцентную точность.

План выполнения каждого нашего алгоритма (с оговоркой про вырожденные случаи) можно кратко представить такой общей последовательностью:

Разбиваем исходную строку на что-то значащие самостоятельные части. Токенизация. Можно сделать один раз и дальше использовать во всех алгоритмах.

По-хорошему, нужно слова естественного языка привести к общему числу и склонению, а идентификаторы типа “АВС15МХ” (это кириллица, если что) конвертировать в латиницу. Нормализация токенов. И привести все к одному регистру.

Пытаемся понять, что каждая из частей значит. Категоризация токенов. Например, можно выделить категорию, производителя, цвет и так далее.

Поиск лучшего кандидата на совпадение.

Оценка вероятности того, что исходная строка и лучший кандидат действительно обозначают один и тот же товар.

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

Тут мы поступили просто, разбиваем строку на части по специальным символам типа пробела, слэша и так далее. Токенизация. Набор символов со временем получился значительным, но ничего сложного в самом алгоритме мы не применяли.

Конвертируем их в нижний регистр. Потом нам нужно нормализовать токены. Ещё у нас есть небольшой словарь, и мы переводим наши токены на английский. Вместо того, чтобы приводить все к именительному падежу, мы просто отрезаем окончания. Где не сумели перевести, меняем похожие по написанию кириллические символы на латиницу. Помимо прочего, перевод избавляет нас от синонимов, похожие по смыслу русские слова на английский переводятся одинаково. Даже там, где не ждешь подвоха, например, в строке “Samsung UЕ43NU7100U” кириллическая Е вполне может встретиться). (Совсем не лишнее, как оказалось.

image

Мы можем выделить категорию, производителя, модель, артикул, EAN, цвет. Категоризация токенов. У нас есть данные о конкурентах, которые нам предоставляют торговые площадки. У нас есть каталог, где данные структурированы. Мы можем исправить ошибки или опечатки, например, производителя или цвет, которые встречаются только один раз во всех наших источниках, не считать за производителя и цвет соответственно. При их обработке, где возможно, мы структурируем данные. Теоретически, можно иметь открытый список категорий и какой-то умный алгоритм классификации, но и наш базовый подход работает хорошо, а категоризация не является узким местом. Как следствие, у нас есть большой словарь возможных производителей, моделей, артикулов, цветов, и категоризация токена — это просто поиск по словарю за O(1).

Тогда мы сохраняем структуру данных, а эвристически делим токены на категории только из неструктурированных данных. Тут следует оговориться, что иногда поставщик предоставляет уже структурированные данные, например, артикул находится в отдельной ячейке в таблице, или поставщик при оптовых продажах делает скидку от розницы, а розничные цены можно получить в yml (xml) формате.

А теперь о том, какие алгоритмы и в каком порядке применяем.

Точные и почти точные совпадения

Самый простой случай. Строки разбили на токены, привели их к одному виду. Дальше придумали хэш-функцию, не чувствительную к порядку токенов. К тому же, сопоставляя по хэшу, мы можем держать все данные в памяти, условные 16 мегабайт на словарь с миллионом ключей мы можем себе позволить. На практике алгоритм показал себя лучше, чем простое сравнение строк.

Что касается хэширования, то напрашивается применение «исключающего или» и функция типа такой:

public static long GetLongHashCode(IEnumerable<string> tokens)
{ long hash = 0; foreach (var token in tokens.Distinct()) { hash ^= GetLongHashCode(token); } return hash;
}

Самое интересное на этом этапе — получение хэша отдельной строки. На практике выяснилось, что 32 бита мало, получается много коллизий. А еще — что нельзя просто взять исходный код функции из фреймворка и поменять тип возвращаемого значения, коллизий для отдельных строк становится меньше, но после «исключающего или» они все равно встречаются, поэтому мы написали свою. По сути, просто добавили в функцию из фреймворка нелинейности от входных данных. Стало однозначно лучше, с новой функцией с коллизией мы встретились только один раз на наших миллионах записей, записали и отложили до лучших времен.

Работает такой поиск за O(1). Таким образом, мы ищем совпадения без учета порядка слов и их формы.

С такими вещами мы тоже научились справляться, но об этом позже. К сожалению, редко, но бывает и так: “ABC 42 Type 16” и “ABC 16 Type 42”, и это два разных товара.

Сопоставление подтвержденных человеком товаров

У нас есть товары, сопоставленные вручную (чаще всего это товары, сопоставленные автоматически, но попавшие на ручную проверку). По сути, то же самое мы делаем и в этом случае, только у нас теперь добавился словарь сопоставленных хэшей, поиск по которому не изменил временную сложность алгоритма.

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

Сопоставление по атрибутам

Первые два алгоритма быстрые и точные, но их недостаточно. Следующим мы применяем сопоставление по атрибутам.

В этой главе я называю категории токенов атрибутами. Раньше мы уже представили данные в виде нормализованных токенов и даже отсортировали их по категориям.

Совпадения EAN дают почти стопроцентную гарантию того, что это один и тот же продукт. Самый надежный атрибут — это EAN (https://ru.wikipedia.org/wiki/European_Article_Number). Все бы хорошо, но в наших данных EAN встречается редко, поэтому его влияние на сопоставление на уровне погрешности. Несовпадение EAN, при этом, ничего не говорит, потому что у одного товара могут быть разные EAN.

Туда часто попадает что-то странное прямо из структурированных данных поставщика, но в любом случае на данном этапе мы его используем. Артикул менее надежен.

Хэширование позволяет производить все операции в памяти. Как и на прошлом этапе, тут мы используем словари (поиск за O(1)), а в качестве ключа используется хэш от (производитель + модель + артикул). При этом учитываем еще и цвет, если он совпадает или его нет, то считаем, что товары совпали.

Поиск наилучшего совпадения

Предыдущие этапы были простыми, быстрыми и довольно надежными, но, к сожалению, они покрывают меньше половины сопоставлений.

Токены, содержащие цифры, ценятся больше, чем буквенные. В поиске наилучшего совпадения лежит простая идея: совпадение редких токенов имеет большой вес, совпадение частых — малый вес. Длинные совпадения лучше коротких. Токены, которые совпадают в одинаковом порядке, ценятся больше, чем токены, которые переставлены местами.

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

Теперь второй уровень. Мы придумали представить наш каталог в виде словаря словарей, на первом уровне ключом будет хэш от производителя (данные в каталоге структурированы, производителя мы знаем), значением — словарь. Причем в данном случае мы используем в том числе комбинации токенов в том порядке, в котором они встречаются в нашем каталоге. Ключом на втором уровне будет хэш от токена, значением — список id товаров из каталога, где этот токен встречается. На рисунке я упрощенно изобразил эту структуру, без хэшей и без нормализации. Что использовать как комбинацию, а что — нет решаем в зависимости от количества токенов, их длины и так далее, это компромисс между скоростью, точностью и требуемой памятью.

image

Различных токенов при этом будет не более чем в 20 раз больше, чем товаров в каталоге. Если на каждый товар в среднем использовать 20 токенов, то в наших списках, которые значения вложенного словаря, ссылка на товар встретится в среднем 20 раз. Итого — 480 мегабайт. Приблизительно можно посчитать память, которая требуется на каталог в миллион записей: 20 миллионов ключей по 4 байта на каждый, 20 миллионов id товаров по 4 байта на каждый, накладные расходы на организацию словарей и списков (порядок такой же, но поскольку размер списков и словарей мы не знаем заранее, а увеличиваем на ходу, умножаем на два). Что приемлемо, возможности современного железа позволяют одновременно хранить в памяти больше сотни каталогов такого размера. В реальности получилось чуть больше токенов на товар, и нам требуется до 800 мегабайт на каталог в миллион товаров.

Имея строку, которую нам надо сопоставить, мы можем определить производителя (у нас есть алгоритм категоризации), а потом получить токены по такому же алгоритму, что и для товаров из каталога. Вернемся к алгоритму. Тут я подразумеваю в том числе комбинации токенов.

Для каждого токена мы можем быстро найти все товары, в которых он встречается, оценить вес каждого совпадения с учетом всего того, о чем мы говорили раньше — длины, частоты, наличия цифр или спецсимволов, и оценить “похожесть” всех найденных кандидатов. Дальше все сравнительно просто. В реальности тут тоже есть оптимизации, всех кандидатов мы не считаем, сначала формируем небольшой список по совпадениям токенов с большим весом, а совпадения токенов с малым весом применяем не ко всем товарам, а только к этому списку.

Дальше у нас есть два пороговых значения П1 и П2, П1 < П2. Выбираем лучшее соответствие, смотрим совпадения токенов, которые получилось отнести к категориям и считаем оценку сопоставления. Если между двух значений — предлагаем посмотреть сопоставление вручную, до этого оно не будет участвовать в формировании цен. Если оценка получилась больше порогового значения П2 — участия человека не требуется, все происходит автоматически. Если меньше П1 — скорее всего, такого товара нет в каталоге, ничего не возвращаем.

Решение на удивление простое — если у нескольких товаров одинаковые хэши, то мы их не сопоставляем по хэшу. Вернемся к строкам “ABC 42 Type 16” и “ABC 16 Type 42”. Теоретически, такие строки в прайсе поставщика нельзя сопоставить чему-то произвольному, где числа 16 и 42 не встречаются вообще. А последний алгоритм учтет порядок токенов. Практически с такой необходимостью мы не сталкивались.

Скорость и точность сопоставления

Теперь что касается скорости всего этого. Время, которое требуется на подготовку словарей, линейно зависит от размеров каталога. Время, которое требуется непосредственно на сопоставление, линейно зависит от количества сопоставляемых товаров. Все структуры данных, участвующие в поиске, не изменяются после создания. Это дает нам возможность использовать многопоточность на этапе сопоставления. Подготовительная работа для каталога в миллион записей занимает порядка 40-80 секунд. Сопоставление работает со скоростью 20-40 тысяч записей в секунду и не зависит от размера каталога. Потом, правда, нужно сохранить результаты. Выбранный подход в целом выгодный для больших объемов, но файл с десятком записей будет обрабатываться непропорционально долго. Поэтому мы используем кэш и пересчитываем наши поисковые структуры раз в 15 минут.

Так что итоговое число 2-4 тысячи записей в секунду. Правда, данные для сопоставления нужно где-то прочитать (чаще всего это эксель файл), а сопоставленные предложения нужно где-то сохранить, и это тоже занимает время.

Алгоритм после каждого изменения тестировался на этих данных. Для того, чтобы оценить точность, мы подготовили тестовый набор из примерно 20 000 проверенных вручную сопоставлений разных поставщиков из разных категорий. Результаты получились такие:

  • товар есть в каталоге и был сопоставлен правильно — 84%
  • товар есть в каталоге, но не был сопоставлен, требуется ручное сопоставление — 16%
  • товар есть в каталоге и был сопоставлен неправильно — 0.2%
  • товара нет в каталоге, и программа это правильно определила — 98.5%
  • товара нет в каталоге, но программа его сопоставила какому-то из товаров — 1.5%

В 80% случаев, когда товар был сопоставлен, не требуется ручное подтверждение (автоматически подтверждаем сопоставление), среди таких подтвержденных автоматически предложений 0.1% ошибок.

1% ошибок — это много, оказывается. Кстати, 0. А много это потому что покупатели именно такие записи находят лучше всего. На миллион сопоставленных записей это тысяча записей, сопоставленных неправильно. Впрочем, эта тысяча ошибок — это на старте работы на миллионе предложений, постепенно они были исправлены. Ну как не заказать трактор по цене фары от этого трактора. Карантин для подозрительных цен, который закрывает этот вопрос, у нас появился позже, первые пару месяцев мы работали без него.

Отчасти поэтому мы не учитываем цену в сопоставлении. Есть еще одна категория ошибок, не связанная с сопоставлением, это неправильные цены наших поставщиков. Мы решили, что раз уж у нас есть дополнительная информация в виде цены, то будем с ее помощью пытаться определить не только свои, но и чужие ошибки.

Поиск неправильных цен

Это та часть, которая над которой мы сейчас активно экспериментируем. Базовая версия есть, и она не позволяет продавать телефон по цене чехла, но у меня есть ощущение, что можно лучше.

В зависимости того, какие данные есть, мы учитываем цены поставщиков на этот товар, цены конкурентов, цены поставщиков на товары этого производителя в этой категории. Для каждого товара находим границы приемлемых цен поставщиков. Вручную можно отметить такую подозрительную цену как нормальную, тогда мы для данного товара запоминаем это и пересчитываем границы приемлемых цен. Те цены, которые не попали в границы, мы помещаем в карантин и игнорируем во всех наших алгоритмах.

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

Анализ формы распределения — как раз место, где, как мне кажется, можно улучшить алгоритм. Мы в расчетах используем медианные значения (средние дают худший результат) и пока не анализируем форму распределения.

Работа с базой данных

Из всего выше написанного можно сделать вывод, что данные по поставщикам и конкурентам мы обновляем часто и помногу, и работа с базой может стать узким местом. В принципе, мы изначально обратили на это внимание и попытались добиться максимальной производительности. При работе с большим количеством записей мы делаем следующее:

  • удаляем индексы из таблицы, с которой работаем
  • отключаем full text индексирование по этой таблице
  • удаляем все записи с определенным условием (например, все предложения конкретных поставщиков, которых обрабатываем в данный момент)
  • вставляем новые записи при помощи BULK COPY
  • заново создаем индексы
  • включаем full text индексирование

Bulk copy работает со скоростью 10-40 тысяч записей в секунду, почему такой большой разброс еще предстоит выяснить, но и так приемлемо.

Еще некоторое время требуется на пересоздание индексов. Удаление записей занимает примерно такое же время, что и вставка.

Их мы создаем на лету. Кстати, для каждого каталога у нас отдельная база данных. А сейчас расскажу, почему у нас больше одного каталога.

В чем проблема ведения каталога

А их тоже много. Сейчас будем перечислять:

  • В каталоге примерно 400 тысяч товаров из совершенно разных категорий. Невозможно профессионально разбираться в каждой из категорий.
  • Нужно соблюдать определенную стилистику, следовать общими для всего каталога правилами наименования, выделения подкатегорий и так далее. Таким образом мы пытаемся добиться стройной и логичной структуры каталога.
  • Один и тот же товар можно создать несколько раз, и это проблема. Без инструмента, который анализирует похожие наименования, создание дублей происходит постоянно.
  • Разумно добавлять в каталог те товары, которые есть у поставщиков в наличии. При этом нужно иметь приоритеты на товарные категории.
  • Нам требуется несколько каталогов. Один свой, его мы ведем сами, другой — каталог агрегатора, его обновляем по апи. Смысл второго каталога в том, что площадка-агрегатор работает только с собственным каталогом, и, соответственно, предложения принимает в своей номенклатуре. Это еще одно место, где оказалось нужно сопоставление.

Мы посчитали, что логично и правильно вести каталог там же, где осуществляются сопоставления. Так мы сможем подсказать пользователям, которые администрируют каталог, что есть у поставщика, но нет у нас в каталоге.

Как мы ведем каталог

Речь пойдет о каталоге без подробных характеристик, характеристики — отдельная большая история, о ней как-нибудь в другой раз.

В качестве базовых свойств мы выбрали такие:

  • производитель
  • категория
  • модель
  • артикул
  • цвет
  • EAN

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

Как работает поиск

Удобство управления каталогом, в первую очередь — это возможность быстро найти товар в каталоге или предложение поставщика, причем есть нюансы. Например, нужно уметь искать строчку “LG 21.5" 22MP48D-P (16:9, IPS, VGA, DVI) 22MP48D-P” по запросу “2MP48”.

Полнотекстовый поиск sql сервера из коробки не годится, потому что он так не умеет, а поиск с помощью LIKE ‘%2MP48%’ слишком медленный.

Если точнее — то триграммы. Наше решение достаточно стандартно, мы используем N-граммы. Не уверен, что мы очень рационально используем место в этом случае, но по скорости такое решение подошло, в зависимости от запроса, работает от 50 до 500 миллисекунд, иногда до секунды на массиве в три миллиона записей. А уже по триграммам строим полнотекстовый индекс и производим полнотекстовый поиск.

5" 22MP48D-P (16:9, IPS, VGA, DVI) 22MP48D-P” преобразуется в строку “lg2 g21 215 152 522 22m 2mp mp4 p48 48d 8dp dp1 p16 169 69i 9ip ips psv svg vga gad adv dvi vi2 i22”, которая сохраняется в отдельное поле, которое участвует в полнотекстовом индексе. Поясню, строка “LG 21.

Кстати, триграммы нам еще пригодятся.

Создание нового товара

В большинстве своем, товары в каталоге создаются по предложениям поставщика. То есть, у нас уже есть информация, что поставщик предлагает “LG Монитор LCD 21.5'' [16:9] 1920х1080(FHD) IPS, nonGLARE, 250cd/m2, H178°/V178°, 1000:1, 16.7M Color, 5ms, VGA, DVI, Tilt, 2Y, Black OK 22MP48D-P” по цене 120 долларов, и на складе у него от 5 до 10 единиц.

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

Во-первых, если товар у нас в каталоге есть, с большой вероятностью предложение поставщика будет сопоставлено этому товару автоматически.

(технически это делается с помощью CONTAINSTABLE). Во-вторых, прежде чем показать пользователю форму создания нового товара, мы выполним поиск по триграммам и покажем самые релевантные результаты.

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

Сделаем то же самое и просто не дадим создать товары с одинаковыми хэшами. И в четвертых, помните мы разбивали строки на токены, нормализовали их, считали хэши?

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

Редактирование товара работает почти так же, просто не все применимо.

Как мы формируем свои цены

Задача такая: соблюсти баланс между количеством и маржинальностью продаж, по сути — добиться максимальной прибыли. Все остальные аспекты работы магазина тоже об этом, но именно то, что происходит на этапе формирования цен, оказывает наибольшее влияние.

Еще стоит учесть минимальные розничные и оптовые цены и затраты на доставку, а также финансовые инструменты — кредиты и рассрочки. Как минимум нам понадобится информация о предложениях поставщиков и конкурентов.

Собираем цены конкурентов

Начнем с того, что профилей собственных цен у нас много. Есть профиль для розницы, есть несколько для оптовых клиентов. Все они создаются и настраиваются в нашей системе.

В рознице — другие розничные магазины, в оптовых продажах — наши же поставщики. Соответственно, и конкуренты для каждого профиля разные.

Во-первых, некоторые агрегаторы предоставляют информацию о всех ценах на все товары, которые есть на площадке. С поставщиками все ясно, а для розницы данные о конкурентах собираем несколькими способами. И этого пока почти хватает. В собственной номенклатуре, но мы умеем сопоставлять товары, так что это работает автоматически. Поскольку они пока не автоматизированы и существуют в виде консольных приложений (которые иногда падают), то и пользуемся ими редко. Во-вторых, у нас есть парсеры конкурентов.

Настраимаем профиль

В профиле у нас есть возможность настроить разные диапазоны наценок в зависимости от цены товара у поставщика, категории, производителя, поставщика. Еще есть возможность указать, с какими поставщиками по какой категории или производителю работаем, а с какими — нет, кого из конкурентов учитываем.

Потом настраиваем финансовые инструменты, указываем, какие рассрочки доступны и сколько банк заберет себе.

Это в двух словах так, а на деле я не берусь объяснить простыми словами, что там происходит. И уже в границах наценок мы формируем собственные цены, пытаясь соблюсти тот самый баланс во-первых, и сделать так, чтобы наши складские товары лучше продавались во-вторых.

К сожалению, мы пока не умеем прогнозировать спрос и учитывать затраты на хранение товаров на складе. Могу рассказать, чего не происходит.

Интеграции со сторонними системами

Важная часть с точки зрения бизнеса, но неинтересная с технической точки зрения. В двух словах скажу, что мы умеем отдавать данные в сторонние системы (в том числе инкрементально, то есть, понимаем, что поменялось с прошлого обмена) и умеем делать почтовые рассылки.

Рассылки настраиваемые, так (и не только так) мы доставляем наши предложения оптовым клиентам.

Он пока в активной разработке, заработает буквально через месяц. Еще один способ работать с оптовыми клиентами — b2b портал.

Аккаунты, логирование изменений

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

Если в него влезть (а в нашем случае это EF Core и там даже API есть), то можно получить логирование всего практически в две строчки. Если кратко, то сказать можно следующее: если используется ORM, то в ней есть встроенный механизм отслеживания изменений.

Для истории изменений мы сделали интерфейс, и теперь можно проследить, кто и что менял в настройках системы, кто редактировал или сопоставлял определенные товары и так далее.

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

Немного об общем устройстве системы

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

С готовыми решениями это получалось сложно, плюс это еще одна зависимость, которую надо поддерживать. Кстати, система логирования самописная, нам очень нужно группировать логи, относящиеся к одному запросу или одной тяжелой задаче, кроме того, нужен базовый функционал для их анализа.

NET Core и бутстрапе, а тяжелые опрерации выполняются Windows — сервисом. Веб-интерфейс сделан на ASP.

Полноценный СQRS мы не внедряли, но одну из концепций оттуда взяли. Еще одна особенность, которая пошла проекту на пользу, на мой взгляд, — разные модели на чтение и запись данных. Массовое обновление делаем посредством BULK COPY. Запись в базу у нас через репозитории, но объекты, которые используются для записи, никогда не покидают методов обновления / создания / удаления. Получилось, что можно пользоваться ORM, и при этом избежать тяжелых запросов, обращений к базе в неопределенные моменты (как с ленивой загрузкой), проблемы N + 1. Для чтения сделана отдельная модель и отдельная прослойка доступа к данным, таким образом мы читаем только то, что нужно в конкретный момент. А еще мы используем модель для чтения как DTO.

NET Core, несколько сторонних nuget-пакетов и MS SQL Server. Из крупных зависимостей у нас ASP. Для того, чтобы полностью развернуть проект локально, достаточно установить SQL Server, забрать исходный код из системы контроля версий и собрать проект. Пока возможно, стараемся не зависеть от множества сторонних систем. Возможно, придется изменить одну-две строчки в конфигурации. Нужные базы будут созданы автоматически, а больше ничего и не надо.

Что не сделали

Пока не сделали систему знаний по проекту. Хотим делать вики и подсказки по месту. Не сделали простого интуитивного интерфейса, тот, что есть неплох, но для неподготовленного человека немного запутан. CI/CD пока только в планах.

Тоже планируем, но пока нет конкретного срока. Не сделали обработку подробных характеристик товаров.

image

Итоги с точки зрения бизнеса

С момента начала активной разработки до запуска в продакшен над проектом работало два человека в течение 7 месяцев. На старте у нас был прототип, сделанный в свободное время. Сложнее всего дались интеграции с существующими системами.

Сайт всегда запаздывает, потому что ему нужны характеристики, картинки, описания товаров. За три месяца, которые мы в продакшене, количество товаров в наличии для оптовых клиентов выросло с 70 тысяч до 230 тысяч, количество товаров на сайте — с 60 тысяч до 140 тысяч. Количество человек, работающих с каталогом, не изменилось. На агрегатор мы выгружаем 106 тысяч предложений вместо 40 тысяч три месяца назад.

Отслеживаем цены более тысячи конкурентов. Мы работаем с 425 поставщиками, это число выросло почти в два раза за три месяца. Ну как отслеживаем — система для парсинга у нас есть, но в большинстве случаев мы берем готовые данные у тех, кто их регулярно предоставляет.

Спрос сезонный, и напрямую сравнить месяц к предыдущему месяцу нельзя. Про продажи, к сожалению, не могу рассказать, у меня самого нет надежных данных. Очень и очень условно, плюс-минус километр, рост каталога, более гибкие и конкурентные цены и связанный с этим рост продаж уже окупили разработку и внедрение. А за год произошло слишком много всего, чтобы из всех факторов выделить влияние нашей системы.

Так было задумано с самого начала, и этот план почти сработал. Еще один итог — получился проект, по сути не связанный с инфраструктурой конкретного магазина, и из него можно сделать публичный сервис. Чтобы предлагать проект как сервис, где можно зарегистрироваться, поставить галочку “я согласен”, и который работает “как есть”, без адаптации к клиенту, нужно переработать интерфейс, добавить гибкости и создать вики. Коробочного решения еще, к сожалению, не получилось. Сейчас у нас из средств обеспечения надежности есть только регулярные бекапы. А еще сделать инфраструктуру легко масштабируемой и устранить единую точку отказа. Дело за малым — найти бизнес. Как энтерпрайз решение, думаю, мы уже готовы решать задачи бизнеса.

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

image

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть