mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 13:11:08 -05:00
1428 lines
50 KiB
Markdown
1428 lines
50 KiB
Markdown
# Search-Location Integration Plan - ThrillWiki
|
||
|
||
## Executive Summary
|
||
|
||
This document outlines the comprehensive integration strategy for enhancing ThrillWiki's existing search system with location capabilities. The plan builds upon the current django-filters based search architecture and integrates it with the designed domain-specific location models (ParkLocation, RideLocation, CompanyHeadquarters) and unified map service.
|
||
|
||
## Current State Analysis
|
||
|
||
### Existing Search Architecture
|
||
- **Framework**: Django-filters with [`ParkFilter`](parks/filters.py:26) and [`RideSearchView`](rides/views.py:1)
|
||
- **Current Capabilities**: Text search, status filtering, operator filtering, date ranges, numeric filters
|
||
- **Performance**: Basic queryset optimization with select_related/prefetch_related
|
||
- **UI**: HTMX-driven filtered results with [`AdaptiveSearchView`](core/views/search.py:5)
|
||
- **Templates**: Structured template hierarchy in [`templates/core/search/`](templates/core/search/)
|
||
|
||
### Location System State
|
||
- **Current**: Hybrid system with both generic Location model and domain-specific models
|
||
- **Designed**: Complete transition to [`ParkLocation`](memory-bank/features/location-models-design.md:17), [`RideLocation`](memory-bank/features/location-models-design.md:213), [`CompanyHeadquarters`](memory-bank/features/location-models-design.md:317)
|
||
- **Spatial Features**: PostGIS with PointField, spatial indexing, distance calculations
|
||
- **Map Integration**: [`UnifiedMapService`](memory-bank/features/map-service-design.md:35) designed for clustering and filtering
|
||
|
||
## 1. Search Index Enhancement Plan
|
||
|
||
### 1.1 Location Field Integration
|
||
|
||
#### Current Search Fields Extension
|
||
```python
|
||
# Enhanced ParkFilter in parks/filters.py
|
||
class LocationEnhancedParkFilter(ParkFilter):
|
||
search_fields = [
|
||
'name__icontains',
|
||
'description__icontains',
|
||
'park_location__city__icontains', # Domain-specific model
|
||
'park_location__state__icontains',
|
||
'park_location__country__icontains',
|
||
'park_location__highway_exit__icontains', # Road trip specific
|
||
'park_location__postal_code__icontains'
|
||
]
|
||
```
|
||
|
||
#### Spatial Search Fields
|
||
```python
|
||
# New spatial search capabilities
|
||
class SpatialSearchMixin:
|
||
near_point = PointFilter(
|
||
method='filter_near_point',
|
||
help_text="Search within radius of coordinates"
|
||
)
|
||
|
||
within_radius = NumberFilter(
|
||
method='filter_within_radius',
|
||
help_text="Radius in miles (used with near_point)"
|
||
)
|
||
|
||
within_bounds = BoundsFilter(
|
||
method='filter_within_bounds',
|
||
help_text="Search within geographic bounding box"
|
||
)
|
||
```
|
||
|
||
#### Index Performance Strategy
|
||
- **Spatial Indexes**: Maintain GIST indexes on all PointField columns
|
||
- **Composite Indexes**: Add indexes on frequently combined fields (city+state, country+state)
|
||
- **Text Search Optimization**: Consider adding GIN indexes for full-text search if performance degrades
|
||
|
||
### 1.2 Cross-Domain Location Indexing
|
||
|
||
#### Unified Location Search Index
|
||
```python
|
||
class UnifiedLocationSearchService:
|
||
"""Service for searching across all location-enabled models"""
|
||
|
||
def search_all_locations(self, query: str, location_types: Set[str] = None):
|
||
results = []
|
||
|
||
# Parks via ParkLocation
|
||
if not location_types or 'park' in location_types:
|
||
park_results = self._search_parks(query)
|
||
results.extend(park_results)
|
||
|
||
# Rides via RideLocation (optional)
|
||
if not location_types or 'ride' in location_types:
|
||
ride_results = self._search_rides(query)
|
||
results.extend(ride_results)
|
||
|
||
# Companies via CompanyHeadquarters
|
||
if not location_types or 'company' in location_types:
|
||
company_results = self._search_companies(query)
|
||
results.extend(company_results)
|
||
|
||
return self._rank_and_sort_results(results)
|
||
```
|
||
|
||
## 2. Spatial Search Architecture
|
||
|
||
### 2.1 Geographic Query Patterns
|
||
|
||
#### Distance-Based Search
|
||
```python
|
||
class DistanceSearchMixin:
|
||
"""Mixin for distance-based filtering"""
|
||
|
||
def filter_near_point(self, queryset, name, value):
|
||
"""Filter by proximity to a point"""
|
||
if not value or not hasattr(value, 'coords'):
|
||
return queryset
|
||
|
||
radius_miles = self.data.get('within_radius', 50) # Default 50 miles
|
||
|
||
from django.contrib.gis.measure import D
|
||
return queryset.filter(
|
||
park_location__point__distance_lte=(
|
||
value, D(mi=radius_miles)
|
||
)
|
||
).annotate(
|
||
distance=Distance('park_location__point', value)
|
||
).order_by('distance')
|
||
|
||
def filter_within_bounds(self, queryset, name, value):
|
||
"""Filter within geographic bounding box"""
|
||
if not value or not hasattr(value, 'extent'):
|
||
return queryset
|
||
|
||
return queryset.filter(
|
||
park_location__point__within=value.extent
|
||
)
|
||
```
|
||
|
||
#### Geocoding Integration Pattern
|
||
```python
|
||
class GeocodingSearchMixin:
|
||
"""Handle address-to-coordinate conversion in search"""
|
||
|
||
def filter_near_address(self, queryset, name, value):
|
||
"""Search near an address (geocoded to coordinates)"""
|
||
if not value:
|
||
return queryset
|
||
|
||
# Use OpenStreetMap Nominatim for geocoding
|
||
coordinates = self._geocode_address(value)
|
||
if not coordinates:
|
||
# Graceful fallback to text search
|
||
return self._fallback_text_search(queryset, value)
|
||
|
||
return self.filter_near_point(queryset, name, coordinates)
|
||
|
||
def _geocode_address(self, address: str) -> Optional[Point]:
|
||
"""Convert address to coordinates with caching"""
|
||
cache_key = f"geocode:{hash(address)}"
|
||
cached = cache.get(cache_key)
|
||
if cached:
|
||
return cached
|
||
|
||
# Implementation using Nominatim API
|
||
result = nominatim_geocode(address)
|
||
if result:
|
||
point = Point(result['lon'], result['lat'])
|
||
cache.set(cache_key, point, timeout=86400) # 24 hours
|
||
return point
|
||
return None
|
||
```
|
||
|
||
### 2.2 Database Query Optimization
|
||
|
||
#### Optimized Spatial Queries
|
||
```python
|
||
class SpatialQueryOptimizer:
|
||
"""Optimize spatial queries for performance"""
|
||
|
||
def get_optimized_queryset(self, base_queryset, spatial_filters):
|
||
"""Apply optimizations based on query type"""
|
||
|
||
# Use spatial index hints for PostgreSQL
|
||
queryset = base_queryset.extra(
|
||
select={'spatial_distance': 'ST_Distance(park_location.point, %s)'},
|
||
select_params=[spatial_filters.get('reference_point')]
|
||
) if spatial_filters.get('reference_point') else base_queryset
|
||
|
||
# Limit radius searches to reasonable bounds
|
||
max_radius = min(spatial_filters.get('radius', 100), 500) # Cap at 500 miles
|
||
|
||
# Pre-filter with bounding box before distance calculation
|
||
if spatial_filters.get('reference_point'):
|
||
# Create bounding box slightly larger than radius for pre-filtering
|
||
bbox = self._create_bounding_box(
|
||
spatial_filters['reference_point'],
|
||
max_radius * 1.1
|
||
)
|
||
queryset = queryset.filter(park_location__point__within=bbox)
|
||
|
||
return queryset
|
||
```
|
||
|
||
## 3. "Near Me" Functionality Design
|
||
|
||
### 3.1 Geolocation Integration
|
||
|
||
#### Frontend Geolocation Handling
|
||
```javascript
|
||
class LocationSearchService {
|
||
constructor() {
|
||
this.userLocation = null;
|
||
this.locationPermission = 'prompt';
|
||
}
|
||
|
||
async requestUserLocation() {
|
||
try {
|
||
if (!navigator.geolocation) {
|
||
throw new Error('Geolocation not supported');
|
||
}
|
||
|
||
const position = await new Promise((resolve, reject) => {
|
||
navigator.geolocation.getCurrentPosition(
|
||
resolve,
|
||
reject,
|
||
{
|
||
enableHighAccuracy: true,
|
||
timeout: 10000,
|
||
maximumAge: 300000 // 5 minutes
|
||
}
|
||
);
|
||
});
|
||
|
||
this.userLocation = {
|
||
lat: position.coords.latitude,
|
||
lng: position.coords.longitude,
|
||
accuracy: position.coords.accuracy
|
||
};
|
||
|
||
this.locationPermission = 'granted';
|
||
return this.userLocation;
|
||
|
||
} catch (error) {
|
||
this.locationPermission = 'denied';
|
||
await this._handleLocationError(error);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
async _handleLocationError(error) {
|
||
switch(error.code) {
|
||
case error.PERMISSION_DENIED:
|
||
await this._tryIPLocationFallback();
|
||
break;
|
||
case error.POSITION_UNAVAILABLE:
|
||
this._showLocationUnavailableMessage();
|
||
break;
|
||
case error.TIMEOUT:
|
||
this._showTimeoutMessage();
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
#### Privacy and Permission Handling
|
||
```python
|
||
class LocationPrivacyMixin:
|
||
"""Handle location privacy concerns"""
|
||
|
||
def get_context_data(self, **kwargs):
|
||
context = super().get_context_data(**kwargs)
|
||
|
||
# Add location permission context
|
||
context.update({
|
||
'location_features_available': True,
|
||
'privacy_policy_url': reverse('privacy'),
|
||
'location_consent_required': True,
|
||
'ip_location_available': self._ip_location_available(),
|
||
})
|
||
|
||
return context
|
||
|
||
def _ip_location_available(self) -> bool:
|
||
"""Check if IP-based location estimation is available"""
|
||
# Could integrate with MaxMind GeoIP or similar service
|
||
return hasattr(self.request, 'META') and 'HTTP_CF_IPCOUNTRY' in self.request.META
|
||
```
|
||
|
||
### 3.2 Fallback Strategies
|
||
|
||
#### IP-Based Location Approximation
|
||
```python
|
||
class IPLocationService:
|
||
"""Fallback location service using IP geolocation"""
|
||
|
||
def get_approximate_location(self, request) -> Optional[Dict]:
|
||
"""Get approximate location from IP address"""
|
||
try:
|
||
# Try Cloudflare country header first
|
||
country = request.META.get('HTTP_CF_IPCOUNTRY')
|
||
if country and country != 'XX':
|
||
return self._country_to_coordinates(country)
|
||
|
||
# Fallback to GeoIP database
|
||
ip_address = self._get_client_ip(request)
|
||
return self._geoip_lookup(ip_address)
|
||
|
||
except Exception:
|
||
return None
|
||
|
||
def _country_to_coordinates(self, country_code: str) -> Dict:
|
||
"""Convert country code to approximate center coordinates"""
|
||
country_centers = {
|
||
'US': {'lat': 39.8283, 'lng': -98.5795, 'accuracy': 'country'},
|
||
'CA': {'lat': 56.1304, 'lng': -106.3468, 'accuracy': 'country'},
|
||
# Add more countries as needed
|
||
}
|
||
return country_centers.get(country_code.upper())
|
||
```
|
||
|
||
## 4. Location-Based Filtering Integration
|
||
|
||
### 4.1 Enhanced Filter Integration
|
||
|
||
#### Geographic Region Filters
|
||
```python
|
||
class GeographicFilterMixin:
|
||
"""Add geographic filtering to existing filter system"""
|
||
|
||
# State/Province filtering
|
||
state = ModelChoiceFilter(
|
||
field_name='park_location__state',
|
||
queryset=None, # Dynamically populated
|
||
empty_label='Any state/province',
|
||
method='filter_by_state'
|
||
)
|
||
|
||
# Country filtering
|
||
country = ModelChoiceFilter(
|
||
field_name='park_location__country',
|
||
queryset=None, # Dynamically populated
|
||
empty_label='Any country',
|
||
method='filter_by_country'
|
||
)
|
||
|
||
# Metropolitan area clustering
|
||
metro_area = ChoiceFilter(
|
||
method='filter_by_metro_area',
|
||
choices=[] # Dynamically populated
|
||
)
|
||
|
||
def filter_by_metro_area(self, queryset, name, value):
|
||
"""Filter by predefined metropolitan areas"""
|
||
metro_definitions = {
|
||
'orlando': {
|
||
'center': Point(-81.3792, 28.5383),
|
||
'radius_miles': 30
|
||
},
|
||
'los_angeles': {
|
||
'center': Point(-118.2437, 34.0522),
|
||
'radius_miles': 50
|
||
},
|
||
# Add more metropolitan areas
|
||
}
|
||
|
||
metro = metro_definitions.get(value)
|
||
if not metro:
|
||
return queryset
|
||
|
||
from django.contrib.gis.measure import D
|
||
return queryset.filter(
|
||
park_location__point__distance_lte=(
|
||
metro['center'], D(mi=metro['radius_miles'])
|
||
)
|
||
)
|
||
```
|
||
|
||
#### Performance-Optimized Filtering
|
||
```python
|
||
class OptimizedLocationFilter(GeographicFilterMixin, ParkFilter):
|
||
"""Location filtering with performance optimizations"""
|
||
|
||
def __init__(self, *args, **kwargs):
|
||
super().__init__(*args, **kwargs)
|
||
|
||
# Dynamically populate geographic choices based on available data
|
||
self._populate_geographic_choices()
|
||
|
||
def _populate_geographic_choices(self):
|
||
"""Populate geographic filter choices efficiently"""
|
||
|
||
# Cache geographic options for performance
|
||
cache_key = 'location_filter_choices'
|
||
cached_choices = cache.get(cache_key)
|
||
|
||
if not cached_choices:
|
||
# Query distinct values efficiently
|
||
states = ParkLocation.objects.values_list(
|
||
'state', flat=True
|
||
).distinct().order_by('state')
|
||
|
||
countries = ParkLocation.objects.values_list(
|
||
'country', flat=True
|
||
).distinct().order_by('country')
|
||
|
||
cached_choices = {
|
||
'states': [(s, s) for s in states if s],
|
||
'countries': [(c, c) for c in countries if c]
|
||
}
|
||
|
||
cache.set(cache_key, cached_choices, timeout=3600) # 1 hour
|
||
|
||
# Update filter choices
|
||
self.filters['state'].extra['choices'] = cached_choices['states']
|
||
self.filters['country'].extra['choices'] = cached_choices['countries']
|
||
```
|
||
|
||
## 5. Query Integration Patterns
|
||
|
||
### 5.1 Hybrid Search Scoring
|
||
|
||
#### Relevance + Proximity Scoring
|
||
```python
|
||
class HybridSearchRanking:
|
||
"""Combine text relevance with geographic proximity"""
|
||
|
||
def rank_results(self, queryset, search_query: str, user_location: Point = None):
|
||
"""Apply hybrid ranking algorithm"""
|
||
|
||
# Base text relevance scoring
|
||
queryset = queryset.annotate(
|
||
text_rank=Case(
|
||
When(name__iexact=search_query, then=Value(100)),
|
||
When(name__icontains=search_query, then=Value(80)),
|
||
When(description__icontains=search_query, then=Value(60)),
|
||
When(park_location__city__icontains=search_query, then=Value(40)),
|
||
default=Value(20),
|
||
output_field=IntegerField()
|
||
)
|
||
)
|
||
|
||
# Add proximity scoring if user location available
|
||
if user_location:
|
||
queryset = queryset.annotate(
|
||
distance_miles=Distance('park_location__point', user_location),
|
||
proximity_rank=Case(
|
||
When(distance_miles__lt=25, then=Value(50)), # Very close
|
||
When(distance_miles__lt=100, then=Value(30)), # Close
|
||
When(distance_miles__lt=300, then=Value(10)), # Regional
|
||
default=Value(0),
|
||
output_field=IntegerField()
|
||
)
|
||
)
|
||
|
||
# Combined score: text relevance + proximity bonus
|
||
queryset = queryset.annotate(
|
||
combined_rank=F('text_rank') + F('proximity_rank')
|
||
).order_by('-combined_rank', 'distance_miles')
|
||
else:
|
||
queryset = queryset.order_by('-text_rank', 'name')
|
||
|
||
return queryset
|
||
```
|
||
|
||
### 5.2 Cross-Domain Location Search
|
||
|
||
#### Unified Search Across Entities
|
||
```python
|
||
class UnifiedLocationSearchView(TemplateView):
|
||
"""Search across parks, rides, and companies with location context"""
|
||
|
||
template_name = "core/search/unified_results.html"
|
||
|
||
def get_context_data(self, **kwargs):
|
||
context = super().get_context_data(**kwargs)
|
||
|
||
query = self.request.GET.get('q', '')
|
||
location_types = self.request.GET.getlist('types', ['park', 'ride', 'company'])
|
||
user_location = self._get_user_location()
|
||
|
||
results = {
|
||
'parks': [],
|
||
'rides': [],
|
||
'companies': [],
|
||
'unified': []
|
||
}
|
||
|
||
# Search each entity type with location context
|
||
if 'park' in location_types:
|
||
results['parks'] = self._search_parks(query, user_location)
|
||
|
||
if 'ride' in location_types:
|
||
results['rides'] = self._search_rides(query, user_location)
|
||
|
||
if 'company' in location_types:
|
||
results['companies'] = self._search_companies(query, user_location)
|
||
|
||
# Create unified ranked results
|
||
results['unified'] = self._create_unified_results(results, user_location)
|
||
|
||
context.update({
|
||
'results': results,
|
||
'search_query': query,
|
||
'user_location': user_location,
|
||
'total_results': sum(len(r) for r in results.values() if isinstance(r, list))
|
||
})
|
||
|
||
return context
|
||
```
|
||
|
||
## 6. Geocoding Integration Strategy
|
||
|
||
### 6.1 OpenStreetMap Nominatim Integration
|
||
|
||
#### Geocoding Service Implementation
|
||
```python
|
||
class GeocodingService:
|
||
"""Geocoding service using OpenStreetMap Nominatim"""
|
||
|
||
def __init__(self):
|
||
self.base_url = "https://nominatim.openstreetmap.org"
|
||
self.session = requests.Session()
|
||
self.session.headers.update({
|
||
'User-Agent': 'ThrillWiki/1.0 (contact@thrillwiki.com)'
|
||
})
|
||
|
||
def geocode_address(self, address: str, country_bias: str = None) -> Optional[Dict]:
|
||
"""Convert address to coordinates"""
|
||
cache_key = f"geocode:{hashlib.md5(address.encode()).hexdigest()}"
|
||
cached = cache.get(cache_key)
|
||
if cached:
|
||
return cached
|
||
|
||
params = {
|
||
'q': address,
|
||
'format': 'json',
|
||
'limit': 1,
|
||
'addressdetails': 1,
|
||
'extratags': 1
|
||
}
|
||
|
||
if country_bias:
|
||
params['countrycodes'] = country_bias
|
||
|
||
try:
|
||
response = self.session.get(
|
||
f"{self.base_url}/search",
|
||
params=params,
|
||
timeout=5
|
||
)
|
||
response.raise_for_status()
|
||
|
||
results = response.json()
|
||
if results:
|
||
result = results[0]
|
||
geocoded = {
|
||
'lat': float(result['lat']),
|
||
'lng': float(result['lon']),
|
||
'display_name': result['display_name'],
|
||
'confidence': float(result.get('importance', 0.5)),
|
||
'address_components': result.get('address', {})
|
||
}
|
||
|
||
# Cache successful results for 7 days
|
||
cache.set(cache_key, geocoded, timeout=604800)
|
||
return geocoded
|
||
|
||
except (requests.RequestException, ValueError, KeyError) as e:
|
||
logger.warning(f"Geocoding failed for '{address}': {e}")
|
||
|
||
# Cache failed attempts for 1 hour to prevent repeated API calls
|
||
cache.set(cache_key, None, timeout=3600)
|
||
return None
|
||
|
||
def reverse_geocode(self, lat: float, lng: float) -> Optional[Dict]:
|
||
"""Convert coordinates to address"""
|
||
cache_key = f"reverse_geocode:{lat:.4f},{lng:.4f}"
|
||
cached = cache.get(cache_key)
|
||
if cached:
|
||
return cached
|
||
|
||
params = {
|
||
'lat': lat,
|
||
'lon': lng,
|
||
'format': 'json',
|
||
'addressdetails': 1
|
||
}
|
||
|
||
try:
|
||
response = self.session.get(
|
||
f"{self.base_url}/reverse",
|
||
params=params,
|
||
timeout=5
|
||
)
|
||
response.raise_for_status()
|
||
|
||
result = response.json()
|
||
if result:
|
||
address = {
|
||
'display_name': result['display_name'],
|
||
'components': result.get('address', {})
|
||
}
|
||
|
||
cache.set(cache_key, address, timeout=604800) # 7 days
|
||
return address
|
||
|
||
except (requests.RequestException, ValueError) as e:
|
||
logger.warning(f"Reverse geocoding failed for {lat},{lng}: {e}")
|
||
|
||
return None
|
||
```
|
||
|
||
### 6.2 Search Query Enhancement
|
||
|
||
#### Intelligent Address Detection
|
||
```python
|
||
class SmartQueryProcessor:
|
||
"""Detect and process different types of search queries"""
|
||
|
||
def __init__(self):
|
||
self.geocoding_service = GeocodingService()
|
||
|
||
def process_search_query(self, query: str) -> Dict:
|
||
"""Analyze query and determine search strategy"""
|
||
|
||
query_analysis = {
|
||
'original_query': query,
|
||
'is_address': self._looks_like_address(query),
|
||
'is_coordinates': self._looks_like_coordinates(query),
|
||
'has_location_keywords': self._has_location_keywords(query),
|
||
'processed_query': query,
|
||
'geocoded_location': None,
|
||
'search_strategy': 'text'
|
||
}
|
||
|
||
if query_analysis['is_coordinates']:
|
||
coords = self._parse_coordinates(query)
|
||
if coords:
|
||
query_analysis['geocoded_location'] = coords
|
||
query_analysis['search_strategy'] = 'spatial'
|
||
|
||
elif query_analysis['is_address'] or query_analysis['has_location_keywords']:
|
||
geocoded = self.geocoding_service.geocode_address(query)
|
||
if geocoded:
|
||
query_analysis['geocoded_location'] = geocoded
|
||
query_analysis['search_strategy'] = 'spatial_text_hybrid'
|
||
|
||
return query_analysis
|
||
|
||
def _looks_like_address(self, query: str) -> bool:
|
||
"""Detect if query looks like an address"""
|
||
address_patterns = [
|
||
r'\d+\s+\w+\s+(street|st|avenue|ave|road|rd|boulevard|blvd)',
|
||
r'\w+,\s*\w+\s*\d{5}', # City, State ZIP
|
||
r'\w+,\s*\w+,\s*\w+', # City, State, Country
|
||
]
|
||
|
||
return any(re.search(pattern, query, re.IGNORECASE) for pattern in address_patterns)
|
||
|
||
def _looks_like_coordinates(self, query: str) -> bool:
|
||
"""Detect if query contains coordinates"""
|
||
coord_pattern = r'-?\d+\.?\d*\s*,\s*-?\d+\.?\d*'
|
||
return bool(re.search(coord_pattern, query))
|
||
```
|
||
|
||
## 7. Performance Optimization Strategy
|
||
|
||
### 7.1 Database Optimization
|
||
|
||
#### Spatial Index Strategy
|
||
```sql
|
||
-- Essential indexes for location-enhanced search
|
||
CREATE INDEX CONCURRENTLY idx_park_location_point_gist
|
||
ON parks_parklocation USING GIST (point);
|
||
|
||
CREATE INDEX CONCURRENTLY idx_park_location_city_state
|
||
ON parks_parklocation (city, state);
|
||
|
||
CREATE INDEX CONCURRENTLY idx_park_location_country
|
||
ON parks_parklocation (country);
|
||
|
||
-- Composite indexes for common filter combinations
|
||
CREATE INDEX CONCURRENTLY idx_park_status_location
|
||
ON parks_park (status)
|
||
INCLUDE (id)
|
||
WHERE status = 'OPERATING';
|
||
|
||
-- Partial indexes for performance
|
||
CREATE INDEX CONCURRENTLY idx_ride_location_point_not_null
|
||
ON rides_ridelocation USING GIST (entrance_point)
|
||
WHERE entrance_point IS NOT NULL;
|
||
```
|
||
|
||
#### Query Optimization Patterns
|
||
```python
|
||
class SpatialQueryOptimizer:
|
||
"""Optimize spatial queries for performance"""
|
||
|
||
def optimize_distance_query(self, queryset, reference_point, max_radius_miles=100):
|
||
"""Optimize distance-based queries with bounding box pre-filtering"""
|
||
|
||
# Create bounding box for initial filtering (much faster than distance calc)
|
||
bbox = self._create_bounding_box(reference_point, max_radius_miles)
|
||
|
||
# Pre-filter with bounding box, then apply precise distance filter
|
||
return queryset.filter(
|
||
park_location__point__within=bbox
|
||
).filter(
|
||
park_location__point__distance_lte=(reference_point, D(mi=max_radius_miles))
|
||
).annotate(
|
||
distance=Distance('park_location__point', reference_point)
|
||
).order_by('distance')
|
||
|
||
def optimize_multi_location_query(self, park_qs, ride_qs, bounds=None):
|
||
"""Optimize queries across multiple location types"""
|
||
|
||
# Use common table expressions for complex spatial queries
|
||
if bounds:
|
||
# Apply spatial filtering early
|
||
park_qs = park_qs.filter(park_location__point__within=bounds)
|
||
ride_qs = ride_qs.filter(ride_location__entrance_point__within=bounds)
|
||
|
||
# Use union for combining different location types efficiently
|
||
return park_qs.union(ride_qs, all=False)
|
||
```
|
||
|
||
### 7.2 Caching Strategy
|
||
|
||
#### Multi-Level Caching Architecture
|
||
```python
|
||
class LocationSearchCache:
|
||
"""Caching strategy for location-enhanced search"""
|
||
|
||
CACHE_TIMEOUTS = {
|
||
'geocoding': 604800, # 7 days
|
||
'search_results': 300, # 5 minutes
|
||
'location_filters': 3600, # 1 hour
|
||
'spatial_index': 86400, # 24 hours
|
||
}
|
||
|
||
def cache_search_results(self, cache_key: str, results: QuerySet,
|
||
user_location: Point = None):
|
||
"""Cache search results with location context"""
|
||
|
||
# Include location context in cache key
|
||
if user_location:
|
||
location_hash = hashlib.md5(
|
||
f"{user_location.x:.4f},{user_location.y:.4f}".encode()
|
||
).hexdigest()[:8]
|
||
cache_key = f"{cache_key}_loc_{location_hash}"
|
||
|
||
# Cache serialized results
|
||
serialized = self._serialize_results(results)
|
||
cache.set(
|
||
cache_key,
|
||
serialized,
|
||
timeout=self.CACHE_TIMEOUTS['search_results']
|
||
)
|
||
|
||
def get_cached_search_results(self, cache_key: str, user_location: Point = None):
|
||
"""Retrieve cached search results"""
|
||
|
||
if user_location:
|
||
location_hash = hashlib.md5(
|
||
f"{user_location.x:.4f},{user_location.y:.4f}".encode()
|
||
).hexdigest()[:8]
|
||
cache_key = f"{cache_key}_loc_{location_hash}"
|
||
|
||
cached = cache.get(cache_key)
|
||
return self._deserialize_results(cached) if cached else None
|
||
|
||
def invalidate_location_cache(self, location_type: str = None):
|
||
"""Invalidate location-related caches when data changes"""
|
||
patterns = [
|
||
'search_results_*',
|
||
'location_filters',
|
||
'geocoding_*'
|
||
]
|
||
|
||
if location_type:
|
||
patterns.append(f'{location_type}_search_*')
|
||
|
||
# Use cache versioning for efficient invalidation
|
||
for pattern in patterns:
|
||
cache.delete_pattern(pattern)
|
||
```
|
||
|
||
### 7.3 Performance Monitoring
|
||
|
||
#### Search Performance Metrics
|
||
```python
|
||
class SearchPerformanceMonitor:
|
||
"""Monitor search performance and spatial query efficiency"""
|
||
|
||
def __init__(self):
|
||
self.metrics_logger = logging.getLogger('search.performance')
|
||
|
||
def track_search_query(self, query_type: str, query_params: Dict,
|
||
execution_time: float, result_count: int):
|
||
"""Track search query performance"""
|
||
|
||
metrics = {
|
||
'query_type': query_type,
|
||
'has_spatial_filter': bool(query_params.get('user_location')),
|
||
'has_text_search': bool(query_params.get('search')),
|
||
'filter_count': len([k for k, v in query_params.items() if v]),
|
||
'execution_time_ms': execution_time * 1000,
|
||
'result_count': result_count,
|
||
'timestamp': timezone.now().isoformat()
|
||
}
|
||
|
||
# Log performance data
|
||
self.metrics_logger.info(json.dumps(metrics))
|
||
|
||
# Alert on slow queries
|
||
if execution_time > 1.0: # Queries over 1 second
|
||
self._alert_slow_query(metrics)
|
||
|
||
def _alert_slow_query(self, metrics: Dict):
|
||
"""Alert on performance issues"""
|
||
# Implementation for alerting system
|
||
pass
|
||
```
|
||
|
||
## 8. API Enhancement Design
|
||
|
||
### 8.1 Location-Aware Search Endpoints
|
||
|
||
#### Enhanced Search API
|
||
```python
|
||
class LocationSearchAPIView(APIView):
|
||
"""REST API for location-enhanced search"""
|
||
|
||
def get(self, request):
|
||
"""
|
||
Enhanced search endpoint with location capabilities
|
||
|
||
Query Parameters:
|
||
- q: Search query (text)
|
||
- lat, lng: User coordinates for proximity search
|
||
- radius: Search radius in miles (default: 50)
|
||
- bounds: Geographic bounding box (format: north,south,east,west)
|
||
- types: Entity types to search (park,ride,company)
|
||
- filters: Additional filters (status, operator, etc.)
|
||
"""
|
||
|
||
try:
|
||
search_params = self._parse_search_params(request.GET)
|
||
results = self._execute_search(search_params)
|
||
|
||
return Response({
|
||
'status': 'success',
|
||
'data': {
|
||
'results': results['items'],
|
||
'total_count': results['total'],
|
||
'search_params': search_params,
|
||
'has_more': results['has_more']
|
||
},
|
||
'meta': {
|
||
'query_time_ms': results['execution_time'] * 1000,
|
||
'cache_hit': results['from_cache'],
|
||
'location_used': bool(search_params.get('user_location'))
|
||
}
|
||
})
|
||
|
||
except ValidationError as e:
|
||
return Response({
|
||
'status': 'error',
|
||
'error': 'Invalid search parameters',
|
||
'details': str(e)
|
||
}, status=400)
|
||
|
||
def _parse_search_params(self, params: QueryDict) -> Dict:
|
||
"""Parse and validate search parameters"""
|
||
|
||
# Parse user location
|
||
user_location = None
|
||
if params.get('lat') and params.get('lng'):
|
||
try:
|
||
lat = float(params['lat'])
|
||
lng = float(params['lng'])
|
||
if -90 <= lat <= 90 and -180 <= lng <= 180:
|
||
user_location = Point(lng, lat)
|
||
except (ValueError, TypeError):
|
||
raise ValidationError("Invalid coordinates")
|
||
|
||
# Parse bounding box
|
||
bounds = None
|
||
if params.get('bounds'):
|
||
try:
|
||
north, south, east, west = map(float, params['bounds'].split(','))
|
||
bounds = Polygon.from_bbox((west, south, east, north))
|
||
except (ValueError, TypeError):
|
||
raise ValidationError("Invalid bounds format")
|
||
|
||
return {
|
||
'query': params.get('q', '').strip(),
|
||
'user_location': user_location,
|
||
'radius_miles': min(float(params.get('radius', 50)), 500), # Cap at 500 miles
|
||
'bounds': bounds,
|
||
'entity_types': params.getlist('types') or ['park'],
|
||
'filters': self._parse_filters(params),
|
||
'page': int(params.get('page', 1)),
|
||
'page_size': min(int(params.get('page_size', 20)), 100) # Cap at 100
|
||
}
|
||
```
|
||
|
||
#### Autocomplete API with Location Context
|
||
```python
|
||
class LocationAutocompleteAPIView(APIView):
|
||
"""Autocomplete API with location awareness"""
|
||
|
||
def get(self, request):
|
||
"""
|
||
Location-aware autocomplete for search queries
|
||
|
||
Returns suggestions based on:
|
||
1. Entity names (parks, rides, companies)
|
||
2. Location names (cities, states, countries)
|
||
3. Address suggestions
|
||
"""
|
||
|
||
query = request.GET.get('q', '').strip()
|
||
if len(query) < 2:
|
||
return Response({'suggestions': []})
|
||
|
||
user_location = self._parse_user_location(request.GET)
|
||
|
||
suggestions = []
|
||
|
||
# Entity name suggestions (with location context for ranking)
|
||
entity_suggestions = self._get_entity_suggestions(query, user_location)
|
||
suggestions.extend(entity_suggestions)
|
||
|
||
# Location name suggestions
|
||
location_suggestions = self._get_location_suggestions(query)
|
||
suggestions.extend(location_suggestions)
|
||
|
||
# Address suggestions (via geocoding)
|
||
if self._looks_like_address(query):
|
||
address_suggestions = self._get_address_suggestions(query)
|
||
suggestions.extend(address_suggestions)
|
||
|
||
# Rank and limit suggestions
|
||
ranked_suggestions = self._rank_suggestions(suggestions, user_location)[:10]
|
||
|
||
return Response({
|
||
'suggestions': ranked_suggestions,
|
||
'query': query
|
||
})
|
||
```
|
||
|
||
### 8.2 Enhanced Response Formats
|
||
|
||
#### Unified Location Response Format
|
||
```python
|
||
class LocationSearchResultSerializer(serializers.Serializer):
|
||
"""Unified serializer for location-enhanced search results"""
|
||
|
||
id = serializers.CharField()
|
||
type = serializers.CharField() # 'park', 'ride', 'company'
|
||
name = serializers.CharField()
|
||
slug = serializers.CharField()
|
||
|
||
# Location data
|
||
location = serializers.SerializerMethodField()
|
||
|
||
# Entity-specific data
|
||
entity_data = serializers.SerializerMethodField()
|
||
|
||
# Search relevance
|
||
relevance_score = serializers.FloatField(required=False)
|
||
distance_miles = serializers.FloatField(required=False)
|
||
|
||
def get_location(self, obj):
|
||
"""Get unified location data"""
|
||
if hasattr(obj, 'park_location'):
|
||
location = obj.park_location
|
||
return {
|
||
'coordinates': [location.point.y, location.point.x] if location.point else None,
|
||
'address': location.formatted_address,
|
||
'city': location.city,
|
||
'state': location.state,
|
||
'country': location.country,
|
||
'highway_exit': location.highway_exit
|
||
}
|
||
elif hasattr(obj, 'ride_location'):
|
||
location = obj.ride_location
|
||
return {
|
||
'coordinates': [location.entrance_point.y, location.entrance_point.x]
|
||
if location.entrance_point else None,
|
||
'park_area': location.park_area,
|
||
'park_location': self._get_park_location(obj.park)
|
||
}
|
||
# Add other location types...
|
||
return None
|
||
|
||
def get_entity_data(self, obj):
|
||
"""Get entity-specific data based on type"""
|
||
if obj._meta.model_name == 'park':
|
||
return {
|
||
'status': obj.status,
|
||
'operator': obj.operating_company.name if obj.operating_company else None,
|
||
'ride_count': getattr(obj, 'ride_count', 0),
|
||
'coaster_count': getattr(obj, 'coaster_count', 0),
|
||
'website': obj.website,
|
||
'opening_date': obj.opening_date
|
||
}
|
||
elif obj._meta.model_name == 'ride':
|
||
return {
|
||
'category': obj.category,
|
||
'park': {
|
||
'name': obj.park.name,
|
||
'slug': obj.park.slug
|
||
},
|
||
'manufacturer': obj.ride_model.manufacturer.name
|
||
if obj.ride_model and obj.ride_model.manufacturer else None,
|
||
'opening_date': obj.opening_date
|
||
}
|
||
# Add other entity types...
|
||
return {}
|
||
```
|
||
|
||
## 9. User Experience Design
|
||
|
||
### 9.1 Search Interface Enhancements
|
||
|
||
#### Location-Aware Search Form
|
||
```html
|
||
<!-- Enhanced search form with location features -->
|
||
<form id="location-search-form" class="search-form">
|
||
<!-- Primary search input -->
|
||
<div class="search-input-group">
|
||
<input type="text"
|
||
id="search-query"
|
||
name="q"
|
||
placeholder="Search parks, rides, or locations..."
|
||
autocomplete="off"
|
||
data-autocomplete-url="{% url 'api:autocomplete' %}">
|
||
|
||
<!-- Location context button -->
|
||
<button type="button"
|
||
id="use-my-location"
|
||
class="location-btn"
|
||
title="Search near my location">
|
||
<i class="icon-location"></i>
|
||
<span class="sr-only">Use my location</span>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Location context display -->
|
||
<div id="location-context" class="location-context hidden">
|
||
<span class="location-label">Searching near:</span>
|
||
<span id="location-display"></span>
|
||
<button type="button" id="clear-location" class="clear-btn">×</button>
|
||
</div>
|
||
|
||
<!-- Advanced filters toggle -->
|
||
<div class="filter-controls">
|
||
<button type="button"
|
||
id="toggle-filters"
|
||
class="filter-toggle"
|
||
aria-expanded="false">
|
||
<i class="icon-filter"></i>
|
||
Filters
|
||
</button>
|
||
|
||
<!-- Location-specific filters -->
|
||
<div id="location-filters" class="filter-section hidden">
|
||
<label for="search-radius">Within:</label>
|
||
<select id="search-radius" name="radius">
|
||
<option value="25">25 miles</option>
|
||
<option value="50" selected>50 miles</option>
|
||
<option value="100">100 miles</option>
|
||
<option value="250">250 miles</option>
|
||
</select>
|
||
|
||
<label for="entity-types">Show:</label>
|
||
<div class="checkbox-group">
|
||
<label><input type="checkbox" name="types" value="park" checked> Parks</label>
|
||
<label><input type="checkbox" name="types" value="ride"> Rides</label>
|
||
<label><input type="checkbox" name="types" value="company"> Companies</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</form>
|
||
```
|
||
|
||
#### Location Permission Handling
|
||
```javascript
|
||
class LocationPermissionManager {
|
||
constructor() {
|
||
this.permissionStatus = 'unknown';
|
||
this.setupEventHandlers();
|
||
}
|
||
|
||
setupEventHandlers() {
|
||
// Location button click handler
|
||
document.getElementById('use-my-location').addEventListener('click',
|
||
() => this.requestLocation());
|
||
|
||
// Privacy-conscious permission checking
|
||
if ('permissions' in navigator) {
|
||
navigator.permissions.query({name: 'geolocation'})
|
||
.then(permission => {
|
||
this.permissionStatus = permission.state;
|
||
this.updateLocationButton();
|
||
|
||
permission.addEventListener('change', () => {
|
||
this.permissionStatus = permission.state;
|
||
this.updateLocationButton();
|
||
});
|
||
});
|
||
}
|
||
}
|
||
|
||
async requestLocation() {
|
||
// Show privacy notice if first time
|
||
if (this.permissionStatus === 'prompt') {
|
||
const consent = await this.showPrivacyConsent();
|
||
if (!consent) return;
|
||
}
|
||
|
||
try {
|
||
this.showLocationLoading();
|
||
const location = await this.getCurrentPosition();
|
||
this.handleLocationSuccess(location);
|
||
} catch (error) {
|
||
this.handleLocationError(error);
|
||
}
|
||
}
|
||
|
||
showPrivacyConsent() {
|
||
return new Promise(resolve => {
|
||
const modal = document.createElement('div');
|
||
modal.className = 'location-consent-modal';
|
||
modal.innerHTML = `
|
||
<div class="modal-content">
|
||
<h3>Use Your Location</h3>
|
||
<p>ThrillWiki would like to use your location to show nearby parks and improve search results.</p>
|
||
<p>Your location data is not stored or shared with third parties.</p>
|
||
<div class="modal-actions">
|
||
<button class="btn-secondary" data-action="deny">No Thanks</button>
|
||
<button class="btn-primary" data-action="allow">Allow Location</button>
|
||
</div>
|
||
<a href="/privacy" target="_blank">Privacy Policy</a>
|
||
</div>
|
||
`;
|
||
|
||
modal.addEventListener('click', (e) => {
|
||
if (e.target.dataset.action === 'allow') {
|
||
resolve(true);
|
||
} else if (e.target.dataset.action === 'deny') {
|
||
resolve(false);
|
||
}
|
||
modal.remove();
|
||
});
|
||
|
||
document.body.appendChild(modal);
|
||
});
|
||
}
|
||
}
|
||
```
|
||
|
||
### 9.2 Results Display with Location Context
|
||
|
||
#### Distance-Enhanced Results
|
||
```html
|
||
<!-- Results template with location context -->
|
||
<div class="search-results">
|
||
{% for result in results %}
|
||
<div class="result-item" data-type="{{ result.type }}" data-id="{{ result.id }}">
|
||
<div class="result-header">
|
||
<h3 class="result-title">
|
||
<a href="{{ result.get_absolute_url }}">{{ result.name }}</a>
|
||
</h3>
|
||
|
||
{% if result.distance_miles %}
|
||
<div class="distance-badge">
|
||
<i class="icon-location-arrow"></i>
|
||
{{ result.distance_miles|floatformat:1 }} miles away
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<div class="result-location">
|
||
{% if result.location.address %}
|
||
<i class="icon-map-pin"></i>
|
||
{{ result.location.address }}
|
||
|
||
{% if result.location.highway_exit %}
|
||
<div class="highway-info">
|
||
<i class="icon-highway"></i>
|
||
Exit: {{ result.location.highway_exit }}
|
||
</div>
|
||
{% endif %}
|
||
{% endif %}
|
||
</div>
|
||
|
||
<div class="result-actions">
|
||
<a href="{{ result.get_absolute_url }}" class="btn-primary">View Details</a>
|
||
|
||
{% if result.location.coordinates %}
|
||
<button type="button"
|
||
class="btn-secondary get-directions"
|
||
data-lat="{{ result.location.coordinates.0 }}"
|
||
data-lng="{{ result.location.coordinates.1 }}"
|
||
data-name="{{ result.name }}">
|
||
<i class="icon-directions"></i>
|
||
Directions
|
||
</button>
|
||
{% endif %}
|
||
|
||
{% if user_location and result.location.coordinates %}
|
||
<button type="button"
|
||
class="btn-secondary show-on-map"
|
||
data-result-id="{{ result.id }}">
|
||
<i class="icon-map"></i>
|
||
Show on Map
|
||
</button>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
```
|
||
|
||
### 9.3 Map Integration
|
||
|
||
#### Search-Map Bidirectional Integration
|
||
```javascript
|
||
class SearchMapIntegration {
|
||
constructor(mapInstance, searchForm) {
|
||
this.map = mapInstance;
|
||
this.searchForm = searchForm;
|
||
this.searchResults = [];
|
||
this.setupIntegration();
|
||
}
|
||
|
||
setupIntegration() {
|
||
// Update search when map viewport changes
|
||
this.map.on('moveend', () => {
|
||
if (this.shouldUpdateSearchOnMapMove()) {
|
||
this.updateSearchFromMap();
|
||
}
|
||
});
|
||
|
||
// Show search results on map
|
||
this.searchForm.addEventListener('results-updated', (e) => {
|
||
this.showResultsOnMap(e.detail.results);
|
||
});
|
||
|
||
// Handle "Show on Map" button clicks
|
||
document.addEventListener('click', (e) => {
|
||
if (e.target.matches('.show-on-map')) {
|
||
const resultId = e.target.dataset.resultId;
|
||
this.highlightResultOnMap(resultId);
|
||
}
|
||
});
|
||
}
|
||
|
||
updateSearchFromMap() {
|
||
const bounds = this.map.getBounds();
|
||
const boundsParam = [
|
||
bounds.getNorth(),
|
||
bounds.getSouth(),
|
||
bounds.getEast(),
|
||
bounds.getWest()
|
||
].join(',');
|
||
|
||
// Update search form with map bounds
|
||
const boundsInput = document.getElementById('search-bounds');
|
||
if (boundsInput) {
|
||
boundsInput.value = boundsParam;
|
||
this.searchForm.submit();
|
||
}
|
||
}
|
||
|
||
showResultsOnMap(results) {
|
||
// Clear existing result markers
|
||
this.clearResultMarkers();
|
||
|
||
// Add markers for each result with location
|
||
results.forEach(result => {
|
||
if (result.location && result.location.coordinates) {
|
||
const marker = this.addResultMarker(result);
|
||
this.searchResults.push({
|
||
id: result.id,
|
||
marker: marker,
|
||
data: result
|
||
});
|
||
}
|
||
});
|
||
|
||
// Adjust map view to show all results
|
||
if (this.searchResults.length > 0) {
|
||
this.fitMapToResults();
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
## 10. Implementation Phases
|
||
|
||
### Phase 1: Foundation (Weeks 1-2)
|
||
**Goal**: Establish basic location search infrastructure
|
||
|
||
**Tasks**:
|
||
1. **Enhanced Filter Classes**
|
||
- Extend [`ParkFilter`](parks/filters.py:26) with spatial search mixins
|
||
- Create `DistanceSearchMixin` and `GeographicFilterMixin`
|
||
- Add location-based filter fields (state, country, metro area)
|
||
|
||
2. **Geocoding Service Integration**
|
||
- Implement `GeocodingService` with OpenStreetMap Nominatim
|
||
- Add address detection and coordinate parsing
|
||
- Set up caching layer for geocoding results
|
||
|
||
3. **Database Optimization**
|
||
- Add spatial indexes to location models
|
||
- Create composite indexes for common filter combinations
|
||
- Optimize existing queries for location joins
|
||
|
||
**Deliverables**:
|
||
- Enhanced filter classes with location capabilities
|
||
- Working geocoding service with caching
|
||
- Optimized database indexes
|
||
|
||
### Phase 2: Core Search Enhancement (Weeks 3-4)
|
||
**Goal**: Integrate location capabilities into existing search
|
||
|
||
**Tasks**:
|
||
1. **Search View Enhancement**
|
||
- Extend [`AdaptiveSearchView`](core/views/search.py:5) with location processing
|
||
- Add user location detection and handling
|
||
- Implement hybrid text + proximity ranking
|
||
|
||
2. **API Development**
|
||
- Create location-aware search API endpoints
|
||
- Implement autocomplete with location context
|
||
- Add proper error handling and validation
|
||
|
||
3. **Query Optimization**
|
||
- Implement spatial query optimization patterns
|
||
- Add performance monitoring for search queries
|
||
- Create caching strategies for search results
|
||
|
||
**Deliverables**:
|
||
- Location-enhanced search views and APIs
|
||
- Optimized spatial query patterns
|
||
- Performance monitoring infrastructure
|
||
|
||
### Phase 3: User Experience (Weeks 5-6)
|
||
**Goal**: Create intuitive location search features
|
||
|
||
**Tasks**:
|
||
1. **Frontend Enhancement**
|
||
- Implement "near me" functionality with geolocation
|
||
- Add location permission handling and privacy controls
|
||
- Create enhanced search form with location context
|
||
|
||
2. **Results Display**
|
||
- Add distance information to search results
|
||
- Implement "get directions" functionality
|
||
- Create map integration for result visualization
|
||
|
||
3. **Progressive Enhancement**
|
||
- Ensure graceful fallback for users without location access
|
||
- Add IP-based location approximation
|
||
- Implement accessibility improvements
|
||
|
||
**Deliverables**:
|
||
- Enhanced search interface with location features
|
||
- Map integration for search results
|
||
- Accessibility-compliant location features
|
||
|
||
### Phase 4: Advanced Features (Weeks 7-8)
|
||
**Goal**: Implement advanced location search capabilities
|
||
|
||
**Tasks**:
|
||
1. **Cross-Domain Search**
|
||
- Implement unified search across parks, rides, companies
|
||
- Create location-aware ranking algorithms
|
||
- Add entity-specific location features
|
||
|
||
2. **Advanced Filtering**
|
||
- Implement metropolitan area filtering
|
||
- Add route-based search (search along a path)
|
||
- Create clustering for dense geographic areas
|
||
|
||
3. **Performance Optimization**
|
||
- Implement advanced caching strategies
|
||
- Add query result pagination for large datasets
|
||
- Optimize for mobile and low-bandwidth scenarios
|
||
|
||
**Deliverables**:
|
||
- Unified cross-domain location search
|
||
- Advanced geographic filtering options
|
||
- Production-ready performance optimizations
|
||
|
||
## Performance Benchmarks and Success Criteria
|
||
|
||
### Performance Targets
|
||
- **Text + Location Search**: < 200ms for 90th percentile queries
|
||
- **Spatial Queries**: < 300ms for radius searches up to 100 miles
|
||
- **Geocoding**: < 100ms cache hit rate > 85%
|
||
- **API Response**: < 150ms for location-enhanced autocomplete
|
||
|
||
### Success Metrics
|
||
- **User Adoption**: 40% of searches use location features within 3 months
|
||
- **Search Improvement**: 25% increase in search result relevance scores
|
||
- **Performance**: No degradation in non-location search performance
|
||
- **Coverage**: Location data available for 95% of parks in database
|
||
|
||
### Monitoring and Alerting
|
||
- Query performance tracking with detailed metrics
|
||
- Geocoding service health monitoring
|
||
- User location permission grant rates
|
||
- Search abandonment rate analysis
|
||
|
||
## Risk Mitigation
|
||
|
||
### Technical Risks
|
||
1. **Performance Degradation**: Comprehensive testing and gradual rollout
|
||
2. **Geocoding Service Reliability**: Multiple fallback providers and caching
|
||
3. **Privacy Compliance**: Clear consent flows and data minimization
|
||
|
||
### User Experience Risks
|
||
1. **Location Permission Denial**: Graceful fallbacks and alternative experiences
|
||
2. **Accuracy Issues**: Clear accuracy indicators and user feedback mechanisms
|
||
3. **Complexity Overload**: Progressive disclosure and intuitive defaults
|
||
|
||
## Conclusion
|
||
|
||
This integration plan provides a comprehensive roadmap for enhancing ThrillWiki's search system with sophisticated location capabilities while maintaining performance and user experience. The phased approach ensures manageable implementation complexity and allows for iterative improvement based on user feedback and performance metrics.
|
||
|
||
The integration leverages existing Django-filters architecture while adding powerful spatial search capabilities that will significantly enhance the user experience for theme park enthusiasts planning visits and exploring new destinations. |