Ontology in a graph database is not an academic exercise β it is the difference between a graph that answers questions and a graph that is a tangled hairball. An ontology defines what kinds of things exist in your domain, what properties they carry, and how they relate to one another. Without one, your graph grows without structure and querying becomes guesswork.
The LeadGraph project offers a concrete, production-grade example of ontology design. It is a market intelligence platform for Siemens Healthineers that ingests data from 15 external sources (FDA 510(k) clearances, clinical trials, patents, conference exhibitor lists, grant databases, GitHub repositories, and more), normalises everything into a Neo4j knowledge graph, and scores companies by commercial relevance.
This article walks through the ontology decisions in LeadGraph and extracts the general principles that apply to any graph database project.
What Ontology Means in Neo4j
In Neo4j, ontology manifests as:
Node labels β :Company, :Product, :Application, :Signal. These are your entity types.
Property constraints β :Company.normalizedName is unique; :Signal.type is indexed.
Relationship types β SUPPLIES, DEVELOPS, HAS_SIGNAL, USED_IN. These encode the semantic edges between entities.
Value ranges β :Signal.confidence is a float from 0 to 1; :Signal.type is one of 12 enumerated signal types.
A well-designed ontology answers three questions: what exists, what matters about it, and how it connects.
The LeadGraph Ontology
The ontology is seeded explicitly β not discovered. The seedGraph() function in ontology.ts creates constraints, nodes, and relationships in a single transaction-friendly batch:
CREATE CONSTRAINT IF NOT EXISTS FOR (c:Company) REQUIRE c.normalizedName IS UNIQUE;
CREATE CONSTRAINT IF NOT EXISTS FOR (a:Application) REQUIRE a.name IS UNIQUE;
CREATE INDEX IF NOT EXISTS FOR (s:Signal) ON (s.type);
These three lines capture the ontology's backbone: every company must have a unique normalised name, every application area must be a named entity, and signals must be queryable by type.
Node Labels and Their Semantics
The ontology defines seven node labels:
Label
Purpose
Key Properties
Uniqueness
Company
An organisation in the diagnostics market
name, normalizedName, domain, segment, region
normalizedName
Product
A specific Siemens product or reagent
catalogId, name, category
catalogId
Application
A clinical application area
name, category
name
Signal
An external indicator of activity
type, date, confidence, description, url
none (event)
Contact
A person at a company
name, email, role
none
PipelineStage
A sales pipeline milestone
stage, enteredAt
none
Activity
A logged interaction
type, note, date
none
The distinction between Company and Product is straightforward. The interesting design choice is Application as a separate node rather than a property on Company. This enables multi-hop traversal: you can find companies that develop assays in the same application area as a given Siemens product β a query that would be expensive with property-based filtering on a relationship.
Relationship Semantics
The six relationship types encode the domain's business logic:
(:Company)-[:SUPPLIES]->(:Product) // Siemens manufactures this product
(:Company)-[:DEVELOPS]->(:Application) // Company works in this application area
(:Company)-[:HAS_SIGNAL]->(:Signal) // Company triggered this external signal
(:Product)-[:USED_IN]->(:Application) // Product is relevant to this clinical area
(:Contact)-[:CONTACT_AT]->(:Company) // Person works at this organisation
(:Contact)-[:HAS_ACTIVITY]->(:Activity) // Person had an interaction
(:Contact)-[:IN_STAGE]->(:PipelineStage) // Current pipeline status
The DEVELOPS relationship is where the ontology does its heaviest lifting. Every external data point β an FDA clearance, a conference appearance, a new hire β is mapped through the application area classification. When the scoring engine runs, it computes productFitScore as the overlap ratio between a company's application areas and Siemens' product portfolio:
Different raw data, same ontology. This is the central value of a well-designed graph ontology: it makes disparate data sources queryable through a single model.
Why Confidence Is Part of the Ontology
Notice the confidence field on every signal. The ontology encodes uncertainty because not all data sources are equally reliable. An FDA clearance (confidence 0.9) is a stronger signal than a news article mentioning a company (confidence 0.5). By embedding confidence as a property, the scoring engine can weight signals by source reliability:
This is a deliberate ontology decision: uncertainty is a first-class property of your data model, not an afterthought.
What Makes a Good Graph Ontology
Drawing from the LeadGraph example and general graph database practice, here are the principles:
1. Entities Are Nodes, Values Are Properties
A common mistake is storing important domain concepts as properties on other nodes. In LeadGraph, Application is a separate node, not a string array on Company. This seems trivial but has major implications:
You can query all companies in an application area without full scans.
You can attach metadata to the application area itself (market size, growth rate).
You can join through application areas across different entity types (companies and products).
Rule of thumb: if you query by it, filter on it, or join through it, it should be a node.
2. Relationships Are Named, Not Tagged
Another common anti-pattern is using a generic RELATED_TO relationship with a type property to distinguish semantics. This forces every query to filter by property values, destroying performance and readability.
LeadGraph uses distinct relationship types (DEVELOPS, SUPPLIES, HAS_SIGNAL) that are self-documenting and indexable.
3. The Ontology Must Be Explicitly Seeded
LeadGraph seeds its ontology in code β the application areas and Siemens product portfolio are defined as TypeScript arrays:
This is not an accident. An ontology that emerges organically from data is rarely coherent. Explicit seeding means every node label, relationship type, and property constraint is a conscious design decision.
4. External Data Must Be Normalised, Not Imported Raw
The adapter pattern is critical. Each data source maps to the shared ontology via normalize(). If you import raw FDA data in FDA's schema and raw patent data in the patent office's schema, you have not built a knowledge graph β you have a data lake.
5. Constraints Are Ontology Enforcement
Neo4j constraints (REQUIRE ... IS UNIQUE) are not optional performance hints. They enforce the ontology at the database level. If two source adapters produce a company with the same name but different spellings, the normalizedName constraint catches the collision. The normalizeCompanyName() function handles deduplication:
export function normalizeCompanyName(name: string): string {
return name
.toLowerCase()
.replace(/[^a-z0-9\s]/g, "")
.replace(/gmbh|ag|limited|ltd|inc|corp|llc/g, "")
.trim()
.replace(/\s+/g, "-");
}
Common Ontology Mistakes
From building and iterating on the LeadGraph ontology, the recurring pitfalls are:
Over-normalisation. Splitting everything into nodes creates traversal hell. A company's region (EUROPE, NORTH_AMERICA) is a property, not a node β querying "all companies in Europe" requires only a property index, not a three-hop traversal.
Under-normalisation. Storing application areas as a comma-separated string on Company loses the ability to query across the application dimension. The rule of thumb above applies.
Ignoring time. Signals have dates, contacts have activity timestamps, pipeline stages have entry dates. If your ontology does not model time, your graph cannot answer "what changed."
Treating all relationships as equal. A HAS_SIGNAL relationship with a type property on the relationship is different from a named HAS_SIGNAL relationship with a Signal node that has a type property. The latter allows the signal to have its own properties (confidence, date, description) and to exist independently of the relationship.
Testing the Ontology
LeadGraph tests the ontology through integration tests that seed the graph and verify query results:
// From neo4j.test.ts
test("seeded ontology has correct structure", async () => {
const result = await seedGraph();
expect(result.constraintsCreated).toBe(3);
expect(result.companiesSeeded).toBeGreaterThan(1);
expect(result.applicationAreas).toBe(7);
expect(result.productsSeeded).toBeGreaterThan(0);
});
More importantly, the scoring tests validate that the ontology supports the expected queries:
If the ontology changes, these tests fail β providing a safety net for schema evolution.
Property Graph Ontology vs. Formal Ontology (RDF/OWL)
A question that arises naturally after building a property graph ontology is: how does this compare to formal ontology languages like RDF and OWL? The short answer is that property graphs and RDF/OWL occupy different points on the formality spectrum, and the right choice depends on whether your priority is operational query performance or semantic interoperability.
The Fundamental Difference
Property graph ontologies (Neo4j's model) are implicit. The ontology lives in the application code β the node labels, relationship types, and property constraints are conventions enforced by your application layer and database constraints. There is no formal language for declaring that :Company is a subclass of :Organisation, or that DEVELOPS is the inverse of DEVELOPED_BY.
RDF/OWL ontologies are explicit. They use formal languages (RDFS, OWL, SKOS) to declare classes, properties, axioms, and inference rules. A triple store reasons over these declarations automatically β if you assert that DEVELOPS rdfs:subPropertyOf :isInvolvedWith, the store infers that any DEVELOPS triple also implies isInvolvedWith.
Dimension
Property Graph (Neo4j)
RDF/OWL (triple store)
Ontology representation
Implicit β labels, types, constraints in application code
Explicit β RDFS/OWL declarations stored as triples
Schema enforcement
Database constraints (UNIQUE, INDEX, NODE KEY)
OWL axioms, SHACL shapes, SPIN rules
Inference
Manual β join logic in queries or application code
Automatic β reasoners infer implicit triples
Query language
Cypher (pattern matching)
SPARQL (graph patterns + reasoning)
Interoperability
Proprietary β no standard exchange format for ontology
W3C standards β RDF, OWL, SKOS are vendor-neutral
Performance at scale
Excellent β traversal over indexed property graph
Variable β reasoning overhead grows with axiom complexity
Best for
Operational applications, real-time queries, OLTP
Data integration, linked data, semantic interoperability
When to Push Your Property Graph Ontology Toward RDF
There are situations where a pure property graph ontology is insufficient and layering on RDF/OWL techniques adds value:
Cross-domain data integration. If your graph must merge ontologies from multiple independent sources β for example, combining a medical ontology (SNOMED CT) with a corporate registry (legal entity types) β RDF's URI-based naming avoids collisions. Two different concepts called :Product in different systems remain distinguishable as distinct URIs.
Inference-heavy queries. Suppose you define that :CardiacMarkers is a subclass of :Application. In Neo4j, querying "all applications" requires knowing to include :CardiacMarkers. This knowledge lives in your application code. In an OWL ontology, the query ?app rdf:type :Application automatically includes subclasses through the reasoner. Neo4j can approximate this with a label hierarchy pattern β using multiple labels (:Application:CardiacMarkers) on the same node β but it is a convention, not an inference rule.
Long-term schema governance. If your knowledge graph has a lifespan measured in years with multiple teams contributing, an explicit ontology document (an OWL file or a SHACL shapes graph) provides a single source of truth that application code alone cannot.
The Neosemantics Bridge
The neosemantics (n10s) plugin bridges the two worlds. It allows Neo4j to import RDFS/OWL/SKOS ontologies as property graphs and export property graphs back to RDF. This means you can:
Import it into Neo4j via CALL n10s.onto.import.fetch("file:///ontology.owl", "RDF/XML").
Use the imported labels and relationship types as the backbone of your property graph.
Optionally export subsets back to RDF for exchange with external systems.
This pattern is especially useful for regulated industries (medical devices, financial compliance) where an explicit, auditable ontology is a regulatory requirement rather than an engineering preference.
Decision Framework
Your Situation
Recommended Approach
Single team, single domain, operational app
Property graph ontology (Neo4j with constraints)
Multiple teams contributing to one KG
Property graph ontology + n10s for an OWL mapping layer
Cross-organisation data exchange
RDF/OWL triple store, or Neo4j + n10s export
Inference-heavy, class hierarchy queries
OWL reasoner, or Neo4j with label hierarchy convention
The LeadGraph project sits firmly in the property graph camp, which is the right choice for a single-team operational intelligence platform. The ontology is enforced through TypeScript code and Cypher constraints β explicit enough to be testable, implicit enough to avoid the overhead of a formal reasoner.
Ontology Versioning and Evolution
An ontology is not static. As LeadGraph added data sources, the original seven node labels and six relationship types evolved. New signal types were introduced, application areas were split, and property constraints were tightened. Managing this evolution without breaking existing queries is a distinct engineering challenge that deserves explicit treatment.
The Problem with Schema-Less Evolution
Because Neo4j does not enforce a schema at the database level (labels and relationship types are created on first use), it is easy to drift into an inconsistent state. Consider what happens when a developer adds a new signal type without updating the ontology definition:
// Old ontology: signals have type, date, confidence
CREATE (:Signal {type: "NEWS_ARTICLE", date: "2026-06-01", confidence: 0.5})
// But the new source also includes a `sourceUrl` property
CREATE (:Signal {type: "NEWS_ARTICLE", date: "2026-06-01", confidence: 0.5, sourceUrl: "https://..."})
Now some Signal nodes have sourceUrl and others do not. The ontology has drifted. Queries that expect uniform structure either break silently or produce incomplete results.
Four Strategies for Ontology Versioning
1. Schema Constraints as Contracts. Neo4j's NODE KEY constraint is more powerful than UNIQUE β it enforces that every node with a given label must have a specified set of properties. For LeadGraph, adding a node key for Signal would prevent property drift:
CREATE CONSTRAINT IF NOT EXISTS FOR (s:Signal)
REQUIRE (s.type, s.date, s.confidence) IS NODE KEY;
This constraint rejects any CREATE or MERGE that omits or adds properties beyond the key set. The trade-off is reduced flexibility β you must update the constraint before adding new properties.
2. Ontology Migration Scripts. Treat ontology changes like database migrations. Each change gets a versioned script that applies the mutation atomically:
// migration_003.cql β Add sourceUrl to Signal nodes
// Applied: 2026-06-15
// Step 1: Add the property to existing signals
MATCH (s:Signal)
WHERE s.sourceUrl IS NULL
SET s.sourceUrl = '';
// Step 2: Update the node key constraint
DROP CONSTRAINT IF EXISTS FOR (s:Signal) REQUIRE (s.type, s.date, s.confidence) IS NODE KEY;
CREATE CONSTRAINT IF NOT EXISTS FOR (s:Signal)
REQUIRE (s.type, s.date, s.confidence, s.sourceUrl) IS NODE KEY;
// Step 3: (Optional) Re-index if needed
CREATE INDEX IF NOT EXISTS FOR (s:Signal) ON (s.sourceUrl);
Version-controlled migration scripts make ontology history auditable and reversible.
3. Ontology Metadata in the Graph. For long-lived knowledge graphs, store the ontology version as metadata inside the graph itself:
CREATE (v:OntologyVersion {
version: "2.1.0",
appliedAt: datetime("2026-06-15T10:00:00Z"),
changes: ["Added sourceUrl to Signal node key", "Split Autoimmune Diagnostics into sub-application areas"],
migrationScript: "migration_003.cql"
});
This is useful for debugging β when a query returns unexpected results, check which ontology version was active when the data was ingested.
4. Backward-Compatible Query Views. When deprecating a label or relationship type, maintain backward compatibility through a transition period by creating a virtual mapping layer:
// Old applications were single labels. New ontology uses sub-areas.
// Maintain compatibility by querying through a union:
MATCH (c:Company)-[:DEVELOPS]->(app)
WHERE app.name IN ["Autoimmune Diagnostics"]
OR (app:Application AND exists((app)-[:SUB_AREA_OF]->(:Application {name: "Autoimmune Diagnostics"})))
RETURN c.name, app.name
As deprecated patterns phase out, the compatibility layer shrinks until it can be removed entirely.
Practical Versioning Workflow
Phase
Action
Verification
Proposal
Write migration script in a PR; include expected query changes
Code review confirms backward compatibility
Staging
Run migration against a snapshot of production; compare query results
Integration tests pass; slow queries identified
Deployment
Apply migration; update ontology metadata node
CALL db.schema.visualization() confirms new structure
Monitoring
Track error rates for queries using deprecated patterns
Alert if query latency increases by more than 10%
Cleanup
Remove compatibility views after transition period
Remove deprecated labels; verify no queries reference them
LeadGraph adopted the migration-script approach (strategy 2) after an incident where an unconstrained property addition caused a scoring query to return incorrect results. The migration scripts live alongside the source adapters in the repository and are applied as part of the deployment pipeline.
Summary
The LeadGraph project demonstrates that ontology design is the single most impactful decision in a graph database project. It determines what queries are possible, how performant they are, and whether the graph remains coherent as new data sources are added.
The key takeaways for building your own graph ontology:
Explicitly seed your ontology β do not let it emerge from data.
Entities are nodes, values are properties β if you query by it, make it a node.
Name your relationships β generic RELATED_TO is a code smell.
Normalise external data into your ontology β each source adapter is a translation layer.
Model uncertainty β confidence scores are first-class ontology properties.
Enforce with constraints β UNIQUE and INDEX are ontology guarantees, not performance hacks.
Test the ontology β if a schema change breaks queries, your tests should catch it.
A graph without ontology is just a collection of nodes. A graph with ontology is a knowledge graph.
Robinson, I., Webber, J. & Eifrem, E. Graph Databases, 2nd Edition. O'Reilly Media, 2015. ISBN 978-1-449-35625-5. β The definitive reference on graph database modeling, covering node labels, relationship types, and the property graph model.