---
title: "Supabase pgvectorの実装手順：RPC検索関数・HNSW・RLS併用のRAG構成"
url: "https://www.issoh.co.jp/tech/details/19496/"
published: 2026-11-08
updated: 2026-11-08
categories: ["データベース"]
publisher: "株式会社一創"
---

# Supabase pgvectorの実装手順：RPC検索関数・HNSW・RLS併用のRAG構成

Supabase で pgvector を使うとき、拡張を有効にするのはボタン1つで終わります。手が止まるのはその先です。`supabase-js` の `.from()` では距離演算子が書けず、rpc の後ろに絞り込みを足すと件数が欠け、Edge Functions の組み込みモデルで日本語を埋め込むと検索精度が出ません。この記事では、vector 拡張の有効化と列の設計、Postgres 関数で包んで rpc から呼ぶ検索、HNSW インデックスと絞り込みの扱い、RLS を効かせたままユーザーごとに検索範囲を絞る RAG の構成までを、2026年11月時点の公式ドキュメントと pgvector 0.8系に沿ってコード付きで並べます。

## まとめ：Supabaseでpgvectorを組む前に決める埋め込みモデルと権限の境界

先に結論です。Supabase 上の pgvector は、検索を必ず Postgres 関数に入れ、rpc から呼ぶ形で組みます。PostgREST が pgvector の類似度演算子に対応していないためで、ここは選択肢ではなく前提です。

着手前に決めるのは2点だけです。1点目は埋め込みモデルで、列の次元数がモデルの出力で固定されます。後から変えると全行の埋め込み直しとインデックスの作り直しになるため、最も巻き戻しの高い判断です。日本語の文書を扱うなら、Edge Functions 組み込みの gte-small ではなく多言語対応の外部モデルを選んでください。2点目は検索関数の権限で、呼び出したユーザーの権限で動く既定の形を保てば、RLS のポリシーがそのまま類似検索にも効きます。

データが数百万件に届かず、認可を Supabase Auth のユーザー単位で表せるなら、専用のベクトル DB を足さずに押し切って構いません。

## Supabaseでvector拡張を有効化しベクトル列のテーブルを作る手順

拡張名は `vector` で、Supabase では `extensions` スキーマに入れるのが標準の置き方です。手順自体は[Supabase公式のpgvector拡張ページ](https://supabase.com/docs/guides/database/extensions/pgvector)に載っています。

### ダッシュボードとSQLの2通りで拡張を有効化し版番号を確かめる手順

ダッシュボードなら Database の Extensions を開き、「vector」を検索して有効にします。マイグレーションで管理するなら SQL で書いてください。環境ごとに手作業がずれる事故を防げます。

```
-- vector拡張をextensionsスキーマへ入れる
create extension if not exists vector
with
  schema extensions;

-- 入っている版を確認する
select extversion
from pg_extension
where extname = 'vector';
```

版の確認は飛ばさないでください。後述の iterative scan は pgvector 0.8.0 以降の機能で、[pgvectorのGitHubリポジトリ](https://github.com/pgvector/pgvector)では2026年11月時点のインストール手順が v0.8.7 を指しています。プロジェクトに入っている版がそれより古ければ、使える設定項目も異なるため確認が必要です。マイグレーションの流し方は[Supabase CLIとローカル開発環境の記事](https://www.issoh.co.jp/tech/details/17063/)で扱っています。

### 埋め込みモデルの出力次元に合わせたvector列の型と次元の上限

列の次元数はモデルの出力と一致させます。一致しないベクトルは挿入時に弾かれ、異なるモデルで作ったベクトル同士の距離には意味がありません。

| モデルの出力           | 列の型          | HNSWの作成      |
| ---------------- | ------------ | ------------ |
| 384次元（gte-small） | vector(384)  | そのまま作成可      |
| 1,536次元          | vector(1536) | そのまま作成可      |
| 3,072次元          | vector(3072) | halfvecへキャスト |

[SupabaseのHNSWインデックスのページ](https://supabase.com/docs/guides/ai/vector-indexes/hnsw-indexes)によると、インデックスを張れるのは vector 型で2,000次元、halfvec 型で4,000次元までです。3,072次元のモデルを選ぶなら、列は vector のまま、インデックスだけ halfvec へのキャスト式で作ります。次元数そのものの考え方は[ベクトルDBの仕組みとインデックス構造の記事](https://www.issoh.co.jp/tech/details/7691/)で計算しています。

```
create table public.documents (
  id bigint generated by default as identity primary key,
  title text not null,
  body text not null,
  embedding extensions.vector(1536)
);
```

## supabase-jsから類似検索するPostgres関数とrpc呼び出しの実装

[Supabase公式のVector columnsガイド](https://supabase.com/docs/guides/ai/vector-columns)は、PostgREST が pgvector の類似度演算子を現時点で扱えないと明記しています。`.from('documents').select()` の延長では検索が書けません。

### PostgRESTが距離演算子を扱えないため検索を関数で包む定義

検索クエリを Postgres 関数に入れると、rpc のエンドポイントとして公開されます。距離にはコサイン距離 `<=>` を使い、類似度は「1 − 距離」で返す形が公式例と同じ考え方です。

```
create or replace function public.match_documents (
  query_embedding extensions.vector(1536),
  match_threshold float,
  match_count int
)
returns table (id bigint, title text, body text, similarity float)
language sql
stable
security invoker
set search_path = ''
as $$
  select
    d.id,
    d.title,
    d.body,
    1 - (d.embedding operator(extensions.<=>) query_embedding) as similarity
  from public.documents as d
  where d.embedding operator(extensions.<=>) query_embedding < 1 - match_threshold
  order by d.embedding operator(extensions.<=>) query_embedding asc
  limit least(match_count, 200);
$$;
```

押さえる点は2つです。並び替えは類似度の列ではなく、距離演算子の結果そのもので昇順に書きます。pgvector の README は、インデックスが使われる条件を「ORDER BY が距離演算子の結果で昇順、かつ LIMIT がある」と定めているためです。もう1点は検索パスを空にしていることです。その代わり演算子をスキーマ付きで書く必要があり、それが `operator(extensions.<=>)` の形になります。関数設計の一般論は[Supabaseの自動生成APIとRPCの使い分けの記事](https://www.issoh.co.jp/tech/details/17070/)にまとめています。

### rpcで検索関数を呼ぶTypeScriptコードと閾値・件数の渡し方

呼び出し側は、関数のパラメータ名をキーにしたオブジェクトを渡すだけです。検索語の埋め込みは、文書側と同じモデルで作ったものを渡します。

```
import { createClient } from '@supabase/supabase-js'

const supabase = createClient(SUPABASE_URL, SUPABASE_PUBLISHABLE_KEY)

const { data, error } = await supabase.rpc('match_documents', {
  query_embedding: embedding, // number[]（1536要素）
  match_threshold: 0.78,
  match_count: 10,
})

if (error) throw error
console.log(data.map((d) => [d.title, d.similarity]))
```

閾値の 0.78 は公式例の値で、モデルによって適正値が大きく変わります。閾値を決め打ちすると、日本語の短い質問で1件も返らない事態が起きます。最初は閾値を 0 にして上位10件の類似度を並べ、正解の文書がどの値に分布しているかを見てから決めてください。

### rpcの後ろに絞り込みを重ねると件数が欠ける理由と関数内フィルタ

カテゴリで絞りたくなったとき、`.rpc(...).eq('category', 'blog')` と書くと動いてしまうので厄介です。[Supabase公式のSemantic searchガイド](https://supabase.com/docs/guides/ai/semantic-search)は、この書き方では類似度順の上位と LIMIT が先に決まり、絞り込みが後から外側で掛かるため、返却件数が `match_count` を下回ると説明しています。10件頼んで2件しか返らない、という症状になります。

絞り込みは引数にして関数の中へ入れます。上の関数に `filter_category text` を足し、`where` 句に `d.category = filter_category` を加える形です。条件の種類が増えるなら、jsonb の `metadata` 列と `@>` による包含条件で1つの引数にまとめると、関数のシグネチャが増え続けずに済みます。

## HNSWインデックスの作成と、絞り込み検索で件数が欠ける問題の対処

件数が数千件のうちはインデックスなしの全件走査でも返ってきます。HNSW は精度をわずかに譲って速度を取る近似検索なので、「遅くなってから足す」のではなく、テーブル作成と同時に張っておくことも可能です。Supabase のドキュメントも HNSW をベクトルインデックスの第一候補としています。

### 演算子クラスを距離関数と揃えるHNSW作成SQLと既定パラメータ

演算子クラスは、検索関数で使う距離演算子と一致させます。コサイン距離で検索するなら `vector_cosine_ops`、内積なら `vector_ip_ops` です。ずれていると、インデックスがあるのに使われません。

```
create index documents_embedding_hnsw
on public.documents
using hnsw (embedding extensions.vector_cosine_ops);

-- 3,072次元の列はhalfvecへのキャスト式で張る
-- create index on public.documents
-- using hnsw ((embedding::extensions.halfvec(3072)) extensions.halfvec_cosine_ops);
```

パラメータの既定値は、pgvector の README で `m` が16、`ef_construction` が64、検索時の `hnsw.ef_search` が40です。再現率が足りないと感じるまでは既定値で構いません。ベクトルを正規化して保存しているなら内積がもっとも速い傾向がある、と Supabase のガイドは述べています。gte-small の公式例は `normalize: true` で出力しているため、この条件に当てはまる設定です。パラメータを変えたときの再現率の動きは[pgvectorのインストール手順と使い方の記事](https://www.issoh.co.jp/tech/details/6231/)で詳しく扱っています。

### 絞り込みで返却件数がLIMITを下回る現象とiterative\_scanの設定

関数内で絞り込んでも、件数欠けは別の理由でもう一度起きることがあるため注意が必要です。HNSW はまず `ef_search` 件の候補を集め、そこへ `where` 句を当てるため、該当する行が少ない条件では候補がほとんど落ちてしまいます。pgvector 0.8.0 以降は、足りない分をインデックスから追加で読む iterative scan で緩和できます。

```
-- 関数に設定を持たせ、呼び出しごとに反復スキャンを有効にする
alter function public.match_documents(extensions.vector, float, int)
  set hnsw.iterative_scan = relaxed_order;

-- 効き方は関数の中身のクエリを直接流して確かめる（category列を足した前提）
begin;
set local hnsw.iterative_scan = relaxed_order;
explain analyze
select d.id
from public.documents as d
where d.category = 'blog'
order by d.embedding operator(extensions.<=>)
  (select embedding from public.documents limit 1)
limit 10;
rollback;
```

`relaxed_order` はわずかな順序の入れ替わりを許す代わりに件数を確保する設定で、`strict_order` は距離順を厳密に保ちます。走査する行数の上限は `hnsw.max_scan_tuples`（既定 20,000）で決まり、Supabase のページに既定値の記載があります。テナントごとに文書数が極端に偏るなら、iterative scan で粘るより、テナント列で部分インデックスを分けるほうが素直です。

## RLSを効かせたままRAGの検索範囲をユーザーごとに絞る設計

社内文書の RAG で最初に問われるのは精度より、「他部署や他社の文書が回答に混ざらないか」です。Supabase では、この判定をアプリのコードではなくデータベースのポリシーに置けます。

### 文書とチャンクを分けたテーブルとauth.uid()で絞るポリシーのSQL

[Supabase公式のRAG with Permissionsガイド](https://supabase.com/docs/guides/ai/rag-with-permissions)は、所有者を持つ文書テーブルと、分割した本文と埋め込みを持つチャンクテーブルに分ける構成を示しています。権限は親の文書で持ち、チャンクは親をたどって判定します。下の例の `public.docs` は、`owner_id` 列に `auth.users` の ID を持つ文書テーブルです。検索関数は前章の `match_documents` と同じ形で、参照先をこのチャンクテーブルに替えます。

```
create table public.document_sections (
  id bigint generated by default as identity primary key,
  document_id bigint not null references public.docs (id),
  content text not null,
  embedding extensions.vector(1536)
);

alter table public.document_sections enable row level security;

create policy "own document sections are readable"
on public.document_sections
for select
to authenticated
using (
  document_id in (
    select id from public.docs
    where owner_id = (select auth.uid())
  )
);
```

この状態でチャンクを検索すると、類似度の計算より前に、読めない行がポリシーで落ちます。ガイドも、類似検索や追加の `where` 条件が RLS のポリシーを尊重すると説明しています。ポリシーの例示は SELECT だけなので、文書の登録や削除をクライアントから許すなら、INSERT と DELETE のポリシーを別に書いてください。部署単位や共有設定を含むポリシーの書き方は[Supabase RLSの設定手順とAuth連携ポリシー設計の記事](https://www.issoh.co.jp/tech/details/17056/)が詳しく扱っています。

### security invokerのRLS適用とservice\_roleの落とし穴

[Supabase公式のDatabase functionsページ](https://supabase.com/docs/guides/database/functions)は、関数を呼び出し元の権限で動かす `security invoker` を既定かつ推奨としています。先の検索関数を invoker のままにしておけば、関数の中の `select` もポリシーの適用対象です。逆に `security definer` にすると作成者の権限で動き、呼び出し元が読めない行まで返し得る、と同じページが警告しています。

見落としやすいのは関数の外側です。Edge Functions から rpc を呼ぶとき、サーバー用の secret キー（旧 service\_role）でクライアントを作ると、RLS を素通りして全ユーザーの文書が検索対象になります。ユーザーのリクエストに付いてきたアクセストークンを `Authorization` ヘッダに載せてクライアントを作り、そのユーザーとして呼んでください。鍵の使い分けは[SupabaseのAPIキー管理の記事](https://www.issoh.co.jp/tech/details/17068/)にまとめています。

ガイドは、RLS がレイテンシに効きやすいため、導入前に実行計画で所要時間を測るよう勧めています。サブクエリが毎行評価されていないか、`explain analyze` を通常ロールで流して確かめてください。

## 埋め込みの生成場所をEdge Functionsと外部APIで分ける判断と自動化

検索の仕組みが組めても、埋め込みを「いつ・どこで作るか」が決まっていないと運用で崩れます。文書が更新されたのに古い埋め込みが残る状態が、RAG の回答がずれる典型的な原因です。

### 組み込みのgte-smallは英語専用で日本語RAGに外部モデルが要る理由

Edge Functions には `Supabase.ai.Session('gte-small')` という組み込みモデルがあり、外部 API なしで384次元の埋め込みが作れます。ただし[Supabase公式のRunning AI Modelsページ](https://supabase.com/docs/guides/functions/ai-models)は、このモデルの対象を英語のテキストに限り、512トークンを超える入力は切り詰めると明記しています。日本語の社内文書には向きません。

日本語を扱うなら、Edge Functions から外部の多言語埋め込み API を呼びます。検索語の埋め込みと rpc をまとめた関数の例です。API キーはシークレットに置きます（[Supabase Edge Functionsの記事](https://www.issoh.co.jp/tech/details/17061/)参照）。

```
import { createClient } from 'jsr:@supabase/supabase-js@2'

Deno.serve(async (req) => {
  const { query } = await req.json()

  // 文書側と同じモデルで検索語を埋め込む
  const res = 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 embedding = (await res.json()).data[0].embedding

  // 利用者のトークンで呼び、RLSを効かせる
  const supabase = createClient(
    Deno.env.get('SUPABASE_URL')!,
    Deno.env.get('SUPABASE_ANON_KEY')!,
    { global: { headers: { Authorization: req.headers.get('Authorization')! } } },
  )
  const { data, error } = await supabase.rpc('match_documents', {
    query_embedding: embedding,
    match_threshold: 0.3,
    match_count: 8,
  })
  if (error) return new Response(error.message, { status: 500 })
  return Response.json(data)
})
```

モデル名は差し替え可能な設定にしておきます。ただしモデルを変えた時点で既存の埋め込みは全件作り直しになり、混在させると検索結果が崩れます。

### pgmqとpg\_cronで挿入・更新時に埋め込みを非同期生成する公式構成

文書の登録時に同期で埋め込みを作ると、外部 API の遅延や失敗が登録処理そのものを止めます。[Supabase公式のAutomatic embeddingsガイド](https://supabase.com/docs/guides/ai/automatic-embeddings)は、トリガーでジョブを pgmq のキューに積み、pg\_cron が10秒ごとに処理関数を起動して、pg\_net から Edge Functions の `embed` を呼ぶ構成を示しています。1回の起動でまとめて処理する件数は10件ずつで、完了しなかったジョブは可視性タイムアウト（既定5分）の後に再試行される仕組みです。

この構成の利点は、`title` や `content` が更新されたときにもトリガーで再生成が走ることです。ガイドは text-embedding-3-small の1,536次元を halfvec 列に入れる例で組んでいます。キューの仕組み自体は[pgmqとSupabase Queues連携の記事](https://www.issoh.co.jp/tech/details/4240/)で解説しています。

## Supabaseのpgvectorで押し切る条件と専用ベクトルDBへ移す判断基準

どこまで Supabase の中で済ませるかを、条件で言い切ります。

### Supabase内で完結させてよい3条件と、移行を検討し始める目安

次の3つが揃うなら、専用のベクトル DB を足さずに Supabase の pgvector で押し切ってください。第一に、検索対象のチャンクが数百万件に届かないこと。第二に、検索範囲の認可が Supabase Auth のユーザーやテナントで表せること。第三に、検索結果を業務データと結合して返す必要があることです。三つ目が当てはまるなら、別 DB に分けた時点で結合とデータの同期を自前で書くことになり、運用の手数が増えます。

移行を考え始める合図は、iterative scan を入れても絞り込み検索の件数や応答時間が要件に届かないときと、インデックスの構築時間がデータ更新の頻度に追いつかなくなったときです。キーワード一致も混ぜたいだけなら、移行より先に[ハイブリッド検索とRRFの記事](https://www.issoh.co.jp/tech/details/8014/)の構成を同じ Postgres の中で試すほうが早く済みます。AWS上で同じ構成を組む場合は[Aurora pgvectorの実装手順：対応版の確認・HNSW索引・Bedrockナレッジベース連携](https://www.issoh.co.jp/tech/details/19498/)を参照してください。

### この構成を見送るべき場面と、RAG全体の設計で先に決めること

この構成を見送るべき場面も、次の条件で判断できます。検索対象が数千万件規模でチャンクが日々大量に入れ替わる場合、ベクトル検索の負荷が業務トランザクションと同じデータベースに乗り、片方の負荷がもう片方の応答を巻き込みます。権限の正本が社外の ID 基盤や既存の文書管理システムにあり、Supabase Auth へ写せない場合も、ポリシーで判定する利点がなくなるため、採用を見送る条件です。この2つのどちらかに当たるなら、Supabase は業務データ側に留め、検索は専用基盤へ分けてください。

どちらに転んでも、チャンクの切り方、埋め込みモデル、権限の持ち方の3点は検索基盤を選ぶより先に決めておく必要がある事項です。社内文書の RAG をこれから組む段階なら、[RAG構築支援](https://www.issoh.co.jp/service/ai/rag/)として要件整理から相談を受けています。製品全体の機能範囲と料金は[SupabaseとFirebaseの比較記事](https://www.issoh.co.jp/column/details/3002/)で整理しています。

## よくある質問

Supabase で pgvector を使い始めるときに出やすい疑問をまとめます。

### pgvectorの埋め込み列はどれくらいの容量を使いますか？

pgvector の README によると、vector 型は1要素4バイトに8バイトを足した容量を使います。1,536次元なら1行あたり約6KBで、10万チャンクで約600MBが埋め込みだけで埋まる計算です。インデックスはこれとは別に領域を取ります。容量が気になる場合は、要素を2バイトで持つ halfvec 型か、次元数の小さいモデルを選ぶと抑えられます。

### supabase-jsの.from()で類似検索を書けないのはなぜですか？

supabase-js は PostgREST を経由してデータベースに接続しますが、PostgREST が pgvector の類似度演算子に対応していないためです。検索クエリを Postgres 関数に入れ、`rpc()` から呼ぶのが公式の方法です。関数にしておけば、権限の制御や絞り込みの条件も1か所にまとめられます。

### match\_documentsで結果が0件になるのはなぜですか？

まず閾値を疑ってください。`match_threshold` の適正値はモデルごとに違い、公式例の 0.78 をそのまま日本語の検索に使うと全件が落ちることがあります。次に、文書と検索語を同じモデルで埋め込んでいるか、RLS で行が見えなくなっていないかも確認の対象です。閾値を 0 にして上位の類似度を並べると切り分けが早く進みます。

### HNSWとIVFFlatはどちらを選べばよいですか？

Supabase のドキュメントは HNSW を第一候補にしています。IVFFlat はデータを入れてから作る必要があり、データが増えるとリストの作り直しが必要です。HNSW はテーブル作成直後に作れて、追加分も自動で反映されます。構築時間とメモリが厳しい大規模データでなければ HNSW で始めて構いません。

### OpenAI以外の埋め込みモデルも使えますか？

使えます。pgvector はベクトルの出どころを問わないため、列の次元数をモデルの出力に合わせれば、どの埋め込み API でも保存と検索ができます。注意点は、文書側と検索語側を必ず同じモデルにすることと、2,000次元を超えるモデルでは halfvec のキャストでインデックスを作ることです。

## 関連記事

- [pgvectorとは？インストール手順（Windows・Ubuntu・Docker Compose）と使い方【0.8.7対応】](https://www.issoh.co.jp/tech/details/6231/)：拡張そのものの仕組みと演算子です。
- [ベクトルDBとは？仕組み・インデックス構造・製品比較とpgvectorでの作り方【2026年版】](https://www.issoh.co.jp/tech/details/7691/)：専用DBとの比較材料です。
- [Supabase 自動生成API（PostgREST）とRPCの使い分け：select・embed・関数設計](https://www.issoh.co.jp/tech/details/17070/)：検索関数の設計の土台です。
- [Supabase RLSの設定手順とAuth連携ポリシー設計｜有効化漏れの検知まで](https://www.issoh.co.jp/tech/details/17056/)：検索範囲を絞るポリシーです。
- [ハイブリッド検索とは？RAGでBM25とベクトル検索を統合する仕組み・RRF・製品別の実装【2026年版】](https://www.issoh.co.jp/tech/details/8014/)：精度を上げる次の一手です。

---

出典: [Supabase pgvectorの実装手順：RPC検索関数・HNSW・RLS併用のRAG構成](<https://www.issoh.co.jp/tech/details/19496/>)（株式会社一創）
