Hướng dẫn PostgreSQL pgvector: Lưu trữ Embedding và Xây dựng Tìm kiếm Ngữ nghĩa Tốc độ cao

Database tutorial - IT technology blog
Database tutorial - IT technology blog

Vào lúc 2:15 sáng tháng trước, cảnh báo PagerDuty on-call của chúng tôi reo lên. Cụm cơ sở dữ liệu vector độc lập bắt đầu trả về lỗi 504 gateway timeout khi lưu lượng truy cập bất ngờ tăng vọt lên 4.000 RPM.

Tệ hơn nữa, luồng đồng bộ CDC bất đồng bộ bị trễ tới 45 phút, khiến hàng nghìn người dùng nhận về kết quả tìm kiếm cũ và sai lệch. Ngay trong sprint tiếp theo, chúng tôi đã dỡ bỏ hoàn toàn kiến trúc hai cơ sở dữ liệu cồng kềnh đó. Bằng việc chuyển toàn bộ các thao tác vector trực tiếp vào PostgreSQL với extension pgvector, chúng tôi đã loại bỏ vĩnh viễn vấn đề trễ đồng bộ dữ liệu.

Bắt đầu nhanh (5 phút): Lưu trữ và Truy vấn Embedding

Để chạy các truy vấn vector similarity, hãy kích hoạt extension và gán kiểu dữ liệu vector cho cột của bạn. Đảm bảo bạn khai báo chính xác số chiều (dimensions) do mô hình embedding tạo ra—chẳng hạn như 1536 đối với text-embedding-3-small của OpenAI hoặc 384 đối với các mô hình Hugging Face gọn nhẹ như all-MiniLM-L6-v2.

1. Kích hoạt Extension và Tạo Bảng

-- Kích hoạt extension pgvector
CREATE EXTENSION IF NOT EXISTS vector;

-- Tạo bảng lưu trữ tài liệu với vector embedding 1536 chiều
CREATE TABLE documents (
    id BIGSERIAL PRIMARY KEY,
    title TEXT NOT NULL,
    content TEXT NOT NULL,
    category VARCHAR(50),
    embedding vector(1536),
    created_at TIMESTAMPTZ DEFAULT NOW()
);

2. Chèn Dữ liệu Vector

-- Chèn bản ghi kèm vector embedding tương ứng
INSERT INTO documents (title, content, category, embedding)
VALUES (
    'Postgres Connection Pooling',
    'Tìm hiểu chuyên sâu về cách quản lý quá tải kết nối bằng PgBouncer.',
    'database',
    '[-0.0023, 0.0142, -0.0311, ...]' -- 1536 giá trị float
);

3. Thực hiện Tìm kiếm Láng giềng Gần nhất (Nearest Neighbor)

-- Lấy top 5 tài liệu liên quan nhất về mặt ngữ nghĩa bằng Cosine Similarity
SELECT 
    id,
    title,
    1 - (embedding <=> '[-0.0023, 0.0142, -0.0311, ...]') AS similarity_score
FROM documents
ORDER BY embedding <=> '[-0.0023, 0.0142, -0.0311, ...]'
LIMIT 5;

Phân tích Chuyên sâu: Phép toán Vector và Lựa chọn Index

Việc vận hành kiến trúc hai cơ sở dữ liệu song song—PostgreSQL cho dữ liệu quan hệ và Pinecone, Qdrant hoặc Milvus cho vector—tạo ra rất nhiều trở ngại trong vận hành. Bạn phải trả gấp đôi chi phí hạ tầng, mất đi tính nhất quán giao dịch (transactional consistency) và tăng thêm độ trễ mạng (network hops). Với pgvector, bạn vẫn giữ trọn tính tuân thủ ACID, dễ dàng join embedding trực tiếp với các bảng dữ liệu khách hàng và ràng buộc toàn vẹn khóa ngoại nghiêm ngặt.

Hiểu rõ các Toán tử Khoảng cách (Distance Operators)

Hãy chọn toán tử phù hợp dựa trên cách mô hình embedding của bạn tính toán khoảng cách:

  • <=> Cosine Distance: Tính khoảng cách góc bất kể độ dài (magnitude) của vector. Đây là lựa chọn chuẩn xác cho OpenAI, Cohere và hầu hết các mô hình text embedding hiện đại.
  • <-> Euclidean / L2 Distance: Đo khoảng cách hình học trực tiếp. Thường dùng trong các mô hình computer vision và phân loại âm thanh.
  • <#> Negative Inner Product: Tính giá trị tích vô hướng âm (negative dot product). Dùng toán tử này cho các unit vector đã được chuẩn hóa (pre-normalized) khi tích vô hướng tương đương với cosine similarity, giúp tiết kiệm chu kỳ xử lý của CPU.

Lựa chọn Index Phù hợp: HNSW và IVFFlat

Quét chính xác láng giềng gần nhất (sequential scans) hoạt động ổn định khi dữ liệu dưới 10.000 dòng. Vượt qua ngưỡng này, độ trễ truy vấn sẽ tăng vọt từ 15ms lên nhiều giây. Môi trường production đòi hỏi phải có index Approximate Nearest Neighbor (ANN).

-- 1. HNSW Index (Hierarchical Navigable Small World) - Khuyên dùng cho Production
CREATE INDEX idx_documents_embedding_hnsw ON documents 
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);

-- 2. IVFFlat Index (Inverted File Flat) - Tiết kiệm RAM hơn, yêu cầu có dữ liệu trước khi tạo
CREATE INDEX idx_documents_embedding_ivfflat ON documents 
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);
Loại Index Tốc độ Build Độ trễ Truy vấn (1 triệu dòng) Mức tiêu thụ RAM Tỷ lệ Recall
HNSW Chậm hơn (~15-30 phút / 1 triệu vector) < 8ms ~1.5 GB / 1 triệu vector 97% – 99%
IVFFlat Nhanh (~2-5 phút / 1 triệu vector) ~35-70ms ~300 MB / 1 triệu vector 85% – 92%

Nên ưu tiên chọn HNSW làm mặc định cho các ứng dụng phục vụ người dùng trực tiếp. Khác với IVFFlat, HNSW có thể build ổn định ngay cả trên bảng trống, chịu tải ghi liên tục mà không làm suy giảm độ chính xác (recall), đồng thời duy trì độ trễ phản hồi ổn định dưới 10ms.

Sử dụng Nâng cao: Pipeline Python và Hybrid Search

Tìm kiếm vector thuần túy thường gặp khó khăn với các kết quả khớp từ khóa chính xác như số hóa đơn, mã SKU hay mã sản phẩm đặc thù. Việc kết hợp full-text search của PostgreSQL (dạng tsvector kiểu BM25) với dense semantic search thông qua thuật toán Reciprocal Rank Fusion (RRF) mang lại chất lượng tìm kiếm tối ưu nhất cho cả hai trường hợp.

Truy vấn Hybrid Search cho Production (Reciprocal Rank Fusion)

WITH semantic_search AS (
    SELECT id, RANK() OVER (ORDER BY embedding <=> $1) AS rank
    FROM documents
    ORDER BY embedding <=> $1
    LIMIT 20
),
keyword_search AS (
    SELECT id, RANK() OVER (ORDER BY ts_rank_cd(to_tsvector('english', content), query) DESC) AS rank
    FROM documents, plainto_tsquery('english', $2) query
    WHERE to_tsvector('english', content) @@ query
    LIMIT 20
)
SELECT 
    d.id, 
    d.title,
    COALESCE(1.0 / (60 + s.rank), 0.0) + 
    COALESCE(1.0 / (60 + k.rank), 0.0) AS rrf_score
FROM documents d
LEFT JOIN semantic_search s ON d.id = s.id
LEFT JOIN keyword_search k ON d.id = k.id
WHERE s.id IS NOT NULL OR k.id IS NOT NULL
ORDER BY rrf_score DESC
LIMIT 10;

Tích hợp Python với Psycopg 3

import psycopg
from pgvector.psycopg import register_vector
from openai import OpenAI

client = OpenAI(api_key="your-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

# Kết nối tới PostgreSQL và truy vấn embedding 1536 chiều
with psycopg.connect("postgresql://postgres:secret@localhost:5432/app_db") as conn:
    register_vector(conn)
    
    query_text = "Làm thế nào để tối ưu hóa giới hạn kết nối PgBouncer?"
    query_vector = get_embedding(query_text)
    
    with conn.cursor() as cur:
        cur.execute("""
            SELECT id, title, content
            FROM documents
            ORDER BY embedding <=> %s::vector
            LIMIT 3;
        """, (query_vector,))
        
        for doc_id, title, snippet in cur.fetchall():
            print(f"[{doc_id}] {title}: {snippet[:80]}...")

Kinh nghiệm Thực chiến: Đảm bảo Ổn định dưới Tải Production

1. Cấp phát maintenance_work_mem Khi Tạo Index

Quá trình xây dựng HNSW index tiêu tốn lượng lớn bộ nhớ trong giai đoạn dựng đồ thị. Giá trị mặc định của maintenance_work_mem (thường chỉ 64MB) sẽ buộc Postgres phải swap các buffer tạm ra đĩa cứng, biến một tác vụ build 10 phút thành một đợt tắc nghẽn kéo dài 4 tiếng. Hãy tăng thông số này trong phiên migration của bạn:

SET maintenance_work_mem = '4GB';
CREATE INDEX CONCURRENTLY idx_documents_embedding_hnsw 
ON documents USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);

2. Sử dụng Partial Index cho Hệ thống Multi-Tenant

Tránh quét trên một vector index toàn cục với điều kiện lọc thông thường như WHERE tenant_id = 42. Postgres sẽ duyệt qua toàn bộ đồ thị HNSW toàn cục trước rồi mới lọc bỏ các bản ghi không khớp sau đó, điều này làm sụt giảm nghiêm trọng hiệu năng truy vấn của các tenant quy mô nhỏ.

Thay vào đó, hãy tạo các partial index riêng biệt cho từng tenant hoặc theo điều kiện lọc cụ thể:

-- Phân vùng index riêng cho tenant khách hàng doanh nghiệp
CREATE INDEX idx_tenant_42_vectors ON documents 
USING hnsw (embedding vector_cosine_ops)
WHERE tenant_id = 42;

3. Giữ Vector Index trong Shared Buffers bằng pg_prewarm

Quá trình duyệt đồ thị HNSW đòi hỏi các lượt đọc bộ nhớ ngẫu nhiên liên tục (random memory page lookups). Nếu các trang dữ liệu bị đẩy từ RAM xuống ổ đĩa NVMe (cold pages), độ trễ truy vấn có thể nhảy vọt từ 6ms lên 250ms. Hãy nạp sẵn các index đang hoạt động vào cache ngay sau khi khởi động lại database hoặc deploy:

CREATE EXTENSION IF NOT EXISTS pg_prewarm;
SELECT pg_prewarm('idx_documents_embedding_hnsw', 'buffer', 'main');
Share: