Implement hybrid filtering strategy for parks and rides

- Added comprehensive documentation for hybrid filtering implementation, including architecture, API endpoints, performance characteristics, and usage examples.
- Developed a hybrid pagination and client-side filtering recommendation, detailing server-side responsibilities and client-side logic.
- Created a test script for hybrid filtering endpoints, covering various test cases including basic filtering, search functionality, pagination, and edge cases.
This commit is contained in:
pacnpal
2025-09-14 21:07:17 -04:00
parent 0fd6dc2560
commit 35f8d0ef8f
42 changed files with 8490 additions and 224 deletions

View File

@@ -538,6 +538,10 @@ class Ride(TrackedModel):
max_digits=3, decimal_places=2, null=True, blank=True
)
# Computed fields for hybrid filtering
opening_year = models.IntegerField(null=True, blank=True, db_index=True)
search_text = models.TextField(blank=True, db_index=True)
# Image settings - references to existing photos
banner_image = models.ForeignKey(
"RidePhoto",
@@ -639,14 +643,14 @@ class Ride(TrackedModel):
pass
# If park changed or this is a new ride, ensure slug uniqueness within the park
park_changed = original_ride and original_ride.park_id != self.park_id
park_changed = original_ride and original_ride.park.id != self.park.id
if not self.pk or park_changed:
self._ensure_unique_slug_in_park()
# Handle park area validation when park changes
if park_changed and self.park_area:
# Check if park_area belongs to the new park
if self.park_area.park_id != self.park_id:
if self.park_area.park.id != self.park.id:
# Clear park_area if it doesn't belong to the new park
self.park_area = None
@@ -658,8 +662,93 @@ class Ride(TrackedModel):
self.url = f"{frontend_domain}/parks/{self.park.slug}/rides/{self.slug}/"
self.park_url = f"{frontend_domain}/parks/{self.park.slug}/"
# Populate computed fields
self._populate_computed_fields()
super().save(*args, **kwargs)
def _populate_computed_fields(self) -> None:
"""Populate computed fields for hybrid filtering."""
# Extract opening year from opening_date
if self.opening_date:
self.opening_year = self.opening_date.year
else:
self.opening_year = None
# Build comprehensive search text
search_parts = []
# Basic ride info
if self.name:
search_parts.append(self.name)
if self.description:
search_parts.append(self.description)
# Park info
if self.park:
search_parts.append(self.park.name)
if hasattr(self.park, 'location') and self.park.location:
if self.park.location.city:
search_parts.append(self.park.location.city)
if self.park.location.state:
search_parts.append(self.park.location.state)
if self.park.location.country:
search_parts.append(self.park.location.country)
# Park area
if self.park_area:
search_parts.append(self.park_area.name)
# Category
if self.category:
category_display = dict(CATEGORY_CHOICES).get(self.category, '')
if category_display:
search_parts.append(category_display)
# Status
if self.status:
status_display = dict(self.STATUS_CHOICES).get(self.status, '')
if status_display:
search_parts.append(status_display)
# Companies
if self.manufacturer:
search_parts.append(self.manufacturer.name)
if self.designer:
search_parts.append(self.designer.name)
# Ride model
if self.ride_model:
search_parts.append(self.ride_model.name)
if self.ride_model.manufacturer:
search_parts.append(self.ride_model.manufacturer.name)
# Roller coaster stats if available
try:
if hasattr(self, 'coaster_stats') and self.coaster_stats:
stats = self.coaster_stats
if stats.track_type:
search_parts.append(stats.track_type)
if stats.track_material:
material_display = dict(RollerCoasterStats.TRACK_MATERIAL_CHOICES).get(stats.track_material, '')
if material_display:
search_parts.append(material_display)
if stats.roller_coaster_type:
type_display = dict(RollerCoasterStats.COASTER_TYPE_CHOICES).get(stats.roller_coaster_type, '')
if type_display:
search_parts.append(type_display)
if stats.launch_type:
launch_display = dict(RollerCoasterStats.LAUNCH_CHOICES).get(stats.launch_type, '')
if launch_display:
search_parts.append(launch_display)
if stats.train_style:
search_parts.append(stats.train_style)
except Exception:
# Ignore if coaster_stats doesn't exist or has issues
pass
self.search_text = ' '.join(filter(None, search_parts)).lower()
def _ensure_unique_slug_in_park(self) -> None:
"""Ensure the ride's slug is unique within its park."""
base_slug = slugify(self.name)
@@ -685,7 +774,6 @@ class Ride(TrackedModel):
Returns:
dict: Summary of changes made
"""
from django.apps import apps
old_park = self.park
old_url = self.url