Skip to content

Hybrid search and reranking behind switches (spec 019) - #19

Merged
YBazanPro merged 4 commits into
mainfrom
feat/spec-019-hybrid-search-reranking
Oct 20, 2026
Merged

YBazanPro merged 4 commits into
mainfrom
feat/spec-019-hybrid-search-reranking

Conversation

@YBazanPro

@YBazanPro YBazanPro commented Oct 20, 2026 •

Copy link
Copy Markdown
Owner

Summary

Implements spec 019. Both stages ship switched off: the evaluation gate (AC-20 to AC-22) is deferred until after spec 020.

Based on #18 (nightly workflow fix and spec 018 calibration). Its two commits appear here until #18 is merged.

Hybrid search (Documents)

  • Migration HybridSearch:
    • the unaccent extension and the documents.french_unaccent text search configuration;
    • passages.search_config (english, documents.french_unaccent or simple) and a generated, GIN-indexed search_vector.
  • Database triggers set each passage's configuration from its document language on insert, and update it in the same transaction when the language changes, so imports, edits and language detection all stay consistent.
  • Keyword pool: websearch_to_tsquery in the question's language (SearchFilters.QueryLanguage, detected question language, never the requested answer language), ranked with ts_rank_cd. It uses the same filters and ReadableBy as the vector pool and is fetched while the query is embedded.
  • Fusion: reciprocal rank fusion (Documents:Retrieval:Hybrid:RrfK, default 60). Without reranking, cosine still gates, so a keyword-only hit needs cosine ≥ MinScore.

Reranking

  • Contract: IPassageReranker in SharedKernel/Contracts/Retrieval. The API implements it with a typed HttpClient against Text Embeddings Inference serving BAAI/bge-reranker-v2-m3 (ADR 0010, image cpu-1.9.4).
  • Flow: the top Candidates (30) go in one call with a 2 s timeout. They are then reordered and gated by the rerank score (Reranking:MinScore / ConfidentScore), and near-duplicate reduction and topK apply after.
  • Fallback: a timeout, error, unconfigured reranker or wrong score count gives rerank: skipped and cosine gating, plus a documents.rerank.skipped counter. A question never fails because of the reranker.
  • AppHost: the reranker container exists only with the new https-rerank launch profile, or --Parameters:reranking=true.

Diagnostics

  • Per-passage stages: vector and keyword ranks, cosine, ts_rank_cd, fused and rerank scores, admittedBy.
  • Where they show: the search endpoint, the request trace (migration RetrievalStages) and the admin request inspector. The activity panel and the SSE retrieval.completed event show mode, rerank and admittedBy only.
  • Search endpoint overrides: mode, rerank and queryLanguage.
  • Admin configuration: the hybrid and rerank settings are on the Documents configuration, which owns them, rather than the Chat one the spec names. Chat cannot read Documents options across the module boundary.
  • Metrics: documents.rerank.duration, documents.rerank.skipped, documents.search.keyword_hits, and the Chat metrics tagged retrieval.mode.

Harness

  • run --variants measures retrieval for vector, hybrid and hybrid + rerank on the same queries.
  • Reports record the stage settings, and a run mixing retrieval modes is marked invalid.
  • sweep calibrates rerank thresholds on their own grid.
  • The nightly workflow now runs with --variants; without TEI on the runner, the rerank variant is skipped.

Docs

ADR 0010, CLAUDE.md (module table, stack, commands) and the rag-pipeline skill.

Measured so far (not the gate)

  • TEI cpu-1.9.4 on the reference machine: 18–24 s to rerank 30 corpus-length passages, against the 800 ms budget. Relevant passages scored 0.99, unrelated ones 0.0000. With the 2 s default timeout, reranking is skipped on a CPU.
  • Postgres image: unaccent ships with the pinned pgvector/pgvector:0.8.7-pg18-trixie image, as the spec required.

Test plan

  • dotnet build with 0 warnings, and dotnet format --verify-no-changes clean
  • Full suite: 1,288 tests, all passing after updating ChatMetricsTests for the new low-cardinality retrieval.mode label
  • New tests:
    • HybridRankingTests (AC-9, ordering, stages, rerank order);
    • HybridSearchTests (AC-1 to AC-8, AC-15, AC-16);
    • RerankingTests (AC-10, AC-12 to AC-14);
    • RetrievalStagesTests (AC-11, AC-12 trace, AC-17, keyword configuration of the question's language);
    • TeiPassageRerankerTests.
  • AC-19: the AppHost started with https-rerank (API and Keycloak ready), but whether TEI came up and the API reached it was not checked before the run was stopped
  • AC-20 to AC-22: evaluation gate and the spec's Calibration section, deferred until after spec 020; until then Hybrid:Enabled and Reranking:Enabled stay false

🤖 Generated with Claude Code

YBazanPro and others added 4 commits October 20, 2026 11:04
…(spec 021)

dotnet run on an AppHost with AspireUseCliBundle starts bin/Debug whatever
configuration is passed, so the Release-only build gave the first nightly
run nothing to start.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s (specs 018, 021)

From the first passing nightly run (39445089883, qwen3:1.7b, 87 cases):
- the threshold sweep moves Documents:Retrieval:ConfidentScore from 0.60
  to 0.55 (behaviour 0.71 -> 0.83, refusals and recall unchanged);
  MinScore 0.55 and DuplicateOverlap 0.8 stay. The low-confidence tier
  is empty by default as a result, which spec 018's Calibration records.
- qwen3:1.7b follows the injected passage of i-en-01; ministral-3:8b and
  llama3.2:3b resist the same prompt when sent by hand.
- spec 021's Verification records both dispatches and the Debug-build fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Hybrid search (Documents): passages get search_config (english,
documents.french_unaccent or simple, from the document language, kept
in step by database triggers on insert and on a language change) and a
generated, GIN-indexed search_vector. In hybrid mode a keyword pool
(websearch_to_tsquery in the question's language, ts_rank_cd, same
filters and ReadableBy as the vector pool, fetched while the query is
embedded) is fused with the vector pool by reciprocal rank fusion.

Reranking: IPassageReranker in the shared kernel, implemented by the
API with Text Embeddings Inference serving bge-reranker-v2-m3
(ADR 0010). The best candidates go in one call with their own timeout;
reranked results are ordered and gated by the rerank score, and any
failure falls back to cosine gating as "rerank: skipped". The AppHost
starts TEI only with the https-rerank launch profile.

Diagnostics: every passage carries PassageStages (vector and keyword
ranks, cosine, ts_rank_cd, fused and rerank scores, admittedBy). The
search endpoint takes mode, rerank and queryLanguage overrides; the
SSE stream, request trace (migration RetrievalStages), admin inspector,
activity, configuration and Chat metrics record mode and rerank status.

Harness: run --variants measures retrieval for vector, hybrid and
hybrid + rerank on the same queries; reports record the stage settings,
a run mixing retrieval modes is invalid, and sweep calibrates rerank
thresholds on their own grid. Both stages stay off until the
calibration in spec 019 decides.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Vector against hybrid on the same queries every night; without a reranker
on the runner the rerank variant is skipped at no cost.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@YBazanPro
YBazanPro merged commit eb5bfed into main Oct 20, 2026
1 check failed
@YBazanPro
YBazanPro deleted the feat/spec-019-hybrid-search-reranking branch October 20, 2026 16:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant