Skip to content
RAG Explained Better

Metadata Filtering in Vector Search

Pre-filter, post-filter and the recall collapse that happens when you get it wrong.

Metadata filtering restricts ANN vector search to chunks whose structured attributes match a predicate — tenant, date, document type, language, or ACL tag — so similarity runs inside an allow-list rather than hoping the embedding ranks those constraints correctly.

It looks like a SQL WHERE clause on the index, but it is not free: whether the store pre-filters, post-filters, or walks a filter-aware HNSW graph decides whether top-k is true neighbours inside the slice or an underfilled, silent miss. This page covers those strategies, when recall collapses, and what must already exist on the chunk from metadata at ingest.

How do pre-filtering and post-filtering differ?

Pre-filtering and post-filtering are two orderings of the same metadata predicate relative to the ANN pass — and they fail in opposite ways when the filter is selective.

Pre-filter vs post-filter on vector search
StrategyOrderMain riskWhen it is usually enough
Post-filter ANN on the full graph → drop non-matches Underfilled or empty top-k even when matches exist Loose filters (most of the corpus still eligible)
Pre-filter Build allow-list from metadata → ANN only on survivors Brute-force cost or starved graph walk on awkward subsets Selective filters when the store is filter-aware

Post-filtering runs approximate nearest-neighbour search first, then removes candidates that fail the predicate. Guides such as About Vector Database’s pre- vs post-filter topic treat oversampling — requesting on the order of 5–10× k candidates before the filter — as the usual repair so enough survivors remain. Weaviate’s filtering concepts doc and Pinecone’s “Missing WHERE Clause” explainer both state the failure: a selective filter can leave fewer than k matches (or zero) inside the unfiltered candidate list even when eligible neighbours exist deeper in the index. Microsoft Learn’s Azure AI Search vector-filter docs add a stricter sibling, strictPostFilter, that can return zero results for selective filters or small k because only unfiltered global top-k rows are eligible.

Pre-filtering builds an allow-list from a metadata or inverted index first, then restricts vector search to those ids. Weaviate’s filtering concepts page frames filtered vector search as pre-filtering with an inverted-index allow-list passed into HNSW. Milvus’s filtered-search docs describe the same order as “standard filtering”: scalar predicate first, ANN inside the matching entities. The trap, which Pinecone’s pre-filter section and Ilir Rivezaj’s vector-database engineering notes both call out, is that “search restricted to that set” is real work — a naive engine either brute-forces the survivors or walks an HNSW graph whose useful neighbours were just declared ineligible.

Popular vector stores that expose filtered ANN include Weaviate, Pinecone, Qdrant, Milvus, and pgvector — with very different defaults for which of these orderings you actually get.

What is filtered HNSW and why does plain pre-filter fail on graphs?

Filtered HNSW (also called filter-aware traversal or single-stage filtering) bakes the metadata predicate into the graph walk so the search expands eligible neighbourhoods instead of treating the filter as a separate stage around an unmodified ANN pass.

HNSW (Hierarchical Navigable Small World) is a layered proximity graph: a query descends from sparse long-range layers into a dense base layer (Malkov & Yashunin, 2016). A metadata predicate that removes nodes also removes edges. When enough edges disappear, the surviving subgraph can fragment — the walk converges in whichever component the entry point landed in, and recall falls off a cliff rather than smoothly (MONA / BestAIWeb, May–June 2026 explainer, citing the ACORN line of work). That is why “pre-filter then HNSW as usual” is not automatically correct on graphs.

Three production shapes of the third strategy show up in vendor docs (as of this page’s July 2026 capture — verify before you pin versions):

  • Weaviate pre-filter + ACORN. Weaviate builds an allow-list from the inverted index, then traverses HNSW while only admitting allow-listed ids to the result set (Weaviate docs, concepts/filtering). Starting in v1.34, the default filterStrategy is ACORN — a multi-hop neighbour expansion inspired by Patel et al.’s ACORN paper — with sweeping still available as the older graph walk. Restrictive filters can auto-switch to flat (brute-force) search around a configured cutoff of roughly 15% of the dataset matched so HNSW does not become an exhaustive crawl of a tiny slice.
  • Pinecone single-stage filtering. Pinecone’s learn article describes merging vector and metadata indexes so filter evaluation is not a separate pre- or post-pass around ANN — marketed as pre-filter accuracy without the naive brute-force penalty.
  • Qdrant Filterable HNSW / Milvus standard+iterative. BestAIWeb’s three-strategy writeup summarises Qdrant’s payload-aware HNSW and cardinality-based fall-back to a payload-index scan below a full-scan threshold; Milvus documents standard pre-filtering plus an iterative mode that applies scalar checks while iterating ANN candidates when expressions are expensive.

Graph-parameter depth (M, ef_construct, ef_search) lives on HNSW. This page only needs the filtering consequence: connectivity, not just candidate count, decides filtered recall.

When does metadata filtering collapse recall?

Metadata filtering collapses recall when the engine still returns a plausible top-k from the wrong neighbourhood — or underfills k — without throwing an error.

Three mechanisms dominate published writeups:

  • Selective post-filter. Franck Pachot’s MongoDB / pgvector walkthrough (dev.to) shows an HNSW scan with ef_search = 40: of those forty unfiltered candidates, only 11 matched color = green after a post-filter, so a green-only top-15 cannot be a true filtered nearest-neighbour list. Pre-filtering during the graph walk would have kept filling green candidates up to the limit.
  • Graph fragmentation under restrictive filters. BestAIWeb’s filtered-HNSW explainer and Weaviate’s flat-search-cutoff rationale both describe the same geometry: as the allow-list shrinks, HNSW traversal approaches brute-force cost on the wrong structure unless the engine switches strategy (ACORN expansion, flat search on the survivors, or a payload-index rescore).
  • Wrong or missing metadata. Kandaanusha’s Medium overview flags nullable or absent fields as a “where did my data go?” failure: a correct-looking predicate excludes the gold chunk because the attribute was never written at ingest.

The detection signature used elsewhere on this site is recall(no-filter) − recall(with-filter) > 0 on the failing query: drop the predicate and the gold chunk reappears. Also log underfilled responses — returned count < k when matches exist. Full ordered diagnosis of “the document is indexed but invisible” lives on missing document; the inverse failure — a missing tenant predicate that admits foreign ids — lives on leakage.

When should you use pre-filter vs post-filter?

Use pre-filter or filter-aware traversal when the predicate is selective and missing a match is costlier than latency; use post-filter when the predicate is loose and predictable speed matters more than filling every slot of k.

About Vector Database’s rule of thumb matches the Microsoft Learn comparison table for Azure AI Search vectorFilterMode: highly selective filters (on the order of 1% of the corpus in the About Vector Database FAQ) favour pre-filtering for recall inside the slice; loose filters often keep post-filtering “sufficient and easier.” Microsoft’s published QPS benches on small (100,000 vectors), medium (1 million), and large (1 billion) indexes report that prefiltering is almost always slower than postfiltering on larger indexes — “orders of magnitude” slower on the billion-scale workload — yet remains the recommended default because it guarantees k results when eligible rows exist. Their takeaways say to reach for postfilter when filters are not overly selective and prefilter throughput is unacceptable; avoid relying on postfilter (especially strict postfilter) when false negatives are unacceptable.

Measure recall at the selectivity profile your production traffic actually has, not at uniform random predicates (BestAIWeb). Tenant and ACL constraints that must never underfill usually want storage-layer isolation as well as a filter — that model choice is multi-tenancy, not a post-filter oversample knob.

What metadata does filtering need at ingest?

Filtering can only predicate on fields that already exist on indexed chunks — typed, indexed, and populated at ingest, not invented at query time.

  • Stable filterable attributes. Dataquest’s metadata-filtering guide and OneUptime’s January 2026 implementation notes both treat category, year/date, language, tenant, and similar discrete fields as the working set. High-cardinality unique strings without a plan (every document id as the only key) blow up payload indexes; prose dumped into metadata is the anti-pattern Dataquest names explicitly.
  • Attribute indexes enabled. Weaviate’s indexFilterable (roaring bitmaps for match filters) and indexRangeFilters (numeric/date ranges) exist because an unindexed property makes every filter a scan. Other stores expose equivalent payload or scalar indexes — enable them on the fields you actually filter.
  • No silent nulls. A missing tenant_id or doc_type does not warn at query time; it simply never matches (Kandaanusha). Populate defaults deliberately or fail ingest.

How those fields get extracted and enriched is owned by metadata extraction. When a natural-language question should write the filter, that constructor/translator pattern is self-query retrieval — this page is the store execution half.

How do you implement metadata filtering?

Implementation is store configuration plus a filtered-recall check, not a new embedding model: declare the attributes, enable their indexes, pick the filter mode your engine exposes, and measure recall@k with production predicates on and off.

Weaviate attaches filters beside vector/hybrid search and selects ACORN or sweeping on the HNSW config; Azure AI Search exposes preFilter / postFilter / strictPostFilter; Milvus chooses standard vs iterative filtering; Pinecone passes a metadata filter object on query. Pin versions and verify defaults — Weaviate’s ACORN default dating from v1.34 is exactly the kind of volatile claim that drifts. Runnable end-to-end wiring belongs with building the pipeline; the parent stage map is indexing.

What is metadata filtering in vector search?

Metadata filtering restricts approximate nearest-neighbour search to chunks whose structured attributes match a predicate — for example tenant, date, document type, or language — so similarity runs inside an allow-list instead of hoping the embedding ranks those constraints. It is the index-layer half of filtered retrieval; how natural-language questions become those predicates is covered on /retrieval/self-query.

How do pre-filtering and post-filtering differ?

Post-filtering runs ANN on the full index first, then drops candidates that fail the metadata predicate — simple, but selective filters underfill top-k or return zero even when matches exist. Pre-filtering builds an allow-list from the metadata index first, then searches only survivors — correct top-k inside the slice in principle, but a naive graph walk can starve or fall back to brute force. Filter-aware / filtered HNSW bakes the predicate into traversal so eligible neighbourhoods stay navigable.

Why does filtered search miss documents I know exist?

Usually a selective post-filter, a fragmented filtered HNSW walk, or wrong/missing metadata excluded the gold chunk from the candidate set. Detect with recall(no-filter) − recall(with-filter) > 0 and by logging underfilled responses (returned count less than k when matches exist). The full ordered diagnosis lives on /failures/missing-document; this page covers the index mechanics.

What is ACORN / filtered HNSW?

Filtered HNSW walks the proximity graph while respecting a metadata allow-list so search does not treat filtering as a separate stage that blindly deletes candidates. Weaviate’s ACORN filterStrategy (default since v1.34 per Weaviate docs) uses multi-hop neighbour expansion when filtered-out nodes would otherwise break connectivity; sweeping is the older alternative. Other stores ship related ideas under names like single-stage filtering or Filterable HNSW.

What metadata must exist at ingest for filtering to work?

Typed fields you will predicate on — tenant_id, doc_type, created_at, language, ACL tags — must be written on every indexed chunk, with payload or scalar indexes enabled for those properties. Missing or null attributes silently never match. How fields are extracted and enriched is covered on /ingestion/metadata; how an LLM writes filters from natural language is on /retrieval/self-query.