RFC-094: Search-Powered Surfaces — Relational-Query Layer + Front-End Retrieval State¶
- Status: Completed (v2.6.0–v2.7) — relational query layer shipped via #882–#888 (PR #890); OQ-1 panel cache shipped via #1075 chunk 4 (PR #1089); OQ-2
activeSearchContextPinia store + OQ-3 skeleton-first async cards live on main (web/gi-kg-viewer/src/stores/activeSearchContext.tsconsumed byLibraryView.vue+GraphCanvas.vue;PersonLandingView.vue+TopicEntityView.vuerender skeleton-first with stale-gating). MCP exposure landed via PRD-034 / RFC-095 (16 tools, stdio). - v2 closure (RFC-097, 2026-06-20): stable
person:/topic:/org:IDs formalized via CIL (RFC-072 + RFC-097); v2-materialized ABOUT + MENTIONS_PERSON + MENTIONS_ORG edges enablewho_said_what_about(topic),insights_about(person),mentions_of_person(org)queries against per-artifact data (no CorpusGraph composition required). - Authors: Marko
- Stakeholders: Core team
- Related PRDs:
docs/prd/PRD-033-search-powered-surfaces.md— the consumer-side spec this RFC implements the shared layer fordocs/prd/PRD-031-search.md,docs/prd/PRD-032-hybrid-corpus-search.md- Related RFCs:
docs/rfc/RFC-090-hybrid-retrieval.md— hybrid retrieval backend (ranking)docs/rfc/RFC-091-kg-proximity-signal.md— KG proximity rejected as a retrieval signal; relational structure comes from edges, not proximitydocs/rfc/RFC-093-litm-context-packs.md— MCP briefing packs (orthogonal; Dashboard)- Related ADRs:
- (none yet)
Abstract¶
PRD-033 maps how every viewer surface consumes the shipped retrieval + relational-edge foundation.
Most surfaces (Search, Library, Digest) already have what they need — the existing hybrid
/api/search path. But the Detail panels and Graph need relational queries that do not
exist yet: "what positions does this person hold," "who said what about this topic," "what entities
does this insight concern," "what episodes belong to this show." The edges to answer these shipped
in #874 (Person→Insight, Insight→Entity, Podcast→HAS_EPISODE→Episode) and live in the
cross-layer CorpusGraph — but nothing exposes them as queries.
This RFC defines the one shared, cross-cutting piece PRD-033 depends on: a relational-query layer (a small read API over the corpus graph + hybrid index) and the front-end retrieval-state architecture that surfaces use to consume it (resolving PRD-033 OQ-1/2/3). The surface UIs themselves are execution against this contract and are tracked as per-surface issues, not here.
Ranking comes from hybrid (BM25+dense); relational structure comes from the edges. KG-proximity is not used (RFC-091 Decision Record).
Problem Statement¶
The relational edges are in the corpus but unconsumed — the same dormancy risk that retired KG-proximity. Concretely:
- No relational query API.
positions_of(person)etc. require traversingCorpusGraph(Person ←SPOKEN_BY← Quote ←SUPPORTED_BY← Insight,Insight ─MENTIONS→ Entity,Podcast ─HAS_EPISODE→ Episode) + ranking with hybrid. There is no module or route that does this. - No front-end retrieval-state architecture. PRD-033 OQ-1/2/3 are unresolved: per-entity panel caching, cross-surface search context, async card rendering.
Without this shared layer, every Detail/Graph surface would re-implement graph traversal + caching ad hoc.
Goals¶
- A read-only relational-query layer exposing a small, fixed set of queries over the shipped edges + hybrid index, with stable request/response contracts.
- Resolve PRD-033 OQ-1/2/3 (panel cache, search context, async cards) as concrete front-end patterns.
- Be additive and non-blocking: no new extraction, no new edges; consumes only what shipped.
Non-Goals¶
- The surface UIs (per-surface issues).
- New edges or extraction (contradiction, etc. are orthogonal — PRD-033 §Orthogonal capabilities).
- The MCP briefing-pack layer (RFC-093) — Dashboard's richest cards are orthogonal.
Design & Implementation¶
1. Relational-query layer (search/relational_queries.py)¶
A read-only module over get_corpus_graph(corpus_dir) (#849 cache) + the hybrid retrieval path.
Each function takes a resolved canonical id (via the existing EntityResolver) and returns a typed
result. Ranking of supporting evidence uses hybrid; structure uses graph traversal.
| Query | Inputs | Returns | Backed by |
|---|---|---|---|
positions_of(person_id) |
person: id |
the person's insights (their positions) + grounding segments | Person→Insight + compounds |
who_said(topic_id) |
topic: id |
per-person insights on the topic | Insight ─ABOUT→ Topic + Person→Insight |
insights_about(entity_id) |
person:/org: id |
insights that concern the entity | Insight ─MENTIONS→ Entity |
entities_in(insight_id) |
insight: id |
the people/orgs an insight concerns | Insight ─MENTIONS→ Entity |
episodes_of(show_id) |
podcast: id |
the show's episodes | Podcast ─HAS_EPISODE→ Episode |
cross_show_synthesis(topic_id) |
topic: id |
top insight per distinct show covering the topic | hybrid scoped to topic, grouped by show |
related_insights(scope) |
topic/episode scope | insights ranked for the scope | hybrid (topic/episode-scoped) |
Contracts are plain dataclasses (id, text, score, source_tier, show_id, episode_id, grounding). All
queries degrade gracefully (empty result, never raise) and are bounded by k.
2. API surface (server/routes/relational.py)¶
Thin FastAPI routes wrapping the layer, one per query, e.g. GET /api/relational/positions?person=….
Same auth/middleware as /api/search. (MCP-tool exposure of the same functions is a later,
orthogonal step — RFC-093.)
3. Front-end retrieval state (resolves PRD-033 OQ-1/2/3)¶
- OQ-1
PanelRetrievalStore(Pinia). Keyed by canonical id, short TTL. A panel open triggers one relational-query call; opens within TTL are instant. The single cache all Detail panels share. - OQ-2
activeSearchContext(Pinia). The current search/filter context, so Library rows show "why this episode" snippets and Graph can weight nodes by the active query. Read-only consumers; one writer (the search bar). - OQ-3 async cards. Retrieval-backed cards render skeleton-first and populate from the layer async; no blocking on retrieval.
4. How surfaces consume it (maps to PRD-033 FRs)¶
| Surface | Uses |
|---|---|
| Search / Library / Digest | existing /api/search (+ activeSearchContext); no new layer needed — can start immediately |
| Detail — Person Landing | positions_of + compounds (PanelRetrievalStore) |
| Detail — Topic Entity View | cross_show_synthesis + who_said + insights_about |
| Detail — Episode Detail | related_insights(episode) |
| Graph | node/edge signals from the layer; node click → populated Detail (above) |
| Dashboard | related_insights(topic) for cards (briefing-pack form is orthogonal, RFC-093) |
Rollout & Sequencing¶
- Layer first (
relational_queries.py+ routes) — unblocks Detail + Graph. - Front-end stores (OQ-1/2/3).
- Per-surface issues consume the layer. Search/Library/Digest can proceed in parallel before the
layer lands (they reuse
/api/search).
Open Questions¶
- OQ-A Caching scope. Per-corpus process cache for
CorpusGraphalready exists (#849); the panel TTL is front-end. Confirm no server-side per-query cache is needed at current scale. - OQ-B MCP exposure. Whether the same functions are surfaced as MCP tools now or with RFC-093. Default: routes now, MCP with the briefing-pack work.
Benefits¶
- The shipped edges stop being dormant — a real consumer, with a stable contract.
- One place defines the relational queries; surfaces don't re-implement traversal.
- Front-end retrieval state is decided once, not per panel.
Migration Path¶
Additive — new module + routes + Pinia stores. No changes to existing search, indexing, or edges.