Airport Search
PostgreSQL full-text search implementation for intelligent airport lookup with composite ranking algorithm. Searches across 9,068+ airports with weighted relevance scoring based on IATA codes, city names, airport names, and airport classification.
Quick Start
Section titled “Quick Start”Basic Search
Section titled “Basic Search”use App\Models\Airport;
// Search for airports$results = Airport::active() ->fullTextSearch('london') ->limit(10) ->get();
foreach ($results as $airport) { echo "{$airport->iata_code} - {$airport->name} ({$airport->city})\n";}Output:
LGW - Gatwick Airport (London)LHR - Heathrow Airport (London)LCY - London City Airport (London)FilamentPHP Integration
Section titled “FilamentPHP Integration”Select::make('departure_airport') ->label('From') ->searchable() ->getSearchResultsUsing(fn (string $search): array => Airport::active() ->fullTextSearch($search) ->limit(10) ->get() ->mapWithKeys(fn (Airport $airport): array => [$airport->iata_code => "{$airport->iata_code} - {$airport->name} ({$airport->city})"] ) ->toArray() ) ->required()Architecture
Section titled “Architecture”Search Components
Section titled “Search Components”Database Layer:
search_vectorcolumn of typetsvector, with a weighted GIN index (airports_search_vector_gin_idx USING GIN (search_vector))- Auto-updating trigger for vector maintenance
to_tsqueryprefix matching (term:*) for partial word search, plusLOWER(col) LIKE 'x%'for IATA/city/name prefix scoring- Composite scoring algorithm
The unaccent extension is enabled in the database (2026_03_26_095800_enable_unaccent_extension.php) but is not wired into the airports search trigger or query, so accent folding does not apply here (e.g., “Bogotá” is not normalized to “Bogota” for this search). There is no pg_trgm extension in this search path.
Model Layer:
scopeFullTextSearch()- Main search scopescopeActive()- Filter active airports- Helper methods for display formatting
Ranking Algorithm
Section titled “Ranking Algorithm”The search uses a composite scoring system:
relevance_score = (FTS Relevance × 1000) + -- Full-text match quality (Exact IATA Match × 10,000) + -- Exact IATA code (e.g., "LHR") (IATA Prefix Match × 5,000) + -- IATA starts with query (Exact City Match × 3,000) + -- Exact city name (City Prefix Match × 1,500) + -- City starts with query (Name Prefix Match × 500) + -- Airport name starts with query (Airport Type Bonus) -- Based on classificationAirport Type Bonuses:
- Large airports: 2,500 points (96 major hubs)
- Medium airports: 400 points
- Small airports: 100 points
Weighted Fields
Section titled “Weighted Fields”| Field | Weight | Priority | Purpose |
|---|---|---|---|
iata_code |
A | Highest | Primary identifier (e.g., “LHR”) |
icao_code |
A | Highest | Secondary identifier (e.g., “EGLL”) |
city |
B | High | Location search (e.g., “London”) |
name |
C | Medium | Airport name (e.g., “Heathrow”) |
country |
D | Low | Country-based filtering |
Database Schema
Section titled “Database Schema”Migration Structure
Section titled “Migration Structure”File: 2026_01_16_123135_add_search_vector_to_airports_table.php
Components:
- search_vector column (
tsvector) - Stores precomputed weighted text search vectors - GIN index - Fast full-text search queries, built directly on the column
- Auto-update trigger - Maintains search vectors on INSERT/UPDATE
Search Vector Column
Section titled “Search Vector Column”ALTER TABLE airports ADD COLUMN search_vector tsvector;
CREATE INDEX IF NOT EXISTS airports_search_vector_gin_idxON airportsUSING GIN (search_vector);Auto-Update Trigger
Section titled “Auto-Update Trigger”CREATE FUNCTION airports_search_vector_trigger() RETURNS trigger AS $$BEGIN NEW.search_vector := setweight(to_tsvector('english', COALESCE(NEW.iata_code, '')), 'A') || setweight(to_tsvector('english', COALESCE(NEW.icao_code, '')), 'A') || setweight(to_tsvector('english', COALESCE(NEW.city, '')), 'B') || setweight(to_tsvector('english', COALESCE(NEW.name, '')), 'C') || setweight(to_tsvector('english', COALESCE(NEW.country, '')), 'D'); RETURN NEW;END$$ LANGUAGE plpgsql;
CREATE TRIGGER airports_search_vector_updateBEFORE INSERT OR UPDATE ON airportsFOR EACH ROW EXECUTE FUNCTION airports_search_vector_trigger();Large Airport Classification
Section titled “Large Airport Classification”96 major international hubs receive a 2,500-point ranking bonus:
Europe & UK (32 airports):
- London: LHR, LGW, MAN, EDI
- France: CDG, ORY
- Germany: FRA, MUC, BER
- Spain: MAD, BCN
- Italy: FCO, MXP, VCE
- Plus: AMS, IST, ZRH, VIE, CPH, ARN, OSL, HEL, DUB, LIS, ATH, WAW, PRG, BUD, BRU
Asia-Pacific (30 airports):
- Major hubs: SIN, HKG, ICN, NRT, HND, KIX
- China: PVG, PEK, CAN, CTU, SZX, SHA, CKG, XIY
- Southeast Asia: BKK, KUL, MNL, CGK
- India: DEL, BOM, BLR
North America (29 airports):
- US West: LAX, SFO, SEA
- US Central: ORD, DEN, DFW, IAH, MSP, DTW, CLT, ATL
- US East: JFK, EWR, IAD, BOS, MIA, MCO, PHX, LAS
- Canada: YYZ, YVR, YUL, YYC
Middle East (9 airports): DXB, AUH, DOH, JED, RUH, CAI, TLV, AMM, KWI
Latin America (10 airports): MEX, PTY, CUN, BOG, LIM, GRU, GIG, EZE, SCL
Oceania (5 airports): SYD, MEL, BNE, PER, AKL
Africa (6 airports): JNB, CPT, ADD, NBO, LOS, CMN
Usage Examples
Section titled “Usage Examples”Search by IATA Code
Section titled “Search by IATA Code”// Exact IATA match (10,000 points bonus)$results = Airport::active() ->fullTextSearch('LHR') ->limit(5) ->get();
// Returns: Heathrow (LHR) firstSearch by City
Section titled “Search by City”// Exact city match (3,000 points bonus)$results = Airport::active() ->fullTextSearch('barcelona') ->limit(5) ->get();
// BCN gets additional 2,500 points for being large_airportCombining with Other Scopes
Section titled “Combining with Other Scopes”// Search + filter by country$results = Airport::active() ->fullTextSearch('charles') ->country('FR') ->limit(5) ->get();
// Search + filter by type$majorHubs = Airport::active() ->fullTextSearch('new york') ->type('large_airport') ->get();Performance
Section titled “Performance”Index Performance
Section titled “Index Performance”GIN Index Benefits:
- Sub-millisecond search across 9,068+ airports
- Efficient prefix matching via
to_tsquery(term:*) - Scales to millions of records
- Low maintenance overhead
Query Performance:
// Typical search: ~2-5ms$results = Airport::active()->fullTextSearch('london')->limit(10)->get();
// Large result set: ~10-20ms$results = Airport::active()->fullTextSearch('international')->limit(100)->get();Memory Optimization
Section titled “Memory Optimization”// Use limit() for dropdown searches->fullTextSearch($search)->limit(10)->get()
// Use cursor() for large exportsAirport::active() ->fullTextSearch($search) ->cursor() ->each(function ($airport) { // Process one at a time });Troubleshooting
Section titled “Troubleshooting”Search Returns No Results
Section titled “Search Returns No Results”Verify the column type is tsvector:
SELECT column_name, data_type FROM information_schema.columnsWHERE table_name = 'airports' AND column_name = 'search_vector';Verify index:
SELECT indexname FROM pg_indexesWHERE tablename = 'airports'AND indexname = 'airports_search_vector_gin_idx';Rebuild vectors:
php artisan tinker>>> DB::statement("UPDATE airports SET search_vector = setweight(to_tsvector('english', COALESCE(iata_code, '')), 'A') || setweight(to_tsvector('english', COALESCE(icao_code, '')), 'A') || setweight(to_tsvector('english', COALESCE(city, '')), 'B') || setweight(to_tsvector('english', COALESCE(name, '')), 'C') || setweight(to_tsvector('english', COALESCE(country, '')), 'D')");Performance Issues
Section titled “Performance Issues”Check query plan:
EXPLAIN ANALYZESELECT * FROM airportsWHERE COALESCE(search_vector, ''::tsvector) @@ to_tsquery('english', 'london:*')LIMIT 10;Expected: Bitmap Index Scan using airports_search_vector_gin_idx
Testing
Section titled “Testing”test('full text search returns relevant results', function () { $results = Airport::active() ->fullTextSearch('london') ->get();
expect($results) ->not->toBeEmpty() ->and($results->first()->city)->toContain('London');});
test('exact IATA match ranks highest', function () { $results = Airport::active() ->fullTextSearch('lhr') ->get();
expect($results->first()->iata_code)->toBe('LHR');});Related Documentation
Section titled “Related Documentation”- Flight Search UI - FilamentPHP interface using airport search
- AerTicket Integration - Flight search consuming airport data