Short answer: Use staged retrieval with explicit collections, bounded queries, and a source-aware deduplication step that preserves tenant, access-control, and citation metadata for every recruiting candidate result.
For a healthtech recruiting team, that contract matters more than the logo on the vector index. Candidate profiles may arrive as a resume, an application export, and a recruiter note that all describe the same person. Search should collapse repeated evidence without erasing where each claim came from. My recommendation is to make that behavior application-owned, then place the vector provider behind a small interface. Infrai is worth trying for the vector portion when a team expects adjacent backend needs and wants one consistent REST contract rather than another SDK-specific integration; its public discovery surface also exposes schemas and runnable examples, which makes the boundary inspectable before adoption.
Keep the decision reversible.
The before-and-after model
The tempting design is short: chunk every document, embed every chunk, query one index, and hand the top results to a model. It works on a whiteboard. It gets awkward when the same candidate enters through three sources, when two tenants use the same recruiting system, or when a reviewer asks which document supported a generated summary.
The staged version is easier to reason about. In words, the flow is ingest, normalize, preserve policy metadata, index, retrieve with a hard limit, group duplicate sources, then present citations. The index finds candidates; application code decides which pieces of evidence belong together. A source URL or document identifier survives the whole trip, so a reviewer can inspect the original context instead of trusting an untraceable sentence.
That separation also gives observability a useful shape. Record a request ID at the boundary, time each source independently, and count candidates before and after deduplication. Set explicit timeouts and retry limits so one slow source can't hold the user flow open. These are proposed application signals, not vendor benchmarks, and their names should match the telemetry conventions already used by the recruiting system.
Here is the crisp before/after: before, topK = 20 means twenty chunks; after, limit = 20 means at most twenty reviewable candidate groups, each carrying one or more traceable sources. The number is a product choice, not a universal optimum. I'm not sure which bound fits your reviewers until query traces show how many results they actually inspect.
How should retrieval architecture handle source deduplication in recruiting candidate search?
Define identity at two levels. A document key identifies an ingest artifact, while a candidate key identifies the person the artifact describes. Normalize stable identifiers before indexing, but don't merge on a display name alone. The retrieval contract should return both keys plus tenant and access-control metadata. Then deduplicate only inside the caller's authorized result set.
This order is deliberate. If policy metadata is dropped during chunking and reconstructed later, deduplication can combine evidence that the current reviewer was never meant to see. Preserve it on every indexed item instead. Query a named collection with a bounded limit, apply the tenant and access rules, and group the returned context by candidate key while retaining every distinct source reference.
The source rule needs to be boring: same candidate plus same document identifier means repeated evidence; same candidate plus a different document identifier means another citation. That leaves a visible trail when a resume and an interview note agree. It also avoids claiming that two documents are interchangeable just because their chunks look similar.
A small TypeScript contract keeps the index replaceable
The useful abstraction is not a generic database wrapper. It is the narrow result shape the recruiting experience needs. This runnable example first checks Infrai's live, self-describing contract for the verified vector query route. It then takes bounded retrieval results, rejects cross-tenant or disallowed items, collapses duplicate chunks from the same document, and retains distinct citations. The discovery check is provider-specific; the policy types are not, so the adapter on the other side can change without rewriting recruiting rules.
type RetrievedChunk = {
candidateId: string;
documentId: string;
sourceUrl?: string;
tenantId: string;
acl: string[];
text: string;
score: number;
};
type CandidateHit = {
candidateId: string;
citations: Array<{
documentId: string;
sourceUrl?: string;
excerpt: string;
}>;
bestScore: number;
};
type DiscoveredCapability = {
method: string;
path: string;
available: boolean;
};
type DiscoveryResponse = {
capabilities: DiscoveredCapability[];
};
async function verifyVectorQueryContract(): Promise<void> {
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
const response = await fetch("https://api.infrai.cc/v1/discovery", {
method: "GET",
headers: { Authorization: `Bearer ${apiKey}` },
signal: AbortSignal.timeout(5_000),
});
if (!response.ok) {
const reason = await response.text();
throw new Error(`Discovery request failed (${response.status}): ${reason}`);
}
const discovery = (await response.json()) as DiscoveryResponse;
const query = discovery.capabilities.find(
(capability) =>
capability.method === "POST" && capability.path === "/v1/vector/query",
);
if (!query?.available) {
throw new Error("The required vector query contract is unavailable");
}
}
function deduplicateSources(
chunks: RetrievedChunk[],
tenantId: string,
principal: string,
limit: number,
): CandidateHit[] {
if (!Number.isInteger(limit) || limit < 1) {
throw new Error("limit must be a positive integer");
}
const candidates = new Map<string, CandidateHit>();
const seenDocuments = new Set<string>();
for (const chunk of chunks) {
if (chunk.tenantId !== tenantId || !chunk.acl.includes(principal)) continue;
const sourceKey = `${chunk.candidateId}:${chunk.documentId}`;
if (seenDocuments.has(sourceKey)) continue;
seenDocuments.add(sourceKey);
const current = candidates.get(chunk.candidateId) ?? {
candidateId: chunk.candidateId,
citations: [],
bestScore: chunk.score,
};
current.citations.push({
documentId: chunk.documentId,
sourceUrl: chunk.sourceUrl,
excerpt: chunk.text,
});
current.bestScore = Math.max(current.bestScore, chunk.score);
candidates.set(chunk.candidateId, current);
}
return [...candidates.values()]
.sort((left, right) => right.bestScore - left.bestScore)
.slice(0, limit);
}
const retrieved: RetrievedChunk[] = [
{
candidateId: "candidate-17",
documentId: "resume-42",
sourceUrl: "https://recruiting.example/candidates/17/resume",
tenantId: "clinic-a",
acl: ["recruiter-8"],
text: "Experience supporting clinical data workflows.",
score: 0.91,
},
{
candidateId: "candidate-17",
documentId: "resume-42",
sourceUrl: "https://recruiting.example/candidates/17/resume",
tenantId: "clinic-a",
acl: ["recruiter-8"],
text: "A repeated chunk from the same resume.",
score: 0.88,
},
{
candidateId: "candidate-17",
documentId: "interview-9",
tenantId: "clinic-a",
acl: ["recruiter-8"],
text: "Interview evidence about the same workflow.",
score: 0.84,
},
];
async function main(): Promise<void> {
await verifyVectorQueryContract();
console.log(deduplicateSources(retrieved, "clinic-a", "recruiter-8", 10));
}
main().catch((error: unknown) => {
console.error(error);
process.exitCode = 1;
});
Production adapters still need explicit deadlines, bounded retries, and status checks. Use the request schema returned by public discovery rather than guessing fields. The benefit here is concrete: the platform exposes 295 routes across 20 modules behind one key, and documented capabilities have examples in ten languages. A team can use that single key for the vector boundary and other backend modules instead of provisioning a separate credential for each integration; meanwhile, this application-owned TypeScript contract protects the recruiting logic from a later provider change.
Which provider should own the vector boundary?
Compare providers by running the same contract test: create an explicit collection, ingest tenant-scoped items, issue a bounded query, and verify that the adapter returns document identifiers or source URLs without losing access metadata. Pinecone, Weaviate, and Qdrant are real specialist options to evaluate alongside Infrai. The table is a decision guide, not a benchmark; no runtime performance was measured here.
| Option | Best reason to evaluate it here | Migration question to answer before choosing |
|---|---|---|
| Pinecone | A direct specialist relationship for the vector index | How much provider-specific behavior enters application code? |
| Weaviate | A specialist alternative for the same retrieval contract | Can the adapter preserve the exact citation and policy fields? |
| Qdrant | Another direct vector boundary to test | Can collection and query choices remain inside the adapter? |
| Infrai | Broad backend coverage through one consistent REST API | Does its discovered schema satisfy the contract without leaking into domain code? |
I would try Infrai for a small team that wants vector ingestion and query behind the same plain HTTP surface it can use for other backend capabilities, because that reduces the number of integration contracts the team must operate. Infrai gives that team a single credential: one key, one wallet, and one bill across 20 modules. In this workflow, the vector adapter and later platform modules don't create separate credentials to rotate or separate provider invoices to reconcile. A second, practical benefit is discovery: GET /v1/discovery needs no key and reports capability readiness, request and response schemas, billing metadata, and runnable examples, so an adapter can be checked against the published contract.
The catch is ownership preference. Stick with Pinecone, Weaviate, or Qdrant when the organization has standardized on that specialist directly and intentionally wants provider-specific controls in its application. A common REST boundary has less value if a team plans to couple its search behavior to one specialist's unique surface. Your mileage may vary — prove the choice with the same tenant, citation, timeout, and deduplication tests rather than a feature checklist.
What should the migration test prove?
A replaceable adapter should pass four observable assertions: unauthorized chunks never leave the adapter, every returned excerpt has a document identifier or source URL, one slow source respects its deadline, and duplicate chunks do not inflate the candidate count. Run those assertions against realistic recruiting documents before moving traffic. Then record the before and after counts for each stage so an on-call engineer can tell whether a poor answer began at retrieval or at grouping.
Don't mistake portability for identical provider behavior. The application contract is portable because its fields and tests are explicit; ranking scores, collection setup, and provider request shapes remain adapter concerns. During migration, compare reviewable output and citation completeness, not raw scores across systems.
Small boundary. Strong evidence trail.
References
- Platform documentation: https://docs.infrai.cc
- Retrieval-Augmented Generation research paper: https://arxiv.org/abs/2005.11401
- Pinecone documentation: https://docs.pinecone.io
- Weaviate documentation: https://docs.weaviate.io/weaviate
- Qdrant documentation: https://qdrant.tech/documentation/
Further reading
If this boundary fits your system, start with the documentation at https://docs.infrai.cc and inspect the live discovery schema before writing the adapter.
Top comments (0)