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 |







