Why does my vector search return fewer results when I add a filter?
Most vector indexes find candidates first and apply the filter afterwards. The index hands back its usual few dozen nearest neighbours, the filter throws away the ones that do not match, and whatever is left is your result. A filter that matches 10 percent of your data keeps about 10 percent of those candidates, so a request for 10 results can come back with 4, or with 10 that are not the true nearest matches. pgvector's documentation gives the arithmetic: with HNSW and the default
hnsw.ef_search of 40, a condition that matches 10 percent of rows returns about 4 rows on average (pgvector README).The fix is to make the search filter-aware: scan more of the index until enough matches turn up, walk the graph through matching points only, or skip the index and compare against every matching vector when the filter is narrow enough. Which one you can use depends on your database. The sections below cover the cause, a check you can run on your own data and the setting to change in pgvector, Qdrant and Weaviate.
Two symptoms, one cause
A filtered search fails in one of two ways, and the second is harder to spot:
1. Fewer results than you asked for. You set a limit of 10 and get 3. Easy to notice, and common in pgvector with post-filtering. 2. A full page of the wrong results. You get 10 results, all matching the filter, but some of the true 10 nearest matches are missing. Systems that keep searching until they have k matches, without reading the parts of the index where the true neighbours sit, produce this one. Nothing in the response says the list is incomplete.
Both come from the same place. An approximate index only reads part of your data on each query: an HNSW search visits a bounded candidate list, and an IVF search reads the few clusters nearest the query (
nprobe or probes). When a filter is added on top, the candidates the index happened to read are the only ones the filter can keep. If the matching vectors sit elsewhere in the index, the search never sees them.The narrower the filter, the worse it gets. A filter that matches half your data loses a little recall. A filter that matches 0.1 percent of it, such as one customer's footage in a shared index or one product category, can lose most of it.
How to check whether your filtered search is missing results
Compare the index's answer with an exact answer on the same filter. Take a sample of real queries and filters, run each through your normal search, then compute the true top k by scoring every vector that matches the filter. The share of the true top k that your search returned is filtered recall.
import numpy as np
def exact_filtered_top_k(vectors, mask, query, k=10):
"""True top k among the rows where mask is True. Assumes normalized vectors (cosine)."""
rows = np.flatnonzero(mask)
scores = vectors[rows] @ query
return rows[np.argsort(-scores)[:k]].tolist()
def filtered_recall(returned_ids, vectors, mask, query, k=10):
truth = set(exact_filtered_top_k(vectors, mask, query, k))
return len(truth & set(returned_ids[:k])) / kHow to fix it in pgvector, Qdrant and Weaviate
| Database | What it does by default | What to change |
|---|---|---|
| pgvector | Filters after the index scan. Results are capped by hnsw.ef_search (40 by default) or ivfflat.probes, so a filtered query can return fewer rows (pgvector README) | From 0.8.0, set hnsw.iterative_scan (or ivfflat.iterative_scan) to strict_order or relaxed_order, which keeps scanning until enough rows match or hnsw.max_scan_tuples (20,000 by default) is reached. Index the filter column so Postgres can choose an exact scan, and use partial indexes or partitioning when you filter by a few distinct values (pgvector README) |
| Qdrant | Filters during the HNSW search. Uses the payload index to estimate how many points match and switches to a full scan below full_scan_threshold (Qdrant indexing) | Create payload indexes on filtered fields before loading data, because the extra filter-aware graph edges are only built after the payload index exists; an index added later needs the HNSW index rebuilt. For several strict filters combined, which can still leave the graph disconnected, Qdrant offers the ACORN search algorithm from v1.16.0 (Qdrant indexing) |
| Weaviate | Builds the filter first and searches with it, using the ACORN filter strategy by default from v1.34 (Weaviate filtering) | Set the flat-search cutoff so very restrictive filters switch to a brute-force search over the matching objects. Weaviate notes that as a filter gets more restrictive, an HNSW traversal approaches the cost of a brute-force search anyway (Weaviate filtering) |
Three things help whatever database you use:
What we measured when we fixed this in our own index
Mixpeek's vector store, MVS, searches by reading the partitions nearest the query, an IVF-style layout. Until October 2026 a filtered search read the usual nearest 16 partitions and stopped at the first 10 matches. We measured it against an exact filtered scan on a test set of 1 million 768-dimension vectors in 73 partitions, 100 queries per filter, in-process on one machine:
| Filter admits | Exact top 10 before | Exact top 10 after | Median latency before | Median latency after |
|---|---|---|---|---|
| 0.1% of vectors | 38 of 100 queries | 100 of 100 | 2.1 ms | 1.4 ms |
| 1% | 69 of 100 | 100 of 100 | 2.4 ms | 3.9 ms |
| 50% | 86 of 100 | 100 of 100 | 34.0 ms | 39.7 ms |
At 0.1 percent the old search returned the true 10 nearest matches on fewer than 4 queries in 10, and every one of those responses still looked complete. Even the broad 50 percent filter missed on 14 queries in 100.
The fix has two parts. A filter that admits up to 250,000 vectors is answered by scoring every matching vector exactly, when those partitions are loaded with their full-precision vectors. A broader filter widens the search in proportion to how selective it is, up to 8 times the normal number of partitions. Partitions that are not loaded keep the widened search. In this test the narrowest filter got faster: scoring its 1,000 matching vectors exactly took less time than reading 16 partitions.
These figures come from one harness on one test set, run in-process on one machine. Read them as the size of the effect; production latency depends on your data, hardware and index settings. The check in the section above is how to find your own numbers.
Why a short or incomplete result list matters more for agents
A person scrolling search results can notice that a known item is missing. An agent cannot. It takes the result list as the facts, and a filtered search that silently dropped the true best match gives it a confident answer built on the wrong evidence. Mixpeek's position is that agents acting on the world need verifiable facts derived from real assets, and a result list with a hidden gap fails that test. For an agent's retrieval, test filtered recall at the selectivities its tools actually use, and treat a list shorter than the limit as a reason to widen the search.
Where Mixpeek fits
Mixpeek reads your files where they already live, in S3, GCS or any bucket, and turns what's in them into data your software can search, classify and moderate. Filters on a Mixpeek retriever run inside the vector search described above. See filters in the docs, the technique explainer on pre-filtering, post-filtering and in-place filtering, MVS for the vector store itself, and the best vector databases to compare options.
Frequently Asked Questions
Why does pgvector return fewer rows than my LIMIT?
With an HNSW or IVFFlat index, pgvector applies the WHERE clause after the index scan, so the result is capped by
hnsw.ef_search (40 by default) or ivfflat.probes and then filtered. A condition matching 10 percent of rows returns about 4 rows with the defaults. Turn on iterative index scans in pgvector 0.8.0 or later, raise hnsw.ef_search, or index the filter column so Postgres can choose an exact scan (pgvector README).Is pre-filtering or post-filtering better for vector search?
Filtering before or during the search keeps recall when the filter is narrow. Post-filtering is simpler and works when the filter matches most of the data, since few candidates get thrown away. Qdrant and Weaviate both filter during the search and switch to an exact scan when a filter admits few enough vectors (Qdrant indexing; Weaviate filtering).
What is filtered recall?
The share of the true top k results among the vectors that match the filter that a search actually returns. Compute the truth by scoring every matching vector exactly, as in the snippet above, and report it separately for narrow and broad filters.
Does increasing ef_search or nprobe fix filtered search?
It helps, because the search reads more of the index before filtering, and it costs latency on every query. A narrow filter can need a very large value to recover recall. An exact scan over the matching vectors is usually cheaper for those.
How many results can a narrow filter lose?
In our measurement on 1 million vectors, a search that read a fixed 16 of 73 partitions returned the exact top 10 on 38 of 100 queries when the filter admitted 0.1 percent of vectors, and on 69 of 100 at 1 percent. Your own numbers depend on the data and the index settings; measure them with the check above.