UNPKG

@solapi/mcp-server

Version:

MCP server for SOLAPI document search and integration

164 lines 6.09 kB
/** * @class 검색 엔진 * @description TF-IDF 알고리즘과 역 인덱스를 사용한 고성능 검색 */ export class TfidfSearchEngine { documents = []; invertedIndex = new Map(); // 인덱스 맵핑: 키워드 -> 문서 tfidfScores = new Map(); // TF-IDF 점수 캐싱 isIndexed = false; constructor() { this.documents = []; this.invertedIndex = new Map(); this.tfidfScores = new Map(); this.isIndexed = false; } /** * 문서 추가 및 인덱싱 * @param docs - 문서 배열 */ addDocuments(docs) { this.documents = docs; this.buildInvertedIndex(); this.calculateTFIDF(); this.isIndexed = true; } /** * 역 인덱스 구축 - O(n*m) 시간 복잡도 */ buildInvertedIndex() { this.invertedIndex.clear(); this.documents.forEach((doc, docIndex) => { const allText = `${doc.title} ${doc.content} ${doc.tags?.join(' ') || ''}`.toLowerCase(); const terms = this.tokenize(allText); terms.forEach(term => { if (!this.invertedIndex.has(term)) { this.invertedIndex.set(term, new Set()); } this.invertedIndex.get(term).add(docIndex); }); }); } /** * TF-IDF 점수 계산 */ calculateTFIDF() { this.tfidfScores.clear(); const docCount = this.documents.length; this.documents.forEach((doc, docIndex) => { const allText = `${doc.title} ${doc.content} ${doc.tags?.join(' ') || ''}`.toLowerCase(); const terms = this.tokenize(allText); const termFreq = new Map(); // TF 계산 terms.forEach(term => { termFreq.set(term, (termFreq.get(term) || 0) + 1); }); const docScores = new Map(); // TF-IDF 계산 termFreq.forEach((tf, term) => { const df = this.invertedIndex.get(term)?.size || 1; const idf = Math.log(docCount / df); const tfidf = (tf / terms.length) * idf; docScores.set(term, tfidf); }); this.tfidfScores.set(docIndex, docScores); }); } /** * 텍스트 토큰화 * @param text - 토큰화할 텍스트 * @returns 토큰 배열 */ tokenize(text) { return text .toLowerCase() .replace(/[^\w\s가-힣]/g, ' ') .split(/\s+/) .filter(term => term.length > 1); } /** * 고급 검색 수행 * @param query - 검색 쿼리 * @param limit - 결과 제한 (기본값: 5) * @returns 정렬된 검색 결과 */ search(query, limit = 5) { if (!this.isIndexed) { return []; } const queryTerms = this.tokenize(query); if (queryTerms.length === 0) { return []; } const docScores = new Map(); // 각 쿼리 용어에 대해 일치하는 문서 찾기 queryTerms.forEach(term => { const matchingDocs = this.invertedIndex.get(term); if (matchingDocs) { matchingDocs.forEach(docIndex => { const tfidfScore = this.tfidfScores.get(docIndex)?.get(term) || 0; // 가중치 계산 (제목에서 발견시 2배, 키워드에서 발견시 1.5배) let weight = 1; const doc = this.documents[docIndex]; if (doc && doc.title.toLowerCase().includes(term)) { weight = 2; } else if (doc && doc.tags?.some(tag => tag.toLowerCase().includes(term))) { weight = 1.5; } const currentScore = docScores.get(docIndex) || 0; docScores.set(docIndex, currentScore + (tfidfScore * weight)); }); } }); // 정확한 구문 매칭 보너스 점수 const queryLower = query.toLowerCase(); this.documents.forEach((doc, index) => { const titleMatch = doc.title.toLowerCase().includes(queryLower); const contentMatch = doc.content.toLowerCase().includes(queryLower); if (titleMatch || contentMatch) { const bonus = titleMatch ? 1.0 : 0.5; const currentScore = docScores.get(index) || 0; docScores.set(index, currentScore + bonus); } }); // 결과 정렬 및 제한 return Array.from(docScores.entries()) .sort((a, b) => b[1] - a[1]) // 점수순 내림차순 .slice(0, limit) .map(([docIndex, score]) => { const doc = this.documents[docIndex]; if (!doc) { throw new Error(`Document at index ${docIndex} not found`); } return { id: doc.id, title: doc.title, content: doc.content, url: doc.url, score: parseFloat(score.toFixed(3)), snippet: this.generateSnippet(doc.content, query), metadata: doc.metadata }; }); } /** * 검색 결과 스니펫 생성 * @param content - 문서 내용 * @param query - 검색 쿼리 * @returns 스니펫 문자열 */ generateSnippet(content, query) { const queryLower = query.toLowerCase(); const contentLower = content.toLowerCase(); const index = contentLower.indexOf(queryLower); if (index === -1) { return content.substring(0, 200) + '...'; } const start = Math.max(0, index - 100); const end = Math.min(content.length, index + query.length + 100); const snippet = content.substring(start, end); return (start > 0 ? '...' : '') + snippet + (end < content.length ? '...' : ''); } } //# sourceMappingURL=tfidfSearchEngine.js.map