7.8 Filterable HNSW và query planner
Tầng 5 đã gọi tên cái bẫy: filter + vector search. Mục này là bản đầy đủ, và nó là thứ Qdrant làm khác nhất so với một thư viện ANN.
Đây cũng là mục có nhiều hệ RAG đang âm thầm mất recall nhất mà không ai biết, vì hỏng kiểu này không sinh ra lỗi — nó chỉ trả về ít kết quả hơn, hoặc kết quả tệ hơn.
Vấn đề, phát biểu chính xác
Phần tiêu đề “Vấn đề, phát biểu chính xác”Bạn muốn: “10 chunk giống câu hỏi nhất, trong phòng ban kế toán.”
Hai cách hiển nhiên, cả hai đều sai:
Post-filter — tìm trước, lọc sau
Phần tiêu đề “Post-filter — tìm trước, lọc sau”HNSW lấy top-100 trên toàn corpus → lọc phòng ban = kế toán → còn lại 2 kết quảNếu kế toán chỉ chiếm 1% corpus thì kỳ vọng chỉ có 1 điểm trong 100 điểm đầu thoả filter. Bạn xin 10, nhận về 1. Và không có lỗi nào được báo.
Đây là chế độ hỏng nguy hiểm nhất vì nó im lặng và phụ thuộc dữ liệu: hôm nay phòng kế toán chiếm 30% corpus thì mọi thứ ổn; tháng sau bạn nạp thêm tài liệu phòng khác, tỷ lệ tụt xuống 2%, và chất lượng sập mà không ai đổi một dòng code nào.
Pre-filter ngây thơ — lọc trước, quét sau
Phần tiêu đề “Pre-filter ngây thơ — lọc trước, quét sau”lọc phòng ban = kế toán → được 200.000 điểm → brute-force cosine trên 200.000 điểmĐúng về kết quả, nhưng bạn vừa vứt bỏ toàn bộ lý do dùng ANN.
Và cách thứ ba, cũng sai: đi HNSW rồi bỏ qua node không khớp
Phần tiêu đề “Và cách thứ ba, cũng sai: đi HNSW rồi bỏ qua node không khớp”Nghe hợp lý: đi đồ thị như bình thường, gặp node không thoả filter thì không nhận nó vào kết quả nhưng vẫn đi tiếp qua nó.
Nó hỏng vì một lý do đồ thị: filter chặt làm đồ thị đứt thành nhiều mảnh. HNSW đi được là nhờ các cạnh nối; nếu 99% node bị loại, các node còn lại có thể không còn đường nối với nhau. Greedy descent mắc kẹt trong một mảnh nhỏ và không bao giờ tới được mảnh chứa đáp án.
Cách Qdrant giải: thêm cạnh vào đồ thị
Phần tiêu đề “Cách Qdrant giải: thêm cạnh vào đồ thị”Ý tưởng của Qdrant, một dòng:
Nếu filter làm đứt đồ thị, hãy dựng sẵn thêm những cạnh nối các điểm cùng giá trị payload lại với nhau.
Đồ thị HNSW gốc Đồ thị có thêm cạnh theo payload(● = kế toán, ○ = khác)
○───○───● ○───○───● │ │ │ │ │ │╲ ● ○ ○ lọc ● ⇒ ● ○ ○ ╲ │ │ │ ba mảnh │╲__│___│__╲ ○───●───○ rời rạc ○───●───○ (cạnh phụ nối ● với ●) ⇒ các ● vẫn thông nhauQdrant dựng các đồ thị con theo từng giá trị payload rồi gộp chúng trở lại đồ thị đầy đủ.
Tham số điều khiển số cạnh phụ này là payload_m (mặc định null = dùng m).
Hệ quả cực kỳ quan trọng, và là lỗi triển khai phổ biến nhất trong cả Phần 7:
⚠️ Cạnh phụ chỉ được tạo cho những trường đã có payload index tại thời điểm xây đồ thị HNSW. Tạo payload index sau khi đã nạp dữ liệu ⇒ đồ thị hiện có không có cạnh phụ, và toàn bộ cơ chế này không hoạt động — dù
GET /collections/{name}vẫn báo là index đã tồn tại.Cách đúng: tạo payload index trước khi nạp dữ liệu. Cách chữa nếu lỡ: buộc dựng lại HNSW bằng cách tăng
ef_constructlên 1 (7.7).
Query planner: ba đường, chọn theo cardinality
Phần tiêu đề “Query planner: ba đường, chọn theo cardinality”Qdrant không luôn dùng filterable HNSW. Trước mỗi truy vấn có filter, nó ước lượng cardinality — có bao nhiêu điểm thoả filter — rồi chọn một trong ba đường:
ước lượng số điểm thoả filter │ ┌───────────────────────┼───────────────────────┐ ▼ ▼ ▼ RẤT ÍT VỪA PHẢI RẤT NHIỀU (< full_scan_threshold) (gần như cả corpus) │ │ │ ▼ ▼ ▼ Bỏ HNSW hoàn toàn. Filterable HNSW: HNSW thường, Dùng payload index đi đồ thị có cạnh phụ, bỏ qua node lấy tập điểm rồi chỉ nhận node thoả không khớp chấm điểm trực tiếp filter khi duyệt │ │ │ recall = 1.0 recall cao recall cao và nhanh hơn HNSW (filter loại ít)Ba nhận xét về sơ đồ này:
- Nhánh trái cho recall hoàn hảo. Filter chặt không phải là tin xấu — nó là tin tốt, miễn là bạn có payload index. Không có index thì Qdrant không ước lượng được cardinality và không đi được nhánh này.
- Nhánh phải rẻ vì filter loại ít nên đồ thị gần như không đứt.
- Nhánh giữa là chỗ khó, và là lý do filterable HNSW tồn tại.
Ngưỡng chuyển giữa nhánh trái và giữa là full_scan_threshold_kb, mặc định 10000 KB
(≈ 10.000 vector 256 chiều, hoặc ≈ 1.700 vector 1536 chiều — xem
7.4).
ACORN — khi cạnh phụ vẫn không đủ
Phần tiêu đề “ACORN — khi cạnh phụ vẫn không đủ”Từ v1.16, Qdrant có thêm chiến lược ACORN, dựa trên paper cùng tên (SIGMOD 2024).
Cạnh phụ theo payload hoạt động tốt khi filter là một điều kiện trên một trường đã index. Nó đuối trong hai tình huống:
- Nhiều filter chặt kết hợp —
phòng ban = kế toán AND ngôn ngữ = vi AND năm = 2026. Không có đồ thị con nào được dựng sẵn cho giao của ba điều kiện. - Nhiều điểm đã xoá mềm — đồ thị đầy lỗ thủng mà vacuum chưa kịp dọn (7.6).
ACORN xử lý bằng cách nhìn xa hơn một bước: khi các láng giềng trực tiếp đều bị filter loại, nó xét láng giềng của láng giềng để bắc cầu qua vùng chết. Đổi lại là latency cao hơn. Đây là đánh đổi có ý thức, không phải bữa trưa miễn phí.
Cách bạn tự phát hiện mình đang bị
Phần tiêu đề “Cách bạn tự phát hiện mình đang bị”Bài kiểm tra, làm được trong nửa giờ, và nên làm trước khi bạn nghi ngờ bất cứ thứ gì khác:
Với cùng một bộ truy vấn, đo bốn số:
(a) recall@k không filter, exact: true ← trần tuyệt đối (b) recall@k không filter, HNSW ← ANN lấy đi bao nhiêu (c) recall@k CÓ filter, exact: true ← trần khi có filter (d) recall@k CÓ filter, HNSW ← số thật của productionĐọc kết quả:
| Quan sát | Chẩn đoán |
|---|---|
| (a) ≈ (b) và (c) ≈ (d) | Lành mạnh. Vấn đề của bạn nằm chỗ khác |
| (a) ≈ (b) nhưng (c) ≫ (d) | Đúng bệnh của mục này. Filter đang phá HNSW |
| (a) ≫ (b) | Bệnh HNSW thuần — tăng hnsw_ef (7.7) |
| (a) thấp | Bệnh không nằm ở Qdrant. Xem 5.4 |
Điểm mấu chốt: hầu hết người ta chỉ đo (b). Đo recall khi không filter rồi triển khai một hệ luôn luôn có filter là cách đo sai phổ biến nhất khi dùng vector database — và nó khớp đúng với tinh thần “bảy lỗi phương pháp” ở Phần 2 — 7.
Nếu (c) ≫ (d), làm gì
Phần tiêu đề “Nếu (c) ≫ (d), làm gì”Theo thứ tự, từ rẻ tới đắt:
- Kiểm payload index có tồn tại không. Không có index thì không có gì trong mục này hoạt động cả.
- Kiểm index được tạo trước hay sau khi nạp dữ liệu. Nếu sau: buộc dựng lại HNSW
(
ef_construct+1). - Tăng
hnsw_efcho các truy vấn có filter. Rẻ, hiệu lực ngay. - Tăng
full_scan_threshold_kbđể filter chặt rơi vào nhánh quét thẳng — recall 1.0. - Bật ACORN nếu bạn kết hợp nhiều filter chặt.
- Xem lại
payload_mnếu trường lọc chính có ít giá trị phân biệt.