Short answer: use a staged retrieval design with explicit collections, bounded queries, and source context that survives every hop to the answer. For an internal financial research dashboard, choose the storage engine after defining the chunk and freshness contract, not before.
| Choice | Best fit | Main trade-off |
|---|---|---|
| Infrai | A small team that wants one plain REST contract, one key, and one bill while retaining the option to swap the vendor behind the capability without changing application code | Less direct control than owning the vector stack; its public discovery surface exposes the active contract |
| Pinecone | A team that wants a managed vector database | Adds a dedicated data service and its operating model |
| Weaviate | A team that wants an open-source vector database with managed and self-managed paths | Self-management creates more operational work |
| pgvector | A team already keeping research metadata in PostgreSQL | Vector workload and transactional workload share a database boundary |
| Elasticsearch | A team whose research search already depends on its search engine | Vector retrieval joins a broader search configuration surface |
Recommendation: make freshness and citation lineage application-owned invariants. Then pick the least complicated engine that preserves them. The engine is replaceable. The evidence contract isn't.
How should retrieval observability protect financial research dashboard freshness?
Start with the answer a researcher sees. Each cited claim needs a stable source ID, the source's effective time, its ingestion time, a chunk revision, the collection searched, the applied filters, and the rank returned. Those fields form the retrieval contract. A trace that only says vector_query: 84ms is tidy but weak: it can tell you that a request happened, not whether the dashboard used yesterday's filing after this morning's amendment arrived.
Keep three stages separate: ingestion decides what evidence exists, querying decides what evidence is eligible, and answer assembly decides what gets cited. Give each stage its own event and correlation ID. This division makes the hard questions answerable. Did the new 10-Q fail to ingest? Was it present but excluded by asOf? Did retrieval return it while the answer cited an older chunk? One giant rag.request span hides all three failure classes behind a green check.
There is a useful constraint here: observability should record decisions, not dump document bodies. Log identifiers, revisions, timestamps, filter values, result ranks, and counts. Keep sensitive research text out of routine telemetry. A trace needs enough context to reproduce selection without becoming a second, poorly governed document store.
The bounded query matters too. Require a collection, a positive topK with a small upper bound, an asOf timestamp, and explicit metadata filters. Reject an unbounded request at your own API boundary. Don't let a dashboard silently search every issuer, region, and reporting period because one caller omitted a field.
That's the contract.
Chunking is a versioned data decision
Financial documents resist one universal chunk size. A filing has sections, footnotes, tables, amendments, and page-level context; an analyst memo has a different shape. Fixed token windows are easy to benchmark, but they can split a number from its unit or separate a footnote from the claim it qualifies. Structure-aware chunks preserve those relationships, though they produce uneven chunk sizes and demand more ingestion logic. Use a retrieval unit that can be revised without losing identity. A practical record carries sourceId, sourceRevision, chunkId, section, effectiveAt, ingestedAt, and a content digest. Treat the digest as change detection, not identity. If an issuer republishes a document, create a new revision and close the validity interval of the old one. Do not overwrite history in place; asOf retrieval depends on it. Freshness is not one timestamp. effectiveAt answers when the information applies. ingestedAt answers when the system learned about it. The gap is ingestion lag, and it should be visible. I'm not sure a dashboard can honestly call a result current without defining an acceptable gap for each source class. A live news feed and a quarterly filing don't need the same threshold. Your mileage may vary -- the threshold is a product decision -- but the two timestamps are still required to enforce it.
Benchmark chunk variants with representative documents and known awkward cases: a table continued across pages, an amended filing, two companies with similar names, and a memo that cites an older filing. Measure retrieval recall and precision against labeled questions. Also inspect citation completeness. A chunking strategy that finds the right sentence but loses the source revision is not ready for financial research.
A small TypeScript contract makes the stages testable
Before implementing the query adapter, list its explicit collections. This call is runnable with an API origin and key supplied by the deployment. It makes no assumptions about response fields.
const apiOrigin = process.env.INFRAI_API_ORIGIN; const infraiApiKey = process.env.INFRAI_API_KEY; if (!apiOrigin || !infraiApiKey) { throw new Error("Set INFRAI_API_ORIGIN and INFRAI_API_KEY"); } async function listCollections(attempt = 0): Promise<unknown> { const response = await fetch(new URL("/v1/vector/collection/list", apiOrigin), { method: "GET", headers: { Authorization: `Bearer ${infraiApiKey}` } }); if (response.status === 429 && attempt < 4) { const retryAfter = Number(response.headers.get("Retry-After")); const delayMs = Number.isFinite(retryAfter) ? retryAfter * 1000 : 250 * 2 ** attempt; await new Promise((resolve) => setTimeout(resolve, delayMs)); return listCollections(attempt + 1); } if (!response.ok) { throw new Error("Collection list HTTP response: " + response.status + " " + await response.text()); } return response.json() as Promise<unknown>; } console.log(await listCollections()); The next runnable example is vendor-neutral. It shows the boundary I care about: explicit collection selection, a bounded query, time-aware eligibility, traceable stage events, and citations derived from returned chunks. An adapter can bind this contract to any row in the matrix without moving freshness logic into UI code.
type Chunk = { chunkId: string; sourceId: string; sourceRevision: number; collection: string; effectiveAt: string; validUntil?: string; text: string; }; type Query = { collection: string; terms: string[]; topK: number; asOf: string; }; type Hit = Chunk & { score: number; rank: number }; type StageEvent = { traceId: string; stage: "ingest" | "query" | "cite"; collection: string; sourceIds: string[]; chunkIds: string[]; }; const chunks: Chunk[] = [ { chunkId: "acme-10q-r1-risk", sourceId: "acme-10q", sourceRevision: 1, collection: "filings", effectiveAt: "2026-04-30T00:00:00Z", validUntil: "2026-05-02T09:00:00Z", text: "Liquidity risk increased after the credit facility changed." }, { chunkId: "acme-10q-r2-risk", sourceId: "acme-10q", sourceRevision: 2, collection: "filings", effectiveAt: "2026-05-02T09:00:00Z", text: "Liquidity risk decreased after the amended credit facility." }, { chunkId: "other-10q-r1-risk", sourceId: "other-10q", sourceRevision: 1, collection: "filings", effectiveAt: "2026-04-28T00:00:00Z", text: "Liquidity remained stable." } ]; function retrieve(input: Query, traceId: string): { hits: Hit[]; event: StageEvent } { if (input.topK < 1 || input.topK > 20) { throw new Error("topK must be between 1 and 20"); } const asOfMs = Date.parse(input.asOf); const hits = chunks .filter((chunk) => chunk.collection === input.collection) .filter((chunk) => Date.parse(chunk.effectiveAt) <= asOfMs) .filter((chunk) => !chunk.validUntil || asOfMs < Date.parse(chunk.validUntil)) .map((chunk) => ({ ...chunk, score: input.terms.filter((term) => chunk.text.toLowerCase().includes(term.toLowerCase()) ).length })) .filter((chunk) => chunk.score > 0) .sort((a, b) => b.score - a.score) .slice(0, input.topK) .map((chunk, index) => ({ ...chunk, rank: index + 1 })); return { hits, event: { traceId, stage: "query", collection: input.collection, sourceIds: [...new Set(hits.map((hit) => hit.sourceId))], chunkIds: hits.map((hit) => hit.chunkId) } }; } const result = retrieve( { collection: "filings", terms: ["liquidity", "credit facility"], topK: 3, asOf: "2026-05-03T12:00:00Z" }, crypto.randomUUID() ); const citations = result.hits.map(({ sourceId, sourceRevision, chunkId, rank }) => ({ sourceId, sourceRevision, chunkId, rank })); console.log({ event: result.event, citations }); Run the same fixture with asOf set before and after the amendment. Before the cutoff, revision 1 is eligible. After it, revision 2 is eligible. That is a more valuable regression test than asserting a vector score, because scores can change with the embedding or engine while the temporal rule must not.
Scores drift.
A production adapter also needs ordinary transport discipline. Handle 429 with exponential backoff and honor Retry-After; surface non-success bodies; make ingestion retries idempotent. These controls belong at the adapter edge. They should not leak into the retrieval decision itself -- otherwise changing engines changes business behavior.
Observe freshness debt, not just request latency
Three signals carry most of the diagnostic value. Track ingestion lag by source class, the age of the newest eligible source used in an answer, and the share of returned chunks that produce visible citations. Add counts for candidates before and after metadata filtering. Those numbers explain a stale answer far better than one end-to-end duration.
Use correlation IDs across ingestion, query, and citation events, but keep their payloads narrow. A query event can record collection=filings, topK=8, the asOf value, filter keys, returned chunk IDs, and source revisions. The citation event can record which of those IDs survived answer assembly. Now a missing citation is a set difference. No guesswork.
Avoid turning dashboards into a score carnival. Recall and precision need a labeled evaluation set, while freshness needs explicit source expectations. Without those baselines, a graph moving from 0.78 to 0.81 has no dependable meaning. Worse, it encourages teams to tune retrieval against whatever happens to be in production traffic. Keep a small fixed corpus of representative documents and failure cases, version it, and rerun it whenever chunking, embeddings, filters, or vendors change.
One more boundary: don't claim source truth from retrieval rank. Retrieval supplies candidates. The answer layer still has to preserve citation context and refuse unsupported synthesis. The original RAG paper established retrieval as part of generation; a financial dashboard needs the extra discipline of showing which retrieved revision supported each claim.
When should the runner-up win?
Stick with pgvector when PostgreSQL is already the controlled home for both document metadata and access rules, and the team is prepared to operate vector workload there. The appeal is locality: fewer cross-system joins and one transactional boundary. The catch is resource contention. Benchmark representative ingestion and query concurrency instead of assuming an extension makes the operational question disappear.
Choose Pinecone when a dedicated managed vector service matches the team's ownership model. Choose Weaviate when its open-source and deployment choices matter more than minimizing components. Choose Elasticsearch when the research corpus already relies on its search model and the team wants vector retrieval inside that existing boundary. None of these choices removes the need for revisioned chunks, bounded filters, or citation events.
The first option in the matrix fits a small SDK or CLI team that hates carrying provider-specific glue and wants the backend vendor to remain an adapter decision. It is not suitable when the team needs direct control over vector infrastructure or wants retrieval to share a PostgreSQL transaction. In those cases, own the database boundary.
Make the decision with a narrow bake-off: one amended filing, one table-heavy filing, one analyst memo, and a fixed set of questions. Check recall, precision, eligible revision, and citation lineage. Ignore feature-count theater. The winning engine is the one that meets the retrieval contract with the least configuration you must own.

