PostgreSQL拡張「pgvector」で、RAG用ベクトルデータベースを構築する

[更新: 2026年08月03日]

この記事では、PostgreSQL拡張「pgvector」で、RAG用ベクトルデータベースを構築する方法を解説します。シンプルAIを対象にしています。

RAG専用ベクトルデータベースとは

RAG(検索拡張生成:Retrieval-Augmented Generation)とは、生成AIに検索機能を組み合わせて最新文書や社内情報を根拠に回答させる仕組みです。

生成AIやRAGでは、文章を「埋め込みベクトル」という数百〜数千次元の数値配列に変換して、「そのベクトル同士の近さ=意味の近さ」として扱います。「似た意味の文章を探す」処理を、このベクトル間の距離計算に置き換えるわけです。

問題は、通常のRDBMSでは「値が一致するか」「大小比較」は得意でも、「ベクトルが近いか」を高速に検索する仕組みを持っていないことです。そこで従来は、専用のベクトルデータベースを別サービスとして構築する必要がありました。

そこで役立つのが、PostgreSQLにベクトル検索の機能を追加する「pgvector」です。

pgvectorとは

pgvectorは、PostgreSQLにベクトル検索の機能を追加するオープンソースの拡張機能です。「ベクトル(数値の配列)」を格納する専用のデータ型と、「似ているベクトルを高速に探す」ための検索機能をPostgreSQLに追加します。

pgvectorを使うと、AIが生成する埋め込み(embedding)ベクトルを、既存のPostgreSQLのテーブルにそのまま保存し、SQLで類似検索できるようになります。わざわざ別のベクトルデータベースを用意しなくても、PostgreSQL単体でベクトル検索ができます。元データ・メタデータ・埋め込みベクトルを1つのデータベースにまとめて置けるので、システム構成がシンプルになります。

複数の距離関数に対応ベクトルの「近さ」の測り方は用途によって変わります。pgvectorは、L2距離(ユークリッド距離)<->、コサイン距離 <=>、内積 <#>、L1距離(マンハッタン距離)<+> などをサポートしています。埋め込みモデルの多くはコサイン類似度を前提としているため、<=>を使う場面が多くなります。

デフォルトでは「完全一致検索」で全件を厳密に比較しますが、データが増えると遅くなります。そこで近似最近傍探索(ANN)用のインデックスを張ることで、多少の精度と引き換えに大幅に高速化できます。用意されているのはHNSWとIVFFlatの2種類で、一般にHNSWのほうが検索速度と精度のバランスに優れています(そのぶん構築は遅く、メモリを多く使います)。

さくらのクラウドでは、2026年7月にデータベースアプライアンスPostgreSQLをアップデートして、pgvectorに対応しました。pgvectorを使うことで、PostgreSQLのRDB機能とベクトルDB機能を利用可能になります。

RAGプログラムの実装

シンプルAIとPostgreSQL拡張「pgvector」を組み合わせて、RAGを構築する手順を解説します。

さくらのクラウドに、DMZのような領域を作成して本手順の実行用サーバ(踏み台)を構築する方法は、次の記事を参考してください。

1. 仮想サーバの起動

まず、データベースアプライアンス(pgvector)とシンプルAIに接続し、プログラムを実行する仮想サーバを起動します。

eth0を共有セグメント、eth1をスイッチに接続し、eth1に`IPアドレス 192.168.0.1/24`を設定します。

sudo ip addr add 192.168.0.1/24 dev eth1
ip addr show eth1

2. PostgreSQLクライアントのインストール

シェルから、PostgreSQLクライアントのインストールします。

sudo apt update
sudo apt install -y postgresql-client

3. データベースアプライアンスの起動

データベースアプライアンスの起動手順は、次のとおりです。

  1. コントロールパネルで、対象のデータベースアプライアンスを選択する

  2. 画面右上の「電源操作」メニューから「起動」を選ぶ

  3. メニュー表示や情報タブの「ステータス」で、電源ONを確認する

データベースアプライアンスの起動

起動できたら接続テストします。

psql -h 192.168.0.3 -p 5432 -U testcloud -d testcloud -c "SHOW server_version;"

testcloud は、自分の環境に合わせて書き換えてください。

4. pgvectorの有効化

psql -h 192.168.0.3 -p 5432 -U testcloud -d testcloud \
-c "CREATE EXTENSION IF NOT EXISTS vector;"
psql -h 192.168.0.3 -p 5432 -U testcloud -d testcloud -c "\dx"

次のように vector というスキーマが出来ていれば成功です。

Name| Version | Default version | Schema |Description
------------------------+---------+-----------------+------------+------------------------------------------------------------
 fuzzystrmatch| 1.1 | 1.1 | public | determine similarities and distance between strings
 plpgsql| 1.0 | 1.0 | pg_catalog | PL/pgSQL procedural language
 postgis| 3.5.6 | 3.5.6 | public | PostGIS geometry and geography spatial types and functions
 postgis_sfcgal | 3.5.6 | 3.5.6 | public | PostGIS SFCGAL functions
 postgis_tiger_geocoder | 3.5.6 | 3.5.6 | tiger| PostGIS tiger geocoder and reverse geocoder
 postgis_topology | 3.5.6 | 3.5.6 | topology | PostGIS topology spatial types and functions
 vector | 0.8.2 | 0.8.2 | public | vector data type and ivfflat and hnsw access methods

5. RAG clientの設定

シンプルAIのAPIキーを発行します。

操作は、 シンプルAIの利用手順 を参照してください。

シンプルAIのAPIキーを発行

発行されたら環境変数にセットします。

export SAKURA_AI_TOKEN='sakura_simpleai_xxxxx'

では、推論APIと埋め込みAPIを試します。

まず、推論APIを呼び出します。

curl https://simpleai.is1.api.sacloud.jp/v1/chat/completions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $SAKURA_AI_TOKEN" \
  -d '{
    "model": "gpt-oss-120b",
    "messages": [
      { "role": "user", "content": "こんにちは、自己紹介してください。" }
    ]
  }'

次のように返ってきます。

{
  "id": "chatcmpl-c2482e88-164e-419a-8c69-1ed72789bd75",
  "object": "chat.completion",
  "created": 1785109687,
  "model": "gpt-oss-120b",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "こんにちは!はじめまして。私はXXXXXXと申します。XXXXXXXXが提供する大規模言語モデルで、様々なトピックについて会話したり、質問に答えたり、文章を作成したりすることが得意です。..."
      },
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 71,
    "completion_tokens": 359,
    "total_tokens": 430
  }
}

続いて、埋め込みAPIを呼び出します。

curl https://simpleai.is1.api.sacloud.jp/v1/embeddings \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $SAKURA_AI_TOKEN" \
  -d '{
    "model": "multilingual-e5-large",
    "input": "passage: これは埋め込み対象の文書です。",
    "encoding_format": "float"
  }'

埋め込みモデルmultilingual-e5-largeは、次のように1024次元のベクトルを返します。

{
  "id": "embd-eee9d553-94d1-4268-b508-90f96dad0850",
  "object": "list",
  "created": 1785109734,
  "model": "multilingual-e5-large",
  "data": [
    {
      "index": 0,
      "object": "embedding",
      "embedding": [0.029348229989409447, 0.005340240430086851, ...]
    }
  ]
}

6. Node.jsで RAGを実装する

いよいよ本体です。RAGの利用は、大きく2つのフェーズに分かれています。

  • 登録(ingest):文書を埋め込みベクトルに変換し、DBに保存する

  • 質問(ask):質問を埋め込み、近い文書を検索し、それを文脈としてLLMに渡して回答させる

今回は、PDFファイルの内容をベクトルDBに登録して、シンプルAIの生成と組み合わせて検索できるようにします。

PostgreSQL接続とPDFテキスト抽出のため、次のパッケージをインストールします。

npm init
npm install pg pdf-parse

次に PostgreSQLへの接続パスワードを環境変数に入れておきます。

export PGPASSWORD='xxxxx'

以下のコードを rag.js として保存します。

rag.js

// さくらのシンプルAI + PostgreSQL(pgvector) で作る最小RAG(1ファイル・PDF対応版)
// ※ pdf-parse 2.x 系に対応
//
// 事前準備:
//   npm install pg pdf-parse
//   export SAKURA_AI_TOKEN='sakura_simpleai_xxxxx'
//   export PGPASSWORD='xxxxx'
//
// 使い方:
//   node rag.js init                 # テーブル作成(無ければ作る)
//   node rag.js reset                # テーブルを削除して作り直す
//   node rag.js add "文書本文"        # テキストを直接登録
//   node rag.js addpdf ./file.pdf    # PDFから抽出して登録
//   node rag.js ask "質問文"          # 質問して回答を得る

import fs from "node:fs";
import pg from "pg";
import { PDFParse } from "pdf-parse";

const TOKEN = process.env.SAKURA_AI_TOKEN;
const BASE = "https://simpleai.is1.api.sacloud.jp/v1";

const db = new pg.Pool({
  host: process.env.PGHOST || "192.168.0.3",
  port: Number(process.env.PGPORT || 5432),
  user: process.env.PGUSER || "kameoncloud",
  password: process.env.PGPASSWORD,
  database: process.env.PGDATABASE || "kameoncloud",
});

// --- さくらのシンプルAI 呼び出し ---

// テキスト → 1024次元ベクトル。文書は "passage:"、質問は "query:" を付ける。
async function embed(text, kind) {
  const res = await fetch(`${BASE}/embeddings`, {
    method: "POST",
    headers: { "Content-Type": "application/json", Authorization: `Bearer ${TOKEN}` },
    body: JSON.stringify({
      model: "multilingual-e5-large",
      input: `${kind}: ${text}`,
      encoding_format: "float",
    }),
  });
  if (!res.ok) throw new Error(`embed ${res.status}: ${await res.text()}`);
  const json = await res.json();
  return json.data[0].embedding;
}

async function chat(messages) {
  const res = await fetch(`${BASE}/chat/completions`, {
    method: "POST",
    headers: { "Content-Type": "application/json", Authorization: `Bearer ${TOKEN}` },
    body: JSON.stringify({ model: "gpt-oss-120b", messages, temperature: 0.3 }),
  });
  if (!res.ok) throw new Error(`chat ${res.status}: ${await res.text()}`);
  const json = await res.json();
  return json.choices[0].message.content;
}

// 数値配列 → pgvector リテラル文字列 "[0.1,0.2,...]"
const toVector = (arr) => `[${arr.join(",")}]`;

// --- テキスト処理 ---

// 文字数ベースの素朴なチャンク分割(重複あり)
function splitIntoChunks(text, size = 800, overlap = 100) {
  const clean = text
    .replace(/\r\n/g, "\n")
    .replace(/\n{3,}/g, "\n\n")
    .trim();
  if (clean.length <= size) return clean ? [clean] : [];
  const chunks = [];
  for (let s = 0; s < clean.length; s += size - overlap) {
    chunks.push(clean.slice(s, s + size));
  }
  return chunks;
}

// チャンク配列をまとめて登録
async function ingestChunks(chunks, source) {
  if (chunks.length === 0) {
    console.log("登録するテキストがありません。");
    return;
  }
  console.log(`${chunks.length} 個のチャンクを登録します...`);
  for (const [i, c] of chunks.entries()) {
    const vec = await embed(c, "passage");
    await db.query(
      `INSERT INTO documents (content, source, embedding)
       VALUES ($1, $2, $3::vector)`,
      [c, source, toVector(vec)]
    );
    console.log(`  ✓ ${i + 1}/${chunks.length}`);
  }
  console.log("✓ 登録が完了しました。");
}

// --- コマンド ---

async function createTable() {
  await db.query(`CREATE EXTENSION IF NOT EXISTS vector;`);
  await db.query(`
    CREATE TABLE IF NOT EXISTS documents (
      id        bigserial PRIMARY KEY,
      content   text NOT NULL,
      source    text,
      embedding vector(1024)
    );`);
  await db.query(`
    CREATE INDEX IF NOT EXISTS documents_embedding_hnsw
    ON documents USING hnsw (embedding vector_cosine_ops);`);
}

async function init() {
  await createTable();
  console.log("✓ テーブルを作成しました。");
}

async function reset() {
  await db.query(`DROP TABLE IF EXISTS documents;`);
  console.log("✓ 既存テーブルを削除しました。");
  await createTable();
  console.log("✓ テーブルを作り直しました。");
}

async function add(text) {
  await ingestChunks(splitIntoChunks(text), "inline");
}

async function addPdf(path) {
  if (!fs.existsSync(path)) throw new Error(`ファイルが見つかりません: ${path}`);
  const buf = fs.readFileSync(path);
  const parser = new PDFParse({ data: buf });
  const result = await parser.getText();
  const text = result.text;
  if (!text || !text.trim()) {
    throw new Error(
      "PDFからテキストを抽出できませんでした。スキャン画像PDFの可能性があります(OCRが必要)。"
    );
  }
  await ingestChunks(splitIntoChunks(text), path);
}

async function ask(question) {
  const qvec = await embed(question, "query");
  const { rows } = await db.query(
    `SELECT content, source, embedding <=> $1::vector AS distance
     FROM documents ORDER BY distance LIMIT 4`,
    [toVector(qvec)]
  );

  if (rows.length === 0) {
    console.log("文書がありません。先に add / addpdf してください。");
    return;
  }

  console.log(`\n--- 参照した文書 (${rows.length}件) ---`);
  rows.forEach((r, i) => {
    const preview = r.content.slice(0, 50).replace(/\n/g, " ");
    console.log(
      `[${i + 1}] dist=${Number(r.distance).toFixed(4)} (${r.source}) ${preview}...`
    );
  });

  const context = rows.map((r, i) => `[${i + 1}] ${r.content}`).join("\n\n");
  const answer = await chat([
    {
      role: "developer",
      content:
        "参考情報だけを根拠に日本語で答えてください。無い場合は「分かりません」と答え、根拠の番号[1]等を示してください。",
    },
    { role: "user", content: `# 参考情報\n${context}\n\n# 質問\n${question}` },
  ]);

  console.log("\n--- 回答 ---");
  console.log(answer);
}

// --- エントリポイント ---

async function main() {
  if (!TOKEN) throw new Error("SAKURA_AI_TOKEN が未設定です。");
  const [cmd, ...rest] = process.argv.slice(2);
  const arg = rest.join(" ").trim();

  if (cmd === "init") await init();
  else if (cmd === "reset") await reset();
  else if (cmd === "add" && arg) await add(arg);
  else if (cmd === "addpdf" && arg) await addPdf(arg);
  else if (cmd === "ask" && arg) await ask(arg);
  else {
    console.log(
      '使い方:\n' +
        "  node rag.js init\n" +
        "  node rag.js reset\n" +
        '  node rag.js add "文書本文"\n' +
        "  node rag.js addpdf ./file.pdf\n" +
        '  node rag.js ask "質問文"'
    );
  }
  await db.end();
}

main().catch((e) => {
  console.error(e.message);
  process.exit(1);
});

7. RAGを操作する

では、実際にRAGを使ってみましょう。

まずは、ベクトル格納用テーブルを作成します。

 node rag.js init
✓ テーブルを作成しました。

rag.jsの以下のコードで、テーブル作成と同時にインデックスを作成しています。

await db.query(`
  CREATE INDEX IF NOT EXISTS documents_embedding_hnsw
  ON documents USING hnsw (embedding vector_cosine_ops);`);

では、実際の操作です。

任意のPDFファイル(file.pdf)を取り込んでみます。

node rag.js addpdf ./file.pdf
30個のチャンクを登録します...
✓ 1/30
✓ 2/30
✓ 3/30
✓ 4/30
✓ 5/30
✓ 6/30
✓ 7/30
✓ 8/30
✓ 9/30
✓ 10/30
✓ 11/30
✓ 12/30
✓ 13/30
✓ 14/30
✓ 15/30
✓ 16/30
✓ 17/30
✓ 18/30
✓ 19/30
✓ 20/30
✓ 21/30
✓ 22/30
✓ 23/30
✓ 24/30
✓ 25/30
✓ 26/30
✓ 27/30
✓ 28/30
✓ 29/30
✓ 30/30
✓ 登録が完了しました。

最後に質問(ask)を実行します。

node rag.js ask "質問文"

次のように、「個人情報とは?」という質問内容に対して、回答が返ってきます。

node rag.js ask "個人情報とは?"
--- 参照した文書 (4件) ---
[1] dist=0.1644 (pii.pdf) 趣味・嗜好等に関する高精度な推定(いわゆるプロファイリング)、さらには、大量の個人情報等を取り扱う...
[2] dist=0.1644 (pii.pdf) 趣味・嗜好等に関する高精度な推定(いわゆるプロファイリング)、さらには、大量の個人情報等を取り扱う...
[3] dist=0.1702 (pii.pdf) る国の政策の企画立案を担うとともに、個人情報取扱事業者、仮名加工情報取扱事業者、匿名加工取扱事業者...
[4] dist=0.1702 (pii.pdf) る国の政策の企画立案を担うとともに、個人情報取扱事業者、仮名加工情報取扱事業者、匿名加工取扱事業者...

--- 回答 ---
**個人情報とは**
個人情報は、個人を識別できる情報であり、プライバシーを含む個人の人格と密接に関わる情報です。日本国憲法第13条が定める「個人として尊重される」ことの下で、個人情報は慎重に取り扱わなければならないとされています [3][4]。

このように、個人情報は「個人の権利利益を保護すべき対象」として法的に位置付けられ、個人情報取扱事業者などが適正に管理・利用することが求められています [3][4]。

(参考:法第3条における個人情報の位置付け)

まとめ

このように、pgvectorを使うことで、使い慣れたPostgreSQLにベクトル検索機能を追加して、シンプルAIのRAG用にベクトルデータベースとして活用できます。専用のベクトルデータベースを構築が不要になり、システム構成がシンプルになります。

関連ページ