pgvector 向量搜尋:從 Embedding 到 RAG 的完整實戰 | PostgreSQL
pgvector 是 PostgreSQL 最受歡迎的向量搜尋擴展,讓你可以在關聯式資料庫中直接儲存、索引並查詢高維度 Embedding 向量。無論是建構 RAG 知識庫、語意搜尋引擎還是推薦系統,pgvector 都能讓你用熟悉的 SQL 完成所有操作,不需要另外維護一套專用向量資料庫。本文從原理到實戰,帶你掌握 pgvector 的完整使用方式。
為什麼選擇 pgvector
在 AI 應用爆發的時代,「向量搜尋」已經成為基礎建設。Pinecone、Qdrant、Weaviate 等專用向量資料庫各有強項,但對已經使用 PostgreSQL 的團隊來說,pgvector 有著無可取代的優勢:
| 優勢 | 說明 |
|---|---|
| 零額外基礎設施 | 不需要另外部署、維護向量資料庫,一個 PostgreSQL 搞定所有 |
| ACID 事務保證 | 向量資料與業務資料在同一筆事務中更新,確保一致性 |
| SQL 整合 | 向量搜尋結果可以直接 JOIN 關聯式資料表,不需要跨系統查詢 |
| 生態系完整 | LangChain、LlamaIndex、Supabase 等主流框架原生支援 |
| 成本低 | 無 SaaS 按用量計費,只有基礎設施成本 |
向量搜尋原理
向量搜尋的核心概念是將非結構化資料(文字、圖片、音訊)透過 Embedding 模型轉換為高維度的數值向量,再用數學距離函式找出語義最相近的結果。
Embedding 的直觀理解——語義相似的文字在向量空間中的位置也相近:
「貓咪很可愛」→ [0.21, -0.08, 0.74, ..., 0.33] (1536 維)
「小貓真有趣」→ [0.19, -0.11, 0.71, ..., 0.35] (1536 維,向量距離近)
「股票市場」 → [-0.55, 0.82, -0.12, ..., 0.01] (1536 維,向量距離遠)
傳統 SQL 的 WHERE 子句是精確匹配,無法表達「語義相近」這種模糊概念。向量搜尋填補了這個缺口,使「找出意思相近的文件」成為可能。
向量搜尋在 RAG 中的角色:
使用者查詢
│
▼
Embedding 模型(如 OpenAI text-embedding-3-small)
│
▼
查詢向量 [0.21, -0.08, ...]
│
▼
pgvector 相似性搜尋 → 取得 Top-K 相關文件
│
▼
將文件上下文注入 LLM 提示詞
│
▼
LLM 生成最終回答
這個流程就是 RAG(Retrieval-Augmented Generation),目前企業 AI 應用最常見的架構之一。
安裝與啟用
系統套件安裝(推薦)
# Ubuntu / Debian
sudo apt install postgresql-17-pgvector
# macOS(Homebrew)
brew install pgvector
# 確認安裝
psql -c "SELECT * FROM pg_available_extensions WHERE name = 'vector';"
Docker 方式
# docker-compose.yml
services:
postgres:
image: pgvector/pgvector:pg17
environment:
POSTGRES_PASSWORD: secret
volumes:
- pgdata:/var/lib/postgresql/data
啟用擴展
-- 在目標資料庫中啟用
CREATE EXTENSION IF NOT EXISTS vector;
-- 確認版本
SELECT extversion FROM pg_extension WHERE extname = 'vector';
-- 結果:0.8.0
雲端託管支援
| 平台 | 支援方式 |
|---|---|
| Supabase | 預設啟用,無需額外設定 |
| Neon | CREATE EXTENSION vector 啟用 |
| AWS RDS (PG 15+) | 透過 Parameter Group 啟用 |
| Azure Flexible Server | 原生支援 |
| Google Cloud SQL (PG 14+) | 原生支援 |
向量型別
pgvector 提供四種向量型別,適用於不同場景:
| 型別 | 精度 | 每維度大小 | 最大維度 | 適用情境 |
|---|---|---|---|---|
vector | float32 | 4 bytes | 16,000 | 通用,OpenAI/Cohere Embedding |
halfvec | float16 | 2 bytes | 16,000 | 記憶體受限、大規模部署 |
sparsevec | float32 | 僅非零值 | 1,000,000+ | 稀疏 Embedding(SPLADE 等) |
bit | 二元 | 1/8 byte | 無限制 | 二元向量、高速篩選 |
vector — 標準浮點向量
-- 建立含向量欄位的資料表
CREATE TABLE documents (
id BIGSERIAL PRIMARY KEY,
content TEXT NOT NULL,
metadata JSONB,
embedding vector(1536) -- OpenAI text-embedding-3-small 的維度
);
-- 插入向量資料
INSERT INTO documents (content, embedding)
VALUES (
'機器學習是人工智慧的子領域',
'[0.021, -0.043, 0.187, ..., 0.034]'::vector
);
-- 基本相似性查詢(餘弦距離,取最近 5 筆)
SELECT id, content,
embedding <=> '[0.019, -0.041, 0.191, ...]'::vector AS distance
FROM documents
ORDER BY distance
LIMIT 5;
halfvec — 半精度向量(0.7.0+)
-- 使用 16 位元半精度浮點數,儲存空間減少 50%
CREATE TABLE documents_half (
id BIGSERIAL PRIMARY KEY,
content TEXT,
embedding halfvec(3072) -- text-embedding-3-large 的維度
);
-- 查詢語法相同
SELECT content, embedding <=> '[0.019, ...]'::halfvec AS distance
FROM documents_half
ORDER BY distance
LIMIT 10;
sparsevec — 稀疏向量(0.7.0+)
-- 只儲存非零元素,適合 BM25/SPLADE 等稀疏表示
CREATE TABLE sparse_docs (
id BIGSERIAL PRIMARY KEY,
content TEXT,
embedding sparsevec(100000)
);
-- 稀疏向量表示格式:{index:value, ...}/dimensions
INSERT INTO sparse_docs (content, embedding)
VALUES (
'PostgreSQL 向量搜尋',
'{1:0.8, 42:0.3, 1337:0.5}/100000'::sparsevec
);
距離函式
pgvector 支援三種主要距離度量,每種都有對應的運算子:
| 距離函式 | 運算子 | 公式 | 適用情境 |
|---|---|---|---|
| L2 距離(歐氏距離) | <-> | √(Σ(aᵢ - bᵢ)²) | 向量已正規化、圖像特徵比對 |
| 內積(Dot Product) | <#> | -Σ(aᵢ × bᵢ) | 已正規化 Embedding、推薦系統 |
| 餘弦距離 | <=> | 1 - (a·b)/(‖a‖‖b‖) | 文字 Embedding、語義相似度 |
-- 餘弦距離(最常用於文字 Embedding)
SELECT id, content, embedding <=> query_vec AS cosine_dist
FROM documents ORDER BY cosine_dist LIMIT 5;
-- L2 距離
SELECT id, content, embedding <-> query_vec AS l2_dist
FROM documents ORDER BY l2_dist LIMIT 5;
-- 內積(注意回傳負值,最小值 = 最相似)
SELECT id, content, embedding <#> query_vec AS neg_inner_product
FROM documents ORDER BY neg_inner_product LIMIT 5;
距離函式選擇指引:
| 情境 | 建議距離函式 | 原因 |
|---|---|---|
| OpenAI / Cohere Embedding | 餘弦距離或內積 | 模型輸出已正規化向量 |
| 圖像特徵向量 | L2 距離 | 向量長度具有語義意義 |
| 推薦系統 | 內積 | 效能最佳,向量通常已正規化 |
| 正規化向量(單位向量) | 三者等價 | 可優先選內積獲得最佳效能 |
0.7.0 版本另加入 L1 距離(<+>)、漢明距離(<~>)、Jaccard 距離(<%>)。
索引演算法:IVFFlat vs HNSW
向量搜尋的核心挑戰是「維度詛咒」——在高維空間中,精確最近鄰搜尋的計算量隨維度爆炸性增長。pgvector 提供兩種近似最近鄰(ANN)索引來解決這個問題。
IVFFlat 索引
IVFFlat(Inverted File with Flat Compression)的工作原理:
全量向量空間(N 個向量)
├── Centroid 1 → [向量A, 向量B, 向量C, ...]
├── Centroid 2 → [向量D, 向量E, ...]
├── Centroid 3 → [向量F, 向量G, ...]
│ ...
└── Centroid K → [...]
查詢時只搜尋最近的 probes 個 Centroid 的列表
- 訓練期:K-means 將所有向量分群為
lists個群集 - 查詢期:找出最近的
probes個群集,只在這些群集中精確搜尋
-- 建立 IVFFlat 索引(餘弦距離)
CREATE INDEX ON documents
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);
-- 查詢時調整 probes(搜尋更多群集 = 更高 Recall)
SET ivfflat.probes = 10;
關鍵參數:
| 參數 | 說明 | 建議值 |
|---|---|---|
lists | 分群數量 | rows / 1000(至少 100) |
ivfflat.probes | 查詢搜尋的群集數 | √lists 到 lists/10 |
注意:索引建立前需要資料已存在(K-means 需要訓練資料)。
HNSW 索引
HNSW(Hierarchical Navigable Small World)的工作原理:
Layer 2(最稀疏):A ─────────────── H
Layer 1: A ── C ── E ── G ── H
Layer 0(最密): A─B─C─D─E─F─G─H─...
查詢從 Layer 2 快速定位大致方向,逐層精細搜尋
-- 建立 HNSW 索引(餘弦距離)
CREATE INDEX ON documents
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
-- 查詢時調整 ef_search(更大 = 更高 Recall、更慢)
SET hnsw.ef_search = 100;
關鍵參數:
| 參數 | 說明 | 預設值 | 調校建議 |
|---|---|---|---|
m | 每個節點的最大連結數 | 16 | Recall 要求高則增大(最大 256) |
ef_construction | 建立時的候選列表大小 | 64 | 增大提高 Recall,建立更慢 |
hnsw.ef_search | 查詢時的候選列表大小 | 40 | 增大提高 Recall,查詢更慢 |
兩者比較
| 指標 | IVFFlat | HNSW |
|---|---|---|
| 建立速度 | 快 | 慢(約 3-5 倍) |
| 查詢速度 | 較慢 | 快 |
| 記憶體使用 | 低 | 高(約 2-3 倍) |
| Recall@10 | ~95% | ~98% |
| 動態插入 | 差(需重建索引) | 佳(無需重建) |
| 適用場景 | 靜態資料集、記憶體受限 | 動態資料、高 Recall 需求 |
選擇建議:大多數場景推薦 HNSW,除非記憶體嚴重受限或資料幾乎不更新。
確認索引被使用
-- 使用 EXPLAIN 確認查詢計畫
EXPLAIN (ANALYZE, BUFFERS)
SELECT id, content
FROM documents
ORDER BY embedding <=> '[0.021, -0.043, ...]'::vector
LIMIT 10;
-- 若看到 "Index Scan using xxx_idx" → 索引有效
-- 若看到 "Seq Scan" → 可能原因:
-- 1. 資料量不足(優化器認為全表掃描更快)
-- 2. LIMIT 值太大
-- 3. 索引 operator class 與查詢運算子不符
LangChain 整合
LangChain 是最主流的 RAG 框架,與 pgvector 整合非常簡單:
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain_community.vectorstores import PGVector
from langchain_core.documents import Document
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser
# 設定連線
CONNECTION_STRING = "postgresql+psycopg2://user:password@localhost:5432/mydb"
# 初始化 Embedding 模型
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
# 建立或連接向量資料庫
vectorstore = PGVector(
collection_name="documents",
connection_string=CONNECTION_STRING,
embedding_function=embeddings,
)
# 新增文件
docs = [
Document(page_content="PostgreSQL 是功能強大的關聯式資料庫", metadata={"source": "wiki"}),
Document(page_content="pgvector 讓 PostgreSQL 支援向量搜尋", metadata={"source": "docs"}),
Document(page_content="RAG 架構使用向量搜尋增強 LLM 回答", metadata={"source": "blog"}),
]
vectorstore.add_documents(docs)
# 相似性搜尋
results = vectorstore.similarity_search("PostgreSQL 如何支援 AI 應用?", k=3)
# 建立完整 RAG Chain
retriever = vectorstore.as_retriever(search_kwargs={"k": 4})
llm = ChatOpenAI(model="gpt-4o-mini")
prompt = ChatPromptTemplate.from_template("""
根據以下上下文回答問題:
上下文:{context}
問題:{question}
""")
def format_docs(docs):
return "\n\n".join(doc.page_content for doc in docs)
rag_chain = (
{"context": retriever | format_docs, "question": RunnablePassthrough()}
| prompt
| llm
| StrOutputParser()
)
answer = rag_chain.invoke("pgvector 有什麼優勢?")
print(answer)
原生 Python 實作 RAG
不依賴框架,直接用 psycopg2 + OpenAI API 實作完整 RAG 流程:
import psycopg2
import psycopg2.extras
import openai
def get_embedding(text: str, model: str = "text-embedding-3-small") -> list:
"""呼叫 OpenAI API 取得 Embedding 向量"""
client = openai.OpenAI()
response = client.embeddings.create(input=text, model=model)
return response.data[0].embedding
def ingest_document(conn, content: str, metadata: dict = None):
"""將文件及其 Embedding 存入 PostgreSQL"""
embedding = get_embedding(content)
with conn.cursor() as cur:
cur.execute(
"""INSERT INTO documents (content, metadata, embedding)
VALUES (%s, %s, %s::vector) RETURNING id""",
(content, psycopg2.extras.Json(metadata or {}), embedding)
)
doc_id = cur.fetchone()[0]
conn.commit()
return doc_id
def semantic_search(conn, query: str, k: int = 5, threshold: float = 0.8):
"""語意搜尋:找出與查詢最相似的文件"""
query_embedding = get_embedding(query)
with conn.cursor() as cur:
cur.execute(
"""SELECT id, content, metadata,
1 - (embedding <=> %s::vector) AS similarity
FROM documents
WHERE 1 - (embedding <=> %s::vector) > %s
ORDER BY embedding <=> %s::vector
LIMIT %s""",
(query_embedding, query_embedding, threshold, query_embedding, k)
)
return [
{"id": r[0], "content": r[1], "metadata": r[2], "similarity": r[3]}
for r in cur.fetchall()
]
def rag_answer(conn, question: str) -> str:
"""完整 RAG 流程:搜尋 → 組合提示詞 → LLM 回答"""
# Step 1: 向量搜尋
relevant_docs = semantic_search(conn, question, k=4)
context = "\n\n".join(doc["content"] for doc in relevant_docs)
# Step 2: 呼叫 LLM
client = openai.OpenAI()
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": f"根據以下資料回答:\n{context}\n\n問題:{question}"}]
)
return response.choices[0].message.content
混合搜尋(Hybrid Search)
結合向量搜尋與全文搜尋,使用 Reciprocal Rank Fusion(RRF) 融合排名:
-- 混合搜尋:向量相似度 + BM25 全文搜尋
WITH vector_results AS (
SELECT id, content,
ROW_NUMBER() OVER (
ORDER BY embedding <=> '[...]'::vector
) AS vector_rank
FROM documents
ORDER BY embedding <=> '[...]'::vector
LIMIT 50
),
text_results AS (
SELECT id, content,
ROW_NUMBER() OVER (
ORDER BY ts_rank(
to_tsvector('english', content),
plainto_tsquery('pgvector')
) DESC
) AS text_rank
FROM documents
WHERE to_tsvector('english', content) @@ plainto_tsquery('pgvector')
LIMIT 50
)
-- RRF 融合排名(k=60 是常用常數)
SELECT
COALESCE(v.id, t.id) AS id,
COALESCE(v.content, t.content) AS content,
COALESCE(1.0 / (60 + v.vector_rank), 0) +
COALESCE(1.0 / (60 + t.text_rank), 0) AS rrf_score
FROM vector_results v
FULL OUTER JOIN text_results t ON v.id = t.id
ORDER BY rrf_score DESC
LIMIT 10;
混合搜尋的優勢在於:向量搜尋擅長語義理解,全文搜尋擅長精確關鍵字匹配,兩者互補能顯著提升搜尋品質。
百萬級向量效能調校
PostgreSQL 設定調校
-- postgresql.conf 關鍵設定
-- 共享記憶體(建議為 RAM 的 25%)
shared_buffers = '8GB'
-- 工作記憶體(影響排序、Hash Join)
work_mem = '256MB'
-- 維護記憶體(影響 HNSW 建立、VACUUM)
maintenance_work_mem = '2GB'
-- 平行查詢
max_parallel_workers_per_gather = 4
-- 平行建立 HNSW 索引
SET max_parallel_maintenance_workers = 7;
分批次插入
from psycopg2.extras import execute_values
def batch_insert(conn, records, batch_size=1000):
"""分批次插入向量,避免單次事務過大"""
for i in range(0, len(records), batch_size):
batch = records[i:i + batch_size]
with conn.cursor() as cur:
execute_values(
cur,
"INSERT INTO documents (content, embedding) VALUES %s",
[(r["content"], r["embedding"]) for r in batch],
template="(%s, %s::vector)"
)
conn.commit()
print(f"已插入 {min(i + batch_size, len(records))}/{len(records)} 筆")
索引建立策略
-- 先大量插入,再建立索引(最快)
DROP INDEX IF EXISTS documents_embedding_idx;
-- 大量 INSERT ...
-- HNSW 百萬級參數建議
CREATE INDEX CONCURRENTLY documents_embedding_idx
ON documents USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
-- m: 16(平衡效能與記憶體)
-- ef_construction: 64-128(品質,愈大愈慢但 Recall 更高)
-- ef_search: 40-100(視延遲容忍度調整)
量化加速(0.8.0+)
-- Scalar Quantization:float32 → int8,節省 75% 索引記憶體
CREATE INDEX ON documents USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64, quantization = sq);
-- Binary Quantization:最激進壓縮,轉為 1-bit
-- 適合維度 >= 1024,搭配重新排序使用
CREATE INDEX ON documents USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64, quantization = bq);
資料表分割
-- 對百萬級向量使用範圍分割
CREATE TABLE documents (
id BIGSERIAL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
content TEXT,
embedding vector(1536)
) PARTITION BY RANGE (created_at);
-- 按月建立分割
CREATE TABLE documents_2026_01 PARTITION OF documents
FOR VALUES FROM ('2026-01-01') TO ('2026-02-01');
-- 在每個分割上建立 HNSW 索引
CREATE INDEX ON documents_2026_01 USING hnsw (embedding vector_cosine_ops);
效能監控
-- 監控索引大小
SELECT indexname,
pg_size_pretty(pg_relation_size(indexname::regclass)) AS index_size
FROM pg_indexes
WHERE tablename = 'documents';
-- 查看快取命中率
SELECT
heap_blks_hit::float / NULLIF(heap_blks_hit + heap_blks_read, 0) AS table_cache,
idx_blks_hit::float / NULLIF(idx_blks_hit + idx_blks_read, 0) AS index_cache
FROM pg_statio_user_tables
WHERE relname = 'documents';
pgvector vs 專用向量資料庫
| 比較維度 | pgvector | Pinecone | Qdrant |
|---|---|---|---|
| 部署方式 | 自行管理或雲端 PaaS | 全託管 SaaS | 自行部署或雲端 |
| 開源 | 是 | 否 | 是 |
| 關聯式資料整合 | 原生(同一資料庫) | 無 | 有限 |
| ACID 事務 | 完整支援 | 不支援 | 部分支援 |
| 最大向量規模 | 億級(調校後) | 十億級 | 十億級 |
| 混合搜尋 | 需自行實作 | 原生支援 | 原生支援 |
| 費用 | 基礎設施成本 | 依用量計費(較貴) | 基礎設施成本 |
適合 pgvector 的場景:已使用 PostgreSQL、需要 ACID 事務、向量資料與業務資料緊密結合、中小規模(< 1000 萬向量)、成本敏感。
不適合 pgvector 的場景:數十億向量且對延遲有毫秒級要求、純向量搜尋產品、需要原生複雜混合搜尋 API。
常見問題與陷阱
| 問題 | 原因 | 解決方案 |
|---|---|---|
| 查詢走 Seq Scan 不走索引 | 資料量太少或 LIMIT 太大 | 確保資料量足夠,LIMIT 在合理範圍(< 100) |
| Recall 太低 | HNSW 的 ef_search 或 IVFFlat 的 probes 太小 | 增大 ef_search / probes |
| 索引建立超慢 | maintenance_work_mem 太小 | 設為 2GB+,增加 max_parallel_maintenance_workers |
| 記憶體不足 | HNSW 記憶體消耗大 | 改用 IVFFlat 或 halfvec 減少記憶體 |
| 向量維度不匹配 | 插入向量維度與欄位定義不同 | 確認 Embedding 模型的輸出維度 |
| INSERT 效能差 | 已有 HNSW 索引時逐筆插入 | 分批插入,或先 DROP INDEX 再重建 |
| 距離函式選錯 | 索引的 ops class 與查詢運算子不符 | 確保 vector_cosine_ops 對應 <=> 運算子 |
版本演進
| 版本 | 發布時間 | 重要新功能 |
|---|---|---|
| 0.4.0 | 2023-05 | 初始穩定版本,IVFFlat 索引 |
| 0.5.0 | 2023-09 | HNSW 索引、維度上限提升至 16,000 |
| 0.6.0 | 2024-01 | 平行建立 HNSW 索引 |
| 0.7.0 | 2024-04 | halfvec、sparsevec、bit 型別 |
| 0.8.0 | 2024-11 | Scalar/Binary Quantization |
-- 升級 pgvector Extension
ALTER EXTENSION vector UPDATE;
-- 升級後可能需要重建索引(尤其跨大版本)
REINDEX INDEX CONCURRENTLY documents_embedding_idx;
總結
pgvector 讓 PostgreSQL 成為 AI 時代的一站式資料平台——你不需要額外維護一套向量資料庫,就能在熟悉的 SQL 環境中完成 Embedding 儲存、向量索引和語義搜尋。搭配 HNSW 索引和適當的效能調校,百萬級向量的查詢延遲可以控制在個位數毫秒。
關鍵要點:
- HNSW 是大多數場景的首選索引,支援動態插入且 Recall 更高
- 距離函式選擇取決於 Embedding 模型——OpenAI 等已正規化向量用餘弦距離或內積
- 百萬級規模需要系統性調校:分批插入、先 DROP INDEX 再重建、調整
maintenance_work_mem - 混合搜尋(向量 + 全文)用 RRF 融合排名,能顯著提升搜尋品質
下一篇,我們將探討 PostGIS 地理空間——從空間資料型別、座標系統到幾何查詢與空間索引的完整實戰。