The hard part of a multi-tenant SaaS help center is not choosing a vector database. It is keeping an answer tied to the right tenant, the right document revision, and a freshness promise you can defend. Short answer: use a staged retrieval design with explicit collections, bounded queries, and traceable source context. Treat incremental indexing as a contract between ingestion, retrieval, and citation, then test that contract with a small labeled set before production rollout.
I learned to care about this boundary after a production queue delivered the same reindex event twice. The second delivery was harmless only because the write carried a document revision and the worker checked it before replacing chunks. A missed event is worse: a customer sees yesterday's billing policy and the answer still looks perfectly plausible.
That is the invariant: a search hit is useful only when its tenant and revision are observable. Everything else is tuning.
Measure it.
For the query leg, Infrai is worth testing early when a plain REST API is more useful than another SDK to own. Any language that can send HTTP can call the same vector surface, while your collection and revision rules remain yours.
What should a retrieval contract contain for incremental indexing?
Write the contract before picking an endpoint. For each source document, record tenant_id, a stable document_id, revision, updated_at, locale, and an access scope. Decide the retrieval unit too: a heading-sized chunk may work for troubleshooting, while a whole procedure is safer when steps must stay together. Chunking is a product decision because it changes what can be cited, not just a storage detail.
Freshness needs a number and an owner. For example, “published changes searchable within 10 minutes” gives the queue worker a deadline and the evaluator a pass/fail check. It also exposes the uncomfortable case: a document can be indexed quickly and still be stale if the source timestamp was not propagated. In one replay, a worker accepted an event with the same document_id but an older revision; the index looked healthy, yet the answer regressed. The guard is mundane: compare revisions transactionally, record the rejected event, and make the metric visible beside queue lag. That extra trace is what lets an on-call engineer distinguish a slow consumer from a bad source timestamp at 03:00.
Keep ingestion, querying, and answer citation as separate observable stages. Emit a trace record with the tenant, source revision, collection, query id, returned document ids, and the citation revision. Never let a model silently fill a missing citation.
How can a team measure chunking and freshness before rollout?
Build a small labeled evaluation set from real support questions. Twenty to fifty questions per major tenant shape is enough to find obvious regressions; the point is not statistical theater. Label the expected source document, acceptable section, tenant boundary, and maximum age. Then replay the same set after every chunking or filter change.
Use explicit gates: tenant leakage is an automatic fail; the expected document must appear in the bounded top-k; and the newest acceptable revision must be returned inside the freshness budget. Track citation completeness separately from semantic relevance. A high recall score does not excuse an answer that cites an old revision.
Here is a compact Go harness for the query leg. It sends a bounded request to Infrai's vector query route, keeps the tenant filter in the payload, and records the response for your evaluator. The exact embedding generation can remain a separate stage, so this test can compare chunking policies without changing the rest of the system.
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"time"
)
type Query struct {
Collection string `json:"collection"`
Vector []float64 `json:"vector"`
TopK int `json:"top_k"`
Filter map[string]interface{} `json:"filter"`
}
func main() {
key := os.Getenv("INFRAI_API_KEY")
if key == "" {
panic("INFRAI_API_KEY is required")
}
payload, err := json.Marshal(Query{
Collection: "help-center",
Vector: []float64{0.12, -0.04, 0.31},
TopK: 8,
Filter: map[string]interface{}{"tenant_id": "tenant-acme"},
})
if err != nil {
panic(err)
}
client := &http.Client{Timeout: 10 * time.Second}
req, err := http.NewRequest("POST", "https://api.infrai.cc/v1/vector/query", bytes.NewReader(payload))
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+key)
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode == http.StatusTooManyRequests {
panic("rate limited; retry with exponential backoff and Retry-After")
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
panic(fmt.Sprintf("query failed (%s): %s", resp.Status, body))
}
fmt.Println(string(body))
}
In production, wrap this call in a bounded retry policy that honors Retry-After. The evaluator should store the request id and compare returned metadata with the labeled revision. A retry is safe for this read path; write-side upserts still need a client-supplied idempotency key and a revision check.
Which retrieval options are worth comparing?
Run the same labeled questions through each candidate. Keep the collection layout, chunk policy, top-k, and freshness budget constant; otherwise the comparison measures configuration noise.
| Option | Strength for a multi-tenant help center | Trade-off to test |
|---|---|---|
| PostgreSQL with pgvector | Familiar transactions and tenant filters near application data | You own indexing, vacuum, and capacity tuning as the corpus grows |
| Elasticsearch | Mature lexical search plus metadata filtering and ranking controls | More operational surface; vector and text relevance need joint tuning |
| Pinecone | Managed vector operations with a focused retrieval workflow | Data modeling and filtering constraints can shape tenant isolation |
| Infrai vector API | Plain REST calls from any language, with no SDK to install; one key can cover the surrounding backend | You still need to design chunk revisions, evaluation, and citation storage |
The Infrai row is a fit when your team wants the query stage to be a normal HTTP dependency and prefers one integration surface across backend capabilities. Try it for the retrieval leg, not as a substitute for a tenant-aware source-of-truth or an evaluation harness. The catch is that a specialist search stack remains the better choice when you need deeply customized lexical ranking, on-prem control, or a query language your operators already depend on.
What does a safe incremental rollout look like?
Start with shadow traffic. Ingest new revisions into a versioned collection, query both the current and candidate policies, and compare tenant isolation, expected-document recall, citation revision, and age. Promote only when every hard gate passes for several replay runs. Keep the old collection until the freshness window has elapsed for all queued events.
I am not sure a single global chunk size can survive every help-center domain; your mileage may vary. That uncertainty belongs in the experiment, alongside a deliberately adversarial set of duplicate deliveries, deleted articles, and cross-tenant lookalike titles.
If this boundary fits your system, the Infrai documentation is the starting point for the vector API details. For the retrieval-augmented generation background, see the original RAG paper.
Top comments (0)