<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
  <channel>
    <title><![CDATA[Gustavo Llermaly - Elasticsearch Labs]]></title>
    <description><![CDATA[Articles and tutorials from the Search team at Elastic]]></description>
    <copyright><![CDATA[© 2026. Elasticsearch B.V. All Rights Reserved]]></copyright>
    <image>
      <title><![CDATA[Gustavo Llermaly - Elasticsearch Labs]]></title>
      <url>https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1121c0bf0e8a6e65/6a88da6340a1841030ef456f/search-labs-thumbnail.png</url>
      <link>https://www.elastic.co/fr/search-labs/author/gustavo-llermaly</link>
    </image>
    <link>https://www.elastic.co/fr/search-labs/author/gustavo-llermaly</link>
    <atom:link href="https://www.elastic.co/fr/search-labs/rss/author/gustavo-llermaly.xml" rel="self" type="application/rss+xml"/>
    <language><![CDATA[fr]]></language>
    <lastBuildDate>Tue, 29 Sep 2026 10:24:17 GMT</lastBuildDate>
  <item>
    <title><![CDATA[Monitorer des vues des tableaux de bord Kibana avec Elastic Workflows]]></title>
    <description><![CDATA[Découvrez comment utiliser Elastic Workflows pour collecter les indicateurs des vues du tableau de bord Kibana toutes les 30 minutes et les indexer dans Elasticsearch, afin de pouvoir créer des analyses et des visualisations personnalisées à partir de vos propres données.]]></description>
    <content:encoded><![CDATA[<p><a href="https://www.elastic.co/kibana">Kibana</a> enregistre le nombre de consultations de chaque tableau de bord, mais ces données ne sont pas accessibles nativement dans les tableaux de bord intégrés. Dans cet article, nous utiliserons <strong>Elastic Workflows</strong> pour collecter automatiquement ces données toutes les 30 minutes et les indexer dans Elasticsearch, afin de pouvoir créer nos propres analyses.</p><p><a href="https://www.elastic.co/docs/explore-analyze/workflows">Elastic Workflows</a> est un moteur d'automatisation intégré à Kibana qui vous permet de définir des processus à plusieurs étapes à l'aide d'une configuration YAML simple. Chaque workflow peut être déclenché selon une planification ou un événement, ou en tant qu'outil dans <a href="https://www.elastic.co/docs/explore-analyze/ai-features/elastic-agent-builder">Elastic Agent Builder</a>, et chaque étape peut appeler les API de Kibana, interroger Elasticsearch ou transformer des données.</p><p>Nous utiliserons les compteurs de vues du tableau de bord comme exemple concret, mais le même modèle s'applique à n'importe quel indicateur exposé via l'API des objets enregistrés de Kibana.</p><h2>Produits requis</h2><ul><li><p><a href="https://www.elastic.co/cloud">Elastic Cloud</a> ou cluster <a href="https://www.elastic.co/docs/deploy-manage/deploy/self-managed">autogéré </a>exécutant la version 9.3</p></li><li><p><a href="https://www.elastic.co/docs/explore-analyze/workflows/get-started#workflows-prerequisites">Workflows activés</a> (paramètres avancés)</p></li></ul><h2>Étape 1 : Explorer les données brutes dans les <a href="https://www.elastic.co/docs/explore-analyze/query-filter/tools/console">Outils de développement</a></h2><p>Avant toute chose, examinons les données disponibles. Kibana stocke la majeure partie de sa configuration et de ses métadonnées sous forme d'<a href="https://www.elastic.co/docs/api/doc/kibana/group/endpoint-saved-objects">objets enregistrés</a> dans un index interne dédié. Parmi les éléments suivis par Kibana figurent les consultations des tableaux de bord, grâce à un type d'objet enregistré spécifique appelé "compteurs d'utilisation". Vous pouvez les interroger directement depuis les outils de développement :</p>GET kbn:/api/saved_objects/_find?type=usage-counter&amp;filter=usage-counter.attributes.domainId:"dashboard"%20and%20usage-counter.attributes.counterType:"viewed"&amp;per_page=10000<p>La réponse se présente comme suit :</p>{
  "page": 1,
  "per_page": 10000,
  "total": 1,
  "saved_objects": [
    {
      "type": "usage-counter",
      "id": "dashboard:346f3c64-ebca-484d-9d57-ec600067d596:viewed:server:20260310",
      "attributes": {
        "domainId": "dashboard",
        "counterName": "346f3c64-ebca-484d-9d57-ec600067d596",
        "counterType": "viewed",
        "source": "server",
        "count": 1
      },
      ...
    }
  ]<p>Le champ <code>counterName</code> est l'identifiant du tableau de bord, et <code>count</code> est le nombre cumulé de vues pour ce tableau de bord ce jour-là. Kibana crée un objet compteur par tableau de bord par jour ; vous pouvez voir le suffixe de date dans l'ID de l'objet (...viewed:server:20260310). Le nombre augmente tout au long de la journée lorsque les utilisateurs ouvrent le tableau de bord.</p><p>Plutôt que de reproduire ce modèle de document quotidien dans notre index, nous créerons un document par exécution de workflow. Chaque document enregistre le nombre de vues que ce tableau de bord avait accumulées pour la journée au moment de la capture.</p><h2>Étape 2 : Créer l'index de destination</h2><p>Nous avons besoin d'un index pour stocker les snapshots de nos vues de tableau de bord. La commande suivante le crée avec des mappings explicites afin de pouvoir les agréger et les visualiser ultérieurement. Exécutez cette commande dans les outils de développement :</p>PUT dashboard-views
{
  "mappings": {
    "properties": {
      "captured_at": {
        "type": "date"
      },
      "dashboard_id": {
        "type": "keyword"
      },
      "dashboard_name": {
        "type": "keyword"
      },
      "view_count": {
        "type": "integer"
      }
    }
  }
}<p>L'utilisation des mappings <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/keyword"><code>keyword</code></a> pour les ID et les noms permet des <a href="https://www.elastic.co/docs/explore-analyze/query-filter/aggregations">agrégations</a>. L'utilisation de <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/number"><code>integer</code></a> pour <code>view_count</code> est une valeur par défaut sûre, car Kibana réinitialise le compteur quotidiennement ; atteindre la limite de 32 bits (plus de 2 milliards de vues en une seule journée) n'est donc pas un problème réaliste. Il prend toujours en compte les opérations numériques, comme <a href="https://www.elastic.co/docs/reference/aggregations/search-aggregations-metrics-max-aggregation"><code>max</code></a>, <a href="https://www.elastic.co/docs/reference/aggregations/search-aggregations-metrics-avg-aggregation"><code>avg</code></a> et <a href="https://www.elastic.co/docs/reference/aggregations/search-aggregations-metrics-min-aggregation"><code>min</code></a> entre autres.</p><h2>Étape 3 : Créer le workflow</h2><p>Accédez à <strong>Stack Management &gt; Workflows &gt; Nouveau workflow</strong>, puis collez la configuration YAML de workflow suivante :</p>name: dashboard-views-ingestion
triggers:
  - type: scheduled
    with:
      every: 30m

steps:
  - name: fetch_dashboard_views
    type: kibana.request
    with:
      method: GET
      path: &gt;-
        /api/saved_objects/_find?type=usage-counter&amp;per_page=10000&amp;filter=usage-counter.attributes.domainId:"dashboard"%20and%20usage-counter.attributes.counterType:"viewed"

  - name: index_each_dashboard
    type: foreach
    foreach: "{{ steps.fetch_dashboard_views.output.saved_objects }}"
    steps:
      - name: fetch_dashboard_name
        type: kibana.request
        with:
          method: GET
          path: /api/saved_objects/dashboard/{{ foreach.item.attributes.counterName }}
        on-failure:
          continue: true

      - name: index_doc
        type: elasticsearch.request
        with:
          method: POST
          path: /dashboard-views/_doc
          body:
            dashboard_id: "{{ foreach.item.attributes.counterName }}"
            dashboard_name: "{{ steps.fetch_dashboard_name.output.attributes.title }}"
            view_count: "${{ foreach.item.attributes.count | plus: 0 }}"
            captured_at: "{{ execution.startedAt | date: '%Y-%m-%dT%H:%M:%SZ' }}"<p>Dans la section suivante, nous allons détailler le workflow étape par étape.</p><h3>Fonctionnement du workflow</h3><h4>Déclencheurs</h4><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt7672aa533b4bc9ed/6a17dc5b420229d07c29f4d4/5670991d65c64ee833924225c2d375a1be868b13-325x162.png" alt=" Déclencheurs planifiés" /><p>Le workflow s'exécute sur un déclencheur planifié toutes les 30 minutes. Cela nous permet d'obtenir des données temporelles sans solliciter l'API.</p><h4>récupérer les vues du tableau de bord</h4><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltab2a16f1f11304ea/6a17dc5d25daab26f608a117/66eaec147c3d01c524c67cf1c7f663ac56a3259d-812x215.png" alt=" Récupérer le tableau de bord" /><p>Utilise <code>kibana.request</code> pour appeler l'API des objets enregistrés de Kibana. Aucune configuration d'authentification n'est nécessaire : le moteur de workflow associe automatiquement les en-têtes corrects en fonction du contexte de l'exécution.</p><h4>index_each_dashboard (foreach)</h4><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blte6b2611b0216555e/6a17dc5f445de95b584cffe5/aad45e8aed8dc81ded6260cd6199ff78dcffe3b4-1892x290.png" alt="Indexer chaque tableau de bord" /><p>Itère sur le tableau <a href="https://www.elastic.co/docs/api/doc/kibana/group/endpoint-saved-objects"><code>saved_objects</code></a> renvoyé par l'étape précédente. L'élément actuel de chaque itération est disponible sous la forme <code>foreach.item</code>. À l'intérieur de la boucle, nous exécutons deux étapes imbriquées pour chaque tableau de bord.</p><p><strong>1. </strong><strong><code>fetch_dashboard_name</code></strong><strong> :</strong></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltb3733b3c24629abf/6a17dc60a292993fe3d02b75/db21ec5094b743018b9cd66c5052681f14c7d7e3-1999x431.png" alt=": Récupérer le nom du tableau de bord" /><p>Résout le titre du tableau de bord lisible par l'utilisateur en appelant <code>GET /api/saved_objects/dashboard/{id}</code>. Nous ajoutons <code>on-failure: continue: true</code> pour que, si un tableau de bord a été supprimé mais qu'il contient encore des compteurs de vues, la boucle continue au lieu d'interrompre l'exécution.</p><p><strong>2. </strong><strong><code>index_doc</code></strong><strong> :</strong></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt385c1f29d717c280/6a17dc62faa91353cb93c759/f49dd0c9f0817bb1e1e5d9f4a2b05d13ef331054-1999x626.png" alt=" Demande Elasticsearch" /><p>Indexe chaque document à l'aide de <code>POST /dashboard-views/_doc</code> (sans identifiant explicite), ce qui permet à Elasticsearch de générer automatiquement des identifiants. Un nouveau document est ainsi créé à chaque exécution, ce qui permet d'établir un historique du nombre de vues au fil du temps plutôt que d'écraser le snapshot précédent.</p><p>Deux choses à noter :</p><ul><li><p>Le champ <code>captured_at</code> utilise le filtre de date pour formater l'horodatage au format <a href="https://www.iso.org/iso-8601-date-and-time-format.html">ISO 8601</a>. Sans ce filtre, la valeur apparaît sous la forme d'une chaîne de date JavaScript, comme <code>Tue Mar 10 2026 05:03:47 GMT+0000</code>, qu'Elasticsearch ne pourra pas interpréter comme une date.</p></li><li><p>Le <code>view_count</code> utilise la syntaxe <code>${{ }}</code> avec <code>| plus: 0</code> pour préserver le type numérique. L'utilisation de <code>{{ }}</code> le rendrait sous forme de chaîne, ce qui empêcherait les opérations mathématiques dans le tableau de bord.</p></li></ul><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt3b94bb6c22c0253e/6a17dc6425daab37a508a11b/6d48c8784d5df6192e8b5175e69dbab5098194bc-919x774.png" alt="" /><p><em>L'interface utilisateur vous permet de déboguer efficacement chacune des étapes du workflow.</em></p><h2>Étape 4 : Créer le tableau de bord des statistiques</h2><p>Une fois que le workflow a été exécuté plusieurs fois et que les données ont été collectées, créez un nouveau tableau de bord dans Kibana en utilisant la data view dashboard-views.</p><p>Voici quelques panneaux pour commencer :</p><ul><li><p><strong>Principaux tableaux de bord par vues</strong> : utilisez un <a href="https://www.elastic.co/docs/explore-analyze/visualize/charts/bar-charts"><strong>graphique à barres</strong></a> avec <code>dashboard_name</code> sur l'axe des X et <code>last_value(view_count)</code> sur l'axe des Y. Cela indique le nombre actuel de vues quotidiennes par tableau de bord.</p></li><li><p><strong>Vues au fil du temps</strong> : utilisez un <a href="https://www.elastic.co/docs/explore-analyze/visualize/charts/line-charts"><strong>graphique linéaire</strong></a> avec <code>captured_at</code> sur l'axe X et <code>last_value(view_count)</code> sur l'axe Y, décomposé par <code>dashboard_name</code>. Comme chaque exécution ajoute un nouveau document, utilisez la dernière valeur pour obtenir le nombre maximal par intervalle temporel au lieu d'additionner les doublons.</p></li><li><p><strong>Snapshot actuel</strong> : utilisez un <a href="https://www.elastic.co/docs/explore-analyze/visualize/charts/tables"><strong>tableau de données</strong></a> avec les dernières valeurs <code>captured_at</code> pour afficher les nombres de vues les plus récents sur tous les tableaux de bord.</p></li></ul><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt18d0390e0526b215/6a17dc65a292991da7d02b79/e245b95f67daf76a2aaf4cb9df2c75ef4cfef582-1462x747.png" alt="" /><p>Étant donné que chaque workflow crée un nouveau document, vous pouvez filtrer par plage horaire pour analyser l'activité sur des périodes spécifiques, comparer d'une semaine à l'autre ou créer des alertes lorsqu'un tableau de bord passe en dessous d'un seuil d'affichage.</p><h2><strong>Conclusion</strong></h2><p>Elastic Workflows est parfaitement adapté à ce type de collecte de données périodique, car la source (API Kibana) et la destination (Elasticsearch) sont natives, ce qui élimine toute gestion d'identifiants. Le moteur de workflow gérant automatiquement l'authentification pour les étapes <code>kibana.request</code> et <code>elasticsearch.request</code>, vous n'avez qu'à écrire la logique.</p><h2><strong>Ressources</strong></h2><ul><li><p><a href="https://www.elastic.co/docs/explore-analyze/workflows">Elastic Workflows</a></p></li><li><p><a href="https://www.elastic.co/docs/api/doc/kibana/">API Kibana</a></p></li></ul>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/monitor-kibana-dashboard-views-elastic-workflows</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/monitor-kibana-dashboard-views-elastic-workflows</guid>
    <category><![CDATA[Expérience développeur]]></category>
    <dc:creator><![CDATA[Gustavo Llermaly]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltef604bbb6dee6be0/6a17dc67a29299db23d02b7d/0ed94ce00962287b5507f45c92ecb60fdcbf2718-1280x720.png" length="0" type="image/png"/>
    <pubDate>Fri, 03 Apr 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Elastic Open Web Crawler en tant que code]]></title>
    <description><![CDATA[Apprenez à utiliser les actions GitHub pour gérer les configurations d'Elastic Open Crawler, de sorte que chaque fois que nous apportons des modifications au référentiel, celles-ci sont automatiquement appliquées à l'instance déployée du crawler.]]></description>
    <content:encoded><![CDATA[<p>Avec <a href="https://github.com/elastic/crawler">Elastic Open Web Crawler</a> et son architecture pilotée par CLI, il est désormais assez simple d'avoir des configurations de crawler versionnées et un pipeline CI/CD avec des tests locaux.</p><p>Traditionnellement, la gestion des robots d'indexation était un processus manuel et sujet aux erreurs. Il s'agissait de modifier les configurations directement dans l'interface utilisateur et de se débattre avec le clonage des configurations de crawl, le retour en arrière, la gestion des versions, etc. Traiter les configurations des robots comme du code résout ce problème en offrant les mêmes avantages que ceux que nous attendons du développement de logiciels : répétabilité, traçabilité et automatisation.</p><p>Ce flux de travail facilite l'intégration de l'Open Web Crawler dans votre pipeline CI/CD pour les retours en arrière, les sauvegardes et les migrations, tâches qui étaient beaucoup plus délicates avec les Elastic Crawlers précédents, tels que l'Elastic Web Crawler ou l'App Search Crawler.</p><p>Dans cet article, nous allons apprendre à.. :</p><ul><li><p>Gérer nos configurations de crawl en utilisant GitHub</p></li><li><p>Disposer d'une installation locale pour tester les pipelines avant de les déployer</p></li><li><p>Créer une configuration de production pour exécuter le robot d'exploration avec de nouveaux paramètres à chaque fois que nous apportons des modifications à notre branche principale.</p></li></ul><p>Vous pouvez trouver le dépôt du projet <a href="https://github.com/llermaly/elastic-open-crawler-as-code"><em><strong>ici</strong></em></a><em><strong>. </strong></em><em>Pour l'instant, j'utilise Elasticsearch 9.1.3 et Open Web Crawler 0.4.2.</em></p><h2>Produits requis</h2><ul><li><p>Bureau Docker</p></li><li><p>Instance Elasticsearch</p></li><li><p>Machine virtuelle avec accès SSH (par exemple, AWS EC2) et Docker installé.</p></li></ul><h2>Étapes</h2><ol><li><p>Structure des dossiers</p></li><li><p>Configuration du robot</p></li><li><p>Fichier Docker-compose (environnement local)</p></li><li><p>Actions Github</p></li><li><p>Tests au niveau local</p></li><li><p>Déploiement vers prod</p></li><li><p>Modifications et redéploiement</p></li></ol><h2>Structure des dossiers</h2><p>Pour ce projet, nous aurons la structure de fichier suivante :</p>├── docker-compose.yml # Local elasticsearch + crawler
├── config/crawler-config.yml # Crawler config
├── .github/workflows/deploy.yml # GH Action to deploy changes
├── local.sh # Script to run our local crawler<h2>Configuration du robot</h2><p>Sous <code>crawler-config.yml,</code>, nous mettrons les éléments suivants :</p>output_sink: elasticsearch
output_index: web-crawl-index
max_crawl_depth: 1

elasticsearch:
  host: ${ES_HOST}
  api_key: ${ES_API_KEY}
     
domains:
  - url: https://web-scraping.dev
    seed_urls:
      - https://web-scraping.dev/product/1
      - https://web-scraping.dev/product/2
      - https://web-scraping.dev/product/3<p>Il s'agit d'une recherche à partir de <a href="https://web-scraping.dev/products">https://web-scraping.dev/products,</a> un site fictif pour les produits. Nous ne parcourrons que les trois premières pages du produit. Le paramètre <code>max_crawl_depth</code> empêchera le robot d'exploration de découvrir d'autres pages que celles définies comme <code>seed_urls</code> en n'ouvrant pas les liens qu'elles contiennent.</p><p>Elasticsearch <code>host</code> et <code>api_key</code> seront alimentés dynamiquement en fonction de l'environnement dans lequel nous exécutons le script.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt7d37b966aafbd3c1/6a17ef9842022946e929f6b9/f9831034e1c4ccb554d37bdd188f2824338355a0-890x624.png" alt="La page produit pour &quot;Boîte de bonbons au chocolat&quot; provient du domaine web-scraping.dev, un site fictif pour tester le web scraping. La page affiche le titre du produit, l'image, la description et les éléments HTML pour les boutons de prix et d'achat." /><h2>Fichier Docker-compose (environnement local)</h2><p>Pour le site local <code>docker-compose.yml,</code>, nous allons déployer le crawler et un seul cluster Elasticsearch + Kibana, de sorte que nous puissions facilement visualiser nos résultats de crawling <em><strong>avant de</strong></em> les déployer en production.</p>services:
  es01:
    image: docker.elastic.co/elasticsearch/elasticsearch:9.1.3
    environment:
      - discovery.type=single-node
      - xpack.security.enabled=false
      - ES_JAVA_OPTS=-Xms1g -Xmx1g
    ports:
      - "9200:9200"
    networks: [esnet]
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:9200"]
      interval: 5s
      timeout: 5s
      retries: 10

  kibana:
    image: docker.elastic.co/kibana/kibana:9.1.3
    environment:
      - ELASTICSEARCH_HOSTS=http://es01:9200
    ports:
      - "5601:5601"
    networks: [esnet]
    depends_on: [es01]

  crawler:
    image: docker.elastic.co/integrations/crawler:0.4.2
    environment:
      - ES_HOST=http://es01:9200
      - CRAWLER_JRUBY_OPTS=--server
    container_name: crawler
    volumes:
      - ./config:/home/app/config
    networks: [esnet]
    entrypoint: ["/home/app/bin/crawler", "crawl", "/home/app/config/crawl-config-final.yml"]
    stdin_open: true
    tty: true

networks:
  esnet:
    driver: bridge<p>Notez que le crawler attend qu'Elasticsearch soit prêt à fonctionner.</p><h2>Actions Github</h2><p>Nous devons maintenant créer une action GitHub qui copiera les nouveaux paramètres et exécutera le crawler dans notre machine virtuelle à chaque poussée vers main. Ainsi, nous disposons toujours de la dernière configuration déployée, sans avoir à entrer manuellement dans la machine virtuelle pour mettre à jour les fichiers et exécuter le crawler. Nous allons utiliser AWS EC2 comme fournisseur de machines virtuelles.</p><p>La première étape consiste à ajouter l'hôte (<code>VM_HOST</code>), l'utilisateur de la machine (<code>VM_USER</code>), la clé RSA SSH (<code>VM_KEY</code>), l'hôte Elasticsearch (<code>ES_HOST</code>) et la clé API Elasticsearch (<code>ES_API_KEY</code>) aux secrets d'action GitHub :</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltb5a0fcfff9b7f997/6a17ef9a6df731d7d40a0fdf/e1075bc54151b4b94eac2a6bd2682e9997e6c709-1106x707.png" alt="Une page de configuration &quot;actions, secrets et variables&quot;, affichant les secrets du référentiel tels que VM_HOST, VM_KEY et VM_USER dans une interface web." /><p>De cette manière, l'action pourra accéder à notre serveur pour copier les nouveaux fichiers et exécuter le crawl.</p><p>Maintenant, créons notre fichier <code>.github/workflows/deploy.yml</code>:</p>name: Deploy

on:
  push:
    branches: [main]

jobs:
  Deploy:
    name: Deploy to EC2
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v5

      - name: Deploy crawler
        env:
          HOSTNAME: ${{ secrets.VM_HOST }}
          USER_NAME: ${{ secrets.VM_USER }}
          PRIVATE_KEY: ${{ secrets.VM_KEY }}
          ES_HOST: ${{ secrets.ES_HOST }}
          ES_API_KEY: ${{ secrets.ES_API_KEY }}
        run: |
          # Save private key
          echo "$PRIVATE_KEY" &gt; private_key
          chmod 600 private_key

          # Generate final config locally
          envsubst &lt; config/crawler-config.yml &gt; config/crawl-config-final.yml

          # Copy the config folder to VM
          scp -o StrictHostKeyChecking=no -i private_key -r config ${USER_NAME}@${HOSTNAME}:~/config

          # SSH into VM and run crawler
          ssh -o StrictHostKeyChecking=no -i private_key ${USER_NAME}@${HOSTNAME} &lt;&lt; EOF
            docker run --rm \
              -v ~/config:/config \
              docker.elastic.co/integrations/crawler:latest jruby \
              bin/crawler crawl /config/crawl-config-final.yml
          EOF<p>Cette action exécutera les étapes suivantes à chaque fois que des modifications seront apportées au fichier de configuration du crawler :</p><ol><li><p>Renseigner l'hôte Elasticsearch et la clé API dans la configuration yml</p></li><li><p>Copier le dossier config sur notre VM</p></li><li><p>Se connecter via SSH à notre VM</p></li><li><p>Exécuter le crawl avec la configuration que nous venons de copier depuis le repo</p></li></ol><h2>Tests au niveau local</h2><p>Pour tester notre crawler localement, nous avons créé un script bash qui remplit l'hôte Elasticsearch avec l'hôte local de Docker et démarre un crawl. Vous pouvez lancer <code>./local.sh</code> pour l'exécuter.</p>#!/bin/bash

# Exit on any error
set -e

# Load environment variables
export ES_HOST="http://es01:9200"

# Generate final crawler config
envsubst &lt; ./config/crawler-config.yml &gt; ./config/crawl-config-final.yml

# Bring everything up
docker compose up --build<p>Regardons Kibana DevTools pour confirmer que le site<code> web-crawler-index</code> a été correctement renseigné :</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt989660368fe14db8/6a17ef9b9da390c79ce46562/18551635e8265866e389a9632c4e4540958e4468-990x723.png" alt="Code Kibana DevTools pour confirmer que le web-crawler-index est correctement configuré." /><h2>Déploiement vers prod</h2><p>Nous sommes maintenant prêts à pousser vers la branche principale, ce qui déploiera le crawler dans votre machine virtuelle et commencera à envoyer des logs à votre instance Serverless Elasticsearch.</p>git add .
git commit -m "First commit"
git push<p>Cela déclenchera l'action GitHub, qui exécutera le script de déploiement dans la machine virtuelle et commencera l'exploration.</p><p>Vous pouvez confirmer que l'action a été exécutée en allant sur le dépôt GitHub et en visitant l'onglet "Actions" :</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt986f1a4e4f3288d2/6a17ef9c7f6f1584fdc09c10/67ba3a7164d7a8049fe5661264820826cb18ed64-667x325.png" alt="Les actions se déploient sur l'onglet EC2 dans un dépôt GitHub." /><h2>Modifications et redéploiement</h2><p>Vous avez peut-être remarqué que le site <code>price</code> de chaque produit fait partie du champ "body" du document. L'idéal serait de stocker le prix dans un champ distinct afin de pouvoir utiliser des filtres.</p><p>Ajoutons cette modification au fichier <code>crawler.yml</code> afin d'utiliser des <a href="https://github.com/elastic/crawler/blob/main/docs/features/EXTRACTION_RULES.md">règles d'extraction</a> pour extraire le prix de la classe CSS <code>product-price</code>:</p>output_sink: elasticsearch
output_index: web-crawl-index
max_crawl_depth: 1

elasticsearch:
  host: ${ES_HOST}
  api_key: ${ES_API_KEY}
     
  # Index ingest pipeline to process documents before indexing          
  pipeline_enabled: true
  pipeline: pricing-pipeline

domains:
  - url: https://web-scraping.dev
    seed_urls:
      - https://web-scraping.dev/product/1
      - https://web-scraping.dev/product/2
      - https://web-scraping.dev/product/3
    extraction_rulesets:
      - url_filters:
          - type: ends
            pattern: /product/*
        rules:
          - action: extract
            field_name: price
            selector: .product-price
            join_as: string
            source: html<p>Nous constatons également que le prix comprend un signe de dollar (<code>$</code>), que nous devons supprimer si nous voulons exécuter des requêtes de plage. Nous pouvons utiliser un pipeline d'acquisition pour cela. Notez que nous y faisons référence dans notre nouveau fichier de configuration du crawler ci-dessus :</p>PUT _ingest/pipeline/pricing-pipeline
{
  "processors": [
    {
      "script": {
        "source": """
                ctx['price'] = ctx['price'].replace("$","")
            """
      }
    }
  ]
}<p>Nous pouvons exécuter cette commande dans notre cluster Elasticsearch de production. Pour celui de développement, comme il est éphémère, nous pouvons intégrer la création du pipeline dans le fichier <code>docker-compose.yml</code> en ajoutant le service suivant. Notez que nous avons également ajouté un <code>depends_on</code> au service crawler afin qu'il démarre après la création réussie du pipeline.</p> crawler:
    image: docker.elastic.co/integrations/crawler:0.4.2
    environment:
      - ES_HOST=http://es01:9200
      - CRAWLER_JRUBY_OPTS=--server
    container_name: crawler
    volumes:
      - ./config:/home/app/config
    networks: [esnet]
    entrypoint: ["/home/app/bin/crawler", "crawl", "/home/app/config/crawl-config-final.yml"]
    depends_on:
      pipeline-init:
        condition: service_completed_successfully
    stdin_open: true
    tty: true  


  pipeline-init:
    image: curlimages/curl:latest
    depends_on:
      es01:
        condition: service_healthy
    networks: [esnet]
    entrypoint: &gt;
        sh -c "
        echo 'Creating ingest pipeline...';
        curl -s -X PUT http://es01:9200/_ingest/pipeline/pricing-pipeline \\
          -H 'Content-Type: application/json' \\
          -d '{\"processors\":[{\"script\":{\"source\":\"ctx.price = ctx.price.replace(\\\"$\\\", \\\"\\\")\"}}]}';
        echo 'Pipeline created!';
        "<p>Exécutons maintenant <code>`./local.sh`</code> pour voir les changements localement :</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt0f390aedf67cb4fe/6a17ef9eaf47b62bd2cde05b/dc1801599344a9f69f072b07ff828c4ba3815d7b-738x473.png" alt="Exécution de `./local.sh` pour voir le prix changer localement." /><p>C'est très bien ! Poussons maintenant la modification :</p>git add crawler-config.yml
git commit -m "added price CSS selector"
git push<p>Pour confirmer que tout fonctionne, vous pouvez vérifier votre Kibana de production, qui devrait refléter les changements et afficher le prix comme un nouveau champ sans le signe du dollar.</p><h2>Conclusion</h2><p>Elastic Open Web Crawler vous permet de gérer votre crawler en tant que code, ce qui signifie que vous pouvez automatiser l'ensemble du pipeline - du développement au déploiement - et ajouter des environnements locaux éphémères et des tests sur les données explorées de manière programmatique, pour ne citer que quelques exemples.</p><p>Vous êtes invités à cloner le dépôt officiel et à commencer à indexer vos propres données à l'aide de ce flux de travail. Vous pouvez également lire <a href="https://www.elastic.co/search-labs/blog/semantic-search-open-crawler">cet article</a> pour apprendre comment effectuer une recherche sémantique sur les index produits par le crawler.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/elastic-open-crawler-config-as-code</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/elastic-open-crawler-config-as-code</guid>
    <category><![CDATA[Indexer des données]]></category>
    <category><![CDATA[Opérations]]></category>
    <dc:creator><![CDATA[Gustavo Llermaly]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt00e7c95012dc38cc/6a17efa0fbc5f80dad491b93/0ac41f55c85ad3f647cb0e0d750ed80bacd397f3-1036x581.png" length="0" type="image/png"/>
    <pubDate>Mon, 22 Sep 2025 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Création d'une application de recherche avec Blazor et Elasticsearch]]></title>
    <description><![CDATA[Apprenez à construire une application de recherche en utilisant Blazor et Elasticsearch, et à utiliser le client Elasticsearch .NET pour la recherche hybride.]]></description>
    <content:encoded><![CDATA[<p>Dans cet article, vous apprendrez à tirer parti de vos compétences en C# pour créer une application de recherche à l'aide de Blazor et d'Elasticsearch. Nous allons utiliser le client <a href="https://www.elastic.co/guide/en/elasticsearch/client/net-api/current/introduction.html">Elasticsearch .NET</a> pour exécuter des requêtes de recherche <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/full-text-queries.html">plein texte</a>, <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/semantic-search.html">sémantique</a> et <a href="https://www.elastic.co/search-labs/tutorials/search-tutorial/vector-search/hybrid-search">hybride</a>.</p><p><strong>NOTE</strong> Si vous êtes familier avec l'ancienne version du client Elasticsearch C# <a href="https://www.elastic.co/guide/en/elasticsearch/client/net-api/7.17/nest.html">NEST</a>, lisez cet <a href="https://www.elastic.co/search-labs/blog/net-client-evolution">article de blog</a> sur la dépréciation du client NEST et les nouvelles fonctionnalités. <em>NEST était la génération précédente du client .NET, qui a été remplacée par le client </em><code>Elastic.Clients.Elasticsearch package</code></p><p></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltca8d1da68641fac0/6a17f65763173044d4585bfb/18f890286ca122cd97286c09ebf740208b802d0b-650x395.png" alt="Construire une application blazor : esre avec diagramme blazor" /><ul><li><p><a href="https://www.elastic.co/search-labs/blog/search-app-with-esre-blazor#what-is-blazor?">Qu'est-ce que Blazor ?</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/search-app-with-esre-blazor#what-is-esre?">Qu'est-ce que l'ESRE ?</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/search-app-with-esre-blazor#configuring-elser">Configuration d'ELSER</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/search-app-with-esre-blazor#indexing-data">Indexation des données</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/search-app-with-esre-blazor#building-the-app-with-blazor-&amp;-elasticsearch">Demande de permis de construire</a></p></li></ul><h2>Qu'est-ce que Blazor ?</h2><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt91278ae422883e28/6a17f6592f4a5c3213fa8a95/622741915d016b68bf94f742332d10d736d60052-707x461.png" alt="Serveur Blazor" /><p><a href="https://dotnet.microsoft.com/en-us/apps/aspnet/web-apps/blazor">Blazor</a> est un framework web open source basé sur HTML, CSS et C# créé par Microsoft pour permettre aux développeurs de créer des applications web qui s'exécutent sur le client ou le serveur. Blazor vous permet également de créer des composants réutilisables pour construire des applications plus rapidement ; il permet aux développeurs de construire la vue HTML et les actions en C# dans le même fichier, ce qui aide à maintenir un code lisible et propre. De plus, avec Blazor Hybrid, vous pouvez créer des applications mobiles natives accédant aux capacités de la plateforme native via le code .NET.</p><p>Quelques-unes des caractéristiques qui font de Blazor un framework idéal pour travailler :</p><ul><li><p>Options de rendu côté serveur et côté client</p></li><li><p>Composants réutilisables de l'interface utilisateur</p></li><li><p>Mises à jour en temps réel avec SignalR</p></li><li><p>Gestion intégrée des états</p></li><li><p>Système de routage intégré</p></li><li><p>Vérifications solides du typage et de la compilation</p></li></ul><h3>Pourquoi Blazor ?</h3><p>Blazor offre plusieurs avantages par rapport à d'autres cadres et bibliothèques : il permet aux développeurs d'utiliser le langage C# pour le code client et le code serveur, en fournissant un typage fort et une vérification au moment de la compilation, ce qui améliore la fiabilité. Il s'intègre parfaitement à l'écosystème .NET, permettant la réutilisation de bibliothèques et d'outils .NET, et offre un support de débogage robuste.</p><h2>Qu'est-ce que l'ESRE ?</h2><p><a href="https://www.elastic.co/elasticsearch/elasticsearch-relevance-engine">Elasticsearch Relevance Engine™ (ESRE</a> ) est un ensemble d'<a href="https://www.elastic.co/guide/en/esre/current/learn.html">outils permettant de créer des applications de recherche</a> utilisant l'apprentissage automatique et l'intelligence artificielle au-dessus du puissant moteur de recherche Elasticsearch.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf313ba986de01b93/6a17d851505ac306fcad8975/4c0f2645ed1c27fe3ef61a1a9126adadfd8d5368-721x421.png" alt="esre" /><p>Pour en savoir plus sur l'ESRE, vous pouvez lire notre article de blog <a href="https://www.elastic.co/search-labs/blog/introducing-elasticsearch-relevance-engine-esre">ici.</a></p><h2>Configuration d'ELSER</h2><p>Pour tirer parti des capacités <a href="https://www.elastic.co/elasticsearch/elasticsearch-relevance-engine">ESRE</a> d'Elastic, nous allons utiliser <a href="https://www.elastic.co/guide/en/machine-learning/current/ml-nlp-elser.html">ELSER</a> comme fournisseur de modèle.</p><p><em>Remarque : pour utiliser les modèles ELSER d'Elasticsearch, vous devez disposer d'une licence Platinum ou Enterprise, et d'un nœud de Machine Lerning (ML) dédié d'une taille minimale de 4 Go. Pour en savoir plus,</em> <a href="https://www.elastic.co/guide/en/machine-learning/8.15/ml-nlp-elser.html#elser-req"><em>cliquez ici.</em></a></p><p>Commencez par créer le point d'arrivée de l'inférence :</p>PUT _inference/sparse_embedding/my-elser-model
{
  "service": "elser",
  "service_settings": {
    "num_allocations": 1,
    "num_threads": 1
  }
}<p>Si vous utilisez ELSER pour la première fois, il se peut que vous rencontriez une erreur 502 Bad Gateway lorsque le modèle se charge en arrière-plan. Vous pouvez vérifier l'état du modèle sur <code>Machine Learning &gt; Trained Models</code> dans Kibana. Une fois qu'il est déployé, vous pouvez passer à l'étape suivante.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltee295a3516338302/6a17f65b414c640deb94531d/a7ff94b72d337cde892165d744b9f42fba702a87-1440x649.png" alt="Vérification des modèles formés" /><h2>Indexation des données</h2><p>Vous pouvez télécharger le jeu de données <a href="https://github.com/elastic/elasticsearch-labs/tree/main/supporting-blog-content/esre-with-blazor/books.zip">ici</a> et importer les données à l'aide de Kibana. Pour ce faire, allez sur la page d'accueil et cliquez sur "Upload data". Ensuite, téléchargez le fichier et cliquez sur <code>Import</code>. Enfin, allez dans l'onglet <code>Advanced</code> et collez les mappings suivants :</p>{
   "properties":{
      "authors":{
         "type":"keyword"
      },
      "categories":{
         "type":"keyword"
      },
      "longDescription":{
         "type":"semantic_text",
         "inference_id":"my-elser-model",
         "model_settings":{
            "task_type":"sparse_embedding"
         }
      },
      "pageCount":{
         "type":"integer"
      },
      "publishedDate":{
         "type":"date"
      },
      "shortDescription":{
         "type":"text"
      },
      "status":{
         "type":"keyword"
      },
      "thumbnailUrl":{
         "type":"keyword"
      },
      "title":{
         "type":"text"
      }
   }
}<img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt0f07300647edd2ca/6a17f65dfaa913172a93ca0c/1aea0b9c51e275f89339f5d463fcaef799fc3943-1235x1083.png" alt="Importer des données" /><p>Nous allons créer un index capable d'exécuter des requêtes sémantiques et en texte intégral. Le type de champ <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/semantic-text.html">semantic_text</a> se chargera du découpage et de l'intégration des données. <em>Notez que nous indexons</em> <em><code>longDescription</code></em> <em>comme</em> <em><code>semantic_text</code></em>,<em> vous pouvez utiliser copy_to si vous voulez indexer un champ comme</em> <em><code>semantic_text</code></em> <em>et text`.</em></p><h2>Construire l'application avec Blazor &amp; Elasticsearch</h2><h3>Clé API</h3><p>La première chose à faire est de créer une clé API pour authentifier nos requêtes à Elasticsearch. La clé API doit être en lecture seule et n'être autorisée qu'à interroger l'index <code>books-blazor</code>.</p>POST /_security/api_key
{
  "name": "books-blazor-key",
  "role_descriptors": {
    "books-blazor-reader": {
      "indices": [
        {
          "names": ["books-blazor"],
          "privileges": ["read"]
        }
      ]
    }
  }
}<p>Vous verrez quelque chose comme ceci :
</p>{
  "id": "XXXXXXXXXXXXXXXXXXXXXXXX",
  "name": "books-blazor-key",
  "api_key": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
  "encoded": "XXXXXXXXXXXXXXXXXXXXXXXX=="
}<p>Enregistrez la valeur du champ de réponse <code>encoded</code> car vous en aurez besoin ultérieurement. Si vous travaillez sur <a href="https://www.elastic.co/cloud/">Elastic Cloud</a>, vous aurez également besoin de votre Cloud ID. (Vous pouvez le trouver <a href="https://www.elastic.co/search-labs/tutorials/install-elasticsearch/elastic-cloud#finding-your-cloud-id">ici</a>).</p><h4>Création du projet Blazor</h4><p>Commencez par installer Blazor et créez un projet d'exemple en suivant les <a href="https://dotnet.microsoft.com/en-us/learn/aspnet/blazor-tutorial/install">instructions officielles.</a></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltb5c20b896fe1e98e/6a17f65f6317301566585bff/f5720867d960c91fd1b3c4ae9be174e062e6b2fc-975x830.png" alt="Blazor tutorial - Création du projet Blazor" /><p>Une fois le projet créé, la structure des dossiers et des fichiers doit ressembler à ceci :</p>BlazorApp/
|-- BlazorApp.csproj
|-- BlazorApp.sln
|-- Program.cs
|-- appsettings.Development.json
|-- appsettings.json
|-- Properties/
|   `-- launchSettings.json
|-- Components/
|   |-- App.razor
|   |-- Routes.razor
|   |-- _Imports.razor
|   |-- Layout/
|   |   |-- MainLayout.razor
|   |   |-- MainLayout.razor.css
|   |   |-- NavMenu.razor
|   |   `-- NavMenu.razor.css
|   `-- Pages/
|       |-- Counter.razor
|       |-- Error.razor
|       |-- Home.razor
|       `-- Weather.razor
|-- wwwroot/
|-- bin/
`-- obj/ <p>Le modèle d'application comprend <a href="https://blog.getbootstrap.com/2021/08/04/bootstrap-5-1-0/">Bootstrap v5.1.0</a> pour le stylisme.</p><p>Terminez la configuration du projet en installant le client <a href="https://www.elastic.co/guide/en/elasticsearch/client/net-api/8.0/installation.html">Elasticsearch .NET :</a></p>dotnet add package Elastic.Clients.Elasticsearch<p>Une fois cette étape terminée, votre page devrait ressembler à ceci :</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt3b90e394ded7ff56/6a17f660faa913d49f93ca10/ef9d2186c2cf49e78a307c4aa69c51632e84578d-940x529.png" alt="Blazor hello world" /><h3>Structure des dossiers</h3><p>Nous allons maintenant organiser nos dossiers comme suit :</p>BlazorApp/
|-- Components/
|   |-- Pages/
|   |   |-- Search.razor
|   |   `-- Search.razor.css
|   `-- Elasticsearch/
|       |-- SearchBar.razor
|       |-- Results.razor
|       `-- Facet.razor
|-- Models/
|   |-- Book.cs
|   `-- Response.cs
`-- Services/
    `-- ElasticsearchService.cs<p>Les dossiers sont expliqués :</p><ul><li><p>Components/Pages/Search.razor : page principale contenant la barre de recherche, les résultats et les filtres.</p></li><li><p>Composants/Pages/Search.razor.css : les styles de page.</p></li><li><p>Components/Elasticsearch/SearchBar.razor : composant de barre de recherche.</p></li><li><p>Components/Elasticsearch/Results.razor : composant de résultats.</p></li><li><p>Components/Elasticsearch/Facet.razor : composant de filtres.</p></li><li><p>Components/Svg/GlassIcon.razor : icône de recherche.</p></li><li><p>Components/_Imports.razor : ce fichier importera tous les composants.</p></li><li><p>Models/Book.cs : ce fichier contient le schéma du champ livre.</p></li><li><p>Models/Response.cs : ce fichier stocke le schéma de la réponse, y compris les résultats de la recherche, les facettes et le nombre total de résultats.</p></li><li><p>Services/ElasticsearchService.cs : Service Elasticsearch. Il gère la connexion et les requêtes à Elasticsearch.</p></li></ul><h4>Configuration initiale</h4><p>Commençons par un peu de nettoyage.</p><p>Supprimer les fichiers :</p><ul><li><p>Composants/Pages/Compteur.razor</p></li><li><p>Composants/Pages/Météo.razor</p></li><li><p>Composants/Pages/Home.razor</p></li><li><p>Components/Layout/NavMenu.razor</p></li><li><p>Composants/Layout/NavMenu.razor.css</p></li></ul><p>Vérifiez le fichier <code>/Components/_Imports.razor</code>. Vous devriez avoir les importations suivantes :</p>@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using BlazorApp
@using BlazorApp.Components<h4>Intégrer Elastic dans le projet</h4><p>Maintenant, importons les composants d'Elasticsearch :</p>@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using BlazorApp
@using BlazorApp.Components
@using BlazorApp.Components.Elasticsearch @* &lt;--- Add this line *@<p>Nous allons supprimer la barre latérale par défaut afin de disposer de plus d'espace pour notre application en la supprimant du fichier <code>/Components/Layout/MainLayout.razor</code>:</p>@inherits LayoutComponentBase

&lt;div class="page"&gt;
    &lt;main&gt;
        &lt;article class="content"&gt;
            @Body
        &lt;/article&gt;
    &lt;/main&gt;
&lt;/div&gt;

&lt;div id="blazor-error-ui"&gt;
    An unhandled error has occurred.
    &lt;a href="" class="reload"&gt;Reload&lt;/a&gt;
    &lt;a class="dismiss"&gt;🗙&lt;/a&gt;
&lt;/div&gt;<img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt441012c5aeac85f8/6a17f661ec0f8949f15a67ac/55bfff607f9167a532f83ad074b479364a346c6e-816x472.png" alt="barre de navigation supprimée de la mise en page" /><p>Entrons maintenant les informations d'identification Elasticsearch pour les <a href="https://learn.microsoft.com/en-us/aspnet/core/security/app-secrets?view=aspnetcore-8.0&amp;tabs=linux#secret-manager">secrets d'utilisateur</a>:</p>dotnet user-secrets init
dotnet user-secrets set ElasticsearchCloudId "your Cloud ID"
dotnet user-secrets set ElasticsearchApiKey "your API Key"<p>Grâce à cette approche, .Net 8 stocke les données sensibles dans un emplacement distinct, en dehors du dossier du projet, et les rend accessibles à l'aide de l'interface <code>IConfiguration</code>. Ces variables seront disponibles pour tout projet .Net qui utilise les mêmes secrets d'utilisateur.</p><p>Ensuite, modifions le fichier <code>Program.cs</code> pour lire les secrets et monter le client Elasticsearch :</p><p>Tout d'abord, il faut importer les bibliothèques nécessaires :</p>using BlazorApp.Services;
using Elastic.Clients.Elasticsearch;
using Elastic.Transport;<ul><li><p>BlazorApp.Services : contient le service Elasticsearch.</p></li><li><p>Elastic.Clients.Elasticsearch : importe la bibliothèque .Net 8 du client Elasticsearch.</p></li><li><p>Elastic.Transport : importe la bibliothèque de transport Elasticsearch, ce qui nous permet d'utiliser la classe ApiKey pour authentifier nos requêtes.</p></li></ul><p>Deuxièmement, insérez le code suivant avant la ligne <code>var app = builder.Build()</code>:</p>// Initialize the Elasticsearch client.
builder.Services.AddScoped(sp =&gt;
{
    // Getting access to the configuration service to read the Elasticsearch credentials.
    var configuration = sp.GetRequiredService&lt;IConfiguration&gt;();
    var cloudId = configuration["ElasticsearchCloudId"];
    var apiKey = configuration["ElasticsearchApiKey"];

    if (string.IsNullOrEmpty(cloudId) || string.IsNullOrEmpty(apiKey))
    {
        throw new InvalidOperationException(
            "Elasticsearch credentials are missing in configuration."
        );
    }

    var settings = new ElasticsearchClientSettings(cloudId, new ApiKey(apiKey)).EnableDebugMode();
    return new ElasticsearchClient(settings);
});<p>Ce code va lire les informations d'identification Elasticsearch à partir des secrets d'utilisateur et créer une instance de client Elasticsearch.</p><p>Après l'initialisation du client ElasticSearch, ajoutez la ligne suivante pour enregistrer le service Elasticsearch :</p>builder.Services.AddScoped&lt;ElasticsearchService&gt;();<p>L'étape suivante consistera à construire la logique de recherche dans le fichier <code>/Services/ElasticsearchService.cs</code>:</p><p>Tout d'abord, il faut importer les bibliothèques et les modèles nécessaires :</p>using BlazorApp.Models;
using Elastic.Clients.Elasticsearch;
using Elastic.Clients.Elasticsearch.QueryDsl;<p>Ensuite, ajoutez la classe <code>ElasticsearchService</code>, le constructeur et les variables :</p>namespace BlazorApp.Services
{
    public class ElasticsearchService
    {
        private readonly ElasticsearchClient _client;

        // The logger is used to log information, warnings and errors about the Elasticsearch service and requests.
        private readonly ILogger&lt;ElasticsearchService&gt; _logger;

        public ElasticsearchService(
            ElasticsearchClient client,
            ILogger&lt;ElasticsearchService&gt; logger
        )
        {
            _client = client ?? throw new ArgumentNullException(nameof(client));
            _logger = logger;
        }
    }
}<h4>Configuration de la recherche</h4><p>Construisons maintenant notre logique de recherche :</p>private static Action&lt;RetrieverDescriptor&lt;BookDoc&gt;&gt; BuildHybridQuery(
    string searchTerm,
    Dictionary&lt;string, List&lt;string&gt;&gt; selectedFacets
)
{
    var filters = BuildFilters(selectedFacets);

    return retrievers =&gt;
        retrievers.Rrf(rrf =&gt;
            rrf.RankWindowSize(50)
                .RankConstant(20)
                .Retrievers(
                    retrievers =&gt;
                        retrievers.Standard(std =&gt;
                            std.Query(q =&gt;
                                q.Bool(b =&gt;
                                    b.Must(m =&gt;
                                            m.MultiMatch(mm =&gt;
                                                mm.Query(searchTerm)
                                                    .Fields(
                                                        new[]
                                                        {
                                                            "title",
                                                            "shortDescription",
                                                        }
                                                    )
                                            )
                                        )
                                        .Filter(filters.ToArray())
                                )
                            )
                        ),
                    retrievers =&gt;
                        retrievers.Standard(std =&gt;
                            std.Query(q =&gt;
                                q.Bool(b =&gt;
                                    b.Must(m =&gt;
                                            m.Semantic(sem =&gt;
                                                sem.Field("longDescription")
                                                    .Query(searchTerm)
                                            )
                                        )
                                        .Filter(filters.ToArray())
                                )
                            )
                        )
                )
        );
}

public static List&lt;Action&lt;QueryDescriptor&lt;BookDoc&gt;&gt;&gt; BuildFilters(
    Dictionary&lt;string, List&lt;string&gt;&gt; selectedFacets
)
{
    var filters = new List&lt;Action&lt;QueryDescriptor&lt;BookDoc&gt;&gt;&gt;();

    if (selectedFacets != null)
    {
        foreach (var facet in selectedFacets)
        {
            foreach (var value in facet.Value)
            {
                var field = facet.Key.ToLower();
                if (!string.IsNullOrEmpty(field))
                {
                    filters.Add(m =&gt; m.Term(t =&gt; t.Field(new Field(field)).Value(value)));
                }
            }
        }
    }

    return filters;
}<ul><li><p><code>BuildFilters</code> construira les filtres pour la requête de recherche en utilisant les facettes sélectionnées par l'utilisateur.</p></li><li><p><code>BuildHybridQuery</code> construira une requête de <a href="https://www.elastic.co/search-labs/tutorials/search-tutorial/vector-search/hybrid-search">recherche hybride</a> qui combine la recherche plein texte et la recherche sémantique.</p></li></ul><p>Ensuite, ajoutez la méthode de recherche :</p>public async Task&lt;ElasticResponse&gt; SearchBooksAsync(
    string searchTerm,
    Dictionary&lt;string, List&lt;string&gt;&gt; selectedFacets
)
{
    try
    {
        _logger.LogInformation($"Performing search for: {searchTerm}");

        // Retrieve the hybrid query with filters applied.
        var retrieverQuery = BuildHybridQuery(searchTerm, selectedFacets);

        var response = await _client.SearchAsync&lt;BookDoc&gt;(s =&gt;
            s.Index("elastic-blazor-books")
                .Retriever(retrieverQuery)
                .Aggregations(aggs =&gt;
                    aggs.Add("Authors", agg =&gt; agg.Terms(t =&gt; t.Field(p =&gt; p.Authors)))
                        .Add(
                            "Categories",
                            agg =&gt; agg.Terms(t =&gt; t.Field(p =&gt; p.Categories))
                        )
                        .Add("Status", agg =&gt; agg.Terms(t =&gt; t.Field(p =&gt; p.Status)))
                )
        );

        if (response.IsValidResponse)
        {
            _logger.LogInformation($"Found {response.Documents.Count} documents");

            var hits = response.Total;
            var facets =
                response.Aggregations != null
                    ? FormatFacets(response.Aggregations)
                    : new Dictionary&lt;string, Dictionary&lt;string, long&gt;&gt;();

            var elasticResponse = new ElasticResponse
            {
                TotalHits = hits,
                Documents = response.Documents.ToList(),
                Facets = facets,
            };

            return elasticResponse;
        }
        else
        {
            _logger.LogWarning($"Invalid response: {response.DebugInformation}");
            return new ElasticResponse();
        }
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "Error performing search");
        return new ElasticResponse();
    }
}

public static Dictionary&lt;string, Dictionary&lt;string, long&gt;&gt; FormatFacets(
    Elastic.Clients.Elasticsearch.Aggregations.AggregateDictionary aggregations
)
{
    var facets = new Dictionary&lt;string, Dictionary&lt;string, long&gt;&gt;();

    foreach (var aggregation in aggregations)
    {
        if (
            aggregation.Value
            is Elastic.Clients.Elasticsearch.Aggregations.StringTermsAggregate termsAggregate
        )
        {
            var facetName = aggregation.Key;
            var facetDictionary = ConvertFacetDictionary(
                termsAggregate.Buckets.ToDictionary(b =&gt; b.Key, b =&gt; b.DocCount)
            );
            facets[facetName] = facetDictionary;
        }
    }

    return facets;
}

private static Dictionary&lt;string, long&gt; ConvertFacetDictionary(
    Dictionary&lt;Elastic.Clients.Elasticsearch.FieldValue, long&gt; original
)
{
    var result = new Dictionary&lt;string, long&gt;();
    foreach (var kvp in original)
    {
        result[kvp.Key.ToString()] = kvp.Value;
    }
    return result;
}<ul><li><p><code>SearchBooksAsync</code>: effectuera la recherche en utilisant la requête hybride et renverra les résultats en incluant les agrégations pour construire les facettes.</p></li><li><p><code>FormatFacets</code>: formatera la réponse des agrégations dans un dictionnaire.</p></li><li><p><code>ConvertFacetDictionary</code>: convertit le dictionnaire de facettes dans un format plus lisible.</p></li></ul><p>L'étape suivante consiste à créer les modèles qui représenteront les données renvoyées dans le site <code>hits</code> de la requête Elasticsearch et qui seront imprimées en tant que résultats dans notre page de recherche.</p><p>Nous commençons par créer le fichier <code>/Models/Book.cs</code> et y ajouter ce qui suit :</p>namespace BlazorApp.Models
{
    public class BookDoc
    {
        public string? Title { get; set; }
        public int? PageCount { get; set; }
        public string? PublishedDate { get; set; }
        public string? ThumbnailUrl { get; set; }
        public string? ShortDescription { get; set; }
        public LongDescription? LongDescription { get; set; }
        public string? Status { get; set; }
        public List&lt;string&gt;? Authors { get; set; }
        public List&lt;string&gt;? Categories { get; set; }
    }

    public class LongDescription
    {
        public string? Text { get; set; }
    }
}<p>Ensuite, il faut configurer la réponse Elastic dans le fichier <code>/Models/Response.cs</code> et ajouter ce qui suit :</p>namespace BlazorApp.Models
{
    public class ElasticResponse
    {
        public ElasticResponse()
        {
            Documents = new List&lt;BookDoc&gt;();
            Facets = new Dictionary&lt;string, Dictionary&lt;string, long&gt;&gt;();
        }

        public long TotalHits { get; set; }
        public List&lt;BookDoc&gt; Documents { get; set; }
        public Dictionary&lt;string, Dictionary&lt;string, long&gt;&gt; Facets { get; set; }
    }
}<h4>Configuration d'une interface utilisateur de base</h4><p>Ensuite, ajoutez le composant SearchBar. Dans le fichier <code>/Components/Elasticsearch/SearchBar.razor</code>, ajoutez ce qui suit :</p>@using System.Threading.Tasks

&lt;form @onsubmit="SubmitSearch"&gt;
  &lt;div class="input-group mb-3"&gt;
    &lt;input type="text" @bind-value="searchTerm" class="form-control" placeholder="Enter search term..." /&gt;
    &lt;button type="submit" class="btn btn-primary input-btn"&gt;
      &lt;span class="input-group-svg"&gt;
        Search
      &lt;/span&gt;
    &lt;/button&gt;
  &lt;/div&gt;
&lt;/form&gt;

@code {
  [Parameter]
  public EventCallback&lt;string&gt; OnSearch { get; set; }

  private string searchTerm = "";

  private async Task SubmitSearch()
  {
    await OnSearch.InvokeAsync(searchTerm);
  }
}<p>Ce composant contient une barre de recherche et un bouton pour effectuer la recherche.</p><p>Blazor offre une grande flexibilité en permettant de générer du HTML dynamiquement en utilisant du code C# dans le même fichier.</p><p>Ensuite, dans le fichier <code>/Components/Elasticsearch/Results.razor</code>, nous construirons le composant de résultats qui affichera les résultats de la recherche :</p>@using BlazorApp.Models

@if (SearchResults != null &amp;&amp; SearchResults.Any())
{
  &lt;div class="row"&gt;
  @foreach (var result in SearchResults)
    {
      &lt;div class="col-12 mb-3"&gt;
        &lt;div class="card"&gt;
          &lt;div class="row g-0"&gt;
            &lt;div class="col-md-3 image-container"&gt;
              @if (!string.IsNullOrEmpty(result?.ThumbnailUrl))
              {
                &lt;img src="@result?.ThumbnailUrl" class="img-fluid rounded-start" alt="Thumbnail"&gt;
              }
              else
              {
                &lt;div class="placeholder"&gt;
                  @result?.Title
                &lt;/div&gt;
              }
            &lt;/div&gt;

            &lt;div class="col-md-9"&gt; &lt;!-- Adjusted to use the remaining 75% --&gt;
              &lt;div class="card-body"&gt;
                &lt;h4 class="card-title"&gt;
                  @result?.Title
                &lt;/h4&gt;

                &lt;div class="details-container"&gt;
                  &lt;div class=""&gt;

                    @if (result?.Authors?.Any() == true)
                    {
                      &lt;p class="card-text p-first"&gt;
                        Authors: &lt;small class="text-muted"&gt;@string.Join(", ", result.Authors)&lt;/small&gt;
                      &lt;/p&gt;
                    }

                    @if (result?.Categories?.Any() == true)
                    {
                      &lt;p class="card-text p-second"&gt;
                        Categories: &lt;small class="text-muted"&gt;@string.Join(", ", result.Categories)&lt;/small&gt;
                      &lt;/p&gt;
                    }
                  &lt;/div&gt;
                  &lt;div class="numPages-status"&gt;
                    @if (result?.PageCount != null)
                    {
                      &lt;p class="card-text p-first"&gt;
                        Pages: &lt;small class="text-muted"&gt;@result.PageCount&lt;/small&gt;
                      &lt;/p&gt;
                    }

                    @if (result?.Status != null)
                    {
                      &lt;p class="card-text p-second"&gt;
                        Status: &lt;small class="text-muted"&gt;@result.Status&lt;/small&gt;
                      &lt;/p&gt;
                    }
                  &lt;/div&gt;
                &lt;/div&gt;

                &lt;div class="long-text-container"&gt;
                  &lt;p class="card-text"&gt;&lt;small class="text-muted"&gt;@result?.LongDescription?.Text&lt;/small&gt;&lt;/p&gt;
                &lt;/div&gt;
                @if (!string.IsNullOrEmpty(result?.PublishedDate))
                {
                  &lt;div class="date-container"&gt;
                    &lt;p class="card-text"&gt;
                      Published Date: &lt;small class="text-muted small-date"&gt;@FormatDate(result.PublishedDate)&lt;/small&gt;
                    &lt;/p&gt;
                  &lt;/div&gt;
                }
              &lt;/div&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    }
  &lt;/div&gt;
}
else if (SearchResults != null)
{
  &lt;p&gt;No results found.&lt;/p&gt;
}

@code {
  [Parameter]
  public List&lt;BookDoc&gt; SearchResults { get; set; } = new List&lt;BookDoc&gt;();

  private string FormatDate(string? date)
  {
    if (DateTime.TryParse(date, out DateTime parsedDate))
    {
      return parsedDate.ToString("MMMM dd, yyyy");
    }
    return "";
  }
}<p>Enfin, nous devons créer des facettes pour filtrer les résultats de la recherche.</p><p><em>Remarque : les facettes sont des filtres qui permettent aux utilisateurs d'affiner les résultats de leur recherche en fonction d'attributs ou de catégories spécifiques, tels que le type de produit, la fourchette de prix ou la marque. Ces filtres sont généralement présentés sous forme d'options cliquables, souvent sous forme de cases à cocher, afin d'aider les utilisateurs à affiner leur recherche et à trouver plus facilement des résultats pertinents. Dans le contexte d'Elasticsearch, les facettes sont créées à l'aide d'</em> <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html"><em>agrégations</em></a><em>.</em></p><p>Nous configurons les facettes en plaçant le code suivant dans le fichier <code>/Components/Elasticsearch/Facet.razor</code>:</p>@if (Facets != null)
{
  &lt;div class="facets-container"&gt;
  @foreach (var facet in Facets)
    {
      &lt;h3&gt;@facet.Key&lt;/h3&gt;
      @foreach (var option in facet.Value)
      {
        &lt;div&gt;
          &lt;input type="checkbox" checked="@IsFacetSelected(facet.Key, option.Key)"
            @onclick="() =&gt; ToggleFacet(facet.Key, option.Key)" /&gt;
          @option.Key (@option.Value)
        &lt;/div&gt;
      }
    }
  &lt;/div&gt;
}


@code {
  [Parameter]
  public Dictionary&lt;string, Dictionary&lt;string, long&gt;&gt;? Facets { get; set; }

  [Parameter]
  public EventCallback&lt;Dictionary&lt;string, List&lt;string&gt;&gt;&gt; OnFacetChanged { get; set; }

  private Dictionary&lt;string, List&lt;string&gt;&gt; selectedFacets = new();

  private void ToggleFacet(string facetName, string facetValue)
  {
    if (!selectedFacets.TryGetValue(facetName, out var facetValues))
    {
      facetValues = selectedFacets[facetName] = new List&lt;string&gt;();
    }

    if (!facetValues.Remove(facetValue))
    {
      facetValues.Add(facetValue);
    }

    OnFacetChanged.InvokeAsync(selectedFacets);
  }

  private bool IsFacetSelected(string facetName, string facetValue)
  {
    return selectedFacets.ContainsKey(facetName) &amp;&amp; selectedFacets[facetName].Contains(facetValue);
  }
}<p>Ce composant lit une agrégation <code>terms</code> sur les champs <code>author</code>, <code>categories</code> et <code>status</code>, puis produit une liste de filtres à renvoyer à Elasticsearch.</p><p>Mettons tout cela bout à bout.</p><p>Dans le fichier <code>/Components/Pages/Search.razor</code>:</p>@page "/"
@rendermode InteractiveServer
@using BlazorApp.Models
@using BlazorApp.Services
@inject ElasticsearchService ElasticsearchService
@inject ILogger&lt;Search&gt; Logger

&lt;PageTitle&gt;Search&lt;/PageTitle&gt;

&lt;div class="top-row px-4 "&gt;

    &lt;div class="searchbar-container"&gt;
        &lt;h4&gt;Semantic Search with Elasticsearch and Blazor&lt;/h4&gt;

        &lt;SearchBar OnSearch="PerformSearch" /&gt;
    &lt;/div&gt;

    &lt;a href="https://www.elastic.co/search-labs/esre-with-blazor" target="_blank"&gt;About&lt;/a&gt;
&lt;/div&gt;

&lt;div class="px-4"&gt;

    &lt;div class="search-details-container"&gt;
        &lt;p role="status"&gt;Current search term: @currentSearchTerm&lt;/p&gt;
        &lt;p role="status"&gt;Total results: @totalResults&lt;/p&gt;
    &lt;/div&gt;

    &lt;div class="results-facet-container"&gt;
        &lt;div class="facets-container"&gt;
            &lt;Facet Facets="facets" OnFacetChanged="OnFacetChanged" /&gt;
        &lt;/div&gt;
        &lt;div class="results-container"&gt;
            &lt;Results SearchResults="searchResults" /&gt;
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;

@code {
    private string currentSearchTerm = "";
    private long totalResults = 0;
    private List&lt;BookDoc&gt; searchResults = new List&lt;BookDoc&gt;();
    private Dictionary&lt;string, Dictionary&lt;string, long&gt;&gt; facets = new Dictionary&lt;string, Dictionary&lt;string, long&gt;&gt;();
    private Dictionary&lt;string, List&lt;string&gt;&gt; selectedFacets = new Dictionary&lt;string, List&lt;string&gt;&gt;();

    protected override async Task OnInitializedAsync()
    {
        await PerformSearch();
    }

    private async Task PerformSearch(string searchTerm = "")
    {
        try
        {
            currentSearchTerm = searchTerm;

            var response = await ElasticsearchService.SearchBooksAsync(currentSearchTerm, selectedFacets);
            if (response != null)
            {
                searchResults = response.Documents;
                facets = response.Facets;
                totalResults = response.TotalHits;
            }
            else
            {
                Logger.LogWarning("Search response is null.");
            }

            StateHasChanged();
        }
        catch (Exception ex)
        {
            Logger.LogError(ex, "Error performing search.");
        }
    }

    private async Task OnFacetChanged(Dictionary&lt;string, List&lt;string&gt;&gt; newSelectedFacets)
    {
        selectedFacets = newSelectedFacets;
        await PerformSearch(currentSearchTerm);
    }
}<p>Notre page fonctionne !</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc750e55fff43de4b/6a17f6636864a4ab08b68931/5f0f25cb029a34df7f577a9f307278226ed07c3f-816x473.png" alt="Exemple de page Blazor" /><p>Comme vous pouvez le constater, la page est fonctionnelle mais manque de style. Ajoutons quelques feuilles de style CSS pour le rendre plus organisé et plus réactif.</p><p>Commençons par remplacer les styles de présentation. Dans le fichier <code>Components/Layout/MainLayout.razor.css</code>:</p>.page {
  position: relative;
  display: flex;
  flex-direction: column;
}

main {
  flex: 1;
}

#blazor-error-ui {
  background: lightyellow;
  bottom: 0;
  box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
  display: none;
  left: 0;
  padding: 0.6rem 1.25rem 0.7rem 1.25rem;
  position: fixed;
  width: 100%;
  z-index: 1000;
}

#blazor-error-ui .dismiss {
  cursor: pointer;
  position: absolute;
  right: 0.75rem;
  top: 0.5rem;
}<p>Ajoutez les styles pour la page de recherche dans le fichier <code>Components/Pages/Search.razor.css</code>:</p>.input-group .input-group-svg {
  background: transparent;
  border: transparent;
  pointer-events: none;
}

.results-facet-container {
  display: flex;
  margin-top: 1rem;
  overflow-x: auto;
}

.search-details-container {
  display: flex;
  justify-content: space-between;
  margin-top: 1rem;
}

.searchbar-container {
  padding-top: 2rem;
  display: flex;
  flex-direction: column; 
  flex-grow: 1;
  height: 100%;
  max-width: 100%; 
}

.searchbar-container h4 {
  margin: 0;
}

.top-row {
  margin-top: -1.1rem;
  position: relative; 
  background-color: hsl(216, 29%, 67%);
  border-bottom: 1px solid #d6d5d5;
  display: flex;
  align-items: center;
  height: 100%;
  padding: 0 1rem;
}

.top-row a {
  margin-left: auto;
  margin-top: -4rem; 
  color: #000000;
  text-decoration: none;
}

.top-row a:hover {
  text-decoration: underline;
}

@media (max-width: 640.98px) {
  .top-row {
    justify-content: space-between;
  }

  .top-row ::deep a,
  .top-row ::deep .btn-link {
    margin-left: 0;
  }
}

@media (min-width: 641px) {
  .top-row.auth ::deep a:first-child {
    flex: 1;
    text-align: right;
    width: 0;
  }

  .top-row,
  article {
    padding-left: 2rem !important;
    padding-right: 1.5rem !important;
  }
}<p>Notre page commence à s'améliorer :</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt3ecc49f404f18591/6a17f6654b055d959b432384/3dadd7ade2dd9070300a4e0514a4da2ae9cc9fb9-817x473.png" alt="Page Blazor après l'ajout de styles pour la page de recherche" /><p>Mettons-y les dernières touches :</p><p>Créez les fichiers suivants :</p><ul><li><p>Composants/Elasticsearch/Facet.razor.css</p></li><li><p>Composants/Elasticsearch/Results.razor.css</p></li></ul><p>Et ajoutez les styles pour <code>Facet.razor.css</code>:</p>.facets-container {
  font-size: 15px;
  margin-right: 4rem;
  overflow-x: auto;
  white-space: nowrap;
  max-width: 300px;
}

.results-facet-container {
  display: flex;
  margin-top: 1rem;
  overflow-x: auto;
}

.results-facet-container &gt; * {
  flex-shrink: 0;
}<p>Pour <code>Results.razor.css</code>:</p>.image-container {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100%;
  padding: 1rem;
  box-sizing: border-box;
}

.image-container img {
  max-width: 100%;
  height: auto;
  border-radius: 0.5rem;
}

.placeholder {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100%;
  width: 100%;
  background-color: #f0f0f0;
  border: 1px solid #ccc;
  font-size: 0.9rem;
  color: #888;
  text-align: center;
  padding: 1rem;
  border-radius: 0.5rem;
}

.card-body {
  padding: 1rem;
}

.details-container {
  display: flex;
  justify-content: space-between;
  padding: 1.5rem 0;
}

.date-container {
  margin-top: 1rem;
  display: flex;
  justify-content: flex-end;
}

.date-container .small-date {
  font-weight: bold;
}<p>Résultat final :</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt78bbd8e6f9e23988/6a17f6664b055d2ed8432388/3e68ec4a38775fdfbeaa1a990e6a1e11dfb081d8-816x472.png" alt="Résultat final de la construction de la page de l'application blazor" /><p>Pour lancer l'application, vous pouvez utiliser la commande suivante :</p><p><code>dotnet watch</code></p><p>Vous avez réussi ! Vous pouvez désormais rechercher des livres dans votre index Elasticsearch en utilisant la barre de recherche et filtrer les résultats par auteur, catégorie et statut.</p><h3>Effectuer une recherche plein texte et sémantique</h3><p>Par défaut, notre application effectuera une <a href="https://www.elastic.co/search-labs/tutorials/search-tutorial/vector-search/hybrid-search">recherche hybride</a> utilisant à la fois le <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-multi-match-query.html">texte intégral</a> et la <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-semantic-query.html">recherche sémantique</a>. Vous pouvez modifier la logique de recherche en créant deux méthodes distinctes, l'une pour le texte intégral et l'autre pour la recherche sémantique, puis en sélectionnant une méthode pour élaborer la requête en fonction des données saisies par l'utilisateur.</p><p>Ajoutez les méthodes suivantes à la classe <code>ElasticsearchService</code> dans le fichier <code>/Services/ElasticsearchService.cs</code>:</p>private static Action&lt;QueryDescriptor&lt;BookDoc&gt;&gt; BuildSemanticQuery(
    string searchTerm,
    Dictionary&lt;string, List&lt;string&gt;&gt; selectedFacets
)
{
    var filters = BuildFilters(selectedFacets);

    return query =&gt;
        query.Bool(b =&gt;
            b.Must(m =&gt; m.Semantic(sem =&gt; sem.Field("longDescription").Query(searchTerm)))
                .Filter(filters.ToArray())
        );
}

private static Action&lt;QueryDescriptor&lt;BookDoc&gt;&gt; BuildMultiMatchQuery(
    string searchTerm,
    Dictionary&lt;string, List&lt;string&gt;&gt; selectedFacets
)
{
    var filters = BuildFilters(selectedFacets);

    if (string.IsNullOrEmpty(searchTerm))
    {
        return query =&gt; query.Bool(b =&gt; b.Filter(filters.ToArray()));
    }

    return query =&gt;
        query.Bool(b =&gt;
            b.Should(m =&gt;
                    m.MultiMatch(mm =&gt;
                        mm.Query(searchTerm).Fields(new[] { "title", "shortDescription" })
                    )
                )
                .Filter(filters.ToArray())
        );
}<p>Ces deux méthodes fonctionnent de manière similaire à la méthode <code>BuildHybridQuery</code>, mais elles n'effectuent qu'une recherche plein texte ou sémantique.</p><p>Vous pouvez modifier la méthode <code>SearchBooksAsync</code> pour utiliser la méthode de recherche sélectionnée :</p>public async Task&lt;ElasticResponse&gt; SearchBooksAsync(
    string searchTerm,
    Dictionary&lt;string, List&lt;string&gt;&gt; selectedFacets
)
{
    try
    {
        _logger.LogInformation($"Performing search for: {searchTerm}");
        
        // Modify the query builder to use the selected search method.
        var multiMatchQuery = BuildMultiMatchQuery(searchTerm, selectedFacets); // For full text search
        var semanticQuery = BuildSemanticQuery(searchTerm, selectedFacets); // For semantic search

        // In this case we will not use retrievers, but you can add them if you want to use them.
        var response = await _client.SearchAsync&lt;BookDoc&gt;(s =&gt;
            s.Index("elastic-blazor-books")
                .Query(multiMatchQuery) // Change this line to use different search methods, for example: .Query(semanticQuery) for semantic search
                .Aggregations(aggs =&gt;
                    aggs.Add("Authors", agg =&gt; agg.Terms(t =&gt; t.Field(p =&gt; p.Authors)))
                        .Add(
                            "Categories",
                            agg =&gt; agg.Terms(t =&gt; t.Field(p =&gt; p.Categories))
                        )
                        .Add("Status", agg =&gt; agg.Terms(t =&gt; t.Field(p =&gt; p.Status)))
                )
        );

        if (response.IsValidResponse)
        {
            _logger.LogInformation($"Found {response.Documents.Count} documents");

            var hits = response.Total;
            var facets =
                response.Aggregations != null
                    ? FormatFacets(response.Aggregations)
                    : new Dictionary&lt;string, Dictionary&lt;string, long&gt;&gt;();

            var elasticResponse = new ElasticResponse
            {
                TotalHits = hits,
                Documents = response.Documents.ToList(),
                Facets = facets,
            };

            return elasticResponse;
        }
        else
        {
            _logger.LogWarning($"Invalid response: {response.DebugInformation}");
            return new ElasticResponse();
        }
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "Error performing search");
        return new ElasticResponse();
    }
}<p>Vous pouvez trouver le formulaire complet <a href="https://github.com/elastic/elasticsearch-labs/tree/main/supporting-blog-content/esre-with-blazor">ici</a></p><h2>Conclusion</h2><p>Blazor est un framework efficace qui vous permet de construire des applications web en utilisant C#. Elasticsearch est un moteur de recherche puissant qui vous permet de créer des applications de recherche. En combinant les deux, vous pouvez facilement créer des applications de recherche robustes, en tirant parti de la puissance d'ESRE pour créer une <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/semantic-search.html">expérience de recherche sémantique</a> en peu de temps.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/search-app-with-esre-blazor</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/search-app-with-esre-blazor</guid>
    <category><![CDATA[Base vectorielle]]></category>
    <category><![CDATA[.NET]]></category>
    <dc:creator><![CDATA[Gustavo Llermaly]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blte7424ac5f0b223b4/6a17f668414c641971945323/7ba0d6bec908bfcae966b7f38626fabd682c6f3d-1200x628.png" length="0" type="image/png"/>
    <pubDate>Wed, 09 Oct 2024 00:00:00 GMT</pubDate>
  </item>
  </channel>
</rss>