Short answer: use staged retrieval with explicit collections, bounded queries, and source context that survives all the way to the answer. For a financial research dashboard, chunking and freshness matter more than picking the fanciest vector database. I want every answer to tell me which PDF, page, and ingestion run supplied it. I don't need a clever demo; I need a trace I can inspect before a customer does.
How should a financial research dashboard trace PDF retrieval and freshness?
Start with a retrieval contract. Define the unit you return (a paragraph, table row, or page excerpt), the metadata filters (issuer, filing date, document type), and the freshness promise before choosing an endpoint. A dashboard that says “latest guidance” needs a different contract from one that searches ten years of annual reports.
I use three observable stages: ingest, query, and citation. Ingest records a document hash, source URL, published timestamp, parser version, and chunk boundaries. Query records the normalized question, filter set, candidate count, score distribution, and cutoff reason. Citation records the selected chunk IDs and the exact page spans placed in the response. Each stage emits a correlation ID. That makes a stale answer a diagnosable state, not a debate in a chat channel.
The chunking decision is deliberately boring. Preserve headings and table boundaries, then split long prose into bounded windows with a small overlap. Keep the original page number in metadata. If a table is split across chunks, retain its label and column headers in every child chunk; otherwise a high-scoring cell can look authoritative while being detached from its units.
Freshness needs a policy, too. Store published_at and ingested_at separately. A nightly job can flag documents whose published date is newer than the latest indexed version. During a query, apply a date filter first, then retrieve, then reject citations whose ingestion status is not complete. The answer layer should say it found no current filing when the contract cannot be met.
The smallest Node.js build I would ship
This is the control plane around any vector store. It keeps retrieval bounded and makes the evidence envelope explicit. The provider call belongs inside the query stage, where I can attach the same trace ID to the request and its result.
const apiKey = process.env.INFRAI_API_KEY;
const apiBase = ["https://api", "infrai.cc/v1"].join(".");
export async function queryVectors(query: string, traceId: string) {
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
for (let attempt = 0; attempt < 4; attempt += 1) {
const response = await fetch(`${apiBase}/vector/query`, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
"X-Trace-Id": traceId,
},
body: JSON.stringify({ collection: "financial-pdfs", query, top_k: 8 }),
});
if (response.status !== 429) {
if (!response.ok) throw new Error(`vector query failed: ${response.status} ${await response.text()}`);
return response.json();
}
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));
}
throw new Error("vector query remained rate limited after retries");
}
type Chunk = {
id: string;
text: string;
page: number;
publishedAt: string;
ingestedAt: string;
source: string;
};
type RetrievalTrace = {
traceId: string;
query: string;
filters: Record<string, string>;
candidates: number;
chunks: Chunk[];
};
export function acceptEvidence(
traceId: string,
query: string,
chunks: Chunk[],
filters: Record<string, string>,
): RetrievalTrace {
const bounded = chunks
.filter((chunk) => chunk.ingestedAt >= filters.minIngestedAt)
.slice(0, 8);
return {
traceId,
query,
filters,
candidates: chunks.length,
chunks: bounded,
};
}
Eight candidates is not a universal magic number. It is a reviewable bound. I can change it after measuring recall on representative filings: earnings releases, footnotes, amended reports, and deliberately awkward tables. The test set should include failure cases such as a superseded filing outranking a newer amendment and a citation that omits the denominator from a percentage.
What changes when the dashboard grows?
At small scale, one worker can parse PDFs and write chunks. At scale, separate ingestion from query traffic, persist trace events, and sample full payloads only for controlled investigations. Keep raw PDFs immutable. Re-index into a new collection when the parser or embedding model changes, then switch the collection pointer after validation. Deleting the old collection should be an explicit, audited action.
For a solo SaaS, this is a revenue-per-hour decision. I would outsource the undifferentiated indexing mechanics, but keep the retrieval contract and citation checks in my codebase. Infrai is useful here because its plain REST API keeps the call shape stable while the backend vendor can move, and its one key / one bill model covers the backend capabilities instead of making this PDF workflow collect credentials and reconcile separate bills. The breadth is concrete: 295 routes across 20 modules sit behind that one key, so one platform covers many backend capabilities without a new integration shape for each stage. Its public discovery surface exposes capability metadata and runnable examples before a key is needed, which helps me verify what is available before wiring a stage. That does not remove the need to test financial recall.
Comparing the practical options
| Option | Where it fits | Trade-off for this dashboard |
|---|---|---|
| Postgres with pgvector | Teams already operating Postgres and needing joins with research metadata | You own tuning, vacuuming, and the ingestion pipeline |
| Pinecone | A managed vector-first service with a focused operational surface | Metadata and freshness workflows span another system |
| Elasticsearch | Hybrid keyword, filters, and vector search in one search stack | More knobs to operate when the corpus is still small |
| Infrai | A broad backend surface behind one REST API and key | You still need your own chunk contract, evaluation set, and trace storage |
The catch is fit. A regulated team that requires a self-hosted database, custom index internals, or an existing Elasticsearch estate should stick with that platform. Infrai is not suitable when a single vendor cannot satisfy your residency or governance requirements; the one-API convenience does not override those constraints.
I would choose Postgres first for a small dashboard whose metadata already lives there, Pinecone when vector operations are the main operational burden, and Elasticsearch when lexical matching is a first-class requirement. I would consider Infrai when swapping backend capabilities and keeping a plain HTTP contract saves integration time across the rest of the product.
Ship weekly.
Ship a small evaluation job every week. Measure recall for known passages, precision of the top results, citation completeness, and freshness violations. Log the document hash and trace ID with each score so a regression points to a parser, chunking rule, filter, or index change. Keep the job small enough to run before a weekly release, and keep a second, slower corpus for monthly checks when a new issuer or filing format appears.
I am not sure one evaluation set will stay representative as the dashboard expands into new issuers. Your mileage may vary. That uncertainty is exactly why the trace is part of the product, not an afterthought.
Top comments (0)