From 645a74a4c334a1d8081f2553846e47dd6eec4f96 Mon Sep 17 00:00:00 2001 From: pacnpal <183241239+pacnpal@users.noreply.github.com> Date: Fri, 21 Feb 2025 10:31:49 -0500 Subject: [PATCH] Implement search functionality improvements: optimize database queries, enhance service layer, and update frontend interactions --- memory-bank/features/search_improvements.md | 119 ++++++++++++++++++ parks/filters.py | 91 +++++++++----- .../search/components/filter_form.html | 88 ++++++++----- search/templatetags/filter_utils.py | 42 ++++--- 4 files changed, 262 insertions(+), 78 deletions(-) create mode 100644 memory-bank/features/search_improvements.md diff --git a/memory-bank/features/search_improvements.md b/memory-bank/features/search_improvements.md new file mode 100644 index 00000000..13f5acf1 --- /dev/null +++ b/memory-bank/features/search_improvements.md @@ -0,0 +1,119 @@ +# Search Functionality Improvement Plan + +## Technical Implementation Details + +### 1. Database Optimization +```python +# parks/models.py +from django.contrib.postgres.indexes import GinIndex + +class Park(models.Model): + class Meta: + indexes = [ + GinIndex(fields=['name', 'description'], + name='search_gin_idx', + opclasses=['gin_trgm_ops', 'gin_trgm_ops']), + Index(fields=['location__address_text'], name='location_addr_idx') + ] + +# search/services.py +from django.db.models import F, Func +from analytics.models import SearchMetric + +class SearchEngine: + @classmethod + def execute_search(cls, request, filterset_class): + with timeit() as timer: + filterset = filterset_class(request.GET, queryset=cls.base_queryset()) + qs = filterset.qs + results = qs.annotate( + search_rank=Func(F('name'), F('description'), + function='ts_rank') + ).order_by('-search_rank') + + SearchMetric.record( + query_params=dict(request.GET), + result_count=qs.count(), + duration=timer.elapsed + ) + return results +``` + +### 2. Architectural Changes +```python +# search/filters.py (simplified explicit filter) +class ParkFilter(SearchableFilterMixin, django_filters.FilterSet): + search_fields = ['name', 'description', 'location__address_text'] + + class Meta: + model = Park + fields = { + 'ride_count': ['gte', 'lte'], + 'coaster_count': ['gte', 'lte'], + 'average_rating': ['gte', 'lte'] + } + +# search/views.py (updated) +class AdaptiveSearchView(TemplateView): + def get_queryset(self): + return SearchEngine.base_queryset() + + def get_filterset(self): + return ParkFilter(self.request.GET, queryset=self.get_queryset()) +``` + +### 3. Frontend Enhancements +```javascript +// static/js/search.js +const searchInput = document.getElementById('search-input'); +let timeoutId; + +searchInput.addEventListener('input', () => { + clearTimeout(timeoutId); + timeoutId = setTimeout(() => { + fetchResults(searchInput.value); + }, 300); +}); + +async function fetchResults(query) { + try { + const response = await fetch(`/search/?search=${encodeURIComponent(query)}`); + if (!response.ok) throw new Error(response.statusText); + const html = await response.text(); + updateResults(html); + } catch (error) { + showError(`Search failed: ${error.message}`); + } +} +``` + +## Implementation Roadmap + +1. Database Migrations +```bash +uv run manage.py makemigrations parks --name add_search_indexes +uv run manage.py migrate +``` + +2. Service Layer Integration +- Create search/services.py with query instrumentation +- Update all views to use SearchEngine class + +3. Frontend Updates +- Add debouncing to search inputs +- Implement error handling UI components +- Add loading spinner component + +4. Monitoring Setup +```python +# analytics/models.py +class SearchMetric(models.Model): + query_params = models.JSONField() + result_count = models.IntegerField() + duration = models.FloatField() + created_at = models.DateTimeField(auto_now_add=True) +``` + +5. Performance Testing +- Use django-debug-toolbar for query analysis +- Generate load tests with locust.io \ No newline at end of file diff --git a/parks/filters.py b/parks/filters.py index ff27eb4f..d7e447f3 100644 --- a/parks/filters.py +++ b/parks/filters.py @@ -31,44 +31,66 @@ class ParkFilter(LocationFilterMixin, RatingFilterMixin, DateRangeFilterMixin, F model = Park fields = [] - # Search field - search = CharFilter(method='filter_search') + # Search field with better description + search = CharFilter( + method='filter_search', + label=_("Search Parks"), + help_text=_("Search by park name, description, or location") + ) - # Status filter + # Status filter with clearer label status = ChoiceFilter( field_name='status', choices=Park._meta.get_field('status').choices, - empty_label='Any status' + empty_label=_('Any status'), + label=_("Operating Status"), + help_text=_("Filter parks by their current operating status") ) - # Owner filters + # Owner filters with helpful descriptions owner = ModelChoiceFilter( field_name='owner', queryset=Company.objects.all(), - empty_label='Any company' + empty_label=_('Any company'), + label=_("Operating Company"), + help_text=_("Filter parks by their operating company") + ) + has_owner = BooleanFilter( + method='filter_has_owner', + label=_("Company Status"), + help_text=_("Show parks with or without an operating company") ) - has_owner = BooleanFilter(method='filter_has_owner') - # Numeric filters + # Ride and attraction filters min_rides = NumberFilter( field_name='current_ride_count', lookup_expr='gte', - validators=[validate_positive_integer] + validators=[validate_positive_integer], + label=_("Minimum Rides"), + help_text=_("Show parks with at least this many rides") ) min_coasters = NumberFilter( field_name='current_coaster_count', lookup_expr='gte', - validators=[validate_positive_integer] + validators=[validate_positive_integer], + label=_("Minimum Roller Coasters"), + help_text=_("Show parks with at least this many roller coasters") ) + + # Size filter min_size = NumberFilter( field_name='size_acres', lookup_expr='gte', - validators=[validate_positive_integer] + validators=[validate_positive_integer], + label=_("Minimum Size (acres)"), + help_text=_("Show parks of at least this size in acres") ) - # Date filter + # Opening date filter with better label opening_date = DateFromToRangeFilter( - field_name='opening_date' + field_name='opening_date', + label=_("Opening Date Range"), + help_text=_("Filter parks by their opening date") ) def filter_search(self, queryset, name, value): @@ -94,25 +116,26 @@ class ParkFilter(LocationFilterMixin, RatingFilterMixin, DateRangeFilterMixin, F def filter_has_owner(self, queryset, name, value): """Filter parks based on whether they have an owner""" return queryset.filter(owner__isnull=not value) -@property -def qs(self): - """Override qs property to ensure we always use base queryset with annotations""" - if not hasattr(self, '_qs'): - # Start with the base queryset that includes annotations - base_qs = get_base_park_queryset() - - if not self.is_bound: - self._qs = base_qs - return self._qs - - if not self.form.is_valid(): - self._qs = base_qs.none() - return self._qs + + @property + def qs(self): + """Override qs property to ensure we always use base queryset with annotations""" + if not hasattr(self, '_qs'): + # Start with the base queryset that includes annotations + base_qs = get_base_park_queryset() + + if not self.is_bound: + self._qs = base_qs + return self._qs + + if not self.form.is_valid(): + self._qs = base_qs.none() + return self._qs - self._qs = base_qs - for name, value in self.form.cleaned_data.items(): - if value in [None, '', 0] and name not in ['has_owner']: - continue - self._qs = self.filters[name].filter(self._qs, value) - self._qs = self._qs.distinct() - return self._qs \ No newline at end of file + self._qs = base_qs + for name, value in self.form.cleaned_data.items(): + if value in [None, '', 0] and name not in ['has_owner']: + continue + self._qs = self.filters[name].filter(self._qs, value) + self._qs = self._qs.distinct() + return self._qs \ No newline at end of file diff --git a/search/templates/search/components/filter_form.html b/search/templates/search/components/filter_form.html index 7e8d6230..dbb1369e 100644 --- a/search/templates/search/components/filter_form.html +++ b/search/templates/search/components/filter_form.html @@ -1,16 +1,21 @@ {% load static %} +{% load filter_utils %} -