01The problem
We were building an agentic system to search across millions of financial documents: the filings, reports, and contracts that hold numbers people actually act on. Two things make that hard in a way ordinary search is not built for.
The answers have to be checkable. When an agent pulls a figure out of a sensitive filing, or works one out across several, you cannot take its word for it. You need a trace: exactly which documents, which facts, and which steps produced the answer, so a person can verify it before anyone acts on it. In finance, "the model said so" is not an answer.
And the questions are not lookups. They are comparisons, inferences, and temporal questions, and no single passage answers them:
comparisonwhich of these two issuers carries more leverage?
inferencewho is the ultimate parent, three ownership hops up?
temporalhow did this segment's revenue move across the last three filings?
They need search by meaning, reasoning across many documents, and an answer you can audit.
We looked at what exists, and at our scale no single system did all of it:
- A connection database (Neo4j) gives you the trace and can follow inferences across documents, but it cannot search by meaning, and it needs an expert to hand-design a schema for every new kind of document.
- A vector database (Milvus) searches by meaning at scale, but it returns no trace to verify, and it does not reason: it retrieves passages and hands them to a language model to guess the answer.
- A reasoning retriever (PageIndex) does produce a trace and reasons well over a document, but by design it abandons meaning-based search, and it drives that reasoning with a language-model call at each step of its tree, so searching millions of documents economically is not what it is built for.
So we built the system this post is about: search by meaning and traceable, verifiable inference and comparison and temporal reasoning, over millions of documents, on plain Postgres. The rest is how it held up.
02The approach
We do two things at once. First, we search by meaning rather than by exact wording: the system recognizes what a passage is about, so a question phrased nothing like the source can still find it. Second, we keep track of how facts relate to one another, so the system can follow the connections from one document to the next instead of judging each page on its own.
The part we are proud of is that those connections are discovered automatically from your documents. Nobody has to sit down and design a data model for your subject first. You point the system at a new set of documents and it works. Exactly how the connections are discovered, and how the two kinds of search are combined, is the part we keep to ourselves. This post is about what that combination does for you, not how it is built.
Here is the part an operations team will appreciate: there is no dedicated vector database and no separate graph database anywhere in this design. The engine does both jobs directly on PostgreSQL, the same ordinary, widely used database that millions of teams already run, using pgvector, a standard, free add-on for meaning-based search, alongside everyday tables. The engine itself is just an application that talks to that one database, the way any service talks to its store. So there is one database to back up, secure, and monitor: no second data system to run, scale, or keep in sync, and no second copy of your data living somewhere else.
03Advantages
The system brings together two well-established strengths that normally live in two different products, plus one thing neither of them does. Here is what each buys you, in plain terms.
- It finds information by what it means, not the exact words you typed. Ask a question worded nothing like the document that answers it, and it still comes back. There is no special query language to learn and no need to guess the one magic keyword. You ask in ordinary language, the way you would ask a colleague. And it reads your documents as they already are, messy and unstructured, so there is no tagging, cleanup, or reformatting to do first. This is the strength that dedicated meaning-based search products (such as Milvus) are known for.
- It connects related facts across different documents. Many questions cannot be answered by any one page. The answer lives in how a few facts link together, often scattered across separate documents. Because the system keeps track of those connections, it can follow the trail from one fact to the next instead of weighing each page in isolation. That is what lets it handle multi-part questions that single-page search simply cannot. This is the strength that connection-mapping (graph) databases (such as Neo4j) are known for.
- The structure is induced, not designed, so there is no data model to build for each subject. This was the whole game for us. We are one product sold to many companies, each with its own domain-heavy documents, and we could not hand-build a data model for every one, which is exactly what a traditional connection database demands: an expert deciding up front what kinds of things exist and how they may connect, redone from scratch for every customer and every subject. So we do not design the structure at all. The engine induces it from each company's own documents, filings, medical records, or product manuals alike, with nothing to model, write down, or maintain, and no specialist in the loop.
- You get both capabilities, with the setup of neither. Meaning-based search and connection-following normally mean two specialized systems to buy, run, and reconcile against each other. Here you get both at once, without either system's setup or upkeep, from the single ordinary database you are already running.
The easiest way to see the difference is against the two kinds of product teams usually reach for. A meaning-based search product understands language but does not track how facts connect. A connection-mapping database tracks how facts connect but needs an expert to design its data model for every new subject, and it is a whole extra system to run. We take the genuinely useful half of each, and ask you to run neither.
| What it does | Ours | Meaning-based search (e.g. Milvus) |
Connection database (e.g. Neo4j) |
|---|---|---|---|
| Finds information by meaning, not exact words | Yes | Yes | Added recently, not its focus |
| Follows connections between facts across documents | Yes | No | Yes |
| Both of the above, in one system | Yes | No | Only if you model it yourself |
| No data model to design per subject | Yes | Yes | No |
| Nothing to run beyond an ordinary database | Yes | No | No |
04Financial inference
On financial data the contest stops being about recall and becomes about trust. A retrieve-and-summarise system finds the passages and hands them to a language model, which reads the figures off and does the arithmetic itself, so it can misread a number, miscount a total, or invent one outright. Because the graph is typed, it does something a retriever cannot: it infers and verifies the answer. Feed the engine a set of filings and it reads a financial vocabulary straight out of them, with nobody writing it down: types like company and segment, relations like owns and reports, figures like revenue and fiscal year. Over that vocabulary runs a small set of typed operations:
- Read a figure. Pull the revenue, or the fiscal year, off a named company or one of its segments.
- Follow a relation. Walk an ownership chain, or list the segments a company reports, along the specific kind of link that means it.
- Compute, with the units checked. A ratio between two figures, or a segment's share of total revenue. The units have to line up, so "dollars over dollars" is allowed and "dollars over years" is refused before it ever runs.
- Compare and infer, soundly. Whether one figure exceeds another, or whether ownership carries down a chain. Every answer arrives with the steps that justify it.
Take a concrete one. To answer "what is Segment A's share of company revenue?" the engine reads two figures off the filing, Segment A's revenue and the company's revenue, then divides them with the units checked. Neither number is generated, and the division is verified, not guessed.
| Ask a filing | Retrieve, then let a model compute (e.g. Milvus) | Read, then compute by rule (ours) |
|---|---|---|
| A single figure, like a segment's revenue | finds the passage, then a model reads the number off and can misread it | reads the exact figure off the filing, grounded to its source |
| A ratio, like a segment's share of revenue | the model does the division itself and can get it wrong | computed by rule with the units checked, or refused |
| A total across subsidiaries | the model has to find and add every one, and may miss some | walks the ownership links and totals them, every step audited |
| Can you trust the number? | no guarantee it was not invented or miscalculated | every figure is read, every step leaves a paper trail |
Two things make this trustworthy for numbers. Every figure in a calculation is read from a filing, never generated, so a model cannot slip in a value that was not on the page. And the arithmetic is checked, not guessed, so "segment revenue over total revenue" is either computed exactly or refused, never approximated. The result is an auditable number with a paper trail to its source, which is the bar financial work actually holds. A system that only fetches passages and asks a language model to read the figures off cannot promise any of that. Paired with a strong frontier model it will often be right, and such models are capable calculators, but it rests on the model reading every figure correctly and never slipping a digit, with no guarantee that it did. On a financial number, usually right is not the same as right.
05Typed queries
The connections the system builds are not just links, they are typed links: it learns that one page is about a film, another a person, and that a relationship like "directed by" joins a film to a person. That is what lets it answer a precise, multi-step question by walking the right kind of link at each step, rather than hoping the wording lines up.
Take a real one: "What nationality is the director of Dark River?" The film's page names its director but says nothing about nationality. The director's page gives the nationality but never mentions the film. Neither page answers the question on its own. The typed links do: the system knows a film connects to a person by "directed by," and a person to a country by "nationality," so it walks the two hops and reads off the answer.
The typing also keeps the search efficient. Following a specific kind of link at each step, rather than fanning out to every neighbour and sifting the pile afterwards, keeps the walk small and precise even as the graph grows large. And none of it changes how you ask: you type an ordinary question, and the typed structure does the walking.
06Results
On plain Postgres, our engine beats Milvus's Vector-Graph RAG, the top-scoring vector-graph retrieval system in this field, on all three public benchmarks, and posts the best average of the group (88.6 vs 87.8). No dedicated vector or graph database.
We measured on three public question-answering benchmarks: standard, openly available collections of hard, multi-part questions that researchers use to compare systems fairly. Recall@5 = the share of a question's supporting passages found in the top 5 results (standard passage recall), averaged over questions. Higher is better. The headline: on plain Postgres we beat the leading vector-graph system (Milvus) on all three benchmarks, and post the best average of the group. Competitor figures are published numbers, cited below.
| Recall@5 | MuSiQue | HotpotQA | 2Wiki | Average |
|---|---|---|---|---|
| Ours (on Postgres) | 73.2 | 97.0 | 95.6 | 88.6 |
| Vector Graph RAG (Milvus) | 73.0 | 96.3 | 94.1 | 87.8 |
| HippoRAG 2 | 74.7 | 96.3 | 90.4 | 87.1 |
| NV-Embed-v2 | 69.7 | 94.5 | 76.5 | 80.2 |
| Naive RAG | 55.6 | 90.8 | 73.7 | 73.4 |
Underlines mark where our Postgres result is the top figure among the systems shown: HotpotQA (97.0), 2Wiki (95.6), and the three-benchmark average (88.6), the best average of any system here. We beat Milvus on all three benchmarks; on MuSiQue (73.2) we edge Milvus (73.0), with only HippoRAG 2 higher (74.7). All three are measured on one binary, n=1000, standard Recall@5, on nothing but Postgres.
Competitor figures: Milvus "Vector Graph RAG" evaluation (zilliztech.github.io/vector-graph-rag/evaluation).
07Trade-offs
Being honest about where this helps least matters as much as showing where it helps most.
- The biggest gains are on the hard questions. Where the improvement really shows is multi-part questions whose answer is spread across sources. For simple questions that a single document already answers well, ordinary search is nearly as good, so the extra benefit there is small.
- It is only as good as the source it is given. The connections are drawn from your documents, so garbled or very poor-quality source material produces weaker results. Clean, readable sources get the most out of it.
- Some questions stay hard. A minority of questions gain nothing from the added connections and remain difficult. The approach narrows the gap; it does not erase it.
- Measured on public tests, not on everything. Our evidence comes from three public question-answering benchmarks, which are demanding and widely used sets of multi-part questions. They are a strong test, but no benchmark can stand in for every subject area or workload.
08Memory and performance footprint
The whole benchmark above ran on a single Mac. MuSiQue is not a toy set: 11,656 passages, roughly 92,000 distinct entities, and 103,000 relationships between them. From one binary of about 80 MB, against an ordinary Postgres, the engine built the graph, ran the multi-step retrieval, and scored all 1,000 questions on that one machine. No cluster, no message queue, no object store, nothing to stand up.
That is a deliberately different design point from a dedicated vector database. Milvus is a fully distributed, Kubernetes-native system written in Go and C++, built to scale horizontally to billions of vectors across a cluster; even its single-machine image bundles its own storage and coordination services. That engineering buys enormous scale, and at billions of vectors it is the right tool for the job. But it is a system to deploy and operate. Ours is a small application on the database you already run, light enough to build, test, and run the entire pipeline, benchmark and all, on a single machine.
And small does not mean it cannot grow. Because the engine holds no state of its own, scaling it up is simply running more copies of it: put it in Kubernetes behind an autoscaler and it adds and sheds instances with load, every one of them sharing the same Postgres. The same build that runs the whole pipeline on one machine serves production traffic across a fleet, with nothing to re-architect in between.
Competitor numbers are published figures, shown for orientation. Our numbers are from a verified end-to-end run (n=1000 per benchmark, standard Recall@5, gpt-oss-120b listwise rerank on plain Postgres).