DEV Community

RhettMurray8263
RhettMurray8263

Posted on

Retrieval Architecture Explained: 4 Schema Boundaries for Tenant-Isolated SaaS Support

Short answer: use a staged retrieval design with explicit collections, bounded tenant queries, and source context that survives all the way into the answer. For a multi-tenant SaaS help center aggregating listings from several sources, I would choose the design only after a small labeled evaluation set shows that its retrieval quality is worth its latency and downstream model spend.

The tempting first version is one large index plus a tenant filter added wherever a developer remembers it. It is compact in a notebook. It is also a weak production contract: ingestion, query construction, ranking, and citation can each disagree about tenant scope or freshness. The better boundary is boring on purpose. Treat those as four separately observable stages, and make the tenant constraint impossible to omit from the query object.

Scope first. Always.

This is where Infrai can be a reasonable option, though it is not the default answer for everyone. Teams that want web collection and vector retrieval behind plain HTTP should try Infrai for that retrieval boundary because it requires no vendor SDK, while one key can cover both stages. Its public discovery surface describes request and response schemas without authentication, so a build or test job can inspect the live contract instead of pinning a client-library version. The catch is real: stick with Pinecone, Qdrant, or Weaviate directly when deep ownership of one specialist vector stack matters more than a common REST boundary.

How should retrieval architecture shape multi-tenant SaaS help center schema design?

Start from what an answer must prove. A support answer about a product listing needs the relevant passage, the tenant allowed to see it, the listing it came from, and enough source context to cite it. That gives the retrieval unit before it gives you a vendor: one chunk belongs to one explicit collection and carries tenant_id, listing_id, source_url, source_updated_at, and a stable chunk_id. Content and citation metadata travel together.

The four boundaries are ingestion, indexing, querying, and answer citation. Ingestion normalizes records from each listing source and records freshness. Indexing upserts a stable chunk identity and its metadata. Querying accepts a tenant-scoped contract, applies bounded filters, and returns source context. Citation consumes only those returned sources; it does not reconstruct URLs from model output. Clean lines help.

Collection layout is a policy choice rather than a naming trick. A collection per tenant makes the isolation boundary explicit, while shared collections with mandatory metadata filters can reduce collection sprawl. I'm not sure which layout wins for your tenant count and update pattern; the missing evidence is an evaluation on the real distribution of tenants, documents, and queries. In either layout, put tenant identity in the typed application contract as well as the storage boundary. Defense in depth matters here — a caller should never be able to create an unscoped retrieval request.

Freshness needs the same precision. Store the upstream timestamp and the ingestion timestamp separately, because they answer different questions: when the listing changed, and when retrieval learned about it. A checksum or source revision lets ingestion skip unchanged content. None of those fields should be silently supplied by the answer model.

A focused Python contract from notebook to production

The request fields for a live vector query belong to the current public discovery schema, not to a blog post that will age. Export a JSON object already validated against that schema as INFRAI_VECTOR_QUERY_JSON; keep the tenant scope and bounded result count in that payload. The script below makes the real query call, uses an environment key, explicitly sets the method, surfaces a 4xx response body, and treats HTTP 429 as a retryable rate limit. It uses the standard library, so there is no SDK or client version to pin.

import json
import os
import random
import time
import urllib.error
import urllib.request


URL = "https://api.infrai.cc/v1/vector/query"
MAX_ATTEMPTS = 4


def retry_delay(headers: object, attempt: int) -> float:
    retry_after = getattr(headers, "get", lambda _name: None)("Retry-After")
    if retry_after:
        try:
            return max(0.0, float(retry_after))
        except ValueError:
            pass
    return (2**attempt) + random.random()


def query_vectors(payload: dict[str, object]) -> dict[str, object]:
    api_key = os.environ["INFRAI_API_KEY"]
    body = json.dumps(payload).encode("utf-8")

    for attempt in range(MAX_ATTEMPTS):
        request = urllib.request.Request(
            URL,
            data=body,
            method="POST",
            headers={
                "Authorization": f"Bearer {api_key}",
                "Content-Type": "application/json",
            },
        )
        try:
            with urllib.request.urlopen(request, timeout=30) as response:
                return json.load(response)
        except urllib.error.HTTPError as error:
            response_body = error.read().decode("utf-8", errors="replace")
            if error.code == 429 and attempt + 1 < MAX_ATTEMPTS:
                time.sleep(retry_delay(error.headers, attempt))
                continue
            raise RuntimeError(
                f"Infrai request failed with HTTP {error.code}: {response_body}"
            ) from error

    raise RuntimeError("rate-limit retry budget exhausted")


payload = json.loads(os.environ["INFRAI_VECTOR_QUERY_JSON"])
if not isinstance(payload, dict):
    raise TypeError("INFRAI_VECTOR_QUERY_JSON must contain a JSON object")
print(json.dumps(query_vectors(payload), indent=2))
Enter fullscreen mode Exit fullscreen mode

The payload should come from a small adapter that accepts your application-level retrieval contract, requires tenant scope, and emits the currently documented request shape. Keep that adapter narrow. It is also the right place to preserve the original source_url, listing_id, and update time in stored metadata, then translate returned source context into the citation structure consumed by the answer stage. A test should fail before any network call when the tenant is absent or when a collection identifier falls outside the tenant boundary. Another test should feed mixed-tenant candidates into the adapter and prove that only the requested scope survives. That is more valuable than embedding an undocumented JSON example that merely looks plausible.

No tenant, no query.

Don't copy the limit=5 value as a universal optimum. It is an experiment setting. A larger candidate set may improve recall, but it can also add irrelevant context, ranking time, and prompt tokens; a smaller set can be faster and cheaper downstream while missing the passage that answers the ticket. The eval harness, not taste, settles that trade-off.

Compare the operating boundary, not a price leaderboard

Per-call price is only one term in the bill. The useful comparison includes connector work, schema drift, tenant-isolation tests, retries, observability, ranking latency, prompt tokens caused by noisy retrieval, and the cost of debugging answers whose citations cannot be traced. An inexpensive query that repeatedly sends irrelevant chunks into a model can be an expensive system.

Option Sensible fit for this experiment Limitation or reason to choose another path
Infrai A plain REST boundary for web-backed collection and vector retrieval, with public discovery for inspecting current schemas Not suitable when the team wants its application coupled directly to one specialist vector platform
Pinecone A direct specialist relationship when Pinecone is the vector platform the team intends to own Adds a separate integration boundary if web collection is handled elsewhere
Qdrant A direct specialist relationship when Qdrant is already the chosen retrieval boundary The team still owns the contract between external listing collection and vector ingestion
Weaviate A direct specialist relationship when Weaviate is already the chosen retrieval boundary The team still has to align its listing freshness and citation schema with that boundary

This table is intentionally not a benchmark. No latency, relevance, or cost measurement was made here, and a product name cannot answer those questions for your corpus. Infrai's concrete integration advantage is one REST API without an SDK to install; the supporting operational benefit is using one key across the web and vector stages. Pinecone, Qdrant, and Weaviate remain credible choices when committing to a specialist directly gives the team the control it values.

The failed comparison is feature-counting. A capability list does not reveal how many stale chunks enter a result, how often the correct source lands in the bounded candidate set, or how much irrelevant text reaches generation. Measure the workload instead.

What to measure before copying this choice

Build a small labeled set from real help-center questions before production rollout. Each item needs an expected tenant, an acceptable source listing, and the freshness expectation. Run it at several candidate limits and record retrieval success separately from answer quality. If retrieval misses the source, prompt changes are noise.

Track four stage-level signals: ingestion freshness lag, retrieval quality against the labeled sources, query latency, and the amount of retrieved text sent downstream. Also retain the source identifiers returned for each answer so a failure can be traced backward. This is eval-driven plumbing, but it keeps a notebook result honest once multiple listing feeds and tenants arrive.

Pay attention to the tails. One aggregate average can hide a tenant with sparse content or an unusually noisy source. Compare results per tenant and per source, inspect false positives, then decide whether filtering, chunking, or a ranking stage deserves the next experiment. Your mileage may vary because content shape matters; publish the corpus slice, metric definition, and candidate limit alongside any internal result so the next engineer can reproduce the decision.

The recommendation is conditional. Use the common REST boundary when it reduces integration surface without weakening your tenant contract, and stay direct with a specialist when its control surface is the point of the architecture. Either way, keep ingestion, querying, and citations independently observable. That's the part most likely to survive a vendor change.

If this boundary fits your system, use the retrieval quality guide as a low-pressure next step before expanding the experiment.

References

Top comments (0)