@@ -579,46 +579,107 @@ type insertFunc func(ctx context.Context, idxCtx *Context, result *SearchResult)
579579// tries again with another partition, and so on. Eventually, the search will
580580// succeed, and searchForInsertHelper returns a search result containing the
581581// insert partition (never nil).
582+ //
583+ // There are two cases to consider, depending on whether we're inserting into a
584+ // root partition or a non-root partition:
585+ //
586+ // 1. Root partition: If the root partition does not allow inserts, then we
587+ // need to instead insert into one of its target partitions. However, there
588+ // are race conditions where those can be splitting in turn, in which case
589+ // we need to retry the search.
590+ // 2. Non-root partition: If the top search result does not allow inserts, then
591+ // we need to try a different result. We start by fetching 2 results, so
592+ // that there's a backup. If both results do not allow inserts, we widen the
593+ // search to 4 results, then to 8, and so on (up to 32, upon which we return
594+ // an error).
595+ //
596+ // Note that it's possible for retry to switch back and forth between #1 and #2,
597+ // if we're racing with levels being added to or removed from the tree.
582598func (vi * Index ) searchForInsertHelper (
583599 ctx context.Context , idxCtx * Context , fn insertFunc ,
584600) (* SearchResult , error ) {
585- // In most cases, the top result is the best insert partition. However, if
586- // that partition does not allow inserts, we need to fall back on a target
587- // paritition.
588- idxCtx .tempSearchSet = SearchSet {MaxResults : 1 }
589- err := vi .searchHelper (ctx , idxCtx , & idxCtx .tempSearchSet )
590- if err != nil {
591- return nil , err
592- }
593- results := idxCtx .tempSearchSet .PopResults ()
594- if len (results ) != 1 {
595- return nil , errors .AssertionFailedf (
596- "SearchForInsert should return exactly one result, got %d" , len (results ))
597- }
598- result := & results [0 ]
599-
600- // Loop until we find an insert partition.
601- var metadata PartitionMetadata
601+ // Loop until we find an insert partition. Fetch two candidate partitions to
602+ // start so that we rarely need to re-search, even in the case where the first
603+ // candidate partition does not allow inserts.
604+ var original , results []SearchResult
605+ var seen map [PartitionKey ]struct {}
606+ maxResults := 2
607+ allowRetry := true
602608 for {
603- err = fn (ctx , idxCtx , result )
609+ // If there are no more results, get more now.
610+ if len (results ) == 0 {
611+ if ! allowRetry || maxResults > 32 {
612+ // This is an extreme edge case where none of the partitions we've
613+ // checked allow inserts.
614+ // TODO(andyk): Do we need to do something better here?
615+ return nil , errors .Newf (
616+ "search failed to find a partition (level=%d) that allows inserts: %v" ,
617+ idxCtx .level , original )
618+ }
619+
620+ // In most cases, the top result is the best insert partition. However, if
621+ // that partition does not allow inserts, we need to fall back to another
622+ // partition. Set MaxResults in the same way as searchHelper does.
623+ idxCtx .tempSearchSet = SearchSet {MaxResults : maxResults }
624+ err := vi .searchHelper (ctx , idxCtx , & idxCtx .tempSearchSet )
625+ if err != nil {
626+ return nil , err
627+ }
628+ original = idxCtx .tempSearchSet .PopResults ()
629+ if len (original ) < 1 {
630+ return nil , errors .AssertionFailedf ("SearchForInsert should return at least one result" )
631+ }
632+ results = original
633+ allowRetry = false
634+ }
635+
636+ // Check first result.
637+ partitionKey := results [0 ].ChildKey .PartitionKey
638+ if seen != nil {
639+ // Don't re-check partitions we've already checked.
640+ if _ , ok := seen [partitionKey ]; ok {
641+ results = results [1 :]
642+ continue
643+ }
644+ }
645+
646+ // Allow retry since there's at least one result we haven't yet seen.
647+ allowRetry = true
648+
649+ err := fn (ctx , idxCtx , & results [0 ])
604650 if err == nil {
605- // Partition supports inserts.
651+ // This partition supports inserts, so done .
606652 break
607653 }
608654
609655 var errConditionFailed * ConditionFailedError
610656 if errors .Is (err , ErrRestartOperation ) {
611- // Entire operation needs to be restarted.
612- return vi .searchForInsertHelper (ctx , idxCtx , fn )
657+ // Redo search operation.
658+ results = results [:0 ]
659+ continue
613660 } else if errors .As (err , & errConditionFailed ) {
614- // This partition does not allow adds or removes, so fallback on a
615- // target partition.
616- metadata = errConditionFailed .Actual
617- partitionKey := results [0 ].ChildKey .PartitionKey
618- result , err = vi .fallbackOnTargets (ctx , idxCtx , partitionKey ,
619- idxCtx .randomized , metadata .StateDetails , results )
620- if err != nil {
621- return nil , err
661+ // This partition does not allow adds or removes, so fallback to
662+ // another partition.
663+ if partitionKey == RootKey {
664+ // This is the root partition, so fallback to its target partitions.
665+ metadata := errConditionFailed .Actual
666+ results , err = vi .fallbackOnTargets (ctx , idxCtx ,
667+ partitionKey , idxCtx .randomized , metadata .StateDetails , results )
668+ if err != nil {
669+ return nil , err
670+ }
671+ } else {
672+ // This is a non-root partition, so check the next partition in the
673+ // result list. If there are no more partitions to check, then expand
674+ // the search to a wider set of partitions by increasing MaxResults.
675+ results = results [1 :]
676+ if len (results ) == 0 {
677+ if seen == nil {
678+ seen = make (map [PartitionKey ]struct {})
679+ }
680+ seen [partitionKey ] = struct {}{}
681+ maxResults *= 2
682+ }
622683 }
623684 } else {
624685 return nil , err
@@ -642,24 +703,22 @@ func (vi *Index) searchForInsertHelper(
642703 results [0 ].ParentPartitionKey , partitionKey , false /* singleStep */ )
643704 }
644705
645- return result , nil
706+ return & results [ 0 ] , nil
646707}
647708
648709// fallbackOnTargets is called when none of the partitions returned by a search
649710// allow inserting a vector, because they are in a Draining state. Instead, the
650711// search needs to continue with the target partitions of the split (or merge).
651- // fallbackOnTargets returns a search result for the target that's closest to
652- // the query vector.
653- // NOTE: "tempResults" is reused within this method and a pointer to one of its
654- // entries is returned as the best result.
712+ // fallbackOnTargets returns an ordered list of search results for the targets.
713+ // NOTE: "tempResults" is overwritten within this method by results.
655714func (vi * Index ) fallbackOnTargets (
656715 ctx context.Context ,
657716 idxCtx * Context ,
658717 partitionKey PartitionKey ,
659718 vec vector.T ,
660719 state PartitionStateDetails ,
661720 tempResults []SearchResult ,
662- ) (* SearchResult , error ) {
721+ ) ([] SearchResult , error ) {
663722 if state .State == DrainingForSplitState {
664723 // Synthesize one search result for each split target partition to pass
665724 // to getFullVectors.
@@ -677,7 +736,9 @@ func (vi *Index) fallbackOnTargets(
677736 var err error
678737 tempResults , err = vi .getFullVectors (ctx , idxCtx , tempResults )
679738 if err != nil {
680- return nil , err
739+ return nil , errors .Wrapf (err ,
740+ "fetching centroids for target partitions %d and %d, for splitting partition %d" ,
741+ state .Target1 , state .Target2 , partitionKey )
681742 }
682743 if len (tempResults ) != 2 {
683744 return nil , errors .AssertionFailedf (
@@ -691,10 +752,13 @@ func (vi *Index) fallbackOnTargets(
691752 tempResults [0 ].QuerySquaredDistance = dist1
692753 tempResults [1 ].QuerySquaredDistance = dist2
693754
694- if dist1 < dist2 {
695- return & tempResults [0 ], nil
755+ if dist1 > dist2 {
756+ // Swap results.
757+ tempResult := tempResults [0 ]
758+ tempResults [0 ] = tempResults [1 ]
759+ tempResults [1 ] = tempResult
696760 }
697- return & tempResults [ 1 ] , nil
761+ return tempResults , nil
698762 }
699763
700764 // TODO(andyk): Add support for DrainingForMergeState.
0 commit comments