Development of Product Rating System in 1C-Bitrix
Product rating is an aggregated score that affects ranking in catalog, is displayed in product cards and lists. 1C-Bitrix has a vote module (voting), but for product ratings it's excessive and poorly integrated with catalog. More practical to store ratings in separate table and aggregate value into infoblock property.
Rating Storage Architecture
Table b_product_vote for individual ratings:
| Field | Type | Purpose |
|---|---|---|
| ID | int | Primary key |
| PRODUCT_ID | int | Product ID |
| USER_ID | int | User ID (NULL = guest) |
| IP | varchar(45) | IP address (for guests and anti-gaming) |
| RATING | tinyint | Rating 1–5 |
| CREATED_AT | datetime | When voted |
ORM class ProductVoteTable inherits from \Bitrix\Main\ORM\Data\DataManager.
In product infoblock add two numeric properties:
-
RATING_AVG— average rating (float, updated after each vote) -
RATING_COUNT— number of ratings
This allows sorting and filtering by rating via standard CIBlockElement::GetList() without JOIN.
Voting Algorithm
Voting is implemented via AJAX request. Component outputs form with stars, click sends POST to controller:
// local/ajax/product_vote.php
\Bitrix\Main\Loader::includeModule('main');
\Bitrix\Main\Loader::includeModule('catalog');
$productId = (int)($_POST['product_id'] ?? 0);
$rating = (int)($_POST['rating'] ?? 0);
if ($rating < 1 || $rating > 5 || $productId <= 0) {
echo json_encode(['success' => false, 'error' => 'invalid_data']);
exit;
}
$userId = $GLOBALS['USER']->GetID() ?: null;
$ip = \Bitrix\Main\Context::getCurrent()->getRequest()->getRemoteAddress();
// Check: already voted?
$existing = ProductVoteTable::getList([
'filter' => ['=PRODUCT_ID' => $productId, '=USER_ID' => $userId ?: false, '=IP' => $ip],
'limit' => 1,
])->fetch();
if ($existing && $userId === null) {
echo json_encode(['success' => false, 'error' => 'already_voted']);
exit;
}
For authorized users check by USER_ID. For guests — by IP. Authorized user can change their rating (update existing record instead of creating new).
Aggregated Rating Recalculation
After each vote update aggregates:
function updateProductRating(int $productId): void
{
$conn = \Bitrix\Main\Application::getConnection();
$row = $conn->query(
"SELECT AVG(RATING) as AVG_RATING, COUNT(*) as CNT
FROM b_product_vote
WHERE PRODUCT_ID = {$productId}"
)->fetch();
\CIBlockElement::SetPropertyValuesEx($productId, false, [
'RATING_AVG' => round((float)$row['AVG_RATING'], 2),
'RATING_COUNT' => (int)$row['CNT'],
]);
}
SetPropertyValuesEx is faster than Update() on whole element — it updates only specified properties.
Visualization: Star Widget
On frontend rating displays as SVG stars. Partial fill logic: for rating 4.3, four stars are filled completely, fifth at 30%. Implemented via CSS-clip or SVG-gradient with width proportional to fractional part.
Component for displaying rating in product card and list takes parameters:
$APPLICATION->IncludeComponent('custom:product.rating', '', [
'PRODUCT_ID' => $arResult['ID'],
'SHOW_FORM' => $USER->IsAuthorized() ? 'Y' : 'N',
'CURRENT_RATING' => $arResult['PROPERTIES']['RATING_AVG']['VALUE'],
'VOTE_COUNT' => $arResult['PROPERTIES']['RATING_COUNT']['VALUE'],
]);
Anti-gaming Protection
IP-based limits work for guests but not organized manipulation:
- For authorized — one rating per product (strict
USER_ID + PRODUCT_IDcheck). - Limit: cannot vote for product that was never viewed (check via
b_stat_sessionor custom views table). - Optional: allow voting only for users who purchased product (similar to review verification).
Development Timeframe
| Scope | Components | Duration |
|---|---|---|
| Basic | ORM model, AJAX voting, star widget, aggregation to property | 3–4 days |
| Full | Rating change, anti-gaming protection, catalog sorting by rating, voting history | 5–7 days |







