Skip to content

Commit 2b2c768

Browse files
committed
fix: add CountVectors method and optimize ExportVectors logic
1 parent a7dc09a commit 2b2c768

3 files changed

Lines changed: 28 additions & 83 deletions

File tree

internal/infra/imsearch/db/crud.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,14 @@ func (d *DB) GetCount(ctx context.Context) (int64, int64, error) {
173173
return images, total.Int64, nil
174174
}
175175

176+
func (d *DB) CountVectors(ctx context.Context) (int64, error) {
177+
var n int64
178+
err := d.sql.QueryRowContext(ctx,
179+
`SELECT COALESCE(SUM(LENGTH(vector)/?), 0) FROM vector`,
180+
32).Scan(&n)
181+
return n, err
182+
}
183+
176184
type VectorBound struct {
177185
ImageID int64
178186
Total int64 // inclusive upper bound of this image's vector ids

internal/infra/imsearch/imdb/build.go

Lines changed: 17 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -11,78 +11,25 @@ import (
1111
var errNotTrained = errors.New("index backend not trained: run train first")
1212

1313
func (m *IMDB) ExportVectors(ctx context.Context, count int) ([][]byte, error) {
14-
imgCount, totalVec, err := m.db.GetCount(ctx)
14+
_, totalVec, err := m.db.GetCount(ctx)
1515
if err != nil {
1616
return nil, err
1717
}
18-
if imgCount == 0 || totalVec == 0 {
18+
if totalVec == 0 {
1919
return nil, nil
2020
}
2121
if int64(count) >= totalVec {
2222
return m.exportSequential(ctx, count)
2323
}
24-
25-
avgPerImg := max(int(totalVec/imgCount), 1)
26-
imgSample := min(max(count/avgPerImg, 1), int(imgCount))
27-
28-
out := make([][]byte, 0, count)
29-
sqlDB := m.db.SQL()
30-
31-
rows, err := sqlDB.QueryContext(ctx,
32-
`SELECT v.vector FROM vector v
33-
WHERE v.id IN (
34-
SELECT id FROM image
35-
WHERE id IN (SELECT abs(random()) % (SELECT coalesce(max(id),0)+1 FROM image) FROM image LIMIT ?)
36-
)
37-
ORDER BY v.id ASC`, imgSample*3)
38-
if err != nil {
39-
// Fallback: sequential stride sampling (slow but memory-safe).
40-
return m.exportStride(ctx, count, totalVec)
41-
}
42-
defer rows.Close()
43-
44-
keepRatio := float64(count) / float64(totalVec)
45-
step := 1
46-
if keepRatio > 0 && keepRatio < 1 {
47-
step = int(1.0 / keepRatio)
24+
// totalVec from GetCount is a high-water mark (MAX(total_vector_count)),
25+
// not the live vector count after deletes. Use the actual count to compute
26+
// the sampling stride so we don't under-sample.
27+
actualVec, err := m.db.CountVectors(ctx)
28+
if err != nil || actualVec == 0 {
29+
actualVec = totalVec
4830
}
49-
if step < 1 {
50-
step = 1
51-
}
52-
53-
for rows.Next() {
54-
var blob []byte
55-
if err := rows.Scan(&blob); err != nil {
56-
return nil, err
57-
}
58-
n := len(blob) / m.codeSize
59-
for i := 0; i < n; i += step {
60-
code := make([]byte, m.codeSize)
61-
copy(code, blob[i*m.codeSize:(i+1)*m.codeSize])
62-
out = append(out, code)
63-
if len(out) >= count {
64-
return out, nil
65-
}
66-
}
67-
}
68-
if len(out) < count {
69-
// Random sampling didn't yield enough; fall back to stride.
70-
return m.exportStride(ctx, count, totalVec)
71-
}
72-
return out, rows.Err()
73-
}
74-
75-
// exportStride reads vectors in id order with a stride, keeping memory bounded.
76-
func (m *IMDB) exportStride(ctx context.Context, count int, totalVec int64) ([][]byte, error) {
77-
keepRatio := float64(count) / float64(totalVec)
78-
step := 1
79-
if keepRatio > 0 && keepRatio < 1 {
80-
step = int(1.0 / keepRatio)
81-
}
82-
if step < 1 {
83-
step = 1
84-
}
85-
out := make([][]byte, 0, count)
31+
keepRatio := float64(count) / float64(actualVec)
32+
var out [][]byte
8633
const batch = 500
8734
var offset int64
8835
for len(out) < count {
@@ -95,6 +42,13 @@ func (m *IMDB) exportStride(ctx context.Context, count int, totalVec int64) ([][
9542
}
9643
for _, r := range rows {
9744
n := len(r.Vector) / m.codeSize
45+
step := 1
46+
if keepRatio > 0 {
47+
step = int(1.0 / keepRatio)
48+
}
49+
if step < 1 {
50+
step = 1
51+
}
9852
for i := 0; i < n; i += step {
9953
code := make([]byte, m.codeSize)
10054
copy(code, r.Vector[i*m.codeSize:(i+1)*m.codeSize])

internal/infra/imsearch/index/local/local.go

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -337,31 +337,15 @@ func (bl *builder) commitShards(nSub int) error {
337337
return bl.mergeIntoNew(nSub, finalPath)
338338
}
339339

340-
// invlists.bin exists: merge new shards into it to avoid VStack proliferation.
341-
return bl.mergeIntoExisting(nSub, finalPath)
340+
// invlists.bin exists: shards are saved alongside it and the searcher
341+
// loads them all via VStack. No expensive full-rewrite merge needed.
342+
return nil
342343
}
343344

344345
func (bl *builder) mergeIntoNew(nSub int, finalPath string) error {
345-
return bl.mergeShards(nSub, finalPath, false)
346-
}
347-
348-
func (bl *builder) mergeIntoExisting(nSub int, finalPath string) error {
349-
return bl.mergeShards(nSub, finalPath, true)
350-
}
351-
352-
func (bl *builder) mergeShards(nSub int, finalPath string, includeExisting bool) error {
353346
var subs []invlists.InvertedLists
354347
var closers []*invlists.OnDisk
355348

356-
if includeExisting {
357-
od, err := invlists.LoadOnDisk(finalPath)
358-
if err != nil {
359-
return fmt.Errorf("merge: load existing invlists: %w", err)
360-
}
361-
subs = append(subs, od)
362-
closers = append(closers, od)
363-
}
364-
365349
for i := range nSub {
366350
od, err := invlists.LoadOnDisk(bl.backend.subIndexPath(i))
367351
if err != nil {
@@ -394,7 +378,6 @@ func (bl *builder) mergeShards(nSub int, finalPath string, includeExisting bool)
394378
if err := os.Rename(tmpMerged, finalPath); err != nil {
395379
return fmt.Errorf("merge: rename: %w", err)
396380
}
397-
// Remove merged shards.
398381
for i := range nSub {
399382
os.Remove(bl.backend.subIndexPath(i))
400383
}

0 commit comments

Comments
 (0)