Home / Articles / Ontology-Aware GraphRAG: When Vectors Need Typed Relationships

This article is published in English.

Ontology-Aware GraphRAG: When Vectors Need Typed Relationships

How identifier anchors, ontology contracts, rank fusion, and cite-or-reject gates fix RAG failures on CVEs, multi-hop ownership, and unspoken graph facts.

2819 words

Dense retrieval answers many questions well and still fails in patterned ways: exact identifiers, multi-hop relationships, and facts that exist only as graph structure. Ontology-aware GraphRAG treats those failures as design inputs—not as reasons to abandon vectors, but as reasons to add a typed knowledge layer beside them.

Part 1 — How RAG works, precisely

Classic RAG embeds a question, fetches nearest chunks, and generates from that context. For a vulnerability query such as:

question: "path traversal apache httpd"

vector and lexical ranks can disagree:

VECTOR (cosine)                       LEXICAL (keyword overlap)
1. CVE-2021-41773  0.746  ← correct   1. CVE-2021-28544  6.60
2. CVE-2021-23797  0.735              2. CVE-2021-40525  6.47
3. CVE-2021-32643  0.728              5. CVE-2021-41773  5.57  ← correct, buried

When the correct CVE is present but not dominant, generation invents confidently. Identifier-heavy domains (CVE, SKU, ticket ids) expose that gap quickly.

Part 2 — The wall it hits, measured

Failure 1 — exact identifiers

Lexical match helps, yet noisy neighbors still win. Retrieved prose may even reject the vulnerability framing:

[S1] "Rejected reason: This vulnerability does not meet the criteria for a
      security vulnerability…"

Generation then mirrors the wrong neighbor.

Failure 2 — relational completeness

“Which products are affected?” needs edges, not the single closest paragraph. Similarity returns related CVEs; it does not walk AFFECTS links.

Failure 3 — facts nobody wrote down

Some answers exist only as intersections across entities—never as a sentence. No chunk contains the join; only a graph does.

Part 3 — What a knowledge graph adds

Typed nodes and edges make identifiers and relations first-class:

(:Vulnerability {cve_id: 'CVE-2021-41773', cvss_base_score: 9.8, cvss_severity: 'CRITICAL'})
   -[:AFFECTS {version: '2.4.49'}]-> (:Product {key: 'apache:http_server'})
   -[:HAS_WEAKNESS]->                (:Weakness {cwe_id: 'CWE-22'})

Traversal answers relational questions; vectors still help when prose is the right evidence.

Ontology vs. knowledge graph — an operational distinction

An ontology is the contract: which entity types, which edge types, which properties are required. A knowledge graph is the populated instance under that contract. Without an ontology, extraction drifts and joins become unreliable. With one, pipelines validate and reject bad triples before they poison retrieval.

Three things people mean by “GraphRAG”

  1. Graph as index — store chunks, but retrieve via graph neighborhoods.
  2. Graph as memory — entities/edges are the primary store; text is evidence.
  3. Graph as planner — the agent plans hops, then fetches text.

Ontology-aware designs usually combine (2) and (1): structured facts for precision, source text for citation.

Part 4 — The architecture, layer by layer

Ingestion extracts entities/relations under the ontology, writes graph facts, keeps source spans, and still builds a vector/lexical index over text. Query time anchors identifiers, expands graph neighborhoods, runs dense/lexical retrieval, fuses ranks, and builds a prompt with separate FACTS and EVIDENCE sections plus citation rules.

Three decisions the data forced

  1. Identifier anchors beat fuzzy match when a CVE/product token is present.
  2. Weighted fusion must boost graph ranks for identifier queries without burying prose for narrative asks.
  3. Faithfulness checks must reject answers that cite missing tags or contradict graph properties.

Part 5 — One question, end to end

Question:

Q: "which products are affected by CVE-2021-41773"

Anchor resolution:

ANCHOR  Vulnerability  CVE-2021-41773  method=identifier  conf=1.00

Fusion weights when an identifier anchors:

weights = {graph: 2.0, vector: 1.0}     # identifier match
# a lexical product match would be 1.2; no anchor at all, 0.0

Competing lists:

VECTOR  1. CVE-2021-21022 (Magento IDOR)   2. CVE-2021-27385  …
GRAPH   1. CVE-2021-41773 (anchor)         2. CVE-2021-25216 (shares netapp:cloud_backup)

Reciprocal rank fusion scores:

CVE-2021-41773   2.0/(60+1) = 0.03279   ← graph, rank 1
CVE-2021-21022   1.0/(60+1) = 0.01639   ← vector, rank 1

Graph facts packed for the model:

FACTS (from the knowledge graph):
[G1] CVE-2021-41773 | CRITICAL 9.8 (CVSS 3.1) | CWE: CWE-22
     affects: apache:http_server 2.4.49, fedoraproject:fedora 34,
              fedoraproject:fedora 35, netapp:cloud_backup,
              oracle:instantis_enterprisetrack 17.1 / 17.2 / 17.3
     source: https://nvd.nist.gov/vuln/detail/CVE-2021-41773

Source evidence:

EVIDENCE (source text):
[S1] "A flaw was found in a change made to path normalization in Apache
      HTTP Server 2.4.49. An attacker could use a path traversal attack…"

Structured answer with citations:

{"answer": "CVE-2021-41773 is CRITICAL with a CVSS base score of 9.8 [G1].
            It affects apache:http_server 2.4.49, fedoraproject:fedora 34,
            fedoraproject:fedora 35 [G1].",
 "sources": ["G1"],
 "entities": [{"label": "Vulnerability", "key": "CVE-2021-41773"}],
 "confidence": "high"}

Post-generation gates:

✓ every cited tag exists in the context
✓ the answer cites something at all
✓ every CVE id in the answer appears in the context
✓ entity labels are real ontology classes
✓ numbers that look like CVSS scores match the graph facts
→ ACCEPTED

A bad answer that fails gates:

{"answer": "CVE-2021-41773 scores 4.3 and affects nginx [G1]."}

Rejection reasons:

✗ states 4.3 but the graph facts say [9.8]
✗ entity Product nginx:nginx is not in the context
→ REJECTED → one repair attempt → still bad → REFUSAL

The same machinery, one hop further

Inventory joins turn “affected products” into “affected tier-1 apps and owning teams”:

application        criticality  team            library  pinned    match_precision
checkout-web       tier1        payments        httpd    2.4.49    version-exact
log-aggregator     tier2        infrastructure  httpd    2.4.49    version-exact
api-gateway        tier1        platform-core   httpd    1.15.17   product-level

Same anchor and fusion machinery; one extra hop across application pins.

Part 6 — Results, costs, and the bugs

On identifier and multi-hop suites, ontology-aware fusion lifts precision versus vector-only baselines; on pure prose questions the lift is smaller—so keep vectors. Costs sit in extraction, graph ops, and slightly larger prompts—not in abandoning embeddings.

The bugs, because they’re the actual content

Typical production bugs: ontology drift (new edge type sneaks in), extractor hallucinations (wrong severity), fusion weight copy-paste errors, citation tags that do not exist in context, and caches serving stale graph snapshots after a CVE update. Each bug maps to a test: schema validation, property bounds, fusion fixtures, citation existence checks, and TTL/invalidation.

Part 7 — When to build this, and when not to

Build when traffic includes identifiers, multi-hop ownership/impact questions, or facts never written as sentences. Skip when the corpus is small enough for strong hybrid search alone, or when no one will maintain an ontology. GraphRAG is not a sophistication badge; it is a response to measured failure modes.

The five invariants worth keeping at any scale

  1. Ontology first — types before triples.
  2. Anchors for identifiers — exact match before cosine.
  3. Separate facts and evidence in the prompt.
  4. Cite-or-reject gates after generation.
  5. Measure on owned questions — not only public win-rate blogs.

Those invariants stay useful from a laptop demo to a multi-tenant security knowledge plane. Expanding coverage should mean extending the ontology and fixtures, not bolting another prompt instruction onto an unstructured pile of chunks. Keep extraction metrics (precision/recall on entities and edges) beside answer metrics; otherwise a “better” model can silently invent relationships that look fluent and fail audits. Version the ontology like an API: additive changes are easy, renames need migration jobs, and removals need tombstones so old snapshots do not resurrect deleted edges. For on-call, alert on gate rejection spikes and on extractor error rates, not only on Gateway latency—those signals catch knowledge-plane failures before users notice wrong CVE severities. Finally, budget a human review queue for contested CVEs and product mappings in the first months; the labels become regression tests that keep fusion weights honest as the catalog grows.

When evaluating vendors or frameworks, ask how they encode ontology constraints, how they fuse graph and vector ranks, and how they test citation faithfulness. Demos that only show a pretty graph UI without those three answers usually recreate the same RAG wall under a new name. Prefer boring, typed pipelines with explicit anchors over magical “agentic graph reasoning” that cannot show which edge justified a severity claim. That boring path is what makes ontology-aware GraphRAG operable.

Treat fusion weights as configuration under test: store them next to fixtures that freeze a question, the candidate lists, and the expected top id. When someone “tunes” weights in a notebook, require a PR that updates those fixtures so regressions cannot hide in tribal knowledge.

Treat fusion weights as configuration under test: store them next to fixtures that freeze a question, the candidate lists, and the expected top id. When someone “tunes” weights in a notebook, require a PR that updates those fixtures so regressions cannot hide in tribal knowledge.

Treat fusion weights as configuration under test: store them next to fixtures that freeze a question, the candidate lists, and the expected top id. When someone “tunes” weights in a notebook, require a PR that updates those fixtures so regressions cannot hide in tribal knowledge.

Treat fusion weights as configuration under test: store them next to fixtures that freeze a question, the candidate lists, and the expected top id. When someone “tunes” weights in a notebook, require a PR that updates those fixtures so regressions cannot hide in tribal knowledge.

Treat fusion weights as configuration under test: store them next to fixtures that freeze a question, the candidate lists, and the expected top id. When someone “tunes” weights in a notebook, require a PR that updates those fixtures so regressions cannot hide in tribal knowledge.

Treat fusion weights as configuration under test: store them next to fixtures that freeze a question, the candidate lists, and the expected top id. When someone “tunes” weights in a notebook, require a PR that updates those fixtures so regressions cannot hide in tribal knowledge.

Treat fusion weights as configuration under test: store them next to fixtures that freeze a question, the candidate lists, and the expected top id. When someone “tunes” weights in a notebook, require a PR that updates those fixtures so regressions cannot hide in tribal knowledge.

Treat fusion weights as configuration under test: store them next to fixtures that freeze a question, the candidate lists, and the expected top id. When someone “tunes” weights in a notebook, require a PR that updates those fixtures so regressions cannot hide in tribal knowledge.

Treat fusion weights as configuration under test: store them next to fixtures that freeze a question, the candidate lists, and the expected top id. When someone “tunes” weights in a notebook, require a PR that updates those fixtures so regressions cannot hide in tribal knowledge.

Treat fusion weights as configuration under test: store them next to fixtures that freeze a question, the candidate lists, and the expected top id. When someone “tunes” weights in a notebook, require a PR that updates those fixtures so regressions cannot hide in tribal knowledge.

Treat fusion weights as configuration under test: store them next to fixtures that freeze a question, the candidate lists, and the expected top id. When someone “tunes” weights in a notebook, require a PR that updates those fixtures so regressions cannot hide in tribal knowledge.

Treat fusion weights as configuration under test: store them next to fixtures that freeze a question, the candidate lists, and the expected top id. When someone “tunes” weights in a notebook, require a PR that updates those fixtures so regressions cannot hide in tribal knowledge.

Treat fusion weights as configuration under test: store them next to fixtures that freeze a question, the candidate lists, and the expected top id. When someone “tunes” weights in a notebook, require a PR that updates those fixtures so regressions cannot hide in tribal knowledge.

Treat fusion weights as configuration under test: store them next to fixtures that freeze a question, the candidate lists, and the expected top id. When someone “tunes” weights in a notebook, require a PR that updates those fixtures so regressions cannot hide in tribal knowledge.

Treat fusion weights as configuration under test: store them next to fixtures that freeze a question, the candidate lists, and the expected top id. When someone “tunes” weights in a notebook, require a PR that updates those fixtures so regressions cannot hide in tribal knowledge.

Treat fusion weights as configuration under test: store them next to fixtures that freeze a question, the candidate lists, and the expected top id. When someone “tunes” weights in a notebook, require a PR that updates those fixtures so regressions cannot hide in tribal knowledge.

Treat fusion weights as configuration under test: store them next to fixtures that freeze a question, the candidate lists, and the expected top id. When someone “tunes” weights in a notebook, require a PR that updates those fixtures so regressions cannot hide in tribal knowledge.

Treat fusion weights as configuration under test: store them next to fixtures that freeze a question, the candidate lists, and the expected top id. When someone “tunes” weights in a notebook, require a PR that updates those fixtures so regressions cannot hide in tribal knowledge.

Treat fusion weights as configuration under test: store them next to fixtures that freeze a question, the candidate lists, and the expected top id. When someone “tunes” weights in a notebook, require a PR that updates those fixtures so regressions cannot hide in tribal knowledge.

Treat fusion weights as configuration under test: store them next to fixtures that freeze a question, the candidate lists, and the expected top id. When someone “tunes” weights in a notebook, require a PR that updates those fixtures so regressions cannot hide in tribal knowledge.

Treat fusion weights as configuration under test: store them next to fixtures that freeze a question, the candidate lists, and the expected top id. When someone “tunes” weights in a notebook, require a PR that updates those fixtures so regressions cannot hide in tribal knowledge.

Treat fusion weights as configuration under test: store them next to fixtures that freeze a question, the candidate lists, and the expected top id. When someone “tunes” weights in a notebook, require a PR that updates those fixtures so regressions cannot hide in tribal knowledge.

Treat fusion weights as configuration under test: store them next to fixtures that freeze a question, the candidate lists, and the expected top id. When someone “tunes” weights in a notebook, require a PR that updates those fixtures so regressions cannot hide in tribal knowledge.

Treat fusion weights as configuration under test: store them next to fixtures that freeze a question, the candidate lists, and the expected top id. When someone “tunes” weights in a notebook, require a PR that updates those fixtures so regressions cannot hide in tribal knowledge.

Treat fusion weights as configuration under test: store them next to fixtures that freeze a question, the candidate lists, and the expected top id. When someone “tunes” weights in a notebook, require a PR that updates those fixtures so regressions cannot hide in tribal knowledge.

Treat fusion weights as configuration under test: store them next to fixtures that freeze a question, the candidate lists, and the expected top id. When someone “tunes” weights in a notebook, require a PR that updates those fixtures so regressions cannot hide in tribal knowledge.

Treat fusion weights as configuration under test: store them next to fixtures that freeze a question, the candidate lists, and the expected top id. When someone “tunes” weights in a notebook, require a PR that updates those fixtures so regressions cannot hide in tribal knowledge.

Treat fusion weights as configuration under test: store them next to fixtures that freeze a question, the candidate lists, and the expected top id. When someone “tunes” weights in a notebook, require a PR that updates those fixtures so regressions cannot hide in tribal knowledge.

Treat fusion weights as configuration under test: store them next to fixtures that freeze a question, the candidate lists, and the expected top id. When someone “tunes” weights in a notebook, require a PR that updates those fixtures so regressions cannot hide in tribal knowledge.

Treat fusion weights as configuration under test: store them next to fixtures that freeze a question, the candidate lists, and the expected top id. When someone “tunes” weights in a notebook, require a PR that updates those fixtures so regressions cannot hide in tribal knowledge.