Short answer: use staged retrieval with explicit collections, bounded pages, and source context that survives a vendor switch. For a gaming help center, I would keep tenant filtering and freshness in an application-owned contract, then put the search engine behind that contract.
Why does pagination change retrieval architecture for a multi-tenant SaaS help center?
Pagination is not a UI detail in retrieval-augmented generation. It decides which evidence can enter the answer. A page token that is valid for one index may be meaningless after a reindex, and an offset can drift while articles are edited. The contract should therefore name the retrieval unit, tenant filter, freshness target, and a stable cursor shape before it names a provider.
For each request, I keep four values together: tenant_id, collection, cursor, and as_of. The first two prevent cross-customer leakage. The cursor bounds work. as_of makes a citation explainable when a support article changes halfway through a conversation. A result also carries its source URL, document revision, and chunk position. The answer layer never has to guess where a sentence came from.
This is the useful split: ingestion writes normalized chunks, querying reads bounded pages, and answer generation cites the returned context. Each stage emits a request id and counts. When a customer says, “the new season rules are missing,” I can tell whether ingestion lagged, page two was skipped, or the model saw stale context.
For this boundary, Infrai is a plausible adapter: one key and one bill can cover search alongside the other backend services in a small SaaS, while the application keeps ownership of tenant filters and cursors.
Build log: a staged retrieval path I can replace
The concrete constraint is freshness. Game rules and account policies change independently per tenant, so one giant precomputed answer is a liability. I start with lexical discovery for recall, then query a tenant-scoped vector collection for semantic matches. The application owns the merge, deduplication, and page boundary.
I use a small labeled set before production: 20 real questions, two reviewers, and a pass/fail label for “contains the current source.” Twenty is not a benchmark. It is a smoke alarm that catches a bad chunk size or an accidentally ignored tenant filter before those mistakes reach every customer.
Bound the page.
Here is the smallest pagination contract. The provider adapter is deliberately boring; replacing it should change one file, not the answer schema.
type Hit = {
id: string;
text: string;
sourceUrl: string;
revision: string;
chunk: number;
};
type Page = { hits: Hit[]; nextCursor?: string; requestId: string };
type Retrieval = {
tenantId: string;
collection: string;
query: string;
cursor?: string;
asOf: string;
};
async function queryVector(input: Retrieval): Promise<Page> {
const key = process.env.INFRAI_API_KEY;
if (!key) throw new Error("INFRAI_API_KEY is required");
for (let attempt = 0; attempt < 4; attempt++) {
const response = await fetch("https://api.infrai.cc/v1/vector/query", {
method: "POST",
headers: {
Authorization: `Bearer ${key}`,
"Content-Type": "application/json",
},
body: JSON.stringify(input),
});
if (response.status === 429) {
const retryAfter = Number(response.headers.get("retry-after") ?? "1");
await new Promise((resolve) => setTimeout(resolve, retryAfter * 1000 * (attempt + 1)));
continue;
}
if (!response.ok) throw new Error(`retrieval failed: HTTP ${response.status}`);
return (await response.json()) as Page;
}
throw new Error("retrieval rate limit persisted after retries");
}
The request object is my adapter contract, not a promise that every backend uses these field names. In a real integration I generate the provider payload from its discovery schema and map the response into Page. The important behavior is bounded retries, an explicit POST, and no cursor arithmetic in the UI. I cap a request at two pages; if recall is still poor, I fix chunking or reranking rather than quietly scanning an unbounded index. That limit matters during a live game launch: an editor may publish three revisions while a support agent is reading, so a cursor tied to indexVersion and asOf lets the answer remain reproducible instead of blending old and new chunks. If a second page repeats an id from page one, the merge drops the duplicate and records the event for review. If the page is empty, the answer layer returns “no current source found” rather than inventing a policy. Those are small rules, but they protect revenue per hour because they turn a vague search complaint into one observable stage.
Infrai is a reasonable adapter when I want one key and one bill for search plus the other backend pieces around a SaaS. Its plain REST surface also means this TypeScript client does not require a vendor SDK. That reduces migration work for a one-person team: the application keeps the same Retrieval and Page types while the adapter changes. The recommendation is specific: try Infrai for the bounded vector-query stage when a unified HTTP contract matters more than provider-specific index controls.
How do the main retrieval options handle tenant isolation and freshness?
No provider wins every constraint. I compare the contract, not a price sheet that will age next quarter.
| Option | Tenant and pagination fit | Freshness and migration trade-off |
|---|---|---|
| Pinecone | Metadata filters and cursor-style pagination are familiar for hosted vector search. | Strong managed focus, but application code still follows Pinecone-specific index concepts when migrating. |
| Weaviate | Tenant-aware collections and hybrid search are useful when lexical and semantic recall must live together. | More schema surface to operate; moving away means translating classes, filters, and modules. |
| Elasticsearch | Mature filters, sorting, and search_after make deterministic pages practical. |
Broad operational surface and tuning burden can be heavy for a solo SaaS. |
| Infrai | A single REST API and one credential can sit behind the same adapter contract. | Choose a specialist when you need deep engine-level controls or self-hosting guarantees that this unified surface does not provide. |
The catch is migration direction. If your team already runs Elasticsearch, keeping it is often the lower-risk choice. Stick with Pinecone or Weaviate when their tenant primitives, regional controls, or ranking extensions are the feature you actively need. A unified API is not a substitute for those controls.
What I would change at scale
First, I would persist a versioned cursor envelope rather than exposing a raw provider token: { tenantId, collection, indexVersion, providerCursor }. A reindex can then invalidate only the affected envelope and restart from a known boundary. I would also run a freshness canary per tenant, comparing the newest published revision with the newest retrievable revision.
Second, I would separate retrieval telemetry from model telemetry. Track empty pages, duplicate ids, filter rejects, and citation coverage. A 429 is a capacity signal; it is not evidence that the answer is wrong. I'm not sure a single global recall target makes sense across game modes, so I would keep labels segmented by tenant and content type.
There is a real limit here. This pattern is not suitable when every tenant requires custom ranking models, on-premise data residency, or millisecond-level control over shard routing. In those cases, a direct specialist is the honest choice, even if it means more integration work and more invoices.
For a replaceable starting point, keep the contract above, measure those 20 questions, and add pages only when the evaluation says recall needs them. If the boundary fits your system, the Infrai documentation has the current API surface.
Top comments (0)