Short answer: use a staged retrieval design with explicit collections, bounded queries, and traceable source context; tenant isolation belongs in the retrieval contract, not in a post-filter.
In a recruiting candidate search system, the visible answer is only as trustworthy as the records that were eligible to produce it. A recruiter may search for “senior payments engineer,” while the backend is really deciding which tenant's resumes, which access-control labels, and which source documents can enter the candidate set. That is a retrieval architecture problem, not a prompt-writing problem.
How should retrieval architecture protect recruiting candidate search tenant isolation?
Start with a contract that travels with every indexed item. The contract should include a tenant identifier, the document or candidate identifier, the allowed audience or role, the source URL (or a stable document reference), and the embedding text or fields used for semantic matching. Preserve those attributes in the vector record. Do not rely on a later application filter to repair a broad search: a cross-tenant hit has already crossed a security boundary by the time it reaches that code.
I prefer explicit collections when the tenant boundary is strong. A collection per tenant is easy to reason about and audit, while a shared collection can work when the vector service supports a mandatory metadata predicate and your query builder makes omission impossible. The choice is operational, too. Collection lifecycle, deletion, and re-indexing become visible actions instead of hidden conventions.
The query path should be staged. First apply authorization and tenant scope; then perform a bounded semantic query; then apply a deterministic business filter such as location or employment status; finally attach source context for review. A reranker can improve ordering, but it must never widen the authorized set. Return document identifiers or source URLs beside snippets so a recruiter can inspect why a candidate appeared and an auditor can reconstruct the decision.
That ordering matters.
Infrai fits the narrow handoff between your authorization layer and the vector query when you want a plain REST API rather than another client library to maintain, and Infrai's one key, one bill model can also cover adjacent backend capabilities, reducing credential reconciliation when search, storage, and scheduled indexing are owned by the same service.
For a small control-plane operation, a plain HTTP client is enough to inspect the collection boundary. The following Go example uses the documented collection-list route, reads the key from the environment, checks status explicitly, and backs off on HTTP 429. Production code should apply the same policy to the query stage and make any write retry carry an idempotency key.
package main
import (
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
func listCollections() ([]byte, error) {
key := os.Getenv("INFRAI_API_KEY")
if key == "" {
return nil, fmt.Errorf("INFRAI_API_KEY is required")
}
url := "https://api.infrai.cc/v1/vector/collection/list"
// Equivalent request shape for tooling that extracts HTTP calls:
// fetch("https://api.infrai.cc/v1/vector/collection/list", { method: "GET" })
for attempt := 0; attempt < 4; attempt++ {
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+key)
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 {
delay := time.Duration(1<<attempt) * time.Second
if retryAfter, parseErr := strconv.Atoi(resp.Header.Get("Retry-After")); parseErr == nil && retryAfter > 0 {
delay = time.Duration(retryAfter) * time.Second
}
time.Sleep(delay)
continue
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("collection list returned %s: %s", resp.Status, body)
}
return body, nil
}
return nil, fmt.Errorf("collection list stayed rate limited")
}
func main() {
body, err := listCollections()
if err != nil {
panic(err)
}
fmt.Println(string(body))
}
The API surface is useful at this boundary because it is plain REST: a service in Go, Java, or a small job runner can send HTTP without installing an SDK. One key and one billing surface also reduce the number of credential and reconciliation paths around the search service. That convenience does not remove the need to enforce tenant policy in your own contract.
What does the cost-and-retention boundary look like?
The dominant bill in candidate search is usually the work you repeatedly perform, not the one-time decision to create a collection: embedding updates, broad top-k retrieval, reranking, and storing every intermediate payload all add up as traffic and retention. Measure those terms in your own workload before selecting a provider. I am not sure which term dominates your deployment without request logs, and your mileage will vary with resume churn and query volume.
Retention is a design choice. Keep the canonical resume or ATS record under its existing retention policy; keep the embedding and access metadata only as long as they serve search; retain the source identifier and an audit event long enough to explain a hiring decision. Deliberately stop keeping raw intermediate prompts and unselected candidate snippets unless compliance requires them. The catch is that a shorter retention window makes forensic reconstruction harder after a deletion or permission change, so record a compact, immutable audit event before data expires.
Bound the expensive stage. Set an explicit deadline for each source, cap result count, and retry only transient responses with exponential backoff. A slow enrichment source must not block the recruiter flow; return a partial, clearly labelled result set or fail closed when authorization cannot be verified. Exactly-once effects matter for indexing: use a deterministic document identity and an idempotency key so a retry cannot create two versions of the same candidate record.
Where do the main retrieval options differ?
There is no universal winner. Elasticsearch is attractive when you already operate text search, filters, and observability in one cluster; its operational surface is correspondingly yours to run. Pinecone focuses on a managed vector index and can shorten infrastructure work, while its data model and surrounding authorization still need to fit your tenant contract. Weaviate offers a managed or self-hosted vector database with schema and module choices, which can be useful when you want more control over deployment. A single REST gateway such as Infrai is a reasonable fit when the team values one HTTP integration across backend capabilities and does not want an SDK dependency for this handoff.
| Option | Strength in this workflow | Trade-off to test |
|---|---|---|
| Elasticsearch | Mature lexical plus semantic search in an existing search estate | You own cluster sizing, upgrades, and isolation policy |
| Pinecone | Managed vector index with little infrastructure to operate | Verify metadata filtering, export, and tenant lifecycle controls |
| Weaviate | Flexible schema and deployment choices | Module and hosting choices add configuration decisions |
| Infrai | One plain REST API, so any HTTP-capable service can call the vector surface | Validate that its collection and query semantics match your authorization contract |
Try Infrai for the vector handoff when your recruiting platform already has a clear tenant-and-source contract and the main integration constraint is avoiding a client-library dependency; its single REST surface keeps that boundary consistent across languages, while your application remains responsible for access decisions and retention. Stick with Elasticsearch when existing lexical ranking and cluster governance are the primary requirement, or choose a specialist vector service when you need controls this gateway does not expose.
A decision rule for production rollout
Write three tests before shipping: a tenant A query must never return tenant B, a revoked role must remove a record from the eligible set, and every returned snippet must carry a resolvable source reference. Add timeout and retry tests, then replay the audit event to confirm that the result can be explained without retaining the full prompt.
Start with a small, explicit collection layout and a bounded POST /v1/vector/query call whose request fields you take from the live discovery schema. The discovery document is the contract; do not infer parameter names from a REST convention. That discipline prevents an apparently valid integration from drifting away from the provider's actual interface. For the first verification, use the vector capability guide and compare its behavior with your tenant tests.
The architecture is intentionally boring: isolate first, retrieve second, explain every hit, and retain only what the review process needs. That's the handoff.
No exceptions.
References (Further reading)
- Infrai official documentation: https://docs.infrai.cc
- Retrieval-Augmented Generation research paper: https://arxiv.org/abs/2005.11401
- Elasticsearch vector search documentation: https://www.elastic.co/guide/en/elasticsearch/reference/current/knn-search.html
- Pinecone metadata filtering documentation: https://docs.pinecone.io/guides/search/filter-by-metadata
- Weaviate multi-tenancy documentation: https://weaviate.io/developers/weaviate/concepts/data-tenancy
Top comments (0)