Skip to content

How harvested RDF is modelled

A curator registers a GitHub repository or a Zenodo record in the spreadsheet, and the harvester_rdf_files DAG turns each RDF file it finds into its own named graph. The file's own triples go in untouched — nothing is remodelled — but two blocks of metadata are added around them, and those blocks are the subject of this page.

Every example below is real output, copied from a harvest of creep-testing-ontology and a Zenodo record, not written by hand for the documentation.


1. The graph describes itself

Each named graph carries the same self-description every pipeline graph carries, so a harvested graph is indistinguishable from a curated one to the dashboard, to /graphs, and to SPARQL.

cto:cto-full.ttl
    a void:Dataset , nfdi:NFDI_0000009 ;
    rdfs:label "HosseinBeygiNasrabadi/creep-testing-ontology — cto-full.ttl" ;
    void:triples          13057 ;
    void:distinctSubjects  3694 ;
    void:classes             21 ;
    void:properties          67 ;
    void:dataDump   "https://raw.githubusercontent.com/…/1.0.0/cto-full.ttl"^^xsd:anyURI ;
    obo:RO_0002353  cto:cto-full.ttl#load ;      # is output of  → the load process
    obo:IAO_0000235 cto:cto-full.ttl#desc .      # denoted by    → the description

Four kinds of thing appear, and telling them apart is the whole model:

Node Kind Why it matters
…/cto-full.ttl the graph — void:Dataset a continuant: it persists, it does not happen
#desc textual description also a continuant — information, not an event
#load the load process the only occurrent, so the only thing that may occupy time
#t0 temporal instant zero-dimensional; when the load happened
#source the input file carries the download URL and the sha256: of the bytes

Drag the nodes, switch the layout, or use Group to collapse each namespace into one hexagon. Reasoning shows what a reasoner adds on top of what is asserted.

The two rules that are easy to break

The description is NFDI_0001018 textual description. It is emphatically not NFDI_0000018 academic event — one digit away. A description sits on the right-hand side of denoted by, which requires information rather than an event, so using the wrong IRI makes every graph carrying it logically inconsistent. This mistake was live in the pipeline and is the reason the Graph Metadata shape exists.

occupies temporal region goes on the load process only — never on the graph, never on the description. It has domain process or process boundary and is functional. If you want to say when a continuant was valid, that is a different pattern; see Role Bearer.

Statistics are part of the graph

The counts are written as VoID, including per-class and per-predicate partitions, so "how big is this graph and what is in it" is answerable by SPARQL rather than only by a dashboard:

SELECT ?class ?n WHERE {
  GRAPH ?g {
    ?g void:classPartition [ void:class ?class ; void:entities ?n ]
  }
} ORDER BY DESC(?n)

One registration can produce several named graphs — an ontology repository might ship four RDF files. If those graphs only existed on their own, the connection back to the repository would be lost. So a second block, written into the TriG's default graph and published to …/matwerk/rdf_files_metadata, links every file-graph to its source with BFO part-of / has-part:

mw:github/HosseinBeygiNasrabadi/creep-testing-ontology
    rdfs:label "HosseinBeygiNasrabadi/creep-testing-ontology" ;
    obo:BFO_0000051 cto:cto-full.ttl .           # has part → the file graph

cto:cto-full.ttl
    a nfdi:NFDI_0000027 ;                        # file data item
    obo:BFO_0000050 mw:github/…/creep-testing-ontology ;   # part of → the repository
    nfdi:NFDI_0001008 "https://raw.githubusercontent.com/…"^^xsd:anyURI ;
    stat:registeredFrom "https://github.com/HosseinBeygiNasrabadi/creep-testing-ontology" ;
    stat:sourceFile "cto-full.ttl" ;
    stat:triples 13057 ;
    stat:version "1.0.0" .

When the repository is already an entity in the KG, the harvester attaches the graphs to that entity instead of minting a new node — so harvested data hangs off what the KG already knows, rather than beside it.

Why the flat stat: predicates exist

stat:registeredFrom repeats, as a plain literal, the URL exactly as the curator typed it in the spreadsheet. That makes the round trip a one-hop query, which is what lets a spreadsheet look up its own rows:

PREFIX stat: <https://nfdi.fiz-karlsruhe.de/matwerk/msekg/stat/>
SELECT ?graph ?file ?triples ?version WHERE {
  GRAPH <https://nfdi.fiz-karlsruhe.de/matwerk/rdf_files_metadata> {
    ?graph stat:registeredFrom "https://github.com/OWNER/REPO" ;
           stat:sourceFile ?file ; stat:triples ?triples .
    OPTIONAL { ?graph stat:version ?version }
  }
}

3. Graph IRIs are deterministic

https://nfdi.fiz-karlsruhe.de/matwerk/github/<owner>/<repo>/<file>
https://nfdi.fiz-karlsruhe.de/matwerk/zenodo/<record id>/<file>

Because the IRI is a pure function of (source, key, filename), a re-run updates the same graph in place instead of minting a second copy. That is what makes the weekly schedule safe: the latest release wins, and an unchanged file — detected by its sha256 — is skipped entirely.


4. Vocabulary

Only BFO, IAO, nfdicore, VoID and W3C-Time are used. There is deliberately no PROV and no Dublin Core in anything this pipeline writes, matching the rest of the graphs.

Third-party dcterms triples do appear inside harvested graphs — the CTO file uses them internally. That is correct and intentional: harvested content is inserted verbatim, and rewriting someone else's vocabulary would misrepresent their data.

Term IRI Used for
textual description nfdi:NFDI_0001018 the description node
file data item nfdi:NFDI_0000027 a harvested RDF file
research data repository nfdi:NFDI_0001201 a minted GitHub repository node
denoted by obo:IAO_0000235 entity → description / URL / version
has part / part of obo:BFO_0000051 / obo:BFO_0000050 repository ↔ file graph
is output of obo:RO_0002353 graph → the load process
has input obo:RO_0002233 load process → the source file
process obo:BFO_0000015 the load
occupies temporal region obo:BFO_0000199 the load only
has value / has url nfdi:NFDI_0001007 / nfdi:NFDI_0001008 literals

The dataset class is version-dependent: MWO 3.0.2 deprecates nfdi:NFDI_0000009 in favour of obo:IAO_0001000. The DAGs read the class from the matwerk_mwo_version Airflow Variable rather than hard-coding it, so the vocabulary moves with the ontology.