Development of a product selection form for 1C-Bitrix

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
    1177
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Website development for FIXPER company
    811
  • 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
    564
  • 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
    655
  • image_crm_technotorgcomplex_453_0.webp
    Development based on Bitrix24 for the company TECHNOTORGKOMPLEKS
    976

Product Selection Form Development on 1C-Bitrix

A product selection form is an interactive assistant that asks users questions about their needs and, as a result, suggests specific products from the catalog. Used where the assortment is large or products are technically complex: choosing a laptop, tires, paint, or industrial equipment. In essence, it is a recommendation system disguised as a questionnaire. The key technical challenge is the algorithm for matching answers to products in the infoblock.

Two Approaches to Selection

1. Direct filtering. Each user answer = a filter by an infoblock property. Result = intersection of all filters. Simple and predictable, but works poorly when the user gave "imprecise" answers — the result can be empty.

2. Scoring. Each answer option adds points to specific products or categories. Products are ranked by total score. Better for recommendations, more complex to configure.

For most catalogs — a combination: mandatory filters (e.g., budget) + scoring by additional criteria.

Data Structure: Mapping Questions to Properties

// Selector configuration (stored in b_option or an HL-block)
$selectorConfig = [
    'iblock_id' => CATALOG_IBLOCK_ID,
    'questions' => [
        [
            'id'     => 'q_budget',
            'text'   => 'Your budget',
            'type'   => 'select',
            'filter_mode' => 'range',  // Filter type
            'options' => [
                ['value' => 'low',  'label' => 'under $300',      'filter' => ['<CATALOG_PRICE_1' => 300]],
                ['value' => 'mid',  'label' => '$300–$600',        'filter' => ['>=CATALOG_PRICE_1' => 300, '<CATALOG_PRICE_1' => 600]],
                ['value' => 'high', 'label' => 'over $600',        'filter' => ['>=CATALOG_PRICE_1' => 600]],
            ],
        ],
        [
            'id'     => 'q_purpose',
            'text'   => 'What tasks is it for',
            'type'   => 'radio',
            'filter_mode' => 'property',
            'property_code' => 'PURPOSE',  // Property code in the infoblock
            'options' => [
                ['value' => 'work',   'label' => 'Work / office', 'property_value' => 'WORK'],
                ['value' => 'gaming', 'label' => 'Gaming',        'property_value' => 'GAMING'],
                ['value' => 'study',  'label' => 'Study',         'property_value' => 'STUDY'],
            ],
        ],
        [
            'id'     => 'q_weight',
            'text'   => 'Is the device weight important?',
            'type'   => 'radio',
            'filter_mode' => 'scoring',
            'options' => [
                ['value' => 'yes', 'label' => 'Yes, I carry it with me', 'scores' => ['lightweight' => 10]],
                ['value' => 'no',  'label' => 'No, it stays at my desk', 'scores' => ['performance' => 5]],
            ],
        ],
    ],
];

Filtering and Scoring Algorithm

class ProductSelector
{
    private array $config;

    public function findProducts(array $answers): array
    {
        $hardFilters  = ['IBLOCK_ID' => $this->config['iblock_id'], 'ACTIVE' => 'Y'];
        $scoringTags  = []; // tag => score

        foreach ($this->config['questions'] as $question) {
            $answer = $answers[$question['id']] ?? null;
            if ($answer === null) continue;

            $selectedOption = null;
            foreach ($question['options'] as $opt) {
                if ($opt['value'] === $answer) {
                    $selectedOption = $opt;
                    break;
                }
            }

            if (!$selectedOption) continue;

            switch ($question['filter_mode']) {
                case 'range':
                case 'property':
                    // Add to hard filters
                    $hardFilters = array_merge($hardFilters, $selectedOption['filter'] ?? []);
                    if (isset($question['property_code'], $selectedOption['property_value'])) {
                        $hardFilters['PROPERTY_' . $question['property_code']] = $selectedOption['property_value'];
                    }
                    break;

                case 'scoring':
                    foreach ($selectedOption['scores'] ?? [] as $tag => $score) {
                        $scoringTags[$tag] = ($scoringTags[$tag] ?? 0) + $score;
                    }
                    break;
            }
        }

        // Retrieve products by hard filters
        $result = \CIBlockElement::GetList(
            ['SORT' => 'ASC'],
            $hardFilters,
            false,
            ['nPageSize' => 20],
            ['ID', 'NAME', 'DETAIL_PAGE_URL', 'PREVIEW_PICTURE', 'CATALOG_PRICE_1', 'PROPERTY_TAGS']
        );

        $products = [];
        while ($item = $result->GetNext()) {
            $score = 0;
            // Apply scoring by product tags
            $productTags = explode(',', $item['PROPERTY_TAGS_VALUE'] ?? '');
            foreach ($scoringTags as $tag => $tagScore) {
                if (in_array(trim($tag), array_map('trim', $productTags))) {
                    $score += $tagScore;
                }
            }
            $item['_SCORE'] = $score;
            $products[]     = $item;
        }

        // Sort by score (descending)
        usort($products, fn($a, $b) => $b['_SCORE'] <=> $a['_SCORE']);

        return $products;
    }
}

Client-Side Part

class ProductSelectorUI {
    constructor(configJson) {
        this.config  = configJson;
        this.answers = {};
        this.step    = 0;
        this.renderStep();
    }

    renderStep() {
        const question = this.config.questions[this.step];
        const container = document.getElementById('selector-step');

        container.innerHTML = `
            <h3>${question.text}</h3>
            <div class="selector-options">
                ${question.options.map(opt => `
                    <button class="selector-option" data-value="${opt.value}" data-question="${question.id}">
                        ${opt.label}
                    </button>
                `).join('')}
            </div>
        `;

        container.querySelectorAll('.selector-option').forEach(btn => {
            btn.addEventListener('click', e => {
                const questionId = e.target.dataset.question;
                const value      = e.target.dataset.value;
                this.selectAnswer(questionId, value);
            });
        });

        this.updateProgress();
    }

    selectAnswer(questionId, value) {
        this.answers[questionId] = value;

        if (this.step < this.config.questions.length - 1) {
            this.step++;
            this.renderStep();
        } else {
            this.fetchResults();
        }
    }

    async fetchResults() {
        document.getElementById('selector-step').innerHTML = '<div class="loading">Finding options for you...</div>';

        const response = await fetch('/local/ajax/product_selector.php', {
            method: 'POST',
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify({answers: this.answers, sessid: BX.bitrix_sessid()}),
        });

        const data = await response.json();
        this.renderResults(data.products);
    }

    renderResults(products) {
        const container = document.getElementById('selector-results');
        if (products.length === 0) {
            container.innerHTML = '<p>Unfortunately, nothing was found matching your criteria. <a href="/catalog/">Browse the full catalog</a></p>';
            return;
        }

        container.innerHTML = products.slice(0, 6).map(p => `
            <div class="product-card">
                <img src="${p.PREVIEW_PICTURE?.SRC || ''}" alt="${p.NAME}">
                <h4><a href="${p.DETAIL_PAGE_URL}">${p.NAME}</a></h4>
                <div class="price">${p.CATALOG_PRICE_1}</div>
                <a href="${p.DETAIL_PAGE_URL}" class="btn">View details</a>
            </div>
        `).join('');

        document.getElementById('selector-container').style.display = 'none';
        document.getElementById('selector-results-container').style.display = 'block';
    }

    updateProgress() {
        const bar = document.getElementById('selector-progress');
        if (bar) bar.style.width = ((this.step / this.config.questions.length) * 100) + '%';
    }
}

Result and Lead

If no suitable products are found — the form offers to leave contact details for a consultation:

if (empty($products)) {
    echo json_encode([
        'products' => [],
        'show_contact_form' => true,
        'message' => 'We will find the right option for you individually',
    ]);
    exit;
}

If the user leaves contact details after the selection — save the chosen parameters in the lead's comments (marketing value: the manager immediately knows what the client is looking for).

Development Timeline

Option Scope Timeline
Direct filtering Questions → filters → product list 4–6 days
With scoring + Point system, ranking 6–10 days
With configurator Question management via the admin panel 10–15 days