ポケモンBotでGo - Botからグラフデータベース (Grakn.ai) に問い合わせてみる(クエリ編)


はじめに

Grakn.aiは2015に発表されたグラフデータベースの一種で、オブジェクト指向的オントロジーを併せ持つ一風変わったグラフである。データ格納にはTinkerPopを通してグラフデータベースを使用するが、Grakn独自のオントロジー層によってKR(知識表現)やFSMのモデリング、ビヘイビア等々、通常それなりに特殊なドメインナレッジを必要とするモデリングも比較的容易に行うことが出来る。今回はまず単純にボットからGraknまでの問い合わせを試そうと思う。(Graknについて興味のある方はこちらをどうぞ。)

今回のお題

テストデータ

Graknには様々なサンプルが含まれているが、中でも一番とっつきやすいポケモンのモデルを使うことにする。このモデルには151体のポケモンに対する情報が格納されているが、調べてみたところ全国ポケモン図鑑における第一世代を網羅している模様。GraknのポケモンデータはPokéapi - The RESTful Pokémon APIから抽出されているようだが、如何せん日本語化対応がなされていないのでそのままでは使えない。

今回は合わせて日本語化パッチも準備することとする。データには各ポケモンの図鑑番号と呼ばれるID(英語ではPokedex Numberと言うらしい)も格納されており、これがマッチしているので日本語のポケモン名へのマッピングはポケモンのドメイン知識が無くても可能なようなので私にも出来そう。(ポケモンデータの出所は:Grakn.ai - Pokemon Example。ポケモンサンプルデータの説明はこちら。)

推論の元となるInference Rule(詳しくはこちら)もこのサンプルデータでは用意されていないので、今回はテストデータの日本語化パッチと合わせて足すこととする。

環境

クライアントはLINE、NLPはDialogflowを採用し直結。今回のサービスはDialogflowへのwebhookとしてNodeで作成する。webhookはHeroku、検索対象となるGraknのインスタンスはAzureに用意したVMにてホスティングする。

各種ライブラリバージョン

  • Grakn:0.17.1
  • Node:6.10.1
  • express:4.16.2
  • body-parser:1.18.2
  • socket.io:2.0.4

Step 1:テストデータの投入

Pokemonエンティティにはname、pokedex-no、height、weight、descriptionが用意されている。まずはexampleフォルダに既に含まれているポケモンデータを投入:

grakn/graql console -f grakn/example/pokemon.gql -k pokemon

今回は新たに日本語用の名前と概要のフィールドを追加する。また、ポケモンの属性を司るpokemmon-typeエンティティにも同様のフィールドを追加する。

define
  #name attribute for Japanese
  name_jp sub attribute datatype string;

  #adding Japanese name to pokemon and pokemnon_type
  pokemon has name_jp;
  pokemon-type has name_jp;

属性の新規登録は他のサンプルにもあるものと同じ構文。既存オントロジーに対して属性を追加する場合、そのまま「エンティティ has 新規属性」で良い。色々試行錯誤したが結局一番シンプルな構文が有効だった。

次にデータ。こちらはmatch構文の後にinsertを足す事により設定が可能。GraknではInsert/Updateでは常にinsertを命令語として使い、また新規データの場合にはinsert後に複数のエンティティやリレーションシップの定義を並べるとバルクインサートされる。今回はアップデートなので各命令文にinsertを含んで個別のトランザクションとした。

define
## Pokemon types
match $type_normal isa pokemon-type, has name "normal"; insert $type_normal has name_jp "ノーマル";
match $type_fighting isa pokemon-type, has name "fighting"; insert $type_fighting has name_jp "かくとう";
match $type_flying isa pokemon-type, has name "flying"; insert $type_flying has name_jp "ひこう";
match $type_poison isa pokemon-type, has name "poison"; insert $type_poison has name_jp "どく";
...

## Pokemons
match $pokemon1 isa pokemon has pokedex-no = 1; insert $pokemon1 has name_jp "フシギダネ";
match $pokemon2 isa pokemon has pokedex-no = 2; insert $pokemon2 has name_jp "フシギソウ";
match $pokemon3 isa pokemon has pokedex-no = 3; insert $pokemon3 has name_jp "フシギバナ";
match $pokemon4 isa pokemon has pokedex-no = 4; insert $pokemon4 has name_jp "ヒトカゲ";
...

以下のようになればOK。

ちなみに今回用意したポケモンサンプルのパッチファイルはこちらに格納した(pokemon_jp_patch.gql)ので宜しければどうぞ。

Step 2:クエリ(Graql)を書く

このボットにはポケモン博士になって欲しいんだが、如何せん含まれている情報の種類に限りがあるのでそこまで高望みは出来ない。今回は以下のシナリオに絞ってみる:

  • ポケモンの身長/体重を答える
  • 特定の弱点(2つ指定)をもつポケモンを挙げる。
  • ポケモンの進化について答える

さほど素晴らしい事は教えてくれないが、とりあえずいくつかのクエリパターンを見る機会にはなるかと思う。

クエリ#1: 身長と体重

クエリ的にはポケモンの名前を元に属性を引っ張ってくるだけなので、最も簡単なクエリとなる。

match
$pokemon isa pokemon has name_jp "ピカチュウ";
get;

1行目がクエリの種類、最後の行が取得する項目、その間が条件となる。

クエリ#2: 弱点からポケモンを特定

先程はpokemonエンティティ単体へのクエリだったが、属性はpokemon_typeエンティティにあたるので、今回はリレーショナルモデルでいうところのJOINが必要になる。

サンプルデータを確認してみると、得意/弱点はsueper-effectiveというリレーションシップで定義されており、これはpokemon-type同士を繋げている。よって、ポケモンからpokemon-typeを繋げ、そのpokemon-typeがdefending-typeとなるpokemon-typeを取ってくる必要がある。

match
$pokemon isa pokemon;
$pokemon-type isa pokemon-type;
$defend1 isa pokemon-type has name_jp "でんき";
$defend2 isa pokemon-type has name_jp "くさ";
(pokemon-with-type:$pokemon, type-of-pokemon:$pokemon-type) isa has-type;
(attacking-type:$defend1, defending-type:$pokemon-type) isa super-effective;
(attacking-type:$defend2, defending-type:$pokemon-type) isa super-effective;
get $pokemon;

中間の条件は順不同で、AND条件として捉えられるので列記すれば良。「リレーションシップの観点からクエリを書く」という点さえしっくりくれば、graqlはすぐ書けるようになる。

クエリ#3: 進化を追う

進化はevolutionというリレーションシップで定義されており、pokemonエンティティ同士を繋げている。試しにクエリを実行してみると:

match
$pokemon isa pokemon has name_jp "フシギダネ";
$descendant isa pokemon;
(ancestor: $pokemon, descendant: $descendant) isa evolution;
get;


むむ、フシギソウしか取れない。フシギソウはさらに進化するとフシギバナになるので、フシギバナも進化形態だと人間は結論付けれる。しかしながら進化は個別のポケモン同士を繋げるリレーションシップとして定義されているので、DBとしてはこの推論が立てれない。クエリでも書けるが、ここはGraknの強力な機能であるInference(推論/演繹)を使って、この理解(知識)をオントロジーに組み込んでみる。

クエリ#3.1: - フシギダネからフシギバナを見つける

Inferenceはグラフで直接定義されていないリレーションシップを条件を元に特定する機能であり、この条件はオントロジーに定義する。もう少し具体的にGrakn.ai - 定義していない暗黙の関連性をグラフから導き出す。Grakn流の知識表現(Knowledge Representation & Reasoning)にも書いたが、人間なら分かる関連性の導き方を指定するので、Inferenceによって同じクエリでも異なる結果を抽出する事が出来る。

Inferenceはオントロジーに定義するで、defineキーワードをもって指定する:

define
  induced-genealogy sub rule,
    when {
      (ancestor: $pokemon1, descendant: $pokemon2) isa evolution;
      (ancestor: $pokemon2, descendant: $pokemon3) isa evolution;
    } then {
      (ancestor: $pokemon1, descendant: $pokemon3) isa evolution;
    };

何となく読めると思うが、「おじいちゃんとおとうさんが先祖/子孫の関係にあり、おとうさんとこどもが先祖/子孫の関係にある場合、おじいちゃんとこどもも先祖/子孫の関係にある」この定義1つで、どれだけ長いつながりでも、どれだけ入り組んだツリーでも、対象エンティティは全て抽出できる。

上記を実行後、先程と同じクエリを実行すると:

今度はフシギバナもフシギソウ同様にフシギダネの進化形態である事が分かる。Inference Ruleで導き出せるのは「明示的に定義してない関連性」であり、つまりリレーションシップしか定義出来ない。

最後に

今回はポケモンBot構築の前準備として、Graknのパッケージにてサンプルとして提供されているポケモンデータに対する日本語化と使用するクエリ(graql)の準備をしながら、Graknのクエリの扱い方を紐解いてみた。

次回は今回の準備を元に、実際にLINEからDialogflowに問いかけを送り、webhookを通してNodeのプログラムからGraknのREST APIにクエリを投げる、一連の流れについて説明したい。このあたりは他のボットとあまり変わり映えしない情報なので、インテグレーションのサンプルとしてご一読頂ければ幸いである。