LlamaIndexllama-index-core 0.14 · Python 3.10+
Dashboard
0%
1
Curious builder0 XP earned · 300 to level 2
0 daysFinish a lesson to begin
Badge collection0 of 6 unlocked
30 small wins to finish your pathNext lesson →

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.

markdown
# 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.

python
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.

python
from llama_index.core.vector_stores import MetadataFilter, MetadataFilters, FilterOperator

customers_only = MetadataFilters(
    filters=[MetadataFilter(key="audience", value="customer", operator=FilterOperator.EQ)]
)
Project files used on this pageThis lesson builds on a project from earlier lessons. The code below imports these files. Click a file to see its code, or follow the link to the lesson that wrote it. To run the code yourself, keep them in the same folder.
View the code here
help/lamps.md
# 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.
help/refunds.md
# 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.
help/delivery.md
# 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

Example
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?")])

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 filterCustomer filter
CandidatesEvery chunk, staff includedCustomer chunks only
Top resultrefund-approvals.mdrefunds.md
RiskStaff rules leak to customersStaff 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.
Filters run on the server, from the role
The filter must come from the logged-in user's role set by your application, never from something the user can type. The older ExactMatchFilter still imports in 0.14, but the docs steer to MetadataFilter with an operator, which is what this lesson uses.
Try it yourself
  • Make a filter for staff and 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.