DEV Community

puffball1567
puffball1567

Posted on

Why Filtered Vector Search Returns Fewer Than K Results

Vector similarity is rarely the only rule that decides whether a result is useful.

A support assistant may need documents for one tenant and one product version.
A code assistant may need the active repository and branch. An internal RAG system may need to enforce permissions, publication status, language, and a date range before it returns any text to a model.

These constraints are commonly called metadata filters. But adding a filter to an API request does not tell us when the filter is applied or how much vector search work it avoids.

That distinction matters:

A filter can reduce the final result set without reducing the vector search.

This article focuses on that execution boundary. A broader guide to reducing the candidate set is available in
RAG Retrieval Optimization: Reduce Vector Search Before Ranking.

Pre-filtering vs post-filtering in vector search

Consider a search over one million document vectors:

tenant_id = "acme"
product = "billing"
language = "en"
version = "2026.2"
published = true
Enter fullscreen mode Exit fullscreen mode

Suppose only 2,000 vectors satisfy all five conditions. The application asks for the ten most similar eligible documents.

There are several ways a system can execute that request:

Strategy Simplified execution
Post-filtering Search broadly, then remove results that fail the filter
Overfetch and post-filter Search for more than ten candidates, filter them, then keep ten
Pre-filtering Build an eligible set first, then search within it
Filter-aware ANN search Use metadata indexes while traversing the vector index
Namespace or partition search Select a smaller stored subset, then run vector search
Application routing Route directly to a known tenant, repository, or data neighborhood

All six can expose an API that looks like "vector search with metadata filtering." Their cost and failure behavior are not the same.

Why post-filtered vector search returns fewer than k results

Post-filtering first asks the vector index for its nearest results and then removes candidates that do not satisfy the metadata conditions.

If the unfiltered search returns 50 candidates but only 0.2 percent of the corpus is eligible, many requests may produce fewer than ten valid results.

Some may produce none, even when matching documents exist elsewhere in the index.

Overfetching reduces that risk:

wanted results: 10
initial vector candidates: 500
apply metadata filter
return the first 10 eligible results
Enter fullscreen mode Exit fullscreen mode

But the correct overfetch factor depends on filter selectivity and on how eligible vectors are distributed in the vector space. A fixed multiplier may work for one tenant and fail for another. A retry loop can improve completeness, but it adds work and makes tail latency harder to predict.

Post-filtering is still reasonable when filters are broad, the candidate pool is already small, or occasional short result sets are acceptable. It is a poor default for strict authorization boundaries or highly selective filters.

Weaviate's documentation describes the same two post-filtering risks: an unpredictable result count and the possibility that a restrictive filter leaves no match in the initial vector results. Its implementation uses pre-filtering with an allow list instead.

Weaviate filtering concepts

Pre-filtering changes the eligible search space

Pre-filtering determines which records are eligible before similarity ranking.
A simplified plan looks like this:

authorization filter
  -> tenant and product scope
  -> eligible vector IDs
  -> vector similarity search
  -> optional reranking
  -> context construction
Enter fullscreen mode Exit fullscreen mode

This makes the result contract easier to understand: the nearest neighbors are selected from the eligible set rather than selected globally and checked later.

However, "pre-filtering" does not necessarily mean that the engine performs an exact scan over every eligible vector. An implementation may combine an inverted metadata index with an ANN graph, pass an allow list into graph traversal, choose between exact and approximate search based on cardinality, or use another filter-aware strategy.

The important questions are therefore practical:

  1. Is eligibility established before results enter the top-k set?
  2. Can the vector index traverse efficiently under a selective filter?
  3. Does the engine fall back to an exact scan for some filter shapes?
  4. Which metadata fields require their own indexes?
  5. What happens when the eligible set is empty or extremely small?

For example, Qdrant recommends creating payload indexes for fields used in filters, preferably before ingestion. Its documentation treats fields such as availability, location, and price as business constraints that embeddings do not express reliably.

Qdrant filtering documentation

How filter selectivity changes vector search performance

Filter selectivity is the fraction of the corpus that remains eligible. A filter matching 800,000 of one million records has 80 percent selectivity. A filter matching 2,000 records has 0.2 percent selectivity.

That number can change the best execution plan:

  • a broad filter may add metadata work without removing many vector candidates;
  • a selective post-filter may discard almost every ANN result;
  • a selective pre-filter may make an exact scan of the eligible set practical;
  • an intermediate filter may need filter-aware graph traversal;
  • a stable and highly selective scope may be better represented by routing or partitioning.

Do not infer performance from the final result count. Two queries can both return ten rows while one considers thousands more vector candidates. Record eligible cardinality and vector work for every important filter shape.

Metadata correctness comes before metadata performance

Filtering is not only an optimization. Some filters define whether a document may be considered at all.

A useful order for a RAG retrieval pipeline is:

  1. enforce authorization and tenant isolation;
  2. select a stable application scope such as repository, product, or version;
  3. apply dynamic metadata such as status, language, and date;
  4. perform vector or hybrid candidate ranking;
  5. rerank the bounded candidate set;
  6. project fields and enforce the context budget.

Authorization should not depend on whether an ineligible vector happens to miss the top-k cutoff. It should be enforced as a hard boundary independently of relevance ranking.

The metadata model also needs explicit semantics. For example:

Field Useful question
tenant_id Is this a security boundary, a routing boundary, or both?
repository Can one query intentionally search multiple repositories?
version Should older versions be excluded or merely ranked lower?
language Is fallback to another language allowed?
published Can drafts ever enter model context?
valid_from and valid_to Which time is authoritative for the request?

A vector database cannot infer these policies from an embedding. The application still owns the meaning of the fields and the rules for combining them.

Tenant-scoped vector search: filter or namespace?

Namespaces and partitions can reduce the search space before vector ranking, but they are not a replacement for metadata indexes.

A good routing or partition boundary is usually:

  • known before retrieval;
  • stable enough that records do not move constantly;
  • selective enough to exclude substantial unrelated data;
  • meaningful to authorization or application behavior;
  • limited to a manageable number of scopes.

Tenant, repository, product, and corpus source often fit. Free-form tags, temporary UI filters, arbitrary price ranges, and frequently changing status values usually fit metadata filtering better.

For multi-tenant vector search, the choice depends on the workload. A namespace or routed partition makes tenant scope explicit and can avoid cross-tenant search work. A tenant_id metadata filter is more flexible when queries may legitimately span tenants or when creating many physical scopes would be operationally expensive.

Neither choice automatically implements authorization. Tenant identity must come from an authenticated context, and every retrieval path must enforce the same policy.

This produces a layered design:

stable scope
  -> dynamic metadata filter
  -> vector or hybrid ranking
  -> reranking
  -> LLM context
Enter fullscreen mode Exit fullscreen mode

The stable scope makes the first problem smaller. Metadata filters express conditions inside that scope. Vector similarity orders the remaining semantic candidates.

How to measure vectors scanned in filtered vector search

Returning ten results does not mean that only ten vectors were considered.
When evaluating filtered vector search, record at least:

  • total vectors in the corpus;
  • vectors in the selected namespace or partition;
  • records eligible after metadata filtering;
  • vector candidates visited or scored;
  • results remaining after filtering;
  • recall at k for the eligible ground truth;
  • empty-result and short-result rates;
  • p50 and p95 retrieval latency;
  • candidates passed to the reranker;
  • tokens passed to the model.

Test several filter selectivities. A query matching 80 percent of a collection has a different execution shape from one matching 0.1 percent. Also test correlation: eligible vectors may be clustered together in the vector space or scattered across it.

Finally, include a deliberately wrong scope in the evaluation. A narrow query can be fast because it excluded the correct documents. Latency improvement is not useful unless recall and authorization correctness remain intact.

A practical decision checklist

Before shipping vector search metadata filtering, ask:

  1. Which filters are authorization rules rather than relevance hints?
  2. Are filters applied before or after the vector top-k is formed?
  3. Which filter fields have indexes?
  4. How does performance change as filter selectivity changes?
  5. Can a stable namespace or application route reduce the search space first?
  6. Does overfetching hide an incomplete post-filtering design?
  7. Are recall, vectors examined, latency, and context tokens measured together?
  8. What is the fallback when the chosen scope is wrong or empty?

Filtered vector search FAQ

Does metadata filtering reduce vector search work?

Only when the engine uses the filter before or during vector candidate selection, or when the application routes the query to a smaller scope. A post-filter can reduce the returned rows while leaving the original vector search unchanged.

Why does vector search return fewer results than k?

The corpus may contain fewer than k eligible records. If enough eligible records do exist, a post-filter may have removed too many of the initial ANN candidates, or an approximate filtered search may have exhausted its search budget before finding k matches.

Does pre-filtering reduce vector search recall?

Recall must be measured against the eligible ground truth. A correct filter intentionally excludes out-of-scope records, but a wrong filter can exclude the answer. Filtered ANN traversal can also have different recall behavior from an unfiltered graph, so it should be compared with exact search over the same eligible set.

Should tenant ID use a metadata filter or a namespace?

Use a namespace or routing boundary when tenant scope is stable, selective, and almost every query belongs to one tenant. Use an indexed metadata filter when the scope is dynamic or legitimate cross-tenant queries are common. Some systems combine both: route to a tenant-level scope, then apply metadata filters inside it.

A different approach: placement-aware retrieval in KoutenDB

Metadata filtering is not the only way to reduce vector work before ranking.
When a useful search scope is predictable while data is being stored, placement itself can provide the first candidate boundary.

KoutenDB is an open-source document and vector database written in Nim.

KoutenDB does not implement a ring as a conventional metadata filter. A ring is an application-defined locality coordinate chosen when data is placed. The same coordinate can later become the starting scope for retrieval.

For example, documentation for different Laravel releases can remain in separate rings while a stellar lens records that they belong to the same framework context. The lens changes visibility metadata; it does not copy the document payloads between rings:

import koutendb

var db = koutendb.open(dataDir = "data")
db.attachStellar("framework/laravel", "docs/laravel/10")
db.attachStellar("framework/laravel", "docs/laravel/11")

let queryVec = @[1.0'f32, 0.0'f32]
let relatedVersions = db.readStellar("framework/laravel")
let hits = db.retrieve(
  queryVec,
  ring = "docs/laravel/11",
  budget = 8
)
Enter fullscreen mode Exit fullscreen mode

The stellar read keeps the Laravel 10 and Laravel 11 results grouped by their original ring, so the application can see them as related without collapsing their version boundaries. subrings can narrow a stellar read when only one member coordinate is needed.

The current vector retrieve API does not use a stellar lens as an implicit vector filter. After the appropriate member ring is selected, KoutenDB scans the vectors stored in that ring, computes their exact cosine similarity, and returns up to eight results. It does not first run a global vector search and discard results from other versions afterward.

This is placement-aware retrieval rather than general-purpose metadata filtering. Rings preserve stable distinctions such as framework version, tenant, product, language, or document family. Stellar lenses can expose related rings through one read context without erasing those distinctions.
Frequently changing conditions such as publication status or price may still need ring-read filters, application logic, or an indexed metadata filtering system. A ring or lens also should not be treated as authorization by itself;

authentication and access policy remain separate concerns.

The trade-off is explicit. KoutenDB works best when the application, import rule, or operator can express a useful locality while storing the data. If no meaningful locality is known, retrieval without a ring becomes a broad exact scan. KoutenDB also exposes retrieval statistics such as totalVectors,
scanned, skippedVectors, and candidateReduction, making the amount of vector work observable instead of inferring it from the number of returned results.

The goal is not to avoid vector search. It is to make vector search operate on the smallest valid candidate set. Metadata filters determine which records are\ eligible. When a useful scope is known in advance, placement-aware retrieval can make that eligible set smaller before ranking starts.

Top comments (0)