@restnfeel/agentc-starter-kit
Version:
한국어 기업용 CMS 모듈 - Task Master AI와 함께 빠르게 웹사이트를 구현할 수 있는 재사용 가능한 컴포넌트 시스템
200 lines (197 loc) • 8.31 kB
JavaScript
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