Skip to content

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.

  1. Direct

    weight 1.0

    Hybrid 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.

    always runs
  2. 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.

  3. 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.

  1. 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)

  2. Trigram similarity

    Catches the typo, the plural, the stray punctuation — the cases where two strings clearly mean one thing.

    pg_trgm

  3. 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

  4. 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.

POST /v1/search/test — per-step diagnostics
{
  "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.

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.

Read the full methodology

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