Sketch2 DuckDB Extension
April 29, 2026 ยท View on GitHub
This repository contains a DuckDB extension that lets DuckDB query existing Sketch2 datasets.
Sketch2 is intentionally focused on vector storage and vector search, while DuckDB is strong at SQL, joins, analytics, and metadata processing. This extension connects those two pieces: Sketch2 does the nearest-neighbor work, and DuckDB gives you a convenient relational surface around it.
This repository started from https://github.com/duckdb/extension-template and then evolved into a Sketch2-specific extension with:
- session-scoped dataset opening
- KNN search from DuckDB
- named allow-list pushdown through
sketch2_bitset_filter - query vectors accepted as Sketch2 text or DuckDB float arrays
Why This Extension Exists
Sketch2 documentation describes Sketch2 as a vector storage and compute engine, not a general-purpose database. That design is deliberate: Sketch2 optimizes how vectors are stored, scanned, and scored on real hardware, while a host database is expected to own relational querying, joins, metadata filters, and result shaping.
DuckDB is a very good fit for that host-database role:
- you can keep metadata in normal DuckDB tables
- you can query Sketch2 from SQL instead of writing custom C or Python code
- you can join KNN results with structured data
- you can generate candidate id sets in DuckDB and push them down into Sketch2
In short, this extension exists so users can treat Sketch2 as a specialized vector engine inside DuckDB workflows.
What The Extension Provides
Today this extension is a query-side integration. It does not create datasets or write vectors from DuckDB. Instead, it assumes the Sketch2 dataset already exists and exposes the read/query path inside DuckDB.
The current SQL surface is:
sketch2_version(): returns the Sketch2 library versionsketch2_dataset(): returns the currently opened dataset name for the current connectionPRAGMA sketch2_open(database_path, dataset_name): opens a Sketch2 dataset for the current DuckDB connectionPRAGMA sketch2_close: closes the currently opened Sketch2 dataset for the current DuckDB connectionsketch2_knn(query_vector, k, bitset_filter_name): returns top-k nearest neighbors as(id, score)sketch2_bitset_filter(id, name): aggregate that turns a DuckDB result set of ids into a named Sketch2 allow-listsketch2_bitset_load(name): validates a named Sketch2 allow-listsketch2_bitset_drop(name): removes a named Sketch2 allow-listsketch2_bitset_cache_remove(name): removes one named bitset filter from Sketch2's in-process cachesketch2_bitset_cache_clear(): clears Sketch2's in-process bitset filter cache
How It Works
The integration follows the same separation of responsibilities that Sketch2 already uses for SQLite:
- Sketch2 owns the dataset files, read path, and scoring.
- DuckDB owns SQL execution, joins, metadata filters, and application-facing query composition.
- The extension keeps a Sketch2 handle in DuckDB client context state, so the opened dataset belongs to the current DuckDB connection.
sketch2_knncalls into the Sketch2 C API and returns rows back to DuckDB as a table function.sketch2_bitset_filterbuilds a named Sketch2 bitset filter from ids produced by a DuckDB query. Sketch2 owns the persisted filter, andsketch2_knnlater loads it by name.
Important behavioral details:
- one DuckDB connection tracks one active opened Sketch2 dataset at a time
- opening a new dataset replaces the previous handle in that connection
- bitset filters are named and owned by Sketch2
- named filters can be reused across DuckDB queries
sketch2_bitset_load(name)validates a named filter explicitlysketch2_bitset_drop(name)removes a named filter when it is no longer neededsketch2_bitset_cache_remove(name)evicts one named filter from Sketch2's cachesketch2_bitset_cache_clear()evicts all named filters from Sketch2's cache- this extension is read/query oriented; dataset creation and mutation still happen through Sketch2 itself
Building
Managing dependencies
This extension depends on an external Sketch2 build. Before configuring or
building, set SKETCH2_ROOT to the root of your Sketch2 repository:
export SKETCH2_ROOT=/path/to/sketch2
During the build, this project uses:
"$SKETCH2_ROOT/install/include"as an additional compiler include path"$SKETCH2_ROOT/install/lib"to findlibsketch2.sofor linking
That means Sketch2 should already be built and installed into the install
subdirectory under SKETCH2_ROOT.
Build steps
Build the extension with:
make
The main binaries that will be built are:
./build/release/duckdb
./build/release/test/unittest
./build/release/extension/sketch2/sketch2.duckdb_extension
duckdbis the DuckDB shell with the extension code already linked inunittestis DuckDB's test runner with the extension linked insketch2.duckdb_extensionis the loadable extension artifact
Using The Extension In DuckDB
If you use the shell built by this repository, start:
./build/release/duckdb
If you use another DuckDB client, load the built extension artifact:
LOAD '/absolute/path/to/build/release/extension/sketch2/sketch2.duckdb_extension';
The extension queries an existing Sketch2 dataset. In Sketch2 terminology:
database_pathis the Sketch2 database root directorydataset_nameis the dataset to open inside that root
SQL API Reference
sketch2_version()
Scalar function that returns the Sketch2 library version.
Example:
SELECT sketch2_version();
sketch2_dataset()
Scalar function that returns the currently opened dataset name.
Errors:
- raises an error if no dataset is open on the current connection
Example:
SELECT sketch2_dataset();
PRAGMA sketch2_open(database_path, dataset_name)
PRAGMA that opens a Sketch2 dataset for the current DuckDB connection.
PRAGMA sketch2_open('/mnt/nvme/sketch2/db', 'items');
Notes:
- arguments must be non-
NULL - the dataset must already exist
sketch2_knnrequiresPRAGMA sketch2_open(...)to be called first
PRAGMA sketch2_close
PRAGMA that closes the currently opened Sketch2 dataset for the current DuckDB connection. If no dataset is open, this is a no-op.
Example:
PRAGMA sketch2_close;
sketch2_knn(query_vector, k, bitset_filter_name)
Table function that returns nearest neighbors with schema:
(id UBIGINT, score DOUBLE)
Arguments:
query_vector: the query vectork: number of neighbors to return, must be> 0and<= 1,000,000bitset_filter_name: optional name of a filter created bysketch2_bitset_filter(id, name); passNULLfor no filter
Supported query-vector formats:
- Sketch2 text format as
VARCHAR - DuckDB
FLOAT[] - DuckDB
DOUBLE[] - DuckDB float/double
ARRAY
For VARCHAR, the extension forwards the string to Sketch2's normal parser, so
the accepted text forms are whatever Sketch2 supports for the dataset, such as
comma-delimited values like '1.0, 2.0, 3.0, 4.0'.
Examples:
SELECT *
FROM sketch2_knn('1.0, 2.0, 3.0, 4.0', 5, NULL);
SELECT *
FROM sketch2_knn([1.0, 2.0, 3.0, 4.0]::FLOAT[], 5, NULL);
SELECT *
FROM sketch2_knn([1.0, 2.0, 3.0, 4.0]::FLOAT[], 5, 'books_filter');
Score ordering depends on the Sketch2 dataset metric:
- for
l2andcos, smaller scores are better, so useORDER BY score ASC - for
dot, larger scores are better, so useORDER BY score DESC
sketch2_bitset_filter(id, name)
Aggregate function that turns a set of ids into a named Sketch2 allow-list and echoes the provided filter name on success.
Requirements and limits:
- input ids must be non-negative
BIGINT namemust be a constant, non-NULL, non-emptyVARCHAR- rows with
NULLids are ignored - at most 1,000,000 ids are allowed per aggregate group
- the named filter is persisted by Sketch2 and can be reused by later queries
Example:
SELECT sketch2_bitset_filter(id, 'books_filter')
FROM metadata
WHERE category = 'books';
DuckDB does not require ids passed to sketch2_bitset_filter(id, name) to be
pre-sorted. The aggregate collects ids during execution and hands them to
Sketch2's builder at finalize time, and Sketch2 owns the resulting named
filter.
sketch2_bitset_load(name)
Scalar function that validates a named Sketch2 allow-list. sketch2_knn(..., bitset_filter_name) can load filters lazily by name, so this function is mainly
useful as an explicit preflight step.
Arguments:
name: constant, non-NULL, non-empty name of the filter to load
Returns the filter name on success. Missing, invalid, or malformed filters raise a DuckDB error.
Example:
SELECT sketch2_bitset_load('books_filter');
sketch2_bitset_drop(name)
Scalar function that removes a named Sketch2 allow-list.
Arguments:
name: constant, non-NULL, non-empty name of the filter to remove
Returns 1 when a filter file was removed and 0 when there was no matching
filter.
Example:
SELECT sketch2_bitset_drop('books_filter');
sketch2_bitset_cache_remove(name)
Scalar function that evicts one named Sketch2 allow-list from Sketch2's in-process cache. The on-disk filter file is not removed.
Arguments:
name: constant, non-NULL, non-empty name of the cached filter to remove
Returns 1 when a cache entry was removed and 0 when there was no matching
cached entry.
Example:
SELECT sketch2_bitset_cache_remove('books_filter');
sketch2_bitset_cache_clear()
Scalar function that clears Sketch2's in-process bitset-filter cache.
Returns true on success.
Example:
SELECT sketch2_bitset_cache_clear();
Query Examples
1. Open a dataset and inspect state
PRAGMA sketch2_open('/mnt/nvme/sketch2/db', 'items');
SELECT sketch2_version() AS sketch2_version;
SELECT sketch2_dataset() AS opened_dataset;
2. Basic KNN query for an l2 or cos dataset
SELECT id, score
FROM sketch2_knn(
[7.4, 7.4, 7.4, 7.4]::FLOAT[],
5,
NULL
)
ORDER BY score, id;
3. Basic KNN query for a dot dataset
SELECT id, score
FROM sketch2_knn(
'1.0, 1.0, 1.0, 1.0',
5,
NULL
)
ORDER BY score DESC, id;
dot uses similarity-style scoring rather than distance-style scoring, so
higher scores are better matches. That is why this query orders by score DESC
instead of ascending order.
4. Join KNN results with DuckDB metadata
CREATE TABLE metadata (
id BIGINT PRIMARY KEY,
title VARCHAR,
category VARCHAR
);
SELECT n.id, n.score, m.title, m.category
FROM sketch2_knn([7.4, 7.4, 7.4, 7.4]::FLOAT[], 5, NULL) AS n
JOIN metadata AS m ON m.id = n.id
ORDER BY n.score, n.id;
This is the main value of the integration: Sketch2 handles the vector search, while DuckDB handles metadata joins and the rest of the SQL pipeline.
5. Push a metadata-derived allow-list into Sketch2
The name-based filter workflow is:
- build a named filter from DuckDB rows
- pass that filter name into
sketch2_knn
Step 1:
SELECT sketch2_bitset_filter(id, 'books_filter') AS filter_name
FROM metadata
WHERE category = 'books';
Step 2:
SELECT n.id, n.score, m.title
FROM sketch2_knn([7.4, 7.4, 7.4, 7.4]::FLOAT[], 5, 'books_filter') AS n
JOIN metadata AS m ON m.id = n.id
ORDER BY n.score, n.id;
For example, from Python:
import duckdb
con = duckdb.connect(":memory:", config={"allow_unsigned_extensions": "true"})
con.execute(
"LOAD '/absolute/path/to/build/release/extension/sketch2/sketch2.duckdb_extension'"
)
con.execute("PRAGMA sketch2_open(?, ?)", ["/mnt/nvme/sketch2/db", "items"])
filter_name = con.execute(
"""
SELECT sketch2_bitset_filter(id, ?)
FROM metadata
WHERE category = 'books'
""",
["books_filter"],
).fetchone()[0]
rows = con.execute(
"""
SELECT n.id, n.score, m.title
FROM sketch2_knn(?, ?, ?) AS n
JOIN metadata AS m ON m.id = n.id
ORDER BY n.score, n.id
""",
[[7.4, 7.4, 7.4, 7.4], 5, filter_name],
).fetchall()
This pattern is useful when DuckDB can cheaply derive a candidate id set from relational predicates and Sketch2 should search only within that subset.
Named filters can be dropped when they are no longer needed:
SELECT sketch2_bitset_drop('books_filter');
What This Extension Does Not Do
To avoid confusion, the current extension does not yet expose:
- dataset creation from DuckDB
- staged writes, deletes, or merges from DuckDB
- dataset management DDL
- automatic one-statement filter pushdown equivalent to Sketch2's SQLite virtual-table hidden columns
Those capabilities still live in Sketch2's own C/Python APIs and other integrations.
Running The Tests
The primary extension tests are SQLLogicTests in ./test/sql:
make test
Those test targets generate the SQL fixture dataset under test/generated/
before running the SQL tests.
For Python integration tests, first install the repo-local test dependencies:
make python-test-deps
Then run:
make pytest
Installing Deployed Binaries
To install your extension binaries from S3, DuckDB must be launched with
allow_unsigned_extensions enabled. How to do that depends on the client.
CLI:
duckdb -unsigned
Python:
con = duckdb.connect(':memory:', config={'allow_unsigned_extensions': 'true'})
NodeJS:
db = new duckdb.Database(':memory:', {"allow_unsigned_extensions": "true"});
Then set the custom extension repository:
SET custom_extension_repository='bucket.s3.eu-west-1.amazonaws.com/<your_extension_name>/latest';
After that you can install and load the extension with normal DuckDB commands:
INSTALL sketch2;
LOAD sketch2;