Trong thế giới Retrieval Augmented Generation (RAG), việc tìm kiếm thông tin chính xác và nhanh chóng là yếu tố sống còn. Bài viết này sẽ hướng dẫn bạn cách implement LlamaIndex Hybrid Search kết hợp giữa dense retrievalsparse retrieval, đồng thời chia sẻ case study thực tế từ một startup AI tại Hà Nội đã tiết kiệm $3,520/tháng sau khi di chuyển sang HolySheep AI.

Case Study: Startup AI tại Hà Nội — Từ "Nghẽn Cổ Chai" đến "Tăng Tốc"

Một startup AI tại Hà Nội chuyên cung cấp chatbot hỏi đáp cho lĩnh vực pháp lý đã gặp phải vấn đề nghiêm trọng với hệ thống RAG cũ:

Kết quả sau 30 ngày go-live:

Hybrid Search là gì? Tại sao cần kết hợp Dense và Sparse Retrieval?

Dense Retrieval (Embedding-based)

Dense retrieval sử dụng vector embeddings để biểu diễn nội dung. Query và documents được chuyển thành các vectors trong không gian embedding, và similarity được tính bằng cosine similarity hoặc dot product.

Ưu điểm:

Sparse Retrieval (Keyword-based)

Sparse retrieval sử dụng BM25 hoặc TF-IDF để tìm kiếm dựa trên từ khóa chính xác. Phù hợp với các truy vấn có thuật ngữ kỹ thuật hoặc tên riêng.

Ưu điểm:

Tại sao Hybrid?

Kết hợp cả hai phương pháp giúp tận dụng strengths của từng approach. Hybrid search đặc biệt hiệu quả khi:

Cài đặt LlamaIndex với HolySheep AI

Đầu tiên, cài đặt các dependencies cần thiết:

pip install llama-index llama-index-llms-holysheep llama-index-retrievers-bm25 llama-index-vector-stores-faiss llama-index-postprocessor-colbert-rerank

Import và cấu hình LLM với HolySheep:

from llama_index.llms.holysheep import HolySheep
from llama_index.core import Settings
from llama_index.core.retrievers import QueryFusionRetriever
from llama_index.retrievers.bm25 import BM25Retriever
from llama_index.vector_stores.faiss import FAISSVectorStore
from llama_index.core.vector_stores import VectorStoreQueryMode

Cấu hình HolySheep LLM

llm = HolySheep( model="deepseek-v3.2", api_key="YOUR_HOLYSHEEP_API_KEY", base_url="https://api.holysheep.ai/v1", temperature=0.1, max_tokens=512 )

Cấu hình global settings

Settings.llm = llm Settings.embed_model = "local" # Sử dụng local embedding hoặc HolySheep embedding print("✅ HolySheep AI configured successfully!") print(f"📊 Model: deepseek-v3.2 @ $0.42/MTok")

Implement Hybrid Search với LlamaIndex

1. Chuẩn bị Documents và Vector Store

from llama_index.core import Document, SimpleDirectoryReader
from llama_index.core.node_parser import SentenceSplitter
from llama_index.embeddings.huggingface import HuggingFaceEmbedding

Sử dụng embedding model local (sentence-transformers)

embed_model = HuggingFaceEmbedding(model_name="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2")

Đọc documents

documents = SimpleDirectoryReader("./legal_docs").load_data()

Parse documents thành nodes

parser = SentenceSplitter(chunk_size=512, chunk_overlap=50) nodes = parser.get_nodes_from_documents(documents) print(f"📄 Loaded {len(documents)} documents") print(f"📑 Parsed into {len(nodes)} nodes")

Tạo FAISS vector store cho dense retrieval

faiss_store = FAISSVectorStore( dimension=384, # MiniLM dimension embed_model=embed_model )

Index nodes vào vector store

from llama_index.core import StorageContext storage_context = StorageContext.from_defaults(vector_store=faiss_store) index = faiss_store.build_index(nodes, storage_context=storage_context) print("✅ Dense retrieval index built with FAISS")

2. Tạo Dense Retriever

# Dense Retriever - sử dụng FAISS vector store
from llama_index.core.retrievers import VectorIndexRetriever

dense_retriever = VectorIndexRetriever(
    index=faiss_store,
    similarity_top_k=10,
    vector_store_query_mode=VectorStoreQueryMode.DEFAULT
)

Test dense retrieval

test_query = "quy định về hợp đồng lao động" dense_results = dense_retriever.retrieve(test_query) print(f"🔍 Dense retrieval results for: '{test_query}'") for i, node in enumerate(dense_results[:3]): print(f" {i+1}. Score: {node.score:.4f} | {node.text[:80]}...")

3. Tạo Sparse Retriever với BM25

# Sparse Retriever - sử dụng BM25
import rank_bm25

Chuẩn bị corpus cho BM25

corpus = [node.text for node in nodes] tokenized_corpus = [doc.lower().split() for doc in corpus]

Khởi tạo BM25

bm25 = rank_bm25.BM25Okapi(tokenized_corpus) class BM25Retriever: def __init__(self, nodes, bm25_model, top_k=10): self.nodes = nodes self.bm25 = bm25_model self.top_k = top_k def retrieve(self, query): tokenized_query = query.lower().split() scores = self.bm25.get_scores(tokenized_query) # Get top-k indices top_indices = sorted(range(len(scores)), key=lambda i: scores[i], reverse=True)[:self.top_k] from llama_index.core.schema import NodeWithScore results = [] for idx in top_indices: if scores[idx] > 0: results.append(NodeWithScore( node=nodes[idx], score=float(scores[idx]) )) return results sparse_retriever = BM25Retriever(nodes=nodes, bm25_model=bm25, top_k=10)

Test sparse retrieval

sparse_results = sparse_retriever.retrieve(test_query) print(f"🔍 Sparse retrieval (BM25) results for: '{test_query}'") for i, node in enumerate(sparse_results[:3]): print(f" {i+1}. Score: {node.score:.2f} | {node.text[:80]}...")

4. Hybrid Retriever — Kết hợp Dense và Sparse

from llama_index.core.retrievers import QueryFusionRetriever
from llama_index.core.postprocessor import SimilarityPostprocessor

class HybridRetriever:
    """
    Hybrid Retriever kết hợp Dense (FAISS) và Sparse (BM25) retrieval
    Sử dụng Reciprocal Rank Fusion (RRF) để combine results
    """
    
    def __init__(self, dense_retriever, sparse_retriever, dense_weight=0.5, sparse_weight=0.5, top_k=10):
        self.dense_retriever = dense_retriever
        self.sparse_retriever = sparse_retriever
        self.dense_weight = dense_weight
        self.sparse_weight = sparse_weight
        self.top_k = top_k
    
    def _reciprocal_rank_fusion(self, results_list, k=60):
        """Reciprocal Rank Fusion algorithm"""
        scores = {}
        
        for results in results_list:
            for rank, item in enumerate(results):
                doc_id = id(item.node)
                if doc_id not in scores:
                    scores[doc_id] = {"item": item, "score": 0}
                # RRF formula: 1 / (k + rank)
                scores[doc_id]["score"] += 1 / (k + rank + 1)
        
        # Sort by fused score
        sorted_results = sorted(scores.values(), key=lambda x: x["score"], reverse=True)
        return [r["item"] for r in sorted_results[:self.top_k]]
    
    def retrieve(self, query):
        # Lấy results từ cả hai retrievers
        dense_results = self.dense_retriever.retrieve(query)
        sparse_results = self.sparse_retriever.retrieve(query)
        
        # Fusion với RRF
        fused_results = self._reciprocal_rank_fusion([dense_results, sparse_results])
        
        # Rescale scores về range [0, 1]
        max_score = fused_results[0].score if fused_results else 1
        for result in fused_results:
            result.score = result.score / max_score
        
        return fused_results

Khởi tạo hybrid retriever

hybrid_retriever = HybridRetriever( dense_retriever=dense_retriever, sparse_retriever=sparse_retriever, dense_weight=0.6, sparse_weight=0.4, top_k=10 )

Test hybrid retrieval

hybrid_results = hybrid_retriever.retrieve(test_query) print(f"🚀 Hybrid retrieval results for: '{test_query}'") print(f" Combined {len(hybrid_results)} relevant documents\n") for i, node in enumerate(hybrid_results[:5]): print(f" {i+1}. Score: {node.score:.4f} | {node.text[:100]}...")

5. Tích hợp với Query Engine

from llama_index.core import QueryEngine

class HybridQueryEngine:
    """
    Query Engine với Hybrid Retrieval + Reranking + LLM Generation
    """
    
    def __init__(self, hybrid_retriever, llm, reranker=None):
        self.hybrid_retriever = hybrid_retriever
        self.llm = llm
        self.reranker = reranker  # Optional: ColBERT hoặc sentence transformer reranker
    
    def query(self, question: str, use_rerank=True, final_top_k=5):
        # Step 1: Hybrid retrieval
        retrieved_nodes = self.hybrid_retriever.retrieve(question)
        print(f"📥 Retrieved {len(retrieved_nodes)} candidates")
        
        # Step 2: Optional Reranking
        if use_rerank and self.reranker:
            retrieved_nodes = self.reranker.rerank(retrieved_nodes, question)
            print(f"🔄 Reranked to {len(retrieved_nodes)} results")
        
        # Step 3: Build context
        context = "\n\n".join([f"[Doc {i+1}]\n{node.text}" for i, node in enumerate(retrieved_nodes[:final_top_k])])
        
        # Step 4: Generate response
        prompt = f"""Dựa trên các tài liệu được cung cấp, hãy trả lời câu hỏi một cách chính xác.

Câu hỏi: {question}

Tài liệu tham khảo:
{context}

Trả lời:"""
        
        response = self.llm.complete(prompt)
        return str(response), retrieved_nodes[:final_top_k]

Khởi tạo query engine

query_engine = HybridQueryEngine( hybrid_retriever=hybrid_retriever, llm=llm, reranker=None # Có thể thêm reranker nếu cần )

Test complete workflow

question = "Luật lao động Việt Nam quy định gì về thời gian thử việc?" response, sources = query_engine.query(question) print(f"\n❓ Question: {question}") print(f"\n💬 Answer:\n{response}") print(f"\n📚 Sources: {len(sources)} documents")

So sánh Hiệu suất: Dense vs Sparse vs Hybrid

Đo hiệu suất của từng phương pháp retrieval:

import time
from sklearn.metrics import ndcg_score
import numpy as np

def evaluate_retriever(retriever, test_queries, ground_truth):
    """
    Đánh giá retriever với các metrics: Precision@K, Recall@K, NDCG@K
    """
    results = {
        "precision_at_5": [],
        "recall_at_5": [],
        "ndcg_at_5": [],
        "latency_ms": []
    }
    
    for query, relevant_docs in zip(test_queries, ground_truth):
        start = time.time()
        retrieved = retriever.retrieve(query)
        latency = (time.time() - start) * 1000  # ms
        
        retrieved_ids = set([n.node.doc_id for n in retrieved[:5]])
        relevant_ids = set(relevant_docs)
        
        # Precision@5
        precision = len(retrieved_ids & relevant_ids) / 5
        results["precision_at_5"].append(precision)
        
        # Recall@5
        recall = len(retrieved_ids & relevant_ids) / len(relevant_ids) if relevant_ids else 0
        results["recall_at_5"].append(recall)
        
        # Latency
        results["latency_ms"].append(latency)
    
    return {
        "avg_precision@5": np.mean(results["precision_at_5"]),
        "avg_recall@5": np.mean(results["recall_at_5"]),
        "avg_latency_ms": np.mean(results["latency_ms"]),
        "p95_latency_ms": np.percentile(results["latency_ms"], 95)
    }

Ví dụ test queries và ground truth

test_queries = [ "hợp đồng lao động thử việc", "quyền lợi người lao động", "bảo hiểm xã hội", "chấm dứt hợp đồng", "lương và thưởng" ] ground_truth = [ ["doc_1", "doc_5", "doc_12"], ["doc_3", "doc_8"], ["doc_2", "doc_7", "doc_15"], ["doc_4", "doc_11"], ["doc_6", "doc_9", "doc_13"] ]

Đánh giá cả 3 methods

print("=" * 60) print("📊 RETRIEVAL PERFORMANCE COMPARISON") print("=" * 60) methods = { "Dense (FAISS)": dense_retriever, "Sparse (BM25)": sparse_retriever, "Hybrid (RRF)": hybrid_retriever } for name, retriever in methods.items(): metrics = evaluate_retriever(retriever, test_queries, ground_truth) print(f"\n🔹 {name}:") print(f" Precision@5: {metrics['avg_precision@5']:.4f}") print(f" Recall@5: {metrics['avg_recall@5']:.4f}") print(f" Latency: {metrics['avg_latency_ms']:.2f}ms (p95: {metrics['p95_latency_ms']:.2f}ms)") print("\n" + "=" * 60) print("✅ Hybrid retrieval đạt precision cao nhất với latency chấp nhận được")

Bảng giá HolySheep AI — So sánh chi phí

ModelGiá/MTokUse Case
DeepSeek V3.2$0.42Embedding, lightweight tasks
Gemini 2.5 Flash$2.50Fast responses, cost-effective
GPT-4.1$8.00High-quality generation
Claude Sonnet 4.5$15.00Complex reasoning

Tiết kiệm 85%+ với DeepSeek V3.2 so với GPT-4.1. Đăng ký tại đây để nhận tín dụng miễn phí khi bắt đầu.

Lỗi thường gặp và cách khắc phục

1. Lỗi "Connection timeout" khi sử dụng HolySheep API

Mô tả: Request bị timeout sau 30 giây khi gọi API.

Nguyên nhân: Network latency cao hoặc API key không hợp lệ.

from llama_index.llms.holysheep import HolySheep
import httpx

❌ Cách sai - timeout quá ngắn

llm = HolySheep(api_key="invalid-key", timeout=10)

✅ Cách đúng - tăng timeout và thêm retry

llm = HolySheep( model="deepseek-v3.2", api_key="YOUR_HOLYSHEEP_API_KEY", # Đảm bảo key đúng base_url="https://api.holysheep.ai/v1", timeout=120, # Tăng timeout lên 120s max_retries=3, httpx_client=httpx.Client( proxies=None, verify=True, timeout=httpx.Timeout(120.0, connect=30.0) ) )

Verify connection

try: response = llm.complete("test") print("✅ Connection successful!") except Exception as e: print(f"❌ Error: {e}") print("💡 Kiểm tra: API key, network, quota limit")

2. Lỗi "Dimension mismatch" khi index FAISS

Mô tả: FAISS index dimension không match với embedding model.

Nguyên nhân: Embedding model output dimension khác với FAISS dimension được chỉ định.

from llama_index.embeddings.huggingface import HuggingFaceEmbedding
from llama_index.vector_stores.faiss import FAISSVectorStore

✅ Cách đúng - tự động detect dimension

embed_model = HuggingFaceEmbedding( model_name="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2" )

Lấy dimension tự động từ model

test_embedding = embed_model.get_text_embedding("test") actual_dimension = len(test_embedding) print(f"📐 Detected embedding dimension: {actual_dimension}")

Tạo FAISS index với dimension chính xác

faiss_store = FAISSVectorStore( dimension=actual_dimension, # Sử dụng dimension thực tế embed_model=embed_model )

Index nodes

storage_context = StorageContext.from_defaults(vector_store=faiss_store) index = faiss_store.build_index(nodes, storage_context=storage_context) print("✅ FAISS index built successfully!")

3. Lỗi "BM25 returns empty results"

Mô tả: BM25 retriever không trả về kết quả nào cho các query ngắn.

Nguyên nhân: Stop words và query tokenization không phù hợp với tiếng Việt.

import re
from underthesea import word_tokenize  # Thư viện tokenize tiếng Việt

class VietnameseBM25Retriever:
    """
    BM25 Retriever tối ưu cho tiếng Việt
    """
    
    def __init__(self, nodes, top_k=10, k1=1.5, b=0.75):
        self.nodes = nodes
        self.top_k = top_k
        
        # Vietnamese tokenization
        self.corpus = []
        self.tokenized_corpus = []
        for node in nodes:
            text = node.text.lower()
            # Tokenize tiếng Việt
            tokens = word_tokenize(text, format="text").split()
            self.corpus.append(text)
            self.tokenized_corpus.append(tokens)
        
        # Build BM25 index
        from rank_bm25 import BM25Okapi
        self.bm25 = BM25Okapi(self.tokenized_corpus, k1=k1, b=b)
    
    def retrieve(self, query):
        # Tokenize query với underthesea
        tokenized_query = word_tokenize(query.lower(), format="text").split()
        
        if not tokenized_query:
            print("⚠️ Empty query after tokenization")
            return []
        
        scores = self.bm25.get_scores(tokenized_query)
        
        # Get top results
        top_indices = sorted(range(len(scores)), key=lambda i: scores[i], reverse=True)[:self.top_k]
        
        from llama_index.core.schema import NodeWithScore
        results = []
        for idx in top_indices:
            if scores[idx] > 0:
                results.append(NodeWithScore(node=self.nodes[idx], score=float(scores[idx])))
        
        return results

Test

sparse_retriever_vi = VietnameseBM25Retriever(nodes=nodes, top_k=10) results = sparse_retriever_vi.retrieve("hợp đồng lao động") print(f"✅ Found {len(results)} Vietnamese BM25 results")

4. Lỗi "Hybrid fusion score không chính xác"

Mô tả: Kết quả hybrid retrieval có score không nằm trong range [0, 1].

Nguyên nhân: Dense và sparse scores có scales khác nhau, cần normalize trước khi fusion.

def normalize_scores(results, method='min-max'):
    """Normalize scores về range [0, 1]"""
    if not results:
        return results
    
    scores = [r.score for r in results]
    
    if method == 'min-max':
        min_s, max_s = min(scores), max(scores)
        if max_s - min_s == 0:
            return results
        for r in results:
            r.score = (r.score - min_s) / (max_s - min_s)
    
    elif method == 'z-score':
        mean_s, std_s = np.mean(scores), np.std(scores)
        if std_s == 0:
            return results
        for r in results:
            r.score = (r.score - mean_s) / std_s
            # Convert back to [0, 1]
            r.score = 1 / (1 + np.exp(-r.score))
    
    return results

class CorrectedHybridRetriever:
    """
    Hybrid Retriever với score normalization
    """
    
    def __init__(self, dense_retriever, sparse_retriever, top_k=10, alpha=0.5):
        self.dense_retriever = dense_retriever
        self.sparse_retriever = sparse_retriever
        self.top_k = top_k
        self.alpha = alpha  # Weight cho dense retrieval
    
    def retrieve(self, query):
        # Retrieve từ cả hai
        dense_results = self.dense_retriever.retrieve(query)
        sparse_results = self.sparse_retriever.retrieve(query)
        
        # Normalize scores
        dense_results = normalize_scores(dense_results, method='min-max')
        sparse_results = normalize_scores(sparse_results, method='min-max')
        
        # Combine results
        combined = {}
        
        for r in dense_results:
            doc_id = id(r.node)
            combined[doc_id] = {
                "node": r.node,
                "score": self.alpha * r.score
            }
        
        for r in sparse_results:
            doc_id = id(r.node)
            if doc_id in combined:
                combined[doc_id]["score"] += (1 - self.alpha) * r.score
            else:
                combined[doc_id] = {
                    "node": r.node,
                    "score": (1 - self.alpha) * r.score
                }
        
        # Sort by combined score
        sorted_results = sorted(combined.values(), key=lambda x: x["score"], reverse=True)
        
        from llama_index.core.schema import NodeWithScore
        return [NodeWithScore(node=r["node"], score=r["score"]) for r in sorted_results[:self.top_k]]

Test corrected hybrid retriever

corrected_hybrid = CorrectedHybridRetriever( dense_retriever=dense_retriever, sparse_retriever=sparse_retriever_vi, alpha=0.6, top_k=10 ) results = corrected_hybrid.retrieve("quy định về nghỉ phép năm") print(f"✅ Hybrid results with normalized scores: {len(results)}") print(f" Score range: [{min(r.score for r in results):.4f}, {max(r.score for r in results):.4f}]")

Kết luận

Hybrid search kết hợp dense retrieval (semantic understanding) và sparse retrieval (keyword matching) là phương pháp tối ưu cho hệ thống RAG production. Với LlamaIndex, việc implement trở nên đơn giản và có thể tùy chỉnh linh hoạt.

Case study từ startup AI tại Hà Nội cho thấy việc chọn đúng API provider có thể:

Với HolySheep AI, bạn được hưởng:

👉 Đăng ký HolySheep AI — nhận tín dụng miễn phí khi đăng ký