Módulo 3: Query Optimization

Cápsula 06: HyDE — buscar con la respuesta hipotética en lugar de la pregunta

Descripción de la cápsula

Hasta ahora todas las técnicas de query optimization manipulan la query como texto (expansion, rewriting, decomposition). HyDE (Hypothetical Document Embeddings) hace algo distinto y contraintuitivo: en vez de buscar con el embedding de la query, generas una respuesta hipotética con un LLM y embebes esa respuesta. Lo que buscas en el corpus es "documentos similares al tipo de respuesta que un experto daría a esto" — más cercano semánticamente a los docs reales que la pregunta cruda.

El insight es geométrico: queries y documentos viven en regiones distintas del espacio de embeddings. Una query es corta, interrogativa, abstracta. Un documento es largo, declarativo, específico. Aún siendo sobre el mismo tema, su distancia coseno es modesta — típicamente 0.7-0.75. Pero un documento hipotético generado para responder la query vive en la misma región del espacio que los docs reales — su similaridad con docs relevantes sube a 0.85-0.92.

HyDE es la técnica más sofisticada del módulo. Es útil cuando el resto no alcanza, especialmente para queries abiertas en dominios donde el "estilo" del documento es predecible (documentación técnica, artículos académicos, manuales).

Al finalizar esta cápsula serás capaz de:

  • ✅ Explicar el insight geométrico que justifica HyDE
  • ✅ Implementar HyDE básico con un LLM y embebido del doc generado
  • ✅ Implementar las dos variantes principales: Multi-HyDE y Hybrid (query + HyDE)
  • ✅ Decidir cuándo HyDE gana sobre query expansion o rewriting
  • ✅ Anticipar las dos trampas principales: docs hipotéticos que alucinan y costo desproporcionado
  • ✅ Calcular el ROI de HyDE comparado con técnicas más simples

Tiempo estimado: 30-35 minutos


El insight geométrico: queries y docs viven en regiones distintas

Imagina el espacio de embeddings como un mapa donde textos parecidos están cerca. Las queries y los documentos no comparten neighborhood, aunque traten del mismo tema.

                  Espacio de embeddings (visualización 2D)

           ┌─────────────────────────────────────────────────┐
           │                                                  │
           │   Q  Q  Q                                        │
           │     Q   Q                ← región de QUERIES     │
           │   Q  Q                                           │
           │                                                  │
           │           ............                           │
           │            (gap)                                 │
           │                                                  │
           │                          D     D                 │
           │                       D    D       D             │
           │                       D    D    D     ← región   │
           │                          D       D    de DOCS    │
           │                       D     D                    │
           │                                                  │
           └──────────────────────────────────────────────────┘

  cosine(query, doc) = típicamente 0.7-0.75
  (cerca pero no muy cerca — están en regiones distintas)

Por qué pasa: los modelos de embeddings codifican estilo además de tema. Una query "¿cómo implemento OAuth2 en FastAPI?" tiene tono interrogativo, es corta, abstracta. Un documento "To implement OAuth2 in FastAPI, use the OAuth2PasswordBearer class from fastapi.security..." tiene tono declarativo, es largo, concreto. Aunque ambos hablan de OAuth2 + FastAPI, el modelo los pone en regiones diferentes.

El truco de HyDE: generas un documento hipotético con un LLM. Ese doc ahora vive en la región de docs:

Query  ──> LLM ──> Hypothetical doc ──> Embedding
"How to              "To implement                 (vive en
 implement            OAuth2 in FastAPI,            región de
 OAuth2"              use OAuth2PasswordBearer..."  docs reales)

Cuando buscas con el embedding del doc hipotético, encuentras docs reales que son cercanos a él — y como ese doc hipotético "sabe" cómo se ve la respuesta correcta, los docs reales que matchean son típicamente los relevantes.

Mejora típica:

Cosine(query, doc real)        =  0.72
Cosine(hypothetical_doc, real) =  0.88
                                  ────
                                  +22% mejora en similaridad → mejor ranking

Implementación básica

# hyde.py
from openai import OpenAI
import os

client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))


HYDE_PROMPT = """Write a detailed technical document that would perfectly answer this question.

Write as if you are an expert author writing documentation or a tutorial. Be specific,
technical, and use natural language. Include code examples if relevant.

Length: 200-400 words.

Question: {query}

Document:"""


def generate_hypothetical_document(query: str) -> str:
    """Generate a document that would answer the query."""
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {"role": "system", "content": "You are a technical writer creating documentation."},
            {"role": "user", "content": HYDE_PROMPT.format(query=query)},
        ],
        temperature=0.3,  # baja para consistencia
        max_tokens=500,
    )
    return response.choices[0].message.content.strip()


def search_with_hyde(query: str, top_k: int = 5) -> dict:
    """HyDE search: generate hypothetical doc → embed → search."""
    # 1. Generar doc hipotético
    hyde_doc = generate_hypothetical_document(query)

    # 2. Buscar con el doc hipotético (ChromaDB embebe automáticamente)
    results = collection.query(
        query_texts=[hyde_doc],  # Buscamos con el doc, no la query
        n_results=top_k,
    )

    return {
        "query": query,
        "hypothetical_doc": hyde_doc,
        "documents": results['documents'][0],
        "ids": results['ids'][0],
    }


# Probar
result = search_with_hyde(
    "¿Cómo implemento OAuth2 en FastAPI?",
    top_k=5
)

print(f"Hypothetical doc generated:\n{result['hypothetical_doc'][:300]}...\n")
print(f"Top 5 retrieved docs:")
for i, doc in enumerate(result['documents'], 1):
    print(f"\n#{i}: {doc[:120]}...")

Output típico:

Hypothetical doc generated:
Para implementar OAuth2 en FastAPI, primero hay que importar `OAuth2PasswordBearer`
desde el módulo `fastapi.security`. Esta clase actúa como dependency que extrae el
token del header de Authorization. La configuración básica requiere instalar
`python-jose` para manejo de JWT y `passlib` para hashing de passwords.

Pasos típicos:

1. Definir el scheme: `oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")`
2. Crear endpoint de login que devuelve un JWT...

Top 5 retrieved docs:

#1: FastAPI OAuth2 implementation guide using OAuth2PasswordBearer dependency...
#2: Securing FastAPI endpoints with JWT tokens and password hashing using passlib...
#3: How to implement role-based access control in FastAPI applications using OAuth2...

Variantes principales

Variante 1: Multi-HyDE (más robusto)

Generar múltiples documentos hipotéticos con temperature variada, hacer search con cada uno, fusionar con RRF. Más caro pero más robusto contra documentos hipotéticos ocasionalmente erróneos.

def multi_hyde_search(query: str, num_docs: int = 3, top_k: int = 5):
    """Genera N docs hipotéticos y fusiona resultados."""
    all_rankings = []

    for i in range(num_docs):
        # Variar temperatura para diversidad
        temp = 0.2 + (i * 0.2)  # 0.2, 0.4, 0.6
        response = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[
                {"role": "system", "content": "You are a technical writer."},
                {"role": "user", "content": HYDE_PROMPT.format(query=query)},
            ],
            temperature=temp,
            max_tokens=500,
        )
        hyde_doc = response.choices[0].message.content.strip()

        # Buscar con este doc hipotético
        results = collection.query(query_texts=[hyde_doc], n_results=10)
        all_rankings.append(results['ids'][0])

    # Fusionar con RRF (visto en cápsula 03)
    fused = reciprocal_rank_fusion(all_rankings)
    top_ids = [doc_id for doc_id, _ in fused[:top_k]]

    # Recuperar contenido
    return collection.get(ids=top_ids)

Mejora: ~+3-5% recall vs single HyDE. Cuesta 3x más LLM calls.

Variante 2: Hybrid (query + HyDE)

Buscar con la query original AND con el doc hipotético, fusionar. Combina precision de la query directa con recall de HyDE.

def hybrid_hyde_search(query: str, top_k: int = 5):
    """Combina búsqueda con query directa + HyDE."""
    # Búsqueda 1: query directa
    results_query = collection.query(query_texts=[query], n_results=10)

    # Búsqueda 2: HyDE
    hyde_doc = generate_hypothetical_document(query)
    results_hyde = collection.query(query_texts=[hyde_doc], n_results=10)

    # Fusionar con RRF
    fused = reciprocal_rank_fusion([
        results_query['ids'][0],
        results_hyde['ids'][0],
    ])
    top_ids = [doc_id for doc_id, _ in fused[:top_k]]

    return collection.get(ids=top_ids)

Mejora: balance precision-recall. Útil cuando query directa también es buena pero HyDE encuentra docs adicionales.


Cuándo HyDE gana sobre las otras técnicas

                                        ¿Recall es el problema?
                                                  │
                              ┌───────────────────┴──────────────────┐
                              │ Sí                                   │ No (problema = precision)
                              ▼                                      ▼
                  ┌─────────────────────┐           ┌─────────────────────────┐
                  │ ¿La query es        │           │ Considerar:              │
                  │ ambigua             │           │ - Re-ranking (M04)       │
                  │ (1-3 tokens)?        │           │ - Hybrid search (M05)    │
                  └──────────┬──────────┘           │ - Metadata filter (M06)  │
                             │                      └──────────────────────────┘
              ┌──────────────┴───────────────┐
              │ Sí                           │ No
              ▼                              ▼
    ┌──────────────────┐         ┌──────────────────────────┐
    │ Query expansion  │         │ ¿Doc style es predecible │
    │ (cápsula 03)     │         │ y específico al dominio? │
    └──────────────────┘         └────────────┬─────────────┘
                                              │
                              ┌───────────────┴────────────┐
                              │ Sí                         │ No
                              ▼                            ▼
                    ┌─────────────────┐         ┌──────────────────┐
                    │ HyDE 🎯         │         │ Query rewriting   │
                    │                 │         │ (cápsula 04)      │
                    └─────────────────┘         └──────────────────┘

HyDE gana especialmente cuando:

  • El dominio tiene un estilo de documentación predecible (docs técnicos, papers, manuales).
  • Las queries son abiertas y esperan respuestas largas.
  • Recall es la métrica que quieres mejorar (no precision).
  • Latencia >700ms es aceptable.

HyDE NO gana cuando:

  • Queries factuales simples ("¿qué es X?") — el doc hipotético sería trivial y no ayuda.
  • Match exacto importa (códigos, IDs) — HyDE no preserva exactitud.
  • Latencia <500ms requerida.
  • Dominio narrativo no estructurado (literatura, conversación informal).

Trampas y errores comunes

Trampa 1: docs hipotéticos que alucinan

El error: el LLM genera un doc hipotético con información incorrecta. Ej: para "¿cuál es la versión actual de FastAPI?", inventa "FastAPI 5.0".

Síntoma: HyDE busca con el embedding de un doc que dice "FastAPI 5.0", cuando el corpus tiene "FastAPI 0.110". Match pobre.

Cómo prevenir: HyDE NO es para queries factuales con respuestas concretas. Es para queries abiertas donde el "estilo" del doc importa más que la corrección de la respuesta hipotética. Para queries factuales, usar búsqueda directa.

Trampa 2: temperature muy alta = doc hipotético creativo pero fuera de tema

El error: temperature=1.0 para "diversidad".

Síntoma: el LLM genera un doc creativo que se desvía del tema de la query. HyDE busca docs sobre el tema desviado.

Cómo prevenir: temperature=0.2-0.4. El doc hipotético debe ser consistente con la query, no creativo.

Trampa 3: doc hipotético demasiado corto

El error: max_tokens=100. El doc hipotético tiene 50 palabras.

Síntoma: el embedding del doc corto vive más cerca de la región de queries que de la región de docs reales. HyDE pierde su ventaja geométrica.

Cómo prevenir: doc hipotético de 200-400 palabras. Suficiente para imitar el "estilo" de un doc real.

Trampa 4: usar HyDE para queries factuales triviales

El error: search_with_hyde("¿qué es Python?").

Síntoma: generar doc hipotético + 1 búsqueda extra agrega 700ms y costo, para una query que la búsqueda directa resuelve perfectamente.

Cómo prevenir: detectar queries factuales triviales y saltarse HyDE.

def needs_hyde(query: str) -> bool:
    word_count = len(query.split())
    is_definitional = any(query.lower().startswith(w) for w in ["what is", "qué es", "define"])
    
    if is_definitional and word_count <= 5:
        return False  # query factual trivial, no HyDE
    if word_count > 8 and word_count <= 25:
        return True  # query abierta de tamaño medio, HyDE ayuda
    return False

Trampa 5: medir HyDE solo con recall

El error: activas HyDE, recall sube 20%, deployas. No miraste precision.

Síntoma: HyDE encuentra más docs relevantes (recall+) pero también introduce algunos docs tangencialmente relacionados que no eran relevantes (precision-). El sistema parece mejor pero el LLM downstream se confunde.

Cómo prevenir: medir precision Y recall juntas. Si precision cae >3 puntos, no vale la mejora de recall sin un re-ranker que filtre.

Trampa 6: HyDE sin re-rank produce contexto ruidoso

El error: HyDE → top-5 → directo al LLM.

Síntoma: el top-5 de HyDE puede incluir docs que matchean el "estilo" del hypothetical doc pero no responden la query exacta del usuario. El LLM ve contexto parcialmente relevante.

Cómo prevenir: HyDE → top-20 → cross-encoder rerank con la query original → top-5. El rerank con la query original (no el doc hipotético) prioriza relevancia real.

def hyde_with_rerank(query: str, top_k: int = 5):
    # HyDE retrieval amplio
    hyde_doc = generate_hypothetical_document(query)
    results = collection.query(query_texts=[hyde_doc], n_results=20)
    candidates = results['documents'][0]

    # Rerank con la QUERY ORIGINAL (no el hyde_doc)
    reranked = cross_encoder_rerank(query, candidates, top_k=top_k)
    return reranked

Ejercicio aplicado

Escenario: eres AI Engineer en una plataforma de papers académicos. Datos:

  • 500K papers en computer science, indexados con OpenAI text-embedding-3-large
  • Queries típicas: investigadores buscando trabajos relacionados con sus proyectos
  • Ejemplos de queries:
    • "transformer architectures for time-series forecasting" (compleja, abierta)
    • "BERT vs GPT differences" (comparativa)
    • "what is attention mechanism" (factual)
    • "recent advances in retrieval augmented generation 2024" (temporal + abierta)

Métricas:

  • Precision@10: 78%
  • Recall@10: 64% (problema — investigadores se quejan de que no encuentran papers relevantes)

Tu trabajo:

  1. Decide si HyDE aplica para este caso. Para qué tipos de query y para cuáles no.
  2. Diseña pipeline con skip dinámico.
  3. Estima impacto y costo.
Solución

1. Análisis: HyDE es muy buena fit para este dominio

Por qué:

  • Dominio académico = doc style predecible: los papers tienen estructura típica (intro, related work, method, results). HyDE puede generar un doc hipotético que imita ese estilo bien.
  • Queries abiertas: investigadores buscan "trabajos sobre X" — exactamente el tipo de query donde HyDE brilla.
  • Recall es el problema: 64% es bajo. HyDE típicamente aumenta recall +15-25%.
  • Latencia tolerable: investigadores aceptan esperar 2-3 segundos por búsqueda profunda.

Pero NO aplicar HyDE a:

  • Queries factuales triviales: "what is attention mechanism" — la búsqueda directa basta.
  • Queries comparativas: "BERT vs GPT differences" — usar decomposition (cápsula 05), no HyDE.

2. Pipeline con skip dinámico

def smart_paper_search(query: str, top_k: int = 10) -> list[dict]:
    """Pipeline con técnica óptima por tipo de query."""

    # Detectar tipo
    word_count = len(query.split())
    is_factual = any(query.lower().startswith(w) for w in ["what is", "define"])
    is_comparative = "vs" in query.lower() or "compare" in query.lower() or "difference" in query.lower()
    is_open_research = word_count > 5 and not is_factual and not is_comparative

    if is_factual and word_count <= 5:
        # Query factual: búsqueda directa
        results = collection.query(query_texts=[query], n_results=20)
        return cross_encoder_rerank(query, results['documents'][0], top_k=top_k)

    elif is_comparative:
        # Query comparativa: decomposition
        return decompose_then_rerank(query, final_top_k=top_k)

    elif is_open_research:
        # Query abierta de research: HyDE + rerank
        hyde_doc = generate_hypothetical_document(query)
        results = collection.query(query_texts=[hyde_doc], n_results=30)
        # Rerank con query ORIGINAL, no con hyde_doc
        return cross_encoder_rerank(query, results['documents'][0], top_k=top_k)

    else:
        # Default: búsqueda directa con rerank
        results = collection.query(query_texts=[query], n_results=20)
        return cross_encoder_rerank(query, results['documents'][0], top_k=top_k)

3. Estimación de impacto y costo

Impacto esperado en recall:

Categoría             %       Recall actual    Recall con técnica       Mejora ponderada
─────────────────────────────────────────────────────────────────────────────────────────
Open research        50%       60%              ~80% (con HyDE+rerank)   +10 pts
Comparative          15%       55%              ~80% (con decomposition) +3.75 pts
Factual              25%       70%              ~75% (con rerank simple) +1.25 pts
Otras                10%       65%              ~70%                     +0.5 pts

Mejora total esperada: 64% → ~80% recall@10 (+16 puntos)

Latencia:

Categoría        Sin optimization    Con técnica    Latencia extra
──────────────────────────────────────────────────────────────────
Open research    400ms              1100ms          +700ms
Comparative      400ms              1300ms          +900ms
Factual          400ms              500ms           +100ms
Otras            400ms              500ms           +100ms

Latencia promedio ponderada: ~860ms (vs 400ms baseline)

860ms es aceptable para búsqueda académica donde investigadores esperan resultados de calidad.

Costo:

queries_per_day = 2000  # estimación
days_per_month = 30

# Distribución
PCT_HYDE = 0.50  # open research → HyDE
PCT_DECOMP = 0.15  # comparative → decomposition
PCT_DIRECT = 0.35  # factual + otras → directo

# Costos por LLM call
COST_HYDE_LLM = 0.0005      # gpt-4o-mini, ~500 tokens out
COST_DECOMP_LLM = 0.0003    # decomposition decision
COST_RERANK = 0             # cross-encoder local, gratis

monthly_cost = (
    queries_per_day * days_per_month * PCT_HYDE * COST_HYDE_LLM +
    queries_per_day * days_per_month * PCT_DECOMP * COST_DECOMP_LLM
)
print(f"Costo mensual: ${monthly_cost:.2f}")

Resultado: ~$20-25/mes. Despreciable.

Plan de validación:

  1. Construir eval set de 100 queries reales de investigadores con ground truth (papers relevantes anotados manualmente).
  2. Medir baseline sobre el eval set por categoría.
  3. Implementar smart_paper_search con feature flag.
  4. A/B test: 50% pipeline actual, 50% smart_paper_search durante 2 semanas.
  5. Métrica primaria: NDCG@10 (más sensible que precision/recall para ranking).
  6. Si NDCG sube >0.05, deployar.

Riesgos a monitorear:

  • HyDE alucinando para queries con nombres de autores/papers específicos. Mitigación: detectar queries con citation patterns y saltar HyDE.
  • Precision en queries factuales: si rerank hace que precision baje en queries simples, ajustar threshold.
  • Latencia p95: si supera 1.5s, considerar reducir n_results del retrieval inicial.

Resumen y siguiente paso

Lo que aprendiste:

  • HyDE busca con el embedding de un documento hipotético generado por LLM, en lugar de la query.
  • Insight geométrico: doc-to-doc cosine ~0.88 vs query-to-doc cosine ~0.72. Mejor match.
  • Mejora típica: +15-25% recall, especialmente en dominios con doc style predecible.
  • Costo: ~700ms latencia + ~$0.001 por query (LLM call para generación).
  • Variantes: Multi-HyDE (3 docs en paralelo + RRF) y Hybrid (query + HyDE fusionados).
  • HyDE NO sirve para queries factuales triviales ni para match exacto de identificadores.
  • Combinación óptima: HyDE para retrieval amplio + cross-encoder rerank con la query original (no con hyde_doc).
  • Trampa principal: docs hipotéticos que alucinan en queries factuales. Skip dinámico previene.

Checkpoint: antes de avanzar, deberías poder:

  • Explicar por qué buscar con un doc hipotético tiene mejor cosine que buscar con la query directa.
  • Implementar HyDE con structured outputs y skip dinámico.
  • Decidir cuándo HyDE gana sobre query expansion, rewriting o decomposition.

Siguiente cápsula: 07 — Comparación de técnicas de query optimization.

Cubrimos las cuatro técnicas principales: expansion, rewriting, decomposition, HyDE. La cápsula 07 las pone lado a lado con un decision framework reproducible. Es la cápsula que vas a consultar para elegir la técnica correcta dado un escenario.


Recursos

  1. HyDE Paper — Precise Zero-Shot Dense Retrieval (Gao et al., 2022) — Paper original
  2. Pinecone — HyDE Explained — Tutorial visual con benchmarks
  3. LangChain — HyDE Implementation — Implementación de referencia
  4. LlamaIndex — HypotheticalDocumentEmbedder — Patrón en LlamaIndex
  5. Anthropic — Contextual Retrieval — Técnica complementaria
  6. Stanford NLP — Dense Retrieval Surveys — Foundational sobre query-document matching

Tiempo estimado: 30-35 minutos Siguiente: 07-technique-comparison-1.md