Developing a filter for ratings and reviews in 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

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.