<?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/pt/search-labs/blog/category/javascript-programming</link>
    </image>
    <link>https://www.elastic.co/pt/search-labs/blog/category/javascript-programming</link>
    <atom:link href="https://www.elastic.co/pt/search-labs/rss/category/javascript-programming.xml" rel="self" type="application/rss+xml"/>
    <language><![CDATA[pt]]></language>
    <lastBuildDate>Tue, 29 Sep 2026 02:19:26 GMT</lastBuildDate>
  <item>
    <title><![CDATA[Construindo um assistente RAG ativo com JavaScript, Mastra e Elasticsearch.]]></title>
    <description><![CDATA[Aprenda a criar agentes de IA no ecossistema JavaScript.]]></description>
    <content:encoded><![CDATA[<p>Essa ideia me ocorreu durante uma acirrada e decisiva liga de basquete de fantasia. Eu me perguntei: <em>será que eu conseguiria criar um agente de IA que me ajudasse a dominar meus confrontos semanais? Com certeza!</em></p><p>Neste artigo, exploraremos como construir um assistente RAG agente usando <a href="https://mastra.ai/en/docs">o Mastra</a> e um aplicativo web JavaScript leve para interagir com ele. Ao conectar este agente ao Elasticsearch, damos a ele acesso a dados estruturados dos jogadores e a capacidade de executar agregações estatísticas em tempo real, para fornecer recomendações baseadas em estatísticas dos jogadores. Acesse o <a href="https://github.com/jdarmada/nba-ai-assistant-js.git">repositório</a> do GitHub para acompanhar; o <a href="https://github.com/jdarmada/nba-ai-assistant-js/blob/main/README.md">arquivo README</a> fornece instruções sobre como clonar e executar o aplicativo por conta própria. </p><p>Eis como deverá ficar quando tudo estiver montado:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt63ea3e7a09306fbf/6a17f1d97f6f150e22c09c50/1c73bd1dc1b5fe54f025c7a2b7c322acc9122f3a-1999x1393.png" alt="" /><p>Nota: Este post do blog complementa o artigo “<a href="https://www.elastic.co/search-labs/blog/ai-agents-ai-sdk-elasticsearch">Criando agentes de IA com o SDK de IA e o Elastic</a>”. Se você é iniciante no estudo de agentes de IA em geral e em suas possíveis aplicações, comece por aí.
</p><h2><strong>Visão geral da arquitetura</strong></h2><p>No núcleo do sistema está um modelo de linguagem abrangente (LLM, na sigla em inglês), que atua como o motor de raciocínio do agente (o cérebro). Ele interpreta a entrada do usuário, decide quais ferramentas utilizar e orquestra as etapas necessárias para gerar uma resposta relevante.</p><p>O próprio agente é estruturado pelo Mastra, um framework de agentes no ecossistema JavaScript. O Mastra integra o LLM com infraestrutura de backend, expõe-no como um endpoint de API e fornece uma interface para definir ferramentas, prompts do sistema e comportamento do agente.</p><p>Na interface, usamos <a href="https://vite.dev/guide/">o Vite</a> para criar rapidamente um aplicativo web React que fornece uma interface de chat para enviar perguntas ao agente e receber suas respostas.</p><p>Por fim, temos o Elasticsearch, que armazena estatísticas de jogadores e dados de confrontos que o agente pode consultar e agregar.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blte13f09493f217047/6a17f1db1d1b83178d93e546/443bdc00d84ed1dd49e9f9e431e86ca4b0892563-1999x977.png" alt="" /><h2><strong>Histórico</strong></h2><p>Vamos revisar alguns conceitos fundamentais:</p><h3><strong>O que é RAG agentivo?</strong></h3><p>Os agentes de IA podem interagir com outros sistemas, operar de forma independente e executar ações com base em parâmetros definidos por eles. O Agentic RAG combina a autonomia de um agente de IA com os princípios da geração aumentada por recuperação, permitindo que um LLM escolha quais ferramentas utilizar e quais dados usar como contexto para gerar uma resposta. Leia mais sobre a RAG <a href="https://www.elastic.co/search-labs/blog/retrieval-augmented-generation-rag">aqui</a>.</p><h3><strong>Ao escolher uma estrutura, por que ir além do AI-SDK?</strong></h3><p>Existem muitas estruturas de agentes de IA disponíveis e você provavelmente já ouviu falar das mais 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> e <a href="https://www.elastic.co/search-labs/blog/build-rag-workflow-langgraph-elasticsearch">LangGraph</a>. A maioria dessas estruturas compartilha um conjunto comum de funcionalidades, incluindo suporte para diferentes modelos, uso de ferramentas e gerenciamento de memória.</p><p>Segue abaixo uma <a href="https://docs.google.com/spreadsheets/d/1B37VxTBuGLeTSPVWtz7UMsCdtXrqV5hCjWkbHN8tfAo/edit?gid=0#gid=0">tabela comparativa</a> de frameworks elaborada por Harrison Chase (CEO da LangChain).</p><p>O que despertou meu interesse no Mastra foi o fato de ser um framework que prioriza o JavaScript, criado para que desenvolvedores full-stack possam integrar agentes facilmente em seu ecossistema. O SDK de IA da Vercel também faz a maior parte disso, mas o grande diferencial do Mastra é quando seus projetos incluem fluxos de trabalho de agentes mais complexos. O Mastra aprimora os padrões básicos definidos pelo AI-SDK e, neste projeto, usaremos os dois em conjunto.</p><h3><strong>Considerações sobre estruturas e escolha de modelos</strong></h3><p>Embora essas estruturas possam ajudá-lo a criar agentes de IA rapidamente, existem algumas desvantagens a serem consideradas. Por exemplo, ao usar qualquer outra estrutura fora dos agentes de IA ou de qualquer camada de abstração em geral, você perde um pouco do controle. Se o LLM não usar as ferramentas corretamente ou fizer algo que você não deseja, a abstração dificulta a depuração. Ainda assim, na minha opinião, essa troca vale a pena pela facilidade e rapidez que se obtém ao construir, especialmente porque essas estruturas estão ganhando força e sendo constantemente aprimoradas.</p><p>Novamente, essas estruturas são agnósticas em relação ao modelo, o que significa que você pode usar diferentes modelos sem problemas. Lembre-se de que os modelos variam nos conjuntos de dados em que foram treinados e, consequentemente, variam nas respostas que fornecem. Alguns modelos sequer suportam a chamada de ferramentas. Portanto, é possível alternar e testar diferentes modelos para ver qual oferece as melhores respostas, mas lembre-se de que provavelmente você terá que reescrever o prompt do sistema para cada um deles. Por exemplo, usando Llama3.3 Em comparação com o GPT-40, é necessário muito mais estímulo e instruções específicas para obter a resposta desejada.</p><h3><strong>Basquete de fantasia da NBA</strong></h3><p>O basquete de fantasia envolve a criação de uma liga com um grupo de amigos (atenção: dependendo do nível de competitividade do grupo, isso pode afetar o status das suas amizades), geralmente com algum dinheiro em jogo. Cada um de vocês monta uma equipe de 10 jogadores para competir contra a equipe de 10 jogadores de um amigo, alternando semanalmente. Os pontos que contribuem para a sua pontuação geral são definidos pelo desempenho de cada um dos seus jogadores contra os adversários em uma determinada semana.</p><p>Se um jogador da sua equipe se lesionar, for suspenso, etc., existe uma lista de jogadores disponíveis no mercado para adicionar à sua equipe. É aqui que entra em jogo grande parte da estratégia complexa nos esportes de fantasia, porque você tem um número limitado de jogadores para escolher e todos estão constantemente em busca do melhor jogador.</p><p>É aqui que nosso assistente de IA da NBA brilhará, especialmente em situações em que você precisa decidir rapidamente qual jogador escolher. Em vez de ter que pesquisar manualmente o desempenho de um jogador contra um adversário específico, o assistente pode encontrar esses dados rapidamente e comparar as médias para fornecer uma recomendação precisa.</p><p>Agora que você já conhece alguns conceitos básicos sobre RAG agentivo e basquete fantasy da NBA, vamos ver como funciona na prática.</p><h2><strong>Construindo o projeto</strong></h2><p>Se você ficar preso em algum ponto ou não quiser construir tudo do zero, consulte o <a href="https://github.com/jdarmada/nba-ai-assistant-js.git">repositório</a>.</p><h3><strong>O que abordaremos</strong></h3><ol><li><p><strong>Estruturando o projeto:</strong></p><ol><li><p><strong>Backend (Mastra):</strong> Use o comando `npx create mastra@latest` para criar a estrutura do backend e definir a lógica do agente.</p></li><li><p><strong>Frontend (Vite + React):</strong> Use o comando `npm create vite@latest` para criar a interface de chat do frontend para interação com o agente.</p></li></ol></li><li><p><strong>Configurando variáveis de ambiente</strong></p><ol><li><p>Instale o dotenv para gerenciar variáveis de ambiente.</p></li><li><p>Crie um arquivo .env arquive e forneça as variáveis necessárias.</p></li></ol></li><li><p><strong>Configurando o Elasticsearch</strong></p><ol><li><p>Crie um cluster Elasticsearch (localmente ou na nuvem).</p></li><li><p>Instale o cliente oficial do Elasticsearch.</p></li><li><p>Garanta que as variáveis de ambiente estejam acessíveis.</p></li><li><p>Estabelecer conexão com o cliente.</p></li></ol></li><li><p><strong>Ingestão em massa de dados da NBA no Elasticsearch</strong></p><ol><li><p>Crie um índice com os mapeamentos apropriados para habilitar agregações.</p></li><li><p>Importar em massa estatísticas de jogo de jogadores de um arquivo CSV para um índice do Elasticsearch.</p></li></ol></li><li><p><strong>Definir agregações do Elasticsearch</strong></p><ol><li><p>Consulta para calcular as médias históricas contra um adversário específico.</p></li><li><p>Consulta para calcular as médias da temporada contra um adversário específico.</p></li></ol></li><li><p><strong>Arquivo utilitário de comparação de jogadores</strong></p><ol><li><p>Consolida funções auxiliares e agregações do Elasticsearch.</p></li></ol></li><li><p><strong>Construindo o agente</strong></p><ol><li><p>Adicione a definição do agente e o prompt do sistema.</p></li><li><p>Instale o Zod e defina as ferramentas.</p></li><li><p>Adicionar configuração de middleware para lidar com CORS.</p></li></ol></li><li><p><strong>Integrando o frontend</strong></p><ol><li><p>Utilizando o useChat do AI-SDK para interagir com o agente.</p></li><li><p>Crie a interface do usuário para manter conversas formatadas adequadamente.</p></li></ol></li><li><p><strong>Executando o aplicativo</strong></p><ol><li><p>Inicie tanto o backend (servidor Mastra) quanto o frontend (aplicativo React).</p></li><li><p>Exemplos de consultas e uso.</p></li></ol></li><li><p><strong>O que vem a seguir: tornar o agente mais inteligente.</strong></p><ol><li><p>Adicionando recursos de busca semântica para possibilitar recomendações mais relevantes.</p></li><li><p>Habilite consultas dinâmicas movendo a lógica de busca para o servidor Elasticsearch MCP (Model Context Protocol).</p></li></ol></li></ol><h3><strong>Pré-requisitos</strong></h3><ul><li><p><strong>Node.js e npm</strong>: Tanto o backend quanto o frontend são executados em Node. Certifique-se de ter o Node 18+ e o npm v9+ instalados (que já vêm incluídos no Node 18+).</p></li><li><p><strong>Cluster Elasticsearch:</strong> Um cluster Elasticsearch ativo, seja localmente ou na nuvem.</p></li><li><p><strong>Chave da API da OpenAI</strong>: Gere uma na página de chaves da API no <a href="https://platform.openai.com/api-keys">portal de desenvolvedores da OpenAI</a>.</p></li></ul><p></p><h3><strong>Estrutura do projeto</strong></h3><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt749baa120552e4ab/6a17f1dd1d1b83bfe993e54a/1c0bde11ad0eead523a95e03b9b905aa776e3fd1-1420x934.png" alt="" /><h4><strong>Etapa 1: Estruturando o projeto</strong></h4><ol><li><p>Primeiro, crie o diretório nba-ai-assistant-js e navegue até ele usando: </p></li></ol>mkdir nba-ai-assistant-js &amp;&amp; cd nba-ai-assistant-js<p><strong>Backend:</strong></p><ol><li><p>Utilize a ferramenta de criação do Mastra com o comando: </p></li></ol>npx create-mastra@latest<p>2. Você deverá receber algumas mensagens no seu terminal. Para a primeira, vamos nomear o backend do projeto:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt65abf68fe588e968/6a17f1de63baff2814741d5b/de2725031ed6837db99a979efcdd0ece1e197dbb-608x84.png" alt="" /><p>3. Em seguida, manteremos a estrutura padrão para armazenar os arquivos Mastra, então insira <code>src/</code>.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt89bd829fcf0ae6b9/6a17f1e04b055dd30e432302/88919d9ff1852126395e1fcd700ecb1b59aac63c-866x116.png" alt="" /><p>4. Em seguida, escolheremos a OpenAI como nosso provedor padrão de LLM.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltfd167cc77a40b9a8/6a17f1e11480099e29b48863/2328761e769f3ded134e5a21e8a0bf8f41e88f68-404x210.png" alt="" /><p>5. Por fim, será solicitada a sua chave de API da OpenAI. Por agora, vamos escolher a opção de ignorar e fornecer isso mais tarde em um arquivo<code> .env</code> .</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt12654151ed495370/6a17f1e22f4a5c0f84fa89f9/0662de9bd28758e377e4c63df8d08b479068ce63-444x120.png" alt="" /><p><strong>Front-end:</strong></p><ol><li><p>Volte ao diretório raiz e execute a <a href="https://vite.dev/guide/">ferramenta de criação do Vite</a> usando este comando: <code>npm create vite@latest frontend -- --template react</code></p></li></ol><p>Isso deverá criar um aplicativo React leve chamado <code>frontend</code> com um modelo específico para React.</p><p>Se tudo correr bem, dentro do diretório do seu projeto, você deverá ver um diretório backend que contém o código Mastra e um diretório <code>frontend</code> com seu aplicativo React.</p><p></p><h4><strong>Etapa 2: Configurando as variáveis de ambiente</strong></h4><ol><li><p>Para gerenciar chaves sensíveis, usaremos o pacote <code>dotenv</code> para carregar nossas variáveis de ambiente do arquivo .env. arquivo. Navegue até o diretório backend e instale <code>dotenv</code>:</p></li></ol>cd backend
npm install dotenv --save<p>2. No diretório backend, um arquivo example.env é fornecido com as variáveis apropriadas para preenchimento. Se você criar o seu próprio, certifique-se de incluir as seguintes variáveis:</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: Certifique-se de que este arquivo seja excluído do seu controle de versão adicionando <code>.env</code> a <code>.gitignore</code>.</p><h4><strong>Etapa 3: Configurando o Elasticsearch</strong></h4><p>Primeiro, você precisa de um cluster Elasticsearch ativo. Existem duas opções:</p><ul><li><p><strong>Opção A: Usar o Elasticsearch Cloud</strong></p><ul><li><p>Inscreva-se no <a href="https://cloud.elastic.co/registration">Elastic Cloud.</a></p></li><li><p>Criar uma nova implantação</p></li><li><p>Obtenha o URL do seu endpoint e a chave da API (codificada).</p></li></ul></li><li><p><strong>Opção B: Executar o Elasticsearch localmente</strong></p><ul><li><p>Instale e execute o Elasticsearch localmente.</p></li><li><p>Use http://localhost:9200 como seu endpoint.</p></li><li><p>Gere uma chave de API</p></li></ul></li></ul><p></p><p><strong>Instalando o cliente Elasticsearch no servidor:</strong></p><ol><li><p>Primeiro, instale o cliente oficial do Elasticsearch no diretório do seu backend:</p></li></ol>npm install @elastic/elasticsearch<p>2. Em seguida, crie um diretório chamado lib para armazenar funções reutilizáveis e navegue até ele:</p>mkdir lib &amp;&amp; cd lib<p>3. Dentro da pasta, crie um novo arquivo chamado <a href="https://github.com/jdarmada/nba-ai-assistant-js/blob/main/backend/lib/elasticClient.js">elasticClient.js</a>. Este arquivo inicializará o cliente Elasticsearch e o disponibilizará para uso em todo o seu projeto.</p><p>4. Como estamos usando módulos ECMAScript (ESM), os nomes de arquivo __dirname and __não estão disponíveis. Para garantir que suas variáveis de ambiente sejam carregadas corretamente a partir do arquivo .env No arquivo localizado na pasta backend, adicione esta configuração ao início do seu arquivo:</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. Agora, inicialize o cliente Elasticsearch usando suas variáveis de ambiente e verifique a conexão:</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>Agora, podemos importar essa instância de cliente para qualquer arquivo que precise interagir com o seu cluster Elasticsearch.</p><p></p><h4><strong>Etapa 4: Ingestão em massa de dados da NBA no Elasticsearch</strong></h4><p><strong>Conjunto de dados:</strong></p><p>Para este projeto, utilizaremos como referência os conjuntos de dados disponíveis no diretório <a href="https://github.com/jdarmada/nba-ai-assistant-js/tree/main/backend">backend/data</a> do repositório. Nosso assistente da NBA usará esses dados como base de conhecimento para realizar comparações estatísticas e gerar recomendações.</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> - Estatísticas de jogo de jogadores (por exemplo, pontos, rebotes, roubos de bola, etc., por jogo, por jogador, ao longo de toda a sua carreira na NBA). Usaremos esse conjunto de dados para realizar agregações. (Observação: estes são dados fictícios, pré-gerados para fins de demonstração e não provenientes de fontes oficiais da NBA.)</p></li><li><p><a href="https://github.com/jdarmada/nba-ai-assistant-js/blob/main/backend/data/playerAndTeamInfo.js">playerAndTeamInfo.js</a> - Substitui os metadados de jogadores e equipes que normalmente seriam fornecidos por uma chamada de API, permitindo que o agente associe nomes de jogadores e equipes a IDs. Como estamos usando dados de exemplo, não queremos a sobrecarga de buscar dados em uma API externa, então definimos alguns valores fixos que o agente pode referenciar.</p></li></ul><p></p><p><strong>Implementação:</strong></p><ol><li><p>No diretório <code>backend/lib</code> , crie um arquivo chamado <a href="https://github.com/jdarmada/nba-ai-assistant-js/blob/main/backend/lib/playerDataIngestion.js">playerDataIngestion.js</a>.</p></li><li><p>Configure as importações, resolva o caminho do arquivo CSV e configure a análise sintática. Novamente, como estamos usando ESM, precisamos reconstruir <code>__dirname</code> para resolver o caminho para o CSV de amostra. Além disso, importaremos <a href="http://node.js/">o Node.js.</a> módulos integrados, <code>fs</code> e <code>readline</code>, para analisar o arquivo CSV fornecido linha por linha.</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>Isso prepara você para ler e analisar o CSV de forma eficiente quando chegarmos à etapa de ingestão em massa.</p><p>3. Crie um índice com o mapeamento apropriado. Embora o Elasticsearch possa inferir automaticamente os tipos de campo com <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/dynamic">mapeamento dinâmico</a>, queremos ser explícitos aqui para que cada estatística seja tratada como um campo numérico. Isso é importante porque usaremos esses campos para agregações mais tarde. Também queremos usar o tipo <code>float </code>para estatísticas como pontos, rebotes, etc., para garantir que incluamos valores decimais. Finalmente, queremos adicionar a propriedade de mapeamento <code>dynamic: 'strict'</code> para que o Elasticsearch não mapeie dinamicamente campos não reconhecidos. 
</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. Adicione a função para ingerir em massa os dados CSV no seu índice Elasticsearch. Dentro do bloco de código, omitimos a linha de cabeçalho. Em seguida, separe cada item da linha por vírgula e insira-os no objeto do documento. Esta etapa também os limpa e garante que sejam do tipo correto. Em seguida, inserimos os documentos na matriz bulkBody juntamente com as informações do índice, que servirão como carga útil para a ingestão em massa no 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. Então, podemos usar <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-bulk">a API Bulk</a> do Elasticsearch com <code>elasticClient.bulk()</code> para ingerir vários documentos em uma única solicitação. O tratamento de erros abaixo está estruturado para fornecer uma contagem de quantos documentos não foram ingeridos e quantos foram ingeridos com sucesso.</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. Execute a função <code>main()</code> abaixo para executar sequencialmente as funções <code>createIndex()</code> e <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>Se você vir um registro no console indicando que a ingestão em massa foi bem-sucedida, faça uma verificação rápida no seu índice do Elasticsearch para confirmar se os documentos foram realmente ingeridos com sucesso.</p><h4><strong>Etapa 5: Definindo e consolidando as agregações do Elasticsearch</strong></h4><p>Essas serão as principais funções que serão utilizadas quando definirmos as ferramentas para o Agente de IA, a fim de comparar as estatísticas dos jogadores entre si.</p><p>1. Navegue até o diretório <code>backend/lib</code> e crie um arquivo chamado <a href="https://github.com/jdarmada/nba-ai-assistant-js/blob/main/backend/lib/elasticAggs.js">elasticAggs.js</a>.</p><p>2. Adicione a consulta abaixo para calcular as médias históricas de um jogador contra um adversário específico. Esta consulta usa um <a href="https://www.elastic.co/search-labs/tutorials/search-tutorial/full-text-search/filters">filtro</a> <code>bool</code> com 2 condições: uma que corresponde <code>player_id</code> e outra que corresponde a <code>opponent_team_id</code>, para recuperar apenas os jogos relevantes. Não precisamos retornar nenhum documento, só nos interessam as agregações, então definimos <code>size:0</code>. No bloco <code>aggs</code> , executamos várias <a href="https://www.elastic.co/docs/explore-analyze/query-filter/aggregations">agregações</a> de métricas em paralelo em campos como <code>points, rebounds, assists, steals, blocks</code> e <code>fg_percentage</code> para calcular seus valores médios. Os cálculos dos LLMs podem ser inconsistentes, e essa solução transfere esse processo para o Elasticsearch, garantindo que nosso assistente de IA da NBA tenha acesso a dados 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 as médias da temporada de um jogador contra um adversário específico, usaremos praticamente a mesma consulta que a consulta histórica. A única diferença nesta consulta é que o filtro <code>bool</code> tem uma condição adicional para <code>game_date</code>. O campo <code>game_date</code> tem que estar dentro do intervalo da temporada atual da NBA. Neste caso, o intervalo está entre <code>2024-10-01</code> e <code>2025-06-30</code>. Essa condição adicional abaixo garante que as agregações subsequentes isolarão apenas os jogos desta temporada.
</p>        {
                            range: {
                    //Range for this season, change to match current season
                                game_date: {
                                    gte: '2024-10-01',
                                    lte: '2025-06-30',
                                },
                            },
<h4><strong>Etapa 6: Ferramenta de comparação de jogadores</strong></h4><p>Para manter nosso código modular e de fácil manutenção, criaremos um arquivo utilitário que consolida funções auxiliares de metadados e agregações do Elasticsearch. Isso alimentará a principal ferramenta usada pelo agente. Mais sobre isso adiante:</p><p>1. Crie um novo arquivo <a href="https://github.com/jdarmada/nba-ai-assistant-js/blob/main/backend/lib/comparePlayers.js">comparePlayers.js</a> no diretório <code>backend/lib</code> .</p><p>2. Adicione a função abaixo para consolidar os auxiliares de metadados e a lógica de agregação do Elasticsearch em uma única função que alimenta a ferramenta principal usada pelo 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>Etapa 7: Construindo o agente</strong></h4><p>Agora que você criou a estrutura básica do frontend e do backend, importou os dados dos jogos da NBA e estabeleceu uma conexão com o Elasticsearch, podemos começar a juntar todas as peças para construir o agente.</p><p><strong>Definindo o agente</strong></p><p>1. Navegue até o arquivo <a href="https://github.com/jdarmada/nba-ai-assistant-js/blob/main/backend/src/mastra/agents/index.ts">index.ts</a> dentro do diretório <code>backend/src/mastra/agents</code> e adicione a definição do agente. Você pode especificar campos como:</p><ul><li><p><strong>Nome:</strong> Dê ao seu agente um nome que será usado como referência quando ele for chamado na interface.</p></li><li><p><strong>Instruções/mensagem do sistema: </strong>Uma mensagem do sistema fornece ao LLM o contexto inicial e as regras a seguir durante a interação. É semelhante à mensagem que os usuários enviam pelo chat, mas esta é exibida antes de qualquer interação do usuário. Novamente, isso irá variar dependendo do modelo que você escolher.</p></li><li><p><strong>Modelo:</strong> Qual modelo de aprendizagem de linguagem (LLM) usar (o Mastra suporta modelos OpenAI, antrópicos, locais, etc.).</p></li><li><p><strong>Ferramentas:</strong> Uma lista de funções de ferramentas que o agente pode chamar.</p></li><li><p><strong>Memória:</strong> (Opcional) se quisermos que o agente se lembre do histórico da conversa, etc. Para simplificar, podemos começar sem memória persistente, embora o Mastra a suporte.</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>
Ferramentas de definição</strong></p><ol><li><p>Navegue até o arquivo <a href="https://github.com/jdarmada/nba-ai-assistant-js/blob/main/backend/src/mastra/tools/index.ts">index.ts</a> dentro do diretório <code>backend/src/mastra/tools</code> .</p></li><li><p>Instale o Zod usando o comando:</p></li></ol>npm install zod<p>3. Adicionar definições de ferramentas. Observe que importamos a função dentro do arquivo <code>comparePlayers.js</code> como a função principal que o agente usará ao chamar esta ferramenta. Usando a função <code>createTool()</code> do Mastra, vamos registrar nosso <code>playerComparisonTool</code>. Os campos incluem:</p><ul><li><p><code>id</code>Esta é uma descrição em linguagem natural para ajudar o agente a entender o que a ferramenta faz.</p></li><li><p><code>input schema</code>Para definir o formato da entrada para a ferramenta, o Mastra utiliza o esquema <a href="https://zod.dev/">Zod</a> , que é uma biblioteca de validação de esquemas TypeScript. Zod ajuda garantindo que o agente insira dados estruturados corretamente e impede que a ferramenta seja executada caso a estrutura de entrada não corresponda.</p></li><li><p><code>description</code>Esta é uma descrição em linguagem natural para ajudar o agente a entender quando ligar e usar a ferramenta.</p></li><li><p><code>execute</code>A lógica que é executada quando a ferramenta é chamada. No nosso caso, estamos usando uma função auxiliar importada para retornar estatísticas de desempenho.</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>Adicionando middleware para lidar com CORS</strong></p><p>Adicione um middleware no servidor Mastra para lidar com <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS">CORS</a>. Dizem que existem três coisas na vida que você não pode evitar: a morte, os impostos e, para desenvolvedores web, o CORS. Em resumo, o Compartilhamento de Recursos de Origem Cruzada (CORS) é um recurso de segurança do navegador que impede que o frontend faça solicitações a um backend executado em um domínio ou porta diferente. Embora executemos tanto o backend quanto o frontend em localhost, eles usam portas diferentes, acionando a política CORS. Precisamos adicionar o middleware especificado na <a href="https://mastra.ai/en/docs/server-db/middleware">documentação do Mastra</a> para que nosso backend permita essas solicitações do frontend.</p><p>1. Navegue até o arquivo <a href="https://github.com/jdarmada/nba-ai-assistant-js/blob/main/backend/src/mastra/index.ts">index.ts</a> dentro do diretório <code>backend/src/mastra</code> e adicione a configuração para CORS:</p><ul><li><p><code>origin: ['http://localhost:5173']</code></p><ul><li><p>Permite solicitações somente deste endereço (endereço padrão do Vite)</p></li></ul></li><li><p><code>allowMethods: ["GET", "POST"]</code></p><ul><li><p>Métodos HTTP permitidos. Na maioria das vezes, será utilizado o método 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>Essas configurações definem quais cabeçalhos personalizados podem ser usados nas solicitações.</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>Etapa 8: Integrando o frontend</strong></h4><p>Este componente React fornece uma interface de chat simples que se conecta ao agente de IA Mastra usando o 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>. Também usaremos esse recurso para exibir o uso de tokens, chamadas de ferramentas e para renderizar a conversa. No prompt do sistema acima, também pedimos ao agente para exibir a resposta em markdown, então usaremos <code>react-markdown</code> para formatar a resposta corretamente.</p><p></p><p>1. No diretório frontend, instale o pacote @ai-sdk/react para usar o gancho useChat().</p>npm install @ai-sdk/react<p>2. Ainda no mesmo diretório, instale o React Markdown para que possamos formatar corretamente a resposta gerada pelo agente.</p>npm install react-markdown<p>3. Implemente <code>useChat()</code>. Este gancho gerenciará a interação entre seu frontend e o backend do seu agente de IA. Ele gerencia o estado das mensagens, a entrada do usuário, o status e fornece ganchos de ciclo de vida para fins de observabilidade. As opções que passamos incluem:</p><ul><li><p><code>api:</code> Isso define o ponto final do seu agente Mastra AI. A porta padrão é a 4111 e também queremos adicionar a rota que suporta respostas em fluxo contínuo.</p></li><li><p><code>onToolCall</code>Este comando é executado sempre que o agente chama uma ferramenta; estamos usando-o para rastrear quais ferramentas nosso agente está chamando.</p></li><li><p><code>onFinish</code>Esta ação é executada depois que o agente conclui uma resposta completa. Mesmo que tenhamos habilitado o streaming, <code>onFinish</code> ainda será executado após o recebimento da mensagem completa e não após cada parte. Aqui, estamos usando isso para rastrear o uso de nossos tokens. Isso pode ser útil para monitorar e otimizar os custos do LLM.</p></li></ul><p>4. Finalmente, acesse o componente <a href="https://github.com/jdarmada/nba-ai-assistant-js/blob/main/frontend/components/ChatUI.jsx">ChatUI.jsx</a> no diretório <code>frontend/components</code> para criar a interface do usuário para manter nossa conversa. Em seguida, envolva a resposta em um componente <code>ReactMarkdown</code> para formatar corretamente a resposta do 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>Etapa 9: Executando o aplicativo</strong></h4><p>Parabéns! Agora você está pronto para executar o aplicativo. Siga estes passos para iniciar tanto o backend quanto o frontend.</p><ol><li><p>Em uma janela de terminal, partindo do diretório raiz, navegue até o diretório de backend e inicie o servidor Mastra:</p></li></ol>cd backend

npm run dev<p>2. Em outra janela do terminal, partindo do diretório raiz, navegue até o diretório frontend e inicie o aplicativo React:</p><p></p>cd frontend

npm run dev<p></p><p>3. Acesse seu navegador e navegue até:</p><p></p><p><a href="http://localhost:5173/">http://localhost:5173</a></p><p></p><p>Você deverá conseguir visualizar a interface de bate-papo. Experimente estas sugestões:</p><ul><li><p>"Compare LeBron James e Stephen Curry"</p></li><li><p>"Quem devo escolher entre Jayson Tatum e Luka Doncic?"</p></li></ul><p></p><h3><strong>O que vem a seguir: tornar o agente mais inteligente.</strong></h3><p>Para tornar o assistente mais proativo e as recomendações mais relevantes, adicionarei algumas melhorias importantes na próxima versão.</p><p></p><p><strong>Busca semântica para notícias da NBA</strong></p><p>Existem inúmeros fatores que podem afetar o desempenho do jogador, muitos dos quais não aparecem nas estatísticas brutas. Informações como relatórios de lesões, alterações na escalação ou até mesmo análises pós-jogo só podem ser encontradas em artigos de notícias. Para capturar esse contexto adicional, adicionarei recursos de busca semântica para que o agente possa recuperar artigos relevantes da NBA e incorporar essa narrativa em suas recomendações.</p><p></p><p><strong>Pesquisa dinâmica com o servidor Elasticsearch MCP</strong></p><p>O MCP (Model Context Protocol) está rapidamente se tornando o padrão para a forma como os agentes se conectam às fontes de dados. Vou migrar a lógica de busca para o servidor Elasticsearch MCP, o que permite que o agente construa consultas dinamicamente em vez de depender de funções de busca predefinidas que fornecemos. Isso nos permite usar fluxos de trabalho em linguagem mais natural e reduz a necessidade de escrever manualmente cada consulta de pesquisa. Saiba mais sobre o servidor Elasticsearch MCP e o estado atual do ecossistema <a href="https://www.elastic.co/search-labs/blog/mcp-current-state">aqui</a>.</p><p></p><p>Essas mudanças já estão em andamento, fique ligado!</p><h3><strong>Conclusão</strong></h3><p></p><p>Neste blog, criamos um assistente RAG interativo que fornece recomendações personalizadas para o seu time de basquete de fantasia usando JavaScript, Mastra e Elasticsearch. Nós abordamos os seguintes tópicos:</p><ul><li><p><strong>Fundamentos do RAG agético</strong> e como a combinação da autonomia de um agente de IA com as ferramentas para usar o RAG de forma eficaz pode levar a agentes mais dinâmicos e com nuances.</p></li><li><p><strong>Elasticsearch </strong>e como seus recursos de armazenamento de dados e poderosas agregações nativas o tornam um excelente parceiro como base de conhecimento para um mestrado em Direito (LLM).</p></li><li><p><strong>O framework Mastra </strong>e como ele simplifica a criação desses agentes para desenvolvedores no ecossistema JavaScript.</p></li></ul><p>Seja você um fanático por basquete, esteja explorando como construir agentes de IA, ou ambos como eu, espero que este blog tenha lhe dado algumas bases para começar. O repositório completo está disponível no <a href="https://github.com/jdarmada/nba-ai-assistant-js">GitHub</a>. Sinta-se à vontade para cloná-lo e fazer alterações. Agora, vá ganhar essa liga de fantasia!</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[IA 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 em JavaScript da maneira correta, parte II]]></title>
    <description><![CDATA[Conheça as práticas recomendadas de produção e como executar o cliente Elasticsearch Node.js em ambientes serverless para reduzir erros na codificação. ]]></description>
    <content:encoded><![CDATA[<p>Esta é a segunda parte da nossa série sobre Elasticsearch em JavaScript. Na<a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i"> primeira parte,</a> aprendemos como configurar nosso ambiente corretamente, configurar o cliente Node.js, indexar dados e realizar buscas. Nesta segunda parte, aprenderemos como implementar as melhores práticas de produção e executar o cliente Elasticsearch <a href="http://node.js">Node.js</a> em ambientes Serverless.</p><p>Analisaremos:</p><ul><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-ii#production-best-practices">Melhores práticas de produção</a></p><ul><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-ii#error-handling">Tratamento de erros</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-ii#testing">Teste</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">Ambientes sem 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">Executando o cliente no 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">Executando o cliente em um ambiente de função como serviço.</a></p></li></ul></li></ul><p><em>Você pode conferir o código-fonte com os exemplos </em><a href="https://github.com/Delacrobix/JS-client-best-practices_article"><em><strong>aqui</strong></em></a><em><strong>.</strong></em></p><h2>Melhores práticas de produção</h2><h3>Tratamento de erros no Elasticsearch</h3><p>Uma funcionalidade útil do cliente Elasticsearch em Node.js é que ele expõe objetos para os possíveis erros no Elasticsearch, permitindo que você os valide e trate de diferentes maneiras.</p><p>Para <a href="https://www.elastic.co/docs/reference/elasticsearch/clients/javascript/connecting#client-error-handling">ver todos</a>, execute o seguinte comando: </p>const { errors } = require('@elastic/elasticsearch')
console.log(errors)<p>Vamos voltar ao exemplo de pesquisa e tratar de alguns dos possíveis erros:</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> em particular, ocorrerá quando a resposta for <code>4xx</code> ou <code>5xx</code>, o que significa que a solicitação está incorreta ou o servidor não está disponível.</p><p>Podemos testar esse tipo de erro gerando consultas incorretas, como tentar <strong>fazer uma consulta de termo em um campo do tipo texto:</strong></p><p>Erro padrão:</p> {
    "success": false,
    "results": null,
    "error": "parsing_exception\n\tRoot causes:\n\t\tparsing_exception: [terms] query does not support [visit_details]"
}<p>Erro personalizado: </p>{
    "erroStatus": 400,
    "success": false,
    "results": null,
    "error": "Response error!, query malformed or server down; contact the administrator!"
}<p>Também podemos capturar e lidar com cada tipo de erro de uma determinada maneira. Por exemplo, podemos adicionar lógica de repetição em um <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>Teste</h3><p>Os testes são essenciais para garantir a estabilidade do aplicativo. Para testar o código de forma isolada do Elasticsearch, podemos usar a biblioteca <a href="https://github.com/elastic/elasticsearch-js-mock">elasticsearch-js-mock</a> ao criar nosso cluster.</p><p>Esta biblioteca permite instanciar um cliente muito semelhante ao real, mas que responderá à nossa configuração substituindo apenas a camada HTTP do cliente por uma camada simulada, mantendo o restante igual ao original.</p><p>Vamos instalar a biblioteca mocks e <a href="https://github.com/avajs/ava">o AVA</a> para testes automatizados.</p><p><code>npm install @elastic/elasticsearch-mock</code></p><p><code>npm install --save-dev ava</code></p><p>Vamos configurar o arquivo <code>package.json</code> para executar os testes. Certifique-se de que esteja assim:</p>"type": "module",
	"scripts": {
		"test": "ava"
	},
	"devDependencies": {
		"ava": "^5.0.0"
	}<p>Vamos agora criar um arquivo <code>test.js</code> e instalar nosso cliente de simulação:</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>Agora, adicione uma simulação para pesquisa 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>Agora podemos criar um teste para o nosso código, garantindo que a parte do Elasticsearch sempre retorne os mesmos 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>Vamos executar os testes.</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>Pronto! A partir de agora, podemos testar nosso aplicativo focando 100% no código e não em fatores externos.</p><h2>Ambientes sem servidor</h2><h3>Como executar o cliente no Elastic Serverless</h3><p>Já abordamos a execução do Elasticsearch na nuvem ou em infraestrutura local; no entanto, o cliente Node.js também oferece suporte a conexões com o <a href="https://www.elastic.co/guide/en/serverless/current/intro.html">Elastic Cloud Serverless</a>.</p><p>O Elastic Cloud Serverless permite que você crie um projeto onde não precisa se preocupar com a infraestrutura, já que a Elastic cuida disso internamente, e você só precisa se preocupar com os dados que deseja indexar e por quanto tempo deseja ter acesso a eles.</p><p>Do ponto de vista da utilização, o Serverless separa o processamento do armazenamento, proporcionando recursos de escalonamento automático tanto para <a href="https://www.elastic.co/search-labs/blog/elasticsearch-serverless-tier-autoscaling">pesquisa</a> quanto para <a href="https://www.elastic.co/search-labs/blog/elasticsearch-ingest-autoscaling">indexação</a>. Isso permite que você cultive apenas os recursos de que realmente precisa.</p><p>O cliente realiza as seguintes adaptações para se conectar ao Serverless:</p><ul><li><p>Desativa a detecção de pacotes e ignora quaisquer opções relacionadas a ela.</p></li><li><p>Ignora todos os nós passados na configuração, exceto o primeiro, e ignora quaisquer opções de filtragem e seleção de nós.</p></li><li><p>Habilita a compressão e o método `TLSv1_2_method` (igual à configuração para o Elastic Cloud).</p></li><li><p>Adiciona um cabeçalho HTTP `elastic-api-version` a todas as requisições.</p></li><li><p>Utiliza `CloudConnectionPool` por padrão em vez de `WeightedConnectionPool`.</p></li><li><p>Desativa os cabeçalhos `content-type` e `accept` fornecidos pelo fornecedor, em favor dos tipos MIME padrão.</p></li></ul><p>Para conectar seu projeto sem servidor, você precisa usar o 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>Como executar o cliente em um ambiente de função como serviço</h3><p>No exemplo, usamos um servidor Node.js, mas você também pode se conectar usando um ambiente de função como serviço com funções 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>Outro exemplo é conectar-se a serviços como o Vercel, que também é serverless. Você pode conferir este <a href="https://github.com/elastic/elasticsearch-js/blob/main/docs/examples/proxy/README.md">exemplo completo</a> de como fazer isso, mas a parte mais relevante do <a href="https://github.com/elastic/elasticsearch-js/blob/main/docs/examples/proxy/api/search.js">endpoint de pesquisa</a> se parece com isto:</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 na pasta /api e é executado a partir do lado do servidor, de forma que o cliente só tenha controle sobre o parâmetro “texto” que corresponde ao termo de pesquisa.</p><p>A implicação de usar a função como serviço é que, ao contrário de um servidor que funciona 24 horas por dia, 7 dias por semana, as funções apenas ativam a máquina que executa a função e, assim que ela termina, a máquina entra em modo de repouso para consumir menos recursos.</p><p>Essa configuração pode ser conveniente se o aplicativo não receber muitas solicitações; caso contrário, os custos podem ser elevados. Você também precisa levar em consideração o <a href="https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtime-environment.html">ciclo de vida das funções</a> e os tempos de execução (que, em alguns casos, podem ser de apenas alguns segundos).</p><h2>Conclusão</h2><p>Neste artigo, aprendemos como lidar com erros, o que é crucial em ambientes de produção. Também abordamos os testes da nossa aplicação enquanto simulávamos o serviço Elasticsearch, o que proporciona testes confiáveis independentemente do estado do cluster e nos permite focar no nosso código.</p><p>Por fim, demonstramos como criar uma infraestrutura totalmente sem servidor, provisionando tanto o Elastic Cloud Serverless quanto um aplicativo 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[Noções básicas]]></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 em JavaScript da maneira correta, parte I]]></title>
    <description><![CDATA[Explicando como criar um backend Elasticsearch pronto para produção em JavaScript.  

Saiba como usar o Elasticsearch com JavaScript para criar um servidor com diferentes endpoints de busca para consultar documentos do Elasticsearch, seguindo as melhores práticas de cliente/servidor.]]></description>
    <content:encoded><![CDATA[<p>Este é o primeiro artigo de uma série que aborda como usar o Elasticsearch com JavaScript. Nesta série, você aprenderá o básico de como usar o Elasticsearch em um ambiente JavaScript e revisará os recursos mais relevantes e as melhores práticas para criar um aplicativo de busca. Ao final, você saberá tudo o que precisa para executar o Elasticsearch usando JavaScript.</p><p>Nesta primeira parte, vamos analisar:</p><ul><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i#environment">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 ou serverless?</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i#connecting-the-client">Conectando o 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">Documentos de indexação</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">Mapeamentos semânticos</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i#bulk-helper">Auxiliar em massa</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">Dados de pesquisa</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 Lexical</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>Você pode conferir o código-fonte com os exemplos </em><a href="https://github.com/Delacrobix/JS-client-best-practices_article"><em><strong>aqui</strong></em></a><em><strong>.</strong></em></p><h3>O que é o cliente Elasticsearch para Node.js?</h3><p>O <a href="https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/index.html">cliente Elasticsearch para Node.js</a> é uma biblioteca JavaScript que converte as chamadas HTTP REST da API do Elasticsearch em código JavaScript. Isso facilita o manuseio e permite o uso de ferramentas auxiliares que simplificam tarefas como a indexação de documentos em lotes.</p><h2>Ambiente</h2><h3>Frontend, backend ou serverless?</h3><p>Para criar nosso aplicativo de busca usando o cliente JavaScript, precisamos de pelo menos dois componentes: um cluster Elasticsearch e um ambiente de execução JavaScript para executar o cliente.</p><p>O cliente JavaScript é compatível com todas as soluções Elasticsearch (Cloud, on-premise e Serverless), e não há grandes diferenças entre elas, já que o cliente lida com todas as variações internamente, então você não precisa se preocupar com qual usar.</p><p>O ambiente de execução JavaScript, no entanto, deve ser executado a partir do <strong>servidor</strong> e <strong>não diretamente do navegador.</strong></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltd3ec469c83e3a71a/6a17e3d5445de91da44d00b6/92ce6cfd923c8008fa44f617a58193642d9d5879-661x410.png" alt="Elasticsearch em ambiente JavaScript." /><p>Isso ocorre porque, ao acessar o Elasticsearch pelo navegador, o usuário pode obter informações confidenciais, como a chave da API do cluster, o host ou a própria consulta. A Elasticsearch recomenda <strong>nunca expor o cluster diretamente à internet </strong>e usar uma camada intermediária que abstraia todas essas informações, de forma que o usuário possa ver apenas os parâmetros. Você pode ler mais sobre este tópico <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/es-security-principles.html#security-protect-cluster-traffic">aqui</a>.</p><p>Sugerimos usar um esquema como este:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt4d7f215f2e70230a/6a17e3d6fbc5f83de6491a13/a08769f08ec73fe57bf2e961cfdfbb1cdd57919d-972x429.png" alt="Configurando o cliente Node.js do Elasticsearch." /><p>Nesse caso, o cliente envia apenas os termos de pesquisa e uma chave de autenticação para o seu servidor, enquanto o seu servidor mantém o controle total da consulta e da comunicação com o Elasticsearch.</p><h3>Conectando o cliente</h3><p>Comece criando uma chave de API seguindo <a href="https://www.elastic.co/search-labs/tutorials/install-elasticsearch/elastic-cloud">estes passos</a>.</p><p>Seguindo o exemplo anterior, criaremos um servidor Express simples e nos conectaremos a ele usando um cliente de um servidor Node.js.</p><p>Vamos inicializar o projeto com o NPM e instalar o cliente Elasticsearch e <a href="https://expressjs.com/">o Express.</a> Esta última é uma biblioteca para iniciar servidores em Node.js. Usando o Express, podemos interagir com nosso backend via HTTP.</p><p>Vamos inicializar o projeto:</p><p><code>npm init -y</code></p><p>Instalar dependências:</p><p><code>npm install @elastic/elasticsearch express split2 dotenv</code></p><p>Deixe-me explicar melhor:</p><ul><li><p><a href="https://www.npmjs.com/package/@elastic/elasticsearch"><em><strong>@elastic/elasticsearch</strong></em></a>: É o cliente oficial do Node.js.</p></li><li><p><a href="https://www.npmjs.com/package/express"><em><strong>Express</strong></em></a>: Isso nos permitirá criar um servidor Node.js leve para expor o Elasticsearch.</p></li><li><p><a href="https://www.npmjs.com/package/split2"><em><strong>split2</strong></em></a>: Divide linhas de texto em um fluxo. Útil para processar nossos arquivos ndjson linha por linha.</p></li><li><p><a href="https://www.npmjs.com/package/dotenv"><em><strong>dotenv</strong></em></a>: Permite gerenciar variáveis de ambiente usando um arquivo .env. arquivo</p></li></ul><p>Crie um arquivo .env Abra o arquivo na raiz do projeto e adicione as seguintes linhas:</p>ELASTICSEARCH_ENDPOINT="Your Elasticsearch endpoint"
ELASTICSEARCH_API_KEY="Your Elasticssearch API"<p>Dessa forma, podemos importar essas variáveis usando o pacote <code>dotenv</code> .</p><p>Crie um arquivo <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 um servidor Express.js básico que escuta na porta 3000 e se conecta a um cluster Elasticsearch usando uma chave de API para autenticação. Inclui um endpoint /ping que, quando acessado por meio de uma solicitação GET, consulta o cluster Elasticsearch para obter informações básicas usando o método <code>.info()</code> do cliente Elasticsearch. </p><p>Se a consulta for bem-sucedida, ela retorna as informações do cluster em formato JSON; caso contrário, retorna uma mensagem de erro. O servidor também utiliza o middleware body-parser para lidar com os corpos das requisições JSON.</p><p>Execute o arquivo para iniciar o servidor:</p><p><code>node server.js</code></p><p>A resposta deve ser semelhante a esta:</p>Server running on port 3000<p>E agora, vamos consultar o endpoint <code>/ping</code> para verificar o status do nosso cluster Elasticsearch.</p>curl http://localhost:3000/ping
{
    "success": true,
    "clusterInfo": {
        "name": "instance-0000000000",
        "cluster_name": "61b7e19eec204d59855f5e019acd2689",
        "cluster_uuid": "BIfvfLM0RJWRK_bDCY5ldg",
        "version": {
            "number": "9.0.0",
            "build_flavor": "default",
            "build_type": "docker",
            "build_hash": "112859b85d50de2a7e63f73c8fc70b99eea24291",
            "build_date": "2025-04-08T15:13:46.049795831Z",
            "build_snapshot": false,
            "lucene_version": "10.1.0",
            "minimum_wire_compatibility_version": "8.18.0",
            "minimum_index_compatibility_version": "8.0.0"
        },
        "tagline": "You Know, for Search"
    }
}<h2>Documentos de indexação</h2><p>Uma vez conectados, podemos indexar documentos usando mapeamentos como <a href="https://www.elastic.co/search-labs/blog/semantic-search-simplified-semantic-text">semantic_text</a> para pesquisa semântica e text para consultas de texto completo. Com esses dois tipos de campo, também podemos fazer <a href="https://www.elastic.co/what-is/hybrid-search">buscas híbridas</a>.</p><p>Criaremos um novo arquivo <code>load.js</code> para gerar os mapeamentos e carregar os documentos.</p><h3>Cliente Elasticsearch</h3><p>Primeiro precisamos instanciar e autenticar o 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>Mapeamentos semânticos</h3><p>Criaremos um índice com dados sobre um hospital veterinário. Armazenaremos as informações do dono, do animal de estimação e os detalhes da visita.</p><p>Os dados nos quais desejamos realizar uma busca de texto completo, como nomes e descrições, serão armazenados como texto. Os dados das categorias, como a espécie ou raça do animal, serão armazenados como palavras-chave.</p><p>Além disso, copiaremos os valores de todos os campos para um campo semantic_text para podermos executar também uma pesquisa semântica nessas informações.</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>Auxiliar em massa</h3><p>Outra vantagem do cliente é que podemos usar a <a href="https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/client-helpers.html#bulk-helper">função auxiliar</a> de indexação em lotes. A função auxiliar de processamento em lote nos permite lidar facilmente com aspectos como concorrência, novas tentativas e o que fazer com cada documento que passa pela função, seja com sucesso ou com falha.</p><p>Uma característica interessante dessa ferramenta auxiliar é a possibilidade de trabalhar com fluxos de dados. Essa função permite enviar um arquivo linha por linha, em vez de armazenar o arquivo inteiro na memória e enviá-lo para o Elasticsearch de uma só vez.</p><p>Para enviar os dados para o Elasticsearch, crie um arquivo chamado data.ndjson na raiz do projeto e adicione as informações abaixo (alternativamente, você pode baixar o arquivo com o conjunto de dados <a href="https://github.com/Delacrobix/JS-client-best-practices_article/blob/main/data.ndjson">aqui</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 o split2 para transmitir as linhas do arquivo enquanto o auxiliar de processamento em lote as envia para o 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>O código acima lê um arquivo .ndjson. indexa cada objeto JSON em um índice Elasticsearch especificado usando o método <code>helpers.bulk</code> . Ele transmite o arquivo usando <code>createReadStream</code> e <code>split2</code>, configura metadados de indexação para cada documento e registra quaisquer documentos que não puderem ser processados. Após a conclusão, registra o número de itens indexados com sucesso.</p><p>Alternativamente à função <code>indexData</code> , você pode fazer o upload do arquivo diretamente pela interface do usuário usando o Kibana e usar a <a href="https://www.elastic.co/docs/manage-data/ingest/upload-data-files">interface de upload de arquivos de dados.</a></p><p>Executamos o arquivo para enviar os documentos para o nosso cluster Elasticsearch.</p><p><code>node load.js</code></p>Creating mappings for index vet-visits...
Index created successfully: { acknowledged: true, shards_acknowledged: true, index: 'vet-visits' }
Indexing data from ./data.ndjson into vet-visits...
Bulk indexing completed. Total documents: 10, Failed: 0<h2>Buscando dados no Elasticsearch</h2><p>Voltando ao nosso arquivo <code>server.js</code> , criaremos diferentes endpoints para realizar buscas lexicais, semânticas ou híbridas.</p><p>Em resumo, esses tipos de pesquisa não são mutuamente exclusivos, mas dependerão do tipo de pergunta que você precisa responder.</p><p>Tipo de consulta</p><p>Caso de uso</p><p>Exemplo de pergunta</p><p>Consulta lexical</p><p>As palavras ou radicais presentes na pergunta provavelmente aparecerão nos documentos indexados. Similaridade entre tokens na pergunta e nos documentos.</p><p>Estou procurando uma camiseta esportiva azul.</p><p>Consulta semântica</p><p>É improvável que as palavras da pergunta apareçam nos documentos. Similaridade conceitual entre a pergunta e os documentos.</p><p>Estou procurando roupas para clima frio.</p><p>Busca híbrida</p><p>A questão contém componentes lexicais e/ou semânticos. Similaridade semântica e de tokens entre a pergunta e os documentos.</p><p>Estou procurando um vestido tamanho P para um casamento na praia.</p><p>As partes <em><strong>lexicais </strong></em>da pergunta provavelmente fazem parte de títulos e descrições, ou nomes de categorias, enquanto as partes <em><strong>semânticas </strong></em>são conceitos relacionados a esses campos. <em><strong>"Azul"</strong></em> provavelmente será o nome de uma categoria ou parte de uma descrição, e <em><strong>"casamento na praia"</strong></em> provavelmente não será, mas pode estar semanticamente relacionado a roupas de linho.</p><h3>Consulta lexical (/search/lexic?q=)&lt;query_term&gt;</h3><p>A busca lexical, também chamada de busca de texto completo, significa pesquisar com base na similaridade de tokens; ou seja, após uma análise, os documentos que incluem os tokens da busca serão retornados.</p><p>Você pode conferir nosso tutorial prático de busca lexical <a href="https://www.elastic.co/demo-gallery/lexical-search">aqui</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>Testamos com: <em><strong>corte de unhas</strong></em></p>curl http://localhost:3000/search/lexic?q=nail%20trimming<p>Responder:</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&lt;query_term&gt; (/search/semantic?q=)</h3><p>A busca semântica, diferentemente da busca lexical, encontra resultados que são semelhantes ao significado dos termos de busca por meio de busca vetorial.</p><p>Você pode conferir nosso tutorial prático de busca semântica <a href="https://www.elastic.co/demo-gallery/semantic-search">aqui</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>Fizemos o teste com a seguinte pergunta: <em><strong>Quem fez pedicure?</strong></em></p>curl http://localhost:3000/search/semantic?q=Who%20got%20a%20pedicure?<p>Responder:</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>A busca híbrida permite combinar a busca semântica e a busca lexical, obtendo assim o melhor dos dois mundos: a precisão da busca por token, juntamente com a proximidade de significado da busca 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>Fizemos o teste com a pergunta: “<em><strong>Quem fez pedicure ou tratamento dentário?”</strong></em></p>curl http://localhost:3000/search/hybrid?q=who%20got%20a%20pedicure%20or%20dental%20treatment<p>Resposta.</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>Conclusão</h2><p>Nesta primeira parte da nossa série, explicamos como configurar nosso ambiente e criar um servidor com diferentes endpoints de pesquisa para consultar os documentos do Elasticsearch, seguindo as melhores práticas de cliente/servidor. Confira <a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i">a segunda parte</a> da nossa série, na qual você aprenderá as melhores práticas de produção e como executar o cliente Elasticsearch Node.js em ambientes 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[Noções básicas]]></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>