Retrieval.
Three strategies with different failure modes, run together and fused — because no single one of them is reliable across the range of questions people actually ask.
Why not just one
Lexical search is exact and useless for paraphrase. Vector search handles paraphrase and cheerfully returns something plausible when the corpus has nothing. Graph traversal finds relationships and says nothing about text. Each is a good answer to a different question, so the planner picks a mix per query rather than committing to one.
The strategies
| Strategy | Good at | Fails at |
|---|---|---|
| Lexical | Exact identifiers — a CVE id, a hash, a domain | Paraphrase, synonyms, questions |
| Semantic | Natural-language questions, conceptual similarity | Precision on identifiers; returns near-misses confidently |
| Graph | Pivots and relationships between entities | Anything textual |
Lexical runs over Postgres full-text. Semantic runs over kb_chunks embeddings in pgvector. Graph walks graph_edges.
Fusion
Results from each retriever are merged and re-ranked into one context. What survives is what the answer is generated from, and every surviving hit keeps its document id — which is how the citation later resolves.
- Duplicates across strategies collapse to one hit, keeping the best score.
- Quarantined documents are excluded before ranking, not demoted.
- Tenant scope is applied by row-level security, so it cannot be forgotten in the query.
The trace
Every response reports which retrievers ran, how many hits each returned and how long each took. This is the difference between a system you can debug and one you have to trust.
A semantic-only trace with few hits on a question containing an exact identifier usually means the identifier was not matched lexically — worth checking the corpus actually contains it. A graph trace with zero hits means no relationships were materialised for that entity yet, not that none exist upstream.
Live fetch
Some questions are about something not in the corpus. Where a live lookup is available, retrieval can fall back to fetching — but anything it uses is retained first, so the citation still points at a stored document rather than at a URL that may have changed since.