はじめに
Amazon Neptune 上に OWL (Web Ontology Language) で厳密に定義した EC サイトドメインのオントロジーを構築し、SPARQL で検索できる環境を作ります。
本記事(前編)では、Neptune クラスターの構築からオントロジー設計、データ投入までを扱います。後編では EC2 + セッションマネージャーで SPARQL を直接叩ける環境を構築します。
なぜ Neptune + OWL?
RDB ではテーブル間の関係は外部キーで定義しますが、「この顧客は Person の一種で、Agent の一種でもある」「Electronics と Furniture は互いに排他」といった 意味的な制約 は表現できません。
OWL オントロジーを使うと:
- クラス階層 (Customer is-a Person is-a Agent)
- プロパティ制約 (各 Order は必ず1人の Customer に紐づく)
- Disjoint制約 (ElectronicsProduct と BookProduct は排他)
- 列挙型 (MembershipTier は Bronze/Silver/Gold/Platinum のいずれか)
をグラフ上で形式的に定義でき、SPARQL でこれらの意味構造を活かした検索が可能になります。
構成図
┌──────────────────────────────────────────────────┐
│ VPC (us-east-1) │
│ │
│ ┌─────────────┐ ┌─────────────────────┐ │
│ │ Lambda │──8182──→│ Neptune (SPARQL) │ │
│ │ (ブリッジ) │ │ ecommerce-ontology │ │
│ └─────────────┘ └─────────────────────┘ │
│ │
└──────────────────────────────────────────────────┘
↑ invoke
─────┴─────
AWS CLI (ローカル)
前提条件
- AWS アカウント
- AWS CLI インストール・設定済み
- Python 3 + boto3
- us-east-1 リージョン(デフォルト VPC あり)
Step 1: ネットワーク・セキュリティの準備
セキュリティグループ作成
SG_ID=$(aws ec2 create-security-group \
--description "Security group for Neptune ontology cluster" \
--group-name neptune-ontology-sg \
--vpc-id <YOUR_VPC_ID> \
--region us-east-1 \
--query 'GroupId' --output text)
# VPC内からポート8182への接続を許可
aws ec2 authorize-security-group-ingress \
--group-id $SG_ID \
--protocol tcp --port 8182 \
--cidr 172.31.0.0/16 \
--region us-east-1
Neptune サブネットグループ作成
aws neptune create-db-subnet-group \
--db-subnet-group-name neptune-ontology-subnet-group \
--db-subnet-group-description "Subnet group for Neptune ontology cluster" \
--subnet-ids '["<SUBNET_AZ_A>", "<SUBNET_AZ_B>", "<SUBNET_AZ_C>"]' \
--region us-east-1
最低2つの異なるAZのサブネットが必要です。デフォルトVPCのサブネットをそのまま使えます。
Step 2: Neptune クラスター作成
# クラスター作成
aws neptune create-db-cluster \
--db-cluster-identifier ecommerce-ontology \
--db-subnet-group-name neptune-ontology-subnet-group \
--engine neptune \
--no-deletion-protection \
--vpc-security-group-ids "[\"$SG_ID\"]" \
--region us-east-1
# => Endpoint を控える
# インスタンス作成
aws neptune create-db-instance \
--db-cluster-identifier ecommerce-ontology \
--db-instance-identifier ecommerce-ontology-instance-1 \
--db-instance-class db.t3.medium \
--engine neptune \
--region us-east-1
クラスターが available になるまで 5〜10分 かかります。以下で状態確認:
aws neptune describe-db-clusters \
--db-cluster-identifier ecommerce-ontology \
--query 'DBClusters[0].Status' --output text
Step 3: IAM ロール設定
Neptune S3 アクセスロール
aws iam create-role --role-name NeptuneS3LoadRole \
--assume-role-policy-document '{
"Version":"2012-10-17",
"Statement":[{"Effect":"Allow","Principal":{"Service":"rds.amazonaws.com"},"Action":"sts:AssumeRole"}]
}'
aws iam put-role-policy --role-name NeptuneS3LoadRole \
--policy-name NeptuneS3ReadPolicy \
--policy-document '{
"Version":"2012-10-17",
"Statement":[{"Effect":"Allow","Action":["s3:GetObject","s3:ListBucket"],
"Resource":["arn:aws:s3:::<YOUR_BUCKET>","arn:aws:s3:::<YOUR_BUCKET>/*"]}]
}'
ハマりポイント: ロール紐づけのタイミング
add-role-to-db-cluster はクラスターが available になってから実行してください。creating 中に実行すると以下のエラーになります:
InvalidParameterValue: IAM role ARN value is invalid or does not include the required permissions
# クラスターが available になってから実行
aws neptune add-role-to-db-cluster \
--db-cluster-identifier ecommerce-ontology \
--role-arn arn:aws:iam::<ACCOUNT_ID>:role/NeptuneS3LoadRole \
--region us-east-1
Lambda 用ロール
aws iam create-role --role-name NeptuneLambdaRole \
--assume-role-policy-document '{
"Version":"2012-10-17",
"Statement":[{"Effect":"Allow","Principal":{"Service":"lambda.amazonaws.com"},"Action":"sts:AssumeRole"}]
}'
aws iam attach-role-policy --role-name NeptuneLambdaRole \
--policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole
Step 4: OWL オントロジー設計
クラス階層
Agent
└── Person
└── Customer
Product
├── ElectronicsProduct ─┐
├── FurnitureProduct ───┼── (disjoint)
└── BookProduct ────────┘
Order ──hasOrderItem──→ OrderItem ──containsProduct──→ Product
│
├── orderedBy ──→ Customer
└── inRegion ──→ SalesRegion {Northeast, West, Southeast, Midwest}
Customer ──hasMembership──→ MembershipTier {Bronze, Silver, Gold, Platinum}
OWL で表現する意味的制約
| 制約 | OWL表現 |
|---|---|
| Product のサブカテゴリは排他 | owl:AllDisjointClasses |
| MembershipTier は4値の列挙 |
owl:oneOf + owl:equivalentClass
|
| 各 Order は必ず1人の顧客 |
owl:cardinality "1" on orderedBy
|
| orderedBy と placedOrder は逆関係 | owl:inverseOf |
| email は顧客ごとに1つ |
owl:FunctionalProperty + owl:cardinality "1"
|
Turtle ファイル (ecommerce-ontology.ttl)
全文(クリックで展開)
@prefix : <http://example.org/ecommerce#> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
# === Ontology Declaration ===
<http://example.org/ecommerce> rdf:type owl:Ontology ;
rdfs:label "E-Commerce Domain Ontology"@en ;
rdfs:comment "OWL ontology modeling an e-commerce domain."@en ;
owl:versionInfo "1.0.0" .
# === Classes ===
:Agent rdf:type owl:Class ; rdfs:label "Agent"@en ;
rdfs:comment "An entity capable of performing actions."@en .
:Person rdf:type owl:Class ; rdfs:subClassOf :Agent ;
rdfs:label "Person"@en ; rdfs:comment "A human being."@en .
:Customer rdf:type owl:Class ; rdfs:subClassOf :Person ;
rdfs:label "Customer"@en ;
rdfs:comment "A person who has registered on the e-commerce platform."@en ;
owl:equivalentClass [ rdf:type owl:Restriction ;
owl:onProperty :hasSignupDate ; owl:someValuesFrom xsd:date ] .
:Product rdf:type owl:Class ; rdfs:label "Product"@en .
:ElectronicsProduct rdf:type owl:Class ; rdfs:subClassOf :Product ;
rdfs:label "Electronics Product"@en .
:FurnitureProduct rdf:type owl:Class ; rdfs:subClassOf :Product ;
rdfs:label "Furniture Product"@en .
:BookProduct rdf:type owl:Class ; rdfs:subClassOf :Product ;
rdfs:label "Book Product"@en .
[] rdf:type owl:AllDisjointClasses ;
owl:members ( :ElectronicsProduct :FurnitureProduct :BookProduct ) .
:Order rdf:type owl:Class ; rdfs:label "Order"@en .
:OrderItem rdf:type owl:Class ; rdfs:label "Order Item"@en .
:MembershipTier rdf:type owl:Class ; rdfs:label "Membership Tier"@en ;
owl:equivalentClass [ rdf:type owl:Class ;
owl:oneOf ( :Bronze :Silver :Gold :Platinum ) ] .
:SalesRegion rdf:type owl:Class ; rdfs:label "Sales Region"@en ;
owl:equivalentClass [ rdf:type owl:Class ;
owl:oneOf ( :Northeast :West :Southeast :Midwest ) ] .
# === Object Properties ===
:placedOrder rdf:type owl:ObjectProperty ;
rdfs:domain :Customer ; rdfs:range :Order .
:orderedBy rdf:type owl:ObjectProperty, owl:FunctionalProperty ;
rdfs:domain :Order ; rdfs:range :Customer ; owl:inverseOf :placedOrder .
:hasOrderItem rdf:type owl:ObjectProperty ;
rdfs:domain :Order ; rdfs:range :OrderItem .
:containsProduct rdf:type owl:ObjectProperty, owl:FunctionalProperty ;
rdfs:domain :OrderItem ; rdfs:range :Product .
:hasMembership rdf:type owl:ObjectProperty, owl:FunctionalProperty ;
rdfs:domain :Customer ; rdfs:range :MembershipTier .
:inRegion rdf:type owl:ObjectProperty, owl:FunctionalProperty ;
rdfs:domain :Order ; rdfs:range :SalesRegion .
# === Data Properties ===
:firstName rdf:type owl:DatatypeProperty, owl:FunctionalProperty ;
rdfs:domain :Person ; rdfs:range xsd:string .
:lastName rdf:type owl:DatatypeProperty, owl:FunctionalProperty ;
rdfs:domain :Person ; rdfs:range xsd:string .
:email rdf:type owl:DatatypeProperty, owl:FunctionalProperty ;
rdfs:domain :Customer ; rdfs:range xsd:string .
:hasSignupDate rdf:type owl:DatatypeProperty, owl:FunctionalProperty ;
rdfs:domain :Customer ; rdfs:range xsd:date .
:lifetimeValue rdf:type owl:DatatypeProperty, owl:FunctionalProperty ;
rdfs:domain :Customer ; rdfs:range xsd:decimal .
:productName rdf:type owl:DatatypeProperty, owl:FunctionalProperty ;
rdfs:domain :Product ; rdfs:range xsd:string .
:unitPrice rdf:type owl:DatatypeProperty, owl:FunctionalProperty ;
rdfs:domain :Product ; rdfs:range xsd:decimal .
:category rdf:type owl:DatatypeProperty, owl:FunctionalProperty ;
rdfs:domain :Product ; rdfs:range xsd:string .
:orderDate rdf:type owl:DatatypeProperty, owl:FunctionalProperty ;
rdfs:domain :Order ; rdfs:range xsd:date .
:totalAmount rdf:type owl:DatatypeProperty, owl:FunctionalProperty ;
rdfs:domain :Order ; rdfs:range xsd:decimal .
:quantity rdf:type owl:DatatypeProperty, owl:FunctionalProperty ;
rdfs:domain :OrderItem ; rdfs:range xsd:integer .
:lineTotal rdf:type owl:DatatypeProperty, owl:FunctionalProperty ;
rdfs:domain :OrderItem ; rdfs:range xsd:decimal .
# === OWL Restrictions ===
:Order rdfs:subClassOf [ rdf:type owl:Restriction ;
owl:onProperty :hasOrderItem ; owl:minCardinality "1"^^xsd:nonNegativeInteger ] .
:Order rdfs:subClassOf [ rdf:type owl:Restriction ;
owl:onProperty :orderedBy ; owl:cardinality "1"^^xsd:nonNegativeInteger ] .
:OrderItem rdfs:subClassOf [ rdf:type owl:Restriction ;
owl:onProperty :containsProduct ; owl:cardinality "1"^^xsd:nonNegativeInteger ] .
:Customer rdfs:subClassOf [ rdf:type owl:Restriction ;
owl:onProperty :email ; owl:cardinality "1"^^xsd:nonNegativeInteger ] .
# === Individuals: Tiers & Regions ===
:Bronze rdf:type owl:NamedIndividual, :MembershipTier ; rdfs:label "Bronze"@en .
:Silver rdf:type owl:NamedIndividual, :MembershipTier ; rdfs:label "Silver"@en .
:Gold rdf:type owl:NamedIndividual, :MembershipTier ; rdfs:label "Gold"@en .
:Platinum rdf:type owl:NamedIndividual, :MembershipTier ; rdfs:label "Platinum"@en .
:Northeast rdf:type owl:NamedIndividual, :SalesRegion ; rdfs:label "Northeast"@en .
:West rdf:type owl:NamedIndividual, :SalesRegion ; rdfs:label "West"@en .
:Southeast rdf:type owl:NamedIndividual, :SalesRegion ; rdfs:label "Southeast"@en .
:Midwest rdf:type owl:NamedIndividual, :SalesRegion ; rdfs:label "Midwest"@en .
Step 5: Lambda 関数デプロイ(Neptune へのブリッジ)
Neptune は VPC 内にあるためローカルから直接アクセスできません。VPC 内に Lambda を配置し、SPARQL の実行ブリッジとします。
Lambda コード
import json
import urllib.request
import urllib.error
import urllib.parse
import ssl
NEPTUNE_ENDPOINT = "<YOUR_NEPTUNE_ENDPOINT>"
NEPTUNE_PORT = 8182
def lambda_handler(event, context):
action = event.get('action', 'sparql')
ctx = ssl.create_default_context()
base_url = f"https://{NEPTUNE_ENDPOINT}:{NEPTUNE_PORT}"
if action == 'sparql':
query = event.get('query', 'SELECT ?s ?p ?o WHERE { ?s ?p ?o } LIMIT 10')
url = f"{base_url}/sparql"
payload = urllib.parse.urlencode({'query': query}).encode('utf-8')
req = urllib.request.Request(url, data=payload, method='POST')
req.add_header('Content-Type', 'application/x-www-form-urlencoded')
req.add_header('Accept', 'application/sparql-results+json')
try:
resp = urllib.request.urlopen(req, context=ctx, timeout=60)
return {"statusCode": 200, "body": json.loads(resp.read().decode())}
except urllib.error.HTTPError as e:
return {"statusCode": e.code, "body": e.read().decode()}
except Exception as e:
return {"statusCode": 500, "body": f"{type(e).__name__}: {str(e)}"}
elif action == 'update':
update = event.get('update', '')
url = f"{base_url}/sparql"
payload = urllib.parse.urlencode({'update': update}).encode('utf-8')
req = urllib.request.Request(url, data=payload, method='POST')
req.add_header('Content-Type', 'application/x-www-form-urlencoded')
try:
resp = urllib.request.urlopen(req, context=ctx, timeout=60)
return {"statusCode": 200, "body": resp.read().decode()}
except urllib.error.HTTPError as e:
return {"statusCode": e.code, "body": e.read().decode()}
except Exception as e:
return {"statusCode": 500, "body": f"{type(e).__name__}: {str(e)}"}
デプロイ
zip lambda_function.zip lambda_function.py
aws lambda create-function \
--function-name neptune-ontology-loader \
--runtime python3.12 \
--role arn:aws:iam::<ACCOUNT_ID>:role/NeptuneLambdaRole \
--handler lambda_function.lambda_handler \
--zip-file fileb://lambda_function.zip \
--timeout 120 --memory-size 256 \
--vpc-config SubnetIds=<SUBNET_A>,<SUBNET_B>,SecurityGroupIds=<SG_ID> \
--region us-east-1
ハマりポイント: Lambda の VPC ENI 作成
Lambda を VPC に配置すると ENI の作成に 2〜3分 かかります。Active になるまで待ってください:
aws lambda get-function --function-name neptune-ontology-loader \
--query 'Configuration.State' --output text
ハマりポイント: Neptune は HTTPS
Neptune 1.4 はデフォルトで HTTPS を使用します。HTTP でアクセスすると Connection refused になります。Lambda コード内で https:// を使い、ssl.create_default_context() を渡してください。
Step 6: データ投入
方針: SPARQL INSERT DATA を使う
Neptune のバルクローダー API (POST /loader) は VPC 内から呼び出す必要がありますが、Lambda 経由だとタイムアウトする場合があります。また、SPARQL の LOAD コマンドは S3 プロトコルに非対応です。
データ量が小さい場合(数百トリプル)は SPARQL INSERT DATA で直接投入するのが確実です。
Turtle → SPARQL INSERT DATA 変換
import re, json
def ttl_to_insert_data(ttl_content):
"""Turtle形式をSPARQL INSERT DATA文に変換"""
prefix_lines = []
body_lines = []
for line in ttl_content.split('\n'):
stripped = line.strip()
if stripped.startswith('@prefix'):
match = re.match(r'@prefix\s+(\S+)\s+<([^>]+)>\s*\.', stripped)
if match:
prefix_lines.append(f"PREFIX {match.group(1)} <{match.group(2)}>")
elif stripped.startswith('#') or stripped == '':
continue
else:
body_lines.append(line)
return '\n'.join(prefix_lines) + '\nINSERT DATA {\n' + '\n'.join(body_lines) + '\n}'
# 変換してペイロード生成
with open('ecommerce-ontology.ttl', 'r') as f:
ontology_sparql = ttl_to_insert_data(f.read())
with open('ecommerce-data.ttl', 'r') as f:
data_sparql = ttl_to_insert_data(f.read())
# Lambda 用 JSON ペイロードに書き出し
with open('ontology-payload.json', 'w') as f:
json.dump({"action": "update", "update": ontology_sparql}, f)
with open('data-payload.json', 'w') as f:
json.dump({"action": "update", "update": data_sparql}, f)
投入実行
同名クラスターを再作成した場合、以前のデータが残っていることがあります。確実にクリーンにするには先に DROP ALL を実行してください:
aws lambda invoke --function-name neptune-ontology-loader \
--cli-binary-format raw-in-base64-out \
--payload '{"action": "update", "update": "DROP ALL"}' \
--region us-east-1 /tmp/out.json > /dev/null 2>&1
# オントロジー投入
aws lambda invoke --function-name neptune-ontology-loader \
--cli-binary-format raw-in-base64-out \
--payload file://ontology-payload.json \
--region us-east-1 \
/tmp/out.json > /dev/null 2>&1 && cat /tmp/out.json
# データ投入
aws lambda invoke --function-name neptune-ontology-loader \
--cli-binary-format raw-in-base64-out \
--payload file://data-payload.json \
--region us-east-1 \
/tmp/out.json > /dev/null 2>&1 && cat /tmp/out.json
確認
aws lambda invoke --function-name neptune-ontology-loader \
--cli-binary-format raw-in-base64-out \
--payload '{"action": "sparql", "query": "SELECT (COUNT(*) as ?count) WHERE { ?s ?p ?o }"}' \
--region us-east-1 \
/tmp/out.json > /dev/null 2>&1 && cat /tmp/out.json | python3 -m json.tool
# => 472 triples
前編まとめ
ここまでで以下を構築しました:
| リソース | 内容 |
|---|---|
| Neptune クラスター |
ecommerce-ontology (SPARQL 1.1, db.t3.medium) |
| OWL オントロジー | 11クラス, 6オブジェクトプロパティ, 10データプロパティ, OWL制約 |
| RDF データ | 9顧客, 10商品, 10注文 (計472トリプル) |
| Lambda |
neptune-ontology-loader (SPARQL ブリッジ) |
後編では EC2 + セッションマネージャーで curl ベースの SPARQL クエリ実行環境を構築し、様々なクエリ例を紹介します。