Short answer: for retrieval architecture in a multi-tenant SaaS help center, use staged retrieval with explicit collections, bounded queries, and source context that survives pagination. Pick the backend whose filtering and citation behavior you can measure, then keep ingestion, retrieval, and answer generation as separate SLO-bearing stages.
That decision rule matters in a B2B help center because a page of results is not a cosmetic UI detail. Tenant isolation, ranking, freshness, and citation all have to remain true on page two, after a document has been reindexed, and when two customers ask the same question with different entitlements.
What should a multi-tenant help center retrieve?
Start with the retrieval contract, not an endpoint. Define one retrieval unit (for example, a paragraph with its article ID), the tenant identifier, product and locale filters, a freshness target, and the citation fields returned with every hit. A useful hit has a stable source ID, a bounded text span, and a cursor or page token that can be logged. Offset pagination is easy to explain, but a cursor tied to a stable sort avoids duplicates when writes happen between requests. Write this contract down next to the schema and test it with fixtures that include two tenants, two locales, deleted articles, and a reindexed paragraph. That fixture becomes the boundary between search engineering and answer engineering: if a source ID disappears, the citation stage should fail loudly instead of quietly inventing provenance, and if a filter is missing, the test should show the cross-tenant hit before production does.
I keep a small labeled set before rollout: real questions, expected source articles, and an acceptable rank range. Measure recall at k, citation hit rate, and stale-result rate separately. I once treated a 20-question smoke test as representative; it missed a tenant filter that only failed on the second page. That was a design error, not a ranking mystery.
How do pagination and source citations fit the retrieval architecture?
Use two bounded stages. The first stage applies tenant and metadata filters and asks for a modest candidate window. The second stage reranks those candidates and carries their source IDs into the answer prompt. Never let a UI page size become the model's candidate limit: a user requesting 10 rows may still need 40 candidates to produce three well-grounded citations.
Here is a minimal Go probe against a vector query. It keeps the request explicit, reads the key from the environment, and backs off on rate limiting. The exact query fields belong in the collection schema you discover for your deployment; the example shows the pagination boundary as an application-level cursor rather than inventing an undeclared server parameter.
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strconv"
"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")
payload, _ := json.Marshal(Query{
Collection: "help-center",
Vector: []float64{0.12, -0.04, 0.88},
TopK: 40,
Filter: map[string]interface{}{"tenant_id": "tenant-42", "locale": "en-US"},
})
baseURL := os.Getenv("INFRAI_BASE_URL")
for attempt := 0; attempt < 4; attempt++ {
req, err := http.NewRequest("POST", baseURL+"/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 := http.DefaultClient.Do(req)
if err != nil { panic(err) }
body, _ := io.ReadAll(resp.Body)
resp.Body.Close()
if resp.StatusCode == http.StatusTooManyRequests {
delay := time.Duration(1<<attempt) * time.Second
if v, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil { delay = time.Duration(v) * time.Second }
time.Sleep(delay)
continue
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 { panic(fmt.Sprintf("query failed: %s", body)) }
fmt.Println(string(body))
return
}
panic("rate limit persisted after retries")
}
The cursor belongs in your own result envelope, alongside the candidate IDs and the evaluation trace. On the next page, rerank only the next bounded slice, then deduplicate by source ID before composing citations. A short-lived cache can reduce repeated embedding work, but it must be keyed by tenant, collection version, and filter set.
Which backend is a fair fit for this workload?
There is no universal winner. Elasticsearch gives deep boolean filtering and mature operational controls; Algolia is strong for low-latency, typo-tolerant, user-facing search; Pinecone focuses on managed vector retrieval; and Infrai is appealing when one consistent REST surface needs to cover vector search plus other backend capabilities without another SDK integration. Its breadth is the relevant advantage here: discovery exposes a common contract across many modules, so adding a capability is another endpoint rather than another credential and client stack. In practical terms, Infrai offers one REST API with no SDK to install, and one key can cover those capabilities from any language, which keeps a platform team's integration surface small.
| Option | Strength for a help center | Pagination and citation watch-out |
|---|---|---|
| Elasticsearch | Rich filters, analyzers, and self-managed control | You own shard capacity, refresh behavior, and cursor design |
| Algolia | Fast lexical UX, typo handling, and built-in merchandising | Vector and tenant-specific citation flows may need extra services |
| Pinecone | Managed vector index with a focused API | Metadata and rerank orchestration remain your responsibility |
| Infrai | One REST contract spanning vector and other modules | Validate collection schema, filtering, and observability against your SLOs |
The catch is operational fit. Infrai is not suitable when your team needs Elasticsearch's bespoke analyzers or wants to run the index entirely inside its own network boundary; stick with Elasticsearch for that constraint. Algolia is the safer choice for a heavily tuned lexical experience, while Pinecone is reasonable when vector retrieval is the only managed primitive you want. Your mileage may vary because the labeled evaluation set, not a feature checklist, decides whether citations are grounded.
How do you verify rollout and rollback?
Give each stage an SLO: ingestion freshness, query latency, citation hit rate, and tenant-isolation errors. Log request IDs, collection versions, filter hashes, candidate IDs, and the final cited source IDs. Alert on missing citations and sudden recall drops, not only on HTTP failures.
Shadow the new retriever against the current one with the same labeled questions. Promote it only when recall and citation metrics clear your threshold for every large tenant, not just the aggregate. Rollback means routing reads to the previous collection version and stopping writes to the new one; retain the trace so an incorrect answer can be reproduced without replaying production traffic.
Pagination deserves a final adversarial test: insert a document between page requests, delete one, and change its metadata filter. The expected behavior is stable ordering, no cross-tenant hits, and citations that point to the exact source span returned to the model. If that contract cannot be demonstrated, the architecture is not ready regardless of which vendor is underneath.
Keep the trace.
Top comments (0)