OllamaとPostgreSQL/pgvectorでローカルベクターDBを構築する方法|既存DBインフラにEmbeddingを統合してRAGをエンタープライズ化する手順

宮崎智広 この記事の監修:宮崎智広(Linux実務・教育歴20年以上・受講者3,100名超)
HOMELinux技術 リナックスマスター.JP(Linuxマスター.JP)ローカルLLM > OllamaとPostgreSQL/pgvectorでローカルベクターDBを構築する方法|既存DBインフラにEmbeddingを統合してRAGをエンタープライズ化する手順
「社内RAGを構築したが、FAISSやChromaのインメモリDBではサーバー再起動のたびにデータが消えてしまう」
「既存のPostgreSQLインフラをそのまま活かして、OllamaのEmbeddingを永続保存・全文検索と併用したい」

そんな悩みを抱えるインフラエンジニアやDBA担当者は多いはずです。この記事では、PostgreSQLの拡張機能pgvectorをUbuntu Serverに導入し、OllamaのEmbedding APIと組み合わせてローカル完結のベクターDBを構築する手順を解説します。インメモリ系ベクターDBとの違い、テーブル設計とHNSWインデックスの選定、Pythonによる格納・類似検索コード、実用RAGパイプラインの実装まで一気通貫でカバーします。クラウドAPIへのデータ送信なし、社内サーバーだけで動く構成です。

この記事のポイント

・CREATE EXTENSION vector; の1行でPostgreSQLにpgvectorを組み込める
・nomic-embed-textの768次元EmbeddingをHNSWインデックスで高速類似検索できる
・psycopg3 + pgvectorライブラリでPythonからベクターをそのまま格納・取得できる
・Ollama推論もEmbeddingもPostgreSQLも社内サーバーで完結するRAGが完成する


OllamaとPostgreSQL/pgvectorでローカルベクターDBを構築する方法|既存DBインフラにEmbeddingを統合してRAGをエンタープライズ化する手順

「このままじゃマズい」と感じていませんか?
参考書を開く気力もない、同年代に取り残される不安——
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら

pgvectorとは何か — FAISS・Chromaと何が違うのか

pgvectorはPostgreSQLに組み込めるベクトル演算拡張です。`vector`型のカラムを追加し、内積・コサイン距離・L2距離を使ったANN(近似最近傍)インデックス(IVFFlat・HNSW)で類似検索できます。GitHubで公開されているオープンソースプロジェクトで、バージョン0.8.0以降はHNSWインデックスが安定して使えるようになっています。

FAISSはMeta製の高速インメモリライブラリです。検索速度はトップクラスですが、永続化は自前実装になります。Pythonプロセスが落ちるとインデックスを再構築する必要があり、本番運用では別途ファイルへのシリアライズと読み込みを管理しなければなりません。

Chromaはベクターに特化した軽量DBですが、サーバーモードでの運用では別プロセスを立てる必要があり、本番環境ではインフラ管理のレイヤーが増えます。

pgvectorの強みは「既存のPostgreSQLサーバーにCREATE EXTENSIONの1行で組み込める」点です。企業のDBチームがすでにPostgreSQLを運用している場合、バックアップ・レプリケーション・アクセス制御をそのまま引き継げます。ベクターデータとメタデータを同一トランザクションで扱えるため、ACID保証が必要なエンタープライズ用途での採用が増えています。

Ollamaの構築手順については「Ubuntu ServerでローカルLLMを構築する方法|Ollamaで機密データを外に出さず業務AIを動かす完全ガイド」で詳しく解説しています。pgvectorを導入する前にOllamaが起動済みであることを確認してください。

Ubuntu ServerにPostgreSQL+pgvectorをインストールする

1. PostgreSQL 16をaptでインストールする

Ubuntu 22.04/24.04ではPostgreSQL公式リポジトリからPostgreSQL 16を導入するのが最も安定しています。

# PostgreSQL公式リポジトリを追加 sudo apt install -y gnupg curl curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc \ | sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/postgresql.gpg echo "deb https://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" \ | sudo tee /etc/apt/sources.list.d/pgdg.list sudo apt update sudo apt install -y postgresql-16 postgresql-server-dev-16 # サービス起動確認 sudo systemctl status postgresql # 出力例(抜粋): # Active: active (running) since ...

2. pgvector拡張をインストールする

Ubuntu 24.04ではpgvectorがaptパッケージとして提供されています。

sudo apt install -y postgresql-16-pgvector # 拡張が利用可能か確認 sudo -u postgres psql -c "SELECT name, default_version FROM pg_available_extensions WHERE name = 'vector';" # 出力例: # name | default_version # --------+----------------- # vector | 0.8.0

Ubuntu 22.04など古い環境ではaptパッケージが提供されていない場合があります。その場合はソースビルドします。

# ソースビルドが必要な場合(Ubuntu 22.04など) sudo apt install -y git build-essential git clone https://github.com/pgvector/pgvector.git cd pgvector make sudo make install

3. RAG用データベースとユーザーを作成する

sudo -u postgres psql << 'EOF' CREATE DATABASE ragdb; CREATE USER raguser WITH PASSWORD 'StrongPass123!'; GRANT ALL PRIVILEGES ON DATABASE ragdb TO raguser; \c ragdb CREATE EXTENSION vector; GRANT ALL ON SCHEMA public TO raguser; EOF

パスワードは実運用では `.pgpass` ファイルまたは環境変数 `PGPASSWORD` で管理し、スクリプトにハードコードしないようにします。`CREATE EXTENSION vector;` はDB単位で実行するため、複数DBでpgvectorを使う場合はそれぞれのDB内で実行が必要です。

pgvectorのテーブルとインデックスを設計する

1. Embeddingを格納するテーブルを作成する

Ollamaの `nomic-embed-text` モデルが出力するベクトルは768次元です。テーブル設計では、ベクトルと一緒にソース情報(ファイル名・チャンク番号・本文抜粋)を持たせます。RAG検索でヒットしたチャンクをLLMのプロンプトに渡すため、`content` カラムは必須です。

psql -U raguser -d ragdb << 'EOF' CREATE TABLE documents ( id BIGSERIAL PRIMARY KEY, source TEXT NOT NULL, -- ファイル名やURL chunk_index INTEGER NOT NULL, -- チャンク連番(0始まり) content TEXT NOT NULL, -- 本文テキスト(検索ヒット時にLLMへ渡す) embedding vector(768), -- nomic-embed-text は768次元 created_at TIMESTAMP DEFAULT NOW() ); -- HNSWインデックス(コサイン距離)を作成 CREATE INDEX ON documents USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 64); EOF

2. HNSWとIVFFlatの選択基準

インデックス方式は2種類あります。HNSWはデータ追加のコストは高いものの、検索精度と速度のバランスが良く、件数が少ない初期段階でも機能します。IVFFlatはバルクINSERT後にVACUUM ANALYZEが必要なため、「まず動かす」フェーズではHNSWが無難です。

`m = 16` はHNSWのグラフの各ノードが持つ最大接続数です。値を大きくすると精度が上がりますがメモリ使用量が増加します。`ef_construction = 64` はインデックス構築時の探索幅です。初期値として m=16, ef_construction=64 を使い、精度不足なら両方を増やして再作成します。件数が100万行を超えたらIVFFlatへの移行も検討します。

OllamaのEmbedding APIで文書をベクトル化する

1. nomic-embed-textモデルを取得して動作確認する

ollama pull nomic-embed-text # curlでEmbeddingを取得して次元数を確認 curl -s http://localhost:11434/api/embeddings \ -d '{"model":"nomic-embed-text","prompt":"PostgreSQLでベクター検索をする方法"}' \ | python3 -c "import sys,json; d=json.load(sys.stdin); print(f'次元数: {len(d[\"embedding\"])}')" # 出力例: # 次元数: 768

他のEmbeddingモデルを使う場合は次元数が変わります。`mxbai-embed-large` は1024次元なので、テーブルの `vector(768)` を `vector(1024)` に変更する必要があります。モデルを途中で変えると格納済みデータと次元数が一致しなくなるため、プロジェクト開始時にモデルを固定するのが鉄則です。

モデル選定の詳細は「ローカルLLMのモデルを比較する方法|Llama3.3・Mistral・Gemma・Phi-4をUbuntuで使い分けるポイント」も参照してください。

2. テキストをチャンク分割する方針

OllamaのEmbedding APIは長いテキストでも受け付けますが、意味の薄い大きなチャンクは検索精度を下げます。500文字前後でオーバーラップ50字の分割が実績のある設定です。1チャンクが長すぎると複数トピックが混在して意味ベクトルが拡散し、短すぎると前後の文脈が失われます。

文書の種類によって最適なチャンク長は異なります。契約書・仕様書など段落構造が明確な文書は段落単位で分割するほうが精度が上がる場合があります。議事録・メールなど短文が多い文書は200字前後でも十分です。

PythonとpsycopgでEmbeddingをPostgreSQLに格納する

1. 必要パッケージをインストールする

pip install "psycopg[binary]" pgvector requests

psycopg3(`psycopg` パッケージ)はpgvectorの `register_vector()` ヘルパーが提供されており、Pythonのリストをそのまま `vector` 型として送れます。psycopg2系を使う場合は `from pgvector.psycopg2 import register_vector` をインポートします。

2. 文書をチャンク分割してDBに格納するスクリプト

#!/usr/bin/env python3 import psycopg from pgvector.psycopg import register_vector import requests import os import sys OLLAMA_URL = "http://localhost:11434/api/embeddings" EMBED_MODEL = "nomic-embed-text" DSN = "postgresql://raguser:StrongPass123!@localhost/ragdb" def get_embedding(text: str) -> list: resp = requests.post(OLLAMA_URL, json={"model": EMBED_MODEL, "prompt": text}) resp.raise_for_status() return resp.json()["embedding"] def chunk_text(text: str, size: int = 500, overlap: int = 50) -> list: chunks, start = [], 0 while start < len(text): end = min(start + size, len(text)) chunks.append(text[start:end]) start += size - overlap return chunks def ingest_file(filepath: str): with open(filepath, encoding="utf-8") as f: content = f.read() chunks = chunk_text(content) with psycopg.connect(DSN) as conn: register_vector(conn) with conn.cursor() as cur: for i, chunk in enumerate(chunks): emb = get_embedding(chunk) cur.execute( "INSERT INTO documents (source, chunk_index, content, embedding)" " VALUES (%s, %s, %s, %s)", (os.path.basename(filepath), i, chunk, emb) ) conn.commit() print(f"{filepath}: {len(chunks)}チャンクをDBに格納完了") if __name__ == "__main__": for path in sys.argv[1:]: ingest_file(path)

実行例は次のとおりです。

# テキストファイルを格納 python3 ingest.py /var/docs/manual.txt /var/docs/faq.txt # 格納件数を確認 psql -U raguser -d ragdb -c "SELECT source, count(*) FROM documents GROUP BY source;" # 出力例: # source | count # -------------+------- # manual.txt | 142 # faq.txt | 58

大量ファイルを一括取り込む場合は `ThreadPoolExecutor` でOllama APIへの並列呼び出しを検討します。ただしOllamaのデフォルト並列数は1なので、先に `OLLAMA_NUM_PARALLEL=4` を systemd の override.conf で設定してから並列化します。

ベクトル類似検索のSQLと実用RAGパイプラインを実装する

1. pgvectorの類似検索演算子を確認する

pgvectorは3種類の距離演算子を提供しています。
・`<=>` — コサイン距離(HNSWの vector_cosine_ops インデックスが効く)
・`<->` — L2ユークリッド距離(vector_l2_ops インデックスが対応)
・`<#>` — 負の内積(内積最大化が目的の場合に使用)

テキストEmbeddingの類似検索ではコサイン距離が最も一般的です。コサイン距離は0(完全一致)~2(正反対)の範囲で、1 - コサイン距離でコサイン類似度(0~1)に変換できます。

-- コサイン距離での類似検索(Pythonから埋め込みベクトルを%sで渡す) SELECT source, chunk_index, content, 1 - (embedding <=> %s::vector) AS cosine_similarity FROM documents ORDER BY embedding <=> %s::vector LIMIT 5;

2. OllamaとpgvectorをつないだRAGパイプライン

#!/usr/bin/env python3 import psycopg from pgvector.psycopg import register_vector import requests OLLAMA_EMBED = "http://localhost:11434/api/embeddings" OLLAMA_CHAT = "http://localhost:11434/api/generate" EMBED_MODEL = "nomic-embed-text" LLM_MODEL = "llama3.3:70b-instruct-q4_0" DSN = "postgresql://raguser:StrongPass123!@localhost/ragdb" def retrieve(question: str, top_k: int = 5) -> list: emb = requests.post(OLLAMA_EMBED, json={"model": EMBED_MODEL, "prompt": question}).json()["embedding"] with psycopg.connect(DSN) as conn: register_vector(conn) with conn.cursor() as cur: cur.execute( "SELECT content FROM documents ORDER BY embedding <=> %s::vector LIMIT %s", (emb, top_k) ) return [row[0] for row in cur.fetchall()] def answer(question: str) -> str: contexts = retrieve(question) prompt = "以下の資料を参考に質問に答えてください。\n\n" prompt += "\n---\n".join(contexts) prompt += f"\n\n質問: {question}\n回答:" resp = requests.post(OLLAMA_CHAT, json={"model": LLM_MODEL, "prompt": prompt, "stream": False}) return resp.json()["response"] if __name__ == "__main__": q = "pgvectorのHNSWインデックスの設定方法を教えてください" print(answer(q))

この構成でOllamaのEmbedding取得・LLM推論、PostgreSQLでのベクター格納・検索がすべてローカルで完結します。クラウドAPIへのデータ送信はゼロです。社内機密文書をRAGで活用する際の情報漏洩リスクについては「社内でChatGPTが使えないときの代替手段|機密データを守るローカルLLMという選択肢」も参考にしてください。

3. 検索精度を上げるための工夫

Top-K件の検索結果をそのままLLMに渡すと、低スコアのチャンクが混入して回答精度が下がる場合があります。コサイン類似度が0.7未満のチャンクを除外するフィルタリングが有効です。

SQLでフィルタリングする場合は `WHERE 1 - (embedding <=> %s::vector) >= 0.7` の条件を追加します。閾値はドキュメントの性質によって異なるため、実データで検索ログを取りながら調整します。

また、ハイブリッド検索(ベクター類似検索+PostgreSQLのFULL TEXT SEARCH)を組み合わせると、固有名詞や型番など意味ベクトルが似ていない語句でも高精度に検索できます。pgvectorとto_tsvectorを併用する構成はPostgreSQL単体で実現できます。

pgvector運用時のトラブルと注意点

次元数ミスマッチエラーが出る

モデルを変更したり、誤った次元数でテーブルを作成すると `ERROR: expected 768 dimensions, not 1024` が出ます。格納済みデータと次元数が一致しなくなった場合は、テーブルを再作成してデータを再取り込みします。Embeddingモデルの変更はスキーマ変更と同義です。複数モデルを試したい場合は `embedding_768 vector(768)` と `embedding_1024 vector(1024)` の2カラムを持つ設計も有効です。

HNSWインデックスが使われずSeq Scanになる

`EXPLAIN ANALYZE` で確認し `Seq Scan` になっている場合は行数不足が原因の可能性があります。数百行以下ではオプティマイザがシーケンシャルスキャンのほうが速いと判断します。開発・テスト中に強制的にインデックスを使わせたい場合は `SET enable_seqscan = off;` を接続ごとに設定します。本番では行数が数千以上になると自然にインデックスが使われるようになります。

初期大量投入でインサートが遅い

HNSWインデックスはデータ追加のたびにグラフを更新するため、初期の大量投入では速度が低下します。数万件以上の初期取り込みでは、一時的にインデックスを削除してからINSERTし、完了後に `CREATE INDEX ON documents USING hnsw (embedding vector_cosine_ops)` で再作成するほうが大幅に高速です。再作成中に検索が止まるのを避けるには `CREATE INDEX CONCURRENTLY` オプションを使います。

register_vector(conn)を忘れてエラーになる

psycopg3では、コネクションを確立するたびに `register_vector(conn)` を呼ぶ必要があります。コネクションプール(`psycopg.pool.ConnectionPool`)を使う場合は `configure` コールバックに登録します。

from psycopg.pool import ConnectionPool from pgvector.psycopg import register_vector pool = ConnectionPool( DSN, configure=lambda conn: register_vector(conn) )

まとめ

操作・要素 コマンド・設定例 ポイント
pgvector有効化 CREATE EXTENSION vector; DB単位で実行。postgresql-16-pgvectorをaptで事前インストール
テーブル設計 embedding vector(768) nomic-embed-textは768次元。モデル変更はスキーマ変更と同義
HNSWインデックス USING hnsw (embedding vector_cosine_ops) WITH (m=16, ef_construction=64) 初期推奨値。精度不足なら両値を増やして再作成
類似検索SQL ORDER BY embedding <=> %s::vector LIMIT 5 <=> はコサイン距離。1-距離でコサイン類似度に変換
Pythonライブラリ pip install "psycopg[binary]" pgvector requests register_vector(conn)をコネクションごとに呼ぶ
大量初期投入の高速化 インデックス削除→INSERT→CREATE INDEX CONCURRENTLY の順 HNSWのグラフ更新コストを回避して大幅に短縮
接続プール対応 ConnectionPool(DSN, configure=lambda conn: register_vector(conn)) プール全コネクションに自動でregister_vectorを適用
Ollamaとpgvectorを組み合わせると、既存のPostgreSQLインフラをそのままにローカル完結のRAGを本番レベルで構築できます。インメモリベクターDBの「再起動でデータが消える」問題を解消しながら、PostgreSQLの堅牢なバックアップ・レプリケーション・アクセス制御を引き継げるのが最大のメリットです。チャンク設計・閾値フィルタリング・ハイブリッド検索を段階的に加えることで、社内文書RAGの精度を継続的に高めていけます。

pgvector+OllamaのRAGを2日間ハンズオンで実機構築する

PostgreSQLへのpgvector導入からOllamaのEmbedding取得、RAGパイプラインの実装まで、記事を読んだだけでは迷いやすい箇所を実機で一気に体験したい方向けに、「ローカルAIマスターセミナー」を開催しています。
少人数(最大8名)ZOOMハンズオン形式で実施しています。

>> ローカルAIマスターセミナーの詳細を確認する
ローカルLLMの構築・運用に関する関連記事もあわせて参考にしてください。

Ubuntu ServerでローカルLLMを構築する方法|Ollamaで機密データを外に出さず業務AIを動かす完全ガイド
社内でChatGPTが使えないときの代替手段|機密データを守るローカルLLMという選択肢
ローカルLLMのモデルを比較する方法|Llama3.3・Mistral・Gemma・Phi-4をUbuntuで使い分けるポイント

無料メルマガで学習を続ける

Linuxの実践スキルをメールで毎週お届け。
登録は30秒、解除もいつでも可。

登録無料・いつでも解除できます

暗記不要・1時間後にはサーバーが動く

3,100名以上が実践した「型」を無料で公開中

プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。

姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら

Linux無料マニュアル(図解60P) 名前とメールで30秒登録
宮崎 智広

この記事を書いた人

宮崎 智広(みやざき ともひろ)

株式会社イーネットマーキュリー代表。現役のLinuxサーバー管理者として20年以上の実務経験を持ち、これまでに累計3,100名以上のエンジニアを指導してきたLinux教育のプロフェッショナル。「現場で本当に使える技術」を体系的に伝えることをモットーに、実践型のLinuxセミナーの開催や無料マニュアルの配布を通じてLinux人材の育成に取り組んでいる。

趣味は、キャンプにカメラ、トラウト釣り。好きな食べ物は、ラーメンにお酒。休肝日が作れない、酒量を減らせないのが悩み。最近、ドラマ「フライトエンジェル」を観て涙腺が崩壊しました。