Module 6: Metadata Filtering — the component almost nobody implements first but everyone ends up needing

Module 6: Metadata Filtering — the component almost nobody implements first but everyone ends up needing

Module description

Up to now you've optimized retrieval for quality: query optimization (M03), re-ranking (M04), hybrid search (M05). Each technique improves how well you find the right documents within the whole corpus. Metadata filtering attacks a different and complementary problem: searching fewer documents.

The idea is simple: if your corpus has 1M documents but your user is authenticated in the acme_corp workspace, there's no point searching the 950K documents from the other workspaces. Filter down to the 50K relevant ones first, then run the retrieval. The result: 10x lower latency, better precision (less noise), and — critically — security isolation between tenants.

This capsule introduces the module. You'll learn why metadata filtering isn't optional in multi-tenant systems, when to apply it in the pipeline (before or after retrieval), how to design the schema from the start so the filters you'll need are easy to express, and how to combine it with everything that came before.

By the end of this module you'll be able to:

  • ✅ Tell pre-filtering from post-filtering and pick the right one for your case
  • ✅ Design a metadata schema that covers the filters you'll need (without over-engineering)
  • ✅ Implement filters in ChromaDB with where clauses (equality, ranges, IN, AND/OR)
  • ✅ Isolate data by workspace_id or tenant_id as a multi-tenant security control
  • ✅ Apply time-based filters (recency) and semantic ones (tags, categories)
  • ✅ Integrate metadata filtering with the hybrid search from M05 for real production

Estimated module time: 3-4 hours (8 capsules).


Why metadata filtering matters more than it looks

Three situations that happen in RAG systems that ignored it early on:

Situation 1: data leakage between tenants

Your product has 50 customers. Each one has its own document corpus. Without metadata filtering, when customer A runs a query, the retrieval searches the whole corpus — including customer B's documents. If the queries and the embeddings are similar enough, A can see fragments of B.

"Compliance found that the chatbot cited a document from Customer C when Customer A asked about pricing. Reportable to the regulator."

This happens. It isn't theoretical. Without where={"tenant_id": "customer_a"}, there's no technical guarantee of isolation — you're just hoping the cosine similarity between tenants is low. When the corpora share vocabulary, it isn't low.

Situation 2: latency that grows with the corpus

Six months ago your corpus was 100K vectors. Today it's 5M (it grew with new docs from every customer). Retrieval latency went from 30ms to 250ms. The SLA started breaking.

"Why is the bot slow? It used to be instant."

The root cause: you're searching the whole corpus every time. If you filter by tenant first, each customer searches their subset (~100K vectors on average), and latency stays at 30ms even as the global corpus grows.

Situation 3: answers with outdated information

Your corpus has 5 years of documentation. The query is "how do I configure feature X?". The system returns a doc from 2021 describing the old, already-deprecated API. The user follows the instructions and fails.

"The bot is giving me wrong info. The API it says to use doesn't exist anymore."

Without a date filter, the old docs rank the same as the new ones. The fix: where={"created_at": {"$gte": one_year_ago}} to prefer recent content.

These three problems are not solved by better chunking, better reranking, or hybrid search. They're solved by metadata filtering.


The insight: filtering before search is always faster and safer

                              Without metadata filtering:

           Query
             ↓
    ┌──────────────────┐
    │ Vector DB        │
    │ 5M vectors       │ ← searches ALL of them
    │ (multi-tenant,   │
    │  all mixed)      │
    └──────┬───────────┘
           ↓
        Top-K
        (may include docs
         from other tenants —
         data leak risk)


                              With metadata filtering (pre-filter):

           Query + tenant_id
                ↓
    ┌──────────────────┐
    │ Filter by        │
    │ tenant_id        │ ← reduces to ~100K
    │                  │
    └──────┬───────────┘
           ↓
    ┌──────────────────┐
    │ Vector DB searches│
    │ the 100K subset   │
    └──────┬───────────┘
           ↓
        Top-K (from the tenant only)
        (10x lower latency,
         data leak impossible)

Pre-filter vs post-filter: the architectural decision

There are two ways to apply metadata filtering:

Pre-filter: filter the corpus first, then search.

# Pre-filtering (ChromaDB does this by default when you pass `where`)
results = collection.query(
    query_texts=[query],
    where={"tenant_id": "acme", "language": "es"},
    n_results=5,
)

Post-filter: search first, then filter the results.

# Post-filtering (manual)
all_results = collection.query(query_texts=[query], n_results=100)
filtered = [
    doc for doc in all_results["documents"][0]
    if doc.metadata["tenant_id"] == "acme"
]
top_5 = filtered[:5]

Which is better?

AspectPre-filterPost-filter
LatencyBetter (searches fewer vectors)Worse (searches everything, discards afterwards)
Recall (with restrictive filters)ExcellentPoor (the relevant ones can fall outside the top-100)
Multi-tenant securityGuaranteedPossible bug if the post-filter fails
ImplementationNative support in ChromaDB/PineconeManual, error-prone

Pre-filter is always the right choice, except in extreme cases. Capsule 02 covers why, with benchmarks.


Module roadmap

CapsuleTopicWhat you'll buildTime
01 (this one)Module introductionThe map, the motivation, the success criteria10-15 min
02Pre-filtering vs post-filteringA decision framework with benchmarks25-30 min
03Designing the metadata schemaA robust schema from the start30 min
04Filters with ChromaDB's whereThe complete syntax: equality, ranges, IN, AND/OR30-35 min
05Multi-tenant isolationSecure isolation by workspace + audit logging30-35 min
06Time-based + tag filteringRecency and fine-grained categorization25-30 min
07Integration with hybrid searchThe complete pipeline: filter → hybrid → rerank30 min
08Capstone projectAn end-to-end Metadata-Filtered RAG45-60 min

Total: 3-4 hours. It's one of the modules with the least technical complexity but the most operational impact.


Connection with the previous and following modules

Previous modules:
  ├─ M01: The RAG pipeline → metadata gets attached to each chunk during indexing
  ├─ M02: Chunking → each chunk carries metadata inherited from its parent document
  ├─ M03: Query Optimization → filters can be derived from the query (e.g. detect the language → filter)
  ├─ M04: Re-ranking → the re-rank runs over the filtered subset
  └─ M05: Hybrid Search → the filters apply before BM25 + semantic run in parallel

This module prepares you for:
  ├─ M07: Production with Pinecone → Pinecone supports metadata filtering natively
  └─ M08: RAG Evaluation → how to measure filtering's impact on the metrics

The "state of the art" production-ready RAG pattern (May 2026):

Query + user_context (tenant_id, language, date, etc.)
  ↓
Query optimization (M03) — optional
  ↓
Metadata filtering (M06) — shrink the search space
  ↓
Hybrid search (M05) — BM25 + semantic in parallel over the filtered subset
  ↓
Re-ranking (M04) — refine the top-K
  ↓
LLM generation

Each component reduces a different problem. Metadata filtering is the most operational one — without it, the others work against you at scale.


Expected improvement with metadata filtering

Over typical multi-tenant datasets:

MetricNo filterWith filter (tenant + date)Gain
Latency p95250ms30-50ms-80%
Precision@575%89%+14 pts
Recall@578%88%+10 pts
Data leak riskHIGHZEROcritical
Infra cost (RAM)High (a big index in RAM)The sameunchanged

The reading: the most important gain is NOT a quality metric — it's eliminating data leak risk. In multi-tenant systems, that's non-negotiable.


The module's limits: what we do NOT cover

  • IAM and cloud policies — separation at the infra level (separate vector DB instances per tenant) is a different pattern. Useful when metadata filtering isn't enough, but more expensive.
  • Engine-level optimization (custom ANN indexes) — efficient metadata filtering depends on backend features. We cover what to look for, not how to modify the engine.
  • Personalization based on learning-to-rank with a user profile — out of scope. That's personalized retrieval, not metadata filtering.
  • Specific GDPR/compliance work — filtering helps with the "right to access" and "data isolation", but full legal compliance is broader.

Prerequisites before starting the module

Make sure you have:

  • ✅ A working RAG pipeline with M01-M05 implemented (or at least M01 + a basic retrieval).
  • ✅ A decision on the vector DB you'll use — this module assumes ChromaDB, but the concepts apply to Pinecone, Weaviate, Qdrant.
  • ✅ Your own eval set of at least 30 queries with the expected metadata (e.g. query "X" should filter tenant_id=Y).
  • ✅ If your product is multi-tenant: working authentication that gives you a tenant_id per request. Without that, metadata filtering isn't secure.

Self-check before moving on

Before starting capsule 02, make sure you can answer:

  1. Why is metadata filtering NOT optional in multi-tenant systems?
  2. Name three problems metadata filtering solves that no other technique in the module can.
  3. What's the difference between a pre-filter and a post-filter, intuitively?
Answers
  1. Because without a tenant_id filter there's no technical guarantee of isolation between tenants. Cosine similarity can pull in documents from other customers if they share vocabulary. That's data leakage — a compliance risk, a legal risk, a risk of losing customer trust. No retrieval, rerank or hybrid algorithm fixes it. Only the explicit filter does.

  2. (a) Multi-tenant isolation: no retrieval-quality algorithm prevents another tenant's docs from showing up; only the tenant_id filter guarantees it. (b) Latency that grows with the corpus: chunking, rerank and hybrid don't scale any better — they all search the whole corpus. Only filtering shrinks the search space. (c) Recency: no algorithm "prefers" new documents on its own; you need an explicit filter on created_at or a date boost.

  3. Pre-filter shrinks the corpus BEFORE searching — the vector DB only considers vectors that match the filter. Post-filter searches everything, then discards what doesn't match the filter. Pre-filter is faster (it searches less), safer (the discarded ones were never considered) and better on recall (the top-K comes from the relevant subset). Post-filter is slower, prone to losing relevant docs (the top-100 may not include everything from the subset), and vulnerable to bugs.


Next step: Capsule 02

The next capsule digs into pre-filtering vs post-filtering with concrete benchmarks. You'll see, with numbers, how much post-filter loses on latency and recall, and why pre-filter is always the right choice except in very specific cases. It's the architectural foundation that defines everything that follows.


Resources

  1. ChromaDB — Metadata Filtering — Official documentation on where filters
  2. Pinecone — Metadata Filtering — Filters in production
  3. Multi-tenancy in Vector Databases — Isolation patterns
  4. LangChain — Retrieval Filters — Integrations with filtering
  5. Anthropic — Contextual Retrieval — Additional context for retrieval
  6. GDPR Article 32 — Security of Processing — Why technical isolation matters legally

Estimated time: 10-15 minutes Next: 02-pre-filtering-vs-post-filtering.md


Final notes

This is the last module of "Phase 2 — Retrieval techniques" in the path. When you close it, you'll have implemented the complete production-ready RAG pattern:

Query optimization (M03)
  ↓
Metadata filter (M06)
  ↓
Hybrid search (M05) — semantic + BM25 with RRF
  ↓
Re-ranking (M04) — cross-encoder or LLM
  ↓
LLM generation

Each module added a component. Combined, they take precision@5 from ~70% (the RAG MVP in M01) up to 90-95% in production.

The next module (M07) adds no techniques — it takes everything you built and deploys it to Pinecone (a managed vector DB) to scale from 1M docs to 10M+ with production SLAs. It's the transition from "working demo" to "service running 24/7".

And the last one (M08) covers how to measure that everything you built actually works: golden datasets, Ragas metrics, regression testing, CI/CD for RAG. It's the piece that separates "we think it works" from "we know it works".


An important reminder

From this module on, every project assumes your system is production-ready from day 1. That means:

  • Multi-tenant isolation by design (not optional).
  • Automated tests that run in CI.
  • Audit logging for compliance.
  • Documentation a Tech Lead can sign off on.

If your current RAG doesn't meet this, go back and fix that before moving forward. It's easier to do now than after an incident.