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

7.13 Filtering: mệnh đề và điều kiện

7.8 nói filter được thực thi thế nào. 7.9 nói phải đánh index gì. Mục này là bảng tra cú pháp, cộng với một cái bẫy đủ nghiêm trọng để nó là lý do chính mục này tồn tại.

Filter là một cây, và cây đó lồng nhau tuỳ ý:

Mệnh đềTương đươngNghĩa
mustANDmọi điều kiện phải thoả
shouldORít nhất một điều kiện thoả
must_notNOTkhông điều kiện nào được thoả
min_should“ít nhất N trong số”chỉ định số lượng should tối thiểu

min_should là thứ hay bị bỏ quên. Nó cho phép diễn đạt “khớp ít nhất 2 trong 5 thẻ” mà không phải liệt kê thủ công mọi tổ hợp.

Cú phápNghĩaCần index
match: {value: x}bằng đúng xkeyword / integer / bool
match: {any: [...]}INnhư trên
match: {except: [...]}NOT INnhư trên
match: {prefix: "..."}bắt đầu bằngkeyword có bật prefix (từ v1.19)
match: {text: "..."}chứa các tokentext
match: {text_any: "..."}chứa bất kỳ token nào (từ v1.16)text
match: {phrase: "..."}khớp cụm đúng thứ tựtext có bật phrase_matching

Phân biệt texttext_any rất đáng nhớ: text yêu cầu mọi token của truy vấn có mặt (giống AND); text_any chỉ cần một (giống OR). Nhầm hai cái này là nguyên nhân của cả “lọc quá chặt, không ra gì” lẫn “lọc quá lỏng, ra tất cả”.

range: {gt, gte, lt, lte} cho integer / float, và cùng cú pháp đó cho datetime với giá trị theo RFC 3339. Nếu ngày là trục lọc chính, nhớ is_principal: true (7.9).

geo_bounding_box (hình chữ nhật), geo_radius (hình tròn, bán kính tính bằng mét), geo_polygon (đa giác, có thể khoét lỗ bằng interiors).

Điều kiệnNghĩaBẫy
is_empty: {key: f}trường thiếu, null, hoặc mảng rỗngKhác is_null
is_null: {key: f}trường tồn tại và có giá trị nullKhác is_empty
values_count: {gt, lt, …}lọc theo số phần tử của mảngRất hữu ích, hay bị quên
has_id: [...]lọc theo ID cụ thểKhông dùng được trong nested
has_vector: "ten"điểm có named vector này khôngHữu ích khi migrate model
slice: {index, total}chia collection thành phần rời nhau, xác định (v1.19)Cho quét song song, xem 7.11

is_emptyis_null khác nhau và người ta nhầm liên tục. Nếu bạn muốn “những điểm chưa được gán phòng ban”, trong đó có cả điểm thiếu trường lẫn điểm có trường bằng null, thì is_empty mới là cái bạn cần.

has_vector đáng chú ý trong quy trình đổi model embedding: nạp vector v4 dần dần, rồi dùng has_vector: "v4" để biết còn bao nhiêu điểm chưa xử lý.

Đây là lý do chính mục này tồn tại. Nó âm thầm, phổ biến, và cho ra kết quả sai mà trông đúng.

Giả sử payload:

{ "phe_duyet": [ {"nguoi": "an", "trang_thai": "tu_choi"},
{"nguoi": "binh","trang_thai": "dong_y"} ] }

Bạn muốn: “tài liệu có người an đã đồng ý.” Viết theo bản năng:

{ "must": [ {"key": "phe_duyet[].nguoi", "match": {"value": "an"}},
{"key": "phe_duyet[].trang_thai", "match": {"value": "dong_y"}} ] }

Điểm này khớp. Nhưng nó không đúng ý bạn: an có mặt (ở phần tử 1) và dong_y có mặt (ở phần tử 2) — hai điều kiện thoả ở hai phần tử khác nhau. Qdrant, như phần lớn kho tài liệu, mặc định làm phẳng mảng trước khi lọc.

Cách đúng là nested:

{ "must": [ { "nested": { "key": "phe_duyet",
"filter": { "must": [ {"key": "nguoi", "match": {"value": "an"}},
{"key": "trang_thai", "match": {"value": "dong_y"}} ] } } } ] }

nested áp bộ điều kiện lên từng phần tử một, và điểm chỉ khớp nếu có ít nhất một phần tử thoả toàn bộ. Đó mới là “an đã đồng ý”.

Quy tắc nhớ: hễ payload có mảng object và bạn ràng buộc hai trường trở lên bên trong cùng một phần tử, bạn phải dùng nested. Nếu không, bạn đang lọc lỏng hơn mình nghĩ — và không có gì báo cho bạn biết.

Hạn chế: has_idslice không dùng được bên trong nested.

  1. Mọi trường trong filter phải có payload index. Không phải để nhanh — mà để cơ chế ở 7.8 hoạt động.
  2. Filter càng chặt càng nên đo. Trực giác “lọc bớt thì nhanh hơn” đúng với SQL, không luôn đúng với vector search: filter chặt làm đứt đồ thị. Đo cặp (c)(d) ở 7.8.
  3. must_not trên trường cardinality cao là dấu hiệu xấu. “Tất cả trừ X” thường ước lượng cardinality kém và đẩy query planner vào lựa chọn tồi. Nếu diễn đạt lại được thành must thì nên làm.
Phần 7 — Qdrant