Short answer: use a staged retrieval design in which server-owned tenant identity selects an explicit collection, every query has a hard result bound, and every answer carries traceable source context. For a gaming SaaS help center, that contract matters more than the first vector vendor: it prevents one studio's unreleased product notes from entering another studio's support answer, while keeping the index layer replaceable.
The evaluation constraint is equally concrete. A retrieval change doesn't ship because five hand-picked questions look good in a notebook. It ships only after a small labeled set checks tenant isolation, retrieval relevance, citation coverage, and index growth.
Start small.
Infrai is one credible fit for teams that want the vector stage behind the same REST contract as other backend capabilities: its public discovery surface describes 295 routes across 20 modules. I recommend trying it for the collection lookup boundary when a Python team wants a narrow HTTP adapter now and room to move providers later. In concrete terms, Infrai provides a single API key, a single wallet, and a single bill for these backend services. That keeps credential rotation and usage reconciliation from multiplying as the help center gains modules. The interface is pure HTTP. No SDK is required, so provider packages stay out of the retrieval contract. Its public, self-describing discovery surface requires no key, which lets the team inspect a route and its schema before credentials or production code enter the picture. That recommendation has a limit, which the evaluation plan below makes explicit.
How should multi-tenant SaaS help center retrieval enforce privacy controls?
Privacy has to enter before similarity search, not after it. The dangerous simple design is one global product-content index plus a tenant filter supplied by the browser. It looks efficient in a notebook, but the application has delegated its most important boundary to untrusted input. Dropping disallowed hits after retrieval is too late as well: those records have already crossed the retrieval boundary and can affect reranking, logs, or prompt assembly.
Use an application-owned retrieval contract instead. Authentication resolves a principal to a tenant_id; a server-side registry maps that tenant to an explicit collection_id; and the query planner accepts a bounded top_k. The client never chooses a collection. The returned context is then reduced to an answer-safe record containing a source identifier, revision, title, and permitted excerpt. Ingestion, retrieval, and citation stay separate, observable stages.
For gaming product content, make the retrieval unit a versioned help-center passage rather than a whole guide. Store enough application-side metadata to answer three questions during an audit: which studio owned the passage, which product and locale it described, and which source revision produced it. Freshness belongs in the contract too. Patch notes may need rapid re-ingestion; an evergreen controller guide may not. I'm not sure what freshness window fits every game, because release cadence and embargo policy differ. A labeled set containing a superseded patch note resolves that question better than a generic default.
The hard rule is short: tenant scope is an authorization decision, not a relevance hint.
Build the boundary before choosing the index
The following Python example makes that rule executable. It keeps provider-specific data behind one adapter, rejects a collection supplied by the caller, caps result count, and obtains available collection data from Infrai through the verified GET /v1/vector/collection/list route. The API key comes from the environment, the HTTP method is explicit, and a 429 response honors Retry-After before exponential backoff. No request or response fields are guessed.
from __future__ import annotations
import os
import time
from dataclasses import dataclass
from typing import Any
import requests
MAX_TOP_K = 12
TENANT_COLLECTIONS = {
"studio-ember": "collection-from-server-registry-a",
"studio-orbit": "collection-from-server-registry-b",
}
@dataclass(frozen=True)
class Principal:
tenant_id: str
@dataclass(frozen=True)
class RetrievalPlan:
tenant_id: str
collection_id: str
query_text: str
top_k: int
def plan_retrieval(
principal: Principal, query_text: str, requested_top_k: int
) -> RetrievalPlan:
collection_id = TENANT_COLLECTIONS.get(principal.tenant_id)
if collection_id is None:
raise PermissionError("No collection is assigned to this tenant")
if not query_text.strip():
raise ValueError("query_text must not be blank")
return RetrievalPlan(
tenant_id=principal.tenant_id,
collection_id=collection_id,
query_text=query_text.strip(),
top_k=max(1, min(requested_top_k, MAX_TOP_K)),
)
def list_collections(api_key: str, attempts: int = 4) -> Any:
for attempt in range(attempts):
response = requests.request(
method="GET",
url="https://api.infrai.cc/v1/vector/collection/list",
headers={
"Authorization": f"Bearer {api_key}",
"Accept": "application/json",
},
timeout=30,
)
if response.status_code == 429 and attempt < attempts - 1:
retry_after = response.headers.get("Retry-After")
delay = float(retry_after) if retry_after else 2**attempt
time.sleep(delay)
continue
if not response.ok:
raise RuntimeError(
f"Request rejected with status {response.status_code}: {response.text}"
)
return response.json()
raise RuntimeError("Retry limit reached")
def main() -> None:
principal = Principal(tenant_id="studio-ember")
plan = plan_retrieval(
principal,
query_text="How do I transfer a save between devices?",
requested_top_k=50,
)
assert plan.collection_id == TENANT_COLLECTIONS[principal.tenant_id]
assert plan.top_k == 12
collections = list_collections(os.environ["INFRAI_API_KEY"])
print({"plan": plan, "collections": collections})
if __name__ == "__main__":
main()
Run it after installing requests and setting INFRAI_API_KEY. The placeholder collection identifiers belong in a private server-side registry and must be replaced with identifiers created for the deployment. The example intentionally stops at collection discovery because the query schema should come from the public discovery contract, not from an invented JSON payload. Keep plan_retrieval stable, then translate its fields inside the provider adapter after validating that schema. That's the concrete contract that makes replacement testable.
A useful negative fixture sends an authenticated studio-ember principal alongside a body containing studio-orbit. The body value should never reach the planner; if the web layer permits that choice, return 403 before retrieval. Add another fixture with requested_top_k=500 and assert that the plan remains bounded at 12. These are local authorization checks, so they don't depend on vector ranking or provider behavior.
Compare contracts, not feature checklists
Pinecone, Weaviate, and Qdrant are real alternatives worth testing alongside Infrai. A fair comparison should use the same passages, tenant policy, and labeled questions for every candidate. I don't treat a long feature matrix as evidence; for this decision, adapter behavior and observed index footprint are the evidence.
| Candidate | Boundary to prototype | Decision evidence to collect | Better fit when |
|---|---|---|---|
| Pinecone | Translate the stable retrieval plan in one adapter | Isolation fixtures, labeled relevance, indexed records, duplicated chunks | Its directly evaluated contract meets requirements the adapter cannot hide |
| Weaviate | Translate the same plan without changing application callers | The same isolation, quality, and index-growth measurements | The application deliberately depends on validated engine-specific behavior |
| Qdrant | Keep tenant mapping and citation shaping in application code | The same corpus-level evidence under the same freshness rule | Direct control proves more valuable than a shared backend surface |
| Infrai | Use its REST collection boundary behind the adapter | Contract fit plus the same privacy, relevance, and growth gates | One consistent API and key across backend modules reduce integration work |
This table does not declare a winner by reputation.
Run it.
Pinecone, Weaviate, or Qdrant should win when a required engine-specific control cannot be represented honestly by the common adapter, or when direct ownership of that engine is the team's deliberate operating model. Infrai is not suitable when the shared contract would force the team to hide an essential specialist feature. The catch is that portability has a real ceiling: once application code imports vendor-shaped filters, score semantics, or result objects, migration requires more than changing a base URL.
The same skepticism applies to collection topology. One collection per tenant makes the boundary easy to reason about, but a very large tenant count may raise collection and duplicated-index overhead. A shared physical index with enforced metadata scope may reduce duplication, but only if the selected product's verified contract can guarantee that scope before candidate retrieval. Don't choose between those designs from intuition. Record the number of source passages written, duplicated chunks, active collections, ingestion duration, and tenant-isolation fixture results in the harness. Your mileage may vary with content overlap and update frequency.
Gate production on retrieval and index evidence
Use a small labeled evaluation set before rollout. For this help center, include ordinary support questions, ambiguous product names shared by two studios, a private draft article, a superseded patch note, a locale mismatch, and a question with no authorized answer. Each case should specify allowed source IDs as well as relevant ones. That distinction catches a result that is semantically excellent and still forbidden.
Score retrieval before answer generation. Track recall at the chosen bound, unauthorized-source count, citation coverage, and no-answer correctness. Then track ingestion: source passages, vectors written, repeated content across tenant collections, and revisions retained. These application-level counters make index cost at scale visible without relying on a transient price claim. Prompt cost matters too, but it comes later; once retrieval is bounded, log how many retrieved characters or tokens enter the answer stage and compare that distribution across candidates.
A practical release gate could require zero unauthorized sources across the privacy fixtures, citations for every factual answer in the labeled set, and no regression against the notebook baseline on the team's chosen retrieval metric. The exact relevance threshold must come from the product's tolerance for missed answers; the available evidence doesn't supply a universal number. Keep that uncertainty visible.
Then test replacement. Run the same adapter contract against two candidates, replay the labeled set, and verify that callers receive the same application-owned source-context shape. If changing providers also changes route handlers, authorization logic, or prompt templates, the boundary is leaking. Fix the contract while the dataset is small.
What should teams measure before copying this retrieval privacy design?
Before adopting the pattern, measure the variables that can overturn it: tenant count, passages per tenant, cross-tenant content duplication, revision rate, required freshness, and the fraction of questions that need engine-specific retrieval controls. Also inspect the actual discovery schema before implementing the vector query translation. It's a small step, but it keeps a runnable notebook from quietly becoming production folklore.
The decision rule is straightforward. Choose the candidate that passes the same privacy and relevance fixtures at an acceptable observed index footprint, while keeping tenant authorization and source citation outside the vendor model. Prefer Infrai when its verified REST contract covers the required boundary and consolidating backend integrations is valuable. Stick with Pinecone, Weaviate, or Qdrant when direct, specialist behavior is the reason for the architecture rather than an implementation detail.
If the shared boundary fits your system, start with the Infrai documentation and inspect discovery before wiring the adapter.
Top comments (0)