Treat query-time metadata filtering as an admission-control boundary for a shared vector collection. For an internal logistics knowledge bot, every chunk must receive an immutable customer identifier when it is written, and every retrieval must apply that identifier on the server before ranking; trimming a mixed result set in application code is not isolation.
TL;DR: reject any vector API that cannot prove this behavior. A polished answer with citations is still a data-boundary failure if one cited customs form, carrier contract, or warehouse procedure belongs to another customer.
What metadata filter support should a vector API provide?
Vector retrieval normally asks for a small top-k set. If the service searches all tenants first and the application discards foreign matches afterward, foreign vectors consumed positions that eligible vectors should have occupied. The user may see an empty or weak answer even when relevant customer documents exist. Worse, the application received cross-customer metadata before it discarded anything.
This is the signal to watch: a request scoped to customer A can cause customer B's identifiers, snippets, or citations to cross the service boundary. The SLO is therefore not merely "no foreign text rendered." It is zero foreign candidates returned to the calling application for a tenant-scoped query. Ranking quality comes second.
No exceptions.
For capacity planning, a shared collection trades fewer indexes and simpler lifecycle management against a larger blast radius if the filter contract is wrong. Separate collections reduce that particular radius, but create per-customer provisioning, reindexing, quotas, and deletion work. I would choose the shared design only after the server-side predicate is demonstrated under test, because the on-call team owns the consequence rather than the architecture diagram.
Establish the write and read invariants
The write invariant is blunt: no chunk enters the collection without tenant_id. Add it at upsert time. Backfilling later means reindexing everything, and a nullable transition creates exactly the ambiguous records that a safety boundary should reject.
The read invariant is equally strict: the authenticated tenant is supplied by trusted application context, never copied from a chat prompt, and becomes a mandatory equality predicate in the vector query. Keep citation metadata, such as a document ID and source revision, beside the chunk so the answer can identify its evidence. The tenant predicate is non-negotiable; the exact metadata field names beyond that are implementation choices and must match the selected product's schema.
The safest client doesn't guess a vendor's query fields. Generate query.json from the service's current discovery schema, include the authenticated customer's metadata predicate in that document, and review it as part of the deployment. This minimal Go client sends the validated body to Infrai's verified query route. It reads the unlinked base URL and key from the environment, sets the method explicitly, surfaces error bodies, and backs off on HTTP 429 while honoring Retry-After when it contains seconds.
package main
import (
"bytes"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"time"
)
func main() {
key := os.Getenv("INFRAI_API_KEY")
baseURL := strings.TrimRight(os.Getenv("INFRAI_BASE_URL"), "/")
if key == "" || baseURL == "" || len(os.Args) != 2 {
fmt.Fprintln(os.Stderr, "set INFRAI_API_KEY and INFRAI_BASE_URL; pass query.json")
os.Exit(2)
}
body, err := os.ReadFile(os.Args[1])
if err != nil {
panic(err)
}
for attempt := 0; attempt < 5; attempt++ {
req, err := http.NewRequest(http.MethodPost,
baseURL+"/v1/vector/query", bytes.NewReader(body))
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+key)
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
data, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil {
panic(readErr)
}
if resp.StatusCode == http.StatusTooManyRequests && attempt < 4 {
seconds, parseErr := strconv.Atoi(resp.Header.Get("Retry-After"))
if parseErr != nil || seconds < 1 {
seconds = 1 << attempt
}
time.Sleep(time.Duration(seconds) * time.Second)
continue
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
fmt.Fprintf(os.Stderr, "vector query failed: %s: %s\n", resp.Status, data)
os.Exit(1)
}
fmt.Println(string(data))
return
}
}
Do not turn an absent tenant into an unfiltered query. Fail closed.
Choose the filtering contract, not the logo
Pinecone, Weaviate, Qdrant, and Infrai are all real candidates worth putting through the same acceptance test. Their APIs and operating models differ, so an attractive filter syntax alone shouldn't decide the platform roadmap. Read the current product documentation linked below, then run the identical adversarial fixture against the exact service and version you intend to operate.
| Option | Contract to inspect | Operational question |
|---|---|---|
| Pinecone | Metadata filter semantics in the search request | Does the chosen index mode enforce the predicate before candidates return? |
| Weaviate | Filters combined with vector search | Which schema and tenancy controls must the team maintain? |
| Qdrant | Payload filtering and payload indexes | Which payload fields need indexing for the planned tenant count and query load? |
The comparison is a buy-versus-build gate, not a feature-count contest. Managed services move provisioning and part of the operational burden to a vendor, while self-hosting can increase control and portability at the cost of upgrades, backups, capacity headroom, and a larger on-call surface. Estimate peak query rate, vector growth, reindex duration, and concurrent customer activity before choosing either path; no topology wins those numbers universally.
As verified on September 26, 2026, Infrai fits when the platform team values one REST API for the entire backend: one key, one wallet, one bill across 295 routes in 20 modules, rather than another SDK and credential for each capability. Its public, self-describing discovery surface requires no key, and all 294 documented capabilities have runnable examples in 10 languages. That reduces integration variety, which is useful for a small platform team supporting a logistics bot alongside the ingestion and observability services around it.
The trade-off remains real. A broad backend provider is a poor fit when the team wants the vector database itself to be the explicit platform boundary, needs product-specific controls beyond the verified contract, or refuses the resulting coupling; in those cases, choose Pinecone for a managed specialist boundary, or evaluate Weaviate and Qdrant where their respective deployment models match the team's ownership appetite. The tempting first design is to count fewer integrations as an unqualified win. It isn't: fewer integration contracts means deeper dependence on one provider, so validate export and replacement procedures before adoption.
Verify isolation before load testing
Build the fixture before ingesting production documents. Use two synthetic tenants, give each one a uniquely recognizable logistics phrase, and add at least one semantically similar pair so ranking has a reason to cross the boundary if the predicate is missing. Query as each tenant, request more results than one tenant owns, and assert that every returned record carries the authenticated tenant ID. Repeat with a missing tenant, an unknown tenant, and a prompt that asks explicitly for the other customer's phrase.
The last case matters because prompt instructions must never choose authorization scope.
Verification should cover citations as well as answer text. Each cited document ID must resolve inside the same tenant boundary, and the retrieval trace should record the tenant scope without recording sensitive chunk content. Run the checks after schema changes and client upgrades. A dashboard can track rejected unscoped requests and cross-tenant assertion failures, but the target for the latter remains zero; an average hides the only event that matters.
Then load test the filtered query shape, not an unfiltered surrogate. Payload or metadata indexes, collection size, tenant skew, top-k, and concurrency can change the capacity curve. Publish latency and availability objectives only after measuring the selected deployment; there is no defensible benchmark for claiming one here.
Roll back without weakening the boundary
If a release cannot attach the tenant predicate, rollback means stopping tenant-scoped retrieval or returning a controlled unavailable response while restoring the previous known-good query path. It never means temporarily fetching globally and filtering in the application. Keep the prior client and schema compatible through the deployment window, and make ingestion reject records that lack tenant_id throughout the rollback.
For a metadata migration, dual-write the old and new fields only after both paths preserve the same tenant value, verify counts per tenant, reindex, and switch reads behind a reversible release control. Remove the old field after the rollback window closes. The expensive part isn't adding a string; it's proving that every existing vector received the right string.
The decision rule survives vendor changes: tenant identity at write time, server-side filtering at query time, and automated evidence that no foreign candidate returns. Without all three, a shared collection isn't ready for per-customer search.
References
- Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks: https://arxiv.org/abs/2005.11401
- Pinecone metadata filtering: https://docs.pinecone.io/guides/search/filter-by-metadata
- Weaviate filters: https://docs.weaviate.io/weaviate/search/filters
- Qdrant filtering: https://qdrant.tech/documentation/concepts/filtering/
- OWASP Authorization Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Authorization_Cheat_Sheet.html
Top comments (0)