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

Multi-tenant operations

A tenant is a promise about blast radius, not a column. Shared filters, per-tenant collections and separate clusters trade cost for isolation, and every one of them needs quotas, a delete path and a test with two tenants.

Part 9 of 10

Advanced16 min readBack to Courses

Isolation is a promise about blast radius

A tenant is not a column. It is a promise that one customer’s data, load and mistakes cannot reach another’s. The design decides how much of that promise the database enforces and how much your code has to.

The three common shapes are a shared collection with a tenant filter, a collection per tenant, and a database or cluster per tenant. They sit on a line from cheap and shared to expensive and isolated. The right point depends on the contract you signed rather than on the size of the corpus.

Shared is the default because one index serves everyone: one backup, one rebuild, one set of settings to tune. The price is that a single missing filter is a cross-tenant leak, and one large ingest is everyone’s latency. Isolation is bought with operational work, not with a keyword. A tenant that leaves takes its share of the index with it, and a tenant that grows changes the shape of the cluster for everyone.

Make the scope impossible to omit

Make the scope an argument the query cannot be built without. A data-layer function takes the tenant id first, resolves it from the logged-in request, and builds the filter itself. That is harder to misuse than a client that feature code is trusted to filter. The function can also log the tenant with the query, which makes an incident review possible without guessing.

Defence in depth is worth the duplication here. Use a payload filter in the vector database and row-level security in Postgres (a rule that hides other tenants’ rows). Add a credential that can only see one tenant where the product allows it. Each layer assumes the one above it failed.

Scope the derived copies too. A cache keyed by the query text alone can leak across tenants without touching the index. So can a reranker that sees the global candidate list, or a log line with a document body. The test is the same: two tenants, identical queries, and a check that the result sets never overlap.

A filter the caller cannot forget

from qdrant_client import QdrantClient, models

client = QdrantClient(url="http://localhost:6333")
client.create_payload_index(
    collection_name="documents",
    field_name="tenant_id",
    field_schema=models.PayloadSchemaType.KEYWORD,
)

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

The filter that must not be optional

Run both searches and count the rows that cross a tenant boundary. Then add a third tenant and confirm the naive search leaks its documents too.

Output
Click "Run" to execute your code

Noisy neighbours and quotas

A shared cluster makes every tenant sensitive to every other tenant’s traffic. A bulk re-embedding job competes with live search for the same CPU and memory. A large delete triggers compaction while someone else is measuring p99 (the slow end of the range). A fair scheduler should also age requests, or a tenant that is always busy will still win.

Quotas are the control: vectors stored, queries per second, ingestion bytes per hour, and a maximum request size. Enforce them at the edge so abusive traffic never reaches the index, and again in the engine so a direct client cannot bypass the edge. The two limits should agree. An edge that allows more than the engine is a queue with a polite name.

Fairness is separate from quotas. A tenant under its limit can still hog a queue, so schedule requests round-robin (one per tenant in turn) or by weight, and keep bulk ingestion on its own queue. Measure p99 per tenant; a global p99 is an average that hides the customer being starved.

A tenant filter that lives in a handler is one refactor away from a breach. Put it in the data access layer and prove it with a test that queries as tenant A and asserts tenant B never appears.

Delete, export and migrate

Offboarding is a delete with evidence. Delete by tenant, count what remains, and check the backups, snapshots, caches and exported files that the index does not own. A deletion that leaves a copy in last week’s snapshot is not a deletion. Keep a list of every place the tenant appears, because the index is rarely the only one.

Per-tenant encryption keys make the hard case easier: destroying the key shreds the data everywhere it was copied, including backups, without rewriting every segment. It is the one operation that survives an incomplete list of copies.

Migrations run tenant by tenant. Re-embed a pilot tenant, compare recall and cost, then move the rest in batches with a dual-write window and a rollback path. Onboarding needs the same care. A new tenant should get its index settings, quotas and retention (how long data is kept) from a template rather than from a runbook someone remembers.

Fair scheduling under a burst

Run the two schedules, then give acme forty requests and compare the average wait per tenant. Decide which schedule you would put in front of an interactive search endpoint.

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

What is the main risk of one shared collection with a tenant filter?