# Metadata Filtering by [kirupa](https://www.kirupa.com/me/index.htm) | filed under [AI Fundamentals](https://www.kirupa.com/ai_fundamentals/index.htm) Leah lives in Toronto. She bought a tent from Trailhead three weeks ago, and it turns out to be a bit too small for her and her very large dog. She asks the help assistant: *How long do I have to return a tent?* Trailhead has three return policies sitting in its help center: - the **US return policy**, current: 60 days - the **Canada return policy**, current: 30 days - the **US return policy from 2023**, archived: 30 days (These are made-up numbers for our made-up store, but the shape of the problem is very real.) All three documents are about the same thing, written in nearly the same words. When the retrieval system turns Leah's question into an [embedding](https://www.kirupa.com/ai_fundamentals/embeddings.htm) and looks for the closest matches, all three land right on top of each other. Whichever one wins by a hair goes to the model, and the model will answer confidently from it. If that's the US policy, Leah gets told she has 60 days. She doesn't. Here is what that looks like in Leah's chat: ![Trailhead's help assistant chat window. Leah asks, How long do I have to return a tent? The assistant replies, You have 60 days to return your tent. A handwritten note flags that reply: Not in Canada!](https://www.kirupa.com/ai_fundamentals/images/metadata_filtering_chat_returns.png) What's tricky here is that nothing about the search is broken. Every result really is relevant to her question. The issue is that the right answer depends on facts that aren't in her question at all: where she lives, and which version of the policy is in effect today. Here is what the search sees: ![Leah, with a Canadian flag, asks How long do I have to return a tent? Three return policies score almost the same: US current at 0.91, marked wrong country, US 2023 archived at 0.90, and Canada current at 0.89.](https://www.kirupa.com/ai_fundamentals/images/metadata_filtering_policies.png) Onwards! ## Similarity Can't See Labels Similarity search answers one question: ***Which text means something close to this text?*** It's very good at that. It just has no idea which country a document applies to, whether it was replaced last year, or which product line it covers, unless those facts happen to change the meaning of the words, which they usually don't. Fortunately, we usually know those facts already. When we load documents into the index, we can attach structured labels to every chunk. These labels are called **metadata**. For Trailhead's return policies, they might look like this: ```json { "doc": "Return policy", "region": "CA", "status": "current", "doc_type": "policy", "updated_at": "2026-02-01" } ``` Here is a chunk wearing its tags: ![A chunk of the return policy with four tags attached: region CA, status current, doc_type policy, and updated_at 2026-02-01.](https://www.kirupa.com/ai_fundamentals/images/metadata_filtering_tagged.png) **Metadata filtering** means using those labels as hard rules during search. Before we care about how similar anything is, we say: only consider chunks where `region` is `CA` and `status` is `current`. Similarity then ranks whatever survives. That split is the useful way to think about it. Filters handle the facts that are simply true or false, and similarity handles the fuzzy question of meaning. ## Filter Before, or Filter After? There are two places we could apply the filter, and they behave very differently. **Post-filtering** runs the similarity search first, grabs the top 10 results, and then throws away anything that doesn't match the filter. It's easy to bolt on. The trouble is that our top 10 might be mostly US documents, because the US help center is bigger. After filtering, we might be left with one Canadian result, or none at all. The good Canadian chunk at #14 never had a chance, because it was never in the top 10 to begin with. **Pre-filtering** (also called filtered search) applies the filter during the search. We're effectively saying ***find the 10 closest chunks among only the Canadian, current ones***. We get a full set of results, and every one of them is allowed. Here are the two side by side: ![Two panels. Filter after: the top 10 results by similarity are nine crossed-out US tiles and one CA tile, leaving 1 usable result because the best Canadian chunk was number 14. Filter before: the search only considers current Canadian chunks, so all 10 tiles are CA and there are 10 usable results.](https://www.kirupa.com/ai_fundamentals/images/metadata_filtering_before_after.png) Pre-filtering is almost always what we want. The catch is that the fast lookup structures inside vector databases are built for ***find nearby points***, and adding ***but only these points*** makes their job harder. Most vector databases support filtered search well today, but it's worth checking how yours does it, especially when a filter is very narrow, like a single customer's documents out of millions. ## Where the Filter Values Come From A filter is only as good as the values we put into it. For Leah, there are a few places we can get `region: CA`: 1. We can use what we already know about her. She's logged in, and her account says Canada. This is the best source because it's trustworthy and costs nothing. 2. We can ask the app. The help center might have a country picker, or Leah might be browsing the Canadian site. 3. We can read it from her question. If she had asked ***What's the return window in Canada?***, a model could pull `region: CA` out of the text. This is sometimes called self-querying. The third option is handy, but treat what the model extracts as a suggestion. Check it against the list of values that actually exist (there's no region called ***Canadia***), and never let it override facts you got from a trustworthy source. And never use a model-extracted value for anything security-related. If the filter decides who is *allowed* to see a document, it has to come from the logged-in user's identity, which is the topic of [Permission-Aware Retrieval](https://www.kirupa.com/ai_fundamentals/permission_aware_retrieval.htm). ## Don't Filter Yourself Into a Corner Filters can go wrong in the other direction too. If we filter by `region: CA` and `product_line: tents` and `updated_at` after last week, we may get zero results, and the model will either say ***I don't know*** or, worse, make something up. A few habits help here: - Use filters for facts that truly decide correctness (country, current versus archived, product), and leave fuzzier preferences to ranking. - When a filtered search comes back empty, relax the least important filter and try again, and let the answer say so (***I couldn't find a Canada-specific policy, but here's the general one***). - Make sure every chunk actually *has* the metadata. A chunk with no `region` tag silently disappears from every region-filtered search. That last one bites people. Metadata gets added to new documents but not to the old ones, and nobody notices until a whole section of the help center becomes invisible. ## Conclusion Leah's question had three perfectly relevant answers, and only one of them was correct for her. Similarity search couldn't tell them apart, because the thing that separated them (country and version) wasn't in the meaning of the words. By tagging each chunk with metadata and filtering on it during the search, we narrowed the field to current Canadian policies first, then let similarity pick the best match among those. So basically, filters answer ***is this allowed to be an answer?*** and similarity answers ***which allowed answer fits best?*** Keeping those two jobs separate makes both of them easier. Metadata filtering decides which chunks are *eligible*. Once we have a set of eligible chunks, the next step is getting the very best ones to the top of the list, which is what [Reranking](https://www.kirupa.com/ai_fundamentals/reranking.htm) is for. Leah, by the way, has 30 days, and she's well inside them. The dog is getting his bigger tent.