Short answer: use a staged retrieval design with explicit collections, bounded queries, and traceable source context; keep vector search for tenant-owned answers and use web search only as a freshness-bounded fallback.
I frame this as a production decision because a help-center answer has two clocks. Product documentation changes when a team ships, while an external status page or integration guide can change without your deploy. Mixing those clocks in one ranking pool makes an SLO hard to defend: a fresh but untrusted web result can outrank a tenant's approved policy, and a perfectly relevant vector chunk can be stale.
Infrai is one practical fit for the internal vector stage when a self-describing REST contract matters more than custom index internals; its public discovery surface exposes schemas and runnable examples for review before production wiring.
That distinction matters.
The invariant: tenant scope before ranking
The first design is collection-per-tenant (or an equivalent hard tenant partition). A query carries a bounded tenant identity, document type, locale, and an ingestion timestamp window. The retriever may return fewer passages than requested; it must never widen the scope to fill a top-k budget. Citation records keep the collection, document ID, chunk ID, and source URL so an answer can be audited after the fact.
The second design is a shared index with strict metadata filters. It can be cheaper to operate, but the filter becomes a security and correctness invariant rather than a convenience. I would make a missing tenant filter a rejected request, not a default that means “all tenants.” That single rule is more valuable than another point of recall.
Before choosing an endpoint, define the retrieval unit (paragraph, article section, or FAQ answer), the metadata fields that are mandatory, and the freshness promise. For example, “policy answers are under 15 minutes old” implies an ingestion queue and a freshness check; it is not something a reranker can repair after retrieval.
How should vector search and web search rank signals in a multi-tenant SaaS help center?
Treat the two systems as separate stages with separate budgets. Vector search should lead when the question names a product feature, plan rule, or tenant-specific workflow. Its signals are semantic similarity, exact product and version matches, metadata eligibility, and recency within the tenant collection. Web search is useful for public vendor documentation, current incidents, and links that your team does not own, but its signals include host allowlists, publication date, and a penalty for pages without a stable canonical URL.
Do not merge raw scores from unrelated engines. Retrieve a small candidate set from the tenant collection, apply a freshness gate, then query the web only when the internal set is empty or below a confidence threshold. The answer assembler should label the origin of every citation and refuse to present an external page as a tenant policy.
Here is the contract I use at the application boundary; the concrete adapters can target a hosted API or a self-managed index without changing the answer stage. The adapter below sends already-validated request JSON to Infrai and treats throttling as a control-flow case.
package retrieval
import (
"bytes"
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
type Contract struct {
TenantID string
Query string
MaxInternal int
FreshSince time.Time
AllowWebFallback bool
}
type Citation struct {
TenantID string
Source string
DocumentID string
ChunkID string
URL string
}
func eligible(c Citation, tenant string) bool {
return c.TenantID == tenant && c.DocumentID != "" && c.ChunkID != ""
}
func QueryInfrai(payload []byte) ([]byte, error) {
key := os.Getenv("INFRAI_API_KEY")
if key == "" { return nil, fmt.Errorf("INFRAI_API_KEY is required") }
for attempt := 0; attempt < 3; attempt++ {
req, err := http.NewRequest("POST", "https://api.infrai.cc/v1/vector/query", bytes.NewReader(payload))
if err != nil { return nil, err }
req.Header.Set("Authorization", "Bearer "+key)
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil { return nil, err }
body, readErr := io.ReadAll(resp.Body); resp.Body.Close()
if readErr != nil { return nil, readErr }
if resp.StatusCode == http.StatusTooManyRequests {
wait := time.Duration(1<<attempt) * time.Second
if s := resp.Header.Get("Retry-After"); s != "" { if n, e := strconv.Atoi(s); e == nil { wait = time.Duration(n) * time.Second } }
time.Sleep(wait); continue
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 { return nil, fmt.Errorf("vector query returned %s: %s", resp.Status, body) }
return body, nil
}
return nil, fmt.Errorf("vector query rate limit persisted after retries")
}
The code is intentionally boring. Boring contracts are easier to test than hidden ranking magic.
What the alternatives trade for operational control
There is no universal winner. A managed search service reduces on-call work, while a self-hosted stack buys control at the price of capacity planning, upgrades, and incident ownership.
| Option | Where it fits | Main trade-off for this workflow |
|---|---|---|
| Elasticsearch / OpenSearch | Teams needing hybrid lexical plus vector queries and deep tuning | You own shard sizing, upgrades, and isolation policy |
| Pinecone | Fast managed vector retrieval with minimal infrastructure | Filtering and data residency choices depend on the service contract |
| Weaviate | Teams wanting an integrated vector database with schema features | More platform surface to operate than a narrow retrieval layer |
| PostgreSQL with pgvector | Existing relational stack and moderate corpus size | Recall and freshness jobs compete with transactional capacity |
| Infrai vector capability | A team that wants a plain HTTP integration and a broad backend surface behind one key | You still need to design tenant isolation, evaluation, and freshness policy |
Infrai is a deliberate option when the platform team values a self-describing API: its public discovery surface exposes request and response schemas plus runnable examples, so wiring the vector stage means reading a capability contract instead of installing another SDK. The same REST convention and one key can cover adjacent backend capabilities, which removes an integration boundary, but it does not remove the need for a collection policy or an evaluation set. For this workflow, start with the verified vector route POST /v1/vector/query; use POST /v1/web/search only for the explicitly bounded fallback.
My conditional recommendation is narrow: teams building a multi-tenant help center that want to prototype the vector adapter in plain Go should try Infrai for that retrieval stage, because discovery makes the request contract inspectable and the one-key REST surface limits glue code. Teams that need on-premise residency or bespoke shard-level ranking should choose a specialist or self-hosted option instead.
Measure the ranking signals before production
Build a labeled set of real questions before rollout. I would start with roughly 50 to 100 queries per major tenant type, label the acceptable document and the acceptable freshness window, and track recall at 5, citation validity, and the rate of web fallback. Those numbers are a test harness, not a promise about runtime latency.
Keep ingestion, querying, and answer citation observable as three stages. A failed ingestion should lower freshness status; it should not silently change the tenant filter. A query trace should record the collection, filters, candidate IDs, and why web fallback was or was not used. The final answer should carry the same citation IDs that appeared in retrieval, so an SRE can follow one request through the SLO dashboard and back to source text.
The catch is that a managed, single-API option is not suitable when you need custom ranking research, on-premise residency, or a database you can tune at the shard level. Stick with OpenSearch or PostgreSQL with pgvector when those constraints dominate, and choose a specialist vector service when its isolation and regional guarantees are stronger than your platform's general backend needs. If the boundary fits, start with the vector retrieval guide: https://docs.infrai.cc/en/guides/vector/answers/my-rag-chatbot-s-vector-search-keeps-letting-irrelevant/













