> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cognee.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Ontologies

> Enrich your knowledge graph with external vocabularies.

## What is an ontology in Cognee?

An **ontology** is an optional RDF/OWL file you can provide to Cognee.
It acts as a **reference vocabulary**, making sure that entity types ("classes") and entity mentions ("individuals") extracted from your data are linked to canonical, well-defined concepts.

## How it works

* You supply an ontology when running [Cognify](../main-operations/legacy-operations/cognify) in one of two ways (see the [practical example](#additional-details-and-examples) below):
  * Set the `ONTOLOGY_FILE_PATH` environment variable and call `cognee.cognify()` normally.
  * Pass an ontology resolver through the `config` argument.
* Cognee parses the file with [RDFLib](https://rdflib.dev/) and loads its classes and relationships.
* Once the LLM has extracted a graph from a chunk, its entities and types are checked against the ontology before any graph nodes are built from it:
  * If a match is found, the node is marked `ontology_valid=True`.
  * Parent classes and object-property links from the ontology are attached as extra edges.
* If no ontology is provided, extraction still works, just without validation or enrichment.

## Why use an ontology

* **Consistency**: standardize how entities and types are represented
* **Enrichment**: bring in inherited relationships from a domain schema
* **Control**: align Cognee's graph with existing enterprise or scientific vocabularies

## Where to get ontologies

Cognee works best with **manually curated, focused ontologies** that fit your dataset. Ontology design itself is outside the scope of Cognee, so if you need to create or model an ontology from scratch, use dedicated ontology tools and references first, then bring the resulting RDF/OWL file into Cognee.

Public resources like **Wikidata** or **DBpedia** define millions of classes and entities, which makes them too big to use directly in Cognee. If you start from a public ontology, always work with a subset, not the full ontology:

* **Select only the pieces you need** (specific classes, properties, or individuals)
* **Save the subset** in a format Cognee can parse with [`rdflib`](https://rdflib.readthedocs.io/)
* **If needed, enrich the subset manually** by adding extra classes or relationships relevant to your domain
* **Keep it small and relevant** so matching stays precise and performance remains fast

<AccordionGroup>
  <Accordion title="Common sources">
    - **General vocabularies**: schema.org, Dublin Core Terms (DC/Terms), SKOS, PROV-O, FOAF
    - **Knowledge graph backbones**: DBpedia Ontology, Wikidata (Wikibase RDF ontology)
    - **Domain examples**:
      * Healthcare: SNOMED CT (licensed), ICD, UMLS, MeSH, HL7/FHIR RDF
      * Finance: FIBO (Financial Industry Business Ontology)
      * Geo/IoT: GeoSPARQL, SOSA/SSN, GeoNames
      * Units: QUDT
  </Accordion>

  <Accordion title="Why subsetting is essential">
    Every public ontology is **too broad to ingest wholesale**. Creating a subset is what makes them usable in Cognee:

    * Improves matching precision (fewer false matches when mapping LLM output)
    * Keeps performance acceptable (smaller graphs → faster resolution)
    * Lets you curate only the relevant parts of a domain
  </Accordion>

  <Accordion title="How subsetting works">
    Different communities provide different ways to extract subsets (e.g., "slims" in OBO ontologies, WDumper for Wikidata, module extraction in Protégé). The details vary, but the general principle is the same:

    1. Pick the terms (classes or properties) you care about
    2. Extract those terms plus their immediate context (e.g. parent classes, related properties)
    3. Save the result in an `rdflib`-readable RDF format
  </Accordion>
</AccordionGroup>

## Supported formats

Any format [RDFLib](https://rdflib.readthedocs.io/) can parse:

* RDF/XML (`.owl`, `.rdf`)
* Turtle (`.ttl`)
* N-Triples, JSON-LD, and others

## RDF read/write surface

Beyond consuming an ontology as extraction scaffolding, Cognee can preserve external IRIs end-to-end and treat the memory graph as RDF. This is aimed at teams that maintain knowledge natively as RDF and want Cognee as a complementary agentic-memory layer over their RDF knowledge base.

<Note>
  The RDF surface relies on [`rdflib`](https://rdflib.dev/), which ships with Cognee's ontology support. Everything here is **backward compatible**: `ontology_uri` defaults to `None` and existing text-extraction ingestion is unchanged — nothing is required for existing workflows.
</Note>

### URI preservation

When an extracted entity or type matches your ontology, Cognee now keeps the matched IRI on the persisted node in [`DataPoint.ontology_uri`](/core-concepts/building-blocks/datapoints#core-structure) instead of flattening it to a local label. Grounded `Entity`/`EntityType` nodes carry their stable external IRI; ungrounded nodes keep `ontology_uri = None`. The field never affects node identity.

### Exporting the memory graph to RDF

The `cognee.modules.graph.rdf` module builds an RDF view over the live memory graph so you can serialize it or query it with SPARQL, decoupled from the underlying graph engine's query language.

```python theme={null}
from cognee.modules.graph.rdf import (
    serialize_memory_graph,
    query_memory_graph_sparql,
    export_memory_graph_to_rdf,   # returns an rdflib.Graph
    graph_data_to_rdf,            # pure builder over (nodes, edges) tuples
)

# Serialize the whole memory graph to RDF (Turtle by default).
turtle = await serialize_memory_graph(rdf_format="turtle")

# Or run SPARQL directly over an RDF view of the graph.
rows = await query_memory_graph_sparql(
    "SELECT ?s WHERE { ?s a <http://example.org/mm#CNCMachine> }"
)
```

How nodes and edges map to RDF:

* **Grounded nodes** are emitted under their preserved `ontology_uri`. **Ungrounded nodes** get a minted IRI under `DEFAULT_BASE_IRI` (`https://cognee.ai/graph/…`) so the RDF stays well-formed and nothing is dropped.
* A node's `name` becomes an `rdfs:label`.
* The `is_a` relationship resolves to `rdf:type` for an individual→class link and to `rdfs:subClassOf` for a class→class link. Other relationships become predicate IRIs — a minted `…/prop/<name>` IRI, or, for RDF-ingested edges that carry a `predicate_uri`, that original RDF predicate IRI.

<Note>
  `export_memory_graph_to_rdf` / `serialize_memory_graph` / `query_memory_graph_sparql` materialize the whole graph into an in-memory `rdflib` store on each call. This is convenient for querying and export but costs memory proportional to graph size — keep that in mind for very large graphs.
</Note>

### Ingesting RDF into datapoints

The `cognee.modules.ontology.rdf_xml.rdf_ingest` module ingests an RDF T-Box + A-Box directly into Cognee datapoints, keeping the external IRIs verbatim rather than canonicalizing entities into a local vocabulary.

```python theme={null}
from cognee.modules.ontology.rdf_xml.rdf_ingest import ingest_rdf

# Accepts a file path, list of paths, file-like object, or a parsed rdflib.Graph.
data_points = await ingest_rdf("knowledge_base.ttl")
```

* **OWL classes** become `EntityType` nodes; **individuals** typed by a known class become `Entity` nodes. `rdf:type` and `rdfs:subClassOf` are kept as `is_a` relationships.
* **Node identity is derived from the IRI** (not the label), so distinct IRIs stay distinct and **re-ingesting the same RDF is idempotent** — this is an open-world model with no fuzzy canonicalization.
* **Object-property assertions** between two ingested individuals become explicit graph edges that preserve the original RDF predicate IRI as `predicate_uri`, so they round-trip back out on export.
* Parsing reuses the ontology resolver, so any RDF syntax RDFLib understands (RDF/XML, Turtle, N-Triples, JSON-LD, …) is supported.

Lower-level helpers are available when you need them: `load_rdf_graph(source)` parses a source into an `rdflib.Graph`, and `build_datapoints_from_rdf(graph)` / `build_graph_from_rdf(graph)` turn a parsed graph into datapoints (and custom edges) without persisting.

<Note>
  **Scope / limits.** RDF ingestion preserves object-property assertions only when both endpoints are ingested individuals. Blank nodes, arbitrary literal/data-property round-trip, and RDF reasoning/entailment (e.g. OWL RL) are out of scope — no inference is applied to the ingested graph.
</Note>

## Additional details and examples

<AccordionGroup>
  <Accordion title="Practical example">
    `cognee.cognify()` has **no `ontology_file_path` parameter** — passing one raises `Unrecognized request argument supplied: ontology_file_path`. Supply the ontology through the `config` argument or the `ONTOLOGY_FILE_PATH` environment variable instead.

    <Tabs>
      <Tab title="Python API">
        ```python theme={null}
        import cognee
        from cognee.modules.ontology.rdf_xml.RDFLibOntologyResolver import RDFLibOntologyResolver
        from cognee.modules.ontology.ontology_config import Config

        config: Config = {
            "ontology_config": {
                "ontology_resolver": RDFLibOntologyResolver(ontology_file="subset.owl")  # your curated subset here
            }
        }

        await cognee.cognify(datasets=["my_dataset"], config=config)
        ```
      </Tab>

      <Tab title="Environment variable">
        ```bash theme={null}
        ONTOLOGY_FILE_PATH=/path/to/subset.owl
        ```

        ```python theme={null}
        import cognee

        # With ONTOLOGY_FILE_PATH set, cognify picks up the ontology automatically.
        await cognee.cognify(datasets=["my_dataset"])
        ```
      </Tab>
    </Tabs>
  </Accordion>

  <Accordion title="Default behavior without an ontology">
    Cognee does **not** ship with or apply a built-in default ontology. When you don't set `ONTOLOGY_FILE_PATH` (or pass an ontology resolver via `config`), no ontology resolver is constructed at all: the grounding step is skipped entirely and extracted graphs go straight through the ontology-free construction path. There is no reference vocabulary to validate or enrich against, and no per-entity ontology lookups are performed.

    In that mode, node labels and relationship names are produced **directly by the LLM** during graph extraction. There is no fixed, predefined list of relationship types — the model infers them from your text. Consistency is *guided* by the extraction prompt rather than *enforced*, so the same concept may occasionally surface under slightly different labels across chunks. The prompt asks the model to:

    * Use **basic node types** (e.g. label a person as `Person`, not `Mathematician` or `Scientist` — those become properties).
    * Use **snake\_case relationship names** (e.g. `acted_in`).
    * Apply **coreference resolution** so an entity referred to by different names or pronouns maps to a single, consistent node.

    Typical relationship names the model produces are plain, real-world verbs and roles derived from the text, for example:

    * People: `married_to`, `parent_of`, `friend_of`, `works_at`, `colleague_of`
    * Ownership and roles: `owns`, `owned_by`, `member_of`, `founder_of`, `employed_by`
    * Things and places: `produces`, `located_in`, `part_of`, `created_by`, `acted_in`

    Provide an ontology when you need these labels to be **standardized and validated** against a fixed vocabulary instead of inferred per document.
  </Accordion>

  <Accordion title="Using multiple ontology files">
    Cognee can load several OWL files and merge them into one in-memory graph, which is useful when you split a large ontology into focused modules.

    **Environment variable — comma-separated paths:**

    ```bash theme={null}
    ONTOLOGY_FILE_PATH=/path/to/domain.owl,/path/to/entities.owl
    ```

    **Python API — list of paths:**

    ```python theme={null}
    from cognee.modules.ontology.rdf_xml.RDFLibOntologyResolver import RDFLibOntologyResolver
    from cognee.modules.ontology.ontology_config import Config

    resolver = RDFLibOntologyResolver(
        ontology_file=["/path/to/domain.owl", "/path/to/entities.owl"]
    )

    config: Config = {"ontology_config": {"ontology_resolver": resolver}}
    await cognee.cognify(config=config)
    ```

    Files that cannot be found or parsed are skipped with a warning; at least one valid file is required for ontology grounding to take effect.

    **How the merge works.**

    `RDFLibOntologyResolver` parses each file with RDFLib and adds all triples to the same in-memory `rdflib.Graph`. The result is a single unified graph — Cognee does not create disjoint subgraphs, even when the ontologies share no classes or properties. There is no explicit conflict-resolution step: RDFLib performs an additive merge of triples, and any classes or properties that happen to share IRIs across files naturally coexist in the same graph.

    **One ontology per `cognify()` run.**

    The ontology resolver is configured at the `cognify()` call level (via `ONTOLOGY_FILE_PATH` or the `ontology_config` in the `Config` payload), not per dataset or per document. If you need different vocabularies for different data, run `cognify()` separately for each dataset with its own resolver instance.
  </Accordion>

  <Accordion title="Creating or editing ontologies">
    Cognee does not provide ontology-authoring features. If you need to create, edit, or validate an ontology, use dedicated RDF/OWL tooling such as Protégé or your team's existing ontology workflow, then load the resulting file into Cognee.

    When preparing a file for Cognee:

    * Keep the ontology focused on the classes, properties, and individuals relevant to your dataset
    * Prefer a curated subset over a large general-purpose ontology
    * Save it in a format RDFLib can parse, such as RDF/XML (`.owl`, `.rdf`) or Turtle (`.ttl`)
  </Accordion>

  <Accordion title="How does an ontology relate to a custom graph?">
    An ontology **extends** the graph Cognee builds — it never replaces it. The LLM extracts a graph from each chunk first, and grounding then runs as a **canonicalize-first pre-pass**: it rewrites the extracted graph *before* any graph nodes are constructed from it, rather than validating nodes one by one as they are built. For every extracted entity and entity type, Cognee looks the name up in the ontology. Then:

    * **On a match**, the node is *canonicalized* in place: its `name` and `type` are rewritten to the ontology term, so the id derived from that name makes different surface forms collapse into one node. When several nodes in the same extracted graph resolve to the same ontology individual, only one of them survives and the edges of the collapsed nodes are rewired onto the survivor. The surviving node gets `ontology_valid = True` and the matched IRI in `ontology_uri`.
    * Cognee then walks the matched term's neighbourhood in the OWL file and **adds** those classes and individuals as extra nodes, along with their `is_a` (`rdf:type` / `rdfs:subClassOf`) and object-property edges. An ontology edge is attached only when **both** of its endpoints are part of the matched subgraph; an edge pointing at a term outside it is skipped rather than inventing a node for that endpoint. This is how parent classes and related individuals show up in your graph even when the text never mentioned them.
    * **On no match**, the node is kept exactly as the LLM produced it, with `ontology_valid = False`. Nothing is rejected or discarded.

    So an OWL file layers a curated skeleton on top of the extracted graph. It cannot remove nodes, and attaching one later does not retro-fit data that has already been processed — re-run [Cognify](../main-operations/legacy-operations/cognify) for that.

    The other way to shape the graph is a [custom graph model](/guides/custom-graph-model). Neither of them is where entities come from: the entities are always read out of **your data** by the LLM. A `graph_model` constrains which shapes that extraction may return, and an ontology renames and extends what it did return — so an ontology is *not* "the graph model", and a graph model is *not* an ontology. Both shape the graph, but they act at different stages and are not alternatives to one another:

    |                       | [Custom graph model](/guides/custom-graph-model)                             | Ontology                                                                                |
    | --------------------- | ---------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
    | **What it is**        | A Pydantic `DataPoint` schema passed as `graph_model`                        | An RDF/OWL file passed via `ontology_config` or `ONTOLOGY_FILE_PATH`                    |
    | **When it acts**      | *Before* extraction — it **is** the LLM's structured-output schema           | *After* extraction — extracted names are matched against the vocabulary                 |
    | **What it controls**  | Which node types, fields, and relationships the LLM may produce at all       | Which of the produced entities and types get canonical names, IRIs, and inherited edges |
    | **How strict**        | Hard: the LLM cannot return anything outside the schema                      | Soft: unmatched entities are kept as-is, never dropped                                  |
    | **Reach for it when** | You know the exact *shape* you want (invoices, tickets, people → activities) | You already own a domain vocabulary and want *naming and typing* aligned to it          |

    **Combining the two.**

    Grounding runs only inside the default `KnowledgeGraph` extraction path. If you pass a `graph_model` that is not a `KnowledgeGraph` subclass, Cognee stores your model's output directly and skips grounding, so **the two do not stack within a single `cognify()` / `remember()` call**. A [`custom_prompt`](/guides/custom-prompts) is independent of this: it replaces the extraction system prompt and works with either mode. Practical ways to get both worlds:

    * **Ontology + [custom prompt](/guides/custom-prompts).** Keep the default schema and ontology grounding, and use `custom_prompt` to steer the LLM toward your ontology's class names so more entities match. This is the closest thing to "both" in one run.
    * **Two passes over the same data.** Run one `cognify()` with your custom `graph_model` for the strictly-shaped part of the graph, and another with the default schema plus an ontology for the grounded part. Both write into the same graph.
    * **Ingest the vocabulary directly.** If your ontology already contains the individuals you care about, [`ingest_rdf`](#ingesting-rdf-into-datapoints) loads its classes and individuals as datapoints with IRIs preserved, alongside anything extracted from text.
  </Accordion>

  <Accordion title="Which OWL constructs grounding actually reads">
    Grounding uses a deliberately small slice of OWL. RDFLib parses the whole file, but `RDFLibOntologyResolver` only ever queries these triples:

    | Construct                     | How it is used                                                                                                                                                                          |
    | ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
    | `rdf:type owl:Class`          | Builds the **classes** lookup — matched against an extracted node's `type`, materialized as an `EntityType`                                                                             |
    | `rdf:type <SomeClass>`        | Builds the **individuals** lookup (only for classes already in the lookup) — matched against an extracted node's `name`, materialized as an `Entity`; also re-emitted as an `is_a` edge |
    | `rdfs:subClassOf`             | Followed upward from the matched term and emitted as `is_a` edges, so parent classes come along                                                                                         |
    | `rdf:type owl:ObjectProperty` | Any assertion whose predicate is a declared object property becomes an edge named after the predicate's local name — but only between two terms inside the matched subgraph             |

    Everything else is ignored:

    * **`owl:DatatypeProperty` is never queried.** Literal-valued attributes in the OWL file are not copied onto graph nodes; an attached ontology node is created with only its `name`, a `description` mirroring it, `ontology_valid`, and `ontology_uri` — no attribute from the OWL file is copied over. Node properties come from extraction or from your [custom graph model](/guides/custom-graph-model)'s fields, never from the ontology.
    * **No axioms are enforced.** `rdfs:domain` / `rdfs:range`, `owl:equivalentClass`, `owl:sameAs`, `owl:Restriction`, and cardinality all land in the in-memory graph but are never consulted — no reasoner runs.
    * **`rdfs:label` is not used for matching.** Grounding keys on the **IRI local name** (the fragment after `#`, or the last path segment), lowercased with spaces replaced by underscores. A term with a readable label but an opaque IRI (e.g. `…#C0004096`) will never match text, so give your terms readable IRIs. ([`ingest_rdf`](#ingesting-rdf-into-datapoints) does read `rdfs:label`; grounding does not.)

    A practical consequence of the lookup rules: a class must be declared `rdf:type owl:Class` to be findable at all. A class that only ever appears as the object of an `rdfs:subClassOf`, or an individual declared only as `owl:NamedIndividual` without a type from a declared class, never enters the lookup. The resolver still traverses such terms when walking a matched term's neighbourhood, but they are dropped when the subgraph is materialized: only declared classes and their typed individuals become nodes, and any edge touching an undeclared term is discarded with it. So declare every term you want in the graph as an `owl:Class`, or type it with one.
  </Accordion>

  <Accordion title="What ontology_valid actually means">
    `ontology_valid` is a boolean marker on every [`DataPoint`](/core-concepts/building-blocks/datapoints#core-structure), defaulting to `False`. It records **whether grounding found a match** — nothing more:

    * **It is not a gate.** Nodes with `ontology_valid = False` are stored, embedded, and returned by search exactly like grounded ones. There is no "reject entities that aren't in the ontology" mode, and no node is auto-created in your `.owl` file either — Cognee never writes back to the ontology.
    * **It is not schema validation.** Matching is purely name-based: names are lowercased with spaces replaced by underscores, then compared using `difflib` fuzzy matching at a `0.8` similarity cutoff (`FuzzyMatchingStrategy` — the only strategy Cognee ships, `MATCHING_STRATEGY=fuzzy`). Only `owl:Class` terms (as *classes*) and individuals typed by one of those classes (as *individuals*) are candidates. OWL `rdfs:domain` / `rdfs:range` axioms are **not** enforced, and no reasoning or entailment is applied.
    * **It is a node-level flag.** Only `Entity` and `EntityType` nodes carry `ontology_valid`. The edges Cognify writes do not have the property at all, whether they were copied in from the ontology subgraph or extracted by the LLM: relationship names are never matched against the ontology's object properties, so there is no edge-level grounding verdict to record. Filter on the endpoints, not the edge.
    * **What it is good for.** It is provenance you can filter on: [`visualize_graph`](/guides/graph-visualization) colors grounded nodes differently, and you can select on the property in your own graph queries.
  </Accordion>
</AccordionGroup>

For more detailed examples of working with ontologies in Cognee, check out the demo scripts in the repository:

* [Basic ontology demo](https://github.com/topoteretes/cognee/tree/main/examples/guides) - Shows fundamental ontology integration
* [Advanced ontology demo](https://github.com/topoteretes/cognee/tree/dev/examples/advanced_guides/ontology_reference_vocabulary) - Demonstrates more complex ontology workflows
