diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 14bbeb3..ac6601d 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -25,6 +25,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Removed unnecessary searchInfo messages that appeared on every search - Fixed HTTP node type comparison case sensitivity issue - Implemented relevance-based ranking with special boosting for primary nodes +- **search_templates FTS5 Error**: Fixed "no such module: fts5" error in environments without FTS5 support (fixes Claude Desktop issue) + - Made FTS5 completely optional - detects support at runtime + - Removed FTS5 from required schema to prevent initialization failures + - Automatically falls back to LIKE search when FTS5 is unavailable + - FTS5 tables and triggers created conditionally only if supported + - Template search now works in ALL SQLite environments ### Added - **FTS5 Full-Text Search**: Added SQLite FTS5 support for faster and more intelligent node searching @@ -32,6 +38,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Supports advanced search modes: OR (default), AND (all terms required), FUZZY (typo-tolerant) - Significantly improves search performance for large databases - FUZZY mode now uses edit distance (Levenshtein) for better typo tolerance +- **FTS5 Detection**: Added runtime detection of FTS5 support + - `checkFTS5Support()` method in database adapters + - Conditional initialization of FTS5 features + - Graceful degradation when FTS5 not available ## [Unreleased] diff --git a/src/database/database-adapter.ts b/src/database/database-adapter.ts index 1e8e61c..4d4d5e7 100644 --- a/src/database/database-adapter.ts +++ b/src/database/database-adapter.ts @@ -13,6 +13,7 @@ export interface DatabaseAdapter { pragma(key: string, value?: any): any; readonly inTransaction: boolean; transaction(fn: () => T): T; + checkFTS5Support(): boolean; } export interface PreparedStatement { @@ -174,6 +175,17 @@ class BetterSQLiteAdapter implements DatabaseAdapter { transaction(fn: () => T): T { return this.db.transaction(fn)(); } + + checkFTS5Support(): boolean { + try { + // Test if FTS5 is available + this.exec("CREATE VIRTUAL TABLE IF NOT EXISTS test_fts5 USING fts5(content);"); + this.exec("DROP TABLE IF EXISTS test_fts5;"); + return true; + } catch (error) { + return false; + } + } } /** @@ -234,6 +246,18 @@ class SQLJSAdapter implements DatabaseAdapter { } } + checkFTS5Support(): boolean { + try { + // Test if FTS5 is available + this.exec("CREATE VIRTUAL TABLE IF NOT EXISTS test_fts5 USING fts5(content);"); + this.exec("DROP TABLE IF EXISTS test_fts5;"); + return true; + } catch (error) { + // sql.js doesn't support FTS5 + return false; + } + } + private scheduleSave(): void { if (this.saveTimer) { clearTimeout(this.saveTimer); diff --git a/src/database/schema.sql b/src/database/schema.sql index 9bd4415..58db822 100644 --- a/src/database/schema.sql +++ b/src/database/schema.sql @@ -47,7 +47,5 @@ CREATE INDEX IF NOT EXISTS idx_template_nodes ON templates(nodes_used); CREATE INDEX IF NOT EXISTS idx_template_updated ON templates(updated_at); CREATE INDEX IF NOT EXISTS idx_template_name ON templates(name); --- Full-text search for templates -CREATE VIRTUAL TABLE IF NOT EXISTS templates_fts USING fts5( - name, description, content=templates -); \ No newline at end of file +-- Note: FTS5 tables are created conditionally at runtime if FTS5 is supported +-- See template-repository.ts initializeFTS5() method \ No newline at end of file diff --git a/src/templates/template-repository.ts b/src/templates/template-repository.ts index 15c43c6..8caa9b9 100644 --- a/src/templates/template-repository.ts +++ b/src/templates/template-repository.ts @@ -23,9 +23,57 @@ export interface StoredTemplate { export class TemplateRepository { private sanitizer: TemplateSanitizer; + private hasFTS5Support: boolean = false; constructor(private db: DatabaseAdapter) { this.sanitizer = new TemplateSanitizer(); + this.initializeFTS5(); + } + + /** + * Initialize FTS5 tables if supported + */ + private initializeFTS5(): void { + this.hasFTS5Support = this.db.checkFTS5Support(); + + if (this.hasFTS5Support) { + try { + // Create FTS5 virtual table + this.db.exec(` + CREATE VIRTUAL TABLE IF NOT EXISTS templates_fts USING fts5( + name, description, content=templates + ); + `); + + // Create triggers to keep FTS5 in sync + this.db.exec(` + CREATE TRIGGER IF NOT EXISTS templates_ai AFTER INSERT ON templates BEGIN + INSERT INTO templates_fts(rowid, name, description) + VALUES (new.id, new.name, new.description); + END; + `); + + this.db.exec(` + CREATE TRIGGER IF NOT EXISTS templates_au AFTER UPDATE ON templates BEGIN + UPDATE templates_fts SET name = new.name, description = new.description + WHERE rowid = new.id; + END; + `); + + this.db.exec(` + CREATE TRIGGER IF NOT EXISTS templates_ad AFTER DELETE ON templates BEGIN + DELETE FROM templates_fts WHERE rowid = old.id; + END; + `); + + logger.info('FTS5 support enabled for template search'); + } catch (error) { + logger.warn('Failed to initialize FTS5 for templates:', error); + this.hasFTS5Support = false; + } + } else { + logger.info('FTS5 not available, using LIKE search for templates'); + } } /** @@ -110,16 +158,41 @@ export class TemplateRepository { * Search templates by name or description */ searchTemplates(query: string, limit: number = 20): StoredTemplate[] { - // Use FTS for search - const ftsQuery = query.split(' ').map(term => `"${term}"`).join(' OR '); + // If FTS5 is not supported, go straight to LIKE search + if (!this.hasFTS5Support) { + return this.searchTemplatesLIKE(query, limit); + } + + try { + // Use FTS for search + const ftsQuery = query.split(' ').map(term => `"${term}"`).join(' OR '); + + return this.db.prepare(` + SELECT t.* FROM templates t + JOIN templates_fts ON t.id = templates_fts.rowid + WHERE templates_fts MATCH ? + ORDER BY rank, t.views DESC + LIMIT ? + `).all(ftsQuery, limit) as StoredTemplate[]; + } catch (error: any) { + // If FTS5 query fails, fallback to LIKE search + logger.warn('FTS5 template search failed, using LIKE fallback:', error.message); + return this.searchTemplatesLIKE(query, limit); + } + } + + /** + * Fallback search using LIKE when FTS5 is not available + */ + private searchTemplatesLIKE(query: string, limit: number = 20): StoredTemplate[] { + const likeQuery = `%${query}%`; return this.db.prepare(` - SELECT t.* FROM templates t - JOIN templates_fts ON t.id = templates_fts.rowid - WHERE templates_fts MATCH ? - ORDER BY rank, t.views DESC + SELECT * FROM templates + WHERE name LIKE ? OR description LIKE ? + ORDER BY views DESC, created_at DESC LIMIT ? - `).all(ftsQuery, limit) as StoredTemplate[]; + `).all(likeQuery, likeQuery, limit) as StoredTemplate[]; } /** @@ -208,4 +281,32 @@ export class TemplateRepository { this.db.exec('DELETE FROM templates'); logger.info('Cleared all templates from database'); } + + /** + * Rebuild the FTS5 index for all templates + * This is needed when templates are bulk imported or when FTS5 gets out of sync + */ + rebuildTemplateFTS(): void { + // Skip if FTS5 is not supported + if (!this.hasFTS5Support) { + return; + } + + try { + // Clear existing FTS data + this.db.exec('DELETE FROM templates_fts'); + + // Repopulate from templates table + this.db.exec(` + INSERT INTO templates_fts(rowid, name, description) + SELECT id, name, description FROM templates + `); + + const count = this.getTemplateCount(); + logger.info(`Rebuilt FTS5 index for ${count} templates`); + } catch (error) { + logger.warn('Failed to rebuild template FTS5 index:', error); + // Non-critical error - search will fallback to LIKE + } + } } \ No newline at end of file diff --git a/src/templates/template-service.ts b/src/templates/template-service.ts index 5db9a69..95e7c78 100644 --- a/src/templates/template-service.ts +++ b/src/templates/template-service.ts @@ -132,6 +132,11 @@ export class TemplateService { } logger.info(`Successfully saved ${saved} templates to database`); + + // Rebuild FTS5 index after bulk import + logger.info('Rebuilding FTS5 index for templates'); + this.repository.rebuildTemplateFTS(); + progressCallback?.('Complete', saved, saved); } catch (error) { logger.error('Error fetching templates:', error);