<?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/jp/search-labs/blog/category/javascript-programming</link>
    </image>
    <link>https://www.elastic.co/jp/search-labs/blog/category/javascript-programming</link>
    <atom:link href="https://www.elastic.co/jp/search-labs/rss/category/javascript-programming.xml" rel="self" type="application/rss+xml"/>
    <language><![CDATA[jp]]></language>
    <lastBuildDate>Mon, 28 Sep 2026 07:33:53 GMT</lastBuildDate>
  <item>
    <title><![CDATA[JavaScript、Mastra、Elasticsearch を使用したエージェント型 RAG アシスタントの構築]]></title>
    <description><![CDATA[JavaScript エコシステムで AI エージェントを構築する方法を学ぶ]]></description>
    <content:encoded><![CDATA[<p>このアイデアは、白熱したハイリスクなファンタジー バスケットボール リーグの最中に思いつきました。私はこう考えました。 <em>「毎週の対戦で優位に立つのに役立つ AI エージェントを構築できるだろうか？」 もちろんです!</em></p><p>この記事では、 <a href="https://mastra.ai/en/docs">Mastra</a>とそれと対話するための軽量 JavaScript Web アプリケーションを使用して、エージェント RAG アシスタントを構築する方法について説明します。このエージェントを Elasticsearch に接続することで、構造化されたプレイヤーデータへのアクセスとリアルタイムの統計集計の実行が可能になり、プレイヤー統計に基づいた推奨事項を提供できるようになります。GitHub<a href="https://github.com/jdarmada/nba-ai-assistant-js.git">リポジトリ</a>にアクセスして手順を確認してください。README<a href="https://github.com/jdarmada/nba-ai-assistant-js/blob/main/README.md">に</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>」に基づいています。AI エージェント全般とその用途についてよく知らない場合は、まずそこから始めてください。
</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 Web アプリケーションを迅速に構築します。</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>AI エージェントは他のシステムと対話し、独立して動作し、定義されたパラメータに基づいてアクションを実行できます。Agentic RAG は、AI エージェントの自律性と検索拡張生成の原理を組み合わせ、LLM が応答を生成するために呼び出すツールとコンテキストとして使用するデータを選択できるようにします。RAG の詳細については、<a href="https://www.elastic.co/search-labs/blog/retrieval-augmented-generation-rag">こちらを</a>ご覧ください。</p><h3><strong>フレームワークを選択する場合、なぜ AI-SDK を超えるのでしょうか?</strong></h3><p>利用可能な AI エージェント フレームワークは数多くあり、 <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>こちらは、Harrison Chase (LangChain CEO) によるフレームワーク<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>これらのフレームワークは AI エージェントを迅速に構築するのに役立ちますが、考慮すべき欠点もいくつかあります。たとえば、AI エージェントや一般的な抽象化レイヤー以外のフレームワークを使用する場合、制御が少し失われます。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>適切にフォーマットされた会話を保持するための UI を作成します。</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 (Model Context Protocol) サーバーに移動することで、動的クエリを有効にします。</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. ターミナルにいくつかのプロンプトが表示されます。最初のプロンプトでは、プロジェクトに backend という名前を付けます。</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. 次に、デフォルトの LLM プロバイダーとして OpenAI を選択します。</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>これにより、React 専用のテンプレートを使用して、 <code>frontend</code>という名前の軽量 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>.env</code> <code>.gitignore</code>に追加して、このファイルがバージョン管理から除外されていることを確認してください。</p><h4><strong>ステップ3: Elasticsearchの設定</strong></h4><p>まず、アクティブな Elasticsearch クラスターが必要です。次の 2 つのオプションがあります。</p><ul><li><p><strong>オプションA: Elasticsearch Cloudを使用する</strong></p><ul><li><p><a href="https://cloud.elastic.co/registration">Elastic Cloud</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">backend/data</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> - 通常は API 呼び出しによって提供されるプレーヤーとチームのメタデータを置き換え、エージェントがプレーヤーとチームの名前を ID に一致できるようにします。サンプル データを使用しているため、外部 API から取得する際のオーバーヘッドを避け、エージェントが参照できるいくつかの値をハードコードしました。</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 を使用しているため、サンプル CSV へのパスを解決するには<code>__dirname</code>を再構築する必要があります。また、 <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>を使用します。最後に、Elasticsearch が認識されないフィールドを動的にマッピングしないように、マッピング プロパティ<code>dynamic: 'strict'</code>を追加します。
</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">Bulk API を</a>使用して、1 回のリクエストで複数のドキュメントを取り込むことができます。以下のエラー処理は、取り込みに失敗したドキュメントの数と、取り込みに成功したドキュメントの数を示すように構成されています。</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>これらは、プレイヤーの統計を相互に比較するために AI エージェントのツールを定義するときに使用される主な関数になります。</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. 特定の対戦相手に対するプレイヤーの過去の平均を計算するには、以下のクエリを追加します。このクエリでは、2 つの条件（1 つは<code>player_id</code>に一致し、もう 1 つは<code>opponent_team_id</code>に一致する）を持つ<code>bool</code><a href="https://www.elastic.co/search-labs/tutorials/search-tutorial/full-text-search/filters">フィルター</a>を使用して、関連するゲームのみを取得します。ドキュメントを返す必要はなく、集計のみを対象とするため、 <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 は TypeScript スキーマ検証ライブラリである<a href="https://zod.dev/">Zod</a>スキーマを使用します。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><a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS">CORS を</a>処理するために、Mastra サーバーにミドルウェアを追加します。人生には避けられないことが 3 つあると言われています。死、税金、そして Web 開発者にとっては CORS です。簡単に言うと、クロスオリジン リソース共有は、フロントエンドが別のドメインまたはポートで実行されているバックエンドにリクエストを送信するのをブロックするブラウザのセキュリティ機能です。バックエンドとフロントエンドの両方をローカルホストで実行しているにもかかわらず、それらは異なるポートを使用するため、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 エージェントに接続するシンプルなチャット インターフェースを提供します。このフックを使用して、トークンの使用状況やツールの呼び出しを表示したり、会話をレンダリングしたりします。上記のシステム プロンプトでは、エージェントに応答をマークダウンで出力するように要求しているため、 <code>react-markdown</code>を使用して応答を適切にフォーマットします。</p><p></p><p>1.フロントエンド ディレクトリにいる間に、useChat() フックを使用するために @ai-sdk/react パッケージをインストールします。</p>npm install @ai-sdk/react<p>2. 同じディレクトリで、React Markdown をインストールして、エージェントが生成する応答を適切にフォーマットできるようにします。</p>npm install react-markdown<p>3. <code>useChat()</code>を実装します。このフックは、フロントエンドと AI エージェントのバックエンド間のやり取りを管理します。メッセージの状態、ユーザー入力、ステータスを処理し、監視の目的でライフサイクル フックを提供します。渡すオプションは次のとおりです。</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>コンポーネントに移動して、会話を行うための UI を作成します。次に、エージェントからの応答を適切にフォーマットするために、応答を<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 サーバーに移行します。これにより、エージェントは、私たちが提供する定義済みの検索機能に頼るのではなく、動的にクエリを構築できるようになります。これにより、より自然な言語ワークフローを使用できるようになり、すべての検索クエリを手動で記述する必要性が軽減されます。Elasticsearch MCP サーバーとエコシステムの現在の状態の詳細については、<a href="https://www.elastic.co/search-labs/blog/mcp-current-state">こちらを</a>ご覧ください。</p><p></p><p>これらの変更はすでに進行中ですので、お楽しみに!</p><h3><strong>まとめ</strong></h3><p></p><p>このブログでは、JavaScript、Mastra、Elasticsearch を使用して、ファンタジー バスケットボール チームに合わせた推奨事項を提供するエージェント RAG アシスタントを構築しました。取り上げた内容:</p><ul><li><p><strong>エージェント RAG の基礎</strong>と、AI エージェントの自律性と RAG を効果的に使用するツールを組み合わせることで、より繊細で動的なエージェントを実現できる方法について説明します。</p></li><li><p><strong>Elasticsearch</strong>とそのデータ ストレージ機能および強力なネイティブ集約により、それが LLM のナレッジ ベースとして優れたパートナーとなる理由について説明します。</p></li><li><p><strong>Mastra</strong>フレームワークと、それが JavaScript エコシステムの開発者にとってこれらのエージェントの構築をどのように簡素化するかについて説明します。</p></li></ul><p>あなたがバスケットボールの熱狂的なファンであっても、AI エージェントの構築方法を検討している方であっても、あるいは私のようにその両方であっても、このブログが、始めるための基礎を提供できれば幸いです。完全なリポジトリは<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 を正しく使う方法、パート II]]></title>
    <description><![CDATA[本番環境でのベストプラクティスと、コーディングエラーを減らすためにServerless環境でElasticsearch Node.jsクライアントを実行する方法についてご覧ください。 ]]></description>
    <content:encoded><![CDATA[<p>これは、JavaScript での Elasticsearch シリーズの第 2 部です。<a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i">最初の部分では、</a>環境を正しくセットアップし、Node.js クライアントを構成し、データをインデックスして検索する方法を学びました。この第 2 部では、実稼働のベスト プラクティスを実装し、サーバーレス環境で 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">Function-as-a-Service 環境でクライアントを実行する</a></p></li></ul></li></ul><p><a href="https://github.com/Delacrobix/JS-client-best-practices_article"><em><strong>ここで</strong></em></a><em> 例付きのソースコードを確認できます</em><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>自動テスト用に、モック ライブラリと<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>クライアントは、Serverless に接続するために次の調整を行います。</p><ul><li><p>スニッフィングをオフにし、スニッフィング関連のオプションを無視します</p></li><li><p>最初のノードを除いて、config で渡されたすべてのノードを無視し、ノードのフィルタリングと選択のオプションも無視します。</p></li><li><p>圧縮と `TLSv1_2_method` を有効にします（Elastic Cloud 用に構成した場合と同じ）</p></li><li><p>すべてのリクエストに `elastic-api-version` HTTP ヘッダーを追加します</p></li><li><p>デフォルトでは `WeightedConnectionPool` ではなく `CloudConnectionPool` を使用します</p></li><li><p>標準の MIME タイプを優先して、ベンダーの `content-type` および `accept` ヘッダーをオフにします。</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>Function-as-a-Service環境でクライアントを実行する方法</h3><p>この例では Node.js サーバーを使用しましたが、AWS lambda、GCP Run などの機能を備えた Function-as-a-Service 環境を使用して接続することもできます。</p>'use strict'

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

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

exports.handler = async function (event, context) {
  // use the client
}<p>もう 1 つの例は、同じくサーバーレスである 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>Function-as-a-Service を使用する意味は、24 時間 365 日稼働するサーバーとは異なり、関数は関数を実行するマシンのみを起動し、関数が終了するとマシンは休止モードになり、消費するリソースが少なくなることです。</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 を正しく使う方法、パート 1]]></title>
    <description><![CDATA[JavaScript で本番環境対応の Elasticsearch バックエンドを作成する方法を説明します。  

ElasticsearchをJavaScriptで使用して、クライアント/サーバーのベストプラクティスに従って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><a href="https://github.com/Delacrobix/JS-client-best-practices_article"><em><strong>ここで</strong></em></a><em> 例付きのソースコードを確認できます</em><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>は、Elasticsearch API からの HTTP REST 呼び出しを JavaScript に配置する JavaScript ライブラリです。これにより、処理が容易になり、ドキュメントのインデックス作成などのタスクを一括で簡素化するヘルパーが利用できるようになります。</p><h2>環境</h2><h3>フロントエンド、バックエンド、それともサーバーレス？</h3><p>JavaScript クライアントを使用して検索アプリを作成するには、Elasticsearch クラスターとクライアントを実行する JavaScript ランタイムという少なくとも 2 つのコンポーネントが必要です。</p><p>JavaScript クライアントはすべての Elasticsearch ソリューション (クラウド、オンプレミス、サーバーレス) をサポートしており、クライアントがすべてのバリエーションを内部で処理するため、ソリューション間に大きな違いはありません。そのため、どれを使用するかについて心配する必要はありません。</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>express</strong></em></a> : 軽量なNode.jsサーバーを立ち上げてElasticsearchを公開できるようになります</p></li><li><p><a href="https://www.npmjs.com/package/split2"><em><strong>split2</strong></em></a> : テキスト行をストリームに分割します。ndjsonファイルを1行ずつ処理するのに便利です</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>このコードは、ポート 3000 をリッスンし、認証用の API キーを使用して Elasticsearch クラスターに接続する基本的な Express.js サーバーをセットアップします。これには、GET リクエストを介してアクセスすると、Elasticsearch クライアントの<code>.info()</code>メソッドを使用して Elasticsearch クラスターに基本情報を照会する /ping エンドポイントが含まれています。</p><p>クエリが成功した場合は、クラスター情報が JSON 形式で返され、それ以外の場合はエラー メッセージが返されます。サーバーは、JSON リクエスト本体を処理するために body-parser ミドルウェアも使用します。</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">semantic_text</a>やフルテキストクエリ用の text などのマッピングを使用してドキュメントのインデックスを作成できます。これら 2 つのフィールド タイプを使用すると、<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>クライアントのもう 1 つの利点は、<a href="https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/client-helpers.html#bulk-helper">一括ヘルパーを</a>使用してインデックスを一括で作成できることです。バルク ヘルパーを使用すると、同時実行、再試行、関数を通過して成功または失敗した各ドキュメントの処理などを簡単に処理できます。</p><p>このヘルパーの魅力的な機能は、ストリームを操作できることです。この機能を使用すると、ファイル全体をメモリに保存して Elasticsearch に一度に送信するのではなく、ファイルを 1 行ずつ送信できます。</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>バルク ヘルパーがファイル行を Elasticsearch に送信する間、split2 を使用してファイル行をストリーミングします。</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を読み取りますファイルを 1 行ずつ読み込み、 <code>helpers.bulk</code>メソッドを使用して各 JSON オブジェクトを指定された Elasticsearch インデックスに一括インデックスします。<code>createReadStream</code>と<code>split2</code>を使用してファイルをストリーミングし、各ドキュメントのインデックス メタデータを設定し、処理に失敗したドキュメントをログに記録します。完了すると、正常にインデックスが作成されたアイテムの数を記録します。</p><p><code>indexData</code>関数を使用する代わりに、Kibana を使用して UI 経由でファイルを直接アップロードし、<a href="https://www.elastic.co/docs/manage-data/ingest/upload-data-files">データ ファイルのアップロード UI を使用することもできます。</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">パート 2</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>