Vector-DB design and metadata filtering
A toy vector store just ranks everything by similarity and returns the closest. A production vector store does something crucial first: it filters by metadata, then ranks the survivors.
Why filter first? Because "nearest" isn't enough. You only want this tenant's documents, only the ai namespace, only docs from this year. Searching across everything and ranking by similarity alone returns confident, well-matched, *wrong-context* results. The order is the whole lesson:
- 1. Filter on metadata — drop anything that fails the
whereclause ({"ns": "ai", "year": 2026}). - 2. Score only the survivors with your similarity function.
- 3. Top-k — sort by score descending and take the best
k.
The metadata match is an AND across every key: every key in where must match, and an empty where matches everything. For determinism, break score ties by id (so the same query always returns the same order).
Here similarity is a toy overlap score and the store is an in-memory list. In production this is pgvector or Pinecone, where the metadata filter becomes a WHERE clause pushed down *before* the approximate-nearest-neighbour search — same order, real engine.
Syntax
def passes(meta, where):
# AND across every key; empty where matches all
return all(meta.get(k) == v for k, v in where.items())
def query(vec, k, where):
candidates = [d for d in DOCS if passes(d["meta"], where)] # 1. filter
scored = [(similarity(vec, d["vec"]), d["id"]) for d in candidates] # 2. score
scored.sort(key=lambda p: (-p[0], p[1])) # score DESC, id ASC
return [doc_id for _score, doc_id in scored[:k]] # 3. top-kWorked examples
The metadata filter is an AND; empty where matches all
def passes(meta, where):
for key, val in where.items():
if meta.get(key) != val:
return False
return True
print(passes({"ns": "ai", "year": 2026}, {"ns": "ai"})) # one key matches
print(passes({"ns": "ai", "year": 2025}, {"ns": "ai", "year": 2026})) # year differs
print(passes({"ns": "ai"}, {})) # empty where
# Output:
# True
# False
# TrueFilter on metadata FIRST, then rank by similarity
def similarity(a, b):
return len(set(a) & set(b)) # toy overlap score
DOCS = [
{"id": "d1", "vec": ["ai", "agent", "loop"], "meta": {"ns": "ai", "year": 2026}},
{"id": "d2", "vec": ["ai", "rag"], "meta": {"ns": "ai", "year": 2025}},
{"id": "d3", "vec": ["soil", "plants"], "meta": {"ns": "home", "year": 2026}},
{"id": "d4", "vec": ["ai", "agent", "rag"], "meta": {"ns": "ai", "year": 2026}},
]
def passes(meta, where):
return all(meta.get(k) == v for k, v in where.items())
def query(vec, k, where):
cands = [d for d in DOCS if passes(d["meta"], where)]
scored = [(similarity(vec, d["vec"]), d["id"]) for d in cands]
scored.sort(key=lambda p: (-p[0], p[1]))
return [doc_id for _s, doc_id in scored[:k]]
# only ns=ai, year=2026 survive the filter (d2 is 2025, d3 is home)
print(query(["ai", "agent", "rag"], 2, {"ns": "ai", "year": 2026}))
# Output:
# ['d4', 'd1']Tie-break by id keeps results deterministic
def similarity(a, b):
return len(set(a) & set(b))
docs = [
{"id": "b", "vec": ["ai"]},
{"id": "a", "vec": ["ai"]},
]
# both score 1 — without the id tie-break the order would be arbitrary
scored = [(similarity(["ai"], d["vec"]), d["id"]) for d in docs]
scored.sort(key=lambda p: (-p[0], p[1]))
print([doc_id for _s, doc_id in scored])
# Output:
# ['a', 'b']Common mistakes & gotchas
Ranking before filtering
If you score everything first and only then drop the wrong-namespace docs, you've done expensive similarity work on documents you were always going to throw away — and in a real ANN index you can't easily filter after the fact. Filter first, score the survivors. It's both correct and cheaper.
Forgetting the tie-break
scored.sort(key=lambda p: -p[0]) sorts by score alone, so two equal-score docs come back in whatever order they happened to be in. That makes tests flaky and results non-reproducible. Add a secondary key (p[1], the id) so equal scores always order the same way.
Using == on floats for the metadata match
Metadata filters here compare exact values (a namespace string, an int year), which is fine. But never ==-compare the *similarity scores* or any computed float for equality — floating-point rounding means 0.1 + 0.2 != 0.3. Compare scores with < / > for ranking, and use a tolerance if you must test closeness.
Why it matters
Retrieval quality makes or breaks a RAG system, and most retrieval bugs aren't bad embeddings — they're missing filters returning confidently-wrong context (last year's price, another tenant's data). Designing the store as filter-then-rank, with deterministic ordering, is the difference between a demo and something you can trust in production.