7.3 Nhiều biểu diễn trên một point: named vector, sparse, multivector
Một point không bị buộc phải có đúng một vector. Đây là điểm khiến Qdrant hợp với kiến trúc RAG nhiều tầng hơn là một thư viện ANN thuần: cả ba tầng của 5.1 — lexical, dense, late-interaction — có thể sống chung trên cùng một point, và được ghép lại bằng một request duy nhất (7.11).
Point id = 42 ├── "dense" : [0.11, -0.4, …] 768 chiều ← ngữ nghĩa ├── "bm25" : {12: 1.7, 908: 0.9, 4471: 2.3} sparse ← từ khoá └── "colbert" : [[…], […], […], …] multivector ← xếp lại (late interaction) payload : {"phong_ban": "ktoan", "text": "…"}Named vector — nhiều không gian trong một collection
Phần tiêu đề “Named vector — nhiều không gian trong một collection”Mỗi vector có một tên, và mỗi tên có số chiều và khoảng cách riêng. Chúng độc lập
hoàn toàn: bạn có thể để dense dùng Cosine 768 chiều còn image dùng Dot 512 chiều
trong cùng một collection.
Ba lý do thật để dùng nhiều named vector:
- Đa phương thức. Một sản phẩm có vector từ ảnh và vector từ mô tả. Người dùng tìm bằng chữ, bạn tìm trên cả hai rồi hợp nhất.
- Nhiều tầng, một point. Vector rẻ để lọc thô, vector đắt để xếp lại — xem phần cascade ở 7.11.
- Chuyển đổi model. Giữ song song
v3vàv4trong cùng collection, đo, rồi mới chuyển. Từ v1.18 Qdrant cho phép tạo và xoá named vector trên collection đang chạy, nên việc này không còn đòi nạp lại từ đầu.
Cái giá. Mỗi named vector là một index HNSW riêng, tốn RAM riêng. Nếu một vector chỉ dùng để xếp lại chứ không bao giờ để tìm, hãy đặt
m: 0cho nó — Qdrant sẽ không xây đồ thị HNSW cho vector đó. Việc rescore không dùng HNSW, nên đây là RAM cho không. Đây là mẹo tiết kiệm bộ nhớ dễ bỏ sót nhất trong cả Phần 7.
Sparse vector — BM25 sống ngay trong Qdrant
Phần tiêu đề “Sparse vector — BM25 sống ngay trong Qdrant”Sparse vector là một {chỉ số: trọng số} thưa: không có số chiều cố định, chiều nào
không xuất hiện thì coi như 0. Đây chính là cách biểu diễn một dòng của inverted index
mà 3.3 đã dựng bằng tay.
Ba nguồn sinh ra sparse vector, khác nhau về chất:
| Sinh ra thế nào | Bắt được cái gì | Chi phí | |
|---|---|---|---|
| BM25 | thống kê thuần (tf, idf, độ dài) | trùng từ khoá | gần như 0 |
| SPLADE | mạng nơ-ron mở rộng từ vựng | trùng từ khoá + từ đồng nghĩa | inference nặng, vector bớt thưa |
| miniCOIL | BM25 nhân thêm một thành phần nghĩa | trùng từ khoá có phân biệt ngữ cảnh | nhẹ hơn SPLADE nhiều |
Từ v1.15.2 Qdrant sinh được biểu diễn BM25 ngay trên server từ văn bản thô, và
idf được tính trên corpus thật trong collection thay vì phải tự tính ở phía client
(v1.19 còn thêm IDF theo từng truy vấn). Nghĩa là: bạn không cần dựng riêng
Elasticsearch chỉ để có nhánh lexical của hybrid search.
Vì sao điều này quan trọng với tiếng Việt: Tầng 0
và 3.15 đã cho thấy nhánh
lexical là thứ cứu các truy vấn chứa mã định danh (E2003, POST /v1/transactions) mà
embedding luôn làm hỏng. Có BM25 nằm cùng chỗ với vector nghĩa là bạn hợp nhất được hai
nhánh trong một request, thay vì tự gọi hai hệ rồi ghép ở tầng ứng dụng.
⚠️ Cảnh báo tiếng Việt. BM25 trong Qdrant dùng tokenizer của Qdrant, không phải tokenizer tiếng Việt. Với văn bản tiếng Việt, hãy kiểm tra
tokenizer(word,whitespace,multilingual) và cân nhắcascii_folding— chi tiết ở 7.9. Đừng giả định rằng “có BM25 sẵn” là “có BM25 tiếng Việt tốt”.
miniCOIL — BM25 biết phân biệt “con dơi” với “cây gậy”
Phần tiêu đề “miniCOIL — BM25 biết phân biệt “con dơi” với “cây gậy””miniCOIL là mô hình sparse do chính Qdrant huấn luyện và mở nguồn. Ý tưởng một dòng: giữ nguyên công thức BM25, rồi nhân thêm một hệ số đo mức khớp nghĩa của từ trong ngữ cảnh cụ thể. Mỗi từ trong từ vựng được gán một vector 4 chiều biểu diễn nghĩa, và các chiều đó nằm vào các ô liền nhau của biểu diễn thưa — nên inverted index thông thường vẫn dùng được. Từ nào không có trong từ vựng thì rơi về BM25 thuần.
Số Qdrant công bố trên BEIR (số dẫn từ nguồn ngoài, nDCG@10, miniCOIL không được huấn luyện trên các tập này):
| Tập | BM25 | miniCOIL |
|---|---|---|
| MS MARCO | 0,237 | 0,244 |
| NQ | 0,304 | 0,319 |
| Quora | 0,784 | 0,802 |
| FiQA-2018 | 0,252 | 0,257 |
| HotpotQA | 0,634 | 0,633 |
Đọc bảng này cho đúng: mức cải thiện là nhỏ, và có tập nó thua. Giá trị của miniCOIL không nằm ở “hơn BM25 vài phần trăm” mà ở chỗ nó hơn mà không mất tính khái quát ngoài miền — thứ mà SPLADE hay đánh đổi. Với tiếng Việt thì chưa có số công bố; hãy coi đây là thứ đáng thử nghiệm chứ chưa phải mặc định.
Multivector — một point, một ma trận
Phần tiêu đề “Multivector — một point, một ma trận”Từ v1.10, một named vector có thể chứa nhiều vector cùng số chiều thay vì một. Đây là cách Qdrant đỡ các model late interaction như ColBERT: mỗi token một vector nhỏ.
Điểm giữa hai ma trận tính bằng max_sim — với mỗi vector của truy vấn, lấy độ tương đồng
lớn nhất với các vector của tài liệu, rồi cộng lại. Đây đúng là công thức MaxSim ở
5.6:
Cấu hình chỉ có một khoá: multivector_config: {comparator: "max_sim"}.
Cái giá là dung lượng, và nó lớn. Một đoạn 200 token thành 200 vector thay vì 1. Vì
vậy khuyến nghị của chính Qdrant — và cũng là khuyến nghị của
5.6 — là dùng multivector để xếp lại,
không phải để tìm ở tầng một. Kết hợp với mẹo m: 0 ở trên: vector ColBERT không cần
đồ thị HNSW, vì nó chỉ chấm điểm các ứng viên đã có.
Kiểu dữ liệu: chỗ tiết kiệm rẻ nhất
Phần tiêu đề “Kiểu dữ liệu: chỗ tiết kiệm rẻ nhất”Mỗi vector storage chọn được datatype:
datatype | Bit/chiều | Ghi chú |
|---|---|---|
float32 | 32 | Mặc định |
float16 | 16 | Nửa bộ nhớ, Qdrant mô tả là “gần như không ảnh hưởng chất lượng” |
uint8 | 8 | Cần embedding đã ở dạng số nguyên 0–255, hoặc để Qdrant quantization |
turbo4 | 4 | TurboQuant làm kiểu lưu trữ chính, từ v1.19; chỉ cho dense |
Chú ý phân biệt: datatype là cách lưu vector gốc; quantization_config là một bản
nén thêm vào bên cạnh vector gốc. Hai thứ khác nhau, và nhầm chúng là nguyên nhân của
tình huống “tôi bật quantization mà RAM lại tăng”. Xem 7.10.
float16 là thứ nên thử trước tiên khi cần tiết kiệm: nó rẻ, dễ hoàn tác, và không kéo
theo rescore hay oversampling như quantization.