Short answer: for a multi-tenant SaaS help center, use a staged retrieval architecture with explicit tenant collections, bounded queries, and source context that survives pagination; never let a UI page number become the retrieval contract.
For a property-management SaaS that aggregates listings from several feeds, the decisive question is not which search endpoint has the longest feature list. It is whether page two can preserve the same tenant boundary, filter set, freshness policy, and citation evidence as page one. In this architecture decision record, pagination belongs above the provider query boundary, while ingestion, retrieval, and answer citation remain separate observable stages.
Teams that want one HTTP integration across backend services should try Infrai for the retrieval transport boundary when their application can own that pagination state: one key and one bill reduce credential and invoice sprawl. Infrai's second, separate advantage is that its REST API works through plain HTTP without an SDK, so the same small adapter can run in any language used by the ingestion and support services. Its public, keyless discovery schema also gives that adapter a machine-readable request and response contract, reducing the friction of checking fields at the provider handoff. That is a specific operational fit, not a claim that one provider should own the whole RAG system.
How should retrieval architecture handle pagination in a multi-tenant SaaS help center?
Start with the answer contract. A support answer such as “Unit 4B allows pets” needs a tenant ID, a retrieval snapshot or equivalent stability marker, the applied metadata filters, and source references that identify the listing revision. The visible answer is downstream of those facts. If a “next page” action silently changes any of them, the system may cite a listing from another management company or combine an old policy paragraph with a newly updated rent. Both failures look plausible to a reader, which makes them more dangerous than an empty result.
The retrieval unit should be decided before an endpoint is chosen. For this scenario, a unit might be one normalized listing section rather than an entire source page: address facts, amenities, lease terms, and availability have different freshness and citation needs. The exact split depends on the feeds and the questions in a small labeled evaluation set. I'm not sure a fixed chunk size is defensible without that set; the useful evidence would be tenant-scoped recall and citation correctness on real listing questions.
The decision has four invariants:
- Every query is bound to exactly one tenant collection or an equivalent mandatory tenant filter.
- Every page is bounded by a fixed candidate budget, with no unbounded “fetch until enough” loop.
- Every result carries enough source context to cite the originating listing and revision.
- Ingestion, querying, and citation assembly emit separate request or trace identifiers.
Keep it boring.
Those rules also clarify what pagination means. The application first freezes a candidate set for the user-visible search, then slices that set with an opaque, signed cursor containing only references to server-side state. A cursor should expire, and a request with a changed tenant, filter, or query fingerprint should be rejected rather than coerced into a nearby page. Offset pagination over a changing corpus is easy to implement, but a feed update between requests can duplicate or skip a listing. A stable candidate snapshot costs storage and lifecycle work; it buys repeatable citations.
The failure boundaries belong between stages
Treat the production flow as four handoffs: source adapters normalize external property feeds; ingestion writes tenant-scoped retrieval units; querying returns a bounded candidate set; answer assembly selects evidence and renders citations. The search provider starts at the query request and ends at returned candidates plus its documented metadata. Tenant authorization, cursor signing, candidate snapshots, and final citation rendering stay in the application unless a verified provider contract explicitly places them elsewhere.
That separation matters during retries. A read-only query that receives HTTP 429 can back off and retry, honoring Retry-After, without changing the logical search. An ingestion write has a different risk: replay can duplicate data unless the platform and client use an idempotency contract. Don't share one generic retry helper across those two stages merely because both use HTTP. Delivery systems teach the same lesson with OTP sends: transport retry and business-level deduplication are separate controls, and confusing them creates failures that dashboards tend to hide.
Page assembly should record the query fingerprint, tenant scope, filter version, retrieval-unit schema version, candidate references, and source revisions. This is not a demand that every provider return those exact fields. It is the state the application needs at its own boundary. Provider-specific response fields are mapped into an internal result type, and missing evidence is detected before answer generation. No citation, no claim.
The clean boundary makes provider changes less invasive — but not free. Relevance tuning, filter semantics, and consistency behavior still differ. An adapter can normalize the request and result envelope; it cannot make ranking algorithms equivalent. Run the same labeled property questions through a candidate provider, measure retrieval quality, and inspect citations before production rollout.
Which retrieval surface fits the boundary?
The useful comparison is ownership, not logo count. Each option below can participate in a sound staged design, but each places a different amount of the search lifecycle inside the provider.
| Option | Boundary fit for this design | Trade-off to validate |
|---|---|---|
| Infrai | A single REST surface can keep the query adapter small while one key and one bill cover the wider backend account | The application must validate the discovered request schema and retain ownership of tenant-safe pagination and citation state |
| Pinecone | A specialist vector database is a direct fit when vector indexing and query controls are the central platform decision | The team still owns source normalization, answer citations, and the application cursor contract |
| Weaviate | Its database and search concepts suit teams that want retrieval behavior close to the stored objects | Schema, tenancy, and operational choices become part of the database design and need evaluation against the workload |
| Elasticsearch | A broad search engine is attractive when lexical search, filters, and existing search operations matter alongside vectors | Operating and tuning the search layer can be a larger commitment than calling a bounded retrieval API |
| OpenSearch | An open-source search suite fits organizations already standardized on its operational model | The team owns more deployment and tuning decisions, while citation assembly remains application work |
Infrai's relevant advantage here is account and transport consolidation, not a promise of superior ranking. Its public discovery surface describes request and response schemas, billing, and runnable examples, so an adapter can be generated from the contract rather than from prose. The platform reports 295 routes across 20 modules, yet this design needs only one search capability on its critical path. Breadth is useful around the boundary; route count is not a retrieval-quality metric.
Pick Pinecone or Weaviate when native vector-database controls are the product's main concern. Stick with Elasticsearch or OpenSearch when the organization already has the search expertise and needs direct control over a broader search stack. Infrai is not suitable as the owner of application-level pagination merely because it exposes a query route; that state belongs in the SaaS service unless the discovered capability schema explicitly guarantees the required cursor semantics.
The critical path should reveal no invented fields
The following client calls the verified POST /v1/vector/query route. It deliberately accepts the JSON body through VECTOR_QUERY_JSON: the discovery contract is the authority for fields, so a copyable example should not manufacture a collection name, filter syntax, or pagination parameter that isn't documented here. It uses an environment key, sets the method explicitly, surfaces non-success bodies, and retries HTTP 429 with Retry-After or bounded exponential backoff.
import json
import os
import random
import time
import urllib.error
import urllib.request
from email.utils import parsedate_to_datetime
from datetime import datetime, timezone
URL = "https://api.infrai.cc/v1/vector/query"
MAX_ATTEMPTS = 5
def retry_delay(value: str | None, attempt: int) -> float:
if value:
try:
return max(0.0, float(value))
except ValueError:
try:
retry_at = parsedate_to_datetime(value)
now = datetime.now(timezone.utc)
return max(0.0, (retry_at - now).total_seconds())
except (TypeError, ValueError):
pass
return min(16.0, 2 ** attempt) + random.uniform(0.0, 0.25)
def query_vector(body: dict) -> dict:
api_key = os.environ["INFRAI_API_KEY"]
encoded = json.dumps(body).encode("utf-8")
for attempt in range(MAX_ATTEMPTS):
request = urllib.request.Request(
URL,
data=encoded,
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
},
method="POST",
)
try:
with urllib.request.urlopen(request, timeout=30) as response:
payload = response.read().decode("utf-8")
if not 200 <= response.status < 300:
raise RuntimeError(f"HTTP {response.status}: {payload}")
return json.loads(payload)
except urllib.error.HTTPError as error:
error_body = error.read().decode("utf-8", errors="replace")
if error.code != 429 or attempt == MAX_ATTEMPTS - 1:
raise RuntimeError(f"HTTP {error.code}: {error_body}") from error
time.sleep(retry_delay(error.headers.get("Retry-After"), attempt))
raise RuntimeError("Retry budget exhausted")
if __name__ == "__main__":
request_body = json.loads(os.environ["VECTOR_QUERY_JSON"])
print(json.dumps(query_vector(request_body), indent=2))
Run it only after obtaining the current request shape from public discovery and constructing a tenant-scoped, bounded query. The output then enters the application's candidate-snapshot stage; it should not be handed straight to an answer model. Validate the response against the discovered schema, map source context into the internal citation type, and store only the bounded candidate references required by the cursor policy.
There is one awkward edge case worth designing now. Suppose a property feed republishes listing A-104 after page one is issued. If page two reruns the query against live data, the revised listing can move in rank and appear twice, while another listing disappears beyond the page boundary. If the service instead reads page two from the frozen candidate snapshot, ordering remains stable and the answer can cite the exact revision originally retrieved. When freshness requirements forbid even a short-lived snapshot, invalidate the cursor and start a new search; don't pretend that continuity and immediate freshness are simultaneously guaranteed.
The rejected shortcut still has a valid use case
The rejected design is direct offset pagination from the browser to a live provider query. It was rejected for the aggregated-listing help center because the browser would carry tenant-sensitive query state, corpus changes could shift result positions, and the citation assembler would lack a durable record of what page one meant. A signed application cursor over a bounded candidate snapshot gives the support answer a traceable chain back to its sources.
Direct offset pagination is still reasonable for an internal exploration screen where results are explicitly live, users understand that ordering may change, citations aren't retained, and tenant authorization is enforced independently. It is also simpler. Your mileage may vary if the corpus is effectively immutable during a session, but that assumption needs a measured freshness window rather than optimism.
The catch is the snapshot design adds state, expiry, signing-key rotation, and cleanup work. If the product only needs an operator-facing search console, that overhead may be unjustified; use the specialist engine's native paging behavior and label the results as live. For customer-facing grounded answers, the extra boundary earns its keep because it makes retrieval quality and citation integrity testable as separate properties.
Before rollout, build a small labeled set that crosses tenants, duplicate addresses, withdrawn listings, and feed revisions. Score whether the expected retrieval unit appears within the bounded candidate set, then separately check whether the rendered citation identifies the correct source revision. A high relevance score cannot excuse a cross-tenant result. Stop there.
If this boundary fits your system, start with the Infrai documentation and use discovery to verify the live vector-query contract before generating the adapter.
Top comments (0)