Wikidataのクエリサービスを用いたMAP作成

## WikidataのSPARQLクエリを用いたMAP生成
以前に紹介した **「[Wikidataを用いた経路検索リンク付きMAPの作成](https://qiita.com/koujikozaki/items/60e209517a1dd0b747a8)」** を誰でも簡単に行えるように，Webサービス化しました．
- サービスの実行は https://wd-map.hozo.jp/ から
- ソースコードは https://github.com/oecu-kozaki-lab/WikidataMapVIS にて公開

![Wikidataのクエリサービスを用いたMAP作成サービスの概要](https://private-user-images.githubusercontent.com/8015932/376100827-971b6ae5-79df-48f5-aec7-bbac936ffe66.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3Mjg4OTkzNzQsIm5iZiI6MTcyODg5OTA3NCwicGF0aCI6Ii84MDE1OTMyLzM3NjEwMDgyNy05NzFiNmFlNS03OWRmLTQ4ZjUtYWVjNy1iYmFjOTM2ZmZlNjYucG5nP1gtQW16LUFsZ29yaXRobT1BV1M0LUhNQUMtU0hBMjU2JlgtQW16LUNyZWRlbnRpYWw9QUtJQVZDT0RZTFNBNTNQUUs0WkElMkYyMDI0MTAxNCUyRnVzLWVhc3QtMSUyRnMzJTJGYXdzNF9yZXF1ZXN0JlgtQW16LURhdGU9MjAyNDEwMTRUMDk0NDM0WiZYLUFtei1FeHBpcmVzPTMwMCZYLUFtei1TaWduYXR1cmU9MTNjYjA2NzE1NWU3NDczYTQ3YmY3ODY3ZTA5OWQ1YmU2OTE2ZjQxNWVkN2JjYzljN2JlMGQ4N2JkMzk5MDk0ZiZYLUFtei1TaWduZWRIZWFkZXJzPWhvc3QifQ.nwRHzor50WsLFlxDdaYxpHqBQkH2x6KGRA8yRktFXmw)

この作業に伴って，以前に作成した **「経路検索リンク付きのMAP生成用のSPARQLクエリ」** を改善したので，その詳細を解説します．
改善点のみ解説しますので，以前の記事を未読の方は，[こちら](https://qiita.com/koujikozaki/items/60e209517a1dd0b747a8)を先にお読み下さい．

## SPARQLクエリの改善点
今回，SPARQLクエリの主な改善点は，以下２つです．
1. 経路検索を「名称」でなく「緯度経度」で行うように変更
2. MAP上に複数の点に分かれて表示されるデータを１つにまとめるように修正
それぞれについて，クエリ例を示しつつ解説します．

### 「緯度経度」を用いた経路検索用のSPARQLクエリ
Googleマップを用いた経路検索は，
例）https://www.google.com/maps/dir/現在地/住吉神社
のように，「名称」を用いて目的地を指定することができます．
しかし，[「住吉神社」をWikidataで検索](https://w.wiki/BXcq)すると34件(2024/10/14時点)見つかり，上記の名称を用いた経路検索では，「福岡市にある一の宮の[『住吉神社』](https://www.wikidata.org/wiki/Q29682)の経路を調べるつもりが，現在地からの最寄りの住吉神社への経路が見つかってしまいます．

この問題を避けるために，
例）https://www.google.com/maps/dir/現在地/33.58586244,130.41362643
のような，目的地の「緯度経度」を用いた経路検索を行うURLを使用することにしました．
このようなURLをWikidataへのSPARQLクエリから取得するには，Wikidataにおける位置情報の表現形式（例：**Point(130.41362643 33.58586244)**）から，「**緯度,経度**」という形式への変換が必要です．
この変換を行い，「緯度経度」を用いた経路検索を行うURLを取得するSPARQLが下記の **「[クエリ例１](https://w.wiki/BXgS)」** です．
```SPARQL:クエリ例１
SELECT ?item ?itemLabel ?loc 
    (URI(CONCAT("https://www.google.com/maps/dir/現在地/", 
            (strafter((REPLACE(str(?loc),"\\)",""))," ")) , ",",
            strbefore((REPLACE(str(?loc),"Point\\(",""))," "))
    ) AS ?map)
WHERE {
    ?item wdt:P31/wdt:P279* wd:Q1656379. #分類の指定
    ?item wdt:P625 ?loc.  #位置情報の取得
    SERVICE wikibase:label { bd:serviceParam wikibase:language "ja". }
}
```
このSPARQLクエリでは, **?loc** として得られたWikidataにおける位置情報を，
1. **str**(?loc) で文字列に変換し，
2. **REPLACE**(str(?loc),**"\\\\)"**,**""**) で，末尾の "**)**" を削除し，
2. **strafter**(REPLACE(str(?loc),"\\\\)",""),**" "**) で，半角スペース(" ")よりも **「後ろの文字列」を取り出す**ことで，**「緯度の数値」** を取得しています．　
2. 経度に対しても同様にして， **REPLACE**(str(?loc),**"Point\\\\("**,**""**) で，先頭の "**Point(**" を削除し，
2.  **strbefore**(REPLACE(str(?loc),"Point\\\\(",""),**" "**) で，半角スペース(" ")よりも **「前の文字列」を取り出す**ことで，**「経度の数値」** を取得しています．　

という処理で「**緯度,経度**」という形式への変換した上で，**CONCAT(...)** で "https://www.google.com/maps/dir/現在地/" の末尾に結合し，**URI(...)** でURIの形式に変換することで，目的地の「緯度経度」を用いた経路検索を行うURLを得ています．

### MAP上に複数の点に分かれて表示されるデータを１つにまとめる方法
[以前に作成したSPARQLクエリで作成したマップ](https://w.wiki/BXfK)において，例えば札幌市にある「北海道神宮」の位置をクリックすると，下記のように6つの点に分かれて表示されます．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103276/323aec02-44db-3e59-9644-6985a11f3132.png)
これは，[「北海道神宮」のデータ](https://w.wiki/BXfK)を検索した結果が，
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103276/dcbaea42-98b6-69fe-6d40-3a2387b7e97f.png)
のように，**住所（addr），ウェブサイト（web）に複数データがあるため，それらを組み合わせた複数行だけ得られる** ことが原因です．
このようなデータを
- **「データのID(item)，名称(itemLabel)，位置情報(loc)」** が同一のものは１つの行だけ取得する


ように変更したSPARQLクエリが下記の **「[クエリ例２](https://w.wiki/BXgQ)」** です．
```SPARQL:クエリ例２
SELECT ?item ?itemLabel ?loc
    (SAMPLE(?addr) AS ?address) (SAMPLE(?web) AS ?w) 
    (SAMPLE(?img) AS ?image) (SAMPLE(?kana) AS ?nameKana) 
WHERE {
    ?item rdfs:label "北海道神宮"@ja .
    ?item wdt:P625 ?loc.  #位置情報の取得
    OPTIONAL{?item wdt:P1814 ?kana.}
    OPTIONAL{?item wdt:P18 ?img.}
    OPTIONAL{?item wdt:P6375 ?addr.}
    OPTIONAL{?item wdt:P856 ?web.}
    SERVICE wikibase:label { bd:serviceParam wikibase:language "ja". }
}
GROUP BY ?item ?itemLabel ?loc    
```
このSPARQLクエリでは，
- **GROUP BY** ?item ?itemLabel ?loc で 「データのID(item)，名称(itemLabel)，位置情報(loc)」が同一のデータで **「グループ化」** を行い
- 複数のデータが得られる可能性のある「住所(addr)，ウェブサイト(web)，...」などについては，SELECT句で 「(**SAMPLE**(?addr) AS ?address)」のように，「**複数データから１つのみを取り出して，別の変数に代入**（ASで代入先を指定）」する

ことで，
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103276/631743f9-b210-1ee5-f169-016161c2e721.png)
のように， **「データのID(item)，名称(itemLabel)，位置情報(loc)」が同一のものに対して１つの行だけ** データを取得できます．

## 最終的なSPARQLクエリ
上記の改善点ふまえ，最終的に **[「経路検索リンク付きのMAPの作成」に使用したSPARQLクエリ](https://w.wiki/BXgB)** は下記の通りです．
```SPARQL:クエリ例３
#defaultView:Map{"hide":["?loc"]}
SELECT ?item ?itemLabel ?loc 
    (URI(CONCAT("https://kgs.hozo.jp/sample/details.html?key=",str(?item))) AS ?detail)
    ("■Wikidataでの定義を見る" AS ?detailTitle)   
    (URI(CONCAT("https://www.google.com/maps/dir/現在地/", 
            (strafter((REPLACE(str(?loc),"\\)",""))," ")) , ",",
            strbefore((REPLACE(str(?loc),"Point\\(",""))," "))
    ) AS ?map)
    ("■経路検索" AS ?mapTitle)   
    (SAMPLE(?addr) AS ?address)(SAMPLE(?web) AS ?w) (SAMPLE(?img) AS ?image)
    (CONCAT("(",SAMPLE(?kana),")") AS ?nameKana) 
    (?itemLabel AS ?name) 
WHERE {
    ?item wdt:P31/wdt:P279* wd:Q1656379. #分類の指定
    ?item wdt:P625 ?loc.  #位置情報の取得
    ?item wdt:P17 wd:Q17.
    OPTIONAL{?item wdt:P1814 ?kana.}
    OPTIONAL{?item wdt:P18 ?img.}
    OPTIONAL{?item wdt:P6375 ?addr.FILTER(lang(?addr)="ja")}
    OPTIONAL{?item wdt:P856 ?web.}
    SERVICE wikibase:label { bd:serviceParam wikibase:language "ja". }
}
GROUP BY ?item ?itemLabel ?loc   
    
```
このSPARQLクエリでは，前述の改善点に加え，
- MAPをクリックした際に表示されるデータの順番，表示内容を見やすく調整
- Wikidata上の定義を見るためのリンクを，該当データの「Wikidataのページ」に加え，「データの内容を日本語で見やすく可視化したページ」を用意
- データ数が多くなりすぎて「クエリのタイムアウト」が発生するのを避けるために，「日本のデータに限定（`?item wdt:P17 wd:Q17.`）」

といった工夫をしています．

## おわりに
今回のようなMAP作成は，もっと単純なSPARQLクエリで取得したデータを使用し，表示内容の詳細はJavaScriptなどのプログラミング言語で処理することでも実現することが可能です．
それに対して今回は，
- **極力，SPARRQLのみで，できるだけ処理を完結させる**

というアプローチを取りました．
その結果，SPARQLクエリは少し（かなり？）複雑にはなりましたが，基本的な部分については，それ以上のプログラムでの処理をせずに，目的とするMAPの生成が実現できました．

Wikidataをはじめとする知識グラフ（ナレッジグラフ）の活用方法の１つして，何らかの参考になれば幸いです．