Setting up personalized recommendations based on 1C-Bitrix behavior

Our company is engaged in the development, support and maintenance of Bitrix and Bitrix24 solutions of any complexity. From simple one-page sites to complex online stores, CRM systems with 1C and telephony integration. The experience of developers is confirmed by certificates from the vendor.
Our competencies:
Development stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1212
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Website development for FIXPER company
    815
  • image_bitrix-bitrix-24-1c_development_of_an_online_appointment_booking_widget_for_a_medical_center_594_0.webp
    Development based on Bitrix, Bitrix24, 1C for the company Development of an Online Appointment Booking Widget for a Medical Center
    565
  • image_bitrix-bitrix-24-1c_mirsanbel_458_0.webp
    Development based on 1C Enterprise for MIRSANBEL
    747
  • image_crm_dolbimby_434_0.webp
    Website development on CRM Bitrix24 for DOLBIMBY
    657
  • image_crm_technotorgcomplex_453_0.webp
    Development based on Bitrix24 for the company TECHNOTORGKOMPLEKS
    980

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
    ");
});