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?
- Analysis of current orders and data structure
- SQL query optimized for your volume
- Pre-calculation agent development
- Component with caching and fallback
- Cart block integration
- Deployment and configuration documentation
- 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.

