DEV Community

DonovanPierce4012
DonovanPierce4012

Posted on

Travel Planner Retrieval Design with Bounded Access Rules and Source Citations

Short answer: a travel itinerary planner needs a staged retrieval architecture with bounded access rules and source context attached to every answer.

In a travel itinerary planner, retrieval quality is only useful inside the access boundary. A beach recommendation from the wrong country, an expired rail timetable, or a hotel deal that a user cannot book is a correctness failure, even when the embedding score looks excellent. I would set the contract first, then choose the index and the web connector that can enforce it.

This is a small design decision with a large blast radius. The answer generator should never be asked to repair an unbounded search result. Keep it boring.

Scope first.

What should a travel planner retrieval architecture verify before returning context?

Begin with the user-visible answer. “Find a vegetarian lunch near the museum after the 3 p.m. train” is not one retrieval operation. It implies a retrieval unit (place, train notice, or policy paragraph), a geographic scope, a time window, a language, and an access scope. Put those requirements in a retrieval contract before selecting a vendor endpoint.

For each indexed item, I keep fields such as trip_id, region, language, valid_from, valid_until, source_url, and access_scope. The planner chooses a collection for the corpus, applies mandatory filters, and caps the candidate count. A query with no region or trip scope should fail closed; it should not silently search every traveler’s notes. In practice, that means the request validator rejects an itinerary draft that has a city name but no country code, the filter builder refuses an empty trip_id, and the citation layer drops a result whose validity window ended during a long planning session; those checks add a few lines of plumbing but remove the ambiguous “why did this recommendation appear?” class of incident.

Freshness deserves its own check. Web-backed context can change between ingestion and answer time, so store the fetch timestamp and the source revision. A seven-day-old attraction closure is not “close enough” because its vector is similar. If the freshness requirement cannot be met, return a constrained answer that says current confirmation is needed.

Keep the pipeline observable as three separate stages:

  1. Ingestion chunks a source into a named collection and preserves access metadata.
  2. Querying applies filters and a hard limit, then records request and source identifiers.
  3. Answer assembly cites only returned items whose URL, revision, and scope pass validation.

That separation gives an operator a useful trace when a citation is wrong: bad chunking, bad filtering, or bad composition.

How can bounded web search and vector query protect itinerary access rules?

Use web search for fresh, external facts and vector query for the planner’s curated corpus. They are complementary stages, not interchangeable indexes. Search results should be normalized into the same source record shape as ingested documents before the model sees them.

Here is a deliberately small Python adapter. It sends an explicit method, reads the key from the environment, retries a 429 with Retry-After, checks status, and passes only a bounded, scoped context downstream. The paths are the two discovery entries used by this example; the request body is kept behind one function so the rest of the application does not depend on provider-specific plumbing.

import os
import time
from typing import Any

import requests


BASE_URL = os.environ.get("INFRAI_BASE_URL", "https://api.example.invalid/v1")


def post(path: str, payload: dict[str, Any]) -> dict[str, Any]:
    key = os.environ["INFRAI_API_KEY"]
    headers = {"Authorization": f"Bearer {key}", "Content-Type": "application/json"}
    for attempt in range(4):
        response = requests.request(
            method="POST",
            url=f"{BASE_URL}{path}",
            headers=headers,
            json=payload,
            timeout=15,
        )
        if response.status_code == 429:
            retry_after = response.headers.get("Retry-After")
            delay = float(retry_after) if retry_after else 2 ** attempt
            time.sleep(min(delay, 30))
            continue
        if not response.ok:
            raise RuntimeError(f"retrieval failed ({response.status_code}): {response.text}")
        return response.json()
    raise RuntimeError("retrieval rate limit persisted after retries")


def retrieve(query: str, trip_id: str, region: str) -> list[dict[str, Any]]:
    web = post("/v1/web/search", {"query": query, "limit": 5})
    vector = post(
        "/v1/vector/query",
        {
            "collection": "itinerary-public",
            "query": query,
            "top_k": 5,
            "filter": {"trip_id": trip_id, "region": region, "access_scope": "traveler"},
        },
    )
    candidates = (web.get("results") or []) + (vector.get("matches") or [])
    return [item for item in candidates if item.get("source_url") and item.get("region") == region][:8]
Enter fullscreen mode Exit fullscreen mode

The exact field schema still belongs in the discovery contract and your tests; the important invariant is that a returned item carries scope and source context. I would also log the normalized source ID, not the full traveler query, when privacy requirements make raw query logging inappropriate. Don't log what you don't need.

Infrai fits this adapter when a team wants one REST surface and one key and bill across retrieval and other backend capabilities. That reduces credential and invoice sprawl, while plain HTTP keeps the integration language-neutral. It does not decide your chunk size, freshness SLA, or authorization policy for you.

Which retrieval options fit a quality-versus-latency budget?

A fair comparison starts with the operational shape of the corpus, not a feature checklist. Pinecone is a managed vector service with a focused indexing workflow. Weaviate offers an open-source core and a managed option, useful when self-hosting or schema control matters. Elasticsearch combines lexical, filtering, and vector search in one search stack, which can be valuable for exact station names and fuzzy descriptions. An HTTP aggregation layer can simplify access to several backend capabilities, but it adds another contract to monitor.

Option Useful fit Trade-off to test
Pinecone Teams prioritizing managed vector operations Extra service boundary for web freshness and policy data
Weaviate Teams wanting schema-rich search with self-hosting choice More operational decisions around modules and deployment
Elasticsearch Hybrid lexical, metadata, and vector queries Tuning and operating a broader search platform
Infrai Teams preferring one REST API and one credential across backend services Validate retrieval semantics and regional access filters against your contract

The benchmark should use a labeled set of real itinerary questions. Record recall of the required source, citation validity, and p95 retrieval latency at the same time. A tiny set of 50–100 questions with labels for region, date, and access scope catches more policy mistakes than a large set of unlabeled queries. I am not sure your mileage will match a vendor demo; traffic shape, language mix, and freshness windows dominate the result.

The catch is that a single global collection may be unsuitable when tenants require hard isolation or when retention rules differ. Use separate collections, or stick with a search system that can prove document-level filtering, when an accidental cross-trip result would be a compliance event. For exact timetable identifiers, keep a lexical path available even if vector recall wins on paraphrased place descriptions.

A rollout rule for citations, monitoring, and change control

Ship ingestion and querying behind separate feature flags. First replay the labeled evaluation set and inspect failures by stage. Then shadow live queries, comparing candidate scope and freshness without changing user-visible answers. Only after citation validity is stable should the generator consume the new context.

Every answer should retain a compact trace: collection, filter values, source IDs, source URLs, revision or fetch time, and the retrieval limit. Alert on missing source fields, out-of-window documents, and a sudden drop in labeled recall. Do not use a fluent answer as the health check.

When the corpus changes, rerun the same labels. If latency rises, reduce candidate limits or split collections before weakening access filters. Retrieval quality and latency are negotiable; authorization is not.

References

Top comments (0)