apple-hig-mcp
Version:
High-performance MCP server providing instant access to Apple's Human Interface Guidelines via hybrid static/dynamic content delivery
165 lines • 7.13 kB
JavaScript
/**
* Enhanced Keyword Search Service
*
* Provides improved search functionality without requiring TensorFlow.
* Uses advanced keyword matching, stemming, and relevance scoring.
*/
export class EnhancedKeywordSearchService {
config;
defaultConfig = {
maxResults: 20,
minScore: 0.1,
exactMatchBoost: 3.0,
titleBoost: 2.0,
keywordBoost: 1.5,
platformBoost: 1.2,
categoryBoost: 1.1
};
synonyms = {
'authentication': ['login', 'sign in', 'signin', 'auth', 'credential'],
'login': ['sign in', 'signin', 'authentication', 'auth'],
'password': ['passkey', 'credentials', 'security', 'authentication'],
'security': ['privacy', 'protection', 'secure', 'safety', 'password'],
'privacy': ['security', 'protection', 'data protection', 'confidential'],
'button': ['control', 'input', 'interaction', 'tap', 'press'],
'navigation': ['menu', 'nav', 'routing', 'flow', 'path'],
'error': ['validation', 'feedback', 'alert', 'warning', 'message'],
'validation': ['error', 'feedback', 'check', 'verify', 'validate'],
'ui': ['interface', 'user interface', 'design', 'layout'],
'ux': ['user experience', 'experience', 'usability', 'interaction']
};
commonWords = new Set([
'the', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with', 'by',
'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been', 'have', 'has', 'had',
'do', 'does', 'did', 'will', 'would', 'should', 'could', 'can', 'may',
'might', 'must', 'shall', 'this', 'that', 'these', 'those', 'it', 'its'
]);
constructor(config = {}) {
this.config = config;
this.config = { ...this.defaultConfig, ...config };
}
/**
* Enhanced search with synonym expansion and better relevance scoring
*/
async search(query, sections, platform, category) {
const expandedQuery = this.expandQueryWithSynonyms(query);
const queryTokens = this.tokenizeQuery(expandedQuery);
const results = [];
for (const section of sections) {
if (!section)
continue;
// Apply filters
if (platform && platform !== 'universal' &&
section.platform !== platform && section.platform !== 'universal') {
continue;
}
if (category && section.category !== category) {
continue;
}
const score = this.calculateRelevanceScore(section, query, queryTokens);
if (score >= this.config.minScore) {
results.push({
id: section.id,
title: section.title,
platform: section.platform,
category: section.category,
url: section.url,
snippet: section.snippet || '',
relevanceScore: score,
type: 'section',
highlights: this.generateHighlights(section, queryTokens)
});
}
}
// Sort by relevance and return top results
return results
.sort((a, b) => (b.relevanceScore || 0) - (a.relevanceScore || 0))
.slice(0, this.config.maxResults);
}
/**
* Expand query with synonyms for better matching
*/
expandQueryWithSynonyms(query) {
const words = query.toLowerCase().split(/\s+/);
const expandedWords = new Set(words);
for (const word of words) {
if (this.synonyms[word]) {
this.synonyms[word].forEach(synonym => expandedWords.add(synonym));
}
}
return Array.from(expandedWords).join(' ');
}
/**
* Tokenize query into meaningful search terms
*/
tokenizeQuery(query) {
return query
.toLowerCase()
.split(/\s+/)
.filter(word => word.length > 2 && !this.commonWords.has(word))
.map(word => word.replace(/[^\w]/g, ''))
.filter(word => word.length > 0);
}
/**
* Calculate comprehensive relevance score
*/
calculateRelevanceScore(section, originalQuery, queryTokens) {
let score = 0;
const originalQueryLower = originalQuery.toLowerCase();
const titleLower = (section.title || '').toLowerCase();
const snippetLower = (section.snippet || '').toLowerCase();
const keywords = section.keywords || [];
// 1. Exact phrase match in title (highest priority)
if (titleLower.includes(originalQueryLower)) {
score += this.config.exactMatchBoost * this.config.titleBoost;
}
// 2. Exact phrase match in snippet
if (snippetLower.includes(originalQueryLower)) {
score += this.config.exactMatchBoost;
}
// 3. Individual word matches in title
const titleWordMatches = queryTokens.filter(token => titleLower.includes(token));
score += titleWordMatches.length * this.config.titleBoost;
// 4. Individual word matches in keywords
const keywordMatches = queryTokens.filter(token => keywords.some((keyword) => keyword.toLowerCase().includes(token)));
score += keywordMatches.length * this.config.keywordBoost;
// 5. Individual word matches in snippet
const snippetWordMatches = queryTokens.filter(token => snippetLower.includes(token));
score += snippetWordMatches.length * 0.5;
// 6. Bonus for platform/category relevance
if (section.platform === 'universal') {
score += this.config.platformBoost * 0.5; // Universal content gets a small boost
}
// 7. Penalty for very short content (likely incomplete)
if (section.snippet && section.snippet.length < 100) {
score *= 0.8;
}
// 8. Bonus for authentication-related sections
const authTerms = ['sign', 'login', 'auth', 'privacy', 'security', 'password'];
const hasAuthTerms = authTerms.some(term => titleLower.includes(term) || snippetLower.includes(term) ||
keywords.some((k) => k.toLowerCase().includes(term)));
if (hasAuthTerms && queryTokens.some(token => authTerms.includes(token))) {
score += 0.5;
}
return Math.round(score * 100) / 100; // Round to 2 decimal places
}
/**
* Generate highlights for search results
*/
generateHighlights(section, queryTokens) {
const highlights = [];
const title = section.title || '';
const snippet = section.snippet || '';
// Find highlighted terms in title
for (const token of queryTokens) {
if (title.toLowerCase().includes(token)) {
highlights.push(`title:${token}`);
}
if (snippet.toLowerCase().includes(token)) {
highlights.push(`content:${token}`);
}
}
return [...new Set(highlights)]; // Remove duplicates
}
}
//# sourceMappingURL=enhanced-keyword-search.service.js.map