Review Aggregation
Review aggregation calculates and maintains product review statistics, including average ratings, rating distributions, and review counts. These aggregates are used for product display and search ranking.
Aggregation Overview
What is Aggregation?
Aggregation computes summary statistics from individual reviews:
- Average Rating: Mean rating across all approved reviews
- Rating Distribution: Count of reviews per rating (1-5 stars)
- Review Count: Total number of approved reviews
- Last Review Date: Most recent review timestamp
Aggregation Flow
Aggregate Data Structure
Aggregate Entity
interface VariantReviewAggregate {
variantId: string;
averageRating: number; // 0.0 - 5.0
reviewCount: number; // Total approved reviews
rating1Count: number; // 1-star reviews
rating2Count: number; // 2-star reviews
rating3Count: number; // 3-star reviews
rating4Count: number; // 4-star reviews
rating5Count: number; // 5-star reviews
updatedAt: Date;
}
Aggregation Process
Recompute Aggregate
async recomputeAggregate(variantId: string): Promise<void> {
// Get all approved reviews for this variant
const approvedReviews = await db
.select({
rating: reviews.rating,
})
.from(reviews)
.where(
and(
eq(reviews.variantId, variantId),
eq(reviews.status, "approved"),
),
);
const reviewCount = approvedReviews.length;
if (reviewCount === 0) {
// No reviews - set to zero/default
await this.setEmptyAggregate(variantId);
return;
}
// Calculate aggregates
const ratingCounts = {
1: 0,
2: 0,
3: 0,
4: 0,
5: 0,
};
let totalRating = 0;
for (const review of approvedReviews) {
ratingCounts[review.rating as keyof typeof ratingCounts]++;
totalRating += review.rating;
}
const averageRating = totalRating / reviewCount;
// Update database
await db
.insert(variantReviewAggregate)
.values({
variantId,
averageRating,
reviewCount,
rating1Count: ratingCounts[1],
rating2Count: ratingCounts[2],
rating3Count: ratingCounts[3],
rating4Count: ratingCounts[4],
rating5Count: ratingCounts[5],
updatedAt: new Date(),
})
.onConflictDoUpdate({
target: variantReviewAggregate.variantId,
set: {
averageRating,
reviewCount,
rating1Count: ratingCounts[1],
rating2Count: ratingCounts[2],
rating3Count: ratingCounts[3],
rating4Count: ratingCounts[4],
rating5Count: ratingCounts[5],
updatedAt: new Date(),
},
});
// Update cache
await this.cacheService.setAggregate(variantId, {
variantId,
averageRating,
reviewCount,
rating1Count: ratingCounts[1],
rating2Count: ratingCounts[2],
rating3Count: ratingCounts[3],
rating4Count: ratingCounts[4],
rating5Count: ratingCounts[5],
updatedAt: new Date(),
});
}
Empty Aggregate
async setEmptyAggregate(variantId: string): Promise<void> {
await db
.insert(variantReviewAggregate)
.values({
variantId,
averageRating: 0,
reviewCount: 0,
rating1Count: 0,
rating2Count: 0,
rating3Count: 0,
rating4Count: 0,
rating5Count: 0,
})
.onConflictDoUpdate({
target: variantReviewAggregate.variantId,
set: {
averageRating: 0,
reviewCount: 0,
rating1Count: 0,
rating2Count: 0,
rating3Count: 0,
rating4Count: 0,
rating5Count: 0,
updatedAt: new Date(),
},
});
// Update cache
await this.cacheService.setAggregate(variantId, {
variantId,
averageRating: 0,
reviewCount: 0,
rating1Count: 0,
rating2Count: 0,
rating3Count: 0,
rating4Count: 0,
rating5Count: 0,
updatedAt: new Date(),
});
}
Triggering Aggregation
When Aggregation Happens
Aggregation is triggered when:
- Review Approved: Review status changes to
approved - Review Rejected: Review status changes to
rejected(recalculate) - Review Updated: Review rating or content updated
- Review Deleted: Review removed from system
Aggregation Triggers
// On review approval
async approveReview(reviewId: string): Promise<void> {
// ... approval logic ...
// Trigger aggregation
await this.aggregationService.recomputeAggregate(review.variantId);
}
// On review rejection
async rejectReview(reviewId: string): Promise<void> {
// ... rejection logic ...
// Recalculate aggregate (exclude rejected review)
await this.aggregationService.recomputeAggregate(review.variantId);
}
// On review update
async updateReview(reviewId: string, updates: ReviewUpdate): Promise<void> {
// ... update logic ...
// Recalculate if rating changed
if (updates.rating) {
await this.aggregationService.recomputeAggregate(review.variantId);
}
}
Retrieving Aggregates
Get Aggregate
async getAggregate(
variantId: string,
): Promise<ReviewAggregateDto | null> {
// Try cache first
const cached = await this.cacheService.getAggregate(variantId);
if (cached) {
return cached;
}
// Get from database
const [aggregate] = await db
.select()
.from(variantReviewAggregate)
.where(eq(variantReviewAggregate.variantId, variantId))
.limit(1);
if (!aggregate) {
return null;
}
const result: ReviewAggregateDto = {
variantId: aggregate.variantId,
averageRating: Number(aggregate.averageRating),
reviewCount: aggregate.reviewCount,
rating1Count: aggregate.rating1Count,
rating2Count: aggregate.rating2Count,
rating3Count: aggregate.rating3Count,
rating4Count: aggregate.rating4Count,
rating5Count: aggregate.rating5Count,
updatedAt: aggregate.updatedAt,
};
// Cache it
await this.cacheService.setAggregate(variantId, result);
return result;
}
Average Rating Calculation
Rating Formula
averageRating = sum(allRatings) / reviewCount
Example Calculation
For reviews with ratings: [5, 4, 5, 3, 4]
totalRating = 5 + 4 + 5 + 3 + 4 = 21
reviewCount = 5
averageRating = 21 / 5 = 4.2
Rating Distribution
// Rating distribution example
{
rating1Count: 0, // 0 reviews
rating2Count: 0, // 0 reviews
rating3Count: 1, // 1 review
rating4Count: 2, // 2 reviews
rating5Count: 2, // 2 reviews
}
Real-time Updates
Incremental Updates
Aggregates update immediately when reviews change:
// Review approved → aggregate updated
await this.approveReview(reviewId);
// Aggregate automatically recalculated
// Review rejected → aggregate updated
await this.rejectReview(reviewId);
// Aggregate automatically recalculated
Batch Processing
For bulk operations, aggregates can be batch updated:
async batchRecomputeAggregates(variantIds: string[]): Promise<void> {
for (const variantId of variantIds) {
await this.recomputeAggregate(variantId);
}
}
Cache Integration
Cache Strategy
// Cache aggregate with no expiration (persistent)
await this.cacheService.setAggregate(variantId, aggregate);
// Cache key: product_review_agg:{variantId}
// TTL: 0 (no expiration, invalidated on updates)
Cache Invalidation
// Invalidate cache when aggregate changes
async recomputeAggregate(variantId: string): Promise<void> {
// ... calculate aggregate ...
// Update cache
await this.cacheService.setAggregate(variantId, aggregate);
// Cache is automatically updated, no need to invalidate
}
API Endpoints
Get Review Aggregate
GET /storefront/products/{productId}/variants/{variantId}/reviews/aggregate
Response:
{
"variantId": "variant-123",
"averageRating": 4.5,
"reviewCount": 42,
"rating1Count": 2,
"rating2Count": 3,
"rating3Count": 5,
"rating4Count": 15,
"rating5Count": 17,
"updatedAt": "2024-12-18T10:00:00Z"
}
Performance Considerations
Database Indexes
-- Optimized index for aggregate lookups
CREATE INDEX CONCURRENTLY variant_review_aggregate_variant_id_idx
ON variant_review_aggregate(variant_id);
Query Optimization
- Aggregates stored in separate table (denormalized)
- Fast lookups via variant ID
- Cached for instant access
Use Cases
Product Display
// Display average rating and review count
const aggregate = await this.getAggregate(variantId);
displayRating(aggregate.averageRating, aggregate.reviewCount);
Search Ranking
// Use aggregate for search ranking
const aggregate = await this.getAggregate(variantId);
const score = calculateSearchScore({
averageRating: aggregate.averageRating,
reviewCount: aggregate.reviewCount,
});
Filtering
// Filter products by rating
const products = await db
.select()
.from(products)
.innerJoin(
variantReviewAggregate,
eq(products.variantId, variantReviewAggregate.variantId),
)
.where(gte(variantReviewAggregate.averageRating, 4.0));
Best Practices
- Real-time Updates: Update aggregates immediately on review changes
- Cache First: Check cache before database lookup
- Batch Operations: Batch aggregate updates for bulk operations
- Error Handling: Handle aggregation failures gracefully
- Monitoring: Track aggregation performance
Edge Cases
No Reviews
- Aggregate set to zero/default values
- Average rating is 0
- Review count is 0
Single Review
- Average rating equals review rating
- Distribution shows one rating count
Rating Changes
- Aggregate recalculated when rating updated
- Old rating removed, new rating added
- Average and distribution updated