SKOS: Modelling Controlled Vocabularies, Thesauri and Taxonomies

The language carrying most of the world’s published controlled vocabulary is not OWL, and it is deliberately, aggressively weak. SKOS is the de facto standard for publishing thesauri, taxonomies, classification schemes and subject heading lists on the web, and the institutions running on it include the EU Publications Office, the FAO, national libraries across Europe, and the tagging layer of most enterprise content platforms that have one. It cannot express a cardinality, cannot detect a contradiction, and refuses on principle to tell you what its own hierarchy means. Lesson 6 gave you a language designed to entail as much as possible. This lesson gives you its opposite, and makes the case that the opposite is usually what the business actually asked for.

SKOS: Modelling Controlled Vocabularies, Thesauri and Taxonomies

Why a weak language, immediately after a strong one

SKOS, the Simple Knowledge Organization System, became a W3C Recommendation on 18 August 2009, developed by the Semantic Web Deployment Working Group. Its namespace is http://www.w3.org/2004/02/skos/core#, conventionally prefixed skos:. Its design principle is minimal ontological commitment: it gives you just enough structure to publish a thesaurus on the web and almost nothing more, because the thesauri it was designed to carry had been built for decades by people who were never making logical claims in the first place.

That is the point most engineers miss. When a librarian files “Centrifugal pumps” under “Pumps” with a BT relation, they are not asserting that every centrifugal pump is a pump, in the sense that would let a reasoner derive new facts and flag a contradiction. They are asserting something much more modest and much more useful: a person looking for one is plausibly interested in the other, and a search system should behave accordingly. Thesaurus hierarchies mix generic relations, whole-part relations and instance relations freely, because for retrieval it rarely matters which one you meant. Force that structure into rdfs:subClassOf and you have promoted forty years of navigational convenience into forty years of logical claims, most of which are false.

SKOS exists so you do not have to. It models the vocabulary as data about concepts rather than as a theory about the world. The concepts are individuals, not classes. The hierarchy is a documented editorial relation, not an entailment. This is why it takes weeks rather than months, why the domain experts can maintain it without you, and why it survives contact with reality.

The three moving parts: schemes, concepts, labels

A SKOS vocabulary has three structural ingredients, and you can build something useful knowing only these.

The concept scheme is the container. skos:ConceptScheme is your vocabulary as a thing with an identity, a publisher and a version. Concepts declare membership with skos:inScheme, whose range is fixed to skos:ConceptScheme. The entry points are declared with skos:hasTopConcept, or from the other end with skos:topConceptOf, which the specification defines as a sub-property of skos:inScheme and as the inverse of skos:hasTopConcept. That sub-property relationship is a small kindness: declare a top concept and its scheme membership follows, so you cannot accidentally publish an entry point that belongs to nothing. Note also that skos:ConceptScheme is disjoint with skos:Concept. A scheme is not itself a concept, and the instinct to make your vocabulary its own root node is a modelling error the specification explicitly forbids.

The concept is an skos:Concept: a unit of thought with a URI, not a word. This is the distinction that makes the whole thing work multilingually. “Pump”, “Pumpe” and “pompe” are three labels on one concept, not three concepts. Give the concept an opaque URI and you can change every label in every language without breaking a single tagged document.

The labels come in three flavours and the specification constrains them precisely. skos:prefLabel, skos:altLabel and skos:hiddenLabel are pairwise disjoint properties, so the same string cannot be both preferred and alternative for a concept. And a resource has no more than one value of skos:prefLabel per language tag. One preferred label per language, as many alternatives as you like, and hidden labels for the strings you want to match on but never display: misspellings, obsolete forms, the way the shop floor actually writes it. The hidden label is the single most underused construct in SKOS and the one that most reliably improves search recall.

@prefix skos: <http://www.w3.org/2004/02/skos/core#> .
@prefix ex:   <https://example.org/aberdeen/> .

ex:EquipmentScheme a skos:ConceptScheme ;
    skos:prefLabel "Aberdeen-2 equipment types"@en ;
    skos:hasTopConcept ex:RotatingEquipment .

ex:RotatingEquipment a skos:Concept ;
    skos:topConceptOf ex:EquipmentScheme ;
    skos:prefLabel "Rotating equipment"@en ;
    skos:narrower ex:CentrifugalPump .

ex:CentrifugalPump a skos:Concept ;
    skos:inScheme ex:EquipmentScheme ;
    skos:prefLabel "Centrifugal pump"@en ;
    skos:altLabel  "Pump, centrifugal"@en ;      # maintenance catalogue form
    skos:hiddenLabel "centrifgal pump"@en ;      # the spelling in 300 work orders
    skos:notation "CP"^^ex:TagCode ;
    skos:broader ex:RotatingEquipment ;
    skos:definition "A pump that moves fluid by means of a rotating impeller."@en .

Semantic relations, and the trap in skos:broader

SKOS gives you three relations between concepts inside a scheme: skos:broader, its inverse skos:narrower, and the symmetric skos:related. Everything interesting about them is in what they refuse to do.

skos:broader is not transitive. The specification is explicit, and it catches almost everyone: “Note that skos:broader is not a transitive property. Similarly, skos:narrower is not a transitive property.” Instead, condition S22 declares that skos:broader is a sub-property of skos:broaderTransitive, and it is skos:broaderTransitive that carries transitivity. So the transitive closure is available when you want it, by asking for the transitive property, and is not forced on you when you do not. This matters because thesaurus hierarchies are full of links that do not compose. A concept can be narrower than two unrelated parents, and chaining three hops of mixed generic and partitive links produces conclusions no editor intended. SKOS lets the closure be an explicit request rather than an ambient fact.

skos:related is disjoint with skos:broaderTransitive. Condition S27, and the consequence is worth stating plainly: two concepts cannot be both hierarchically and associatively related, at any depth. Because skos:related is symmetric and the transitive hierarchical properties are inverses of each other, the disjointness also covers skos:narrowerTransitive. This single condition catches a large share of real vocabulary defects, because “related” is where editors put links they could not be bothered to think about.

skos:broader is not rdfs:subClassOf, and it is not part-of. It means, precisely, “has a broader concept”. The traditional thesaurus distinctions of BTG for generic, BTP for partitive and BTI for instance relations are collapsed into one relation on purpose, because retaining them requires editors to make a metaphysical decision on every link and the retrieval benefit is near zero. If you need the distinction, you have outgrown SKOS, which is lesson 9’s subject.

Worked example · the Aberdeen-2 asset register

Where we left it: schema v0.1 with a working error detector, and three source systems still answering the same question in three dialects.

The register’s oldest wound is not logical, it is lexical. Engineering calls it a centrifugal pump, the maintenance catalogue files it as “Pump, centrifugal”, the contractor’s survey sheet says “CP” and, in three hundred work orders, “centrifgal pump”. Lesson 6 built an ontology that says what a pump is. None of that helps a search for “pump, centrifugal” find anything, because OWL models meaning and this is a problem about strings.

So we add a second, cheaper layer beside the ontology, not inside it. One skos:ConceptScheme, ex:EquipmentScheme, with a concept per equipment type. Each source dialect becomes a label on the concept, not a concept of its own: the engineering term is the skos:prefLabel, the catalogue form is an skos:altLabel, the misspelling is a skos:hiddenLabel that matches in search and never renders, and the contractor’s “CP” becomes a typed skos:notation. Three vocabularies, one concept, zero renaming of anyone’s source system. The whole thing took an afternoon, and the crucial property is that the maintenance planner can maintain it, because nothing in it requires understanding description logic.

Note what we deliberately did not do. We did not make ex:CentrifugalPump the SKOS concept and ex:CentrifugalPump the OWL class the same resource, and we did not attach ex:P101 to the scheme. The scheme is about vocabulary; the ontology is about what things are; the register is about which things exist. Keeping the three apart is what lets each be edited by the people competent to edit it.

State of the register: a concept scheme absorbing three dialects, with the ontology untouched. Next lesson turns this working file into a governed product, because a vocabulary nobody owns rots within a year.

Collections, notation and the documentation properties

Three further constructs earn their place in real vocabularies.

Collections handle the node label problem. Thesauri routinely contain grouping headings, “pumps by drive type”, that organise the display but are not concepts anybody would ever tag a document with. Making them concepts pollutes the vocabulary; leaving them out loses the structure. SKOS gives you skos:Collection and its subclass skos:OrderedCollection, both instances of owl:Class, with skos:member whose range is the union of skos:Concept and skos:Collection, and skos:memberList, a functional property whose range is an rdf:List, for when the order is meaningful. A collection is not a concept and cannot be broader than anything, which is exactly the constraint you want.

skos:notation carries the code. It is an owl:DatatypeProperty, and by convention it is used only with a typed literal whose datatype URI denotes the particular notation system in play. That convention is doing real work: it lets one concept carry a UNSPSC code, an internal tag code and a national classification number simultaneously without any of them being confusable. Note that no domain is stated for skos:notation, so the effective domain is everything, and you can notate collections and schemes too.

The documentation properties are where the editorial intelligence lives, and they are the part engineers skip. skos:definition, skos:scopeNote, skos:example, skos:historyNote, skos:editorialNote and skos:changeNote all specialise skos:note. The one that prevents the most damage is skos:scopeNote, which records where the boundary of a concept was drawn and why. A vocabulary without scope notes will be re-litigated by every new indexer, and the same concept will drift in meaning while its URI stays stable, which is the worst possible failure because nothing detects it.

SKOS-XL: when a label needs its own identity

Ordinary SKOS labels are plain literals, so you cannot say anything about them. The moment you need to record who approved a translation, when a term was deprecated, which acronym expands to which full form, or that two labels are historically the same term, you need labels to be resources. Appendix B of the same Recommendation defines the SKOS eXtension for Labels, SKOS-XL, which reifies exactly that: skosxl:Label instances carry a skosxl:literalForm, are attached with skosxl:prefLabel, skosxl:altLabel and skosxl:hiddenLabel, and can be related to each other with skosxl:labelRelation.

The cost is real. Every label becomes a node, the graph roughly triples in size, and every query gains a hop. The rule of thumb that holds up: use plain SKOS until you have an identified stakeholder who needs per-label metadata, and note that multilingual terminology work in institutional settings almost always produces that stakeholder, which is why the EU Publications Office and the FAO both run SKOS-XL and most enterprise tagging vocabularies do not.

SKOS and ISO 25964

SKOS did not appear from nowhere and it does not replace the thesaurus standards. ISO 25964-1, published in 2011, covers thesauri for information retrieval; ISO 25964-2, published in 2013, covers interoperability with other vocabularies. The relationship between the ISO data model and SKOS is formally documented rather than left to folklore: NISO, as maintenance agency, publishes the ISO 25964-1 data model, an XML schema for exchange, and a correspondence document dated 11 December 2013 mapping ISO 25964 to SKOS, SKOS-XL and MADS, with a machine-readable RDF version at purl.org/iso25964/skos-thes. That RDF schema, usually prefixed iso-thes:, is what you reach for when plain SKOS is not quite enough: it supplies thesaurus arrays, concept groups and the compound-equivalence machinery that ISO thesauri use and SKOS core deliberately omitted.

On the American side, ANSI/NISO Z39.19-2005, reaffirmed in 2010, covers the construction and management of monolingual controlled vocabularies. It overlaps ISO 25964 and is broadly compatible where they meet, but it supplies no data model and does not address multilingual vocabularies, which is why serious multilingual work cites the ISO standard.

Thesaurus tradition, SKOS construct, and the OWL thing it is not
Thesaurus term SKOS construct What it does NOT mean Why the difference bites
USE / preferred term skos:prefLabel Not the concept’s identity; the URI is Renaming a term must not break tagged content
UF / used for skos:altLabel Not owl:sameAs between two concepts Synonymy is a naming fact, not an identity claim
BT / broader term skos:broader Not rdfs:subClassOf, not part-of, not transitive Mechanical conversion to subClassOf imports false axioms
RT / related term skos:related Not usable alongside a hierarchical path (S27) The commonest integrity violation in published vocabularies
Node label / facet indicator skos:Collection Not a concept; cannot be broader than anything Grouping headings must never become taggable terms
Notation / class number skos:notation, typed literal Not a label; never displayed as a name One concept can carry several code systems unambiguously
Scope note skos:scopeNote Not decoration Without it, concept meaning drifts under a stable URI

What breaks · converting skos:broader to rdfs:subClassOf

A perfectly reasonable-sounding integration task: the equipment scheme is “just a taxonomy”, the ontology needs a class hierarchy, so a script walks the scheme and emits a subclass axiom per broader link. It runs clean. Nothing errors. Then the reasoner from lesson 6 starts returning nonsense, and the shape of the nonsense is instructive.

# In the scheme, an editor filed the platform under its region.
ex:Aberdeen2       skos:broader ex:NorthSeaAssets .
# And filed a pump casing under the pump, because that is where a user looks.
ex:PumpCasing      skos:broader ex:CentrifugalPump .

# The script emits:
ex:Aberdeen2  rdfs:subClassOf ex:NorthSeaAssets .   # instance treated as class
ex:PumpCasing rdfs:subClassOf ex:CentrifugalPump .  # part treated as subtype

The second axiom is the expensive one. It now entails that every pump casing is a centrifugal pump. Lesson 6 declared ex:Pump disjoint with several other equipment classes, so the moment a casing is also typed as a component the ontology goes inconsistent and the reasoner stops classifying anything at all, which is how a whole-part link filed for user convenience takes down the pipeline. The first axiom is quieter and worse: ex:Aberdeen2 is a particular platform, so it has been silently promoted from an individual to a class, and the pump count query that filters by platform now returns zero with no error.

The defect is not the script, it is the premise. skos:broader is one relation covering generic, partitive and instance links precisely because the thesaurus tradition never needed to distinguish them, so the information required to convert it correctly does not exist in the source. There are only three honest remedies: keep the scheme as SKOS and relate it to the ontology with mapping properties rather than subsumption; convert manually, deciding link by link what each one meant, which is a modelling project and not a migration; or, where the vocabulary really is a clean generic hierarchy, prove that first with a reasoner over a sample rather than assuming it. Lesson 9 works through which of the three you should want.

Check yourself

An editor wants a SPARQL query to return a concept and everything beneath it, at any depth, in a published SKOS scheme. Which is the correct thing to rely on?

  1. skos:narrowerTransitive, because skos:narrower is a sub-property of it and the closure is what you are asking for
  2. skos:narrower, because it is transitive
  3. rdfs:subClassOf, after converting the scheme
  4. skos:related, because it collects everything associated with the concept
Show the answer, and why each wrong option is wrong

A is correct. Condition S22 makes skos:narrower a sub-property of skos:narrowerTransitive, and it is the transitive property that carries the closure. Asking for it is how SKOS lets you opt into transitivity deliberately. In practice you either run a store that materialises the sub-property entailment, or you write the closure directly as a property path over skos:narrower; what you must not do is assume the closure is already there.

B is the answer most people give and the specification contradicts it in as many words: skos:broader and skos:narrower are not transitive properties. This is a deliberate design choice, not an oversight, because thesaurus hierarchies mix relation types and chaining them composes claims no editor made. C imports exactly the false axioms the block above dissects, and swaps a retrieval question for a logical one you did not need to ask. D is not merely weaker but formally excluded: S27 makes skos:related disjoint with skos:broaderTransitive, so a well-formed vocabulary guarantees that a related concept is never in the hierarchical closure. The two relations partition the space rather than overlapping.

Further reading

  • Miles, A. and Bechhofer, S., eds. (2009). SKOS Simple Knowledge Organization System Reference. W3C Recommendation, 18 August 2009. The integrity conditions S1 to S46 are the part to read twice. https://www.w3.org/TR/skos-reference/
  • Isaac, A. and Summers, E., eds. (2009). SKOS Simple Knowledge Organization System Primer. W3C Group Note. The gentler entry point, with worked vocabulary fragments. https://www.w3.org/TR/skos-primer/
  • Baker, T., Bechhofer, S., Isaac, A., Miles, A., Schreiber, G. and Summers, E. (2013). Key Choices in the Design of Simple Knowledge Organization System (SKOS). Journal of Web Semantics. Why the language is weak on purpose, written by the people who made it weak. https://arxiv.org/abs/1302.1224
  • ISO 25964 maintenance pages at NISO: the ISO 25964-1 data model, the exchange schema, and the ISO 25964 to SKOS, SKOS-XL and MADS correspondence of 11 December 2013. https://www.niso.org/schemas/iso25964
  • The iso-thes RDF schema, the machine-readable form of that correspondence, for thesaurus arrays and concept groups. http://purl.org/iso25964/skos-thes
  • Zeng, M.L. and Mayr, P. (2018). Knowledge Organization Systems in the Semantic Web: a multi-dimensional review. International Journal on Digital Libraries. Useful map of what else exists beside SKOS. https://arxiv.org/abs/1801.04479

Apply it to your work