brainstorming
brainstorming
You MUST use this before any creative work - creating features, building components, adding functionality, modifying behavior, or planning anything new, in software or out of it (a talk, a business, a renovation).
agentsop-hybrid-retrievalEnhancement-overlay SOP for adding sparse (BM25 / keyword) retrieval alongside dense (embedding) retrieval. Activate when a calling agent is building, reviewing, or debugging a retrieval pipeline whose corpus contains exact-match tokens — identifiers, error codes, SKUs, API/function names, proper nouns, citations, rare jargon — that pure dense embedding sil…
Independent GitHub snapshot · Automated classification · Original documentation governs
name: agentsop-hybrid-retrieval version: 0.1.0 description: | Enhancement-overlay SOP for adding sparse (BM25 / keyword) retrieval alongside dense (embedding) retrieval. Activate when a calling agent is building, reviewing, or debugging a retrieval pipeline whose corpus contains exact-match tokens — identifiers, error codes, SKUs, API/function names, proper nouns, citations, rare jargon — that pure dense embedding silently misses. Encodes the single decision rule (hybrid is traffic-driven, not theoretical: add sparse only when the query share that depends on exact tokens is non-trivial), the wiring of QueryFusionRetriever-style fusion (RRF vs alpha-weighted), and per-query-type alpha tuning. Frame the work as recovering lexical identity that dense pooling destroys, not as "add keyword search for completeness". Cross-links [[llamaindex]]. overlay: true cross_links: [llamaindex, langchain] trigger_keywords:
Third-person operating model for a coder agent that owns retrieval recall on a corpus where both meaning and exact tokens matter. The audience is the LLM agent writing or reviewing retrieval code — not an end user.
One sentence: Dense captures meaning, sparse captures exact tokens; hybrid wins when both matter — but only fuse them when traffic actually carries exact-match queries, and tune the blend per query type or hybrid loses to dense.
Activate this skill when any of the following holds:
ERR_SSL_PROTOCOL), SKUs / part numbers (A1-2293-X), API or
function names (as_query_engine), proper nouns, legal/medical citations
(42 U.S.C. § 1983), version strings, rare jargon, ticket IDs.index.as_retriever(...) / similarity_search(...) with no
sparse leg).QueryFusionRetriever,
EnsembleRetriever, or vector-store-native hybrid (Qdrant/Weaviate/Pinecone).Do not activate when:
Three principles. Violating any of them is why teams "try hybrid and conclude it didn't help".
Dense embedding models destroy lexical identity by pooling token representations: querying a specific error string yields a vector that captures "document about SSL errors" rather than "document containing this exact string". BM25 does the inverse — it scores against an inverted index of exact tokens and is blind to synonyms and paraphrase. (TianPan, Hybrid search in production, 2026; cited in [[llamaindex]] Dilemma 2.)
Operational corollary: the symptom "I pasted the exact code and got nothing" is not a bug in the embedding model — it is the embedding model working as designed. The fix is a second retriever that indexes tokens, not a better embedding.
Whether to add sparse is decided by the query-type distribution of real traffic, not by a belief that "more retrievers = better". The decision threshold is the lexical share: the fraction of queries whose correct answer hinges on an exact token. Below ~5% → dense-only. 5–50% → hybrid. Above ~50% (code, logs, legal) → invert to sparse-first with dense as a rerank signal. (Cited in [[llamaindex]] OP-04 / Dilemma 2.)
alpha is the dense↔sparse blend (alpha=1 → pure dense, alpha=0 → pure sparse).
A semantic query wants high alpha; a lexical-identity query wants low alpha. A single
global alpha picked to help lexical queries hurts the semantic slice — which is
exactly why a flat alpha "loses to pure dense" and teams wrongly conclude hybrid
failed. Tune alpha per query type, or route per type and pick alpha per route.
(LlamaIndex alpha-tuning blog; [[llamaindex]] Dilemma 2 结果.)
The fusion method (RRF vs weighted) and the fusion parameter (alpha) are two separate decisions. RRF is score-scale-robust and parameter-light; weighted fusion is tunable but requires score normalization. Choosing neither deliberately is the third silent failure.
Four stages. Each gates the next. This overlay assumes a working dense baseline + eval loop already exists (see [[llamaindex]] Stages 1–2); do not start here.
foo_bar") / mixed.< 5% → dense-only. Stop; hybrid is over-engineering. Record the decision.5–50% → add hybrid (Stages 1–3).> 50% (code search, log search, legal citation lookup) → invert: BM25-first,
dense as a fallback / rerank signal.Artifact: a 3-line decision note co-located with the retriever recording the lexical share and the chosen shape.
from llama_index.core.retrievers import QueryFusionRetriever
from llama_index.retrievers.bm25 import BM25Retriever
dense = index.as_retriever(similarity_top_k=10) # embedding leg
sparse = BM25Retriever.from_defaults(nodes=nodes, similarity_top_k=10)
Hard rules:
Stage 1-alt — vector-store-native hybrid. If the vector store does sparse
internally (Qdrant sparse_vectors, Weaviate hybrid(alpha=...), Pinecone sparse-dense,
pgvector + ts_rank), prefer it: one round-trip, one consistency domain, no separate
BM25 index to keep in sync. Use the framework fusion path only when the store has no
native hybrid.
Pick the fusion method deliberately (OP-02):
# Reciprocal Rank Fusion — score-scale-robust, parameter-light. Good default.
fused = QueryFusionRetriever(
[dense, sparse],
mode="reciprocal_rerank", # RRF: combine by rank, ignore raw score scales
similarity_top_k=8,
num_queries=1, # set >1 to also fan out query rewrites
use_async=True,
)
# Relative-score / weighted fusion — tunable blend; requires score normalization.
fused = QueryFusionRetriever(
[dense, sparse],
mode="relative_score",
retriever_weights=[0.6, 0.4], # ~ alpha=0.6 toward dense
similarity_top_k=8,
)
Default to RRF unless you have a labeled set to tune weights on — RRF sidesteps the dense-cosine vs BM25-score scale mismatch that breaks naive weighted sums.
retriever_weights) at {0.0, 0.25, 0.5, 0.75, 1.0} on each
type separately, scoring recall / MRR / hit-rate per type.The most common false negative: tuning one global alpha, watching semantic recall drop, and reverting to dense. The correct read is "alpha was global; tune per type".
Format: Trigger / Action / Output / Evidence.
mode="reciprocal_rerank") — rank-based, robust to the
dense-cosine vs BM25-score scale gap, no weight to tune. Use weighted /
relative-score only when you have a labeled set to fit weights and have normalized
scores. Never naive-sum un-normalized scores.QueryFusionRetriever modes;
LangChain EnsembleRetriever (RRF default).BM25Retriever.from_defaults(nodes=...) over the same node
set as the dense index; persist/rebuild it as a deployment artifact alongside the
vector index. Set per-leg similarity_top_k wide (10–20).BM25Retriever docs; [[llamaindex]] OP-04 AddHybridBM25.Prefetch +
fusion, Weaviate hybrid(query, alpha=...), Pinecone sparse-dense vectors, pgvector
full-text + vector) instead of a separate BM25 index. One round-trip, one
consistency domain.{0, 0.25, 0.5, 0.75, 1.0} on labeled
subsets per query type, not globally. Lexical → low alpha, semantic → high.困境: Dense is the modern default; BM25 looks like "the old keyword thing". Adding hybrid doubles the index footprint, adds a BM25 index to keep in sync, and introduces alpha tuning. Is the complexity justified, or is a better embedding model enough?
约束:
决策步骤:
<5% → dense-only; the complexity is not justified.5–50% → add hybrid; tune alpha per type.>50% (legal, code, logs) → invert: BM25-first, dense as rerank signal.{0, 0.25, 0.5, 0.75, 1.0} on labeled per-type subsets.结果: Hybrid lifts the lexical slice with no degradation on the semantic slice — if alpha is tuned per type. A single global alpha often loses to pure dense on semantic queries, which is why teams sometimes wrongly conclude "hybrid didn't help". The decision is traffic-driven, not theoretical. (Verbatim from [[llamaindex]] Dilemma 2.)
可提取的操作: OP-01, OP-05, OP-07, OP-08.
困境: After wiring hybrid, a single alpha must serve both a user pasting
ERR_TLS_CERT_INVALID (wants exact-token match, low alpha) and a user asking "why is
my connection failing?" (wants meaning, high alpha). Picking alpha=0.5 helps neither
fully; picking alpha to win lexical regresses semantic, and vice versa.
约束:
alpha=1 = pure dense, alpha=0 = pure sparse; the optimum differs by query
type, not by corpus.决策步骤:
>50% lexical corpora, skip the balancing act: invert to BM25-first (OP-07).结果: Per-type alpha (or per-type routing) makes both slices peak simultaneously. The error to avoid is treating alpha as a single global hyperparameter — that is the documented cause of "hybrid underperforms dense". (LlamaIndex alpha-tuning blog; [[llamaindex]] Dilemma 2.)
可提取的操作: OP-05, OP-06, OP-08.
| # | Anti-pattern | Why it's wrong | Correct move |
|---|---|---|---|
| A1 | Adding hybrid by default on a corpus that is purely semantic | Doubles index footprint and ops surface for no recall gain; <5% lexical traffic | OP-01: gate on lexical share; dense-only is the right answer for semantic corpora |
| A2 | Picking a single global alpha for mixed traffic | Helps lexical, regresses semantic (or vice versa) → "hybrid hurt us" | OP-05/OP-06: tune alpha per query type, or route per type |
| A3 | Ignoring the fusion method — naive-summing dense cosine + BM25 score | Score scales are incomparable; the larger-scale leg dominates arbitrarily | OP-02: use RRF (rank-based) or normalize before weighting |
| A4 | BM25 leg built over a different/stale node set than the dense index | Sparse can only return tokens it indexed; silent recall loss | OP-03: build both legs over the same nodes; version them together |
| A5 | "We added hybrid, recall on semantic dropped, hybrid is bad" | The conclusion is wrong; the alpha was global | OP-05: re-evaluate per type before reverting |
| A6 | Final cut taken before fusion (narrow per-leg top_k=3) | Fusion has too few candidates to combine; near-misses never surface | Retrieve wide per leg (10–20), narrow after fusion / rerank |
| A7 | Separate BM25 index when the vector store has native hybrid | Extra index to keep in sync; two consistency domains | OP-04: prefer native hybrid (Qdrant/Weaviate/Pinecone/pgvector) |
| A8 | Hybrid as a substitute for a reranker (or vice versa) | Different jobs: hybrid widens candidate recall, rerank reorders | Hybrid then rerank ([[llamaindex]] OP-03); they compose, don't replace |
| A9 | Shipping hybrid with no per-slice eval | A lexical lift can hide a semantic regression | OP-08: gate on both slices vs the dense baseline |
QueryFusionRetriever([...]) with no mode= set and no comment on fusion choice.alpha / retriever_weights with no per-type eval behind it.BM25Retriever.from_defaults(nodes=other_nodes) where other_nodes differs from
the dense index's node set.similarity_top_k equal to the final desired count (no headroom for fusion).dense_scores + bm25_scores summed without normalization.How "dense + sparse, fused" looks across the common stacks. Cross-links [[llamaindex]].
QueryFusionRetrieverfrom llama_index.core.retrievers import QueryFusionRetriever
from llama_index.retrievers.bm25 import BM25Retriever
dense = index.as_retriever(similarity_top_k=10)
sparse = BM25Retriever.from_defaults(nodes=nodes, similarity_top_k=10)
fused = QueryFusionRetriever(
[dense, sparse],
mode="reciprocal_rerank", # or "relative_score" / "dist_based_score"
retriever_weights=[0.6, 0.4], # used by weighted modes; ~alpha toward dense
similarity_top_k=8,
num_queries=1, # >1 also fans out query rewrites
)
Fusion modes: reciprocal_rerank (RRF, default-recommended), relative_score,
dist_based_score (the latter two are weighted). ([[llamaindex]] OP-04.)
EnsembleRetrieverfrom langchain.retrievers import EnsembleRetriever
from langchain_community.retrievers import BM25Retriever
bm25 = BM25Retriever.from_texts(texts); bm25.k = 10
dense = vectorstore.as_retriever(search_kwargs={"k": 10})
ensemble = EnsembleRetriever(
retrievers=[bm25, dense],
weights=[0.4, 0.6], # weighted RRF blend
)
EnsembleRetriever fuses with Reciprocal Rank Fusion under the hood; weights
biases the RRF contribution per retriever (the LangChain analogue of alpha).
def rrf(result_lists, k=60, top_k=8):
scores = {}
for results in result_lists: # each = ranked list of doc ids
for rank, doc_id in enumerate(results):
scores[doc_id] = scores.get(doc_id, 0) + 1.0 / (k + rank + 1)
return sorted(scores, key=scores.get, reverse=True)[:top_k]
fused = rrf([dense_ids, bm25_ids]) # rank-based, no score normalization
RRF combines by rank, so dense-cosine and BM25 raw scores never need to share a
scale — the reason it is the safe default (OP-02). k≈60 is the canonical constant
(Cormack et al., 2009).
| Store | Mechanism | Alpha knob |
|---|---|---|
| Qdrant | sparse + dense vectors, Prefetch + FusionQuery(fusion=RRF) | server-side fusion (RRF/DBSF) |
| Weaviate | collection.query.hybrid(query=..., alpha=0.5) | alpha (0=BM25, 1=dense) — the canonical alpha |
| Pinecone | sparse-dense vectors in one index | alpha weighting on the sparse/dense split |
| pgvector | tsvector full-text + vector distance, combined in SQL | hand-weighted in the ORDER BY expression |
| Milvus | hybrid search with WeightedRanker / RRFRanker | ranker choice + weights |
Prefer native hybrid when available (OP-04): one round-trip, one consistency domain,
no separate BM25 index to keep in sync. Weaviate's alpha is the literal parameter the
[[llamaindex]] alpha-tuning blog generalizes.
QueryFusionRetriever / BM25Retriever module guides:
https://developers.llamaindex.ai/python/framework/EnsembleRetriever (RRF) docs:
https://python.langchain.com/docs/how_to/ensemble_retriever/alpha):
https://docs.weaviate.io/weaviate/search/hybridreferences/R1-source-evidence.md — extraction provenance: which [[llamaindex]]
SKILL / R3 passages each section, OP, and dilemma derive from.intermediate/operation_candidates.json — machine-readable operation list.[[llamaindex]] — parent RAG SOP; this overlay extracts and standalone-izes its
Stage-3 step-4 hybrid recipe + Dilemma 2 (alpha tuning per query type).[[langchain]] — EnsembleRetriever equivalent.[[agentsop-query-routing]] — mechanism for per-query-type alpha (OP-06).