Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ QueryGym implements the following query reformulation methods:
| **ThinkQE** | Multi-round reasoning-based query expansion with corpus feedback | [Lei et al., 2025](https://arxiv.org/abs/2506.09260) |
| **Query2E** | Query to entity/keyword expansion | [Jagerman et al., 2023](https://arxiv.org/abs/2305.03653)|
| **ReFormeR** | Pattern-based, document-conditioned reformulation via learned transformation rules | [Bigdeli et al., 2026](https://arxiv.org/abs/2604.01417) |
| **HyDE** | Precise Zero-Shot Dense Retrieval without Relevance Labels | [Gao et al., 2022](https://arxiv.org/abs/2212.10496) |

For detailed usage and parameters, see the [Methods Reference](https://querygym.readthedocs.io/en/latest/user-guide/methods-reference/).

Expand Down
10 changes: 10 additions & 0 deletions docs/about/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ All notable changes to querygym will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added
- HyDE reformulation method, from *Precise Zero-Shot Dense Retrieval without Relevance Labels* ([Gao et al., 2022](https://arxiv.org/abs/2212.10496)): generates 8 hypothetical documents per query and appends them to the query
- Task-specific prompts from the original implementation (`hyde.web_search.v1`, `hyde.scifact.v1`, `hyde.arguana.v1`, `hyde.trec_covid.v1`, `hyde.fiqa.v1`, `hyde.dbpedia_entity.v1`, `hyde.trec_news.v1`), selected with the `task` parameter
- `querygym.methods.hyde.task_for_dataset()` maps BEIR datasets to their prompt; every other dataset uses `web search`. The example pipeline uses it when `task` isn't set
- Paper sampling defaults in `defaults.yaml` (`temperature: 0.7`, `max_tokens: 512`, `top_p: 1.0`, `stop: ["\n\n\n"]`)
- `querygym run --output-format plain` writes one column per hypothetical document
- HyDE listed in README supported methods table

## [0.3.0] - 2026-10-04

### Added
Expand Down
21 changes: 21 additions & 0 deletions docs/api/methods.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,3 +135,24 @@ reformulator = qg.create_reformulator(
- `max_demo_len` (int): Optional word truncation for each passage before prompting (default: None)
- `no_thinking` (bool): Prefill `</think>` to disable reasoning traces (default: False)
- `searcher`: Pre-configured searcher instance (recommended)

### HyDE

Hypothetical Document Embeddings, from *Precise Zero-Shot Dense Retrieval without Relevance Labels* ([Gao et al., 2022](https://arxiv.org/abs/2212.10496)), adapted to text: the LLM generates `n_docs` hypothetical documents with a task-specific prompt, and the reformulated query is the original query followed by the documents.

```python
reformulator = qg.create_reformulator(
"hyde",
model="gpt-4.1",
params={"task": "scifact", "n_docs": 8},
llm_config={"temperature": 0.7, "max_tokens": 512, "top_p": 1.0, "stop": ["\n\n\n"]}
)
```

**Requires Context:** No
**Parameters:**
- `task` (str): Prompt template: `web search`, `scifact`, `arguana`, `trec-covid`, `fiqa`, `dbpedia-entity`, `trec-news` (default: `web search`). An unknown task raises `ValueError`
- `n_docs` (int): Number of hypothetical documents to generate (default: 8)
- `repeat_query_weight` (int): Times the original query appears before the documents (default: 1)

`querygym.methods.hyde.task_for_dataset(dataset_id)` returns the task for a dataset id (`web search` for any dataset without a dedicated prompt, or `None`).
2 changes: 2 additions & 0 deletions docs/getting-started/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ Available methods:
- `query2e` - Query to entity expansion
- `csqe` - Context-based sentence extraction
- `thinkqe` - Multi-round reasoning-based passage expansion
- `reformer` - Pattern-based, document-conditioned reformulation
- `hyde` - Hypothetical document generation

### 3. Reformulate Queries

Expand Down
80 changes: 80 additions & 0 deletions docs/user-guide/methods-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Complete reference guide for all query reformulation methods in QueryGym, includ
- [CSQE](#csqe)
- [ThinkQE](#thinkqe)
- [ReFormeR](#reformer)
- [HyDE](#hyde)

---

Expand Down Expand Up @@ -736,6 +737,85 @@ If the pattern selection response cannot be parsed as valid JSON, the method fal

---

### HyDE

**Method Name:** `"hyde"`
**Requires Context:** No
**Description:** Hypothetical Document Embeddings, from *Precise Zero-Shot Dense Retrieval without Relevance Labels* ([Gao et al., 2022](https://arxiv.org/abs/2212.10496)). The LLM writes `n_docs` hypothetical documents that answer the query, using a task-specific prompt. The reformulated query is the original query followed by all generated documents: `query + doc_1 + ... + doc_8`.

!!! note "Text adaptation"
The original HyDE averages the embeddings of the query and the hypothetical documents for dense retrieval. QueryGym emits text instead, so the output works with any retriever (BM25, SPLADE++, dense). The query appears once, matching its one-in-nine weight in the original embedding average. Dense encoders with a 512-token limit (e.g. BGE) only see the query and roughly the first document.

#### Parameters

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `task` | str | `"web search"` | Prompt template to use (see [Tasks](#tasks)). An unknown task raises `ValueError` |
| `n_docs` | int | 8 | Number of hypothetical documents to generate (one LLM call each) |
| `repeat_query_weight` | int | 1 | Number of times the original query appears before the documents |

#### LLM Configuration

Defaults follow the HyDE paper:

| Parameter | Default | Description |
|-----------|---------|-------------|
| `temperature` | 0.7 | Sampling temperature |
| `max_tokens` | 512 | Maximum tokens per document |
| `top_p` | 1.0 | Nucleus sampling |
| `stop` | `["\n\n\n"]` | Stop sequences |

!!! warning
`qg.create_reformulator` fills in `temperature=0.8` and `max_tokens=256` when `llm_config` doesn't set them. Pass `llm_config={"temperature": 0.7, "max_tokens": 512}` to use the paper settings. `querygym run` and the example pipeline config already use them.

#### Tasks

| `task` | Prompt ID | Prompt |
|--------|-----------|--------|
| `web search` | `hyde.web_search.v1` | Please write a passage to answer the question. |
| `scifact` | `hyde.scifact.v1` | Please write a scientific paper passage to support/refute the claim. |
| `arguana` | `hyde.arguana.v1` | Please write a counter argument for the passage. |
| `trec-covid` | `hyde.trec_covid.v1` | Please write a scientific paper passage to answer the question. |
| `fiqa` | `hyde.fiqa.v1` | Please write a financial article passage to answer the question. |
| `dbpedia-entity` | `hyde.dbpedia_entity.v1` | Please write a passage to answer the question. |
| `trec-news` | `hyde.trec_news.v1` | Please write a news passage about the topic. |

The example pipeline (`examples/querygym_pyserini/`) picks the task from the dataset when `task` isn't set, using `querygym.methods.hyde.task_for_dataset()`:

| Dataset | `task` |
|---------|--------|
| `beir-v1.0.0-scifact`, `-arguana`, `-trec-covid`, `-fiqa`, `-dbpedia-entity`, `-trec-news` | matching task |
| Everything else (MS MARCO, DL-HARD, BRIGHT, custom query files) | `web search` |

An explicit `task` always wins over the dataset mapping.

#### Usage Example

```python
import querygym as qg

reformulator = qg.create_reformulator(
"hyde",
model="gpt-4.1",
params={"task": "scifact", "n_docs": 8},
llm_config={"temperature": 0.7, "max_tokens": 512, "top_p": 1.0, "stop": ["\n\n\n"]},
)

result = reformulator.reformulate(qg.QueryItem("q1", "0-dimensional biomaterials show inductive properties."))
print(result.reformulated) # query + 8 hypothetical documents
print(result.metadata["hypothetical_docs"]) # the generated documents
```

#### How It Works

1. **Prompt**: render the prompt for `task` with the query.
2. **Generate**: call the LLM `n_docs` times with the same prompt. Empty generations are dropped.
3. **Concatenate**: the original query (`repeat_query_weight` times) followed by the documents, with newlines flattened. If every generation is empty, the original query is returned.

Metadata: `task`, `n_docs`, `hypothetical_docs`.

---

## See Also

- [API Reference](../api/methods.md) - Technical API documentation
Expand Down
14 changes: 14 additions & 0 deletions docs/user-guide/reformulation.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,19 @@ reformulator = qg.create_reformulator(
)
```

### HyDE

Generates 8 hypothetical documents with a task-specific prompt and appends them to the query.

```python
reformulator = qg.create_reformulator(
"hyde",
model="gpt-4.1",
params={"task": "web search", "n_docs": 8},
llm_config={"temperature": 0.7, "max_tokens": 512},
)
```

## Method Comparison

| Method | Requires Context | Type | Best For |
Expand All @@ -126,6 +139,7 @@ reformulator = qg.create_reformulator(
| query2e | No | Entity expansion | Entity queries |
| csqe | Yes | Sentence extraction | Precision-focused |
| thinkqe | Yes | Iterative reasoning | Multi-round feedback |
| hyde | No | Hypothetical documents | Zero-shot retrieval |

## Custom Parameters

Expand Down
6 changes: 6 additions & 0 deletions examples/querygym_pyserini/reformulate_queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,12 @@ def reformulate_queries(
retrieval_k = method_params.get('retrieval_k', 10)
logging.info(f"Searcher configured with retrieval_k={retrieval_k}")

# HyDE: pick the task-specific prompt from the dataset unless one was given
if method == 'hyde' and 'task' not in method_params:
from querygym.methods.hyde import task_for_dataset
method_params['task'] = task_for_dataset(dataset_name)
logging.info(f"HyDE task (from dataset): {method_params['task']}")

# Create reformulator
logging.info(f"Creating reformulator: {method}")
logging.info(f"Model: {model}")
Expand Down
14 changes: 14 additions & 0 deletions examples/querygym_pyserini/reformulation_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,20 @@ methods:
max_tokens: 128
params: {} # No special parameters

# HyDE: Hypothetical document generation (query + 8 generated passages)
hyde:
enabled: true
model: "qwen2.5:7b"
llm:
temperature: 0.7 # HyDE paper sampling settings
max_tokens: 512
top_p: 1.0
stop: ["\n\n\n"]
params:
n_docs: 8 # Number of hypothetical documents to generate
# Note: task is picked from the dataset (e.g. scifact for beir-v1.0.0-scifact,
# web search otherwise). Set task here only to force one prompt for all datasets.

# LameR: Context-based passage synthesis (requires retrieval)
lamer:
enabled: true
Expand Down
2 changes: 2 additions & 0 deletions querygym/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
CSQE,
ThinkQE,
ReFormeR,
HyDE,
)

# High-level runner
Expand Down Expand Up @@ -241,6 +242,7 @@ def load_examples(path: str, **kwargs):
"CSQE",
"ThinkQE",
"ReFormeR",
"HyDE",
# High-level API
"run_method",
"build_llm",
Expand Down
13 changes: 13 additions & 0 deletions querygym/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,15 @@ def write_plain_format(output_path):
"reformulation_5",
]
)
elif method == "hyde":
# HyDE: qid \t passage_1 \t passage_2 \t ... \t passage_n
num_passages = (
len(results[0].metadata.get("hypothetical_docs", [])) if results else 0
)
if not num_passages:
num_passages = 8 # Default n_docs
header = ["qid"] + [f"passage_{i+1}" for i in range(num_passages)]
w.writerow(header)
elif method == "qa_expand":
# QA-Expand: qid \t final_refined_query
w.writerow(["qid", "refined_query"])
Expand Down Expand Up @@ -348,6 +357,10 @@ def write_plain_format(output_path):
parts = reformulated.replace(original, "").strip().split()
cleaned_parts = [clean_text(p) for p in parts]
w.writerow([r.qid] + cleaned_parts)
elif method == "hyde":
# HyDE: qid \t passage_1 \t passage_2 \t ... \t passage_n
passages = r.metadata.get("hypothetical_docs", [])
w.writerow([r.qid] + [clean_text(p) for p in passages])
elif method == "query2doc":
# Query2Doc: qid \t passage
passage = r.metadata.get("pseudo_doc", "")
Expand Down
10 changes: 10 additions & 0 deletions querygym/config/defaults.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -71,5 +71,15 @@ reformer:
context_docs: 3 # top-N docs fed to pattern selection (step 1)
# patterns_path: null # override to load a custom patterns JSON file

hyde:
llm:
temperature: 0.7 # HyDE paper sampling settings
max_tokens: 512
top_p: 1.0
stop: ["\n\n\n"]
params:
task: "web search" # web search, scifact, arguana, trec-covid, fiqa, dbpedia-entity, trec-news
n_docs: 8 # Number of hypothetical documents to generate

seed: 42
retries: 2
1 change: 1 addition & 0 deletions querygym/core/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from ..methods.csqe import CSQE
from ..methods.thinkqe import ThinkQE
from ..methods.reformer import ReFormeR
from ..methods.hyde import HyDE


def build_llm(cfg: MethodConfig):
Expand Down
1 change: 1 addition & 0 deletions querygym/methods/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@
from .csqe import CSQE
from .thinkqe import ThinkQE
from .reformer import ReFormeR
from .hyde import HyDE
Loading
Loading