Bladeren bron

upgrade searcher

latapp 9 maanden geleden
bovenliggende
commit
013d3ed970
2 gewijzigde bestanden met toevoegingen van 151 en 81 verwijderingen
  1. 59 16
      public/main/js/app.js
  2. 92 65
      public/main/js/utils/searching.js

+ 59 - 16
public/main/js/app.js

@@ -162,7 +162,7 @@ function setupSearchListener() {
         // Limpiar el timer anterior si existe
         clearTimeout(debounceTimer);
         const specialCases = {
-            "shop": ["shop","cerveza"]
+            "shop": ["cerveza"]
         }
         // Agregar una clase de "buscando" para el feedback visual
         productListElement.style.opacity = "0.7";
@@ -176,19 +176,44 @@ function setupSearchListener() {
                 renderProductsWithAnimation(Allproducts);
                 return;
             }
-            const finded = Allproducts.filter(product => {
-                const productKeywords = [product.name.split(" "), product.type.toLowerCase()].flat();
-                const specialKeywords = specialCases[product.type.toLowerCase()] || [];
-                return smartSearch([...productKeywords, ...specialKeywords], searchTerm).length > 0;
-            });
+            const finded = smartSearch(Allproducts, searchTerm);
+            console.log(finded)
             // Renderizar con animación
-            renderProductsWithAnimation(finded);
+            renderProductsWithAnimation(finded, false, searchTerm);
         }, 200);
     });
 }
 //#endregion
 //#region ===== Utilidad =====
 
+  /**
+   * Ordena resultados por relevancia
+   */
+function sortByRelevance(items) {
+    console.log("Sorting results by relevance...", items);
+    const sortedResults = items
+      .sort((a, b) => {
+        // Primero por score (descendente)
+        if (a.score !== b.score) {
+          return b.score - a.score;
+        }
+        
+        // Luego por distancia (ascendente)
+        if (a.distance !== b.distance) {
+          return a.distance - b.distance;
+        }
+        
+        // Finalmente por longitud del texto (ascendente)
+        const aText = typeof a.item === 'string' ? a.item : JSON.stringify(a.item);
+        const bText = typeof b.item === 'string' ? b.item : JSON.stringify(b.item);
+        return aText.length - bText.length;
+      })
+      .map(result => result.item);
+
+      console.log("Sorted results: ",sortedResults);
+      return sortedResults;
+  }
+
 function setCookie(name, value, days) {
     const expires = new Date(Date.now() + days * 24 * 60 * 60 * 1000).toUTCString();
     document.cookie = `${name}=${value}; expires=${expires}; path=/`;
@@ -228,7 +253,7 @@ async function checkCache() {
             const [emailInputContainer, pinInputContainer] = [document.getElementById("emailInputContainer"), document.getElementById("pinInputContainer")];
             const [emailInput, pinInput] = [document.getElementById("emailInput"), document.getElementById("pinInput")];
             const loginTitle = document.getElementById("loginTitle");
-            loginTitle.textContent = `¡Bienvenido de nuevo, ${user.name}!`;
+            loginTitle.textContent = `¡Bienvenido ${user.name.split(" ")[0]}!`;
             emailInputContainer.classList.add("hidden");
             emailInput.removeAttribute("required");
             pinInputContainer.classList.add("hidden");
@@ -285,15 +310,19 @@ async function createCategories(products) {
     return categoryContainers;
 }
 
-async function renderProducts(products) {
+async function renderProducts(products, groupInCategories = true, searchTerm = "") {
     if (!productListElement) return;
 
     const template = document.getElementById("product-card-template");
     if (!template) return;
 
     productListElement.innerHTML = "";
-
-    const categoryContainers = await createCategories(products);
+    let categoryContainers = [];
+    if (groupInCategories) {
+        categoryContainers = await createCategories(products);
+    }else{
+        categoryContainers = [ { category: searchTerm, container: productListElement } ];
+    }
     if (products.length === 0) {
         const noProductsMessage = document.createElement("p");
         noProductsMessage.textContent = "No hay productos disponibles.";
@@ -302,7 +331,12 @@ async function renderProducts(products) {
     }
 
     for (const { category, container } of categoryContainers) {
-        let productsInCategory = products.filter(product => product.type === category);
+        let productsInCategory
+        if (groupInCategories){
+            productsInCategory = products.filter(product => product.type === category);
+        }else{
+            productsInCategory = products;
+        }
 
         if (productsInCategory.length === 0) continue;
 
@@ -328,7 +362,7 @@ async function renderProducts(products) {
     }
 }
 
-async function renderProductsWithAnimation(products) {
+async function renderProductsWithAnimation(products, groupInCategories = true, searchTerm = "") {
     if (!productListElement) return;
 
     const template = document.getElementById("product-card-template");
@@ -340,8 +374,12 @@ async function renderProductsWithAnimation(products) {
     
     setTimeout(async () => {
         productListElement.innerHTML = "";
-
-        const categoryContainers = await createCategories(products);
+        let categoryContainers = [];
+        if (groupInCategories){
+            categoryContainers = await createCategories(products);
+        }else {
+            categoryContainers = [ { category: searchTerm, container: productListElement } ];
+        }
         console.log("Category containers created:", categoryContainers);
         if (products.length === 0) {
             const noProductsMessage = document.createElement("p");
@@ -365,7 +403,12 @@ async function renderProductsWithAnimation(products) {
 
         let animationDelay = 0;
         for (const { category, container } of categoryContainers) {
-            let productsInCategory = products.filter(product => product.type === category);
+            let productsInCategory = [];
+            if (groupInCategories){
+                productsInCategory = products.filter(product => product.type === category);
+            }else {
+                productsInCategory = products
+            }
 
             if (productsInCategory.length === 0) continue;
 

+ 92 - 65
public/main/js/utils/searching.js

@@ -6,13 +6,22 @@ class SmartSearch {
       maxLevenshteinDistance: 3,
       proportionalTolerance: 0.25,
       prefixTolerance: 2,
-      
+      wordTolerance: 1,
+
       // 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
       cacheEnabled: true,
@@ -31,7 +40,6 @@ class SmartSearch {
    */
   search(items, searchTerm, options = {}) {
     const { 
-      key = null, 
       sortByRelevance = true, 
       caseSensitive = false 
     } = options;
@@ -42,14 +50,8 @@ class SmartSearch {
       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
     const finalResults = sortByRelevance 
       ? this.sortByRelevance(results) 
@@ -58,22 +60,19 @@ class SmartSearch {
     // Limitar resultados
     const limitedResults = finalResults.slice(0, this.config.maxResults);
 
-    // Guardar en cache
-    if (this.config.cacheEnabled) {
-      this.cache.set(cacheKey, limitedResults);
-    }
+  
     return limitedResults;
   }
 
   /**
    * Realiza la búsqueda y scoring
    */
-  performSearch(items, searchTerm, key, caseSensitive) {
+  performSearch(items, searchTerm, caseSensitive) {
     const results = [];
     const termLength = searchTerm.length;
 
     for (const item of items) {
-      const text = this.extractText(item, key);
+      const text = this.extractText(item);
       const normalizedText = this.normalizeTerm(text, caseSensitive);
       
       const match = this.calculateMatch(searchTerm, normalizedText, termLength);
@@ -88,7 +87,7 @@ class SmartSearch {
 
         // Early exit si tenemos suficientes coincidencias exactas
         if (this.config.enableEarlyExit && 
-            match.score === this.config.exactMatchScore && 
+            match.score === this.config.exactNameMatchScore && 
             results.length >= 10) {
           break;
         }
@@ -104,53 +103,101 @@ class SmartSearch {
    */
   calculateMatch(searchTerm, text, termLength) {
     // 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
-    if (text.includes(searchTerm)) {
+    if (name.includes(searchTerm)) {
       const position = text.indexOf(searchTerm);
       // Bonus si empieza con el término
       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 };
     }
 
-    // 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) {
+      //verificar prefijo con tolerancia
       const prefix = text.substring(0, termLength + 2);
-      const distance = this.getLevenshteinDistance(searchTerm, prefix);
+      let distance = this.getLevenshteinDistance(searchTerm, prefix);
       
       if (distance <= this.config.prefixTolerance) {
         const score = this.config.fuzzyScore + (2 - distance) * 10;
         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) {
+      
       const maxAllowedDistance = Math.min(
         this.config.maxLevenshteinDistance,
         Math.floor(termLength * this.config.proportionalTolerance)
       );
-      
-      const distance = this.getLevenshteinDistance(searchTerm, text);
-      
+      let distance = this.getLevenshteinDistance(searchTerm, name);
+
       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 };
       }
+
+      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 };
   }
 
@@ -182,10 +229,6 @@ class SmartSearch {
       [str1, str2] = [str2, str1];
     }
 
-    const cacheKey = `${str1}|${str2}`;
-    if (this.levenshteinCache.has(cacheKey)) {
-      return this.levenshteinCache.get(cacheKey);
-    }
 
     // Algoritmo optimizado de Levenshtein
     let previousRow = Array.from({ length: str1.length + 1 }, (_, i) => i);
@@ -206,8 +249,6 @@ class SmartSearch {
     }
 
     const distance = previousRow[str1.length];
-    this.levenshteinCache.set(cacheKey, distance);
-    console.log(`Levenshtein distance between "${str1}" and "${str2}": ${distance}`);
     return distance;
   }
 
@@ -252,15 +293,11 @@ class SmartSearch {
     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();
   }
 
-  /**
-   * 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