Настройка рекомендаций товаров на основе истории покупок 1С-Битрикс
История покупок — самый сильный поведенческий сигнал. Пользователь уже заплатил деньги — это не просмотр, не клик, это подтверждённый интерес. Рекомендации на основе истории покупок работают по двум паттернам: «покупатели этого товара также купили» (item-based) и «ваши прошлые покупки похожи на покупки пользователей X, они взяли ещё Y» (user-based). Оба паттерна реализуются прямо в Битриксе без внешних ML-сервисов.
Таблицы с данными о покупках
Вся история заказов в Битриксе — три ключевые таблицы:
-
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}.







