<?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/es/search-labs/author/jeffrey-rengifo</link>
    </image>
    <link>https://www.elastic.co/es/search-labs/author/jeffrey-rengifo</link>
    <atom:link href="https://www.elastic.co/es/search-labs/rss/author/jeffrey-rengifo.xml" rel="self" type="application/rss+xml"/>
    <language><![CDATA[es]]></language>
    <lastBuildDate>Mon, 28 Sep 2026 05:13:50 GMT</lastBuildDate>
  <item>
    <title><![CDATA[Cómo medir y mejorar la recuperación de búsqueda de Elasticsearch: de 0,43 a 0,75 con búsqueda híbrida]]></title>
    <description><![CDATA[Aprende a medir y mejorar la recuperación de búsqueda en Elasticsearch combinando la búsqueda léxica BM25 con incrustaciones vectoriales de Jina AI, usando la API rank_eval para validar la mejora con cifras reales.]]></description>
    <content:encoded><![CDATA[<p><a href="https://www.elastic.co/docs/solutions/search/full-text">La búsqueda léxica</a> mediante el <a href="https://www.elastic.co/blog/practical-bm25-part-1-how-shards-affect-relevance-scoring-in-elasticsearch">algoritmo de clasificación BM25</a> es económica, rápida y muy eficaz para una amplia gama de consultas. Sin embargo, tiene un punto ciego: las consultas que no comparten tokens con tus documentos. En este artículo, mencionaremos con precisión en qué aspectos se queda corto el BM25. Emplearemos la <a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/search-rank-eval">API de evaluación de clasificación</a> (<code>rank_eval</code>) de Elasticsearch y cerraremos esa brecha agregando <a href="https://www.elastic.co/search-labs/es/blog/jina-embeddings-v3-elastic-inference-service">incrustaciones de Jina AI</a> mediante <a href="https://www.elastic.co/docs/explore-analyze/elastic-inference/eis">Elastic Inference Service</a> (EIS). Verás cómo la puntuación de recuperación pasa de <code>0.43</code> a <code>0.75</code> y entenderás por qué.</p><h2>¿Qué es la recuperación?</h2><p><a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/search-rank-eval#k-recall">La recuperación</a> mide en una escala de <code>0</code> a <code>1</code> cuántos de los documentos que realmente quieren tus usuarios aparecen en algún lugar de los resultados de búsqueda. Si una consulta debe mostrar tres productos y la búsqueda solo arroja dos de ellos en los 10 principales, <code>recall@10 = 0.67</code> para esa consulta. Es una métrica basada en conjuntos: no le importa la posición de los documentos relevantes dentro de esos <em>k</em> resultados. Un documento relevante en la posición 10 cuenta igual que uno en la posición 1. Tener una recuperación alta significa que no estás perdiendo resultados relevantes.</p><p>
</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt5ffd147b13705680/6a170a6fe8fbce11a539fc22/b13af2a5d0ca055535d8bfe3dfe4b3d1093ee6da-1457x796.png" alt="Diagrama de Venn que ilustra cómo se calcula el Recall@10, mostrando la superposición entre todos los documentos relevantes y los 10 primeros resultados recuperados por BM25, lo que da como resultado una puntuación de Recall@10 de 0.40." /><p>El diagrama muestra dos conjuntos: todos los documentos relevantes (izquierda) y lo que BM25 realmente recuperó (10 principales, derecha). Solo la intersección cuenta para la recuperación, se encontraron <code>prod_1</code> y <code>prod_2</code>, mientras que <code>prod_3</code>, <code>prod_4</code> y <code>prod_6</code> se perdieron por completo. Resultado: <code>Recall@10 = 2/5 = </code><strong><code>0.40</code></strong>.</p><h2>Requisitos previos</h2><p>Pongamos manos a la obra para comprender mejor cómo funciona la recuperación. Esta demostración utiliza Python. Puedes seguirlo en el cuaderno complementario (<a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/relevance-tuning-improving-recall-adding-vectors/notebook.ipynb">notebook.ipynb</a>), donde cada bloque de código es una celda lista para ejecutarse.</p><p>El código proporcionado utiliza lo siguiente:</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 archivo <code>.env</code> con tus credenciales de Elasticsearch</p></li></ul>ELASTICSEARCH_URL=https://your-cluster-url
ELASTICSEARCH_API_KEY=your-api-key<h2>El set de datos</h2><p>Emplearemos un catálogo con 1000 productos, que abarcan categorías como calzado, electrónica, herramientas y más.</p><p>Cada documento tiene cuatro campos:</p><p>Campo</p><p>Tipo</p><p>`título`</p><p>texto</p><p>'descripción'</p><p>texto</p><p>`marca`</p><p>palabra clave</p><p>'categoría'</p><p>palabra clave</p><p>El set de datos se carga desde <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>El poder y los límites de la búsqueda léxica</h2><p>BM25 es el algoritmo de clasificación predeterminado en Elasticsearch y en la mayoría de los motores de búsqueda. Califica los documentos según la frecuencia con la que aparecen tus términos de consulta en ellos, ajustado por la longitud del documento y la frecuencia de esos términos en todo el índice. Los <a href="https://www.elastic.co/docs/reference/text-analysis/analyzer-reference">analizadores</a> aparecen en la parte superior: normalización de minúsculas, derivación y eliminación de palabras vacías. Una búsqueda de "zapatillas para correr" coincidirá con "Zapatillas para correr" y probablemente también con "correr".</p><p>Esto funciona bien para una amplia clase de consultas:</p><ul><li><p>"zapatillas de correr" muestra de inmediato los productos que contienen exactamente esas palabras en el título.</p></li><li><p>"parlante con bluetooth" muestra productos de audio portátiles porque los tokens aparecen textualmente.</p></li></ul><p>Los resultados son deterministas y explicables: un documento se clasifica alto porque los términos de la consulta aparecen en él. Depurar la relevancia es sencillo.</p><h3>Cuándo falla</h3><p>Ahora probemos estas búsquedas en el mismo catálogo:</p><ul><li><p><strong>"rutina de skincare":</strong> la palabra "rutina" no aparece en ningún título de producto. BM25 puede hacer una coincidencia parcial con "skincare", pero los sueros faciales, aceites corporales y humectantes se describen utilizando términos como "vitamina C", "retinol" o "aclarante", ninguno de los cuales se superpone con la consulta. Los productos que forman una rutina completa de skincare están dispersos en el índice sin ningún token compartido para anclarlos.</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>"accesorios de viaje para mascotas":</strong> esta es una agrupación de casos de uso, no una categoría de producto. Un transportín para perros, un asiento para mascotas en el automóvil y una jaula de viaje son todos relevantes, pero sus descripciones hablan sobre portabilidad, seguridad y comodidad en vez de "accesorios de viaje". BM25 coincide con "mascota" de manera amplia, pero no tiene ninguna señal para distinguir los productos específicos de viaje del resto del catálogo de mascotas.</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>Este es un <strong>problema de recuperación</strong>. Los documentos relevantes están en tu índice. BM25 simplemente no puede encontrarlos porque las palabras del usuario y las del documento no coinciden lo suficiente.</p><p>Agregar sinónimos es útil en casos conocidos. Pero no se puede enumerar todas las formas en que un usuario podría expresar una intención. Ahí es donde entran los vectores.</p><h2>Por qué deberías medir el recall</h2><p>Antes de solucionar un problema, necesitas cuantificarlo.</p><p><a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/search-rank-eval#k-recall"><strong>Recall@k</strong></a> mide cuántos de los documentos que realmente buscan tus usuarios aparecen en algún lugar de los resultados de búsqueda. Es decir:</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> mide los mejores k resultados y cuántos son realmente relevantes:</p>Precision@k = (relevant documents in top k) / k<p>La alta precisión significa que los resultados que se devuelven son buenos. En el comercio electrónico, no mostrar un producto relevante (baja recuperación) suele ser peor que mostrar un resultado ligeramente imperfecto (menor precisión), porque un producto oculto es una venta perdida.</p><p>La API <code>rank_eval</code> de Elasticsearch te permite medir ambos de forma sistemática. Proporcionas una lista de consultas, cada una con un conjunto de documentos calificados, y Elasticsearch calcula las métricas por ti en todas las consultas.</p><h2>Configuración de la evaluación</h2><p>La API <code>rank_eval</code> necesita un <strong>set de datos de calificaciones</strong>: un mapeo de consultas a los documentos que son relevantes para cada uno, junto con un grado de relevancia (0 = no relevante, 1 = relevante, 2 = altamente relevante).</p><p>En el cuaderno, esta es la <a href="https://www.elastic.co/docs/solutions/search/ranking/learning-to-rank-ltr#learning-to-rank-judgement-list">lista de evaluaciones</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>La combinación es intencionada: <code>q1</code> es una consulta que BM25 maneja bien (tokens exactos en los títulos de productos), mientras que <code>q2</code>, <code>q3</code> y <code>q4</code> son consultas basadas en la intención donde la intención del usuario se expresa como un concepto en lugar de palabras clave específicas de producto.</p><h2>Medición de la recuperación basal de BM25</h2><p>Primero, configura el cliente de Elasticsearch e indexa los datos de texto sin procesar:</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>Ahora crea la solicitud <code>rank_eval</code> para BM25. Cada solicitud en la lista combina una consulta con sus calificaciones:</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>Resultado:</p>BM25 Recall@10: 0.43<p><code>0.43</code> significa que en las cuatro consultas, BM25 encuentra solo el 43 % de los documentos que debería encontrar. La deficiencia se concentra en las consultas basadas en la intención: "rutina de skincare" no encuentra sueros faciales ni aceites corporales porque la palabra "rutina" nunca aparece en los títulos de los productos, y "accesorios de viaje para mascotas" recupera productos para mascotas que no tienen relación con el tema, mientras que no encuentra transportines ni jaulas descritos en términos de portabilidad y seguridad en vez de "accesorios de viaje".</p><p>Esta es nuestra referencia base. Ahora tenemos un número que superar.</p><h2>Agregar búsqueda vectorial con embeddings de Jina</h2><p><a href="https://www.elastic.co/docs/solutions/search/vector"><code>Vector search</code></a> Codifica documentos y consultas como vectores de alta dimensión, un tipo de vector compuesto por cientos o miles de valores numéricos, cada uno de los cuales codifica una característica específica de los datos que representa. Los documentos con significado similar terminan cerca unos de otros en el espacio vectorial, incluso si no comparten palabras. "Equipo de gimnasio" y "conjunto de mancuernas" estarán cerca porque los conceptos están relacionados. Elegí Elasticsearch como mi base de datos vectorial porque admite la búsqueda híbrida, lo que me brinda comprensión semántica y precisión de palabras clave de inmediato.</p><p><a href="https://www.elastic.co/docs/explore-analyze/elastic-inference/eis">EIS</a> incluye soporte integrado para la incorporación de modelos a través de su <a href="https://www.elastic.co/docs/api/doc/elasticsearch/group/endpoint-inference">API de inferencia</a>.</p><h3>Paso 1: usar las incrustaciones de Jina v5 como endpoint de inferencia</h3>INFERENCE_ENDPOINT_ID = ".jina-embeddings-v5-text-small"<p>Si tu clúster tiene recursos de GPU (disponibles en Elastic Cloud y Elasticsearch 9.3+), las incrustaciones se generan en GPU, lo cual es mucho más rápido que la inferencia de CPU y elimina el compromiso de rendimiento que históricamente hacía que los vectores fueran caros a gran escala.</p><p>¿Por qué las incrustaciones de Jina en particular? <a href="https://www.elastic.co/search-labs/blog/jina-embeddings-v5-text">jina-embeddings-v5-text</a> es un modelo multilingüe (más de 119 idiomas) con una ventana de contexto de 32 000 tokens y soporte para <a href="https://arxiv.org/abs/2106.09685">adaptadores de Adaptación de Rango Bajo (LoRA)</a> específicos para tareas. Funciona bien para descripciones cortas de productos que están listas para usar. Lee más sobre el modelo <code>jina-embeddings-v5-text</code> <a href="https://huggingface.co/jinaai/jina-embeddings-v5-text-small">aquí</a>.</p><h3>Paso 2: Crear el índice con un campo semántico</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>El tipo de campo <a href="https://www.elastic.co/docs/solutions/search/semantic-search/semantic-search-semantic-text"><code>semantic_text</code></a> es la clave aquí. Es una abstracción de mayor nivel sobre <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/dense-vector"><code>dense_vector</code></a>: la apuntas a un endpoint de inferencia, y Elasticsearch se encarga de generar incrustaciones de forma automática.</p><p>La propiedad <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/copy-to"><code>copy_to</code></a> en <code>title</code> y <code>description</code> significa que el contenido de ambos campos fluye hacia <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/semantic-text"><code>semantic_field</code></a> para su incrustación, por lo que un solo vector captura la representación completa del producto.</p><h3>Paso 3: Indexar los productos</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>En tiempo de indexación, Elasticsearch llama al endpoint de inferencia para cada documento y almacena la incrustación resultante en <code>semantic_field</code>. No necesitas agregar ningún código adicional.</p><h2>Búsqueda híbrida: combinación de BM25 y vectores con RRF</h2><p>Agregar vectores mejora la recuperación, pero usar solo vectores corre el riesgo de perder precisión en las consultas de coincidencia exacta; "zapatillas para correr" aún debe clasificar primero las coincidencias literales. La búsqueda híbrida retiene el componente léxico específicamente para preservar esa precisión.</p><p>La búsqueda híbrida con <a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/reciprocal-rank-fusion">Reciprocal Rank Fusion</a> (RRF) combina lo mejor de ambos:</p><ul><li><p>BM25 se ocupa de consultas exactas y casi exactas con alta precisión.</p></li><li><p>La búsqueda semántica gestiona consultas basadas en la intención y multilingües con alto nivel de recuperación.</p></li><li><p>RRF combina las dos listas clasificadas en una sola clasificación.</p></li></ul><p>La fórmula RRF asigna a cada documento una puntuación basada en su clasificación en cada lista de resultados:</p>score = sum(1 / (rank_constant + rank))<p>Un documento que se clasifica en una posición alta en ambas listas obtiene una puntuación combinada más alta. El <code>rank_constant</code> controla cuánto peso reciben los documentos de menor rango.</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>Resultado:</p>Hybrid Recall@10: 0.75<p>La búsqueda híbrida mejora sustancialmente sobre BM25 (<code>0.43</code>) y preserva la precisión para consultas de coincidencia exacta como "zapatillas para correr".</p><h2>Resultados: Antes y después</h2><p>Aquí está la comparación completa entre los tres enfoques:</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>Resultado:</p><p>Método</p><p>Recall@10</p><p>BM25 (Lexical)</p><p>0,43</p><p>Híbrido (BM25 + Vectores)</p><p>0,75</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt5a1d72b57056fe64/6a170a71c1e8a56c58f882ab/e49f6c10516b0a48a0ad75962c6590ee07311407-700x500.png" alt="Gráfico de barras que compara Recall@10 entre la búsqueda léxica BM25 y la búsqueda híbrida que combina BM25 con vectores, lo que muestra que la búsqueda híbrida logra una recuperación mucho mayor." /><p>Desglose por consulta:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt871347f754c866d0/6a170a73839dfa40abdcfeb4/40e36dcb7b34cbf4649c512bcb60cef60f1778a6-700x500.png" alt="Gráfico de barras agrupado que compara Recall@10 entre la búsqueda léxica BM25 y la búsqueda híbrida en cuatro consultas de productos, mostrando que la búsqueda híbrida supera con constancia a la búsqueda léxica en cada consulta." /><h2>Conclusión</h2><p>A lo largo de esta publicación, vimos que la búsqueda léxica BM25 es confiable cuando los usuarios teclean consultas exactas, pero pierde recuperación cuando buscan por intención en lugar de palabras clave. Usando <code>rank_eval</code>, establecimos una línea de base reproducible para medir esa brecha con números reales. A partir de ahí, agregamos un <code>semantic_text</code> campo impulsado por incrustaciones de Jina y volvimos a ejecutar la evaluación. El resultado: la búsqueda híbrida mejoró la recuperación de <code>0.43</code> a <code>0.75</code> a la vez que conservó la precisión en las consultas de coincidencia exacta, aunque el margen real dependerá de tu combinación de consultas.</p><p>El patrón escala más allá de este ejemplo: recopila juicios de las consultas reales de tus usuarios, ejecuta <code>rank_eval</code> como línea de base, agrega <code>semantic_text</code> y vuelve a medir. Sabrás exactamente qué mejoró y en qué medida.</p><h2>Pasos siguientes</h2><ul><li><p>Aprende más sobre la recuperación y la búsqueda de vectores: <a href="https://www.elastic.co/search-labs/blog/recall-vector-search-quantization">Recuperación y cuantización de la búsqueda de vectores</a> por Jeff Vestal</p></li><li><p>Añade reclasificación para una precisión aún mejor en los resultados principales</p></li><li><p>Explora la <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/rrf.html">documentación de búsqueda híbrida de Elasticsearch</a></p></li><li><p>Lee más sobre la<a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/search-rank-eval.html"> API rank_eval</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[Búsqueda híbrida]]></category>
    <category><![CDATA[Base de datos vectorial]]></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[Cómo crear un servidor MCP de Elasticsearch con TypeScript]]></title>
    <description><![CDATA[Aprende a crear un servidor MCP de Elasticsearch con TypeScript y Claude Desktop.]]></description>
    <content:encoded><![CDATA[<p>Cuando se trabaja con grandes bases de conocimiento en Elasticsearch, encontrar información es solo la mitad de la batalla. Los ingenieros suelen necesitar sintetizar resultados de varios documentos, generar resúmenes y rastrear las respuestas hasta sus fuentes. El protocolo de contexto de modelo (MCP) proporciona una manera estandarizada de conectar Elasticsearch con aplicaciones basadas en modelos de lenguaje grande (LLM) para lograr esto. Mientras que Elastic ofrece soluciones oficiales, como Elastic Agent Builder (que incluye un <a href="https://www.elastic.co/docs/solutions/search/agent-builder/mcp-server">endpoint MCP</a> entre sus características), construir un servidor MCP personalizado te brinda control total sobre la lógica de búsqueda, el formato de resultados y cómo se pasa el contenido recuperado a un LLM para síntesis, resúmenes y citas.</p><p>En este artículo, exploraremos las ventajas de construir un servidor MCP personalizado de Elasticsearch y mostraremos cómo crear uno en TypeScript que conecte Elasticsearch con aplicaciones impulsadas por LLM.</p><h2>¿Por qué construir un servidor MCP personalizado de Elasticsearch?</h2><p>Elastic ofrece algunas alternativas para los <a href="https://www.elastic.co/docs/solutions/search/mcp">servidores MCP</a>:</p><ul><li><p><a href="https://www.elastic.co/docs/solutions/search/agent-builder/mcp-server">Servidor MCP de Elastic Agent Builder para Elasticsearch 9.2+</a></p></li><li><p><a href="https://github.com/elastic/mcp-server-elasticsearch?tab=readme-ov-file#elasticsearch-mcp-server">Servidor MCP de Elasticsearch para versiones anteriores (Python)</a></p></li></ul><p>Si necesitas más control sobre cómo tu servidor MCP interactúa con Elasticsearch, construir tu propio servidor personalizado te da la flexibilidad de adaptarlo exactamente a tus necesidades. Por ejemplo, el endpoint MCP de Agent Builder está limitado a las consultas de lenguaje de búsqueda (ES|QL) de Elasticsearch, mientras que un servidor personalizado te permite usar el DSL de consulta completo. También obtienes control sobre cómo se formatean los resultados antes de pasarlos al LLM y puedes integrar pasos de procesamiento adicionales, como el resumen impulsado por OpenAI que implementaremos en este tutorial.</p><p>Al final de este artículo, tendrás un servidor MCP en TypeScript que busca información almacenada en un índice de Elasticsearch, la resume y proporciona citas. Usaremos Elasticsearch para la recuperación, el modelo <code>gpt-4o-mini</code> de OpenAI para resumir y generar citas, y Claude Desktop como cliente MCP y UI para recibir las búsquedas de los usuarios y dar respuestas. El resultado final es un asistente de conocimiento interno que ayuda a los ingenieros a descubrir y sintetizar las mejores prácticas en los documentos técnicos de su organización.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltad9133cb083ad352/6a170c19b0367d411e72bd5b/ec5771a874cf9740d4cac6888622cbe8cd6aede7-1999x1133.png" alt="Creación de un servidor MCP de Elastic con TypeScript y Claude Desktop." /><h2>Requisitos previos:</h2><ul><li><p>Node.js 20 +</p></li><li><p>Elasticsearch</p></li><li><p>Clave de API de OpenAI</p></li><li><p>Claude Desktop</p></li></ul><h3>¿Qué es MCP?</h3><p><a href="https://www.elastic.co/what-is/mcp">MCP</a> es un estándar abierto, creado por <a href="https://www.anthropic.com/news/model-context-protocol">Anthropic</a>, que ofrece conexiones seguras y bidireccionales entre los modelos de lenguaje grande (LLM) y sistemas externos, como Elasticsearch. Puedes leer más sobre el estado actual del MCP en <a href="https://www.elastic.co/search-labs/blog/mcp-current-state">este artículo</a>.</p><p>El panorama de MCP <a href="https://www.elastic.co/search-labs/blog/mcp-current-state#mcp-project-updates:-transport,-elicitation,-and-structured-tooling">evoluciona cada día</a>, con servidores disponibles para una amplia gama de casos de uso. Además de eso, desarrollar tu propio servidor MCP personalizado es fácil, como te mostraremos en este artículo.</p><h3>Clientes del MCP</h3><p>Hay una larga <a href="https://modelcontextprotocol.io/clients">lista de clientes del MCP disponibles</a>, cada uno con sus propias características y limitaciones. Por su sencillez y popularidad, usaremos <a href="https://claude.ai/download">Claude Desktop</a> como nuestro cliente MCP. Servirá como interfaz de chat en la que los usuarios podrán hacer preguntas en lenguaje natural e invocar automáticamente las herramientas expuestas por nuestro servidor MCP para buscar documentos y generar resúmenes.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt06fd7a02042094e1/6a170c1b14b2700024e3c651/66eb0b11473347b6cf2d85718251eeac38d6249d-1999x1491.png" alt="Página de Claude Sonnet 4.5, con la nota: &quot;¿Hora del café con Claude? ¿Cómo puedo ayudarte hoy?&quot;" /><h2>Cómo crear un servidor MCP de Elasticsearch</h2><p>Con el <a href="https://github.com/modelcontextprotocol/typescript-sdk">SDK de TypeScript</a>, podemos crear fácilmente un servidor que entiende cómo hacer búsquedas en nuestros datos de Elasticsearch con base en la entrada de búsqueda del usuario.</p><p>Estos son los pasos en este artículo para integrar el servidor MCP de Elasticsearch con el cliente 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">Configurar el servidor MCP para 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">Carga el servidor MCP en Claude Desktop.</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/elastic-mcp-server-typescript-claude#test-it-out">Pruébalo.</a></p></li></ol><h3>Configurar el servidor MCP para Elasticsearch</h3><p>Para comenzar, inicialicemos una aplicación de nodo:</p>npm init -y<p>Esto creará un archivo <code>package.json</code> y, con él, podremos empezar a instalar las dependencias necesarias para esta aplicación.</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> nos dará acceso a la biblioteca de Elasticsearch para Node.js.</p></li><li><p><strong>@modelcontextprotocol/sdk</strong> proporciona las herramientas básicas para crear y administrar un servidor MCP, registrar herramientas y manejar la comunicación con los clientes de MCP.</p></li><li><p><strong>openAI</strong> permite la interacción con modelos OpenAI para generar resúmenes o respuestas en lenguaje natural.</p></li><li><p><a href="https://zod.dev/"><strong>zod</strong></a>ayuda a definir y validar esquemas estructurados para los datos de entrada y salida en cada herramienta.</p></li></ul><p><code>ts-node</code>, <code>@types/node</code> y <code>typescript</code> se usarán durante el desarrollo para escribir el código y compilar los scripts.</p><h4>Configura los sets de datos</h4><p>Para proporcionar los datos que Claude Desktop puede consultar con nuestro servidor de MCP, utilizaremos un <a href="https://github.com/Delacrobix/typescript-elasticsearch-mcp/blob/main/dataset.json">conjunto de datos de base de conocimiento interna simulado</a>. Así es como se verá un documento de este sets de datos:</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>Para cargar los datos, preparamos un script que cree un índice en Elasticsearch y cargue el set de datos en él. Puedes encontrarlo <a href="https://github.com/Delacrobix/typescript-elasticsearch-mcp/blob/main/setup.ts">aquí</a>.</p><h4>Servidor MCP</h4><p>Crea un archivo llamado <a href="https://github.com/Delacrobix/typescript-elasticsearch-mcp/blob/main/index.ts"><code>index.ts</code></a> y agrega el siguiente código para importar las dependencias y gestionar las variables de entorno:</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>Además, preparemos a los clientes para que gestionen las llamadas a Elasticsearch y OpenAI:</p>const openai = new OpenAI({
  apiKey: OPENAI_API_KEY,
});

const _client = new Client({
  node: ELASTICSEARCH_ENDPOINT,
  auth: {
    apiKey: ELASTICSEARCH_API_KEY,
  },
});<p>Para hacer nuestra implementación más robusta y asegurar entradas y salidas estructuradas, definiremos esquemas usando <a href="https://zod.dev/"><code>zod</code></a>. Esto nos permite validar los datos en tiempo de ejecución, detectar errores a tiempo y facilitar el procesamiento de las respuestas de la herramienta mediante código:</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>Descubre más sobre las salidas estructuradas <a href="https://www.elastic.co/search-labs/blog/structured-outputs-elasticsearch-guide">aquí</a>.</p><p>Ahora vamos a inicializar el servidor 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>Definición de las herramientas MCP</h4><p>Ahora que ya tenemos todo configurado, podemos empezar a desarrollar las herramientas que ofrecerá nuestro servidor MCP. Este servidor ofrece dos herramientas:</p><ul><li><p><strong><code>search_docs</code></strong><strong>: </strong>Búsquedas de documentos en Elasticsearch mediante la búsqueda de texto.</p></li><li><p><strong><code>summarize_and_cite</code></strong><strong>:</strong> Resume y sintetiza información de documentos previamente recuperados para responder a una pregunta del usuario. Esta herramienta también agrega citas que hacen referencia a los documentos originales.</p></li></ul><p>Juntas, estas herramientas forman un flujo de trabajo simple de “recuperación y resumen”, donde una herramienta busca documentos relevantes y la otra usa esos documentos para generar una respuesta resumida y citada.</p><h4>Formato de respuesta de herramienta</h4><p>Cada herramienta puede aceptar parámetros de entrada arbitrarios, pero debe responder con la siguiente estructura:</p><ul><li><p><strong>Contenido:</strong> esta es la respuesta de la herramienta en un formato no estructurado. Este campo se suele usar para mostrar texto, imágenes, audio, enlaces o contenido incrustado. Para esta aplicación, se utilizará para devolver texto formateado con la información generada por las herramientas.</p></li><li><p><strong>structuredContent: </strong>este es un retorno opcional que se usa para proporcionar los resultados de cada herramienta en un formato estructurado. Esto es útil para fines programáticos. Aunque no se usa en este servidor de MCP, puede ser útil si quieres desarrollar otras herramientas o procesar los resultados mediante programación.</p></li></ul><p>Con esa estructura en mente, comencemos con cada herramienta en detalle.</p><h4>Herramienta Search_docs</h4><p>Esta herramienta realiza una <a href="https://www.elastic.co/docs/solutions/search/full-text">búsqueda de texto completo</a> en el índice de Elasticsearch para recuperar los documentos más relevantes según la consulta del usuario. Destaca los resultados clave y ofrece una visión general rápida con puntuaciones de relevancia.</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>Configuramos </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> para que tenga una tolerancia tipográfica variable basada en la longitud del token que se está analizando. También establecemos </em><em><code>title^2</code></em><em> para aumentar la puntuación de los documentos donde se produce la coincidencia en el campo de título.</em></p><h4>herramienta de resumen y cita: summarize_and_cite</h4><p>Esta herramienta genera un resumen basado en los documentos recuperados en la búsqueda anterior. Usa el modelo <code>gpt-4o-mini</code> de OpenAI para sintetizar la información más relevante y responder a la pregunta del usuario para obtener respuestas derivadas directamente de los resultados de búsqueda. Además del resumen, también devuelve metadatos de citas para los documentos fuente utilizados.</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>Finalmente, necesitamos iniciar el servidor a través de <a href="https://github.com/modelcontextprotocol/typescript-sdk?tab=readme-ov-file#stdio">stdio</a>. Esto significa que el cliente MCP se comunicará con nuestro servidor leyendo y escribiendo en sus flujos estándar de entrada y salida. stdio es la opción de transporte más sencilla y funciona bien para servidores MCP locales lanzados como subprocesos por el cliente. Agrega el siguiente código al final del archivo:</p>const transport = new StdioServerTransport();
server.connect(transport);<p>Ahora compila el proyecto usando el siguiente comando:</p>npx tsc index.ts --target ES2022 --module node16 --moduleResolution node16 --outDir ./dist --strict --esModuleInterop<p>Esto creará una carpeta <code>dist</code> y, dentro de ella, un archivo <code>index.js</code>.</p><h3>Carga el servidor MCP en Claude Desktop</h3><p>Sigue <a href="https://modelcontextprotocol.io/docs/develop/connect-local-servers">esta guía</a> para configurar el servidor MCP con Claude Desktop. En el archivo de configuración de Claude, tienes que establecer los siguientes valores:</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>El valor <code>args</code> debe apuntar al archivo compilado en la carpeta <code>dist</code>. También es necesario configurar las variables de entorno en el archivo de configuración con los mismos nombres exactos definidos en el código.</p><h3>Pruébalo</h3><p>Antes de ejecutar cada herramienta, haz clic en <strong>Búsqueda y herramientas</strong> para asegurarte de que las herramientas estén habilitadas. Aquí también puedes habilitar o deshabilitar cada una de ellas:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt395a7337021f9820/6a170c1c67045bb74d45c228/172981c2a54adabc70d5819013c3007670935605-1999x1002.png" alt="Página de Claude Sonnet 4.5, con la nota: &quot;Buenas tardes, Jeff.&quot; ¿Cómo puedo ayudarte hoy?&quot;" /><p>Finalmente, probemos el servidor MCP desde el chat de Claude Desktop y comencemos a hacer preguntas:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf4ac458dc0206271/6a170c1e66c4f91328f8c072/03654c0f8c53c714f801fba8b25747071179209b-1999x1353.png" alt="Solicitud de búsqueda del usuario en el chat de Claude Desktop para documentos sobre métodos de autenticación y control de acceso basado en roles, junto con las respuestas de Claude." /><p>Para la consulta<strong>"Buscar documentos sobre métodos de autenticación y control de acceso basado en roles"</strong>, se ejecuta la herramienta <code>search_docs</code> y arroja los siguientes resultados:</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 respuesta es: “¡Genial! Encontré 5 documentos relevantes sobre métodos de autenticación y control de acceso basado en roles. Esto es lo que se encontró:”</p><p>La llamada a la herramienta devuelve los documentos de origen como parte de su carga útil de respuesta, que luego se utilizan para generar citas.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltbaf48a9468770ce2/6a170c21509168bffee1bb14/25ff4c7e9563d99752f95540dafdc7fd211a66e3-800x530.gif" alt="Página de Claude 4.5 Sonnet con respuestas desplazables que incluyen los cinco documentos relevantes sobre los métodos de autenticación y el control de acceso basado en roles." /><p>También es posible encadenar varias herramientas en una sola interacción. En este caso, Claude Desktop analiza la pregunta del usuario y determina que primero debe llamar a <code>search_docs</code> para recuperar documentos relevantes y luego pasar esos resultados a <code>summarize_and_cite</code> para generar la respuesta final, todo eso sin requerir indicaciones separadas del usuario:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blta46ff45274e64192/6a170c230c4857a91501aac1/e6a8a46acb4236e77058f18bcd2f0737b5882c05-1999x1101.png" alt="Chat de escritorio de Claude con la nota: &quot;Jeff regresa&quot;, además de una nueva pregunta de un usuario: &quot;¿Cuáles son las principales recomendaciones para mejorar la autenticación y el control de acceso en todos nuestros sistemas? Incluye referencias.&quot;" /><p>En este caso, para la búsqueda: "<strong>¿Cuáles son las principales recomendaciones para mejorar la autenticación y el control de acceso en todos nuestros sistemas? Incluye referencias.</strong>", obtuvimos los siguientes resultados:</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>Al igual que en el paso anterior, podemos ver la respuesta de cada herramienta a esta pregunta:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt8f633c518e708a99/6a170c25ab7f082991db9ed6/cb606d356b2f7d5e4878a5eff71bc881869ac0ee-800x585.gif" alt="Página de chat de Claude Desktop con texto desplazado que incluye la respuesta de cada herramienta a la pregunta: “¿Cuáles son las principales recomendaciones para mejorar la autenticación y el control de acceso en todos nuestros sistemas? Incluye referencias.”" /><p><em>Nota: si aparece un submenú que pregunta si apruebas el uso de cada herramienta, selecciona </em><em><strong>Permitir siempre</strong></em><em> o </em><em><strong>Permitir una vez</strong></em><em>.</em></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt6627ee0bff1862df/6a170c266f7f040f6f91488c/aea942ba9b0037526ea215bec65690f1a5c3099c-1522x250.png" alt="Claude Desktop te ofrece las opciones &quot;Permitir siempre&quot; y &quot;Permitir una sola vez&quot; para que el usuario elija." /><h2>Conclusión</h2><p>Los servidores MCP representan un paso significativo hacia la estandarización de las herramientas LLM para aplicaciones tanto locales como remotas. Aunque la compatibilidad total todavía está en proceso, nos estamos moviendo rápido en esa dirección.</p><p>En este artículo, aprendimos cómo construir un servidor MCP personalizado en TypeScript que conecta Elasticsearch con aplicaciones impulsadas por modelos LLM. Nuestro servidor expone dos herramientas: <code>search_docs</code> para recuperar documentos relevantes con Query DSL y <code>summarize_and_cite</code> para generar resúmenes con citas a través de modelos de OpenAI y Claude Desktop como client UI.</p><p>El futuro de la compatibilidad entre los distintos proveedores de clientes y servidores parece prometedor. Los próximos pasos consisten en agregar más funcionalidades y flexibilidad a tu agente. Hay un <a href="https://www.elastic.co/search-labs/blog/llm-functions-elasticsearch-intelligent-query">artículo</a> práctico sobre cómo puedes agregar parámetros a tus consultas a través de plantillas de búsqueda para ganar precisión y flexibilidad.</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[AI agéntica]]></category>
    <category><![CDATA[Integraciones]]></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[Uso de la API de inferencia de Elasticsearch junto con modelos de Hugging Face]]></title>
    <description><![CDATA[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.]]></description>
    <content:encoded><![CDATA[<p>En actualizaciones recientes, Elasticsearch introdujo una integración nativa para conectar a modelos hospedados en el <a href="https://endpoints.huggingface.co/">servicio de inferencia Hugging Face</a>. 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 <a href="https://huggingface.co/HuggingFaceTB/SmolLM3-3B">SmolLM3-3B</a>, un modelo ligero de propósito general con un buen equilibrio entre el uso de recursos y la calidad de las respuestas.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt9094997548bd70f8/6a170d6a839dfa0ad6dcff54/7ddadf1976421a860a7d62087239adb9150d808b-1999x1388.png" alt="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." /><h2>Requisitos previos</h2><ul><li><p><strong>Elasticsearch 9.3 o Elastic Cloud Serverless: </strong>puedes crear un despliegue en la cloud siguiendo <a href="https://www.elastic.co/search-labs/tutorials/install-elasticsearch/elastic-cloud">estas instrucciones</a>, o bien puedes utilizar la guía de inicio rápido de <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>.</p></li><li><p><strong>Python 3.12: </strong>Descarga Python <a href="https://www.python.org/">aquí</a>.</p></li><li><p><a href="https://huggingface.co/docs/hub/en/security-tokens">Token de acceso </a><strong>Hugging Face</strong>.</p></li></ul><h2>Finalización de chat usando un endpoint de inferencia de Hugging Face</h2><p>Primero, vamos a crear un ejemplo práctico que conecte Elasticsearch con un <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-inference-put">endpoint de inferencia</a> 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.</p><p>Con este endpoint, <a href="https://www.elastic.co/docs/solutions/search/semantic-search">la búsqueda semántica</a> 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.</p><p>Echemos un vistazo a una visión general de alto nivel del flujo de información que vamos a construir:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf217b7b7db4e1e6c/6a170d6ca929cf8022ae0a3b/1dfbc2323438feaaa42e13ab242dd1f7166f74aa-1200x676.png" alt="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." /><p>En este artículo, probaremos la <strong>capacidad de SmolLM3-3B </strong>paracombinar 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.</p><p>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.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt20e69b9a06fecd65/6a170d6e839dfa6f97dcff58/8d3b86b212f28ff279f2da67a33e6134039f0e4e-1999x949.png" alt="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." /><p>Puedes encontrar la implementación completa de esta aplicación en el <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/elasticsearch-inference-api-and-hugging-face/notebook.ipynb">cuaderno</a> adjunto.</p><h3>Configurar endpoints de inferencia de Elasticsearch</h3><p>Para usar el <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-inference-put-hugging-face">endpoint de inferencia Elasticsearch Hugging Face</a>, necesitamos dos elementos importantes: una clave API de Hugging Face y una URL del endpoint de Hugging Face en funcionamiento. Debería verse así:</p>PUT _inference/chat_completions/hugging-face-smollm3-3b
{
    "service": "hugging_face",
    "service_settings": {
        "api_key": "hugging-face-access-token", 
        "url": "url-endpoint" 
    }
}<p>El endpoint de inferencia Hugging Face en Elasticsearch admite diferentes tipos de tareas: <code>text_embedding</code>, <code>completion</code>, <code>chat_completion</code> y <code>rerank</code>. En esta publicación de blog, usamos <code>chat_completion</code> 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:</p>POST _inference/chat_completion/hugging-face-smollm3-3b/_stream
{
  "messages": [
      { "role": "user", "content": "&lt;user prompt&gt;" }
  ]
}<p>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.</p><h4>Configuración de un endpoint de inferencia en Hugging Face</h4><p>Para desplegar el modelo de Hugging Face, vamos a usar <a href="https://huggingface.co/inference-endpoints/dedicated">despliegues con un clic de Hugging Face</a>, 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.</p><p>Puedes elegir un modelo del catálogo de un clic:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blta7bdfa43d6766324/6a170d6fb339d59e5476a039/b816e9fba1fe172687bf58f5143fb1f838c1077f-549x331.png" alt="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." /><p>Vamos a elegir el modelo <strong>SmolLM3-3B</strong>:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltdb0a2e6ffd7deb20/6a170d710c48574b7401aafc/610d3aba0429f3666c2df3616d513eb6a4397c0c-502x478.png" alt="Interfaz para crear un endpoint para el modelo SmolLM3‑3B, muestra el nombre del modelo, una nota de &quot;verificado por Hugging Face&quot;, 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 &quot;Crear Endpoint&quot;." /><p>Desde aquí, copia la URL del endpoint de Hugging Face:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt25714021711ed6ff/6a170d72c1e8a54853f88336/025094ddb2cfbd1f0f216a5ec4e119b0f4fa2c42-646x328.png" alt="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." /><p>Como se menciona en la <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-inference-put-hugging-face">documentación de endpoints de inferencia Hugging Face</a> 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 <code>/v1/chat/completions</code> a la URL del endpoint de Hugging Face. El resultado final se verá así:</p>https://j2g31h0futopfkli.us-east-1.aws.endpoints.huggingface.cloud/v1/chat/completions<p>Ahora que está todo listo, podemos empezar a programar en un cuaderno de Python.</p><h4>Generando clave API de Hugging Face</h4><p>Crea una <a href="https://huggingface.co/join">cuenta en Hugging Face</a> y obtén un token de API siguiendo <a href="https://huggingface.co/docs/hub/en/security-tokens#user-access-tokens">estas instrucciones</a>. Puedes elegir entre tres tipos de token: <em>detallado</em> (recomendado para producción, ya que proporciona acceso solo a recursos específicos); <em>lectura</em> (para acceso de solo lectura); o <em>escritura</em> (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.</p><h4>Configuración del endpoint de inferencia de Elasticsearch</h4><p>Primero, declaremos un cliente de Python para Elasticsearch:</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>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.</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>Set de datos</h3><p>El conjunto de datos contiene las <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/elasticsearch-inference-api-and-hugging-face/dataset.json">publicaciones de blog</a> que se consultarán, representando un conjunto de contenido multilingüe utilizado a lo largo del flujo de trabajo:</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 de Elasticsearch</h4><p>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 <a href="https://www.elastic.co/docs/manage-data/data-store/mapping">mappings de índices</a> para almacenar los datos en 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>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 <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/copy-to"><code>copy_to</code></a> para copiar el contenido del campo en el campo <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/semantic-text"><code>semantic_text</code></a>. Además, el campo <code>title</code> contiene dos subcampos: el subcampo <code>original</code> almacena el título en inglés o español, dependiendo del idioma original del artículo; y el subcampo <code>translated_title</code> está presente solo para artículos en español y contiene la traducción al inglés del título original.</p><h3>Ingesta de datos</h3><p>El siguiente fragmento de código ingesta el conjunto de datos de las publicaciones de blog en Elasticsearch mediante la <a href="https://www.elastic.co/docs/reference/elasticsearch/clients/javascript/bulk_examples">API de 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>Ahora que tenemos los artículos ingeridos en Elasticsearch, necesitamos crear una función capaz de buscar en el campo <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>También necesitamos una función que llame al endpoint de inferencia. En este caso, llamaremos al endpoint usando el tipo de tarea <strong><code>chat_completion</code></strong>para obtener respuestas de transmisión:</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>Ahora podemos escribir una función que llame a la función de búsqueda semántica, junto con el endpoint de inferencia <code>chat_completions</code> y el endpoint de recomendaciones, para generar los datos que se asignarán en las tarjetas:</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>Por último, extrae la información y dale formato para imprimirla:</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>Pongámoslo a prueba haciendo una pregunta sobre las publicaciones de blog de seguridad:</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>Aquí podemos ver las tarjetas de la consola generadas por el flujo de trabajo:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt4aa221a08a51aeb3/6a170d7460084be1413c45d6/730d35212594bb3db30447c3ea7e2a92857287b7-1999x1515.png" alt="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." /><p>Puedes ver los resultados completos, incluso todas las coincidencias y la respuesta del LLM, en <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/elasticsearch-inference-api-and-hugging-face/results.md">este archivo</a>.</p><p>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.</p><h2>Conclusión</h2><p>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.</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[AI agéntica]]></category>
    <category><![CDATA[Integraciones]]></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[Construye un flujo de trabajo de búsqueda de IA financiera utilizando LangGraph.js y Elasticsearch]]></title>
    <description><![CDATA[Aprende a usar LangGraph.js con Elasticsearch para crear un flujo de trabajo de búsqueda financiera impulsado por IA que convierte consultas en lenguaje natural en filtros dinámicos y condicionales para análisis de inversión y mercado.]]></description>
    <content:encoded><![CDATA[<p>Construir aplicaciones de búsqueda con IA suele implicar la coordinación de múltiples tareas, la recuperación de datos y la extracción de datos en un flujo de trabajo sin problemas. LangGraph simplifica este proceso permitiendo a los desarrolladores orquestar agentes de IA mediante una estructura basada en nodes. En este artículo, explicaremos cómo construir una solución financiera usando <a href="https://langchain-ai.github.io/langgraphjs/">LangGraph.js</a></p><h2>¿Qué es LangGraph?</h2><p><a href="https://langchain-ai.github.io/langgraphjs/">LangGraph</a> es un marco de trabajo para construir agentes de IA y orquestarlos en un flujo de trabajo para crear aplicaciones asistidas por IA. LangGraph tiene una arquitectura de nodes donde podemos declarar funciones que representan tareas y asignarlas como nodes del flujo de trabajo. El resultado de la interacción de varios nodes será un grafo. LangGraph es parte del ecosistema más amplio <a href="https://js.langchain.com/docs/introduction/">LangChain</a>, que proporciona herramientas para construir sistemas de IA modulares y componibles.</p><p>Para explicar mejor por qué LangGraph es útil, vamos a usarlo para resolver una situación problemática.</p><h2>Visión general de la solución</h2><p>En una firma de capital de riesgo, los inversores tienen acceso a una gran base de datos con muchas opciones de filtrado, pero cuando uno quiere combinar criterios, se vuelve difícil y lento. Esto puede hacer que algunas iniciativas relevantes no se encuentren para la inversión. Además, implica pasar muchas horas intentando identificar a los mejores candidatos, o incluso perder oportunidades.</p><p>Con LangGraph y Elasticsearch, podemos realizar búsquedas filtradas utilizando lenguaje natural, eliminando la necesidad de que los usuarios construyan manualmente solicitudes complejas con docenas de filtros. Para hacerlo más flexible, el flujo de trabajo decide automáticamente (basándose en la entrada del usuario) entre dos tipos de consulta:</p><ul><li><p><strong>Consultas centradas en la inversión</strong>: estas se dirigen a aspectos financieros y de financiación de las startups, como <a href="https://www.investopedia.com/articles/personal-finance/102015/series-b-c-funding-what-it-all-means-and-how-it-works.asp">rondas de financiación</a>, valoración o <a href="https://www.investopedia.com/terms/r/revenue.asp">ingresos</a>. <em>Ejemplo:</em> “Encuentra startups con financiamiento Serie A o Serie B entre $8M y $25M e ingresos mensuales superiores a $500K”.</p></li><li><p><strong>Consultas centradas en el mercado</strong>: estas se concentran en <a href="https://en.wikipedia.org/wiki/Vertical_market">verticales de la industria</a>, <a href="https://en.wikipedia.org/wiki/Target_market">mercados geográficos</a> o <a href="https://www.investopedia.com/terms/b/businessmodel.asp">modelos de negocio</a>, ayudando a identificar oportunidades en sectores o regiones específicos. <em>Ejemplo:</em> “Encuentra startups de fintech y salud en San Francisco, Nueva York o Boston”.</p></li></ul><p>Para mantener la solidez de las consultas, haremos que el LLM cree <a href="https://www.elastic.co/docs/solutions/search/search-templates">plantillas de búsqueda</a> en lugar de <a href="https://www.elastic.co/docs/explore-analyze/query-filter/languages/querydsl">consultas DSL</a> completas. De esta manera, siempre obtienes la consulta que deseas, y el LLM solo tiene que completar los espacios en blanco y no cargar con la responsabilidad de construir la consulta que necesitas cada vez.</p><h2>Lo que necesitas para comenzar</h2><ul><li><p>Clave de API de Elasticsearch</p></li><li><p>Clave de API de OpenAPI</p></li><li><p>Node 18 o más reciente</p></li></ul><h2>Instrucciones paso a paso</h2><p>En esta sección, mostramos cómo se verá la app. Para ello, emplearemos <a href="https://www.typescriptlang.org/">TypeScript</a>, un superconjunto de JavaScript que agrega tipos estáticos para hacer el código más fiable, fácil de mantener y seguro, detectando errores pronto mientras se mantiene totalmente compatible con el JavaScript existente.</p><p>El flujo de los nodos será el siguiente:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt90db8f03f372608c/6a170986dc55de6e16e00d93/b47d7f238c4964a6febc0de7fe5e68b186f539c3-363x555.png" alt="" /><p>La imagen de arriba es generada por LangGraph y representa el flujo de trabajo que define el orden de ejecución y la lógica condicional entre nodos:</p><ul><li><p><strong>decideStrategy: </strong>utiliza un LLM para analizar la consulta del usuario y decidir entre dos estrategias de búsqueda especializadas, como centrada en la inversión u orientada al mercado.</p></li><li><p><strong>prepareInvestmentSearch: </strong>extrae valores de filtro de la consulta y construye una plantilla predefinida que destaca los parámetros financieros y de financiación.</p></li><li><p><strong>prepareMarketSearch</strong>: extrae también los valores del filtro, pero construye dinámicamente parámetros que enfatizan el mercado, la industria y el contexto geográfico.</p></li><li><p><strong>executeSearch: </strong>envía la consulta construida a Elasticsearch usando una plantilla de búsqueda y recupera los documentos de startups correspondientes.</p></li><li><p><strong>visualizarResultados: </strong>formatea los resultados finales en un resumen claro y legible que muestre atributos clave de la startup, como financiación, industria e ingresos.</p></li></ul><p>Este flujo incluye una <a href="https://langchain-ai.github.io/langgraphjs/how-tos/branching/?h=conditional#how-to-create-branches-for-parallel-node-execution">ramificación condicional</a>, que funciona como una declaración “si” que determina si se debe usar la ruta de búsqueda de inversión o de mercado según la entrada del usuario. Esta lógica de decisión, impulsada por el LLM, hace que el flujo de trabajo sea adaptativo y consciente del contexto, un mecanismo que exploraremos con más detalle en las siguientes secciones.</p><h3>Estado de LangGraph</h3><p>Antes de ver cada node individualmente, necesitamos entender cómo se comunican y comparten datos. Para ello, LangGraph nos permite definir el estado del flujo de trabajo. Esto define el estado compartido que se pasará entre los nodes.</p><p>El estado actúa como un contenedor compartido que almacena datos intermedios a lo largo del flujo de trabajo: comienza con la consulta en lenguaje natural del usuario, luego guarda la estrategia de búsqueda seleccionada, los parámetros preparados para Elasticsearch, los resultados de búsqueda recuperados y, finalmente, la salida formateada.</p><p>Esta estructura permite que cada node lea y actualice el estado, asegurando un flujo coherente de información desde la entrada del usuario hasta la visualización final.</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>Configure la aplicación</h3><p>Todo el código de esta sección se puede encontrar en el <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/langgraph-js-elasticsearch">repositorio elasticsearch-labs</a>.</p><p>Abra un terminal en la carpeta donde estará la app e inicialice una aplicación Node.js con el comando:</p>npm init -y<p>Ahora podemos instalar las dependencias necesarias para este proyecto:</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>: nos ayuda a gestionar las solicitudes de Elasticsearch, como la ingesta y la recuperación de datos.</p></li><li><p><strong><code>@langchain/langgraph</code></strong>: Dependencia de JS para proporcionar todas las herramientas de LangGraph.</p></li><li><p><strong><code>@langchain/openai</code></strong>: cliente de OpenAI LLM para LangChain.</p></li><li><p>@langchain/core: proporciona los bloques fundamentales del núcleo para las apps de LangChain, incluidas las plantillas de prompts.</p></li><li><p><strong><code>dotenv</code></strong>: dependencia necesaria para usar variables de entorno en JavaScript.</p></li><li><p><strong><code>zod</code></strong>: dependencia para escribir datos.</p></li></ul><p><code>@types/node</code> <code>tsx</code> <code>typescript</code> nos permite escribir y ejecutar código TypeScript.</p><p>Ahora crea los siguientes archivos:</p><ul><li><p><code>elasticsearchSetup</code><a href="http://ingest.ts/"><code>.ts</code></a>: creará los mapping de índice, cargará el conjunto de datos desde un archivo JSON e ingerirá los datos en Elasticsearch.</p></li><li><p><a href="http://main.ts/"><code>main.ts</code></a>: incluirá la aplicación LangGraph.</p></li><li><p><code>.env</code>: archivo para almacenar las variables de entorno</p></li></ul><p>En el archivo <code>.env</code>, agreguemos las siguientes variables de entorno:</p>ELASTICSEARCH_ENDPOINT="your-endpoint-here"
ELASTICSEARCH_API_KEY="your-key-here"
OPENAI_API_KEY="your-key-here"<p>La clave API de OpenAPI no se usará directamente en el código; en su lugar, se usará internamente por la biblioteca <code>@langchain/openai</code>.</p><p>Toda la lógica relacionada con la creación de mapping, la creación de plantillas de búsqueda y la ingesta de sets de datos se encuentra en el archivo <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/langgraph-js-elasticsearch/elasticsearchSetup.ts"><code>elasticsearchSetup.ts</code></a>. En los próximos pasos, nos centraremos en el archivo <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/langgraph-js-elasticsearch/main.ts"><code>main.ts</code></a> . Además, puedes consultar los sets de datos para entender mejor cómo se ven los datos en el <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>Aplicación LangGraph</h3><p>En el archivo <code>main.ts</code>, vamos a importar algunas dependencias necesarias para consolidar la aplicación LangGraph. En este archivo, también debes incluir las funciones del node y la declaración de estado. La declaración del grafo se realizará en un método <code>main</code> en los siguientes pasos. El archivo <code>elasticsearchSetup.ts</code> contendrá ayudantes de Elasticsearch que vamos a usar dentro de los nodes en los próximos pasos.</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>Como se mencionó anteriormente, el cliente LLM se utilizará para generar los parámetros de la plantilla de búsqueda de Elasticsearch basados en la pregunta del usuario.</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>El método anterior genera la imagen del grafo en formato png y utiliza la <a href="https://mermaid.ink/">API de Mermaid.INK</a> en segundo plano. Esto es útil si deseas ver cómo interactúan los nodes de la app con una visualización estilizada.</p><h3>Nodes LangGraph</h3><p>Ahora veamos cada node en detalle:</p><h3>node decideSearchStrategy</h3><p>El <code>decideSearchStrategy</code> node analiza la entrada del usuario y determina si realizar una búsqueda centrada en la inversión o en el mercado. Utiliza un LLM con un esquema de salida estructurado (definido con Zod) para clasificar el tipo de consulta. Antes de tomar la decisión, recupera los filtros disponibles del índice mediante una agregación, lo que garantiza que el modelo cuente con información actualizada sobre sectores, ubicaciones y datos de financiación.</p><p>Para extraer los posibles valores de los filtros y enviarlos al LLM, usemos una consulta <a href="https://www.elastic.co/docs/explore-analyze/query-filter/aggregations">de agregación</a> para obtenerlos directamente del índice de Elasticsearch. Esta lógica se encuentra en un método llamado <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>Con la consulta de agregación anterior, tenemos los siguientes resultados:</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>Vea todos los resultados <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/langgraph-js-elasticsearch/responses/aggregationsResponse.json">aquí</a>.</p><p>Para ambas estrategias, utilizaremos la búsqueda híbrida para detectar tanto la parte estructurada de la pregunta (filtros) como las partes más subjetivas (semántica). A continuación se muestra un ejemplo de ambas consultas utilizando <a href="https://www.elastic.co/docs/solutions/search/search-templates">plantillas de búsqueda</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>Vea las consultas detalladas en el archivo <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/langgraph-js-elasticsearch/elasticsearchSetup.ts#L119"><code>elasticsearchSetup.ts</code></a> . En el siguiente node, se decidirá cuál de las dos consultas se empleará:</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 y prepareMarketSearch</h3><p>Ambos nodos emplean una función auxiliar compartida, <code>extractFilterValues</code>, que aprovecha el LLM para identificar los filtros relevantes mencionados en la entrada del usuario, como la industria, la ubicación, la etapa de financiación, el modelo de negocio, etc. Estamos utilizando este esquema para crear nuestra <a href="https://www.elastic.co/docs/solutions/search/search-templates">plantilla de búsqueda</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>Según de la intención detectada, el flujo de trabajo selecciona una de dos rutas:</p><p><strong>prepareInvestmentSearch:</strong> desarrolla parámetros de búsqueda orientados a la financiación, incluyendo la etapa de financiación, el importe de la inversión, el inversionista y la información de renovación. Puedes encontrar la plantilla completa de consulta en el archivo <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> crea parámetros orientados al mercado centrados en industrias, geografías y modelos de negocio. Ver la consulta completa en el archivo <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>Este node toma los parámetros de búsqueda generados del estado y los envía primero a Elasticsearch, usando la <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-render-search-template">_render API</a> para visualizar la consulta con fines de depuración, y luego envía una petición para recuperar los resultados.</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 visualizarResultados</h3><p>Finalmente, este nodo muestra los resultados de 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>Programáticamente, todo el grafo se ve así:</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>Como puede ver, tenemos una aplicación condicional donde la aplicación decide qué “ruta” o node ejecutar a continuación. Esta característica es útil cuando los flujos de trabajo necesitan lógica de ramificación, como elegir entre múltiples herramientas o incluir un paso con intervención de una persona.</p><p>Con las características básicas del núcleo de LangGraph entendidas, podemos configurar la aplicación donde se ejecutará el código:</p><p>Juntando todo en un flujo de trabajo <code>main</code>, aquí declaramos el grafo con todos los elementos bajo la variable flujo de trabajo:</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 consulta simula la entrada del usuario introducida en una barra de búsqueda hipotética:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltba7189d5f4e63403/6a1709880e2e49cc3041a076/e8d76909eb2bc1bb62f3ca9a8b3e4b85fcec2893-1600x164.png" alt="" /><p>De la frase en lenguaje natural “Encuentra startups con financiamiento de Serie A o Serie B entre $8M y $25M, e ingresos mensuales superiores a $500K” se extraerán todos los filtros.</p><p>Finalmente, invoca el método principal:</p>main().catch(console.error);<h3>Resultados</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>Para la entrada enviada, la aplicación elige la ruta <strong>centrada en la inversión</strong> y, como resultado, podemos ver la consulta de Elasticsearch generada por el flujo de trabajo de LangGraph, que extrae los valores y los rangos de la entrada del usuario. También podemos ver la consulta enviada a Elasticsearch con los valores extraídos aplicados y, finalmente, los resultados formateados por el node <code>visualizeResults</code> con los resultados.</p><p>Ahora vamos a probar el node <strong>centrado en el mercado</strong> usando la consulta “Encuentre startups de fintech y salud en San Francisco, Nueva York o 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>Aprendizajes</h2><p>Durante el proceso de escritura aprendí:</p><ul><li><p>Debemos mostrar al LLM los valores exactos de los filtros; de lo contrario, dependemos de que el usuario escriba los valores exactos de las cosas. Para baja cardinalidad, este enfoque está bien, pero cuando la cardinalidad es alta, necesitamos algún mecanismo para filtrar los resultados.</p></li><li><p>El uso de plantillas de búsqueda hace que los resultados sean mucho más consistentes que dejar que el LLM escriba la consulta de Elasticsearch, y también es más rápido.</p></li><li><p>Los bordes condicionales son un mecanismo potente para crear aplicaciones con múltiples variantes y rutas de ramificación.</p></li><li><p>La salida estructurada es extremadamente útil cuando se genera información con LLM porque impone respuestas predecibles y de tipo seguro. Esto mejora la confiabilidad y reduce las interpretaciones incorrectas de los prompts.</p></li></ul><p>La combinación de búsqueda semántica y estructurada a través de la recuperación híbrida produce resultados mejores y más relevantes, equilibrando la precisión y la comprensión del contexto.</p><h2>Conclusión</h2><p>En este ejemplo, combinamos LangGraph.js con Elasticsearch para crear un flujo de trabajo dinámico capaz de interpretar consultas en lenguaje natural y decidir entre estrategias de búsqueda financieras u orientadas al mercado. Este enfoque reduce la complejidad de la creación de consultas manuales, al tiempo que mejora la flexibilidad y la precisión para los analistas de capital de riesgo.</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[AI]]></category>
    <category><![CDATA[AI agéntica]]></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[Paneles impulsados por IA: de una visión a Kibana]]></title>
    <description><![CDATA[Genera un panel de control usando un LLM para procesar una imagen y convertirla en un Panel de Kibana.
]]></description>
    <content:encoded><![CDATA[<p><a href="https://www.elastic.co/kibana/kibana-lens">Kibana Lens</a> hace que los paneles de control sean muy sencillos, pero cuando necesitas decenas de paneles, los clics se acumulan. ¿Y si pudieras hacer un boceto de un panel de control, hacer capturas de pantalla y dejar que un LLM termine todo el proceso por ti?</p><p>En este artículo, haremos que eso suceda. Crearemos una aplicación que tome una imagen de un panel de control, analice nuestros mapeos y luego genere un panel sin que tengamos que tocar Kibana en absoluto.</p><p><strong>Pasos</strong>:</p><ol><li><p><a href="https://www.elastic.co/search-labs/blog/ai-powered-dashboards#background-&amp;-application-workflow">Antecedentes y flujo de trabajo de la aplicación</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/ai-powered-dashboards#prepare-data">Preparar datos</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/ai-powered-dashboards#llm-configuration">Configuración de LLM</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/ai-powered-dashboards#application-functions">Funciones de aplicación</a></p></li></ol><h2>Antecedentes y flujo de trabajo de la aplicación</h2><p>La primera idea que se me ocurrió fue dejar que el LLM generara todo el formato NDJSON <a href="https://www.elastic.co/docs/explore-analyze/find-and-organize/saved-objects">de los objetos almacenados</a> de Kibana y luego los importara a Kibana.</p><p>Probamos algunos modelos:</p><ul><li><p>Gemini 2.5 pro</p></li><li><p>GPT o3 / o4-mini-alto / 4,1</p></li><li><p>Claude 4 soneto</p></li><li><p>Grok 3</p></li><li><p>Deepseek (Deepthink R1)</p></li></ul><p>Y para los prompts, empezamos tan sencillos como:</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>A pesar de repasar <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.">algunos ejemplos de planos</a> y explicaciones detalladas sobre cómo construir cada visualización, no tuvimos suerte. Si te interesa esta experimentación, puedes encontrar <a href="https://gist.github.com/TomasMurua/a78dc283e115624731beffc98984b70b">detalles aquí</a>.</p><p>El resultado de este enfoque fue ver estos mensajes al intentar subir a Kibana los archivos producidos por el 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>Esto significa que el JSON generado es inválido o está mal formateado. Los problemas más comunes eran que el LLM producía NDJSON incompleto, alucinaban parámetros o devolvían JSON normal en lugar de NDJSON, por mucho que intentáramos hacer cumplir lo contrario.</p><p>Inspirados por <a href="https://www.elastic.co/search-labs/blog/llm-functions-elasticsearch-intelligent-query">este artículo</a> —donde <a href="https://www.elastic.co/docs/solutions/search/search-templates">las plantillas de búsqueda</a> funcionaban mejor que el estilo libre de un LLM— decidimos dar plantillas al LLM en lugar de pedir generar el archivo NDJSON completo y luego, en código, usar los parámetros dados por el LLM para crear las visualizaciones adecuadas. Este enfoque no decepcionó, y es previsible y ampliable, ya que ahora el código hace el trabajo duro y no el LLM.</p><p>El flujo de trabajo de la aplicación será el siguiente:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt9f7738a4c7ddd0cd/6a1707d52b835f0a25f4b166/52c587cf0cf3517fdd4ee7ab95581dd4f2bce030-725x668.png" alt="" /><p></p><p><em>Omitiremos algo de código para simplificar, pero puedes encontrar el código funcional de la aplicación completa en </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>este</strong></em></a><em> cuaderno.</em></p><h2>Prerrequisitos</h2><p>Antes de empezar a desarrollar, necesitarás lo siguiente:</p><ol><li><p>Python 3.8 o superior</p></li><li><p>Un entorno <a href="https://docs.python.org/3/library/venv.html">Venv</a> Python</p></li><li><p>Una instancia de Elasticsearch en ejecución, junto con su endpoint y clave API</p></li><li><p>Una clave de API de OpenAI almacenada bajo el nombre de la variable de entorno OPENAI_API_KEY:</p></li></ol>export OPENAI_API_KEY="your-openai-api-key"<h2>Preparar datos</h2><p>Para los datos, lo mantendremos sencillo y usaremos registros sitio web de muestra de Elastic. Puedes aprender cómo importar esos datos a tu <a href="https://www.elastic.co/docs/manage-data/ingest/sample-data#add-sample-data-sets">clúster aquí</a>.</p><p>Cada documento incluye detalles sobre el anfitrión que emitió las solicitudes a la aplicación, junto con información sobre la propia solicitud y su estado de respuesta. A continuación se muestra un documento de ejemplo:</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>Ahora, vamos a tomar los mapeos del índice que acabamos de cargar, <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>Vamos a pasar los mapeos junto con la imagen que cargaremos más adelante.</p><h2>Configuración de LLM</h2><p>Configuremos el LLM para que use <a href="https://python.langchain.com/docs/concepts/structured_outputs/">una salida estructurada</a> para introducir una imagen y recibir un JSON con la información que necesitamos pasar a nuestra función para producir los objetos JSON.</p><p>Instalamos las dependencias:</p>pip install elasticsearch pydantic langchain langchain-openai -q<p>Elasticsearch nos ayudará a recuperar los <a href="https://www.elastic.co/docs/manage-data/data-store/mapping">mapeos de índice</a>. Pydantic nos permite definir esquemas en Python para luego pedir al LLM que lo siga, y <a href="https://www.elastic.co/search-labs/integrations/langchain">LangChain</a> es el framework que facilita la llamada a LLMs y herramientas de IA.</p><p>Crearemos un esquema Pydantic para definir la salida que queremos del LLM. Lo que necesitamos saber de la imagen es el tipo de gráfico, campo, título de visualización y título del panel de control:</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>Para la entrada de imagen enviaremos un panel de control que acabo de dibujar:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt7870f6421986d11d/6a1707d78b73cb3408189fa3/36441d7b5dc1f3ff2ac2a30710208d57ad41c716-1600x898.jpg" alt="" /><p>Ahora declaramos la llamada al modelo LLM y la carga de imagen. Esta función recibirá los mapeos del índice de Elasticsearch y una imagen del panel de control que queremos generar.</p><p>Con <code>with_structured_output</code> podemos usar nuestro esquema de <code>Dashboard</code> Pydantic como objeto de respuesta que producirá el LLM. Con <a href="https://docs.pydantic.dev/latest/">Pydantic</a>, podemos definir modelos de datos con validación, lo que garantiza que la salida del LLM coincida con la estructura esperada.</p><p>Para convertir la imagen a base64 y enviarla como entrada, puedes usar un <a href="https://www.base64-image.de/">convertidor online</a> o hacerlo <a href="https://www.geeksforgeeks.org/python-convert-image-to-string-and-vice-versa/">en código</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>El LLM ya tiene contexto sobre los paneles Kibana, así que no necesitamos explicar todo en el prompt, solo algunos detalles para cerciorarnos de que no olvide que está funcionando con Elasticsearch y Kibana.</p><p>Vamos a desglosar el prompt:</p><p>Sección</p><p>Razón</p><p>Eres un experto en analizar paneles de Kibana a partir de imágenes para la versión 9.0.0 de Kibana.</p><p>Al reforzar esto es Elasticsearch, y la versión de Elasticsearch reducimos la probabilidad de que el LLM alucine parámetros antiguos o inválidos.</p><p>Se te dará una imagen del panel de control y un mapeo de índice de Elasticsearch.</p><p>Explicamos que la imagen trata sobre paneles para evitar interpretaciones erróneas por parte del LLM.</p><p>A continuación se muestran los mapeos de índices del índice en el que se basa el panel de control. Emplea esto para ayudarte a entender los datos y los campos disponibles. Mapeos de índice: {index_mappings}</p><p>Es crucial proporcionar los mapeos para que el LLM pueda seleccionar campos válidos dinámicamente. De lo contrario, podríamos codificar los mapeos aquí, lo cual es demasiado rígido, o confiar en la imagen que contiene los nombres de campo correctos, lo cual no es fiable.</p><p>Incluye solo los campos relevantes para cada visualización, basándote en lo que sea visible en la imagen.</p><p>Tuvimos que agregar este refuerzo porque a veces intenta agregar campos que no son relevantes para la imagen.</p><p>Esto devolverá un objeto con un serial de visualizaciones para mostrar:</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>Procesamiento de la respuesta de los LLM</h2><p>Creamosun panel de panel de muestra 2x2 y luego lo exportamos en JSON usando <a href="https://www.elastic.co/docs/api/doc/kibana/operation/operation-get-dashboards-dashboard">la API Get a dashboard</a>, y después almacenamos los paneles como plantillas de visualización (pastel, barra, métrica) donde podemos reemplazar algunos parámetros para crear nuevas visualizaciones con diferentes campos según la pregunta.</p><p>Puedes ver los archivos JSON <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>de plantilla aquí</strong></a>. Fíjate en cómo cambiamos los valores de los objetos que queremos reemplazar más adelante por {<code>variable_name</code>}
</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc55d69d84a08e668/6a1707d8a2929903acd00fb8/ec7e1ac0cd8b470df13e60940162b56778acb386-315x234.png" alt="" /><p>Con la información que nos proporcionó el LLM, podemos decidir qué plantilla usar y qué valores reemplazar.</p><p><code>fill_template_with_analysis</code> recibirá los parámetros de un único panel, incluyendo la plantilla JSON de la visualización, un título, un campo y las coordenadas de la visualización en la cuadrícula.</p><p>Luego, reemplazará los valores de la plantilla y devolverá la visualización JSON final.</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>Para simplificar, tendremos coordenadas estáticas que asignaremos a los paneles que el LLM decida crear y produciremos un panel de cuadrícula 2x2 como en la imagen anterior.</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>Dependiendo del tipo de visualización decidido por el LLM, elegiremos una plantilla de archivo JSON y reemplazaremos la información relevante usando <code>fill_template_with_analysis</code> , luego agregaremos el nuevo panel a un array que usaremos más adelante para crear el panel de control.</p><p>Cuando el panel esté listo, usaremos la <a href="https://www.elastic.co/docs/api/doc/kibana/operation/operation-post-dashboards-dashboard-id">API Crear un panel</a> para enviar el nuevo archivo JSON a Kibana y generar el panel:
</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>Para ejecutar el script y generar el panel de control, ejecuta el siguiente comando en la consola:</p>python &lt;file_name&gt;.py<p>El resultado final será el siguiente:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt5ceffed004153a4f/6a1707d9a929cf9147ae0901/e909afbf0e47d9a6e0f7bd07dfb2efcfa5cf06ac-921x715.png" alt="" /><h2>Conclusión</h2><p>Los LLMs muestran sus fuertes capacidades visuales al hacer texto a código o convertir imágenes en código. La API de los paneles también permite convertir archivos JSON en paneles, y con un LLM y algo de código, podemos convertir imágenes en un panel Kibana.</p><p>El siguiente paso es mejorar la flexibilidad de los gráficos del salpicadero empleando diferentes configuraciones de cuadra, tamaños y posiciones de tablero. Además, ofrecer soporte para visualizaciones y tipos de visualización más complejos sería una adición útil a esta aplicación.</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[AI]]></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 de la manera correcta, parte II]]></title>
    <description><![CDATA[Aprende sobre las mejores prácticas de producción y cómo ejecutar el cliente de Node.js de Elasticsearch en entornos sin servidor para reducir errores de código. ]]></description>
    <content:encoded><![CDATA[<p>Esta es la segunda parte de nuestro serial Elasticsearch en JavaScript. En la<a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i"> primera parte,</a> aprendimos a configurar correctamente nuestro entorno, configurar el cliente Node.js, indexar datos y buscar. En esta segunda parte, aprenderemos a implementar las mejores prácticas de producción y a ejecutar el cliente <a href="http://node.js">de Node.js</a> Elasticsearch en entornos sin servidor.</p><p>Revisaremos:</p><ul><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-ii#production-best-practices">Mejores prácticas de producción</a></p><ul><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-ii#error-handling">Manejo de errores</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-ii#testing">Pruebas</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">Entornos sin servidor</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">Ejecutando el cliente en 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">Ejecutando el cliente en un entorno de función como servicio</a></p></li></ul></li></ul><p><em>Puedes consultar el código fuente con los ejemplos </em><a href="https://github.com/Delacrobix/JS-client-best-practices_article"><em><strong>aquí</strong></em></a><em><strong>.</strong></em></p><h2>Mejores prácticas de producción</h2><h3>Manejo de errores en Elasticsearch</h3><p>Una característica útil del cliente Elasticsearch en Node.js es que expone objetos para los posibles errores en Elasticsearch, de modo que puedes validarlos y manejarlos de diferentes maneras.</p><p>Para <a href="https://www.elastic.co/docs/reference/elasticsearch/clients/javascript/connecting#client-error-handling">verlos todos</a>, pon esto: </p>const { errors } = require('@elastic/elasticsearch')
console.log(errors)<p>Volvamos al ejemplo de búsqueda y tratemos algunos de los posibles errores:</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 individuo, ocurrirá cuando la respuesta sea <code>4xx</code> o <code>5xx</code>, lo que significa que la solicitud es incorrecta o el servidor no está disponible.</p><p>Podemos probar este tipo de error generando consultas erróneas, como intentar <strong>hacer una consulta de términos en un campo de tipo texto:</strong></p><p>Error por defecto:</p> {
    "success": false,
    "results": null,
    "error": "parsing_exception\n\tRoot causes:\n\t\tparsing_exception: [terms] query does not support [visit_details]"
}<p>Error personalizado: </p>{
    "erroStatus": 400,
    "success": false,
    "results": null,
    "error": "Response error!, query malformed or server down; contact the administrator!"
}<p>También podemos capturar y manejar cada tipo de error de una manera determinada. Por ejemplo, podemos agregar lógica de reintentos en un <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>Pruebas</h3><p>Las pruebas son clave para garantizar la estabilidad de la aplicación. Para probar el código de forma aislada de Elasticsearch, podemos usar la biblioteca <a href="https://github.com/elastic/elasticsearch-js-mock">elasticsearch-js-mock</a> al crear nuestro clúster.</p><p>Esta biblioteca nos permite instanciar un cliente muy similar al real, pero que responderá a nuestra configuración reemplazando solo la capa HTTP del cliente por una mock, manteniendo el resto igual que el original.</p><p>Instalaremos la biblioteca de simulacros y <a href="https://github.com/avajs/ava">AVA</a> para los exámenes automatizados.</p><p><code>npm install @elastic/elasticsearch-mock</code></p><p><code>npm install --save-dev ava</code></p><p>Configuraremos el archivo <code>package.json</code> para ejecutar las pruebas. Cerciórate de que se vea así:</p>"type": "module",
	"scripts": {
		"test": "ava"
	},
	"devDependencies": {
		"ava": "^5.0.0"
	}<p>Ahora creemos un archivo <code>test.js</code> e instalemos nuestro cliente simulado:</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>Ahora, agrega un simulacro para la búsqueda semántica:</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>Ahora podemos crear una prueba para nuestro código, cerciorándonos de que la parte de Elasticsearch siempre devolverá los mismos resultados:</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>Hagamos las pruebas.</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>¡Hecho! A partir de ahora, podemos probar nuestra app centrándonos al 100% en el código y no en factores externos.</p><h2>Entornos sin servidor</h2><h3>Cómo ejecutar el cliente en Elastic Serverless</h3><p>Cubrimos la ejecución de Elasticsearch en la nube o local; sin embargo, el cliente Node.js también soporta conexiones a <a href="https://www.elastic.co/guide/en/serverless/current/intro.html">Elastic Cloud Serverless</a>.</p><p>Elastic Cloud Serverless te permite crear un proyecto donde no tienes que preocuparte por la infraestructura, ya que Elastic se encarga de eso internamente, y solo tienes que preocuparte por los datos que quieres indexar y cuánto tiempo quieres tener acceso a ellos.</p><p>Desde la perspectiva del uso, los servidores sin servidor desacoplan el cálculo del almacenamiento, proporcionando funciones de escalado automático tanto para <a href="https://www.elastic.co/search-labs/blog/elasticsearch-serverless-tier-autoscaling">búsqueda</a> como <a href="https://www.elastic.co/search-labs/blog/elasticsearch-ingest-autoscaling">para indexación</a>. Esto te permite solo hacer crecer los recursos que realmente necesitas.</p><p>El cliente realiza las siguientes adaptaciones para conectarse a Serverless:</p><ul><li><p>Apaga el olfateo e ignora cualquier opción relacionada con el olfateo</p></li><li><p>Ignora todos los nodos que pasan en la configuración excepto el primero, e ignora cualquier filtrado de nodos y seleccionando opciones</p></li><li><p>Activa la compresión y el 'TLSv1_2_method' (igual que cuando se configura para Elastic Cloud)</p></li><li><p>Agrega un encabezado HTTP 'elastic-api-version' a todas las solicitudes</p></li><li><p>Emplea 'CloudConnectionPool' por defecto en lugar de 'WeightedConnectionPool'</p></li><li><p>Desactiva los encabezados 'content-type' y 'accept' que venden en favor de los tipos MIME estándar</p></li></ul><p>Para conectar tu proyecto serverless, necesitas usar el parámetro serverMode: serverless.</p>const { Client } = require('@elastic/elasticsearch')
const client = new Client({
  node: 'ELASTICSEARCH_ENDPOINT',
  auth: { apiKey: 'ELASTICSEARCH_API_KEY' },
  serverMode: "serverless",
});<h3>Cómo ejecutar el cliente en un entorno de función como servicio</h3><p>En el ejemplo, usamos un servidor Node.js, pero también puedes conectarte usando un entorno de función como servicio con funciones como 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>Otro ejemplo es conectarse a servicios como Vercel, que también es sin servidor. Puedes consultar este <a href="https://github.com/elastic/elasticsearch-js/blob/main/docs/examples/proxy/README.md">ejemplo completo</a> de cómo hacerlo, pero la parte más relevante del <a href="https://github.com/elastic/elasticsearch-js/blob/main/docs/examples/proxy/api/search.js">endpoint de búsqueda</a> es la siguiente:</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>Este endpoint reside en la carpeta /api y se ejecuta desde el lado del servidor, de modo que el cliente solo tiene control sobre el parámetro "texto" que corresponde al término de búsqueda.</p><p>La participación de usar función como servicio es que, a diferencia de un servidor que funciona 24/7, las funciones solo activan la máquina que ejecuta la función y, una vez terminada, la máquina entra en modo reposo para consumir menos recursos.</p><p>Esta configuración puede ser conveniente si la aplicación no recibe demasiadas solicitudes; De lo contrario, los costos pueden ser elevados. También tienes que considerar el <a href="https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtime-environment.html">ciclo de vida de las funciones</a> y los tiempos de ejecución (que en algunos casos pueden ser solo segundos).</p><h2>Conclusión</h2><p>En este artículo, aprendimos a manejar errores, algo crucial en entornos de producción. También cubrimos pruebas de nuestra aplicación simulando el servicio Elasticsearch, que proporciona pruebas fiables independientemente del estado del clúster y nos permite centrarnos en nuestro código.</p><p>Finalmente, demostramos cómo crear una pila totalmente serverless aprovisionando tanto Elastic Cloud Serverless como una aplicación 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[Conceptos básicos]]></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 de la manera correcta, parte I]]></title>
    <description><![CDATA[Explicando cómo crear un backend de Elasticsearch listo para producción en JavaScript.  

Explora cómo usar Elasticsearch con JavaScript para crear un servidor con diferentes endpoints de búsqueda para consultar documentos de Elasticsearch siguiendo las mejores prácticas de cliente/servidor.]]></description>
    <content:encoded><![CDATA[<p>Este es el primer artículo de un serial que explica cómo usar Elasticsearch con JavaScript. En este serial, aprenderás lo básico sobre cómo usar Elasticsearch en un entorno JavaScript y revisarás las características y mejores prácticas más relevantes para crear una aplicación de búsqueda. Al final, sabrás todo lo necesario para ejecutar Elasticsearch usando JavaScript.</p><p>En esta primera parte, repasaremos:</p><ul><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i#environment">Medio ambiente</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 o 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">Conexión del cliente</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">Indexación de documentos</a></p><ul><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i#elasticsearch-client">Cliente 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">Aplicaciones semánticas</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i#bulk-helper">Ayudante a granel</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">Búsqueda de datos</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)">Consulta léxica</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)">Consulta semántica</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)">Consulta híbrida</a></p></li></ul></li></ul><p><em>Puedes consultar el código fuente con los ejemplos </em><a href="https://github.com/Delacrobix/JS-client-best-practices_article"><em><strong>aquí</strong></em></a><em><strong>.</strong></em></p><h3>¿Qué es el cliente de Node.js Elasticsearch?</h3><p>El <a href="https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/index.html">cliente Elasticsearch Node.js</a> es una biblioteca de JavaScript que introduce las llamadas HTTP REST de la API Elasticsearch a JavaScript. Esto facilita la gestión y cuenta con ayudas que simplifican tareas como indexar documentos en lotes.</p><h2>Medio ambiente</h2><h3>¿Frontend, backend o serverless?</h3><p>Para crear nuestra aplicación de búsqueda usando el cliente JavaScript, necesitamos al menos dos componentes: un clúster Elasticsearch y un entorno de ejecución en JavaScript para ejecutar el cliente.</p><p>El cliente JavaScript soporta todas las soluciones Elasticsearch (Cloud, local y Serverless), y no hay diferencias importantes entre ellas ya que el cliente gestiona todas las variaciones internamente, así que no tienes que preocuparte por cuál usar.</p><p>Sin embargo, el tiempo de ejecución de JavaScript debe ejecutar desde el <strong>servidor</strong> y <strong>no directamente desde el navegador.</strong></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltd3ec469c83e3a71a/6a17e3d5445de91da44d00b6/92ce6cfd923c8008fa44f617a58193642d9d5879-661x410.png" alt="Elasticsearch en el entorno JavaScript." /><p>Esto se debe a que, al llamar a Elasticsearch desde el navegador, el usuario puede obtener información sensible como la clave de la API del clúster, el host o la propia consulta. Elasticsearch recomienda <strong>no exponer nunca el clúster directamente a Internet </strong>y usar una capa intermedia que abstraiga toda esta información para que el usuario solo pueda ver los parámetros. Puedes leer más sobre <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/es-security-principles.html#security-protect-cluster-traffic">este tema aquí</a>.</p><p>Sugerimos usar un esquema como este:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt4d7f215f2e70230a/6a17e3d6fbc5f83de6491a13/a08769f08ec73fe57bf2e961cfdfbb1cdd57919d-972x429.png" alt="Configuración del cliente de Elasticsearch Node.js." /><p>En este caso, el cliente solo envía los términos de búsqueda y una clave de autenticación para tu servidor mientras este tiene el control total de la consulta y la comunicación con Elasticsearch.</p><h3>Conexión del cliente</h3><p>Empieza creando una clave API siguiendo <a href="https://www.elastic.co/search-labs/tutorials/install-elasticsearch/elastic-cloud">estos pasos</a>.</p><p>Siguiendo el ejemplo anterior, crearemos un servidor Express sencillo y nos conectaremos a él usando un cliente de un servidor Node.JS.</p><p>Inicializaremos el proyecto con NPM e instalaremos el cliente Elasticsearch y <a href="https://expressjs.com/">Express.</a> Esta última es una biblioteca para abrir servidores en Node.js. Usando Express, podemos interactuar con nuestro backend vía HTTP.</p><p>Inicialemos el proyecto:</p><p><code>npm init -y</code></p><p>Dependencias de instalación:</p><p><code>npm install @elastic/elasticsearch express split2 dotenv</code></p><p>Déjame explicártelo:</p><ul><li><p><a href="https://www.npmjs.com/package/@elastic/elasticsearch"><em><strong>@elastic/elasticsearch</strong></em></a>: Es el cliente oficial de Node.js</p></li><li><p><a href="https://www.npmjs.com/package/express"><em><strong>express</strong></em></a>: Nos permitirá montar un servidor nodejs ligero para exponer Elasticsearch</p></li><li><p><a href="https://www.npmjs.com/package/split2"><em><strong>split2</strong></em></a>: Divide líneas de texto en un flujo. Útil para procesar nuestros archivos de ndjson línea a línea</p></li><li><p><a href="https://www.npmjs.com/package/dotenv"><em><strong>dotenv</strong></em></a>: Permítenos gestionar variables de entorno usando un .env archivo</p></li></ul><p>Crea un .env archiva en la raíz del proyecto y agrega las siguientes líneas:</p>ELASTICSEARCH_ENDPOINT="Your Elasticsearch endpoint"
ELASTICSEARCH_API_KEY="Your Elasticssearch API"<p>De esta manera, podemos importar esas variables usando el paquete <code>dotenv</code> .</p><p>Crea un archivo <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>Este código configura un servidor de Express.js básico que escucha en el puerto 3000 y se conecta a un clúster Elasticsearch usando una clave API para autenticación. Incluye un punto final /ping que, al acceder mediante una solicitud GET, consulta al clúster de Elasticsearch información básica empleando el método <code>.info()</code> del cliente Elasticsearch. </p><p>Si la consulta tiene éxito, devuelve la información del clúster en formato JSON; de lo contrario, devuelve un mensaje de error. El servidor también emplea middleware de parser corporal para gestionar los cuerpos de las solicitudes JSON.</p><p>Ejecuta el archivo para abrir el servidor:</p><p><code>node server.js</code></p><p>La respuesta debería ser la siguiente:</p>Server running on port 3000<p>Y ahora, consultemos el <code>/ping</code> endpoint para comprobar el estado de nuestro clúster de 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>Indexación de documentos</h2><p>Una vez conectados, podemos indexar documentos usando mapeos como <a href="https://www.elastic.co/search-labs/blog/semantic-search-simplified-semantic-text">semantic_text</a> para búsqueda semántica y texto para consultas de texto completo. Con estos dos tipos de campos, también podemos hacer <a href="https://www.elastic.co/what-is/hybrid-search">búsqueda híbrida</a>.</p><p>Crearemos un nuevo archivo <code>load.js</code> para generar los mapeos y subir los documentos.</p><h3>Cliente Elasticsearch</h3><p>Primero necesitamos instanciar y autenticar al cliente:</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>Aplicaciones semánticas</h3><p>Crearemos un índice con datos sobre un hospital veterinario. Almacenaremos la información del dueño, la mascota y los detalles de la visita.</p><p>Los datos sobre los que queremos realizar la búsqueda en texto completo, como nombres y descripciones, se almacenarán como texto. Los datos de categorías, como la especie o raza del animal, se almacenarán como palabras clave.</p><p>Además, copiaremos los valores de todos los campos en un campo semantic_text para poder ejecutar una búsqueda semántica también con esa información.</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>Ayudante a granel</h3><p>Otro beneficio del cliente es que podemos usar el <a href="https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/client-helpers.html#bulk-helper">helper de mayor</a> volumen para indexar en lotes. El helper en bulk nos permite gestionar fácilmente cosas como la concurrencia, los intentos y qué hacer con cada documento que pasa por la función y que tiene éxito o fracasa.</p><p>Una característica atractiva de este asistente es que puedes trabajar con streams. Esta función te permite enviar un archivo línea por línea en lugar de almacenar el archivo completo en la memoria y enviarlo a Elasticsearch de una sola vez.</p><p>Para subir los datos a Elasticsearch, crea un archivo llamado data.ndjson en la raíz del proyecto y agrega la información que aparece a continuación (alternativamente, puedes descargar el archivo con el conjunto de datos <a href="https://github.com/Delacrobix/JS-client-best-practices_article/blob/main/data.ndjson">desde aquí</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>Usamos split2 para transmitir las líneas de archivo mientras el asistente masivo las envía a 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>El código anterior dice un .ndjson archivo línea por línea y en volumen indexa cada objeto JSON en un índice Elasticsearch especificado usando el método <code>helpers.bulk</code> . Transmite el archivo usando <code>createReadStream</code> y <code>split2</code>, establece metadatos de indexación para cada documento y registra cualquier documento que no se procese. Una vez completado, registra el número de elementos indexados con éxito.</p><p>Alternativamente a la función <code>indexData</code> , puedes subir el archivo directamente a través de la interfaz usando Kibana y usar la <a href="https://www.elastic.co/docs/manage-data/ingest/upload-data-files">interfaz de archivos de datos de subida.</a></p><p>Ejecutamos el archivo para subir los documentos a nuestro clúster de 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>Búsqueda de datos en Elasticsearch</h2><p>Volviendo a nuestro archivo <code>server.js</code> , crearemos diferentes endpoints para realizar búsquedas léxicas, semánticas o híbridas.</p><p>En resumen, este tipo de búsquedas no son mutuamente excluyentes, sino que dependerán del tipo de pregunta que necesites responder.</p><p>Tipo de consulta</p><p>Caso de uso</p><p>Pregunta de ejemplo</p><p>Consulta léxica</p><p>Las palabras o raíces de palabras en la pregunta probablemente aparecerán en los documentos del índice. Similitud de token entre la pregunta y los documentos.</p><p>Busco una camiseta deportiva azul.</p><p>Consulta semántica</p><p>Las palabras de la pregunta probablemente no aparecerán en los documentos. Similitud conceptual entre la pregunta y los documentos.</p><p>Busco ropa para el frío.</p><p>Búsqueda híbrida</p><p>La pregunta contiene componentes léxicos y/o semánticos. Similitud de tokens y semántica entre la pregunta y los documentos.</p><p>Estoy buscando un vestido talla S para una boda en la playa.</p><p>Las partes <em><strong>léxicas </strong></em>de la pregunta probablemente formarán parte de títulos y descripciones, o nombres de categorías, mientras que las partes <em><strong>semánticas </strong></em>son conceptos relacionados con esos campos. <em><strong>El azul</strong></em> probablemente será un nombre de categoría o parte de una descripción, y <em><strong>la boda en la playa</strong></em> probablemente no lo sea, pero puede estar semánticamente relacionada con la ropa de lino.</p><h3>Consulta léxica (/search/lexic?q=&lt;query_term&gt;)</h3><p>La búsqueda léxica, también llamada búsqueda de texto completo, significa búsqueda basada en la similitud de los tokens; es decir, tras un análisis, se devolverán los documentos que incluyan los tokens en la búsqueda.</p><p>Puedes consultar nuestro tutorial práctico de búsqueda <a href="https://www.elastic.co/demo-gallery/lexical-search">léxica aquí</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>Probamos con: <em><strong>corte de uñas</strong></em></p>curl http://localhost:3000/search/lexic?q=nail%20trimming<p>Respuesta:</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>Consulta semántica (/search/semantic?q=&lt;query_term&gt;)</h3><p>La búsqueda semántica, a diferencia de la búsqueda léxica, encuentra resultados similares al significado de los términos de búsqueda mediante la búsqueda vectorial.</p><p>Puedes consultar nuestro tutorial práctico de búsqueda <a href="https://www.elastic.co/demo-gallery/semantic-search">semántica aquí</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>Hacemos la prueba con: <em><strong>¿Quién se hizo una pedicura?</strong></em></p>curl http://localhost:3000/search/semantic?q=Who%20got%20a%20pedicure?<p>Respuesta:</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>Consulta híbrida (/search/hybrid?q=&lt;query_term&gt;)</h3><p>La búsqueda híbrida nos permite combinar la búsqueda semántica y léxica, obteniendo así lo mejor de ambos mundos: se obtiene la precisión de buscar por token, junto con la proximidad de significado de la búsqueda semántica.</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>Hacemos pruebas con "<em><strong>¿Quién se hizo pedicura o tratamiento dental?"</strong></em></p>curl http://localhost:3000/search/hybrid?q=who%20got%20a%20pedicure%20or%20dental%20treatment<p>Respuesta:</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>Conclusión</h2><p>En esta primera parte de nuestro serial, explicamos cómo configurar nuestro entorno y crear un servidor con diferentes endpoints de búsqueda para consultar los documentos de Elasticsearch siguiendo las mejores prácticas cliente/servidor. Consulta <a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i">la segunda parte</a> de nuestro serial, en la que aprenderás las mejores prácticas de producción y cómo ejecutar el cliente de Node.js Elasticsearch en entornos serverless.</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[Conceptos básicos]]></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[Uso de Ollama con la API de inferencia]]></title>
    <description><![CDATA[Aprende cómo integrar Ollama con Elasticsearch usando la API de Inference.]]></description>
    <content:encoded><![CDATA[<p>En este artículo, aprenderemos cómo conectar modelos locales al modelo de inferencia de Elasticsearch usando Ollama y luego hacer preguntas a tus documentos usando Playground.</p><p>Elasticsearch permite a los usuarios conectarse a LLM empleando la <a href="https://www.elastic.co/es/guide/en/elasticsearch/reference/current/inference-apis.html">API de Open Inference</a>, compatible con proveedores como Amazon Bedrock, Cohere, Google AI, Azure AI Studio, HuggingFace, como servicio, entre otros.</p><p><a href="https://ollama.com">Ollama</a> es una herramienta que le permite descargar y ejecutar modelos LLM empleando su propia infraestructura (su máquina/servidor local). <a href="https://ollama.com/library">Aquí</a> puedes encontrar una lista de los modelos disponibles que son compatibles con Ollama.</p><p>Ollama es una gran opción si quieres alojar y probar diferentes modelos de código abierto sin tener que preocuparte por las diferentes formas en que se podría tener que configurar cada uno de los modelos, o por cómo crear una API para acceder a las funciones del modelo, ya que Ollama se encarga de todo.</p><p>Dado que la API de Ollama es compatible con la API de OpenAI, podemos integrar fácilmente el modelo de inferencia y crear una aplicación RAG empleando Playground.</p><h2>Prerrequisitos</h2><ol><li><p>Elasticsearch 8.17</p></li><li><p>Kibana 8.17</p></li><li><p>Python</p></li></ol><h2>Pasos</h2><ol><li><p><a href="https://www.elastic.co/es/search-labs/blog/ollama-with-inference-api#setting-up-ollama-llm-server">Configuración del servidor Ollama LLM</a></p></li><li><p><a href="https://www.elastic.co/es/search-labs/blog/ollama-with-inference-api#creating-mappings">Creación de asignaciones</a></p></li><li><p><a href="https://www.elastic.co/es/search-labs/blog/ollama-with-inference-api#indexing-data">Indexación de datos</a></p></li><li><p><a href="https://www.elastic.co/es/search-labs/blog/ollama-with-inference-api#asking-questions-using-playground">Hacer preguntas con Playground</a></p></li></ol><h2>Configuración del servidor Ollama LLM</h2><p>Vamos a configurar un servidor LLM para conectarlo a nuestra instancia de Playground usando Ollama. Necesitaremos:</p><ul><li><p>Descargue y ejecute Ollama.</p></li><li><p>Use ngrok para acceder al servidor sitio web local que hospeda Ollama a través de Internet</p></li></ul><h3>Descarga y ejecuta Ollama</h3><p>Para usar Ollama, primero debemos <a href="https://ollama.com/download">descargarlo</a>. Ollama ofrece soporte para Linux, Windows y macOS, así que simplemente descargue la versión de Ollama compatible con su sistema operativo <a href="https://ollama.com/download">aquí.</a> Una vez instalado Ollama, podemos elegir un modelo de esta <a href="https://ollama.com/library">lista</a> de LLM compatibles. En este ejemplo, usaremos el modelo <a href="https://ollama.com/library/llama3.2">llama3.2</a>, un modelo general multilingüe. En el proceso de configuración, habilitará la herramienta de línea de comandos para Ollama. Una vez descargado, puede ejecutar la siguiente línea:</p>ollama pull llama3.2<p>Lo que dará como resultado:</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>Una vez instalado, puede probarlo con este comando:</p>ollama run llama3.2<p>Hagamos una pregunta:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc12f240920e897f5/6a17f39425daab32a508a367/ad1eff81c1b04d2a747c3afd0ecbc215e5bd96fd-800x501.gif" alt="Ejecuta Ollama y hazle una pregunta" /><p>Con el modelo en ejecución, Ollama habilita una API que se ejecutaría de forma predeterminada en el puerto "11434". Hagamos una solicitud a esa API, siguiendo la <a href="https://github.com/ollama/ollama/blob/main/docs/api.md">documentación oficial</a>:</p>curl http://localhost:11434/api/generate -d '{                                          
  "model": "llama3.2",               
  "prompt": "What is the capital of France?"
}' <p>Esta es la respuesta que obtuvimos:</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>Tenga en cuenta que la respuesta específica para este punto de conexión es una transmisión.</em></p><h3>Exponer el punto de conexión a Internet mediante ngrok</h3><p>Dado que nuestro endpoint funciona en un entorno local, no se puede acceder a él desde otro punto, como nuestra instancia de Elastic Cloud, a través de Internet. <a href="https://ngrok.com">ngrok</a> nos permite exponer un puerto que ofrece una IP pública. Cree una cuenta en ngrok y siga la <a href="https://dashboard.ngrok.com/get-started/setup">guía de configuración</a> oficial.</p><p>Una vez instalado y configurado el agente ngrok, podemos exponer el puerto que usa Ollama:</p>ngrok http 11434 --host-header="localhost:11434"<p><em>Nota: El encabezado </em><em><code>--host-header="localhost:11434"</code></em><em> garantiza que el encabezado "Host" de las solicitudes coincida con "localhost:11434"</em></p><p>La ejecución de este comando devolverá un vínculo público que funcionará siempre que ngrok y el servidor de Ollama se ejecuten localmente.</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>En "Reenvío" podemos ver que ngrok generó una dirección URL. Almacénalo para más tarde.</p><p>Intentemos realizar una solicitud HTTP al punto de conexión de nuevo, ahora con la dirección URL generada por ngrok:</p>curl https://your-ngrok-endpoint.ngrok-free.app/api/generate -d '{                                          
  "model": "llama3.2",               
  "prompt": "What is the capital of France?"
}'<p>La respuesta debe ser similar a la anterior.</p><h2>Creación de asignaciones</h2><h3>Punto final ELSER</h3><p>Para este ejemplo, <a href="https://www.elastic.co/es/guide/en/elasticsearch/reference/current/put-inference-api.html">crearemos un punto final de inferencia mediante la API de inferencia de Elasticsearch</a>. Además, usaremos <a href="https://www.elastic.co/es/guide/en/machine-learning/current/ml-nlp-elser.html">ELSER</a> para generar las incrustaciones.</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>Para este ejemplo, imaginemos que tiene una farmacia que vende dos tipos de medicamentos:</p><ul><li><p>Medicamentos que requieren receta médica.</p></li><li><p>Medicamentos que NO requieren receta médica.</p></li></ul><p>Esta información se incluiría en el campo de descripción de cada medicamento.</p><p>El LLM debe interpretar este campo, por lo que estas son las asignaciones de datos que usaremos:</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>El campo <code>text_description</code> almacenará el texto sin formato de las descripciones, mientras que <code>semantic_field</code>, que es un tipo de campo <a href="https://www.elastic.co/es/guide/en/elasticsearch/reference/current/semantic-text.html">semantic_text</a> , almacenará las incrustaciones generadas por ELSER.</p><p>La propiedad <a href="https://www.elastic.co/es/guide/en/elasticsearch/reference/current/copy-to.html">copy_to</a> copiará el contenido del nombre y la <code>text_description</code> de los campos en el campo semántico para que se generen las incrustaciones de esos campos.</p><h2>Indexación de datos</h2><p>Ahora, indexemos los datos usando la <a href="https://www.elastic.co/es/guide/en/elasticsearch/reference/current/docs-bulk.html">API de _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>Respuesta:</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>Hacer preguntas con Playground</h2><p><a href="https://www.elastic.co/es/guide/en/kibana/current/playground.html">Playground</a> es una herramienta de Kibana que te permite crear rápidamente un sistema RAG empleando índices de Elasticsearch y un proveedor de LLM. Puedes leer este <a href="https://www.elastic.co/es/search-labs/blog/playground-connectors-data-chat">artículo</a> para saber más al respecto.</p><h3>Conectando el LLM local con Playground</h3><p>Primero debemos crear un conector que use la dirección URL pública que acabamos de crear. En Kibana, ve a <strong>Buscar&gt;Playground</strong> y luego haz clic en "Conectar a un LLM".</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt22148eabfabf6d3f/6a17f3963e9e459f97ba15c6/1854f0808f8150e359fe62ba5d901d32a88d477c-1600x867.png" alt="Conectando el LLM local con Playground for Ollama" /><p>Esta acción revelará un menú en el lado izquierdo de la interfaz de Kibana. Allí, haga clic en "OpenAI".</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt6e28194d9012f141/6a17f39725daab500a08a36b/c83d3c4d7035a518124ad7d22b38764db57b6800-933x1007.png" alt="Seleccione un conector: Open AI Ollama" /><p>Ahora podemos comenzar a configurar el conector OpenAI.</p><p>Vaya a "Configuración del conector" y para el proveedor de OpenAI, seleccione "Otro (servicio compatible con OpenAI)":</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt9984dce6f78a7c08/6a17f3990b0bed0b7add36c4/ecfcdc4b575c309bd55b4e61ca0ddb348aa84f64-917x268.png" alt="Establecer la configuración del conector para usar Ollama con la API de inferencia" /><p>Ahora, configuremos los otros campos. Para este ejemplo, llamaremos a nuestro modelo "medicines-llm". En el campo URL, usa la que genera ngrok (/v1/chat/completions). En el campo "Modelo predeterminado", selecciona "llama3.2". No usaremos una clave API, así que simplemente pon cualquier texto aleatorio para continuar:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt8a24b93a39d380fb/6a17f39b96142a15c7eb1c3c/5d3b5027c8096cbe49fb740d70aa24e849611a9d-916x688.png" alt="Agregar configuración" /><p>Haga clic en "Almacenar" y agregue los medicamentos índice haciendo clic en "Agregar fuentes de datos":</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt4f107a54d5be25f9/6a17f39d4b055deb9d432338/525113da59e902c8235f62bde8fb62371a63e11b-1579x753.png" alt="Agrega fuentes de datos para hacer preguntas a tus documentos con Playground" /><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt03fb36fe05dcdbf5/6a17f39ebe608602f40048be/96138de0bbe2c2ac619f64889d3487df62739ca4-466x805.png" alt="Agregar datos de consulta" /><p>¡Bien! Ahora tenemos acceso a Playground usando el LLM que estamos ejecutando localmente como motor RAG.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltb1b27580107259b3/6a17f3a096142abefceb1c40/cfb48b33c70f4534ab77eb01f58008237f65e6f4-1600x851.png" alt="Seleccionar la configuración del modelo en Playground" /><p>Antes de probarlo, agreguemos instrucciones más específicas al agente y aumentemos el número de documentos enviados al modelo a 10, para que la respuesta tenga la mayor cantidad de documentos posibles disponibles. El campo de contexto será <code>semantic_field</code>, que incluye el nombre y la descripción de los medicamentos, gracias a la propiedad copy_to.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt4fbc97dc87c6fc62/6a17f3a1e8fbce052c3a1aa0/0c57c9c0e1a0e7b58fffdd3ef81d67d41e2990c4-580x806.png" alt="Configuración de Moel en Elastic Playground" /><p>Ahora hagamos la pregunta: <em><strong>¿Puedo comprar clonazepam sin receta?</strong></em> y vea qué sucede:</p><p>Como era de esperar, obtuvimos la respuesta correcta.</p><h3>Pasos siguientes</h3><p>¡El siguiente paso es crear su propia aplicación! Playground proporciona un script de código en Python que puede ejecutar en su máquina y personalizarlo para satisfacer sus necesidades. Por ejemplo, colocándolo detrás de un servidor <a href="https://fastapi.tiangolo.com/">FastAPI</a> para crear un chatbot de medicamentos de control de calidad consumido por su interfaz de usuario.</p><p>Puedes encontrar este código haciendo clic en el botón <em><strong>Ver código</strong></em> en la sección superior derecha de Playground:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt42fe193b8aa08830/6a17f3a33e9e4569e8ba15ca/816bfd0e5f936ad65dbe719d5df10714e550a40b-380x121.png" alt="Ver botón de código" /><p>Y usa los <em><strong>puntos finales y las claves API</strong></em> para generar la variable de entorno <code>ES_API_KEY</code> requerida en el código.</p><p>Para este ejemplo en individuo, el código es el siguiente:</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>Para que funcione con Ollama, debe cambiar el cliente OpenAI para conectarse al servidor Ollama en lugar del servidor OpenAI. Puede encontrar la lista completa de ejemplos de OpenAI y puntos finales compatibles aquí.</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>Y también cambie el modelo a llama3.2 al llamar al método de finalización:</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>Agreguemos nuestra pregunta: <em><strong>¿Puedo comprar Clonazepam sin receta? </strong></em>A la consulta de 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>Y también a la llamada de finalización con un par de impresiones, para que podamos confirmar que estamos enviando los resultados de Elasticsearch como parte del contexto de la pregunta:</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>Ahora ejecutemos el comando</p><p><code>pip install -qU elasticsearch openai</code></p><p><code>python main.py</code></p><p>Deberías ver algo como esto:</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>Conclusión</h2><p>En este artículo, podemos ver el poder y la versatilidad de herramientas como Ollama cuando las usamos junto con la API de inferencia de Elasticsearch y Playground.</p><p>Luego de algunos pasos simples, teníamos una aplicación RAG en funcionamiento con un chat que usaba un LLM que se ejecutaba en nuestra propia infraestructura sin costo alguno. Esto también nos permite tener más control sobre los recursos y la información sensible, además de darnos acceso a una variedad de modelos para diferentes tareas.</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[AI]]></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>