Table of Contents23 sections
Đây là bài viết tìm hiểu về kỹ thuật Hypothetical Document Embeddings (HyDE), chúng ta sẽ đi qua về nguyên lý, cách thức triển khai với Python và so sánh với dense retrieval + re-ranking. Kính mời quý anh em.
Vấn Đề Của Vector Search Thông Thường
Dense Retrieval hoạt động như thế nào
Dense retrieval là phương pháp tìm kiếm dựa trên độ tương đồng ngữ nghĩa (semantic similarity). Thay vì so khớp từ khóa như BM25, cả query và document đều được encode thành vector trong không gian chiều cao, rồi tính cosine similarity.
Document Store (đã index):
Doc A: "Arya Stark là con gái thứ ba của Eddard và Catelyn Stark..."
Doc B: "Jon Snow là con trai ngoài giá thú của Lyanna Stark..."
Doc C: "Daenerys Targaryen là người đứng cuối cùng của vương tộc..."
↓ encode bằng sentence-transformer
Doc A → [0.12, -0.34, 0.87, ...]
Doc B → [-0.05, 0.67, 0.23, ...]
Doc C → [0.91, 0.11, -0.45, ...]
Query: "Arya Stark là ai?"
↓ encode
Query → [0.08, -0.29, 0.71, ...]
↓ cosine similarity
Score(Doc A) = 0.94 ✓ cao nhất
Score(Doc B) = 0.31
Score(Doc C) = 0.12Semantic Gap là vấn đề cốt lõi?
Query và document có hình thức rất khác nhau:
Query: ngắn (3-10 từ), dạng câu hỏi, thường mơ hồ
Document: dài (nhiều câu), dạng mô tả/trình bày, chi tiết
Mặc dù cùng nói về cùng một thực thể, sentence-transformer encode chúng thành các vector nằm ở vùng khác nhau trong không gian embedding. Đây gọi là semantic gap khoảng cách ngữ nghĩa.

Ví dụ cụ thể — Query mơ hồ:
Query: "Cách chữa sốt"
Document thực tế: "Paracetamol (acetaminophen) là thuốc giảm đau hạ sốt
được sử dụng phổ biến nhất. Liều dùng cho người lớn là 500-1000mg
mỗi 4-6 giờ, không vượt quá 4g/ngày."
→ Query nói "chữa sốt" nhưng doc dùng thuật ngữ y khoa "giảm đau hạ sốt",
"acetaminophen", "liều dùng" → vector query và vector doc không gần nhau.HyDE — Hypothetical Document Embeddings
Ý tưởng cốt lõi
Thay vì encode query (ngắn, thưa) → tìm document tương tự, HyDE:
Dùng LLM sinh tài liệu giả định (hypothetical document) từ query
Embed tài liệu giả định đó
Dùng embedding này để tìm document thật
Kết quả: Chuyển từ query → document matching (bất đối xứng) sang document → document matching (đối xứng hơn).

Pipeline chi tiết

Tại sao averaging N=5 hypothetical documents?
Sinh nhiều docs rồi trung bình hóa có 3 tác dụng:
Làm mượt noise (smoothing): Mỗi hypothetical doc có thể chứa thông tin sai (hallucination), nhưng trung bình hóa sẽ loại bỏ chi tiết sai, giữ lại xu hướng ngữ nghĩa chung
Ổn định hóa representation: Giảm variance từ tính ngẫu nhiên của LLM (temperature > 0)
Bắt được nhiều góc nhìn: 5 docs có thể diễn đạt cùng concept theo nhiều cách khác nhau, averaging bao phủ tốt hơn
Doc giả định 1: "Paracetamol là thuốc hạ sốt phổ biến..." → [v₁]
Doc giả định 2: "Khi bị sốt, nên uống ibuprofen hoặc..." → [v₂]
Doc giả định 3: "Sốt cao cần được điều trị bằng thuốc..." → [v₃]
Doc giả định 4: "Thuốc giảm đau acetaminophen giúp hạ..." → [v₄]
Doc giả định 5: "Để chữa sốt at home, bạn có thể dùng..." → [v₅]
Average = (v₁ + v₂ + v₃ + v₄ + v₅) / 5
→ Vector kết quả nắm được "domain y tế, thuốc hạ sốt"
dù từng doc riêng lẻ có thể sai về chi tiết
Hands-on: Triển khai HyDE bằng Python
Setup
pip install haystack-ai sentence-transformers datasets numpy
import os
os.environ["OPENAI_API_KEY"] = "sk-your-key-here"Standard Vector Search
from haystack import Pipeline, Document
from haystack.components.embedders import (
SentenceTransformersDocumentEmbedder,
SentenceTransformersTextEmbedder,
)
from haystack.components.preprocessors import DocumentCleaner, DocumentSplitter
from haystack.components.writers import DocumentWriter
from haystack.document_stores.in_memory import InMemoryDocumentStore
from haystack.components.retrievers.in_memory import InMemoryEmbeddingRetriever
from datasets import load_dataset
# --- Indexing Pipeline ---
EMBEDDER_MODEL = "sentence-transformers/all-MiniLM-L6-v2"
document_store = InMemoryDocumentStore()
indexing = Pipeline()
indexing.add_component("cleaner", DocumentCleaner())
indexing.add_component("splitter", DocumentSplitter(split_by="sentence", split_length=10))
indexing.add_component("embedder", SentenceTransformersDocumentEmbedder(model=EMBEDDER_MODEL))
indexing.add_component("writer", DocumentWriter(document_store=document_store))
indexing.connect("cleaner", "splitter")
indexing.connect("splitter", "embedder")
indexing.connect("embedder", "writer")
# Index Game of Thrones dataset
data = load_dataset("Tuana/game-of-thrones")
indexing.run({
"cleaner": {
"documents": [Document.from_dict(doc) for doc in data["train"]]
}
})
# --- Retrieval Pipeline (Standard) ---
standard_retrieval = Pipeline()
standard_retrieval.add_component(
"query_embedder",
SentenceTransformersTextEmbedder(model=EMBEDDER_MODEL),
)
standard_retrieval.add_component(
"retriever",
InMemoryEmbeddingRetriever(document_store=document_store),
)
standard_retrieval.connect("query_embedder.embedding", "retriever.query_embedding")
# Run
query = "Who is Arya Stark?"
results = standard_retrieval.run({
"query_embedder": {"text": query},
"retriever": {"top_k": 5},
})
for doc in results["retriever"]["documents"]:
print(f"[Score: {doc.score:.4f}] {doc.content[:100]}...")
HyDE Retrieval
from haystack import Pipeline, component, Document
from haystack.components.builders import ChatPromptBuilder
from haystack.components.generators.chat import OpenAIChatGenerator
from haystack.components.converters import OutputAdapter
from haystack.components.embedders import SentenceTransformersDocumentEmbedder
from haystack.dataclasses import ChatMessage
from haystack.components.retrievers.in_memory import InMemoryEmbeddingRetriever
from typing import List
from numpy import array, mean
# --- Custom HyDE Component ---
@component
class HypotheticalDocumentEmbedder:
def __init__(
self,
instruct_llm: str = "gpt-4o-mini",
nr_completions: int = 5,
embedder_model: str = "sentence-transformers/all-MiniLM-L6-v2",
):
self.nr_completions = nr_completions
self.embedder_model = embedder_model
self.generator = OpenAIChatGenerator(
model=instruct_llm,
generation_kwargs={
"n": self.nr_completions,
"temperature": 0.75,
"max_tokens": 400,
},
)
template = [
ChatMessage.from_user(
"""Given a question, generate a paragraph of text \
that answers the question.
Question: {{question}}
Paragraph:"""
)
]
self.prompt_builder = ChatPromptBuilder(template=template)
self.adapter = OutputAdapter(
template="{{answers | build_doc}}",
output_type=List[Document],
custom_filters={
"build_doc": lambda data: [Document(content=d) for d in data]
},
)
self.embedder = SentenceTransformersDocumentEmbedder(
model=embedder_model, progress_bar=False
)
# Internal pipeline: prompt → LLM → adapter → embedder
self.pipeline = Pipeline()
self.pipeline.add_component("prompt_builder", self.prompt_builder)
self.pipeline.add_component("generator", self.generator)
self.pipeline.add_component("adapter", self.adapter)
self.pipeline.add_component("embedder", self.embedder)
self.pipeline.connect("prompt_builder", "generator")
self.pipeline.connect("generator.replies", "adapter.answers")
self.pipeline.connect("adapter.output", "embedder.documents")
@component.output_types(hypothetical_embedding=List[float])
def run(self, query: str):
result = self.pipeline.run(
data={"prompt_builder": {"question": query}}
)
# Average N hypothetical document embeddings
docs = result["embedder"]["documents"]
stacked = array([doc.embedding for doc in docs])
avg_embedding = mean(stacked, axis=0)
return {"hypothetical_embedding": avg_embedding.tolist()}
# --- HyDE Retrieval Pipeline ---
hyde_retrieval = Pipeline()
hyde_retrieval.add_component("hyde", HypotheticalDocumentEmbedder(nr_completions=5))
hyde_retrieval.add_component(
"retriever",
InMemoryEmbeddingRetriever(document_store=document_store),
)
hyde_retrieval.connect("hyde.hypothetical_embedding", "retriever.query_embedding")
# Run
query = "Who is Arya Stark?"
results_hyde = hyde_retrieval.run({
"hyde": {"query": query},
"retriever": {"top_k": 5},
})
for doc in results_hyde["retriever"]["documents"]:
print(f"[Score: {doc.score:.4f}] {doc.content[:100]}...")
So sánh kết quả
def compare_results(query, standard_results, hyde_results):
"""So sánh scores giữa standard và HyDE retrieval."""
print(f"Query: {query}\n")
print(f"{'Standard Search':<30} {'HyDE Search':<30}")
print(f"{'-'*30} {'-'*30}")
std_docs = standard_results["retriever"]["documents"]
hyde_docs = hyde_results["retriever"]["documents"]
for i in range(5):
std_score = std_docs[i].score if i < len(std_docs) else 0
hyde_score = hyde_docs[i].score if i < len(hyde_docs) else 0
std_preview = std_docs[i].content[:25] if i < len(std_docs) else ""
hyde_preview = hyde_docs[i].content[:25] if i < len(hyde_docs) else ""
print(f"#{i+1} [{std_score:.4f}] {std_preview:<18} "
f"[{hyde_score:.4f}] {hyde_preview}")
# Check overlap
std_ids = {d.id for d in std_docs}
hyde_ids = {d.id for d in hyde_docs}
overlap = std_ids & hyde_ids
print(f"\nOverlap: {len(overlap)}/{len(std_ids)} docs appear in both")
if hyde_ids - std_ids:
print(f"HyDE found {len(hyde_ids - std_ids)} NEW docs not in standard results")
compare_results(query, results, results_hyde)
Phân Tích Kỹ Thuật: Tại Sao HyDE Tăng Độ Tương Quan
Từ góc nhìn không gian vector

Ngay cả khi LLM "sai" thì... HyDE vẫn hiệu quả
Đây là điểm counter-intuitive quan trọng nhất. Hypothetical document có thể sai về facts, nhưng HyDE vẫn hoạt động vì sentence-transformer encode hướng ngữ nghĩa (topic, domain, entities), không phải facts chính xác.
Ví dụ: LLM nói "Arya Stark là con gái của Robert Baratheon" (sai) nhưng vector vẫn nằm ở khu vực "Game of Thrones, nhân vật, gia đình Stark" (đúng domain). Averaging 5 docs làm mượt chi tiết sai, giữ lại xu hướng chung
Đối tượng | Factually Correct? | Vector Direction? | Cosine Sim |
Doc giả 1 | ✓ đúng | đúng hướng | cao |
Doc giả 2 | ✗ sai facts | đúng hướng | khá cao |
Doc giả 3 | ~ đúng một phần | đúng hướng | khá cao |
Doc giả 4 | ✓ đúng | đúng hướng | cao |
Doc giả 5 | ✗ sai facts | đúng hướng | khá cao |
Average | không quan trọng | ĐÚNG HƯỚNG | CAO ✓ |
Sentence-transformer "không quan tâm" facts, nó quan tâm semantic direction
Giới hạn của HyDE
Nếu LLM hoàn toàn hiểu sai query và sinh doc ở domain khác, HyDE sẽ retrieve sai
Với queries đã rõ ràng và specific, standard retrieval thường đủ tốt, HyDE thêm cost không cần thiết
Mỗi query cần gọi LLM (thêm ~0.5-2s, tốn API cost)
So Sánh Với Re-ranking (Cross-Encoder)
Re-ranking là gì
Re-ranking là chiến lược post-retrieval, bi-encoder lấy Top-K nhanh, rồi dùng cross-encoder chấm lại chính xác từng cặp (query, doc).

Hands-on: Thêm Re-ranking
# Cài đặt thêm
# pip install sentence-transformers
from sentence_transformers import CrossEncoder
# Load cross-encoder model
cross_encoder = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-6-v2")
def rerank_results(query: str, documents: list, top_n: int = 5):
"""Re-rank retrieved documents using cross-encoder."""
# Tạo cặp (query, doc) cho cross-encoder
pairs = [(query, doc.content) for doc in documents]
# Chấm điểm từng cặp
scores = cross_encoder.predict(pairs)
# Sắp xếp lại theo score
scored_docs = list(zip(documents, scores))
scored_docs.sort(key=lambda x: x[1], reverse=True)
return scored_docs[:top_n]
# Re-rank kết quả từ standard search
query = "Who is Arya Stark?"
standard_results = standard_retrieval.run({
"query_embedder": {"text": query},
"retriever": {"top_k": 20}, # Lấy rộng hơn, để re-rank lọc lại
})
reranked = rerank_results(
query,
standard_results["retriever"]["documents"],
top_n=5,
)
print("After Re-ranking:")
for doc, score in reranked:
print(f" [Cross-Encoder: {score:.4f}] {doc.content[:100]}...")
So sánh ba phương pháp

Khía cạnh | Standard | HyDE | Re-ranking | HyDE + Re-ranking |
|---|---|---|---|---|
Recall | Baseline | Tăng | Giữ nguyên | Tăng |
Precision | Baseline | Tăng nhẹ | Tăng mạnh | Tăng mạnh |
Latency thêm | — | ~0.5-2s (LLM) | ~50-200ms | Cộng dồn |
Cost | — | $$$ (LLM API) | $ (local model) | |
Chạy local? | ✓ | ✗ (cần LLM) | ✓ | Cần LLM |
Lấy lại doc đã miss? | — | ✓ | ✗ | ✓ |
5.4 Khi nào dùng gì
Bối cảnh / Mục tiêu | Query rõ ràng, corpus chất lượng | Query mơ hồ, corpus phức tạp |
Chỉ cần OK | Standard ✓ | |
Cần precision tốt | + Re-ranking ✓ | |
Cần recall tốt | HyDE ✓ | |
Cần cả hai | HyDE + Re-ranking ✓ | |
Budget hạn chế | + Re-ranking ✓ | Thử HyDE với cheap LLM (gpt-4o-mini) |
6. Tổng Kết
HyDE giải quyết semantic gap bằng cách chuyển query thành document-format representation trước khi retrieval. Điểm mạnh nhất là tăng recall — lấy được docs mà standard search bỏ sót. Trade-off chính là chi phí LLM và latency thêm.
Re-ranking giải quyết precision bằng cách chấm lại từng cặp (query, doc) với cross-attention. Điểm mạnh là tăng precision ở top-K với chi phí thấp, nhưng không thể lấy lại docs đã miss ở bước retrieval ban đầu.
Kết hợp cả hai cho kết quả tốt nhất: HyDE cải thiện recall ở bước retrieval, Re-ranking cải thiện precision ở bước post-processing.
Tham Khảo
Gao, L., Ma, J., Xu, J., & Callan, J. (2023). Precise Zero-Shot Dense Retrieval without Relevance Labels. ACL 2023. Paper
