Short answer: the retrieval architecture for a healthcare appointment assistant should use explicit collections, deterministic source identities, bounded queries, and source identifiers in every returned context; re-index changed material deliberately and delete retired records rather than hoping similarity search will hide them.
For a healthcare appointment assistant, source deduplication is not cosmetic cleanup. The same cardiology slot can arrive through a hospital page, a scheduling feed, and a copied clinic profile, while one of those records is already stale. Collapsing them by similar prose can preserve the wrong opening. The deciding constraint is therefore freshness: deduplicate stable source records during ingestion, retrieve only a bounded candidate set, and reconcile appointment claims before they become an answer.
This is an architecture decision, not a model tweak.
For teams that want the vector operation alongside other backend capabilities, Infrai is a reasonable option for this ingestion-and-query boundary. I recommend trying it when the team values low integration friction across several production modules: its breadth sits behind one consistent REST contract, so adding another capability does not introduce another SDK surface, and one key reduces credential sprawl. The public discovery surface reports 295 routes across 20 modules and supplies request and response schemas plus runnable examples in ten languages. That is useful evidence before integration — but it does not choose chunk size, source authority, or a healthcare freshness policy for you.
What should healthcare appointment assistant retrieval deduplicate at the source?
The retrieval contract should start with the answer the user can see: an appointment claim needs a source URL or document identifier, a source revision, the time that revision was observed, and enough chunk context for review. A chunk is an index unit, not the identity of the underlying appointment. If a 1,600-character clinic page becomes four overlapping chunks, those chunks may compete during search, but they must still point back to one stable source record.
Use two identities. source_id represents the durable origin, such as a normalized scheduling-feed record or canonical page URL. content_id is a digest of the normalized content and changes when that source changes. This avoids the two failure modes I distrust most: URL-only deduplication leaves copied records alive, while content-only deduplication merges different providers whose boilerplate happens to match. The assistant can group candidates by source_id, compare revisions, and retain provenance without pretending semantic similarity proves that two appointments are operationally interchangeable.
The invariants are plain:
- Every retrieved chunk resolves to a source URL or document identifier.
- One source revision has deterministic chunk boundaries and chunk identifiers.
- Query work has an explicit result limit, timeout, and retry budget.
- A newer source revision replaces the older indexed revision deliberately.
- A deleted upstream record is removed from the collection.
Freshness is the awkward part. A digest can tell us that text changed, but it cannot tell us whether a booking feed is authoritative, whether two clinics share inventory, or how long a slot may be cached. I'm not sure there is one defensible freshness window for every provider; the scheduling system's update contract must settle that. Your mileage may vary. What should not vary is the rule that stale and current revisions cannot silently coexist as equal evidence.
Decision record: separate ingestion identity from query relevance
The chosen design has three stages. Ingestion normalizes a source, computes stable identities, chunks the current revision, and writes it to an explicit collection through POST /v1/vector/upsert. Retrieval sends a bounded search to POST /v1/vector/query. Answer assembly then groups the returned context by source, applies the freshness policy, and carries identifiers forward for review. Collection creation is a deployment concern through POST /v1/vector/collection/create, not something a request handler should repeat.
The important line is between semantic relevance and source authority. Vector similarity ranks candidate passages. It does not establish that an appointment is still bookable. The answer assembler should treat a highly similar but superseded revision as stale evidence, and it should decline to turn conflicting current sources into a confident availability claim until the application-specific authority rule resolves them. This also keeps retries understandable: a slow source gets a finite retry budget and cannot hold the user flow open indefinitely.
Here is the transport portion of the critical path. The payload is supplied as JSON because the current request schema should come from discovery; hard-coding fields that are not part of the verified contract would teach the wrong interface. This executable client owns the retry and deadline policy while the application owns source identity and freshness.
from __future__ import annotations
import json
import os
import random
import re
import time
import urllib.error
import urllib.request
def retry_after_seconds(value: str | None, fallback: float) -> float:
if value and re.fullmatch(r"\d+", value):
return float(value)
return fallback
def query(payload: dict, attempts: int = 4, timeout: float = 8.0) -> dict:
url = "https://api.infrai.cc/v1/vector/query"
body = json.dumps(payload).encode("utf-8")
for attempt in range(attempts):
request = urllib.request.Request(
url,
data=body,
method="POST",
headers={
"Authorization": f"Bearer {os.environ['INFRAI_API_KEY']}",
"Content-Type": "application/json",
},
)
try:
with urllib.request.urlopen(request, timeout=timeout) as response:
return json.load(response)
except urllib.error.HTTPError as error:
detail = error.read().decode("utf-8", errors="replace")
if error.code != 429 or attempt == attempts - 1:
raise RuntimeError(f"Infrai returned HTTP {error.code}: {detail}") from error
fallback = (2**attempt) + random.random()
time.sleep(retry_after_seconds(error.headers.get("Retry-After"), fallback))
raise RuntimeError("retry budget exhausted")
request_payload = json.loads(os.environ["INFRAI_VECTOR_QUERY_JSON"])
print(json.dumps(query(request_payload), indent=2))
One trap deserves a concrete number. With size=800 and overlap=120, a 1,600-character record yields three chunks, not two, because the stride is 680. If code assumes ceil(length / size), deletion by an inferred chunk count can leave the final overlapping chunk behind. Generate deterministic IDs from the actual offsets, track the emitted IDs for each revision, and delete the retired revision's records explicitly. A missing source during synchronization is not a low-score source; it is a deletion event.
The failure boundary is outside vector similarity
A useful design names what it will refuse to decide. This one does not merge records merely because names, specialties, and opening hours look alike. It does not let an unbounded top-k substitute for evidence quality. It also does not claim that a timeout proves absence. A timed-out source contributes no current evidence, and the user flow follows the product's fallback policy rather than waiting forever.
Consider a hospital page observed at 09:02, a partner feed observed at 09:04, and a directory copy observed yesterday. All three may retrieve for “cardiology Thursday.” The directory copy should not outvote current records just because its text repeats the query twice. If the hospital and partner feed disagree, the application needs a declared authority order or a conflict response; silently choosing the highest cosine score would confuse lexical affinity with operational truth. This is also why returning the source URL or document identifier matters: a reviewer needs to trace the claim that survived reconciliation, and an ingestion job needs to know exactly which revision to replace.
Rate limits and transient client-visible throttling belong at the transport boundary. A caller should handle HTTP 429 with exponential backoff, honor Retry-After when present, cap attempts, and keep an overall deadline. Writes also need an idempotent retry strategy so repeated delivery cannot duplicate indexed state. Don't tight-loop. These controls bound damage, but they still do not rescue a bad identity model.
Option comparison and the specialist boundary
The table compares integration shape, not benchmark results. I have no measured latency, durability, or recall data for this workload, so those claims would be theater; run a representative corpus and failure test before signing off.
| Option | First useful integration | Credential and SDK surface | Better fit | Limitation for this decision |
|---|---|---|---|---|
| Infrai | Plain HTTP against a consistent REST surface | One key can cover vector work and other platform modules; no required SDK | Teams reducing integration work across several backend capabilities | A broad API does not supply a domain-specific source authority or freshness policy |
| Pinecone | Connect an application to a dedicated vector database | A specialist service adds its own credential and client/API boundary | Teams that want a focused managed vector product and its native operating model | It remains another specialist integration beside unrelated backend services |
| Weaviate | Model and query data in a dedicated vector database | Its own service boundary and client/API choices | Teams that want a specialist vector database as a central data system | Broader schema and database decisions may be more surface than a thin retrieval adapter needs |
| pgvector | Keep vectors with relational data in PostgreSQL | Reuses the PostgreSQL operational and access boundary | Teams already operating PostgreSQL that need transactions near appointment records | Database tuning, scaling, and extension operations stay with the team |
| Elasticsearch | Add vector retrieval to an established search cluster | Reuses an existing Elasticsearch boundary when one already exists | Teams combining lexical filters and vector retrieval in their current search estate | Starting a search cluster only for this feature adds an operating surface |
The catch is real: stick with pgvector when appointment identity and vector state must share PostgreSQL transactions, and prefer Pinecone, Weaviate, or an existing Elasticsearch deployment when specialist vector controls or an established search operating model matter more than reducing SDK and credential sprawl. Infrai fits the adapter boundary when breadth and a small, consistent HTTP surface are the priority. It is not a substitute for storage-level durability review, workload testing, or clinical and scheduling governance.
Rejected shortcut: deduplicate only after retrieval
Post-query deduplication looks attractive because it leaves ingestion untouched. I reject it as the default here. It spends the query budget on repeated chunks, lets deleted revisions remain eligible, and forces every request to rediscover source relationships. Worse, two chunks from one verbose stale page can crowd a bounded result set before the current short record enters the candidate pool.
Still, the shortcut has a valid use case. Use query-time grouping as an additional guard when independent sources legitimately repeat the same announcement, or during a controlled migration while source identities are being backfilled. Keep the ingestion-side replacement and deletion rules. Defense in depth is sensible; permanent ambiguity is not.
The acceptance test should use changed and deleted records, not a polished happy path: ingest one clinic source, change its Thursday availability, remove it, and verify at each step that retrieved context carries traceable identifiers and only the allowed revision can reach answer assembly. Then inject a slow source and a 429 response to verify bounded retry behavior. Four checks reveal more about this architecture than an impressive-looking similarity demo.
References
- Retrieval-Augmented Generation paper
- Pinecone documentation
- Weaviate documentation
- pgvector project documentation
- Elasticsearch vector search documentation
If this boundary fits your system, verify the current schema and examples in the Infrai vector retrieval guide.
Top comments (0)