github.com/weaviate/weaviate@v1.24.6/adapters/repos/db/vector/hnsw/config_update.go (about)

     1  //                           _       _
     2  // __      _____  __ ___   ___  __ _| |_ ___
     3  // \ \ /\ / / _ \/ _` \ \ / / |/ _` | __/ _ \
     4  //  \ V  V /  __/ (_| |\ V /| | (_| | ||  __/
     5  //   \_/\_/ \___|\__,_| \_/ |_|\__,_|\__\___|
     6  //
     7  //  Copyright © 2016 - 2024 Weaviate B.V. All rights reserved.
     8  //
     9  //  CONTACT: hello@weaviate.io
    10  //
    11  
    12  package hnsw
    13  
    14  import (
    15  	"os"
    16  	"sync/atomic"
    17  
    18  	enterrors "github.com/weaviate/weaviate/entities/errors"
    19  	"github.com/weaviate/weaviate/usecases/configbase"
    20  
    21  	"github.com/pkg/errors"
    22  	"github.com/weaviate/weaviate/entities/schema"
    23  	ent "github.com/weaviate/weaviate/entities/vectorindex/hnsw"
    24  )
    25  
    26  func ValidateUserConfigUpdate(initial, updated schema.VectorIndexConfig) error {
    27  	initialParsed, ok := initial.(ent.UserConfig)
    28  	if !ok {
    29  		return errors.Errorf("initial is not UserConfig, but %T", initial)
    30  	}
    31  
    32  	updatedParsed, ok := updated.(ent.UserConfig)
    33  	if !ok {
    34  		return errors.Errorf("updated is not UserConfig, but %T", updated)
    35  	}
    36  
    37  	immutableFields := []immutableParameter{
    38  		{
    39  			name:     "efConstruction",
    40  			accessor: func(c ent.UserConfig) interface{} { return c.EFConstruction },
    41  		},
    42  		{
    43  			name:     "maxConnections",
    44  			accessor: func(c ent.UserConfig) interface{} { return c.MaxConnections },
    45  		},
    46  		{
    47  			// NOTE: There isn't a technical reason for this to be immutable, it
    48  			// simply hasn't been implemented yet. It would require to stop the
    49  			// current timer and start a new one. Certainly possible, but let's see
    50  			// if anyone actually needs this before implementing it.
    51  			name:     "cleanupIntervalSeconds",
    52  			accessor: func(c ent.UserConfig) interface{} { return c.CleanupIntervalSeconds },
    53  		},
    54  		{
    55  			name:     "distance",
    56  			accessor: func(c ent.UserConfig) interface{} { return c.Distance },
    57  		},
    58  	}
    59  
    60  	for _, u := range immutableFields {
    61  		if err := validateImmutableField(u, initialParsed, updatedParsed); err != nil {
    62  			return err
    63  		}
    64  	}
    65  
    66  	return nil
    67  }
    68  
    69  type immutableParameter struct {
    70  	accessor func(c ent.UserConfig) interface{}
    71  	name     string
    72  }
    73  
    74  func validateImmutableField(u immutableParameter,
    75  	previous, next ent.UserConfig,
    76  ) error {
    77  	oldField := u.accessor(previous)
    78  	newField := u.accessor(next)
    79  	if oldField != newField {
    80  		return errors.Errorf("%s is immutable: attempted change from \"%v\" to \"%v\"",
    81  			u.name, oldField, newField)
    82  	}
    83  
    84  	return nil
    85  }
    86  
    87  func (h *hnsw) UpdateUserConfig(updated schema.VectorIndexConfig, callback func()) error {
    88  	parsed, ok := updated.(ent.UserConfig)
    89  	if !ok {
    90  		callback()
    91  		return errors.Errorf("config is not UserConfig, but %T", updated)
    92  	}
    93  
    94  	// Store automatically as a lock here would be very expensive, this value is
    95  	// read on every single user-facing search, which can be highly concurrent
    96  	atomic.StoreInt64(&h.ef, int64(parsed.EF))
    97  	atomic.StoreInt64(&h.efMin, int64(parsed.DynamicEFMin))
    98  	atomic.StoreInt64(&h.efMax, int64(parsed.DynamicEFMax))
    99  	atomic.StoreInt64(&h.efFactor, int64(parsed.DynamicEFFactor))
   100  	atomic.StoreInt64(&h.flatSearchCutoff, int64(parsed.FlatSearchCutoff))
   101  
   102  	if !parsed.PQ.Enabled && !parsed.BQ.Enabled {
   103  		callback()
   104  		return nil
   105  	}
   106  
   107  	h.pqConfig = parsed.PQ
   108  	if asyncEnabled() {
   109  		callback()
   110  		return nil
   111  	}
   112  
   113  	if !h.compressed.Load() {
   114  		// the compression will fire the callback once it's complete
   115  		return h.TurnOnCompression(callback)
   116  	} else {
   117  		h.compressor.SetCacheMaxSize(int64(parsed.VectorCacheMaxObjects))
   118  		callback()
   119  		return nil
   120  	}
   121  }
   122  
   123  func asyncEnabled() bool {
   124  	return configbase.Enabled(os.Getenv("ASYNC_INDEXING"))
   125  }
   126  
   127  func (h *hnsw) TurnOnCompression(callback func()) error {
   128  	h.logger.WithField("action", "compress").Info("switching to compressed vectors")
   129  
   130  	err := ent.ValidatePQConfig(h.pqConfig)
   131  	if err != nil {
   132  		callback()
   133  		return err
   134  	}
   135  
   136  	enterrors.GoWrapper(func() { h.compressThenCallback(callback) }, h.logger)
   137  
   138  	return nil
   139  }
   140  
   141  func (h *hnsw) compressThenCallback(callback func()) {
   142  	defer callback()
   143  
   144  	uc := ent.UserConfig{
   145  		PQ: h.pqConfig,
   146  		BQ: ent.BQConfig{
   147  			Enabled: !h.pqConfig.Enabled,
   148  		},
   149  	}
   150  	if err := h.compress(uc); err != nil {
   151  		h.logger.Error(err)
   152  		return
   153  	}
   154  	h.logger.WithField("action", "compress").Info("vector compression complete")
   155  }