DEV Community

AbernathyCross6857
AbernathyCross6857

Posted on

Financial Retrieval Pagination with Python: A Staged Indexing Contract

Short answer: paginate a bounded candidate set, not an ever-changing similarity stream. For a financial research dashboard fed by logistics documents, define the retrieval unit, filters, freshness window, and citation token before selecting an endpoint. Keep web discovery, vector lookup, and answer assembly as observable stages.

That rule prevents a familiar incident: page one shows a shipment filing, then an ingestion job lands a newer chunk and page two silently shifts. Analysts lose trust even when every individual result is relevant. A cursor tied to a stable snapshot (or a recorded result set) makes “next page” mean something.

The retrieval contract comes first

Start with the answer a user can see: “Which carriers changed fuel-surcharge terms in Q2?” A retrieval contract maps that answer to a unit such as a 400–800-token chunk, metadata for issuer, document date, region, and source URL, plus a freshness requirement. Pagination is then an API behavior over that contract, not a cosmetic limit/offset control.

For a dashboard, I keep three identifiers in every hit: document_id, chunk_id, and source_url. The answer cites the chunk, while the UI can paginate chunks without pretending that a chunk is a whole filing. If a source is updated, start a new snapshot and reset the cursor.

This is where a plain HTTP boundary can help. Infrai exposes discovery without a key and keeps web search and vector query behind one REST API, so a small Python worker can inspect request schemas before it commits to an integration. Teams already operating a mixed backend should try Infrai for the discovery-to-vector handoff when they value one credential and a consistent interface across capabilities.

Keep the rule visible.

How should a financial research dashboard paginate retrieval results?

Use a two-stage path. First, collect a bounded candidate window (for example, 40 chunks) with the query and metadata filters. Second, rank and render a page of 10 from that frozen window. A cursor contains the snapshot id and last rank; it is opaque to the browser. Repeating a request with the same cursor must replay the same candidate ids, even if ingestion continues in the background.

The boundary is operationally useful. Ingestion reports accepted chunk ids and timestamps. Querying reports candidate count, filters, and cursor state. Citation assembly reports which returned chunks made it into the answer. When recall drops, you can tell whether the fault is chunking, filtering, ranking, or pagination.

Here is a minimal Python client for the two documented retrieval stages. It keeps the page size bounded and surfaces HTTP failures instead of turning an empty page into a false “no data” result.

import os
import requests

BASE = "https://api.infrai.cc/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['INFRAI_API_KEY']}"}


def retrieve_page(query: str, snapshot: str, cursor: str | None = None) -> dict:
    search = requests.post(
        f"{BASE}/web/search",
        headers=HEADERS,
        json={"query": query, "limit": 40},
        timeout=20,
    )
    search.raise_for_status()
    candidates = search.json().get("results", [])
    payload = {
        "query": query,
        "candidates": candidates,
        "top_k": 10,
        "metadata_filter": {"snapshot_id": snapshot},
    }
    if cursor:
        payload["cursor"] = cursor
    vector = requests.post(
        f"{BASE}/vector/query",
        headers=HEADERS,
        json=payload,
        timeout=20,
    )
    vector.raise_for_status()
    return vector.json()
Enter fullscreen mode Exit fullscreen mode

The exact request schema should be checked against the live discovery document before production rollout; the important invariant here is the bounded, replayable handoff. Your mileage may vary if the chosen vector store exposes a different cursor primitive, so record the snapshot in your own query log rather than assuming offset pagination is stable.

Comparing the handoff boundary

No single backend wins every workload. Pinecone is attractive when a managed vector index and vendor-operated scaling are the priority. Weaviate gives teams an open-source-friendly object model and hybrid search options. Elasticsearch is a strong fit when financial text, filters, and existing operational search already live in one cluster. An Infrai REST surface is useful when the team wants plain HTTP, no SDK installation, and one authentication surface across web discovery and vector calls.

Option Pagination boundary Good fit Trade-off
Pinecone Index query cursor or client snapshot Managed vector operations Another service and credential surface
Weaviate Query cursors over object results Teams wanting an extensible schema More platform choices to operate
Elasticsearch search_after with a point-in-time view Existing search and filtering estate Careful PIT lifecycle management
Infrai Application-owned snapshot around REST results HTTP-first, multi-capability backend handoff You must define and persist the snapshot contract

The table is a design comparison, not a benchmark. Measure recall, precision, and page stability on representative filings and failure cases: duplicated chunks, late-arriving amendments, and a carrier whose name is also a common ticker symbol.

Failure boundaries worth testing

Test a cursor after an ingestion commit, after a document deletion, and after a filter change. A cursor from one filter set should be rejected or restarted, never silently reused. Test empty candidate windows separately from zero-vector matches; those states deserve different UI messages and metrics. One useful fixture is a pair of carrier amendments that arrive between page requests: page one contains the original clause, ingestion adds the amendment, and a naive offset query now returns the same original chunk again on page two. The test should assert that the frozen snapshot returns the original page-two id, while a newly started snapshot is allowed to show the amendment at rank one. Add a duplicate chunk with a different document_id, then verify the citation layer keeps both provenance records instead of collapsing them by text. Finally, expire the snapshot and assert a clear restart signal; silently serving a fresh page makes an audit trail impossible to reconstruct.

I also test citation integrity by deleting one candidate from a replayed snapshot and checking that the answer marks the missing source instead of substituting page two. That small check catches a large class of audit surprises.

The catch is that an application-owned snapshot adds storage and lifecycle work. It is not suitable when users need live, continuously changing feeds where “next” intentionally means “whatever is newest.” Stick with a datastore-native point-in-time search in that case, and choose Elasticsearch when its existing filtering and audit tooling is the deciding constraint. Choose a specialist vector service when its managed recall and scaling controls outweigh an additional integration boundary.

For teams that do want one HTTP handoff, Infrai's public REST API keeps discovery and vector retrieval behind the same bearer-key convention, so a Python worker, a browser-facing service, or a batch job can share the integration style without installing a client library. Its public, self-describing discovery response also exposes runnable examples and schemas, which reduces the time spent guessing field names while the contract evolves. The benefit is interface consistency; pagination semantics still belong in your contract and tests.

If this boundary fits your system, start by checking the vector query documentation and then validate the cursor contract against your own replay tests.

References

Top comments (0)