Articles de blog

Utilisation de l'API d'inférence Elasticsearch avec les modèles Hugging Face

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.

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 service d'inférence Hugging Face. 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 SmolLM3-3B, un modèle léger et polyvalent offrant un bon compromis entre consommation de ressources et qualité des réponses.

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.

Produits requis

  • Elasticsearch 9.3 ou Elastic Cloud Serverless : vous pouvez créer un déploiement dans le cloud en suivant ces instructions, ou utiliser le démarrage rapide start-local à la place.

  • Python 3.12 : téléchargez Python ici.

  • Jeton d'accès Hugging Face.

Complétions de chat utilisant un point de terminaison d'inférence Hugging Face

Nous allons d'abord créer un exemple pratique connectant Elasticsearch à un point de terminaison d'inférence 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.

Avec ce point de terminaison, la recherche sémantique 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.

Examinons d'abord les grandes lignes du flux d'informations que nous allons mettre en place :

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.

Dans cet article, nous allons tester la capacité de SmolLM3-3B à à 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.

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.

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.

Vous trouverez la mise en œuvre complète de cette application dans le notebook associé.

Configuration des points de terminaison d’inférence Elasticsearch

Pour utiliser le point de terminaison d'inférence Hugging Face d'Elasticsearch, 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 :

PUT _inference/chat_completions/hugging-face-smollm3-3b
{
    "service": "hugging_face",
    "service_settings": {
        "api_key": "hugging-face-access-token", 
        "url": "url-endpoint" 
    }
}

Le point de terminaison d'inférence Hugging Face dans Elasticsearch prend en charge différents types de tâches : text_embedding, completion, chat_completion et rerank. Dans cet article de blog, nous utilisons chat_completion, 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 :

POST _inference/chat_completion/hugging-face-smollm3-3b/_stream
{
  "messages": [
      { "role": "user", "content": "<user prompt>" }
  ]
}

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.

Configuration du point de terminaison d'inférence sur Hugging Face

Pour déployer le modèle Hugging Face, nous allons utiliser le service de déploiement en un clic de Hugging Face, 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.

Vous pouvez choisir un modèle dans le catalogue accessible en un clic :

Vue d'interface d'un catalogue de modèles filtré sur "smoll3", montrant un modèle nommé "smollm3‑3b" 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.

Sélectionnons le modèle SmolLM3-3B :

Interface permettant de créer un point de terminaison pour le modèle SmolLM3‑3B, affichant le nom du modèle, une note "vérifié par Hugging Face", 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 "Créer un point de terminaison".

À partir d'ici, veuillez récupérer l'URL du point de terminaison Hugging Face :

Vue du tableau de bord d'un point de terminaison d'inférence Hugging Face nommé "smollm3‑3b‑pnz", 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é.

Comme indiqué dans la documentation Elasticsearch relative aux points de terminaison d'inférence Hugging Face, 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 /v1/chat/completions à à l'URL de point de terminaison Hugging Face. Le résultat final ressemblera à ceci :

https://j2g31h0futopfkli.us-east-1.aws.endpoints.huggingface.cloud/v1/chat/completions

Une fois ces éléments en place, nous pouvons commencer à coder dans un notebook Python.

Génération de la clé API Hugging Face

Créez un compte Hugging Face et obtenez un jeton API en suivant ces instructions. Vous avez le choix entre trois types de jetons : un jeton granulaire (recommandé pour la production, car il ne donne accès qu'à des ressources spécifiques), un jeton de lecture (pour un accès en lecture seule) ou un jeton d'écriture (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.

Configuration du point de terminaison d'inférence Elasticsearch

Tout d'abord, déclarons un client Elasticsearch Python :

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"]
)

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.

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"],
            },
        },
    )

Ensemble de données

L'ensemble de données contient les articles de blog sur lesquels des requêtes seront exécutées ; il s'agit d'un ensemble de contenus multilingues utilisé tout au long du workflow :

// 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."
  }

Mappings Elasticsearch

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 mappings d'index suivants seront utilisés pour stocker les données dans Elasticsearch :

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)

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é copy_to pour copier le contenu du champ dans le champ semantic_text. De plus, le champ title contient deux sous-champs : le sous-champ original stocke le titre en anglais ou en espagnol, selon la langue d'origine de l'article, et le sous-champ translated_title n'est présent que pour les articles en espagnol et contient la traduction anglaise du titre original.

Ingestion des données

L'extrait de code suivant ingère l'ensemble de données des articles de blog dans Elasticsearch à l'aide de l'API Bulk :

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)}")

Maintenant que nous avons intégré les articles dans Elasticsearch, nous devons créer une fonction capable de rechercher dans le champ semantic_text :

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 []

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 chat_completion pour obtenir des réponses en streaming :

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"]) > 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)}"

Nous pouvons maintenant écrire une fonction qui appelle la fonction de recherche sémantique, ainsi que le point de terminaison d'inférence chat_completions et le point de terminaison de recommandations, afin de générer les données qui seront allouées dans les fiches :

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

Enfin, nous devons extraire les informations et les mettre en forme pour l'impression :

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 <think> tags
        cleaned_text = re.sub(
            r"<think>.*?</think>", "", 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 + "┘")

Faisons un test en posant une question sur les articles de blog relatifs à la sécurité :

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)

Nous pouvons voir ici les fiches générées par le workflow dans la console :

Section intitulée "Articles recommandés" 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.

Vous trouverez les résultats complets, y compris tous les résultats et la réponse du LLM dans ce fichier.

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.

Conclusion

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.

Pour aller plus loin

Agent Builder, bien plus qu’une interface de discussion : vers une infrastructure augmentée

Alexander Wert

Gestion de la mémoire agentique avec Elasticsearch.

Someshwaran Mohankumar

Construire un agent d'IA pour les RH avec Elastic Agent Builder et GPT-OSS

Tomás Murúa

Création d'un serveur Elasticsearch MCP avec TypeScript

Jeffrey Rengifo

L'outil shell n'est pas une solution miracle pour l'ingénierie du contexte

Leonie Monigatti

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

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

Jugez-en par vous-même