SaaSを題材にしたオントロジーを作ってみようシリーズ #3.5 番外編 セマンティックレイヤー

作ったオントロジーは、結局どこで使うのか?

セマンティックレイヤーという考え方を、手を動かして理解する

今回は番外編です。新しいクラスは1つも追加しません。

前回は、FeatureとEntitlementを導入しました。あわせて、Tenantから見たhasEntitlementと、Entitlementから見たentitledTenantという、逆プロパティのペアも定義しました。

ここでいったんクラスを増やす手を止めて、そこまでに作った語彙が実際どこで役に立つのかを確かめます。というのも、ここまで3回クラスを増やしてきて、そろそろこう思っている方がいるはずだからです。

これ、作ったはいいけど、結局どこでどう使うの?

たとえば、実際の業務では次のような問いが出てきます。

株式会社ABCというテナントは、SaaSの機能としての販売管理機能を使えるのか?

この質問に答えるために、あなたは今、どうしますか?

・請求管理システムの契約画面を開いて確認する?
・社内の管理DBにSQLを投げる?
・Slackでカスタマーサクセスに聞く?

どの方法でも答えは出ます。しかし、問いの意味自体は、どの方法で調べても同じはずです。

このTenantに、
販売管理Featureに対する有効なEntitlementが存在するか?

これが、問いの正体です。第3回でFeatureとEntitlementを定義したのは、まさにこの「問いの正体」を書けるようにするためでした。

そして、この「意味の定義」を、複数のシステムから共通で使える形にしたものが、セマンティックレイヤーです。


1. セマンティックレイヤーとは何か

一言でいうと、

セマンティックレイヤー:
    データの意味を一箇所に定義し、
    どのツール・どのシステムからでも
    同じ意味で問い合わせられるようにする仕組み

もう少し具体的にいきます。

セマンティックレイヤーが無い世界:

    請求システム  →  grantedFeature という項目で機能を持つ
    CRM           →  enabledFeature という項目で機能を持つ
    社内DB        →  entitlements テーブルの feature_id カラム

    3つとも同じことを記録しているのに、
    呼び名がバラバラで、横断して聞けない。

セマンティックレイヤーがある世界:

    共通語彙 saas:entitlesFeature
    ├── 請求システムの grantedFeature
    ├── CRMの enabledFeature
    └── 社内DBの feature_id

    どのシステムのデータでも、1つの問いで横断できる。

セマンティックレイヤーの本質は、「どのカラムに何が入っているか」ではなく、「それが何を意味するか」を定義することです。

「レイヤー(層)」と呼ぶのは、これが問い合わせる側とデータの間に挟まる層だからです。

問い合わせる側(人、BIツール、AI)
    │  共通の語彙で問う
    ↓
セマンティックレイヤー(意味の定義と、各システムとの対応)
    │  各システムの書き方の違いを、ここで吸収する
    ↓
各システムのデータ(請求システム、CRM、社内DB…)

間に層が1枚あるので、問い合わせる側は、下にあるデータがどう書かれているかを知らなくても済みます。

そして今回は、この「バラバラの語彙を1つの問いで横断する」ところまで、実際に手元で動かします。

ところで、「セマンティックレイヤー」で検索すると、たぶん次のようなものが出てきます。

・Snowflake の Semantic Views
・Databricks の Metric Views(Unity Catalog semantics)
・Power BI / Microsoft Fabric の Semantic Model
・dbt Semantic Layer、Cube、Looker の LookML

そして、そこに書かれているのは、だいたいこういう話です。

「売上」「アクティブユーザー数」の計算式を1箇所で定義して、
どのBIツールからでも同じ数字が出るようにしましょう。

一方、このシリーズで作っているのは、Turtleで書いたクラスとプロパティです。計算式は1つも出てきません。

あれ?
このシリーズの「セマンティックレイヤー」と、
世の中の「セマンティックレイヤー」、
同じものの話をしている?

結論から書きます。

同じ言葉で、2つの系統のものが呼ばれている。

    メトリクス型:数字の「計算のしかた」を定義する
    オントロジー型:世界の「何が何であるか」を定義する

このシリーズが作っているのは、後者。

この記事では、まずこの2つの違いを整理してから、オントロジー型のほうを実際に動かします。後半で手を動かしているときに「あれ?」とならないためです。

製品の名称や提供状況は動きが速いため、本記事の内容は2026年10月時点の公式ドキュメントに基づいています。出典は末尾にまとめました。

2つの系統とも、上で書いた「データの意味を一箇所に定義する」という点は同じです。違うのは、「意味」として何を定義するかです。

メトリクス型が定義する「意味」:
    「月間アクティブテナント数」とは、
    subscriptions テーブルの status = 'active' の行を
    tenant_id で重複排除して数えたもの

オントロジー型が定義する「意味」:
    「Tenant」とは、Subscription を持ち、
    Entitlement を通じて Feature を利用できる単位であり、
    hasEntitlement と entitledTenant は互いに逆向きの関係である

前者は数字の作り方、後者はものごとの関係を定義しています。

この違いがどこから来たのかを見ていきます。


2. メトリクス型セマンティックレイヤーは、なぜ生まれたか

2.1 出発点は「数字が合わない」問題

メトリクス型が解こうとしてきた問題は、とても具体的です。

営業のダッシュボード:  今月のアクティブテナント  128社
CSのダッシュボード:    今月のアクティブテナント  131社
経営会議の資料:        今月のアクティブテナント  125社

3つとも間違ってはいません。トライアル中を含めるか、解約予定を含めるか、月末時点か月中の最大か。計算式が、ダッシュボードごとに少しずつ違うのです。

そこで、「計算式を1箇所に集めて、全員がそれを使う」という発想が生まれました。

2.2 置き場所の変遷

この「計算式の置き場所」は、時代とともに移動してきました。

第1世代: BIツールの中
    Looker の LookML、Power BI のデータセット(現 Semantic Model)
    → もともと、そのBIツールのために設計された。
      いまは外部からも使えるが(Looker の Open SQL Interface、
      Power BI の XMLA エンドポイントなど)、
      ツール固有の言語や接続方式に合わせる必要がある。

第2世代: BIツールから独立(ヘッドレス)
    dbt Semantic Layer(MetricFlow)、Cube、AtScale
    → どのBIツールからでも、APIやSQLで同じ定義を使える。
      (dbt の API は dbt platform で提供される)

第3世代: データ基盤の中
    Snowflake Semantic Views、Databricks Metric Views
    → データと同じ場所で、同じ権限管理の下に置かれる。

Microsoft Fabric の Semantic Model は、第1世代の Power BI の Semantic Model と同じものかと思います。ただ、Fabric 上では Direct Lake という方式で OneLake のデータを直接読めるので、第1世代と第3世代の中間にあたります。

なお、この「世代」は説明のための筆者の整理で、業界やベンダーの公式な区分ではありません。新しい世代ほど優れている、という意味でもありません。いま「セマンティックレイヤー」として話題になっているのが、主に第3世代だということを表現したいため、この整理をしました。

2.3 実際には、こう書く

第1回で作ったTenant・Subscription・Planが、ふつうのテーブルとしてデータウェアハウスに入っているとして、具体的に見てみます。

tenants       (tenant_id, name)
subscriptions (subscription_id, tenant_id, plan_id, status, started_at)
plans         (plan_id, plan_name)

ここで、次の問いを考えます。

Q1. プラン別に、毎月いくつ新しい契約が始まっているか?

※Q2は9章で出てきます。

SnowflakeのSemantic Viewでは、こう定義します。

-- あくまでもイメージで、動作保証はないです
CREATE SEMANTIC VIEW subscription_sv
  TABLES (
    subscriptions PRIMARY KEY (subscription_id),
    plans         PRIMARY KEY (plan_id)
  )
  RELATIONSHIPS (
    subscriptions (plan_id) REFERENCES plans
  )
  DIMENSIONS (
    plans.plan_name           AS plans.plan_name,
    subscriptions.start_month AS DATE_TRUNC('MONTH', subscriptions.started_at)
  )
  METRICS (
    subscriptions.new_subscriptions AS COUNT(DISTINCT subscriptions.subscription_id)
  )
  COMMENT = 'プラン別・月別の新規契約';

使う側は、計算式を知らなくても、こう問い合わせるだけです。

SELECT plan_name, start_month, AGG(new_subscriptions)
FROM subscription_sv
GROUP BY plan_name, start_month;

DatabricksのMetric Viewでは、YAMLで同じことを書きます。

-- あくまでもイメージで、動作保証はないです
CREATE VIEW main.saas.subscription_metrics
WITH METRICS LANGUAGE YAML AS $$
version: 1.1
source: main.saas.subscriptions
joins:
  - name: plan
    source: main.saas.plans
    on: source.plan_id = plan.plan_id
fields:
  - name: plan_name
    expr: plan.plan_name
  - name: start_month
    expr: DATE_TRUNC('MONTH', started_at)
measures:
  - name: new_subscriptions
    expr: COUNT(DISTINCT subscription_id)
$$;

SELECT plan_name, start_month, MEASURE(new_subscriptions)
FROM main.saas.subscription_metrics
GROUP BY ALL;

書き方は違いますが、やっていることは同じです。

2.4 メトリクス型の「共通の形」

製品は違っても、中身の構成要素はほぼ共通しています。

論理テーブル     どのテーブルを使うか
リレーション     テーブル同士をどう結合するか
ディメンション   どの切り口で分けるか(プラン別、月別…)
メジャー         何をどう集計するか(COUNT, SUM, 比率…)
同義語・説明     人間とAIのための補足

そして、問い合わせ方も共通しています。

Snowflake   AGG(new_subscriptions)
Databricks  MEASURE(new_subscriptions)
Cube        MEASURE(...)
Looker      AGGREGATE(...)

AGG()やMEASURE()は、「定義側に書いてある集計式(ここではCOUNT(DISTINCT …))で集計して」という指示です。使う側は、COUNTやSUMを自分で書く必要がありません。

集計のしかたは定義側が持ち、使う側は切り口を選ぶだけ。これがメトリクス型の設計の核です。

この「共通の形」を標準にする動きも進んでいます。2025年9月、Open Semantic Interchange(OSI)が立ち上がり、2026年1月にはYAML/JSONで記述する仕様の初版(v0.1)が公開されました。2026年7月にはApache Incubatorに入り、Apache Ossie(incubating)に改称されています。

2026年1月の初版仕様はRDF/OWLに触れられていませんでしたが、2026年5月にはオントロジー仕様の草案が出ており、7月のApache移管の時点では『セマンティックレイヤーとオントロジーの両方のための仕様』と明示的に説明されています。


3. オントロジー型セマンティックレイヤー

もう1つの系統は、このシリーズで作っているものです。

オントロジー型が定義するもの:

    クラス         Tenant, Subscription, Plan, Feature, Entitlement
    プロパティ     hasSubscription, basedOnPlan, hasEntitlement
    関係の性質     hasEntitlement と entitledTenant は互いに逆向き
    階層           ある関係は、別の関係の一種である(7章で実例を見ます)
    制約           Entitlement には必ず Tenant と Feature が1つずつ必要

ルーツは、W3Cが標準化したSemantic Webの技術(RDF、OWL、SPARQL、SHACL)です。データベースのテーブルではなく、「主語・述語・目的語」の3つ組(トリプル)で世界を記述します。

メトリクス型との一番の違いは、定義から新しい事実を導けること(推論) と、どのテーブルにも属さない「意味の対応」を書けることです。「意味の対応」とは、「請求システムのこの項目と、CRMのこの項目は同じ意味だ」という取り決めのことで、7章で実際に書きます。

メトリクス型:
    定義は「テーブルの上」に乗っている。
    テーブルが先にあり、その上に計算式を載せる。

オントロジー型:
    定義は「テーブルから独立」している。
    意味の体系が先にあり、各システムのデータをそこに接続する。

このシリーズで作っているのは、こちらです。

「Tenantとは何か」
「FeatureとEntitlementはどう違うか」
「PlanとFeatureの関係は何か」

このあたりは、言葉で説明するより動かしたほうが早いので、このあと実際に手を動かして確かめます。


4. T-BoxとA-Box

オントロジーを実際に使うとき、記述は2つに分かれます。

T-Box(Terminology Box)= 語彙の定義
    「Tenantというクラスが存在する」
    「hasEntitlementはTenantからEntitlementへの関係である」
    「entitledTenantはhasEntitlementの逆方向である」

A-Box(Assertion Box)= 事実の記述
    「ex:tenant-abcはTenantである」
    「ex:entitlement-abc-csv-exportの対象テナントはex:tenant-abcである」

たとえで言うと、こうです。

T-Box = 言語のルールブック
A-Box = そのルールで書かれた文章

第1回〜第3回で書いてきた「オントロジー定義のTurtle」がT-Box、「具体的なデータのTurtle」がA-Boxです。

ここで大事なのは、セマンティックレイヤーの本体はT-Boxのほうだという点です。A-Boxはデータなので日々変わりますが、T-Boxは「意味の取り決め」なので、そう頻繁には変わりません。

では、そのT-Boxは具体的に何の役に立つのでしょうか。ここからは、手を動かして確認します。


5. 手を動かす準備

5.1 インストール

pip install rdflib owlrl

rdflibは、これまでの記事でも使ったRDF操作ライブラリです。owlrlは、そのrdflibの上でOWL/RDFSの推論を行うライブラリです。推論とは、T-Boxに書かれたルールを使って、データに書かれていない事実を導き出すことを指します。今回はこの推論が主役になります。

5.2 T-Boxを保存する

ontology.ttlとして保存します。第1回〜第3回で定義した語彙のうち、今回使う分だけの抜粋です。

# ontology.ttl — T-Box(語彙の定義)
@prefix saas: <https://example.com/ontology/saas#> .
@prefix rdf:  <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix xsd:  <http://www.w3.org/2001/XMLSchema#> .

#
# Classes
#

saas:Tenant a owl:Class ;
    rdfs:label "Tenant"@en , "テナント"@ja .

saas:Plan a owl:Class ;
    rdfs:label "Plan"@en , "プラン"@ja .

saas:Subscription a owl:Class ;
    rdfs:label "Subscription"@en , "契約"@ja .

saas:Feature a owl:Class ;
    rdfs:label "Feature"@en , "機能"@ja .

saas:Entitlement a owl:Class ;
    rdfs:label "Entitlement"@en , "機能利用資格"@ja .

#
# Tenant and Subscription
#

saas:hasSubscription a owl:ObjectProperty ;
    rdfs:domain saas:Tenant ;
    rdfs:range saas:Subscription ;
    rdfs:label "has subscription"@en .

saas:basedOnPlan a owl:ObjectProperty ;
    rdfs:domain saas:Subscription ;
    rdfs:range saas:Plan ;
    rdfs:label "based on plan"@en .

#
# Plan and Feature
#

saas:includesFeature a owl:ObjectProperty ;
    rdfs:domain saas:Plan ;
    rdfs:range saas:Feature ;
    rdfs:label "includes feature"@en .

#
# Tenant and Entitlement(第3回で定義した逆プロパティのペア)
#

saas:hasEntitlement a owl:ObjectProperty ;
    rdfs:domain saas:Tenant ;
    rdfs:range saas:Entitlement ;
    rdfs:label "has entitlement"@en ,
               "機能利用資格を持つ"@ja .

saas:entitledTenant a owl:ObjectProperty ;
    owl:inverseOf saas:hasEntitlement ;
    rdfs:domain saas:Entitlement ;
    rdfs:range saas:Tenant ;
    rdfs:label "entitled tenant"@en ,
               "利用資格の対象テナント"@ja .

saas:entitlesFeature a owl:ObjectProperty ;
    rdfs:domain saas:Entitlement ;
    rdfs:range saas:Feature ;
    rdfs:label "entitles feature"@en ,
               "利用可能にする機能"@ja .

#
# Datatype Properties
#

saas:name a owl:DatatypeProperty ;
    rdfs:range xsd:string .

saas:featureCode a owl:DatatypeProperty ;
    rdfs:domain saas:Feature ;
    rdfs:range xsd:string .

saas:entitlementStatus a owl:DatatypeProperty ;
    rdfs:domain saas:Entitlement ;
    rdfs:range xsd:string .

今回の主役は、この1行です。

saas:entitledTenant a owl:ObjectProperty ;
    owl:inverseOf saas:hasEntitlement ;

「entitledTenantはhasEntitlementの逆プロパティである」という宣言です。第3回で定義したものを、そのまま持ってきています。

5.3 A-Boxを保存する

data.ttlとして保存します。第3回のインスタンス例と同じく、Entitlement側からsaas:entitledTenantでTenantを指す書き方です。

# data.ttl — A-Box(実データ)
@prefix saas: <https://example.com/ontology/saas#> .
@prefix ex:   <https://example.com/data/> .
@prefix xsd:  <http://www.w3.org/2001/XMLSchema#> .

#
# Tenant
#

ex:tenant-abc a saas:Tenant ;
    saas:name "株式会社ABCテナント" ;
    saas:hasSubscription ex:subscription-abc-standard .

#
# Subscription
#

ex:subscription-abc-standard a saas:Subscription ;
    saas:basedOnPlan ex:plan-standard .

#
# Plans
#

ex:plan-standard a saas:Plan ;
    saas:name "Standard Plan" ;
    saas:includesFeature
        ex:feature-project-management ,
        ex:feature-csv-export ,
        ex:feature-api-access .

ex:plan-enterprise a saas:Plan ;
    saas:name "Enterprise Plan" ;
    saas:includesFeature
        ex:feature-project-management ,
        ex:feature-csv-export ,
        ex:feature-api-access ,
        ex:feature-saml-sso ,
        ex:feature-audit-log ,
        ex:feature-sales-management ,
        ex:feature-inventory-management .

#
# Features
#

ex:feature-project-management a saas:Feature ;
    saas:name "基本プロジェクト管理" ;
    saas:featureCode "core.project-management" .

ex:feature-csv-export a saas:Feature ;
    saas:name "CSVエクスポート" ;
    saas:featureCode "data.csv-export" .

ex:feature-api-access a saas:Feature ;
    saas:name "APIアクセス" ;
    saas:featureCode "integration.api-access" .

ex:feature-saml-sso a saas:Feature ;
    saas:name "SAML SSO" ;
    saas:featureCode "security.saml-sso" .

ex:feature-audit-log a saas:Feature ;
    saas:name "監査ログ" ;
    saas:featureCode "security.audit-log" .

ex:feature-sales-management a saas:Feature ;
    saas:name "販売管理" ;
    saas:featureCode "sales.management" .

ex:feature-inventory-management a saas:Feature ;
    saas:name "在庫管理" ;
    saas:featureCode "inventory.management" .

#
# Entitlements(Standard Plan由来。販売管理機能に対するEntitlementは存在しない)
#

ex:entitlement-abc-project-management a saas:Entitlement ;
    saas:entitledTenant ex:tenant-abc ;
    saas:entitlesFeature ex:feature-project-management ;
    saas:entitlementStatus "active" .

ex:entitlement-abc-csv-export a saas:Entitlement ;
    saas:entitledTenant ex:tenant-abc ;
    saas:entitlesFeature ex:feature-csv-export ;
    saas:entitlementStatus "active" .

ex:entitlement-abc-api-access a saas:Entitlement ;
    saas:entitledTenant ex:tenant-abc ;
    saas:entitlesFeature ex:feature-api-access ;
    saas:entitlementStatus "active" .

5.4 実行スクリプトを保存する

これまでの記事ではsparqlqueryコマンドを使いましたが、あのコマンドは推論をしてくれません。今回は推論の有無を切り替えたいので、短いスクリプトを用意します。query.pyとして保存してください。

"""query.py — Turtleファイルを読み込んでSPARQLを実行する。

使い方:
    python query.py <ttlファイル...> <クエリファイル.rq>
    python query.py --inference <ttlファイル...> <クエリファイル.rq>

--inference を付けると、T-Boxの定義(owl:inverseOf、rdfs:subPropertyOfなど)に
基づく推論を行ったうえで問い合わせる。
"""

import sys

from rdflib import Graph

def main(argv: list[str]) -> int:
    use_inference = "--inference" in argv
    paths = [a for a in argv if a != "--inference"]

    if len(paths) < 2:
        print(__doc__)
        return 1

    *data_files, query_file = paths

    graph = Graph()
    for path in data_files:
        graph.parse(path, format="turtle")

    triples_before = len(graph)

    if use_inference:
        import owlrl

        owlrl.DeductiveClosure(owlrl.OWLRL_Semantics).expand(graph)

    with open(query_file, encoding="utf-8") as f:
        query = f.read()

    results = graph.query(query)

    print(",".join(str(v) for v in results.vars))
    for row in results:
        print(",".join("" if value is None else str(value) for value in row))

    mode = "推論あり" if use_inference else "推論なし"
    print(
        f"\n-- {len(results)} 行 / {mode}"
        f"(トリプル数 {triples_before} -> {len(graph)})"
    )
    return 0

if __name__ == "__main__":
    sys.exit(main(sys.argv[1:]))

やっていることは3つだけです。

1. 指定されたTurtleファイルを全部1つのグラフに読み込む
2. --inference が付いていたら推論して、導けるトリプルを追加する
3. SPARQLを実行して結果を表示する

推論にOWLRL_Semanticsを指定している点だけ、先に理由を書いておきます。owlrlには軽いRDFS_Semanticsもありますが、今回主役になるowl:inverseOfはOWLの語彙なので、RDFS推論では処理されません。この違いは8章でもう一度触れます。


6. T-Boxは、本当に結果を変えるのか

6.1 まず問い合わせてみる

「株式会社ABCが使える機能は何か?」を聞きます。query-features.rqとして保存してください。

# query-features.rq — 株式会社ABCが使える機能は何か?
PREFIX saas: <https://example.com/ontology/saas#>
PREFIX ex:   <https://example.com/data/>

SELECT ?featureName ?featureCode
WHERE {
    ex:tenant-abc saas:hasEntitlement ?entitlement .
    ?entitlement saas:entitlesFeature ?feature .
    ?entitlement saas:entitlementStatus "active" .
    ?feature saas:name ?featureName .
    ?feature saas:featureCode ?featureCode .
}
ORDER BY ?featureCode

注目してほしいのは1行目のパターンです。

ex:tenant-abc saas:hasEntitlement ?entitlement .

Tenant側からsaas:hasEntitlementでたどっています。一方、data.ttlに書いてあるのは逆向きのsaas:entitledTenantだけでした。

実行します。

python query.py ontology.ttl data.ttl query-features.rq

結果:

featureName,featureCode

-- 0 行 / 推論なし(トリプル数 103 -> 103)

0行です。T-Boxを読み込んでいるにもかかわらず、何も返りません。

これは、多くの人が最初にハマるところです。

T-Boxに owl:inverseOf と書いただけでは、
逆方向のトリプルは自動的には存在しない。

RDFのグラフには、書かれたトリプルしか入っていません。ex:entitlement-abc-csv-export saas:entitledTenant ex:tenant-abcは存在しますが、ex:tenant-abc saas:hasEntitlement ex:entitlement-abc-csv-exportは、まだどこにも書かれていないのです。

ここで、正直に書いておかなければならないことがあります。

「逆方向をたどるには推論が必須」というわけではない。

次の2つは、どちらも推論なしで3件返ります。

# データが書かれている向きで、そのまま問う
?entitlement saas:entitledTenant ex:tenant-abc .

# SPARQLの逆パス演算子 ^ を使う
ex:tenant-abc ^saas:entitledTenant ?entitlement .

つまり、推論の価値は「逆方向が取れること」そのものではありません。

推論の本当の価値:

    データがどちら向きに書かれていても、
    問い合わせる側は saas:hasEntitlement 一本で書ける。

    そして次の7章で見るように、
    クエリを1文字も変えないまま、
    複数のシステムのデータを束ねられる。

データがどちら向きに書かれているかという違いは、間に挟まる層が吸収します。だから問い合わせる側は、データの書かれ方を知らなくてよい。1章で説明した「レイヤー(層)」の価値は、ここにあります。

6.2 推論を有効にする

--inferenceを付けて、もう一度実行します。

python query.py --inference ontology.ttl data.ttl query-features.rq

結果:

featureName,featureCode
基本プロジェクト管理,core.project-management
CSVエクスポート,data.csv-export
APIアクセス,integration.api-access

-- 3 行 / 推論あり(トリプル数 103 -> 368)

3行返ってきました。そして、トリプル数が103から368に増えています(この増え方の内訳は8章で見ます)。

何が起きたのでしょうか。

T-Boxにこう書いてあった:
    saas:entitledTenant owl:inverseOf saas:hasEntitlement

A-Boxにこう書いてあった:
    ex:entitlement-abc-csv-export saas:entitledTenant ex:tenant-abc

推論エンジンは、この2つから次を導き出した:
    ex:tenant-abc saas:hasEntitlement ex:entitlement-abc-csv-export

つまり、T-Boxに書いた1行(owl:inverseOf)が、問い合わせの結果を0行から3行に変えたのです。T-Boxは人間向けの説明書きではありません。推論エンジンが読んで、データに書かれていない事実を導くためのルールとして働きました。

ontology.ttlを読み込まずに推論しても、この結果は出ません。逆方向であるという情報が、どこにも無いからです。ぜひontology.ttlを外して試してみてください。

6.3 販売管理機能について聞いてみる

では冒頭の問いに戻ります。query-sales.rqとして保存してください。

# query-sales.rq — 株式会社ABCは、販売管理機能を使えるか?
PREFIX saas: <https://example.com/ontology/saas#>
PREFIX ex:   <https://example.com/data/>

SELECT ?featureName ?status
WHERE {
    ex:tenant-abc saas:hasEntitlement ?entitlement .
    ?entitlement saas:entitlesFeature ex:feature-sales-management .
    ?entitlement saas:entitlementStatus ?status .

    ex:feature-sales-management saas:name ?featureName .
}

実行します。

python query.py --inference ontology.ttl data.ttl query-sales.rq

結果:

featureName,status

-- 0 行 / 推論あり(トリプル数 103 -> 368)

推論を有効にしても0行です。これは推論不足ではなく、グラフの中に該当するEntitlementが存在しないという答えです。

厳密に言うと、OWLの推論は「書かれていない=偽」とは考えません。これを開世界仮定と呼びます。反対に、書かれていないことを偽とみなす考え方を閉世界仮定と呼びます。ふつうのデータベースは、こちらの考え方です。

それでもここで「使えない」と言い切れるのは、「Entitlementはすべてこのグラフに記録されている」という業務上の前提を置いているからです。Entitlementの記録元を網羅しているからこそ、0行を「無い」と読めます。ただし、7章で見るとおり、実務ではこの前提こそが崩れやすいのです。

株式会社ABCは、販売管理機能を使えない。
販売管理機能を対象とした有効なEntitlementが、
グラフの中に1つも存在しないから。

data.ttlを見ると、ABCが契約しているStandard Planにも、販売管理は含まれていません。クエリの結果とも矛盾しません。

冒頭の問いに、事実に基づいて答えられました。


7. 本番: バラバラの語彙を、1つの問いで横断する

ここまでは1つのシステムのデータだけでした。ここからが、セマンティックレイヤーが本当に役に立つ場面です。

7.1 現実の状況

実務では、こうなっています。

株式会社ABCは、商談の結果、
Standard Planに含まれないSAML SSOと監査ログを
個別に付与されている。

ただし、その事実の記録場所はバラバラ:

    請求システム : SAML SSO を課金対象として登録
                  → billing:grantedFeature という項目名

    CRM          : 監査ログ を有効化済みとして記録
                  → crm:enabledFeature という項目名

同じ「機能を利用可能にした」という意味なのに、項目名が違います。普通なら、どちらかに寄せる変換バッチを書くところです。

セマンティックレイヤーでは、データを書き換えず、意味の対応だけを宣言します。

7.2 マッピングを宣言する

mapping.ttlとして保存してください。

# mapping.ttl — 各ソースシステムの語彙を、共通語彙 saas: に接続する
@prefix saas:    <https://example.com/ontology/saas#> .
@prefix rdfs:    <http://www.w3.org/2000/01/rdf-schema#> .
@prefix billing: <https://example.com/source/billing#> .
@prefix crm:     <https://example.com/source/crm#> .

billing:grantedFeature rdfs:subPropertyOf saas:entitlesFeature .
billing:forCustomer    rdfs:subPropertyOf saas:entitledTenant .

crm:enabledFeature     rdfs:subPropertyOf saas:entitlesFeature .
crm:forAccount         rdfs:subPropertyOf saas:entitledTenant .

rdfs:subPropertyOfは、「このプロパティは、あのプロパティの一種である」という宣言です。

billing:grantedFeature rdfs:subPropertyOf saas:entitlesFeature

意味:
    「請求システムのgrantedFeatureで結ばれているものは、
      saas:entitlesFeatureでも結ばれていると見なしてよい」

これだけです。4行の宣言で、2つのシステムの語彙が共通語彙に接続されました。

7.3 各システムのデータをそのまま置く

data-multi-source.ttlとして保存してください。各システムの語彙のまま、一切書き換えていません。

# data-multi-source.ttl — 2つの別システムから取り込んだ、そのままの形のデータ
@prefix saas:    <https://example.com/ontology/saas#> .
@prefix ex:      <https://example.com/data/> .
@prefix billing: <https://example.com/source/billing#> .
@prefix crm:     <https://example.com/source/crm#> .

#
# 請求システム由来(billing: 語彙)
#

ex:billing-entitlement-001
    billing:forCustomer ex:tenant-abc ;
    billing:grantedFeature ex:feature-saml-sso ;
    saas:entitlementStatus "active" .

#
# CRM由来(crm: 語彙)
#

ex:crm-entitlement-042
    crm:forAccount ex:tenant-abc ;
    crm:enabledFeature ex:feature-audit-log ;
    saas:entitlementStatus "active" .

7.4 同じクエリで、横断して問い合わせる

なお、a saas:Entitlementというクラス型付けを書いていません。第3回でEntitlementをクラスとして定義したので気になるかと思いますが、今回の問いはプロパティの経路だけでたどれるため、省いています。型付けが必要になるのは、SHACLで検証する場合や、クラス単位で数え上げる場合です。

ここが大事なところです。クエリは6.1節のものを、1文字も変えません。

python query.py --inference \
    ontology.ttl mapping.ttl data.ttl data-multi-source.ttl \
    query-features.rq

結果:

featureName,featureCode
基本プロジェクト管理,core.project-management
CSVエクスポート,data.csv-export
APIアクセス,integration.api-access
監査ログ,security.audit-log
SAML SSO,security.saml-sso

-- 5 行 / 推論あり(トリプル数 113 -> 410)

3行から5行に増えました。増えたのは、請求システム由来のSAML SSOと、CRM由来の監査ログです。

改めて、何をやったか整理します。

やったこと:
    ・請求システムのデータを、billing: 語彙のまま置いた
    ・CRMのデータを、crm: 語彙のまま置いた
    ・「これらは saas: の何に相当するか」を4行宣言した

やらなかったこと:
    ・項目名をそろえるために、データを書き換える変換バッチやETL処理を書く
    ・クエリを1文字でも変更する

これが、セマンティックレイヤーです。

データを1箇所に集めて形を揃えるのではなく、
「意味の対応」を宣言することで、
バラバラのままのデータを1つの語彙で問い合わせる。

新しいシステムが増えたら、mapping.ttlに数行足すだけです。クエリを書く側は、各システムの項目名を知らなくてよい。違いは間の層がすべて引き受けます。これが「レイヤー」と呼ばれる理由です。


8. SQLと比べて、何が違うのか

「同じことをSQLでもできるのでは?」という疑問に、正直に答えます。

まずSQLで書いてみます。

SELECT f.name, f.code
FROM entitlements e
JOIN features f ON e.feature_id = f.id
WHERE e.tenant_id = 'abc' AND e.status = 'active'

UNION

SELECT f.name, f.code
FROM billing_entitlements be
JOIN features f ON be.granted_feature_id = f.id
WHERE be.customer_id = 'abc' AND be.status = 'active'

UNION

SELECT f.name, f.code
FROM crm_entitlements ce
JOIN features f ON ce.enabled_feature_id = f.id
WHERE ce.account_id = 'abc' AND ce.status = 'active';

できます。ちゃんと動きます。ただし、違いが2つあります。

違い1: 増えたときに変わるのはどちらか

SQL:
    4つ目のシステムが増えたら、
    このクエリ自体に UNION を1ブロック足す。
    このクエリを使っている全ての箇所を直す。

セマンティックレイヤー:
    mapping.ttl に2行足す。
    クエリは変更しない。

ただし公平に言えば、実務のSQLでは、このUNIONをあちこちに書いたりはしません。dbtなどの変換処理で1つのentitlementsテーブル(またはビュー)にまとめておけば、直す場所は1箇所で済みます。2章で見たメトリクス型のセマンティックレイヤーも、この前提に立っています。

そのため、本当の違いは次の「違い2」のほうです。

違い2: 「同じ意味である」がどこに書かれているか

SQLのUNIONは、「この3つのテーブルは同じものとして扱う」という判断をクエリの中に埋め込んでいます。その判断の根拠は、クエリを書いた人の頭の中にしかありません。

一方、rdfs:subPropertyOfは、その判断をクエリの外に、機械が読める宣言として書いています。だから推論エンジンが自動で適用できるし、他の人も他のツールも同じ判断を再利用できます。

もう1点あります。よく「SPARQLならスキーマ変更に強い」と言われますが、それは半分だけ本当です。

SPARQLも、述語のIRIが変わればクエリは壊れる。
その意味では、カラム名が変わればSQLが壊れるのと同じ。

本当の違いは、
「変更に強い」ことではなく、
「意味の対応関係を、クエリの外に、
  機械可読な形で書き出せる」こと。

そして、これにはコストもあります。

推論のコスト:
    今回は103トリプルが368に増えた(約3.5倍)。

ただし、この「3.5倍」を鵜呑みにしないでください。増えた265トリプルの内訳を数えると、こうなっています。

増加 265トリプルの内訳:

    262件  W3C語彙(owl:, rdfs: など)に関わるトリプル
           ・大半(約230件)は、データの量と関係なく毎回追加されるもの
             (例:「owl:Thing はクラスである」のような事実)
           ・残りは、データ中のIRIや値ごとに機械的に付く自明なもの
             (例:「X は X と同じものである(owl:sameAs)」)
      3件  saas:hasEntitlement の導出(=実際に欲しかったもの)

つまり3.5倍の大半は、データ量に関係なく発生する固定オーバーヘッドです。A-Boxが小さいほど倍率は大きく見えます。データを増やすと、倍率そのものは下がっていきます。実際にdata.ttlと同じ形のデータを20倍に増やして試すと、推論後のトリプル数は推論前の約1.8倍まで下がりました。

とはいえ、絶対的な計算量とメモリは確実に増えます。本番では次のような判断が必要になります。

・推論結果を事前に計算して保存しておく(マテリアライズ。問い合わせのたびには推論しない)
・推論を使わず、SPARQLの逆パス演算子(6.1節の ^)で書く
・必要な導出だけを CONSTRUCT(クエリ結果から新しいトリプルを作る構文)で明示的に作る

ここで1つ注意点があります。「軽いRDFS推論に絞る」という対策をよく見かけますが、今回のハンズオンではそれをやると、うまく動かなくなります。

owl:inverseOf は、RDFS推論では処理されない。

実測:
    RDFS_Semantics  → 0行
    OWLRL_Semantics → 3行

rdfs:subPropertyOfはRDFS語彙なのでRDFS推論でも処理されますが、owl:inverseOfはOWLの語彙です。この記事のquery.pyがOWLRL_Semanticsを使っているのは、そのためです。

魔法ではありません。トレードオフのある設計選択です。


9. メトリクス型と比べて、何が違うのか

8章ではSQLと比べました。ここでは、2章で見たメトリクス型のセマンティックレイヤーと比べます。

違いが一番よくわかるのは、得意な問いが違うところです。2つの問いを用意しました。

Q1. プラン別に、毎月いくつ新しい契約が始まっているか?
    (集計の問い)

Q2. 株式会社ABCというテナントは、販売管理機能を使えるか?
    (関係をたどる問い。しかも記録が複数システムに散らばっている)

9.1 Q1(集計)は、メトリクス型の得意分野

Q1は、2.3節で見たとおり、メトリクス型ならきれいに書けます。

オントロジー型でも、SPARQLで集計はできます。Subscriptionの開始日を表すsaas:subscriptionStartは後続の会で正式に定義する予定ですが、ここでは先取りして使います。

SELECT ?planName ?month (COUNT(DISTINCT ?sub) AS ?newSubscriptions)
WHERE {
    ?sub a saas:Subscription ;
         saas:basedOnPlan ?plan ;
         saas:subscriptionStart ?started .
    ?plan saas:name ?planName .
    BIND(SUBSTR(STR(?started), 1, 7) AS ?month)
}
GROUP BY ?planName ?month

書けます。ただし、次のような点でメトリクス型に分があります。

・「new_subscriptions とはこの計算式である」という定義を、
  再利用可能な部品として置く標準的な仕組みが、OWLにはない。
  (OWLは「何が存在するか」の言語であって、「どう数えるか」の言語ではない。
    統計などの多次元データを記述する W3C の RDF Data Cube 語彙はあるが、
    計算式そのものを定義する、広く使われた標準はない)

・時間の粒度(日・週・月)、足し算できない指標(期末残高など)の扱いを、
  メトリクス型は定義側で面倒を見てくれる。SPARQLでは毎回書く。

・数億行の集計は、列指向のデータウェアハウスのほうが圧倒的に速い。

9.2 Q2(関係・横断)は、オントロジー型の得意分野

Q2の答えは、「ABCに、販売管理機能を対象とした有効なEntitlementがあるか」です。しかも現実には、その記録が請求システムとCRMに、別々の項目名で散らばっています。

請求システム: billing_entitlements.granted_feature_id
CRM:          crm_entitlements.enabled_feature_id

オントロジー型では、こう書くだけで2つを同じ意味として扱えます。

billing:grantedFeature rdfs:subPropertyOf saas:entitlesFeature .
crm:enabledFeature     rdfs:subPropertyOf saas:entitlesFeature .

問い合わせる側はsaas:entitlesFeature一本で書けばよく、データ自体は書き換えません。これは7章で実際に確かめたとおりです。

では、メトリクス型ではQ2はどうなるか。ここは公平に書いておきます。

「メトリクス型は複数ソースを束ねられない」は、正確ではない。

メトリクス型の世界では、
複数ソースの統合は、セマンティックレイヤーの「下」で済ませておく。
dbt などの変換処理で、請求とCRMのテーブルを
1つの entitlements テーブルに揃えてから、その上に定義を載せる。

つまり、違いは「束ねられるか」ではなく、束ねる判断をどこに書くかです。

メトリクス型:
    「grantedFeature と enabledFeature は同じ意味だ」という判断は、
    変換処理(SQL)の中に書かれる。

オントロジー型:
    同じ判断を、変換処理の外に、
    機械が読める「意味の宣言」として書く。

そして、Q2はそもそも集計の問いではありません。メトリクスとディメンションという型に、自然には収まらない問いです。

9.3 比較表

ここまでの比較を表にまとめます。

観点メトリクス型オントロジー型
定義する単位メジャー・ディメンション・結合クラス・プロパティ・関係
答えやすい問いどれだけ?(集計)何が何と、どうつながっている?
定義の置き場所テーブルの上(DWHのカタログ)テーブルから独立(Turtleなど)
複数ソースの語彙合わせ下の変換層(dbt等)で済ませておくマッピングとして宣言する
推論なし(集計の意味づけはある)あり(逆関係、上位プロパティ、クラス階層)
書かれていない事実の扱い「無い」とみなす(閉世界仮定)推論では「わからない」とみなす(開世界仮定、6.3節)。ただしSPARQLの結果やSHACLの検証は、グラフにあるものだけで判断する
制約の検証定義の整合性は作成時に検証される。データの制約はテスト(dbt testsなど)で確認するSHACLで宣言する
集計性能強い(列指向エンジン)弱め(大規模集計は不得意)
標準Apache Ossie(旧OSI)が整備中W3C標準(RDF/OWL/SPARQL/SHACL)
人材・ツール豊富少ない
AIとの組み合わせセマンティックモデルを手がかりに、自然文→SQL(Cortex Analyst、Genieなど)自然文→SPARQL、ナレッジグラフ

9.4 メトリクス型のPros / Cons

Pros:
    ・「数字が合わない」問題に、直接対応する
    ・データウェアハウスの中にあり、権限管理も性能もそのまま使える
    ・SQLの延長なので、データ分析の人材がそのまま使える
    ・BIツールやAIエージェントとの連携が、製品として整っている

Cons:
    ・定義はテーブル構造に依存する。テーブルが先、意味が後
    ・「AとBは同じ意味」という判断が、変換処理のSQLに埋もれやすい
    ・推論がない。書いた計算式以上のことは出てこない
    ・集計にならない問い(関係・資格・依存)は、型に収まりにくい

9.5 オントロジー型のPros / Cons

Pros:
    ・意味の体系がテーブルから独立しているので、ソースが増えても影響を受けにくい
    ・意味の対応関係を、クエリの外に機械可読な形で書ける
    ・推論で、書かれていない事実を導ける
    ・W3C標準なので、特定ベンダーに閉じない

Cons:
    ・集計・時間粒度・大規模データの扱いは、メトリクス型に劣る
    ・RDF/SPARQLを扱える人が少ない
    ・既存のRDBのデータをRDFとして扱う工程(変換か仮想化)が別途必要
    ・推論には計算コストがかかり、設計判断が要る

どちらも魔法ではありません。目的が違う道具です。


10. AIがセマンティックレイヤーを使う場合

もう1つ、いまの時代に重要な応用に触れておきます。

LLM(大規模言語モデル)に「株式会社ABCは販売管理機能を使えますか?」と聞いたとき、何が起きるべきでしょうか。

セマンティックレイヤーが無い場合:
    LLMが学習データからそれっぽい答えを生成する
    → ハルシネーション(幻覚)のリスク

セマンティックレイヤーがある場合:
    LLMが、問いをSPARQLに変換する
    → セマンティックレイヤーに問い合わせる
    → 返ってきた事実に基づいて答える

セマンティックレイヤーがあれば、LLMは「答えを創作する」のではなく「問いを翻訳する」役割に徹することができます。

人間の質問:
    「株式会社ABCは販売管理機能を使える?」

LLMが生成するSPARQL:
PREFIX saas: <https://example.com/ontology/saas#>
PREFIX ex:   <https://example.com/data/>

SELECT ?featureName ?status
WHERE {
    ex:tenant-abc saas:hasEntitlement ?e .
    ?e saas:entitlesFeature ?f .
    ?f saas:name ?featureName .
    FILTER(CONTAINS(?featureName, "販売管理"))
    ?e saas:entitlementStatus ?status .
}
セマンティックレイヤーの応答:
    (0行)

LLMの最終回答:
    「株式会社ABCは現在、販売管理機能を利用できません。
      この機能に対する有効なEntitlementが
      存在しないためです。」

ここでLLMが正しいクエリを組み立てられるのは、T-Boxに「Tenant」「Entitlement」「Feature」が何であり、どうつながっているかが書いてあるからです。T-Boxからは、クエリの形(どのプロパティをたどるか)が分かります。絞り込む値の「販売管理」は、LLMが質問の言葉から取り出しています。

LLMにテーブル定義だけを渡した場合:
    「entitlements.status って何の状態?」
    「tenant_id と customer_id は同じもの?」
    → 推測が入る

LLMにT-Boxを渡した場合:
    第3回で Entitlement の rdfs:comment に書いた
    「特定のTenantに、特定のFeatureを提供する権利、資格または状態」
    が読める
    → 推測が減る

第1回からrdfs:labelとrdfs:commentを丁寧に書いてきたのは、人間のためだけではなかったわけです。

なお、今回のontology.ttlは、説明を短くするためにrdfs:commentを省いています。実際にLLMへ渡すなら、第3回までの定義をコメントごと渡します。

この方向性は、メトリクス型の製品にも共通しています。どの製品も、AIエージェント向けの機能を足しています。

Snowflake   Semantic View に、AI向けの指示や検証済みクエリを書ける句がある
            → Cortex Analyst などが参照する
Databricks  Metric View に synonyms(同義語)を書ける
            → Genie が参照する
Looker      LookML を、Gemini の会話型分析の根拠として使う

目的は共通していて、「LLMに計算を推測させず、定義に従って決定的に答えさせる」ことです。これは、この章で見た「LLMを『答えを創作する役』から『問いを翻訳する役』にする」という話と、まったく同じ方向です。


11. 2つの系統は近づいている

ここまで「2つの系統」として分けてきましたが、実は最近、両者の境界は少しずつ曖昧になっています。きっかけは、10章で見たとおり、どちらもAIに正確に答えさせる手段として注目されていることです。

11.1 Microsoft Fabric IQのOntology

一番わかりやすい合流の例が、MicrosoftがIgnite 2025で発表したFabric IQです。その中核に、Ontologyというアイテム(2026年10月時点でプレビュー)があります。

Fabric IQ の Ontology で定義するもの(公式ドキュメントより):

    entity type      (≒ クラス)
    relationship     (≒ プロパティ)
    継承・namespace
    metric           ← Power BI の Semantic Model の DAX メジャーを参照する
    rule             (自然言語で書く)

面白いのは、metricをオントロジーの中で「参照」している点です。計算式そのもの(DAX)は元のSemantic Modelが持ち続け、オントロジーはそれを「このエンティティに関する指標」として結びつけます。

オントロジー   : 「Tenant とは何か」「何とつながっているか」
メトリクス層   : 「Tenant ごとの数字を、どう計算するか」

→ 置き換えるのではなく、重ねている

また、FabricのOntologyはRDF / Turtle / OWLのサブセットの取り込みと、RDF / Turtleでの書き出しをドキュメントに載せています(取り込み先は空のOntologyアイテムで、取り込み結果は、そのまま保たれた要素・自動で調整された要素・対応していない要素に分けて報告されます)。データ・分析基盤の大手が、W3Cのオントロジー形式との行き来を公式にうたった例です。

さらに、FabricのOntologyは基本的には自分でデータを持たない「仮想的な意味のレイヤー」で、実際の問い合わせは接続先のデータソースで実行されます(多段の関係をたどる場合などは、FabricのGraphにデータを任意で実体化することもできます)。これは、RDFの世界で以前からあるR2RML / Ontop(SPARQLをSQLに書き換えて、元のDBで実行する)とよく似た発想です。

11.2 Apache Ossie も、オントロジーを対象に加えた

2章で触れたOSI(現 Apache Ossie)も、同じ方向に動いています。2026年7月にApache Incubatorへ移ったとき、Ossieは自らを「セマンティックレイヤーとオントロジーの両方のための仕様」と位置づけ直しました。

さらに、草案段階(v0.2.0.dev0)のオントロジー仕様では、概念や関係にiriフィールドを持たせ、既存のRDF/OWL語彙のクラスやプロパティと対応づけられるようになっています。メトリクス型から始まった標準化が、オントロジー、さらにW3Cの語彙との接続にまで広がってきたわけです。

11.3 「オントロジー」という言葉の広がり

ちなみに、Palantir FoundryのOntologyは、オブジェクト型・リンク型・アクション型という独自のモデルで業務の世界を表現するもので、RDF/OWLをデータモデルの基盤にはしていません(Palantir自身の資料では、RDF/OWL形式との入出力もうたわれています)。MicrosoftのGraphRAGも、LLMでテキストからナレッジグラフを作るもので、RDFではありません。

「オントロジー」「ナレッジグラフ」と名乗っていても、
W3C標準の RDF/OWL とは限らない。

ここは混乱しやすいので、覚えておくと便利です。


12. レイヤー構成を整理する

最後に、全体像を整理します。

問い合わせる側(BIツール、AI、スクリプト、人間)
    │
    │  共通語彙 saas: で問う
    ↓
セマンティックレイヤー
    ├── ontology.ttl = T-Box(意味の定義)
    └── mapping.ttl  = 各ソース語彙との意味の対応
    │
    │  推論して、書かれていない事実も導く
    ↓
RDFとして表現されたデータ(A-Box)
    │
    │  ここに、元のデータをRDFに変換する(取り込む)工程が必要
    ↓
実際のデータソース
    ├── Postgres
    ├── SaaSのAPI
    └── CSV など

ここで、正直に書いておかなければならないことがあります。

今回の記事では、data.ttl を手で書いた。

しかし実務では、データはPostgresやSaaSのAPIの中にある。
それをA-Box(RDF)にする工程は、
誰かが作らなければならない。

この工程は、この記事では扱っていません。実際には、次のような選択肢があります。

・定期的にRDFへ書き出す(マテリアライズ)
・問い合わせ時にSQLへ変換する(R2RML / Ontop などの仮想化。データはRDBに置いたまま、SPARQLをSQLに書き換えて実行する)
・アプリケーション側でRDFとして書き出す

「セマンティックレイヤーがあれば変換が不要になる」わけではありません。7章で不要になったのは、項目名をそろえるための変換でした(mapping.ttlが語彙どうしの対応を引き受けたからです)。PostgresなどのデータをRDFの形にする変換は、依然として必要です。セマンティックレイヤーが不要にするのは、「システムごとにバラバラな意味の解釈を、問い合わせる側が毎回やり直すこと」です。

メトリクス型のセマンティックレイヤーも含めて描き直すと、こうなります。

問い合わせる側(BIツール、AIエージェント、人間)
    │
    ├──「どれだけ?」────→ メトリクス型セマンティックレイヤー
    │                      (Snowflake / Databricks / Fabric / dbt)
    │                         │
    │                         │ 「Tenant」「Plan」の意味は
    │                         │ オントロジーに従う
    │                         ↓
    └──「何が何と、どう?」→ オントロジー型セマンティックレイヤー
                              (このシリーズで作っている saas: 語彙)
                                 │
                                 ↓
                            各システムのデータ
                            (SaaS管理基盤、請求、CRM、サポート…)
オントロジー   = 意味の基準
    「Tenantとは何か」
    「EntitlementとSubscriptionはどう違うか」
    「請求システムの customer と CRM の account は同じものか」

メトリクス層   = 計算の基準
    「Tenantごとの月間利用量を、どう集計するか」
    「プラン別の新規契約数を、どう数えるか」

メトリクス型で「アクティブテナント数」を定義するとき、その前提には「そもそもTenantとは何か」「どの状態をアクティブと呼ぶか」という取り決めがあるはずです。その取り決めを、テーブルやSQLの中に埋もれさせずに書き出したものが、オントロジーです。

2つは競合ではなく、積み重なるものだと考えています。そして、このシリーズが作っているのは下の層、つまり意味の基準のほうです。

ここまで見てきた「意味の定義」は、このシリーズの第1回から第3回で作ってきたもの、そのものです。

第1回:Tenant, User, Membership, Role, Permission, Subscription, Plan, Resource
第2回:Organization, Workspace, Team, RoleAssignment
第3回:Feature, Entitlement

つまり、オントロジーを作ること=セマンティックレイヤーの語彙を構築することでした。第4回以降で語彙が増えるたびに、このセマンティックレイヤーが答えられる問いも増えていきます。


まとめ

「セマンティックレイヤー」には、2つの系統がある:

    メトリクス型(Snowflake / Databricks / Fabric / dbt / Cube / Looker)
        数字の計算のしかたを、テーブルの上に定義する
        → 「どれだけ?」に強い。数字のズレをなくす

    オントロジー型(RDF / OWL / SPARQL / SHACL)
        ものごとの関係と意味を、テーブルから独立して定義する
        → 「何が何と、どうつながっている?」に強い。推論と横断ができる

    このシリーズが作っているのは、オントロジー型

セマンティックレイヤーとは:
    データの意味を一箇所に定義し、
    どのツールからでも同じ意味で問い合わせられるようにする仕組み

オントロジーとの関係:
    T-Box(クラスとプロパティの定義)がセマンティックレイヤーの本体
    A-Box(実データ)はT-Boxの語彙で記述される

T-Boxは飾りではない:
    owl:inverseOf を書いておくと、推論で逆方向をたどれる
    → 推論なし 0行、推論あり 3行

意味の対応を宣言すれば、語彙の違いを吸収できる:
    rdfs:subPropertyOf で各システムの語彙を共通語彙に接続
    → データもクエリも変更せず、3行から5行に

正直な限界:
    ・逆方向は、推論なしでも(逆パス ^ などで)取れる。
      推論の価値は「問い合わせる側が共通語彙一本で書けること」
    ・owl:inverseOf はRDFS推論では処理されない(OWLの推論が必要)
    ・推論の増加分の大半は固定オーバーヘッド(265件中、約230件)
    ・SPARQLも述語IRIが変われば壊れる
    ・PostgresなどからA-Boxへの変換工程は別途必要

手を動かした手順は、次の4ステップでした。

1. pip install rdflib owlrl
2. ontology.ttl(T-Box)と data.ttl(A-Box)を保存する
3. python query.py ontology.ttl data.ttl query-features.rq
   → 0行(hasEntitlement 方向のトリプルがまだ存在しない)
4. python query.py --inference ontology.ttl data.ttl query-features.rq
   → 3行(T-Boxの owl:inverseOf から、逆向きの事実が導かれた)

ここまでで、このシリーズで作っているオントロジーがどう使われるかが、なんとなく伝わったかと思います。具体的なユースケースでどう使うの?というのは、さらに別の会でもっと具体的に説明したいと思います。

次回(第4回)からは、語彙の構築に戻ります。テーマは「使える。でも、どこまで?」です。Featureが使えるとして、その上限と消費量をどう表現するか、という話に入っていこうと思います!

こちらからは以上です。


参考資料(2026年10月時点)