@@ -4,7 +4,7 @@ import type { EmbeddingRunner } from "../embeddings/EmbeddingRunner";
44import { runPromotionSweep } from "../core/SalienceEngine" ;
55import { computeSubgraphBounds } from "../core/HotpathPolicy" ;
66import type { QueryResult } from "./QueryResult" ;
7- import { rankPages , spillToWarm } from "./Ranking" ;
7+ import { rankPages , rankBooks , rankVolumes , rankShelves , spillToWarm } from "./Ranking" ;
88import { buildMetroid } from "./MetroidBuilder" ;
99import { detectKnowledgeGap } from "./KnowledgeGapDetector" ;
1010import { solveOpenTSP } from "./OpenTSPSolver" ;
@@ -46,11 +46,82 @@ export async function query(
4646
4747 const rankingOptions = { vectorStore, metadataStore } ;
4848
49- // --- HOT path: score resident pages ---
50- const hotpathEntries = await metadataStore . getHotpathEntries ( "page" ) ;
51- const hotpathIds = hotpathEntries . map ( ( e ) => e . entityId ) ;
49+ // --- Hierarchical routing: Shelf → Volume → Book → Page ---
50+ // When higher-tier hotpath entries exist, we route through the hierarchy
51+ // to narrow the candidate set before flat page scoring.
52+ const hotpathShelfEntries = await metadataStore . getHotpathEntries ( "shelf" ) ;
53+ const hotpathVolumeEntries = await metadataStore . getHotpathEntries ( "volume" ) ;
54+ const hotpathBookEntries = await metadataStore . getHotpathEntries ( "book" ) ;
55+ const hotpathPageEntries = await metadataStore . getHotpathEntries ( "page" ) ;
56+
57+ // Collect candidate page IDs from hierarchical routing.
58+ const hierarchyPageIds = new Set < Hash > ( ) ;
59+
60+ // Shelf → Volume → Book → Page drill-down
61+ if ( hotpathShelfEntries . length > 0 ) {
62+ const topShelves = await rankShelves (
63+ queryEmbedding ,
64+ hotpathShelfEntries . map ( ( e ) => e . entityId ) ,
65+ Math . max ( 2 , Math . ceil ( hotpathShelfEntries . length / 2 ) ) ,
66+ rankingOptions ,
67+ ) ;
68+ for ( const s of topShelves ) {
69+ const shelf = await metadataStore . getShelf ( s . id ) ;
70+ if ( shelf ) {
71+ for ( const vid of shelf . volumeIds ) hierarchyPageIds . add ( vid ) ;
72+ }
73+ }
74+ }
75+
76+ // Rank volumes — include both hotpath volumes and those found via shelf drill-down
77+ const volumeCandidateIds = new Set < Hash > ( [
78+ ...hotpathVolumeEntries . map ( ( e ) => e . entityId ) ,
79+ ...hierarchyPageIds ,
80+ ] ) ;
81+ hierarchyPageIds . clear ( ) ;
82+
83+ if ( volumeCandidateIds . size > 0 ) {
84+ const topVolumes = await rankVolumes (
85+ queryEmbedding ,
86+ [ ...volumeCandidateIds ] ,
87+ Math . max ( 2 , Math . ceil ( volumeCandidateIds . size / 2 ) ) ,
88+ rankingOptions ,
89+ ) ;
90+ for ( const v of topVolumes ) {
91+ const volume = await metadataStore . getVolume ( v . id ) ;
92+ if ( volume ) {
93+ for ( const bid of volume . bookIds ) hierarchyPageIds . add ( bid ) ;
94+ }
95+ }
96+ }
5297
53- const hotResults = await rankPages ( queryEmbedding , hotpathIds , topK , rankingOptions ) ;
98+ // Rank books — include both hotpath books and those found via volume drill-down
99+ const bookCandidateIds = new Set < Hash > ( [
100+ ...hotpathBookEntries . map ( ( e ) => e . entityId ) ,
101+ ...hierarchyPageIds ,
102+ ] ) ;
103+ hierarchyPageIds . clear ( ) ;
104+
105+ if ( bookCandidateIds . size > 0 ) {
106+ const topBooks = await rankBooks (
107+ queryEmbedding ,
108+ [ ...bookCandidateIds ] ,
109+ Math . max ( 2 , Math . ceil ( bookCandidateIds . size / 2 ) ) ,
110+ rankingOptions ,
111+ ) ;
112+ for ( const b of topBooks ) {
113+ const book = await metadataStore . getBook ( b . id ) ;
114+ if ( book ) {
115+ for ( const pid of book . pageIds ) hierarchyPageIds . add ( pid ) ;
116+ }
117+ }
118+ }
119+
120+ // --- HOT path: score resident pages merged with hierarchy-discovered pages ---
121+ const hotpathIds = hotpathPageEntries . map ( ( e ) => e . entityId ) ;
122+ const combinedPageIds = new Set < Hash > ( [ ...hotpathIds , ...hierarchyPageIds ] ) ;
123+
124+ const hotResults = await rankPages ( queryEmbedding , [ ...combinedPageIds ] , topK , rankingOptions ) ;
54125 const seenIds = new Set ( hotResults . map ( ( r ) => r . id ) ) ;
55126
56127 // --- Warm spill: fill up to topK if hot path is insufficient ---
@@ -75,8 +146,7 @@ export async function query(
75146 . map ( ( r ) => r . score ) ;
76147
77148 // --- MetroidBuilder: build dialectical probe ---
78- // Candidates: hotpath book medoid pages + hotpath pages themselves
79- const hotpathBookEntries = await metadataStore . getHotpathEntries ( "book" ) ;
149+ // Candidates: hotpath book medoid pages + top-ranked pages
80150 const bookCandidates = (
81151 await Promise . all (
82152 hotpathBookEntries . map ( async ( e ) => {
0 commit comments