Мы внедряем персонализированные рекомендации на основе истории покупок в 1С-Битрикс. Типичная конверсия в повторные продажи — менее 5%. После внедрения нашего подхода показатель вырастает до 20–30%. При этом не требуется подключать внешние ML-сервисы — вся логика реализуется на SQL и PHP силами самого Битрикса, что даёт полный контроль и снижает затраты на внешние подписки.
Большая часть рекомендаций — item-based («с этим товаром часто покупают») и user-based («ваши прошлые покупки похожи на покупки других»). Оба паттерна используют стандартные таблицы Битрикса и оптимизируются индексами. Без правильного индексирования JOIN на таблице b_sale_order_basket в магазине с 500 000 заказов выполняется 30+ секунд; с составным индексом (PRODUCT_ID, ORDER_ID) запрос укладывается в 0,1 секунды. Внутренняя реализация окупается в течение нескольких месяцев за счёт отсутствия ежемесячной платы за ML-сервисы и снижения нагрузки на сервер.
Два основных паттерна рекомендаций
Item-based: «с этим товаром часто покупают». Анализируем совместную встречаемость товаров в заказах. User-based: «ваши прошлые покупки похожи на покупки пользователей X, они взяли ещё Y». Оба паттерна строятся на данных из стандартных таблиц Битрикса.
Таблицы с данными о покупках
Вся история заказов в Битриксе — три ключевые таблицы:
-
b_sale_order— заказы: поляUSER_ID,CANCELED,STATUS_ID,PRICE -
b_sale_order_basket— состав заказов:ORDER_ID,PRODUCT_ID,QUANTITY,PRICE -
b_catalog_product— наличие товара:QUANTITY,AVAILABLE
Для рекомендаций используем только незаменённые заказы (CANCELED = 'N') в финальных статусах. Статус F (Finished) — стандартный финальный, но на многих проектах используются кастомные статусы.
Item-based: «с этим часто покупают»
Основной паттерн — блок «Вместе с этим товаром покупают» на карточке товара:
SELECT ob2.PRODUCT_ID, COUNT(DISTINCT ob1.ORDER_ID) AS co_purchase_count, SUM(ob2.QUANTITY) AS total_qty FROM b_sale_order_basket ob1 JOIN b_sale_order_basket ob2 ON ob1.ORDER_ID = ob2.ORDER_ID AND ob2.PRODUCT_ID != ob1.PRODUCT_ID JOIN b_sale_order o ON o.ID = ob1.ORDER_ID AND o.CANCELED = 'N' AND o.DATE_INSERT > NOW() - INTERVAL '90 days' WHERE ob1.PRODUCT_ID = :target_product_id GROUP BY ob2.PRODUCT_ID ORDER BY co_purchase_count DESC LIMIT 20; Этот запрос выполняется офлайн через агент Битрикса — раз в 4 часа. Результат пишется в таблицу:
CREATE TABLE b_product_cross_sell ( SOURCE_ID INT NOT NULL, RECOMMENDED_ID INT NOT NULL, SCORE INT NOT NULL, UPDATED_AT TIMESTAMP DEFAULT NOW(), PRIMARY KEY (SOURCE_ID, RECOMMENDED_ID) ); CREATE INDEX idx_cross_sell_source ON b_product_cross_sell(SOURCE_ID, SCORE DESC); Индекс (PRODUCT_ID, ORDER_ID) на b_sale_order_basket критически важен — без него JOIN на больших магазинах (100k+ заказов) будет выполняться секундами.
User-based: персональные рекомендации для авторизованного пользователя
Для конкретного пользователя строится список товаров, которые купили «похожие» покупатели. «Похожесть» — пересечение истории покупок.
function getUserBasedRecs(int $userId, int $limit = 8): array { // 1. История покупок текущего пользователя $myOrderIds = array_column( \Bitrix\Sale\OrderTable::getList([ 'filter' => ['USER_ID' => $userId, 'CANCELED' => 'N'], 'select' => ['ID'], ])->fetchAll(), 'ID' ); if (empty($myOrderIds)) return getPopularItems($limit); $myProductIds = array_column( \Bitrix\Sale\Internals\BasketTable::getList([ 'filter' => ['ORDER_ID' => $myOrderIds], 'select' => ['PRODUCT_ID'], ])->fetchAll(), 'PRODUCT_ID' ); // 2. Пользователи, купившие те же товары // 3. Товары этих пользователей, которых у нас нет $res = $GLOBALS['DB']->Query(" SELECT ob2.PRODUCT_ID, COUNT(DISTINCT o2.USER_ID) AS score FROM b_sale_order_basket ob1 JOIN b_sale_order o1 ON o1.ID = ob1.ORDER_ID AND o1.USER_ID = {$userId} JOIN b_sale_order_basket ob2 ON ob2.ORDER_ID IN ( SELECT DISTINCT o3.ID FROM b_sale_order o3 JOIN b_sale_order_basket ob3 ON ob3.ORDER_ID = o3.ID AND ob3.PRODUCT_ID IN (" . implode(',', array_map('intval', $myProductIds)) . ") WHERE o3.USER_ID != {$userId} AND o3.CANCELED = 'N' ) WHERE ob2.PRODUCT_ID NOT IN (" . implode(',', array_map('intval', $myProductIds)) . ") GROUP BY ob2.PRODUCT_ID ORDER BY score DESC LIMIT {$limit} "); $ids = []; while ($row = $res->Fetch()) $ids[] = (int)$row['PRODUCT_ID']; return $ids; } Фильтрация рекомендованных товаров
Рекомендованные ID передаются в финальный фильтр перед отображением — убрать неактивные, снятые с продажи, с нулевым остатком:
$availableIds = \CIBlockElement::GetList( ['SORT' => 'ASC'], [ 'ID' => $recommendedIds, 'ACTIVE' => 'Y', 'IBLOCK_ID' => CATALOG_IBLOCK_ID, '>CATALOG_QUANTITY' => 0, ], false, ['nTopCount' => 8], ['ID'] )->fetchAll(); Кеш и инвалидация
Кеш item-based рекомендаций: по PRODUCT_ID, TTL = 4 часа (синхронно с агентом обновления). Кеш user-based: по USER_ID, TTL = 30 минут — короче, потому что история пользователя меняется. Инвалидация: при сохранении нового заказа (OnSaleOrderSaved) сбрасывать кеш для всех товаров из заказа через тег product_recs_{id}.
Преимущества внутренней реализации перед внешними ML-сервисами
Готовые сервисы (Recombee, Nosto) требуют ежемесячной оплаты и интеграции REST API. Внутренняя реализация на Битриксе:
- не требует внешних зависимостей,
- работает быстрее, так как данные уже в БД,
- даёт полный контроль над алгоритмом,
- легко модифицируется под специфику каталога.
| Параметр | Item-based | User-based |
|---|---|---|
| Принцип | «с этим товаром часто берут» | «люди с похожей историей купили» |
| Данные | Совместные покупки в заказах | Пересечение пользовательских корзин |
| Обновление | Раз в 4 часа агент | Онлайн при запросе (кеш 30 мин) |
| Когда эффективен | Товар имеет связанные позиции | У пользователя есть история покупок |
Типичные проблемы с производительностью и их решения
| Проблема | Причина | Решение |
|---|---|---|
| JOIN выполняется минуты | Отсутствует индекс (PRODUCT_ID, ORDER_ID) на b_sale_order_basket |
Создать составной индекс |
| Кеш устаревает некорректно | Нет инвалидации по событию | Добавить обработчик OnSaleOrderSaved с тегированной очисткой |
| User-based медленный для новых пользователей | Отсутствие истории | Использовать item-based или популярные товары как fallback |
Почему индекс (PRODUCT_ID, ORDER_ID) критичен?
Без этого составного индекса JOIN по b_sale_order_basket в магазине с 100 000 заказов выполняется 10–30 секунд. Софтверные решения вроде временных таблиц не экономят ресурсы. Индекс сокращает время до 0,05–0,1 секунды, что критично для агента, выполняющегося раз в 4 часа.
Как обеспечивается актуальность рекомендаций?
Item-based пересчитываются раз в 4 часа, user-based — при каждом запросе с кешем на 30 минут. Дополнительно при создании нового заказа сбрасывается кеш для затронутых товаров. Это гарантирует, что пользователь видит свежие рекомендации, а нагрузка на сервер остаётся низкой.
Что входит в нашу работу
- Аудит текущей структуры данных и нагрузок на БД.
- Реализация агентов расчёта item-based и user-based.
- Создание таблицы
b_product_cross_sellи необходимых индексов. - Интеграция фильтрации по активности и остаткам.
- Настройка тегированного кеширования и инвалидации по событиям.
- Документация по архитектуре и инструкция по деплою.
- Обучение вашего разработчика поддержке.
Сроки и стоимость
Срок реализации — от 3 до 7 рабочих дней в зависимости от сложности каталога и объёма данных. Точная стоимость рассчитывается после аудита — закажите аудит, и мы оценим проект бесплатно.
Опираясь на опыт десятка внедрений в магазинах с товарооборотом от $450k–650k, гарантируем стабильную работу без просадок производительности. Свяжитесь с нами — проведём аудит вашего каталога и рассчитаем точную стоимость. Получите персонализированные рекомендации уже сегодня.







