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