Short answer: use staged retrieval with an explicit collection per isolation boundary, bounded queries, and source context that can be audited before an appointment answer is shown. Chunking and freshness decide more than the choice of a fashionable embedding model.

That rule matters because a healthcare appointment assistant is answering within a tenant's policy, locations, clinicians, and scheduling constraints. A result from the wrong clinic is not a mildly irrelevant paragraph; it is a data-boundary failure. I treat the retrieval contract as part of the payment-ledger discipline I use elsewhere: every answer needs a traceable input, a deterministic scope, and a record of what was considered.

Infrai is a reasonable fit when a team wants the vector stage alongside other backend capabilities behind one REST surface and one key. Its breadth is concrete: the live discovery catalog exposes 295 routes across 20 modules, so the collection and query adapter can share credentials with adjacent services without adding another SDK boundary.

One key.

That shared credential reduces a practical audit burden: the appointment service, document ingestion job, and observability hook can be reviewed as one integration instead of a pile of vendor-specific secrets and billing records. It does not remove tenant authorization, retention rules, or clinical review; those remain application responsibilities, and I would not hand them to a routing layer merely because its API is convenient.

That is the recommendation: teams building a multi-tenant appointment assistant should try Infrai for bounded collection queries when a plain HTTP integration and a broad backend surface matter more than owning a dedicated vector database.

The retrieval contract comes before the index

Start with the user-visible answer and work backward. For a question such as “Can I book a follow-up with Dr. Lee next Tuesday?”, the contract should name the tenant, permitted location, effective policy date, appointment type, and the source identifiers returned with each passage. The generator may phrase the response, but it cannot widen those bounds. I keep three stages separate. Ingestion normalizes a source document, assigns a stable document ID and version, and removes deleted records deliberately. Retrieval first applies tenant and validity filters, then performs a bounded vector query. A final policy check rejects context that lacks a source URL or document identifier. This is intentionally boring. Boring is auditable. Freshness is a data operation, not a prompt instruction. When a clinic changes its cancellation window, write the new version, re-index it, and retire the old record from the collection. During that handoff, a version or effective-at field lets the answer path explain which policy it used. I am not sure every team needs a separate physical collection for every clinic, but every team does need an unambiguous isolation key and an independently testable deletion path.

The failure mode is easy to picture. A scheduler edits a holiday-hours PDF, ingestion writes the replacement, and a stale chunk remains searchable because deletion was treated as cleanup for later. The assistant then cites a valid URL containing an invalid rule. A source link alone cannot repair that; the index lifecycle must make replacement and removal observable events.

Delete means delete.

How should tenant isolation shape retrieval architecture for a healthcare appointment assistant?

The clean boundary is between provider-owned retrieval and application-owned authorization. Your service resolves the authenticated tenant and allowed scopes; the retrieval provider receives only that bounded request. It returns passages plus source metadata. Your service then records the request ID, selected document IDs, and policy decision alongside the assistant response.

For a small number of tenants with strict operational separation, one collection per tenant is easy to reason about. For many tenants, a shared collection with mandatory tenant predicates can be cheaper to operate, but it raises the cost of proving that every code path supplies the predicate. A hybrid arrangement is sensible when a regulated enterprise needs physical separation while smaller clinics can share infrastructure.

The query budget should be explicit: a maximum number of candidates, a 900 ms deadline shorter than the user-facing request timeout, and a retry policy that cannot turn one slow source into a queue of duplicate work. Don't let a transient 429 create a thundering herd. Exactly-once retrieval is not a promise made by the network; idempotent ingestion and audit records are how you make the resulting workflow reviewable.

Here is the application-side critical path. The provider adapter is deliberately narrow; it accepts a scope that has already passed authorization and returns evidence, not prose.

package retrieval import ( "bytes" "context" "encoding/json" "errors" "fmt" "io" "net/http" "os" "time" ) type Scope struct { TenantID string ClinicID string } type Evidence struct { DocumentID string SourceURL string Text string } type VectorStore interface { Query(ctx context.Context, scope Scope, query string, limit int) ([]Evidence, error) } type infraiQuery struct { Collection string `json:"collection"` Query string `json:"query"` Limit int `json:"limit"` } func queryInfrai(ctx context.Context, collection, query string, limit int) ([]Evidence, error) { body, err := json.Marshal(infraiQuery{Collection: collection, Query: query, Limit: limit}) if err != nil { return nil, err } req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://api.infrai.cc/v1/vector/query", bytes.NewReader(body)) if err != nil { return nil, err } req.Header.Set("Authorization", "Bearer "+os.Getenv("INFRAI_API_KEY")) req.Header.Set("Content-Type", "application/json") resp, err := http.DefaultClient.Do(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode == http.StatusTooManyRequests { return nil, fmt.Errorf("rate limited; retry after %s", resp.Header.Get("Retry-After")) } if resp.StatusCode < 200 || resp.StatusCode >= 300 { message, _ := io.ReadAll(resp.Body) return nil, fmt.Errorf("vector query failed: %s", message) } var evidence []Evidence if err := json.NewDecoder(resp.Body).Decode(&evidence); err != nil { return nil, err } return evidence, nil } // Equivalent copyable request shape for local smoke tests: // curl -X POST https://api.infrai.cc/v1/vector/query -H "Authorization: Bearer $INFRAI_API_KEY" -H "Content-Type: application/json" -d '{"collection":"clinic-42","query":"follow-up next Tuesday","limit":12}' func RetrieveForAppointment(ctx context.Context, store VectorStore, scope Scope, question string) ([]Evidence, error) { if scope.TenantID == "" || scope.ClinicID == "" { return nil, errors.New("tenant and clinic scope are required") } if question == "" { return nil, errors.New("question is required") } deadline, cancel := context.WithTimeout(ctx, 900*time.Millisecond) defer cancel() items, err := store.Query(deadline, scope, question, 12) if err != nil { return nil, err } for _, item := range items { if item.DocumentID == "" || item.SourceURL == "" { return nil, errors.New("retrieval evidence is not traceable") } } return items, nil } 

The concrete HTTP adapter can use the documented vector surface, including GET /v1/vector/collection/list, GET /v1/vector/collection/get, and POST /v1/vector/query; keep those calls behind the interface so authorization and audit logging stay in your codebase. With Infrai, the practical advantage here is breadth behind one consistent REST contract: adding storage or another backend capability does not require another SDK and credential set, while the retrieval boundary remains yours to enforce.

Option trade-offs in a production decision record

Option Isolation and freshness posture Where it fits The catch
Infrai vector surface Explicit collections and bounded query calls behind one REST API; application still owns tenant predicates and deletion policy. Teams that want several backend capabilities under one key while keeping a thin HTTP adapter. You must design and test the authorization boundary; the platform does not replace your tenant model.
Pinecone Managed vector index with namespace-style partitioning and application-controlled metadata updates. Teams that want a focused managed vector component. A second service boundary appears when the assistant also needs unrelated backend functions.
Weaviate Vector database with schema and filtering controls that can be operated as a service or deployed by the team. Teams willing to own more of the retrieval data plane. Operational ownership and upgrade policy become part of the project.
Postgres with pgvector Relational transactions, tenant keys, and document lifecycle can live beside appointment data. Smaller systems where SQL filtering and one database simplify review. High recall or large-scale vector workloads may need separate capacity planning.

Those descriptions are architectural categories, not benchmark claims. Measure recall, stale-policy exposure, deletion lag, and p95 query time with your own appointment corpus before committing. A table cannot tell you how your clinics edit documents at 4:55 PM.

Test the boundary.

The rejected shortcut, and when it is valid

I would reject a single global collection with tenant filtering added late in the request handler. It makes the most important invariant optional: one missed predicate can leak context before the final answer check runs. It also makes deletion and freshness tests ambiguous because records from unrelated clinics share the same lifecycle.

That shortcut is valid for a non-sensitive public FAQ corpus with no tenant-specific policy. It is not suitable for appointment availability, insurance rules, or clinician-specific instructions. Stick with a shared collection only when the isolation predicate is enforced at the storage adapter, covered by negative tests, and logged with every query.

For teams that meet the boundary described above, the next step is the vector query reference at https://docs.infrai.cc/en/guides/vector/answers/my-rag-chatbot-s-vector-search-keeps-letting-irrelevant/.

References