このプログラムの挙動はGoogle Chrome 59.0.3071.86で確認を行いました。
例として、odpに登録されている、バス停データを用います。
今回使用するクエリは以下のとおりです。
PREFIX jrrk: <http://purl.org/jrrk#>
construct {
?busstop ?p ?o.
?o ?q ?r.
} where {
{
select ?busstop {
?busstop a jrrk:BusStop.
} limit 10
}
?busstop ?p ?o.
optional {
?o ?q ?r.
filter(isBlank(?o)).
}
}
取得に、Fetch APIを利用します。Fetch APIの詳細については、MDNによるFetch APIの解説をご覧ください。
まず、SPARQLクエリを準備します。
const sparql = `
PREFIX jrrk: <http://purl.org/jrrk#>
construct {
?busstop ?p ?o.
?o ?q ?r.
} where {
{
select ?busstop {
?busstop a jrrk:BusStop.
} limit 10
}
?busstop ?p ?o.
optional {
?o ?q ?r.
filter(isBlank(?o)).
}
}
`;
次に、Fetch API用の設定を追加します。
const call_sparql_config = {
method: "GET",
headers: {
"Accept": "application/ld+json"
},
mode: "cors",
cache: "default",
};
この時、headersにAccept: application/ld+jsonを指定していることに注意してください。
JSON形式でRDFを表現する JSON-LD で取得を行うには、Acceptヘッダにこのように指定をする必要があります。
以下のコードで、通信を行います。
const odp_endpoint = "https://sparql.odp.jig.jp/data/sparql";
document.addEventListener("DOMContentLoaded", function(e){
main();
});
function main(){
let params = new URLSearchParams();
params.append("query", sparql);
fetch(odp_endpoint+"?"+params.toString(), call_sparql_config).then(function(response){
return response.json();
}).then( json => console.log(json));
}
URLSearchParamsは、クエリストリングを生成するAPIです。詳細はMDNの解説ページをご覧ください。
上記のmain関数は、odpエンドポイントから、サンプルクエリを発行し、結果をコンソールにJSON形式で受信するものです。バス停を1件だけ取得するようにした結果は以下の通りです。
{
"@graph" : [ {
"@id" : "_:b0",
"identifier" : "2",
"label" : {
"@language" : "ja",
"@value" : "京福バス池田線 下り"
}
}, {
"@id" : "_:b1",
"表記" : {
"@language" : "ja",
"@value" : "稲荷"
},
"ic:表記" : {
"@language" : "ja",
"@value" : "稲荷"
}
}, {
"@id" : "http://odp.jig.jp/rdf/jp/fukui/imadate/ikeda/741#2/%E4%BA%AC%E7%A6%8F%E3%83%90%E3%82%B9%E6%B1%A0%E7%94%B0%E7%B7%9A%E3%80%80%E4%B8%8B%E3%82%8A/%E7%A8%B2%E8%8D%B7/35.885572/136.343482/",
"@type" : "jrrk:BusStop",
"名称" : "_:b1",
"地理座標" : "http://odp.jig.jp/res/geopoint/+35.885571/+136.343475/",
"ic:名称" : {
"@id" : "_:b1"
},
"ic:地理座標" : {
"@id" : "http://odp.jig.jp/res/geopoint/+35.885571/+136.343475/"
},
"http://odp.jig.jp/odp/1.0#hasRoute" : {
"@id" : "_:b0"
},
"hasRoute" : "_:b0",
"label" : {
"@language" : "ja",
"@value" : "稲荷"
},
"lat" : "35.885572",
"long" : "136.343482"
} ],
"@context" : {
"hasRoute" : {
"@id" : "http://purl.org/jrrk#hasRoute",
"@type" : "@id"
},
"地理座標" : {
"@id" : "http://imi.go.jp/ns/core/rdf#地理座標",
"@type" : "@id"
},
"label" : "http://www.w3.org/2000/01/rdf-schema#label",
"lat" : {
"@id" : "http://www.w3.org/2003/01/geo/wgs84_pos#lat",
"@type" : "http://www.w3.org/2001/XMLSchema#float"
},
"long" : {
"@id" : "http://www.w3.org/2003/01/geo/wgs84_pos#long",
"@type" : "http://www.w3.org/2001/XMLSchema#float"
},
"名称" : {
"@id" : "http://imi.go.jp/ns/core/rdf#名称",
"@type" : "@id"
},
"表記" : "http://imi.go.jp/ns/core/rdf#表記",
"identifier" : "http://purl.org/dc/terms/identifier",
"schema" : "http://schema.org/",
"cc" : "http://creativecommons.org/ns#",
"icnew" : "http://imi.go.jp/ns/core/rdf#",
"jrrk" : "http://purl.org/jrrk#",
"uncefactISO4217" : "urn:un:unece:uncefact:codelist:standard:ISO:ISO3AlphaCurrencyCode:2012-08-31#",
"dct" : "http://purl.org/dc/terms/",
"owl" : "http://www.w3.org/2002/07/owl#",
"xsd" : "http://www.w3.org/2001/XMLSchema#",
"rdfs" : "http://www.w3.org/2000/01/rdf-schema#",
"ic" : "http://imi.ipa.go.jp/ns/core/rdf#",
"foaf" : "http://xmlns.com/foaf/0.1/"
}
}
この結果は、@graphと@contextの2つによって成り立っております。@graphは実際に取得したトリプルです。@contextはそれぞれのプロパティがどのIRIを意味するか、型は何の型か、といった情報が含まれております。詳細は JSON-LDの仕様をご確認ください。
JSON-LDには@contextの内容を全て@graphに埋め込むようにしたExpanded書式、逆に@contextに可能な限り情報を入れるようにし、@graphが最小になるようにしたCompacted書式等があります。
上記の書式は、主語1つにつき配列1要素を割り当てるようにしたFlattened書式です。
これはそれぞれの主語に対して処理を行うときに便利な書式です。
帰ってくる書式はサーバの実装により異なるため、統一して扱えるようにjsonld.jsなどで事前に書式を変えておくと良いでしょう。
odpのサーバでは、デフォルトでFlattened形式を取得してくれるようなので、このまま進めます。
@graph内から、型がjrrk:BusStopであるトリプルの抽出を行います。@contextの中から、jrrk:BusStopがどのようなプロパティで入っているかを求めます。@context内には省略名がフルパスの参照であるケースと、短縮IRIのプリフィックスの参照であるケースがあります。
例えば、
{
"hasRoute" : {
"@id" : "http://purl.org/jrrk#hasRoute"
}
}
上記の要素が@context内にあった場合、結果内のhasRouteはhttp://purl.org/jrrk#hasRouteを示します。
{
"jrrk" : "http://purl.org/jrrk#"
}
上記の要素が@context内にあった場合、結果内のjrrk:XXXはhttp://purl.org/jrrk#XXXを示します。
戦略として、まず絶対パスが参照されているケースを求め、次に相対参照されているケースを求めます。もしなければ、@graph内に絶対パスで指定されています。
@contextでは、キーに対して純粋にIRIの文字列が格納されているか、@id要素にIRIが格納されています。相対参照の場合は、直接文字列が指定されています。
以下の関数によって、使われている値を検索することが可能です。
function find_key(context, full_iri){
// 短縮IRIによる探索準備
let splited = /^(.*[/#])([^/#]*)$/.exec(full_iri);
let prefix_iri = splited[1];
let suffix = splited[2];
let shorten_result = null;
let full_result = null;
// フルIRIによる探索
for (i in context) {
let compare_target = typeof(context[i]) === "string" ?
context[i] :
context[i]["@id"];
if( compare_target === full_iri ){
// fullは一意
full_result=i;
break;
}
if( compare_target === prefix_iri ){
shorten_result = i + ":" + suffix;
}
}
if (full_result !== null ){
return full_result;
}
if (shorten_result !== null){
return shorten_result;
}
return full_iri;
}
準備が整ったので、抜き出しを行います。結果jsonに対して、@graphからjrrk:BusStop型のトリプルの抽出を行います。
let busstops_graphs = json["@graph"].filter(triple => triple["@type"] === find_key(json, "http://purl.org/jrrk#BusStop"));
今回は、IRIと名前の表示を行います。名前はhttp://imi.go.jp/ns/core/rdf#名称/http://imi.go.jp/ns/core/rdf#表記を辿り、日本語のものを表示します。
トリプルから、IRIと名前を持つプロパティへの加工を行います。
let Name = find_key(json["@context"], "http://imi.go.jp/ns/core/rdf#名称");
let Transcribe = find_key(json["@context"], "http://imi.go.jp/ns/core/rdf#表記");
let busstops = busstop_graphs.map(triples => {
let names = json["@graph"].filter(t => t["@id"] === triples[Name]);
let name = undefined;
if (names.length > 0) {
name = names[0][Transcribe]["@value"];
}
return {
iri: triples["@id"],
name: name,
};
});
最初の2行は、@contextによるキーを求めています。
3行目から、各トリプルに対し、名前を抽出、オブジェクトへの変換処理を行っています。
4行目で名前を持つグラフの抜き出しを行います。名前は複数持っている可能性もありますが、
今回は簡略化のため単一の名前を持っているものとします。
7行目で、実際の表示するための名前の抜き出しを行います。
多言語化されたものを含めても、名前が単一であれば、以下のようなオブジェクトが格納されます。
もし名前が複数あったのであれば、以下のオブジェクトが配列形式で格納されます。
{
"@value": "名前",
"@lang": "ja"
}
@valueは、そのままトリプルに対するリテラルの値を指します。@langは、それが何語で示すかを表しています。
今回は単一であることがわかっているので、そのまま格納されているオブジェクトの@valueを参照します。
これにて、IRIと名前が格納されたオブジェクトを作成することができました。
今回は言語が日本語のものしかありませんでしたが、データが多言語対応したときのために、アプリ側も多言語化できるようにしましょう。
もし多言語化されていた場合は、以下のような形式で格納されています。
[
{
"@value": "名前",
"@lang": "ja"
},
{
"@value": "Name",
"@lang": "en"
}
]
ja, enの順に優先して表示することを考えます。はじめに、優先順位のテーブルを用意します。
そこから、フィルタ・並び替えを行います。
let langPriority = {
"ja": 0,
"en": 1,
};
name_values = names[0][Transcribe];
if(Array.isArray(name_values) && name_values.length > 0) {
name = name_values
.filter(value => allowLang.indexOf(value["@lang"]) >= 0)
.sort( (a, b) => langPriority[a["@lang"]] - langPriority[b["@lang"]])[0]["@value"];
} else {
name = name_values["@value"];
}
上記のコードを応用すれば、国際化に対応したアプリの作成が容易になります。
このように、RDFをJSONで表した形式であるJSON-LDを加工すると、SPARQLの結果や、元々のRDFデータから柔軟にデータを取り出すことができます。