「社内マニュアルをチャットで検索できるようにしたい」「自社ドキュメントに基づいて正確に回答するボットを作りたい」という相談を受けたとき、私が最初に手を伸ばすのが Supabase Vector + Gemini API の組み合わせです。
PostgreSQL の拡張として pgvector が使えるため、既存の Supabase プロジェクトにベクトルストアを自然に組み込めます。Antigravity と組み合わせると、スキーマ設計・SQL 生成・埋め込み処理のコードを一気に書き上げられるので、個人開発でも週末に動くものが作れます。
ここではAntigravity を使いながら Supabase pgvector ベースの RAG アプリを実装する手順を、具体的なコードと一緒にまとめます。
なぜ Supabase Vector(pgvector)を選ぶのか
RAG のベクトルストアには Pinecone、Weaviate、Qdrant など多くの選択肢があります。それでも Supabase を選ぶ理由は、既存の Supabase プロジェクトにゼロ追加コストで統合できるからです。
pgvector は PostgreSQL の拡張です。Supabase ダッシュボードから1行の SQL で有効化でき、通常のテーブルにベクトルカラムを追加するだけでベクトルストアになります。別サービスとの連携コスト・ネットワーク遅延・追加の API キー管理が不要なのは、個人開発にとって大きな利点です。
また Supabase の Row Level Security(RLS)がそのまま使えるため、「ユーザーごとにアクセスできるドキュメントを制限する」マルチテナント RAG もシンプルに実装できます。
Antigravity で環境をセットアップする
まず Antigravity のチャットパネルで「Supabase pgvector を使って RAG アプリを作りたい。TypeScript、Next.js 15 App Router、Supabase JS v2 を使う前提でセットアップ手順と必要なパッケージをリストアップして」と伝えてください。
Antigravity が生成する初期コマンドはおおむね以下のようになります:
# Next.js プロジェクトへの依存追加
npm install @supabase/supabase-js @ai-sdk/google ai
# 環境変数(.env.local)
# NEXT_PUBLIC_SUPABASE_URL=https://xxxx.supabase.co
# NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
# GOOGLE_GENERATIVE_AI_API_KEY=YOUR_GEMINI_API_KEY次に Supabase ダッシュボードの SQL Editor で pgvector を有効化し、ドキュメントテーブルを作ります:
-- pgvector 拡張を有効化
create extension if not exists vector;
-- ドキュメントテーブル(embedding は 768 次元 = text-embedding-004 の出力)
create table documents (
id uuid default gen_random_uuid() primary key,
content text not null,
metadata jsonb default '{}',
embedding vector(768),
created_at timestamptz default now()
);
-- コサイン類似度での近傍探索インデックス
create index on documents
using ivfflat (embedding vector_cosine_ops)
with (lists = 100);このスキーマを Antigravity に渡すと、以降の型定義やクエリ生成がより正確になります。「このスキーマを前提に TypeScript の型と Supabase クライアントを書いて」と一言添えるだけで、型安全なコードが出てきます。
ドキュメントのベクトル化と保存
RAG の核心は「ドキュメントを埋め込みベクトルに変換して保存する」処理です。Antigravity に「Gemini の text-embedding-004 モデルでテキストを埋め込み、Supabase に保存する関数を書いて」と依頼すると、次のようなコードが生成されます:
// lib/embeddings.ts
import { createClient } from "@supabase/supabase-js";
import { GoogleGenerativeAI } from "@google/generative-ai";
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
const genai = new GoogleGenerativeAI(process.env.GOOGLE_GENERATIVE_AI_API_KEY!);
const embeddingModel = genai.getGenerativeModel({ model: "text-embedding-004" });
// ドキュメントを埋め込みベクトルに変換して Supabase に保存
export async function upsertDocument(
content: string,
metadata: Record<string, unknown> = {}
): Promise<{ id: string }> {
// テキストを 768 次元のベクトルに変換
const result = await embeddingModel.embedContent(content);
const embedding = result.embedding.values;
const { data, error } = await supabase
.from("documents")
.insert({ content, metadata, embedding })
.select("id")
.single();
if (error) throw new Error(`Supabase insert error: ${error.message}`);
return { id: data.id };
}ポイントは text-embedding-004 の出力が 768 次元であることです。テーブル作成時の vector(768) と次元数を揃えないと挿入時にエラーになります。Antigravity は指示された次元数に合わせてコードを調整してくれますが、スキーマとモデルの次元数の一致は自分で確認する習慣をつけておくとよいです。
セマンティック検索と Gemini API 連携
保存したドキュメントに対してセマンティック検索を実行し、関連チャンクを取り出して Gemini に渡す流れが RAG の本体です。
// lib/rag.ts
import { generateText } from "ai";
import { google } from "@ai-sdk/google";
// クエリに類似するドキュメントを取得
async function searchDocuments(query: string, topK = 5) {
// クエリ文もベクトル化
const result = await embeddingModel.embedContent(query);
const queryEmbedding = result.embedding.values;
// pgvector のコサイン類似度でトップ K 件を取得
const { data, error } = await supabase.rpc("match_documents", {
query_embedding: queryEmbedding,
match_threshold: 0.7, // 類似度の閾値
match_count: topK,
});
if (error) throw new Error(`Search error: ${error.message}`);
return data as Array<{ content: string; metadata: unknown; similarity: number }>;
}
// RAG: 検索 → コンテキスト構築 → Gemini で回答生成
export async function ragQuery(userQuestion: string): Promise<string> {
const relevantDocs = await searchDocuments(userQuestion);
if (relevantDocs.length === 0) {
return "関連するドキュメントが見つかりませんでした。";
}
// コンテキストを結合
const context = relevantDocs
.map((doc, i) => `[ドキュメント ${i + 1}]\n${doc.content}`)
.join("\n\n");
const { text } = await generateText({
model: google("gemini-2.0-flash"),
prompt: `以下のドキュメントを参照して、質問に回答してください。\n\n${context}\n\n質問: ${userQuestion}`,
});
return text;
}match_documents は Supabase の RPC 関数として SQL Editor に登録します。Antigravity に「上記の search 関数で使う match_documents PostgreSQL 関数を書いて」と伝えると、次のような SQL が生成されます:
-- ベクトル類似度検索のストアドファンクション
create or replace function match_documents (
query_embedding vector(768),
match_threshold float,
match_count int
)
returns table (
id uuid,
content text,
metadata jsonb,
similarity float
)
language sql stable
as $$
select
id,
content,
metadata,
1 - (embedding <=> query_embedding) as similarity
from documents
where 1 - (embedding <=> query_embedding) > match_threshold
order by similarity desc
limit match_count;
$$;<=> 演算子は pgvector のコサイン距離です。1 - 距離 で類似度(0〜1)に変換しています。この SQL も Antigravity が一発で出してくれるので、ベクトル検索に詳しくない方でもすぐ使えます。
実装でよくつまずくポイント
Supabase + pgvector の RAG を実装していて実際に詰まった箇所を共有します。
次元数ミスマッチ: vector(768) のテーブルに 1,536 次元(OpenAI text-embedding-3-small)を入れようとするとエラーになります。モデルを変えるときはマイグレーションで次元数も変更が必要です。
IVFFlat インデックスのタイミング: lists = 100 は挿入件数の目安に依存します。Supabase の公式ガイドでは「行数の sqrt 程度」が推奨です。少量データの段階でインデックスを張っても精度向上の効果は薄く、挿入速度が落ちるだけです。1,000 件以上たまってから張るのが現実的です。
RLS との干渉: Row Level Security を有効にしたまま service_role キーで RPC を呼ぶと意図せずバイパスされることがあります。本番では API Route から anon キーを使い、ユーザーセッションと RLS ポリシーを組み合わせた設計をおすすめします。
Antigravity はこれらのトラブルを「こういうエラーが出た」と貼り付けるだけで原因と修正コードを出してくれます。Supabase と Antigravity の組み合わせで使える機能全般については別の記事でまとめていますので、あわせて参照してみてください。
全体を振り返って
Supabase pgvector + Antigravity の RAG 実装は、インフラを追加せず既存の Supabase プロジェクトに乗せられるのが最大の強みです。まずは10〜20件のドキュメントを登録して検索精度を確認することから始めてみてください。
セマンティック検索の精度に満足できたら、RAG パイプラインのベクトル検索設計で chunk サイズや overlap の最適化について深掘りするとよいでしょう。
Vercel AI SDK の streamText に切り替えると回答をストリーミングで返せるので、チャット UI の体験がかなり向上します。次のステップとしてぜひ試してみてください。