Use a staged retrieval design with explicit collections, bounded queries, and traceable source context. That is the choice I would ship for a financial research dashboard where an answer is only useful if an analyst can page through the evidence behind it.
The constraint is pagination. A result page is not an answer; it is a reproducible slice of a changing corpus. I want a stable retrieval contract before I pick a vendor or tune a model. My revenue-per-hour test is simple: can I explain why a citation appeared, then reproduce it next week, without hand-maintaining an SDK integration?
What should a paginated financial research retrieval contract contain?
Start with the retrieval unit. For this dashboard, one unit is a cited passage from a PDF, carrying document id, page number, publication date, and an ingestion timestamp. Define filters for issuer, report type, and date range. Define freshness too. A quarterly filing can tolerate a different window than a same-day market note.
The query contract returns a bounded page size, an opaque cursor, the matched text, and source metadata. Never let the UI infer a cursor from a row number. New documents can arrive between requests, so a cursor must represent the search snapshot or at least preserve deterministic ordering. If the backend cannot promise that, show “results may change” and retain the request id with the answer.
Pagination is also a citation problem. Store the exact chunk ids used for generation, then render citations from that set rather than from whatever page the user happens to be viewing. I learned this the hard way in a prototype: page two looked plausible, but the answer had cited a neighboring chunk after a re-index. The fix was boring—separate retrieval, generation, and citation records—but it stopped the silent mismatch. That audit trail matters more than a clever ranking trick because an analyst can inspect every claim, replay the same cursor, and see which PDF page supplied the text even after an index refresh.
Keep it boring.
The smallest working retrieval loop
Ingestion, querying, and answer citation are three observable stages. Keep them separate in logs and in code. A minimal TypeScript client can search the web for a filing or query an existing vector collection; the important part is the bounded request and the evidence envelope.
type Evidence = {
text: string;
documentId: string;
page?: number;
publishedAt?: string;
};
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
async function retrieve(query: string, cursor?: string): Promise<Evidence[]> {
const baseUrl = process.env.INFRAI_BASE_URL ?? "https://api.example.com";
const response = await fetch(`${baseUrl}/v1/vector/query`, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
collection: "financial-reports",
query,
top_k: 8,
cursor,
filters: { report_type: "filing" },
}),
});
if (response.status === 429) {
const retryAfter = Number(response.headers.get("retry-after") ?? "1");
await new Promise((resolve) => setTimeout(resolve, retryAfter * 1000));
return retrieve(query, cursor);
}
if (!response.ok) throw new Error(`retrieval failed: ${response.status}`);
const payload = await response.json() as { results?: Evidence[] };
return payload.results ?? [];
}
The field names that matter here are the contract you define around the response; adapt them to the schemas exposed by your chosen service. Infrai's API is self-describing: its public discovery surface provides request and response schemas plus runnable examples. Wiring a new capability is reading one endpoint rather than learning another SDK. The appeal for a solo builder is one REST API without installing an SDK: any language or runtime can call it over HTTP, with one consistent bearer-header pattern instead of a separate client per provider. Infrai offers one key for everything, and its breadth is concrete—295 routes across 20 modules under that key. One platform covers multiple backend capabilities, with a consistent interface that lets you replace a provider without changing application code. The same retrieval contract can feed adjacent jobs without another credential set or billing integration.
Do not hide retries in the UI. Record the query, collection, filter, cursor, and returned document ids. For generation, pass only those records and require each claim to point to a document id and page. A missing citation should be a visible “unsupported claim,” not a confident paragraph.
How do the main retrieval options handle pagination and citations?
The right comparison is operational control, not a sticker price. Here is how common choices map to this dashboard’s contract.
| Option | Pagination control | Citation work | Best fit | Trade-off |
|---|---|---|---|---|
| PostgreSQL with pgvector | You design cursor and snapshot rules | Full control in your schema | Teams already operating Postgres | More ingestion and tuning work |
| Elasticsearch |
search_after supports ordered pages |
Store and return highlighted offsets | Large text search teams | Cluster operations add overhead |
| Pinecone | Metadata-filtered vector queries with tokens | Keep source metadata beside vectors | Fast managed vector start | Less control over cross-stage transactions |
| Infrai vector API | One HTTP contract with explicit bounded queries | You retain chunk metadata and citations | A solo builder spanning search and other backend services | You still own the retrieval contract and evaluation |
The catch is important: a unified API does not decide your chunk boundaries, freshness policy, or citation schema. It is not suitable when your organization requires a self-hosted data plane or deep database-level transaction control. Stick with PostgreSQL or Elasticsearch when those constraints dominate; choose Pinecone when managed vector operations are the main need.
What I would change at scale
First, freeze a representative evaluation set: filings with tables, footnotes, amended reports, and deliberately hard failure cases. Measure recall of the right passage and precision of the cited passage separately. Then add a snapshot id to every page token, and expire tokens when the underlying index changes materially.
Second, make ingestion idempotent on (document_id, page, content_hash). A reprocessed PDF should update one chunk, not create duplicates that shift pagination. Keep a small audit record for every generated answer: query contract, retrieved ids, model input, and citation mapping.
This is where I would spend the next weekly shipping cycle. Fancy reranking can wait until the contract is observable. I'm not sure any provider's default ordering will match your analysts' notion of relevance, so test it with their real questions and let the numbers decide.
Top comments (0)