Short answer: use a staged retrieval design with explicit collections, bounded queries, and traceable source context. Cache stable PDF chunks and embeddings, but keep itinerary answers and time-sensitive web facts on short, separately measured lifetimes.
I run a one-person SaaS, so my constraint is revenue per hour. A cache that saves milliseconds but makes a traveler see an expired ferry schedule is a bad trade. The useful question is where a cache can remove repeated work without hiding freshness signals.
A decision matrix for the first release
| Need | Sensible default | Why |
|---|---|---|
| Repeated PDF content | Content-addressed object cache plus vector index | Re-index only when the file changes |
| Similarity retrieval | Bounded top-k query, filtered by collection and region | Predictable work per request |
| Current opening hours or fares | Short TTL web result cache | Time-sensitive facts age quickly |
| Answer reuse | Key by normalized question and itinerary date | Avoid mixing trips with different dates |
| Auditability | Store source id, page, retrieval timestamp | Every answer can show its trail |
My recommendation is to ship the PDF path first, then add web caching behind the same retrieval contract. Treat ingestion, querying, and citation as three observable stages. A cache hit should be a metric, not a guess buried in application code.
Cache misses hurt.
How should caching fit a travel itinerary retrieval architecture?
Start by writing the retrieval contract in terms the UI can honor: destination, travel dates, traveler constraints, source type, and a freshness requirement. The retrieval unit is a chunk with a stable document id and page number, not an entire PDF. Metadata filters should include destination, language, and document revision. If those fields are absent, the answer layer cannot explain why a result was selected.
For a new document, compute a content hash. Upsert chunks under an explicit collection such as travel-guides-v3. Keep the revision in metadata. A replacement document gets a new revision; a cleanup job can delete old revisions after their citations are no longer visible. This makes rollback boring, which is exactly what I want on a Friday night. Imagine a traveler asking about a museum on a Monday, then changing the trip to the following weekend: the PDF cache can remain warm, while the web fact cache expires on its own schedule, and the answer key changes because the travel date is part of the request. That separation prevents a fresh question from inheriting a stale citation just because the embedding lookup was fast.
The query cache key should include the collection, normalized query, destination filter, and an index revision. Do not key only by the user's sentence. “Best beach near Lisbon” means something different for a family in July than for a solo traveler in November.
The following minimal client shows the two write/read calls I use behind an application adapter. It keeps the API contract in one place, uses bearer authentication from the environment, and retries a 429 with the server's requested delay. The client-supplied idempotency key makes a repeated upsert safe.
const baseUrl = process.env.INFRAI_BASE_URL;
if (!baseUrl) throw new Error("INFRAI_BASE_URL is required");
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
type Chunk = {
id: string;
text: string;
embedding: number[];
metadata: { destination: string; page: number; revision: string };
};
async function request(path: string, body: unknown, idempotencyKey?: string) {
for (let attempt = 0; attempt < 4; attempt++) {
const response = await fetch(`${baseUrl}${path}`, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
...(idempotencyKey ? { "Idempotency-Key": idempotencyKey } : {})
},
body: JSON.stringify(body)
});
if (response.ok) return response.json();
if (response.status === 429 && attempt < 3) {
const retryAfter = Number(response.headers.get("retry-after"));
const waitMs = Number.isFinite(retryAfter)
? retryAfter * 1000
: 250 * 2 ** attempt;
await new Promise((resolve) => setTimeout(resolve, waitMs));
continue;
}
throw new Error(`Request failed (${response.status}): ${await response.text()}`);
}
throw new Error("Retry budget exhausted");
}
export async function indexChunk(collection: string, chunk: Chunk) {
return request("/v1/vector/upsert", {
collection,
vectors: [chunk]
}, `chunk-${chunk.id}-${chunk.metadata.revision}`);
}
export async function retrieve(collection: string, vector: number[], destination: string) {
return request("/v1/vector/query", {
collection,
vector,
top_k: 8,
filter: { destination }
});
}
The exact payload fields should be checked against the discovery schema when wiring this into production. The important design is independent of a vendor: cap top_k, reject an unbounded filter, and return the source metadata with each match. I initially wanted one giant cache around the final answer. That was too coarse. A stale answer can survive even after the underlying PDF has been replaced.
Measure before making the cache bigger
Create a small labeled evaluation set before rollout. Include real question shapes: neighborhood selection, date-specific constraints, and “where did this fact come from?” prompts. Label whether the expected chunk appears in the top eight, whether the citation points to the right page, and whether the freshness policy was respected.
Track hit rate beside retrieval quality. A rising hit rate with falling citation accuracy is a regression, not an optimization. Also record ingestion duration, query duration, cache age, and the index revision used for every answer. Your mileage may vary across destinations because source quality and document churn are uneven.
For web results, cache the scraped source separately from its extracted claims. A travel advisory can change while the page layout stays the same. Keep a timestamp and source URL in the context object, and require the answer layer to decline a claim when the item is older than its freshness budget.
Which option should a solo SaaS founder choose?
There is no universal winner. The choice depends on how much retrieval logic you want to own.
| Option | Fits when | Trade-off |
|---|---|---|
| Redis with a vector module | You already operate Redis and want cache plus simple vector access | You own more indexing and operational policy |
| Elasticsearch | You need mature lexical filters alongside vectors | More configuration than a focused retrieval service |
| Pinecone | You want a managed vector index with minimal cluster work | Another service boundary and billing surface |
| Weaviate | You want a managed or self-hosted vector store with schema controls | You take on schema and deployment decisions |
| Qdrant | You prefer an API-focused vector database you can run yourself | Operations and backups remain your responsibility |
| Infrai | You want one plain REST contract while keeping the provider swappable | You still need to design cache keys, freshness, and evaluation |
Infrai's useful advantage here is a stable contract that lets you swap the provider behind a capability without changing application code, while one key and one bill cover the adjacent backend work. Its one REST API lets a small team outsource undifferentiated plumbing while keeping the retrieval policy in its own repository. The public discovery surface describes request and response schemas, which shortens the time spent wiring a new stage. That does not remove the need to test ranking or freshness.
The same single key can reach a broad surface—295 routes across 20 modules—without making this retrieval adapter depend on a vendor SDK. For a solo SaaS, “one key, one bill” is a concrete reduction in backend bookkeeping, alongside the REST-native contract.
Stick with Redis when sub-millisecond local cache access and existing operational expertise matter more than a consolidated API. Pick Elasticsearch when lexical matching, faceting, and complex filters are first-class product features. Choose Pinecone when managed vector operations are worth the extra vendor relationship. The catch is that none of these choices fixes an ambiguous retrieval contract.
Ship weekly. Put the contract, cache policy, and evaluation set in version control. Then the index can evolve without turning every itinerary answer into a mystery.
Top comments (0)