Skip to content

Commit 8b46986

Browse files
pmcfadinclaude
andcommitted
refactor: rewrite rating aggregate to use incremental counters on summary table
Replace full-scan recompute (scan all ratings → update videos table) with $inc-based counter updates on video_ratings summary table. New ratings increment rating_counter and rating_total; updates adjust only rating_total by the delta. get_video_ratings_summary now reads directly from the summary table rather than pulling pre-computed fields from the video record. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent bf29e6d commit 8b46986

2 files changed

Lines changed: 213 additions & 167 deletions

File tree

app/services/rating_service.py

Lines changed: 65 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -30,45 +30,56 @@
3030

3131
async def _update_video_aggregate_rating(
3232
video_id: VideoID,
33-
ratings_db_table: AstraDBCollection,
34-
videos_db_table: AstraDBCollection,
33+
new_rating: int,
34+
old_rating: int | None = None,
35+
summary_db_table: AstraDBCollection | None = None,
3536
) -> None:
36-
"""Recalculate average and total ratings count for the given video."""
37+
"""Increment counters on the video_ratings summary table.
3738
38-
cursor = ratings_db_table.find(
39-
filter={"videoid": str(video_id)}, projection={"rating": 1}
40-
)
41-
docs: List[Dict[str, Any]] = (
42-
await cursor.to_list() if hasattr(cursor, "to_list") else cursor
43-
)
39+
* **New rating** (old_rating is None): increment rating_counter by 1 and
40+
rating_total by new_rating.
41+
* **Updated rating** (old_rating provided): increment rating_total by
42+
(new_rating - old_rating) only — counter stays the same.
43+
"""
44+
45+
if summary_db_table is None:
46+
summary_db_table = await get_table(RATINGS_SUMMARY_TABLE_NAME)
47+
48+
vid_str = str(video_id)
4449

45-
if docs:
46-
values = [int(d["rating"]) for d in docs if "rating" in d]
47-
total = len(values)
48-
average = sum(values) / total if total else None
50+
if old_rating is None:
51+
inc_doc: Dict[str, Any] = {"rating_counter": 1, "rating_total": new_rating}
4952
else:
50-
total = 0
51-
average = None
53+
delta = new_rating - old_rating
54+
inc_doc = {"rating_total": delta}
5255

5356
try:
54-
await videos_db_table.update_one(
55-
filter={"videoid": str(video_id)},
56-
update={
57-
"$set": {
58-
"averageRating": average,
59-
"totalRatingsCount": total,
60-
"updatedAt": datetime.now(timezone.utc),
61-
}
62-
},
57+
await summary_db_table.update_one(
58+
filter={"videoid": vid_str},
59+
update={"$inc": inc_doc},
60+
upsert=True,
6361
)
6462
except DataAPIResponseException as exc:
65-
# If the videos table schema does not include these columns (common
66-
# when running against the default KillrVideo schema) Astra will
67-
# reject the update with UNKNOWN_TABLE_COLUMNS. That is not fatal –
68-
# the API can still compute aggregates on-the-fly.
69-
if "UNKNOWN_TABLE_COLUMNS" not in str(exc):
63+
if "Update operation not supported" in str(
64+
exc
65+
) or "unsupported operations" in str(exc):
66+
existing = await summary_db_table.find_one(
67+
filter={"videoid": vid_str}
68+
)
69+
counter = int(existing.get("rating_counter", 0)) if existing else 0
70+
total = int(existing.get("rating_total", 0)) if existing else 0
71+
if old_rating is None:
72+
counter += 1
73+
total += new_rating
74+
else:
75+
total += new_rating - old_rating
76+
await summary_db_table.update_one(
77+
filter={"videoid": vid_str},
78+
update={"$set": {"rating_counter": counter, "rating_total": total}},
79+
upsert=True,
80+
)
81+
else:
7082
raise
71-
# Otherwise silently ignore so the rating operation succeeds.
7283

7384

7485
async def rate_video(
@@ -152,9 +163,12 @@ async def rate_video(
152163
"user_activity insert failed for rate; ignoring", exc_info=True
153164
)
154165

155-
# update aggregate
166+
# update aggregate counters on the summary table
167+
old_rating_value: int | None = None
168+
if existing_doc:
169+
old_rating_value = int(existing_doc["rating"])
156170
await _update_video_aggregate_rating(
157-
video_id, db_table, await get_table(video_service.VIDEOS_TABLE_NAME)
171+
video_id, new_rating=request.rating, old_rating=old_rating_value
158172
)
159173
return rating_obj
160174

@@ -168,19 +182,35 @@ async def get_video_ratings_summary(
168182
video_id: VideoID,
169183
current_user_id: UUID | None = None,
170184
ratings_db_table: Optional[AstraDBCollection] = None,
185+
summary_db_table: Optional[AstraDBCollection] = None,
171186
) -> AggregateRatingResponse:
172187
"""Return aggregated rating info for a video and optionally the caller's rating."""
173188

174-
# Fetch video to access pre-computed aggregates
189+
# 404 check – make sure the video exists
175190
target_video = await video_service.get_video_by_id(video_id)
176191
if target_video is None:
177192
raise HTTPException(
178193
status_code=status.HTTP_404_NOT_FOUND, detail="Video not found"
179194
)
180195

181-
avg = target_video.averageRating
182-
total = target_video.totalRatingsCount
196+
# Read counters from the video_ratings summary table
197+
if summary_db_table is None:
198+
summary_db_table = await get_table(RATINGS_SUMMARY_TABLE_NAME)
199+
200+
summary_doc = await summary_db_table.find_one(
201+
filter={"videoid": str(video_id)}
202+
)
203+
204+
if summary_doc:
205+
rating_counter = int(summary_doc.get("rating_counter", 0))
206+
rating_total = int(summary_doc.get("rating_total", 0))
207+
avg = round(rating_total / rating_counter, 2) if rating_counter > 0 else None
208+
total = rating_counter
209+
else:
210+
avg = None
211+
total = 0
183212

213+
# Look up current user's individual rating
184214
user_rating_value: RatingValue | None = None
185215
if current_user_id is not None:
186216
if ratings_db_table is None:

0 commit comments

Comments
 (0)