Blog

Uso de la API de inferencia de Elasticsearch junto con modelos de Hugging Face

Aprende a conectar Elasticsearch a modelos de Hugging Face usando endpoints de inferencia y crea un sistema multilingüe de recomendación de blogs con búsqueda semántica y finalización de chat.

En actualizaciones recientes, Elasticsearch introdujo una integración nativa para conectar a modelos hospedados en el servicio de inferencia Hugging Face. En esta publicación, veremos cómo configurar esta integración y realizar inferencias mediante llamadas a la API sencillas utilizando un modelo de lenguaje grande (LLM). Usaremos SmolLM3-3B, un modelo ligero de propósito general con un buen equilibrio entre el uso de recursos y la calidad de las respuestas.

Diagrama de dispersión que muestra varios modelos de lenguaje pequeños graficados por tamaño del modelo (en miles de millones de parámetros) en el eje X y tasa de victorias (porcentaje) en el eje Y. SmolLM3‑3B aparece cerca de la parte superior de la tendencia de eficiencia, con una tasa de victorias más alta que otros modelos de tamaño similar.

Requisitos previos

Finalización de chat usando un endpoint de inferencia de Hugging Face

Primero, vamos a crear un ejemplo práctico que conecte Elasticsearch con un endpoint de inferencia de Hugging Face para generar recomendaciones basadas en IA a partir de una colección de publicaciones de blog. Para la base de conocimientos de la app, usaremos un set de datos de artículos del blog de la compañía, que contiene información valiosa pero a menudo difícil de navegar.

Con este endpoint, la búsqueda semántica recupera los artículos más relevantes para una consulta dada, y un LLM de Hugging Face genera recomendaciones breves y contextuales basadas en esos resultados.

Echemos un vistazo a una visión general de alto nivel del flujo de información que vamos a construir:

Diagrama de flujo que muestra un índice de Elasticsearch que envía resultados de búsqueda semántica a un endpoint de inferencia, el cual devuelve recomendaciones de artículos.

En este artículo, probaremos la capacidad de SmolLM3-3B para combinar su tamaño compacto con fuertes capacidades multilingües de razonamiento y llamadas a herramientas. A partir de una búsqueda, enviaremos todo el contenido correspondiente (en inglés y español) al LLM para generar una lista de artículos recomendados con una descripción personalizada basada en la búsqueda y los resultados.

Así podría ser la UI de un sitio web de artículos con un sistema de generación de recomendaciones basado en inteligencia artificial.

UI de un sitio web de artículos con un sistema de recomendaciones basado en IA; se incluyen tres ejemplos, con el texto en inglés y los títulos en inglés o en español.

Puedes encontrar la implementación completa de esta aplicación en el cuaderno adjunto.

Configurar endpoints de inferencia de Elasticsearch

Para usar el endpoint de inferencia Elasticsearch Hugging Face, necesitamos dos elementos importantes: una clave API de Hugging Face y una URL del endpoint de Hugging Face en funcionamiento. Debería verse así:

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

El endpoint de inferencia Hugging Face en Elasticsearch admite diferentes tipos de tareas: text_embedding, completion, chat_completion y rerank. En esta publicación de blog, usamos chat_completion porque necesitamos que el modelo genere recomendaciones conversacionales basadas en los resultados de búsqueda y una solicitud del sistema. Este endpoint nos permite realizar finalizaciones de chat directamente desde Elasticsearch de una manera sencilla empleando la API de Elasticsearch:

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

Esto servirá como núcleo de la aplicación, recibirá la indicación y los resultados de búsqueda que pasarán por el modelo. Ya abordamos la teoría, ahora comencemos a implementar la aplicación.

Configuración de un endpoint de inferencia en Hugging Face

Para desplegar el modelo de Hugging Face, vamos a usar despliegues con un clic de Hugging Face, un servicio fácil y rápido para desplegar endpoints de modelos. Ten en cuenta que este es un servicio de pago y que su uso puede generar costos adicionales. En este paso se creará la instancia del modelo que se usará para generar las recomendaciones de artículos.

Puedes elegir un modelo del catálogo de un clic:

Vista de la interfaz de un catálogo de modelos filtrado por “smoll3”, que muestra un modelo llamado “smollm3‑3b” con generación de texto, vLLM, GPU 1× NVIDIA L4 y un precio de lista de $0.8, además de una nota que sugiere ampliar la búsqueda a todos los modelos de Hugging Face.

Vamos a elegir el modelo SmolLM3-3B:

Interfaz para crear un endpoint para el modelo SmolLM3‑3B, muestra el nombre del modelo, una nota de "verificado por Hugging Face", un campo de nombre de endpoint, un costo de $0.80 por hora por réplica en ejecución, una opción de cURL y un botón de "Crear Endpoint".

Desde aquí, copia la URL del endpoint de Hugging Face:

Vista del dashboard de un endpoint de inferencia de Hugging Face llamado “smollm3‑3b‑pnz”, que muestra un estado de ejecución verde, una réplica activa, cero solicitudes en la última hora, pestañas de navegación y la URL del endpoint.

Como se menciona en la documentación de endpoints de inferencia Hugging Face de Elasticsearch, la generación de texto requiere un modelo compatible con la API de OpenAI. Por esa razón, necesitamos anexar la ruta secundaria /v1/chat/completions a la URL del endpoint de Hugging Face. El resultado final se verá así:

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

Ahora que está todo listo, podemos empezar a programar en un cuaderno de Python.

Generando clave API de Hugging Face

Crea una cuenta en Hugging Face y obtén un token de API siguiendo estas instrucciones. Puedes elegir entre tres tipos de token: detallado (recomendado para producción, ya que proporciona acceso solo a recursos específicos); lectura (para acceso de solo lectura); o escritura (para acceso de lectura y escritura). Para este tutorial, un token de lectura es suficiente, ya que solo necesitamos llamar al endpoint de inferencia. Guarda esta clave para el siguiente paso.

Configuración del endpoint de inferencia de Elasticsearch

Primero, declaremos un cliente de Python para Elasticsearch:

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

A continuación, vamos a crear un endpoint de inferencia de Elasticsearch que use el modelo Hugging Face. Este endpoint nos permitirá generar respuestas basadas en las entradas del blog y en el prompt que se pasó al modelo.

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

Set de datos

El conjunto de datos contiene las publicaciones de blog que se consultarán, representando un conjunto de contenido multilingüe utilizado a lo largo del flujo de trabajo:

// 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 de Elasticsearch

Una vez definido el set de datos, necesitamos crear un esquema de datos que se ajuste correctamente a la estructura de la publicación de blog. Se emplearán las siguientes mappings de índices para almacenar los datos en 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)

Aquí, podemos ver mejor cómo se estructuran los datos. Usaremos la búsqueda semántica para recuperar resultados basados en lenguaje natural, junto con la propiedad copy_to para copiar el contenido del campo en el campo semantic_text. Además, el campo title contiene dos subcampos: el subcampo original almacena el título en inglés o español, dependiendo del idioma original del artículo; y el subcampo translated_title está presente solo para artículos en español y contiene la traducción al inglés del título original.

Ingesta de datos

El siguiente fragmento de código ingesta el conjunto de datos de las publicaciones de blog en Elasticsearch mediante la API de 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)}")

Ahora que tenemos los artículos ingeridos en Elasticsearch, necesitamos crear una función capaz de buscar en el campo 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 []

También necesitamos una función que llame al endpoint de inferencia. En este caso, llamaremos al endpoint usando el tipo de tarea chat_completion para obtener respuestas de transmisión:

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

Ahora podemos escribir una función que llame a la función de búsqueda semántica, junto con el endpoint de inferencia chat_completions y el endpoint de recomendaciones, para generar los datos que se asignarán en las tarjetas:

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

Por último, extrae la información y dale formato para imprimirla:

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 + "┘")

Pongámoslo a prueba haciendo una pregunta sobre las publicaciones de blog de seguridad:

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)

Aquí podemos ver las tarjetas de la consola generadas por el flujo de trabajo:

Sección titulada “Artículos recomendados” que muestra cinco resúmenes de artículos en cajas, incluso temas sobre una vulnerabilidad del sistema de autenticación, riesgos de migración, mejoras en el rendimiento y autenticación de la API REST v2, cambios en el sistema de notificaciones y una guía completa de la nueva API.

Puedes ver los resultados completos, incluso todas las coincidencias y la respuesta del LLM, en este archivo.

Estamos solicitando artículos relacionados con: “Seguridad y vulnerabilidades”. Esta pregunta se emplea como consulta de búsqueda en los documentos almacenados en Elasticsearch. Los resultados obtenidos se pasan al modelo, el cual genera recomendaciones basadas en su contenido. Como podemos ver, el modelo hizo un excelente trabajo generando textos cortos y atractivos que pueden motivar al lector a hacer clic en ellos.

Conclusión

Este ejemplo muestra cómo se pueden combinar Elasticsearch y Hugging Face para crear un sistema centralizado rápido y eficiente para aplicaciones de IA. Este enfoque reduce el esfuerzo manual y proporciona flexibilidad, gracias al extenso catálogo de modelos de Hugging Face. El uso de SmolLM3-3B, en particular, muestra cómo los modelos compactos y multilingües aún pueden ofrecer razonamiento significativo y generación de contenido cuando se combinan con la búsqueda semántica. En conjunto, estas herramientas ofrecen una base escalable y eficaz para construir análisis de contenidos inteligentes y aplicaciones multilingües.

Contenido relacionado

Agent Builder, más allá del chat: presentamos la infraestructura aumentada

Alexander Wert

Gestión de la memoria agentic con Elasticsearch

Someshwaran Mohankumar

Construir un agente de IA para RRHH con Elastic Agent Builder y GPT-OSS

Tomás Murúa

Cómo crear un servidor MCP de Elasticsearch con TypeScript

Jeffrey Rengifo

La herramienta de shell no es una solución mágica para la ingeniería de contexto

Leonie Monigatti

¿Estás listo para crear experiencias de búsqueda de última generación?

No se logra una búsqueda suficientemente avanzada con los esfuerzos de uno. Elasticsearch está impulsado por científicos de datos, operaciones de ML, ingenieros y muchos más que son tan apasionados por la búsqueda como tú. Conectemos y trabajemos juntos para crear la experiencia mágica de búsqueda que te dará los resultados que deseas.

Pruébalo tú mismo