Мы внедряем персонализированные рекомендации на основе истории покупок в 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 рабочих дней в зависимости от сложности каталога и объёма данных. Точная стоимость рассчитывается после аудита — закажите аудит, и мы оценим проект бесплатно.
Опираясь на опыт десятка внедрений в магазинах с товарооборотом от 50 млн ₽, гарантируем стабильную работу без просадок производительности. Свяжитесь с нами — проведём аудит вашего каталога и рассчитаем точную стоимость. Получите персонализированные рекомендации уже сегодня.







