Fix sqlite-vec dimension changes for local profiles

This commit is contained in:
“BeeRad”
2026-05-02 10:46:02 +10:00
parent 782ace9a34
commit 9575815d83
4 changed files with 108 additions and 30 deletions
+14 -6
View File
@@ -551,12 +551,19 @@ function ensureCoreSchema(db) {
`); `);
} }
function tryInitVectorTables(db, dbPath) { function getEmbeddingDimensions(env) {
const defaultDimensions = env.EMBEDDING_PROFILE === 'openai-compatible' || env.EMBEDDING_PROFILE === 'custom'
? '1024'
: '1536';
return Number(env.EMBEDDING_DIMENSIONS || defaultDimensions);
}
function tryInitVectorTables(db, dbPath, env) {
const extension = process.platform === 'darwin' ? 'dylib' : process.platform === 'win32' ? 'dll' : 'so'; const extension = process.platform === 'darwin' ? 'dylib' : process.platform === 'win32' ? 'dll' : 'so';
const extensionPath = process.env.SQLITE_VEC_EXTENSION_PATH || path.join(repoDir, 'vendor', 'sqlite-extensions', `vec0.${extension}`); const extensionPath = process.env.SQLITE_VEC_EXTENSION_PATH || path.join(repoDir, 'vendor', 'sqlite-extensions', `vec0.${extension}`);
const dimensions = Number(process.env.EMBEDDING_DIMENSIONS || '1536'); const dimensions = getEmbeddingDimensions(env);
if (!Number.isInteger(dimensions) || dimensions <= 0) { if (!Number.isInteger(dimensions) || dimensions <= 0) {
throw new Error(`Invalid EMBEDDING_DIMENSIONS="${process.env.EMBEDDING_DIMENSIONS}"`); throw new Error(`Invalid EMBEDDING_DIMENSIONS="${env.EMBEDDING_DIMENSIONS}"`);
} }
try { try {
@@ -585,11 +592,12 @@ function main() {
ensureEnvFile(); ensureEnvFile();
const env = parseEnvFile(targetEnv); const env = { ...parseEnvFile(targetEnv), ...process.env };
if (process.env.SQLITE_DB_PATH) { if (process.env.SQLITE_DB_PATH) {
ensureEnvValue('SQLITE_DB_PATH', process.env.SQLITE_DB_PATH); ensureEnvValue('SQLITE_DB_PATH', process.env.SQLITE_DB_PATH);
env.SQLITE_DB_PATH = process.env.SQLITE_DB_PATH;
} }
const dbPath = expandPath(process.env.SQLITE_DB_PATH || env.SQLITE_DB_PATH || getDefaultDbPath()); const dbPath = expandPath(env.SQLITE_DB_PATH || getDefaultDbPath());
fs.mkdirSync(path.dirname(dbPath), { recursive: true }); fs.mkdirSync(path.dirname(dbPath), { recursive: true });
if (!fs.existsSync(dbPath)) { if (!fs.existsSync(dbPath)) {
fs.closeSync(fs.openSync(dbPath, 'w')); fs.closeSync(fs.openSync(dbPath, 'w'));
@@ -598,7 +606,7 @@ function main() {
const db = new Database(dbPath); const db = new Database(dbPath);
try { try {
ensureCoreSchema(db); ensureCoreSchema(db);
tryInitVectorTables(db, dbPath); tryInitVectorTables(db, dbPath, env);
} finally { } finally {
db.close(); db.close();
} }
+5
View File
@@ -1,6 +1,7 @@
import { getSQLiteClient } from '@/services/database/sqlite-client'; import { getSQLiteClient } from '@/services/database/sqlite-client';
import { NodeEmbedder } from '@/services/typescript/embed-nodes'; import { NodeEmbedder } from '@/services/typescript/embed-nodes';
import { UniversalEmbedder } from '@/services/typescript/embed-universal'; import { UniversalEmbedder } from '@/services/typescript/embed-universal';
import { getVectorBackendType } from '@/services/vectorBackend';
async function maybeRecreateQdrantCollections() { async function maybeRecreateQdrantCollections() {
if (process.env.VECTOR_BACKEND !== 'qdrant' || process.env.QDRANT_RECREATE_COLLECTIONS !== 'true') { if (process.env.VECTOR_BACKEND !== 'qdrant' || process.env.QDRANT_RECREATE_COLLECTIONS !== 'true') {
@@ -30,6 +31,10 @@ async function maybeRecreateQdrantCollections() {
async function main() { async function main() {
const sqlite = getSQLiteClient(); const sqlite = getSQLiteClient();
await maybeRecreateQdrantCollections(); await maybeRecreateQdrantCollections();
if (getVectorBackendType() === 'sqlite-vec') {
sqlite.recreateVectorTables();
console.log('[rebuild-embeddings] Recreated sqlite-vec tables for the active embedding dimensions.');
}
const nodeRows = sqlite.query<{ id: number; source?: string | null }>(` const nodeRows = sqlite.query<{ id: number; source?: string | null }>(`
SELECT id, source SELECT id, source
FROM nodes FROM nodes
+73 -23
View File
@@ -18,6 +18,7 @@ export interface SQLiteQueryResult<T = any> {
} }
type FtsSurfaceName = 'nodes' | 'chunks'; type FtsSurfaceName = 'nodes' | 'chunks';
type VectorTableName = 'vec_nodes' | 'vec_chunks';
interface IntegrityProbeResult { interface IntegrityProbeResult {
ok: boolean; ok: boolean;
@@ -283,33 +284,82 @@ class SQLiteClient {
public ensureVectorExtensions(): void { public ensureVectorExtensions(): void {
try { try {
const dimensions = getEmbeddingProviderInfo().dimensions; const dimensions = getEmbeddingProviderInfo().dimensions;
// Test for vec_nodes and vec_chunks; create them if missing this.ensureVectorTable('vec_nodes', dimensions);
const hasVecNodes = this.db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name=?").get('vec_nodes'); this.ensureVectorTable('vec_chunks', dimensions);
if (!hasVecNodes) {
this.db.exec(`
CREATE VIRTUAL TABLE vec_nodes USING vec0(
node_id INTEGER PRIMARY KEY,
embedding FLOAT[${dimensions}]
);
`);
console.log('Created vec_nodes virtual table');
}
const hasVecChunks = this.db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name=?").get('vec_chunks');
if (!hasVecChunks) {
this.db.exec(`
CREATE VIRTUAL TABLE vec_chunks USING vec0(
chunk_id INTEGER PRIMARY KEY,
embedding FLOAT[${dimensions}]
);
`);
console.log('Created vec_chunks virtual table');
}
} catch (error) { } catch (error) {
console.warn('Vector extension not available:', error); console.warn('Vector extension not available:', error);
} }
} }
private getVectorTableSql(tableName: VectorTableName): string | null {
const row = this.db.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name=?")
.get(tableName) as { sql?: string } | undefined;
return row?.sql || null;
}
public getVectorTableDimensions(): Record<'nodes' | 'chunks', number | null> {
return {
nodes: this.getVectorTableDimension('vec_nodes'),
chunks: this.getVectorTableDimension('vec_chunks'),
};
}
private getVectorTableDimension(tableName: VectorTableName): number | null {
const sql = this.getVectorTableSql(tableName);
const match = sql?.match(/embedding\s+FLOAT\[(\d+)\]/i);
return match ? Number(match[1]) : null;
}
private createVectorTable(tableName: VectorTableName, dimensions: number): void {
const idColumn = tableName === 'vec_nodes' ? 'node_id' : 'chunk_id';
this.db.exec(`
CREATE VIRTUAL TABLE ${tableName} USING vec0(
${idColumn} INTEGER PRIMARY KEY,
embedding FLOAT[${dimensions}]
);
`);
}
private ensureVectorTable(tableName: VectorTableName, dimensions: number): void {
const existingSql = this.getVectorTableSql(tableName);
if (!existingSql) {
this.createVectorTable(tableName, dimensions);
console.log(`Created ${tableName} virtual table`);
return;
}
const existingDimensions = this.getVectorTableDimension(tableName);
if (existingDimensions === dimensions) {
return;
}
const rowCount = this.countVectorRows(tableName);
if (rowCount === 0 || this.maintenanceMode) {
this.db.exec(`DROP TABLE IF EXISTS ${tableName};`);
this.createVectorTable(tableName, dimensions);
console.warn(`Recreated ${tableName} virtual table for ${dimensions} dimensions (was ${existingDimensions ?? 'unknown'}).`);
return;
}
console.warn(
`${tableName} uses ${existingDimensions ?? 'unknown'} dimensions, but the active embedding profile requires ${dimensions}. ` +
'Run npm run rebuild:embeddings to recreate derived vectors.'
);
}
public recreateVectorTables(): void {
if (this.readOnly) {
throw new Error('Cannot recreate vector tables while SQLITE_READONLY=true.');
}
const dimensions = getEmbeddingProviderInfo().dimensions;
this.db.exec(`
DROP TABLE IF EXISTS vec_nodes;
DROP TABLE IF EXISTS vec_chunks;
`);
this.createVectorTable('vec_nodes', dimensions);
this.createVectorTable('vec_chunks', dimensions);
}
private ensureVectorTables(): void { private ensureVectorTables(): void {
if (this.readOnly) { if (this.readOnly) {
return; return;
@@ -1333,7 +1383,7 @@ class SQLiteClient {
); );
} }
private countVectorRows(tableName: 'vec_nodes' | 'vec_chunks'): number { private countVectorRows(tableName: VectorTableName): number {
try { try {
const exists = this.db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name=?").get(tableName); const exists = this.db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name=?").get(tableName);
if (!exists) return 0; if (!exists) return 0;
@@ -99,10 +99,25 @@ export class SqliteVecBackend implements VectorBackend {
async healthCheck(): Promise<VectorBackendHealth> { async healthCheck(): Promise<VectorBackendHealth> {
const sqlite = getSQLiteClient(); const sqlite = getSQLiteClient();
const ok = await sqlite.checkVectorExtension(); const ok = await sqlite.checkVectorExtension();
const expectedDimensions = getEmbeddingProviderInfo().dimensions;
const tableDimensions = sqlite.getVectorTableDimensions();
const mismatchedTables = Object.entries(tableDimensions)
.filter(([, dimensions]) => dimensions !== null && dimensions !== expectedDimensions)
.map(([table, dimensions]) => `${table}=${dimensions}`);
if (mismatchedTables.length > 0) {
return {
ok: false,
backend: 'sqlite-vec',
dimensions: expectedDimensions,
detail: `sqlite-vec table dimensions mismatch (${mismatchedTables.join(', ')}), expected ${expectedDimensions}. Run npm run rebuild:embeddings.`,
};
}
return { return {
ok, ok,
backend: 'sqlite-vec', backend: 'sqlite-vec',
dimensions: getEmbeddingProviderInfo().dimensions, dimensions: expectedDimensions,
detail: ok ? 'sqlite-vec extension loaded' : 'sqlite-vec extension unavailable', detail: ok ? 'sqlite-vec extension loaded' : 'sqlite-vec extension unavailable',
}; };
} }