2026.03.12
Retrieval design for a resume screener that has to handle more than one client
The recruitment screener parses resumes and scores candidates against a job description. The obvious first implementation is: embed the JD, embed chunks of the resume, take the top-k most similar chunks, feed them to the model, done. That version works in a demo and falls apart the first time you screen a genuinely strong, senior candidate.
Why plain top-k retrieval rewards the wrong resumes
Cosine-similarity top-k has a specific failure mode: it doesn't know the difference between "relevant" and "relevant and already said." A resume that repeats "5+ years of production Python" in the summary, the experience section, and the skills list will have three near-duplicate chunks all scoring high against a JD that mentions Python — and top-k will happily retrieve all three, burning half the context budget on the same fact restated, while a candidate whose Python experience is stated once but backed by three different, more relevant project chunks (the one on distributed systems, the one on the exact domain the JD cares about, the one with a metric attached) gets partially crowded out.
In other words: plain similarity search is biased toward candidates who repeat keywords, not candidates with the most relevant and diverse evidence. For a screener whose entire job is separating genuinely strong candidates from resumes optimized to pattern-match a JD, that's the exact bias you cannot afford.
MMR: optimizing for relevance and coverage, not just relevance
Maximal Marginal Relevance reranks candidates for retrieval using both the query similarity and the similarity to chunks already selected:
MMR = argmax_{d_i ∈ R \ S} [ λ · sim(d_i, q) − (1 − λ) · max_{d_j ∈ S} sim(d_i, d_j) ]
Read plainly: after picking the single best-matching chunk, every subsequent pick is scored on how relevant it is to the JD minus how similar it already is to what's been picked. λ controls the tradeoff — closer to 1 behaves like plain top-k, closer to 0 optimizes almost entirely for diversity. For resume screening specifically, this means the five chunks that make it into the scoring prompt cover five different aspects of the candidate's background instead of five restatements of the same one — which is a much better basis for an LLM to actually judge fit against a multi-requirement JD.
[FILL IN: a real metric or before/after comparison from tuning λ on actual resumes — e.g. a case where λ too close to 1 caused a strong candidate to under-score, or the λ value that ended up working best in practice and why.]
Namespaces, not separate indexes, for multi-tenant isolation
The second design problem: this runs for multiple clients, and one client's candidate pool must never be retrievable by another client's query — not as a display bug, a privacy incident. There are two ways to get that isolation in Pinecone: a separate index per client, or a single shared index with per-client namespaces.
Separate indexes give the strongest isolation but the worst operational
story — every new client is new infrastructure to provision, monitor, and
pay for, and most clients' resume volume doesn't come close to justifying
a dedicated index. Namespaces give the same query-level isolation
(a query scoped to namespace: client_id structurally cannot return
vectors from another namespace — it's not a filter that can be
misconfigured, it's a partition the query never sees past) with none of the
per-client infrastructure overhead, and the added query latency is
negligible since Pinecone routes within a namespace at the storage layer,
not via a post-filter over the whole index.
Note
The isolation guarantee matters more than the latency number here — for a tool handling candidate PII across multiple employers, "queries are scoped by namespace, full stop" is a much easier security claim to defend than "queries are filtered by client_id and we've tested that the filter always applies."
[FILL IN: an edge case you actually hit with namespace-based multi-tenancy — e.g. namespace sizing/cost behavior, a migration between namespace schemes, or an incident where isolation almost broke and what caught it.]
Where this leaves the pipeline
Resume chunk embedded → MMR-reranked retrieval scoped to the client's namespace → top diverse chunks passed to the scoring model alongside the JD → structured score with the specific evidence cited per requirement, not a bare number. The score is only as trustworthy as the retrieval underneath it, which is the part that's easy to skip when you're moving fast and easy to regret once a client's hiring manager asks why a strong candidate scored low.