When a customer opens a product card on a clothing website, they see a size chart — but in 60% of cases, they choose the wrong size. We rewrote the logic: instead of a static table — a dynamic algorithm that, based on height, weight, and circumferences, gives a specific size considering the garment's fit. Over 10 years, we have implemented more than 50 such advisors on 1C-Bitrix. After implementation, the return rate due to 'did not fit' drops by 15–20%, and conversion to cart from the product page increases by 12–18%.
According to one project, recommendation accuracy rose from 60% to 85%, and the development investment pays off in 2–3 months thanks to reduced logistics and repeat orders. With an average return value of $20 (adjusted to local currency), an 18% reduction saves $5,400 per 1,000 orders.
The fit algorithm works based on parameters
First, we link three data sets:
- Customer measurements (height, weight, circumferences)
- Product size chart: for each size, we define physical measurement ranges considering fit (slim – narrow allowance, oversized – wide)
- Inventory: we check if the recommended size is in stock, otherwise we offer an alternative
Key point — measurement ranges, not exact values. Size 46 fits chest 88–92 cm, not strictly 90. The range width depends on fit type: slim fit — ±2 cm, oversized — ±5 cm. If all measurements fall into one range — the size is considered ideal. If measurements are scattered between two adjacent sizes — the algorithm selects the larger or smaller depending on fit and preferences (e.g., for tight fit, selects the smaller).
Fit by parameters outperforms a static size chart
| Criteria | Static size chart | Algorithmic fit by parameters |
|---|---|---|
| Accuracy | ~60% (customer chooses) | ~85% (system calculates) |
| Fit type considered | No | Slim/regular/oversized |
| Alternative | None | Automatic neighbor size suggestion |
| Stock integration | No | Check after recommendation |
| Personalization | No | Save measurements in account |
Data architecture
We extend the size chart structure with additional tolerance parameters. We use a HL block to store the rules:
CREATE TABLE b_size_fit_rules (
ID SERIAL PRIMARY KEY,
CHART_ID INT NOT NULL REFERENCES b_size_charts(ID),
SIZE_RU VARCHAR(10),
CHEST_MIN NUMERIC(5,1), CHEST_MAX NUMERIC(5,1),
WAIST_MIN NUMERIC(5,1), WAIST_MAX NUMERIC(5,1),
HIPS_MIN NUMERIC(5,1), HIPS_MAX NUMERIC(5,1),
HEIGHT_MIN SMALLINT, HEIGHT_MAX SMALLINT,
WEIGHT_MIN SMALLINT, WEIGHT_MAX SMALLINT,
FIT_TYPE VARCHAR(20) -- 'slim', 'regular', 'oversized'
);
Recommendation algorithm
// SizeFitAdvisor.php
class SizeFitAdvisor {
public function recommend(array $measurements, int $chartId, string $fitType = 'regular'): array {
$rules = SizeFitRulesTable::getList([
'filter' => ['=CHART_ID' => $chartId, '=FIT_TYPE' => $fitType],
'order' => ['SIZE_RU' => 'ASC'],
])->fetchAll();
$scores = [];
foreach ($rules as $rule) {
$score = 0;
$matched = 0;
foreach (['CHEST', 'WAIST', 'HIPS'] as $param) {
if (!isset($measurements[$param])) continue;
$val = (float) $measurements[$param];
$min = (float) $rule[$param . '_MIN'];
$max = (float) $rule[$param . '_MAX'];
if ($val >= $min && $val <= $max) {
$score++;
} elseif ($val < $min) {
$score -= ($min - $val) / 10;
} else {
$score -= ($val - $max) / 10;
}
$matched++;
}
if ($matched > 0) {
$scores[$rule['SIZE_RU']] = $score / $matched;
}
}
arsort($scores);
$best = array_key_first($scores);
$next = array_keys($scores)[1] ?? null;
return ['primary' => $best, 'alternative' => $next, 'scores' => $scores];
}
}
The algorithm returns not just one size but a primary recommendation and an alternative. This is important: if the primary size is unavailable, show the alternative with a note like 'if 46 is not available, take 48 — it will fit your build.'
Checking availability of the recommended size
After getting the recommendation, the server checks the stock of offers with that size. If the item is out of stock, an alternative size is immediately offered. The logic is implemented via the standard CIBlockElement::GetList with filter by size property and stock.
Fit form on the product card
Typical mistakes when implementing the form:
- Asking all measurements at once — scares customers. Better to use a step-by-step survey with a progress bar.
- Not checking stock before showing the result — user gets disappointed.
- Ignoring fit type — tight and loose clothing require different charts.
Single-step form (all measurements at once) — for experienced customers:
<form class="size-advisor-form">
<div class="form-row">
<label>Height (cm): <input type="number" name="height" min="140" max="220"></label>
<label>Weight (kg): <input type="number" name="weight" min="40" max="200"></label>
</div>
<div class="form-row">
<label>Chest circumference (cm): <input type="number" name="chest" min="60" max="160"></label>
<label>Waist circumference (cm): <input type="number" name="waist" min="50" max="150"></label>
<label>Hip circumference (cm): <input type="number" name="hips" min="70" max="170"></label>
</div>
<label>Fit type:
<select name="fit_type">
<option value="slim">Slim</option>
<option value="regular" selected>Regular</option>
<option value="oversized">Oversized</option>
</select>
</label>
<button type="submit">Find my size</button>
</form>
AJAX request to PHP controller
The form sends data to the server because:
- The fit algorithm runs server-side — data is not exposed to competitors
- The server immediately checks stock and returns the final answer
- The result can be personalized (save measurements for authenticated users)
// SizeAdvisorController.php
public function recommendAction(): array {
$measurements = [
'CHEST' => (float) $this->request->getPost('chest'),
'WAIST' => (float) $this->request->getPost('waist'),
'HIPS' => (float) $this->request->getPost('hips'),
];
$fitType = $this->request->getPost('fit_type', 'regular');
$productId = (int) $this->request->getPost('product_id');
$chartId = $this->getChartForProduct($productId);
$advisor = new SizeFitAdvisor();
$result = $advisor->recommend($measurements, $chartId, $fitType);
$availability = $advisor->checkAvailability($result['primary'], $productId);
if (is_object(global_user()) && !global_user()->IsGuest()) {
UserMeasurementsTable::saveForUser(global_user()->GetID(), $measurements);
}
return [
'recommended_size' => $result['primary'],
'alternative_size' => $result['alternative'],
'available' => $availability['available'],
'offers' => $availability['offers'],
];
}
Saving measurements in the personal account
If the user is authenticated, measurements are saved in a separate table and pre-filled the next time they use the advisor — even on a different product. In the personal account, there is a 'My measurements' section for manual editing.
What's included in the work
- Documentation on setting up size charts and calculation rules
- Source code of the module with comments
- Access to Git repository with change history
- Instructions for uploading sizes via Bitrix admin panel
- Training for content managers (1 hour online)
- Technical support for 14 days after launch
Timelines
| Option | What's included | Duration |
|---|---|---|
| Basic advisor | Form, algorithm, result | 1–2 weeks |
| With stock check | + integration with trade points, stock | 2–3 weeks |
| + Personal account with measurements | + profile saving | +1 week |
A size advisor based on measurements provides the most accurate recommendation and minimizes the return probability. Among all sizing tools, this is the most labor-intensive but also the most effective. Contact us for a cost and timeline estimate for your project. Get a detailed plan for integrating the advisor into your catalog. Order development, and we'll show you how much you'll save on returns.







