initial commit
This commit is contained in:
123
docs/adr/0003-agent-hybrid-retrieval.md
Normal file
123
docs/adr/0003-agent-hybrid-retrieval.md
Normal file
@@ -0,0 +1,123 @@
|
||||
# 0003. Agent hybrid retrieval (dense + dense + sparse + late-interaction rerank)
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
|
||||
The AI agent needs to retrieve the most relevant chunks for a query against
|
||||
the `chunks` collection defined in ADR-0001, which already provisions four
|
||||
named vectors — `dense_nomic`, `dense_openai`, `sparse`, and
|
||||
`late_interaction` — for exactly this purpose. The goal is higher relevance
|
||||
than single-dense-vector search by combining two independent semantic
|
||||
signals with lexical (sparse) retrieval, then refining the result with a
|
||||
late-interaction (ColBERT-style) rerank.
|
||||
|
||||
Qdrant's Query API supports doing this as a single server-side request via
|
||||
`prefetch` stages, fusion, and a final rerank stage — avoiding a client-side
|
||||
fan-out of multiple separate queries. It also explicitly cautions that
|
||||
hybrid search/reranking can *mask* underlying embedding-model quality issues
|
||||
rather than fix them, so it shouldn't be adopted without a baseline
|
||||
comparison.
|
||||
|
||||
Both dense models are now fixed: `dense_nomic` uses `nomic-embed-text-v2-moe`
|
||||
([0004](0004-docx-csv-chunking-strategy.md)), which requires a
|
||||
`search_query: ` task prefix on the embedded query text (mirroring
|
||||
`search_document: ` on the ingestion side); `dense_openai` uses the OpenAI
|
||||
large embedding model, no prefix convention needed. `sparse` uses
|
||||
`bm25-fa-norm-stop` (ADR-0001). The late-interaction/reranker model is now
|
||||
decided — `jina-colbert-v2` — see [0005](0005-reranking-model-and-sparse-analyzer-selection.md)
|
||||
for the model comparison, the GPU dependency it introduces, and its
|
||||
unresolved commercial-license status.
|
||||
|
||||
## Decision
|
||||
|
||||
### Query shape
|
||||
|
||||
A single Qdrant Query API request per agent query, structured as:
|
||||
|
||||
1. **Prefetch 1 — dense ANN**: search the `dense_nomic` vector, top ~100–200
|
||||
candidates.
|
||||
2. **Prefetch 2 — dense ANN**: search the `dense_openai` vector, top ~100–200
|
||||
candidates.
|
||||
3. **Prefetch 3 — sparse ANN**: search the `sparse` vector, top ~100–200
|
||||
candidates.
|
||||
4. **Fusion**: combine all three prefetch results via **RRF** (Reciprocal
|
||||
Rank Fusion) as the default — per Qdrant's own guidance, RRF "ignores
|
||||
score magnitude, a decent default to start with" and handles the
|
||||
incomparable score scales across two dense models and a sparse model.
|
||||
5. **Rerank**: apply late-interaction (`late_interaction` multivector,
|
||||
max-sim) reranking over the fused top-N (not the full collection) to
|
||||
produce the final top-k returned to the agent, using `jina-colbert-v2`
|
||||
([0005](0005-reranking-model-and-sparse-analyzer-selection.md)).
|
||||
|
||||
Every prefetch and the final query carries the same `tenant_id`/`domain`
|
||||
filter from ADR-0001's payload schema — identical isolation guarantee to
|
||||
ADR-0002.
|
||||
|
||||
### Context-window expansion
|
||||
|
||||
For each chunk returned by the fused/reranked query, the agent may expand
|
||||
its context by pulling the immediately preceding and following chunks before
|
||||
building the prompt. This uses the `previous_chunk_id`/`next_chunk_id`
|
||||
pointer fields from ADR-0001 — a single batch "retrieve points by ID" call
|
||||
per result chunk, not an additional filtered search. Since this runs on every
|
||||
returned chunk, it relies on those pointers being kept correct by ADR-0002's
|
||||
CRUD mutation logic (reorder/insert/delete).
|
||||
|
||||
### Bounding rerank cost
|
||||
|
||||
Late-interaction rerank is the most compute-expensive stage. It is applied
|
||||
only to the fusion's top-N output (e.g. top 50), never to the full
|
||||
candidate set or the full collection, to keep latency bounded.
|
||||
|
||||
### Evaluate before enabling in production
|
||||
|
||||
Before turning this on for real traffic, run an evaluation comparing hybrid
|
||||
+ rerank against a dense-only baseline on representative queries. Qdrant's
|
||||
own guidance warns against adopting hybrid search prematurely, since it can
|
||||
paper over embedding-model quality problems instead of addressing them. If
|
||||
the baseline is already good, the added complexity/cost of this pipeline
|
||||
should be justified with evidence, not assumed.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
- Single round-trip server-side fusion + rerank — lower latency than
|
||||
orchestrating multiple queries and fusing client-side in FastAPI.
|
||||
- Directly reuses the vector schema and tenant isolation already established
|
||||
in ADR-0001/0002 — no divergent data model for the agent path.
|
||||
- Bounding rerank to the fused top-N keeps the expensive late-interaction
|
||||
step's cost predictable regardless of collection size.
|
||||
|
||||
### Negative
|
||||
- Late-interaction multivectors are storage/compute heavy; with
|
||||
`jina-colbert-v2` chosen ([0005](0005-reranking-model-and-sparse-analyzer-selection.md))
|
||||
this stage now also requires GPU capacity (`flash_attn`/CUDA), not just
|
||||
the Docker deployment ADR-0001 assumed.
|
||||
- RRF is score-magnitude-agnostic by design — if a use case later needs
|
||||
absolute score information (e.g. a relevance threshold), a different
|
||||
fusion method (DBSF or a custom `FormulaQuery`) would need to be
|
||||
evaluated separately.
|
||||
- Adds a mandatory evaluation step before production rollout, rather than
|
||||
shipping hybrid search immediately.
|
||||
- Three prefetch stages instead of two (two dense + one sparse) means every
|
||||
agent query embeds the question with both `dense_nomic` and `dense_openai`
|
||||
— an extra embedding call and an external OpenAI API dependency on the
|
||||
query's critical path, not just at ingestion time.
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
- **Client-side fan-out** (separate dense and sparse queries against Qdrant,
|
||||
manual RRF fusion in FastAPI): rejected — more round trips and latency
|
||||
than Qdrant's native server-side Query API fusion, for no functional
|
||||
benefit.
|
||||
- **Cross-encoder reranking via FastEmbed instead of late-interaction**: not
|
||||
adopted as the initial decision, since ADR-0001 already reserves a
|
||||
multivector field for late-interaction rerank; noted as a possible
|
||||
*additional* future stage rather than a replacement.
|
||||
- **DBSF or FormulaQuery as the default fusion method**: rejected as the
|
||||
starting point — RRF is simpler and Qdrant's recommended default; DBSF/
|
||||
FormulaQuery remain available as a later optimization if empirical
|
||||
evaluation shows RRF underperforming.
|
||||
Reference in New Issue
Block a user