JavaScriptでJSON-LDを操作する話

このプログラムの挙動は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内にあった場合、結果内のhasRoutehttp://purl.org/jrrk#hasRouteを示します。

{
    "jrrk" : "http://purl.org/jrrk#"
}

上記の要素が@context内にあった場合、結果内のjrrk:XXXhttp://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データから柔軟にデータを取り出すことができます。

カテゴリー