Add DiskANN index for vec0 virtual table

Add DiskANN graph-based index: builds a Vamana graph with configurable R
(max degree) and L (search list size, separate for insert/query), supports
int8 quantization with rescore, lazy reverse-edge replacement, pre-quantized
query optimization, and insert buffer reuse. Includes shadow table management,
delete support, KNN integration, compile flag (SQLITE_VEC_ENABLE_DISKANN),
release-demo workflow, fuzz targets, and tests. Fixes rescore int8
quantization bug.
This commit is contained in:
Alex Garcia 2026-03-29 19:46:53 -07:00
parent e2c38f387c
commit 575371d751
23 changed files with 6550 additions and 135 deletions

View file

@ -61,6 +61,10 @@ SQLITE_EXTENSION_INIT1
#define LONGDOUBLE_TYPE long double
#endif
#ifndef SQLITE_VEC_ENABLE_DISKANN
#define SQLITE_VEC_ENABLE_DISKANN 1
#endif
#ifndef _WIN32
#ifndef __EMSCRIPTEN__
#ifndef __COSMOPOLITAN__
@ -2544,6 +2548,7 @@ enum Vec0IndexType {
VEC0_INDEX_TYPE_RESCORE = 2,
#endif
VEC0_INDEX_TYPE_IVF = 3,
VEC0_INDEX_TYPE_DISKANN = 4,
};
#if SQLITE_VEC_ENABLE_RESCORE
@ -2575,6 +2580,75 @@ struct Vec0IvfConfig {
struct Vec0IvfConfig { char _unused; };
#endif
// ============================================================
// DiskANN types and constants
// ============================================================
#define VEC0_DISKANN_DEFAULT_N_NEIGHBORS 72
#define VEC0_DISKANN_MAX_N_NEIGHBORS 256
#define VEC0_DISKANN_DEFAULT_SEARCH_LIST_SIZE 128
#define VEC0_DISKANN_DEFAULT_ALPHA 1.2f
/**
* Quantizer type used for compressing neighbor vectors in the DiskANN graph.
*/
enum Vec0DiskannQuantizerType {
VEC0_DISKANN_QUANTIZER_BINARY = 1, // 1 bit per dimension (1/32 compression)
VEC0_DISKANN_QUANTIZER_INT8 = 2, // 1 byte per dimension (1/4 compression)
};
/**
* Configuration for a DiskANN index on a single vector column.
* Parsed from `INDEXED BY diskann(neighbor_quantizer=binary, n_neighbors=72)`.
*/
struct Vec0DiskannConfig {
// Quantizer type for neighbor vectors
enum Vec0DiskannQuantizerType quantizer_type;
// Maximum number of neighbors per node (R in the paper). Must be divisible by 8.
int n_neighbors;
// Search list size (L in the paper) — unified default for both insert and query.
int search_list_size;
// Per-path overrides (0 = fall back to search_list_size).
int search_list_size_search;
int search_list_size_insert;
// Alpha parameter for RobustPrune (distance scaling factor, typically 1.0-1.5)
f32 alpha;
// Buffer threshold for batched inserts. When > 0, inserts go into a flat
// buffer table and are flushed into the graph when the buffer reaches this
// size. 0 = disabled (legacy per-row insert behavior).
int buffer_threshold;
};
/**
* Represents a single candidate during greedy beam search.
* Used in priority queues / sorted arrays during LM-Search.
*/
struct Vec0DiskannCandidate {
i64 rowid;
f32 distance;
int visited; // 1 if this candidate's neighbors have been explored
};
/**
* Returns the byte size of a quantized vector for the given quantizer type
* and number of dimensions.
*/
size_t diskann_quantized_vector_byte_size(
enum Vec0DiskannQuantizerType quantizer_type, size_t dimensions) {
switch (quantizer_type) {
case VEC0_DISKANN_QUANTIZER_BINARY:
return dimensions / CHAR_BIT; // 1 bit per dimension
case VEC0_DISKANN_QUANTIZER_INT8:
return dimensions * sizeof(i8); // 1 byte per dimension
}
return 0;
}
struct VectorColumnDefinition {
char *name;
int name_length;
@ -2586,6 +2660,7 @@ struct VectorColumnDefinition {
struct Vec0RescoreConfig rescore;
#endif
struct Vec0IvfConfig ivf;
struct Vec0DiskannConfig diskann;
};
struct Vec0PartitionColumnDefinition {
@ -2743,6 +2818,126 @@ static int vec0_parse_ivf_options(struct Vec0Scanner *scanner,
struct Vec0IvfConfig *config);
#endif
/**
* Parse the options inside diskann(...) parentheses.
* Scanner should be positioned right before the '(' token.
*
* Recognized options:
* neighbor_quantizer = binary | int8 (required)
* n_neighbors = <integer> (optional, default 72)
* search_list_size = <integer> (optional, default 128)
*/
static int vec0_parse_diskann_options(struct Vec0Scanner *scanner,
struct Vec0DiskannConfig *config) {
int rc;
struct Vec0Token token;
int hasQuantizer = 0;
// Set defaults
config->n_neighbors = VEC0_DISKANN_DEFAULT_N_NEIGHBORS;
config->search_list_size = VEC0_DISKANN_DEFAULT_SEARCH_LIST_SIZE;
config->search_list_size_search = 0;
config->search_list_size_insert = 0;
config->alpha = VEC0_DISKANN_DEFAULT_ALPHA;
config->buffer_threshold = 0;
int hasSearchListSize = 0;
int hasSearchListSizeSplit = 0;
// Expect '('
rc = vec0_scanner_next(scanner, &token);
if (rc != VEC0_TOKEN_RESULT_SOME || token.token_type != TOKEN_TYPE_LPAREN) {
return SQLITE_ERROR;
}
while (1) {
// key
rc = vec0_scanner_next(scanner, &token);
if (rc == VEC0_TOKEN_RESULT_SOME && token.token_type == TOKEN_TYPE_RPAREN) {
break; // empty parens or trailing comma
}
if (rc != VEC0_TOKEN_RESULT_SOME || token.token_type != TOKEN_TYPE_IDENTIFIER) {
return SQLITE_ERROR;
}
char *optKey = token.start;
int optKeyLen = token.end - token.start;
// '='
rc = vec0_scanner_next(scanner, &token);
if (rc != VEC0_TOKEN_RESULT_SOME || token.token_type != TOKEN_TYPE_EQ) {
return SQLITE_ERROR;
}
// value (identifier or digit)
rc = vec0_scanner_next(scanner, &token);
if (rc != VEC0_TOKEN_RESULT_SOME) {
return SQLITE_ERROR;
}
char *optVal = token.start;
int optValLen = token.end - token.start;
if (sqlite3_strnicmp(optKey, "neighbor_quantizer", optKeyLen) == 0) {
if (sqlite3_strnicmp(optVal, "binary", optValLen) == 0) {
config->quantizer_type = VEC0_DISKANN_QUANTIZER_BINARY;
} else if (sqlite3_strnicmp(optVal, "int8", optValLen) == 0) {
config->quantizer_type = VEC0_DISKANN_QUANTIZER_INT8;
} else {
return SQLITE_ERROR; // unknown quantizer
}
hasQuantizer = 1;
} else if (sqlite3_strnicmp(optKey, "n_neighbors", optKeyLen) == 0) {
config->n_neighbors = atoi(optVal);
if (config->n_neighbors <= 0 || (config->n_neighbors % 8) != 0 ||
config->n_neighbors > VEC0_DISKANN_MAX_N_NEIGHBORS) {
return SQLITE_ERROR;
}
} else if (sqlite3_strnicmp(optKey, "search_list_size_search", optKeyLen) == 0 && optKeyLen == 23) {
config->search_list_size_search = atoi(optVal);
if (config->search_list_size_search <= 0) {
return SQLITE_ERROR;
}
hasSearchListSizeSplit = 1;
} else if (sqlite3_strnicmp(optKey, "search_list_size_insert", optKeyLen) == 0 && optKeyLen == 23) {
config->search_list_size_insert = atoi(optVal);
if (config->search_list_size_insert <= 0) {
return SQLITE_ERROR;
}
hasSearchListSizeSplit = 1;
} else if (sqlite3_strnicmp(optKey, "search_list_size", optKeyLen) == 0) {
config->search_list_size = atoi(optVal);
if (config->search_list_size <= 0) {
return SQLITE_ERROR;
}
hasSearchListSize = 1;
} else if (sqlite3_strnicmp(optKey, "buffer_threshold", optKeyLen) == 0) {
config->buffer_threshold = atoi(optVal);
if (config->buffer_threshold < 0) {
return SQLITE_ERROR;
}
} else {
return SQLITE_ERROR; // unknown option
}
// Expect ',' or ')'
rc = vec0_scanner_next(scanner, &token);
if (rc == VEC0_TOKEN_RESULT_SOME && token.token_type == TOKEN_TYPE_RPAREN) {
break;
}
if (rc != VEC0_TOKEN_RESULT_SOME || token.token_type != TOKEN_TYPE_COMMA) {
return SQLITE_ERROR;
}
}
if (!hasQuantizer) {
return SQLITE_ERROR; // neighbor_quantizer is required
}
if (hasSearchListSize && hasSearchListSizeSplit) {
return SQLITE_ERROR; // cannot mix search_list_size with search_list_size_search/insert
}
return SQLITE_OK;
}
int vec0_parse_vector_column(const char *source, int source_length,
struct VectorColumnDefinition *outColumn) {
// parses a vector column definition like so:
@ -2763,8 +2958,9 @@ int vec0_parse_vector_column(const char *source, int source_length,
#endif
struct Vec0IvfConfig ivfConfig;
memset(&ivfConfig, 0, sizeof(ivfConfig));
struct Vec0DiskannConfig diskannConfig;
memset(&diskannConfig, 0, sizeof(diskannConfig));
int dimensions;
vec0_scanner_init(&scanner, source, source_length);
// starts with an identifier
@ -2931,6 +3127,16 @@ int vec0_parse_vector_column(const char *source, int source_length,
}
#else
return SQLITE_ERROR; // IVF not compiled in
#endif
} else if (sqlite3_strnicmp(token.start, "diskann", indexNameLen) == 0) {
#if SQLITE_VEC_ENABLE_DISKANN
indexType = VEC0_INDEX_TYPE_DISKANN;
rc = vec0_parse_diskann_options(&scanner, &diskannConfig);
if (rc != SQLITE_OK) {
return rc;
}
#else
return SQLITE_ERROR;
#endif
} else {
// unknown index type
@ -2956,6 +3162,7 @@ int vec0_parse_vector_column(const char *source, int source_length,
outColumn->rescore = rescoreConfig;
#endif
outColumn->ivf = ivfConfig;
outColumn->diskann = diskannConfig;
return SQLITE_OK;
}
@ -3154,6 +3361,7 @@ static sqlite3_module vec_eachModule = {
#pragma endregion
#pragma region vec0 virtual table
#define VEC0_COLUMN_ID 0
@ -3214,6 +3422,9 @@ static sqlite3_module vec_eachModule = {
#define VEC0_SHADOW_AUXILIARY_NAME "\"%w\".\"%w_auxiliary\""
#define VEC0_SHADOW_METADATA_N_NAME "\"%w\".\"%w_metadatachunks%02d\""
#define VEC0_SHADOW_VECTORS_N_NAME "\"%w\".\"%w_vectors%02d\""
#define VEC0_SHADOW_DISKANN_NODES_N_NAME "\"%w\".\"%w_diskann_nodes%02d\""
#define VEC0_SHADOW_DISKANN_BUFFER_N_NAME "\"%w\".\"%w_diskann_buffer%02d\""
#define VEC0_SHADOW_METADATA_TEXT_DATA_NAME "\"%w\".\"%w_metadatatext%02d\""
#define VEC_INTERAL_ERROR "Internal sqlite-vec error: "
@ -3388,6 +3599,24 @@ struct vec0_vtab {
* Must be cleaned up with sqlite3_finalize().
*/
sqlite3_stmt *stmtRowidsGetChunkPosition;
// === DiskANN additions ===
#if SQLITE_VEC_ENABLE_DISKANN
// Shadow table names for DiskANN, per vector column
// e.g., "{schema}"."{table}_vectors{00..15}"
char *shadowVectorsNames[VEC0_MAX_VECTOR_COLUMNS];
// e.g., "{schema}"."{table}_diskann_nodes{00..15}"
char *shadowDiskannNodesNames[VEC0_MAX_VECTOR_COLUMNS];
// Prepared statements for DiskANN operations (per vector column)
// These will be lazily prepared on first use.
sqlite3_stmt *stmtDiskannNodeRead[VEC0_MAX_VECTOR_COLUMNS];
sqlite3_stmt *stmtDiskannNodeWrite[VEC0_MAX_VECTOR_COLUMNS];
sqlite3_stmt *stmtDiskannNodeInsert[VEC0_MAX_VECTOR_COLUMNS];
sqlite3_stmt *stmtVectorsRead[VEC0_MAX_VECTOR_COLUMNS];
sqlite3_stmt *stmtVectorsInsert[VEC0_MAX_VECTOR_COLUMNS];
#endif
};
#if SQLITE_VEC_ENABLE_RESCORE
@ -3427,6 +3656,13 @@ void vec0_free_resources(vec0_vtab *p) {
sqlite3_finalize(p->stmtIvfRowidMapLookup[i]); p->stmtIvfRowidMapLookup[i] = NULL;
sqlite3_finalize(p->stmtIvfRowidMapDelete[i]); p->stmtIvfRowidMapDelete[i] = NULL;
sqlite3_finalize(p->stmtIvfCentroidsAll[i]); p->stmtIvfCentroidsAll[i] = NULL;
#if SQLITE_VEC_ENABLE_DISKANN
sqlite3_finalize(p->stmtDiskannNodeRead[i]); p->stmtDiskannNodeRead[i] = NULL;
sqlite3_finalize(p->stmtDiskannNodeWrite[i]); p->stmtDiskannNodeWrite[i] = NULL;
sqlite3_finalize(p->stmtDiskannNodeInsert[i]); p->stmtDiskannNodeInsert[i] = NULL;
sqlite3_finalize(p->stmtVectorsRead[i]); p->stmtVectorsRead[i] = NULL;
sqlite3_finalize(p->stmtVectorsInsert[i]); p->stmtVectorsInsert[i] = NULL;
#endif
}
#endif
}
@ -3464,6 +3700,13 @@ void vec0_free(vec0_vtab *p) {
p->shadowRescoreVectorsNames[i] = NULL;
#endif
#if SQLITE_VEC_ENABLE_DISKANN
sqlite3_free(p->shadowVectorsNames[i]);
p->shadowVectorsNames[i] = NULL;
sqlite3_free(p->shadowDiskannNodesNames[i]);
p->shadowDiskannNodesNames[i] = NULL;
#endif
sqlite3_free(p->vector_columns[i].name);
p->vector_columns[i].name = NULL;
}
@ -3484,6 +3727,12 @@ void vec0_free(vec0_vtab *p) {
}
}
#if SQLITE_VEC_ENABLE_DISKANN
#include "sqlite-vec-diskann.c"
#else
static int vec0_all_columns_diskann(vec0_vtab *p) { (void)p; return 0; }
#endif
int vec0_num_defined_user_columns(vec0_vtab *p) {
return p->numVectorColumns + p->numPartitionColumns + p->numAuxiliaryColumns + p->numMetadataColumns;
}
@ -3753,6 +4002,25 @@ int vec0_get_vector_data(vec0_vtab *pVtab, i64 rowid, int vector_column_idx,
void **outVector, int *outVectorSize) {
vec0_vtab *p = pVtab;
int rc, brc;
#if SQLITE_VEC_ENABLE_DISKANN
// DiskANN fast path: read from _vectors table
if (p->vector_columns[vector_column_idx].index_type == VEC0_INDEX_TYPE_DISKANN) {
void *vec = NULL;
int vecSize;
rc = diskann_vector_read(p, vector_column_idx, rowid, &vec, &vecSize);
if (rc != SQLITE_OK) {
vtab_set_error(&pVtab->base,
"Could not fetch vector data for %lld from DiskANN vectors table",
rowid);
return SQLITE_ERROR;
}
*outVector = vec;
if (outVectorSize) *outVectorSize = vecSize;
return SQLITE_OK;
}
#endif
i64 chunk_id;
i64 chunk_offset;
@ -4653,6 +4921,26 @@ static int vec0_init(sqlite3 *db, void *pAux, int argc, const char *const *argv,
(i64)vecColumn.dimensions, SQLITE_VEC_VEC0_MAX_DIMENSIONS);
goto error;
}
// DiskANN validation
if (vecColumn.index_type == VEC0_INDEX_TYPE_DISKANN) {
if (vecColumn.element_type == SQLITE_VEC_ELEMENT_TYPE_BIT) {
sqlite3_free(vecColumn.name);
*pzErr = sqlite3_mprintf(
VEC_CONSTRUCTOR_ERROR
"DiskANN index is not supported on bit vector columns");
goto error;
}
if (vecColumn.diskann.quantizer_type == VEC0_DISKANN_QUANTIZER_BINARY &&
(vecColumn.dimensions % CHAR_BIT) != 0) {
sqlite3_free(vecColumn.name);
*pzErr = sqlite3_mprintf(
VEC_CONSTRUCTOR_ERROR
"DiskANN with binary quantizer requires dimensions divisible by 8");
goto error;
}
}
pNew->user_column_kinds[user_column_idx] = SQLITE_VEC0_USER_COLUMN_KIND_VECTOR;
pNew->user_column_idxs[user_column_idx] = numVectorColumns;
memcpy(&pNew->vector_columns[numVectorColumns], &vecColumn, sizeof(vecColumn));
@ -4881,6 +5169,31 @@ static int vec0_init(sqlite3 *db, void *pAux, int argc, const char *const *argv,
}
}
// DiskANN columns cannot coexist with aux/metadata/partition columns
for (int i = 0; i < numVectorColumns; i++) {
if (pNew->vector_columns[i].index_type == VEC0_INDEX_TYPE_DISKANN) {
if (numAuxiliaryColumns > 0) {
*pzErr = sqlite3_mprintf(
VEC_CONSTRUCTOR_ERROR
"Auxiliary columns are not supported with DiskANN-indexed vector columns");
goto error;
}
if (numMetadataColumns > 0) {
*pzErr = sqlite3_mprintf(
VEC_CONSTRUCTOR_ERROR
"Metadata columns are not supported with DiskANN-indexed vector columns");
goto error;
}
if (numPartitionColumns > 0) {
*pzErr = sqlite3_mprintf(
VEC_CONSTRUCTOR_ERROR
"Partition key columns are not supported with DiskANN-indexed vector columns");
goto error;
}
break;
}
}
sqlite3_str *createStr = sqlite3_str_new(NULL);
sqlite3_str_appendall(createStr, "CREATE TABLE x(");
if (pkColumnName) {
@ -4984,6 +5297,20 @@ static int vec0_init(sqlite3 *db, void *pAux, int argc, const char *const *argv,
goto error;
}
}
#endif
#if SQLITE_VEC_ENABLE_DISKANN
if (pNew->vector_columns[i].index_type == VEC0_INDEX_TYPE_DISKANN) {
pNew->shadowVectorsNames[i] =
sqlite3_mprintf("%s_vectors%02d", tableName, i);
if (!pNew->shadowVectorsNames[i]) {
goto error;
}
pNew->shadowDiskannNodesNames[i] =
sqlite3_mprintf("%s_diskann_nodes%02d", tableName, i);
if (!pNew->shadowDiskannNodesNames[i]) {
goto error;
}
}
#endif
}
#if SQLITE_VEC_EXPERIMENTAL_IVF_ENABLE
@ -5060,7 +5387,32 @@ static int vec0_init(sqlite3 *db, void *pAux, int argc, const char *const *argv,
}
sqlite3_finalize(stmt);
#if SQLITE_VEC_ENABLE_DISKANN
// Seed medoid entries for DiskANN-indexed columns
for (int i = 0; i < pNew->numVectorColumns; i++) {
if (pNew->vector_columns[i].index_type != VEC0_INDEX_TYPE_DISKANN) {
continue;
}
char *key = sqlite3_mprintf("diskann_medoid_%02d", i);
char *zInsert = sqlite3_mprintf(
"INSERT INTO " VEC0_SHADOW_INFO_NAME "(key, value) VALUES (?1, ?2)",
pNew->schemaName, pNew->tableName);
rc = sqlite3_prepare_v2(db, zInsert, -1, &stmt, NULL);
sqlite3_free(zInsert);
if (rc != SQLITE_OK) {
sqlite3_free(key);
sqlite3_finalize(stmt);
goto error;
}
sqlite3_bind_text(stmt, 1, key, -1, sqlite3_free);
sqlite3_bind_null(stmt, 2); // NULL means empty graph
if (sqlite3_step(stmt) != SQLITE_DONE) {
sqlite3_finalize(stmt);
goto error;
}
sqlite3_finalize(stmt);
}
#endif
// create the _chunks shadow table
char *zCreateShadowChunks = NULL;
@ -5118,7 +5470,7 @@ static int vec0_init(sqlite3 *db, void *pAux, int argc, const char *const *argv,
for (int i = 0; i < pNew->numVectorColumns; i++) {
#if SQLITE_VEC_ENABLE_RESCORE
// Rescore and IVF columns don't use _vector_chunks
// Non-FLAT columns don't use _vector_chunks
if (pNew->vector_columns[i].index_type != VEC0_INDEX_TYPE_FLAT)
continue;
#endif
@ -5159,6 +5511,84 @@ static int vec0_init(sqlite3 *db, void *pAux, int argc, const char *const *argv,
}
#endif
#if SQLITE_VEC_ENABLE_DISKANN
// Create DiskANN shadow tables for indexed vector columns
for (int i = 0; i < pNew->numVectorColumns; i++) {
if (pNew->vector_columns[i].index_type != VEC0_INDEX_TYPE_DISKANN) {
continue;
}
// Create _vectors{NN} table
{
char *zSql = sqlite3_mprintf(
"CREATE TABLE " VEC0_SHADOW_VECTORS_N_NAME
" (rowid INTEGER PRIMARY KEY, vector BLOB NOT NULL);",
pNew->schemaName, pNew->tableName, i);
if (!zSql) {
goto error;
}
rc = sqlite3_prepare_v2(db, zSql, -1, &stmt, 0);
sqlite3_free(zSql);
if ((rc != SQLITE_OK) || (sqlite3_step(stmt) != SQLITE_DONE)) {
sqlite3_finalize(stmt);
*pzErr = sqlite3_mprintf(
"Could not create '_vectors%02d' shadow table: %s", i,
sqlite3_errmsg(db));
goto error;
}
sqlite3_finalize(stmt);
}
// Create _diskann_nodes{NN} table
{
char *zSql = sqlite3_mprintf(
"CREATE TABLE " VEC0_SHADOW_DISKANN_NODES_N_NAME " ("
"rowid INTEGER PRIMARY KEY, "
"neighbors_validity BLOB NOT NULL, "
"neighbor_ids BLOB NOT NULL, "
"neighbor_quantized_vectors BLOB NOT NULL"
");",
pNew->schemaName, pNew->tableName, i);
if (!zSql) {
goto error;
}
rc = sqlite3_prepare_v2(db, zSql, -1, &stmt, 0);
sqlite3_free(zSql);
if ((rc != SQLITE_OK) || (sqlite3_step(stmt) != SQLITE_DONE)) {
sqlite3_finalize(stmt);
*pzErr = sqlite3_mprintf(
"Could not create '_diskann_nodes%02d' shadow table: %s", i,
sqlite3_errmsg(db));
goto error;
}
sqlite3_finalize(stmt);
}
// Create _diskann_buffer{NN} table (for batched inserts)
{
char *zSql = sqlite3_mprintf(
"CREATE TABLE " VEC0_SHADOW_DISKANN_BUFFER_N_NAME " ("
"rowid INTEGER PRIMARY KEY, "
"vector BLOB NOT NULL"
");",
pNew->schemaName, pNew->tableName, i);
if (!zSql) {
goto error;
}
rc = sqlite3_prepare_v2(db, zSql, -1, &stmt, 0);
sqlite3_free(zSql);
if ((rc != SQLITE_OK) || (sqlite3_step(stmt) != SQLITE_DONE)) {
sqlite3_finalize(stmt);
*pzErr = sqlite3_mprintf(
"Could not create '_diskann_buffer%02d' shadow table: %s", i,
sqlite3_errmsg(db));
goto error;
}
sqlite3_finalize(stmt);
}
}
#endif
// See SHADOW_TABLE_ROWID_QUIRK in vec0_new_chunk() — same "rowid PRIMARY KEY"
// without INTEGER type issue applies here.
for (int i = 0; i < pNew->numMetadataColumns; i++) {
@ -5293,6 +5723,45 @@ static int vec0Destroy(sqlite3_vtab *pVtab) {
sqlite3_finalize(stmt);
for (int i = 0; i < p->numVectorColumns; i++) {
#if SQLITE_VEC_ENABLE_DISKANN
if (p->vector_columns[i].index_type == VEC0_INDEX_TYPE_DISKANN) {
// Drop DiskANN shadow tables
zSql = sqlite3_mprintf("DROP TABLE IF EXISTS " VEC0_SHADOW_VECTORS_N_NAME,
p->schemaName, p->tableName, i);
if (zSql) {
rc = sqlite3_prepare_v2(p->db, zSql, -1, &stmt, 0);
sqlite3_free((void *)zSql);
if ((rc != SQLITE_OK) || (sqlite3_step(stmt) != SQLITE_DONE)) {
rc = SQLITE_ERROR;
goto done;
}
sqlite3_finalize(stmt);
}
zSql = sqlite3_mprintf("DROP TABLE IF EXISTS " VEC0_SHADOW_DISKANN_NODES_N_NAME,
p->schemaName, p->tableName, i);
if (zSql) {
rc = sqlite3_prepare_v2(p->db, zSql, -1, &stmt, 0);
sqlite3_free((void *)zSql);
if ((rc != SQLITE_OK) || (sqlite3_step(stmt) != SQLITE_DONE)) {
rc = SQLITE_ERROR;
goto done;
}
sqlite3_finalize(stmt);
}
zSql = sqlite3_mprintf("DROP TABLE IF EXISTS " VEC0_SHADOW_DISKANN_BUFFER_N_NAME,
p->schemaName, p->tableName, i);
if (zSql) {
rc = sqlite3_prepare_v2(p->db, zSql, -1, &stmt, 0);
sqlite3_free((void *)zSql);
if ((rc != SQLITE_OK) || (sqlite3_step(stmt) != SQLITE_DONE)) {
rc = SQLITE_ERROR;
goto done;
}
sqlite3_finalize(stmt);
}
continue;
}
#endif
#if SQLITE_VEC_ENABLE_RESCORE
if (p->vector_columns[i].index_type != VEC0_INDEX_TYPE_FLAT)
continue;
@ -7088,6 +7557,171 @@ cleanup:
#include "sqlite-vec-rescore.c"
#endif
#if SQLITE_VEC_ENABLE_DISKANN
/**
* Handle a KNN query using the DiskANN graph search.
*/
static int vec0Filter_knn_diskann(
vec0_cursor *pCur, vec0_vtab *p, int idxNum,
const char *idxStr, int argc, sqlite3_value **argv) {
int rc;
int vectorColumnIdx = idxNum;
struct VectorColumnDefinition *vector_column = &p->vector_columns[vectorColumnIdx];
struct vec0_query_knn_data *knn_data;
knn_data = sqlite3_malloc(sizeof(*knn_data));
if (!knn_data) return SQLITE_NOMEM;
memset(knn_data, 0, sizeof(*knn_data));
// Parse query_idx and k_idx from idxStr
int query_idx = -1;
int k_idx = -1;
for (int i = 0; i < argc; i++) {
if (idxStr[1 + (i * 4)] == VEC0_IDXSTR_KIND_KNN_MATCH) {
query_idx = i;
}
if (idxStr[1 + (i * 4)] == VEC0_IDXSTR_KIND_KNN_K) {
k_idx = i;
}
}
assert(query_idx >= 0);
assert(k_idx >= 0);
// Extract query vector
void *queryVector;
size_t dimensions;
enum VectorElementType elementType;
vector_cleanup queryVectorCleanup = vector_cleanup_noop;
char *pzError;
rc = vector_from_value(argv[query_idx], &queryVector, &dimensions,
&elementType, &queryVectorCleanup, &pzError);
if (rc != SQLITE_OK) {
vtab_set_error(&p->base, "Invalid query vector: %z", pzError);
sqlite3_free(knn_data);
return SQLITE_ERROR;
}
if (elementType != vector_column->element_type ||
dimensions != vector_column->dimensions) {
vtab_set_error(&p->base, "Query vector type/dimension mismatch");
queryVectorCleanup(queryVector);
sqlite3_free(knn_data);
return SQLITE_ERROR;
}
i64 k = sqlite3_value_int64(argv[k_idx]);
if (k <= 0) {
knn_data->k = 0;
knn_data->k_used = 0;
pCur->knn_data = knn_data;
pCur->query_plan = VEC0_QUERY_PLAN_KNN;
queryVectorCleanup(queryVector);
return SQLITE_OK;
}
// Run DiskANN search
i64 *resultRowids = sqlite3_malloc(k * sizeof(i64));
f32 *resultDistances = sqlite3_malloc(k * sizeof(f32));
if (!resultRowids || !resultDistances) {
sqlite3_free(resultRowids);
sqlite3_free(resultDistances);
queryVectorCleanup(queryVector);
sqlite3_free(knn_data);
return SQLITE_NOMEM;
}
int resultCount;
rc = diskann_search(p, vectorColumnIdx, queryVector, dimensions,
elementType, (int)k, 0,
resultRowids, resultDistances, &resultCount);
if (rc != SQLITE_OK) {
queryVectorCleanup(queryVector);
sqlite3_free(resultRowids);
sqlite3_free(resultDistances);
sqlite3_free(knn_data);
return rc;
}
// Scan _diskann_buffer for any buffered (unflushed) vectors and merge
// with graph results. This ensures no recall loss for buffered vectors.
{
sqlite3_stmt *bufStmt = NULL;
char *zSql = sqlite3_mprintf(
"SELECT rowid, vector FROM " VEC0_SHADOW_DISKANN_BUFFER_N_NAME,
p->schemaName, p->tableName, vectorColumnIdx);
if (!zSql) {
queryVectorCleanup(queryVector);
sqlite3_free(resultRowids);
sqlite3_free(resultDistances);
sqlite3_free(knn_data);
return SQLITE_NOMEM;
}
int bufRc = sqlite3_prepare_v2(p->db, zSql, -1, &bufStmt, NULL);
sqlite3_free(zSql);
if (bufRc == SQLITE_OK) {
while (sqlite3_step(bufStmt) == SQLITE_ROW) {
i64 bufRowid = sqlite3_column_int64(bufStmt, 0);
const void *bufVec = sqlite3_column_blob(bufStmt, 1);
f32 dist = vec0_distance_full(
queryVector, bufVec, dimensions, elementType,
vector_column->distance_metric);
// Check if this buffer vector should replace the worst graph result
if (resultCount < (int)k) {
// Still have room, just add it
resultRowids[resultCount] = bufRowid;
resultDistances[resultCount] = dist;
resultCount++;
} else {
// Find worst (largest distance) in results
int worstIdx = 0;
for (int wi = 1; wi < resultCount; wi++) {
if (resultDistances[wi] > resultDistances[worstIdx]) {
worstIdx = wi;
}
}
if (dist < resultDistances[worstIdx]) {
resultRowids[worstIdx] = bufRowid;
resultDistances[worstIdx] = dist;
}
}
}
sqlite3_finalize(bufStmt);
}
}
queryVectorCleanup(queryVector);
// Sort results by distance (ascending)
for (int si = 0; si < resultCount - 1; si++) {
for (int sj = si + 1; sj < resultCount; sj++) {
if (resultDistances[sj] < resultDistances[si]) {
f32 tmpD = resultDistances[si];
resultDistances[si] = resultDistances[sj];
resultDistances[sj] = tmpD;
i64 tmpR = resultRowids[si];
resultRowids[si] = resultRowids[sj];
resultRowids[sj] = tmpR;
}
}
}
knn_data->k = resultCount;
knn_data->k_used = resultCount;
knn_data->rowids = resultRowids;
knn_data->distances = resultDistances;
knn_data->current_idx = 0;
pCur->knn_data = knn_data;
pCur->query_plan = VEC0_QUERY_PLAN_KNN;
return SQLITE_OK;
}
#endif /* SQLITE_VEC_ENABLE_DISKANN */
int vec0Filter_knn(vec0_cursor *pCur, vec0_vtab *p, int idxNum,
const char *idxStr, int argc, sqlite3_value **argv) {
assert(argc == (strlen(idxStr)-1) / 4);
@ -7098,6 +7732,13 @@ int vec0Filter_knn(vec0_cursor *pCur, vec0_vtab *p, int idxNum,
struct VectorColumnDefinition *vector_column =
&p->vector_columns[vectorColumnIdx];
#if SQLITE_VEC_ENABLE_DISKANN
// DiskANN dispatch
if (vector_column->index_type == VEC0_INDEX_TYPE_DISKANN) {
return vec0Filter_knn_diskann(pCur, p, idxNum, idxStr, argc, argv);
}
#endif
struct Array *arrayRowidsIn = NULL;
sqlite3_stmt *stmtChunks = NULL;
void *queryVector;
@ -8567,24 +9208,37 @@ int vec0Update_Insert(sqlite3_vtab *pVTab, int argc, sqlite3_value **argv,
goto cleanup;
}
// Step #2: Find the next "available" position in the _chunks table for this
// row.
rc = vec0Update_InsertNextAvailableStep(p, partitionKeyValues,
&chunk_rowid, &chunk_offset,
&blobChunksValidity,
&bufferChunksValidity);
if (rc != SQLITE_OK) {
goto cleanup;
if (!vec0_all_columns_diskann(p)) {
// Step #2: Find the next "available" position in the _chunks table for this
// row.
rc = vec0Update_InsertNextAvailableStep(p, partitionKeyValues,
&chunk_rowid, &chunk_offset,
&blobChunksValidity,
&bufferChunksValidity);
if (rc != SQLITE_OK) {
goto cleanup;
}
// Step #3: With the next available chunk position, write out all the vectors
// to their specified location.
rc = vec0Update_InsertWriteFinalStep(p, chunk_rowid, chunk_offset, rowid,
vectorDatas, blobChunksValidity,
bufferChunksValidity);
if (rc != SQLITE_OK) {
goto cleanup;
}
}
// Step #3: With the next available chunk position, write out all the vectors
// to their specified location.
rc = vec0Update_InsertWriteFinalStep(p, chunk_rowid, chunk_offset, rowid,
vectorDatas, blobChunksValidity,
bufferChunksValidity);
if (rc != SQLITE_OK) {
goto cleanup;
#if SQLITE_VEC_ENABLE_DISKANN
// Step #4: Insert into DiskANN graph for indexed vector columns
for (int i = 0; i < p->numVectorColumns; i++) {
if (p->vector_columns[i].index_type != VEC0_INDEX_TYPE_DISKANN) continue;
rc = diskann_insert(p, i, rowid, vectorDatas[i]);
if (rc != SQLITE_OK) {
goto cleanup;
}
}
#endif
#if SQLITE_VEC_ENABLE_RESCORE
rc = rescore_on_insert(p, chunk_rowid, chunk_offset, rowid, vectorDatas);
@ -9126,29 +9780,43 @@ int vec0Update_Delete(sqlite3_vtab *pVTab, sqlite3_value *idValue) {
// 4. Zero out vector data in all vector column chunks
// 5. Delete value in _rowids table
// 1. get chunk_id and chunk_offset from _rowids
rc = vec0_get_chunk_position(p, rowid, NULL, &chunk_id, &chunk_offset);
if (rc != SQLITE_OK) {
return rc;
#if SQLITE_VEC_ENABLE_DISKANN
// DiskANN graph deletion for indexed columns
for (int i = 0; i < p->numVectorColumns; i++) {
if (p->vector_columns[i].index_type != VEC0_INDEX_TYPE_DISKANN) continue;
rc = diskann_delete(p, i, rowid);
if (rc != SQLITE_OK) {
return rc;
}
}
#endif
if (!vec0_all_columns_diskann(p)) {
// 1. get chunk_id and chunk_offset from _rowids
rc = vec0_get_chunk_position(p, rowid, NULL, &chunk_id, &chunk_offset);
if (rc != SQLITE_OK) {
return rc;
}
// 2. clear validity bit
rc = vec0Update_Delete_ClearValidity(p, chunk_id, chunk_offset);
if (rc != SQLITE_OK) {
return rc;
}
// 3. zero out rowid in chunks.rowids
rc = vec0Update_Delete_ClearRowid(p, chunk_id, chunk_offset);
if (rc != SQLITE_OK) {
return rc;
}
// 4. zero out any data in vector chunks tables
rc = vec0Update_Delete_ClearVectors(p, chunk_id, chunk_offset);
if (rc != SQLITE_OK) {
return rc;
}
}
// 2. clear validity bit
rc = vec0Update_Delete_ClearValidity(p, chunk_id, chunk_offset);
if (rc != SQLITE_OK) {
return rc;
}
// 3. zero out rowid in chunks.rowids
rc = vec0Update_Delete_ClearRowid(p, chunk_id, chunk_offset);
if (rc != SQLITE_OK) {
return rc;
}
// 4. zero out any data in vector chunks tables
rc = vec0Update_Delete_ClearVectors(p, chunk_id, chunk_offset);
if (rc != SQLITE_OK) {
return rc;
}
#if SQLITE_VEC_ENABLE_RESCORE
// 4b. zero out quantized data in rescore chunk tables, delete from rescore vectors
@ -9172,20 +9840,22 @@ int vec0Update_Delete(sqlite3_vtab *pVTab, sqlite3_value *idValue) {
}
}
// 7. delete metadata
for(int i = 0; i < p->numMetadataColumns; i++) {
rc = vec0Update_Delete_ClearMetadata(p, i, rowid, chunk_id, chunk_offset);
if (rc != SQLITE_OK) {
return rc;
// 7. delete metadata and reclaim chunk (only when using chunk-based storage)
if (!vec0_all_columns_diskann(p)) {
for(int i = 0; i < p->numMetadataColumns; i++) {
rc = vec0Update_Delete_ClearMetadata(p, i, rowid, chunk_id, chunk_offset);
if (rc != SQLITE_OK) {
return rc;
}
}
}
// 8. reclaim chunk if fully empty
{
int chunkDeleted;
rc = vec0Update_Delete_DeleteChunkIfEmpty(p, chunk_id, &chunkDeleted);
if (rc != SQLITE_OK) {
return rc;
// 8. reclaim chunk if fully empty
{
int chunkDeleted;
rc = vec0Update_Delete_DeleteChunkIfEmpty(p, chunk_id, &chunkDeleted);
if (rc != SQLITE_OK) {
return rc;
}
}
}
@ -9481,8 +10151,12 @@ static int vec0Update(sqlite3_vtab *pVTab, int argc, sqlite3_value **argv,
const char *cmd = (const char *)sqlite3_value_text(idVal);
vec0_vtab *p = (vec0_vtab *)pVTab;
int cmdRc = ivf_handle_command(p, cmd, argc, argv);
#if SQLITE_VEC_ENABLE_DISKANN
if (cmdRc == SQLITE_EMPTY)
cmdRc = diskann_handle_command(p, cmd);
#endif
if (cmdRc != SQLITE_EMPTY) return cmdRc; // handled (or error)
// SQLITE_EMPTY means not an IVF command — fall through to normal insert
// SQLITE_EMPTY means not a recognized command — fall through to normal insert
}
#endif
return vec0Update_Insert(pVTab, argc, argv, pRowid);
@ -9638,9 +10312,16 @@ static sqlite3_module vec0Module = {
#define SQLITE_VEC_DEBUG_BUILD_IVF ""
#endif
#if SQLITE_VEC_ENABLE_DISKANN
#define SQLITE_VEC_DEBUG_BUILD_DISKANN "diskann"
#else
#define SQLITE_VEC_DEBUG_BUILD_DISKANN ""
#endif
#define SQLITE_VEC_DEBUG_BUILD \
SQLITE_VEC_DEBUG_BUILD_AVX " " SQLITE_VEC_DEBUG_BUILD_NEON " " \
SQLITE_VEC_DEBUG_BUILD_RESCORE " " SQLITE_VEC_DEBUG_BUILD_IVF
SQLITE_VEC_DEBUG_BUILD_RESCORE " " SQLITE_VEC_DEBUG_BUILD_IVF " " \
SQLITE_VEC_DEBUG_BUILD_DISKANN
#define SQLITE_VEC_DEBUG_STRING \
"Version: " SQLITE_VEC_VERSION "\n" \