Vocabulary for this course

The terms the chapters assume, in plain words. You can open this from any chapter.

approximate nearest neighbour (ANN)
A search that checks only part of the index, so it is fast but may miss some of the true closest matches.
collection
A named group of vectors that share one configuration and are searched together.
cosine similarity
The dot product of two unit-length arrows, from 1 when they point the same way to -1 when they oppose.
dot product
Multiply matching numbers in two lists and add the results; it grows with both length and agreement.
embedding
A list of numbers that places a piece of text in a space where similar meanings sit close together.
embedding model
The trained program that turns text into vectors, with one fixed output width.
hybrid search
Running a word-matching search and a meaning-based search together, then combining their ranked lists into one.
index
A structure built ahead of time so the database can find nearby items without comparing against every one.
magnitude
The length of a vector, found by squaring its numbers, adding them and taking the square root.
metadata filter
A rule that keeps only the items whose stored facts, such as date or owner, match before or during the search.
model card
A short document describing a trained system: its data, its scores and its known limits.
normalised vector
A vector divided by its own length, so it has length one and only direction remains.
quantisation
Storing each number with less precision, which shrinks memory and usually costs a little accuracy.
recall
The share of the truly closest items that a search actually returned.
reranking
Scoring a short list of results again with a slower, more careful method and putting the best first.
similarity function
A rule that scores two vectors and returns a number saying how alike they are.
top-k
The shortlist a search returns: its best few results, cut off at a number chosen in advance.
training loss
The average error an embedding model makes on the text pairs it is trained to match.
unit length
A vector whose magnitude is exactly 1, which makes a dot product read as pure agreement.
vector
An ordered list of numbers, where each position is one measured property.

Vector databases from the ground up

Metadata filtering

A filter decides which vectors are eligible before ranking. Where you apply it decides whether you return k results or k attempts, and a selective filter can make an ANN index useless.

Part 4 of 10

Intermediate15 min readBack to Courses

A filter is a constraint, not a cleanup

Metadata is everything you know about a vector that is not the vector: tenant, product, language, date, access level. A filtered search asks for the k nearest vectors among the ones that satisfy a predicate (a true-or-false test on those fields). The predicate is part of the query, not a step after it. That sounds like an implementation detail. It is actually the difference between a search that answers a question and one that answers a different question and then apologises.

The distinction matters because top-k is a promise about the result set. If you rank first and filter second, you have promised ten results and delivered three, or none, while the correct answer sat at position eleven the whole time. The user does not see the eleven. They see an empty page and conclude the product has no data.

Filters also come in two flavours that are easy to confuse. A relevance filter removes results the user would not want; a security filter removes results the user must not see. The first can be approximate, the second cannot. Treating them as the same kind of clause is how a search feature becomes an access-control bug with a relevance bug on top.

Three places to apply it

Pre-filtering selects the eligible ids first and searches only those. It is exact and predictable, and it turns into a scan when the filter is not selective. Post-filtering is the bug above. Filtered traversal pushes the predicate into the index walk, which is what the good databases do. Each has a cost profile, and the profile changes with the shape of the filter.

A filtered graph is the hard case. HNSW needs neighbours to walk through, and a filter that removes most of them can cut the path to the answer. Some engines fall back to a full scan when the filter is too selective, which is the right behaviour and worth knowing about. Others return an empty list and leave you to find the reason in a support ticket.

A filter that travels with the query

from qdrant_client import QdrantClient, models

client = QdrantClient(url="http://localhost:6333")

hits = client.query_points(
    collection_name="docs",
    query=query_vector,
    limit=5,
    query_filter=models.Filter(
        must=[
            models.FieldCondition(
                key="tenant",
                match=models.MatchValue(value="acme"),
            )
        ]
    ),
).points

Filter before you slice

Compare the two functions for the tenant acme with k=3, then with k=2. Notice that post-filtering returns fewer rows than asked for, while pre-filtering keeps the promise.

Output
Click "Run" to execute your code

Selectivity is the number that matters

A filter that keeps ten percent of the corpus is easy. A filter that keeps one percent, or one tenant out of ten thousand, is where indexes stop helping. The eligible set is small enough that scanning it is cheaper than walking a graph that mostly leads to excluded points. The crossover is not a fixed number; it depends on how the engine stores the filter and how the vectors are laid out.

The practical answer is to know the selectivity (the share of rows a filter keeps) of your common filters and to test the worst one. If a filter can match zero rows, decide now whether an empty result is correct or whether it should fall back to a scan. A filter that silently returns nothing looks the same as a corpus that has nothing. Both look like a bug to the person typing the query.

A metadata filter is not an access control. A tenant boundary enforced by a filter is one forgotten clause away from a leak; use a separate collection, namespace or database for data that must not mix.

Partition, then filter

The cheapest filter is the one the query planner never has to evaluate, because the data was already separated. Partition by tenant or by month and the filter becomes a choice of partition, which is a lookup rather than a predicate. That is also the boundary that survives an incident: a missing clause cannot cross a partition that was never queried.

Inside a partition, keep the filters that are about relevance: language, product, a date range. Those narrow the candidate set without carrying a security promise, and they are safe to change as the product changes. The test is whether getting the filter wrong would show the wrong document or merely a worse one. Only the first belongs in the security boundary.

A useful exercise is to list every filter the product exposes and write down its selectivity and what getting it wrong would show. “Language = German” keeps perhaps a tenth of the corpus and shows a worse result if it is wrong. “Tenant = acme” keeps a thousandth and shows another customer’s data if it is wrong. The two belong in different layers, reviewed by different people.

When the filter empties the index

Run the four counts, then raise nprobe until the filtered ANN returns all five. Note how much of the corpus it has to touch to do it.

Output
Click "Run" to execute your code

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

Why filter before ranking?