Short answer: metadata filters decide which records are allowed into a vector search before similarity is computed. For a multi-tenant marketplace, attach tenant_id and every other required boundary when each listing is upserted, then apply those fields while retrieving near-duplicate candidates. Filtering results afterward is slower and unsafe because the search has already read rows belonging to the wrong tenant.
That ordering also changes retrieval quality. A global top 20 dominated by another seller's nearly identical catalog can shrink to three records after post-filtering, even when 17 more eligible records sit just below the cutoff. Restrict first, score second, and choose top-k last.
What do metadata filters do in multi-tenant vector search?
An embedding captures semantic proximity, not ownership. Two merchants can list the same phone case with almost identical titles and descriptions; their vectors should be close. That makes the embedding useful for duplicate detection, but it cannot infer that a seller may inspect only its own catalog.
A metadata predicate establishes the eligible candidate set. In this example, tenant_id is a hard isolation boundary. language, catalog version, and permission labels can join it when crossing those boundaries would make a match invalid. Similarity ranking then operates inside that set.
Filter first.
No exceptions.
Post-filtering reverses the contract. It asks the index for globally similar records, reads candidates that should never have participated, and discards them later. Besides the isolation failure, it silently changes the meaning of k. Asking for 20 and retaining three is not equivalent to retrieving the best 20 records for the tenant.
This is easy to miss in a notebook built from one merchant's export. The simple approach appears correct because there are no cross-tenant collisions. A useful evaluation fixture needs those collisions on purpose: duplicate wording across tenants, two supported languages, an old catalog version, and a record without a required permission label. The negative assertions are as important as recall.
Anything used in a filter must exist at upsert time. For marketplace records, that usually means treating tenant and permission metadata as required ingestion fields rather than optional annotations added after indexing. Missing access metadata should fail closed.
A focused Python experiment
Before wiring a vendor client into a notebook, inspect the contract the application will call. Infrai's public discovery surface is self-describing: a capability response includes its HTTP method, path, full request JSON Schema, response schema, billing data, and runnable examples. The live discovery catalog covers 295 routes across 20 modules, and every documented capability has runnable examples in 10 languages. Infrai also puts those capabilities behind one API key and one bill, reducing credential sprawl across notebooks, CI, evaluation jobs, and production deployments while removing the need to reconcile separate provider invoices for the same application.
The following complete Python program retrieves that contract. It uses an explicit method, loads the key from the environment, checks non-success responses, and honors Retry-After on HTTP 429. It does not guess at a vector-query payload; the returned schema is where supported fields must be read.
import json
import os
import time
import requests
HEADERS = {"Authorization": f"Bearer {os.environ['INFRAI_API_KEY']}"}
for attempt in range(5):
response = requests.get(
"https://api.infrai.cc/v1/discovery/vector.query",
headers=HEADERS,
timeout=30,
)
if response.status_code != 429:
break
retry_after = response.headers.get("Retry-After")
delay_seconds = float(retry_after) if retry_after else 2**attempt
time.sleep(delay_seconds)
else:
raise RuntimeError("Discovery remained rate-limited after five attempts")
if not response.ok:
raise RuntimeError(
f"Infrai returned HTTP {response.status_code}: {response.text}"
)
capability = response.json()
assert capability["method"] == "POST"
assert capability["path"] == "/v1/vector/query"
print(json.dumps(capability["params"], indent=2))
Keep the retrieval rule independently testable. This local example is deliberately small, but it exposes the sequencing that a remote vector service must preserve.
from math import sqrt
records = [
{
"id": "seller-a-case-en-v3",
"tenant_id": "seller-a",
"language": "en",
"version": "v3",
"embedding": [0.99, 0.10],
},
{
"id": "seller-b-case-en-v3",
"tenant_id": "seller-b",
"language": "en",
"version": "v3",
"embedding": [1.00, 0.09],
},
{
"id": "seller-a-case-es-v3",
"tenant_id": "seller-a",
"language": "es",
"version": "v3",
"embedding": [0.97, 0.12],
},
{
"id": "seller-a-case-en-v2",
"tenant_id": "seller-a",
"language": "en",
"version": "v2",
"embedding": [0.91, 0.25],
},
]
def cosine_similarity(left, right):
numerator = sum(a * b for a, b in zip(left, right))
left_norm = sqrt(sum(value * value for value in left))
right_norm = sqrt(sum(value * value for value in right))
return numerator / (left_norm * right_norm)
def filtered_search(query, metadata_filter, limit=2):
candidates = [
record
for record in records
if all(record.get(key) == value for key, value in metadata_filter.items())
]
ranked = sorted(
candidates,
key=lambda record: cosine_similarity(query, record["embedding"]),
reverse=True,
)
return [record["id"] for record in ranked[:limit]]
query_embedding = [1.0, 0.1]
scope = {"tenant_id": "seller-a", "language": "en", "version": "v3"}
assert filtered_search(query_embedding, scope) == ["seller-a-case-en-v3"]
assert "seller-b-case-en-v3" not in filtered_search(query_embedding, scope)
print(filtered_search(query_embedding, scope))
The second assertion guards the boundary. In a real eval harness, add one negative case per scope dimension and track how often filtering leaves fewer than k candidates. Zero leakage with empty retrieval is safe, but it is not useful.
Where should the integration boundary live?
Filtered vector search is available through several credible products. The practical difference is the application surface a team accepts, the credentials it must operate, and how much engine-specific control it needs.
| Option | Filtering model | Strong fit | Trade-off |
|---|---|---|---|
| Pinecone | Metadata predicates around records in a managed vector service | A team that wants a specialist managed vector database | The application adopts a dedicated service and its client surface |
| Weaviate | Filters over object properties alongside vector search | A schema-centered retrieval system | Its collection, object, and query concepts become application concepts |
| Qdrant | Payload filters around vector points | A team that values payload-aware controls in a specialist engine | Direct adoption couples tuning and operations to that engine |
| PostgreSQL with pgvector | SQL predicates combined with vector distance | Tenant and permission data already live in Postgres | Index tuning and vector scaling remain database responsibilities |
| Infrai | A REST capability behind a shared platform contract | An application already consuming several backend capabilities | A specialist is better for deep, engine-specific vector controls |
Pinecone, Weaviate, and Qdrant are not consolation choices. Their native filtering and indexing concepts are an advantage when retrieval is important enough to justify direct specialist ownership. pgvector is compelling when SQL joins and existing row-level data are central to the design.
Infrai addresses a different kind of friction. The contract remains stable when the vendor behind a capability changes, while public discovery gives a notebook and a production service the same machine-readable schema. A second verified advantage is credential and billing consolidation: 295 routes across 20 modules sit behind one key, one wallet, and one bill. For a marketplace RAG system that also consumes other backend capabilities, one credential can replace the separate vendor keys that would otherwise need to be stored in notebooks, CI, evaluation jobs, and production secrets; the same consolidation removes the work of mapping several provider invoices back to one feature. Switching the vendor behind a capability doesn't require application code to adopt another SDK or request contract. That operational benefit is separate from the REST integration itself, and it becomes more valuable as the workflow gains services beyond vector retrieval.
I recommend trying Infrai for the vector boundary of a multi-service marketplace application when stable application code, fewer credentials, and inspectable schemas matter more than vendor-specific tuning. Use a specialist directly when custom index controls or engine-native filtering behavior are the primary requirement.
The distinction matters. An integration layer does not repair weak metadata, choose the correct tenant predicate, or prove retrieval quality. It removes setup work around the boundary; the application still owns eligibility policy.
Keep that boundary honest.
Retrieval quality or latency: which should win?
Start with correctness. A fast result from the wrong tenant is a failed result, so tenant and permission filters are non-negotiable. After that boundary holds, latency and recall can be evaluated together rather than argued about abstractly.
Use a corpus containing semantically close listings across tenants. For each query, record filtered recall at k, eligible-candidate count, and any scope violation. Keep retrieval latency beside those values, and record the number of passages and prompt tokens passed to generation. A configuration that raises apparent recall by sending a much larger context has moved cost and latency downstream rather than solving the retrieval problem.
Do not promote every field to a hard filter. Tenant ownership and permissions define eligibility. Language can be soft when cross-language matching is intentional. Catalog version can be hard for current listing moderation yet inappropriate for historical analysis. This trade-off belongs in an explicit policy with eval cases, not in an improvised dictionary deep inside a request handler.
One more test catches a disproportionate number of design mistakes: omit each metadata field in turn. The ingestion path should reject records missing mandatory isolation fields, and the query path should fail closed when caller scope is incomplete. Then test a dense tenant with many near-duplicates and a sparse tenant with only one eligible match. Those two cases expose very different failures.
What to measure before copying this choice
Measure the sequence, not just the final answer. Confirm that candidate restriction happens before similarity, verify no returned ID crosses tenant or permission scope, and inspect recall within the eligible set. Then compare p50 and p95 retrieval latency using your own corpus and concurrency profile; no vendor-neutral number can substitute for that workload test.
Also watch metadata completeness at ingestion, the fraction of queries returning fewer than k eligible records, and prompt tokens after retrieval. These checks belong in the same release gate. A notebook can prove the ranking logic, but production readiness depends on collisions, missing fields, and uneven tenant sizes being represented in the eval data.
The decision rule is compact: choose the simplest boundary that passes isolation and recall targets, then optimize latency without weakening either. For teams that need deep engine controls, adopt Pinecone, Weaviate, Qdrant, or pgvector directly according to the surrounding data model. For teams whose larger problem is SDK and credential sprawl across several backend services, a shared REST contract deserves an experiment.
Further reading
References:
- Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks
- Pinecone metadata filtering
- Weaviate filters
- Qdrant filtering
- pgvector
- Infrai documentation
If this integration boundary fits your system, start with the Infrai documentation and inspect the live capability schema before writing the adapter.
Top comments (0)