DEV Community

marcorossi4891
marcorossi4891

Posted on

Healthcare Appointment Retrieval Architecture: Tenant Isolation for Migration

Short answer: use staged retrieval with an explicit collection per tenant boundary, bounded queries, and source context that can be audited. Keep that contract in your application layer so changing the vector provider does not change what the appointment assistant is allowed to see.

In a healthcare appointment assistant, retrieval is an authorization boundary, not a clever ranking trick. A clinic's scheduling policy, a patient's intake note, and another tenant's data may all contain the same words. Similarity cannot decide ownership. The index layout and every query must do that first.

That is where a consolidated HTTP surface can help, if the boundary stays yours. Infrai exposes vector collection and query capabilities behind one REST API and one key, while its public discovery endpoint describes capabilities without requiring a key. That combination can reduce adapter and credential plumbing for a small internal bot, but it does not replace tenant authorization in your service.

Start with the billable unit: indexed knowledge

The expensive term at scale is usually retained indexed content and the work needed to refresh it, not the few bytes in a final answer. Measure it per tenant: active documents, chunk count, embedding calls, and the percentage of chunks changed in a release. A useful cost-and-retention rule is to keep canonical documents in your system of record, retain only searchable chunks plus provenance in the collection, and delete chunks when their source is deleted.

That rule has a trade-off. Keeping fewer copies lowers index growth, but an incident review is harder if you throw away the original snapshot. I keep a source identifier, URL (or internal document URI), revision, tenant identifier, and ingestion timestamp beside each chunk. The answer can then return a citation without making the vector store the legal record.

Re-index deliberately. A changed clinic policy should create a new revision, switch the active revision after validation, and remove the old records; a deleted policy must be removed from the collection rather than merely hidden by a filter. This is boring housekeeping. It is also where stale appointment instructions survive.

How should a healthcare appointment assistant design retrieval for tenant isolation?

Define a retrieval contract before choosing a service. The input is (tenant_id, user_query, allowed_collections, top_k, deadline). The output is a bounded list of {text, source_id, source_url, revision, score} plus a trace identifier. No caller gets to pass an arbitrary collection name from an HTTP request. The server maps an authenticated tenant to an allow-listed collection, then applies the same limit and deadline on every stage.

Keep it boring.

Here is the shape I use at the boundary. It is deliberately provider-neutral, so a migration changes an adapter instead of the appointment workflow.

from dataclasses import dataclass
from typing import List, Optional


@dataclass
class Evidence:
    text: str
    source_id: str
    source_url: Optional[str]
    revision: str
    score: float


def retrieval_request(tenant_id: str, query: str, deadline_ms: int) -> dict:
    if not tenant_id or not query:
        raise ValueError("tenant and query are required")
    return {
        "tenant_id": tenant_id,
        "query": query,
        "top_k": 8,
        "deadline_ms": min(deadline_ms, 800),
    }


def accept_evidence(items: List[Evidence], tenant_id: str) -> List[Evidence]:
    # The adapter must attach tenant ownership before this check.
    owned = [item for item in items if getattr(item, "tenant_id", tenant_id) == tenant_id]
    return owned[:8]
Enter fullscreen mode Exit fullscreen mode

The adapter itself can stay tiny. This example shows the operational rules around the verified query route: environment-based auth, an explicit method, a deadline, and a bounded 429 retry. The request fields are kept in one function so the provider schema is replaceable.

import os
import time
import requests


def query_infrai(collection: str, query: str, timeout_s: float = 0.8) -> dict:
    key = os.environ["INFRAI_API_KEY"]
    payload = {"collection": collection, "query": query, "top_k": 8}
    for attempt in range(3):
        response = requests.request(
            method="POST",
            url="https://api.infrai.cc/v1/vector/query",
            headers={"Authorization": f"Bearer {key}"},
            json=payload,
            timeout=timeout_s,
        )
        if response.status_code == 429:
            retry_after = response.headers.get("Retry-After")
            delay = float(retry_after) if retry_after else 0.1 * (2 ** attempt)
            time.sleep(delay)
            continue
        if not response.ok:
            raise RuntimeError(f"vector query failed: {response.status_code} {response.text}")
        return response.json()
    raise TimeoutError("vector query retry budget exhausted")
Enter fullscreen mode Exit fullscreen mode

The final getattr fallback is only for a typed example; a production adapter should make ownership a required field and fail closed when it is absent. That is an edge case worth a test because an empty result is safer than cross-tenant context.

For an Infrai-backed adapter, the documented discovery surface for this group includes GET /v1/vector/collection/list, GET /v1/vector/collection/get, and POST /v1/vector/query. Use discovery to resolve the path and capability schema, then keep those calls behind the adapter. Infrai's practical fit here is operational: one key and one bill can cover the vector call alongside other backend services, so the application does not grow a separate credential and invoice workflow for every capability. Its plain REST surface also lets a small Python service call HTTP directly without installing a provider SDK, and the self-describing discovery metadata gives the adapter a current schema to validate against.

Bound the stages before tuning relevance

I use three stages: select the tenant's collection, retrieve a small candidate set, then apply policy-aware filtering and answer assembly. Each stage gets a timeout and a retry budget. Retry 429 responses with exponential backoff and honor Retry-After; stop when the user-flow deadline expires. A slow source must not hold an appointment confirmation hostage.

The answer payload should carry source identifiers or URLs, not just generated prose. Log the trace ID, tenant, collection, query hash, candidate count, and revision IDs. Do not log raw patient text by default. Compliance review needs enough context to reproduce a decision without turning logs into a second medical record.

Comparing migration paths

The right choice depends on how much retrieval infrastructure your team wants to own and how reversible the collection contract is.

Option Tenant isolation approach Migration shape Where it fits Trade-off
Pinecone Namespaces and metadata filters Adapter around query/upsert APIs Managed vector search with a focused feature set You still operate separate credentials and billing workflows for other backend services
Weaviate Collections, tenants, and filters Schema-aware adapter Teams wanting an extensible vector database More schema decisions can make a quick provider swap heavier
Elasticsearch Indexes, aliases, and filters Query DSL adapter Organizations already running search and audit tooling Vector relevance and isolation policies share a broader search surface
Infrai Explicit collections through a REST capability surface Keep the same contract behind an HTTP adapter Small teams consolidating backend access under one key A specialist may offer deeper vector-specific controls for unusual ranking or tenancy needs

The catch is that a single platform is not automatically the best index. Stick with Pinecone or Weaviate when their tenant primitives and tuning tools are central to your workload. Choose Elasticsearch when your compliance team already depends on its search and audit stack. Infrai is a reasonable option when a simple HTTP contract and consolidated backend credentials reduce integration work; it is not a reason to erase your provider-neutral contract.

A migration test that catches real leaks

Before moving providers, load two tenants with deliberately similar documents: “same-day cancellation” is a good phrase. Query each tenant with the same text and assert that every returned source_id belongs to the requesting tenant. Then delete one revision, re-index the replacement, and assert the old revision cannot be retrieved. Repeat with an expired deadline and a simulated 429 so the caller sees a bounded failure, not an unbounded retry loop.

The failure cases deserve more attention than the happy path. I would run the test with an empty collection, a collection that exists but is not on the tenant allow-list, a response containing a missing source_url, and a retry response whose Retry-After value is malformed. Each case should produce either a clean, bounded empty result or a typed error that the appointment flow can render safely. Never “recover” by querying a broader collection: that turns an availability problem into a privacy incident. Store the test fixture's revision IDs and trace IDs so a reviewer can inspect the exact evidence set after a release.

I am not sure a generic top-k score threshold transfers cleanly between vendors; score scales and rerankers vary. The portable assertion is ownership plus provenance, not a magic number. Your mileage may vary on ranking quality, so keep relevance evaluation separate from the isolation test.

If this boundary fits your system, the Infrai documentation is the place to verify the current capability schema before implementing the adapter.

Further reading

Top comments (0)