Vấn đề: Khi Semantic Search thất bại
Hầu hết các kỹ sư bắt đầu dự án RAG (Retrieval-Augmented Generation) bằng cách chuyển văn bản thành embedding và đưa chúng vào cơ sở dữ liệu vector. Cảm giác lúc đó thật kỳ diệu. Nhưng phép màu đó nhanh chóng biến mất khi người dùng tìm kiếm một SKU hoặc mã lỗi cụ thể nhưng lại nhận được hướng dẫn khắc phục sự sự cố chung chung.
Trong một đợt kiểm tra gần đây đối với chatbot hỗ trợ kỹ thuật, chúng tôi phát hiện ra rằng tìm kiếm ngữ nghĩa thuần túy (pure semantic search) đã thất bại ở 18% các truy vấn liên quan đến ID duy nhất như “0x8004210B”. Vấn đề là vector search mang tính ngữ nghĩa, không phải tìm kiếm chính xác từng ký tự (literal). Nó biết “king” ở gần “queen”, nhưng thường coi “Lỗi A” và “Lỗi B” là có thể thay thế cho nhau vì chúng chia sẻ cùng bối cảnh ngôn ngữ. Trong các môi trường yêu cầu độ chính xác cao — như hồ sơ y tế hoặc nhật ký tài chính — việc “gần đúng” chính là một thất bại. Bạn cần kết quả khớp chính xác.
Hybrid Search khắc phục điều này. Bằng cách kết hợp khả năng hiểu ngữ nghĩa của Dense Vector với độ chính xác tuyệt đối của BM25 (Sparse Vector), bạn có thể xây dựng một hệ thống hiểu được ý định của người dùng mà không bỏ sót các chi tiết kỹ thuật cụ thể.
Bắt đầu nhanh: Triển khai Weighted Fusion
Để xây dựng một hệ thống hybrid, bạn cần hợp nhất kết quả từ hai thế giới khác nhau. Reciprocal Rank Fusion (RRF) là tiêu chuẩn ngành trong trường hợp này. Nó cho phép bạn kết hợp các kết quả khớp từ khóa và các kết quả lân cận trong không gian vector mà không cần lo lắng về thang điểm khác nhau của chúng. Dưới đây là mã Python minh họa cách hợp nhất các luồng dữ liệu này.
from rank_bm25 import BM25Okapi
import numpy as np
# Các tài liệu kỹ thuật mẫu
docs = [
"Cách khắc phục lỗi 0x8004210B trong Outlook",
"Hướng dẫn thiết lập chung cho ứng dụng email",
"Mã linh kiện #99-AF-12: Khắc phục sự cố giao thức mạng nâng cao"
]
# 1. Thiết lập tìm kiếm từ khóa (BM25)
tokenized_corpus = [doc.split(" ") for doc in docs]
bm25 = BM25Okapi(tokenized_corpus)
# 2. Mô phỏng truy vấn cho một mã lỗi chính xác
query = "0x8004210B"
tokenized_query = query.split(" ")
# Tính toán điểm BM25
bm25_scores = bm25.get_scores(tokenized_query)
print(f"Điểm BM25: {bm25_scores}")
# BM25 sẽ đánh trọng số rất cao cho kết quả khớp chính xác mã lỗi
BM25 nắm bắt được các token cụ thể mà embedding thường bỏ qua. Trong môi trường thực tế, việc thêm lớp này có thể giảm đáng kể lỗi truy xuất “sai tài liệu”, đặc biệt là đối với dữ liệu chứa nhiều thuật ngữ chuyên ngành hoặc số hiệu linh kiện.
Tại sao BM25 vẫn chiếm ưu thế về độ chính xác
Dense Vector đối đầu Sparse Vector
Dense vector đại diện cho “sắc thái” của một câu. Chúng nắm bắt tốt các từ đồng nghĩa và ý định. Tuy nhiên, chúng dễ bị “xung đột” (collision), nơi hai thuật ngữ kỹ thuật riêng biệt lại nằm trong cùng một vùng không gian vector vì chúng xuất hiện trong các câu có cấu trúc tương tự nhau.
BM25 (Best Matching 25) là một bước tiến hóa của TF-IDF. Nó đếm tần suất xuất hiện của từ nhưng áp dụng đường cong bão hòa. Điều này ngăn một từ lặp lại quá nhiều lần chiếm ưu thế hoàn toàn trong điểm số. Đây là cách tiếp cận “Sparse” (thưa thớt) vì nó tập trung vào các token riêng biệt, chính xác thay vì các mối quan hệ trừu tượng.
Cơ chế của Reciprocal Rank Fusion (RRF)
Bạn không thể chỉ đơn giản là cộng điểm cosine similarity (từ 0.0 đến 1.0) với điểm BM25 (có thể lên tới hơn 20). RRF giải quyết vấn đề này bằng cách xem xét thứ hạng (rank). Nếu một tài liệu đứng thứ #1 trong BM25 và thứ #50 trong Vector search, RRF sẽ tính toán điểm kết hợp dựa trên vị trí của nó trong cả hai danh sách.
Công thức tiêu chuẩn là: score = 1 / (rank + k). Chúng ta thường đặt k bằng 60. Hằng số này đảm bảo rằng các mục có thứ hạng rất cao không áp đảo hoàn toàn các mục có liên quan nhưng xếp hạng thấp hơn một chút trong danh sách còn lại.
Tối ưu hóa: Điều chỉnh sự cân bằng Hybrid
Không phải mọi tập dữ liệu đều giống nhau. Một số cần tập trung nhiều hơn vào từ khóa; số khác cần nhiều ngữ nghĩa hơn. Hãy sử dụng tham số alpha có trọng số để tìm ra điểm cân bằng lý tưởng cho dữ liệu cụ thể của bạn.
def hybrid_score(vector_rank, bm25_rank, alpha=0.3):
# Alpha = 0.3 ưu tiên BM25 (Tốt cho tài liệu kỹ thuật)
# Alpha = 0.7 ưu tiên Vector Search (Tốt cho văn bản sáng tạo)
k = 60
return (alpha * (1 / (vector_rank + k))) + ((1 - alpha) * (1 / (bm25_rank + k)))
Giải quyết vấn đề “Out-of-Vocabulary”
Các mô hình embedding thường gặp khó khăn với tên dự án nội bộ hoặc mã định danh thương hiệu mới không có trong dữ liệu huấn luyện của chúng. Nếu người dùng tìm kiếm “ProjectX-Alpha”, mô hình vector có thể ánh xạ nó tới “Project Alpha”, làm mất đi mã định danh “X” quan trọng. BM25 coi mã đó là một dấu vân tay duy nhất, đảm bảo tài liệu đúng luôn nằm ở vị trí đầu tiên.
Checklist cho môi trường Production
- Bộ lọc Metadata cứng: Đừng tìm kiếm trên toàn bộ cơ sở dữ liệu nếu bạn biết người dùng chỉ quan tâm đến các tài liệu năm “2024”. Hãy lọc theo metadata trước, sau đó mới chạy tìm kiếm hybrid.
- Làm sạch Token: BM25 dựa trên việc đối khớp chính xác. Việc chuẩn hóa từ (Stemming) và loại bỏ dấu câu đảm bảo rằng “Fixing,” “Fixes,” và “Fix” đều được tính là cùng một kết quả khớp.
- Thiết lập Alpha khôn ngoan: Nếu tài liệu của bạn chứa 70% thuật ngữ chuyên môn, hãy bắt đầu với alpha là 0.3. Đối với các trang wiki dạng hội thoại, hãy thử 0.7.
- Đo kiểm với 50 truy vấn: Đừng đoán mò. Hãy kiểm tra thủ công Top-3 kết quả cho 50 câu hỏi phổ biến. Khoản đầu tư nhỏ này sẽ giúp bạn tránh được các phiên gỡ lỗi (debug) mệt mỏi sau này.
Hybrid search không chỉ là một bản nâng cấp kỹ thuật; nó là một giải pháp về độ tin cậy. Bằng cách thừa nhận rằng semantic search có những điểm mù, bạn có thể xây dựng một quy trình RAG mà người dùng thực sự tin tưởng về độ chính xác, chứ không chỉ vì khả năng bắt chước ngôn ngữ con người.

