ユーザーデータの永続化
ユーザープロフィール、設定情報、サブスクリプション状態の保存。認証・認可と連携したアクセス制御。マルチテナント対応のデータ分離。SaaSモデルでは必須のデータ層です。
RAGのベクトルストア
テキストや画像をベクトル(エンベディング)に変換して保存し、意味的に類似したコンテンツを高速検索します。RAG(検索拡張生成)の基盤として不可欠な機能。ハルシネーション削減の切り札です。
チャット履歴の管理
AIチャットの会話履歴を保存し、コンテキストの継続性を実現。セッション管理、スレッド管理、メッセージの検索・フィルタリング。ユーザー体験の向上に直結する重要な機能です。
分析ログ・改善サイクル
AIの入出力ログ、ユーザー行動データ、フィードバック(いいね/悪いね)の蓄積。プロンプトの改善、モデルの微調整、A/Bテストの基盤。データドリブンなAI改善に不可欠です。
スケーラビリティ
ユーザー数やデータ量の増加に伴い、性能を維持できるか。サーバーレス型はオートスケーリングが標準。垂直スケーリング(マシンスペックUP)と水平スケーリング(レプリカ追加)の両方を評価しましょう。
コスト
無料枠の充実度、従量課金の単価、予測可能性を比較。MVP段階では無料枠で十分か。スケール後のコスト推移をシミュレーション。ベクトル検索のコンピュートコストも見落とさないように。
開発速度とベクトル検索対応
SDKの充実度、ドキュメントの質、コミュニティの活発さ。そしてベクトル検索(pgvector等)のネイティブ対応。AIアプリでは後者が特に重要です。後付けよりネイティブ対応を選びましょう。
オープンソースのFirebase代替として急成長中のBaaS。PostgreSQLをベースに、pgvectorによるベクトル検索、RLS(Row Level Security)による細かなアクセス制御、Edge Functionsによるサーバーレス関数、リアルタイムサブスクリプション、認証・ストレージを統合的に提供します。AI開発のフルスタックバックエンドとして最も人気の高い選択肢です。
サーバーレスPostgreSQLのパイオニア。ストレージとコンピュートを分離したアーキテクチャにより、使用していない時はゼロにスケールダウン。ブランチング機能でDBのコピーを瞬時に作成でき、プレビュー環境やCI/CDとの統合が容易。pgvector対応でAI開発にも最適です。
Vitess(YouTube開発のMySQL互換分散DB)を基盤としたサーバーレスDBプラットフォーム。Gitのようなスキーマブランチング、ゼロダウンタイムのマイグレーション、自動シャーディングが特徴。大規模なMySQLワークロードに最適化されており、エンタープライズ利用で高い実績があります。
Google製のNoSQLドキュメントDB。リアルタイム同期が最大の強みで、クライアント-サーバー間のデータ変更を即座に反映。Firebase Authenticationによる認証統合、Cloud Functionsとの連携、優れたモバイルSDKを提供。プロトタイピングの速度は随一です。
libSQL(SQLiteのフォーク)をベースにしたエッジデータベース。世界中のエッジロケーションにレプリカを配置し、ユーザーに最も近い場所からデータを提供。超低レイテンシーを実現します。SQLite互換のためローカル開発との親和性が高く、組み込みリプリカでオフライン対応も可能です。
| 項目 | Supabase | Neon | PlanetScale | Firebase | Turso |
|---|---|---|---|---|---|
| スケーラビリティ | ○ 良好 | ◎ 自動スケール | ◎ 自動シャーディング | ◎ Google基盤 | ○ エッジ分散 |
| ベクトル検索 | ◎ pgvector | ◎ pgvector | △ 非対応 | △ 非対応 | △ 非対応 |
| リアルタイム同期 | ○ 対応 | △ 非対応 | △ 非対応 | ◎ 最強 | △ 非対応 |
| 無料枠 | ◎ 充実 | ◎ 充実 | ○ なし | ◎ 充実 | ◎ 充実 |
| 学習コスト | ◎ 低い | ○ 普通 | ○ 普通 | ◎ 低い | ○ 普通 |
| 日本語ドキュメント | ○ コミュニティ | △ 少ない | △ 少ない | ◎ 充実 | △ 少ない |
-- pgvector 拡張を有効化 CREATE EXTENSION IF NOT EXISTS vector; -- エンベディングを保存するテーブルを作成 CREATE TABLE documents ( id bigserial PRIMARY KEY, user_id uuid REFERENCES auth.users(id), content text NOT NULL, metadata jsonb DEFAULT '{}', embedding vector(1536), -- OpenAI text-embedding-3-small の次元数 created_at timestamptz DEFAULT now() ); -- HNSW インデックスを作成(IVFFlatより高速・高精度) CREATE INDEX ON documents USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 64);
-- ベクトル類似検索用のRPC関数を作成 CREATE OR REPLACE FUNCTION match_documents( query_embedding vector(1536), match_threshold float DEFAULT 0.7, match_count int DEFAULT 5 ) RETURNS TABLE ( id bigint, content text, metadata jsonb, similarity float ) LANGUAGE plpgsql AS $$ BEGIN RETURN QUERY SELECT documents.id, documents.content, documents.metadata, 1 - (documents.embedding <=> query_embedding) AS similarity FROM documents WHERE 1 - (documents.embedding <=> query_embedding) > match_threshold ORDER BY documents.embedding <=> query_embedding LIMIT match_count; END; $$;
import { createClient } from '@supabase/supabase-js'; const supabase = createClient( process.env.SUPABASE_URL, process.env.SUPABASE_ANON_KEY ); // ドキュメントをエンベディングに変換して保存 async function storeDocument(content: string, metadata: object) { const embeddingRes = await openai.embeddings.create({ model: 'text-embedding-3-small', input: content, }); const { data, error } = await supabase .from('documents') .insert({ content, metadata, embedding: embeddingRes.data[0].embedding, }); return { data, error }; } // ベクトル類似検索を実行 async function searchDocuments(query: string) { const embeddingRes = await openai.embeddings.create({ model: 'text-embedding-3-small', input: query, }); const { data } = await supabase.rpc('match_documents', { query_embedding: embeddingRes.data[0].embedding, match_threshold: 0.7, match_count: 5, }); return data; }
-- 全文検索用のカラムを追加 ALTER TABLE documents ADD COLUMN fts tsvector GENERATED ALWAYS AS (to_tsvector('japanese', content)) STORED; CREATE INDEX ON documents USING gin(fts); -- ハイブリッド検索関数(キーワード30% + セマンティック70%) CREATE OR REPLACE FUNCTION hybrid_search( search_query text, query_embedding vector(1536), match_count int DEFAULT 5, keyword_weight float DEFAULT 0.3, semantic_weight float DEFAULT 0.7 ) RETURNS TABLE ( id bigint, content text, metadata jsonb, score float ) LANGUAGE plpgsql AS $$ BEGIN RETURN QUERY SELECT d.id, d.content, d.metadata, (keyword_weight * ts_rank(d.fts, websearch_to_tsquery(search_query))) + (semantic_weight * (1 - (d.embedding <=> query_embedding))) AS score FROM documents d ORDER BY score DESC LIMIT match_count; END; $$;
-- RLSを有効化(必須!これなしではデータが全公開になります) ALTER TABLE documents ENABLE ROW LEVEL SECURITY; -- ユーザーは自分のドキュメントのみ閲覧可能 CREATE POLICY "Users can view own documents" ON documents FOR SELECT USING (auth.uid() = user_id); -- ユーザーは自分のドキュメントのみ作成可能 CREATE POLICY "Users can insert own documents" ON documents FOR INSERT WITH CHECK (auth.uid() = user_id); -- ユーザーは自分のドキュメントのみ更新可能 CREATE POLICY "Users can update own documents" ON documents FOR UPDATE USING (auth.uid() = user_id); -- ユーザーは自分のドキュメントのみ削除可能 CREATE POLICY "Users can delete own documents" ON documents FOR DELETE USING (auth.uid() = user_id);
import { serve } from 'https://deno.land/std/http/server.ts'; import { createClient } from 'https://esm.sh/@supabase/supabase-js'; serve(async (req) => { const { query } = await req.json(); // 1. クエリをエンベディングに変換 const embeddingRes = await fetch('https://api.openai.com/v1/embeddings', { method: 'POST', headers: { 'Authorization': `Bearer ${Deno.env.get('OPENAI_API_KEY')}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ model: 'text-embedding-3-small', input: query, }), }); const { data } = await embeddingRes.json(); // 2. ベクトル検索で関連ドキュメントを取得 const supabase = createClient( Deno.env.get('SUPABASE_URL'), Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ); const { data: docs } = await supabase.rpc('match_documents', { query_embedding: data[0].embedding, match_count: 5, }); // 3. Claude APIで回答を生成 const context = docs.map(d => d.content).join('\n---\n'); const claudeRes = await fetch('https://api.anthropic.com/v1/messages', { method: 'POST', headers: { 'x-api-key': Deno.env.get('ANTHROPIC_API_KEY'), 'content-type': 'application/json', 'anthropic-version': '2023-06-01', }, body: JSON.stringify({ model: 'claude-sonnet-4-20250514', max_tokens: 1024, messages: [{ role: 'user', content: `以下のコンテキストを参考に回答してください。\n\nContext:\n${context}\n\nQuestion: ${query}`, }], }), }); const answer = await claudeRes.json(); return new Response(JSON.stringify({ answer: answer.content[0].text, sources: docs, })); });
チャット履歴
conversationsテーブル(id, user_id, title, created_at)とmessagesテーブル(id, conversation_id, role, content, tokens_used, created_at)の1対多リレーション。roleはuser/assistantのenum型。トークン数の記録でコスト管理も可能に。
ドキュメント管理
collections(ナレッジベースの単位)、documents(元ファイル情報)、chunks(分割されたテキスト + embedding)の3層構造。チャンクにはsource_url、page_numberなどのメタデータを付与してソース追跡を実現。
ユーザー設定
profilesテーブルにdisplay_name、avatar_url、preferences(JSONB)を格納。preferencesにはAIモデル選択、温度パラメータ、言語設定などをJSON形式で柔軟に保存。スキーマ変更なしで設定項目を追加可能。
等の元データ
に分割(20%重複)
変換(1536次元)
保存 + HNSW
関連文書を取得
回答を生成
Supabase CLI マイグレーション
supabase migration newでSQLマイグレーションファイルを生成し、Gitで管理。supabase db pushで本番環境に適用。ローカルのSupabase CLI環境でテスト後に本番デプロイするのが安全なワークフローです。
ブランチング戦略
Supabase Branchingを使い、PR ごとにプレビューDBを自動生成。スキーマ変更の影響をプレビュー環境で事前検証できます。マージ時に本番へ自動適用。Neonの場合はブランチ機能で同様のワークフローが可能です。
ゼロダウンタイム移行
カラム追加はALTER TABLE一発ですが、カラム削除やリネームは注意が必要。まず新カラムを追加 → アプリを両方対応に更新 → 旧カラムを削除の3段階で実施。CONCURRENTLY付きのインデックス作成でロックを回避。
自動バックアップ
Supabaseは全プランでデイリーバックアップを提供。Proプランではポイントインタイムリカバリ(PITR)が可能。重要データはpg_dumpで定期エクスポートし、S3やGCSに外部保存することも推奨します。
パフォーマンス監視
Supabaseダッシュボードでクエリ実行時間、接続数、ストレージ使用量を監視。pg_stat_statementsでスロークエリを特定。ベクトル検索のレイテンシーはインデックスパラメータのチューニングで改善できます。
障害対応計画
RPO(目標復旧時点)とRTO(目標復旧時間)を事前に定義。リードレプリカによる読み取りワークロードの分散、フェイルオーバー手順の文書化、定期的な復旧訓練を実施しましょう。