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

This commit is contained in:
upstream-mirror
2026-09-15 22:27:16 +05:00
commit f8d3d9f0cf
797 changed files with 159570 additions and 0 deletions

View File

@@ -0,0 +1,849 @@
// 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 (
"fmt"
"io"
"os"
"path/filepath"
"strings"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/blevesearch/bleve/v2/document"
"github.com/blevesearch/bleve/v2/util"
index "github.com/blevesearch/bleve_index_api"
)
// newMinimalScorchForTrainer returns a *Scorch with just the fields required by
// vectorTrainer tests that do not touch BoltDB or the segment plugin.
func newMinimalScorchForTrainer(t *testing.T) *Scorch {
t.Helper()
return &Scorch{
closeCh: make(chan struct{}),
segmentConfig: make(map[string]interface{}),
config: make(map[string]interface{}),
path: t.TempDir(),
}
}
// waitGroupDone blocks until wg.Wait() returns or d elapses, returning true
// iff the wait completed in time.
func waitGroupDone(wg *sync.WaitGroup, d time.Duration) bool {
done := make(chan struct{})
go func() { wg.Wait(); close(done) }()
select {
case <-done:
return true
case <-time.After(d):
return false
}
}
// -----------------------------------------------------------------------
// initTrainer
// -----------------------------------------------------------------------
func TestInitTrainerFlagAbsent(t *testing.T) {
s := newMinimalScorchForTrainer(t)
if tr := initTrainer(s, s.config); tr != nil {
t.Fatal("expected nil trainer when flag is absent, got non-nil")
}
}
func TestInitTrainerFlagFalse(t *testing.T) {
s := newMinimalScorchForTrainer(t)
cfg := map[string]interface{}{IndexTrainedWithFastMerge: false}
if tr := initTrainer(s, cfg); tr != nil {
t.Fatal("expected nil trainer when flag is false, got non-nil")
}
}
func TestInitTrainerReturnsTrainer(t *testing.T) {
s := newMinimalScorchForTrainer(t)
cfg := map[string]interface{}{IndexTrainedWithFastMerge: true}
if tr := initTrainer(s, cfg); tr == nil {
t.Fatal("expected non-nil trainer when flag is true")
}
}
func TestInitTrainerClonesConfig(t *testing.T) {
s := newMinimalScorchForTrainer(t)
s.config["pre-existing"] = "value"
cfg := map[string]interface{}{IndexTrainedWithFastMerge: true}
tr := initTrainer(s, cfg)
if tr == nil {
t.Fatal("expected non-nil trainer")
}
// Mutations to the trainer's config must not affect the parent's config.
tr.config["canary"] = true
if _, leaked := s.config["canary"]; leaked {
t.Error("trainer config is the same map as parent config — expected a clone")
}
}
// -----------------------------------------------------------------------
// getInternal
// -----------------------------------------------------------------------
func TestGetInternalDefaultFalse(t *testing.T) {
s := newMinimalScorchForTrainer(t)
tr := &vectorTrainer{parent: s, config: map[string]interface{}{}}
got, err := tr.getInternal(util.BoltTrainCompleteKey)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if string(got) != "false" {
t.Errorf("expected 'false', got %q", got)
}
}
func TestGetInternalTrueAfterSet(t *testing.T) {
s := newMinimalScorchForTrainer(t)
tr := &vectorTrainer{parent: s, config: map[string]interface{}{}}
tr.trainingComplete.Store(true)
got, err := tr.getInternal(util.BoltTrainCompleteKey)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if string(got) != "true" {
t.Errorf("expected 'true', got %q", got)
}
}
func TestGetInternalUnknownKey(t *testing.T) {
s := newMinimalScorchForTrainer(t)
tr := &vectorTrainer{parent: s, config: map[string]interface{}{}}
got, err := tr.getInternal([]byte("no-such-key"))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got != nil {
t.Errorf("expected nil for unknown key, got %v", got)
}
}
// -----------------------------------------------------------------------
// getTrainedIndex
// -----------------------------------------------------------------------
func TestGetTrainedIndexNil(t *testing.T) {
s := newMinimalScorchForTrainer(t)
tr := &vectorTrainer{parent: s, config: map[string]interface{}{}}
got, err := tr.getTrainedIndex("vec")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got != nil {
t.Errorf("expected nil, got %v", got)
}
}
func TestGetTrainedIndexNotTrainedSegment(t *testing.T) {
s := newMinimalScorchForTrainer(t)
tr := &vectorTrainer{
parent: s,
config: map[string]interface{}{},
// mockSegmentBase does NOT implement segment.TrainedSegment.
trainedIndex: &SegmentSnapshot{segment: &mockSegmentBase{}},
}
_, err := tr.getTrainedIndex("vec")
if err == nil {
t.Fatal("expected error for segment that does not implement TrainedSegment")
}
}
// mockTrainedSeg embeds mockSegmentBase and additionally satisfies
// segment.TrainedSegment by implementing GetCoarseQuantizer.
type mockTrainedSeg struct {
mockSegmentBase
coarseQuantizerFn func(string) (interface{}, error)
}
func (m *mockTrainedSeg) GetCoarseQuantizer(field string) (interface{}, error) {
if m.coarseQuantizerFn != nil {
return m.coarseQuantizerFn(field)
}
return nil, nil
}
func TestGetTrainedIndexReturnsCQ(t *testing.T) {
sentinel := struct{ label string }{"centroid"}
s := newMinimalScorchForTrainer(t)
tr := &vectorTrainer{
parent: s,
config: map[string]interface{}{},
trainedIndex: &SegmentSnapshot{
segment: &mockTrainedSeg{
coarseQuantizerFn: func(string) (interface{}, error) {
return sentinel, nil
},
},
},
}
got, err := tr.getTrainedIndex("vec")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got != sentinel {
t.Errorf("expected sentinel coarse quantizer, got %v", got)
}
}
func TestGetTrainedIndexPropagatesError(t *testing.T) {
s := newMinimalScorchForTrainer(t)
tr := &vectorTrainer{
parent: s,
config: map[string]interface{}{},
trainedIndex: &SegmentSnapshot{
segment: &mockTrainedSeg{
coarseQuantizerFn: func(string) (interface{}, error) {
return nil, fmt.Errorf("coarse quantizer unavailable")
},
},
},
}
_, err := tr.getTrainedIndex("vec")
if err == nil {
t.Fatal("expected error from GetCoarseQuantizer, got nil")
}
}
// -----------------------------------------------------------------------
// trainLoop — exit paths that do not require BoltDB
// -----------------------------------------------------------------------
// TestTrainLoopExitsWhenComplete verifies that the loop returns
// immediately (without touching any channel) when trainingComplete is already
// set, and that it flushes the sample count into parent stats.
func TestTrainLoopExitsWhenComplete(t *testing.T) {
s := newMinimalScorchForTrainer(t)
tr := &vectorTrainer{
parent: s,
config: map[string]interface{}{},
trainCh: make(chan *trainRequest, 1),
}
tr.trainedSamples = 42
tr.trainingComplete.Store(true)
s.asyncTasks.Add(1)
go tr.trainLoop()
if !waitGroupDone(&s.asyncTasks, 2*time.Second) {
t.Fatal("trainLoop did not exit within deadline")
}
if got := atomic.LoadUint64(&s.stats.TotTrainedSamples); got != 42 {
t.Errorf("expected TotTrainedSamples=42, got %d", got)
}
}
// TestTrainLoopCloseChNoRequest verifies that closing the
// parent's closeCh causes the loop to exit cleanly when there is no buffered
// train request.
func TestTrainLoopCloseChNoRequest(t *testing.T) {
s := newMinimalScorchForTrainer(t)
tr := &vectorTrainer{
parent: s,
config: map[string]interface{}{},
trainCh: make(chan *trainRequest, 1),
}
s.asyncTasks.Add(1)
go tr.trainLoop()
close(s.closeCh)
if !waitGroupDone(&s.asyncTasks, 2*time.Second) {
t.Fatal("trainLoop did not exit within deadline")
}
}
// TestTrainLoopCloseChAcksRequest verifies that the goroutine
// exits after closeCh is closed, and that any ackCh buffered in trainCh is
// handled before the goroutine returns (no leaked ackCh).
//
// Due to Go's non-deterministic select, the loop may either:
//
// (a) react to closeCh first → drain the request and ack it with an error, OR
// (b) consume the request first (nil-sample, non-final → close ackCh) and
// then exit via closeCh on the next iteration.
//
// Either outcome is correct. The invariant under test is that ackCh is
// drained/closed before the goroutine exits, preventing a caller goroutine leak.
func TestTrainLoopCloseChAcksRequest(t *testing.T) {
s := newMinimalScorchForTrainer(t)
tr := &vectorTrainer{
parent: s,
config: map[string]interface{}{},
trainCh: make(chan *trainRequest, 1),
}
// Buffered so that the loop's non-blocking write (if it takes closeCh path)
// does not deadlock.
ackCh := make(chan error, 1)
tr.trainCh <- &trainRequest{finalSample: false, sample: nil, ackCh: ackCh}
s.asyncTasks.Add(1)
go tr.trainLoop()
close(s.closeCh)
if !waitGroupDone(&s.asyncTasks, 2*time.Second) {
t.Fatal("trainLoop did not exit within deadline")
}
// ackCh must be drainable without blocking regardless of which select path
// was taken.
select {
case <-ackCh:
// received error (closeCh drain path) or nil (closed by normal processing)
case <-time.After(100 * time.Millisecond):
t.Fatal("ackCh was not handled after trainLoop exited — possible goroutine leak")
}
}
// TestTrainLoopNilSampleAcks verifies that a nil-sample,
// non-final request is acknowledged without error, and that the loop continues
// running afterwards (cleaned up by closing closeCh).
func TestTrainLoopNilSampleAcks(t *testing.T) {
s := newMinimalScorchForTrainer(t)
tr := &vectorTrainer{
parent: s,
config: map[string]interface{}{},
trainCh: make(chan *trainRequest, 1),
}
ackCh := make(chan error, 1)
tr.trainCh <- &trainRequest{finalSample: false, sample: nil, ackCh: ackCh}
s.asyncTasks.Add(1)
go tr.trainLoop()
// The loop should close ackCh with no value written to it.
select {
case err := <-ackCh:
if err != nil {
t.Errorf("expected nil error for nil-sample non-final request, got: %v", err)
}
case <-time.After(2 * time.Second):
t.Fatal("timed out waiting for ackCh to be closed")
}
// Stop the loop cleanly.
close(s.closeCh)
if !waitGroupDone(&s.asyncTasks, 2*time.Second) {
t.Fatal("trainLoop did not exit within deadline after closeCh")
}
}
// mockUnpersistedSeg satisfies both segment.Segment (via mockSegmentBase) and
// segment.UnpersistedSegment. persistFn controls the return value of Persist.
type mockUnpersistedSeg struct {
mockSegmentBase
persistFn func(path string) error
}
func (m *mockUnpersistedSeg) Persist(path string) error {
if m.persistFn != nil {
return m.persistFn(path)
}
return nil
}
// TestTrainLoopPersistErrorAcks verifies that
// when persist-to-directory fails for a non-nil UnpersistedSegment, the loop:
// 1. sends an error on ackCh and closes it, and
// 2. continues running (does not return), so a subsequent closeCh can stop it.
func TestTrainLoopPersistErrorAcks(t *testing.T) {
s := newMinimalScorchForTrainer(t)
tr := &vectorTrainer{
parent: s,
config: map[string]interface{}{},
trainCh: make(chan *trainRequest, 1),
}
ackCh := make(chan error, 1)
tr.trainCh <- &trainRequest{
finalSample: false,
sample: &mockUnpersistedSeg{
persistFn: func(string) error { return fmt.Errorf("disk full") },
},
ackCh: ackCh,
}
s.asyncTasks.Add(1)
go tr.trainLoop()
// The loop should write an error and close ackCh before continuing.
select {
case err, ok := <-ackCh:
if !ok {
t.Fatal("ackCh was closed before the error value could be read")
}
if err == nil {
t.Fatal("expected non-nil error from failed persist, got nil")
}
case <-time.After(2 * time.Second):
t.Fatal("timed out waiting for error on ackCh")
}
// Loop must still be running; stop it cleanly.
close(s.closeCh)
if !waitGroupDone(&s.asyncTasks, 2*time.Second) {
t.Fatal("trainLoop did not exit within deadline after closeCh")
}
}
func TestTrainerSampleIngestion(t *testing.T) {
cfg := CreateConfig("TestTrainerSampleIngestion")
cfg[IndexTrainedWithFastMerge] = true
err := InitTest(cfg)
if err != nil {
t.Fatal(err)
}
analysisQueue := index.NewAnalysisQueue(1)
s, err := NewScorch(Name, cfg, analysisQueue)
if err != nil {
t.Fatalf("failed to create Scorch: %v", err)
}
defer s.Close()
sc, ok := s.(*Scorch)
if !ok {
t.Fatal("expected Scorch instance")
}
batch := index.NewBatch()
for i := 0; i < 100; i++ {
doc := document.NewDocument(fmt.Sprintf("doc-%d", i))
stubVal := float32(i)
doc.AddField(document.NewVectorField("vec", nil, []float32{stubVal, stubVal + 1, stubVal + 2}, 3, "cosine", index.IndexOptimizedForRecall))
batch.Update(doc)
}
tr, ok := sc.trainer.(*vectorTrainer)
if !ok {
t.Fatal("expected vectorTrainer instance")
}
if sc.rootBolt == nil {
err := sc.openBolt()
if err != nil {
t.Fatalf("opening bolt failed %v", err)
}
}
sc.asyncTasks.Add(1)
go tr.trainLoop()
err = sc.Train(batch)
if err != nil {
t.Fatalf("training failed: %v", err)
}
val, err := tr.getInternal(util.BoltTrainCompleteKey)
if err != nil {
t.Fatalf("failed to get internal value: %v", err)
}
if string(val) != "false" {
t.Errorf("expected 'false' for training complete key, got %q", val)
}
if tr.trainedSamples != 100 {
t.Errorf("expected 100 trained samples, got %d", tr.trainedSamples)
}
b := index.NewBatch()
doc := document.NewDocument("doc-998")
doc.AddField(document.NewVectorField("vec", nil, []float32{998, 998, 998}, 3, "cosine", index.IndexOptimizedForRecall))
b.Update(doc)
err = sc.Train(b)
if err != nil {
t.Fatalf("training failed: %v", err)
}
val, err = tr.getInternal(util.BoltTrainCompleteKey)
if err != nil {
t.Fatalf("failed to get internal value: %v", err)
}
if string(val) != "false" {
t.Errorf("expected 'false' for training complete key, got %q", val)
}
if tr.trainedSamples != 101 {
t.Errorf("expected 101 trained samples, got %d", tr.trainedSamples)
}
b.Reset()
b.SetInternal(util.BoltTrainCompleteKey, []byte("true"))
sc.Train(b)
if err != nil {
t.Fatalf("training failed: %v", err)
}
val, err = tr.getInternal(util.BoltTrainCompleteKey)
if err != nil {
t.Fatalf("failed to get internal value: %v", err)
}
if string(val) != "true" {
t.Errorf("expected 'true' for training complete key, got %q", val)
}
if tr.trainedSamples != 101 {
t.Errorf("expected 101 trained samples (no increment after training complete), got %d", tr.trainedSamples)
}
}
func TestTrainerBlocksDataIngestion(t *testing.T) {
cfg := CreateConfig("TestTrainerBlocksDataIngestion")
cfg[IndexTrainedWithFastMerge] = true
err := InitTest(cfg)
if err != nil {
t.Fatal(err)
}
analysisQueue := index.NewAnalysisQueue(1)
s, err := NewScorch(Name, cfg, analysisQueue)
if err != nil {
t.Fatalf("failed to create Scorch: %v", err)
}
defer s.Close()
sc, ok := s.(*Scorch)
if !ok {
t.Fatal("expected Scorch instance")
}
batch := index.NewBatch()
for i := 0; i < 100; i++ {
doc := document.NewDocument(fmt.Sprintf("doc-%d", i))
stubVal := float32(i)
doc.AddField(document.NewVectorField("vec", nil, []float32{stubVal, stubVal + 1, stubVal + 2}, 3, "cosine", index.IndexOptimizedForRecall))
batch.Update(doc)
}
tr, ok := sc.trainer.(*vectorTrainer)
if !ok {
t.Fatal("expected vectorTrainer instance")
}
if sc.rootBolt == nil {
err := sc.openBolt()
if err != nil {
t.Fatalf("opening bolt failed %v", err)
}
}
// spawn all the routines to check the data ingest as well
err = sc.Open()
if err != nil {
t.Fatalf("failed to open Scorch: %v", err)
}
err = sc.Train(batch)
if err != nil {
t.Fatalf("training failed: %v", err)
}
val, err := tr.getInternal(util.BoltTrainCompleteKey)
if err != nil {
t.Fatalf("failed to get internal value: %v", err)
}
if string(val) != "false" {
t.Errorf("expected 'false' for training complete key, got %q", val)
}
if tr.trainedSamples != 100 {
t.Errorf("expected 100 trained samples, got %d", tr.trainedSamples)
}
batch.Reset()
doc := document.NewDocument("doc-998")
doc.AddField(document.NewVectorField("vec", nil, []float32{998, 998, 998}, 3, "cosine", index.IndexOptimizedForRecall))
batch.Update(doc)
err = sc.Batch(batch)
if err == nil {
t.Fatalf("data ingestion should be blocked during training")
}
batch.Reset()
batch.SetInternal(util.BoltTrainCompleteKey, []byte("true"))
err = sc.Train(batch)
if err != nil {
t.Fatalf("training failed: %v", err)
}
val, err = tr.getInternal(util.BoltTrainCompleteKey)
if err != nil {
t.Fatalf("failed to get internal value: %v", err)
}
if string(val) != "true" {
t.Errorf("expected 'true' for training complete key, got %q", val)
}
batch.Reset()
doc = document.NewDocument("doc-998")
doc.AddField(document.NewVectorField("vec", nil, []float32{998, 998, 998}, 3, "cosine", index.IndexOptimizedForRecall))
batch.Update(doc)
err = sc.Batch(batch)
if err != nil {
t.Fatalf("data ingestion expected to suceed after training is complete")
}
err = sc.Train(batch)
if err == nil {
t.Fatalf("re-training is not allowed on the index")
}
}
type dummyFileCopyAPI interface {
CopyFile(fileName string, dst index.IndexDirectory) error
SetPathInBolt(key []byte, value []byte) error
}
// trainedIndexWriter implements the writer used when copying the trained index
// file from the source partition into this partition's index directory.
type trainedIndexWriter struct {
rootpath string
destIndex dummyFileCopyAPI
}
func (c *trainedIndexWriter) GetWriter(path string) (io.WriteCloser, error) {
if !(strings.HasSuffix(path, index.TrainedIndexFileName)) {
return nil, fmt.Errorf("write not allowed on path %s", path)
}
return os.OpenFile(filepath.Join(c.rootpath, filepath.Base(path)), os.O_CREATE|os.O_WRONLY, 0600)
}
func (c *trainedIndexWriter) SetPathInBolt(key []byte, value []byte) error {
return c.destIndex.SetPathInBolt(key, value)
}
func TestTrainerIndexCopy(t *testing.T) {
cfg := CreateConfig("TestTrainerIndexCopy")
cfg[IndexTrainedWithFastMerge] = true
err := InitTest(cfg)
if err != nil {
t.Fatal(err)
}
cfg["path"] = filepath.Clean(cfg["path"].(string))
analysisQueue := index.NewAnalysisQueue(1)
s, err := NewScorch(Name, cfg, analysisQueue)
if err != nil {
t.Fatalf("failed to create Scorch: %v", err)
}
defer s.Close()
sc, ok := s.(*Scorch)
if !ok {
t.Fatal("expected Scorch instance")
}
batch := index.NewBatch()
for i := 0; i < 100; i++ {
doc := document.NewDocument(fmt.Sprintf("doc-%d", i))
stubVal := float32(i)
doc.AddField(document.NewVectorField("vec", nil, []float32{stubVal, stubVal + 1, stubVal + 2}, 3, "cosine", index.IndexOptimizedForRecall))
batch.Update(doc)
}
tr, ok := sc.trainer.(*vectorTrainer)
if !ok {
t.Fatal("expected vectorTrainer instance")
}
if sc.rootBolt == nil {
err := sc.openBolt()
if err != nil {
t.Fatalf("opening bolt failed %v", err)
}
}
sc.asyncTasks.Add(1)
go tr.trainLoop()
err = sc.Train(batch)
if err != nil {
t.Fatalf("training failed: %v", err)
}
val, err := tr.getInternal(util.BoltTrainCompleteKey)
if err != nil {
t.Fatalf("failed to get internal value: %v", err)
}
if string(val) != "false" {
t.Errorf("expected 'false' for training complete key, got %q", val)
}
// -----------------------------------------------------------------------
// At this point the source trainer has been trained with 100 samples, and the
// trained index file should be available on disk. Now we will create a new
// trainer in a new Scorch instance, and copy the trained index file from the
// source trainer into the destination trainer's index directory, simulating
// the file copy between partitions to avoid re-training.
// -----------------------------------------------------------------------
dstCfg := CreateConfig("TestTrainerIndexCopyDest")
dstCfg[IndexTrainedWithFastMerge] = true
err = InitTest(dstCfg)
if err != nil {
t.Fatal(err)
}
dstCfg["path"] = filepath.Clean(dstCfg["path"].(string))
dstWriter := &trainedIndexWriter{
rootpath: dstCfg["path"].(string),
}
dstScorch, err := NewScorch(Name, dstCfg, analysisQueue)
if err != nil {
t.Fatalf("failed to create new Scorch: %v", err)
}
defer dstScorch.Close()
dst, ok := dstScorch.(*Scorch)
if !ok {
t.Fatal("expected Scorch instance")
}
dstWriter.destIndex = dstScorch.(dummyFileCopyAPI)
dstTrainer, ok := dst.trainer.(*vectorTrainer)
if !ok {
t.Fatal("expected vectorTrainer instance")
}
if dst.rootBolt == nil {
err := dst.openBolt()
if err != nil {
t.Fatalf("opening bolt failed %v", err)
}
}
dst.asyncTasks.Add(1)
go dstTrainer.trainLoop()
src, ok := s.(dummyFileCopyAPI)
if !ok {
t.Fatal("expected dummyFileCopyAPI instance")
}
err = src.CopyFile(index.TrainedIndexFileName, dstWriter)
if err != nil {
t.Fatalf("CopyFile failed: %v", err)
}
val, err = dstTrainer.getInternal(util.BoltTrainCompleteKey)
if err != nil {
t.Fatalf("failed to get internal value: %v", err)
}
if string(val) != "false" {
t.Errorf("expected 'false' for training complete key in destination trainer, got %q", val)
}
if trainedIndex, err := dstTrainer.getTrainedIndex("vec"); err != nil || trainedIndex == nil {
t.Errorf("expected trained index in destination trainer, got nil")
}
}
func TestTrainerFallbackNaiveMerge(t *testing.T) {
cfg := CreateConfig("TestTrainerFallbackNaiveMerge")
cfg[IndexTrainedWithFastMerge] = true
err := InitTest(cfg)
if err != nil {
t.Fatal(err)
}
cfg["path"] = filepath.Clean(cfg["path"].(string))
analysisQueue := index.NewAnalysisQueue(1)
s, err := NewScorch(Name, cfg, analysisQueue)
if err != nil {
t.Fatalf("failed to create Scorch: %v", err)
}
sc, ok := s.(*Scorch)
if !ok {
t.Fatal("expected Scorch instance")
}
tr, ok := sc.trainer.(*vectorTrainer)
if !ok {
t.Fatal("expected vectorTrainer instance")
}
if sc.rootBolt == nil {
err := sc.openBolt()
if err != nil {
t.Fatalf("opening bolt failed %v", err)
}
}
// spawn all the routines to check the data ingest as well
err = sc.Open()
if err != nil {
t.Fatalf("failed to open Scorch: %v", err)
}
// mocking the scenario where the application would've failed to create the trained index file
// due to some fail of operation but expects the underlying the indexing layer
// to continue ingestion and fallback to naive merge and complete the training
// without the trained index file.
batch := index.NewBatch()
batch.SetInternal(util.BoltTrainCompleteKey, []byte("true"))
err = sc.Train(batch)
if err != nil {
t.Fatalf("training failed: %v", err)
}
batch.Reset()
doc := document.NewDocument("doc-998")
doc.AddField(document.NewVectorField("vec", nil, []float32{998, 998, 998}, 3, "cosine", index.IndexOptimizedForRecall))
batch.Update(doc)
err = sc.Batch(batch)
if err != nil {
t.Fatalf("data ingestion expected to suceed after training is complete")
}
val, err := tr.getInternal(util.BoltTrainCompleteKey)
if err != nil {
t.Fatalf("failed to get internal value: %v", err)
}
if string(val) != "true" {
t.Errorf("expected 'true' for training complete key, got %q", val)
}
s.Close()
// Re-open the index to verify that it can be loaded without the trained index file.
sn, err := NewScorch(Name, cfg, analysisQueue)
if err != nil {
t.Fatalf("failed to re-open Scorch: %v", err)
}
err = sn.Open()
if err != nil {
t.Fatalf("failed to open Scorch after fallback: %v", err)
}
defer sn.Close()
}