Mục lục
- Mục tiêu bài học
- Metadata filter là gì
- 2 chiến lược thực hiện filter
- Selectivity — yếu tố quyết định chiến lược
- Filter syntax so sánh 4 Vector DB
- Composite filter — AND, OR, NOT
- 4 pattern phổ biến trong RAG
- Payload indexing — tăng tốc filter
- Hybrid search với filter
- Common pitfalls
- Ví dụ end-to-end
- Tóm tắt
- Bài tiếp theo
Mục tiêu bài học
Sau bài này bạn sẽ:
- ✅ Hiểu metadata filter hoạt động như thế nào bên dưới
- ✅ Phân biệt pre-filter và post-filter, khi nào dùng cái nào
- ✅ Biết khái niệm selectivity và tác động đến latency
- ✅ Viết được filter syntax đúng cho ChromaDB, Pinecone, Qdrant, Weaviate
- ✅ Áp dụng 4 pattern thực tế: multi-tenant, time-range, RBAC, tag
- ✅ Tránh được các lỗi phổ biến khi filter trong RAG production
Metadata filter là gì
Mỗi vector trong Vector DB thường kèm theo một metadata dict — tập key-value mô tả nguồn gốc hoặc thuộc tính của document đó. Ví dụ:
# Mỗi vector được lưu cùng metadata
{
"id": "doc-042",
"embedding": [0.12, -0.34, ...], # 1536 chiều
"metadata": {
"source": "report_q1.pdf",
"page": 7,
"author": "alice",
"team": "Engineering",
"timestamp": 1716768000, # Unix epoch
"tags": ["python", "fastapi"],
"user_id": "u123",
}
}
Query có filter nghĩa là: tìm các vector gần query vector và thỏa mãn điều kiện trên metadata. Về mặt toán học:
top-K results = argmin_{v ∈ S} distance(q, v)
trong đó S = { v | metadata(v) thỏa condition }
Đây là điểm mà Vector DB khác hoàn toàn so với thư viện ANN thuần (FAISS, Annoy). FAISS không có khái niệm metadata — nó chỉ biết vector và index. Mọi logic filter phải tự viết tay bên ngoài.
Use case thực tế
- Multi-tenant: mỗi user chỉ search trong dữ liệu của mình (
user_id = "u123") - Date range: chỉ tìm docs trong 30 ngày qua (
timestamp >= now - 30d) - Document type: chỉ search trong PDF, bỏ qua HTML (
source_type = "pdf") - Category: search trong đúng nhóm sản phẩm hoặc bộ phận
- Role-based access (RBAC): chỉ trả về docs mà user có quyền đọc
2 chiến lược thực hiện filter
Pre-filter (filter trước, search sau)
Luồng xử lý:
1. Metadata index (B-tree / inverted index)
→ tập con S thỏa condition
2. ANN search trên S
→ top-K kết quả
Ưu điểm:
- Kết quả chính xác — mọi vector trả về đều thỏa filter
- Deterministic top-K: luôn trả đúng K item nếu S có ít nhất K phần tử
- Tốt khi filter selective thấp (nhiều vector match)
Nhược điểm:
- Nếu filter rất strict → S nhỏ → ANN index không còn hiệu quả → có thể fallback linear scan
- Qdrant gọi tình huống này là "small cardinality set" và tự chuyển sang brute-force trên S
Post-filter (search trước, filter sau)
Luồng xử lý:
1. ANN search trên toàn collection
→ top-K_inflate (vd K=10 → fetch 100)
2. Lọc kết quả theo condition
→ giữ lại những item thỏa filter
Ưu điểm:
- Tận dụng tối đa ANN index → latency thấp hơn pre-filter khi filter loại ra ít vector
- Không cần dựng metadata index riêng
Nhược điểm:
- Filter strict → sau bước 2 còn rất ít item → trả về < K kết quả
- Inflate factor phải lớn để bù, tăng memory và tính toán bước 1
- Không đảm bảo trả đúng K item
Hybrid — cách Vector DB hiện đại xử lý
Qdrant, Weaviate, Pinecone đều tự động chọn chiến lược dựa trên selectivity ước tính:
- Filter match nhiều → post-filter
- Filter match ít → pre-filter (hoặc hybrid: lấy candidates từ cả 2 phía)
ChromaDB v0.5+ cũng có metadata index nhưng logic tự chọn ít tinh vi hơn.
So sánh nhanh
| Tiêu chí | Pre-filter | Post-filter |
|---|---|---|
| Luồng | metadata → ANN | ANN → metadata |
| Đảm bảo top-K | Có (nếu S ≥ K) | Không (phụ thuộc inflate) |
| Phù hợp với | Low selectivity filter | High selectivity filter |
| Nguy cơ | Brute-force khi S nhỏ | Trả < K khi filter strict |
Selectivity — yếu tố quyết định chiến lược
Selectivity = tỉ lệ vectors thỏa điều kiện filter trên tổng số vectors trong collection:
selectivity = |S| / N
Ví dụ:
N = 1,000,000 vectors
filter: user_id = "u123" → S = 5,000 vectors
selectivity = 5,000 / 1,000,000 = 0.5% (thấp)
filter: team IN ["Engineering", "Marketing"] → S = 600,000
selectivity = 600,000 / 1,000,000 = 60% (cao)
Ngưỡng thực tế cần ghi nhớ
| Selectivity | Chiến lược phù hợp | Ghi chú |
|---|---|---|
| ≥ 50% | Post-filter | ANN index hiệu quả, inflate factor nhỏ |
| 5% – 50% | Hybrid hoặc pre-filter | DB tự quyết |
| < 5% | Pre-filter cần thiết | Cần payload index trên field filter |
| < 0.1% | Skip ANN hoàn toàn | Tập S nhỏ đến mức brute-force nhanh hơn HNSW |
Vì sao quan trọng trong production: Nếu collection có 10 triệu vectors và bạn filter theo user_id (mỗi user có ~1000 docs), selectivity = 0.01%. Post-filter sẽ phải inflate K lên ~10,000 lần để đảm bảo đủ kết quả — rất tốn kém. Pre-filter với payload index là bắt buộc trong trường hợp này.
Filter syntax so sánh 4 Vector DB
Mỗi DB có cú pháp filter khác nhau nhưng đều hỗ trợ các operator tương đương. Ví dụ dưới đây thực hiện cùng 1 điều kiện: source = "doc1.pdf" VÀ page > 10.
ChromaDB (v0.5.x)
results = collection.query(
query_texts=["tìm kiếm về async trong FastAPI"],
n_results=5,
where={
"$and": [
{"source": {"$eq": "doc1.pdf"}},
{"page": {"$gt": 10}},
]
},
# where_document: filter trên nội dung text (nếu lưu)
# where_document={"$contains": "async"},
)
Operators ChromaDB: $eq, $ne, $gt, $gte, $lt, $lte, $in, $nin, $and, $or.
Lưu ý: khi chỉ có 1 condition, không cần wrap $and — viết trực tiếp where={"source": "doc1.pdf"} là tương đương $eq.
Pinecone (SDK v5+)
results = index.query(
vector=query_embedding,
top_k=5,
filter={
"$and": [
{"source": {"$eq": "doc1.pdf"}},
{"page": {"$gt": 10}},
]
},
include_metadata=True,
)
Operators Pinecone: $eq, $ne, $gt, $gte, $lt, $lte, $in, $nin, $and, $or. Cú pháp JSON giống MongoDB.
Giới hạn: Pinecone tự index metadata, nhưng chỉ hỗ trợ tối đa 40 field metadata có thể filter và mỗi array value tối đa 128 phần tử (tính đến SDK v5.4).
Qdrant (qdrant-client v1.9+)
from qdrant_client.models import (
Filter, FieldCondition, MatchValue, Range
)
results = client.query_points(
collection_name="docs",
query=query_embedding,
query_filter=Filter(
must=[
FieldCondition(
key="source",
match=MatchValue(value="doc1.pdf"),
),
FieldCondition(
key="page",
range=Range(gt=10),
),
]
),
limit=5,
)
Qdrant dùng mô hình filter tường minh hơn với 3 clause:
must: tương đương AND — tất cả điều kiện phải đúngshould: tương đương OR — ít nhất 1 điều kiện đúngmust_not: tương đương NOT — không thỏa điều kiện nào trong list
Qdrant cũng hỗ trợ MatchAny (tương đương $in), MatchExcept ($nin), và HasId để filter theo vector ID.
Weaviate (weaviate-client v4.x)
from weaviate.classes.query import Filter
collection = client.collections.get("Docs")
results = collection.query.near_vector(
near_vector=query_embedding,
limit=5,
filters=(
Filter.by_property("source").equal("doc1.pdf")
& Filter.by_property("page").greater_than(10)
),
)
for obj in results.objects:
print(obj.properties)
Weaviate v4 dùng fluent API với operator & (AND) và | (OR) — gần với cú pháp Python hơn. Các method: .equal(), .not_equal(), .greater_than(), .greater_or_equal(), .less_than(), .less_or_equal(), .contains_any(), .contains_all().
Bảng so sánh operators
| Phép toán | ChromaDB | Pinecone | Qdrant | Weaviate v4 |
|---|---|---|---|---|
| Bằng | $eq |
$eq |
MatchValue |
.equal() |
| Không bằng | $ne |
$ne |
must_not MatchValue |
.not_equal() |
| Lớn hơn | $gt |
$gt |
Range(gt=...) |
.greater_than() |
| Trong mảng | $in |
$in |
MatchAny |
.contains_any() |
| AND | $and |
$and |
must=[...] |
& |
| OR | $or |
$or |
should=[...] |
| |
Composite filter — AND, OR, NOT
Ví dụ yêu cầu: "Docs từ team Marketing HOẶC Sales, được tạo sau 2024-01-01".
ChromaDB / Pinecone (JSON operator)
where = {
"$and": [
{
"$or": [
{"team": {"$eq": "Marketing"}},
{"team": {"$eq": "Sales"}},
]
},
{"date": {"$gte": "2024-01-01"}},
]
}
Qdrant
from qdrant_client.models import Filter, FieldCondition, MatchAny, Range
query_filter = Filter(
must=[
Filter(
should=[
FieldCondition(key="team", match=MatchValue(value="Marketing")),
FieldCondition(key="team", match=MatchValue(value="Sales")),
]
),
FieldCondition(key="date", range=Range(gte="2024-01-01")),
]
)
Hoặc viết gọn hơn bằng MatchAny:
from qdrant_client.models import MatchAny
query_filter = Filter(
must=[
FieldCondition(key="team", match=MatchAny(any=["Marketing", "Sales"])),
FieldCondition(key="date", range=Range(gte="2024-01-01")),
]
)
Weaviate v4
from weaviate.classes.query import Filter
f = (
(Filter.by_property("team").equal("Marketing")
| Filter.by_property("team").equal("Sales"))
& Filter.by_property("date").greater_or_equal("2024-01-01")
)
Pitfall: lồng filter quá sâu
Mỗi tầng AND/OR thêm overhead khi DB evaluate. Nếu có thể, gộp các OR về một field thành $in / MatchAny thay vì lồng nhiều $or:
# Thay vì:
{"$or": [{"team": "Marketing"}, {"team": "Sales"}, {"team": "Finance"}]}
# Dùng:
{"team": {"$in": ["Marketing", "Sales", "Finance"]}}
# Hoặc Qdrant:
FieldCondition(key="team", match=MatchAny(any=["Marketing", "Sales", "Finance"]))
4 pattern phổ biến trong RAG
Pattern 1 — Multi-tenant
Mỗi user chỉ search trong tập dữ liệu của mình. Có 2 cách tiếp cận:
Cách 1 — Native isolation (ưu tiên): Dùng tính năng namespace/tenant có sẵn của DB.
- Pinecone:
namespace="user_u123"— vectors thuộc namespace khác nhau lưu tách hoàn toàn - Weaviate: multi-tenancy ở cấp collection, mỗi tenant có segment riêng
# Pinecone — dùng namespace thay vì filter
results = index.query(
vector=query_embedding,
top_k=5,
namespace=f"user_{user_id}", # tách hẳn, không "nhìn thấy" data user khác
)
# Weaviate — multi-tenancy
collection = client.collections.get("Docs")
tenant_collection = collection.with_tenant(f"user_{user_id}")
results = tenant_collection.query.near_vector(near_vector=query_embedding, limit=5)
Lý do prefer native: namespace lưu vector tách hoàn toàn → ANN search không duyệt qua data của tenant khác → nhanh hơn và không có nguy cơ data leak do bug filter.
Cách 2 — Metadata filter: Dùng khi DB không có namespace, hoặc khi số tenant quá lớn (hàng triệu user).
# ChromaDB — không có namespace
results = collection.query(
query_embeddings=[query_embedding],
n_results=5,
where={"user_id": user_id},
)
Lưu ý: filter theo user_id có selectivity rất thấp → cần payload index trên field user_id.
Pattern 2 — Time-based filter
Chỉ tìm docs được tạo trong N ngày gần đây.
import time
# Khi upsert: lưu timestamp là Unix epoch int (không phải ISO string)
metadata = {
"source": "report.pdf",
"timestamp": int(time.time()), # VD: 1716768000
}
# Khi query: filter 30 ngày gần nhất
now = int(time.time())
thirty_days_ago = now - 30 * 86400
# ChromaDB
where = {"timestamp": {"$gte": thirty_days_ago}}
# Qdrant
from qdrant_client.models import Range
FieldCondition(key="timestamp", range=Range(gte=thirty_days_ago))
Pitfall quan trọng: Phải nhất quán về kiểu dữ liệu. Nếu có doc lưu "timestamp": 1716768000 (int) và doc khác lưu "timestamp": "2024-05-27" (string), filter sẽ không match được phần xung đột. Chọn 1 format và enforce khi upsert.
Pattern 3 — Role-based access (RBAC)
Mỗi document có danh sách role được phép đọc. Chỉ trả về docs mà role của user hiện tại có trong danh sách đó.
# Khi upsert doc — lưu danh sách role được phép
metadata = {
"title": "Q4 Financial Report",
"allowed_roles": ["admin", "finance", "manager"],
}
# Khi query — user có role "manager"
user_role = "manager"
# Pinecone
filter = {"allowed_roles": {"$in": [user_role]}}
# ChromaDB — dùng $contains (array chứa phần tử)
where = {"allowed_roles": {"$contains": user_role}}
# Qdrant — MatchAny trên array field
from qdrant_client.models import MatchAny
FieldCondition(key="allowed_roles", match=MatchAny(any=[user_role]))
Lưu ý: behavior khi filter trên array field khác nhau giữa các DB. Pinecone $in kiểm tra "mảng metadata có chứa ít nhất 1 phần tử trong list filter". ChromaDB $contains kiểm tra "mảng metadata chứa đúng phần tử đó". Đọc kỹ docs của từng DB trước khi dùng array filter trong production.
Pattern 4 — Tag/Category filter
Phổ biến trong knowledge base phân loại theo chủ đề.
# Khi upsert
metadata = {
"category": "engineering",
"tags": ["python", "fastapi", "async"],
}
# Query: tìm trong category engineering
where_category = {"category": "engineering"}
# Query: tìm docs có tag "fastapi" (bất kể có tag gì khác)
# Pinecone
where_tag = {"tags": {"$in": ["fastapi"]}}
# ChromaDB
where_tag = {"tags": {"$contains": "fastapi"}}
# Query: tìm docs có CẢ "fastapi" VÀ "async"
# Qdrant — MatchAll
from qdrant_client.models import MatchAll # qdrant-client >= 1.7
FieldCondition(key="tags", match=MatchAll(except_=None, all=["fastapi", "async"]))
# Hoặc dùng 2 FieldCondition riêng trong must=[]
Payload indexing — tăng tốc filter
Không phải mọi metadata field đều được index mặc định. Khi filter trên field không có index, DB phải scan tuyến tính — latency tăng theo O(N). Quy tắc đơn giản: field nào dùng để filter thường xuyên thì cần index.
Qdrant — tạo payload index thủ công
from qdrant_client.models import PayloadSchemaType
# Index field "user_id" — dạng keyword (exact match)
client.create_payload_index(
collection_name="docs",
field_name="user_id",
field_schema=PayloadSchemaType.KEYWORD,
)
# Index field "timestamp" — dạng integer (range query)
client.create_payload_index(
collection_name="docs",
field_name="timestamp",
field_schema=PayloadSchemaType.INTEGER,
)
# Index field "tags" — dạng keyword cho array
client.create_payload_index(
collection_name="docs",
field_name="tags",
field_schema=PayloadSchemaType.KEYWORD,
)
Qdrant hỗ trợ: keyword, integer, float, bool, datetime, text (full-text). Có thể tạo nhiều index, mỗi field là 1 index riêng.
Pinecone — tự động index
Pinecone tự index mọi metadata field khi upsert, không cần cấu hình thêm. Giới hạn thực tế: tối đa 40 field có thể filter per index (tính đến tháng 5/2026). Nếu vượt quá, field thứ 41+ sẽ không thể filter.
ChromaDB — không có payload index riêng
ChromaDB v0.5.x không có API tạo index riêng cho metadata. Mọi filter là scan tuyến tính trên tập kết quả candidates. Với collection nhỏ (< 100k vectors) điều này ổn, nhưng ở quy mô lớn hơn sẽ là bottleneck. Đây là 1 trong những lý do ChromaDB được định vị cho prototype hơn là production quy mô lớn.
Weaviate — inverted index tự động
Weaviate tự build inverted index cho mọi property khi định nghĩa schema. Có thể tắt index cho field không cần filter để tiết kiệm bộ nhớ:
import weaviate.classes.config as wc
# Khi tạo collection — tắt index cho field "raw_text" (không cần filter)
client.collections.create(
name="Docs",
properties=[
wc.Property(name="source", data_type=wc.DataType.TEXT),
wc.Property(name="page", data_type=wc.DataType.INT),
wc.Property(
name="raw_text",
data_type=wc.DataType.TEXT,
index_filterable=False, # không tạo inverted index
index_searchable=False,
),
],
)
Tóm tắt payload indexing
| DB | Cách tạo index | Mặc định |
|---|---|---|
| Qdrant | create_payload_index() |
Không index |
| Pinecone | Tự động | Index tất cả (giới hạn 40 field) |
| ChromaDB | Không có API | Scan linear |
| Weaviate | Tự động (có thể tắt) | Index tất cả |
Hybrid search với filter
Hybrid search kết hợp vector similarity và BM25 (keyword search). Filter metadata có thể áp dụng lên cả hybrid query.
Weaviate hybrid + filter
from weaviate.classes.query import Filter, HybridFusion
results = collection.query.hybrid(
query="async endpoint trong FastAPI",
alpha=0.5, # 0 = BM25 thuần, 1 = vector thuần, 0.5 = cân bằng
fusion_type=HybridFusion.RELATIVE_SCORE,
limit=5,
filters=Filter.by_property("team").equal("Engineering"),
)
for obj in results.objects:
print(obj.properties["source"], obj.metadata.score)
Qdrant sparse + dense + filter
Qdrant v1.7+ hỗ trợ hybrid qua query_points với prefetch:
from qdrant_client.models import (
Filter, FieldCondition, MatchValue,
Prefetch, FusionQuery, Fusion,
)
results = client.query_points(
collection_name="docs",
prefetch=[
Prefetch(query=dense_vector, using="dense", limit=20),
Prefetch(query=sparse_vector, using="sparse", limit=20),
],
query=FusionQuery(fusion=Fusion.RRF),
query_filter=Filter(
must=[FieldCondition(key="team", match=MatchValue(value="Engineering"))]
),
limit=5,
)
Hybrid search kết hợp với filter tốt cho RAG khi user query vừa mang ngữ nghĩa (cần vector) vừa có keyword cụ thể (cần BM25), đồng thời cần giới hạn domain bởi metadata.
Common pitfalls
1. Filter trên field không có index
Triệu chứng: latency tăng tuyến tính theo N. P50 có vẻ ổn (10ms), nhưng P95/P99 tệ hơn nhiều (500ms+) vì outlier tập dataset lớn. Luôn đo P95, không chỉ P50.
Fix: tạo payload index cho field đó (xem mục 8).
2. Metadata type không nhất quán
# Doc A: timestamp là int
{"timestamp": 1716768000}
# Doc B: timestamp là string (upsert nhầm)
{"timestamp": "2024-05-27"}
# Filter này sẽ bỏ sót Doc B
where = {"timestamp": {"$gte": 1716768000}}
Fix: validate và enforce kiểu trước khi upsert. Dùng Pydantic model để đảm bảo:
from pydantic import BaseModel
class DocMetadata(BaseModel):
source: str
page: int
timestamp: int # bắt buộc int
user_id: str
tags: list[str] = []
# Validate trước khi upsert
meta = DocMetadata(**raw_metadata)
collection.add(embeddings=[...], metadatas=[meta.model_dump()])
3. Filter quá strict — trả về 0 kết quả
Xảy ra khi filter loại hết mọi vector. RAG pipeline cần xử lý trường hợp này:
def rag_query_with_fallback(query, user_id, category, days=30):
now = int(time.time())
cutoff = now - days * 86400
# Thử filter đầy đủ
results = collection.query(
query_texts=[query],
n_results=5,
where={
"$and": [
{"user_id": user_id},
{"category": category},
{"timestamp": {"$gte": cutoff}},
]
},
)
# Fallback: bỏ time filter nếu không có kết quả
if not results["ids"][0]:
results = collection.query(
query_texts=[query],
n_results=5,
where={
"$and": [
{"user_id": user_id},
{"category": category},
]
},
)
return results
4. Lưu raw text vào metadata
# SAI — lưu text vào metadata
metadata = {
"source": "doc.pdf",
"full_text": "nội dung tài liệu dài hàng nghìn ký tự...", # tệ
}
# ĐÚNG — chỉ lưu pointer
metadata = {
"source": "doc.pdf",
"s3_key": "docs/doc.pdf#page=7", # load khi cần
"snippet": "đoạn trích 200 ký tự", # cho display
}
Metadata được load vào RAM cùng với vector index. Text vài MB/vector nhân với hàng triệu vector = vấn đề bộ nhớ nghiêm trọng. Lưu text thực sự trong object store (S3, GCS) và chỉ load khi cần trả về context cho LLM.
5. Quên include_metadata trong response
# Pinecone — mặc định không trả metadata
results = index.query(vector=[...], top_k=5)
# results[0].metadata → None
# Phải thêm flag
results = index.query(
vector=[...],
top_k=5,
include_metadata=True, # bắt buộc
)
ChromaDB và Qdrant trả metadata mặc định. Pinecone yêu cầu include_metadata=True tường minh.
Ví dụ end-to-end
RAG pipeline với ChromaDB, filter theo user_id + timestamp + category. Dùng sentence-transformers để tạo embedding (không cần API key).
Setup và dữ liệu
import time
import chromadb
from sentence_transformers import SentenceTransformer
# Khởi tạo
model = SentenceTransformer("all-MiniLM-L6-v2")
client = chromadb.Client() # ephemeral
collection = client.create_collection("company_docs")
# 20 docs giả lập với metadata đa dạng
docs = [
{"id": "d01", "text": "FastAPI async endpoint giảm latency tới 40%", "user_id": "u123", "category": "engineering", "timestamp": int(time.time()) - 2 * 86400},
{"id": "d02", "text": "Pydantic v2 cải thiện validation performance 5-50x", "user_id": "u123", "category": "engineering", "timestamp": int(time.time()) - 5 * 86400},
{"id": "d03", "text": "Q1 2024 doanh thu tăng 20% so với cùng kỳ", "user_id": "u123", "category": "finance", "timestamp": int(time.time()) - 10 * 86400},
{"id": "d04", "text": "Vector DB so sánh: Qdrant vs Weaviate vs Pinecone", "user_id": "u123", "category": "engineering", "timestamp": int(time.time()) - 40 * 86400}, # quá 30 ngày
{"id": "d05", "text": "RAG pipeline tối ưu với metadata filtering", "user_id": "u123", "category": "engineering", "timestamp": int(time.time()) - 1 * 86400},
{"id": "d06", "text": "Chiến lược marketing Q2 cho sản phẩm AI", "user_id": "u456", "category": "marketing", "timestamp": int(time.time()) - 3 * 86400},
{"id": "d07", "text": "Docker multi-stage build giảm image size 60%", "user_id": "u123", "category": "engineering", "timestamp": int(time.time()) - 7 * 86400},
{"id": "d08", "text": "HNSW index: M=16 ef_construction=100 là baseline tốt", "user_id": "u123", "category": "engineering", "timestamp": int(time.time()) - 3 * 86400},
]
# Upsert hàng loạt
embeddings = model.encode([d["text"] for d in docs]).tolist()
collection.add(
ids=[d["id"] for d in docs],
embeddings=embeddings,
documents=[d["text"] for d in docs],
metadatas=[
{
"user_id": d["user_id"],
"category": d["category"],
"timestamp": d["timestamp"],
}
for d in docs
],
)
print(f"Đã upsert {len(docs)} docs")
Query với composite filter
def rag_retrieve(query_text: str, user_id: str, category: str, days: int = 30):
now = int(time.time())
cutoff = now - days * 86400
query_embedding = model.encode([query_text]).tolist()
results = collection.query(
query_embeddings=query_embedding,
n_results=3,
where={
"$and": [
{"user_id": user_id},
{"category": category},
{"timestamp": {"$gte": cutoff}},
]
},
include=["documents", "metadatas", "distances"],
)
hits = results["ids"][0]
if not hits:
print("Không tìm thấy kết quả với filter này.")
return []
output = []
for doc_id, text, meta, dist in zip(
results["ids"][0],
results["documents"][0],
results["metadatas"][0],
results["distances"][0],
):
output.append({
"id": doc_id,
"text": text,
"category": meta["category"],
"days_ago": (now - meta["timestamp"]) // 86400,
"distance": round(dist, 4),
})
return output
# Test: user u123, category engineering, 30 ngày qua
hits = rag_retrieve(
query_text="tìm tài liệu về vector database và search optimization",
user_id="u123",
category="engineering",
days=30,
)
for h in hits:
print(f"[{h['id']}] ({h['days_ago']} ngày trước, dist={h['distance']}) {h['text'][:60]}")
Output minh họa
Đã upsert 8 docs
[d08] (3 ngày trước, dist=0.2134) HNSW index: M=16 ef_construction=100 là baseline tốt
[d05] (1 ngày trước, dist=0.2891) RAG pipeline tối ưu với metadata filtering
[d02] (5 ngày trước, dist=0.3412) Pydantic v2 cải thiện validation performance 5-50x
Doc d04 (40 ngày trước) bị loại bởi time filter. Doc d03 (finance) và d06 (user u456) không xuất hiện vì filter category và user_id.
Cùng query nhưng dùng Qdrant
from qdrant_client import QdrantClient
from qdrant_client.models import (
Distance, VectorParams, PointStruct,
Filter, FieldCondition, MatchValue, Range,
PayloadSchemaType,
)
qclient = QdrantClient(":memory:")
qclient.create_collection(
collection_name="company_docs",
vectors_config=VectorParams(size=384, distance=Distance.COSINE),
)
# Tạo payload index trước khi upsert
for field, schema in [
("user_id", PayloadSchemaType.KEYWORD),
("category", PayloadSchemaType.KEYWORD),
("timestamp", PayloadSchemaType.INTEGER),
]:
qclient.create_payload_index("company_docs", field, schema)
# Upsert
points = [
PointStruct(
id=i,
vector=embeddings[i],
payload={
"doc_id": docs[i]["id"],
"text": docs[i]["text"],
"user_id": docs[i]["user_id"],
"category": docs[i]["category"],
"timestamp": docs[i]["timestamp"],
},
)
for i in range(len(docs))
]
qclient.upsert(collection_name="company_docs", points=points)
# Query với filter
now = int(time.time())
cutoff = now - 30 * 86400
query_vec = model.encode(["vector database search optimization"]).tolist()[0]
results = qclient.query_points(
collection_name="company_docs",
query=query_vec,
query_filter=Filter(
must=[
FieldCondition(key="user_id", match=MatchValue(value="u123")),
FieldCondition(key="category", match=MatchValue(value="engineering")),
FieldCondition(key="timestamp", range=Range(gte=cutoff)),
]
),
limit=3,
)
for pt in results.points:
print(f"[{pt.payload['doc_id']}] score={pt.score:.4f} {pt.payload['text'][:60]}")
Tóm tắt
- Metadata filter = tìm vector gần query và thỏa điều kiện trên metadata
- Pre-filter (metadata → ANN) phù hợp với low-selectivity; post-filter (ANN → metadata) phù hợp với high-selectivity
- Selectivity < 1%: cần payload index; < 0.1%: có thể bỏ qua ANN hoàn toàn
- 4 DB dùng cú pháp khác nhau — ChromaDB/Pinecone dùng JSON operators, Qdrant dùng typed models, Weaviate v4 dùng fluent API
- Multi-tenant: ưu tiên namespace/tenant native thay vì filter trên
user_id - Time filter: lưu Unix epoch int, không lưu ISO string
- Tạo payload index cho field filter thường dùng (đặc biệt với Qdrant)
- Không lưu raw text vào metadata; chỉ lưu pointer
- Luôn đo P95 latency khi có filter, không chỉ P50
