<?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/kr/search-labs/blog/category/javascript-programming</link>
    </image>
    <link>https://www.elastic.co/kr/search-labs/blog/category/javascript-programming</link>
    <atom:link href="https://www.elastic.co/kr/search-labs/rss/category/javascript-programming.xml" rel="self" type="application/rss+xml"/>
    <language><![CDATA[kr]]></language>
    <lastBuildDate>Wed, 23 Sep 2026 02:34:47 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 웹 애플리케이션을 사용하여 에이전트 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">읽어보세요</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>에이전트 자체는 자바스크립트 에코시스템의 에이전트 프레임워크인 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>AI 에이전트는 다른 시스템과 상호 작용하고, 독립적으로 작동하며, 정의된 매개변수에 따라 작업을 수행할 수 있습니다. 에이전틱 RAG는 AI 에이전트의 자율성과 검색 증강 생성의 원칙을 결합하여 LLM이 응답을 생성하기 위해 호출할 도구와 컨텍스트로 사용할 데이터를 선택할 수 있도록 합니다. <a href="https://www.elastic.co/search-labs/blog/retrieval-augmented-generation-rag">여기에서</a> RAG에 대해 자세히 알아보세요.</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>다음은 해리슨 체이스(LangChain의 CEO)가 작성한 프레임워크 <a href="https://docs.google.com/spreadsheets/d/1B37VxTBuGLeTSPVWtz7UMsCdtXrqV5hCjWkbHN8tfAo/edit?gid=0#gid=0">비교표입니다</a>.</p><p>Mastra에 관심을 갖게 된 이유는 풀스택 개발자가 에이전트를 에코시스템에 쉽게 통합할 수 있도록 구축된 자바스크립트 우선 프레임워크라는 점입니다. Vercel의 AI-SDK도 이 작업의 대부분을 수행하지만, 프로젝트에 더 복잡한 에이전트 워크플로가 포함될 때 Mastra가 빛을 발합니다. Mastra는 AI-SDK에서 설정한 기본 패턴을 개선하며, 이 프로젝트에서는 이 두 가지를 함께 사용할 예정입니다.</p><h3><strong>프레임워크 및 모델 선택 고려 사항</strong></h3><p>이러한 프레임워크는 AI 에이전트를 빠르게 구축하는 데 도움이 될 수 있지만 고려해야 할 몇 가지 단점이 있습니다. 예를 들어, AI 에이전트나 일반적인 추상화 계층이 아닌 다른 프레임워크를 사용하면 제어권을 약간 잃게 됩니다. LLM이 도구를 올바르게 사용하지 않거나 원치 않는 작업을 수행하는 경우 추상화로 인해 디버깅이 더 어려워집니다. 하지만 제 생각에는 이러한 프레임워크가 모멘텀을 얻고 있고 지속적으로 반복되고 있기 때문에 구축할 때 얻을 수 있는 쉽고 빠른 속도를 고려하면 이러한 절충안은 가치가 있습니다.</p><p>다시 말하지만, 이러한 프레임워크는 모델에 구애받지 않으므로 다양한 모델을 플러그 앤 플레이할 수 있으며, 학습된 데이터 세트에 따라 모델이 달라지고 그에 따라 응답도 달라진다는 점을 기억하세요. 일부 모델은 도구 호출도 지원하지 않습니다. 따라서 여러 모델을 전환하고 테스트하여 어떤 모델이 가장 좋은 응답을 제공하는지 확인할 수 있지만 각 모델에 대해 시스템 프롬프트를 다시 작성해야 할 가능성이 높다는 점에 유의하세요. 예를 들어, Llama3.3 사용 보다 훨씬 더 많은 프롬프트와 구체적인 지침을 통해 원하는 응답을 얻을 수 있습니다.</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>백엔드(마스트라):</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(모델 컨텍스트 프로토콜) 서버로 이동하여 동적 쿼리를 활성화합니다.</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. 그런 다음 기본 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>.gitignore</code> 에 <code>.env</code> 을 추가하여 이 파일이 버전 관리에서 제외되었는지 확인하세요.</p><h4><strong>3단계: Elasticsearch 설정하기</strong></h4><p>먼저 활성 Elasticsearch 클러스터가 필요합니다. 두 가지 옵션이 있습니다:</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. 그런 다음 재사용 가능한 함수를 저장할 디렉터리 라이브러리를 만들고 그 안으로 이동합니다:</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와 일치시킬 수 있도록 일반적으로 API 호출을 통해 제공되는 플레이어 및 팀 메타데이터를 대체합니다. 샘플 데이터를 사용하기 때문에 외부 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">플레이어데이터인입.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>이는 플레이어의 통계를 서로 비교하기 위해 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. 아래 쿼리를 추가하여 특정 상대에 대한 플레이어의 기록 평균을 계산합니다. 이 쿼리는 <code>player_id</code> 과 일치하는 조건과 <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">비교플레이어.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> 스키마를 사용합니다. 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라고 합니다. 간단히 말해, 교차 출처 리소스 공유는 프론트엔드에서 다른 도메인이나 포트에서 실행되는 백엔드에 요청을 하지 못하도록 차단하는 브라우저 보안 기능입니다. 로컬호스트에서 백엔드와 프론트엔드를 모두 실행하지만 서로 다른 포트를 사용하므로 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. 프론트엔드 디렉터리에서 @ai-sdk/react 패키지를 설치하여 useChat() 훅을 사용합니다.</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. 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> 사항과 AI 에이전트의 자율성과 RAG를 효과적으로 사용할 수 있는 도구를 결합하여 보다 미묘하고 역동적인 에이전트를 만드는 방법에 대해 알아보세요.</p></li><li><p>데이터 저장 기능과 강력한 기본 집계 기능을 갖춘 <strong>Elasticsearch가 </strong>어떻게 LLM의 지식 베이스로서 훌륭한 파트너가 되는지 알아보세요.</p></li><li><p>자바스크립트 에코시스템에서 개발자를 위해 이러한 에이전트 구축을 간소화하는 <strong>Mastra </strong>프레임워크와 그 방법을 소개합니다.</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[올바른 방법으로 자바스크립트에서 Elasticsearch, 2부]]></title>
    <description><![CDATA[서버리스 환경에서 Elasticsearch Node.js 클라이언트를 실행하는 방법과 프로덕션 모범 사례를 학습하여 코딩 오류를 줄이는 방법을 알아보세요. ]]></description>
    <content:encoded><![CDATA[<p>이번 글은 자바스크립트에서의 Elasticsearch 시리즈의 두 번째 파트입니다. <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 서버리스에서 클라이언트 실행</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></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>자동화된 테스트를 위해 모의 라이브러리와 <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 서버리스에</a> 대한 연결도 지원합니다.</p><p>Elastic Cloud Serverless를 사용하면 Elastic이 내부적으로 처리하므로 인프라에 대해 걱정할 필요가 없는 프로젝트를 만들 수 있으며, 사용자는 색인하려는 데이터와 액세스 기간에 대해서만 걱정하면 됩니다.</p><p>사용 관점에서 서버리스는 스토리지에서 컴퓨팅을 분리하여 <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`를 활성화합니다(Elastic Cloud에 대해 구성할 때와 동일).</p></li><li><p>모든 요청에 `elastic-api-version` HTTP 헤더를 추가합니다.</p></li><li><p>기본적으로 '가중치 연결 풀' 대신 '클라우드 연결 풀'을 사용합니다.</p></li><li><p>표준 MIME 유형을 위해 공급업체의 `content-type` 및 `accept` 헤더를 끕니다.</p></li></ul><p>서버리스 프로젝트를 연결하려면 서버모드: 서버리스 매개변수를 사용해야 합니다.</p>const { Client } = require('@elastic/elasticsearch')
const client = new Client({
  node: 'ELASTICSEARCH_ENDPOINT',
  auth: { apiKey: 'ELASTICSEARCH_API_KEY' },
  serverMode: "serverless",
});<h3>서비스형 함수(FaaS) 환경에서 클라이언트를 실행하는 방법</h3><p>이 예제에서는 Node.js 서버를 사용했지만 AWS 람다, GCP 실행 등과 같은 기능을 갖춘 서비스형 기능 환경을 사용하여 연결할 수도 있습니다.</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 폴더에 있으며 서버 측에서 실행되므로 클라이언트는 검색어에 해당하는 'text' 매개변수만 제어할 수 있습니다.</p><p>함수형 서비스 사용의 의미는 24시간 연중무휴로 실행되는 서버와 달리 함수는 해당 기능을 실행하는 컴퓨터만 불러오고, 작업이 끝나면 컴퓨터가 휴식 모드로 전환되어 리소스를 덜 소비한다는 것입니다.</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 서버리스와 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[올바른 방법으로 자바스크립트에서 Elasticsearch, 파트 1]]></title>
    <description><![CDATA[자바스크립트로 프로덕션에 사용할 수 있는 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></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> Elasticsearch API의 HTTP REST 호출을 JavaScript로 변환하는 JavaScript 라이브러리입니다. 이렇게 하면 문서를 일괄 색인하는 것과 같은 작업을 간소화하는 도우미를 더 쉽게 처리하고 사용할 수 있습니다.</p><h2>환경</h2><h3>프론트엔드, 백엔드 또는 서버리스?</h3><p>JavaScript 클라이언트를 사용하여 검색 앱을 생성하려면 최소한 두 가지 구성 요소, 즉 Elasticsearch 클러스터와 클라이언트를 실행하기 위한 JavaScript 런타임이 필요합니다.</p><p>JavaScript 클라이언트는 모든 Elasticsearch 솔루션(클라우드, 온프레미스, 서버리스)을 지원하며, 클라이언트가 내부적으로 모든 변형을 처리하므로 어떤 것을 사용할지 고민할 필요가 없습니다.</p><p>하지만 자바스크립트 런타임은 <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>: 경량 노드js 서버를 회전시켜 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>이 코드는 포트 3000에서 수신 대기하고 인증을 위해 API 키를 사용하여 Elasticsearch 클러스터에 연결하는 기본 Express.js 서버를 설정합니다. 여기에는 GET 요청을 통해 액세스할 때 Elasticsearch 클라이언트의 <code>.info()</code> 메서드를 사용하여 기본 정보를 위해 Elasticsearch 클러스터에 쿼리하는 /ping 엔드포인트가 포함되어 있습니다. </p><p>쿼리가 성공하면 클러스터 정보를 JSON 형식으로 반환하고, 그렇지 않으면 오류 메시지를 반환합니다. 또한 서버는 본문 파서 미들웨어를 사용하여 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">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">데이터 파일 업로드 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>파란색 스포츠 티셔츠를 찾고 있습니다.</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>하이브리드&lt;query_term&gt; 쿼리(/검색/하이브리드?q=)</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>