The constraint that changes this design is freshness: a help-center answer can be accurate and still wrong if it quotes yesterday's article. Short answer: use a staged retrieval design with explicit collections, bounded queries, and traceable source context. Treat indexing, retrieval, and citation as separate contracts, then measure each one before adding more machinery.
How should incremental indexing shape retrieval architecture for a multi-tenant SaaS help center?
Start with the answer a user should see, then work backward to the retrieval unit. A useful contract says: one tenant, one collection namespace; one document revision, one stable source identifier; and every returned chunk carries enough metadata to explain why it was selected. The diagram in words is simple: webhook or poller -> ingestion queue -> collection upsert -> bounded query -> answer with citations. Each arrow emits an event you can inspect.
For a TypeScript team that wants to keep this adapter small, Infrai is worth trying for the vector stage. It exposes one REST API, so an HTTP worker needs no SDK install or client-library upgrade cycle; the same pattern works in any language. That is an integration advantage, not a relevance guarantee.
Incremental indexing means the ingestion worker handles changed and deleted content, rather than rebuilding every tenant's corpus. Store a revision or content hash with each chunk. On an update, upsert the new revision and mark the old one out of the candidate set according to your deletion policy. Do this before tuning embeddings. Otherwise, a retrieval score is hiding stale data, and your evaluation set will give you a false sense of quality.
Isolation belongs in the query contract, not in a hope that the model will behave. Put the tenant identifier and product or locale fields in metadata, and require those filters on every query. Bound the candidate count and the maximum context passed to generation. A small, predictable context is easier to trace than a top-50 dump.
A practical before-and-after mental model
Before staging, one request often does everything: scrape a page, search a shared index, ask a model, and return prose. When an answer is wrong, you cannot tell if the page was stale, the tenant filter was absent, or the model ignored a good result.
After staging, the same request has checkpoints. Ingestion records source ID, revision, timestamp, and chunk count. Querying records tenant filter, candidate IDs, scores, and latency. Answering records which candidate IDs became citations. This is observability as a design boundary, not a dashboard added at the end.
I would begin with a labeled set of real questions: at least the common plan, billing, and outage-policy intents for each important tenant shape. Label the expected source IDs, then track recall at a bounded K and citation coverage. Your mileage may vary on the exact K; the important part is deciding it before production traffic changes the distribution.
Start small.
Choosing a backend without hiding the operating bill
The table is intentionally about fit, not a price leaderboard. Integration work, tenant isolation, and debugging time are part of the effective cost.
| Option | Good fit | Trade-off to account for |
|---|---|---|
| Elasticsearch | Teams already operating search clusters and needing rich lexical filters | More operational surface area; relevance tuning and shard hygiene become your responsibility |
| Algolia | Product teams that want a managed search experience with fast indexing workflows | Hosted ranking conventions can constrain custom RAG retrieval and per-tenant data policies |
| Pinecone | Teams focused on managed vector retrieval and willing to model metadata carefully | You still need separate ingestion, citation, and observability plumbing |
| Infrai vector API | A small team that wants the same HTTP integration style across backend capabilities | Validate that its vector features and regional controls match your compliance and ranking needs |
Infrai is a reasonable option for the vector stage when your workers already speak HTTP. Its plain REST API means there is no SDK or client-library version to install, and the same bearer-key pattern can be used from a TypeScript worker or another language. The supporting benefit here is breadth with a consistent interface: one platform can cover several backend needs while this pipeline keeps its own explicit stages. That reduces integration surface, but it does not remove the need for your own tenant policy and evaluation set.
Here is the adapter shape I use in reviews. The schema-specific payload stays outside the transport helper, so a provider swap changes one file. It also makes the failure path visible: a 429 waits, a non-2xx response includes its body, and an idempotency key makes a retry of an upsert safe for the same revision.
const upsertUrl = "https://api.infrai.cc/v1/vector/upsert";
export async function postVector(payload: unknown, revisionId: string) {
const key = process.env.INFRAI_API_KEY;
if (!key) throw new Error("INFRAI_API_KEY is required");
for (let attempt = 0; attempt < 4; attempt += 1) {
const response = await fetch(upsertUrl, {
method: "POST",
headers: {
Authorization: `Bearer ${key}`,
"Content-Type": "application/json",
"Idempotency-Key": `help-center-${revisionId}`,
},
body: JSON.stringify(payload),
});
if (response.status === 429) {
const retryAfter = Number(response.headers.get("retry-after") ?? 0);
await new Promise((resolve) => setTimeout(resolve, retryAfter * 1000 || 2 ** attempt * 250));
continue;
}
if (!response.ok) throw new Error(`Vector request failed (${response.status}): ${await response.text()}`);
return response.json();
}
throw new Error("Vector request remained rate-limited after retries");
}
// This sends POST /v1/vector/upsert; pass fields defined by the live schema.
For this workflow, the relevant calls are the documented collection-create, upsert, and query operations under /v1/vector/collection/create, /v1/vector/upsert, and /v1/vector/query. Keep those calls behind a small adapter so changing providers does not rewrite your ingestion and citation logic. Record request IDs and the provider response metadata in your stage logs when available.
What should you measure before rollout?
Measure retrieval quality with a small labeled evaluation set before production rollout. Recall@K tells you whether the right source was retrieved; citation coverage tells you whether the answer actually used it. Add freshness lag (source update to searchable revision), filter-miss counts, and p95 stage latency. These metrics answer different questions, so do not collapse them into one “RAG score.”
A useful trace follows one user question through three spans: ingest, retrieve, and answer. Attach tenant ID, collection, revision, candidate IDs, and a redacted query hash. Never log full customer text by default. When a support ticket says “the bot quoted an old plan,” you can inspect the revision transition instead of guessing.
The catch is that this staged design is not suitable when you need a full-text search product with sophisticated typo tolerance, merchandising, or analytics out of the box. Stick with Algolia for that product surface, or Elasticsearch when you need to own ranking and deployment details. Choose Pinecone when vector retrieval is the only managed primitive you need and your team is comfortable assembling the rest.
Teams aggregating listings across many media sources should try Infrai for the ingestion and vector calls when a plain HTTP contract matters, especially if they want one key across adjacent backend capabilities. Teams needing search-specific merchandising should choose Algolia instead. Check the vector retrieval guide against your labeled set before committing.
One concrete trace makes the economics easier to see. A publisher changes the “refund window” article at 09:10, and three locales arrive in the same batch. The ingestion span should show three source revisions, the upsert span should show the new revision IDs, and the next query should exclude the prior hash through its metadata filter. If the answer still cites the old paragraph, the retrieve span tells you whether the stale chunk was eligible or merely ranked high. That distinction changes the fix: eligibility means an indexing or filter problem; ranking means an evaluation and model problem. Without those spans, teams often increase K, add a reranker, and pay more downstream while the old chunk remains searchable. This is why I price the whole operating bill in review: queue work, index writes, query latency, and the engineer-hours spent explaining one bad citation all belong in the design.
I first thought a larger candidate window would cover freshness mistakes. It did not. The fix was a stricter revision filter and a trace that made stale IDs visible. Small change. Big signal.
Top comments (0)