PRD-032: Hybrid Corpus Search¶
- Status: Draft
- Author: Marko
- Created: 2026-05-24
- Target: v2.7
- Related PRDs:
docs/prd/PRD-031-search.md— product surface that consumes this backenddocs/prd/PRD-033-search-powered-surfaces.md— cross-surface propagationdocs/prd/PRD-021-semantic-corpus-search.md— predecessor (FAISS semantic search)docs/prd/PRD-028-position-tracker.md— position evolution data (temporal queries)- Related RFCs:
docs/rfc/RFC-090-hybrid-retrieval.md— two-tier index + BM25 + vector + RRF (implements core)docs/rfc/RFC-091-kg-proximity-signal.md— KG proximity signaldocs/rfc/RFC-092-ml-query-router.md— ML query routerdocs/rfc/RFC-093-litm-context-packs.md— LITM-aware context packsdocs/rfc/RFC-072-canonical-identity-layer-cross-layer-bridge.md— canonical identitydocs/rfc/RFC-088-enrichment-layer-architecture.md— enrichment layer- Related issues: #849 (Search retrieval prerequisites), #466 (GI/KG depth roadmap — CLOSED, superseded), #484 (Semantic Corpus Search — CLOSED, predecessor), #485 (Topic nodes + ABOUT edges — CLOSED)
Stabilization note (2026-05-30): Rebased to this repo. The original draft pointed at an MCP-integration RFC and "Position Tracker"/"Guest Intelligence Brief" PRDs that do not exist under those numbers here. Corrected: enrichment → RFC-088; Position Tracker → PRD-028; the MCP tool layer and a guest-briefing PRD are [TBD — not yet specified]. Module paths are corrected to
src/podcast_scraper/….
Summary¶
Podcast Scraper's search today is single-signal FAISS vector retrieval over GIL insight nodes only
(src/podcast_scraper/search/, shipped via #484). This misses raw transcript evidence, fails on
named-entity queries, ignores KG relational structure, and gives agents unstructured result dumps.
This PRD covers the evolution to hybrid corpus search: a two-tier index (transcript segments +
GIL insights) with two retrieval signals fused via RRF (BM25 + dense vector), intent-aware
query routing, mixed-tier result ranking, and LITM-aware agent context packs. It is implemented by
RFC-090 (core); RFC-091's KG-proximity signal was evaluated and rejected (Decision Record) —
relational structure comes from typed edges instead (#874) — with RFC-092/093 additive.
Background & Context¶
- What problem this solves. Single-signal vector search over insights answers similarity but not exact-match, relational, or raw-evidence questions. The corpus has structure (typed entities, topics, shows, time) that retrieval ignores.
- Why now. Semantic corpus search (#484, PRD-021/RFC-061) shipped and exposed its own ceiling: named-entity queries degrade, transcript-level evidence is unreachable, and the KG contributes nothing to ranking. The GI/KG depth roadmap (#466) names exactly these gaps.
- How it relates to existing features. Builds on the existing FAISS pipeline, the GIL insight
contracts (
src/podcast_scraper/gi/contracts.py), the KG artifacts (*.kg.json), and the canonical identity layer (RFC-072). It is the backend for the Search product (PRD-031).
Goals¶
- Two-tier retrieval. Transcript segments and GIL insights are both indexed and retrievable; results are mixed by score, not grouped by tier.
- Named-entity recall. Searches for person names, show names, and specific terminology return directly relevant results, not just semantic neighbours.
- Graph-aware structure. KG-proximity was evaluated as a third retrieval signal and rejected
(RFC-091 Decision Record); relational structure comes from typed edges (
Person→Insight,Insight→Entity, #874) that surfaces traverse, not from proximity ranking. - Intent-aware routing. Query intent is classified and retrieval strategy adjusted: person lookup, synthesis, raw evidence, and temporal queries each get the right signal/tier mix.
- Clean backend abstraction. A
SearchBackendprotocol makes the backend swappable; LanceDB embedded is the initial implementation. Cloud backends require no retrieval-logic changes. - Agent-ready context packs. MCP search tools return LITM-aware, compressed, grounded context, not raw lists (RFC-093).
Non-Goals¶
- Real-time / sub-second indexing of new episodes.
- Cross-language search (corpus is English-only).
- User-facing faceted search UI (filter chips are existing UX work; this PRD provides the backend filter mechanism).
- Search personalisation (no auth layer yet).
- Cross-encoder reranking (deferred, post-RRF baseline).
- Cloud search backends (turbopuffer, Qdrant Cloud) — supported by the abstraction, not implemented.
Personas¶
- Beta researcher: Wants exact quotes, named-entity hits, and cross-show breadth — not just similar snippets.
- Autoresearch / agent consumer: Issues typed queries and needs shaped, tier-weighted results
without specifying signal weights manually. (Autoresearch is today an eval/prompt-tuning harness
—
autoresearch/— and is not yet a live search consumer; this is the integration target.) - Operator (you): Tunes retrieval quality via the eval loop and owns the backend choice.
User Stories¶
- As a beta researcher, I can search a specific phrase or quote and find the transcript segment containing it, so that raw evidence is reachable even when no insight was extracted from it.
- As a beta researcher, I can search a person's name and get results containing their name directly, so that named-entity queries don't degrade to topical neighbours.
- As a beta researcher, I can ask a cross-show question and see results from multiple shows weighted by breadth, so that synthesis queries surface diversity.
- As an agent via MCP, I can receive a shaped context pack — grounded insights first, supporting segments middle, caveats last, within a token budget — so that I extract more value per token.
- As the autoresearch loop, I can issue typed queries and receive tier-weighted results appropriate to my intent without specifying signal weights manually.
Functional Requirements¶
FR1: Two-Tier Index¶
- FR1.1 Segment tier (Tier 1): Transcript chunks, 200–300 words, 50-word overlap over Whisper
output segments. Indexed fields: chunk text (BM25 + vector),
show_id,episode_id,speaker_id(if diarized),start_time,end_time,linked_insight_ids. Catches exact phrases, terminology, raw evidence, content that did not surface as an insight. - FR1.2 Insight tier (Tier 2): GIL insight nodes. Indexed fields: insight text (BM25 + vector),
show_id,episode_id,speaker_id,entity_type,confidence,derived,source_segment_id. Catches distilled claims and positions. - FR1.3: A chunker already exists at
src/podcast_scraper/search/chunker.py(used byindexer.py); RFC-090 extends rather than recreates it.
FR2: Mixed Ranking (RRF)¶
- FR2.1: Both tiers searched in parallel; results merged via RRF into a single ranked list.
- FR2.2:
source_tier: "segment" | "insight"is a payload field on every result — the score decides order, not the tier. - FR2.3: Default RRF tier weights — insights 1.2, segments 1.0. Query router overrides per intent (e.g. raw-evidence → segments 1.3, insights 0.9).
FR3: Deduplication (Compound Results)¶
- FR3.1: When a segment result and an insight result refer to the same underlying content (segment contains the insight's grounding quote), merge into a single compound result at the retrieval layer, carrying both the raw segment and the synthesized insight.
- FR3.2: Compound result takes the higher of the two scores; the consumer receives one object.
FR4: Intent-Aware Routing¶
- FR4.1: Classify query intent (rules-based in RFC-090; ML in RFC-092) and adjust signal/tier weights accordingly.
- FR4.2: Misclassification degrades to sub-optimal weights, not wrong results (RRF is robust).
FR5: Backend Abstraction¶
- FR5.1: A
SearchBackendprotocol decouples signal generation from fusion (RFC-090 §3.2). - FR5.2: LanceDB embedded is the initial backend; swapping backends costs zero changes outside
config/search.yaml.
FR6: Agent Context Packs¶
- FR6.1: MCP search tools return LITM-aware, token-budgeted context packs (RFC-093). Requires an MCP tool layer, which does not exist yet — [TBD].
Success Metrics¶
| Metric | Target |
|---|---|
| Named-entity recall@10 | ≥ 90% top-10 contains exact-match result for queried person/show |
| nDCG@10 vs FAISS baseline | Statistically significant improvement on held-out query set |
| Segment-only result rate | > 20% of top-10 are segment-tier (validates two-tier value) |
| Cross-show diversity (synthesis queries) | ≥ 3 distinct shows in top-10 |
| Compound-result dedup rate | Measurable — confirms segment↔insight linking works |
| Backend swap cost | Zero changes outside config/search.yaml |
Dependencies¶
- RFC-090 Hybrid Retrieval Pipeline — Hard — Draft.
- RFC-091/092/093 — Soft/additive — Draft; the KG and MCP features depend on prerequisites below.
- RFC-072 Canonical Identity — Hard for KG proximity — Partial: only
identity/slugify.pyexists; no entity resolver yet. - GIL contracts — Hard — shipped; grounding quotes carry
timestamp_start_ms/timestamp_end_ms, enabling segment↔insight linking. - Prerequisites not yet built (tracked in #849): file-local entity resolver; in-memory
KnowledgeGraph access layer (
neighbors()/get_node()); typed KG edges beyond the currentMENTIONS/RELATED_TO(note:ABOUTedges + Topic nodes already exist via #485); MCP tool layer.
Constraints & Assumptions¶
Constraints:
- Embedded, local-first backend (LanceDB) on the operator's machine; no cloud dependency.
- English-only corpus.
Assumptions:
- Whisper output segments (
{text, start, end, speaker_id?}) are available to the chunker. Today segments are passed into GI/KG builders as optional sidecar data (src/podcast_scraper/gi/pipeline.py), not retained as a first-class artifact — RFC-090 OQ-5 covers diarization integration. - Embedding model
all-MiniLM-L6-v2(384-dim); validate dimensionality on migration.
Design Considerations¶
Decisions carried from draft review (now resolved):
- Dedup strategy: Compound result at the retrieval layer; consumer receives one object with both segment and insight.
- Result presentation: Uniform ranking by score;
source_tieras a payload field; no forced grouping. - Default tier weights: Insight 1.2, segment 1.0; query router overrides per intent.
- Segment granularity: 200–300 words, 50-word overlap over Whisper output.
- Segment–insight linking:
linked_insight_idson the segment doc;source_segment_idon the insight doc.
Open Questions¶
Risks and their mitigations (tracked as open until validated):
- Segment index size. ~700 episodes × ~200 chunks ≈ ~140k segment docs vs ~15k insights. LanceDB handles this at embedded scale — validate before assuming.
- LanceDB FTS rebuild on upserts. Schedule as a post-ingestion step, not per-upsert (RFC-090 OQ-1).
- Compound-result linking quality. Depends on timestamp overlap between Whisper segments and GIL grounding quotes. Mitigation: tolerance window; log unlinked insights.
- Tier-weight calibration. Defaults are heuristic; weights are config-driven and the eval loop produces tuning signal.
- Rules-based router misclassification. Degrades to wrong weights, not wrong results; RFC-092 ML router fixes systematic misclassification once eval data exists.
Related Work¶
docs/rfc/RFC-090-hybrid-retrieval.mddocs/rfc/RFC-091-kg-proximity-signal.mddocs/rfc/RFC-092-ml-query-router.mddocs/rfc/RFC-093-litm-context-packs.mddocs/rfc/RFC-072-canonical-identity-layer-cross-layer-bridge.mddocs/rfc/RFC-088-enrichment-layer-architecture.mddocs/prd/PRD-031-search.mddocs/prd/PRD-028-position-tracker.md- Issue #849 (Search retrieval prerequisites), #466 (GI/KG depth roadmap — superseded), #484, #485.
Release Checklist¶
- [ ] PRD reviewed and approved
- [ ] RFC-090 (core) approved; prerequisite issues for entity resolver / graph access / typed edges filed in #849
- [ ] Two-tier index + RRF fusion + compound dedup implemented
- [ ] Segment chunking wired post-transcription (extend existing chunker)
- [ ] Eval: two-tier hybrid vs FAISS baseline on held-out query set
- [ ]
config/search.yamlbackend abstraction in place - [ ] FAISS deprecated on confirmed improvement
Future¶
- Turbopuffer or Qdrant Cloud backend (abstraction ready).
- Cross-encoder reranking post-RRF baseline.
- Query expansion / HyDE for sparse queries.
- Personalisation (requires auth layer).