「既存の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が完成する
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
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など) 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
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
モデル選定の詳細は「ローカル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
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
ベクトル類似検索の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))
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を適用 |
pgvector+OllamaのRAGを2日間ハンズオンで実機構築する
PostgreSQLへのpgvector導入からOllamaのEmbedding取得、RAGパイプラインの実装まで、記事を読んだだけでは迷いやすい箇所を実機で一気に体験したい方向けに、「ローカルAIマスターセミナー」を開催しています。
少人数(最大8名)ZOOMハンズオン形式で実施しています。
・Ubuntu ServerでローカルLLMを構築する方法|Ollamaで機密データを外に出さず業務AIを動かす完全ガイド
・社内でChatGPTが使えないときの代替手段|機密データを守るローカルLLMという選択肢
・ローカルLLMのモデルを比較する方法|Llama3.3・Mistral・Gemma・Phi-4をUbuntuで使い分けるポイント
3,100名以上が実践した「型」を無料で公開中
プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら
- 前のページへ:OllamaをKubernetesにデプロイする方法|GPU NodeとHelm ChartでローカルLLMクラスターを本番スケール対応にする手順
- この記事の属するカテゴリ:ローカルLLMへ戻る

無料メルマガで学習を続ける
Linuxの実践スキルをメールで毎週お届け。
登録は30秒、解除もいつでも可。
登録無料・いつでも解除できます