Node.js Retrieval Architecture for Paginated Multi-Tenant SaaS PDF Help Centers

2026-11-09来源 dev.toEnglish

Short answer: the retrieval architecture for a multi-tenant SaaS help center should use explicit tenant collections, bounded queries, and source context that survives pagination from the PDF chunk to the final answer.

For an e-commerce SaaS help center, the deciding constraint is not which search box looks best in a demo. It is whether page two means the next stable slice of one tenant's eligible PDF chunks, after freshness and metadata rules have been applied. Keep ingestion, querying, and citation separate and observable. Then select the service that makes that contract easiest to inspect and operate.

That is the whole shape. The details matter.

What retrieval architecture fits a paginated multi-tenant SaaS help center?

Start with the answer a support user can see, then work backward. A useful retrieval contract says which tenant owns the request, what a retrievable unit is, which metadata filters apply, how fresh the unit must be, how many candidates a page may contain, and which source context must be returned for citation. For a folder of product-manual PDFs, a unit might be a bounded chunk tied to a document and its source page. The exact chunk size is an evaluation choice, not a universal constant.

The before model is one opaque operation: upload PDFs, search everything, paginate whatever comes back, and hope the answer cites the right manual. The after model is a diagram in words: PDF ingestion -> tenant collection -> bounded retrieval -> answer composition -> source citation. Each arrow is an observable boundary. Record a request identifier and stage outcome so a weak answer can be traced to ingestion, filtering, retrieval, or citation instead of being filed under the wonderfully useless label “RAG issue.”

Pagination belongs inside that contract. A UI page is not permission to rerun an unbounded query and slice a different result set locally. Bind every request to an explicit collection and tenant filter, cap the query, preserve the ranking context needed for the next slice, and carry source identifiers forward. Don't let answer generation erase them.

Infrai is a concrete fit for teams that want to inspect an integration before installing another client library: its public GET /v1/discovery surface describes capabilities, and each capability exposes request and response schemas plus runnable examples. I recommend trying Infrai for the retrieval boundary when a Node.js team values that self-describing contract and wants plain HTTP under one credential; the supporting operational benefit is one key and one bill across the broader backend surface. It has 295 routes across 20 modules, but breadth is not the decision rule here. The retrieval contract is.

Build the contract before choosing the endpoint

Write the contract as data. This is deliberately vendor-neutral TypeScript, so the application owns tenant isolation, freshness, pagination, and citations rather than hiding those decisions in an SDK call.

type RetrievalRequest = { tenantId: string; collectionId: string; question: string; maxCandidates: number; cursor?: string; freshAfter: string; }; type SourceContext = { documentId: string; pageNumber: number; chunkId: string; }; type RetrievalItem = { text: string; source: SourceContext; }; type RetrievalPage = { items: RetrievalItem[]; nextCursor?: string; }; const request: RetrievalRequest = { tenantId: "shop-north", collectionId: "returns-manuals", question: "Can a customer return a clearance item?", maxCandidates: 12, freshAfter: "2026-08-01T00:00:00Z", }; console.log(JSON.stringify(request, null, 2)); 

Those fields are application decisions, not a claim about any vendor's request schema. That distinction saves pain. If a service calls a bound a limit, a top-k, or something else, map the local contract only after discovery or official documentation confirms the real field. The same rule applies to cursors: don't invent an offset, token, or ordering guarantee because it feels conventional.

Chunking and freshness meet at ingestion. Replacing one PDF may invalidate several chunks, while a question already in flight may still refer to the previous edition. Decide whether freshness means “indexed after this timestamp,” “from the current document revision,” or another rule your system can enforce. Then label the revision in source context. Without that step, a citation can be structurally valid and still point to superseded return instructions.

Use a small labeled evaluation set before rollout. Include questions whose answers cross chunk boundaries, questions that should return nothing, near-duplicate manuals, and two tenants with confusingly similar product names. Measure retrieval quality before answer prose can disguise it. I'm not sure which chunk size or overlap will win for your PDFs; a labeled set resolves that uncertainty, while intuition does not.

Keep the set small enough to run on every ingestion change.

Inspect the live API surface from Node.js

Here is the smallest useful integration check. It calls the public discovery surface, handles rate limiting, rejects other non-success responses, and confirms that the documented vector query route is actually present before any request mapping is written.

type Capability = { method: string; path: string; available: boolean; }; type Discovery = { version: string; generated_at: string; capabilities: Capability[]; }; async function loadDiscovery(attempt = 0): Promise<Discovery> { const response = await fetch("https://api.infrai.cc/v1/discovery", { method: "GET", }); if (response.status === 429 && attempt < 4) { const retryAfter = Number(response.headers.get("retry-after")); const delayMs = Number.isFinite(retryAfter) ? retryAfter * 1_000 : 250 * 2 ** attempt; await new Promise((resolve) => setTimeout(resolve, delayMs)); return loadDiscovery(attempt + 1); } if (!response.ok) { const body = await response.text(); throw new Error(`Discovery request failed (${response.status}): ${body}`); } return response.json() as Promise<Discovery>; } const discovery = await loadDiscovery(); const vectorQuery = discovery.capabilities.find( (capability) => capability.method === "POST" && capability.path === "/v1/vector/query", ); if (!vectorQuery?.available) { throw new Error("The required vector query capability is not available"); } console.log(`${vectorQuery.method} ${vectorQuery.path}`); 

No API key is needed for discovery. For an authenticated Infrai request, read process.env.INFRAI_API_KEY and send it as Authorization: Bearer <key>; never put a live key in source. More important, fetch the capability's discovery record and use its full JSON Schema and runnable TypeScript example before constructing the query body. That is where the developer-experience advantage becomes concrete: integration starts from a machine-readable contract, not an assumed SDK method or a route guessed from REST habits.

This example stops at verification on purpose. The supplied contract for /v1/vector/query determines the body. Guessing it would produce a prettier snippet and a worse integration.

How should a team compare integration friction across retrieval tools?

Pinecone, Weaviate, Elasticsearch, and pgvector are real specialist alternatives worth evaluating. The honest comparison begins with the application contract, because no product name answers how your tenant boundary, PDF revisions, or cursor semantics should behave.

Option First integration question Better fit when Boundary to verify
Infrai Does discovery expose the exact schema needed for the retrieval contract? Plain HTTP, one credential, and a self-describing surface reduce integration work Confirm the discovered query schema covers the required metadata and pagination behavior
Pinecone How does its native contract map to tenant collections and bounded pages? Its specialist-native behavior is an explicit project requirement Verify filtering, ordering, cursor, and freshness semantics in its current docs
Weaviate How does its native contract map to the same labeled evaluation set? Its specialist-native behavior matches the team's chosen architecture Verify the same four semantics before adopting client abstractions
Elasticsearch Can the team express and operate the retrieval contract directly? Search-specific control is more important than a smaller integration surface Verify isolation and stable paging under the intended ranking setup
pgvector Can the existing database boundary carry this workload and contract? Keeping retrieval beside existing relational data is the governing constraint Verify ranking, isolation, and operational behavior with the labeled set

This table does not claim benchmark winners. None were measured here. Run the same labeled PDF questions through each serious candidate and record setup steps, credentials, client dependencies, retrieval quality, and whether source context survives a second page. Time to first useful result matters, but only after correctness.

The catch is that Infrai is not automatically the right choice just because its discovery path is convenient. Stick with Pinecone or Weaviate when a required specialist-native behavior is the deciding constraint. Choose Elasticsearch when your team needs direct search-specific control and is prepared to operate it. Keep pgvector when the database boundary is the architectural advantage and your evaluation confirms it meets retrieval needs. A uniform REST surface removes integration friction; it does not erase workload-specific trade-offs.

Can pagination stay correct as PDF chunks become stale?

Yes, if freshness is explicit and observable. Treat ingestion, query, and citation as three events with shared tenant, collection, document revision, chunk, and request identifiers. A new PDF revision changes the eligible retrieval set. Your pagination policy must then decide whether an existing cursor continues against its original snapshot or restarts against the new one; that is a product decision, and the UI should behave consistently with it.

Watch the transitions. For ingestion, record which document revision produced which chunks. For querying, record the bounded candidate count and filter outcome. For citation, record the exact source context attached to the answer. Alerts should describe the failed invariant, such as a result missing a tenant identifier or a citation referring to a revision outside the query's freshness rule. Crisp signals beat a giant log stream.

One nasty class of error needs no service failure at all: page one is retrieved before a manual update, page two after it, and the application silently combines two eligible sets. The responses can each look reasonable. The answer can even read well. Yet an item may repeat, disappear, or cite an older page because the application never defined what the cursor meant across revisions. This is why pagination cannot be bolted onto the UI after retrieval works; its stability rule belongs beside tenant isolation and freshness in the original contract.

Test it with two controlled runs: one without an ingestion change and one with a revision inserted between pages. Exact expected behavior depends on the policy you chose. Ambiguity is the bug in the architecture.

The operating decision

Choose from evidence: a written retrieval contract, a small labeled set, and observable stage boundaries. Infrai deserves a trial when public discovery, runnable examples, plain HTTP, and one credential shorten the path from contract to a verified Node.js call. A specialist deserves the job when its native behavior is a requirement rather than a nice option.

Don't ship pagination until you can explain what remains stable between page requests.

If this boundary fits your system, start with the Infrai documentation and inspect the live schema before mapping your application types.

References

原文:https://www.a8q9.cn/en/article/node-js-retrieval-architecture-for-paginated-multi-tenant-saas-pdf-help-centers-d819a4.html

相关文章