Search
Natural-language catalog search — query understanding, franchise & time-aware queries, opt-in personalization, scoring, and coverage.
POST /v1/search searches your airline’s catalog (plus the shared global
catalog) in natural language. You send a passenger-style query; the platform
parses it into structured constraints, ranks the matching titles semantically,
and tells you exactly how it understood the query. Optionally, a seatback
integration can opt a search into behavior personalization
for the passenger who is asking.
Request
The request body is capped at 64 KiB (413 above it). There is no
airline field — your credential carries the airline.
Search is the platform’s one session-optional endpoint: with no
X-Session-Id header it is a plain airline-scoped search (the unchanged
behavior for server-to-server callers). Send the header to opt into
personalization — but a malformed value is then a
400 session.unresolved, same as on the session-required
endpoints, never
silently ignored.
How your query is understood
Every response echoes the parse in parsed, so you never have to guess what
the platform did with the text:
originalQuery— your text, verbatim.filters— the structured constraints extracted from it:yearFrom/yearTo,genres[](all listed genres must match),cast[],titleContains,contentKind,ratingMax,entities[],runtimeMax,scoreMin,awardWinning— plusairlineId, the tenant your credential resolved to (server-side; you never send it).semanticText— the meaning left over after constraint extraction (“feel-good”), used for semantic ranking. Empty when nothing remains.source— which parser produced the filters:"ai"(the model parser, the primary path) or"rules"(the deterministic fallback).fitsFlightTime—truewhen the query asked for content that fits the remaining flight.recommendForMe—truewhen the query asked for personal picks rather than stating content constraints.
Parsing handles the phrasings passengers actually use:
- Decades: ”90s” or “1990s” ⇒
yearFrom: 1990, yearTo: 1999. - Compound genres: “romcom” ⇒
genres: ["Romance", "Comedy"]. - Kind keywords: “shows”, “series”, “TV” ⇒
contentKind: "TV".
How parsing runs. Query parsing is AI-primary: a language model
extracts the structured constraints — including cast names, with fuzzy
correction of well-known names (“Tom Cruz” → “Tom Cruise”) — and the
response’s "source": "ai" tells you the model produced them. If the model
is briefly unavailable the platform falls back to the deterministic rules
engine automatically ("source": "rules") and never fails the request —
but the rules engine extracts a narrower set: no cast, titleContains,
or ratingMax, it never populates entities, scoreMin, or
awardWinning, and it never flags fitsFlightTime or recommendForMe.
Branch on parsed.source if that distinction matters to you.
Content kinds
kindOverride forces the content kind instead of the parsed one, and it
accepts all four kinds the catalog carries:
kindOverride is the documented channel for audio and game: the query
parser does not infer those kinds from free text, so “play me some jazz”
without an override is not read as an audio request. Anything outside the
accepted set is a 422 request.validation whose message lists the kinds.
The catalog-serve kind filter is stricter, on
purpose. Serve takes the four canonical values — Movie, TV, Audio,
Game — in any casing and no synonyms: music and games resolve on
search’s kindOverride and are a 422 on serve. The non-parity is
deliberate (a free-text query surface and a structured filter parameter are
not the same thing), not an oversight.
Audio and game titles are hidden from younger members today. Audio and
game content ratings carry no recognized scale yet and normalize to null,
and an unrated title is treated as most-restrictive — so audio and game
results are excluded for Child, Tween, and Unknown members and
returned normally for Adult members. An audio or game search for a
younger member returns an empty result set, honestly, rather than an error.
Quality & acclaim queries
Queries that state a rating bar or ask for acclaim are understood as constraints, and every result carries its scores:
- The score bar follows the data-gap rule. A title with a known score below the bar is excluded; a title with no known score is not excluded — exactly as with the rating and runtime filters. A data gap is not a violation.
- Award matching is grounded. A title qualifies only when its own catalog data carries award recognition — never inferred from popularity or acclaim.
- Matching results carry reason strings like
"IMDB 7.2"and"Award winner". - Quality phrases are never treated as franchise entities. “Award winning action movies” is a quality constraint, not a search for a franchise called “award winning” — a deterministic guard, so these queries return a full lineup rather than a handful of accidental matches.
Every result item carries the scores it was judged on, whether or not the query asked:
These are catalog-supplied review and editorial data, as carried by your own catalog feed. Display them with the result — do not re-derive or recompute them.
Franchise & entity queries
Queries that name a franchise, character, or creator — “Bond films”, “Marvel movies” — resolve against what your catalog’s own data actually carries, not against words in titles:
parsed.filters.entitieslists the canonical entity names the query stated (["James Bond"]). It is always present, empty when the query names none.- A title matches when its own catalog data — keywords, title, cast, or synopsis — carries the entity. A title keyworded “James Bond” matches even if its name contains no “Bond”.
- Matching results carry a reason naming the franchise (e.g.
"James Bond franchise"). - An entity your catalog does not carry returns zero results — never invented or look-alike titles. Grounded-empty is the designed behavior, not a failure.
Time-aware queries: “fits the flight”
When the seatback knows how long is left in the flight, send it — and when the query itself asks for something finishable (“something I can finish before we land”), the platform turns it into a runtime ceiling:
- The ceiling is the smaller of
flightMinutesRemainingand any explicit duration in the query (“under 2 hours” ⇒ 120). It lands inparsed.filters.runtimeMax(minutes; omitted when no ceiling applies). - Titles whose known runtime exceeds the ceiling are excluded. Titles with unknown runtime are not excluded — a data gap is not a violation, the same rule the rating gate follows.
- Fitting results carry a reason like
"Runtime 92 min fits your 95 min". parsed.fitsFlightTime: trueconfirms the query was understood as flight-fit. If it istrueon a request that sent noflightMinutesRemainingand the session is not bound to a flight, no ceiling was applied — treat that as the signal to start sending the field.- A query that never mentions flight-time fit ignores
flightMinutesRemainingentirely.
When you don’t send the minutes
If the session is bound to a flight and a flight-fit query
arrives without flightMinutesRemaining, the platform derives the
remaining minutes from that flight’s scheduled arrival time rather than
skipping the ceiling.
A request-supplied flightMinutesRemaining is always authoritative — the
aircraft knows its own delays, the schedule does not.
The response’s flightMinutesRemainingServerDerived (boolean, always
present) tells you where the value came from: true only when the platform
derived it. Read it precisely — it reports the origin of the remaining
minutes value, not which ceiling ultimately applied. A tighter runtime limit
stated in the query text (“under 90 minutes”) still wins while this flag
reads true.
Personalization & personal picks
Search is unpersonalized by default. A seatback integration opts in per request by sending the passenger’s session and member locator alongside its event stream:
When the session and locator resolve a real behavior profile:
- Results get a bounded additive re-rank toward the member’s observed affinities — a nudge on top of relevance, never a replacement for it.
- Re-ranked items carry a deterministic reason string, e.g.
"Matches your Thriller affinity". - The response’s
personalized: trueconfirms a profile actually shaped this ranking.
personalized: false is the honest answer for every other case — no locator
sent, sessionless call, or a member with no observed behavior yet (cold
start). It is never faked.
Personal-pick queries. A query that asks for recommendations instead of
stating constraints — “show me movies you think I’d like”, “what should I
watch”, “surprise me” — flips ranking from semantic matching to the member’s
affinity-ranked lineup, echoed as parsed.recommendForMe: true. Stated
filters still apply (“comedies you think I’d like” stays comedies). A cold
member gets a neutral, deterministic lineup — sensible, not random, and
personalized stays false. Because no semantic query is being ranked in
this mode, results carry cosineScore: 0.0 and no "semantic …" reason —
the affinity reason is the ranking evidence.
Locator semantics are the platform-wide ones: fail-closed, never loud — an unknown seat or unbound member personalizes nothing and errors nothing.
Member age-gating on session-bound search
Sending X-Session-Id opts into more than personalization: it also opts into
child-safety filtering for the identified member.
- With a session — results are hard-filtered to what is age-appropriate
for the member your
seatId/memberSlotnames, before ranking. The member’s age tier comes from the session roster, never from the request. - Without a session — a sessionless, machine-to-machine search is not age-filtered. Results are byte-identical to what they have always been.
Two fail-closed rules apply when a session is supplied:
- An unresolved or unbound member gets the most-restrictive tier. An unknown locator narrows results; it never widens them and never errors.
- A title with no content rating is treated as most-restrictive, so an
unrated title is hidden from
Child,Tween, andUnknownmembers. (This is what currently hides audio and game titles from those members.)
So the same query can legitimately return different results for the same airline depending on whether you sent a session — that is the gate working, not an inconsistency.
Flight-bound search
If the session is bound to a flight (via flightInstanceId at session
setup) and that flight has a pushed active
catalog, search results are intersected with
the flight’s active set — on every search mode, semantic queries and the
recommend-for-me lineup alike.
The intersection can only ever narrow. It never widens the airline catalog and never overrides member age-gating: an active title that is age-inappropriate for the member stays hidden. A session with no flight binding, a flight with no push, and every sessionless call keep full-catalog behavior exactly as before.
Search is one of three narrowed surfaces, alongside scored catalog serve and suggestions. Together they carry the guarantee end to end: a flight-bound session cannot be shown a title that is not loaded on the aircraft.
Results & scoring
Ranking is a fixed pipeline: structured filters prune the candidate set, vector similarity ranks the survivors, and an exact-title containment match adds a fixed boost on top of the semantic score.
Each result carries:
If you switch on the itemId prefix, handle all four. The prefix set is
M: / T: / A: / G:. The M: and T: forms are byte-identical to
earlier releases, so nothing you already parse changes — but a client that
branches on the prefix, or validates it against a two-value pattern, will
now meet A: and G: on any airline whose catalog carries audio or game
titles. This is the one place in the four-kind rollout where an
existing integration can actually break.
candidateCount is not a catalog-wide match total. Retrieval is
index-accelerated: ranking examines the query’s nearest-candidate set —
typically 3×topK, clamped to [50, 300]; queries with a cast filter
examine a wider set for recall — and candidateCount reports how many of
those examined candidates survived the filters. Treat it as “matches
among the examined nearest candidates”. For catalog-size signals use
totalItems and coverage (read the basis rule below) — never
candidateCount. (Shopping search’s
candidateCount is unaffected: that endpoint filters its whole catalog.)
What totalItems counts
totalItems is this request’s catalog: the titles the request could draw
from, before any query filter or age gate.
- On an ordinary request that is your visible catalog — your airline’s titles plus shared global content.
- On a flight-narrowed request it is your visible catalog
intersected with the flight’s active set.
coverageuses the same basis.
totalItems is a per-request basis figure, not a library size. It
equals your airline’s catalog only when the request is not narrowed — the
moment a session binds a flight with a pushed active catalog, the same
field reports a per-flight number. Don’t cache it as a catalog statistic,
and don’t compare it across bound and unbound requests.
Coverage, reported honestly
coverage tells you how complete the metadata behind your results is —
reported, never papered over:
The three …Pct fields are percentages on the 0–100 scale (42.5 means
42.5%), never 0–1 fractions. They report how many of the items on this
request’s basis carry at least one genre, at least one cast
member, and a synopsis respectively — if a coverage number is low, semantic
ranking quality is limited by the catalog metadata, not by your queries.
Acceptable use & content safety
Search serves in-flight entertainment discovery, and every request passes a
content-safety check before any model runs. A query refused on safety or
scope grounds — attempts to misuse the search surface rather than find
catalog content — returns 422 content.refused with a friendly generic
message:
Three things an integrator should build against:
- Handle it as “show the message, don’t retry”. The refusal is about the
query text, not your integration — surface the message (or your own copy)
and let the passenger rephrase. It is deliberately distinct from
422 request.validation(your request broke a rule — fix and retry), so branch oncode, not on the 422 status. - The message never says which check fired. No rule identifier is disclosed, by design.
- Refusal ≠ degradation. If the AI parse is merely unavailable
(throttled, transport fault, or a moderation-provider outage), search still
degrades to the deterministic rules fallback and returns a normal
200with"source": "rules"— a refused query is the only case that 422s.
Benign catalog and personalization queries — everything this guide documents — are unaffected. The same check protects shopping search; every other model-backed endpoint surfaces content blocks in its own documented denial shape.
Worked example
Abridged response (sandbox tenant):
Errors
Standard envelope: 422 request.validation (query length,
topK range, flightMinutesRemaining below 1, bad kindOverride),
422 content.refused, 400 session.unresolved (a
malformed X-Session-Id — only when you send the header),
429 rate.limited over the per-client request-rate
limit (honor the Retry-After header),
401 / 403, 413 request.malformed (body over 64 KiB), 500.
Full request and response schemas are in the API Reference.