本章で学ぶこと

前章でSPARQLの基本構文を学びました。本章では、より高度なクエリ機能を学びます。これらの機能を使いこなすことで、複雑なデータ取得や分析が可能になります。筆者がジャパンサーチやWikidata、さらにはOdeuropa等のエンドポイントに対して実践してきたクエリ技法を交えながら解説します。

クエリの種類

SPARQLには、SELECT以外にも3つのクエリ形式があります。

ASK

ASKクエリは、パターンにマッチするデータが存在するかどうかを真偽値(true/false)で返します。

PREFIX wd: <http://www.wikidata.org/entity/>
PREFIX wdt: <http://www.wikidata.org/prop/direct/>

ASK {
  wd:Q1195 wdt:P31 wd:Q5 .
}

このクエリは「Q1195(夏目漱石)はQ5(人間)のインスタンスであるか?」を確認します。結果は true です。データの存在確認やバリデーションに使えます。

DESCRIBE

DESCRIBEクエリは、指定したリソースに関するすべてのトリプルを返します。返されるトリプルの範囲はエンドポイントの実装に依存します。

DESCRIBE <http://dbpedia.org/resource/Natsume_Sōseki>

このクエリを実行すると、夏目漱石に関するすべてのプロパティとその値が返されます。データの探索や、あるリソースにどのような情報が含まれているかを確認する際に便利です。

CONSTRUCT

CONSTRUCTクエリは、検索結果から新しいRDFグラフを構築して返します。既存のRDFデータを別の語彙や構造に変換する際に使います。

PREFIX dbo: <http://dbpedia.org/ontology/>
PREFIX dbr: <http://dbpedia.org/resource/>
PREFIX schema: <http://schema.org/>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>

CONSTRUCT {
  ?person schema:name ?name .
  ?person schema:birthDate ?birthDate .
  ?person schema:birthPlace ?birthPlaceLabel .
}
WHERE {
  ?person rdf:type dbo:Writer .
  ?person dbo:birthPlace dbr:Japan .
  ?person rdfs:label ?name .
  ?person dbo:birthDate ?birthDate .
  ?person dbo:birthPlace ?birthPlace .
  ?birthPlace rdfs:label ?birthPlaceLabel .
  FILTER (lang(?name) = "ja")
  FILTER (lang(?birthPlaceLabel) = "ja")
}
LIMIT 10

CONSTRUCTクエリは、データのフォーマット変換やETL(抽出・変換・ロード)処理に活用できます。筆者はCultural JapanのRDFストアに格納されている展覧会情報を分析する際にも、CONSTRUCTクエリを活用しました。詳しくはCultural JapanのRDFストアに格納されている展覧会情報の活用をご覧ください。

GROUP BY / HAVING

GROUP BYは、結果を特定の変数の値でグループ化し、集約関数(COUNTSUMAVGMINMAX)を適用するために使います。HAVINGは、グループ化後の条件を指定します。

PREFIX dbo: <http://dbpedia.org/ontology/>
PREFIX dbr: <http://dbpedia.org/resource/>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>

SELECT ?birthPlace ?birthPlaceLabel (COUNT(?person) AS ?count)
WHERE {
  ?person rdf:type dbo:Writer .
  ?person dbo:birthPlace ?birthPlace .
  ?birthPlace dbo:country dbr:Japan .
  ?birthPlace rdfs:label ?birthPlaceLabel .
  FILTER (lang(?birthPlaceLabel) = "ja")
}
GROUP BY ?birthPlace ?birthPlaceLabel
HAVING (COUNT(?person) > 5)
ORDER BY DESC(?count)

このクエリは、日本の作家の出身地ごとの人数を集計し、5人以上の出身地を持つ場所だけを人数の多い順に表示します。

筆者はジャパンサーチのデータに対しても同様の集約クエリを用いて、最新の更新年ごとのアイテム数を視覚化する分析を行いました。詳しくはジャパンサーチ利活用スキーマについて、最新の更新年ごとのアイテム数を視覚化するをご覧ください。

集約関数一覧

関数説明
COUNT(?var)値の数をカウントCOUNT(?person)
SUM(?var)数値の合計SUM(?population)
AVG(?var)数値の平均AVG(?age)
MIN(?var)最小値MIN(?birthDate)
MAX(?var)最大値MAX(?birthDate)
GROUP_CONCAT(?var; separator=",")値を文字列に連結GROUP_CONCAT(?name; separator=", ")
SAMPLE(?var)グループからサンプル値を1つ取得SAMPLE(?label)

GROUP_CONCATの活用

GROUP_CONCAT は、グループ内の複数の値を1つの文字列に連結する便利な関数です。

PREFIX wdt: <http://www.wikidata.org/prop/direct/>
PREFIX wd: <http://www.wikidata.org/entity/>

SELECT ?author ?authorLabel (GROUP_CONCAT(DISTINCT ?genreLabel; separator=", ") AS ?genres)
WHERE {
  ?work wdt:P50 ?author .
  ?work wdt:P136 ?genre .
  ?author wdt:P27 wd:Q17 .
  SERVICE wikibase:label {
    bd:serviceParam wikibase:language "ja,en" .
    ?author rdfs:label ?authorLabel .
    ?genre rdfs:label ?genreLabel .
  }
}
GROUP BY ?author ?authorLabel
LIMIT 20

UNION

UNIONは、複数のグラフパターンのいずれかにマッチするデータを取得します(OR条件)。

PREFIX dbo: <http://dbpedia.org/ontology/>
PREFIX dbr: <http://dbpedia.org/resource/>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>

SELECT ?person ?name ?type
WHERE {
  {
    ?person rdf:type dbo:Writer .
    ?person dbo:birthPlace dbr:Tokyo .
    ?person rdfs:label ?name .
    BIND("作家" AS ?type)
  }
  UNION
  {
    ?person rdf:type dbo:Painter .
    ?person dbo:birthPlace dbr:Tokyo .
    ?person rdfs:label ?name .
    BIND("画家" AS ?type)
  }
  FILTER (lang(?name) = "ja")
}
LIMIT 20

VALUES

VALUESは、変数に複数の値を指定してパターンマッチングに使用します。IN句のような機能です。

PREFIX wd: <http://www.wikidata.org/entity/>
PREFIX wdt: <http://www.wikidata.org/prop/direct/>

SELECT ?person ?personLabel ?birthDate
WHERE {
  VALUES ?person { wd:Q1195 wd:Q272877 wd:Q313041 }
  ?person wdt:P569 ?birthDate .
  SERVICE wikibase:label { bd:serviceParam wikibase:language "ja,en" . }
}

このクエリは、夏目漱石(Q1195)、森鷗外(Q272877)、芥川龍之介(Q313041)の3人の生年月日を取得します。

サブクエリ

SPARQLではクエリの中にクエリを埋め込むサブクエリが使えます。内側のクエリの結果を外側のクエリで利用できます。

PREFIX wdt: <http://www.wikidata.org/prop/direct/>
PREFIX wd: <http://www.wikidata.org/entity/>

SELECT ?author ?authorLabel ?workCount
WHERE {
  {
    SELECT ?author (COUNT(?work) AS ?workCount)
    WHERE {
      ?work wdt:P31 wd:Q7725634 .
      ?work wdt:P50 ?author .
      ?author wdt:P27 wd:Q17 .
    }
    GROUP BY ?author
    HAVING (COUNT(?work) >= 10)
  }
  SERVICE wikibase:label { bd:serviceParam wikibase:language "ja,en" . }
}
ORDER BY DESC(?workCount)
LIMIT 20

プロパティパス

プロパティパスは、RDFグラフ上の経路をパターンで指定する強力な機能です。通常のトリプルパターンでは1ステップのリンクしかたどれませんが、プロパティパスを使うと複数ステップのリンクを一度にたどれます。

基本的なパス演算子

演算子意味
/パスの連結(AからBを経由してCへ)wdt:P131/wdt:P131
|パスの選択(AまたはB)rdfs:label|skos:prefLabel
*0回以上の繰り返しwdt:P131*
+1回以上の繰り返しrdfs:subClassOf+
?0回または1回wdt:P131?
^逆方向のパス^wdt:P50
()グルーピング(wdt:P131/wdt:P17)

パスの連結(/

PREFIX wdt: <http://www.wikidata.org/prop/direct/>
PREFIX wd: <http://www.wikidata.org/entity/>

SELECT ?city ?cityLabel ?countryLabel
WHERE {
  ?city wdt:P31 wd:Q1549591 .
  ?city wdt:P131/wdt:P17 ?country .
  SERVICE wikibase:label { bd:serviceParam wikibase:language "ja,en" . }
}
LIMIT 20

推移的閉包(+*

階層構造をたどるのに特に有用です。筆者がOdeuropa Explorerの語彙階層構造を調査した際にも、プロパティパスの推移的閉包を活用してSKOS語彙の階層関係を探索しました。詳しくはOdeuropa Explorer の語彙階層構造を調査するをご覧ください。

PREFIX wdt: <http://www.wikidata.org/prop/direct/>
PREFIX wd: <http://www.wikidata.org/entity/>

SELECT ?class ?classLabel
WHERE {
  wd:Q7725634 wdt:P279+ ?class .
  SERVICE wikibase:label { bd:serviceParam wikibase:language "ja,en" . }
}

wdt:P279+ は「サブクラス(P279)を1回以上たどる」ことを意味します。

逆方向のパス(^

PREFIX wdt: <http://www.wikidata.org/prop/direct/>
PREFIX wd: <http://www.wikidata.org/entity/>

SELECT ?work ?workLabel
WHERE {
  wd:Q1195 ^wdt:P50 ?work .
  SERVICE wikibase:label { bd:serviceParam wikibase:language "ja,en" . }
}
LIMIT 20

MINUS / NOT EXISTS

特定のパターンにマッチしないデータを取得するための構文です。

FILTER NOT EXISTS

PREFIX wdt: <http://www.wikidata.org/prop/direct/>
PREFIX wd: <http://www.wikidata.org/entity/>

SELECT ?person ?personLabel
WHERE {
  ?person wdt:P31 wd:Q5 .
  ?person wdt:P27 wd:Q17 .
  ?person wdt:P106 wd:Q36180 .
  FILTER NOT EXISTS { ?person wdt:P569 ?birth . }
  SERVICE wikibase:label { bd:serviceParam wikibase:language "ja,en" . }
}
LIMIT 20

このクエリは、「日本国籍の作家であるが、生年月日(P569)が登録されていない人物」を取得します。データの欠損を発見するのに便利です。

MINUS

PREFIX wdt: <http://www.wikidata.org/prop/direct/>
PREFIX wd: <http://www.wikidata.org/entity/>

SELECT ?person ?personLabel
WHERE {
  ?person wdt:P31 wd:Q5 .
  ?person wdt:P27 wd:Q17 .
  ?person wdt:P106 wd:Q36180 .
  MINUS { ?person wdt:P570 ?death . }
  SERVICE wikibase:label { bd:serviceParam wikibase:language "ja,en" . }
}
LIMIT 20

MINUSFILTER NOT EXISTS と似ていますが、変数のバインディングの扱いが微妙に異なります。一般的には FILTER NOT EXISTS の方が意図通りの結果を得やすいですが、MINUS が適切な場面もあります。

SERVICE(連合クエリ)

SERVICE キーワードを使うと、外部のSPARQLエンドポイントにクエリの一部を委譲する「連合クエリ(Federated Query)」が実行できます。

PREFIX wdt: <http://www.wikidata.org/prop/direct/>
PREFIX wd: <http://www.wikidata.org/entity/>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>

SELECT ?person ?personLabel ?dbpediaAbstract
WHERE {
  ?person wdt:P31 wd:Q5 .
  ?person wdt:P27 wd:Q17 .
  ?person wdt:P106 wd:Q36180 .
  SERVICE wikibase:label { bd:serviceParam wikibase:language "ja,en" . }

  OPTIONAL {
    ?person wdt:P1842 ?dbpediaId .
    SERVICE <http://dbpedia.org/sparql> {
      ?dbpediaId <http://dbpedia.org/ontology/abstract> ?dbpediaAbstract .
      FILTER (lang(?dbpediaAbstract) = "ja")
    }
  }
}
LIMIT 5

連合クエリは強力ですが、外部エンドポイントの応答速度に依存するため、パフォーマンスに注意が必要です。また、すべてのエンドポイントが連合クエリをサポートしているわけではありません。筆者の経験では、SPARQL クライアントをApache Jena Fusekiに対応させる際に連合クエリに関するいくつかの注意点がありました。詳しくはSPARQL クライアントを Apache Jena Fuseki に対応させるときにハマった 3 つのことをご覧ください。

文字列操作関数

SPARQLには豊富な文字列操作関数が用意されています。

SELECT
  (STRLEN(?name) AS ?nameLength)
  (UCASE(?name) AS ?upper)
  (LCASE(?name) AS ?lower)
  (SUBSTR(?name, 1, 2) AS ?firstTwo)
  (CONCAT(?familyName, " ", ?givenName) AS ?fullName)
  (REPLACE(?name, "夏目", "NATSUME") AS ?replaced)
WHERE {
  BIND("夏目漱石" AS ?name)
  BIND("夏目" AS ?familyName)
  BIND("漱石" AS ?givenName)
}
関数説明
STRLEN(str)文字列の長さ
SUBSTR(str, start, length)部分文字列の抽出
UCASE(str)大文字変換
LCASE(str)小文字変換
CONCAT(str1, str2, ...)文字列の連結
REPLACE(str, pattern, replacement)文字列の置換(正規表現対応)
ENCODE_FOR_URI(str)URIエンコード
STRBEFORE(str, separator)区切り文字より前の部分
STRAFTER(str, separator)区切り文字より後の部分

条件式(IF / COALESCE)

IF

SELECT ?person ?name
  (IF(?birthYear < 1900, "近代以前", "近代以降") AS ?era)
WHERE {
  ?person dbo:birthDate ?birthDate .
  ?person rdfs:label ?name .
  BIND(YEAR(?birthDate) AS ?birthYear)
  FILTER(lang(?name) = "ja")
}
LIMIT 20

COALESCE

COALESCE は、引数のうち最初にバインドされている(NULLでない)値を返します。

SELECT ?item (COALESCE(?jaLabel, ?enLabel, STR(?item)) AS ?label)
WHERE {
  ?item rdf:type dbo:City .
  OPTIONAL { ?item rdfs:label ?jaLabel . FILTER(lang(?jaLabel) = "ja") }
  OPTIONAL { ?item rdfs:label ?enLabel . FILTER(lang(?enLabel) = "en") }
}
LIMIT 20

このクエリは、日本語ラベルがあればそれを、なければ英語ラベルを、それもなければURIの文字列を表示します。多言語データの処理で重宝します。

クエリの最適化のヒント

SPARQLクエリのパフォーマンスを改善するためのいくつかのヒントを紹介します。

1. 適切なLIMITを指定する: 必要以上のデータを取得しないように、LIMIT を適切に設定します。

2. FILTERの位置を工夫する: FILTERは、関連するトリプルパターンのできるだけ近くに配置します。

3. OPTIONALを使いすぎない: OPTIONAL は便利ですが、多用するとパフォーマンスが悪化する場合があります。

4. プロパティパスの * に注意: *(0回以上の繰り返し)は無制限に探索が広がる可能性があるため、大規模なグラフでは使用に注意が必要です。

5. 連合クエリは最小限に: SERVICE による連合クエリはネットワーク遅延の影響を受けるため、必要最小限の結果を外部に問い合わせるようにします。

まとめ

本章では、SPARQLの高度な機能を学びました。重要なポイントを振り返ります。

  • ASKDESCRIBECONSTRUCT はSELECT以外のクエリ形式
  • GROUP BY / HAVING で集約とグループ化ができる
  • UNION でOR条件、VALUES で複数値の指定ができる
  • サブクエリでクエリを入れ子にできる
  • プロパティパスでグラフ上の経路を柔軟に指定できる
  • FILTER NOT EXISTS / MINUS で否定条件を表現できる
  • SERVICE で外部エンドポイントへの連合クエリが可能

次章では、これらのSPARQL機能を活用して、Wikidataの実践的なクエリを書いていきます。

関連記事