Rating and Review Filtering for 1C-Bitrix
Filtering "only products rated 4+" or "only products with reviews" is a marketing-effective tool: users are looking for proven products and are willing to pay more for them. Technically this is a non-trivial task, because ratings and reviews in Bitrix are stored in the vote module (the legacy mechanism) or as custom properties — not in the infoblock structure — so direct filtering via CIBlockElement::GetList is not available.
Where Ratings and Reviews Are Stored
Vote module (standard):
-
b_vote_event— votes (ratings) -
b_vote_answer— answer options -
b_vote_event_answer— relationship between events and answers
Iblock module (reviews as infoblock elements):
- Reviews are stored as elements of a dedicated infoblock, linked to the product via a property
- Rating — a numeric property of the review
Denormalization (recommended approach):
- Average rating and review count are stored as properties of the product infoblock
- Updated when a review is added or modified
Denormalization is recommended — it is the only approach that enables fast filtering without JOINs to external tables.
Denormalizing Rating into Product Properties
// Add properties to the product infoblock
// RATING_AVG (N) — average rating, REVIEW_COUNT (N) — number of reviews
// Recalculate when a review is added
function recalcProductRating(int $productId, int $reviewIblockId): void
{
$sum = 0;
$count = 0;
$rs = \CIBlockElement::GetList(
[],
[
'IBLOCK_ID' => $reviewIblockId,
'ACTIVE' => 'Y',
'PROPERTY_PRODUCT_ID' => $productId,
],
false, false,
['PROPERTY_RATING_VALUE']
);
while ($review = $rs->Fetch()) {
$rating = (float)$review['PROPERTY_RATING_VALUE'];
if ($rating > 0) {
$sum += $rating;
$count++;
}
}
$avgRating = $count > 0 ? round($sum / $count, 1) : 0;
// Update product properties
\CIBlockElement::SetPropertyValuesEx($productId, $productIblockId, [
'RATING_AVG' => $avgRating,
'REVIEW_COUNT' => $count,
]);
}
// Call after saving a review
AddEventHandler('iblock', 'OnAfterIBlockElementAdd', function($fields) use ($reviewIblockId) {
if ($fields['IBLOCK_ID'] == $reviewIblockId) {
$productId = $fields['PROPERTY_VALUES']['PRODUCT_ID'] ?? null;
if ($productId) recalcProductRating($productId, $reviewIblockId);
}
});
Rating Filtering
After denormalization, the filter is standard numeric filtering:
// Products with rating 4 and above
if (!empty($filterData['min_rating'])) {
$filter['>=PROPERTY_RATING_AVG'] = (float)$filterData['min_rating'];
}
// Only products with reviews
if (!empty($filterData['has_reviews'])) {
$filter['>PROPERTY_REVIEW_COUNT'] = 0;
}
UI: Star Rating Filter
<!-- RatingFilter.vue -->
<template>
<div class="rating-filter">
<div class="rating-filter__label">Minimum rating</div>
<div class="rating-filter__stars">
<button
v-for="star in [5, 4, 3, 2, 1]"
:key="star"
class="rating-filter__option"
:class="{ active: selectedRating === star }"
@click="selectRating(star)"
>
<span class="stars">
<span
v-for="i in 5"
:key="i"
class="star"
:class="{ filled: i <= star }"
>★</span>
</span>
<span class="rating-filter__value">{{ star }}+</span>
<span class="rating-filter__count" v-if="counts[star]">
({{ counts[star] }})
</span>
</button>
</div>
<button v-if="selectedRating" class="rating-filter__reset" @click="resetRating">
Reset
</button>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
const props = defineProps(['sectionId', 'currentRating'])
const emit = defineEmits(['change'])
const selectedRating = ref(props.currentRating || null)
const counts = ref({})
onMounted(() => loadCounts())
async function loadCounts() {
// Get product count for each minimum rating
const response = await fetch(`/api/catalog/rating-counts?section=${props.sectionId}`)
counts.value = await response.json()
}
function selectRating(rating) {
selectedRating.value = selectedRating.value === rating ? null : rating
emit('change', selectedRating.value)
}
function resetRating() {
selectedRating.value = null
emit('change', null)
}
</script>
Product Counts by Rating
// Method for getting rating distribution in a section
public function getRatingCountsAction(int $sectionId): array
{
$counts = [];
foreach ([5, 4, 3, 2, 1] as $minRating) {
$rs = \CIBlockElement::GetList(
[],
[
'IBLOCK_ID' => CATALOG_IBLOCK_ID,
'ACTIVE' => 'Y',
'SECTION_ID' => $sectionId,
'INCLUDE_SUBSECTIONS' => 'Y',
'>=PROPERTY_RATING_AVG' => $minRating,
],
[],
false,
['ID']
);
$counts[$minRating] = $rs->SelectedRowsCount();
}
return $counts;
}
Sorting by Rating
// Sort by rating accounting for review count (Bayesian formula)
// weighted_rating = (count / (count + min_count)) * avg + (min_count / (count + min_count)) * global_avg
// Calculated in PHP, result cached in the WEIGHTED_RATING property
$rsElements = \CIBlockElement::GetList(
['PROPERTY_WEIGHTED_RATING' => 'DESC'],
$filter,
false,
$navParams,
$selectFields
);
For simple cases, sorting by PROPERTY_RATING_AVG DESC with a secondary sort by PROPERTY_REVIEW_COUNT DESC is sufficient.
SEO Integration
Rating filter pages can have SEO value: "high-rated phones", "best products by reviews". Configuration via smart filter SEO templates is a separate task.
Timeline
Star rating filter with denormalization and AJAX — 2–3 business days. Full system with automatic rating recalculation, Bayesian formula, SEO, and sorting widget — 4–6 business days.







