🐷

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を使用してこれを実装しました。

// src/app/api/auth/[...nextauth]/authOptions.ts
export const authOptions: NextAuthOptions = {
  providers: [
    {
      id: "gakunin",
      name: "GakuNin RDM",
      type: "oauth",
      clientId: process.env.GAKUNIN_CLIENT_ID,
      clientSecret: process.env.GAKUNIN_CLIENT_SECRET,
      authorization: {
        url: "https://accounts.rdm.nii.ac.jp/oauth2/authorize",
        params: {
          scope: "osf.full_read osf.full_write",
          response_type: "code",
        },
      },
      token: "https://accounts.rdm.nii.ac.jp/oauth2/token",
      userinfo: "https://api.rdm.nii.ac.jp/v2/users/me/",
    },
  ],
};

1.2 トークンの自動リフレッシュ

長時間のセッションを維持するため、アクセストークンの自動リフレッシュ機能を実装しました。

async jwt({ token, account, user }) {
  // 初回ログイン時
  if (account) {
    token.accessToken = account.access_token;
    token.refreshToken = account.refresh_token;
    token.expiresAt = account.expires_at;
  }

  // トークンの有効期限チェック(5分前にリフレッシュ)
  if (token.expiresAt) {
    const currentTime = Math.floor(Date.now() / 1000);
    const shouldRefresh = currentTime >= token.expiresAt - 300;

    if (shouldRefresh && token.refreshToken) {
      // リフレッシュトークンで新しいアクセストークンを取得
      const refreshedTokens = await refreshAccessToken(token.refreshToken);
      return {
        ...token,
        accessToken: refreshedTokens.access_token,
        refreshToken: refreshedTokens.refresh_token ?? token.refreshToken,
        expiresAt: Math.floor(Date.now() / 1000) + refreshedTokens.expires_in,
      };
    }
  }

  return token;
}

1.3 プロジェクトとファイルの取得

GakuNin RDM APIを通じて、ユーザーのプロジェクト一覧とファイルを取得します。

// プロジェクト一覧の取得
const response = await fetch(
  "https://api.rdm.nii.ac.jp/v2/users/me/nodes/?filter[category]=project",
  {
    headers: {
      Authorization: `Bearer ${session.accessToken}`,
    },
  }
);

// ストレージ内のファイル取得
const filesResponse = await fetch(
  `https://api.rdm.nii.ac.jp/v2/nodes/${projectId}/files/${provider}/`,
  {
    headers: {
      Authorization: `Bearer ${session.accessToken}`,
    },
  }
);

2. Dydra RDFデータベースの活用

2.1 非公開データの管理

Dydraは本来公開RDFデータベースですが、APIトークンを使用することで非公開データの管理が可能です。

環境変数の設定:

DYDRA_ACCOUNT=your-account
DYDRA_REPOSITORY=your-repository
DYDRA_API_TOKEN=your-secret-token

APIトークンを使用したクエリ:

const response = await fetch(
  `https://dydra.com/${account}/${repository}/sparql`,
  {
    method: 'POST',
    headers: {
      'Accept': 'application/sparql-results+json',
      'Authorization': `Bearer ${process.env.DYDRA_API_TOKEN}`,
    },
    body: new URLSearchParams({
      query: sparqlQuery,
    }),
  }
);

2.2 Named Graphによるデータ分離

Named Graphを使用することで、プロジェクトごとにデータを論理的に分離します。

Named Graphの設計:

GakuNin RDM APIから得られるリソースURIをそのままNamed Graph URIとして使用することで、データの出所とグラフの対応を明確にしています。

統合グラフ: https://api.rdm.nii.ac.jp/v2/nodes/{projectId}/

プロジェクトに関連するすべてのデータ(メタデータ、SKOS主題、プロファイル)を単一のNamed Graphに統合することで、以下のメリットがあります:

  • シンプルなクエリ: 複数グラフをUNIONで結合する必要がない
  • 効率的な検索: メタデータとSKOS主題の横断検索が容易
  • データの一貫性: すべてのデータが同じグラフ内で管理される
  • 外部システムとの相互運用性: RDMのリソースURIとRDFグラフの直接的な対応
  • 管理の簡素化: グラフURIの統一による運用負荷の軽減

例えば、メタデータのdc:subjectでSKOS概念を参照している場合、同一グラフ内なので直接JOINして主題ラベルを取得できます:

SELECT ?file ?title ?subjectLabel
FROM <https://api.rdm.nii.ac.jp/v2/nodes/{projectId}/>
WHERE {
  ?file dc:title ?title ;
        dc:subject ?subject .
  ?subject skos:prefLabel ?subjectLabel .
}

メタデータ登録時のNamed Graph指定:

const graphUri = `https://api.rdm.nii.ac.jp/v2/nodes/${projectId}/`;

const insertQuery = `
  PREFIX dc: <http://purl.org/dc/elements/1.1/>
  PREFIX dcterms: <http://purl.org/dc/terms/>

  INSERT DATA {
    GRAPH <${graphUri}> {
      <${resourceUri}> a dcterms:BibliographicResource ;
        dc:title "${metadata.title}" ;
        dc:creator "${metadata.creator}" ;
        dc:description "${metadata.description}" ;
        dc:subject <${metadata.subject}> .
    }
  }
`;

プロジェクト単位での検索:

const graphUri = `https://api.rdm.nii.ac.jp/v2/nodes/${projectId}/`;

const searchQuery = `
  PREFIX dc: <http://purl.org/dc/elements/1.1/>
  PREFIX dcterms: <http://purl.org/dc/terms/>

  SELECT ?resource ?title ?creator ?description
  FROM <${graphUri}>
  WHERE {
    ?resource a dcterms:BibliographicResource ;
      dc:title ?title ;
      dc:creator ?creator .
    OPTIONAL { ?resource dc:description ?description }
    FILTER(CONTAINS(LCASE(?title), LCASE("${keyword}")))
  }
`;

2.3 Dublin Coreメタデータスキーマ

Dublin Core 15要素を完全にサポートしています。

interface DublinCoreMetadata {
  title: string;        // dc:title
  creator: string;      // dc:creator
  subject: string;      // dc:subject
  description: string;  // dc:description
  publisher?: string;   // dc:publisher
  contributor?: string; // dc:contributor
  date?: string;        // dc:date
  type?: string;        // dc:type
  format?: string;      // dc:format
  identifier?: string;  // dc:identifier
  source?: string;      // dc:source
  language?: string;    // dc:language
  relation?: string;    // dc:relation
  coverage?: string;    // dc:coverage
  rights?: string;      // dc:rights
}

3. メタデータの登録と検索

3.1 メタデータ登録フロー

1. GakuNin RDMからファイル情報を取得
2. ユーザーがDublin Coreメタデータを入力
3. RDFトリプルに変換
4. DydraにSPARQL UPDATEで登録(Named Graph使用)

登録API実装:

export async function POST(request: NextRequest) {
  const session = await getServerSession(authOptions);
  if (!session?.accessToken) {
    return NextResponse.json({ error: "認証が必要です" }, { status: 401 });
  }

  const metadata = await request.json();

  // GakuNin RDM APIのプロジェクトURIをNamed Graph URIとして使用
  const graphUri = `https://api.rdm.nii.ac.jp/v2/nodes/${metadata.projectId}/`;

  // SPARQL INSERTクエリを構築
  const insertQuery = buildInsertQuery(metadata, graphUri);

  // Dydraに登録
  const response = await fetch(
    `https://dydra.com/${account}/${repository}/sparql`,
    {
      method: 'POST',
      headers: {
        'Content-Type': 'application/sparql-update',
        'Authorization': `Bearer ${process.env.DYDRA_API_TOKEN}`,
      },
      body: insertQuery,
    }
  );

  return NextResponse.json({ success: true });
}

3.2 高度な検索機能

複数のDublin Coreフィールドを横断したキーワード検索を実装しています。

PREFIX dc: <http://purl.org/dc/elements/1.1/>
PREFIX dcterms: <http://purl.org/dc/terms/>

SELECT DISTINCT ?resource ?title ?creator ?subject ?description
FROM <https://api.rdm.nii.ac.jp/v2/nodes/{projectId}/>
WHERE {
  ?resource a dcterms:BibliographicResource .
  OPTIONAL { ?resource dc:title ?title }
  OPTIONAL { ?resource dc:creator ?creator }
  OPTIONAL { ?resource dc:subject ?subject }
  OPTIONAL { ?resource dc:description ?description }

  # キーワードを複数フィールドで検索
  FILTER(
    CONTAINS(LCASE(?title), LCASE("keyword")) ||
    CONTAINS(LCASE(?creator), LCASE("keyword")) ||
    CONTAINS(LCASE(?subject), LCASE("keyword")) ||
    CONTAINS(LCASE(?description), LCASE("keyword"))
  )
}
ORDER BY ?title

4. SKOS主題階層管理

4.1 SKOS概念スキーマの実装

主題の階層構造を管理するため、SKOS(Simple Knowledge Organization System)を採用しました。

interface SKOSConcept {
  uri: string;
  prefLabel: string;
  broader?: string;  // 上位概念
  narrower?: string[]; // 下位概念
}

SKOS登録のSPARQLクエリ:

PREFIX skos: <http://www.w3.org/2004/02/skos/core#>

INSERT DATA {
  GRAPH <https://api.rdm.nii.ac.jp/v2/nodes/{projectId}/> {
    <{conceptUri}> a skos:Concept ;
      skos:prefLabel "{label}"@ja ;
      skos:broader <{broaderUri}> .
  }
}

4.2 階層構造の取得と主題ラベルの活用

統合グラフにより、メタデータと主題の関連を簡単にクエリできます:

PREFIX skos: <http://www.w3.org/2004/02/skos/core#>
PREFIX dc: <http://purl.org/dc/elements/1.1/>

SELECT ?concept ?label ?broader ?broaderLabel
FROM <https://api.rdm.nii.ac.jp/v2/nodes/{projectId}/>
WHERE {
  ?concept a skos:Concept ;
    skos:prefLabel ?label .
  OPTIONAL {
    ?concept skos:broader ?broader .
    ?broader skos:prefLabel ?broaderLabel .
  }
}
ORDER BY ?label

5. プロジェクトRDFエクスポート機能

5.1 プロジェクト全体のRDFデータ取得

統合グラフにより、すべてのデータ(メタデータ、SKOS主題、プロファイル)を単一のCONSTRUCTクエリでエクスポートできます。

export async function GET(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  const projectId = params.id;
  const graphUri = `https://api.rdm.nii.ac.jp/v2/nodes/${projectId}/`;

  // CONSTRUCT クエリでRDFグラフを取得(UNIONは不要)
  const query = `
    PREFIX dc: <http://purl.org/dc/elements/1.1/>
    PREFIX dcterms: <http://purl.org/dc/terms/>
    PREFIX skos: <http://www.w3.org/2004/02/skos/core#>
    PREFIX sh: <http://www.w3.org/ns/shacl#>

    CONSTRUCT {
      ?s ?p ?o
    }
    WHERE {
      GRAPH <${graphUri}> {
        ?s ?p ?o
      }
    }
  `;

  const response = await fetch(
    `https://dydra.com/${account}/${repository}/sparql`,
    {
      method: 'POST',
      headers: {
        'Accept': 'text/turtle',
        'Authorization': `Bearer ${process.env.DYDRA_API_TOKEN}`,
      },
      body: new URLSearchParams({ query }),
    }
  );

  const rdfData = await response.text();

  return new NextResponse(rdfData, {
    headers: {
      'Content-Type': 'text/turtle; charset=utf-8',
      'Content-Disposition': `attachment; filename="project_${projectId}.ttl"`,
    },
  });
}

6. UIコンポーネントの設計

6.1 メタデータエディタ

Dublin Core 15要素すべてを編集可能なフォームコンポーネントを実装しています。

const MetadataEditor = ({ fileId, projectId }: Props) => {
  const [metadata, setMetadata] = useState<DublinCoreMetadata>({
    title: '',
    creator: '',
    subject: '',
    description: '',
    // ... 他のフィールド
  });

  const handleSave = async () => {
    const response = await fetch('/api/metadata/register', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        ...metadata,
        fileId,
        projectId,
      }),
    });

    if (response.ok) {
      alert('メタデータを登録しました');
    }
  };

  return (
    <div className="space-y-4">
      {/* 15個のDublin Core入力フィールド */}
      <input
        value={metadata.title}
        onChange={(e) => setMetadata({ ...metadata, title: e.target.value })}
        placeholder="タイトル"
      />
      {/* ... */}
      <button onClick={handleSave}>保存</button>
    </div>
  );
};

6.2 検索インターフェース

キーワード検索とフィールド別検索の両方をサポートしています。

const SearchInterface = ({ projectId }: Props) => {
  const [keyword, setKeyword] = useState('');
  const [fieldSearch, setFieldSearch] = useState({
    title: '',
    creator: '',
    subject: '',
    // ...
  });

  const handleSearch = async () => {
    const response = await fetch(
      `/api/projects/${projectId}/search`,
      {
        method: 'POST',
        body: JSON.stringify({ keyword, ...fieldSearch }),
      }
    );
    const results = await response.json();
    setResults(results);
  };

  return (
    <div>
      <input
        value={keyword}
        onChange={(e) => setKeyword(e.target.value)}
        placeholder="キーワードで検索"
      />
      {/* フィールド別検索フォーム */}
      <button onClick={handleSearch}>検索</button>
    </div>
  );
};

7. セキュリティとパフォーマンス

7.1 認証とアクセス制御

  • すべてのAPIエンドポイントでセッション検証
  • アクセストークンの自動リフレッシュ
  • Named Graphによるプロジェクト単位のデータ分離

7.2 パフォーマンス最適化

  • Next.js App Routerによるサーバーサイドレンダリング
  • SPARQL クエリの最適化(OPTIONAL句の活用)
  • トークンリフレッシュの先行実行(有効期限5分前)

8. デプロイとインフラ

8.1 Vercelへのデプロイ

# 環境変数の設定
NEXT_PUBLIC_SITE_URL=https://your-domain.com
NEXTAUTH_URL=https://your-domain.com
NEXTAUTH_SECRET=your-secret
GAKUNIN_CLIENT_ID=your-client-id
GAKUNIN_CLIENT_SECRET=your-client-secret
DYDRA_ACCOUNT=your-account
DYDRA_REPOSITORY=your-repository
DYDRA_API_TOKEN=your-api-token

# デプロイ
vercel --prod

8.2 GakuNin RDM OAuth設定

リダイレクトURIの登録:

https://your-domain.com/api/auth/callback/gakunin

まとめ

本システムでは、以下の技術的課題を解決しました:

  1. OAuth 2.0認証の実装 - GakuNin RDMとの安全な連携
  2. トークン自動リフレッシュ - 長時間セッションの維持
  3. Named Graphによるデータ分離 - プロジェクト単位の管理
  4. 非公開RDFデータの管理 - APIトークンによるアクセス制御
  5. Dublin Core完全対応 - 標準メタデータスキーマの実装
  6. SKOS主題階層管理 - 構造化された主題分類
  7. SPARQL検索 - 柔軟なメタデータ検索

このシステムは、研究データ管理における実用的なソリューションとして、オープンサイエンスの推進に貢献します。

参考リンク

ソースコード

完全なソースコードは以下のリポジトリで公開しています:
https://github.com/nakamura196/next-dydra

Discussion