Nỗi bất lực của tìm kiếm từ khóa truyền thống
Gõ "món gì ăn ấm bụng ngày mưa" trên ứng dụng giao hàng và nhận về dòng chữ "0 kết quả tìm thấy" — một trải nghiệm người dùng rất quen thuộc. Các kỹ thuật kinh điển như LIKE '%ấm bụng%' hay Postgres Full-Text Search (tsvector) chỉ chăm chăm so khớp bề mặt chữ cái. Quán ăn gắn nhãn "phở bò sốt vang" hay "cháo gà cay nóng" sẽ bị bỏ qua hoàn toàn vì không trùng từ khóa.
Để giải quyết tìm kiếm ngữ nghĩa (semantic search), phản xạ quen thuộc của nhiều đội ngũ là dựng ngay cụm vector database chuyên biệt như Pinecone, Weaviate hay Qdrant. Giải pháp này hiệu quả nhưng đổi lại chi phí không rẻ: ít nhất 70$ – 100$/tháng cho một cụm cụ thể, kèm theo bài toán CDC (Change Data Capture) đồng bộ hai chiều giữa DB chính và vector DB. Chưa kể rủi ro lệch dữ liệu và tăng thêm một điểm lỗi (single point of failure) trong hệ thống.
Hầu hết dự án web hiện nay đều đã chạy PostgreSQL. Vậy tại sao phải gánh thêm cụm hạ tầng mới khi cơ sở dữ liệu hiện tại hoàn toàn có thể đảm đương tốt?
pgvector chính là câu trả lời. Tiện ích mở rộng (extension) nguồn mở viết bằng C này biến PostgreSQL thành một vector database thực thụ, cho phép bạn lưu trữ và tính toán vector embeddings ngay trong bảng dữ liệu quen thuộc cùng với users hay orders.
Khái niệm cốt lõi: Vector Embeddings và Semantic Search
Vector Embeddings là gì?
Hãy hình dung bạn chấm điểm tính cách của một người trên thang đo từ 0 đến 1: mức độ hướng ngoại, sự kiên nhẫn và khiếu hài hước. Một người có thể được biểu diễn ngắn gọn bằng tọa độ [0.85, 0.20, 0.95].
Vector Embedding trong AI hoạt động y như vậy. Thay vì chấm điểm 3 tiêu chí, mô hình ngôn ngữ (như OpenAI text-embedding-3-small) chuyển đổi văn bản thành một chuỗi 1.536 con số thực. Chuỗi số này định vị ý nghĩa của đoạn văn trong không gian toán học nhiều chiều:
- Hai câu "Con mèo đang sưởi nắng" và "Chú miêu nằm ngủ ngoài trời" mang nghĩa tương đồng, tọa độ của chúng sẽ nằm sát nhau.
- Câu "Tỷ giá USD hôm nay lao dốc" mang ngữ nghĩa khác hẳn, tọa độ sẽ bị đẩy ra rất xa.
Semantic Search vận hành thế nào?
Thay vì đếm số chữ trùng lặp, tìm kiếm ngữ nghĩa tính khoảng cách hình học giữa vector câu hỏi và vector của từng tài liệu đã lưu. Hai thước đo phổ biến nhất gồm:
- Cosine Distance (Khoảng cách Cosine): Đo góc lệch giữa hai vector mà không phụ thuộc vào độ dài văn bản. Đây là lựa chọn tối ưu cho hầu hết mô hình embedding văn bản hiện nay.
- L2 / Euclidean Distance: Đo độ dài đường thẳng nối giữa hai điểm trong không gian.
pgvector xử lý bài toán này ra sao?
pgvector bổ sung kiểu dữ liệu vector(n) (với n tối đa lên tới 16.000 chiều trong các phiên bản mới) cùng hai thuật toán lập chỉ mục (index) vector phổ biến:
- IVFFlat: Chia vector thành các cụm (clusters) bằng thuật toán k-means. Thời gian build index nhanh, tốn rất ít RAM, nhưng yêu cầu bảng phải có sẵn dữ liệu để huấn luyện cụm và độ chính xác (recall) sẽ giảm khi tập dữ liệu biến động nhiều.
- HNSW (Hierarchical Navigable Small World): Xây dựng đồ thị đa tầng liên kết các vector lân cận. HNSW cho tốc độ truy vấn vượt trội và độ chính xác đạt trên 95-98%. Điểm đánh đổi duy nhất là tốn nhiều RAM hơn (khoảng 1.5 GB – 2 GB RAM cho 1 triệu vector 1.536 chiều).
Thực hành: Xây dựng hệ thống Semantic Search với PostgreSQL
Bước 1: Khởi tạo PostgreSQL có sẵn pgvector
Cách nhanh nhất là dùng Docker image chính thức từ nhóm phát triển pgvector:
# Chạy PostgreSQL 16 tích hợp sẵn extension pgvector
docker run -d \
--name pgvector-demo \
-e POSTGRES_USER=postgres \
-e POSTGRES_PASSWORD=mysecretpassword \
-e POSTGRES_DB=knowledge_base \
-p 5432:5432 \
pgvector/pgvector:pg16
Bước 2: Bật extension và tạo schema
Kết nối vào database bằng psql hoặc TablePlus rồi thực thi câu lệnh:
-- Bật extension vector
CREATE EXTENSION IF NOT EXISTS vector;
-- Tạo bảng documents lưu vector 1536 chiều
CREATE TABLE documents (
id BIGSERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
content TEXT NOT NULL,
category VARCHAR(50) NOT NULL,
embedding vector(1536),
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
Bước 3: Tạo chỉ mục tìm kiếm HNSW
Không có index, Postgres buộc phải quét toàn bộ bảng (sequential scan), khiến thời gian truy vấn tăng tuyến tính theo số dòng. Với chỉ mục HNSW và toán tử khoảng cách Cosine vector_cosine_ops, truy vấn sẽ được tối ưu đáng kể:
CREATE INDEX idx_documents_embedding_hnsw
ON documents
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
Thông số m = 16 xác định số lượng liên kết tối đa của mỗi node trên đồ thị, còn ef_construction = 64 quyết định độ phủ tìm kiếm khi xây dựng index. Bộ thông số này đã đủ tốt cho phần lớn ứng dụng sản phẩm.
Bước 4: Pipeline Python: Nhúng, Lưu và Tìm kiếm
Đoạn script dưới đây minh họa luồng end-to-end: tạo embedding từ OpenAI, lưu vào database và thực hiện tìm kiếm ngữ nghĩa:
import psycopg2
from openai import OpenAI
client = OpenAI(api_key="sk-your-openai-api-key")
def get_embedding(text: str) -> list[float]:
response = client.embeddings.create(
input=text,
model="text-embedding-3-small"
)
return response.data[0].embedding
conn = psycopg2.connect(
dbname="knowledge_base",
user="postgres",
password="mysecretpassword",
host="localhost",
port="5432"
)
cursor = conn.cursor()
# 1. Nạp dữ liệu mẫu
sample_data = [
("Chính sách đổi trả", "Khách hàng được hoàn tiền 100% trong vòng 30 ngày nếu giữ nguyên bao bì.", "policy"),
("Điều khoản bảo hành", "Thiết bị được hỗ trợ 1 đổi 1 trong 12 tháng nếu phát sinh lỗi phần cứng từ nhà sản xuất.", "warranty"),
("Gợi ý ẩm thực", "Nồi lẩu kim chi hải sản chua cay nóng hổi là lựa chọn số một cho buổi tối se lạnh.", "lifestyle")
]
for title, content, cat in sample_data:
emb = get_embedding(content)
cursor.execute(
"INSERT INTO documents (title, content, category, embedding) VALUES (%s, %s, %s, %s)",
(title, content, cat, emb)
)
conn.commit()
# 2. Truy vấn ngữ nghĩa
query_text = "Hàng bị lỗi muốn lấy lại tiền thì làm thế nào?"
query_vector = get_embedding(query_text)
# Tinh chỉnh độ chính xác tìm kiếm của HNSW (mặc định là 40)
cursor.execute("SET hnsw.ef_search = 100;")
# Toán tử <=> tính Cosine distance: giá trị càng nhỏ càng tương đồng
cursor.execute("""
SELECT id, title, content, (embedding <=> %s::vector) AS distance
FROM documents
ORDER BY distance ASC
LIMIT 2;
""", (query_vector,))
for row in cursor.fetchall():
print(f"[{row[1]}] Khoảng cách: {row[3]:.4f}")
print(f"Nội dung: {row[2]}\n")
cursor.close()
conn.close()
Sức mạnh cốt lõi: Metadata Filtering
Trong ví dụ trên, toán tử <=> trả về Cosine distance nằm trong khoảng [0, 2] (tương ứng với công thức 1 - cosine_similarity). Điểm số càng tiệm cận 0, mức độ trùng khớp ngữ nghĩa càng cao. Tài liệu "Chính sách đổi trả" được xếp đầu tiên dù câu hỏi không hề chứa từ "30 ngày" hay "bao bì".
Nhưng lợi thế thực sự của pgvector nằm ở khả năng kết hợp điều kiện quan hệ với tìm kiếm vector ngay trong một câu lệnh (hybrid filtering):
-- Tìm kiếm vector kết hợp lọc theo danh mục và thời gian tạo
SELECT title, content
FROM documents
WHERE category = 'policy'
AND created_at >= '2024-01-01'
ORDER BY embedding <=> '[0.012, -0.045, ...]'::vector
LIMIT 5;
Nếu dùng vector database rời, bạn thường phải truy vấn top 100 ID từ vector DB, sau đó quay lại Postgres để lọc tenant_id hay is_deleted. Khi tập dữ liệu bị lọc quá nhiều, kết quả trả về có thể rỗng hoặc thiếu hụt. Với pgvector, Postgres Optimizer tự động tính toán execution plan phù hợp nhất, xử lý bộ lọc trước hoặc sau khi quét index.
Khi nào nên dùng pgvector?
pgvector không sinh ra để thay thế hoàn toàn Milvus hay Pinecone trong mọi tình huống. Tuy nhiên, nó là điểm xuất phát lý tưởng:
- Tập dữ liệu dưới 5 triệu vector: Với cấu hình phần cứng tiêu chuẩn (4–8 vCPU, 16–32 GB RAM), pgvector xử lý truy vấn HNSW với độ trễ dưới 15ms rất mượt mà.
- Cần đảm bảo ACID và bảo mật: Tận dụng sẵn Row-Level Security (RLS), ACID transactions và cơ chế backup/restore hiện có của Postgres.
- Đội ngũ tinh gọn: Giảm thiểu một dịch vụ bên ngoài, tiết kiệm cả ngân sách hạ tầng lẫn chi phí bảo trì.
Hãy bắt đầu với pgvector trước khi quyết định mở rộng sang các hệ thống phức tạp hơn. Đơn giản hóa kiến trúc luôn là chiến lược tốt nhất cho giai đoạn đầu của sản phẩm.

