Odeuropa Visualization: SKOS語彙とSPARQLを活用した香りデータの可視化プラットフォーム
はじめに
Odeuropaは、ヨーロッパの香りの歴史を研究するプロジェクトで、絵画、文学、その他の歴史的資料に描かれた香りの表現を収集・分析しています。本記事では、OdeuropaのSPARQLエンドポイントを活用し、SKOS(Simple Knowledge Organization System)語彙体系に基づいた香りデータの可視化Webアプリケーションの実装について紹介します。
プロジェクト概要
技術スタック
- フロントエンド: 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)
- SKOS語彙による香りの源でフィルタ(
-
リッチな情報表示
- 香りのラベル、ソース情報(タイトル、画像、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のデータでは、概念間の階層関係が複数の方法で表現されています:
-
skos:broader- 子から親への参照 -
skos:narrower- 親から子への参照 -
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/routingのuseRouterを使用:
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;
}
});
今後の展望
-
検索機能の拡張
- 全文検索
- ファセット検索
- 詳細フィルタ
-
可視化の強化
- ネットワークグラフ
- タイムライン表示
- 地理的分布マップ
-
データの充実
- 他のSPARQLエンドポイントとの連携
- Linked Open Dataの活用
- ユーザー生成コンテンツ
まとめ
本プロジェクトでは、SKOS語彙とSPARQLを活用することで、複雑な階層構造を持つ香りのデータを効果的に可視化しました。セマンティックWebの標準技術を採用することで:
- データの相互運用性が向上
- 拡張性の高いアーキテクチャを実現
- 多言語対応が容易に
今後も、ユーザビリティの向上とデータの充実を図りながら、香りの歴史研究に貢献するプラットフォームを目指します。
リンク
本記事は Claude Code によって生成されました。
Discussion