@@ -361,16 +361,31 @@ public function search(array $query, int $k = 10, ?int $ef = null): array
361361 [$ epDist , $ ep ] = $ this ->searchLayerGreedy ($ qv , $ ep , $ epDist , $ lc );
362362 }
363363
364- // Full beam search at layer 0.
365- $ W = $ this ->searchLayer ($ qv , [[$ epDist , $ ep ]], $ ef , 0 );
364+ // Full beam search at layer 0, retrying with a larger ef when soft-deleted
365+ // nodes shrink the active result set below $k. Doubling ef on each retry
366+ // costs at most O(log(totalNodes / ef)) extra passes in the worst case.
367+ $ currentEf = $ ef ;
368+ $ totalNodes = count ($ this ->nodes );
369+
370+ do {
371+ $ W = $ this ->searchLayer ($ qv , [[$ epDist , $ ep ]], $ currentEf , 0 );
372+
373+ // Filter out soft-deleted nodes.
374+ if (!empty ($ this ->deleted )) {
375+ $ W = array_values (array_filter (
376+ $ W ,
377+ fn (array $ pair ) => !isset ($ this ->deleted [$ pair [1 ]])
378+ ));
379+ }
366380
367- // Filter out soft-deleted nodes and take the k nearest.
368- if (!empty ($ this ->deleted )) {
369- $ W = array_values (array_filter (
370- $ W ,
371- fn (array $ pair ) => !isset ($ this ->deleted [$ pair [1 ]])
372- ));
373- }
381+ // Stop when we have enough active results, or ef already spans all nodes
382+ // (further expansion cannot surface new candidates).
383+ if (count ($ W ) >= $ k || $ currentEf >= $ totalNodes ) {
384+ break ;
385+ }
386+
387+ $ currentEf = min ($ currentEf * 2 , $ totalNodes );
388+ } while (true );
374389
375390 $ topK = array_slice ($ W , 0 , $ k );
376391 return $ this ->toSearchResults ($ topK );
0 commit comments