Virtuoso / Dydra 向けに作られた SPARQL Explorer「Snorql」を Apache Jena Fuseki でも動くようにしました。SPARQL は W3C 標準ですが、エンドポイント実装ごとの挙動差は意外と大きいです。Fuseki 対応で直面した 3 つの問題と、その解決方法を記録します。
開発環境
Docker で Fuseki を起動し、ローカルで検証しました。
1. DESCRIBE のレスポンス形式が違う
症状
Fuseki に DESCRIBE クエリを投げると、結果が画面に表示されません。コンソールには JSON パースエラーが出ていました。
調査
SPARQL の DESCRIBE / CONSTRUCT は SELECT と違い、RDF グラフを返します。その形式がエンドポイントによって異なります。
| エンドポイント | DESCRIBE のレスポンス | output= パラメータ |
|---|---|---|
| Virtuoso / Dydra | SPARQL Results JSON (独自拡張) | — |
| Fuseki | RDF 形式 (Turtle, RDF/JSON 等) | Accept ヘッダより優先される |
Snorql は以前から output=json を URL パラメータに付けて送信していました。Fuseki はこれを解釈して JSON-LD を返しますが、Snorql 側は RDF/JSON を期待しているため、パースが壊れます。
curl で切り分けると原因がはっきりします。
Fuseki ではoutput=json が邪魔をしていました。 Accept ヘッダの content negotiation に任せれば正しい形式が返ります。
修正 (5 箇所)
sparql.js — output パラメータの条件付き送信
_output が空文字なら output= を送らないようにしました。
sparql.js — Content-Type で JSON を判定
output パラメータを送っていなくても、レスポンスの Content-Type が json を含んでいれば JSON.parse します。Fuseki が application/rdf+json を返した場合に対応しています。
snorql.js — DESCRIBE 時の Accept ヘッダと output 制御
typo の修正 (jsonapplication/json → ,application/json) と、output を空文字にして Fuseki の content negotiation を有効化しました。
snorql.js — 空文字の output を正しく伝播
JavaScript の if(output) は空文字 "" を falsy と評価するため、!== undefined に変更しました。
snorql_def.js — endpoint_type の設定
2. PREFIX 宣言がないとクエリが通らない
症状
DESCRIBE は表示されるようになりましたが、ラベル取得や関連リソース検索が 400 エラーになります。
原因
Snorql は DESCRIBE 表示後、ラベル取得等の補助クエリを内部的に発行します。これらは rdfs:label や schema:name 等のプレフィックス名をそのまま使っています。
| エンドポイント | PREFIX 宣言 |
|---|---|
| Virtuoso | rdfs: 等を暗黙解決 (宣言不要) |
| Fuseki | すべて明示的な PREFIX 宣言が必要 (W3C 準拠) |
Virtuoso の暗黙解決に依存していたコードが、Fuseki では動きませんでした。
修正
snorql_ldb.js — 補助クエリに PREFIX を自動付与
SPARQL.Service.setPrefix() で名前空間を登録しておくと、クエリ送信時に PREFIX rdfs: <...> が自動で先頭に付きます。namespaces.js に定義済みの全プレフィックスを一括登録しました。
snorql_ldb.js — null ガード
0
未定義プロパティが undefined として渡された場合の TypeError を防止しています。
3. is_virtuoso フラグが 2 つの仕事をしている
症状
Fuseki に加えて Dydra も並行運用しようとすると、Dydra で 400 エラーが出ました。
1
原因
is_virtuoso フラグが 2 つの異なる責務を兼ねていました。
| 責務 | 場所 | truthy のとき |
|---|---|---|
| レスポンス形式 | snorql.js | output=json を送信 |
| Virtuoso 固有構文 | util.js | define sql:describe-mode "CBD" 等を付与 |
Dydra は output=json が必要 (Virtuoso と同じレスポンス形式) ですが、Virtuoso 固有の define は受け付けません。is_virtuoso を truthy にすると両方が有効になり、define でエラーになっていました。
修正: 責務の分離
1 つのフラグを 3 つの設定に分けました。
| 設定 | 役割 |
|---|---|
endpoint_type | Virtuoso define ディレクティブの ON/OFF |
is_virtuoso | レスポンス形式 (output=json vs Accept ヘッダ) |
describe_as_construct | DESCRIBE → CONSTRUCT 自動変換 |
util.js — define ディレクティブを endpoint_type で判定
2
util.js — DESCRIBE → CONSTRUCT 自動変換
3
JPS の Virtuoso は DESCRIBE でサーバーエラー (SR452) を返すため、DESCRIBE <uri> を等価な CONSTRUCT に変換するオプションを追加しました。
各エンドポイントの設定
4
データフロー (修正後)
5
まとめ
SPARQL は標準仕様ですが、実装ごとの差異は意外と多いです。
| 差異 | Virtuoso | Fuseki |
|---|---|---|
| DESCRIBE レスポンス | 独自 JSON | W3C 準拠 RDF 形式 |
| PREFIX 解決 | 暗黙解決 | 明示的宣言が必要 |
独自拡張 (define) | あり | なし (パースエラーになる) |
これらを吸収するために is_virtuoso の二重責務を endpoint_type / is_virtuoso / describe_as_construct の 3 つに分離しました。新しいエンドポイント種別を追加する際は snorql_def.js にフラグを足すだけで、既存の動作は壊れません。
差異の発見には curl での Content-Type 確認と、ブラウザの Network タブが最も役立ちました。仕様上は同じはずの SPARQL でも、レスポンスヘッダを見ると実装の個性がよく分かります。