Unit 05.01: Metadata filters similarity cannot replace
Some conditions are exact. Similarity cannot express an exact condition, only degrees of topical closeness.
Hard boundaries belong in filters
"Refunds only" and "since 2025" are boolean. A vector for 2023 sits close to one for 2026, and that closeness is worse than useless for a date bound.
The code searches with and without a filter.
from langchain_core.documents import Document
from langchain_core.embeddings import DeterministicFakeEmbedding
from langchain_core.vectorstores import InMemoryVectorStore
DOCS = [
Document("Refunds within 7 days.", metadata={"section": "refunds", "year": 2026}),
Document("Refunds within 30 days.", metadata={"section": "refunds", "year": 2023}),
Document("Shipping takes 3 days.", metadata={"section": "shipping", "year": 2026}),
]
store = InMemoryVectorStore.from_documents(DOCS, DeterministicFakeEmbedding(size=64))
print("no filter:")
for d in store.similarity_search("refund", k=3):
print(f" [{d.metadata['section']:8} {d.metadata['year']}] {d.page_content}")
print("\nfilter first (current refunds only):")
for d in store.similarity_search(
"refund", k=3,
filter=lambda doc: doc.metadata["section"] == "refunds"
and doc.metadata["year"] >= 2025):
print(f" [{d.metadata['section']:8} {d.metadata['year']}] {d.page_content}")
# "Refunds only" and "since 2025" are exact conditions. Similarity has no way to
# express a hard boundary -- a vector for 2023 sits close to one for 2026 -- so
# these belong in a filter applied before ranking, not in the query text.
Unfiltered, the top results include a stale 2023 refund policy and a shipping document. Filtered, the ineligible documents never compete for a slot at all.
That is a quality gain as well as a correctness one. Every slot occupied by an ineligible document is a slot a good document could have had, so filtering usually improves answers more than switching embedding models would - and costs a lambda.
The mistake this prevents
Filters are not the only thing similarity is bad at. Exact identifiers - product codes, error numbers, ticket references - retrieve poorly by embedding, because the geometry blurs ERR-4417 and ERR-4471 together exactly as it blurs synonyms. Keyword search handles those precisely, which is why production systems commonly run both and merge the results. Use lexical matching for identifiers and vector search for language.
The mistake is post-filtering: retrieve top-k, then drop the ineligible ones. You end up with fewer than k results, the dropped slots are wasted, and any access-controlled document was ranked - meaning read - before being removed.
Takeaway
Put exact conditions in a filter applied before ranking. Similarity turns a hard guarantee into a soft preference, and post-filtering wastes slots while still touching restricted content.
