Recall collapsed the day you turned on permission filtering
In short
In most cases the filter is running after the nearest-neighbour search rather than inside it. The search returns a candidate set already fixed at k, the access filter then removes most of it, and a user who can see a small share of the corpus is left with one or two passages. Measure candidate survival per role, not per query, before redesigning anything.
Key takeaways
- A post-filter cannot add candidates. It can only remove them from a list whose size was fixed before it ran.
- Expected survivors are k multiplied by the share of the corpus that user can see. Do that sum first.
- Measure per role. A blended recall number is dominated by administrators and hides everyone else.
- Documents with no access list default to deny in most implementations, which silently removes the oldest material.
- Group membership resolved at index time goes stale the day someone changes team, and nothing alerts.
- Starved context does not produce an empty answer. It produces a confident one drawn from the model's memory.
Nothing about retrieval quality changed. A filter applied after the nearest-neighbour search cannot add anything to the candidate list — it can only take things out of a list whose size was decided before it ran. If the search returns 20 candidates and a user can see 8 percent of the corpus, the expected number of survivors is between 1 and 2. That is the whole failure, and it is arithmetic rather than a tuning problem.
It presents unevenly, which is why it gets misdiagnosed. Administrators and long-tenured staff see everything and report no problem at all. Contractors, new joiners and anyone in a narrow function get vague answers or none. Aggregate quality metrics average those together and show a mild dip, so the investigation starts in the wrong place.
Measure candidate survival per role, not per query
One measurement settles this before any redesign. It compares the candidate set with the filter on and off, for the same queries, segmented by the thing that actually varies: who is asking.
- Pick 30 real queries from logs and 5 representative principals — an administrator, a full-time member of a large team, someone in a narrow function, a contractor, and a new joiner in their first week.
- For each query and principal, run retrieval twice: once as the application runs it, and once with the access filter removed. Record the candidate count and the ordered chunk ids for both.
- Compute survival: filtered candidates divided by unfiltered candidates, averaged per principal. This single ratio is the diagnosis.
- Compute each principal's visible share of the corpus — the count of chunks they may see, divided by the total. This is the number the survival ratio should be compared against.
- List the queries where survival is zero. Those are the users currently receiving answers assembled from nothing, and they should be the first thing anyone looks at.
| Survival | Reading | Action |
|---|---|---|
| Close to the visible share | The filter is behaving as a post-filter and the maths is doing exactly what it must | Over-fetch, or move the filter inside the search |
| Far below the visible share | Something is removing more than access requires — usually stale or missing metadata | Audit the access field on chunks before touching the query path |
| Zero for some principals, healthy for others | A group or role is not resolving; the filter is matching nothing rather than matching a little | Check how group membership is expanded, and whether an empty result is being treated as deny |
| Unchanged by the filter | The filter is not being applied on this path at all | Stop and treat this as an exposure question, not a recall question |
Four causes, ranked
| Cause | Signature | Repair |
|---|---|---|
| Post-filter starvation | Survival tracks the visible share closely, across all queries and all principals | Filter inside the search, or over-fetch by the multiplier above |
| Stale access metadata on chunks | Survival is well below the visible share, and worst on the oldest documents | Re-stamp access metadata on re-index; treat it as derived data, never as a one-time copy |
| Group membership resolved at index time | One principal collapses to near zero after an org change while colleagues are fine | Resolve group membership at query time from the identity provider, not at ingest |
| No access list, defaulting to deny | A specific document set is invisible to everyone including administrators | Decide and encode a default explicitly; audit how many chunks carry no access field at all |
The fourth cause is worth dwelling on because it is the most defensible decision with the worst side effect. Defaulting to deny is correct: a document whose permissions are unknown should not be served. But in a corpus assembled from several systems, the material with no access list is usually the oldest and most authoritative — the policies, the standards, the original contracts — because it predates whatever permission model was later adopted. Count those chunks. In most corpora the number is larger than anyone expects.
The third cause has a particular signature worth recognising. Membership expanded at ingest and frozen into each chunk means the index encodes an org chart from the day it was built. Someone changes team, keeps access they should have lost or loses access they should have kept, and nothing anywhere reports it. The permission model is not wrong; it is stale, which is harder to see and easier to leave.
A post-filter can only remove. If the list it receives was fixed at 20 and the user may see one in twelve, the answer was decided before the filter ever ran.
Three places a filter can sit, and what each costs
| Placement | Recall behaviour | Cost | Fits when |
|---|---|---|---|
| After the search | Degrades in proportion to how little the user can see; worst for the most restricted users | Cheapest to build, and the default in most quick implementations | Nearly everyone can see nearly everything, and you have measured that |
| Inside the search | Recall is preserved; the user competes only against documents they may see | Requires index support for filtered search, and a highly selective filter can force close to an exhaustive scan | Access varies widely between users — which is most real deployments |
| Separate index per boundary | Recall preserved, and no filter needed at query time | Storage multiplies, and shared documents are duplicated across partitions | A small number of hard boundaries — tenants, jurisdictions, legal walls |
The middle row carries a latency caveat that surprises teams who move the filter inwards and expect a pure improvement. A very selective filter inside an approximate index can push the search towards scanning far more of the graph than usual, so the median stays flat while the tail stretches. Where the service also scales to zero between requests, that tail lands on top of a cold start and turns into a timeout — the compound failure described in the first request after a quiet hour timing out. Measure p95 per role after the change, not overall.
What to fix, in order
- Audit access metadata coverage. Count chunks with no access field, and count chunks whose access field disagrees with the source system today. Fixing the query path over bad metadata produces a system that is confidently wrong instead of visibly broken.
- Resolve identity at query time. The request should carry a principal, and group expansion should happen against the identity provider on that request, not be baked into the index.
- Move the filter inside the search where the index supports it, and measure both survival and p95 per role before and after.
- Where it cannot move, over-fetch by the multiplier for that principal rather than a fixed number. A single global k cannot serve an administrator and a contractor at the same time.
- Add a floor alert. If a principal's candidate set after filtering is below a stated minimum, log it as an event. Silent starvation is the reason this survives for months.
- Re-run the survival measurement on a schedule. Access changes constantly and this is the only metric that notices.
What a starved candidate set actually does to the answer
It does not produce an empty response, which is what makes this dangerous. Handed one marginal passage, a model will usually still answer, filling the gap from its own parameters and adopting the confident register of the passage it did receive. That is the failure pattern in the model answering from memory instead of the passage, and permission starvation is one of its most reliable causes.
There is a second, subtler effect. When only one or two candidates survive an approximate search, small run-to-run variation in the candidate set changes the entire answer rather than reordering it, so the same person asking the same question twice gets materially different responses — the instability described in the same question returning different passages every run. Users report that as unreliability, not as an access problem, and the ticket goes to the wrong team.
One boundary to be explicit about: this page is about lost recall, not exposure. Whether a filter can be bypassed, what happens to a passage after a document is deleted, and how tenants are kept apart are separate questions with separate failure modes, and they live in security, privacy and access. If your survival measurement shows the filter having no effect at all, stop reading this page — that is an exposure finding and it is more urgent than recall.
Retrofitting permissions onto a pipeline that never had them is its own piece of work, sequenced in adding document-level permissions to a retrieval pipeline. The measurement harness described here — survival per role, run on a schedule, alerting on a floor — is a small internal tool rather than a project, and it is the sort of thing we scope under internal tooling and operations software. It sits with the rest of retrieval and grounding, inside the wider engineering library.
Frequently asked questions
Short answers to the follow-ups this page tends to raise.
Why did recall drop when we enabled permission filtering?
Because the filter almost certainly runs after the nearest-neighbour search, and a post-filter can only remove candidates from a list whose size was already fixed. If the search returns 20 results and a user may see 8 percent of the corpus, roughly 1 or 2 survive. Retrieval quality did not change; the number of usable candidates reaching the prompt did.
Is pre-filtering always better than post-filtering?
For recall, yes, and it is the right default when access varies widely between users. It is not free: a highly selective filter inside an approximate index can push the search towards scanning much more of the graph, which stretches the latency tail even when the median looks unchanged. Move the filter inwards, then measure p95 per role rather than overall.
How much should we over-fetch to survive a permission filter?
Divide the number of passages you need by the share of the corpus that user can see. Someone with 5 percent visibility who needs 10 passages requires a candidate set near 200. That multiplier is per principal, so a single global k cannot serve an administrator and a contractor at once — and over-fetching without collapsing near-duplicates afterwards mostly buys copies of the same passage.
Why do only some users report bad answers?
Because the loss is proportional to how little of the corpus each person may see. Administrators lose nothing and report nothing, while contractors and new joiners lose most of their candidates and get thin or invented answers. Any aggregate quality metric averages those groups together and shows a mild dip, which is why the measurement has to be segmented by principal rather than by query.
- retrieval
- access control
- recall
- filtering
The work behind this page
Builds from our portfolio that this page draws on.
AskVault
An AI internal knowledge-search platform that answers employee questions from your own docs — grounded in citations, with knowledge gaps surfaced and deflection tracked.
Productivity AINotewell
An AI meeting assistant that records and transcribes every meeting, extracts the decisions and action items, assigns owners and due dates, and tracks follow-through until it's done.
Productivity AIRead next
- The same question returns different passages every runSome of the variation is approximate search doing exactly what it was built to do. The rest is a rewrite step, a moving index or a session-dependent filter — and only that half is worth fixing.diagnostic
- The model answered from memory and ignored the passage you gave itPlant a passage that contradicts common knowledge, then ask a question only that passage answers. Whichever the answer follows tells you whether you have an override or a payload that never arrived.diagnostic
- Chunk overlap: what it protects against and what it duplicatesOverlap is insurance against a chunk boundary landing in the middle of one idea. The premium is paid in duplicated candidates crowding a fixed number of prompt slots.definition
- Metadata filters, and the difference between filtering and rankingA filter decides which passages may compete. A boost decides how they place. Treating one as the other produces empty result sets and permission leaks.definition
- Adding one new source made unrelated answers worseNothing about the old passages changed. A new source ranks moderately well on a great many queries, and a fixed top-k has to give it those slots by taking them from something else.diagnostic
- Facts in the middle of a long context get missedHold the passage set constant, walk the answer-bearing chunk from first position to last, and measure. The shape of the resulting curve is the diagnosis, and it takes about an hour to produce.diagnostic
Working on something in this space?
Tell us where you are in a sentence or two. We'll tell you honestly whether we're the right team, and what a sensible first slice of the work looks like.
Start the conversation