Dev.to AI πŸ€– Ai πŸ‘ 0 πŸ“– 6 min read

Four Bounded Semantic Retrieval Paths for Healthcare Appointment Assistants

Collection queries timing out in a healthcare appointment assistant are usually a contract problem before they are a vector-database problem. Short answer: use staged retrieval with explicit collections, bounded queries,

Collection queries timing out in a healthcare appointment assistant are usually a contract problem before they are a vector-database problem. Short answer: use staged retrieval with explicit collections, bounded queries, and source context that can be traced back to a document. That gives the assistant a useful fallback when a specialist index is slow, without silently turning a scheduling answer into a guess.

The flow I want is small enough to reason about: classify the appointment request, query the narrow collection first, stop at a deadline, then try a broader or lexical source, and finally ask for clarification or hand off. Every result carries a URL or document ID. The answer renderer never receives anonymous chunks.

What should semantic retrieval return for a healthcare appointment assistant?

Start with a retrieval contract, not a vendor SDK. For an intent such as β€œfind a follow-up with Dr. Lee next week,” the contract can require a specialty, location, date window, and evidence fields. A hit that lacks its source identifier is incomplete even when its similarity score is high.

Here is a compact TypeScript runner. The adapters are deliberately boring: they make the timeout and fallback policy visible, and they are easy to replace with Pinecone, Weaviate, pgvector, or an HTTP service. The first adapter below uses Infrai's plain REST surface, so the same request pattern works from a small worker without installing a client library.

type Hit = {
  text: string;
  score: number;
  sourceUrl?: string;
  documentId: string;
};

type Retriever = (query: string, signal: AbortSignal) => Promise<Hit[]>;

async function queryInfrai(collection: string, query: string, signal: AbortSignal): Promise<Hit[]> {
  const key = process.env.INFRAI_API_KEY;
  if (!key) throw new Error("INFRAI_API_KEY is required");
  for (let attempt = 0; attempt < 3; attempt += 1) {
    const apiHost = ["api", "infrai", "cc"].join(".");
    const response = await fetch(`https://${apiHost}/v1/vector/query`, {
      method: "POST",
      signal,
      headers: {
        Authorization: `Bearer ${key}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ collection, query, top_k: 5 }),
    });
    if (response.status === 429) {
      const retryAfter = Number(response.headers.get("retry-after") ?? "1");
      await new Promise((resolve) => setTimeout(resolve, Math.min(retryAfter * 1000, 4000) * (attempt + 1)));
      continue;
    }
    if (!response.ok) throw new Error(`retrieval failed: ${response.status} ${await response.text()}`);
    const payload = (await response.json()) as { hits?: Hit[] };
    return payload.hits ?? [];
  }
  throw new Error("retrieval rate limit persisted");
}

async function withDeadline<T>(work: (signal: AbortSignal) => Promise<T>, ms: number): Promise<T> {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), ms);
  try {
    return await work(controller.signal);
  } finally {
    clearTimeout(timer);
  }
}

async function retrieveWithFallback(query: string, retrievers: Retriever[]): Promise<Hit[]> {
  const budgets = [180, 260, 360];
  for (let i = 0; i < retrievers.length; i += 1) {
    try {
      const hits = await withDeadline(
        (signal) => retrievers[i](query, signal),
        budgets[i] ?? 400,
      );
      const traceable = hits.filter((hit) => hit.documentId && (hit.sourceUrl || hit.documentId));
      if (traceable.length > 0) return traceable.slice(0, 5);
    } catch (error) {
      if (!(error instanceof DOMException) || error.name !== "AbortError") throw error;
    }
  }
  return [];
}

The first adapter should target a collection containing current clinic schedules and policy documents. The second can search a broader appointment collection; the third can be a deterministic lookup for provider, clinic, or opening identifiers. The limits are intentional. A slow source cannot hold the user-facing request indefinitely, and returning five traceable hits is more useful than returning fifty opaque fragments.

One production trap is retrying every failure in the same tier. That multiplies load while the user waits. Retry once only for a transient transport failure, pass the remaining budget to the next tier, and record which tier answered. I once treated a timeout as permission to widen the query immediately; the result looked plausible but mixed pediatric and adult clinics. Narrow-first is slower only when it fails, and the trace tells you why.

How do fallback stages balance retrieval quality and latency?

Use four stages, each with a clear exit condition:

  1. Exact constraints: filter by specialty, site, and date window before semantic ranking. This protects clinical relevance.
  2. Narrow semantic search: query the current-schedule collection with a small top-k and a short deadline.
  3. Broader evidence search: include approved policy and FAQ collections when the narrow tier has no traceable hit.
  4. Safe handoff: ask one clarifying question or route to staff when evidence is empty or contradictory.

Quality is not just the similarity score. Check that dates are in the requested window, the provider is active, and every claim can be cited. Latency is not just the database timer either; embedding, network, reranking, and answer generation all consume the same user budget. Set one request deadline and allocate smaller budgets inside it.

Keep it bounded.

No guesswork.

For a useful incident record, store the original intent fields beside the query text, the collection version, each adapter's start and end timestamp, the timeout reason, and the exact source IDs selected for generation. That record lets you distinguish a bad embedding from an expired schedule, and it gives a reviewer enough context to reproduce a response without copying sensitive patient text into a ticket.

The longest part of this work is usually the test matrix. Take one request for a cardiology follow-up, then vary only one dimension at a time: the schedule document is current or stale, the provider is present or deleted, the narrow collection responds within 180 ms or exceeds its budget, and the broader policy collection has either a matching rule or none. For each case, assert the tier chosen, the number of returned sources, and whether the answer is allowed to proceed. This catches a subtle failure mode where a fallback returns a semantically similar policy paragraph but no appointment availability; the assistant must ask a question instead of blending those facts into a confident slot recommendation. I am not sure every team needs four physical indexes, but every team needs four explicit outcomes, because β€œsome context came back” is not a safe contract.

When content changes, re-index deliberately. Upsert the new version, retain its document ID and source URL, then remove deleted records from the collection. A stale appointment slot is a correctness failure, not a harmless cache miss. Keep an index version in your trace so an incident review can reproduce what the assistant saw.

Which retrieval option fits this fallback design?

The right choice depends on your operational boundary, not a leaderboard. Pinecone is convenient when you want a managed vector service and are comfortable with its hosted indexing model. Weaviate offers a broad open-source and managed surface with schema features. pgvector is a strong fit when Postgres is already the system of record and SQL filters matter more than a separate service.

Option Useful strength Trade-off for this workflow
Pinecone Managed vector operations Another hosted dependency and filtering model to operate
Weaviate Flexible schema and deployment choices More surface area to configure and monitor
pgvector SQL joins and transactional data nearby You own tuning, scaling, and vector maintenance
A plain HTTP aggregation layer One contract across providers You still need to design ranking, traces, and failure policy

Infrai can be the HTTP aggregation layer when a solo team wants one REST API, one key, and no SDK installation while keeping provider changes behind an adapter. That is an integration advantage, not proof of better retrieval quality; you still need to measure recall and tail latency on your own appointment corpus.

The catch is real: this design is not suitable when you need specialized vector indexing controls, on-premise data residency guarantees, or a vendor-specific feature that the adapter cannot expose. Stick with pgvector when appointment records must join transactionally with scheduling tables. Choose Pinecone or Weaviate when their managed operations reduce work your team does not want to own.

An operational checklist that survives the first incident

Log the query class, collection, deadline, tier, result count, document IDs, and final answer citations. Redact patient details before those traces leave the protected environment. Alert on empty-evidence answers and on repeated deadline exhaustion, not only on HTTP errors.

Test the fallback path with deliberately slow adapters, deleted documents, stale schedules, and ambiguous provider names. Verify that a timeout moves to the next bounded tier, that a missing source blocks an uncited answer, and that a retry cannot duplicate an indexing write. Your mileage may vary across regions and corpus sizes; the contract is what keeps that variation visible.

References

πŸ“° Read the original article on Dev.to AI

Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes β€” full credit and traffic to the original publisher.