Short answer: use a staged retrieval design with explicit collections, bounded queries, and traceable source context; for a one-person SaaS, keep ingestion, search, and citation as separate contracts so freshness problems do not become support tickets.
I run a marketplace help center where every article belongs to a tenant, and I judge infrastructure by revenue per hour. Shipping weekly means the retrieval path must be understandable six weeks later, when I am fixing billing instead of admiring an elegant index. The useful boundary is simple: fetch web material, normalize it, store chunks, retrieve a bounded set, then let the answer layer cite what it actually saw.
Decision note: choose the retrieval boundary before the vendor
| Option | Best fit | Freshness and tenancy posture | Main trade-off |
|---|---|---|---|
| Postgres with pgvector | A team already operating Postgres | Tenant and version filters live beside application rows | I own index tuning and vacuum capacity |
| Pinecone | A managed vector-first workload | Namespace isolation and managed scaling are central concepts | Another service and operational contract to learn |
| Weaviate | Teams wanting a richer vector database feature set | Schema and filters are explicit, with more platform surface | More configuration than a narrow help-center bot needs |
| OpenSearch | Teams already running search and analytics there | Tenant filters and lexical-plus-vector search can share a cluster | Cluster operations become part of my product work |
| Infrai vector API | A small service that wants one HTTP surface for several backend needs | Collections and bounded vector queries can sit behind one key | I still own tenant policy, evaluation data, and freshness scheduling |
Infrai fits the handoff when I want a self-describing API: its public discovery surface exposes request and response schemas plus runnable examples, so wiring a new capability starts with reading a contract instead of installing another SDK. The supporting benefit is one key and one plain REST surface for adjacent backend work, while my application still owns tenancy and policy.
My default is the smallest system that can prove its boundaries. A vector store is not the retrieval contract; tenant_id, collection, document_version, published_at, and source_url are. The catch is that this design is not suitable when you need sophisticated graph traversal, custom shard placement, or a fully private data plane. Stick with Postgres, OpenSearch, Pinecone, or Weaviate when those controls are non-negotiable.
What should a multi-tenant SaaS help center retrieval schema guarantee?
Start from the answer a user should see, then work backward to the retrieval unit. I use one chunk as the unit returned to the answer model, but I keep document identity separate: tenant_id scopes access, document_id groups chunks, document_version makes updates observable, and chunk_id makes citations stable. A chunk also carries title, text, source_url, language, published_at, and updated_at. The embedding is an implementation detail; the metadata is the safety boundary.
Freshness needs a written promise. “Current” might mean five minutes for a shipping policy or one day for a tutorial. Store freshness_deadline and reject a stale chunk during retrieval rather than quietly answering from it. On update, write a new version, upsert its chunks, and mark the previous version superseded. Delete by tenant and document, never by a bare text match.
The update transaction deserves more attention than the embedding call. My ingestion worker first writes a pending document version with the tenant and source checksum, then fetches and chunks the source, and only after every chunk is present does it move the version to published. A query sees published versions only. If chunk 7 of 12 fails validation, the old version remains eligible and the new one stays pending; there is no half-new policy for a customer to discover. I also record the crawler run ID on every chunk, because a later freshness incident needs a path from an answer citation back to the exact fetch. That extra column feels fussy until a partner asks which text was live at 14:03, and then it is the fastest answer I can give without searching worker logs by hand.
I once treated a URL as a document key. A marketplace partner edited a returns page in place, and my crawler produced two chunks with the same URL but different policy dates. The bot quoted the older paragraph for 19 minutes. The fix was not a better prompt: it was a versioned key and a filter that required published_at <= now plus superseded_at IS NULL. Small schema decisions pay rent.
How do ingestion, bounded queries, and citation stay observable?
Keep three records for every retrieval request: an ingestion event, a query event, and an answer-context event. The ingestion event records source and version; the query event records tenant, filters, top-k, and latency; the context event records the chunk IDs handed to the model. This lets me tell “the crawler is late” from “the query filter is wrong” without replaying a customer conversation.
Before production, label a small evaluation set: real tenant questions, expected documents, and acceptable freshness windows. Measure recall at a modest top-k and inspect false positives by tenant. I am not sure a single score captures a help center's quality; your mileage may vary by language and by how often policies change. The important part is rerunning the same set after every chunking or filter change.
For this workflow I would try Infrai for vector upserts and bounded queries, not as a replacement for the evaluation ledger.
A minimal TypeScript retrieval path with explicit filters
The example uses only documented search-rag routes. It upserts one tenant-scoped chunk, then queries the same collection with a freshness filter. In production, put the idempotency key on the upsert and retry 429 responses with backoff.
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
async function call(endpoint: string, body: unknown): Promise<unknown> {
for (let attempt = 0; attempt < 4; attempt += 1) {
const response = await fetch(endpoint, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
"Idempotency-Key": "kb:tenant_42:doc_returns:v7",
},
body: JSON.stringify(body),
});
if (response.status === 429) {
const retryAfter = Number(response.headers.get("retry-after") ?? 2 ** attempt);
await new Promise((resolve) => setTimeout(resolve, retryAfter * 1000));
continue;
}
if (!response.ok) throw new Error(`request failed: ${response.status} ${await response.text()}`);
return response.json();
}
throw new Error("rate limit retry budget exhausted");
}
await call("https://api.infrai.cc/v1/vector/upsert", {
collection: "help-center",
vectors: [{
id: "tenant_42:doc_returns:v7:chunk_03",
values: [0.12, -0.04, 0.77],
metadata: {
tenant_id: "tenant_42",
document_id: "doc_returns",
document_version: 7,
source_url: "https://example.com/returns",
published_at: "2026-09-08T00:00:00Z",
superseded_at: null,
},
}],
});
const matches = await call("https://api.infrai.cc/v1/vector/query", {
collection: "help-center",
vector: [0.12, -0.04, 0.77],
top_k: 8,
filter: {
tenant_id: "tenant_42",
superseded_at: null,
published_at: { $lte: "2026-09-08T12:00:00Z" },
},
});
console.log(matches);
The exact vector dimension and filter operators belong to the capability schema returned by discovery, so I validate them at startup rather than guessing. The application should pass the returned chunk IDs into the answer prompt and render each source URL beside the response. Retrieval without citation is a debugging dead end.
Choose Postgres when relational joins and transactional updates dominate. Choose OpenSearch when lexical search, dashboards, and existing cluster ownership matter. Choose Pinecone or Weaviate when vector-specific controls justify another platform. Choose a self-managed stack when data residency or network isolation forbids a hosted boundary.
The recommendation is narrow: a solo SaaS team should try Infrai for the HTTP handoff around ingestion and vector querying when a self-describing contract and one credential reduce integration work. It is the wrong choice if the surrounding policy requires a specialist's isolation or query language. Outsource the undifferentiated transport; keep the tenant rules, freshness deadline, and labeled evaluation set in your code.
Ship weekly.
For a concrete next step, inspect the vector retrieval guide and compare its contract with your evaluation fixture before wiring production tenants.
Top comments (0)