UNPKG

@restnfeel/agentc-starter-kit

Version:

한국어 기업용 CMS 모듈 - Task Master AI와 함께 빠르게 웹사이트를 구현할 수 있는 재사용 가능한 컴포넌트 시스템

200 lines (197 loc) 8.31 kB
import { QueryProcessor } from './query-processor.js'; class HybridRetriever { constructor(vectorStore, embeddingModel, config = {}) { this.documentIndex = new Map(); // chunkId -> content this.vectorStore = vectorStore; this.embeddingModel = embeddingModel; this.queryProcessor = new QueryProcessor(); this.config = { vectorWeight: 0.7, keywordWeight: 0.3, maxResults: 10, minScore: 0.1, enableReranking: true, ...config, }; // Ensure weights sum to 1 const totalWeight = this.config.vectorWeight + this.config.keywordWeight; if (Math.abs(totalWeight - 1.0) > 0.01) { this.config.vectorWeight = this.config.vectorWeight / totalWeight; this.config.keywordWeight = this.config.keywordWeight / totalWeight; } } async search(query, k) { const maxResults = k || this.config.maxResults; // Step 1: Process the query const processedQuery = await this.queryProcessor.processQuery(query); // Step 2: Perform vector search const vectorResults = await this.performVectorSearch(processedQuery, maxResults * 2); // Step 3: Perform keyword search const keywordResults = await this.performKeywordSearch(processedQuery, maxResults * 2); // Step 4: Combine and score results const combinedResults = this.combineResults(vectorResults, keywordResults, processedQuery); // Step 5: Re-rank if enabled let finalResults = combinedResults; if (this.config.enableReranking) { finalResults = await this.rerankResults(combinedResults, processedQuery); } // Step 6: Filter by minimum score and limit results return finalResults .filter((result) => result.score >= this.config.minScore) .slice(0, maxResults); } async performVectorSearch(processedQuery, k) { try { // Use the processed query for better semantic matching const searchQuery = processedQuery.processed || processedQuery.original; return await this.vectorStore.search(searchQuery, k); } catch (error) { console.warn("Vector search failed:", error); return []; } } async performKeywordSearch(processedQuery, k) { try { const keywords = processedQuery.keywords; if (keywords.length === 0) { return []; } const results = []; // Simple keyword matching - in production, you'd use a proper search engine for (const [chunkId, content] of this.documentIndex) { const matches = []; let score = 0; const lowerContent = content.toLowerCase(); for (const keyword of keywords) { const regex = new RegExp(`\\b${keyword}\\b`, "gi"); const matchCount = (content.match(regex) || []).length; if (matchCount > 0) { matches.push(keyword); // TF-like scoring score += matchCount * Math.log(content.length / (matchCount + 1)); } } if (matches.length > 0) { // Normalize score by content length score = score / Math.sqrt(content.length); results.push({ chunkId, score, matches }); } } return results.sort((a, b) => b.score - a.score).slice(0, k); } catch (error) { console.warn("Keyword search failed:", error); return []; } } combineResults(vectorResults, keywordResults, processedQuery) { const combinedMap = new Map(); // Add vector search results for (const result of vectorResults) { const key = result.chunk.id; const weightedScore = result.score * this.config.vectorWeight; combinedMap.set(key, { ...result, score: weightedScore, }); } // Add or update with keyword search results for (const keywordResult of keywordResults) { const key = keywordResult.chunkId; const weightedScore = keywordResult.score * this.config.keywordWeight; if (combinedMap.has(key)) { // Combine scores const existing = combinedMap.get(key); existing.score += weightedScore; } else { // Find the corresponding vector result or create a minimal one const vectorResult = vectorResults.find((vr) => vr.chunk.id === key); if (vectorResult) { combinedMap.set(key, { ...vectorResult, score: weightedScore, }); } } } return Array.from(combinedMap.values()).sort((a, b) => b.score - a.score); } async rerankResults(results, processedQuery) { // Simple re-ranking based on multiple factors return results .map((result) => { let bonusScore = 0; // Bonus for exact phrase matches if (result.chunk.content .toLowerCase() .includes(processedQuery.original.toLowerCase())) { bonusScore += 0.1; } // Bonus for keyword density const keywordCount = processedQuery.keywords.reduce((count, keyword) => { const regex = new RegExp(`\\b${keyword}\\b`, "gi"); return count + (result.chunk.content.match(regex) || []).length; }, 0); const keywordDensity = keywordCount / result.chunk.content.split(" ").length; bonusScore += Math.min(keywordDensity * 0.5, 0.2); // Bonus for recent documents (if timestamp available) if (result.document.metadata.updatedAt) { try { // Ensure updatedAt is a Date object const updatedAt = result.document.metadata.updatedAt instanceof Date ? result.document.metadata.updatedAt : new Date(result.document.metadata.updatedAt); const daysSinceUpdate = (Date.now() - updatedAt.getTime()) / (1000 * 60 * 60 * 24); if (daysSinceUpdate < 30) { bonusScore += 0.05; } } catch (dateError) { console.warn("Failed to process updatedAt for bonus scoring:", dateError); } } // Bonus for language match if (processedQuery.language === result.document.metadata.language) { bonusScore += 0.05; } return { ...result, score: Math.min(result.score + bonusScore, 1.0), }; }) .sort((a, b) => b.score - a.score); } // Method to update the document index for keyword search updateDocumentIndex(chunkId, content) { this.documentIndex.set(chunkId, content); } // Method to remove from document index removeFromIndex(chunkId) { this.documentIndex.delete(chunkId); } // Method to update search configuration updateConfig(config) { this.config = { ...this.config, ...config }; // Ensure weights sum to 1 const totalWeight = this.config.vectorWeight + this.config.keywordWeight; if (Math.abs(totalWeight - 1.0) > 0.01) { this.config.vectorWeight = this.config.vectorWeight / totalWeight; this.config.keywordWeight = this.config.keywordWeight / totalWeight; } } // Method to get search statistics getSearchStats() { return { indexSize: this.documentIndex.size, config: this.config, }; } // Method to clear the index clearIndex() { this.documentIndex.clear(); } } export { HybridRetriever }; //# sourceMappingURL=hybrid-retriever.js.map