SADIを使ってSW的なウェブサービスを構築する

SADIはSemantic Automated Discovery and Integrationの略で、セマンティックウェブ的な枠組みを利用し、オンラインでアクセス可能なウェブサービスやデータベース(オンライン資源)を統一した方法で活用出来るように提案されているサービス提供形式の一つです。具体的にいうと、オンライン資源へのアクセスはHTTP、入出力のデータフォーマットはRDF、入出力の型の定義はOWLで、という具合です。SADIに沿って構築されたオンライン資源のエンドポイントにHTTP GETすると、入出力の型や処理内容がRDF形式で得られ、処理させたいデータを同エンドポイントにHTTP POSTすると、実際の処理が行われ、結果が得られます。また、入力データと出力データの関係が陽に分かるように、両者のRDFにおける主語は同じでなければならない、という規定があります。つまり、例えば「山本の身長と体重はそれぞれ170cm、60Kgである」というRDFを入力データとしてBMI (Body mass index)計算サービスエンドポイント(ここでは仮に http://example.org/getBMI とします)に与えると、「山本のBMIは20.76である」、「山本の体重クラスは標準である」というRDFが出力として得られるということを述べています。この場合、いずれも主語は山本になります。

今回はSADIのPerlモジュールを使い、略語を与えると対応する展開形のリストが得られるAllieサービスを試しにSADI対応させてみたので、その際に得られた知識をここに記します。なお、SADIに対応させるために入出力データの型をOWLで定義しますが、そのためにはオントロジーエディタのProtegeを使うと便利です。というのも、Protege用のSADIプラグインが配布されていて、サービスをSADI対応させる際に必要な作業を支援する機能が利用出来るからです。例えば、ブラウザなどを使うことなく、構築したエンドポイントを簡単に試せる機能が提供されています。本プラグインの利用方法についてはこちらを参考にしてください。今回は本ページからリンクが張られているこちらを参考にして作業しました。

SADIのPerlモジュールをインストール

さて、今回インストールしたPerlモジュールはSADI-1.13で、これをLinuxマシンのPerl(5.10.1)環境で実行しました。上記モジュールのページに書かれている通り、インストールが問題なく行われたことを確認したら、まずはsadi-install.plを実行します。これにより開発に必要なディレクトリ構成が作られます。具体的には下記のスライドの3ページに書かれているように、Perl-SADIというディレクトリをトップに幾つかのディレクトリがその下に属する構成になります。

提供サービスについて関連ファイルを生成

続いて、これからSADIに対応させるサービス名を決め、そのために必要なファイルを生成します。今回はAllieGetLongformとしました。

sadi-generate-services.pl -D AllieGetLongform

と実行します。この結果、Perl-SADIディレクトリの下にあるdefinitionsディレクトリにAllieGetLongformというファイルが作られ、この時点で機械的に定められる事項が書き込まれます。definitionsにあるファイルは、ユーザがHTTP GETを行った際に得られるサービスの概要(入出力データの型等)が記述されており、適宜編集する必要があります。プレインテキストファイル形式で、実際に開いてみると、その内容は理解しやすいので直ぐに編集出来ると思います。また、上述のProtege SADIプラグインを用いて当該ファイルを生成することも出来ます。SADIプラグインの「SADI Code Generator」がそれで、 先述のPerlプログラムを使わずにファイルを生成できます。

具体的な処理をコーディング

次に必要な作業は、提供するサービスが余り複雑なオントロジーを使わないならば、

sadi-generate-services.pl AllieGetLongform

を実行します。これにより、同じくPerl-SADI以下のcgiおよびservices/Serviceディレクトリにそれぞれ、AllieGetLongform、AllieGetLongform.pmというファイルが自動生成されます。cgiディレクトリ以下に作られるファイルは、その名前から想像出来る通り、ユーザからのリクエスト(GET / POST)がApacheなどのhttpサーバーに届いたときに最初に実行されるファイルが生成されます。ここにあるファイルを修正する必要はありません。

そして、services/Services/AllieGetLongform.pmは実際にリクエストを処理する実行ファイルで、これを適宜編集していくことになります。このファイルも、既に最低限のコードは書き込まれているので、あとはprocess_it関数内の入力データを処理して出力する、サービスの本質的な部分のコードを書くだけになっています。最初からサンプルとして書かれている以下のコードはRDFトリプルを3個生成するもので、全て主語が$input->getURIとなっていることから、それが入力データの主語と一致していることが分かります。(ここでは、node、predicate、valueがそれぞれ主語、述語、目的語にあたります。)

  foreach my $output (0..2) {
    $core->addOutputData(
        node => $input->getURI,
        value => "$output",
        predicate => "http://sadiframework.org/ontologies/predicates.owl#somePredicate$output"
    );
  }

ここを適宜、自身の提供したいデータの出力に合わせて修正していくことになります。

よりセマンティックウェブ (SW) 的なアプローチ

さて、もう一つのアプローチ。これがまさにセマンティックウェブっぽい面だと思いますが、自身の提供したいサービスが扱う情報(あるいは知識)についてOWLでその構造(オントロジー, Ontology)を予め用意しておけば、それを基にしたPerlのモジュールが自動生成され、SADIに対応させる際に書かなくてはならないコードを必要最小限にすることが出来るようになります。すなわち、OWLで定義されているクラスやプロパティについて、それぞれが一つのPerlモジュールとして生成されます。これらのモジュールはOWL2Perlモジュール群で定義されている各オブジェクトクラスについて、その内容に合わせてそれぞれ継承した形で生成されます。

メタボオントロジー概要図

図1:肥満度判定サービスに関するオントロジー

先のBMIの例で説明します(図1)。まず、そのOWLには「人」というクラスがありそうです。「人」クラスの持つべきプロパティ(すなわち、述語として使われる概念)としては、「の体重は」、「の身長は」、「のBMIは」が要りそうです。更に得られたBMIに基づき、「体重クラス」が決められ、そのインスタンスとして「低体重」、「標準」、「標準以上」、「肥満」のいずれか一つが該当することになります (WikiPedia)。従って「人」クラスの持つべきプロパティに「の体重クラスは」も加わります。

さて、ここまで特に問題無さそうに感じられますが、実はSADIでは結果の属するクラスは一つしか定義出来ません。ですから、以前書いていた本記述には誤りがありました。SADIにおいて、出力データの型は、入力データに対して提供サービスが新たに追加する属性を持たなくてはならないと規定されたクラスです。ですから、「の体重は」と「の身長は」クラスが必須の「人」クラスの下位概念として「のBMIは」と「の体重クラスは」プロパティも必須としたものを必要とします。今回はそれらを分かり易いようにひとまとめとするために、「体重クラス」クラスの上位概念に「肥満度判定結果」クラスを用意し、その「人」クラスの下位概念が持つべきプロパティを敢えて「のBMIは」と「の体重クラスは」ではなくて、「の肥満度は」とすることにします。そして、両者は「肥満度判定結果」クラスの持つべきプロパティとしましょう。つまり、二つのプロパティ「のBMIは」と「の体重クラスは」の主語を「人」クラスから「肥満度判定結果」クラスに変更したことになります。更に、当該下位概念クラスを「肥満度情報を持つ人」とします。

また、最近はペットもメタボになるという話もあるので、「犬」「猫」 クラスも用意しておきましょう。そして、「人」「犬」「猫」みなメタボが問題になっているということで「メタボ傾向生物」という上位クラスを用意してみます。ここで「メタボ傾向生物」が持たなくてはならないプロパティを「の体重は」および「の身長は」それぞれ一つとし、「の肥満度は」を一つ持てるとします(もちろん現在のBMIは人専用ですが、それはスルーして)。更に、「の肥満度は」プロパティを必須とする下位概念「肥満度情報を持つメタボ傾向生物」クラスを用意します。それぞれ上位クラスで持たなくてはならないプロパティが定義されているので、「人」「犬」「猫」あるいは「肥満度情報を持つ人」「肥満度情報を持つ犬」「肥満度情報を持つ猫」クラスの持つプロパティについて改めて定義する必要はなく、上位クラスの制限が継承されます。このとき、先のOWL2Perlで生成されるのは、OWL::Data::OWL::Classを継承した「メタボ傾向生物」「肥満度情報を持つメタボ傾向生物」「人」「犬」「猫」「肥満度情報を持つ人」「肥満度情報を持つ犬」「肥満度情報を持つ猫」「肥満度判定結果」「体重クラス」の10個、OWL::Data::OWL::DatatypePropertyを継承した「の体重は」「の身長は」「のBMIは」の3個、そしてOWL::Data::OWL::ObjectPropertyを継承した「の肥満度は」「の体重クラスは」の2つ、計15のモジュールになります。なお、DatatypePropertyには目的語がリテラル(数値や具体的な文字列など、URIではないもの)になるプロパティ(述語)が含まれ、ObjectPropertyには目的語がURIとなるプロパティが含まれます。また、現バージョンでは、特定のクラスに属すべきインスタンスを規程するモジュールは生成されないようです。

このとき、getBMIサービスが規定する入出力データの型は、それぞれ、「人」クラスと、「肥満度判定結果」「肥満度情報を持つ人」クラスになります。すなわち「人」クラスのインスタンスを取得して「の肥満度は」プロパティおよび、「肥満度判定結果」クラスに属するその目的語に対して「のBMIは」と「の体重クラスは」プロパティを更に追加した「肥満度情報を持つ人」クラスのインスタンスを主語とするRDFを返すことになります。以上の規程(オントロジー)はProtegeを用いて作成でき、適切なOWLファイルを生成できます。なお、Protegeの使い方についてはここでは割愛しますがここにまとめましたので参考にしてみてください。他にも、日本語の参考ページとしてここがあります。また、書籍としては、筆者も監訳として携わっている、「セマンティックWeb プログラミング」(オライリー)に簡単な解説があります。さて、生成されたファイルをmetabo.owlとするとき、以下のコマンドを実行することでPerl-SADI/generated以下に必要なモジュール群が生成されます。

sadi-generate-datatypes.pl -u file:/metabo.owl

ここでファイルは絶対パスで指定する必要があることに注意です。また、OWLファイルがオンライン上にあることも多いですが、その場合は、file:以下の部分をhttp://…とします。ProtegeのSADIプラグイン(SADI OWL 2 Code Generator)でも生成可能としているようですが、筆者の環境では動作しませんでした。

実際のAllieについてはhttp://purl.org/allie/ontology/201108にアクセスすることでOWLファイルが得られます。このアプローチのメリットはサービスを提供するデータについて予め構造や関係を機械可読な形式で用意しておくことで、そのサービスの特性や目的を再認識出来ることです。メタボの例をみるとイメージが掴み易いかもしれませんが、この作業をしていく中で、入力と出力の要件やサービスの提供する付加価値は何かを考えさせられます。そして実際にPerlでサービスの本質的な処理をコーディングする際にも見通し良く作業を進められます。Allieの場合は、略語と展開形のペアクラスに属する略語を入力として受け取り、対応する展開形のリストを返しますが、その処理を行うコードを直感的に書くことが出来ます。

Allieの具体例はやや複雑なので、再びメタボの例に戻ります。各クラスやプロパティはそれぞれURIとして表現されるわけですが、そのプレフィックスを仮にhttp://example.org/BMIとすると、http://example.org/BMI#人http://example.org/BMI#のBMIは、などとなります。そしてsadi-generate-datatypes.plを用いてモジュールを生成させると、generatedディレクトリの下に、example/org/BMI/人.pmやexampel/org/BMI/のBMIは.pmといったファイルが生成されます(現バージョンではUTF-8未対応なので、実際には仮名漢字まじりのOWLファイルは処理出来ません。バグレポート済です)。そしてservices/Service/getBMI.pmの中で、「人」クラスの「山本」インスタンスに付けられているプロパティ「の体重は」に対応する目的語を取得するには、次のようなコードを書きます。

$体重リスト = $core->getObjects(
  subject => $input->getURI,
  predicate => ($input->getNamespace)."の体重は",
);

ここで$input->getURIhttp://example.org/BMI#山本が得られます。そして、$体重リストには、以上の条件にマッチする目的語の配列への参照が代入されます。例では「山本の体重は60Kgである」なので、実際には要素数1で、その最初の要素に60が代入された配列への参照になります。身長についても同様に得られます。

続いて、「山本のBMIは20.76である」、「山本の体重クラスは標準である」という新たなプロパティを加えるために、次のようなコードを書きます。

require org::example::BMI::肥満度情報を持つ人;
require org::example::BMI::体重クラス;
require org::example::BMI::肥満度判定結果;

my $outClass = org::example::BMI::肥満度情報を持つ人->new(sprintf('%s', $input->getURI));
my $result = org::example::BMI::肥満度判定結果->new();
$outClass->add_の肥満度は($result);
$result->add_のBMIは(20.76);
$result->add_の体重クラスは(
  org::example::BMI::体重クラス->new("標準")
);

ここでのポイントは、require example::org::BMI::肥満度情報を持つ人およびrequire example::org::BMI::肥満度判定結果すれば、$outClass->add_の肥満度は$result->add_の体重クラスはというメソッドが所与になることで、本アプローチを選択してコーディングする際の利点になります。最後に次のコードを書くことで利用者に結果が返されます。

$core->addOutputData(node => $outClass);

サービスのテスト
ひとたびコーディングが終われば、デプロイとなるわけですが、その前に動作確認を簡単にすることが出来るようになっています。入力データとして下記のデータをyamamoto.xmlとして保存します。

<rdf:RDF xmlns="http://example.org/BMI#"
  xml:base="http://example.org/BMI#"
  xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
  xmlns:rdfs="http://www.w3.rog/2000/01/rdf-schema#"
  xmlns:owl="http://www.w3.org/2002/07/owl#>

<owl:NamedIndividual rdf:about="http://example.org/BMI#山本">
  <rdf:type rdf:resource="http://example.org/BMI#人"/>
  <rdfs:label xml:lang="ja">山本</rdfs:label>
  <の体重は>60</の体重は>
  <の身長は>170</の身長は>
</owl:NamedIndividual>
</rdf:RDF>

そして下記のコマンドを実行します。

sadi-testing-service.pl Service::getBMI yamamoto.xml

問題なくコードが書かれていれば次のような結果が得られることになります。

<rdf:RDF xmlns:a="http://example.org/BMI#"
 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">

<rdf:Description rdf:nodeID="r0">
<a:のBMIは>20.76</a:のBMIは>
<a:の体重クラスは rdf:resource="http://example.org/BMI#標準"/>
</rdf:Description>
<rdf:Description rdf:about="http://example.org/BMI#山本">
<a:の肥満度は rdf:nodeID="r0"/>
<rdf:type rdf:resource="http://example.org/BMI#肥満度情報を持つ人"/>
</rdf:Description>
</rdf:RDF>

デプロイ
動作確認が出来たらあとはデプロイですね。お使いのhttpサーバーでcgi実行可能なディレクトリ以下にシンボリックリンクでPerl-SADI/cgi/getBMIを置けば終わりです。デプロイ後の動作を確認したい場合は、同じくsadi-testing-service.plを利用して以下の通りにします。

sadi-testing-service.pl -e http://example.org/getBMI yamamoto.xml

すると、問題なければ以下のような結果が得られるはずです。 

HTTP/1.1 200 OK
Connection: close
Date: Fri, 12 Aug 2011 01:46:02 GMT
Server: Apache/2.2.14
Vary: Accept-Encoding,User-Agent
Content-Type: application/rdf+xml
Client-Date: Fri, 12 Aug 2011 01:46:04 GMT
Client-Peer: 172.0.0.1:80
Client-Response-Num: 1
Client-Transfer-Encoding: chunked

<rdf:RDF xmlns:a="http://example.org/BMI#"
 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<rdf:Description rdf:nodeID="r0">
<a:のBMIは>20.76</a:のBMIは>
<a:の体重クラスは rdf:resource="http://example.org/BMI#標準"/>
</rdf:Description>
<rdf:Description rdf:about="http://example.org/BMI#山本">
<a:の肥満度は rdf:nodeID="r0"/>
<rdf:type rdf:resource="http://example.org/BMI#肥満度情報を持つ人"/>
</rdf:Description>
</rdf:RDF>

もちろん、wget や curl コマンドを利用しても問題ありません。また、ProtegeのSADIプラグインからも試すことが出来ます。この場合は、「SADI Simple Client」タブを選択します。プラグインを使う場合は入力データを、編集中のOWLデータの中からクリックするだけで指定出来るのでデバッグ作業がし易くなっています。

SADIの仕様通り、入力データを与えずに、同じアクセスポイントに対してHTTP GETによりアクセスすると、Perl-SADI/definitions/getBMIに書かれている内容に従い、入出力の型やサービスの概要が得られます。その一部を抜粋します。

<rdf:RDF>
  <rdf:Description rdf:about="http://example.org/getBMI">
    <rdf:type rdf:resource="http://www.mygrid.org.uk/mygrid-moby-service#serviceDescription"/>
    <b:format>sadi</b:format>
    <b:identifier>http://example.org/getBMI</b:identifier>
    <a:locationURI>http://example.org/getBMI</a:locationURI>
    <a:hasServiceDescriptionText>体重と身長からBMIを計算し、それに基づいて肥満度を得ます</a:hasServiceDescriptionText>
    <a:hasServiceDescriptionLocation>http://example.org/getBMI</a:hasServiceDescriptionLocation>
    <a:hasServiceNameText>getBMI</a:hasServiceNameText>
    <a:hasOperation>
        <rdf:Description rdf:about="getBMI_authority.for.getBMI_1">
            <a:hasOperationNameText>getBMI</a:hasOperationNameText>
            <rdf:type rdf:resource="http://www.mygrid.org.uk/mygrid-moby-service#operation"/>
            <a:performsTask>
                <rdf:Description rdf:about="getBMI_authority.for.getBMI_2">
                    <rdf:type rdf:resource="http://www.mygrid.org.uk/mygrid-moby-service#operationTask"/>
                    <rdf:type rdf:resource="http://someontology.org/services/sometype"/>
                </rdf:Description>
            </a:performsTask>
            <a:inputParameter>
                <rdf:Description rdf:about="getBMI_authority.for.getBMI_3">
                    <rdf:type rdf:resource="http://www.mygrid.org.uk/mygrid-moby-service#parameter"/>
                    <a:objectType>
                        <rdf:Description rdf:about="http://example.org/BMI#人"/>
                    </a:objectType>
                </rdf:Description>
            </a:inputParameter>
            <a:outputParameter>
                <rdf:Description rdf:about="getBMI_authority.for.getBMI_4">
                    <rdf:type rdf:resource="http://www.mygrid.org.uk/mygrid-moby-service#parameter"/>
                    <a:objectType>
                        <rdf:Description rdf:about="http://example.org/BMI#肥満度情報を持つ人"/>
                    </a:objectType>
                </rdf:Description>
            </a:outputParameter>
        </rdf:Description>
    </a:hasOperation>
  </rdf:Description>
</rdf:RDF>

<a:hasServiceDescriptionText>にサービス概要が、また、<a:inputParameter>や<a:outputParameter>に入出力データの型が規程されています。

サービスの登録
デプロイが問題なく出来たら、より広くサービスを利用してもらうために、SADI registryに登録しましょう!

以上です。