<?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/cn/search-labs/blog/category/javascript-programming</link>
    </image>
    <link>https://www.elastic.co/cn/search-labs/blog/category/javascript-programming</link>
    <atom:link href="https://www.elastic.co/cn/search-labs/rss/category/javascript-programming.xml" rel="self" type="application/rss+xml"/>
    <language><![CDATA[cn]]></language>
    <lastBuildDate>Mon, 21 Sep 2026 00:51:10 GMT</lastBuildDate>
  <item>
    <title><![CDATA[使用 JavaScript、Mastra 和 Elasticsearch 构建代理 RAG 助手]]></title>
    <description><![CDATA[了解如何在 JavaScript 生态系统中构建人工智能代理]]></description>
    <content:encoded><![CDATA[<p>我是在激烈的高风险梦幻篮球联赛中萌生这个想法的。我想知道<em>我能否建立一个人工智能代理，帮助我在每周的对阵中占据优势？当然可以！</em></p><p>在本篇文章中，我们将探讨如何使用<a href="https://mastra.ai/en/docs">Mastra</a>和一个轻量级 JavaScript 网络应用程序来构建一个代理 RAG 助手，并与其进行交互。通过将该代理连接到 Elasticsearch，我们可以让它访问结构化的球员数据，并能够运行实时统计汇总，从而为您提供基于球员统计数据的推荐。请访问 GitHub<a href="https://github.com/jdarmada/nba-ai-assistant-js.git">软件源</a>，了解如何克隆和运行应用程序；<a href="https://github.com/jdarmada/nba-ai-assistant-js/blob/main/README.md">README</a>提供了相关说明。 </p><p>下面是全部组装好后的样子：</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt63ea3e7a09306fbf/6a17f1d97f6f150e22c09c50/1c73bd1dc1b5fe54f025c7a2b7c322acc9122f3a-1999x1393.png" alt="" /><p>注：本博文以 "<a href="https://www.elastic.co/search-labs/blog/ai-agents-ai-sdk-elasticsearch">使用 AI SDK 和 Elastic 构建 AI 代理</a>"为基础。如果您是第一次接触人工智能代理及其用途，请从这里开始。
</p><h2><strong>结构概述</strong></h2><p>该系统的核心是一个大型语言模型（LLM），它充当了代理的推理引擎（大脑）。它能解释用户输入，决定调用哪些工具，并协调生成相关响应所需的步骤。</p><p>代理本身由 JavaScript 生态系统中的代理框架 Mastra 搭建脚手架。Mastra 将 LLM 与后端基础设施封装在一起，将其作为 API 端点公开，并提供了一个用于定义工具、系统提示和代理行为的接口。</p><p>在前端，我们使用<a href="https://vite.dev/guide/">Vite</a>快速搭建了一个 React 网络应用程序，它提供了一个聊天界面，用于向代理发送查询并接收其回复。</p><p>最后，我们还有 Elasticsearch，它存储了代理可以查询和汇总的球员统计数据和对阵数据。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blte13f09493f217047/6a17f1db1d1b83178d93e546/443bdc00d84ed1dd49e9f9e431e86ca4b0892563-1999x977.png" alt="" /><h2><strong>背景</strong></h2><p>让我们来回顾一下几个基本概念：</p><h3><strong>什么是代理 RAG？</strong></h3><p>人工智能代理可以与其他系统互动，独立运行，并根据其定义的参数执行操作。代理式 RAG 将人工智能代理的自主性与检索增强生成的原则相结合，使 LLM 能够选择调用哪些工具和使用哪些数据作为上下文来生成响应。<a href="https://www.elastic.co/search-labs/blog/retrieval-augmented-generation-rag">点击此处</a>了解有关 RAG 的更多信息。</p><h3><strong>选择框架，为什么要超越 AI-SDK？</strong></h3><p>目前有许多人工智能代理框架，你可能听说过<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>和<a href="https://www.elastic.co/search-labs/blog/build-rag-workflow-langgraph-elasticsearch">LangGraph</a> 等比较流行的框架。这些框架大多有一套共同的功能，包括支持不同的模型、工具使用和内存管理。</p><p>下面是哈里森-蔡斯（LangChain 首席执行官）的框架<a href="https://docs.google.com/spreadsheets/d/1B37VxTBuGLeTSPVWtz7UMsCdtXrqV5hCjWkbHN8tfAo/edit?gid=0#gid=0">比较表</a>。</p><p>让我对 Mastra 产生兴趣的是，它是一个 JavaScript 优先框架，专为全栈开发人员设计，可以轻松地将代理集成到他们的生态系统中。Vercel 的 AI-SDK 也能实现大部分功能，但 Mastra 的优势在于当项目包含更复杂的代理工作流程时。Mastra 增强了 AI-SDK 设置的基本模式，在本项目中，我们将同时使用它们。</p><h3><strong>框架和模型选择考虑因素</strong></h3><p>虽然这些框架可以帮助您快速构建人工智能代理，但也有一些缺点需要考虑。例如，在使用人工智能代理或任何抽象层之外的其他框架时，你会失去一些控制权。如果 LLM 没有正确使用工具，或者做了一些你不希望它做的事情，抽象化就会增加调试难度。不过，在我看来，这种折衷还是值得的，尤其是因为这些框架的发展势头越来越好，而且还在不断迭代。</p><p>同样，这些框架与模型无关，这意味着您可以即插即用不同的模型，但请记住，模型在不同的数据集上训练出来的结果是不同的，反过来，它们给出的响应也是不同的。有些型号甚至不支持工具调用。因此，可以切换和测试不同的型号，看看哪种型号能给您带来最好的响应，但请记住，您很可能需要为每种型号重写系统提示。例如，使用 Llama3.3与 GPT-4o 相比，它需要更多的提示和具体指令才能得到您想要的回应。</p><h3><strong>NBA 梦幻篮球</strong></h3><p>梦幻篮球就是和你的一群朋友组成一个联盟（警告，这可能会影响你们的友谊，这取决于你们的竞争有多激烈），通常会涉及到一些金钱问题。然后，你们每个人起草一支由 10 名球员组成的队伍，每周轮流与另一位朋友的 10 名球员比赛。您的总得分取决于您的每位球员在一周内与对手的对战情况。</p><p>如果您队中有球员受伤、停赛等，会有一份自由球员名单供您选择。这也是梦幻体育中最难思考的地方，因为你只有有限的选择权，而每个人都在不断地寻找最好的球员。</p><p>这正是我们的 NBA AI 助手大显身手的地方，尤其是在您必须迅速决定选择哪位球员的情况下。助手无需手动查找球员在与特定对手比赛时的表现，而是可以快速找到这些数据并比较平均值，从而为您提供明智的建议。</p><p>现在，您已经了解了代理 RAG 和 NBA 梦幻篮球的一些基本知识，让我们来看看它的实际应用。</p><h2><strong>建设项目</strong></h2><p>如果您遇到任何问题或不想从头开始构建，请参考<a href="https://github.com/jdarmada/nba-ai-assistant-js.git">软件仓库</a>。</p><h3><strong>我们的内容</strong></h3><ol><li><p><strong>为项目搭建脚手架：</strong></p><ol><li><p><strong>后端（Mastra）：</strong>使用 npx create mastra@latest 构建后端并定义代理逻辑。</p></li><li><p><strong>前端（Vite + React）：</strong>使用 npm create vite@latest 构建与代理交互的前端聊天界面。</p></li></ol></li><li><p><strong>设置环境变量</strong></p><ol><li><p>安装 dotenv 来管理环境变量。</p></li><li><p>创建 .env文件，并提供所需的变量。</p></li></ol></li><li><p><strong>设置 Elasticsearch</strong></p><ol><li><p>启动 Elasticsearch 集群（本地或云端）。</p></li><li><p>安装官方 Elasticsearch 客户端。</p></li><li><p>确保环境变量可访问。</p></li><li><p>建立与客户端的连接。</p></li></ol></li><li><p><strong>将 NBA 数据批量导入 Elasticsearch</strong></p><ol><li><p>创建具有适当映射的索引，以启用聚合。</p></li><li><p>将 CSV 文件中的玩家游戏统计数据批量导入 Elasticsearch 索引。</p></li></ol></li><li><p><strong>定义 Elasticsearch 聚合</strong></p><ol><li><p>查询计算与特定对手的历史平均值。</p></li><li><p>查询计算对特定对手的赛季平均分。</p></li></ol></li><li><p><strong>播放器比较实用程序文件</strong></p><ol><li><p>整合辅助函数和 Elasticsearch 聚合。</p></li></ol></li><li><p><strong>建立代理</strong></p><ol><li><p>添加代理定义和系统提示。</p></li><li><p>安装 zod 和定义工具。</p></li><li><p>添加中间件设置以处理 CORS。</p></li></ol></li><li><p><strong>整合前端</strong></p><ol><li><p>使用 AI-SDK 的 useChat 与代理互动。</p></li><li><p>创建用户界面，以保存格式正确的对话。</p></li></ol></li><li><p><strong>运行应用程序</strong></p><ol><li><p>同时启动后端（Mastra 服务器）和前端（React 应用程序）。</p></li><li><p>查询和使用示例。</p></li></ol></li><li><p><strong>下一步是什么？让代理更智能</strong></p><ol><li><p>增加语义搜索功能，提供更具洞察力的建议。</p></li><li><p>将搜索逻辑移至 Elasticsearch MCP（模型上下文协议）服务器，从而启用动态查询。</p></li></ol></li></ol><h3><strong>准备工作</strong></h3><ul><li><p><strong>Node.js 和 npm</strong>：后端和前端都在 Node 上运行。确保已安装 Node 18+ 和 npm v9+（与 Node 18+ 绑定）。</p></li><li><p><strong>Elasticsearch 集群：</strong>本地或云端的活动 Elasticsearch 集群。</p></li><li><p><strong>OpenAI API 密钥</strong>：在<a href="https://platform.openai.com/api-keys">OpenAI 开发人员门户网站</a>的 API 密钥页面上生成一个。</p></li></ul><p></p><h3><strong>项目结构</strong></h3><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt749baa120552e4ab/6a17f1dd1d1b83bfe993e54a/1c0bde11ad0eead523a95e03b9b905aa776e3fd1-1420x934.png" alt="" /><h4><strong>步骤 1：为项目搭建脚手架</strong></h4><ol><li><p>首先，创建目录 nba-ai-assistant-js，并在其中导航： </p></li></ol>mkdir nba-ai-assistant-js &amp;&amp; cd nba-ai-assistant-js<p><strong>后台</strong></p><ol><li><p>使用 Mastra 创建工具并执行命令： </p></li></ol>npx create-mastra@latest<p>2.你的终端应该会收到一些提示，第一个提示是命名项目后台：</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt65abf68fe588e968/6a17f1de63baff2814741d5b/de2725031ed6837db99a979efcdd0ece1e197dbb-608x84.png" alt="" /><p>3.接下来，我们将保留存储 Mastra 文件的默认结构，因此输入<code>src/</code>.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt89bd829fcf0ae6b9/6a17f1e04b055dd30e432302/88919d9ff1852126395e1fcd700ecb1b59aac63c-866x116.png" alt="" /><p>4.然后，我们将选择 OpenAI 作为默认的 LLM 提供商。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltfd167cc77a40b9a8/6a17f1e11480099e29b48863/2328761e769f3ded134e5a21e8a0bf8f41e88f68-404x210.png" alt="" /><p>5.最后，它会要求你提供 OpenAI API 密钥。现在，我们选择跳过选项，稍后在<code> .env</code> 文件中提供。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt12654151ed495370/6a17f1e22f4a5c0f84fa89f9/0662de9bd28758e377e4c63df8d08b479068ce63-444x120.png" alt="" /><p><strong>前台</strong></p><ol><li><p>返回根目录，使用此命令运行<a href="https://vite.dev/guide/">Vite 创建工具</a>： <code>npm create vite@latest frontend -- --template react</code></p></li></ol><p>这将创建一个名为<code>frontend</code> 的轻量级 React 应用程序，并为 React 提供特定模板。</p><p>如果一切顺利，在你的项目目录中，你应该会看到一个存放 Mastra 代码的后台目录和一个存放 React 应用程序的<code>frontend</code> 目录。</p><p></p><h4><strong>步骤 2：设置环境变量</strong></h4><ol><li><p>为了管理敏感键，我们将使用<code>dotenv</code> 软件包从 .env 中加载环境变量。锉刀导航至后台目录，安装<code>dotenv</code> ：</p></li></ol>cd backend
npm install dotenv --save<p>2.在后台目录中，会提供一个 example.env 文件，其中包含需要填写的相应变量。如果您自己创建，请确保包含以下变量：</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>注意：通过在<code>.gitignore</code> 中添加<code>.env</code> ，确保将此文件排除在版本控制之外。</p><h4><strong>第 3 步：设置 Elasticsearch</strong></h4><p>首先，您需要一个活动的 Elasticsearch 集群。有两种选择：</p><ul><li><p><strong>选项 A：使用 Elasticsearch 云</strong></p><ul><li><p>注册<a href="https://cloud.elastic.co/registration">弹性云</a></p></li><li><p>创建新的部署</p></li><li><p>获取端点 URL 和 API 密钥（已编码）</p></li></ul></li><li><p><strong>选项 B：在本地运行 Elasticsearch</strong></p><ul><li><p>在本地安装并运行 Elasticsearch</p></li><li><p>使用 http://localhost:9200 作为终端</p></li><li><p>生成 API 密钥</p></li></ul></li></ul><p></p><p><strong>在后台安装 Elasticsearch 客户端：</strong></p><ol><li><p>首先，在后台目录中安装 Elasticsearch 官方客户端：</p></li></ol>npm install @elastic/elasticsearch<p>2.然后创建一个 lib 目录来存放可重复使用的函数，并导航进入该目录：</p>mkdir lib &amp;&amp; cd lib<p>3.在其中创建一个名为<a href="https://github.com/jdarmada/nba-ai-assistant-js/blob/main/backend/lib/elasticClient.js">elasticClient.js</a> 的新文件。该文件将初始化 Elasticsearch 客户端，并在整个项目中公开使用。</p><p>4.由于我们使用的是 ECMAScript 模块 (ESM)，因此无法使用__dirname and __文件名。为确保您的环境变量能从 .env文件，将此设置添加到文件顶部：</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.现在，使用环境变量初始化 Elasticsearch 客户端并检查连接：</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>现在，我们可以将此客户端实例导入任何需要与 Elasticsearch 集群交互的文件。</p><p></p><h4><strong>第 4 步：将 NBA 数据批量导入 Elasticsearch</strong></h4><p><strong>数据集：</strong></p><p>在本项目中，我们将引用软件版本<a href="https://github.com/jdarmada/nba-ai-assistant-js/tree/main/backend">中后端/数据</a>目录下的数据集。我们的 NBA 助手将以这些数据为知识基础，进行统计比较并生成建议。</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>- NBA 球员职业生涯的球员比赛统计数据样本（如得分、篮板、抢断等）。我们将使用该数据集进行聚合。(注：这是模拟数据，为演示目的而预先生成，并非来自 NBA 官方来源）。</p></li><li><p><a href="https://github.com/jdarmada/nba-ai-assistant-js/blob/main/backend/data/playerAndTeamInfo.js">playerAndTeamInfo.js</a>- 替代通常由应用程序接口调用提供的球员和球队元数据，以便代理能将球员和球队名称与 ID 匹配。由于我们使用的是样本数据，我们不希望从外部应用程序接口获取数据造成开销，因此我们硬编码了一些代理可以引用的值。</p></li></ul><p></p><p><strong>实施：</strong></p><ol><li><p>在<code>backend/lib</code> 目录中，创建名为<a href="https://github.com/jdarmada/nba-ai-assistant-js/blob/main/backend/lib/playerDataIngestion.js">playerDataIngestion.js</a> 的文件。</p></li><li><p>设置导入、解析 CSV 文件路径并设置解析。同样，由于我们使用的是 ESM，因此需要重构<code>__dirname</code> 来解析 CSV 样本的路径。此外，我们还将导入<a href="http://node.js/">Node.js</a>的内置模块<code>fs</code> 和<code>readline</code> 逐行解析给定的 CSV 文件。</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>这样，当我们进入批量摄取步骤时，就能高效地读取和解析 CSV。</p><p>3.创建具有适当映射的索引。虽然 Elasticsearch 可以通过<a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/dynamic">动态映射</a>自动推断字段类型，但我们希望在此明确说明，以便每个统计信息都被视为数字字段。这一点很重要，因为稍后我们将使用这些字段进行聚合。我们还希望对得分、篮板等统计数据使用<code>float </code>类型，以确保包含小数值。最后，我们要添加映射属性<code>dynamic: 'strict'</code> ，这样 Elasticsearch 就不会动态映射未识别的字段。 
</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.添加将 CSV 数据批量导入 Elasticsearch 索引的函数。在代码块内，我们跳过标题行。然后，用逗号分隔每个行项目，并将其推入文档对象。这一步骤还可以清洁它们，并确保它们是正确的类型。接下来，我们将文档连同索引信息一起推送到 bulkBody 数组中，作为批量摄取到 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.然后，我们可以通过<code>elasticClient.bulk()</code> 使用 Elasticsearch 的<a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-bulk">批量 API</a>，在一次请求中摄取多个文档。下面的错误处理结构可以让你计算有多少文档未能被摄取，有多少文档被成功摄取。</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.运行下面的<code>main()</code> 函数，依次运行<code>createIndex()</code> 和<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>如果看到控制台日志显示批量摄取成功，请在 Elasticsearch 索引上执行快速检查，查看是否确实成功摄取了文档。</p><h4><strong>步骤 5：定义 Elasticsearch 聚合和合并</strong></h4><p>这些将是我们为人工智能代理定义工具时使用的主要功能，以便对球员的统计数据进行比较。</p><p>1.导航至<code>backend/lib</code> 目录，创建名为<a href="https://github.com/jdarmada/nba-ai-assistant-js/blob/main/backend/lib/elasticAggs.js">elasticAggs.js</a> 的文件。</p><p>2.添加下面的查询，计算球员对特定对手的历史平均分。该查询使用<code>bool</code> <a href="https://www.elastic.co/search-labs/tutorials/search-tutorial/full-text-search/filters">过滤器</a>，其中包含两个条件：一个匹配<code>player_id</code> ，另一个匹配<code>opponent_team_id</code> ，以便只检索相关游戏。我们不需要返回任何文档，我们只关心聚合，因此我们设置<code>size:0</code> 。在<code>aggs</code> 块下，我们在<code>points, rebounds, assists, steals, blocks</code> 和<code>fg_percentage</code> 等字段上并行运行多个度量<a href="https://www.elastic.co/docs/explore-analyze/query-filter/aggregations">聚合</a>，以计算它们的平均值。LLM 的计算可能会出现偏差，而这一功能可将计算过程卸载到 Elasticsearch，确保我们的 NBA AI 助手能够访问准确的数据。</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.要计算一名球员对阵特定对手的赛季平均值，我们将使用与历史查询几乎相同的查询方式。该查询的唯一区别是<code>bool</code> 过滤器对<code>game_date</code> 附加了一个条件。<code>game_date</code> 必须在当前 NBA 赛季的范围内。在这种情况下，范围介于<code>2024-10-01</code> 和<code>2025-06-30</code> 之间。下面这个额外的条件确保了后面的汇总将只分离出本赛季的比赛。
</p>        {
                            range: {
                    //Range for this season, change to match current season
                                game_date: {
                                    gte: '2024-10-01',
                                    lte: '2025-06-30',
                                },
                            },
<h4><strong>步骤 6：球员比较实用程序</strong></h4><p>为了保持代码的模块化和可维护性，我们将创建一个实用程序文件来整合元数据辅助函数和 Elasticsearch 聚合。这将为特工使用的主要工具提供动力。稍后再详述：</p><p>1.在<code>backend/lib</code> 目录中新建一个文件<a href="https://github.com/jdarmada/nba-ai-assistant-js/blob/main/backend/lib/comparePlayers.js">comparePlayers.js</a>。</p><p>2.添加下面的函数，将元数据助手和 Elasticsearch 聚合逻辑合并为一个函数，为代理使用的主要工具提供动力。
</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>步骤 7：建立代理</strong></h4><p>现在，您已经创建了前端和后端脚手架，摄取了 NBA 游戏数据，并建立了与 Elasticsearch 的连接，我们可以开始将所有部件组装在一起以构建代理。</p><p><strong>定义代理</strong></p><p>1.导航至<code>backend/src/mastra/agents</code> 目录中的<a href="https://github.com/jdarmada/nba-ai-assistant-js/blob/main/backend/src/mastra/agents/index.ts">index.ts</a>文件并添加代理定义。您可以指定以下字段</p><ul><li><p><strong>名称：</strong>给代理起一个名字，在前台调用时用作参考。</p></li><li><p><strong>指令/系统提示： </strong>系统提示为 LLM 提供交互过程中需要遵循的初始环境和规则。它类似于用户通过聊天框发出的提示，但这个提示是在用户输入之前发出的。同样，这也会根据您选择的机型而变化。</p></li><li><p><strong>模型：</strong>使用哪种 LLM（Mastra 支持 OpenAI、Anthropic、本地模型等）。</p></li><li><p><strong>工具：</strong>代理可调用的工具功能列表。</p></li><li><p><strong>记忆：</strong>（可选）如果我们希望代理记住对话历史等。为了简单起见，我们可以不使用持久内存，尽管 Mastra 支持持久内存。</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>
定义工具</strong></p><ol><li><p>导航至<code>backend/src/mastra/tools</code> 目录中的<a href="https://github.com/jdarmada/nba-ai-assistant-js/blob/main/backend/src/mastra/tools/index.ts">index.ts</a>文件。</p></li><li><p>使用命令安装 Zod：</p></li></ol>npm install zod<p>3.添加工具定义。请注意，我们将<code>comparePlayers.js</code> 文件中的函数导入为代理在调用该工具时将使用的主函数。使用 Mastra 的<code>createTool()</code> 功能，我们将注册<code>playerComparisonTool</code> 。这些领域包括</p><ul><li><p><code>id</code>:这是一种自然语言描述，用于帮助代理理解工具的功能。</p></li><li><p><code>input schema</code>:为了定义工具的输入形状，Mastra 使用了<a href="https://zod.dev/">Zod</a>模式，这是一个 TypeScript 模式验证库。Zod 可确保代理输入结构正确的输入，并在输入结构不匹配时阻止工具执行。</p></li><li><p><code>description</code>:这是一种自然语言描述，帮助代理了解何时呼叫和使用工具。</p></li><li><p><code>execute</code>:调用工具时运行的逻辑。在本例中，我们使用一个导入的辅助函数来返回性能统计信息。</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>添加中间件处理 CORS</strong></p><p>在 Mastra 服务器中添加中间件以处理<a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS">CORS</a>。俗话说，人生有三件事无法避免：死亡、税收，而对于网络开发人员来说，就是 CORS。简而言之，跨源资源共享是一种浏览器安全功能，可阻止前台向运行在不同域或端口的后台发出请求。尽管我们在 localhost 上运行后端和前端，但它们使用不同的端口，从而触发了 CORS 策略。我们需要添加<a href="https://mastra.ai/en/docs/server-db/middleware">Mastra 文档</a>中指定的中间件，以便我们的后端允许来自前端的请求。</p><p>1.导航至<code>backend/src/mastra</code> 目录中的<a href="https://github.com/jdarmada/nba-ai-assistant-js/blob/main/backend/src/mastra/index.ts">index.ts</a>文件，并添加 CORS 配置：</p><ul><li><p><code>origin: ['http://localhost:5173']</code></p><ul><li><p>只允许来自该地址的请求（Vite 默认地址）</p></li></ul></li><li><p><code>allowMethods: ["GET", "POST"]</code></p><ul><li><p>允许使用的 HTTP 方法。大多数情况下，它将使用 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>它们决定了哪些自定义标头可以在请求中使用</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>步骤 8：整合前端</strong></h4><p>这个 React 组件提供了一个简单的聊天界面，可使用<code>@ai-sdk/react</code> 中的<a href="https://mastra.ai/en/docs/frameworks/agentic-uis/ai-sdk#using-the-usechat-hook">useChat()</a>钩子连接到 Mastra AI 代理。我们还将使用此钩子来显示标记的使用情况、工具调用情况并渲染对话。在上面的系统提示中，我们还要求代理以 markdown 格式输出响应，因此我们将使用<code>react-markdown</code> 来正确格式化响应。</p><p></p><p>1.在前端目录中，安装 @ai-sdk/react 软件包以使用 useChat() 钩子。</p>npm install @ai-sdk/react<p>2.在同一目录下，安装 React Markdown，这样我们就能正确格式化代理生成的响应。</p>npm install react-markdown<p>3.实施<code>useChat()</code> 。此钩子将管理前台与人工智能代理后台之间的交互。它可以处理消息状态、用户输入和状态，并为您提供生命周期钩子，以实现可观察性。我们提供的选项包括</p><ul><li><p><code>api:</code> 这定义了 Mastra AI 代理的端点。默认端口为 4111，我们还要添加支持流式响应的路由。</p></li><li><p><code>onToolCall</code>:在代理调用工具时执行；我们用它来跟踪代理调用了哪些工具。</p></li><li><p><code>onFinish</code>:在代理完成完整响应后执行。尽管我们启用了流式传输，但<code>onFinish</code> 仍将在收到完整报文后运行，而不是在每个分块后运行。在这里，我们用它来跟踪令牌的使用情况。这对监控 LLM 成本和优化成本很有帮助。</p></li></ul><p>4.最后，前往<code>frontend/components</code> 目录中的<a href="https://github.com/jdarmada/nba-ai-assistant-js/blob/main/frontend/components/ChatUI.jsx">ChatUI.jsx</a>组件，创建用户界面来进行对话。接下来，用<code>ReactMarkdown</code> 组件封装响应，以便正确格式化来自代理的响应。</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>步骤 9：运行应用程序</strong></h4><p>祝贺你现在就可以运行应用程序了。按照以下步骤启动后台和前台。</p><ol><li><p>在终端窗口中，从根目录开始，导航到后台目录并启动 Mastra 服务器：</p></li></ol>cd backend

npm run dev<p>2.在另一个终端窗口中，从根目录开始，导航到前端目录并启动 React 应用程序：</p><p></p>cd frontend

npm run dev<p></p><p>3.打开浏览器，导航到</p><p></p><p><a href="http://localhost:5173/">http://localhost:5173</a></p><p></p><p>您应该可以看到聊天界面。试试这些提示样本：</p><ul><li><p>"对比勒布朗-詹姆斯和斯蒂芬-库里"</p></li><li><p>"我应该在杰森-塔图姆和卢卡-东契奇之间选谁？"</p></li></ul><p></p><h3><strong>下一步是什么？让代理更智能</strong></h3><p>为了让助手更具代理能力，建议更具洞察力，我将在下一次迭代中添加一些关键升级。</p><p></p><p><strong>NBA 新闻的语义搜索</strong></p><p>有很多因素会影响球员的表现，其中很多并不会在原始数据中体现出来。像伤病报告、阵容变化，甚至赛后分析，你只能在新闻报道中找到。为了捕捉这些额外的上下文，我将添加语义搜索功能，这样代理就可以检索相关的 NBA 文章，并将这些叙述纳入其推荐中。</p><p></p><p><strong>使用 Elasticsearch MCP 服务器进行动态搜索</strong></p><p>MCP（模型上下文协议）正迅速成为代理连接数据源的标准。我将把搜索逻辑迁移到 Elasticsearch MCP 服务器中，这样代理就可以动态建立查询，而不是依赖我们提供的预定义搜索功能。这使我们能够使用更多的自然语言工作流，并减少了手动编写每个搜索查询的需要。<a href="https://www.elastic.co/search-labs/blog/mcp-current-state">点击此处</a>了解有关 Elasticsearch MCP 服务器和生态系统现状的更多信息。</p><p></p><p>这些更改正在进行中，敬请期待！</p><h3><strong>结论</strong></h3><p></p><p>在本博客中，我们使用 JavaScript、Mastra 和 Elasticsearch 构建了一个代理 RAG 助手，为您的梦幻篮球队提供量身定制的建议。我们报道了</p><ul><li><p><strong>代理 RAG 的基本原理</strong>，以及如何将人工智能代理的自主性与有效使用 RAG 的工具相结合，从而产生更细致入微、更具活力的代理。</p></li><li><p><strong>Elasticsearch </strong>及其数据存储能力和强大的本地聚合功能如何使其成为法律硕士知识库的最佳合作伙伴。</p></li><li><p><strong>Mastra </strong>框架及其如何为 javaScript 生态系统中的开发人员简化这些代理的构建。</p></li></ul><p>无论你是篮球迷，还是在探索如何构建人工智能代理，或者像我一样两者兼而有之，我都希望这篇博客能为你提供一些入门的基础知识。完整的软件源可在<a href="https://github.com/jdarmada/nba-ai-assistant-js">GitHub</a> 上获取，请随意克隆和修补。现在，去赢得梦幻联赛吧！</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/agentic-rag</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/agentic-rag</guid>
    <category><![CDATA[AI]]></category>
    <category><![CDATA[智能体 AI]]></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[正确使用 JavaScript 的 Elasticsearch，第二部分]]></title>
    <description><![CDATA[了解生产环境最佳实践，并学习如何在 Serverless 环境中运行 Elasticsearch Node.js 客户端，以减少代码错误。 ]]></description>
    <content:encoded><![CDATA[<p>这是 Elasticsearch in JavaScript 系列的第二部分。在<a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i"> 第一部分 中 ，</a> 我们学习了如何正确设置环境、配置 Node.js 客户端、索引数据和搜索。在第二部分中，我们将学习如何实施生产最佳实践，并在无服务器环境中运行 Elasticsearch<a href="http://node.js">Node.js</a>客户端。</p><p>我们将审查</p><ul><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-ii#production-best-practices">生产最佳实践</a></p><ul><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-ii#error-handling">错误处理能力</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-ii#testing">测试</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">无服务器环境</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">在 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">在功能即服务环境中运行客户端</a></p></li></ul></li></ul><p><em>您可以 </em><a href="https://github.com/Delacrobix/JS-client-best-practices_article"><em><strong>在这里</strong></em></a>查看示例的源代码 <em><strong>。</strong></em></p><h2>生产最佳实践</h2><h3>Elasticsearch 中的错误处理</h3><p>Node.js 中 Elasticsearch 客户端的一个有用功能是，它为 Elasticsearch 中可能出现的错误提供了对象，因此您可以用不同的方式验证和处理这些错误。</p><p>要<a href="https://www.elastic.co/docs/reference/elasticsearch/clients/javascript/connecting#client-error-handling">查看全部内容</a>，请执行此操作： </p>const { errors } = require('@elastic/elasticsearch')
console.log(errors)<p>让我们回到搜索示例，处理一些可能出现的错误：</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> 尤其是当答案为<code>4xx</code> 或<code>5xx</code> 时，即表示请求不正确或服务器不可用。</p><p>我们可以通过生成错误查询来测试这类错误，比如尝试<strong>在文本类型字段上进行术语查询：</strong></p><p>默认错误：</p> {
    "success": false,
    "results": null,
    "error": "parsing_exception\n\tRoot causes:\n\t\tparsing_exception: [terms] query does not support [visit_details]"
}<p>定制错误： </p>{
    "erroStatus": 400,
    "success": false,
    "results": null,
    "error": "Response error!, query malformed or server down; contact the administrator!"
}<p>我们还可以以某种方式捕捉和处理每种类型的错误。例如，我们可以在<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>测试</h3><p>测试是保证应用程序稳定性的关键。为了以一种与 Elasticsearch 隔离的方式测试代码，我们可以在创建集群时使用<a href="https://github.com/elastic/elasticsearch-js-mock">elasticsearch-js-mock</a>库。</p><p>通过该库，我们可以实例化一个与真实客户端非常相似的客户端，但只需将客户端的 HTTP 层替换为模拟层，其他部分与原始客户端保持一致，就能满足我们的配置要求。</p><p>我们将安装 mocks 库和用于自动测试的<a href="https://github.com/avajs/ava">AVA</a>。</p><p><code>npm install @elastic/elasticsearch-mock</code></p><p><code>npm install --save-dev ava</code></p><p>我们将配置<code>package.json</code> 文件以运行测试。确保它看起来是这样的：</p>"type": "module",
	"scripts": {
		"test": "ava"
	},
	"devDependencies": {
		"ava": "^5.0.0"
	}<p>现在，让我们创建<code>test.js</code> 文件并安装我们的模拟客户端：</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>现在，为语义搜索添加一个模拟：</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>现在我们可以为代码创建一个测试，确保 Elasticsearch 部分始终返回相同的结果：</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>让我们进行测试。</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>完成！从现在起，我们就可以测试我们的应用程序，100% 专注于代码而不是外部因素。</p><h2>无服务器环境</h2><h3>如何在 Elastic Serverless 上运行客户端</h3><p>我们介绍了在云端或内部运行 Elasticsearch 的情况；不过，Node.js 客户端也支持与<a href="https://www.elastic.co/guide/en/serverless/current/intro.html">Elastic Cloud Serverless</a> 的连接。</p><p>Elastic Cloud Serverless 允许您创建一个项目，在这个项目中，您无需担心基础设施问题，因为 Elastic 会在内部处理这些问题，您只需担心您想索引的数据以及您想在多长时间内访问这些数据。</p><p>从使用角度来看，Serverless 将计算与存储分离，为<a href="https://www.elastic.co/search-labs/blog/elasticsearch-serverless-tier-autoscaling">搜索</a>和<a href="https://www.elastic.co/search-labs/blog/elasticsearch-ingest-autoscaling">索引</a>提供了自动扩展功能。这样，您就可以只增长实际需要的资源。</p><p>客户端会进行以下调整，以连接到无服务器：</p><ul><li><p>关闭嗅探，忽略任何与嗅探相关的选项</p></li><li><p>忽略配置中传递的除第一个节点外的所有节点，并忽略任何节点过滤和选择选项</p></li><li><p>启用压缩和 "TLSv1_2_method"（与为弹性云配置时相同）</p></li><li><p>为所有请求添加 "elastic-api-version "HTTP 头信息</p></li><li><p>默认使用 "云连接池"，而不是 "加权连接池</p></li><li><p>关闭卖方 "内容类型 "和 "接受 "标头，转而使用标准 MIME 类型</p></li></ul><p>要连接无服务器项目，需要使用参数 serverMode：serverless。</p>const { Client } = require('@elastic/elasticsearch')
const client = new Client({
  node: 'ELASTICSEARCH_ENDPOINT',
  auth: { apiKey: 'ELASTICSEARCH_API_KEY' },
  serverMode: "serverless",
});<h3>如何在函数即服务环境中运行客户端</h3><p>在示例中，我们使用了 Node.js 服务器，但您也可以使用功能即服务环境连接 AWS lambda、GCP Run 等功能。</p>'use strict'

const { Client } = require('@elastic/elasticsearch')

const client = new Client({
  // client initialisation
})

exports.handler = async function (event, context) {
  // use the client
}<p>另一个例子是连接像 Vercel 这样的服务，它也是无服务器的。您可以查看这个<a href="https://github.com/elastic/elasticsearch-js/blob/main/docs/examples/proxy/README.md">完整的示例</a>，了解如何做到这一点，但<a href="https://github.com/elastic/elasticsearch-js/blob/main/docs/examples/proxy/api/search.js">搜索端点</a>最相关的部分如下所示：</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>该端点位于 /api 文件夹中，从服务器端运行，因此客户端只能控制与搜索词相对应的 "文本 "参数。</p><p>使用 "功能即服务 "的意义在于，与全天候运行的服务器不同，功能只启动运行该功能的机器，一旦完成，机器就会进入休息模式，以减少资源消耗。</p><p>如果应用程序没有收到太多请求，这种配置会很方便；否则，成本会很高。您还需要考虑<a href="https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtime-environment.html">函数的生命周期</a>和运行时间（在某些情况下可能只有几秒钟）。</p><h2>结论</h2><p>在本文中，我们学习了如何处理错误，这在生产环境中至关重要。我们还介绍了在模拟 Elasticsearch 服务的过程中测试应用程序的方法，无论集群的状态如何，这种方法都能提供可靠的测试，让我们专注于我们的代码。</p><p>最后，我们演示了如何通过配置 Elastic Cloud Serverless 和 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[基础功能]]></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[正确使用 JavaScript 的 Elasticsearch，第一部分]]></title>
    <description><![CDATA[讲解如何用 JavaScript 创建可投入生产的 Elasticsearch 后端。  

探索如何使用 JavaScript 与 Elasticsearch，遵循客户端/服务器最佳实践，搭建包含多个搜索端点的服务器，用于查询 Elasticsearch 文档。]]></description>
    <content:encoded><![CDATA[<p>本文是系列文章的第一篇，介绍如何使用 JavaScript 使用 Elasticsearch。在本系列中，您将学习如何在 JavaScript 环境中使用 Elasticsearch 的基础知识，并回顾创建搜索应用程序的最相关功能和最佳实践。最后，您将了解使用 JavaScript 运行 Elasticsearch 所需的一切。</p><p>在第一部分中，我们将回顾</p><ul><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i#environment">环境</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?">前端、后端还是无服务器？</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">连接客户端</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">编制文件索引</a></p><ul><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i#elasticsearch-client">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">语义映射</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i#bulk-helper">批量助手</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">搜索数据</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)">词法查询</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)">语义查询</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)">混合查询</a></p></li></ul></li></ul><p><em>您可以 </em><a href="https://github.com/Delacrobix/JS-client-best-practices_article"><em><strong>在这里</strong></em></a>查看示例的源代码 <em><strong>。</strong></em></p><h3>什么是 Elasticsearch Node.js 客户端？</h3><p><a href="https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/index.html">Elasticsearch Node.js 客户端</a>是一个 JavaScript 库，它将 Elasticsearch API 的 HTTP REST 调用放到了 JavaScript 中。这样就能更轻松地处理和使用帮助程序，简化批量编制文档索引等任务。</p><h2>环境</h2><h3>前端、后端还是无服务器？</h3><p>要使用 JavaScript 客户端创建搜索应用程序，我们至少需要两个组件：Elasticsearch 集群和运行客户端的 JavaScript 运行时。</p><p>JavaScript 客户端支持所有 Elasticsearch 解决方案（云、on-prem 和 Serverless），它们之间没有重大区别，因为客户端内部会处理所有变化，所以你不必担心使用哪一种。</p><p>不过，JavaScript 运行时必须从<strong>服务器</strong>运行，而<strong>不能直接从浏览器</strong>运行。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltd3ec469c83e3a71a/6a17e3d5445de91da44d00b6/92ce6cfd923c8008fa44f617a58193642d9d5879-661x410.png" alt="在 JavaScript 环境中使用 Elasticsearch。" /><p>这是因为从浏览器调用 Elasticsearch 时，用户可能会获得敏感信息，如集群 API 密钥、主机或查询本身。Elasticsearch 建议<strong>永远不要将集群直接暴露在互联网上 </strong>，而是使用一个中间层来抽象所有这些信息，这样用户只能看到参数。您可以<a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/es-security-principles.html#security-protect-cluster-traffic">在这里</a>了解更多相关信息。</p><p>我们建议使用这样的模式：</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt4d7f215f2e70230a/6a17e3d6fbc5f83de6491a13/a08769f08ec73fe57bf2e961cfdfbb1cdd57919d-972x429.png" alt="设置 Elasticsearch Node.js 客户端。" /><p>在这种情况下，客户端只向服务器发送搜索条件和验证密钥，而服务器则完全控制查询和与 Elasticsearch 的通信。</p><h3>连接客户端</h3><p>首先，按照<a href="https://www.elastic.co/search-labs/tutorials/install-elasticsearch/elastic-cloud">以下步骤</a>创建一个 API 密钥。</p><p>按照前面的示例，我们将创建一个简单的 Express 服务器，并使用 Node.JS 服务器的客户端连接到该服务器。</p><p>我们将使用 NPM 初始化项目，并安装 Elasticsearch 客户端和<a href="https://expressjs.com/">Express。</a>后者是一个在 Node.js 中调用服务器的库。使用 Express，我们可以通过 HTTP 与后端交互。</p><p>让我们初始化项目：</p><p><code>npm init -y</code></p><p>安装依赖项：</p><p><code>npm install @elastic/elasticsearch express split2 dotenv</code></p><p>让我来为你分析一下：</p><ul><li><p><a href="https://www.npmjs.com/package/@elastic/elasticsearch"><em><strong>@elastic/elasticsearch</strong></em></a>：它是 Node.js 的官方客户端</p></li><li><p><a href="https://www.npmjs.com/package/express"><em><strong>快递</strong></em></a>：它将使我们能够运行一个轻量级的 nodejs 服务器，以暴露 Elasticsearch</p></li><li><p><a href="https://www.npmjs.com/package/split2"><em><strong>split2</strong></em></a>： 将文本行分割成数据流。每次处理一行 ndjson 文件时非常有用</p></li><li><p><a href="https://www.npmjs.com/package/dotenv"><em><strong>dotenv</strong></em></a>：允许我们使用 .env 管理环境变量文件</p></li></ul><p>创建 .env文件，并添加以下几行：</p>ELASTICSEARCH_ENDPOINT="Your Elasticsearch endpoint"
ELASTICSEARCH_API_KEY="Your Elasticssearch API"<p>这样，我们就可以使用<code>dotenv</code> 软件包导入这些变量。</p><p>创建<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>这段代码设置了一个基本的 Express.js 服务器，该服务器监听端口 3000，并使用 API 密钥进行身份验证，连接到 Elasticsearch 集群。它包括一个 /ping 端点，通过 GET 请求访问时，可使用 Elasticsearch 客户端的<code>.info()</code> 方法查询 Elasticsearch 集群的基本信息。 </p><p>如果查询成功，会以 JSON 格式返回群集信息；否则会返回错误信息。服务器还使用 body-parser 中间件来处理 JSON 请求体。</p><p>运行文件，启动服务器：</p><p><code>node server.js</code></p><p>答案应该是这样的</p>Server running on port 3000<p>现在，让我们查阅端点<code>/ping</code> ，检查 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>编制文件索引</h2><p>一旦连接起来，我们就可以使用语义<a href="https://www.elastic.co/search-labs/blog/semantic-search-simplified-semantic-text">_文本（</a>用于语义搜索）和文本（用于全文查询）等映射对文档进行索引。有了这两种字段类型，我们还可以进行<a href="https://www.elastic.co/what-is/hybrid-search">混合搜索</a>。</p><p>我们将创建一个新的<code>load.js</code> 文件来生成映射并上传文件。</p><h3>Elasticsearch 客户端</h3><p>我们首先需要对客户端进行实例化和身份验证：</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>语义映射</h3><p>我们将创建一个包含兽医院数据的索引。我们将保存主人、宠物和访问详情的信息。</p><p>我们要进行全文搜索的数据，如名称和描述，将以文本形式存储。类别中的数据，如动物的种类或品种，将以关键字的形式存储。</p><p>此外，我们还将把所有字段的值复制到一个 semantic_text 字段中，以便也能针对这些信息运行语义搜索。</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>批量助手</h3><p>客户端的另一个优势是，我们可以使用<a href="https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/client-helpers.html#bulk-helper">批量助手</a>来分批建立索引。通过批量辅助器，我们可以轻松处理并发、重试等问题，以及如何处理通过函数成功或失败的每个文档。</p><p>该助手的一个吸引人的特点是可以使用数据流。该功能允许您逐行发送文件，而不是将整个文件存储在内存中并一次性发送到 Elasticsearch。</p><p>要将数据上传到 Elasticsearch，请在项目根目录下创建名为 data.ndjson 的文件，并添加以下信息（也可以从<a href="https://github.com/Delacrobix/JS-client-best-practices_article/blob/main/data.ndjson">此处</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>我们使用 split2 对文件行进行流式处理，而批量助手则将它们发送到 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>上面的代码读取 .ndjson文件，并使用<code>helpers.bulk</code> 方法将每个 JSON 对象批量索引到指定的 Elasticsearch 索引中。它使用<code>createReadStream</code> 和<code>split2</code> 对文件进行流式处理，为每个文件设置索引元数据，并记录处理失败的文件。完成后，它会记录成功索引的项目数。</p><p>除<code>indexData</code> 功能外，您还可以使用 Kibana 直接通过用户界面上传文件，并使用<a href="https://www.elastic.co/docs/manage-data/ingest/upload-data-files">上传数据文件用户界面。</a></p><p>我们运行文件，将文件上传到 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>在 Elasticsearch 中搜索数据</h2><p>回到<code>server.js</code> 文件，我们将创建不同的端点来执行词法、语义或混合搜索。</p><p>简而言之，这些类型的搜索并不相互排斥，而是取决于您需要回答的问题类型。</p><p>查询类型</p><p>用例</p><p>问题示例</p><p>词法查询</p><p>问题中的单词或词根很可能出现在索引文件中。问题与文件之间的标记相似性。</p><p>我在找一件蓝色运动 T 恤。</p><p>语义查询</p><p>问题中的词语不可能出现在文件中。问题与文件之间的概念相似性。</p><p>我在寻找适合寒冷天气穿的衣服。</p><p>混合搜索</p><p>问题包含词汇和/或语义成分。问题与文档之间的标记和语义相似性。</p><p>我想为海滩婚礼找一件 S 码的礼服。</p><p>问题的<em><strong>词汇 </strong></em>部分很可能是标题和说明的一部分，或者是类别名称，而<em><strong>语义 </strong></em>部分则是与这些领域相关的概念。<em><strong>蓝色</strong></em>可能是一个类别名称或描述的一部分，<em><strong>海滩婚礼</strong></em>不太可能是，但可以与亚麻服装在语义上相关。</p><h3>词法查询 (/search/lexic?q=&lt;query_term&gt;)</h3><p>词法搜索也称全文搜索，是指基于标记的相似性进行搜索；也就是说，经过分析后，将返回包含搜索标记的文档。</p><p>您可以<a href="https://www.elastic.co/demo-gallery/lexical-search">点击此处</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>我们测试：<em><strong>修剪指甲</strong></em></p>curl http://localhost:3000/search/lexic?q=nail%20trimming<p>请回答：</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>语义查询 (/search/semantic?q=&lt;query_term&gt;)</h3><p>语义搜索与词汇搜索不同，它通过矢量搜索找到与搜索词含义相似的结果。</p><p>您可以<a href="https://www.elastic.co/demo-gallery/semantic-search">点击这里</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>我们进行测试：<em><strong>谁做了修脚？</strong></em></p>curl http://localhost:3000/search/semantic?q=Who%20got%20a%20pedicure?<p>请回答：</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>混合查询 (/search/hybrid?q=&lt;query_term&gt;)</h3><p>混合搜索允许我们将语义搜索和词法搜索结合起来，从而获得两全其美的效果：既能获得标记搜索的精确性，又能获得语义搜索的意义接近性。</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>我们以 "<em><strong>谁做了修脚或牙科治疗？"</strong></em></p>curl http://localhost:3000/search/hybrid?q=who%20got%20a%20pedicure%20or%20dental%20treatment<p>响应：</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>结论</h2><p>在本系列的第一部分中，我们介绍了如何按照客户端/服务器最佳实践设置环境并创建带有不同搜索端点的服务器，以查询 Elasticsearch 文档。查看我们系列的<a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i">第二部分</a>，您将了解生产最佳实践以及如何在无服务器环境中运行 Elasticsearch Node.js 客户端。</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[基础功能]]></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>