# MyScaleにおけるマルチテナンシーの実装

LLMがより広く認識され、採用されるにつれて、開発者はさまざまなユーザーまたはユーザーグループ向けのアプリケーションを構築するためにCVP(ChatGPT、ベクトルデータベース、プロンプト)スタックを探求しています。マルチテナンシーのサポートをベクトルデータベースに実装することは、この要件を満たすために重要です。このガイドでは、MyScaleにおけるマルチテナンシーの理解と実装方法について説明します。

# マルチテナンシーの理解

ベクトルデータベース(アプリケーション)におけるマルチテナンシーとは、データベースが複数のテナントまたはユーザー(またはユーザーグループ)に対応し、それらのデータを論理的に分離して保持する能力を指します。マルチテナントアーキテクチャでは、テナントごとのデータが別々に格納され、管理されますが、物理的なインフラストラクチャとリソースは共有されます。

ベクトルデータベースアプリケーションにおけるマルチテナンシーの実装には、次の4つの主要な要件を満たす必要があります。

  • パフォーマンスの中立性: すべてのマルチテナントおよび単一ユーザーワークロード、すべてのCRUD(作成、読み取り、更新、削除)操作を含む、一貫したおよび比較可能なパフォーマンスを維持することを重視する概念。
  • データの分離: 各テナントに属するデータは別々に格納されるべきです。
  • スケーラビリティ: アプリケーションは大量のテナントをサポートする必要があります。
  • 効率的なオンボーディングとオフボーディング: テナントの追加や削除が他のテナントに影響を与えないようにする必要があります。

# マルチテナンシーの実装戦略

MyScaleにおけるマルチテナンシーの実装には、データの分離、セキュリティ、パフォーマンスを確保するために慎重な計画と考慮が必要です。

以下は、マルチテナンシーを効果的に実装するためのいくつかの戦略とベストプラクティスです。

# テーブル指向のマルチテナンシー

MyScaleでは、各テーブルは別々に格納されるため、単一のクラスタ内で複数のテーブルを作成することが容易です。各テナントに対して専用のテーブルを作成することができます。例えば、次のSQL文は、チャットボットのテナント用のテーブルを作成します。

CREATE TABLE db.message_chatbot1
(
    user_id      FixedString(16),
    message_id   FixedString(16),
    timestamp    DateTime,
    message_embedding  Array(Float32),
    CONSTRAINT check_length CHECK length(message_embedding) = 768
) ENGINE = MergeTree
ORDER BY message_id

この戦略では、ベクトル検索は単一テナントの場合と同様です。テナントを削除する場合は、そのテーブルを削除するだけで、他のテナントには影響しません。ただし、この戦略はリソースの浪費を引き起こす可能性があり、大規模なマルチテナンシーには適していません。そのため、MyScaleクラスタのポッドサイズx1で100以上のテーブルを持つことは推奨されません。

この戦略はまた、ロールベースのアクセス制御(RBAC)も可能にし、異なるユーザーやロールを各テーブルに割り当てることで、細かいアクセス制御を実現します。

# メタデータフィルタリング指向のマルチテナンシー

メタデータフィルタリング指向のマルチテナンシーは、データベースシステムにおけるマルチテナンシーの実装方法の一つで、複数のテナントのデータが同じテーブル内に格納されます。データへのアクセスは、各データレコードに関連付けられたメタデータや属性に基づいて制御され、効率的なストレージを提供しながら堅牢なデータの分離とセキュリティを実現します。

実際には、メタデータフィルタリング指向のマルチテナンシーは、パーティションキーに基づく方法、プライマリキーに基づく方法、またはその両方の組み合わせで実現できます。

# パーティションキーに基づくマルチテナンシー

データの分離は、各テナントに固有のパーティションを割り当てることで実現されます。次のSQL文は、ユーザーIDでパーティション分割されたテーブルを作成する方法を示しています。ここで、各ユーザーはテナント(パーティション)に対応します。

CREATE TABLE db.message_app
(
    user_id      FixedString(16),
    message_id   FixedString(16),
    timestamp    DateTime,
    message_embedding  Array(Float32),
    CONSTRAINT check_length CHECK length(message_embedding) = 768
) ENGINE = MergeTree
ORDER BY message_id
PARTITION BY user_id

ベクトル検索を実行する際には、テナントフィールドをフィルターとして使用して適切なパーティションを特定するため、次のSQL文のようになります。

SELECT message_id, distance(data, [...]) AS dist
FROM db.message_app 
WHERE user_id = 'xxxxxxxxxxxxxxxx'
ORDER BY dist LIMIT 10

さらに、次のコマンドを使用して簡単にパーティション(テナント)を削除することができます。

ALTER TABLE db.message_app DROP PARTITION 'xxxxxxxxxxxxxxxx'

TIP

最適なINSERTおよびクエリのパフォーマンスを実現するためには、MyScaleクラスタのポッドサイズx1でテーブル内のパーティションの総数を100以下に保つことが望ましいです。

# プライマリキーに基づくマルチテナンシー

この戦略では、テナントフィールドがプライマリキーとなり、テナントデータの取得が高速化されます。

この戦略では、テナントの数に制限はありません。異なるテナントのデータはすべて同じパーティションに格納されます。例えば、次のSQL文は、各ユーザーをテナントとして扱うテーブルの作成方法を示しています。

CREATE TABLE db.message_app
(
    user_id      FixedString(16),
    timestamp    DateTime,
    message_id   FixedString(16),
    message_embedding  Array(Float32),
    CONSTRAINT check_length CHECK length(message_embedding) = 768
) ENGINE = MergeTree
ORDER BY (user_id, message_id)

この戦略では、パーティションキーに基づく戦略と同様にベクトル検索が行われます。

ただし、次のSQL文のようにテナントを削除する場合は、削除には時間がかかり、DELETE FROM文が必要です。

DELETE FROM TABLE db.message_app WHERE user_id = 'xxxxxxxxxxxxxxxx' 

# パーティション + プライマリキーに基づくマルチテナンシー

パーティションとプライマリキーのマルチテナンシー戦略を組み合わせることで、リソースの分離とテナントのスケーラビリティをバランスさせることができます。この方法では、複数のテナントのデータを同じパーティションに格納し、大規模なマルチテナンシーをサポートしながら適度な分離を維持します。

例えば、次のSQL文は、テナントデータを10個のパーティションに分散して格納する方法を示しています。

CREATE TABLE db.message_app
(
    user_id      FixedString(16),
    timestamp    DateTime,
    message_id   FixedString(16),
    message_embedding  Array(Float32),
    CONSTRAINT check_length CHECK length(message_embedding) = 768
) ENGINE = MergeTree
ORDER BY (user_id, message_id)
PARTITION BY sipHash64(user_id) % 10

ベクトル検索の実行とテナントの削除は、プライマリキーに基づく戦略と同様です。ただし、INSERTのパフォーマンスは遅くなる場合があります。特に、単一の挿入ブロック内で複数のパーティションを処理する場合は、遅くなる可能性があります。そのため、同じパーティション内のデータを単一のブロックで挿入することがこの戦略のベストプラクティスです。

# まとめ

以下は、私たちが推奨し、使用しているマルチテナンシーの戦略の概要です。

マルチテナンシーモデル パフォーマンスオーバーヘッド 分離レベル テナントのスケール オンボーディング/オフボーディングの容易さ
テーブル指向 強い 中程度
パーティションキーに基づく 中程度 中程度
プライマリキーに基づく 弱い 大規模 中程度
パーティション + プライマリキーに基づく 中程度 弱い 大規模 中程度