Tại sao mình chọn pgvector thay vì vector database riêng?
Dự án RAG đầu tiên mình tham gia, team đã cài Qdrant riêng — một service nữa để deploy, monitor, backup. Sau vài tháng vận hành, câu hỏi đặt ra là: nếu PostgreSQL đã chạy ổn định rồi, tại sao lại cần thêm một hệ thống chỉ để lưu vector?
Mình đã làm việc với MySQL, PostgreSQL và MongoDB trong nhiều dự án khác nhau. PostgreSQL với extension pgvector cho phép lưu vector embedding ngay trong database hiện có, dùng SQL quen thuộc, tận dụng transaction, backup và replication sẵn có. Với team nhỏ hoặc dự án vừa, đây là lựa chọn thực dụng hơn nhiều so với duy trì thêm một hệ thống độc lập.
pgvector hỗ trợ:
- Lưu vector dạng
float4[]với kiểu dữ liệuvector(n) - Tìm kiếm nearest neighbor (L2, cosine, inner product)
- Index IVFFlat và HNSW để tìm kiếm nhanh trên dataset lớn
- Tích hợp trực tiếp với
pgbouncer,pg_stat_statementsnhư mọi extension khác
Cài đặt pgvector
Cài trên Ubuntu/Debian
Nếu dùng PostgreSQL từ repo PGDG chính thức, một lệnh là xong:
# PostgreSQL 16
sudo apt install postgresql-16-pgvector
# Restart để load extension
sudo systemctl restart postgresql
Không có repo PGDG? Build từ source:
sudo apt install -y postgresql-server-dev-16 git build-essential
git clone https://github.com/pgvector/pgvector.git
cd pgvector
make
sudo make install
Cài bằng Docker
Cách nhanh nhất để test — image pgvector/pgvector đã bundle sẵn extension, không cần cài thêm gì:
docker run -d \
--name pgvector-dev \
-e POSTGRES_PASSWORD=secret \
-e POSTGRES_DB=vectordb \
-p 5432:5432 \
pgvector/pgvector:pg16
Enable extension trong database
Một điểm hay của PostgreSQL: extension cài ở system level nhưng bật/tắt riêng cho từng database. Không lo extension này ảnh hưởng sang database khác:
-- Kết nối vào database cần dùng
\c vectordb
-- Enable extension
CREATE EXTENSION IF NOT EXISTS vector;
-- Kiểm tra
SELECT * FROM pg_extension WHERE extname = 'vector';
Cấu hình và sử dụng chi tiết
Tạo bảng lưu vector embedding
Giả sử đang xây dựng hệ thống RAG cho knowledge base nội bộ — tài liệu được chunk thành đoạn nhỏ, mỗi đoạn có một embedding vector 1536 chiều (output của text-embedding-3-small của OpenAI):
CREATE TABLE documents (
id BIGSERIAL PRIMARY KEY,
content TEXT NOT NULL,
source TEXT,
metadata JSONB,
embedding vector(1536),
created_at TIMESTAMPTZ DEFAULT NOW()
);
Dimension phải khớp chính xác với model embedding đang dùng — sai dimension là lỗi ngay khi insert:
text-embedding-3-small: 1536 dimstext-embedding-3-large: 3072 dimsnomic-embed-text: 768 dimsall-MiniLM-L6-v2: 384 dims
Insert dữ liệu từ Python
import psycopg2
from openai import OpenAI
import json
client = OpenAI()
def get_embedding(text: str) -> list[float]:
response = client.embeddings.create(
model="text-embedding-3-small",
input=text
)
return response.data[0].embedding
def insert_document(conn, content: str, source: str, metadata: dict):
embedding = get_embedding(content)
with conn.cursor() as cur:
cur.execute(
"""
INSERT INTO documents (content, source, metadata, embedding)
VALUES (%s, %s, %s, %s)
""",
(content, source, json.dumps(metadata), embedding)
)
conn.commit()
# Kết nối
conn = psycopg2.connect(
host="localhost",
dbname="vectordb",
user="postgres",
password="secret"
)
insert_document(
conn,
content="pgvector cho phép lưu trữ vector embedding trực tiếp trong PostgreSQL",
source="docs/pgvector.md",
metadata={"chapter": 1, "topic": "overview"}
)
Tìm kiếm semantic search
Tìm 5 đoạn tài liệu liên quan nhất với query của user — dùng cosine distance (phù hợp nhất với text embedding):
def semantic_search(conn, query: str, top_k: int = 5) -> list[dict]:
query_embedding = get_embedding(query)
with conn.cursor() as cur:
cur.execute(
"""
SELECT
id,
content,
source,
metadata,
1 - (embedding <=> %s::vector) AS similarity
FROM documents
ORDER BY embedding <=> %s::vector
LIMIT %s
""",
(query_embedding, query_embedding, top_k)
)
rows = cur.fetchall()
return [
{
"id": row[0],
"content": row[1],
"source": row[2],
"metadata": row[3],
"similarity": float(row[4])
}
for row in rows
]
results = semantic_search(conn, "Cách cài đặt pgvector trên Ubuntu")
for r in results:
print(f"[{r['similarity']:.3f}] {r['content'][:100]}")
pgvector có 3 operator tính khoảng cách — chọn sai operator ảnh hưởng trực tiếp đến chất lượng kết quả:
<=>— cosine distance (dùng cho text embedding)<->— Euclidean / L2 distance<#>— negative inner product (dùng khi vector đã normalize)
Tạo index để tăng tốc tìm kiếm
Dưới 50,000 rows, sequential scan vẫn nhanh — latency thường dưới 10ms. Vượt ngưỡng đó mà không có index thì query chậm rõ rệt, nhất là khi nhiều request đồng thời.
HNSW index (khuyến nghị cho production — query nhanh hơn IVFFlat, không cần training trước):
-- HNSW index với cosine distance
CREATE INDEX ON documents
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
-- Tăng ef_search khi query để cân bằng accuracy vs speed
SET hnsw.ef_search = 100;
IVFFlat index (tốt khi dataset đã ổn định, ít insert mới):
-- Cần có data trước khi tạo IVFFlat
-- lists ~ sqrt(số rows), tối thiểu 100
CREATE INDEX ON documents
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);
-- Tăng probes khi query (mặc định 1, càng cao càng chính xác)
SET ivfflat.probes = 10;
Mình thường chọn HNSW cho production vì không cần biết trước số lượng data, và accuracy giữ ổn định khi dataset tăng dần. IVFFlat dành cho trường hợp dataset gần như cố định và cần tốc độ build index nhanh hơn.
Filter kết hợp metadata
Đây là lúc pgvector thực sự toả sáng so với vector database riêng — filter và JOIN bằng SQL như bình thường, không cần học API mới:
-- Tìm trong tài liệu thuộc chapter 1
SELECT content, 1 - (embedding <=> '[0.1, 0.2, ...]'::vector) AS sim
FROM documents
WHERE metadata->>'chapter' = '1'
ORDER BY embedding <=> '[0.1, 0.2, ...]'::vector
LIMIT 5;
-- Tìm và join với bảng khác
SELECT d.content, u.name AS author, sim
FROM (
SELECT id, content, 1 - (embedding <=> %s::vector) AS sim
FROM documents
ORDER BY embedding <=> %s::vector
LIMIT 10
) d
JOIN document_authors u ON d.id = u.document_id
WHERE sim > 0.7;
Kiểm tra và Monitoring
Kiểm tra index đang được dùng
-- Xem query plan — phải thấy "Index Scan" thay vì "Seq Scan"
EXPLAIN (ANALYZE, BUFFERS)
SELECT content
FROM documents
ORDER BY embedding <=> '[0.1, 0.2, ...]'::vector
LIMIT 5;
-- Xem thông tin index
SELECT
indexname,
pg_size_pretty(pg_relation_size(indexname::regclass)) AS index_size
FROM pg_indexes
WHERE tablename = 'documents';
Monitor performance
-- Kích thước bảng và index
SELECT
pg_size_pretty(pg_table_size('documents')) AS table_size,
pg_size_pretty(pg_indexes_size('documents')) AS indexes_size,
pg_size_pretty(pg_total_relation_size('documents')) AS total_size;
-- Query chậm (cần pg_stat_statements)
SELECT query, calls, mean_exec_time, total_exec_time
FROM pg_stat_statements
WHERE query LIKE '%embedding%'
ORDER BY mean_exec_time DESC
LIMIT 5;
Maintenance định kỳ
HNSW không cần rebuild. Riêng IVFFlat thì khác — sau nhiều delete/update, dead tuples tích tụ làm accuracy giảm dần. REINDEX định kỳ giải quyết vấn đề này:
-- Rebuild index (không lock bảng — dùng CONCURRENTLY)
REINDEX INDEX CONCURRENTLY documents_embedding_idx;
-- Vacuum để thu hồi dead tuples sau nhiều update
VACUUM ANALYZE documents;
Benchmark nhanh
import time
import random
def benchmark_search(conn, n_queries=100):
# Lấy ngẫu nhiên các embedding có sẵn để test
with conn.cursor() as cur:
cur.execute("SELECT embedding FROM documents ORDER BY random() LIMIT %s", (n_queries,))
sample_embeddings = [row[0] for row in cur.fetchall()]
start = time.time()
for emb in sample_embeddings:
with conn.cursor() as cur:
cur.execute(
"SELECT id FROM documents ORDER BY embedding <=> %s::vector LIMIT 5",
(emb,)
)
cur.fetchall()
elapsed = time.time() - start
print(f"{n_queries} queries in {elapsed:.2f}s")
print(f"Average: {elapsed/n_queries*1000:.1f}ms per query")
# HNSW trên ~100k rows thường đạt 5-20ms/query tùy ef_search
benchmark_search(conn)
Một vài lưu ý từ thực tế
Chạy pgvector production mấy tháng, đây là những gì mình ước mình biết sớm hơn:
- Chọn dimension phù hợp: Model embedding nhỏ hơn (384-768 dims) vừa nhanh hơn vừa tốn ít storage — 100k docs với 384 dims chỉ tốn khoảng 150MB. Chỉ lên 1536+ khi thực sự cần độ chính xác cao.
- HNSW vs IVFFlat: Dataset tăng liên tục thì chọn HNSW, không phải bàn. IVFFlat chỉ hợp khi data gần như cố định và cần build index nhanh hơn trong lần đầu.
- Backup không cần lo: pgvector data được
pg_dumpkéo theo bình thường — không cần tool riêng, không cần thêm bước nào vào quy trình backup hiện tại. - Connection pooling: pgbouncer phía trước hoạt động tốt với pgvector như mọi workload PostgreSQL khác — không có gì đặc biệt ở đây.
- Khi nào mới cần Qdrant hay Weaviate: Khi dataset vector vượt vài chục triệu records và cần horizontal sharding hoặc multi-tenancy chuyên biệt. Dưới 5 triệu records, pgvector hoàn toàn đủ dùng — và đơn giản hơn nhiều.
