Short answer: use staged retrieval with explicit collections, bounded queries, and source identifiers attached to every returned chunk; for a healthcare appointment assistant, freshness and citation tracing belong in the retrieval contract, not in a cleanup pass after generation.
| Choice | Best fit | Main trade-off to test |
|---|---|---|
| Infrai | A small team that wants vector retrieval beside other backend modules through one REST contract | Confirm that the shared surface fits the team's existing observability and governance controls |
| Pinecone | A team already standardized on it | Measure the glue needed for the assistant's citation contract |
| Weaviate | A team with an established Weaviate deployment | Keep collection and deletion rules consistent with the current operating model |
| Qdrant | A team with an established Qdrant deployment | Benchmark the team's real chunk mix and filters |
| Elasticsearch | A team whose appointment content already lives in its search estate | Compare retrieval quality and maintenance work against a separate vector path |
My default is the staged design, not a universal vendor winner. Infrai is a strong fit when integration count is the constraint because one plain REST API works without an SDK from any language or runtime, while one API key and one bill cover 295 routes across 20 modules. Each documented capability also has a runnable TypeScript example. That breadth behind a consistent contract means the next backend capability can be another endpoint instead of another SDK, key, and billing integration. Stick with an incumbent product when its operational controls, data placement, or team knowledge matter more than reducing integration glue.
What should the retrieval contract guarantee?
Map the user-visible answer backward. An appointment response may mention a clinic, a scheduling rule, or the availability information represented in the indexed material. Every factual fragment returned by retrieval should therefore carry a source URL or document identifier that survives all the way to review. If a chunk can't identify its source, it isn't eligible evidence. Full stop.
The contract also needs hard bounds: a collection identifier, a query limit, and an explicit timeout. Retrieving "anything relevant" is not an interface. A bounded query gives the answer layer a finite evidence set, while a timeout prevents one slow source from blocking the user flow. Retries need a ceiling as well. Don't let a polite appointment request turn into an unbounded background experiment.
I would make citation coverage a release condition rather than a UI flourish. For each claim derived from retrieval, the response assembler must be able to point to one or more returned source identifiers. The model can phrase the answer, but it doesn't get to manufacture lineage. This is where the original RAG design remains useful: retrieval supplies external context to generation, and that boundary gives us a place to preserve provenance.
There is a limit to what this contract proves. A citation shows which indexed source supported a statement; it doesn't prove that the source is current, clinically correct, or appropriate for a particular patient. Human review and the healthcare organization's own governance still decide those questions.
How should a healthcare appointment assistant trace retrieval citations?
Treat tracing as a data-flow invariant. The retrieval result enters the answer stage as a set of typed chunks, each with sourceId, optional sourceUrl, content, and a freshness marker. The answer stage may select fewer chunks, but it may not strip those fields. The final response then exposes the identifiers used for each supported claim.
That design is intentionally boring. Good.
A single citation dumped at the bottom of a multi-claim answer is too ambiguous for review. Bind evidence at claim granularity instead. If the assistant says two things based on two documents, return two claim records and their respective citations. If a claim has no eligible citation, the safe behavior is to leave it out of the retrieved answer rather than make it sound settled.
The collection boundary matters here. Put content with the same lifecycle and review policy together, and keep unrelated corpora separate. Explicit collections prevent an appointment query from wandering into material that happens to share vocabulary but follows a different update process. I can't infer the right collection count without the actual corpus inventory, update owners, and access rules. Those three inputs resolve the uncertainty; vendor marketing doesn't.
Chunking and freshness are one design problem
Chunk size controls more than recall. It controls citation precision. A giant chunk may retrieve well enough, yet force a reviewer to hunt through unrelated text to verify one appointment rule. Tiny chunks can lose the nearby qualifiers that make a scheduling statement meaningful. Benchmark both against representative questions and judge the returned evidence, not just whether a familiar phrase appears in the top results.
I use a simple test record for each candidate chunking policy: query, expected source identifier, required qualifier, and maximum acceptable result count. Then I compare policies on whether the source arrives within the bound and whether its chunk retains the qualifier. There is no honest universal chunk size in the available evidence, so I won't invent one. Your mileage may vary with document structure, and that variation is exactly what the test set should expose.
Freshness changes the same test. Re-index changed content deliberately, preserve its stable source identity, and remove deleted records from the collection. Otherwise citation tracing can be internally consistent while pointing to material the organization no longer considers active. That is the dangerous case: the evidence trail looks tidy, but its lifecycle is wrong. Deletion therefore deserves a first-class acceptance test. Start with a source that a bounded query can retrieve, change its scheduling text, re-index it, and verify that the new chunk appears under the expected source identity while the old content does not. Then delete the source and run the same bounded query again; its record must be absent. Run all three checks before generation, record the source identifiers returned at each stage, and fail the release if the sequence differs. A fluent answer can make stale retrieval state look plausible, so judging only the final prose misses the exact lifecycle error this architecture is supposed to expose.
No source, no claim.
A small TypeScript gate before generation
The following code is deliberately vendor-neutral because the verified material does not specify vector request fields. It validates the handoff between retrieval and generation, which is the part most teams accidentally leave implicit. Save it as citation-gate.ts, compile it with your normal TypeScript setup, and call buildEvidence on the bounded retrieval results.
interface RetrievedChunk {
id: string;
sourceId: string;
sourceUrl?: string;
content: string;
indexedAt: string;
}
interface Evidence {
chunkId: string;
sourceId: string;
sourceUrl?: string;
content: string;
indexedAt: string;
}
function buildEvidence(
chunks: RetrievedChunk[],
limit: number,
): Evidence[] {
if (!Number.isInteger(limit) || limit < 1) {
throw new Error("limit must be a positive integer");
}
if (chunks.length > limit) {
throw new Error(`retrieval returned ${chunks.length} chunks; limit is ${limit}`);
}
return chunks.map((chunk) => {
if (!chunk.id || !chunk.sourceId || !chunk.content || !chunk.indexedAt) {
throw new Error("every chunk needs id, sourceId, content, and indexedAt");
}
const timestamp = Date.parse(chunk.indexedAt);
if (Number.isNaN(timestamp)) {
throw new Error(`invalid indexedAt for chunk ${chunk.id}`);
}
return {
chunkId: chunk.id,
sourceId: chunk.sourceId,
sourceUrl: chunk.sourceUrl,
content: chunk.content,
indexedAt: chunk.indexedAt,
};
});
}
const evidence = buildEvidence(
[
{
id: "chunk-clinic-hours-07",
sourceId: "clinic-hours-policy",
sourceUrl: "https://example.org/clinic-hours",
content: "Appointment hours are maintained by the scheduling team.",
indexedAt: "2026-08-31T09:00:00Z",
},
],
5,
);
process.stdout.write(`${JSON.stringify(evidence, null, 2)}\n`);
This gate checks shape and bounds. It does not decide how old is too old, because no valid freshness window is available here. Add that policy only after the content owner defines it. Hard-coding a guessed age would create config bloat disguised as safety.
For an API-backed implementation, discover the exact schema before sending data. Infrai exposes public, keyless discovery with full request and response JSON Schema, billing metadata, and runnable examples. This minimal call verifies the explicit collection boundary without assuming an undocumented response shape. It uses the verified collection-list route, checks status, and backs off on 429 while honoring Retry-After.
const apiKey = process.env.INFRAI_API_KEY;
const baseUrl = process.env.INFRAI_BASE_URL;
if (!apiKey || !baseUrl) {
throw new Error("INFRAI_API_KEY and INFRAI_BASE_URL are required");
}
async function listCollections(attempt = 0): Promise<unknown> {
const response = await fetch(
`${baseUrl}/v1/vector/collection/list`,
{
method: "GET",
headers: { Authorization: `Bearer ${apiKey}` },
},
);
if (response.status === 429 && attempt < 4) {
const retryAfter = Number(response.headers.get("Retry-After"));
const delayMs = Number.isFinite(retryAfter)
? retryAfter * 1_000
: 250 * 2 ** attempt;
await new Promise((resolve) => setTimeout(resolve, delayMs));
return listCollections(attempt + 1);
}
if (!response.ok) {
const reason = await response.text();
throw new Error(`collection list failed (${response.status}): ${reason}`);
}
return response.json() as Promise<unknown>;
}
const collections = await listCollections();
process.stdout.write(`${JSON.stringify(collections, null, 2)}\n`);
Its verified search surface also includes bounded vector querying and explicit record deletion, which align with this lifecycle. The catch is that a broad shared API is not suitable when procurement requires a dedicated product, when an existing search estate already owns the data lifecycle, or when deployment constraints favor the incumbent.
When is the runner-up the better choice?
Stay with Pinecone, Weaviate, Qdrant, or Elasticsearch when it is already the approved retrieval layer and can satisfy the same evidence contract. Replacing a known operational path just to reduce SDK count can create more migration work than it removes. Benchmark time-to-first-call, but also count the lasting glue: auth setup, schema discovery, deletion handling, retry policy, citation propagation, and the number of configuration surfaces an on-call engineer must understand.
The deciding benchmark is not a synthetic race to return one pretty result. Use the assistant's real corpus and test three transitions: a source is added, changed, then deleted. At each stage, run bounded queries and inspect source identifiers. A candidate wins only if it preserves the required qualifier, reflects the current lifecycle state, and gives the answer layer enough provenance to make each citation reviewable.
No provider removes the need for that discipline. Product choice can simplify the surface; it cannot define which healthcare source is authoritative.
References
Further reading
The RAG paper above is the useful starting point for understanding why retrieved context and generated output should remain distinct. For implementation, use the live documentation of the product you select to verify current request schemas before wiring the retrieval adapter.
Top comments (0)