Skip to content

SIFT1M benchmark and faster distance functions - #2

Merged
justintout merged 5 commits into
mainfrom
feature/faster-distance
Sep 14, 2026
Merged

justintout merged 5 commits into
mainfrom
feature/faster-distance

Conversation

@justintout

Copy link
Copy Markdown
Owner

Summary

  • Add a SIFT1M benchmark (sift_test.go, behind the sift build tag) that reports insert time, single-threaded query latency, and recall@1/10/100 against the published ground truth.
  • Add bench/sift_compare.py (uv inline script) that runs the same queries through sqlite-vec, FAISS, and hnswlib for comparison. Results are in the README.
  • vector_distance now computes distance directly on the blob bytes in an unrolled loop, with no per-row allocations. Previously it decoded both arguments into new []float32 slices for every row.
  • vector_distance_q computes the distance from int8 differences and scales once, since dequantization is affine. dequantize is removed.
  • README adds a PRAGMA mmap_size tip.
  • Remove PLAN.md, which is complete, and the CLAUDE.md rules that depended on it.

Results

SIFT1M: 1M vectors × 128 dimensions, k=100, one thread, Apple M3 Max, default SQLite settings.

p50 before p50 after
vector_distance (float32) 733 ms 362 ms
vector_distance_q (int8) 659 ms 339 ms

Recall is unchanged. With PRAGMA mmap_size = 1 GiB, float32 queries take 246 ms. For comparison, sqlite-vec's vec_distance_l2 takes 306 ms and vec0 takes 143 ms under default settings.

Behavior notes

  • Float32 distances can differ from the previous implementation in the last bits, because the unrolled loop sums in a different order.
  • Quantized distances differ from the dequantize-then-L2 path by at most 3e-8 relative (checked on 10,000 SIFT-like vector pairs).

Testing

  • go vet ./... and go vet -tags sift ./...
  • go test -race ./...
  • go test -tags sift -run TestSIFT1M -timeout 0 -v -sift.dir .tmp/sift

🤖 Generated with Claude Code

https://claude.ai/code/session_01PTvMD95uUfGT1jasgFsLdP

justintout and others added 4 commits September 13, 2026 20:30
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PTvMD95uUfGT1jasgFsLdP
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PTvMD95uUfGT1jasgFsLdP
vector_distance decoded both arguments into new []float32 slices for every
row scanned. It now decodes values inside an unrolled distance loop with no
allocations. vector_distance_q computes the distance from int8 differences
and scales once, since dequantization is affine; dequantize is removed.

On SIFT1M (1M x 128, k=100, single thread) p50 query latency drops from
733 ms to 362 ms for float32 and from 659 ms to 339 ms for int8, with
unchanged recall. README adds a PRAGMA mmap_size tip (246 ms float32).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PTvMD95uUfGT1jasgFsLdP
The implementation plan is complete. CLAUDE.md drops the rules that
depended on it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PTvMD95uUfGT1jasgFsLdP
* bump zombiezen.com/go/sqlite for function error fix

v1.4.2 shadowed err in resultError, so errors returned from SQL functions
were silently dropped. The fix (zombiezen/go-sqlite#133) is merged but not
yet tagged, so this pins the main pseudo-version. Un-skips the error tests
that were blocked on it and removes the obsolete stub-error test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PTvMD95uUfGT1jasgFsLdP

* add vector_index virtual table

vector_index stores vectors in chunks of 1024 inside shadow tables and
answers k-nearest neighbor queries by reading whole chunks and scanning
them in Go across GOMAXPROCS goroutines, instead of evaluating a SQL
function per row. Supports float32 and int8 storage, k = ? and LIMIT
(with OFFSET), rowid lookups, full scans, INSERT/UPDATE/DELETE, rename,
and drop. Reconnecting with a different dimension or quantization range
is an error.

SIFT1M p50 (k=100, default settings): 139 ms float32 and 46 ms int8,
versus 363 ms and 336 ms for the scalar functions. Inserting 1M vectors
takes 9.8 s (float32) and 6.5 s (int8).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PTvMD95uUfGT1jasgFsLdP

* size vector_index chunks by bytes and add chunk_size

Chunks held a fixed 1024 vectors, which is 512 KiB at 128 dimensions but
6 MiB at 1536. A sweep over 128, 768, and 1536 dimensions and chunk data
sizes from 64 KiB to 4 MiB found search fastest near 1 MiB and inserts
4.5-16x slower at 4 MiB than at 1 MiB, for both float32 and int8.

The default capacity is now max(1, 1 MiB / vector bytes). A chunk_size=N
argument overrides it. Both are stored in name_info at creation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PTvMD95uUfGT1jasgFsLdP

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@justintout
justintout merged commit 76a4572 into main Sep 14, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant