Metadata filters: who may see which documents
A metadata filter is a rule that limits retrieval to chunks whose metadata matches, so a customer never reaches a staff-only document.
Last updated: 28 Sep, 2026 · LlamaIndex 0.14
A cutoff in Refusing when nothing fits: similarity cutoffs decides whether a chunk is relevant. A filter decides whether the asker is allowed to see it at all. A staff-only file now joins the help centre in its own folder.
# Refund approvals (staff only)
Refunds over 200 need a team lead's approval before they are paid. Refunds for customers flagged for abuse must be sent to the fraud team.Marking each file with an audience
with_audience tags each file staff or customer by its folder. recursive=True reads both folders. The audience key is excluded from embedding and from the prompt: it controls access, it is not content.
from llama_index.core import Settings
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
Settings.embed_model = HuggingFaceEmbedding(model_name="sentence-transformers/all-MiniLM-L6-v2")
import os
from llama_index.core import SimpleDirectoryReader, VectorStoreIndex
def with_audience(path):
audience = "staff" if "/staff/" in path else "customer"
return {"file_name": os.path.basename(path), "audience": audience}
documents = SimpleDirectoryReader(".", recursive=True, required_exts=[".md"], file_metadata=with_audience).load_data()
for document in documents:
document.excluded_embed_metadata_keys = ["audience"]
document.excluded_llm_metadata_keys = ["audience"]
index = VectorStoreIndex.from_documents(documents)Filtering to the customer audience
MetadataFilters holds one or more MetadataFilter rules. Each names a key, a value and an operator; FilterOperator.EQ keeps only chunks whose audience equals customer.
from llama_index.core.vector_stores import MetadataFilter, MetadataFilters, FilterOperator
customers_only = MetadataFilters(
filters=[MetadataFilter(key="audience", value="customer", operator=FilterOperator.EQ)]
)View the code here
# Lamps
The LMP-204 desk lamp has a known cable fault. Stop using a lamp with a damaged cable and we will replace it free of charge.
All lamps come with a two year guarantee against electrical faults.
Bulbs are not covered by the refund policy once they have been used.
The LMP-310 floor lamp needs a bulb with an E27 fitting, which is sold separately.
# Refunds
You can get a full refund within 30 days of delivery. The money goes back to the card you paid with within 5 working days of us receiving the item.
Items bought in a sale can be refunded too, but the delivery charge is not returned.
To start a refund, open the order in your account and choose Return an item. Print the label and drop the parcel at any post office.
Personalised items cannot be refunded unless they arrive damaged.
# Delivery
Standard delivery takes 3 to 5 working days and is free on orders over 40.
Express delivery arrives the next working day if you order before 2pm. It costs 6.
We deliver to the mainland only. Parcels to islands take 2 extra working days.
If a parcel has not arrived after 10 working days, contact us and we will send a replacement.
Retrieving with and without the filter
retriever = index.as_retriever(similarity_top_k=2)
print("no filter: ", [n.metadata["file_name"] for n in retriever.retrieve("Who approves a large refund?")])
from llama_index.core.vector_stores import MetadataFilter, MetadataFilters, FilterOperator
customers_only = MetadataFilters(filters=[MetadataFilter(key="audience", value="customer", operator=FilterOperator.EQ)])
retriever = index.as_retriever(similarity_top_k=2, filters=customers_only)
print("customer: ", [n.metadata["file_name"] for n in retriever.retrieve("Who approves a large refund?")])no filter: ['refund-approvals.md', 'refunds.md'] customer: ['refunds.md', 'lamps.md']
Reading the filtered results
- Unfiltered, the staff file comes first. A customer asking about large refunds would get the internal approval and fraud rules.
- Filtered, the staff file is not a candidate. It cannot be retrieved however similar it is, because the filter removes it before scoring matters.
- The filter runs at retrieval. A chunk that is never retrieved can never be quoted, which is why permissions belong here, not in the prompt.
No filter vs a customer filter
| No filter | Customer filter | |
|---|---|---|
| Candidates | Every chunk, staff included | Customer chunks only |
| Top result | refund-approvals.md | refunds.md |
| Risk | Staff rules leak to customers | Staff rules cannot be reached |
When to filter by metadata
- Some documents are for staff, some for customers, and the two must never mix in an answer.
- Different teams or tenants share one index and each may see only their own data.
- You want per-request access decided by the logged-in role, not by the model.
ExactMatchFilter still imports in 0.14, but the docs steer to MetadataFilter with an operator, which is what this lesson uses.Related
- Previous: Refusing when nothing fits: similarity cutoffs
- Next: Structured output: a typed answer with Pydantic
- Make a filter for
staffand ask the same question. - Add a third audience,
partner, and a document for it. - Use the customer filter in a query engine with
ExtractiveLLM.
Slow is fine. Stopping is the only problem.