Skip to content

Ontology & Reasoning

An ontology captures meaning about your data—relationships that exist at the type level, not just instance level. You need ontology when:

  • Type hierarchies: “A Podcast is a type of Media” (query for Media, get Podcasts too)
  • Concept relationships: “Machine Learning is narrower than AI” (topic navigation)
  • Constraints: “A Person cannot also be an Organization” (prevent invalid data)
  • Edge implications: “If Alice is married to Bob, she also knows Bob” (inferred relationships)
  • Bidirectional queries: “manages and managedBy are inverses” (traverse in either direction)

Without ontology, you’d implement these manually—if statements scattered throughout your code, hand-rolled validation, duplicate queries. Ontology centralizes this logic in your schema.

TypeGraph treats semantic relationships between types as meta-edges—edges at the type level rather than instance level:

// Instance edges: relationships between INSTANCES
// "Alice knows Bob"
const knows = defineEdge("knows");
// Meta-edges: relationships between TYPES
// "Employee subClassOf Person"
subClassOf(Employee, Person);

When you define an ontology, TypeGraph:

  1. Precomputes closures at store initialization (not query time)
  2. Expands queries automatically based on relationships
  3. Enforces constraints when creating nodes and edges

TypeGraph provides a standard set of meta-edges:

import { subClassOf, broader, narrower, equivalentTo, sameAs, differentFrom, disjointWith, partOf, hasPart, relatedTo, inverseOf, implies } from "@nicia-ai/typegraph";

subClassOf: Defines type inheritance where instances of the child are also instances of the parent.

subClassOf(Podcast, Media);
subClassOf(Article, Media);
subClassOf(Company, Organization);

Query Behavior:

Subclass expansion is opt-in via includeSubClasses: true:

// Without expansion: returns only nodes with kind="Media"
const mediaOnly = await store
.query()
.from("Media", "m")
.select((ctx) => ctx.m)
.execute();
// With expansion: returns Media, Podcast, AND Article nodes
const allMedia = await store
.query()
.from("Media", "m", { includeSubClasses: true })
.select((ctx) => ctx.m)
.execute();
// Results include nodes of kind "Media", "Podcast", and "Article"

This is a fundamental difference from traditional ORM inheritance—TypeGraph stores the concrete type (kind: "Podcast") in the database, and expands at query time when requested.

broader and narrower: Define conceptual hierarchy without identity.

broader(MachineLearning, ArtificialIntelligence);
broader(DeepLearning, MachineLearning);
broader(ArtificialIntelligence, Technology);

Important: This is different from subClassOf. A topic instance of “ML” is related to “AI”, but is not an instance of “AI”.

// Get all topics narrower than Technology
const narrowerTopics = registry.expandNarrower("Technology");
// ["ArtificialIntelligence", "MachineLearning", "DeepLearning", ...]

equivalentTo: Defines semantic equivalence between types or external IRIs.

equivalentTo(Person, "https://schema.org/Person");
equivalentTo(Organization, "https://schema.org/Organization");

sameAs: Declares identity between individuals (for deduplication).

differentFrom: Explicitly asserts non-identity.

disjointWith: Declares that two types cannot share the same ID.

disjointWith(Person, Organization);
disjointWith(Podcast, Article);

Effect: Attempting to create a node that violates disjointness throws DisjointError:

// Create a Person with ID "entity-1"
await store.nodes.Person.create({ name: "Alice" }, { id: "entity-1" });
// Throws DisjointError: Person and Organization are disjoint
await store.nodes.Organization.create({ name: "Acme" }, { id: "entity-1" });

partOf and hasPart: Define compositional relationships.

partOf(Chapter, Book);
hasPart(Book, Chapter);
partOf(Episode, Podcast);
hasPart(Podcast, Episode);

inverseOf: Declares two edge kinds as inverses of each other.

inverseOf(manages, managedBy);
inverseOf(cites, citedBy);
inverseOf(follows, followedBy);

Effect: You can query in either direction using the registry:

const inverse = registry.getInverseEdge("manages"); // "managedBy"

You can also expand traversals to include inverse edge kinds at query time:

const relationships = await store
.query()
.from("Person", "p")
.traverse("manages", "e", { expand: "inverse" })
.to("Person", "other")
.select((ctx) => ({
other: ctx.other.name,
via: ctx.e.kind,
}))
.execute();

For symmetric relationships, declare an edge as its own inverse:

inverseOf(sameAs, sameAs);

implies: Declares that one edge kind implies another exists.

implies(marriedTo, knows);
implies(bestFriends, friends);
implies(friends, knows);

Effect: Query for knows can include marriedTo, bestFriends, and friends edges:

const connections = await store
.query()
.from("Person", "p")
.traverse("knows", "e", { expand: "implying" })
.to("Person", "other")
.select((ctx) => ctx.other)
.execute();
const graph = defineGraph({
id: "knowledge_base",
nodes: { ... },
edges: { ... },
ontology: [
// Type hierarchy
subClassOf(Podcast, Media),
subClassOf(Article, Media),
subClassOf(Company, Organization),
// Concept hierarchy
broader(MachineLearning, ArtificialIntelligence),
broader(DeepLearning, MachineLearning),
// Constraints
disjointWith(Person, Organization),
disjointWith(Media, Person),
// Composition
partOf(Episode, Podcast),
// Edge relationships
inverseOf(cites, citedBy),
implies(marriedTo, knows),
],
});

The type registry (accessed via store.registry) provides methods to query the ontology:

const registry = store.registry;
// Subsumption
registry.isSubClassOf("Podcast", "Media"); // true
registry.expandSubClasses("Media"); // ["Media", "Podcast", "Article"]
// Hierarchy
registry.expandNarrower("Technology"); // ["AI", "ML", "DL", ...]
registry.expandBroader("DeepLearning"); // ["ML", "AI", "Technology"]
// Constraints
registry.areDisjoint("Person", "Organization"); // true
registry.getDisjointKinds("Person"); // ["Organization", "Media", ...]
// Edge relationships
registry.getInverseEdge("cites"); // "citedBy"
registry.getImpliedEdges("marriedTo"); // ["knows"]
registry.getImplyingEdges("knows"); // ["marriedTo", "bestFriends", "friends"]

Define domain-specific meta-edges:

import { metaEdge } from "@nicia-ai/typegraph";
// Custom meta-edge for prerequisite relationships
const prerequisiteOf = metaEdge("prerequisiteOf", {
transitive: true,
inference: "hierarchy",
description: "Learning prerequisite (Calculus prerequisiteOf LinearAlgebra)",
});
// Custom meta-edge for superseding relationships
const supersedes = metaEdge("supersedes", {
transitive: true,
inference: "substitution",
description: "Replacement relationship (v2 supersedes v1)",
});

Each meta-edge can be configured with these properties to control how TypeGraph computes closures and expands queries:

PropertyTypeDescription
transitivebooleanA→B, B→C implies A→C
symmetricbooleanA→B implies B→A
reflexivebooleanA→A is always true
inversestringName of inverse meta-edge
inferenceInferenceTypeHow this affects queries

The inference property determines how the meta-edge affects query behavior:

TypeDescription
"subsumption"Query for X includes instances of subclasses
"hierarchy"Enables broader/narrower traversal
"substitution"Can substitute equivalent types
"constraint"Validation rules
"composition"Part-whole navigation
"association"Discovery/recommendation
"none"No automatic inference

TypeGraph precomputes transitive closures at store initialization:

// subClassOf closure
// If: Podcast subClassOf Media, Episode subClassOf Media
// Then: expandSubClasses("Media") = ["Media", "Podcast", "Episode"]
// implies closure
// If: marriedTo implies partneredWith, partneredWith implies knows
// Then: getImpliedEdges("marriedTo") = ["partneredWith", "knows"]

This makes queries efficient—expansion happens at query compilation time, not execution time.

These have different semantics:

  • subClassOf: Instance identity (a Podcast is a Media)
  • broader: Conceptual relation (ML relates to AI, but ML instance ≠ AI instance)
// CORRECT: Type hierarchy
subClassOf(Podcast, Media);
// CORRECT: Concept hierarchy
broader(MachineLearning, ArtificialIntelligence);
// WRONG: Don't mix them
// subClassOf(MachineLearning, ArtificialIntelligence);

Prevent impossible combinations:

// Good: Prevent ID conflicts
disjointWith(Person, Organization);
disjointWith(Person, Product);
disjointWith(Organization, Product);
// Relationship hierarchy: specific → general
implies(marriedTo, partneredWith);
implies(partneredWith, knows);
implies(parentOf, relatedTo);
implies(siblingOf, relatedTo);
implies(relatedTo, knows);
inverseOf(manages, managedBy);
inverseOf(follows, followedBy);
inverseOf(cites, citedBy);

This lets you query efficiently in either direction without duplicating edges.

Declares type inheritance.

function subClassOf(child: NodeType, parent: NodeType): OntologyRelation;

Declares hierarchical relationship (narrower concept to broader concept).

function broader(narrower: NodeType, broader: NodeType): OntologyRelation;

Declares hierarchical relationship (broader concept to narrower concept).

function narrower(broader: NodeType, narrower: NodeType): OntologyRelation;

Declares semantic equivalence between types or with external IRIs.

function equivalentTo(
a: NodeType | string,
b: NodeType | string
): OntologyRelation;

Declares identity between individuals.

function sameAs(a: NodeType, b: NodeType): OntologyRelation;

Declares non-identity.

function differentFrom(a: NodeType, b: NodeType): OntologyRelation;

Declares mutual exclusion (types cannot share the same ID).

function disjointWith(a: NodeType, b: NodeType): OntologyRelation;

Declares compositional relationship (part to whole).

function partOf(part: NodeType, whole: NodeType): OntologyRelation;

Declares compositional relationship (whole to part).

function hasPart(whole: NodeType, part: NodeType): OntologyRelation;

Declares association between types.

function relatedTo(a: NodeType, b: NodeType): OntologyRelation;

Declares edge types as inverses of each other.

function inverseOf(edgeA: EdgeType, edgeB: EdgeType): OntologyRelation;

Declares that one edge type implies another exists.

function implies(edgeA: EdgeType, edgeB: EdgeType): OntologyRelation;

Creates a custom meta-edge for domain-specific relationships.

function metaEdge(
name: string,
options?: {
transitive?: boolean;
symmetric?: boolean;
reflexive?: boolean;
inverse?: string;
inference?: InferenceType;
description?: string;
},
): MetaEdge;

The type registry is available via store.registry and provides methods to query the ontology at runtime.

Checks if a type is a subclass of another.

registry.isSubClassOf(child: string, parent: string): boolean;
registry.isSubClassOf("Podcast", "Media"); // true

Returns a type and all its subclasses.

registry.expandSubClasses(type: string): readonly string[];
registry.expandSubClasses("Media"); // ["Media", "Podcast", "Article"]

Checks if two types are disjoint.

registry.areDisjoint(a: string, b: string): boolean;
registry.areDisjoint("Person", "Organization"); // true

Returns all types disjoint with the given type.

registry.getDisjointKinds(type: string): readonly string[];
registry.getDisjointKinds("Person"); // ["Organization", "Media", ...]

Returns all types narrower than the given type (via broader relationships).

registry.expandNarrower(type: string): readonly string[];
registry.expandNarrower("Technology"); // ["AI", "ML", "DeepLearning", ...]

Returns all types broader than the given type.

registry.expandBroader(type: string): readonly string[];
registry.expandBroader("DeepLearning"); // ["MachineLearning", "AI", "Technology"]

Returns the inverse of an edge type.

registry.getInverseEdge(edgeType: string): string | undefined;
registry.getInverseEdge("manages"); // "managedBy"

Returns edges implied by an edge type.

registry.getImpliedEdges(edgeType: string): readonly string[];
registry.getImpliedEdges("marriedTo"); // ["knows"]

Returns edges that imply an edge type.

registry.getImplyingEdges(edgeType: string): readonly string[];
registry.getImplyingEdges("knows"); // ["marriedTo", "bestFriends", "friends"]

Returns an edge type and all edges that imply it.

registry.expandImplyingEdges(edgeType: string): readonly string[];
registry.expandImplyingEdges("knows"); // ["knows", "marriedTo", "bestFriends", "friends"]