Trustworthy
Why search misses things, and what to do about it.
Your question rarely uses the same words as the document that answers it. This page is the whole mechanism we use to close that gap — including the parts that did not work.
In short
We search three ways at once and merge the results. The plain search always runs, so it can only ever get better — never worse. The extra two passes find content your words did not match, and the documents that back the answer up.
Direct
weight 1.0
always runsHybrid dense and keyword search on the query exactly as it was asked.
This lane is the floor. It runs on every search, at full weight, whatever the graph does or fails to do.
Expanded
weight 0.6
Entities in the query are resolved to graph nodes, traversed two hops, then the query is re-embedded together with the names it reached.
This is how a search reaches content whose wording the question never used. Ask about React and the traversal pulls in JSX and Meta.
Evidence
weight 0.6
The original question is asked again, restricted to the documents that provide provenance for the facts the traversal crossed.
Ranked by corroboration — how many distinct documents assert each fact. This is the lane that finds the passage supporting a point rather than the passage matching a phrase.
Reciprocal rank fusion
The three result sets are merged on rank rather than on score, so no lane can dominate because its similarity numbers happen to be on a different scale. The top documents are then hydrated to chunk text and synthesised into one cited answer.
Lane by lane
What each lane contributes.
Direct
weight 1.0
Hybrid dense and keyword retrieval on the query as written. This lane is why the floor never drops: it runs at full weight on every search, and if everything graph-related fails you are left with a competent vector search rather than an error.
Expanded
weight 0.6
A language-model call extracts entities from the question. Those are resolved against the graph, traversed two hops, and the query is re-embedded together with the entity names the traversal reached. The effect is reaching content whose wording the question never used — asking about React surfaces material that only ever says JSX or Meta.
Evidence
weight 0.6
The original question is asked a second time, restricted to the documents that provide provenance for the facts the traversal crossed, ordered by how many distinct documents assert each one. This is the lane that finds a passage supporting a claim rather than a passage resembling a phrase — and it is why the evidence for an answer is a real set of documents rather than whichever chunk scored highest.
Evidence hydration
Every supporting document, not one arbitrary mention.
When a fact is used, chunks are pulled from all of the documents that assert it — because each source may carry a subtlety the others lack.
The alternative, and the thing most implementations do, is to hydrate one document per fact and move on. That is cheaper and it quietly loses the reason you wanted a graph: three documents agreeing on a fact usually agree for three different reasons, and the useful one is rarely the first row returned.
Entity resolution
One thing, one node — under concurrency.
Resolution runs in three escalating stages, and the whole thing sits inside a transaction for a reason.
Exact match on the normalised name
Identity is the triple of workspace, normalised name and type. Most mentions resolve here and cost nothing.(workspace, normalized_name, type)
Trigram similarity
Catches the typo, the plural, the stray punctuation — the cases where two strings clearly mean one thing.pg_trgm
Embedding similarity
The last resort, for mentions that are semantically the same entity but share no characters. Vectors live in Postgres alongside the rows they describe.pgvector
All of it inside one transaction
This is the part that matters and the part that is easy to get wrong. Two workers extracting two documents that both mention the same thing must end up with one entity, not two. Resolution is transactional so that race cannot happen.why parallel extraction does not duplicate
Answer synthesis
One language-model call, and a prompt that refuses.
The retrieval is only half the product. The other half is not overstating what the retrieval found.
One call, every search
Synthesis is not an opt-in extra tier. If a search returns documents, it returns prose about them.
Grounding in the prompt
The instruction is explicit: answer only from the numbered sources, and say plainly when they do not contain the answer rather than filling the gap from the model's own knowledge.
Markers validated after the fact
Every [n] the model emits is checked against the documents actually returned. Invented markers are dropped rather than passed through.
Graph facts are context, not sources
Traversed facts help the model understand how documents relate. They are never citable, because a citation has to resolve to something you can open.
Diagnostics
Every step, with the latency it actually took.
There is a dedicated diagnostics endpoint, and the Search Playground is a front end for it. Use it to explain why a search returned what it did.
Timings arrive in the normal search response too, so you do not need a special call to find out which lane is slow in production.
{
"steps": [
{ "name": "direct", "ms": 142, "hits": 8 },
{ "name": "entities", "ms": 96,
"resolved": ["Onboarding", "Churn"] },
{ "name": "traversal", "ms": 187,
"hops": 2, "facts": 11 },
{ "name": "expanded", "ms": 131, "hits": 6 },
{ "name": "evidence", "ms": 201, "hits": 6,
"documents": 6 },
{ "name": "fusion", "ms": 3,
"kind": "rrf", "k": 10 },
{ "name": "synthesis", "ms": 890,
"tokensIn": 3184, "tokensOut": 96 }
]
}Graceful degradation
Graph enhances, never replaces.
| If this fails | What you still get |
|---|---|
| The graph store is unreachable | Vector search returns results as normal. The expanded and evidence lanes are skipped. |
| Entity extraction on the query finds nothing | Nothing to traverse, so the direct lane is the answer. No error, no empty response. |
| Traversal reaches no facts | Graph expansion contributes nothing and fusion has one lane to fuse. Results stand. |
| Answer synthesis fails | The response carries a null answer and the ranked documents intact. The caller loses the prose, never the sources. |
What you still get
- The graph store is unreachable
- Vector search returns results as normal. The expanded and evidence lanes are skipped.
- Entity extraction on the query finds nothing
- Nothing to traverse, so the direct lane is the answer. No error, no empty response.
- Traversal reaches no facts
- Graph expansion contributes nothing and fusion has one lane to fuse. Results stand.
- Answer synthesis fails
- The response carries a null answer and the ranked documents intact. The caller loses the prose, never the sources.
What the graph has and hasn't been shown to do.
Graph fusion contributes to 201 of 231 question runs on our evaluation corpus, so the expanded and evidence lanes are genuinely doing work on the large majority of searches.
What we cannot yet show is that this produces an answer RAG-only retrieval would have missed. On the question sets we can score, it has not. We publish the harness and the reasoning rather than a chart, because a chart here would be marketing rather than evidence.
FAQ
Frequently asked
Mostly the design decisions, including the ones we reversed.
Start with the free tier. Ship the first answer today.
RAG is free, with no card and no sales call. Add the graph when your corpus earns it.
- no card required
- first workspace is free
- one POST to ingest