github.com/weaviate/weaviate@v1.24.6/adapters/repos/db/vector/flat/index.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 flat
    13  
    14  import (
    15  	"context"
    16  	"encoding/binary"
    17  	"fmt"
    18  	"io"
    19  	"math"
    20  	"strings"
    21  	"sync"
    22  	"sync/atomic"
    23  
    24  	"github.com/pkg/errors"
    25  	"github.com/sirupsen/logrus"
    26  	"github.com/weaviate/weaviate/adapters/repos/db/helpers"
    27  	"github.com/weaviate/weaviate/adapters/repos/db/lsmkv"
    28  	"github.com/weaviate/weaviate/adapters/repos/db/priorityqueue"
    29  	"github.com/weaviate/weaviate/adapters/repos/db/vector/cache"
    30  	"github.com/weaviate/weaviate/adapters/repos/db/vector/common"
    31  	"github.com/weaviate/weaviate/adapters/repos/db/vector/compressionhelpers"
    32  	"github.com/weaviate/weaviate/adapters/repos/db/vector/hnsw/distancer"
    33  	"github.com/weaviate/weaviate/entities/schema"
    34  	flatent "github.com/weaviate/weaviate/entities/vectorindex/flat"
    35  	"github.com/weaviate/weaviate/usecases/floatcomp"
    36  )
    37  
    38  const (
    39  	compressionBQ   = "bq"
    40  	compressionPQ   = "pq"
    41  	compressionNone = "none"
    42  )
    43  
    44  type flat struct {
    45  	sync.Mutex
    46  	id                  string
    47  	targetVector        string
    48  	dims                int32
    49  	store               *lsmkv.Store
    50  	logger              logrus.FieldLogger
    51  	distancerProvider   distancer.Provider
    52  	trackDimensionsOnce sync.Once
    53  	rescore             int64
    54  	bq                  compressionhelpers.BinaryQuantizer
    55  
    56  	pqResults *common.PqMaxPool
    57  	pool      *pools
    58  
    59  	compression string
    60  	bqCache     cache.Cache[uint64]
    61  }
    62  
    63  type distanceCalc func(vecAsBytes []byte) (float32, error)
    64  
    65  func New(cfg Config, uc flatent.UserConfig, store *lsmkv.Store) (*flat, error) {
    66  	if err := cfg.Validate(); err != nil {
    67  		return nil, errors.Wrap(err, "invalid config")
    68  	}
    69  
    70  	logger := cfg.Logger
    71  	if logger == nil {
    72  		l := logrus.New()
    73  		l.Out = io.Discard
    74  		logger = l
    75  	}
    76  
    77  	index := &flat{
    78  		id:                cfg.ID,
    79  		targetVector:      cfg.TargetVector,
    80  		logger:            logger,
    81  		distancerProvider: cfg.DistanceProvider,
    82  		rescore:           extractCompressionRescore(uc),
    83  		pqResults:         common.NewPqMaxPool(100),
    84  		compression:       extractCompression(uc),
    85  		pool:              newPools(),
    86  		store:             store,
    87  	}
    88  	index.initBuckets(context.Background())
    89  	if uc.BQ.Enabled && uc.BQ.Cache {
    90  		index.bqCache = cache.NewShardedUInt64LockCache(index.getBQVector, uc.VectorCacheMaxObjects, cfg.Logger, 0)
    91  	}
    92  
    93  	return index, nil
    94  }
    95  
    96  func (flat *flat) getBQVector(ctx context.Context, id uint64) ([]uint64, error) {
    97  	key := flat.pool.byteSlicePool.Get(8)
    98  	defer flat.pool.byteSlicePool.Put(key)
    99  	binary.BigEndian.PutUint64(key.slice, id)
   100  	bytes, err := flat.store.Bucket(flat.getCompressedBucketName()).Get(key.slice)
   101  	if err != nil {
   102  		return nil, err
   103  	}
   104  	return uint64SliceFromByteSlice(bytes, make([]uint64, len(bytes)/8)), nil
   105  }
   106  
   107  func extractCompression(uc flatent.UserConfig) string {
   108  	if uc.BQ.Enabled && uc.PQ.Enabled {
   109  		return compressionNone
   110  	}
   111  
   112  	if uc.BQ.Enabled {
   113  		return compressionBQ
   114  	}
   115  
   116  	if uc.PQ.Enabled {
   117  		return compressionPQ
   118  	}
   119  
   120  	return compressionNone
   121  }
   122  
   123  func extractCompressionRescore(uc flatent.UserConfig) int64 {
   124  	compression := extractCompression(uc)
   125  	switch compression {
   126  	case compressionPQ:
   127  		return int64(uc.PQ.RescoreLimit)
   128  	case compressionBQ:
   129  		return int64(uc.BQ.RescoreLimit)
   130  	default:
   131  		return 0
   132  	}
   133  }
   134  
   135  func (index *flat) storeCompressedVector(id uint64, vector []byte) {
   136  	index.storeGenericVector(id, vector, index.getCompressedBucketName())
   137  }
   138  
   139  func (index *flat) storeVector(id uint64, vector []byte) {
   140  	index.storeGenericVector(id, vector, index.getBucketName())
   141  }
   142  
   143  func (index *flat) storeGenericVector(id uint64, vector []byte, bucket string) {
   144  	idBytes := make([]byte, 8)
   145  	binary.BigEndian.PutUint64(idBytes, id)
   146  	index.store.Bucket(bucket).Put(idBytes, vector)
   147  }
   148  
   149  func (index *flat) isBQ() bool {
   150  	return index.compression == compressionBQ
   151  }
   152  
   153  func (index *flat) isBQCached() bool {
   154  	return index.bqCache != nil
   155  }
   156  
   157  func (index *flat) Compressed() bool {
   158  	return index.compression != compressionNone
   159  }
   160  
   161  func (index *flat) getBucketName() string {
   162  	if index.targetVector != "" {
   163  		return fmt.Sprintf("%s_%s", helpers.VectorsBucketLSM, index.targetVector)
   164  	}
   165  	return helpers.VectorsBucketLSM
   166  }
   167  
   168  func (index *flat) getCompressedBucketName() string {
   169  	if index.targetVector != "" {
   170  		return fmt.Sprintf("%s_%s", helpers.VectorsCompressedBucketLSM, index.targetVector)
   171  	}
   172  	return helpers.VectorsCompressedBucketLSM
   173  }
   174  
   175  func (index *flat) initBuckets(ctx context.Context) error {
   176  	if err := index.store.CreateOrLoadBucket(ctx, index.getBucketName(),
   177  		lsmkv.WithForceCompation(true),
   178  		lsmkv.WithUseBloomFilter(false),
   179  		lsmkv.WithCalcCountNetAdditions(false),
   180  	); err != nil {
   181  		return fmt.Errorf("Create or load flat vectors bucket: %w", err)
   182  	}
   183  	if index.isBQ() {
   184  		if err := index.store.CreateOrLoadBucket(ctx, index.getCompressedBucketName(),
   185  			lsmkv.WithForceCompation(true),
   186  			lsmkv.WithUseBloomFilter(false),
   187  			lsmkv.WithCalcCountNetAdditions(false),
   188  		); err != nil {
   189  			return fmt.Errorf("Create or load flat compressed vectors bucket: %w", err)
   190  		}
   191  	}
   192  	return nil
   193  }
   194  
   195  func (index *flat) AddBatch(ctx context.Context, ids []uint64, vectors [][]float32) error {
   196  	if err := ctx.Err(); err != nil {
   197  		return err
   198  	}
   199  	if len(ids) != len(vectors) {
   200  		return errors.Errorf("ids and vectors sizes does not match")
   201  	}
   202  	if len(ids) == 0 {
   203  		return errors.Errorf("insertBatch called with empty lists")
   204  	}
   205  	for i := range ids {
   206  		if err := ctx.Err(); err != nil {
   207  			return err
   208  		}
   209  		if err := index.Add(ids[i], vectors[i]); err != nil {
   210  			return err
   211  		}
   212  	}
   213  	return nil
   214  }
   215  
   216  func byteSliceFromUint64Slice(vector []uint64, slice []byte) []byte {
   217  	for i := range vector {
   218  		binary.LittleEndian.PutUint64(slice[i*8:], vector[i])
   219  	}
   220  	return slice
   221  }
   222  
   223  func byteSliceFromFloat32Slice(vector []float32, slice []byte) []byte {
   224  	for i := range vector {
   225  		binary.LittleEndian.PutUint32(slice[i*4:], math.Float32bits(vector[i]))
   226  	}
   227  	return slice
   228  }
   229  
   230  func uint64SliceFromByteSlice(vector []byte, slice []uint64) []uint64 {
   231  	for i := range slice {
   232  		slice[i] = binary.LittleEndian.Uint64(vector[i*8:])
   233  	}
   234  	return slice
   235  }
   236  
   237  func float32SliceFromByteSlice(vector []byte, slice []float32) []float32 {
   238  	for i := range slice {
   239  		slice[i] = math.Float32frombits(binary.LittleEndian.Uint32(vector[i*4:]))
   240  	}
   241  	return slice
   242  }
   243  
   244  func (index *flat) Add(id uint64, vector []float32) error {
   245  	index.trackDimensionsOnce.Do(func() {
   246  		atomic.StoreInt32(&index.dims, int32(len(vector)))
   247  
   248  		if index.isBQ() {
   249  			index.bq = compressionhelpers.NewBinaryQuantizer(nil)
   250  		}
   251  	})
   252  	if len(vector) != int(index.dims) {
   253  		return errors.Errorf("insert called with a vector of the wrong size")
   254  	}
   255  	vector = index.normalized(vector)
   256  	slice := make([]byte, len(vector)*4)
   257  	index.storeVector(id, byteSliceFromFloat32Slice(vector, slice))
   258  
   259  	if index.isBQ() {
   260  		vectorBQ := index.bq.Encode(vector)
   261  		if index.isBQCached() {
   262  			index.bqCache.Grow(id)
   263  			index.bqCache.Preload(id, vectorBQ)
   264  		}
   265  		slice = make([]byte, len(vectorBQ)*8)
   266  		index.storeCompressedVector(id, byteSliceFromUint64Slice(vectorBQ, slice))
   267  	}
   268  	return nil
   269  }
   270  
   271  func (index *flat) Delete(ids ...uint64) error {
   272  	for i := range ids {
   273  		if index.isBQCached() {
   274  			index.bqCache.Delete(context.Background(), ids[i])
   275  		}
   276  		idBytes := make([]byte, 8)
   277  		binary.BigEndian.PutUint64(idBytes, ids[i])
   278  
   279  		if err := index.store.Bucket(index.getBucketName()).Delete(idBytes); err != nil {
   280  			return err
   281  		}
   282  
   283  		if index.isBQ() {
   284  			if err := index.store.Bucket(index.getCompressedBucketName()).Delete(idBytes); err != nil {
   285  				return err
   286  			}
   287  		}
   288  	}
   289  	return nil
   290  }
   291  
   292  func (index *flat) searchTimeRescore(k int) int {
   293  	// load atomically, so we can get away with concurrent updates of the
   294  	// userconfig without having to set a lock each time we try to read - which
   295  	// can be so common that it would cause considerable overhead
   296  	if rescore := int(atomic.LoadInt64(&index.rescore)); rescore > k {
   297  		return rescore
   298  	}
   299  	return k
   300  }
   301  
   302  func (index *flat) SearchByVector(vector []float32, k int, allow helpers.AllowList) ([]uint64, []float32, error) {
   303  	switch index.compression {
   304  	case compressionBQ:
   305  		return index.searchByVectorBQ(vector, k, allow)
   306  	case compressionPQ:
   307  		// use uncompressed for now
   308  		fallthrough
   309  	default:
   310  		return index.searchByVector(vector, k, allow)
   311  	}
   312  }
   313  
   314  func (index *flat) searchByVector(vector []float32, k int, allow helpers.AllowList) ([]uint64, []float32, error) {
   315  	heap := index.pqResults.GetMax(k)
   316  	defer index.pqResults.Put(heap)
   317  
   318  	vector = index.normalized(vector)
   319  
   320  	if err := index.findTopVectors(heap, allow, k,
   321  		index.store.Bucket(index.getBucketName()).Cursor,
   322  		index.createDistanceCalc(vector),
   323  	); err != nil {
   324  		return nil, nil, err
   325  	}
   326  
   327  	ids, dists := index.extractHeap(heap)
   328  	return ids, dists, nil
   329  }
   330  
   331  func (index *flat) createDistanceCalc(vector []float32) distanceCalc {
   332  	return func(vecAsBytes []byte) (float32, error) {
   333  		vecSlice := index.pool.float32SlicePool.Get(len(vecAsBytes) / 4)
   334  		defer index.pool.float32SlicePool.Put(vecSlice)
   335  
   336  		candidate := float32SliceFromByteSlice(vecAsBytes, vecSlice.slice)
   337  		distance, _, err := index.distancerProvider.SingleDist(vector, candidate)
   338  		return distance, err
   339  	}
   340  }
   341  
   342  func (index *flat) searchByVectorBQ(vector []float32, k int, allow helpers.AllowList) ([]uint64, []float32, error) {
   343  	rescore := index.searchTimeRescore(k)
   344  	heap := index.pqResults.GetMax(rescore)
   345  	defer index.pqResults.Put(heap)
   346  
   347  	vector = index.normalized(vector)
   348  	vectorBQ := index.bq.Encode(vector)
   349  
   350  	if index.isBQCached() {
   351  		if err := index.findTopVectorsCached(heap, allow, rescore, vectorBQ); err != nil {
   352  			return nil, nil, err
   353  		}
   354  	} else {
   355  		if err := index.findTopVectors(heap, allow, rescore,
   356  			index.store.Bucket(index.getCompressedBucketName()).Cursor,
   357  			index.createDistanceCalcBQ(vectorBQ),
   358  		); err != nil {
   359  			return nil, nil, err
   360  		}
   361  	}
   362  
   363  	distanceCalc := index.createDistanceCalc(vector)
   364  	idsSlice := index.pool.uint64SlicePool.Get(heap.Len())
   365  	defer index.pool.uint64SlicePool.Put(idsSlice)
   366  
   367  	for i := range idsSlice.slice {
   368  		idsSlice.slice[i] = heap.Pop().ID
   369  	}
   370  	for _, id := range idsSlice.slice {
   371  		candidateAsBytes, err := index.vectorById(id)
   372  		if err != nil {
   373  			return nil, nil, err
   374  		}
   375  		distance, err := distanceCalc(candidateAsBytes)
   376  		if err != nil {
   377  			return nil, nil, err
   378  		}
   379  		index.insertToHeap(heap, k, id, distance)
   380  	}
   381  
   382  	ids, dists := index.extractHeap(heap)
   383  	return ids, dists, nil
   384  }
   385  
   386  func (index *flat) createDistanceCalcBQ(vectorBQ []uint64) distanceCalc {
   387  	return func(vecAsBytes []byte) (float32, error) {
   388  		vecSliceBQ := index.pool.uint64SlicePool.Get(len(vecAsBytes) / 8)
   389  		defer index.pool.uint64SlicePool.Put(vecSliceBQ)
   390  
   391  		candidate := uint64SliceFromByteSlice(vecAsBytes, vecSliceBQ.slice)
   392  		return index.bq.DistanceBetweenCompressedVectors(candidate, vectorBQ)
   393  	}
   394  }
   395  
   396  func (index *flat) vectorById(id uint64) ([]byte, error) {
   397  	idSlice := index.pool.byteSlicePool.Get(8)
   398  	defer index.pool.byteSlicePool.Put(idSlice)
   399  
   400  	binary.BigEndian.PutUint64(idSlice.slice, id)
   401  	return index.store.Bucket(index.getBucketName()).Get(idSlice.slice)
   402  }
   403  
   404  // populates given heap with smallest distances and corresponding ids calculated by
   405  // distanceCalc
   406  func (index *flat) findTopVectors(heap *priorityqueue.Queue[any],
   407  	allow helpers.AllowList, limit int, cursorFn func() *lsmkv.CursorReplace,
   408  	distanceCalc distanceCalc,
   409  ) error {
   410  	var key []byte
   411  	var v []byte
   412  	var id uint64
   413  	allowMax := uint64(0)
   414  
   415  	cursor := cursorFn()
   416  	defer cursor.Close()
   417  
   418  	if allow != nil {
   419  		// nothing allowed, skip search
   420  		if allow.IsEmpty() {
   421  			return nil
   422  		}
   423  
   424  		allowMax = allow.Max()
   425  
   426  		idSlice := index.pool.byteSlicePool.Get(8)
   427  		binary.BigEndian.PutUint64(idSlice.slice, allow.Min())
   428  		key, v = cursor.Seek(idSlice.slice)
   429  		index.pool.byteSlicePool.Put(idSlice)
   430  	} else {
   431  		key, v = cursor.First()
   432  	}
   433  
   434  	// since keys are sorted, once key/id get greater than max allowed one
   435  	// further search can be stopped
   436  	for ; key != nil && (allow == nil || id <= allowMax); key, v = cursor.Next() {
   437  		id = binary.BigEndian.Uint64(key)
   438  		if allow == nil || allow.Contains(id) {
   439  			distance, err := distanceCalc(v)
   440  			if err != nil {
   441  				return err
   442  			}
   443  			index.insertToHeap(heap, limit, id, distance)
   444  		}
   445  	}
   446  	return nil
   447  }
   448  
   449  // populates given heap with smallest distances and corresponding ids calculated by
   450  // distanceCalc
   451  func (index *flat) findTopVectorsCached(heap *priorityqueue.Queue[any],
   452  	allow helpers.AllowList, limit int, vectorBQ []uint64,
   453  ) error {
   454  	var id uint64
   455  	allowMax := uint64(0)
   456  
   457  	if allow != nil {
   458  		// nothing allowed, skip search
   459  		if allow.IsEmpty() {
   460  			return nil
   461  		}
   462  
   463  		allowMax = allow.Max()
   464  
   465  		id = allow.Min()
   466  	} else {
   467  		id = 0
   468  	}
   469  	all := index.bqCache.Len()
   470  
   471  	// since keys are sorted, once key/id get greater than max allowed one
   472  	// further search can be stopped
   473  	for ; id < uint64(all) && (allow == nil || id <= allowMax); id++ {
   474  		if allow == nil || allow.Contains(id) {
   475  			vec, err := index.bqCache.Get(context.Background(), id)
   476  			if err != nil {
   477  				return err
   478  			}
   479  			if len(vec) == 0 {
   480  				continue
   481  			}
   482  			distance, err := index.bq.DistanceBetweenCompressedVectors(vec, vectorBQ)
   483  			if err != nil {
   484  				return err
   485  			}
   486  			index.insertToHeap(heap, limit, id, distance)
   487  		}
   488  	}
   489  	return nil
   490  }
   491  
   492  func (index *flat) insertToHeap(heap *priorityqueue.Queue[any],
   493  	limit int, id uint64, distance float32,
   494  ) {
   495  	if heap.Len() < limit {
   496  		heap.Insert(id, distance)
   497  	} else if heap.Top().Dist > distance {
   498  		heap.Pop()
   499  		heap.Insert(id, distance)
   500  	}
   501  }
   502  
   503  func (index *flat) extractHeap(heap *priorityqueue.Queue[any],
   504  ) ([]uint64, []float32) {
   505  	len := heap.Len()
   506  
   507  	ids := make([]uint64, len)
   508  	dists := make([]float32, len)
   509  	for i := len - 1; i >= 0; i-- {
   510  		item := heap.Pop()
   511  		ids[i] = item.ID
   512  		dists[i] = item.Dist
   513  	}
   514  	return ids, dists
   515  }
   516  
   517  func (index *flat) normalized(vector []float32) []float32 {
   518  	if index.distancerProvider.Type() == "cosine-dot" {
   519  		// cosine-dot requires normalized vectors, as the dot product and cosine
   520  		// similarity are only identical if the vector is normalized
   521  		return distancer.Normalize(vector)
   522  	}
   523  	return vector
   524  }
   525  
   526  func (index *flat) SearchByVectorDistance(vector []float32, targetDistance float32, maxLimit int64, allow helpers.AllowList) ([]uint64, []float32, error) {
   527  	var (
   528  		searchParams = newSearchByDistParams(maxLimit)
   529  
   530  		resultIDs  []uint64
   531  		resultDist []float32
   532  	)
   533  
   534  	recursiveSearch := func() (bool, error) {
   535  		totalLimit := searchParams.TotalLimit()
   536  		ids, dist, err := index.SearchByVector(vector, totalLimit, allow)
   537  		if err != nil {
   538  			return false, errors.Wrap(err, "vector search")
   539  		}
   540  
   541  		// if there is less results than given limit search can be stopped
   542  		shouldContinue := !(len(ids) < totalLimit)
   543  
   544  		// ensures the indexes aren't out of range
   545  		offsetCap := searchParams.OffsetCapacity(ids)
   546  		totalLimitCap := searchParams.TotalLimitCapacity(ids)
   547  
   548  		if offsetCap == totalLimitCap {
   549  			return false, nil
   550  		}
   551  
   552  		ids, dist = ids[offsetCap:totalLimitCap], dist[offsetCap:totalLimitCap]
   553  		for i := range ids {
   554  			if aboveThresh := dist[i] <= targetDistance; aboveThresh ||
   555  				floatcomp.InDelta(float64(dist[i]), float64(targetDistance), 1e-6) {
   556  				resultIDs = append(resultIDs, ids[i])
   557  				resultDist = append(resultDist, dist[i])
   558  			} else {
   559  				// as soon as we encounter a certainty which
   560  				// is below threshold, we can stop searching
   561  				shouldContinue = false
   562  				break
   563  			}
   564  		}
   565  
   566  		return shouldContinue, nil
   567  	}
   568  
   569  	var shouldContinue bool
   570  	var err error
   571  	for shouldContinue, err = recursiveSearch(); shouldContinue && err == nil; {
   572  		searchParams.Iterate()
   573  		if searchParams.MaxLimitReached() {
   574  			index.logger.
   575  				WithField("action", "unlimited_vector_search").
   576  				Warnf("maximum search limit of %d results has been reached",
   577  					searchParams.MaximumSearchLimit())
   578  			break
   579  		}
   580  	}
   581  	if err != nil {
   582  		return nil, nil, err
   583  	}
   584  
   585  	return resultIDs, resultDist, nil
   586  }
   587  
   588  func (index *flat) UpdateUserConfig(updated schema.VectorIndexConfig, callback func()) error {
   589  	parsed, ok := updated.(flatent.UserConfig)
   590  	if !ok {
   591  		callback()
   592  		return errors.Errorf("config is not UserConfig, but %T", updated)
   593  	}
   594  
   595  	// Store automatically as a lock here would be very expensive, this value is
   596  	// read on every single user-facing search, which can be highly concurrent
   597  	atomic.StoreInt64(&index.rescore, extractCompressionRescore(parsed))
   598  
   599  	callback()
   600  	return nil
   601  }
   602  
   603  func (index *flat) Drop(ctx context.Context) error {
   604  	// nothing to do here
   605  	// Shard::drop will take care of handling store's buckets
   606  	return nil
   607  }
   608  
   609  func (index *flat) Flush() error {
   610  	// nothing to do here
   611  	// Shard will take care of handling store's buckets
   612  	return nil
   613  }
   614  
   615  func (index *flat) Shutdown(ctx context.Context) error {
   616  	// nothing to do here
   617  	// Shard::shutdown will take care of handling store's buckets
   618  	return nil
   619  }
   620  
   621  func (index *flat) SwitchCommitLogs(context.Context) error {
   622  	return nil
   623  }
   624  
   625  func (index *flat) ListFiles(ctx context.Context, basePath string) ([]string, error) {
   626  	// nothing to do here
   627  	// Shard::ListBackupFiles will take care of handling store's buckets
   628  	return []string{}, nil
   629  }
   630  
   631  func (i *flat) ValidateBeforeInsert(vector []float32) error {
   632  	return nil
   633  }
   634  
   635  func (index *flat) PostStartup() {
   636  	if !index.isBQCached() {
   637  		return
   638  	}
   639  	cursor := index.store.Bucket(index.getCompressedBucketName()).Cursor()
   640  	defer cursor.Close()
   641  
   642  	for key, v := cursor.First(); key != nil; key, v = cursor.Next() {
   643  		id := binary.BigEndian.Uint64(key)
   644  		index.bqCache.Grow(id)
   645  		index.bqCache.Preload(id, uint64SliceFromByteSlice(v, make([]uint64, len(v)/8)))
   646  	}
   647  }
   648  
   649  func (index *flat) Dump(labels ...string) {
   650  	if len(labels) > 0 {
   651  		fmt.Printf("--------------------------------------------------\n")
   652  		fmt.Printf("--  %s\n", strings.Join(labels, ", "))
   653  	}
   654  	fmt.Printf("--------------------------------------------------\n")
   655  	fmt.Printf("ID: %s\n", index.id)
   656  	fmt.Printf("--------------------------------------------------\n")
   657  }
   658  
   659  func (index *flat) DistanceBetweenVectors(x, y []float32) (float32, bool, error) {
   660  	return index.distancerProvider.SingleDist(x, y)
   661  }
   662  
   663  func (index *flat) ContainsNode(id uint64) bool {
   664  	return true
   665  }
   666  
   667  func (index *flat) DistancerProvider() distancer.Provider {
   668  	return index.distancerProvider
   669  }
   670  
   671  func newSearchByDistParams(maxLimit int64) *common.SearchByDistParams {
   672  	initialOffset := 0
   673  	initialLimit := common.DefaultSearchByDistInitialLimit
   674  
   675  	return common.NewSearchByDistParams(initialOffset, initialLimit, initialOffset+initialLimit, maxLimit)
   676  }
   677  
   678  type immutableParameter struct {
   679  	accessor func(c flatent.UserConfig) interface{}
   680  	name     string
   681  }
   682  
   683  func validateImmutableField(u immutableParameter,
   684  	previous, next flatent.UserConfig,
   685  ) error {
   686  	oldField := u.accessor(previous)
   687  	newField := u.accessor(next)
   688  	if oldField != newField {
   689  		return errors.Errorf("%s is immutable: attempted change from \"%v\" to \"%v\"",
   690  			u.name, oldField, newField)
   691  	}
   692  
   693  	return nil
   694  }
   695  
   696  func ValidateUserConfigUpdate(initial, updated schema.VectorIndexConfig) error {
   697  	initialParsed, ok := initial.(flatent.UserConfig)
   698  	if !ok {
   699  		return errors.Errorf("initial is not UserConfig, but %T", initial)
   700  	}
   701  
   702  	updatedParsed, ok := updated.(flatent.UserConfig)
   703  	if !ok {
   704  		return errors.Errorf("updated is not UserConfig, but %T", updated)
   705  	}
   706  
   707  	immutableFields := []immutableParameter{
   708  		{
   709  			name:     "distance",
   710  			accessor: func(c flatent.UserConfig) interface{} { return c.Distance },
   711  		},
   712  		{
   713  			name:     "bq.cache",
   714  			accessor: func(c flatent.UserConfig) interface{} { return c.BQ.Cache },
   715  		},
   716  		{
   717  			name:     "pq.cache",
   718  			accessor: func(c flatent.UserConfig) interface{} { return c.PQ.Cache },
   719  		},
   720  		{
   721  			name:     "pq",
   722  			accessor: func(c flatent.UserConfig) interface{} { return c.PQ.Enabled },
   723  		},
   724  		{
   725  			name:     "bq",
   726  			accessor: func(c flatent.UserConfig) interface{} { return c.BQ.Enabled },
   727  		},
   728  	}
   729  
   730  	for _, u := range immutableFields {
   731  		if err := validateImmutableField(u, initialParsed, updatedParsed); err != nil {
   732  			return err
   733  		}
   734  	}
   735  	return nil
   736  }