SHACLって結局何なの?実際にデータを検証してみて理解する
オントロジー関連の記事を読んでいると、いきなり「SHACL」というものが当たり前のように登場します。
「SPARQL」と違うの?なにそれ?って感じになったりしますよね。
たとえば、
saas:WorkspaceShape a sh:NodeShape ;
sh:targetClass saas:Workspace ;
sh:property [
sh:path saas:workspaceBelongsToTenant ;
sh:minCount 1 ;
sh:maxCount 1 ;
sh:class saas:Tenant ;
] .
こういうコードが、説明なしにいきなり出てきます。
しかし、「これは何をしているのか」「これはどうやって動かすのか」が分からないまま読み進めるのは、正直つらいと思います。ぼく自身も、最初にこれを見たとき、完全においていかれました。
この記事は、そのおいていかれた気持ちを解消するための記事です。SHACLだけに絞って、それが何であり、どうやって実際に手元で動かすのかまで説明します。
※この記事は、RDF・RDFS・OWL・Turtleについて説明済みの前提で書きます。まだの方は、先に RDF・RDFS・OWL・Turtleの違いを整理して、ファイルで理解するオントロジー入門 を読んでおいてください。そこで使った「山田」「ABC社」「鈴木」の例を、この記事でもそのまま使います。
1. SHACLとは何か
一言でいうと、SHACLは次のようなものです。
SHACL:RDFデータが期待する形になっているかを検証するための言語
正式名称はShapes Constraint Languageです。読み方は「シャクル」です。
別のブログで、次のように書きました。
OWLは強力だが、
データベースの入力チェックとは考え方が異なる。
OWLは一般にオープンワールド仮定を使うため、
「情報が書かれていない」ことと
「その情報が存在しない」ことを区別できない。
つまり、OWLだけでは、次のような検証ができません。
employeeIdは必須であるemployeeIdは必ず1つだけである- 年齢は0以上である
これらを検証するために使われるのが、SHACLです。
役割を並べると、次のようになります。
RDF
情報をトリプルとして表す
RDFS・OWL
データが何を意味するかを定義する
SHACL
データが期待する形になっているかを検証する
RDFS・OWLが「意味」を定義するのに対して、SHACLは「形」を検証します。
2. SHACLも、実はRDFで書かれている
これは、最初に知っておくと理解が早くなる点です。
SHACLの制約(これをShapeと呼びます)自体も、RDFのトリプルとして表現されます。つまり、Turtleで書けます。
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix ex: <https://example.com/company#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
sh:という接頭辞は、SHACLの語彙を指す名前空間です。ex:と、扱いは何も変わりません。
sh:NodeShape
sh:targetClass
sh:property
これらは、SHACLという名前空間に定義された、クラスやプロパティです。RDFの外側にある特別な魔法の構文ではありません。
3. 最初のShapeを書く
「Employeeには社員番号(employeeId)が必ず1つ必要」という制約を、Shapeとして書いてみます。
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix ex: <https://example.com/company#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
ex:EmployeeShape a sh:NodeShape ;
sh:targetClass ex:Employee ;
sh:property [
sh:path ex:employeeId ;
sh:minCount 1 ;
sh:maxCount 1 ;
sh:datatype xsd:string ;
sh:message "Employeeには社員番号が1つ必要です。"@ja ;
] .
1行ずつ見ていきます。
ex:EmployeeShape a sh:NodeShape ;
これは、「ex:EmployeeShapeという名前のShape(検証ルールのまとまり)を1つ作る」という宣言です。
sh:targetClass ex:Employee ;
これは、「このShapeは、ex:Employeeという型を持つリソースすべてに適用する」という指定です。
sh:property [ ... ]
これは、「対象リソースが持つ、あるプロパティについての制約」です。角括弧[ ]の中に、具体的な制約を書きます。
sh:path ex:employeeId ;
チェック対象のプロパティはex:employeeIdです。
sh:minCount 1 ;
sh:maxCount 1 ;
ex:employeeIdは、最低1個、最大1個、つまり必ず1個だけ必要です。
sh:datatype xsd:string ;
値の型は、文字列(xsd:string)でなければなりません。
sh:message "Employeeには社員番号が1つ必要です。"@ja ;
この制約に違反したときに表示するメッセージです。
ここまでで、sh:propertyの基本的な語彙は出そろいました。
sh:path :どのプロパティを見るか
sh:minCount :最低いくつ必要か
sh:maxCount :最大いくつまでか
sh:datatype :値の型は何か
sh:message :違反したときに何を表示するか
4. 値ではなくリソースを要求する場合
employeeIdは文字列でしたが、「Employeeは必ずOrganizationにworksForしている」という制約も書いてみます。今回の目的語は、文字列ではなく、Organizationという型を持つ別のリソースです。
ex:EmployeeShape a sh:NodeShape ;
sh:targetClass ex:Employee ;
sh:property [
sh:path ex:employeeId ;
sh:minCount 1 ;
sh:maxCount 1 ;
sh:datatype xsd:string ;
sh:message "Employeeには社員番号が1つ必要です。"@ja ;
] ;
sh:property [
sh:path ex:worksFor ;
sh:minCount 1 ;
sh:class ex:Organization ;
sh:message "Employeeは必ずOrganizationに勤務している必要があります。"@ja ;
] .
ここで使っているのはsh:datatypeではなくsh:classです。
sh:datatype ex:Organization ← 誤り。datatypeはリテラルの型を指定するもの
sh:class ex:Organization ← 正しい。classはリソースの型を指定するもの
値がリテラル(文字列・数値など)ならsh:datatype、値が別のリソース(IRIで識別されるもの)ならsh:classを使います。目的語がリテラルかリソースかという区別は、rdf-turtle.mdの10節で説明したrdf:resourceの話とまったく同じ区別です。
同じsh:propertyが、;でつながって2つ並んでいます。これはTurtleの記法で、「ex:EmployeeShapeは、この2つのsh:propertyを持つ」という意味です。
5. 実際に検証してみる
ここまでは、Shapeの書き方の説明でした。ここからは、実際にこのShapeを使って、データを検証してみます。
5.1 準備
Pythonの実行環境があれば、次のコマンドだけで準備できます。
pip install pyshacl
pyshaclは、SHACLをPythonで実装した、コマンドラインから使えるツールです。RDF/SPARQLの実装で知られるRDFLibプロジェクトの一部として公開されています。
インストールが終わると、pyshaclというコマンドが使えるようになります。
5.2 Shapeファイルを保存する
先ほどのex:EmployeeShapeを、shapes.ttlというファイルに保存します。
# shapes.ttl
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix ex: <https://example.com/company#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
ex:EmployeeShape a sh:NodeShape ;
sh:targetClass ex:Employee ;
sh:property [
sh:path ex:employeeId ;
sh:minCount 1 ;
sh:maxCount 1 ;
sh:datatype xsd:string ;
sh:message "Employeeには社員番号が1つ必要です。"@ja ;
] ;
sh:property [
sh:path ex:worksFor ;
sh:minCount 1 ;
sh:class ex:Organization ;
sh:message "Employeeは必ずOrganizationに勤務している必要があります。"@ja ;
] .
5.3 検証したいデータを保存する
テストデータとして、山田のデータを、data.ttlというファイルに保存します。
# data.ttl
@prefix ex: <https://example.com/company#> .
ex:yamada a ex:Employee ;
ex:employeeId "E001" ;
ex:worksFor ex:abc .
ex:abc a ex:Organization .
山田は、社員番号(E001)を持ち、ABC社(ex:abc、Organization型)に勤務しています。2つの制約を、どちらも満たしているはずです。
5.4 検証を実行する
同じフォルダにshapes.ttlとdata.ttlを置いた状態で、次のコマンドを実行します。
pyshacl -s shapes.ttl data.ttl
-sのあとにShapeファイル、そのあとに検証したいデータファイルを指定します。
結果は、次のように表示されます。
Validation Report
Conforms: True
Conforms: Trueは、「このデータは、すべてのShapeを満たしている」という意味です。実際に手元で動かして、この結果が出ることを確認してみてください。
6. 制約に違反するデータを検証してみる
次は、あえて制約に違反するデータを用意します。data.ttlの内容を、次のように書き換えます。
# data.ttl
@prefix ex: <https://example.com/company#> .
ex:suzuki a ex:Employee ;
ex:employeeId "E002" .
鈴木は、社員番号(E002)は持っていますが、ex:worksForが書かれていません。「Employeeは必ずOrganizationに勤務している」という制約に違反しているはずです。
同じコマンドを、もう一度実行します。
pyshacl -s shapes.ttl data.ttl
今度は、次のように表示されます。
Validation Report
Conforms: False
Results (1):
Constraint Violation in MinCountConstraintComponent (http://www.w3.org/ns/shacl#MinCountConstraintComponent):
Severity: sh:Violation
Source Shape: [ sh:class ex:Organization ; sh:message Literal("Employeeは必ずOrganizationに勤務している必要があります。", lang=ja) ; sh:minCount Literal("1", datatype=xsd:integer) ; sh:path ex:worksFor ]
Focus Node: ex:suzuki
Result Path: ex:worksFor
Message: Employeeは必ずOrganizationに勤務している必要があります。
Conforms: Falseになり、違反の内容が具体的に表示されました。
7. 違反レポートの読み方
出力が長く見えますが、押さえるべき点は3つだけです。
Focus Node :どのリソースが違反したか
Result Path :どのプロパティについての違反か
Message :なぜ違反なのか
今回の場合は、次のように読めます。
Focus Node : ex:suzuki
Result Path : ex:worksFor
Message : Employeeは必ずOrganizationに勤務している必要があります。
つまり、「ex:suzukiが、ex:worksForという点で、この理由により違反している」ということです。
Constraint Violation in MinCountConstraintComponentという行は、「どの種類のSHACL制約(今回は個数の制約)に違反したか」を表しています。これは内部的な分類名なので、最初は無視してかまいません。まずFocus Node・Result Path・Messageの3つだけ読めれば十分です。
Severity: sh:Violationという行にも触れておきます。SHACLの制約には、重大度(severity)を指定できます。既定ではsh:Violation(違反、ConformsをFalseにする)ですが、sh:Warningやsh:Infoを指定すると、「レポートには表示されるが、ConformsはFalseにならない」という、もう少し緩い検証を書けます。
8. もう少し複雑な検証について
ここまでで説明したsh:propertyは、「1つのリソースが持つ、1つのプロパティ」についての制約でした。
しかし、場合によっては次のような制約も出てきます。
TeamがアクセスするWorkspaceは、
Teamと同じTenantに所属していなければならない
これは、「1つのプロパティの値」だけでは判定できません。TeamのTenantと、WorkspaceのTenantという、2つの値を比較する必要があります。
こうした複雑な条件を書くために、SHACLにはsh:sparqlという仕組みがあります。名前のとおり、SPARQL(RDFに対する検索言語)を使って、制約を自由に書けます。
「SPARQL」ってなんなのよ!という方は、
まずこちらをざっと眺めてみてください!
saas:TeamWorkspaceTenantConsistencyShape
a sh:NodeShape ;
sh:targetClass saas:Team ;
sh:sparql [
a sh:SPARQLConstraint ;
sh:select """
SELECT $this
WHERE {
$this saas:canAccessWorkspace ?workspace .
...
FILTER (?teamTenant != ?workspaceTenant)
}
"""
] .
ここで大事なのは、次の1点だけです。
sh:property :1つのプロパティについての、単純な制約
sh:sparql :複数の値を比較するような、複雑な制約
sh:propertyで書けることには限界がありますが、その限界を超えたいときにsh:sparqlがある、ということだけ知っておけば、「ああ、複雑な条件を書くための機能を使っているんだな」となんとなく読めるはずです。
まとめ
SHACLとは
RDFデータが期待する形になっているかを検証する言語
sh:NodeShape / sh:targetClass
どのクラスに制約をかけるか
sh:property / sh:path
どのプロパティに、どんな制約をかけるか
sh:minCount / sh:maxCount
個数の制約
sh:datatype / sh:class
値がリテラルかリソースかによって使い分ける型の制約
sh:message
違反したときに表示する説明文
sh:sparql
sh:propertyでは書けない、複雑な制約を書くための仕組み
そして、実際に検証を動かす手順は、次の3ステップです。
1. pip install pyshacl
2. Shapeを shapes.ttl に、データを data.ttl に保存する
3. pyshacl -s shapes.ttl data.ttl を実行する
Conforms: Trueならデータは制約を満たしており、Conforms: Falseなら、Focus Node・Result Path・Messageを読めば、何が問題なのかが分かります。
ここまで読めば、突然出てくるSHACLの例も、もう初見のコードではなくなっているはずです。ぜひ戻って読んでみてください。
こちらからは以上です。