@@ -352,7 +352,6 @@ func (e *Engine) SearchCode(ctx context.Context, filePath, queryText string, lim
352352 primaryLang = a .Name ()
353353 }
354354
355- // Ensure at least the primary collection exists before embedding (fast fail + indexing trigger)
356355 primaryColl := wctx .CollectionName (primaryLang )
357356 t1 := time .Now ()
358357
@@ -369,25 +368,33 @@ func (e *Engine) SearchCode(ctx context.Context, filePath, queryText string, lim
369368 e .StartIndexingAsync (wctx .Root , wctx .ID , nil , false )
370369 }
371370 }
372- logger .Instance .Info ("[IDX] ws=%s Indexing in progress (%d%%) — searching available results " , filepath .Base (wctx .Root ), idxStatus .GlobalPercent )
371+ logger .Instance .Info ("[IDX] ws=%s Indexing in progress (%d%%) — will search available collections " , filepath .Base (wctx .Root ), idxStatus .GlobalPercent )
373372 }
374373 }
375- exists , err := e .search .CollectionExists (ctx , primaryColl )
374+
375+ // Check if the primary collection exists.
376+ // If not, trigger background indexing but do NOT block — the fan-out below
377+ // will search any other language collections that do exist.
378+ primaryExists , err := e .search .CollectionExists (ctx , primaryColl )
376379 logger .Instance .Debug ("[TIMER] SearchCode collection_exists_primary=%v (cached=%v)" , time .Since (t1 ), time .Since (t1 ) < time .Millisecond )
377380 if err != nil {
378381 return nil , fmt .Errorf ("failed to check collection: %w" , err )
379382 }
380- if ! exists {
381- if _ , ok := e .indexingJobs .Load (wctx .ID ); ok {
382- return nil , & ErrIndexingInProgress {WorkspaceRoot : wctx .Root , WorkspaceID : wctx .ID }
383- }
384- e .StartIndexingAsync (wctx .Root , wctx .ID , nil , false )
385- return nil , & ErrIndexingStarted {WorkspaceRoot : wctx .Root , WorkspaceID : wctx .ID }
383+
384+ needsTriggerIndexing := false
385+ if ! primaryExists {
386+ needsTriggerIndexing = true
386387 }
387388
388389 if wctx .ReindexRequired {
389390 logger .Instance .Info ("[IDX] Git state change detected (Head: %s), triggering background re-indexing for %s" , wctx .HeadSHA , wctx .Root )
390- e .StartIndexingAsync (wctx .Root , wctx .ID , nil , false )
391+ needsTriggerIndexing = true
392+ }
393+
394+ if needsTriggerIndexing {
395+ if _ , alreadyRunning := e .indexingJobs .Load (wctx .ID ); ! alreadyRunning {
396+ e .StartIndexingAsync (wctx .Root , wctx .ID , nil , false )
397+ }
391398 }
392399
393400 // Embed ONCE, fan-out to all language collections in parallel
@@ -453,6 +460,7 @@ func (e *Engine) SearchCode(ctx context.Context, filePath, queryText string, lim
453460 var primaryResults []storage.SearchResult
454461 var otherResults []storage.SearchResult
455462 var firstErr error
463+ var existingCollections int
456464 reportLang := primaryLang
457465 reportColl := primaryColl
458466
@@ -464,6 +472,7 @@ func (e *Engine) SearchCode(ctx context.Context, filePath, queryText string, lim
464472 }
465473 continue
466474 }
475+ existingCollections ++
467476 logger .Instance .Debug ("[TIMER] SearchCode lang=%s hits=%d elapsed=%v" , lr .lang , len (lr .results ), lr .elapsed )
468477 if lr .coll == primaryColl {
469478 primaryResults = lr .results
@@ -474,16 +483,58 @@ func (e *Engine) SearchCode(ctx context.Context, filePath, queryText string, lim
474483
475484 all := append (primaryResults , otherResults ... )
476485
486+ // If no collections exist at all, the workspace is truly not indexed.
487+ // Before returning an error, try a direct AST-based fallback search.
488+ if existingCollections == 0 && len (all ) == 0 {
489+ // Ensure background indexing is triggered
490+ if needsTriggerIndexing {
491+ if _ , alreadyRunning := e .indexingJobs .Load (wctx .ID ); ! alreadyRunning {
492+ e .StartIndexingAsync (wctx .Root , wctx .ID , nil , false )
493+ }
494+ }
495+
496+ // Fallback: direct AST search on filesystem — no Qdrant needed
497+ fallbackResults := e .FallbackDirectSearch (ctx , wctx .Root , queryText , limit )
498+ if len (fallbackResults ) > 0 {
499+ return & SearchCodeResult {
500+ Results : fallbackResults ,
501+ WorkspaceRoot : wctx .Root ,
502+ WorkspaceID : wctx .ID ,
503+ Collection : "fallback" ,
504+ Language : primaryLang ,
505+ MismatchRisk : wctx .MismatchRisk ,
506+ DetectionSource : wctx .DetectionSource ,
507+ }, nil
508+ }
509+
510+ // Fallback also empty — report indexing status
511+ if _ , alreadyRunning := e .indexingJobs .Load (wctx .ID ); alreadyRunning {
512+ return nil , & ErrIndexingInProgress {WorkspaceRoot : wctx .Root , WorkspaceID : wctx .ID }
513+ }
514+ if needsTriggerIndexing {
515+ return nil , & ErrIndexingStarted {WorkspaceRoot : wctx .Root , WorkspaceID : wctx .ID }
516+ }
517+ if firstErr != nil {
518+ return nil , fmt .Errorf ("search failed: %w" , firstErr )
519+ }
520+ return nil , & ErrNotIndexed {WorkspaceRoot : wctx .Root , Collection : primaryColl , Language : primaryLang }
521+ }
522+
477523 // Global sort by score descending across all language results, then cap to limit.
478524 // Without this, primary-language results always win regardless of score.
479525 sort .Slice (all , func (i , j int ) bool { return all [i ].Score > all [j ].Score })
480526 if limit > 0 && len (all ) > limit {
481527 all = all [:limit ]
482528 }
483529
484- // If nothing was found and there were errors, surface the error
485- if len (all ) == 0 && firstErr != nil {
486- return nil , fmt .Errorf ("search failed: %w" , firstErr )
530+ // If vector search returned nothing but collections exist, supplement with fallback
531+ if len (all ) == 0 {
532+ fallbackResults := e .FallbackDirectSearch (ctx , wctx .Root , queryText , limit )
533+ if len (fallbackResults ) > 0 {
534+ all = fallbackResults
535+ } else if firstErr != nil {
536+ return nil , fmt .Errorf ("search failed: %w" , firstErr )
537+ }
487538 }
488539
489540 logger .Instance .Debug ("[TIMER] SearchCode TOTAL=%v (detect=%v embed=%v fanout=%v)" ,
@@ -515,7 +566,7 @@ func (e *Engine) HybridSearchCode(ctx context.Context, filePath, queryText strin
515566
516567 collection := wctx .CollectionName (lang )
517568
518- // Auto-resume from index_status.json (Task4) : if indexing was interrupted, resume it.
569+ // Auto-resume from index_status.json: if indexing was interrupted, resume it.
519570 if idxStatus := loadIndexStatus (wctx .Root ); idxStatus != nil {
520571 if idxStatus .WorkspaceID == wctx .ID && idxStatus .GlobalPercent > 0 && idxStatus .GlobalPercent < 100 && idxStatus .State == "running" {
521572 if _ , ok := e .indexingJobs .Load (wctx .ID ); ! ok {
@@ -527,7 +578,7 @@ func (e *Engine) HybridSearchCode(ctx context.Context, filePath, queryText strin
527578 e .StartIndexingAsync (wctx .Root , wctx .ID , nil , false )
528579 }
529580 }
530- logger .Instance .Info ("[IDX] ws=%s Indexing in progress (%d%%) — searching available results " , filepath .Base (wctx .Root ), idxStatus .GlobalPercent )
581+ logger .Instance .Info ("[IDX] ws=%s Indexing in progress (%d%%) — will search available collections " , filepath .Base (wctx .Root ), idxStatus .GlobalPercent )
531582 }
532583 }
533584
@@ -537,16 +588,21 @@ func (e *Engine) HybridSearchCode(ctx context.Context, filePath, queryText strin
537588 }
538589
539590 if ! exists {
540- if _ , ok := e .indexingJobs .Load (wctx .ID ); ok {
541- return nil , & ErrIndexingInProgress {WorkspaceRoot : wctx .Root , WorkspaceID : wctx .ID }
591+ // Trigger background indexing but do NOT block — SmartSearch runs
592+ // SearchCode (with fan-out) in parallel and will provide results
593+ // from any available language collections.
594+ if _ , alreadyRunning := e .indexingJobs .Load (wctx .ID ); ! alreadyRunning {
595+ e .StartIndexingAsync (wctx .Root , wctx .ID , nil , false )
542596 }
543- e . StartIndexingAsync ( wctx . Root , wctx .ID , nil , false )
544- return nil , & ErrIndexingStarted { WorkspaceRoot : wctx . Root , WorkspaceID : wctx . ID }
597+ logger . Instance . Info ( "[IDX] ws=%s HybridSearch: collection %s not found — returning empty (indexing in background)" , filepath . Base ( wctx .Root ), collection )
598+ return nil , nil
545599 }
546600
547601 if wctx .ReindexRequired {
548602 logger .Instance .Info ("[IDX] Git state change detected, triggering background re-indexing for ws=%s" , filepath .Base (wctx .Root ))
549- e .StartIndexingAsync (wctx .Root , wctx .ID , nil , false )
603+ if _ , alreadyRunning := e .indexingJobs .Load (wctx .ID ); ! alreadyRunning {
604+ e .StartIndexingAsync (wctx .Root , wctx .ID , nil , false )
605+ }
550606 }
551607
552608 results , err := e .search .HybridSearch (ctx , collection , queryText , limit )
0 commit comments