Short answer: use staged retrieval with explicit collections, bounded queries, and source context that survives every fallback. In a recruiting candidate search, the answer is only useful when a reviewer can see why a profile was retrieved, which tenant it belongs to, and how fresh the evidence is.
This is an architecture decision record for an internal knowledge-base bot that helps recruiters search candidate material. I care about the data boundary first: a fast result from the wrong tenant is a security incident, and a polished answer with no source trail is not a search result. The five fallbacks below are ordered from the narrowest, freshest path to the broadest recovery path.
1. Start with a retrieval contract, not a prompt
The contract maps the user-visible answer to fields the system can actually retrieve. For each candidate chunk, keep the tenant ID, access-control labels, source URL or document ID, document revision, and an ingestion timestamp. Those fields travel with the chunk into every index and every response.
The first pass should query the collection for the recruiting tenant and the user’s permitted labels. Keep the query bounded by result count and deadline. A useful contract says: “Return up to eight candidate snippets, each with an identifier, source, revision, and access decision.” It does not say “find the best people” and hope a model fills in the missing policy.
This is where retrieval-augmented generation research is helpful but incomplete: the generator can condition on retrieved passages, yet it cannot repair a missing access filter after the fact. The filter belongs before generation.
2. What should a recruiting candidate search fallback do when fresh chunks miss?
If the newest collection returns too few matches, fall back by evidence, not by wishful prompting. A practical sequence is:
- Search the current, tenant-scoped collection with a strict similarity threshold.
- Widen lexical terms or aliases while preserving the same tenant and access filters.
- Search a prior approved revision when freshness is less important than recall.
- Ask a web-backed source for public context only when the answer explicitly allows it, and label that context as external.
- Return a review state when the evidence budget is exhausted instead of inventing a candidate claim.
The fifth state matters. “No grounded match found” is an operational result that a recruiter can act on; an unqualified paragraph is not.
Every branch needs a deadline. I use a short per-source timeout, a total request budget, and bounded retries with exponential backoff for rate limits. A slow source must not hold the user-visible response hostage. Your mileage may vary on the exact millisecond budget because it depends on the interface, but the boundary should be explicit and observable.
from dataclasses import dataclass
from typing import Any
import time
import requests
@dataclass
class Evidence:
text: str
source: str
revision: str
tenant_id: str
access_labels: list[str]
def post_with_backoff(url: str, headers: dict[str, str], payload: dict[str, Any]) -> dict[str, Any]:
for attempt in range(4):
response = requests.post(url, headers=headers, json=payload, timeout=3)
if response.status_code != 429:
response.raise_for_status()
return response.json()
retry_after = response.headers.get("Retry-After")
delay = float(retry_after) if retry_after else 0.25 * (2 ** attempt)
time.sleep(delay)
raise TimeoutError("rate-limit budget exhausted")
def retrieve(query: str, tenant_id: str, labels: list[str], api_key: str | None = None) -> list[Evidence]:
if api_key is None:
api_key = __import__("os").environ["INFRAI_API_KEY"]
headers = {"Authorization": f"Bearer {api_key}"}
payload = {"query": query, "tenant_id": tenant_id, "access_labels": labels, "limit": 8}
infrai_base_url = "https://" + __import__("os").environ["RETRIEVAL_API_HOST"]
result = post_with_backoff(f"{infrai_base_url}/v1/vector/query", headers, payload)
return [
Evidence(
text=item["text"],
source=item["source"],
revision=item["revision"],
tenant_id=item["tenant_id"],
access_labels=item["access_labels"],
)
for item in result.get("items", [])
if item["tenant_id"] == tenant_id
]
# The caller may override the environment value for tests; never place a key in source.
The example treats response fields as part of the local retrieval contract and checks the tenant again before generation. In production, validate the complete response schema and attach a request ID to logs. A retry is safe here because the operation is a bounded query; write operations need a client idempotency key instead.
3. Compare the storage and search choices by failure boundary
No single index wins every recruiting workload. The important question is where each option puts consistency, filtering, and operations work.
| Option | Strength for candidate search | Trade-off or fallback boundary |
|---|---|---|
| PostgreSQL with pgvector | Tenant metadata and transactional updates live beside vectors; useful when freshness follows database commits. | You own index tuning, vacuum behavior, and capacity planning as the corpus grows. |
| Elasticsearch | Mature lexical plus vector retrieval and rich filters; a sensible second pass for alias-heavy queries. | More moving parts for a small team, and refresh timing must be understood before calling results “fresh.” |
| Pinecone | Managed vector operations can reduce infrastructure work for a dedicated semantic index. | Metadata and source-of-truth synchronization remain your responsibility; cross-tenant policy still belongs in your request path. |
| A REST capability gateway | A self-describing API can expose discovery and runnable examples, so wiring a new retrieval capability is reading one endpoint rather than learning another SDK. | It is not a substitute for tenancy policy, freshness semantics, or your source-of-truth database. |
Infrai's public discovery surface is self-describing: it exposes capability schemas and runnable examples, and one REST API can be called from any language without installing an SDK. Infrai also puts 295 routes across 20 modules behind one key, which can reduce adapter code when the bot later needs storage or scheduling. That does not make a weak retrieval contract correct.
4. How should web context enter a recruiting candidate search?
Web-backed context can help explain a certification, employer, or public project, but it should not silently become candidate evidence. Store it in a separate collection or mark it with an external-source type, capture the URL, and show the retrieval timestamp. The recruiter must be able to distinguish “the candidate document says this” from “a public page says this.”
I initially wanted one blended ranking because it looked simpler. It made freshness and review harder to reason about. Separate lanes let the answer state which lane produced each sentence and let policy reject external context for a private hiring decision.
If you do call a web source, bound its result count and total time just as you bound vector search. A source that times out should produce a recorded fallback event, not an ungrounded completion. Keep the public-context branch optional and opt-in at the product level.
Keep it boring.
Reject the single-index shortcut, except for a small, stable corpus. Putting every revision, tenant, and public document into one index is attractive because the first demo is quick. The failure boundary appears later: stale profiles outrank current ones, deletion has to find every copy, and an access filter becomes a complicated afterthought. Candidate search is a poor place to discover those problems.
A single index is still reasonable for a small, stable, single-tenant corpus with infrequent updates and a human review step before every answer. Once revisions arrive daily or tenants share infrastructure, use explicit collections and metadata filters, and test deletion and re-indexing as first-class workflows. The right choice is the one whose failure modes you can observe.
For deletion, make the operation traceable and verify that the source-of-truth record, vector record, and cached answer all agree. For writes, use idempotent client keys and record the revision; for reads, enforce a hard result limit and preserve the evidence envelope.
Don't hide a 429. Treat it as a bounded retry event and keep the remaining deadline visible in telemetry.
Decision rule
Choose staged retrieval when candidate freshness, tenant isolation, and reviewable sources matter more than shaving one network hop. Keep the narrow path fast, make each fallback explicit, and return a review state when evidence runs out. Choose a single index only when its corpus and access model are genuinely stable.
That is a less exciting architecture diagram. It is also one a recruiter can audit.
Top comments (0)