Draft: feat(chatqna): multi-turn vector-space blending (issue #833)
Context
Resolves #833.
ChatQnA retrieval is stateless: only the last user message is embedded and searched, so pronoun-heavy follow-ups ("can you elaborate on this?") retrieve garbage. This MR adds feature-flagged multi-turn vector-space blending — blends the query embedding with the previous N turns (V = α·EQ + (1-α)·EH) so follow-ups retrieve the prior turn's subject.
MULTI_TURN_BLEND_ENABLED defaults to false (no-op until explicitly enabled).
Design (E2 — batch via existing embedding node)
Zero duplicated embedding logic, single batched TEI call:
-
handle_requestextracts history_text →initial_inputs={text: query, _blend_history_text, _blend_alpha} -
align_inputsEMBEDDING:inputs['input'] = [query, history](batch) when history present, single-string otherwise (baseline unchanged) -
align_outputsEMBEDDING: blends batch responsedata[0]+data[1]; setstextto the isolated query string so the retriever'stext:strfield and BM25 leg (when hybrid is explicitly enabled) are not broken - Retriever consumes the blended
embeddingpreferentially (skips its internal re-embed)
Rejected alternatives documented in commit + code comments: side-channel httpx embed (duplicates logic), Redis cache (hit rate ≈0, no session_id in payload, silent failure modes), incremental combine (non-validated approximation), LLM query-rewriting (benchmark favored blending).
Validation
-
TEI batch validated live on 10.0.0.102 (El Salvador stack):
POST embedding:6000/v1/embeddings {"input":[t1,t2]}→ 2 distinct 768-dim vectors (cos=0.7476). OPEA wrapper passes batch through to TEI. -
Benchmark (Roman, external): blending Recall@1=91.7% vs baseline 66.7%; beat LLM query-rewriting head-to-head. Reproduction into
tests/rag-benchmarks/is a follow-up (multi-turn gold dataset + Recall@k metric don't exist yet). -
Tests: 162 pass (29 new). Code review caught + fixed a Critical (align_outputs sent batch list as
text→ would 422 retriever / BM25 garbage). -
CI: pipeline #5187
✅ success (11 jobs). Rebased on main (2026-07-24).
Limitations (documented, accepted)
Under the default production configuration (RETRIEVER_HYBRID_RETRIEVAL_ENABLED=false), the retriever runs dense-only — so the blended vector controls all retrieval. This has no practical impact unless you explicitly enable hybrid retrieval (opt-in), in which case only the dense leg is blended and the BM25 lexical leg still uses the isolated query text.
The reranker scores against the isolated query text (separate concern).
Files (10)
genieai_chatqna.py (config + 2 pure helpers + handle_request + align_inputs/outputs), test_chatqna.py (29 tests), env, docker-compose.yaml, chatqna/README.md, new site/docs/rag/multi-turn-retrieval.md, architecture.md, install-guide.md, ansible/env.j2, ansible/README.md.
Also documents the previously-undocumented RETRIEVER_HYBRID_RETRIEVAL_ENABLED flag in retrieval.md and install-guide.md (was missing from user-facing docs — discovered during multi-turn blend review).
Follow-ups (separate issues)
- Build multi-turn gold dataset + Recall@k metric in
tests/rag-benchmarks/to reproduce the benchmark in-repo. - Confirm with Roman whether the external benchmark ran with hybrid ON or OFF.
- Incremental embedding + optional cache IF a measured latency problem justifies it (currently no evidence).