Skip to content

Retrieval

varagity.retrieval.base

Retriever protocol and registry (the spec §5.1 registry pattern).

Each retrieval method module defines one implementation decorated with @register("name"); callers resolve the configured method with get_retriever(settings.RETRIEVAL_METHOD). Registered: semantic, bm25, hybrid (v1), reranked (v2, spec_v2 §5), and hyde (ADR-016); adding a method later means one new file plus its import line — no caller edits, exactly as the reranked and hyde additions proved.

Retriever

Bases: Protocol

Interface every retrieval method implements.

runtime_checkable because the protocol appears in Prefect flow signatures (varagity.pipeline.query_flow): Prefect builds a pydantic parameter schema from the annotations at decoration time, which requires types usable with isinstance.

Source code in varagity/retrieval/base.py
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
@runtime_checkable
class Retriever(Protocol):
    """Interface every retrieval method implements.

    ``runtime_checkable`` because the protocol appears in Prefect flow
    signatures (``varagity.pipeline.query_flow``): Prefect builds a pydantic
    parameter schema from the annotations at decoration time, which requires
    types usable with ``isinstance``.
    """

    def encode_query(self, query: str, verbose: int | None = None) -> list[float] | None:
        """Encode a query into the vector its ``retrieve`` would use.

        Split out of ``retrieve`` so callers that track pipeline stages
        (``varagity.pipeline.query_flow``, spec §10.1 step 2) can run query
        embedding as its own stage and pass the result back via
        ``query_vector``.

        Args:
            query: The user's query, unformatted (each method owns its own
                query encoding — e.g. e5 query mode for ``semantic``).
            verbose: Console verbosity (0–2); defaults to
                ``settings.DEFAULT_VERBOSE``.

        Returns:
            The query embedding, or ``None`` for methods that never encode
            queries (``bm25``).
        """
        ...

    def retrieve(
        self,
        query: str,
        k: int,
        verbose: int | None = None,
        *,
        query_vector: list[float] | None = None,
    ) -> list[RetrievedChunk]:
        """Retrieve the top-k chunks for a query.

        Args:
            query: The user's query, unformatted (each method owns its own
                query encoding — e.g. e5 query mode for ``semantic``).
            k: Number of chunks to return.
            verbose: Console verbosity (0–2); defaults to
                ``settings.DEFAULT_VERBOSE``.
            query_vector: Pre-computed :meth:`encode_query` output; when
                omitted, methods that need a vector encode the query
                themselves. Methods that don't (``bm25``) ignore it.

        Returns:
            The top-k chunks, best first, each carrying its ``score`` and the
            full persisted metadata record.
        """
        ...

encode_query(query, verbose=None)

Encode a query into the vector its retrieve would use.

Split out of retrieve so callers that track pipeline stages (varagity.pipeline.query_flow, spec §10.1 step 2) can run query embedding as its own stage and pass the result back via query_vector.

Parameters:

Name Type Description Default
query str

The user's query, unformatted (each method owns its own query encoding — e.g. e5 query mode for semantic).

required
verbose int | None

Console verbosity (0–2); defaults to settings.DEFAULT_VERBOSE.

None

Returns:

Type Description
list[float] | None

The query embedding, or None for methods that never encode

list[float] | None

queries (bm25).

Source code in varagity/retrieval/base.py
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
def encode_query(self, query: str, verbose: int | None = None) -> list[float] | None:
    """Encode a query into the vector its ``retrieve`` would use.

    Split out of ``retrieve`` so callers that track pipeline stages
    (``varagity.pipeline.query_flow``, spec §10.1 step 2) can run query
    embedding as its own stage and pass the result back via
    ``query_vector``.

    Args:
        query: The user's query, unformatted (each method owns its own
            query encoding — e.g. e5 query mode for ``semantic``).
        verbose: Console verbosity (0–2); defaults to
            ``settings.DEFAULT_VERBOSE``.

    Returns:
        The query embedding, or ``None`` for methods that never encode
        queries (``bm25``).
    """
    ...

retrieve(query, k, verbose=None, *, query_vector=None)

Retrieve the top-k chunks for a query.

Parameters:

Name Type Description Default
query str

The user's query, unformatted (each method owns its own query encoding — e.g. e5 query mode for semantic).

required
k int

Number of chunks to return.

required
verbose int | None

Console verbosity (0–2); defaults to settings.DEFAULT_VERBOSE.

None
query_vector list[float] | None

Pre-computed :meth:encode_query output; when omitted, methods that need a vector encode the query themselves. Methods that don't (bm25) ignore it.

None

Returns:

Type Description
list[RetrievedChunk]

The top-k chunks, best first, each carrying its score and the

list[RetrievedChunk]

full persisted metadata record.

Source code in varagity/retrieval/base.py
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
def retrieve(
    self,
    query: str,
    k: int,
    verbose: int | None = None,
    *,
    query_vector: list[float] | None = None,
) -> list[RetrievedChunk]:
    """Retrieve the top-k chunks for a query.

    Args:
        query: The user's query, unformatted (each method owns its own
            query encoding — e.g. e5 query mode for ``semantic``).
        k: Number of chunks to return.
        verbose: Console verbosity (0–2); defaults to
            ``settings.DEFAULT_VERBOSE``.
        query_vector: Pre-computed :meth:`encode_query` output; when
            omitted, methods that need a vector encode the query
            themselves. Methods that don't (``bm25``) ignore it.

    Returns:
        The top-k chunks, best first, each carrying its ``score`` and the
        full persisted metadata record.
    """
    ...

get_retriever(name)

Look up a registered retrieval method by name.

Parameters:

Name Type Description Default
name str

Registry key (e.g. "semantic").

required

Returns:

Type Description
Retriever

The registered retriever instance.

Raises:

Type Description
KeyError

If no retriever is registered under name (message lists the available ones).

Source code in varagity/retrieval/base.py
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
def get_retriever(name: str) -> Retriever:
    """Look up a registered retrieval method by name.

    Args:
        name: Registry key (e.g. ``"semantic"``).

    Returns:
        The registered retriever instance.

    Raises:
        KeyError: If no retriever is registered under ``name`` (message lists
            the available ones).
    """
    if name not in RETRIEVER_REGISTRY:
        raise KeyError(f"Unknown retrieval method {name!r}. Available: {list(RETRIEVER_REGISTRY)}")
    return RETRIEVER_REGISTRY[name]

register(name)

Class decorator registering a retriever instance under name.

Parameters:

Name Type Description Default
name str

Registry key (the RETRIEVAL_METHOD env value).

required

Returns:

Type Description
Callable[[T], T]

The decorator, which instantiates and registers the class unchanged.

Source code in varagity/retrieval/base.py
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
def register[T: type[Any]](name: str) -> Callable[[T], T]:
    """Class decorator registering a retriever instance under ``name``.

    Args:
        name: Registry key (the ``RETRIEVAL_METHOD`` env value).

    Returns:
        The decorator, which instantiates and registers the class unchanged.
    """

    def deco(cls: T) -> T:
        RETRIEVER_REGISTRY[name] = cls()
        return cls

    return deco

varagity.retrieval.semantic

Dense (semantic) retrieval over pgvector (spec §10.1 step 3, §11.2).

Embeds the query in e5 query mode — the asymmetric counterpart of the passage-mode embedding used at ingest time — and runs the cosine top-k search of :class:~varagity.stores.vector_store.ContextualVectorDB.

SemanticRetriever

Retriever backed by the pgvector cosine index.

The registry instantiates it without arguments (no I/O at import time); dependencies then resolve from settings per call — a fresh store connection per query keeps a long-lived chat session robust. Tests and the eval harness inject their own store/client instead.

Source code in varagity/retrieval/semantic.py
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
@register("semantic")
class SemanticRetriever:
    """Retriever backed by the pgvector cosine index.

    The registry instantiates it without arguments (no I/O at import time);
    dependencies then resolve from settings per call — a fresh store
    connection per query keeps a long-lived chat session robust. Tests and
    the eval harness inject their own store/client instead.
    """

    def __init__(
        self,
        *,
        store: ContextualVectorDB | None = None,
        embeddings: EmbeddingsClient | None = None,
    ) -> None:
        """Create the retriever.

        Args:
            store: Vector store to search; opened from settings per call
                when omitted.
            embeddings: Embeddings client for query encoding; resolved via
                the model registry when omitted.
        """
        self._store = store
        self._embeddings = embeddings

    def encode_query(self, query: str, verbose: int | None = None) -> list[float]:
        """Embed a query in e5 query mode (spec §9.5).

        Args:
            query: The user's query, unformatted.
            verbose: Console verbosity (0–2); defaults to
                ``settings.DEFAULT_VERBOSE``.

        Returns:
            The query embedding.

        Raises:
            ValueError: If ``verbose`` is invalid.
            openai.APIError: If query embedding still fails after retries.
        """
        verbose = check_verbose(get_settings().DEFAULT_VERBOSE if verbose is None else verbose)
        embeddings = self._embeddings if self._embeddings is not None else get_model("embedding")
        return embeddings.embed_query(query, verbose=verbose)

    def retrieve(
        self,
        query: str,
        k: int,
        verbose: int | None = None,
        *,
        query_vector: list[float] | None = None,
    ) -> list[RetrievedChunk]:
        """Retrieve the top-k chunks by cosine similarity.

        Args:
            query: The user's query; embedded in e5 query mode.
            k: Number of chunks to return.
            verbose: Console verbosity (0–2); defaults to
                ``settings.DEFAULT_VERBOSE``.
            query_vector: Pre-computed :meth:`encode_query` output; encoded
                here when omitted.

        Returns:
            The top-k chunks, best first, with cosine-similarity scores.

        Raises:
            ValueError: If ``verbose`` is invalid.
            openai.APIError: If query embedding still fails after retries.
            psycopg.OperationalError: If the vector store is unreachable.
        """
        verbose = check_verbose(get_settings().DEFAULT_VERBOSE if verbose is None else verbose)
        if query_vector is None:
            query_vector = self.encode_query(query, verbose=verbose)
        if self._store is not None:
            chunks = self._store.search(query_vector, k, verbose=verbose)
        else:
            with ContextualVectorDB() as store:
                chunks = store.search(query_vector, k, verbose=verbose)
        # Single-arm trace (spec_v2 §9.2): the cosine ranking is the ranking.
        chunks = [
            chunk.model_copy(
                update={
                    "trace": RetrievalTrace(
                        semantic_rank=rank,
                        semantic_score=chunk.score,
                        fused_score=chunk.score,
                        fused_rank=rank,
                        final_rank=rank,
                    )
                }
            )
            for rank, chunk in enumerate(chunks, start=1)
        ]
        v_retrieve(chunks, verbose)
        return chunks

__init__(*, store=None, embeddings=None)

Create the retriever.

Parameters:

Name Type Description Default
store ContextualVectorDB | None

Vector store to search; opened from settings per call when omitted.

None
embeddings EmbeddingsClient | None

Embeddings client for query encoding; resolved via the model registry when omitted.

None
Source code in varagity/retrieval/semantic.py
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
def __init__(
    self,
    *,
    store: ContextualVectorDB | None = None,
    embeddings: EmbeddingsClient | None = None,
) -> None:
    """Create the retriever.

    Args:
        store: Vector store to search; opened from settings per call
            when omitted.
        embeddings: Embeddings client for query encoding; resolved via
            the model registry when omitted.
    """
    self._store = store
    self._embeddings = embeddings

encode_query(query, verbose=None)

Embed a query in e5 query mode (spec §9.5).

Parameters:

Name Type Description Default
query str

The user's query, unformatted.

required
verbose int | None

Console verbosity (0–2); defaults to settings.DEFAULT_VERBOSE.

None

Returns:

Type Description
list[float]

The query embedding.

Raises:

Type Description
ValueError

If verbose is invalid.

APIError

If query embedding still fails after retries.

Source code in varagity/retrieval/semantic.py
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
def encode_query(self, query: str, verbose: int | None = None) -> list[float]:
    """Embed a query in e5 query mode (spec §9.5).

    Args:
        query: The user's query, unformatted.
        verbose: Console verbosity (0–2); defaults to
            ``settings.DEFAULT_VERBOSE``.

    Returns:
        The query embedding.

    Raises:
        ValueError: If ``verbose`` is invalid.
        openai.APIError: If query embedding still fails after retries.
    """
    verbose = check_verbose(get_settings().DEFAULT_VERBOSE if verbose is None else verbose)
    embeddings = self._embeddings if self._embeddings is not None else get_model("embedding")
    return embeddings.embed_query(query, verbose=verbose)

retrieve(query, k, verbose=None, *, query_vector=None)

Retrieve the top-k chunks by cosine similarity.

Parameters:

Name Type Description Default
query str

The user's query; embedded in e5 query mode.

required
k int

Number of chunks to return.

required
verbose int | None

Console verbosity (0–2); defaults to settings.DEFAULT_VERBOSE.

None
query_vector list[float] | None

Pre-computed :meth:encode_query output; encoded here when omitted.

None

Returns:

Type Description
list[RetrievedChunk]

The top-k chunks, best first, with cosine-similarity scores.

Raises:

Type Description
ValueError

If verbose is invalid.

APIError

If query embedding still fails after retries.

OperationalError

If the vector store is unreachable.

Source code in varagity/retrieval/semantic.py
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
def retrieve(
    self,
    query: str,
    k: int,
    verbose: int | None = None,
    *,
    query_vector: list[float] | None = None,
) -> list[RetrievedChunk]:
    """Retrieve the top-k chunks by cosine similarity.

    Args:
        query: The user's query; embedded in e5 query mode.
        k: Number of chunks to return.
        verbose: Console verbosity (0–2); defaults to
            ``settings.DEFAULT_VERBOSE``.
        query_vector: Pre-computed :meth:`encode_query` output; encoded
            here when omitted.

    Returns:
        The top-k chunks, best first, with cosine-similarity scores.

    Raises:
        ValueError: If ``verbose`` is invalid.
        openai.APIError: If query embedding still fails after retries.
        psycopg.OperationalError: If the vector store is unreachable.
    """
    verbose = check_verbose(get_settings().DEFAULT_VERBOSE if verbose is None else verbose)
    if query_vector is None:
        query_vector = self.encode_query(query, verbose=verbose)
    if self._store is not None:
        chunks = self._store.search(query_vector, k, verbose=verbose)
    else:
        with ContextualVectorDB() as store:
            chunks = store.search(query_vector, k, verbose=verbose)
    # Single-arm trace (spec_v2 §9.2): the cosine ranking is the ranking.
    chunks = [
        chunk.model_copy(
            update={
                "trace": RetrievalTrace(
                    semantic_rank=rank,
                    semantic_score=chunk.score,
                    fused_score=chunk.score,
                    fused_rank=rank,
                    final_rank=rank,
                )
            }
        )
        for rank, chunk in enumerate(chunks, start=1)
    ]
    v_retrieve(chunks, verbose)
    return chunks

varagity.retrieval.bm25

Sparse (BM25) retrieval over Elasticsearch (spec §10.1 step 3, §11.3).

A thin wrapper over :class:~varagity.stores.bm25_store.ElasticsearchBM25: search the contextual BM25 index, then hydrate the full chunk records from pgvector by the (doc_id, original_index) identity — Elasticsearch only stores the identity and text fields, and grounded answers need the full metadata (source for citations, context for the prompt).

BM25Retriever

Retriever backed by the contextual BM25 index.

The registry instantiates it without arguments (no I/O at import time); dependencies then resolve from settings per call — fresh store connections per query keep a long-lived chat session robust. Tests and the eval harness inject their own stores instead.

Source code in varagity/retrieval/bm25.py
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
@register("bm25")
class BM25Retriever:
    """Retriever backed by the contextual BM25 index.

    The registry instantiates it without arguments (no I/O at import time);
    dependencies then resolve from settings per call — fresh store
    connections per query keep a long-lived chat session robust. Tests and
    the eval harness inject their own stores instead.
    """

    def __init__(
        self,
        *,
        bm25: ElasticsearchBM25 | None = None,
        store: ContextualVectorDB | None = None,
    ) -> None:
        """Create the retriever.

        Args:
            bm25: BM25 store to search; opened from settings per call when
                omitted.
            store: Vector store for hydration; opened from settings per call
                when omitted.
        """
        self._bm25 = bm25
        self._store = store

    def encode_query(self, query: str, verbose: int | None = None) -> None:
        """Return ``None``: BM25 searches raw text and never encodes queries.

        Args:
            query: The user's query (unused).
            verbose: Console verbosity (0–2); defaults to
                ``settings.DEFAULT_VERBOSE``.

        Returns:
            Always ``None`` (the ``query_vector`` of a bm25-only pipeline
            state stays empty, spec §10.1).

        Raises:
            ValueError: If ``verbose`` is invalid.
        """
        check_verbose(get_settings().DEFAULT_VERBOSE if verbose is None else verbose)
        return None

    def retrieve(
        self,
        query: str,
        k: int,
        verbose: int | None = None,
        *,
        query_vector: list[float] | None = None,
    ) -> list[RetrievedChunk]:
        """Retrieve the top-k chunks by BM25 relevance.

        Args:
            query: The user's query, passed to Elasticsearch unformatted.
            k: Number of chunks to return.
            verbose: Console verbosity (0–2); defaults to
                ``settings.DEFAULT_VERBOSE``.
            query_vector: Accepted for protocol compatibility; unused (BM25
                is purely lexical).

        Returns:
            The top-k chunks, best first, with BM25 scores and full hydrated
            metadata.

        Raises:
            ValueError: If ``verbose`` is invalid.
            elastic_transport.ConnectionError: If Elasticsearch is still
                unreachable after retries.
            psycopg.OperationalError: If the vector store is unreachable.
        """
        verbose = check_verbose(get_settings().DEFAULT_VERBOSE if verbose is None else verbose)
        hits = self._search_bm25(query, k, verbose)
        scored_keys = [((hit.doc_id, hit.original_index), hit.score) for hit in hits]
        # Single-arm trace (spec_v2 §9.2): the BM25 ranking is the ranking.
        traces = {
            key: RetrievalTrace(
                bm25_rank=rank,
                bm25_score=score,
                fused_score=score,
                fused_rank=rank,
                final_rank=rank,
            )
            for rank, (key, score) in enumerate(scored_keys, start=1)
        }
        if self._store is not None:
            chunks = hydrate(scored_keys, self._store, traces=traces)
        else:
            with ContextualVectorDB() as store:
                chunks = hydrate(scored_keys, store, traces=traces)
        v_retrieve(chunks, verbose)
        return chunks

    def _search_bm25(self, query: str, k: int, verbose: int) -> list[BM25Hit]:
        """Run the BM25 search on the injected or a per-call store.

        Args:
            query: The user's query.
            k: Number of hits to request.
            verbose: Validated console verbosity.

        Returns:
            The BM25 hits, best first.
        """
        if self._bm25 is not None:
            return self._bm25.search(query, k, verbose=verbose)
        with ElasticsearchBM25() as bm25:
            return bm25.search(query, k, verbose=verbose)

__init__(*, bm25=None, store=None)

Create the retriever.

Parameters:

Name Type Description Default
bm25 ElasticsearchBM25 | None

BM25 store to search; opened from settings per call when omitted.

None
store ContextualVectorDB | None

Vector store for hydration; opened from settings per call when omitted.

None
Source code in varagity/retrieval/bm25.py
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
def __init__(
    self,
    *,
    bm25: ElasticsearchBM25 | None = None,
    store: ContextualVectorDB | None = None,
) -> None:
    """Create the retriever.

    Args:
        bm25: BM25 store to search; opened from settings per call when
            omitted.
        store: Vector store for hydration; opened from settings per call
            when omitted.
    """
    self._bm25 = bm25
    self._store = store

encode_query(query, verbose=None)

Return None: BM25 searches raw text and never encodes queries.

Parameters:

Name Type Description Default
query str

The user's query (unused).

required
verbose int | None

Console verbosity (0–2); defaults to settings.DEFAULT_VERBOSE.

None

Returns:

Type Description
None

Always None (the query_vector of a bm25-only pipeline

None

state stays empty, spec §10.1).

Raises:

Type Description
ValueError

If verbose is invalid.

Source code in varagity/retrieval/bm25.py
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
def encode_query(self, query: str, verbose: int | None = None) -> None:
    """Return ``None``: BM25 searches raw text and never encodes queries.

    Args:
        query: The user's query (unused).
        verbose: Console verbosity (0–2); defaults to
            ``settings.DEFAULT_VERBOSE``.

    Returns:
        Always ``None`` (the ``query_vector`` of a bm25-only pipeline
        state stays empty, spec §10.1).

    Raises:
        ValueError: If ``verbose`` is invalid.
    """
    check_verbose(get_settings().DEFAULT_VERBOSE if verbose is None else verbose)
    return None

retrieve(query, k, verbose=None, *, query_vector=None)

Retrieve the top-k chunks by BM25 relevance.

Parameters:

Name Type Description Default
query str

The user's query, passed to Elasticsearch unformatted.

required
k int

Number of chunks to return.

required
verbose int | None

Console verbosity (0–2); defaults to settings.DEFAULT_VERBOSE.

None
query_vector list[float] | None

Accepted for protocol compatibility; unused (BM25 is purely lexical).

None

Returns:

Type Description
list[RetrievedChunk]

The top-k chunks, best first, with BM25 scores and full hydrated

list[RetrievedChunk]

metadata.

Raises:

Type Description
ValueError

If verbose is invalid.

ConnectionError

If Elasticsearch is still unreachable after retries.

OperationalError

If the vector store is unreachable.

Source code in varagity/retrieval/bm25.py
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
def retrieve(
    self,
    query: str,
    k: int,
    verbose: int | None = None,
    *,
    query_vector: list[float] | None = None,
) -> list[RetrievedChunk]:
    """Retrieve the top-k chunks by BM25 relevance.

    Args:
        query: The user's query, passed to Elasticsearch unformatted.
        k: Number of chunks to return.
        verbose: Console verbosity (0–2); defaults to
            ``settings.DEFAULT_VERBOSE``.
        query_vector: Accepted for protocol compatibility; unused (BM25
            is purely lexical).

    Returns:
        The top-k chunks, best first, with BM25 scores and full hydrated
        metadata.

    Raises:
        ValueError: If ``verbose`` is invalid.
        elastic_transport.ConnectionError: If Elasticsearch is still
            unreachable after retries.
        psycopg.OperationalError: If the vector store is unreachable.
    """
    verbose = check_verbose(get_settings().DEFAULT_VERBOSE if verbose is None else verbose)
    hits = self._search_bm25(query, k, verbose)
    scored_keys = [((hit.doc_id, hit.original_index), hit.score) for hit in hits]
    # Single-arm trace (spec_v2 §9.2): the BM25 ranking is the ranking.
    traces = {
        key: RetrievalTrace(
            bm25_rank=rank,
            bm25_score=score,
            fused_score=score,
            fused_rank=rank,
            final_rank=rank,
        )
        for rank, (key, score) in enumerate(scored_keys, start=1)
    }
    if self._store is not None:
        chunks = hydrate(scored_keys, self._store, traces=traces)
    else:
        with ContextualVectorDB() as store:
            chunks = hydrate(scored_keys, store, traces=traces)
    v_retrieve(chunks, verbose)
    return chunks

hydrate(scored_keys, store, traces=None)

Hydrate identity tuples into full chunk records (spec §11.4).

Shared by the bm25 and hybrid retrievers.

Parameters:

Name Type Description Default
scored_keys list[tuple[tuple[str, int], float]]

((doc_id, original_index), score) pairs, best first; the score becomes the returned chunk's score.

required
store ContextualVectorDB

The vector store holding the full rows.

required
traces dict[tuple[str, int], RetrievalTrace] | None

Per-identity rank provenance to attach (spec_v2 §9.2); when omitted the chunks carry trace=None (the pre-v2 shape).

None

Returns:

Type Description
list[RetrievedChunk]

The hydrated chunks in the given order. A key missing from pgvector

list[RetrievedChunk]

(the stores disagree — e.g. a partially failed ingest) is dropped

list[RetrievedChunk]

with a warning rather than surfacing an uncitable result.

Source code in varagity/retrieval/bm25.py
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
def hydrate(
    scored_keys: list[tuple[tuple[str, int], float]],
    store: ContextualVectorDB,
    traces: dict[tuple[str, int], RetrievalTrace] | None = None,
) -> list[RetrievedChunk]:
    """Hydrate identity tuples into full chunk records (spec §11.4).

    Shared by the bm25 and hybrid retrievers.

    Args:
        scored_keys: ``((doc_id, original_index), score)`` pairs, best first;
            the score becomes the returned chunk's score.
        store: The vector store holding the full rows.
        traces: Per-identity rank provenance to attach (spec_v2 §9.2); when
            omitted the chunks carry ``trace=None`` (the pre-v2 shape).

    Returns:
        The hydrated chunks in the given order. A key missing from pgvector
        (the stores disagree — e.g. a partially failed ingest) is dropped
        with a warning rather than surfacing an uncitable result.
    """
    rows = store.fetch_by_identity([key for key, _ in scored_keys])
    chunks: list[RetrievedChunk] = []
    for key, score in scored_keys:
        row = rows.get(key)
        if row is None:
            logger.warning(
                "chunk %r is indexed in Elasticsearch but missing from pgvector — "
                "dropping it (re-run `ingest` to reconcile the stores)",
                key,
            )
            continue
        update: dict[str, object] = {"score": score}
        if traces is not None:
            update["trace"] = traces.get(key)
        chunks.append(row.model_copy(update=update))
    return chunks

varagity.retrieval.hybrid

Hybrid retrieval — weighted reciprocal-rank fusion (spec §10.1, §11.4).

The cookbook's fusion, verbatim in structure: over-retrieve k * 10 from each store, combine by weighted reciprocal rank (weight * 1/(rank+1) — only rank positions matter, raw scores from the two stores are never compared), dedupe on the (doc_id, original_index) identity, take the top-k, and hydrate full rows from pgvector. This is the default RETRIEVAL_METHOD (≈49% tier of the Anthropic retrieval-quality ladder); the reranked method (spec_v2 §5) composes this retriever and cross-encodes its output for the ≈67% tier.

HybridRetriever

Retriever fusing the pgvector and Elasticsearch stores.

The registry instantiates it without arguments (no I/O at import time); dependencies then resolve from settings per call — fresh store connections per query keep a long-lived chat session robust. Tests and the eval harness inject their own stores/client instead.

Source code in varagity/retrieval/hybrid.py
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
@register("hybrid")
class HybridRetriever:
    """Retriever fusing the pgvector and Elasticsearch stores.

    The registry instantiates it without arguments (no I/O at import time);
    dependencies then resolve from settings per call — fresh store
    connections per query keep a long-lived chat session robust. Tests and
    the eval harness inject their own stores/client instead.
    """

    def __init__(
        self,
        *,
        store: ContextualVectorDB | None = None,
        bm25: ElasticsearchBM25 | None = None,
        embeddings: EmbeddingsClient | None = None,
    ) -> None:
        """Create the retriever.

        Args:
            store: Vector store (semantic arm + hydration); opened from
                settings per call when omitted.
            bm25: BM25 store (sparse arm); opened from settings per call
                when omitted.
            embeddings: Embeddings client for query encoding; resolved via
                the model registry when omitted.
        """
        self._store = store
        self._bm25 = bm25
        self._embeddings = embeddings

    def encode_query(self, query: str, verbose: int | None = None) -> list[float]:
        """Embed a query in e5 query mode for the semantic arm (spec §9.5).

        Args:
            query: The user's query, unformatted.
            verbose: Console verbosity (0–2); defaults to
                ``settings.DEFAULT_VERBOSE``.

        Returns:
            The query embedding (the BM25 arm searches the raw text).

        Raises:
            ValueError: If ``verbose`` is invalid.
            openai.APIError: If query embedding still fails after retries.
        """
        verbose = check_verbose(get_settings().DEFAULT_VERBOSE if verbose is None else verbose)
        embeddings = self._embeddings if self._embeddings is not None else get_model("embedding")
        return embeddings.embed_query(query, verbose=verbose)

    def retrieve(
        self,
        query: str,
        k: int,
        verbose: int | None = None,
        *,
        query_vector: list[float] | None = None,
    ) -> list[RetrievedChunk]:
        """Retrieve the top-k chunks by weighted rank fusion.

        Args:
            query: The user's query; the semantic arm embeds it in e5 query
                mode, the BM25 arm searches it unformatted.
            k: Number of chunks to return.
            verbose: Console verbosity (0–2); defaults to
                ``settings.DEFAULT_VERBOSE``.
            query_vector: Pre-computed :meth:`encode_query` output; encoded
                here when omitted.

        Returns:
            The top-k chunks, best first, with fused scores (maximum 1.0
            given weights summing to 1.0) and full hydrated metadata.

        Raises:
            ValueError: If ``verbose`` is invalid.
            openai.APIError: If query embedding still fails after retries.
            elastic_transport.ConnectionError: If Elasticsearch is still
                unreachable after retries.
            psycopg.OperationalError: If the vector store is unreachable.
        """
        settings = get_settings()
        verbose = check_verbose(settings.DEFAULT_VERBOSE if verbose is None else verbose)
        if query_vector is None:
            query_vector = self.encode_query(query, verbose=verbose)

        with ExitStack() as stack:
            store = self._store
            if store is None:
                store = stack.enter_context(ContextualVectorDB())
            bm25 = self._bm25
            if bm25 is None:
                bm25 = stack.enter_context(ElasticsearchBM25())

            semantic_results = store.search(query_vector, k * OVERSAMPLE, verbose=0)
            bm25_hits = bm25.search(query, k * OVERSAMPLE, verbose=0)
            fused, traces = fuse_with_traces(
                [((chunk.doc_id, chunk.original_index), chunk.score) for chunk in semantic_results],
                [((hit.doc_id, hit.original_index), hit.score) for hit in bm25_hits],
                semantic_weight=settings.SEMANTIC_WEIGHT,
                bm25_weight=settings.BM25_WEIGHT,
                k=k,
            )
            chunks = hydrate(fused, store, traces=traces)
        v_retrieve(chunks, verbose)
        return chunks

__init__(*, store=None, bm25=None, embeddings=None)

Create the retriever.

Parameters:

Name Type Description Default
store ContextualVectorDB | None

Vector store (semantic arm + hydration); opened from settings per call when omitted.

None
bm25 ElasticsearchBM25 | None

BM25 store (sparse arm); opened from settings per call when omitted.

None
embeddings EmbeddingsClient | None

Embeddings client for query encoding; resolved via the model registry when omitted.

None
Source code in varagity/retrieval/hybrid.py
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
def __init__(
    self,
    *,
    store: ContextualVectorDB | None = None,
    bm25: ElasticsearchBM25 | None = None,
    embeddings: EmbeddingsClient | None = None,
) -> None:
    """Create the retriever.

    Args:
        store: Vector store (semantic arm + hydration); opened from
            settings per call when omitted.
        bm25: BM25 store (sparse arm); opened from settings per call
            when omitted.
        embeddings: Embeddings client for query encoding; resolved via
            the model registry when omitted.
    """
    self._store = store
    self._bm25 = bm25
    self._embeddings = embeddings

encode_query(query, verbose=None)

Embed a query in e5 query mode for the semantic arm (spec §9.5).

Parameters:

Name Type Description Default
query str

The user's query, unformatted.

required
verbose int | None

Console verbosity (0–2); defaults to settings.DEFAULT_VERBOSE.

None

Returns:

Type Description
list[float]

The query embedding (the BM25 arm searches the raw text).

Raises:

Type Description
ValueError

If verbose is invalid.

APIError

If query embedding still fails after retries.

Source code in varagity/retrieval/hybrid.py
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
def encode_query(self, query: str, verbose: int | None = None) -> list[float]:
    """Embed a query in e5 query mode for the semantic arm (spec §9.5).

    Args:
        query: The user's query, unformatted.
        verbose: Console verbosity (0–2); defaults to
            ``settings.DEFAULT_VERBOSE``.

    Returns:
        The query embedding (the BM25 arm searches the raw text).

    Raises:
        ValueError: If ``verbose`` is invalid.
        openai.APIError: If query embedding still fails after retries.
    """
    verbose = check_verbose(get_settings().DEFAULT_VERBOSE if verbose is None else verbose)
    embeddings = self._embeddings if self._embeddings is not None else get_model("embedding")
    return embeddings.embed_query(query, verbose=verbose)

retrieve(query, k, verbose=None, *, query_vector=None)

Retrieve the top-k chunks by weighted rank fusion.

Parameters:

Name Type Description Default
query str

The user's query; the semantic arm embeds it in e5 query mode, the BM25 arm searches it unformatted.

required
k int

Number of chunks to return.

required
verbose int | None

Console verbosity (0–2); defaults to settings.DEFAULT_VERBOSE.

None
query_vector list[float] | None

Pre-computed :meth:encode_query output; encoded here when omitted.

None

Returns:

Type Description
list[RetrievedChunk]

The top-k chunks, best first, with fused scores (maximum 1.0

list[RetrievedChunk]

given weights summing to 1.0) and full hydrated metadata.

Raises:

Type Description
ValueError

If verbose is invalid.

APIError

If query embedding still fails after retries.

ConnectionError

If Elasticsearch is still unreachable after retries.

OperationalError

If the vector store is unreachable.

Source code in varagity/retrieval/hybrid.py
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
def retrieve(
    self,
    query: str,
    k: int,
    verbose: int | None = None,
    *,
    query_vector: list[float] | None = None,
) -> list[RetrievedChunk]:
    """Retrieve the top-k chunks by weighted rank fusion.

    Args:
        query: The user's query; the semantic arm embeds it in e5 query
            mode, the BM25 arm searches it unformatted.
        k: Number of chunks to return.
        verbose: Console verbosity (0–2); defaults to
            ``settings.DEFAULT_VERBOSE``.
        query_vector: Pre-computed :meth:`encode_query` output; encoded
            here when omitted.

    Returns:
        The top-k chunks, best first, with fused scores (maximum 1.0
        given weights summing to 1.0) and full hydrated metadata.

    Raises:
        ValueError: If ``verbose`` is invalid.
        openai.APIError: If query embedding still fails after retries.
        elastic_transport.ConnectionError: If Elasticsearch is still
            unreachable after retries.
        psycopg.OperationalError: If the vector store is unreachable.
    """
    settings = get_settings()
    verbose = check_verbose(settings.DEFAULT_VERBOSE if verbose is None else verbose)
    if query_vector is None:
        query_vector = self.encode_query(query, verbose=verbose)

    with ExitStack() as stack:
        store = self._store
        if store is None:
            store = stack.enter_context(ContextualVectorDB())
        bm25 = self._bm25
        if bm25 is None:
            bm25 = stack.enter_context(ElasticsearchBM25())

        semantic_results = store.search(query_vector, k * OVERSAMPLE, verbose=0)
        bm25_hits = bm25.search(query, k * OVERSAMPLE, verbose=0)
        fused, traces = fuse_with_traces(
            [((chunk.doc_id, chunk.original_index), chunk.score) for chunk in semantic_results],
            [((hit.doc_id, hit.original_index), hit.score) for hit in bm25_hits],
            semantic_weight=settings.SEMANTIC_WEIGHT,
            bm25_weight=settings.BM25_WEIGHT,
            k=k,
        )
        chunks = hydrate(fused, store, traces=traces)
    v_retrieve(chunks, verbose)
    return chunks

fuse(semantic_keys, bm25_keys, *, semantic_weight, bm25_weight, k)

Weighted reciprocal-rank fusion of two ranked lists (spec §11.4).

Each occurrence of an identity contributes weight * 1/(rank+1) for its rank in that list; an identity present in both lists accumulates both contributions (which is also what dedupes it). Ties keep semantic-list order (sorted is stable and the semantic list is scored first).

Parameters:

Name Type Description Default
semantic_keys list[Identity]

(doc_id, original_index) identities from the semantic arm, best first.

required
bm25_keys list[Identity]

Identities from the BM25 arm, best first.

required
semantic_weight float

Weight of the semantic arm.

required
bm25_weight float

Weight of the BM25 arm.

required
k int

Number of fused results to keep.

required

Returns:

Type Description
list[tuple[Identity, float]]

The top-k (identity, fused_score) pairs, best first. The maximum

list[tuple[Identity, float]]

possible score is semantic_weight + bm25_weight (rank 0 in both

list[tuple[Identity, float]]

lists).

Source code in varagity/retrieval/hybrid.py
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
def fuse(
    semantic_keys: list[Identity],
    bm25_keys: list[Identity],
    *,
    semantic_weight: float,
    bm25_weight: float,
    k: int,
) -> list[tuple[Identity, float]]:
    """Weighted reciprocal-rank fusion of two ranked lists (spec §11.4).

    Each occurrence of an identity contributes ``weight * 1/(rank+1)`` for
    its rank in that list; an identity present in both lists accumulates both
    contributions (which is also what dedupes it). Ties keep semantic-list
    order (``sorted`` is stable and the semantic list is scored first).

    Args:
        semantic_keys: ``(doc_id, original_index)`` identities from the
            semantic arm, best first.
        bm25_keys: Identities from the BM25 arm, best first.
        semantic_weight: Weight of the semantic arm.
        bm25_weight: Weight of the BM25 arm.
        k: Number of fused results to keep.

    Returns:
        The top-k ``(identity, fused_score)`` pairs, best first. The maximum
        possible score is ``semantic_weight + bm25_weight`` (rank 0 in both
        lists).
    """
    scores: defaultdict[Identity, float] = defaultdict(float)
    for rank, key in enumerate(semantic_keys):
        scores[key] += semantic_weight * 1.0 / (rank + 1)
    for rank, key in enumerate(bm25_keys):
        scores[key] += bm25_weight * 1.0 / (rank + 1)
    top = sorted(scores, key=scores.__getitem__, reverse=True)[:k]
    return [(key, scores[key]) for key in top]

fuse_with_traces(semantic, bm25, *, semantic_weight, bm25_weight, k)

Fuse two scored ranked lists, keeping the per-arm ranks (spec_v2 §9.2).

The trace-building sibling of :func:fuse: fusion math is identical (delegated), but the per-arm rank/score maps :func:fuse builds and discards are preserved into a :class:~varagity.stores.records .RetrievalTrace per fused survivor — the "why it ranked here" data the provenance panel renders. Raw arm scores still never enter the fusion math; they ride along for display only.

Parameters:

Name Type Description Default
semantic list[tuple[Identity, float]]

(identity, cosine_score) pairs from the semantic arm, best first.

required
bm25 list[tuple[Identity, float]]

(identity, bm25_score) pairs from the BM25 arm, best first.

required
semantic_weight float

Weight of the semantic arm.

required
bm25_weight float

Weight of the BM25 arm.

required
k int

Number of fused results to keep.

required

Returns:

Name Type Description
The list[tuple[Identity, float]]

func:fuse result (top-k (identity, fused_score) pairs,

dict[Identity, RetrievalTrace]

best first) plus a per-identity trace for each survivor, with

tuple[list[tuple[Identity, float]], dict[Identity, RetrievalTrace]]

1-based per-arm/fused ranks and final_rank == fused_rank (the

tuple[list[tuple[Identity, float]], dict[Identity, RetrievalTrace]]

rerank stage, when active, overwrites the final fields downstream).

Source code in varagity/retrieval/hybrid.py
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
def fuse_with_traces(
    semantic: list[tuple[Identity, float]],
    bm25: list[tuple[Identity, float]],
    *,
    semantic_weight: float,
    bm25_weight: float,
    k: int,
) -> tuple[list[tuple[Identity, float]], dict[Identity, RetrievalTrace]]:
    """Fuse two *scored* ranked lists, keeping the per-arm ranks (spec_v2 §9.2).

    The trace-building sibling of :func:`fuse`: fusion math is identical
    (delegated), but the per-arm rank/score maps :func:`fuse` builds and
    discards are preserved into a :class:`~varagity.stores.records
    .RetrievalTrace` per fused survivor — the "why it ranked here" data the
    provenance panel renders. Raw arm scores still never enter the fusion
    math; they ride along for display only.

    Args:
        semantic: ``(identity, cosine_score)`` pairs from the semantic arm,
            best first.
        bm25: ``(identity, bm25_score)`` pairs from the BM25 arm, best first.
        semantic_weight: Weight of the semantic arm.
        bm25_weight: Weight of the BM25 arm.
        k: Number of fused results to keep.

    Returns:
        The :func:`fuse` result (top-k ``(identity, fused_score)`` pairs,
        best first) plus a per-identity trace for each survivor, with
        1-based per-arm/fused ranks and ``final_rank == fused_rank`` (the
        rerank stage, when active, overwrites the final fields downstream).
    """
    fused = fuse(
        [key for key, _ in semantic],
        [key for key, _ in bm25],
        semantic_weight=semantic_weight,
        bm25_weight=bm25_weight,
        k=k,
    )
    semantic_arm = {key: (rank, score) for rank, (key, score) in enumerate(semantic, start=1)}
    bm25_arm = {key: (rank, score) for rank, (key, score) in enumerate(bm25, start=1)}
    traces: dict[Identity, RetrievalTrace] = {}
    for fused_rank, (key, fused_score) in enumerate(fused, start=1):
        semantic_entry = semantic_arm.get(key)
        bm25_entry = bm25_arm.get(key)
        traces[key] = RetrievalTrace(
            semantic_rank=semantic_entry[0] if semantic_entry else None,
            semantic_score=semantic_entry[1] if semantic_entry else None,
            bm25_rank=bm25_entry[0] if bm25_entry else None,
            bm25_score=bm25_entry[1] if bm25_entry else None,
            fused_score=fused_score,
            fused_rank=fused_rank,
            final_rank=fused_rank,
        )
    return fused, traces

varagity.retrieval.reranked

Cross-encoder re-ranking over a base retriever (spec_v2 §5 — the ≈67% tier).

Re-ranking composes an existing retrieval method rather than replacing fusion: over-fetch a wide candidate pool (RERANK_CANDIDATES) from the configured RERANK_BASE_METHOD, cross-encode every candidate's original content against the query (the contextual blurb already did its job at the embedding/BM25 stage), and keep the RERANK_TOP_N most relevant — the Anthropic cookbook's 150→20 over-fetch, scaled to this corpus.

RERANK_ENABLED is a kill switch orthogonal to method selection: when false the retriever degrades to its base method's ranking (logged), so a GUI toggle and the eval baseline both work without renaming the method.

RerankedRetriever

Base retriever → cross-encoder rerank → top-N (spec_v2 §5.2).

The registry instantiates it without arguments (no I/O at import time); the base retriever and rerank client then resolve from settings per call, so fusion improvements flow through and a config change needs no restart. Tests and the eval harness inject their own instead.

Source code in varagity/retrieval/reranked.py
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
@register("reranked")
class RerankedRetriever:
    """Base retriever → cross-encoder rerank → top-N (spec_v2 §5.2).

    The registry instantiates it without arguments (no I/O at import time);
    the base retriever and rerank client then resolve from settings per
    call, so fusion improvements flow through and a config change needs no
    restart. Tests and the eval harness inject their own instead.
    """

    def __init__(
        self,
        *,
        base: Retriever | None = None,
        rerank: RerankClient | None = None,
    ) -> None:
        """Create the retriever.

        Args:
            base: Retriever producing the candidate pool; resolved from
                ``settings.RERANK_BASE_METHOD`` per call when omitted.
            rerank: Cross-encoder client; resolved via the model registry
                (``get_model("rerank")``) per call when omitted.
        """
        self._base = base
        self._rerank = rerank

    def _base_retriever(self) -> Retriever:
        """Resolve the composed base retriever (injected or from settings).

        Returns:
            The base retriever instance.

        Raises:
            KeyError: If ``settings.RERANK_BASE_METHOD`` names an
                unregistered method (config validation makes this
                unreachable for env-sourced settings).
        """
        if self._base is not None:
            return self._base
        return get_retriever(get_settings().RERANK_BASE_METHOD)

    def encode_query(self, query: str, verbose: int | None = None) -> list[float] | None:
        """Encode a query exactly as the base retriever would (spec_v2 §5.2).

        Args:
            query: The user's query, unformatted.
            verbose: Console verbosity (0–2); defaults to
                ``settings.DEFAULT_VERBOSE``.

        Returns:
            The base retriever's query encoding (``None`` for a ``bm25``
            base — nothing to encode).

        Raises:
            ValueError: If ``verbose`` is invalid.
            openai.APIError: If query embedding still fails after retries.
        """
        return self._base_retriever().encode_query(query, verbose)

    def retrieve(
        self,
        query: str,
        k: int,
        verbose: int | None = None,
        *,
        query_vector: list[float] | None = None,
    ) -> list[RetrievedChunk]:
        """Retrieve a wide candidate pool, cross-encode it, keep the top-N.

        The pool is ``max(RERANK_CANDIDATES, k)`` from the base retriever;
        the cut is ``min(k, RERANK_TOP_N)`` (re-ranking narrows — it never
        returns more than the caller asked for). With ``RERANK_ENABLED``
        off, the base ranking passes through the same cut, logged as a
        degradation.

        Args:
            query: The user's query; the base retriever owns its encoding,
                the cross-encoder scores the raw pair.
            k: Number of chunks the caller wants (bounds the ``top_n`` cut).
            verbose: Console verbosity (0–2); defaults to
                ``settings.DEFAULT_VERBOSE``.
            query_vector: Pre-computed :meth:`encode_query` output; passed
                through to the base retriever.

        Returns:
            The ``min(k, RERANK_TOP_N)`` most relevant chunks, best first,
            with cross-encoder scores and the full rank provenance trace
            (or the base ranking when the kill switch is off).

        Raises:
            ValueError: If ``verbose`` is invalid, or the rerank request is
                permanently rejected (e.g. ``RERANK_MODEL`` is not a served
                cross-encoder).
            httpx.HTTPError: If the rerank request still fails after
                retries.
        """
        settings = get_settings()
        verbose = check_verbose(settings.DEFAULT_VERBOSE if verbose is None else verbose)
        top_n = min(k, settings.RERANK_TOP_N)
        pool = max(settings.RERANK_CANDIDATES, k)
        candidates = self._base_retriever().retrieve(
            query, k=pool, verbose=0, query_vector=query_vector
        )
        if not settings.RERANK_ENABLED:
            logger.info(
                "RERANK_ENABLED=false — degrading to the %r base method's ranking",
                settings.RERANK_BASE_METHOD,
            )
            chunks = candidates[:top_n]
        else:
            client = self._rerank if self._rerank is not None else get_model("rerank")
            rerank_started = time.perf_counter()
            scored = client.rerank(query, [chunk.content for chunk in candidates], verbose=0)
            chunks = apply_rerank(candidates, scored)[:top_n]
            # The rerank sub-stage's share of retrieval (spec_v2 §6.2 —
            # "is it earning its latency?"); the flow's `retrieve`
            # observation includes it.
            metrics.observe_query_stage("rerank", "reranked", time.perf_counter() - rerank_started)
        v_retrieve(chunks, verbose)
        return chunks

__init__(*, base=None, rerank=None)

Create the retriever.

Parameters:

Name Type Description Default
base Retriever | None

Retriever producing the candidate pool; resolved from settings.RERANK_BASE_METHOD per call when omitted.

None
rerank RerankClient | None

Cross-encoder client; resolved via the model registry (get_model("rerank")) per call when omitted.

None
Source code in varagity/retrieval/reranked.py
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
def __init__(
    self,
    *,
    base: Retriever | None = None,
    rerank: RerankClient | None = None,
) -> None:
    """Create the retriever.

    Args:
        base: Retriever producing the candidate pool; resolved from
            ``settings.RERANK_BASE_METHOD`` per call when omitted.
        rerank: Cross-encoder client; resolved via the model registry
            (``get_model("rerank")``) per call when omitted.
    """
    self._base = base
    self._rerank = rerank

encode_query(query, verbose=None)

Encode a query exactly as the base retriever would (spec_v2 §5.2).

Parameters:

Name Type Description Default
query str

The user's query, unformatted.

required
verbose int | None

Console verbosity (0–2); defaults to settings.DEFAULT_VERBOSE.

None

Returns:

Type Description
list[float] | None

The base retriever's query encoding (None for a bm25

list[float] | None

base — nothing to encode).

Raises:

Type Description
ValueError

If verbose is invalid.

APIError

If query embedding still fails after retries.

Source code in varagity/retrieval/reranked.py
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
def encode_query(self, query: str, verbose: int | None = None) -> list[float] | None:
    """Encode a query exactly as the base retriever would (spec_v2 §5.2).

    Args:
        query: The user's query, unformatted.
        verbose: Console verbosity (0–2); defaults to
            ``settings.DEFAULT_VERBOSE``.

    Returns:
        The base retriever's query encoding (``None`` for a ``bm25``
        base — nothing to encode).

    Raises:
        ValueError: If ``verbose`` is invalid.
        openai.APIError: If query embedding still fails after retries.
    """
    return self._base_retriever().encode_query(query, verbose)

retrieve(query, k, verbose=None, *, query_vector=None)

Retrieve a wide candidate pool, cross-encode it, keep the top-N.

The pool is max(RERANK_CANDIDATES, k) from the base retriever; the cut is min(k, RERANK_TOP_N) (re-ranking narrows — it never returns more than the caller asked for). With RERANK_ENABLED off, the base ranking passes through the same cut, logged as a degradation.

Parameters:

Name Type Description Default
query str

The user's query; the base retriever owns its encoding, the cross-encoder scores the raw pair.

required
k int

Number of chunks the caller wants (bounds the top_n cut).

required
verbose int | None

Console verbosity (0–2); defaults to settings.DEFAULT_VERBOSE.

None
query_vector list[float] | None

Pre-computed :meth:encode_query output; passed through to the base retriever.

None

Returns:

Type Description
list[RetrievedChunk]

The min(k, RERANK_TOP_N) most relevant chunks, best first,

list[RetrievedChunk]

with cross-encoder scores and the full rank provenance trace

list[RetrievedChunk]

(or the base ranking when the kill switch is off).

Raises:

Type Description
ValueError

If verbose is invalid, or the rerank request is permanently rejected (e.g. RERANK_MODEL is not a served cross-encoder).

HTTPError

If the rerank request still fails after retries.

Source code in varagity/retrieval/reranked.py
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
def retrieve(
    self,
    query: str,
    k: int,
    verbose: int | None = None,
    *,
    query_vector: list[float] | None = None,
) -> list[RetrievedChunk]:
    """Retrieve a wide candidate pool, cross-encode it, keep the top-N.

    The pool is ``max(RERANK_CANDIDATES, k)`` from the base retriever;
    the cut is ``min(k, RERANK_TOP_N)`` (re-ranking narrows — it never
    returns more than the caller asked for). With ``RERANK_ENABLED``
    off, the base ranking passes through the same cut, logged as a
    degradation.

    Args:
        query: The user's query; the base retriever owns its encoding,
            the cross-encoder scores the raw pair.
        k: Number of chunks the caller wants (bounds the ``top_n`` cut).
        verbose: Console verbosity (0–2); defaults to
            ``settings.DEFAULT_VERBOSE``.
        query_vector: Pre-computed :meth:`encode_query` output; passed
            through to the base retriever.

    Returns:
        The ``min(k, RERANK_TOP_N)`` most relevant chunks, best first,
        with cross-encoder scores and the full rank provenance trace
        (or the base ranking when the kill switch is off).

    Raises:
        ValueError: If ``verbose`` is invalid, or the rerank request is
            permanently rejected (e.g. ``RERANK_MODEL`` is not a served
            cross-encoder).
        httpx.HTTPError: If the rerank request still fails after
            retries.
    """
    settings = get_settings()
    verbose = check_verbose(settings.DEFAULT_VERBOSE if verbose is None else verbose)
    top_n = min(k, settings.RERANK_TOP_N)
    pool = max(settings.RERANK_CANDIDATES, k)
    candidates = self._base_retriever().retrieve(
        query, k=pool, verbose=0, query_vector=query_vector
    )
    if not settings.RERANK_ENABLED:
        logger.info(
            "RERANK_ENABLED=false — degrading to the %r base method's ranking",
            settings.RERANK_BASE_METHOD,
        )
        chunks = candidates[:top_n]
    else:
        client = self._rerank if self._rerank is not None else get_model("rerank")
        rerank_started = time.perf_counter()
        scored = client.rerank(query, [chunk.content for chunk in candidates], verbose=0)
        chunks = apply_rerank(candidates, scored)[:top_n]
        # The rerank sub-stage's share of retrieval (spec_v2 §6.2 —
        # "is it earning its latency?"); the flow's `retrieve`
        # observation includes it.
        metrics.observe_query_stage("rerank", "reranked", time.perf_counter() - rerank_started)
    v_retrieve(chunks, verbose)
    return chunks

apply_rerank(candidates, scored)

Reorder candidates by cross-encoder relevance, filling the rerank trace.

Each result's index points into candidates (which arrive best-first from the base retriever, so index + 1 is the pre-rerank rank). The surviving chunk's score becomes the cross-encoder relevance — the "final score" the provenance panel shows — while the base retriever's fused score/ranks stay intact on the trace.

Parameters:

Name Type Description Default
candidates list[RetrievedChunk]

The base retriever's candidate pool, best first.

required
scored list[RerankResult]

Cross-encoder judgments for (a subset of) the candidates.

required

Returns:

Type Description
list[RetrievedChunk]

The judged candidates, most relevant first, each with

list[RetrievedChunk]

rerank_score, rerank_delta (pre-rank − post-rank: + moved

list[RetrievedChunk]

up, − moved down), and final_rank recorded on its trace. A

list[RetrievedChunk]

candidate without a base trace (an injected test fake) gets one

list[RetrievedChunk]

built from its pre-rerank score and rank.

Source code in varagity/retrieval/reranked.py
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
def apply_rerank(
    candidates: list[RetrievedChunk],
    scored: list[RerankResult],
) -> list[RetrievedChunk]:
    """Reorder candidates by cross-encoder relevance, filling the rerank trace.

    Each result's ``index`` points into ``candidates`` (which arrive
    best-first from the base retriever, so ``index + 1`` is the pre-rerank
    rank). The surviving chunk's ``score`` becomes the cross-encoder
    relevance — the "final score" the provenance panel shows — while the
    base retriever's fused score/ranks stay intact on the trace.

    Args:
        candidates: The base retriever's candidate pool, best first.
        scored: Cross-encoder judgments for (a subset of) the candidates.

    Returns:
        The judged candidates, most relevant first, each with
        ``rerank_score``, ``rerank_delta`` (pre-rank − post-rank: + moved
        up, − moved down), and ``final_rank`` recorded on its trace. A
        candidate without a base trace (an injected test fake) gets one
        built from its pre-rerank score and rank.
    """
    ordered = sorted(scored, key=lambda result: result.relevance_score, reverse=True)
    reranked: list[RetrievedChunk] = []
    for final_rank, result in enumerate(ordered, start=1):
        candidate = candidates[result.index]
        pre_rank = result.index + 1
        trace = candidate.trace
        if trace is None:
            trace = RetrievalTrace(
                fused_score=candidate.score, fused_rank=pre_rank, final_rank=pre_rank
            )
        trace = trace.model_copy(
            update={
                "rerank_score": result.relevance_score,
                "rerank_delta": pre_rank - final_rank,
                "final_rank": final_rank,
            }
        )
        reranked.append(
            candidate.model_copy(update={"score": result.relevance_score, "trace": trace})
        )
    return reranked

varagity.retrieval.hyde

HyDE — Hypothetical Document Embeddings over a base retriever (ADR-016).

The Gao et al. (2022) pattern adapted to the registry convention: one non-streaming LLM call writes a hypothetical answer passage for the query, and the dense arm searches with that passage's embedding instead of the query's. The passage is embedded in e5 passage mode (:meth:~varagity.models.embeddings.EmbeddingsClient.embed_passages — the paper's document-encoder choice), landing it in the same vector space as the ingested corpus, where passage↔passage neighbors are exactly the chunks that look like the answer.

Like reranked, HyDE composes a base retriever (HYDE_BASE_METHOD: semantic | hybrid) rather than forking fusion — and the substitution is dense-arm only: the base receives the user's original query text (a hybrid base's BM25 arm keeps its exact keyword recall) plus the hypothetical passage's vector via the query_vector seam. Pairing with the cross-encoder stacks the other way around: RETRIEVAL_METHOD=reranked + RERANK_BASE_METHOD=hyde, so the reranker judges candidates against the user's real query, never the hypothetical (hyde therefore refuses reranked as its own base — config-validated).

Failure is a fallback, not an error (the condense-stage posture): a transient LLM failure (after the client's own tenacity retries), an empty cleaned passage, or an absurdly long one all degrade to the base method's raw-query retrieval at WARNING. HYDE_ENABLED=false is the kill switch, checked inside the retriever exactly as RERANK_ENABLED is — deliberately orthogonal to method selection. The generated passage must pass through :func:~varagity.models.llm.clean_response: llama.cpp emits <think> blocks and the non-streaming :meth:~varagity.models.llm.LLMClient.generate does not strip them — an unstripped one embedded as the search probe silently destroys retrieval.

HydeRetriever

LLM hypothetical passage → passage-mode embedding → base retriever.

The registry instantiates it without arguments (no I/O at import time); the base retriever and model clients then resolve from settings per call, so a config change needs no restart. Tests and the eval harness inject their own instead.

Source code in varagity/retrieval/hyde.py
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
@register("hyde")
class HydeRetriever:
    """LLM hypothetical passage → passage-mode embedding → base retriever.

    The registry instantiates it without arguments (no I/O at import time);
    the base retriever and model clients then resolve from settings per
    call, so a config change needs no restart. Tests and the eval harness
    inject their own instead.
    """

    def __init__(
        self,
        *,
        base: Retriever | None = None,
        llm: LLMClient | None = None,
        embeddings: EmbeddingsClient | None = None,
    ) -> None:
        """Create the retriever.

        Args:
            base: Retriever the hypothetical-passage vector is handed to;
                resolved from ``settings.HYDE_BASE_METHOD`` per call when
                omitted.
            llm: Chat client generating the passage; resolved via the model
                registry (``settings.HYDE_MODEL_TYPE``) per call when
                omitted.
            embeddings: Embeddings client for the passage-mode encoding;
                resolved via the model registry (``get_model("embedding")``)
                per call when omitted.
        """
        self._base = base
        self._llm = llm
        self._embeddings = embeddings

    def _base_retriever(self) -> Retriever:
        """Resolve the composed base retriever (injected or from settings).

        Returns:
            The base retriever instance.

        Raises:
            KeyError: If ``settings.HYDE_BASE_METHOD`` names an unregistered
                method (config validation makes this unreachable for
                env-sourced settings).
        """
        if self._base is not None:
            return self._base
        return get_retriever(get_settings().HYDE_BASE_METHOD)

    def _hypothetical(self, query: str, verbose: int) -> str | None:
        """Generate the hypothetical answer passage, or ``None`` to fall back.

        One non-streaming LLM call, post-processed exactly like the
        condense stage: ``<think>`` stages stripped
        (:func:`~varagity.models.llm.clean_response`), a prompt-label echo
        removed, then the empty/overlong guards. Every unusable outcome —
        including a transport failure after the client's own retries —
        returns ``None`` at ``WARNING`` so the caller degrades to the
        base method's raw-query retrieval instead of failing the turn.

        Args:
            query: The search query (the chat engine's ``search_query`` —
                already condensed when that stage ran).
            verbose: Validated console verbosity (0–2).

        Returns:
            The cleaned passage, or ``None`` when no usable passage was
            generated.
        """
        settings = get_settings()
        prompt = HYDE_PROMPT.format(query=query)
        # HYDE_MODEL_TYPE is validated to the LLM aliases, so the registry
        # always resolves an LLMClient here; the cast (rather than an
        # isinstance gate) keeps duck-typed test doubles usable at this
        # seam, matching the flows' injectable-fake convention.
        client = (
            self._llm
            if self._llm is not None
            else cast("LLMClient", get_model(settings.HYDE_MODEL_TYPE))
        )
        started = time.perf_counter()
        try:
            # verbose=0: the sub-call renders nothing; v_hyde below is this
            # stage's console output (the reranked-retriever pattern).
            raw = client.generate(
                [{"role": "user", "content": prompt}],
                max_tokens=settings.HYDE_MAX_TOKENS,
                verbose=0,
            )
        except Exception:  # any failure falls back — the query must not die here
            logger.warning("HyDE LLM call failed — searching with the raw query", exc_info=True)
            return None
        passage = clean_response(raw)
        if passage.upper().startswith(HYDE_PASSAGE_LABEL):
            passage = passage[len(HYDE_PASSAGE_LABEL) :].strip()
        if not passage:
            logger.warning("HyDE returned an empty passage — searching with the raw query")
            return None
        if len(passage) > settings.HYDE_MAX_CHARS:
            logger.warning(
                "HyDE passage is %d chars (HYDE_MAX_CHARS=%d) — the generator "
                "misbehaved; searching with the raw query",
                len(passage),
                settings.HYDE_MAX_CHARS,
            )
            return None
        # The generation sub-stage's share of the embed stage ("is it
        # earning its latency?" — the rerank sub-stage pattern); the flow's
        # `embed` observation includes it.
        metrics.observe_query_stage("hyde", "hyde", time.perf_counter() - started)
        v_hyde(query, passage, verbose)
        return passage

    def encode_query(self, query: str, verbose: int | None = None) -> list[float] | None:
        """Encode the *hypothetical passage* the dense arm searches with.

        Generates the passage and embeds it in e5 **passage mode** — the
        vector its :meth:`retrieve` uses, per the protocol contract. With
        ``HYDE_ENABLED=false`` (logged), or whenever no usable passage was
        generated (the fallback), the base method's own query encoding is
        returned instead — retrieval then behaves exactly like the base.

        Args:
            query: The user's query, unformatted.
            verbose: Console verbosity (0–2); defaults to
                ``settings.DEFAULT_VERBOSE``.

        Returns:
            The passage-mode embedding of the hypothetical passage, or the
            base retriever's query encoding on the degraded paths.

        Raises:
            ValueError: If ``verbose`` is invalid.
            openai.APIError: If embedding still fails after retries.
        """
        settings = get_settings()
        verbose = check_verbose(settings.DEFAULT_VERBOSE if verbose is None else verbose)
        if not settings.HYDE_ENABLED:
            logger.info(
                "HYDE_ENABLED=false — degrading to the %r base method's query encoding",
                settings.HYDE_BASE_METHOD,
            )
            return self._base_retriever().encode_query(query, verbose)
        passage = self._hypothetical(query, verbose)
        if passage is None:
            return self._base_retriever().encode_query(query, verbose)
        embeddings = self._embeddings if self._embeddings is not None else get_model("embedding")
        return embeddings.embed_passages([passage], verbose=0)[0]

    def retrieve(
        self,
        query: str,
        k: int,
        verbose: int | None = None,
        *,
        query_vector: list[float] | None = None,
    ) -> list[RetrievedChunk]:
        """Retrieve top-k via the base method, dense arm steered by HyDE.

        The base retriever receives the user's **original query text** (a
        ``hybrid`` base's BM25 arm keeps exact keyword recall; a composing
        ``reranked`` cross-encodes the real query) together with the
        hypothetical passage's vector — the dense arm is the only arm HyDE
        redirects. Chunks and their traces pass through untouched: the
        base's ranking *is* the ranking.

        Args:
            query: The user's query; searched verbatim by every non-dense
                consumer.
            k: Number of chunks to return.
            verbose: Console verbosity (0–2); defaults to
                ``settings.DEFAULT_VERBOSE``.
            query_vector: Pre-computed :meth:`encode_query` output (the
                flow's embed stage — already the hypothetical passage's
                vector); generated and encoded here when omitted.

        Returns:
            The base retriever's top-k chunks, best first.

        Raises:
            ValueError: If ``verbose`` is invalid.
            openai.APIError: If embedding still fails after retries.
        """
        verbose = check_verbose(get_settings().DEFAULT_VERBOSE if verbose is None else verbose)
        if query_vector is None:
            query_vector = self.encode_query(query, verbose=verbose)
        chunks = self._base_retriever().retrieve(query, k=k, verbose=0, query_vector=query_vector)
        v_retrieve(chunks, verbose)
        return chunks

__init__(*, base=None, llm=None, embeddings=None)

Create the retriever.

Parameters:

Name Type Description Default
base Retriever | None

Retriever the hypothetical-passage vector is handed to; resolved from settings.HYDE_BASE_METHOD per call when omitted.

None
llm LLMClient | None

Chat client generating the passage; resolved via the model registry (settings.HYDE_MODEL_TYPE) per call when omitted.

None
embeddings EmbeddingsClient | None

Embeddings client for the passage-mode encoding; resolved via the model registry (get_model("embedding")) per call when omitted.

None
Source code in varagity/retrieval/hyde.py
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
def __init__(
    self,
    *,
    base: Retriever | None = None,
    llm: LLMClient | None = None,
    embeddings: EmbeddingsClient | None = None,
) -> None:
    """Create the retriever.

    Args:
        base: Retriever the hypothetical-passage vector is handed to;
            resolved from ``settings.HYDE_BASE_METHOD`` per call when
            omitted.
        llm: Chat client generating the passage; resolved via the model
            registry (``settings.HYDE_MODEL_TYPE``) per call when
            omitted.
        embeddings: Embeddings client for the passage-mode encoding;
            resolved via the model registry (``get_model("embedding")``)
            per call when omitted.
    """
    self._base = base
    self._llm = llm
    self._embeddings = embeddings

encode_query(query, verbose=None)

Encode the hypothetical passage the dense arm searches with.

Generates the passage and embeds it in e5 passage mode — the vector its :meth:retrieve uses, per the protocol contract. With HYDE_ENABLED=false (logged), or whenever no usable passage was generated (the fallback), the base method's own query encoding is returned instead — retrieval then behaves exactly like the base.

Parameters:

Name Type Description Default
query str

The user's query, unformatted.

required
verbose int | None

Console verbosity (0–2); defaults to settings.DEFAULT_VERBOSE.

None

Returns:

Type Description
list[float] | None

The passage-mode embedding of the hypothetical passage, or the

list[float] | None

base retriever's query encoding on the degraded paths.

Raises:

Type Description
ValueError

If verbose is invalid.

APIError

If embedding still fails after retries.

Source code in varagity/retrieval/hyde.py
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
def encode_query(self, query: str, verbose: int | None = None) -> list[float] | None:
    """Encode the *hypothetical passage* the dense arm searches with.

    Generates the passage and embeds it in e5 **passage mode** — the
    vector its :meth:`retrieve` uses, per the protocol contract. With
    ``HYDE_ENABLED=false`` (logged), or whenever no usable passage was
    generated (the fallback), the base method's own query encoding is
    returned instead — retrieval then behaves exactly like the base.

    Args:
        query: The user's query, unformatted.
        verbose: Console verbosity (0–2); defaults to
            ``settings.DEFAULT_VERBOSE``.

    Returns:
        The passage-mode embedding of the hypothetical passage, or the
        base retriever's query encoding on the degraded paths.

    Raises:
        ValueError: If ``verbose`` is invalid.
        openai.APIError: If embedding still fails after retries.
    """
    settings = get_settings()
    verbose = check_verbose(settings.DEFAULT_VERBOSE if verbose is None else verbose)
    if not settings.HYDE_ENABLED:
        logger.info(
            "HYDE_ENABLED=false — degrading to the %r base method's query encoding",
            settings.HYDE_BASE_METHOD,
        )
        return self._base_retriever().encode_query(query, verbose)
    passage = self._hypothetical(query, verbose)
    if passage is None:
        return self._base_retriever().encode_query(query, verbose)
    embeddings = self._embeddings if self._embeddings is not None else get_model("embedding")
    return embeddings.embed_passages([passage], verbose=0)[0]

retrieve(query, k, verbose=None, *, query_vector=None)

Retrieve top-k via the base method, dense arm steered by HyDE.

The base retriever receives the user's original query text (a hybrid base's BM25 arm keeps exact keyword recall; a composing reranked cross-encodes the real query) together with the hypothetical passage's vector — the dense arm is the only arm HyDE redirects. Chunks and their traces pass through untouched: the base's ranking is the ranking.

Parameters:

Name Type Description Default
query str

The user's query; searched verbatim by every non-dense consumer.

required
k int

Number of chunks to return.

required
verbose int | None

Console verbosity (0–2); defaults to settings.DEFAULT_VERBOSE.

None
query_vector list[float] | None

Pre-computed :meth:encode_query output (the flow's embed stage — already the hypothetical passage's vector); generated and encoded here when omitted.

None

Returns:

Type Description
list[RetrievedChunk]

The base retriever's top-k chunks, best first.

Raises:

Type Description
ValueError

If verbose is invalid.

APIError

If embedding still fails after retries.

Source code in varagity/retrieval/hyde.py
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
def retrieve(
    self,
    query: str,
    k: int,
    verbose: int | None = None,
    *,
    query_vector: list[float] | None = None,
) -> list[RetrievedChunk]:
    """Retrieve top-k via the base method, dense arm steered by HyDE.

    The base retriever receives the user's **original query text** (a
    ``hybrid`` base's BM25 arm keeps exact keyword recall; a composing
    ``reranked`` cross-encodes the real query) together with the
    hypothetical passage's vector — the dense arm is the only arm HyDE
    redirects. Chunks and their traces pass through untouched: the
    base's ranking *is* the ranking.

    Args:
        query: The user's query; searched verbatim by every non-dense
            consumer.
        k: Number of chunks to return.
        verbose: Console verbosity (0–2); defaults to
            ``settings.DEFAULT_VERBOSE``.
        query_vector: Pre-computed :meth:`encode_query` output (the
            flow's embed stage — already the hypothetical passage's
            vector); generated and encoded here when omitted.

    Returns:
        The base retriever's top-k chunks, best first.

    Raises:
        ValueError: If ``verbose`` is invalid.
        openai.APIError: If embedding still fails after retries.
    """
    verbose = check_verbose(get_settings().DEFAULT_VERBOSE if verbose is None else verbose)
    if query_vector is None:
        query_vector = self.encode_query(query, verbose=verbose)
    chunks = self._base_retriever().retrieve(query, k=k, verbose=0, query_vector=query_vector)
    v_retrieve(chunks, verbose)
    return chunks