04 · RAG production với PostgreSQL + pgvector

RAG (Retrieval-Augmented Generation — tìm tài liệu liên quan rồi mới đưa cho model đọc) là cách phổ biến nhất để LLM trả lời bằng dữ liệu của công ty. Demo RAG mất một buổi chiều; RAG chạy đúng trong production mất vài tháng, và gần như mọi lỗi nằm ở phần tìm kiếm, không nằm ở LLM. Với team đã chạy PostgreSQL, pgvector là lựa chọn tự nhiên — bài này đo xem nó hỏng ở đâu.

Đã verify gì, chưa verify gì. Lab chạy thật trên PostgreSQL 16.13 + pgvector 0.6.0 (cluster tạm cổng 55440, không đụng DB nào khác): 50 000 vector 384 chiều dạng cụm, đo recall/latency theo ef_search, bẫy WHERE + HNSW, bộ nhớ khi build index, giới hạn số chiều, full-text tiếng Việt + unaccent, hybrid RRF. Vector là dữ liệu tổng hợp, không phải embedding thật (máy lab không tải được model embedding) — đo được hành vi của index, không đo được chất lượng ngữ nghĩa. Chưa verify: iterative index scan (pgvector ≥ 0.8), halfvec (≥ 0.7), reranker, contextual retrieval — ghi nguồn tại chỗ. Máy lab không có repo postgresql-learning nên không chạy trên PostgreSQL 18.4 như quy ước của repo.

Câu trả lời 60 giây

"RAG là một search engine đặt trước LLM: chất lượng câu trả lời bị chặn trên bởi recall của bước tìm — đoạn đúng không được lấy ra thì model không thể trả lời đúng, và thường sẽ trả lời sai một cách tự tin. Tôi dùng pgvector vì dữ liệu, quyền và vector nằm chung một transaction, không phải đồng bộ hai hệ thống. Nhưng HNSW là tìm gần đúng: trong lab 50k vector, recall@10 chỉ 0,81 ở ef_search mặc định 40, lên 0,98 ở 200. Bẫy lớn nhất là lọc: thêm WHERE tenant = ... cho tenant chiếm 1% dữ liệu thì top-10 trả về 0–2 dòng thay vì 10, vì index lọc sau khi đã lấy ứng viên. Nên tôi đo recall trên bộ câu hỏi thật, dùng hybrid với full-text cho mã lỗi/mã sản phẩm, và xử lý lọc theo tenant bằng index riêng hoặc quét chính xác."

Thuật ngữ

Thuật ngữNghĩa ngắn
embeddingVector số (vài trăm đến vài nghìn chiều) biểu diễn ý nghĩa đoạn văn; gần nhau = giống nghĩa.
chunkMẩu tài liệu được cắt ra để embed và truy xuất.
ANN (approximate nearest neighbor)Tìm láng giềng gần gần đúng — nhanh hơn nhiều, đổi lại có thể bỏ sót.
HNSWHierarchical Navigable Small World — index dạng đồ thị nhiều tầng cho ANN.
IVFFlatIndex chia vector thành cụm (lists), chỉ quét vài cụm gần nhất (probes).
recall@kTrong k kết quả trả về, bao nhiêu phần là kết quả đúng (so với tìm chính xác).
hybrid searchKết hợp tìm theo từ khoá (full-text/BM25) và theo vector.
RRF (reciprocal rank fusion)Gộp nhiều danh sách xếp hạng: điểm = Σ 1/(k + hạng).
rerankerModel chấm lại độ liên quan của từng cặp (câu hỏi, đoạn) — chậm hơn nhưng chính xác hơn embedding.

1. Bài toán gốc và bối cảnh ra đời

LLM không biết chính sách đổi trả của shop bạn, không biết đơn hàng hôm qua, và kiến thức dừng ở knowledge cutoff. Ba cách đưa kiến thức vào:

CáchGiới hạn
Fine-tune model trên tài liệuĐắt, chậm cập nhật (đổi chính sách phải train lại), không trích dẫn được nguồn, không phân quyền theo người đọc, vẫn hallucinate
Nhồi toàn bộ tài liệu vào promptGiới hạn cửa sổ, chi phí mỗi lần gọi, context rot (bài 03)
RAG (Lewis et al., Facebook AI Research, 2020)Chất lượng phụ thuộc bước tìm; thêm một hệ thống phải vận hành

RAG thắng vì cập nhật tức thì (sửa tài liệu → re-embed chunk đó), trích dẫn được, và lọc được theo quyền người hỏi. Làn sóng vector database 2022–2023 (Pinecone, Weaviate, Qdrant, Milvus) đi cùng ChatGPT; pgvector (Andrew Kane, 2021) đưa kiểu vector và index ANN vào PostgreSQL.

RAG tối ưu cho: câu hỏi mà câu trả lời nằm trong một vài đoạn tài liệu (FAQ, chính sách, tài liệu kỹ thuật, ticket cũ). Không tối ưu cho: câu hỏi tổng hợp ("doanh thu tháng 9 theo kênh" → SQL), câu hỏi cần đọc toàn bộ kho ("tóm tắt mọi khiếu nại quý này" → map-reduce/batch), tra cứu chính xác theo mã ("đơn 1001" → tool SQL), hay dữ liệu thay đổi từng giây (gọi API sống).

2. Mô hình tư duy cốt lõi

RAG = search engine + LLM đọc hiểu. Model chỉ đọc được những gì search trả về; chất lượng câu trả lời ≤ recall của retrieval. Embedding biến "tìm theo nghĩa" thành "tìm điểm gần nhất trong không gian vector", và ANN đổi độ chính xác lấy tốc độ.
flowchart LR
  subgraph ING["Ingest (offline, khi tài liệu đổi)"]
    D["Tài liệu"] --> C["Chunking"] --> E1["Embedding model"] --> DB[("PostgreSQL: chunk + metadata + vector")]
  end
  subgraph QRY["Query (mỗi câu hỏi)"]
    Q["Câu hỏi"] --> E2["Embedding (cùng model!)"] --> R["ANN top-k + full-text + lọc quyền"]
    DB --> R
    R --> RR["Rerank (tuỳ chọn)"] --> P["Prompt: câu hỏi + top đoạn + nguồn"] --> L["LLM"] --> A["Câu trả lời + trích dẫn"]
  end

Hệ quả của abstraction:

3. Internals và quyết định thiết kế

3.1 Chunking

Chiến lượcĐượcMất
Cố định N token, chồng lấn (vd 400 token, overlap 50)Đơn giản, đoán được chi phíCắt ngang bảng, danh sách, câu; chunk mất ngữ cảnh ("Điều 3 áp dụng cho..." — điều 3 của văn bản nào?)
Theo cấu trúc (heading, mục, hàm)Chunk có nghĩa trọn vẹnKích thước không đều; cần parser theo định dạng
Contextual chunk (thêm 1–2 câu mô tả chunk nằm ở đâu trong tài liệu, do LLM sinh)Anthropic báo cáo (9/2024, "Contextual Retrieval"): giảm tỉ lệ truy xuất hỏng top-20 49% khi kết hợp contextual embedding + contextual BM25, 67% khi thêm rerankTốn một lần gọi LLM mỗi chunk lúc ingest (dùng prompt cache cho toàn văn bản để giảm)

Nguyên tắc: chunk đủ nhỏ để embedding "tập trung" một ý, đủ lớn để tự đứng được; luôn lưu kèm document_id, tiêu đề, vị trí, phiên bản, quyền, để trích dẫn và lọc.

3.2 Embedding và khoảng cách

Embedding model chọn theo: hỗ trợ tiếng Việt (model đa ngôn ngữ), số chiều (384–3072; chiều càng nhiều càng tốn RAM/disk), giá, và có chạy được nội bộ không (dữ liệu nhạy cảm). Anthropic không cung cấp embedding model riêng; tài liệu của họ gợi ý nhà cung cấp bên thứ ba — nghĩa là pipeline RAG thường có hai nhà cung cấp AI.

Kích thước lưu trữ đo trong lab (kiểu vector = float4):

 bytes_vec384 | bytes_vec1536
--------------+---------------
         1544 |          6152

1 triệu chunk × 1536 chiều ≈ 6,2 GB chỉ riêng cột vector, chưa kể index (thường cỡ bảng hoặc lớn hơn — xem 3.3). Khoảng cách: dùng cosine (<=>, ops vector_cosine_ops) cho embedding văn bản; nếu vector đã chuẩn hoá độ dài 1 thì inner product (<#>) cho cùng thứ tự và rẻ hơn.

3.3 Index: exact, IVFFlat, HNSW

IndexCách chạyĐượcMất
Không index (seq scan)Tính khoảng cách với mọi dòngRecall 100%, luôn đúng với WHEREO(N) — lab: 24,8 ms/query ở 50k, tăng tuyến tính
IVFFlatK-means chia lists cụm; query quét probes cụm gần nhấtBuild nhanh, ít RAMCần dữ liệu có sẵn để train cụm; dữ liệu trôi theo thời gian thì cụm lệch, phải rebuild
HNSW (Malkov & Yashunin, 2016)Đồ thị nhiều tầng: tầng trên thưa để nhảy xa, tầng dưới dày để tinh chỉnh; tham số m (số cạnh), ef_construction, ef_searchTrade-off recall/latency tốt nhất; thêm dữ liệu không cần train lạiBuild chậm và ngốn RAM; index to; kết quả build không tất định

Lab: recall vs ef_search (50 000 vector, 384 chiều, 200 cụm, HNSW m=16, ef_construction=64, 50 query):

nạp 50000 vector 384 chiều: 6.2s
exact (seq scan) p50 = 24.8 ms/query
build HNSW maintenance_work_mem= 16MB:  53.1s, index 98 MB (bảng 78 MB)
   NOTICE: hnsw graph no longer fits into maintenance_work_mem after 7450 tuples
build HNSW maintenance_work_mem=512MB:   6.3s, index 98 MB (bảng 78 MB)

 ef_search  recall@10   p50 ms   p95 ms
        10      0.664     0.78     1.25
        20      0.730     0.75     0.97
        40      0.808     0.77     1.04
       100      0.916     1.02     1.72
       200      0.976     1.12     2.23

Đọc kết quả:

Dữ liệu tổng hợp dễ hơn hay khó hơn thật? Vector lab chia 200 cụm rõ rệt; embedding văn bản thật thường có cấu trúc kém rõ hơn và nhiều chiều hơn (768–1536), nên recall ở cùng tham số có thể thấp hơn. Con số đúng cho hệ thống của bạn chỉ có được bằng cách đo trên embedding thật của bạn — chưa verify.

3.4 Lọc (WHERE) + HNSW: bẫy lớn nhất

Multi-tenant là chuẩn trong SaaS: mỗi shop chỉ được tìm trong tài liệu của mình. Lab: tenant shop_nho chiếm ~1% (470/50 000 chunk):

Tenant 'shop_nho' có 470 chunk. Query top-10 có WHERE tenant='shop_nho':
   HNSW ef_search=40 : số dòng trả về / 20 query = [1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 2, 0, 0]
   plan: Limit | ->  Index Scan using tmp_chunks_hnsw on tmp_chunks | Order By: (emb <=> '[...]'::vector) | Filter: (tenant = 'shop_nho'::text)
   lọc trước + exact  : số dòng = [10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10], p50 0.9 ms

LIMIT 10 nhưng nhận 0–2 dòng. Plan cho thấy lý do: index scan lấy ~ef_search ứng viên gần nhất trên toàn bảng, rồi Filter mới bỏ dòng không thuộc tenant. 40 ứng viên × 1% ≈ 0,4 dòng. Không có lỗi, không có cảnh báo; với người dùng của shop nhỏ, bot "không tìm thấy gì" hoặc trả lời không có tài liệu. Tệ hơn: nếu lọc quyền được làm sau bước tìm ở tầng ứng dụng, cùng cơ chế này dẫn tới rò dữ liệu tenant khác nếu ai đó quên lọc.

Cách xử lýKhi nào hợpCái giá
Lọc trước + quét chính xác (index B-tree trên tenant)Tenant nhỏ (vài nghìn chunk) — lab: 10/10 dòng, 0,9 msChậm với tenant lớn; planner có thể không tự chọn (lab phải SET enable_indexscan = off)
Partial index / partition theo tenant lớnVài tenant lớn chiếm phần lớn dữ liệuNhiều index phải quản lý
Tăng ef_searchBộ lọc không quá hiếm (≥10%)Không cứu được bộ lọc 1%
Iterative index scan (pgvector ≥ 0.8.0: SET hnsw.iterative_scan = relaxed_order)Mặc định nên bật khi có lọcCó trần hnsw.max_scan_tuples; relaxed_order có thể lệch thứ tự nhẹ. Chưa verify — lab dùng 0.6.0

3.5 Hybrid search và tiếng Việt

Embedding giỏi "nghĩa gần", kém với chuỗi chính xác: mã lỗi E1234, SKU, tên riêng. Full-text search giỏi đúng chỗ đó. PostgreSQL không có từ điển tiếng Việt, nên cấu hình simple + unaccent là mức tối thiểu. Lab:

--- 1. PostgreSQL không có từ điển tiếng Việt: config simple chỉ tách theo khoảng trắng, không bỏ dấu
                         simple_tsv
-------------------------------------------------------------
 'bị':5 'e1234':7 'hoàn':1 'hàng':4 'lỗi':6 'tiền':2 'đơn':3

 khop_khong_dau
----------------
 f

--- 2. Thêm unaccent: người gõ không dấu vẫn khớp
 khop_sau_unaccent
-------------------
 t

Người dùng gõ "hoan tien" không khớp "Hoàn tiền" nếu thiếu unaccent. (Ghi chú: simple tách theo khoảng trắng nên "hoàn tiền" là hai token rời; không có tách từ ghép tiếng Việt — muốn tốt hơn cần công cụ tách từ bên ngoài hoặc search engine có plugin tiếng Việt.)

Gộp hai danh sách bằng RRF (k = 60), chạy thật với 5 đoạn FAQ và vector 3 chiều viết tay (chỉ minh hoạ cơ chế, không nói về chất lượng):

 id |                       body                       | hang_keyword | hang_vector |   rrf
----+--------------------------------------------------+--------------+-------------+---------
  1 | Chính sách hoàn tiền trong 30 ngày kể từ khi nhậ |            1 |           3 | 0.03227
  3 | Đổi trả sản phẩm lỗi do nhà sản xuất             |            3 |           1 | 0.03227
  2 | Mã lỗi E1234: thanh toán thẻ bị từ chối bởi ngân |            2 |           4 | 0.03175
  4 | Hướng dẫn trả lại hàng và nhận lại tiền          |            4 |           2 | 0.03175

RRF chỉ dùng thứ hạng, không dùng điểm — nên không cần chuẩn hoá thang điểm giữa ts_rank và khoảng cách cosine, đổi lại mất thông tin "cách biệt lớn hay nhỏ". Toàn bộ SQL ở phụ lục.

3.6 Rerank và agentic RAG

Mẫu phổ biến: lấy rộng (top 50–100 từ hybrid) → reranker cross-encoder chấm lại → đưa 3–10 đoạn tốt nhất vào prompt. Reranker đắt hơn mỗi cặp nhưng chỉ chạy trên vài chục ứng viên. Ở mức agent, retrieval trở thành một tool (search_kb(query, filters)): model tự viết lại câu truy vấn, tìm nhiều lần, đọc thêm tài liệu gốc — tốt hơn với câu hỏi nhiều bước, đổi lại nhiều vòng gọi hơn (bài 02).

4. Đảm bảo và giới hạn

Khía cạnhpgvector đảm bảoKhông đảm bảo
Nhất quánVector, chunk, quyền nằm chung transaction MVCC — commit là thấy, rollback là mất, không có dual-write—
RecallSeq scan: 100%HNSW/IVFFlat: gần đúng (lab 0,66–0,98 tuỳ ef_search); khác nhau giữa các lần build
LọcKết quả luôn thoả WHEREĐủ LIMIT dòng khi lọc hiếm (bản < 0.8)
Số chiều indexHNSW trên vector: tới 2 000 chiềuLab: ERROR: column cannot have more than 2000 dimensions for hnsw index với 3 072 chiều. Bản ≥ 0.7 có halfvec (tới 4 000 chiều theo tài liệu — chưa verify)
ScaleỔn tới vài triệu–vài chục triệu vector nếu index vừa RAMScale ngang tự động; build index song song nhiều máy
Độ trễDưới vài ms khi index nằm trong RAM (lab)Khi index bị đẩy ra đĩa, latency tăng hàng chục lần — chưa đo

Về CAP: pgvector kế thừa PostgreSQL — một primary nhận ghi, replica đọc bất đồng bộ (đọc từ replica có thể thấy chunk cũ vài giây). Vector DB chuyên dụng phân tán thường chọn eventual consistency để scale ghi.

5. Failure modes

LỗiHệ quảPhát hiệnPhục hồi / phòng
Lọc hiếm + HNSW (lab 3.4)Tenant nhỏ "không tìm thấy gì"Metric số kết quả trả về < k theo tenantIterative scan (≥0.8), exact cho tenant nhỏ, partition
Build index thiếu RAM (lab)Migration chậm 8×, khoá bảng lâu nếu không CONCURRENTLYNOTICE trong log, thời gian migrationTăng maintenance_work_mem trong session build; CREATE INDEX CONCURRENTLY
Đổi embedding model (lab)expected 384 dimensions, not 1536; tệ hơn: cùng số chiều khác model → không lỗi, kết quả rácLưu embedding_model cạnh vector; kiểm khi queryCột mới + backfill + index mới + chuyển đọc (mục 6)
Chunk cũ (tài liệu sửa, chunk không re-embed)Trả lời theo chính sách hết hạn — rủi ro kiểu Air CanadaSo updated_at tài liệu với chunkIngest theo sự kiện (observer/queue), lưu phiên bản tài liệu
Chunk cắt ngang câu trả lờiModel thấy nửa điều khoảnEval retrieval: đoạn đúng có nằm trọn trong chunkChunk theo cấu trúc, overlap, trả thêm chunk kề bên
Rò quyềnKhách A đọc được tài liệu nội bộ / của khách BTest bảo mật với câu hỏi chéo tenantLọc quyền trong SQL (RLS), không lọc sau LLM
RAG poisoning (OWASP LLM08)Tài liệu/review do người ngoài viết chứa chỉ dẫn độc, được retrieve vào promptNguồn dữ liệu được đánh dấu tin cậy/khôngKhông cho nội dung người dùng tạo vào cùng kho với chính sách; coi đoạn retrieve là dữ liệu (bài 08)
Bloat index do UPDATE/DELETE nhiềuIndex phình, chậm dầnKích thước index theo thời gianVacuum, REINDEX CONCURRENTLY định kỳ — xem dead tuple & index bloat

6. Vận hành trong production

Metric

Tham số

Tham sốGợi ý
hnsw.ef_searchĐặt theo query bằng SET LOCAL trong transaction; bắt đầu 100, đo recall
m, ef_constructionMặc định 16/64 là điểm khởi đầu tốt; tăng khi recall không đủ dù ef_search cao
maintenance_work_memĐủ để đồ thị vừa RAM lúc build (lab: 512MB cho 50k×384); đặt trong session migration, không đặt toàn cục
max_parallel_maintenance_workersBuild HNSW song song (pgvector ≥ 0.6)

Capacity planning (ước lượng)

Từ lab: 50k × 384 chiều → bảng 78 MB, index 98 MB (~1 960 byte/vector cho index). Nội suy tuyến tính (ước lượng, chưa đo ở quy mô lớn): 2 triệu chunk × 1536 chiều → cột vector ~12 GB, index HNSW ~15 GB. Cần RAM cho cả index + working set của các bảng khác; nếu không vừa, cân nhắc giảm chiều (model nhỏ hơn, halfvec), lượng tử hoá, hoặc tách riêng.

Migration đổi embedding model (zero-downtime)

flowchart LR
  A["Thêm cột emb_v2 vector(1024) NULL + cột embedding_model"] --> B["Job nền embed lại theo lô, ghi emb_v2"]
  B --> C["CREATE INDEX CONCURRENTLY trên emb_v2"]
  C --> D["Chạy eval retrieval với emb_v2"]
  D --> E["Feature flag chuyển đọc sang emb_v2"]
  E --> F["Ingest ghi cả hai cột một thời gian"]
  F --> G["Xoá cột và index cũ"]

Chi tiết index không khoá bảng: CREATE INDEX CONCURRENTLY; nguyên tắc migration nhiều bước: migration PostgreSQL zero-downtime.

Gánh nặng cho team

Pipeline ingest là một hệ thống dữ liệu thật: parser nhiều định dạng (PDF, HTML, Word), hàng đợi, retry, re-embed khi đổi model, quyền theo tài liệu. Người trực cần biết trả lời "vì sao bot không tìm thấy tài liệu X" — cần công cụ tra: tài liệu X có bao nhiêu chunk, embed lúc nào, bằng model nào, câu hỏi này retrieve ra những chunk nào.

7. So sánh và ma trận quyết định

TrụcpgvectorVector DB chuyên dụng (Qdrant, Weaviate, Milvus, Pinecone)Elasticsearch / OpenSearch kNNKhông RAG: long context + cacheAgentic search (SQL/grep qua tool)
Nhất quán với dữ liệu nghiệp vụCùng transactionDual-write, eventualDual-write—Đọc thẳng nguồn
Lọc + phân quyềnSQL/RLS đầy đủ; bẫy lọc hiếm (<0.8)Lọc payload tốt, tối ưu sẵnTốt—Theo quyền của tool
HybridTự ghép FTS + RRF bằng SQLNhiều bản có sẵnBM25 mạnh nhất, có sẵn——
ScaleTới chục triệu (vừa RAM)Trăm triệu+ , scale ngangScale ngangGiới hạn cửa sổTheo nguồn
Vận hành thêmKhông (đã có PostgreSQL)Một hệ thống mớiMột cụm mới (nếu chưa có)KhôngKhông
Tuyển ngườiDễ (biết Postgres)Trung bìnhTrung bìnhDễDễ

Dùng pgvector khi: đã chạy PostgreSQL, dưới vài chục triệu chunk, cần quyền/transaction chặt, team nhỏ. Tránh / cân nhắc khác khi: hàng trăm triệu vector, ghi embedding liên tục với throughput cao, cần lọc phức tạp trên tenant rất nhỏ mà không nâng được pgvector, hoặc đã có sẵn Elasticsearch làm search chính. Không cần RAG khi: kho tài liệu nhỏ (vài chục nghìn token) — nhồi vào prompt với cache rẻ và đơn giản hơn.

8. Góc nhìn tech lead: tác động tổ chức

9. Anti-patterns và bài học thực tế

Điều người vận hành lâu năm đều biết: 80% cải thiện RAG đến từ dữ liệu (làm sạch, chunk đúng cấu trúc, metadata, bỏ tài liệu trùng/cũ) và hybrid search, không đến từ đổi embedding model đắt hơn.

10. Câu hỏi phụ interviewer hay hỏi tiếp

HNSW hay IVFFlat?
HNSW cho đa số trường hợp: recall/latency tốt, không cần train, thêm dữ liệu dần được. IVFFlat khi RAM build hạn chế và dữ liệu tĩnh; nhớ rebuild khi phân bố dữ liệu đổi.
Vì sao không dùng vector DB riêng?
Vì mất tính nhất quán (dual-write), thêm hệ thống vận hành, và quyền phải đồng bộ hai nơi. Ở quy mô dưới chục triệu vector, pgvector đủ; khi vượt, tách ra có lý do rõ ràng.
Làm sao biết recall của hệ thống?
Lấy mẫu query thật, chạy tìm chính xác (seq scan) làm ground truth, so với kết quả index — như lab. Với chất lượng ngữ nghĩa, cần bộ câu hỏi có đoạn đúng do người gán nhãn.
Multi-tenant làm sao cho đúng?
Lọc tenant trong SQL (lý tưởng là RLS), xử lý bẫy lọc hiếm (iterative scan / exact cho tenant nhỏ / partition), test chéo tenant trong CI.
Long context 1M có giết RAG không?
Không với kho lớn và nhiều người dùng: chi phí mỗi lần gọi, TTFT, context rot, và phân quyền. Với kho nhỏ, cố định, dùng lại nhiều → long context + cache có thể đơn giản hơn RAG.
Hybrid search có đáng không?
Có, gần như luôn luôn cho dữ liệu nghiệp vụ có mã, tên riêng, số hiệu — embedding yếu với chuỗi chính xác. RRF là cách gộp đơn giản, không cần chuẩn hoá điểm.

Phụ lục: lab đã chạy

Môi trường: Ubuntu 24.04, PostgreSQL 16.13 (apt), pgvector 0.6.0 (postgresql-16-pgvector), cluster tạm cổng 55440, Python 3.11 + psycopg 3.3.6 + numpy. Output gốc: labs/ai/out/04-*.txt.

# Cluster PostgreSQL tạm, không đụng DB đang chạy
sudo apt-get install -y postgresql-16-pgvector
sudo -u postgres /usr/lib/postgresql/16/bin/initdb -D /var/lib/postgresql/ailab/data -A trust
sudo -u postgres /usr/lib/postgresql/16/bin/pg_ctl -D /var/lib/postgresql/ailab/data -o '-p 55440 -k /tmp' -l /tmp/ailab.log start
psql -h /tmp -p 55440 -U postgres -c "CREATE DATABASE ailab"

# Recall/latency, bẫy WHERE, maintenance_work_mem, đổi số chiều (~2 phút)
DSN="host=/tmp port=55440 user=postgres dbname=ailab" python3 labs/ai/py/pgvector_lab.py

# Full-text tiếng Việt + unaccent + hybrid RRF
psql -h /tmp -p 55440 -U postgres -d ailab -f labs/ai/sql/hybrid_search_vi.sql

Truy vấn hybrid (RRF) dùng trong lab:

WITH q AS (SELECT plainto_tsquery('simple', unaccent('lỗi E1234 hoàn tiền')) AS tsq,
                  '[0.8,0.3,0.05]'::vector AS v),
kw AS (SELECT id, row_number() OVER (ORDER BY ts_rank(tsv, q.tsq) DESC) AS r
       FROM tmp_faq, q WHERE tsv @@ to_tsquery('simple', replace(q.tsq::text, '&', '|'))),
vs AS (SELECT id, row_number() OVER (ORDER BY emb <=> q.v) AS r
       FROM tmp_faq, q ORDER BY emb <=> q.v LIMIT 4)
SELECT f.id, kw.r AS hang_keyword, vs.r AS hang_vector,
       COALESCE(1.0/(60+kw.r),0) + COALESCE(1.0/(60+vs.r),0) AS rrf
FROM tmp_faq f LEFT JOIN kw USING (id) LEFT JOIN vs USING (id)
WHERE kw.r IS NOT NULL OR vs.r IS NOT NULL
ORDER BY rrf DESC;

Chưa verify: chất lượng ngữ nghĩa với embedding thật; iterative index scan (0.8), halfvec (0.7); hành vi khi index không vừa RAM; reranker; contextual retrieval. Nguồn: tài liệu pgvector (README, mục Filtering / Iterative Index Scans); Anthropic Engineering, "Introducing Contextual Retrieval" (9/2024); Malkov & Yashunin, "Efficient and robust approximate nearest neighbor search using HNSW graphs" (2016); Lewis et al., "Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks" (2020); OWASP Top 10 for LLM Applications 2025 (LLM08).

Bài trước: 03 · Context engineering · Bài tiếp: 05 · Workflow patterns và multi-agent.