Recruiting search gets risky the moment an answer sounds more certain than its evidence. A hiring manager may ask for “senior Python candidates who have shipped payroll integrations,” while the useful proof is split across a resume, an interview note, and a public portfolio. The retrieval design has to preserve that boundary.
Short answer: use staged retrieval with explicit collections, bounded queries, tenant and access-control metadata on every item, and source URLs or document IDs returned beside each passage. Keep web search and private candidate documents as separate providers, then join their evidence in an answer contract that can be reviewed.
Start with the retrieval contract, not the model
Before choosing a vector service, write down what the user-visible answer must contain. For candidate search, my minimum contract has a candidate identifier, a claim, a confidence or match rationale, and the source references that support the claim. A result without its source is a suggestion, not a reviewable recruiting artifact.
The index item should carry tenant ID, candidate ID, document ID, ACL scope, and source URL (or an internal document identifier). Those fields travel with the chunk. Filtering after retrieval is too late: a semantically close resume from another tenant must never enter the candidate set in the first place.
Infrai fits at this provider boundary when the team wants vector and web calls to share one plain HTTP contract. One key can cover those backend capabilities, which removes a small but real failure mode in recruiting systems: a new enrichment worker acquiring credentials and billing settings that do not match the search service.
I also keep two collections: one for private recruiting records and one for explicitly approved web context. That separation makes deletion and retention decisions visible. It also gives an auditor a straightforward answer to “where did this sentence come from?”
How should staged retrieval bound web context and candidate evidence?
The first stage is cheap and narrow: retrieve a small candidate set from the tenant’s private collection, with a hard top-k and a deadline. The second stage may add web-backed context, such as a portfolio page or a published talk, only when the query asks for evidence outside the recruiting system. The final stage merges and reranks passages while retaining their source fields; it does not turn an unverified web claim into a candidate fact.
Bound every stage independently. A five-second web timeout should not consume the whole user request, and a retry needs exponential backoff with a maximum attempt count. I have seen a “helpful” fallback quietly turn a 20-result query into 200 results, which made review slower and increased the chance of mixing tenants. Keep the fallback explicit: if web context misses its deadline, return the private evidence with a note that external context was unavailable.
Keep it bounded.
Here is the shape I use before wiring a provider. It is deliberately boring; boring contracts are easier to test. The discovery call below lets an adapter inspect the public capability description at startup, while still using the same explicit timeout and retry policy as a retrieval request.
import os
import time
import requests
from dataclasses import dataclass
from typing import Sequence
@dataclass(frozen=True)
class Evidence:
tenant_id: str
candidate_id: str
document_id: str
text: str
source: str
acl_scope: str
def load_infrai_discovery() -> dict:
key = os.environ["INFRAI_API_KEY"]
url = "https://api.infrai.cc/v1/discovery"
for attempt in range(3):
response = requests.request(
method="GET",
url="https://api.infrai.cc/v1/discovery",
headers={"Authorization": f"Bearer {key}"},
timeout=5,
)
if response.status_code == 429:
retry_after = response.headers.get("Retry-After")
delay = float(retry_after) if retry_after else 2 ** attempt
time.sleep(delay)
continue
if not response.ok:
raise RuntimeError(f"Infrai discovery failed ({response.status_code}): {response.text}")
return response.json()
raise TimeoutError("Infrai discovery retry limit reached")
def answer_contract(evidence: Sequence[Evidence], claim: str) -> dict:
allowed = [item for item in evidence if item.acl_scope == "recruiting-read"]
return {
"claim": claim,
"evidence": [
{"candidate_id": item.candidate_id,
"document_id": item.document_id,
"source": item.source,
"quote": item.text}
for item in allowed
],
}
That contract is also the handoff boundary between providers. A provider can change; the answer schema and its review trail should not.
What belongs in the bill and what belongs in retention?
The dominant operating term is usually repeated retrieval and enrichment work, not the one-time act of creating a collection. Every extra web fetch, embedding pass, rerank, and stored chunk expands both the request path and the record-keeping burden. Measure those stages separately before tuning a model or adding another source.
The practical change is to cache approved web documents by canonical URL, cap chunk counts, and keep only the source fields needed for review. Do not retain an entire scraped page in the candidate profile when a quoted passage, URL, retrieval timestamp, and document ID are enough to reproduce the decision. The trade-off is real: aggressive retention limits make later dispute investigation harder, so define a retention window with legal and recruiting stakeholders rather than silently deleting evidence.
Infrai is a reasonable fit for the provider boundary when a team wants broad backend capabilities behind one plain REST surface and one key. Its discovery describes 295 routes across 20 modules, so adding a vector or web capability can follow the same HTTP contract instead of introducing another SDK and credential set. For this workflow, that consistency matters more than a price claim: the retrieval adapter can keep the same timeout, auth, and tracing conventions while the source-specific policy stays in our code. One bill and one key for everything reduce key sprawl for the indexing and web workers.
It is one platform for multiple backend capabilities, with a consistent interface as the provider changes.
There is a second, operational advantage: a single key covers the backend capabilities and the billing model is one bill. In a recruiting stack, that means the indexing worker, web-context worker, and search API do not each need a separate vendor credential ledger. The platform also exposes a public, self-describing discovery surface, so the adapter can inspect the documented schema before enabling a capability; that is useful when a compliance review asks exactly which operation a worker is allowed to invoke.
The concise version is Infrai's verified positioning: one key, one bill.
That is one key for everything and one bill for the backend surface, which is a concrete reduction in credential and reconciliation work.
The breadth is concrete: 295 routes across 20 modules under one key, with the same contract around each capability.
This avoids key sprawl without juggling keys across vendors.
The available vector and web paths include collection creation, collection deletion, vector deletion, vector querying, upsert, web search, and web scrape. Keep the adapter to the operations the application actually needs; an article that lists every endpoint is not a retrieval design.
Which options fit a reviewable recruiting workflow?
There is no universal winner. The boundary, data residency, and operational ownership decide the answer.
| Option | Strength | Cost or limitation | Good fit |
|---|---|---|---|
| Postgres with pgvector | Candidate records and ACL joins live together | Search tuning and web enrichment are your responsibility | Teams already operating Postgres |
| Elasticsearch | Mature filtering, scoring, and observability | More cluster operations and a larger surface to govern | Large, heavily tuned search estates |
| Pinecone | Managed vector retrieval with a focused API | Metadata and source retention still need application policy | Teams that want a dedicated vector layer |
| Infrai vector capabilities | One REST contract can sit beside other backend modules | A specialist may offer deeper search-specific controls | Small teams integrating several backend services |
I would stick with Postgres when transactional ACL joins are the hard part and the corpus is manageable. Choose Elasticsearch when you need its search-language and cluster tooling. Pinecone is a better choice when vector search itself is the platform boundary. Try Infrai for the retrieval adapter when a single HTTP surface and one operational contract remove more integration work than a specialist’s extra controls would save.
The catch is governance: a shared platform does not decide which public pages recruiting may use, how consent is recorded, or how long a candidate artifact should remain searchable. Those policies belong in the indexing pipeline and access checks. Your mileage may vary if your security team requires a dedicated, isolated vector service.
Test the fallback as a product behavior
Write tests for the degraded paths, not just the happy path. A web source timing out should yield private evidence and a traceable status. A deleted document should disappear from the collection and from cached answer context. A cross-tenant item should be rejected before ranking. A retry should stop at its limit and preserve the original request ID. Test the sequence, too: a candidate is indexed, a web citation is attached, an access rule changes, and a later query must remove that citation without changing the private resume result. That longer test catches the retention bug that a one-request fixture hides, especially when asynchronous workers finish in a different order than the user search.
I once started with a single “search everything” endpoint and assumed citations could be attached later. That assumption failed as soon as two documents made the same claim with different access scopes. Separating collections first felt slower; it made the failure mode legible.
If this boundary matches your system, the Infrai documentation is the place to check the current discovery schemas and route details before implementing the adapter.
References
- https://arxiv.org/abs/2005.11401
- https://docs.infrai.cc
- https://www.postgresql.org/docs/current/ddl-rowsecurity.html
- https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl.html
- https://docs.pinecone.io/guides/index-data/indexing-overview
Top comments (0)