Les bases de la recherche

Maintenant que vous avez construit un index Elasticsearch et que vous y avez chargé quelques documents, vous êtes prêt à mettre en œuvre la recherche plein texte.

Fonctionnement de la recherche

Passons rapidement en revue le fonctionnement de la solution de recherche dans l'application du tutoriel. Une fois l'application Flask lancée, vous pouvez vous rendre sur http://localhost:5001 pour accéder à la page principale, qui ressemble à ceci :

Le code qui rend cette page est implémenté dans le fichier app.py :

Il s'agit d'un point de terminaison très simple qui rend un modèle HTML. Dans les applications Flask, les modèles sont situés dans un sous-répertoire templates, vous y trouverez donc ce modèle et les autres modèles inclus dans l'application.

Examinons la mise en œuvre du champ de recherche dans le fichier templates/index.html. Voici la partie pertinente de ce modèle :

Vous pouvez voir ici qu'il s'agit d'un formulaire HTML avec un seul champ de type text nommé query. L'attribut method du formulaire est défini à POST, ce qui indique au navigateur de soumettre ce formulaire dans une requête POST. L'attribut action est défini sur l'URL qui correspond au point de terminaison handle_search de l'application Flask. Lorsque le formulaire est soumis, la fonction handle_search() est exécutée.

La mise en œuvre actuelle de handle_search() est illustrée ci-dessous :

La fonction obtient le texte tapé par l'utilisateur dans le champ de texte à partir du dictionnaire request.form de Flask et le stocke dans la variable locale query. La fonction restitue ensuite le modèle index.html, mais ce type de fonction transmet des arguments supplémentaires afin que la page puisse afficher les résultats de la recherche. Les quatre arguments que le modèle reçoit sont les suivants

  • query: le texte de la requête saisi par l'utilisateur dans le formulaire.
  • resultsune liste des résultats de la recherche
  • from_: l'indice de base zéro du premier résultat
  • totalle nombre total de résultats

La fonctionnalité de recherche n'étant pas mise en œuvre, les arguments transmis à la fonction render_template() indiquent pour l'instant qu'aucun résultat n'a été trouvé.

Il s'agit maintenant d'implémenter une requête plein texte et de transmettre les résultats réels afin que la page index.html puisse les afficher.

Les services Elasticsearch utilisent un DSL (Domain Specific Language) basé sur le format JSON pour définir les requêtes.

Le client Elasticsearch pour Python possède une méthode search() qui est utilisée pour soumettre une requête de recherche. Ajoutons une méthode d'aide search() dans search.py qui utilise cette méthode :

Cette méthode invoque la méthode search() du client Elasticsearch avec le nom de l'index. L'argument query_args capture tous les arguments de mots-clés fournis à la méthode, puis les transmet à la méthode es.search(). Ces arguments vont permettre à l'appelant de spécifier ce qu'il doit rechercher.

Requêtes de correspondance

Le DSL Elasticsearch Query offre de nombreuses façons d'interroger un index. En parcourant les sous-sections de la documentation, vous vous familiariserez avec les différents types de requêtes possibles. La tâche très courante de recherche de texte est couverte dans la section Requêtes en texte intégral.

Pour la première mise en œuvre de la recherche, utilisons la requête Match. Vous trouverez ci-dessous un exemple d'utilisation de cette requête :

L'exemple ci-dessus est donné dans un format qui ressemble à une requête HTTP brute. Il est utile de se familiariser avec ce format, car il est largement utilisé dans la documentation d'Elasticsearch et dans la console API d'Elasticsearch. Heureusement, ce format est très facile à traduire en appel à l'aide de la bibliothèque client Python. Vous trouverez ci-dessous le code Python équivalent à l'exemple ci-dessus :

Lors de la conversion des exemples de la console API en Python, n'oubliez pas que les clés de premier niveau dans le corps de la requête doivent être converties en arguments de mot-clé dans l'appel Python. Les exemples ne précisent pas non plus d'index, ce qui serait nécessaire lors de l'appel à Python.

En examinant la structure de la requête, vous pouvez probablement déduire le type de recherche demandé. L'appel demande une requête match sur un champ appelé name, et le texte à rechercher est search text here.

Ce type d'interrogation est relativement facile à intégrer dans les applications didactiques. Ouvrez app.py et trouvez la méthode handle_search(). Remplacer la version actuelle par la nouvelle :

L'appel à es.search() dans la deuxième ligne de cette nouvelle version du point de terminaison invoque la méthode search() ajoutée ci-dessus dans search.py, qui appelle à son tour la méthode search() du client Elasticsearch.

Pouvez-vous comprendre ce que la requête va faire ? Il s'agit d'une requête match similaire à l'exemple précédent. Le champ sur lequel va porter la recherche est name, qui contient les titres des documents de l'index my_documents que vous avez créé dans la section précédente. Le texte à rechercher est celui que l'utilisateur a saisi dans le champ de recherche de la page web et qui est stocké dans la variable locale query.

La partie de la réponse à la recherche qui contient les résultats est response['hits']. Il s'agit d'un objet comportant quelques clés, dont deux présentent un intérêt dans le cadre de cette mise en œuvre :

  • response['hits']['hits']: la liste des résultats de la recherche.
  • response['hits']['total']: le nombre total de résultats disponibles. Le nombre de résultats est indiqué dans une sous-clé value. En pratique, l'expression permettant d'obtenir le nombre total de résultats est results['hits']['total']['value']. Notez que le nombre total de résultats peut être une approximation lorsqu'il y a un grand nombre de résultats. Voir la documentation sur le corps de la réponse pour plus de détails.

L'appel à render_template() dans cette nouvelle version du point d'accès transmet la liste des résultats dans l'argument du modèle results et le nombre total de résultats dans total. L'argument query reçoit la chaîne de requête comme auparavant, et from_ est toujours codé en dur à 0, car il sera implémenté plus tard lorsque la pagination sera ajoutée.

De plus, l'application dispose d'une première implémentation de la recherche en texte intégral. Retournez à votre navigateur web et naviguez vers http://localhost:5001 pour ouvrir l'application. Si, pour une raison quelconque, l'application Flask n'est pas en cours d'exécution, redémarrez-la avant de procéder à cette opération. Saisissez un texte de recherche tel que policy ou work from home et vous obtiendrez des résultats pertinents. Ci-dessous vous pouvez voir les résultats pour la recherche de work from home:

Le modèle index.html que vous avez téléchargé avec l'application de démarrage comprend toute la logique nécessaire au rendu des résultats de la recherche. Si vous êtes curieux, voici la section de ce modèle qui affiche la liste des résultats :

Dans ce code, il est intéressant de noter que les données associées à un résultat retourné sont disponibles sous la clé _source. Il existe également un champ _id qui contient l'identifiant unique attribué au résultat.

Un score associé à chaque résultat peut être obtenu à partir de _score. Le score fournit une mesure de la pertinence, les scores les plus élevés indiquant une correspondance plus étroite avec le texte de la requête. Par défaut, les résultats sont renvoyés dans l'ordre de leur score, du plus élevé au plus bas. Les scores dans Elasticsearch sont calculés à l'aide de l'algorithme Okapi BM25.

Si vous souhaitez approfondir les sujets abordés dans cette section, vous pouvez utiliser les liens suivants :

Récupération des résultats individuels

Vous avez peut-être remarqué que le modèle index.html présente le titre de chaque résultat de recherche sous la forme d'un lien. Le lien pointe vers le troisième et dernier point d'arrivée qui a été mis en œuvre dans l'application Flask de départ, appelé get_document. L'implémentation fournie renvoie un texte codé en dur "Document not found". C'est donc ce que vous verrez si vous cliquez sur l'un des résultats lorsque vous jouez avec l'application.

Pour rendre correctement les documents individuels, ajoutons une méthode d'aide retrieve_document() dans search.py, en utilisant la méthode get() du client Elasticsearch :

Vous pouvez voir ici l'utilité des identifiants uniques attribués à chaque document, car c'est ce que l'application peut utiliser pour se référer à des documents individuels.

Voici l'implémentation actuelle du point de terminaison get_document():

Vous pouvez voir que l'URL associée à ce point de terminaison inclut le document id, et les liens qui sont rendus pour chaque résultat de recherche ont également l'id incorporé dans les URL respectives, donc tout ce qui manque est de remplacer cette implémentation simpliste par une implémentation qui récupère le document et le rend. Remplacer le point final par cette version mise à jour :

Ici, la méthode retrieve_document() de search.py est utilisée pour obtenir le document demandé. Le document.html est ensuite rendu, avec un titre provenant du champ name et une liste de paragraphes provenant de content.

Essayez de lancer d'autres requêtes et de cliquer sur les résultats, ce qui devrait vous permettre d'afficher le contenu complet.

Recherche dans plusieurs champs

Après avoir joué avec l'application pendant un certain temps, vous avez peut-être remarqué qu'un grand nombre de requêtes ne donnent aucun résultat. Comme vous vous en souvenez, la recherche est actuellement effectuée sur le champ name de chaque document, qui est l'endroit où les titres des documents sont stockés. Les documents comportent également les champs summary et content, qui contiennent des textes plus longs susceptibles d'être recherchés également, mais pour l'instant, ces champs sont ignorés.

Dans cette section, vous allez découvrir une autre requête courante de recherche en texte intégral, la recherche multiple, qui demande qu'une recherche soit effectuée sur plusieurs champs d'un index.

Voici un exemple de requête multi-match tiré de la documentation :

Utilisons cet exemple comme base pour étendre le point de terminaison handle_search() afin d'exécuter des requêtes multiples sur les champs name, summary et content combinés. Voici le code mis à jour du point final :

Avec ce changement, il y a beaucoup plus de texte à rechercher, à tel point que certaines requêtes peuvent avoir plus que le maximum de 10 résultats qui sont renvoyés par défaut. Dans le prochain chapitre, vous apprendrez à traiter les longues listes de résultats grâce à la pagination.

Précédemment

Créer un index

Suivant

Pagination

Prêt à créer des expériences de recherche d'exception ?

Une recherche suffisamment avancée ne se fait pas avec les efforts d'une seule personne. Elasticsearch est alimenté par des data scientists, des ML ops, des ingénieurs et bien d'autres qui sont tout aussi passionnés par la recherche que vous. Mettons-nous en relation et travaillons ensemble pour construire l'expérience de recherche magique qui vous permettra d'obtenir les résultats que vous souhaitez.

Jugez-en par vous-même