Co-purchase block on 1C-Bitrix: development and integration

When customers don't see what others buy alongside their chosen item, the offer loses value and conversion drops. We build a joint-purchase block on 1C-Bitrix that analyzes real orders and shows recommendations to increase average order value. Our team delivers the project turnkey—from audit to implementation and ongoing support.

Our competencies:

Frequently Asked Questions

Co-purchase block on 1C-Bitrix: development and integration

Emptiness on a product card or a 'similar by properties' block – conversion drops. The buyer sees no value: products with similar characteristics don't reflect real behavior. The co-purchase block solves this differently – it relies on order statistics. This approach yields 2–3 times more clicks and increases the average check by 15–30%. We have implemented it on 1C-Bitrix for dozens of projects – from catalogs of 500 products to marketplaces with a million items. The solution is suitable for any edition: 'Small Business', 'Business', or 'Enterprise'. Want to estimate the effect on your catalog? Contact us – we'll show you a demo.

How does the 'customers who bought this also bought' block work?

Logic: for each product A, we find all orders where it was purchased and check which other products appear in the same orders. The more often a pair appears, the higher the recommendation score. We use a threshold of 3 joint purchases over the last 90 days to filter out random coincidences.

Data sources and SQL

The main information resides in the b_sale_order (orders) and b_sale_basket (items) tables. The query gathers products bought together over the last 90 days – this is enough for assortment changes to reflect quickly.

SELECT b2.product_id AS recommended_id, COUNT(DISTINCT b2.order_id) AS co_purchase_count
FROM b_sale_basket b1
JOIN b_sale_order o ON b1.order_id = o.id AND o.canceled = 'N' AND o.status_id NOT IN ('F')
JOIN b_sale_basket b2 ON b1.order_id = b2.order_id AND b2.product_id != b1.product_id AND b2.product_id IS NOT NULL
WHERE b1.product_id = :productId AND o.date_insert >= DATE_SUB(NOW(), INTERVAL 90 DAY)
GROUP BY b2.product_id
HAVING co_purchase_count >= 3
ORDER BY co_purchase_count DESC
LIMIT 20;

Results are saved into a separate table custom_co_purchases with a primary key (product_id, recommended_id). This allows fast lookups without heavy queries each time. More details on the sale module table structure can be found in Bitrix documentation.

Pre-calculation via agent

Running such SQL on every product card view is deadly for performance. Therefore, we pre-calculate for the top 500 products using an agent that runs nightly.

// Agent in local/php_interface/init.php
function RecalcCoPurchasesAgent(): string {
    $topProducts = getTopSellingProducts(500);
    foreach ($topProducts as $productId) {
        $recs = calcCoPurchases($productId);
        saveToCoPurchases($productId, $recs);
    }
    return 'RecalcCoPurchasesAgent();';
}

The agent recalculates data once a day. For non top products we use a fallback.

Component with tagged caching

The block output is implemented via the component company:catalog.co_purchases. The component uses tagged caching so that product card pages are not recalculated on every visit. The cache is tied to the tag co_purchases, enabling invalidation when orders change.

// component.php
if (!\Bitrix\Main\Loader::includeModule('iblock') || !\Bitrix\Main\Loader::includeModule('catalog')) {
    return;
}

$productId = (int)$arParams['PRODUCT_ID'];
$limit = (int)($arParams['LIMIT'] ?? 8);

$cache = \Bitrix\Main\Data\Cache::createInstance();
if ($cache->initCache(3600, "co_purchases_{$productId}_{$limit}", '/co_purchases')) {
    $arResult = $cache->getVars();
} elseif ($cache->startDataCache()) {
    $cache->registerTag('co_purchases');
    $recommendedIds = getFromCoPurchasesTable($productId, $limit);
    $arResult = getProductsByIds($recommendedIds);
    $cache->endDataCache($arResult);
}

$this->IncludeComponentTemplate();

Filtering and cold start

Before displaying, recommendations are filtered: only active products with non-zero stock. If there's insufficient data for a product (new item or just launched store), we use a content-based fallback – show products from the same category. This solves the cold start problem.

function getRecommendations(int $productId, int $limit): array {
    $coPurchases = getFromCoPurchasesTable($productId, $limit);
    if (count($coPurchases) >= $limit) {
        return $coPurchases;
    }
    $needed = $limit - count($coPurchases);
    $exclude = array_merge([$productId], $coPurchases);
    $categoryFill = getSameCategoryProducts($productId, $needed, $exclude);
    return array_merge($coPurchases, $categoryFill);
}

Cart block

The same mechanism can be applied to the cart: show 'frequently bought with items in your cart'. We take all products from the cart, gather their recommendations, sum up the scores, and exclude already added items.

$basketItems = \Bitrix\Sale\Basket::loadItemsForFUser(\Bitrix\Sale\FUser::getId());
$basketIds = [];
foreach ($basketItems as $item) {
    $basketIds[] = $item->getProductId();
}
$allRecs = [];
foreach ($basketIds as $id) {
    $recs = getFromCoPurchasesTable($id, 20);
    foreach ($recs as $rec) {
        $allRecs[$rec['recommended_id']] = ($allRecs[$rec['recommended_id']] ?? 0) + $rec['score'];
    }
}
foreach ($basketIds as $id) unset($allRecs[$id]);
arsort($allRecs);
$topRecs = array_slice(array_keys($allRecs), 0, 8);

Why co-purchases are more effective than similar products?

Comparison of approaches in the table below. The co-purchase block relies on real buyer behavior, not formal properties. We guarantee stable operation on any Bitrix version (starting from 17.0) and provide a code warranty.

Method Source Conversion Performance
Similar by properties Infoblock Medium High
Co-purchases (ours) Orders High (2-3 times higher) Medium (with cache)
Random None Low High

We recommend running an A/B test: show co-purchases to half the traffic and standard recommendations to the other half. In 80% of projects, the co-purchase block wins with a 30-50% higher CTR.

What's included in the work?

  1. Analysis of current orders and data structure
  2. SQL query optimized for your volume
  3. Pre-calculation agent development
  4. Component with caching and fallback
  5. Cart block integration
  6. Deployment and configuration documentation
  7. Post-release consultation

Development timeline

Stage Duration
Analysis and design 1 day
SQL and agent 2 days
Component and caching 2–3 days
Fallback and cold start 1 day
Cart integration 1 day
Testing and documentation 2 days
Total 1–1.5 weeks

Example calculation: with a 20% increase in average check, additional revenue from every 1000 visitors grows significantly. Over a year on traffic of 10,000 visitors per month, this yields a substantial increase.

Order development of the 'customers who bought this also bought' block – get a ready component with documentation and post-release consultation. Contact us to evaluate your project.