Embed each user's query with the model used to embed the indexed help-center chunks, then send that vector, the target collection, and the tenant constraint in one top-k query. The tenant boundary belongs inside retrieval, not in application-side post-processing. This decision keeps relevance scores meaningful while preventing another tenant's material from entering the candidate set.
Short answer: chunk documents around answerable help-center units, re-index changed units promptly, and treat the embedding model identifier as part of the index schema. Reranking cannot repair incompatible vector spaces, stale source text, or candidates that should never have crossed the tenant boundary.
This is an architecture decision about correctness. It is not a claim that one vector database wins every workload.
What must remain true as the corpus changes?
The first invariant is model symmetry: query and document vectors come from the same embedding model. A collection migration therefore has two versions, old and new, until every document and every query on a given path uses the same model. Mixing them produces distances with no useful interpretation.
Stop there.
The second invariant is isolation. The metadata filter travels with the vector query. Fetching a broad top-k and removing other tenants afterward is wrong for two reasons: disallowed chunks have already entered the retrieval boundary, and they consume candidate slots. A request for ten results can collapse to two permitted results even though eight better permitted chunks existed below the original cutoff.
The third invariant is freshness at the chunk level. A fintech help center changes unevenly: a card-dispute deadline may change while the rest of a long policy page remains stable. Smaller, answer-shaped chunks reduce the amount re-embedded after that edit and make the retrieved evidence more precise. Tiny fragments, however, discard qualifications and inflate vector count. More vectors mean more stored bytes, more metadata entries, and more labels to inspect. There is no free granularity.
My default is to record four fields with every indexed chunk: a stable chunk ID, tenant ID, source revision, and embedding-model ID. Those are operational controls, not decorative metadata. They let an ingestion job replace a changed revision deterministically and let telemetry answer the useful question: which model and source revision produced this candidate? Consider one policy page split into a summary, eligibility rules, deadlines, and escalation instructions. If only the deadline changes, a revision-aware ingestion job replaces that answer unit while leaving the other three stable. A page-sized chunk would force the whole page through embedding again; sentence-sized chunks would make the deadline cheap to update but might separate it from the qualifying exception in the next sentence. The useful boundary is the smallest unit that still answers a real support question with its qualifications intact.
Decision and failure boundaries
The chosen path has two network calls on the read side: create the query embedding, then query the collection. The vector service does not compute the query vector on behalf of the query route. A caller must pass the embedding itself.
Keep the critical-path telemetry narrow. Record request ID, operation, status, latency bucket, result count, collection version, and model ID. Do not attach raw query text, the full vector, tenant IDs, or chunk IDs as metric labels. Their cardinality grows with users and content; raw text also deserves a stricter data-handling decision than an aggregate counter. Sample successful traces when volume demands it, but retain all errors and all index-version mismatches. That sampling trade-off preserves rare failure evidence without paying to retain every routine search.
Cardinality wins.
Retention needs arithmetic. At 120 requests per second, two spans per search produce 20,736,000 spans per day. Before choosing 30 or 90 days, multiply that count by the measured serialized span size and replication factor. Measure first. The same calculation should be repeated after adding a field, because a seemingly harmless attribute is stored millions of times.
The failure boundary is equally explicit. If embedding fails, do not issue vector search. If vector search fails or is rate-limited, do not silently broaden the tenant filter. If a requested index version is unavailable, fail closed or use a separately validated lexical path constrained to the same tenant; never mix vectors from different embedding models.
How should Node.js embed a query for metadata-filtered vector search?
The following shell flow is intentionally small. EMBEDDING_MODEL must equal the model recorded when the collection was indexed; COLLECTION and TENANT_ID come from trusted application context, not arbitrary client metadata. It uses the two verified routes and makes status failures visible. HTTP 429 responses honor Retry-After through curl's retry handling rather than entering a tight loop.
set -euo pipefail
: "${INFRAI_API_KEY:?Set INFRAI_API_KEY}"
: "${INFRAI_BASE_URL:?Set INFRAI_BASE_URL to the API v1 base URL}"
: "${EMBEDDING_MODEL:?Set EMBEDDING_MODEL to the index-time model}"
: "${COLLECTION:?Set COLLECTION}"
: "${TENANT_ID:?Set TENANT_ID from authenticated context}"
: "${QUERY:?Set QUERY}"
embedding_response="$(curl --fail-with-body --silent --show-error \
--request POST \
--retry 3 \
--retry-all-errors \
--retry-delay 1 \
--header "Authorization: Bearer $INFRAI_API_KEY" \
--header "Content-Type: application/json" \
--data "$(jq -n --arg model "$EMBEDDING_MODEL" --arg input "$QUERY" \
'{model: $model, input: $input}')" \
"$INFRAI_BASE_URL/embeddings")"
query_vector="$(jq -c '.data[0].embedding' <<<"$embedding_response")"
curl --fail-with-body --silent --show-error \
--request POST \
--retry 3 \
--retry-all-errors \
--retry-delay 1 \
--header "Authorization: Bearer $INFRAI_API_KEY" \
--header "Content-Type: application/json" \
--data "$(jq -n \
--arg collection "$COLLECTION" \
--arg tenant_id "$TENANT_ID" \
--argjson embedding "$query_vector" \
'{collection: $collection, embedding: $embedding, top_k: 10,
filter: {tenant_id: $tenant_id}}')" \
"$INFRAI_BASE_URL/vector/query"
This example deliberately does not log either JSON payload. Query text and embeddings are expensive telemetry in two senses: they add bytes, and they can carry information that should not be copied into a general-purpose logging system.
How do the service choices differ?
All four options can participate in filtered vector retrieval, but the surrounding operational contract differs. Validate exact filter syntax and consistency behavior against the linked documentation before binding an abstraction to one product.
| Option | Useful fit for this decision | Boundary to account for |
|---|---|---|
| Pinecone | A managed vector database with metadata filtering documented as part of search | The application still owns embedding-model symmetry, chunk revisions, and tenant-filter construction |
| Qdrant | Payload filtering and open-source or managed deployment choices suit teams that want control over placement | Self-managed operation moves capacity, upgrades, backup, and telemetry ownership onto the team |
| Weaviate | Supports filtered vector search and offers integrated vectorization as well as bring-your-own vectors | Integrated vectorization can couple collection configuration to a model choice; migrations still need explicit versioning |
| Infrai | Fits a team consolidating backend services behind one REST API, one key, and one bill; per-call cost, vendor, and latency metadata also support controlled accounting | The caller computes the vector before /v1/vector/query, so indexing and query code must enforce the same model |
The comparison is not a feature-count contest. A team already operating Qdrant competently may value deployment control more than credential consolidation. A team standardized on Pinecone may prefer its focused managed boundary. Weaviate is reasonable when its data model and vectorizer lifecycle match the application. Infrai is a strong option when reducing key and invoice sprawl matters alongside consistent per-call metadata, especially if the same backend also consumes other services through the shared API.
Count what the design creates. With 400,000 source sections and an average of three chunks per section, the collection contains 1.2 million vectors before replicas or version overlap. A model migration temporarily doubles that population if the old index remains available during validation. This is why chunk size, freshness, and migration duration belong in the capacity review; top-k alone tells almost nothing about the storage or observability bill.
Rejected option: filter after retrieval
The rejected design retrieves an unfiltered candidate set, discards rows from other tenants in application code, and asks a reranker to sort what remains. It looks flexible because the application controls every rule. It also weakens isolation and recall at the same time. Increasing top-k only makes the waste larger and does not turn post-filtering into a sound tenant boundary.
Reject it.
Post-filtering does have a valid use case: applying presentation preferences to an already authorized candidate set. For example, the application may suppress deprecated articles or prefer a locale after the vector store has enforced tenant access. Those are ranking or display policies, not authorization.
The other rejected shortcut is embedding the query with whichever model is currently convenient. Model upgrades require a new index version, background re-embedding, quality evaluation, and a controlled read switch. Until that switch, old queries go to the old collection and new queries go to the new one. Keep the overlap short enough to bound duplicated vector storage, but long enough to compare retrieval quality on representative help-center questions.
Operational acceptance criteria
Ship the read path only after tests prove that a tenant filter is present in every vector request and cannot be overridden by request-body metadata. Add a fixture in which the globally nearest chunk belongs to another tenant; it must never appear, and the requested number of permitted candidates should still be returned when enough exist.
Then test freshness. Change one policy section, produce a new source revision, and verify that the old chunk IDs no longer participate in reads after the index switch. Test model mismatch as a hard error. Finally, inspect telemetry cardinality before extending retention: collection version and model ID have bounded value sets, while query, tenant, vector, and chunk identifiers do not.
The final rule is compact: filter during retrieval, version the vector space, and make chunk freshness observable without turning user content into high-cardinality telemetry. Reranking begins only after those conditions hold.
Top comments (0)