Skip to learning content
← All articles
RAG free

Build a working ontology from defence logistics data

Build an ontology from an empty Python file, inspect actual graph records and their sources, and extend a maintenance example to multiple required parts. Six chapters combine plain-language explanations with a downloadable, locally tested implementation.

The question: How do we actually create, validate and use an ontology from operational source records?

You’ll learn: Construct an ontology step by step, trace RDF facts to CSV records, validate and query them, and migrate the model to support several required parts per order.

For: Domain experts, analysts and engineers; concepts are explained before the runnable commands.

Executed teaching prototype · AI-assisted source check . This is not a human expert endorsement.

Chapter 6: Extend the ontology: one work order, several required parts

6. Extend the ontology: one work order, several required parts#

The maintenance team changes its question: “WO-204 needs two filters and one replacement seal. Which requirement is blocked, and by which evidence?” The original model cannot express those two part-and-quantity pairs safely. This chapter implements the change, migrates the existing records, and tests the resulting graph.

You will create a PartRequirement class, preserve the original order identity, add three source files, and run a query that keeps each part's evidence and shortage separate. The complete downloadable lab includes the finished extension under extensions/multipart/; its implementation is multipart_lab.py. All steps below run locally with the Python environment prepared in Chapter 2.

Step 1 — identify the modelling failure before adding fields#

The first model has one part and one requiredQuantity directly on a WorkOrder. Adding a second value to both properties would say “this order has PF-9 and SEAL-2, and quantities 2 and 1.” It would not say which quantity belongs to which part. A query could produce the wrong pairings. A comma-separated string such as PF-9:2,SEAL-2:1 would hide that structure inside text.

Give each requirement its own identity. WO-204-R1 owns the filter/count pair; WO-204-R2 owns the seal/count pair. Both belong to the same order. This is why a line item becomes a class: the relationship itself has facts that need to stay together. Later, a requirement could carry its own due date or approved substitute without overloading the WorkOrder.

text
Before
WO-204 → part → PF-9
WO-204 → requiredQuantity → 2

After
WO-204 → requirement → WO-204-R1 → part → PF-9
                       └→ requiredQuantity → 2
WO-204 → requirement → WO-204-R2 → part → SEAL-2
                       └→ requiredQuantity → 1

A relational database can model the same structure with an order-requirement table. The useful ontology lesson is explicit meaning, stable identity, and relationships that several systems can share. Subtracting stock does not, by itself, justify adopting a graph database.

Step 2 — create the new terms and replace the old constraints#

Open extensions/multipart/multipart_lab.py and find model(). It starts from the six-class baseline schema, changes its version to 2.0.0-multipart, and adds these terms. This Turtle fragment describes the same additions; the script generates the complete results/schema.ttl file.

turtle
@prefix m: <https://example.org/maintenance/> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .

m:PartRequirement a owl:Class .
m:requirement a owl:ObjectProperty ; rdfs:range m:PartRequirement .
m:order a owl:ObjectProperty ; rdfs:range m:WorkOrder .

The WorkOrder now needs one or more requirement links. Every PartRequirement needs exactly one order, one part, one positive integer requiredQuantity, and one unit equal to each, plus its organization and record ID. The ordinary part, requiredQuantity, and unit properties remain valid on requirement records.

The migration also forbids those three old properties directly on a WorkOrder. Without that check, old and new consumers could read two conflicting versions of the requirement. These are the actual added work-order constraints from model(); the existing asset, organization and ID constraints are retained. minCount and maxCount constrain the number of values in the data being validated.601

turtle
@prefix m: <https://example.org/maintenance/> .
@prefix sh: <http://www.w3.org/ns/shacl#> .

m:WorkOrderShape
    sh:property [ sh:path m:requirement ; sh:minCount 1 ;
                  sh:class m:PartRequirement ; sh:nodeKind sh:IRI ] ;
    sh:property [ sh:path m:part ; sh:maxCount 0 ] ;
    sh:property [ sh:path m:requiredQuantity ; sh:maxCount 0 ] ;
    sh:property [ sh:path m:unit ; sh:maxCount 0 ] .

Step 3 — migrate the existing facts before adding new ones#

The extension loads the already verified baseline graph into memory. It does not modify the baseline's CSVs or generated files. For each existing order, build() first creates a requirement whose stable ID is the order ID followed by -R1. It copies the original part, quantity and unit into that record, links the record to its order, and copies the original source-row lineage. Only then does it remove the three old properties from the in-memory WorkOrder. For order IDs longer than 77 characters, migrated_line_id() uses a deterministic shortened ID with a hash suffix to stay within the 80-character identifier limit; the original order identity is unchanged.

The line() helper performs the guarded creation. It rejects unknown or cross-organization references, duplicate requirement IDs, unsupported units, and a repeated part on the same order. The last rule prevents two lines from independently counting the same available stock. Supporting split lines would require an explicit allocation policy, not simply removing that check.

For the supplied dataset, results/migration.json records two migrations: ORG-TRAIN's WO-204 becomes WO-204-R1, and ORG-OTHER's WO-888 becomes WO-888-R1. Their original order, asset and part identifiers are preserved. The additional requirement comes next; it is not invented while migrating old records.

Provenance stays attached to the source records that support each migrated line, and an additional activity identifies the migration. This uses PROV's derivation and activity relationships to describe lineage.602 It does not turn the migration into an authenticated audit service. Compare the baseline file hashes before and after if you want to verify that its files were preserved; the test suite makes that comparison automatically.

Step 4 — supply the second part, its stock and its requirement#

The extension's three small CSV files are already included. Their paths are relative to the lab directory. Keep the exact headers when creating a fresh copy. First, extensions/multipart/fixtures/parts.csv defines the new part type:

csv
part_id,org,name,unit
SEAL-2,ORG-TRAIN,Training replacement seal,each

Next, extensions/multipart/fixtures/inventory.csv declares three fictional physical seal identities. The observation uses the same time as the baseline's stock records so the existing freshness policy applies consistently.

csv
snapshot_id,org,depot,part_ref,on_hand,unit,unit_ids,observed_at,revision
SNAP-SEALS,ORG-TRAIN,North,SEAL-2,3,each,S1|S2|S3,2026-09-14T07:30:00Z,1

Finally, extensions/multipart/fixtures/requirements.csv says that the work order needs one of that part type:

csv
requirement_id,org,order_id,part_id,required_quantity,unit
WO-204-R2,ORG-TRAIN,WO-204,SEAL-2,1,each

Three seals are an explicit exercise input, not a measured operational count. This closed fixture declares zero reservations and zero quarantined seals; existing filter allocations remain unchanged. A real importer would need a completeness contract for its allocation feed before interpreting an absent row as zero. The extension also checks that the three seal unit IDs are unique and match on_hand.

Step 5 — build and validate the extended ontology#

From the directory containing ontology_lab.py, run these commands. If you have just extracted the archive, the first two rebuild and verify the baseline before migrating it. Each failure stops the sequence so an invalid build is not mistaken for an expected result.

powershell
.\.venv\Scripts\python.exe ontology_lab.py build
if ($LASTEXITCODE -ne 0) { throw "Baseline build failed." }
.\.venv\Scripts\python.exe ontology_lab.py validate
if ($LASTEXITCODE -ne 0) { throw "Baseline validation failed." }

.\.venv\Scripts\python.exe extensions/multipart/multipart_lab.py build
if ($LASTEXITCODE -ne 0) { throw "Extension build failed." }
.\.venv\Scripts\python.exe extensions/multipart/multipart_lab.py validate
if ($LASTEXITCODE -ne 0) { throw "Extension validation failed." }

On Linux or macOS, use .venv/bin/python in place of the Windows interpreter path. The recorded execution used Python 3.12.10 on Windows. The extension uses the baseline's pinned dependencies; it installs nothing and calls no remote service during the exercise.

Inspect extensions/multipart/results/schema.ttl, shapes.ttl, and instances.ttl. The supplied inputs produced 168 instance triples, 100 provenance triples, and three PartRequirement records across both organizations. The validation command returned conforms: true. Find WO-204 and confirm it has two requirement links and no direct part or quantity property. This checks the model change, rather than only the program's exit code.

Step 6 — query each requirement without losing missing evidence#

The fixed query is extensions/multipart/requirements_inventory.rq. It follows WorkOrder → PartRequirement → PartType → StockSnapshot, binds the selected organization on every business join, and aggregates existing allocations per stock snapshot. Its key change is that the requirement is mandatory but its stock match is OPTIONAL. SPARQL's optional pattern permits a result to retain a requirement when matching stock information is absent.603

That matters operationally. If a seal snapshot is missing, the requirement must remain visible as unresolved; dropping the entire row could make the order look complete. The Python query handler requires exactly one in-scope stock snapshot per requirement. Missing or multiple snapshots produce a null availability and an explicit issue. It applies the baseline's two-hour freshness rule to stock, existing allocations and inspection evidence.

powershell
.\.venv\Scripts\python.exe extensions/multipart/multipart_lab.py query --org ORG-TRAIN --order WO-204
if ($LASTEXITCODE -ne 0) { throw "Extension query failed." }

The actual result in extensions/multipart/results/explanation.json contains these values:

RequirementPartNeededAvailableShortage
WO-204-R1PF-9211
WO-204-R2SEAL-2130

For PF-9, available = 6 on hand − 3 reserved − 2 quarantined = 1. For SEAL-2, available = 3 on hand − 0 reserved − 0 quarantined = 3. All quantities use each.

The whole order's status is shortage_needs_human_review. A surplus of seals cannot cancel a shortage of filters, so there is no summed cross-part shortage field. Each line carries its own source locators, including the original filter rows or the added seal CSV rows. The inspection remains pending, and maintenanceReleaseAllowed remains false.

Step 7 — change a real requirement and predict the result#

Use a disposable copy of the lab. In the extension's requirements.csv, change only required_quantity from 1 to 4. The changed data row should read:

csv
WO-204-R2,ORG-TRAIN,WO-204,SEAL-2,4,each

Run the query before rebuilding: it must refuse the changed source with an explicit rebuild-required error. Then rerun the extension's build, validate and query commands. PF-9 remains needed 2, available 1, shortage 1. SEAL-2 becomes needed 4, available 3, shortage 1. Restore the original quantity and rebuild before comparing with the shipped output.

This exact exercise is implemented in test_increased_second_requirement_has_its_own_shortage. It demonstrates a useful property of the new model: changing one line's quantity leaves the other line's meaning and arithmetic intact. No code change is needed for this data change because the ontology already represents one-to-many requirements.

Step 8 — run the regression and failure checks#

powershell
.\.venv\Scripts\python.exe extensions/multipart/multipart_lab.py test
if ($LASTEXITCODE -ne 0) { throw "Extension tests failed." }

The extension suite runs 20 tests against temporary fixture copies. Its reports are extensions/multipart/results/test-summary.json and test-results.txt; the baseline has its separate 24-test suite. These are executable acceptance checks for the local implementation, not claims of load testing or operational accreditation.

Failure introduced by the testExpected result
Requirement points to the other organization's orderEntire extension build rejected
Same part appears twice on one orderRejected until a deliberate consolidation or allocation policy exists
Quantity is 1.5, unit is box, or stock IDs disagree with the countInput rejected
Seal stock is stale or missingSeal result unresolved; whole order has insufficient evidence
Two seal depots are present without a selection policyAvailability is unresolved rather than silently summed
Old direct quantity remains on a migrated WorkOrderSHACL rejects the mixed representation
Input or generated graph changes after a successful buildQuery requires a rebuild
Invalid rebuild leaves earlier result files on diskInvalid manifest prevents querying those stale files
Baseline changes and is rebuiltExtension also requires rebuilding against that new baseline
Extend the ontology through a tested change

Original engineering sequence. The practical result of an ontology change is a versioned schema, mapping, validation rule and query that still agree—not merely another box on a diagram.

Select a box to inspect its role. On a small screen, scroll the diagram sideways.
definemapcheckverifyrecord
Write the new question

State what a user must be able to answer and which source owns the required fact. This limits the classes and properties that need to be added.

Read the complete diagram as text
  • Write the new question: State what a user must be able to answer and which source owns the required fact. This limits the classes and properties that need to be added.
  • Change the schema: Add the necessary class or property, its meaning and its expected relationships. Keep stable identifiers and plan what happens to old records.
  • Add the source mapping: Map real source fields into the new representation with explicit identifiers, units, dates and provenance. Include unresolved cases in the test inputs.
  • Add shapes & tests: Add positive and negative cases for the new constraint. Re-run the existing tests to catch changes that invalidate earlier questions.
  • Run the new query: Check its result against known source records and make the explanation traceable. Include a question whose correct result is missing evidence.
  • Record the version: Keep the schema, input and tool versions with the results. A deployment needs a separately tested migration, publication and rollback process.
  • Write the new question → define → Change the schema
  • Change the schema → map → Add the source mapping
  • Add the source mapping → check → Add shapes & tests
  • Add shapes & tests → verify → Run the new query
  • Run the new query → record → Record the version

What you created: a new ontology class, explicit one-to-many relationships, a versioned migration, additional source mappings, revised constraints, a per-requirement query, and tests that check both preserved behaviour and new failure cases. What to check: both requirement identities exist, source locators resolve, the old WorkOrder fields are absent, and the observed query values match the table.

The remaining deployment work is concrete: authenticated identity, source-feed completeness, authorized reads, live revision/freshness checks, depot selection, allocation across competing orders, and durable approval/action handling. The local --org flag implements none of those services. Keep the reproducible graph and tests as the acceptance baseline when adding them.

Source 601 ↗

W3C, SHACL cardinality constraints — minimum and maximum value counts; checked 14 September 2026. The migration's shape design is our implementation.

Source 602 ↗

W3C, PROV-O derivation and activities — lineage vocabulary; checked 14 September 2026. The local migration and integrity limitations are our implementation.

Source 603 ↗

W3C, SPARQL optional graph patterns — retaining solutions when an optional match is absent; checked 14 September 2026. Requirement policy and result interpretation are our implementation.

Sources & further reading

  1. https://www.w3.org/TR/rdf11-concepts/ ↗
  2. OWL 2 Web Ontology Language Primer ↗
  3. On the Construction of Knowledge Graphs from Unstructured Text using Neuro-Symbolic Networks ↗
  4. Operational ontology overview ↗
  5. Shapes Constraint Language ↗
  6. PROV-O derivation and activities ↗
  7. SPARQL 1.1 Query Language ↗
  8. Getting started ↗
  9. Object types and instances ↗
  10. Declared property types ↗
  11. Relationships between object types ↗
  12. Shapes Constraint Language ↗
  13. SP 800-162: Attribute Based Access Control ↗
  14. Action submission criteria ↗
  15. External writeback and webhook behavior ↗
  16. SHACL cardinality constraints ↗
  17. SPARQL optional graph patterns ↗