The demo returns five useful passages. Add tenant_id = current_tenant, and the same retrieval call returns none. There are still relevant passages in the database.
Before changing the embedding model, inspect where the filter runs and how many candidates the search is allowed to inspect.
Five answers beyond the candidate budget
Imagine 55 records already sorted by distance to a query. The first 50 belong to tenant B. The last five belong to tenant A. We request five results for A, but the global search stops after 40 candidates.
rows = [(i, "B" if i < 50 else "A") for i in range(55)]
post_filtered = [row for row in rows[:40] if row[1] == "A"][:5]
exact_eligible = [row for row in rows if row[1] == "A"][:5]
assert len(post_filtered) == 0
assert len(exact_eligible) == 5
The authorization predicate is correct in both expressions. The candidate budget is different. The first expression cannot recover rows it never considered.
This is a deliberately simple candidate-truncation example, not a simulation of HNSW graph traversal. Run it as part of the Python fixture. It isolates the mechanism without claiming an ANN performance result.
The baseline is the eligible corpus
For a query with tenant, permission and document-state predicates, first define the set of records that actually satisfy all of them. Compute the exact nearest neighbors within that set. Compare the approximate result against those neighbors.
If only three eligible records exist and you request ten, returning three is not a recall failure. If 300 exist and the exact top ten are available but your approximate query returns two, investigate candidate exhaustion and plan choice.
Report result count, recall against that baseline, latency, and work consumed. Keep “unauthorized results returned” as a separate invariant with an expected count of zero. A better recall score never licenses dropping an authorization filter.
Use production-shaped filters in the evaluation: one large tenant, a tiny tenant, restrictive document permissions, combinations of predicates, and tenants whose data clusters differently in embedding space. A uniformly random filter can hide the failure you actually serve.
What pgvector documents
The pgvector README explicitly discusses filtering after an approximate index scan. A limited candidate set can leave too few results. Since version 0.8.0, iterative scans can continue searching until enough results are found or configured limits are reached.
That gives several options, with different costs:
| Situation | Candidate approach | Check before choosing |
|---|---|---|
| Small eligible set | Filter with a regular index, then compute exact distances | Planner choice and distance-computation cost |
| Approximate scan runs out of matches | Iterative scans and search-budget tuning | Recall, tail latency, work and memory caps |
| Few stable filter values | Partial vector indexes | Index count, write overhead, query predicate matching |
| Many tenant partitions with useful isolation | Partitioning | Operational complexity and queries spanning partitions |
An iterative scan is bounded work, not a proof of exact recall. strict_order orders the results it finds; it does not mean it found every true nearest neighbor. Inspect the executed plan rather than inferring it from the SQL text.
Why graph structure also matters
Qdrant’s August 2026 ACORN study compares query-time traversal, filter-aware graph edges and planner fallback. It helps explain why selective or correlated predicates can change graph connectivity, not just reduce the number of matches.
Treat the result within its scope: Qdrant 1.18.2, one million 96-dimensional image vectors, 500 queries per filter, serial execution on one machine. The reported latency is a mean, not a production p95. ACORN was opt-in in the described setup. This is a vendor-authored study of its own strategies, not evidence that one database universally beats another. We have not rerun its benchmark.
A retrieval diagnosis that saves a model change
Capture a failing query with its actual authorization context. Compute the eligible exact baseline. Then vary only the approximate search budget or plan while holding embeddings, predicates and reranking fixed. A recovery in this experiment points toward candidate search; a poor exact baseline points toward a different problem.
A reranker can reorder retrieved passages. It cannot rank a relevant passage that was excluded from the candidate set. Likewise, the generator cannot cite evidence it never received.
Use this as a companion to retrieval fundamentals. Once eligibility and candidate recall are correct, the RAG chapter helps with the remaining retrieval-to-answer pipeline.