@@ -3,6 +3,13 @@ import type { PythonVersion } from "./config";
33import { CONFIG } from "./config" ;
44import { htmlToMarkdown } from "./html-to-markdown" ;
55import type { Logger } from "./logger" ;
6+ import {
7+ createSearchIndex ,
8+ getAvailableTypes ,
9+ inferTypesForQuery ,
10+ type SearchIndex ,
11+ type TypeInferenceResult ,
12+ } from "./search-index" ;
613import type { CachedDoc , DocIndex , FetchedDoc } from "./types" ;
714
815async function fetchWithTimeout ( url : string , log : Logger ) : Promise < string > {
@@ -25,8 +32,22 @@ async function fetchWithTimeout(url: string, log: Logger): Promise<string> {
2532/** Service for fetching and searching Python documentation. */
2633export interface DocService {
2734 getIndex ( version : PythonVersion ) : Promise < DocIndex > ;
35+ getSearchIndex ( version : PythonVersion ) : Promise < SearchIndex > ;
2836 getDoc ( version : PythonVersion , path : string ) : Promise < FetchedDoc > ;
2937 search ( index : DocIndex , query : string , type ?: string , limit ?: number ) : DocIndex [ "entries" ] ;
38+ searchWithFallback (
39+ index : DocIndex ,
40+ searchIndex : SearchIndex ,
41+ query : string ,
42+ type ?: string ,
43+ limit ?: number ,
44+ ) : Promise < {
45+ results : DocIndex [ "entries" ] ;
46+ fallbackUsed : boolean ;
47+ typeInference ?: TypeInferenceResult ;
48+ } > ;
49+ suggestTypes ( searchIndex : SearchIndex , query : string ) : TypeInferenceResult ;
50+ getAvailableTypes ( searchIndex : SearchIndex ) : string [ ] ;
3051}
3152
3253/**
@@ -51,6 +72,32 @@ export function createDocService(cache: CacheManagerInterface, log: Logger): Doc
5172 return index ;
5273 } ,
5374
75+ async getSearchIndex ( version : PythonVersion ) : Promise < SearchIndex > {
76+ const searchIndexPath = cache . getSearchIndexPath ( version ) ;
77+
78+ // Check if we have a valid cached search index
79+ if ( cache . isValid ( searchIndexPath , CONFIG . indexTtlMs ) ) {
80+ const cached = cache . read < SearchIndex > ( searchIndexPath ) ;
81+ if ( cached ) {
82+ await log . info ( `Using cached search index for Python ${ version } ` ) ;
83+ return cached ;
84+ }
85+ }
86+
87+ // Build search index from the main index
88+ await log . info ( `Building search index for Python ${ version } ...` ) ;
89+ const index = await this . getIndex ( version ) ;
90+ const searchIndex = createSearchIndex ( index , version ) ;
91+
92+ // Cache the search index
93+ cache . write ( searchIndexPath , searchIndex ) ;
94+ await log . info (
95+ `Cached search index with ${ searchIndex . keywordMappings . length } keyword mappings` ,
96+ ) ;
97+
98+ return searchIndex ;
99+ } ,
100+
54101 async getDoc ( version : PythonVersion , path : string ) : Promise < FetchedDoc > {
55102 const normalizedPath = path . endsWith ( ".html" ) ? path . slice ( 0 , - 5 ) : path ;
56103 const docPath = cache . getDocPath ( version , normalizedPath ) ;
@@ -89,7 +136,7 @@ export function createDocService(cache: CacheManagerInterface, log: Logger): Doc
89136 if ( results . length >= maxResults ) break ;
90137
91138 const nameMatch = entry . name . toLowerCase ( ) . includes ( q ) ;
92- const typeMatch = ! t || entry . type . toLowerCase ( ) . includes ( t ) ;
139+ const typeMatch = ! t || entry . type . toLowerCase ( ) === t ;
93140
94141 if ( nameMatch && typeMatch ) {
95142 results . push ( entry ) ;
@@ -98,5 +145,71 @@ export function createDocService(cache: CacheManagerInterface, log: Logger): Doc
98145
99146 return results ;
100147 } ,
148+
149+ async searchWithFallback (
150+ index : DocIndex ,
151+ searchIndex : SearchIndex ,
152+ query : string ,
153+ type ?: string ,
154+ limit ?: number ,
155+ ) : Promise < {
156+ results : DocIndex [ "entries" ] ;
157+ fallbackUsed : boolean ;
158+ typeInference ?: TypeInferenceResult ;
159+ } > {
160+ // Try the requested search first
161+ const results = this . search ( index , query , type , limit ) ;
162+
163+ // If we have results or no type filter, return as-is
164+ if ( results . length > 0 || ! type ) {
165+ return { results, fallbackUsed : false } ;
166+ }
167+
168+ // No results with type filter - use type inference to suggest alternatives
169+ await log . info ( `No results for "${ query } " with type "${ type } ". Running type inference...` ) ;
170+
171+ const typeInference = inferTypesForQuery ( query , searchIndex ) ;
172+
173+ // Try searching without the type filter
174+ const resultsNoFilter = this . search ( index , query , undefined , limit ) ;
175+
176+ // Try searching with the most likely inferred types
177+ let resultsWithInferred : DocIndex [ "entries" ] = [ ] ;
178+ for ( const inferredType of typeInference . inferredTypes . slice ( 0 , 2 ) ) {
179+ const inferredResults = this . search (
180+ index ,
181+ query ,
182+ inferredType ,
183+ Math . ceil ( ( limit ?? CONFIG . defaultLimit ) / 2 ) ,
184+ ) ;
185+ resultsWithInferred = resultsWithInferred . concat ( inferredResults ) ;
186+ }
187+
188+ // Combine results: prioritize inferred type matches, then no-filter matches
189+ const seen = new Set < string > ( ) ;
190+ const combinedResults : DocIndex [ "entries" ] = [ ] ;
191+
192+ for ( const entry of resultsWithInferred . concat ( resultsNoFilter ) ) {
193+ if ( ! seen . has ( entry . path ) ) {
194+ seen . add ( entry . path ) ;
195+ combinedResults . push ( entry ) ;
196+ if ( combinedResults . length >= ( limit ?? CONFIG . defaultLimit ) ) break ;
197+ }
198+ }
199+
200+ return {
201+ results : combinedResults ,
202+ fallbackUsed : true ,
203+ typeInference,
204+ } ;
205+ } ,
206+
207+ suggestTypes ( searchIndex : SearchIndex , query : string ) : TypeInferenceResult {
208+ return inferTypesForQuery ( query , searchIndex ) ;
209+ } ,
210+
211+ getAvailableTypes ( searchIndex : SearchIndex ) : string [ ] {
212+ return getAvailableTypes ( searchIndex ) ;
213+ } ,
101214 } ;
102215}
0 commit comments