Chào anh em, lại là Hải đây. Hôm nay ngồi code xong vài dòng script, tiện tay pha ấm trà lào đặc, lại nhớ cái thời còn vật vã với mấy cái search index lởm khởm. Giờ thì khác rồi, anh em mình “chơi” tới Hybrid Retrieval rồi, chứ không còn lẹt đẹt kiểu thuần túy nữa. Bài này, mình sẽ mổ xẻ sâu vào cái gọi là Hybrid Retrieval: Dense + Sparse Retrieval Strategies. Mục tiêu không gì khác ngoài việc kết hợp sức mạnh của BM25 (Sparse) và ANN (Dense) để cho ra kết quả search nó “ngon lành” hơn, đồng thời cân bằng giữa độ chính xác và latency.
Mình sẽ vào vai Hải “Architect” với cái nhìn tổng quan về cách các khái niệm này kết hợp với nhau, rồi luồn sâu vào chi tiết kỹ thuật từng phần.
Hybrid Retrieval: Khi Sparse và Dense “bắt tay” nhau cho kết quả search “chất” hơn
Mấy hôm trước, có một bạn hỏi mình về cách làm sao để cái chức năng search trên hệ thống của bạn ấy nó thông minh hơn. Hiện tại, hệ thống đang dùng BM25, kết quả trả về thì có cái đúng, có cái sai, user phàn nàn nhiều. Muốn scale lên, xử lý lượng request 10.000 user/giây mà vẫn giữ được độ chính xác là cả một bài toán.
Anh em mình ai làm mảng search backend cũng hiểu, có hai trường phái chính để tìm kiếm tài liệu/thông tin:
- Sparse Retrieval: Tiêu biểu là các thuật toán dựa trên term frequency (TF) và inverse document frequency (IDF), mà nổi tiếng nhất là BM25. Cái này nó “cơ bắp” ở chỗ hiểu rõ từng từ khóa, mức độ quan trọng của nó trong từng văn bản và toàn bộ tập dữ liệu. Nó tốt cho việc match chính xác từ khóa.
- Dense Retrieval: Sử dụng các mô hình học sâu (Deep Learning) để nhúng (embed) văn bản thành các vector số học. Các vector này nắm bắt ý nghĩa ngữ nghĩa (semantic meaning) của văn bản. Tìm kiếm ở đây là đi tìm các vector gần nhất trong không gian nhiều chiều. Tiêu biểu là các kỹ thuật dùng Approximate Nearest Neighbor (ANN) search như Faiss, Annoy, Pinecone, Weaviate,… để tìm kiếm vector hiệu quả. Cái này nó “tinh tế” ở chỗ hiểu được ý nghĩa tổng thể, cả những từ khóa người dùng không gõ ra nhưng ý đồ lại tương đồng.
Vấn đề là, cả hai phương pháp đều có điểm mạnh và điểm yếu riêng.
- Sparse (BM25):
- Mạnh: Match từ khóa chính xác, hiểu rõ sự xuất hiện của từ, hiệu quả với các truy vấn ngắn, có tính từ khóa cao. Tốc độ phản hồi thường nhanh.
- Yếu: Không hiểu ngữ nghĩa sâu sắc. Sai từ đồng nghĩa, sai ngữ cảnh. Một câu “apple pie recipe” có thể không match với “how to make apple tart” dù ý nghĩa tương đồng.
- Dense (ANN):
- Mạnh: Nắm bắt ngữ nghĩa, hiểu các từ đồng nghĩa, ý nghĩa tương đồng. Phù hợp với các truy vấn dài, phức tạp, mang tính mô tả.
- Yếu: Cần tài nguyên tính toán lớn để tạo và tìm kiếm embedding. Có thể “lạc đề” nếu query quá ngắn hoặc mang tính kỹ thuật cao mà embedding model không được huấn luyện tốt cho domain đó. Latency khi scale lớn có thể trở thành vấn đề nếu không có cơ sở hạ tầng ANN tối ưu. Dễ bị ảnh hưởng bởi “mù từ” khi từ khóa quan trọng bị lặp lại quá nhiều trong một tài liệu.
Thế thì làm sao để “ăn cả hai cái bánh”?
Đó chính là lúc Hybrid Retrieval lên tiếng. Ý tưởng cốt lõi là kết hợp điểm mạnh của Sparse và Dense để khắc chế điểm yếu của nhau.
1. Kiến trúc tổng thể của một hệ thống Hybrid Retrieval
Để anh em dễ hình dung, mình vẽ cái sơ đồ luồng đi của dữ liệu thế này:
graph TD
A[User Query] --> B{Query Preprocessing};
B --> C[Sparse Retrieval (BM25)];
B --> D[Dense Retrieval (ANN)];
C --> E{Re-Ranking};
D --> E;
E --> F[Final Ranked Results];
F --> G[User Response];
Giải thích luồng:
- User Query: Người dùng nhập vào thanh tìm kiếm.
- Query Preprocessing: Chuẩn bị query cho cả hai hệ thống. Có thể bao gồm:
- Tokenization & Normalization: Tách từ, chuẩn hóa chữ hoa/thường, loại bỏ stop words nếu cần.
- Embedding Generation: Tạo vector embedding cho query dùng cùng model đã dùng để nhúng tài liệu.
- Query Expansion (Optional): Mở rộng query với các từ đồng nghĩa hoặc thuật ngữ liên quan, đặc biệt hữu ích cho Dense Retrieval.
- Sparse Retrieval (BM25): Query được gửi đến engine BM25. Engine này quét inverted index và trả về một tập hợp các tài liệu có điểm BM25 cao nhất.
- Dense Retrieval (ANN): Query (dưới dạng vector) được gửi đến ANN index. ANN engine tìm kiếm các vector tài liệu gần nhất với vector query và trả về một tập hợp các tài liệu tương ứng.
- Re-Ranking: Đây là “trái tim” của Hybrid Retrieval. Các tài liệu từ cả hai nguồn (Sparse và Dense) được gộp lại. Một thuật toán re-ranking (có thể là một model học máy khác, hoặc một công thức kết hợp điểm số) sẽ đánh giá lại tất cả các tài liệu và xếp hạng chúng dựa trên một tiêu chí tổng hợp. Thường thì điểm số từ BM25 và điểm tương đồng của vector (ví dụ: cosine similarity) sẽ được dùng làm feature cho re-ranker.
- Final Ranked Results: Tập hợp các tài liệu được xếp hạng cuối cùng, trả về cho người dùng.
- User Response: Hiển thị kết quả cho người dùng.
2. Mổ xẻ chi tiết kỹ thuật từng thành phần
2.1. Sparse Retrieval: BM25 và những lưu ý khi scale
BM25 hiện tại vẫn là một lực lượng đáng gờm trong mảng tìm kiếm từ khóa. Các engine phổ biến như Elasticsearch, Apache Solr đều tích hợp BM25 rất tốt.
Công thức BM25 (phiên bản đơn giản):
$BM25(D, Q) = \sum_{i \in Q} IDF(t_i) \cdot \frac{freq(t_i, D) \cdot (k_1 + 1)}{freq(t_i, D) + k_1 \cdot (1 – b + b \cdot \frac{|D|}{avgdl})}$
Trong đó:
- $Q$: Tập hợp các term trong query.
- $t_i$: Một term trong query $Q$.
- $D$: Tài liệu đang xét.
- $freq(t_i, D)$: Số lần xuất hiện của term $t_i$ trong tài liệu $D$.
- $|D|$: Độ dài của tài liệu $D$.
- $avgdl$: Độ dài trung bình của tất cả các tài liệu trong corpus.
- $k_1$ và $b$: Hai tham số tùy chỉnh (hyperparameters). $k_1$ kiểm soát tần suất xuất hiện của term, $b$ kiểm soát sự ảnh hưởng của độ dài tài liệu. Giá trị phổ biến cho $k_1$ là $\approx 1.5-2.0$, $b$ là $0.75$.
- $IDF(t_i)$: Inverse Document Frequency, thường được tính là:
$IDF(t_i) = \log \left( \frac{N + 1}{df(t_i) + 1} \right)$
Trong đó $N$ là tổng số tài liệu, $df(t_i)$ là số tài liệu chứa term $t_i$.
Sử dụng BM25 hiệu quả:
- Lựa chọn tham số $k_1$ và $b$: Cái này không có công thức chung, tùy vào tập dữ liệu và loại truy vấn mà bạn cần thử nghiệm. Có thể dùng Grid Search hoặc các thuật toán tối ưu hóa khác.
- Cấu trúc Inverted Index: Tối ưu hóa cấu trúc inverted index là cực kỳ quan trọng. Elasticsearch sử dụng cấu trúc Roaring Bitmaps hoặc Lucene’s Postings Lists cho hiệu quả cao. Khi hệ thống xử lý 10.000 request/giây, mỗi ms đều quý giá. Viêc đọc dữ liệu từ index phải cực kỳ nhanh.
- Preprocessing Query: Nên chuẩn hóa query trước khi tìm kiếm. Ví dụ, nếu corpus của bạn tiếng Việt, việc loại bỏ dấu tiếng Việt, chuẩn hóa về chữ thường là bước cần thiết.
- Caching: Caching các query phổ biến và kết quả của chúng (ví dụ: dùng Redis) có thể giảm tải đáng kể cho engine BM25.
Dẫn chứng: Tài liệu của Apache Lucene (nền tảng của Elasticsearch và Solr) luôn là nguồn tham khảo chính về các thuật toán ranking, bao gồm cả BM25.
2.2. Dense Retrieval: ANN và những thách thức về scale
Dense retrieval dựa trên việc biểu diễn văn bản thành các vector trong không gian nhiều chiều. Việc tìm kiếm vector “gần nhất” với vector query được gọi là Nearest Neighbor Search. Tìm kiếm chính xác (Exact Nearest Neighbor – ENN) là NP-hard, nên trong thực tế, chúng ta dùng Approximate Nearest Neighbor (ANN).
Các thuật toán ANN phổ biến:
- Product Quantization (PQ) / Inverted File Index (IVF): Phương pháp quen thuộc trong Faiss (Facebook AI Similarity Search). Faiss là một thư viện cực kỳ mạnh mẽ hỗ trợ nhiều loại index ANN.
- IVF: Chia không gian vector thành các “cell” (cluster) và chỉ tìm kiếm trong các cell gần với query center.
- PQ: Nén các vector bằng cách chia chúng thành các phần nhỏ và lượng tử hóa từng phần. Giúp giảm dung lượng lưu trữ và tăng tốc độ tính toán.
- IVF-PQ: Kết hợp cả hai để có hiệu quả cao.
- Hierarchical Navigable Small Worlds (HNSW): Một đồ thị phân cấp, cho phép tìm kiếm hiệu quả bằng cách đi qua các lớp, từ các nút “xa” đến các nút “gần” hơn. Rất phổ biến trong các thư viện như Annoy (Spotify), NMSLIB, hoặc được tích hợp trong Weaviate, Pinecone. HNSW có xu hướng cho độ chính xác cao tại mức độ nén nhất định, nhưng cấu trúc đồ thị có thể tốn bộ nhớ.
- ScaNN (Scalable Nearest Neighbors): Một thuật toán được Google phát triển, sử dụng phép chiếu vector và lượng tử hóa để tăng tốc.
Sử dụng ANN hiệu quả:
- Embedding Model: Lựa chọn mô hình embedding là cực kỳ quan trọng. Với các ứng dụng cần hiểu sâu về ngôn ngữ tự nhiên, các mô hình Transformer như Sentence-BERT, MPNet, E5 (từ Microsoft), hoặc các mô hình được fine-tune trên domain cụ thể sẽ cho kết quả tốt hơn. OpenAI Embeddings API hay Cohere Embeddings cũng là những lựa chọn thương mại tốt nếu budget cho phép. Phiên bản gần đây nhất của OpenAI dùng
text-embedding-3-large. - Index Parameters: Cấu hình index ANN (ví dụ: số lượng centroids trong
ivfcủa Faiss,ef_constructionvàMtrong HNSW) là yếu tố quyết định giữa độ chính xác (recall) và tốc độ tìm kiếm (latency).- Recall (Độ phủ): Tỉ lệ các vector thực sự gần nhất mà thuật toán tìm được.
- Latency (Độ trễ): Thời gian để trả về kết quả.
- Resource Management: ANN index có thể tốn kha khá RAM. Faiss có thể sử dụng GPU để tăng tốc tìm kiếm, đặc biệt hữu ích khi xử lý lượng lớn request.
- Data Partitioning & Sharding: Khi tập dữ liệu quá lớn, không thể nhét hết vào một máy, cần chia nhỏ (shard) index và phân tán trên nhiều node.
Dẫn chứng:
* Faiss documentation: Tham khảo chi tiết các loại index, cách build và search.
* HNSW paper: “Hierarchical Navigable Small Worlds” bởi Y. Malkov và A. Yashunin.
* Vector Database Benchmarks: Các nguồn như Pinecone’s benchmarks hay các bài blog kỹ thuật thường có so sánh hiệu năng giữa các engine ANN. StackOverflow Survey 2023 cũng cho thấy sự gia tăng việc sử dụng Vector Databases.
2.3. Re-Ranking: Chìa khóa của sự kết hợp
Đây là bước “pha trộn” và “điều chỉnh” để tạo ra kết quả cuối cùng. Có nhiều cách để re-rank:
- Linear Combination: Cộng hoặc nhân điểm số từ Sparse và Dense theo một trọng số cố định.
$Score_{Hybrid} = w_{sparse} \cdot Score_{Sparse} + w_{dense} \cdot Score_{Dense}$
Hoặc dùng các điểm số đã được chuẩn hóa (normalized) để tránh lệch pha do thang đo khác nhau. Ví dụ:
$Score_{Hybrid} = \alpha \cdot \frac{BM25_Score}{Max_BM25} + (1-\alpha) \cdot CosineSimilarity(QueryVec, DocVec)$
Tham số $\alpha$ sẽ được điều chỉnh. - Learning to Rank (LTR): Sử dụng một mô hình học máy (ví dụ: Gradient Boosting Machines như XGBoost, LightGBM hoặc Neural Networks) để học cách xếp hạng. Các features cho mô hình LTR này bao gồm:
- Điểm BM25 của tài liệu.
- Cosine Similarity giữa query vector và document vector.
- Tần suất xuất hiện của query terms trong document (từ BM25).
- Độ dài tài liệu.
- Các feature khác liên quan đến metadata document.
- Mô hình LTR này cần được huấn luyện trên một tập dữ liệu có nhãn (ví dụ: người dùng có đánh giá là kết quả nào hữu ích hơn).
- Cross-Encoder Re-ranking: Đây là kỹ thuật mạnh mẽ nhưng tốn kém. Mô hình Cross-Encoder nhận cả query và document làm input cùng lúc để đưa ra điểm số. Khác với Bi-Encoder (mô hình embedding sinh vector cho query và document riêng lẻ), Cross-Encoder có thể nắm bắt tương tác ngữ nghĩa sâu sắc hơn giữa query và document. Tuy nhiên, chúng chậm hơn rất nhiều và thường chỉ dùng để re-rank một tập nhỏ các tài liệu ứng viên đã lọc qua Sparse/Dense retrieval. Các mô hình như MonoT5, DeBERTa-v3 có thể được fine-tune cho nhiệm vụ này.
Best Practice cho Re-ranking:
Không bao giờ chạy re-ranking trên toàn bộ corpus. Luôn dùng Dense và/hoặc Sparse retrieval để lọc ra một tập nhỏ (ví dụ: top 100-1000 tài liệu) trước khi áp dụng re-ranking.
3. Cân bằng Latency và Performance
Mục tiêu 10.000 request/giây là con số không nhỏ. Việc kết hợp nhiều hệ thống sẽ tăng overall latency.
- Parallel Execution: Chạy Sparse Retrieval và Dense Retrieval song song.
import threading import queue def sparse_search(query, results_queue): # Call BM25 engine sparse_results = bm25_engine.search(query) results_queue.put(("sparse", sparse_results)) def dense_search(query_vec, results_queue): # Call ANN engine dense_results = ann_engine.search(query_vec) results_queue.put(("dense", dense_results)) query = "how to build a microservice architecture" query_vec = embedding_model.encode(query) results_queue = queue.Queue() sparse_thread = threading.Thread(target=sparse_search, args=(query, results_queue)) dense_thread = threading.Thread(target=dense_search, args=(query_vec, results_queue)) sparse_thread.start() dense_thread.start() sparse_thread.join() dense_thread.join() all_candidate_docs = [] while not results_queue.empty(): source, docs = results_queue.get() all_candidate_docs.extend(docs) # Proceed to re-ranking - Gating Mechanisms: Dựa vào độ phức tạp của query, có thể quyết định có chạy cả hai hay chỉ một phương pháp. Ví dụ: query ngắn, nhiều keyword –> ưu tiên Sparse. Query dài, mô tả –> ưu tiên Dense.
- Caching: Cache kết quả của cả hai hệ thống, đặc biệt là các truy vấn phổ biến.
- Optimized ANN/BM25 Engines: Sử dụng các phiên bản đã được build với các tùy chọn tối ưu hiệu năng (ví dụ: dùng các thư viện C++ core cho Python bindings).
- Hardware Acceleration: Sử dụng GPU cho ANN search nếu có thể.
Metric quan trọng cần theo dõi:
- Latency (End-to-End): Từ lúc request vào cho đến lúc trả response.
- RPS (Requests Per Second): Khả năng xử lý của hệ thống.
- Recall@k: Tỉ lệ kết quả đúng trong top k.
- Memory Usage: Đặc biệt quan trọng với ANN index.
- CPU Usage: Cho cả query processing và index serving.
4. Bảng so sánh kỹ thuật: BM25 vs ANN (về mặt concept tìm kiếm)
| Tiêu chí | BM25 (Sparse) | ANN (Dense) |
|---|---|---|
| Cách hoạt động | Dựa trên thống kê từ khóa (TF-IDF, BM25) | Dựa trên ngữ nghĩa (vector embedding) |
| Hiểu biết | Từ khóa chính xác, tần suất xuất hiện. | Ngữ nghĩa, mối quan hệ ngữ nghĩa, từ đồng nghĩa. |
| Độ khó triển khai | Tương đối dễ, nhiều engine hỗ trợ. | Phức tạp hơn, yêu cầu mô hình embedding, quản lý index. |
| Hiệu năng (Query) | Nhanh với query ngắn, nhiều keyword. | Nhanh với query dài, mang tính mô tả. |
| Hiệu năng (Index) | Index có thể lớn nhưng truy cập nhanh. | index (vector store) có thể tốn nhiều RAM, cần tối ưu. |
| Độ chính xác | Cao cho match từ khóa, thấp cho ý nghĩa. | Cao cho ý nghĩa, có thể bị “lạc đề” với từ khóa hẹp. |
| Tài nguyên | CPU, Disk I/O cho index traversal. | CPU/GPU, RAM lớn cho embedding và ANN index. |
| Cộng đồng Support | Rất lớn (Elasticsearch, Solr). | Lớn và đang phát triển mạnh (Faiss, Pinecone, Weaviate). |
| Learning Curve | Thấp đến trung bình. | Trung bình đến cao. |
| Use Cases | Tìm kiếm sản phẩm phổ thông, tài liệu kỹ thuật cơ bản. | Chatbots, recommendation systems, search theo mô tả. |
5. Kết bài: 3 điểm cốt lõi cần nhớ
- Hybrid Retrieval là sự kết hợp tất yếu: Để có kết quả search “thông minh” và chính xác hơn, việc kết hợp sức mạnh của Sparse (BM25) và Dense (ANN) là gần như bắt buộc, đặc biệt khi scale hệ thống lên hàng chục nghìn RPS.
- Re-Ranking là “linh hồn”: Cách bạn gộp và đánh giá lại kết quả từ cả hai nguồn ảnh hưởng trực tiếp đến chất lượng cuối cùng. LTR hoặc Cross-Encoder là các kỹ thuật mạnh cho bước này.
- Cân bằng là chìa khóa: Luôn phải trade-off giữa độ chính xác, latency, và tài nguyên sử dụng. Tối ưu hóa từng thành phần (embedding model, ANN index parameters, BM25 configuration) và chạy song song là cách để đạt được mục tiêu RPS đề ra.
Hy vọng bài viết này giúp anh em có cái nhìn rõ ràng hơn về cách xây dựng một hệ thống search hiện đại.
Câu hỏi thảo luận: Anh em đã từng gặp pain point nào khi triển khai Hybrid Search chưa? Kinh nghiệm re-ranking của anh em thế nào? Chia sẻ bên dưới cho anh em cùng học hỏi nhé.
Nếu anh em đang làm Content hay SEO mà muốn tự động hóa quy trình thì tham khảo bộ công cụ bên noidungso.io.vn nhé, đỡ tốn cơm gạo thuê nhân sự part-time.
Nội dung được Hải định hướng, trợ lý AI giúp mình viết chi tiết.








