Filtres à facettes: comment cuisiner et avec quoi servir

De quoi s'agit-il 



Comment faire une recherche à facettes dans une boutique en ligne? Comment les valeurs sont-elles générées dans les filtres de recherche à facettes? Comment le choix d'une valeur dans un filtre affecte-t-il les valeurs des filtres adjacents? À la recherche de réponses, j'ai atteint la cinquième page des résultats de recherche Google. Je n'ai pas trouvé d'informations exhaustives, j'ai dû les découvrir moi-même. L'article décrit:



  1. comment l'interface utilisateur réagit lorsque l'utilisateur utilise des filtres;
  2. algorithme pour générer des valeurs de filtre; 
  3. Modèles de requêtes ElasticSearch et structures d'index avec explications.


Il n'y a pas de solutions toutes faites ici. Vous ne pouvez pas copier et coller. Pour résoudre votre propre problème, vous devez vous y plonger.







Des concepts pour clarifier les choses 



Recherche en texte intégral - recherchez des produits par mot ou expression. Pour l'utilisateur, il s'agit d'un champ de saisie de texte avec le bouton "Rechercher", disponible sur n'importe quelle page du site.



Recherche à facettes - recherche d'un produit par plusieurs caractéristiques: couleur, taille, taille mémoire, prix, etc. Pour l'utilisateur, c'est un ensemble de filtres. Chaque filtre est associé à une seule caractéristique et vice versa. Les valeurs de filtre sont toutes les valeurs possibles pour une caractéristique. L'utilisateur voit des filtres sur la page de section, catégorie, sur la page avec des résultats de recherche en texte intégral. Lorsque l'utilisateur sélectionne une valeur, le filtre est considéré comme actif.



Comportement du filtre à facettes 



En bref, un filtre filtre les produits et filtre les options de sélection dans d'autres filtres. 



Filtres produits



C'est facile avec ça. L'utilisateur a sélectionné:



  1. une valeur, voit les produits qui correspondent à la valeur;
  2. plusieurs valeurs dans un filtre, voit les produits qui correspondent à au moins un;
  3. valeurs dans plusieurs filtres, voit les produits qui correspondent à la valeur de chaque filtre.


En termes d'algèbre booléenne: il y a un "ET" logique entre les filtres, un "OU" logique entre les valeurs du filtre . Logique simple. 



Choix de filtres dans d'autres filtres



"Eh bien ... quelles sont les options - affichées, quoi non - cachées" - c'est ainsi que l'entreprise décrit le comportement des filtres. Cela semble logique. En pratique, cela fonctionne comme ceci:



  1. Nous allons dans la section Téléphones, nous voyons les filtres par caractéristiques: Marque, Diagonale, Mémoire. Chaque filtre contient des valeurs. 
  2. . . 1.
  3. . , . , 2. 
  4. . . , 3.
  5. «» . 3 ..


Le nombre de valeurs de filtre dépend du nombre de produits: plus il y a de produits avec des valeurs caractéristiques différentes, plus il y a de valeurs dans le filtre. L'utilisateur a réduit le nombre de produits dans la sélection pour les filtres restants lorsqu'il a sélectionné une marque. Cela a abouti à une mise à jour des listes de valeurs.



Cela donne lieu à une règle universelle: les valeurs de filtre sont extraites d'une sélection de produits formée par le reste des filtres actifs.



Chaque filtre actif a sa propre sélection de produits.



Si nous avons N filtres et:



  • ne sont pas actifs, alors l'échantillon est général. Il en va de même pour tous les filtres et correspond aux résultats de la recherche;
  • M est actif et M <N, alors le nombre d'échantillons est M + 1, où 1 est l'échantillon sur lequel tous les filtres actifs sont appliqués. Il en va de même pour tous les filtres inactifs et coïncide avec les résultats de la recherche;
  • actif M, et N = M, puis le nombre d'échantillons N. Chaque filtre a son propre échantillon.


Finalement, lorsque l'utilisateur sélectionne une valeur de filtre de facette, ce qui suit se produit: 



  1. une sélection de recherche de produits est formée; 
  2. les valeurs des filtres inactifs sont extraites de la sélection de recherche;
  3. pour chaque filtre actif, un nouvel échantillon est formé et de nouvelles valeurs de filtres actifs en sont extraites.


La question se pose - comment mettre en œuvre cela dans la pratique?



Implémentation d'Elasticsearch (ES)



Les caractéristiques des produits ne sont pas universelles, vous ne trouverez donc pas ici de structure d'index prête à l'emploi pour stocker des produits ou des requêtes toutes faites. Au lieu de cela, il y aura des liens vers la documentation expliquant comment créer vous-même les index et requêtes "corrects". «Correct» - basé sur mon expérience et mes connaissances. 



Types de zones de texte "corrects"



En ES, nous nous intéressons à 2 types de données: 



  • texte pour la recherche en texte intégral. Les champs de ce type ne peuvent pas être utilisés pour la comparaison exacte, le tri, l'agrégation;
  • mot-clé pour les chaînes impliquées dans les opérations de comparaison exacte, de tri, d'agrégation.


ES analyse les valeurs dans le champ de texte et crée un dictionnaire pour la recherche en texte intégral. Les valeurs du champ de mot - clé sont indexées comme reçues. L'agrégation et le tri ne sont disponibles que pour les champs de mots-clés.



L'utilisateur utilise des caractéristiques dans les deux cas: en recherche plein texte et à travers des filtres. ES ne vous permet pas d'attribuer 2 types à un seul champ, mais propose d'autres solutions: les



champs 

PUT my_index
{
  «mappings»: {
    «properties»: {
      «some_property»: { 
        «type»: «text», // 1
        «fields»: { // 2
          «raw»: { 
            «type»: «keyword»
          }
        }
      }
    }
  }
}


  1. nous déclarons les caractéristiques du produit dans un champ de type  texte
  2. à l'aide du paramètre fields, créez un champ virtuel enfant de type mot-clé . Virtuel, car il est présent dans l'index et non dans la description du produit. ES enregistre automatiquement les données dans le champ enfant au fur et à mesure de leur réception.


Donc pour chaque caractéristique.  



Dans les requêtes pour des opérations de comparaison, de tri et d'agrégation exactes, vous devez utiliser un champ virtuel enfant de type mot-clé . Dans l'exemple, il s'agit de some_property.raw . Pour la recherche de texte - parent.



copy_to .

PUT my_index
{
  «mappings»: {
    «properties»: {
      «all_properties»: { // 1
        «type»: «text»
      },      «some_property_1»: {
        «type»: «keyword»,
        «copy_to»: «all_properties» // 2 
      },
      «some_property_2»: {
        «type»: «keyword»,
        «copy_to»: «all_properties»
      }
    }
  }


  1. Créez un champ virtuel avec le type de texte dans l'index .    
  2. Déclarez chaque caractéristique comme motclé avec le paramètre copy_to . Spécifiez le champ virtuel avec la valeur du paramètre. ES copie la valeur de toutes les caractéristiques dans le champ virtuel lorsque le document est enregistré. 


Pour les opérations de comparaison exacte, de tri et d'agrégation, vous devez utiliser le champ de caractéristique, pour la recherche de texte - un champ avec les valeurs de toutes les caractéristiques.



Les deux approches créent des champs supplémentaires dans l'index qui ne sont pas présents dans la structure du document d'origine. Par conséquent, pour créer une requête, vous devez connaître la structure de l'index.



Je préfère l'option copy_to . Ensuite, pour construire une requête de recherche en texte intégral, il suffit de connaître un champ avec une copie des valeurs de toutes les caractéristiques. 



Demandes 



Rechercher des produits 



Nous supposerons que la structure de l'index est la même que dans la variante copy_to . Pour la recherche en texte intégral dans ES, la construction de correspondance est utilisée , pour la comparaison avec les valeurs des filtres à facettes - requête de termesLa requête booléenne combine les constructions en une seule requête. Ce sera quelque chose comme ça:



{
  «query» : { 
    «bool»: {
      «must»: {
        «match»: { 
          «virtual_field_for_fulltext_searching»: {
            «query»: «some text»
          }
        }
      },
      «filter»: { 
        «must»: [
           {«property_1»: [ «value_1_1», …, «value_1_n»]},
          … 
           {«property_n»: [ «value_n_1», …, «value_n_m»]}
        ]
      }
    }
  }
}


query.bool.must.match recherche de texte intégral principal 

query.bool.filter filtre pour affiner la requête principale. doit à l' intérieur signifie "et" logique entre les filtres. Le tableau de valeurs de chaque filtre est un booléen ou.   



Pour les valeurs de filtre



La clause d'agrégation des termes regroupe les produits par valeur de caractéristique et calcule la quantité dans chaque groupe. Cette opération est appelée agrégation. La difficulté est que pour chaque filtre actif, l'  agrégation des termes doit être effectuée sur une sélection de produits formés par d'autres filtres actifs. Pour les filtres inactifs - sur une sélection qui correspond aux résultats de la recherche. La construction d' agrégation de filtre vous permet de créer une sélection distincte pour chaque opération d'agrégation et de «regroupement» en une seule requête.



La structure de la demande sera comme ceci: 

{
  «size»: 0,
  «query» : { 
    «bool»: {
      «must»: {
        «match»: {
          «field_for_fulltext_searching»: {
            «fuzziness»: 2,
            «query»: «some text»
          }
        }
      },
      «filter»: {
      
      }
    }
  },
  «aggs» : {
    «inavtive_filter_agg» : {
      «filter» : {        … 
      },
      «aggs»: {
        «some_inavtive_filter_subagg»: { 
          «terms» : {
            «field» : «some_property»
          }
        },
        ... 
        «some_other_inavtive_filter_subagg»: {
          «terms» : {
            «field» : «some_other_property»
          }
        }
      }
      
    }, 
    «active_filter_1_agg» : {
       «filter»: {
         …        },
       «aggs»: {
          «active_filter_1_subagg»: {
             «terms» : {
                «field»: «property_1»
             }
          }
       }
    },
    …,  
    «active_filter_N_agg» : {
       «filter»: {
         …        
      },
       «aggs»: {
          «active_filter_N_subagg»: {
             «terms» : {
                «field»: «property_N»
             }
          }
       }
    }
  }
}


query.bool - requête principale, les opérations de filtrage sont effectuées dans son contexte. Cela consiste en: 

  • match - demande de recherche en texte intégral;
  • filtres - filtre par caractéristiques qui ne sont pas associées aux filtres à facettes et doivent être présentes dans n'importe quel sous-ensemble. Cela peut être un filtre par in_stock, is_visible, si vous souhaitez toujours afficher uniquement les produits en stock ou uniquement les produits visibles.


aggs.inavtive_filter_agg - l' agrégation des filtres à facettes inactifs se compose de:

  • filtre -   conditions par caractéristiques formées par des filtres à facettes actifs. Avec la requête principale, une sélection de produits est formée, sur laquelle les agrégations enfants de cette section sont effectuées; 
  • aggs est un objet d'agrégation nommé pour chaque filtre inactif


aggs.active_filter_1_agg - agrégation d'obtention des valeurs du premier des filtres de facette actifs. Chaque conception est associée à un filtre à facettes. Consiste en: 

  • filtre - conditions par caractéristiques formées par des filtres à facettes actifs, à l'exception du filtre actuel. Avec la requête principale, il forme une sélection de produits, sur laquelle l'agrégation enfant de cette section est effectuée;
  • aggs - un objet d'une agrégation selon la caractéristique du filtre à facettes actuellement actif. 


Il est important de spécifier "size": 0 , sinon vous obtiendrez une liste de produits correspondant à la requête principale sans agrégations. 



Finalement 



Reçu deux demandes:



  1. pour les résultats de recherche, renvoie les produits à afficher à l'utilisateur;
  2. pour les valeurs de filtre, effectue l'agrégation, renvoie les valeurs de filtre et le nombre de produits avec cette valeur.


Chaque requête est autonome, il est donc préférable de les exécuter de manière asynchrone.   



PS J'avoue qu'il existe des approches et des outils plus «corrects» pour résoudre le problème de la recherche à facettes. Je serais reconnaissant pour des informations supplémentaires et des exemples dans les commentaires.



All Articles