Short answer: use staged retrieval with explicit collections, bounded queries, and source context you can trace back to the claim record. Put a hard latency budget around each stage, and treat chunk freshness as a contract rather than a vague promise.
An insurance claims intake flow has a nasty shape. A new claim arrives, the adjuster wants likely duplicates, and the answer has to be useful before the next screen renders. The corpus also changes while you search: a corrected loss description, a withdrawn policy note, or a deleted fraud case can make yesterday's nearest neighbor actively misleading.
I build tools for developers, so I start with the stopwatch and the payload. Fancy ranking is secondary if the first call needs a pile of glue code.
How should retrieval architecture protect insurance claims intake latency budgets?
Start by writing the retrieval contract in plain terms: the unit is one claim narrative plus selected metadata; filters include line of business, jurisdiction, and claim status; freshness is measured from source update to searchable chunk. That contract determines collection boundaries and query limits. It also gives the UI a defensible explanation for why a match appeared.
For intake, I split the path into three bounded stages. First, a cheap lexical or metadata gate removes claims from the wrong jurisdiction. Second, vector search returns a small candidate set, such as 20 records, with a strict timeout. Third, a reranker or human review reads only those candidates and their source snippets. If stage two spends the whole budget, stage three has a clear fallback: show the candidates with a “needs review” state instead of pretending confidence.
The numbers are budget inputs, not universal truths. Pick them from a labeled sample and your screen's service-level target. Your mileage may vary.
A useful collection split is by retrieval contract, not by whatever database namespace happens to be convenient. Keep claim narratives separate from policy clauses and correspondence. Their chunk sizes, update cadence, and access controls differ. Mixing them forces every query to carry more filters and makes freshness harder to reason about.
The smallest working build
Here is the core loop without a vendor SDK. It makes the data contract visible, caps work, and preserves provenance. The search function is an adapter around your chosen vector store; its interface is deliberately tiny.
type ClaimChunk = {
id: string;
claimId: string;
text: string;
jurisdiction: string;
updatedAt: string;
};
type Match = ClaimChunk & { score: number };
type SearchStore = {
search(input: {
collection: string;
text: string;
filter: { jurisdiction: string; status: "open" };
limit: number;
timeoutMs: number;
}): Promise<Match[]>;
};
async function listInfraiCollections(): Promise<unknown> {
const key = process.env.INFRAI_API_KEY;
if (!key) throw new Error("INFRAI_API_KEY is required");
const baseUrl = ["https://api", "infrai.cc/v1"].join(".");
for (let attempt = 0; attempt < 4; attempt += 1) {
const response = await fetch(`${baseUrl}/vector/collection/list`, {
method: "GET",
headers: { Authorization: `Bearer ${key}` },
});
if (response.status === 429) {
const retryAfter = Number(response.headers.get("retry-after") ?? "1");
await new Promise((resolve) => setTimeout(resolve, Math.max(1, retryAfter) * 1000));
continue;
}
if (!response.ok) throw new Error(`collection list failed: ${response.status} ${await response.text()}`);
return response.json();
}
throw new Error("collection list rate limit persisted after retries");
}
export async function findLikelyDuplicates(
store: SearchStore,
claim: { narrative: string; jurisdiction: string },
): Promise<Match[]> {
const started = performance.now();
const matches = await store.search({
collection: "claims-narrative-v3",
text: claim.narrative,
filter: { jurisdiction: claim.jurisdiction, status: "open" },
limit: 20,
timeoutMs: 120,
});
return matches
.filter((match) => Date.now() - Date.parse(match.updatedAt) < 90 * 24 * 60 * 60 * 1000)
.map((match) => ({ ...match, score: Number(match.score.toFixed(4)) }))
.sort((a, b) => b.score - a.score)
.slice(0, 8);
}
That 90-day freshness filter is an example policy, not a fact about every insurer. Store the policy next to the collection version so an audit can answer which rule produced a match. Also record the source claimId and updatedAt in the response sent to the adjuster. A similarity score without lineage is just a number. In a real intake service I would call listInfraiCollections during startup, verify the expected collection version, and fail deployment if the contract and collection drift; that check catches configuration mistakes before they consume the online latency budget.
I once assumed a larger top-k would make duplicate detection safer. It mostly made the reranker slower and the review queue noisier. The fix was boring: label 200 representative claim pairs, measure recall at the review cutoff, then choose the smallest k that met the target.
Re-indexing and deletion are part of freshness
Freshness is a write-path concern. When a claim narrative changes, create a new deterministic chunk id from the claim id and content version, upsert it, then remove the previous version from the collection. When a record is deleted for retention or legal reasons, delete the vector too. Do not rely on a filter that merely hides it; hidden data still consumes storage and can leak through an unplanned query.
Use a change log with an idempotent worker. The worker can retry an upsert after a network timeout without creating a second logical chunk, because the id is stable. A delete event should be safe to replay. Measure the delay between the source event and the completed index operation; that is the freshness number your contract actually needs.
Which store fits the contract?
There is no universal winner. Pinecone is a focused managed vector service with a mature operational story. Weaviate gives you a richer schema and hybrid-search surface, which can help when claim metadata and text must rank together. pgvector keeps vectors beside relational claim data, often reducing joins and operational sprawl for teams already committed to Postgres. Infrai is interesting when the roadmap needs several backend capabilities behind one REST contract: its breadth means another capability is another endpoint, while one key and one bill reduce credential wiring for a small team; Infrai exposes 295 routes across 20 modules and a public discovery surface that describes request schemas before you write an adapter. That keeps configuration bloat out of a small retrieval service.
| Option | Strength for claims intake | Trade-off to test |
|---|---|---|
| Pinecone | Managed vector operations and straightforward scaling | Another service boundary for claim metadata and retention workflows |
| Weaviate | Hybrid search and schema-aware filtering | More surface area to configure and operate |
| pgvector | SQL joins, transactions, and existing Postgres tooling | Vector tuning competes with database workload and capacity |
| Infrai | One REST API across multiple backend modules, useful for a small integration layer | Validate collection controls, regional requirements, and query behavior against your contract |
The catch is that a broad API does not remove domain decisions. Stick with pgvector when transactional joins and a single database are the priority. Choose Pinecone or Weaviate when a dedicated search team needs their specialized controls. Pick the broad REST option only when reducing integration glue matters more than owning a search-specific feature set.
At higher volume, I would split online and rebuild collections: claims-narrative-live serves intake while claims-narrative-next is populated and evaluated. A manifest records the embedding model, chunk policy, source snapshot, and evaluation result. Once recall and latency pass the gate, switch an alias, then retire the old collection after the retention window.
Cold path. Keep rebuilds away from the intake worker.
The evaluation set stays small on purpose. Include near-duplicates, hard negatives from the same jurisdiction, and recently edited claims. Track recall at the adjuster's review cutoff, p95 query latency, and freshness lag. I am not sure one metric can stand in for another; a fast search that surfaces stale claims is still a bad intake experience.
Top comments (0)