🐡

Odeuropa Visualization: SKOS語彙とSPARQLを活用した香りデータの可視化プラットフォーム

に公開

はじめに

Odeuropaは、ヨーロッパの香りの歴史を研究するプロジェクトで、絵画、文学、その他の歴史的資料に描かれた香りの表現を収集・分析しています。本記事では、OdeuropaのSPARQLエンドポイントを活用し、SKOS(Simple Knowledge Organization System)語彙体系に基づいた香りデータの可視化Webアプリケーションの実装について紹介します。

https://odeuropa-seven.vercel.app/ja/

プロジェクト概要

技術スタック

  • フロントエンド: Next.js 15 (App Router)
  • UI: Material-UI v5
  • 国際化: next-intl
  • データ取得: SPARQLクエリ (Odeuropa SPARQLエンドポイント)
  • 言語: TypeScript
  • ホスティング: 静的サイト生成(SSG)

主な機能

1. 香り検索 (/odeuropa-sources)

アプリケーションの中核となる機能で、Odeuropaプロジェクトが収集した香りの知覚イベント(smell perception events)を検索・閲覧できます。

主な特徴:

  • 複雑なSPARQLクエリによるデータ取得
    • 香り放出イベント(emission)、香りオブジェクト、ソース(絵画・文学作品など)、テキスト断片を結合
    • CRMベースのオントロジー(ecrm:P67_refers_to, od:F1_generatedなど)を活用
  • 多軸フィルタリング
    • SKOS語彙による香りの源でフィルタ(?xパラメータ)
    • ソースタイプフィルタ(視覚的アイテム E36_Visual_Item / 言語オブジェクト E33_Linguistic_Object
  • リッチな情報表示
    • 香りのラベル、ソース情報(タイトル、画像、URI)
    • テキスト断片の引用
    • 嗅覚体験の質的情報(Olfactory Experience)
  • ページネーション - 20件ずつの効率的な表示

SPARQLクエリ例:

SELECT DISTINCT ?source ?source_title ?fragment ?fragment_value
                ?emission ?smell ?smell_label ?source_image
WHERE {
    ?emission od:F3_had_source ?x .
    ?emission od:F1_generated ?smell .

    # ソースとの関連(フラグメント経由または直接)
    {
        ?fragment ecrm:P67_refers_to ?emission .
        ?fragment rdf:value ?fragment_value .
        ?source ecrm:P165_incorporates ?fragment .
    } UNION {
        ?source ecrm:P67_refers_to ?emission .
    }
}

2. 香り詳細ページ (/odeuropa-sources/item)

個別の香りに関する詳細情報を表示するページです。

表示情報:

  • 基本情報
    • 香りのURI、ラベル
    • Wikidataへのリンク(owl:sameAs
    • 香りの源(smell source)
  • 関連するソース
    • 複数のソース(絵画、文学作品など)を一覧表示
    • 各ソースの画像、タイトル、URI
    • テキスト断片の引用
    • 嗅覚体験の詳細情報
  • メタデータ
    • 時間情報(time:hasTime
    • 著者、作成日、言語

実装のポイント:

// URLパラメータから香りのURIを取得
const smellUri = searchParams.get('uri');

// 香り詳細を取得するSPARQLクエリ
const getSmellDetailQuery = (uri: string) => `
  SELECT ?smell ?smell_label ?smell_source ?source ?source_title ...
  WHERE {
    BIND(<${uri}> AS ?smell)
    # 香りに関連するすべての情報を取得
  }
`;

3. コンセプト一覧 (/concepts)

SKOS Top Conceptsを一覧表示し、香りの語彙体系の全体像を把握できます。

主な特徴:

  • SKOS Top Conceptsのみを表示(skos:topConceptOf
  • 画像、代替ラベル、Wikidataリンクなどのメタデータ表示
  • 子コンセプト数の表示
  • 階層ツリーへの遷移ボタン
  • アルファベット順でのソート

4. 階層ツリー (/hierarchy)

香りの源の階層構造を対話的に探索できます。

主な特徴:

  • 動的な階層構造の可視化
  • 遅延読み込みによる効率的なデータ取得
  • ?top=URIパラメータによる柔軟なルート指定
  • レベル別の色分け
  • 各ノードから香り検索へのリンク

5. コレクション (/collections)

SKOS Collectionsを表示し、テーマ別にグループ化された香りの源を閲覧できます。

主な特徴:

  • SKOS Collectionsの一覧表示
  • メンバー数の表示
  • コレクションメンバーの検索

Odeuropaのデータ構造とSPARQL

データモデルの概要

Odeuropaプロジェクトは、CIDOC-CRM(Conceptual Reference Model)を拡張した独自のオントロジー(Odeuropa Data Model)を使用しています。主要なエンティティと関係性は以下の通りです:

1. 香りのイベントモデル

# 香り放出イベント(Smell Emission)
?emission a od:L11_Smell_Emission ;
    od:F3_had_source ?smellSource ;      # 香りの源
    od:F1_generated ?smell ;              # 生成された香り
    time:hasTime ?timeInterval .          # 時間情報

# 香りオブジェクト(Smell)
?smell a od:L1_Smell ;
    rdfs:label ?smellLabel ;              # ラベル
    od:has_smell_source ?source .         # 香りの源への参照

# 香りの源(SKOS語彙)
?smellSource a skos:Concept ;
    skos:prefLabel ?prefLabel ;
    skos:broader ?broaderConcept ;
    skos:narrower ?narrowerConcept .

2. ソースとテキスト断片の関係

# ソース(絵画、文学作品など)
?source a ecrm:E36_Visual_Item ;          # または E33_Linguistic_Object
    rdfs:label ?sourceTitle ;
    schem:image ?sourceImage ;
    ecrm:P165_incorporates ?fragment .    # テキスト断片を含む

# テキスト断片
?fragment a ecrm:E33_Linguistic_Object ;
    rdf:value ?fragmentValue ;            # 実際のテキスト
    ecrm:P67_refers_to ?emission .        # 香り放出イベントへの参照

3. 嗅覚体験(Olfactory Experience)

# 嗅覚体験の割り当て
?experienceAssignment a ecrm:E13_Attribute_Assignment ;
    ecrm:P140_assigned_attribute_to ?smell ;
    ecrm:P141_assigned ?quality .

# 質的情報
?quality rdfs:label "pleasant"@en .

使用している主な語彙とプレフィックス

PREFIX schem: <https://schema.org/>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX ecrm: <http://erlangen-crm.org/current/>
PREFIX olf: <http://data.odeuropa.eu/vocabulary/olfactory-objects/>
PREFIX od: <http://data.odeuropa.eu/ontology/>
PREFIX skos: <http://www.w3.org/2004/02/skos/core#>
PREFIX time: <http://www.w3.org/2006/time#>
PREFIX owl: <http://www.w3.org/2002/07/owl#>

主要な語彙の説明:

プレフィックス 用途
od: Odeuropa独自のオントロジー(香り放出、香り生成など)
ecrm: CIDOC-CRM(文化遺産のメタデータ標準)
skos: SKOS(知識組織システム、香りの源の分類)
schem: Schema.org(画像、著者、日付などの基本メタデータ)
time: OWL-Time(時間情報)
owl: OWL(Wikidataなどへの同一性リンク)

SPARQLクエリの実装

1. 香り検索クエリの実装

香り検索機能では、複数のエンティティを横断する複雑なクエリを実行します。

基本的なクエリ構造

PREFIX od: <http://data.odeuropa.eu/ontology/>
PREFIX ecrm: <http://erlangen-crm.org/current/>
PREFIX skos: <http://www.w3.org/2004/02/skos/core#>

SELECT DISTINCT ?source ?source_title ?fragment ?fragment_value
                ?emission ?smell ?smell_label ?source_image
WHERE {
    # 1. 香り放出イベント(フィルタ条件)
    ?emission od:F3_had_source ?x .

    # 2. 香りオブジェクトの生成
    ?emission od:F1_generated ?smell .

    OPTIONAL {
        ?smell rdfs:label ?smell_label .
    }

    # 3. ソースとの関連(2つのパターン)
    {
        # パターン1: フラグメント経由
        ?fragment ecrm:P67_refers_to ?emission .
        ?fragment rdf:value ?fragment_value .
        ?source ecrm:P165_incorporates ?fragment .
        ?source rdfs:label ?source_title .
    } UNION {
        # パターン2: 直接参照
        ?source ecrm:P67_refers_to ?emission .
        ?source rdfs:label ?source_title .
    }

    # 4. ソース画像(オプション)
    OPTIONAL {
        ?source schem:image ?source_image .
        FILTER(STRSTARTS(STR(?source_image), "https://data.odeuropa.eu/image/"))
    }
}
LIMIT 20
OFFSET 0

フィルタリングの実装

SKOS語彙によるフィルタ(?xパラメータ):

# 指定されたコンセプトとその子孫でフィルタ
{
  ?x skos:broader* <http://data.odeuropa.eu/vocabulary/olfactory-objects/405> .
} UNION {
  # 指定されたコンセプト自体も含める
  FILTER(?x = <http://data.odeuropa.eu/vocabulary/olfactory-objects/405>)
}

skos:broader* はプロパティパスで、階層的な関係を再帰的にたどります。これにより「食べ物(Food)」を指定すると、「パン」「肉」などの子孫概念も含めて検索できます。

ソースタイプによるフィルタ:

# 視覚的アイテム(絵画など)のみ
?source rdf:type <http://erlangen-crm.org/current/E36_Visual_Item> .

# または言語オブジェクト(文学作品など)のみ
?source rdf:type <http://erlangen-crm.org/current/E33_Linguistic_Object> .

パフォーマンスの考慮

初期実装ではGROUP_CONCATを使用してデータを集約していましたが、パフォーマンス問題によりタイムアウトが発生しました。現在はシンプルなクエリを使用し、クライアント側でデータを処理しています:

// クライアント側でのデータマッピング
const items = data.results.bindings.map((binding) => ({
  emissionUri: binding.emission.value,
  sourceUri: binding.source?.value,
  sourceTitle: binding.source_title?.value,
  sourceImageUrl: binding.source_image?.value,
  smellLabel: binding.smell_label?.value,
  // ...
}));

2. 階層的な関係の取得

SKOS語彙の階層構造を取得するクエリです。Odeuropaのデータでは、概念間の階層関係が複数の方法で表現されています:

  1. skos:broader - 子から親への参照
  2. skos:narrower - 親から子への参照
  3. skos:topConceptOf - トップコンセプトの宣言

これらすべてに対応するため、UNIONを使用しています:

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

SELECT DISTINCT ?concept ?prefLabel ?lang (COUNT(DISTINCT ?child) as ?childCount)
WHERE {
  {
    # 子から親への参照
    ?concept skos:broader <${topUri}> .
  } UNION {
    # 親から子への参照
    <${topUri}> skos:narrower ?concept .
  } UNION {
    # トップコンセプト宣言
    ?concept skos:topConceptOf <${topUri}> .
  }

  # 語彙プレフィックスでフィルタ
  FILTER(STRSTARTS(STR(?concept), "http://data.odeuropa.eu/vocabulary/olfactory-objects/"))

  ?concept skos:prefLabel ?prefLabel .
  BIND(LANG(?prefLabel) AS ?lang)

  # 子コンセプトのカウント
  OPTIONAL {
    {
      ?child skos:broader ?concept .
      FILTER(STRSTARTS(STR(?child), "http://data.odeuropa.eu/vocabulary/olfactory-objects/"))
    } UNION {
      ?concept skos:narrower ?child .
      FILTER(STRSTARTS(STR(?child), "http://data.odeuropa.eu/vocabulary/olfactory-objects/"))
    }
  }
}
GROUP BY ?concept ?prefLabel ?lang
ORDER BY ?prefLabel

多言語対応

SKOSのラベルは言語タグ付きで格納されているため、クライアント側で適切な言語を選択します:

const label =
  concept.prefLabel[locale] ||           // ユーザーのロケール
  concept.prefLabel['en'] ||             // 英語(フォールバック)
  Object.values(concept.prefLabel)[0] || // 最初に利用可能な言語
  concept.uri.split('/').pop();          // URIの最後の部分

パフォーマンス最適化

1. 語彙プレフィックスによるフィルタリング

全体のデータセットから必要なデータのみを取得するため、FILTER(STRSTARTS(...))を使用:

FILTER(STRSTARTS(STR(?concept), "http://data.odeuropa.eu/vocabulary/olfactory-objects/"))

2. 遅延読み込み(Lazy Loading)

階層ツリーでは、初回表示時にトップレベルのみを取得し、ユーザーがノードを展開したときに初めて子ノードを取得します:

const handleToggle = async () => {
  if (!expanded && !node.loaded) {
    await onExpand(node);  // 子ノードを取得
  }
  setExpanded(!expanded);
};

3. クエリの事前表示

ユーザーがSPARQLクエリを確認できるよう、結果が返る前からクエリを表示します:

// クエリを即座に生成
const query = getConceptsQuery();

// コンポーネント内で直接使用
<SparqlQueryDisplay query={query} />

UIとUX

Material-UIによるモダンなデザイン

  • カード型レイアウトによる視認性の向上
  • ホバーエフェクトとトランジション
  • レスポンシブデザイン(Grid / Flexbox)
<Card
  sx={{
    height: '100%',
    display: 'flex',
    flexDirection: 'column',
    transition: 'all 0.3s',
    '&:hover': {
      boxShadow: 6,
      transform: 'translateY(-4px)',
    },
  }}
>

階層構造の可視化

  • 展開/折りたたみ可能なツリー構造
  • レベルごとの色分け
  • 子コンセプト数の表示
<Box
  sx={{
    borderLeft: 4,
    borderColor: `hsl(${level * 40}, 70%, 60%)`,
  }}
>

国際化(i18n)

next-intlを使用して英語・日本語に対応:

// サーバーコンポーネント
const t = await getTranslations('Header');

// クライアントコンポーネント
const t = useTranslations('ConceptsPage');

翻訳ファイルの構造:

{
  "ConceptsPage": {
    "title": "SKOS Concepts",
    "description": "Browse SKOS concepts from the Odeuropa vocabulary"
  }
}

ルーティングとナビゲーション

動的パラメータによる柔軟な表示

階層ツリーページでは、URLパラメータで起点となるコンセプトを指定可能:

// /hierarchy?top=http://data.odeuropa.eu/vocabulary/olfactory-objects/405
const topConceptUri = searchParams.get('top') || DEFAULT_TOP_CONCEPT_URI;

i18nルーティングの考慮

ロケール対応のルーティングには@/i18n/routinguseRouterを使用:

import { useRouter } from '@/i18n/routing';

const router = useRouter();
router.push(`/hierarchy/?top=${encodeURIComponent(conceptUri)}`);
// → /ja/hierarchy/?top=... または /en/hierarchy/?top=...

データモデル

TypeScript型定義

interface Concept {
  uri: string;
  prefLabel: { [lang: string]: string };
  altLabel: { [lang: string]: string[] };
  narrowerCount: number;
  inScheme?: string;
  topConceptOf?: string;
  sameAs?: string[];
  image?: string;
}

SPARQLレスポンスの処理

複数行にわたる同一コンセプトのデータをマージ:

const conceptMap = new Map<string, Concept>();

result.results.bindings.forEach((binding) => {
  const uri = binding.concept.value;

  if (!conceptMap.has(uri)) {
    conceptMap.set(uri, {
      uri,
      prefLabel: {},
      altLabel: {},
      narrowerCount: 0,
    });
  }

  const concept = conceptMap.get(uri)!;

  // 言語別にラベルを追加
  if (binding.prefLabel) {
    const lang = binding.lang?.value || 'en';
    concept.prefLabel[lang] = binding.prefLabel.value;
  }
});

今後の展望

  1. 検索機能の拡張

    • 全文検索
    • ファセット検索
    • 詳細フィルタ
  2. 可視化の強化

    • ネットワークグラフ
    • タイムライン表示
    • 地理的分布マップ
  3. データの充実

    • 他のSPARQLエンドポイントとの連携
    • Linked Open Dataの活用
    • ユーザー生成コンテンツ

まとめ

本プロジェクトでは、SKOS語彙とSPARQLを活用することで、複雑な階層構造を持つ香りのデータを効果的に可視化しました。セマンティックWebの標準技術を採用することで:

  • データの相互運用性が向上
  • 拡張性の高いアーキテクチャを実現
  • 多言語対応が容易に

今後も、ユーザビリティの向上とデータの充実を図りながら、香りの歴史研究に貢献するプラットフォームを目指します。

リンク


本記事は Claude Code によって生成されました。

Discussion