Slice a vector index by structured criteria — tenant, document type, time-window — without losing recall.
Library aisle + index card. You go to the right aisle (tenant: ‘acme’; type: ‘pdf’) and then read the index cards in that aisle. Pre-filter goes to the aisle first; post-filter skims the entire library and throws away non-acme results.
pre-filter : filter -> each matching -> ANN over subset
post-filter : ANN over all -> filter -> top-K
pre-filter preserves recall; post-filter can return fewer than K
Pre-filter: push structured predicates into the vector search; the ANN runs only on rows that match. Recall and latency stable. Post-filter: run ANN over the whole collection, then drop non-matching rows. Cheaper conceptually; prone to returning fewer than K matching items when filters are tight. Pick pre-filter for tight filters (5 percent of corpus matches); post-filter for very loose filters (90 percent matches).
# pgvector: pre-filter via WHERE before ANN
SELECT id FROM chunks
WHERE tenant_id = :tenant
AND ts >= NOW() - INTERVAL '30 days'
ORDER BY embedding <=> :query_vec
LIMIT 5;
Pre-filter pushes metadata into the index plan — the ANN only scans matching rows.
# vector DBs with server-side metadata filter
pinecone
results = index.query(vector=q, top_k=5,
filter={'tenant':'acme', 'source_kind':'pdf'})
Server-side filter is one round-trip; client-side post-filter is two.
# pattern to avoid: post-filter collapse
# 1) ann over all -> 1000 candidates
# 2) filter -> only 12 left -- way less than K
# Fix: pre-filter to 1.5x K, then ANN over that subset
Pre-filtering to 1.5 times K before ANN guarantees you can return top K after filtering.
Post-filtering after ANN — the index returns 1000 candidates and most are dropped, returning only a few of the requested K.
Pre-filter most of the time; it preserves recall and saves work. Post-filter only when filters are loose (<5 percent selectivity makes pre-filter less efficient).