マロングラッセ Mk.II

赤西真論のブログらしきもの

im@sparqlを触ってみた

はじめに

この記事はSLP KBIT Advent Calendar 2019の6日目の記事です。

adventar.org

今年もアドベントカレンダーの時期がやってきました。ということで枠が空いていたので今年も記事を書きます。2016年からなんだかんだで4回目の記事ですね。大学のサークルのアドベントカレンダーに書くのはこれが最後ですが、会社でもアドベントカレンダーを書くかも?

今年の内容

なんだかんだ薄っぺらい内容しか今まで書いてないのですが、今年も薄っぺらいです。ただ、いつもよりはちょっと分厚いかも?

内容はタイトルからも分かる通り、im@sparqlを触ってみただけです。だけと言っても、ただクエリを実行するだけじゃ面白くないので、Webアプリケーションを作りましたよ。まあ、僕Webエンジニアになる予定なので...

im@sparqlとはなんぞや

sparql.crssnky.xyz

書いてある通り、アイマスのデータがまとまっているデータベースみたいなものです。正確にはRDFと呼ばれる形式で管理されており、検索にはSPARQLを利用します。

RDFとはなんぞや

まあ、そうなりますよね。これに関してはWikipedia読んで

ja.wikipedia.org

簡単に言えばデータを記述するための方法で主語、述語、目的語の3要素で関係を示した枠組み(フレームワークだからね)です。

Wikipediaにもある通り、グラフ構造で表現することが出来ます。もちろんim@sparql内のデータもグラフ表現出来ます。

SPARQLとはなんぞや

で、RDFのデータを検索するために使用する言語がSPARQLです。RDBを操作するときにSQL文を利用するみたいなものですね。SQLに似ているようで全然違うものです。ここを扱えるようになるのが一番大変だった。

とりあえず使ってみようぜ

まあ、実際に触ってみましょう。触っても理解するのに時間がかかるんだが...

実行環境を用意する必要はありません。先程のim@sparqlのトップページにTryと書かれた部分があります。そこのInput Queryに入力すれば実行出来ます。なお、Webアプリケーションなどから呼び出す場合も自前で環境を構築するわけではなく、ここのAPIというかURLにクエリを投げるだけです。簡単だね。

で、簡単な使い方はその下のim@sparql.docに載ってます。僕は最初全く気づかずにゴリゴリ自分で書いていました。ちゃんと見ましょう。まあ、その下のクエリの例はめっちゃ一生懸命読んでいたんですが...

てか、im@sparql.docに僕が最初に説明した内容に近いことが細かく書いてあります...

ただ、よく分からない部分もあったのでとりあえず下のクエリを実行してみましょう。

PREFIX schema: <http://schema.org/>
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX imas: <https://sparql.crssnky.xyz/imasrdf/URIs/imas-schema.ttl#>
SELECT DISTINCT ?名前 ?タイトル
WHERE {
  ?i rdf:type imas:Idol;
     schema:name ?名前;
     imas:Title ?タイトル.
     filter(regex(?タイトル,"765AS") || regex(?タイトル,"MillionStars"))
     filter(lang(?名前)="ja")
}

実行するとjson形式のデータが表示されたと思います。今回は765PROに所属しているアイドルの名前を出してみました。実行結果はまあ、見れば分かると思うのでクエリの説明をしていきます。

まず、1~3行目が名前空間エイリアスです。は?

ちゃんと説明します。ページ閉じないで。エイリアスと書いてある通り、例えば1行目であれば、http://schema.org/にschema:という名前を割り当てるという意味になります。これによってクエリ本体で短く書けるというわけです。なら、このURLはなんやねんってことになりますがこれは実際にはURLではなく、URIという扱いになります。指し示すデータの位置、DBにおけるカラム名みたいなものです。(正直説明出来るほど理解していない)

次に5行目。ここで表示するデータを指定しています。SELECTを使うのを見るとSQLっぽいですね。DISTINCTは重複を取り除くために指定しています。今回は別に無くても重複は起きないのですが、まあおまじないのように付けていても特に支障はありません。その後ろに?から始まる単語がありますが、これは変数です。この変数に次の行からのWHEREで取り出したデータが入ってきます。

次に6行目から12行目までが取り出すデータの条件指定です。ここの指定がすべて真(True)になるデータが取り出されます。

まず、7行目です。これはiという変数にrdf:typeがimas:Idolのデータを取り出しています。これが最初に出てきたRDFのトリプルです。グラフにすると下の感じになります。

この関係が真になるようなデータが取り出されます。?iは変数で現在は何も入っていないため、すべてのデータ郡です。そこからrdf:typeがimas:Idolであるデータのみを?iに格納します。

で、この文は最後が;で終わっています。ここが次で重要になります。

次に8行目です。おや、2つしかありません。schema:nameと?名前だけです。これではトリプルを表現出来ません。

ここで登場するのが7行目の;です。SPARQLでは、最後が;で終わっている場合は次の文の主語が自動的に前の文の主語と同じになります。したがって、?i schema:name ?名前と表現したことと同じになります。で、この文では?iのschema:nameを?名前の変数に格納しています。7行目でrdf:typeがimas:Idolと制限したため、rdf:typeがimas:Idolのデータのschema:nameを?名前に格納するということになります。

次に9行目です。ここも8行目と同じようにrdf:typeがimas:Idolでschema:nameが存在する(必ず真になるため、存在しないものは弾かれます。これが後から響いてきます。)データのimas:Titleを?タイトルという変数に格納するという意味になります。なお、この文は;ではなく、.で終了しています。.で終了した場合は次の文に主語が引き継がれません。

最後に10行目と11行目です。ここでは取り出したデータの条件を指定しています。トリプルだけでは取り除けない条件をこのfilterで指定します。今回は10行目で?タイトルが765ASもしくはMiillionStarsのデータのみとし、?名前は言語がjaのもののみとしています。フィルターの書き方も色々あるんですよね...

すると実行結果のような結果となります。

指定する述語と目的語分からなくね?

ですよね。僕も困りました。ですが、調べる方法があります。

1つはim@sparql.docに書いてある方法です。最初に主語だけを取り出します。

PREFIX schema: <http://schema.org/>
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX imas: <https://sparql.crssnky.xyz/imasrdf/URIs/imas-schema.ttl#>
SELECT ?千早の主語
WHERE {
  ?千早の主語 rdf:type imas:Idol;
             schema:name ?アイドル名.
  filter(contains(?アイドル名,"如月千早"))
}

これはim@s@parql.docに載っているクエリです。千早の主語を取り出します。

これを実行すると何やらURL(本当はURIだよ)だけが格納されている状態になると思います。これをコピーして別のタブなどで開いてみてください。

するとなんと便利なことでしょう。千早の情報がすべて出てきました。

必要なのはそこに載っている語彙と内容です。これは千早を主語として場合に指定できる述語と目的語です。はっきり言って目的語を指定するのは最初ぐらいで後は変数にすることが多いので語彙だけ知っていればいいはずです。また、そこに書いてあるのはURIなのでPREFIXに置き換えて利用する必要があります。

もう1つの方法はGitHubにあるRDFを自分で読んでしまうことです。僕は基本的にこちらを利用しました。何が入っているのがマジでよく分からなかったので直に見てしまおうってことです。ただ、実際のデータとは異なる部分があるらしいのでそこだけは注意する必要があります。

github.com

ここにあるRDFsが実態です。曲とか全然入ってないし、ライブも2013から2019に飛んでいたりとそこまで多い情報が取れるわけではないって感じがします。これ、コントリビュートするにしても大変だしなぁ

さて、ここまでで紹介は終わりです。次からは僕が実際に作ったものになります。

何を作ったのか

これです。

github.com

765PROのユニットを一覧で表示して、そのメンバーと各アイドルのカラーを表示するだけのものです。超簡単。サーバーなんて要らない。

はい、サーバーなんて用意していません。GitHub.ioで公開中です。

marron-akanishi.github.io

名前がMiraiなのは特に何も思いつかなかったからです。いつもなら作るアプリケーションに関係しそうなアイドルを選択するのですが、Arisaはもう使っていたので適当に選びました。

何がしんどいのか

まず、SPARQLが叩けない。これです。とりあえず、僕が色々試行錯誤の末に編み出したクエリの数々をご覧ください。

// 伊吹翼がいるユニットの一覧
PREFIX schema: <http://schema.org/>
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX imas: <https://sparql.crssnky.xyz/imasrdf/URIs/imas-schema.ttl#>
SELECT DISTINCT ?ユニット名 (group_concat(?メンバー名;separator=", ")as ?メンバー)
WHERE {
  ?i rdf:type imas:Idol;
     schema:name ?name;
     schema:memberOf ?u.
     filter(regex(?name,"伊吹翼"))
  ?u schema:name ?ユニット名;
     schema:member/schema:name ?メンバー名
     filter(lang(?メンバー名)="ja")
}group by (?ユニット名) order by(?ユニット名)
=======================================================================================
// 765PROのユニットの一覧(遅い)
PREFIX schema: <http://schema.org/>
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX imas: <https://sparql.crssnky.xyz/imasrdf/URIs/imas-schema.ttl#>
SELECT DISTINCT ?ユニット名 (group_concat(?メンバー名;separator=", ")as ?メンバー) (group_concat(?色;separator=", ")as ?カラー)
WHERE {
 ?i rdf:type imas:Idol;
    schema:name ?名前;
    imas:Title ?タイトル.
    filter(regex(?タイトル,"765AS") || regex(?タイトル,"MillionStars"))
    filter(lang(?名前)="ja")
 ?u rdf:type imas:Unit;
    schema:name ?ユニット名;
    schema:member ?m.
 ?m imas:Color ?色;
    schema:name? ?名前;
    schema:name ?メンバー名.
    filter(lang(?メンバー名)="ja")
}group by (?ユニット名) order by(?ユニット名)
======================================================================================
// 765PROのユニットの一覧(速い)
PREFIX schema: <http://schema.org/>
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX imas: <https://sparql.crssnky.xyz/imasrdf/URIs/imas-schema.ttl#>
SELECT DISTINCT ?ユニット名 (group_concat(?メンバー名;separator=", ")as ?メンバー) (group_concat(?色;separator=", ")as ?カラー)
WHERE {
 ?u rdf:type imas:Unit;
    schema:name ?ユニット名;
    schema:member/schema:name ?メンバー名.
    filter(lang(?メンバー名)="ja")
 { SELECT ?メンバー名 ?色
   WHERE{
    ?i rdf:type imas:Idol;
       schema:name ?メンバー名;
       imas:Color ?色;
       imas:Title ?タイトル.
       filter(regex(?タイトル,"765AS") || regex(?タイトル,"MillionStars"))
   }
 }
}group by (?ユニット名) order by(?ユニット名)
======================================================================================
// 765PROのユニットの一覧(ジュリア対応)
PREFIX schema: <http://schema.org/>
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX imas: <https://sparql.crssnky.xyz/imasrdf/URIs/imas-schema.ttl#>
SELECT DISTINCT ?ユニット名 (group_concat(?メンバー名;separator=", ")as ?メンバー) (group_concat(?色;separator=", ")as ?カラー)
WHERE {
  ?u rdf:type imas:Unit;
     schema:name ?ユニット名;
     schema:member/schema:name|schema:member/schema:alternateName ?メンバー名.
     filter(lang(?メンバー名)="ja")
  { SELECT ?メンバー名 ?色
    WHERE{
      {
       ?i rdf:type imas:Idol;
          schema:name ?メンバー名;
          imas:Color ?色;
          imas:Title ?タイトル.
          filter(regex(?タイトル,"765AS") || regex(?タイトル,"MillionStars"))
      }
      UNION
      {
       ?i rdf:type imas:Idol;
         schema:alternateName ?メンバー名;
         imas:Color ?色.
         filter(regex(?メンバー名,"ジュリア"))
      }
    }
   }
}group by (?ユニット名) order by(?ユニット名)

めんどくさいので4つ一気に載せました。各クエリの説明は最初に書いてあります。

まず条件指定の練習として伊吹翼がいるユニットだけを取り出すことにしました。それが1つ目のクエリです。ユニット名とメンバーを取り出すのは例として載っていますが、今回はその例とは別のアプローチを行っています。

imas:Idolにはすでにschema:memberOfとして所属しているユニットが一覧で取り出せます。そのユニットの情報をクエリ例のように取り出すだけです。ユニットの一覧から伊吹翼がメンバーとして入っているというアプローチも出来ますが、こっちのほうが綺麗なのでこのようにしています。最初に見るのをimas:Unitにするかimas:Idolにするかといった感じです。

では、実際に欲しい765PROのユニットだけを取り出すという処理を書いてみます。ユニットにどのタイトルのユニットかみたいな情報が入っていれば簡単なのですが、存在しないので765PROのアイドル全員を取り出して、そのアイドルが所属するユニットを取り出すという処理を行います。ついでに色が欲しいのでメンバーごとの色も一緒に取り出します。それで出来たのが2つ目の処理です。

やばいように見えますが、単純です。まず、765PROのアイドル全員を取り出します。ここは一番最初に実行したクエリと全く同じですね。次にユニットの一覧も取り出します。これはim@sparqlの例にあるクエリとほぼ同じです。ユニットメンバー(?m)から765プロのアイドルだけを探します。全部見たことあるかと思いきや、謎の文がありますね。

schema:name? ?name;です。なんで述語の最後に?が付いているのかと言いますと、こうすることによって?nameとschema:nameが一致するデータのみを取り出すことが出来ます。filterを?nameの内容で行っているみたいなものです。

これで取り出せます。ですが、実行すれば分かりますがめっちゃ遅いです。

原因は?mに対して?名前の比較を行っているからです。全条件に対して真になるものを探し出すため、アイドルとユニットの全組み合わせ分調べます。やばいですね。

ということで色々調べた結果出来たのが3つ目の処理です。WHEREの中にSELECTが入っています。なんだこれ????

これはサブクエリと呼ばれるものです。こうするとサブクエリ内とメインクエリ内とで一致する変数を自動的に括り付けてくれます。SQLのJOINですね。これは便利。

ということでやっていることは全ユニットのメンバーを出してそのメンバーと765PROのアイドルが結びつくようにしています。ここの条件がすべて真になるように出されるため、結びつかなかったユニットは除外されます。ちなみに真にならない、つまり結びつくことが無くても出したい場合はOPTIONALをサブクエリの前に付けます。

これを実行すると先程より早く処理が帰ってきます。これなら実用範囲でしょう。

さて、これで完成したように見えます。ですが、返ってきたデータを見るとなんか足りません。

ジュリアが足りない

これは大変です。アイドルが足りないのは大問題。

さて、なぜこんなことが起きるのか。ここでジュリアの語彙の一覧を見てみます。ですが、im@sparql.docの例だと出ません。なんで?

しょうがないのでRDF(765MillionStars.rdf)を読みます。RDFはID順なのでジュリアは最後から3番目に居ます。

はい、schema:nameが存在しません。そうです、ジュリアは本名では無いためalternateNameとして入っているのです。で、先程までのクエリではschema:nameしか指定していないため、schema:nameが存在しないジュリアは真と判定されずに除外されてしまいます。

おや?もうひとり本名ではない子が居ます。ロコです。ですが、ロコは本名である伴田路子がschema:nameに入っているため、取り出せたのです。

ということでジュリアだけ特別な処理を入れます。それが4つ目です。

サブクエリにジュリア専用のクエリが追加されています。2つのクエリをくっつけて1つの結果にするにはUNIONを使います。この歌声がUNION!!

さて、これでクエリは完成です。ロコに関しては後ほど置換処理をすれば良いでしょう。

Webアプリケーションにする

では、このクエリを利用してWebアプリケーションにします。ちなみにim@sparql.docには組み込み方まで書いてあります。まあ、この書き方は嫌なので1から書きました。

ソースコードGitHubに載っているのでそれを参考にしてください。Vue.jsを使おうと思ったんですけど、大したことないものなので使いませんでした。てか、使ってたら多分間に合いません。

ロコが伴田路子になっていたり、エミリーがエミリースチュアートになっていたりするのですが、それはJavaScriptのほうで置換処理を行っています。このぐらいならまあ簡単なのでSPARQLを修正するより早いです。

あと、クエリが長すぎてどうやって書こうか悩んだ挙げ句、im@sparqlのトップページにあるTryを実行したときのURLをそのまま貼り付けるという面白いことをしています。結局返ってくるのはjsonなのでこれで良いです。

あ、あとね、innerHTMLを直で弄ると対象となるHTMLを構築し直す影響でとんでもなく遅い処理になるのでinsertAdjacentHTMLを使ったほうがいいですね。これ、Takane作った時にも同じこと調べた気がするぞ。

そのほかのミリオンライブに関係するデータベース

im@sparqlは慣れると非常に簡単に情報を取り出せますが、まだまだ足りません。特に楽曲情報が壊滅的です。

なので、他のデータベース(主に楽曲)をいくつか上げておきます。

imas-db.jp

最大手ですね。アイマスDBさんです。ここを見れば何でも分かります。ただ、これAPIとか無いよなぁ...

fujiwarahaji.me

こちらは楽曲のDBです。なんでトップじゃないかと言いますとトップページが現在特殊デザインになってしまっていてどこに飛べばいいのか全然分からないからですw

ここはWP REST APIによるAPI取得が可能です。可能なのですが、イマイチ使い方が分かりません。情報量が多いのでぜひ使いたいのですが...

imasdb.sakaki333.com

最後はミリオンライブの楽曲だけのDBです。ユニットとかも見れるようですね。こちらもAPIとかはないかと。今回色々調べているうちに見つけたので載せておきます。

最後に

最初はim@sparqlでクエリを実行してみただけの記事にしようと思ったのですが、なんか物足りないのでWebアプリケーションも作ってみました。(WebページとWebアプリケーションって何が違うんだろう、動的かどうか?)

その結果、この長さです。多分、今まで自分が書いたアドベントカレンダーの記事で一番長いのでは?(SECCONのwriteupもそれなりに書いたような気が

本当は楽曲を結びつけたり、出演者からのセトリ予想をしたりだとかもしたいのですが、まあ時間があって何かよいAPIが見つかればやろうかなって思ってます。てか、こういうのってすでに誰かが作ってそうだからあまりやりたくないんですよね。n番煎じは面白くないし、話題にもなりにくいので... 僕の記事はこれで終わり。

参考

クエリを書く時に参考にしたサイト

www.aise.ics.saitama-u.ac.jp

virtuoso-no-nazo.blogspot.com

imihito.hatenablog.jp

ちなみにSPARQLのリファレンスは超読みにくいのでオススメしません。