Bỏ qua để đến nội dung
Search & RAG

Tầng 5 — ANN & hạ tầng vector

Bản cụ thể hoá. Mục này nói về cơ chế chung. Nếu bạn đang thật sự chạy Qdrant, toàn bộ những thứ dưới đây có một bản chi tiết theo đúng cài đặt của nó ở Phần 7 — Qdrant: segment và optimizer (7.4, 7.6), tham số HNSW thật (7.7), bẫy filter (7.8), và mô hình bộ nhớ (7.10).

Recall là thứ đánh đổi được, không phải thứ cho không

Phần tiêu đề “Recall là thứ đánh đổi được, không phải thứ cho không”

Brute-force cho recall 1.0 với chi phí O(N). ANN đánh đổi recall lấy tốc độ. Recall của ANN là một tham số bạn chọn, và nhiều hệ production đang chạy ở recall 0.85 mà không ai biết.

HNSW greedy descent — đi qua từng tầng
HNSW greedy descent — đi qua từng tầngMở toàn màn hình
IVF hai tầng — truy vấn, chi phí, và bẫy biên cụm
IVF hai tầng — truy vấn, chi phí, và bẫy biên cụmMở toàn màn hình

Cơ chế bên trong — vì sao greedy routing trên đồ thị lại nhanh (Kleinberg, NSW), năm thuật toán của paper gốc viết ra thành pseudo-code, tính tầng của một điểm bằng tay, vì sao phải cắt tỉa bằng heuristic, chỗ HNSW đau khi xoá, và Vamana/DiskANN: 6.2 — ANN đồ thị: từ small world tới HNSW. Mục này chỉ nói xoay núm nào.

Đồ thị nhiều tầng. Tầng trên thưa (bước dài, đi xa nhanh), tầng dưới dày (bước ngắn, tinh chỉnh). Tìm kiếm = greedy descent: vào từ tầng trên, đi đến điểm gần nhất, tụt xuống tầng dưới, lặp lại.

Tham sốÝ nghĩaĐánh đổi
Msố kết nối mỗi nodecao → recall tốt, RAM nhiều hơn (~M × 8 byte/vector)
efConstructionđộ rộng tìm kiếm khi xâycao → đồ thị chất lượng hơn, build chậm hơn. Chỉ trả giá một lần.
efSearchđộ rộng tìm kiếm khi truy vấnđây là núm điều chỉnh recall/latency tại runtime

→ Nếu bạn chỉ nhớ một điều: efSearch là núm bạn xoay khi cần recall cao hơn, và nó không cần build lại index.

IVF: phân cụm vector thành nlist cụm, truy vấn chỉ quét nprobe cụm gần nhất. Đây là cách giảm số phép so sánh. (Cơ chế đầy đủ — vì sao nlist ≈ 4√N, vì sao IVF cần train trước nên không dựng được từ index rỗng, và bảng so IVF vs HNSW theo mười một trục: 6.2 §1.1–1.3. Bản thân bước phân cụm — thuật toán Lloyd, chi phí train, khởi tạo, spherical cho cosine, và centroid lệch dần: 6.2 §1.1 — k-means.) Quantization là cách giảm kích thước mỗi phép so sánh — hai trục độc lập, thường dùng chung.

Cơ chế bên trong — scalar / PQ / binary hoạt động thế nào, tính bằng tay, vì sao binary cần vector nhiều chiều: 6.1 — Quantization cho vector. Mục này chỉ nói chọn cái nào và cấu hình ra sao.

Scalar quantization (SQ) — mỗi chiều float32 → uint8. Nén . Qdrant ghi sai số “thường dưới 1%”, và so sánh int8 được SIMD tăng tốc nên còn nhanh hơn float32. Đây là mặc định an toàn, và là lựa chọn duy nhất còn tốt với khoảng cách Manhattan (L1).

Product quantization (PQ) — chia vector thành m đoạn, mỗi đoạn quantization về 1 byte codebook (k-means). 1536 chiều × 4 byte = 6 KB → m=96 → 96 byte, nén ~64×. Nén sâu nhất, nhưng phép tính khoảng cách không SIMD-friendly nên chậm hơn scalar. Chỉ dùng khi bộ nhớ là ràng buộc số một.

Binary quantization (BQ) — 1 bit/chiều, nén 32×, so sánh bằng Hamming distance (rất nhanh — Qdrant báo tới ~40× nhanh hơn vector gốc). Điều kiện: vector phải đủ nhiều chiều và phân bố các chiều phải cân quanh 0. Số Qdrant công bố: text-embedding-ada-002 (1536 chiều) đạt recall@100 = 0.98 với oversampling 4×; Cohere embed-english-v2.0 (4096 chiều) đạt recall@50 = 0.98 với oversampling 2×. Ở 384 chiều thì BQ sập. Từ v1.5 có thêm biến thể 2-bit (nén 16×) và 1.5-bit (nén 24×) để bớt mất mát.

TurboQuant (TQ) — Qdrant v1.18+, dựa trên paper ICLR 2026. Đây là thứ mới đáng chú ý nhất trong nhóm này. Ý tưởng một dòng: quay vector bằng một phép quay trực giao ngẫu nhiên trước khi nén, để thông tin dàn đều ra các chiều thay vì dồn vào vài chiều — cùng một số bit nhưng méo ít hơn. 4-bit (mặc định) nén 8×, 2-bit 16×, 1.5-bit 24×, 1-bit 32×. Mặc định bất đối xứng: query giữ full precision, chỉ vector lưu trong index bị nén.

Trên benchmark 10 dataset của Qdrant (arxiv-384, dbpedia-ada/gemini/openai-3, wikipedia-cohere, H&M 2048 chiều, LAION CLIP…):

  • TQ 4-bit đạt recall ngang scalar (lệch 1–2 pp, có dataset còn cao hơn tới 4.6 pp) mà nén gấp đôi (8× vs 4×).
  • cùng mức nén, TQ hơn binary 9–24 pp tại 16× và 9–21 pp tại 32×.

Hỗ trợ cosine / dot / L2 với SIMD; L1 chạy được nhưng chậm đáng kể.

Muốn biết vì sao phép quay lại đủ? Cơ chế bên trong — phân bố Beta của toạ độ sau khi quay, bảng mã Lloyd–Max cố định nên không cần huấn luyện, tầng QJL 1 bit khử thiên lệch inner product, và cận dưới lý thuyết ≈ 2,7 lần — nằm ở 6.1 mục 6.

Các con số ở mục này là số Qdrant/paper công bố, không phải số đo trên data/corpus.jsonl của repo này (50 tài liệu — quá nhỏ để quantization có ý nghĩa).

Mức nénNên dùngGhi chú
Scalar (int8)An toàn nhất; nhưng cân nhắc TQ 4-bit để được 8× cùng recall
TurboQuant 4-bitĐiểm ngọt hiện tại: recall ≈ scalar, nén gấp đôi
16×TQ 2-bit hoặc BQ 2-bitTQ recall tốt hơn nhiều; BQ nhanh hơn
24×TQ 1.5-bit hoặc BQ 1.5-bitCùng đánh đổi như trên
32×TQ 1-bit hoặc BQ 1-bitRecall gần nhau hơn ở mức này; BQ nhanh hơn
tới 64×Product quantizationChỉ khi bộ nhớ là ưu tiên số một
ScalarNếu bạn dùng khoảng cách L1 (TQ/BQ không phù hợp)

Ba núm phải biết trước khi bật quantization

Phần tiêu đề “Ba núm phải biết trước khi bật quantization”
  • rescore — lấy top-k thô bằng vector nén, rồi tính lại điểm bằng vector gốc. Bật mặc định cho BQ và TQ 1 / 1.5 / 2-bit. Đây là thứ mua lại phần lớn recall đã mất — nhưng nó đòi vector gốc vẫn truy cập được. Nếu vector gốc nằm trên disk chậm, rescore giết latency; khi đó Qdrant khuyên tắt rescore và tăng oversampling.
  • oversampling — lấy oversampling × limit ứng viên trước khi rescore (ví dụ 2.0, 4.0). Đây là núm đổi latency lấy chất lượng tại runtime, không cần build lại index — vai trò giống efSearch của HNSW.
  • quantile (chỉ scalar) — mặc định 0.99: bỏ 1% giá trị cực trị khi tính khoảng [min, max] để map sang int8. Hạ xuống nếu embedding có outlier làm hỏng thang đo.

Ngoài ra memory quyết định bản nào nằm RAM (pinned) bản nào nằm disk (cold). Ba cấu hình thực dụng: (1) gốc cached + nén trong RAM — nhanh nhất; (2) gốc cold + nén pinned — cân bằng, cân nhắc tắt rescore; (3) tất cả cold — nhỏ nhất, cần NVMe.

Muốn biết quantization đang lấy đi bao nhiêu recall: chạy lại đúng bộ truy vấn với params: {quantization: {ignore: true}} rồi so hai con số. Đừng bật quantization mà không đo bước này.

⚠️ Nhớ rằng bản nén là bản thêm vào, không phải bản thay thế — trừ khi bạn đẩy vector gốc sang cold. Bật quantization mà vẫn giữ vector gốc trong RAM thì bộ nhớ tăng, chỉ tốc độ mới lợi.

Bạn muốn “tìm 10 chunk giống nhất trong phòng ban X”. Hai cách sai:

  • Post-filter: lấy top-100 bằng vector, rồi lọc phòng ban X. Nếu X chỉ chiếm 1% corpus thì top-100 có thể còn 0 kết quả. Recall sập không báo lỗi.
  • Pre-filter naive: lọc trước rồi brute-force trong tập con. Đúng nhưng chậm khi tập con lớn.

Cách đúng: filtered ANN — engine phải hỗ trợ điều hướng đồ thị có ràng buộc (Qdrant, Weaviate, pgvector với điều kiện) và bạn phải đo recall khi có filter, không chỉ đo recall khi không filter. Cơ chế đầy đủ, và quy trình đo bốn số để tự phát hiện mình có đang bị hay không: 7.8 — Filterable HNSW và query planner. Đây là chỗ hầu hết hệ RAG có filter đang bị mất recall âm thầm.

Toán bộ nhớ (làm trước khi chọn hạ tầng)

Phần tiêu đề “Toán bộ nhớ (làm trước khi chọn hạ tầng)”
1M chunk × 1536 chiều × 4 byte (float32) = 6.1 GB vector
+ HNSW M=32 ≈ 0.3 GB đồ thị
+ payload/metadata tuỳ
→ scalar int8 (4×) = 1.5 GB
→ TurboQuant 4-bit (8×) = 0.77 GB
→ binary / TurboQuant 1-bit (32×) = 0.19 GB
→ PQ x64 = 0.10 GB
→ hoặc hạ 384 chiều (MRL) rồi + int8 = 0.4 GB

Lưu ý: các dòng nén là kích thước bản nén, cộng thêm vào vector gốc nếu bạn còn giữ vector gốc trong RAM (xem núm memory ở trên).

Phần 4 — Lý thuyết