HyDE With Hybrid Search

Combine HyDE with PgVector hybrid search, keeping the original question in the search string with include_query.

Run HyDE over PgVector hybrid search. include_query=True keeps the question in the search string so the keyword half of hybrid search still matches the words the user typed. HyDE sets its own model for the generation step.

hyde_with_hybrid_search.py
"""
HyDE With Hybrid Search
=======================
Hybrid search runs two retrievals and merges them: a vector half that matches on
meaning, and a keyword half that matches literal words. Both halves are given the same
query string.

include_query=True searches with the question and the invented passage together.

This example also sets model explicitly. Generating the passage does not need the model
answering the question, and a smaller one is usually enough for a few sentences of
plausible prose.

Setup:
    ./cookbook/scripts/run_pgvector.sh

See also: 12_hyde_query_transformer.py for HyDE against plain vector search.
"""

import asyncio

from agno.agent import Agent
from agno.knowledge.embedder.openai import OpenAIEmbedder
from agno.knowledge.knowledge import Knowledge
from agno.knowledge.query_transformer.hyde import HyDE
from agno.models.openai import OpenAIResponses
from agno.vectordb.pgvector import PgVector
from agno.vectordb.search import SearchType

# ---------------------------------------------------------------------------
# Setup
# ---------------------------------------------------------------------------

db_url = "postgresql+psycopg://ai:ai@localhost:5532/ai"

vector_db = PgVector(
    table_name="hyde_hybrid_demo",
    db_url=db_url,
    search_type=SearchType.hybrid,
    embedder=OpenAIEmbedder(id="text-embedding-3-small"),
)

# Start clean, so re-running does not stack copies from a previous run.
if vector_db.exists():
    vector_db.drop()
vector_db.create()

knowledge = Knowledge(
    vector_db=vector_db,
    query_transformer=HyDE(
        model=OpenAIResponses(id="gpt-5.6-luna"),
        # Keep the question in the search string, so the keyword half of hybrid search
        # still matches on the words the user actually typed.
        include_query=True,
    ),
)

agent = Agent(
    model=OpenAIResponses(id="gpt-5.6-luna"),
    knowledge=knowledge,
    markdown=True,
)

# ---------------------------------------------------------------------------
# Run Demo
# ---------------------------------------------------------------------------

NOTES = [
    (
        "refund-policy",
        "This policy explains when a refund request is approved and which team owns the "
        "decision for orders above the standard threshold.",
    ),
    (
        "q3-support-review",
        "Customers waited an average of six days for money back last quarter because "
        "approvals queued behind a single reviewer in the billing team.",
    ),
    (
        "shipping",
        "Orders ship within two business days, and tracking is emailed once the carrier "
        "scans the parcel.",
    ),
]

QUERY = "Why are refunds slow?"


def show(label: str, results) -> None:
    print(label)
    for document in results:
        snippet = " ".join(document.content.split())[:70]
        print(f"  {document.name}: {snippet}...")
    print()


if __name__ == "__main__":

    async def main():
        for name, text in NOTES:
            await knowledge.ainsert(text_content=text, name=name)

        print("\n" + "=" * 64)
        print("PgVector hybrid search + HyDE")
        print("=" * 64 + "\n")

        plain = Knowledge(vector_db=vector_db)
        show("Searching with the question", await plain.asearch(QUERY, max_results=3))
        show("With HyDE, question kept", await knowledge.asearch(QUERY, max_results=3))

        # The search string itself is what the flag changes, so print it: the ranking
        # above may well be identical on a corpus this small.
        transformed = await knowledge.query_transformer.atransform(QUERY)
        print("Searched with:")
        print(f"  {' '.join(transformed.split())[:150]}...\n")

        await agent.aprint_response(QUERY, stream=True)

    asyncio.run(main())

What Happens

The script prints the ranking for a plain hybrid search and for a HyDE search, then prints the transformed search string, which is the question followed by the hypothetical answer. On a corpus this small the two rankings can be identical. See Search Types for how the transform applies to each search type.

Run the Example

Set up your virtual environment

uv venv --python 3.12
source .venv/bin/activate

Install dependencies

uv pip install -U agno openai pgvector "psycopg[binary]" sqlalchemy

Export your OpenAI API key

export OPENAI_API_KEY="your_openai_api_key_here"

Run PgVector

docker run -d \
  -e POSTGRES_DB=ai \
  -e POSTGRES_USER=ai \
  -e POSTGRES_PASSWORD=ai \
  -e PGDATA=/var/lib/postgresql \
  -v pgvolume:/var/lib/postgresql \
  -p 5532:5432 \
  --name pgvector \
  agnohq/pgvector:18

Run the example

Save the code above as hyde_with_hybrid_search.py, then run:

python hyde_with_hybrid_search.py

Full source: cookbook/07_knowledge/02_building_blocks/13_hyde_with_hybrid_search.py