Note
PhotoStructure's Production Fork: This is PhotoStructure's actively maintained fork of asg017/sqlite-vec, optimized for production use with additional features, comprehensive testing, and ongoing maintenance funded by PhotoStructure Inc.
Credits: Alex Garcia (original implementation), Vlad Lasky (community fork with 15+ merged upstream PRs), PhotoStructure Inc. (ongoing maintenance and improvements).
Why this fork exists:
- PhotoStructure depends on sqlite-vec for production vector search in our photo management platform
- We've added production-critical features, security hardening, and comprehensive testing
- We're committed to maintaining this for as long as PhotoStructure exists
- All improvements remain open source (MIT/Apache-2.0) for the community
Fork improvements:
- Testing: AddressSanitizer/Valgrind/UBSan integration, 30+ error path tests, memory leak fixes
- Security: Safe integer parsing, vendor checksum validation, pinned CI actions, OIDC releases
- Node.js: Alpine/musl + Windows ARM64 prebuilds, bundled binaries (no post-install scripts)
- Features: Distance constraints, OPTIMIZE command, ALTER TABLE RENAME, GLOB/LIKE operators
- Documentation: Comprehensive error path coverage, KNN filtering behavior, production deployment guides
Maintained by PhotoStructure Inc. Contributions welcome. See CHANGELOG.md for detailed changes.
An extremely small, "fast enough" vector search SQLite extension that runs
anywhere! A successor to sqlite-vss
- Store and query float, int8, and binary vectors in
vec0virtual tables - Written in pure C, no dependencies, runs anywhere SQLite runs (Linux/MacOS/Windows, in the browser with WASM, Raspberry Pis, etc.)
- Store non-vector data in metadata, auxiliary, or partition key columns
sqlite-vec is a
Mozilla Builders project,
with additional sponsorship from
Fly.io ,
Turso,
SQLite Cloud, and
Shinkai.
See the Sponsors section for more details.
Prebuilt binaries for all major platforms are published to npm:
npm install @photostructure/sqlite-vecSupported platforms: Linux (x64, ARM64, musl), macOS (x64, ARM64), Windows (x64, ARM64)
For Python, Ruby, Rust, Go, and other language bindings, see the original asg017/sqlite-vec or Vlad Lasky's fork. This fork only publishes the Node.js package.
Note
This fork no longer carries Python, Ruby, Rust, Lua, or Go bindings. Installing them from this repository's main branch (for example pip install git+https://github.com/photostructure/sqlite-vec) no longer works; release tags up to v2.0.2 still include the Python, Ruby, Rust, and Lua bindings. Install them from asg017/sqlite-vec or Vlad Lasky's fork instead. Go users can also use ncruces/go-sqlite3's built-in ext/vec1 vector extension. This fork dropped its Go bindings in v1.2.0, after the ncruces/go-sqlite3 WASM build broke when it migrated to wasm2go, which no longer supports injecting a custom C extension at build time.
The native extension is automatically resolved from app.asar.unpacked when running inside a packaged Electron app. You need to configure your build tool to unpack the extension binaries:
electron-builder:
{
"asarUnpack": ["node_modules/@mceachen/sqlite-vec/**/*.{so,dylib,dll}"]
}electron-forge:
packagerConfig: {
asar: {
unpack: "*.{so,dylib,dll}";
}
}See CHANGELOG.md for a complete list of improvements, bug fixes, and merged upstream PRs.
Vector types: sqlite-vec supports three vector types with different trade-offs:
-- Float vectors (32-bit floating point, most common)
CREATE VIRTUAL TABLE vec_floats USING vec0(embedding float[384]);
-- Int8 vectors (8-bit integers, smaller memory footprint)
CREATE VIRTUAL TABLE vec_int8 USING vec0(embedding int8[384]);
-- Binary vectors (1 bit per dimension, maximum compression)
CREATE VIRTUAL TABLE vec_binary USING vec0(embedding bit[384]);Usage example:
.load ./vec0
create virtual table vec_examples using vec0(
sample_embedding float[8]
);
-- vectors can be provided as JSON or in a compact binary format
insert into vec_examples(rowid, sample_embedding)
values
(1, '[0.279, -0.95, -0.45, -0.554, 0.473, 0.353, 0.784, -0.826]'),
(2, '[-0.156, -0.94, -0.563, 0.011, -0.947, -0.602, 0.3, 0.09]'),
(3, '[-0.559, 0.179, 0.619, -0.987, 0.612, 0.396, -0.319, -0.689]'),
(4, '[0.914, -0.327, -0.815, -0.807, 0.695, 0.207, 0.614, 0.459]'),
(5, '[0.072, 0.946, -0.243, 0.104, 0.659, 0.237, 0.723, 0.155]'),
(6, '[0.409, -0.908, -0.544, -0.421, -0.84, -0.534, -0.798, -0.444]'),
(7, '[0.271, -0.27, -0.26, -0.581, -0.466, 0.873, 0.296, 0.218]'),
(8, '[-0.658, 0.458, -0.673, -0.241, 0.979, 0.28, 0.114, 0.369]'),
(9, '[0.686, 0.552, -0.542, -0.936, -0.369, -0.465, -0.578, 0.886]'),
(10, '[0.753, -0.371, 0.311, -0.209, 0.829, -0.082, -0.47, -0.507]'),
(11, '[0.123, -0.475, 0.169, 0.796, -0.201, -0.561, 0.995, 0.019]'),
(12, '[-0.818, -0.906, -0.781, 0.255, 0.584, -0.156, -0.873, -0.237]'),
(13, '[0.992, 0.058, 0.942, 0.722, -0.977, 0.441, 0.363, 0.074]'),
(14, '[-0.466, 0.282, -0.777, -0.13, -0.093, 0.908, 0.752, -0.473]'),
(15, '[0.001, -0.643, 0.825, 0.741, -0.403, 0.278, 0.218, -0.694]'),
(16, '[0.525, 0.079, 0.557, 0.061, -0.999, -0.352, -0.961, 0.858]'),
(17, '[0.757, 0.663, -0.385, -0.884, 0.756, 0.894, -0.829, -0.028]'),
(18, '[-0.862, 0.521, 0.532, -0.743, -0.049, 0.1, -0.47, 0.745]'),
(19, '[-0.154, -0.576, 0.079, 0.46, -0.598, -0.377, 0.99, 0.3]'),
(20, '[-0.124, 0.035, -0.758, -0.551, -0.324, 0.177, -0.54, -0.56]');
-- Find 3 nearest neighbors using LIMIT
select
rowid,
distance
from vec_examples
where sample_embedding match '[0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5]'
order by distance
limit 3;
/*
┌───────┬──────────────────┐
│ rowid │ distance │
├───────┼──────────────────┤
│ 5 │ 1.16368770599365 │
│ 13 │ 1.75137972831726 │
│ 11 │ 1.83941268920898 │
└───────┴──────────────────┘
*/How vector search works: The MATCH operator finds vectors similar to your query vector. In the example above, sample_embedding MATCH '[0.5, ...]' searches for vectors closest to [0.5, ...] and returns them ordered by distance (smallest = most similar).
Under the hood, a vector column without an indexed by clause stores its vectors in fixed-size chunks and scans each chunk to find the top-K nearest results, using SIMD instructions (AVX on x86_64, NEON on ARM) to accelerate distance calculations. Results from each chunk are merged using a two-pointer technique to produce the final sorted output. This brute-force search reads every vector and needs no index to maintain or tune; the Rescore Index and DiskANN Index sections describe the indexed alternatives.
Note: All vector similarity queries require LIMIT or k = ? (where k is the number of nearest neighbors to return). This prevents accidentally returning too many results on large datasets, since finding all vectors within a distance threshold requires calculating distance to every vector in the table.
This fork adds several powerful features for production use:
Filter results by distance thresholds using >, >=, <, <= operators on the distance column:
-- KNN query with distance constraint
-- Requests k=10 neighbors, but only returns those with distance < 1.5
select rowid, distance
from vec_examples
where sample_embedding match '[0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5]'
and k = 10
and distance < 1.5
order by distance;
/*
┌───────┬──────────────────┐
│ rowid │ distance │
├───────┼──────────────────┤
│ 5 │ 1.16368770599365 │
└───────┴──────────────────┘
*/
-- KNN query with range constraint: find vectors in a specific distance range
select rowid, distance
from vec_examples
where sample_embedding match '[0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5]'
and k = 20
and distance between 1.5 and 2.0
order by distance;
/*
┌───────┬──────────────────┐
│ rowid │ distance │
├───────┼──────────────────┤
│ 13 │ 1.75137972831726 │
│ 11 │ 1.83941268920898 │
│ 7 │ 1.89339029788971 │
│ 8 │ 1.92658650875092 │
│ 10 │ 1.93983662128448 │
└───────┴──────────────────┘
*/Instead of using OFFSET (which is slow for large datasets), you can use the last result's distance value as a 'cursor' to fetch the next page. This is more efficient because you're filtering directly rather than skipping rows.
-- First page: get initial results
select rowid, distance
from vec_examples
where sample_embedding match '[0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5]'
and k = 3
order by distance;
/*
┌───────┬──────────────────┐
│ rowid │ distance │
├───────┼──────────────────┤
│ 5 │ 1.16368770599365 │
│ 13 │ 1.75137972831726 │
│ 11 │ 1.83941268920898 │
└───────┴──────────────────┘
*/
-- Next page: use last distance as cursor (distance > 1.83941268920898)
select rowid, distance
from vec_examples
where sample_embedding match '[0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5]'
and k = 3
and distance > 1.83941268920898
order by distance;
/*
┌───────┬──────────────────┐
│ rowid │ distance │
├───────┼──────────────────┤
│ 7 │ 1.89339029788971 │
│ 8 │ 1.92658650875092 │
│ 10 │ 1.93983662128448 │
└───────┴──────────────────┘
*/A DELETE that removes the last row of a chunk deletes the chunk, and
optimize compacts the chunks that deletes leave partly empty. To shrink the
database file:
-- Before creating vec tables: enable autovacuum and apply it (recommended)
PRAGMA auto_vacuum = FULL; -- or INCREMENTAL
VACUUM; -- activates the setting
-- Use WAL for better concurrency
PRAGMA journal_mode = WAL;After deletes, reclaim space:
-- Compact shadow tables
INSERT INTO vec_examples(vec_examples) VALUES('optimize');
-- Flush WAL
PRAGMA wal_checkpoint(TRUNCATE);
-- Reclaim freed pages (if using auto_vacuum=INCREMENTAL)
PRAGMA incremental_vacuum;
-- If you did NOT enable autovacuum, run VACUUM (after checkpoint) to shrink the file.
-- With autovacuum on, VACUUM is optional.
VACUUM;VACUUM should not corrupt vec tables; a checkpoint first is recommended when
using WAL so the rewrite starts from a clean state.
optimize, like the oversample=N command of a rescore index and the
search_list_size=N commands of a DiskANN index, runs through a hidden column
named after the table, so a vec0 table cannot have another column with its
name. CREATE rejects tables such as
CREATE VIRTUAL TABLE emb USING vec0(emb float[4]), and tables named
distance, k, mmr_lambda, or (without a primary key column) rowid, which
are names of columns vec0 always declares. Older releases allowed these
names. Such tables still open, but without optimize; rename the
table to get it back:
ALTER TABLE emb RENAME TO emb_vectors;
INSERT INTO emb_vectors(emb_vectors) VALUES('optimize');A float vector column declared indexed by rescore(quantizer=bit) or
indexed by rescore(quantizer=int8) also keeps each vector quantized, to 1 bit
or 8 bits per dimension. A KNN query scans the quantized vectors for the
k * oversample nearest rows (5 * k * oversample with mmr_lambda below 1,
at most 4096), then ranks those candidates by the distances of their float
vectors. The distances it returns are exact, and its rows are those of an exact
search whenever the nearest rows are among the candidates:
create virtual table vec_rescore using vec0(
sample_embedding float[8] indexed by rescore(quantizer=bit)
);
insert into vec_rescore(rowid, sample_embedding)
select rowid, sample_embedding from vec_examples;
select rowid, distance
from vec_rescore
where sample_embedding match '[0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5]'
and k = 3;
/*
┌───────┬──────────────────┐
│ rowid │ distance │
├───────┼──────────────────┤
│ 5 │ 1.16368770599365 │
│ 13 │ 1.75137972831726 │
│ 11 │ 1.83941268920898 │
└───────┴──────────────────┘
*/oversample is 8 unless the column sets it, as in
indexed by rescore(quantizer=bit, oversample=16), from 1 to 128. The
oversample=N command sets it for every rescore column of the table it runs
on, for the queries of the connection that runs it, until SQLite reconnects the
table, as after VACUUM, ALTER TABLE ... RENAME, or another connection's
schema change. With an oversample of 1, the query above keeps 3 candidates: rows
5 and 13, at bit distance 1, and row 16, which the scan keeps of the five rows
tied at bit distance 3, so row 11 is not among them:
insert into vec_rescore(vec_rescore) values ('oversample=1');
select rowid, distance
from vec_rescore
where sample_embedding match '[0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5]'
and k = 3;
/*
┌───────┬──────────────────┐
│ rowid │ distance │
├───────┼──────────────────┤
│ 5 │ 1.16368770599365 │
│ 13 │ 1.75137972831726 │
│ 16 │ 2.36842274665833 │
└───────┴──────────────────┘
*/quantizer=bitsets one bit for each value >= 0, and needs a number of dimensions divisible by 8.quantizer=int8maps [-1, 1] onto int8 values, -1 to -128 and 1 to 126, and clamps its results to [-128, 127], so it suits vectors whose values lie in [-1, 1], such as normalized embeddings.- Distance constraints apply only to the candidates, by their exact distances.
A query with
distance > x, as in cursor-based pagination, returns fewer thankrows once fewer thankcandidates are farther thanx, so paging that way reaches at most as many rows as there are candidates, not every row. - A table with a rescore column cannot have metadata or partition key columns. Auxiliary columns work.
indexed by flat()declares the exhaustive search that a column without the clause gets.
A float or int8 vector column declared
indexed by diskann(neighbor_quantizer=binary) or
indexed by diskann(neighbor_quantizer=int8) keeps a graph of its rows, after
the LM-DiskANN paper: each row links to up to n_neighbors others and keeps
their vectors quantized. A KNN query walks the graph from one entry row,
keeping the search_list_size nearest rows it has seen, and returns the rows
it reached, ranked by their exact distances. It reads a few hundred rows of a large
table where a flat column reads them all, and its distances are exact, but a
row it never reaches is missing from its results:
create virtual table vec_diskann using vec0(
sample_embedding float[8] indexed by diskann(neighbor_quantizer=binary, n_neighbors=8)
);
insert into vec_diskann(rowid, sample_embedding)
select rowid, sample_embedding from vec_examples;
select rowid, distance
from vec_diskann
where sample_embedding match '[0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5]'
and k = 3;
/*
┌───────┬──────────────────┐
│ rowid │ distance │
├───────┼──────────────────┤
│ 5 │ 1.16368770599365 │
│ 13 │ 1.75137972831726 │
│ 11 │ 1.83941268920898 │
└───────┴──────────────────┘
*/neighbor_quantizer=binarykeeps a bit for each value > 0, and needs a number of dimensions divisible by 8.neighbor_quantizer=int8keeps a byte per dimension: for afloatcolumn each value mapped as rescore'squantizer=int8maps it, -1 to -128 and 1 to 126, with results clamped to [-128, 127], and for anint8column its own values.n_neighborsis 72 unless the column sets it, a multiple of 8 up to 256.search_list_sizeis 128 unless the column sets it, for inserts and queries alike, orsearch_list_size_searchandsearch_list_size_insertset the two apart. A query keeps at leastkrows. The commandssearch_list_size=N,search_list_size_search=Nandsearch_list_size_insert=Nset them for every DiskANN column, as ininsert into vec_diskann(vec_diskann) values ('search_list_size_search=256'), for the connection that runs them until SQLite reconnects the table, as afterVACUUM,ALTER TABLE ... RENAME, or another connection's schema change. A table with no DiskANN column has no such command.- With
buffer_threshold=N, inserted rows wait in a buffer and join the graphNat a time. A KNN query compares every row in the buffer by its exact distance. - With
rowid = ?orrowid in (...), a KNN query ranks the rows they name by their exact distances, without the graph. - Distance constraints apply only to the rows the search found (the larger of
search_list_sizeandk, withkfive times larger whenmmr_lambdais below 1, plus every row in the buffer), by their exact distances. A query withdistance > x, as in cursor-based pagination, returns fewer thankrows once fewer thankof them are farther thanx, so paging that way reaches at most that many rows, not every row. MMR reranks them as it does any KNN query. - A table with a DiskANN column cannot have metadata or partition key columns,
and a
bitcolumn cannot be indexed by DiskANN. Auxiliary columns work. UPDATEof a DiskANN column's vector fails, since its row's links were chosen for the vector it has; delete the row and insert it again.- A
DELETEgives each of the row's neighbors, in the slot that named the row, one of the row's other neighbors it lacks, and reads every node of the graph to remove the links to the row.
Note
The sponsors listed below support the original asg017/sqlite-vec project by Alex Garcia, not this community fork.
Development of the original sqlite-vec is supported by multiple generous sponsors! Mozilla
is the main sponsor through the new Builders project.
sqlite-vec is also sponsored by the following companies:
As well as multiple individual supporters on Github sponsors!
If your company interested in sponsoring sqlite-vec development, send me an
email to get more info: https://alexgarcia.xyz
For full API reference and guides, see the upstream sqlite-vec documentation.
sqlite-ecosystem, Maybe more 3rd party SQLite extensions I've developedsqlite-rembed, Generate text embeddings from remote APIs like OpenAI/Nomic/Ollama, meant for testing and SQL scriptssqlite-lembed, Generate text embeddings locally from embedding models in the.ggufformat