2h sáng, PagerDuty réo dồn dập. Node.js API đồng loạt trả về mã 504 Gateway Timeout. Cùng lúc đó, cụm PostgreSQL production chạm trần 100% CPU. Mở pg_stat_activity và Grafana kiểm tra, nguyên nhân lộ rõ ngay lập tức. Câu lệnh tìm kiếm sản phẩm tương tự đang chạy sequential scan qua hơn 450.000 dòng vector mà không có index. Latency vọt từ 45ms lên hơn 8.000ms mỗi request.
Nếu team bạn đang dựng Semantic Search hay RAG (Retrieval-Augmented Generation) mà chưa muốn chi hàng nghìn USD mỗi tháng cho Pinecone hay Milvus, pgvector là lựa chọn thực tế nhất. PostgreSQL vẫn đủ sức gánh hàng triệu vector nếu bạn cấu hình chuẩn. Dưới đây là cách team mình cứu hệ thống trong đêm và chuẩn hóa pipeline từ đầu.
1. Quick start: Dựng môi trường test trong 5 phút
Lúc sự cố đang xảy ra, đừng tốn thời gian tự biên dịch extension từ source. Cách nhanh nhất để dựng cụm staging hoặc kiểm thử cục bộ là dùng Docker image chính thức từ pgvector team.
Khởi động PostgreSQL với pgvector bằng Docker
# Chạy container postgres kèm pgvector
docker run -d \
--name postgres-vector \
-e POSTGRES_USER=appuser \
-e POSTGRES_PASSWORD=secretpassword \
-e POSTGRES_DB=vector_db \
-p 5432:5432 \
pgvector/pgvector:pg16
Kích hoạt extension và tạo schema
Kết nối vào database và bật extension. Lưu ý: kiểu dữ liệu vector(n) bắt buộc phải khớp chính xác số chiều của model embedding bạn dùng. Ví dụ: OpenAI text-embedding-3-small dùng 1.536 chiều, còn các local model như all-MiniLM-L6-v2 chỉ dùng 384 chiều.
-- Bật extension pgvector
CREATE EXTENSION IF NOT EXISTS vector;
-- Tạo bảng tài liệu mẫu với vector 1536 chiều
CREATE TABLE documents (
id SERIAL PRIMARY KEY,
content TEXT NOT NULL,
embedding vector(1536)
);
Chèn dữ liệu và truy vấn cosine similarity
-- Chèn dữ liệu thử nghiệm
INSERT INTO documents (content, embedding)
VALUES ('Sửa lỗi timeout nodejs postgres', '[0.012, -0.023, 0.045, ...]'); -- vector 1536 chiều
-- Lấy top 5 tài liệu gần nhất theo Cosine Distance (<=>)
SELECT id, content, 1 - (embedding <=> '[0.010, -0.020, 0.040, ...]') AS similarity
FROM documents
ORDER BY embedding <=> '[0.010, -0.020, 0.040, ...]' ASC
LIMIT 5;
2. Cách thức hoạt động của pgvector
pgvector bổ sung kiểu dữ liệu vector vào PostgreSQL. Đi kèm với đó là các toán tử hình học để tính khoảng cách giữa các điểm trong không gian nhiều chiều.
3 toán tử đo khoảng cách cốt lõi
- L2 distance (Euclidean Distance)
<->: Đo khoảng cách đường thẳng giữa 2 vector. Phù hợp khi độ lớn tuyệt đối của vector mang thông tin quan trọng (thường gặp trong xử lý ảnh). - Negative Inner Product
<#>: Tích vô hướng đổi dấu. Do PostgreSQL sắp xếp theo thứ tựASC, toán tử này trả về số âm để vector có độ tương đồng cao nhất nằm ở đầu danh sách. Tốc độ tính toán nhanh nhất nếu vector đã được chuẩn hóa (normalized L2 norm = 1). - Cosine Distance
<=>: Đo góc giữa 2 vector, công thức là1 - cosine_similarity. Độ dài văn bản không làm lệch ngữ nghĩa. Đây là lựa chọn tiêu chuẩn cho xử lý ngôn ngữ tự nhiên.
Tích hợp vào backend Node.js
Driver pg tiêu chuẩn của Node.js tương thích trực tiếp với pgvector. Bạn chỉ cần serialize mảng số thực float thành chuỗi định dạng PostgreSQL:
import pg from 'pg';
import OpenAI from 'openai';
const { Pool } = pg;
const pool = new Pool({
connectionString: process.env.DATABASE_URL || 'postgresql://appuser:secretpassword@localhost:5432/vector_db',
max: 20,
idleTimeoutMillis: 30000,
});
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
async function semanticSearch(userQuery, limit = 5) {
// 1. Tạo embedding từ câu query của người dùng
const embeddingResponse = await openai.embeddings.create({
model: 'text-embedding-3-small',
input: userQuery,
});
const queryVector = embeddingResponse.data[0].embedding;
// 2. Chuyển mảng thành chuỗi '[x1,x2,x3,...]'
const vectorString = JSON.stringify(queryVector);
// 3. Truy vấn top documents theo cosine distance
const query = `
SELECT id, content, 1 - (embedding <=> $1::vector) AS similarity
FROM documents
ORDER BY embedding <=> $1::vector ASC
LIMIT $2;
`;
const client = await pool.connect();
try {
const res = await client.query(query, [vectorString, limit]);
return res.rows;
} finally {
client.release();
}
}
// Ví dụ chạy thử
semanticSearch('làm thế nào scale database postgresql')
.then(results => console.log('Kết quả tìm kiếm:', results))
.catch(err => console.error('Lỗi truy vấn:', err));
3. Đánh index: Chìa khóa cứu production khỏi quá tải
Tại sao bảng 450.000 dòng lại khiến database quá tải? Mặc định khi chưa có index, câu lệnh ORDER BY embedding <=> ... buộc database chạy Exact Nearest Neighbor (KNN). Điều đó nghĩa là với 50 concurrent requests, PostgreSQL phải tính khoảng cách 22,5 triệu lần mỗi giây.
Để kéo độ trễ từ vài giây xuống dưới 25ms, bạn phải chuyển sang Approximate Nearest Neighbor (ANN) bằng index.
IVFFlat hay HNSW?
- IVFFlat (Inverted File Flat): Chia không gian thành các cụm (Voronoi cells). Tạo index nhanh, tốn ít RAM. Nhược điểm lớn: bạn phải chờ bảng có đủ dữ liệu mới tạo index hiệu quả, và độ chính xác (recall) giảm mạnh nếu dữ liệu phân bố lệch.
- HNSW (Hierarchical Navigable Small World): Xây dựng đồ thị đa tầng. Build tốn thời gian hơn và ngốn RAM gấp 1.5 – 2 lần IVFFlat. Đổi lại, tốc độ truy vấn vượt trội và recall duy trì ổn định ở mức 95% – 99%. Với hệ thống production hiện nay, HNSW luôn là ưu tiên hàng đầu.
Cấu hình HNSW Index chuẩn
-- Cấp thêm RAM tạm thời để build index nhanh hơn trong session hiện tại
SET maintenance_work_mem = '2GB';
-- Tạo HNSW index cho phép toán Cosine Distance
CREATE INDEX documents_embedding_hnsw_idx
ON documents
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
Ý nghĩa các thông số:
m = 16: Số lượng liên kết tối đa trên mỗi node đồ thị. Tăng lên 24 hoặc 32 nếu bạn cần recall cao hơn ở tập dữ liệu lớn.ef_construction = 64: Kích thước queue duyệt khi xây dựng index. Giá trị càng cao, đồ thị càng chính xác nhưng thời gian build càng lâu.hnsw.ef_search: Tham số lúc query. Mặc định là 40. Nếu cần query nhanh hơn nữa, hạ xuống 20; nếu cần độ chính xác cao hơn, tăng lên 100 bằng lệnhSET hnsw.ef_search = 80;.
4. 4 kinh nghiệm thực chiến sau sự cố
Đưa hệ thống vượt qua sự cố là một chuyện, giữ cho nó chạy ổn định lâu dài lại là bài toán khác. Đây là những điểm mấu chốt bạn cần lưu tâm:
1. Đo đạc bằng Shadow Traffic trước khi switch
Đừng chuyển đổi từ Elasticsearch hay search ILIKE sang pgvector trong một lần deploy duy nhất. Hãy cho Node.js gửi query song song (shadow traffic) sang pgvector trong ít nhất 3 ngày. Ghi nhận p95/p99 latency và đo chỉ số Mean Reciprocal Rank (MRR) để đảm bảo chất lượng tìm kiếm không bị suy giảm.
2. Kiểm soát Connection Pool và RAM
HNSW index phải nằm trọn trong RAM (shared_buffers và OS cache) thì query mới đạt tốc độ dưới 20ms. Một index HNSW cho 500.000 vector 1.536 chiều chiếm khoảng 1.6GB đến 2GB RAM. Đặt max connection của Node.js trong khoảng 15-25, hoặc dùng PgBouncer ở transaction mode. Mở quá nhiều connection sẽ khiến PostgreSQL cạn kiệt bộ nhớ và kích hoạt Linux OOM Killer.
3. Tách riêng bảng chứa vector
Vector 1.536 chiều chiếm khoảng 6KB mỗi dòng. Nếu bạn nhét vector chung bảng với các cột TEXT dài hoặc JSONB, bảng sẽ phình to rất nhanh. Hậu quả là cache page của PostgreSQL bị loãng nghiêm trọng. Hãy tách riêng bảng vector:
-- Bảng nghiệp vụ thông thường
CREATE TABLE articles (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
content TEXT NOT NULL
);
-- Bảng chuyên lưu vector, join khi có kết quả search
CREATE TABLE article_embeddings (
article_id INT PRIMARY KEY REFERENCES articles(id) ON DELETE CASCADE,
embedding vector(1536) NOT NULL
);
4. Dùng Hybrid Search kết hợp RRF
Semantic Search hiểu ý nghĩa câu nói rất tốt, nhưng lại thường trả kết quả trượt khi người dùng tìm mã lỗi cụ thể (như ECONNRESET) hoặc mã sản phẩm (SKU-8821). Bạn nên kết hợp Full-Text Search (dùng tsvector có sẵn của Postgres) cùng pgvector. Dùng thuật toán Reciprocal Rank Fusion (RRF) để gộp điểm 2 bên lại. Kết quả trả về sẽ vừa đúng ngữ nghĩa, vừa khớp chính xác từ khóa.

