Virtuoso / Dydra 向けに作られた SPARQL Explorer「Snorql」を Apache Jena Fuseki でも動くようにしました。SPARQL は W3C 標準ですが、エンドポイント実装ごとの挙動差は意外と大きいです。Fuseki 対応で直面した 3 つの問題と、その解決方法を記録します。

開発環境

Docker で Fuseki を起動し、ローカルで検証しました。

#seodrflfovuuucisicpemskcemoonoeeeekanr-v--l-skrsigttiu:i-::eas"rAFmf-c:i:3oDUeudon0nMSssamse3mIE:etptr0eNKkaoa_:n_Ii:sin3tP_-ena0:ADd./m3SAayje0STtme:"WAalnOS:afRE/-uDTffs=_uuea1sskd=eeimtkkieiinst
d#coucr-klH-edr-'aXCtcoaoPn-mOtbpSeioTnnstae'-rhTyutyptp@pet-::ed/s/ttledoxactta/alt.hutortstltl:e3'030/test/data'

1. DESCRIBE のレスポンス形式が違う

症状

Fuseki に DESCRIBE クエリを投げると、結果が画面に表示されません。コンソールには JSON パースエラーが出ていました。

調査

SPARQL の DESCRIBE / CONSTRUCT は SELECT と違い、RDF グラフを返します。その形式がエンドポイントによって異なります。

エンドポイントDESCRIBE のレスポンスoutput= パラメータ
Virtuoso / DydraSPARQL Results JSON (独自拡張)
FusekiRDF 形式 (Turtle, RDF/JSON 等)Accept ヘッダより優先される

Snorql は以前から output=json を URL パラメータに付けて送信していました。Fuseki はこれを解釈して JSON-LD を返しますが、Snorql 側は RDF/JSON を期待しているため、パースが壊れます。

curl で切り分けると原因がはっきりします。

#c##c#uuor'or'ulhulhttCttCp-top-touHpnuHpnt:tt:t'/e='/eA/njA/ncltscltco-oco-ecTnecTpaypaytlptlp:he:heo:o:asasptaptaAp:pAp:pcl3pcl3pci0lci0lec3iec3ipa0cpa0ctt/att/aittittoeioeinsonso/tn/tnr//r//dsrdslfpdfpd+af+a+jr+jrjsqjsqsRolsoloDn?on?nF'qn'ouJuJeStSrOpOyNuN=-tDL=EDjSsCoRnI&BqEu+e<rhyt=tDpE:S/C/ReIxBaEm+p<lhet.topr:g///peexrasmopnl1e>.'org/person1>'

Fuseki ではoutput=json が邪魔をしていました。 Accept ヘッダの content negotiation に任せれば正しい形式が返ります。

修正 (5 箇所)

sparql.js — output パラメータの条件付き送信

-+uirfl(Q_uoeurtypSuttr)inugrl+Q=ueroyuSttpruitn=g'+=oouuttppuutt=+''&';output+'&';

_output が空文字なら output= を送らないようにしました。

sparql.js — Content-Type で JSON を判定

variisosJuJstsoponunt=?==(oJu'StjOpsNuo.tnp'a=r=?se'((jJxsShoOrnN.'.rpeasrpso(enx(shxerhT.reg.xertte)RsepspnosnesTeeHxeta)der('Content-Type').match(/json/i);

output パラメータを送っていなくても、レスポンスの Content-Type が json を含んでいれば JSON.parse します。Fuseki が application/rdf+json を返した場合に対応しています。

snorql.js — DESCRIBE 時の Accept ヘッダと output 制御

-++ii}ff((!!ttqqhhppiiaassrr..aahhmmoo..mmaoeecuddcteeepffpu..ttiiss==__vv""iia"rrp;ttpuulooisscooa))to{iuqotpnpa/urrtadmf.+ajcscoenp,tap=pl"iacpaptliiocna/tjisoonn/"r;df+json,jsonapplication/json";

typo の修正 (jsonapplication/json,application/json) と、output を空文字にして Fuseki の content negotiation を有効化しました。

snorql.js — 空文字の output を正しく伝播

-+iiff((oouuttppuutt)!s=e=rvuincdee.fsienteOdu)tpsuetr(voiuctep.uste)t;Output(output);

JavaScript の if(output) は空文字 "" を falsy と評価するため、!== undefined に変更しました。

snorql_def.js — endpoint_type の設定

-+eennddppooiinntt__ttyyppee::""vfiursteukois"o,",

2. PREFIX 宣言がないとクエリが通らない

症状

DESCRIBE は表示されるようになりましたが、ラベル取得や関連リソース検索が 400 エラーになります。

Parseerror:Unresolvedprefixedname:rdfs:label

原因

Snorql は DESCRIBE 表示後、ラベル取得等の補助クエリを内部的に発行します。これらは rdfs:labelschema:name 等のプレフィックス名をそのまま使っています。

エンドポイントPREFIX 宣言
Virtuosordfs: 等を暗黙解決 (宣言不要)
Fusekiすべて明示的な PREFIX 宣言が必要 (W3C 準拠)

Virtuoso の暗黙解決に依存していたコードが、Fuseki では動きませんでした。

修正

snorql_ldb.js — 補助クエリに PREFIX を自動付与

s}e,t_si}refer(tv!uitttttfrchhhhhaoneiiiiirr:sssssn(t.....anvhfsssssmsaiueeeeeersnrrrrrs=.cvvvvvppstiiiiiatfeicccccchxroeeeeeeivn)...ssii({=sss..ncmeeejaeentttspn;teMROpshweeu.)otqtsdShupnt)Poeuqh{AdstliR(t(.sQmH"_.Leejns.tasaeShdomreoenevrdr"siv()pci";aecAc.eces("csetGe;thEpPiTtrs""e.),fe;in"xda(pppopfilxni,tc)an;tsi[opnf/xs]p)a;rql-results+json,*/*");

SPARQL.Service.setPrefix() で名前空間を登録しておくと、クエリ送信時に PREFIX rdfs: <...> が自動で先頭に付きます。namespaces.js に定義済みの全プレフィックスを一括登録しました。

snorql_ldb.js — null ガード

d#coucr-klH-edr-'aXCtcoaoPn-mOtbpSeioTnnstae'-rhTyutyptp@pet-::ed/s/ttledoxactta/alt.hutortstltl:e3'030/test/data'

0

未定義プロパティが undefined として渡された場合の TypeError を防止しています。


3. is_virtuoso フラグが 2 つの仕事をしている

症状

Fuseki に加えて Dydra も並行運用しようとすると、Dydra で 400 エラーが出ました。

d#coucr-klH-edr-'aXCtcoaoPn-mOtbpSeioTnnstae'-rhTyutyptp@pet-::ed/s/ttledoxactta/alt.hutortstltl:e3'030/test/data'

1

原因

is_virtuoso フラグが 2 つの異なる責務を兼ねていました。

責務場所truthy のとき
レスポンス形式snorql.jsoutput=json を送信
Virtuoso 固有構文util.jsdefine sql:describe-mode "CBD" 等を付与

Dydra は output=json が必要 (Virtuoso と同じレスポンス形式) ですが、Virtuoso 固有の define は受け付けません。is_virtuoso を truthy にすると両方が有効になり、define でエラーになっていました。

修正: 責務の分離

1 つのフラグを 3 つの設定に分けました。

設定役割
endpoint_typeVirtuoso define ディレクティブの ON/OFF
is_virtuosoレスポンス形式 (output=json vs Accept ヘッダ)
describe_as_constructDESCRIBE → CONSTRUCT 自動変換

util.js — define ディレクティブを endpoint_type で判定

d#coucr-klH-edr-'aXCtcoaoPn-mOtbpSeioTnnstae'-rhTyutyptp@pet-::ed/s/ttledoxactta/alt.hutortstltl:e3'030/test/data'

2

util.js — DESCRIBE → CONSTRUCT 自動変換

d#coucr-klH-edr-'aXCtcoaoPn-mOtbpSeioTnnstae'-rhTyutyptp@pet-::ed/s/ttledoxactta/alt.hutortstltl:e3'030/test/data'

3

JPS の Virtuoso は DESCRIBE でサーバーエラー (SR452) を返すため、DESCRIBE <uri> を等価な CONSTRUCT に変換するオプションを追加しました。

各エンドポイントの設定

d#coucr-klH-edr-'aXCtcoaoPn-mOtbpSeioTnnstae'-rhTyutyptp@pet-::ed/s/ttledoxactta/alt.hutortstltl:e3'030/test/data'

4


データフロー (修正後)

d#coucr-klH-edr-'aXCtcoaoPn-mOtbpSeioTnnstae'-rhTyutyptp@pet-::ed/s/ttledoxactta/alt.hutortstltl:e3'030/test/data'

5

まとめ

SPARQL は標準仕様ですが、実装ごとの差異は意外と多いです。

差異VirtuosoFuseki
DESCRIBE レスポンス独自 JSONW3C 準拠 RDF 形式
PREFIX 解決暗黙解決明示的宣言が必要
独自拡張 (define)ありなし (パースエラーになる)

これらを吸収するために is_virtuoso の二重責務を endpoint_type / is_virtuoso / describe_as_construct の 3 つに分離しました。新しいエンドポイント種別を追加する際は snorql_def.js にフラグを足すだけで、既存の動作は壊れません。

差異の発見には curl での Content-Type 確認と、ブラウザの Network タブが最も役立ちました。仕様上は同じはずの SPARQL でも、レスポンスヘッダを見ると実装の個性がよく分かります。