<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
  <channel>
    <title><![CDATA[Javascript - Elasticsearch Labs]]></title>
    <description><![CDATA[Articles and tutorials from the Search team at Elastic]]></description>
    <copyright><![CDATA[© 2026. Elasticsearch B.V. All Rights Reserved]]></copyright>
    <image>
      <title><![CDATA[Javascript - Elasticsearch Labs]]></title>
      <url>https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1121c0bf0e8a6e65/6a88da6340a1841030ef456f/search-labs-thumbnail.png</url>
      <link>https://www.elastic.co/es/search-labs/blog/category/javascript-programming</link>
    </image>
    <link>https://www.elastic.co/es/search-labs/blog/category/javascript-programming</link>
    <atom:link href="https://www.elastic.co/es/search-labs/rss/category/javascript-programming.xml" rel="self" type="application/rss+xml"/>
    <language><![CDATA[es]]></language>
    <lastBuildDate>Mon, 28 Sep 2026 16:08:15 GMT</lastBuildDate>
  <item>
    <title><![CDATA[Construcción de un asistente RAG agente con JavaScript, Mastra y Elasticsearch]]></title>
    <description><![CDATA[Aprende a construir agentes de IA en el ecosistema JavaScript]]></description>
    <content:encoded><![CDATA[<p>Esta idea se me ocurrió en medio de una liga de baloncesto de fantasía muy intensa y de alto riesgo. Me preguntaba: <em>¿Podría crear un agente de IA que me ayudara a dominar mis enfrentamientos semanales? ¡Absolutamente!</em></p><p>En esta entrada, exploraremos cómo construir un asistente RAG agente usando <a href="https://mastra.ai/en/docs">Mastra</a> y una aplicación sitio web ligero en JavaScript para interactuar con él. Al conectar este agente con Elasticsearch, le damos acceso a datos estructurados de jugadores y la capacidad de ejecutar agregaciones estadísticas en tiempo real, para ofrecerte recomendaciones basadas en estadísticas de jugadores. Visita el <a href="https://github.com/jdarmada/nba-ai-assistant-js.git">repositorio</a> de GitHub para seguirle; el <a href="https://github.com/jdarmada/nba-ai-assistant-js/blob/main/README.md">README</a> proporciona instrucciones sobre cómo clonar y ejecutar la aplicación por tu cuenta. </p><p>Así debería ver cuando todo esté montado:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt63ea3e7a09306fbf/6a17f1d97f6f150e22c09c50/1c73bd1dc1b5fe54f025c7a2b7c322acc9122f3a-1999x1393.png" alt="" /><p>Nota: Esta entrada del blog se basa en "<a href="https://www.elastic.co/search-labs/blog/ai-agents-ai-sdk-elasticsearch">Construcción de agentes de IA con SDK de IA y Elastic</a>". Si eres nuevo en los agentes de IA en general y en qué podrían servir, empieza por ahí.
</p><h2><strong>Visión general de la arquitectura</strong></h2><p>En el núcleo del sistema se encuentra un gran modelo de lenguaje (LLM), que actúa como motor de razonamiento del agente (el cerebro). Interpreta la entrada del usuario, decide qué herramientas llamar y orquesta los pasos necesarios para generar una respuesta relevante.</p><p>El propio agente está estructurado por Mastra, un framework de agentes dentro del ecosistema JavaScript. Mastra envuelve el LLM con infraestructura backend, lo expone como un endpoint API y proporciona una interfaz para definir herramientas, indicaciones del sistema y comportamiento de agentes.</p><p>En el frontend, usamos <a href="https://vite.dev/guide/">Vite</a> para andamiar rápidamente una aplicación sitio web React que proporciona una interfaz de chat para enviar consultas al agente y recibir sus respuestas.</p><p>Por último, tenemos Elasticsearch, que almacena estadísticas de jugadores y datos de emparejamientos que el agente puede consultar y agregar.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blte13f09493f217047/6a17f1db1d1b83178d93e546/443bdc00d84ed1dd49e9f9e431e86ca4b0892563-1999x977.png" alt="" /><h2><strong>Fondo</strong></h2><p>Vamos a repasar algunos conceptos fundamentales:</p><h3><strong>¿Qué es el RAG agente?</strong></h3><p>Los agentes de IA pueden interactuar con otros sistemas, operar de forma independiente y realizar acciones basadas en sus parámetros definidos. El RAG agente combina la autonomía de un agente de IA con los principios de generación aumentada por recuperación, permitiendo a un LLM elegir qué herramientas llamar y qué datos usar como contexto para generar una respuesta. Lee más sobre <a href="https://www.elastic.co/search-labs/blog/retrieval-augmented-generation-rag">RAG aquí</a>.</p><h3><strong>Elegir un framework, ¿por qué ir más allá del SDK de IA?</strong></h3><p>Hay muchos frameworks de agentes de IA disponibles y probablemente oíste hablar de los más populares como <a href="https://www.elastic.co/search-labs/blog/using-crewai-with-elasticsearch">CrewAI</a>, <a href="https://www.elastic.co/search-labs/blog/using-autogen-with-elasticsearch">AutoGen</a> y <a href="https://www.elastic.co/search-labs/blog/build-rag-workflow-langgraph-elasticsearch">LangGraph</a>. La mayoría de estos frameworks comparten un conjunto común de funcionalidades, incluyendo soporte para diferentes modelos, uso de herramientas y gestión de memoria.</p><p>Aquí tienes una <a href="https://docs.google.com/spreadsheets/d/1B37VxTBuGLeTSPVWtz7UMsCdtXrqV5hCjWkbHN8tfAo/edit?gid=0#gid=0">hoja comparativa</a> de marcos de Harrison Chase (CEO de LangChain).</p><p>Lo que despertó mi interés con Mastra es que es un framework JavaScript-first diseñado para que desarrolladores full-stack integren fácilmente agentes en su ecosistema. El AI-SDK de Vercel también hace la mayoría de esto, pero donde Mastra destaca es cuando tus proyectos incluyen flujos de trabajo de agentes más complejos. Mastra mejora los patrones base establecido por el AI-SDK y en este proyecto los usaremos conjuntamente.</p><h3><strong>Marcos y consideraciones sobre la elección del modelo</strong></h3><p>Aunque estos frameworks pueden ayudarte a construir agentes de IA rápidamente, existen algunos inconvenientes a tener en cuenta. Por ejemplo, al usar cualquier otro framework fuera de agentes de IA o cualquier capa de abstracción en general, pierdes un poco de control. Si el LLM no emplea correctamente las herramientas o hace algo que no quieres, la abstracción dificulta la depuración. Aun así, en mi opinión, este equilibrio merece la facilidad y rapidez que obtienes al construir, especialmente porque estos frameworks están ganando impulso y se iteran constantemente.</p><p>De nuevo, estos frameworks son independientes de los modelos, lo que significa que puedes conectar y jugar diferentes modelos; recuerda que los modelos varían en los conjuntos de datos con los que fueron capacitados y, a su vez, varían en las respuestas que dan. Algunos modelos ni siquiera soportan la llamada de herramientas. Así que es posible cambiar y probar diferentes modelos para ver cuál te da las mejores respuestas, pero ten en cuenta que probablemente tendrás que reescribir el prompt del sistema para cada uno. Por ejemplo, usando Llama3.3 en comparación con GPT-4o, implica mucho más indicación e instrucciones específicas para obtener la respuesta que buscas.</p><h3><strong>Baloncesto de fantasía de la NBA</strong></h3><p>El baloncesto de fantasía consiste en empezar una liga con un grupo de colegas (advertencia, dependiendo de lo competitivo que sea tu grupo, esto podría afectar al estado de tus amistades), normalmente con algo de dinero en juego. Cada uno de vosotros selecciona un equipo de 10 jugadores para competir contra los 10 jugadores de otro amigo alternando semanalmente. Los puntos que contribuyen a tu puntaje global son cómo se desempeña cada uno de tus jugadores contra sus oponentes en una semana determinada.</p><p>Si un jugador de tu equipo se lesiona, es suspendido, etc., hay una lista de jugadores agentes libres disponibles para agregar a tu equipo. Aquí es donde ocurre gran parte de la dificultad en los deportes de fantasía, porque solo tienes un número limitado de fichas y todos están constantemente a la caza del mejor jugador.</p><p>Aquí es donde nuestro asistente de IA de la NBA brillará, especialmente en situaciones en las que tienes que decidir rápidamente qué jugador elegir. En lugar de tener que buscar manualmente cómo rinde un jugador contra un oponente específico, el asistente puede encontrar esos datos rápidamente y comparar promedios para darte una recomendación informada.</p><p>Ahora que ya sabes lo básico sobre el RAG agente y el baloncesto fantasy de la NBA, vamos a verlo en la práctica.</p><h2><strong>Construcción del proyecto</strong></h2><p>Si te quedas atascado en algún momento o no quieres montarlo desde cero, por favor consulta el <a href="https://github.com/jdarmada/nba-ai-assistant-js.git">repositorio</a>.</p><h3><strong>Lo que vamos a cubrir</strong></h3><ol><li><p><strong>Andamiaje del proyecto:</strong></p><ol><li><p><strong>Backend (Mastra):</strong> Usa NPX Create mastra@latest para andamiar el backend y definir la lógica del agente.</p></li><li><p><strong>Frontend (Vite + React):</strong> Usa npm create vite@latest para construir la interfaz de chat del frontend y así interactuar con el agente.</p></li></ol></li><li><p><strong>Configuración de variables de entorno</strong></p><ol><li><p>Instala dotenv para gestionar las variables del entorno.</p></li><li><p>Crea un .env archiva y proporciona las variables requeridas.</p></li></ol></li><li><p><strong>Configuración de Elasticsearch</strong></p><ol><li><p>Crea un clúster de Elasticsearch (ya sea localmente o en la nube).</p></li><li><p>Instala el cliente oficial de Elasticsearch.</p></li><li><p>Cerciórate de que las variables del entorno sean accesibles.</p></li><li><p>Establece conexión con el cliente.</p></li></ol></li><li><p><strong>Ingiriendo en masa datos de la NBA en Elasticsearch</strong></p><ol><li><p>Crea un índice con los mapeos apropiados para permitir agregaciones.</p></li><li><p>Ingirir en masa las estadísticas de los jugadores de un archivo CSV en un índice de Elasticsearch.</p></li></ol></li><li><p><strong>Definir agregaciones de Elasticsearch</strong></p><ol><li><p>Consulta para calcular promedios históricos contra un oponente específico.</p></li><li><p>Consulta para calcular los promedios de temporada contra un rival específico.</p></li></ol></li><li><p><strong>Archivo utilitario de comparación de jugadores</strong></p><ol><li><p>Consolida funciones auxiliares y agregaciones de Elasticsearch.</p></li></ol></li><li><p><strong>Construcción del agente</strong></p><ol><li><p>Agrega la definición del agente y el prompt del sistema.</p></li><li><p>Instala Zod y define herramientas.</p></li><li><p>Agregar una configuración de middleware para manejar CORS.</p></li></ol></li><li><p><strong>Integración del frontend</strong></p><ol><li><p>Usando el useChat de AI-SDK para interactuar con el agente.</p></li><li><p>Crea la interfaz para mantener conversaciones bien formateadas.</p></li></ol></li><li><p><strong>Ejecutando la aplicación</strong></p><ol><li><p>Inicia tanto el backend (servidor Mestra) como el frontend (app React).</p></li><li><p>Ejemplos de consultas y uso.</p></li></ol></li><li><p><strong>Qué sigue: Hacer que el agente sea más inteligente</strong></p><ol><li><p>Agregar capacidades de búsqueda semántica para permitir recomendaciones más profundas.</p></li><li><p>Habilitar la consulta dinámica moviendo la lógica de búsqueda al servidor Elasticsearch MCP (Model Context Protocol).</p></li></ol></li></ol><h3><strong>Prerrequisitos</strong></h3><ul><li><p><strong>Node.js y NPM</strong>: Tanto el backend como el frontend funcionan en Node. Cerciórate de tener instalados Node 18+ y npm v9+ (que vienen incluidos con Node 18+).</p></li><li><p><strong>Clúster de Elasticsearch:</strong> Un clúster activo de Elasticsearch, ya sea localmente o en la nube.</p></li><li><p><strong>Clave API de OpenAI</strong>: Genera una en la página de claves API <a href="https://platform.openai.com/api-keys">del portal para desarrolladores de OpenAI</a>.</p></li></ul><p></p><h3><strong>Estructura del proyecto</strong></h3><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt749baa120552e4ab/6a17f1dd1d1b83bfe993e54a/1c0bde11ad0eead523a95e03b9b905aa776e3fd1-1420x934.png" alt="" /><h4><strong>Paso 1: Estructurar el proyecto</strong></h4><ol><li><p>Primero, crea el directorio nba-ai-assistant-js y navega dentro usando: </p></li></ol>mkdir nba-ai-assistant-js &amp;&amp; cd nba-ai-assistant-js<p><strong>Backend:</strong></p><ol><li><p>Usa la herramienta de creación de Mastra con el comando: </p></li></ol>npx create-mastra@latest<p>2. Deberías recibir algunos prompts en tu terminal; para el primero, llamaremos al backend del proyecto:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt65abf68fe588e968/6a17f1de63baff2814741d5b/de2725031ed6837db99a979efcdd0ece1e197dbb-608x84.png" alt="" /><p>3. A continuación, mantendremos la estructura predeterminada para almacenar los archivos Mastra, así que introduzca <code>src/</code>.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt89bd829fcf0ae6b9/6a17f1e04b055dd30e432302/88919d9ff1852126395e1fcd700ecb1b59aac63c-866x116.png" alt="" /><p>4. Luego, elegiremos OpenAI como nuestro proveedor de LLM por defecto.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltfd167cc77a40b9a8/6a17f1e11480099e29b48863/2328761e769f3ded134e5a21e8a0bf8f41e88f68-404x210.png" alt="" /><p>5. Finalmente, te pedirá la clave de la API de OpenAI. Por ahora, elegiremos la opción de saltarlo y proporcionarlo más adelante en un archivo<code> .env</code> .</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt12654151ed495370/6a17f1e22f4a5c0f84fa89f9/0662de9bd28758e377e4c63df8d08b479068ce63-444x120.png" alt="" /><p><strong>Frontend:</strong></p><ol><li><p>Vuelve al directorio raíz y ejecuta la <a href="https://vite.dev/guide/">herramienta de creación de Vite</a> usando este comando: <code>npm create vite@latest frontend -- --template react</code></p></li></ol><p>Esto debería crear una aplicación ligera de React llamada <code>frontend</code> con una plantilla específica para React.</p><p>Si todo va bien, dentro de tu directorio de proyecto, deberías estar mirando un directorio backend que contenga el código Mastra y un directorio <code>frontend</code> con tu app React.</p><p></p><h4><strong>Paso 2: Configuración de variables de entorno</strong></h4><ol><li><p>Para gestionar claves sensibles, usaremos el paquete <code>dotenv</code> para cargar nuestras variables de entorno desde el .env archivo. Navega al directorio del backend e instala <code>dotenv</code>:</p></li></ol>cd backend
npm install dotenv --save<p>2. Mientras está en el directorio backend, se proporciona un archivo example.env con las variables adecuadas para rellenar. Si creas el tuyo propio, cerciórate de incluir las siguientes variables:</p># OpenAI Configuration
OPENAI_API_KEY=your_openai_api_key_here

# Elasticsearch Configuration
ELASTIC_ENDPOINT=your_elasticsearch_endpoint_here
ELASTIC_API_KEY=your_elasticsearch_api_key_here
<p></p><p>Nota: Cerciórate de que este archivo esté excluido de tu control de versiones agregando <code>.env</code> a <code>.gitignore</code>.</p><h4><strong>Paso 3: Configuración de Elasticsearch</strong></h4><p>Primero, necesitas un clúster activo de Elasticsearch. Hay dos opciones:</p><ul><li><p><strong>Opción A: Usar Elasticsearch Cloud</strong></p><ul><li><p>Apúntate a <a href="https://cloud.elastic.co/registration">Elastic Cloud</a></p></li><li><p>Crear un nuevo despliegue</p></li><li><p>Obtén la URL de tu endpoint y la clave API (codificada)</p></li></ul></li><li><p><strong>Opción B: Ejecutar Elasticsearch localmente</strong></p><ul><li><p>Instala y ejecuta Elasticsearch localmente</p></li><li><p>Usa http://localhost:9200 como punto final</p></li><li><p>Generar una clave API</p></li></ul></li></ul><p></p><p><strong>Instalación del cliente Elasticsearch en el backend:</strong></p><ol><li><p>Primero, instala el cliente oficial de Elasticsearch en tu directorio backend:</p></li></ol>npm install @elastic/elasticsearch<p>2. Luego crea una lib de directorios para almacenar funciones reutilizables y navega hacia ella:</p>mkdir lib &amp;&amp; cd lib<p>3. Dentro, crea un nuevo archivo llamado <a href="https://github.com/jdarmada/nba-ai-assistant-js/blob/main/backend/lib/elasticClient.js">elasticClient.js</a>. Este archivo inicializará el cliente Elasticsearch y lo expondrá para su uso en todo tu proyecto.</p><p>4. Como usamos módulos ECMAScript (ESM), __dirname and __nombre de archivo no están disponibles. Para cerciorarte de que tus variables de entorno se carguen correctamente desde el archivo .env Archivo en la carpeta de backend, agrega esta configuración en la parte superior de tu archivo:</p>import { config } from 'dotenv';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
import { Client } from '@elastic/elasticsearch';

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

// Load environment variables from the correct path
config({ path: envPath });<p>5. Ahora, inicializa el cliente Elasticsearch usando tus variables de entorno y comprueba la conexión:</p>//Elastic client Initialization, make sure environment variables are being loaded in correctly
const config= {
    node: `${process.env.ELASTIC_ENDPOINT}`,
    auth: {
        apiKey: `${process.env.ELASTIC_API_KEY}`,
    },
};

export const elasticClient = new Client(config);

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

checkConnection();
<p>Ahora, podemos importar esta instancia cliente a cualquier archivo que necesite interactuar con tu clúster de Elasticsearch.</p><p></p><h4><strong>Paso 4: Ingirir en masa datos de la NBA en Elasticsearch</strong></h4><p><strong>Conjunto de datos:</strong></p><p>Para este proyecto, consultaremos los conjuntos de datos disponibles en el <a href="https://github.com/jdarmada/nba-ai-assistant-js/tree/main/backend">directorio de backend/datos</a> del repositorio. Nuestro asistente de la NBA empleará estos datos como base de conocimiento para realizar comparaciones estadísticas y generar recomendaciones.</p><ul><li><p><a href="https://github.com/jdarmada/nba-ai-assistant-js/blob/main/backend/data/sample_nba_data.csv">sample_player_game_stats.csv</a> - Estadísticas de juego de un jugador (por ejemplo, puntos, rebotes, robos, etc., por partido por jugador a lo largo de toda su carrera en la NBA). Emplearemos este conjunto de datos para realizar agregaciones. (Nota: Estos son datos simulados, pregenerados para fines demo y no provenientes de fuentes oficiales de la NBA.)</p></li><li><p><a href="https://github.com/jdarmada/nba-ai-assistant-js/blob/main/backend/data/playerAndTeamInfo.js">playerAndTeamInfo.js</a> - Sustituye los metadatos de jugadores y equipos que normalmente proporcionaría una llamada a la API para que el agente pueda relacionar los nombres de jugadores y equipos con IDs. Como usamos datos de muestra, no queremos la sobrecarga de buscar desde una API externa, así que codificamos de forma fija algunos valores que el agente puede referenciar.</p></li></ul><p></p><p><strong>Implementación:</strong></p><ol><li><p>Mientras estés en el directorio <code>backend/lib</code> , crea un archivo llamado <a href="https://github.com/jdarmada/nba-ai-assistant-js/blob/main/backend/lib/playerDataIngestion.js">playerDataIngestion.js</a>.</p></li><li><p>Configura las importaciones, resuelve la ruta del archivo CSV y configura el análisis sintáctico. De nuevo, como estamos usando ESM, necesitamos reconstruir <code>__dirname</code> para resolver el camino hacia el CSV de muestra. Además, importaremos <a href="http://node.js/">Node.js</a> módulos incorporados, <code>fs</code> y <code>readline</code>, para analizar el archivo CSV dado línea por línea.</p></li></ol>import fs from 'fs';
import readline from 'readline';
import path from 'path';
import { fileURLToPath } from 'url';
import { elasticClient } from './elasticClient.js';

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

//Since we are using ES modules __dirname and __filename don't exist, so this is a workaround that allows us to use the absolute file path for our sample data.
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const filePath = path.resolve(__dirname, '../data/sample_nba_data.csv');<p>Esto te prepara para leer y analizar eficientemente el CSV cuando lleguemos al paso de la ingestión masiva.</p><p>3. Crear un índice con el mapeo adecuado. Aunque Elasticsearch puede inferir automáticamente los tipos de campos con <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/dynamic">mapeo dinámico</a>, queremos ser explícitos aquí para que cada estadística se trate como un campo numérico. Esto es importante porque usaremos estos campos para agregaciones más adelante. También queremos usar el tipo <code>float </code>para estadísticas como puntos, rebotes, etc., para cerciorarnos de incluir valores decimales. Finalmente, queremos agregar la propiedad de mapeo <code>dynamic: 'strict'</code> para que Elasticsearch no mapee dinámicamente campos no reconocidos. 
</p>// Function to create an index with mappings
async function createIndex() {
    try {
        // Check if the index already exists
        const exists = await elasticClient.indices.exists({ index: indexName });

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

        console.log('Index created:', response);
        return true;
    } catch (error) {
        console.error('Error creating index:', error);
        return false;
    }
}
<p>4. Agregar la función para ingirte en masa los datos CSV en tu índice de Elasticsearch. Dentro del bloque de código, saltamos la línea de cabecera. Luego, divide cada elemento de línea por una coma y empújalos en el objeto documento. Este paso también los limpia y cerciora que sean del tipo adecuado. A continuación, enviamos los documentos al array bulkBody junto con la información del índice, que servirá como carga útil para la ingestión masiva en Elasticsearch.</p>async function bulkIngestCsv(filePath) {
    const readStream = fs.createReadStream(filePath);
    const rl = readline.createInterface({
        input: readStream,
        crlfDelay: Infinity,
    });

    const bulkBody = [];
    let lineNum = 0;

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

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

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

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

    console.log(`Parsed ${lineNum} lines from CSV`);
<p>5. Luego, podemos usar la <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-bulk">API Bulk</a> de Elasticsearch con <code>elasticClient.bulk()</code> para ingirse varios documentos en una sola solicitud. La gestión de errores a continuación está estructurada para darte un recuento de cuántos documentos no se ingirieron y cuántos fueron exitosos.</p>try {
        // Perform the bulk request
        const response = await elasticClient.bulk({ body: bulkBody });

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

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

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

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

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

    } catch (error) {
        console.error('Error performing bulk ingestion:', error);
    }
}
<p>6. Ejecuta la función <code>main()</code> a continuación para ejecutar secuencialmente las funciones <code>createIndex()</code> y <code>bulkIngestCsv()</code> .</p>// Run this function
async function main() {
    const result = await createIndex();
    if (!result) {
        console.error('Index setup failed. Aborting.');
        return;
    }

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

main();
<p>Si ves un registro de consola que indique que la ingesta masiva fue exitosa, realiza una comprobación rápida en tu índice de Elasticsearch para ver si los documentos realmente se ingirieron con éxito.</p><h4><strong>Paso 5: Definición de agregaciones de Elasticsearch y consolidación</strong></h4><p>Estas serán las funciones principales que se emplearán cuando definamos las herramientas para el Agente de IA para comparar las estadísticas de los jugadores entre sí.</p><p>1. Navega al directorio <code>backend/lib</code> y crea un archivo llamado <a href="https://github.com/jdarmada/nba-ai-assistant-js/blob/main/backend/lib/elasticAggs.js">elasticAggs.js</a>.</p><p>2. Agregar la consulta siguiente para calcular los promedios históricos de un jugador frente a un oponente específico. Esta consulta emplea un <a href="https://www.elastic.co/search-labs/tutorials/search-tutorial/full-text-search/filters">filtro</a> <code>bool</code> con 2 condiciones: una que coincide con <code>player_id</code> y otra que coincide con la <code>opponent_team_id</code>, para recuperar solo los juegos relevantes. No necesitamos devolver ningún documento, solo nos importan las agregaciones, así que establecemos <code>size:0</code>. Bajo el bloque <code>aggs</code> , ejecutamos múltiples <a href="https://www.elastic.co/docs/explore-analyze/query-filter/aggregations">agregaciones métricas</a> en paralelo en campos como <code>points, rebounds, assists, steals, blocks</code> y <code>fg_percentage</code> para calcular sus valores medios. Los LLMs pueden ser impredecibles con los cálculos y esto transfiere ese proceso a Elasticsearch, cerciorando que nuestro asistente de IA de la NBA tenga acceso a datos precisos.</p>export async function getHistoricalAveragesAgainstOpponent(player_id, opponent_team_id) {
    try {
        //Query for Historical Averages
        const historicalQuery = await elasticClient.search({
            index: 'sample-nba-player-data', 
            size: 0,
            query: {
                bool: {
                    must: [
                        {
                            term: {
                                player_id: {
                                    value: player_id,
                                },
                            },
                        },
                        {
                            term: {
                                opponent_team_id: {
                                    value: opponent_team_id,
                                },
                            },
                        },
                    ],
                },
            },
            aggs: {
                avg_points: { avg: { field: 'points' } },
                avg_rebounds: { avg: { field: 'rebounds' } },
                avg_assists: { avg: { field: 'assists' } },
                avg_steals: { avg: { field: 'steals' } },
                avg_blocks: { avg: { field: 'blocks' } },
             avg_fg_percentage: { avg: { field: 'fg_percentage' } },
            },
        });

        return {
            points: historicalQuery.aggregations.avg_points.value || 0,
            rebounds: historicalQuery.aggregations.avg_rebounds.value || 0,
            assists: historicalQuery.aggregations.avg_assists.value || 0,
            steals: historicalQuery.aggregations.avg_steals.value || 0,
            blocks: historicalQuery.aggregations.avg_blocks.value || 0,
            fgPercentage: historicalQuery.aggregations.avg_fg_percentage.value || 0,
        };
    } catch (error) {
        console.error('Query error from getHistoricalAveragesAgainstOpponent function:', error);
        return { error: 'Queries failed in getting historical averages against opponent.' };
    }
}
<p>3. Para calcular los promedios de temporada de un jugador frente a un oponente específico, usaremos prácticamente la misma consulta que la histórica. La única diferencia en esta consulta es que el filtro <code>bool</code> tiene una condición adicional para <code>game_date</code>. El <code>game_date</code> del campo tiene que estar dentro del rango de la temporada actual de la NBA. En este caso, el rango está entre <code>2024-10-01</code> y <code>2025-06-30</code>. Esta condición adicional a continuación garantiza que las agregaciones que sigan aislarán solo los partidos de esta temporada.
</p>        {
                            range: {
                    //Range for this season, change to match current season
                                game_date: {
                                    gte: '2024-10-01',
                                    lte: '2025-06-30',
                                },
                            },
<h4><strong>Paso 6: Utilidad de comparación de jugadores</strong></h4><p>Para mantener nuestro código modular y mantenible, crearemos un archivo de utilidad que consolide las funciones auxiliares de metadatos y las agregaciones de Elasticsearch. Esto alimentará la herramienta principal que emplea el agente. Más sobre eso más adelante:</p><p>1. Crear un nuevo <a href="https://github.com/jdarmada/nba-ai-assistant-js/blob/main/backend/lib/comparePlayers.js">archivo comparePlayers.js</a> en el directorio <code>backend/lib</code> .</p><p>2. Agregar la función siguiente para consolidar los asistentes de metadatos y la lógica de agregación de Elasticsearch en una única función que alimente la herramienta principal empleada por el agente.
</p>import { playersByName } from '../data/playerAndTeamInfo.js';
import { teamsByName } from '../data/playerAndTeamInfo.js';
import { upcomingMatchups } from '../data/playerAndTeamInfo.js';
import { getHistoricalAveragesAgainstOpponent } from './elasticAggs.js';
import { getSeasonAveragesAgainstOpponent } from './elasticAggs.js';

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

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

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

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

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

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

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

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

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

    return [player1, player2];
}
<h4><strong>Paso 7: Construir el agente</strong></h4><p>Ahora que creaste el frontend y el backend de estructura, absorbiste datos de los partidos de la NBA y establecido una conexión con Elasticsearch, podemos empezar a juntar todas las piezas para construir el agente.</p><p><strong>Definición del agente</strong></p><p>1. Navega al archivo <a href="https://github.com/jdarmada/nba-ai-assistant-js/blob/main/backend/src/mastra/agents/index.ts">index.ts</a> dentro del directorio <code>backend/src/mastra/agents</code> y agrega la definición del agente. Puedes especificar campos como:</p><ul><li><p><strong>Nombre:</strong> Dale a tu agente un nombre que se use como referencia cuando te llamen en el frontend.</p></li><li><p><strong>Instrucciones/prompt del sistema: </strong>Un prompt del sistema proporciona al LLM el contexto inicial y las reglas a seguir durante la interacción. Es similar al prompt que los usuarios envían a través del cuadro de chat, pero este se da antes de cualquier entrada del usuario. De nuevo, esto cambiará según el modelo que elijas.</p></li><li><p><strong>Modelo:</strong> ¿Qué LLM usar (Mastra soporta OpenAI, Anthropic, modelos locales, etc.).</p></li><li><p><strong>Herramientas:</strong> Una lista de funciones de la herramienta que el agente puede llamar.</p></li><li><p><strong>Memoria:</strong> (Opcional) si queremos que el agente recuerde el historial de conversaciones, etc. Para simplificar, podemos empezar sin memoria persistente, aunque Mastra la admite.</p></li></ul><p></p>import { openai } from '@ai-sdk/openai';
import { Agent } from '@mastra/core/agent';
import { playerComparisonTool } from '../tools';

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

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

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

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

      Format your response using Markdown syntax. Use:

        Example output format:

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


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

        #### Fantasy Recommendation  
        Explain which player is the better fantasy pickup and why.
      
    `,
    model: openai('gpt-4o'),
    tools: { playerComparisonTool },
});
<p><strong>
Definición de herramientas</strong></p><ol><li><p>Navega al archivo <a href="https://github.com/jdarmada/nba-ai-assistant-js/blob/main/backend/src/mastra/tools/index.ts">index.ts</a> dentro del directorio <code>backend/src/mastra/tools</code> .</p></li><li><p>Instala Zod usando el comando:</p></li></ol>npm install zod<p>3. Agregar definiciones de herramientas. Ten en cuenta que importamos la función dentro del archivo <code>comparePlayers.js</code> como la función principal que el agente usará al llamar a esta herramienta. Usando la función <code>createTool()</code> de Mastra, registraremos nuestro <code>playerComparisonTool</code>. Los campos incluyen:</p><ul><li><p><code>id</code>: Esta es una descripción en lenguaje natural para ayudar al agente a entender qué hace la herramienta.</p></li><li><p><code>input schema</code>: Para definir la forma de la entrada de la herramienta, Mastra emplea el esquema <a href="https://zod.dev/">Zod</a> , que es una biblioteca de validación de esquema TypeScript. Zod ayuda cerciorar de que el agente reciba entradas estructuradas correctamente y evita que la herramienta se ejecute si la estructura de entrada no coincide.</p></li><li><p><code>description</code>: Esta es una descripción en lenguaje natural para ayudar al agente a entender cuándo llamar y usar la herramienta.</p></li><li><p><code>execute</code>: La lógica que se ejecuta cuando se llama a la herramienta. En nuestro caso, estamos usando una función auxiliar importada para devolver estadísticas de rendimiento.</p></li></ul>import { comparePlayersForNextMatchup } from '../../../lib/comparePlayers.js'
import { createTool } from "@mastra/core/tools";
import { z } from "zod";

export const playerComparisonTool = createTool({
    id: "Compare two NBA players",
    inputSchema: z.object({
        player1:z.string(),
        player2:z.string()
    }),
    description: "Use this tool to compare two players given in the user prompt.",
    execute: async ({ context: { player1, player2 } }) =&gt; {
        return await comparePlayersForNextMatchup(player1, player2);
      },
})<p><strong>Adición de middleware para manejar CORS</strong></p><p>Agregar middleware en el servidor Mastra para manejar <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS">CORS</a>. Dicen que hay tres cosas en la vida que no puedes evitar: la muerte, los impuestos, y para los desarrolladores web es el CORS. En resumen, el Intercambio de Recursos entre Orígenes es una función de seguridad en el navegador que bloquea al frontend para que no haga peticiones a un backend que se ejecuta en otro dominio o puerto. Aunque ejecutamos tanto el backend como el frontend en localhost, usan puertos diferentes, lo que activa la política CORS. Necesitamos agregar el middleware especificado en la <a href="https://mastra.ai/en/docs/server-db/middleware">documentación de Mastra</a> para que nuestro backend permita esas peticiones desde el frontend.</p><p>1. Navega al archivo <a href="https://github.com/jdarmada/nba-ai-assistant-js/blob/main/backend/src/mastra/index.ts">index.ts</a> dentro del directorio <code>backend/src/mastra</code> y agrega la configuración para CORS:</p><ul><li><p><code>origin: ['http://localhost:5173']</code></p><ul><li><p>Permite solicitudes solo desde esta dirección (dirección predeterminada de Vite)</p></li></ul></li><li><p><code>allowMethods: ["GET", "POST"]</code></p><ul><li><p>Métodos HTTP que están permitidos. La mayoría de las veces, será usando POST.</p></li></ul></li><li><p><code>allowHeaders: ["Content-Type", "Authorization", "x-mastra-client-type, "x-highlight-request", "traceparent"],</code></p><ul><li><p>Estos deciden qué encabezados personalizados pueden usar en las solicitudes</p></li></ul></li></ul><p></p>import { Mastra } from '@mastra/core/mastra';
import { basketballAgent } from './agents';

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

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

});

console.log('Mastra server configured.'); // Log after server configuration
<h4><strong>Paso 8: Integración del frontend</strong></h4><p>Este componente React proporciona una interfaz de chat sencilla que se conecta al agente Mastra AI usando el gancho <a href="https://mastra.ai/en/docs/frameworks/agentic-uis/ai-sdk#using-the-usechat-hook">useChat()</a> de <code>@ai-sdk/react</code>. También vamos a usar este gancho para mostrar el uso de tokens, llamadas a herramientas y para renderizar la conversación. En el prompt del sistema anterior, también pedimos al agente que genere la respuesta en markdown, así que usaremos <code>react-markdown</code> para formatear correctamente la respuesta.</p><p></p><p>1. Mientras estés en el directorio frontend, instala el paquete @ai-sdk/react para usar el hook useChat().</p>npm install @ai-sdk/react<p>2. Mientras estés en el mismo directorio, instala React Markdown para que podamos formatear correctamente la respuesta que genera el agente.</p>npm install react-markdown<p>3. Implementar <code>useChat()</code>. Este gancho gestionará la interacción entre tu frontend y el backend de tu agente de IA. Gestiona el estado del mensaje, la entrada del usuario, el estado y te proporciona ganchos de ciclo de vida para fines de observabilidad. Las opciones que descartamos incluyen:</p><ul><li><p><code>api:</code> Esto define el punto final de tu Mastra AI Agent. Por defecto se pone en el puerto 4111 y también queremos agregar la ruta que soporta respuestas en streaming.</p></li><li><p><code>onToolCall</code>: Esto se ejecuta cada vez que el agente llama a una herramienta; Lo estamos usando para rastrear qué herramientas está llamando nuestro agente.</p></li><li><p><code>onFinish</code>: Esto se ejecuta después de que el agente complete una respuesta completa. Aunque activamos el streaming, <code>onFinish</code> seguirá ejecutar luego de recibir el mensaje completo y no luego de cada chunk. Aquí, lo estamos usando para rastrear nuestro uso de tokens. Esto puede ser útil para monitorizar los costos de los LLM y optimizarlos.</p></li></ul><p>4. Por último, ve al componente <a href="https://github.com/jdarmada/nba-ai-assistant-js/blob/main/frontend/components/ChatUI.jsx">ChatUI.jsx</a> en el directorio <code>frontend/components</code> para crear la interfaz que mantenga nuestra conversación. A continuación, envuelve la respuesta en un componente <code>ReactMarkdown</code> para formatear correctamente la respuesta del agente.</p>import React, { useState } from 'react';
import { useChat } from '@ai-sdk/react';
import ReactMarkdown from 'react-markdown';

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

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

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

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

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

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

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

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

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

            &lt;form onSubmit={handleSubmit}&gt;
                &lt;input
                    type="text"
                    value={input}
                    onChange={handleInputChange}
                    placeholder="Input two players you want to compare."
                    className="input-box"
                /&gt;
                &lt;button type="submit" disabled={status === 'streaming'}&gt;
                    {status === 'streaming' ? 'Thinking...' : 'Send'}
                &lt;/button&gt;
            &lt;/form&gt;
        &lt;/div&gt;
    );
}<h4><strong>Paso 9: Ejecutar la aplicación</strong></h4><p>¡Felicidades! Ahora estás listo para ejecutar la aplicación. Sigue estos pasos para empezar tanto el backend como el frontend.</p><ol><li><p>En una ventana de terminal, empezando desde el directorio raíz, navega hasta el directorio backend e inicia el servidor Mestra:</p></li></ol>cd backend

npm run dev<p>2. En otra ventana de terminal, empezando desde el directorio raíz, navega hasta el directorio frontend e inicia la aplicación React:</p><p></p>cd frontend

npm run dev<p></p><p>3. Ve a tu navegador y navega a:</p><p></p><p><a href="http://localhost:5173/">http://localhost:5173</a></p><p></p><p>Deberías poder ver la interfaz del chat. Prueba estos ejemplos de prompts:</p><ul><li><p>"Compara a LeBron James y Stephen Curry"</p></li><li><p>"¿A quién debería elegir entre Jayson Tatum y Luka Doncic?"</p></li></ul><p></p><h3><strong>Qué sigue: Hacer que el agente sea más inteligente</strong></h3><p>Para que el asistente sea más agenciado y las recomendaciones más perspicaces, agregaré algunas mejoras clave en la próxima iteración.</p><p></p><p><strong>Búsqueda semántica de noticias de la NBA</strong></p><p>Hay un montón de factores que pueden afectar al rendimiento de los jugadores, muchos de los cuales no aparecen en las estadísticas brutas. Cosas como reportes de lesiones, cambios en la alineación o incluso análisis postpartido, solo se pueden encontrar en artículos de prensa. Para captar este contexto adicional, agregaré capacidades de búsqueda semántica para que el agente pueda recuperar artículos relevantes de la NBA e incluir esa narrativa en sus recomendaciones.</p><p></p><p><strong>Búsqueda dinámica con el servidor MCP de Elasticsearch</strong></p><p>MCP (Model Context Protocol) se está convirtiendo rápidamente en el estándar para cómo los agentes se conectan a fuentes de datos. Migraré la lógica de búsqueda al servidor Elasticsearch MCP, que permite al agente construir consultas dinámicamente en lugar de depender de funciones de búsqueda predefinidas que proporcionamos. Esto nos permite emplear más flujos de trabajo en lenguaje natural y reduce la necesidad de escribir manualmente cada consulta de búsqueda. Descubre más sobre el servidor MCP de Elasticsearch y el estado actual del <a href="https://www.elastic.co/search-labs/blog/mcp-current-state">ecosistema aquí</a>.</p><p></p><p>Estos cambios ya están en marcha, ¡estad atentos!</p><h3><strong>Conclusión</strong></h3><p></p><p>En este blog, creamos un asistente RAG agente que ofrece recomendaciones personalizadas para tu equipo de baloncesto de fantasía usando JavaScript, Mastra y Elasticsearch. Cubrimos lo siguiente:</p><ul><li><p><strong>Fundamentos del RAG agente</strong> y cómo combinar la autonomía de un agente de IA con las herramientas para usar RAG de forma eficaz puede conducir a agentes más matizados y dinámicos.</p></li><li><p><strong>Elasticsearch </strong>y cómo sus capacidades de almacenamiento de datos y poderosas agregaciones nativas lo convierten en un gran socio como base de conocimiento para un LLM.</p></li><li><p><strong>El framework Mastra </strong>y cómo simplifica la construcción de estos agentes para desarrolladores en el ecosistema javaScript.</p></li></ul><p>Tanto si eres un fanático del baloncesto, explorando cómo crear agentes de IA, o ambos como yo, espero que este blog te dio algunos cimientos para empezar. El repositorio completo está disponible en <a href="https://github.com/jdarmada/nba-ai-assistant-js">GitHub</a>, siéntete libre de clonar y trastear. ¡Ahora, gana esa liga de fantasía!</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/agentic-rag</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/agentic-rag</guid>
    <category><![CDATA[AI]]></category>
    <category><![CDATA[AI agéntica]]></category>
    <category><![CDATA[Javascript]]></category>
    <dc:creator><![CDATA[JD Armada]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt8ffd561836a4cb20/6a17f1e47b54f978588b39e4/8132ed781c1ea5d46ca244182f421ed5c721f23b-1200x628.png" length="0" type="image/png"/>
    <pubDate>Tue, 01 Jul 2025 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Elasticsearch en JavaScript 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>
  </channel>
</rss>