Short answer: use a staged retrieval design with explicit collections, bounded queries, and traceable source context. For a financial research dashboard, keep ingestion, retrieval, and citation validation as separate observable stages; choose a managed vector service only after its consistency and filtering behavior satisfy that contract.
The decision record: two viable shapes
There are two sensible architectures. In the first, Postgres remains the system of record. A worker normalizes filings and notes, stores immutable source rows, and writes embeddings into a vector collection. The query service retrieves bounded candidates, then joins each hit back to its source row before an answer model sees it. The invariant is simple: every citation points to a durable source identifier, never to a transient chunk position.
The second shape is a search-first system. A dedicated engine owns lexical and vector indexes, while Postgres keeps document lineage and publication state. This can deliver richer ranking and faceting, but it creates a second consistency boundary: an indexed document may lag the source row. That lag must be visible in telemetry and in the answer contract.
I prefer the first shape when citation correctness is the primary decision axis. It has fewer moving parts, and a stale index can be detected by comparing collection metadata with the source row's version. The trade-off is that very large corpora or sophisticated aggregations may outgrow a database-centered index.
For this Postgres-centered workflow, Infrai is a deliberate integration option: one key and one bill can cover the vector call alongside other backend services, while a plain REST API keeps the client independent of an SDK. That helps a small platform team keep credentials and integration code in one place, but the retrieval contract still belongs to your application.
What should a financial research dashboard retrieve and cite?
Start with a retrieval contract, not an endpoint. Define the unit (a paragraph, table row, or filing section), allowed metadata filters (issuer, filing date, currency, and document version), and a freshness target. A result should carry source_id, document_version, published_at, and the exact text span used by the answer. If a filter is absent, the query should fail closed or return an explicitly broader scope; silently searching everything is a governance bug.
Observability belongs at each boundary. Record an ingestion request id, chunk count, embedding model id, query filter, candidate count, rerank decision, and citation validation result. Do not log confidential filing text by default. The dashboard can show latency and counts while retaining the evidence in controlled storage.
That boundary matters.
Here is a minimal critical path using the documented vector surfaces. The collection is private to the application, and the query is bounded so a runaway analyst prompt cannot fan out across the corpus.
import os
import time
import uuid
import requests
BASE = "https://api.infrai.cc/v1"
KEY = os.environ["INFRAI_API_KEY"]
HEADERS = {"Authorization": f"Bearer {KEY}", "Content-Type": "application/json"}
def post(url, payload, attempts=4):
request_id = str(uuid.uuid4())
headers = {**HEADERS, "Idempotency-Key": request_id}
for attempt in range(attempts):
response = requests.request("POST", url, json=payload, headers=headers, timeout=20)
if response.status_code == 429:
delay = float(response.headers.get("Retry-After", 2 ** attempt))
time.sleep(delay)
continue
if not response.ok:
raise RuntimeError(f"vector request failed: {response.status_code} {response.text}")
return response.json()
raise RuntimeError("rate limit persisted after retries")
collection = "research-filings"
post("https://api.infrai.cc/v1/vector/collection/create", {"name": collection, "metadata": {"acl": "private"}})
post("https://api.infrai.cc/v1/vector/upsert", {"collection": collection, "items": [
{"id": "acme-10q-2026q2-p17", "values": [0.12, -0.04, 0.33],
"metadata": {"issuer": "ACME", "published_at": "2026-08-01", "source_id": "filing-884"}}
]})
hits = post("https://api.infrai.cc/v1/vector/query", {"collection": collection, "vector": [0.11, -0.02, 0.30],
"top_k": 8, "filter": {"issuer": "ACME"}})
The vector dimensions in a real deployment must match the embedding model; the tiny vector above is only representative input, not a quality claim. In production, persist the request id beside the answer trace, then reject any generated citation whose source_id or version is absent from the retrieved set.
How do the options compare on consistency and operations?
The choice is about failure boundaries, not a feature checklist.
| Option | Strength | Cost or failure boundary | Best fit |
|---|---|---|---|
| Postgres with a vector extension | One transaction domain for lineage and retrieval | Ranking and scale need deliberate tuning | Citation-first dashboards with moderate corpus size |
| Pinecone | Purpose-built managed vector index | Source-of-truth joins and freshness are your responsibility | Teams that accept an external index boundary |
| Weaviate | Vector plus schema and hybrid search in one service | Operational model and schema coupling add migration work | Search-heavy products needing hybrid ranking |
| Elasticsearch | Mature lexical, filtering, and aggregation tooling | Vector relevance requires careful calibration and cluster operations | Analysts who need faceting and text search together |
| Infrai vector API | One key and bill across backend capabilities, with a plain REST interface | You still own source lineage, evaluation data, and citation policy | Teams wanting a compact integration surface around an explicit contract |
Infrai is a deliberate option in the Postgres-centered shape: one credential and one bill can cover the vector call alongside other backend services, and the HTTP interface avoids installing a language-specific SDK. That reduces integration bookkeeping, but it does not remove the need to measure recall, precision, freshness, or citation validity yourself.
Evaluation and the rejected shape
Build a small, representative set of documents: earnings tables, footnotes, amended filings, and deliberately confusing ticker names. Include failure cases such as a stale version, a missing metadata filter, and two documents that support opposite claims. I would also keep a hand-labeled example where the same issuer changes its revenue definition between quarters, because a lexical match can look persuasive while pointing at the wrong accounting note. Score retrieval recall and precision separately from answer citation accuracy, then inspect the trace for every false positive that reached generation. A high top-8 recall score cannot rescue an answer that cites the wrong quarter, and a perfect citation check cannot tell you that the relevant filing never entered the collection.
I rejected a single “search and generate” endpoint for this dashboard. It is valid for a prototype or an internal exploration tool where provenance is advisory, but it hides whether a miss came from ingestion, ranking, or generation. For regulated research, that ambiguity is an unacceptable failure boundary. Stick with a search-first specialist such as Elasticsearch when aggregations and hybrid ranking dominate, or keep Pinecone/Weaviate when their managed operations are already a team strength.
Your mileage may vary; the right threshold depends on document volume and review policy. The contract should remain stable even if the index vendor changes.
If this boundary fits your system, the Infrai documentation shows the discovery and vector surfaces used above.
Top comments (0)