Search the cruise catalog with a read-only PostgreSQL SELECT you write.
RULES: PostgreSQL dialect; one SELECT statement; exact string values are validated —
misspellings are rejected before execution with suggestions.
SEMANTIC SEARCH:
embed(text) — Embeds a phrase into a query vector (same model as every `embedding` column). Accepts a single string literal, e.g. embedding <=> embed('romantic'). Only the first 256 tokens are embedded.
These tables carry an `embedding` column: excursion_reviews, excursions, lodges, places, ships, stateroom_categories, stateroom_subgroups. No other table has one.
In particular `products` and `voyages` have none, so a whole trip has no vector of its
own — match the mood on the ship, the ports it visits, or its excursions, and join back
to the sailing.
Express soft/semantic constraints in SQL with pgvector's cosine-distance operator
`<=>`, always written as `<table>.embedding`, for example:
`ORDER BY ships.embedding <=> embed('romantic sunset dinner') LIMIT 20`
`WHERE (places.embedding <=> embed('glacier hike')) < 0.6`
Treat the distance as a score — a bare `embedding` column can't be returned.
PRODUCT KINDS: the catalog has two products.kind values — 'cruise' and 'cruisetour'
(a cruise bundled with a land tour). A cruisetour is a cruise. When the user says
"cruise" without singling out one kind, search both; add a `kind` filter only when
they explicitly ask for one (e.g. "cruisetour packages", or "a cruise, not a land tour").
AVAILABILITY: only `pricing.available` says whether a sailing can still be booked —
you MUST check it whenever you're picking sailings to recommend — including when the
shopper mentioned no price or cabin.
RESULTS: capped at ~500 KB of data, not a fixed row
count — small rows pack more in. When more rows match than fit, `truncated` is true and the
true match count is reported.
Database date range: 2026-09-26 to 2028-11-18
TABLES (column comments carry units, joins, and valid values):
ships — Princess ships. Join voyages.ship_code = ships.code.
code text — 2-letter ship code, e.g. 'RP'.
name text — Ship name, e.g. 'Royal Princess'.
description text — Marketing prose about the ship.
image_url text
ship_version int
spec jsonb — Label/value rows (guest capacity, crew, tonnage, …).
stateroom_types text[]
amenity_categories jsonb — Marketing groups (dining, entertainment) with items.
zones jsonb — Deck zones (Forward/Midship/Aft).
version_dates jsonb
embedding vector(1024) — Vector of name, description, and dining/entertainment/amenity items.
places — Everywhere a cruise stops. Whether a given stop is a port call or a land stop is a property of the stop, so read itinerary_days.stop_type for that, not this table.
code text — 3-char place code, e.g. 'JNU'.
name text — Full name, carrying the state or country, e.g. 'Juneau, Alaska'.
is_port boolean — TRUE when the place is a port. A place can be both port and city at the same time — do NOT treat these as mutually exclusive.
is_city boolean — TRUE when the place is a cruisetour land tour stop, e.g. Denali.
country_code text
latitude double precision
longitude double precision
description text
url text
points_of_interest jsonb — A list of points of interest, e.g. {"title": "Glacier Gardens", "description": "This 50-acre garden features species native to this temperate rainforest …"}
images jsonb — The place's gallery as a URL array.
embedding vector(1024) — Vector of name, description, and points of interest.
products — Itinerary templates; one product has many voyages (departures). Same product = same route on different dates.
id text — Product code, e.g. 'ASG070'.
kind text — 'cruise' or 'cruisetour' (cruise + land tour package).
name text
region_code text
region text — Destination region, e.g. 'Alaska', 'Caribbean'.
subtrade_codes text[]
images jsonb — Image URLs, keyed by role: 'hero' = destination/ship photo shown on a result card (NOT a map), 'map' = route-map graphic of the sailing (for a cruisetour, its cruise leg), 'detail_hero' = wide destination banner for the detail view, keyed by sub-trade rather than product.
voyages — THE search grain: one row per departure (unique date + itinerary). Day-by-day stops are in itinerary_days; prices in pricing.
id text — Voyage id, e.g. '1629' (cruise) or '6616-T6AHB5' (cruisetour departure).
product_id text
ship_code text
nights int — Nights at sea (cruise portion only). Unit: nights.
depart_date date — Trip start date — when the guest travels. For land-first cruisetours the land tour runs first, so the ship sails later: see sail_date. Use this for 'when does the trip start / depart'.
sail_date date — Date the ship sails. Equals depart_date except on land-first cruisetours, where it is depart_date + tour_nights. Use this only for 'when does the ship sail'.
return_date date
start_port_code text — Embarkation port (join places for the name).
end_port_code text — Disembarkation port.
tour_nights int — Land-tour nights (cruisetours only, else NULL). Total trip = nights + tour_nights.
is_land_first boolean — Cruisetours only: true = land tour before the cruise.
itinerary_days — Day-by-day stops per voyage — the only place the visited-port sequence lives. For 'visits X' read place_name; for attributes join place_code = places.code.
voyage_id text
seq int
day_number int — 0-based trip day. Two stops can share a day; order by seq.
stop_type text — 'port' = sea port call, 'city' = cruisetour land stop, 'sea' = day at sea. This is what says which kind of stop it is — places itself does not, since a code can be both. Filter here for 'ports only'.
place_code text — Visited place (places.code); NULL only on sea days.
place_name text — Resolved stop name (places.name); NULL on sea days. Use for 'what does it visit' without a join.
arrival_date date
depart_date date
arrival_time text — Local time text, e.g. '09:00 AM'; empty on embark/sea days.
depart_time text
boarding_time text
pier_code text
notes text[] — Special-day notes, e.g. 'SCENIC CRUISING'.
pricing — One row per voyage x stateroom category (sparse — a voyage with no rows has unknown pricing, NOT sold out). Join pricing.voyage_id = voyages.id. Filter available = true for bookable prices.
voyage_id text — = voyages.id.
room_type text — Bookable category code, e.g. 'BF'. Join stateroom_categories (ship_code, code) for name and cabin group.
cabin_group text — Cabin group letter (I/O/B/M/S, per stateroom_categories.meta); NULL if unknown.
fare decimal(10, 2) — Price in USD, avg/person, 2 guests, includes taxes & fees
original_fare decimal(10, 2) — Brochure (strike-through) price, same basis as fare (USD, avg/person, 2 guests). fare < original_fare means discounted.
currency text
available boolean
available_cabins text — Remaining cabins in this category, deliberately coarse. An exact count as text ('1'…'9') when fewer than 10 remain; otherwise the literal 'Inventory available', meaning 10 or more. TEXT, not a number — compare with = or IN (e.g. available_cabins IN ('1','2')), never with < or >. Never state or imply a figure for 'Inventory available': say inventory is available.
status text — How the cabin is sold: 'available' | 'guarantee' | 'sold_out'. A guarantee cabin is bookable — you get the category, the room is assigned later — but it is only sold to 1–2 guests.
promo_code text — Princess's internal promo code on this fare, e.g. 'KKS'; NULL if none. Not guest-facing — join promotions (voyage_id, promo_code) for the sale's name, and only some codes have one.
fetched_at timestamptz
promotions — Named sales per departure, with the dates each runs. Join promotions.voyage_id = voyages.id. ALWAYS filter to the sale's window — `CURRENT_DATE BETWEEN effective_date AND shut_off_date` — a row outside it is a past or future sale, not a live one. Sparse and partial by design: only Princess's "core" promotions have display text, so a voyage with no rows is not necessarily a voyage with no sale, and this never lists every offer princess.com shows.
voyage_id text — = voyages.id.
promo_code text — Princess's internal promo code, e.g. 'KKS'. Same code the live pricing response carries; not guest-facing — show `name` instead.
sale_code text — Sale code the display text is keyed by, e.g. 'CSC'. Internal; many promo_codes can share one.
name text — Guest-facing sale name, e.g. 'Up to 40% Off'. The only promotion text safe to show or narrate.
effective_date date — First day the sale runs, inclusive (from 12:00am PST that day).
shut_off_date date — Last day the sale runs, inclusive (through 11:59pm PST that day). Varies per voyage for the same promo.
stateroom_categories — Bookable stateroom categories per ship; pricing.room_type resolves here.
ship_code text
code text — Bookable category code, e.g. 'BF', 'DH'; pricing.room_type joins here.
name text — Category display name, e.g. 'Balcony', 'Premium Suite'.
short_desc text
sort_order int
meta text — Cabin group letter: I=Interior, O=Oceanview, B=Balcony, M=Mini-Suite, S=Suite.
sub_meta text — Single-char sub-code within the class; join (ship_code, meta, sub_meta) to stateroom_subgroups for the sub-group's names/description/marketing.
total_berths int
cabins jsonb — Cabin numbers.
embedding vector(1024) — Vector of name and short description.
stateroom_subgroups — Cabin sub-groups per ship — one per stateroom sub-type (Balcony, Premium Suite, …): cabin class, display names/descriptions, and marketing copy. Coarser than stateroom_categories (many bookable categories per sub-group); join both on (ship_code, meta, sub_meta).
ship_code text
meta text — Cabin group letter: I=Interior, O=Oceanview, B=Balcony, M=Mini-Suite, S=Suite. Same scheme as stateroom_categories.meta.
sub_meta text — Single-char sub-code within the class — same scheme as stateroom_categories.sub_meta; join both on (ship_code, meta, sub_meta).
name text — Sub-group display name, e.g. 'Deluxe Balcony', 'Grand Suite'.
short_desc text
description text
points jsonb — Marketing feature bullets, e.g. 'Balcony with 2 chairs and table'.
size_text text — Verbatim, e.g. 'Approx. 158 sq. ft.'.
max_guests int — Sleeps up to N guests.
image_urls jsonb — Marketing image URLs.
product_url text
embedding vector(1024) — Vector of name, short description, and description.
ship_decks — Decks per ship with the stateroom category codes present on each.
ship_code text
deck_id text
name text
deck_group text
categories text[]
excursions — Shore and land excursions. Join place_code = places.code; excursion_type says which kind.
id text — Excursion id, e.g. 'JNU-850'.
place_code text — Where the excursion runs (places.code); always set.
name text
description text
short_description text
duration_hours decimal(4, 1)
price_usd decimal(10, 2) — Per-person price in USD.
currency text
excursion_type text — 'S' = shore excursion, 'L' = land-tour excursion.
activity_level text
accessible boolean
shopping text
food_service text
points_of_interest jsonb
images jsonb
url text
rating_avg decimal(3, 2) — Average guest rating 1-5; NULL = unrated.
rating_count int
embedding vector(1024) — Vector of name, descriptions, and points of interest.
excursion_reviews — Guest reviews per excursion (BazaarVoice).
id bigint
excursion_id text
rating int
title text
review_text text
submitted_at timestamptz
nickname text
is_recommended boolean
embedding vector(1024) — Vector of title and review text.
lodges — Princess Alaska wilderness lodges (cruisetour accommodation).
id text
place_code text — The lodge's land-tour stop (places.code).
name text
latitude double precision
longitude double precision
address text
description text
amenities text
atmosphere text
images jsonb
embedding vector(1024) — Vector of name, description, amenities, and atmosphere.
search