Configuring Behavior-Based Personalized Recommendations on 1C-Bitrix
Behavior-based personalized recommendations are a middle ground between "similar products by category" and full-fledged ML. No need for separate Python infrastructure: everything is built from Bitrix database data and a few SQL queries. Works worse than neural networks but many times better than simple "similar products" and requires only PHP + PostgreSQL.
Behavioral Signals and Their Weights
Different user actions have different value as interest signals. Typical scale:
| Event | Weight |
|---|---|
| Product purchase | 10 |
| Add to cart | 5 |
| Add to wishlist | 4 |
| Product view (>30 sec) | 2 |
| Product view (<30 sec) | 1 |
| Search query with transition | 3 |
Weights are stored in configuration, events—in b_user_behavior table (structure described in personalization article). Key field: EVENT_TYPE with values purchase, cart_add, wishlist, view.
Item-Based Collaborative Filtering Without ML
Essence: "users who viewed product A also viewed products B, C, D". This is calculated directly from database without ML model:
SELECT
b2.ENTITY_ID AS recommended_id,
COUNT(DISTINCT b2.USER_ID) AS co_view_count
FROM b_user_behavior b1
JOIN b_user_behavior b2
ON b1.USER_ID = b2.USER_ID
AND b1.ENTITY_ID != b2.ENTITY_ID
AND b2.EVENT_TYPE IN ('view', 'cart_add', 'purchase')
AND b2.DATE_CREATE > NOW() - INTERVAL '60 days'
WHERE
b1.ENTITY_ID = :current_item_id
AND b1.EVENT_TYPE IN ('view', 'cart_add', 'purchase')
GROUP BY b2.ENTITY_ID
ORDER BY co_view_count DESC
LIMIT 20;
This query is executed offline (once per hour via agent) and result is cached in a separate table:
CREATE TABLE b_item_recommendations (
ITEM_ID INT NOT NULL,
RECOMMENDED_ID INT NOT NULL,
SCORE FLOAT NOT NULL,
UPDATED_AT TIMESTAMP DEFAULT NOW(),
PRIMARY KEY (ITEM_ID, RECOMMENDED_ID)
);
CREATE INDEX idx_item_recs_item ON b_item_recommendations(ITEM_ID, SCORE DESC);
Personal Rating for Specific User
From 20 recommendation candidates, select 6–8 most relevant for current user. Use their behavioral profile:
function getPersonalizedRecs(int $itemId, int $userId, int $limit = 8): array {
// 1. Get candidates from item-based table
$candidates = getCandidates($itemId, 20);
if (empty($candidates) || !$userId) {
return array_slice($candidates, 0, $limit);
}
// 2. Get categories from user's history
$userCategoryIds = getUserTopCategories($userId, 10);
// 3. Boosting: raise products from preferred categories
foreach ($candidates as &$candidate) {
$sectionId = getElementSectionId($candidate['id']);
if (in_array($sectionId, $userCategoryIds)) {
$candidate['score'] *= 1.5;
}
}
// 4. Remove already purchased products
$purchased = getUserPurchasedIds($userId);
$candidates = array_filter($candidates,
fn($c) => !in_array($c['id'], $purchased)
);
usort($candidates, fn($a, $b) => $b['score'] <=> $a['score']);
return array_column(array_slice($candidates, 0, $limit), 'id');
}
Caching and Display
Custom component local:catalog.recommendations takes ELEMENT_ID and outputs recommended products. Cache is built on item_id level—not personal, but item-based (same candidates for everyone). Personal boost applied via separate AJAX call after main block loads.
This approach allows caching the main recommendation block at Bitrix level while showing each user personalized order without extra database queries on page render.
History Transfer on Authorization
Bitrix doesn't automatically transfer anonymous user's behavioral history to authorized user. OnAfterUserLogin handler:
AddEventHandler('main', 'OnAfterUserLogin', function($fields) {
$fuserId = \CSaleUser::GetAnonymousUserID();
if (!$fuserId) return;
$DB->Query("
UPDATE b_user_behavior SET USER_ID = " . (int)$fields['USER_ID'] . "
WHERE SESSION_ID = '" . $DB->ForSql(session_id()) . "'
AND USER_ID IS NULL
");
});







