Налаштування рекомендацій товарів на основі історії покупок 1С-Bitrix
Історія покупок — найсильніший поведінковий сигнал. Користувач уже заплатив гроші — це не перегляд, не клік, це підтверджений інтерес. Рекомендації на основі історії покупок працюють за двома патернами: «покупці цього товару також купили» (item-based) та «ваші минулі покупки схожі на покупки користувачів X, вони також взяли Y» (user-based). Обидва патерни реалізуються прямо в Bitrix без зовнішніх ML-сервісів.
Таблиці з даними про покупки
Уся історія замовлень в Bitrix — три ключові таблиці:
-
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;
Цей запит виконується офлайн через агент Bitrix — раз на 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}.







