|
@@ -6,13 +6,22 @@ class SmartSearch {
|
|
|
maxLevenshteinDistance: 3,
|
|
maxLevenshteinDistance: 3,
|
|
|
proportionalTolerance: 0.25,
|
|
proportionalTolerance: 0.25,
|
|
|
prefixTolerance: 2,
|
|
prefixTolerance: 2,
|
|
|
-
|
|
|
|
|
|
|
+ wordTolerance: 1,
|
|
|
|
|
+
|
|
|
// Configuración de scoring
|
|
// Configuración de scoring
|
|
|
- exactMatchScore: 100,
|
|
|
|
|
- startsWithScore: 90,
|
|
|
|
|
- containsScore: 80,
|
|
|
|
|
- subsequenceScore: 60,
|
|
|
|
|
- fuzzyScore: 40,
|
|
|
|
|
|
|
+ exactNameMatchScore: 100,
|
|
|
|
|
+ exactCategoryMatchScore: 90,
|
|
|
|
|
+
|
|
|
|
|
+ startsWithNameScore: 92,
|
|
|
|
|
+ startsWithCategoryScore: 88,
|
|
|
|
|
+
|
|
|
|
|
+ containsNameScore: 80,
|
|
|
|
|
+ containsCategoryScore: 78,
|
|
|
|
|
+
|
|
|
|
|
+ fuzzyNameScore: 40,
|
|
|
|
|
+ fuzzyCategoryScore: 38,
|
|
|
|
|
+ fuzzyWordScore: 35,
|
|
|
|
|
+ subsequenceScore: 30,
|
|
|
|
|
|
|
|
// Optimizaciones
|
|
// Optimizaciones
|
|
|
cacheEnabled: true,
|
|
cacheEnabled: true,
|
|
@@ -31,7 +40,6 @@ class SmartSearch {
|
|
|
*/
|
|
*/
|
|
|
search(items, searchTerm, options = {}) {
|
|
search(items, searchTerm, options = {}) {
|
|
|
const {
|
|
const {
|
|
|
- key = null,
|
|
|
|
|
sortByRelevance = true,
|
|
sortByRelevance = true,
|
|
|
caseSensitive = false
|
|
caseSensitive = false
|
|
|
} = options;
|
|
} = options;
|
|
@@ -42,14 +50,8 @@ class SmartSearch {
|
|
|
return items;
|
|
return items;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // Verificar cache
|
|
|
|
|
- const cacheKey = this.getCacheKey(normalizedTerm, key);
|
|
|
|
|
- if (this.config.cacheEnabled && this.cache.has(cacheKey)) {
|
|
|
|
|
- return this.cache.get(cacheKey);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const results = this.performSearch(items, normalizedTerm, key, caseSensitive);
|
|
|
|
|
-
|
|
|
|
|
|
|
+ const results = this.performSearch(items, normalizedTerm, caseSensitive);
|
|
|
|
|
+ console.log(results)
|
|
|
// Ordenar por relevancia si está habilitado
|
|
// Ordenar por relevancia si está habilitado
|
|
|
const finalResults = sortByRelevance
|
|
const finalResults = sortByRelevance
|
|
|
? this.sortByRelevance(results)
|
|
? this.sortByRelevance(results)
|
|
@@ -58,22 +60,19 @@ class SmartSearch {
|
|
|
// Limitar resultados
|
|
// Limitar resultados
|
|
|
const limitedResults = finalResults.slice(0, this.config.maxResults);
|
|
const limitedResults = finalResults.slice(0, this.config.maxResults);
|
|
|
|
|
|
|
|
- // Guardar en cache
|
|
|
|
|
- if (this.config.cacheEnabled) {
|
|
|
|
|
- this.cache.set(cacheKey, limitedResults);
|
|
|
|
|
- }
|
|
|
|
|
|
|
+
|
|
|
return limitedResults;
|
|
return limitedResults;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* Realiza la búsqueda y scoring
|
|
* Realiza la búsqueda y scoring
|
|
|
*/
|
|
*/
|
|
|
- performSearch(items, searchTerm, key, caseSensitive) {
|
|
|
|
|
|
|
+ performSearch(items, searchTerm, caseSensitive) {
|
|
|
const results = [];
|
|
const results = [];
|
|
|
const termLength = searchTerm.length;
|
|
const termLength = searchTerm.length;
|
|
|
|
|
|
|
|
for (const item of items) {
|
|
for (const item of items) {
|
|
|
- const text = this.extractText(item, key);
|
|
|
|
|
|
|
+ const text = this.extractText(item);
|
|
|
const normalizedText = this.normalizeTerm(text, caseSensitive);
|
|
const normalizedText = this.normalizeTerm(text, caseSensitive);
|
|
|
|
|
|
|
|
const match = this.calculateMatch(searchTerm, normalizedText, termLength);
|
|
const match = this.calculateMatch(searchTerm, normalizedText, termLength);
|
|
@@ -88,7 +87,7 @@ class SmartSearch {
|
|
|
|
|
|
|
|
// Early exit si tenemos suficientes coincidencias exactas
|
|
// Early exit si tenemos suficientes coincidencias exactas
|
|
|
if (this.config.enableEarlyExit &&
|
|
if (this.config.enableEarlyExit &&
|
|
|
- match.score === this.config.exactMatchScore &&
|
|
|
|
|
|
|
+ match.score === this.config.exactNameMatchScore &&
|
|
|
results.length >= 10) {
|
|
results.length >= 10) {
|
|
|
break;
|
|
break;
|
|
|
}
|
|
}
|
|
@@ -104,53 +103,101 @@ class SmartSearch {
|
|
|
*/
|
|
*/
|
|
|
calculateMatch(searchTerm, text, termLength) {
|
|
calculateMatch(searchTerm, text, termLength) {
|
|
|
// 1. Coincidencia exacta
|
|
// 1. Coincidencia exacta
|
|
|
- if (text === searchTerm) {
|
|
|
|
|
- return { score: this.config.exactMatchScore, type: 'exact', distance: 0 };
|
|
|
|
|
|
|
+
|
|
|
|
|
+ const [name, category] = text.split("&cat&");
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ if (name === searchTerm) {
|
|
|
|
|
+ return { score: this.config.exactNameMatchScore, type: 'exact', distance: 0 };
|
|
|
|
|
+ }
|
|
|
|
|
+ if (category === searchTerm) {
|
|
|
|
|
+ return { score: this.config.exactCategoryMatchScore, type: 'exact', distance: 0 };
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 2. Contiene el término completo
|
|
// 2. Contiene el término completo
|
|
|
- if (text.includes(searchTerm)) {
|
|
|
|
|
|
|
+ if (name.includes(searchTerm)) {
|
|
|
const position = text.indexOf(searchTerm);
|
|
const position = text.indexOf(searchTerm);
|
|
|
// Bonus si empieza con el término
|
|
// Bonus si empieza con el término
|
|
|
const score = position === 0
|
|
const score = position === 0
|
|
|
- ? this.config.startsWithScore
|
|
|
|
|
- : this.config.containsScore;
|
|
|
|
|
|
|
+ ? this.config.startsWithNameScore
|
|
|
|
|
+ : this.config.containsNameScore;
|
|
|
return { score, type: position === 0 ? 'startsWith' : 'contains', distance: 0 };
|
|
return { score, type: position === 0 ? 'startsWith' : 'contains', distance: 0 };
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 3. Para términos cortos: verificar prefijo con tolerancia
|
|
|
|
|
|
|
+ if (category.includes(searchTerm)) {
|
|
|
|
|
+ const position = text.indexOf(searchTerm);
|
|
|
|
|
+ // Bonus si empieza con el término
|
|
|
|
|
+ const score = position === 0
|
|
|
|
|
+ ? this.config.startsWithCategoryScore
|
|
|
|
|
+ : this.config.containsCategoryScore;
|
|
|
|
|
+ return { score, type: position === 0 ? 'startsWith' : 'contains', distance: 0 };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 3. Para términos cortos:
|
|
|
if (termLength <= 4) {
|
|
if (termLength <= 4) {
|
|
|
|
|
+ //verificar prefijo con tolerancia
|
|
|
const prefix = text.substring(0, termLength + 2);
|
|
const prefix = text.substring(0, termLength + 2);
|
|
|
- const distance = this.getLevenshteinDistance(searchTerm, prefix);
|
|
|
|
|
|
|
+ let distance = this.getLevenshteinDistance(searchTerm, prefix);
|
|
|
|
|
|
|
|
if (distance <= this.config.prefixTolerance) {
|
|
if (distance <= this.config.prefixTolerance) {
|
|
|
const score = this.config.fuzzyScore + (2 - distance) * 10;
|
|
const score = this.config.fuzzyScore + (2 - distance) * 10;
|
|
|
return { score, type: 'prefix_fuzzy', distance };
|
|
return { score, type: 'prefix_fuzzy', distance };
|
|
|
}
|
|
}
|
|
|
|
|
+ // Verificar si el término es una palabra dentro del texto
|
|
|
|
|
+ const words = name.split(" ");
|
|
|
|
|
+ for (const word of words) {
|
|
|
|
|
+ const distance = this.getLevenshteinDistance(searchTerm, word);
|
|
|
|
|
+ if (distance <= this.config.wordTolerance) {
|
|
|
|
|
+ const score = this.config.fuzzyScore + (2 - distance) * 10;
|
|
|
|
|
+ return { score, type: 'word_fuzzy', distance };
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 4. Búsqueda por subsequencia (caracteres en orden)
|
|
|
|
|
- if (this.isSubsequence(searchTerm, text)) {
|
|
|
|
|
- const coverage = termLength / text.length;
|
|
|
|
|
- const score = this.config.subsequenceScore + (coverage * 20);
|
|
|
|
|
- return { score, type: 'subsequence', distance: text.length - termLength };
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 5. Distancia de Levenshtein para términos más largos
|
|
|
|
|
|
|
+ // 4. Distancia de Levenshtein para términos más largos
|
|
|
if (termLength > 4) {
|
|
if (termLength > 4) {
|
|
|
|
|
+
|
|
|
const maxAllowedDistance = Math.min(
|
|
const maxAllowedDistance = Math.min(
|
|
|
this.config.maxLevenshteinDistance,
|
|
this.config.maxLevenshteinDistance,
|
|
|
Math.floor(termLength * this.config.proportionalTolerance)
|
|
Math.floor(termLength * this.config.proportionalTolerance)
|
|
|
);
|
|
);
|
|
|
-
|
|
|
|
|
- const distance = this.getLevenshteinDistance(searchTerm, text);
|
|
|
|
|
-
|
|
|
|
|
|
|
+ let distance = this.getLevenshteinDistance(searchTerm, name);
|
|
|
|
|
+
|
|
|
if (distance <= maxAllowedDistance) {
|
|
if (distance <= maxAllowedDistance) {
|
|
|
- const score = this.config.fuzzyScore - (distance * 5);
|
|
|
|
|
|
|
+ const score = this.config.fuzzyNameScore - (distance * 5);
|
|
|
return { score: Math.max(1, score), type: 'fuzzy', distance };
|
|
return { score: Math.max(1, score), type: 'fuzzy', distance };
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ const words = name.split(" ");
|
|
|
|
|
+ const distances = []
|
|
|
|
|
+ for (const word of words) {
|
|
|
|
|
+ const distance = this.getLevenshteinDistance(searchTerm, word);
|
|
|
|
|
+ distances.push(distance);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ const minDistance = Math.min(...distances);
|
|
|
|
|
+
|
|
|
|
|
+ if (minDistance <= this.config.wordTolerance) {
|
|
|
|
|
+ const score = this.config.fuzzyWordScore + (2 - minDistance) * 10;
|
|
|
|
|
+ if (words.includes("summer")){
|
|
|
|
|
+ console.log(words, distances);
|
|
|
|
|
+ console.log("min distance", minDistance);
|
|
|
|
|
+ console.log("score", score);
|
|
|
|
|
+
|
|
|
}
|
|
}
|
|
|
|
|
+ return { score, type: 'word_fuzzy', distance: minDistance };
|
|
|
|
|
+ }}
|
|
|
|
|
|
|
|
|
|
+ // 5. Búsqueda por subsequencia (caracteres en orden)
|
|
|
|
|
+ if (this.isSubsequence(searchTerm, name)) {
|
|
|
|
|
+ const coverage = termLength / name.length;
|
|
|
|
|
+ const score = this.config.subsequenceScore + (coverage * 20);
|
|
|
|
|
+ if (score > this.config.subsequenceScore + 20) {
|
|
|
|
|
+ return { score, type: 'subsequence', distance: name.length - termLength };
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
return { score: 0, type: 'no_match', distance: Infinity };
|
|
return { score: 0, type: 'no_match', distance: Infinity };
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -182,10 +229,6 @@ class SmartSearch {
|
|
|
[str1, str2] = [str2, str1];
|
|
[str1, str2] = [str2, str1];
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- const cacheKey = `${str1}|${str2}`;
|
|
|
|
|
- if (this.levenshteinCache.has(cacheKey)) {
|
|
|
|
|
- return this.levenshteinCache.get(cacheKey);
|
|
|
|
|
- }
|
|
|
|
|
|
|
|
|
|
// Algoritmo optimizado de Levenshtein
|
|
// Algoritmo optimizado de Levenshtein
|
|
|
let previousRow = Array.from({ length: str1.length + 1 }, (_, i) => i);
|
|
let previousRow = Array.from({ length: str1.length + 1 }, (_, i) => i);
|
|
@@ -206,8 +249,6 @@ class SmartSearch {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const distance = previousRow[str1.length];
|
|
const distance = previousRow[str1.length];
|
|
|
- this.levenshteinCache.set(cacheKey, distance);
|
|
|
|
|
- console.log(`Levenshtein distance between "${str1}" and "${str2}": ${distance}`);
|
|
|
|
|
return distance;
|
|
return distance;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -252,15 +293,11 @@ class SmartSearch {
|
|
|
return normalized;
|
|
return normalized;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- extractText(item, key) {
|
|
|
|
|
- if (key && typeof item === 'object' && item !== null) {
|
|
|
|
|
- return String(item[key] || '');
|
|
|
|
|
|
|
+ extractText(item) {
|
|
|
|
|
+ if (!item.name){
|
|
|
|
|
+ console.log("Item sin nombre:", item);
|
|
|
}
|
|
}
|
|
|
- return String(item || '');
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- getCacheKey(term, key) {
|
|
|
|
|
- return `${term}:${key || 'default'}`;
|
|
|
|
|
|
|
+ return `${item.name}&cat&${item.type}`;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -271,16 +308,6 @@ class SmartSearch {
|
|
|
this.levenshteinCache.clear();
|
|
this.levenshteinCache.clear();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /**
|
|
|
|
|
- * Obtiene estadísticas del cache
|
|
|
|
|
- */
|
|
|
|
|
- getCacheStats() {
|
|
|
|
|
- return {
|
|
|
|
|
- searchCacheSize: this.cache.size,
|
|
|
|
|
- levenshteinCacheSize: this.levenshteinCache.size,
|
|
|
|
|
- totalMemoryUsage: this.cache.size + this.levenshteinCache.size
|
|
|
|
|
- };
|
|
|
|
|
- }
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Función de conveniencia para uso rápido
|
|
// Función de conveniencia para uso rápido
|