RAG nâng cao 3 — Hybrid Search: kết hợp từ khoá & vector
Ở RAG nâng cao 1 — Tổng quan ta đã đặt vấn đề: chất lượng câu trả lời của RAG phụ thuộc gần như hoàn toàn vào chất lượng retrieval — nếu đoạn văn bản đưa vào ngữ cảnh sai, LLM dù giỏi đến đâu cũng trả lời sai. Ở RAG nâng cao 2 — Chunking & Indexing ta đã cắt và đánh chỉ mục tài liệu cho tốt. Bài này giải quyết câu hỏi tiếp theo: truy hồi bằng cách nào? Câu trả lời thực chiến gần như luôn là hybrid search — kết hợp tìm kiếm từ khoá và tìm kiếm vector.
Vì sao chỉ vector search là chưa đủ
Vector search (dense retrieval) nén mỗi đoạn văn bản thành một vector vài trăm đến hơn nghìn chiều, rồi đo độ gần (cosine) để tìm đoạn "gần nghĩa" nhất. Nó giỏi ngữ nghĩa: hỏi "hồ sơ giải ngân cần giấy tờ gì" vẫn 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ừ. Đây là thứ mà tìm kiếm từ khoá thuần không làm được (chi tiết nền tảng ở LLM 3 — Embeddings & Vector).
Nhưng dense retrieval có điểm yếu cố hữu, và trong ngân hàng nó rất đau:
- Mã, số hiệu, tên riêng, viết tắt. Người dùng gõ số văn bản
QĐ-1502/2024/NCBhay mã sản phẩmNCB-VAY-TC-2024mong tìm đúng bản ghi đó. Mô hình embedding học từ ngữ liệu chung, chưa từng "hiểu" các token nội bộ này — chúng 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; ngược lại đúng mã cần tìm lại không được ưu tiên. - Khớp chính xác (exact match). Với số hợp đồng
HD-2024-08815, nhu cầu là so khớp ký tự, không phải "gần nghĩa". Dense search trả về "những hợp đồng na ná" — sai hoàn toàn. - Từ hiếm nhưng quyết định. Một truy vấn dài trong đó chỉ một từ là mấu chốt ("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á đó tụt hạng.
- 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%.
Tìm kiếm từ khoá kinh điển (lexical / full-text) như BM25 lại giỏi đúng những chỗ dense thua: khớp token chính xác, ưu tiên từ hiếm, 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 hybrid search: chạy cả hai, rồi hợp nhất kết quả.
| Tiêu chí | 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 / ngoài từ điển (OOV) | Tốt (IDF cao) | Kém |
| Ngôn ngữ tự nhiên mơ hồ | Trung bình | Tốt |
| Cần train mô hình | Không | Có (mô hình embedding) |
BM25 và sparse embeddings — nguyên lý ngắn gọn
Sparse (thưa) biểu diễn văn bản bằng một 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ỉ token nào xuất hiện mới có trọng số. Chấm điểm chạy trên inverted index (chỉ mục nghịch đảo): với mỗi token, engine biết ngay tài liệu nào chứa nó, nên rất nhanh và không cần GPU.
BM25 (Best Matching 25) là hàm chấm điểm lexical chuẩn ngành. Trực giác của điểm BM25 gồm ba thành phần:
- Tần suất từ (TF) — từ khoá xuất hiện càng nhiều trong tài liệu, điểm càng cao, nhưng bão hoà (xuất hiện 10 lần không gấp 10 lần một lần — tránh spam từ khoá).
- Độ hiếm (IDF) — từ càng hiếm trong toàn bộ kho, càng "đắt giá". "tất toán" hiếm hơn "và", nên khớp "tất toán" đáng giá hơn nhiều.
- Chuẩn hoá độ dài tài liệu — tài liệu dài tự nhiên chứa nhiều từ hơn; BM25 phạt độ dài để tài liệu dài không thắng oan.
BM25 không học gì, chỉ là công thức thống kê trên kho văn bản — nên nó bắt đúng mã, số, tên riêng mà không cần "hiểu" chúng.
Learned sparse (SPLADE) là bước tiến: dùng một mô hình ngôn ngữ để sinh vector thưa nhưng có mở rộng từ khoá — tự thêm các token liên quan (kiểu đồng nghĩa) vào biểu diễn, rồi vẫn chấm điểm trên inverted index như BM25. Nó là điểm giữa: giữ khả năng exact-match của sparse, thêm chút ngữ nghĩa. Đổi lại phải chạy mô hình khi index và query. Với đa số dự án, BM25 + dense đã đủ tốt; SPLADE dành cho khi cần đẩy thêm recall.
Kết hợp kết quả: RRF và trọng số chuẩn hoá
Vấn đề cốt lõi của hybrid: hai nhánh trả về hai thang điểm không so sánh được. Điểm BM25 có thể là 12,7; điểm cosine là 0,83. Không thể cộng thẳng. Có hai hướng giải quyết.
Reciprocal Rank Fusion (RRF)
RRF là cách phổ biến nhất vì đơn giản và không cần cân chỉnh thang điểm — nó bỏ qua điểm số tuyệt đối, chỉ dùng thứ hạng (rank). Với mỗi tài liệu d, điểm hợp nhất là:
RRF(d) = Σ 1 / (k + rank_i(d))
(mỗi danh sách i mà d xuất hiện)
Trong đó rank_i(d) là thứ hạng của d trong danh sách kết quả thứ i (1 = hạng nhất), và k là hằng số làm mượt, mặc định phổ biến k = 60.
Trực giác:
- Tài liệu đứng đầu một danh sách đóng góp
1/(60+1) ≈ 0,0164; đứng hạng 2 đóng góp1/(60+2) ≈ 0,0161; hạng 50 chỉ1/110 ≈ 0,0091. Nghĩa là được xếp cao ở bất kỳ nhánh nào đều có giá trị, và chênh lệch giữa các hạng đầu là nhỏ dần — tránh việc một nhánh áp đảo. - Tài liệu xuất hiện ở cả hai nhánh được cộng dồn hai lần → nổi lên trên. Đây chính là hiệu ứng mong muốn: thứ vừa khớp từ khoá vừa gần nghĩa là ứng viên tốt nhất.
kcàng lớn thì càng "san bằng" khác biệt giữa các thứ hạng;knhỏ thì ưu ái mạnh cho hạng đầu.
RRF an toàn vì không giả định gì về phân phối điểm, chịu được việc một nhánh cho điểm "kỳ dị". Nhược điểm: vì bỏ điểm tuyệt đối, nó không phân biệt được "khớp rất mạnh" với "khớp vừa" nếu cùng thứ hạng.
Trọng số điểm chuẩn hoá (weighted / convex)
Cách thứ hai: chuẩn hoá điểm mỗi nhánh về cùng thang (ví dụ min-max về [0,1]) rồi cộng có trọng số:
score(d) = α · norm(dense) + (1 − α) · norm(bm25)
α (0..1) chỉnh độ nghiêng về ngữ nghĩa hay từ khoá. Cách này giữ được thông tin "khớp mạnh yếu" nhưng nhạy với chuẩn hoá và cần tinh chỉnh α theo từng tập dữ liệu/loại truy vấn. Trong thực tế, nhiều đội bắt đầu bằng RRF (không tham số), chỉ chuyển sang weighted khi có tập đánh giá để tinh chỉnh α.
Metadata filtering: lọc kết hợp với search
Trong ngân hàng, retrieval gần như không bao giờ chạy trên "toàn bộ kho". Ta luôn kèm bộ lọc metadata: phòng ban, ngày hiệu lực, loại tài liệu, và đặc biệt độ mật (classification). Có hai chiến lược:
- Pre-filter (lọc trước): thu hẹp tập ứng viên theo metadata rồi mới search trong tập đó. Ưu điểm: đảm bảo tuyệt đối không lộ tài liệu ngoài quyền, kết quả top-k luôn đủ số lượng trong phạm vi cho phép. Nhược: nếu bộ lọc quá hẹp, chỉ mục ANN có thể kém hiệu quả (phải quét nhiều).
- Post-filter (lọc sau): search trước, rồi bỏ đi kết quả không thoả metadata. Ưu: tận dụng tối đa tốc độ index. Nhược: nếu lọc mạnh, top-k ban đầu có thể bị lọc gần hết, còn quá ít kết quả — phải nới
klên nhiều.
Với ràng buộc độ mật, nguyên tắc an toàn là pre-filter (hoặc filter ngay trong lúc duyệt index nếu engine hỗ trợ): người dùng không được quyền xem tài liệu "Mật" thì tài liệu đó phải bị loại trước khi vào bảng xếp hạng, không bao giờ dựa vào bước lọc sau. Nhiều vector DB hiện đại hỗ trợ filtered ANN search — nhúng bộ lọc vào quá trình duyệt đồ thị/danh sách, vừa an toàn vừa nhanh.
Luồng hybrid search
Hybrid thường chỉ là bước đầu của pipeline retrieval. Nó tối ưu recall — kéo được nhiều đoạn đúng vào top-N. Nhưng thứ tự trong top-N còn thô. Bước sau là reranking bằng cross-encoder để tối ưu precision ở đầu bảng — chi tiết ở RAG nâng cao 4 — Reranking. Mẫu hình chuẩn là: hybrid retrieve rộng (N = 50–100) → rerank → giữ top-k nhỏ (k = 3–8) đưa vào LLM.
Pseudocode: chạy hai nhánh song song rồi hợp nhất
Đoạn dưới chỉ minh hoạ logic RRF, không phải API của một thư viện cụ thể:
# MINH HOẠ — không phải API thật của thư viện nào
def hybrid_search(query, filters, top_n=50, k_rrf=60, top_k=8):
# 1) Hai nhánh chạy song song, cùng áp pre-filter metadata
bm25_hits = bm25_index.search(query, filter=filters, limit=top_n)
vector_hits = vector_index.search(embed(query), filter=filters, limit=top_n)
# 2) Reciprocal Rank Fusion: gộp theo thứ hạng
scores = {}
for rank, doc in enumerate(bm25_hits, start=1):
scores[doc.id] = scores.get(doc.id, 0) + 1.0 / (k_rrf + rank)
for rank, doc in enumerate(vector_hits, start=1):
scores[doc.id] = scores.get(doc.id, 0) + 1.0 / (k_rrf + rank)
# 3) Sắp xếp giảm dần, lấy top-k cho bước rerank
fused = sorted(scores.items(), key=lambda kv: kv[1], reverse=True)
return [doc_id for doc_id, _ in fused[:top_k]]
Điểm cần nhớ trong pseudocode: cả hai nhánh đều nhận cùng filters (metadata), và RRF chỉ dùng biến rank chứ không đụng tới điểm gốc của từng nhánh.
Minh hoạ lexical bằng Postgres full-text
PostgreSQL có full-text search sẵn (không cần extension): to_tsvector biến văn bản thành danh sách token đã chuẩn hoá, plainto_tsquery biến truy vấn thành điều kiện, toán tử @@ kiểm tra khớp, và ts_rank cho điểm giống tinh thần BM25 (TF + độ hiếm). Đây là cách nhánh sparse có thể triển khai ngay trong Postgres (kết hợp với cột vector của pgvector để làm hybrid trong cùng một CSDL). Câu dưới chạy được trên sandbox — xếp hạng khách theo mức khớp từ khoá trong tên:
-- ▶ Chạy được
SELECT id, full_name,
ts_rank(to_tsvector('simple', full_name),
plainto_tsquery('simple', 'nguyen')) AS rank
FROM customers
WHERE to_tsvector('simple', full_name) @@ plainto_tsquery('simple', 'nguyen')
ORDER BY rank DESC
LIMIT 5;
Đây chỉ là minh hoạ nhánh lexical trên dữ liệu sandbox; hệ thống thật sẽ chạy full-text trên nội dung tài liệu và ghép với nhánh vector.
Hạ tầng: những engine hỗ trợ hybrid
Không cần dựng hai hệ thống riêng — nhiều engine đã hỗ trợ cả sparse lẫn dense (xem thêm góc nhìn retrieval ở Vector DB 7 — RAG Retrieval):
| Engine | Sparse (BM25) | Dense (vector) | Ghi chú |
|---|---|---|---|
| Elasticsearch / OpenSearch | Có (mặc định) | Có (kNN) | Hỗ trợ RRF sẵn; mạnh về filter & full-text |
PostgreSQL (pgvector + tsvector) | to_tsvector/ts_rank | pgvector | Hybrid trong một CSDL, tiện cho đội đã có Postgres |
| Qdrant | Sparse vectors | Có | Filtered search tốt, hỗ trợ RRF/fusion |
| Weaviate | BM25 tích hợp | Có | hybrid query với alpha (weighted) |
| Milvus | Sparse vectors | Có | Hybrid + nhiều loại index ANN |
Chọn engine nào phụ thuộc hạ tầng sẵn có: đội đã vận hành Elasticsearch cho log/search thì tận dụng luôn; đội "Postgres-centric" thì pgvector + tsvector giảm được một hệ thống phải bảo trì.
Tiếng Việt cần tokenizer cho BM25
Một lưu ý sống còn với dữ liệu tiếng Việt: BM25 khớp theo token, mà tiếng Việt không tách từ bằng khoảng trắng như tiếng Anh — "ngân hàng", "tất toán", "giải ngân" là các từ ghép hai âm tiết. Nếu để tokenizer mặc định cắt theo khoảng trắng, "ngân" và "hàng" thành hai token rời, chất lượng lexical giảm mạnh. Cần bộ phân tích từ tiếng Việt (ví dụ tách từ dựa trên từ điển/mô hình như VnCoreNLP, underthesea, hoặc analyzer tiếng Việt của engine) ở cả bước index lẫn query. Ngoài ra nên xử lý dấu tiếng Việt nhất quán (đừng bỏ dấu tuỳ tiện, vì "má/ma/mà" khác nghĩa) và chuẩn hoá viết hoa. Đây là bước hay bị bỏ quên khiến "hybrid nhưng BM25 gần như vô dụng".
Use case thực tế
Bối cảnh — trợ lý tra cứu văn bản nội bộ NCB. Kho gồm khoảng 12.000 tài liệu: quy trình tín dụng, quyết định, thông báo lãi suất, sản phẩm huy động/cho vay, công văn. Cán bộ chi nhánh hỏi bằng ngôn ngữ tự nhiên lẫn mã/số hiệu — ví dụ:
- "Điều kiện vay tín chấp cho khách hàng hưởng lương qua NCB là gì?" (ngữ nghĩa)
- "Cho tôi QĐ-1502/2024/NCB về biểu phí tất toán" (số hiệu chính xác)
- "Sản phẩm NCB-VAY-TC-2024 áp dụng lãi suất bao nhiêu?" (mã + số)
Chỉ dùng vector search (đo trên ~200 truy vấn có nhãn của đội nghiệp vụ): recall@10 tốt cho câu ngữ nghĩa nhưng sập với nhóm mã/số hiệu — số hiệu bị nén thành vector nhiễu, đoạn đúng thường không lọt top-10. Recall@10 tổng hợp ước lượng ~0,68.
Sau khi bật hybrid (BM25 + vector, RRF k=60): nhánh BM25 bắt chính xác QĐ-1502/2024/NCB và NCB-VAY-TC-2024 ngay hạng đầu; nhánh vector vẫn lo phần ngữ nghĩa. Recall@10 tổng hợp lên ~0,89 — cải thiện rõ nhất đúng ở nhóm mã/số hiệu trước đây gần như thất bại. Thêm bước rerank cross-encoder ở RAG 4 đẩy precision@3 (đoạn thật sự dùng được ở top-3) từ ~0,55 lên ~0,78.
(Các con số trên là ước lượng minh hoạ theo bậc độ lớn thường gặp khi bật hybrid, không phải số đo chính thức của NCB.)
Vì sao cần metadata filter theo độ mật. Cùng một câu hỏi, một chuyên viên chi nhánh và một cán bộ hội sở có quyền xem khác nhau. Tài liệu gắn nhãn độ mật (Công khai / Nội bộ / Mật) và phòng ban chủ quản. Ta pre-filter theo độ mật + quyền người dùng trước khi retrieve, nên tài liệu "Mật" không bao giờ lọt vào bảng xếp hạng của người không đủ quyền — an toàn không phụ thuộc bước lọc sau. Ngày hiệu lực cũng được lọc để không trả về biểu phí đã hết hiệu lực. Cách tiếp cận filter-trước này khớp với yêu cầu kiểm soát truy cập dữ liệu ở Governance — Quyền riêng tư & tuân thủ.
Ghi nhớ
- Vector giỏi nghĩa, yếu khớp chính xác; BM25 giỏi khớp chính xác (mã, số hiệu, tên riêng), mù nghĩa. Hybrid ghép cả hai để bù trừ — gần như luôn thắng từng cái riêng lẻ.
- BM25 = tần suất từ (bão hoà) + độ hiếm (IDF) + chuẩn hoá độ dài; không cần train. SPLADE là learned sparse có mở rộng từ khoá, mạnh hơn nhưng tốn hơn.
- RRF hợp nhất theo thứ hạng (
1/(k+rank), k≈60), không cần cân chỉnh thang điểm — mặc định an toàn. Weighted dùng điểm chuẩn hoá + trọng sốα, giữ được độ mạnh yếu nhưng phải tinh chỉnh. - Metadata filtering là bắt buộc trong ngân hàng; với độ mật, ưu tiên pre-filter (hoặc filtered ANN) để không bao giờ lộ tài liệu ngoài quyền.
- Hybrid là bước đầu (recall); nối tiếp bằng rerank (precision) → RAG 4. Mẫu: retrieve N=50–100 → rerank → top-k=3–8.
- Tiếng Việt cần tách từ cho BM25 (VnCoreNLP/underthesea/analyzer), xử lý dấu nhất quán — bỏ qua bước này thì nhánh BM25 gần như vô dụng.
- Nhiều engine hỗ trợ hybrid sẵn: Elasticsearch/OpenSearch (RRF), Postgres
pgvector+tsvector, Qdrant, Weaviate, Milvus — chọn theo hạ tầng đang có.
Nguồn tham khảo
- Robertson, S. & Zaragoza, H. (2009), The Probabilistic Relevance Framework: BM25 and Beyond — Foundations and Trends in Information Retrieval (tài liệu nền tảng của BM25).
- Cormack, G. V., Clarke, C. L. A. & Büttcher, S. (2009), Reciprocal Rank Fusion Outperforms Condorcet and Individual Rank Learning Methods — SIGIR 2009 (nguồn gốc của RRF).
- Formal, T., Piwowarski, B. & Clinchant, S. (2021), SPLADE: Sparse Lexical and Expansion Model for First Stage Ranking — SIGIR 2021.
- PostgreSQL Documentation — Chapter: "Full Text Search" (
to_tsvector,plainto_tsquery,ts_rank). - pgvector — README/Documentation (GitHub: pgvector/pgvector)
- Elasticsearch Documentation — "Reciprocal rank fusion (RRF)" và "k-nearest neighbor (kNN) search".
- Weaviate Documentation — "Hybrid search" (tham số
alpha, fusion).
Bài viết liên quan
Đặt nền cho chuỗi AI: phân biệt ba vòng tròn lồng nhau AI ⊃ ML ⊃ DL và khác biệt bản chất giữa lập trình truyền thống với học từ dữ liệu. Giới thiệu ba kiểu học máy (supervised, unsupervised, reinforcement), phân loại descriptive/predictive/prescriptive, quy trình ML end-to-end, chia train/validation/test, overfitting/underfitting và các thuật ngữ nền tảng, gắn với ứng dụng ngân hàng NCB.
Harness — lớp scaffolding quanh model (vòng lặp, tool, context, memory, verify, sub-agent) — mới là thứ quyết định agent chạy được hay chỉ là demo. Bài này mổ xẻ giải phẫu một harness, 7 kỹ năng cốt lõi khi xây agent, single vs multi-agent (kèm số liệu hiệu quả/chi phí), các repo nên dùng, và một quickstart Python dựng-là-chạy cho bối cảnh ngân hàng.
Hiểu LLM từ gốc: bản chất dự đoán token, ba giai đoạn huấn luyện (pretraining, fine-tuning, RLHF), token, context window và các tham số sinh (temperature, top-p). Nắm hiện tượng hallucination và kỹ thuật prompt engineering (vai trò, few-shot, chain-of-thought, ràng buộc đầu ra), kèm ví dụ gọi API model Claude mới nhất với adaptive thinking.
Vì sao dữ liệu quyết định chất lượng mô hình hơn cả thuật toán. Bài này đi qua toàn bộ pipeline chuẩn bị dữ liệu: phân loại dữ liệu, làm sạch (thiếu/ngoại lai/trùng lặp), mã hoá hạng mục, scaling, feature engineering, giảm chiều, và cách phòng data leakage — soi qua bài toán chấm điểm tín dụng.
Cảm nhận của bạn
Bình luận
Chưa có bình luận. Hãy là người đầu tiên chia sẻ!