fix: make FTS5 optional for template search (fixes Claude Desktop compatibility)
- Added runtime FTS5 detection in database adapters - Removed FTS5 from required schema to prevent "no such module" errors - FTS5 tables/triggers created conditionally only if supported - Template search automatically falls back to LIKE when FTS5 unavailable - Works in ALL SQLite environments (Claude Desktop, restricted envs, etc.) This ensures search_templates() works correctly regardless of SQLite build, while still providing optimal performance when FTS5 is available. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
- Removed unnecessary searchInfo messages that appeared on every search
|
||||||
- Fixed HTTP node type comparison case sensitivity issue
|
- Fixed HTTP node type comparison case sensitivity issue
|
||||||
- Implemented relevance-based ranking with special boosting for primary nodes
|
- 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
|
### Added
|
||||||
- **FTS5 Full-Text Search**: Added SQLite FTS5 support for faster and more intelligent node searching
|
- **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)
|
- Supports advanced search modes: OR (default), AND (all terms required), FUZZY (typo-tolerant)
|
||||||
- Significantly improves search performance for large databases
|
- Significantly improves search performance for large databases
|
||||||
- FUZZY mode now uses edit distance (Levenshtein) for better typo tolerance
|
- 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]
|
## [Unreleased]
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export interface DatabaseAdapter {
|
|||||||
pragma(key: string, value?: any): any;
|
pragma(key: string, value?: any): any;
|
||||||
readonly inTransaction: boolean;
|
readonly inTransaction: boolean;
|
||||||
transaction<T>(fn: () => T): T;
|
transaction<T>(fn: () => T): T;
|
||||||
|
checkFTS5Support(): boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PreparedStatement {
|
export interface PreparedStatement {
|
||||||
@@ -174,6 +175,17 @@ class BetterSQLiteAdapter implements DatabaseAdapter {
|
|||||||
transaction<T>(fn: () => T): T {
|
transaction<T>(fn: () => T): T {
|
||||||
return this.db.transaction(fn)();
|
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 {
|
private scheduleSave(): void {
|
||||||
if (this.saveTimer) {
|
if (this.saveTimer) {
|
||||||
clearTimeout(this.saveTimer);
|
clearTimeout(this.saveTimer);
|
||||||
|
|||||||
@@ -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_updated ON templates(updated_at);
|
||||||
CREATE INDEX IF NOT EXISTS idx_template_name ON templates(name);
|
CREATE INDEX IF NOT EXISTS idx_template_name ON templates(name);
|
||||||
|
|
||||||
-- Full-text search for templates
|
-- Note: FTS5 tables are created conditionally at runtime if FTS5 is supported
|
||||||
CREATE VIRTUAL TABLE IF NOT EXISTS templates_fts USING fts5(
|
-- See template-repository.ts initializeFTS5() method
|
||||||
name, description, content=templates
|
|
||||||
);
|
|
||||||
@@ -23,9 +23,57 @@ export interface StoredTemplate {
|
|||||||
|
|
||||||
export class TemplateRepository {
|
export class TemplateRepository {
|
||||||
private sanitizer: TemplateSanitizer;
|
private sanitizer: TemplateSanitizer;
|
||||||
|
private hasFTS5Support: boolean = false;
|
||||||
|
|
||||||
constructor(private db: DatabaseAdapter) {
|
constructor(private db: DatabaseAdapter) {
|
||||||
this.sanitizer = new TemplateSanitizer();
|
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
|
* Search templates by name or description
|
||||||
*/
|
*/
|
||||||
searchTemplates(query: string, limit: number = 20): StoredTemplate[] {
|
searchTemplates(query: string, limit: number = 20): StoredTemplate[] {
|
||||||
// Use FTS for search
|
// If FTS5 is not supported, go straight to LIKE search
|
||||||
const ftsQuery = query.split(' ').map(term => `"${term}"`).join(' OR ');
|
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(`
|
return this.db.prepare(`
|
||||||
SELECT t.* FROM templates t
|
SELECT * FROM templates
|
||||||
JOIN templates_fts ON t.id = templates_fts.rowid
|
WHERE name LIKE ? OR description LIKE ?
|
||||||
WHERE templates_fts MATCH ?
|
ORDER BY views DESC, created_at DESC
|
||||||
ORDER BY rank, t.views DESC
|
|
||||||
LIMIT ?
|
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');
|
this.db.exec('DELETE FROM templates');
|
||||||
logger.info('Cleared all templates from database');
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -132,6 +132,11 @@ export class TemplateService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`Successfully saved ${saved} templates to database`);
|
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);
|
progressCallback?.('Complete', saved, saved);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error fetching templates:', error);
|
logger.error('Error fetching templates:', error);
|
||||||
|
|||||||
Reference in New Issue
Block a user