データベース

Azure Cosmos DB

概要

Azure Cosmos DBは、Microsoftが提供するグローバル分散型のマルチモデルデータベースサービスです。高可用性、自動スケーリング、一桁ミリ秒の応答時間を特徴とし、NoSQLとベクターデータベースの機能を組み合わせた最新のクラウドネイティブデータベースです。複数のAPIをサポートし、世界中のあらゆる規模のアプリケーションに対応します。

詳細

Azure Cosmos DBは2017年にMicrosoftによって発表され、従来のDocumentDBを進化させた次世代のデータベースサービスです。以下の特徴を持ちます。

主要特徴

  • グローバル分散: 世界54以上のリージョンで即座にデータを複製
  • マルチモデル: SQL、MongoDB、Cassandra、Gremlin、Table APIをサポート
  • 自動スケーリング: オートパイロット機能による無制限スケーリング
  • 低レイテンシ: 読み取り・書き込みともに10ms未満
  • 複数の整合性レベル: Strong、Bounded staleness、Session、Consistent prefix、Eventualから選択
  • AI統合: ベクター検索、フルテキスト検索、ハイブリッド検索
  • 無制限のスループット: 任意のスケールでのパフォーマンス保証
  • 包括的SLA: 可用性99.999%、レイテンシ、スループット、整合性を保証

2024-2025年の新機能

  • ベクター検索: DiskANNアルゴリズムによる高性能ベクター検索
  • フルテキスト検索: BM25アルゴリズムによるキーワード・フレーズ検索
  • ハイブリッド検索: ベクター検索とフルテキスト検索の組み合わせ
  • 多言語サポート: 英語、フランス語、スペイン語、ドイツ語対応
  • AI Foundry統合: Azure AIとの統合によるエージェントアプリケーション開発
  • ファジー検索: タイポや文字変更への耐性

アーキテクチャ

  • データモデル: 階層構造(アカウント > データベース > コンテナ > アイテム)
  • 自動インデックス: 全フィールドの自動インデックス作成
  • スキーマレス: 柔軟なJSONドキュメント構造
  • パーティション: 水平分散によるスケーラビリティ
  • マルチマスター: 複数リージョンでの書き込み対応

整合性レベル

  • Strong: 線形化可能性保証
  • Bounded staleness: 設定可能な遅延上限
  • Session: 単一セッション内での読み取り一貫性
  • Consistent prefix: プレフィックス一貫性
  • Eventual: 結果整合性

メリット・デメリット

メリット

  • グローバルスケール: 世界規模での自動データ分散と複製
  • 高パフォーマンス: 一桁ミリ秒の応答時間とオートスケーリング
  • 包括的SLA: 99.999%可用性、レイテンシ、スループット保証
  • マルチモデル: 複数のAPIとデータモデルを単一サービスで提供
  • 運用不要: フルマネージドサービスでインフラ管理不要
  • AI機能: ベクター検索、フルテキスト検索、AI統合
  • 柔軟な整合性: アプリケーションニーズに応じた整合性レベル選択
  • Azure統合: Azureエコシステムとの完全統合

デメリット

  • 高いコスト: 従量課金制で大規模利用時は高額になる可能性
  • Azure依存: Azureクラウドのみでの提供、ベンダーロックイン
  • 学習コスト: 複数のAPIと設定オプションの理解が必要
  • 複雑な料金体系: RU(Request Unit)ベースの課金体系
  • データサイズ制限: アイテムあたり2MBの制限
  • クエリ制限: 複雑なクエリや集計処理に制約
  • リージョン制限: 一部リージョンでは利用不可

主要リンク

書き方の例

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

# Azure CLIでCosmos DBアカウント作成
az cosmosdb create \
  --name mycosmosaccount \
  --resource-group myresourcegroup \
  --default-consistency-level Session \
  --locations regionName=Japan East failoverPriority=0 isZoneRedundant=False

# データベースとコンテナ作成
az cosmosdb sql database create \
  --account-name mycosmosaccount \
  --name mydatabase \
  --resource-group myresourcegroup

az cosmosdb sql container create \
  --account-name mycosmosaccount \
  --database-name mydatabase \
  --name mycontainer \
  --partition-key-path "/partitionKey" \
  --throughput 400 \
  --resource-group myresourcegroup

# Node.js SDK インストール
npm install @azure/cosmos

# .NET SDK インストール
dotnet add package Microsoft.Azure.Cosmos

# Python SDK インストール
pip install azure-cosmos

基本操作(CRUD)

// Node.js での接続と基本操作
const { CosmosClient } = require('@azure/cosmos');

// 接続設定
const client = new CosmosClient({
  endpoint: 'https://myaccount.documents.azure.com:443/',
  key: 'your-primary-key'
});

const database = client.database('mydatabase');
const container = database.container('mycontainer');

async function basicOperations() {
  try {
    // ドキュメント作成
    const newItem = {
      id: 'item-001',
      partitionKey: 'electronics',
      name: 'ノートパソコン',
      brand: 'Sample Brand',
      price: 120000,
      category: 'electronics',
      specifications: {
        cpu: 'Intel Core i7',
        memory: '16GB',
        storage: '512GB SSD'
      },
      tags: ['コンピュータ', 'ビジネス', 'モバイル'],
      createdAt: new Date().toISOString()
    };

    const { resource: createdItem } = await container.items.create(newItem);
    console.log('作成されたアイテム:', createdItem);

    // ドキュメント読み取り
    const { resource: readItem } = await container.item('item-001', 'electronics').read();
    console.log('読み取ったアイテム:', readItem);

    // ドキュメント更新
    readItem.price = 115000;
    readItem.updatedAt = new Date().toISOString();
    const { resource: updatedItem } = await container.item('item-001', 'electronics').replace(readItem);
    console.log('更新されたアイテム:', updatedItem);

    // クエリ実行
    const querySpec = {
      query: 'SELECT * FROM c WHERE c.category = @category AND c.price < @maxPrice',
      parameters: [
        { name: '@category', value: 'electronics' },
        { name: '@maxPrice', value: 150000 }
      ]
    };

    const { resources: results } = await container.items.query(querySpec).fetchAll();
    console.log('クエリ結果:', results);

    // ドキュメント削除
    await container.item('item-001', 'electronics').delete();
    console.log('アイテムが削除されました');

  } catch (error) {
    console.error('操作中にエラーが発生しました:', error);
  }
}

basicOperations();

データモデリング

// 複雑なドキュメント構造の例
const customerDocument = {
  id: 'customer-12345',
  partitionKey: 'customer-12345',
  customerType: 'premium',
  profile: {
    firstName: '田中',
    lastName: '太郎',
    email: '[email protected]',
    phone: '090-1234-5678',
    birthDate: '1985-04-15',
    address: {
      zipCode: '100-0001',
      prefecture: '東京都',
      city: '千代田区',
      street: '千代田1-1-1',
      building: 'サンプルビル101'
    }
  },
  orders: [
    {
      orderId: 'order-001',
      orderDate: '2024-01-15T10:30:00Z',
      status: 'delivered',
      items: [
        {
          productId: 'prod-001',
          name: 'ワイヤレスヘッドフォン',
          quantity: 1,
          unitPrice: 25000,
          totalPrice: 25000
        }
      ],
      totalAmount: 25000,
      deliveryAddress: {
        zipCode: '100-0001',
        prefecture: '東京都',
        city: '千代田区',
        street: '千代田1-1-1'
      }
    }
  ],
  preferences: {
    language: 'ja',
    currency: 'JPY',
    notifications: {
      email: true,
      sms: false,
      push: true
    },
    categories: ['electronics', 'books', 'clothing']
  },
  loyaltyProgram: {
    tier: 'gold',
    points: 15000,
    joinDate: '2023-01-01T00:00:00Z'
  },
  metadata: {
    createdAt: '2023-01-01T00:00:00Z',
    updatedAt: '2024-01-15T15:45:00Z',
    version: 2,
    source: 'mobile-app'
  }
};

// インデックス戦略
async function indexingStrategy() {
  // 複合インデックス設定例
  const indexingPolicy = {
    indexingMode: 'consistent',
    automatic: true,
    includedPaths: [
      {
        path: '/*'
      }
    ],
    excludedPaths: [
      {
        path: '/metadata/*'
      }
    ],
    compositeIndexes: [
      [
        { path: '/customerType', order: 'ascending' },
        { path: '/profile/email', order: 'ascending' }
      ],
      [
        { path: '/orders/[]/status', order: 'ascending' },
        { path: '/orders/[]/orderDate', order: 'descending' }
      ]
    ]
  };

  console.log('インデックス戦略:', indexingPolicy);
}

ベクター検索・AI機能

// ベクター検索の実装例
async function vectorSearchExample() {
  // ベクター埋め込み付きドキュメント
  const documentWithVector = {
    id: 'doc-vector-001',
    partitionKey: 'documents',
    title: 'Azure Cosmos DB の紹介',
    content: 'Azure Cosmos DB は、グローバルに分散されたマルチモデルデータベースサービスです。',
    category: 'technology',
    // OpenAI等で生成されたベクター埋め込み
    contentVector: [0.1, 0.2, -0.3, 0.4, 0.5, /* ... 1536次元のベクター */],
    createdAt: new Date().toISOString()
  };

  await container.items.create(documentWithVector);

  // ベクター検索クエリ
  const searchVector = [0.15, 0.25, -0.25, 0.35, 0.45]; // 検索用ベクター
  
  const vectorSearchQuery = {
    query: `
      SELECT TOP 10 c.id, c.title, c.content, 
             VectorDistance(c.contentVector, @searchVector) AS similarity
      FROM c 
      WHERE c.category = @category
      ORDER BY VectorDistance(c.contentVector, @searchVector)
    `,
    parameters: [
      { name: '@searchVector', value: searchVector },
      { name: '@category', value: 'technology' }
    ]
  };

  const { resources: vectorResults } = await container.items.query(vectorSearchQuery).fetchAll();
  console.log('ベクター検索結果:', vectorResults);

  // ハイブリッド検索(フルテキスト + ベクター)
  const hybridSearchQuery = {
    query: `
      SELECT c.id, c.title, c.content,
             VectorDistance(c.contentVector, @searchVector) AS vectorSimilarity,
             RANK FullTextScore(c.content, @searchTerms) AS textScore
      FROM c 
      WHERE CONTAINS(c.content, @searchTerms)
      ORDER BY (VectorDistance(c.contentVector, @searchVector) * 0.6 + 
                (1.0 - RANK FullTextScore(c.content, @searchTerms)) * 0.4)
    `,
    parameters: [
      { name: '@searchVector', value: searchVector },
      { name: '@searchTerms', value: 'データベース サービス' }
    ]
  };

  const { resources: hybridResults } = await container.items.query(hybridSearchQuery).fetchAll();
  console.log('ハイブリッド検索結果:', hybridResults);
}

パフォーマンス最適化

// 高パフォーマンス操作の実装
class CosmosDBOptimizer {
  constructor(container) {
    this.container = container;
  }

  // バルクオペレーション
  async bulkInsert(documents) {
    const operations = documents.map(doc => ({
      operationType: 'Create',
      resourceBody: doc
    }));

    const { result, statusCode } = await this.container.items.bulk(operations);
    console.log(`バルク挿入完了: ${result.length}件, ステータス: ${statusCode}`);
    return result;
  }

  // 効率的なページネーション
  async paginateItems(querySpec, pageSize = 100) {
    const iterator = this.container.items.query(querySpec, {
      maxItemCount: pageSize
    });

    const results = [];
    while (iterator.hasMoreResults()) {
      const { resources, continuationToken } = await iterator.fetchNext();
      results.push(...resources);
      
      console.log(`取得件数: ${resources.length}, 継続トークン: ${continuationToken}`);
      
      if (results.length >= 1000) { // 最大1000件で停止
        break;
      }
    }
    
    return results;
  }

  // トランザクショナル操作
  async transactionalBatch(partitionKey, operations) {
    const transactionalBatch = this.container.items.batch(operations, partitionKey);
    const { result } = await transactionalBatch.execute();
    
    console.log('トランザクション実行結果:', result);
    return result;
  }

  // 効率的な集計クエリ
  async aggregateData() {
    const aggregateQuery = {
      query: `
        SELECT 
          c.category,
          COUNT(1) as itemCount,
          AVG(c.price) as averagePrice,
          SUM(c.price) as totalValue,
          MIN(c.price) as minPrice,
          MAX(c.price) as maxPrice
        FROM c 
        GROUP BY c.category
      `
    };

    const { resources } = await this.container.items.query(aggregateQuery).fetchAll();
    return resources;
  }
}

// 使用例
const optimizer = new CosmosDBOptimizer(container);

// パフォーマンス監視
async function monitorPerformance() {
  const startTime = Date.now();
  
  try {
    const result = await container.items.query({
      query: 'SELECT * FROM c WHERE c.category = @category',
      parameters: [{ name: '@category', value: 'electronics' }]
    }).fetchAll();
    
    const endTime = Date.now();
    const duration = endTime - startTime;
    
    console.log(`クエリ実行時間: ${duration}ms`);
    console.log(`結果件数: ${result.resources.length}`);
    console.log(`RU消費量: ${result.requestCharge}`);
    
  } catch (error) {
    console.error('クエリエラー:', error);
  }
}

セキュリティ・ベストプラクティス

// セキュリティ設定とベストプラクティス
const { DefaultAzureCredential } = require('@azure/identity');

// Azure ADによる認証
const aadClient = new CosmosClient({
  endpoint: 'https://myaccount.documents.azure.com:443/',
  aadCredentials: new DefaultAzureCredential()
});

// データ暗号化設定
const encryptionConfig = {
  encryptionKey: {
    encryptionAlgorithm: 'AEAD_AES_256_CBC_HMAC_SHA256',
    wrappingAlgorithm: 'RSA_OAEP',
    keyWrapMetadata: {
      name: 'my-key',
      type: 'AzureKeyVault',
      value: 'https://my-keyvault.vault.azure.net/keys/my-key'
    }
  }
};

// 接続時のセキュリティ設定
const secureClient = new CosmosClient({
  endpoint: 'https://myaccount.documents.azure.com:443/',
  key: 'your-key',
  connectionPolicy: {
    enableEndpointDiscovery: false,
    preferredLocations: ['Japan East'],
    useMultipleWriteLocations: false
  },
  plugins: [
    {
      on: 'request',
      plugin: async (context, diagNode) => {
        // セキュリティヘッダーの追加
        context.headers['x-ms-client-version'] = 'secure-app-v1.0';
        return context;
      }
    }
  ]
});

// リソースベースアクセス制御
async function setupResourcePermissions() {
  // 読み取り専用ユーザー用のリソーストークン生成例
  const permissions = {
    permissionMode: 'Read',
    resource: 'dbs/mydatabase/colls/mycontainer',
    tokenExpiryTime: new Date(Date.now() + 60 * 60 * 1000) // 1時間有効
  };
  
  console.log('リソース権限設定:', permissions);
}

// データマスキング
function maskSensitiveData(document) {
  const masked = { ...document };
  
  // 個人情報のマスキング
  if (masked.profile?.email) {
    masked.profile.email = masked.profile.email.replace(/(.{2}).*(@.*)/, '$1***$2');
  }
  
  if (masked.profile?.phone) {
    masked.profile.phone = masked.profile.phone.replace(/(\d{3}).*(\d{4})/, '$1-****-$2');
  }
  
  return masked;
}