apple-hig-mcp
Version:
High-performance MCP server providing instant access to Apple's Human Interface Guidelines via hybrid static/dynamic content delivery
216 lines • 7.91 kB
JavaScript
/**
* Enhanced Search Indexer Service
* Single Responsibility: Generate and manage search indices with keyword-based search
*/
export class SearchIndexerService {
contentProcessor;
searchIndex = {};
constructor(contentProcessor, _keywordConfig) {
this.contentProcessor = contentProcessor;
console.log('[SearchIndexer] ⚡ Using keyword search only');
}
/**
* Add section to search index
*/
addSection(section) {
// Validate required fields
if (!section.title) {
throw new Error('Section title is required');
}
// Skip sections without content
if (!section.content && !section.structuredContent) {
return;
}
const keywords = this.extractKeywords(section);
const snippet = this.generateSnippet(section);
const structureAnalysis = this.analyzeContentStructure(section);
const entry = {
id: section.id,
title: section.title,
platform: section.platform,
category: section.category,
url: section.url,
keywords,
snippet,
quality: section.quality,
lastUpdated: new Date(),
...structureAnalysis
};
this.searchIndex[section.id] = entry;
}
/**
* Generate search index metadata
*/
generateIndex() {
return {
metadata: {
version: '2.0-keyword',
totalSections: Object.keys(this.searchIndex).length,
semanticEnabled: false,
semanticStats: {
totalIndexedSections: 0,
isInitialized: false,
modelLoaded: false,
config: {}
},
lastUpdated: new Date().toISOString(),
indexType: 'keyword-only'
},
keywordIndex: this.searchIndex,
searchCapabilities: {
keywordSearch: true,
exactMatch: true,
fieldBoostingSupported: true,
structuredContentSearch: true,
crossPlatformSearch: true
}
};
}
/**
* Perform keyword-based search
*/
async search(query, sectionsOrOptions, options) {
// Handle both old and new method signatures
let searchOptions = {};
if (Array.isArray(sectionsOrOptions)) {
// Old signature: search(query, sections, options)
searchOptions = options || {};
}
else {
// New signature: search(query, options)
searchOptions = sectionsOrOptions || {};
}
const { limit = 10, platform, category, minScore = 0.1 } = searchOptions;
// Simple keyword-based search through the index
const results = Object.values(this.searchIndex)
.map(entry => {
let score = 0;
const queryLower = query.toLowerCase();
const queryWords = queryLower.split(/\s+/).filter(word => word.length > 2);
// Title matches (highest weight)
if (entry.title.toLowerCase().includes(queryLower)) {
score += 3.0;
}
else {
const titleMatches = queryWords.filter(word => entry.title.toLowerCase().includes(word)).length;
score += titleMatches * 1.5;
}
// Keyword matches
const keywordMatches = queryWords.filter(word => entry.keywords.some(keyword => keyword.toLowerCase().includes(word))).length;
score += keywordMatches * 1.0;
// Snippet matches
if (entry.snippet.toLowerCase().includes(queryLower)) {
score += 0.5;
}
return { ...entry, score, relevanceScore: score };
})
.filter(result => {
if (result.score < minScore)
return false;
if (platform && result.platform !== platform && result.platform !== 'universal')
return false;
if (category && result.category !== category)
return false;
return true;
})
.sort((a, b) => b.score - a.score)
.slice(0, limit);
return results;
}
/**
* Extract keywords from section content
*/
extractKeywords(section) {
const text = `${section.title} ${section.content || ''} ${section.url}`.toLowerCase();
const words = text.match(/\b\w{3,}\b/g) || [];
// Remove common words
const commonWords = new Set([
'the', 'and', 'for', 'are', 'but', 'not', 'you', 'all', 'can', 'had', 'her', 'was', 'one', 'our', 'out', 'day', 'get', 'has', 'him', 'his', 'how', 'man', 'new', 'now', 'old', 'see', 'two', 'way', 'who', 'boy', 'did', 'its', 'let', 'put', 'say', 'she', 'too', 'use'
]);
const keywords = [...new Set(words)]
.filter(word => !commonWords.has(word))
.slice(0, 20); // Limit to top 20 keywords
return keywords;
}
/**
* Generate snippet for search results
*/
generateSnippet(section) {
const content = section.content || '';
const cleanContent = content.replace(/#{1,6}\s+/g, '').replace(/\n+/g, ' ').trim();
return cleanContent.length > 200 ? cleanContent.substring(0, 200) + '...' : cleanContent;
}
/**
* Analyze content structure
*/
analyzeContentStructure(section) {
const content = section.content || '';
const structured = section.structuredContent;
const analysis = {
hasStructuredContent: content.includes('##') || content.includes('###') || !!structured,
hasGuidelines: /guidelines?|best practices?|recommendations?/i.test(content) ||
!!(structured?.guidelines && structured.guidelines.length > 0),
hasExamples: /example|for instance|such as/i.test(content) ||
!!(structured?.examples && structured.examples.length > 0),
hasSpecifications: /specification|requirement|standard/i.test(content) ||
!!structured?.specifications,
conceptCount: (content.match(/##\s+/g) || []).length +
(structured?.relatedConcepts?.length || 0)
};
return analysis;
}
/**
* Clear search index
*/
clear() {
this.searchIndex = {};
}
/**
* Get index size
*/
getIndexSize() {
return Object.keys(this.searchIndex).length;
}
/**
* Get indexing statistics
*/
getStatistics() {
const entries = Object.values(this.searchIndex);
const averageKeywordsPerSection = entries.length > 0
? entries.reduce((sum, entry) => sum + entry.keywords.length, 0) / entries.length
: 0;
return {
totalSections: entries.length,
averageKeywordCount: averageKeywordsPerSection,
keywordIndex: {
totalEntries: entries.length,
averageKeywordsPerSection,
lastUpdated: new Date().toISOString()
},
capabilities: {
supportedFeatures: [
'keyword-search',
'field-boosting',
'exact-match',
'partial-match',
'platform-filtering',
'category-filtering'
],
semanticSearchEnabled: false
}
};
}
/**
* Check if keyword search is available
*/
isKeywordSearchEnabled() {
return true;
}
/**
* Check if semantic search is enabled (always false now)
*/
isSemanticSearchEnabled() {
return false;
}
}
//# sourceMappingURL=search-indexer.service.js.map