Ми впроваджуємо персоналізовані рекомендації на основі історії покупок у 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 млн грн, гарантуємо стабільну роботу без просадок продуктивності. Зв'яжіться з нами — проведемо аудит вашого каталогу та розрахуємо точну вартість. Отримайте персоналізовані рекомендації вже сьогодні.







