Help us shape the future of Go! Take the 2026 Go Developer Survey

sqlvec

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 20, 2026 License: MIT Imports: 4 Imported by: 0

README

sqlvec-go

A cgo-free SQLite driver with vector search built in: SQLite 3.53.4 and sqlite-vector 1.1.2, compiled to WebAssembly and translated to Go. No cgo, no C toolchain, nothing to ship beside the binary.

The extension's sources live in the parent project, sqlvec, which integrates sqlite-vector into SQLite as a built-in extension the way FTS5 and R-Tree are built in. This module is that work made available to Go.

go get github.com/axe-or/sqlite3vec-go
import (
	"database/sql"

	_ "github.com/axe-or/sqlite3vec-go"
)

db, err := sql.Open("sqlite3", "file:docs.db")

That is the whole setup. The blank import registers the sqlite3 driver and arranges for every connection it opens to carry the extension, so vector_as_f32, vector_init, vector_full_scan and the rest answer from the first query — the same "no .load, no sqlite3_vector_init() call" the parent project gives a C build.

Nothing is compiled at install time: CGO_ENABLED=0 GOOS=windows GOARCH=arm64 go build works like any other Go package.

How it works

SQLite comes from ncruces/go-sqlite3, which does not interpret WebAssembly: it compiles a Wasm build of SQLite to Go source with wasm2go and ships the translation. Extensions load the way SQLite loads a shared library — a Wasm dylink module placed in the host's memory and function table — so the parent project's sqlite/ext/vector/ is compiled into one of those, translated the same way, and committed here as internal/vector/.

The upshot is that nothing is forked. This module depends on released ncruces/go-sqlite3, and the extension it loads is that tree's, built by wasm/build.sh.

make wasm-tools    # once: fetch wasi-sdk and binaryen into wasm/tools/
make wasm          # rebuild internal/vector/ after changing the extension
make test

SQLITE_TREE says where that tree is; it defaults to .., which is where it sits today.

The two halves meet through SQLite's own ABI, so they must be the same version of SQLite: internal/vector/ is compiled against the parent tree's sqlite3.h, and ncruces/go-sqlite3 has to be a release built from the same one. TestSQLiteVersion is the guard, and it is the first thing to check after bumping either side.

vector_init is per connection

vector_init() records its configuration on the connection that ran it, and database/sql hands out connections from a pool — so a query can land on a connection that never saw it. Either hold the pool to one connection:

db.SetMaxOpenConns(1)

or run vector_init on each new connection, which driver.Open will do for you:

db, err := driver.Open("file:docs.db", func(c *sqlite3.Conn) error {
	return c.Exec(`SELECT vector_init('docs', 'embedding',
	                                  'type=FLOAT32,dimension=384,distance=COSINE')`)
})

The extension is already loaded on that connection by then; the callback only has to configure it.

To take the automatic registration out and do it by hand, build with -tags sqlvec_manual and pass sqlvec.Register to driver.Open yourself.

Performance

wasm2go does not accept Wasm SIMD, so this build has only the scalar kernels — the AVX-512, AVX2, SSE2, NEON and RVV ones cannot survive the translation. SELECT vector_backend() reports CPU here for that reason, where a native build on this machine reports AVX2.

What that costs, on one vector_full_scan over a FLOAT32 corpus (Ryzen 5 4500U, make bench, which runs go test -bench . and then the same scan through the native build/sqlite3):

corpus Go (scalar) native (AVX2) ratio
10k × 384 15.2 ms 5.6 ms 2.7×
100k × 384 149.6 ms 45.5 ms 3.3×
10k × 768 28.0 ms 9.2 ms 3.0×
100k × 768 271.6 ms 90.3 ms 3.0×

About three times slower, consistently, and it scales linearly in both directions — so the translation is not the problem, the missing vector instructions are.

Brute force over a large corpus is the worst case for this; vector_quantize and vector_quantize_scan cut the arithmetic by far more than SIMD does, and they work here exactly as they do natively.

Verification

make test    # both registration modes

vector_version() and vector_backend(), the example above, every vector type round tripping, a quantized scan, a full scan whose distances are checked against the same L2 computed in Go, and the driver.Open pattern above across sixteen concurrent connections.

wasm/build.sh is reproducible: rerunning it leaves internal/vector/ byte-identical. CGO_ENABLED=0 go build and cross-builds for windows/arm64, windows/amd64, darwin/arm64, freebsd/amd64 and linux/386 all pass.

License

SQLite is public domain; sqlite-vector is Apache 2.0, and the bundled FP16 headers are MIT. The translated extension in internal/vector/ therefore carries the Apache 2.0 terms, as does anything built from it.

Documentation

Overview

Package sqlvec is a cgo-free SQLite driver with vector search built in.

Importing it for its side effects registers the "sqlite3" driver with database/sql, and arranges for every connection that driver opens to have the sqlite-vector extension loaded:

import (
	"database/sql"

	_ "github.com/axe-or/sqlite3vec-go"
)

db, err := sql.Open("sqlite3", "file:docs.db")

The vector_* functions are then available without a .load or any further setup, the way fts5 and rtree are:

CREATE TABLE docs(id INTEGER PRIMARY KEY, embedding BLOB, label TEXT);
INSERT INTO docs(embedding, label)
  VALUES (vector_as_f32('[1.0, 0.0, 0.0, 0.0]'), 'x');

SELECT vector_init('docs', 'embedding', 'type=FLOAT32,dimension=4,distance=L2');

SELECT d.label, v.distance FROM docs AS d
  JOIN vector_full_scan('docs', 'embedding',
                        vector_as_f32('[1.0,0.0,0.0,0.0]'), 10) AS v
    ON d.id = v.rowid;

SQLite itself comes from github.com/ncruces/go-sqlite3, whose API this package does not wrap or hide -- use it directly for prepared statements, custom functions, virtual tables, VFSes and the rest.

vector_init is per connection

vector_init() records its configuration on the connection that ran it, and database/sql hands out connections from a pool, so a later query can land on one that never saw it. Either hold the pool to a single connection with db.SetMaxOpenConns(1), or have driver.Open run vector_init on each new connection:

db, err := driver.Open("file:docs.db", func(c *sqlite3.Conn) error {
	return c.Exec(`SELECT vector_init('docs', 'embedding',
	                                  'type=FLOAT32,dimension=384')`)
})

The extension is already loaded by the time that callback runs; it only has to configure it.

Performance

The extension is compiled to WebAssembly and translated to Go, so its distance kernels are the portable scalar ones: the AVX-512, AVX2, SSE2, NEON and RVV kernels of a native build cannot come along, because the translator does not accept Wasm SIMD. SELECT vector_backend() reports CPU here for that reason.

A brute-force vector_full_scan is about three times slower than the same scan in a native AVX2 build -- see the README for the measurements. Quantized search (vector_quantize, then vector_quantize_scan) removes far more work than SIMD does, and behaves here exactly as it does natively.

Index

Constants

View Source
const Version = "1.1.2"

Version is the version of sqlite-vector this package carries. It is what SELECT vector_version() returns.

Variables

This section is empty.

Functions

func Register

func Register(db *sqlite3.Conn) error

Register loads the vector extension into a single connection.

The package already does this for every connection, so Register is only needed to opt out of that: build with the sqlvec_manual tag, which drops the automatic registration, and pass Register to driver.Open.

Types

This section is empty.

Directories

Path Synopsis
internal

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL