@binsarjr/shadcn-svelte-mcp
Version:
MCP server for shadcn-svelte development with curated knowledge, components, and examples
954 lines • 43.9 kB
JavaScript
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