Vector DB 6 — Hybrid search & Reranking
Vì sao chỉ vector search chưa đủ?
Suốt series này (từ Vector DB 2 — Embedding & độ tương tự) ta xây dựng tìm kiếm dựa trên dense embedding: nén văn bản thành một vector, đo cosine, trả về những đoạn "gần nghĩa" nhất. Cách này giỏi hiểu ý: hỏi "hồ sơ giải ngân cần giấy tờ gì" vẫn tìm ra đoạn viết "chứng từ chứng minh mục đích sử dụng vốn" dù không trùng một từ nào. Đây chính là sức mạnh mà từ khoá thuần không có.
Nhưng dense embedding có một điểm yếu cố hữu, và trong ngân hàng nó rất đau: khớp chính xác từ khoá, mã, tên riêng, số hợp đồng. Vì sao?
- Thuật ngữ hiếm / out-of-vocabulary: mô hình embedding học từ ngữ liệu chung. Với một mã sản phẩm nội bộ như
NCB-VAY-TC-2024hay điều khoảnĐiều 7.3.b, mô hình chưa từng "hiểu" token này — nó bị nén vào một vùng vector nhiễu, gần như ngẫu nhiên. Hai mã khác nhau một ký tự có thể nằm sát nhau trong không gian vector. - Khớp chính xác (exact match): người dùng gõ số hợp đồng
HD-2024-08815mong tìm đúng bản ghi đó. Dense search trả về "những hợp đồng na ná" — sai hoàn toàn nhu cầu. Ở đây ta cần so khớp ký tự, không phải "gần nghĩa". - Từ khoá hiếm nhưng quyết định: một truy vấn dài trong đó chỉ một từ là mấu chốt (ví dụ "phí tất toán trước hạn") dễ bị embedding "trung bình hoá" làm loãng, khiến đoạn chứa đúng từ khoá đó không được ưu tiên.
- Số, ngày, đơn vị: "lãi suất 8,5%", "kỳ hạn 36 tháng" — dense embedding xử lý số rất kém, dễ nhầm 8,5% với 5,8%.
Ngược lại, tìm kiếm từ khoá kinh điển (lexical/full-text) như BM25 lại giỏi đúng những chỗ dense thua: nó khớp token chính xác, ưu tiên từ hiếm (IDF cao), tìm đúng số hợp đồng ngay lập tức. Nhưng nó mù nghĩa: hỏi "giải ngân" không ra "cho vay", hỏi "ô tô" không ra "xe hơi".
Hai phương pháp bù trừ cho nhau gần như hoàn hảo. Đó là lý do của tìm kiếm lai (hybrid search): chạy cả hai, rồi hợp nhất.
| Sparse / lexical (BM25) | Dense / semantic (vector) | |
|---|---|---|
| Khớp từ khoá, mã, số HĐ | Rất tốt | Kém |
| Hiểu đồng nghĩa, diễn giải | Kém | Rất tốt |
| Từ hiếm / OOV | Tốt (IDF cao) | Kém |
| Ngôn ngữ tự nhiên mơ hồ | Trung bình | Tốt |
| Cần training | Không | Có (mô hình embedding) |
Sparse vs dense: hai họ retriever
Sparse (thưa) biểu diễn văn bản bằng vector rất dài (bằng kích thước từ điển, hàng chục nghìn chiều) nhưng hầu hết bằng 0 — chỉ các token xuất hiện mới có trọng số. BM25 là hàm chấm điểm lexical chuẩn ngành (nền tảng của full-text search trong PostgreSQL, Elasticsearch, OpenSearch): điểm cao khi truy vấn và tài liệu chia sẻ token, có trọng số theo tần suất từ (TF), độ hiếm (IDF) và độ dài tài liệu. Không cần GPU, không cần train.
Dense (dày) là embedding đã bàn kỹ ở Vector DB 2: vector ngắn (768–1024 chiều), mọi chiều đều có giá trị, mã hoá ngữ nghĩa. Tìm bằng ANN (xem Vector DB 3).
Một hướng lai ngay ở tầng biểu diễn là learned sparse như SPLADE: dùng mô hình ngôn ngữ để sinh ra vector thưa nhưng có mở rộng từ khoá — nó tự thêm các token liên quan (kiểu đồng nghĩa) vào biểu diễn thưa, nên vừa giữ khả năng exact-match của sparse vừa có chút ngữ nghĩa. SPLADE chấm điểm bằng chỉ mục nghịch đảo (inverted index) như BM25 nhưng thông minh hơn. Đây là điểm giữa BM25 và dense; một số engine hỗ trợ sparse-dense learned song song.
Hợp nhất kết quả: fusion
Chạy hai retriever xong ta có hai danh sách xếp hạng — nhưng điểm số của chúng không cùng thang (BM25 cho điểm ~0–30 không chuẩn hoá; cosine cho ~0–1). Không thể cộng thẳng. Có ba cách hợp nhất phổ biến.
1. Reciprocal Rank Fusion (RRF)
RRF là cách được ưa dùng nhất vì đơn giản và bền vững: nó bỏ qua điểm số tuyệt đối, chỉ dùng thứ hạng (rank). Với mỗi tài liệu, điểm RRF là tổng nghịch đảo hạng của nó trong từng danh sách:
$$\text{RRF}(d) = \sum_{i \in \text{retrievers}} \frac{1}{k + \text{rank}_i(d)}$$
với k là hằng số làm mượt (thường k=60). Tài liệu đứng đầu ở cả hai danh sách được cộng dồn hai lần → lên top. Ưu điểm: không cần chuẩn hoá điểm, chống nhiễu tốt, gần như "cắm là chạy". Đây là mặc định an toàn cho hybrid.
# Minh hoạ RRF (KHÔNG phải SQL chạy được) — hợp nhất 2 danh sách theo thứ hạng
def rrf(list_bm25, list_dense, k=60):
scores = {}
for rank, doc in enumerate(list_bm25, start=1):
scores[doc] = scores.get(doc, 0) + 1.0 / (k + rank)
for rank, doc in enumerate(list_dense, start=1):
scores[doc] = scores.get(doc, 0) + 1.0 / (k + rank)
return sorted(scores, key=scores.get, reverse=True)
2. Weighted score (kết hợp có trọng số)
Chuẩn hoá điểm mỗi retriever về [0,1] (min-max), rồi cộng có trọng số: score = α·dense_norm + (1−α)·sparse_norm. α chỉnh cán cân nghiêng về nghĩa hay từ khoá (ví dụ tra cứu điều khoản pháp lý nên tăng phần lexical). Linh hoạt hơn RRF nhưng nhạy cảm với cách chuẩn hoá và phải tinh chỉnh α theo dữ liệu.
3. Convex combination
Là dạng tổng quát của weighted với các trọng số không âm và cộng lại bằng 1 (α + β = 1). Về bản chất giống weighted score nhưng ràng buộc trọng số để điểm hợp nhất nằm trong khoảng có ý nghĩa. Nhiều engine (Weaviate, một số bản OpenSearch) dùng chính công thức này cho tham số alpha của hybrid.
Thực chiến: bắt đầu bằng RRF (ít tham số, khó sai). Chỉ chuyển sang weighted/convex khi bạn có tập đánh giá và muốn tinh chỉnh cán cân nghĩa–từ khoá.
Metadata filtering: lọc trước, rồi mới tương tự
Hybrid không chỉ là "sparse + dense". Trong thực tế, lọc theo metadata (đã bàn ở Vector DB 4) thường là bước bắt buộc trước khi so khớp: giới hạn không gian tìm theo phòng ban / loại tài liệu / thời hạn hiệu lực / ngôn ngữ, rồi mới tính similarity trong tập đã lọc.
Điều này vừa đúng nghiệp vụ (một cán bộ tín dụng chỉ được xem tài liệu phòng Tín dụng còn hiệu lực) vừa tăng độ chính xác (thu hẹp ứng viên nhiễu) và giảm tải. Cần dùng filtered ANN (lọc trong lúc duyệt đồ thị) thay vì post-filter thô để tránh bẫy "top-N không đủ k sau lọc". Kết hợp đủ ba tầng: filter (metadata) → hybrid retrieve (sparse+dense) → rerank.
Reranking: chấm lại top-K cho chính xác
Fusion cho ta một danh sách hợp nhất tốt về recall (đúng đoạn cần nằm đâu đó trong top-100). Nhưng thứ tự trong top vẫn chưa tối ưu về precision — đoạn đúng nhất có thể đứng thứ 7 thay vì thứ 1. Đây là lúc reranking vào cuộc.
Cốt lõi là sự khác biệt giữa hai kiến trúc chấm điểm:
- Bi-encoder (chính là embedding): mã hoá truy vấn và tài liệu độc lập thành hai vector, rồi đo khoảng cách. Rất nhanh — vector tài liệu tính sẵn, truy vấn chỉ so một lần với chỉ mục. Đổi lại kém tinh: truy vấn và tài liệu không "nhìn thấy nhau" khi mã hoá. Hợp cho recall trên cả kho.
- Cross-encoder (reranker): đưa cặp (truy vấn, tài liệu) vào cùng một lượt qua mô hình, cho phép mọi token của truy vấn "chú ý" tới mọi token của tài liệu → chấm điểm liên quan chính xác hơn hẳn. Đổi lại chậm: mỗi cặp là một lần chạy mô hình, không tính sẵn được. Không thể chạy trên cả triệu tài liệu.
Cách ghép hai thế mạnh: bi-encoder (nhanh) lọc lấy top-K cho recall → cross-encoder (chậm nhưng tinh) chấm lại K đó cho precision. Vì K nhỏ (20–100), chi phí cross-encoder chấp nhận được. Đây là pipeline retrieve → rerank kinh điển.
Các reranker phổ biến: Cohere Rerank (API managed), bge-reranker (mã nguồn mở, self-host được — quan trọng cho ngân hàng), các dòng cross-encoder trên nền BERT. Chúng nhận (query, danh sách đoạn) và trả điểm liên quan để sắp xếp lại.
Ví dụ pseudocode toàn pipeline (minh hoạ, không phải SQL chạy được):
# Minh hoạ pipeline hybrid + rerank (KHÔNG phải SQL chạy được)
def search(query, dept, top_k=50, top_n=5):
flt = {"department": dept, "active": True} # 1) lọc metadata
bm25_hits = lexical_search(query, filter=flt, k=top_k) # 2a) sparse
dense_hits = vector_search(embed(query), filter=flt, k=top_k) # 2b) dense
fused = rrf(bm25_hits, dense_hits, k=60)[:top_k] # 3) hợp nhất
scored = reranker.rank(query, [d.text for d in fused]) # 4) cross-encoder
return sorted(scored, reverse=True)[:top_n] # 5) top-N cho LLM
Trong một hệ RAG, chính top-N sau rerank này là ngữ cảnh (context) đưa vào LLM — chất lượng rerank ảnh hưởng trực tiếp tới chất lượng câu trả lời.
Đánh giá: đo cải thiện, không "cảm tính"
Đừng thêm hybrid/rerank rồi tin là tốt hơn — phải đo trên một tập đánh giá (truy vấn kèm đoạn đúng đã gán nhãn). Ba chỉ số cốt lõi:
- Recall@k: trong top-k trả về, có bao nhiêu % chứa đoạn đúng. Đo khả năng "không bỏ sót" — retriever/hybrid chịu trách nhiệm ở đây.
- Precision@k / MRR: đoạn đúng có đứng cao không (MRR = trung bình nghịch đảo hạng của kết quả đúng đầu tiên). Đo "đúng ngay top" — reranker cải thiện mạnh chỉ số này.
- nDCG@k (normalized Discounted Cumulative Gain): chỉ số ranking đầy đủ nhất, thưởng cho việc đặt đoạn liên quan cao lên đầu, chiết khấu theo vị trí. Là thước đo chuẩn để so sánh trước–sau.
Cách làm đúng: đo baseline dense-only, rồi +hybrid, rồi +rerank, xem từng bước nDCG/recall tăng bao nhiêu. Điển hình: hybrid nâng recall (bắt được truy vấn từ khoá/mã), rerank nâng precision/nDCG (đưa đoạn đúng lên đầu). Nếu một bước không cải thiện đo được → cân nhắc bỏ để tiết kiệm độ trễ.
Đánh đổi độ trễ vs chất lượng
Mỗi tầng thêm chất lượng nhưng cũng thêm độ trễ:
- Dense-only: nhanh nhất (một truy vấn ANN, ~vài ms).
- + Sparse (hybrid): chạy song song hai retriever + fusion → thêm ít ms; nếu chạy song song thì độ trễ ≈ nhánh chậm hơn.
- + Rerank: cross-encoder trên K đoạn là phần nặng nhất — chấm 50 cặp (query, đoạn) có thể tốn 50–200 ms tuỳ mô hình và phần cứng (CPU vs GPU). K càng lớn càng chậm.
Các cần điều chỉnh: K (số ứng viên đưa vào rerank — giảm K để nhanh hơn, đổi lấy rủi ro bỏ sót), kích thước reranker (mô hình nhỏ nhanh nhưng kém tinh), batch + GPU, và cache kết quả cho truy vấn lặp. Nguyên tắc: chỉ trả tiền độ trễ khi đo được cải thiện chất lượng tương xứng với SLA của ứng dụng.
Use case thực tế
Bối cảnh: Trợ lý tra cứu nội bộ NCB (dựng ở Vector DB 3, chạy trên Postgres + pgvector) gặp phàn nàn: người dùng tìm "phí tất toán sản phẩm NCB-VAY-TC-2024" hoặc "điều khoản của hợp đồng HD-2024-08815" thì kết quả sai — dense trả về "những khoản vay tín chấp na ná", không ra đúng mã/số hợp đồng. Trong khi câu hỏi ngữ nghĩa như "vay không cần tài sản đảm bảo" lại rất tốt. Đúng điểm mù của dense-only.
Giải pháp — thêm hybrid + rerank:
- Sparse: bật full-text (BM25) trong Postgres trên cùng nội dung chunk, tận dụng chỉ mục nghịch đảo để khớp chính xác mã sản phẩm và số hợp đồng.
- Filter metadata trước:
department,doc_type,valid_until— giới hạn theo quyền và hiệu lực. - Fusion: mỗi nhánh lấy top-50, hợp nhất bằng RRF (k=60) → top-50 hợp nhất.
- Rerank: bge-reranker self-host (giữ dữ liệu on-prem, tuân thủ) chấm lại 50 đoạn → lấy top-5 đưa vào LLM.
Đo trước–sau trên tập 300 truy vấn nghiệp vụ có gán nhãn:
| Cấu hình | Recall@50 | nDCG@10 | p95 độ trễ |
|---|---|---|---|
| Dense-only (baseline) | 0,86 | 0,71 | 12 ms |
| + Hybrid (RRF) | 0,96 | 0,79 | 18 ms |
| + Rerank (bge) | 0,96 | 0,91 | 95 ms |
(Số liệu minh hoạ cho use case.) Hybrid vá đúng lỗ hổng mã/số hợp đồng (recall 0,86 → 0,96); rerank đẩy đoạn đúng lên đầu (nDCG 0,71 → 0,91). Độ trễ tăng lên ~95 ms — vẫn dưới ngưỡng chấp nhận cho tra cứu nội bộ (không phải luồng giao dịch realtime). Kết quả: người dùng vừa tra được theo nghĩa, vừa khớp chính xác mã sản phẩm/điều khoản/số hợp đồng — đúng nhu cầu ngân hàng.
Ghi nhớ
- Dense-only chưa đủ: kém ở khớp chính xác từ khoá, mã sản phẩm, tên riêng, số hợp đồng, số/ngày và thuật ngữ hiếm (OOV). Lexical (BM25) mạnh đúng chỗ đó nhưng mù nghĩa → hai bên bù trừ.
- Hybrid search = chạy sparse (BM25/full-text) + dense (vector) rồi hợp nhất. SPLADE là learned-sparse, điểm giữa hai bên.
- Fusion: RRF (dùng thứ hạng, không cần chuẩn hoá điểm, mặc định an toàn —
k≈60); weighted / convex combination (chuẩn hoá + trọng sốα, linh hoạt nhưng phải tinh chỉnh). - Lọc metadata trước (phòng ban/loại/hiệu lực) rồi mới similarity — dùng filtered ANN, không post-filter thô.
- Reranking: bi-encoder nhanh cho recall trên cả kho → cross-encoder (Cohere Rerank, bge-reranker) chậm nhưng tinh, chấm lại top-K cho precision. Pipeline: filter → hybrid retrieve → rerank → top-N.
- Đánh giá bắt buộc: đo recall@k, precision/MRR, nDCG@k trước–sau từng tầng. Hybrid nâng recall, rerank nâng precision/nDCG.
- Đánh đổi độ trễ: rerank là phần nặng nhất (tỉ lệ với K); chỉnh K, kích thước reranker, GPU, cache. Chỉ trả tiền độ trễ khi cải thiện chất lượng đo được.
Bài viết liên quan
Phân biệt AI/ML/DL, các kiểu học máy, quy trình ML end-to-end và thuật ngữ nền tảng.
Chất lượng dữ liệu, xử lý thiếu/ngoại lai, mã hoá, chuẩn hoá, tạo đặc trưng và tránh data leakage.
Mô hình ngôn ngữ lớn hoạt động ra sao, token, context window, tham số sinh, và kỹ thuật viết prompt.
Retrieval-Augmented Generation, vector database, tool calling, agent và MCP để xây ứng dụng LLM thực tế.