UNPKG

@binsarjr/shadcn-svelte-mcp

Version:

MCP server for shadcn-svelte development with curated knowledge, components, and examples

954 lines 43.9 kB
import { Database } from "bun:sqlite"; import { existsSync } from "fs"; import { join } from "path"; import { getConfigPaths } from "./utils/config.js"; import { enhanceQuery, getBM25Weights, preprocessContent } from "./utils/search-optimization.js"; import { createSearchCache } from "./utils/search-cache.js"; import { createSearchAnalytics } from "./utils/search-analytics.js"; import { createAdvancedIndexing } from "./utils/advanced-indexing.js"; export class ShadcnSvelteSearchDB { db; dbPath; cache; // SearchCache instance analytics; // SearchAnalytics instance indexing; // AdvancedIndexing instance initialized = false; CURRENT_VERSION = 2; // Increment when schema changes constructor() { // Use config utilities to get paths with environment variable support const { configDir, databasePath } = getConfigPaths(); this.dbPath = databasePath; try { this.db = new Database(this.dbPath); } catch (error) { // Handle corrupted database file by removing it and creating a new one const errorMessage = error instanceof Error ? error.message : String(error); if (errorMessage.includes('file is not a database')) { const fs = require('fs'); if (fs.existsSync(this.dbPath)) { fs.unlinkSync(this.dbPath); } this.db = new Database(this.dbPath); } else { throw error; } } // Initialize optimization components this.cache = createSearchCache(join(configDir, 'search-cache.json')); this.analytics = createSearchAnalytics(join(configDir, 'search-analytics.json')); this.indexing = createAdvancedIndexing({ enableContentPreprocessing: true, enableKeywordExtraction: true, enableComplexityScoring: true, enableMetadataIndexing: true, minKeywordLength: 3, maxKeywords: 50 }); // Initialize database with corruption handling try { this.checkAndUpgradeDatabase(); this.initializeSchema(); this.initialized = true; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); if (errorMessage.includes('file is not a database')) { const fs = require('fs'); if (fs.existsSync(this.dbPath)) { fs.unlinkSync(this.dbPath); } this.db = new Database(this.dbPath); this.checkAndUpgradeDatabase(); this.initializeSchema(); this.initialized = true; } else { throw error; } } } checkAndUpgradeDatabase() { // Check if database exists and has version table const versionTableExists = this.db.query("SELECT name FROM sqlite_master WHERE type='table' AND name='database_version'").get(); // Check for incompatible schema (old synonyms table without context column) const synonymsTableExists = this.db.query("SELECT name FROM sqlite_master WHERE type='table' AND name='synonyms'").get(); let needsRebuild = false; if (synonymsTableExists) { // Check if synonyms table has context column const synonymsSchema = this.db.query("PRAGMA table_info(synonyms)").all(); const hasContextColumn = synonymsSchema.some(col => col.name === 'context'); if (!hasContextColumn) { console.log("🔍 Detected old synonyms table schema without context column"); needsRebuild = true; } } if (!versionTableExists) { // New database or old database without versioning this.createVersionTable(); this.setDatabaseVersion(this.CURRENT_VERSION); if (needsRebuild) { console.log("🔄 Rebuilding database with compatible schema"); this.upgradeDatabase(1, this.CURRENT_VERSION); this.setDatabaseVersion(this.CURRENT_VERSION); console.log(`✅ Database rebuilt with version ${this.CURRENT_VERSION}`); } return; } const currentVersion = this.getDatabaseVersion(); if (currentVersion < this.CURRENT_VERSION || needsRebuild) { console.log(`🔄 Upgrading database from version ${currentVersion} to ${this.CURRENT_VERSION}`); this.upgradeDatabase(currentVersion, this.CURRENT_VERSION); this.setDatabaseVersion(this.CURRENT_VERSION); console.log(`✅ Database upgraded to version ${this.CURRENT_VERSION}`); } } createVersionTable() { this.db.exec(` CREATE TABLE IF NOT EXISTS database_version ( version INTEGER PRIMARY KEY, upgraded_at DATETIME DEFAULT CURRENT_TIMESTAMP, changelog TEXT ) `); } getDatabaseVersion() { const result = this.db.query("SELECT version FROM database_version ORDER BY version DESC LIMIT 1").get(); return result?.version || 1; } setDatabaseVersion(version) { const changelog = this.getChangelogForVersion(version); this.db.query("INSERT OR REPLACE INTO database_version (version, changelog) VALUES (?, ?)").all(version, changelog); } getChangelogForVersion(version) { const changelogs = { 1: "Initial database with knowledge, examples, components tables and FTS5", 2: "Added synonyms table with context column and enhanced search optimization" }; return changelogs[version] || "Version upgrade"; } upgradeDatabase(fromVersion, toVersion) { // Drop existing database and recreate this.db.exec("PRAGMA foreign_keys = OFF"); // Get all table names const tables = this.db.query("SELECT name FROM sqlite_master WHERE type='table'").all(); // Drop all tables except database_version and system tables tables.forEach(table => { if (table.name !== 'database_version' && !table.name.startsWith('sqlite_')) { this.db.exec(`DROP TABLE IF EXISTS ${table.name}`); } }); // Drop virtual tables const virtualTables = this.db.query("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE '%_fts'").all(); virtualTables.forEach(table => { this.db.exec(`DROP TABLE IF EXISTS ${table.name}`); }); this.db.exec("PRAGMA foreign_keys = ON"); console.log(`🗑️ Dropped old tables and recreated database schema for version ${toVersion}`); } initializeSchema() { // Enable performance optimizations this.db.exec("PRAGMA foreign_keys = ON"); this.db.exec("PRAGMA journal_mode = WAL"); this.db.exec("PRAGMA synchronous = NORMAL"); this.db.exec("PRAGMA cache_size = -10000"); // 10MB cache this.db.exec("PRAGMA temp_store = MEMORY"); this.db.exec("PRAGMA mmap_size = 268435456"); // 256MB mmap // Create main tables this.db.exec(` CREATE TABLE IF NOT EXISTS knowledge ( id INTEGER PRIMARY KEY AUTOINCREMENT, question TEXT NOT NULL, answer TEXT NOT NULL, category TEXT NOT NULL, tags TEXT NOT NULL -- JSON array as text ) `); this.db.exec(` CREATE TABLE IF NOT EXISTS examples ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, description TEXT NOT NULL, component TEXT NOT NULL, code TEXT NOT NULL, category TEXT NOT NULL, tags TEXT NOT NULL, -- JSON array as text complexity TEXT NOT NULL CHECK(complexity IN ('basic', 'intermediate', 'advanced')), dependencies TEXT NOT NULL -- JSON array as text ) `); this.db.exec(` CREATE TABLE IF NOT EXISTS components ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, description TEXT NOT NULL, category TEXT NOT NULL, props TEXT NOT NULL, -- JSON object as text usage TEXT NOT NULL, installation TEXT NOT NULL, variants TEXT NOT NULL, -- JSON array as text dependencies TEXT NOT NULL -- JSON array as text ) `); // Create FTS5 virtual tables with enhanced configuration this.db.exec(` CREATE VIRTUAL TABLE IF NOT EXISTS knowledge_fts USING fts5( question, answer, category, tags, content='knowledge', content_rowid='id', tokenize='unicode61 remove_diacritics 2', prefix='2 3 4', columnsize=0 ) `); this.db.exec(` CREATE VIRTUAL TABLE IF NOT EXISTS examples_fts USING fts5( title, description, component, code, category, tags, complexity, dependencies, content='examples', content_rowid='id', tokenize='unicode61 remove_diacritics 2', prefix='2 3 4', columnsize=0 ) `); this.db.exec(` CREATE VIRTUAL TABLE IF NOT EXISTS components_fts USING fts5( name, description, category, props, usage, installation, variants, dependencies, content='components', content_rowid='id', tokenize='unicode61 remove_diacritics 2', prefix='2 3 4', columnsize=0 ) `); // Create enhanced indexes for better filtering performance this.db.exec(` CREATE INDEX IF NOT EXISTS knowledge_category_idx ON knowledge(category); `); this.db.exec(` CREATE INDEX IF NOT EXISTS examples_complexity_idx ON examples(complexity); `); this.db.exec(` CREATE INDEX IF NOT EXISTS examples_component_idx ON examples(component); `); this.db.exec(` CREATE INDEX IF NOT EXISTS components_category_idx ON components(category); `); this.db.exec(` CREATE INDEX IF NOT EXISTS components_name_idx ON components(name); `); // Create triggers to maintain FTS sync this.createFTSTriggers(); // Create synonyms for better search results this.createSynonyms(); } createFTSTriggers() { // Knowledge FTS triggers this.db.exec(` CREATE TRIGGER IF NOT EXISTS knowledge_fts_insert AFTER INSERT ON knowledge BEGIN INSERT INTO knowledge_fts(rowid, question, answer, category, tags) VALUES (new.id, new.question, new.answer, new.category, new.tags); END `); this.db.exec(` CREATE TRIGGER IF NOT EXISTS knowledge_fts_delete AFTER DELETE ON knowledge BEGIN INSERT INTO knowledge_fts(knowledge_fts, rowid, question, answer, category, tags) VALUES ('delete', old.id, old.question, old.answer, old.category, old.tags); END `); this.db.exec(` CREATE TRIGGER IF NOT EXISTS knowledge_fts_update AFTER UPDATE ON knowledge BEGIN INSERT INTO knowledge_fts(knowledge_fts, rowid, question, answer, category, tags) VALUES ('delete', old.id, old.question, old.answer, old.category, old.tags); INSERT INTO knowledge_fts(rowid, question, answer, category, tags) VALUES (new.id, new.question, new.answer, new.category, new.tags); END `); // Examples FTS triggers this.db.exec(` CREATE TRIGGER IF NOT EXISTS examples_fts_insert AFTER INSERT ON examples BEGIN INSERT INTO examples_fts(rowid, title, description, component, code, category, tags, complexity, dependencies) VALUES (new.id, new.title, new.description, new.component, new.code, new.category, new.tags, new.complexity, new.dependencies); END `); this.db.exec(` CREATE TRIGGER IF NOT EXISTS examples_fts_delete AFTER DELETE ON examples BEGIN INSERT INTO examples_fts(examples_fts, rowid, title, description, component, code, category, tags, complexity, dependencies) VALUES ('delete', old.id, old.title, old.description, old.component, old.code, old.category, old.tags, old.complexity, old.dependencies); END `); this.db.exec(` CREATE TRIGGER IF NOT EXISTS examples_fts_update AFTER UPDATE ON examples BEGIN INSERT INTO examples_fts(examples_fts, rowid, title, description, component, code, category, tags, complexity, dependencies) VALUES ('delete', old.id, old.title, old.description, old.component, old.code, old.category, old.tags, old.complexity, old.dependencies); INSERT INTO examples_fts(rowid, title, description, component, code, category, tags, complexity, dependencies) VALUES (new.id, new.title, new.description, new.component, new.code, new.category, new.tags, new.complexity, new.dependencies); END `); // Components FTS triggers this.db.exec(` CREATE TRIGGER IF NOT EXISTS components_fts_insert AFTER INSERT ON components BEGIN INSERT INTO components_fts(rowid, name, description, category, props, usage, installation, variants, dependencies) VALUES (new.id, new.name, new.description, new.category, new.props, new.usage, new.installation, new.variants, new.dependencies); END `); this.db.exec(` CREATE TRIGGER IF NOT EXISTS components_fts_delete AFTER DELETE ON components BEGIN INSERT INTO components_fts(components_fts, rowid, name, description, category, props, usage, installation, variants, dependencies) VALUES ('delete', old.id, old.name, old.description, old.category, old.props, old.usage, old.installation, old.variants, old.dependencies); END `); this.db.exec(` CREATE TRIGGER IF NOT EXISTS components_fts_update AFTER UPDATE ON components BEGIN INSERT INTO components_fts(components_fts, rowid, name, description, category, props, usage, installation, variants, dependencies) VALUES ('delete', old.id, old.name, old.description, old.category, old.props, old.usage, old.installation, old.variants, old.dependencies); INSERT INTO components_fts(rowid, name, description, category, props, usage, installation, variants, dependencies) VALUES (new.id, new.name, new.description, new.category, new.props, new.usage, new.installation, new.variants, new.dependencies); END `); } createSynonyms() { this.db.exec(` CREATE TABLE IF NOT EXISTS synonyms ( term TEXT PRIMARY KEY, synonyms TEXT NOT NULL, -- JSON array of synonyms context TEXT, -- Context information for better matching created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) `); // Enhanced synonym data with context const synonymData = [ { term: 'button', synonyms: JSON.stringify(['btn', 'clickable', 'action-button', 'cta-button', 'submit-button']), context: 'ui-components' }, { term: 'input', synonyms: JSON.stringify(['field', 'form', 'text', 'entry', 'textbox', 'input-field']), context: 'form-components' }, { term: 'modal', synonyms: JSON.stringify(['dialog', 'popup', 'overlay', 'window', 'lightbox']), context: 'overlay-components' }, { term: 'dropdown', synonyms: JSON.stringify(['select', 'menu', 'picker', 'combobox', 'choice']), context: 'form-components' }, { term: 'card', synonyms: JSON.stringify(['container', 'box', 'panel', 'section', 'tile']), context: 'layout-components' }, { term: 'theme', synonyms: JSON.stringify(['dark', 'light', 'style', 'appearance', 'mode', 'color-scheme']), context: 'styling' }, { term: 'component', synonyms: JSON.stringify(['ui', 'element', 'widget', 'control', 'interface']), context: 'general' }, { term: 'tailwind', synonyms: JSON.stringify(['css', 'style', 'class', 'utility', 'tailwindcss']), context: 'styling' }, { term: 'accessibility', synonyms: JSON.stringify(['a11y', 'screen-reader', 'aria', 'inclusive', 'disability']), context: 'accessibility' }, { term: 'animation', synonyms: JSON.stringify(['transition', 'motion', 'effect', 'smooth', 'interactive']), context: 'animations' }, { term: 'responsive', synonyms: JSON.stringify(['mobile', 'tablet', 'desktop', 'breakpoint', 'adaptive']), context: 'responsive-design' }, { term: 'form', synonyms: JSON.stringify(['validation', 'submit', 'input', 'field', 'form-element']), context: 'form-components' }, { term: 'chart', synonyms: JSON.stringify(['graph', 'visualization', 'data', 'plot', 'data-viz']), context: 'data-display' }, { term: 'table', synonyms: JSON.stringify(['grid', 'data', 'row', 'column', 'datatable', 'data-table']), context: 'data-display' }, { term: 'navigation', synonyms: JSON.stringify(['nav', 'menu', 'breadcrumb', 'sidebar', 'navigation-menu']), context: 'navigation' }, // Shadcn-specific terms { term: 'cva', synonyms: JSON.stringify(['class-variance-authority', 'variant-configuration', 'variant-utility']), context: 'shadcn-specific' }, { term: 'bits-ui', synonyms: JSON.stringify(['bits', 'bits-components', 'bits-ui-library']), context: 'shadcn-specific' }, { term: 'tailwind-merge', synonyms: JSON.stringify(['clsx-merge', 'class-merging', 'tailwind-class-merging']), context: 'shadcn-specific' }, // Action-oriented terms { term: 'install', synonyms: JSON.stringify(['setup', 'add', 'include', 'integrate', 'implement']), context: 'actions' }, { term: 'create', synonyms: JSON.stringify(['build', 'make', 'implement', 'develop', 'generate']), context: 'actions' }, { term: 'use', synonyms: JSON.stringify(['implement', 'utilize', 'apply', 'work-with', 'integrate']), context: 'actions' }, { term: 'customize', synonyms: JSON.stringify(['modify', 'extend', 'override', 'style', 'configure']), context: 'actions' } ]; const insertSynonym = this.db.prepare('INSERT OR REPLACE INTO synonyms (term, synonyms, context) VALUES (?, ?, ?)'); for (const { term, synonyms, context } of synonymData) { insertSynonym.run(term, synonyms, context); } } expandQuery(query, intent) { // Sanitize the query first to remove special characters that cause FTS5 errors const sanitizeQuery = (q) => { return q .replace(/[@]/g, ' ') // Remove @ symbols specifically .replace(/[^\w\s]/g, ' ') // Remove other special characters .replace(/\s+/g, ' ') .trim(); }; const sanitizedQuery = sanitizeQuery(query); const words = sanitizedQuery.toLowerCase().split(/\s+/).filter(w => w.length > 0); const expandedWords = []; const getSynonyms = this.db.prepare('SELECT synonyms FROM synonyms WHERE term = ?'); for (const word of words) { expandedWords.push(word); // Direct synonym lookup const result = getSynonyms.get(word); if (result) { const synonyms = JSON.parse(result.synonyms); expandedWords.push(...synonyms); } // Contextual expansion based on intent if (intent && intent.primary === 'component') { if (word.includes('button')) expandedWords.push('clickable', 'action', 'click'); if (word.includes('form')) expandedWords.push('input', 'validation', 'field'); if (word.includes('modal')) expandedWords.push('dialog', 'popup', 'overlay'); } // Fuzzy matching for common typos const fuzzyMatches = this.getFuzzyMatches(word); expandedWords.push(...fuzzyMatches); } return [...new Set(expandedWords)].join(' '); } getFuzzyMatches(word) { const matches = []; const threshold = 0.7; // Common typo corrections for shadcn-svelte terms const commonTypos = { 'btn': 'button', 'modle': 'modal', 'drpdown': 'dropdown', 'thm': 'theme', 'cmpnt': 'component', 'acessibility': 'accessibility', 'resposive': 'responsive', 'naviagation': 'navigation' }; if (commonTypos[word]) { matches.push(commonTypos[word]); } return matches; } async searchKnowledge(query, limit = 5) { const startTime = performance.now(); let error = false; let cacheHit = false; let resultCount = 0; try { // Check cache first const cachedResult = await this.cache.get(query, 'knowledge', limit); if (cachedResult) { cacheHit = true; resultCount = cachedResult.results.length; this.analytics.trackQuery(query, 0, resultCount, true, 'knowledge', false); return cachedResult; } // Enhanced query processing const enhancedQuery = enhanceQuery(query); const expandedQuery = this.expandQuery(enhancedQuery.processed, enhancedQuery.intent); const bm25Weights = getBM25Weights(enhancedQuery.intent.primary); // Try multiple query variations for better recall let results = []; for (const queryVariation of enhancedQuery.variations) { // Build BM25 weights string dynamically but safely const bm25WeightsStr = bm25Weights.join(', '); // Escape the query variation for SQL injection safety const escapedQuery = queryVariation.replace(/'/g, "''"); // Use direct SQL instead of parameterized query for FTS5 MATCH // Handle the query safely - if it already contains quotes from variation generation, use as-is let matchQuery; if (escapedQuery.startsWith('"') && escapedQuery.endsWith('"')) { // Already quoted phrase query matchQuery = escapedQuery; } else if (escapedQuery.includes(' ')) { // Multi-word query - quote it for phrase matching matchQuery = `"${escapedQuery}"`; } else { // Single word query matchQuery = `'${escapedQuery}'`; } const sql = ` SELECT k.*, snippet(knowledge_fts, 0, '<mark>', '</mark>', '...', 64) as highlighted_question, snippet(knowledge_fts, 1, '<mark>', '</mark>', '...', 64) as highlighted_answer, bm25(knowledge_fts, ${bm25WeightsStr}) as relevance_score FROM knowledge k JOIN knowledge_fts ON k.id = knowledge_fts.rowid WHERE knowledge_fts MATCH ${matchQuery} ORDER BY relevance_score DESC LIMIT ${limit * 2} `; const variationResults = this.db.query(sql).all(); results = [...results, ...variationResults]; } // Remove duplicates and sort by relevance const uniqueResults = this.deduplicateResults(results); const finalResults = uniqueResults .sort((a, b) => b.relevance_score - a.relevance_score) .slice(0, limit); resultCount = finalResults.length; const endTime = performance.now(); let responseTime = Math.round(endTime - startTime); // Ensure minimum response time of 1ms if search took any time if (responseTime === 0 && endTime > startTime) { responseTime = 1; } const searchResult = { results: finalResults.map(r => ({ id: r.id, question: r.question, answer: r.answer, category: r.category, tags: typeof r.tags === 'string' ? JSON.parse(r.tags) : r.tags, relevance_score: r.relevance_score, highlighted_question: r.highlighted_question, highlighted_answer: r.highlighted_answer })), total: finalResults.length, query, search_time_ms: responseTime, }; // Cache the result await this.cache.set(query, 'knowledge', limit, searchResult); // Track analytics this.analytics.trackQuery(query, responseTime, resultCount, false, 'knowledge', false); return searchResult; } catch (err) { error = true; this.analytics.trackQuery(query, 0, 0, false, 'knowledge', true); throw err; } } async searchExamples(query, limit = 5) { const startTime = performance.now(); let error = false; let cacheHit = false; let resultCount = 0; try { // Check cache first const cachedResult = await this.cache.get(query, 'examples', limit); if (cachedResult) { cacheHit = true; resultCount = cachedResult.results.length; this.analytics.trackQuery(query, 0, resultCount, true, 'examples', false); return cachedResult; } // Enhanced query processing const enhancedQuery = enhanceQuery(query); const expandedQuery = this.expandQuery(enhancedQuery.processed, enhancedQuery.intent); const bm25Weights = getBM25Weights(enhancedQuery.intent.primary); // Try multiple query variations for better recall let results = []; for (const queryVariation of enhancedQuery.variations) { // Build BM25 weights string dynamically but safely const bm25WeightsStr = bm25Weights.join(', '); // Escape the query variation for SQL injection safety const escapedQuery = queryVariation.replace(/'/g, "''"); // Use direct SQL instead of parameterized query for FTS5 MATCH // Handle the query safely - if it already contains quotes from variation generation, use as-is let matchQuery; if (escapedQuery.startsWith('"') && escapedQuery.endsWith('"')) { // Already quoted phrase query matchQuery = escapedQuery; } else if (escapedQuery.includes(' ')) { // Multi-word query - quote it for phrase matching matchQuery = `"${escapedQuery}"`; } else { // Single word query matchQuery = `'${escapedQuery}'`; } const sql = ` SELECT e.*, snippet(examples_fts, 0, '<mark>', '</mark>', '...', 64) as highlighted_title, snippet(examples_fts, 1, '<mark>', '</mark>', '...', 64) as highlighted_description, snippet(examples_fts, 2, '<mark>', '</mark>', '...', 64) as highlighted_component, snippet(examples_fts, 3, '<mark>', '</mark>', '...', 64) as highlighted_code, snippet(examples_fts, 5, '<mark>', '</mark>', '...', 64) as highlighted_tags, bm25(examples_fts, ${bm25WeightsStr}) as relevance_score FROM examples e JOIN examples_fts ON e.id = examples_fts.rowid WHERE examples_fts MATCH ${matchQuery} ORDER BY relevance_score DESC LIMIT ${limit * 2} `; const variationResults = this.db.query(sql).all(); results = [...results, ...variationResults]; } // Remove duplicates and sort by relevance const uniqueResults = this.deduplicateResults(results); const finalResults = uniqueResults .sort((a, b) => b.relevance_score - a.relevance_score) .slice(0, limit); resultCount = finalResults.length; const endTime = performance.now(); let responseTime = Math.round(endTime - startTime); // Ensure minimum response time of 1ms if search took any time if (responseTime === 0 && endTime > startTime) { responseTime = 1; } const searchResult = { results: finalResults.map(r => ({ id: r.id, title: r.title, description: r.description, component: r.component, code: r.code, category: r.category, tags: typeof r.tags === 'string' ? JSON.parse(r.tags) : r.tags, complexity: r.complexity, dependencies: typeof r.dependencies === 'string' ? JSON.parse(r.dependencies) : r.dependencies, relevance_score: r.relevance_score, highlighted_title: r.highlighted_title, highlighted_description: r.highlighted_description, highlighted_component: r.highlighted_component, highlighted_code: r.highlighted_code, highlighted_tags: r.highlighted_tags })), total: finalResults.length, query, search_time_ms: responseTime, }; // Cache the result await this.cache.set(query, 'examples', limit, searchResult); // Track analytics this.analytics.trackQuery(query, responseTime, resultCount, false, 'examples', false); return searchResult; } catch (err) { error = true; this.analytics.trackQuery(query, 0, 0, false, 'examples', true); throw err; } } async searchComponents(query, limit = 5) { const startTime = performance.now(); let error = false; let cacheHit = false; let resultCount = 0; try { // Check cache first const cachedResult = await this.cache.get(query, 'components', limit); if (cachedResult) { cacheHit = true; resultCount = cachedResult.results.length; this.analytics.trackQuery(query, 0, resultCount, true, 'components', false); return cachedResult; } // Enhanced query processing const enhancedQuery = enhanceQuery(query); const expandedQuery = this.expandQuery(enhancedQuery.processed, enhancedQuery.intent); const bm25Weights = getBM25Weights(enhancedQuery.intent.primary); // Try multiple query variations for better recall let results = []; for (const queryVariation of enhancedQuery.variations) { // Build BM25 weights string dynamically but safely const bm25WeightsStr = bm25Weights.join(', '); // Escape the query variation for SQL injection safety const escapedQuery = queryVariation.replace(/'/g, "''"); // Use direct SQL instead of parameterized query for FTS5 MATCH // Handle the query safely - if it already contains quotes from variation generation, use as-is let matchQuery; if (escapedQuery.startsWith('"') && escapedQuery.endsWith('"')) { // Already quoted phrase query matchQuery = escapedQuery; } else if (escapedQuery.includes(' ')) { // Multi-word query - quote it for phrase matching matchQuery = `"${escapedQuery}"`; } else { // Single word query matchQuery = `'${escapedQuery}'`; } const sql = ` SELECT c.*, snippet(components_fts, 4, '<mark>', '</mark>', '...', 64) as highlighted_usage, bm25(components_fts, ${bm25WeightsStr}) as relevance_score FROM components c JOIN components_fts ON c.id = components_fts.rowid WHERE components_fts MATCH ${matchQuery} ORDER BY relevance_score DESC LIMIT ${limit * 2} `; const variationResults = this.db.query(sql).all(); results = [...results, ...variationResults]; } // Remove duplicates and sort by relevance const uniqueResults = this.deduplicateResults(results); const finalResults = uniqueResults .sort((a, b) => b.relevance_score - a.relevance_score) .slice(0, limit); resultCount = finalResults.length; const endTime = performance.now(); let responseTime = Math.round(endTime - startTime); // Ensure minimum response time of 1ms if search took any time if (responseTime === 0 && endTime > startTime) { responseTime = 1; } const searchResult = { results: finalResults.map(r => ({ id: r.id, name: r.name, description: r.description, category: r.category, props: r.props, usage: r.usage, installation: r.installation, variants: typeof r.variants === 'string' ? JSON.parse(r.variants) : r.variants, dependencies: typeof r.dependencies === 'string' ? JSON.parse(r.dependencies) : r.dependencies, relevance_score: r.relevance_score, highlighted_usage: r.highlighted_usage })), total: finalResults.length, query, search_time_ms: responseTime, }; // Cache the result await this.cache.set(query, 'components', limit, searchResult); // Track analytics this.analytics.trackQuery(query, responseTime, resultCount, false, 'components', false); return searchResult; } catch (err) { error = true; this.analytics.trackQuery(query, 0, 0, false, 'components', true); throw err; } } populateFromFolders(dataDir, forceResync = false) { const knowledgeDir = join(dataDir, 'knowledge'); const examplesDir = join(dataDir, 'examples'); const componentsDir = join(dataDir, 'components'); // Check if data already exists const knowledgeCount = this.db.prepare('SELECT COUNT(*) as count FROM knowledge').get(); const examplesCount = this.db.prepare('SELECT COUNT(*) as count FROM examples').get(); const componentsCount = this.db.prepare('SELECT COUNT(*) as count FROM components').get(); if (!forceResync && knowledgeCount.count > 0 && examplesCount.count > 0 && componentsCount.count > 0) { console.log('📚 Database already populated. Use --force to resync.'); return; } console.log('🔄 Populating shadcn-svelte database...'); // Clear existing data if force resyncing if (forceResync) { this.db.exec('DELETE FROM knowledge'); this.db.exec('DELETE FROM examples'); this.db.exec('DELETE FROM components'); } // Load knowledge, examples, and components this.loadKnowledgeFromFolder(knowledgeDir); this.loadExamplesFromFolder(examplesDir); this.loadComponentsFromFolder(componentsDir); const newKnowledgeCount = this.db.prepare('SELECT COUNT(*) as count FROM knowledge').get(); const newExamplesCount = this.db.prepare('SELECT COUNT(*) as count FROM examples').get(); const newComponentsCount = this.db.prepare('SELECT COUNT(*) as count FROM components').get(); console.log(`✅ Database populated: ${newKnowledgeCount.count} knowledge, ${newExamplesCount.count} examples, ${newComponentsCount.count} components`); } loadKnowledgeFromFolder(folderPath) { if (!existsSync(folderPath)) return; const { loadJsonlFromDirectory } = require('./utils/jsonl.js'); const entries = loadJsonlFromDirectory(folderPath); const insertStmt = this.db.prepare(` INSERT INTO knowledge (question, answer, category, tags) VALUES (?, ?, ?, ?) `); this.db.transaction(() => { for (const entry of entries) { // Preprocess content for better indexing const processedAnswer = preprocessContent(entry.answer); insertStmt.run(entry.question, processedAnswer, entry.category || 'general', JSON.stringify(entry.tags || [])); } })(); // Index entries for advanced search this.indexKnowledgeEntries(entries); } loadExamplesFromFolder(folderPath) { if (!existsSync(folderPath)) return; const { loadJsonlFromDirectory } = require('./utils/jsonl.js'); const entries = loadJsonlFromDirectory(folderPath); const insertStmt = this.db.prepare(` INSERT INTO examples (title, description, component, code, category, tags, complexity, dependencies) VALUES (?, ?, ?, ?, ?, ?, ?, ?) `); this.db.transaction(() => { for (const entry of entries) { // Preprocess content for better indexing const processedCode = preprocessContent(entry.code); insertStmt.run(entry.title, entry.description, entry.component, processedCode, entry.category || 'general', JSON.stringify(entry.tags || []), entry.complexity || 'intermediate', JSON.stringify(entry.dependencies || [])); } })(); // Index entries for advanced search this.indexExampleEntries(entries); } loadComponentsFromFolder(folderPath) { if (!existsSync(folderPath)) return; const { loadJsonlFromDirectory } = require('./utils/jsonl.js'); const entries = loadJsonlFromDirectory(folderPath); const insertStmt = this.db.prepare(` INSERT OR REPLACE INTO components (name, description, category, props, usage, installation, variants, dependencies) VALUES (?, ?, ?, ?, ?, ?, ?, ?) `); this.db.transaction(() => { for (const entry of entries) { // Preprocess content for better indexing const processedUsage = preprocessContent(entry.usage); insertStmt.run(entry.name, entry.description, entry.category || 'general', JSON.stringify(entry.props || {}), processedUsage, entry.installation, JSON.stringify(entry.variants || []), JSON.stringify(entry.dependencies || [])); } })(); // Index entries for advanced search this.indexComponentEntries(entries); } // Helper methods for advanced indexing indexKnowledgeEntries(entries) { for (const entry of entries) { const metadata = this.indexing.generateMetadata(entry, 'knowledge'); this.indexing.indexKeywords(entry.id || 0, metadata.keywords); } } indexExampleEntries(entries) { for (const entry of entries) { const metadata = this.indexing.generateMetadata(entry, 'examples'); this.indexing.indexKeywords(entry.id || 0, metadata.keywords); } } indexComponentEntries(entries) { for (const entry of entries) { const metadata = this.indexing.generateMetadata(entry, 'components'); this.indexing.indexKeywords(entry.id || 0, metadata.keywords); } } deduplicateResults(results) { const seen = new Set(); return results.filter(result => { if (result.id === undefined || seen.has(result.id)) { return false; } seen.add(result.id); return true; }); } // Public methods for analytics and monitoring getAnalytics() { return this.analytics.getInsights(); } getCacheStats() { return this.cache.getStats(); } getIndexStats() { return this.indexing.getStats(); } getPerformanceMetrics() { return this.analytics.getMetrics(); } rebuildFTSTables() { // Simple approach: just delete and reinsert all data into FTS tables this.db.exec('DELETE FROM knowledge_fts'); this.db.exec('DELETE FROM examples_fts'); this.db.exec('DELETE FROM components_fts'); // Repopulate FTS tables with existing data this.db.exec(` INSERT INTO knowledge_fts(rowid, question, answer, category, tags) SELECT id, question, answer, category, tags FROM knowledge `); this.db.exec(` INSERT INTO examples_fts(rowid, title, description, component, code, category, tags, complexity, dependencies) SELECT id, title, description, component, code, category, tags, complexity, dependencies FROM examples `); this.db.exec(` INSERT INTO components_fts(rowid, name, description, category, props, usage, installation, variants, dependencies) SELECT id, name, description, category, props, usage, installation, variants, dependencies FROM components `); console.log('✅ FTS tables repopulated'); } optimizeDatabase() { // Optimize FTS5 indexes this.db.exec('INSERT INTO knowledge_fts(knowledge_fts) VALUES("optimize")'); this.db.exec('INSERT INTO examples_fts(examples_fts) VALUES("optimize")'); this.db.exec('INSERT INTO components_fts(components_fts) VALUES("optimize")'); // Optimize advanced indexes this.indexing.optimize(); // Clear stale cache entries this.cache.clear(); console.log('✅ Database optimized successfully'); } exportReport() { return this.analytics.exportReport(); } close() { // Save analytics and clean up this.analytics.destroy(); this.cache.destroy(); this.db.close(); } } //# sourceMappingURL=ShadcnSvelteSearchDB.js.map