@aiquants/fuzzy-search
Version:
Advanced fuzzy search library with Levenshtein distance, n-gram indexing, and Web Worker support
2 lines • 14.5 kB
JavaScript
var p={threshold:.4,caseSensitive:!1,learningWeight:1.2,debounceMs:300,multiTermOperator:"and",autoSearchOnIndexRebuild:!0,customWeights:{},ngramSize:2,minNgramOverlap:1,sortBy:"relevance",sortOrder:"desc",enableIndexFiltering:!0,enableLevenshtein:!0,parallelSearchStrategy:"balanced",indexWorkerOptions:{strategy:"hybrid",threshold:.4,ngramOverlapThreshold:.3,minCandidatesRatio:.1,maxCandidatesRatio:.5,jaroWinklerPrefix:.1,maxResults:1e3,relevanceFieldWeight:.2,relevancePerfectMatchBonus:.1},levenshteinWorkerOptions:{threshold:.3,lengthSimilarityThreshold:.1,partialMatchBonus:.1,lengthDiffPenalty:1,maxResults:1e3,relevanceFieldWeight:.15}};var R="[fuzzy-search]",k=class{constructor(){this.stats={totalSearches:0,totalProcessingTime:0,averageSearchTime:0,lastSearchTime:0,errorCount:0,timestamp:Date.now()};this.detailedStats=new Map;this.performanceHistory=[];this.operationDistribution=new Map;this.fieldStats=new Map;this.metrics={timings:{averageProcessingTime:0,fastestProcessing:1/0,slowestProcessing:0,lastProcessingTime:0},throughput:{operationsPerSecond:0,totalOperations:0},quality:{successRate:1,errorRate:0,timeoutRate:0}}}updateStats(a,e=!0){this.stats.totalSearches++,this.stats.totalProcessingTime+=a,this.stats.averageSearchTime=this.stats.totalProcessingTime/this.stats.totalSearches,this.stats.lastSearchTime=a,this.metrics.timings.lastProcessingTime=a,this.metrics.timings.averageProcessingTime=this.stats.averageSearchTime,this.metrics.timings.fastestProcessing=Math.min(this.metrics.timings.fastestProcessing,a),this.metrics.timings.slowestProcessing=Math.max(this.metrics.timings.slowestProcessing,a),this.metrics.throughput.totalOperations++,this.metrics.timings.averageProcessingTime>0&&(this.metrics.throughput.operationsPerSecond=1e3/this.metrics.timings.averageProcessingTime),e||this.stats.errorCount++;let t=this.metrics.throughput.totalOperations;this.metrics.quality.successRate=(t-this.stats.errorCount)/t,this.metrics.quality.errorRate=this.stats.errorCount/t}getStats(){return{...this.stats}}getMetrics(){return{...this.metrics}}resetStats(){this.stats={totalSearches:0,totalProcessingTime:0,averageSearchTime:0,lastSearchTime:0,errorCount:0,timestamp:Date.now()},this.metrics={timings:{averageProcessingTime:0,fastestProcessing:1/0,slowestProcessing:0,lastProcessingTime:0},throughput:{operationsPerSecond:0,totalOperations:0},quality:{successRate:1,errorRate:0,timeoutRate:0}},this.detailedStats.clear(),this.performanceHistory=[],this.operationDistribution.clear(),this.fieldStats.clear(),console.log(`${R} \u{1F4CA} ${this.getWorkerType()} Worker: Statistics reset completed`)}updateWorkerStats(a,e,t={}){this.updateStats(e,!0);let r=this.operationDistribution.get(a)||0;this.operationDistribution.set(a,r+1),Object.entries(t).forEach(([s,i])=>{this.detailedStats.set(s,i)}),this.addPerformanceEntry({timestamp:Date.now(),operationType:a,processingTime:e,queryLength:t.queryLength||0,itemCount:t.itemCount||0,resultCount:t.resultCount||0,additionalMetrics:t})}updateFieldStats(a,e){let t=this.fieldStats.get(a)||{matchCount:0,averageScore:0,totalScore:0,bestScore:0,worstScore:1};t.matchCount+=1,t.totalScore+=e,t.averageScore=t.totalScore/t.matchCount,t.bestScore=Math.max(t.bestScore,e),t.worstScore=Math.min(t.worstScore,e),this.fieldStats.set(a,t)}addPerformanceEntry(a){this.performanceHistory.push(a),this.performanceHistory.length>100&&(this.performanceHistory=this.performanceHistory.slice(-100))}getWorkerStats(){return{basic:{...this.stats,timestamp:Date.now()},workerSpecific:{workerType:this.getWorkerType(),operationCount:this.getTotalOperations(),operationDistribution:Object.fromEntries(this.operationDistribution),specificMetrics:this.getSpecificMetrics()},analysis:{fieldPerformance:Object.fromEntries(this.fieldStats),performanceInsights:this.generatePerformanceInsights(),recentHistory:this.performanceHistory.slice(-10)}}}sendStatsUpdate(){let a={type:"stats",id:`stats-${Date.now()}`,stats:this.getWorkerStats(),metrics:this.getMetrics(),processingTime:0,workerType:this.getWorkerType(),operationMeta:{}};console.log(`${R} \u{1F4CA} ${this.getWorkerType()} Worker: Sending unified stats update`),self.postMessage(a)}getTotalOperations(){return Array.from(this.operationDistribution.values()).reduce((a,e)=>a+e,0)}generatePerformanceInsights(){if(this.performanceHistory.length===0)return{recentAverageProcessingTime:0,recentAverageResultCount:0,throughputPerSecond:0,totalHistoryEntries:0,mostCommonQueryLength:0,bestPerformingField:"",similarityDistribution:{highSimilarity:0,mediumSimilarity:0,lowSimilarity:0,veryLowSimilarity:0}};let a=this.performanceHistory.slice(-10),e=a.reduce((n,o)=>n+o.processingTime,0)/a.length,t=a.reduce((n,o)=>n+o.resultCount,0)/a.length,r=new Map;this.performanceHistory.forEach(n=>{let o=r.get(n.queryLength)||0;r.set(n.queryLength,o+1)});let s=0,i=0;r.forEach((n,o)=>{n>i&&(i=n,s=o)});let c="",l=0;return this.fieldStats.forEach((n,o)=>{n.averageScore>l&&(l=n.averageScore,c=o)}),{recentAverageProcessingTime:e,recentAverageResultCount:t,throughputPerSecond:e>0?1e3/e:0,totalHistoryEntries:this.performanceHistory.length,mostCommonQueryLength:s,bestPerformingField:c,similarityDistribution:{highSimilarity:0,mediumSimilarity:0,lowSimilarity:0,veryLowSimilarity:0}}}handleMessage(a){console.warn(`${R} \u26A0\uFE0F BaseWorker.handleMessage: No implementation provided`)}};var b="[fuzzy-search]",O=class extends k{constructor(){super();this.currentItems=[];this.currentSearchFields=[];this.distanceDistribution=new Map;this.queryLengthStats=new Map;this.similarityThresholdStats={above80:0,above60:0,above40:0,below40:0};this.levenshteinStats={totalCalculations:0,totalDistanceCalculations:0,averageDistance:0,minDistance:1/0,maxDistance:0,partialMatches:0,perfectMatches:0};this.setupMessageHandler()}getWorkerType(){return"levenshtein"}getSpecificMetrics(){return{totalCalculations:this.levenshteinStats.totalCalculations,totalDistanceCalculations:this.levenshteinStats.totalDistanceCalculations,averageDistance:this.levenshteinStats.averageDistance,minDistance:this.levenshteinStats.minDistance!==1/0?this.levenshteinStats.minDistance:void 0,maxDistance:this.levenshteinStats.maxDistance,partialMatches:this.levenshteinStats.partialMatches,perfectMatches:this.levenshteinStats.perfectMatches,distanceDistribution:Object.fromEntries(this.distanceDistribution),similarityThresholds:this.similarityThresholdStats,queryLengthStats:Object.fromEntries(this.queryLengthStats)}}resetStats(){super.resetStats(),this.distanceDistribution.clear(),this.queryLengthStats.clear(),this.similarityThresholdStats={above80:0,above60:0,above40:0,below40:0},this.levenshteinStats={totalCalculations:0,totalDistanceCalculations:0,averageDistance:0,minDistance:1/0,maxDistance:0,partialMatches:0,perfectMatches:0},console.log(`${b} \u{1F4CA} Levenshtein Worker: All statistics have been reset`)}updateSimilarityStats(e){e>=.8?this.similarityThresholdStats.above80++:e>=.6?this.similarityThresholdStats.above60++:e>=.4?this.similarityThresholdStats.above40++:this.similarityThresholdStats.below40++}setupMessageHandler(){self.onmessage=e=>{this.handleMessage(e)}}handleMessage(e){let t=performance.now(),{data:r}=e;try{switch(r.type){case"ping":self.postMessage({type:"pong",id:r.id});break;case"levenshteinSearch":this.handleLevenshteinSearch(r,t);break;case"getStats":this.handleGetStats(r,t);break;case"resetStats":this.handleResetStats(r,t);break;default:this.sendError(r.id,`Unknown request type: ${r.type}`,t)}}catch(s){this.sendError(r.id,s instanceof Error?s.message:String(s),t)}}handleLevenshteinSearch(e,t){if(console.log(`${b} \u{1F4CF} Levenshtein Worker: Starting search for "${e.query}" with ${e.items?.length||0} items`),!(e.query&&e.items)||!e.searchFields){this.sendError(e.id,"Query, items, and searchFields are required for levenshteinSearch",t);return}let{query:r,items:s,searchFields:i,options:c}=e;this.currentItems=s,this.currentSearchFields=i,console.log(`${b} \u{1F4CF} Levenshtein Worker: Configuration - query: "${r}", fields: [${i.join(", ")}], threshold: ${c?.levenshteinWorkerOptions?.threshold||p.levenshteinWorkerOptions?.threshold||.3}`);let l=c?.levenshteinWorkerOptions?.threshold||p.levenshteinWorkerOptions?.threshold||.3,n=this.calculateSimilarity(r,Array.from({length:s.length},(h,u)=>u),c||{},l),o=performance.now()-t;this.updateWorkerStats("levenshteinSearch",o,{queryLength:r.length,itemCount:s.length,resultCount:n.length,threshold:l,averageDistance:n.reduce((h,u)=>h+(1-u.score),0)/(n.length||1)}),n.forEach(h=>{h.matchedFields.forEach(u=>{this.updateFieldStats(u,h.score)})});let m={type:"levenshteinSearch",id:e.id,results:n,stats:this.getWorkerStats(),metrics:this.getMetrics(),processingTime:o,workerType:this.getWorkerType(),operationMeta:{threshold:l,totalCalculations:n.length,averageDistance:n.reduce((h,u)=>h+(1-u.score),0)/(n.length||1)}};console.log(`${b} \u26A1 Levenshtein Worker: Calculated similarity for ${s.length} items, returned ${n.length} results`),console.log(`${b} \u{1F4CA} Levenshtein Worker: Sending unified response with stats:`,{totalCalculations:m.operationMeta?.totalCalculations,averageDistance:m.operationMeta?.averageDistance,statsKeys:Object.keys(m.stats||{}),processingTime:m.processingTime}),self.postMessage(m),this.sendStatsUpdate()}handleGetStats(e,t){let r={type:"stats",id:e.id,stats:this.getWorkerStats(),metrics:this.getMetrics(),processingTime:performance.now()-t,workerType:this.getWorkerType(),operationMeta:{}};self.postMessage(r)}handleResetStats(e,t){this.resetStats();let r={type:"resetStats",id:e.id,stats:this.getWorkerStats(),metrics:this.getMetrics(),processingTime:performance.now()-t,workerType:this.getWorkerType(),operationMeta:{resetCompleted:!0}};self.postMessage(r)}calculateSimilarity(e,t,r,s){console.log(`${b} \u{1F504} Levenshtein Worker: Processing ${t.length} candidates with threshold ${s}`);let i=[],c=r.caseSensitive?e:e.toLowerCase(),l=this.getQueryTokens(e,r),o=this.getMultiTermOperator(r)==="and"&&l.length>1,m=r.sortBy||p.sortBy,h=m==="relevance",u=h?r.levenshteinWorkerOptions?.relevanceFieldWeight??p.levenshteinWorkerOptions?.relevanceFieldWeight??.15:0,x=h?r.levenshteinWorkerOptions?.partialMatchBonus??p.levenshteinWorkerOptions?.partialMatchBonus??.1:0;for(let g of t){if(g>=this.currentItems.length)continue;let d=this.currentItems[g];if(o&&!this.itemMatchesTokens(d,l,r))continue;let y=0,S=0,v=[],D=[];for(let F of this.currentSearchFields){let W=String(d[F]??"");if(!W)continue;let z=r.caseSensitive?W:W.toLowerCase(),M=this.calculateLevenshteinSimilarity(c,z,r);this.levenshteinStats.totalCalculations++;let T=M;D.push(T),T>0&&v.push(F),M>S&&(S=M),T>y&&(y=T)}let w=S;h&&v.length>0&&(w=S+v.length*u+x),w>=s&&i.push({item:d,score:w,baseScore:S,matchedFields:v,originalIndex:g})}let P=r.sortOrder||p.sortOrder,f;m==="relevance"?f=i.sort((g,d)=>P==="asc"?g.score-d.score:d.score-g.score):m==="original"?f=i.sort((g,d)=>{let y=g.originalIndex??0,S=d.originalIndex??0;return P==="asc"?y-S:S-y}):f=i.sort((g,d)=>P==="asc"?g.score-d.score:d.score-g.score);let C=r.levenshteinWorkerOptions?.maxResults;return typeof C=="number"?f.slice(0,C):f}getMultiTermOperator(e){return e.multiTermOperator??p.multiTermOperator??"or"}getQueryTokens(e,t){return(t.caseSensitive?e:e.toLowerCase()).split(/\s+/).filter(s=>s.length>0)}itemMatchesTokens(e,t,r){if(t.length===0)return!0;let s=new Set(t);for(let i of this.currentSearchFields){if(s.size===0)break;let c=String(e[i]??"");if(!c)continue;let l=r.caseSensitive?c:c.toLowerCase();for(let n of t)if(s.has(n)&&l.includes(n)&&(s.delete(n),s.size===0))break}return s.size===0}calculateLevenshteinSimilarity(e,t,r){if(e===t)return this.levenshteinStats.perfectMatches++,1;if(e.length===0)return t.length===0?1:0;if(t.length===0)return 0;let s=Math.abs(e.length-t.length),i=Math.max(e.length,t.length),c=r.levenshteinWorkerOptions?.lengthDiffPenalty||p.levenshteinWorkerOptions?.lengthDiffPenalty||1,l=1-s/i*c,n=r.levenshteinWorkerOptions?.lengthSimilarityThreshold||p.levenshteinWorkerOptions?.lengthSimilarityThreshold||.1;if(l<n)return 0;let o=this.levenshteinDistance(e,t),m=1-o/i;this.levenshteinStats.totalDistanceCalculations+=1,this.levenshteinStats.minDistance=Math.min(this.levenshteinStats.minDistance,o),this.levenshteinStats.maxDistance=Math.max(this.levenshteinStats.maxDistance,o);let h=this.distanceDistribution.get(o)||0;this.distanceDistribution.set(o,h+1);let u=this.levenshteinStats.totalDistanceCalculations,x=this.levenshteinStats.averageDistance;return this.levenshteinStats.averageDistance=(x*(u-1)+o)/u,this.updateSimilarityStats(m),t.includes(e)&&this.levenshteinStats.partialMatches++,Math.max(0,m)}levenshteinDistance(e,t){let r=[];for(let s=0;s<=t.length;s++)r[s]=[s];for(let s=0;s<=e.length;s++)r[0][s]=s;for(let s=1;s<=t.length;s++)for(let i=1;i<=e.length;i++)t.charAt(s-1)===e.charAt(i-1)?r[s][i]=r[s-1][i-1]:r[s][i]=Math.min(r[s-1][i-1]+1,r[s][i-1]+1,r[s-1][i]+1);return r[t.length][e.length]}generatePerformanceInsights(){if(this.performanceHistory.length===0)return{recentAverageProcessingTime:0,recentAverageResultCount:0,throughputPerSecond:0,totalHistoryEntries:0,mostCommonQueryLength:0,bestPerformingField:"",similarityDistribution:{highSimilarity:0,mediumSimilarity:0,lowSimilarity:0,veryLowSimilarity:0}};let e=this.performanceHistory.slice(-10),t=e.reduce((s,i)=>s+i.processingTime,0)/e.length,r=e.reduce((s,i)=>s+i.resultCount,0)/e.length;return{recentAverageProcessingTime:t,recentAverageResultCount:r,throughputPerSecond:e.length>0?1e3/t:0,totalHistoryEntries:this.performanceHistory.length,mostCommonQueryLength:this.getMostCommonQueryLength(),bestPerformingField:this.getBestPerformingField(),similarityDistribution:{highSimilarity:this.similarityThresholdStats.above80,mediumSimilarity:this.similarityThresholdStats.above60,lowSimilarity:this.similarityThresholdStats.above40,veryLowSimilarity:this.similarityThresholdStats.below40}}}getMostCommonQueryLength(){let e=0,t=0;return this.queryLengthStats.forEach((r,s)=>{r.count>e&&(e=r.count,t=s)}),t}getBestPerformingField(){let e="",t=0;return this.fieldStats.forEach((r,s)=>{r.averageScore>t&&(t=r.averageScore,e=s)}),e}sendError(e,t,r){this.updateStats(performance.now()-r,!1);let s={type:"error",id:e,error:t,stats:this.getWorkerStats(),metrics:this.getMetrics(),processingTime:performance.now()-r,workerType:this.getWorkerType(),operationMeta:{}};self.postMessage(s)}};new O;
//# sourceMappingURL=levenshteinWorker.mjs.map