snapshot of https://github.com/blevesearch/bleve.git (master, 2026-09-15; blobs > 131072B omitted)
Some checks failed
Coverage / coverage (push) Failing after 6s
Tests / test (1.24.x, ubuntu-latest) (push) Failing after 42s
Tests / test (1.26.x, ubuntu-latest) (push) Failing after 38s
Tests / test (1.25.x, ubuntu-latest) (push) Failing after 35s
Tests / test (1.25.x, windows-latest) (push) Has been cancelled
Tests / test (1.26.x, macos-latest) (push) Has been cancelled
Tests / test (1.26.x, windows-latest) (push) Has been cancelled
Tests / test (1.24.x, macos-latest) (push) Has been cancelled
Tests / test (1.24.x, windows-latest) (push) Has been cancelled
Tests / test (1.25.x, macos-latest) (push) Has been cancelled
Some checks failed
Coverage / coverage (push) Failing after 6s
Tests / test (1.24.x, ubuntu-latest) (push) Failing after 42s
Tests / test (1.26.x, ubuntu-latest) (push) Failing after 38s
Tests / test (1.25.x, ubuntu-latest) (push) Failing after 35s
Tests / test (1.25.x, windows-latest) (push) Has been cancelled
Tests / test (1.26.x, macos-latest) (push) Has been cancelled
Tests / test (1.26.x, windows-latest) (push) Has been cancelled
Tests / test (1.24.x, macos-latest) (push) Has been cancelled
Tests / test (1.24.x, windows-latest) (push) Has been cancelled
Tests / test (1.25.x, macos-latest) (push) Has been cancelled
This commit is contained in:
626
index/scorch/train_vector.go
Normal file
626
index/scorch/train_vector.go
Normal file
@@ -0,0 +1,626 @@
|
||||
// Copyright (c) 2026 Couchbase, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build vectors
|
||||
// +build vectors
|
||||
|
||||
package scorch
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"maps"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/RoaringBitmap/roaring/v2"
|
||||
"github.com/blevesearch/bleve/v2/util"
|
||||
index "github.com/blevesearch/bleve_index_api"
|
||||
segment "github.com/blevesearch/scorch_segment_api/v2"
|
||||
)
|
||||
|
||||
type trainRequest struct {
|
||||
finalSample bool
|
||||
sampleSize int
|
||||
ackCh chan error
|
||||
sample segment.Segment
|
||||
trainingParams *index.TrainingParams
|
||||
removedFileWriterIDs map[string]struct{}
|
||||
}
|
||||
|
||||
type vectorTrainer struct {
|
||||
trainingComplete atomic.Bool
|
||||
trainedSamples uint64
|
||||
parent *Scorch
|
||||
config map[string]interface{}
|
||||
|
||||
m sync.RWMutex
|
||||
// not a searchable segment in the sense that it won't return
|
||||
// the data vectors, returns trained centroid layout
|
||||
trainedIndex *SegmentSnapshot
|
||||
trainCh chan *trainRequest
|
||||
// closed by trainLoop when it exits (via close, training completion, or
|
||||
// error). Producers select on it so they never block on a request that the
|
||||
// trainer will never consume/ack.
|
||||
doneCh chan struct{}
|
||||
}
|
||||
|
||||
const IndexTrainedWithFastMerge = "vector_index_fast_merge"
|
||||
|
||||
func initTrainer(s *Scorch, config map[string]interface{}) *vectorTrainer {
|
||||
if f, ok := config[IndexTrainedWithFastMerge]; ok {
|
||||
feature, ok := f.(bool)
|
||||
if ok && feature {
|
||||
trainer := vectorTrainer{
|
||||
parent: s,
|
||||
config: maps.Clone(s.segmentConfig),
|
||||
trainCh: make(chan *trainRequest, 1),
|
||||
doneCh: make(chan struct{}),
|
||||
}
|
||||
// update the parent scorch config with the trainer's callback to fetch the trained index
|
||||
s.segmentConfig[index.TrainedIndexCallback] = index.TrainedIndexCallbackFn(trainer.getTrainedIndex)
|
||||
return &trainer
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func moveFile(sourcePath, destPath string) error {
|
||||
// rename is supposed to be atomic on the same filesystem
|
||||
err := os.Rename(sourcePath, destPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error renaming file: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *vectorTrainer) persistToBolt(trainReq *trainRequest) error {
|
||||
tx, err := t.parent.rootBolt.Begin(true)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error starting bolt transaction: %v", err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
snapshotsBucket, err := tx.CreateBucketIfNotExists(util.BoltSnapshotsBucket)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating snapshots bucket: %v", err)
|
||||
}
|
||||
|
||||
trainerBucket, err := snapshotsBucket.CreateBucketIfNotExists(util.BoltTrainerKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating trained index bucket: %v", err)
|
||||
}
|
||||
|
||||
segmentPath := t.parent.path + string(os.PathSeparator) + index.TrainedIndexFileName
|
||||
if _, err := os.Stat(segmentPath); !os.IsNotExist(err) {
|
||||
err = trainerBucket.Put(util.BoltPathKey, []byte(index.TrainedIndexFileName), nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error updating trained index bucket: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
t.trainingComplete.Store(trainReq.finalSample)
|
||||
err = trainerBucket.Put(util.BoltTrainCompleteKey, []byte(strconv.FormatBool(trainReq.finalSample)), nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error updating train complete key: %v", err)
|
||||
}
|
||||
|
||||
totSamples := atomic.AddUint64(&t.trainedSamples, uint64(trainReq.sampleSize))
|
||||
err = trainerBucket.Put(util.BoltTrainedSamplesKey, binary.LittleEndian.AppendUint64(nil, totSamples), nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error updating trained samples key: %v", err)
|
||||
}
|
||||
|
||||
err = tx.Commit()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error committing bolt transaction: %v", err)
|
||||
}
|
||||
|
||||
return t.parent.rootBolt.Sync()
|
||||
}
|
||||
|
||||
// trainLoop is not a routine that runs throughout the lifetime of the index.
|
||||
// Its purpose is only to train the vector index before data ingestion starts.
|
||||
// It is the sole consumer of trainCh and the sole owner of trainedIndex
|
||||
// mutations originating from training.
|
||||
func (t *vectorTrainer) trainLoop() {
|
||||
defer t.parent.asyncTasks.Done()
|
||||
// signal any blocked producers that no further requests will be serviced.
|
||||
defer close(t.doneCh)
|
||||
|
||||
start := time.Now()
|
||||
path := filepath.Join(t.parent.path, index.TrainedIndexFileName)
|
||||
// config is owned exclusively by this goroutine. t.config is read
|
||||
// concurrently by train(), so it must not be mutated here.
|
||||
config := maps.Clone(t.config)
|
||||
|
||||
for {
|
||||
// exit once the final sample set has been ingested and training is complete.
|
||||
if t.trainingComplete.Load() {
|
||||
t.recordTrainingStats(start)
|
||||
return
|
||||
}
|
||||
select {
|
||||
case <-t.parent.closeCh:
|
||||
// the deferred close(doneCh) releases any producer blocked on
|
||||
// trainCh or its ackCh, so there is no need to drain here.
|
||||
return
|
||||
case req := <-t.trainCh:
|
||||
shutdown, err := t.handleRequest(req, path, config)
|
||||
ackRequest(req, err)
|
||||
if shutdown {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// recordTrainingStats publishes the final training counters once the loop exits.
|
||||
func (t *vectorTrainer) recordTrainingStats(start time.Time) {
|
||||
atomic.StoreUint64(&t.parent.stats.TotTrainedSamples, atomic.LoadUint64(&t.trainedSamples))
|
||||
atomic.StoreUint64(&t.parent.stats.TotTrainTime, uint64(time.Since(start).Milliseconds()))
|
||||
}
|
||||
|
||||
// ackRequest delivers a request's result to the producer waiting in submit and
|
||||
// then releases it. A non-nil err is sent before the close so submit observes
|
||||
// it; the send blocks until submit receives, which the shutdown handshake in
|
||||
// submit relies on.
|
||||
func ackRequest(req *trainRequest, err error) {
|
||||
if err != nil {
|
||||
req.ackCh <- err
|
||||
}
|
||||
close(req.ackCh)
|
||||
}
|
||||
|
||||
// handleRequest processes a single training request. The returned err, if
|
||||
// non-nil, is already formatted for the producer. shutdown reports whether the
|
||||
// trainLoop must exit: every failure is fatal except a failed initial persist,
|
||||
// which leaves the trainer able to retry on the next sample.
|
||||
func (t *vectorTrainer) handleRequest(req *trainRequest, path string, config map[string]interface{}) (shutdown bool, err error) {
|
||||
if req.trainingParams != nil {
|
||||
// mutate the goroutine-local config only; t.config is read concurrently
|
||||
// by train() and must stay immutable after init.
|
||||
config[index.TrainingKey] = req.trainingParams
|
||||
}
|
||||
|
||||
// remove any file writer ids that are no longer in use
|
||||
if req.removedFileWriterIDs != nil {
|
||||
if err := t.removeFileWriterIDs(req.removedFileWriterIDs); err != nil {
|
||||
return true, fmt.Errorf("error removing file writer ids: %v", err)
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// no sample segment: just persist state if this is the final sample.
|
||||
if req.sample == nil {
|
||||
if req.finalSample {
|
||||
if err := t.persistToBolt(req); err != nil {
|
||||
return true, fmt.Errorf("error persisting to bolt: %v", err)
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// snapshot the current trained segment under the read lock; MergeUsing below
|
||||
// only reads it, so concurrent getTrainedIndex readers stay safe.
|
||||
t.m.RLock()
|
||||
prev := t.trainedIndex
|
||||
t.m.RUnlock()
|
||||
|
||||
if prev == nil {
|
||||
if seg, ok := req.sample.(segment.UnpersistedSegment); ok {
|
||||
if err := persistToDirectory(seg, nil, path); err != nil {
|
||||
// recoverable: retry on the next sample rather than tear down.
|
||||
return false, fmt.Errorf("error persisting segment: %v", err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// merge the new sample with the existing index into a .tmp file; it is
|
||||
// renamed into place by publishTrainedIndex (Os.Open on the live path is
|
||||
// unsafe during the merge).
|
||||
_, _, mErr := t.parent.segPlugin.MergeUsing([]segment.Segment{prev.segment, req.sample},
|
||||
[]*roaring.Bitmap{nil, nil}, path+".tmp", t.parent.closeCh, nil, config)
|
||||
if mErr != nil {
|
||||
return true, fmt.Errorf("error merging trained index: %v", mErr)
|
||||
}
|
||||
}
|
||||
|
||||
if err := t.publishTrainedIndex(req, prev, path, config); err != nil {
|
||||
return true, err
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// publishTrainedIndex makes the freshly written index visible to readers. The
|
||||
// old segment handle must be closed before moveFile can replace the backing
|
||||
// file, so the close -> rename -> open -> pointer-swap sequence runs under the
|
||||
// write lock; otherwise a reader holding RLock could observe a closed segment
|
||||
// or a missing file. On any error past the close, the pointer is reset to nil
|
||||
// so readers never see the freed prev segment (callers fall back to naive
|
||||
// merge). The bolt write acts as a failover-recovery checkpoint: callers
|
||||
// downstream can rely on the trained index being available once this completes.
|
||||
// todo: rethink the frequency of bolt writes
|
||||
func (t *vectorTrainer) publishTrainedIndex(req *trainRequest, prev *SegmentSnapshot, path string, config map[string]interface{}) error {
|
||||
t.m.Lock()
|
||||
defer t.m.Unlock()
|
||||
|
||||
if prev != nil {
|
||||
if err := prev.segment.Close(); err != nil {
|
||||
t.trainedIndex = nil
|
||||
return fmt.Errorf("error closing previous trained index: %v", err)
|
||||
}
|
||||
if err := moveFile(path+".tmp", path); err != nil {
|
||||
t.trainedIndex = nil
|
||||
return fmt.Errorf("error renaming trained index: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := t.persistToBolt(req); err != nil {
|
||||
if prev != nil {
|
||||
t.trainedIndex = nil
|
||||
}
|
||||
return fmt.Errorf("error persisting to bolt: %v", err)
|
||||
}
|
||||
|
||||
trainedIndex, err := t.parent.segPlugin.OpenUsing(path, config)
|
||||
if err != nil {
|
||||
t.trainedIndex = nil
|
||||
return fmt.Errorf("error opening trained index: %v", err)
|
||||
}
|
||||
t.trainedIndex = &SegmentSnapshot{segment: trainedIndex}
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadTrainedData loads the trained-index metadata from boltdb during init. The
|
||||
// trainedIndex write is guarded because callback-driven readers may already be
|
||||
// registered by the time this runs.
|
||||
func (t *vectorTrainer) loadTrainedData(bucket *util.BoltBucketImpl) error {
|
||||
if bucket == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
reader, err := util.NewFileReader("", nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating file reader: %v", err)
|
||||
}
|
||||
|
||||
// segmentSnapshot is constructed only if the trained index file path exists. If
|
||||
// application layer fails to create one and the underlying path doesn't exist, we'll
|
||||
// just skip loading the trained index and fallback to naive merge
|
||||
var segmentSnapshot *SegmentSnapshot
|
||||
pathBytes, err := bucket.Get(util.BoltPathKey, nil)
|
||||
if pathBytes != nil {
|
||||
segmentSnapshot, err = t.parent.loadSegment(bucket, reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// get the training status out of bolt
|
||||
trainComplete, err := bucket.Get(util.BoltTrainCompleteKey, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting train complete: %v", err)
|
||||
}
|
||||
trainedSamples, err := bucket.Get(util.BoltTrainedSamplesKey, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting trained samples: %v", err)
|
||||
}
|
||||
if len(trainedSamples) == 8 {
|
||||
atomic.StoreUint64(&t.trainedSamples, binary.LittleEndian.Uint64(trainedSamples))
|
||||
}
|
||||
if len(trainComplete) > 0 {
|
||||
comp, err := strconv.ParseBool(string(trainComplete))
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing train complete: %v", err)
|
||||
}
|
||||
t.trainingComplete.Store(comp)
|
||||
}
|
||||
|
||||
t.m.Lock()
|
||||
defer t.m.Unlock()
|
||||
t.trainedIndex = segmentSnapshot
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *vectorTrainer) train(batch *index.Batch) error {
|
||||
// regulate the Train function
|
||||
start := time.Now()
|
||||
t.parent.FireIndexEvent()
|
||||
atomic.AddUint64(&t.parent.stats.TotTrainFireIndexEventTime, uint64(time.Since(start)))
|
||||
atomic.AddUint64(&t.parent.stats.TotTrainFireIndexEvents, 1)
|
||||
if t.trainingComplete.Load() {
|
||||
return fmt.Errorf("training is already complete, cannot accept more training data")
|
||||
}
|
||||
|
||||
var trainData []index.Document
|
||||
for _, doc := range batch.IndexOps {
|
||||
if doc != nil {
|
||||
// insert _id field
|
||||
// no need to track updates/deletes over here since
|
||||
// the API is singleton
|
||||
doc.AddIDField()
|
||||
}
|
||||
trainData = append(trainData, doc)
|
||||
}
|
||||
|
||||
trainComplete := batch.InternalOps[string(util.BoltTrainCompleteKey)]
|
||||
if trainComplete == nil {
|
||||
trainComplete = []byte("false")
|
||||
}
|
||||
fin, err := strconv.ParseBool(string(trainComplete))
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing train complete: %v", err)
|
||||
}
|
||||
|
||||
trainReq := &trainRequest{
|
||||
finalSample: fin,
|
||||
sampleSize: len(trainData),
|
||||
ackCh: make(chan error),
|
||||
}
|
||||
// setting the training params using the internal value before the actual
|
||||
// training has started
|
||||
config := t.config
|
||||
if atomic.LoadUint64(&t.trainedSamples) == 0 {
|
||||
trainingParamsBytes := batch.InternalOps[index.TrainingKey]
|
||||
var trainingParams index.TrainingParams
|
||||
if trainingParamsBytes != nil {
|
||||
err = util.UnmarshalJSON(trainingParamsBytes, &trainingParams)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing training params: %v", err)
|
||||
}
|
||||
trainReq.trainingParams = &trainingParams
|
||||
config = maps.Clone(t.config)
|
||||
config[index.TrainingKey] = &trainingParams
|
||||
}
|
||||
}
|
||||
|
||||
// just builds a new vector index out of the train data provided
|
||||
// this is not necessarily the final train data since this is submitted
|
||||
// as a request to the trainer component to be merged. once the training
|
||||
// is complete, the template will be used for other operations down the line
|
||||
// like merge and search.
|
||||
//
|
||||
// todo: updates/deletes -> data drift detection
|
||||
if len(trainData) > 0 {
|
||||
trainReq.sample, _, err = t.parent.segPlugin.NewUsing(trainData, config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := t.submit(trainReq); err != nil {
|
||||
return fmt.Errorf("train_vector: train() err'd out with: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// submit hands a request to the trainLoop and waits for its ack. It never
|
||||
// blocks past trainer shutdown: if the trainLoop has already exited (via close,
|
||||
// training completion, or error) the doneCh releases the caller instead of
|
||||
// letting it hang on a request that will never be consumed or acked.
|
||||
func (t *vectorTrainer) submit(req *trainRequest) error {
|
||||
select {
|
||||
case t.trainCh <- req:
|
||||
case <-t.doneCh:
|
||||
return fmt.Errorf("trainer is closed")
|
||||
}
|
||||
|
||||
select {
|
||||
case err := <-req.ackCh:
|
||||
return err
|
||||
case <-t.doneCh:
|
||||
// trainLoop acks every request it consumes before returning, so if our
|
||||
// request was processed the ack is already available — prefer it over
|
||||
// reporting a shutdown that didn't actually drop this request.
|
||||
select {
|
||||
case err := <-req.ackCh:
|
||||
return err
|
||||
default:
|
||||
return fmt.Errorf("trainer is closed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *vectorTrainer) getInternal(key []byte) ([]byte, error) {
|
||||
switch string(key) {
|
||||
case string(util.BoltTrainCompleteKey):
|
||||
return []byte(strconv.FormatBool(t.trainingComplete.Load())), nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (t *vectorTrainer) getTrainedIndex(field string) (interface{}, error) {
|
||||
// return the coarse quantizer of the trained faiss index belonging to the field
|
||||
// if its not available then zap performs naive merge
|
||||
t.m.RLock()
|
||||
defer t.m.RUnlock()
|
||||
if t.trainedIndex != nil {
|
||||
trainedSegment, ok := t.trainedIndex.segment.(segment.TrainedSegment)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("segment is not a trained index segment")
|
||||
}
|
||||
|
||||
coarseQuantizer, err := trainedSegment.GetCoarseQuantizer(field)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return coarseQuantizer, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (t *vectorTrainer) copyFileLOCKED(file string, d index.IndexDirectory) error {
|
||||
if strings.HasSuffix(file, index.TrainedIndexFileName) {
|
||||
// trained index file - this is outside the snapshots domain so the bolt update is different
|
||||
err := d.SetPathInBolt(util.BoltTrainerKey, []byte(file))
|
||||
if err != nil {
|
||||
return fmt.Errorf("error updating dest index bolt: %w", err)
|
||||
}
|
||||
|
||||
// update the stats in destination
|
||||
err = d.SetPathInBolt(util.BoltTrainedSamplesKey, binary.LittleEndian.AppendUint64(nil, atomic.LoadUint64(&t.trainedSamples)))
|
||||
if err != nil {
|
||||
return fmt.Errorf("error updating trained samples key: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *vectorTrainer) updateBolt(snapshotsBucket *util.BoltBucketImpl, key []byte, value []byte) error {
|
||||
switch string(key) {
|
||||
case string(util.BoltTrainerKey):
|
||||
trainerBucket, err := snapshotsBucket.CreateBucketIfNotExists(util.BoltTrainerKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if trainerBucket == nil {
|
||||
return fmt.Errorf("trainer bucket not found")
|
||||
}
|
||||
|
||||
// guard against duplicate updates
|
||||
existingValue, err := trainerBucket.Get(util.BoltPathKey, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error checking existing value: %v", err)
|
||||
}
|
||||
if existingValue != nil {
|
||||
return fmt.Errorf("key already exists %v %v", t.parent.path, string(existingValue))
|
||||
}
|
||||
|
||||
err = trainerBucket.Put(util.BoltPathKey, value, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
reader, err := util.NewFileReader("", nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating file reader: %v", err)
|
||||
}
|
||||
|
||||
// update the centroid index pointer under the write lock; getTrainedIndex
|
||||
// and trainLoop may read t.trainedIndex concurrently.
|
||||
seg, err := t.parent.loadSegment(trainerBucket, reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.m.Lock()
|
||||
t.trainedIndex = seg
|
||||
t.m.Unlock()
|
||||
|
||||
case string(util.BoltTrainedSamplesKey):
|
||||
trainedSamples := binary.LittleEndian.Uint64(value)
|
||||
atomic.StoreUint64(&t.trainedSamples, trainedSamples)
|
||||
trainerBucket, err := snapshotsBucket.CreateBucketIfNotExists(util.BoltTrainerKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = trainerBucket.Put(util.BoltTrainedSamplesKey, value, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// dropFileWriterIDs removes the given file writer ids from the trained index.
|
||||
// This is a lifecycle operation (key rotation) that must work for the entire
|
||||
// life of the trained index, not just while training is running.
|
||||
func (t *vectorTrainer) dropFileWriterIDs(ids map[string]struct{}) error {
|
||||
if t.trainingComplete.Load() {
|
||||
if err := t.removeFileWriterIDs(ids); err != nil {
|
||||
return fmt.Errorf("train_vector: dropFileWriterIDs() err'd out with: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
trainReq := &trainRequest{
|
||||
ackCh: make(chan error),
|
||||
removedFileWriterIDs: ids,
|
||||
}
|
||||
if err := t.submit(trainReq); err != nil {
|
||||
return fmt.Errorf("train_vector: dropFileWriterIDs() err'd out with: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *vectorTrainer) removeFileWriterIDs(ids map[string]struct{}) error {
|
||||
// trainer bucket in the bolt file doesn't have any sensitive data to be encrypted,
|
||||
// so we don't have to update it with new writer ids.
|
||||
// invoke the merge API and let the zap side of things handle the update of writer IDs and also
|
||||
// the data in the trained index.
|
||||
t.m.Lock()
|
||||
defer t.m.Unlock()
|
||||
trainedIndexPath := filepath.Join(t.parent.path, index.TrainedIndexFileName)
|
||||
if t.trainedIndex != nil {
|
||||
if encryptedIndex, ok := t.trainedIndex.segment.(segment.SegmentWithCallbacks); ok {
|
||||
if _, ok := ids[encryptedIndex.CallbackId()]; ok {
|
||||
_, _, err := t.parent.segPlugin.MergeUsing([]segment.Segment{t.trainedIndex.segment},
|
||||
[]*roaring.Bitmap{nil}, trainedIndexPath+"temp", t.parent.closeCh, nil, t.config)
|
||||
if err != nil {
|
||||
if serr := os.Remove(trainedIndexPath + "temp"); serr != nil {
|
||||
return fmt.Errorf("error removing temp trained index: %v", serr)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
err = t.trainedIndex.segment.Close()
|
||||
if err != nil {
|
||||
if serr := os.Remove(trainedIndexPath + "temp"); serr != nil {
|
||||
return fmt.Errorf("error removing temp trained index: %v", serr)
|
||||
}
|
||||
t.trainedIndex = nil
|
||||
return err
|
||||
}
|
||||
|
||||
if err := moveFile(trainedIndexPath+"temp", trainedIndexPath); err != nil {
|
||||
if serr := os.Remove(trainedIndexPath + "temp"); serr != nil {
|
||||
return fmt.Errorf("error removing temp trained index: %v", serr)
|
||||
}
|
||||
t.trainedIndex = nil
|
||||
return err
|
||||
}
|
||||
|
||||
trainedIndex, err := t.parent.segPlugin.OpenUsing(trainedIndexPath, t.config)
|
||||
if err != nil {
|
||||
t.trainedIndex = nil
|
||||
return err
|
||||
}
|
||||
|
||||
t.trainedIndex = &SegmentSnapshot{segment: trainedIndex}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *vectorTrainer) fileWriterIDsInUse() (map[string]struct{}, error) {
|
||||
t.m.RLock()
|
||||
defer t.m.RUnlock()
|
||||
writerIDs := make(map[string]struct{})
|
||||
if t.trainedIndex != nil {
|
||||
if seg, ok := t.trainedIndex.segment.(segment.SegmentWithCallbacks); ok {
|
||||
writerIDs[seg.CallbackId()] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
return writerIDs, nil
|
||||
}
|
||||
Reference in New Issue
Block a user