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.ttldata.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 NodeResult PathMessageの3つだけ読めれば十分です。

Severity: sh:Violationという行にも触れておきます。SHACLの制約には、重大度(severity)を指定できます。既定ではsh:Violation(違反、ConformsをFalseにする)ですが、sh:Warningsh: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 NodeResult PathMessageを読めば、何が問題なのかが分かります。

ここまで読めば、突然出てくるSHACLの例も、もう初見のコードではなくなっているはずです。ぜひ戻って読んでみてください。

こちらからは以上です。