データベース

ArangoDB

概要

ArangoDBは、ドキュメント、グラフ、キーバリューの3つのデータモデルを単一のエンジンで統合したマルチモデルデータベースです。独自のクエリ言語「AQL(ArangoDB Query Language)」により、異なるデータモデル間を横断する複雑なクエリを効率的に実行できます。ネイティブグラフ処理、ACID特性、水平スケーリングを兼ね備えた高性能なNoSQLデータベースソリューションです。

詳細

ArangoDBは2011年にドイツで開発が開始され、2012年にオープンソースとして公開されました。従来のデータベースでは複数のシステムが必要だった用途を、単一のデータベースで解決できる革新的なアプローチが特徴です。

ArangoDBの主な特徴:

  • マルチモデルアーキテクチャ: ドキュメント、グラフ、キーバリューを統合
  • AQLクエリ言語: SQLライクな統一クエリ言語
  • ネイティブグラフ処理: 高速なトラバーサルアルゴリズム
  • ACID特性: 完全なトランザクション保証
  • 分散アーキテクチャ: 自動シャーディングとレプリケーション
  • Foxxマイクロサービス: データベース内でのアプリケーション実行
  • RESTful API: HTTPベースのアクセスインターフェース
  • 地理空間インデックス: GeoJSONサポート
  • 全文検索: ArangoSearchエンジン内蔵
  • Web インターフェース: 直感的な管理・可視化ツール

メリット・デメリット

メリット

  • 統合性: 複数のデータモデルを単一システムで管理
  • 柔軟性: スキーマレス設計で進化するデータに対応
  • パフォーマンス: 最適化されたクエリ実行エンジン
  • スケーラビリティ: 水平スケーリングとクラスタリング
  • 開発効率: 統一されたクエリ言語による開発生産性向上
  • 豊富な機能: グラフ分析、全文検索、地理空間処理を内蔵
  • マイクロサービス: Foxxによるサーバーサイドアプリケーション
  • 運用性: Web UI による直感的な管理

デメリット

  • 学習コスト: AQLとマルチモデル概念の習得が必要
  • メモリ使用量: インメモリ処理により多くのメモリを消費
  • エコシステム: 特定分野ではNeo4jやMongoDBより限定的
  • 複雑性: 多機能ゆえの設定・チューニングの複雑さ
  • 特化システムとの差: 単一モデル特化DBと比較した性能劣化の可能性

主要リンク

書き方の例

インストール・セットアップ

# Docker での実行
docker run -e ARANGO_ROOT_PASSWORD=password \
  -p 8529:8529 \
  -v arango-data:/var/lib/arangodb3 \
  -v arango-apps:/var/lib/arangodb3-apps \
  arangodb:latest

# Docker Compose 設定
cat > docker-compose.yml << EOF
version: '3.8'
services:
  arangodb:
    image: arangodb:latest
    environment:
      ARANGO_ROOT_PASSWORD: password
    ports:
      - "8529:8529"
    volumes:
      - arango-data:/var/lib/arangodb3
      - arango-apps:/var/lib/arangodb3-apps
volumes:
  arango-data:
  arango-apps:
EOF

docker-compose up -d

# Web インターフェースアクセス
# http://localhost:8529

# Python ドライバーインストール
pip install python-arango

# Node.js ドライバーインストール
npm install arangojs

# ArangoDB CLI クライアント(arangosh)
docker exec -it arangodb_container arangosh

基本操作(マルチモデルCRUD)

// AQLによる基本CRUD操作

// コレクション作成
db._create("users");          // ドキュメントコレクション
db._createEdgeCollection("friends"); // エッジコレクション

// ドキュメント作成(Create)
FOR doc IN [
  {name: "田中太郎", age: 30, email: "[email protected]", city: "東京"},
  {name: "佐藤花子", age: 25, email: "[email protected]", city: "大阪"},
  {name: "鈴木一郎", age: 35, email: "[email protected]", city: "名古屋"}
]
INSERT doc INTO users

// キーバリューアクセス
INSERT {_key: "user001", name: "山田太郎", status: "active"} INTO users

// ドキュメント読み取り(Read)
FOR user IN users
  RETURN user

// 条件付きクエリ
FOR user IN users
  FILTER user.age > 25
  RETURN {name: user.name, age: user.age}

// キーによる直接アクセス
RETURN DOCUMENT("users/user001")

// ドキュメント更新(Update)
FOR user IN users
  FILTER user.name == "田中太郎"
  UPDATE user WITH {age: 31, location: "東京都渋谷区"} IN users

// 条件付き更新
UPDATE {_key: "user001"} WITH {lastLogin: DATE_NOW()} IN users

// ドキュメント削除(Delete)
FOR user IN users
  FILTER user.email == "[email protected]"
  REMOVE user IN users

// キー指定削除
REMOVE "user001" IN users

// エッジ(関係性)作成
INSERT {_from: "users/user1", _to: "users/user2", type: "friend", since: "2024-01-01"} 
INTO friends

マルチモデルクエリ

// ドキュメント + グラフ + キーバリューの統合クエリ

// グラフトラバーサル
FOR v, e, p IN 1..3 OUTBOUND "users/user1" friends
  RETURN {
    friend: v.name,
    relationship: e.type,
    path_length: LENGTH(p.edges)
  }

// ドキュメント検索 + グラフ分析
FOR user IN users
  FILTER user.city == "東京"
  FOR friend IN 1..2 OUTBOUND user friends
    COLLECT friendCity = friend.city WITH COUNT INTO friendCount
    RETURN {
      city: friendCity,
      friends_count: friendCount
    }

// キーバリュー + ドキュメント結合
LET userKeys = ["user001", "user002", "user003"]
FOR key IN userKeys
  LET user = DOCUMENT(CONCAT("users/", key))
  FILTER user != null
  RETURN {
    key: key,
    user: user,
    is_active: user.status == "active"
  }

// 複合データモデルクエリ
FOR order IN orders
  LET customer = DOCUMENT(order.customer_id)
  LET items = (
    FOR item_id IN order.items
      RETURN DOCUMENT(CONCAT("products/", item_id))
  )
  RETURN {
    order_id: order._key,
    customer_name: customer.name,
    total_items: LENGTH(items),
    total_value: SUM(items[*].price)
  }

インデックス・最適化

// インデックス作成
db.users.ensureIndex({type: "hash", fields: ["email"], unique: true});
db.users.ensureIndex({type: "skiplist", fields: ["age"]});
db.users.ensureIndex({type: "fulltext", fields: ["name", "description"]});

// 地理空間インデックス
db.locations.ensureIndex({type: "geo", fields: ["coordinates"]});

// 複合インデックス
db.users.ensureIndex({type: "skiplist", fields: ["city", "age"]});

// インデックス確認
db.users.getIndexes();

// クエリ最適化分析
db._explain(`
  FOR user IN users
    FILTER user.age > 25 AND user.city == "東京"
    RETURN user
`);

// プロファイリング
db._profile(`
  FOR user IN users
    FOR friend IN 1..2 OUTBOUND user friends
    RETURN {user: user.name, friend: friend.name}
`);

// 全文検索インデックス
db.articles.ensureIndex({type: "fulltext", fields: ["title", "content"]});

// 全文検索クエリ
FOR doc IN FULLTEXT(articles, "title,content", "ArangoDB マルチモデル")
  RETURN doc

高度な機能

// 地理空間クエリ
FOR location IN locations
  FILTER GEO_DISTANCE(location.coordinates, [139.6917, 35.6895]) < 1000
  RETURN {
    name: location.name,
    distance: GEO_DISTANCE(location.coordinates, [139.6917, 35.6895])
  }

// グラフアルゴリズム(最短パス)
FOR path IN OUTBOUND SHORTEST_PATH "users/alice" TO "users/bob" friends
  RETURN path

// 中心性分析
FOR user IN users
  LET connections = LENGTH(
    FOR v IN 1..1 ANY user friends
      RETURN v
  )
  SORT connections DESC
  LIMIT 10
  RETURN {user: user.name, connections: connections}

// ウィンドウ関数
FOR sale IN sales
  SORT sale.date
  RETURN {
    date: sale.date,
    amount: sale.amount,
    running_total: SUM(
      FOR s IN sales
        FILTER s.date <= sale.date
        RETURN s.amount
    )
  }

// 配列操作
FOR user IN users
  FILTER LENGTH(user.skills) > 0
  RETURN {
    name: user.name,
    primary_skill: FIRST(user.skills),
    skill_count: LENGTH(user.skills),
    has_javascript: "JavaScript" IN user.skills
  }

// JSON処理
FOR document IN documents
  LET parsed = JSON_PARSE(document.json_data)
  FILTER parsed.type == "user_event"
  RETURN {
    id: document._key,
    event_type: parsed.event_type,
    timestamp: parsed.timestamp
  }

実用例

// 推薦システム
FOR user IN users
  FILTER user._key == "current_user"
  FOR friend IN 2..3 OUTBOUND user friends
    FOR product IN 1..1 OUTBOUND friend purchases
      FILTER NOT (user)-[:PURCHASED]->(product)
      COLLECT productId = product._key WITH COUNT INTO score
      SORT score DESC
      LIMIT 5
      RETURN {
        product_id: productId,
        recommendation_score: score
      }

// 不正検知(異常パターン)
FOR transaction IN transactions
  FILTER transaction.amount > 100000
  AND transaction.timestamp > DATE_SUBTRACT(DATE_NOW(), 1, "day")
  LET user_transactions = (
    FOR t IN transactions
      FILTER t.user_id == transaction.user_id
      AND t.timestamp > DATE_SUBTRACT(DATE_NOW(), 1, "day")
      RETURN t
  )
  FILTER LENGTH(user_transactions) > 10
  RETURN {
    user_id: transaction.user_id,
    suspicious_transactions: LENGTH(user_transactions),
    total_amount: SUM(user_transactions[*].amount)
  }

// ソーシャルネットワーク分析
FOR user IN users
  FILTER user._key == "target_user"
  FOR friend IN 2..2 OUTBOUND user friends
    FILTER friend._key != user._key
    AND NOT (user)-[:FRIENDS_WITH]->(friend)
    COLLECT suggestion = friend WITH COUNT INTO mutualFriends
    SORT mutualFriends DESC
    LIMIT 5
    RETURN {
      suggested_friend: suggestion.name,
      mutual_connections: mutualFriends
    }

// リアルタイム分析ダッシュボード
LET stats = {
  total_users: LENGTH(users),
  active_sessions: LENGTH(
    FOR session IN sessions
      FILTER session.last_activity > DATE_SUBTRACT(DATE_NOW(), 15, "minute")
      RETURN session
  ),
  recent_orders: LENGTH(
    FOR order IN orders
      FILTER order.created_at > DATE_SUBTRACT(DATE_NOW(), 1, "hour")
      RETURN order
  )
}
RETURN stats

Pythonでの使用例

from arango import ArangoClient

# データベース接続
client = ArangoClient(hosts='http://localhost:8529')
sys_db = client.db('_system', username='root', password='password')

# データベース作成
if not sys_db.has_database('example_db'):
    sys_db.create_database('example_db')

# データベース接続
db = client.db('example_db', username='root', password='password')

# コレクション作成
if not db.has_collection('users'):
    users = db.create_collection('users')
else:
    users = db.collection('users')

if not db.has_collection('friends'):
    friends = db.create_collection('friends', edge=True)
else:
    friends = db.collection('friends')

# ドキュメント操作
def create_user(name, age, email):
    user_data = {
        'name': name,
        'age': age,
        'email': email,
        'created_at': '2024-01-01'
    }
    return users.insert(user_data)

def find_users_by_city(city):
    aql = """
    FOR user IN users
        FILTER user.city == @city
        RETURN user
    """
    return list(db.aql.execute(aql, bind_vars={'city': city}))

def create_friendship(user1_id, user2_id):
    edge_data = {
        '_from': f'users/{user1_id}',
        '_to': f'users/{user2_id}',
        'type': 'friend',
        'created_at': '2024-01-01'
    }
    return friends.insert(edge_data)

def get_user_friends(user_id):
    aql = """
    FOR v, e, p IN 1..1 OUTBOUND @start_vertex friends
        RETURN {
            friend: v,
            relationship: e,
            path_length: LENGTH(p.edges)
        }
    """
    return list(db.aql.execute(aql, bind_vars={'start_vertex': f'users/{user_id}'}))

# 実行例
user1 = create_user("田中太郎", 30, "[email protected]")
user2 = create_user("佐藤花子", 25, "[email protected]")

friendship = create_friendship(user1['_key'], user2['_key'])
friends_list = get_user_friends(user1['_key'])

print(f"Created users: {user1['_key']}, {user2['_key']}")
print(f"Friends: {len(friends_list)}")

設定とチューニング

# arangod.conf の主要設定

[server]
endpoint = tcp://0.0.0.0:8529

[database]
maximal-journal-size = 1073741824

[cache]
size = 2147483648

[javascript]
startup-directory = /var/lib/arangodb3-apps

[cluster]
my-role = SINGLE

[rocksdb]
block-cache-size = 1073741824
total-write-buffer-size = 536870912

[log]
level = info
file = /var/log/arangodb3/arangod.log

[ssl]
keyfile = /etc/ssl/arangodb/server.pem

# パフォーマンスチューニング
[query]
cache-mode = on
smart-joins = true

# セキュリティ設定
[server]
authentication = true
jwt-secret = your-secret-key

[ssl]
protocol = 5