The Semantic Layer, From ScratchChapter 5
Chapter 5

The T-Box, part 1: classes (RDFS)

Where a graph keeps its schema, what a "class" really is, and why that schema describes your data instead of policing it. Then you write the first real piece of the c360 model.

Imagine inserting one triple saying that Carolyn Inworth's shoe size is 38. There is no shoe-size property anywhere, no class that mentions shoes, no schema at all. Stardog says "OK" and stores it. Try that in Aurora (UPDATE customer SET shoe_size = 38 WHERE cid = 225) and Postgres stops you cold: column does not exist. Your instinct says the graph is being sloppy. By the end of this chapter you will see that it is deliberate, that this choice is what makes a semantic layer over many databases possible, and where to go when you do want the database-style "no". Along the way you will write the first ten classes of the real c360 model: build step 1.

5.1Schema and data: the T-Box and the A-Box

So far every triple you have written was about one particular thing: Carolyn's email, the price of one order. Knowledge-graph people split triples into two families:

Why new names for "schema" and "rows"? Because in a graph they are made of the same stuff: both are triples, both sit in named graphs, both are queried with the same SPARQL. The names say which role a triple plays. The whole T-Box for a domain is an ontology (Stardog's tools also call it a model): one shared, machine-readable vocabulary for an organization. The single triple :Customer a owl:Class is already a very small ontology. Chapters 2 to 4 were A-Box work; this chapter and the next are T-Box work.

T-BOX: THE MODEL (KINDS OF THINGS) <urn:stardog:training-c360:1.0:model> :Order :Customer :Address A-BOX: THE DATA (PARTICULAR THINGS) order pos363f9784… customer:225 location:b80ea2ce… :purchasedBy :address a a (rdf:type) a

Press Play to build the two layers and connect them.

Figure 5.1: The T-Box names kinds of things; the A-Box describes particular things; rdf:type triples (dashed) connect each thing to its kind. All three are just triples.

Schema and data are both triples: there is no DDL/DML split.

Example: :Customer a owl:Class (T-Box) and customer:225 a :Customer (A-Box) are written and queried the same way.

5.2RDFS and OWL: why there are two schema languages

RDFS (RDF Schema, prefix rdfs: = http://www.w3.org/2000/01/rdf-schema#) came first (W3C, 1999 to 2004), because plain RDF can state facts but cannot say what kinds of things exist. It adds the bare minimum: classes, subclasses, labels, comments, and a way to say which classes a property connects. Tiny example: :Customer a rdfs:Class ; rdfs:label "Customer" .

OWL (the Web Ontology Language, prefix owl: = http://www.w3.org/2002/07/owl#; the letters are deliberately shuffled) followed in 2004, with OWL 2 in 2009. OWL is built on top of RDFS: everything RDFS says still holds, and OWL adds more precise statements. Three of them, each with our data (the property :placedOrder and the CRM name are hypothetical; the rest is real):

OWL can say…Written asWhat you gain
1. This link is the reverse of that one.
"Order → purchased by → Customer" and "Customer → placed order → Order" are the same fact read from opposite ends.
:placedOrder owl:inverseOf :purchasedBy . You store only order:363f9784… :purchasedBy customer:225. Ask for customer:225 :placedOrder ?o and Stardog answers order:363f9784… (Carolyn's Wax Paper order), although that triple was never stored.
2. These two kinds can never overlap.
Nothing can be both a Customer and a Product.
:Customer owl:disjointWith :Product . If a bad mapping ever produces customer:225 a :Product, a reasoner reports it as a contradiction (Carolyn can't be a product) instead of silently mixing customers into product lists.
3. These two names are the same thing.
A CRM calls Carolyn crm:person:C-0225; we call her customer:225.
customer:225 owl:sameAs crm:person:C-0225 . Facts about either name now count for both: the CRM's loyalty tier and our purchases come back together when you ask about Carolyn.

RDFS alone can say none of these three things. It knows that a Customer is a kind of thing and that one kind sits under another, but not reverses, exclusions or aliases. The tiny example :Customer a owl:Class . declares a class exactly as rdfs:Class does; for this book the two are interchangeable. The difference is in the extra statements OWL makes possible, like the three above.

RDFS is the foundation, OWL the extension, and real ontologies mix both. The c360 kit declares classes with owl:Class (every OWL class is also an RDFS class) and labels them with rdfs:label. Chapter 6 ends with a one-page comparison once you have seen properties too.

TermWhat it isWhy it existsTiny example
Ontology / modelThe full T-Box for a domainOne shared vocabulary for people, tools and AIthe c360 schema.ttl
T-BoxTriples about kinds of thingsTo describe what the data means:Order a owl:Class
A-BoxTriples about particular thingsThe facts themselvescustomer:225 :firstName "Carolyn"
ClassA named set of thingsTo group things of the same kind:Customer
rdf:type (a)"x is a member of class C"To connect data to the modelcustomer:225 a :Customer
rdfs:subClassOf"Every C1 is also a C2"Hierarchies and inherited membership:Big_Spender rdfs:subClassOf :Customer
rdfs:label, rdfs:commentHuman-readable name and descriptionIRIs are for machines; people and AI need words:CreditCard rdfs:label "Credit Card"
Open-world assumptionAbsent means "unknown", not "false"Data from many sources is never completeno phone triple ≠ no phone

5.3Classes and rdf:type

A class is a named set of things: all customers, all orders. You declare one by giving its IRI a type of its own, and connect a thing to it with rdf:type, the predicate you have been typing as a since Chapter 3:

:Customer a owl:Class .                            # T-Box: Customer is a class
<urn:stardog:demos:c360:customer:225> a :Customer .   # A-Box: 225 is a Customer

The class declaration has no column list. A class does not own properties the way a table owns columns; properties are declared separately and point at classes (Chapter 6). And because rdf:type is just a triple, you get freedoms a table never gives you:

By convention class names are capitalized (:CreditCard) and properties are not (:firstName). Nothing enforces it; everyone relies on it.

Now the labels. rdfs:label gives a human-readable name ("Credit Card") and rdfs:comment a description (the kit's "A concept representing a Customer's rewards account."). They look like decoration but are load-bearing: when Voicebox (Chapter 13) or your agent (Chapter 14) turns "Who are our top spenders in Wisconsin?" into SPARQL, labels and comments are much of what the language model reads to decide which class "spenders" means.

5.4Subclasses: inheritance you get for free

rdfs:subClassOf says every member of one class is also a member of another. This triple is real; it is in the kit's extended model, which you load in Chapter 10:

:Big_Spender rdfs:subClassOf :Customer .

The pay-off is inference: a new fact concluded from existing ones. Given customer:225 a :Big_Spender, a reasoner concludes customer:225 a :Customer, although nobody stored it.

:Customer :Big_Spender rdfs:subClassOf :Order :Large_Order rdfs:subClassOf customer:225 a (stored or rule-derived) a (inferred) The inferred triple is never stored. It exists only in the answers of a query run with reasoning on.

Press Play to watch membership flow up the hierarchy.

Figure 5.2: Two real subclass axioms from the kit's extended model (Chapter 10). Given type :Big_Spender, the reasoner adds type :Customer at query time.

Two practical notes. Inference happens only with Studio's reasoning toggle on, and it stays off until Chapter 10. And inferences are computed at query time, never written to disk, so they cannot go stale; that is why reasoning works over virtual graphs, whose data is not in Stardog at all. The base c360 schema has no subClassOf triples: ten flat classes cover the core questions, and the hierarchy arrives with the business rules in Chapter 10.

5.5The open-world assumption

Back to Carolyn's shoe size: this is the mindset shift of the book. Every database you have used runs on the closed-world assumption: the database is the complete record of its slice of the world. No order row for customer 225 in March means she ordered nothing in March; a bad foreign key is rejected at the door. Absence means "false", and the schema is a contract data must pass to get in.

RDFS and OWL run on the open-world assumption: a graph is what is known, never a claim to know everything. A missing triple means "not stated here", not "false". No phone triple for Marlane does not mean she has no phone (she has one; you left it out in Chapter 4).

Missing means unknown, not false.

Example: no :phone triple for Marlane means "we don't know her phone", not "she has no phone".

CLOSED WORLD (SQL database) OPEN WORLD (RDFS / OWL) Q: Does Marlane have a phone? phone column is NULL / no row A: No. Q: Does Marlane have a phone? no :phone triple in this graph A: Unknown. Not stated here. Set Carolyn's shoe_size = 38 (no such column) Rejected: column does not exist. Insert customer:225 :shoeSize 38 (no such property declared) Accepted and stored. The schema is a contract that data must pass. The schema is a description to reason with.
Figure 5.3: The same two situations in each world. Neither is "right"; they serve different needs.

Four consequences, from most to least surprising:

  1. The schema does not reject data. Nothing stops an undeclared property, a customer with three emails, or an order with no customer. RDFS and OWL have no NOT NULL, no CHECK, no foreign keys.
  2. Where SQL raises an error, a reasoner draws a conclusion. The reasoner fills gaps instead of refusing. Chapter 6 shows the famous case: tell it that only orders are "purchased by" someone, feed it a buggy triple, and it concludes that a box of Wax Paper is an order.
  3. Different names are not assumed to be different things. Two IRIs might name the same customer. Ours are built from the same keys on both sides (the "IRI trick", Chapter 9), so this rarely bites.
  4. Queries still answer from what is there. SPARQL evaluates patterns over the triples it has: FILTER NOT EXISTS { ?c :phone ?p } returns customers with no phone triple, a perfectly closed-world answer. The open world does not change how SPARQL runs; it changes how you read the result ("no phone triple in the graphs we queried") and what the reasoner may conclude. Recall Chapter 4's Query 5, where Marlane "outspent" Carolyn only because we loaded two orders each.

Why build it this way? Because the open world is the honest assumption when data comes from many places. Aurora does not "know" that a customer has no orders; orders are not its job. A closed-world schema over Aurora and Redshift would either reject good data or lie about completeness. A semantic layer describes data it does not own. The price: validation becomes a separate, explicit step.

5.6Where the c360 model lives

The model gets its own named graph, <urn:stardog:training-c360:1.0:model>, in the kit-c360 database (the name comes from the c360 knowledge kit; 1.0 is a version). A separate graph pays off three times: in Chapter 6 you register it as the reasoning schema; from Chapter 8 on the data stays in Aurora and Redshift, so this graph (plus Chapter 10's rules) is all that is physically stored in Stardog; and in Chapter 12 named graphs are the unit of permission, so everyone can read the model even when they cannot read the PII.

5.7Real build step 1: the ten classes

This is not scratch work: these triples start the model the reasoner, Voicebox and your agent will use. They are word for word the class declarations in schema.ttl, the c360 kit's ontology: one Turtle file holding the whole vocabulary, 161 triples, published in Stardog's public GitHub repository (https://github.com/stardog-union/knowledge-kits/blob/main/examples/c360/models/schema.ttl). Loading the full file in Chapter 6 creates nothing different.

Check yourself

Which are T-Box and which A-Box: (a) :Order a owl:Class, (b) customer:225 :firstName "Carolyn", (c) customer:225 a :Customer, (d) :Big_Spender rdfs:subClassOf :Customer?

(a) and (d) are T-Box (kinds of things); (b) and (c) are A-Box (one customer). (c) is the bridge: an A-Box triple whose object is a T-Box class.

Why do both RDFS and OWL exist, and which does the c360 kit use?

RDFS is deliberately minimal (classes, subclasses, labels, domain/range). OWL builds on it to express inverse relationships, disjoint classes and identity. The kit uses both: owl:Class and OWL property types, with rdfs:label and rdfs:comment.

Someone inserts customer:999 :favoriteColor "teal", and no such property is declared. What happens?

Stardog stores it. Under the open world the model describes; it does not reject. To have such data reported, use SHACL (Chapter 11).

A query for customers without an email returns Marlane. What exactly may you conclude?

Only that no :email triple for her exists in the graphs you queried, not that she has no email. (In Aurora she has one.)

With reasoning on, why does ?c a :Customer return a thing whose only stored type is :Big_Spender?

Because :Big_Spender rdfs:subClassOf :Customer lets the reasoner infer the type at query time. The inferred triple is never written to disk.

Chapter recap

Paste it to Claude with what you saw (the counts or results), so we can confirm it matches before the next build step relies on it.