Technique

Procédure pour créer rapidement des expériences de recherche optimales basées sur React

On ne va pas vous mentir, mettre en place des expériences de recherche est une tâche ardue. À première vue, cela peut sembler simple : on construit une barre de recherche, on met des données dans une base de données, puis on se sert des entrées utilisateur pour alimenter les requêtes dans la base de données. Néanmoins, de nombreux aspects sont à prendre à compte dans la modélisation des données, la logique sous-jacente et, bien entendu, la conception générale et l’expérience utilisateur.

Nous allons voir comment créer des expériences de recherche optimales basées sur React à l’aide de la bibliothèque open source Search UI d’Elastic. Comptez environ 30 minutes pour réaliser cette procédure. Vous serez ensuite prêt à implanter l’option de recherche dans n’importe quelle application qui en a besoin.

En premier lieu, qu’est-ce qui rend la création de recherche si difficile ?

Recherche rime avec complexité

Il y a quelques semaines, un article très intéressant, intitulé Falsehoods Programmers Believe About Search, a circulé sur Internet. Cet article contient une liste exhaustive des fausses suppositions sur lesquelles les développeurs s’appuient dans la mise en place de la recherche.

Voici quelques-unes de ces croyances tenaces :

  • “Les clients qui savent ce qu’ils recherchent le feront de la façon que vous avez prévue.”
  • “Vous pouvez écrire un programme d’analyse de recherches qui analysera systématiquement la recherche de façon correcte.”
  • “Une fois définie, la recherche aura toujours la même façon de fonctionner la semaine d’après.”
  • “Les synonymes sont simples à trouver.”
  • ...Et bien d’autres perles ! Vous devriez y jeter un œil.

Ce qu’il faut retenir, c’est que la recherche présente de nombreuses difficultés. Dont certaines que vous n’avez probablement pas envisagées. Vous devez réfléchir à la façon dont vous allez gérer les états, construire les composants de filtrage, de recherche à facettes, de tri, de pagination, de synonymes, de traitement du langage, et encore beaucoup, beaucoup d’autres sujets. Mais, pour résumer :

La création d’une recherche optimale nécessite deux composants sophistiqués : (1) le moteur de recherche, qui fournit des API pour booster la recherche, et (2) la bibliothèque de recherche, qui constitue la trame de l’expérience de recherche.

Pour le moteur de recherche, nous étudierons Elastic App Search.

Pour l’expérience de recherche, nous vous présenterons une bibliothèque de recherche OS : Search UI.

Quand nous aurons terminé, voici à quoi cela ressemblera :

image2.png

Moteur de recherche : Elastic App Search

App Search est disponible sous forme de service géré payant, ou sous forme de distribution gratuite auto-gérée. Dans le cadre de ce tutoriel, nous nous servirons du service géré. Rappelez-vous néanmoins que votre équipe peut utiliser Search UI et App Search avec une licence de base sans frais si vous en assurez l’hébergement vous-même.

Voici le scénario : nous allons indexer des documents relatifs aux meilleurs jeux vidéo de tous les temps dans un moteur de recherche, puis nous allons concevoir et optimiser une expérience de recherche à partir de là.

Tout d’abord, inscrivez-vous pour bénéficier d’une version d’essai de 14 jours. Pas besoin de carte bancaire !

Créez un moteur. Vous pouvez faire une sélection parmi 13 langues différentes.

Appelons ce moteur video-games et choisissons la langue anglaise.

image4.png

Téléchargez l’ensemble de données sur les meilleurs jeux vidéo, puis chargez-le dans App Search à l’aide du programme d’importation.

Ensuite, cliquez dans Engine (Moteur), puis sélectionnez l’onglet Credentials (Informations d’identification).

Créez une nouvelle clé Public Search Key avec un accès limité uniquement au moteur video-games.

Récupérez la nouvelle clé Public Search Key ainsi que votre identificateur d’hôte.

Même si son apparence reste “brute“, notre moteur de recherche est parfaitement opérationnel et prêt à effectuer des recherches dans nos données de jeux vidéo à l’aide d’une API de recherche optimisée.

Voici ce que nous avons fait jusqu’à maintenant :

  • Créer un moteur de recherche
  • Ingérer des documents
  • Créer un schéma par défaut
  • Récupérer des informations d’identification appropriées disponibles que nous pouvons présenter au navigateur

Du côté d’App Search, nous en avons terminé pour l’instant.

Construisons maintenant notre expérience de recherche avec Search UI.

Bibliothèque de recherche : Search UI

Nous nous servirons de l’utilitaire d’échafaudage create-react-app pour créer une application React :

npm install -g create-react-app
create-react-app video-game-search --use-npm
cd video-game-search

Dans cette base, nous installerons Search UI et le connecteur App Search :

npm install --save @elastic/react-search-ui @elastic/search-ui-app-search-connector

Et nous lancerons l’application en mode de développement :

npm start

Ouvrez src/App.js dans votre éditeur de texte préféré.

Nous allons commencer avec un peu de code standard, que nous allons ensuite décompacter.

Lisez bien les commentaires !

// Étape 1 : Importer des instructions
import React from "react";
import AppSearchAPIConnector from "@elastic/search-ui-app-search-connector";
import { SearchProvider, Results, SearchBox } from "@elastic/react-search-ui";
import { Layout } from "@elastic/react-search-ui-views";
import "@elastic/react-search-ui-views/lib/styles/styles.css";
// Étape 2 : Connecteur
const connector = new AppSearchAPIConnector({
  searchKey: "[YOUR_SEARCH_KEY]",
  engineName: "video-games",
  hostIdentifier: "[YOUR_HOST_IDENTIFIER]"
});
// Étape 3 : Options de configuration
const configurationOptions = {
  apiConnector: connector
  // Complétons ensemble.
};
// Étape 4, SearchProvider : Finitions
export default function App() {
  return (
    <SearchProvider config={configurationOptions}>
      <div className="App">
        <Layout
        // Complétons ensemble.
        />
      </div>
    </SearchProvider>
  );
}

Étape 1 : Importer des instructions

Nous devons importer nos dépendances Search UI et React.

Les composants principaux, le connecteur et les composants d’affichage sont contenus au sein de trois packages différents :

  • @elastic/search-ui-app-search-connector
  • @elastic/react-search-ui
  • @elastic/react-search-ui-views

Nous en apprendrons davantage sur chacun d’eux au fur et à mesure que nous avancerons.

import React from "react";
import AppSearchAPIConnector from "@elastic/search-ui-app-search-connector";
import { SearchProvider, Results, SearchBox } from "@elastic/react-search-ui";
import { Layout } from "@elastic/react-search-ui-views";

Nous allons également importer une feuille de styles pour ce projet afin d’avoir une belle apparence sans avoir à écrire une seule ligne de CSS :

import "@elastic/react-search-ui-views/lib/styles/styles.css";

Étape 2 : Connecteur

Nous disposons de notre clé Public Search Key et de notre identificateur d’hôte venant d’App Search.

Il est temps de les mettre en action !

L’objet Connector dans Search UI utilise les informations d’identification pour accéder à App Search et lancer la recherche :

const connector = new AppSearchAPIConnector({
  searchKey: "[YOUR_SEARCH_KEY]",
  engineName: "video-games",
  hostIdentifier: "[YOUR_HOST_IDENTIFIER]"
});

Search UI fonctionne avec n’importe quelle API de recherche, car les connecteurs font en sorte que ça soit le cas, sans qu’il soit besoin de configurer davantage.

Étape 3 : configurationOptions

Avant que nous entamions la partie sur l’objet configurationOptions, prenons quelques instants pour réfléchir.

Nous avons importé un ensemble de données dans notre moteur de recherche. Mais au fait... de quel type de données s’agit-il ?

Plus nous avons d’informations sur nos données, mieux nous saurons comment les présenter aux utilisateurs qui feront des recherches. Et cela nous permettra de déterminer comment configurer l’expérience de recherche.

Penchons-nous sur un objet, le meilleur objet de cet ensemble de données :

{ 
  "id":"final-fantasy-vii-ps-1997",
  "name":"Final Fantasy VII",
  "year":1997,
  "platform":"PS",
  "genre":"Role-Playing",
  "publisher":"Sony Computer Entertainment",
  "global_sales":9.72,
  "critic_score":92,
  "user_score":9,
  "developer":"SquareSoft",
  "image_url":"https://r.hswstatic.com/w_907/gif/finalfantasyvii-MAIN.jpg"
}

Comme nous pouvons le constater, il dispose de plusieurs champs textuels, comme name, year, platform, etc. ainsi que des champs numériques, comme critic_score, global_sales et user_score.

Pour construire une solide expérience de recherche, il suffit de répondre à trois questions clés :

  • Comment les utilisateurs effectueront-ils principalement leurs recherches ? En effectuant une recherche sur le nom du jeu vidéo.
  • Quelles sont les informations que la majorité des utilisateurs souhaitent obtenir ? Le nom du jeu vidéo, son genre, son éditeur, les notes et la plateforme qu’il utilise.
  • Comment les utilisateurs procèderont généralement pour le filtrage, le tri et la recherche à facettes ? Par note, genre, éditeur et plate-forme.

Maintenant que nous avons ces réponses, nous pouvons les convertir dans configurationOptions :

const configurationOptions = {
  apiConnector: connector,
  searchQuery: {
    search_fields: {
      // 1. Recherche par nom de jeu vidéo.
      name: {}
    },
    // 2. Résultats : nom, genre, éditeur, notes et plate-forme.
    result_fields: {
      name: {
        // Un extrait implique que les termes de recherche correspondants soient entourés de balises <em>.
        snippet: {
          size: 75, // Limitation de l’extrait à 75 caractères.
          fallback: true // Solution de secours : résultat "brut". 
        }
      },
      genre: {
        snippet: {
          size: 50,
          fallback: true
        }
      },
      publisher: {
        snippet: {
          size: 50,
          fallback: true
        }
      },
      critic_score: {
        // Les notes correspondent à des nombres, pas d’extrait dans ce cas.
        raw: {}
      },
      user_score: {
        raw: {}
      },
      platform: {
        snippet: {
          size: 50,
          fallback: true
        }
      },
      image_url: {
        raw: {}
      }
    },
    // 3. Recherche à facettes par notes, genre, éditeur et plate-forme, dont nous nous servirons par la suite pour mettre en place des filtres.
    facets: {
      user_score: {
        type: "range",
        ranges: [
          { from: 0, to: 5, name: "Not good" },
          { from: 5, to: 7, name: "Not bad" },
          { from: 7, to: 9, name: "Pretty good" },
          { from: 9, to: 10, name: "Must play!" }
        ]
      },
      critic_score: {
        type: "range",
        ranges: [
          { from: 0, to: 50, name: "Not good" },
          { from: 50, to: 70, name: "Not bad" },
          { from: 70, to: 90, name: "Pretty good" },
          { from: 90, to: 100, name: "Must play!" }
        ]
      },
      genre: { type: "value", size: 100 },
      publisher: { type: "value", size: 100 },
      platform: { type: "value", size: 100 }
    }
  }
};

Nous avons connecté Search UI à notre moteur de recherche. Nous disposons maintenant d’options qui régissent la façon dont nous effectuerons nos recherches dans les données, dont nous afficherons nos résultats, et dont nous explorerons ces résultats. Néanmoins, il nous faut un lien pour rassembler tous ces aspects avec les composants frontend dynamiques de Search UI.

Étape 4 : SearchProvider

C’est l’objet qui régit tous les éléments. C’est dans SearchProvider que tous les autres composants sont imbriqués.

Search UI fournit un composant Layout, qui sert à mettre en place une couche de recherche type. Il existe des options de personnalisation intéressantes, mais nous ne les aborderons pas dans ce tutoriel.

Nous allons effectuer deux actions :

  1. Transférer configurationOptions vers SearchProvider.
  2. Placer certains piliers de construction dans Layout et ajouter deux composants de base : SearchBox et Results.
export default function App() {
  return (
    <SearchProvider config={configurationOptions}>
      <div className="App">
        <Layout
          header={<SearchBox />}
          // titleField est le champ le plus important dans un résultat : c’est l’en-tête du résultat.
          bodyContent={<Results titleField="name" urlField="image_url" />}
        />
      </div>
    </SearchProvider>
  );
}

À ce stade, nous avons défini les paramètres de base du frontend. Mais avant de lancer une recherche, nous avons encore quelques détails à régler. Nous devons aussi travailler le modèle de pertinence pour affiner la recherche, afin qu'elle réponde aux besoins uniques de ce projet.

Revenons à App Search...

De retour au labo

App Search dispose de fonctionnalités de moteur de recherche puissantes et optimisées. Pour affiner notre projet, c’est un jeu d’enfant ! Nous pouvons procéder à des ajustements de pertinence précis et des changements de schéma transparents en seulement quelques clics. 

Nous allons commencer par ajuster le schéma pour le voir en action.

Connectez-vous à App Search, accédez au moteur video-games, puis cliquez sur Schema (Schéma) sous la section Manage (Gérer).

Le schéma s’affiche. Chacun des 11 champs est considéré comme du texte par défaut.

Dans l’objet configurationOptions, nous avons défini deux facettes "range" pour faciliter la recherche des nombres : user_score et critic_score. Pour qu'une facette "range" fonctionne comme prévu, le type de champ doit être numérique.

Cliquez sur le menu déroulant en regard de chaque champ, sélectionnez number (nombre), puis cliquez sur Update Types (Mettre à jour les types) :

image1.png

Le moteur effectue une réindexation à la volée. Par la suite, lorsque nous ajouterons les composants de recherche à facettes à notre couche, les filtres de plage fonctionneront comme prévu. Revenons à nos moutons.

Attention : section essentielle

Il existe trois principales fonctionnalités relatives à la pertinence : les synonymes, les curations et le réglage de la pertinence.

Sélectionnez chaque fonctionnalité dans la section Search Settings (Paramètres de recherche) de la barre latérale :

image8.png

Synonymes

Certaines personnes conduisent une voiture, d’autres une automobile, et d’autres encore un tacot. Internet a une envergure mondiale, et de ce fait, on utilise des termes différents autour du globe pour décrire les choses. Les synonymes vous aident à créer des ensembles de termes considérés comme représentant un seul et même mot.

Pour en revenir à notre moteur de recherche de jeux vidéo, une chose est sûre : certains utilisateurs voudront en savoir plus sur Final Fantasy. Mais peut-être qu’ils écriront FF à la place.

Cliquez dans Synonyms (Synonymes), puis sélectionnez Create a Synonym Set (Créer un ensemble de synonymes) et entrez les termes :

image6.png

Cliquez sur Save (Enregistrer). Vous pouvez ajouter autant d’ensembles de synonymes que vous le souhaitez.

À présent, les recherches effectuées sur FF auront le même poids que les recherches effectuées sur Final Fantasy.

Curations

Les curations sont un favori. Que se passe-t-il si quelqu’un fait une recherche sur Final Fantasy ou FF ? Il y a un nombre assez important de jeux dans cette série. Sur quelle version tomberont-ils ?

Par défaut, les cinq premiers résultats sont les suivants :

1. Final Fantasy VIII

2. Final Fantasy X

3. Final Fantasy Tactics

4. Final Fantasy IX

5. Final Fantasy XIII

Ça ne paraît pas très pertinent. Final Fantasy VII a été le meilleur de toute la série. Alors que Final Fantasy XIII n’était pas si bien que ça ! 😜

Pouvons-nous faire en sorte que le premier résultat renvoyé pour une personne effectuant une recherche sur Final Fantasy soit Final Fantasy VII ? Et pouvons-nous supprimer Final Fantasy XIII de nos résultats de recherche ?

La réponse tient en trois lettres : oui !

Cliquez dans Curations et entrez une recherche : Final Fantasy.

Ensuite, faites glisser le document Final Fantasy VII dans la section Promoted Documents (Documents promus) en utilisant la poignée tout à gauche du tableau. Pour le document Final Fantasy XIII, cliquez sur le bouton Hide Result (Masquer le résultat) (l’œil traversé par une ligne) :

image7.png

Désormais, toute personne faisant une recherche sur Final Fantasy ou FF verra Final Fantasy VII s’afficher en premier.

Et elle ne verra en aucun cas Final Fantasy XIII. Et toc !

Nous pouvons promouvoir et masquer de nombreux documents. Nous pouvons même trier les documents promus de sorte à maintenir un contrôle total sur les premiers résultats renvoyés pour chaque recherche.

Réglage de la pertinence

Cliquez sur Relevance Tuning (Réglage de la pertinence) dans la barre latérale.

Nous effectuons une recherche dans un champ textuel, le champ name. Mais que se passe-t-il si les champs textuels sur lesquels les utilisateurs effectuent une recherche sont nombreux, comme les champs name et description ? L’ensemble de données de jeux vidéo que nous utilisons ne contient aucun champ de description. Nous allons donc simuler quelques documents pour avoir matière à réflexion.

Imaginons que nos documents se présentent comme suit :

{ 
  "name":"Magical Quest",
  "description": "A dangerous journey through caves and such." 
},
{ 
  "name":"Dangerous Quest",
  "description": "A magical journey filled with magical magic. Highly magic." 
}

Si quelqu’un veut trouver le jeu Magical Quest, il l’entrera en tant qu’élément de recherche. Mais le premier résultat qu’il obtiendrait serait Dangerous Quest :

image3.png

Pourquoi ? Parce que le terme “magical” apparaît trois fois dans la description de Dangerous Quest. Or, le moteur de recherche ne sait pas qu’un champ est plus important que l’autre. C’est pourquoi il renverra Dangerous Quest en premier lieu. C’est là qu’intervient la fonctionnalité de réglage de pertinence.

Nous pouvons sélectionner un champ et, entre autres, augmenter le poids de sa pertinence :

image5.gif

Nous constatons que, lorsque nous augmentons le poids du champ name, c’est l’élément approprié, à savoir Magical Quest, qui ressort en premier. Pour cela, il suffit d’augmenter le curseur sur une valeur plus élevée, puis cliquer sur Save (Enregistrer).

Nous utilisons maintenant App Search pour :

  • Ajuster le schéma et changer user_score et critic_score sur des champs number (nombre).
  • Régler le modèle de pertinence.

Et voilà, nous en avons fini avec les fonctionnalités élaborées de “tableau de bord”. Chacune est associée à un point de terminaison d’API que vous pouvez utiliser pour tout faire fonctionner de façon programmée si vous n’êtes pas très à l’aise avec les interfaces utilisateur graphiques.

Enfin, finalisons l’interface utilisateur.

Finitions

Votre interface utilisateur doit maintenant être fonctionnelle. Essayez quelques requêtes et regardez ce qui se passe. On se rend rapidement compte qu'il nous manque des outils pour explorer nos résultats, comme des fonctionnalités de filtrage, d'attributs, ou encore de tri. Mais la recherche fonctionne bien. Nous devons étoffer l’interface utilisateur.

Dans le fichier src/App.js initial, nous avons importé trois composants de base :

import { SearchProvider, Results, SearchBox } from "@elastic/react-search-ui";

Ajoutons-en quelques-uns, maintenant que nous avons défini nos options de configuration.

L’importation des composants suivants activeront les capacités manquantes au sein de l’interface utilisateur :

  • PagingInfo: Afficher des informations sur la page actuelle.
  • ResultsPerPage: Configurer le nombre de résultats par page.
  • Paging: Naviguer entre les différentes pages.
  • Facet: Filtrer et explorer les données de façons uniques selon le type de données.
  • Sorting: Réorienter les résultats pour un champ donné.
import {
  PagingInfo,
  ResultsPerPage,
  Paging,
  Facet,
  SearchProvider,
  Results,
  SearchBox,
  Sorting
} from "@elastic/react-search-ui";

Une fois importés, les composants peuvent être placés dans la couche.

Le composant Layout divise la page en sections, et les composants peuvent être placés dans ces sections par l’intermédiaire de propositions.

Voici les sections qu’il contient :

  • header: Zone/barre de recherche
  • bodyContent: Conteneur de résultats
  • sideContent: Barre latérale, qui contient des options de recherche à facettes et de tri
  • bodyHeader: “Enveloppe” autour des résultats avec des informations contextuelles comme la page actuelle et le nombre de résultats par page
  • bodyFooter: Options de pagination pour une navigation rapide entre les pages

Les composants effectuent le rendu des données. Les données sont renvoyées en fonction des paramètres de recherche définis dans configurationOptions. Maintenant, nous allons placer chaque composant dans la section Layout (Couche) appropriée.

Par exemple, nous avons décrit cinq dimensions des facettes dans configurationOptions. Nous allons donc créer cinq composants Facet. Chaque composant Facet utilisera une proposition de "champ" comme une clé vers nos données.

Nous les mettrons dans la section sideContent, avec notre composant Sorting, puis nous placerons les composants Paging, PagingInfo et ResultsPerPage dans les sections appropriées qui leur correspondent :

<Layout
  header={<SearchBox />}
  bodyContent={<Results titleField="name" urlField="image_url" />}
  sideContent={
    <div>
      <Sorting
        label={"Sort by"}
        sortOptions={[
          {
            name: "Relevance",
            value: "",
            direction: ""
          },
          {
            name: "Name",
            value: "name",
            direction: "asc"
          }
        ]}
      />
      <Facet field="user_score" label="User Score" />
      <Facet field="critic_score" label="Critic Score" />
      <Facet field="genre" label="Genre" />
      <Facet field="publisher" label="Publisher" isFilterable={true} />
      <Facet field="platform" label="Platform" />
    </div>
  }
  bodyHeader={
    <>
      <PagingInfo />
      <ResultsPerPage />
    </>
  }
  bodyFooter={<Paging />}
/>

Jetons maintenant un œil à l’expérience de recherche dans l’environnement de développement local.

C’est bien mieux ! Nous disposons de tout un panel d’options pour explorer les résultats de recherche.

Nous avons apporté quelques autres améliorations, comme les options de tri multiple, et nous avons mis en place un filtrage pour la recherche à facettes de l’éditeur en ajoutant un seul indicateur. Effectuez une recherche à l’aide d’une requête vide pour explorer toutes ces options.

Et pour terminer, il nous reste une dernière fonctionnalité de l’expérience de recherche à étudier, et pas des moindres !

Il s’agit de la saisie semi-automatique.

La saisie semi-automatique

Les utilisateurs qui effectuent des recherches adorent la saisie semi-automatique car elle donne un feedback instantané. Ses suggestions se classent en deux catégories : les résultats et les recherches. Un utilisateur qui effectue une recherche obtiendra des résultats pertinents ou des requêtes potentielles conduisant aux résultats.

Dans notre cas, nous nous pencherons sur la saisie semi-automatique qui génère des suggestions de requête.

Pour cela, deux changements sont nécessaires.

Tout d’abord, nous devons ajouter la saisie semi-automatique à l’objet configurationOptions :

const configurationOptions = {
  autocompleteQuery: {
    suggestions: {
      types: {
        documents: {
          // Champs à partir desquels émettre des suggestions
          fields: ["name"]
        }
      },
      // Nombre de suggestions qui s’affichent
      size: 5
    }
  },
  ...
};

Ensuite, nous devons activer la saisie semi-automatique en tant que fonction de SearchBox :

...
        <Layout
          ...
          header={<SearchBox autocompleteSuggestions={true} />}
/>
...

Et voilà !

Commencez à entrer une requête : grâce à la saisie semi-automatique, des suggestions de recherche apparaissent au fur et à mesure que vous tapez.

Résumé

Nous avons désormais mis en place une expérience de recherche fonctionnelle et conviviale. Et nous avons évité un bon nombre de pièges dans lesquels les utilisateurs ont l’habitude de tomber lorsqu’ils essayent de mettre en œuvre la recherche. Et tout ça en seulement 30 minutes ! Pas mal, non ?

Search UI est un framework React flexible et moderne qui favorise une mise en place rapide d’expériences de recherche. Elastic App Search est un moteur de recherche robuste construit sur Elasticsearch. Il s’agit d’un service payant géré. Mais vous pouvez très bien le faire fonctionner par vous-même gratuitement avec une licence de base.

Quoi qu’il en soit, nous sommes très curieux de voir ce que vous allez construire avec Search UI. Alors, faites un petit arrêt sur Gitter. Et apportez votre pierre à l’édifice si vous le souhaitez !

  • Nous recrutons !

    Rejoignez une équipe internationale distribuée dont tous les membres sont facilement joignables par visioconférence. Vous cherchez un poste aux conditions flexibles et à l'impact réel ? Vous souhaitez bénéficier d'opportunités de développement en permanence ?