package vectord import ( "fmt" "math/rand" "testing" ) // BenchmarkSingleAdd vs BenchmarkBatchAdd quantifies the lock-amortization // win for the HTTP-batch shape. Same N items, same vectors; one path // takes the lock N times, the other takes it once. Run with: // go test ./internal/vectord/ -bench=. -benchmem -benchtime=1x func BenchmarkSingleAdd(b *testing.B) { for _, n := range []int{16, 128, 1024} { b.Run(fmt.Sprintf("N=%d", n), func(b *testing.B) { items := makeBatch(n, 768) for i := 0; i < b.N; i++ { idx := mustIndex(b) for _, it := range items { if err := idx.Add(it.ID, it.Vector, it.Metadata); err != nil { b.Fatalf("Add: %v", err) } } } }) } } func BenchmarkBatchAdd(b *testing.B) { for _, n := range []int{16, 128, 1024} { b.Run(fmt.Sprintf("N=%d", n), func(b *testing.B) { items := makeBatch(n, 768) for i := 0; i < b.N; i++ { idx := mustIndex(b) if err := idx.BatchAdd(items); err != nil { b.Fatalf("BatchAdd: %v", err) } } }) } } // TestBatchAdd_IntraBatchDedup guards the 2026-04-29 scrum BLOCK: // without dedup, coder/hnsw's "node not added" length-invariant // panics when the same ID appears twice in one batch. Last-write- // wins semantics; the second vector for a duplicate ID replaces the // first. func TestBatchAdd_IntraBatchDedup(t *testing.T) { idx := mustIndex(t) items := []BatchItem{ {ID: "a", Vector: makeVec(768, 1)}, {ID: "b", Vector: makeVec(768, 2)}, {ID: "a", Vector: makeVec(768, 99)}, // duplicate — should win } if err := idx.BatchAdd(items); err != nil { t.Fatalf("BatchAdd: %v", err) } if idx.Len() != 2 { t.Errorf("Len: want 2, got %d", idx.Len()) } // "a" should hold the LATER vector (the 99 one), not the first. v, _, ok := idx.Lookup("a") if !ok { t.Fatal("a not found") } if v[0] != 99 { t.Errorf("last-write-wins: want vec[0]=99, got %v", v[0]) } } func makeVec(dim int, val float32) []float32 { v := make([]float32, dim) v[0] = val v[1] = 1 // non-zero-norm under cosine return v } func mustIndex(tb testing.TB) *Index { tb.Helper() idx, err := NewIndex(IndexParams{ Name: "bench", Dimension: 768, M: DefaultM, EfSearch: DefaultEfSearch, Distance: DistanceCosine, }) if err != nil { tb.Fatalf("NewIndex: %v", err) } return idx } func makeBatch(n, dim int) []BatchItem { rng := rand.New(rand.NewSource(int64(n))) out := make([]BatchItem, n) for i := range out { v := make([]float32, dim) for j := range v { v[j] = rng.Float32()*2 - 1 } out[i] = BatchItem{ID: fmt.Sprintf("k-%06d", i), Vector: v} } return out }