<?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[Jeffrey Rengifo - 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[Jeffrey Rengifo - Elasticsearch Labs]]></title>
      <url>https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1121c0bf0e8a6e65/6a88da6340a1841030ef456f/search-labs-thumbnail.png</url>
      <link>https://www.elastic.co/fr/search-labs/author/jeffrey-rengifo</link>
    </image>
    <link>https://www.elastic.co/fr/search-labs/author/jeffrey-rengifo</link>
    <atom:link href="https://www.elastic.co/fr/search-labs/rss/author/jeffrey-rengifo.xml" rel="self" type="application/rss+xml"/>
    <language><![CDATA[fr]]></language>
    <lastBuildDate>Wed, 23 Sep 2026 03:31:49 GMT</lastBuildDate>
  <item>
    <title><![CDATA[Comment mesurer et améliorer le rappel de recherche Elasticsearch : de 0,43 à 0,75 avec la recherche hybride]]></title>
    <description><![CDATA[Découvrez comment mesurer et améliorer le rappel de recherche dans Elasticsearch en combinant la recherche lexicale BM25 avec les embeddings vectoriels de Jina AI, en utilisant l’API rank_eval pour valider l’amélioration avec des données chiffrées.]]></description>
    <content:encoded><![CDATA[<p><a href="https://www.elastic.co/docs/solutions/search/full-text">La recherche lexicale</a> utilisant <a href="https://www.elastic.co/blog/practical-bm25-part-1-how-shards-affect-relevance-scoring-in-elasticsearch">l’algorithme de classement BM25</a> est peu coûteuse, rapide et très efficace pour de nombreuses requêtes. Mais elle présente un inconvénient : les requêtes qui ne partagent pas de jetons avec vos documents. Dans cet article, vous allez mesurer exactement les points faibles de la BM25. Nous utiliserons <a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/search-rank-eval">l’API d’évaluation de classement</a> (<code>rank_eval</code>) d’Elasticsearch et comblerons cet écart en ajoutant <a href="https://www.elastic.co/search-labs/es/blog/jina-embeddings-v3-elastic-inference-service">des embeddings Jina AI</a> via <a href="https://www.elastic.co/docs/explore-analyze/elastic-inference/eis">Elastic Inference Service</a> (EIS). Vous verrez le score de rappel passer de <code>0.43</code> à <code>0.75</code> et vous comprendrez pourquoi.</p><h2>Qu'est-ce que le rappel ?</h2><p>Le <a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/search-rank-eval#k-recall">rappel</a> mesure, sur une échelle allant de <code>0</code> à <code>1</code>, le nombre de documents réellement souhaités par vos utilisateurs qui apparaissent quelque part dans vos résultats de recherche. Si une requête doit faire apparaître trois produits et que votre recherche ne renvoie que deux d’entre eux dans le top 10, <code>recall@10 = 0.67</code> pour cette requête. C’est une métrique basée sur des ensembles : la position des documents pertinents dans ces <em>k</em> résultats n’a pas d’importance. Un document pertinent en position 10 compte autant qu’un document en position 1. Un taux de rappel élevé signifie que vous ne perdez pas de résultats pertinents.</p><p>
</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt5ffd147b13705680/6a170a6fe8fbce11a539fc22/b13af2a5d0ca055535d8bfe3dfe4b3d1093ee6da-1457x796.png" alt="Diagramme de Venn illustrant le mode de calcul de Recall@10 en montrant le chevauchement entre tous les documents pertinents et les 10 meilleurs résultats obtenus par BM25, soit un score Recall@10 de 0,40." /><p>Le diagramme montre deux ensembles : tous les documents pertinents (à gauche) et ce que BM25 a réellement récupéré (les 10 premiers, à droite). Seules les intersections comptent pour le rappel, <code>prod_1</code> et <code>prod_2</code> ont été trouvés, tandis que <code>prod_3</code>, <code>prod_4</code> et <code>prod_6</code> ont été totalement manqués. Résultat : <code>Recall@10 = 2/5 = </code><strong><code>0.40</code></strong>.</p><h2>Produits requis</h2><p>Entrons dans le vif du sujet pour mieux comprendre le fonctionnement du rappel. Cette démonstration utilise Python. Vous pouvez la suivre dans le cahier d’accompagnement (<a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/relevance-tuning-improving-recall-adding-vectors/notebook.ipynb">notebook.ipynb</a>), où chaque bloc de code est une cellule prête à être exécutée.</p><p>Le code fourni utilise les éléments suivants :</p><ul><li><p>Elasticsearch 9.3+</p></li><li><p>Python 3.10+</p></li></ul>pip install elasticsearch pandas plotly python-dotenv<ul><li><p>Un fichier <code>.env</code> avec vos identifiants Elasticsearch</p></li></ul>ELASTICSEARCH_URL=https://your-cluster-url
ELASTICSEARCH_API_KEY=your-api-key<h2>L’ensemble de données</h2><p>Nous utiliserons un catalogue de produits de 1 000 articles, couvrant des catégories telles que les chaussures, l’électronique, les outils, et bien d’autres.</p><p>Chaque document comporte quatre champs :</p><p>Champ</p><p>Type</p><p>`title`</p><p>Texte</p><p>description</p><p>Texte</p><p>marque</p><p>mot-clé</p><p>Catégorie</p><p>mot-clé</p><p>L’ensemble de données est chargé à partir de <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/relevance-tuning-improving-recall-adding-vectors/dataset.csv"><code>dataset.csv</code></a>.</p><h2>La puissance et les limites de la recherche lexicale</h2><p>BM25 est l’algorithme de classement par défaut d’Elasticsearch et de la plupart des moteurs de recherche. Il attribue des scores aux documents en fonction de la fréquence d’apparition de vos termes de requête dans ceux-ci, ajustée en fonction de la longueur du document et de la fréquence de ces termes dans l’ensemble de l’index. Vous disposez d’<a href="https://www.elastic.co/docs/reference/text-analysis/analyzer-reference">analyseurs</a> en plus : normalisation des minuscules, troncature et suppression des mots vides. Une requête pour « chaussures de course » correspondra à « Chaussures de course » et probablement aussi à « courir ».</p><p>Cette méthode fonctionne bien pour une grande catégorie de requêtes :</p><ul><li><p>« chaussures de course » associe immédiatement les produits dont le titre correspond exactement à ces termes.</p></li><li><p>L’expression « enceinte Bluetooth » fait apparaître des produits audio portables, car les termes apparaissent tels quels.</p></li></ul><p>Les résultats sont déterministes et explicables : un document est bien classé parce que les termes de la requête y apparaissent. La pertinence du débogage est simple.</p><h3>Les cas d’échec</h3><p>Essayons maintenant ces requêtes sur le même catalogue :</p><ul><li><p><strong>« Routine de soins de la peau » :</strong> Le mot « routine » n’apparaît dans aucun titre de produit. BM25 peut correspondre partiellement à la requête « soins de la peau », mais les sérums pour le visage, les huiles corporelles et les crèmes hydratantes sont décrits à l’aide de termes comme « vitamine C », « rétinol » ou « éclaircissant », dont aucun ne correspond à la requête. Les produits qui forment une routine de soins de la peau complète sont dispersés dans l’index sans aucun élément commun permettant de les regrouper.</p></li></ul>ID: B06XX6DS3P, Score: 9.0552, Title: Replenix Retinol Smooth + Tighten Body Lotion - Collagen-Boosting, Regenerating Anti-Aging Body Cream, Reduces Appearance of Stretch Marks, 6.7 oz.

  ID: B08XMPKJ1L, Score: 5.2699, Title: Bio-Oil Skincare Body Oil (Natural) Serum for Scars and Stretchmarks, Face and Body Moisturizer Hydrates Skin, with Organic Jojoba Oil and Vitamin E, For All Skin Types, 6.7 oz

  ID: B01CY764KQ, Score: 5.0057, Title: Nike Up Or Down Men Deodorant - Pack of 2 | Long-Lasting Fragrance, Body Spray Combo for Men | Deodorant for Active Living | Nike Men's Deo Set | Ultimate Odor Protection | Grooming Essentials | Signature Nike Scent | High-Performance Men's Deodorant<ul><li><p><strong>« Accessoires de voyage pour animaux de compagnie » :</strong> il s’agit d’un regroupement de cas d’utilisation, et non d’une catégorie de produits. Un sac kangourou pour chien, un siège auto pour animal et une caisse de voyage sont tous pertinents, mais leurs descriptions parlent de portabilité, de sécurité et de confort plutôt que d’« accessoires de voyage ». BM25 trouve des correspondances pour le terme « animal de compagnie » au sens large, mais ne comporte aucun signal permettant de distinguer les produits spécifiques aux voyages du reste du catalogue pour animaux de compagnie.</p></li></ul>ID: B0BVV7BKTW, Score: 7.4371, Title: Large Foldable Travel Duffel Bag with Shoes Compartment

ID: B07TNPHYNV, Score: 6.6455, Title: 40 Pieces Christmas Bronze Jingle Bells Craft Small Bells

ID: B08R8FRW53, Score: 6.6335, Title: CUBY Dog and Cat Sling Carrier
ID: B08QMCQYGM, Score: 6.5259, Title: YTFGGY Whiteboard Pinstripe Tape 6 Rolls 1/8"
ID: B0CP3LQSWM, Score: 6.2994, Title: Portable Dog Water Bottle 32 Oz<p>Il s'agit d'un <strong>problème de rappel</strong>. Les documents pertinents se trouvent dans votre index. BM25 ne peut tout simplement pas les trouver car les mots de l'utilisateur et les mots du document ne correspondent pas suffisamment étroitement.</p><p>L'ajout de synonymes est utile pour les cas connus. Mais vous ne pouvez pas énumérer toutes les façons dont un utilisateur pourrait exprimer une intention. C'est là que les vecteurs entrent en jeu.</p><h2>Pourquoi il est conseillé de mesurer le rappel</h2><p>Avant de résoudre un problème, il faut le quantifier.</p><p><a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/search-rank-eval#k-recall"><strong>Recall@k</strong></a> mesure combien de documents vos utilisateurs souhaitent réellement voir apparaître dans vos résultats de recherche. Au sens strict :</p>Recall@k = (relevant documents found in top k) / (total relevant documents)<p><a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/search-rank-eval#k-precision"><strong>Precision@k</strong></a> mesure les k premiers résultats et combien sont réellement pertinents :</p>Precision@k = (relevant documents in top k) / k<p>Une grande précision garantit la qualité des résultats obtenus. Dans le commerce électronique, l’absence d’un produit pertinent (faible taux de rappel) est souvent pire que l’affichage d’un résultat légèrement imparfait (précision moindre), car un produit caché est une vente perdue.</p><p>L’API d’Elasticsearch <code>rank_eval</code> vous permet de mesurer les deux de manière systématique. Vous fournissez une liste de requêtes, chacune avec un ensemble de documents évalués, et Elasticsearch calcule les métriques pour vous pour l’ensemble des requêtes.</p><h2>Configuration de l'évaluation</h2><p>L’API <code>rank_eval</code> nécessite un <strong>ensemble de données d’évaluations</strong> : un mappage des requêtes vers les documents pertinents pour chacune d’elles, accompagné d’un grade de pertinence (0 = non pertinent, 1 = pertinent, 2 = très pertinent).</p><p>Dans le cahier, il s’agit de la <a href="https://www.elastic.co/docs/solutions/search/ranking/learning-to-rank-ltr#learning-to-rank-judgement-list">liste des jugements</a> :</p>judgments = [
    # Query 1: "running shoes" BM25 handles well (tokens appear in product titles) 
    {"query_id": "q1", "doc_id": "B09NQJFRW6", "grade": 2, "query": "running shoes"},
    {"query_id": "q1", "doc_id": "B08JMD4LMM", "grade": 2, "query": "running shoes"},
    {"query_id": "q1", "doc_id": "B08VRJ6F2Q", "grade": 2, "query": "running shoes"},
    {"query_id": "q1", "doc_id": "B07S8NRRWR", "grade": 2, "query": "running shoes"},
    {"query_id": "q1", "doc_id": "B01HD620I8", "grade": 2, "query": "running shoes"},
    {"query_id": "q1", "doc_id": "B07DX86321", "grade": 2, "query": "running shoes"},
    {"query_id": "q1", "doc_id": "B0968YVLQ8", "grade": 1, "query": "running shoes"},
    {"query_id": "q1", "doc_id": "B093QJ39ZS", "grade": 1, "query": "running shoes"},
    {"query_id": "q1", "doc_id": "B096FGSC39", "grade": 1, "query": "running shoes"},
    {"query_id": "q1", "doc_id": "B01GVQWVV2", "grade": 1, "query": "running shoes"},

    # Query 2: "skincare routine" intent-based, "routine" never appears in product titles
    {"query_id": "q2", "doc_id": "B08XMPKJ1L", "grade": 2, "query": "skincare routine"},
    {"query_id": "q2", "doc_id": "B0BN3WQB92", "grade": 2, "query": "skincare routine"},
    {"query_id": "q2", "doc_id": "B0BT7B7P5T", "grade": 2, "query": "skincare routine"},
    {"query_id": "q2", "doc_id": "B00NPA2WEY", "grade": 2, "query": "skincare routine"},
    {"query_id": "q2", "doc_id": "B06XX6DS3P", "grade": 1, "query": "skincare routine"},
    {"query_id": "q2", "doc_id": "B07PDRD1KT", "grade": 1, "query": "skincare routine"},
    {"query_id": "q2", "doc_id": "B074J7869B", "grade": 1, "query": "skincare routine"},
    {"query_id": "q2", "doc_id": "B08JV31QW4", "grade": 1, "query": "skincare routine"},
    {"query_id": "q2", "doc_id": "B00K3TVJMQ", "grade": 1, "query": "skincare routine"},

    # Query 3: "study desk setup" intent-based, products are desks/stands/organizers
    {"query_id": "q3", "doc_id": "B08CS35J2T", "grade": 2, "query": "study desk setup"},
    {"query_id": "q3", "doc_id": "B09B3LFDXJ", "grade": 2, "query": "study desk setup"},
    {"query_id": "q3", "doc_id": "B07W58LMND", "grade": 1, "query": "study desk setup"},
    {"query_id": "q3", "doc_id": "B0CHYDX91L", "grade": 1, "query": "study desk setup"},

    # Query 4: "pet travel accessories" use-case grouping, products are carriers/crates/seats
    {"query_id": "q4", "doc_id": "B08R8FRW53", "grade": 2, "query": "pet travel accessories"},
    {"query_id": "q4", "doc_id": "B01MYUYX33", "grade": 2, "query": "pet travel accessories"},
    {"query_id": "q4", "doc_id": "B003C5RKE4", "grade": 2, "query": "pet travel accessories"},
    {"query_id": "q4", "doc_id": "B09GF8GBF6", "grade": 1, "query": "pet travel accessories"},
    {"query_id": "q4", "doc_id": "B0CP3LQSWM", "grade": 1, "query": "pet travel accessories"},
]<p>Le mélange est intentionnel : <code>q1</code> est une requête que BM25 gère bien (jetons exacts dans les titres des produits), tandis que <code>q2</code>, <code>q3</code>, et <code>q4</code> sont des requêtes basées sur l’intention où l’intention de l’utilisateur est exprimée sous forme de concept plutôt que de mots-clés spécifiques sur les produits.</p><h2>Mesurer le rappel de référence du BM25</h2><p>Commencez par configurer le client Elasticsearch et indexez les données textuelles brutes :</p>import os
import json
import pandas as pd
import plotly.graph_objects as go
from elasticsearch import Elasticsearch, helpers
from dotenv import load_dotenv

load_dotenv()

es = Elasticsearch(
    os.getenv("ELASTICSEARCH_URL"),
    api_key=os.getenv("ELASTICSEARCH_API_KEY")
)

INDEX_NAME = "ecommerce-products"<p>Maintenant, créez la requête <code>rank_eval</code> pour BM25. Chaque requête dans la liste combine une requête avec ses notations :</p>judgments_df = pd.DataFrame(judgments)

bm25_requests = []
for query_id, query_text in (
    judgments_df[["query_id", "query"]].drop_duplicates().values
):
    relevant_docs = judgments_df[judgments_df["query_id"] == query_id]
    ratings = [
        {"_index": INDEX_NAME, "_id": row["doc_id"], "rating": row["grade"]}
        for _, row in relevant_docs.iterrows()
    ]

    bm25_requests.append({
        "id": query_id,
        "request": {
            "query": {
                "multi_match": {
                    "query": query_text,
                    "fields": ["title", "description"]
                }
            }
        },
        "ratings": ratings,
    })

bm25_eval = {
    "requests": bm25_requests,
    "metric": {"recall": {"k": 10, "relevant_rating_threshold": 1}},
}

bm25_result = es.rank_eval(index=INDEX_NAME, body=bm25_eval)
print("BM25 Recall@10:", bm25_result.body["metric_score"])<p>Voici le résultat.</p>BM25 Recall@10: 0.43<p><code>0.43</code> Signifie que sur l’ensemble des quatre requêtes, BM25 ne trouve que 43 % des documents qu’il devrait trouver. Le problème se situe principalement dans les requêtes basées sur l’intention : « routine de soins de la peau » ne trouve pas les sérums pour le visage et les huiles corporelles car le mot « routine » n’apparaît jamais dans les titres des produits, et « accessoires de voyage pour animaux de compagnie » renvoie des produits pour animaux de compagnie hors sujet tout en ne trouvant pas les cages et les caisses de transport dont la description précise les fonctionnalités de portabilité et de sécurité plutôt que d’« accessoires de voyage ».</p><p>Ceci est notre référence. Nous avons maintenant un chiffre à battre.</p><h2>Ajout de la recherche vectorielle avec les embeddings Jina</h2><p><a href="https://www.elastic.co/docs/solutions/search/vector"><code>Vector search</code></a> encode les documents et les requêtes sous forme de vecteurs de grande dimension, composés de centaines ou de milliers de valeurs numériques, chacune encodant une fonctionnalité spécifique des données représentées. Les documents ayant une signification similaire se retrouvent proches les uns des autres dans l’espace vectoriel, même s’ils ne partagent aucun mot. « Équipement de gym » et « kit d’haltères » seront proches l’un de l’autre, car les concepts sont liés. J’ai choisi Elasticsearch comme base vectorielle, car il prend en charge la recherche hybride, ce qui me permet de bénéficier d'emblée à la fois d’une compréhension sémantique et d’une précision par mot-clé.</p><p><a href="https://www.elastic.co/docs/explore-analyze/elastic-inference/eis">EIS</a> inclut une prise en charge prête à l’emploi pour l’intégration de modèles via son <a href="https://www.elastic.co/docs/api/doc/elasticsearch/group/endpoint-inference">API d’inférence</a>.</p><h3>Étape 1 : utiliser les embeddings Jina v5 comme point de terminaison d’inférence</h3>INFERENCE_ENDPOINT_ID = ".jina-embeddings-v5-text-small"<p>Si votre cluster dispose de ressources GPU (disponibles dans Elastic Cloud et Elasticsearch 9.3+), les embeddings sont générés sur GPU, ce qui est nettement plus rapide que l’inférence CPU, et supprime le compromis de performance qui rendait auparavant l’utilisation des vecteurs coûteuses à grande échelle.</p><p>Pourquoi spécifiquement les embeddings Jina ? <a href="https://www.elastic.co/search-labs/blog/jina-embeddings-v5-text">jina-embeddings-v5-text</a> est un modèle multilingue (plus de 119 langues) avec une fenêtre de contexte de 32 000 jetons et une prise en charge <a href="https://arxiv.org/abs/2106.09685">des adaptateurs Low-Rank Adaptation (LoRA)</a> spécifiques à la tâche. Il fonctionne bien pour les courtes descriptions de produits prêtes à l’emploi. En savoir plus sur le modèle <code>jina-embeddings-v5-text</code> <a href="https://huggingface.co/jinaai/jina-embeddings-v5-text-small">ici</a>.</p><h3>Étape 2 : créer l’index avec un champ sémantique</h3>index_mappings = {
    "mappings": {
        "properties": {
            "title": {"type": "text", "copy_to": "semantic_field"},
            "description": {"type": "text", "copy_to": "semantic_field"},
            "brand": {"type": "keyword"},
            "category": {"type": "keyword"},
            "semantic_field": {
                "type": "semantic_text",
                "inference_id": INFERENCE_ENDPOINT_ID,
            },
        }
    }
}

if not es.indices.exists(index=INDEX_NAME):
    es.indices.create(index=INDEX_NAME, body=index_mappings)
    print(f"Created index: {INDEX_NAME}")<p>Le type de champ <a href="https://www.elastic.co/docs/solutions/search/semantic-search/semantic-search-semantic-text"><code>semantic_text</code></a> est ici la clé. C’est une abstraction de niveau supérieur par rapport à <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/dense-vector"><code>dense_vector</code></a> : vous le dirigez vers un point de terminaison d’inférence, et Elasticsearch se charge de générer automatiquement les plongements.</p><p>La propriété <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/copy-to"><code>copy_to</code></a> sur <code>title</code> et <code>description</code> signifie que le contenu des deux champs est transmis à <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/semantic-text"><code>semantic_field</code></a> pour intégration, de sorte qu’un seul vecteur capture la représentation complète du produit.</p><h3>Étape 3 : indexer les produits</h3>def bulk_index(products, index_name):
    actions = []
    for product in products:
        doc_id = product.get("_id")
        source = {k: v for k, v in product.items() if k != "_id"}
        action = {"_index": index_name, "_source": source}
        if doc_id:
            action["_id"] = doc_id
        actions.append(action)

    success, failed = helpers.bulk(es, actions, raise_on_error=False)
    if failed:
        for error in failed:
            print(f"Error: {error}")
    else:
        print(f"Successfully indexed {success} documents")

bulk_index(products, INDEX_NAME)<p>Au moment de l’indexation, Elasticsearch appelle le point de terminaison d’inférence pour chaque document et stocke le vecteur d’intégration résultant dans <code>semantic_field</code>. Aucun code supplémentaire n’est nécessaire de votre côté.</p><h2>Recherche hybride : combinaison de BM25 et de vecteurs avec RRF</h2><p>L'ajout de vecteurs améliore le taux de rappel, mais le recours exclusif aux vecteurs risque d'entraîner une perte de précision pour les requêtes en correspondance exacte. Les résultats pour « chaussures de course » devraient toujours classer en premier les correspondances exactes. La recherche hybride conserve la composante lexicale spécifiquement pour préserver cette précision.</p><p>La recherche hybride avec la <a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/reciprocal-rank-fusion">Fusion des rangs réciproques</a> (RRF) combine le meilleur des deux :</p><ul><li><p>BM25 gère les requêtes exactes et quasi-exactes avec une grande précision.</p></li><li><p>La recherche sémantique gère les requêtes basées sur l’intention et multilingues avec un rappel élevé.</p></li><li><p>RRF fusionne les deux listes classées en un seul classement.</p></li></ul><p>La formule RRF attribue à chaque document un score basé sur son classement dans chaque liste de résultats :</p>score = sum(1 / (rank_constant + rank))<p>Un document bien classé dans les deux listes obtient un score combiné plus élevé. Le paramètre <code>rank_constant</code> détermine le poids attribué aux documents moins bien classés.</p>hybrid_requests = []

for query_id, query_text in (
    judgments_df[["query_id", "query"]].drop_duplicates().values
):
    relevant_docs = judgments_df[judgments_df["query_id"] == query_id]
    ratings = [
        {"_index": INDEX_NAME, "_id": row["doc_id"], "rating": row["grade"]}
        for _, row in relevant_docs.iterrows()
    ]

    hybrid_requests.append({
        "id": query_id,
        "request": {
            "retriever": {
                "rrf": {
                    "retrievers": [
                        {
                            "standard": {
                                "query": {
                                    "multi_match": {
                                        "query": query_text,
                                        "fields": ["title", "description"],
                                    }
                                }
                            }
                        },
                        {
                            "standard": {
                                "query": {
                                    "match": {
                                        "semantic_field": {"query": query_text}
                                    }
                                }
                            }
                        },
                    ],
                    "rank_window_size": 50,
                    "rank_constant": 5,
                }
            }
        },
        "ratings": ratings,
    })

hybrid_eval = {
    "requests": hybrid_requests,
    "metric": {"recall": {"k": 10, "relevant_rating_threshold": 1}},
}

hybrid_result = es.rank_eval(index=INDEX_NAME, body=hybrid_eval)
print("Hybrid Recall@10:", hybrid_result.body["metric_score"])<p>Voici le résultat.</p>Hybrid Recall@10: 0.75<p>Hybrid s’améliore nettement par rapport à BM25 (<code>0.43</code>) et préserve la précision des requêtes de correspondance exacte, comme « chaussures de course ».</p><h2>Résultats : avant et après</h2><p>Voici un comparatif complet des trois approches :</p>methods = {
    "BM25 (Lexical)": bm25_requests,
    "Hybrid (BM25 + Vectors)": hybrid_requests,
}

recall_metric = {"recall": {"k": 10, "relevant_rating_threshold": 1}}

comparison_data = []
for method_name, requests in methods.items():
    result = es.rank_eval(
        index=INDEX_NAME,
        body={"requests": requests, "metric": recall_metric}
    )
    comparison_data.append({
        "method": method_name,
        "recall@10": result.body["metric_score"]
    })

comparison_df = pd.DataFrame(comparison_data)
print(comparison_df.to_string(index=False))<p>Voici le résultat.</p><p>Méthode</p><p>Recall@10</p><p>BM25 (Lexical)</p><p>0,43</p><p>Hybride (BM25 + vecteurs)</p><p>0,75</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt5a1d72b57056fe64/6a170a71c1e8a56c58f882ab/e49f6c10516b0a48a0ad75962c6590ee07311407-700x500.png" alt="Graphique en barres comparant Recall@10 entre la recherche lexicale BM25 et la recherche hybride combinant BM25 avec des vecteurs, montrant que la recherche hybride atteint un rappel significativement plus élevé." /><p>Répartition des données par requête :</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt871347f754c866d0/6a170a73839dfa40abdcfeb4/40e36dcb7b34cbf4649c512bcb60cef60f1778a6-700x500.png" alt="Graphique en barres groupées comparant Recall@10 entre la recherche lexicale BM25 et la recherche hybride pour quatre requêtes de produits, illustrant que la recherche hybride obtient systématiquement de meilleurs résultats que la recherche lexicale pour chaque requête." /><h2>Conclusion</h2><p>Tout au long de cet article, nous avons vu que la recherche lexicale BM25 est fiable lorsque les utilisateurs tapent des requêtes exactes, mais qu’elle perd en rappel lorsqu’ils recherchent par intention plutôt que par mots-clés. En utilisant <code>rank_eval</code>, nous avons établi une base reproductible pour mesurer cet écart avec des nombres réels. Ensuite, nous avons ajouté un champ <code>semantic_text</code> alimenté par les embeddings Jina et relancé l’évaluation. Le résultat : la recherche hybride a amélioré le rappel de <code>0.43</code> à <code>0.75</code>, tout en préservant la précision des requêtes à correspondance exacte, bien que la marge réelle dépende de la composition de vos requêtes.</p><p>Le modèle s’étend au-delà de cet exemple : collectez les jugements à partir des requêtes réelles de vos utilisateurs, exécutez <code>rank_eval</code> comme référence, ajoutez <code>semantic_text</code>, puis mesurez à nouveau. Vous saurez exactement ce qui s’est amélioré et de combien.</p><h2>Étapes suivantes</h2><ul><li><p>Découvrez la recherche de rappel et de recherche vectorielle : <a href="https://www.elastic.co/search-labs/blog/recall-vector-search-quantization">quantification du rappel et de la recherche vectorielle</a> par Jeff Vestal</p></li><li><p>Ajoutez le reclassement pour une précision encore meilleure sur les meilleurs résultats</p></li><li><p>Consultez <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/rrf.html">la documentation sur la recherche hybride avec Elasticsearch</a></p></li><li><p>En savoir plus sur <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/search-rank-eval.html"><code>rank_eval</code></a><a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/search-rank-eval.html">l’API</a></p></li></ul>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/elasticsearch-relevance-tuning-improve-recall</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/elasticsearch-relevance-tuning-improve-recall</guid>
    <category><![CDATA[Recherche hybride]]></category>
    <category><![CDATA[Base vectorielle]]></category>
    <dc:creator><![CDATA[Jeffrey Rengifo]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt37c9d2971b5a2db3/6a170a75cf4f254223b2d149/492c9b5432a2b9e40cebb3b60f0df019a8c7bf6d-1280x720.png" length="0" type="image/png"/>
    <pubDate>Mon, 04 May 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Création d'un serveur Elasticsearch MCP avec TypeScript]]></title>
    <description><![CDATA[Apprenez à créer un serveur MCP Elasticsearch avec TypeScript et Claude Desktop.]]></description>
    <content:encoded><![CDATA[<p>Lorsque vous travaillez avec de grandes bases de connaissances dans Elasticsearch, trouver des informations n’est que la moitié du travail. Les ingénieurs ont souvent besoin de synthétiser des résultats issus de plusieurs documents, de générer des résumés et de faire remonter les réponses à leur source. Model Context Protocol (MCP) fournit un moyen standardisé de connecter Elasticsearch à des applications alimentées par des grands modèles de langage (LLM) afin d’y parvenir. Bien qu’Elastic propose des solutions officielles, comme Elastic Agent Builder (qui inclut un <a href="https://www.elastic.co/docs/solutions/search/agent-builder/mcp-server">point de terminaison MCP</a> parmi ses fonctionnalités), la création d’un serveur MCP personnalisé vous offre un contrôle total sur la logique de recherche, la mise en forme des résultats et la manière dont le contenu récupéré est transmis à un LLM pour la synthèse, les résumés et les citations.</p><p>Dans cet article, nous examinerons les avantages de la création d’un serveur MCP Elasticsearch personnalisé et expliquerons comment en créer un en TypeScript pour connecter Elasticsearch aux applications alimentées par des modèles LLM.</p><h2>Pourquoi créer un serveur Elasticsearch MCP personnalisé ?</h2><p>Elastic propose quelques alternatives pour <a href="https://www.elastic.co/docs/solutions/search/mcp">les serveurs MCP</a> :</p><ul><li><p><a href="https://www.elastic.co/docs/solutions/search/agent-builder/mcp-server">Serveur MCP Elastic Agent Builder pour Elasticsearch 9.2+</a></p></li><li><p><a href="https://github.com/elastic/mcp-server-elasticsearch?tab=readme-ov-file#elasticsearch-mcp-server">Serveur MCP Elasticsearch pour les anciennes versions (Python)</a></p></li></ul><p>Si vous avez besoin de plus de contrôle sur la façon dont votre serveur MCP interagit avec Elasticsearch, la création de votre propre serveur personnalisé vous donne la flexibilité de l'adapter exactement à vos besoins. Par exemple, le point de terminaison MCP d'Agent Builder est limité aux requêtes du langage de requête Elasticsearch (ES|QL), tandis qu'un serveur personnalisé vous permet d'utiliser le langage de requête DSL complet. Vous gagnez également le contrôle sur la façon dont les résultats sont formatés avant d'être transmis au LLM et pouvez intégrer des étapes de traitement supplémentaires, comme la summarisation alimentée par OpenAI que nous mettrons en œuvre dans ce tutoriel.</p><p>À la fin de cet article, vous aurez un serveur MCP dans TypeScript qui recherche les informations stockées dans un index Elasticsearch, les résume et fournit des citations. Nous utiliserons Elasticsearch pour la récupération, le modèle <code>gpt-4o-mini</code> d'OpenAI pour résumer et générer des citations, et Claude Desktop comme client MCP et interface utilisateur pour recevoir les requêtes des utilisateurs et fournir des réponses. Le résultat final est un assistant de connaissances interne qui aide les ingénieurs à découvrir et à synthétiser les bonnes pratiques dans l'ensemble de la documentation technique de leur organisation.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltad9133cb083ad352/6a170c19b0367d411e72bd5b/ec5771a874cf9740d4cac6888622cbe8cd6aede7-1999x1133.png" alt="Création d’un serveur MCP Elastic avec TypeScript et Claude Desktop." /><h2>Produits requis</h2><ul><li><p>Node.js 20 +</p></li><li><p>Elasticsearch</p></li><li><p>Clé API OpenAI</p></li><li><p>Claude Desktop</p></li></ul><h3>Qu'est-ce que le MCP ?</h3><p><a href="https://www.elastic.co/what-is/mcp">MCP</a> est une norme ouverte, créée par <a href="https://www.anthropic.com/news/model-context-protocol">Anthropic</a>, qui fournit des connexions bidirectionnelles sécurisées entre les LLM et les systèmes externes, comme Elasticsearch. Vous pouvez en savoir plus sur l'état actuel du MCP dans <a href="https://www.elastic.co/search-labs/blog/mcp-current-state">cet article</a>.</p><p>Le paysage des MCP <a href="https://www.elastic.co/search-labs/blog/mcp-current-state#mcp-project-updates:-transport,-elicitation,-and-structured-tooling">évolue chaque jour</a>, avec des serveurs disponibles pour un large éventail de cas d'utilisation. De plus, il est facile de créer votre propre serveur MCP personnalisé, comme nous le montrerons dans cet article.</p><h3>Clients MCP</h3><p>Il existe une longue <a href="https://modelcontextprotocol.io/clients">liste de clients MCP disponibles</a>, chacun ayant ses propres caractéristiques et limitations. Par souci de simplicité et de popularité, nous utiliserons <a href="https://claude.ai/download">Claude Desktop</a> comme client MCP. Il servira d'interface de chat où les utilisateurs pourront poser des questions en langage naturel, et il invoquera automatiquement les outils exposés par notre serveur MCP pour rechercher des documents et générer des résumés.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt06fd7a02042094e1/6a170c1b14b2700024e3c651/66eb0b11473347b6cf2d85718251eeac38d6249d-1999x1491.png" alt="Page de Claude 4.5 du Sonnet, avec la note : « Café avec Claude ? » Comment puis-je vous aider aujourd'hui ?" /><h2>Créer un serveur Elasticsearch MCP</h2><p>Grâce au <a href="https://github.com/modelcontextprotocol/typescript-sdk">SDK TypeScript</a>, nous pouvons facilement créer un serveur qui comprend comment interroger nos données Elasticsearch à partir d'une requête utilisateur.</p><p>Voici les étapes dans cet article pour intégrer le serveur Elasticsearch MCP avec le client Claude Desktop :</p><ol><li><p><a href="https://www.elastic.co/search-labs/blog/elastic-mcp-server-typescript-claude#configure-mcp-server-for-elasticsearch">Configurer le serveur MCP pour Elasticsearch.</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/elastic-mcp-server-typescript-claude#load-the-mcp-server-into-claude-desktop">Chargez le serveur MCP dans Claude Desktop.</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/elastic-mcp-server-typescript-claude#test-it-out">Testez-le.</a></p></li></ol><h3>Configurez le serveur MCP pour Elasticsearch</h3><p>Pour commencer, initialisons une application Node :</p>npm init -y<p>Cela créera un fichier <code>package.json</code>, et avec lui, nous pourrons commencer à installer les dépendances nécessaires pour cette application.</p>npm install @elastic/elasticsearch @modelcontextprotocol/sdk openai zod &amp;&amp; npm install --save-dev ts-node @types/node typescript<ul><li><p><strong>@elastic/elasticsearch</strong> nous donnera accès à la bibliothèque de Node.js Elasticsearch.</p></li><li><p><strong>@modelcontextprotocol/sdk</strong> fournit les outils du noyau pour créer et gérer un serveur MCP, enregistrer les outils et gérer la communication avec les clients MCP.</p></li><li><p><strong>OpenAI</strong> permet d'interagir avec les modèles OpenAI pour générer des résumés ou des réponses en langage naturel.</p></li><li><p><a href="https://zod.dev/"><strong>ZOD</strong></a>aide à définir et valider des schémas structurés pour les données d’entrée et de sortie dans chaque outil.</p></li></ul><p><code>ts-node</code>, <code>@types/node</code> et <code>typescript</code> seront utilisés pendant le développement pour écrire le code et compiler les scripts.</p><h4>Configurer l’ensemble de données</h4><p>Pour fournir les données que Claude Desktop peut interroger via notre serveur MCP, nous utiliserons un <a href="https://github.com/Delacrobix/typescript-elasticsearch-mcp/blob/main/dataset.json">ensemble de données simulé de base de connaissances interne</a>. Voici à quoi ressemblera un document issu de cet ensemble de données :</p>{
    "id": 5,
    "title": "Logging Standards for Microservices",
    "content": "Consistent logging across microservices helps with debugging and tracing. Use structured JSON logs and include request IDs and timestamps. Avoid logging sensitive information. Centralize logs in Elasticsearch or a similar system. Configure log rotation to prevent storage issues and ensure logs are searchable for at least 30 days.",
    "tags": ["logging", "microservices", "standards"]
}<p>Pour ingérer les données, nous avons préparé un script qui crée un index dans Elasticsearch et y charge l’ensemble de données. Vous pouvez le trouver <a href="https://github.com/Delacrobix/typescript-elasticsearch-mcp/blob/main/setup.ts">ici</a>.</p><h4>Serveur MCP</h4><p>Créez un fichier nommé <a href="https://github.com/Delacrobix/typescript-elasticsearch-mcp/blob/main/index.ts"><code>index.ts</code></a> et ajoutez le code suivant pour importer les dépendances et gérer les variables d’environnement :</p>// index.ts
import { z } from "zod";
import { Client } from "@elastic/elasticsearch";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import OpenAI from "openai";

const ELASTICSEARCH_ENDPOINT =
  process.env.ELASTICSEARCH_ENDPOINT ?? "http://localhost:9200";
const ELASTICSEARCH_API_KEY = process.env.ELASTICSEARCH_API_KEY ?? "";
const OPENAI_API_KEY = process.env.OPENAI_API_KEY ?? "";
const INDEX = "documents";<p>Aussi, initialisons les clients pour gérer les appels Elasticsearch et OpenAI :</p>const openai = new OpenAI({
  apiKey: OPENAI_API_KEY,
});

const _client = new Client({
  node: ELASTICSEARCH_ENDPOINT,
  auth: {
    apiKey: ELASTICSEARCH_API_KEY,
  },
});<p>Pour rendre notre implémentation plus robuste et garantir des entrées et des sorties structurées, nous définirons des schémas en utilisant <a href="https://zod.dev/"><code>zod</code></a>. Cela nous permet de valider les données au moment de l'exécution, de détecter les erreurs tôt et de rendre les réponses des outils plus faciles à traiter de manière programmatique :</p>const DocumentSchema = z.object({
  id: z.number(),
  title: z.string(),
  content: z.string(),
  tags: z.array(z.string()),
});

const SearchResultSchema = z.object({
  id: z.number(),
  title: z.string(),
  content: z.string(),
  tags: z.array(z.string()),
  score: z.number(),
});

type Document = z.infer&lt;typeof DocumentSchema&gt;;
type SearchResult = z.infer&lt;typeof SearchResultSchema&gt;;<p>Pour en savoir plus sur les sorties structurées, cliquez <a href="https://www.elastic.co/search-labs/blog/structured-outputs-elasticsearch-guide">ici</a>.</p><p>Maintenant, initialisons le serveur MCP :</p>const server = new McpServer({
  name: "Elasticsearch RAG MCP",
  description:
    "A RAG server using Elasticsearch. Provides tools for document search, result summarization, and source citation.",
  version: "1.0.0",
});<h4>Définition des outils MCP</h4><p>Une fois que tout est configuré, nous pouvons commencer à écrire les outils qui seront exposés par notre serveur MCP. Ce serveur expose deux outils :</p><ul><li><p><strong><code>search_docs</code></strong><strong>: </strong>Recherche des documents dans Elasticsearch à l'aide de la recherche full-text.</p></li><li><p><strong><code>summarize_and_cite</code></strong><strong>:</strong> Résume et synthétise les informations provenant de documents précédemment récupérés pour répondre à la question d'un utilisateur. Cet outil ajoute également des citations faisant référence aux documents sources.</p></li></ul><p>Ensemble, ces outils forment un workflow simple de « récupération puis synthèse », où un outil extrait les documents pertinents et l'autre utilise ces documents pour générer une réponse synthétisée et citée.</p><h4>Format de réponse de l'outil</h4><p>Chaque outil peut accepter des paramètres d'entrée arbitraires, mais il doit répondre avec la structure suivante :</p><ul><li><p><strong>Contenu :</strong> il s'agit de la réponse de l'outil dans un format non structuré. Ce champ est généralement utilisé pour renvoyer du texte, des images, de l’audio, des liens ou des plongements. Pour cette application, il sera utilisé pour renvoyer un texte formaté contenant les informations générées par les outils.</p></li><li><p><strong>structuredContent : </strong>il s'agit d'un retour facultatif utilisé pour fournir les résultats de chaque outil dans un format structuré. Ceci est utile à des fins de programmation. Bien qu'il ne soit pas utilisé dans ce serveur MCP, il peut être utile si vous souhaitez développer d'autres outils ou traiter les résultats de manière programmée.</p></li></ul><p>En gardant cette structure à l’esprit, entrons dans le vif du sujet en examinant chaque outil en détail.</p><h4>Outil de recherche</h4><p>Cet outil effectue une <a href="https://www.elastic.co/docs/solutions/search/full-text">recherche full-text</a> dans l’index Elasticsearch pour récupérer les documents les plus pertinents selon la requête de l’utilisateur. Il met en évidence les correspondances clés et offre un aperçu rapide avec des scores de pertinence.</p>server.registerTool(
  "search_docs",
  {
    title: "Search Documents",
    description:
      "Search for documents in Elasticsearch using full-text search. Returns the most relevant documents with their content, title, tags, and relevance score.",
    inputSchema: {
      query: z
        .string()
        .describe("The search query terms to find relevant documents"),
      max_results: z
        .number()
        .optional()
        .default(5)
        .describe("Maximum number of results to return"),
    },
    outputSchema: {
      results: z.array(SearchResultSchema),
      total: z.number(),
    },
  },
  async ({ query, max_results }) =&gt; {
    if (!query) {
      return {
        content: [
          {
            type: "text",
            text: "Query parameter is required",
          },
        ],
        isError: true,
      };
    }

    try {
      const response = await _client.search({
        index: INDEX,
        size: max_results,
        query: {
          bool: {
            must: [
              {
                multi_match: {
                  query: query,
                  fields: ["title^2", "content", "tags"],
                  fuzziness: "AUTO",
                },
              },
            ],
            should: [
              {
                match_phrase: {
                  title: {
                    query: query,
                    boost: 2,
                  },
                },
              },
            ],
          },
        },
        highlight: {
          fields: {
            title: {},
            content: {},
          },
        },
      });

      const results: SearchResult[] = response.hits.hits.map((hit: any) =&gt; {
        const source = hit._source as Document;

        return {
          id: source.id,
          title: source.title,
          content: source.content,
          tags: source.tags,
          score: hit._score ?? 0,
        };
      });

      const contentText = results
        .map(
          (r, i) =&gt;
            `[${i + 1}] ${r.title} (score: ${r.score.toFixed(
              2,
            )})\n${r.content.substring(0, 200)}...`,
        )
        .join("\n\n");

      const totalHits =
        typeof response.hits.total === "number"
          ? response.hits.total
          : (response.hits.total?.value ?? 0);

      return {
        content: [
          {
            type: "text",
            text: `Found ${results.length} relevant documents:\n\n${contentText}`,
          },
        ],
        structuredContent: {
          results: results,
          total: totalHits,
        },
      };
    } catch (error: any) {
      console.log("Error during search:", error);

      return {
        content: [
          {
            type: "text",
            text: `Error searching documents: ${error.message}`,
          },
        ],
        isError: true,
      };
    }
  }
);<p><em>Nous configurons </em><a href="https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-fuzzy-query"><em><code>fuzziness</code></em></a><em><code>: “AUTO”</code></em><em> pour que la tolérance aux fautes de frappe soit variable en fonction de la longueur du jeton analysé. Nous configurons également </em><em><code>title^2</code></em><em> pour qu'il augmente le score des documents dont la correspondance se fait sur le champ du titre.</em></p><h4>outil summarize_and_cite</h4><p>Cet outil génère un résumé basé sur les documents récupérés lors de la recherche précédente. Il utilise le modèle <code>gpt-4o-mini</code> d’OpenAI pour synthétiser les informations les plus pertinentes afin de répondre à la question de l’utilisateur, en fournissant des réponses dérivées directement des résultats de recherche. Outre le résumé, il renvoie également les métadonnées de citation des documents sources utilisés.</p>server.registerTool(
  "summarize_and_cite",
  {
    title: "Summarize and Cite",
    description:
      "Summarize the provided search results to answer a question and return citation metadata for the sources used.",
    inputSchema: {
      results: z
        .array(SearchResultSchema)
        .describe("Array of search results from search_docs"),
      question: z.string().describe("The question to answer"),
      max_length: z
        .number()
        .optional()
        .default(500)
        .describe("Maximum length of the summary in characters"),
      max_docs: z
        .number()
        .optional()
        .default(5)
        .describe("Maximum number of documents to include in the context"),
    },
    outputSchema: {
      summary: z.string(),
      sources_used: z.number(),
      citations: z.array(
        z.object({
          id: z.number(),
          title: z.string(),
          tags: z.array(z.string()),
          relevance_score: z.number(),
        })
      ),
    },
  },
  async ({ results, question, max_length, max_docs }) =&gt; {
    if (!results || results.length === 0 || !question) {
      return {
        content: [
          {
            type: "text",
            text: "Both results and question parameters are required, and results must not be empty",
          },
        ],
        isError: true,
      };
    }

    try {
      const used = results.slice(0, max_docs);

      const context = used
        .map(
          (r: SearchResult, i: number) =&gt;
            `[Document ${i + 1}: ${r.title}]\\n${r.content}`
        )
        .join("\n\n---\n\n");

      // Generate summary with OpenAI
      const completion = await openai.chat.completions.create({
        model: "gpt-4o-mini",
        messages: [
          {
            role: "system",
            content:
              "You are a helpful assistant that answers questions based on provided documents. Synthesize information from the documents to answer the user's question accurately and concisely. If the documents don't contain relevant information, say so.",
          },
          {
            role: "user",
            content: `Question: ${question}\\n\\nRelevant Documents:\\n${context}`,
          },
        ],
        max_tokens: Math.min(Math.ceil(max_length / 4), 1000),
        temperature: 0.3,
      });

      const summaryText =
        completion.choices[0]?.message?.content ?? "No summary generated.";

      const citations = used.map((r: SearchResult) =&gt; ({
        id: r.id,
        title: r.title,
        tags: r.tags,
        relevance_score: r.score,
      }));

      const citationText = citations
        .map(
          (c: any, i: number) =&gt;
            `[${i + 1}] ID: ${c.id}, Title: "${c.title}", Tags: ${c.tags.join(
              ", ",
            )}, Score: ${c.relevance_score.toFixed(2)}`,
        )
        .join("\n");

      const combinedText = `Summary:\\n\\n${summaryText}\\n\\nSources used (${citations.length}):\\n\\n${citationText}`;

      return {
        content: [
          {
            type: "text",
            text: combinedText,
          },
        ],
        structuredContent: {
          summary: summaryText,
          sources_used: citations.length,
          citations: citations,
        },
      };
    } catch (error: any) {
      return {
        content: [
          {
            type: "text",
            text: `Error generating summary and citations: ${error.message}`,
          },
        ],
        isError: true,
      };
    }
  }
);<p>Enfin, il faut démarrer le serveur avec <a href="https://github.com/modelcontextprotocol/typescript-sdk?tab=readme-ov-file#stdio">stdio</a>. Cela signifie que le client MCP communiquera avec notre serveur en lisant et en écrivant dans ses flux d'entrée et de sortie standard. StDIO est l’option de transport la plus simple et fonctionne bien pour les serveurs MCP locaux lancés en sous-processus par le client. Ajoutez le code suivant à la fin du fichier :</p>const transport = new StdioServerTransport();
server.connect(transport);<p>Compilez le projet en utilisant la commande suivante :</p>npx tsc index.ts --target ES2022 --module node16 --moduleResolution node16 --outDir ./dist --strict --esModuleInterop<p>Cela créera un dossier <code>dist</code>, dans lequel se trouvera un fichier <code>index.js</code>.</p><h3>Chargez le serveur MCP dans Claude Desktop.</h3><p>Suivez <a href="https://modelcontextprotocol.io/docs/develop/connect-local-servers">ce guide</a> pour configurer le serveur MCP avec Claude Desktop. Dans le fichier de configuration Claude, nous devons définir les valeurs suivantes :</p>{
  "mcpServers": {
    "elasticsearch-rag-mcp": {
      "command": "node",
      "args": [   "/Users/user-name/app-dir/dist/index.js"
      ],
      "env": {
        "ELASTICSEARCH_ENDPOINT": "your-endpoint-here",
        "ELASTICSEARCH_API_KEY": "your-api-key-here",
        "OPENAI_API_KEY": "your-openai-key-here"
      }
    }
  }
}<p>La valeur <code>args</code> doit pointer vers le fichier compilé dans le dossier <code>dist</code> . Vous devez également définir les variables d'environnement dans le fichier de configuration avec les noms exacts définis dans le code.</p><h3>Testez-le</h3><p>Avant d’exécuter chaque outil, cliquez sur <strong>Recherche et Outils</strong> pour vous assurer que les outils sont activés. Vous pouvez également activer ou désactiver chaque option ici :</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt395a7337021f9820/6a170c1c67045bb74d45c228/172981c2a54adabc70d5819013c3007670935605-1999x1002.png" alt="Claude 4.5 Page de sonnet, avec la note : « Bonjour, Jeff. » Comment puis-je vous aider aujourd'hui ?" /><p>Enfin, testons le serveur MCP depuis le chat Claude Desktop et commençons à poser des questions :</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf4ac458dc0206271/6a170c1e66c4f91328f8c072/03654c0f8c53c714f801fba8b25747071179209b-1999x1353.png" alt="Requête de recherche d'utilisateur dans le chat de Claude Desktop pour des documents sur les méthodes d'authentification et le contrôle d'accès basé sur les rôles, ainsi que sur les réponses de Claude." /><p>Pour la question « <strong>Recherche de documents sur les méthodes d’authentification et le contrôle d’accès basé sur les rôles</strong> », l’outil <code>search_docs</code> est exécuté et renvoie les résultats suivants :</p>Most Relevant Documents:
Access Control and Role Management (highest relevance) - This document covers role-based access control (RBAC) principles, including ensuring users only have necessary permissions, regular auditing of user roles, revoking inactive accounts, and implementing just-in-time access for sensitive operations.
User Authentication with OAuth 2.0 - This document explains OAuth 2.0 authentication, which enables secure delegated access without credential sharing. It covers configuring identity providers, token management with limited scope and lifetime, and secure storage of refresh tokens.
Container Security Guidelines - While primarily about container security, this document touches on access control aspects like running containers as non-root users and avoiding embedded credentials.
Incident Response Playbook - This mentions role assignment during incidents (incident commander, communications lead, etc.), which relates to access control in emergency scenarios.
Logging Standards for Microservices - This document includes guidance on avoiding logging sensitive information, which is relevant to authentication security.<p>La réponse est : « Super ! J'ai trouvé 5 documents pertinents sur les méthodes d'authentification et le contrôle d'accès basé sur les rôles. Voici ce qui a été découvert : »</p><p>L'appel d'outil renvoie les documents sources dans le cadre de sa charge utile de réponse, qui sont ensuite utilisés pour générer des citations.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltbaf48a9468770ce2/6a170c21509168bffee1bb14/25ff4c7e9563d99752f95540dafdc7fd211a66e3-800x530.gif" alt="Page Claude 4.5 Sonnet, avec des réponses qui défilent et incluent les cinq documents pertinents sur les méthodes d'authentification et le contrôle d'accès basé sur les rôles." /><p>Il est également possible d'enchaîner plusieurs outils dans une même interaction. Dans ce cas, Claude Desktop analyse la question de l’utilisateur et détermine qu’il doit d’abord appeler <code>search_docs</code> pour récupérer les documents pertinents, puis transmettre ces résultats à <code>summarize_and_cite</code> pour générer la réponse finale, le tout sans nécessiter d’invites séparées de la part de l’utilisateur :</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blta46ff45274e64192/6a170c230c4857a91501aac1/e6a8a46acb4236e77058f18bcd2f0737b5882c05-1999x1101.png" alt="Chat de bureau Claude, avec la note « Jeff revient », plus une nouvelle question d'utilisateur : « Quelles sont les principales recommandations pour améliorer l'authentification et le contrôle d'accès à travers nos systèmes ? » Incluez des références. »" /><p>Dans ce cas, pour la requête « <strong>Quelles sont les principales recommandations pour améliorer l’authentification et le contrôle d’accès dans l’ensemble de nos systèmes ? Veuillez inclure des références.</strong> », Nous avons obtenu les résultats suivants :</p>Based on the documentation, here are the main recommendations to improve authentication and access control across your systems:

Key Recommendations

1. Implement Role-Based Access Control (RBAC)
Ensure users have only the permissions necessary for their job functions. This principle of least privilege minimizes security risks by limiting access to what's truly needed. [1]

2. Regular Access Audits
Conduct regular audits of user roles and promptly revoke access for inactive accounts. This helps prevent unauthorized access from dormant or compromised accounts. [1]

3. Just-in-Time (JIT) Access
Implement just-in-time access for sensitive operations, where permissions are granted only when needed and for a limited duration. This reduces the attack surface by minimizing standing privileges. [1]

4. OAuth 2.0 for Secure Authentication
Use OAuth 2.0 to enable secure delegated access without sharing user credentials. This protocol allows third-party applications to access resources on behalf of users without exposing passwords. [2]

5. Token Security and Management
Configure your identity provider to issue tokens with limited scope and lifetime

Store refresh tokens securely
Validate access tokens consistently to maintain security [2]
References

Access Control and Role Management (Tags: security, access-control)
User Authentication with OAuth 2.0 (Tags: authentication, oauth)
These recommendations work together to create a defense-in-depth approach, where multiple security layers protect your systems from unauthorized access.<p>Comme à l’étape précédente, nous pouvons voir la réponse de chaque outil à cette question :</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt8f633c518e708a99/6a170c25ab7f082991db9ed6/cb606d356b2f7d5e4878a5eff71bc881869ac0ee-800x585.gif" alt="Page de chat de Claude Desktop, avec un texte défilant qui inclut la réponse de chaque outil à la question, « Quelles sont les principales recommandations pour améliorer l'authentification et le contrôle d'accès à travers nos systèmes ? » Incluez des références. »" /><p><em>Note : Si un sous-menu apparaît demandant si vous approuvez l’utilisation de chaque outil, sélectionnez </em><em><strong>Toujours autoriser</strong></em><em> ou </em><em><strong>Permettre une fois</strong></em><em>.</em></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt6627ee0bff1862df/6a170c266f7f040f6f91488c/aea942ba9b0037526ea215bec65690f1a5c3099c-1522x250.png" alt="Claude Desktop propose à l’utilisateur les options « Toujours autoriser » et « Autoriser une seule fois »." /><h2>Conclusion</h2><p>Les serveurs MCP représentent une étape importante vers la standardisation des outils LLM pour les applications locales et distantes. Bien que la compatibilité totale soit encore en cours de développement, nous avançons rapidement dans cette direction.</p><p>Dans cet article, nous avons appris à créer un serveur MCP personnalisé en TypeScript qui connecte Elasticsearch aux applications basées sur LLM. Notre serveur propose deux outils : <code>search_docs</code> pour récupérer les documents pertinents à l'aide de Query DSL ; et <code>summarize_and_cite</code> pour générer des résumés avec des citations via des modèles OpenAI et Claude Desktop comme interface utilisateur client.</p><p>L'avenir de la compatibilité entre les différents fournisseurs côté client et côté serveur semble prometteur. Les prochaines étapes consistent à ajouter davantage de fonctionnalités et de flexibilité à votre agent. Vous trouverez un <a href="https://www.elastic.co/search-labs/blog/llm-functions-elasticsearch-intelligent-query">article</a> pratique expliquant comment paramétrer vos requêtes à l'aide de modèles de rechercher pour gagner en précision et en flexibilité.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/elastic-mcp-server-typescript-claude</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/elastic-mcp-server-typescript-claude</guid>
    <category><![CDATA[IA agentique]]></category>
    <category><![CDATA[Intégrations]]></category>
    <dc:creator><![CDATA[Jeffrey Rengifo]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt5600198cb47666a5/6a170c28509168ce3ae1bb18/0bb24c05fff391f42070c2883182ea6fe9cb9680-1280x720.png" length="0" type="image/png"/>
    <pubDate>Fri, 27 Mar 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Utilisation de l'API d'inférence Elasticsearch avec les modèles Hugging Face]]></title>
    <description><![CDATA[Découvrez comment connecter Elasticsearch aux modèles Hugging Face à l'aide de points de terminaison d'inférence, et comment créer un système de recommandation de blogs multilingue avec recherche sémantique et complétion de chat.]]></description>
    <content:encoded><![CDATA[<p>Dans ses dernières mises à jour, Elasticsearch a introduit une intégration native permettant de se connecter aux modèles hébergés sur le <a href="https://endpoints.huggingface.co/">service d'inférence Hugging Face</a>. Dans cet article, nous verrons comment configurer cette intégration et effectuer des inférences via de simples appels d'API à l'aide d'un grand modèle de langage (LLM). Nous utiliserons <a href="https://huggingface.co/HuggingFaceTB/SmolLM3-3B">SmolLM3-3B</a>, un modèle léger et polyvalent offrant un bon compromis entre consommation de ressources et qualité des réponses.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt9094997548bd70f8/6a170d6a839dfa0ad6dcff54/7ddadf1976421a860a7d62087239adb9150d808b-1999x1388.png" alt="Diagramme de dispersion présentant plusieurs petits modèles de langage, classés selon leur taille (en milliards de paramètres) en abscisse et leur taux de réussite (en pourcentage) en ordonnée. Le modèle SmolLM3-3B se distingue par une efficacité supérieure, avec un taux de réussite plus élevé que les autres modèles de taille similaire." /><h2>Produits requis</h2><ul><li><p><strong>Elasticsearch 9.3 ou Elastic Cloud Serverless</strong> : vous pouvez créer un déploiement dans le cloud en suivant <a href="https://www.elastic.co/search-labs/tutorials/install-elasticsearch/elastic-cloud">ces instructions</a>, ou utiliser le démarrage rapide <a href="https://www.elastic.co/docs/deploy-manage/deploy/self-managed/local-development-installation-quickstart#local-dev-quick-start"><code>start-local</code></a> à la place.</p></li><li><p><strong>Python 3.12</strong> : téléchargez <a href="https://www.python.org/">Python ici</a>.</p></li><li><p><strong>Jeton d'accès Hugging Face</strong><a href="https://huggingface.co/docs/hub/en/security-tokens"></a>.</p></li></ul><h2>Complétions de chat utilisant un point de terminaison d'inférence Hugging Face</h2><p>Nous allons d'abord créer un exemple pratique connectant Elasticsearch à un <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-inference-put">point de terminaison d'inférence</a> Hugging Face afin de générer des recommandations alimentées par l'IA à partir d'une collection d'articles de blog. Pour la base de connaissances de l'application, nous utiliserons un ensemble de données d'articles de blogs d'entreprise, qui contiennent des informations précieuses mais souvent difficiles à consulter.</p><p>Avec ce point de terminaison, la <a href="https://www.elastic.co/docs/solutions/search/semantic-search">recherche sémantique</a> extrait les articles les plus pertinents pour une requête donnée, et un LLM Hugging Face génère de courtes recommandations contextuelles sur la base de ces résultats.</p><p>Examinons d'abord les grandes lignes du flux d'informations que nous allons mettre en place :</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf217b7b7db4e1e6c/6a170d6ca929cf8022ae0a3b/1dfbc2323438feaaa42e13ab242dd1f7166f74aa-1200x676.png" alt="Diagramme de flux montrant un index Elasticsearch alimentant un point de terminaison d'inférence avec les résultats de recherche sémantique, lequel renvoie des recommandations d'articles." /><p>Dans cet article, nous allons tester la capacité de <strong>SmolLM3-3B </strong>àà allier sa taille compacte à de puissantes fonctionnalités de raisonnement multilingue et d'appel d'outils. À partir d'une requête de recherche, nous enverrons tous les contenus correspondants (en anglais et en espagnol) au LLM afin de générer une liste d'articles recommandés, accompagnés d'une description personnalisée basée sur la requête et les résultats de recherche.</p><p>Voici à quoi pourrait ressembler l'interface utilisateur d'un site d'articles doté d'un système de génération de recommandations par IA.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt20e69b9a06fecd65/6a170d6e839dfa6f97dcff58/8d3b86b212f28ff279f2da67a33e6134039f0e4e-1999x949.png" alt="Interface utilisateur d'un site d'articles doté d'un système de génération de recommandations par IA, présentant trois exemples, avec un texte en anglais et des titres en anglais ou en espagnol." /><p>Vous trouverez la mise en œuvre complète de cette application dans le <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/elasticsearch-inference-api-and-hugging-face/notebook.ipynb">notebook</a> associé.</p><h3>Configuration des points de terminaison d’inférence Elasticsearch</h3><p>Pour utiliser le <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-inference-put-hugging-face">point de terminaison d'inférence Hugging Face d'Elasticsearch</a>, nous avons besoin de deux éléments importants : une clé API Hugging Face et une URL de point de terminaison Hugging Face en cours d'exécution. Cela devrait ressembler à ceci :</p>PUT _inference/chat_completions/hugging-face-smollm3-3b
{
    "service": "hugging_face",
    "service_settings": {
        "api_key": "hugging-face-access-token", 
        "url": "url-endpoint" 
    }
}<p>Le point de terminaison d'inférence Hugging Face dans Elasticsearch prend en charge différents types de tâches : <code>text_embedding</code>, <code>completion</code>, <code>chat_completion</code> et <code>rerank</code>. Dans cet article de blog, nous utilisons <code>chat_completion</code>, car nous avons besoin que le modèle génère des recommandations conversationnelles basées sur les résultats de recherche et un prompt système. Ce point de terminaison nous permet d'effectuer des complétions de chat directement depuis Elasticsearch de manière simple grâce à l'API Elasticsearch :</p>POST _inference/chat_completion/hugging-face-smollm3-3b/_stream
{
  "messages": [
      { "role": "user", "content": "&lt;user prompt&gt;" }
  ]
}<p>Ceci va constituer le cœur de l'application, recevant la requête et les résultats de recherche qui seront ensuite traités par le modèle. La théorie étant posée, passons à la mise en œuvre de l'application.</p><h4>Configuration du point de terminaison d'inférence sur Hugging Face</h4><p>Pour déployer le modèle Hugging Face, nous allons utiliser le <a href="https://huggingface.co/inference-endpoints/dedicated">service de déploiement en un clic de Hugging Face</a>, une solution simple et rapide pour déployer des points de terminaison de modèles. Notez qu'il s'agit d'un service payant et que son utilisation peut engendrer des coûts supplémentaires. Cette étape créera l'instance du modèle qui servira à générer les recommandations d'articles.</p><p>Vous pouvez choisir un modèle dans le catalogue accessible en un clic :</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blta7bdfa43d6766324/6a170d6fb339d59e5476a039/b816e9fba1fe172687bf58f5143fb1f838c1077f-549x331.png" alt="Vue d'interface d'un catalogue de modèles filtré sur &quot;smoll3&quot;, montrant un modèle nommé &quot;smollm3‑3b&quot; avec génération de texte, vLLM, GPU 1× Nvidia L4 et un prix indiqué de 0,8 $, ainsi qu'une note suggérant d'étendre la recherche à tous les modèles Hugging Face." /><p>Sélectionnons le modèle <strong>SmolLM3-3B</strong> :</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltdb0a2e6ffd7deb20/6a170d710c48574b7401aafc/610d3aba0429f3666c2df3616d513eb6a4397c0c-502x478.png" alt="Interface permettant de créer un point de terminaison pour le modèle SmolLM3‑3B, affichant le nom du modèle, une note &quot;vérifié par Hugging Face&quot;, un champ de nom de point de terminaison, un coût de 0,80 $ par heure et par réplique en cours d'exécution, une option cURL et un bouton &quot;Créer un point de terminaison&quot;." /><p>À partir d'ici, veuillez récupérer l'URL du point de terminaison Hugging Face :</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt25714021711ed6ff/6a170d72c1e8a54853f88336/025094ddb2cfbd1f0f216a5ec4e119b0f4fa2c42-646x328.png" alt="Vue du tableau de bord d'un point de terminaison d'inférence Hugging Face nommé &quot;smollm3‑3b‑pnz&quot;, affichant un statut d'exécution vert, une réplique active, zéro requête au cours de la dernière heure, des onglets de navigation et l'URL du point de terminaison affiché." /><p>Comme indiqué dans la <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-inference-put-hugging-face">documentation Elasticsearch relative aux points de terminaison d'inférence Hugging Face</a>, la génération de texte nécessite un modèle compatible avec l'API OpenAI. Pour cette raison, nous devons ajouter le sous-chemin <code>/v1/chat/completions</code> à à l'URL de point de terminaison Hugging Face. Le résultat final ressemblera à ceci :</p>https://j2g31h0futopfkli.us-east-1.aws.endpoints.huggingface.cloud/v1/chat/completions<p>Une fois ces éléments en place, nous pouvons commencer à coder dans un notebook Python.</p><h4>Génération de la clé API Hugging Face</h4><p>Créez un <a href="https://huggingface.co/join">compte Hugging Face</a> et obtenez un jeton API en suivant <a href="https://huggingface.co/docs/hub/en/security-tokens#user-access-tokens">ces instructions</a>. Vous avez le choix entre trois types de jetons : un jeton <em>granulaire</em> (recommandé pour la production, car il ne donne accès qu'à des ressources spécifiques), un jeton de <em>lecture</em> (pour un accès en lecture seule) ou un jeton d'<em>écriture</em> (pour un accès en lecture et en écriture). Pour ce tutoriel, un jeton de lecture suffit, car nous n'avons besoin d'appeler que le point de terminaison d'inférence. Enregistrez cette clé pour la prochaine étape.</p><h4>Configuration du point de terminaison d'inférence Elasticsearch</h4><p>Tout d'abord, déclarons un client Elasticsearch Python :</p>os.environ["ELASTICSEARCH_API_KEY"] = "your-elasticsearch-api-key"
os.environ["ELASTICSEARCH_URL"] = "https://xxxx.us-central1.gcp.cloud.es.io:443"

es_client = Elasticsearch(
    os.environ["ELASTICSEARCH_URL"], api_key=os.environ["ELASTICSEARCH_API_KEY"]
)<p>Ensuite, nous allons créer un point de terminaison d'inférence Elasticsearch qui utilise le modèle Hugging Face. Ce point de terminaison nous permettra de générer des réponses en fonction des articles de blog et du prompt transmis au modèle.</p>INFERENCE_ENDPOINT_ID = "smollm3-3b-pnz"

os.environ["HUGGING_FACE_INFERENCE_ENDPOINT_URL"] = (
 "https://j2g31h0futopfkli.us-east-1.aws.endpoints.huggingface.cloud/v1/chat/completions"
)
os.environ["HUGGING_FACE_API_KEY"] = "hf_xxxxx"

resp = es_client.inference.put(
        task_type="chat_completion",
        inference_id=INFERENCE_ENDPOINT_ID,
        body={
            "service": "hugging_face",
            "service_settings": {
                "api_key": os.environ["HUGGING_FACE_API_KEY"],
                "url": os.environ["HUGGING_FACE_INFERENCE_ENDPOINT_URL"],
            },
        },
    )<h3>Ensemble de données</h3><p>L'ensemble de données contient les <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/elasticsearch-inference-api-and-hugging-face/dataset.json">articles de blog</a> sur lesquels des requêtes seront exécutées ; il s'agit d'un ensemble de contenus multilingues utilisé tout au long du workflow :</p>// Articles dataset document example: 
{
    "id": "6",
    "title": "Complete guide to the new API: Endpoints and examples",
    "author": "Tomas Hernandez",
    "date": "2025-11-06",
    "category": "tutorial",
    "content": "This guide describes in detail all endpoints of the new API v2. It includes code examples in Python, JavaScript, and cURL for each endpoint. We cover authentication, resource creation, queries, updates, and deletion. We also explain error handling, rate limiting, and best practices. Complete documentation is available on our developer portal."
  }<h4>Mappings Elasticsearch</h4><p>Une fois l'ensemble de données défini, nous devons créer un schéma de données adapté à la structure des articles de blog. Les <a href="https://www.elastic.co/docs/manage-data/data-store/mapping">mappings d'index</a> suivants seront utilisés pour stocker les données dans Elasticsearch :</p>INDEX_NAME = "blog-posts"

mapping = {
    "mappings": {
        "properties": {
            "id": {"type": "keyword"},
            "title": {
                "type": "object",
                "properties": {
                    "original": {
                        "type": "text",
                        "copy_to": "semantic_field",
                        "fields": {"keyword": {"type": "keyword"}},
                    },
                    "translated_title": {
                        "type": "text",
                        "fields": {"keyword": {"type": "keyword"}},
                    },
                },
            },
            "author": {"type": "keyword", "copy_to": "semantic_field"},
            "category": {"type": "keyword", "copy_to": "semantic_field"},
            "content": {"type": "text", "copy_to": "semantic_field"},
            "date": {"type": "date"},
            "semantic_field": {"type": "semantic_text"},
        }
    }
}


es_client.indices.create(index=INDEX_NAME, body=mapping)<p>Ici, nous pouvons voir plus clairement comment les données sont structurées. Nous utiliserons la recherche sémantique pour récupérer les résultats basés sur le langage naturel, ainsi que la propriété <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/copy-to"><code>copy_to</code></a> pour copier le contenu du champ dans le champ <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/semantic-text"><code>semantic_text</code></a>. De plus, le champ <code>title</code> contient deux sous-champs : le sous-champ <code>original</code> stocke le titre en anglais ou en espagnol, selon la langue d'origine de l'article, et le sous-champ <code>translated_title</code> n'est présent que pour les articles en espagnol et contient la traduction anglaise du titre original.</p><h3>Ingestion des données</h3><p>L'extrait de code suivant ingère l'ensemble de données des articles de blog dans Elasticsearch à l'aide de l'<a href="https://www.elastic.co/docs/reference/elasticsearch/clients/javascript/bulk_examples">API Bulk</a> :</p>def build_data(json_file, index_name):
    with open(json_file, "r") as f:
        data = json.load(f)

    for doc in data:
        action = {"_index": index_name, "_source": doc}
        yield action


try:
    success, failed = helpers.bulk(
        es_client,
        build_data("dataset.json", INDEX_NAME),
    )
    print(f"{success} documents indexed successfully")

    if failed:
        print(f"Errors: {failed}")
except Exception as e:
    print(f"Error: {str(e)}")<p>Maintenant que nous avons intégré les articles dans Elasticsearch, nous devons créer une fonction capable de rechercher dans le champ <code>semantic_text</code> :</p>def perform_semantic_search(query_text, index_name=INDEX_NAME, size=5):
    try:
        query = {
            "query": {
                "match": {
                    "semantic_field": {
                        "query": query_text,
                    }
                }
            },
            "size": size,
        }

        response = es_client.search(index=index_name, body=query)
        hits = response["hits"]["hits"]

        return hits
    except Exception as e:
        print(f"Semantic search error: {str(e)}")
        return []<p>Nous avons également besoin d'une fonction qui appelle le point de terminaison d'inférence. Dans ce cas, nous appellerons le point de terminaison en utilisant le type de tâche <strong><code>chat_completion</code></strong>pour obtenir des réponses en streaming :</p>def stream_chat_completion(messages: list, inference_id: str = INFERENCE_ENDPOINT_ID):
    url = f"{ELASTICSEARCH_URL}/_inference/chat_completion/{inference_id}/_stream"
    payload = {"messages": messages}
    headers = {
        "Authorization": f"ApiKey {ELASTICSEARCH_API_KEY}",
        "Content-Type": "application/json",
    }

    try:
        response = requests.post(url, json=payload, headers=headers, stream=True)
        response.raise_for_status()

        for line in response.iter_lines(decode_unicode=True):
            if line:
                line = line.strip()

                if line.startswith("event:"):
                    continue

                if line.startswith("data: "):
                    data_content = line[6:]

                    if not data_content.strip() or data_content.strip() == "[DONE]":
                        continue

                    try:
                        chunk_data = json.loads(data_content)

                        if "choices" in chunk_data and len(chunk_data["choices"]) &gt; 0:
                            choice = chunk_data["choices"][0]
                            if "delta" in choice and "content" in choice["delta"]:
                                content = choice["delta"]["content"]
                                if content:
                                    yield content

                    except json.JSONDecodeError as json_err:
                        print(f"\nJSON decode error: {json_err}")
                        print(f"Problematic data: {data_content}")
                        continue

    except requests.exceptions.RequestException as e:
        yield f"Error: {str(e)}"<p>Nous pouvons maintenant écrire une fonction qui appelle la fonction de recherche sémantique, ainsi que le point de terminaison d'inférence <code>chat_completions</code> et le point de terminaison de recommandations, afin de générer les données qui seront allouées dans les fiches :</p>def recommend_articles(search_query, index_name=INDEX_NAME, max_articles=5):
    print(f"\n{'='*80}")
    print(f"🔍 Search Query: {search_query}")
    print(f"{'='*80}\n")

    articles = perform_semantic_search(search_query, index_name, size=max_articles)

    if not articles:
        print("❌ No relevant articles found.")
        return None, None

    print(f"✅ Found {len(articles)} relevant articles\n")

    # Build context with found articles
    context = "Available blog articles:\n\n"
    for i, article in enumerate(articles, 1):
        source = article.get("_source", article)
        context += f"Article {i}:\n"
        context += f"- Title: {source.get('title', 'N/A')}\n"
        context += f"- Author: {source.get('author', 'N/A')}\n"
        context += f"- Category: {source.get('category', 'N/A')}\n"
        context += f"- Date: {source.get('date', 'N/A')}\n"
        context += f"- Content: {source.get('content', 'N/A')}\n\n"

    system_prompt = """You are an expert content curator that recommends blog articles.

    Write recommendations in a conversational style starting with phrases like:
    - "If you're interested in [topic], this article..."
    - "This post complements your search with..."
    - "For those looking into [topic], this article provides..."


    FORMAT REQUIREMENTS:
    - Return ONLY a JSON array
    - Each element must have EXACTLY these three fields: "article_number", "title", "recommendation"
    - If the original title is in spanish, use the "translated_title" subfield in the "title" field

    Keep each recommendation concise (2-3 sentences max) and focused on VALUE to the reader.

    EXAMPLE OF CORRECT FORMAT:
    [
        {"article_number": 1, "title": "Article title in english", "recommendation": "If you are interested in [topic], this article provides..."},
        {"article_number": 2, "title": "Article title in english", "recommendation": " for those looking into [topic], this article provides..."}
    ]

    Return ONLY the JSON array following this exact structure."""

    user_prompt = f"""Search query: "{search_query}"

    Generate recommendations for the following articles: {context}
    """

    messages = [
        {"role": "system", "content": "/no_think"},
        {"role": "system", "content": system_prompt},
        {"role": "user", "content": user_prompt},
    ]

    # LLM generation
    print(f"{'='*80}")
    print("🤖 Generating personalized recommendations...\n")

    full_response = ""

    for chunk in stream_chat_completion(messages):
        print(chunk, end="", flush=True)
        full_response += chunk

    return context, articles, full_response<p>Enfin, nous devons extraire les informations et les mettre en forme pour l'impression :</p>def display_recommendation_cards(articles, recommendations_text):
    print("\n" + "=" * 100)
    print("📇 RECOMMENDED ARTICLES".center(100))
    print("=" * 100 + "\n")

    # Parse JSON recommendations - clean tags and extract JSON
    recommendations_list = []
    try:

        # Clean up &lt;think&gt; tags
        cleaned_text = re.sub(
            r"&lt;think&gt;.*?&lt;/think&gt;", "", recommendations_text, flags=re.DOTALL
        )
        # Remove markdown code blocks ( ... ``` or ``` ... ```)
        cleaned_text = re.sub(r"```(?:json)?", "", cleaned_text)
        cleaned_text = cleaned_text.strip()

        parsed = json.loads(cleaned_text)

        # Extract recommendations from list format
        for item in parsed:
            article_number = item.get("article_number")
            title = item.get("title", "")
            rec_text = item.get("recommendation", "")

            if article_number and rec_text:
                recommendations_list.append(
                    {
                        "article_number": article_number,
                        "title": title,
                        "recommendation": rec_text,
                    }
                )
    except json.JSONDecodeError as e:
        print(f"⚠️  Could not parse recommendations as JSON: {e}")
        return

    for i, article in enumerate(articles, 1):
        source = article.get("_source", article)

        # Card border
        print("┌" + "─" * 98 + "┐")

        # Find recommendation and title for this article number
        recommendation = None
        title = None
        for rec in recommendations_list:
            if rec.get("article_number") == i:
                recommendation = rec.get("recommendation")
                title = rec.get("title")
                break

        # Print title
        title_lines = textwrap.wrap(f"📌 {title}", width=94)
        for line in title_lines:
            print(f"│  {line}".ljust(99) + "│")

        # Card border
        print("├" + "─" * 98 + "┤")

        # Print recommendation
        if recommendation:
            recommendation_lines = textwrap.wrap(recommendation, width=94)
            for line in recommendation_lines:
                print(f"│  {line}".ljust(99) + "│")

        # Card bottom
        print("└" + "─" * 98 + "┘")<p>Faisons un test en posant une question sur les articles de blog relatifs à la sécurité :</p>search_query = "Security and vulnerabilities"

context, articles, recommendations = recommend_articles(search_query)

print("\nElasticsearch context:\n", context)

# Display visual cards
display_recommendation_cards(articles, recommendations)<p>Nous pouvons voir ici les fiches générées par le workflow dans la console :</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt4aa221a08a51aeb3/6a170d7460084be1413c45d6/730d35212594bb3db30447c3ea7e2a92857287b7-1999x1515.png" alt="Section intitulée &quot;Articles recommandés&quot; présentant cinq résumés d'articles en encadré, portant sur une vulnérabilité du système d'authentification, les risques de migration, les améliorations des performances et de l'authentification de l'API REST v2, les modifications du système de notification et un guide complet de la nouvelle API." /><p>Vous trouverez les résultats complets, y compris tous les résultats et la réponse du LLM dans <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/elasticsearch-inference-api-and-hugging-face/results.md">ce fichier</a>.</p><p>Nous recherchons des articles portant sur le thème "Security et vulnérabilités". Cette question est utilisée comme requête de recherche sur les documents stockés dans Elasticsearch. Les résultats récupérés sont ensuite transmis au modèle, qui génère des recommandations basées sur leur contenu. Comme nous pouvons le constater, le modèle a parfaitement réussi à générer des textes courts et attrayants qui incitent le lecteur à cliquer dessus.</p><h2>Conclusion</h2><p>Cet exemple illustre comment combiner Elasticsearch et Hugging Face pour créer un système centralisé, rapide et performant pour les applications d'IA. Cette approche réduit les interventions manuelles et offre une grande flexibilité grâce au vaste catalogue de modèles de Hugging Face. L'utilisation de SmolLM3-3B, en particulier, montre comment des modèles multilingues compacts peuvent fournir un raisonnement pertinent et une génération de contenu efficace lorsqu'ils sont associés à la recherche sémantique. Ensemble, ces outils constituent une base scalable et performante pour le développement d'applications d'analyse de contenu intelligentes et multilingues.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/hugging-face-elasticsearch-inference-api</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/hugging-face-elasticsearch-inference-api</guid>
    <category><![CDATA[IA agentique]]></category>
    <category><![CDATA[Intégrations]]></category>
    <dc:creator><![CDATA[Jeffrey Rengifo]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt5f961af4cb26ec97/6a170d767d8d6790c770e790/1417d6ff033712206c9bd4bcc22074ee3437ce96-1999x1125.png" length="0" type="image/png"/>
    <pubDate>Mon, 23 Mar 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Créez un workflow de recherche IA financière avec LangGraph.js et Elasticsearch]]></title>
    <description><![CDATA[Apprenez à utiliser LangGraph.js avec Elasticsearch pour créer un workflow de recherche financière alimenté par l'IA qui transforme les requêtes en langage naturel en filtres dynamiques et conditionnels pour l'analyse des investissements et du marché.]]></description>
    <content:encoded><![CDATA[<p>La création d'applications de recherche IA implique souvent la coordination de plusieurs tâches, la récupération et l'extraction de données dans un workflow fluide. LangGraph simplifie ce processus en permettant aux développeurs d'orchestrer les agents d'IA à l'aide d'une structure basée sur des nodes. Dans cet article, nous allons construire une solution financière en utilisant <a href="https://langchain-ai.github.io/langgraphjs/">LangGraph.js</a>.</p><h2>Qu'est-ce que LangGraph ?</h2><p><a href="https://langchain-ai.github.io/langgraphjs/">LangGraph</a> est un framework pour construire des agents d’IA et les orchestrer dans un workflow afin de créer des applications assistées par l’IA. LangGraph dispose d’une architecture de nodes où nous pouvons déclarer des fonctions représentant des tâches et les assigner comme nodes du workflow. Le résultat de l'interaction de plusieurs nodes sera un graphe. LangGraph fait partie du <a href="https://js.langchain.com/docs/introduction/">LangChain</a> écosystème plus large, qui fournit des outils pour construire des systèmes d'IA modulaires et composables.</p><p>Pour mieux comprendre l’utilité de LangGraph, résolvons une situation problématique en l’utilisant.</p><h2>Aperçu de la solution</h2><p>Dans une société de capital-risque, les investisseurs ont accès à une vaste base de données avec de nombreuses options de filtrage, mais lorsqu'ils veulent combiner des critères, cela devient difficile et lent. Il se peut donc que certaines start-ups pertinentes ne soient pas trouvées pour l'investissement. Cela conduit à passer beaucoup de temps à essayer d'identifier les meilleurs candidats, voire à perdre des opportunités.</p><p>Avec LangGraph et Elasticsearch, vous pouvez effectuer des recherches filtrées en utilisant le langage naturel, ce qui évite aux utilisateurs de devoir construire manuellement des requêtes complexes avec des dizaines de filtres. Pour plus de flexibilité, le workflow choisit automatiquement, en fonction de l'entrée de l'utilisateur, entre deux types de requêtes :</p><ul><li><p><strong>Requêtes d’investissement</strong> : elles visent les données financières et de financement des start-up, notamment les <a href="https://www.investopedia.com/articles/personal-finance/102015/series-b-c-funding-what-it-all-means-and-how-it-works.asp">tours de table</a>, la valorisation ou le <a href="https://www.investopedia.com/terms/r/revenue.asp">CA</a>. <em>Exemple :</em> « Trouvez des startups avec un financement de série A ou série B entre 8 millions et 25 millions de dollars et un chiffre d’affaires mensuel supérieur à 500 000 $. »</p></li><li><p><strong>Requêtes axées sur le marché</strong>: elles se concentrent sur <a href="https://en.wikipedia.org/wiki/Vertical_market">les secteurs d’activité</a>, <a href="https://en.wikipedia.org/wiki/Target_market">les marchés géographiques</a> ou <a href="https://www.investopedia.com/terms/b/businessmodel.asp">les modèles économiques</a>, en aidant à identifier des opportunités dans des secteurs ou régions spécifiques. <em>Exemple :</em> « Trouvez des startups de la fintech et de la santé à San Francisco, New York ou Boston. »</p></li></ul><p>Pour garantir la robustesse des requêtes, nous allons faire en sorte que le LLM génère des <a href="https://www.elastic.co/docs/solutions/search/search-templates">modèles de recherche</a> au lieu de <a href="https://www.elastic.co/docs/explore-analyze/query-filter/languages/querydsl">requêtes DSL</a> complètes. De cette façon, vous obtenez toujours la requête souhaitée, et le LLM n'a qu'à compléter les informations manquantes sans avoir à élaborer la requête dont vous avez besoin à chaque fois.</p><h2>Ce dont vous avez besoin pour commencer</h2><ul><li><p>Clé API Elasticsearch</p></li><li><p>Clé d'API OpenAPI</p></li><li><p>Node 18 ou version ultérieure</p></li></ul><h2>Instructions étape par étape</h2><p>Dans cette section, voyons comment l'application sera présentée. Nous utiliserons <a href="https://www.typescriptlang.org/">TypeScript</a>, un sur-ensemble de JavaScript qui ajoute des types statiques. Cela rend le code plus fiable, plus facile à maintenir et plus sûr en détectant les erreurs dès le début, tout en assurant une compatibilité totale avec JavaScript.</p><p>Le flux des nœuds se présentera comme suit :</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt90db8f03f372608c/6a170986dc55de6e16e00d93/b47d7f238c4964a6febc0de7fe5e68b186f539c3-363x555.png" alt="" /><p>L'image ci-dessus est générée par LangGraph et représente le workflow qui définit l'ordre d'exécution et la logique conditionnelle entre les nodes :</p><ul><li><p><strong>decideStrategy : </strong>utilise un LLM pour rechercher la requête de l'utilisateur et choisir entre deux stratégies de recherche spécialisées, axée sur l'investissement ou axée sur le marché.</p></li><li><p><strong>PrepareInvestmentSearch : </strong>extrait les valeurs de filtre de la requête et crée un modèle prédéfini mettant l'accent sur les paramètres financiers et liés au financement.</p></li><li><p><strong>PrepareMarketSearch</strong>: extrait également les valeurs des filtres, mais crée dynamiquement des paramètres en mettant l'accent sur le marché, le secteur et le contexte géographique.</p></li><li><p><strong>ExecuteSearch : </strong>envoie la recherche construite à Elasticsearch à l'aide d'un modèle de recherche et extrait les documents de démarrage correspondants.</p></li><li><p><strong>VisualiserResults : </strong>met en forme les résultats finaux sous la forme d'un résumé clair et lisible présentant les principaux attributs de la start-up tels que le financement, le secteur d'activité et le chiffre d'affaires.</p></li></ul><p>Ce flux comprend un <a href="https://langchain-ai.github.io/langgraphjs/how-tos/branching/?h=conditional#how-to-create-branches-for-parallel-node-execution">branchement conditionnel</a>, fonctionnant comme une instruction « si », qui détermine s'il faut rechercher le chemin d'investissement ou de recherche de marché en fonction de l'entrée de l'utilisateur. Cette logique de décision, pilotée par le LLM, rend le workflow adaptatif et conscient du contexte, un mécanisme que nous explorerons plus en détail dans les sections suivantes.</p><h3>État de LangGraph</h3><p>Avant de voir chaque node individuellement, nous devons comprendre comment les nodes communiquent et partagent les données. Pour cela, LangGraph nous permet de définir l'état du workflow. Cela définit l'état partagé qui sera transmis entre les nodes.</p><p>L’état agit comme un conteneur partagé stockant les données intermédiaires du workflow : il enregistre d’abord la requête en langage naturel de l’utilisateur, puis la stratégie de recherche choisie, les paramètres prêts pour Elasticsearch, les résultats de recherche et, pour finir, le résultat formaté.</p><p>Cette architecture permet à chaque nœud de lire et de modifier l’état, ce qui garantit un flux d’informations constant, de l’entrée de l’utilisateur jusqu’à la visualisation finale.</p>const VCState = Annotation.Root({
  input: Annotation&lt;string&gt;(), // User's natural language query
  searchStrategy: Annotation&lt;string&gt;(), // Search strategy chosen by LLM
  searchParams: Annotation&lt;any&gt;(), // Prepared search parameters
  results: Annotation&lt;any[]&gt;(), // Search results
  final: Annotation&lt;string&gt;(), // Final formatted response
});<h3>Configurer l'application</h3><p>Tout le code de cette section se trouve dans le <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/langgraph-js-elasticsearch">dépôt elasticsearch-labs</a>.</p><p>Dans le dossier où l’application sera installée, ouvrez un terminal et initialisez une application Node.js avec la commande :</p>npm init -y<p>Nous pouvons maintenant installer les dépendances nécessaires à ce projet :</p>npm install @elastic/elasticsearch @langchain/langgraph @langchain/openai @langchain/core dotenv zod &amp;&amp; npm install --save-dev @types/node tsx typescript<ul><li><p><strong><code>@elastic/elasticsearch</code></strong>: Permet de gérer les requêtes Elasticsearch, comme l’ingestion et la récupération des données.</p></li><li><p><strong><code>@langchain/langgraph</code></strong>: Dépendance JS pour fournir tous les outils LangGraph.</p></li><li><p><strong><code>@langchain/openai</code></strong>: Client OpenAI LLM pour LangChain.</p></li><li><p>@langchain/core : Offre les composantes de base essentielles aux applications LangChain, notamment les modèles d’invite.</p></li><li><p><strong><code>dotenv</code></strong>: Dépendance nécessaire pour utiliser les variables d'environnement en JavaScript.</p></li><li><p><strong><code>zod</code></strong>: Dépendance au type de données.</p></li></ul><p><code>@types/node</code> <code>tsx</code> <code>typescript</code> nous permet d'écrire et d'exécution du code TypeScript.</p><p>Créez maintenant les fichiers suivants :</p><ul><li><p><code>elasticsearchSetup</code><a href="http://ingest.ts/"><code>.ts</code></a>: Créera les mapping d'index, chargera les données à partir d'un fichier JSON, et ingérera les données dans Elasticsearch.</p></li><li><p><a href="http://main.ts/"><code>main.ts</code></a>: inclura l’application LangGraph.</p></li><li><p><code>.env</code>: fichier pour stocker les variables d’environnement</p></li></ul><p>Dans le fichier <code>.env</code>, ajoutons les variables d’environnement suivantes :</p>ELASTICSEARCH_ENDPOINT="your-endpoint-here"
ELASTICSEARCH_API_KEY="your-key-here"
OPENAI_API_KEY="your-key-here"<p>La clé APIK de l'OpenAPI ne sera pas utilisée directement dans le code ; elle sera utilisée en interne par la bibliothèque <code>@langchain/openai</code>.</p><p>Toute la logique concernant la création de mappages, la création de modèles de recherche et l’ingestion des ensembles de données se trouve dans le fichier <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/langgraph-js-elasticsearch/elasticsearchSetup.ts"><code>elasticsearchSetup.ts</code></a>. Dans les prochaines étapes, nous nous concentrerons sur le fichier <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/langgraph-js-elasticsearch/main.ts"><code>main.ts</code></a>. Vous pouvez également consulter l'ensemble de données pour mieux comprendre l'aspect des données sur le site <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/langgraph-js-elasticsearch/dataset.json"><code>dataset.json</code></a>.</p><h3>Application LangGraph</h3><p>Dans le fichier <code>main.ts</code>, importons certaines dépendances nécessaires pour consolider l'application LangGraph. Dans ce fichier, vous devez également inclure les fonctions node et la déclaration d’état. La déclaration du graphe sera effectuée dans une méthode <code>main</code> dans les prochaines étapes. Le fichier <code>elasticsearchSetup.ts</code> contiendra les aides Elasticsearch que nous allons utiliser dans les Nodes dans les étapes suivantes.</p>import { writeFileSync } from "node:fs";
import { StateGraph, Annotation, START, END } from "@langchain/langgraph";
import { ChatOpenAI } from "@langchain/openai";
import { z } from "zod";
import {
  esClient,
  ingestDocuments,
  createSearchTemplates,
  INDEX_NAME,
  INVESTMENT_FOCUSED_TEMPLATE,
  MARKET_FOCUSED_TEMPLATE,
  createIndex,
} from "./elasticsearchSetup.js";

const llm = new ChatOpenAI({ model: "gpt-4o-mini" });<p>Ainsi que nous l’avons vu, le client LLM sera mobilisé pour générer les paramètres du modèle de recherche Elasticsearch en fonction de la question de l’utilisateur.</p>async function saveGraphImage(app: any): Promise&lt;void&gt; {
  try {
    const drawableGraph = app.getGraph();
    const image = await drawableGraph.drawMermaidPng();
    const arrayBuffer = await image.arrayBuffer();

    const filePath = "./workflow_graph.png";
    writeFileSync(filePath, new Uint8Array(arrayBuffer));
    console.log(`📊 Workflow graph saved as: ${filePath}`);
  } catch (error: any) {
    console.log("⚠️  Could not save graph image:", error.message);
  }
}<p>La méthode ci-dessus génère l'image du graphe au format png et utilise l'<a href="https://mermaid.ink/">API Mermaid.INK</a> en arrière-plan. Ceci est utile si vous souhaitez voir comment les nodes de l'application interagissent dans le cadre d'une visualisation stylisée.</p><h3>Nodes LangGraph</h3><p>À présent, voyons chaque node en détail :</p><h3>Node decideSearchStrategy</h3><p>Le node <code>decideSearchStrategy</code> analyse les entrées de l'utilisateur et détermine s'il convient d'effectuer une rechercher axée sur les investissements ou axée sur le marché. Il utilise un LLM avec un schéma de sortie structuré (défini avec Zod) pour classer le type de requête. Avant de prendre la décision, il récupère les filtres disponibles de l'index en utilisant une agrégation, en garantissant que le modèle dispose d'un contexte à jour sur les industries, les localisations et les données de financement.</p><p>Pour extraire les valeurs possibles des filtres et les envoyer au LLM, utilisons une requête d'<a href="https://www.elastic.co/docs/explore-analyze/query-filter/aggregations">agrégation</a> pour les récupérer directement depuis l'index Elasticsearch. Cette logique est allouée dans une méthode appelée <code>getAvailableFilters</code>:</p>async function getAvailableFilters() {
  try {
    const response = await esClient.search({
      index: INDEX_NAME,
      size: 0,
      aggs: {
        industries: {
          terms: { field: "industry", size: 100 },
        },
        locations: {
          terms: { field: "location", size: 100 },
        },
        funding_stages: {
          terms: { field: "funding_stage", size: 20 },
        },
        business_models: {
          terms: { field: "business_model", size: 10 },
        },
        lead_investors: {
          terms: { field: "lead_investor", size: 100 },
        },
        funding_amount_stats: {
          stats: { field: "funding_amount" },
        },
      },
    });

    return response.aggregations;
  } catch (error) {
    console.error("❌ Error getting available filters:", error);
    return {};
  }
}<p>Avec la requête d'agrégation ci-dessus, nous avons les résultats suivants :</p>{
  "industries": {
    "doc_count_error_upper_bound": 0,
    "sum_other_doc_count": 0,
    "buckets": [
      {
        "key": "logistics",
        "doc_count": 5
      },
      ...
    ]
  },
  "locations": {
    "doc_count_error_upper_bound": 0,
    "sum_other_doc_count": 0,
    "buckets": [
      {
        "key": "San Francisco, CA",
        "doc_count": 4
      },
      {
        "key": "New York, NY",
        "doc_count": 3
      },
      ...
    ]
  },
  "funding_stages": {
    "doc_count_error_upper_bound": 0,
    "sum_other_doc_count": 0,
    "buckets": [
      {
        "key": "Series A",
        "doc_count": 8
      },
      ...
    ]
  },
  "business_models": {
    "doc_count_error_upper_bound": 0,
    "sum_other_doc_count": 0,
    "buckets": [
      {
        "key": "B2B",
        "doc_count": 13
      },
      ...
    ]
  },
  "lead_investors": {
    "doc_count_error_upper_bound": 0,
    "sum_other_doc_count": 0,
    "buckets": [
      {
        "key": "Battery Ventures",
        "doc_count": 1
      },
      {
        "key": "Benchmark Capital",
        "doc_count": 1
      },
      ...
    ]
  },
  "funding_amount_stats": {
    "count": 20,
    "min": 4500000,
    "max": 35000000,
    "avg": 14075000,
    "sum": 281500000
  }
}<p>Découvrez tous les résultats <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/langgraph-js-elasticsearch/responses/aggregationsResponse.json">ici</a>.</p><p>Pour les deux stratégies, nous allons utiliser la recherche hybride afin de détecter à la fois la partie structurée de la question (filtres) et les parties plus subjectives (sémantique). Voici un exemple des deux requêtes utilisant des <a href="https://www.elastic.co/docs/solutions/search/search-templates">modèles de recherche</a> :</p>await esClient.putScript({
      id: INVESTMENT_FOCUSED_TEMPLATE,
      script: {
        lang: "mustache",
        source: `{
          "size": 5,
          "retriever": {
            "rrf": {
              "retrievers": [
                {
                  "standard": {
                    "query": {
                      "semantic": {
                        "field": "semantic_field",
                        "query": "{{query_text}}"
                      }
                    }
                  }
                },
                {
                  "standard": {
                    "query": {
                      "bool": {
                        "filter": [
                          {"terms": {"funding_stage": {{#join}}{{#toJson}}funding_stage{{/toJson}}{{/join}}}},
                          {"range": {"funding_amount": {"gte": {{funding_amount_gte}}{{#funding_amount_lte}},"lte": {{funding_amount_lte}}{{/funding_amount_lte}}}}},
                          {"terms": {"lead_investor": {{#join}}{{#toJson}}lead_investor{{/toJson}}{{/join}}}},
                          {"range": {"monthly_revenue": {"gte": {{monthly_revenue_gte}}{{#monthly_revenue_lte}},"lte": {{monthly_revenue_lte}}{{/monthly_revenue_lte}}}}}
                        ]
                      }
                    }
                  }
                }
              ],
              "rank_window_size": 100,
              "rank_constant": 20
            }
          }
        }`,
      },
    });<p>Regardez les requêtes détaillées dans le fichier <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/langgraph-js-elasticsearch/elasticsearchSetup.ts#L119"><code>elasticsearchSetup.ts</code></a> . Dans le node suivant, il sera décidé laquelle des deux requêtes sera utilisée :</p>// Node 1: Decide search strategy using LLM
async function decideSearchStrategy(state: typeof VCState.State) {
  // Zod schema for specialized search strategy decision
  const SearchDecisionSchema = z.object({
    search_type: z
      .enum(["investment_focused", "market_focused"])
      .describe("Type of specialized search strategy to use"),
    reasoning: z
      .string()
      .describe("Brief explanation of why this search strategy was chosen"),
  });

  const decisionLLM = llm.withStructuredOutput(SearchDecisionSchema);

  // Get dynamic filters from Elasticsearch
  const availableFilters = await getAvailableFilters();

  const prompt = `Query: "${state.input}"
    Available filters: ${JSON.stringify(availableFilters, null, 2)}

    Choose between two specialized search strategies:
    
    - investment_focused: For queries about funding stages, funding amounts, monthly revenue, lead investors, financial performance
    
    - market_focused: For queries about industries, locations, business models, market segments, geographic markets
    
    Analyze the query intent and choose the most appropriate strategy.
  `;

  try {
    const result = await decisionLLM.invoke(prompt);
    console.log(
      `🤔 Search strategy: ${result.search_type} - ${result.reasoning}`
    );

    return {
      searchStrategy: result.search_type,
    };
  } catch (error: any) {
    console.error("❌ Error in decideSearchStrategy:", error.message);
    return {
      searchStrategy: "investment_focused",
    };
  }
}<h3>Nodes prepareInvestmentSearch et prepareMarketSearch</h3><p>Les deux nœuds utilisent une fonction d’assistance partagée, <code>extractFilterValues</code>, qui exploite le LLM pour identifier les filtres pertinents mentionnés dans les entrées de l’utilisateur, tels que l’industrie, la localisation, le stade de financement, le modèle économique, etc. Nous utilisons ce schéma pour construire notre <a href="https://www.elastic.co/docs/solutions/search/search-templates">modèle de recherche</a>.</p>// Extract all possible filter values from user input
async function extractFilterValues(input: string) {
  const FilterValuesSchema = z.object({
    // Investment-focused filters
    funding_stage: z
      .array(z.string())
      .default([])
      .describe("Funding stage values mentioned in query"),
    funding_amount_gte: z
      .number()
      .default(0)
      .describe("Minimum funding amount in USD"),
    funding_amount_lte: z
      .number()
      .default(100000000)
      .describe("Maximum funding amount in USD"),
    lead_investor: z
      .array(z.string())
      .default([])
      .describe("Lead investor values mentioned in query"),
    monthly_revenue_gte: z
      .number()
      .default(0)
      .describe("Minimum monthly revenue in USD"),
    monthly_revenue_lte: z
      .number()
      .default(10000000)
      .describe("Maximum monthly revenue in USD"),
    industry: z
      .array(z.string())
      .default([])
      .describe("Industry values mentioned in query"),
    location: z
      .array(z.string())
      .default([])
      .describe("Location values mentioned in query"),
    business_model: z
      .array(z.string())
      .default([])
      .describe("Business model values mentioned in query"),
  });

  const extractorLLM = llm.withStructuredOutput(FilterValuesSchema);
  const availableFilters = await getAvailableFilters();

  const extractPrompt = `Extract ALL relevant filter values from: "${input}"
    Available options: ${JSON.stringify(availableFilters, null, 2)}
    Extract only values explicitly mentioned in the query. Leave fields empty if not mentioned.`;

  return await extractorLLM.invoke(extractPrompt);
}<p>Selon l'intention détectée, le workflow sélectionne l'un des deux chemins :</p><p><strong>prepareInvestmentSearch :</strong> définit des paramètres de rechercher orientés sur la finance, notamment l''étape du financement, le montant du financement, les informations relatives à l''investisseur et au renouvellement. Vous pouvez trouver le modèle complet de requête dans le fichier <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/langgraph-js-elasticsearch/elasticsearchSetup.ts"><code>elasticsearchSetup.ts</code></a> :</p>// Node 2A: Prepare Investment-Focused Search Parameters 
async function prepareInvestmentSearch(state: typeof VCState.State) {
  console.log(
    "💰 Preparing INVESTMENT-FOCUSED search parameters with financial emphasis..."
  );

  try {
    // Extract all filter values from input
    const values = await extractFilterValues(state.input);

    let searchParams: any = {
      template_id: INVESTMENT_FOCUSED_TEMPLATE,
      query_text: state.input,
      ...values,
    };

    return { searchParams };
  } catch (error) {
    console.error("❌ Error preparing investment-focused params:", error);
    return {
      searchParams: {},
    };
  }
}<p><strong>prepareMarketSearch :</strong> crée des paramètres orientés vers le marché, axés sur les industries, les régions géographiques et les modèles économiques. Voir l’intégralité de la requête dans le fichier <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/langgraph-js-elasticsearch/elasticsearchSetup.ts"><code>elasticsearchSetup.ts</code></a> :</p>// Node 2B: Prepare Market-Focused Search Parameters
async function prepareMarketSearch(state: typeof VCState.State) {
  console.log(
    "🔍 Preparing MARKET-FOCUSED search parameters with market emphasis..."
  );

  try {
    // Extract all filter values from input
    const values = await extractFilterValues(state.input);

    let searchParams: any = {
      template_id: MARKET_FOCUSED_TEMPLATE,
      query_text: state.input,
      ...values,
    };

    return { searchParams };
  } catch (error) {
    console.error("❌ Error preparing market-focused params:", error);
    return {};
  }
}<h3>Node executeSearch</h3><p>Ce node prend les paramètres de rechercher générés à partir de l'état et les envoie d'abord à Elasticsearch, en utilisant l'<a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-render-search-template">API _render</a> pour visualiser la requête à des fins de débogage, puis envoie une demande pour récupérer les résultats.</p>// Node 3: Execute Search
async function executeSearch(state: typeof VCState.State) {
  const { searchParams } = state;

  try {
    // getting formed query from template for debugging
    const renderedTemplate = await esClient.renderSearchTemplate({
      id: searchParams.template_id,
      params: searchParams,
    });

    console.log(
      "📋 Complete query:",
      JSON.stringify(renderedTemplate.template_output, null, 2)
    );

    const results = await esClient.searchTemplate({
      index: INDEX_NAME,
      id: searchParams.template_id,
      params: searchParams,
    });

    return {
      results: results.hits.hits.map((hit: any) =&gt; hit._source),
    };
  } catch (error: any) {
    console.error(`❌ ${state.searchParams.search_type} search error:`, error);
    return { results: [] };
  }
}<h3>Node visualizeResults</h3><p>Enfin, ce node affiche les résultats d’Elasticsearch.</p>// Node 4: Visualize results
async function visualizeResults(state: typeof VCState.State) {
  const results = state.results || [];

  let formattedResults = `🎯 Found ${results.length} startups matching your criteria:\n\n`;

  results.forEach((startup: any, index: number) =&gt; {
    formattedResults += `${index + 1}. **${startup.company_name}**\n`;
    formattedResults += `   📍 ${startup.location} | 🏢 ${startup.industry} | 💼 ${startup.business_model}\n`;
    formattedResults += `   💰 ${startup.funding_stage} - $${(
      startup.funding_amount / 1000000
    ).toFixed(1)}M\n`;
    formattedResults += `   👥 ${startup.employee_count} employees | 📈 $${(
      startup.monthly_revenue / 1000
    ).toFixed(0)}K MRR\n`;
    formattedResults += `   🏦 Lead: ${startup.lead_investor}\n`;
    formattedResults += `   📝 ${startup.description}\n\n`;
  });

  return {
    final: formattedResults,
  };
}<p>Par programmation, l'ensemble du graphe ressemble à ceci :</p>  const workflow = new StateGraph(VCState)
    // Register nodes - these are the processing functions
    .addNode("decideStrategy", decideSearchStrategy)
    .addNode("prepareInvestment", prepareInvestmentSearch)
    .addNode("prepareMarket", prepareMarketSearch)
    .addNode("executeSearch", executeSearch)
    .addNode("visualizeResults", visualizeResults)
    // Define execution flow with conditional branching
    .addEdge(START, "decideStrategy") // Start with strategy decision
    .addConditionalEdges(
      "decideStrategy",
      (state: typeof VCState.State) =&gt; state.searchStrategy, // Conditional function
      {
        investment_focused: "prepareInvestment", // If investment focused -&gt; RRF template preparation
        market_focused: "prepareMarket", // If market focused -&gt; dynamic query preparation
      }
    )
    .addEdge("prepareInvestment", "executeSearch") // Investment prep -&gt; execute
    .addEdge("prepareMarket", "executeSearch") // Market prep -&gt; execute
    .addEdge("executeSearch", "visualizeResults") // Execute -&gt; visualize
    .addEdge("visualizeResults", END); // End workflow<p>Comme vous pouvez le constater, nous avons une arête conditionnelle où l'application décide quel « chemin » ou node sera exécuté ensuite. Cette fonctionnalité est utile lorsque les workflows nécessitent une logique de branchement, comme le choix entre plusieurs outils ou l’inclusion d’une étape humaine dans la boucle.</p><p>Maintenant que vous maîtrisez les fonctionnalités clés de LangGraph, nous pouvons préparer l’application qui exécutera le code :</p><p>Rassemblons tout dans une méthode <code>main</code> , ici nous déclarons le graphe avec tous les éléments sous la variable workflow :</p>async function main() {
  await createIndex();
  await createSearchTemplates();
  await ingestDocuments();

  // Create the workflow graph with shared state
  const workflow = new StateGraph(VCState)
    // Register nodes - these are the processing functions
    .addNode("decideStrategy", decideSearchStrategy)
    .addNode("prepareInvestment", prepareInvestmentSearch)
    .addNode("prepareMarket", prepareMarketSearch)
    .addNode("executeSearch", executeSearch)
    .addNode("visualizeResults", visualizeResults)
    // Define execution flow with conditional branching
    .addEdge(START, "decideStrategy") // Start with strategy decision
    .addConditionalEdges(
      "decideStrategy",
      (state: typeof VCState.State) =&gt; state.searchStrategy, // Conditional function
      {
        investment_focused: "prepareInvestment", // If investment focused -&gt; RRF template preparation
        market_focused: "prepareMarket", // If market focused -&gt; dynamic query preparation
      }
    )
    .addEdge("prepareInvestment", "executeSearch") // Investment prep -&gt; execute
    .addEdge("prepareMarket", "executeSearch") // Market prep -&gt; execute
    .addEdge("executeSearch", "visualizeResults") // Execute -&gt; visualize
    .addEdge("visualizeResults", END); // End workflow


  const app = workflow.compile();

  await saveGraphImage(app);

  const query =
    "Find startups with Series A or Series B funding between $8M-$25M and monthly revenue above $500K";

  const marketResult = await app.invoke({ input: query });
  console.log(marketResult.final);
}<p>La variable de requête simule l'entrée utilisateur saisie dans une barre de recherche hypothétique :</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltba7189d5f4e63403/6a1709880e2e49cc3041a076/e8d76909eb2bc1bb62f3ca9a8b3e4b85fcec2893-1600x164.png" alt="" /><p>D’après la phrase en langage naturel « Trouvez des startups avec un financement de la série A ou de la série B entre 8 millions et 25 millions de dollars et un chiffre d’affaires mensuel supérieur à 500 000 $ », tous les filtres seront extraits.</p><p>Enfin, invoquez la méthode principale :</p>main().catch(console.error);<h3>Résultats</h3>🔍 Checking if index exists...
🏗️ Creating index...
✅ Index created successfully!
Ingesting documents...
✅ Documents ingested successfully!
✅ Investment-focused template created successfully!
✅ Market-focused template created successfully!

📊 Workflow graph saved as: ./workflow_graph.png

🔍 Query: "Find startups with Series A or Series B funding between $8M-$25M and monthly revenue above $500K"

🤔 Search strategy: investment_focused - The query specifically seeks profitable fintech startups with defined funding amounts and high monthly revenue, which aligns closely with financial performance metrics and investment-related criteria.

💰 Preparing INVESTMENT-FOCUSED search parameters with financial emphasis...

📋 Complete query: {
  "size": 5,
  "retriever": {
    "rrf": {
      "retrievers": [
        {
          "standard": {
            "query": {
              "semantic": {
                "field": "semantic_field",
                "query": "Find startups with Series A or Series B funding between $8M-$25M and monthly revenue above $500K"
              }
            }
          }
        },
        {
          "standard": {
            "query": {
              "bool": {
                "filter": [
                  {
                    "terms": {
                      "funding_stage": [
                        "Series A",
                        "Series B"
                      ]
                    }
                  },
                  {
                    "range": {
                      "funding_amount": {
                        "gte": 8000000,
                        "lte": 25000000
                      }
                    }
                  },
                  {
                    "terms": {
                      "lead_investor": []
                    }
                  },
                  {
                    "range": {
                      "monthly_revenue": {
                        "gte": 500000,
                        "lte": 0
                      }
                    }
                  }
                ]
              }
            }
          }
        }
      ],
      "rank_window_size": 100,
      "rank_constant": 20
    }
  }
}
🎯 Found 5 startups matching your criteria:

1. **TechFlow**
   📍 San Francisco, CA | 🏢 logistics | 💼 B2B
   💰 Series A - $8.0M
   👥 45 employees | 📈 $500K MRR
   🏦 Lead: Sequoia Capital
   📝 TechFlow optimizes supply chain operations using AI-powered route optimization and real-time tracking. Founded in 2023, shows remarkable growth with $500K monthly revenue.

2. **DataViz**
   📍 New York, NY | 🏢 enterprise software | 💼 B2B
   💰 Series A - $10.0M
   👥 42 employees | 📈 $450K MRR
   🏦 Lead: Battery Ventures
   📝 DataViz creates intuitive data visualization tools for enterprise customers. No-code platform allows business users to create dashboards without technical expertise.

3. **FinanceAI**
   📍 San Francisco, CA | 🏢 fintech | 💼 B2C
   💰 Series C - $25.0M
   👥 120 employees | 📈 $1200K MRR
   🏦 Lead: Tiger Global Management
   📝 FinanceAI provides AI-powered investment advisory services to retail investors. Uses machine learning to analyze market trends with over 100,000 active users.

4. **UrbanMobility**
   📍 New York, NY | 🏢 logistics | 💼 B2B2C
   💰 Series B - $15.0M
   👥 78 employees | 📈 $750K MRR
   🏦 Lead: Kleiner Perkins
   📝 UrbanMobility revolutionizes urban transportation through autonomous delivery drones and smart logistics hubs. Partners with major retailers for same-day delivery across Manhattan and Brooklyn.

5. **HealthTech Solutions**
   📍 Boston, MA | 🏢 healthcare | 💼 B2B
   💰 Series B - $18.0M
   👥 95 employees | 📈 $900K MRR
   🏦 Lead: General Catalyst
   📝 HealthTech Solutions develops medical devices and software for remote patient monitoring. Comprehensive telehealth platform reducing hospital readmissions by 30%.

✨  Done in 18.80s.<p>Pour l'entrée envoyée, l'application choisit le chemin <strong>axé sur l'investissement</strong> et, par conséquent, nous pouvons voir la requête Elasticsearch générée par le workflow, qui extrait les valeurs et les plages de l'entrée de l'utilisateur. Nous pouvons également voir la requête envoyée à Elasticsearch avec les valeurs extraites appliquées, et enfin, les résultats formatés par le nœud <code>visualizeResults</code> avec les résultats.</p><p>Testons maintenant le node <strong>axé sur le marché</strong> en utilisant la requête « Trouver des startups fintech et de la santé à San Francisco, New York ou Boston » :</p>...

🔍 Query: Find fintech and healthcare startups in San Francisco, New York, or Boston

🤔 Search strategy: market_focused - The query is focused on finding fintech startups in San Francisco that are disrupting traditional banking and payment systems, which pertains to specific industries (fintech) and locations (San Francisco). Thus, a market-focused strategy is more appropriate.

🔍 Preparing MARKET-FOCUSED search parameters with market emphasis...

📋 Complete query: {
  "size": 5,
  "retriever": {
    "rrf": {
      "retrievers": [
        {
          "standard": {
            "query": {
              "semantic": {
                "field": "semantic_field",
                "query": "Find fintech and healthcare startups in San Francisco, New York, or Boston"
              }
            }
          }
        },
        {
          "standard": {
            "query": {
              "bool": {
                "filter": [
                  {
                    "terms": {
                      "industry": [
                        "fintech",
                        "healthcare"
                      ]
                    }
                  },
                  {
                    "terms": {
                      "location": [
                        "San Francisco, CA",
                        "New York, NY",
                        "Boston, MA"
                      ]
                    }
                  },
                  {
                    "terms": {
                      "business_model": []
                    }
                  }
                ]
              }
            }
          }
        }
      ],
      "rank_window_size": 50,
      "rank_constant": 10
    }
  }
}
🎯 Found 5 startups matching your criteria:

1. **FinanceAI**
   📍 San Francisco, CA | 🏢 fintech | 💼 B2C
   💰 Series C - $25.0M
   👥 120 employees | 📈 $1200K MRR
   🏦 Lead: Tiger Global Management
   📝 FinanceAI provides AI-powered investment advisory services to retail investors. Uses machine learning to analyze market trends with over 100,000 active users.

2. **CryptoWallet**
   📍 Miami, FL | 🏢 fintech | 💼 B2C
   💰 Series B - $16.0M
   👥 73 employees | 📈 $820K MRR
   🏦 Lead: Coinbase Ventures
   📝 CryptoWallet provides secure digital wallet solutions for cryptocurrency trading and storage. Multi-chain support with enterprise-grade security features.

...

✨  Done in 7.41s.<h2>Enseignements</h2><p>Pendant le processus d'écriture, j'ai appris :</p><ul><li><p>Nous devons montrer au LLM les valeurs exactes des filtres, sinon nous attendons de l'utilisateur qu'il saisisse les valeurs exactes des éléments. Pour une faible cardinalité, cette approche convient, mais lorsque la cardinalité est élevée, nous avons besoin d'un mécanisme pour filtrer les résultats</p></li><li><p>Utiliser des modèles de recherche rend les résultats bien plus cohérents que de laisser le LLM écrire la requête Elasticsearch, et c’est aussi plus rapide</p></li><li><p>Les arêtes conditionnelles constituent un mécanisme puissant pour construire des applications avec de multiples variantes et chemins de branchement.</p></li><li><p>La sortie structurée est extrêmement utile lors de la génération d'informations avec des LLM, car elle applique des réponses prévisibles et sécurisées. Cela améliore la fiabilité et réduit les erreurs d'interprétation.</p></li></ul><p>La combinaison de la recherche sémantique et de la recherche structurée par le biais d'une recherche hybride produit des résultats meilleurs et plus pertinents, en équilibrant précision et compréhension du contexte.</p><h2>Conclusion</h2><p>Dans cet exemple, nous combinons LangGraph.js avec Elasticsearch pour créer un workflow dynamique capable d'interpréter les requêtes en langage naturel et de décider entre des stratégies de recherche axées sur la finance ou le marché. Cette approche réduit la complexité de la création de requêtes manuelles tout en améliorant la flexibilité et la précision pour les analystes en capital-risque.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/ai-agent-workflow-finance-langgraph-elasticsearch</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/ai-agent-workflow-finance-langgraph-elasticsearch</guid>
    <category><![CDATA[IA]]></category>
    <category><![CDATA[IA agentique]]></category>
    <dc:creator><![CDATA[Jeffrey Rengifo]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt013eba5d152f11f3/6a1709892b835f6784f4b1a6/12b6057d84c6356267cd178a3c6c1a5c61123ece-2000x1256.png" length="0" type="image/png"/>
    <pubDate>Fri, 05 Dec 2025 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Tableaux de bord alimentés par l'IA : D'une vision à Kibana]]></title>
    <description><![CDATA[Générer un tableau de bord en utilisant un LLM pour traiter une image et la transformer en tableau de bord Kibana.
]]></description>
    <content:encoded><![CDATA[<p><a href="https://www.elastic.co/kibana/kibana-lens">Kibana Lens</a> simplifie le glisser-déposer des tableaux de bord, mais lorsque vous avez besoin de dizaines de panneaux, les clics s'accumulent. Et si vous pouviez dessiner un tableau de bord, en faire une capture d'écran et laisser un LLM terminer tout le processus à votre place ?</p><p>Dans cet article, nous allons y parvenir. Nous allons créer une application qui prend une image d'un tableau de bord, analyse nos mappings et génère un tableau de bord sans que nous ayons à toucher à Kibana !</p><p><strong>Les étapes</strong>:</p><ol><li><p><a href="https://www.elastic.co/search-labs/blog/ai-powered-dashboards#background-&amp;-application-workflow">Contexte &amp; flux de travail de l'application</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/ai-powered-dashboards#prepare-data">Préparer les données</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/ai-powered-dashboards#llm-configuration">Configuration LLM</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/ai-powered-dashboards#application-functions">Fonctions d'application</a></p></li></ol><h2>Contexte &amp; flux de travail de l'application</h2><p>La première idée qui m'est venue à l'esprit a été de laisser le LLM générer l'ensemble des <a href="https://www.elastic.co/docs/explore-analyze/find-and-organize/saved-objects">objets sauvegardés au</a> format NDJSON dans Kibana, puis de les importer dans Kibana.</p><p>Nous avons essayé une poignée de modèles :</p><ul><li><p>Gemini 2.5 pro</p></li><li><p>GPT o3 / o4-mini-high / 4.1</p></li><li><p>Sonnet de Claude 4</p></li><li><p>Grok 3</p></li><li><p>Deepseek (Deepthink R1)</p></li></ul><p>En ce qui concerne les messages-guides, nous avons commencé par une phrase simple :</p>You are an Elasticsearch Saved-Object generator (Kibana 9.0).
INPUTS
=====
1. PNG screenshot of a 4-panel dashboard (attached).
2. Index mapping (below) – trimmed down to only the fields present in the screenshot.
3. Example NDJSON of *one* metric visualization (below) for reference.

TASK
====
Return **only** a valid NDJSON array that recreates the dashboard exactly:
* 2 metric panels (Visits, Unique Visitors)
* 1 pie chart (Most used OS)
* 1 vertical bar chart (State Geo Dest)
* Use index pattern `kibana_sample_data_logs`.
* Preserve roughly the same layout (2×2 grid).
* Use `panelIndex` values 1-4 and random `id` strings.
* Kibana version: 9.0<p>Bien que nous ayons parcouru des <a href="https://www.elastic.co/search-labs/blog/function-calling-with-elastic#:~:text=Few%2Dshot%20prompting%20involves%20providing%20examples%20of%20the%20types%20of%20queries%20you%20want%20it%20to%20return%2C%20which%20helps%20in%20increasing%20consistency.">exemples en quelques images</a> et des explications détaillées sur la manière de construire chaque visualisation, nous n'avons pas eu de chance. Si vous êtes intéressé par cette expérimentation, vous pouvez trouver des détails <a href="https://gist.github.com/TomasMurua/a78dc283e115624731beffc98984b70b">ici.</a></p><p>Le résultat de cette approche était l'apparition de ces messages lorsque l'on essayait de télécharger vers Kibana les fichiers produits par le LLM :</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt9ea005966a783057/6a1707d266c4f90e4ef8bf88/2b599443b5613c9f0fc3235581614add5b4b3900-891x98.png" alt="" /><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt5e5632d6d95b998c/6a1707d3a6c2b9441de79661/d87ccfc033bc00ee8188c5cae18043fbca22784c-741x233.png" alt="" /><p>Cela signifie que le JSON généré est invalide ou mal formaté. Les problèmes les plus fréquents étaient que le LLM produisait des NDJSON incomplets, des paramètres hallucinants ou retournait du JSON normal au lieu de NDJSON, même si nous essayions de faire en sorte qu'il en soit autrement.</p><p>Inspirés par <a href="https://www.elastic.co/search-labs/blog/llm-functions-elasticsearch-intelligent-query">cet article</a> - où les <a href="https://www.elastic.co/docs/solutions/search/search-templates">modèles de recherche</a> ont mieux fonctionné que le LLM freestyle - nous avons décidé de donner des modèles au LLM au lieu de demander de générer le fichier NDJSON complet et ensuite nous, dans le code, utilisons les paramètres donnés par le LLM pour créer les visualisations appropriées.</p><p>Le processus de candidature sera le suivant :</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt9f7738a4c7ddd0cd/6a1707d52b835f0a25f4b166/52c587cf0cf3517fdd4ee7ab95581dd4f2bce030-725x668.png" alt="" /><p></p><p><em>Nous omettons une partie du code pour des raisons de simplicité, mais vous pouvez trouver le code de travail de l'application complète sur </em><a href="https://github.com/elastic/elasticsearch-labs/tree/main/supporting-blog-content/from-image-idea-to-kibana-dashboard-using-ai/from-image-idea-to-kibana-dashboard-using-ai.ipynb"><em><strong>ce</strong></em></a><em> carnet.</em></p><h2>Produits requis</h2><p>Avant de commencer à développer, vous aurez besoin des éléments suivants :</p><ol><li><p>Python 3.8 ou supérieur</p></li><li><p>Un environnement <a href="https://docs.python.org/3/library/venv.html">Venv</a> Python</p></li><li><p>Une instance Elasticsearch en cours d'exécution, ainsi que son point d'accès et sa clé API</p></li><li><p>Une clé d'API OpenAI stockée dans la variable d'environnement OPENAI_API_KEY :</p></li></ol>export OPENAI_API_KEY="your-openai-api-key"<h2>Préparer les données</h2><p>Pour les données, nous resterons simples et utiliserons les journaux web de l'échantillon Elastic. Pour savoir comment importer ces données dans votre cluster <a href="https://www.elastic.co/docs/manage-data/ingest/sample-data#add-sample-data-sets">, cliquez ici.</a></p><p>Chaque document contient des informations sur l'hôte qui a envoyé des demandes à l'application, ainsi que des informations sur la demande elle-même et l'état de sa réponse. Vous trouverez ci-dessous un exemple de document :</p>{
    "agent": "Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24",
    "bytes": 8509,
    "clientip": "70.133.115.149",
    "extension": "css",
    "geo": {
        "srcdest": "US:IT",
        "src": "US",
        "dest": "IT",
        "coordinates": {
            "lat": 38.05134111,
            "lon": -103.5106908
        }
    },
    "host": "cdn.elastic-elastic-elastic.org",
    "index": "kibana_sample_data_logs",
    "ip": "70.133.115.149",
    "machine": {
        "ram": 5368709120,
        "os": "osx"
    },
    "memory": null,
    "message": "70.133.115.149 - - [2018-08-30T23:35:31.492Z] \"GET /styles/semantic-ui.css HTTP/1.1\" 200 8509 \"-\" \"Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24\"",
    "phpmemory": null,
    "referer": "http://twitter.com/error/john-phillips",
    "request": "/styles/semantic-ui.css",
    "response": 200,
    "tags": [
        "success",
        "info"
    ],
    "@timestamp": "2025-07-03T23:35:31.492Z",
    "url": "https://cdn.elastic-elastic-elastic.org/styles/semantic-ui.css",
    "utc_time": "2025-07-03T23:35:31.492Z",
    "event": {
        "dataset": "sample_web_logs"
    },
    "bytes_gauge": 8509,
    "bytes_counter": 51201128
}<p>Prenons maintenant les mappings de l'index que nous venons de charger, <code>kibana_sample_data_logs</code>:</p>INDEX_NAME = "kibana_sample_data_logs"

es_client = Elasticsearch(
    [os.getenv("ELASTICSEARCH_URL")],
    api_key=os.getenv("ELASTICSEARCH_API_KEY"),
)

result = es_client.indices.get_mapping(index=INDEX_NAME)
index_mappings = result[list(result.keys())[0]]["mappings"]["properties"]<p>Nous allons transmettre les mappings avec l'image que nous chargerons plus tard.</p><h2>Configuration LLM</h2><p>Configurons le LLM pour qu'il utilise la <a href="https://python.langchain.com/docs/concepts/structured_outputs/">sortie structurée</a> afin d'entrer une image et de recevoir un JSON contenant les informations que nous devons transmettre à notre fonction pour produire les objets JSON.</p><p>Nous installons les dépendances :</p>pip install elasticsearch pydantic langchain langchain-openai -q<p>Elasticsearch nous aidera à récupérer les <a href="https://www.elastic.co/docs/manage-data/data-store/mapping">mappages d'index</a>. Pydantic nous permet de définir des schémas en Python pour demander au LLM de les suivre, et <a href="https://www.elastic.co/search-labs/integrations/langchain">LangChain</a> est le cadre qui facilite l'appel aux LLM et aux outils d'IA.</p><p>Nous allons créer un schéma pydantique pour définir les résultats que nous voulons obtenir du LLM. Ce que nous devons savoir à partir de l'image, c'est le type de graphique, le champ, le titre de la visualisation et le titre du tableau de bord :</p>class Visualization(BaseModel):
    title: str = Field(description="The dashboard title")
    type: List[Literal["pie", "bar", "metric"]]
    field: str = Field(
        description="The field that this visualization use based on the provided mappings"
    )


class Dashboard(BaseModel):
    title: str = Field(description="The dashboard title")
    visualizations: List[Visualization]<p>Pour la saisie de l'image, nous enverrons un tableau de bord que je viens de dessiner :</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt7870f6421986d11d/6a1707d78b73cb3408189fa3/36441d7b5dc1f3ff2ac2a30710208d57ad41c716-1600x898.jpg" alt="" /><p>Nous déclarons maintenant l'appel au modèle LLM et le chargement de l'image. Cette fonction recevra les mappings de l'index Elasticsearch et une image du tableau de bord que nous voulons générer.</p><p>Avec <code>with_structured_output</code>, nous pouvons utiliser notre schéma Pydantic <code>Dashboard</code> comme objet de réponse que le LLM produira. Avec <a href="https://docs.pydantic.dev/latest/">Pydantic</a>, nous pouvons définir des modèles de données avec validation, ce qui garantit que la sortie LLM correspond à la structure attendue.</p><p>Pour convertir l'image en base64 et l'envoyer en entrée, vous pouvez utiliser un <a href="https://www.base64-image.de/">convertisseur en ligne</a> ou le faire <a href="https://www.geeksforgeeks.org/python-convert-image-to-string-and-vice-versa/">en code</a>.</p>prompt = f"""
    You are an expert in analyzing Kibana dashboards from images for the version 9.0.0 of Kibana.

    You will be given a dashboard image and an Elasticsearch index mapping.

    Below are the index mappings for the index that the dashboard is based on.
    Use this to help you understand the data and the fields that are available.

    Index Mappings:
    {index_mappings}

    Only include the fields that are relevant for each visualization, based on what is visible in the image.
    """

message = [
    {
        "role": "user",
        "content": [
            {"type": "text", "text": prompt},
            {
                "type": "image",
                "source_type": "base64",
                "data": image_base64,
                "mime_type": "image/png",
            },
        ],
    }
]


try:
    llm = init_chat_model("gpt-4.1-mini")
    llm = llm.with_structured_output(Dashboard)
    dashboard_values = llm.invoke(message)

    print("Dashboard values generated by the LLM successfully")
    print(dashboard_values)
except Exception as e:
    print(f"Failed to analyze image and match fields: {str(e)}")<p>Le LLM connaît déjà le contexte des tableaux de bord Kibana, nous n'avons donc pas besoin de tout expliquer dans l'invite, juste quelques détails pour s'assurer qu'il n'oublie pas qu'il travaille avec Elasticsearch et Kibana.</p><p>Décortiquons l'invitation :</p><p>Section</p><p>Raison</p><p>Vous êtes un expert en analyse de tableaux de bord Kibana à partir d'images pour la version 9.0.0 de Kibana.</p><p>En insistant sur le fait qu'il s'agit d'Elasticsearch et de la version d'Elasticsearch, nous réduisons la probabilité que le LLM hallucine des paramètres anciens/invalides.</p><p>Vous recevrez une image de tableau de bord et un mappage d'index Elasticsearch.</p><p>Nous expliquons que l'image concerne les tableaux de bord afin d'éviter toute interprétation erronée de la part du LLM.</p><p>Vous trouverez ci-dessous les correspondances d'index pour l'index sur lequel le tableau de bord est basé, ce qui vous aidera à comprendre les données et les champs disponibles. Mappages d'index : {index_mappings}</p><p>Il est essentiel de fournir les correspondances afin que le LLM puisse sélectionner les champs valides de manière dynamique. Sinon, nous pourrions coder en dur les correspondances ici, ce qui est trop rigide, ou compter sur le fait que l'image contienne les bons noms de champs, ce qui n'est pas fiable.</p><p>N'incluez que les champs pertinents pour chaque visualisation, en fonction de ce qui est visible dans l'image.</p><p>Nous avons dû ajouter ce renforcement parce qu'il arrive que l'on essaie d'ajouter des champs qui ne sont pas pertinents pour l'image.</p><p>Cela renvoie un objet contenant un tableau de visualisations à afficher :</p>"Dashboard values generated by the LLM successfully
title=""Client, Extension, OS, and Response Keyword Analysis""visualizations="[
   "Visualization(title=""Count of Client IP",
   "type="[
      "metric"
   ],
   "field=""clientip"")",
   "Visualization(title=""Extension Keyword Distribution",
   "type="[
      "pie"
   ],
   "field=""extension.keyword"")",
   "Visualization(title=""Most Used OS",
   "type="[
      "bar"
   ],
   "field=""machine.os.keyword"")",
   "Visualization(title=""Response Keyword Distribution",
   "type="[
      "bar"
   ],
   "field=""response.keyword"")"
]<h2>Traitement de la réponse au mécanisme d'apprentissage tout au long de la vie</h2><p>Nous avons créé un exemple de tableau de bord 2x2 panneaux à l'adresseet l'avons exporté en JSON à l'aide de l'<a href="https://www.elastic.co/docs/api/doc/kibana/operation/operation-get-dashboards-dashboard">API Get a dashboard</a>, puis nous avons stocké les panneaux en tant que modèles de visualisation (camembert, barre, métrique) dans lesquels nous pouvons remplacer certains paramètres pour créer de nouvelles visualisations avec différents champs en fonction de la question.</p><p>Vous pouvez consulter les fichiers JSON du modèle <a href="https://github.com/Delacrobix/elasticsearch-labs/tree/supporting-blog-content/from-image-idea-to-kibana-dashboard-using-ai/supporting-blog-content/from-image-idea-to-kibana-dashboard-using-ai/templates"><strong>ici.</strong></a> Notez que nous avons modifié les valeurs de l'objet que nous voulons remplacer plus tard par {<code>variable_name</code>}
</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc55d69d84a08e668/6a1707d8a2929903acd00fb8/ec7e1ac0cd8b470df13e60940162b56778acb386-315x234.png" alt="" /><p>Grâce aux informations fournies par le mécanisme d'apprentissage tout au long de la vie, nous pouvons décider du modèle à utiliser et des valeurs à remplacer.</p><p><code>fill_template_with_analysis</code> recevra les paramètres pour un seul panneau, y compris le modèle JSON de la visualisation, un titre, un champ et les coordonnées de la visualisation sur la grille.</p><p>Ensuite, il remplacera les valeurs du modèle et renverra la visualisation JSON finale.</p>def fill_template_with_analysis(
    template: Dict[str, Any],
    visualization: Visualization,
    grid_data: Dict[str, Any],
):
    template_str = json.dumps(template)
    replacements = {
	 "{visualization_id}": str(uuid.uuid4()),
        "{title}": visualization.title,
        "{x}": grid_data["x"],
        "{y}": grid_data["y"],
    }

    if visualization.field:
        replacements["{field}"] = visualization.field

    for placeholder, value in replacements.items():
        template_str = template_str.replace(placeholder, str(value))

    return json.loads(template_str)<p>Pour faire simple, nous aurons des coordonnées statiques que nous assignerons aux panneaux que le LLM décidera de créer et nous produirons un tableau de bord à grille 2x2 comme l'image ci-dessus.</p># Filling templates fields
panels = []    
grid_data = [
    {"x": 0, "y": 0},
    {"x": 12, "y": 0},
    {"x": 0, "y": 12},
    {"x": 12, "y": 12},
]


i = 0

for vis in dashboard_values.visualizations:
    for vis_type in vis.type:
        template = templates.get(vis_type, templates.get("bar", {}))
        filled_panel = fill_template_with_analysis(template, vis, grid_data[i])
        panels.append(filled_panel)
        i += 1<p>En fonction du type de visualisation décidé par le LLM, nous choisirons un modèle de fichier JSON et remplacerons les informations pertinentes à l'aide de <code>fill_template_with_analysis</code> , puis nous ajouterons le nouveau panneau à un tableau que nous utiliserons ultérieurement pour créer le tableau de bord.</p><p>Lorsque le tableau de bord est prêt, nous utilisons l'<a href="https://www.elastic.co/docs/api/doc/kibana/operation/operation-post-dashboards-dashboard-id">API Create a dashboard</a> pour envoyer le nouveau fichier JSON à Kibana afin de générer le tableau de bord :
</p>try:
    dashboard_id = str(uuid.uuid4())

    # post request to create the dashboard endpoint
    url = f"{os.getenv('KIBANA_URL')}/api/dashboards/dashboard/{dashboard_id}"

    dashboard_config = {
        "attributes": {
            "title": dashboard_values.title,
            "description": "Generated by AI",
            "timeRestore": True,
            "panels": panels,  # Visualizations with the values generated by the LLM
            "timeFrom": "now-7d/d",
            "timeTo": "now",
        },
    }

    headers = {
        "Content-Type": "application/json",
        "kbn-xsrf": "true",
        "Authorization": f"ApiKey {os.getenv('ELASTICSEARCH_API_KEY')}",
    }

    requests.post(
        url,
        headers=headers,
        json=dashboard_config,
    )

    # Url to the generated dashboard
    dashboard_url = f"{os.getenv('KIBANA_URL')}/app/dashboards#/view/{dashboard_id}"

    print("Dashboard URL: ", dashboard_url)
    print("Dashboard ID: ", dashboard_id)

except Exception as e:
    print(f"Failed to create dashboard: {str(e)}")<p>Pour exécuter le script et générer le tableau de bord, exécutez la commande suivante dans la console :</p>python &lt;file_name&gt;.py<p>Le résultat final sera le suivant :</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt5ceffed004153a4f/6a1707d9a929cf9147ae0901/e909afbf0e47d9a6e0f7bd07dfb2efcfa5cf06ac-921x715.png" alt="" /><h2>Conclusion</h2><p>Les LLM démontrent leurs fortes capacités visuelles lorsqu'ils transforment du texte en code ou des images en code. L'API des tableaux de bord permet également de transformer des fichiers JSON en tableaux de bord, et avec un LLM et un peu de code, nous pouvons transformer des images en tableau de bord Kibana.</p><p>L'étape suivante consiste à améliorer la flexibilité des visuels des tableaux de bord en utilisant différents paramètres de grille, différentes tailles de tableau de bord et différentes positions. De plus, la prise en charge de visualisations et de types de visualisation plus complexes serait un ajout utile à cette application.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/ai-powered-dashboards</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/ai-powered-dashboards</guid>
    <category><![CDATA[Kibana]]></category>
    <category><![CDATA[IA]]></category>
    <dc:creator><![CDATA[Jeffrey Rengifo,Tomás Murúa]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt41727cbee6155a68/6a1707dbb0367dd2fd72bc86/eb60ceb2fbc3941745b21ae3357cbb6ea8fab18c-1443x811.png" length="0" type="image/png"/>
    <pubDate>Wed, 16 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>
  <item>
    <title><![CDATA[Utiliser Ollama avec l'API d'inférence]]></title>
    <description><![CDATA[Apprenez à intégrer Ollama avec Elasticsearch en utilisant l'API Inference.]]></description>
    <content:encoded><![CDATA[<p>Dans cet article, nous allons apprendre à connecter des modèles locaux au modèle d'inférence d'Elasticsearch à l'aide d'Ollama, puis à poser des questions à vos documents à l'aide de Playground.</p><p>Elasticsearch permet aux utilisateurs de se connecter aux LLM à l'aide de l'<a href="https://www.elastic.co/fr/guide/en/elasticsearch/reference/current/inference-apis.html">API Open</a> Inference, qui prend en charge des fournisseurs tels qu'Amazon Bedrock, Cohere, Google AI, Azure AI Studio, HuggingFace - en tant que service, entre autres.</p><p><a href="https://ollama.com">Ollama</a> est un outil qui vous permet de télécharger et d'exécuter des modèles LLM en utilisant votre propre infrastructure (votre machine/serveur local). Vous trouverez <a href="https://ollama.com/library">ici</a> une liste des modèles disponibles qui sont compatibles avec Ollama.</p><p>Ollama est une excellente option si vous souhaitez héberger et tester différents modèles open source sans avoir à vous soucier des différentes façons dont chacun des modèles pourrait être configuré, ou de la façon de créer une API pour accéder aux fonctions du modèle, car Ollama s'occupe de tout.</p><p>L'API d'Ollama étant compatible avec l'API d'OpenAI, nous pouvons facilement intégrer le modèle d'inférence et créer une application RAG à l'aide de Playground.</p><h2>Produits requis</h2><ol><li><p>Elasticsearch 8.17</p></li><li><p>Kibana 8.17</p></li><li><p>Python</p></li></ol><h2>Étapes</h2><ol><li><p><a href="https://www.elastic.co/fr/search-labs/blog/ollama-with-inference-api#setting-up-ollama-llm-server">Mise en place du serveur Ollama LLM</a></p></li><li><p><a href="https://www.elastic.co/fr/search-labs/blog/ollama-with-inference-api#creating-mappings">Création de mappings</a></p></li><li><p><a href="https://www.elastic.co/fr/search-labs/blog/ollama-with-inference-api#indexing-data">Indexation des données</a></p></li><li><p><a href="https://www.elastic.co/fr/search-labs/blog/ollama-with-inference-api#asking-questions-using-playground">Poser des questions à l'aide de l'aire de jeu</a></p></li></ol><h2>Mise en place du serveur Ollama LLM</h2><p>Nous allons mettre en place un serveur LLM pour le connecter à notre instance Playground en utilisant Ollama. Nous en aurons besoin :</p><ul><li><p>Téléchargez et exécutez Ollama.</p></li><li><p>Utilisez ngrok pour accéder à votre serveur web local qui héberge Ollama sur Internet.</p></li></ul><h3>Télécharger et lancer Ollama</h3><p>Pour utiliser Ollama, il faut d'abord <a href="https://ollama.com/download">le télécharger.</a> Ollama est compatible avec Linux, Windows et macOS. Il vous suffit donc de télécharger la version d'Ollama compatible avec votre système d'exploitation <a href="https://ollama.com/download">ici.</a> Une fois Ollama installé, nous pouvons choisir un modèle dans cette <a href="https://ollama.com/library">liste</a> de LLMs supportés. Dans cet exemple, nous utiliserons le modèle <a href="https://ollama.com/library/llama3.2">llama3.2</a>, un modèle général multilingue. Dans le processus d'installation, vous activerez l'outil de ligne de commande pour Ollama. Une fois qu'il est téléchargé, vous pouvez exécuter la ligne suivante :</p>ollama pull llama3.2<p>Ce qui produira un résultat :</p>pulling manifest
pulling dde5aa3fc5ff... 100% ▕█████████████████████████████████████████████████████████████████████████████████████████▏ 2.0 GB
pulling 966de95ca8a6... 100% ▕█████████████████████████████████████████████████████████████████████████████████████████▏ 1.4 KB
pulling fcc5a6bec9da... 100% ▕█████████████████████████████████████████████████████████████████████████████████████████▏ 7.7 KB
pulling a70ff7e570d9... 100% ▕█████████████████████████████████████████████████████████████████████████████████████████▏ 6.0 KB
pulling 56bb8bd477a5... 100% ▕█████████████████████████████████████████████████████████████████████████████████████████▏   96 B
pulling 34bb5ab01051... 100% ▕█████████████████████████████████████████████████████████████████████████████████████████▏  561 B
verifying sha256 digest
writing manifest
success<p>Une fois installé, vous pouvez le tester avec cette commande :</p>ollama run llama3.2<p>Posons une question :</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc12f240920e897f5/6a17f39425daab32a508a367/ad1eff81c1b04d2a747c3afd0ecbc215e5bd96fd-800x501.gif" alt="Lancez Ollama et posez-lui une question" /><p>Une fois le modèle en cours d'exécution, Ollama active une API qui s'exécute par défaut sur le port "11434". Faisons une demande à cette API, en suivant la <a href="https://github.com/ollama/ollama/blob/main/docs/api.md">documentation officielle :</a></p>curl http://localhost:11434/api/generate -d '{                                          
  "model": "llama3.2",               
  "prompt": "What is the capital of France?"
}' <p>Voici la réponse que nous avons reçue :</p>{"model":"llama3.2","created_at":"2024-11-28T21:48:42.152817532Z","response":"The","done":false}
{"model":"llama3.2","created_at":"2024-11-28T21:48:42.251884485Z","response":" capital","done":false}
{"model":"llama3.2","created_at":"2024-11-28T21:48:42.347365913Z","response":" of","done":false}
{"model":"llama3.2","created_at":"2024-11-28T21:48:42.446837322Z","response":" France","done":false}
{"model":"llama3.2","created_at":"2024-11-28T21:48:42.542367394Z","response":" is","done":false}
{"model":"llama3.2","created_at":"2024-11-28T21:48:42.644580384Z","response":" Paris","done":false}
{"model":"llama3.2","created_at":"2024-11-28T21:48:42.739865362Z","response":".","done":false}
{"model":"llama3.2","created_at":"2024-11-28T21:48:42.834347518Z","response":"","done":true,"done_reason":"stop","context":[128006,9125,128007,271,38766,1303,33025,2696,25,6790,220,2366,18,271,128009,128006,882,128007,271,3923,374,279,6864,315,9822,30,128009,128006,78191,128007,271,791,6864,315,9822,374,12366,13],"total_duration":6948567145,"load_duration":4386106503,"prompt_eval_count":32,"prompt_eval_duration":1872000000,"eval_count":8,"eval_duration":684000000}<p><em>Notez que la réponse spécifique pour ce point d'accès est un flux.</em></p><h3>Exposer un point de terminaison à l'internet en utilisant ngrok</h3><p>Comme notre point d'extrémité fonctionne dans un environnement local, il n'est pas possible d'y accéder à partir d'un autre point, comme notre instance Elastic Cloud, via l'internet. <a href="https://ngrok.com">ngrok</a> nous permet d'exposer un port en offrant une IP publique. Créez un compte dans ngrok et suivez le <a href="https://dashboard.ngrok.com/get-started/setup">guide d'installation</a> officiel.</p><p>Une fois l'agent ngrok installé et configuré, nous pouvons exposer le port utilisé par Ollama :</p>ngrok http 11434 --host-header="localhost:11434"<p><em>Remarque : l'en-tête </em><em><code>--host-header="localhost:11434"</code></em><em> garantit que l'en-tête "Host" dans les demandes correspond à "localhost:11434."</em></p><p>L'exécution de cette commande renverra un lien public qui fonctionnera tant que le ngrok et le serveur Ollama fonctionneront localement.</p>Session Status                online                                                                                                                                                                              
Account                       xxxx@yourEmailProvider.com (Plan: Free)                                                                                                                                             
Version                       3.18.4                                                                                                                                                                              
Region                        United States (us)                                                                                                                                                                  
Latency                       561ms                                                                                                                                                                               
Web Interface                 http://127.0.0.1:4040                                                                                                                                                               
Forwarding                    https://your-ngrok-url.ngrok-free.app -&gt; http://localhost:11434                                                                                                                   


Connections                   ttl     opn     rt1     rt5     p50     p90                                                                                                                                         
                              0       0       0.00    0.00    0.00    0.00                                                ```<p>Dans "Forwarding", nous pouvons voir que ngrok a généré une URL. Gardez-le pour plus tard.</p><p>Essayons à nouveau de faire une requête HTTP vers le point d'accès, en utilisant maintenant l'URL générée par le moteur de recherche :</p>curl https://your-ngrok-endpoint.ngrok-free.app/api/generate -d '{                                          
  "model": "llama3.2",               
  "prompt": "What is the capital of France?"
}'<p>La réponse devrait être similaire à la précédente.</p><h2>Création de mappings</h2><h3>Point final ELSER</h3><p>Pour cet exemple, nous allons <a href="https://www.elastic.co/fr/guide/en/elasticsearch/reference/current/put-inference-api.html">créer un point de terminaison d'inférence à l'aide de l'API d'inférence d'Elasticsearch</a>. En outre, nous utiliserons <a href="https://www.elastic.co/fr/guide/en/machine-learning/current/ml-nlp-elser.html">ELSER</a> pour générer les enchâssements.</p>PUT _inference/sparse_embedding/medicines-inference
{
  "service": "elasticsearch",
  "service_settings": {
    "num_allocations": 1,
    "num_threads": 1,
    "model_id": ".elser_model_2_linux-x86_64"
  }
}<p>Pour cet exemple, imaginons que vous ayez une pharmacie qui vend deux types de médicaments :</p><ul><li><p>Médicaments nécessitant une ordonnance.</p></li><li><p>Médicaments qui ne nécessitent pas d'ordonnance.</p></li></ul><p>Cette information serait incluse dans le champ de description de chaque médicament.</p><p>Le LLM doit interpréter ce champ, c'est donc le mappage des données que nous utiliserons :</p>PUT medicines
{
  "mappings": {
    "properties": {
      "name": {
        "type": "text",
        "copy_to": "semantic_field"
      },
      "semantic_field": {
        "type": "semantic_text",
        "inference_id": "medicines-inference"
      },
      "text_description": {
        "type": "text",
        "copy_to": "semantic_field"
      }
    }
  }
}<p>Le champ <code>text_description</code> stockera le texte brut des descriptions tandis que <code>semantic_field</code>, qui est un champ de type <a href="https://www.elastic.co/fr/guide/en/elasticsearch/reference/current/semantic-text.html">semantic_text</a>, stockera les enchâssements générés par ELSER.</p><p>La propriété <a href="https://www.elastic.co/fr/guide/en/elasticsearch/reference/current/copy-to.html">copy_to</a> copiera le contenu des champs name et <code>text_description</code> dans le champ sémantique afin de générer les embeddings pour ces champs.</p><h2>Indexation des données</h2><p>Maintenant, indexons les données à l'aide de l'<a href="https://www.elastic.co/fr/guide/en/elasticsearch/reference/current/docs-bulk.html">API _bulk</a>.</p>POST _bulk
{"index":{"_index":"medicines"}}
{"id":1,"name":"Paracetamol","text_description":"An analgesic and antipyretic that does NOT require a prescription."}
{"index":{"_index":"medicines"}}
{"id":2,"name":"Ibuprofen","text_description":"A nonsteroidal anti-inflammatory drug (NSAID) available WITHOUT a prescription."}
{"index":{"_index":"medicines"}}
{"id":3,"name":"Amoxicillin","text_description":"An antibiotic that requires a prescription."}
{"index":{"_index":"medicines"}}
{"id":4,"name":"Lorazepam","text_description":"An anxiolytic medication that strictly requires a prescription."}
{"index":{"_index":"medicines"}}
{"id":5,"name":"Omeprazole","text_description":"A medication for stomach acidity that does NOT require a prescription."}
{"index":{"_index":"medicines"}}
{"id":6,"name":"Insulin","text_description":"A hormone used in diabetes treatment that requires a prescription."}
{"index":{"_index":"medicines"}}
{"id":7,"name":"Cold Medicine","text_description":"A compound formula to relieve flu symptoms available WITHOUT a prescription."}
{"index":{"_index":"medicines"}}
{"id":8,"name":"Clonazepam","text_description":"An antiepileptic medication that requires a prescription."}
{"index":{"_index":"medicines"}}
{"id":9,"name":"Vitamin C","text_description":"A dietary supplement that does NOT require a prescription."}
{"index":{"_index":"medicines"}}
{"id":10,"name":"Metformin","text_description":"A medication used for type 2 diabetes that requires a prescription."}<p>Réponse :</p>{
   "errors": false,
   "took": 34732020848,
   "items": [
 	{
   	"index": {
     	"_index": "medicines",
     	"_id": "mYoeMpQBF7lnCNFTfdn2",
     	"_version": 1,
     	"result": "created",
     	"_shards": {
       	"total": 2,
       	"successful": 2,
       	"failed": 0
     	},
     	"_seq_no": 0,
     	"_primary_term": 1,
     	"status": 201
   	}
 	},
 	{
   	"index": {
     	"_index": "medicines",
     	"_id": "mooeMpQBF7lnCNFTfdn2",
     	"_version": 1,
     	"result": "created",
     	"_shards": {
       	"total": 2,
       	"successful": 2,
       	"failed": 0
     	},
     	"_seq_no": 1,
     	"_primary_term": 1,
     	"status": 201
   	}
 	},
 	{
   	"index": {
     	"_index": "medicines",
     	"_id": "m4oeMpQBF7lnCNFTfdn2",
     	"_version": 1,
     	"result": "created",
     	"_shards": {
       	"total": 2,
       	"successful": 2,
       	"failed": 0
     	},
     	"_seq_no": 2,
     	"_primary_term": 1,
     	"status": 201
   	}
 	},
 	{
   	"index": {
     	"_index": "medicines",
     	"_id": "nIoeMpQBF7lnCNFTfdn2",
     	"_version": 1,
     	"result": "created",
     	"_shards": {
       	"total": 2,
       	"successful": 2,
       	"failed": 0
     	},
     	"_seq_no": 3,
     	"_primary_term": 1,
     	"status": 201
   	}
 	},
 	{
   	"index": {
     	"_index": "medicines",
     	"_id": "nYoeMpQBF7lnCNFTfdn2",
     	"_version": 1,
     	"result": "created",
     	"_shards": {
       	"total": 2,
       	"successful": 2,
       	"failed": 0
     	},
     	"_seq_no": 4,
     	"_primary_term": 1,
     	"status": 201
   	}
 	},
 	{
   	"index": {
     	"_index": "medicines",
     	"_id": "nooeMpQBF7lnCNFTfdn2",
     	"_version": 1,
     	"result": "created",
     	"_shards": {
       	"total": 2,
       	"successful": 2,
       	"failed": 0
     	},
     	"_seq_no": 5,
     	"_primary_term": 1,
     	"status": 201
   	}
 	},
 	{
   	"index": {
     	"_index": "medicines",
     	"_id": "n4oeMpQBF7lnCNFTfdn2",
     	"_version": 1,
     	"result": "created",
     	"_shards": {
       	"total": 2,
       	"successful": 2,
       	"failed": 0
     	},
     	"_seq_no": 6,
     	"_primary_term": 1,
     	"status": 201
   	}
 	},
 	{
   	"index": {
     	"_index": "medicines",
     	"_id": "oIoeMpQBF7lnCNFTfdn2",
     	"_version": 1,
     	"result": "created",
     	"_shards": {
       	"total": 2,
       	"successful": 2,
       	"failed": 0
     	},
     	"_seq_no": 7,
     	"_primary_term": 1,
     	"status": 201
   	}
 	},
 	{
   	"index": {
     	"_index": "medicines",
     	"_id": "oYoeMpQBF7lnCNFTfdn2",
     	"_version": 1,
     	"result": "created",
     	"_shards": {
       	"total": 2,
       	"successful": 2,
       	"failed": 0
     	},
     	"_seq_no": 8,
     	"_primary_term": 1,
     	"status": 201
   	}
 	},
 	{
   	"index": {
     	"_index": "medicines",
     	"_id": "oooeMpQBF7lnCNFTfdn2",
     	"_version": 1,
     	"result": "created",
     	"_shards": {
       	"total": 2,
       	"successful": 2,
       	"failed": 0
     	},
     	"_seq_no": 9,
     	"_primary_term": 1,
     	"status": 201
   	}
 	}
   ]
 }<h2>Poser des questions à l'aide de l'aire de jeu</h2><p><a href="https://www.elastic.co/fr/guide/en/kibana/current/playground.html">Playground</a> est un outil Kibana qui vous permet de créer rapidement un système RAG en utilisant des index Elasticsearch et un fournisseur LLM. Vous pouvez lire cet <a href="https://www.elastic.co/fr/search-labs/blog/playground-connectors-data-chat">article</a> pour en savoir plus.</p><h3>Connecter le programme local d'éducation et de formation tout au long de la vie à l'aire de jeux</h3><p>Nous devons d'abord créer un connecteur qui utilise l'URL publique que nous venons de créer. Dans Kibana, allez sur <strong>Search&gt;Playground</strong> et cliquez sur "Connect to an LLM".</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt22148eabfabf6d3f/6a17f3963e9e459f97ba15c6/1854f0808f8150e359fe62ba5d901d32a88d477c-1600x867.png" alt="Connecter le mécanisme local d'apprentissage tout au long de la vie au terrain de jeu d'Ollama" /><p>Cette action fait apparaître un menu sur le côté gauche de l'interface Kibana. Cliquez ensuite sur "OpenAI".</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt6e28194d9012f141/6a17f39725daab500a08a36b/c83d3c4d7035a518124ad7d22b38764db57b6800-933x1007.png" alt="Sélectionnez un connecteur : Open AI Ollama" /><p>Nous pouvons maintenant commencer à configurer le connecteur OpenAI.</p><p>Allez sur "Connector settings" et pour le fournisseur OpenAI, sélectionnez "Other (OpenAI Compatible Service)":</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt9984dce6f78a7c08/6a17f3990b0bed0b7add36c4/ecfcdc4b575c309bd55b4e61ca0ddb348aa84f64-917x268.png" alt="Définir les paramètres du connecteur pour l'utilisation d'Ollama avec l'API Inference" /><p>Configurons maintenant les autres champs. Pour cet exemple, nous nommerons notre modèle "medicines-llm". Dans le champ URL, utilisez celle générée par ngrok (/v1/chat/completions). Dans le champ "Modèle par défaut", sélectionnez "llama3.2". Nous n'utiliserons pas de clé API, vous pouvez donc saisir n'importe quel texte au hasard :</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt8a24b93a39d380fb/6a17f39b96142a15c7eb1c3c/5d3b5027c8096cbe49fb740d70aa24e849611a9d-916x688.png" alt="Ajouter des paramètres" /><p>Cliquez sur "Sauvegarder" et ajoutez les médicaments de l'index en cliquant sur "Ajouter des sources de données":</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt4f107a54d5be25f9/6a17f39d4b055deb9d432338/525113da59e902c8235f62bde8fb62371a63e11b-1579x753.png" alt="Ajouter des sources de données pour poser des questions à vos documents à l'aide de Playground" /><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt03fb36fe05dcdbf5/6a17f39ebe608602f40048be/96138de0bbe2c2ac619f64889d3487df62739ca4-466x805.png" alt="Ajouter des données d'interrogation" /><p>Excellent ! Nous avons maintenant accès à Playground en utilisant le LLM que nous exécutons localement en tant que moteur RAG.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltb1b27580107259b3/6a17f3a096142abefceb1c40/cfb48b33c70f4534ab77eb01f58008237f65e6f4-1600x851.png" alt="Sélectionner les paramètres du modèle dans l'aire de jeu" /><p>Avant de le tester, ajoutons des instructions plus spécifiques à l'agent et augmentons le nombre de documents envoyés au modèle à 10, afin que la réponse dispose du plus grand nombre possible de documents. Le champ contextuel sera <code>semantic_field</code>, qui comprend le nom et la description des médicaments, grâce à la propriété copy_to.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt4fbc97dc87c6fc62/6a17f3a1e8fbce052c3a1aa0/0c57c9c0e1a0e7b58fffdd3ef81d67d41e2990c4-580x806.png" alt="Paramètres de Moel dans Elastic Playground" /><p>Posons maintenant la question : <em><strong>Puis-je acheter du Clonazepam sans ordonnance ?</strong></em> et voir ce qui se passe :</p><p>Comme prévu, nous avons obtenu la bonne réponse.</p><h3>Étapes suivantes</h3><p>L'étape suivante consiste à créer votre propre application ! Playground fournit un code script en Python que vous pouvez exécuter sur votre machine et adapter à vos besoins. Par exemple, en le plaçant derrière un serveur <a href="https://fastapi.tiangolo.com/">FastAPI</a> pour créer un chatbot d'assurance qualité consommé par votre interface utilisateur.</p><p>Vous pouvez trouver ce code en cliquant sur le bouton <em><strong>Voir le code</strong></em> dans la partie supérieure droite de l'aire de jeux :</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt42fe193b8aa08830/6a17f3a33e9e4569e8ba15ca/816bfd0e5f936ad65dbe719d5df10714e550a40b-380x121.png" alt="Bouton de visualisation du code" /><p>Et vous utilisez les <em><strong>clés de l'API Endpoints &amp; </strong></em> pour générer la variable d'environnement <code>ES_API_KEY</code> requise dans le code.</p><p>Pour cet exemple particulier, le code est le suivant :</p>## Install the required packages
## pip install -qU elasticsearch openai
import os
from elasticsearch import Elasticsearch
from openai import OpenAI
es_client = Elasticsearch(
    "https://your-deployment.us-central1.gcp.cloud.es.io:443",
    api_key=os.environ["ES_API_KEY"]
)
openai_client = OpenAI(
    api_key=os.environ["OPENAI_API_KEY"],
)
index_source_fields = {
    "medicines": [
        "semantic_field"
    ]
}
def get_elasticsearch_results():
    es_query = {
        "retriever": {
            "standard": {
                "query": {
                    "nested": {
                        "path": "semantic_field.inference.chunks",
                        "query": {
                            "sparse_vector": {
                                "inference_id": "medicines-inference",
                                "field": "semantic_field.inference.chunks.embeddings",
                                "query": query
                            }
                        },
                        "inner_hits": {
                            "size": 2,
                            "name": "medicines.semantic_field",
                            "_source": [
                                "semantic_field.inference.chunks.text"
                            ]
                        }
                    }
                }
            }
        },
        "size": 3
    }
    result = es_client.search(index="medicines", body=es_query)
    return result["hits"]["hits"]
def create_openai_prompt(results):
    context = ""
    for hit in results:
        inner_hit_path = f"{hit['_index']}.{index_source_fields.get(hit['_index'])[0]}"
        ## For semantic_text matches, we need to extract the text from the inner_hits
        if 'inner_hits' in hit and inner_hit_path in hit['inner_hits']:
            context += '\n --- \n'.join(inner_hit['_source']['text'] for inner_hit in hit['inner_hits'][inner_hit_path]['hits']['hits'])
        else:
            source_field = index_source_fields.get(hit["_index"])[0]
            hit_context = hit["_source"][source_field]
            context += f"{hit_context}\n"
    prompt = f"""
  Instructions:
  - You are an assistant specializing in answering questions about the sale of medicines.
  - Answer questions truthfully and factually using only the context presented.
  - If you don't know the answer, just say that you don't know, don't make up an answer.
  - You must always cite the document where the answer was extracted using inline academic citation style [], using the position.
  - Use markdown format for code examples.
  - You are correct, factual, precise, and reliable.
  Context:
  {context}
  """
    return prompt
def generate_openai_completion(user_prompt, question):
    response = openai_client.chat.completions.create(
        model="gpt-3.5-turbo",
        messages=[
            {"role": "system", "content": user_prompt},
            {"role": "user", "content": question},
        ]
    )
    return response.choices[0].message.content
if __name__ == "__main__":
    question = "my question"
    elasticsearch_results = get_elasticsearch_results()
    context_prompt = create_openai_prompt(elasticsearch_results)
    openai_completion = generate_openai_completion(context_prompt, question)
    print(openai_completion)<p>Pour que cela fonctionne avec Ollama, vous devez modifier le client OpenAI pour qu'il se connecte au serveur Ollama au lieu du serveur OpenAI. Vous pouvez trouver la liste complète des exemples OpenAI et des points de terminaison compatibles ici.</p>openai_client = OpenAI(
    # you can use http://localhost:11434/v1/ if running this code locally.
    base_url='https://your-ngrok-url.ngrok-free.app/v1/',
    # required but ignored
    api_key='ollama',
)<p>Il faut également changer le modèle en llama3.2 lors de l'appel de la méthode d'achèvement :</p>def generate_openai_completion(user_prompt, question):
    response = openai_client.chat.completions.create(
        model="llama3.2",
        messages=[
            {"role": "system", "content": user_prompt},
            {"role": "user", "content": question},
        ]
    )
    return response.choices[0].message.content<p>Ajoutons notre question : <em><strong>Puis-je acheter du Clonazepam sans ordonnance ? </strong></em>Pour la requête Elasticsearch :</p>def get_elasticsearch_results():
    es_query = {
        "retriever": {
            "standard": {
                "query": {
                    "nested": {
                        "path": "semantic_field.inference.chunks",
                        "query": {
                            "sparse_vector": {
                                "inference_id": "medicines-inference",
                                "field": "semantic_field.inference.chunks.embeddings",
                                "query": "Can I buy Clonazepam without a prescription?"
                            }
                        },
                        "inner_hits": {
                            "size": 2,
                            "name": "medicines.semantic_field",
                            "_source": [
                                "semantic_field.inference.chunks.text"
                            ]
                        }
                    }
                }
            }
        },
        "size": 3
    }
    result = es_client.search(index="medicines", body=es_query)
    return result["hits"]["hits"]<p>Et aussi à l'appel d'achèvement avec quelques impressions, pour que nous puissions confirmer que nous envoyons les résultats d'Elasticsearch dans le cadre du contexte de la question :</p>if __name__ == "__main__":
    question = "Can I buy Clonazepam without a prescription?"
    elasticsearch_results = get_elasticsearch_results()
    context_prompt = create_openai_prompt(elasticsearch_results)
    print("========== Context Prompt START ==========")
    print(context_prompt)
    print("========== Context Prompt END ==========")
    print("========== Ollama Completion START ==========")
    openai_completion = generate_openai_completion(context_prompt, question)
    print(openai_completion)
    print("========== Ollama Completion END ==========")<p>Exécutons maintenant la commande</p><p><code>pip install -qU elasticsearch openai</code></p><p><code>python main.py</code></p><p>Vous devriez voir quelque chose comme ceci :</p>========== Context Prompt START ==========
  Instructions:
  - You are an assistant specializing in answering questions about the sale of medicines.
  - Answer questions truthfully and factually using only the context presented.
  - If you don't know the answer, just say that you don't know, don't make up an answer.
  - You must always cite the document where the answer was extracted using inline academic citation style [], using the position.
  - Use markdown format for code examples.
  - You are correct, factual, precise, and reliable.
  Context:
  Clonazepam
 ---
An antiepileptic medication that requires a prescription.A nonsteroidal anti-inflammatory drug (NSAID) available WITHOUT a prescription.
 ---
IbuprofenAn anxiolytic medication that strictly requires a prescription.
 ---
Lorazepam


========== Context Prompt END ==========
========== Ollama Completion START ==========
No, you cannot buy Clonazepam over-the-counter (OTC) without a prescription [1]. It is classified as a controlled substance in the United States due to its potential for dependence and abuse. Therefore, it can only be obtained from a licensed healthcare provider who will issue a prescription for this medication.
========== Ollama Completion END ==========<h2>Conclusion</h2><p>Dans cet article, nous pouvons voir la puissance et la polyvalence d'outils comme Ollama lorsque nous les utilisons avec l'API d'inférence Elasticsearch et Playground.</p><p>Après quelques étapes simples, nous avions une application RAG fonctionnelle avec un chat qui utilisait un LLM fonctionnant dans notre propre infrastructure à un coût nul. Cela nous permet également de mieux contrôler les ressources et les informations sensibles, tout en nous donnant accès à une variété de modèles pour différentes tâches.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/ollama-with-inference-api</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/ollama-with-inference-api</guid>
    <category><![CDATA[IA]]></category>
    <dc:creator><![CDATA[Jeffrey Rengifo]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltd9c8eb0fc946920e/6a17f3a46864a4b2fbb688f0/399b9ef527be633845fb6505b68132cc03bc9e09-1150x628.png" length="0" type="image/png"/>
    <pubDate>Fri, 14 Feb 2025 00:00:00 GMT</pubDate>
  </item>
  </channel>
</rss>