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

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:

  1. Đ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.
  2. Nhiều tầng, một point. Vector rẻ để lọc thô, vector đắt để xếp lại — xem phần cascade ở 7.11.
  3. Chuyển đổi model. Giữ song song v3v4 trong 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: 0 cho 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 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 index3.3 đã dựng bằng tay.

Ba nguồn sinh ra sparse vector, khác nhau về chất:

Sinh ra thế nàoBắt được cái gìChi phí
BM25thống kê thuần (tf, idf, độ dài)trùng từ khoágần như 0
SPLADEmạng nơ-ron mở rộng từ vựngtrùng từ khoá + từ đồng nghĩainference nặng, vector bớt thưa
miniCOILBM25 nhân thêm một thành phần nghĩatrùng từ khoá có phân biệt ngữ cảnhnhẹ 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 03.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ắc ascii_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ậpBM25miniCOIL
MS MARCO0,2370,244
NQ0,3040,319
Quora0,7840,802
FiQA-20180,2520,257
HotpotQA0,6340,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.

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:

score(Q,D)=qQ  maxdD  sim(q,d)\mathrm{score}(Q, D) = \sum_{q \in Q} \; \max_{d \in D} \; \mathrm{sim}(q, d)

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:

datatypeBit/chiềuGhi chú
float3232Mặc định
float1616Nửa bộ nhớ, Qdrant mô tả là “gần như không ảnh hưởng chất lượng”
uint88Cần embedding đã ở dạng số nguyên 0–255, hoặc để Qdrant quantization
turbo44TurboQuant làm kiểu lưu trữ chính, từ v1.19; chỉ cho dense

Chú ý phân biệt: datatypecách lưu vector gốc; quantization_configmộ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.

Phần 7 — Qdrant