Spaces:
Sleeping
Sleeping
File size: 5,148 Bytes
b2931f4 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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 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 | """Final-stage reranking via Cohere Rerank v3 (cross-encoder).
The reranker fixes the failure modes hybrid retrieval can't:
- Lexical accidents (BM25 dragging an entity-mismatched chunk to the top
because keyword co-occurrence happens to be high)
- Query-document interaction blindness (bi-encoders can't see "Apple" in
the query and "Tesla" in the candidate at the same time)
Flow:
hybrid_search(top_k=N_CANDIDATES) βββΆ list of RetrievedChunk
β
βΌ
Cohere Rerank v3
(rerank-english-v3.0)
β
βΌ
Reordered, top_k chosen
score = relevance_score (0β1)
"""
from __future__ import annotations
import time
from cohere.errors import TooManyRequestsError
from finrag.retrieval.hybrid import hybrid_search
from finrag.retrieval.vector import RetrievedChunk, get_cohere_client
RERANK_MODEL = "rerank-english-v3.0"
# Default candidate-pool size fed to the reranker. 50 is the sweet spot:
# wide enough that the reranker can rescue chunks ranked 20+ by hybrid,
# narrow enough to stay under Rerank v3's pricing tier (1 search unit per
# 100 docs) and latency budget (~200β400ms at 50 docs).
DEFAULT_CANDIDATES = 50
# Trial-key safety net β same pattern as ingestion/embed.py
RERANK_RETRY_INITIAL_BACKOFF_SECONDS = 5.0
RERANK_MAX_RETRIES = 4
def _rerank_with_retry(
query: str, documents: list[str], top_n: int
) -> list[tuple[int, float]]:
"""Call Cohere Rerank v3 with exponential-backoff retry on 429s.
Returns list of (original_index, relevance_score) in reranker order.
"""
co = get_cohere_client()
backoff = RERANK_RETRY_INITIAL_BACKOFF_SECONDS
for attempt in range(1, RERANK_MAX_RETRIES + 1):
try:
response = co.rerank(
model=RERANK_MODEL,
query=query,
documents=documents,
top_n=top_n,
)
return [(r.index, r.relevance_score) for r in response.results]
except TooManyRequestsError:
if attempt == RERANK_MAX_RETRIES:
raise
print(
f" β rerank rate-limited; sleeping {backoff:.0f}s "
f"(retry {attempt}/{RERANK_MAX_RETRIES - 1})"
)
time.sleep(backoff)
backoff *= 2
raise RuntimeError("rerank retry loop exited without resolving")
def rerank_search(
question: str,
top_k: int = 5,
ticker: str | None = None,
fiscal_year: int | None = None,
chunk_type: str | None = None,
candidates: int = DEFAULT_CANDIDATES,
) -> list[RetrievedChunk]:
"""Hybrid retrieval + cross-encoder rerank. The user-facing default.
The `score` field on returned chunks is the rerank relevance_score
(0β1, semantically meaningful) β *not* the RRF score from the hybrid
stage. You can compare these across queries: 0.85 means "strongly
relevant" no matter what was asked.
"""
# 1. Get a wide candidate pool from hybrid fusion. Pass the same filters
# through β scoping should happen before rerank, not after.
pool = hybrid_search(
question=question,
top_k=candidates,
ticker=ticker,
fiscal_year=fiscal_year,
chunk_type=chunk_type,
)
if not pool:
return []
# If we have fewer candidates than top_k, no reranking is meaningful β
# just return what we have.
if len(pool) <= top_k:
return pool
# 2. Cross-encoder rerank
documents = [c.text for c in pool]
ranked = _rerank_with_retry(
query=question,
documents=documents,
top_n=top_k,
)
# 3. Reassemble in rerank order with the rerank score replacing RRF
return [
pool[orig_idx].model_copy(update={"score": rel_score})
for orig_idx, rel_score in ranked
]
# ββ CLI βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def main() -> None:
"""Side-by-side: hybrid (RRF only) vs hybrid+rerank, same queries."""
queries = [
"How did Apple's services revenue change in 2023?",
"Tesla R&D expense fiscal 2023",
"SG&A expense",
"JPMorgan net interest income",
"supply chain risk",
]
for q in queries:
print(f"\n=== {q!r} ===")
print(" Hybrid (RRF only) top-3:")
for c in hybrid_search(q, top_k=3):
print(
f" {c.ticker} FY{c.fiscal_year} rrf={c.score:.4f} | {c.text[:60]}"
)
print(" Hybrid + Rerank v3 top-3:")
for c in rerank_search(q, top_k=3):
print(
f" {c.ticker} FY{c.fiscal_year} rel={c.score:.3f} | {c.text[:60]}"
)
if __name__ == "__main__":
main()
|