Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
32a9608
fastmerge wip
Thejas-bhat Jun 16, 2025
c89331e
use callbacks to collect and use train data while merging
Thejas-bhat Jun 17, 2025
77f147d
serialized float array
Thejas-bhat Jun 18, 2025
581ef8b
collect training sample on the file path as well
Thejas-bhat Jul 8, 2025
d2aebc1
cleanup debug logs
Thejas-bhat Aug 21, 2025
93b313c
vector sources API
Thejas-bhat Oct 28, 2025
1552829
batch training support
Thejas-bhat Nov 26, 2025
b08d0e1
wip: batch training + interfaces to reuse pre-trained file
Thejas-bhat Nov 26, 2025
a0ed1d9
bug fix, debug logging
Thejas-bhat Dec 11, 2025
d3d9fcb
wip: implement async trainer loop with incremental training support
Thejas-bhat Dec 15, 2025
2aeef1b
regulate train function using EventKindIndexStart
Thejas-bhat Dec 15, 2025
5cd09f7
incremental training bug fixes + better recoverability
Thejas-bhat Jan 9, 2026
0d93779
cleanup:
Thejas-bhat Jan 15, 2026
499e619
cleanup and refactor the code to have the foundational stuff
Thejas-bhat Jan 26, 2026
373b0e4
refactor file transfer
Thejas-bhat Jan 29, 2026
4430261
fix var name
Thejas-bhat Jan 29, 2026
2ae17cf
refactor the trainer
Thejas-bhat Jan 29, 2026
2d0d7b8
fix trainer impls
Thejas-bhat Jan 29, 2026
c8677f7
fix trainer init
Thejas-bhat Jan 29, 2026
7c939de
merge conflict resolve
Thejas-bhat Feb 2, 2026
cf0b034
cleanup + refactor code
Thejas-bhat Feb 4, 2026
c4249f8
enabling fastmerge only via index mapping
Thejas-bhat Feb 17, 2026
cb5e5f1
bug fix: loading from bolt
Thejas-bhat Feb 17, 2026
9dec22e
cleanup + refactor code
Thejas-bhat Feb 4, 2026
f652807
Merge branch 'fmflag' into fastmerge
Thejas-bhat Feb 24, 2026
2146a74
bug fix cacheMeta fetch API
Thejas-bhat Feb 24, 2026
869d25f
use callbacks to collect and use train data while merging
Thejas-bhat Jun 17, 2025
186ba20
collect training sample on the file path as well
Thejas-bhat Jul 8, 2025
fc78809
cleanup debug logs
Thejas-bhat Aug 21, 2025
162cdda
batch training support
Thejas-bhat Nov 26, 2025
230c714
bug fix, debug logging
Thejas-bhat Dec 11, 2025
5255aea
refactor file transfer
Thejas-bhat Jan 29, 2026
596a913
implement file transfer APIs
Thejas-bhat Jan 29, 2026
f1df0cf
move file transfer to train files
Thejas-bhat Jan 29, 2026
de31a6a
fix file transfer logic
Thejas-bhat Feb 4, 2026
0cb6976
implement file transfer APIs
Thejas-bhat Jan 29, 2026
1495c45
use callbacks to collect and use train data while merging
Thejas-bhat Jun 17, 2025
1e45af8
collect training sample on the file path as well
Thejas-bhat Jul 8, 2025
1b42c08
cleanup debug logs
Thejas-bhat Aug 21, 2025
cb12112
batch training support
Thejas-bhat Nov 26, 2025
8930689
bug fix, debug logging
Thejas-bhat Dec 11, 2025
d9a44c8
cleanup and refactor the code to have the foundational stuff
Thejas-bhat Jan 26, 2026
e265dd6
refactor file transfer
Thejas-bhat Jan 29, 2026
84f52f8
track training progress to recover a trained state
Thejas-bhat Jan 30, 2026
35420ae
bug fix: fix zero values handling
Thejas-bhat Jan 30, 2026
5b9858d
tracking internal data in the train bucket
Thejas-bhat Feb 12, 2026
f2797a1
cleanup trainer after complete
Thejas-bhat Feb 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions centroid_index_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
//go:build vectors
// +build vectors

package bleve

import (
"encoding/json"
"fmt"
"os"
"testing"

"github.com/blevesearch/bleve/v2/analysis/lang/en"
"github.com/blevesearch/bleve/v2/mapping"
index "github.com/blevesearch/bleve_index_api"
)

func loadSiftData() ([]map[string]interface{}, error) {
fileContent, err := os.ReadFile("~/fts/data/datasets/vec-sift-bucket.json")
if err != nil {
return nil, err
}
var documents []map[string]interface{}
err = json.Unmarshal(fileContent, &documents)
if err != nil {
return nil, err
}
return documents, nil
}

func TestCentroidIndex(t *testing.T) {
_, _, err := readDatasetAndQueries(testInputCompressedFile)
if err != nil {
t.Fatal(err)
}
documents, err := loadSiftData()
if err != nil {
t.Fatal(err)
}
contentFieldMapping := NewTextFieldMapping()
contentFieldMapping.Analyzer = en.AnalyzerName

vecFieldMappingL2 := mapping.NewVectorFieldMapping()
vecFieldMappingL2.Dims = 128
vecFieldMappingL2.Similarity = index.EuclideanDistance

indexMappingL2Norm := NewIndexMapping()
indexMappingL2Norm.DefaultMapping.AddFieldMappingsAt("content", contentFieldMapping)
indexMappingL2Norm.DefaultMapping.AddFieldMappingsAt("vector", vecFieldMappingL2)

idx, err := newIndexUsing(t.TempDir(), indexMappingL2Norm, Config.DefaultIndexType, Config.DefaultKVStore, nil)
if err != nil {
t.Fatal(err)
}
defer func() {
err := idx.Close()
if err != nil {
t.Fatal(err)
}
}()

batch := idx.NewBatch()
for _, doc := range documents[:100000] {
docId := fmt.Sprintf("%s:%s", index.TrainDataPrefix, doc["id"])
err = batch.Index(docId, doc)
if err != nil {
t.Fatal(err)
}
}

err = idx.Train(batch)
if err != nil {
t.Fatal(err)
}
}
22 changes: 22 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,25 @@ require (
github.com/spf13/pflag v1.0.9 // indirect
golang.org/x/sys v0.40.0 // indirect
)

replace github.com/blevesearch/bleve/v2 => /Users/thejas.orkombu/fts/blevesearch/bleve

replace github.com/blevesearch/zapx/v11 => /Users/thejas.orkombu/fts/blevesearch/zapx11

replace github.com/blevesearch/zapx/v12 => /Users/thejas.orkombu/fts/blevesearch/zapx12

replace github.com/blevesearch/zapx/v13 => /Users/thejas.orkombu/fts/blevesearch/zapx13

replace github.com/blevesearch/zapx/v14 => /Users/thejas.orkombu/fts/blevesearch/zapx14

replace github.com/blevesearch/zapx/v15 => /Users/thejas.orkombu/fts/blevesearch/zapx15

replace github.com/blevesearch/zapx/v16 => /Users/thejas.orkombu/fts/blevesearch/zapx

replace github.com/blevesearch/scorch_segment_api/v2 => /Users/thejas.orkombu/fts/blevesearch/scorch_segment_api

replace github.com/blevesearch/go-faiss => /Users/thejas.orkombu/fts/blevesearch/go-faiss

replace github.com/blevesearch/bleve_index_api => /Users/thejas.orkombu/fts/blevesearch/bleve_index_api

replace github.com/blevesearch/sear => /Users/thejas.orkombu/fts/blevesearch/sear
11 changes: 11 additions & 0 deletions index.go
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,13 @@ type IndexCopyable interface {
CopyTo(d index.Directory) error
}

// IndexFileCopyable is an index supporting the transfer of a single file between
// two indexes
type IndexFileCopyable interface {
UpdateFileInBolt(key []byte, value []byte) error
CopyFile(file string, d index.IndexDirectory) error
}

// FileSystemDirectory is the default implementation for the
// index.Directory interface.
type FileSystemDirectory string
Expand Down Expand Up @@ -396,3 +403,7 @@ type InsightsIndex interface {
// CentroidCardinalities returns the centroids (clusters) from IVF indexes ordered by data density.
CentroidCardinalities(field string, limit int, desceding bool) ([]index.CentroidCardinality, error)
}
type VectorIndex interface {
Index
Train(*Batch) error
}
24 changes: 23 additions & 1 deletion index/scorch/persister.go
Original file line number Diff line number Diff line change
Expand Up @@ -425,7 +425,6 @@ func (s *Scorch) persistSnapshotMaybeMerge(snapshot *IndexSnapshot, po *persiste
var totSize int
var numSegsToFlushOut int
var totDocs uint64

// legacy behaviour of merge + flush of all in-memory segments in one-shot
if legacyFlushBehaviour(po.MaxSizeInMemoryMergePerWorker, po.NumPersisterWorkers) {
val := &flushable{
Expand Down Expand Up @@ -575,6 +574,11 @@ func copyToDirectory(srcPath string, d index.Directory) (int64, error) {
return 0, fmt.Errorf("GetWriter err: %v", err)
}

// skip
if dest == nil {
return 0, nil
}

sourceFileStat, err := os.Stat(srcPath)
if err != nil {
return 0, err
Expand Down Expand Up @@ -853,6 +857,10 @@ func zapFileName(epoch uint64) string {
return fmt.Sprintf("%012x.zap", epoch)
}

func (s *Scorch) loadTrainedData(bucket *bolt.Bucket) error {
return s.trainer.loadTrainedData(bucket)
}

// bolt snapshot code

func (s *Scorch) loadFromBolt() error {
Expand All @@ -861,6 +869,7 @@ func (s *Scorch) loadFromBolt() error {
if snapshots == nil {
return nil
}
var mappingBytes []byte
foundRoot := false
c := snapshots.Cursor()
for k, _ := c.Last(); k != nil; k, _ = c.Prev() {
Expand Down Expand Up @@ -902,8 +911,21 @@ func (s *Scorch) loadFromBolt() error {
_ = rootPrev.DecRef()
}

mappingBytes, err = indexSnapshot.GetInternal(util.MappingInternalKey)
foundRoot = true
}

// try init trainer with the mapping details
if s.trainer == nil {
s.config["index_mapping"] = mappingBytes
s.trainer = initTrainer(s, s.config)
}
trainerBucket := snapshots.Bucket(util.BoltTrainerKey)
err := s.trainer.loadTrainedData(trainerBucket)
if err != nil {
return err
}

return nil
})
if err != nil {
Expand Down
130 changes: 130 additions & 0 deletions index/scorch/scorch.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package scorch
import (
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"sync"
Expand Down Expand Up @@ -78,6 +79,10 @@ type Scorch struct {
persisterNotifier chan *epochWatcher
rootBolt *bolt.DB
asyncTasks sync.WaitGroup
// not a real searchable segment
centroidIndex *SegmentSnapshot

trainer trainer

onEvent func(event Event) bool
onAsyncError func(err error, path string)
Expand All @@ -89,6 +94,33 @@ type Scorch struct {
spatialPlugin index.SpatialAnalyzerPlugin
}

// trainer interface is used for training an index that has the concept
// of "learning". Naturally, a vector index is one such thing that would
// implement this interface. There can be multiple implementations of the
// training itself even for the same index type.
//
// this component is not supposed to interact with the other master routines
// of scorch and will be used only for training the index before the actual data
// ingestion starts. The routine should also be released once the
// training is marked as complete - which can be done using the BoltTrainCompleteKey
// key and a bool value. However the struct is still maintained for the pointer to
// the instance so that we can use in the later stages of the index lifecycle.
type trainer interface {
// ephemeral
trainLoop()
// for the training state and the ingestion of the samples
train(batch *index.Batch) error

// to load the metadata from the bolt under the BoltTrainerKey
loadTrainedData(*bolt.Bucket) error
// to fetch the internal data from the component
getInternal(key []byte) ([]byte, error)

// file transfer operations
copyFileLOCKED(file string, d index.IndexDirectory) error
updateBolt(snapshotsBucket *bolt.Bucket, key []byte, value []byte) error
}

type ScorchErrorType string

func (t ScorchErrorType) Error() string {
Expand Down Expand Up @@ -170,6 +202,11 @@ func NewScorch(storeName string,
}
}

segConfig, ok := config["segmentConfig"].(map[string]interface{})
if ok {
rv.segmentConfig = segConfig
}

typ, ok := config["spatialPlugin"].(string)
if ok {
if err := rv.loadSpatialAnalyzerPlugin(typ); err != nil {
Expand Down Expand Up @@ -207,6 +244,10 @@ func NewScorch(storeName string,
return nil, err
}

if trainer := initTrainer(rv, config); trainer != nil {
rv.trainer = trainer
}

return rv, nil
}

Expand Down Expand Up @@ -261,6 +302,11 @@ func (s *Scorch) Open() error {
s.asyncTasks.Add(1)
go s.introducerLoop()

if s.trainer != nil {
s.asyncTasks.Add(1)
go s.trainer.trainLoop()
}

if !s.readOnly && s.path != "" {
s.asyncTasks.Add(1)
go s.persisterLoop()
Expand Down Expand Up @@ -534,6 +580,31 @@ func (s *Scorch) Batch(batch *index.Batch) (err error) {
return err
}

func (s *Scorch) getInternal(key []byte) ([]byte, error) {
s.rootLock.RLock()
defer s.rootLock.RUnlock()

switch string(key) {
case string(util.BoltTrainCompleteKey):
fallthrough
case string(util.BoltTrainedSamplesKey):
if s.trainer != nil {
return s.trainer.getInternal(key)
} else {
return nil, fmt.Errorf("getInternal on train keys is not supported" +
" with this build")
}
}
return nil, nil
}

func (s *Scorch) Train(batch *index.Batch) error {
if s.trainer != nil {
return s.trainer.train(batch)
}
return fmt.Errorf("training is not supported with this build")
}

func (s *Scorch) prepareSegment(newSegment segment.Segment, ids []string,
internalOps map[string][]byte, persistedCallback index.BatchCallback, stats *fieldStats,
) error {
Expand Down Expand Up @@ -979,6 +1050,65 @@ func (s *Scorch) CopyReader() index.CopyReader {
return rv
}

func (s *Scorch) UpdateFileInBolt(key []byte, value []byte) error {
tx, err := s.rootBolt.Begin(true)
if err != nil {
return err
}
defer func() {
if err != nil {
_ = tx.Rollback()
}
}()

snapshotsBucket, err := tx.CreateBucketIfNotExists(util.BoltSnapshotsBucket)
if err != nil {
return err
}

// currently this is specific to centroid index file update
err = s.trainer.updateBolt(snapshotsBucket, key, value)
if err != nil {
return err
}

err = tx.Commit()
if err != nil {
return err
}

return s.rootBolt.Sync()
}

// CopyFile copies a specific file to a destination directory which has an access to a bleve index
// doing a io.Copy() isn't enough because the file needs to be tracked in bolt file as well
func (s *Scorch) CopyFile(file string, d index.IndexDirectory) error {
s.rootLock.Lock()
defer s.rootLock.Unlock()

dest, err := d.GetWriter(filepath.Join("store", file))
if err != nil {
return err
}

source, err := os.Open(filepath.Join(s.path, file))
if err != nil {
return err
}

defer source.Close()
defer dest.Close()
_, err = io.Copy(dest, source)
if err != nil {
return err
}

// this code is currently specific to copying trained data but is future proofed for other files
// to be updated in the dest's bolt
err = s.trainer.copyFileLOCKED(file, d)
return err
}

// external API to fire a scorch event (EventKindIndexStart) externally from bleve
func (s *Scorch) FireIndexEvent() {
s.fireEvent(EventKindIndexStart, 0)
Expand Down
4 changes: 4 additions & 0 deletions index/scorch/snapshot_index.go
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,10 @@ func (is *IndexSnapshot) Fields() ([]string, error) {
}

func (is *IndexSnapshot) GetInternal(key []byte) ([]byte, error) {
_, ok := is.internal[string(key)]
if !ok {
return is.parent.getInternal(key)
}
return is.internal[string(key)], nil
}

Expand Down
5 changes: 4 additions & 1 deletion index/scorch/snapshot_segment.go
Original file line number Diff line number Diff line change
Expand Up @@ -370,8 +370,11 @@ func (c *cachedMeta) updateMeta(field string, val interface{}) {

func (c *cachedMeta) fetchMeta(field string) (rv interface{}) {
c.m.RLock()
defer c.m.RUnlock()
if c.meta == nil {
return nil
}
rv = c.meta[field]
c.m.RUnlock()
return rv
}

Expand Down
Loading
Loading