wikidataにSPARQLを投げて生まれ変わりを辿る

[Wikidataアクセスのサンプルとして作ったクソアプリ](https://codepen.io/ozota/pen/xmVWNQ)


* あなたの誕生日から3か月以内に死亡した人を選び、勝手に生まれ変わり前認定して表示します
* 生まれ変わり前の生まれ変わり前も検索します

# 本題
## [wikidata](https://www.wikidata.org/wiki/Wikidata:Main_Page)とは
* ざっくり言うと「Wikipediaのデータベース」版です
  * RDFという形式でデータが構造化されています
  * SPARQLと呼ばれるクエリ言語でデータ取得できます     

## RDFについて
* 主語、述語、目的語の3つの組み合わせで表現されるデータ構造になります       
* 例えば「[織田信長](https://www.wikidata.org/wiki/Q171411)」の項目については、次のような構造で情報が保存されています
      
|主語|述語|目的語|
|:--:|:--:|:--:|
|Oda Nobunaga|instance of|human|
|Oda Nobunaga|father|Oda Nobuhide|
|Oda Nobunaga|mother|Dota Gozen|


* 上記表で目的語として挙げた項目について、それを主語としたデータもまた格納されています

|主語|述語|目的語|
|:--:|:--:|:--:|
|Oda Nobuhide|instance of|human|
|Oda Nobuhide|father|Oda Nobusada|


* 図で表すとこんな感じで,複雑なグラフ構造になります
![image.png](https://qiita-image-store.s3.amazonaws.com/0/304012/71261eda-e715-421b-599a-def13ee024a7.png)

## SPARQLについて
* RDFからデータ取得するためのクエリ言語です
  * SQLに似た構文です 
  * 主語・述語・目的語の各項目について、IDが振られています。そのIDを用いてクエリを構成します
* 織田信長の父親を問い合わせる簡単なクエリのサンプルは次のようになります

```sql:織田信長の父親を問い合わせるSPARQL

SELECT ?human 
WHERE
{
  wd:Q171411 wdt:P22 ?human. 
}
```

* `wd:Q171411`が織田信長のID、`wdt:P22`が述語「父親」のID、 ?で始まる`?human`が変数になります。
  * selectで変数(?human)の値を取り出してます
  * `wd:Q171411`を主語、`wdt:P22`を述語とするレコードを探し、そのレコードの目的語が`?human`に入ります 
* [Wikidata Query Service](https://query.wikidata.org/#SELECT%20%3Fhuman%20%0AWHERE%0A%7B%0A%20%20wd%3AQ171411%20wdt%3AP22%20%3Fhuman.%20%0A%7D)で実際にwikidataにクエリを投げて、その結果を見ることができます。
    * Wikidata Query Serviceの左端にある ![image.png](https://qiita-image-store.s3.amazonaws.com/0/304012/6d205fcc-51bf-c5ef-abdd-101282e1a793.png)
 を押してください
    * 下に出た結果のリンク ![image.png](https://qiita-image-store.s3.amazonaws.com/0/304012/cb4e1ed7-1695-9204-093e-0dc8b8a29a9c.png) から Oda Nobuhide の wikidataページに飛びます
  * IDはWikidata Query Serviceの補完機能を使って調べることができます。
    * ![image.png](https://qiita-image-store.s3.amazonaws.com/0/304012/26686ae3-1a2d-59ff-7668-b3ae8f8fbe70.png)
      * 主語・目的語の場合は「wd:」の後に検索したい語を入力してCtr+Spaceキーを押すと補完されます
      * 述語の場合は「wdt:」の後に入力してCtr+Spaceキーを押します
      * 「wd:」,「wdt:」は接頭語と呼ばれ、他にもいろいろあります。
          * 例えば「rdfs:」という接頭語は「rdfs:label」という述語を持ち、ID等から名前を取り出すのに使用します
  * これらを用い、where句の中で構造を指定します
      



* 主語を変数にすることもできます
  
    ```sql
    
    SELECT ?human 
    WHERE
    {
       ?human wdt:P22  wd:Q171411.
    }
    ```
  
  * 信長を父親に持つ人たち、つまり信長の子供達がぞろぞろ取得できます。[実行リンク](https://query.wikidata.org/#SELECT%20%3Fhuman%20%0AWHERE%0A%7B%0A%20%20wd%3AQ171411%20wdt%3AP22%20%3Fhuman.%20%0A%7D)

* 述語も変数にできます
  * 次のクエリは織田信長と織田信秀の関係は？と聞いてるのと同じです。   
  
   ```sql

   SELECT ?rel 
   WHERE
   {
     wd:Q171411 ?rel wd:Q1153962. 
   }
   ```




* 複数条件も指定できます

    ```sql
    
    SELECT ?human 
    WHERE
    {
       ?human wdt:P22 wd:Q171411.
       ?human wdt:P21 wd:Q6581072
    }
    ```

  * 信長を父親に持つ、かつ性別が女性、の項目がhumanに格納されます
  * 主語が同じ場合、`?human wdt:P22 wd:Q171411; wdt:P21 wd:Q6581072` というように`;`を使って主語を省略することもできます。

* 他にもたくさん指定できますが、きりがないので割愛します

# アプリを作ってみる
* 例として冒頭に張り付けた、「生まれかわり検索」を作ってみます
  * 死亡年と誕生年が3か月以内の人を勝手に生まれ変わり認定します。 
  * 自分の誕生日を入力すると生まれ変わりを一覧表示します

## まずクエリをWikidata Query Serviceで組み立ててみます。
* 例：誕生日2018-01-01から遡って1年以内に死んだ人たちを探すクエリ
  * 誕生日部分は後で作るアプリで埋め込む想定で、まずはクエリのひな型を試行錯誤しながら作ります

  ```sql
  SELECT ?id ?name ?deth_date ?article #1
  WHERE
  {
    BIND("2018-01-01"^^xsd:date as ?birth_date) #2
    ?id wdt:P570 ?deth_date; wdt:P1559 ?name.
    
    BIND(?birth_date - ?deth_date as ?diff)
    FILTER( ?diff > 0 && ?diff < 365) #3

    OPTIONAL { #4
      ?article schema:about wd:Q171411 .
      ?article schema:inLanguage "ja" .
      FILTER (SUBSTR(str(?article), 1, 25) = "https://ja.wikipedia.org/")
    }
  }
  ORDER BY ?diff
  LIMIT 100

  ```
* この例は次のようなクエリです
  * #1: Id, 人名、没年、wikipediaの記事を取得します。
  * #2: `^^xsd:date`で文字列を日付に変換しています。 結果をBIND・ASで変数に格納します
  * #3: 誕生日と没年の差が1年未満のものでフィルタします
  * #4: wikipediaの記事を取得します。ない場合もあるのでOPTIONALとしています
  　  
* ぐぐったり、Wikidata Query Service左端の![image.png](https://qiita-image-store.s3.amazonaws.com/0/304012/44de8742-098f-41dd-c869-3b263cad677e.png)からサンプルを探すと書き方が調べられます

* 同様にして人のid(wd:Q171411)から仕事を問い合わせるクエリも作ってみます

   ```sql
    SELECT ?jobLabel
    WHERE
    {
      wd:Q171411 wdt:P106 ?job. 
      ?job rdfs:label ?jobLabel #a
      SERVICE wikibase:label { bd:serviceParam wikibase:language "ja". } #b
    }
    LIMIT 1
   ```
* 
  * #a: IDからラベルへ変換します
  * #b: ラベル名の対象として日本語のみとします

## アプリを書く
* 無理にcopdepenに実装しました。
  * Qiitaに埋め込んでしまいたいからですがダメでした(;w;)
      * どうもQiitaのサポートしない構文のせいみたいです  
  * 素直にやるならgithub-pagesがいいと思います

### 解説
* APIエンドポイントは`https://query.wikidata.org/sparql?format=json&query=${query文字列}`です
  * これに対してfetch APIでデータ取得します  
  * 結果は次のようなjsonになります
  
  ```json
  {
    "head" : {
      "vars" : [ "id", "name", "deth_date", "article" ]
    },
    "results" : {
      "bindings" : [ {
        "article" : {
          "type" : "uri",
          "value" : "https://ja.wikipedia.org/wiki/%E7%B9%94%E7%94%B0%E4%BF%A1%E9%95%B7"
        },
        "id" : {
          "type" : "uri",
          "value" : "http://www.wikidata.org/entity/Q11634158"
        },
        "name" : {
          "xml:lang" : "ja",
          "type" : "literal",
          "value" : "豊田 達郎"
        },
        "deth_date" : {
          "datatype" : "http://www.w3.org/2001/XMLSchema#dateTime",
          "type" : "literal",
          "value" : "2017-12-30T00:00:00Z"
        }
      }, {
  ...
  ``` 

* Wikidataのクエリは30秒以上かかるとタイムアウトでエラーになります
  * クエリを細かく分割して投げるといいと思います
  * 生まれ変わり検索では人物情報を取得するクエリを投げた後に、職業を問い合わせるクエリを投げています
    * 1度に問い合わせるとタイムアウトするためです 

* その他同時アクセスは3つまで等の制限があります
  * 今回はクライアントで動作するので特に問題になりませんでした
      * 再帰関数で1並列で呼び出しています 

* ちょっと複雑なクエリを投げるとWikidataはかなり重いのですが、これはどうしようもなさそうです