· postgres / pgvector / vector-search
Cách thiết lập Vector Search với pgvector trong Postgres
Thêm semantic search vào Postgres với pgvector v0.8.2 — cài extension, tạo cột vector, sinh embeddings với OpenAI và build HNSW index. Ví dụ Node.js và Python.
Bởi Ethan · Cập nhật 29 tháng 5, 2026
1.920 từ · 10 phút đọc
pgvector cho phép bạn thêm semantic search vào Postgres mà không cần dựng thêm một vector store riêng. Nếu bạn đang dùng Postgres rồi, đây là con đường nhanh nhất để triển khai tìm kiếm dựa trên embedding — không cần tài khoản Pinecone, không có service mới phải theo dõi, không có thêm hóa đơn phát sinh.
Hướng dẫn này đi từ đầu đến một truy vấn semantic search hoàn chỉnh. Kết thúc, bạn sẽ có một bảng documents với cột vector 1,536 chiều, một HNSW index, và một hàm Node.js trả về những kết quả gần nhất với chuỗi truy vấn bằng OpenAI embeddings.
Dành cho ai
Bạn đã biết Postgres. Bạn đã nghe về embeddings và RAG nhưng chưa triển khai. Bạn muốn semantic search trong một app đang dùng Postgres — không phải lý do để chuyển sang database khác.
Nếu bạn xử lý hơn 100M vector với tốc độ ghi QPS cao, pgvector sẽ chạm trần. Đó là bài toán khác, không phải bài toán hướng dẫn này giải quyết.
Bước 1: Cài pgvector
Phiên bản ổn định hiện tại là v0.8.2 (ngày 25 tháng 2 năm 2026). Các phiên bản 0.8.0 và 0.8.1 có lỗi buffer overflow đã xác nhận trong quá trình build HNSW index song song. Dùng v0.8.2. Yêu cầu Postgres 13 trở lên.
Postgres tự host
# Ubuntu / Debian — thay 16 bằng major version Postgres của bạn
sudo apt-get install postgresql-16-pgvector
CREATE EXTENSION IF NOT EXISTS vector;
Các cách cài khác: Docker image pgvector/pgvector đã có extension được tích hợp sẵn; Homebrew (brew install pgvector), PGXN, conda-forge và Postgres.app đều hoạt động được.
Supabase
pgvector đã được cài sẵn. Bật từ Dashboard (Database → Extensions → vector) hoặc bằng SQL:
CREATE EXTENSION vector WITH SCHEMA extensions;
Một lưu ý riêng cho Supabase: PostgREST — lớp REST mà Supabase sử dụng — không hỗ trợ trực tiếp các toán tử khoảng cách của pgvector. Hãy bọc các truy vấn tương đồng trong Postgres functions và gọi qua rpc() từ thư viện client. Supabase trên production còn nhiều bẫy khác — xem Supabase RLS pitfalls thực tế trước khi expose kết quả query cho client.
Supabase có gói miễn phí hào phóng và là lựa chọn hosted ít ma sát nhất nếu bạn chưa dùng provider nào khác.
Neon
CREATE EXTENSION IF NOT EXISTS vector;
Neon cập nhật phiên bản pgvector mới nhất và một phiên bản trước đó. Mô hình serverless (scale to zero) phù hợp tốt với workload có lưu lượng không đều hoặc thấp. Lưu ý với Neon: giá trị maintenance_work_mem mặc định thay đổi theo kích thước compute. Đặt nó ở mức 50–60% RAM khả dụng trước khi build HNSW index lớn, nếu không quá trình build sẽ chậm đáng kể.
Railway và Render
Cả hai hỗ trợ pgvector trên Postgres được quản lý:
CREATE EXTENSION IF NOT EXISTS vector;
Railway cung cấp template pgvector được bật sẵn. Railway hỗ trợ bật pgvector trực tiếp — dùng lệnh CREATE EXTENSION ở trên. Render bật trực tiếp — không cần migration.
Bước 2: Tạo bảng với cột vector
CREATE TABLE documents (
id BIGSERIAL PRIMARY KEY,
content TEXT NOT NULL,
embedding VECTOR(1536) -- khớp với output mặc định của text-embedding-3-small
);
VECTOR(n) lưu một mảng float4 có độ dài cố định. Với HNSW indexing, số chiều tối đa là 2,000 cho kiểu vector và 4,000 cho halfvec. text-embedding-3-small của OpenAI xuất ra 1,536 chiều theo mặc định — nằm trong giới hạn.
Nếu dùng text-embedding-3-large (3,072 chiều theo mặc định), chuyển sang HALFVEC(3072) hoặc truyền dimensions=1536 vào API để giảm kích thước.
Bước 3: Sinh embeddings
Bạn cần có vector trước khi insert hoặc query. Hướng dẫn này dùng text-embedding-3-small của OpenAI: 1,536 chiều, $0.02 mỗi triệu input token, tối đa 8,192 input token mỗi request.
Node.js
import OpenAI from "openai";
const openai = new OpenAI(); // đọc OPENAI_API_KEY từ env
async function embed(text) {
const response = await openai.embeddings.create({
model: "text-embedding-3-small",
input: text,
// dimensions: 512 // tùy chọn: embedding ngắn hơn đánh đổi một ít recall để tiết kiệm bộ nhớ
});
return response.data[0].embedding; // mảng float, độ dài 1536
}
Python
from openai import OpenAI
client = OpenAI() # đọc OPENAI_API_KEY từ env
def embed(text: str) -> list[float]:
response = client.embeddings.create(
model="text-embedding-3-small",
input=text,
# dimensions=512 # tùy chọn
)
return response.data[0].embedding # list 1536 float
Lỗi thường gặp: truyền chuỗi rỗng sẽ trả về zero vector. pgvector bỏ qua zero vector khi build cosine index, nên những dòng đó sẽ không xuất hiện trong kết quả tương đồng. Hãy lọc bỏ chuỗi rỗng trước khi tạo embedding.
Bước 4: Insert vector
Sau khi có embedding là mảng float, insert nó cùng với văn bản gốc.
Node.js (dùng pg)
import { Pool } from "pg";
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
async function insertDocument(content) {
const vector = await embed(content);
await pool.query(
"INSERT INTO documents (content, embedding) VALUES ($1, $2)",
[content, JSON.stringify(vector)]
);
}
Python (dùng psycopg2)
import os
import psycopg2
conn = psycopg2.connect(os.environ["DATABASE_URL"])
cur = conn.cursor()
def insert_document(content: str):
vector = embed(content)
cur.execute(
"INSERT INTO documents (content, embedding) VALUES (%s, %s)",
(content, vector)
)
conn.commit()
Driver Node pg nhận vector dưới dạng mảng JSON-stringified. Driver Python psycopg2 nhận trực tiếp list Python — adapter của pgvector xử lý việc cast.
Bước 5: Query bằng cosine similarity
Toán tử <=> tính cosine distance — giá trị càng thấp thì càng tương đồng. Toán tử <-> tính L2 (Euclidean) distance.
Với text embeddings, cosine hầu như luôn là lựa chọn đúng. Nó đo góc, không đo độ lớn, nên độ dài vector không làm lệch kết quả.
-- Trả về 10 tài liệu tương đồng nhất với một query embedding
SELECT
id,
content,
1 - (embedding <=> $1::vector) AS similarity
FROM documents
ORDER BY embedding <=> $1::vector
LIMIT 10;
1 - distance chuyển cosine distance thành similarity (1 = giống hệt, 0 = vuông góc). ORDER BY dùng distance thô — đây chỉ là điều chỉnh hiển thị.
Lỗi thường gặp: bọc cột embedding trong một function hoặc cast trong mệnh đề ORDER BY sẽ làm mất tác dụng của index. Query planner sẽ không dùng HNSW index nếu bạn viết ORDER BY normalize(embedding) <=> $1. Hãy để nguyên cột.
Bước 6: Thêm HNSW index để mở rộng quy mô
Không có index, mọi query đều quét toàn bộ bảng. Thời gian query tăng tuyến tính theo số hàng — ổn với vài nghìn hàng, nhưng là điểm nghẽn trong production. HNSW index thay thế sequential scan bằng graph traversal gần đúng, giữ lookup trong khoảng vài millisecond khi bảng phình to.
CREATE INDEX ON documents
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
Các tham số điều chỉnh:
m(mặc định 16): số kết nối mỗi node trong HNSW graph. Giá trị cao hơn cho recall tốt hơn nhưng dùng nhiều bộ nhớ hơn. Không tăng quá 32 trừ khi recall đo được thấp hơn mục tiêu.ef_construction(mặc định 64): phạm vi tìm kiếm trong quá trình build index. Dùng 256–512 cho các use case đòi hỏi độ chính xác cao; lợi ích giảm dần sau ~128 và thời gian build tăng.hnsw.ef_search(mặc định 40, đặt theo session): phạm vi tìm kiếm lúc query. Tăng lên 100+ khi recall quan trọng hơn latency.
-- Điều chỉnh recall vs. latency lúc query
SET hnsw.ef_search = 100;
HNSW mất nhiều thời gian build hơn IVFFlat và dùng nhiều bộ nhớ trong quá trình xây dựng. Với dữ liệu ghi một lần và query nhiều, đó là chi phí trả một lần. Với ghi real-time nặng, cần theo dõi insert latency — index cập nhật theo mỗi hàng.
HNSW index phải vừa với bộ nhớ. Nếu bị đẩy ra khỏi shared_buffers bởi các query khác, latency sẽ tăng vọt. Đảm bảo index vừa thoải mái trong bộ nhớ trước khi đưa lên production.
Xử lý NULL và zero vector: các hàng có embedding NULL bị index bỏ qua. Các hàng có zero vector bị bỏ qua với cosine distance. Dùng partial index nếu muốn đảm bảo độ bao phủ:
CREATE INDEX ON documents
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64)
WHERE embedding IS NOT NULL;
Bước 7: Query có lọc kết hợp với iterative scan
Khi kết hợp mệnh đề ORDER BY distance với bộ lọc WHERE, HNSW index có thể không tìm đủ ứng viên trong lần quét đầu và trả về ít kết quả hơn LIMIT của bạn. Đây là lỗi thường gặp nhất trong production với pgvector.
Cách khắc phục: bật iterative scan (ra mắt từ v0.8.0):
SET hnsw.iterative_scan = relaxed_order;
Với relaxed_order, pgvector tiếp tục quét thêm nếu lần đầu không tìm đủ kết quả phù hợp. Nó đánh đổi thứ tự distance chặt chẽ lấy sự đầy đủ về kết quả. Với hầu hết các use case tìm kiếm ngữ nghĩa có lọc, sự đầy đủ quan trọng hơn thứ tự chính xác.
Bước 8: Ví dụ hoàn chỉnh — tìm kiếm tài liệu theo ngữ nghĩa
Đây là module Node.js kết hợp tất cả các bước trên.
import OpenAI from "openai";
import { Pool } from "pg";
const openai = new OpenAI();
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
async function embed(text) {
const res = await openai.embeddings.create({
model: "text-embedding-3-small",
input: text,
});
return res.data[0].embedding;
}
async function addDocument(content) {
if (!content.trim()) {
throw new Error("Empty content produces a zero vector — skipped by cosine index");
}
const vector = await embed(content);
await pool.query(
"INSERT INTO documents (content, embedding) VALUES ($1, $2)",
[content, JSON.stringify(vector)]
);
}
async function search(query, limit = 5) {
const vector = await embed(query);
const { rows } = await pool.query(
`SELECT id, content, 1 - (embedding <=> $1::vector) AS similarity
FROM documents
ORDER BY embedding <=> $1::vector
LIMIT $2`,
[JSON.stringify(vector), limit]
);
return rows;
}
// Ví dụ
await addDocument("pgvector adds vector similarity search to Postgres");
await addDocument("HNSW indexes trade memory for fast approximate nearest neighbor queries");
const results = await search("how does pgvector handle indexing?");
console.log(results);
// [
// { id: 2, content: 'HNSW indexes trade memory...', similarity: 0.94 },
// { id: 1, content: 'pgvector adds vector...', similarity: 0.81 }
// ]
Giới hạn đã biết
- Prisma: chưa có hỗ trợ pgvector native tính đến cuối 2025. Dùng raw SQL hoặc Drizzle ORM, vốn có extension pgvector.
- text-embedding-3-large (3,072 chiều): vượt giới hạn 2,000 chiều của HNSW với kiểu
vector. DùngHALFVEC(3072)hoặc yêu cầudimensions=1536từ API. - Hơn 10M vector với QPS ghi cao: các vector database chuyên dụng (Pinecone, Qdrant, Weaviate) có sharding được thiết kế cho mục đích này. pgvector thì không. Với hầu hết workload RAG — từ hàng trăm nghìn đến vài triệu tài liệu — giới hạn này không đáng lo.
Tham khảo
- pgvector README — toán tử, loại index, giới hạn số chiều
- pgvector CHANGELOG — chi tiết bản vá bảo mật v0.8.2
- Supabase: hướng dẫn vector columns
- Neon: hướng dẫn pgvector
- Railway: bài viết về pgvector
- Render: các extension được hỗ trợ
- OpenAI: thông báo text-embedding-3-small
- Instaclustr: benchmark HNSW vs IVFFlat