ホーム 記事一覧 ブック DH週間トピックス 検索 このサイトについて
English
SPARQL クライアントを Apache Jena Fuseki に対応させるときにハマった 3 つのこと

SPARQL クライアントを Apache Jena Fuseki に対応させるときにハマった 3 つのこと

Virtuoso / Dydra 向けに作られた SPARQL Explorer「Snorql」を Apache Jena Fuseki でも動くようにしました。SPARQL は W3C 標準ですが、エンドポイント実装ごとの挙動差は意外と大きいです。Fuseki 対応で直面した 3 つの問題と、その解決方法を記録します。 開発環境 Docker で Fuseki を起動し、ローカルで検証しました。 # docker-compose.yml services: fuseki: image: stain/jena-fuseki container_name: fuseki ports: - "3030:3030" environment: - ADMIN_PASSWORD=admin - FUSEKI_DATASET_1=test volumes: - fuseki-data:/fuseki volumes: fuseki-data: docker compose up -d # テストデータ投入 curl -X POST 'http://localhost:3030/test/data' \ -H 'Content-Type: text/turtle' \ --data-binary @testdata.ttl 1. DESCRIBE のレスポンス形式が違う 症状 Fuseki に DESCRIBE クエリを投げると、結果が画面に表示されません。コンソールには JSON パースエラーが出ていました。 ...

Dydra JSON-LDシリアライゼーションの挙動と回避策

Dydra JSON-LDシリアライゼーションの挙動と回避策

概要 Dydraは優れたクラウドベースのRDFトリプルストアですが、JSON-LDシリアライゼーションにおいて、一部のケースで期待と異なる出力が得られることがあります。このブログでは、その挙動と、我々が実装した回避策について解説します。 確認された挙動 期待される出力 JSON-LD仕様では、URI参照は以下のようにオブジェクト形式で出力されることが一般的です: { "@id": "https://example.com/item/1", "@type": ["prov:Entity"], "prov:wasAttributedTo": { "@id": "https://sepolia.etherscan.io/address/0x1234..." }, "prov:wasGeneratedBy": { "@id": "https://sepolia.etherscan.io/tx/0xabcd..." } } Dydraで確認された出力 DydraのJSON-LDエンドポイントでは、一部のURI参照が単なる文字列として出力されるケースが確認されました: { "@id": "https://example.com/item/1", "@type": ["prov:Entity"], "prov:wasAttributedTo": "https://sepolia.etherscan.io/address/0x1234...", "prov:wasGeneratedBy": "https://sepolia.etherscan.io/tx/0xabcd..." } 注意 : この挙動は全てのプロパティで発生するわけではなく、@contextの定義やプロパティの種類によって異なる場合があります。 挙動の違いによる影響 形式 JSON-LDパーサーの解釈 { "@id": "..." } URI参照(他ノードへのリンク) "..." リテラル文字列 この違いにより、以下の影響が生じる可能性があります: グラフ構造のトラバーサルに影響 一部のSPARQLクエリ結果に影響 JSON-LDフレーミング処理に影響 型付きリテラルについて 同様に、xsd:dateTime などの型付きリテラルでも型情報が省略されるケースがあります。 期待される出力 : { "prov:startedAtTime": { "@value": "2025-01-15T10:30:00Z", "@type": "xsd:dateTime" } } 確認された出力 : { "prov:startedAtTime": "2025-01-15T10:30:00Z" } 回避策 アプローチ:TTL形式で取得してJSON-LDを構築 DydraはTurtle (TTL) 形式では正確にシリアライズするため、以下の戦略を採用しました: [クライアント] │ │ Accept: text/turtle v [Dydra SPARQL Endpoint] │ │ TTL形式で返却 v [n3パーサー] │ │ Quadsに変換 v [JSON-LD構築ロジック] │ │ 正しいJSON-LD v [アプリケーション] 実装 import { Parser } from "n3"; /** * TTLをパースしてJSON-LDに変換 * DydraのJSON-LDシリアライゼーションの挙動を回避 */ function turtleToJsonLd(turtle: string): RDFGraph { const parser = new Parser(); const quads = parser.parse(turtle); // Subject別にトリプルをグループ化 const subjects = new Map<string, Map<string, unknown[]>>(); for (const quad of quads) { const subjectId = quad.subject.value; if (!subjects.has(subjectId)) { subjects.set(subjectId, new Map()); } const predicates = subjects.get(subjectId)!; const predicateId = quad.predicate.value; if (!predicates.has(predicateId)) { predicates.set(predicateId, []); } // オブジェクトの値を型情報付きで構築 let objectValue: unknown; if (quad.object.termType === "NamedNode") { // URI参照: { "@id": "..." } ← ここがポイント objectValue = { "@id": quad.object.value }; } else if (quad.object.termType === "Literal") { const literal = quad.object; if (literal.language) { // 言語タグ付きリテラル objectValue = { "@value": literal.value, "@language": literal.language }; } else if (literal.datatype && literal.datatype.value !== "http://www.w3.org/2001/XMLSchema#string") { // 型付きリテラル(xsd:string以外) objectValue = { "@value": literal.value, "@type": literal.datatype.value }; } else { // プレーンリテラル objectValue = literal.value; } } else if (quad.object.termType === "BlankNode") { objectValue = { "@id": `_:${quad.object.value}` }; } else { objectValue = quad.object.value; } predicates.get(predicateId)!.push(objectValue); } // JSON-LD @graphを構築 const graph: Array<Record<string, unknown>> = []; for (const [subjectId, predicates] of subjects) { const node: Record<string, unknown> = { "@id": subjectId }; for (const [predicateId, objects] of predicates) { if (predicateId === "http://www.w3.org/1999/02/22-rdf-syntax-ns#type") { // @typeは特別扱い node["@type"] = objects.map((o) => { if (typeof o === "object" && o !== null && "@id" in o) { return (o as { "@id": string })["@id"]; } return o; }); } else { // 単一値の場合は配列から取り出す node[predicateId] = objects.length === 1 ? objects[0] : objects; } } graph.push(node); } return { "@context": JSONLD_CONTEXT, "@graph": graph, }; } 使用例 // TTL形式で取得して変換 const response = await fetch(`${DYDRA_ENDPOINT}/sparql`, { method: "POST", headers: { "Accept": "text/turtle", // TTLで取得 }, body: query, }); const turtle = await response.text(); const jsonld = turtleToJsonLd(turtle); // JSON-LDに変換 依存ライブラリ この回避策には n3 ライブラリが必要です: ...

GakuNin RDMとDydraを連携したRDFメタデータ管理システムの開発

GakuNin RDMとDydraを連携したRDFメタデータ管理システムの開発

はじめに 本記事では、GakuNin RDM(Research Data Management)とDydra RDFデータベースを連携させた、研究データのメタデータ管理システムの開発について解説します。このシステムは、研究プロジェクトのファイル管理とDublin Coreメタデータの登録・検索を統合的に扱うことができます。 システム概要 アーキテクチャ ┌─────────────────┐ │ Next.js 14 │ │ (App Router) │ └────────┬────────┘ │ ┌────┴────┐ │ │ ┌───▼───┐ ┌──▼─────┐ │GakuNin│ │ Dydra │ │ RDM │ │ RDF │ │ API │ │ DB │ └───────┘ └────────┘ 主要技術スタック: Next.js 14 (App Router) NextAuth.js (OAuth 2.0認証) Dydra (RDFデータベース) GakuNin RDM API SPARQL (クエリ言語) 1. GakuNin RDMとの連携 1.1 OAuth 2.0認証の実装 GakuNin RDMはOAuth 2.0による認証をサポートしています。NextAuth.jsを使用してこれを実装しました。 ...

DydraへのAPI経由でのRDFデータ登録ガイド

DydraへのAPI経由でのRDFデータ登録ガイド

はじめに Dydraは、クラウドベースのRDFデータベースサービスで、SPARQL endpointとREST APIを提供しています。本記事では、Dydra APIを使用してプログラマティックにRDFデータを登録する方法を解説します。 前提条件 Dydraアカウント APIキー Node.js環境(v16以上推奨、Node.js使用時) 注意: 本記事のコード例では、サンプルとして以下を使用しています: アカウント名: your-account リポジトリ名: your-repository APIキー: your_api_key_here 実際に使用する際は、ご自身のDydraアカウント情報に置き換えてください。 APIの基本情報 エンドポイント構成 ベースURL: https://dydra.com/{account}/{repository} 例: https://dydra.com/your-account/your-repository SPARQL Query: /sparql (GET) SPARQL Update: /sparql (POST) Statements: /statements (POST/GET/DELETE) 認証 DydraではBearerトークン認証を使用します: Authorization: Bearer YOUR_API_KEY 実装方法 Dydra APIへのアクセスは、curl、Python、Node.jsなど様々な方法で実装できます。それぞれの方法を紹介します。 方法1: curlでの実装(最もシンプル) curlを使えば、プログラミング言語なしで即座にデータ登録が可能です。 基本的な認証 # APIキーを環境変数に設定 export DYDRA_API_KEY="your_api_key_here" export DYDRA_BASE_URL="https://dydra.com/your-account/your-repository" Turtle形式でのデータ登録 # data.ttl ファイルから登録 curl -X POST \ -H "Authorization: Bearer ${DYDRA_API_KEY}" \ -H "Content-Type: text/turtle" \ --data-binary @data.ttl \ "${DYDRA_BASE_URL}/statements" data.ttl の例: ...

「教科書の中の源氏物語LOD」を使ってみる

「教科書の中の源氏物語LOD」を使ってみる

概要 「教科書の中の源氏物語LOD」を使ってみましたので、備忘録です。 https://linkdata.org/work/rdf1s10294i 以下のように説明されています。 教科書の中の源氏物語LODは、高等学校古典分野の戦後検定教科書における『源氏物語』掲載データをLOD化したものである。 「教科書の中の源氏物語LOD」を作成および公開してくださった関係者の皆様に感謝いたします。 SPARQLエンドポイントの作成 今回はDyDraを使用します。また、以下の記事を参考に、Pythonで登録しました。 DYDRA_ENDPOINT=https://dydra.com/ut-digital-archives/genji_u/sparql DYDRA_API_KEY=xxxxx from dydra_py.api import DydraClient endpoint, api_key = DydraClient.load_env("../.env") client = DydraClient(endpoint, api_key) # genjimaki_listの登録 client.import_by_file("./data/genjimaki_list_ttl.txt", "turtle") # genjitext_listの登録 client.import_by_file("./data/genjitext_list_ttl.txt", "turtle") 注意点として、RDF内のURIについて、http://linkdata.org/resource/rdf1s10294i#とhttps://linkdata.org/resource/rdf1s10294i#が一部混在しておりました。今回は、http://linkdata.org/resource/rdf1s10294i#に統一する置換処理を施したのち、SPARQLエンドポイントに登録しました。 Snorqlによる確認 構築したSPARQLエンドポイントに対して問い合わせを行うSnorqlを作成しました。 https://nakamura196.github.io/snorql_examples/genji/ 例えば以下では、桐壺巻が使用されている教科書がschema:workExampleで関連付けられています。 https://nakamura196.github.io/snorql_examples/genji/?describe=http%3A%2F%2Flinkdata.org%2Fresource%2Frdf1s10294i%23genji_01 また以下では、教科書「高等古文 3」に掲載されている巻がdct:hasPartで関連づけられています。 https://nakamura196.github.io/snorql_examples/genji/?describe=http%3A%2F%2Flinkdata.org%2Fresource%2Frdf1s10294i%23text_001 Yasguiを用いた可視化 さらに、Yasguiを用いた可視化も試みました。Yasguiについては、以下も参考にしてください。 教科書毎の巻数のカウント 詳細 PREFIX dct: <http://purl.org/dc/terms/> SELECT ?textTitle ?publisher (count(?volume) as ?count) ?text WHERE { ?text dct:hasPart ?volume; dct:title ?textTitle; dct:publisher ?publisher } GROUP BY ?text ?textTitle ?publisher ORDER BY desc(?count) 巻毎の教科書数のカウント 詳細 PREFIX dct: <http://purl.org/dc/terms/> PREFIX schema: <http://schema.org/> SELECT ?chapterTitle (count(?text) as ?count) WHERE { ?chapter schema:workExample ?text; dct:title ?chapterTitle } GROUP BY ?chapterTitle ORDER BY desc(?count) 「桐壺」が最も多く含まれていることが分かります。 検定年毎の教科書数 詳細 PREFIX jp-textbook: <https://w3id.org/jp-textbook/> SELECT (str(?year) as ?year) (count(?year) as ?count) WHERE { ?text jp-textbook:authorizedYear ?year . } GROUP BY ?year ORDER BY asc(?year) ...

Pythonを使ってRDFデータをDydraに登録する

Pythonを使ってRDFデータをDydraに登録する

概要 Pythonを使ってRDFデータをDydraに登録するライブラリを作成しました。 https://github.com/nakamura196/dydra-py 中途半端な実装が含まれますが、お役に立つ場面があれば幸いです。 工夫 インポートは以下で行なっています。 https://github.com/nakamura196/dydra-py/blob/main/dydra_py/api.py#L55 以下のように、SPARQLのINSERT DATA オペレーションを使用しています。 def import_by_file(self, file_path, format, graph_uri=None, verbose=False): """ Imports RDF data from a file into the Dydra store. Args: file_path (str): The path to the RDF file to import. format (str): The format of the RDF file (e.g., 'xml', 'nt'). graph_uri (str, optional): URI of the graph where data will be inserted. Defaults to None. """ headers = { "Authorization": f"Bearer {self.api_key}", "Content-Type": "application/sparql-update" } files = self._chunk_rdf_file(file_path, format=format) print("Number of chunks: ", len(files)) for file in tqdm(files): # RDFファイルの読み込み graph = rdflib.Graph() graph.parse(file, format=format) # フォーマットはファイルに応じて変更 nt_data = graph.serialize(format='nt') if graph_uri is None: query = f""" INSERT DATA {{ {nt_data} }} """ else: query = f""" INSERT DATA {{ GRAPH <{graph_uri}> {{ {nt_data} }} }} """ if verbose: print(query) response = requests.post(self.endpoint, data=query, headers=headers) if response.status_code == 200: print("Data successfully inserted.") else: print(f"Error: {response.status_code} {response.text}") 工夫 工夫した点として、サイズが大きいRDFファイルを一度にアップロードした際、プロセスが途中で止まってしまうケースがありました。 ...

学習指導要領LODを使う

学習指導要領LODを使う

概要 学習指導要領LODは以下のように説明されています。 学習指導要領LODは、文部科学省が公開している学習指導要領と教育要領の内容・コードおよび関連する情報をLinked Open Data (LOD) として公開します。LOD化の対象は、現在公開されている全ての学校種別の新旧学習指導要領と教育要領(一部改正を含む)コード表の最新版です。 https://jp-cos.github.io/ このデータセットを使う機会がありましたので、使い方に関する備忘録です。 SPARQLエンドポイントの構築 今回は、DYDRAを使用します。 https://dydra.com/ 以下が登録した結果です。 https://dydra.com/ut-digital-archives/jp-cos/ Snorqlの作成 SPARQLエンドポイントを使いやすくするため、Snorqlを用意しました。 https://sukilam-educational-metadata.github.io/etc/jp-cos/ 検索例 以下のクエリにより、「学習指導要領(jp-cos:CourseOfStudy)」毎の「細目(jp-cos:Item)」の数を把握することができます。 https://sukilam-educational-metadata.github.io/etc/jp-cos/?query=PREFIX+rdf%3A+<http%3A%2F%2Fwww.w3.org%2F1999%2F02%2F22-rdf-syntax-ns%23> PREFIX+jp-cos%3A+<https%3A%2F%2Fw3id.org%2Fjp-cos%2F> SELECT+%3FcourseOfStudy+(count(distinct+%3Fs)+as+%3Fcount)++WHERE+{ ++%3FcourseOfStudy+rdf%3Atype+<https%3A%2F%2Fw3id.org%2Fjp-cos%2FCourseOfStudy>+.+ ++%3Fs+jp-cos%3AcourseOfStudy+%3FcourseOfStudy+.+ }+ GROUP+BY+%3FcourseOfStudy ORDER+BY+desc(%3Fcount) Yasguiで可視化する 以下がYasuiでの可視化例です。UpperSecondary(高等学校, jp-cos:school/UpperSecondary)の細目が多いようです。 https://api.triplydb.com/s/Vhi86jwiH まとめ 学習指導要領LODの作成者の方々に感謝いたします。 学習指導要領LODの利用にあたり、参考になりましたら幸いです。