@@ -62,6 +62,7 @@ import {
6262 structuredSearch ,
6363 addLineNumbers ,
6464 type ExpandedQuery ,
65+ type HybridQueryExplain ,
6566 type StructuredSubSearch ,
6667 DEFAULT_EMBED_MODEL ,
6768 DEFAULT_RERANK_MODEL ,
@@ -1767,6 +1768,7 @@ type OutputOptions = {
17671768 all ?: boolean ;
17681769 collection ?: string | string [ ] ; // Filter by collection name(s)
17691770 lineNumbers ?: boolean ; // Add line numbers to output
1771+ explain ?: boolean ; // Include retrieval score traces (query only)
17701772 context ?: string ; // Optional context for query expansion
17711773 candidateLimit ?: number ; // Max candidates to rerank (default: 40)
17721774} ;
@@ -1792,6 +1794,10 @@ function formatScore(score: number): string {
17921794 return `${ c . dim } ${ pct } %${ c . reset } ` ;
17931795}
17941796
1797+ function formatExplainNumber ( value : number ) : string {
1798+ return value . toFixed ( 4 ) ;
1799+ }
1800+
17951801// Shorten directory path for display - relative to $HOME (used for context paths, not documents)
17961802function shortPath ( dirpath : string ) : string {
17971803 const home = homedir ( ) ;
@@ -1828,7 +1834,20 @@ function printEmptySearchResults(format: OutputFormat, reason: EmptySearchReason
18281834 console . log ( "No results found." ) ;
18291835}
18301836
1831- function outputResults ( results : { file : string ; displayPath : string ; title : string ; body : string ; score : number ; context ?: string | null ; chunkPos ?: number ; hash ?: string ; docid ?: string } [ ] , query : string , opts : OutputOptions ) : void {
1837+ type OutputRow = {
1838+ file : string ;
1839+ displayPath : string ;
1840+ title : string ;
1841+ body : string ;
1842+ score : number ;
1843+ context ?: string | null ;
1844+ chunkPos ?: number ;
1845+ hash ?: string ;
1846+ docid ?: string ;
1847+ explain ?: HybridQueryExplain ;
1848+ } ;
1849+
1850+ function outputResults ( results : OutputRow [ ] , query : string , opts : OutputOptions ) : void {
18321851 const filtered = results . filter ( r => r . score >= opts . minScore ) . slice ( 0 , opts . limit ) ;
18331852
18341853 if ( filtered . length === 0 ) {
@@ -1857,6 +1876,7 @@ function outputResults(results: { file: string; displayPath: string; title: stri
18571876 ...( row . context && { context : row . context } ) ,
18581877 ...( body && { body } ) ,
18591878 ...( snippet && { snippet } ) ,
1879+ ...( opts . explain && row . explain && { explain : row . explain } ) ,
18601880 } ;
18611881 } ) ;
18621882 console . log ( JSON . stringify ( output , null , 2 ) ) ;
@@ -1896,6 +1916,28 @@ function outputResults(results: { file: string; displayPath: string; title: stri
18961916 // Line 4: Score
18971917 const score = formatScore ( row . score ) ;
18981918 console . log ( `Score: ${ c . bold } ${ score } ${ c . reset } ` ) ;
1919+ if ( opts . explain && row . explain ) {
1920+ const explain = row . explain ;
1921+ const ftsScores = explain . ftsScores . length > 0
1922+ ? explain . ftsScores . map ( formatExplainNumber ) . join ( ", " )
1923+ : "none" ;
1924+ const vecScores = explain . vectorScores . length > 0
1925+ ? explain . vectorScores . map ( formatExplainNumber ) . join ( ", " )
1926+ : "none" ;
1927+ const contribSummary = explain . rrf . contributions
1928+ . slice ( )
1929+ . sort ( ( a , b ) => b . rrfContribution - a . rrfContribution )
1930+ . slice ( 0 , 3 )
1931+ . map ( c => `${ c . source } /${ c . queryType } #${ c . rank } :${ formatExplainNumber ( c . rrfContribution ) } ` )
1932+ . join ( " | " ) ;
1933+
1934+ console . log ( `${ c . dim } Explain: fts=[${ ftsScores } ] vec=[${ vecScores } ]${ c . reset } ` ) ;
1935+ console . log ( `${ c . dim } RRF: total=${ formatExplainNumber ( explain . rrf . totalScore ) } base=${ formatExplainNumber ( explain . rrf . baseScore ) } bonus=${ formatExplainNumber ( explain . rrf . topRankBonus ) } rank=${ explain . rrf . rank } ${ c . reset } ` ) ;
1936+ console . log ( `${ c . dim } Blend: ${ Math . round ( explain . rrf . weight * 100 ) } %*${ formatExplainNumber ( explain . rrf . positionScore ) } + ${ Math . round ( ( 1 - explain . rrf . weight ) * 100 ) } %*${ formatExplainNumber ( explain . rerankScore ) } = ${ formatExplainNumber ( explain . blendedScore ) } ${ c . reset } ` ) ;
1937+ if ( contribSummary . length > 0 ) {
1938+ console . log ( `${ c . dim } Top RRF contributions: ${ contribSummary } ${ c . reset } ` ) ;
1939+ }
1940+ }
18991941 console . log ( ) ;
19001942
19011943 // Snippet with highlighting (diff-style header included)
@@ -2179,6 +2221,7 @@ async function querySearch(query: string, opts: OutputOptions, _embedModel: stri
21792221 limit : opts . all ? 500 : ( opts . limit || 10 ) ,
21802222 minScore : opts . minScore || 0 ,
21812223 candidateLimit : opts . candidateLimit ,
2224+ explain : ! ! opts . explain ,
21822225 hooks : {
21832226 onEmbedStart : ( count ) => {
21842227 process . stderr . write ( `${ c . dim } Embedding ${ count } ${ count === 1 ? 'query' : 'queries' } ...${ c . reset } ` ) ;
@@ -2203,6 +2246,7 @@ async function querySearch(query: string, opts: OutputOptions, _embedModel: stri
22032246 limit : opts . all ? 500 : ( opts . limit || 10 ) ,
22042247 minScore : opts . minScore || 0 ,
22052248 candidateLimit : opts . candidateLimit ,
2249+ explain : ! ! opts . explain ,
22062250 hooks : {
22072251 onStrongSignal : ( score ) => {
22082252 process . stderr . write ( `${ c . dim } Strong BM25 signal (${ score . toFixed ( 2 ) } ) — skipping expansion${ c . reset } \n` ) ;
@@ -2263,6 +2307,7 @@ async function querySearch(query: string, opts: OutputOptions, _embedModel: stri
22632307 score : r . score ,
22642308 context : r . context ,
22652309 docid : r . docid ,
2310+ explain : r . explain ,
22662311 } ) ) , displayQuery , { ...opts , limit : results . length } ) ;
22672312 } , { maxDuration : 10 * 60 * 1000 , name : 'querySearch' } ) ;
22682313}
@@ -2292,6 +2337,7 @@ function parseCLI() {
22922337 xml : { type : "boolean" } ,
22932338 files : { type : "boolean" } ,
22942339 json : { type : "boolean" } ,
2340+ explain : { type : "boolean" } ,
22952341 collection : { type : "string" , short : "c" , multiple : true } , // Filter by collection(s)
22962342 // Collection options
22972343 name : { type : "string" } , // collection name
@@ -2346,6 +2392,7 @@ function parseCLI() {
23462392 collection : values . collection as string [ ] | undefined ,
23472393 lineNumbers : ! ! values [ "line-numbers" ] ,
23482394 candidateLimit : values [ "candidate-limit" ] ? parseInt ( String ( values [ "candidate-limit" ] ) , 10 ) : undefined ,
2395+ explain : ! ! values . explain ,
23492396 } ;
23502397
23512398 return {
@@ -2449,6 +2496,7 @@ function showHelp(): void {
24492496 console . log ( " --full - Output full document instead of snippet" ) ;
24502497 console . log ( " -C, --candidate-limit <n> - Max candidates to rerank (default 40, lower = faster)" ) ;
24512498 console . log ( " --line-numbers - Include line numbers in output" ) ;
2499+ console . log ( " --explain - Include retrieval score traces (query --json/CLI)" ) ;
24522500 console . log ( " --files | --json | --csv | --md | --xml - Output format" ) ;
24532501 console . log ( " -c, --collection <name> - Filter by one or more collections" ) ;
24542502 console . log ( "" ) ;
0 commit comments