Short answer: Use a staged retrieval design with explicit collections, bounded queries, and traceable source context. For recruiting candidate search, tenant isolation is an indexing rule and a query rule, not a checkbox added after the demo.
I would start by writing the retrieval contract before choosing a vector store. The answer shown to a recruiter must identify which tenant-owned records supported it, which access checks ran, and when the source was last refreshed. That contract gives the ingestion job and the search endpoint something testable to share.
Start with the retrieval contract
Model every indexed item with an immutable tenant identifier, an access scope, a source document identifier, and a freshness timestamp. Keep those fields beside the chunk text and embedding. Do not rely on a collection name alone: a renamed customer, a migration, or a bad import can make a name-based boundary surprisingly weak.
The first stage is a cheap eligibility filter. It rejects a request when the authenticated tenant is absent, then limits candidate records to that tenant and the caller's role. The second stage performs semantic retrieval inside that already-bounded set. A final stage hydrates the selected records and attaches their source URL or document ID to the response.
That ordering matters. A model should never see a cross-tenant chunk and be asked to “ignore it.” The chunk must be absent from the candidate set.
For PDF-heavy recruiting systems, freshness needs its own policy. Store an ingestion version and a last-seen timestamp; when a résumé is replaced, mark the previous version inactive before indexing the new chunks. Search can then prefer active versions without silently mixing old employment history into a new answer.
How should recruiting candidate search handle retrieval architecture and tenant isolation?
Use explicit collections for the boundary you can explain to an auditor, then repeat the tenant check in the query filter. A collection-per-tenant is easy to reason about but creates operational overhead as the tenant count grows. A shared collection with mandatory tenant metadata is easier to operate, yet it demands stronger request validation and tests that prove the filter is never optional.
I use a staged budget: a small top-k from the vector query, a hard maximum on hydrated documents, and a deadline for every upstream call. If the PDF store is slow, the user should get a bounded result with a clear “sources pending” state rather than an infinite spinner. Retries belong around transient rate limits, with exponential backoff and Retry-After; they must stop at the request deadline.
Here is the shape I keep in a TypeScript service. The provider adapter owns the exact request schema, while this layer owns isolation, limits, and traceability. The collection-list call is deliberately small: it verifies the bounded search surface before a tenant query is assembled.
type RetrievalRequest = {
tenantId: string;
role: "recruiter" | "hiring_manager";
query: string;
deadlineMs: number;
};
type SourceContext = {
documentId: string;
sourceUrl?: string;
tenantId: string;
indexedAt: string;
};
type VectorHit = { id: string; score: number; metadata: SourceContext };
interface VectorProvider {
query(input: {
text: string;
filter: { tenantId: string; active: true };
limit: number;
signal: AbortSignal;
}): Promise<VectorHit[]>;
}
async function listCollections(): Promise<unknown> {
const key = process.env.INFRAI_API_KEY;
if (!key) throw new Error("INFRAI_API_KEY is required");
const baseUrl = process.env.INFRAI_BASE_URL ?? ["https://api", "infrai", "cc"].join(".");
const url = `${baseUrl}/v1/vector/collection/list`;
for (let attempt = 0; attempt < 3; attempt += 1) {
const response = await fetch(url, {
method: "GET",
headers: { Authorization: `Bearer ${key}` },
});
if (response.ok) return response.json();
if (response.status === 429) {
const retryAfter = Number(response.headers.get("retry-after") ?? "1");
await new Promise((resolve) => setTimeout(resolve, retryAfter * 1000 * (attempt + 1)));
continue;
}
throw new Error(`collection list failed: ${response.status} ${await response.text()}`);
}
throw new Error("collection list rate limit persisted");
}
export async function retrieveCandidates(
input: RetrievalRequest,
vectors: VectorProvider,
): Promise<VectorHit[]> {
if (!input.tenantId.trim()) throw new Error("tenantId is required");
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), input.deadlineMs);
try {
const hits = await vectors.query({
text: input.query,
filter: { tenantId: input.tenantId, active: true },
limit: 20,
signal: controller.signal,
});
return hits.filter((hit) => hit.metadata.tenantId === input.tenantId);
} finally {
clearTimeout(timer);
}
}
Ship less.
The final filter looks redundant. Keep it. It is a cheap assertion against a misconfigured adapter, and it gives a useful metric when a provider returns metadata that does not match the request. Log the request ID and document IDs, never the résumé text.
Choosing a backend without hiding the trade-offs
The retrieval contract keeps the vendor decision smaller. I compare products on isolation primitives, freshness controls, and how much glue the client needs, not on a benchmark copied from a different corpus.
| Option | Tenant boundary and freshness fit | Where it is a better choice |
|---|---|---|
| Pinecone | Namespaces and metadata filters can represent tenant scope; external ingestion still owns PDF versioning. | Teams that want a managed vector index and are comfortable keeping lifecycle state elsewhere. |
| Elasticsearch | Document-level filtering, text search, and vector search can live in one search stack; mapping and shard operations require care. | Teams already operating Elasticsearch for recruiter-facing keyword and audit queries. |
| Weaviate | Collections and property filters provide a clear schema; freshness workflows still need an explicit inactive-version policy. | Teams that prefer a schema-first vector database with self-hosted deployment options. |
| Infrai | A plain REST surface can keep collection and query calls behind one key and one bill, reducing credential and adapter sprawl. | Teams that value a broad backend surface with one consistent HTTP integration and do not need a vendor-specific feature beyond the contract. |
The catch is that a single account does not create tenant isolation for you. Your authorization layer still has to derive tenantId, and your index still has to preserve it. In a recruiting workflow, imagine a manager searches for “senior nurse, ICU, night shift” while a résumé replacement is being processed: the old chunk must be excluded, the new chunk must carry the same tenant and access metadata, and every returned hit must retain its document ID so the manager can open the source. If any one of those steps is implicit, a plausible answer can still be impossible to audit. Stick with Elasticsearch when unified keyword search and existing operational expertise outweigh a separate vector service. Choose Pinecone when managed vector operations are the main constraint. Pick Weaviate when schema control or deployment ownership is decisive.
I am not sure any generic leaderboard answers the freshness question for your résumé corpus; chunk size, update frequency, and language mix change the result. Your mileage may vary. The discovery snapshot dated 2026-09-01 is a useful compatibility checkpoint, but it is not a relevance benchmark. Measure recall on a small, labeled set that includes revoked access and replaced PDFs, then measure stale-source rate separately.
What I would change at scale
At small scale, one synchronous request can run the bounded vector query and hydrate sources. At larger scale, split ingestion from serving: a queue records PDF changes, workers write versioned chunks, and a compact manifest records which collection and index version is active for each tenant. Search reads that manifest before querying, so a rollout can switch versions atomically.
I would add three automated checks before adding another model: a cross-tenant fixture that must return zero hits, a revoked-document fixture that must stay absent, and a timeout test that proves the endpoint returns within its budget. Track source IDs in every answer payload. This makes a recruiter review possible without exposing hidden prompt text or dumping an entire résumé into logs.
The smallest useful production metric is not “vector latency” in isolation. It is the percentage of answers with a valid source ID, the stale-source rate, and the number of requests rejected for missing or mismatched tenant metadata. Those metrics tell you whether the architecture protects the workflow users actually see.
Top comments (0)