mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 08:11:13 -05:00
Refactor: Update user list item interfaces
This commit is contained in:
277
src/components/search/AdvancedRideFilters.tsx
Normal file
277
src/components/search/AdvancedRideFilters.tsx
Normal file
@@ -0,0 +1,277 @@
|
||||
import { useState } from 'react';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
|
||||
import { ChevronDown, Plus, X } from 'lucide-react';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
|
||||
export interface TechnicalSpecFilter {
|
||||
spec_name: string;
|
||||
spec_value?: string;
|
||||
min_value?: number;
|
||||
max_value?: number;
|
||||
operator: 'equals' | 'contains' | 'range' | 'has_spec';
|
||||
}
|
||||
|
||||
export interface CoasterStatFilter {
|
||||
stat_name: string;
|
||||
min_value?: number;
|
||||
max_value?: number;
|
||||
}
|
||||
|
||||
interface AdvancedRideFiltersProps {
|
||||
technicalSpecFilters: TechnicalSpecFilter[];
|
||||
coasterStatFilters: CoasterStatFilter[];
|
||||
onTechnicalSpecFiltersChange: (filters: TechnicalSpecFilter[]) => void;
|
||||
onCoasterStatFiltersChange: (filters: CoasterStatFilter[]) => void;
|
||||
}
|
||||
|
||||
const COMMON_TECH_SPECS = [
|
||||
'Track Material',
|
||||
'Manufacturer Model',
|
||||
'Train Configuration',
|
||||
'Launch System',
|
||||
'Braking System',
|
||||
'Control System',
|
||||
'Safety System',
|
||||
];
|
||||
|
||||
const COMMON_COASTER_STATS = [
|
||||
'Max Speed',
|
||||
'Height',
|
||||
'Drop Height',
|
||||
'Length',
|
||||
'Duration',
|
||||
'Inversions',
|
||||
'G-Force',
|
||||
'Capacity',
|
||||
];
|
||||
|
||||
export function AdvancedRideFilters({
|
||||
technicalSpecFilters,
|
||||
coasterStatFilters,
|
||||
onTechnicalSpecFiltersChange,
|
||||
onCoasterStatFiltersChange,
|
||||
}: AdvancedRideFiltersProps) {
|
||||
const [isSpecsOpen, setIsSpecsOpen] = useState(false);
|
||||
const [isStatsOpen, setIsStatsOpen] = useState(false);
|
||||
|
||||
const addTechnicalSpecFilter = () => {
|
||||
onTechnicalSpecFiltersChange([
|
||||
...technicalSpecFilters,
|
||||
{ spec_name: '', operator: 'has_spec' },
|
||||
]);
|
||||
};
|
||||
|
||||
const updateTechnicalSpecFilter = (index: number, updates: Partial<TechnicalSpecFilter>) => {
|
||||
const updated = [...technicalSpecFilters];
|
||||
updated[index] = { ...updated[index], ...updates };
|
||||
onTechnicalSpecFiltersChange(updated);
|
||||
};
|
||||
|
||||
const removeTechnicalSpecFilter = (index: number) => {
|
||||
onTechnicalSpecFiltersChange(technicalSpecFilters.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
const addCoasterStatFilter = () => {
|
||||
onCoasterStatFiltersChange([
|
||||
...coasterStatFilters,
|
||||
{ stat_name: '' },
|
||||
]);
|
||||
};
|
||||
|
||||
const updateCoasterStatFilter = (index: number, updates: Partial<CoasterStatFilter>) => {
|
||||
const updated = [...coasterStatFilters];
|
||||
updated[index] = { ...updated[index], ...updates };
|
||||
onCoasterStatFiltersChange(updated);
|
||||
};
|
||||
|
||||
const removeCoasterStatFilter = (index: number) => {
|
||||
onCoasterStatFiltersChange(coasterStatFilters.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Technical Specifications Filters */}
|
||||
<Collapsible open={isSpecsOpen} onOpenChange={setIsSpecsOpen}>
|
||||
<CollapsibleTrigger asChild>
|
||||
<div className="flex items-center justify-between cursor-pointer p-3 bg-muted/50 rounded-lg hover:bg-muted transition-colors">
|
||||
<div className="flex items-center gap-2">
|
||||
<h4 className="font-semibold">Technical Specifications</h4>
|
||||
{technicalSpecFilters.length > 0 && (
|
||||
<Badge variant="secondary">{technicalSpecFilters.length}</Badge>
|
||||
)}
|
||||
</div>
|
||||
<ChevronDown className={`w-4 h-4 transition-transform ${isSpecsOpen ? 'rotate-180' : ''}`} />
|
||||
</div>
|
||||
</CollapsibleTrigger>
|
||||
|
||||
<CollapsibleContent className="space-y-3 pt-3">
|
||||
{technicalSpecFilters.map((filter, index) => (
|
||||
<div key={index} className="p-3 border rounded-lg space-y-3">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex-1 grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<div className="space-y-2">
|
||||
<Label>Specification Name</Label>
|
||||
<Select
|
||||
value={filter.spec_name}
|
||||
onValueChange={(value) => updateTechnicalSpecFilter(index, { spec_name: value })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select specification" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{COMMON_TECH_SPECS.map((spec) => (
|
||||
<SelectItem key={spec} value={spec}>
|
||||
{spec}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Filter Type</Label>
|
||||
<Select
|
||||
value={filter.operator}
|
||||
onValueChange={(value: any) => updateTechnicalSpecFilter(index, { operator: value })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="has_spec">Has Specification</SelectItem>
|
||||
<SelectItem value="equals">Equals Value</SelectItem>
|
||||
<SelectItem value="contains">Contains Text</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => removeTechnicalSpecFilter(index)}
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{(filter.operator === 'equals' || filter.operator === 'contains') && (
|
||||
<div className="space-y-2">
|
||||
<Label>Value</Label>
|
||||
<Input
|
||||
value={filter.spec_value || ''}
|
||||
onChange={(e) => updateTechnicalSpecFilter(index, { spec_value: e.target.value })}
|
||||
placeholder="Enter value to match"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={addTechnicalSpecFilter}
|
||||
className="w-full"
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Add Technical Spec Filter
|
||||
</Button>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
|
||||
{/* Coaster Statistics Filters */}
|
||||
<Collapsible open={isStatsOpen} onOpenChange={setIsStatsOpen}>
|
||||
<CollapsibleTrigger asChild>
|
||||
<div className="flex items-center justify-between cursor-pointer p-3 bg-muted/50 rounded-lg hover:bg-muted transition-colors">
|
||||
<div className="flex items-center gap-2">
|
||||
<h4 className="font-semibold">Coaster Statistics</h4>
|
||||
{coasterStatFilters.length > 0 && (
|
||||
<Badge variant="secondary">{coasterStatFilters.length}</Badge>
|
||||
)}
|
||||
</div>
|
||||
<ChevronDown className={`w-4 h-4 transition-transform ${isStatsOpen ? 'rotate-180' : ''}`} />
|
||||
</div>
|
||||
</CollapsibleTrigger>
|
||||
|
||||
<CollapsibleContent className="space-y-3 pt-3">
|
||||
{coasterStatFilters.map((filter, index) => (
|
||||
<div key={index} className="p-3 border rounded-lg space-y-3">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex-1 space-y-3">
|
||||
<div className="space-y-2">
|
||||
<Label>Statistic Name</Label>
|
||||
<Select
|
||||
value={filter.stat_name}
|
||||
onValueChange={(value) => updateCoasterStatFilter(index, { stat_name: value })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select statistic" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{COMMON_COASTER_STATS.map((stat) => (
|
||||
<SelectItem key={stat} value={stat}>
|
||||
{stat}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-2">
|
||||
<Label>Min Value</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={filter.min_value || ''}
|
||||
onChange={(e) =>
|
||||
updateCoasterStatFilter(index, {
|
||||
min_value: e.target.value ? parseFloat(e.target.value) : undefined,
|
||||
})
|
||||
}
|
||||
placeholder="Min"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Max Value</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={filter.max_value || ''}
|
||||
onChange={(e) =>
|
||||
updateCoasterStatFilter(index, {
|
||||
max_value: e.target.value ? parseFloat(e.target.value) : undefined,
|
||||
})
|
||||
}
|
||||
placeholder="Max"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => removeCoasterStatFilter(index)}
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={addCoasterStatFilter}
|
||||
className="w-full"
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Add Coaster Stat Filter
|
||||
</Button>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import { ChevronDown, Filter, X } from 'lucide-react';
|
||||
import { Combobox } from '@/components/ui/combobox';
|
||||
import { useCountries, useStatesProvinces, useManufacturers, useCompanyHeadquarters } from '@/hooks/useAutocompleteData';
|
||||
import { DateRangePicker, DateRange } from '@/components/ui/date-range-picker';
|
||||
import { AdvancedRideFilters, TechnicalSpecFilter, CoasterStatFilter } from './AdvancedRideFilters';
|
||||
|
||||
export interface SearchFilters {
|
||||
// Park filters
|
||||
@@ -36,6 +37,8 @@ export interface SearchFilters {
|
||||
speedMax?: number;
|
||||
intensityLevel?: string;
|
||||
rideDateRange?: DateRange;
|
||||
technicalSpecFilters?: TechnicalSpecFilter[];
|
||||
coasterStatFilters?: CoasterStatFilter[];
|
||||
|
||||
// Company filters
|
||||
companyType?: string;
|
||||
@@ -391,6 +394,16 @@ export function SearchFiltersComponent({ filters, onFiltersChange, activeTab }:
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Advanced Ride Filters */}
|
||||
<div className="pt-4 border-t">
|
||||
<AdvancedRideFilters
|
||||
technicalSpecFilters={filters.technicalSpecFilters || []}
|
||||
coasterStatFilters={filters.coasterStatFilters || []}
|
||||
onTechnicalSpecFiltersChange={(filters) => updateFilter('technicalSpecFilters', filters)}
|
||||
onCoasterStatFiltersChange={(filters) => updateFilter('coasterStatFilters', filters)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
177
src/hooks/useAdvancedRideSearch.ts
Normal file
177
src/hooks/useAdvancedRideSearch.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { supabase } from '@/integrations/supabase/client';
|
||||
import { TechnicalSpecFilter, CoasterStatFilter } from '@/components/search/AdvancedRideFilters';
|
||||
import { useDebounce } from './useDebounce';
|
||||
|
||||
interface AdvancedSearchOptions {
|
||||
query?: string;
|
||||
category?: string;
|
||||
manufacturer?: string;
|
||||
technicalSpecFilters?: TechnicalSpecFilter[];
|
||||
coasterStatFilters?: CoasterStatFilter[];
|
||||
speedMin?: number;
|
||||
speedMax?: number;
|
||||
heightMin?: number;
|
||||
heightMax?: number;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
export function useAdvancedRideSearch(options: AdvancedSearchOptions) {
|
||||
const [results, setResults] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const debouncedQuery = useDebounce(options.query || '', 300);
|
||||
|
||||
useEffect(() => {
|
||||
const performSearch = async () => {
|
||||
if (!debouncedQuery && !options.category && !options.manufacturer &&
|
||||
(!options.technicalSpecFilters || options.technicalSpecFilters.length === 0) &&
|
||||
(!options.coasterStatFilters || options.coasterStatFilters.length === 0)) {
|
||||
setResults([]);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
let query = supabase
|
||||
.from('rides')
|
||||
.select(`
|
||||
*,
|
||||
parks!inner(id, name, slug),
|
||||
companies!rides_manufacturer_id_fkey(id, name, slug)
|
||||
`);
|
||||
|
||||
// Basic text search
|
||||
if (debouncedQuery) {
|
||||
query = query.or(`name.ilike.%${debouncedQuery}%,description.ilike.%${debouncedQuery}%`);
|
||||
}
|
||||
|
||||
// Category filter
|
||||
if (options.category) {
|
||||
query = query.eq('category', options.category);
|
||||
}
|
||||
|
||||
// Manufacturer filter
|
||||
if (options.manufacturer) {
|
||||
query = query.eq('manufacturer_id', options.manufacturer);
|
||||
}
|
||||
|
||||
// Speed range filter
|
||||
if (options.speedMin !== undefined) {
|
||||
query = query.gte('max_speed_kmh', options.speedMin);
|
||||
}
|
||||
if (options.speedMax !== undefined) {
|
||||
query = query.lte('max_speed_kmh', options.speedMax);
|
||||
}
|
||||
|
||||
// Height range filter
|
||||
if (options.heightMin !== undefined) {
|
||||
query = query.gte('max_height_meters', options.heightMin);
|
||||
}
|
||||
if (options.heightMax !== undefined) {
|
||||
query = query.lte('max_height_meters', options.heightMax);
|
||||
}
|
||||
|
||||
query = query.limit(options.limit || 50);
|
||||
|
||||
const { data: initialResults, error: queryError } = await query;
|
||||
|
||||
if (queryError) throw queryError;
|
||||
|
||||
let filteredResults = initialResults || [];
|
||||
|
||||
// Apply technical specification filters
|
||||
if (options.technicalSpecFilters && options.technicalSpecFilters.length > 0) {
|
||||
for (const filter of options.technicalSpecFilters) {
|
||||
if (!filter.spec_name) continue;
|
||||
|
||||
const { data: specsData, error: specsError } = await supabase
|
||||
.from('ride_technical_specifications')
|
||||
.select('ride_id, spec_name, spec_value')
|
||||
.eq('spec_name', filter.spec_name);
|
||||
|
||||
if (specsError) throw specsError;
|
||||
|
||||
const matchingRideIds = new Set<string>();
|
||||
|
||||
specsData?.forEach((spec) => {
|
||||
let matches = false;
|
||||
|
||||
switch (filter.operator) {
|
||||
case 'has_spec':
|
||||
matches = true;
|
||||
break;
|
||||
case 'equals':
|
||||
matches = spec.spec_value === filter.spec_value;
|
||||
break;
|
||||
case 'contains':
|
||||
matches = filter.spec_value ?
|
||||
spec.spec_value.toLowerCase().includes(filter.spec_value.toLowerCase()) :
|
||||
false;
|
||||
break;
|
||||
}
|
||||
|
||||
if (matches) {
|
||||
matchingRideIds.add(spec.ride_id);
|
||||
}
|
||||
});
|
||||
|
||||
filteredResults = filteredResults.filter((ride) => matchingRideIds.has(ride.id));
|
||||
}
|
||||
}
|
||||
|
||||
// Apply coaster statistics filters
|
||||
if (options.coasterStatFilters && options.coasterStatFilters.length > 0) {
|
||||
for (const filter of options.coasterStatFilters) {
|
||||
if (!filter.stat_name) continue;
|
||||
|
||||
let statsQuery = supabase
|
||||
.from('ride_coaster_statistics')
|
||||
.select('ride_id, stat_name, stat_value')
|
||||
.eq('stat_name', filter.stat_name);
|
||||
|
||||
if (filter.min_value !== undefined) {
|
||||
statsQuery = statsQuery.gte('stat_value', filter.min_value);
|
||||
}
|
||||
if (filter.max_value !== undefined) {
|
||||
statsQuery = statsQuery.lte('stat_value', filter.max_value);
|
||||
}
|
||||
|
||||
const { data: statsData, error: statsError } = await statsQuery;
|
||||
|
||||
if (statsError) throw statsError;
|
||||
|
||||
const matchingRideIds = new Set(statsData?.map((stat) => stat.ride_id) || []);
|
||||
filteredResults = filteredResults.filter((ride) => matchingRideIds.has(ride.id));
|
||||
}
|
||||
}
|
||||
|
||||
setResults(filteredResults);
|
||||
} catch (err) {
|
||||
console.error('Advanced search error:', err);
|
||||
setError(err instanceof Error ? err.message : 'Search failed');
|
||||
setResults([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
performSearch();
|
||||
}, [
|
||||
debouncedQuery,
|
||||
options.category,
|
||||
options.manufacturer,
|
||||
options.speedMin,
|
||||
options.speedMax,
|
||||
options.heightMin,
|
||||
options.heightMax,
|
||||
options.limit,
|
||||
JSON.stringify(options.technicalSpecFilters),
|
||||
JSON.stringify(options.coasterStatFilters),
|
||||
]);
|
||||
|
||||
return { results, loading, error };
|
||||
}
|
||||
@@ -36,43 +36,10 @@ export function transformParkData(submissionData: any): ParkInsert {
|
||||
|
||||
/**
|
||||
* Transform ride submission data to database insert format
|
||||
* Note: Relational data (technical_specs, coaster_stats, former_names) are now
|
||||
* stored in separate tables and should not be included in the main ride insert.
|
||||
*/
|
||||
export function transformRideData(submissionData: any): RideInsert {
|
||||
// Parse JSON fields if they're strings
|
||||
let coasterStats = null;
|
||||
let technicalSpecs = null;
|
||||
let formerNames = null;
|
||||
|
||||
try {
|
||||
if (submissionData.coaster_stats) {
|
||||
coasterStats = typeof submissionData.coaster_stats === 'string'
|
||||
? JSON.parse(submissionData.coaster_stats)
|
||||
: submissionData.coaster_stats;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse coaster_stats:', e);
|
||||
}
|
||||
|
||||
try {
|
||||
if (submissionData.technical_specs) {
|
||||
technicalSpecs = typeof submissionData.technical_specs === 'string'
|
||||
? JSON.parse(submissionData.technical_specs)
|
||||
: submissionData.technical_specs;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse technical_specs:', e);
|
||||
}
|
||||
|
||||
try {
|
||||
if (submissionData.former_names) {
|
||||
formerNames = typeof submissionData.former_names === 'string'
|
||||
? JSON.parse(submissionData.former_names)
|
||||
: submissionData.former_names;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse former_names:', e);
|
||||
}
|
||||
|
||||
return {
|
||||
name: submissionData.name,
|
||||
slug: submissionData.slug,
|
||||
@@ -99,9 +66,6 @@ export function transformRideData(submissionData: any): RideInsert {
|
||||
coaster_type: submissionData.coaster_type || null,
|
||||
seating_type: submissionData.seating_type || null,
|
||||
intensity_level: submissionData.intensity_level || null,
|
||||
coaster_stats: coasterStats,
|
||||
technical_specs: technicalSpecs,
|
||||
former_names: formerNames || [],
|
||||
banner_image_url: submissionData.banner_image_url || null,
|
||||
banner_image_id: submissionData.banner_image_id || null,
|
||||
card_image_url: submissionData.card_image_url || null,
|
||||
@@ -136,20 +100,10 @@ export function transformCompanyData(
|
||||
|
||||
/**
|
||||
* Transform ride model submission data to database insert format
|
||||
* Note: Technical specifications are now stored in the ride_model_technical_specifications
|
||||
* table and should not be included in the main ride model insert.
|
||||
*/
|
||||
export function transformRideModelData(submissionData: any): RideModelInsert {
|
||||
let technicalSpecs = null;
|
||||
|
||||
try {
|
||||
if (submissionData.technical_specs) {
|
||||
technicalSpecs = typeof submissionData.technical_specs === 'string'
|
||||
? JSON.parse(submissionData.technical_specs)
|
||||
: submissionData.technical_specs;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse technical_specs:', e);
|
||||
}
|
||||
|
||||
return {
|
||||
name: submissionData.name,
|
||||
slug: submissionData.slug,
|
||||
@@ -157,7 +111,6 @@ export function transformRideModelData(submissionData: any): RideModelInsert {
|
||||
category: submissionData.category,
|
||||
ride_type: submissionData.ride_type || null,
|
||||
description: submissionData.description || null,
|
||||
technical_specs: technicalSpecs,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
-- ============================================
|
||||
-- Phase 5: Advanced Search Performance Indexes
|
||||
-- ============================================
|
||||
|
||||
-- Search indexes for technical specifications
|
||||
CREATE INDEX IF NOT EXISTS idx_ride_tech_specs_search
|
||||
ON ride_technical_specifications(ride_id, spec_name, spec_value);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_ride_tech_specs_category
|
||||
ON ride_technical_specifications(category, spec_name);
|
||||
|
||||
-- GIN index for flexible text search on spec values
|
||||
CREATE INDEX IF NOT EXISTS idx_ride_tech_specs_value_gin
|
||||
ON ride_technical_specifications USING gin(to_tsvector('english', spec_value));
|
||||
|
||||
-- Search indexes for coaster statistics
|
||||
CREATE INDEX IF NOT EXISTS idx_ride_coaster_stats_search
|
||||
ON ride_coaster_statistics(ride_id, stat_name, stat_value);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_ride_coaster_stats_value_range
|
||||
ON ride_coaster_statistics(stat_name, stat_value);
|
||||
|
||||
-- Composite index for rides filtering
|
||||
CREATE INDEX IF NOT EXISTS idx_rides_search_composite
|
||||
ON rides(category, status, manufacturer_id, park_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_rides_speed_height
|
||||
ON rides(max_speed_kmh, max_height_meters)
|
||||
WHERE max_speed_kmh IS NOT NULL OR max_height_meters IS NOT NULL;
|
||||
|
||||
-- Ride model technical specifications indexes
|
||||
CREATE INDEX IF NOT EXISTS idx_ride_model_tech_specs_search
|
||||
ON ride_model_technical_specifications(ride_model_id, spec_name, spec_value);
|
||||
|
||||
-- Park search optimization
|
||||
CREATE INDEX IF NOT EXISTS idx_parks_search_composite
|
||||
ON parks(park_type, status, operator_id, property_owner_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_parks_location
|
||||
ON parks(location_id) WHERE location_id IS NOT NULL;
|
||||
|
||||
-- Company search optimization
|
||||
CREATE INDEX IF NOT EXISTS idx_companies_type_search
|
||||
ON companies(company_type, name);
|
||||
|
||||
-- Text search indexes for fuzzy matching
|
||||
CREATE INDEX IF NOT EXISTS idx_rides_name_gin
|
||||
ON rides USING gin(to_tsvector('english', name));
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_parks_name_gin
|
||||
ON parks USING gin(to_tsvector('english', name));
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_companies_name_gin
|
||||
ON companies USING gin(to_tsvector('english', name));
|
||||
|
||||
COMMENT ON INDEX idx_ride_tech_specs_search IS 'Optimizes technical specification searches by ride and spec name';
|
||||
COMMENT ON INDEX idx_ride_coaster_stats_search IS 'Optimizes coaster statistics searches';
|
||||
COMMENT ON INDEX idx_rides_search_composite IS 'Composite index for common ride search filters';
|
||||
COMMENT ON INDEX idx_rides_name_gin IS 'Full-text search index for ride names';
|
||||
Reference in New Issue
Block a user