<?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[JavaScript - 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[JavaScript - 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/blog/category/javascript-programming</link>
    </image>
    <link>https://www.elastic.co/fr/search-labs/blog/category/javascript-programming</link>
    <atom:link href="https://www.elastic.co/fr/search-labs/rss/category/javascript-programming.xml" rel="self" type="application/rss+xml"/>
    <language><![CDATA[fr]]></language>
    <lastBuildDate>Sun, 20 Sep 2026 22:45:10 GMT</lastBuildDate>
  <item>
    <title><![CDATA[Construire un assistant agentique RAG avec JavaScript, Mastra et Elasticsearch]]></title>
    <description><![CDATA[Apprendre à construire des agents d'intelligence artificielle dans l'écosystème JavaScript]]></description>
    <content:encoded><![CDATA[<p>Cette idée m'est venue alors que je me trouvais au beau milieu d'une ligue de basket-ball fantastique passionnante et aux enjeux considérables. Je me suis posé la question : <em>Pourrais-je construire un agent IA qui m'aiderait à dominer mes matchs hebdomadaires ? Absolument !</em></p><p>Dans ce billet, nous allons voir comment construire un assistant RAG agentique en utilisant <a href="https://mastra.ai/en/docs">Mastra</a> et une application web JavaScript légère pour interagir avec lui. En connectant cet agent à Elasticsearch, nous lui donnons accès aux données structurées des joueurs et la possibilité d'exécuter des agrégations statistiques en temps réel, afin de vous donner des recommandations fondées sur les statistiques des joueurs. Rendez-vous sur le <a href="https://github.com/jdarmada/nba-ai-assistant-js.git">repo</a> GitHub pour suivre le processus ; le <a href="https://github.com/jdarmada/nba-ai-assistant-js/blob/main/README.md">README</a> fournit des instructions sur la manière de cloner et d'exécuter l'application par vos propres moyens. </p><p>Voici à quoi il devrait ressembler une fois assemblé :</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt63ea3e7a09306fbf/6a17f1d97f6f150e22c09c50/1c73bd1dc1b5fe54f025c7a2b7c322acc9122f3a-1999x1393.png" alt="" /><p>Remarque : cet article de blog s'appuie sur "<a href="https://www.elastic.co/search-labs/blog/ai-agents-ai-sdk-elasticsearch">Building AI Agents with AI SDK and Elastic"</a> ( Créer des agents d'intelligence artificielle avec AI SDK et Elastic). Si vous ne connaissez pas encore les agents d'intelligence artificielle en général et leur utilité, commencez par là.
</p><h2><strong>Aperçu de l'architecture</strong></h2><p>Au cœur du système se trouve un grand modèle de langage (LLM), qui agit comme le moteur de raisonnement de l'agent (le cerveau). Il interprète les données de l'utilisateur, décide des outils à appeler et orchestre les étapes nécessaires pour générer une réponse pertinente.</p><p>L'agent lui-même est soutenu par Mastra, un cadre d'agent dans l'écosystème JavaScript. Mastra intègre le LLM à une infrastructure dorsale, l'expose en tant que point d'extrémité de l'API et fournit une interface pour définir les outils, les invites du système et le comportement de l'agent.</p><p>Sur le frontend, nous utilisons <a href="https://vite.dev/guide/">Vite</a> pour mettre en place rapidement une application web React qui fournit une interface de chat pour envoyer des requêtes à l'agent et recevoir ses réponses.</p><p>Enfin, nous avons Elasticsearch, qui stocke les statistiques des joueurs et les données de correspondance que l'agent peut interroger et agréger.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blte13f09493f217047/6a17f1db1d1b83178d93e546/443bdc00d84ed1dd49e9f9e431e86ca4b0892563-1999x977.png" alt="" /><h2><strong>Arrière-plan</strong></h2><p>Passons en revue quelques concepts fondamentaux :</p><h3><strong>Qu'est-ce que le RAG agentique ?</strong></h3><p>Les agents d'intelligence artificielle peuvent interagir avec d'autres systèmes, fonctionner de manière indépendante et effectuer des actions en fonction de paramètres définis. Le RAG agentique combine l'autonomie d'un agent d'intelligence artificielle avec les principes de la génération augmentée par récupération, ce qui permet à un LLM de choisir les outils à utiliser et les données à utiliser comme contexte pour générer une réponse. Pour en savoir plus sur le RAG <a href="https://www.elastic.co/search-labs/blog/retrieval-augmented-generation-rag">, cliquez ici.</a></p><h3><strong>Pourquoi aller plus loin que AI-SDK dans le choix d'un framework ?</strong></h3><p>Il existe de nombreuses structures d'agents d'IA et vous avez probablement entendu parler des plus populaires comme <a href="https://www.elastic.co/search-labs/blog/using-crewai-with-elasticsearch">CrewAI</a>, <a href="https://www.elastic.co/search-labs/blog/using-autogen-with-elasticsearch">AutoGen</a> et <a href="https://www.elastic.co/search-labs/blog/build-rag-workflow-langgraph-elasticsearch">LangGraph</a>. La plupart de ces cadres partagent un ensemble commun de fonctionnalités, notamment la prise en charge de différents modèles, l'utilisation d'outils et la gestion de la mémoire.</p><p>Voici une <a href="https://docs.google.com/spreadsheets/d/1B37VxTBuGLeTSPVWtz7UMsCdtXrqV5hCjWkbHN8tfAo/edit?gid=0#gid=0">fiche comparative de</a> Harrison Chase (PDG de LangChain).</p><p>Ce qui a suscité mon intérêt pour Mastra, c'est qu'il s'agit d'un framework JavaScript conçu pour les développeurs full-stack afin d'intégrer facilement des agents dans leur écosystème. L'AI-SDK de Vercel permet également de réaliser la plupart de ces tâches, mais c'est lorsque vos projets incluent des flux de travail d'agents plus complexes que Mastra brille. Mastra améliore les modèles de base définis par l'AI-SDK et, dans ce projet, nous les utiliserons en tandem.</p><h3><strong>Cadres et considérations sur le choix du modèle</strong></h3><p>Si ces frameworks peuvent vous aider à créer rapidement des agents d'intelligence artificielle, ils présentent néanmoins certains inconvénients. Par exemple, l'utilisation d'autres cadres en dehors des agents d'IA ou de toute couche d'abstraction en général vous fait perdre un peu de contrôle. Si le LLM n'utilise pas les outils correctement ou fait quelque chose que vous ne voulez pas qu'il fasse, l'abstraction rend le débogage plus difficile. Cependant, à mon avis, ce compromis vaut la facilité et la rapidité que vous obtenez lors de la construction, en particulier parce que ces cadres gagnent du terrain et font l'objet d'itérations constantes.</p><p>Encore une fois, ces cadres sont agnostiques, ce qui signifie que vous pouvez brancher et utiliser différents modèles. N'oubliez pas que les modèles varient en fonction des ensembles de données sur lesquels ils ont été formés et qu'à leur tour, ils varient en fonction des réponses qu'ils donnent. Certains modèles ne prennent même pas en charge l'appel d'outils. Il est donc possible de changer et de tester différents modèles pour voir lequel vous donne les meilleures réponses, mais gardez à l'esprit que vous devrez probablement réécrire l'invite du système pour chacun d'entre eux. Par exemple, en utilisant Llama3.3 par rapport au GPT-4o, implique beaucoup plus d'invites et d'instructions spécifiques pour obtenir la réponse souhaitée.</p><h3><strong>Basket-ball fantaisie NBA</strong></h3><p>Le basket-ball fantaisie consiste à créer une ligue avec un groupe d'amis (attention, selon le degré de compétition de votre groupe, cela peut affecter le statut de vos amitiés), généralement avec de l'argent en jeu. Chacun d'entre vous constitue ensuite une équipe de 10 joueurs pour affronter les 10 joueurs d'un autre ami, en alternance chaque semaine. Les points qui contribuent à votre score global sont les résultats obtenus par chacun de vos joueurs contre leurs adversaires au cours d'une semaine donnée.</p><p>Si un joueur de votre équipe se blesse, est suspendu, etc., il y a une liste d'agents libres disponibles pour compléter votre équipe. C'est là qu'intervient une grande partie de la réflexion dans les sports fantastiques, car vous ne disposez que d'un nombre limité de choix et tout le monde est constamment à la recherche du meilleur joueur.</p><p>C'est là que notre assistant NBA AI va briller, en particulier dans les situations où vous devez rapidement décider quel joueur choisir. Au lieu de devoir rechercher manuellement les performances d'un joueur contre un adversaire spécifique, l'assistant peut trouver ces données rapidement et comparer les moyennes pour vous donner une recommandation éclairée.</p><p>Maintenant que vous connaissez les bases du RAG agentique et du basket-ball fantastique NBA, voyons ce qu'il en est dans la pratique.</p><h2><strong>Construire le projet</strong></h2><p>Si vous êtes bloqué à un moment ou à un autre ou si vous ne voulez pas le construire à partir de zéro, veuillez vous référer au <a href="https://github.com/jdarmada/nba-ai-assistant-js.git">repo</a>.</p><h3><strong>Ce que nous allons couvrir</strong></h3><ol><li><p><strong>L'échafaudage du projet :</strong></p><ol><li><p><strong>Backend (Mastra) :</strong> Utilisez npx create mastra@latest pour échafauder le backend et définir la logique de l'agent.</p></li><li><p><strong>Frontend (Vite + React) :</strong> Utilisez npm create vite@latest pour construire l'interface de chat frontale pour interagir avec l'agent.</p></li></ol></li><li><p><strong>Mise en place de variables d'environnement</strong></p><ol><li><p>Installer dotenv pour gérer les variables d'environnement.</p></li><li><p>Créer un fichier .env et fournir les variables nécessaires.</p></li></ol></li><li><p><strong>Configuration d'Elasticsearch</strong></p><ol><li><p>Mettre en place un cluster Elasticsearch (localement ou sur le cloud).</p></li><li><p>Installer le client Elasticsearch officiel.</p></li><li><p>S'assurer que les variables d'environnement sont accessibles.</p></li><li><p>Établir la connexion avec le client.</p></li></ol></li><li><p><strong>Acquisition en masse de données NBA dans Elasticsearch</strong></p><ol><li><p>Créez un index avec les mappings appropriés pour permettre les agrégations.</p></li><li><p>Intégrez en masse les statistiques de jeu des joueurs à partir d'un fichier CSV dans un index Elasticsearch.</p></li></ol></li><li><p><strong>Définir les agrégations Elasticsearch</strong></p><ol><li><p>Requête pour calculer les moyennes historiques contre un adversaire spécifique.</p></li><li><p>Requête pour calculer les moyennes de la saison contre un adversaire spécifique.</p></li></ol></li><li><p><strong>Fichier utilitaire de comparaison des joueurs</strong></p><ol><li><p>Consolidation des fonctions d'aide et des agrégations Elasticsearch.</p></li></ol></li><li><p><strong>Construction de l'agent</strong></p><ol><li><p>Ajouter la définition de l'agent et l'invite du système.</p></li><li><p>Installer les outils zod et define.</p></li><li><p>Ajout d'une configuration intermédiaire pour gérer CORS.</p></li></ol></li><li><p><strong>Intégration de l'interface utilisateur</strong></p><ol><li><p>Utilisation de la fonction useChat de l'AI-SDK pour interagir avec l'agent.</p></li><li><p>Créer l'interface utilisateur pour tenir des conversations correctement formatées.</p></li></ol></li><li><p><strong>Exécution de l'application</strong></p><ol><li><p>Démarrez le backend (serveur Mastra) et le frontend (application React).</p></li><li><p>Exemples de requêtes et d'utilisation.</p></li></ol></li><li><p><strong>Et maintenant ? Rendre l'agent plus intelligent</strong></p><ol><li><p>Ajout de capacités de recherche sémantique pour permettre des recommandations plus pertinentes.</p></li><li><p>Activer l'interrogation dynamique en déplaçant la logique de recherche vers le serveur Elasticsearch MCP (Model Context Protocol).</p></li></ol></li></ol><h3><strong>Produits requis</strong></h3><ul><li><p><strong>Node.js et npm</strong>: Le backend et le frontend fonctionnent tous deux sur Node. Assurez-vous d'avoir installé Node 18+ et npm v9+ (qui est fourni avec Node 18+).</p></li><li><p><strong>Cluster Elasticsearch :</strong> Un cluster Elasticsearch actif, soit localement, soit sur le cloud.</p></li><li><p><strong>Clé API OpenAI</strong>: Générez-en une sur la page des clés API du <a href="https://platform.openai.com/api-keys">portail des développeurs d'OpenAI</a>.</p></li></ul><p></p><h3><strong>Structure du projet</strong></h3><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt749baa120552e4ab/6a17f1dd1d1b83bfe993e54a/1c0bde11ad0eead523a95e03b9b905aa776e3fd1-1420x934.png" alt="" /><h4><strong>Étape 1 : Échafaudage du projet</strong></h4><ol><li><p>Tout d'abord, créez le répertoire nba-ai-assistant-js et naviguez à l'intérieur en utilisant : </p></li></ol>mkdir nba-ai-assistant-js &amp;&amp; cd nba-ai-assistant-js<p><strong>Backend :</strong></p><ol><li><p>Utilisez l'outil de création Mastra avec la commande : </p></li></ol>npx create-mastra@latest<p>2. Vous devriez obtenir quelques invites dans votre terminal, pour la première, nous nommerons le projet backend :</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt65abf68fe588e968/6a17f1de63baff2814741d5b/de2725031ed6837db99a979efcdd0ece1e197dbb-608x84.png" alt="" /><p>3. Ensuite, nous conserverons la structure par défaut pour le stockage des fichiers Mastra, en saisissant <code>src/</code>.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt89bd829fcf0ae6b9/6a17f1e04b055dd30e432302/88919d9ff1852126395e1fcd700ecb1b59aac63c-866x116.png" alt="" /><p>4. Ensuite, nous choisirons OpenAI comme fournisseur LLM par défaut.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltfd167cc77a40b9a8/6a17f1e11480099e29b48863/2328761e769f3ded134e5a21e8a0bf8f41e88f68-404x210.png" alt="" /><p>5. Enfin, il vous demandera votre clé API OpenAI. Pour l'instant, nous choisirons d'ignorer l'option et nous la fournirons plus tard dans un fichier<code> .env</code>.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt12654151ed495370/6a17f1e22f4a5c0f84fa89f9/0662de9bd28758e377e4c63df8d08b479068ce63-444x120.png" alt="" /><p><strong>Frontend :</strong></p><ol><li><p>Naviguez à nouveau vers le répertoire racine et exécutez l'<a href="https://vite.dev/guide/">outil de création Vite</a> à l'aide de cette commande : <code>npm create vite@latest frontend -- --template react</code></p></li></ol><p>Cela devrait créer une application React légère nommée <code>frontend</code> avec un modèle spécifique pour React.</p><p>Si tout se passe bien, à l'intérieur de votre répertoire de projet, vous devriez trouver un répertoire backend qui contient le code Mastra et un répertoire <code>frontend</code> avec votre application React.</p><p></p><h4><strong>Étape 2 : Configuration des variables d'environnement</strong></h4><ol><li><p>Pour gérer les clés sensibles, nous utiliserons le paquetage <code>dotenv</code> pour charger nos variables d'environnement à partir du fichier .env. fichier. Naviguez vers le répertoire backend et installez <code>dotenv</code>:</p></li></ol>cd backend
npm install dotenv --save<p>2. Dans le répertoire du backend, un fichier example.env est fourni avec les variables appropriées à remplir. Si vous créez le vôtre, veillez à inclure les variables suivantes :</p># OpenAI Configuration
OPENAI_API_KEY=your_openai_api_key_here

# Elasticsearch Configuration
ELASTIC_ENDPOINT=your_elasticsearch_endpoint_here
ELASTIC_API_KEY=your_elasticsearch_api_key_here
<p></p><p>Note : Assurez-vous que ce fichier est exclu de votre contrôle de version en ajoutant <code>.env</code> à <code>.gitignore</code>.</p><h4><strong>Étape 3 : Configuration d'Elasticsearch</strong></h4><p>Tout d'abord, vous devez disposer d'un cluster Elasticsearch actif. Deux options sont possibles :</p><ul><li><p><strong>Option A : utiliser Elasticsearch Cloud</strong></p><ul><li><p>S'inscrire à <a href="https://cloud.elastic.co/registration">Elastic Cloud</a></p></li><li><p>Créer un nouveau déploiement</p></li><li><p>Obtenez l'URL de votre point de terminaison et la clé API (encodée)</p></li></ul></li><li><p><strong>Option B : Exécuter Elasticsearch localement</strong></p><ul><li><p>Installer et exécuter Elasticsearch localement</p></li><li><p>Utilisez http://localhost:9200 comme point d'arrivée</p></li><li><p>Générer une clé API</p></li></ul></li></ul><p></p><p><strong>Installation du client Elasticsearch sur le backend :</strong></p><ol><li><p>Tout d'abord, installez le client Elasticsearch officiel dans votre répertoire backend :</p></li></ol>npm install @elastic/elasticsearch<p>2. Créez ensuite un répertoire lib pour contenir les fonctions réutilisables et naviguez-y :</p>mkdir lib &amp;&amp; cd lib<p>3. À l'intérieur, créez un nouveau fichier appelé <a href="https://github.com/jdarmada/nba-ai-assistant-js/blob/main/backend/lib/elasticClient.js">elasticClient.js</a>. Ce fichier initialise le client Elasticsearch et l'expose pour qu'il soit utilisé dans votre projet.</p><p>4. Comme nous utilisons des modules ECMAScript (ESM), le nom de fichier __dirname and __n'est pas disponible. Pour vous assurer que vos variables d'environnement sont correctement chargées à partir du fichier .env dans le dossier backend, ajoutez cette configuration au début de votre fichier :</p>import { config } from 'dotenv';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
import { Client } from '@elastic/elasticsearch';

// Grab current directory and load .env from backend folder
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const envPath = join(__dirname, '../.env');

// Load environment variables from the correct path
config({ path: envPath });<p>5. Maintenant, initialisez le client Elasticsearch en utilisant vos variables d'environnement et vérifiez la connexion :</p>//Elastic client Initialization, make sure environment variables are being loaded in correctly
const config= {
    node: `${process.env.ELASTIC_ENDPOINT}`,
    auth: {
        apiKey: `${process.env.ELASTIC_API_KEY}`,
    },
};

export const elasticClient = new Client(config);

//Check if the client is connected
async function checkConnection() { 
    try {
        const info = await elasticClient.info();
        console.log('Elasticsearch is connected:', info);
    } catch (error) {
        console.error('Elasticsearch connection error:', error);
    }
}

checkConnection();
<p>Maintenant, nous pouvons importer cette instance client dans n'importe quel fichier qui doit interagir avec votre cluster Elasticsearch.</p><p></p><h4><strong>Étape 4 : Intégration en masse des données NBA dans Elasticsearch</strong></h4><p><strong>Ensemble de données :</strong></p><p>Pour ce projet, nous ferons référence aux ensembles de données disponibles dans le répertoire <a href="https://github.com/jdarmada/nba-ai-assistant-js/tree/main/backend">backend/data</a> de la base de données. Notre assistant NBA utilisera ces données comme base de connaissances pour effectuer des comparaisons statistiques et générer des recommandations.</p><ul><li><p><a href="https://github.com/jdarmada/nba-ai-assistant-js/blob/main/backend/data/sample_nba_data.csv">sample_player_game_stats.csv</a> - Exemple de statistiques de jeu d'un joueur (par exemple, points, rebonds, interceptions, etc.) par match et par joueur sur l'ensemble de sa carrière en NBA. Nous utiliserons cet ensemble de données pour effectuer des agrégations. (Remarque : il s'agit de données fictives, générées à des fins de démonstration et ne provenant pas de sources officielles de la NBA).</p></li><li><p><a href="https://github.com/jdarmada/nba-ai-assistant-js/blob/main/backend/data/playerAndTeamInfo.js">playerAndTeamInfo.js</a> - Remplace les métadonnées sur les joueurs et les équipes qui seraient normalement fournies par un appel à l'API afin que l'agent puisse faire correspondre les noms des joueurs et des équipes aux identifiants. Comme nous utilisons des données d'échantillon, nous ne voulons pas nous encombrer d'une API externe, c'est pourquoi nous avons codé en dur certaines valeurs auxquelles l'agent peut se référer.</p></li></ul><p></p><p><strong>Mise en œuvre :</strong></p><ol><li><p>Dans le répertoire <code>backend/lib</code>, créez un fichier nommé <a href="https://github.com/jdarmada/nba-ai-assistant-js/blob/main/backend/lib/playerDataIngestion.js">playerDataIngestion.js</a>.</p></li><li><p>Configurer les importations, résoudre le chemin du fichier CSV et configurer l'analyse. Là encore, puisque nous utilisons ESM, nous devons reconstruire <code>__dirname</code> pour résoudre le chemin d'accès à l'échantillon CSV. Nous importerons également le module <a href="http://node.js/">Node.js</a> les modules intégrés, <code>fs</code> et <code>readline</code>, pour analyser le fichier CSV donné ligne par ligne.</p></li></ol>import fs from 'fs';
import readline from 'readline';
import path from 'path';
import { fileURLToPath } from 'url';
import { elasticClient } from './elasticClient.js';

const indexName = 'sample-nba-player-data'; //Replace with your preferred index name

//Since we are using ES modules __dirname and __filename don't exist, so this is a workaround that allows us to use the absolute file path for our sample data.
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const filePath = path.resolve(__dirname, '../data/sample_nba_data.csv');<p>Cela vous permet de lire et d'analyser efficacement le fichier CSV lorsque nous passons à l'étape de l'ingestion en masse.</p><p>3. Créez un index avec la correspondance appropriée. Bien qu'Elasticsearch puisse déduire automatiquement les types de champs avec le <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/dynamic">mappage dynamique</a>, nous voulons être explicites ici pour que chaque statut soit traité comme un champ numérique. Ceci est important car nous utiliserons ces champs pour les agrégations par la suite. Nous voulons également utiliser le type <code>float </code>pour les statistiques telles que les points, les rebonds, etc., afin de nous assurer que nous incluons des valeurs décimales. Enfin, nous voulons ajouter la propriété de mappage <code>dynamic: 'strict'</code> afin qu'Elasticsearch ne mappe pas dynamiquement les champs non reconnus. 
</p>// Function to create an index with mappings
async function createIndex() {
    try {
        // Check if the index already exists
        const exists = await elasticClient.indices.exists({ index: indexName });

        if (exists) {
            console.log(`Index "${indexName}" already exists, deleting it now.`);
            await elasticClient.indices.delete({ index: indexName });
            console.log(`Deleted index "${indexName}".`);
        }
        // Create the index with mappings
        const response = await elasticClient.indices.create({
            index: indexName,
            body: {
                mappings: {
                    dynamic: 'strict', // Prevent dynamic mapping
                    properties: {
                        game_id: { type: 'integer' },
                        game_date: { type: 'date' },
                        player_id: { type: 'integer' },
                        player_full_name: { type: 'text' },
                        player_team_id: { type: 'integer' },
                        player_team_name: { type: 'text' },
                        home_team: { type: 'boolean' },
                        opponent_team_id: { type: 'integer' },
                        opponent_team_name: { type: 'text' },
                        points: { type: 'float' },
                        rebounds: { type: 'float' },
                        assists: { type: 'float' },
                        steals: { type: 'float' },
                        blocks: { type: 'float' },
                        fg_percentage: { type: 'float' },
                        minutes_played: { type: 'float' },
                    },
                },
            },
        });

        console.log('Index created:', response);
        return true;
    } catch (error) {
        console.error('Error creating index:', error);
        return false;
    }
}
<p>4. Ajoutez la fonction permettant d'intégrer en masse les données CSV dans votre index Elasticsearch. À l'intérieur du bloc de code, nous sautons la ligne d'en-tête. Ensuite, divisez chaque ligne par une virgule et insérez-les dans l'objet document. Cette étape permet également de les nettoyer et de s'assurer qu'ils sont du bon type. Ensuite, nous plaçons les documents dans le tableau bulkBody avec les informations d'index, qui serviront de charge utile pour l'ingestion en masse dans Elasticsearch.</p>async function bulkIngestCsv(filePath) {
    const readStream = fs.createReadStream(filePath);
    const rl = readline.createInterface({
        input: readStream,
        crlfDelay: Infinity,
    });

    const bulkBody = [];
    let lineNum = 0;

    //Skip the header line
    let headerLine = true;
    for await (const line of rl) {
        if (headerLine) {
            headerLine = false;
            continue;
        }
        lineNum++;

        // Split the line by comma and remove whitespace
        const [
            game_id,
            game_date,
            player_id,
            player_full_name,
            player_team_id,
            player_team_name,
            home_team,
            opponent_team_id,
            opponent_team_name,
            points,
            rebounds,
            assists,
            steals,
            blocks,
            fg_percentage,
            minutes_played,
        ] = line.split(',');

        // Create a document object
        const document = {
            game_id: parseInt(game_id),
            game_date: game_date.trim(),
            player_id: parseInt(player_id),
            player_full_name: player_full_name.trim(),
            player_team_id: parseInt(player_team_id),
            player_team_name: player_team_name.trim(),
            home_team: home_team.trim() === 'True', // Converts True/False into a boolean
            opponent_team_id: parseInt(opponent_team_id),
            opponent_team_name: opponent_team_name.trim(),
            points: parseFloat(points),
            rebounds: parseFloat(rebounds),
            assists: parseFloat(assists),
            steals: parseFloat(steals),
            blocks: parseFloat(blocks),
            fg_percentage: parseFloat(fg_percentage),
            minutes_played: parseFloat(minutes_played),
        };

        // Prepare the bulk operation format
        bulkBody.push({ index: { _index: indexName } });
        bulkBody.push(document);
    }

    console.log(`Parsed ${lineNum} lines from CSV`);
<p>5. Ensuite, nous pouvons utiliser l'<a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-bulk">API Bulk</a> d'Elasticsearch avec <code>elasticClient.bulk()</code> pour ingérer plusieurs documents en une seule demande. La gestion des erreurs ci-dessous est structurée de manière à vous indiquer le nombre de documents qui n'ont pas été ingérés et le nombre de documents qui ont été ingérés avec succès.</p>try {
        // Perform the bulk request
        const response = await elasticClient.bulk({ body: bulkBody });

        if (response.errors) {
            console.log('Bulk Ingestion had some hiccups:');

            // Count successful vs failed operations
            let successCount = 0;
            let errorCount = 0;
            const errorDetails = [];

            response.items.forEach((item, index) =&gt; {
                const operation = item.index || item.create || item.update || item.delete;
                if (operation.error) {
                    errorCount++;
                    errorDetails.push({
                        document: index + 1,
                        error: operation.error,
                    });
                } else {
                    successCount++;
                }
            });

            console.log(`Successfully indexed: ${successCount} documents`);
            console.log(`Failed to index: ${errorCount} documents, here are the details`, errorDetails);

        } else {
            console.log(`Bulk Ingestion fully successful!`);
        }

    } catch (error) {
        console.error('Error performing bulk ingestion:', error);
    }
}
<p>6. Exécutez la fonction <code>main()</code> ci-dessous pour exécuter séquentiellement les fonctions <code>createIndex()</code> et <code>bulkIngestCsv()</code>.</p>// Run this function
async function main() {
    const result = await createIndex();
    if (!result) {
        console.error('Index setup failed. Aborting.');
        return;
    }

    await bulkIngestCsv(filePath);
    console.log('Bulk ingestion completed!');
}

main();
<p>Si vous voyez un journal de console indiquant que l'ingestion en masse a réussi, effectuez une vérification rapide de votre index Elasticsearch pour voir si les documents ont effectivement été ingérés avec succès.</p><h4><strong>Étape 5 : Définition des agrégations Elasticsearch et consolidation</strong></h4><p>Ce sont les principales fonctions qui seront utilisées lorsque nous définirons les outils de l'agent IA afin de comparer les statistiques des joueurs entre eux.</p><p>1. Naviguez jusqu'au répertoire <code>backend/lib</code> et créez un fichier appelé <a href="https://github.com/jdarmada/nba-ai-assistant-js/blob/main/backend/lib/elasticAggs.js">elasticAggs.js</a>.</p><p>2. Ajoutez la requête ci-dessous pour calculer les moyennes historiques d'un joueur contre un adversaire spécifique. Cette requête utilise un <a href="https://www.elastic.co/search-labs/tutorials/search-tutorial/full-text-search/filters">filtre</a> <code>bool</code> avec 2 conditions : l'une correspondant à <code>player_id</code> et l'autre à <code>opponent_team_id</code>, afin de récupérer uniquement les jeux pertinents. Nous n'avons pas besoin de renvoyer de documents, nous ne nous intéressons qu'aux agrégations, c'est pourquoi nous définissons <code>size:0</code>. Sous le bloc <code>aggs</code>, nous exécutons plusieurs <a href="https://www.elastic.co/docs/explore-analyze/query-filter/aggregations">agrégations</a> métriques en parallèle sur des champs tels que <code>points, rebounds, assists, steals, blocks</code> et <code>fg_percentage</code> pour calculer leurs valeurs moyennes. Les LLM peuvent être aléatoires dans leurs calculs et ce processus est déchargé sur Elasticsearch, ce qui garantit à notre assistant NBA AI l'accès à des données exactes.</p>export async function getHistoricalAveragesAgainstOpponent(player_id, opponent_team_id) {
    try {
        //Query for Historical Averages
        const historicalQuery = await elasticClient.search({
            index: 'sample-nba-player-data', 
            size: 0,
            query: {
                bool: {
                    must: [
                        {
                            term: {
                                player_id: {
                                    value: player_id,
                                },
                            },
                        },
                        {
                            term: {
                                opponent_team_id: {
                                    value: opponent_team_id,
                                },
                            },
                        },
                    ],
                },
            },
            aggs: {
                avg_points: { avg: { field: 'points' } },
                avg_rebounds: { avg: { field: 'rebounds' } },
                avg_assists: { avg: { field: 'assists' } },
                avg_steals: { avg: { field: 'steals' } },
                avg_blocks: { avg: { field: 'blocks' } },
             avg_fg_percentage: { avg: { field: 'fg_percentage' } },
            },
        });

        return {
            points: historicalQuery.aggregations.avg_points.value || 0,
            rebounds: historicalQuery.aggregations.avg_rebounds.value || 0,
            assists: historicalQuery.aggregations.avg_assists.value || 0,
            steals: historicalQuery.aggregations.avg_steals.value || 0,
            blocks: historicalQuery.aggregations.avg_blocks.value || 0,
            fgPercentage: historicalQuery.aggregations.avg_fg_percentage.value || 0,
        };
    } catch (error) {
        console.error('Query error from getHistoricalAveragesAgainstOpponent function:', error);
        return { error: 'Queries failed in getting historical averages against opponent.' };
    }
}
<p>3. Pour calculer les moyennes saisonnières d'un joueur contre un adversaire spécifique, nous utiliserons pratiquement la même requête que la requête historique. La seule différence dans cette requête est que le filtre <code>bool</code> est assorti d'une condition supplémentaire pour <code>game_date</code>. Le champ <code>game_date</code> doit se situer dans la fourchette de la saison NBA en cours. Dans ce cas, la fourchette est comprise entre <code>2024-10-01</code> et <code>2025-06-30</code>. Cette condition supplémentaire ci-dessous garantit que les agrégations qui suivent n'isoleront que les matchs de cette saison.
</p>        {
                            range: {
                    //Range for this season, change to match current season
                                game_date: {
                                    gte: '2024-10-01',
                                    lte: '2025-06-30',
                                },
                            },
<h4><strong>Étape 6 : Utilitaire de comparaison des joueurs</strong></h4><p>Pour que notre code reste modulaire et facile à maintenir, nous allons créer un fichier utilitaire qui consolide les fonctions d'aide aux métadonnées et les agrégations Elasticsearch. Il s'agit de l'outil principal utilisé par l'agent. Nous y reviendrons plus tard :</p><p>1. Créez un nouveau fichier <a href="https://github.com/jdarmada/nba-ai-assistant-js/blob/main/backend/lib/comparePlayers.js">comparePlayers.js</a> dans le répertoire <code>backend/lib</code>.</p><p>2. Ajoutez la fonction ci-dessous pour consolider les aides aux métadonnées et la logique d'agrégation Elasticsearch en une seule fonction qui alimente l'outil principal utilisé par l'agent.
</p>import { playersByName } from '../data/playerAndTeamInfo.js';
import { teamsByName } from '../data/playerAndTeamInfo.js';
import { upcomingMatchups } from '../data/playerAndTeamInfo.js';
import { getHistoricalAveragesAgainstOpponent } from './elasticAggs.js';
import { getSeasonAveragesAgainstOpponent } from './elasticAggs.js';

//Simple helper functions to simulate API calls for player and team metadata. These reference the hardcoded values from playerAndTeamInfo.js in the data directory
export function getPlayerInfo(playerFullName) {
    return playersByName[playerFullName];
}

export function getTeamID(teamFullName) {
    return teamsByName[teamFullName];
}

export function getUpcomingMatchups(teamId) {
    return upcomingMatchups[teamId];
}

//Main function used by the 'playerComparisonTool' agent tool
export async function comparePlayersForNextMatchup(player1Name, player2Name) {
    //Get Player Info
    const player1Info = getPlayerInfo(player1Name);
    const player2Info = getPlayerInfo(player2Name);

    //Get upcoming matchups
    const player1NextGame = getUpcomingMatchups(player1Info.team_id)[0];
    const player2NextGame = getUpcomingMatchups(player2Info.team_id)[0];

    //Get season and historical averages against next opponent for player 1
    const player1SeasonAverages = await getSeasonAveragesAgainstOpponent(
        player1Info.player_id,
        player1NextGame.opponent_team_id
    );
    const player1HistoricalAverages = await getHistoricalAveragesAgainstOpponent(
        player1Info.player_id,
        player1NextGame.opponent_team_id
    );

    //Get season and historical averages against next opponent for player 2
    const player2SeasonAverages = await getSeasonAveragesAgainstOpponent(
        player2Info.player_id,
        player2NextGame.opponent_team_id
    );
    const player2HistoricalAverages = await getHistoricalAveragesAgainstOpponent(
        player2Info.player_id,
        player2NextGame.opponent_team_id
    );

    const player1 = {
        name: player1Name,
        playerId: player1Info.player_id,
        teamId: player1Info.team_id,
        nextOpponent: {
            teamId: player1NextGame.opponent_team_id,
            teamName: player1NextGame.opponent_team_name,
            home: player1NextGame.home,
        },
        stats: {
            seasonAverages: player1SeasonAverages,
            historicalAverages: player1HistoricalAverages,
        },
    };

    const player2 = {
        name: player2Name,
        playerId: player2Info.player_id,
        teamId: player2Info.team_id,
        nextOpponent: {
            teamId: player2NextGame.opponent_team_id,
            teamName: player2NextGame.opponent_team_name,
            home: player2NextGame.home,
        },
        stats: {
            seasonAverages: player2SeasonAverages,
            historicalAverages: player2HistoricalAverages,
        },
    };

    return [player1, player2];
}
<h4><strong>Étape 7 : Création de l'agent</strong></h4><p>Maintenant que vous avez créé les échafaudages frontend et backend, ingéré les données du jeu NBA et établi une connexion à Elasticsearch, nous pouvons commencer à assembler toutes les pièces pour construire l'agent.</p><p><strong>Définition de l'agent</strong></p><p>1. Accédez au fichier <a href="https://github.com/jdarmada/nba-ai-assistant-js/blob/main/backend/src/mastra/agents/index.ts">index.ts</a> dans le répertoire <code>backend/src/mastra/agents</code> et ajoutez la définition de l'agent. Vous pouvez spécifier des champs tels que :</p><ul><li><p><strong>Nom :</strong> Donnez à votre agent un nom qui sera utilisé comme référence lorsqu'il sera appelé sur le frontend.</p></li><li><p><strong>Instructions/Instructions du système : </strong>Une invite système donne au MLD le contexte initial et les règles à suivre pendant l'interaction. Il s'agit d'une invite similaire à celle que les utilisateurs envoient par l'intermédiaire de la boîte de dialogue, mais celle-ci est donnée avant toute entrée de l'utilisateur. Là encore, cela varie en fonction du modèle que vous choisissez.</p></li><li><p><strong>Modèle :</strong> Quel LLM utiliser (Mastra soutient OpenAI, Anthropic, les modèles locaux, etc.)</p></li><li><p><strong>Outils :</strong> Une liste de fonctions d'outils que l'agent peut appeler.</p></li><li><p><strong>Mémoire :</strong> (Facultatif) si nous voulons que l'agent se souvienne de l'historique des conversations, etc. Pour des raisons de simplicité, nous pouvons commencer sans mémoire persistante, bien que Mastra la prenne en charge.</p></li></ul><p></p>import { openai } from '@ai-sdk/openai';
import { Agent } from '@mastra/core/agent';
import { playerComparisonTool } from '../tools';

export const basketballAgent = new Agent({
    name: 'Basketball Agent',
    instructions: `
      You are a NBA Basketball expert.
      Your primary function is to compare two NBA players and recommend which one is the better fantasy pickup.

      Only compare players from the following list:
      - LeBron James
      - Stephen Curry
      - Jayson Tatum
      - Jaylen Brown
      - Nikola Jokic
      - Luka Doncic
      - Kyrie Irving
      - Anthony Davis
      - Kawhi Leonard
      - Russell Westbrook

      Input Handling Rules:
      - If the user asks about a player that is not on this list, respond with the list of available players for comparison.
      - If the user only inputs one player, ask the user to add another player from the list provided.
      - If the user inputs a player with the wrong spelling or capitalizations, infer from the list of available players provided.
      - IMPORTANT: If the user asks a question or asks you to generate a response about anything outside of basketball or the scope of this project, DO NOT answer and affirm you can only talk about basketball.

      Tool Usage:
      - Extract and standardize player names to match the list exactly.
      - Use the playerComparisonTool, passing both names as strings.
      - The tool will return an object with game information, stats, and analysis.

      Format your response using Markdown syntax. Use:

        Example output format:

       
        #### Next Game Info
        - ***LeBron James** vs Warriors, May 24 (Home)  
        - ***Stephen Curry** vs Lakers, May 24 (Away)


        #### Stats Comparison  
        \`\`\`  
        Stat                  LeBron James (vs Warriors)    Stephen Curry (vs Lakers)  
        --------------------  -----------------------------  ----------------------------  
        Historical Points     28.3                          30.3  
        Historical Assists    6.7                           8.7  
        Season Points         28.8                          23.3  
        Season Assists        6.2                           4.7  
        \`\`\`

        #### Fantasy Recommendation  
        Explain which player is the better fantasy pickup and why.
      
    `,
    model: openai('gpt-4o'),
    tools: { playerComparisonTool },
});
<p><strong>
Définition des outils</strong></p><ol><li><p>Naviguez jusqu'au fichier <a href="https://github.com/jdarmada/nba-ai-assistant-js/blob/main/backend/src/mastra/tools/index.ts">index.ts</a> dans le répertoire <code>backend/src/mastra/tools</code>.</p></li><li><p>Installez Zod à l'aide de la commande :</p></li></ol>npm install zod<p>3. Ajouter des définitions d'outils. Notez que nous importons la fonction dans le fichier <code>comparePlayers.js</code> en tant que fonction principale que l'agent utilisera lorsqu'il appellera cet outil. En utilisant la fonction <code>createTool()</code> de Mastra, nous enregistrerons notre <code>playerComparisonTool</code>. Les domaines concernés sont les suivants :</p><ul><li><p><code>id</code>: Il s'agit d'une description en langage naturel qui aide l'agent à comprendre ce que fait l'outil.</p></li><li><p><code>input schema</code>: Pour définir la forme de l'entrée de l'outil, Mastra utilise le schéma <a href="https://zod.dev/">Zod</a>, qui est une bibliothèque de validation de schéma TypeScript. Zod s'assure que l'agent saisit des données correctement structurées et empêche l'outil de s'exécuter si la structure de l'entrée ne correspond pas.</p></li><li><p><code>description</code>: Il s'agit d'une description en langage naturel qui aide l'agent à comprendre quand il doit appeler et utiliser l'outil.</p></li><li><p><code>execute</code>: La logique qui s'exécute lorsque l'outil est appelé. Dans notre cas, nous utilisons une fonction d'aide importée pour renvoyer des statistiques de performance.</p></li></ul>import { comparePlayersForNextMatchup } from '../../../lib/comparePlayers.js'
import { createTool } from "@mastra/core/tools";
import { z } from "zod";

export const playerComparisonTool = createTool({
    id: "Compare two NBA players",
    inputSchema: z.object({
        player1:z.string(),
        player2:z.string()
    }),
    description: "Use this tool to compare two players given in the user prompt.",
    execute: async ({ context: { player1, player2 } }) =&gt; {
        return await comparePlayersForNextMatchup(player1, player2);
      },
})<p><strong>Ajout d'un logiciel intermédiaire pour gérer CORS</strong></p><p>Ajouter un middleware dans le serveur Mastra pour gérer <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS">CORS</a>. On dit qu'il y a trois choses dans la vie qu'on ne peut pas éviter : la mort, les impôts, et pour les développeurs web, c'est CORS. En bref, le partage des ressources inter-origines est une fonction de sécurité du navigateur qui empêche le front-end d'envoyer des requêtes à un back-end fonctionnant sur un domaine ou un port différent. Même si nous exécutons le backend et le frontend sur localhost, ils utilisent des ports différents, ce qui déclenche la politique CORS. Nous devons ajouter l'intergiciel spécifié dans la <a href="https://mastra.ai/en/docs/server-db/middleware">documentation de Mastra</a> afin que notre backend autorise ces requêtes depuis le frontend.</p><p>1. Naviguez jusqu'au fichier <a href="https://github.com/jdarmada/nba-ai-assistant-js/blob/main/backend/src/mastra/index.ts">index.ts</a> dans le répertoire <code>backend/src/mastra</code> et ajoutez la configuration pour CORS :</p><ul><li><p><code>origin: ['http://localhost:5173']</code></p><ul><li><p>Autorise les demandes provenant uniquement de cette adresse (adresse par défaut de Vite)</p></li></ul></li><li><p><code>allowMethods: ["GET", "POST"]</code></p><ul><li><p>Méthodes HTTP autorisées. La plupart du temps, il utilisera POST.</p></li></ul></li><li><p><code>allowHeaders: ["Content-Type", "Authorization", "x-mastra-client-type, "x-highlight-request", "traceparent"],</code></p><ul><li><p>Ils déterminent quels en-têtes personnalisés peuvent être utilisés dans les requêtes</p></li></ul></li></ul><p></p>import { Mastra } from '@mastra/core/mastra';
import { basketballAgent } from './agents';

console.log('Starting Mastra server...');

export const mastra = new Mastra({
  agents: { basketballAgent },
  server:{
    timeout: 10 * 60 * 1000, // 10 minutes
    cors: {
      origin: ['http://localhost:5173'],
      allowMethods: ["GET", "POST"],
      allowHeaders: [
        "Content-Type",
        "Authorization",
        "x-mastra-client-type",
        "x-highlight-request",
        "traceparent",
      ],
      exposeHeaders: ["Content-Length", "X-Requested-With"],
      credentials: false,
    },
  },

});

console.log('Mastra server configured.'); // Log after server configuration
<h4><strong>Étape 8 : Intégration de l'interface utilisateur</strong></h4><p>Ce composant React fournit une interface de chat simple qui se connecte à l'agent IA Mastra en utilisant le hook <a href="https://mastra.ai/en/docs/frameworks/agentic-uis/ai-sdk#using-the-usechat-hook">useChat()</a> de <code>@ai-sdk/react</code>. Nous allons également utiliser ce crochet pour afficher l'utilisation des jetons, les appels d'outils et pour rendre la conversation. Dans l'invite système ci-dessus, nous demandons également à l'agent de produire la réponse en format markdown, nous utiliserons donc <code>react-markdown</code> pour formater correctement la réponse.</p><p></p><p>1. Dans le répertoire frontend, installez le paquetage @ai-sdk/react pour utiliser le hook useChat().</p>npm install @ai-sdk/react<p>2. Dans le même répertoire, installez React Markdown pour que nous puissions formater correctement la réponse générée par l'agent.</p>npm install react-markdown<p>3. Mettre en œuvre <code>useChat()</code>. Ce hook va gérer l'interaction entre votre frontend et votre agent IA backend. Il gère l'état des messages, les entrées de l'utilisateur, l'état et vous donne des crochets de cycle de vie à des fins d'observabilité. Les options que nous transmettons sont les suivantes :</p><ul><li><p><code>api:</code> Ceci définit le point final de votre agent Mastra AI. Le port par défaut est le port 4111 et nous voulons également ajouter la route qui prend en charge les réponses en continu.</p></li><li><p><code>onToolCall</code>: Cette fonction s'exécute chaque fois que l'agent appelle un outil ; nous l'utilisons pour savoir quels outils notre agent appelle.</p></li><li><p><code>onFinish</code>: Cette opération s'exécute après que l'agent a fourni une réponse complète. Même si nous avons activé le streaming, <code>onFinish</code> sera toujours exécuté après la réception du message complet et non après chaque morceau. Ici, nous l'utilisons pour suivre l'utilisation de nos jetons. Cela peut s'avérer utile pour contrôler et optimiser les coûts de la gestion du cycle d'apprentissage tout au long de la vie.</p></li></ul><p>4. Enfin, nous nous rendons au composant <a href="https://github.com/jdarmada/nba-ai-assistant-js/blob/main/frontend/components/ChatUI.jsx">ChatUI.jsx</a> dans le répertoire <code>frontend/components</code> pour créer l'interface utilisateur de notre conversation. Ensuite, la réponse est enveloppée dans un composant <code>ReactMarkdown</code> afin de formater correctement la réponse de l'agent.</p>import React, { useState } from 'react';
import { useChat } from '@ai-sdk/react';
import ReactMarkdown from 'react-markdown';

export default function ChatUI() {
    const [totalTokenUsage, setTotalTokenUsage] = useState(0);
    const [promptTokenUsage, setPromptTokenUsage] = useState(0);
    const [completionTokenUsage, setCompletionTokenUsage] = useState(0);
    const [toolsCalled, setToolsCalled] = useState([]);

    const { messages, input, handleInputChange, handleSubmit, status } = useChat({
        api: 'http://localhost:4111/api/agents/basketballAgent/stream', //Replace with your own endpoint for your agent
        id: 'my-chat-session',

        //Optional parameter to check agent tool calls
        onToolCall: ({ toolCall }) =&gt; {
            setToolsCalled((prev) =&gt; [...prev, toolCall.toolName]);
        },

        //Optional parameter to check token usages
        onFinish: (message, { usage }) =&gt; {
            setTotalTokenUsage((prev) =&gt; prev + usage.totalTokens);
            setPromptTokenUsage((prev) =&gt; prev + usage.promptTokens);
            setCompletionTokenUsage((prev) =&gt; prev + usage.completionTokens);
        },

        //Optional parameter for error handling
        onError: (error) =&gt; {
            console.error('Agent error:', error);
        },
    });

    return (
        &lt;div&gt;
            &lt;div className="agent-info"&gt;
                &lt;h4 className="stats-title"&gt;What's My Agent Doing?&lt;/h4&gt;

                &lt;div className="stats-box"&gt;
                    &lt;strong className="stats-sub-title"&gt;Tools Called:&lt;/strong&gt;
                    &lt;ul className="tool-list"&gt;
                        {toolsCalled.map((tool, idx) =&gt; (
                            &lt;li key={idx}&gt;{tool}&lt;/li&gt;
                        ))}
                        {toolsCalled.length === 0 &amp;&amp; &lt;li&gt;No tools called yet.&lt;/li&gt;}
                    &lt;/ul&gt;

                    &lt;div className="usage-stats"&gt;
                        &lt;p&gt;Prompt Token Usage: {promptTokenUsage}&lt;/p&gt;
                        &lt;p&gt;Completion Token Usage: {completionTokenUsage}&lt;/p&gt;
                        &lt;p&gt;Total Token Usage: {totalTokenUsage}&lt;/p&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
            &lt;/div&gt;

            &lt;strong&gt;Conversation:&lt;/strong&gt;
            &lt;div className="convo-box"&gt;
                {messages.map((msg) =&gt; (
                    &lt;div key={msg.id} className="message-item"&gt;
                        &lt;strong className="message-role"&gt;{msg.role === 'assistant' ? 'Basketbot' : 'You'}:&lt;/strong&gt;
                        &lt;ReactMarkdown&gt;{msg.content}&lt;/ReactMarkdown&gt;
                    &lt;/div&gt;
                ))}
            &lt;/div&gt;

            &lt;form onSubmit={handleSubmit}&gt;
                &lt;input
                    type="text"
                    value={input}
                    onChange={handleInputChange}
                    placeholder="Input two players you want to compare."
                    className="input-box"
                /&gt;
                &lt;button type="submit" disabled={status === 'streaming'}&gt;
                    {status === 'streaming' ? 'Thinking...' : 'Send'}
                &lt;/button&gt;
            &lt;/form&gt;
        &lt;/div&gt;
    );
}<h4><strong>Étape 9 : Exécution de l'application</strong></h4><p>Félicitations ! Vous êtes maintenant prêt à exécuter l'application. Suivez ces étapes pour démarrer le backend et le frontend.</p><ol><li><p>Dans une fenêtre de terminal, à partir du répertoire racine, naviguez jusqu'au répertoire backend et démarrez le serveur Mastra :</p></li></ol>cd backend

npm run dev<p>2. Dans une autre fenêtre de terminal, à partir du répertoire racine, naviguez jusqu'au répertoire frontend et démarrez l'application React :</p><p></p>cd frontend

npm run dev<p></p><p>3. Allez dans votre navigateur et naviguez jusqu'à :</p><p></p><p><a href="http://localhost:5173/">http://localhost:5173</a></p><p></p><p>Vous devriez voir l'interface de chat. Essayez les exemples suivants :</p><ul><li><p>"Comparer LeBron James et Stephen Curry"</p></li><li><p>"Qui choisir entre Jayson Tatum et Luka Doncic ?"</p></li></ul><p></p><h3><strong>Et maintenant ? Rendre l'agent plus intelligent</strong></h3><p>Pour rendre l'assistant plus agentive et les recommandations plus perspicaces, j'ajouterai quelques améliorations clés dans la prochaine itération.</p><p></p><p><strong>Recherche sémantique pour les nouvelles de la NBA</strong></p><p>Il y a une tonne de facteurs qui peuvent affecter les performances des joueurs, dont beaucoup n'apparaissent pas dans les statistiques brutes. Des choses comme les rapports sur les blessures, les changements de composition, ou même une analyse d'après-match, vous ne pouvez les trouver que dans des articles de presse. Pour saisir ce contexte supplémentaire, j'ajouterai des capacités de recherche sémantique afin que l'agent puisse retrouver des articles pertinents de la NBA et tenir compte de ce récit dans ses recommandations.</p><p></p><p><strong>Recherche dynamique avec le serveur Elasticsearch MCP</strong></p><p>Le protocole MCP (Model Context Protocol) devient rapidement la norme pour la connexion des agents aux sources de données. Je vais migrer la logique de recherche dans le serveur Elasticsearch MCP, qui permet à l'agent de construire dynamiquement des requêtes plutôt que de s'appuyer sur les fonctions de recherche prédéfinies que nous fournissons. Cela nous permet d'utiliser davantage de flux de travail en langage naturel et de réduire la nécessité de rédiger manuellement chaque requête de recherche. Pour en savoir plus sur le serveur Elasticsearch MCP et l'état actuel de l'écosystème <a href="https://www.elastic.co/search-labs/blog/mcp-current-state">, cliquez ici.</a></p><p></p><p>Ces changements sont déjà en cours, restez à l'écoute !</p><h3><strong>Conclusion</strong></h3><p></p><p>Dans ce blog, nous avons construit un assistant RAG agentique qui fournit des recommandations personnalisées pour votre équipe de basket-ball fantasy en utilisant JavaScript, Mastra et Elasticsearch. Nous avons couvert :</p><ul><li><p><strong>Les principes fondamentaux de la RAG agentique</strong> et la manière dont la combinaison de l'autonomie d'un agent d'intelligence artificielle avec les outils permettant d'utiliser efficacement la RAG peut déboucher sur des agents plus nuancés et plus dynamiques.</p></li><li><p><strong>Elasticsearch </strong>et comment ses capacités de stockage de données et ses puissantes agrégations natives en font un partenaire idéal en tant que base de connaissances pour un LLM.</p></li><li><p><strong>Le </strong>cadre Mastra et la manière dont il simplifie la construction de ces agents pour les développeurs de l'écosystème JavaScript.</p></li></ul><p>Que vous soyez fanatique de basket-ball, que vous cherchiez à construire des agents d'intelligence artificielle, ou les deux comme moi, j'espère que ce blog vous a donné quelques éléments de base pour commencer. Le repo complet est disponible sur <a href="https://github.com/jdarmada/nba-ai-assistant-js">GitHub</a>, n'hésitez pas à le cloner et à le modifier. Maintenant, allez gagner cette ligue de fantasy !</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/agentic-rag</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/agentic-rag</guid>
    <category><![CDATA[IA]]></category>
    <category><![CDATA[IA agentique]]></category>
    <category><![CDATA[JavaScript]]></category>
    <dc:creator><![CDATA[JD Armada]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt8ffd561836a4cb20/6a17f1e47b54f978588b39e4/8132ed781c1ea5d46ca244182f421ed5c721f23b-1200x628.png" length="0" type="image/png"/>
    <pubDate>Tue, 01 Jul 2025 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Elasticsearch en JavaScript dans les règles de l'art, partie II]]></title>
    <description><![CDATA[Découvrez les bonnes pratiques en production et comment exécuter le client Elasticsearch Node.js dans des environnements serverless pour réduire les erreurs de codage. ]]></description>
    <content:encoded><![CDATA[<p>Voici la deuxième partie de notre série Elasticsearch en JavaScript. Dans la<a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i"> première partie,</a> nous avons appris à mettre en place notre environnement correctement, à configurer le client Node.js, à indexer les données et à effectuer des recherches. Dans cette deuxième partie, nous allons apprendre à mettre en œuvre les meilleures pratiques de production et à exécuter le client Elasticsearch <a href="http://node.js">Node.js</a> dans des environnements Serverless.</p><p>Nous ferons le point :</p><ul><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-ii#production-best-practices">Meilleures pratiques de production</a></p><ul><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-ii#error-handling">Gestion des erreurs</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-ii#testing">Tests</a></p></li></ul></li><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-ii#serverless-environments">Environnements sans serveur</a></p><ul><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-ii#running-the-client-on-elastic-serverless">Exécuter le client sur Elastic Serverless</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-ii#running-the-client-on-function-as-a-service-environment">Exécution du client dans un environnement de services fonctionnels (function-as-a-service)</a></p></li></ul></li></ul><p><em>Vous pouvez consulter le code source avec les exemples </em><a href="https://github.com/Delacrobix/JS-client-best-practices_article"><em><strong>ici</strong></em></a><em><strong>.</strong></em></p><h2>Meilleures pratiques de production</h2><h3>Gestion des erreurs dans Elasticsearch</h3><p>Une caractéristique utile du client Elasticsearch dans Node.js est qu'il expose des objets pour les erreurs possibles dans Elasticsearch afin que vous puissiez les valider et les traiter de différentes manières.</p><p>Pour <a href="https://www.elastic.co/docs/reference/elasticsearch/clients/javascript/connecting#client-error-handling">les voir tous</a>, cliquez ici : </p>const { errors } = require('@elastic/elasticsearch')
console.log(errors)<p>Revenons à l'exemple de la recherche et traitons certaines des erreurs possibles :</p>app.get("/search/lexic", async (req, res) =&gt; {
 ....
  } catch (error) {
    if (error instanceof errors.ResponseError) {
      let errorMessage =
        "Response error!, query malformed or server down, contact the administrator!";

      if (error.body.error.type === "parsing_exception") {
        errorMessage = "Query malformed, make sure mappings are set correctly";
      }

      res.status(error.meta.statusCode).json({
        erroStatus: error.meta.statusCode,
        success: false,
        results: null,
        error: errorMessage,
      });
    }

    res.status(500).json({
      success: false,
      results: null,
      error: error.message,
    });
  }
});<p><code>ResponseError</code> en particulier, se produit lorsque la réponse est <code>4xx</code> ou <code>5xx</code>, ce qui signifie que la demande est incorrecte ou que le serveur n'est pas disponible.</p><p>Nous pouvons tester ce type d'erreur en générant des requêtes erronées, par exemple en essayant d'<strong>effectuer une recherche de terme sur un champ de type texte :</strong></p><p>Erreur par défaut :</p> {
    "success": false,
    "results": null,
    "error": "parsing_exception\n\tRoot causes:\n\t\tparsing_exception: [terms] query does not support [visit_details]"
}<p>Erreur personnalisée : </p>{
    "erroStatus": 400,
    "success": false,
    "results": null,
    "error": "Response error!, query malformed or server down; contact the administrator!"
}<p>Nous pouvons également capturer et traiter chaque type d'erreur d'une certaine manière. Par exemple, nous pouvons ajouter une logique de réessai dans un site <code>TimeoutError</code>.</p>app.get("/search/semantic", async (req, res) =&gt; {
    try {
  ...
  } catch (error) {
    if (error instanceof errors.TimeoutError) {


     // Retry logic...

      res.status(error.meta.statusCode).json({
        erroStatus: error.meta.statusCode,
        success: false,
        results: null,
        error:
          "The request took more than 10s after 3 retries. Try again later.",
      });
    }
  }
});<h3>Tests</h3><p>Les tests sont essentiels pour garantir la stabilité de l'application. Pour tester le code d'une manière isolée d'Elasticsearch, nous pouvons utiliser la bibliothèque <a href="https://github.com/elastic/elasticsearch-js-mock">elasticsearch-js-mock</a> lors de la création de notre cluster.</p><p>Cette bibliothèque nous permet d'instancier un client qui est très similaire au vrai client mais qui répondra à notre configuration en remplaçant seulement la couche HTTP du client par une couche fictive tout en gardant le reste identique à l'original.</p><p>Nous installerons la bibliothèque mocks et <a href="https://github.com/avajs/ava">AVA</a> pour les tests automatisés.</p><p><code>npm install @elastic/elasticsearch-mock</code></p><p><code>npm install --save-dev ava</code></p><p>Nous allons configurer le fichier <code>package.json</code> pour exécuter les tests. Veillez à ce qu'il en soit ainsi :</p>"type": "module",
	"scripts": {
		"test": "ava"
	},
	"devDependencies": {
		"ava": "^5.0.0"
	}<p>Créons maintenant un fichier <code>test.js</code> et installons notre client fictif :</p>const { Client } = require('@elastic/elasticsearch')
const Mock = require('@elastic/elasticsearch-mock')

const mock = new Mock()
const client = new Client({
  node: 'http://localhost:9200',
  Connection: mock.getConnection()
})<p>Maintenant, ajoutez un simulacre de recherche sémantique :</p>function createSemanticSearchMock(query, indexName) {
  mock.add(
    {
      method: "POST",
      path: `/${indexName}/_search`,
      body: {
        query: {
          semantic: {
            field: "semantic_field",
            query: query,
          },
        },
      },
    },
    () =&gt; {
      return {
        hits: {
          total: { value: 2, relation: "eq" },
          hits: [
            {
              _id: "1",
              _score: 0.9,
              _source: {
                owner_name: "Alice Johnson",
                pet_name: "Buddy",
                species: "Dog",
                breed: "Golden Retriever",
                vaccination_history: ["Rabies", "Parvovirus", "Distemper"],
                visit_details:
                  "Annual check-up and nail trimming. Healthy and active.",
              },
            },
            {
              _id: "2",
              _score: 0.7,
              _source: {
                owner_name: "Daniel Kim",
                pet_name: "Mochi",
                species: "Rabbit",
                breed: "Mixed",
                vaccination_history: [],
                visit_details:
                  "Nail trimming and general health check. No issues.",
              },
            },
          ],
        },
      };
    }
  );
}<p>Nous pouvons maintenant créer un test pour notre code, en nous assurant que la partie Elasticsearch renvoie toujours les mêmes résultats :</p>import test from 'ava';

test("performSemanticSearch must return formatted results correctly", async (t) =&gt; {
  const indexName = "vet-visits";
  const query = "Which pets had nail trimming?";

  createSemanticSearchMock(query, indexName);

  async function performSemanticSearch(esClient, q, indexName = "vet-visits") {
    try {
      const result = await esClient.search({
        index: indexName,
        body: {
          query: {
            semantic: {
              field: "semantic_field",
              query: q,
            },
          },
        },
      });

      return {
        success: true,
        results: result.hits.hits,
      };
    } catch (error) {
      if (error instanceof errors.TimeoutError) {
        return {
          success: false,
          results: null,
          error: error.body.error.reason,
        };
      }

      return {
        success: false,
        results: null,
        error: error.message,
      };
    }
  }

  const result = await performSemanticSearch(esClient, query, indexName);

  t.true(result.success, "The search must be successful");
  t.true(Array.isArray(result.results), "The results must be an array");

  if (result.results.length &gt; 0) {
    t.true(
      "_source" in result.results[0],
      "Each result must have a _source property"
    );
    t.true(
      "pet_name" in result.results[0]._source,
      "Results must include the pet_name field"
    );
    t.true(
      "visit_details" in result.results[0]._source,
      "Results must include the visit_details field"
    );
  }
});<p>Exécutons les tests.</p><p><code>npm run test</code></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt36304e286146f362/6a170559d7c02237b2de638f/42feae845ae8eae03c37ad7ad114e8db35984812-1186x302.png" alt="" /><p>C'est fait ! Désormais, nous pouvons tester notre application en nous concentrant à 100 % sur le code et non sur des facteurs externes.</p><h2>Environnements sans serveur</h2><h3>Exécution du client avec Elastic Serverless</h3><p>Nous avons abordé l'exécution d'Elasticsearch sur le Cloud ou sur site ; cependant, le client Node.js prend également en charge les connexions à <a href="https://www.elastic.co/guide/en/serverless/current/intro.html">Elastic Cloud Serverless</a>.</p><p>Elastic Cloud Serverless vous permet de créer un projet dans lequel vous n'avez pas besoin de vous préoccuper de l'infrastructure puisqu'Elastic s'en charge en interne, et vous n'avez qu'à vous préoccuper des données que vous souhaitez indexer et de la durée pendant laquelle vous souhaitez y avoir accès.</p><p>Du point de vue de l'utilisation, Serverless découple le calcul du stockage, offrant des fonctionnalités d'autoscaling pour la <a href="https://www.elastic.co/search-labs/blog/elasticsearch-serverless-tier-autoscaling">recherche</a> et l'<a href="https://www.elastic.co/search-labs/blog/elasticsearch-ingest-autoscaling">indexation</a>. Cela vous permet de n'augmenter que les ressources dont vous avez réellement besoin.</p><p>Le client effectue les adaptations suivantes pour se connecter à Serverless :</p><ul><li><p>Désactive le sniffing et ignore toutes les options liées au sniffing.</p></li><li><p>Ignore tous les nœuds passés dans la configuration sauf le premier, et ignore toutes les options de filtrage et de sélection des nœuds.</p></li><li><p>Active la compression et `TLSv1_2_method` (identique à la configuration pour Elastic Cloud)</p></li><li><p>Ajoute un en-tête HTTP `elastic-api-version` à toutes les requêtes</p></li><li><p>Utilise `CloudConnectionPool` par défaut au lieu de `WeightedConnectionPool`.</p></li><li><p>Désactive les en-têtes `content-type` et `accept` en faveur des types MIME standard.</p></li></ul><p>Pour connecter votre projet serverless, vous devez utiliser le paramètre serverMode : serverless.</p>const { Client } = require('@elastic/elasticsearch')
const client = new Client({
  node: 'ELASTICSEARCH_ENDPOINT',
  auth: { apiKey: 'ELASTICSEARCH_API_KEY' },
  serverMode: "serverless",
});<h3>Exécution du client sur une plateforme FaaS (Function-as-a-Service)</h3><p>Dans l'exemple, nous avons utilisé un serveur Node.js, mais vous pouvez également vous connecter en utilisant un environnement de fonction en tant que service avec des fonctions telles que AWS lambda, GCP Run, etc.</p>'use strict'

const { Client } = require('@elastic/elasticsearch')

const client = new Client({
  // client initialisation
})

exports.handler = async function (event, context) {
  // use the client
}<p>Un autre exemple consiste à se connecter à des services comme Vercel, qui est également sans serveur. Vous pouvez consulter cet <a href="https://github.com/elastic/elasticsearch-js/blob/main/docs/examples/proxy/README.md">exemple complet</a> de la manière de procéder, mais la partie la plus pertinente du <a href="https://github.com/elastic/elasticsearch-js/blob/main/docs/examples/proxy/api/search.js">point de terminaison de la recherche</a> ressemble à ceci :</p>const response = await client.search(
  {
    index: INDEX,
    // You could directly send from the browser
    // the Elasticsearch's query DSL, but it will
    // expose you to the risk that a malicious user
    // could overload your cluster by crafting
    // expensive queries.
    query: {
      match: { field: req.body.text },
    },
  },
  {
    headers: {
      Authorization: `ApiKey ${token}`,
    },
  }
);<p>Ce point d'accès se trouve dans le dossier /api et est exécuté du côté du serveur, de sorte que le client ne contrôle que le paramètre "text" qui correspond au terme de la recherche.</p><p>L'utilisation de la fonction en tant que service implique que, contrairement à un serveur fonctionnant 24 heures sur 24 et 7 jours sur 7, les fonctions ne font appel qu'à la machine qui exécute la fonction et, une fois celle-ci terminée, la machine passe en mode repos afin de consommer moins de ressources.</p><p>Cette configuration peut être pratique si l'application ne reçoit pas trop de demandes ; dans le cas contraire, les coûts peuvent être élevés. Vous devez également tenir compte du <a href="https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtime-environment.html">cycle de vie des fonctions</a> et des durées d'exécution (qui ne peuvent être que de quelques secondes dans certains cas).</p><h2>Conclusion</h2><p>Dans cet article, nous avons appris à gérer les erreurs, ce qui est crucial dans les environnements de production. Nous avons également abordé le test de notre application en simulant le service Elasticsearch, ce qui permet d'obtenir des tests fiables quel que soit l'état du cluster et de se concentrer sur notre code.</p><p>Enfin, nous avons montré comment mettre en place une pile entièrement sans serveur en provisionnant à la fois Elastic Cloud Serverless et une application Vercel.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-ii</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-ii</guid>
    <category><![CDATA[JavaScript]]></category>
    <category><![CDATA[Les bases]]></category>
    <dc:creator><![CDATA[Jeffrey Rengifo]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc58be329ffebcd60/6a17043e47d49c0bc62d88ab/70fb0ff949f6db9ac9b8a28ecb4329ab915ebf46-720x420.png" length="0" type="image/png"/>
    <pubDate>Mon, 19 May 2025 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Elasticsearch en JavaScript dans les règles de l'art, partie I]]></title>
    <description><![CDATA[Expliquer comment créer un backend Elasticsearch prêt pour la production en JavaScript.  

Découvrez comment utiliser Elasticsearch avec JavaScript pour créer un serveur avec différents points de terminaison de recherche afin d’interroger les documents Elasticsearch en suivant les bonnes pratiques client/serveur.]]></description>
    <content:encoded><![CDATA[<p>Cet article est le premier d'une série qui traite de l'utilisation d'Elasticsearch avec JavaScript. Dans cette série, vous apprendrez les bases de l'utilisation d'Elasticsearch dans un environnement JavaScript et passerez en revue les fonctionnalités les plus pertinentes et les meilleures pratiques pour créer une application de recherche. À la fin, vous saurez tout ce dont vous avez besoin pour exécuter Elasticsearch à l'aide de JavaScript.</p><p>Dans cette première partie, nous passerons en revue</p><ul><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i#environment">Environnement</a></p><ul><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i#frontend,-backend,-or-serverless?">Frontend, backend ou serverless ?</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i#connecting-the-client">Connexion du client</a></p></li></ul></li><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i#indexing-documents">Indexation des documents</a></p><ul><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i#elasticsearch-client">Client Elasticsearch</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i#semantic-mappings">Correspondances sémantiques</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i#bulk-helper">Aide en vrac</a></p></li></ul></li><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i#searching-data">Recherche de données</a></p><ul><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i#lexical-query-(/search/lexic?q=%3Cquery-term%3E)">Requête lexicale</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i#semantic-query-(/search/semantic?q=%3Cquery-term%3E)">Requête sémantique</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i#hybrid-query-(/search/hybrid?q=%3Cquery-term%3E)">Requête hybride</a></p></li></ul></li></ul><p><em>Vous pouvez consulter le code source avec les exemples </em><a href="https://github.com/Delacrobix/JS-client-best-practices_article"><em><strong>ici</strong></em></a><em><strong>.</strong></em></p><h3>Qu'est-ce que le client Elasticsearch Node.js ?</h3><p>Le <a href="https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/index.html">client Elasticsearch Node.js</a> est une bibliothèque JavaScript qui transpose les appels HTTP REST de l'API Elasticsearch en JavaScript. Il est ainsi plus facile à manipuler et dispose d'assistants qui simplifient les tâches telles que l'indexation de documents par lots.</p><h2>Environnement</h2><h3>Frontend, backend ou serverless ?</h3><p>Pour créer notre application de recherche à l'aide du client JavaScript, nous avons besoin d'au moins deux composants : un cluster Elasticsearch et un moteur d'exécution JavaScript pour exécuter le client.</p><p>Le client JavaScript prend en charge toutes les solutions Elasticsearch (Cloud, on-prem et Serverless), et il n'y a pas de différences majeures entre elles puisque le client gère toutes les variations en interne, vous n'avez donc pas à vous soucier de savoir laquelle utiliser.</p><p>Le moteur d'exécution JavaScript doit toutefois être exécuté à partir du <strong>serveur</strong> et <strong>non directement à partir du navigateur.</strong></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltd3ec469c83e3a71a/6a17e3d5445de91da44d00b6/92ce6cfd923c8008fa44f617a58193642d9d5879-661x410.png" alt="Elasticsearch dans l’environnement JavaScript." /><p>En effet, en appelant Elasticsearch depuis le navigateur, l'utilisateur peut obtenir des informations sensibles telles que la clé API du cluster, l'hôte ou la requête elle-même. Elasticsearch recommande de <strong>ne jamais exposer le cluster directement à l'internet </strong>et d'utiliser une couche intermédiaire qui abstrait toutes ces informations de sorte que l'utilisateur ne puisse voir que les paramètres. Pour en savoir plus sur ce sujet <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/es-security-principles.html#security-protect-cluster-traffic">, cliquez ici.</a></p><p>Nous suggérons d'utiliser un schéma comme celui-ci :</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt4d7f215f2e70230a/6a17e3d6fbc5f83de6491a13/a08769f08ec73fe57bf2e961cfdfbb1cdd57919d-972x429.png" alt="Configuration du client Elasticsearch Node.js." /><p>Dans ce cas, le client n'envoie que les termes de recherche et une clé d'authentification pour votre serveur, tandis que votre serveur contrôle totalement la requête et la communication avec Elasticsearch.</p><h3>Connexion du client</h3><p>Commencez par créer une clé API en suivant <a href="https://www.elastic.co/search-labs/tutorials/install-elasticsearch/elastic-cloud">ces étapes.</a></p><p>En suivant l'exemple précédent, nous allons créer un simple serveur Express, et nous y connecter en utilisant un client depuis un serveur Node.JS.</p><p>Nous allons initialiser le projet avec NPM et installer le client Elasticsearch et <a href="https://expressjs.com/">Express.</a> Cette dernière est une bibliothèque qui permet d'activer des serveurs dans Node.js. En utilisant Express, nous pouvons interagir avec notre backend via HTTP.</p><p>Initialisons le projet :</p><p><code>npm init -y</code></p><p>Installer les dépendances :</p><p><code>npm install @elastic/elasticsearch express split2 dotenv</code></p><p>Laissez-moi vous expliquer :</p><ul><li><p><a href="https://www.npmjs.com/package/@elastic/elasticsearch"><em><strong>@elastic/elasticsearch</strong></em></a>: C'est le client officiel Node.js</p></li><li><p><a href="https://www.npmjs.com/package/express"><em><strong>express</strong></em></a>: Il nous permettra de faire tourner un serveur nodejs léger pour exposer Elasticsearch.</p></li><li><p><a href="https://www.npmjs.com/package/split2"><em><strong>split2</strong></em></a>: divise les lignes de texte en un flux. Utile pour traiter nos fichiers ndjson une ligne à la fois</p></li><li><p><a href="https://www.npmjs.com/package/dotenv"><em><strong>dotenv</strong></em></a>: Permet de gérer les variables d'environnement à l'aide d'un fichier .env fichier</p></li></ul><p>Créer un fichier .env à la racine du projet et ajoutez les lignes suivantes :</p>ELASTICSEARCH_ENDPOINT="Your Elasticsearch endpoint"
ELASTICSEARCH_API_KEY="Your Elasticssearch API"<p>Ainsi, nous pouvons importer ces variables à l'aide du paquetage <code>dotenv</code>.</p><p>Créer un fichier <code>server.js</code>:</p>const express = require("express");
const bodyParser = require("body-parser");
const { Client } = require("@elastic/elasticsearch");
 
require("dotenv").config(); //environment variables setup

const ELASTICSEARCH_ENDPOINT = process.env.ELASTICSEARCH_ENDPOINT;
const ELASTICSEARCH_API_KEY = process.env.ELASTICSEARCH_API_KEY;
const PORT = 3000;


const app = express();

app.listen(PORT, () =&gt; {
  console.log("Server running on port", PORT);
});
app.use(bodyParser.json());


let esClient = new Client({
  node: ELASTICSEARCH_ENDPOINT,
  auth: { apiKey: ELASTICSEARCH_API_KEY },  
});

app.get("/ping", async (req, res) =&gt; {
  try {
    const result = await esClient.info();

    res.status(200).json({
      success: true,
      clusterInfo: result,
    });
  } catch (error) {
    console.error("Error getting Elasticsearch info:", error);

    res.status(500).json({
      success: false,
      clusterInfo: null,
      error: error.message,
    });
  }
});<p>Ce code met en place un serveur Express.js de base qui écoute sur le port 3000 et se connecte à un cluster Elasticsearch en utilisant une clé API pour l'authentification. Il comprend un point d'extrémité /ping qui, lorsqu'on y accède par une requête GET, interroge le cluster Elasticsearch pour obtenir des informations de base à l'aide de la méthode <code>.info()</code> du client Elasticsearch. </p><p>Si la requête aboutit, elle renvoie les informations sur le cluster au format JSON ; dans le cas contraire, elle renvoie un message d'erreur. Le serveur utilise également un intergiciel d'analyseur de corps pour traiter les corps de requête JSON.</p><p>Exécutez le fichier pour lancer le serveur :</p><p><code>node server.js</code></p><p>La réponse devrait ressembler à ceci :</p>Server running on port 3000<p>Et maintenant, consultons le point de terminaison <code>/ping</code> pour vérifier l'état de notre cluster Elasticsearch.</p>curl http://localhost:3000/ping
{
    "success": true,
    "clusterInfo": {
        "name": "instance-0000000000",
        "cluster_name": "61b7e19eec204d59855f5e019acd2689",
        "cluster_uuid": "BIfvfLM0RJWRK_bDCY5ldg",
        "version": {
            "number": "9.0.0",
            "build_flavor": "default",
            "build_type": "docker",
            "build_hash": "112859b85d50de2a7e63f73c8fc70b99eea24291",
            "build_date": "2025-04-08T15:13:46.049795831Z",
            "build_snapshot": false,
            "lucene_version": "10.1.0",
            "minimum_wire_compatibility_version": "8.18.0",
            "minimum_index_compatibility_version": "8.0.0"
        },
        "tagline": "You Know, for Search"
    }
}<h2>Indexation des documents</h2><p>Une fois connectés, nous pouvons indexer les documents à l'aide de mappings tels que <a href="https://www.elastic.co/search-labs/blog/semantic-search-simplified-semantic-text">semantic_text</a> pour la recherche sémantique et text pour les requêtes en texte intégral. Avec ces deux types de champs, nous pouvons également effectuer une <a href="https://www.elastic.co/what-is/hybrid-search">recherche hybride</a>.</p><p>Nous allons créer un nouveau fichier <code>load.js</code> pour générer les correspondances et télécharger les documents.</p><h3>Client Elasticsearch</h3><p>Nous devons d'abord instancier et authentifier le client :</p>const { Client } = require("@elastic/elasticsearch");

const ELASTICSEARCH_ENDPOINT = "cluster/project_endpoint";
const ELASTICSEARCH_API_KEY = "apiKey";

const esClient = new Client({
  node: ELASTICSEARCH_ENDPOINT,
  auth: { apiKey: ELASTICSEARCH_API_KEY },
});<h3>Correspondances sémantiques</h3><p>Nous allons créer un index contenant des données sur un hôpital vétérinaire. Nous stockons les informations concernant le propriétaire, l'animal et les détails de la visite.</p><p>Les données sur lesquelles nous voulons effectuer une recherche en texte intégral, telles que les noms et les descriptions, seront stockées sous forme de texte. Les données des catégories, telles que l'espèce ou la race de l'animal, seront stockées sous forme de mots-clés.</p><p>En outre, nous copierons les valeurs de tous les champs dans un champ semantic_text afin de pouvoir effectuer une recherche sémantique sur ces informations également.</p>const INDEX_NAME = "vet-visits";

const createMappings = async (indexName, mapping) =&gt; {
  try {
    const body = await esClient.indices.create({
      index: indexName,
      body: {
        mappings: mapping,
      },
    });

    console.log("Index created successfully:", body);
  } catch (error) {
    console.error("Error creating mapping:", error);
  }
};

await createMappings(INDEX_NAME, {
  properties: {
    owner_name: {
      type: "text",
      copy_to: "semantic_field",
    },
    pet_name: {
      type: "text",
      copy_to: "semantic_field",
    },
    species: {
      type: "keyword",
      copy_to: "semantic_field",
    },
    breed: {
      type: "keyword",
      copy_to: "semantic_field",
    },
    vaccination_history: {
      type: "keyword",
      copy_to: "semantic_field",
    },
    visit_details: {
      type: "text",
      copy_to: "semantic_field",
    },
    semantic_field: {
      type: "semantic_text",
    },
  },
});<h3>Aide en vrac</h3><p>Un autre avantage du client est qu'il est possible d'utiliser l'<a href="https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/client-helpers.html#bulk-helper">assistant de masse</a> pour indexer par lots. L'assistant de masse nous permet de gérer facilement des choses comme la concurrence, les tentatives, et ce qu'il faut faire avec chaque document qui passe par la fonction et qui réussit ou échoue.</p><p>L'une des caractéristiques intéressantes de cette aide est qu'elle permet de travailler avec des flux. Cette fonction vous permet d'envoyer un fichier ligne par ligne au lieu de stocker le fichier complet dans la mémoire et de l'envoyer à Elasticsearch en une seule fois.</p><p>Pour télécharger les données vers Elasticsearch, créez un fichier appelé data.ndjson à la racine du projet et ajoutez les informations ci-dessous (vous pouvez également télécharger le fichier avec le jeu de données à partir d'<a href="https://github.com/Delacrobix/JS-client-best-practices_article/blob/main/data.ndjson">ici</a>) :</p>{"owner_name":"Alice Johnson","pet_name":"Buddy","species":"Dog","breed":"Golden Retriever","vaccination_history":["Rabies","Parvovirus","Distemper"],"visit_details":"Annual check-up and nail trimming. Healthy and active."}
{"owner_name":"Marco Rivera","pet_name":"Milo","species":"Cat","breed":"Siamese","vaccination_history":["Rabies","Feline Leukemia"],"visit_details":"Slight eye irritation, prescribed eye drops."}
{"owner_name":"Sandra Lee","pet_name":"Pickles","species":"Guinea Pig","breed":"Mixed","vaccination_history":[],"visit_details":"Loss of appetite, recommended dietary changes."}
{"owner_name":"Jake Thompson","pet_name":"Luna","species":"Dog","breed":"Labrador Mix","vaccination_history":["Rabies","Bordetella"],"visit_details":"Mild ear infection, cleaning and antibiotics given."}
{"owner_name":"Emily Chen","pet_name":"Ziggy","species":"Cat","breed":"Mixed","vaccination_history":["Rabies","Feline Calicivirus"],"visit_details":"Vaccination update and routine physical."}
{"owner_name":"Tomás Herrera","pet_name":"Rex","species":"Dog","breed":"German Shepherd","vaccination_history":["Rabies","Parvovirus","Leptospirosis"],"visit_details":"Follow-up for previous leg strain, improving well."}
{"owner_name":"Nina Park","pet_name":"Coco","species":"Ferret","breed":"Mixed","vaccination_history":["Rabies"],"visit_details":"Slight weight loss; advised new diet."}
{"owner_name":"Leo Martínez","pet_name":"Simba","species":"Cat","breed":"Maine Coon","vaccination_history":["Rabies","Feline Panleukopenia"],"visit_details":"Dental cleaning. Minor tartar buildup removed."}
{"owner_name":"Rachel Green","pet_name":"Rocky","species":"Dog","breed":"Bulldog Mix","vaccination_history":["Rabies","Parvovirus"],"visit_details":"Skin rash, antihistamines prescribed."}
{"owner_name":"Daniel Kim","pet_name":"Mochi","species":"Rabbit","breed":"Mixed","vaccination_history":[],"visit_details":"Nail trimming and general health check. No issues."}<p>Nous utilisons split2 pour streamer les lignes de fichiers pendant que l'assistant bulk les envoie à Elasticsearch.</p>const { createReadStream } = require("fs");
const split = require("split2");
 
const indexData = async (filePath, indexName) =&gt; {
  try {
    console.log(`Indexing data from ${filePath} into ${indexName}...`);

    const result = await esClient.helpers.bulk({
      datasource: createReadStream(filePath).pipe(split()),

      onDocument: () =&gt; {
        return {
          index: { _index: indexName },
        };
      },
      onDrop(doc) {
        console.error("Error processing document:", doc);
      },
    });

    console.log("Bulk indexing successful elements:", result.items.length);
  } catch (error) {
    console.error("Error indexing data:", error);
    throw error;
  }
};

await indexData("./data.ndjson", INDEX_NAME);<p>Le code ci-dessus lit un fichier .ndjson ligne par ligne et indexe en bloc chaque objet JSON dans un index Elasticsearch spécifié à l'aide de la méthode <code>helpers.bulk</code>. Il diffuse le fichier en utilisant <code>createReadStream</code> et <code>split2</code>, met en place des métadonnées d'indexation pour chaque document et enregistre tous les documents qui ne sont pas traités. Une fois l'opération terminée, il enregistre le nombre d'éléments indexés avec succès.</p><p>En lieu et place de la fonction <code>indexData</code>, vous pouvez télécharger le fichier directement via l'interface utilisateur à l'aide de Kibana et utiliser l'<a href="https://www.elastic.co/docs/manage-data/ingest/upload-data-files">interface utilisateur de téléchargement des fichiers de données.</a></p><p>Nous exécutons le fichier pour télécharger les documents vers notre cluster Elasticsearch.</p><p><code>node load.js</code></p>Creating mappings for index vet-visits...
Index created successfully: { acknowledged: true, shards_acknowledged: true, index: 'vet-visits' }
Indexing data from ./data.ndjson into vet-visits...
Bulk indexing completed. Total documents: 10, Failed: 0<h2>Recherche de données dans Elasticsearch</h2><p>En revenant à notre fichier <code>server.js</code>, nous allons créer différents points de terminaison pour effectuer une recherche lexicale, sémantique ou hybride.</p><p>En résumé, ces types de recherche ne s'excluent pas mutuellement, mais dépendent du type de question à laquelle vous devez répondre.</p><p>Type de requête</p><p>Cas d'utilisation</p><p>Exemple de question</p><p>Requête lexicale</p><p>Les mots ou racines de mots de la question sont susceptibles d'apparaître dans les documents de l'index. Similitude des jetons entre la question et les documents.</p><p>Je cherche un t-shirt de sport bleu.</p><p>Requête sémantique</p><p>Les mots de la question ne sont pas susceptibles de figurer dans les documents. Similitude conceptuelle entre la question et les documents.</p><p>Je cherche des vêtements pour le froid.</p><p>Recherche hybride</p><p>La question contient des éléments lexicaux et/ou sémantiques. Similitude toxique et sémantique entre les questions et les documents.</p><p>Je cherche une robe taille S pour un mariage sur la plage.</p><p>Les parties <em><strong>lexicales </strong></em>de la question sont susceptibles de faire partie de titres et de descriptions, ou de noms de catégories, tandis que les parties <em><strong>sémantiques </strong></em>sont des concepts liés à ces domaines. Le <em><strong>bleu</strong></em> sera probablement un nom de catégorie ou une partie de la description, et le <em><strong>mariage à la plage</strong></em> ne le sera probablement pas, mais il peut être sémantiquement lié aux vêtements en lin.</p><h3>Requête lexicale (/search/lexic?q=&lt;query_term&gt;)</h3><p>La recherche lexicale, également appelée recherche en texte intégral, consiste à effectuer une recherche basée sur la similarité des mots-clés, c'est-à-dire qu'après une analyse, les documents qui contiennent les mots-clés de la recherche seront renvoyés.</p><p>Vous pouvez consulter notre tutoriel pratique sur la recherche lexicale <a href="https://www.elastic.co/demo-gallery/lexical-search">ici.</a></p>app.get("/search/lexic", async (req, res) =&gt; {
  const { q } = req.query;

  const INDEX_NAME = "vet-visits";

  try {
    const result = await esClient.search({
      index: INDEX_NAME,
      size: 5,
      body: {
        query: {
          multi_match: {
            query: q,
            fields: ["owner_name", "pet_name", "visit_details"],
          },
        },
      },
    });

    res.status(200).json({
      success: true,
      results: result.hits.hits
    });
  } catch (error) {
    console.error("Error performing search:", error);

    res.status(500).json({
      success: false,
      results: null,
      error: error.message,
    });
  }
});<p>Nous testons avec : <em><strong>coupe-ongles</strong></em></p>curl http://localhost:3000/search/lexic?q=nail%20trimming<p>Réponse :</p>{
    "success": true,
    "results": [
        {
            "_index": "vet-visits",
            "_id": "-RY6RJYBLe2GoFQ6-9n9",
            "_score": 2.7075968,
            "_source": {
                "pet_name": "Mochi",
                "owner_name": "Daniel Kim",
                "species": "Rabbit",
                "visit_details": "Nail trimming and general health check. No issues.",
                "breed": "Mixed",
                "vaccination_history": []
            }
        },
        {
            "_index": "vet-visits",
            "_id": "8BY6RJYBLe2GoFQ6-9n9",
            "_score": 2.560356,
            "_source": {
                "pet_name": "Buddy",
                "owner_name": "Alice Johnson",
                "species": "Dog",
                "visit_details": "Annual check-up and nail trimming. Healthy and active.",
                "breed": "Golden Retriever",
                "vaccination_history": [
                    "Rabies",
                    "Parvovirus",
                    "Distemper"
                ]
            }
        }
    ]
}<h3>Requête sémantique (/search/semantic?q=&lt;query_term&gt;)</h3><p>La recherche sémantique, contrairement à la recherche lexicale, permet de trouver des résultats similaires à la signification des termes de recherche par le biais d'une recherche vectorielle.</p><p>Vous pouvez consulter notre tutoriel pratique sur la recherche sémantique <a href="https://www.elastic.co/demo-gallery/semantic-search">ici.</a></p>app.get("/search/semantic", async (req, res) =&gt; {
  const { q } = req.query;

  const INDEX_NAME = "vet-visits";

  try {
    const result = await esClient.search({
      index: INDEX_NAME,
      size: 5,
      body: {
        query: {
          semantic: {
            field: "semantic_field",
            query: q
          },
        },
      },
    });

    res.status(200).json({
      success: true,
      results: result.hits.hits,
    });
  } catch (error) {
    console.error("Error performing search:", error);

    res.status(500).json({
      success: false,
      results: null,
      error: error.message,
    });
  }
});<p>Nous testons avec : <em><strong>Qui s'est fait faire une pédicure ?</strong></em></p>curl http://localhost:3000/search/semantic?q=Who%20got%20a%20pedicure?<p>Réponse :</p>{
    "success": true,
    "results": [
        {
            "_index": "vet-visits",
            "_id": "-RY6RJYBLe2GoFQ6-9n9",
            "_score": 4.861466,
            "_source": {
                "owner_name": "Daniel Kim",
                "pet_name": "Mochi",
                "species": "Rabbit",
                "breed": "Mixed",
                "vaccination_history": [],
                "visit_details": "Nail trimming and general health check. No issues."
            }
        },
        {
            "_index": "vet-visits",
            "_id": "8BY6RJYBLe2GoFQ6-9n9",
            "_score": 4.7152824,
            "_source": {
                "pet_name": "Buddy",
                "owner_name": "Alice Johnson",
                "species": "Dog",
                "visit_details": "Annual check-up and nail trimming. Healthy and active.",
                "breed": "Golden Retriever",
                "vaccination_history": [
                    "Rabies",
                    "Parvovirus",
                    "Distemper"
                ]
            }
        },
        {
            "_index": "vet-visits",
            "_id": "9RY6RJYBLe2GoFQ6-9n9",
            "_score": 1.6717153,
            "_source": {
                "pet_name": "Rex",
                "owner_name": "Tomás Herrera",
                "species": "Dog",
                "visit_details": "Follow-up for previous leg strain, improving well.",
                "breed": "German Shepherd",
                "vaccination_history": [
                    "Rabies",
                    "Parvovirus",
                    "Leptospirosis"
                ]
            }
        },
        {
            "_index": "vet-visits",
            "_id": "9xY6RJYBLe2GoFQ6-9n9",
            "_score": 1.5600781,
            "_source": {
                "pet_name": "Simba",
                "owner_name": "Leo Martínez",
                "species": "Cat",
                "visit_details": "Dental cleaning. Minor tartar buildup removed.",
                "breed": "Maine Coon",
                "vaccination_history": [
                    "Rabies",
                    "Feline Panleukopenia"
                ]
            }
        },
        {
            "_index": "vet-visits",
            "_id": "-BY6RJYBLe2GoFQ6-9n9",
            "_score": 1.2696637,
            "_source": {
                "pet_name": "Rocky",
                "owner_name": "Rachel Green",
                "species": "Dog",
                "visit_details": "Skin rash, antihistamines prescribed.",
                "breed": "Bulldog Mix",
                "vaccination_history": [
                    "Rabies",
                    "Parvovirus"
                ]
            }
        }
    ]
}<h3>Requête hybride (/search/hybrid?q=&lt;query_term&gt;)</h3><p>La recherche hybride nous permet de combiner la recherche sémantique et la recherche lexicale, et d'obtenir ainsi le meilleur des deux mondes : vous bénéficiez de la précision de la recherche par jeton, ainsi que de la proximité de sens de la recherche sémantique.</p>app.get("/search/hybrid", async (req, res) =&gt; {
  const { q } = req.query;

  const INDEX_NAME = "vet-visits";

  try {
    const result = await esClient.search({
      index: INDEX_NAME,
      body: {
        retriever: {
          rrf: {
            retrievers: [
              {
                standard: {
                  query: {
                    bool: {
                      must: {
                         multi_match: {
             query: q,
            fields: ["owner_name", "pet_name", "visit_details"],
          },
                      },
                    },
                  },
                },
              },
              {
                standard: {
                  query: {
                    bool: {
                      must: {
                        semantic: {
                          field: "semantic_field",
                          query: q,
                        },
                      },
                    },
                  },
                },
              },
            ],
          },
        },
        size: 5,
      },
    });

    res.status(200).json({
      success: true,
      results: result.hits.hits,
    });
  } catch (error) {
    console.error("Error performing search:", error);

    res.status(500).json({
      success: false,
      results: null,
      error: error.message,
    });
  }
});<p>Nous testons avec "<em><strong>Qui a reçu une pédicure ou un traitement dentaire ?"</strong></em></p>curl http://localhost:3000/search/hybrid?q=who%20got%20a%20pedicure%20or%20dental%20treatment<p>Réponse :</p>{
    "success": true,
    "results": [
        {
            "_index": "vet-visits",
            "_id": "9xY6RJYBLe2GoFQ6-9n9",
            "_score": 0.032522473,
            "_source": {
                "pet_name": "Simba",
                "owner_name": "Leo Martínez",
                "species": "Cat",
                "visit_details": "Dental cleaning. Minor tartar buildup removed.",
                "breed": "Maine Coon",
                "vaccination_history": [
                    "Rabies",
                    "Feline Panleukopenia"
                ]
            }
        },
        {
            "_index": "vet-visits",
            "_id": "-RY6RJYBLe2GoFQ6-9n9",
            "_score": 0.016393442,
            "_source": {
                "pet_name": "Mochi",
                "owner_name": "Daniel Kim",
                "species": "Rabbit",
                "visit_details": "Nail trimming and general health check. No issues.",
                "breed": "Mixed",
                "vaccination_history": []
            }
        },
        {
            "_index": "vet-visits",
            "_id": "8BY6RJYBLe2GoFQ6-9n9",
            "_score": 0.015873017,
            "_source": {
                "pet_name": "Buddy",
                "owner_name": "Alice Johnson",
                "species": "Dog",
                "visit_details": "Annual check-up and nail trimming. Healthy and active.",
                "breed": "Golden Retriever",
                "vaccination_history": [
                    "Rabies",
                    "Parvovirus",
                    "Distemper"
                ]
            }
        },
        {
            "_index": "vet-visits",
            "_id": "9RY6RJYBLe2GoFQ6-9n9",
            "_score": 0.015625,
            "_source": {
                "pet_name": "Rex",
                "owner_name": "Tomás Herrera",
                "species": "Dog",
                "visit_details": "Follow-up for previous leg strain, improving well.",
                "breed": "German Shepherd",
                "vaccination_history": [
                    "Rabies",
                    "Parvovirus",
                    "Leptospirosis"
                ]
            }
        },
        {
            "_index": "vet-visits",
            "_id": "8xY6RJYBLe2GoFQ6-9n9",
            "_score": 0.015384615,
            "_source": {
                "pet_name": "Luna",
                "owner_name": "Jake Thompson",
                "species": "Dog",
                "visit_details": "Mild ear infection, cleaning and antibiotics given.",
                "breed": "Labrador Mix",
                "vaccination_history": [
                    "Rabies",
                    "Bordetella"
                ]
            }
        }
    ]
}<h2>Conclusion</h2><p>Dans cette première partie de notre série, nous avons expliqué comment configurer notre environnement et créer un serveur avec différents points de terminaison de recherche pour interroger les documents Elasticsearch en suivant les meilleures pratiques client/serveur. Consultez la <a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i">deuxième partie</a> de notre série, dans laquelle vous découvrirez les meilleures pratiques de production et comment exécuter le client Elasticsearch Node.js dans des environnements sans serveur.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i</guid>
    <category><![CDATA[JavaScript]]></category>
    <category><![CDATA[Les bases]]></category>
    <dc:creator><![CDATA[Jeffrey Rengifo]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt16d00c8a548b32e8/6a17e3d8fbc5f8c740491a19/72200540ed258779d87e53a72ea189f8a138540c-1600x901.png" length="0" type="image/png"/>
    <pubDate>Thu, 15 May 2025 00:00:00 GMT</pubDate>
  </item>
  </channel>
</rss>