<?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[Elasticsearch Labs]]></title>
        <description><![CDATA[Elastic 검색 팀의 기사 및 튜토리얼]]></description>
        <link>https://www.elastic.co/search-labs/kr</link>
        <image>
            <url>https://www.elastic.co/search-labs/favicon.svg</url>
            <title>Elasticsearch Labs</title>
            <link>https://www.elastic.co/search-labs/kr</link>
        </image>
        <generator>Elasticsearch Labs</generator>
        <lastBuildDate>Sun, 19 Apr 2026 22:38:00 GMT</lastBuildDate>
        <atom:link href="https://www.elastic.co/search-labs/kr/rss/feed" rel="self" type="application/rss+xml"/>
        <copyright><![CDATA[© 2026. Elasticsearch B.V. 모든 권리 보유]]></copyright>
        <language><![CDATA[kr]]></language>
        <item>
            <title><![CDATA[Elasticsearch와 Jina 임베딩을 활용한 비지도형 문서 클러스터링]]></title>
            <description><![CDATA[Elasticsearch와 Jina 임베딩을 활용한 비지도형 문서 클러스터링을 위한 실용적이고 재현 가능한 접근 방식.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/unsupervised-document-clustering-elasticsearch-jina-embeddings</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/unsupervised-document-clustering-elasticsearch-jina-embeddings</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Matthew Adams]]></dc:creator>
            <pubDate>Fri, 10 Apr 2026 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/662c00694c3e0c2fb2128098bdb6813df9e86a72-1280x720.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>벡터 검색은 보통 쿼리에서 시작됩니다. 하지만 쿼리가 없다면 어떻게 해야 할까요?</p><p>기업은 고객 지원 티켓, 법률 문서, 뉴스 피드, 연구 논문 등 방대한 양의 문서를 축적하며, 올바른 질문을 던지기 위해서는 먼저 그 문서에 어떤 내용이 담겨 있는지 정확히 파악해야 합니다 레이블이나 학습 데이터가 없는 상태에서 수천 건의 문서를 수동으로 검토하는 것은 현실적으로 불가능합니다. 무엇을 검색해야 할지 모를 때는 기존 검색 방식이 도움이 되지 않습니다.</p><p>이 게시물에서는 이러한 탐색 문제를 해결하는 Elasticsearch 기반의 비지도형 문서 클러스터링 및 시간적 스토리 추적 방식을 안내합니다. 이 과정을 마치고 나면, 여러 날에 걸쳐 전개되는 스토리의 흐름을 다음과 같이 추적할 수 있습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/100a60a7fb85da8ab3813fd071a82c93f2c3f318-1300x650.png" style="width: 100%;" /><p><strong>다룰 내용:</strong></p><ul><li>쿼리 없이 주제를 탐색할 때, (검색용 임베딩이 아닌) <strong>클러스터링 임베딩</strong>을 사용하는 것이 왜 중요한가요?</li><li>Elasticsearch의 k-최근접 이웃(kNN)과 배치(Batched) <code>msearch</code>을(를) 활용하여, 밀도 조사 중심 분류 방식이 문서를 주제별로 그룹화하는 방법.</li><li>모델 학습 없이도 주제를 파악할 수 있도록, <a href="https://www.elastic.co/docs/reference/aggregations/search-aggregations-bucket-significanttext-aggregation"><code>significant_text</code></a>이(가) 클러스터에 자동 레이블 지정을 수행하는 방법.</li><li>시간적 스토리 체인이 일별 클러스터를 연결하여, 주제가 매일 어떻게 진화하는지를 보여 줍니다.</li></ul><div style="display:none">Unknown block type "infoPanel", specify a component for it in the `components.types` option</div><p>이 파이프라인은 BBC 뉴스 및 The Guardian에서 제공하는 2025년 2월 기사 약 8,500개를 테스트 코퍼스로 사용합니다. 뉴스는 시간적 흐름이 명확하기 때문에 활용하기 편리하지만, 이러한 패턴은 법률 검토, 규정 준수 모니터링, 연구 종합, 고객 지원 분류 등 문서 탐색이 중요한 모든 곳에 적용될 수 있습니다.</p><p><strong>스택:</strong></p><ul><li><a href="https://www.elastic.co/search-labs/blog/jina-embeddings-v5-text"><strong>Jina v5</strong></a> <strong>클러스터링 임베딩</strong>: 주제 그룹화를 위한 작업별 LoRA(Low-Rank Adaptation) 어댑터. <a href="https://www.elastic.co/blog/elastic-jina-ai">Jina가 Elastic과 함께하게 되었습니다</a>. <a href="https://www.elastic.co/docs/explore-analyze/elastic-inference/eis">Elastic Inference Service(EIS)</a>에서 Jina 모델을 네이티브로 사용할 수 있습니다.</li><li><strong>Elasticsearch</strong>: 확장 가능한 <a href="https://www.elastic.co/docs/solutions/search/vector/knn">kNN</a>, <code>significant_text</code> 레이블 지정, 및 벡터 스토리지.</li><li><a href="https://www.elastic.co/search-labs/blog/diskbbq-elasticsearch-introduction"><strong>DiskBBQ:</strong></a> 근사 최근접 이웃(ANN) 검색 가속화를 위해 <a href="https://www.elastic.co/docs/reference/elasticsearch/index-settings/bbq">더 나은 이진 양자화(BBQ)</a>와 계층적 K-평균 분할을 결합한 디스크 기반 벡터 인덱스 포맷입니다. 이 인덱스 분할은 벡터 검색 내부에서 이루어지며, 이 게시물에서 사용된 밀도 조사 클러스터링 알고리즘과는 별개입니다. <code>bbq_disk</code>은(는) 양자화된 벡터를 디스크에 저장하고 힙(Heap)에는 분할 메타데이터만 유지합니다. 덕분에 <code>bbq_hnsw</code>과(와) 비교했을 때 높은 재현율을 유지하면서도 리소스 요구 사양을 획기적으로 낮출 수 있습니다.</li><li><strong>전역 클러스터링 + 일별 시간적 연결:</strong> 탐색 및 스토리 진화.</li></ul><p><strong>준비 사항:</strong></p><ul><li>Elasticsearch 배포(Elastic Cloud, Elasticsearch Serverless 또는 Elastic Self-Managed 8.18+/9.0+) <code>bbq_disk</code> 을(를) 사용하려면 8.18 이상이 필요합니다. 선택 사항인 다양성 기반 검색기 섹션은 9.3 이상 또는 서버리스가 필요합니다.</li><li><a href="https://jina.ai/embeddings/">Jina API 키</a>Jina API 키: 무료 티어에는 1,000만 토큰이 포함되어 있으며, 이는 핵심 클러스터링 파이프라인(약 425만 토큰)을 감당할 수 있습니다. 선택 사항인 검색과 클러스터링 비교에는 두 번째 임베딩 패스를 사용합니다.</li><li><a href="https://bonobo.capi.gutools.co.uk/register/developer">Guardian API 키</a> (무료).</li></ul><h2>설정</h2><p>필요한 패키지 설치:</p><pre><code>pip install elasticsearch pandas numpy plotly umap-learn python-dotenv pydantic-settings datasets requests</code></pre><p>선택 사항(이 리포지토리에서 스크래핑 도우미를 실행하는 경우에만):</p><pre><code>pip install beautifulsoup4</code></pre><p>그런 다음 프로젝트 루트에 있는 <code>.env</code> 파일에 API 키를 구성하세요.</p><pre><code>ELASTIC_CLOUD_ID=your-cloud-id        # or ELASTIC_HOST=https://...
ELASTIC_API_KEY=your-api-key
JINA_API_KEY=your-jina-key
GUARDIAN_API_KEY=your-guardian-key</code></pre><p>이 노트북은 <code>load_dotenv(override=True)</code>을(를) 호출하므로, 로컬 <code>.env</code> 값이 우선적으로 적용됩니다.</p><pre><code>Connected to Elasticsearch</code></pre><h2>1부: 탐색 클러스터링 - 왜 임베딩을 클러스터링해야 할까요?</h2><p>대부분의 벡터 검색은 <em>쿼리</em>와 관련 <em>문서</em> 간의 일치 여부를 판단하도록 학습된 <strong>검색용 임베딩</strong>을 사용합니다. 검색에는 완벽하지만, 탐색에는 적합하지 않습니다. 쿼리 없이도 코퍼스에 어떤 주제가 존재하는지 찾으려면 유사한 문서를 함께 그룹화하는 임베딩이 필요합니다.</p><p>Jina v5는 <strong>작업별 LoRA(Low-Rank Adaptation) 어댑터</strong>를 통해 이 문제를 해결합니다. LoRA는 모델의 기본 가중치 대부분을 고정한 채, 대상 내부 레이어에 소규모의 저순위 업데이트를 추가합니다. 이를 통해 모델 전체를 재학습시키지 않고도 특정 작업에 맞춰 모델의 동작을 변화시킬 수 있습니다. 동일한 기본 모델이라도 <code>task</code> 매개변수 설정에 따라 서로 다른 임베딩을 생성합니다.</p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">작업</td><td style="border: 1px solid #ccc; padding: 4px;">학습 용도</td><td style="border: 1px solid #ccc; padding: 4px;">사용 사례</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">retrieval.passage</td><td style="border: 1px solid #ccc; padding: 4px;">쿼리-문서 일치</td><td style="border: 1px solid #ccc; padding: 4px;">검색, Retrieval-Augmented Generation(RAG)</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">클러스터링</td><td style="border: 1px solid #ccc; padding: 4px;">주제 그룹화(긴밀한 클러스터에 최적화됨)</td><td style="border: 1px solid #ccc; padding: 4px;">탐색, 분류</td>
                    </tr>
              </tbody>
            </table>
          <p>클러스터링 어댑터는 동일한 주제의 문서들은 임베딩 공간에서 <em>더 가깝게</em>, 서로 다른 주제의 문서들은 <em>더 멀리</em> 배치되도록 학습되었습니다. 아래의 시각적 비교 자료를 통해 그 차이를 명확히 확인할 수 있습니다.</p><h3>검색과 클러스터링: 시각적 비교</h3><p>그 차이를 확인하기 위해, 두 가지 작업 유형을 각각 모두 적용하여 문서 샘플을 임베딩했습니다. 클러스터링은 원본 데이터인 1024차원의 임베딩 공간에서 수행됩니다. UMAP(Uniform Manifold Approximation and Projection)은 시각화를 위해 이러한 임베딩을 2차원으로 투영하는 용도로만 사용됩니다. UMAP은 데이터의 지역적 이웃 구조를 보존하므로, 클러스터 간의 분리를 비교하는 데 유용합니다.</p><p>아래는 동일한 480개의 문서 샘플에 두 가지 작업 유형을 각각 모두 적용하여 임베딩한 후, UMAP을 통해 2차원으로 투영한 결과입니다. 클러스터링 패널에서 더 긴밀하고, 더 명확히 분리된 색상 그룹을 찾습니다.</p><pre><code>    Full dataset: 8,495 articles
    Sources: guardian: 5749, bbc: 2746
    Date range: 2025-02-01 to 2025-02-28


    Sample: 480 docs across 8 sections
    section
    Film              60
    World news        60
    Australia news    60
    Opinion           60
    Football          60
    US news           60
    Sport             60
    Business          60


    Clustering embeddings: 480
    Retrieval embeddings:  480


    UMAP projection complete</code></pre><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/9bcf7a744900560c1c6c63a2dc3af2f9bfd33e11-1100x500.png" style="width: 100%;" /><p><em>검색용 임베딩(왼쪽)은 주제를 넓게 퍼뜨리는 반면, 클러스터링용 임베딩(오른쪽)은 동일한 문서에서도 더 긴밀하고 명확하게 구분된 그룹을 형성합니다.</em></p><p>클러스터링 임베딩은 더 긴밀하고 시각적으로도 훨씬 뚜렷하게 구별되는 그룹을 생성합니다. 검색 임베딩은 주제를 더 고르게 분산시켜 검색(세밀한 유사도)에 이상적이지만, 탐색의 경우 긴밀한 주제별 클러스터가 중요합니다.</p><p>이것이 바로 본 단계별 안내 나머지 과정에서 <code>task=&quot;clustering&quot;</code>을(를) 사용하는 이유입니다.</p><h3>데이터셋 로딩</h3><p>코퍼스는 2025년 2월의 두 뉴스 소스를 결합합니다.</p><ul><li><strong>BBC 뉴스</strong>: <a href="https://huggingface.co/datasets/RealTimeData/bbc_news_alltime">RealTimeData/bbc_news_alltime</a> HuggingFace 데이터셋 활용.</li><li><strong>The Guardian</strong>: <a href="https://open-platform.theguardian.com/">Guardian Open Platform API</a>활용.</li></ul><p>여러 소스의 데이터를 사용하면, 클러스터링이 <em>소스만의 특정 스타일</em>이 아닌 <em>주제</em>를 제대로 찾아내는지 검증하는 데 도움이 됩니다.</p><pre><code>    Total articles:  8,495
    
    Source breakdown:
    source
    guardian    5749
    bbc         2746
    
    Date range: 2025-02-01 → 2025-02-28
    Days covered: 28
    
    Sample article:
      Source:  guardian
      Title:   Carbon monoxide poisoning ruled out in death of Gene Hackman and wife, police sa
      Section: Film
      Text:    Authorities have ruled out that Gene Hackman and his wife, Betsy Arakawa, died from carbon monoxide poisoning earlier this week in their home in Santa Fe, New Mexico. The Santa Fe county sheriff, Adan...</code></pre><h3>클러스터링 작업을 통한 임베딩</h3><p>모든 문서에 대해 Jina v5 API는 <code>task=&quot;clustering&quot;</code> (으)로 호출됩니다. 임베딩은 디스크에 캐시되므로 이후 실행에서는 API를 완전히 건너뜁니다.</p><p>API 호출은 간단합니다. <code>task</code> 매개변수는 일반적인 임베딩 사용 방식과 구분되는 가장 핵심적인 차이점입니다.</p><pre><code>payload = {
    "model": "jina-embeddings-v5-text-small",
    "input": texts,
    "task": "clustering",  # ← This selects the clustering LoRA adapter
}</code></pre><p>아래 실행 시간은 캐시 적중이 적용되었습니다. API를 처음 실행할 때 걸리는 시간은 코퍼스 크기에 따라 달라집니다.</p><pre><code>    Embeddings ready: 8,495 vectors of dimension 1024
    Time: 0.6s</code></pre><h3>단일 Elasticsearch 인덱스로 색인</h3><p>탐색 클러스터링을 위해, 한 달 전체 데이터를 하나의 인덱스(<code>docs-clustering-all</code>)에 저장합니다. 일일 분할은 시간적 스토리 연결을 위해 나중에 이루어집니다.</p><p>인덱스 매핑 시 벡터 필드에 <a href="https://www.elastic.co/docs/reference/elasticsearch/index-settings/bbq"><code>bbq_disk</code></a> (을)를 사용합니다.</p><pre><code>{
  "embedding": {
    "type": "dense_vector",
    "dims": 1024,
    "index": true,
    "similarity": "cosine",
    "index_options": {
      "type": "bbq_disk"        // hierarchical k-means partitioning for ANN index lookup; separate from this post's clustering algorithm
    }
  }
}</code></pre><p>1024차원의 float32 벡터는 용량이 4KB입니다. <a href="https://www.elastic.co/search-labs/blog/diskbbq-elasticsearch-introduction"><code>bbq_disk</code></a>(은)는 계층적 K-평균을 사용하여 벡터를 작은 클러스터로 나누고, 이를 이진 양자화하여 저장한 뒤, 재계산을 위해 전체 정밀도 벡터를 디스크에 보관합니다. 오직 분할 메타데이터만 힙에 상주하므로, 방대한 코퍼스를 처리할 때도 메모리 사용량을 낮게 유지할 수 있습니다. 더 많은 힙을 감당할 수 있는 워크로드의 경우, <a href="https://www.elastic.co/docs/reference/elasticsearch/index-settings/bbq"><code>bbq_hnsw</code></a>(은)는 리소스 비용이 더 높지만 더 빠른 검색이 가능한 계층적으로 탐색 가능 작은 세계(HNSW) 그래프를 생성합니다.</p><p><a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/dense-vector"><code>dense_vector</code></a> 필드 타입은 다양한 양자화 전략을 지원합니다. 여기서 사용된 1024차원 벡터와 같은 고차원 임베딩에는 <code>bbq_disk</code> 및 <code>bbq_hnsw</code> 방식이 가장 적합합니다.</p><pre><code>    Indexed 8,495 documents into docs-clustering-all
    Time: 57.5s</code></pre><h3>클러스터링: 밀도 조사 중심 분류</h3><p>HDBSCAN과 같은 전통적인 클러스터링 알고리즘은 N×d 크기의 전체 벡터 행렬을 메모리에 올려 둘 수 있고, 전체 데이터를 반복적으로 훑으며 업데이트를 수행할 수 있다는 가정하에 작동합니다. 1,024차원의 문서 8,495개라면 충분히 감당할 수 있는 크기(35MB 수준)입니다. 하지만 추가적인 인프라 없이는 수백만 건의 문서로 확장하기 어려운 접근 방식입니다.</p><p>이 알고리즘은 개념적으로 Voronoi 할당과 노이즈 플로어를 사용하는 KMeans++ 초기화 방식과 유사하지만, Elasticsearch <a href="https://www.elastic.co/docs/solutions/search/vector/knn">kNN 검색</a>을 연산 프리미티브로 사용하여 거의 모든 작업을 서버 측에서 처리합니다.</p><ol><li><strong>전체 문서의 5%를 밀도 조사용으로 샘플링합니다</strong>(무작위 샘플, 최소 50개).</li><li>배치 처리된<a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-msearch"><strong><code>msearch</code></strong></a> <strong>kNN을 통해 밀도를 조사합니다.</strong> 각 조사 지점에서 kNN 쿼리를 실행해 주변의 평균 유사도를 기록합니다. 높은 평균 유사도는 임베딩 공간의 밀집 영역을 의미합니다. <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-msearch"><code>msearch</code></a>(은)는 단일 HTTP 호출로 여러 검색 요청을 보낼 수 있는데, 이 기능이 여기서 매우 중요합니다. 밀도 조사 과정에서 수백 개의 kNN 쿼리가 생성되는데, 이를 배치로 처리함으로써 요청당 발생하는 오버헤드를 방지할 수 있기 때문입니다.</li><li><strong>다양성을 갖춘 고밀도 시드 선택</strong>: 밀도 중앙값 이상의 후보들을 밀도 내림차순으로 정렬한 뒤, 모든 기존 시드와의 코사인 유사도가 분리 임계값보다 낮은 경우에만 탐욕적으로 채택합니다. 이것이 유일한 클라이언트 측 연산입니다.(문서 8,000개 기준 약 0.01초 소요)</li><li><strong><code>msearch</code></strong>  <strong>kNN</strong>을 <strong>통해 모든 문서를 중심 기준으로 분류</strong>: 각 시드가 중심 역할을 수행하며, kNN 검색을 통해 유사도 임계값 이상의 인접 문서를 찾아냅니다. 각 문서는 가장 높은 점수를 반환한 중심에 할당됩니다. 규모가 작은 클러스터들은 해체되어 노이즈로 처리됩니다.</li></ol><p>고난도 작업은 Elasticsearch가 처리합니다. 즉, <code>msearch</code>(은)는 밀도 조사에, <code>msearch</code>(은)는 분류에, 그리고 <code>significant_text</code>(은)는 레이블 지정에 활용됩니다. 이 코퍼스(총 8,495개 문서)의 경우, 5%의 밀도 조사용 샘플을 통해 425개의 kNN 조사 쿼리가 실행됩니다. <code>msearch</code>(은)는 이를 9개의 HTTP 호출(배치 크기 50)로 배치 처리함으로써, 개별 조사마다 발생할 수 있는 요청 오버헤드를 방지합니다. 여기에 <code>bbq_disk</code>의 ANN 조회가 결합되어, 클러스터링 단계를 빠르고 확장 가능하게 유지해 줍니다. 클러스터링 단계에서는 속도를 위해 <a href="https://www.elastic.co/docs/deploy-manage/production-guidance/optimize-performance/approximate-knn-search"><code>num_candidates</code></a> 값을 최소화하여 kNN 쿼리를 실행합니다. 반면, 프로덕션 검색 쿼리에서는 지연 시간이 늘어나더라도 재현율을 높이기 위해 더 큰 <code>num_candidates</code> 값을 사용해야 합니다.</p><p>클러스터의 크기는 엄격한 <code>k</code> 제한이 아니라, 각 중심 주변의 임베딩 공간 밀도에 따라 자연스럽게 결정됩니다. 밀집된 주제 영역은 더 큰 클러스터를 형성하며, 지엽적인 주제 영역은 더 작은 클러스터를 형성합니다.</p><h4>KMeans나 HDBSCAN을 사용하지 않는 이유는 무엇일까요?</h4><p>KMeans는 구형 클러스터를 가정하며 전체 N×d 행렬을 메모리에 저장해야 합니다. 반면, 메모리에 수용 가능한 크기의 코퍼스라면 <a href="https://scikit-learn.org/stable/modules/generated/sklearn.cluster.HDBSCAN.html">HDBSCAN</a>이 강력한 대안이 됩니다. 이는 클러스터의 모양에 구애받지 않으며, 밀도 시맨틱이 잘 정립되어 있습니다.</p><p>밀도 조사 중심 접근 방식은 다른 지엽적인 영역을 공략합니다. 바로 저장 공간, 검색, 클러스터링을 하나의 시스템 내에서 통합 처리하려 하거나, 데이터 규모 때문에 클라이언트 측에서의 행렬 연산이 실용적이지 못한 경우입니다. Elasticsearch kNN을 컴퓨팅 기본 요소로 사용하고, 임의의 클러스터 크기를 처리하며, 거의 모든 계산을 서버 측에서 수행합니다.</p><pre><code>    Clustered global index in 31.6s
      Total clusters: 82
      Total noise:    2420 (28.5%)
      Density probes: 425 kNN queries via 9 _msearch HTTP calls</code></pre><h4>노이즈 비율에 대한 이해</h4><p>약 28%의 노이즈 발생률은 의도적으로 설계된 것이며, 오류 모드가 아닙니다. 구성된 <code>similarity_threshold</code> 기준에 따라 어떤 밀집 클러스터에도 적합하지 않은 문서는, 어설픈 매칭을 강행하는 대신 미할당 상태로 남겨 둡니다. 이는 일종의 품질 검사기 역할을 합니다. 사설 칼럼, 단신, 혹은 일회성 기사들은 응집력 있는 그룹을 형성할 만큼의 주제적 밀도가 낮기 때문에 자연스럽게 클러스터링을 거부하게 됩니다.</p><p>이 임계값은 조정이 가능합니다. <code>similarity_threshold</code>(을)를 낮추면 더 공격적인 클러스터링이 이루어지며 (더 많은 문서가 할당되지만 군집의 결속력은 느슨해짐) 반대로 높이면 클러스터가 더 긴밀해지는 대신 노이즈 비율이 증가합니다. 다양한 뉴스 콘텐츠가 섞인 이번 코퍼스의 경우, 약 30%의 노이즈 비율은 운용상 적절한 수치입니다. 실제 배포 환경에서는 도메인별 품질 기준에 맞춰 임계값을 조정해야 합니다.</p><h3>significant_text를 활용한 자동 레이블</h3><p>이제 각 클러스터에 사람이 읽을 수 있는 레이블을 지정해야 합니다. Elasticsearch의 <code>significant_text</code> 집계는 백그라운드 세트(전체 코퍼스)와 비교했을 때, 포그라운드 세트(클러스터) 내에서 유독 자주 등장하는 용어들을 찾아냅니다.</p><p>내부적으로는 머신 러닝이나 거대 언어 모델(LLM) 호출 없이, 절대 빈도와 상대 빈도의 변화를 균형 있게 고려하는 통계적 휴리스틱(기본값으로 JLH 점수 사용)을 활용합니다 영국 정치에 관한 클러스터라면 <code>starmer</code>, <code>labour</code>, <code>downing</code> 같은 용어들이 등장할 것입니다. 이 용어들이 전체 뉴스 코퍼스에 비해 해당 클러스터 내에서 압도적으로 높은 비중을 차지하기 때문입니다.</p><p>이번 전체 프로세스에서 레이블은 <code>docs-clustering-all</code>을(를) 기준으로 직접 계산됩니다. 따라서 포그라운드와 백그라운드 모두 한 달 전체 분량에서 추출됩니다. 2부의 레이블링 작업에는 일별 인덱스 패턴(<code>docs-clustering-*</code>)이 사용됩니다. 이 와일드카드 패턴은 쿼리가 모든 일치 인덱스를 동시에 쿼리할 수 있게 하여, <code>significant_text</code>이(가) 더 넓은 백그라운드 바탕으로 더 명확한 대비 효과를 얻을 수 있도록 합니다.</p><p>최소한의 쿼리 형태는 다음과 같습니다.</p><pre><code>{
  "size": 0,
  "query": { "term": { "cluster_id": "72" } },
  "aggs": {
    "label_terms": {
      "significant_text": {
        "field": "text",
        "size": 5,
        "filter_duplicate_text": true
      }
    }
  }
}</code></pre><p><code>significant_text</code> 또한 품질 검사기 역할도 합니다. 유미의한 용어를 생성하지 않는 클러스터는 구별되는 어휘가 없습니다. 이는 일관성 없는 그룹으로, 오해의 소지가 있는 레이블을 지정하기보다는 다시 노이즈로 해체되어야 합니다.</p><p>가벼운 결정론적 정제 단계를 거쳐 노이즈가 섞인 레이블 용어(숫자 토큰, 일반 단어 등)를 제거하며, 필요한 경우 대표적인 헤드라인을 대신 사용합니다. 이를 통해 레이블의 가독성을 높이면서도 Elasticsearch 레이블을 네이티브로 유지할 수 있습니다.</p><pre><code>    Sample cluster labels:
      cluster   3  (200 docs)  arsenal | mikel | villa
      cluster   1  (198 docs)  volodymyr | ukrainian | kyiv
      cluster   0  (196 docs)  hostages | hamas | israeli
      cluster   4  (187 docs)  scrum | rugby | borthwick
      cluster  52  (185 docs)  fossil | renewable | renewables
      cluster  10  (156 docs)  labour | gwynne | mps
      cluster  40  (151 docs)  novel | novels | literary
      cluster  11  (149 docs)  mewis | sarina | wiegman
      cluster  44  (143 docs)  flooding | rainfall | rain
      cluster  13  (131 docs)  doge | musk | elon
      cluster  12  (128 docs)  murder | insp | knockholt
      cluster   5  (124 docs)  putin | backstop | starmer


    Reassigned 35 docs from incoherent clusters to noise
    Total docs: 8,495
    Clustered:  6,040 (71.1%)
    Noise:      2,455 (28.9%)</code></pre><h3>클러스터 시각화</h3><p>아래 시각화는 전체 클러스터링 과정에서 발견된 결과물입니다. 클러스터링된 문서와 노이즈 문서의 날짜별 구성 현황, 한 달 전체 데이터의 UMAP 투영도, 그리고 클러스터가이 소스가 아닌 주제를 기준으로 형성되었음을 보여 주는 소스 혼합 차트를 확인할 수 있습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/99099f5adaa945ae4097c50b0d7151c7dd28872e-1000x400.png" style="width: 100%;" /><p>2025년 2월 전체 기간에 걸친 클러스터링된 문서와 노이즈 문서의 일일 분포.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/477c6c7177942955a942f85f5c881da50e517915-1100x700.png" style="width: 100%;" /><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/4f4302556c8974c416842452cf33bca06e90b966-1100x700.png" style="width: 100%;" /><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/327f96a79e382ef30614cb0570aa7fccd822b8f8-1100x700.png" style="width: 100%;" /><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/acfb1524a10e24d6ff2412e7c3ec0f2b3ac75193-900x600.png" style="width: 100%;" /><p>UMAP 상의 각 색상별 섬은 클러스터를 나타냅니다. 이는 오직 임베딩 유사도만을 기반으로 발견된, 동일한 주제에 관한 기사 그룹입니다. 회색으로 표시된 노이즈 지점은 어떤 클러스터에도 명확히 속하지 않는 기사들입니다.(주로 짧은 사설, 또는 단발성 뉴스인 경우가 많음)</p><p>소스 분류 차트를 통해 각 클러스터에 BBC News와 The Guardian의 기사가 <strong>모두</strong> 포함되어 있음을 확인할 수 있습니다. 이 클러스터링 시스템은 <em>소스</em>가 아닌 <em>주제</em>를 찾아내고 있으며, 이는 비지도형 탐색이 생성해야 하는 가장 이상적인 결과입니다.</p><h3>다양성 기반 검색기(diversify retriever)를 사용하여 클러스터 범위를 탐색</h3><p>일반 kNN은 클러스터의 중심(밀집된 핵심)과 가장 유사한 문서를 반환합니다. 그러나 실제 클러스터는 하위 주제를 다룹니다. <a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/retrievers/diversify-retriever"><strong>다양성 기반 검색기</strong></a>는 최대 한계 관련성(MMR)을 사용하여, 클러스터 중심과 관련성이 높으면서도 <em>서로 다른</em> 문서들을 찾아냅니다.</p><p>핵심 매개변수는 <strong>λ(람다)</strong>입니다.</p><ul><li>λ = 1.0 → 순수한 관련성(일반 kNN과 동일)입니다.</li><li>λ = 0.0 → 순수한 다양성 (최대한 분산된 결과).</li><li>λ=0.5 → 균형: 주제와 관련이 있지만 다른 관점을 다룹니다.</li></ul><div style="display:none">Unknown block type "infoPanel", specify a component for it in the `components.types` option</div><p>최소한의 검색기 요청 형태는 다음과 같습니다.</p><pre><code>{
  "size": 8,
  "retriever": {
    "diversify": {
      "type": "mmr",
      "field": "embedding",
      "lambda": 0.5,
      "query_vector": "<cluster-centroid-vector>",
      "retriever": {
        "knn": {
          "field": "embedding",
          "query_vector": "<cluster-centroid-vector>",
          "k": 50,
          "num_candidates": 100
        }
      }
    }
  }
}</code></pre><p>다양성 수준에서 <code>type</code>, <code>field</code>, <code>query_vector</code> 매개변수는 필수입니다. <code>field</code>(은)는 결과 간 유사도를 측정할 때 어떤 dense_vector 필드를 사용할지 MMR에 지시하며, <code>query_vector</code>(은)는 정확도 점수 산정의 기준점을 제공합니다.</p><p>이를 통해 단순히 &#x27;이 클러스터의 중심 내용이 무엇인가?&#x27;를 파악하는 수준을 넘어, &#x27;이 클러스터가 실제로 무엇을 다루고 있는가?&#x27; 라는 질문에 답할 수 있습니다.</p><pre><code>    Exploring cluster 52 (185 docs)
    Label: fossil | renewable | renewables
    Centroid computed (dim=1024)


    ========================================================================
    Plain kNN (closest to centroid)
    ========================================================================
      1. [0.9738] Green campaigners fear ministers are poised to award billions of pounds in fresh subsidies to Drax power station, despite strong concerns...
      2. [0.9710] Thirteen more oil and gas licences could be cancelled as ministers decide new guidance for fossil fuel extraction after a landmark court...
      3. [0.9699] Experts have accused the fossil fuel industry of seeking special treatment after lobbyists argued greenhouse gas emissions from oilfields...
      4. [0.9681] Burning wood is a terrible way of producing electricity . Chopping down trees destroys habitats for wildlife, and growing new trees cannot...
      5. [0.9649] Keir Starmer will do huge damage to the global fight against climate change if he gives in to political pressure and allows the development...
      6. [0.9641] Labour will next week be confronted with stark policy choices that threaten to expose the fault lines between the Treasury and the...
      7. [0.9638] The Drax power station near Selby in north Yorkshire burns imported wood pellets  The government has agreed a new funding arrangement with...
      8. [0.9581] If you care about the world we are handing on to future generations, the news on Thursday morning was dramatic. This January was the...
    
    ========================================================================
    Diversify retriever (MMR, lambda=0.5)
    ========================================================================
      1. [0.9738] Green campaigners fear ministers are poised to award billions of pounds in fresh subsidies to Drax power station, despite strong concerns...
      2. [0.9434] Oil and gas interests have waged a coordinated campaign to kill pro-electrification policies that ban gas connections in new buildings ,...
      3. [0.9303] It was interesting to read that new licences for oil and gas production in the North Sea are being delayed by legal action ( Thirteen more...
      4. [0.9139] The US energy secretary, Chris Wright, has said he “would love to see Australia get in the game of supplying uranium and maybe going down...
      5. [0.9077] Rachel Reeves was facing criticism on Saturday night as it was confirmed that a report she cited as evidence that a third ­runway at...
      6. [0.8996] When Margaret Thatcher opened the Hadley Centre for Climate Change in 1990 journalists suggested she was attempting to appear to be doing...
      7. [0.8993] The vast majority of governments are likely to miss a looming deadline to file vital plans that will determine whether or not the world has...
      8. [0.8987] European imports of seaborne gas shipments fell by a fifth last year to their lowest level since the pandemic, according to a new report,...
    
    Overlap: 1/8 documents appear in both result sets
    
    Avg pairwise similarity (lower = more diverse):
      Plain kNN:          0.9057
      Diversify retriever: 0.6965</code></pre><p>일반 kNN 결과는 주제의 한 가지 관점에 대해서만 클러스터를 이룹니다. 즉, 중심 및 서로 간의 유사도가 가장 높은 문서들만 한데 모이게 됩니다. 다양성 기반 검색기는 동일한 클러스터의 서로 다른 패싯, 즉 하위 주제, 서로 다른 소스, 다양한 관점을 보여 줍니다.</p><p>다양성 지표가 이를 수치적으로 증명합니다. 다양성 기반 검색기 결과의 평균 쌍별 유사성이 더 낮게 나타나는데, 이는 검색된 문서가 더 넓은 범위를 다룬다는 것을 의미합니다.</p><p>이는 다음과 같은 경우에 유용합니다.</p><ul><li><strong>클러스터가 실제로 포괄하는 범위가 어디까지인지 이해하려면</strong>, 단순히 중심부뿐만 아니라 그 외곽 경계까지 살펴봐야 합니다.</li><li><strong>요약 생성</strong>. 다양한 대표 문서가 LLM에 더 나은 자료를 제공합니다.</li><li>사람이 직접 검토하거나 후속 레이블 지정에 사용할 <strong>대표 사례들을 찾아냅니다</strong>.</li><li><strong>품질 검사</strong>. 다양한 결과가 일관성이 없어 보이면 클러스터를 분할해야 할 수도 있습니다.</li></ul><h2>2부: 시간적 스토리 체인</h2><h3>일자별 스토리 추적</h3><p>1부에서는 주제 탐색을 위해 한달 전체를 전역으로 클러스터링했습니다. 시간적 흐름을 파악하기 위해, 동일한 밀도 조사 중심 분류를 <strong>일별 인덱스</strong>에 대해 매일 독립적으로 실행하면, 인접한 날짜 간에 클러스터가 서로 연결됩니다. 참고로 일별 클러스터는 1부의 전역 클러스터와는 별개입니다. 각 날짜마다 그날의 콘텐츠에 맞게 조정된 자체 클러스터 할당과 레이블이 생성됩니다.</p><h4><strong>연결 방식: 샘플링 후 쿼리</strong></h4><p>A일의 각 클러스터에 대하여:</p><ol><li>몇 개의 대표 문서들을 샘플링합니다.</li><li>B일의 인덱스를 대상으로 kNN 검색을 실행합니다.</li><li>B일의 각 클러스터에 검색 결과가 몇 건이나 도달하는지 계산합니다.</li><li>검색 결과의 비율이 임계값(kNN fraction ≥ 0.4)을 초과하면, 두 클러스터 사이의 링크를 기록합니다.</li></ol><p>이 과정은 빠르며(전체 문서가 아닌 클러스터당 몇 개의 문서만 쿼리하기 때문) Elasticsearch의 네이티브 kNN을 사용하므로 외부 도구가 필요하지 않습니다.</p><pre><code>Preparing daily indices for temporal linkage...


Indexed 8,495 docs into 28 daily indices


Temporal links found: 808 in 145.4s

Strongest links:
  2025.02.01 'league | arsenal | premier' -> 2025.02.02 'league | season | striker'  (100%)
  2025.02.03 'league | striker | loan' -> 2025.02.04 'league | striker | season'  (100%)
  2025.02.03 'score | operator | gedling' -> 2025.02.04 'league | striker | season'  (100%)
  2025.02.12 'playoff | leg | bayern' -> 2025.02.13 'league | players | injury'  (100%)
  2025.02.14 'league | injury | football' -> 2025.02.15 'league | premier | football'  (100%)
  2025.02.18 'russia | ukraine | talks' -> 2025.02.19 'saudi | russia | arabia'  (100%)
  2025.02.18 'football | league | bayern' -> 2025.02.19 'league | manchester | players'  (100%)
  2025.02.21 'league | premier | manchester' -> 2025.02.22 'game | players | defeat'  (100%)
  2025.02.21 'rugby | calcutta | brilliant' -> 2025.02.22 'game | players | defeat'  (100%)
  2025.02.26 'metals | kyiv | ukrainian' -> 2025.02.27 'ukraine | russia | talks'  (100%)</code></pre><p>kNN 비율이 100%라는 것은 소스 클러스터에서 샘플링된 모든 문서가 동일한 타겟 클러스터에 도달했음을 의미하며, 이는 일자 간에 형성될 수 있는 가장 강력한 링크입니다. 위에서 확인된 대부분의 링크는 축구 관련 내용입니다. 이는 타당한 결과인데, 프리미어 리그 보도는 매일 발생하며 주제의 일관성 또한 매우 높기 때문입니다.</p><p><code>score | operator | gedling</code> → <code>league | striker | season</code>(으)로 이어지는 링크는 지엽적인 로컬 축구 클러스터(Gedling은 비리그 구단임)가 다음 날 더 광범위한 프리미어 리그 클러스터로 흡수되는 과정을 보여 주는 사례입니다. 이는 매일 다른 세부 단위로 다시 클러스터링이 이루어질 때 나타나는 자연스러운 현상입니다.</p><h3>스토리 체인 구축</h3><p>스토리 체인은 연속된 날짜에 걸쳐 서로 연결된 클러스터들의 시퀀스입니다.</p><p>각각의 쌍별 링크를 통해 월요일의 &#x27;영국 정치&#x27; 클러스터가 화요일의 클러스터와 연결된다는 사실을 알 수 있습니다. 월요일에 시작된 이야기가 한 주 동안 어떻게 전개되고, 금요일에 이르러 어떻게 잦아드는지, 체인을 통해 스토리의 전체 흐름을 파악할 수 있습니다.</p><p>체인은 kNN 비율 0.4 이상의 링크를 바탕으로 탐욕적 방식으로 구축됩니다. 이는 소스 클러스터에서 샘플링된 문서 중 최소 40%가 단일 타겟 클러스터에 매칭되었음을 의미합니다. 가장 이른 시점의 클러스터부터 시작하여, 알고리즘은 항상 가장 강력한 출력 링크를 따라갑니다.<br/></p><pre><code>    Strong links (kNN fraction >= 0.4): 244
    Story chains spanning 3+ days: 18
      Chain 1: 'ukrainian | kyiv | eastern' (19 days: Feb 3 → Feb 21)
      Chain 2: 'playing | opposition' (19 days: Feb 10 → Feb 28)
      Chain 3: 'tadhg | maro | cadan' (10 days: Feb 1 → Feb 10)
      Chain 4: 'invade | china | putin' (8 days: Feb 21 → Feb 28)
      Chain 5: 'elected | labour | leader' (7 days: Feb 12 → Feb 18)
      Chain 6: 'film | swift | awards' (6 days: Feb 2 → Feb 7)
      Chain 7: 'amendment | termination | reporting' (6 days: Feb 12 → Feb 17)
      Chain 8: 'officers | scene | police' (5 days: Feb 1 → Feb 5)</code></pre><p>가장 긴 스토리 체인은 우크라이나-러시아 관련 보도를 19일 연속으로 추적했는데, 2025년 2월의 지속적인 지정학적 긴장 수위를 고려하면 당연한 결과라 할 수 있습니다. 두 번째로 긴 체인은 한 달 중 19일 동안 이어지는 프리미어 리그 축구 소식을 추적합니다. 더 짧은 체인으로는 시상식 시즌(영화/시상식, 6일), 식스 네이션스 럭비(10일), 그리고 영국 정치 리더십 관련 보도(7일) 등이 포착되었습니다. 각 체인은 알고리즘이 일자별 인덱스 간의 임베딩 유사도만을 바탕으로 순수하게 찾아낸 스토리의 흐름을 나타냅니다.</p><h3>Sankey: 스토리 흐름 시각화</h3><p>Sankey 다이어그램은 링크의 폭으로 연결 강도를 표현하는 흐름 시각화 도구입니다. 여기에서 각 수직 밴드는 하루를, 각 노드는 일별 클러스터(문서 수에 따른 크기)를 나타내며, 각각의 색상 경로는 시간을 따라 이어지는 하나의 스토리 체인을 추적합니다. 링크의 폭은 kNN 중첩 강도를 나타냅니다. 링크가 굵을수록 샘플링된 더 많은 문서가 타겟 클러스터에 도달했음을 의미합니다. 체인마다 색상이 일치하므로, 왼쪽에서 오른쪽으로 이어지는 하나의 색상 경로는 하나의 이야기 진행을 나타냅니다.</p><p>예를 들어, (가장 긴 경로 중 하나로 시각화된) 우크라이나-러시아 체인은 2월 초부터 셋째 주까지 계속해서 이어지며, 링크의 폭이 지속적으로 굵게 유지되는 것은 일자별로 강력한 주제적 연속성이 있었음을 보여 줍니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/100a60a7fb85da8ab3813fd071a82c93f2c3f318-1300x650.png" style="width: 100%;" /><p><em>2025년 2월 한 달간 전개되는 시간적 스토리 체인. 각 색상별 경로는 며칠 동안 지속되는 스토리를 나타내며, 링크 폭은 k-NN 중첩 강도를 보여 줍니다.</em></p><h2>이 접근 방식이 제공하는 이점</h2><p>이 단계별 안내에서는 Elasticsearch를 활용하여 비지도형 문서 클러스터링 파이프라인의 전 과정을 살펴보았습니다.</p><ol><li><strong>임베딩 클러스터링</strong>: Jina v5의 작업별 어댑터는 단순한 쿼리-문서 매칭을 넘어, 주제별 그룹화에 최적화된 임베딩을 생성합니다.</li><li><strong>전역 탐색 클러스터링</strong>: 한 달 전체를 하나의 인덱스에 클러스터링하면 일별 주제 탐색을 극대화할 수 있습니다.</li><li><strong>밀도 조사 중심 분류</strong>: 5%를 샘플링하고, <code>msearch</code> kNN을 통해 밀도를 조사하며, 밀도가 높은 다양한 시드를 선택하고, 모든 문서를 이 중심에 맞춰 분류합니다. 연산량이 많아 무거운 작업은 Elasticsearch가 처리하며, 시드 선택 과정만 클라이언트 측에서 실행됩니다.(약 0.01초 소요)</li><li><a href="https://www.elastic.co/docs/reference/aggregations/search-aggregations-bucket-significanttext-aggregation"><strong><code>significant_text</code></strong></a> <strong>레이블 지정</strong>: 유의성 검정을 통해 ML 모델이나 수동 주석 없이도 의미 있는 클러스터 레이블을 생성합니다. 유의미한 용어를 생성하지 않는 클러스터는 일관성이 없으며 노이즈로 강등됩니다. 이는 시스템에 내장된 품질 검사기입니다.</li><li><strong>시간적 스토리 연결</strong>: 일일 인덱스와 샘플 및 쿼리 교차 인덱스 kNN은 시간이 지남에 따라 스토리가 어떻게 진화하는지 추적합니다.</li></ol><p><strong>핵심 사항:</strong></p><ul><li>임베딩 작업 유형 설정이 핵심입니다. 클러스터링 임베딩을 사용하면 주제별 그룹이 훨씬 더 긴밀하게 형성됩니다.</li><li>Elasticsearch는 <a href="https://www.elastic.co/docs/solutions/search/vector/knn">kNN 검색</a>을 통해 저장 계층 <em>및</em> 클러스터링 엔진 역할을 수행할 수 있습니다.</li><li>밀도 조사 중심 분류는 거의 모든 연산을 서버 측에서 처리하며, 임베딩 공간의 밀도에 따라 자연스러운 크기의 클러스터를 생성합니다.</li><li><code>significant_text</code> 빠르고 해석 가능하며 자동 레이블링과 품질 검사 모두에 효과적입니다.</li></ul><p><strong>이 방식이 유용한 경우:</strong></p><ul><li>타임스탬프가 찍힌 텍스트는 있지만, 레이블이 지정된 학습 데이터 없이 주제를 탐색하고 싶을 때 유용합니다.</li><li>저장 공간, 벡터 검색, 레이블 지정 및 시간적 연결을 하나의 스택으로 해결하고 싶을 때 적합합니다.</li></ul><p><strong>탐색할 확장 기능:</strong></p><ul><li>다중 기간 클러스터링(주간, 월간 단위 집계)</li><li>클러스터 할당을 점진적으로 수행하여 실시간 데이터 수집이 가능합니다.</li><li>significant_text 용어를 시드로 활용해 LLM이 클러스터 요약문을 생성합니다.</li><li>규모가 커지면, 샘플링된 KMeans 중심은 밀도 기반 클러스터링을 위한 웜 스타트 시드로 사용될 수 있어, 조사 단계 비용을 줄여 줍니다.</li></ul><h2>직접 사용해 보기</h2><p>자신만의 타임스탬프 기반의 문서 코퍼스를 넣어 보세요. 날짜 정보가 포함된 텍스트 모음이라면 무엇이든 이 파이프라인에서 바로 작동합니다. 전체 노트북과 지원 코드는 <a href="https://github.com/elastic/elasticsearch-labs/tree/main/supporting-blog-content/unsupervised-document-clustering-elasticsearch-jina-embeddings">제공된 리포지토리</a>에서 확인할 수 있습니다.</p><ul><li><a href="https://cloud.elastic.co/registration?onboarding_token=vectorsearch&amp;cta=cloud-registration&amp;tech=trial&amp;plcmt=article%20content&amp;pg=search-labs"><strong>무료 Elastic Cloud 체험판 시작하기</strong></a>: 단 몇 분 만에 <code>bbq_disk</code> 기능이 지원되는 관리형 클러스터를 실행할 수 있습니다.</li><li><a href="https://www.elastic.co/elasticsearch/serverless"><strong>Elasticsearch Serverless 사용해 보기</strong></a>: 클러스터 관리가 필요 없으며, 자동으로 확장하고, 이 단계별 안내의 모든 기능을 지원합니다.</li></ul>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[전자 상거래 검색에 거버넌스가 필요한 이유]]></title>
            <description><![CDATA[거버넌스가 없는 전자 상거래 검색이 왜 한계에 부딪히는지, 그리고 제어 계층이 어떻게 예측 가능하고 의도 기반한 결과를 보장하여 검색 품질을 향상하는지 알아보세요.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/ecommerce-search-governance-improve-retrieval</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/ecommerce-search-governance-improve-retrieval</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Alexander Marquardt, Honza Král, Taylor Roy]]></dc:creator>
            <pubDate>Thu, 09 Apr 2026 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/3793259b01a5653a7520393a2f006610de0d21e7-1280x720.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>전자 상거래 소매 업체는 동일한 시스템 내에서 근본적으로 서로 다른 다양한 쿼리 유형을 처리해야 합니다. 고객이 &#x27;오렌지&#x27;를 검색할 때는 과일 그 자체를 기대하는 것이지, 오렌지 주스나 오렌지 마멀레이드처럼 &#x27;오렌지&#x27;라는 단어가 포함된 상품이나 오렌지와 의미적으로 유사한 다른 감귤류 제품을 찾는 것이 아닙니다. ‘단것을 좋아하는 할아버지를 위한 선물’을 검색하는 고객에게는 단순한 키워드 매칭이 아니라, 의미론적 검색이 필요합니다.</p><p><em>어휘 검색</em>(텍스트 매칭), <em>의미 검색</em>(개념 매칭), 그리고 이 둘을 결합한 <em>하이브리드 검색</em>도 그 자체만으로는 이러한 문제를 해결할 수 없습니다. 어휘 검색은 &#x27;오렌지&#x27;라는 단어가 포함된 것이라면 무엇이든 반환할 수 있고, 반대로 &#x27;오렌지&#x27;처럼 고의도 쿼리에 대해 순수 의미 검색을 적용하면 레몬이나 자몽 같은 연관 상품까지 검색 범위가 지나치게 넓어질 수 있습니다. 하이브리드 검색은 어휘 신호와 의미 신호를 혼합하지만, 해당 쿼리를 목적형으로 간주할지, 어떤 제약 조건을 적용할지, 혹은 어떤 비즈니스 정책을 적용할지는 여전히 결정하지 못합니다. 결국 문제는 검색 기술 그 자체에 있는 것이 아닙니다. 이 쿼리가 어떤 성격인지 파악하고, 검색이 시작되기도 전에 어떤 제약 조건을 적용해야 하는지 판단하는 거버넌스 계층이 없다는 것이 핵심 문제입니다.</p><p>이 블로그에서는 전자 상거래 검색 거버넌스란 무엇인지, 왜 중요한지, 그리고 제어 계층이 어떻게 예측 가능하고 정확한 검색 결과를 보장하는지 자세히 살펴봅니다.</p><h2>전자 상거래 검색에서 거버넌스가 의미하는 것</h2><p>이러한 맥락에서 <em>거버넌스</em>는 사용자의 쿼리와 검색 엔진 사이에 의사 결정 계층을 도입하는 것을 의미합니다. 이 계층은 다음과 같은 기능을 수행합니다.</p><ul><li>쿼리 의도 분류: 이 검색이 목적형(&#x27;오렌지&#x27;)인가요, 아니면 발견형(&#x27;할아버지를 위한 선물&#x27;)인가요?</li><li>비즈니스 제약 조건 적용: 어떤 카테고리 경계, 적격성 규칙, 재고 가용성 제약, 또는 머천다이징 정책을 적용해야 할까요?</li><li>적절한 전략으로 가는 방법: 어휘 검색, 의미 검색, 하이브리드 중 어떤 것을 사용해야 할까요?</li></ul><p>거버넌스 계층은 각 쿼리에 사용할 검색 접근 방식, 적용해야 하는 제약 조건, 검색을 시작하기 전에 적용해야 하는 비즈니스 정책을 결정합니다. 거버넌스와 하이브리드 검색을 혼동하지 않는 것이 중요합니다. 하이브리드 검색은 어휘와 의미 신호를 결합한 하나의 검색 전략일 뿐이지만, 거버넌스는 어휘, 의미, 하이브리드 중 어느 방식을 사용할지 결정하는 상위 의사 결정 계층입니다.</p><h2>현재 상태: 애플리케이션 계층 &quot;스파게티&quot; 구현</h2><p>현재 많은 소매 업체에서 이 문제를 해결하기 위해 애플리케이션 계층에 직접 로직을 추가하려고 시도합니다. 이로 인해 종종 하드코딩된 수천 줄의 if-then 문, 정규 표현식 및 복잡한 검색 템플릿으로 뒤엉킨 <em>스파게티 코드</em>라는 결과가 초래됩니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/f532b099ee103458e15563a711dae92952f8df02-1024x765.png" style="width: 100%;" /><p>이러한 방식은 앞서 살펴본 것처럼 원하는 검색 결과를 제공할 수는 있지만, 운영상 상당한 마찰을 초래한다는 문제가 있습니다.</p><ul><li><strong>엔지니어링 의존성</strong>: 비즈니스 사용자와 머천다이저는 엔지니어링 티켓을 발행하고 대개 몇 주씩 걸리는 긴 배포 주기를 거치지 않고서는 검색 동작을 조금도 수정할 수 없습니다.</li><li><strong>파편화:</strong> 검색 로직이 애플리케이션 코드와 검색 템플릿 사이에 흩어져 있으면 논리적 근거를 설명하거나 감사하기가 어려워지며, 이는 결국 발전 과정에 위험을 초래합니다.</li></ul><p>팀에서 라우팅의 필요성을 인식하더라도 &#x27;어떤 검색 방법을 선택해야 하는가&#x27;라는 잘못된 질문에 초점을 맞추는 경우가 많습니다.</p><h2>잘못된 논쟁: 어휘 검색, 의미 검색, 그리고 하이브리드 검색 사이의 선택</h2><p>검색 팀은 흔히 &#x27;어휘/BM25 검색이냐, 의미/벡터 검색이냐, 아니면 하이브리드냐&#x27;와 같이 어떤 검색 전략을 채택할 것인가를 결정하는 것이 가장 큰 과제라고 생각하곤 합니다. 이러한 관점이 이해되지 않는 것은 아닙니다(검색 방식은 중요합니다). 하지만 이는 실제 배포 환경에서 가장 흔히 발생하는 실패 유형을 간과하고 있습니다. 바로 모든 쿼리에 단일 검색 방식만 사용하다가 결국 최적의 결과를 내지 못하는 상황입니다.</p><p>전자 상거래 검색은 근본적으로 서로 다른 의도가 혼합된 것입니다.</p><ul><li><strong>확정적인 고의도 목적형 검색</strong>(&#x27;오렌지&#x27;, &#x27;우유&#x27;, &#x27;땅콩 없는 초콜릿&#x27;, &#x27;저렴한 올리브 오일&#x27;).</li><li><strong>탐색형 검색</strong>(&#x27;산악용 재킷&#x27;, &#x27;로봇 공학을 좋아하는 12세 어린이를 위한 선물&#x27;).</li><li><strong>운영상의 제약 조건</strong>(재고 가용성, 크기, 가격, 색상).</li><li><strong>머천다이징 및 캠페인</strong>(상위 노출, 하위 노출, 시즌 캠페인).</li></ul><p>시스템이 이처럼 다양한 의도를 모두 동일한 검색 전략으로 라우팅하면, 결과는 흔히 계통적인 오류를 범하게 됩니다. 이는 예측 가능한 실패인데, 운영 모델에 거버넌스가 부재하여 발생하기 때문입니다. 팀이 이를 거버넌스의 공백으로 인식하지 못하면, 결국 팀이 가진 유일한 해결 수단인 추가적인 조정으로 대응합니다.</p><h2>&#x27;정확도 조정&#x27;이 악순환이 되는 이유</h2><p>라우팅 계층이 없으면, &#x27;정확도 개선&#x27; 작업은 흔히 끝이 보이지 않는 무한한 백로그로 변질되곤 합니다.</p><ul><li>이 쿼리에서 핵심 제품 위에 액세서리가 표시되는 이유는 무엇인가요?</li><li>왜 갑자기 이 헤드 쿼리에서 관련 항목들이 나타나기 시작했나요?</li><li>동의어를 추가하고, 분석기를 조정하거나 하이브리드 검색을 활성화했는데, 왜 검색 결과가 변한 걸까요?</li><li>검색어 하나를 수정하는 데 왜 비즈니스 팀이 엔지니어링 배포까지 기다려야 하나요?</li></ul><p>팀은 동의어 추가, 상위 노출, 순위 재지정 실험, 애플리케이션 코드 내 예외 로직 삽입과 같은 더 많은 조정으로 대응합니다. 이 방식이 당분간은 통할지 모르나, 시스템이 여전히 쿼리 유형을 결정하고 검색 전에 올바른 제약 조건을 적용하는 명시적인 결정 계층이 부족하기 때문에 취약한 동작 일으킵니다.</p><h2>전자 상거래 의도 파헤치기: 헤드와 테일</h2><p>이 섹션에서는 전자 상거래에서 흔히 나타나는 목적형(Navigational) 및 탐색형(Exploratory) 쿼리 패턴을 실무적으로 구분하기 위해, 이를 각각 ‘헤드(Head)’와 ‘테일(Tail)’이라는 약어로 지칭하겠습니다. 실제 환경에서는 많은 쿼리가 두 가지 측면을 모두 포함하는 경우가 많습니다.</p><h3>헤드 쿼리(확정적 의도)</h3><p>이는 사용자가 원하는 바를 정확히 알고 입력하는 직접적이고 목적성 뚜렷한 쿼리입니다.</p><ul><li>단일 항목 의도 (&#x27;오렌지&#x27;, &#x27;우유&#x27;, &#x27;빵&#x27;).</li><li>정확한 브랜드 또는 제품군(&#x27;iPhone 15 Pro&#x27; &#x27;다이어트 콜라&#x27;).</li><li>SKU, 모델 번호, 크기(&#x27;ABC123&#x27;, &#x27;air max 270&#x27;).</li></ul><p>이런 쿼리들의 경우, 어휘 검색만으로도 토큰 대응(일치하는 단어)을 충분히 처리할 수 있습니다. 하지만 비즈니스는 제약 조건을 준수하고, 예측 가능한 순위를 반환하며, 결과를 직접 제어할 수 있기를 기대합니다. 머천다이저는 쿼리가 정확한 카테고리 범위 내에서 도출되고, 구매 자격을 준수하며, 특정 비즈니스 우선순위가 반영되도록 보장해야 합니다.</p><p>의도한 도출 결과를 시행하기 위해서는 거버넌스가 필요합니다. 예를 들어, &#x27;오렌지&#x27;는 오렌지 주스, 오렌지 마멀레이드, 오렌지 소다가 아니라 반드시 농산물 카테고리로 연결되어야 합니다.</p><h3>테일 쿼리(탐색형 검색)</h3><p>이는 쇼핑객이 상품을 탐색할 때 나타나는 설명적이고 의도가 명확한 쿼리입니다.</p><ul><li>‘단것을 좋아하는 할아버지를 위한 선물’</li><li>&#x27;산악용 외투&#x27;</li><li>&#x27;오래 서 있어도 편한 신발&#x27;</li></ul><p>이러한 쿼리에서는 어휘 검색이 제 성능을 발휘하지 못하는 경우가 많습니다. 의미 검색은 단어가 정확히 일치하지 않더라도 쿼리의 개념과 상품을 연결할 수 있기 때문에 이러한 경우에 매우 탁월합니다. 그러나 의미 검색만으로도 충분하지 않은 경우도 많습니다. 실제 쿼리에서는 어떤 검색 방식을 사용하든 상관없이, 반드시 준수되어야 하는 제약 조건들이 있기 마련입니다.</p><h2>제약 조건은 검색 방식과 무관</h2><p>의미 검색에 제약 조건을 적용하는 것이 <em>하이브리드 검색</em>을 의미하는 것은 아닙니다. 이것들은 서로 무관합니다. Elasticsearch의 필터 및 상위 노출 같은 제약 조건은 어휘, 의미 또는 하이브리드 검색에 모두 적용할 수 있습니다. 관건은 이 쿼리를 어떻게 해석할지, 어떤 제약 조건을 적용할지, 그리고 어떤 검색 전략을 사용할지 결정하는 것입니다.</p><p>아래는 검색과 엄격한 제약 조건을 결합한 쿼리의 몇 가지 예시입니다.</p><ul><li><strong>오렌지:</strong> &#x27;오렌지&#x27;에 대한 어휘 검색과 &#x27;과일&#x27; 또는 &#x27;농산물&#x27;과 같은 카테고리 제약 조건을 추가하여 오렌지 마멀레이드, 오렌지 주스, 오렌지 소다를 제외합니다.</li><li><strong>4달러 미만의 비타민 C 함량이 높은 과일:</strong> 영양학적 의도에 기반한 의미 검색을 수행하되, 결과를 과일 카테고리 및 4달러 미만 제품으로 제한하는 제약 조건을 적용합니다.</li><li><strong>업무용 편안한 신발:</strong> 문맥적 의도에 따른 의미 검색을 수행하되, 결과를 신발로 제한하는 제약 조건을 적용합니다.</li></ul><p>이러한 쿼리는 단일 방법으로 처리할 수 없습니다.</p><ul><li><strong>순수 어휘 검색</strong>만으로는 충분하지 않은 경우가 많습니다. &#x27;비타민 C 함량이 높은&#x27;이나 &#x27;편안한&#x27; 같은 문구들은 깔끔하고 구조화된 속성으로 존재하지 않을 수 있기 때문입니다. 제품 설명, 리뷰 또는 사양을 통해 추론해야 할 수도 있습니다.</li><li>또한 <strong>순수 의미 검색</strong>으로도 항상 충분하지 않습니다. 명시적인 제약 조건이 없으면 &#x27;비타민 C 함량이 높은 과일&#x27;과 같은 쿼리가 의도한 카테고리와 가격 범위를 넘어 비타민 보충제, 과일 맛 음료 또는 비타민 함량이 높은 채소로 확대될 수 있습니다.</li></ul><p>거버넌스 계층은 쿼리에 어휘 검색, 의미 이해, 제약 조건 적용 또는 이것들의 조합이 필요한지를 결정합니다. 이 계층이 없으면 전자 상거래 팀은 다음과 같은 상황에 처할 수 있습니다.</p><ul><li><strong>과도한 제약:</strong> 의미 요청에 어휘 검색을 사용(예: &#x27;할아버지를 위한 선물&#x27;)합니다.</li><li><strong>제약 조건 미달: </strong>고의도 헤드 쿼리(예: &#x27;오렌지&#x27;)에 의미 쿼리를 사용합니다.</li></ul><p>거버넌스의 과제는 각 쿼리 유형에 대해 적절한 판단을 내릴 수 있는 시스템을 구축하는 것입니다.</p><h2>거버넌스가 부재할 경우 발생하는 문제</h2><p>가장 흔한 실패 유형은 단순합니다. 바로 중간 거버넌스 계층 없이, 사용자 쿼리 그대로를 단일 검색 전략(어휘, 의미 또는 하이브리드)에 직접 전달하는 것입니다.</p><h3>의도한 결과를 도출하지 못하는 어휘 검색</h3><p>사용자가 &#x27;오렌지&#x27;를 검색할 때, 어휘 검색 전략은 해당 토큰이 포함된 모든 상품, 즉 오렌지 주스, 오렌지 마멀레이드, 오렌지 소다 등을 모두 결과로 반환할 수 있습니다. 시스템은 용어를 올바르게 일치시켰지만, 거버넌스 없이는 의도한 쇼핑 맥락(과일)을 도출하지 못할 수 있습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/99abc7a46f9c56a26a68d0a089d7ab830b9b5568-1560x814.png" style="width: 100%;" /><h3>의도한 제약 조건을 넘어 확장하는 의미 검색</h3><p>사용자가 &#x27;오렌지&#x27;를 검색할 때, 의미 시스템은 주변 제품 개념 전반에 걸쳐 개념적으로 관련된 항목을 검색할 수 있습니다. 시스템은 더 넓은 영역(과일이나 농산물)을 정확히 이해할 수 있지만, 명시적인 거버넌스 없이는 여전히 사용자가 의도한 제약 조건(특히 오렌지)을 넘어 범위가 지나치게 넓어질 수 있습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/c9de86363ecbed499fe48259f47b3c5b2c26bc43-1568x796.png" style="width: 100%;" /><h3>문제의 핵심은 결국 거버넌스</h3><p>필요한 것은 검색이 시작되기 전, 쿼리의 의도를 파악하고 적절한 제약 조건을 강제하는 업스트림 의사 결정 계층입니다. 이렇게 하면 다음과 같은 문제가 해결됩니다.</p><ul><li>사용자가 실제로 원했던 항목과 유사하거나 관련된 항목이 함께 표시됩니다.</li><li>모호한 카테고리 경계(예: &#x27;음료&#x27;와 &#x27;농산물&#x27;의 혼선).</li><li>시즌별 상위 노출이나 캠페인 실행 불가.</li><li>예측할 수 없고 설명할 수 없는 결과.</li></ul><h2>의도 이해 및 라우팅: 반드시 필요한 제어 평면</h2><p>거버넌스가 적용된 검색 시스템은 검색 실행 전(Elasticsearch에서 쿼리를 수행하기 전) 단계에 경량화된 제어 평면을 도입합니다. 제어에 대한 자세한 내용은 이번 블로그 시리즈의 3부와 4부에서 다룰 예정입니다. 지금은 이 계층이 어떻게 작동하는지보다는 무엇을 할 수 있는지를 중심으로 살펴보겠습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/88c3d0f9731a128d73a765dcdffed897308110a6-2680x766.png" style="width: 100%;" /><p>제어 평면은 의도를 파악하고, 비즈니스 정책을 적용하며, 다음과 같이 적절한 검색 전략을 보장할 수 있습니다.</p><p><strong>1. 의도 신호 감지</strong></p><ul><li>이 쿼리는 목적을 위한 것인가요, 아니면 발견을 위한 것인가요?</li><li>이미 알고 있는 헤드 쿼리(우유, 빵, 바나나)인가요?</li><li>이미 알고 있는 제품, 브랜드 또는 카테고리 해석이 있나요?(예: &#x27;오렌지&#x27;는 농산물로 도출해야 함).</li><li>쿼리가 SKU와 유사한 패턴인가요?</li><li>쿼리가 현재 진행 중인 캠페인이나 시즌 정책에 해당하나요?(예: 크리스마스 기간 중 칠면조 관련 검색 결과에 상위 노출)</li><li>해당 쿼리에 제약 조건(카테고리, 속성, 제외 항목, 가격/크기/색상)이 포함되어 있습니까?</li></ul><p><strong>2. 거버넌스 및 비즈니스 정책 적용</strong></p><ul><li>확정적 제약 조건(카테고리/속성/제외 조건/재고 가용성)을 먼저 적용합니다.</li><li>활성화된 머천다이징 정책(상위 노출, 하위 노출, 위치 고정, 결과 재정의)을 적용합니다.</li><li>우선순위 규칙에 따라 충돌을 해결합니다(예: 캠페인 재정의와 글로벌 정책 간의 충돌).</li></ul><p><strong>3. 적절한 검색 전략으로 라우팅</strong></p><ul><li>목적형/고의도 헤드 쿼리를 위한 어휘(빠르고 확정적)입니다.</li><li>진정한 발견형 쿼리를 위한 의미 검색입니다.</li><li>명시적인 비즈니스 제약 조건하에, 어휘 및 의미 신호를 결합하여 가치를 더하는 하이브리드 방식입니다.</li></ul><p>실제로, 제어 평면의 출력은 단순히 &#x27;하이브리드 사용&#x27; 또는 &#x27;의미 사용&#x27;이 아닙니다. 이것이 바로 거버넌스 기반의 검색 계획입니다. 즉, 쇼핑객의 의도를 해석하고, 적용해야 할 제약 조건과 정책을 결정하며, 실행할 검색 전략을 수립합하는 것입니다. 몇 가지 간단한 사례를 통해 이를 구체적으로 살펴보겠습니다.</p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">쇼핑객 쿼리</td><td style="border: 1px solid #ccc; padding: 4px;">거버넌스 기반 해석</td><td style="border: 1px solid #ccc; padding: 4px;">검색 계획 예시</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">'땅콩 없는 초콜릿'</td><td style="border: 1px solid #ccc; padding: 4px;">엄격한 배제 제약 조건을 포함한 제품 지향 쿼리</td><td style="border: 1px solid #ccc; padding: 4px;">초콜릿에 대한 어휘 검색 수행 및 땅콩 포함 제품 제외 필터 적용</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">“저렴한 올리브 오일”</td><td style="border: 1px solid #ccc; padding: 4px;">가격 제약 조건이 있는 제품/카테고리 쿼리</td><td style="border: 1px solid #ccc; padding: 4px;">올리브 오일에 대한 어휘 검색 및 소매 업체가 설정한 저가 기준에 맞춘 가격 필터 적용</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">'4달러 미만의 비타민 C 함량이 높은 과일'</td><td style="border: 1px solid #ccc; padding: 4px;">의미 이해와 엄격한 제약 조건이 동시에 요구되는 발견형 쿼리</td><td style="border: 1px solid #ccc; padding: 4px;">영양학적 의도에 대한 의미 검색을 수행하되, 카테고리는 과일로 제한하고 가격은 4달러 미만인 상품으로 필터링</td>
                    </tr>
              </tbody>
            </table>
          <p>제어 평면은 각 쿼리에 대해 일관되고 예측 가능하며 확장 가능한 방식으로 적절한 정책과 검색 전략을 선택합니다. 이러한 방식은 의도에 부합하는 제약 조건을 최우선으로 적용하고 라우팅 결정을 명시화함으로써, 프로덕션에서 고도화된 검색 방법의 예측 가능성을 높여 줍니다.</p><h2>다른 접근 방식과의 관계</h2><p>일부 팀은 제품 의미를 더 잘 파악하기 위해 개선된 임베딩 모델을 사용하여 의미 검색 품질을 실질적으로 향상할 수 있습니다. 또 어떤 팀은 검색 후 참여도 또는 비즈니스 신호를 기반으로 결과 순서를 최적화하기 위해 <a href="https://www.elastic.co/docs/solutions/search/ranking/learning-to-rank-ltr">LTR(Learning To Rank)</a>과 같은 순위 재지정 방식을 사용하기도 합니다. 두 방식 모두 가치 있으며, 대개 상호 보완적입니다. 더 나은 임베딩은 유사성 매칭을 개선합니다. 순위 재지정은 검색된 후보 사이의 노출 순서를 개선합니다.</p><p>거버넌스는 검색의 업스트림에 위치한다는 점에서 다른 계층의 문제를 다룹니다. 제어 평면은 어떤 검색 전략(어휘, 의미, 또는 하이브리드)을 사용할지, 어떤 확정적 제약 조건을 적용할지, 그리고 어떤 쿼리가 여러 비즈니스 정책을 결합할지를 결정합니다.</p><h2>거버넌스 기반의 제어 평면을 통해 가능한 것</h2><p>거버넌스 계층이 구축되면 운영 모델이 근본적으로 바뀝니다. 수익에 직결되는 핵심 쿼리의 결과가 예측 가능해집니다. 비즈니스 팀은 엔지니어링팀의 배포 주기를 기다리지 않고도 검색 동작을 업데이트할 수 있습니다. 또한 의미 및 하이브리드와 같은 고급 검색 방법을 전체 시스템의 온/오프 스위치 대신 라우팅 및 가드레일 뒤에서 점진적으로 도입할 수 있습니다.</p><p>이 시리즈의 다음 게시물에서는 이러한 운영 모델이 실제로 어떻게 작동하는지, 그리고 왜 이 모델이 그 기반이 되는 검색 기술만큼이나 중요한지를 살펴봅니다.</p><p>머천다이저가 수익에 직결되는 핵심 쿼리 문제를 해결하기 위해 Jira 티켓을 생성하고 배포를 기다려야 한다면, 그 병목 지점은 검색 엔진이 아니라 운영 모델에 있는 것입니다. 현대 전자 상거래 검색은 비즈니스 의도를 제어되고 감사 가능한 검색 동작으로 신속하고 안전하게 변환하는 방법이 필요하며, 동시에 고도화된 검색은 실질적인 가치를 더할 수 있는 경우 고급 검색 기능을 활용해야 합니다.</p><h2>이 시리즈의 다음 내용</h2><p>이 시리즈에서 살펴본 패턴은 검색의 업스트림에서 작동합니다. 즉, 쿼리 생성이 시작되기도 전에 비즈니스 의도를 최적의 검색 전략으로 변환합니다. 다음 게시물에서는 기술적인 문제에서 운영의 영역으로 관점을 옮겨보겠습니다. 비즈니스 팀이 엔지니어링 배포 없이 검색 동작을 직접 수정할 수 있게 될 때 어떤 변화가 일어나는지, 그리고 거버넌스가 어떻게 그 과정을 안전하게 보장하는지 살펴봅니다.</p><h2>거버넌스 기반 전자 상거래 검색 실제로 적용해 보기</h2><p>엔지니어링 병목 현상, 취약한 애플리케이션 계층 로직, 예측할 수 없는 검색 결과는 엔터프라이즈 전자 상거래 서비스 계약을 통해 Elastic 서비스가 해결할 수 있는 문제입니다. 본 시리즈에서 설명하는 거버넌스 기반 제어 평면 아키텍처는 Elastic Services Engineering에서 구축했습니다.</p><p>여러분의 팀이 머천다이징 요청을 코드로 옮기는 데 엔지니어링 주기를 허비하고 있거나, 검색 정확성 백로그가 좀처럼 줄어들지 않는다면 Elastic이 도와드릴 수 있습니다. 현재 아키텍처를 평가하고 거버넌스 기반의 기업이 편집 가능한 검색 환경을 구축하는 데 도움을 드릴 수 있습니다. <a href="https://www.elastic.co/consulting">Elastic Services</a>에 문의하세요.</p><h2>논의에 참여하기</h2><p>검색 거버넌스, 검색 전략 또는 전자 상거래 검색 아키텍처에 대해 궁금한 점이 있으신가요? <a href="https://discuss.elastic.co/">Elastic 커뮤니티 대화</a>에 참여하세요.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Mastra와 Elasticsearch로 에이전틱 AI 애플리케이션을 구축하는 방법]]></title>
            <description><![CDATA[실제 예제를 통해 Mastra와 Elasticsearch로 에이전틱 AI 애플리케이션을 구축하는 방법을 알아보세요.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/build-agentic-ai-applications-mastra-elasticsearch</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/build-agentic-ai-applications-mastra-elasticsearch</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Enrico Zimuel]]></dc:creator>
            <pubDate>Wed, 08 Apr 2026 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/ab30baf2f908534840c5d71a46705773807baf54-1280x720.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>이 글에서는 <a href="https://mastra.ai/">Mastra</a> TypeScript 프레임워크를 사용해 <a href="https://www.elastic.co/elasticsearch">Elasticsearch</a>와 상호 작용하는 에이전틱 애플리케이션을 구축하는 방법을 자세히 살펴보겠습니다.</p><p>Elastic은 최근에 Elasticsearch를 벡터 데이터베이스로 지원하도록 추가하여 <a href="https://github.com/mastra-ai/mastra">mastra-ai/mastra</a> 오픈 소스 프로젝트에 기여했습니다. 이 새로운 기능을 통해 Mastra에서 Elasticsearch를 네이티브로 사용하여 임베딩을 저장할 수 있습니다. 벡터 기능 외에도 Elasticsearch는 컨텍스트 엔지니어링 요구 사항을 충족하기 위한 다양한 고급 기능을 제공합니다. (예: <a href="https://www.elastic.co/search-labs/blog/context-engineering-hybrid-search-evolution-agentic-ai">하이브리드 검색 및 순위 재지정</a>)</p><p>이 글에서는 Elasticsearch를 사용하여 Retrieval-Augmented Generation(RAG) 아키텍처를 구현하는 에이전트 생성 과정을 자세히 설명합니다. Elasticsearch에 저장된 SF 영화 데이터 코퍼스와 상호 작용하는 에이전틱 접근법을 사용하는 데모 프로젝트를 선보일 것입니다. 이 프로젝트는 <a href="https://github.com/elastic/mastra-elasticsearch-example">elastic/mastra-elasticsearch-example</a>에서 확인할 수 있습니다.</p><h2>Mastra</h2><p>Mastra는 에이전틱 AI 애플리케이션을 생성하기 위한 TypeScript 프레임워크입니다.</p><p>Mastra의 프로젝트 구조는 다음과 같습니다:</p><pre><code>src/
├── mastra/
│   ├── agents/
│   │   └── weather-agent.ts
│   ├── tools/
│   │   └── weather-tool.ts
│   ├── workflows/
│   │   └── weather-workflow.ts
│   ├── scorers/
│   │   └── weather-scorer.ts
│   └── index.ts
├── .env.example
├── package.json
└── tsconfig.json</code></pre><p>Mastra에서는 <a href="https://mastra.ai/docs/agents/overview">에이전트</a>, <a href="https://mastra.ai/docs/agents/using-tools">도구</a>, <a href="https://mastra.ai/docs/workflows/overview">워크플로우</a> 및 <a href="https://mastra.ai/docs/evals/overview">점수</a>를 구축할 수 있습니다.</p><p><strong>에이전트</strong>는 메시지를 입력으로 받아들이고 응답을 출력으로 생성하는 클래스입니다. 에이전트는 도구, 대형 언어 모델 (LLM), 메모리를 사용할 수 있습니다(그림 1).</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/f6aca2dcc7fcc45d25e06681649be1b2b7eb6781-706x721.png" style="width: 100%;" /><p>에이전트의 <strong>도구</strong>를 사용하면 웹 API와 통신하거나 Elasticsearch 쿼리와 같은 내부 작업을 수행하는 등 &#x27;외부 세계&#x27;와 상호 작용할 수 있습니다. <strong>메모리</strong> 구성 요소는 과거 입력과 출력을 포함하여 대화 기록을 저장하는 데 중요한 역할을 합니다. 이렇게 저장된 컨텍스트를 통해 에이전트는 과거 상호 작용을 활용하여 향후 질문에 대해 더 정보에 입각하고 정확도 높은 응답을 제공할 수 있습니다.</p><p><strong>워크플로우</strong>를 사용하면 단일 에이전트의 추론에 의존하는 대신 명확하고 구조화된 단계를 통해 복잡한 작업 순서를 정의할 수 있습니다(그림 2). 이를 통해 작업을 어떻게 세분화하는지, 데이터가 작업 간에 어떻게 이동하는지, 그리고 언제 무엇이 실행되는지를 완벽하게 제어할 수 있습니다. 워크플로우는 기본적으로 내장된 실행 엔진을 사용하여 실행되거나 <a href="https://mastra.ai/docs/deployment/workflow-runners">워크플로우 러너</a>에 배포할 수 있습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/0dce161cf7891207015dc87532b5b90df1822432-880x252.png" style="width: 100%;" /><p>Mastra에서는 모델 등급, 규칙 기반 및 통계적 방법을 사용하여 에이전트 출력을 평가하는 자동화된 테스트인 점수를 정의할 수 있습니다. 채점기는 <em>점수</em>를 반환합니다. 이 점수는 출력물이 평가 기준을 얼마나 잘 충족하는지를 정량화한 수치 값(일반적으로 0에서 1 사이)입니다. 이러한 점수를 통해 성능을 객관적으로 추적하고, 다양한 접근 방식을 비교하며, AI 시스템에서 개선이 필요한 영역을 식별할 수 있습니다. 채점기는 자신만의 프롬프트와 점수 매기기 기능으로 사용자 지정할 수 있습니다.</p><h2>Elasticsearch</h2><p>데모 프로젝트를 실행하려면 Elasticsearch 인스턴스가 실행 중이어야 합니다. <a href="https://www.elastic.co/cloud">Elastic Cloud</a>에서 무료 체험을 활성화하거나 <a href="https://github.com/elastic/start-local"><code>start-local</code></a> 스크립트를 사용해 로컬에 설치할 수 있습니다.</p><pre><code>curl -fsSL https://elastic.co/start-local | sh</code></pre><p>이렇게 하면 컴퓨터에 Elasticsearch와 Kibana가 설치되고 Mastra 통합 구성에 사용할 API 키가 생성됩니다.</p><p>API 키는 이전 명령어의 출력 결과로 표시되며, elastic-start-local 폴더 안에 있는 <strong>.env</strong> 파일에 저장됩니다.</p><h2>데모 설치 및 구성</h2><p>데모 프로젝트의 소스 코드가 포함된 <a href="https://github.com/elastic/mastra-elasticsearch-example">elastic/mastra-elasticsearch-example</a> 리포지토리를 생성했습니다. 리포지토리에 기재된 예제는 Elasticsearch에서 문서를 검색하기 위한 RAG 아키텍처를 구현하는 에이전트를 Mastra에서 생성하는 방법을 보여 줍니다.</p><p>Elastic은 데모를 위해 SF 영화에 대한 데이터 세트를 제공했습니다. <a href="https://www.kaggle.com/datasets/rajugc/imdb-movies-dataset-based-on-genre/versions/2?select=scifi.csv">Kaggle</a>의 IMDb 데이터 세트에서 500개의 영화를 추출했습니다.</p><p>첫 번째 단계는 다음 명령어를 사용하여 npm으로 프로젝트의 의존성을 설치하는 것입니다.</p><pre><code>npm install</code></pre><p>그런 다음 설정 정보가 담길 <strong>.env</strong> 파일을 구성해야 합니다. 다음 명령을 사용하여 <strong>.env.example</strong> 파일에서 구조를 복사하여 이 파일을 생성할 수 있습니다.</p><pre><code>cp .env.example .env</code></pre><p>이제 누락된 정보를 추가하여 .env 파일을 편집할 수 있습니다.</p><pre><code>OPENAI_API_KEY=
ELASTICSEARCH_URL=
ELASTICSEARCH_API_KEY=
ELASTICSEARCH_INDEX_NAME=scifi-movies</code></pre><p>Elasticsearch 인덱스의 이름은 <strong><code>scifi-movies</code></strong>입니다. 원한다면 env 변수 <code>ELASTICSEARCH_INDEX_NAME</code>을(를) 사용하여 변경할 수 있습니다.</p><p>OpenAI를 임베딩 서비스로 사용했으므로 <code>OPENAI_API_KEY</code> env 변수에 OpenAI용 API 키를 제공해야 합니다.</p><p>이 예시에서 사용된 임베딩 모델은 <a href="https://developers.openai.com/api/docs/models/text-embedding-3-small">openai/text-embedding-3-small</a>이며, 임베딩 차원은 1,536입니다.</p><p>최종 답변 생성을 위해 비용을 절감할 수 있는 <a href="https://developers.openai.com/api/docs/models/gpt-5-nano">openai/gpt-5-nano</a> 모델을 사용했습니다.</p><p>RAG 아키텍처를 사용하면 답변의 근거를 찾는 무거운 작업을 검색 구성 요소(이 경우 Elasticsearch)가 처리하기 때문에, 상대적으로 성능이 낮고(따라서 저렴한) 최종 LLM 모델을 사용할 수 있습니다.</p><p>소형 LLM은 두 가지 주요 작업만 담당합니다.</p><ul><li><strong>쿼리 재구성/임베딩:</strong> 사용자의 자연어 질문을 의미론적 검색이 가능하도록 벡터 임베딩으로 변환합니다.</li><li><strong>답변 합성:</strong> 정확도가 높고 검색된 컨텍스트 덩어리(문서/동영상)를 가져와서 제공된 프롬프트 지침에 따라 일관성 있고 사람이 읽을 수 있는 최종 답변으로 합성합니다.</li></ul><p>RAG 프로세스는 답변에 필요한 <strong>정확한 사실적 컨텍스트를 제공하므로</strong> 최종 LLM은 거대하거나 매우 복잡할 필요가 없으며, 자체 매개변수 내에서 필요한 지식을 모두 보유할 필요도 없습니다.(이는 보통 크고 비싼 모델들이 맡는 잘하는 것입니다.) 본질적으로 모델은 자체가 거대한 지식 기반 역할을 하는 것이 아니라, Elasticsearch가 제공한 정보를 바탕으로 정교하게 요약하고 형식을 지정하는 역할을 수행하는 것입니다. 이렇게 하면 비용 및 지연 최적화에 <code>gpt-5-nano</code> 같은 모델을 사용할 수 있습니다.</p><p>.env 파일 설정을 마친 후, 다음 명령어를 사용하여 영화 데이터를 Elasticsearch으로 수집할 수 있습니다.</p><pre><code>npx tsx src/utility/store.ts</code></pre><p>다음과 같은 출력 내용이 나타나야 합니다.</p><pre><code>🚀 Starting ingestion of 500 movies from 500_scifi_movies.jsonl...
Ingesting ░░░░░░░░░░░░░░░░░░░░░░░░ 1/500 (0%) | ok:1 | fail:0 | chunks:1 | eta:19m 33s | current:Capricorn One
Ingesting ░░░░░░░░░░░░░░░░░░░░░░░░ 2/500 (0%) | ok:2 | fail:0 | chunks:2 | eta:10m 32s | current:Doghouse
Ingesting ░░░░░░░░░░░░░░░░░░░░░░░░ 3/500 (1%) | ok:3 | fail:0 | chunks:3 | eta:7m 33s | current:Dinocroc
Ingesting ░░░░░░░░░░░░░░░░░░░░░░░░ 4/500 (1%) | ok:4 | fail:0 | chunks:7 | eta:6m 10s | current:Back to the Future           
Ingesting ░░░░░░░░░░░░░░░░░░░░░░░░ 5/500 (1%) | ok:5 | fail:0 | chunks:9 | eta:5m 14s | current:The Projected Man            
Ingesting ░░░░░░░░░░░░░░░░░░░░░░░░ 6/500 (1%) | ok:6 | fail:0 | chunks:11 | eta:4m 41s | current:I, Robot
...
✅ Ingestion complete in 1m 46s. Success: 500, Failed: 0, Chunks: 693.</code></pre><p>SF 영화 인덱스의 매핑에는 다음 필드가 포함되어 있습니다.</p><ul><li><strong>임베딩</strong>, 1,536 차원의 dense_vector, 코사인 유사도.</li><li><strong>설명</strong>, 영화에 대한 설명이 포함된 텍스트.</li><li><strong>감독</strong>, 감독의 이름이 포함된 텍스트.</li><li><strong>제목</strong>, 영화의 제목이 포함된 텍스트.</li></ul><p>제목과 설명을 사용하여 임베딩을 생성했습니다. 제목과 설명은 별개의 필드이지만, 이 둘을 연결함으로써 생성된 임베딩 벡터가 영화의 고유한 정체성(제목)과 풍부한 서술적 컨텍스트(설명)을 모두 포착할 수 있게 됩니다. 결과적으로 더욱 정확하고 포괄적인 의미론적 검색이 가능해집니다. 이렇게 결합된 입력은 임베딩 모델에 유사도 매칭을 위한 문서 내용의 더 나은 단일 표현을 제공합니다.</p><h2>데모를 실행하십시오</h2><p>다음 명령어를 사용하여 데모를 실행할 수 있습니다.</p><pre><code>npm run dev</code></pre><p>이 명령어를 실행하면 Mastra Studio에 접속할 수 있는 웹 애플리케이션이 <strong>localhost:4111</strong>에서 시작됩니다(그림 3).</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/1567e309df21a12bcf1dfef4429f82342549956c-1705x1079.png" style="width: 100%;" /><p><a href="https://mastra.ai/docs/getting-started/studio">Mastra Studio</a>는 에이전트를 구축하고 테스트할 수 있는 대화형 UI를 제공하며, Mastra 애플리케이션을 로컬 서비스로 활용할 수 있도록 REST API도 함께 지원합니다. 이를 통해 통합에 대한 걱정 없이 즉시 구축을 시작할 수 있습니다.</p><p><strong>Elasticsearch Agent</strong>를 제공했으며, 이는 Elasticsearch를 사용하여 의미 검색을 실행하는 도구로 Mastra의 <a href="https://mastra.ai/reference/tools/vector-query-tool">createVectorQueryTool</a>을 사용합니다. 이 에이전트는 RAG 접근법을 사용하여 사용자 질문에 답하기 위한 관련 문서(즉, 영화)를 검색합니다.</p><p>이 에이전트는 다음 프롬프트를 사용합니다.</p><pre><code>You are a helpful assistant that answers questions based on the provided context.
Follow these steps for each response:

1. First, carefully analyze the retrieved context chunks and identify key information.
2. Break down your thinking process about how the retrieved information relates to the query.
3. Draw conclusions based only on the evidence in the retrieved context.
4. If the retrieved chunks don't contain enough information, explicitly state what's missing.

Format your response as:
THOUGHT PROCESS:
- Step 1: [Initial analysis of retrieved chunks]
- Step 2: [Reasoning based on chunks]

FINAL ANSWER:
[Your concise answer based on the retrieved context]

Important: When asked to answer a question, please base your answer only on the context provided in the tool. 
If the context doesn't contain enough information to fully answer the question, please state that explicitly and stop it.
Do not add more information than what is present in the retrieved chunks.
Remember: Explain how you're using the retrieved information to reach your conclusions.</code></pre><p><code>Mastra Studio &gt; Agents</code> 메뉴를 클릭하고 <strong>Elasticsearch Agent</strong>를 선택하면, 채팅 시스템을 통해 에이전트를 테스트할 수 있습니다. 예를 들어, 다음과 같은 질문으로 SF 영화에 관한 정보를 요청할 수 있습니다.</p><p><em>UFO에 관한 영화 또는 TV 시리즈 5편을 찾아 줘</em>.</p><p>에이전트가 vectorQueryTool을 실행하는 것을 확인할 수 있습니다. 실행된 도구를 클릭하여 입력과 출력을 확인할 수 있습니다. 실행이 완료되면 LLM은 Elasticsearch의 SF 영화 인덱스에서 제공된 맥락을 바탕으로 질문에 답변을 제공합니다(그림 4).</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/d9998d4f687984de98845dae52d1288166abf448-1344x1071.png" style="width: 100%;" /><p>Mastra는 내부적으로 다음 단계를 실행합니다.</p><ol><li><strong>벡터 변환:</strong> 사용자의 질문인 <em>UFO에 관한 영화 또는 TV 시리즈 5편을 찾아 줘</em>는 OpenAI의 <code>openai/text-embedding-3-small</code> 모델을 사용하여 벡터 임베딩으로 변환됩니다.</li><li><strong>벡터 검색:</strong> 이 임베딩은 벡터 검색을 통해 Elasticsearch를 쿼리하는 데 사용됩니다.</li><li><strong>결과 검색:</strong> Elasticsearch는 쿼리와 관련성이 높은(즉, 사용자의 쿼리 벡터와 가장 가까운 벡터를 가진) 10편의 영화를 반환합니다.</li><li><strong>답변 생성:</strong> 검색된 영화와 원래 사용자 질문은 LLM, 구체적으로<code>openai/gpt-5-nano</code>에 전송됩니다. LLM은 이 정보를 처리하여 최종 답변을 생성하며, 이때 결과를 5개 보여달라는 사용자의 요청 사항이 정확히 반영되도록 합니다.</li></ol><h2>The Elasticsearch Agent</h2><p>여기에서는 Elasticsearch Agent의 소스 코드를 기재했습니다.</p><pre><code>import { Agent } from "@mastra/core/agent";
import { ElasticSearchVector } from '@mastra/elasticsearch';
import { createVectorQueryTool } from '@mastra/rag';
import { ModelRouterEmbeddingModel } from "@mastra/core/llm";
import { Memory } from "@mastra/memory";

const es_url = process.env.ELASTICSEARCH_URL;
const es_apikey = process.env.ELASTICSEARCH_API_KEY;
const es_index_name = process.env.ELASTICSEARCH_INDEX_NAME;
const prompt = 'insert here the previous prompt';

const esVector = new ElasticSearchVector({
  id: 'elasticsearch-vector',
  url: es_url,
  auth: {
    apiKey : es_apikey
  }
});

const vectorQueryTool = createVectorQueryTool({
  vectorStore: esVector,
  indexName: es_index_name,
  model: new ModelRouterEmbeddingModel("openai/text-embedding-3-small")
});

export const elasticsearchAgent = new Agent({
  id: "elasticsearch-agent",
  name: "Elasticsearch Agent",
  instructions: prompt,
  model: 'openai/gpt-5-nano',
  tools: { vectorQueryTool },
  memory: new Memory(),
});</code></pre><p><strong>vectorQueryTool</strong>은 RAG 예시의 검색 부분을 구현하기 위해 호출되는 도구입니다. Elastic 측에서 Mastra에 기여한 <a href="https://mastra.ai/reference/vectors/elasticsearch">ElasticSearchVector</a> 구현을 사용합니다.</p><p>에이전트는 VectorQueryTool, 프롬프트, 메모리를 사용하는 에이전트 클래스의 객체입니다. 보시다시피, Elasticsearch를 에이전트에 연결하기 위해 필요한 코드는 매우 간결합니다.</p><h2>결론</h2><p>이 글에서는 Mastra 프레임워크와 Elasticsearch를 통합하여 정교한 에이전트형 AI 애플리케이션을 구축하는 과정이 얼마나 단순하고 강력한지 살펴보았습니다. 특히, Elasticsearch에 인덱싱된 SF 영화 데이터 코퍼스에 대해 의미 검색을 수행할 수 있는 RAG 에이전트를 만드는 방법을 자세히 살펴보았습니다.</p><p>핵심적인 특징은 Elastic이 Mastra 오픈소스 프로젝트에 직접 기여했다는 점이며, 이를 통해 Elasticsearch를 벡터 저장소로 사용하는 네이티브 지원이 가능해졌습니다. 이러한 통합은 <strong>Elasticsearch Agent</strong> 소스 코드에서 확인할 수 있듯이 진입 장벽을 크게 낮춥니다. <code>ElasticSearchVector</code> 및 <code>createVectorQueryTool</code>(을)를 사용하면, 최소한의 구성 코드 몇 줄로 Elasticsearch를 에이전트에 연결하기 위한 전체 설정이 완료됩니다.</p><p>Elasticsearch는 결과의 정확도를 향상하고자 여러 고급 기능을 제공합니다. 예를 들어, <a href="https://www.elastic.co/elasticsearch/hybrid-search">하이브리드 검색</a>은 어휘 검색과 벡터 검색을 결합하여 정확도를 크게 향상합니다. 또 다른 흥미로운 기능은 하이브리드 검색의 마지막 단계에 적용할 수 있는 최신 <a href="https://www.elastic.co/search-labs/tutorials/jina-tutorial/jina-reranker-v3">Jina 모델</a>을 활용한 순위 재지정입니다. 이러한 기술에 대해 자세히 알아보려면 Elasticsearch Labs의 다음 글을 참조하세요.</p><ul><li><a href="https://www.elastic.co/search-labs/blog/hybrid-search-elasticsearch">Elasticsearch 하이브리드 검색</a>, 발렌틴 크레타즈(Valentin Crettaz) 저</li><li><a href="https://www.elastic.co/search-labs/blog/jina-models-elasticsearch-guide">Jina 모델, 그 기능 및 Elasticsearch에서의 사용법에 대한 소개</a>, 스콧 마르텐스(Scott Martens) 저</li></ul><p>제공된 예시를 살펴보고 Mastra 및 Elasticsearch로 자신만의 데이터 기반 에이전트를 구축해 보시기 바랍니다. Mastra에 대한 더 자세한 정보는 <a href="https://mastra.ai/docs">여기</a>에서 공식 문서를 통해 확인하실 수 있습니다.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Elastic Workflows를 활용한 Kibana 대시보드 조회수 모니터링]]></title>
            <description><![CDATA[Elastic Workflows를 사용하여 30분마다 Kibana 대시보드 조회 지표를 수집하고 이를 Elasticsearch에 인덱싱하는 방법을 알아보세요. 수집된 데이터를 바탕으로 맞춤형 분석 및 시각화 기능을 직접 구축할 수 있습니다.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/monitor-kibana-dashboard-views-elastic-workflows</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/monitor-kibana-dashboard-views-elastic-workflows</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Gustavo Llermaly]]></dc:creator>
            <pubDate>Fri, 03 Apr 2026 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/0ed94ce00962287b5507f45c92ecb60fdcbf2718-1280x720.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p><a href="https://www.elastic.co/kibana">Kibana</a>는 각 대시보드의 조회수를 추적하지만 해당 데이터는 기본으로 제공되는 대시보드 내에서 직접적으로 확인할 수는 없습니다. 이 글에서는 <strong>Elastic Workflows</strong>를 사용하여 30분마다 해당 데이터를 자동으로 수집하고 Elasticsearch에 인덱싱하고 이를 바탕으로 직접 분석 환경을 구축하는 방법을 알아보겠습니다.</p><p><a href="https://www.elastic.co/docs/explore-analyze/workflows">Elastic Workflows</a>는 Kibana에 내장된 자동화 엔진입니다. 간단한 YAML 설정을 통해 여러 단계로 구성된 프로세스를 정의할 수 있습니다. 각 워크플로우는 일정 또는 이벤트에 따라 트리거될 수 있으며 <a href="https://www.elastic.co/docs/explore-analyze/ai-features/elastic-agent-builder">Elastic Agent Builder</a>의 도구로 활용할 수 있습니다. 또한 각 단계에서 Kibana API 호출, Elasticsearch 쿼리 실행, 데이터 변환 작업을 수행할 수 있습니다.</p><p>여기서는 대시보드 조회수를 구체적인 예로 사용하지만 Kibana saved objects API를 통해 노출되는 모든 메트릭에 동일한 패턴을 적용할 수 있습니다.</p><h2>필수 구성 요소</h2><ul><li>9.3 버전이 실행 중인 <a href="https://www.elastic.co/cloud">Elastic Cloud</a> 또는 <a href="https://www.elastic.co/docs/deploy-manage/deploy/self-managed">자체 관리형</a> 클러스터</li><li><a href="https://www.elastic.co/docs/explore-analyze/workflows/get-started#workflows-prerequisites">워크플로우 기능 활성화</a>(고급 설정)</li></ul><div style="display:none">Unknown block type "block", specify a component for it in the `components.types` option</div><p>본격적인 구축에 앞서 우리가 활용할 데이터가 어떤 구성을 갖추고 있는지 먼저 이해해 봅시다. Kibana는 대부분의 설정 정보와 메타데이터를 전용 내부 인덱스에 <a href="https://www.elastic.co/docs/api/doc/kibana/group/endpoint-saved-objects">Saved Objects</a>라는 형태로 저장합니다. Kibana가 이러한 방식으로 추적하는 항목 중 하나는 대시보드 조회수입니다. 이때 &#x27;Usage Counters&#x27;라는 특수한 유형의 Saved Object를 사용하게 됩니다. 다음과 같이 Dev Tools에서 직접 쿼리할 수 있습니다.</p><pre><code>GET kbn:/api/saved_objects/_find?type=usage-counter&filter=usage-counter.attributes.domainId:"dashboard"%20and%20usage-counter.attributes.counterType:"viewed"&per_page=10000</code></pre><p>응답은 다음과 같습니다.</p><pre><code>{
  "page": 1,
  "per_page": 10000,
  "total": 1,
  "saved_objects": [
    {
      "type": "usage-counter",
      "id": "dashboard:346f3c64-ebca-484d-9d57-ec600067d596:viewed:server:20260310",
      "attributes": {
        "domainId": "dashboard",
        "counterName": "346f3c64-ebca-484d-9d57-ec600067d596",
        "counterType": "viewed",
        "source": "server",
        "count": 1
      },
      ...
    }
  ]</code></pre><p><code>counterName</code> 필드는 대시보드의 ID를 나타내며 <code>count</code>는 해당 특정 날짜의 대시보드 누적 조회수입니다. Kibana는 대시보드당 하루에 하나의 카운터 객체를 생성합니다. 객체 ID의 날짜 접미사(예: ...viewed:server:20260310)를 통해 이를 확인할 수 있습니다. 조회수는 사용자가 대시보드를 열 때마다 하루 동안 계속해서 증가합니다.</p><p>인덱스에 이러한 일일 문서 모델을 그대로 복제하는 대신 워크플로우 실행당 하나의 문서를 생성해 보겠습니다. 각 문서는 캡처 시점을 기준으로 해당 대시보드의 당일 누적 조회수를 기록합니다.</p><h2>2단계: 대상 인덱스 생성</h2><p>대시보드 조회 스냅샷을 저장할 인덱스가 필요합니다. 나중에 집계와 시각화가 가능하도록 명시적 매핑을 사용하여 다음과 같이 인덱스를 생성합니다. Dev Tools에서 다음 명령을 실행하세요.</p><pre><code>PUT dashboard-views
{
  "mappings": {
    "properties": {
      "captured_at": {
        "type": "date"
      },
      "dashboard_id": {
        "type": "keyword"
      },
      "dashboard_name": {
        "type": "keyword"
      },
      "view_count": {
        "type": "integer"
      }
    }
  }
}</code></pre><p>ID와 이름에 <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/keyword"><code>keyword</code></a> 매핑을 사용하면 <a href="https://www.elastic.co/docs/explore-analyze/query-filter/aggregations">집계</a>를 수행할 수 있습니다. <code>view_count</code>에 <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/number"><code>integer</code></a> 타입을 사용하는 것은 안전한 기본값입니다. Kibana는 카운터를 매일 초기화하므로 32비트 제한(하루 20억 회 이상의 조회수)에 도달하는 것은 현실적인 우려 사항이 아니기 때문입니다. 이 타입은 여전히 <a href="https://www.elastic.co/docs/reference/aggregations/search-aggregations-metrics-max-aggregation"><code>max</code></a>, <a href="https://www.elastic.co/docs/reference/aggregations/search-aggregations-metrics-avg-aggregation"><code>avg</code></a>, <a href="https://www.elastic.co/docs/reference/aggregations/search-aggregations-metrics-min-aggregation"><code>min</code></a>과 같은 수치 연산을 지원합니다.</p><h2>3단계: 워크플로우 생성</h2><p><strong>Stack Management &gt; Workflows &gt; New Workflow</strong> 메뉴로 이동한 뒤 아래의 워크플로우 YAML 설정 내용을 복사하여 붙여넣으세요.</p><pre><code>name: dashboard-views-ingestion
triggers:
  - type: scheduled
    with:
      every: 30m

steps:
  - name: fetch_dashboard_views
    type: kibana.request
    with:
      method: GET
      path: >-
        /api/saved_objects/_find?type=usage-counter&per_page=10000&filter=usage-counter.attributes.domainId:"dashboard"%20and%20usage-counter.attributes.counterType:"viewed"

  - name: index_each_dashboard
    type: foreach
    foreach: "{{ steps.fetch_dashboard_views.output.saved_objects }}"
    steps:
      - name: fetch_dashboard_name
        type: kibana.request
        with:
          method: GET
          path: /api/saved_objects/dashboard/{{ foreach.item.attributes.counterName }}
        on-failure:
          continue: true

      - name: index_doc
        type: elasticsearch.request
        with:
          method: POST
          path: /dashboard-views/_doc
          body:
            dashboard_id: "{{ foreach.item.attributes.counterName }}"
            dashboard_name: "{{ steps.fetch_dashboard_name.output.attributes.title }}"
            view_count: "${{ foreach.item.attributes.count | plus: 0 }}"
            captured_at: "{{ execution.startedAt | date: '%Y-%m-%dT%H:%M:%SZ' }}"</code></pre><p>다음 섹션에서는 워크플로를 단계별로 자세히 살펴봅니다.</p><h3>워크플로우 작동 방식</h3><h4>트리거</h4><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/5670991d65c64ee833924225c2d375a1be868b13-325x162.png" style="width: 100%;" /><p>워크플로우는 30분 간격의 예약된 트리거에 따라 실행됩니다. 이를 통해 API에 무리를 주지 않으면서도 시계열 데이터를 확보할 수 있습니다.</p><h4>fetch_dashboard_views</h4><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/66eaec147c3d01c524c67cf1c7f663ac56a3259d-812x215.png" style="width: 100%;" /><p><code>kibana.request</code>를 사용하여 Kibana Saved Objects API를 호출합니다. 별도의 인증 설정은 필요하지 않습니다. 워크플로우 엔진이 실행 컨텍스트에 따라 적절한 헤더를 자동으로 첨부하기 때문입니다.</p><h4>index_each_dashboard (foreach)</h4><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/aad45e8aed8dc81ded6260cd6199ff78dcffe3b4-1892x290.png" style="width: 100%;" /><p>이전 단계에서 반환된 <a href="https://www.elastic.co/docs/api/doc/kibana/group/endpoint-saved-objects"><code>saved_objects</code></a> 배열을 순회합니다. 각 반복에서 현재 항목은 <code>foreach.item</code>을 통해 접근할 수 있습니다. 루프 내부에서는 각 대시보드에 대해 두 개의 하위 단계를 실행합니다.</p><p><strong>1. <code>fetch_dashboard_name</code>:</strong></p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/db21ec5094b743018b9cd66c5052681f14c7d7e3-1999x431.png" style="width: 100%;" /><p><code>GET /api/saved_objects/dashboard/{id}</code>를 호출하여 사용자가 읽을 수 있는 대시보드 제목을 확인합니다. <code>on-failure: continue: true</code>설정을 추가합니다. 이를 통해 특정 대시보드가 삭제되었더라도 조회수 카운터가 남아있는 경우 전체 실행이 실패 처리되지 않고 루프가 계속 지속됩니다.</p><p><strong>2. <code>index_doc</code>:</strong></p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/f49dd0c9f0817bb1e1e5d9f4a2b05d13ef331054-1999x626.png" style="width: 100%;" /><p><code>POST /dashboard-views/_doc</code>을 사용해 명시적 ID 없이 각 문서를 인덱싱하며 이 과정에서 Elasticsearch가 ID를 자동으로 생성합니다. 이는 실행할 때마다 새로운 문서를 생성합니다. 이전 스냅샷을 덮어쓰지 않고 시간에 따른 조회수 이력을 쌓을 수 있습니다.</p><p>주목할 만한 두 가지 사항이 있습니다:</p><ul><li><code>captured_at</code> 필드는 날짜 필터를 사용하여 타임스탬프를 <a href="https://www.iso.org/iso-8601-date-and-time-format.html">ISO 8601</a> 형식으로 변환합니다. 이 설정이 없으면 값이 <code>Tue Mar 10 2026 05:03:47 GMT+0000</code>과 같은 JavaScript 날짜 문자열로 출력됩니다. 이 경우 Elasticsearch가 이를 날짜 형식으로 매핑하지 못합니다.</li><li><code>view_count</code>는 숫자 유형을 유지하기 위해 <code>${{ }}</code> 구문과 <code>| plus: 0</code>을 사용합니다. 단순히 <code>{{ }}</code>를 사용하면 값이 문자열로 렌더링되어 대시보드에서 수치 연산을 수행할 수 없게 됩니다.</li></ul><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/6d48c8784d5df6192e8b5175e69dbab5098194bc-919x774.png" style="width: 100%;" /><p><em>UI를 통해 워크플로의 각 단계를 편리하게 디버깅할 수 있습니다.</em></p><h2>4단계: 통계 대시보드 구축</h2><p>워크플로우가 몇 번 실행되어 데이터가 수집되면 dashboard-views 데이터 뷰를 사용하여 Kibana에서 새 대시보드를 만듭니다.</p><p>활용 가능한 패널 예시:</p><ul><li><strong>조회수별 주요 대시보드:</strong> X축에 <code>dashboard_name</code>을, Y축에 <code>last_value(view_count)</code>를 설정한 <a href="https://www.elastic.co/docs/explore-analyze/visualize/charts/bar-charts"><strong>막대 차트</strong></a>를 사용하세요. 이를 통해 각 대시보드의 현재 일일 조회수를 확인할 수 있습니다.</li><li><strong>시간에 따른 조회수 변화:</strong> X축에 <code>captured_at</code>을, Y축에 <code>last_value(view_count)</code>를 설정하고 <code>dashboard_name</code>으로 구분한 <a href="https://www.elastic.co/docs/explore-analyze/visualize/charts/line-charts"><strong>선형 차트</strong></a>를 사용하세요. 워크플로우를 실행할 때마다 새로운 문서가 추가되므로 중복 합산 대신 &#x27;Last value&#x27; 집계를 사용하여 시간 버킷당 최종 수치를 가져옵니다.</li><li><strong>현재 스냅샷:</strong> 가장 최근의 <code>captured_at</code> 데이터를 사용하는 <a href="https://www.elastic.co/docs/explore-analyze/visualize/charts/tables"><strong>데이터 테이블</strong></a>을 만들어 모든 대시보드의 최신 조회수를 표시하세요.</li></ul><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/e245b95f67daf76a2aaf4cb9df2c75ef4cfef582-1462x747.png" style="width: 100%;" /><p>각 워크플로우 실행 시마다 새 문서가 생성되므로 시간 범위를 필터링하여 특정 기간의 활동을 분석하거나 주간 단위로 비교할 수 있습니다. 또한 대시보드 조회수가 임계값 아래로 떨어질 때 알림을 구축할 수 있습니다.</p><h2><strong>결론</strong></h2><p>Elastic Workflows는 이러한 주기적인 데이터 수집에 매우 적합합니다. 데이터 소스(Kibana API)와 대상(Elasticsearch)이 모두 네이티브로 연결되어 있어 자격 증명을 별도로 관리할 필요가 없기 때문입니다. 워크플로우 엔진이 <code>kibana.request</code> 및 <code>elasticsearch.request</code>단계의 인증을 자동으로 처리하므로 사용자는 로직만 작성하면 됩니다.</p><h2><strong>리소스</strong></h2><ul><li><a href="https://www.elastic.co/docs/explore-analyze/workflows">Elastic Workflows</a></li><li><a href="https://www.elastic.co/docs/api/doc/kibana/">Kibana API</a></li></ul>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[TSDS와 ILM의 만남: 늦게 도착하는 데이터를 거부하지 않는 시계열 데이터 스트림 설계]]></title>
            <description><![CDATA[TSDS 시간 제한이 ILM 단계와 상호 작용하는 방법과 늦게 도착하는 메트릭을 처리할 수 있는 정책 설계 방법]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/tsds-ilm-elasticsearch</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/tsds-ilm-elasticsearch</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Bret Wortman]]></dc:creator>
            <pubDate>Thu, 02 Apr 2026 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/e4c46e4a6f746d9c845857e80de036f5d51cd4e7-1280x720.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>최근 한 고객의 메트릭 클러스터를 &#x27;모든 데이터를 hot 티어에 두는&#x27; 구조에서 hot/cold/frozen 아키텍처로 전환했습니다. 이미 수십 번은 해 본 익숙한 작업이었습니다. 그런데 몇 분 만에 Logstash에서 데이터가 더 이상 진행되지 않았습니다.</p><p>Elasticsearch는 늦게 도착한 메트릭을 거부하고 있었습니다. 이로 인해 파이프라인이 밀리기 시작했고, 더 많은 데이터가 늦게 도착하면서 거부가 더욱 늘어나게 되었습니다. 결국 파이프라인은 완전히 멈춰 섰습니다.</p><p>스냅샷에서 복원하고, 데이터를 다시 인덱싱하고, 수집 파이프라인을 다시 설계하여 복구해야 했습니다.</p><p>근본 원인은 인덱스 수명 주기 관리(ILM) 자체가 아니라, 시계열 데이터 스트림(TSDS)과 이를 통해 시간 제한 백킹(backing) 인덱스를 강제하는 방식에 있었습니다.</p><p>TSDS는 메트릭의 저장 공간 요구 사항을 40–70% 줄일 수 있지만, TSDS를 효율적으로 만드는 아키텍처 변경으로 인해 시간이 지남에 따라 인덱스의 동작 방식도 달라집니다. 이러한 변화는 ILM 정책을 설계하거나 수집 파이프라인에서 데이터가 늦게 도착할 수 있는 상황에서 중요합니다.</p><h2>요약</h2><p>TSDS를 사용하는 경우:</p><ul><li>백킹 인덱스는 특정 시간 범위에 해당하는 문서만 허용합니다.</li><li>인덱스가 cold 또는 frozen으로 이동한 뒤 데이터가 늦게 도착하면, Elasticsearch는 해당 문서를 수용하지 않거나(설정된 경우) failure store로 라우팅합니다.</li></ul><p>설계 규칙:</p><pre><code>warm_min_age > rollover_max_age + maximum_expected_lateness</code></pre><h2>시계열 데이터 스트림이란 무엇인가요?</h2><p><em>시계열 데이터 스트림</em>(TSDS)은 메트릭 데이터에 최적화된 특수한 데이터 스트림입니다. 데이터는 관련 문서가 동일한 샤드 내에 위치하도록 라우팅되어 쿼리 및 검색에 최적화됩니다. Elasticsearch가 이를 수행하는 방법은 다음과 같습니다.</p><p>각 문서에는 다음이 포함됩니다.</p><ul><li>타임스탬프.</li><li>시계열을 식별하는 dimension 필드.</li><li>측정값을 나타내는 metric 필드.</li></ul><p>예를 들면 다음과 같습니다.</p><ul><li>호스트당 CPU 사용량.</li><li>서비스별 요청 지연 시간.</li><li>센서별 온도 측정값.</li></ul><p><em>dimensions</em>는 측정하고자 하는 대상을 식별하는 반면, <em>metrics</em>는 시간에 따라 변하는 값을 나타냅니다.</p><h3>dimensions</h3><p>dimensions는 측정되는 엔티티를 설명합니다.</p><p>예:</p><pre><code>host.name
service.name
container.id</code></pre><p>다음과 같이 매핑에서 정의합니다.</p><pre><code>time_series_dimension: true</code></pre><h3>metrics</h3><p>metrics는 숫자 값을 나타내며 다음을 사용하여 정의됩니다.</p><pre><code>time_series_metric</code></pre><p>일반적인 metric 유형:</p><ul><li>gauge: 오르내리는 값.</li><li>counter: 재설정될 때까지 증가하는 값.</li></ul><p>Elastic Agent는 주로 메트릭과 로그 데이터를 수집하므로, TSDS 인덱스를 직접 활성화하지 않은 경우에도 클러스터에 해당 인덱스가 여전히 존재할 수 있습니다.</p><h3>_tsid 필드</h3><p>Elasticsearch는 내부적으로 dimension 필드로부터 <code>_tsid</code> 값을 생성합니다. 이를 통해 동일한 dimensions를 가진 문서는 동일한 샤드로 라우팅되어 다음을 개선합니다.</p><ul><li>압축.</li><li>쿼리 로컬리티.</li><li>집계 성능.</li></ul><h2>주요 차이점: 시간 제한 백킹 인덱스</h2><p>기존 데이터 스트림은 항상 <em>쓰기 인덱스</em>라고 하는 가장 최근의 백킹 인덱스에 기록되지만, TSDS는 다르게 동작합니다.</p><p>각 TSDS 백킹 인덱스에는 정의된 시간 구간이 있으며 해당 구간에 해당하는 <code>@timestamp</code> 값을 가진 문서만 허용합니다.</p><pre><code>GET _data_stream/my-metrics-data-stream


     "index_mode": "time_series",
     "time_series": {
       "temporal_ranges": [
         {
           "start": "2026-01-15T14:35:50.000Z",
           "end": "2026-03-16T11:34:40.000Z"
         }
       ]
     }</code></pre><p>문서가 인덱싱되면 Elasticsearch는 해당 타임스탬프를 담당하는 백킹 인덱스로 라우팅하는데, 이는 기존의 인덱스와 달리 TSDS가 여러 백킹 인덱스에 동시에 쓰기를 수행할 수 있음을 의미합니다.</p><p>그 예는 다음과 같습니다.</p><ul><li>실시간 데이터 → 최신 인덱스.</li><li>지연된 데이터 → 해당 기간을 포함하는 이전 인덱스.</li></ul><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/31af2bb3b3dc24db8342e791e1db77a44659ba7a-1589x502.png" style="width: 100%;" /><h2>늦게 도착하는 데이터를 위한 설계</h2><p>실제 수집 파이프라인은 메트릭을 제시간에 완벽하게 전달하는 경우가 거의 없습니다. 메트릭은 네트워크 장애, 백로그, 배치 수집, 엣지 디바이스의 연결 끊김 등으로 인해 지연될 수 있으며, 다시 연결되면 밀린 데이터를 시작합니다.</p><p>기존 인덱스는 이러한 지연을 조용히 흡수하지만, TSDS는 그렇지 않습니다.</p><p>문서의 타임스탬프가 쓰기 가능한 백킹 인덱스의 범위를 벗어나면 Elasticsearch가 이를 거부하므로, ILM 정책은 늦게 도착하는 데이터를 반드시 고려해야 합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/32a103b95b20e31615c214271e27811a7ee315ae-1999x691.png" style="width: 100%;" /><h2>핵심 제약 조건</h2><p>백킹 인덱스는 지연 데이터를 수용할 수 있을 만큼 충분히 오랫동안 쓰기 가능 상태를 유지해야 합니다.</p><p>실제로는 다음과 같다.</p><pre><code>time_until_readonly > maximum_expected_lateness</code></pre><p>ILM은 롤오버 시점부터 인덱스 나이를 측정하므로, 운영 규칙은 다음과 같습니다.</p><pre><code>warm_or_cold_min_age > rollover_max_age + maximum_expected_lateness</code></pre><p></p><p>예를 들어, 메트릭이 최대 6시간 늦게 도착할 수 있는 경우 인덱스는 롤오버 후 최소 6시간 동안 쓰기 가능한 상태로 유지되어야 합니다.</p><p></p><p>이러한 제약 조건을 고려하지 못한 것이 바로 앞서 설명한 데이터 수집 실패의 원인이었습니다. 늦게 도착한 데이터는 이전 인덱스로 전달됐는데, 해당 인덱스는 이미 cold 티어에 있어서 쓰기가 차단된 상태였습니다.</p><p></p><h2>거부된 문서를 처리하는 방법</h2><p>TSDS가 문서를 거부하면 Elasticsearch는 오류를 반환하는데, 이는 타임스탬프가 쓰기 가능한 인덱스 범위 내에 속하지 않음을 의미합니다. 데이터 수집 파이프라인이 이 오류를 어떻게 처리하느냐에 따라 데이터 손실이 발생할 수도 있고, 수집이 중단될 수도 있습니다.</p><p>거부된 문서를 처리하는 주요 메커니즘은 failure store입니다.</p><h3>failure store(Elasticsearch 9.1 이상에서 권장)</h3><p>Elasticsearch 9.1에서는 failure store를 도입하여 거부된 문서를 자동으로 수집합니다. Elasticsearch는 클라이언트에 오류를 반환하는 대신 실패한 문서를 데이터 스트림 내부의 전용 failure 인덱스에 기록합니다.</p><p>다음을 사용하여 오류를 검사할 수 있습니다:</p><pre><code>GET metrics-myapp::failures/_search</code></pre><p>failure store를 사용하면 수집 파이프라인이 거부 오류로 인해 막히는 것을 방지하면서, 실패한 데이터를 분석 또는 <a href="https://www.elastic.co/docs/manage-data/data-store/data-streams/reindex-tsds">재인덱싱</a>을 위해 보존할 수 있습니다.</p><h2>거부 관련 문제 모니터링</h2><p>늦게 도착하는 문제는 일반적으로 수집 이상 징후로 먼저 나타납니다. 다음과 같은 현상으로 먼저 감지될 수 있습니다.</p><ul><li>인덱싱 속도의 급격한 감소.</li><li>거부된 문서 수의 급증.</li><li>failure store 항목 수 증가.</li><li>파이프라인 입력과 출력 개수 간의 불일치.</li></ul><p>이러한 신호를 기반으로 한 알림을 통해 운영자는 파이프라인이 중단되기 전에 문제를 감지할 수 있습니다. 워크플로우, 머신 러닝 작업 등 다양한 메커니즘을 활용해 탐지 및 알림을 자동화할 수 있습니다.</p><h2>TSDS + ILM 마이그레이션 체크리스트</h2><p>TSDS로 메트릭 클러스터를 마이그레이션하거나, ILM 계층화를 도입하거나, 메트릭이 기본적으로 TSDS인 Elasticsearch 버전으로 업그레이드하는 경우, 먼저 다음 항목을 검토하세요.</p><h3><strong>1. 수집 지연 시간 측정</strong></h3><p>ILM 정책을 변경하기 전에 다음 사항을 확인하세요.</p><ul><li>정상적인 수집 지연 시간.</li><li>인시던트 상황에서의 최대 지연.</li><li>배치 파이프라인으로 인한 지연.</li></ul><p>ILM 설계는 현실적으로 발생할 수 있는 최대 지연을 반드시 고려해야 합니다.</p><h3><strong>2. 인덱스 시간 구간 확인</strong></h3><p>TSDS 백킹 인덱스를 확인합니다.</p><pre><code>GET _data_stream/<your-stream></code></pre><p>다음을 확인하세요.</p><ul><li><code>time_series.start_time</code></li><li><code>time_series.end_time</code></li></ul><p>이러한 경계는 어떤 인덱스가 문서를 수용할 수 있는지를 결정합니다. 이러한 시간 구간을 이해하면 데이터가 어느 정도까지 지연될 수 있는지(거부되기 전까지)를 판단하는 데 도움이 됩니다.</p><h3><strong>3. 지연 데이터를 위한 hot 티어 크기 조정</strong></h3><p>백킹 인덱스는 지연 데이터를 처리할 수 있도록 충분한 기간 동안 쓰기 가능 상태를 유지해야 한다.</p><p>운영 규칙:</p><ul><li><code>warm_min_age &gt; rollover_max_age + maximum_expected_lateness</code></li></ul><p>메트릭이 6시간 늦게 도착할 수 있는 경우, 인덱스는 최소 6시간 동안 쓰기 가능 상태를 유지해야 한다는 점을 잊지 마세요.</p><h3><strong>4. 거부된 문서 처리 방식 결정</strong></h3><p>TSDS를 활성화하기 전에 전략을 선택하세요.</p><ul><li>failure store(Elasticsearch 9.1 이상에서 권장)</li><li>Logstash dead letter queue.</li><li>지연 데이터 처리를 위한 대체 인덱스.</li><li>제한적인 데이터 손실 허용.</li></ul><h3><strong>5. 데이터 수집 상태 모니터링</strong></h3><p>다음 항목에 대한 알림을 추가하세요.</p><ul><li>인덱싱 속도 하락.</li><li>거부된 문서.</li><li>failure store 증가</li><li>파이프라인 입력/출력 불일치.</li></ul><p>늦게 도착하는 데이터 문제는 종종 수집 이상 징후로 먼저 나타납니다.</p><h2>요약</h2><p>시계열 데이터 스트림은 메트릭 워크로드의 스토리지와 성능을 크게 개선하지만, 중요한 아키텍처 변경을 수반합니다. 백킹 인덱스는 시간 기반으로 제한되며, 이는 ILM의 동작 방식에 영향을 미칩니다.</p><p>TSDS를 사용하는 경우:</p><ul><li>인덱스는 지연 데이터를 수용할 수 있을 만큼 충분히 오랫동안 쓰기 가능 상태를 유지해야 합니다.</li><li>수집 파이프라인은 거부된 문서를 안정적으로 처리해야 합니다.</li></ul><p>기억해야 할 핵심 규칙은 다음과 같습니다:</p><pre><code>warm_min_age > rollover_max_age + maximum_expected_lateness</code></pre><p>이 제약 조건을 중심으로 ILM 정책을 설계하면 TSDS는 메트릭 워크로드에 매우 효과적으로 작동합니다.</p><p>하지만 이를 무시하면 수집 파이프라인이 이러한 시간 경계를 직접 겪으며 문제를 발견하게 될 수 있습니다.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[LINQ to ES|QL로 C# 작성 및 Elasticsearch 쿼리 작업]]></title>
            <description><![CDATA[Elasticsearch .NET 클라이언트의 새로운 LINQ to ES|QL 제공자를 살펴보세요. 이 제공자를 사용하면 C# 코드를 작성하여 ES|QL 쿼리로 자동 변환할 수 있습니다.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/linq-esql-c-elasticsearch-net-client</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/linq-esql-c-elasticsearch-net-client</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Florian Bernd, Martijn Laarman]]></dc:creator>
            <pubDate>Wed, 01 Apr 2026 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/e54132e915217063e9ed0ec45059c6cfc38e31dd-1280x720.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p><strong>v9.3.4</strong> 및 <strong>v8.19.18</strong>부터 Elasticsearch .NET 클라이언트에는 런타임에 C# LINQ 표현식을 <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/esql.html">Elasticsearch Query Language(ES|QL)</a> 쿼리로 변환하는 <a href="https://learn.microsoft.com/en-us/dotnet/csharp/linq/">Language Integrated Query (LINQ) </a>제공자가 포함되어 있습니다. ES|QL 스트링을 직접 작성하는 대신 <code>Where</code>, <code>Select</code>, <code>OrderBy</code>, <code>GroupBy</code> 및 기타 표준 연산자를 사용하여 쿼리를 작성합니다. 제공자는 변환, 매개변수화, 결과 역직렬화를 처리하며, 결과 세트 크기와 관계없이 메모리 사용량을 일정하게 유지하는 행별 스트리밍도 포함됩니다.</p><h2>첫 번째 쿼리</h2><p>먼저 Elasticsearch 인덱스에 맵핑되는 일반 CLR 객체(POCO)를 정의합니다. 속성 이름은 표준 <code>System.Text.Json</code> 속성(예: <code>[JsonPropertyName]</code>) 또는 구성된 <code>JsonNamingPolicy</code>을(를) 통해 ES|QL 열 이름으로 확인됩니다. 클라이언트의 나머지 부분에 적용되는 <a href="https://www.elastic.co/docs/reference/elasticsearch/clients/dotnet/source-serialization">소스 직렬화</a> 규칙이 여기에도 동일하게 적용됩니다.</p><pre><code>using System.Text.Json.Serialization;

public class Product
{
    [JsonPropertyName("product_id")]
    public string Id { get; set; }

    public string Name { get; set; }

    public string Brand { get; set; }

    [JsonPropertyName("price_usd")]
    public double Price { get; set; }

    [JsonPropertyName("in_stock")]
    public bool InStock { get; set; }
}</code></pre><p>유형이 지정되면 쿼리는 다음과 같습니다.</p><pre><code>var minPrice = 100.0;
var brand = "TechCorp";

await foreach (var product in client.Esql.QueryAsync<Product>(q => q
    .From("products")
    .Where(p => p.InStock && p.Price >= minPrice && p.Brand == brand)
    .OrderByDescending(p => p.Price)
    .Take(10)))
{
    Console.WriteLine($"{product.Name}: ${product.Price}");
}</code></pre><p>제공자는 이를 다음과 같은 ES|QL로 변환합니다.</p><pre><code>FROM products
| WHERE (in_stock == true AND price_usd >= ?minPrice AND brand == ?brand)
| SORT price_usd DESC
| LIMIT 10</code></pre><p>참고할 만한 몇 가지 세부 사항은 다음과 같습니다.</p><ul><li><strong>속성 이름 확인:</strong> <code>p.Price</code>은(는) <code>[JsonPropertyName]</code> 속성 때문에 <code>price_usd</code>이(가) 되고, <code>p.Brand</code>은(는) 기본 camelCase 명명 정책에 따라 <code>brand</code>이(가) 됩니다.</li><li><strong>매개변수 캡처:</strong> C# 변수 <code>minPrice</code> 및 <code>brand</code> 은(는) 명명된 매개변수(<code>?minPrice</code>, <code>?brand</code>)(으)로 캡처됩니다. 이러한 변수는 JSON 페이로드에서 쿼리 스트링과 별도로 전송되므로 인젝션을 방지하고 서버 측 쿼리 계획 캐싱을 활성화합니다.</li><li><strong>스트리밍:</strong> <code>QueryAsync&lt;T&gt;</code> 은(는)<code>IAsyncEnumerable&lt;T&gt;</code>을(를) 반환합니다. 행은 Elasticsearch에서 도착하는 대로 한 번에 하나씩 구체화됩니다.</li></ul><p>다음과 같이 생성된 쿼리와 해당 매개변수를 실행하지 않고도 확인할 수 있습니다.</p><pre><code>var query = client.Esql.CreateQuery<Product>()
    .Where(p => p.InStock && p.Price >= minPrice && p.Brand == brand)
    .OrderByDescending(p => p.Price)
    .Take(10);

Console.WriteLine(query.ToEsqlString());
// FROM products | WHERE (in_stock == true AND price_usd >= 100) | SORT price_usd DESC | LIMIT 10

Console.WriteLine(query.ToEsqlString(inlineParameters: false));
// FROM products | WHERE (in_stock == true AND price_usd >= ?minPrice AND brand == ?brand) | SORT price_usd DESC | LIMIT 10

var parameters = query.GetParameters();
// { "minPrice": 100.0, "brand": "TechCorp" }</code></pre><h2>작동 원리: LINQ 핵심 개념 되짚어보기</h2><p>LINQ 제공자를 가능하게 하는 메커니즘은 <code>IEnumerable&lt;T&gt;</code>와(과) <code>IQueryable&lt;T&gt;</code>의 구분입니다.</p><p><code>IEnumerable&lt;T&gt;</code> 에 대해<code>.Where(p =&gt; p.Price &gt; 100)</code> 을(를) 호출하면 lambda는 런타임이 프로세스 중에 실행하는 일반 델리게이트인 <code>Func&lt;Product, bool&gt;</code> (으)로 컴파일됩니다. 이것이 LINQ-to-Objects입니다.</p><p><code>IQueryable&lt;T&gt;</code>에서 동일한 메서드를 호출하면 C# 컴파일러는 lambda를<code>Expression&lt;Func&lt;Product, bool&gt;&gt;</code> (으)로 래핑합니다. 이는 실행 가능한 형태가 아닌 코드의 <em>구조</em>를 나타내는 데이터 구조입니다. 표현식 트리는 런타임에 검사, 분석 및 다른 언어로 변환될 수 있습니다.</p><pre><code>// IEnumerable: the lambda is a compiled delegate
IEnumerable<Product> local = products.Where(p => p.Price > 100);

// IQueryable: the lambda is an expression tree, a data structure
IQueryable<Product> remote = queryable.Where(p => p.Price > 100);</code></pre><p><code>IQueryProvider</code> 인터페이스는 확장 지점입니다. 모든 제공자는 <code>CreateQuery&lt;T&gt;</code> 및 <code>Execute&lt;T&gt;</code>을(를) 구현하여 이러한 표현식 트리를 대상 언어로 변환할 수 있습니다. Entity Framework는 이를 사용하여 SQL을 출력합니다. LINQ to ES|QL 제공자는 이를 사용하여 ES|QL을 출력합니다.</p><p>위 쿼리의 표현식 트리는 다음과 같습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/f864cd18a390831f8d28503a29b5835efb1842f7-1000x720.png" style="width: 100%;" /><p><em>예제 쿼리의 표현식 트리입니다.</em></p><p><code>Take</code> 은(는)<code>OrderByDescending</code> 을(를) 감싸고, 이는 <code>Where</code> 을(를) 감싸고, 이는 <code>From</code> 을(를) 감싸고, 이는 <code>EsqlQueryable&lt;Product&gt;</code> 상수를 감싸는 식으로 트리가 안쪽에서 바깥쪽으로 중첩됩니다. <code>Where</code> 술어는 그 자체로 <code>&amp;&amp;</code>, <code>&gt;=</code>, <code>==</code> 연산자에 대한 <code>BinaryExpression</code> 노드의 하위 트리이며, <code>MemberExpression</code> 은(는) 속성 액세스 및 <code>minPrice</code> 및 <code>brand</code> 변수에 대한 클로저 캡처를 위한 리프입니다. 이는 제공자가 최종 ES|QL을 생성하기 위해 거치는 데이터 구조입니다.</p><h2>작동 원리 살펴보기: 변환 파이프라인</h2><p>LINQ 표현식에서 쿼리 결과에 이르는 경로는 다음과 같이 6단계 파이프라인을 따릅니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/2a2c772b63d720f61fc9a28b2f85668fa2db8d38-1999x1036.png" style="width: 100%;" /><p><em>변환 파이프라인 개요.</em></p><h3>1. 표현식 트리 캡처</h3><p><code>.Where()</code>, <code>.OrderBy()</code>, <code>.Take()</code> 및 기타 연산자를 <code>IQueryable&lt;T&gt;</code>에 연결하면 표준 LINQ 인프라가 표현식 트리를 구축합니다. <code>EsqlQueryable&lt;T&gt;</code>은(는) <code>IQueryable&lt;T&gt;</code>을(를) 구현하고 <code>EsqlQueryProvider</code>에 위임합니다.</p><h3>2. 번역</h3><p>쿼리가 실행될 때(열거, <code>ToList()</code> 호출 또는 <code>await foreach)</code> 사용), <code>EsqlExpressionVisitor</code>은(는) 표현식 트리를 안쪽에서 바깥쪽으로 탐색합니다. 각 LINQ 메서드 호출을 전문 방문자에게 전달합니다.</p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">방문자</td><td style="border: 1px solid #ccc; padding: 4px;">번역합니다</td><td style="border: 1px solid #ccc; padding: 4px;">안으로</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">WhereClauseVisitor</td><td style="border: 1px solid #ccc; padding: 4px;">.Where(predicate)</td><td style="border: 1px solid #ccc; padding: 4px;">WHERE condition</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">SelectProjectionVisitor</td><td style="border: 1px solid #ccc; padding: 4px;">.Select(선택기)</td><td style="border: 1px solid #ccc; padding: 4px;">EVAL + KEEP + RENAME</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">GroupByVisitor</td><td style="border: 1px solid #ccc; padding: 4px;">.GroupBy().Select()</td><td style="border: 1px solid #ccc; padding: 4px;">STATS ... BY</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OrderByVisitor</td><td style="border: 1px solid #ccc; padding: 4px;">.OrderBy() / .ThenBy()</td><td style="border: 1px solid #ccc; padding: 4px;">SORT 필드 [ASC\|DESC]</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">EsqlFunctionTranslator</td><td style="border: 1px solid #ccc; padding: 4px;">EsqlFunctions.*, Math.*, 스트링 메서드</td><td style="border: 1px solid #ccc; padding: 4px;">80개 이상의 ES|QL 함수</td>
                    </tr>
              </tbody>
            </table>
          <p>변환 과정에서 표현식에서 참조되는 C# 변수는 명명된 매개변수로 캡처됩니다.</p><h3>3. 쿼리 모델</h3><p>방문자는 스트링을 직접 생성하지 않습니다. 대신 <code>QueryCommand</code> 객체, 즉 불변의 중간 표현을 생성합니다. <code>FromCommand</code>, <code>WhereCommand</code>, <code>SortCommand</code>, <code>LimitCommand</code>은(는) 각각 하나의 ES|QL 처리 명령을 나타냅니다. 이들은 <code>EsqlQuery</code> 모델로 수집됩니다.</p><p></p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/2adc349b6cf655b96b7b3e826a134e8a17fe42fd-1999x1036.png" style="width: 100%;" /><p><em>쿼리 모델 및 명령 패턴입니다.</em></p><p>이 중간 모델은 표현식 트리 및 출력 형식에서 모두 분리되어 있습니다. 형식을 지정하기 전에 검사하거나, 가로채거나(<code>IEsqlQueryInterceptor</code>을(를) 통해), 수정할 수 있습니다.</p><h3>4. 형식 지정</h3><p><code>EsqlFormatter</code> 각 <code>QueryCommand</code>을(를) 순서대로 방문하여 최종 ES|QL 스트링을 생성합니다. 각 명령은 ES|QL이 처리 명령을 연결하는 데 사용하는 파이프(|) 연산자로 구분되어 한 줄로 표시됩니다. 특수 문자가 포함된 식별자는 백틱으로 자동 이스케이프 처리됩니다.</p><h3>5. 실행</h3><p>형식화된 ES|QL 스트링과 캡처된 매개변수는 JSON 페이로드로 Elasticsearch의 <code>/_query</code> 엔드포인트로 전송됩니다. <code>IEsqlQueryExecutor</code> 인터페이스는 계층형 패키지 아키텍처가 적용되는 전송 계층을 추상화합니다.</p><h3>6. 구체화</h3><p><code>EsqlResponseReader</code> 전체 결과 세트를 메모리에 버퍼링하지 않고 JSON 응답을 스트리밍합니다. 쿼리당 한 번씩 미리 계산되는 <code>ColumnLayout</code> 트리는 플랫 ES|QL 열 이름(예: <code>address.street</code>, <code>address.city</code>)을(를) 중첩된 POCO 속성에 맵핑합니다. 각 행은 <code>T</code> 인스턴스로 조립되어 <code>IEnumerable&lt;T&gt;</code> 또는 <code>IAsyncEnumerable&lt;T&gt;</code>을(를) 통해 한 번에 하나씩 제공됩니다.</p><h2>계층 아키텍처</h2><p>LINQ to ES|QL 기능은 다음과 같이 세 개의 패키지로 나뉩니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/41b8aae860ecdc2480edcb1c1d4cc9b03cfb78c9-1999x1036.png" style="width: 100%;" /><p><em>패키지 아키텍처.</em><br/><a href="https://www.nuget.org/packages/Elastic.Esql"><strong><code>Elastic.Esql</code></strong></a> 은(는) 순수 변환 엔진입니다. HTTP 종속성이 전혀 없으며 표현식 방문자, 쿼리 모델, 포맷터 및 응답 판독기를 포함합니다. Elasticsearch 연결 없이 독립적으로 사용하여 ES|QL 쿼리를 구축하고 검사할 수 있어 테스트, 쿼리 로깅 또는 자체 실행 계층을 구축하는 데 유용합니다.</p><pre><code>// Translation-only: no Elasticsearch connection needed
var provider = new EsqlQueryProvider();
var query = new EsqlQueryable<Product>(provider)
    .From("products")
    .Where(p => p.InStock)
    .OrderByDescending(p => p.Price);

Console.WriteLine(query.ToEsqlString());
// FROM products | WHERE in_stock == true | SORT price_usd DESC</code></pre><p><a href="https://www.nuget.org/packages/Elastic.Clients.Esql"><strong><code>Elastic.Clients.Esql</code></strong></a> 경량 독립형 ES|QL 클라이언트입니다. <code>Elastic.Transport</code>을(를) 통해 <code>Elastic.Esql</code> 위에 HTTP 실행 기능을 추가합니다. 애플리케이션에 ES|QL만 필요하고 다른 Elasticsearch API는 필요하지 않은 경우, 이것이 최소한의 종속성 옵션입니다.</p><p><a href="https://www.nuget.org/packages/Elastic.Clients.Elasticsearch"><strong><code>Elastic.Clients.Elasticsearch</code></strong></a> 완전한 Elasticsearch .NET 클라이언트입니다. 이 또한 <code>Elastic.Esql</code>을(를) 기반으로 하며 <code>client.Esql</code> 네임스페이스를 통해 LINQ 제공자를 노출합니다. 대부분의 애플리케이션에 권장되는 진입점입니다.</p><p>두 실행 계층 패키지 모두 변환과 전송을 연결하는 전략 인터페이스인 <code>IEsqlQueryExecutor</code>의 자체 구현을 제공합니다.</p><p>세 패키지 모두 소스에서 생성된 <code>JsonSerializerContext</code>와(과) 함께 사용할 경우 Native AOT와 호환됩니다. 전체 클라이언트에 대한 자세한 내용은 <a href="https://www.elastic.co/docs/reference/elasticsearch/clients/dotnet/source-serialization#native-aot">Native AOT 설명서</a>를 확인하세요.</p><h2>기본을 넘어서</h2><p>위의 예에서는 필터링, 정렬 및 페이지 매김을 다뤘습니다. 공급자는 더 광범위한 작업을 지원합니다.</p><h3>집계</h3><p><code>GroupBySelect</code>의 집계 함수와 결합하면 ES|QL <a href="https://www.elastic.co/docs/reference/query-languages/esql/commands/stats-by"><code>STATS ... BY</code></a>(으)로 변환됩니다.</p><pre><code>var stats = client.Esql.Query<Product, object>(q => q
    .GroupBy(p => p.Brand)
    .Select(g => new
    {
        Brand = g.Key,
        Count = g.Count(),
        AvgPrice = g.Average(p => p.Price),
        MaxPrice = g.Max(p => p.Price)
    }));

// -> FROM products | STATS COUNT(*), AVG(price_usd), MAX(price_usd) BY brand</code></pre><h3>프로젝션</h3><p><code>Select</code>익명 유형은 <a href="https://www.elastic.co/docs/reference/query-languages/esql/commands/eval"><code>EVAL</code></a>, <a href="https://www.elastic.co/docs/reference/query-languages/esql/commands/keep"><code>KEEP</code></a>, <a href="https://www.elastic.co/docs/reference/query-languages/esql/commands/rename"><code>RENAME</code></a> 명령을 생성합니다.</p><pre><code>var query = client.Esql.CreateQuery<Product>()
    .Select(p => new { ProductName = p.Name, p.Price, p.InStock });

// -> FROM products | KEEP name, price_usd, in_stock | RENAME name AS ProductName</code></pre><h3>풍부한 함수 라이브러리</h3><p><code>EsqlFunctions</code> 클래스를 통해 날짜/시간, 스트링, 수학, IP, 패턴 매칭 및 스코어링을 포함한 80개 이상의 ES|QL 함수를 사용할 수 있습니다. 다음과 같이 표준 <code>Math.*</code> 및 <code>string.*</code> 메서드도 변환됩니다.</p><pre><code>.Where(p => p.Name.Contains("Pro"))       // -> WHERE name LIKE "*Pro*"
.Where(p => EsqlFunctions.CidrMatch(      // -> WHERE CIDR_MATCH(ip, "10.0.0.0/8")
    p.IpAddress, "10.0.0.0/8"))</code></pre><h3>조회 조인</h3><p>교차 인덱스 조회는 ES|QL <a href="https://www.elastic.co/docs/reference/query-languages/esql/commands/lookup-join"><code>LOOKUP JOIN</code></a>(으)로 변환됩니다.</p><pre><code>var enriched = client.Esql.Query<Product, object>(q => q
    .LookupJoin<Product, CategoryLookup, string, object>(
        "category-lookup-index",
        product => product.Id,
        category => category.CategoryId,
        (product, category) => new { product.Name, category!.CategoryLabel }));</code></pre><h3>원시 ES|QL 이스케이프 해치</h3><p>LINQ 제공자가 아직 지원하지 않는 ES|QL 기능의 경우 다음과 같이 원시 조각을 추가할 수 있습니다.</p><pre><code>var results = client.Esql.Query<Product>(q => q
    .Where(p => p.InStock)
    .RawEsql("| EVAL discounted = price_usd * 0.9"));</code></pre><h3>서버 측 비동기 쿼리</h3><p>장기 실행 쿼리의 경우 다음과 같이 서버에서 백그라운드 처리를 위해 제출하세요.</p><pre><code>await using var asyncQuery = await client.Esql.SubmitAsyncQueryAsync<Product>(
    q => q.Where(p => p.InStock),
    asyncQueryOptions: new EsqlAsyncQueryOptions
    {
        WaitForCompletionTimeout = TimeSpan.FromSeconds(5),
        KeepAlive = TimeSpan.FromMinutes(10)
    });

await asyncQuery.WaitForCompletionAsync();
await foreach (var product in asyncQuery.AsAsyncEnumerable())
    Console.WriteLine(product.Name);</code></pre><p>서버 측 비동기 쿼리는 일반적인 시간 초과 임계값을 초과할 수 있는 장기 실행 분석 쿼리/대규모 데이터 세트 처리에 특히 유용합니다. 또한 엄격한 HTTP 시간 초과를 적용하는 로드 밸런서, API 게이트웨이 또는 프록시가 있는 시간 초과에 민감한 환경에서도 유용합니다. 비동기 쿼리는 제출과 결과 검색을 분리하여 연결 끊김을 방지합니다.</p><h2>시작하기</h2><p>LINQ to ES|QL은 다음 버전부터 사용 가능합니다.</p><ul><li><strong>Elastic.Clients.Elasticsearch v9.3.4</strong> (9.x 브랜치)</li><li><strong>Elastic.Clients.Elasticsearch v8.19.18</strong> (8.x 브랜치)</li></ul><p>NuGet에서 설치:</p><p><code>dotnet add package Elastic.Clients.Elasticsearch</code></p><p>진입점은 <code>client.Esql</code> 에 있습니다.</p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">메서드</td><td style="border: 1px solid #ccc; padding: 4px;">반환</td><td style="border: 1px solid #ccc; padding: 4px;">사용 사례</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Query<T>(...)</td><td style="border: 1px solid #ccc; padding: 4px;">IEnumerable<T></td><td style="border: 1px solid #ccc; padding: 4px;">동기식 실행</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">QueryAsync<T>(...)</td><td style="border: 1px solid #ccc; padding: 4px;">IAsyncEnumerable<T></td><td style="border: 1px solid #ccc; padding: 4px;">비동기 스트리밍</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">CreateQuery<T>()</td><td style="border: 1px solid #ccc; padding: 4px;">IEsqlQueryable<T></td><td style="border: 1px solid #ccc; padding: 4px;">고급 구성 및 검사</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">SubmitAsyncQueryAsync<T>(...)</td><td style="border: 1px solid #ccc; padding: 4px;">EsqlAsyncQuery<T></td><td style="border: 1px solid #ccc; padding: 4px;">장기 실행 서버 측 쿼리</td>
                    </tr>
              </tbody>
            </table>
          <p>쿼리 옵션, 다중 필드 액세스, 중첩 객체, 다중 값 필드 처리 등 전체 기능에 대한 참조는 <a href="https://www.elastic.co/docs/reference/elasticsearch/clients/dotnet/linq-to-esql">LINQ to ES|QL 설명서</a>를 확인하세요.</p><h2>결론</h2><p>LINQ to ES|QL은 C# LINQ의 모든 표현력을 Elasticsearch의 ES|QL 쿼리 언어로 제공하여 쿼리 스트링을 직접 작성하지 않고도 강력한 형식의 조합 가능한 쿼리를 작성할 수 있도록 해줍니다. 자동 매개변수 캡처, 스트리밍 구체화, 그리고 독립 실행형 변환부터 전체 Elasticsearch 클라이언트까지 확장 가능한 계층형 패키지 아키텍처를 통해 모든 규모의 .NET 애플리케이션에 자연스럽게 통합됩니다. 최신 클라이언트를 설치하고 LINQ 표현식에서 인덱스를 지정하기만 하면 나머지는 제공자가 처리합니다.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[TypeScript로 Elasticsearch MCP 서버 생성]]></title>
            <description><![CDATA[TypeScript와 Claude Desktop을 사용하여 Elasticsearch MCP 서버를 생성하는 방법을 알아보세요.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/elastic-mcp-server-typescript-claude</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/elastic-mcp-server-typescript-claude</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Jeffrey Rengifo]]></dc:creator>
            <pubDate>Fri, 27 Mar 2026 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/0bb24c05fff391f42070c2883182ea6fe9cb9680-1280x720.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>Elasticsearch에서 대규모 지식 기반을 다룰 때, 정보를 찾아내는 것은 첫 관문을 넘긴 것에 불과합니다. 엔지니어는 종종 여러 문서에서 결과를 종합하고, 요약을 작성하며, 답변을 출처까지 추적해야 합니다. 모델 컨텍스트 프로토콜(MCP)은 이를 달성하기 위해 Elasticsearch를 거대 언어 모델(LLM) 기반 애플리케이션과 연결하는 표준화된 방법을 제공합니다. Elastic은 Elastic Agent Builder(기능 중 <a href="https://www.elastic.co/docs/solutions/search/agent-builder/mcp-server">MCP 엔드포인트</a> 포함)와 같은 공식 솔루션을 제공하지만, 사용자 지정 MCP 서버를 구축하면 검색 논리, 결과 형식, 검색된 콘텐츠가 종합, 요약, 인용을 위해 LLM에 전달되는 방식을 완전히 제어할 수 있습니다.</p><p>이 글에서는 사용자 지정 Elasticsearch MCP 서버 구축의 장점을 살펴보고, Elasticsearch를 LLM 기반 애플리케이션에 연결하는 TypeScript로 서버를 생성하는 방법을 보여드리겠습니다.</p><h2>사용자 지정 Elasticsearch MCP 서버를 구축해야 하는 이유는 무엇입니까?</h2><p>Elastic은 <a href="https://www.elastic.co/docs/solutions/search/mcp">MCP 서버</a>에 대한 몇 가지 대안을 제공합니다.</p><ul><li><a href="https://www.elastic.co/docs/solutions/search/agent-builder/mcp-server">Elasticsearch 9.2 이상 버전용 Elastic Agent Builder MCP 서버</a></li><li><a href="https://github.com/elastic/mcp-server-elasticsearch?tab=readme-ov-file#elasticsearch-mcp-server">구버전용 Elasticsearch MCP 서버(Python)</a></li></ul><p>MCP 서버가 Elasticsearch와 상호 작용하는 방식을 더 세밀하게 제어하고 싶다면, 직접 사용자 지정 서버를 구축하여 요구 사항에 딱 맞게 최적화할 수 있는 유연성을 확보할 수 있습니다. 예를 들어, Agent Builder의 MCP 엔드포인트는 Elasticsearch 쿼리 언어(ES|QL) 쿼리로 제한되지만, 사용자 지정 서버를 사용하면 전체 쿼리 DSL을 사용할 수 있습니다. 또한 결과를 LLM으로 전달되기 전에 결과의 서식을 지정하는 방법을 제어할 수 있으며, 이번 튜토리얼에서 다룰 OpenAI 기반 요약 기능과 같은 추가적인 처리 단계를 통합할 수도 있습니다.</p><p>이 글을 마칠 때쯤이면, Elasticsearch 인덱스에 저장된 정보를 검색하고, 요약하며, 인용을 제공하는 TypeScript로 된 MCP 서버를 갖게 됩니다. 검색에는 Elasticsearch를, 요약 및 인용 생성에는 OpenAI <code>gpt-4o-mini</code> 모델을 사용하며, 사용자 쿼리를 받고 응답을 제공하는 MCP 클라이언트와 UI로는 Claude Desktop을 사용할 것입니다. 최종적으로 엔지니어가 조직 내 기술 문서 전반에서 모범 사례를 발견하고 종합할 수 있도록 돕는 내부 지식 어시스턴트를 구축하게 됩니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/ec5771a874cf9740d4cac6888622cbe8cd6aede7-1999x1133.png" style="width: 100%;" /><h2>필수 구성 요소:</h2><ul><li>Node.js 20+</li><li>Elasticsearch</li><li>OpenAI API 키</li><li>Claude Desktop</li></ul><h3>MCP란 무엇입니까?</h3><p><a href="https://www.elastic.co/what-is/mcp">MCP</a>는 <a href="https://www.anthropic.com/news/model-context-protocol">Anthropic</a>에서 만든 오픈 표준으로, LLM과 Elasticsearch와 같은 외부 시스템 간에 안전한 양방향 연결을 제공합니다. MCP의 현황에 대한 자세한 내용은 <a href="https://www.elastic.co/search-labs/blog/mcp-current-state">이 글</a>에서 확인할 수 있습니다.</p><p>MCP 환경은 광범위한 사용 사례를 지원하는 서버들이 등장하며 <a href="https://www.elastic.co/search-labs/blog/mcp-current-state#mcp-project-updates:-transport,-elicitation,-and-structured-tooling">매일 진화</a>하고 있습니다. 게다가, 이 글에서 보여 드릴 것처럼 자신만의 맞춤형 MCP 서버를 구축하는 것도 매우 쉽습니다.</p><h3>MCP 클라이언트</h3><p><a href="https://modelcontextprotocol.io/clients">사용 가능한 MCP 클라이언트 목록</a>은 매우 방대하며, 각 클라이언트에는 저마다의 특징과 제한 사항이 있습니다. 단순함과 대중성을 고려하여 <a href="https://claude.ai/download">Claude Desktop</a>을 MCP 클라이언트로 사용하겠습니다. Claude Desktop은 사용자가 자연어로 질문을 던지는 채팅 인터페이스 역할을 하며, MCP 서버에 노출된 도구를 자동으로 호출하여 문서를 검색하고 요약을 생성합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/66eb0b11473347b6cf2d85718251eeac38d6249d-1999x1491.png" style="width: 100%;" /><h2>Elasticsearch MCP 서버 생성하기</h2><p><a href="https://github.com/modelcontextprotocol/typescript-sdk">TypeScript SDK</a>를 사용하면, 사용자 쿼리 입력을 기반으로 Elasticsearch 데이터를 쿼리하는 방법을 이해하는 서버를 쉽게 만들 수 있습니다.</p><p>이 글에서는 Elasticsearch MCP 서버를 Claude Desktop 클라이언트와 통합하는 단계를 설명합니다.</p><ol><li><a href="https://www.elastic.co/search-labs/blog/elastic-mcp-server-typescript-claude#configure-mcp-server-for-elasticsearch">Elasticsearch용 MCP 서버를 구성합니다.</a></li><li><a href="https://www.elastic.co/search-labs/blog/elastic-mcp-server-typescript-claude#load-the-mcp-server-into-claude-desktop">MCP 서버를 Claude Desktop에 로드합니다.</a></li><li><a href="https://www.elastic.co/search-labs/blog/elastic-mcp-server-typescript-claude#test-it-out">테스트해 보세요.</a></li></ol><h3>Elasticsearch용 MCP 서버 구성</h3><p>시작하려면 Node 애플리케이션을 초기화하십시오:</p><pre><code>npm init -y</code></pre><p>이렇게 하면 <code>package.json</code> 파일이 생성되며, 이를 통해 이 애플리케이션에 필요한 의존성을 설치하기 시작할 수 있습니다.</p><pre><code>npm install @elastic/elasticsearch @modelcontextprotocol/sdk openai zod && npm install --save-dev ts-node @types/node typescript</code></pre><ul><li><strong>@elastic/elasticsearch</strong> 패키지를 통해 Elasticsearch Node.js 라이브러리에 액세스할 수 있습니다.</li><li><strong>@modelcontextprotocol/sdk</strong>는 MCP 서버 생성 및 관리, 도구 등록, MCP 클라이언트와의 통신 처리를 위한 핵심 도구를 제공합니다.</li><li><strong>openai</strong>를 사용하면 OpenAI 모델과 상호 작용하여 요약이나 자연어 응답을 생성할 수 있습니다.</li><li><a href="https://zod.dev/"><strong>zod</strong></a>는<strong> </strong>각 도구의 입력 및 출력 데이터에 대해 구조화된 스키마를 정의하고 검증하는 것을 돕습니다.</li></ul><p><code>ts-node</code>, <code>@types/node</code>, <code>typescript</code> 는 개발 중에 코드의 타입을 지정하고 스크립트를 컴파일하는 데 사용됩니다.</p><h4>데이터셋 설정</h4><p>Claude Desktop이 MCP 서버를 통해 쿼리할 수 있는 데이터를 제공하기 위해, 가상의 <a href="https://github.com/Delacrobix/typescript-elasticsearch-mcp/blob/main/dataset.json">내부 지식 기반 데이터 세트</a>를 사용하겠습니다. 이 데이터 세트의 문서는 다음과 같습니다.</p><pre><code>{
    "id": 5,
    "title": "Logging Standards for Microservices",
    "content": "Consistent logging across microservices helps with debugging and tracing. Use structured JSON logs and include request IDs and timestamps. Avoid logging sensitive information. Centralize logs in Elasticsearch or a similar system. Configure log rotation to prevent storage issues and ensure logs are searchable for at least 30 days.",
    "tags": ["logging", "microservices", "standards"]
}</code></pre><p>데이터를 수집하기 위해, Elasticsearch에 인덱스를 생성하고 데이터 세트를 로드하는 스크립트를 준비했습니다. <a href="https://github.com/Delacrobix/typescript-elasticsearch-mcp/blob/main/setup.ts">여기서</a> 확인하실 수 있습니다.</p><h4>MCP 서버</h4><p><a href="https://github.com/Delacrobix/typescript-elasticsearch-mcp/blob/main/index.ts"><code>index.ts</code></a>(이)라는 이름의 파일을 생성하고, 의존성을 가져오고 환경 변수를 처리하기 위해 다음 코드를 추가하세요.</p><pre><code>// index.ts
import { z } from "zod";
import { Client } from "@elastic/elasticsearch";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import OpenAI from "openai";

const ELASTICSEARCH_ENDPOINT =
  process.env.ELASTICSEARCH_ENDPOINT ?? "http://localhost:9200";
const ELASTICSEARCH_API_KEY = process.env.ELASTICSEARCH_API_KEY ?? "";
const OPENAI_API_KEY = process.env.OPENAI_API_KEY ?? "";
const INDEX = "documents";</code></pre><p>또한, Elasticsearch와 OpenAI 호출을 처리할 클라이언트들을 초기화해 보겠습니다.</p><pre><code>const openai = new OpenAI({
  apiKey: OPENAI_API_KEY,
});

const _client = new Client({
  node: ELASTICSEARCH_ENDPOINT,
  auth: {
    apiKey: ELASTICSEARCH_API_KEY,
  },
});</code></pre><p>구현을 더 견고하게 만들고 입력 및 출력 데이터의 구조를 보장하기 위해, <a href="https://zod.dev/"><code>zod</code></a>(을)를 사용하여 스키마를 정의하겠습니다. 이를 통해 런타임에 데이터를 검증하고, 오류를 조기에 발견하며, 도구의 응답을 프로그램 방식으로 더 쉽게 처리할 수 있습니다.</p><pre><code>const DocumentSchema = z.object({
  id: z.number(),
  title: z.string(),
  content: z.string(),
  tags: z.array(z.string()),
});

const SearchResultSchema = z.object({
  id: z.number(),
  title: z.string(),
  content: z.string(),
  tags: z.array(z.string()),
  score: z.number(),
});

type Document = z.infer<typeof DocumentSchema>;
type SearchResult = z.infer<typeof SearchResultSchema>;</code></pre><p>구조화된 출력을 자세히 알아보려면 <a href="https://www.elastic.co/search-labs/blog/structured-outputs-elasticsearch-guide">여기</a>를 참조하세요.</p><p>이제 MCP 서버를 초기화해 보겠습니다.</p><pre><code>const server = new McpServer({
  name: "Elasticsearch RAG MCP",
  description:
    "A RAG server using Elasticsearch. Provides tools for document search, result summarization, and source citation.",
  version: "1.0.0",
});</code></pre><h4>MCP 도구 정의</h4><p>모든 구성이 완료되면, MCP 서버가 외부에 제공할 도구를 작성하기 시작할 수 있습니다. 이 서버는 두 가지 도구를 외부에 제공합니다.</p><ul><li><strong><code>search_docs</code>: </strong>전체 텍스트 검색을 사용하여 Elasticsearch에서 문서를 검색합니다.</li><li><strong><code>summarize_and_cite</code>:</strong> 사용자의 질문에 답하기 위해, 이전에 검색된 문서들로부터 정보를 요약하고 종합합니다. 이 도구는 또한 출처 문서를 참조하는 인용 정보를 추가합니다.</li></ul><p>이 도구들은 함께 작동하여 간단한 &#x27;검색 후 요약&#x27; 워크플로우를 형성합니다. 하나의 도구가 관련 문서를 가져오면, 다른 도구가 해당 문서들을 바탕으로 인용구가 포함된 요약 응답을 생성하는 방식입니다.</p><h4>도구 응답 형식</h4><p>각 도구는 임의의 입력 매개변수를 허용할 수 있지만, 다음과 같은 구조로 응답해야 합니다.</p><ul><li><strong>내용:</strong> 비정형 형식으로 된 도구의 응답입니다. 이 필드는 일반적으로 텍스트, 이미지, 오디오, 링크 또는 임베딩을 반환하는 데 사용됩니다. 이 애플리케이션의 경우 도구가 생성한 정보를 포함한 서식 있는 텍스트를 반환하는 데 사용됩니다.</li><li><strong>structuredContent: </strong>각 도구의 결과를 구조화된 형식으로 제공하기 위해 사용되는 선택적 반환 값입니다. 이는 프로그램 방식의 처리에 유용합니다. 비록 이 MCP 서버에서는 사용되지 않지만, 다른 도구를 개발하거나 결과를 프로그램 방식으로 처리하고자 할 때 유용하게 활용될 수 있습니다.</li></ul><p>그 구조를 염두에 두고, 각 도구에 대해 자세히 살펴보겠습니다.</p><h4>Search_docs 도구</h4><p>이 도구는 사용자의 쿼리를 기반으로 가장 관련성 높은 문서들을 검색하기 위해 Elasticsearch 인덱스에서 <a href="https://www.elastic.co/docs/solutions/search/full-text">전체 텍스트 검색</a>을 수행합니다. 또한 주요 일치 항목을 강조하고, 연관성 점수와 함께 빠른 개요를 제공합니다.</p><pre><code>server.registerTool(
  "search_docs",
  {
    title: "Search Documents",
    description:
      "Search for documents in Elasticsearch using full-text search. Returns the most relevant documents with their content, title, tags, and relevance score.",
    inputSchema: {
      query: z
        .string()
        .describe("The search query terms to find relevant documents"),
      max_results: z
        .number()
        .optional()
        .default(5)
        .describe("Maximum number of results to return"),
    },
    outputSchema: {
      results: z.array(SearchResultSchema),
      total: z.number(),
    },
  },
  async ({ query, max_results }) => {
    if (!query) {
      return {
        content: [
          {
            type: "text",
            text: "Query parameter is required",
          },
        ],
        isError: true,
      };
    }

    try {
      const response = await _client.search({
        index: INDEX,
        size: max_results,
        query: {
          bool: {
            must: [
              {
                multi_match: {
                  query: query,
                  fields: ["title^2", "content", "tags"],
                  fuzziness: "AUTO",
                },
              },
            ],
            should: [
              {
                match_phrase: {
                  title: {
                    query: query,
                    boost: 2,
                  },
                },
              },
            ],
          },
        },
        highlight: {
          fields: {
            title: {},
            content: {},
          },
        },
      });

      const results: SearchResult[] = response.hits.hits.map((hit: any) => {
        const source = hit._source as Document;

        return {
          id: source.id,
          title: source.title,
          content: source.content,
          tags: source.tags,
          score: hit._score ?? 0,
        };
      });

      const contentText = results
        .map(
          (r, i) =>
            `[${i + 1}] ${r.title} (score: ${r.score.toFixed(
              2,
            )})\n${r.content.substring(0, 200)}...`,
        )
        .join("\n\n");

      const totalHits =
        typeof response.hits.total === "number"
          ? response.hits.total
          : (response.hits.total?.value ?? 0);

      return {
        content: [
          {
            type: "text",
            text: `Found ${results.length} relevant documents:\n\n${contentText}`,
          },
        ],
        structuredContent: {
          results: results,
          total: totalHits,
        },
      };
    } catch (error: any) {
      console.log("Error during search:", error);

      return {
        content: [
          {
            type: "text",
            text: `Error searching documents: ${error.message}`,
          },
        ],
        isError: true,
      };
    }
  }
);</code></pre><p><em>We configure <code><a href="https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-fuzzy-query">fuzziness</a>: “AUTO”</code> to have a variable typo tolerance based on the length of the token that’s being analyzed. We also set <code>title^2</code> to increase the score of the documents where the match happens on the title 필드.</em></p><h4>summarize_and_cite 도구</h4><p>이 도구는 이전 검색에서 가져온 문서들을 바탕으로 요약을 생성합니다. 사용자의 질문에 답하기 위해 OpenAI의 <code>gpt-4o-mini</code> 모델을 사용하여 가장 관련성 높은 정보를 종합하며, 검색 결과에서 직접 도출된 응답을 제공합니다. 요약과 더불어, 사용된 출처 문서들에 대한 인용 메타데이터도 함께 반환합니다.</p><pre><code>server.registerTool(
  "summarize_and_cite",
  {
    title: "Summarize and Cite",
    description:
      "Summarize the provided search results to answer a question and return citation metadata for the sources used.",
    inputSchema: {
      results: z
        .array(SearchResultSchema)
        .describe("Array of search results from search_docs"),
      question: z.string().describe("The question to answer"),
      max_length: z
        .number()
        .optional()
        .default(500)
        .describe("Maximum length of the summary in characters"),
      max_docs: z
        .number()
        .optional()
        .default(5)
        .describe("Maximum number of documents to include in the context"),
    },
    outputSchema: {
      summary: z.string(),
      sources_used: z.number(),
      citations: z.array(
        z.object({
          id: z.number(),
          title: z.string(),
          tags: z.array(z.string()),
          relevance_score: z.number(),
        })
      ),
    },
  },
  async ({ results, question, max_length, max_docs }) => {
    if (!results || results.length === 0 || !question) {
      return {
        content: [
          {
            type: "text",
            text: "Both results and question parameters are required, and results must not be empty",
          },
        ],
        isError: true,
      };
    }

    try {
      const used = results.slice(0, max_docs);

      const context = used
        .map(
          (r: SearchResult, i: number) =>
            `[Document ${i + 1}: ${r.title}]\\n${r.content}`
        )
        .join("\n\n---\n\n");

      // Generate summary with OpenAI
      const completion = await openai.chat.completions.create({
        model: "gpt-4o-mini",
        messages: [
          {
            role: "system",
            content:
              "You are a helpful assistant that answers questions based on provided documents. Synthesize information from the documents to answer the user's question accurately and concisely. If the documents don't contain relevant information, say so.",
          },
          {
            role: "user",
            content: `Question: ${question}\\n\\nRelevant Documents:\\n${context}`,
          },
        ],
        max_tokens: Math.min(Math.ceil(max_length / 4), 1000),
        temperature: 0.3,
      });

      const summaryText =
        completion.choices[0]?.message?.content ?? "No summary generated.";

      const citations = used.map((r: SearchResult) => ({
        id: r.id,
        title: r.title,
        tags: r.tags,
        relevance_score: r.score,
      }));

      const citationText = citations
        .map(
          (c: any, i: number) =>
            `[${i + 1}] ID: ${c.id}, Title: "${c.title}", Tags: ${c.tags.join(
              ", ",
            )}, Score: ${c.relevance_score.toFixed(2)}`,
        )
        .join("\n");

      const combinedText = `Summary:\\n\\n${summaryText}\\n\\nSources used (${citations.length}):\\n\\n${citationText}`;

      return {
        content: [
          {
            type: "text",
            text: combinedText,
          },
        ],
        structuredContent: {
          summary: summaryText,
          sources_used: citations.length,
          citations: citations,
        },
      };
    } catch (error: any) {
      return {
        content: [
          {
            type: "text",
            text: `Error generating summary and citations: ${error.message}`,
          },
        ],
        isError: true,
      };
    }
  }
);</code></pre><p>마지막으로, <a href="https://github.com/modelcontextprotocol/typescript-sdk?tab=readme-ov-file#stdio">stdio</a>를 사용하여 서버를 시작해야 합니다. 이는 MCP 클라이언트가 서버의 표준 입력과 표준 출력 스트림을 읽고 씀으로써 통신하게 된다는 것을 의미합니다. stdio는 가장 단순한 전송 옵션이며, 클라이언트를 통해 하위 프로세스로 실행되는 로컬 MCP 서버에 적합합니다. 파일 끝에 다음 코드를 추가합니다.</p><pre><code>const transport = new StdioServerTransport();
server.connect(transport);</code></pre><p>이제 다음 명령어를 사용하여 프로젝트를 컴파일하십시오:</p><pre><code>npx tsc index.ts --target ES2022 --module node16 --moduleResolution node16 --outDir ./dist --strict --esModuleInterop</code></pre><p>이렇게 하면 <code>dist</code> 폴더가 생성되고, 그 안에 <code>index.js</code> 파일이 생성됩니다.</p><h3>MCP 서버를 Claude Desktop에 로드</h3><p>Claude Desktop에서 MCP 서버를 구성하려면 <a href="https://modelcontextprotocol.io/docs/develop/connect-local-servers">이 가이드</a>를 따르세요. Claude 구성 파일에서 다음 값들을 설정해야 합니다.</p><pre><code>{
  "mcpServers": {
    "elasticsearch-rag-mcp": {
      "command": "node",
      "args": [   "/Users/user-name/app-dir/dist/index.js"
      ],
      "env": {
        "ELASTICSEARCH_ENDPOINT": "your-endpoint-here",
        "ELASTICSEARCH_API_KEY": "your-api-key-here",
        "OPENAI_API_KEY": "your-openai-key-here"
      }
    }
  }
}</code></pre><p><code>args</code> 값은 <code>dist</code> 폴더 안에 있는 컴파일된 파일을 가리켜야 합니다. 또한 코드에 정의된 것과 똑같은 이름으로 구성 파일 내에 환경 변수를 설정해야 합니다.</p><h3>테스트해 보기</h3><p>각 도구를 실행하기 전에, <strong>검색 및 도구</strong>를 클릭하여 도구들이 활성화되어 있는지 확인하세요. 여기에서 각 도구를 개별적으로 활성화하거나 비활성화할 수도 있습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/172981c2a54adabc70d5819013c3007670935605-1999x1002.png" style="width: 100%;" /><p>마지막으로 Claude Desktop 채팅에서 MCP 서버를 테스트하고 질문을 시작하십시오:</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/03654c0f8c53c714f801fba8b25747071179209b-1999x1353.png" style="width: 100%;" /><p>&#x27;<strong>인증 방법 및 역할 기반 액세스 제어에 관한 문서를 검색해 줘</strong>&#x27;라는 질문에 대해, <code>search_docs</code> 도구가 실행되어 다음과 같은 결과를 반환합니다.</p><pre><code>Most Relevant Documents:
Access Control and Role Management (highest relevance) - This document covers role-based access control (RBAC) principles, including ensuring users only have necessary permissions, regular auditing of user roles, revoking inactive accounts, and implementing just-in-time access for sensitive operations.
User Authentication with OAuth 2.0 - This document explains OAuth 2.0 authentication, which enables secure delegated access without credential sharing. It covers configuring identity providers, token management with limited scope and lifetime, and secure storage of refresh tokens.
Container Security Guidelines - While primarily about container security, this document touches on access control aspects like running containers as non-root users and avoiding embedded credentials.
Incident Response Playbook - This mentions role assignment during incidents (incident commander, communications lead, etc.), which relates to access control in emergency scenarios.
Logging Standards for Microservices - This document includes guidance on avoiding logging sensitive information, which is relevant to authentication security.</code></pre><p>응답 내용은 다음과 같습니다. &#x27;좋습니다! 인증 방식 및 역할 기반 액세스 제어에 관한 관련 문서 5개를 찾았습니다. 검색된 내용은 다음과 같습니다.&#x27;</p><p>도구 호출은 응답 페이로드의 일부로 소스 문서들을 반환하며, 이 문서들은 나중에 인용구를 생성하는 데 사용됩니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/25ff4c7e9563d99752f95540dafdc7fd211a66e3-800x530.gif" style="width: 100%;" /><p>한 번의 상호 작용 내에서 여러 도구를 연결하여 사용할 수도 있습니다. 이 경우, Claude Desktop은 사용자의 질문을 분석한 뒤, 관련 문서를 가져오기 위해 먼저 <code>search_docs</code>(을)를 호출하고, 그 결과를 <code>summarize_and_cite</code>에 전달하여 최종 답변을 생성해야 한다고 판단합니다. 이 모든 과정은 별도의 사용자 프롬프트 없이 이루어집니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/e6a8a46acb4236e77058f18bcd2f0737b5882c05-1999x1101.png" style="width: 100%;" /><p>이 경우, &#x27;<strong>우리 시스템 전반의 인증 및 액세스 제어를 개선하기 위한 핵심 권장 사항이 뭐야? 참고 문헌 포함해 줘.</strong>&#x27;라는 쿼리에 대한 답변입니다. 다음과 같은 결과를 얻었습니다.</p><pre><code>Based on the documentation, here are the main recommendations to improve authentication and access control across your systems:

Key Recommendations

1. Implement Role-Based Access Control (RBAC)
Ensure users have only the permissions necessary for their job functions. This principle of least privilege minimizes security risks by limiting access to what's truly needed. [1]

2. Regular Access Audits
Conduct regular audits of user roles and promptly revoke access for inactive accounts. This helps prevent unauthorized access from dormant or compromised accounts. [1]

3. Just-in-Time (JIT) Access
Implement just-in-time access for sensitive operations, where permissions are granted only when needed and for a limited duration. This reduces the attack surface by minimizing standing privileges. [1]

4. OAuth 2.0 for Secure Authentication
Use OAuth 2.0 to enable secure delegated access without sharing user credentials. This protocol allows third-party applications to access resources on behalf of users without exposing passwords. [2]

5. Token Security and Management
Configure your identity provider to issue tokens with limited scope and lifetime

Store refresh tokens securely
Validate access tokens consistently to maintain security [2]
References

Access Control and Role Management (Tags: security, access-control)
User Authentication with OAuth 2.0 (Tags: authentication, oauth)
These recommendations work together to create a defense-in-depth approach, where multiple security layers protect your systems from unauthorized access.</code></pre><p>이전 단계와 마찬가지로, 이 질문에 대한 각 도구의 응답을 확인할 수 있습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/cb606d356b2f7d5e4878a5eff71bc881869ac0ee-800x585.gif" style="width: 100%;" /><p><em>참고: 각 도구 사용 승인 여부를 묻는 하위 메뉴가 나타나면 <strong>항상 허용</strong> 또는 <strong>한 번 허용</strong>을 선택하십시오.</em></p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/aea942ba9b0037526ea215bec65690f1a5c3099c-1522x250.png" style="width: 100%;" /><h2>결론</h2><p>MCP 서버는 로컬 및 원격 애플리케이션 모두를 위한 LLM 도구 표준화를 향한 중요한 진전을 의미합니다. 완전한 호환성을 구현하기 위해 아직 작업 중이지만, 이를 향해 빠르게 나아가고 있습니다.</p><p>이 글에서 Elasticsearch를 LLM 기반 애플리케이션에 연결하는 사용자 지정 MCP 서버를 TypeScript로 구축하는 방법을 배웠습니다. 서버는 두 가지 도구를 제공합니다. Query DSL을 사용하여 관련 문서를 가져오는 <code>search_docs</code>(와)과, OpenAI 모델을 통해 인용구가 포함된 요약을 생성하고 Claude Desktop을 클라이언트 UI로 사용하는 <code>summarize_and_cite</code>입니다.</p><p>다양한 클라이언트와 서버 제공 업체 간의 호환성 미래는 매우 유망해 보입니다. 다음 단계로는 에이전트에 더 많은 기능과 유연성을 추가하는 과정이 포함됩니다. 검색 템플릿을 사용하여 쿼리를 매개변수화함으로써 정확도와 유연성을 얻는 방법에 대한 실용적인 <a href="https://www.elastic.co/search-labs/blog/llm-functions-elasticsearch-intelligent-query">글</a>을 읽어 보실 수 있습니다.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Kibana 대시보드 읽기 전용 권한 출시 안내]]></title>
            <description><![CDATA[Kibana에 읽기 전용 대시보드 기능이 도입되었습니다. 이제 대시보드 작성자는 세분화된 공유 제어 기능을 통해 분석 결과의 정확성을 유지하고 의도치 않은 변경으로부터 데이터를 안전하게 보호할 수 있습니다.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/kibana-dashboards-read-only-permissions</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/kibana-dashboards-read-only-permissions</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Fabio Busatto, Teresa Alvarez Soler]]></dc:creator>
            <pubDate>Thu, 26 Mar 2026 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/11e578bc317aea30d2e10ccc0334a532f6af2ef9-720x420.jpg?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>누구나 한 번쯤 겪어보셨을 상황입니다. 로그를 모니터링하기 위해 모든 차트와 필터, 레이블 하나하나에 공을 들여 완벽한 대시보드를 만드는 데 한 시간을 투자했다고 가정해 봅시다. 그렇게 만든 대시보드를 팀과 공유합니다. 며칠 후 대시보드를 다시 열어보니 무언가 잘못되어 있습니다. 동료가 쿼리를 살짝 수정했거나, 누군가 날짜 범위를 변경했을 수 있습니다. 아마도 도움을 주려던 의도였겠지만 결국 여러분은 수정 내역을 일일이 뒤져가며 모든 숫자를 다시 확인해야 하는 상황에 놓이게 됩니다. 익숙한 상황인가요?</p><p>이것이 바로 우리가 <strong>읽기 전용 대시보드</strong>를 개발한 이유입니다. 여러분이 기다려온 바로 그 제어 기능입니다. 이제 편집 권한이 있는 다른 사용자가 대시보드를 수정하거나 손상시킬 걱정 없이 안심하고 공유하세요.</p><p>참고: 읽기 전용 권한은 Elastic Cloud Serverless에서 사용할 수 있으며 Elastic Cloud Hosted 및 Elastic 자체 관리형의 경우 버전 9.3부터 제공됩니다.</p><h2>“모든 사람이 편집할 수 있을 때 방해가 되는 경우”</h2><p>그동안 Kibana에서의 <em>공유</em>는 보통 스페이스 수준의 권한을 의미했습니다. 특정 스페이스에서 대시보드를 생성할 수 있는 사용자라면 다른 사용자의 대시보드 역시 편집하거나 삭제할 수 있었습니다. 이는 협업에는 유용하지만 예기치 못한 문제를 일으키기도 합니다. 단 한 번의 편집 실수가 잘못된 의사결정이나 신뢰 상실로 이어지고 막대한 복구 작업이 필요해질 수 있기 때문입니다.</p><p>그동안 <strong>&quot;대시보드 이름에 &#x27;읽기 전용&#x27;이라고 적어두고 사용자들이 제발 알아봐 주기만을 바란다&quot;</strong>는 식의 임시방편들을 많이 봐왔습니다. 혹은 <strong>&quot;태그를 달아두고 아무 일 없기를 간절히 기도한다&quot;</strong>는 분들도 계셨죠. 하지만 이러한 막연한 기대는 제대로 된 권한 관리 모델이 아닙니다. 여러분에게는 스페이스 접근 권한을 완전히 막지 않고도 대시보드만 확실하게 잠글 수 있는 실질적인 방법이 필요했습니다.</p><h2>실제로 어떤 문제가 발생할까요?</h2><p>데브와 케빈은 모두 운영 스페이스 내 로그 모니터링 대시보드에 대한 편집 권한을 가지고 있습니다. 케빈이 차트를 일부 변경합니다. 나중에 데브가 확인했을 때 대시보드의 숫자는 그녀가 제시했던 수치와 일치하지 않습니다. 결국 무엇이 변경되었는지(종종 기억에 의존하여) 일일이 추적하여 수정해야 하며 잘못된 데이터가 포함된 보고가 얼마나 많이 배포되었는지 우려하게 됩니다.</p><h2>읽기 전용 대시보드: 합리적인 소유권과 제어</h2><p>읽기 전용 대시보드는 다른 사용자의 편집 허용 여부를 직접 결정할 수 있는 제어 기능을 제공하여 이 문제를 해결합니다. 대시보드를 공유할 때 <strong>편집</strong>(기본 설정, 기존과 동일) 또는 <strong>보기</strong> 중 하나를 선택할 수 있습니다. <strong>보기</strong> 모드에서는 작성자 본인과 Kibana 관리자만 대시보드를 수정하거나 삭제할 수 있습니다. 그 외 모든 사용자는 대시보드를 열어보고 활용하며 데이터를 신뢰할 수 있지만 임의로 수정할 수는 없습니다.</p><h3>주요 혜택</h3><ul><li><strong>대시보드 무결성:</strong> <strong>보기</strong> 모드에서는 해당 스페이스의 편집 권한이 있는 다른 사용자라도 대시보드를 수정하거나 삭제할 수 없습니다. 만약 시도할 경우에는 대시보드가 잠겨 있다는 안내가 표시됩니다. 여러분의 차트와 로직은 그대로 유지됩니다.</li><li><strong>주도권 유지:</strong> 여러분이 소유자입니다. 소유자는 언제든지 편집하고 세부 조정하거나 업데이트할 수 있습니다. 보기 전용으로 공유한다고 해서 소유자의 권한까지 제한되는 것은 아닙니다. 다만, 다른 사용자가 보게 되는 버전만 고정할 뿐입니다.</li><li><strong>유연한 라이프사이클:</strong> 언제든지 대시보드를 &quot;편집 가능&quot; 상태로 전환할 수 있습니다. 또한 Kibana 관리자는(예: 소유자가 퇴사하는 경우) 모든 대시보드를 지속적으로 관리할 수 있습니다. 관리의 공백이 발생하지 않습니다.</li></ul><p>이제 최종 확정된 핵심 업무용 대시보드를 안심하고 광범위하게 공유하며 데이터의 일관성을 유지할 수 있습니다. 이 기능은 Serverless를 포함한 <strong>모든 Elastic 티어 및 서비스 모델</strong>에서 사용할 수 있습니다.</p><h3>권한별 기능 안내</h3><p>역할별 권한 요약:</p><ul><li><strong>대시보드 소유자:</strong> 직접 생성한 대시보드이므로 소유자가 모든 편집 권한을 가집니다.</li><li><strong>Kibana 관리자:</strong> 모든 대시보드를 관리할 수 있습니다.</li><li><strong>스페이스 편집 권한이 있는 사용자:</strong> 자신의 대시보드를 생성하고 편집할 수 있습니다. 보기 전용 대시보드는 편집하거나 삭제할 수 없습니다.</li><li><strong>스페이스 보기 권한 사용자:</strong> 대시보드 목록 확인 및 조회만 가능합니다.</li></ul>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">작업</td><td style="border: 1px solid #ccc; padding: 4px;">대시보드 소유자</td><td style="border: 1px solid #ccc; padding: 4px;">Kibana 관리자</td><td style="border: 1px solid #ccc; padding: 4px;">스페이스 편집 권한 사용자</td><td style="border: 1px solid #ccc; padding: 4px;">스페이스 보기 권한 사용자</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">대시보드 목록 및 보기</td><td style="border: 1px solid #ccc; padding: 4px;">✔</td><td style="border: 1px solid #ccc; padding: 4px;">✔</td><td style="border: 1px solid #ccc; padding: 4px;">✔</td><td style="border: 1px solid #ccc; padding: 4px;">✔</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">새 대시보드 생성</td><td style="border: 1px solid #ccc; padding: 4px;">✔</td><td style="border: 1px solid #ccc; padding: 4px;">✔</td><td style="border: 1px solid #ccc; padding: 4px;">✔</td><td style="border: 1px solid #ccc; padding: 4px;">✘</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">편집 가능한 대시보드 수정/삭제</td><td style="border: 1px solid #ccc; padding: 4px;">✔</td><td style="border: 1px solid #ccc; padding: 4px;">✔</td><td style="border: 1px solid #ccc; padding: 4px;">✔</td><td style="border: 1px solid #ccc; padding: 4px;">✘</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">읽기 전용 대시보드 수정/삭제</td><td style="border: 1px solid #ccc; padding: 4px;">✔</td><td style="border: 1px solid #ccc; padding: 4px;">✔</td><td style="border: 1px solid #ccc; padding: 4px;">✘</td><td style="border: 1px solid #ccc; padding: 4px;">✘</td>
                    </tr>
              </tbody>
            </table>
          <h2>읽기 전용 모드 활성화 방법</h2><p>새 대시보드를 저장할 때 또는 나중에 공유 메뉴에서 보기 전용으로 설정할 수 있습니다.</p><h3>새 대시보드를 저장하는 경우</h3><ul><li>대시보드를 구성한 후 <strong>저장 버튼</strong>을 클릭합니다.</li><li>&quot;새 대시보드로 저장&quot; 모달에서 <strong>권한</strong>을 찾습니다.</li><li><strong>편집 가능</strong> 에서 <strong>보기 가능</strong>으로 변경합니다.</li><li><strong>저장</strong>을 클릭하면 끝입니다. 이제 다른 모든 사용자에게는 읽기 전용으로 표시됩니다.</li></ul><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/42a71d1bb55f9d50bd079f53bf45a0e1999b27f7-1214x1306.png" style="width: 100%;" /><h2>이미 소유하고 있는 대시보드의 경우</h2><ul><li>대시보드를 엽니다.</li><li><strong>대시보드 공유</strong> 메뉴를 엽니다.</li></ul><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/e8405938557c879b1d4c262b98cf5a7f66408c04-1246x264.png" style="width: 100%;" /><ul><li>공유 모달에서 <strong>권한</strong>을 찾아 <strong>보기 가능</strong>으로 전환합니다. 변경 사항은 즉시 적용되며, 해당 스페이스의 다른 사용자는 더 이상 대시보드를 수정하거나 삭제할 수 없습니다.</li></ul><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/849405bc32701f3ebe0def012d8ae3cf3813ea0a-996x750.png" style="width: 100%;" /><ul><li><strong>공유</strong> 작업 위에 마우스를 올리면 해당 대시보드에 설정된 권한 유형을 바로 확인할 수 있습니다.</li></ul><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/80541ddb1b1bc567b0aeff693944ea8b6871d6a7-1270x320.png" style="width: 100%;" /><h3>잠겨 있는 대시보드 식별</h3><p>메인 대시보드 목록에서 편집이나 삭제가 불가능한 대시보드는 선택 체크박스가 비활성화되어 있습니다. 이를 통해 어떤 항목이 보기 전용인지 한눈에 쉽게 확인할 수 있습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/24b2eba08df86174db949c662e7886c5aea1b460-1999x876.png" style="width: 100%;" /><p>대시보드에서 편집 작업이 비활성화되며 해당 대시보드가 보기 전용으로 설정되었음을 안내하는 툴팁이 표시됩니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/e0a2f9da6dc854e876fc6dc2a7c3ef8b313b52ef-1358x330.png" style="width: 100%;" /><h2>직접 사용해 보기</h2><p>지금 바로 읽기 전용 대시보드를 이용해 보세요. 대시보드를 생성하고 <strong>보기 가능</strong>으로 설정한 뒤 공유하기만 하면 됩니다. 팀은 신뢰할 수 있는 단일 소스를 공유하게 되고 여러분은 안심할 수 있습니다. 이제 대시보드 제목에 &quot;편집 금지&quot;라고 적지 않아도 됩니다.</p><p>읽기 전용 대시보드를 어떻게 활용하고 계신지 여러분의 이야기가 궁금합니다. <a href="https://discuss.elastic.co">커뮤니티 포럼</a>에서 피드백을 공유하세요.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[셸 도구는 컨텍스트 엔지니어링을 위한 만능 해결책이 아닙니다]]></title>
            <description><![CDATA[컨텍스트 엔지니어링을 위해 존재하는 컨텍스트 검색 도구와 그 작동 방식, 장단점에 대해 알아보세요.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/search-tools-context-engineering</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/search-tools-context-engineering</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Leonie Monigatti]]></dc:creator>
            <pubDate>Wed, 25 Mar 2026 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/f91e4d07915ba7bf3b7abf15fac8fab3350f7df2-1280x720.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>에이전트에게 가장 중요한 도구는 자체 컨텍스트를 구축하는 데 사용할 수 있는 검색 도구입니다. 최근 <a href="https://www.llamaindex.ai/blog/files-are-all-you-need">LlamaIndex</a>와 <a href="https://x.com/hwchase17/status/2011814697889316930">LangChain</a>의 게시물은 <em>컨텍스트 엔지니어링을 위해 에이전트에게 셸 도구와 파일 시스템만으로 충분할까?</em>라는 논의를 불러일으켰습니다.안타깝게도 논의는 파일 시스템 대 데이터베이스라는 잘못된 초점으로 빠르게 흘러갔습니다.</p><p>이 글에서는 <em>에이전트가 자체 컨텍스트를 구축하는 데 필요한 적절한 검색 인터페이스는 무엇인가?</em>라는 질문에 다시 초점을 맞춥니다. 먼저 셸 도구와 전용 데이터베이스 도구 간의 절충점을 다룹니다. 이를 통해 에이전트의 요구에 맞는 인터페이스를 찾는 데 유용한 실용적인 프레임워크를 제공합니다.</p><h2>&#x27;컨텍스트 구축&#x27;이란 에이전트에게 실제로 무슨 의미일까요?</h2><p>초기 <a href="https://www.elastic.co/what-is/retrieval-augmented-generation">검색 증강 생성(RAG) 파이프라인</a>에서는 개발자가 고정된 검색 파이프라인을 설계했고, 대규모 언어 모델(LLM)은 컨텍스트의 수동적인 수신자였습니다. 이는 근본적인 한계였습니다. 필요한지 여부에 관계없이 모든 쿼리에서 컨텍스트가 검색되었지만 실제로 도움이 되는지 확인하지 못했습니다.</p><p>에이전트 기반 RAG로의 전환으로 이제 에이전트는 자체 컨텍스트를 구축하기 위한 일련의 검색 도구에 접근할 수 있게 되었습니다. 예를 들어, Claude Code[1]와 Cursor[2] 모두 에이전트가 다양한 검색 도구 간에 선택할 수 있도록 하고, 작업에 실제로 필요한 내용에 따라 이를 연결된 쿼리로 조합할 수도 있습니다.</p><h2>컨텍스트 엔지니어링을 위한 검색 인터페이스에는 어떤 것이 있나요?</h2><p>컨텍스트는 웹, 로컬 파일 시스템 또는 데이터베이스와 같은 다양한 위치에 존재할 수 있습니다. 에이전트는 다양한 도구를 통해 이러한 맥락에서 벗어난 데이터 소스 각각과 상호 작용할 수 있습니다.</p><ul><li><strong>셸 도구는</strong>셸 명령을 실행하고 로컬 파일 시스템에 접근할 수 있습니다. 기본 제공 셸 도구의 예로는 <a href="https://platform.claude.com/docs/en/agents-and-tools/tool-use/bash-tool">Claude API의 bash 도구</a>, <a href="https://docs.openclaw.ai/tools/exec">OpenClaw의 exec 도구</a>, 그리고 <a href="https://docs.langchain.com/oss/python/integrations/tools/bash">LangChain의 shell 도구</a>입니다.</li><li><strong>전용 데이터베이스 도구</strong>는 MCP(모델 컨텍스트 프로토콜) 서버의 도구(예: <a href="https://www.elastic.co/docs/explore-analyze/ai-features/agent-builder/mcp-server">Elastic Agent Builder MCP 서버</a>) 또는 사용자 정의 도구(예: <code>run_esql(query)</code> 또는 <code>db_list_index()</code>)로, 데이터베이스를 쿼리할 수 있습니다.</li><li><strong>전용 파일 검색 도구</strong>는 (완전한 셸 액세스 권한 없이도) 로컬 파일(또는 업로드된 파일)을 검색하고 읽을 수 있습니다. 기본 제공 파일 검색 도구의 예로는 <a href="https://ai.google.dev/gemini-api/docs/file-search">Gemini API의 파일 검색 도구</a> 또는 <a href="https://developers.openai.com/api/docs/guides/tools-file-search">OpenAI의 파일 검색 도구</a>가 있습니다.</li><li><strong>웹 검색 도구</strong>는 웹에서 정보를 검색할 수 있습니다.</li><li><strong>메모리 도구</strong>는 (저장 방식에 관계없이) 장기 기억을 저장하고 불러올 수 있습니다.</li></ul><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/115f20c8ded259e508f51524b2c06bdc702d70ab-1999x1050.png" style="width: 100%;" /><p>보시다시피, 셸 도구는 다재다능하며 다음과 같은 다양한 데이터 소스에서 컨텍스트를 검색하는 데 사용할 수 있습니다.</p><ul><li><strong>파일 시스템:</strong> 에이전트는 디렉터리 구조를 탐색하고(ls, find), 관련 콘텐츠를 검색하고(grep, cat), 충분한 컨텍스트를 구축할 때까지 이 과정을 반복합니다.</li><li><strong>데이터베이스:</strong> 에이전트는 데이터베이스 명령줄 인터페이스 (CLI) 도구(예: <a href="https://www.elastic.co/docs/reference/query-languages/sql/sql-cli"><code>elasticsearch-sql-cli</code></a>)를 사용하거나, curl을 통해 HTTP API를 호출하거나, 스크립트를 실행할 수 있습니다. 이는 올바른 도구 사용을 안내하기 위해 에이전트 컨텍스트에 삽입된 재사용 가능하고 문서화된 예시인 에이전트 스킬과 함께 사용하면 특히 유용합니다(예: <a href="https://github.com/elastic/agent-skills">Elastic Agent</a> Skills for Elasticsearch).</li><li><strong>웹: </strong>에이전트는 검색 제공업체의 API를 통해 curl 명령을 사용하여 웹 검색을 실행할 수 있습니다.</li></ul><p>그러나 셸 도구는 직접적인 시스템 액세스를 제공하므로 격리된 샌드박스 환경에서 실행하고 모든 실행된 명령을 로깅하는 등의 안전 조치가 필요합니다.</p><h2>각 검색 인터페이스의 사용 시점</h2><p>적합한 검색 인터페이스는 데이터, 쿼리 패턴 및 사용 사례에 따라 달라집니다. 이 섹션은 실용적인 출발점이 됩니다.</p><h3>파일 시스템이 데이터베이스를 쓸모없게 만들지는 않습니다</h3><p>파일 시스템과 데이터베이스의 논쟁은 저장 공간 계층에 관한 것이 아닙니다. 예를 들어, LangChain은 <a href="https://x.com/hwchase17/status/2011814697889316930">자체 메모리 시스템</a>이 실제로 실제 파일 시스템에 메모리를 저장하지 않는다고 설명합니다. 대신 데이터베이스에 메모리를 저장하고 이를 에이전트에게 파일 세트로 <em>표현합니다</em>[3].</p><p>파일 시스템은 코딩 에이전트와 같은 파일 기반 사용 사례에 매우 적합합니다. 또한 임시 메모장이나 작업 메모리로 사용하기에 적합하며 동시성이 문제가 되지 않는 단일 사용자 또는 단일 에이전트 시나리오에 적합합니다. 이러한 경우 물리적 파일 시스템 또는 데이터를 파일 시스템으로 표현하는 것은 목적에 맞춰진 인터페이스에 약속하기 전에 유연성을 제공합니다.</p><p>그러나 파일 시스템 저장 공간에는 실제적인 단점이 있습니다. 예를 들어 약한 동시성, 수동 스키마 적용, 원자적 트랜잭션 등이 있습니다. 이러한 단점은 애플리케이션을 확장해야 하거나 다중 에이전트 시나리오로 전환해야 할 때 더욱 분명해집니다. 이러한 단점을 무시하는 사람은 프로덕션 데이터베이스가 이미 제공하는 트랜잭션 안전이나 액세스 제어를 뒷받침하는 수십 년의 엔지니어링 없이 <a href="https://dx.tips/oops-database">더 나쁜 데이터베이스를 고통스럽게 재창조</a>해야 할 운명에 처하게 됩니다. 또한 대부분의 기업 환경에서는 이미 비즈니스에 중요한 데이터가 저장되어 있는 데이터베이스가 존재하기 때문에 데이터베이스 사용 여부를 선택할 필요가 없습니다.</p><h3>셸 도구 + 파일 시스템</h3><p>셸 도구는 파일 시스템 검색의 자연스러운 출발점입니다. 현재 코딩 에이전트가 해당 분야의 발전을 주도하고 있습니다. 로컬 파일의 코드를 다루기 때문에 본질적으로 파일 중심의 사용 사례입니다. 따라서 LLM은 코딩 작업을 위해 훈련 후 단계에서 미세 조정됩니다. 그렇기 때문에 많은 LLM은 코드를 작성하는 것뿐만 아니라 셸 명령을 사용하고 파일 시스템을 탐색하는 데도 능숙합니다.</p><p><code>ls</code> 및 <code>grep</code>과 같이 기본 제공 CLI가 있는 셸 도구를 사용하면 파일을 효과적으로 찾을 수 있습니다. grep에서는 &quot; <code>matplotlib</code>를 가져오는 모든 파일을 찾으세요&quot; 같은 쿼리가 빠르고 정확하며 저렴합니다. 하지만 에이전트가 &quot;우리 앱은 인증 실패를 어떻게 처리하나요?&quot;와 같은 개념적인 쿼리를 처리해야 하는 경우 grep을 사용한 패턴 매칭은 금방 한계에 부딪힐 수 있습니다. 명령줄에 의미 검색 기능을 제공하는 몇 가지 대안이 이러한 격차를 메우기 위해 등장했으며, 여기에는 <a href="https://github.com/jina-ai/jina-grep-cli"><code>jina-grep</code></a>이 포함됩니다.</p><p>그러나 grep과 많은 시맨틱 검색 대안들은 코퍼스 전체에서 O(n) 시간 복잡도로 실행됩니다. 코드베이스를 대상으로 하는 사용 사례에서는 이 방식이 괜찮을 수 있습니다. 그러나 데이터가 증가하면 지연이 눈에 띄게 나타납니다. 이 경우 성능을 유지하기 위해 인덱싱된 데이터 저장소가 필요합니다.</p><h3>셸 도구 + 데이터베이스</h3><p>데이터에 시맨틱 검색이나 하이브리드 검색과 같은 더 많은 검색 기능을 추가하는 또 다른 방법은 Cursor의 예처럼 데이터를 데이터베이스에 저장하는 것입니다. 또한 데이터에 복잡한 관계형 조인이나 집계가 필요한 경우 데이터베이스 인터페이스는 필수적입니다.</p><p>데이터가 파일 시스템이 아닌 데이터베이스에 있을 때, 셸 도구는 특정 사용 사례에서 경량 데이터베이스 인터페이스 역할을 할 수 있습니다. 쿼리가 CLI나 curl 호출로 간단하다면, 전용 데이터베이스 도구는 불필요한 복잡성을 더할 수 있습니다.</p><p>이러한 접근 방식은 초기 탐색 단계에서도 적합합니다. 아직 어떤 쿼리 패턴이 개발될지 모를 때 유용합니다. 이 경우, 에이전트 스킬은 목적에 맞춰 제작된 도구에 의존하지 않고도 에이전트에게 올바르게 쿼리할 수 있는 충분한 구조를 제공할 수 있습니다. 하지만 에이전트가 반복적인 작업을 위해 데이터베이스를 쿼리하는 올바른 방법을 파악하는 데 여러 번의 반복이 필요한 경우, 셸 도구를 인터페이스로 사용하는 데 따른 토큰 오버헤드가 추가 도구를 사용하지 않음으로써 얻는 단순성이라는 이점을 더 이상 정당화하지 못합니다.</p><h3>전용 데이터베이스 도구</h3><p>특히 반복되는 쿼리 패턴이 구조화되거나 분석적일 때 전용 데이터베이스 도구가 필요해집니다. <a href="https://vercel.com/blog/testing-if-bash-is-all-you-need">Vercel과 Braintrust의 블로그 게시물</a>은 고객 지원 티켓 및 영업 통화 기록과 같은 반정형 데이터에 대한 실제 검색 작업에서 서로 다른 검색 도구 세트를 갖춘 에이전트를 비교했습니다(예: &quot;미해결 이슈 중 &#x27;보안&#x27;을 언급한 것은 몇 개인가요?&quot; 또는 &quot;누군가 버그를 보고하고 나중에 누군가 이를 수정한다고 주장하는 PR을 제출한 이슈를 찾으세요?&quot;) [4].</p><p>전용 데이터베이스 도구를 사용한 에이전트는 토큰을 더 적게 사용하고, 더 빠르며, 셸 도구와 파일 시스템만 사용한 에이전트보다 실수를 더 적게 했습니다. 이를 통해 알 수 있는 교훈은 쿼리가 반정형 데이터에 대한 분석적 추론을 필요로 할 때 직접적인 데이터베이스 도구가 올바른 선택이라는 것입니다.</p><h3>검색 인터페이스 결합하기</h3><p>모든 쿼리를 완벽하게 처리할 수 있는 단일 검색 인터페이스는 없습니다. 예를 들어, Cursor는 셸 도구(grep을 통한 검색용)와 시맨틱 검색 도구를 결합하여 에이전트가 사용자 프롬프트에 따라 적합한 도구를 선택할 수 있도록 합니다. 그들은 에이전트가 특정 기호나 스트링을 매칭하기 위해 grep을 선택하고, 개념적 또는 행동 질문에 대해서는 시맨틱 검색을 선택하며, 탐색적 작업에는 둘 다를 사용한다고 보고합니다.</p><p>Vercel 실험 보고서는 동일한 결과를 보여줍니다. 셸 도구와 전용 데이터베이스 도구 모두에 접근할 수 있는 하이브리드 에이전트가 먼저 전용 데이터베이스 도구를 사용한 다음 파일 시스템을 grep하여 결과를 확인함으로써 테스트된 모든 에이전트 중 최고의 성능을 달성했습니다. 하지만 이 접근 방식은 도구 선택과 검증을 위한 추론에 더 많은 토큰과 시간을 사용합니다.</p><p>두 예시의 패턴은 동일합니다. 컴포지션이 단일 인터페이스를 능가하지만 컴포지션에는 추가 비용과 지연 시간이 발생한다는 단점이 있습니다.</p><h2>적합한 도구 세트를 찾기 위한 실용적인 권장 사항</h2><p>적합한 검색 인터페이스는 간결하고 목적에 부합하며 에이전트의 실제 쿼리 패턴에 맞춰 특화되어 있어야 합니다. 현재 가장 좋은 방법은 수백 개의 MCP 도구를 사용하는 에이전트 대신 가능한 한 적은 수의 도구를 사용하는 에이전트를 보유하는 것입니다. 이는 가능한 모든 도구를 미리 노출할 경우 발생하는 단점이 컨텍스트 윈도우를 비대화시키고 에이전트가 실제로 사용할 도구에 대해 혼란스럽게 만든다는 것입니다. 예를 들어, Claude Code는 보고된 바에 따르면 약 20개의 도구만 가지고 있습니다.</p><p>대신, 점진적 공개의 개념은 최소한의 도구 세트로 시작하고 에이전트가 필요할 때만 추가 기능을 발견하도록 하는 것입니다. Anthropic[5] 및 Cursor[6]의 연구에 따르면 이 접근 방식은 47%–85% 사이의 토큰 절감 효과를 가져옵니다. 예를 들어, Claude Code는 이를 직접 구현하여 에이전트가 API나 데이터베이스를 쿼리하는 방법을 점진적으로 발견할 수 있도록 하며, 이러한 지식이 모든 LLM 호출에서 컨텍스트를 소비하지 않도록 합니다.</p><p>에이전트의 쿼리 패턴에 익숙해지면, 에이전트가 기본적으로 액세스할 수 있는 검색 도구 세트를 다시 검토할 수 있습니다. 이 절충점을 고려할 때 유용한 방법은 <a href="https://www.elastic.co/search-labs/blog/database-retrieval-tools-context-engineering#building-the-right-database-retrieval-tools-%5C(%E2%80%9Clow-floor,-high-ceiling%E2%80%9D%5C">&#x27;낮은 바닥, 높은 천장&#x27; 원칙</a>에 다라 어떤 도구를 선택할지 결정하는 것입니다. &#x27;높은 천장&#x27; 도구는 에이전트의 잠재력을 제한하지 않습니다. 예를 들어, 다목적 셸 도구를 사용하면 에이전트가 애매한 쿼리를 포함한 전체 데이터베이스 쿼리를 작성할 수 있지만, 추론 오버헤드, 더 높은 지연 시간, 더 낮은 신뢰성이라는 대가가 따릅니다.</p><p>&#x27;낮은 바닥&#x27; 도구는 그 반대입니다. 이들은 특정 쿼리를 처리하는 전문화된 도구로, 에이전트가 최소한의 추론 오버헤드로 즉시 접근할 수 있어 비용을 낮추고 신뢰성을 높입니다. 그러나 초기 엔지니어링이 필요하고, 모든 가능한 쿼리를 다룰 수 없으며, 에이전트가 올바른 도구를 선택하기 더 어렵게 만들 수 있습니다.</p><p>각 도구를 스펙트럼으로 생각해 보세요. &#x27;낮은 바닥&#x27; 도구는 에이전트가 올바르게 사용하기 쉽지만 범위가 좁습니다. &#x27;높은 천장&#x27; 도구는 다용도이지만 효과적으로 사용하기 위해서는 더 많은 추론이 필요합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/e6d1b973be4b0a0a25c99c74f02a47e98395a3f7-1200x630.png" style="width: 100%;" /><p>대부분의 에이전트는 다양한 검색 도구의 조합이 필요합니다. 하지만 각 도구마다 추가 기능이 필요합니다. 다목적 검색 도구(예: <code>search_database()</code> 도구 또는 셸 도구)로 시작하는 것이 좋습니다. 그런 다음 보안 목적으로 이미 보관하고 있는 명령 로그를 재사용하여 도구 호출, 재시도 및 사용자 쿼리당 호출 횟수를 포함하여 에이전트가 실제로 수행하는 작업을 추적하세요. 그리고 쿼리 패턴이 반복되거나 실패하는 것을 발견하면 이를 위한 전용 도구를 구축해야 한다는 신호입니다.</p><h2>요약</h2><p>파일시스템 대 데이터베이스 논쟁은 엔지니어들이 묻고 있는 <em>에이전트가 자체 컨텍스트를 구축하는 데 필요한 적절한 검색 인터페이스는 무엇인가?</em>라는 실제 질문에서 주의를 분산시키고 있습니다. 정답은 &#x27;<em>하나도 없다</em>&#x27;일 가능성이 높습니다.</p><p>셸 도구는 다양한 외부 소스와 상호 작용할 수 있는 다용도 도구이므로 좋은 출발점이 될 수 있습니다. 하지만 구조화된 분석 쿼리가 필요한 사용 사례에서는 전용 데이터베이스 도구보다 효율성과 정확성이 떨어집니다.</p><p>목표는 에이전트의 실제 쿼리 패턴을 효과적으로 처리할 수 있는 최소한의 검색 도구 세트를 찾는 것입니다. 셸 도구를 사용하여 에이전트가 실제로 수행하는 작업을 기록하세요. 쿼리 패턴이 반복적으로 실패하는 경우, 전문화된 도구를 개발할 시점입니다.</p><h2>참고 자료</h2><p>1. Thariq(Anthropic). <a href="https://x.com/trq212/status/2027463795355095314">Claude Code 구축에서 얻은 교훈: 에이전트처럼 보기</a>(2026).</p><p>2. Cursor: 문서. <a href="https://cursor.com/docs/agent/tools/search">시맨틱 및 에이전트 검색</a>(2026).</p><p>3. Harrison Chase(LangChain). <a href="https://x.com/hwchase17/status/2011814697889316930">Agent Builder의 메모리 시스템 구축 방법</a>(2026).</p><p>4. Ankur Goyal(Braintrust) 및 Andrew Qu(Vercel). <a href="https://vercel.com/blog/testing-if-bash-is-all-you-need">&quot;bash만으로 충분한지&quot; 테스트하기</a>(2026).</p><p>5. Anthropic. <a href="https://www.anthropic.com/engineering/advanced-tool-use">Claude 개발자 플랫폼의 고급 도구 활용 소개</a>(2025).</p><p>6. Cursor. <a href="https://cursor.com/blog/dynamic-context-discovery">동적 컨텍스트 검색</a>(2026년).</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[서버리스 환경 로드 밸런싱을 위한 Elasticsearch 복제본]]></title>
            <description><![CDATA[Elastic Cloud Serverless가 검색 부하에 따라 인덱스 복제본을 자동으로 조정하여 수동 구성 없이도 최적의 쿼리 성능을 보장하는 방법을 확인해 보세요.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/elasticsearch-replicas-load-balancing-serverless</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/elasticsearch-replicas-load-balancing-serverless</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Andrei Dan]]></dc:creator>
            <pubDate>Tue, 24 Mar 2026 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/3c4c1e99b892f026b7aba098973593f8298e2ea6-1280x717.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>Elastic Cloud Serverless는 검색 부하에 따라 인덱스 복제본 수를 자동으로 조정하여 수동 구성 없이도 최적의 쿼리 성능을 보장합니다. 이 블로그에서는 복제본이 확장되는 방식, 시스템이 복제본을 추가하거나 제거하는 경우, 이것이 인덱스에 갖는 의미를 설명합니다.</p><h2>파티가 점점 붐비고 있습니다</h2><p>여러분이 피자 파티를 주최하는 중입니다. 몇 명의 친구들이 파티 장소의 각기 다른 장소에서 서빙을 돕고 있습니다. 각 친구에게 피자를 한 조각씩 건네주면, 친구들은 도착하는 배고픈 손님들에게 피자 조각을 나눠주기 시작합니다.</p><p>처음에는 모든 것이 순조롭게 진행됩니다. 손님 몇 명이 조금씩 들어오고, 친구들이 피자 조각을 나눠 주니, 모두가 만족합니다. 하지만 곧 사워도우 피자에 대한 소문이 퍼지기 시작합니다. 초인종이 계속 울립니다. 손님들이 몰려듭니다. 곧 모두가 먹고 싶어 하는 페퍼로니 피자를 들고 있는 친구 주위로 사람들이 모여들기 시작합니다.</p><p>페퍼로니 피자를 든 친구는 어쩔 줄을 모릅니다. 손님들이 기다리고 있으며, 점점 불만이 쌓이고 있고, 긴 대기 줄이 형성되었습니다. 한편, 마르게리타 피자를 든 친구는 한 조각 달라고 하는 사람이 거의 없이 가만히 서 있습니다.</p><p>이제 어떻게 할까요?</p><p>여러분은 페퍼로니 피자 몇 개를 더 주문하여 다른 친구들에게 건넵니다. 이제 한 명이 아닌 세 명의 친구가 페퍼로니 피자를 들고 있습니다. 인파가 분산되어 갑자기 3배나 많은 손님을 한꺼번에 대접할 수 있습니다.</p><p>파티를 더 주최할수록 몇 가지 사실이 분명해집니다.</p><ul><li><strong>모든 피자가 똑같이 인기 있는 것은 아닙니다.</strong> 어떤 것은 수요가 많고, 다른 것은 수요가 적습니다. 인기 없는 피자는 추가 &#x27;사본&#x27;이 없어도 됩니다. 대기열이 있는 것만 추가하면 됩니다.</li><li><strong>대기열이 너무 길어지기 전에 피자를 더 주문하세요.</strong> 친구가 허둥지둥거리고 손님들이 화를 내며 떠나기 시작할 때까지 기다린다면, 너무 오래 기다린 것입니다. 인파가 몰리는 게 보이면 피자를 한 판 더 주문하는 것이 좋습니다.</li><li><strong>피자를 너무 빨리 버리지 마세요.</strong> 페퍼로니 피자 주변에 5분 동안 인파가 줄었다고 해서 혼잡한 시간이 끝난 것은 아닙니다. 어쩌면 사람들은 음료를 다시 채우고 있거나 서로 대화하고 있을지도 모릅니다(요즘도 그러나요?). 추가 피자를 유지해 주세요. 잠잠한 상태가 한동안 계속된다면, 그땐 피자를 치워도 됩니다.</li><li><strong>도와주는 친구 수만큼만 피자를 나눠줄 수 있습니다.</strong> 도와주는 친구가 네 명이면 피자를 열 판으로 늘려도 결과는 달라지지 않습니다. 한 번에 네 조각만 제공할 수 있습니다. 피자의 수를 도와주는 인원에 맞춰야 합니다.</li><li><strong>친구가 떠나면 피자를 가져오세요.</strong> 친구 중 한 명이 떠나야 한다면 즉시 그들의 피자를 가져오세요. 피자를 방치하면 안 됩니다. 다른 사람에게 건네주거나 치워두세요.</li></ul><h2>피자부터 복제품까지</h2><p>이것을 다시 Elasticsearch에 매핑해 볼까요.</p><p>이 비유에서 피자는 복제본(인덱스 샤드 복사본), 서빙을 돕는 친구들은 검색 노드, 배고픈 손님은 검색 쿼리, 주변에 인파가 모인 인기 있는 피자는 검색 부하가 높은 핫 인덱스입니다.</p><p>특정 인덱스에서 검색 트래픽이 증가하면 추가 복제본을 생성하여 검색 노드 전체에 배포합니다. 모든 복제본은 해당 인덱스에 어떤 쿼리든 서빙할 수 있습니다. 마치 페퍼로니 피자를 든 친구가 페퍼로니 피자 조각을 나눠줄 수 있는 것처럼 말이죠. 복제본 수가 많을수록 처리량이 높아집니다. 복제본이 세 개면 복제본 한 개보다 초당 3배 더 많은 쿼리를 처리할 수 있습니다.</p><h2>허기 측정</h2><p>피자를 몇 개나 주문할지 결정하기 전에, 우리는 사람들이 얼마나 배고픈지 알아야 합니다.</p><p>Elasticsearch는 모든 샤드의 <strong>검색 부하</strong>를 추적합니다. 검색 부하는 샤드가 처리하는 검색 활동의 양을 나타내는 지표입니다. 전체 검색 수요를 파악하기 위해 인덱스의 전체 샤드에서 이를 집계합니다.</p><p>가장 중요한 것은 <strong>상대적 검색 부하</strong>입니다. 즉, 프로젝트의 전체 검색 트래픽 중 각 인덱스에 도달하는 비율을 파악하는 것입니다. 한 인덱스가 전체 검색의 60%를 받고 다른 인덱스가 5%를 받는다면, 어디에 용량을 추가해야 할지 알 수 있겠죠.</p><h2>피자에 숨겨진 수학적 원리</h2><p>다음 공식에 따라 최적의 복제본 수를 계산합니다.</p><pre><code>desired_replicas = min(ceil(L × N / (S × X)), N)</code></pre><p>위치:</p><ul><li><strong>L</strong>은 인덱스의 상대적 검색 부하(0~1 사이)입니다.</li><li><strong>N</strong> =프로젝트에서 원하는 검색 노드 개수입니다.</li><li><strong>S</strong> = 인덱스의 샤드 수입니다.</li><li><strong>X</strong> = 핫스팟을 방지하기 위한 임계값입니다(기본값: 0.5).</li></ul><p>예시: 검색 노드 4개, 기본 샤드 2개를 가진 인덱스 1개, 검색 트래픽의 80%를 수신하는 경우:</p><pre><code>desired_replicas = min(ceil(0.8 × 4 / (2 × 0.5)), 4)
                 = min(4, 4)
                 = 4</code></pre><p>이 핫 인덱스는 검색 노드에 분산된 네 개의 복제본을 갖습니다.</p><p>임계값 X(기본값은 0.5)가 중요합니다. 복제본이 완전히 과부하될 때까지 기다리지 않고, 용량의 절반에 도달하면 규모를 확장합니다. 손님이 떠날 때가 아니라 인파가 모이기 시작할 때 여분의 피자를 나눠줍니다.</p><h2>신속한 확장과 느긋한 축소</h2><p>검색 부하가 증가하면 즉시 복제본을 추가합니다. 사용자를 기다리게 할 이유가 없습니다.</p><p>검색 부하가 떨어지면 조치를 취하기 전에 잠시 기다립니다. 낮은 수요가 약 30분 동안 일관적으로 지속되어야 복제본을 줄입니다. (이는 급증하는 트래픽을 처리하기 위한 것이며 조용한 순간이 파티가 끝났다는 의미는 아닙니다.)</p><p>복제본을 추가하려면 비용이 들기 때문에 이는 중요한 문제입니다. 새로운 복제본은 데이터를 복사하고 캐시를 워밍한 후 쿼리를 효율적으로 처리합니다. 너무 성급하게 복제본을 제거하면 트래픽의 자연스러운 변동에 따라 지속적으로 이러한 시작 비용을 지불하게 됩니다.</p><h2>토폴로지 경계 준수</h2><p>복제본 수는 검색 노드 수를 초과할 수 없습니다. 노드보다 더 많은 복제본을 갖는 것은 아무런 이점이 없습니다(피자 조각을 제공하는 친구의 수만큼만 피자를 제공할 수 있습니다).</p><p>프로젝트에서 노드가 제거되면 복제본 수를 즉시 줄여서 노드 수와 일치시킵니다. 할당되지 않은 복제본을 보유할 수 없기 때문에 쿨다운을 기다리지 않습니다. 친구가 떠나는 순간, 그 피자도 제거됩니다.</p><h2>더 큰 서버리스 그림</h2><p>검색 로드 밸런싱을 위한 복제본은 다른 자동 확장 시스템과 함께 작동합니다.</p><ul><li><strong>검색 자동 확장</strong>은 검색 노드 수(도와주는 친구 수)를 조정합니다.</li><li><strong>검색 부하 분산을 위한 복제본</strong>은 인덱스당 복제본 수(종류별로 필요한 피자 수)를 조정하여 트래픽을 분산합니다.</li><li><strong>데이터 스트림 자동 샤딩</strong>은 쓰기에 대한 샤드 수를 최적화합니다(각 피자를 자르는 방법은 <a href="https://www.elastic.co/search-labs/blog/datastream-autosharding-serverless">이전 포스트</a>에서 다뤘습니다).</li></ul><p>중요한 설계 원칙: 로드 밸런싱용 복제본은 검색 자동 확장을 직접 유발하지 않습니다. 대신, 검색 요청을 더 많은 복제본에 분산함으로써 검색 노드 전체의 리소스 사용량을 높일 수 있습니다. 이렇게 높은 사용량은 필요한 경우 용량을 추가하도록 기존의 자동 확장 로직을 작동시킵니다. 로드 밸런싱을 위한 복제본은 자동 확장이 제 기능을 수행하도록 하여, 다른 노드가 사용되지 않는 동안 모든 트래픽이 단일 복제본에 병목 현상을 일으키는 대신 검색 노드가 실제로 사용되도록 합니다.</p><h2>이것이 귀하에게 의미하는 것</h2><p>어떤 인덱스가 인기를 끌지 예측할 필요는 없습니다. 트래픽 패턴이 변경될 때 복제본을 수동으로 조정하지 않아도 됩니다. 가장 사용량이 많은 인덱스에 과부하가 걸렸다고 해서 새벽 3시에 일어날 필요는 없습니다.</p><p>시스템은 대기열이 형성되는 위치를 감지하여 해당 지점에 더 많은 피자를 주문합니다. 콜드 인덱스는 불필요한 복제본에 자원을 낭비하지 않습니다. 핫 인덱스는 필요한 용량을 확보합니다. 예산은 중요한 곳에 사용됩니다.</p><h2>결론</h2><p><a href="https://www.elastic.co/search-labs/blog/datastream-autosharding-serverless">자동 샤딩 게시물</a>에서 피자를 적절하게 잘랐습니다. 이제 검색 부하 분산을 위한 복제본으로 배고픈 사람들이 몰려들 때 충분한 양의 피자를 적절한 사람들에게 전달하겠습니다.</p><p><a href="https://www.elastic.co/cloud/serverless">Elastic Cloud Serverless</a>를 사용하면 피자 배달을 처리해 드립니다.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Elasticsearch 추론 API와 Hugging Face 모델 함께 사용하기]]></title>
            <description><![CDATA[추론 엔드포인트를 사용하여 Elasticsearch를 Hugging Face 모델에 연결하고, 시맨틱 검색 및 채팅 완성을 갖춘 다국어 블로그 추천 시스템을 구축하는 방법을 알아보세요.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/hugging-face-elasticsearch-inference-api</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/hugging-face-elasticsearch-inference-api</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Jeffrey Rengifo]]></dc:creator>
            <pubDate>Mon, 23 Mar 2026 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/1417d6ff033712206c9bd4bcc22074ee3437ce96-1999x1125.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>최근 업데이트에서 Elasticsearch는 <a href="https://endpoints.huggingface.co/">Hugging Face Inference Service</a>에 호스팅된 모델과 연결할 수 있는 네이티브 통합 기능을 도입했습니다. 이 게시물에서는 대규모 언어 모델(LLM)을 사용하여 간단한 API 호출을 통해 이 통합을 구성하고 추론을 수행하는 방법을 살펴보겠습니다. 리소스 사용량과 답변 품질 간의 균형이 잘 잡힌 경량 범용 모델인 <a href="https://huggingface.co/HuggingFaceTB/SmolLM3-3B">SmolLM3-3B</a>를 사용하겠습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/7ddadf1976421a860a7d62087239adb9150d808b-1999x1388.png" style="width: 100%;" /><h2>필수 구성 요소</h2><ul><li><strong>Elasticsearch 9.3 또는 Elastic Cloud Serverless: </strong> <a href="https://www.elastic.co/search-labs/tutorials/install-elasticsearch/elastic-cloud">이 지침</a>을 따라 클라우드 배포를 생성하거나, <a href="https://www.elastic.co/docs/deploy-manage/deploy/self-managed/local-development-installation-quickstart#local-dev-quick-start"><code>start-local</code></a> 퀵스타트를 사용할 수 있습니다.</li><li><strong>Python 3.12: </strong>Python을 <a href="https://www.python.org/">여기</a>에서 다운로드하세요.</li><li><strong>Hugging Face </strong><a href="https://huggingface.co/docs/hub/en/security-tokens">액세스 토큰</a>.</li></ul><h2>Hugging Face 추론 엔드포인트를 사용하여 채팅 완료 수행하기</h2><p>먼저, Elasticsearch를 Hugging Face <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-inference-put">엔드포인트</a>에 연결하여 블로그 게시물 모음에서 AI 기반 추천을 생성하는 실용적인 예제를 구축할 것입니다. 앱 지식 기반 시스템을 위해, 회사 블로그 기사의 데이터 세트를 사용할 것입니다. 이 데이터 세트에는 귀중하지만 종종 탐색하기 어려운 정보가 포함되어 있습니다.</p><p>이 엔드포인트를 사용하면 <a href="https://www.elastic.co/docs/solutions/search/semantic-search">시맨틱 검색</a>을 통해 주어진 쿼리에 가장 적합한 문서를 검색할 수 있으며, Hugging Face LLM이 해당 결과를 바탕으로 문맥에 맞는 짧은 추천 결과를 생성합니다.</p><p>구축할 정보 흐름에 대한 개괄적인 내용을 살펴보겠습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/1dfbc2323438feaaa42e13ab242dd1f7166f74aa-1200x676.png" style="width: 100%;" /><p>이 기사에서는 <strong>SmolLM3-3B</strong>의 컴팩트한 크기와 강력한 다국어 추론 및 도구 호출 기능을 결합하는 능력을 테스트할 것입니다. 검색 쿼리를 기반으로 일치하는 모든 콘텐츠(영어 및 스페인어)를 LLM으로 전송하고, 검색 쿼리와 결과를 바탕으로 맞춤형 설명이 포함된 추천 기사 목록을 생성합니다.</p><p>AI 추천 생성 시스템이 포함된 기사 사이트의 UI는 다음과 같을 수 있습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/8d3b86b212f28ff279f2da67a33e6134039f0e4e-1999x949.png" style="width: 100%;" /><p>이 애플리케이션의 전체 구현은 연결된 <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/elasticsearch-inference-api-and-hugging-face/notebook.ipynb">노트북</a>에서 확인하실 수 있습니다.</p><h3>Elasticsearch 추론 엔드포인트 구성하기</h3><p>Elasticsearch <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-inference-put-hugging-face">Hugging Face 추론 엔드포인트</a>를 사용하려면 Hugging Face API 키와 실행 중인 Hugging Face 엔드포인트 URL라는 두 가지 중요한 요소가 필요합니다. 다음과 같이 보여야 합니다.</p><pre><code>PUT _inference/chat_completions/hugging-face-smollm3-3b
{
    "service": "hugging_face",
    "service_settings": {
        "api_key": "hugging-face-access-token", 
        "url": "url-endpoint" 
    }
}</code></pre><p>Elasticsearch의 Hugging Face 추론 엔드포인트는 <code>text_embedding</code>, <code>completion</code>, <code>chat_completion</code>, <code>rerank</code> 등 다양한 작업 유형을 지원합니다. 이 블로그 글에서는 검색 결과와 시스템 프롬프트를 바탕으로 대화형 추천을 생성하는 모델이 필요하기 때문에 <code>chat_completion</code>를 사용합니다. 이 엔드포인트를 통해 Elasticsearch API를 사용하여 Elasticsearch에서 직접 채팅 완료를 간단하게 수행할 수 있습니다.</p><pre><code>POST _inference/chat_completion/hugging-face-smollm3-3b/_stream
{
  "messages": [
      { "role": "user", "content": "<user prompt>" }
  ]
}</code></pre><p>이것은 애플리케이션의 핵심 역할을 하며, 프롬프트와 모델을 통과할 검색 결과를 받습니다. 이론을 다뤘으니 이제 애플리케이션 구현을 시작해 보겠습니다.</p><h4>Hugging Face에서 추론 엔드포인트 설정하기</h4><p>Hugging Face 모델을 배포하기 위해 <a href="https://huggingface.co/inference-endpoints/dedicated">Hugging Face 원클릭 배포</a>를 사용할 것입니다. 이는 모델 엔드포인트를 배포하기 위한 쉽고 빠른 서비스입니다. 이 서비스는 유료 서비스이므로 이용 시 추가 비용이 발생할 수 있습니다. 이 단계에서는 기사 추천을 생성하는 데 사용될 모델 인스턴스를 생성합니다.</p><p>원클릭 카탈로그에서 모델을 선택할 수 있습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/b816e9fba1fe172687bf58f5143fb1f838c1077f-549x331.png" style="width: 100%;" /><p><strong>SmolLM3-3B</strong> 모델을 선택합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/610d3aba0429f3666c2df3616d513eb6a4397c0c-502x478.png" style="width: 100%;" /><p>여기에서 Hugging Face 엔드포인트 URL을 가져옵니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/025094ddb2cfbd1f0f216a5ec4e119b0f4fa2c42-646x328.png" style="width: 100%;" /><p>Elasticsearch <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-inference-put-hugging-face">Hugging Face 추론 엔드포인트 설명서</a>에서 언급했듯이, 텍스트 생성에는 OpenAI API와 호환되는 모델이 필요합니다. 그러므로 Hugging Face 엔드포인트 URL에 <code>/v1/chat/completions</code> 하위 경로를 추가해야 합니다. 최종 결과는 다음과 같습니다.</p><pre><code>https://j2g31h0futopfkli.us-east-1.aws.endpoints.huggingface.cloud/v1/chat/completions</code></pre><p>이렇게 준비되면 Python 노트북에서 코딩을 시작할 수 있습니다.</p><h4>Hugging Face API 키 생성하기</h4><p><a href="https://huggingface.co/join">Hugging Face 계정</a>을 만들고 <a href="https://huggingface.co/docs/hub/en/security-tokens#user-access-tokens">다음 안내</a>에 따라 API 토큰을 받습니다. <em>세분화</em>(특정 리소스에만 액세스를 제공하므로 프로덕션에 권장), 읽기(<em>읽기</em> 전용 액세스), <em>쓰기</em>(읽기 및 쓰기 액세스용)의 세 가지 토큰 유형 중 선택할 수 있습니다. 이 튜토리얼에서는 추론 엔드포인트만 호출하면 되므로 읽기 토큰으로 충분합니다. 다음 단계를 위해 이 키를 저장해 두세요.</p><h4>Elasticsearch 추론 엔드포인트 설정</h4><p>먼저, Elasticsearch Python 클라이언트를 선언해 보겠습니다.</p><pre><code>os.environ["ELASTICSEARCH_API_KEY"] = "your-elasticsearch-api-key"
os.environ["ELASTICSEARCH_URL"] = "https://xxxx.us-central1.gcp.cloud.es.io:443"

es_client = Elasticsearch(
    os.environ["ELASTICSEARCH_URL"], api_key=os.environ["ELASTICSEARCH_API_KEY"]
)</code></pre><p>다음으로, Hugging Face 모델을 사용하는 Elasticsearch 추론 엔드포인트를 생성해 보겠습니다. 이 엔드포인트를 통해 블로그 게시물과 모델에 전달된 프롬프트를 기반으로 응답을 생성할 수 있습니다.</p><pre><code>INFERENCE_ENDPOINT_ID = "smollm3-3b-pnz"

os.environ["HUGGING_FACE_INFERENCE_ENDPOINT_URL"] = (
 "https://j2g31h0futopfkli.us-east-1.aws.endpoints.huggingface.cloud/v1/chat/completions"
)
os.environ["HUGGING_FACE_API_KEY"] = "hf_xxxxx"

resp = es_client.inference.put(
        task_type="chat_completion",
        inference_id=INFERENCE_ENDPOINT_ID,
        body={
            "service": "hugging_face",
            "service_settings": {
                "api_key": os.environ["HUGGING_FACE_API_KEY"],
                "url": os.environ["HUGGING_FACE_INFERENCE_ENDPOINT_URL"],
            },
        },
    )</code></pre><h3>데이터 세트</h3><p>데이터 세트에는 전체 워크플로우에서 사용되는 다국어 콘텐츠 세트를 나타내는 쿼리될 <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/elasticsearch-inference-api-and-hugging-face/dataset.json">블로그 게시물</a>이 포함되어 있습니다.</p><pre><code>// Articles dataset document example: 
{
    "id": "6",
    "title": "Complete guide to the new API: Endpoints and examples",
    "author": "Tomas Hernandez",
    "date": "2025-11-06",
    "category": "tutorial",
    "content": "This guide describes in detail all endpoints of the new API v2. It includes code examples in Python, JavaScript, and cURL for each endpoint. We cover authentication, resource creation, queries, updates, and deletion. We also explain error handling, rate limiting, and best practices. Complete documentation is available on our developer portal."
  }</code></pre><h4>Elasticsearch 매핑</h4><p>데이터 세트가 정의되었으므로, 이제 블로그 게시물 구조에 적합한 데이터 스키마를 생성해야 합니다. 다음 <a href="https://www.elastic.co/docs/manage-data/data-store/mapping">인덱스 매핑</a>은 Elasticsearch에 데이터를 저장하는 데 사용됩니다.</p><pre><code>INDEX_NAME = "blog-posts"

mapping = {
    "mappings": {
        "properties": {
            "id": {"type": "keyword"},
            "title": {
                "type": "object",
                "properties": {
                    "original": {
                        "type": "text",
                        "copy_to": "semantic_field",
                        "fields": {"keyword": {"type": "keyword"}},
                    },
                    "translated_title": {
                        "type": "text",
                        "fields": {"keyword": {"type": "keyword"}},
                    },
                },
            },
            "author": {"type": "keyword", "copy_to": "semantic_field"},
            "category": {"type": "keyword", "copy_to": "semantic_field"},
            "content": {"type": "text", "copy_to": "semantic_field"},
            "date": {"type": "date"},
            "semantic_field": {"type": "semantic_text"},
        }
    }
}


es_client.indices.create(index=INDEX_NAME, body=mapping)</code></pre><p>여기에서 데이터가 어떻게 구성되어 있는지 더욱 명확하게 확인할 수 있습니다. 자연어를 기반으로 결과를 검색하는 데 시맨틱 검색을 사용하고, <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/copy-to"><code>copy_to</code></a> 속성을 사용하여 필드 내용을 <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/semantic-text"><code>semantic_text</code></a> 필드로 복사합니다. 또한 <code>title</code> 필드에는 두 개의 하위 필드가 있습니다. <code>original</code> 하위 필드는 기사의 원래 언어에 따라 영어 또는 스페인어로 제목을 저장하며, <code>translated_title</code> 하위 필드는 스페인어 기사에만 존재하고 원래 제목의 영어 번역을 포함합니다.</p><h3>데이터 수집</h3><p>다음 코드 스니펫은 <a href="https://www.elastic.co/docs/reference/elasticsearch/clients/javascript/bulk_examples">벌크 API</a>를 사용하여 블로그 게시물 데이터 세트를 Elasticsearch로 수집합니다.</p><pre><code>def build_data(json_file, index_name):
    with open(json_file, "r") as f:
        data = json.load(f)

    for doc in data:
        action = {"_index": index_name, "_source": doc}
        yield action


try:
    success, failed = helpers.bulk(
        es_client,
        build_data("dataset.json", INDEX_NAME),
    )
    print(f"{success} documents indexed successfully")

    if failed:
        print(f"Errors: {failed}")
except Exception as e:
    print(f"Error: {str(e)}")</code></pre><p>이제 기사들이 Elasticsearch에 수집되었으니, <code>semantic_text</code> 필드에 대해 검색할 수 있는 함수를 만들어야 합니다.</p><pre><code>def perform_semantic_search(query_text, index_name=INDEX_NAME, size=5):
    try:
        query = {
            "query": {
                "match": {
                    "semantic_field": {
                        "query": query_text,
                    }
                }
            },
            "size": size,
        }

        response = es_client.search(index=index_name, body=query)
        hits = response["hits"]["hits"]

        return hits
    except Exception as e:
        print(f"Semantic search error: {str(e)}")
        return []</code></pre><p>추론 엔드포인트를 호출하는 함수도 필요합니다. 이 경우 <strong><code>chat_completion</code> </strong>작업 유형을 사용하여 엔드포인트를 호출하여 스트리밍 응답을 받습니다.</p><pre><code>def stream_chat_completion(messages: list, inference_id: str = INFERENCE_ENDPOINT_ID):
    url = f"{ELASTICSEARCH_URL}/_inference/chat_completion/{inference_id}/_stream"
    payload = {"messages": messages}
    headers = {
        "Authorization": f"ApiKey {ELASTICSEARCH_API_KEY}",
        "Content-Type": "application/json",
    }

    try:
        response = requests.post(url, json=payload, headers=headers, stream=True)
        response.raise_for_status()

        for line in response.iter_lines(decode_unicode=True):
            if line:
                line = line.strip()

                if line.startswith("event:"):
                    continue

                if line.startswith("data: "):
                    data_content = line[6:]

                    if not data_content.strip() or data_content.strip() == "[DONE]":
                        continue

                    try:
                        chunk_data = json.loads(data_content)

                        if "choices" in chunk_data and len(chunk_data["choices"]) > 0:
                            choice = chunk_data["choices"][0]
                            if "delta" in choice and "content" in choice["delta"]:
                                content = choice["delta"]["content"]
                                if content:
                                    yield content

                    except json.JSONDecodeError as json_err:
                        print(f"\nJSON decode error: {json_err}")
                        print(f"Problematic data: {data_content}")
                        continue

    except requests.exceptions.RequestException as e:
        yield f"Error: {str(e)}"</code></pre><p>이제 의미 탐색 함수와 <code>chat_completions</code> 추론 엔드포인트, 추천 엔드포인트를 호출하여 카드에 할당될 데이터를 생성할 수 있습니다.</p><pre><code>def recommend_articles(search_query, index_name=INDEX_NAME, max_articles=5):
    print(f"\n{'='*80}")
    print(f"🔍 Search Query: {search_query}")
    print(f"{'='*80}\n")

    articles = perform_semantic_search(search_query, index_name, size=max_articles)

    if not articles:
        print("❌ No relevant articles found.")
        return None, None

    print(f"✅ Found {len(articles)} relevant articles\n")

    # Build context with found articles
    context = "Available blog articles:\n\n"
    for i, article in enumerate(articles, 1):
        source = article.get("_source", article)
        context += f"Article {i}:\n"
        context += f"- Title: {source.get('title', 'N/A')}\n"
        context += f"- Author: {source.get('author', 'N/A')}\n"
        context += f"- Category: {source.get('category', 'N/A')}\n"
        context += f"- Date: {source.get('date', 'N/A')}\n"
        context += f"- Content: {source.get('content', 'N/A')}\n\n"

    system_prompt = """You are an expert content curator that recommends blog articles.

    Write recommendations in a conversational style starting with phrases like:
    - "If you're interested in [topic], this article..."
    - "This post complements your search with..."
    - "For those looking into [topic], this article provides..."


    FORMAT REQUIREMENTS:
    - Return ONLY a JSON array
    - Each element must have EXACTLY these three fields: "article_number", "title", "recommendation"
    - If the original title is in spanish, use the "translated_title" subfield in the "title" field

    Keep each recommendation concise (2-3 sentences max) and focused on VALUE to the reader.

    EXAMPLE OF CORRECT FORMAT:
    [
        {"article_number": 1, "title": "Article title in english", "recommendation": "If you are interested in [topic], this article provides..."},
        {"article_number": 2, "title": "Article title in english", "recommendation": " for those looking into [topic], this article provides..."}
    ]

    Return ONLY the JSON array following this exact structure."""

    user_prompt = f"""Search query: "{search_query}"

    Generate recommendations for the following articles: {context}
    """

    messages = [
        {"role": "system", "content": "/no_think"},
        {"role": "system", "content": system_prompt},
        {"role": "user", "content": user_prompt},
    ]

    # LLM generation
    print(f"{'='*80}")
    print("🤖 Generating personalized recommendations...\n")

    full_response = ""

    for chunk in stream_chat_completion(messages):
        print(chunk, end="", flush=True)
        full_response += chunk

    return context, articles, full_response</code></pre><p>마지막으로, 정보를 추출하여 인쇄할 수 있도록 서식을 지정해야 합니다.</p><pre><code>def display_recommendation_cards(articles, recommendations_text):
    print("\n" + "=" * 100)
    print("📇 RECOMMENDED ARTICLES".center(100))
    print("=" * 100 + "\n")

    # Parse JSON recommendations - clean tags and extract JSON
    recommendations_list = []
    try:

        # Clean up <think> tags
        cleaned_text = re.sub(
            r"<think>.*?</think>", "", recommendations_text, flags=re.DOTALL
        )
        # Remove markdown code blocks ( ... ``` or ``` ... ```)
        cleaned_text = re.sub(r"```(?:json)?", "", cleaned_text)
        cleaned_text = cleaned_text.strip()

        parsed = json.loads(cleaned_text)

        # Extract recommendations from list format
        for item in parsed:
            article_number = item.get("article_number")
            title = item.get("title", "")
            rec_text = item.get("recommendation", "")

            if article_number and rec_text:
                recommendations_list.append(
                    {
                        "article_number": article_number,
                        "title": title,
                        "recommendation": rec_text,
                    }
                )
    except json.JSONDecodeError as e:
        print(f"⚠️  Could not parse recommendations as JSON: {e}")
        return

    for i, article in enumerate(articles, 1):
        source = article.get("_source", article)

        # Card border
        print("┌" + "─" * 98 + "┐")

        # Find recommendation and title for this article number
        recommendation = None
        title = None
        for rec in recommendations_list:
            if rec.get("article_number") == i:
                recommendation = rec.get("recommendation")
                title = rec.get("title")
                break

        # Print title
        title_lines = textwrap.wrap(f"📌 {title}", width=94)
        for line in title_lines:
            print(f"│  {line}".ljust(99) + "│")

        # Card border
        print("├" + "─" * 98 + "┤")

        # Print recommendation
        if recommendation:
            recommendation_lines = textwrap.wrap(recommendation, width=94)
            for line in recommendation_lines:
                print(f"│  {line}".ljust(99) + "│")

        # Card bottom
        print("└" + "─" * 98 + "┘")</code></pre><p>보안 블로그 게시물에 대해 질문하여 이를 테스트해 보겠습니다.</p><pre><code>search_query = "Security and vulnerabilities"

context, articles, recommendations = recommend_articles(search_query)

print("\nElasticsearch context:\n", context)

# Display visual cards
display_recommendation_cards(articles, recommendations)</code></pre><p>여기서 워크플로우가 생성한 콘솔의 카드를 볼 수 있습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/730d35212594bb3db30447c3ea7e2a92857287b7-1999x1515.png" style="width: 100%;" /><p><a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/elasticsearch-inference-api-and-hugging-face/results.md">이 파일에서</a> 모든 히트와 LLM 응답을 포함한 전체 결과를 확인하실 수 있습니다.</p><p>&quot;보안 및 취약점&quot;과 관련된 기사를 찾고 있습니다. 이 질문은 Elasticsearch에 저장된 문서에 대한 검색 쿼리로 사용됩니다. 검색된 결과는 모델로 전달되어 해당 콘텐츠를 기반으로 추천을 생성합니다. 보시다시피, 이 모델은 독자가 클릭하도록 동기를 부여할 수 있는 매력적인 짧은 텍스트를 훌륭하게 생성했습니다.</p><h2>결론</h2><p>이 예시는 Elasticsearch와 Hugging Face를 결합하여 AI 애플리케이션을 위한 빠르고 효율적인 중앙 집중식 시스템을 만드는 방법을 보여줍니다. 이 접근 방식은 수동 작업을 줄이고 Hugging Face의 광범위한 모델 카탈로그 덕분에 유연성을 제공합니다. SmolLM3-3B를 사용하면 특히 소형 다국어 모델이 시맨틱 검색과 결합될 때 여전히 의미 있는 추론과 콘텐츠 생성을 제공할 수 있음을 보여줍니다. 이러한 도구들을 함께 사용하면 지능형 콘텐츠 분석 및 다국어 애플리케이션 구축을 위한 확장성 있는 효과적인 기반을 마련할 수 있습니다.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[빠른 속도 대 정확도: 양자화된 벡터 검색의 리콜 측정하기]]></title>
            <description><![CDATA[최소한의 설정으로 Elasticsearch에서 벡터 검색의 리콜을 측정하는 방법을 설명합니다.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/recall-vector-search-quantization</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/recall-vector-search-quantization</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Jeff Vestal]]></dc:creator>
            <pubDate>Fri, 20 Mar 2026 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/09f03b9239d66c36763cdab3fafcdac207ff6d83-1280x720.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>모두가 벡터 검색이 즉각적으로 이루어지기를 원합니다. 하지만 고차원 벡터는 무겁습니다. 단일 1,024차원 float-32 벡터는 상당한 메모리를 차지하며, 이를 수백만 개의 다른 벡터와 비교하는 것은 계산상 비용이 많이 듭니다.</p><p>이를 해결하기 위해 Elasticsearch와 같은 검색 엔진은 두 가지 주요 최적화 전략을 사용합니다.</p><ol><li><strong>근사 검색(계층적 탐색 가능 스몰 월드 [HNSW]):</strong> 모든 문서를 스캔하는 대신, 답이 있을 가능성이 높은 근처로 빠르게 이동할 수 있는 탐색 그래프를 구축합니다.</li><li><strong>양자화:</strong> 메모리 사용량을 줄이고 계산 속도를 높이기 위해 벡터를 압축합니다(예: 32비트 부동소수점에서 8비트 정수 또는 1비트 이진 값으로).</li></ol><p>하지만 최적화에는 <strong>정확성</strong>이라는 대가가 따르는 경우가 많습니다.</p><p>&quot;만약 데이터를 압축하고 검색 중에 지름길을 택한다면 최상의 결과를 놓치게 될까요?&quot; 혹은 &quot;이러한 최적화가 검색 엔진의 관련성을 저하시킬까요?&quot;라는 두려움이 당연히 생길 수 있습니다.</p><p>Elastic의 정량화가 결과를 저하시키지 않는다는 것을 증명하기 위해, <a href="https://huggingface.co/datasets/fancyzhx/dbpedia_14"><strong>DBPedia-14</strong> 데이터 세트</a>를 사용해 반복 가능한 테스트 하네스를 구축하여 Elasticsearch에서 기본 최적화를 사용할 때 속도와 얼마나 많은 정확도(특히, <strong>리콜</strong>)를 교환하는지 정확히 계산했습니다.</p><p>tldr: 생각보다 훨씬 적을 가능성이 높습니다. <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/fast_vs_accurate_measuring_the_recall_of_quantized_vector_search/vector_recall_notebook.ipynb">여기에서 노트북</a>을 확인하고 직접 사용해 보세요.</p><h2><strong>정의(비전문가를 위한)</strong></h2><p>코드를 살펴보기 전에 먼저 몇 가지 용어를 정리해 보겠습니다.</p><ul><li><strong>정확도 대 리콜:</strong> <strong>정확도</strong>는 주관적입니다(좋은 것을 찾았는가?). <strong>리콜</strong>은 수학적입니다. 데이터베이스에 검색어와 수학적으로 <em>완벽하게</em> 일치하는 문서가 10개 있고 검색 엔진이 그 중 9개를 찾았다면 리콜 확률은 90%(또는 0.9)입니다.</li><li><strong>정확한 검색(평면 검색):</strong> 때때로 &quot;무차별 대입&quot; 방법이라고도 합니다. 검색 엔진은 색인에 있는 모든 문서를 스캔하여 거리를 계산합니다.<ul><li><em>장점:</em> 리콜이 100% 완벽합니다.</li><li><em>단점:</em> 계산 비용이 높고 규모가 커질수록 느려집니다.</li></ul></li><li><strong>근사 검색(HNSW):</strong> &quot;지름길&quot; 방식입니다. 검색 엔진이 <a href="https://www.elastic.co/search-labs/blog/hnsw-graph">HNSW</a> 그래프를 구축합니다. 그래프를 탐색하여 최근접 이웃을 찾습니다.<ul><li><em>장점:</em> 매우 빠르고 확장 가능합니다.</li><li><em>단점:</em> 그래프 탐색이 너무 일찍 중단되면 이웃을 놓칠 가능성이 있습니다.</li></ul></li></ul><h2><strong>실험: 정확한 값과 근사치 비교</strong></h2><p>리콜을 테스트하기 위해 텍스트 분류 모델을 학습하고 평가하는 데 일반적으로 사용되는 14개 온톨로지 클래스에 걸친 제목과 초록으로 구성된 대규모 데이터 세트인 <strong>DBPedia-14</strong> 데이터 세트를 사용했습니다. 특히 &quot;영화&quot; 카테고리에 초점을 맞출 것입니다. 최적화된 프로덕션 설정을 수학적으로 완벽한 기준 데이터와 비교하고 싶었습니다.</p><p>이 실험에서는 <a href="https://www.elastic.co/search-labs/blog/jina-embeddings-v5-text">jina-embeddings-v5-text-small</a> 모델을 사용하고 있습니다. 이 모델은 텍스트 표현에 대한 업계 벤치마크를 선도하는 최첨단 다국어 모델입니다. 이 모델이 고성능 임베딩의 현재 표준을 정의하기 때문에 이 모델을 선택했습니다. Jina v5의 탁월한 정확도와 Elasticsearch의 네이티브 양자화 기능을 결합함으로써, 계산 효율성이 뛰어나면서도 검색 품질을 저하시키지 않는 검색 아키텍처를 구현할 수 있음을 보여줍니다.</p><p>이중 매핑으로 색인을 설정했습니다. 동일한 텍스트를 두 개의 서로 다른 필드에 동시에 수집했습니다.</p><ol><li><strong><code>content.raw</code> </strong>유형: <code>flat</code>. 이로 인해 Elasticsearch는 전체 Float32 벡터에 대해 무차별 대입 스캔을 수행하게 됩니다. 이렇게 하면 정확한 일치 결과가 반환되며 기준선으로 사용됩니다.</li><li><strong><code>content</code> </strong>유형 <code>semantic_text</code>. 기본값으로 HNSW + 더 나은 이진 양자화(BBQ)를 사용합니다. 이것이 근사 일치를 위한 표준적이고 최적화된 생산 설정입니다.</li></ol><h3><strong>Recall@10 테스트</strong></h3><p>메트릭으로는 Recall@10을 사용했습니다.</p><p>무작위로 영화 50편을 골라 두 필드에 대해 동일한 쿼리를 실행했습니다.</p><ul><li><strong>정확한 (평면)</strong> 검색에서 상위 10개 이웃의 ID가 [1, 2, 3... 10]인 경우.</li><li>그리고 <strong>대략적인 (HNSW)</strong> 검색은 ID [1, 2, 3... 9, 99]를 반환합니다.</li><li>상위 10개 중 9개를 정확히 찾아냈습니다. 점수는 <strong>0.9점</strong>입니다.</li></ul><p>다음은 사용한 매핑입니다.</p><pre><code># The "Control Group": Forces exact brute-force scan
"raw": {
    "type": "semantic_text",
    "inference_id": ".jina-embeddings-v5-text-small",
    "index_options": {
        "dense_vector": {
            "type": "flat"
        }
    }
}</code></pre><p><strong>결과: 성공의 &quot;플랫 라인&quot;</strong></p><p>전체 데이터 세트를 다시 로드하고 1,000~40,000개의 문서 색인 크기에 대해 테스트하는 확장성 테스트를 실행했습니다.</p><p>리콜 점수에 대한 자세한 내용은 다음과 같습니다.</p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">문서</td><td style="border: 1px solid #ccc; padding: 4px;">Recall@10 점수</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">1,000</td><td style="border: 1px solid #ccc; padding: 4px;">1.000(100%)</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">5,000</td><td style="border: 1px solid #ccc; padding: 4px;">0.998 (100%)</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">10,000</td><td style="border: 1px solid #ccc; padding: 4px;">0.992 (99.4%)</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">20,000</td><td style="border: 1px solid #ccc; padding: 4px;">0.999 (99.0%)</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">40,000</td><td style="border: 1px solid #ccc; padding: 4px;">0.992 (98.8%)</td>
                    </tr>
              </tbody>
            </table>
          <p>결과는 놀라울 정도로 안정적이었습니다. 규모를 확장했음에도 불구하고, 근사 검색은 무차별 대입 방식의 정확한 검색과 <strong>99%를 넘는 일치율</strong>을 보였습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/a4bfacb1d0cce6fdf6df0e1a9d4fc5d4007a66da-1999x1209.png" style="width: 100%;" /><h2><strong>왜 그렇게 잘 작동했을까요?</strong></h2><p>벡터를 이진 값으로 압축하면 정확도가 더 많이 저하될 것으로 예상할 수 있습니다. 그렇게 되지 않는 이유는 Elasticsearch가 데이터 검색을 처리하는 방식에 있습니다.</p><p>오늘날 대부분의 임베딩 모델은 크기가 큰 Float32 벡터를 출력합니다. 검색 효율성을 높이기 위해 Elasticsearch는 고차원 벡터에 양자화를 사용합니다. 특히, 9.2 이후로는 기본적으로 <a href="https://www.elastic.co/search-labs/blog/elasticsearch-9-1-bbq-acorn-vector-search">BBQ</a>를 사용합니다.</p><p>BBQ는 <strong>리스코어</strong> 메커니즘을 사용합니다.</p><ol><li><strong>트래버설:</strong> 검색 엔진은 압축(양자화된) 벡터를 사용하여 HNSW 그래프를 빠르게 탐색합니다. 벡터가 작기 때문에 성능 저하 없이 효율적으로 오버샘플링하여 더 많은 후보 목록(예: 거의 비슷한 상위 100개 문서)을 수집할 수 있습니다.</li><li><strong>리스코어:</strong> 후보 문서가 있으면 해당 문서에 대한 전체 정밀도 값을 검색하여 최종적이고 정확한 순위를 계산합니다.</li></ol><p>이는 무거운 작업을 위한 양자화의 속도와 최종 정렬을 위한 부동 소수점의 정밀도, 이 두 가지 장점을 모두 제공합니다.</p><h2><strong>더 나은 성과를 낼 수 있을까요?</strong></h2><p>여기에 표시된 결과는 기본 설정과 무작위 데이터 샘플링을 사용한 결과라는 점에 주목할 필요가 있습니다. 이걸 고성능 출발점이라고 생각해 보세요. Jina v5는 강력한 성능을 자랑하지만, 이러한 리콜 점수가 모든 데이터 세트에 대해 &quot;만능 해결책&quot;을 보장하는 것은 아닙니다. 모든 데이터 수집에는 고유한 특성이 있으며, 더 많은 성능을 끌어내기 위해 더 조정할 수는 있지만, 항상 자신의 특정 데이터와 벤치마킹하여 한계가 어디인지 확인해야 합니다.</p><h2><strong>결론</strong></h2><p>이것은 매우 작은 규모의 테스트입니다. 하지만 이 연습의 요점은 임베딩 모델이나 BBQ를 구체적으로 측정하는 것이 아니라, 최소한의 설정으로 데이터 세트의 리콜을 쉽게 측정하는 방법을 보여주기 위한 것입니다.</p><p>자신의 데이터로 이 테스트를 실행하려면 <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/fast_vs_accurate_measuring_the_recall_of_quantized_vector_search/vector_recall_notebook.ipynb">여기에서 노트북을 확인</a>하여 직접 시도해 보세요.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Elasticsearch용 Gemini CLI 확장 프로그램(도구 및 기술 포함)]]></title>
            <description><![CDATA[개발자 및 에이전트 워크플로우에서 Elasticsearch 데이터를 검색, 조회 및 분석할 수 있는 Google Gemini CLI용 Elastic 확장 프로그램을 소개합니다.
]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/gemini-cli-extension-elasticsearch</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/gemini-cli-extension-elasticsearch</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Walter Rafelsberger, Enrico Zimuel]]></dc:creator>
            <pubDate>Tue, 17 Mar 2026 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/6dfcec4a77b3dc83bf0d974417bf2e211abb1f4f-876x468.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p><a href="https://www.elastic.co/elasticsearch">Elasticsearch</a>와 <a href="https://www.elastic.co/elasticsearch/agent-builder">Elastic Agent Builder</a>의 모든 기능을 AI 개발 워크플로우에 직접 통합할 수 있는 Google Gemini CLI용 Elastic 확장 기능을 출시합니다. 이 확장 프로그램은 Elasticsearch와 상호 작용하기 위한 최근 개발된 몇 가지 에이전트 스킬도 제공합니다.</p><p>확장 프로그램은 <a href="https://github.com/elastic/gemini-cli-elasticsearch">여기</a>에서 오픈 소스 프로젝트로 제공됩니다.</p><h2>Gemini CLI 소개 및 설치 방법</h2><p><a href="https://geminicli.com/">Gemini CLI</a>는 Google의 Gemini 모델을 명령줄로 직접 가져오는 오픈 소스 AI 에이전트입니다. 개발자가 터미널을 통해 AI와 상호 작용하여 코드 생성, 파일 편집, 셸 명령 실행, 웹 정보 검색 등의 작업을 수행할 수 있습니다.</p><p>Gemini CLI는 일반적인 채팅 인터페이스와 달리 로컬 개발 환경과 통합되어 프로젝트 컨텍스트를 이해하고, 파일을 수정하고, 빌드나 테스트를 실행하고, 워크플로우를 터미널 내에서 직접 자동화할 수 있습니다. 이러한 특징 덕분에 개발자, 사이트 신뢰성 엔지니어(SRE)뿐만 아니라 명령줄 워크플로우를 벗어나지 않고 AI 기반 코딩 및 자동화를 원하는 엔지니어에게 유용합니다.</p><p>Gemini CLI는 여러 패키지 관리자를 사용하여 설치할 수 있습니다. 가장 일반적인 방법은 npm을 사용하는 것입니다:</p><pre><code>npm install -g @google/gemini-cli</code></pre><p>다른 설치 옵션을 알고 싶다면 <a href="https://geminicli.com/docs/get-started/installation/">공식 설치 페이지</a>를 참조하세요.</p><p>설치 후, 다음 명령어로 CLI를 실행하세요.</p><pre><code>gemini</code></pre><p>그림 1과 같은 화면이 표시됩니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/6dfcec4a77b3dc83bf0d974417bf2e211abb1f4f-876x468.png" style="width: 100%;" /><h2>Elasticsearch를 구성합니다</h2><p>Elasticsearch 인스턴스가 실행 중이어야 합니다. 모델 컨텍스트 프로토콜(MCP) 서버를 사용하려면 Kibana 9.3+ 버전이 설치되어 있어야 합니다. 아래 설명된 Elasticsearch 쿼리 언어(ES|QL) 스킬(<code>esql</code>)을 사용하려면 Kibana가 없어도 됩니다.</p><p><a href="https://www.elastic.co/cloud">Elastic Cloud</a>에서 무료 체험을 활성화하거나 <a href="https://github.com/elastic/start-local"><code>start-local</code></a> 스크립트를 사용해 로컬에 설치할 수 있습니다.</p><pre><code>curl -fsSL https://elastic.co/start-local | sh</code></pre><p>이 명령을 실행하면 컴퓨터에 Elasticsearch와 Kibana가 설치되고 Gemini CLI 설정에 사용할 API 키가 생성됩니다.</p><p>API 키는 이전 명령의 출력 결과로 표시되며 <strong>.env</strong> 파일로 <strong><code>elastic-start-local</code></strong> 폴더에 저장됩니다.</p><p>온프레미스 Elasticsearch를 사용 중인 경우(예: <code>start-local</code> 사용), Elastic Agent Builder를 MCP와 함께 사용하려면 대규모 언어 모델(LLM)을 연결해야 합니다. <a href="https://www.elastic.co/docs/explore-analyze/ai-features/llm-guides/llm-connectors">이 문서 페이지</a>를 읽으면 다양한 옵션을 파악할 수 있습니다.</p><p>Elastic Cloud(또는 서버리스)를 사용하는 경우 이미 사전 구축된 LLM 연결이 제공됩니다.</p><h2>Elasticsearch 확장 프로그램 설치</h2><p>Elasticsearch 확장 프로그램을 Gemini CLI에 설치하려면 다음 명령어를 사용하세요.</p><pre><code>gemini extensions install https://github.com/elastic/gemini-cli-elasticsearch</code></pre><p>Gemini를 열고 다음 명령어를 실행하면 확장 프로그램이 성공적으로 설치되었는지 확인할 수 있습니다.</p><pre><code>/extensions list</code></pre><p>Elasticsearch 확장 프로그램을 사용할 수 있는지 확인할 수 있습니다.</p><p>MCP 통합을 사용하려면 Elasticsearch 9.3 이상 버전이 설치되어 있어야 합니다. <a href="https://www.elastic.co/kibana">Kibana</a>에서 가져온 MCP 서버 URL이 필요합니다.</p><ul><li>에이전트에서 MCP 서버 URL 가져오기 &gt; 모든 도구 보기 &gt; MCP 관리 &gt; MCP 서버 URL 복사</li><li>표시되는 URL: https://your-kibana-instance/api/agent_builder/mcp</li></ul><p>Elasticsearch 엔드포인트 URL이 필요합니다. 일반적으로 이것은 Kibana Elasticsearch 페이지 상단에 보고됩니다. <code>start-local</code>로 Elasticsearch를 실행 중인 경우, <code>start-local</code> .env 파일의 <code>ES_LOCAL_URL</code> 키에 이미 엔드포인트가 있습니다.</p><p>API 키도 필요합니다. Elasticsearch를 <code>start-local</code>로 실행 중인 경우, 이미 <code>ES_LOCAL_API_KEY</code>가 <code>start-local</code>.env 파일에 있습니다. 그렇지 않은 경우, <a href="https://www.elastic.co/docs/deploy-manage/api-keys/elasticsearch-api-keys">여기</a>에 설명된 대로 Kibana 인터페이스를 사용하여 API 키를 생성할 수 있습니다.</p><ul><li>Kibana: Stack Management &gt; Security &gt; API 키 &gt; API 키 생성</li><li>API 키에 대해 읽기 권한만 설정하여 <a href="https://www.elastic.co/docs/explore-analyze/ai-features/agent-builder/permissions#grant-access-with-roles">여기</a>에 보고된 대로 <code>feature_agentBuilder.read</code> 권한을 활성화할 것을 권장합니다.</li><li>인코딩된 API 키 값을 복사합니다.</li></ul><p>셸에서 필요한 환경 변수를 설정하세요.</p><pre><code>export ELASTIC_URL="your-elasticsearch-url"
export ELASTIC_MCP_URL="your-elasticsearch-mcp-url"
export ELASTIC_API_KEY="your-encoded-api-key"</code></pre><h2>예제 데이터 세트 설치하기</h2><p>Kibana에서 제공되는 <strong>eCommerce orders</strong> 데이터 세트를 설치할 수 있습니다. 이 데이터 세트에는 <strong><code>kibana_sample_data_ecommerce</code></strong>라는 단일 인덱스가 포함되어 있으며 전자상거래 웹사이트의 4,675개 주문 정보를 담고 있습니다. 각 주문에는 다음과 같은 정보가 포함되어 있습니다.</p><ul><li>고객 정보(이름, ID, 생년월일, 이메일 등)</li><li>주문 날짜</li><li>주문 ID</li><li>제품(가격, 수량, ID, 카테고리, 할인, 기타 정보를 포함한 전체 제품 목록)</li><li>SKU.</li><li>총액(세전, 세후)</li><li>총 수량</li><li>지리 정보(도시, 국가, 대륙, 위치, 지역).</li></ul><p>샘플 데이터를 설치하려면 Kibana의 <strong>통합</strong> 페이지를 열고(상단 검색창에서 &#x27;통합&#x27;을 검색) <strong>&quot;Sample Data&quot;</strong>를 설치하세요. 자세한 내용은 <a href="https://www.elastic.co/docs/explore-analyze/#gs-get-data-into-kibana">여기</a> 설명서를 확인하세요.</p><p>이 글의 목표는 Gemini CLI를 설정해 Elasticsearch에 연결하고 <strong><code>kibana_sample_data_ecommerce</code></strong> 인덱스와 손쉽게 상호작용하는 방법을 보여주는 것입니다.</p><h2>Elasticsearch MCP를 사용하는 방법</h2><p>Gemini에서 다음 명령을 사용하여 연결을 확인할 수 있습니다:</p><pre><code>/mcp list</code></pre><p>그림 2와 같이 <strong><code>elastic-agent-builder</code></strong>가 활성화된 것을 확인할 수 있습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/1508423bc1d1bc3c04a1cb01e2d59495a3516ed1-1465x844.png" style="width: 100%;" /><p>Elasticsearch는 기본 도구 세트를 제공합니다. <a href="https://www.elastic.co/docs/explore-analyze/ai-features/agent-builder/tools/builtin-tools-reference">여기</a>에서 설명을 참조하세요.</p><p>이 도구로 Elasticsearch와 상호 작용하며 다음과 같은 질문을 할 수 있습니다.</p><ul><li><code>Give me the list of all the indexes available in Elasticsearch.</code></li><li><code>How many customers are based in the USA in the kibana_sample_data_ecommerce index of Elasticsearch?</code></li></ul><p>질문에 따라 Gemini는 하나 이상의 도구를 사용하여 답변을 시도합니다.</p><h2>/elastic 명령어</h2><p>Gemini CLI의 Elasticsearch 확장 프로그램에서<strong> <code>/elastic</code></strong> 명령어를 추가했습니다.</p><p><strong><code>/help</code></strong> 명령을 실행하면 사용 가능한 모든 <code>/elastic</code> 옵션이 표시됩니다(그림 3).</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/5b2a0727ce7a04354878dd048253d3f4d062324b-1983x230.png" style="width: 100%;" /><p>이 명령어들은 <code>elastic-agent-builder</code> MCP 서버의 특정 도구를 직접 실행하고자 할 때 유용할 수 있습니다. 예를 들어 다음 명령어를 사용하면 <code>kibana_sample_data_ecommerce</code>의 매핑을 얻을 수 있습니다.</p><pre><code>/elastic:get-mapping kibana_sample_data_ecommerce</code></pre><p>이러한 명령은 Gemini 모델에 의존하여 실행할 도구를 결정하는 대신, 특정 도구를 실행하는 바로가기 역할을 합니다.</p><h2>Elasticsearch 스킬 사용 방법</h2><p>이 확장 기능에는 <a href="https://github.com/elastic/gemini-cli-elasticsearch/tree/main/skills/esql">ES|QL용 에이전트 기술</a>과 Elasticsearch에서 사용할 수 있는 <a href="https://www.elastic.co/docs/explore-analyze/discover/try-esql">Elasticsearch 쿼리 언어</a>가 함께 제공됩니다. <a href="https://agentskills.io/home">Agent Skills</a>는 Gemini CLI와 같은 AI 코딩 에이전트에 특정 작업에 대한 사용자 지정 지침을 제공하는 개방형 형식입니다. <em>점진적 공개</em>라는 개념을 사용하여 초기 시스템 프롬프트에 스킬에 대한 간략한 설명만 추가합니다. 에이전트에게 Elasticsearch 쿼리와 같은 작업을 수행하도록 요청하면 에이전트는 해당 요청을 관련 스킬과 연결하고 자세한 지침을 동적으로 불러옵니다. 이는 토큰 예산을 관리하면서 AI에 필요한 컨텍스트를 정확하게 제공하는 효율적인 방법입니다.</p><p><strong><code>esql</code> 스킬</strong>은 Gemini CLI가 클러스터에 직접 ES|QL 쿼리를 작성하고 실행할 수 있도록 설계되었습니다. ES|QL은 데이터 탐색, 로그 분석 및 집계를 매우 직관적으로 수행할 수 있는 강력한 파이프 쿼리 언어입니다. 이 스킬을 활성화하면 ES|QL 구문을 찾아볼 필요가 없습니다. Gemini CLI에 데이터에 대한 자연어 질문을 입력하기만 하면, 에이전트가 나머지 작업을 처리합니다.</p><p>실행은 터미널에서 실행되는 간단한 <a href="https://curl.se/">curl</a> 명령어를 사용하여 수행됩니다. 이것이 가능한 것은 Elasticsearch가 어떤 아키텍처에도 쉽게 통합할 수 있는 풍부한 REST API 세트를 제공하기 때문입니다.</p><p><strong><code>esql</code> 스킬 제공 내용:</strong></p><ul><li><strong>인덱스 및 스키마 검색:</strong> 에이전트가 스킬의 기본 제공 도구를 사용하여 사용 가능한 인덱스를 나열하고 필드 매핑을 가져올 수 있습니다. 예를 들어 전자상거래 데이터세트에 대한 쿼리를 작성하기 전에 에이전트가 <strong><code>kibana_sample_data_ecommerce</code></strong> 스키마 검사를 실행해서 사용 가능한 필드(예: <strong><code>taxful_total_price</code></strong> 또는 <strong><code>category</code></strong>)를 파악할 수 있습니다.</li><li><strong>원활한 자연어 번역:</strong> 이 스킬은 에이전트에 단순한 참조 매뉴얼을 넘어 사용자 의도를 해석할 수 있는 구체적인 가이드를 제공합니다. &#x27;서비스별 평균 응답 시간 표시&#x27;와 같은 자연어 요청을 입력하면 에이전트가 스킬에 내장된 패턴 매칭 기능을 사용하여 사용자가 입력한 내용을 올바른 ES|QL 집계, 필터 및 명령으로 즉시 변환합니다.</li><li><strong>자체 수정:</strong> 만일 타입 불일치 또는 구문 오류로 인해 쿼리가 실패하면, 스킬은 생성된 쿼리와 정확한 Elasticsearch 오류 메시지를 반환하여 에이전트가 즉시 쿼리를 수정하고 사용자가 개입하지 않아도 다시 시도할 수 있게 됩니다.</li></ul><p><code>esql</code> 스킬은 <code>elastic-agent-builder</code> MCP 서버에서도 도구로 제공되므로 이 서버를 잠시 비활성화해야 합니다. 다음 명령어를 사용하여 비활성화할 수 있습니다.</p><pre><code>/mcp disable elastic-agent-builder</code></pre><p>그런 다음 Gemini CLI에 다음과 같은 프롬프트를 입력하기만 하면 됩니다.</p><pre><code>Find the top 5 product categories by total sales revenue in the kibana_sample_data_ecommerce index</code></pre><p>에이전트가 수행하는 작업:</p><ul><li><code>esql</code> 스킬의 필요성을 인식해야 합니다.</li><li><strong><code>kibana_sample_data_ecommerce</code></strong> 스키마를 확인하세요.</li><li><code>FROM kibana_sample_data_ecommerce | STATS total_revenue = SUM(taxful_total_price) BY category.keyword | SORT total_revenue DESC | LIMIT 5</code> 등 ES|QL 쿼리를 구성합니다.</li><li>Elasticsearch API에 대해 쿼리 작업을 실행하세요.</li><li>터미널에서 바로 최종 답을 제시합니다.</li></ul><p>여기서는 이전 프롬프트에 대한 Gemini의 응답 사례를 보고했습니다.</p><pre><code>───────────────────────────────────────────────────────────
> Find the top 5 product categories by total sales revenue in the kibana_sample_data_ecommerce index   ───────────────────────────────────────────────────────────
✓  Activate Skill "esql": Interact with Elasticsearch using ES|QL and curl. Use when querying, managing indices,  …
Skill esql activated. Resources loaded from /home/user/.gemini/extensions/elasticsearch/skills/esql: 
Showing up to 200 items (files + folders).                                                                     /home/user/.gemini/extensions/elasticsearch/skills/esql/                                                            ├───SKILL.md 
└───references/
├───esql-reference.md 
├───generation-tips.md 
└───query-patterns.md

✦ I will start by exploring the mapping of the kibana_sample_data_ecommerce index to identify the fields for product category and sales revenue. Then, I will construct and execute an ES|QL query to determine the top 5 product categories by total sales revenue.

✓  Shell curl -s "${ELASTIC_URL%/}/kibana_sample_data_ecommerce/_mapping" -H "Authorization: ApiKey $(printenv ELAS…                                                                                                                       {  
   "kibana_sample_data_ecommerce": { 
     "mappings": { 
       "properties": {
         "category": { 
           "type": "text", 
           "fields": {
             "keyword": {
               "type": "keyword"
             }
           }
         }, 
         "currency": {
           "type": "keyword"
         }, 

✦ Okay, I have the mapping. The category is products.category.keyword and sales revenue is taxful_total_price. I'll now execute the ES|QL query to get the top 5 categories by revenue.

✓  Shell curl -s -X POST "${ELASTIC_URL%/}/_query" \  -H "Authorization: ApiKey $(printenv ELASTIC_API_KEY)" \  -H …
{ 
   "took": 62,
   "is_partial": false, 
   "completion_time_in_millis": 1772805644164,
   "documents_found": 4675,
   "values_loaded": 12084, 
   "start_time_in_millis": 1772805644102, 
   "expiration_time_in_millis": 1773237644087,  
   "columns": [ 
     { 
       "name": "total_revenue",    
       "type": "double"  
     }, 
     {   
       "name": "products.category.keyword",

✦ The query for top 5 product categories by revenue was successful. I'll format the returned JSON values into a   user-friendly list.

 ✓  Shell 
                             
 Top 5 product categories by total sales revenue: 
 1. Men's Clothing: 149393.91
 2. Women's Clothing: 135099.91
 3. Women's Shoes: 105479.17
 4. Men's Shoes: 91797.92
 5. Women's Accessories: 60830.31 

✦ I've found the answer; now I'll format it for clarity.

 ✓  Shell 

 Here are the top 5 product categories by total sales revenue:                                                        
                                                                      
 | Rank | Category              | Total Revenue | 
 |------|-----------------------|---------------| 
 | 1    | Men's Clothing        | 149,393.91    |
 | 2    | Women's Clothing      | 135,099.91    |
 | 3    | Women's Shoes         | 105,479.17    |
 | 4    | Men's Shoes           | 91,797.92     | 
 | 5    | Women's Accessories   | 60,830.31     | 

✦ I've formatted and presented the answer. I'm finished.
 ───────────────────────────────────────────────────────────</code></pre><p>Gemini 모델이 최종 응답을 도출하는 모든 단계를 보여주는 방식은 흥미로운 부분입니다. 여기에서 모델의 추론 과정에 대한 스킬의 영향력을 확실하게 확인할 수 있습니다. 모델이 스킬을 사용하거나 셸 명령을 실행해야 한다고 처음 인식할 때, 휴먼 인 더 루프 방식을 사용하여 권한을 요청합니다.</p><p><code>esql</code> 스킬은 스키마 발견, 쿼리 생성 및 실행과 같은 복잡한 작업을 처리함으로써 답을 얻는 과정의 메커니즘이 아닌 답 자체에만 집중할 수 있도록 도와줍니다. 필요한 데이터를 제대로 된 형식으로 터미널에서 바로 얻을 수 있습니다. 단 한 줄의 구문도 작성하거나 다른 애플리케이션으로 컨텍스트를 전환할 필요도 없습니다.</p><h2>결론</h2><p>이 글에서는 최근에 출시한 Gemini CLI용 Elasticsearch 확장 기능을 소개했습니다. 이 확장 프로그램을 사용하면 Gemini와 Elastic Agent Builder에서 제공하는 Elasticsearch MCP 서버(버전 9.3.0부터 사용 가능)를 이용하여 Elasticsearch 인스턴스와 상호 작용할 수 있습니다. <code>/elastic</code> 명령어도 사용할 수 있습니다.</p><p>더불어, 이 확장에는 사용자의 요청을 자연어에서 ES|QL 쿼리로 변환하는 <code>esql</code> 스킬도 포함되어 있습니다. 이 기술은 MCP 서버를 사용할 수 없는 경우 특히 유용합니다. 기본 통신이 터미널에서 실행되는 간단한 curl 명령에 의해 이루어지기 때문입니다. Elasticsearch는 어떤 프로젝트에도 쉽게 통합될 수 있는 풍부한 REST API 세트를 제공합니다. 이는 에이전틱 AI 애플리케이션을 개발할 때 특히 유용합니다.</p><p>Gemini CLI 확장 기능에 대한 자세한 내용은 <a href="https://github.com/elastic/gemini-cli-elasticsearch">여기</a> 프로젝트 리포지토리에서 확인하세요.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Agent Skills for Elastic: AI 에이전트를 Elastic 전문가로 전환하기]]></title>
            <description><![CDATA[Elastic Agent Skills로 AI 코딩 에이전트에게 지식을 제공하세요. 데이터를 쿼리하고 시각화하며 보안 및 자동화까지 수행합니다.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/agent-skills-elastic</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/agent-skills-elastic</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Graham Hudgins, Matt Ryan]]></dc:creator>
            <pubDate>Mon, 16 Mar 2026 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/09e64953819083168a9ecef0888c7f8bde1a43bd-720x420.jpg?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>특화된 플랫폼에서 AI 코딩 에이전트를 사용해 본 개발자, 사이트 신뢰성 엔지니어(SRE), 혹은 분석가라면 누구나 한 번쯤 같은 장벽에 부딪힌 적이 있을 것입니다. 에이전트에게 쿼리 작성을 요청하거나 알림 설정 또는 특정 사안을 조사해 달라고 하면 그럴듯하게 답변은 하지만 정확하지는 않은 경우가 있습니다. Elastic은 여기서 큰 강점을 가지고 있습니다. 10년 넘게 축적된 문서, 블로그 게시물 및 커뮤니티 답변 덕분에 AI 에이전트는 이미 다른 데이터 플랫폼보다 Elastic을 더 잘 알고 있습니다. 하지만 그 방대한 지식에는 노이즈도 섞여 있습니다. 최신 API 사이에 이미 사용 중단된 API가 나열됩니다. 이 때문에 낡은 패턴들이 마치 모범 사례인 것처럼 제안되곤 합니다. 에이전트는 3단계 전 버전에서나 통했을 방식을 마치 정답인 양 자신 있게 내놓습니다. 학습 데이터 안에서는 여전히 성공적인 사례로 남아 있기 때문입니다. 결국 사용자는 문서 내용을 수동으로 컨텍스트에 입력하고 AI가 만들어낸 잘못된 구문을 수정하는 등 에이전트와 함께 일하기보다 에이전트를 수습하는 데 더 많은 시간을 할애하게 됩니다. 더 안타까운 점은 사용자가 필요하지 않아서가 아니라 에이전트가 그 존재를 몰라서 고급 기능들이 아예 활용되지 못한다는 것입니다.</p><p>이것이 바로 Elastic이 <a href="https://github.com/elastic/agent-skills">Elastic Agent Skills</a>를 오픈 소스로 공개하는 이유입니다. 이는 Elasticsearch, Kibana, Elastic Observability 및 Elastic Security를 위한 네이티브 플랫폼 전문 지식입니다. 현재 사용 중인 에이전트 런타임에 이 스킬들을 추가해 보세요. 수많은 구문을 추측만 하던 &#x27;범용&#x27; 수준의 에이전트를 Elastic 엔지니어링 팀만큼의 아키텍처 표준을 구사하는 전문가로 업그레이드할 수 있습니다. 이번 초기 기술 프리뷰 버전은 <a href="https://www.elastic.co/cloud/serverless">Elastic Cloud Serverless</a>와 최상의 호환성을 갖춘 스킬을 중심으로 제공됩니다. 하지만 빠른 시일 내에 이전 스택 버전에 대한 지원도 넓혀나갈 예정입니다.</p><p>또한 Elastic은 이 문제를 양방향에서 해결하고 있습니다. 우선 Elastic 플랫폼 내의 에이전트를 위해, 이제 정식 버전으로 출시된 <a href="https://www.elastic.co/search-labs/blog/agent-builder-elastic-ga">Elastic Agent Builder</a>를 제공합니다. 이를 통해 데이터 접근 권한을 상속받고 내장된 검색 및 분석 도구를 사용하며 대시보드, 알림 및 조사와 연동되는 AI 에이전트를 생성하고 대화할 수 있습니다. Elastic은 플랫폼 내에서 최상의 에이전트 중심 경험을 구현하기 위해 전력을 다하고 있습니다. 하지만 모든 에이전트가 Elastic 내부에서만 활동하는 것은 아닙니다. 여러분의 팀은 이미 Cursor, Claude Code 또는 다른 런타임을 사용하고 있을 것이며 이 에이전트들 역시 Elastic을 제대로 활용할 수 있어야 합니다. 바로 그 지점에서 Agent Skills가 핵심적인 역할을 수행합니다.</p><h2>AI 에이전트가 전문 플랫폼에서 어려움을 겪는 이유</h2><p>대규모 언어 모델(LLM)은 다방면에 능한 팔방미인과 같습니다. 학습 데이터의 예제가 풍부하다 보니 Python을 작성하거나 Kubernetes 매니페스트를 설명하고 React 컴포넌트를 리팩토링하는 일을 능숙하게 해냅니다. 그러나 전용 쿼리 언어와 방대한 API 체계 그리고 해당 도메인의 특수한 설계 원칙을 준수해야 하는 전문 플랫폼 작업에서는 예외 없이 한계를 드러내고 맙니다.</p><p>Elasticsearch의 경우 이러한 간극은 다음과 같이 구체적으로 나타납니다.</p><ul><li><strong>Elasticsearch 쿼리 언어(ES|QL)는 새로운 영역입니다.</strong> LLM은 SQL 위주로 집중 학습되었지만 ES|QL은 구문과 함수, 의미 체계가 전혀 다른 파이프 기반의 쿼리 언어입니다. 이 때문에 에이전트는 겉보기엔 그럴듯하지만 실제로는 구문 분석이 되지 않는 쿼리를 작성하는 경우가 많습니다. <code>WHERE</code>을 <code>| WHERE</code>으로 착각하거나 존재하지 않는 함수를 임의로 만들어내고 파이프 중심의 쿼리 구성 방식을 완전히 놓치기도 합니다.</li><li><strong>API 체계는 방대하고 복잡합니다.</strong> Elasticsearch, Kibana 및 Elastic Security는 검색, 수집, 알림, 탐지 규칙, 케이스 관리, 대시보드 등 전반에 걸쳐 수백 개의 API를 제공합니다. 보편적인 학습 데이터만 갖춘 에이전트는 어떤 엔드포인트를 호출할지, 요청 본문은 어떻게 구성할지, 응답은 어떻게 처리할지를 추측에 의존할 수밖에 없습니다. 그리고 이러한 추측이 빈번하게 빗나가면서 결국 사용자의 신뢰를 떨어뜨리게 됩니다.</li><li><strong>학습 데이터에는 실무 노하우가 반영되어 있지 않습니다.</strong> 언제 <code>semantic_text</code>를 사용하고 언제 커스텀 임베딩 파이프라인을 구축할지, 10GB 크기의 CSV 수집 파이프라인은 어떻게 구성해야 할지, 혹은 <a href="https://www.elastic.co/docs/solutions/security/detect-and-alert/mitre-attandckr-coverage">MITRE ATT&amp;CK</a> 기술에 적합한 탐지 규칙 구문은 무엇인지와 같은 고민은 학습 데이터만으로 해결하기 어렵습니다. 범용 에이전트에는 잘 정리되고 신뢰할 수 있는 구조를 갖춘 Elastic 전용 지식이 기본적으로 탑재되어 있지 않기 때문입니다. 에이전트가 직접 정보를 찾아낸다 하더라도 가공되지 않은 문서는 숙련된 전문가가 가진 상황별 판단이나 실무 노하우까지 모두 담아내지는 못합니다.</li></ul><p>그 결과 개발자는 직접 코드를 짤 때보다 에이전트의 결과물을 수정하는 데 더 많은 시간을 허비하게 됩니다. 이는 우리가 기대했던 경험이 전혀 아닙니다.</p><h2>에이전트 스킬: 에이전트를 위해 패키징된 플랫폼 지식</h2><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/8d16ec00d16e70a916c5eef0aaa23fcc735b7186-1067x1280.png" style="width: 100%;" /><p>Agent Skills는 에이전트 런타임이 동적으로 로드할 수 있는 지침, 스크립트 및 참조 자료가 포함된 독립적인 디렉터리입니다. 스킬이 활성화되면 에이전트는 적절한 시점에 적절한 컨텍스트(쿼리 구문, API 패턴, 검증 논리, 실제 실행 사례 등)에 접근할 수 있게 되며 이를 통해 첫 시도만으로도 작업을 정확하게 완료할 수 있습니다.</p><p>각 스킬은 오픈 소스인 <a href="https://agentskills.io">agentskills.io </a>사양을 따릅니다. 이는 메타데이터와 구조화된 지침이 담긴 <code>SKILL.md</code> 파일이 포함된 폴더 형태입니다. 독자적인 포맷도 벤더 종속성도 없습니다. 스킬은 Cursor, Claude Code, GitHub Copilot, Windsurf, Gemini CLI, Cline, Codex를 비롯하여 <a href="https://agentskills.io">다양한</a> 에이전트 런타임에 걸쳐 작동합니다.</p><h3>초기 v0.1.0 릴리스 포함 내용</h3><p>첫 번째 스킬 세트는 Elastic Stack의 다섯 가지 영역을 아우릅니다.</p><ul><li>Elasticsearch API와 상호 작용(검색, 인덱싱, 클러스터 관리)</li><li>Kibana 콘텐츠 구축 및 관리(대시보드, 알림, 커넥터 등)</li><li>Elastic Observability를 위한 도메인 전문성</li><li>Elastic Security를 위한 도메인 전문성</li><li>Agent Builder 내에서 효과적인 에이전트 만들기</li></ul><h3>스킬의 결합성</h3><p>스킬은 거대한 단일 구조가 아닙니다. 설계 단계부터 모듈화되어 있습니다. 에이전트는 현재 수행 중인 작업에 필요한 스킬만 로드합니다. ES|QL 쿼리를 작성 중인가요? ES|QL 스킬이 활성화됩니다. 그 결과로 대시보드를 만들어야 하나요? 대시보드 스킬이 작업을 넘겨받습니다. 애플리케이션의 상태를 평가 중인가요? 서비스 상태 스킬이 작동합니다. 보안 알림을 조사 중인가요? 분석 스킬이 조사의 진행 단계에 따라 케이스 관리 및 대응 스킬로 이어집니다.</p><p>이러한 결합성은 모든 것을 한꺼번에 해결하려는 거대한 단일 프롬프트가 필요 없음을 의미합니다. 각 스킬은 해당 도메인에 필요한 만큼의 컨텍스트만을 담고 있으며 그 이상도 그 이하도 아닙니다.</p><h2>검색 및 AI 애플리케이션을 구축하는 개발자를 위한 스킬</h2><p>Elasticsearch에 데이터를 로드하거나 쿼리를 작성하고 인덱스를 마이그레이션할 때 에이전트 스킬은 코드를 생성하고 오류에 부딪힌 뒤 원인을 찾기 위해 문서를 검색해야 하는 과정을 줄여줍니다.</p><p>에이전트에게 CSV 파일 로드를 요청하면 에이전트는 백프레셔를 처리하고 데이터에서 매핑을 추론하는 스트리밍 수집 도구를 사용합니다. 이는 대용량 파일을 처리하자마자 메모리 부족을 일으키는 임시방편의 수동 _bulk 루프가 아닙니다. ES|QL로 쿼리하도록 요청하면 에이전트는 실제 인덱스 이름과 필드 스키마를 찾아낸 뒤 정확한 구문, 적절한 집계, 버전별 기능을 반영한 유효한 파이프 쿼리를 작성합니다. 세 번씩 디버깅을 반복해야 하는 SQL의 흉내만 낸 추측성 답변이 아닙니다. 클러스터 간 재인덱싱을 요청하면 에이전트는 명시적 매핑으로 대상 인덱스를 생성하고 처리량 최적화를 위한 설정 튜닝까지 수행합니다. 또한 작업을 비동기로 실행하고 완료 후 운영 설정을 복구하는 전체 운영 워크플로우를 따릅니다. 숙련된 운영자가 수행할 단계들을 절반이나 건너뛰는 단순한 _reindex 호출이 아닙니다.</p><p>사용자가 직접 수정해야 하는 그럴싸한 시작점만 제공하는 에이전트 대신 운영 방식을 내재화하여 실행 가능한 결과물을 만드는 에이전트를 얻게 됩니다.</p><p><strong>Elastic Agent Skills 사용에 따른 주요 변화 예시</strong></p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Eval</td><td style="border: 1px solid #ccc; padding: 4px;">스킬 적용 시 변화된 내용</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">es-audit-query-failed-logins</td><td style="border: 1px solid #ccc; padding: 4px;">일반적인 검색 대신 스킬에 정의된 감사 로그 쿼리 패턴을 사용함</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">es-authz-role-mapping-ldap</td><td style="border: 1px solid #ccc; padding: 4px;">정확한 역할 매핑 API 호출 구조를 출력함</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">esql-기본 쿼리</td><td style="border: 1px solid #ccc; padding: 4px;">쿼리 DSL을 통한 ES|QL 파이프 구문 작성</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">esql 오류 처리</td><td style="border: 1px solid #ccc; padding: 4px;">필드 이름을 추측하는 대신 스키마를 우선적으로 확인함</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">esql-schema-discovery</td><td style="border: 1px solid #ccc; padding: 4px;">인덱스 이름을 절대 임의로 추측하지 않음</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">es-ingest-csv-with-infer</td><td style="border: 1px solid #ccc; padding: 4px;">--infer-mappings를 단독 사용하고 빈 인덱스를 유발하는 --source-format csv와의 조합을 피함</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">es-ingest-json-file</td><td style="border: 1px solid #ccc; padding: 4px;">대용량 파일을 처리할 수 있는 견고한 수집 방식을 사용함</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">es-reindex-local-async</td><td style="border: 1px solid #ccc; padding: 4px;">복제본: 0 및 refresh_interval: "-1" 설정으로 대상 인덱스를 먼저 생성한 뒤 비동기 재인덱싱 수행함. 기본 모델은 준비 단계를 건너뜀</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">es-security-403-privileges</td><td style="border: 1px solid #ccc; padding: 4px;">일반적인 조언 대신 권한 오류에 대한 스킬의 진단 워크플로우를 따름</td>
                    </tr>
              </tbody>
            </table>
          <h2>보안 팀을 위한 스킬</h2><p>보안 팀은 매일 알림 심사, 탐지 규칙 최적화, 케이스 관리와 같은 동일한 운영 워크플로우를 반복합니다. Agent Skills는 이러한 절차적 지식을 내재화하여 AI 에이전트가 정확한 필드 이름으로 적절한 API를 올바른 순서에 따라 호출하며 워크플로우를 수행할 수 있게 합니다. IDE를 벗어나지 않고 초기 설정부터 실제 데이터가 완비된 Elastic Security 환경을 구축하는 실습 과정은 <a href="https://www.elastic.co/security-labs/agent-skills-elastic-security">AI 에이전트로 Elastic Security 시작하기</a> 가이드를 참조하세요.</p><h2>통합 가시성 및 운영 팀을 위한 스킬</h2><p>Elastic Observability를 위한 새로운 Agent Skills는 복잡한 시스템의 계측, SLO 관리, 복잡한 데이터 선별, 서비스 상태 평가 등에 드는 운영상의 수고를 줄여줍니다. AI 에이전트에 Elastic의 네이티브 전문 지식을 직접 내재화함으로써 운영 팀은 간단한 자연어만으로도 복잡한 통합 가시성 워크플로우를 실행할 수 있습니다. 이를 통해 SRE 및 운영 팀은 장애를 더 빠르게 해결하고 시스템의 신뢰성을 더욱 쉽게 유지할 수 있습니다. 자세한 내용은 <a href="https://www.elastic.co/observability-labs/blog/elastic-agent-skills-observability-workflows">블로그</a>에서 확인해 보세요.</p><h2>오픈 소스, 오픈 사양, 커뮤니티 중심</h2><p>Elastic은 에이전트의 지식이 공개되어야 한다고 믿기에 Agent Skills를 Apache 2.0 라이선스로 출시합니다. 각 스킬이 따르는 <a href="https://agentskills.io">agentskills.io</a> 사양은 Elastic의 독자적인 포맷이 아닌 오픈 표준입니다. Elastic은 에이전트 스킬이 폐쇄적인 환경이 아닌 커뮤니티가 함께 만들어가는 노력이 되기를 바랍니다.</p><h2>더 큰 그림의 일부</h2><p>Agent Skills는 Elasticsearch를 현존하는 가장 에이전트 친화적인 데이터 플랫폼으로 만들기 위한 광범위한 이니셔티브의 일환입니다. Elasticsearch 플랫폼 기반의 에이전트를 위해 <a href="https://www.elastic.co/search-labs/blog/agent-builder-elastic-ga">Agent Builder</a>는 데이터 액세스 제어 및 권한을 상속받아 한 단계 더 진화된 기능을 제공합니다. 또한 검색 및 분석을 위한 내장 및 커스텀 도구를 제공하며 사용자가 대시보드, 알림 및 조사 워크플로우와 함께 컨텍스트에 맞게 에이전트와 상호 작용할 수 있도록 지원합니다. 마지막으로 Agent Builder에 스킬 지원 기능이 곧 추가될 예정입니다. 이를 통해 개발자는 Elastic Agent Skills뿐만 아니라 다른 소스의 스킬까지 유연하게 활용하여 Elasticsearch 플랫폼에서 보안이 강화되고 컨텍스트가 풍부한 채팅 및 자동화를 구현할 수 있게 됩니다.</p><p>Elastic 외부의 다양한 환경에서 운영되는 에이전트를 위해 Elastic은 다음과 같은 오픈 생태계에 투자하고 있습니다.</p><ul><li><strong>모델 컨텍스트 프로토콜(MCP) 서버 확장:</strong> 현재의 검색, ES|QL, 인덱스 작업을 넘어 더 많은 도구를 사용할 수 있도록 Agent Builder의 <a href="https://www.elastic.co/docs/explore-analyze/ai-features/agent-builder/mcp-server">MCP 엔드포인트</a>를 확장합니다.</li><li><strong>인증 방식 개선:</strong> 수동으로 API 키를 복사하고 붙여넣는 과정을 없애는 것을 목표로 에이전트가 더욱 쉽고 안전하게 연결할 수 있도록 개선합니다.</li><li><strong>LLM이 이해하기 쉬운 문서:</strong> 에이전트가 스스로 Elastic API를 탐색하고 이해할 수 있도록 <code>llms.txt</code> 및 <code>AGENTS.md</code> 파일을 게시합니다.</li><li><strong>에이전트 워크플로우용 명령줄 인터페이스(CLI):</strong> 연결 관리 및 일반적인 작업을 에이전트 친화적으로 만들어 주는 명령줄 도구를 제공합니다.</li></ul><p>스킬은 현재 바로 사용할 수 있는 계층이며 나머지도 곧 추가될 예정입니다.</p><h2>시작하기</h2><p><strong>시작하기 전에:</strong> AI 코딩 에이전트는 실제 자격 증명과 실제 셸 액세스 권한으로 작동하며 대개 에이전트를 실행하는 사용자의 모든 권한을 가집니다. 이러한 에이전트가 보안 워크플로우에 투입될 경우 그 위험성은 더욱 커집니다. 탐지 로직, 대응 조치 및 민감한 텔레메트리에 대한 접근 권한을 자동화된 시스템에 넘겨주는 셈이기 때문입니다. 조직마다 위험 수용 범위는 다릅니다. AI 기반 보안 워크플로우를 활성화하기 전에 <strong>에이전트가 어떤 데이터에 접근할 수 있는지, 어떤 작업을 수행할 수 있는지 그리고 예기치 않게 동작할 경우 어떤 영향을 미칠지 평가해야 합니다</strong>.</p><p>Elastic Agent Skills를 에이전트 런타임에 설치합니다.</p><p><code>npx skills add elastic/agent-skills</code></p><p>이 명령은 설치된 에이전트 런타임을 자동으로 감지하여 올바른 구성 디렉터리에 스킬을 배치합니다. 이후 에이전트는 해당 스킬을 자동으로 인식합니다.</p><p>또한 <a href="https://github.com/elastic/agent-skills">스킬 카탈로그</a>를 직접 탐색하여 스킬 폴더를 에이전트의 구성 디렉터리에 복사함으로써 개별 스킬을 수동으로 설치할 수도 있습니다.</p><p>아직 Elasticsearch 클러스터가 없으신가요? <a href="https://cloud.elastic.co/registration">Elastic Cloud 무료 체험판</a>을 시작해 보세요. 약 1분이면 모든 설정이 완료된 환경을 준비할 수 있습니다.</p><p><strong>프로젝트 살펴보기:</strong></p><ul><li><a href="https://github.com/elastic/agent-skills">Agent Skills 저장소</a></li><li><a href="https://agentskills.io">agentskills.io 사양</a></li><li><a href="https://www.elastic.co/docs">Elasticsearch 문서</a></li><li><a href="https://cloud.elastic.co/registration">Elastic Cloud 무료 체험판</a></li></ul>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Elasticsearch를 통한 엔티티 해석, 4부: 최종 과제]]></title>
            <description><![CDATA[지름길을 방지하도록 설계된 고도로 다양한 '궁극의 과제' 데이터 세트에서 엔티티 해석 문제를 해결하고 평가합니다.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/entity-resolution-elasticsearch-llm-challenges</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/entity-resolution-elasticsearch-llm-challenges</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Jessica Moszkowicz]]></dc:creator>
            <pubDate>Fri, 13 Mar 2026 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/70fb0ff949f6db9ac9b8a28ecb4329ab915ebf46-720x420.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>이제 두 가지 방식으로 구현된 지능형 엔티티 해석을 확인했습니다. 두 접근 방식 모두 동일한 방식으로 시작합니다. 즉, 엔티티 준비 및 추출을 거쳐 Elasticsearch로 후보 검색이 진행됩니다. 그 후, 프롬프트 기반 JSON 생성 또는 함수 호출을 통해 대규모 언어 모델(LLM)을 사용하여 후보를 평가하며, 모델이 판단에 대한 투명한 설명을 제공하도록 요구합니다.</p><p><a href="https://www.elastic.co/search-labs/blog/elasticsearch-entity-resolution-llm-function-calling">이전 게시물</a>에서 앞서 보았듯이, 함수 호출이 제공하는 일관성은 단순한 최적화가 아닌 필수 사항입니다. 평가 루프에서 구조적 오류를 제거하자 표준 시나리오(예: 티어 4 데이터 세트)의 결과가 크게 향상되었습니다.</p><p>하지만 아직 답변이 필요한 질문도 분명히 남아 있습니다.</p><p><em>상황이 아주 복잡해질 때도 이 접근 방식이 여전히 효과가 있을까요?</em></p><p>실제 엔티티 해석은 단순한 경우로 인해 실패하는 일이 거의 없습니다. 이름이 언어, 문화, 문자 체계, 시대, 조직 경계를 넘나들 때 실패하게 됩니다. 사람들이 이름 대신 직함으로 언급되거나, 회사가 이름을 바꾸거나, 음역이 일관되지 않거나, 철자가 아니라 컨텍스트만이 실제 세계의 엔티티와 연결되는 유일한 요소일 때 실패하게 됩니다.</p><p>그래서 이 시리즈의 마지막 포스팅에서 시스템에 &#x27;<strong>궁극의 도전</strong>&#x27;이라는 과정을 진행했습니다.</p><h2>이것이 궁극적인 도전인 이유는 무엇입니까?</h2><p>이전 평가에서는 점점 더 복잡한 데이터 세트를 사용하여 시스템을 테스트했습니다. 이전 게시물에서 논의된 티어 4에 도달했을 때는 이미 별명, 직함, 다국어 이름 및 의미 참조의 혼합을 다루고 있었습니다. 해당 테스트 결과, 아키텍처 자체는 견고했음에도 특히 잘못된 JSON 형식과 같은 신뢰성 문제로 인해 재현율이 저해되는 것으로 나타났습니다.</p><p>함수 호출이 구현됨에 따라 마침내 안정적인 기반을 마련할 수 있었습니다. 덕분에 더 흥미로운 질문을 할 기회가 생겼습니다.</p><p><em>하나의 통합 파이프라인이 <strong>여러 종류의</strong> 엔티티 해석 문제를 동시에 처리할 수 있을까요?</em></p><p>궁극의 과제 데이터 세트는 바로 그러한 차원을 시험하기 위해 설계되었습니다.</p><p>별명이나 음역과 같은 단일한 문제에 집중하는 대신, 이 데이터 세트는 다음과 같은 <strong>50개 이상의 다양한 과제 유형</strong>을 결합합니다.</p><ul><li>문화적 지명 관습</li><li>직함 기반 참조</li><li>비즈니스 관계와 역사적 이름 변경 사항.</li><li>다국어 및 교차 스크립트 언급</li><li>위의 여러 가지가 혼합된 복합 과제입니다.</li></ul><p>무엇보다 중요한 것은 이것이 단 하나의 제한적인 사용 사례에 대한 최적화가 아니라는 것입니다. 엔티티 간에 규칙이 변경될 때 <em>설계 패턴</em>이 유지되는지 테스트하는 것이 중요한 부분입니다.</p><h2>데이터 세트 개요</h2><p>궁극의 과제 데이터 세트는 다음과 같이 구성됩니다.</p><ul><li>사람, 조직, 기관이 포함된 <strong>50개 엔티티</strong>.</li><li>다양한 구조와 언어적 복잡성을 지닌 <strong>약 60개의 기사</strong>.</li><li><strong>51개의 뚜렷한 과제 카테고리</strong>가 다음과 같이 넓게 그룹화됩니다.<ul><li>문화적 지명 관습</li><li>직함 및 전문적 맥락.</li><li>비즈니스 및 조직적 관계</li><li>다국어 및 음역 문제.</li><li>결합 및 극단 시나리오</li></ul></li></ul><p>이 시리즈의 초반부에서 생성형 AI(GenAI)를 사용하여 데이터 세트를 생성하는 것에 장단점이 있다는 것을 확인했습니다. 생성형 AI가 없으면 충분히 크고 다양한 테스트 데이터를 구성하는 것이 매우 어려워집니다. 그러나 제어하지 않으면 모델이 작업을 지나치게 쉽게 만드는 경향이 있습니다.</p><p>초기 세대 합격 예시를 보면 모델이 블라디미르 푸틴의 명시적 별칭으로 &#x27;러시아 대통령&#x27;과 같은 구문을 포함시켰다는 사실이 발견되었습니다. 현재로서는 합리적으로 보일 수 있지만, 이는 상황별 해결 테스트의 목적을 무효화합니다. 기사가 1990년대 러시아에 대해 논의하고 있다면 어떻게 될까요? 시스템은 하드코딩된 별칭에 의존하지 않고 컨텍스트에서 올바른 엔티티를 추론해야 합니다.</p><p>그러한 이유로, 이 데이터 세트는 <strong>지름길이 통하지 않도록</strong> 의도적으로 설계되었습니다. 별칭은 시스템이 의미를 유추해야 할 때 명시적으로 나열되지 않습니다. 설명적 구문이 엔티티에 사전 연결되어 있지 않습니다. 정확한 일치는 단순한 로컬 텍스트가 아닌 문서 수준의 컨텍스트에 의존하기도 합니다.</p><p><strong>중요 사항:</strong> 다양한 시나리오에서 시스템의 기능을 시연하는 것이지만 이는 교육용 프로토타입임을 알려드립니다. 실제 제재 대상 엔티티 모니터링을 처리하는 프로덕션 시스템에는 추가적인 검증, 규정 준수 점검, 감사 추적 및 민감한 사용 사례에 대한 전문적인 처리가 필요합니다.</p><h2>이러한 시나리오가 어려운 이유</h2><p>이 시리즈의 첫 번째 게시물에서 “새로운 Swift 업데이트가 도착했습니다!”라는 간단하지만 모호한 예를 소개했습니다. 문제는 &#x27;Swift&#x27;가 상황에 따라 실제 세계의 여러 엔티티로 해석될 수 있다는 것입니다. 이 예시는 더 큰 진실을 드러냅니다. 자연어가 본질적으로 모호하다는 것이죠.</p><p>따라서 엔티티 해석은 단순히 스트링 일치 문제가 아닙니다. 인간은 일상적으로 공유된 지식, 문화적 규범, 상황적 맥락에 의존하여 참조를 해결하며, 그렇게 하고 있다는 사실을 거의 인지하지 못합니다.</p><p>고려해야 할 일반적인 케이스:</p><ul><li>지정학 및 시대적 컨텍스트가 없다면 &#x27;대통령&#x27;과 같은 직함은 의미가 없습니다.</li><li>회사 이름은 기사 작성 시기에 따라 모회사, 자회사 또는 이전 브랜드를 가리킬 수 있습니다.</li><li>사람의 이름은 언어와 문화에 따라 다른 순서, 문자 체계 또는 음역으로 표현될 수 있습니다.</li><li>동일한 문구가 다른 컨텍스트에서 다른 엔티티를 정당하게 참조할 수 있으며, 시스템은 일치 항목을 수락하는 것만큼 자신 있게 <em>거부</em>할 수 있어야 합니다.</li></ul><p>이러한 모든 상황을 깔끔하게 처리할 수 있는 단일 규칙 세트는 없습니다. 그래서 이 프로토타입은 우려 사항을 매우 철저하게 분리합니다.</p><ul><li>Elasticsearch는 후보 공간을 효율적이고 투명하게 좁힙니다.</li><li>LLM은 판단이 필요하고 스스로 설명해야 하는 경우에만 사용됩니다.</li><li>검색과 추론은 여전히 뚜렷하게 구분됩니다.</li></ul><p>과제 유형이 다양해질수록 이러한 분리가 더욱 중요해집니다.</p><h2>시스템이 특별 사례 없이 다양성을 처리하는 방식</h2><p>이 평가에서 가장 흥미로운 결과 중 하나는 변하지 <em>않은</em> 부분입니다.</p><ul><li>일본어 이름에 대한 특별한 로직을 추가하지 <strong>않았습니다</strong>.</li><li>아랍어 부칭에 대한 사용자 지정 규칙을 추가하지 <strong>않았습니다</strong>.</li><li>과거 회사 이름에 하드코딩된 매핑을 추가하지 <strong>않았습니다</strong> .</li></ul><p>대신 이 시스템은 이 시리즈에서 앞서 소개된 것과 동일한 핵심 요소에 의존했습니다.</p><ul><li>시맨틱 검색을 위해 인덱싱된 컨텍스트가 풍부한 엔티티</li><li>Elasticsearch 내 하이브리드 검색(정확성, 별칭, 의미론적 검색)</li><li>잘 정의된 소규모 후보 일치 집합</li><li>함수 호출 및 최소 스키마에 의해 제한되는 LLM 판단</li></ul><p>이는 시스템의 유연성이 점점 늘어나는 규칙 집합이 아니라 <strong>표현과 구조</strong>에서 온다는 것을 시사합니다.</p><p>시스템이 성공하는 경우는 올바른 후보가 검색되고, LLM이 참조가 특정 엔티티에 매핑되거나 매핑되지 않는 이유를 설명할 충분한 맥락을 제공하기 때문입니다.</p><h2>결과: 성능은 어떠했습니까?</h2><p>궁극의 과제 데이터 세트에서 시스템은 다음과 같은 종합적인 결과를 도출했습니다.</p><ul><li><strong>정밀도:</strong> ~91%</li><li><strong>재현율:</strong> ~86%</li><li><strong>F1 점수:</strong> ~89%</li><li><strong>LLM 합격률:</strong> ~72%</li></ul><h3>도전 과제 유형 전반에 걸친 성과</h3><p>과제 유형별로 결과를 분석하면 강점과 한계가 드러납니다.</p><p><strong>가장 강한 성능(100% F1 점수)</strong>은 다음과 같은 영역에서 관찰되었습니다.</p><ul><li>스크립트 간 매칭(키릴 문자, 한국어, 중국어 사업체)</li><li>히브리어 시나리오(부칭, 전문 직함, 종교 직함, 음역)</li><li>비즈니스 계층(항공우주, 다각적인 제조, 다사업부 기업)</li><li>전문직 칭호(학문적, 군사적, 정치적, 종교적).</li><li>여러 문자 체계를 포함하는 결합된 일본어 시나리오입니다.</li></ul><p><strong>강력한 성능(80–99% F1 점수)</strong> 포함 사항:</p><ul><li>국제 정치인(98%)</li><li>과거 이름 변경 내역(90%)</li><li>복잡한 비즈니스 계층(89%)</li><li>일본 회사 이름(93%)</li><li>교차 문자 체계 음역(86%)</li><li>아랍어 애칭(86%)</li></ul><p><strong>더 까다로운 영역</strong> 포함:</p><ul><li>고급 음역(중국어, 한국어): 0% F1.</li><li>특정 일본어 시나리오(경어법, 이름 순서, 쓰기 체계 변형): ~67% F1.</li><li>일부 아랍어 시나리오(회사명, 기관 참조): ~40% F1.</li></ul><p>여기서 중요한 것은 이 사례에서 시스템이 어려움을 겪은 <em>이유</em>입니다. 이러한 실패는 전반적인 접근 방식이 고장났기 때문이 아니라 특정 구성 요소, 특히 특정 다국어 시나리오에서 시맨틱 검색에 사용되는 고밀도 벡터 모델의 한계로 인한 것이었습니다.</p><p>검색과 판단이 명확하게 분리되어 있기 때문에, 성능 향상을 위해 시스템을 다시 작성할 필요가 없습니다. 보다 뛰어난 다국어 임베딩 모델로 교체하거나, 엔티티 컨텍스트를 강화하거나, 검색 전략을 개선하면 핵심 아키텍처를 변경하지 않고도 이러한 카테고리 전반에 걸쳐 결과를 향상시킬 수 있습니다.</p><p>아키텍처 관점에서는 그것이 진정한 성공 지표입니다.</p><h2>디자인에 대해 알 수 있는 내용</h2><p>시리즈를 되돌아보면, 몇 가지 패턴이 눈에 띕니다.</p><ul><li><strong>영리한 매칭보다 준비가 더 중요합니다. </strong>사전에 컨텍스트로 엔티티를 보강하면 이후 모호성을 크게 줄일 수 있습니다.</li><li><strong>LLM은 검색자가 아니라 심사 위원으로서의 가치가 가장 뛰어납니다.</strong>검색하라고 요청하는 것보다 일치하는 <em>이유</em>를 설명해달라고 요청하는 것이 훨씬 효과적입니다.</li><li><strong>신뢰성이 정확성을 뒷받침합니다. </strong>함수 호출은 JSON을 정리하는 것뿐만 아니라, 검색 단계에서 이미 잠재되어 있던 기억력을 끌어내는 역할을 했습니다.</li><li><strong>일반화가 전문화보다 우수합니다.</strong>잘 선택한 소규모 추상화를 통해 사용자 정의 로직 없이 수십 가지 과제 유형을 처리할 수 있었습니다.</li></ul><p>이것이 바로 프로토타입이 의도적으로 Elasticsearch 네이티브 방식으로 설계되었고, LLM 사용 방식에 있어서도 의도적으로 보수적인 접근 방식을 취한 이유입니다. 목표는 검색을 대체하는 것이 아니라, 의미가 중요한 상황에서 검색을 설명 가능하게 만드는 것입니다.</p><h2>결론</h2><p>궁극의 과제는 완벽한 지표를 추구하기 위한 것이 아니라, 더 근본적인 질문에 답하기 위한 것입니다.</p><p><em>투명한 검색 우선의 LLM 기반 아키텍처가 규칙이나 블랙박스로 전락하지 않고 실제 세계의 엔티티 모호성을 처리할 수 있을까요?</em></p><p>이 교육용 프로토타입의 경우 프로덕션 강화, 규정 준수, 모니터링 및 데이터 품질과 관련된 명확한 주의사항이 있지만 그 답은 &#x27;예&#x27;입니다. 엔티티 일치가 이루어진 <em>이유</em>를 정당화해야 하는 시스템을 구축하는 경우, 이 패턴을 진지하게 고려할 가치가 있습니다. 이 시리즈를 통해 엔티티 해결이 불가사의하지 할 이유가 없다는 것을 알게 되셨기를 바랍니다. 문제를 제대로 분리하면 엔티티 해결을 이해하고, 측정하고, 개선할 수 있습니다.</p><p>이 작업은 또한 더 광범위한 아키텍처 패턴을 제안합니다. 이를 통해 고전적인 Retrieval-Augmented Generation(RAG) 방식의 미미하지만 중요한 진화가 드러납니다. 검색이 생성에 직접 정보를 제공하게 하는 대신, 명시적인 평가 단계를 도입했습니다. LLM이 먼저 사용되어 검색된 후보를 심사하고 무결성을 확인하며 승인된 결과만 생성을 보강하는 것이 허용됩니다. 이를 Generation-Augmented Retrieval-Augmented Generation with Evaluation, 즉 GARAGE라고 생각하시면 됩니다. 이렇게 좋은 약어를 마다할 사람은 없겠죠?</p><p>이 패턴이 다른 어떤 사용 사례에 도움이 될 수 있을까요? 신뢰, 투명성 및 변호 가능한 추론이 필요한 시스템이 당연히 후보가 될 것입니다. 이 분야의 향후 작업은 여기서 본 결과만큼이나 매력적일 것입니다. 커뮤니티가 다음에 어떤 방향으로 나아갈지 정말 기대됩니다.</p><h2>다음 단계: 직접 사용해 보기</h2><p>궁극의 과제가 실제로 작동하는 모습을 확인하고 싶으세요? 실제 구현, 자세한 설명 및 실습 예제가 포함된 <a href="https://github.com/jesslm/entity-resolution-lab-public/tree/main/notebooks#:~:text=5%20minutes%20ago-,05_ultimate_challenge_v3.ipynb,-Initial%20public%20lab"><strong>궁극의 과제 노트북</strong></a>을 확인해 보세요.</p><p>완전한 엔티티 해석 파이프라인은 프로덕션 환경에 필요한 핵심 개념과 아키텍처를 보여줍니다. 이를 기반으로 모든 과정에서 투명성과 설명 가능성을 유지하는 동시에 뉴스 기사를 모니터링하고, 엔티티 언급을 추적하며, 어떤 엔티티가 어떤 기사에 등장하는지에 대한 질문에 답하는 시스템을 구축할 수 있습니다.<br/></p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Elasticsearch의 HNSW를 위한 적응형 조기 종료]]></title>
            <description><![CDATA[Elasticsearch의 HNSW를 위한 새로운 적응형 조기 종료 전략을 소개합니다.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/hnsw-elasticsearch-adaptive-early-termination</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/hnsw-elasticsearch-adaptive-early-termination</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Tommaso Teofili]]></dc:creator>
            <pubDate>Mon, 02 Mar 2026 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/e6d3186f609dd56dc5ffe33d70fa9e5cfa05b51f-1280x720.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>Elasticsearch는 근접 그래프상에서 벡터 검색을 수행하기 위해 <a href="https://www.elastic.co/search-labs/blog/hnsw-graph">계층적으로 탐색 가능한 작은 세계</a>(HNSW) 알고리즘을 사용합니다. HNSW는 k-최근접 이웃(KNN) 결과의 품질과 그에 수반되는 비용 사이에서 훌륭한 절충안을 제공하는 것으로 알려져 있습니다.</p><p>HNSW에서 검색은 그래프 내의 후보 노드들을 반복적으로 확장하며, 현재까지 발견된 최근접 이웃의 제한된 집합을 유지하는 방식으로 진행됩니다. 각 확장 단계마다 비용(벡터 연산, 디스크 임의 탐색 등)이 발생하며, 검색이 진행될수록 그 비용 대비 얻게 되는 한계 이익은 점차 감소하는 경향이 있습니다.</p><p>HNSW 그래프 탐색을 최적화하는 한 가지 방법은, 새로운 진짜 이웃을 찾을 한계 확률이 더 이상 증가하지 않을 때 탐색을 중단하는 것입니다. 이러한 이유로, <a href="https://www.elastic.co/docs/reference/elasticsearch/index-settings/index-modules#index-dense-vector-hnsw-early-termination">Elasticsearch 9.2</a>에서는 새로운 <a href="https://www.elastic.co/search-labs/blog/hnsw-knn-search-early-termination">조기 종료 메커니즘</a>을 도입했습니다. 이 방식은 그래프 노드를 방문해도 새로운 최근접 이웃을 충분히 찾지 못하는 상태가 고정된 횟수만큼 연속으로 발생하면 검색 프로세스를 중단합니다.</p><p>이 글은 다양한 데이터 세트와 데이터 분포에 더 잘 대응할 수 있도록, HNSW의 조기 종료 메커니즘을 개선한 방법을 안내합니다.</p><h2><strong>HNSW의 조기 종료</strong></h2><p>HNSW에서 검색은 근접 그래프 내의 후보 노드들을 반복적으로 확장하며, 현재까지 발견된 근접 이웃의 제한된 집합을 유지하는 방식으로 진행됩니다. 이는 그래프 전체를 방문하거나 특정 조기 종료 기준을 충족할 때까지 계속됩니다.</p><p>따라서 조기 종료는 반드시 항상 최적화를 위한 선택 사항인 것만은 아닙니다. <strong>검색 알고리즘 그 자체의 일부입니다</strong>. 탐색을 중단하기로 결정하는 그 순간이 효율성과 재현율 사이의 균형을 결정합니다. Elasticsearch에는 이미 HNSW 쿼리가 조기 종료될 수 있는 몇 가지 방법이 존재합니다.</p><ul><li>방문하는 노드의 최대 개수는 정해져 있습니다.</li><li>정해진 제한 시간에 도달했습니다.</li></ul><p>이러한 규칙들은 단순하고 예측 가능하지만, <strong>검색이 실제로 어떻게 진행되고 있는지에 대해서는 대체로 무관심합니다</strong>. 또한, 이러한 규칙들은 주로 최종 사용자에게 적절한 시간 내에 쿼리가 완료되도록 하기 위한 용도로 사용됩니다.</p><p><a href="https://www.elastic.co/search-labs/blog/hnsw-knn-search-early-termination">이전 블로그 게시물</a>에서 HNSW의 중복성이라는 개념을 소개한 바 있습니다. 요약하자면, HNSW가 새로운 최근접 이웃을 찾아내지 못하는 새로운 후보 노드들을 계속해서 평가할 때 중복 계산이 발생합니다.</p><h2><strong>인내심: 노력이 아닌 진척도를 측정하기</strong></h2><p><em>인내심</em>이라는 개념은 조기 종료의 기준을 <strong>노력이 아닌 진전</strong>을 중심으로 재정의합니다.</p><p>다음과 같은 질문을 던지는 대신:</p><p>“우리가 몇 걸음을 걸었습니까?”</p><p>새로운 질문은 다음과 같습니다:</p><p>“더 나은 결과를 찾을 가망이 없다고 판단하기까지, 얼마만큼의 연산 낭비를 감수할 수 있을까요?&quot;</p><p>HNSW 검색 과정에서, 초기 탐색은 일반적으로 top-k 후보 집단에 대해 가장 비약적인 개선을 만들어냅니다. HNSW 그래프 탐색의 첫 단계에서는, 알고리즘이 쿼리 벡터에 점점 더 가까운 이웃들을 계속해서 발견함에 따라 이웃 집합이 계속해서 업데이트됩니다. 시간이 흐름에 따라, 검색이 수렴하면서 이러한 개선은 점차 드물어집니다. <a href="https://cs.uwaterloo.ca/~jimmylin/publications/Teofili_Lin_ECIR2025.pdf">인내심 기반 종료</a>는 이러한 패턴을 모니터링하며, 개선이 일정 기간 지속적으로 발생하지 않으면 검색을 종료합니다.</p><p>실제로 HNSW 그래프를 방문하는 동안, 후보 노드들을 거쳐 가며 큐 포화도를 함께 계산합니다. 이는 가장 최근의 그래프 노드를 방문하는 동안 변경되지 않고 그대로 남은 근접 이웃의 비율(또는 마지막 반복 과정에서 새로 추가된 이웃 수의 역수)을 측정합니다. 이러한 비율이 너무 많은 연속된 반복 과정 동안 과도하게 높아지면, 더 이상의 그래프 방문을 중단합니다</p><p>개념적으로 볼 때, 인내심은 HNSW 검색을 <strong>수익 체감의 과정</strong>으로 취급합니다. 수익이 정체되는 시점에 도달하면, 그래프를 계속해서 탐색하는 것은 실질적인 이익을 거의 주지 못합니다.</p><p>이러한 프레이밍은 강력합니다. 종료 시점을 임의로 정해진 고정된 한계치가 아니라, <em>관찰 가능한 결과</em>에 직접 연결하기 때문입니다.</p><p>이러한 스마트 조기 종료 기술을 사용하면, 거의 완벽한 상대적 재현율을 유지하면서도 HNSW 그래프 탐색 과정에서 방문하는 노드 수를 줄일 수 있다는 장점이 있습니다.</p><p>이를 시각화하기 위해, 몇 가지 데이터 세트인 FinancialQA, Quora과 모델인 JinaV3, E5-small 조합을 대상으로 인내심 기반 조기 종료(<em><code>et=static</code></em>와 HNSW 기본 작동 방식(<em><code>et=no</code></em>)을 비교하여, 방문한 노드 수에 따른 재현율의 변화량을 그래프로 그려볼 수 있습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/a9d07c5153ea64a2426c82487c36846030692bb9-1600x945.png" style="width: 100%;" /><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/dac56125c4b16d1b596c9876b6ca9ac7b2dc87fa-1600x944.png" style="width: 100%;" /><h2><strong>정적 임계값 및 HNSW 동역학</strong></h2><p>실제로, Elasticsearch는 이를 <strong>정적 임계값</strong>을 사용하여 구현했습니다. 하나의 임계값은 <strong>포화 임계값</strong>입니다. 이는 우리가 차선이라고 간주하는 포화 비율을 의미합니다. 또 다른 임계값은 <strong>인내심 임계값</strong>입니다. 이는 큐 포화도가 여전히 낮은 상태임에도, 방문을 계속 허용할 연속적인 그래프 노드 방문 횟수를 의미합니다.</p><p>Elasticsearch 9.2에 이 조기 종료 전략을 도입할 당시, 지연 시간과 메모리 소모 측면에서 이득을 보면서도 재현율은 최대한 유지할 수 있도록 보수적인 기본값을 선택하기로 결정했습니다. 이러한 이유로, 포화 임계값을 100%로 설정하고, 인내심 임계값은 KNN 쿼리에서 <a href="https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-knn-query#knn-query-top-level-parameters:~:text=search%20request%20size.-,num_candidates,-(Optional%2C%20integer)%20The"><em><code>num_candidates</code></em></a>의 30% 수준(상한선 제한 있음)으로 설정했습니다.</p><p>많은 시나리오에서 이러한 설정들이 잘 작동하기도 했지만, 동일한 수의 이웃을 요청하는 두 쿼리라도 그 수렴 동작은 근본적으로 다를 수 있습니다. 어떤 쿼리는 밀집된 지역 이웃을 만나 빠르게 포화 상태에 도달하는 반면, 다른 쿼리는 경쟁력 있는 후보군을 찾기까지 길고 희소한 경로를 통과해야만 합니다. 후자가 효과적으로 처리하기 가장 어려운 것으로 나타났습니다.</p><p>그 결과, 때때로 다음과 같은 현상들이 관찰되었습니다:</p><ul><li>쉬운 쿼리에 대한 과도한 탐색.</li><li>까다로운 쿼리에 대한 조기 종료.</li></ul><p>따라서 고정된 임계값이 수렴에 대한 일괄적인 가정을 전제로 하는 반면, HNSW가 다양한 동역학에 더 잘 적응하도록 만들 수 있다는 점을 깨달았습니다.</p><h2><strong>HNSW 조기 종료 적응형 구현</strong></h2><p>적응형 조기 종료는 이 문제에 대해 기존과는 다른 각도에서 접근합니다. 미리 정의된 중단 임계값을 강제하는 대신, 알고리즘은 <strong>검색 역학 자체로부터 언제 멈춰야 할지를 추론합니다</strong>.</p><p>따라서 연속된 두 후보 사이의 큐 포화율을 비교하는 대신, 그래프 방문 중의 (<a href="https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Welford&#x27;s_online_algorithm">웰포드 알고리즘을 사용한</a>) 이동 평균 $\mu_{q,i}$ 및 표준 편차 $\sigma_{q,i}$와(과) 함께, 순간 평활 발견율 $d_{q,i}$(마지막 방문 <em>i</em>에서 쿼리 <em>q</em>에 대해 도입된 새로운 이웃의 수)를 도입하기로 결정했습니다. 발견율에 관한 이러한 통계치들은 각 쿼리당 개별적으로 계산됩니다. 이 정보를 사용하여 각 쿼리의 특성에 맞춰 서로 다른 수준의 인내심을 적용할 수 있습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/1958be7ca4425ade66eaf621ada3533173183598-694x118.png" style="width: 100%;" /><p>이전의 정적 임계값들은 이제 발견율 통계에 따라 적응형으로 변합니다. 포화 임계값은 이동 평균에 표준 편차를 더한 값이 되며, 인내심 수치는 표준 편차에 반비례하여 적응하고 확장되도록 만들었습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/f7ee4a55c24853b657df26052b275e8bd76cf0f9-654x156.png" style="width: 100%;" /><p>조기 종료 규칙은 기존과 동일하게 유지됩니다. 즉, 순간 발견율이 적응형 포화 임계값보다 낮아질 때 포화가 발생합니다. 포화 상태가 적응형 인내심보다 더 많은 횟수의 연속적인 후보 방문 동안 지속되면, 그래프 방문이 중단됩니다</p><p>이러한 방식을 통해, KNN 쿼리의 <em><code>num_candidates</code></em> 매개변수(조기 종료 여부와 상관없이 항상 설정되어 있거나 기본값으로 남겨지는 값)에 의존하지 않으면서도, 각 쿼리와 벡터 분포에 맞춰 동적으로 더 잘 적응하는 동작을 구현할 수 있습니다.</p><p>FinancialQA 및 Quora에서 적응형 전략(<em><code>et=adaptive</code></em>)의 방문 노드당 재현율은 정적 전략(<em><code>et=static</code></em>) 및 기본 HNSW 동작(<em><code>et=no</code></em>)과 비교했을 때 더 높은 수치를 나타냅니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/2a906997d9a25d74c7038bd9661bc97581e7258e-1600x938.png" style="width: 100%;" /><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/3a114911e232c351dbb814cea20e8b0f1415a717-1600x925.png" style="width: 100%;" /><p>적응형 조기 종료는 Elasticsearch 9.3부터 HNSW 밀집 벡터 필드에 기본으로 활성화되며, <a href="https://www.elastic.co/docs/reference/elasticsearch/index-settings/index-modules#index-dense-vector-hnsw-early-termination">동일한 인덱스 수준 설정</a>을 통해 나중에 비활성화할 수도 있습니다.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[공통 표현식 언어(CEL): CEL 입력이 Elastic 에이전트 통합에서 데이터 수집을 개선하는 방법]]></title>
            <description><![CDATA[공통 표현식 언어(CEL)가 다른 프로그래밍 언어와 어떻게 다른지, Filebeat의 CEL 입력을 위해 어떻게 확장되었는지, 또 Elastic Agent 통합에서 데이터 수집 논리를 표현하는 데 얼마나 유연하게 활용할 수 있는지 알아보세요.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/common-expression-language-elasticsearch</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/common-expression-language-elasticsearch</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Chris Berkhout]]></dc:creator>
            <pubDate>Fri, 27 Feb 2026 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/985c50bfabee3348494eb4307f0b3375a97a0644-1280x720.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>사용자는 Elastic Agent <a href="https://www.elastic.co/integrations">통합</a>을 통해 다양한 소스에서 Elasticsearch로 데이터를 수집할 수 있습니다. 수집 논리, 인제스트 파이프라인, 대시보드 및 기타 아티팩트를 Kibana 웹 인터페이스에서 설치 및 관리 가능한 패키지로 결합할 수 있습니다.</p><p>통합은 데이터 수집을 수행하도록 <a href="https://www.elastic.co/docs/reference/beats/filebeat/configuration-filebeat-options">Filebeat 입력</a>을 구성합니다. HTTP API에서 데이터를 수집하는 데 종종 <a href="https://www.elastic.co/docs/reference/beats/filebeat/filebeat-input-httpjson">HTTP JSON 입력</a>이 사용되었습니다. 그러나 기본적인 목록 조회 API도 세부 사항에서 크게 차이가 날 수 있으며, HTTP JSON 입력의 YAML 기반 변환 모델에서는 필요한 수집 논리를 표현하는 것이 불편하거나 불가능한 경우가 있습니다.</p><p><a href="https://www.elastic.co/docs/reference/beats/filebeat/filebeat-input-cel">공통 표현식 언어(CEL) 입력</a>은 HTTP API와 보다 유연한 상호 작용을 촉진하기 위해 도입되었습니다. <a href="https://cel.dev/">CEL</a>은 빠르고 안전하며 확장 가능한 방식으로 조건과 데이터 변환을 표현해야 하는 애플리케이션에 내장되도록 설계된 언어입니다. 통합 빌더는 CEL 입력을 통해 설정을 읽고, 자체 상태를 추적하고, 요청을 생성하고, 응답을 처리하고, 궁극적으로 수집할 준비가 된 이벤트를 반환하는 하나의 표현식을 작성할 수 있습니다.</p><p>이 글에서는 CEL이 다른 프로그래밍 언어와 어떻게 다른지, CEL 입력을 위해 어떻게 확장되었는지, 또 데이터 수집 논리를 표현하는 데 어떤 유연성과 장점을 제공하는지 살펴보겠습니다.</p><h2>CEL과 입력에서의 작동 원리</h2><p>CEL은 표현식 언어입니다. 진술이 아닙니다. CEL을 작성할 때는 명령문으로 무엇을 할지 지시하는 것이 아니라, 표현식으로 어떤 값을 생성할지 정의합니다. 모든 CEL 표현식은 값을 생성하며, 더 작은 표현식을 결합하여 복잡한 규칙에 따른 결과를 만들어낼 수 있습니다. 다른 언어에서 명령문으로 작성되는 기능을 표현식으로 구현하는 방법을 나중에 살펴보겠습니다.</p><p>CEL은 의도적으로 튜링 완전 언어가 아닙니다. 무제한 루프를 허용하지 않습니다. 매크로를 사용하여 리스트와 맵을 처리하는 방법을 후에 살펴보겠지만, 무한 루프를 방지함으로써 개별 표현식에 대해 예측 가능하고 제한된 실행 시간을 보장합니다.</p><p>CEL 입력은 CEL 프로그램(표현식)과 몇 가지 초기 상태로 구성됩니다. 상태는 프로그램에 대한 입력으로 제공되고, 프로그램은 평가를 통해 출력 상태를 생성합니다. 출력 상태에 이벤트 목록이 포함되어 있으면 해당 이벤트가 제거되고 게시됩니다. 나머지 출력 상태는 다음 평가를 위한 입력으로 사용됩니다. 출력 상태에 하나 이상의 이벤트와 플래그 <code>want_more: true</code>가 포함되어 있으면 즉시 다음 평가가 수행되고, 포함되어 있지 않은 경우에는 구성된 시간 동안 대기한 후 진행됩니다. 다음은 입력의 제어 흐름을 간략하게 나타낸 다이어그램입니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/42671541f97e2dba808fd53969fe12f517917f9a-1600x529.png" style="width: 100%;" /><p>각 평가의 출력은 입력이 실행되는 동안 다음 평가의 입력으로 전달됩니다. &quot;<code>cursor</code>&quot; 키 아래의 출력 데이터는 디스크에 보존되어 입력이 재시작된 후 다시 로드되지만, 나머지 상태는 재시작 시 보존되지 않습니다.</p><p>CEL 언어 자체는 기능이 제한적이며 부작용을 방지하지만, 확장 가능합니다. <a href="https://github.com/google/cel-go">cel-go</a> 구현은 선택적 구문 및 유형과 같은 기능을 추가합니다. <a href="https://github.com/elastic/mito">Mito</a> 라이브러리는 cel-go를 기반으로 빌드되어 HTTP 요청 기능을 포함한 더 많은 기능을 추가합니다. CEL 입력은 Mito의 CEL 버전을 사용합니다.</p><h2>Mito와 함께 작동</h2><p>CEL 입력을 사용하여 통합을 빌드하거나 디버깅할 때는 CEL 프로그램이 주어진 입력 상태에 대해 생성할 출력 상태를 이해하는 것이 가장 중요합니다. 개발 중에는 전체 Elastic 스택 환경에서 입력을 통해 CEL 프로그램을 실행하는 것이 번거로울 수 있습니다. Mito의 명령줄 도구를 사용하면 더 빠른 피드백 루프를 구현해 CEL 프로그램을 직접 실행하고 주어진 입력에 대한 출력 결과를 바로 확인할 수 있습니다.</p><p>Mito는 Go 언어로 작성되었으며 다음과 같이 설치할 수 있습니다.</p><pre><code>go install github.com/elastic/mito/cmd/mito@latest</code></pre><p>Mito로 CEL 프로그램을 실행할 때 일반적으로 두 개의 파일을 제공해야 합니다. 하나는 초기 입력 상태를 포함한 JSON 파일이고, 다른 하나는 CEL 프로그램의 소스 코드가 포함된 파일입니다.</p><pre><code>mito -data state.json src.cel</code></pre><p>쉽게 복사하여 붙여넣을 수 있도록 이 글의 예시는 각 파일의 내용을 <code>&lt;(echo &#x27;...content...&#x27;)</code> 로 감싸서 셸이 임시 파일을 즉시 생성하도록 하는 단일 명령으로 작성되었습니다. 자체 개발 시에는 실제 파일을 다루는 것이 더 쉬울 수 있습니다.</p><h2>GitHub에서 이슈 데이터 가져오기</h2><p>다음 예시에서는 <a href="https://docs.github.com/en/rest/issues/issues?apiVersion=2022-11-28#list-repository-issues">GitHub API</a>에서 이슈 데이터를 가져오는 전체 CEL 프로그램을 볼 수 있습니다. 초기 입력 상태에는 API 엔드포인트의 URL과 페이지네이션 처리 방식에 대한 정보가 포함되어 있습니다. CEL 프로그램은 입력 상태의 데이터를 사용하여 요청을 생성합니다. 응답을 디코딩하고, 그로부터 이벤트를 생성한 다음, 이를 출력 상태의 일부로 반환합니다.</p><pre><code>mito -data <(echo '
  {
    "url": "https://api.github.com/repos/elastic/integrations/issues",
    "per_page": 3,
    "max_pages": 3
  }
') <(echo '
  int(state.?cursor.page.orValue(1)).as(page,
    (
      state.url + "?" + {
        "state": ["all"],
        "sort": ["created"],
        "direction": ["asc"],
        "per_page": [string(state.per_page)],
        "page": [string(page)],
      }.format_query()
    ).as(full_url,
      request("GET", full_url).with({
        "Header": {
          "Accept": ["application/vnd.github+json"],
          "X-GitHub-Api-Version": ["2022-11-28"],
        }
      }).do_request().as(resp,
        resp.Body.decode_json().as(data,
          state.with({
            "events": data.map(i, {
              "html_url": i.html_url,
              "title": i.title,
              "created_at": i.created_at,
            }),
            "cursor": { "page": page + 1 },
            "want_more": size(data) == state.per_page && page < state.max_pages,
          })
        )
      )
    )
  )
')</code></pre><p>첫 번째 평가는 다음과 같은 출력을 생성합니다.</p><pre><code>{
  "cursor": {
    "page": 2
  },
  "events": [
    {
      "created_at": "2018-09-14T09:47:35Z",
      "html_url": "https://github.com/elastic/integrations/issues/3250",
      "title": "Increase support of log formats in haproxy filebeat module"
    },
    {
      "created_at": "2019-02-06T12:37:37Z",
      "html_url": "https://github.com/elastic/integrations/issues/487",
      "title": "ETCD Metricbeat module needs polishing and grooming"
    },
    {
      "created_at": "2019-08-13T11:33:11Z",
      "html_url": "https://github.com/elastic/integrations/pull/1",
      "title": "Initial structure"
    }
  ],
  "max_pages": 3,
  "per_page": 3,
  "url": "https://api.github.com/repos/elastic/integrations/issues",
  "want_more": true
}</code></pre><p>이벤트가 제거되고 CEL 입력에서 실행되면 수집을 위해 게시됩니다. 나머지 출력 결과는 다음 CEL 프로그램 평가의 입력 상태로 제공됩니다.</p><p></p><p>CEL 프로그램의 작동 방식을 이해하기 위해, 몇 가지 쉬운 CEL 예시시를 살펴보면서CEL 입력이 작동하는 방식에 대해 자세히 알아보겠습니다.</p><h2>CEL 기본 사항</h2><p>CEL 언어에는 문이 없고 표현식만 있습니다. 모든 성공적인 CEL 표현식은 최종 값으로 평가됩니다. 다음은 출력과 함께 작성할 수 있는 가장 간단한 CEL 표현식 중 하나입니다.</p><pre><code>mito <(echo '
  "hello" + " " + "world"
')</code></pre><pre><code>"hello world"</code></pre><p>간단한 표현식은 대개 직관적입니다. 수학적 연산은 같은 유형의 값(예시: <code>int</code>를 가진 <code>int</code>)에만 지원되므로 필요에 따라 (여기에서는<code>int</code> 에서 <code>double</code>로) 유형을 변환합니다.</p><pre><code>mito <(echo '
  double((1 + 2) * (3 + 4)) / 2.0
')</code></pre><pre><code>10.5</code></pre><p>CEL 언어에는 변수가 없지만, Mito의 <a href="https://pkg.go.dev/github.com/elastic/mito/lib#hdr-As__Macro_-Collections"><code>as</code></a> 매크로를 이용해 표현식에 이름을 부여하고 더 큰 표현식에서 사용할 수 있습니다. 이 예시에서 <code>(1 + 1)</code> 표현식은 <code>2</code> 값으로 평가되고 <code>.as(n, ...)</code>는 해당 값에 <code>n</code> 라는 이름을 부여하여 <code>&quot;one plus one is &quot;+string(n)</code> 표현식에서 사용합니다.</p><pre><code>mito <(echo '
  (1 + 1).as(n, "one plus one is "+string(n))
')</code></pre><pre><code>"one plus one is 2"</code></pre><p>또한 <a href="https://pkg.go.dev/github.com/elastic/mito/lib#hdr-With-Collections"><code>with</code></a> 를 사용한 예처럼 맵에 정보를 축적하고 나중에 표현식에서 사용할 수도 있습니다.</p><pre><code>mito <(echo '
  { "key": "value" }.with({ "key2": "value2" }).as(data,
    {
      "data": data,
      "size": size(data),
    }
  )
')</code></pre><pre><code>{
  "data": {
    "key": "value",
    "key2": "value2"
  },
  "size": 2
}</code></pre><p>이 예시를 다시 살펴보세요. 중첩된 부분 <code>({ &quot;data&quot;: data, &quot;size&quot;: size(data), })</code>은 최종 값의 형태를 제공합니다. <code>&quot;data&quot;</code> 및 <code>&quot;size&quot;</code> 키를 포함한 맵입니다. 이러한 키의 값은 표현식의 외부 부분에 정의된 <code>data</code>에 따라 달라집니다. CEL 표현식을 안쪽에서 바깥쪽으로 읽으면 반환되는 내용을 빠르게 파악하는 데 도움이 됩니다.</p><p>CEL에는 <code>if</code>와 같은 제어 흐름 문이 없지만, 삼항 연산자를 사용해 조건부 분기를 수행할 수 있습니다:</p><pre><code>mito <(echo '
  1 + 1 < 12 ? "few" : "many"
')</code></pre><pre><code>"few"</code></pre><p>CEL은 튜링 완전 언어가 아니기 때문에 무한 루프와 재귀가 지원되지 않습니다. 따라서 실행 시간을 예측할 수 있으며 , 이는 입력 데이터의 크기와 표현식의 복잡성에 비례합니다.</p><p>개별 CEL 표현식에서는 무한 루프가 불가능하지만, <a href="https://github.com/google/cel-spec/blob/master/doc/langdef.md#macros"><code>map</code></a> 과 같은 매크로를 사용해 목록과 맵을 처리할 수 있습니다.</p><pre><code>mito <(echo '
  [1, 2, 3].map(x, x * 2)
')</code></pre><pre><code>[2, 4, 6]</code></pre><p>이 섹션에서는 다음과 같은 내용을 다룹니다.</p><ul><li>스트링, 숫자, 리스트, 맵</li><li>스트링 연결</li><li>수학 연산</li><li>유형 캐스팅</li><li>조건문</li><li>하위 표현식의 이름 지정</li><li>컬렉션 처리</li></ul><p>다음으로 HTTP 요청을 하는 방법을 살펴보겠습니다.</p><h2>요청</h2><p>Mito는 CEL을 확장해 <a href="https://pkg.go.dev/github.com/elastic/mito/lib#HTTP">HTTP 요청</a>을 만드는 기능을 추가합니다.</p><pre><code>mito <(echo '
  get("https://example.com").as(resp, string(resp.Body))
')</code></pre><pre><code>"<!doctype html><html lang=\"en\"><head><title>Example Domain</title>..."</code></pre><p>실행되기 전에 명시적으로 요청을 구성할 수 있습니다. 이를 통해 다양한 HTTP 메서드를 사용하고 헤더와 본문을 추가할 수 있습니다.</p><p>이 예시에서는 <a href="https://pkg.go.dev/github.com/elastic/mito/lib#hdr-Format_Query-HTTP"><code>format_query</code></a>의 도움을 받아 URL을 만들고, 요청에 헤더를 추가한 뒤, 응답 본문을 <a href="https://pkg.go.dev/github.com/elastic/mito/lib#hdr-Decode_JSON-JSON"><code>decode_json</code></a>으로 구문 분석합니다. <code>-log_requests</code> 옵션이 주어지면 Mito는 각 요청 및 응답에 대한 자세한 정보를 JSON 형식으로 로그에 기록합니다.</p><pre><code>mito -log_requests <(echo '
  request("GET",
    "https://postman-echo.com/get?" + {
        "q": ["query value"]
     }.format_query()
  ).with({
    "Header": { "Accept": ["application/json"] }
  }).do_request().as(resp, {
    "status": resp.StatusCode,
    "data": resp.Body.decode_json(),
  })
')</code></pre><pre><code>{"time":"...","level":"INFO","msg":"HTTP request",...}
{"time":"...","level":"INFO","msg":"HTTP response",...}
{
  "data": {
    "args": {
      "q": "query value"
    },
    "headers": {
      "accept": "application/json",
      "accept-encoding": "gzip, br",
      "host": "postman-echo.com",
      "user-agent": "Go-http-client/2.0",
      "x-forwarded-proto": "https"
    },
    "url": "https://postman-echo.com/get?q=query+value"
  },
  "status": 200
}</code></pre><h2>상태 관리 및 평가</h2><p>이제 요청 방법과 원하는 출력 상태를 생성하는 데 필요한 CEL 기본 사항을 알아보았으니 출력 상태에 무엇을 넣어야 하는지, 그리고 이것이 이후 처리를 어떻게 지시하는지 자세히 살펴보겠습니다.</p><p>통합의 CEL 프로그램은 출력 상태가 다음 평가의 입력으로 적합하게 사용될 수 있도록 해야 합니다. 구성은 초기 상태를 설정하며, 적절한 변경이 있을 경우 출력에서 이를 반복해야 합니다. 이를 수행하는 쉬운 방법은 <code>state.with({ ... })</code> 를 사용해 일부 재정의와 함께 상태 맵을 반복하는 것입니다. 작은 프로그램에서는 전체 프로그램을 <code>state.with()</code>로 감싸서 상태 전파가 출력 데이터를 생성하는 각 분기(예시: 성공, 오류)에서 반복되지 않도록 하는 패턴이 자주 사용됩니다.</p><p>상태 값이 초기 입력 상태에 하드코딩되지 않고 평가를 통해 초기화되는 경우, 프로그램은 초기값을 설정하기 전에 기존 값이 있는지 확인해야 합니다. <a href="https://pkg.go.dev/github.com/google/cel-go/cel#OptionalTypes">선택적 구문 및 유형</a>에 대한 지원이 이러한 문제를 해결하는 데 도움이 될 수 있습니다. 맵 키에서 필드 이름 앞에 물음표를 사용하면 액세스가 선택 사항이 됩니다. 값으로 확인될 수도 있고 확인되지 않을 수도 있지만, 추가 선택적 액세스가 가능하며 값이 없는 경우 기본값을 쉽게 제공할 수 있습니다:<br/></p><pre><code>mito -data <(echo '{}') <(echo '
  int(state.?counter.orValue(0)).as(counter,
    state.with({
      "counter": counter + 1,
      "want_more": counter + 1 < 3,
    })
  )
')</code></pre><pre><code>{ "counter": 1, "want_more": true }
{ "counter": 2, "want_more": true }
{ "counter": 3, "want_more": false }</code></pre><p>이 예시의 상태에서 읽은 카운터 값은 JSON과 JavaScript의 <code>Number</code> 유형이 정한 관례에 따라 부동소수점 숫자로 직렬화되어 있으므로 <code>int</code>로 캐스팅됩니다. 또한 여기서 <code>&quot;want_more&quot;: true</code>는 Mito에 의해 존중되지만, CEL 입력에서 실행될 때는 출력에 이벤트가 포함될 때만 평가가 반복된다는 점에 유의해야 합니다.</p><p>CEL 입력으로 실행되는 CEL 프로그램의 경우 출력 맵에 <code>&quot;events&quot;</code> 키를 반환해야 합니다. 이 값은 이벤트 맵의 목록, 빈 목록 또는 단일 이벤트 맵일 수 있습니다. 단일 이벤트 사례는 일반적으로 오류 처리에 사용됩니다. 이벤트는 입력에 의해 게시되지만 해당 값도 로그에 기록되며, <code>error.message</code> 값이 설정되면 해당 값이 통합의 Fleet 상태를 업데이트하는 데 사용됩니다. 프로그램에서 오류가 아닌 단일 이벤트를 생성하는 경우 목록으로 래핑하는 것이 가장 좋습니다.</p><p>앞서 살펴본 GitHub 이슈 프로그램의 결과를 다시 한 번 살펴보세요.</p><pre><code>{
  "url": "https://api.github.com/repos/elastic/integrations/issues",
  "per_page": 3,
  "max_pages": 3,
  "cursor": {
    "page": 2
  },
  "events": [
    { ... },
    { ... },
    { ... }
  ],
  "want_more": true
}</code></pre><p>이 프로그램은 다음과 같은 방법으로 상태를 효과적으로 관리했습니다.</p><ul><li><code>url</code>, <code>per_page</code>, <code>max_pages</code>에서 초기 상태 값을 반복합니다.</li><li><code>cursor.page</code>에서 재시작 후 지속되어야 할 상태를 추가합니다.</li><li><code>events</code> 목록에 게시할 준비가 된 이벤트를 반환합니다.</li><li><code>want_more: true</code>를 사용해 즉각적인 재평가를 요청합니다.</li></ul><p>선택적 액세스 및 상태 관리, CEL 기본 사항 및 HTTP 요청에 대해 배워보았으니, 이제 전체 GitHub 이슈 프로그램을 읽을 수 있습니다. Mito로 실행하고 몇 가지 변경 사항을 실험해 보세요.</p><h2>검토 및 리소스</h2><p>이 글에서는 CEL 언어가 무엇인지, 그리고 Mito 라이브러리에서 CEL 입력에 사용하기 위해 어떻게 확장되었는지 알아봤습니다. GitHub API에서 이슈 정보를 가져오는 예제 프로그램에서 CEL의 유연성을 살펴보고, 초기 상태의 설정 액세스, HTTP API와의 상호작용, 수집할 이벤트 반환, 이후 프로그램 실행을 위한 상태 관리 등 해당 프로그램을 이해하는 데 필요한 모든 세부 사항을 살펴봤습니다.</p><p>CEL 입력을 사용하여 통합 기능을 구축하고 자세히 알아보려면 다음과 같은 여러 자료를 살펴보시기 바랍니다.</p><ul><li><a href="https://www.elastic.co/docs/reference/beats/filebeat/filebeat-input-cel">CEL 입력 - Filebeat 설명서</a></li><li><a href="https://pkg.go.dev/github.com/elastic/mito">Mito 설명서</a></li><li><a href="https://cel.dev/">공통 표현식 언어 - cel.dev 웹사이트</a></li><li><a href="https://www.elastic.co/docs/extend/integrations">통합 생성 - Elastic 문서</a></li></ul><p>CEL 입력을 활용한 통합 구축에 있어 가장 유용한 리소스 중 하나는 GitHub에서 찾아볼 수 있는 기존 Elastic 통합의 CEL 코드입니다.</p><p><a href="https://github.com/search?q=repo%3Aelastic%2Fintegrations+path%3A**%2Fcel.yml.hbs&amp;type=code"><code>cel.yml.hbs</code> Elastic 통합 리포지토리에 저장된 파일 - GitHub</a></p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Elasticsearch 및 LLM을 사용한 엔터티 해석, 2부: LLM 판단 및 시맨틱 검색을 사용한 엔터티 매칭]]></title>
            <description><![CDATA[Elasticsearch에서 엔터티 해석을 위해 시맨틱 검색과 투명한 LLM 판단 사용]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/elasticsearch-entity-resolution-llm-semantic-search</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/elasticsearch-entity-resolution-llm-semantic-search</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Jessica Moszkowicz]]></dc:creator>
            <pubDate>Thu, 26 Feb 2026 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/473ca4357c7d60f690edbd2a844acda169aca9c3-720x420.jpg?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p><a href="https://www.elastic.co/search-labs/blog/entity-resolution-llm-elasticsearch">1부</a>에서는 감시 목록을 준비하고 엔터티 언급을 추출했습니다. 이제 &quot;언급이 실제로 가리키는 엔터티는 무엇인가?&quot;라는 어려운 질문에 답할 준비가 되었습니다. 이 시리즈의 첫 번째 블로그에서 엔터티 해석이 필요한 이유를 설명한 예시, &quot;Swift 업데이트 도착!&quot;으로 돌아가 봅시다. 이 헤드라인이 조금 더 많은 맥락과 함께 제공된다고 생각해 보세요.</p><ol><li>Swift 업데이트 도착! 개발자들은 새로운 기능을 사용해 보고 싶어 합니다.</li><li>Swift 업데이트 도착! 새 앨범이 다음 달에 발매될 예정입니다.</li></ol><p>이 추가 맥락을 통해 &quot;Swift&quot;라는 이름을 올바른 엔터티로 해석할 수 있습니다.</p><p>이전 게시물에서는 <a href="https://www.elastic.co/search-labs/blog/entity-resolution-llm-elasticsearch">감시 목록</a>을 설정하고 엔터티를 추가 맥락으로 보강했습니다. 위의 예시를 보면, 목록에 최소한 다음 두 개의 엔터티, &quot;Taylor Swift&quot;와 &quot;Swift 프로그래밍 언어&quot;가 있어야 합니다. 텍스트에서 엔터티 언급을 추출하는 방법도 다뤘습니다. 이 두 예시 모두 &quot;Swift&quot;를 추출합니다. 보강된 감시 목록과 추출된 엔터티라는 재료가 갖추어졌으니, 이제 주요 주제인 엔터티 매칭을 다룰 준비가 되었습니다.</p><p><strong>유의 사항:</strong> 이것은 엔터티 매칭 개념을 교육하기 위해 설계된 교육용 프로토타입입니다. 프로덕션 시스템에서는 다양한 대규모 언어 모델(LLM), 사용자 지정 매칭 규칙, 특수 판단 파이프라인 또는 여러 매칭 전략을 결합한 앙상블 접근 방식을 사용할 수 있습니다.</p><h2>문제: 매칭이 어려운 이유</h2><p>인간의 언어는 놀라운 것입니다. 가장 흥미로운 속성 중 하나는 끝없는 창의성입니다. 우리는 무한하게 많은 수의 새로운 문장을 생성하고 이해할 수 있습니다. 그렇다면 엔터티 해석에서 정확한 일치가 드문 것이 과연 놀라운 일일까요? 작성자들은 가능한 한 창의적으로 되려고 노력합니다. 엔터티가 언급될 때마다 전체 이름을 쓰고 읽어야 한다면 매우 지루할 것입니다. 정확한 일치는 간편하지만, 실제로는 엔터티 해석을 위해 더 정교한 접근 방식이 필요합니다. 인간 작성자의 무한한 창의성을 최소한 어느 정도는 처리할 수 있을 만큼 강력한 접근 방식이 필요합니다. 그래서 우리는 문제를 두 단계로 분리합니다. Elasticsearch를 사용하여 가능성 있는 후보를 대규모로 검색하고, 그런 다음 LLM을 사용하여 이러한 후보가 실제로 동일한 실제 엔터티를 참조하는지 판단하는 것입니다.</p><h2>해결책: 투명한 LLM 판단을 사용한 3단계 매칭</h2><p>우리는 컴퓨터 사용 방식에서 패러다임 전환의 중심에 있습니다. 인터넷의 등장으로 로컬 컴퓨팅에서 글로벌 연결 네트워크로 전환된 것처럼, 생성형 AI는 콘텐츠, 코드 및 정보 생성 방식을 근본적으로 변화시키고 있습니다. 실제로, 이 시리즈에 포함된 교육용 프로토타입에서는 작성자의 세심한 프롬프트에 따라 거의 전적으로 LLM을 통한 &quot;바이브 코딩&quot; 방식을 사용했습니다. LLM이 인간 언어에 내재한 생산성에 도달했거나 도달할 것이라는 의미는 아니지만, 이제 엔터티 해석에 도움이 되는 강력한 리소스를 갖게 되었다는 것은 명백합니다.</p><p>생성형 AI에서 흔히 사용하는 패턴은 Retrieval-Augmented Generation(RAG)입니다. 여기서 <em>검색(Retrieval)</em>은 엔터티 후보를 검색하는 것(답변 생성이 아님)을 의미하며, LLM은 매칭 평가와 설명에만 엄격히 사용됩니다. LLM에 엔드 투 엔드 엔터티 해석을 <em>요청할 수도 있지만</em>, 시간과 비용 면에서 비용이 많이 듭니다. RAG는 LLM에 더 효율적으로 맥락을 제공하는 방식을 사용함으로써 LLM이 엔터티 해석을 효율적으로 지원하도록 돕습니다.</p><p>RAG의 검색 부분에서는 다시 Elasticsearch를 사용합니다. 먼저 정확한 매칭, 별칭 매칭, 그리고 키워드와 시맨틱 검색을 결합한 하이브리드 검색을 조합하여 잠재적 매칭을 찾습니다. 이 잠재적 매칭을 찾으면 LLM에 보내 판단을 받습니다. LLM은 최종 매칭 평가자 역할을 합니다. 또한 LLM은 그 추론을 설명하게 하는데, 이는 다른 엔터티 해석 시스템과의 중요한 차별점입니다. 이러한 설명이 없으면 엔터티 해석은 블랙박스와도 같습니다. 설명이 있어야 매칭이 타당한 이유를 알 수 있습니다.</p><h2>핵심 개념: 3단계 매칭, 하이브리드 검색, 투명한 LLM 판단</h2><p><strong>3단계 매칭이란?</strong> 이 프로젝트의 시작 시점에 우리는 시맨틱 검색이 시스템의 중요한 부분이 될 것이라고 가정했지만, 모든 매칭이 그러한 정교한 검색을 요구하지는 않습니다. 일치하는 항목을 효율적으로 찾기 위해서, 문제에 대해 점진적인 접근 방식을 취합니다. 먼저 키워드 검색을 사용하여 정확히 일치하는 항목을 확인합니다. 정확히 일치하는 항목을 찾으면 작업이 완료되고 다음 단계로 넘어갈 수 있습니다. 정확히 일치하는 항목이 없으면, 별칭 매칭으로 전환합니다. 프로토타입에서는 간편함을 위해 별칭 매칭에서도 키워드를 사용한 정확한 일치 방식을 사용합니다. 프로덕션 환경에서는 이 단계를 정규화, 음역 규칙, 퍼지 매칭 또는 큐레이션된 별칭 테이블로 확장할 수 있습니다. 처음 두 단계에서 잠재적인 일치 항목을 찾지 못하면, Elasticsearch의 하이브리드 검색과 상호 순위 결합(RRF)을 통해 시맨틱 검색을 사용할 차례입니다.</p><p><strong>하이브리드 검색이란?</strong> Elasticsearch에서는 시맨틱 검색을 사용하여 맥락을 고려한 의미 있는 일치 항목을 찾을 수 있습니다. Elasticsearch는 벡터 검색 및 하이브리드 검색에 널리 사용됩니다. 시맨틱 유사성은 의미 파악에 효과적이지만, 구조화된 필터링(예: 시간 범위, 위치 또는 식별자 기준)을 대체할 수 없으며 정확한 일치 항목이 있을 경우에는 불필요한 경우가 많습니다. Elasticsearch는 시맨틱 검색이 맞지 않는 작업에 뛰어난 어휘 검색으로 두각을 나타냈습니다. 두 접근법을 모두 활용하기 위해 단일 하이브리드 쿼리에서 어휘 검색과 시맨틱 검색을 사용합니다. 그런 다음 결과를 병합해서 RRF로 일치 가능성이 가장 높은 항목을 찾습니다. 프로토타입에서는 상위 두 결과가 LLM 판단을 위해 보낼 수 있는 잠재적 일치 항목이 됩니다.</p><p><strong>LLM 판단이 필요한 이유?</strong> LLM의 판단과 설명을 통해 시스템은 모호성과 맥락을 투명하게 처리할 수 있습니다. 이는 맥락에 따라 여러 개체를 지칭할 수도 있는 &quot;The President&quot; 같은 사례에 매우 중요할 뿐 아니라, 별명이나 문화적 변형 같은 항목도 시스템에서 잘 작동하도록 해줍니다. 마지막으로, 제재 목록에서 엔터티를 식별할 때와 같이 중대한 작업을 고려할 때 시스템을 신뢰하기 위해서는 일치 항목이 수락된 이유를 알아야 합니다. 결정적으로, LLM은 전체 말뭉치를 검색하는 것이 아니라 Elasticsearch가 반환한 작은 후보 집합만 평가합니다.</p><h2>실제 결과: LLM 추론을 사용한 매칭</h2><p>자연어 처리 작업에서 가장 큰 과제 중 하나는 기대 결과를 알려주는 문서, 즉 &quot;정답지&quot;를 만드는 것입니다. 이것 없이는 작업에서 시스템의 성능을 판단하기가 거의 불가능하지만, 이러한 문서를 작성하는 과정은 어려울 수 있습니다. 엔터티 해석 프로토타입 개발을 위해, 테스트에 사용할 데이터를 설정하는 데 다시 한번 생성형 AI의 도움을 받았습니다.</p><p>먼저 별명과 음역법 같은 여러 과제 유형을 정의한 후, 시스템에 점차 더 커지고 어려워지는 티어형 데이터 세트 모음을 만들도록 LLM에 요청했습니다. 데이터 세트 생성은 기대만큼 간단하지 않았습니다. LLM은 정답을 너무 쉽게 찾도록 만들어서 &quot;속임수&quot;를 쓰는 경향이 강했습니다. 예를 들어, 과제 유형 중 하나는 의미론적 맥락에 초점을 맞췄습니다. 이 유형에는 &quot;러시아 작가(Russian author)&quot;를 &quot;레프 톨스토이(Leo Tolstoy)&quot;로 해석하는 것과 같은 것들이 포함되었습니다. LLM은 &quot;러시아 작가(Russian author)&quot;를 &quot;레프 톨스토이(Leo Tolstoy)&quot;의 별칭으로 잘못 표기했으며, 이로 인해 일치 항목을 찾기 위한 하이브리드 검색의 필요성이 없어졌습니다.</p><p>이와 같은 문제를 해결하기 위해 여러 차례 리팩토링을 거친 후, 5개의 데이터 세트 티어를 사용하게 되었습니다. 티어 1~4는 점점 더 커지고 더 많은 유형의 과제를 포함했습니다. 티어 5는 모든 과제 유형 중에서 가장 까다로운 예제들로 구성된 &quot;궁극의 도전 과제&quot; 데이터 세트였습니다. 모든 테스트 데이터는 <a href="https://github.com/jesslm/entity-resolution-lab-public/tree/main/comprehensive_evaluation">종합 평가 디렉터리</a>에서 확인할 수 있습니다.</p><p>프롬프트 기반 엔터티 해석 접근 방식을 평가하기 위해 티어 4 데이터 세트에 집중했습니다. 중요한 점은 이 평가가 통제된 실험으로 수행되어 엔터티 매칭 품질에 집중할 수 있었다는 것입니다. 감시 목록 데이터는 맥락 정보로 사전 보강되었으며, 엔터티는 문서에서 미리 추출되었습니다. 이를 통해 추출 정확도보다는 매칭에 초점을 맞춰 평가할 수 있었습니다. 이를 통해 매칭 품질에 집중합니다. 엔드 투 엔드 성능은 추가적으로 추출 재현율 및 보강 품질에 따라 달라질 수 있습니다.</p><h3>평가 데이터 세트</h3><p>티어 4 평가 데이터 세트는 시스템의 기능을 종합적으로 테스트합니다.[1]</p><ul><li><strong>감시 목록 엔터티:</strong> 다양한 유형의 엔터티 66개(사람, 조직, 위치).</li><li><strong>테스트 문서:</strong> 실제 엔터티 해석 시나리오를 다룬 문서 69개.</li><li><strong>예상 일치 항목:</strong> 모든 문서에서 예상 엔터티 일치 항목 206개.</li><li><strong>과제 유형: </strong>엔터티 해석의 다양한 측면을 테스트하는 과제 유형 15가지.</li></ul><p>데이터 세트에 포함된 과제 유형은 다음과 같습니다.</p><ul><li><strong>별명:</strong> &quot;Bob Smith&quot; → &quot;Robert Smith&quot;(7개 문서).</li><li><strong>직함 및 경칭:</strong> &quot;Dr. Sarah Williams&quot; → &quot;Sarah Williams&quot;(5개 문서).</li><li><strong>의미론적 맥락:</strong> &quot;Russian author&quot; → &quot;Leo Tolstoy&quot;(8개 문서).</li><li><strong>다국어 이름:</strong> 여러 스크립트로 이름 처리(6개 문서).</li><li><strong>비즈니스 엔터티:</strong> 법인명 변형(7개 문서).</li><li><strong>임원 참조: </strong>&quot;Microsoft CEO&quot; → &quot;Satya Nadella&quot;(5개 문서).</li><li><strong>정치 지도자:</strong> 직함 기반 참조(5개 문서).</li><li><strong>이니셜:</strong> &quot;J. Smith&quot; → &quot;John Smith&quot;(3개 문서).</li><li><strong>이름 순서 변형:</strong> 다양한 이름 순서 규칙(3개 문서).</li><li><strong>잘린 이름:</strong> 부분 이름 일치(3개 문서).</li><li><strong>이름 분할:</strong> 텍스트에 걸쳐 이름 분할(3개 문서).</li><li><strong>누락된 공백/하이픈:</strong> 서식 변형(2개 문서).</li><li><strong>음역:</strong> 교차 스크립트 이름 매칭(2개 문서).</li><li><strong>결합된 과제:</strong> 하나의 문서에 여러 과제(6개 문서).</li><li><strong>복잡한 비즈니스:</strong> 계층적 비즈니스 관계(5개 문서).</li></ul><p>프롬프트 기반 엔터티 해석이 어떻게 수행되었는지 살펴보겠습니다.</p><h3>전반적인 성능</h3><p>결과는 LLM 기반 매치 평가에 많은 가능성이 있음을 보여주지만, 동시에 심각한 신뢰성 문제도 드러났습니다. 각 후보 쌍은 LLM에 의해 평가되어야 하므로, 검색이 잘 작동하더라도 구조화된 출력에 오류가 있으면 허용률과 재현율을 억제할 수 있습니다.</p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">메트릭</td><td style="border: 1px solid #ccc; padding: 4px;">값</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">정밀도</td><td style="border: 1px solid #ccc; padding: 4px;">83.8%</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">재현율</td><td style="border: 1px solid #ccc; padding: 4px;">62.6%</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">F1 점수</td><td style="border: 1px solid #ccc; padding: 4px;">71.7%</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">검색된 총 일치 항목 수</td><td style="border: 1px solid #ccc; padding: 4px;">344</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">LLM 합격률</td><td style="border: 1px solid #ccc; padding: 4px;">44.8%</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">오류율</td><td style="border: 1px solid #ccc; padding: 4px;">30.2%</td>
                    </tr>
              </tbody>
            </table>
          <h3>오류율 문제</h3><p>앞에서 설명한 대로, 프로토타입에서 가장 먼저 한 일은 Elasticsearch를 사용하여 잠재적인 일치 쌍을 생성하는 것입니다. 이러한 잠재적 일치 항목 각각은 LLM에 의해 평가되어야 합니다. 모든 일치 항목을 효율적으로 처리하기 위해 LLM 호출을 배치 처리합니다. 이렇게 하면 API 비용과 지연 시간이 줄어들지만, 출력에 잘못된 형식의 JSON이 포함될 위험도 높아집니다. 배치 크기가 증가함에 따라 JSON은 더 길고 복잡해지므로 LLM이 유효하지 않은 JSON을 생성할 가능성이 높아집니다. 여기에서 30% 오류율이 발생합니다. 평가에서는 요청당 5개의 일치 항목을 배치 크기로 사용했습니다. 이처럼 보수적인 배치 크기를 사용했음에도 불구하고 여전히 JSON 구문 분석 오류가 발생하여 평가 결과가 크게 왜곡됩니다.</p><h2>다음 단계: LLM 통합 최적화</h2><p>이제 시맨틱 검색과 LLM 판단을 사용하여 엔터티를 매칭했으므로 완전한 엔터티 해석 파이프라인을 갖추게 되었습니다. 하지만 이 접근 방식은 모델의 판단은 정확하지만 출력 결과가 사용 불가능할 때 새로운 오류 모드를 불러옵니다. 안정성과 비용 효율성을 높이기 위해 LLM 통합을 최적화할 수 있습니다. 다음 게시물에서는 구조화된 출력을 위해 함수 호출을 사용하는 방법을 탐구할 것입니다. 이는 구조와 유형 안전성을 보장하면서 오류와 비용을 줄여줍니다.</p><h2>직접 사용해 보기</h2><p>엔터티 매칭을 직접 확인해 보고 싶으신가요? 실제 구현, 자세한 설명 및 실제 예제가 포함된 <a href="https://github.com/jesslm/entity-resolution-lab-public/tree/main/notebooks#:~:text=5%20minutes%20ago-,03_entity_matching_v3.ipynb,-Initial%20public%20lab">엔터티 매칭 노트북</a>을 확인해 보세요. 이 노트북은 3단계 검색, RRF를 사용한 하이브리드 검색, 그리고 추론을 포함한 LLM 기반 판단을 통해 엔터티를 매칭하는 방법을 정확하게 보여줍니다.</p><p><strong>유의 사항:</strong> 이것은 개념을 교육하기 위해 설계된 교육용 프로토타입입니다. 프로덕션 시스템을 구축할 때는 이 학습 중심 프로토타입에서 다루지 않는 모델 선택, 비용 최적화, 지연 시간 요구 사항, 품질 검증, 오류 처리 및 모니터링과 같은 추가 요소를 고려하세요.</p><h2>참고</h2><ol><li>이 데이터 세트는 교육용으로 설계된 합성 데이터 세트입니다. 실제 문제를 어느 정도 반영하지만 특정 프로덕션 환경을 대표하는 것은 아닙니다.</li></ol>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[OpenSearch보다 최대 8배 빠른 Elasticsearch 벡터 검색]]></title>
            <description><![CDATA[OpenSearch와 Elasticsearch의 필터링된 벡터 검색 벤치마크를 비교하고, 컨텍스트 엔지니어링 시스템에서 벡터 검색 성능이 중요한 이유를 살펴봅니다.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/opensearch-vs-elasticsearch-filtered-vector-search</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/opensearch-vs-elasticsearch-filtered-vector-search</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Sachin Frayne]]></dc:creator>
            <pubDate>Wed, 25 Feb 2026 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/fb7ee623925ca6696d643e437ce8efe5fe749079-1280x720.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<h2>AI 에이전트와 컨텍스트 엔지니어링에서 검색 속도가 중요한 이유</h2><p>문서 코퍼스 2,000만 개에 대한 벤치마크에서 Elasticsearch는 필터링된 벡터 검색에서 OpenSearch 대비 최대 8배 높은 처리량을 제공하는 동시에, 테스트한 모든 구성에서 더 높은 Recall@100을 달성했습니다. 컨텍스트 엔지니어링에는 빠른 벡터 검색 이상이 필요합니다. 워크플로우가 반복되면서 하이브리드 검색 및 필터링과 같은 강력한 관련성 제어, 운영 간소화, 예측 가능한 성능이 필요합니다. 그러나 에이전트가 요청당 검색, 추론, 검색 루프를 여러 번 실행하는 경우가 많기 때문에 검색 지연 시간이 배가되므로, 이 문제를 개선하면 엔드투엔드 응답성 향상과 비용 절감 효과를 누릴 수 있습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/d5a52a07773f0942c2baa732dacfe782aac0f415-1600x683.png" style="width: 100%;" /><p>컨텍스트 엔지니어링에서 검색은 일회성 단계가 아닙니다. 에이전트와 애플리케이션은 검색 → 추론 → 검색과 같은 루프를 반복적으로 실행해 쿼리를 정제하고, 사실을 검증하며, 근거 있는 컨텍스트를 조립하고, 작업을 완료합니다. 이 패턴은 에이전트 워크플로우와 반복 Retrieval-Augmented Generation(RAG)에서 흔히 볼 수 있습니다. 사용자 요청당 검색이 여러 번 호출될 수 있으므로 응답 시간이 지연되거나 인프라 비용이 증가할 수 있습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/54ac476ff20a3cf93484298c9ae47612c12fc110-800x417.png" style="width: 100%;" /><h2>벡터 검색 성능이 중요한 이유</h2><p></p><p>쇼핑 어시스턴트가 &#x27;15인치 노트북이 들어가고, 방수 기능이 있으며, 금요일까지 배송 가능한 6만원 이하의 기내용 백팩을 찾는다&#x27;는 질문에 답하는 경우를 생각해 보겠습니다.</p><p>실제 환경에서 어시스턴트가 벡터 쿼리를 한 번만 실행하고 멈추는 경우가 거의 없습니다. 올바른 컨텍스트를 구축하기 위해 검색 루프를 실행하는데, 각 단계는 일반적으로 가용성, 지역, 배송 예정일, 브랜드 규칙 및 정책 적합성과 같은 필터에 의해 제한을 받습니다.</p><p><strong>1단계: 의도를 해석해 제약 조건으로 변환</strong></p><p>에이전트는 요청을 다음과 같은 구조화된 필터와 의미론적 쿼리로 변환합니다.</p><ul><li>필터: 재고 있음, 사용자의 우편번호로 배송 가능, 금요일까지 배송, 가격 6만원 미만, 유효한 상품 목록</li><li>벡터 쿼리: &#x27;기내 반입 가능한 백팩, 15인치 노트북, 방수&#x27;</li></ul><p><strong>2단계: 후보를 검색한 후 정제</strong></p><p>우수한 일치 항목을 놓치지 않기 위해 종종 변형된 검색을 반복합니다.</p><ul><li>&#x27;노트북 슬리브가 포함된 기내용 백팩&#x27;</li><li>&#x27;통근용 방수 백팩 15인치&#x27;</li><li>&#x27;기내용 경량 백팩&#x27;</li></ul><p>관련성이 없거나 사용할 수 없는 항목을 검색하는 것은 컨텍스트 낭비이므로 각 쿼리는 동일한 자격 필터를 사용합니다.</p><p><strong>3단계: 위험을 줄이기 위해 확장하여 세부 정보 확인</strong></p><p>그런 다음 에이전트는 최종 답변에 영향을 미치는 주요 속성을 확인하기 위해 데이터를 다시 검색합니다.</p><ul><li>재질 및 방수 기능 관련 문구</li><li>치수 및 노트북 수납 공간 크기</li><li>반품 정책 또는 보증 제약 조건</li><li>재고가 부족한 경우 대체 옵션</li></ul><p>이것이 검색, 추론, 검색, 조립을 거치는 다단계 컨텍스트 엔지니어링입니다.</p><h2>컨텍스트 엔지니어링에서 지연 시간과 재현율이 중요한 이유</h2><p>이러한 상호작용은 사용자 세션당 수십 건의 필터링된 검색 호출을 포함할 수 있습니다. 따라서 통화당 지연 시간이 엔드투엔드 응답 시간에 직접적인 영향을 미치며, 재현율이 낮으면 에이전트가 추가 재시도를 하거나 적격 항목을 놓치게 되어 응답 품질이 저하됩니다.</p><p>요점: 컨텍스트 엔지니어링 시스템에서 필터링된 근사 이웃(ANN)은 일회성 조회로 끝나지 않습니다. 이는 제약 조건 하에서 반복되는 작업이므로, 대형 언어 모델(LLM)이 가장 중요한 구성 요소인 경우에도 벡터 검색 성능이 지연 시간, 처리량, 비용에 즉각적으로 영향을 미칩니다.</p><h2>벤치마킹</h2><h3>결과</h3><p>그래프 2에서 각 점은 하나의 테스트 구성을 나타냅니다. 지연 시간이 짧고 재현율이 높은 최상의 결과는 왼쪽 상단에 표시됩니다. Elasticsearch의 결과가 OpenSearch보다 지속적으로 왼쪽 상단에 가깝게 위치한다는 점에서 동일한 워크로드 설정에서 더 나은 속도와 정확도를 확인할 수 있습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/c50d1df00968cac18149a2799e6242fbe49b66a0-1600x990.png" style="width: 100%;" /><h4>주요 인사이트</h4><ul><li><code>s_n_r_value</code>: <code>size_numCandidates_rescoreOversample</code> 의 약어(k 및 numCandidates는 이러한 테스트에서 numCandidates와 동일하게 설정됨), 예를 들어 <code>100_500_1</code> 은 size=100, numCandidates=500 및 k=500, rescore oversample=1을 의미합니다.</li><li>재현율: 해당 구성에 대한 측정된 Recall@100</li><li>평균 지연 시간(ms): 쿼리당 평균 엔드투엔드 지연 시간</li><li>처리량: 초당 쿼리 수</li><li>재현율 %: Elasticsearch와 OpenSearch의 상대적 재현율 상승(Elasticsearch-OpenSearch)/OpenSearch</li><li>지연 시간 Xs: OpenSearch의 평균 지연 시간을 Elasticsearch의 평균 지연 시간으로 나눈 값</li><li>처리량 Xs: Elasticsearch 처리량을 OpenSearch 처리량으로 나눈 값</li></ul>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">엔진</td><td style="border: 1px solid #ccc; padding: 4px;">`s_n_r_value`</td><td style="border: 1px solid #ccc; padding: 4px;">재현율</td><td style="border: 1px solid #ccc; padding: 4px;">평균 지연 시간(ms)</td><td style="border: 1px solid #ccc; padding: 4px;">처리량</td><td style="border: 1px solid #ccc; padding: 4px;">재현율 %</td><td style="border: 1px solid #ccc; padding: 4px;">지연 시간 Xs</td><td style="border: 1px solid #ccc; padding: 4px;">처리량 Xs</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch</td><td style="border: 1px solid #ccc; padding: 4px;">100_250_1</td><td style="border: 1px solid #ccc; padding: 4px;">0.7704</td><td style="border: 1px solid #ccc; padding: 4px;">25</td><td style="border: 1px solid #ccc; padding: 4px;">534.75</td><td style="border: 1px solid #ccc; padding: 4px;">9.70%</td><td style="border: 1px solid #ccc; padding: 4px;">2.28</td><td style="border: 1px solid #ccc; padding: 4px;">1.91</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch</td><td style="border: 1px solid #ccc; padding: 4px;">100_250_1</td><td style="border: 1px solid #ccc; padding: 4px;">0.7023</td><td style="border: 1px solid #ccc; padding: 4px;">57.08</td><td style="border: 1px solid #ccc; padding: 4px;">279.58</td><td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;"></td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch</td><td style="border: 1px solid #ccc; padding: 4px;">100_500_1</td><td style="border: 1px solid #ccc; padding: 4px;">0.8577</td><td style="border: 1px solid #ccc; padding: 4px;">25.42</td><td style="border: 1px solid #ccc; padding: 4px;">524.14</td><td style="border: 1px solid #ccc; padding: 4px;">7.20%</td><td style="border: 1px solid #ccc; padding: 4px;">2.4</td><td style="border: 1px solid #ccc; padding: 4px;">2</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch</td><td style="border: 1px solid #ccc; padding: 4px;">100_500_1</td><td style="border: 1px solid #ccc; padding: 4px;">0.8001</td><td style="border: 1px solid #ccc; padding: 4px;">60.9</td><td style="border: 1px solid #ccc; padding: 4px;">262.12</td><td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;"></td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch</td><td style="border: 1px solid #ccc; padding: 4px;">100_750_1</td><td style="border: 1px solid #ccc; padding: 4px;">0.8947</td><td style="border: 1px solid #ccc; padding: 4px;">29.67</td><td style="border: 1px solid #ccc; padding: 4px;">528.09</td><td style="border: 1px solid #ccc; padding: 4px;">5.72%</td><td style="border: 1px solid #ccc; padding: 4px;">2.25</td><td style="border: 1px solid #ccc; padding: 4px;">2.21</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch</td><td style="border: 1px solid #ccc; padding: 4px;">100_750_1</td><td style="border: 1px solid #ccc; padding: 4px;">0.8463</td><td style="border: 1px solid #ccc; padding: 4px;">66.76</td><td style="border: 1px solid #ccc; padding: 4px;">239.11</td><td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;"></td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch</td><td style="border: 1px solid #ccc; padding: 4px;">100_1000_1</td><td style="border: 1px solid #ccc; padding: 4px;">0.9156</td><td style="border: 1px solid #ccc; padding: 4px;">29.65</td><td style="border: 1px solid #ccc; padding: 4px;">534.5</td><td style="border: 1px solid #ccc; padding: 4px;">4.66%</td><td style="border: 1px solid #ccc; padding: 4px;">2.46</td><td style="border: 1px solid #ccc; padding: 4px;">2.44</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch</td><td style="border: 1px solid #ccc; padding: 4px;">100_1000_1</td><td style="border: 1px solid #ccc; padding: 4px;">0.8748</td><td style="border: 1px solid #ccc; padding: 4px;">72.88</td><td style="border: 1px solid #ccc; padding: 4px;">219.01</td><td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;"></td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch</td><td style="border: 1px solid #ccc; padding: 4px;">100_1500_1</td><td style="border: 1px solid #ccc; padding: 4px;">0.9386</td><td style="border: 1px solid #ccc; padding: 4px;">31.84</td><td style="border: 1px solid #ccc; padding: 4px;">497.3</td><td style="border: 1px solid #ccc; padding: 4px;">3.38%</td><td style="border: 1px solid #ccc; padding: 4px;">2.71</td><td style="border: 1px solid #ccc; padding: 4px;">2.68</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch</td><td style="border: 1px solid #ccc; padding: 4px;">100_1500_1</td><td style="border: 1px solid #ccc; padding: 4px;">0.9079</td><td style="border: 1px solid #ccc; padding: 4px;">86.16</td><td style="border: 1px solid #ccc; padding: 4px;">185.4</td><td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;"></td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch</td><td style="border: 1px solid #ccc; padding: 4px;">100_2000_1</td><td style="border: 1px solid #ccc; padding: 4px;">0.9507</td><td style="border: 1px solid #ccc; padding: 4px;">34.69</td><td style="border: 1px solid #ccc; padding: 4px;">457.2</td><td style="border: 1px solid #ccc; padding: 4px;">2.57%</td><td style="border: 1px solid #ccc; padding: 4px;">2.98</td><td style="border: 1px solid #ccc; padding: 4px;">2.96</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch</td><td style="border: 1px solid #ccc; padding: 4px;">100_2000_1</td><td style="border: 1px solid #ccc; padding: 4px;">0.9269</td><td style="border: 1px solid #ccc; padding: 4px;">103.36</td><td style="border: 1px solid #ccc; padding: 4px;">154.55</td><td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;"></td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch</td><td style="border: 1px solid #ccc; padding: 4px;">100_2500_1</td><td style="border: 1px solid #ccc; padding: 4px;">0.9582</td><td style="border: 1px solid #ccc; padding: 4px;">37.9</td><td style="border: 1px solid #ccc; padding: 4px;">418.43</td><td style="border: 1px solid #ccc; padding: 4px;">1.99%</td><td style="border: 1px solid #ccc; padding: 4px;">3.28</td><td style="border: 1px solid #ccc; padding: 4px;">3.26</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch</td><td style="border: 1px solid #ccc; padding: 4px;">100_2500_1</td><td style="border: 1px solid #ccc; padding: 4px;">0.9395</td><td style="border: 1px solid #ccc; padding: 4px;">124.29</td><td style="border: 1px solid #ccc; padding: 4px;">128.53</td><td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;"></td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch</td><td style="border: 1px solid #ccc; padding: 4px;">100_3000_1</td><td style="border: 1px solid #ccc; padding: 4px;">0.9636</td><td style="border: 1px solid #ccc; padding: 4px;">41.86</td><td style="border: 1px solid #ccc; padding: 4px;">379.4</td><td style="border: 1px solid #ccc; padding: 4px;">1.62%</td><td style="border: 1px solid #ccc; padding: 4px;">3.46</td><td style="border: 1px solid #ccc; padding: 4px;">3.44</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch</td><td style="border: 1px solid #ccc; padding: 4px;">100_3000_1</td><td style="border: 1px solid #ccc; padding: 4px;">0.9482</td><td style="border: 1px solid #ccc; padding: 4px;">144.67</td><td style="border: 1px solid #ccc; padding: 4px;">110.34</td><td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;"></td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch</td><td style="border: 1px solid #ccc; padding: 4px;">100_4000_1</td><td style="border: 1px solid #ccc; padding: 4px;">0.9705</td><td style="border: 1px solid #ccc; padding: 4px;">50.28</td><td style="border: 1px solid #ccc; padding: 4px;">316.21</td><td style="border: 1px solid #ccc; padding: 4px;">1.06%</td><td style="border: 1px solid #ccc; padding: 4px;">3.87</td><td style="border: 1px solid #ccc; padding: 4px;">3.85</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch</td><td style="border: 1px solid #ccc; padding: 4px;">100_4000_1</td><td style="border: 1px solid #ccc; padding: 4px;">0.9603</td><td style="border: 1px solid #ccc; padding: 4px;">194.36</td><td style="border: 1px solid #ccc; padding: 4px;">82.22</td><td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;"></td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch</td><td style="border: 1px solid #ccc; padding: 4px;">100_5000_1</td><td style="border: 1px solid #ccc; padding: 4px;">0.9749</td><td style="border: 1px solid #ccc; padding: 4px;">58.77</td><td style="border: 1px solid #ccc; padding: 4px;">270.91</td><td style="border: 1px solid #ccc; padding: 4px;">0.73%</td><td style="border: 1px solid #ccc; padding: 4px;">4.43</td><td style="border: 1px solid #ccc; padding: 4px;">4.41</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch</td><td style="border: 1px solid #ccc; padding: 4px;">100_5000_1</td><td style="border: 1px solid #ccc; padding: 4px;">0.9678</td><td style="border: 1px solid #ccc; padding: 4px;">260.33</td><td style="border: 1px solid #ccc; padding: 4px;">61.38</td><td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;"></td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch</td><td style="border: 1px solid #ccc; padding: 4px;">100_6000_1</td><td style="border: 1px solid #ccc; padding: 4px;">0.9781</td><td style="border: 1px solid #ccc; padding: 4px;">66.75</td><td style="border: 1px solid #ccc; padding: 4px;">238.59</td><td style="border: 1px solid #ccc; padding: 4px;">0.52%</td><td style="border: 1px solid #ccc; padding: 4px;">4.91</td><td style="border: 1px solid #ccc; padding: 4px;">4.89</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch</td><td style="border: 1px solid #ccc; padding: 4px;">100_6000_1</td><td style="border: 1px solid #ccc; padding: 4px;">0.973</td><td style="border: 1px solid #ccc; padding: 4px;">327.44</td><td style="border: 1px solid #ccc; padding: 4px;">48.81</td><td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;"></td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch</td><td style="border: 1px solid #ccc; padding: 4px;">100_7000_1</td><td style="border: 1px solid #ccc; padding: 4px;">0.9804</td><td style="border: 1px solid #ccc; padding: 4px;">74.64</td><td style="border: 1px solid #ccc; padding: 4px;">213.49</td><td style="border: 1px solid #ccc; padding: 4px;">0.38%</td><td style="border: 1px solid #ccc; padding: 4px;">5.28</td><td style="border: 1px solid #ccc; padding: 4px;">5.27</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch</td><td style="border: 1px solid #ccc; padding: 4px;">100_7000_1</td><td style="border: 1px solid #ccc; padding: 4px;">0.9767</td><td style="border: 1px solid #ccc; padding: 4px;">394.24</td><td style="border: 1px solid #ccc; padding: 4px;">40.53</td><td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;"></td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch</td><td style="border: 1px solid #ccc; padding: 4px;">100_8000_1</td><td style="border: 1px solid #ccc; padding: 4px;">0.9823</td><td style="border: 1px solid #ccc; padding: 4px;">82.28</td><td style="border: 1px solid #ccc; padding: 4px;">193.59</td><td style="border: 1px solid #ccc; padding: 4px;">0.27%</td><td style="border: 1px solid #ccc; padding: 4px;">6.86</td><td style="border: 1px solid #ccc; padding: 4px;">6.83</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch</td><td style="border: 1px solid #ccc; padding: 4px;">100_8000_1</td><td style="border: 1px solid #ccc; padding: 4px;">0.9797</td><td style="border: 1px solid #ccc; padding: 4px;">564.14</td><td style="border: 1px solid #ccc; padding: 4px;">28.33</td><td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;"></td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch</td><td style="border: 1px solid #ccc; padding: 4px;">100_9000_1</td><td style="border: 1px solid #ccc; padding: 4px;">0.9837</td><td style="border: 1px solid #ccc; padding: 4px;">90.08</td><td style="border: 1px solid #ccc; padding: 4px;">176.96</td><td style="border: 1px solid #ccc; padding: 4px;">0.16%</td><td style="border: 1px solid #ccc; padding: 4px;">7.63</td><td style="border: 1px solid #ccc; padding: 4px;">7.61</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch</td><td style="border: 1px solid #ccc; padding: 4px;">100_9000_1</td><td style="border: 1px solid #ccc; padding: 4px;">0.9821</td><td style="border: 1px solid #ccc; padding: 4px;">687.25</td><td style="border: 1px solid #ccc; padding: 4px;">23.25</td><td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;"></td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch</td><td style="border: 1px solid #ccc; padding: 4px;">100_10000_1</td><td style="border: 1px solid #ccc; padding: 4px;">0.9848</td><td style="border: 1px solid #ccc; padding: 4px;">97.64</td><td style="border: 1px solid #ccc; padding: 4px;">163.31</td><td style="border: 1px solid #ccc; padding: 4px;">0.08%</td><td style="border: 1px solid #ccc; padding: 4px;">8.38</td><td style="border: 1px solid #ccc; padding: 4px;">8.36</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch</td><td style="border: 1px solid #ccc; padding: 4px;">100_10000_1</td><td style="border: 1px solid #ccc; padding: 4px;">0.984</td><td style="border: 1px solid #ccc; padding: 4px;">818.64</td><td style="border: 1px solid #ccc; padding: 4px;">19.53</td><td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;"></td>
                    </tr>
              </tbody>
            </table>
          <p>예를 들어, <code>100_9000_1</code>에서 OpenSearch는 검색당 평균 687밀리초를 기록하는 반면, Elasticsearch에서는 90밀리초이며, 10단계 검색 루프에서는 약 10x(687-90)=6초의 추가 대기 시간이 발생합니다. </p><p><a href="https://github.com/elastic/competitive-benchmarking-studies/tree/main/es-9.3-vs-os-3.5-vector-search/jingra/results/20260220">전체 결과</a> 보기.</p><h3>방법론</h3><p>Python을 사용하여 쿼리를 전송하고 응답 시간 및 기타 통계를 추적하기 위해 다음 쿼리를 각 엔진에 전송했습니다. 벡터 검색 엔진의 성능은 고려할 후보 수, 재채점의 적극성, 반환할 컨텍스트의 양 등 핵심 매개변수를 어떻게 조정하느냐에 따라 달라진다는 점을 유의하시기 바랍니다. 이러한 설정은 재현율(올바른 답변을 찾을 가능성)과 대기 시간(결과를 얻는 속도)에 직접적인 영향을 미칩니다.</p><p>벤치마크 테스트에서는 에이전틱 검색 루프에서 일반적으로 조정하는 것과 동일한 후보, 재채점, 결과 크기 설정을 사용하여 해당 워크로드에서 Elasticsearch의 성능을 측정했습니다. 그런 다음 참조를 위해 동일한 설정으로 OpenSearch를 실행했습니다.</p><p>OpenSearch</p><pre><code>GET <INDEX_NAME>/_search
{
  "query": {
    "knn": {
      "<DENSE_VECTOR_FIELD_NAME>": {
        "vector": [...],
        "k": <NUMBER_OF_CANDIDATES>,
        "method_parameters": {
          "ef_search": <NUMBER_OF_CANDIDATES>
        },
        "rescore": {
          "oversample_factor": <OVERSAMPLE>
        },
        "filter": {
          <SOME_FILTER>
        }
      }
    }
  },
  "size": <RESULT_SIZE>,
  "_source": {
    "excludes": [
      "<DENSE_VECTOR_FIELD_NAME>"
    ]
  }
}</code></pre><ul><li><code>&quot;size&quot;: &lt;RESULT_SIZE&gt;</code>: 클라이언트에 반환된 결과 수입니다. 이 벤치마크에서는 Recall@100을 계산하기 위해 결과 크기가 100입니다.</li><li><code>&quot;k&quot;: &lt;NUMBER_OF_CANDIDATES&gt;</code>: 최근접 이웃 후보의 수입니다.</li><li><code>&quot;ef_search&quot;: &lt;NUMBER_OF_CANDIDATES&gt;</code>: 검사할 벡터의 개수.</li><li><code>&quot;oversample_factor&quot;: &lt;OVERSAMPLE&gt;</code>: 재채점 전에 검색되는 후보 벡터의 수입니다.</li></ul><p>Elasticsearch</p><pre><code>GET <INDEX_NAME>/_search
{
  "query": {
    "knn": {
      "field": "<DENSE_VECTOR_FIELD_NAME>",
      "query_vector": [...],
      "k": <NUMBER_OF_CANDIDATES>,
      "num_candidates": <NUMBER_OF_CANDIDATES>,
      "rescore_vector": {
        "oversample": <OVERSAMPLE>
      },
      "filter": {
        <SOME_FILTER>
      }
    }
  },
  "size": <RESULT_SIZE>,
  "_source": {
    "excludes": [
      "<DENSE_VECTOR_FIELD_NAME>"
    ]
  }
}</code></pre><ul><li><code>&quot;size&quot;: &lt;RESULT_SIZE&gt;</code>: 클라이언트에 반환된 결과 수입니다. 이 벤치마크에서는 Recall@100을 계산하기 위해 결과 크기가 100입니다.</li><li><code>&quot;k&quot;: &lt;NUMBER_OF_CANDIDATES&gt;</code>각 샤드에서 반환할 가장 가까운 이웃의 수입니다.</li><li><code>&quot;num_candidates&quot;: &lt;NUMBER_OF_CANDIDATES&gt;</code>: <code>knn</code> 검색 수행 시 샤드별로 고려할 최근접 이웃 후보 수.</li><li><code>&quot;oversample&quot;: &lt;OVERSAMPLE&gt;</code>: 재채점 전에 검색되는 후보 벡터의 수입니다.</li></ul><p>예</p><p><code>Knn</code> 쿼리(<code>100_500_1</code>)는 다음과 같습니다.</p><p>OpenSearch</p><pre><code>GET search_catalog_128/_search
{
  "query": {
    "knn": {
      "search_catalog_embedding": {
        "vector": [...],
        "k": 500,
        "method_parameters": {
          "ef_search": 500
        },
        "rescore": {
          "oversample_factor": 1
        },
        "filter": {
          "term": {
            "valid": true
          }
        }
      }
    }
  },
  "size": 100,
  "_source": {
    "excludes": [
      "search_catalog_embedding"
    ]
  }
}</code></pre><p>Elasticsearch</p><pre><code>GET search_catalog_128/_search
{
  "query": {
    "knn": {
      "field": "search_catalog_embedding",
      "query_vector": [...],
      "k": 500,
      "num_candidates": 500,
      "rescore_vector": {
        "oversample": 1
      },
      "filter": {
        "term": {
          "valid": true
        }
      }
    }
  },
  "size": 100,
  "_source": {
    "excludes": [
      "search_catalog_embedding"
    ]
  }
}</code></pre><p>전체 구성, Terraform 스크립트, Kubernetes 매니페스트 및 벤치마킹 코드는 이 <a href="https://github.com/elastic/competitive-benchmarking-studies">저장소</a>의 <a href="https://github.com/elastic/competitive-benchmarking-studies/tree/main/es-9.3-vs-os-3.5-vector-search">es-9.3-vs-os-3.5-vector-search</a> 폴더에서 확인할 수 있습니다.</p><h3>클러스터 설정</h3><p>테스트는 각각 16개의 vCPU와 64GB RAM을 갖춘 6개의 e2-standard-16 클라우드 서버에서 실행되었습니다. 각 서버에서 검색 엔진 노드를 실행하는 각 Kubernetes 포드에 15개의 vCPU와 56GB RAM을 할당하였으며, 그 중 28GB는 JVM 힙을 위해 예약했습니다.</p><p>클러스터는 Elasticsearch 9.3.0과 OpenSearch 3.5.0 (Lucene 10.3.2)을 실행합니다. 이 벤치마크에서 두 시스템 모두 동일한 Lucene 버전을 사용하기 때문에, 관찰된 처리량과 지연 시간 차이는 Lucene에만 기인하는 것이 아니라 각 엔진이 필터링된 kNN(k-최근 이웃) 검색 및 재채점을 통합하고 실행하는 방식의 차이를 반영합니다. 3개의 기본 샤드와 1개의 복제본이 있는 단일 인덱스를 사용했습니다(따라서 노드당 1개씩 총 6개의 샤드).</p><p>또한 같은 지역 내 별도의 서버를 사용해 벤치마크 클라이언트를 실행하고 타이밍 통계를 수집했습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/f81002c9186e4c2d3e92f49d72418fee9860fc5e-761x401.png" style="width: 100%;" /><h3>데이터 세트</h3><p></p><p>이 벤치마크에서는 대규모 실제 환경에서의 필터링된 벡터 검색을 반영하도록 설계된 대규모 이커머스 스타일의 카탈로그 임베딩 데이터 세트(2,000만 개 문서)을 사용했습니다.</p><p></p><p>각 문서는 카탈로그 항목을 나타내며 다음을 포함합니다.</p><p></p><ul><li>128차원 고밀도 벡터 임베딩으로, 대략적인 kNN 검색에 사용됩니다.</li><li>필터링에 사용되는 구조화된 메타데이터 필드(예: 항목 유효성 및 가용성, 기타 카탈로그 제약 조건)는 적합한 하위 집합 내에서만 가장 가까운 이웃을 검색하는 일반적인 제작 패턴을 지원합니다.</li></ul><p></p><p>이 데이터 세트를 선택한 이유는 선택한 이유는 프로덕션 환경의 에이전틱 및 RAG 스타일 시스템에서 나타나는 핵심 성능 과제를 잘 반영하기 때문입니다. 벡터 유사도만으로는 충분하지 않고, 검색은 필터에 의해 제한을 받는 경우가 많고, 시스템은 해당 제약 조건 하에서 지연 시간을 낮게 유지하면서 높은 재현율을 달성해야 합니다. 소규모 QA 스타일 데이터 세트와 비교했을 때, 2,000만 개 문서 코퍼스는 실제 환경에서 필터링된 ANN 시스템이 직면하는 규모와 후보 압력을 더 잘 반영합니다.</p><h2>결론</h2><p>현대 AI 아키텍처, 특히 컨텍스트 엔지니어링을 중심으로 구축된 아키텍처에서 벡터 검색 속도는 사소한 구현 세부 사항으로 치부할 수 없습니다. 이는 효과를 배가시킵니다. 에이전트와 워크플로가 검색 → 추론 → 검색 과정을 반복할 때, 검색 성능은 엔드투엔드 지연 시간, 처리량, 모델에 입력되는 컨텍스트 품질에 직접적인 영향을 미칩니다.</p><p>벤치마크 테스트 결과, Elasticsearch는 정확한 문서 검색 여부가 중요한 시나리오(단순히 유사한 벡터를 검색하는 것이 아닌)에서 OpenSearch보다 지속적으로 더 높은 재현율과 더 낮은 지연 시간을 제공했습니다. 제어된 데이터 세트에서 차이는 분명하며, 실제 환경에서는 대규모 검색 호출 전반에 걸쳐 이러한 이점이 누적되어 응답성을 향상시키고, 용량 여유를 확대하며, 인프라 비용을 절감합니다.</p><h3>추가 읽기</h3><ol><li><a href="https://www.elastic.co/search-labs/blog/context-engineering-overview">컨텍스트 엔지니어링이란 무엇인가요?</a></li><li><a href="https://www.elastic.co/search-labs/blog/series/context-engineering-hybrid-search-evolution">하이브리드 검색 및 컨텍스트 엔지니어링의 발전</a></li><li><a href="https://www.elastic.co/search-labs/blog/context-engineering-relevance-ai-agents-elasticsearch">AI 에이전트를 위한 컨텍스트 엔지니어링에서 관련성의 영향</a></li></ol>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[jina-embeddings-v5-text: 검색 및 지능형 애플리케이션을 위한 컴팩트한 최신 텍스트 임베딩]]></title>
            <description><![CDATA[jina-embeddings-v5-text 모델을 소개하고, jina-embeddings-v5-text-small 및 jina-embeddings-v5-text-nano를 비롯한 다국어 임베딩 모델을 Elastic Inference Service(EIS)를 통해 사용하는 방법을 설명합니다.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/jina-embeddings-v5-text</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/jina-embeddings-v5-text</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Scott Martens, Sofia Vasileva]]></dc:creator>
            <pubDate>Mon, 23 Feb 2026 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/e98dd30ab925b4bb32830228d71a1a51d02a0917-1600x840.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>Jina AI와 Elastic은 <code>jina-embeddings-v5-text</code>라는 새로운 고성능 간결한 텍스트 임베딩 모델 계열을 출시합니다. 이 모델은 모든 주요 작업 유형에서 비슷한 크기의 모델에 최첨단 성능을 자랑합니다.</p><p>이 제품군에는 두 가지 모델이 포함되어 있습니다:</p><ul><li><code>jina-embeddings-v5-text-small</code></li><li><code>jina-embeddings-v5-text-nano</code></li></ul><p>이러한 모델은 임베딩 모델을 위한 혁신적인 새로운 훈련 방식의 성공적인 결과입니다. 두 모델 모두 크기가 몇 배나 큰 모델보다 성능이 뛰어나 메모리와 컴퓨팅 리소스를 절약하고 요청에 더 빠르게 응답합니다.</p><p><code>jina-embeddings-v5-text-small</code> 모델은 6억 7,700만개의 매개변수를 가지고 있으며, 32,768개의 토큰 입력 컨텍스트 창을 지원하고 기본적으로 1,024차원 임베딩을 생성합니다.</p><p><code>jina-embeddings-v5-text-nano</code> 무게는 형제 제품의 크기의 대략 3분의 1로, 2억 3,900만 개의 매개변수와 8,192개의 토큰의 입력 컨텍스트 창을 가지며, 768차원의 간결한 임베딩을 제공합니다.</p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">모델 이름</td><td style="border: 1px solid #ccc; padding: 4px;">전체 크기</td><td style="border: 1px solid #ccc; padding: 4px;">입력 컨텍스트 창 크기</td><td style="border: 1px solid #ccc; padding: 4px;">임베딩 크기</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">jina-v5-text-small</td><td style="border: 1px solid #ccc; padding: 4px;">677M 매개변수</td><td style="border: 1px solid #ccc; padding: 4px;">32,768개 토큰</td><td style="border: 1px solid #ccc; padding: 4px;">1,024차원</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">jina-v5-text-nano</td><td style="border: 1px solid #ccc; padding: 4px;">239M 매개변수</td><td style="border: 1px solid #ccc; padding: 4px;">8192개의 토큰</td><td style="border: 1px solid #ccc; padding: 4px;">768차원</td>
                    </tr>
              </tbody>
            </table>
          <p>이 두 모델은 전체 MMTEB<a href="https://huggingface.co/spaces/mteb/leaderboard">(다국어 MTEB)</a> 벤치마크 성능에서 동급 최고 수준입니다. 5억 개 미만의 매개변수를 가진 모델 중, <code>jina-embeddings-v5-text-nano</code>는 2억 5,000만 개 미만의 매개변수를 가지고 있음에도 불구하고 최고 성능을 보이며, <code>jina-embeddings-v5-text-small</code> 모델은 7억 5,000만 개 미만의 매개변수를 가진 다국어 임베딩 모델 중 선두에 있습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/d97126285fa196c3045b1913b7754e4b186c1e4c-1300x1100.png" style="width: 100%;" /><p>이러한 모델은 Elastic Inference Service(EIS), 온라인 API를 통해 사용할 수 있으며 로컬 호스팅도 가능합니다. <code>jina-embeddings-v5-text</code> 모델에 액세스하는 방법에 대한 지침은 아래의 &#x27;<strong>시작하기</strong>&#x27; 섹션을 참조하세요.</p><p>임베딩 모델과 의미 색인화는 검색 알고리즘의 정확도를 획기적으로 향상시킬 뿐만 아니라 의미 유사성 및 의미 추출과 관련된 다양한 작업에도 활용될 수 있습니다. 예를 들면 다음과 같습니다.</p><ul><li>중복된 텍스트 찾기.</li><li>의역 및 번역 인식하기.</li><li>주제 발견.</li><li>추천 엔진.</li><li>감정 및 의도 분석.</li><li>스팸 필터링.</li><li>그 밖에도 여러 가지가 있습니다.</li></ul><h2><strong>기능</strong></h2><p>이 새로운 모델 제품군은 관련성을 높이고 비용을 절감하도록 설계된 여러 기능을 갖추고 있습니다.</p><h3>작업 최적화</h3><p><code>jina-embeddings-v5-text</code> 모델을 네 가지 작업 유형에 맞게 최적화했습니다.</p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">작업</td><td style="border: 1px solid #ccc; padding: 4px;">예시 사용 사례</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">검색</td><td style="border: 1px solid #ccc; padding: 4px;">자연어 쿼리를 사용하여 문서 모음에서 가장 관련성이 높은 결과 검색하기.</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">텍스트 일치</td><td style="border: 1px solid #ccc; padding: 4px;">의미적 유사성, 중복 제거, 의미 변환 및 번역 정렬 등</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">클러스터링</td><td style="border: 1px solid #ccc; padding: 4px;">주제 검색, 문서 컬렉션의 자동 정리.</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">분류</td><td style="border: 1px solid #ccc; padding: 4px;">문서 분류, 감정 및 의도 감지, 유사 작업.</td>
                    </tr>
              </tbody>
            </table>
          <p>한 작업에 최적화하는 것은 일반적으로 다른 작업에서 타협을 의미하므로 대부분의 임베딩 모델은 한 종류의 작업에 대해서만 경쟁력 있는 성능을 가집니다. 하지만 <code>jina-embeddings-v5-text</code> 모델은 작업별 <a href="https://arxiv.org/abs/2106.09685">로우랭크 적응(LoRA) 어댑터</a>를 훈련하여 타협 없이 네 가지 영역 모두에 전문성을 갖출 수 있습니다.</p><p>LoRA 어댑터는 AI 모델의 동작을 극적으로 변화시키면서도 전체 크기는 약간만 늘리는 일종의 플러그인입니다. 각 작업마다 수억 개의 매개변수가 있는 전체 모델을 사용하는 대신 <code>jina-embeddings-v5-text</code> 모델 제품군을 사용하면 각 작업마다 컴팩트한 LoRA 어댑터가 포함된 하나의 모델만 사용할 수 있습니다. 이렇게 하면 메모리, 저장 공간, 추론 비용을 절약할 수 있습니다.</p><h3>임베딩 잘라내기</h3><p>최소한의 비용으로 임베딩의 품질을 유지하면서 더 작은 크기로 줄일 수 있는 <a href="https://arxiv.org/abs/2205.13147">Matryoshka 표현 학습</a>을 사용하여 <code>jina-embeddings-v5-text</code> 모델을 훈련시켰습니다.</p><p>기본적으로 <code>jina-embeddings-v5-text-small</code>은 1024차원 임베딩 벡터를 생성하며, 각 벡터는 16비트 숫자로 표현되어 각 임베딩 크기가 2KB입니다. 대규모 문서 컬렉션의 경우 저장해야 할 데이터가 많을 수 있으며, 임베딩으로 가득 찬 벡터 데이터베이스에서 검색하는 것은 데이터베이스의 크기와 저장된 각 벡터의 차원 수 모두에 비례합니다.</p><p>하지만 임베딩의 크기를 절반으로 줄이면(1024차원 중 512차원을 버리면) 공간을 절반으로 줄이면서 검색 속도를 두 배로 높일 수 있습니다. 이는 성능에 영향을 미칩니다. 정보를 버리면 정밀도가 떨어집니다. 그러나 아래의 그래프가 보여주듯이, 임베딩의 절반을 제거하더라도 성능은 약간만 저하됩니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/f11969682c81bcf22b5b29a6204ba6ec40471b3d-900x700.png" style="width: 100%;" /><p>임베딩이 최소 256차원을 가지는 한, 정밀도 손실은 상당히 작게 유지되어야 합니다. 그러나 이 수준 이하에서는 관련성과 정확도가 빠르게 저하됩니다.</p><p>이처럼 임베딩을 잘라내는 방식을 통해 사용자는 정확도와 컴퓨팅 비용 간의 균형을 스스로 관리할 수 있습니다. 이 솔루션은 검색 AI를 통해 효율성을 크게 향상시키고 비용을 대폭 절감할 수 있는 도구를 제공합니다.</p><h3>강력한 양자화</h3><p><em>양자화</em>는 임베딩의 크기를 줄이는 또 다른 방법입니다. 양자화는 각 임베딩의 일부를 버리는 대신 임베딩에 포함된 숫자의 정밀도를 낮춥니다. <code>jina-embeddings-v5-text</code> 모델은 16비트 숫자로 임베딩을 생성하지만, 이 숫자를 반올림하여 정밀도와 저장에 필요한 비트 수를 줄일 수 있습니다. 가장 극단적인 경우, 각 숫자를 1비트(0 또는 1)로 줄여 <code>jina-embeddings-v5-text</code>의 기본 1024차원 임베딩을 2킬로바이트에서 128바이트로 압축할 수 있으며, 이는 이진 양자화만으로 94% 줄어든 것입니다. 잘라내기와 마찬가지로 메모리 및 컴퓨팅 비용을 크게 절감할 수 있습니다. 하지만 잘라내기와 마찬가지로 양자화는 임베딩의 정확도를 떨어뜨립니다.</p><p>정확도 손실을 최소화하여 <a href="https://www.elastic.co/kr/elasticsearch">Elasticsearch</a>의 <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/bbq">더 나은 이진 양자화(BBQ)</a> 기능을 사용하도록 <code>jina-embeddings-v5-text</code> 모델을 훈련시켰으며, 이러한 모델의 이진화된 임베딩에 대한 벤치마크 테스트 결과 이진화되지 않은 임베딩과 거의 동일한 성능을 보여주었습니다. 이진화 성능에 대한 자세한 제거 연구는 <a href="https://arxiv.org/abs/2602.15547">기술 보고서</a>를 참조하세요.</p><h3>다국어 성능</h3><p>많은 임베딩 모델은 다양한 언어가 포함된 자료에 대한 훈련을 거쳤기 때문에 다국어를 지원합니다. 하지만 그렇다고 해서 모든 지원 언어에서 모두 똑같이 잘 작동하는 것은 아닙니다.</p><p>MMTEB 다국어 벤치마크에서 211개 언어를 식별하고 이를 분리하여 언어별로 유사한 모델과 비교할 수 있도록 했습니다. 아래 이미지는 히트맵 형태로 결과를 요약한 것입니다. 각 패치는 언어(ISO-639 코드로 식별)이며, 초록색이 짙을수록 유사한 모델의 평균에 비해 더 나은 성능을 발휘하는 모델입니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/852ac5d0f1977bb0c1124d87f8863a9bb94eb7da-1600x765.png" style="width: 100%;" /><p>언어마다 정확도는 다르지만 <code>jina-embeddings-v5-text</code> 모델은 전 세계 대부분의 언어에서 최첨단 또는 그에 가까운 정확도를 제공합니다.</p><p>다국어 성능에 대한 자세한 내용은 <a href="https://arxiv.org/abs/2602.15547"><code>jina-embeddings-v5-text</code></a> <a href="https://arxiv.org/abs/2602.15547">기술 보고서</a>를 참조하세요.</p><h2><strong>Elastic의 Jina: 검색을 위한 최첨단 네이티브 AI</strong></h2><p>EIS의 <code>jina-embeddings-v5-text</code> 모델을 사용하면 프로비저닝이나 확장을 위한 인프라 없이 완전 관리형 GPU 가속 추론을 통해 고성능 다국어 임베딩 모델을 <a href="https://www.elastic.co/kr/elasticsearch">Elasticsearch</a>에서 기본적으로 실행할 수 있습니다. <code>jina-embeddings-v5-text</code> 모델은 최신 AI 개발로 구동되는 컴팩트한 다국어 모델로 성장하는 EIS 모델 카탈로그를 확장합니다. 이러한 모델은 정보 검색 및 표준 데이터 분석 벤치마크에서 최첨단 성능을 제공하며, 비교할 수 없는 전 세계적인 다국어 지원을 제공합니다.</p><p>크기가 크게 다른 두 가지 모델이 있으므로 사용자는 용도와 예산에 가장 적합한 모델을 선택할 수 있습니다. 또한, 더 작은 크기로 잘라내거나 더 낮은 정밀도로 정량화해도 성능이 유지되는 강력한 임베딩을 통해 <code>jina-embeddings-v5-text </code>모델은 저장 공간 및 컴퓨팅 비용, 처리 지연 시간을 더욱 구체적으로 절감할 수 있는 기회를 제공합니다.</p><p><code>jina-embeddings-v5-text</code> 제품군, Jina Reranker, Elastic의 빠른 벡터 및 BM25 검색을 통해 사용자는 이제 Elastic의 엔드투엔드 최신 <a href="https://www.elastic.co/docs/solutions/search/hybrid-search">하이브리드 검색</a>에 액세스할 수 있습니다. Retrieval-Augmented Generation(RAG) 파이프라인, 검색 애플리케이션, 데이터 분석 등 가장 연관성이 높은 결과가 필요한 경우, Jina 검색 AI 모델이 포함된 Elastic은 견고하고 비용 효율적인 품질을 제공합니다.</p><h2><strong>시작하기</strong></h2><p><code>jina-embeddings-v5-text</code> 모델은 <a href="https://www.elastic.co/docs/explore-analyze/elastic-inference/eis">EIS</a>에 완전히 통합되어 있으며, 색인 생성 시 <strong><code>type</code> </strong>필드를<strong> <code>semantic_text</code></strong>로 설정하고 <code>inference_id</code><strong> </strong>필드에 모델(<code>jina-embeddings-v5-text-small</code> 또는 <code>jina-embeddings-v5-text-nano</code>)을 지정하면 이 예시에서와 같이 사용할 수 있습니다.</p><pre><code>PUT multilingual-semantic-index
{
  "mappings": {
    "properties": {
      "content": {
        "type": "semantic_text",
        "inference_id": ".jina-embeddings-v5-text-small"
      }
    }
  }
}

# Ingest data about France
POST multilingual-semantic-index/_doc
{
  "content": "The capital of France is Paris"}

GET multilingual-semantic-index/_search
{
  "query": {
    "semantic": {
      "field": "content",
      "query": "What is the French capital?"
    }
  }
}</code></pre><p><a href="https://www.elastic.co/kr/elasticsearch">Elasticsearch</a>는 색인 및 검색 과정에서 적절한 LoRA 어댑터를 자동으로 선택합니다. 임베딩 차원(위의 &quot;<strong>임베딩 잘라내기</strong>&quot; 섹션 참조)은 <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-inference-put">사용자 지정 추론 엔드포인트를 생성</a>할 때 설정할 수 있습니다.</p><p><strong><code>jina-embeddings-v5-text</code> </strong>모델 사용에 관한 자세한 내용은 <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/dense-vector">Elasticsearch 문서</a>를 참조하세요.</p><h2><strong>추가 정보</strong></h2><p><code>jina-embeddings-v5-text</code> 모델에 대한 자세한 내용은 <a href="https://jina.ai/news/jina-embeddings-v5-text-distilling-4b-quality-into-sub-1b-multilingual-embeddings/">Jina AI 블로그의 릴리즈 노트</a>와 <a href="https://arxiv.org/abs/2602.15547">기술 보고서</a>를 참조하세요. 성능 및 Jina AI의 혁신적인 새로운 훈련 절차에 대한 자세한 기술 정보를 확인할 수 있습니다. 이러한 모델을 로컬에서 다운로드하고 실행하는 방법에 대한 자세한 내용은 Hugging Face의 <a href="https://huggingface.co/collections/jinaai/jina-embeddings-v5-text"><code>jina-embeddings-v5-text</code> 컬렉션 페이지</a>를 참조하세요.</p><p>Jina AI 모델은 <a href="https://spdx.org/licenses/CC-BY-NC-4.0">CC-BY-NC-4.0 라이선스</a> 하에 제공되므로 무료로 다운로드하여 사용할 수 있으나, 상업적 사용은 <a href="https://www.elastic.co/contact?pg=global&amp;plcmt=nav&amp;cta=205352">Elastic 영업팀</a>에 문의해 주시기 바랍니다.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[최소 점수를 사용하여 시맨틱 정밀도 보장]]></title>
            <description><![CDATA[최소 점수 임계값을 사용하여 시맨틱 정밀도를 향상하세요. 이 글에서는 시맨틱 검색 및 하이브리드 검색에 대한 구체적인 예시를 제공합니다. ]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/semantic-precision-minimum-score</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/semantic-precision-minimum-score</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Mattias Brunnert]]></dc:creator>
            <pubDate>Fri, 20 Feb 2026 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/8b3b5910abfe16d48d309341a0027008b16c4340-720x420.jpg?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>시맨틱 검색은 검색 정확도 측면에서 무궁무진한 가능성을 열어주었습니다. ELSER, E5, Jina Embedding v4와 같은 고품질의 희소 및 밀집 모델은 키워드 일치가 아닌 단어의 의미를 기반으로 관련성 높은 결과를 제공합니다. 그러나 시맨틱 검색은 때때로 검색 결과의 끝부분이나 인덱스에 관련 결과가 부족한 쿼리에 대해 관련성이 떨어지는 결과를 반환할 수 있습니다. 이러한 희소 및 밀집 모델의 속성으로 인해 사용자가 혼란을 겪거나 대규모 언어 모델(LLM)에서 귀중한 토큰이 낭비될 수 있습니다.</p><p>이 글에서는 최소 점수 매개변수를 사용하여 시맨틱 검색 결과의 정밀도를 높이는 방법을 알아봅니다. 이 블로그 게시물에 제공된 예시를 테스트해 보고 싶다면 <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/ensuring-semantic-precision-with-minimum-score/ensuring_semantic_precision_with_minimum_score.ipynb">관련 Jupyter 노트북</a>으로 이동하세요.</p><h2>배경: 정밀도와 재현율</h2><p>검색 정확도에서 <em>정밀도</em>와 <em>재현율</em>은 핵심 개념입니다. 아직 익숙하지 않은 독자라면 관련 내용을 알아보기를 강력히 권장합니다. 다음은 간략한 내용입니다.</p><ul><li><strong>정밀도: </strong>사용자와 관련된 반환된 검색 결과의 비율.</li><li><strong>재현율: </strong>검색 결과 집합에 포함된 코퍼스 내 모든 관련 문서의 비율.</li></ul><p>다시 말해 정밀도는 <strong>오직</strong> 관련성 있는 결과만 반환하는 것이고, 재현율은 관련성 있는 <strong>모든</strong> 결과를 반환하는 것입니다. 짐작하시겠지만, 이 두 가지는 종종 상충되는 요구 사항입니다. 시맨틱 검색은 재현율은 매우 높지만, 정밀도는 떨어지는 경향이 있습니다. 이 속성을 해결하는 방법을 알아보려면 계속 읽어보세요.</p><h2>최소 점수 매개변수 소개</h2><p>&#x27;min_score&#x27; 매개변수를 사용하면 최소 점수를 설정하여 정밀도를 향상할 수 있으며, 이 매개변수는 정의된 임계값보다 낮은 점수를 가진 일치 항목을 제거하여 결과 집합을 잘라냅니다. 다음은 간단한 예시입니다.</p><pre><code>GET search-movies/_search
{
  "retriever": {
    "linear": {
      "min_score": 4,
      "retrievers": [
        ...
      ]
    }
  }
}</code></pre><h2>점수 정규화</h2><p>최소 점수를 설정하는 것도 좋지만, 모든 시맨틱 모델이 정적 임계값에 적합한 점수를 반환하는 것은 아닙니다. 예를 들어 ELSER는 제한이 없는 점수를 반환합니다. <a href="https://huggingface.co/intfloat/e5-small#faq">일부</a> 밀집 모델 점수는 밀집도가 높으며 특정 쿼리의 컨텍스트에서만 의미가 있습니다.</p><p>대부분의 시맨틱 검색 사례의 경우 &#x27;min_score&#x27;를 적용하기 전에 정규화 접근 방식을 사용하는 것이 좋습니다. 정규화는 문서 점수가 정의된 간격 내에 있도록 보장합니다. Elasticsearch 검색기는 &#x27;l2_norm&#x27;과 &#x27;minmax&#x27;라는 두 가지 <a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/retrievers/linear-retriever#linear-retriever-normalizers">정규화 도구</a>를 제공합니다. 가장 일반적으로 사용되는 것은 &#x27;minmax&#x27;입니다. 이해하기 쉽고 많은 시나리오에서 잘 작동하기 때문입니다. &#x27;minmax&#x27;의 주요 속성은 다음과 같습니다.</p><ul><li>문서 점수는 0–1점 사이로 분포됩니다.</li><li>가장 높은 점수를 받은 문서는 항상 1점으로 처리됩니다.</li><li>가장 낮은 점수를 받은 문서는 항상 0점으로 처리됩니다.<ul><li>이로 인해 키워드 검색에 적합하지 않을 수 있습니다. 자세한 내용은 &#x27;하이브리드 검색&#x27; 섹션을 참조하세요.</li></ul></li></ul><p>다음은 <code>min_score</code> 을(를) 사용하여 정규화된 시맨틱 쿼리의 예입니다. 검색 결과 목록을 100개부터 시작하는 더 긴 목록으로 반환할 수 있도록 순위 창 크기를 500으로 늘렸습니다.</p><pre><code>GET search-movies/_search
{
  "size": 100,
  "_source": [
    "title", "overview"
  ],
  "retriever": {
    "linear": {
      "rank_window_size": 500,
      "min_score": 0.25,
      "retrievers": [
        {
          "normalizer": "minmax",
          "retriever": {
            "standard": {
              "query": {
                "semantic": {
                  "field": "overview_vector",
                  "query": "superhero movie"
                }
              }
            }
          }
        }
      ]
    }
  }
}</code></pre><p>이 크기는 실제 프로덕션 환경에서 일반적으로 사용되는 값보다 크게 설정되었습니다. 이는 검색 결과의 품질을 검사하고 결과를 조정하기 위한 것입니다.</p><h2>선형 검색기를 사용한 하이브리드 검색</h2><p>하이브리드 검색의 경우 가장 간단한 접근 방식은 모든 점수를 정규화하고 가중치를 부여한 다음 최소 점수를 적용하는 것입니다. 합이 1인 가중치를 선택하면 총점이 0–1 범위 내에 유지됩니다. 이렇게 하면 최종 점수를 쉽게 파악하고 <code>min_score</code> 을(를) 조정할 수 있습니다. 다음은 그 예입니다.</p><pre><code>GET search-movies/_search
{
  "size": 100,
  "_source": ["title", "overview","keywords"],
  "retriever": {
    "linear": {
      "rank_window_size": 500,
      "min_score": 0.25,
      "retrievers": [
        {
          "weight": 0.6,
          "normalizer": "minmax",
          "retriever": {
            "standard": {
              "query": {
                "semantic": {
                  "field": "overview_vector",
                  "query": "superhero movie"
                }
              }
            }
          }
        },
        {
          "weight": 0.4,
          "normalizer": "minmax",
          "retriever": {
            "standard": {
              "query": {
                "multi_match": {
                  "query": "superhero movie",
                  "fields": ["overview","keywords", "title"],
                  "type": "cross_fields",
                  "minimum_should_match": "2"
                }
              }
            }
          }
        }
      ]
    }
  }
}</code></pre><h2>RRF를 사용한 하이브리드 검색</h2><p>BM25에서는 종종 <code>AND</code> 연산자나 <code>minimum_should_match</code> 와(과) 같은 다른 수단을 통해 정밀도를 제어하기도 합니다. 또한 단일하고 정확하며 희귀한 용어로 구성된 쿼리는 자연스럽게 검색 결과 수가 적고, 그 결과가 모두 관련성이 높은 경우가 많습니다. 이는 다음과 같은 결과로 이어질 수 있습니다.</p><ul><li>BM25 검색기에서 결과 뒷부분에 있는 결과는 절대 BM25 점수가 최고 점수에 근접하더라도 낮은 정규화 점수를 받게 됩니다.</li><li>매우 낮은 BM25 점수를 시맨틱 점수에 더하면 총점이 시맨틱 점수와 거의 같아집니다.</li><li>BM25 점수 기여도가 부족하면 <code>min_score threshold</code> 에 의해 문서가 폐기될 수 있습니다.</li></ul><p>이에 대한 해결책으로 상호 순위 융합(RRF)을 사용하여 BM25와 시맨틱 결과를 결합할 수 있습니다. RRF는 서로 다른 검색 알고리즘의 점수를 비교하는 문제를 해결하기 위해 각 결과 집합에서의 위치에 초점을 맞춥니다. 이 시나리오에서는 <code>min_score</code> 이(가) 시맨틱 검색기에만 적용됩니다.</p><pre><code>GET search-movies/_search
{
  "_source": ["title", "overview","keywords"],
  "retriever": {
    "rrf": {
      "rank_window_size": 500,
      "retrievers": [
        {
          "linear": {
            "rank_window_size": 500,
            "min_score": 0.25,
            "retrievers": [
              {
                "normalizer": "minmax",
                "retriever": {
                  "standard": {
                    "query": {
                      "semantic": {
                        "field": "overview_vector",
                        "query": "superhero movie"
                      }
                    }
                  }
                }
              }
            ]
          }
        },
        {
          "standard": {
            "query": {
              "multi_match": {
                "query": "superhero movie",
                "fields": ["overview", "keywords","title"],
                "type": "cross_fields",
                "minimum_should_match": "2"
              }
            }
          }
        }
      ]
    }
  }
}</code></pre><h2>결론</h2><p><code>min_score</code>을(를) 사용하여 시맨틱 검색 알고리즘의 높은 재현율로 인해 발생하는 결과 집합의 오탐 수를 줄이는 방법을 보여주었습니다. 검색기에 대해 자세히 알아보려면 이 <a href="https://www.elastic.co/search-labs/blog/elasticsearch-retrievers">블로그 게시물</a>과 <a href="https://www.elastic.co/docs/solutions/search/retrievers-overview">Elasticsearch 설명서</a>를 참조하세요.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Kubernetes에서의 종속성 관리]]></title>
            <description><![CDATA[Renovate CLI와 Argo Workflows를 사용하여 Kubernetes에서 종속성 관리를 간소화하는 방법]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/dependency-management-kubernetes</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/dependency-management-kubernetes</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Nikos Fotiou]]></dc:creator>
            <pubDate>Thu, 19 Feb 2026 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/00519840e6eec7101c1fb096afcae976ee0c454e-1280x720.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>다음은 업데이트를 자동화하고 일반적인 취약성 및 노출(CVE)을 신속하게 해결하며 수천 개의 리포지토리에 새 패키지 버전을 효율적으로 전파하기 위해 Kubernetes, Argo Workflows, Argo Events 및 Renovate CLI로 셀프 호스팅된 종속성 관리 플랫폼을 구축한 방법입니다.</p><h2><strong>Elastic의 종속성 관리</strong></h2><p>Elastic에서는 비공개 및 공개를 포함하여 수백 개, 심지어 수천 개의 리포지토리를 관리해야 합니다. 중요한 CVE가 발견되면 여러 질문에 대한 즉각적인 답변과 조치가 필요합니다. &#x27;어떤 리포지토리가 취약한가?&#x27;, &#x27;얼마나 빨리 패치를 적용할 수 있는가?&#x27; 등이 있습니다. 보안 문제 외에 &#x27;수작업에 너무 많은 시간을 들이지 않고 새 패키지 버전의 릴리스를 해당 패키지에 의존하는 모든 리포지토리에 신속하게 전파하는 방법은 무엇인가?&#x27;라는 생산성 관련 질문도 제기됩니다.</p><p>종속성 관리 방법을 모색하게 된 초기 계기는 <a href="https://www.elastic.co/blog/reducing-cves-in-elastic-container-images">CVE 감소</a>를 위해 자동화된 업데이트와 함께 안전한 기반을 구축해야 하는 필요성 때문이었습니다. 종속성 관리 솔루션을 신중하게 검토한 후, 우선 자체 호스팅된 인프라 구축에 착수했습니다. Mend Renovate Community 셀프 호스팅 에디션을 실행하기 위해 자체 Kubernetes 클러스터를 사용하고 있었습니다. 사용자들이 셀프 서비스 방식으로 접근할 수 있는 종속성 관리 플랫폼을 제공하는 것이 아이디어의 핵심이었습니다.</p><p>초기 실험이 성공적이었기 때문에 점점 더 많은 팀이 플랫폼에 온보딩하여 일상적인 리포지토리의 수명 주기에서 업데이트 및 CVE 패치에 이를 사용하기 시작했습니다. 이 과정이 너무 빨리 진행되어 곧 자체 호스팅 설치 용량의 한계에 도달했습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/e14d9f98d4eccaa08a335d5bd23d88e5debbb344-1600x1103.png" style="width: 100%;" /><h3><strong>과제: 대규모 조직에서 수많은 리포지토리를 관리할 때, 종속성 관리 플랫폼을 어떻게 확장할 수 있을까요?</strong></h3><p>기존 종속성 관리 플랫폼은 한 번에 하나의 리포지토리만 처리할 수 있었고, 수많은 리포지토리로 인해 순차적 처리 모델로는 대처할 수 없었습니다. 우리는 <strong>종속성 관리 도구의 단일 인스턴스</strong>로 점점 커지는 리포지토리 목록을 처리할 수 있다는 생각 자체에 문제가 있다는 것을 이미 인식하고 있었습니다. 리포지토리는 대기열에서 기다렸고, 때로는 몇 시간이 걸렸습니다. 매일 50% 이상의 리포지토리가 처리되지 않았습니다. 즉, 리포지토리의 50% 이상이 되기까지 24시간 이상 대기해야 했습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/45ade5bda08f82bed0b3d0d3736cbd6f056e7a4e-1312x816.jpg" style="width: 100%;" /><p>대규모 리포지토리는 방대한 코드베이스와 여러 개의 열린 PR로 인해 더 큰 병목 현상을 일으켰습니다. GitHub 웹훅 이벤트는 시퀀스를 중단시켰습니다. Automerge는 스캔 타이밍을 예측할 수 없기 때문에 신뢰할 수 없었습니다. 사용자들에게 스캔 빈도에 대해 약속했지만, 그 약속을 지키지 못했습니다.</p><h3><strong>자체 구축 결정: Elastic 고유의 확장성 및 보안 요구 사항 충족</strong></h3><p><strong>Mend의 Renovate Self-Hosted Enterprise 셀프 호스팅 에디션</strong>을 포함한 상용 옵션을 고려하면서, Elastic 내부적으로는 몇 가지 주요 이니셔티브를 강화하고 있었습니다.</p><p>Elastic의 타협할 수 없는 특정한 요구 사항을 충족할 수 있는 심도 깊은 맞춤형 솔루션만이 유일한 해결책이라는 인식에 따라 자체 플랫폼을 구축하기로 결정했습니다.</p><ol><li><strong>내부 개발자 플랫폼 투자:</strong> 당시 내부 개발자 플랫폼에 대해 이미 대규모 투자를 시작한 상태였습니다. 각 서비스를 여기에 조화시키는 방식에 대해 논의하고 설계했습니다. 즉, 자체적으로 만든 종속성 관리 플랫폼의 규칙과 관행을 시험적으로 적용해 보고자 했습니다. 게다가 새로운 가이드라인이 시행될 예정이었기 때문에, 그 전에 플랫폼을 설계하고 싶었습니다.</li><li><strong>네이티브 통합 및 워크플로우 맞춤 설정:</strong> 내부 도구 및 내부 프로세스와의 간편한 통합이 필요했습니다. 예를 들어, 서비스 카탈로그(Backstage)를 사용하여 구성을 코드로 중앙 집중화하고자 했습니다. Backstage의 사용에 관한 특정 요구 사항이 있었으며 플랫폼이 이러한 요구 사항과 호환되기를 원했습니다. 따라서 Renovate 셀프 호스팅 API를 Backstage 자동화와 함께 사용하는 것이 가능하지만, 이것으로는 내부 프로세스를 완전히 지원할 수 없습니다.</li><li><strong>Elastic에 특화된 심층 방어 보안:</strong> 엄격한 보안 규정 준수를 위해서는 에코시스템에 맞춘 맞춤형 보안 메커니즘이 필요했습니다. 우리는 <a href="https://entro.security/blog/how-elastic-scaled-secrets-nhi-security-elastics-playbook-from-visibility-to-automation/">&quot;비인간 ID&quot;의 사용을 강화하기 위해 노력하고 있었습니다.</a> 이러한 액세스 강화 방식으로 인해 비표준적인 GitHub 인증 수단은 이 내부 구현을 지원하지 않는 기성 도구에서는 작동하지 않았습니다. 워크플로우에는 부모-자식 워크플로우 비밀 암호화 패턴 구현과 일시적인 일회성 GitHub 토큰 사용이 포함되었습니다. 자체 구축이 이러한 고유한 보안 계층을 내장하고 복잡한 멀티클라우드 환경 전반에 걸쳐 공격 표면을 최소화하는 유일한 현실적인 방법이었습니다.</li></ol><h2><strong>해결책: 종속성 관리를 위한 워크플로우 오케스트레이션</strong></h2><p>솔루션은 이미 사용 중인 종속성 관리 도구를 대체하거나 다른 솔루션을 찾지 않고 이를 기반으로 구축한다는 데서 출발했습니다. 이는 가능성의 징후를 보였으며, 조직 전반에 걸쳐 다양한 요구에 맞추는 유연성이 중요했습니다. 다양한 솔루션을 고려했으며, 우리가 충족해야 할 중대하고 때로는 특별한 요구 사항을 토대로 결정을 내릴 수 있었습니다. 각 리포지토리가 독립적으로 처리되어 병목 현상이 발생하지 않고 성장을 위한 기반을 제공할 수 있도록 신뢰할 수 있고 확장성 있는 종속성 관리 플랫폼을 구축하기로 결정했습니다.</p><p>세 가지 핵심 원칙에 따라 플랫폼을 설계했습니다.</p><h3><strong>1. 병렬 처리</strong></h3><p>모든 리포지토리는 자체 종속성 관리 처리 환경을 갖습니다. 더 이상 대기열이 없습니다. 동시성은 사용하는 리소스의 수에 의해서만 제한됩니다. 또한 GitHub 속도 제한을 피하기 위해 스마트 분산 스케줄링을 적용했습니다.</p><h3><strong>2. 셀프 서비스 가능</strong></h3><p>서비스 카탈로그(백스테이지)를 사용하여 새로운 리포지토리를 자동으로 온보딩하고 관리합니다. 자체 리소스 정의를 사용하여 최종 사용자에게 리포지토리 처리 빈도, 스케줄에 할당할 리소스 수, 그리고 어떤 이유로든 처리를 끌지 아니면 다시 켤지 선택할 수 있는 옵션을 제공합니다. 사용자 요구 사항이 발전하고 새로운 설치 환경에 익숙해짐에 따라 이러한 방식으로 더 많은 옵션을 추가할 계획입니다.</p><h3><strong>3. 비밀 범위 축소 및 네임스페이스 격리</strong></h3><p>보안 강화를 위해, 각 워크플로우 시작 시 생성되는 임시 GitHub 토큰을 종속성 관리 포드에 제공합니다. 그뿐만 아니라, 필요한 비밀 키만 제공받을 수 있도록 워크로드를 특정 네임스페이스로 격리합니다. Kubernetes RBAC를 사용하여 각 종속성 관리 워크플로우에서 액세스할 수 있는 비밀 정보를 제어합니다. 또한 GitHub 토큰을 부모 워크플로우에서 자식 워크플로우로 전파할 때 암호화를 사용합니다.</p><p>Kubernetes를 사용하여 플랫폼을 재구축했으며, Kubernetes의 강력한 기능을 활용해 Argo Workflows는 프로세스 로직을 강화하고, Renovate CLI는 한 번에 하나의 리포지토리를 스캔하고 처리하도록 설정되었습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/5560ed20e2bd9ecdd574a9c835126d12b24c332f-1600x1157.png" style="width: 100%;" /><p><strong>장점:</strong> 검증된 오픈 소스 프로젝트를 독창적인 방식으로 활용하여 모든 프로젝트에 대해 새로운 작동 예제를 제공하는 동시에, 개발 속도를 높이고 팀의 CVE 감소를 강화합니다.</p><h2><strong>종속성 관리 아키텍처: 4개의 마이크로서비스</strong></h2><p>이 플랫폼은 맞춤 제작된 4가지 구성 요소로 이루어져 있습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/2b3d4046c05bb261e45d40c59f864eb51fb9eaa9-1217x1600.png" style="width: 100%;" /><h3><strong>워크플로우 오퍼레이터(Go/Kubebuilder)</strong></h3><p>3가지 맞춤형 리소스 정의(CRD)를 통해 워크플로우 수명 주기를 관리하는 Kubernetes Operator:</p><ul><li><strong>RepoConfig CRD:</strong> 리포지토리 구성을 위한 단일 정보 소스입니다.</li></ul><p>다음은 오퍼레이터에서 RepoConfig가 정의되는 방식입니다.</p><pre><code>// RepoConfig is the Schema for the repoconfigs API
type RepoConfig struct {
	metav1.TypeMeta `json:",inline"`

	// metadata is a standard object metadata
	// +optional
	metav1.ObjectMeta `json:"metadata,omitempty,omitzero"`

	// spec defines the desired state of RepoConfig
	// +required
	Spec RepoConfigSpec `json:"spec"`

	// status defines the observed state of RepoConfig
	// +optional
	Status RepoConfigStatus `json:"status,omitempty,omitzero"`
}</code></pre><p>다음은 RepoConfig 인스턴스의 모습입니다.</p><pre><code>apiVersion: workflows.elastic.co/v1
kind: RepoConfig
metadata:
  generation: 3
  name: elastic-test-repo
  namespace: dependency-management-operator
spec:
  owner: group:my-team
  renovate:
    config:
      resourceGroup: SMALL
      runFrequency: 4h
    enabled: true
  repository: elastic/test-repo</code></pre><ul><li><strong>부모 CRD:</strong> 예약된 스캔을 위한 CronWorkflows를 관리합니다.</li></ul><p>부모 컨트롤러의 조정 루프 내에서 워크플로우 설정이 생성되고 최신 상태로 유지되거나, 필요한 경우 삭제되는지 확인합니다.</p><p>먼저 워크플로우에 대한 몇 가지 전역 설정을 가져옵니다.</p><pre><code>func (r *ParentReconciler) reconcileSubResources(ctx context.Context, req ctrl.Request, parent *workflowsv1.Parent) error {
	logger := logf.FromContext(ctx)
	logger.Info("Reconcile SubResources for Parent", "name", req.NamespacedName)
	wfSet := workflowsettings.WorkflowSettings{
		RunFrequency:   parent.Spec.RunFrequency,
		ResourceGroups: "parent",
	}</code></pre><p>유사한 워크플로우가 동시에 실행되는 것을 방지하기 위해 mutex configmap이 최신 상태인지 확인합니다.</p><pre><code>	cfMngr := resources.NewConfigMapManager(r.Client, r.Scheme, r.OperatorConfig.ParentNamespace)
	err := cfMngr.CreateOrUpdateSyncMutexConfigmap(ctx, fmt.Sprintf("%s%s", r.OperatorConfig.ResourcesPrefix, r.OperatorConfig.SyncMutexCfgMapName), strings.TrimPrefix(parent.Spec.Repository, "elastic/"), r.OperatorConfig.SemaphoreConcurrencyLimit)</code></pre><p>그런 다음 CronWorkflows와 워크플로우 템플릿을 생성하거나 업데이트하는 구조체인 Workflow Manager를 생성합니다.</p><pre><code>	wfMngr := resources.NewArgoWorkflowManager(r.Client,
		r.Scheme,
		curateResourceName(
			strings.ReplaceAll(parent.Spec.Repository, "/", "-"),
		),
		parent.Namespace,
		"parent-workflow",
		false).
		WithOrganization(r.OperatorConfig.GitHubOrg).
		WithRepoName(parent.Spec.Repository).
		Init(true, true).
		WithPrefix(r.OperatorConfig.ResourcesPrefix).
		WithWfTemplateName(r.OperatorConfig.ParentWorkflowTemplate).
		WithResources(wfSet.GetResourceCategory()).
		WithSchedule(wfSet.GetCronSchedule()).
		WithImagePullSecrets([]corev1.LocalObjectReference{{
			Name: r.OperatorConfig.WorkflowImagePullSecrets,
		}}).
		AddArgument(true, true, "extra_cli_args").
		SetArgument(true, false, "extra_cli_args", "none").
		AddTemplate(resources.NewParentDAGTemplateInstance()).
		AddTemplate(resources.NewWorkflowsTemplateInstance("check-child-workflows", r.OperatorConfig.WorkflowImagePullPolicy, r.OperatorConfig.WorkflowNodeSelector)).
		AddTemplate(resources.NewWorkflowsTemplateInstance("security", r.OperatorConfig.WorkflowImagePullPolicy, r.OperatorConfig.WorkflowNodeSelector)).
		AddTemplate(resources.NewWorkflowsTemplateInstance("submit-child-workflow", r.OperatorConfig.WorkflowImagePullPolicy, r.OperatorConfig.WorkflowNodeSelector))
	wfMngr.OverWriteCommand("submit-child-workflow", r.OperatorConfig.ChildNamespace)
	wfMngr.OverwriteWfTemplateName("parent-wftmpl")
	wfMngr.AddSynchronization(fmt.Sprintf("%s%s", r.OperatorConfig.ResourcesPrefix, r.OperatorConfig.SyncMutexCfgMapName), "{{workflow.parameters.repo_name}}")
	err = wfMngr.CreateOrUpdateCronWorkflow(ctx)
	if err != nil {
		return fmt.Errorf("failed to create or update cron workflow: %w", err)
	}
	err = wfMngr.CreateOrUpdateWorkflowTemplate(ctx)
	if err != nil {
		return fmt.Errorf("failed to create or update workflow template: %w", err)
	}
	return nil</code></pre><ul><li><strong>자식 CRD:</strong> 리포지토리별 리소스로 워크플로우 템플릿을 관리합니다.</li></ul><p>자식 컨트롤러는 부모와 유사한 조정 의무를 가지고 있지만, 이번에는 부모 워크플로우에 의해 트리거될 자식 네임스페이스의 워크플로우 템플릿에 대한 책임이 있습니다.</p><pre><code>func (r *ChildReconciler) reconcileSubResources(ctx context.Context, req ctrl.Request, child *workflowsv1.Child) error {
	logger := logf.FromContext(ctx)
	logger.Info("Reconcile SubResources for Child", "name", req.NamespacedName)
	wfSet := workflowsettings.WorkflowSettings{
		ResourceGroups: child.Spec.ResourceCategory,
	}
	wfMngr := resources.NewArgoWorkflowManager(r.Client,
		r.Scheme,
		curateResourceName(
			strings.ReplaceAll(child.Spec.Repository, "/", "-"),
		),
		child.Namespace,
		"runner",
		true).
		Init(false, true). // only manage workflow template
		WithPrefix(r.OperatorConfig.ResourcesPrefix).
		WithSuffix("-child-wftmpl").
		WithRepoName(child.Spec.Repository).
		WithOrganization(r.OperatorConfig.GitHubOrg).
		WithResources(wfSet.GetResourceCategory()). // will override resources of presets if set
		WithImagePullSecrets([]corev1.LocalObjectReference{{
			Name: r.OperatorConfig.WorkflowImagePullSecrets,
		}}).
		AddTemplate(resources.NewWorkflowsTemplateInstance("runner", r.OperatorConfig.WorkflowImagePullPolicy, r.OperatorConfig.WorkflowNodeSelector)).
		AddArgument(false, true, "repo_full_name").
		AddArgument(false, true, "repo_name").
		AddArgument(false, true, "encrypted_token").
		AddArgument(false, true, "extra_cli_args")
	wfMngr.OverWriteCommand("runner", r.OperatorConfig.ChildNamespace)
	err := wfMngr.CreateOrUpdateWorkflowTemplate(ctx)
	if err != nil {
		return fmt.Errorf("failed to create or update workflow template: %w", err)
	}
	return nil
}</code></pre><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/7ac70492a1266ba02cb8afbafc5a486cb38a0edc-1600x1290.png" style="width: 100%;" /><p>멀티 컨트롤러 패턴은 명확한 분리를 제공합니다. RepoConfig 컨트롤러는 온보딩/오프보딩을 처리하고, 부모 컨트롤러는 스케줄링을 관리하며, 자식 컨트롤러는 실행 템플릿을 처리합니다.</p><h3><strong>GitHub Events Gateway(Go)</strong></h3><p>GitHub 웹훅을 수신하고 서명을 확인하고 조직/리포지토리별로 필터링한 후 Argo Events로 라우팅하는 안전한 웹훅 프록시입니다. 종속성 대시보드 상호 작용, PR 이벤트 및 패키지 업데이트에 반응하는 10개의 서로 다른 센서를 구축했습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/4828625456cbd6efa8020a20f10d23f294f98a02-1306x1600.png" style="width: 100%;" /><p>이 게이트웨이는 다음을 통해 GitHub 앱과 통합할 수 있도록 지원합니다.</p><ul><li>보안을 위해 수신되는 GitHub 웹훅 서명을 확인합니다.</li><li>유효한 이벤트를 모든 관련 헤더 및 인증 정보와 함께 Argo Events EventSource로 전달합니다.</li><li>또한 EventSource에 authSecret을 구성하고 전달된 요청의 Bearer 헤더로 이를 제공합니다.</li><li>로깅, 메트릭 및 재시도 로직을 제공합니다.</li></ul><p>각 GitHub 이벤트 요청에 대해 다양한 검증을 수행합니다.</p><p>일부 HTTP 속성이 있는지 확인합니다:</p><pre><code>// ValidateRequestMethod checks if the request method is POST.
func ValidateRequestMethod(r *http.Request) error {
	if r.Method != http.MethodPost {
		return fmt.Errorf("method not allowed, only POST is accepted")
	}
	return nil
}

// ValidateRequiredHeaders checks for required GitHub headers.
func ValidateRequiredHeaders(r *http.Request) error {
	eventType := r.Header.Get("X-GitHub-Event")
	deliveryID := r.Header.Get("X-GitHub-Delivery")
	signature := r.Header.Get("X-Hub-Signature-256")
	if eventType == "" || deliveryID == "" || signature == "" {
		return fmt.Errorf("missing required GitHub headers")
	}
	return nil
}

// ValidateUserAgent checks that the User-Agent header starts with GitHub-Hookshot/
func ValidateUserAgent(r *http.Request) error {
	userAgent := r.Header.Get("User-Agent")
	if !strings.HasPrefix(userAgent, "GitHub-Hookshot/") {
		return fmt.Errorf("invalid User-Agent")
	}
	return nil
}</code></pre><p>또한 각 요청의 서명과 그 구성도 검증합니다.</p><pre><code>// ValidateSignature verifies the GitHub webhook signature.
func ValidateSignature(r *http.Request, secret string) ([]byte, error) {
	payload, err := GitHub.ValidatePayload(r, []byte(secret))
	if err != nil {
		return nil, fmt.Errorf("invalid GitHub signature: %w", err)
	}
	return payload, nil
}

// ValidateAllowedOwner checks if the organization login is in the allowed organizations list.
func ValidateAllowedOwner(payload []byte, allowedGitHubOrganizations []string) (string, error) {
	var orgLogin string
	var payloadMap map[string]any
	if err := json.Unmarshal(payload, &payloadMap); err == nil {
		if orgObj, ok := payloadMap["organization"].(map[string]any); ok {
			if login, ok := orgObj["login"].(string); ok {
				orgLogin = login
			} else if name, ok := orgObj["name"].(string); ok {
				orgLogin = name
			}
		}
	}
	if !slices.Contains(allowedGitHubOrganizations, orgLogin) {
		return orgLogin, fmt.Errorf("organization login not allowed")
	}
	return orgLogin, nil
}</code></pre><p>마지막으로 이벤트 유형에 따라 Argo Events로 라우팅됩니다.</p><pre><code>	// Map eventType to Argo `EventSource` path
	var endpoint string
	switch eventType {
	case "push":
		endpoint = "/push"
	case "issues":
		endpoint = "/issues"
	case "pull_request":
		endpoint = "/pull-requests"
	default:
		slog.Info("Ignoring unhandled event type", "event_type", eventType, "delivery_id", deliveryID)
		w.WriteHeader(http.StatusOK)
		_, _ = w.Write([]byte("ok"))
		return
	}
	forwardURL := h.config.ArgoEventSourceForwardURL + endpoint</code></pre><p>Argo Events 측에서는 10개의 센서가 새로운 이벤트를 감지하기 위해 Argo Events EventBus를 모니터링합니다.</p><pre><code>apiVersion: argoproj.io/v1alpha1
kind: Sensor
metadata:
  name: {{ .Values.sensors.packageUpdateOnDefaultBranch.name }}
  namespace: {{ .Release.Namespace }}
spec:
  eventBusName: {{ .Values.eventBus.name }}</code></pre><p>그 후 스크립트는 각 센서의 논리를 적용합니다:</p><pre><code>script: |
          local e = event
          if not e or not e.body or not e.body.repository then
            return false
          end

          -- e.g., "refs/heads/main"
          local ref = e.body.ref
          local default_branch = e.body.repository.default_branch
          if not ref or not default_branch then
            return false
          end

          local expected = "refs/heads/" .. default_branch
          if ref ~= expected then
            return false
          end

        {{- if .Values.sensors.packageUpdateOnDefaultBranch.packageFiles }}
          patterns = { {{- range $i, $f := .Values.sensors.packageUpdateOnDefaultBranch.packageFiles }}{{ if $i }}, {{ end }}"{{ $f }}"{{- end }} }
        {{- end }}

          local function anyMatch(path)
            if type(path) ~= "string" then return false end
            for _, pat in ipairs(patterns) do
              -- match filename at repo root, or anywhere under subdirs
              if path:match(pat) or path:match(".+/" .. pat) then
                return true
              end
            end
            return false
          end

          local function filesContainPackage(paths)
            if type(paths) ~= "table" then return false end
            for _, p in ipairs(paths) do
              if anyMatch(p) then return true end
            end
            return false
          end

          -- Inspect all commits (GitHub includes added/modified/removed lists)
          local commits = e.body.commits
          if type(commits) ~= "table" then
            -- Fallback: some payloads include only head_commit
            commits = {}
            if type(e.body.head_commit) == "table" then
              table.insert(commits, e.body.head_commit)
            end
          end

          for _, c in ipairs(commits) do
            if filesContainPackage(c.added) or filesContainPackage(c.modified) or filesContainPackage(c.removed) then
              return true
            end
          end

          return false</code></pre><h3><strong>Backstage Syncer(Go)</strong></h3><p>이 작업은 서비스 카탈로그(Backstage)에서 리포지토리 실제 리소스 엔티티를 폴링하고, 이를 RepoConfig CRD로 변환하며, 플랫폼을 구성 변경 사항과 동기화된 상태로 유지합니다. 변경 사항은 3분 이내에 적용됩니다.</p><pre><code>repoMap := make(map[string]map[string]interface{})
			for i := range entities {
				entity := &entities[i]
				if entity.Spec.Type != "GitHub-repository" {
					continue
				}

				implRaw, err := json.Marshal(entity.Spec.Implementation)
				if err != nil {
					logger.Error("Failed to marshal implementation", "error", err)
					continue
				}

				var implMap map[string]interface{}
				err = json.Unmarshal(implRaw, &implMap)
				if err != nil {
					logger.Error("Failed to unmarshal implementation map", "error", err)
					continue
				}
				var repoName string
				if specMap, ok := implMap["spec"].(map[string]interface{}); ok {
					if repo, ok := specMap["repository"].(string); ok {
						repoName = repo
					}
				}
				if repoName == "" {
					continue
				}

				var workflowsRaw []byte
				if v, ok := implMap["spec"].(map[string]interface{}); ok {
					if r, ok := v["renovate"]; ok {
						workflowsRaw, _ = json.Marshal(r)
					} else {
						workflowsRaw = []byte(`{}`)
					}
				} else {
					workflowsRaw = []byte(`{}`)
				}

				var workflowsWithDefaults schema.WorkflowsMetadata
				err = json.Unmarshal(workflowsRaw, &rworkflowsWithDefaults)
				if err != nil {
					logger.Error("Failed to unmarshal workflows config", "error", err)
					continue
				}

				workflowsMap := map[string]interface{}{
					"enabled":        workflowsWithDefaults.Enabled,
					"require_pr":     workflowsWithDefaults.RequirePr,
					"resource_group": string(workflowsWithDefaults.ResourceGroup),
					"run_frequency":  string(workflowsWithDefaults.RunFrequency),
				}
				repoMap[repoName] = map[string]interface{}{
					"renovate": workflowsMap,
					"owner":    entity.Spec.Owner,
				}
			}
			logger.Info("Fetched GitHub Repository data from Backstage", "repository_count", len(repoMap), "status_code", resp.StatusCode)</code></pre><p>마지막으로 해당 데이터를 RepoConfig 인스턴스에 기록합니다.</p><h3><strong>워크플로우 베이스(혼합: JavaScript, Go, Helm)</strong></h3><p>기반 레이어에는 Helm 차트, JavaScript 구성, 암호화 지원 기능을 갖춘 Renovate CLI용 Go 래퍼, 그리고 Alpine 패키지용 맞춤형 APK 인덱서가 포함되어 있습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/908d19278face3ce1119dbee9146c1264b6e2f30-1600x873.png" style="width: 100%;" /><h2><strong>셀프 서비스 구성</strong></h2><p>팀은 Backstage를 통해 리포지토리를 선언적으로 구성합니다.</p><pre><code>spec:
  renovate:
    enabled: true
    config:
      resourceGroup: LARGE      # SMALL | MEDIUM | LARGE  
      runFrequency: "0 */4 * * *"  # Every 4 hours</code></pre><p>리소스 그룹은 리포지토리 크기에 따라 CPU와 메모리를 할당합니다.</p><ul><li><strong>SMALL:</strong> 500m CPU, 1Gi 메모리.</li><li><strong>MEDIUM:</strong> 1000m CPU, 2Gi 메모리.</li><li><strong>LARGE:</strong> 2000m CPU, 4Gi 메모리.</li></ul><p>구성은 버전이 제어되고 감사 가능하며 자동으로 적용됩니다.</p><h2><strong>부모-자식 패턴</strong></h2><p>실행 모델은 부모-자식 워크플로우 패턴을 사용합니다.</p><ul><li><strong>부모 워크플로우:</strong> 예약된 시간에 실행되는 경량 CronWorkflow입니다. 비밀 정보를 암호화하고, 검사 실행 여부를 결정하며, 구성 정보를 자식 워크플로우에 전달합니다.</li><li><strong>자식 워크플로우:</strong> Renovate CLI가 실행되는 임시 포드입니다. 동적으로 리소스를 할당하고, 격리된 상태에서 비밀 정보를 해독하며, 완료 후 종료합니다.</li></ul><p>이러한 분리는 보안(부모 레벨에서 암호화된 비밀), 리소스 최적화(부모 레벨에서 최소한의 리소스 사용), 그리고 확장성(자식 레벨에서 병렬 실행)을 제공합니다.</p><h2><strong>결과</strong></h2><h3><strong>성능 변환</strong></h3><ul><li><strong>이전:</strong> 한 번에 하나의 리포지토리만 처리하고, 일부 리포지토리는 하루 이상 처리되지 않기도 했으며, 하루에 1,000건 미만의 스캔이 처리되었습니다.</li><li><strong>이후:</strong> 100개 이상의 동시 스캔, 하루에 보통 8,000개 스캔, 최대 10,000개의 기록된 스캔이 이루어지며, 사용 가능한 리소스의 양과 GitHub 속도 제한을 처리하는 방법에 의해서만 제한됩니다.</li></ul><h3><strong>비용 효율성</strong></h3><p>이상하게 들릴지 모르지만, 하루에 8,000개의 포드를 실행하는 것이 동일한 결과를 달성하기 위해 하나의 장기 실행 포드를 사용하는 것보다 훨씬 저렴할 수 있습니다.</p><p>이전 설정에서는 단일 인스턴스를 운영했고, 양호한 날에는 500~600번의 스캔을 수행했습니다. 또한, 서로 다른 종류의 리포지토리가 같은 포드에서 실행되기 때문에 가장 큰 리포지토리에 맞게 포드 크기를 조절해야 했습니다. 이는 포드용 CPU 8개와 16G 메모리를 사용하는 현재보다 훨씬 규모가 커질 수 있습니다.</p><p>현재의 일일 출력을 충족하려면 단일 포드는 12일 동안 실행되어야 합니다. 12일 동안 실행되는 단일 포드의 비용을 매일 실행되는 8,000개의 “MEDIUM” 크기 포드와 비교하면, 동일한 스캔 출력에서 새로운 설계가 훨씬 더 효율적입니다.</p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">메트릭</td><td style="border: 1px solid #ccc; padding: 4px;">시나리오 A(워크플로우)</td><td style="border: 1px solid #ccc; padding: 4px;">시나리오 B(장기 실행 단일 포드)</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">설정</td><td style="border: 1px solid #ccc; padding: 4px;">포드 8,000개(1 vCPU/2GB)</td><td style="border: 1px solid #ccc; padding: 4px;">포드 1개(8 vCPU/ 16GB)*</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">기간</td><td style="border: 1px solid #ccc; padding: 4px;">각각 10분</td><td style="border: 1px solid #ccc; padding: 4px;">12일 연속</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">총 작업 시간</td><td style="border: 1px solid #ccc; padding: 4px;">1,333 컴퓨팅 시간</td><td style="border: 1px solid #ccc; padding: 4px;">288 컴퓨팅 시간</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">총 비용</td><td style="border: 1px solid #ccc; padding: 4px;">$65.83</td><td style="border: 1px solid #ccc; padding: 4px;">$113.75</td>
                    </tr>
              </tbody>
            </table>
          <p>이제 워크로드 기본값이 &quot;SMALL&quot;로 설정되어 있으며, 대다수가 0.5 CPU와 1G RAM으로 성공적으로 실행되고 소수만 중간, 대형으로 변경해야 하는 경우를 생각해 보겠습니다. 작업 부하의 60%가 &quot;SMALL&quot;, 30%가 &quot;MEDIUM&quot;, 그리고 10%가 &quot;LARGE&quot;에서 실행된다면 어떻게 되는지 살펴봅시다. 이는 실제 상황에 더 가깝습니다.</p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">메트릭</td><td style="border: 1px solid #ccc; padding: 4px;">시나리오 A(혼합 군집)</td><td style="border: 1px solid #ccc; padding: 4px;">시나리오 B(장기 실행)</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">전략</td><td style="border: 1px solid #ccc; padding: 4px;">포드 8,000개(혼합 크기)</td><td style="border: 1px solid #ccc; padding: 4px;">포드 1개(8 vCPU/ 16GB)*</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">기간</td><td style="border: 1px solid #ccc; padding: 4px;">각각 10분</td><td style="border: 1px solid #ccc; padding: 4px;">12일 연속</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">총 비용</td><td style="border: 1px solid #ccc; padding: 4px;">$52.66</td><td style="border: 1px solid #ccc; padding: 4px;">$113.75</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">비용 절감</td><td style="border: 1px solid #ccc; padding: 4px;">$61.09 (54% 더 저렴합니다)</td><td style="border: 1px solid #ccc; padding: 4px;">—</td>
                    </tr>
              </tbody>
            </table>
          <p>동일한 출력을 얻을 때 현재 설정이 훨씬 더 비용 효율적이라는 것을 알 수 있습니다.</p><h3><strong>보안 강화</strong></h3><ul><li>일시적인 GitHub 토큰(노출 시간이 일 단위가 아닌 분 단위)</li><li>역할 기반 액세스 제어(RBAC) 경계를 사용한 네임스페이스 격리</li><li>부모 워크플로우에서 저장 데이터 비밀 암호화</li><li>직접 볼트 액세스 제거</li></ul><h3><strong>예측 가능한 성능</strong></h3><p>스캔 빈도가 보장되어 마침내 서비스 수준 목표(SLO)를 설정할 수 있습니다. Automerge는 안정적으로 작동합니다. 팀은 플랫폼이 약속대로 실행될 것이라고 신뢰합니다.</p><h2><strong>주요 설계 결정 사항</strong></h2><p>다음은 플랫폼의 모습을 형성하는 데 중요한 역할을 한 몇 가지 주요 설계 결정 사항입니다.</p><ul><li><strong>부모-자식 워크플로우를 사용하는 이유는 무엇인가요?</strong></li></ul><p><strong>심층 방어</strong> 전략을 실행하기 위해 이 패턴을 채택했습니다. 중요한 자격 증명(예: GitHub 앱 비밀)을 잠긴 전용 네임스페이스로 제한함으로써 임시 실행 포드가 민감한 데이터에 임의로 액세스할 수 없도록 하기 위해 <strong>RBAC</strong>를 사용합니다. 최근의 공급망 취약성(예: <strong>&quot;Shai Hulud&quot;</strong> 지속적 통합/지속적 배포[CI/CD] 공격)은 자격 증명 저장소에서 동적 스크립트를 실행하는 런타임 환경을 격리하는 것이 얼마나 중요한지를 보여주었습니다.</p><p>동시에 이러한 분리를 통해 <strong>세분화된 리소스 최적화</strong>가 가능합니다. &quot;부모&quot; 워크플로우는 최소한의 풋프린트로 가벼운 오케스트레이터 역할을 하는 반면, &quot;자식&quot; 워크플로우는 컴퓨팅 집약적인 종속성 스캐닝을 처리합니다. 이러한 분리는 <strong>수명 주기 관리</strong>를 단순화하여 각 계층에 별개의 조정 로직을 적용할 수 있게 하므로, 사용자는 실행 매개 변수(자식)에 대한 제어권을 갖는 반면 관리자는 스케줄링 및 보안 인프라(부모)에 대한 관리 제어권을 유지할 수 있습니다.</p><ul><li><strong>셀프 서비스를 사용하는 이유는 무엇인가요?</strong></li></ul><p>팀이 리포지토리 구성의 병목 현상이 되지 않도록 하는 것은 중요한 요구 사항이었습니다. 다양한 사용 사례를 지원할 수 있는 확장 가능한 <strong>셀프 서비스 플랫폼</strong>을 구축하는 것이 목표였습니다. 우리는 엄청난 양의 리포지토리를 고려할 때, 모든 구성 변경에 대해 <strong>게이트키퍼</strong> 역할을 하는 것은 지속 불가능하다는 것을 인식했습니다. 대신, 사용자가 &quot;열차&quot;를 운전할 수 있도록(실행 및 맞춤화) &quot;레일&quot;(인프라 및 <strong>가드레일</strong>)을 제공한다는 지원 철학을 채택했습니다. 이와 같이 <strong>팀 자율성</strong>으로 전환하여 사용자가 시스템을 특정 운영 요구 사항에 맞게 조정할 수 있도록 하면 생산성이 크게 향상된다고 믿습니다.</p><ul><li><strong>Kubernetes Operator 패턴을 사용하는 이유는 무엇인가요?</strong></li></ul><p>위에서 언급했듯이, 기본적인 설계 원칙은 플랫폼이 완전히 <strong>셀프 서비스 가능</strong>하도록 하는 것이었습니다. 사용자 의도(스캔 전환, 일정 주기 조정, 런타임 리소스 제한 조정 등)를 파악하고 변경 사항을 기본 워크플로우에 즉시 전파하려면 자동화된 메커니즘이 필요했습니다. 또한 향후 요구 사항을 예상하여 시스템은 쉽게 <strong>확장 가능</strong>해야 했습니다.</p><p>이를 위해 맞춤형 <strong>종속성 관리 Kubernetes Operator</strong>를 개발했습니다. <strong>CRD</strong>를 구성 인터페이스로 사용함으로써, <strong>Kubernetes 네이티브 조정 루프</strong>를 구축했습니다. 이 오퍼레이터는 사용자가 정의한 원하는 상태를 지속적으로 모니터링하고, 워크플로우 인프라에 필요한 업데이트를 자동으로 오케스트레이션합니다. 이를 통해 플랫폼 로직이 모든 복잡한 작업을 백그라운드에서 처리하므로 <strong>이벤트 기반</strong>의 원활한 작동이 보장됩니다.</p><ul><li><strong>GitHub Events Gateway를 설계하는 이유는 무엇인가요?</strong></li></ul><p>플랫폼의 응답성을 위해 <strong>이벤트 기반 아키텍처 (EDA)</strong> 채택이 필수적이었습니다. CronWorkflows는 신뢰할 수 있는 기준 일정을 제공했지만, 사용자가 대시보드를 통해 수동으로 스캔을 트리거하는 등 <strong>임시 실행</strong>을 처리할 수 있는 민첩성이 필요했습니다. 이를 위해서는 페이로드 무결성을 검증하고 요청을 지능적으로 라우팅할 전용 <strong>수집 게이트웨이</strong>가 필요했습니다.</p><p>우리는 Argo용 기본 GitHub EventSource를 포함한 기존 솔루션을 평가했지만, <strong>운영 오버헤드</strong> 및 엄격한 <strong>GitHub API 할당량</strong>(예: 리포지토리당 웹훅 제한)과 관련하여 상당한 위험이 있음을 확인했습니다. 결과적으로 이러한 제한으로부터 인프라를 분리하기 위해 맞춤형 게이트웨이를 구축했습니다.</p><p>무엇보다도, 이 게이트웨이는 마이그레이션 중에 전략적인 <strong>트래픽 제어 지점</strong> 역할을 했습니다. 이는 기존 시스템에서 새로운 인프라로 <strong>점진적이고 세분화된 배포</strong>(트래픽 전환)를 수행할 수 있도록 스위치 역할을 했습니다. 이를 통해 수천 개의 리포지토리를 온보딩하는 과정이 &quot;빅뱅&quot; 전환 방식이 아닌 통제되고 위험 없는 프로세스로 수행되었습니다.</p><p></p><h2><strong>얻은 교훈</strong></h2><p>우리가 얻은 몇 가지 교훈은 <a href="https://www.elastic.co/about/our-source-code">Elastic 소스 코드</a>와 밀접한 관련이 있습니다.</p><ol><li><strong>고객 우선: </strong>플랫폼은 사용자를 위해 구축됩니다. 따라서 사용자의 요구를 최우선으로 고려하는 것이 중요합니다. 이를 통해 플랫폼을 효율적으로 설계된 인프라와 애플리케이션으로 형성하여 사용자와의 마찰을 줄이고, 플랫의 확장을 단순화하며 원활한 도입일 촉진할 수 있습니다.</li><li><strong>공간, 시간: </strong>저항이 가장 적은 경로는 때때로 <strong>불안정한 상황</strong>으로 이어지곤 합니다. 처음에는 기존의 순차적 처리 모델을 최적화하려고 했지만, 이것으로 문제를 해결할 수 없었습니다. 오히려 더 많은 복잡성과 미해결 문제를 초래했습니다. 병렬 처리로 플랫폼을 <strong>재설계</strong>하기로 한 과감한 결정은 상당한 사전 노력을 필요로 했습니다. 하지만 결과적으로 플랫폼의 지속 가능한 성장을 위한 길을 열었고, 지루한 일상적인 관리 업무를 사실상 제거했습니다.</li><li><strong>IT, 종속성: </strong>플랫폼은 단독으로 운영될 수 없습니다. 플랫폼의 성공은 더 넓은 에코시스템과 얼마나 잘 통합되는지에 달려 있습니다. 이 사례에서는 <strong>Backstage</strong>와의 통합이 매우 중요했습니다. 원활한 서비스 온보딩을 위한 정보 소스 역할을 하기 때문입니다. 마찬가지로 <strong>Artifactory</strong>에 연결하여 개인 패키지 업데이트를 효율적으로 관리할 수 있게 되었습니다. 그 외에도 필수 통합 목록은 계속 이어집니다.</li><li><strong>발전, 단순한 완벽함: </strong>구현 과정 전반에 걸쳐 초기 가정에 대해 지속적으로 엄격한 테스트를 수행하고 새로운 장벽이 나타나면 이에 맞게 조정했습니다. 완벽주의에 빠지기보다는 <strong>반복 접근 방식</strong>을 채택하여 과제를 하나씩 해결하고 실제 환경에 맞게 마이그레이션 전략을 조정했습니다.</li></ol><h2><strong>그럼 이제 무엇을 해야 할까요?</strong></h2><p>플랫폼 제공은 더 의미 있는 작업을 가능하게 하여 플랫폼의 UX와 효율성을 향상시키는 데 도움이 됩니다. 몇 가지 예는 다음과 같습니다.<br/></p><ul><li><strong>자동 병합 도입을 늘리고 가드레일 구축</strong></li></ul><p>자동 병합 기능은 지루한 수동 작업을 없애 팀의 작업 속도를 크게 향상시킵니다. 하지만 속도 향상으로 인해 보안이 훼손되지 않도록 엄격한 <strong>가드레일</strong>을 마련해야 합니다.<br/></p><ul><li><strong>최종 사용자 경험에 대한 통합 가시성 향상</strong></li></ul><p>로드맵의 중요한 우선순위는 플랫폼 수준뿐만 아니라 특히 <strong>최종 사용자의 관점</strong>에서 통합 가시성을 강화하는 것입니다. 인프라 메트릭을 파악하는 것은 간단하지만, 실제 사용자 경험을 이해하려면 더 심층적인 인사이트가 필요합니다. 우리는 사용자 불만으로 이어지기 <strong>전에</strong> 원격 측정 데이터를 통해 문제점과 성능 문제를 탐지할 수 있도록 사용자 중심의 핵심 성과 지표(KPI)를 정의하고 있습니다.</p><ul><li><strong>장벽을 제거하여 도입 촉진</strong></li></ul><p>앞으로 최우선 과제는 플랫폼 도입을 방해하는 모든 장벽을 파악하고 제거하는 것입니다. 새로운 통합 기능을 개발하든 특정 기능 세트를 배포하든, 데이터 기반 계획 수립에 전념합니다. 확장성을 고려하여 설계된 플랫폼을 성공적으로 구축했으며, 이제 <strong>그 잠재력을 극대화</strong>하는 데 집중하고 있습니다.<br/></p><h2><strong>더 큰 그림</strong></h2><p>종속성 관리 워크플로우 프로젝트는 더 광범위한 원칙을 제시합니다. <strong>오픈 소스 도구를 기본 배포 모델 이상으로 확장해야 할 때 Kubernetes 네이티브 패턴이 해결책을 제시한다는 것입니다</strong>.</p><p>수용함으로써:</p><ul><li>구성용 CRD</li><li>수명 주기 관리를 위한 오퍼레이터</li><li>응답성을 위한 이벤트 중심 아키텍처</li><li>배포용 GitOps</li></ul><p>관리하는 리포지토리 수와 관계없이 확장 가능한 오케스트레이션을 구축했습니다. 하나의 리포지토리를 스캔하는 성능은 100개를 관리하든 1,000개를 관리하든 동일합니다.</p><p>중요한 CVE가 발표되면 이제 몇 시간이 아니라 몇 분 만에 답변을 받을 수 있습니다. 이것이 병목 현상과 경쟁 우위의 차이입니다.</p><h2><strong>감사의 말씀</strong></h2><p>이 플랫폼은 뛰어난 오픈 소스 도구를 기반으로 구축되었습니다.</p><ul><li><strong>Kubebuilder:</strong> 워크플로우를 부트스트랩하고 오케스트레이션하는 Kubernetes Operator를 시작하는 데 사용한 오픈 소스 프레임워크입니다. [<a href="https://github.com/kubernetes-sigs/kubebuilder">1</a>][<a href="https://book.kubebuilder.io/">2</a>]</li><li><strong>Backstage:</strong> 서비스 카탈로그를 구축하는 데 신뢰할 수 있는 정보 소스로 사용한 오픈 소스 프레임워크입니다. [<a href="https://github.com/backstage/backstage">1</a>][<a href="https://backstage.io/">2</a>]</li><li><strong>Argo Workflows 및 Argo Events:</strong> 복잡한 프로세스를 오케스트레이션하고 이벤트에 기반한 동적 처리를 추가하는 데 사용한 오픈 소스 제품군입니다. [<span class="unknown__pt__mark__1e5711660421">1</span>][<a href="https://argo-workflows.readthedocs.io/en/stable/">2</a>][<a href="https://argoproj.github.io/argo-events/">3</a>][<a href="https://github.com/argoproj/argo-events">4</a>]</li><li><strong>Renovate CLI:</strong> 리포지토리를 처리하는 오픈 소스 종속성 관리 도구입니다. [<a href="https://github.com/renovatebot/renovate">1</a>][<a href="https://docs.renovatebot.com/getting-started/running/">2</a>]</li></ul><p>* AWS Fargate 가격 모델이 단일 포드의 비용을 참조하는 데 사용되었지만, 워크로드가 반드시 AWS에서 실행되는 것은 아니며 완전한 Kubernetes 클러스터에서 실행되고 있습니다.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA['best_compression'으로 검색 성능 개선]]></title>
            <description><![CDATA['best_compression'은 일반적으로 Elastic Observability 및 Elastic Security 사용 사례에서 저장 공간을 절약하는 기능으로 간주되지만, 이 블로그에서는 검색의 성능 조정 수단으로써 그 효과를 보여줍니다.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/improve-elasticsearch-performance-best-compression</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/improve-elasticsearch-performance-best-compression</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Sherry Ger, Ryan Eno]]></dc:creator>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/5141a8c2618337207d848ce16b258a86885955b2-1600x1034.jpg?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p></p><p>동시성이 높은 워크로드에 맞게 Elasticsearch를 조정할 때 일반적인 접근 방식은 검색 지연 시간을 줄이기 위해 작업 문서 세트를 메모리에 유지하도록 RAM을 최대화하는 것입니다. 따라서 <a href="https://www.elastic.co/docs/reference/elasticsearch/index-settings/index-modules"><code>best_compression</code></a> 은(는) 주로 저장 공간 효율성이 우선시되는 Elastic Observability와 Elastic Security 사용 사례에서 저장 공간 절약 수단으로 간주되며, 검색 워크로드에는 거의 고려되지 않습니다.</p><p>이 블로그에서는 데이터 세트 크기가 OS 페이지 캐시를 크게 초과하는 경우, <code>best_compression</code> 이(가) I/O 병목 현상을 줄여 검색 성능과 리소스 효율성을 개선하는 방법을 보여줍니다.</p><h2><strong>설정</strong></h2><p>해당 사용 사례는 <a href="https://www.elastic.co/docs/deploy-manage/deploy/elastic-cloud/ec-change-hardware-profile#ec-profiles-compute-optimized-arm">Elastic Cloud CPU에 최적화된 인스턴스</a>에서 실행되는 동시성이 높은 검색 애플리케이션입니다.</p><ul><li>데이터 볼륨: 약 5억 개의 문서</li><li>인프라: 6개의 Elastic Cloud(Elasticsearch Service) 인스턴스(각 인스턴스: 1.76TB 저장 공간 | 60GB RAM | 31.9 vCPU)</li><li>메모리 대 저장 공간 비율: 전체 데이터 세트의 약 5%를 RAM에 저장</li></ul><h2><strong>증상: 긴 지연 시간</strong></h2><p>19시경에 현재 요청 수가 급증할 때 검색 지연 시간이 크게 악화되는 것을 확인했습니다. 그림 1과 그림 2에서 볼 수 있듯이, Elasticsearch 인스턴스당 분당 약 400건의 요청이 발생하는 최고 트래픽 수준에서 평균 쿼리 서비스 시간은 60ms 이상으로 저하되었습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/f9c6cc1882e7db24336c65c54bbc1d38dcdb7fa3-697x311.png" style="width: 100%;" /><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/bbb705ae2fcd14c81d335bf322346caf3bf33765-996x618.png" style="width: 100%;" /><p>초기 연결 처리 이후 CPU 사용량은 비교적 낮은 수준을 유지했는데, 이는 컴퓨팅이 병목 현상이 아님을 나타냅니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/cec15a28d2d22e9adedd2951bb2334b3717890a1-1494x730.png" style="width: 100%;" /><p>쿼리 볼륨과 페이지 오류 사이에 강한 상관관계가 나타났습니다. 요청량이 증가함에 따라 페이지 오류도 비례적으로 증가하여 분당 약 40만 건에 달하는 최고치를 기록했습니다. 이는 활성 데이터 세트가 페이지 캐시에 저장될 수 없음을 의미합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/511f2f10300a9d10ba3d7a82b9a8c8d567ac5636-1492x678.png" style="width: 100%;" /><p>동시에 JVM 힙 사용량은 정상적인 수준을 보였습니다. 이는 가비지 컬렉션 문제가 아님을 시사하며, 병목 현상이 I/O에 있음을 확인시켜 줍니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/336bbad638f866304358dba1d06ee987de0f23cf-1490x568.png" style="width: 100%;" /><h2><strong>진단: I/O 바운드</strong></h2><p>시스템이 I/O 바운드 상태였습니다. <a href="https://www.elastic.co/blog/elasticsearch-caching-deep-dive-boosting-query-speed-one-cache-at-a-time">Elasticsearch는 OS 페이지 캐시를 활용하여 메모리에서 인덱스 데이터를 제공합니다</a>. 인덱스가 캐시에 비해 너무 크면 쿼리는 값비싼 디스크 읽기를 트리거합니다. 일반적인 해결책은 수평적 확장(노드/RAM 추가)이지만, 먼저 기존 리소스를 활용하여 효율성을 개선하고자 했습니다.</p><h2><strong>수정 사항</strong></h2><p>기본적으로 Elasticsearch는 인덱스 세그먼트에 <a href="https://en.wikipedia.org/wiki/LZ4_(compression_algorithm)">LZ4</a> 압축을 사용하여 속도와 크기 사이의 균형을 유지합니다. <code>best_compression</code> ( <a href="https://en.wikipedia.org/wiki/Zstd">zstd</a> 방식 사용)로 전환하면 인덱스 크기가 줄어들 것이라는 가설을 세웠습니다. 설치 공간이 작아지면 페이지 캐시에 더 많은 비율의 인덱스가 들어갈 수 있으므로 압축 해제에 필요한 CPU 사용량의 미미한 증가와 디스크 I/O의 감소를 맞바꿀 수 있습니다.</p><p><code>best_compression</code> 을(를) 활성화하기 위해 인덱스 설정 <code>index.codec: best_compression</code> (으)로 데이터를 다시 색인했습니다. 또는 인덱스를 닫고 인덱스 코덱을 <code>best_compression</code> (으)로 재설정한 다음 세그먼트 병합을 수행해도 동일한 결과를 얻을 수 있습니다.</p><pre><code>POST my-index/_close
PUT my-index/_settings
{
    "codec": "best_compression"
}
  
POST my-index/_open  
POST my-index/_forcemerge?max_num_segments=1</code></pre><h2><strong>결과</strong></h2><p>결과는 가설을 뒷받침했습니다. 저장 공간 효율성 개선은 CPU 사용률의 증가 없이 검색 성능의 상당한 향상으로 직결되었습니다.</p><p><code>best_compression</code> 을(를) 적용함으로써 인덱스 크기가 약 25% 감소했습니다. 반복적인 로그 데이터에서 나타난 감소율보다는 낮지만, 이 25% 감소는 페이지 캐시 용량을 동일한 수준으로 실질적으로 증가시켰습니다.</p><p>다음 로드 테스트(17시 시작) 동안 트래픽은 훨씬 더 높아져 Elasticsearch 노드당 분당 최대 500건의 요청을 기록했습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/fc1902f396cb2115c0013155ad07f6eb87389c60-660x309.png" style="width: 100%;" /><p>로드가 높아졌음에도 CPU 사용률은 이전 실행보다 낮았습니다. 이전 테스트에서 사용률이 높았던 것은 과도한 페이지 오류 처리 및 디스크 I/O 관리로 인한 오버헤드 때문이었을 가능성이 큽니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/15699ef4c65b3f0a9f8a3e1bae8bb18f7b647025-819x352.png" style="width: 100%;" /><p>결정적으로 페이지 오류가 많이 감소했습니다. 처리량이 높아진 상황에서도 오류 수는 분당 20만 건 미만으로, 기준 테스트의 30만 건 이상에 비해 현저히 줄어들었습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/f76ca967976d740af88a9359b66041701abb46fc-764x340.png" style="width: 100%;" /><p>페이지 오류 결과는 여전히 최적에 미치지 못했지만, 쿼리 서비스 시간은 약 50% 단축되었습니다. 로드가 많은 상황에서도 30ms 미만으로 유지되었습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/750ec1c59b8eb5069aed4c066d856ecea82d5bca-620x311.png" style="width: 100%;" /><p></p><h2><strong>결론: 검색을 위한 best_compression</strong></h2><p>데이터 볼륨이 사용 가능한 물리적 메모리를 초과하는 검색 사용 사례에서 <code>best_compression</code>은(는) 강력한 성능 조정 수단입니다.</p><p>캐시 미스를 해결하는 일반적인 해결책은 RAM을 늘려 확장하는 것입니다. 하지만 인덱스 공간을 줄임으로써 페이지 캐시의 문서 수를 최대화하는 동일한 목표를 달성했습니다. 다음 단계에서는 <a href="https://www.elastic.co/blog/space-savings-a-lesser-known-benefit-of-index-sorting-in-elasticsearch"><strong>인덱스 정렬</strong></a>을 통해 저장 공간 최적화를 더 강화하고 기존 리소스에서 성능을 극대화할 것입니다.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Agent Builder 정식 출시: 컨텍스트 기반 에이전트, 몇 분 만에 구축 가능]]></title>
            <description><![CDATA[Agent Builder가 이제 정식 출시되었습니다. 컨텍스트 기반 AI 에이전트를 신속하게 개발할 수 있는 방법을 알아보세요.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/agent-builder-elastic-ga</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/agent-builder-elastic-ga</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Anish Mathur, Evan Castle]]></dc:creator>
            <pubDate>Thu, 22 Jan 2026 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/6840eb7dbb884055ab0e965dcfd614fec54936af-2210x1440.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>Elastic Cloud Serverless 및 곧 출시될 9.3 릴리스에서 Agent Builder가 정식 출시됨을 기쁜 마음으로 알려 드립니다. Agent Builder는 Elasticsearch의 역량을 컨텍스트 엔지니어링 플랫폼으로 활용하여 컨텍스트에 중점을 둔 데이터 중심의 AI 에이전트를 신속하게 개발할 수 있도록 합니다.</p><p>에이전트는 효율성 향상과 더 나은 고객 경험을 제공할 수 있는 잠재력으로 인해 주목받고 있습니다. 그러나 실제로는 에이전트에게 적절한 컨텍스트를 제공하기가 쉽지 않으며, 특히 복잡하고 비정형적인 엔터프라이즈 데이터를 처리할 때는 더욱 그렇습니다. 개발자는 도구, 프롬프트, 상태, 추론 논리, 모델을 관리해야 하며, 특히 비즈니스 소스에서 관련 컨텍스트를 검색하여 정확한 결과와 작업을 제공해야 합니다. Elastic Agent Builder는 안전하고 신뢰할 수 있는 컨텍스트 기반 에이전트를 개발하기 위한 핵심 구성 요소를 제공합니다.</p><h2>Agent Builder의 핵심 기능</h2><p>Agent Builder는 검색 정확도와 검색 증강 생성 분야에 대한 Elastic의 지속적인 투자와 Elasticsearch를 최적의 벡터 데이터베이스로 만들기 위한 노력을 통해, 맥락적이고 데이터에 초점을 둔 AI 에이전트 개발을 단순화합니다.</p><p>Agent Builder로 할 수 있는 일은 다음과 같습니다.</p><ul><li>질문에 답하고, 분석을 수행하며, Elasticsearch에 있는 모든 데이터를 기반으로 조사까지 진행할 수 있는 기본 제공 대화형 에이전트를 즉시 시작할 수 있습니다.</li><li>복잡하고 비정형적인 데이터에서 출발해, 설정 기반의 개발 환경을 갖춘 사용자 정의 에이전트로 신속히 전환할 수 있습니다.</li><li>기본 제공 ES|QL이나 사용자 정의 도구를 활용해 업계 최상급의 하이브리드 검색 정확도를 적용함으로써, 컨텍스트의 품질과 에이전트의 안정성을 높일 수 있습니다.</li><li>복잡한 워크플로우를 재사용 가능한 도구로 실행하여 데이터 강화, 기록 갱신, 메시지 발송 등 다양한 규칙 기반 자동화를 구현할 수 있습니다(미리보기 버전).</li><li>워크플로우 및 MCP를 사용하여 Elasticsearch 외부의 데이터 소스에 연결하고 에이전트에 대한 컨텍스트를 상호 연관시키고 결합할 수 있습니다.</li><li>MCP 기반의 기본 제공·사용자 정의 도구를 통해 다양한 에이전틱 및 애플리케이션 프레임워크와 연동할 수 있고, 외부 MCP 연결(미리보기 버전), A2A 지원, 완전한 API 지원을 제공합니다.</li><li>Agent Builder의 기능을 더욱 확장하려면 복잡한 문서 처리에는 LlamaIndex를, 보안성과 구조화를 갖춘 도구 액세스에는 Arcade.dev를 연동할 수 있습니다.</li></ul><p>Agent Builder의 기능성을 한층 강화하고자, 새로운 규칙 기반 자동화 기능인 Elastic Workflows를 기술 미리보기로 선보입니다. 조직 차원의 작업에서 에이전트는 특정 비즈니스 로직을 구현하기 위해 규칙 기반 조치의 확실성과 신뢰성이 필요한 경우가 종종 있습니다. Elastic Workflows는 에이전트가 내부·외부 시스템을 오케스트레이션해 조치를 실행하고, 데이터와 컨텍스트를 수집 및 가공할 수 있는 단순한 선언형 방법을 제공합니다. 워크플로는 완벽하게 조합 가능하고, 이벤트 중심 구조와 높은 유연성을 갖추고 있으며, MCP를 통해 에이전트에 도구로 노출될 수 있습니다.</p><h2>데이터에서 에이전트로 단 몇 분 만에 전환</h2><p>에이전트 개발은 분산된 데이터 저장소를 통합하고, 수동 파이프라인을 구축하며, 쿼리를 튜닝하고, 복잡한 오케스트레이션을 관리해야 해 초기 단계에서 몇 주가 걸리기도 합니다. Agent Builder는 별도의 데이터 저장소, 벡터 데이터베이스, RAG 파이프라인, 검색 계층, 쿼리 변환기, 도구 오케스트레이터가 필요 없도록 해 주어 에이전트 개발 시간을 단축하고, 에이전트 로직과 애플리케이션 제공에 집중할 수 있게 합니다.</p><p>Agent Builder는 Elasticsearch 플랫폼의 기본 구성 요소를 네이티브로 통합해 에이전트 개발을 빠르게 만듭니다.</p><ul><li>인덱싱된 데이터를 대상으로 즉시 대화하고 추론할 수 있는 기본 제공 대화형 에이전트를 통해 바로 시작할 수 있습니다.</li><li>Kibana, API, MCP, A2A를 통한 대화형 접근을 활용해 에이전트를 애플리케이션, 대시보드 또는 CI/CD 시스템에 통합할 수 있습니다.</li><li>기본 제공 도구를 활용해 데이터 구조를 이해하고, 적절한 인덱스를 선택하며, 최적화된 하이브리드·시맨틱·구조화 쿼리를 생성하고, 자연어 프롬프트를 기반으로 ES|QL을 사용해 시각화를 유연하게 구성할 수 있습니다.</li></ul><p>더 깊이 살펴보려면 전 과정의 <a href="https://www.elastic.co/search-labs/blog/ai-agent-builder-elasticsearch">실습형 단계별 안내를</a> 활용해 보세요.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/b55b63eae40f72952967cc8f3ea4df4cd62d7d70-1080x608.gif" style="width: 100%;" /><h2>컨텍스트 엔지니어링을 위한 완벽한 데이터 플랫폼인 Elasticsearch 기반 구축</h2><p>AI 에이전트의 경우, 효과적인 추론을 제공하고 환각의 위험을 줄이기 위해서는 컨텍스트의 품질이 매우 중요합니다. 많은 기업용 AI 에이전트에게 있어 작업을 수행하는 데 필요한 비즈니스 데이터는 가장 중요한 컨텍스트 정보입니다. 대규모 확장이 가능한 데이터 저장소이자 벡터 데이터베이스이며 검색 정확도 분야의 선두 주자인 Elasticsearch는 이미 강력한 컨텍스트 엔지니어링 핵심 기능을 다수 제공하고 있습니다. 컨텍스트 엔지니어링은 단순한 검색 증강 생성을 넘어, 데이터 조회·순위화·필터링·표현 방식을 조정 및 확장할 수 있게 하여 에이전트가 받는 잡음과 모호함을 줄이는 데 도움이 됩니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/419b9b6f13739e0a8983249d8ac31478e73dac89-1600x901.png" style="width: 100%;" /><p>Elasticsearch는 어휘 검색, 벡터 검색, 구조화 필터링을 결합한 컨텍스트 엔진을 제공하여 모델이 관련성 높고 정확한 컨텍스트에서 동작하도록 보장해 실질적으로 <a href="https://www.elastic.co/search-labs/blog/context-engineering-relevance-ai-agents-elasticsearch">LLM 성능을 향상시킵니다</a>. 이 기능은 에이전트형 검색을 기반으로 하며, 적절한 인덱스를 자동으로 선택하고 자연어를 컨텍스트에 최적화된 쿼리로 변환하는 기본 제공 도구와 검색 로직이 이를 지원합니다.</p><p>Agent Builder를 사용하면 정확도와 순위화를 제어하는 기능을 통해 에이전트가 가장 유용한 컨텍스트를 우선적으로 받도록 할 수 있으며, 점수 매기기·순위화·필터링 로직을 세밀하게 조정할 수 있습니다. Elasticsearch는 불명확한 검색 동작에 의존하는 대신, 무엇이 중요한지, 왜 중요한지, 그리고 어떻게 우선순위를 둘지를 직접 제어할 수 있게 합니다. 이 모든 것은 텍스트, 벡터, 메타데이터, 로그 등 모든 데이터를 하나의 플랫폼에서 저장·확장할 수 있는 확장형 데이터 플랫폼인 Elasticsearch를 기반으로 하여, 에이전트용 컨텍스트 관리를 더욱 용이하게 합니다.</p><h2>재사용 가능한 도구로 복잡한 워크플로우 실행</h2><p>AI 에이전트가 복잡한 작업에서 추론을 수행하더라도, 자동화의 상당 부분은 특정 비즈니스 로직을 적용하는 규칙 기반 조치를 안정적으로 실행하는 데 의존합니다. Elastic Workflows는 내부 및 외부 시스템을 오케스트레이션하여 작업을 실행하고 컨텍스트 또는 데이터를 수집한 뒤, 이를 에이전트에 통합할 수 있도록 하는 단순한 선언형 방법을 제공합니다. 워크플로우는 YAML로 정의되며, 완전한 조합 구조를 지원하여 작업 성격에 따라 단순하게도, 복잡하게도 구성할 수 있습니다. 이를 통해 에이전트는 Elasticsearch 플랫폼과 다양한 솔루션, 나아가 서드파티 애플리케이션까지 아우르며 효율적으로 동작할 수 있습니다.</p><p>워크플로우를 Agent Builder와 통합하는 과정은 세 단계로 진행할 수 있습니다(전제 조건: <a href="https://github.com/elastic/workflows">여기</a> 안내된 자세한 내용에 따라 워크플로우 활성화).</p><p>1. 기본 제공 자동 완성 및 테스트 기능을 갖춘 간단한 YAML 기반 편집기를 사용하여 새 워크플로우를 생성하고 저장합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/308888bf3d2fa013f9391a55be6a6fbd458b6dac-1600x998.png" style="width: 100%;" /><p>2. Agent Builder에서 “워크플로우” 타입의 새 도구를 생성한 뒤, 에이전트가 해당 워크플로우 도구를 사용할 시점을 결정하는 데 도움이 되도록 설명을 입력합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/c04810d30d226112c3610bd58e208607b213fc3d-1600x945.png" style="width: 100%;" /><p>3. 사용자 정의 에이전트에 워크플로우 도구를 추가합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/724cd4ac93c46efb0d339fd140e5caf138f8150f-1600x948.png" style="width: 100%;" /><p>4. 끝입니다! 이제 에이전트가 대화 도중에 워크플로우를 호출할 수 있습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/8dfdd726ab89e31c48b79372650ce33946713dca-1600x929.png" style="width: 100%;" /><h2>나만의 에이전트, 나만의 규칙</h2><p>Agent Builder는 하나의 개발 패러다임만을 강요하지 않습니다. 대신 데이터, 정확도, 모델, 상호 운용성, 보안, 에이전트 설계 전반을 완전히 제어할 수 있는 개방적이고 유연한 에이전트 개발 방식을 지원하도록 설계되었습니다.</p><p>사용자 지정 에이전트 정의를 사용하면 에이전트가 사용할 수 있는 도구를 명확히 지정하고, 사용자 지정 시스템 프롬프트를 삽입하며, 에이전트 지침을 세밀하게 조정하고 보안 범위를 설정할 수 있습니다. 에이전트는 특정 모델에 종속되지 않으므로, 하나의 제공업체에 의존하지 않고 기본 제공 모델부터 외부 에코시스템의 LLM까지 원하는 대로 자유롭게 설정할 수 있습니다.</p><p>도메인별 로직(예: 특정 인덱스 필터, ES|QL 조인, 분석 파이프라인)을 캡슐화한 확장형 도구를 구축하고, 프로덕션 환경에서 안전하게 사용하도록 제약을 설정할 수 있습니다. 완전한 API 지원으로 다른 에이전트형 프레임워크와의 상호 운용이 가능하며, 모델 컨텍스트 프로토콜(MCP)을 기본적으로 지원합니다. A2A 통합을 통해 Elastic 에이전트를 다른 프레임워크, 서비스, 클라이언트 앱에 노출할 수 있으며, 동일한 데이터와 컨텍스트 엔지니어링 로직을 여러 통합 환경에서 재사용할 수 있습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/5e903ba24ffb3f40231e901f63bd494c89cb7757-1600x1004.png" style="width: 100%;" /><p>Agent Builder는 유연하고 개방적인 개발을 지원하며, 널리 사용되는 에이전트 프레임워크 및 플랫폼과 쉽게 통합되도록 설계되었습니다. 효과적인 에이전트를 제공하려면 이러한 통합이 필수적인 요소가 될 수 있습니다. <strong>Arcade.dev의 공동 창립자인 Sam Partee</strong>의 말처럼,</p><p><em>&quot;오늘날 에이전트형 시스템이 실패하는 이유는 AI를 도구와 데이터에 연결하는 과정이 복잡하기 때문입니다. Arcade.dev와 연동된 Elastic Agent Builder는 에이전트의 컨텍스트 조회, 추론, 실행 과정을 개발자가 체계적이고 안전하게 관리할 수 있는 방식을 제공하여, 데모 수준의 에이전트를 프로덕션 등급으로 끌어올릴 수 있게 합니다.</em></p><p>Agent Builder는 복잡한 데이터 처리를 위해 Elasticsearch의 확장성을 활용하기도 합니다. <strong>LlamaIndex CEO인 Jerry Liu</strong>의 말처럼,</p><p><em>&quot;비정형 데이터 소스에서 엔터프라이즈 컨텍스트를 끌어내는 것이 효과적인 에이전트를 구축하는 데 있어 핵심입니다. LlamaIndex의 복잡한 문서 처리 기능과 결합된 Elastic Agent Builder는 핵심 컨텍스트 계층을 강화해, 팀이 데이터를 검색·가공·준비할 수 있도록 지원하며 에이전트가 보다 정확하게 추론하고 더 나은 결과를 제공하도록 합니다.</em></p><h2>무엇을 구축할 수 있나요?</h2><p>Agent Builder는 이미 다양한 사용 사례에 사용되고 있습니다. 아래에는 에이전트 개발을 시작하는 데 참고할 수 있는 몇 가지 예시와 아키텍처가 소개되어 있습니다.</p><ul><li><strong>인프라 자동화: </strong>지원 업무 상황에서 에이전트는 읽고, 생각하고, 대화하는 데 활용되어 왔지만, 지금까지는 실제로 관리 대상인 인프라에 직접 접근해 제어하지는 못했습니다. Elastic의 엔지니어링 팀은 해커톤의 일환으로 <a href="https://www.elastic.co/search-labs/blog/agent-builder-augmented-infrastructure">자동화된 인프라 관리</a>를 위한 에이전트를 구축했습니다. 이 에이전트는 애플리케이션 인프라 문제를 적극적으로 조사하고 자동화된 조치를 취합니다. 인프라 로그를 지능적으로 이해한 결과를 바탕으로 워크플로우를 사용하여 설정을 최적화하고, 문제에 대응하며, 리소스를 확장합니다.</li><li><strong>보안 위협 분석: </strong>Elastic Agent Builder, MCP, Elasticsearch를 사용하여 보안 취약점 에이전트가 개발되었습니다. 내부 보안 데이터와 외부 위협 인텔리전스를 연관시켜 위협 분석을 자동화합니다. 이 에이전트는 과거 인시던트 및 설정 정보를 대상으로 시맨틱 검색을 수행하고, 실시간 인터넷 데이터로 결과를 강화한 뒤 LLM 추론을 적용해 환경적 관련성을 평가하고 위험 우선순위를 정하며 실행 가능한 대응 방안을 도출합니다. <a href="https://www.elastic.co/search-labs/blog/agent-builder-mcp-reference-architecture-elasticsearch">참조 아키텍처</a><strong>를 확인하세요</strong>.</li><li><strong>기술 고객 지원: </strong>에이전트는 사례 요약부터 문제 중복 정리와 신규 문제 생성, 고급 기술 분석까지 여러 지원 작업을 처리할 수 있습니다. Agent Builder는 다단계 하이브리드 검색을 통해 가장 관련성 높은 문제와 해결책, 절차만을 찾아내고, 근본 원인 가설과 조치 계획을 수립할 수 있도록 지원합니다. Agent Builder는 복잡한 <a href="https://www.elastic.co/blog/generative-ai-customer-support-elastic-support-assistant">지원 시스템</a>의 아키텍처를 단순화하고 제공까지 걸리는 시간을 단축할 수 있습니다.</li><li><strong>제품 및 콘텐츠 탐색:</strong> Agent Builder는 <a href="https://www.elastic.co/search-labs/blog/build-voice-agents-elastic-agent-builder">대화형 환경에서 복잡한 제품 카탈로그를 공개하는</a> 과정을 간소화하는 동시에, 조직이 자체 비즈니스 로직과 요구 사항을 유연하게 반영할 수 있도록 합니다.</li><li><strong>직접 구축:</strong> 2026년 1월 22일부터 2월 27일까지 진행되는 <a href="https://elasticsearch.devpost.com/">Agent Builder 해커톤</a>에 참여하세요. 커뮤니티와 협력하여 검색, 워크플로우, 도구, 추론을 통합한 컨텍스트 중심의 다단계 AI 에이전트를 만들고, 현실 세계의 작업을 자동화할 수 있습니다*</li></ul><h2>지금 바로 맞춤형 에이전트를 구축해 보세요</h2><p><a href="https://cloud.elastic.co/registration?onboarding_token=search&amp;pg=en-enterprise-search-page">Elastic Cloud 체험판</a>으로 시작하고, 설명서를 <a href="https://www.elastic.co/docs/solutions/search/elastic-agent-builder">여기</a>에서 확인해 보세요. 기존 고객의 경우, Agent Builder는 Cloud Serverless와 Elastic Cloud Hosted, 자체 관리형의 Enterprise 티어에서 사용할 수 있습니다.</p><p>* 해커톤의 전체 약관, 조건, 참가 자격 요건을 확인하려면 <a href="https://elasticsearch.devpost.com/rules">여기를 클릭하세요</a></p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Elastic Agent Builder를 사용해 음성 에이전트 구축]]></title>
            <description><![CDATA[음성 에이전트의 작동 방식과 Elastic Agent Builder 및 LiveKit을 사용하여 음성 에이전트를 구축하는 방법을 살펴보세요.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/build-voice-agents-elastic-agent-builder</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/build-voice-agents-elastic-agent-builder</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Kenneth Kreindler]]></dc:creator>
            <pubDate>Thu, 22 Jan 2026 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/43ceabb9e2c0966261c188bd40e03178d5a91e5c-1280x720.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>AI는 유리 상자 안에 갇혀 있습니다. 사용자가 명령어를 입력하면 텍스트로 답할 뿐이었습니다. 유용하긴 했지만, 화면 너머 움직이는 누군가를 지켜보는 것처럼 거리감이 느껴졌습니다. 하지만 2026년인 올해, 비즈니스는 이 유리창을 깨고 AI 에이전트를 제품 속으로 직접 끌어들여 진정한 가치를 창출하는 해가 될 것입니다.</p><p>그 유리 상자를 유리 깨뜨리는 방법 중 하나는 <em>음성 에이전트</em>의 도입입니다. 음성 에이전트는 사람의 음성을 인식하고 컴퓨터로 생성된 오디오를 합성하는 AI 에이전트를 말합니다. 저지연 전사, 빠른 대규모 언어 모델(LLM), 그리고 사람이 말하는 것처럼 들리는 텍스트-음성 변환 모델의 등장으로 이러한 것이 가능해졌습니다.</p><p>음성 에이전트가 진정으로 가치 있는 존재가 되려면 비즈니스 데이터에 접근할 수 있어야 합니다. 이 블로그에서는 음성 에이전트가 어떻게 작동하는지 알아보고 <a href="https://livekit.io/">LiveKit</a>과 <a href="https://www.elastic.co/elasticsearch/agent-builder">Elastic Agent Builder</a>를 사용하여 가상의 야외 스포츠 장비 상점인 ElasticSport에 음성 에이전트를 구축해 보겠습니다. 음성 에이전트는 문맥을 인식하고 데이터와 함께 작동합니다.</p><h2>참여 방법</h2><p>음성 에이전트 분야에는 크게 두 가지 패러다임이 있습니다. 첫 번째는 음성 간 변환(speech-to-speech) 모델을 사용하는 방식이고, 두 번째는 음성-텍스트(speech-to-text) 변환, LLM, 텍스트-음성(text-to-speech) 변환으로 구성된 음성 파이프라인을 사용하는 방식입니다. 음성 간 변환 모델도 나름의 장점이 있지만, 음성 파이프라인은 사용되는 기술과 문맥 관리 방식을 훨씬 더 세밀하게 사용자 맞춤화할 수 있으며, 에이전트의 행동을 제어할 수 있다는 이점이 있습니다. 본문에서는 음성 파이프라인 모델을 중점적으로 다루겠습니다.</p><h3>주요 구성 요소</h3><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/b237501903f9c3a71fe1b7755c3990e40c5495c8-1600x653.png" style="width: 100%;" /><h4>전사(음성-텍스트 변환)</h4><p>전사는 음성 파이프라인의 진입점입니다. 전사 구성 요소는 원시 오디오 프레임을 입력으로 받아 음성을 텍스트로 전사한 후 그 텍스트를 출력합니다. 전사된 텍스트는 사용자의 발화가 끝났음을 감지할 때까지 버퍼에 저장되며, 발화가 종료되는 시점에 LLM 생성이 시작됩니다. 다양한 제3자 제공 업체에서 저지연 전사를 제공합니다. 선택 시 지연 시간과 전사 정확도를 고려하고, 스트리밍 전사를 지원하는지 확인하세요.</p><p></p><p>제3자 API 예시: <a href="https://www.assemblyai.com/">AssemblyAI</a>, <a href="https://deepgram.com/product/speech-to-text">Deepgram</a>, <a href="https://platform.openai.com/docs/guides/realtime-transcription">OpenAI</a>, <a href="https://elevenlabs.io/speech-to-text">ElevenLabs</a></p><h4>대화 차례 감지</h4><p>대화 차례 감지는 화자가 말을 마치고 응답 생성을 시작해야 할 시점을 감지하는 파이프라인 구성 요소입니다. 이를 수행하는 일반적인 방법 중 하나는 <a href="https://github.com/snakers4/silero-vad">Silero VAD</a>와 같은 음성 활동 탐지(VAD) 모델을 사용하는 것입니다. VAD는 오디오 에너지 수치를 사용하여 오디오에 음성이 포함되어 있는지와 음성이 종료된 시점을 감지합니다. 그러나 VAD만으로는 일시 중지와 발화 종료의 차이를 식별할 수 없습니다. 이 때문에 중간 잠정 전사(interim transcript) 또는 원시 오디오를 기반으로 화자가 말을 마쳤는지 예측하는 발화 종료 모델과 결합하는 경우가 많습니다.</p><p>예시(Hugging Face): <a href="https://huggingface.co/livekit/turn-detector">livekit/turn-detector</a>, <a href="https://huggingface.co/pipecat-ai/smart-turn-v3">pipecat-ai/smart-turn-v3</a></p><h4>에이전트</h4><p>에이전트는 음성 파이프라인의 핵심입니다. 의도를 파악하고, 적절한 문맥을 수집하고, 텍스트 형식으로 답변을 구성하는 역할을 담당합니다. <a href="https://www.elastic.co/elasticsearch/agent-builder">Elastic Agent Builder</a>는 기본 제공 추론 기능, 도구 라이브러리, 워크플로우 통합을 사용해 기업 내부 데이터를 활용하면서 외부 서비스와 상호 작용할 수 있는 에이전트를 만들 수 있습니다.</p><h4>LLM(텍스트 간 변환)</h4><p>Elastic Agent Builder용 LLM을 선택할 때 고려해야 할 주요 특징은 LLM 추론 벤치마크와 첫 번째 토큰 생성 시간(TTFT) 두 가지입니다.</p><p>추론 벤치마크는 LLM이 얼마나 정확한 응답을 생성할 수 있는지를 나타냅니다. 고려해야 할 벤치마크는 연속 대화(multiturn) 대화 준수도와 지능 벤치마크를 평가하는 것으로, 각각 MT-Bench와 Humanity&#x27;s Last Exam 데이터 세트가 있습니다.</p><p>TTFT 벤치마크는 모델이 첫 번째 출력 토큰을 얼마나 빠르게 생성하는지 평가합니다. 다른 유형의 지연 벤치마크도 있지만, TTFT는 음성 에이전트에게 특히 중요합니다. 첫 번째 토큰이 수신되자마자 음성 합성을 시작할 수 있기 때문인데, 이는 대화 차례 사이의 지연 시간을 줄여 주어 훨씬 자연스러운 대화 경험을 만들어 줍니다</p><p>보통 이 두 가지 요소 중 하나를 선택하면 다른 하나는 어느 정도 포기해야 합니다. 속도가 빠른 모델은 대개 추론 능력이 떨어지는 경향이 있기 때문입니다.</p><p>예시(Hugging Face): <a href="https://huggingface.co/openai/gpt-oss-20b">openai/gpt-oss-20b</a>, <a href="https://huggingface.co/openai/gpt-oss-120b">openai/gpt-oss-120b</a></p><h4>합성(텍스트-음성 변환)</h4><p>파이프라인의 마지막 부분은 텍스트-음성 변환 모델입니다. 이 구성 요소는 LLM에서 출력된 텍스트를 들을 수 있는 음성으로 변환하는 역할을 합니다. LLM과 마찬가지로 지연 시간은 텍스트-음성 변환 제공 업체를 선택할 때 주의해야 할 특성입니다. 텍스트 음성 변환 지연 시간은 첫 바이트까지의 시간(TTFB)으로 측정됩니다. 이는 첫 번째 오디오 바이트가 수신될 때까지 걸리는 시간을 의미합니다. TTFB가 낮으면 대화 차례 사이의 지연 시간도 줄어듭니다.</p><p>예시: <a href="https://elevenlabs.io/text-to-speech-api">ElevenLabs</a>, <a href="https://cartesia.ai/sonic">Cartesia</a>, <a href="https://www.rime.ai/">Rime</a></p><h4>음성 파이프라인 구축</h4><p>Elastic Agent Builder는 여러 가지 수준에서 음성 파이프라인에 통합될 수 있습니다.</p><ol><li>Agent Builder 도구만 해당: 음성-텍스트 변환 → LLM(Agent Builder 도구 사용) → 텍스트-음성 변환</li><li>MCP 방식의 Agent Builder: 음성-텍스트 변환 → LLM(MCP를 통해 Agent Builder에 접근) → 텍스트-음성 변환</li><li>Agent Builder를 핵심으로 사용: 음성-텍스트 변환 → Agent Builder → 텍스트-음성 변환</li></ol><p>이 프로젝트에서는 Agent Builder를 핵심 접근 방식으로 선택했습니다. 이러한 접근 방식을 통해 Agent Builder와 워크플로우의 전체 기능을 사용할 수 있습니다. 이 프로젝트는 LiveKit을 사용하여 음성-텍스트 변환, 발화 차례 감지 및 텍스트-음성 변환을 오케스트레이션하고, Agent Builder와 직접 통합되는 맞춤형 LLM 노드를 구현합니다.</p><h2>Elastic 지원 음성 에이전트</h2><p>ElasticSport라는 가상의 스포츠 매장에 맞춤형 지원 음성 에이전트를 구축 해 보겠습니다. 고객은 헬프라인에 전화해 제품 추천을 요청하고, 제품 세부 정보를 찾고, 주문 상태를 확인하고, 주문 정보를 문자 메시지로 받을 수 있습니다. 이렇게 하려면 먼저 맞춤형 에이전트를 구성하고 Elasticsearch 쿼리 언어(ES|QL) 쿼리 및 워크플로우를 실행하기 위한 도구를 생성해야 합니다.</p><a href="https://videos.elastic.co/watch/jpX6D21URZuh3PtXVV7Ks6">Watch video on Vidyard</a><h3>에이전트 구성</h3><h4>프롬프트</h4><p>프롬프트는 에이전트가 어떤 성격을 가져야 하고 어떻게 응답해야 하는지 지시합니다. 중요한 것은, 응답이 오디오로 제대로 합성되고 오해가 발생하더라도 유연하게 해결될 수 있도록 하는 몇 가지 음성별 프롬프트가 있다는 점입니다.</p><pre><code>You are a Sales Assistant at ElasticSport, an outdoor sport shop specialized in hiking and winter equipment. 

[Profile]
- name: Iva
- company: ElasticSport
- role: Sales Assistant
- language: en-GB
- description: ElasticSport virtual sales assistant

[Context]
- Ask clarifying questions to understand the context.
- Use available tools to answer the user's question.
- Use the knowledge base to retrieve general information

[Style]
- Be informative and comprehensive.
- Maintain a professional, friendly and polite tone.
- Mimic human behavior and speech patterns.
- Be concise. Do not over explain initially

[Response Guideline]
- Present dates in spelled-out month date format (e.g., January fifteenth, two thousand and twenty-four).
- Avoid the use of unpronounceable punctuation such as bullet points, tables, emojis.
- Respond in plain text, avoid any formatting.
- Spell out numbers as words for more natural-sounding speech.
- Respond in short and concise sentences. Responses should be 1 or 2 sentences long.

[ERROR RECOVERY]
### Misunderstanding Protocol
1. Acknowledge potential misunderstanding
2. Request specific clarification</code></pre><h4>워크플로우</h4><p>Twilio의 메시징 API를 통해 SMS를 보내기 위한 작은 워크플로우를 추가해 보겠습니다. 해당 워크플로우는 맞춤형 에이전트에게 하나의 &#x27;도구&#x27; 형태로 제공되며, 이를 통해 에이전트가 통화 중에 발신자에게 SMS 문자를 보낼 수 있는 사용자 경험을 구현할 수 있습니다. 예를 들어 발신자는 &quot;<em>X</em> 에 대한 자세한 내용을 문자로 보내 줄 수 있나요?&quot;라고 물어볼 수 있습니다.</p><pre><code>name: send sms
enabled: true
triggers:
  - type: manual
inputs:
  - name: message
    type: string
    description: The message to send to the phone number.

  - name: phone_number
    type: string
    description: The phone number to send the message to.

consts:
  TWILIO_ACCOUNT: "****"
  BASIC_AUTH: "****"
  FROM_PHONE_NNUMBER: "****"
steps:
  - name: http_step
    type: http
    with:
      url: https://api.twilio.com/2010-04-01/Accounts/{{consts.TWILIO_ACCOUNT}}/Messages.json
      method: POST
      headers:
        Content-Type: application/x-www-form-urlencoded
        Authorization: Basic {{consts.BASIC_AUTH | base64_encode}}
      body: From={{consts.FROM_PHONE_NNUMBER}}&To={{inputs.phone_number}}&Body={{inputs.message}}
      timeout: 30s</code></pre><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/b057e71b0a7c50eb3da47cd4f95e77ec7b4c6126-1600x1245.png" style="width: 100%;" /><h4>ES|QL 도구</h4><p>다음 도구를 사용하여 에이전트가 실제 데이터를 기반으로 관련 응답을 제공할 수 있습니다. 예시 리포지토리에는 제품, 주문, 지식 기반 데이터 세트로 Kibana를 초기화하는 설정 스크립트가 포함되어 있습니다.</p><ul><li><strong>Product.search</strong></li></ul><p>제품 데이터 세트에는 65개의 가상 제품이 포함되어 있습니다. 다음은 예시 문서입니다.</p><pre><code>{
      "sku": "ort3M7k",
      "name": "Ortovox Free Rider 26 Backpack",
      "price": 189,
      "currency": "USD",
      "image": "https://via.placeholder.com/150",
      "description": "The Ortovox Free Rider 26 is a technical freeride backpack with a dedicated safety compartment and diagonal ski carry system. Perfect for backcountry missions.\n\nKey Features:\n- 26L capacity\n- Diagonal ski carry system\n- Safety equipment compartment\n- Helmet holder\n- Hydration system compatible",
      "category": "Accessories",
      "subCategory": "Backpacks",
      "brand": "Ortovox",
      "sizes": ["One Size"],
      "colors": ["Black", "Blue", "Orange"],
      "materials": ["Nylon", "Polyester"]
    }</code></pre><p>이름과 설명 필드는 <code>semantic_text</code>(으)로 맵핑되어 있어 LLM이 ES|QL을 통해 의미를 검색하여 관련 제품을 검색할 수 있습니다. 하이브리드 검색 쿼리는 두 필드 모두에 걸쳐 의미 일치 작업을 수행하며, 이름 필드 일치에는 부스트를 적용하여 약간 더 높은 가중치를 부여합니다.</p><p>쿼리는 먼저 초기 관련성 점수에 따라 순위가 매겨진 상위 20개의 결과를 검색합니다. 이러한 결과는 <code>.rerank-v1-elasticsearch</code> 추론 모델을 사용해 설명 필드를 기반으로 다시 순위가 매겨지고, 최종적으로 가장 관련성 높은 다섯 가지 제품을 선별합니다.</p><pre><code>type: ES|QL
toolId: products.search
description: Use this tool to search through the product catalogue by keywords.
query: |
    FROM products
        METADATA _score
      | WHERE
          MATCH(name, ?query, {"boost": 0.6}) OR
            MATCH(description, ?query, {"boost": 0.4})
      | SORT _score DESC
      | LIMIT 20
      | RERANK ?query
            ON description
            WITH {"inference_id": ".rerank-v1-elasticsearch"}
      | LIMIT 5

parameters:
    query: space separated keywords to search for in catalogue</code></pre><ul><li><strong>Knowledgebase.search</strong></li></ul><p>지식 기반 데이터 세트에는 다음과 같은 형태의 문서가 포함되어 있으며, 여기서 제목과 내용 필드는 의미 텍스트로 저장됩니다.</p><pre><code>{
        id: "8273645",
        createdAt: "2025-11-14",
        title: "International Orders",
        content: `International orders are processed through our international shipping partner. Below are the countries we ship to and average delivery times.
        Germany: 3-5 working days
        France: 3-5 working days
        Italy: 3-5 working days
        Spain: 3-5 working days
        United Kingdom: 3-5 working days
        United States: 3-5 working days
        Canada: 3-5 working days
        Australia: 3-5 working days
        New Zealand: 3-5 working days
        `
}</code></pre><p>이 도구는 <code>product.search</code> 도구와 유사한 쿼리를 사용합니다.</p><pre><code>type: "ES|QL"
toolId: knowledgebase.search
description: Use this tool to search the knowledgebase.
query: |
  FROM knowledge_base
    METADATA _score
  | WHERE
      MATCH(title, ?query, {"boost": 0.6}) OR
      MATCH(content, ?query, {"boost": 0.4})
  | SORT _score DESC
  | LIMIT 20
  | RERANK ?query
      ON content
      WITH {"inference_id": ".rerank-v1-elasticsearch"}
  | LIMIT 5

parameters:
  query: space separated keywords or natural language phrase to semantically search for in the knowledge base</code></pre><ul><li><strong>Orders.search</strong></li></ul><p>마지막으로 추가할 도구는 <code>order_id</code>(으)로 주문을 검색하는 데 사용되는 도구입니다.</p><pre><code>type: "ES|QL"
toolId: order.search
description: Use this tool to retrieve an order by its ID.
query: |
  FROM orders
    METADATA _score
  | WHERE order_id == ?order_id
  | SORT _score DESC
  | LIMIT 1

parameters:
  order_id: "the ID of the order"</code></pre><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/d22bdd540a95b5a9c2bd5f308620835e8e6f7ecb-1600x1361.png" style="width: 100%;" /><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/d91709a50fb5391876b714885242d998b2b21027-1600x1443.png" style="width: 100%;" /><p>에이전트를 구성하고 이러한 워크플로우 및 ES|QL 도구를 에이전트에 연결한 후, Kibana 내에서 에이전트를 테스트할 수 있습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/8691f41624247a6b1352d158c970031e1426ce5e-1600x1056.png" style="width: 100%;" /><p>ElasticSport 지원 에이전트 구축 외에도, 에이전트와 워크플로우 및 도구를 잠재 고객을 발굴하는 영업 에이전트, 주택 수리 서비스 에이전트, 식당 예약 또는 일정 예약 에이전트 등 다른 사용 사례에 맞춰 설정할 수 있습니다.</p><p></p><p>마지막으로, 방금 만든 에이전트를 LiveKit, 텍스트-음성 및 음성-텍스트 변환 모델과 연결합니다. 이 블로그 끝에 링크된 리포지토리에는 LiveKit과 함께 사용할 수 있는 맞춤형 Elastic Agent Builder LLM 노드가 포함되어 있습니다. <code>AGENT_ID</code>을(를) 자신의 것으로 바꾸고 Kibana 인스턴스와 연결하기만 하면 됩니다.</p><h2>시작하기</h2><p><a href="https://github.com/KDKHD/elastic_agent_builder_livekit">여기</a>에서 코드를 확인하고 직접 사용해 보세요. </p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[채팅창을 넘어선 Agent Builder: Augmented Infrastructure 소개]]></title>
            <description><![CDATA[Elastic Agent Builder with Augmented Infrastructurer에 대해 알아보세요. 이 AI 에이전트는 증강 운영, 증강 개발 및 증강 합성을 지원합니다.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/agent-builder-augmented-infrastructure</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/agent-builder-augmented-infrastructure</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Alexander Wert, Bill Easton, Gil Raphaelli, Graham Hudgins, Norrie Taylor]]></dc:creator>
            <pubDate>Thu, 22 Jan 2026 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/e08daf78f328e826f39d06329f6a5487f75d178d-1272x700.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p><strong>빈말이 아닙니다. 직접 구현합니다.</strong></p><p>우리는 모두 AI 에이전트의 부상을 목격했습니다. AI는 텍스트를 요약하고, 코드 스니펫을 작성하고, 문서를 기반으로 질문에 답하는 데 탁월합니다. 하지만 DevOps 및 사이트 안정성 엔지니어링(SRE) 담당자에게는 답답한 한계가 있었습니다. 대부분의 에이전트는 콜센터 패러다임에 갇혀 있어 읽고, 생각하고, 채팅할 수는 있지만, 관리해야 할 인프라에 직접 접근하여 조작할 수는 없다는 것입니다.</p><p>최근 해커톤 프로젝트에서 당사는 이러한 한계를 뛰어넘기로 결정했습니다.</p><p>이에 <strong>Augmented Infrastructure</strong>를 구축했습니다. 이는 단순히 조언만 제공하는 것이 아니라 실시간 환경을 생성, 배포, 모니터링 및 수정하는 인프라 코파일럿입니다.</p><h2><strong>문제: 복사, 형식 변경, 붙여 넣기</strong></h2><p>표준 에이전트는 독립적으로 작동합니다. 앱이 다운되어 회사에 5백만 달러의 손실이 발생하면 표준 에이전트는 문제를 해결하는 방법에 대한 런북을 읽어줄 수 있습니다. 하지만 실제 작업은 여전히 <em>사용자</em>가 직접 해야 합니다. 코드를 복사하고, 환경에 맞게 형식을 변경한 다음, 터미널에 붙여 넣어야 합니다.</p><p>Kubernetes에 대해 <em>이야기하는 것</em>과 Kubernetes를 <em>구성하는 것</em>의 차이를 이해하는 에이전트가 필요했습니다.</p><h2><strong>엔진: Elastic Agent Builder란 무엇입니까?</strong></h2><p>이를 구축하기 위해 처음부터 모든 것을 새로 시작한 것은 아닙니다. <a href="https://www.elastic.co/elasticsearch/agent-builder"><strong>Elastic Agent Builder</strong></a>를 기반으로 구축했습니다. Elastic Agent Builder는 에이전트를 신속하게 개발할 수 있도록 설계된 프레임워크로, 대규모 언어 모델(LLM)(데모에서는 Google Gemini를 사용했습니다)과 Elasticsearch에 저장된 개인 데이터 간의 연결 고리 역할을 합니다.</p><p>Agent Builder는 문서나 로그와 같은 내부 데이터를 기반으로 대화형 AI를 구현하는 데 사용할 수 있습니다. 하지만 Agent Builder의 가장 강력한 기능은 <strong>도구</strong>를 할당할 수 있다는 점입니다. 이러한 도구를 통해 LLM은 채팅 인터페이스를 벗어나 특정 작업을 수행할 수 있습니다. 이 기능을 최대한 활용하면 Agent Builder를 강력한 자동화 도구로 탈바꿈시킬 수 있다는 사실을 깨달았습니다.</p><h2><strong>작동시키기: 첫 번째 버전 구축</strong></h2><p>프로젝트를 시작할 때 에이전트가 외부 세계를 변화시킬 수 있도록 만들고 싶었습니다. 그래서 이런 아이디어를 떠올렸습니다. (에이전트가 호스트에서 생각할 수 있는 모든 명령을 실행하는) &#x27;실행기&#x27; 소프트웨어를 구축하면 어떨까? 그리고 만약 실행기, Elastic Agent Builder, 그리고 사용자가 3자 호출을 하면 어떨까?</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/8dc8317c1301b8eb7b89438529e8d8d17411c95a-1024x559.png" style="width: 100%;" /><p>먼저 Augmented Infrastructure 실행기라는 Python 프로젝트를 구축했는데, 이는 기본적으로 Elastic Agent Builder 대화 API를 매초마다 쿼리하고 당사가 만든 특수 구문을 확인하는 <code>while(true)</code> 루프였습니다.</p><pre><code>{
	"tool_name": "my_tool",
       "tool_arguments": "\{stringified json arguments\}"
}</code></pre><p>그런 다음 새로운 도구 호출 구문에 대해 알려주도록 프롬프트를 업데이트했습니다. Bill은 Python으로 모델 컨텍스트 프로토콜(MCP) 서버를 구축하는 데 가장 많이 사용되는 프레임워크인 <a href="https://gofastmcp.com/getting-started/welcome">FastMCP</a>의 유지 관리자입니다. Bill은 FastMCP 클라이언트와 이 새로운 실행기 소프트웨어를 사용하여 MCP 서버를 마운트하고 실행기에서 해당 도구를 사용할 수 있도록 하는 작업을 시작했습니다. 에이전트가 이를 확인하면 도구 호출을 실행하고 그러면 마치 사용자가 결과를 보낸 것처럼 결과가 대화창에 다시 <code>POST</code>(으)로 전송됩니다. 이렇게 하면 LLM이 결과에 응답하게 되고, 그렇게 진행이 가능해집니다!</p><p>매우 훌륭했지만 다음과 같이 두 가지 주요 문제가 있었습니다.</p><ol><li>에이전트가 이 모든 JSON을 사용자와의 대화로 바로 내보냅니다.</li><li>대화 API를 통해 메시지를 볼 수 있는 가장 빠른 시점이 대화 라운드가 완료된 시점(즉, LLM이 응답한 시점)입니다.</li></ol><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/7b3755aeae17722ff1bb9677712293e9195f96a0-1058x1034.png" style="width: 100%;" /><p>그래서 이를 백그라운드로 옮기는 방법을 파악하기 위해 노력했습니다.</p><p>그다음, 에이전트에 <code>call_external_tool</code>(이)라는 도구를 제공하고 두 개의 인수를 전달하도록 변경했습니다. 하나는 <code>tool_name</code>이고 다른 하나는 문자열화된 JSON 도구 인수입니다. 이 외부 도구 호출은 아무것도 반환하지 않지만, 중요한 것은 대화 API에 대한 <code>GET</code> 요청에서 확인할 수 있다는 점입니다. 그런 다음 실행기에 Elasticsearch에 직접 문서를 작성할 수 있는 권한을 부여했고, Elastic Agent Builder 에이전트는 필요에 따라 해당 문서를 검색할 수 있습니다. 에이전트는 항상 사용자 메시지에 응답하여 작동하므로, 에이전트가 결과를 찾고 처리를 계속할 수 있도록 사용자 메시지로 에이전트를 시작해야 합니다. 그래서 다음과 같이 에이전트가 채팅에 간단한 메시지를 삽입하여 대화를 재개하도록 했습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/61ff59a57c68ed5fad492d19c0580644113a507d-1600x1321.png" style="width: 100%;" /><p>이제 외부 도구 호출이 가능해졌습니다. 하지만 위에서 언급한 두 번째 문제 때문에 마지막 시작 단계를 없애야 했습니다. 그렇지 않으면 외부 도구를 호출할 때마다 결과를 얻기 위해 전체 대화 라운드을 거쳐야 했기 때문입니다.</p><h2><strong>효율적으로 만들기: 워크플로우 소개</strong></h2><p>Agent Builder 에이전트는 Elasticsearch 쿼리 언어(ES|QL) 및 인덱스 검색 도구 호출 외에도 Elastic 워크플로우 기반 도구를 호출할 수 있습니다. Elastic 워크플로우는 임의의 작업 순서와 로직을 실행할 수 있는 유연하고 관리하기 쉬운 방법을 제공합니다. 본 예시에서는 워크플로우가 외부 도구에 대한 Elasticsearch 요청을 저장하고 결과를 폴링할 ID를 반환하기만 하면 됩니다. 따라서 다음과 같은 간단한 워크플로우 정의가 생성됩니다.</p><pre><code>name: ai-tool-call
enabled: true
triggers:
  - type: manual
inputs:
  - name: runner_id
    type: string
  - name: tool_calls
    type: string

steps:
  - name: store_request
    type: elasticsearch.create
    with:
      index: distributed-tool-requests
      id: "{{inputs.runner_id}}_{{ execution.id }}"
      document:
        request_id: "{{ execution.id }}"
        runner_id: "{{inputs.runner_id}}"
        tool_call: "{{inputs.tool_calls}}"
        status: "unhandled"

  - name: output_result
    type: console
    with:
      message: "Called tool, with execution id: {{ execution.id }}. Use this ID to poll the results."</code></pre><p>이를 통해 도구 호출 요청이 대화에 기록되는 것에 의존하는 대신, 실행기는 새로운 외부 도구 요청에 대해 Elasticsearch <code>distributed-tool-requests</code> 인덱스를 폴링하고 제공된 <code>execution.id</code>을(를) 사용하여 결과를 다른 Elasticsearch 인덱스에 보고할 수 있습니다.</p><p>이는 다음과 같이 위에서 언급한 두 가지 주요 문제를 해결합니다.</p><ol><li>대화 기록이 더 이상 외부 도구 호출에 위한 페이로드로 인해 복잡해지지 않습니다.</li><li>실행기는 대화 기록 대신 Elasticsearch 인덱스를 폴링하므로, 외부 도구 요청이 표시되기 위해 대화 라운드가 완료될 때까지 차단되지 않습니다.</li></ol><p>두 번째는 외부 도구 호출 처리가 대화 라운드가 완료된 시점이 아니라 에이전트의 사고 단계 내에서 시작된다는 큰 장점이 있습니다. 이를 통해 시스템 프롬프트에서 LLM에 외부 도구 결과가 나올 때까지 지속적으로 폴링하도록 지시할 수 있으며, 시작 메시지를 표시할 필요가 없습니다. 전반적으로 이러한 방식은 대화 흐름을 더욱 자연스럽게 만들어 줍니다. LLM은 단일 대화 라운드 내에서 여러 외부 도구 요청을 처리할 수 있으므로(도구 요청당 별도의 대화 라운드가 필요하지 않음), 더 복잡한 사용자 요청을 한 번에 처리할 수 있습니다.</p><h2><strong>한곳에 모으기</strong></h2><p>LLM과 서버 랙 간의 격차를 해소하기 위해 다음과 같이 Agent Builder의 도구 기능을 사용하여 특정 아키텍처를 개발했습니다.</p><ol><li><strong>Augmented Infrastructure 실행기:</strong> 대상 환경(서버, Kubernetes 클러스터, 클라우드 계정) 내부에 경량 실행기를 배포했습니다. 이러한 실행기는 각 실행기만 접근 가능한 보안 엔드포인트와 보안 정보를 사용하여 Elastic에 직접 연결됩니다.</li><li><strong>ES|QL 검색:</strong> 코파일럿은 Elastic의 <strong>ES|QL</strong>을 사용하여 하이브리드 검색을 수행합니다. 단순히 지식을 검색하는 것이 아니라 <em>기능</em>을 검색합니다. 연결된 실행기에 쿼리하여 사용 가능한 도구(예: <code>list_ec2_instances</code>, <code>install_helm_chart</code>)를 확인합니다.</li><li><strong>워크플로우 실행:</strong> 에이전트가 작업 과정을 결정하면 구조화된 워크플로우를 생성합니다.</li><li><strong>피드백 루프:</strong> 실행기는 로컬에서 명령을 실행하고 그 결과를 다시 Elasticsearch에 보고합니다. 코파일럿은 인덱스에서 결과를 읽고 다음 단계를 결정합니다.</li></ol><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/76be256da722c1965971fc506502768bd890f0c4-1290x1076.png" style="width: 100%;" /><h2><strong>데모: 장애에서 통합 가시성으로</strong></h2><a href="https://videos.elastic.co/watch/bRgPJfJLeABjPMMhvy1sbB">Watch video on Vidyard</a><p>이 동영상에서는 이 아키텍처의 강력한 성능을 보여주는 두 가지 서로 다른 시나리오를 소개했습니다.</p><h3><strong>시나리오 1: DevOps 지원</strong></h3><p>Kubernetes 클러스터의 사각지대 때문에 5백만 달러 규모의 장애가 발생하여 패닉 상태에 빠진 사용자의 이야기로 시작했습니다.</p><ul><li><strong>요청:</strong> &quot;이런 일이 다시 발생하지 않도록 하려면 어떻게 해야 하나요?&quot;</li><li><strong>작업:</strong> 에이전트는 단순히 튜토리얼만 제공하지 않았습니다. 클러스터 식별, 필요한 네임스페이스 생성, Kubernetes 보안 정보 생성, OpenTelemetry Operator 설치, 실시간 APM 대시보드에 대한 링크 즉시 제공을 수행했습니다.</li><li><strong>결과:</strong> 사용자가 단 한 줄의 YAML도 작성하지 않고도 완전한 Kubernetes 통합 가시성과 애플리케이션 인사이트를 얻을 수 있습니다.</li></ul><h3><strong>시나리오 2: 보안 인계</strong></h3><p>인프라 보안의 기본 원칙은 보이지 않는 것은 보호할 수 없다는 것입니다. DevOps 복구 작업을 수행하는 동안 에이전트는 환경의 보안을 개선할 기회를 포착합니다.</p><p>이전 Elastic Observability 관련 조사에서 발생한 경보를 통해 보안 담당자가 인프라와 직접 소통하는 방법을 시연합니다. 첫째, 클라우드 환경의 자산과 리소스를 열거하고, 둘째, 환경 보안을 보장하는 데 필요한 도구를 배포하는 방법을 보여줍니다.</p><ul><li><strong>탐색:</strong> 코파일럿은 보안 담당자를 위해 AWS 리소스를 열거하고 중요한 문제점을 발견했습니다. 해당 문제점은 엔드포인트 보호가 누락된 공용 엔드포인트가 있는 Amazon Elastic Compute Cloud(EC2) 인스턴스와 Amazon Elastic Kubernetes Service(EKS) 클러스터입니다.</li><li><strong>조치:</strong> 간단한 승인으로 코파일럿은 <strong>Elastic Security</strong> <strong>확장 탐지 및 응답(XDR), 클라우드 탐지 및 응답(CDR)</strong>을 취약한 자산에 배포하여 환경을 실시간으로 보호했습니다.</li><li><strong>결과:</strong> 완벽한 런타임 보안을 통해 배포된 AWS 자산 및 리소스를 보호합니다.</li></ul><h2><strong>미래: 모든 것이 증강되는 세상</strong></h2><p>이 프로젝트는 Elastic Agent Builder가 분산 운영의 핵심 역할을 할 수 있음을 입증합니다. 인프라에만 국한되지 않고, 실행기 기술은 다음과 같은 분야에서도 활용될 수 있습니다.</p><ul><li><strong>증강 합성:</strong> 글로벌 실행기 전반의 TLS 오류 진단.</li><li><strong>증강 개발:</strong> 풀 리퀘스트 생성 및 프론트엔드 서비스에서 CAPTCHA 구현.</li><li><strong>증강 운영:</strong> 장애 발생 시 DNS 리졸버 자동 재구성.</li></ul><h2><strong>직접 사용해 보기</strong></h2><p>AI의 미래가 단순히 채팅 지원에 그치는 것이 아니라, <strong>Augmented Infrastructure</strong>에 관한 것이라고 생각합니다. 즉, 고객과 함께 배포, 수정, 관찰, 보호할 수 있는 파트너를 확보하는 것입니다.</p><p>지금 바로 코드를 확인하고 분산 실행기(<a href="https://github.com/strawgate/augmented-infrastructure">GitHub</a>)와 Elastic Agent Builder를 <a href="https://cloud.elastic.co/">Elastic Cloud Serverless</a>에서 직접 사용해 보세요!</p><ul><li>Elastic Cloud에서 서버리스 프로젝트를 생성하세요.</li><li>실행기에 코드를 배포하세요.</li><li>실행기를 설정하세요.</li><li>mcp.json을 구성하세요.</li><li>실행기를 시작하면 에이전트와 그 도구가 자동으로 생성됩니다.</li><li>분산 실행기에 대한 추론, 계획 및 실행을 수행할 수 있는 에이전트와 채팅하세요!</li></ul><p><strong>팀: </strong><em>알렉스, 빌, 길, 그레이엄, 노리</em></p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[스위스식 해시 테이블을 사용한 더 빠른 ES|QL 통계 처리]]></title>
            <description><![CDATA[스위스식 해싱과 SIMD 친화적인 설계가 Elasticsearch 쿼리 언어(ES|QL)에서 일관되고 측정 가능한 속도 향상을 실현하는 방법.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/esql-swiss-hash-stats</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/esql-swiss-hash-stats</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Chris Hegarty, Matthew Alp, Nik Everet]]></dc:creator>
            <pubDate>Mon, 19 Jan 2026 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/21036e031070f14faccb2b53b22723de2750c391-1280x720.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>최근 Elasticsearch의 해시 테이블 구현의 핵심적인 부분을 스위스식 설계로 교체하였으며, 균일하고 카디널리티가 높은 워크로드에서 빌드 및 반복 시간이 최대 2~3배 빨라지는 결과를 확인했습니다. 결과적으로 Elasticsearch 쿼리 언어(ES|QL) 통계 및 분석 작업에서 더 낮은 지연 시간, 더 나은 처리량 및 더 예측 가능한 성능을 구현할 수 있게 되었습니다.</p><h2>이것이 중요한 이유</h2><p>대부분의 일반적인 분석 워크플로우는 결국 데이터를 그룹화하는 작업으로 귀결됩니다. 호스팅당 평균 바이트 계산, 사용자당 이벤트 계산 또는 다양한 차원에 걸친 메트릭 집계 등의 작업을 수행하더라도 핵심 작업은 동일합니다. 키를 그룹에 맵핑하고 실행 중인 집계를 업데이트하는 것입니다.</p><p>작은 규모에서는 거의 모든 합리적인 해시 테이블이 잘 작동합니다. 대규모 환경(수억 건의 문서와 수백만 개의 개별 그룹)에서는 아주 작은 세부 사항들이 중요해지기 시작합니다. 로드 팩터, 탐색 전략, 메모리 레이아웃 및 캐시 동작이 선형적인 성능 향상을 이뤄내느냐, 혹은 캐시 미스의 장벽에 가로막히느냐를 결정짓는 차이를 만듭니다.</p><p>Elasticsearch는 수년간 이러한 워크로드를 지원해 왔지만, 핵심 알고리즘을 현대화할 수 있는 기회를 항상 모색하고 있습니다. 따라서 스위스 테이블에서 영감을 받은 새로운 접근 방식을 평가하고 이를 ES|QL의 통계 계산 방식에 적용했습니다.</p><h2>스위스 테이블의 본질은 무엇인가요?</h2><p>스위스 테이블은 Google의 SwissTable로 대중화되고 나중에 Abseil 및 기타 라이브러리에서 채택된 현대적인 해시 테이블의 한 계열입니다.</p><p>기존의 해시 테이블은 포인터를 쫓아가거나 키를 로드하는 데 많은 시간을 소비하지만, 그 결과가 일치하지 않는 경우가 많습니다. 스위스 테이블의 주요 특징은 키와 값과는 별도로 저장되는 <em>제어 바이트</em>라는 작은 캐시 상주 배열 구조를 사용하여 대부분의 탐색을 거부함으로써 메모리 트래픽을 획기적으로 줄일 수 있다는 점입니다.</p><p>각 제어 바이트는 하나의 슬롯을 나타내며, 저희의 경우에는 두 가지 정보를 인코딩합니다. 바로 해당 슬롯이 비어 있는지 여부와 해시값에서 추출한 짧은 지문입니다. 이러한 제어 바이트는 일반적으로 16개의 그룹으로 메모리에 연속적으로 배치되어 <a href="https://en.wikipedia.org/wiki/Single_instruction,_multiple_data">단일 명령, 다중 데이터</a>(SIMD) 처리에 적합합니다.</p><p>스위스 테이블은 한 번에 하나의 슬롯을 탐색하는 대신 벡터 명령어를 사용하여 전체 제어 바이트 블록을 스캔합니다. CPU는 단 한 번의 연산으로 새로 들어온 키의 지문을 16개의 슬롯과 비교하고 비어 있는 항목들을 걸러냅니다. 이 빠른 경로에서 살아남은 소수의 후보만 실제 키를 로드하고 비교해야 합니다.</p><p>이 설계는 약간의 추가 메타데이터를 사용하는 대신, 훨씬 뛰어난 캐시 지역성을 확보하고 무작위 로드 횟수를 획기적으로 줄입니다. 테이블이 커지고 탐색 체인이 길어질수록 이러한 속성의 가치는 점점 더 중요해집니다.</p><h2>핵심에 자리잡은 SIMD</h2><p>이 모든 것의 진정한 주인공은 SIMD입니다.</p><p>제어 바이트는 단순히 컴팩트할 뿐만 아니라 벡터 명령어로 처리되도록 명시적으로 설계되었습니다. 단일 SIMD 비교를 통해 한 번에 16개의 지문을 확인할 수 있어, 일반적으로 루프로 처리되는 작업을 몇 가지 광범위한 작업으로 전환할 수 있습니다. 그 예는 다음과 같습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/bd418778f0c6144f8f5f18419f6220ac0c935c7a-903x407.png" style="width: 100%;" /><p>실제로 이는 다음과 같은 의미를 지닙니다.</p><ul><li>분기 명령 감소.</li><li>탐색 체인 단축.</li><li>키 및 값 메모리로부터의 데이터 로드 횟수 감소.</li><li>CPU 실행 유닛의 활용도 대폭 향상.</li></ul><p>대부분의 조회 작업은 제어 바이트 스캔 단계에서 마무리됩니다. 다음 단계로 넘어가는 경우, 남은 작업은 매우 집중적이며 예측 가능합니다. 최신 CPU가 잘 처리하는 워크로드는 바로 이런 종류의 워크로드입니다.</p><h2>SIMD의 작동 원리 살펴보기</h2><p>애플리케이션의 내부 작동 원리를 살펴보고 싶은 독자를 위해, 테이블에 새 키를 삽입할 때 어떤 일이 일어나는지 알려드리겠습니다. 128비트 벡터를 사용하는 파나마 벡터 API를 활용하여 16개의 제어 바이트를 병렬로 처리합니다.</p><p>다음 스니펫은 Intel Rocket Lake에서 AVX-512로 생성된 코드를 보여 줍니다. 명령어는 해당 환경을 반영하지만, 설계는 AVX-512에 의존하지 않습니다. 동일한 고수준 벡터 연산은 동등한 명령어(예: AVX2, SSE 또는 NEON)를 사용하여 다른 플랫폼에서도 실행됩니다.</p><pre><code>; Load 16 control bytes from the control block
vmovdqu xmm0, XMMWORD PTR [r9+r10*1+0x10]

; Broadcast the 7-bit fingerprint of the new key across the vector
vpbroadcastb xmm1, r11d

; Compare all 16 control bytes to the new fingerprint
vpcmpeqb k7, xmm0, xmm1
kmovq rbx, k7

; Check if any matches were found
test rbx, rbx
jne <handle_match></code></pre><p>각 명령어는 삽입 과정에서 명확한 역할을 수행합니다.</p><ul><li><code>vmovdqu</code>: 128비트 <code>xmm0</code> 레지스터에 연속된 16개의 제어 바이트를 로드합니다.</li><li><code>vpbroadcastb</code>새로운 키의 7비트 지문을 <code>xmm1</code> 레지스터의 모든 레인에 복제합니다.</li><li><code>vpcmpeqb</code>: 각 제어 바이트를 브로드캐스트된 지문과 비교하여 일치할 가능성이 있는 마스크를 생성합니다.</li><li><code>kmovq</code> + <code>test</code>: 마스크를 범용 레지스터로 이동하고 일치하는 항목이 있는지 빠르게 확인합니다.</li></ul><p>벤치마킹 결과에 따르면 더 넓은 레지스터를 사용하여 32바이트 또는 64바이트로 확장해도 측정 가능한 성능 이점을 얻을 수 없었기 때문에, 최종적으로 한 번에 16개의 제어 바이트로 구성된 프로빙 그룹을 사용하기로 결정했습니다.</p><h2>ES|QL과의 통합</h2><p>Elasticsearch에서 스위스식 해싱을 채택한 것은 단순히 기존 방식을 대체하는 것이 아니었습니다. ES|QL은 메모리 회계, 안정성, 및 나머지 연산 엔진과의 통합 측면에서 매우 엄격한 요구 사항을 가지고 있습니다.</p><p>새로운 해시 테이블을 페이지 리사이클러 및 서킷 브레이커 회계 기능을 포함한 Elasticsearch의 메모리 관리 기능과 긴밀하게 통합하여 할당이 가시적이고 제한된 범위 내에서 이루어지도록 했습니다. Elasticsearch의 집계는 밀집된 형태로 저장되고 그룹 ID로 색인되어 메모리 레이아웃을 컴팩트하게, 반복 작업 속도를 빠르게 유지할 뿐만 아니라 임의 접근을 허용함으로써 특정 성능 최적화를 가능하게 합니다.</p><p>가변 길이 바이트 키의 경우, 그룹 ID와 함께 전체 해시를 캐시합니다. 이렇게 하면 탐색 과정에서 비용이 많이 드는 해시 코드를 다시 계산하지 않아도 되고, 관련 메타데이터를 서로 가깝게 유지하여 캐시 로컬리티를 개선할 수 있습니다. 리해싱 시에 실제 값을 검사할 필요 없이, 캐싱된 해시 값과 제어 바이트에만 의존하여 처리할 수 있으므로 리사이징 비용을 낮게 유지할 수 있습니다.</p><p>구현에서 한 가지 중요한 단순화 포인트는 항목을 절대 삭제하지 않는다는 점입니다. 이를 통해 <em>툼스톤</em>(이전에 점유했던 슬롯을 식별하는 마커)이 필요 없으며, 빈 슬롯을 완전히 비어 있는 상태로 유지할 수 있게 되었습니다. 이는 탐색 동작을 더욱 개선하고 제어 바이트 스캔을 효율적으로 유지합니다.</p><p>그 결과, Elasticsearch의 실행 모델에 자연스럽게 녹아들면서도 스위스 테이블만의 매력적인 성능 특성을 유지하는 설계가 탄생했습니다.</p><h2>성능은 어떻습니까?</h2><p>작은 카디널리티에서 스위스 테이블은 기존 구현과 거의 동등한 성능을 발휘합니다. 이는 예상된 결과입니다. 테이블의 크기가 작을 때는 캐시 효과의 영향이 적고, 최적화할 만한 탐색 과정이 거의 없기 때문입니다.</p><p>카디널리티가 증가함에 따라 상황은 급격히 달라집니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/24900afc47ab07b0e9933f6117b99d0f4613f794-962x599.png" style="width: 100%;" /><p>위의 히트맵은 1,000개에서 최대 10,000,000개 그룹에 이르는 카디널리티 범위에 걸쳐, 다양한 키 크기(8, 32, 64, 128바이트)별 시간 개선 지수를 보여 줍니다. 카디널리티가 증가함에 따라 개선 지수는 꾸준히 증가하며, 균등 분포의 경우 최대 2~3배에 달합니다.</p><p>이러한 추세는 설계 단계에서 예측한 바와 정확히 일치합니다. 기존의 해시 테이블은 카디널리티가 높아질수록 탐색 체인이 길어지지만, 스위스식 탐색은 대부분의 조회를 SIMD 연산에 최적화된 제어 바이트 블록 내에서 해결합니다.</p><h2>캐시 동작이 알려 주는 이야기</h2><p>속도 향상이 가능했 이유를 더 잘 이해하기 위해, 동일한 JMH <a href="https://github.com/elastic/elasticsearch/pull/139343/files#diff-d0e0cc91a7495bf36b2d44eacce95f5185d01879e5f6c38089ac7a89aad17da7"><code>benchmarks</code></a>를(을) Linux <code>perf</code> 환경에서 실행하여 캐시 및 TLB 통계를 캡처했습니다.</p><p>원본 구현과 비교할 때 스위스 버전은 전반적으로 약 60% 적은 캐시 참조를 수행합니다. 최종 레벨 캐시 로드가 4배 이상 감소하고, LLC 로드 미스는 6배 이상 감소합니다. LLC 미스는 보통 메인 메모리 접근으로 곧장 이어지기 때문에, 이러한 감소 만으로도 전체 성능 향상의 상당 부분을 설명할 수 있습니다.</p><p>CPU와 더 가까울수록 L1 데이터 캐시 미스가 감소하고, 데이터 TLB 미스는 거의 6배나 줄어어, 공간 지역성이 더 치밀해지고 메모리 접근 패턴이 더 예측 가능해졌음을 의미합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/6e49b7609fba83e33692cb9834552b6ca7e42a83-998x499.png" style="width: 100%;" /><p>이것이 바로 SIMD 친화적인 제어 바이트가 가져다주는 실질적인 이득입니다. 메모리에 흩어져 있는 키와 값을 반복해서 로드하는 대신, 대부분의 탐색 과정을 캐시에 상주하는 컴팩트한 구조를 스캔하여 해결합니다. 메모리 접근 횟수가 줄어들면 미스가 줄고, 미스가 적을수록 쿼리가 더 빨라집니다.</p><h2>결론</h2><p>스위스식 해시 테이블 설계를 채택하고 SIMD 친화적인 탐색을 적극적으로 활용함으로써, 카디널리티가 높은 ES|QL 통계 워크로드에서 2~3배의 속도 향상과 더불어 더욱 안정적이고 예측 가능한 성능을 달성했습니다.</p><p>이 연구는 최신 CPU 인식 데이터 구조가 해시 테이블과 같이 이미 잘 알려진 문제에서도 상당한 성능 향상을 가져올 수 있음을 보여줍니다. 여기에는 추가적인 기본 유형 특수화 및 조인과 같은 다른 고카디널리티 경로에서의 사용 등 탐구할 여지가 더 많습니다. 이 모든 것은 Elasticsearch 내부를 지속적으로 현대화하기 위해 광범위하세 진행 중인 노력의 일부일 뿐입니다.</p><p>자세한 내용이 궁금하시거나 작업 과정을 살펴보고 싶으시다면, 이 <a href="https://github.com/elastic/elasticsearch/pull/139343">풀 리퀘스트</a> 및 <a href="https://github.com/elastic/elasticsearch/issues/138799">메타 이슈</a> 추적 진행 상황을 Github에서 확인하세요.</p><p>해싱을 즐기십시오!</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Elasticsearch로 에이전틱 메모리 관리하기]]></title>
            <description><![CDATA[Elasticsearch를 사용하여 메모리를 관리함으로써 컨텍스트 인식 능력이 뛰어나고 효율적인 에이전트를 생성합니다.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/agentic-memory-management-elasticsearch</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/agentic-memory-management-elasticsearch</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Someshwaran Mohankumar]]></dc:creator>
            <pubDate>Fri, 16 Jan 2026 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/80907fd072e72d6ec902470b449c9f337957a0d7-1280x720.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>새롭게 부상하는 <strong>컨텍스트 엔지니어링</strong> 분야에서는 AI 에이전트에 적절한 정보를 적시에 제공하는 것이 매우 중요합니다. 컨텍스트 엔지니어링의 가장 중요한 측면 중 하나는 AI의 <strong>메모리</strong> 관리입니다. 인간과 마찬가지로 AI 시스템도 정보를 기억하기 위해 단기 메모리와 장기 메모리에 모두 의존합니다. 대규모 언어 모델(LLM) 에이전트가 논리적인 대화를 이어가고 사용자 선호도를 기억하며 이전 결과나 응답을 기반으로 구축하려면 효과적인 메모리 메커니즘을 갖춰야 합니다.</p><p>결국 컨텍스트의 모든 요소가 AI의 응답에 영향을 미칩니다. <em>쓰레기를 넣으면 쓰레기가 나온다(Garbage in, garbage out)</em>는 말이 딱 들어맞습니다.</p><p>이 글에서는 AI 에이전트에 단기 메모리와 장기 메모리가 어떤 의미를 갖는지 설명합니다. 특히 다음과 같은 내용을 다룹니다.</p><ul><li>단기 메모리와 장기 메모리의 차이</li><li>Elasticsearch와 같은 벡터 데이터베이스를 사용하는 Retrieval-Augmented Generation(RAG) 기술과의 관계 및 신중한 메모리 관리가 필요한 이유</li><li>컨텍스트 오버플로 및 컨텍스트 오염 등 메모리 관리 소홀의 위험성</li><li>컨텍스트 가지치기, 요약, 관련 정보만 검색 등 에이전트의 메모리를 유용하면서도 안전하게 유지하는 모범 사례</li><li>Elasticsearch를 사용하여 에이전트가 혼동 없이 협업할 수 있도록 멀티 에이전트 시스템에서 메모리를 공유 및 전파하는 방법</li></ul><h2>AI 에이전트의 단기 메모리와 장기 메모리 비교</h2><p>AI 에이전트의 <strong><em>단기 메모리</em></strong>는 일반적으로 즉각적인 대화 컨텍스트 또는 상태, 즉 현재 채팅 기록이나 활성 세션의 최근 메시지를 의미합니다. 여기에는 사용자의 최근 쿼리와 최근 주고받은 대화가 포함됩니다. 이는 사람이 대화 중에 기억하는 정보와 매우 유사합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/4fbcc6f68055b2bccefc4176297a4ca50056dc0d-764x498.png" style="width: 100%;" /><p>AI 프레임워크는 종종 이러한 임시 메모리를 에이전트 상태의 일부로 유지합니다(예: <a href="https://docs.langchain.com/oss/python/langgraph/persistence#checkpoints">LangGraph의 이 예시</a>처럼 체크포인터를 사용하여 대화 상태를 저장하는 방식). 단기 메모리는 <strong><em>세션 범위로 제한</em></strong>됩니다. 즉, 단일 대화 또는 작업 내에 존재하며 해당 세션이 종료되면 명시적으로 다른 곳에 저장하지 않는 한 재설정되거나 지워집니다. 세션에 종속된 단기 메모리의 예로는 ChatGPT에서 제공하는 <strong><a href="https://help.openai.com/en/articles/8914046-temporary-chat-faq">임시 채팅</a> </strong>기능을 들 수 있습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/150bdf209cda5ed20b59cddf34e624ad1a8016aa-1100x577.png" style="width: 100%;" /><p>반면 <strong><em>장기 메모리</em></strong>는 <strong>여러 대화나 세션에 걸쳐</strong> 지속되는 정보를 말합니다. 이는 에이전트가 시간이 지남에 따라 보유하게 되는 지식, 이전에 학습한 사실, 사용자 선호도 또는 영구적으로 기억하도록 지시한 모든 데이터를 말합니다.</p><p>장기 메모리는 보통 즉각적인 컨텍스트 윈도우 밖에 있는 파일이나 벡터 데이터베이스와 같은 외부 소스에서 저장하고 가져오는 방식으로 구현됩니다. 단기 채팅 기록과 달리 장기 메모리는 모든 프롬프트에 자동으로 포함되지 않습니다. 대신 주어진 시나리오에 따라 에이전트가 관련 도구가 호출될 때 이를 <strong>불러오거나</strong> 검색해야 합니다. 실제로 장기 메모리에는 사용자의 프로필 정보, 에이전트가 작성한 이전 답변이나 분석, 에이전트가 쿼리할 수 있는 지식 기반 등이 포함될 수 있습니다.</p><p>예를 들어 여행 플래너 에이전트가 있다면 <em>단기 메모리</em>는 현재 여행 문의(날짜, 목적지, 예산) 및 해당 채팅에서의 후속 질문의 세부 사항을 포함할 것입니다. 반면에 <em>장기 메모리</em>는 사용자의 일반적인 여행 선호도, 과거 여정 및 이전 세션에서 공유된 기타 사실을 저장할 수 있습니다. 사용자가 나중에 다시 방문했을 때, 에이전트는 이러한 장기 데이터 저장소(예: 사용자는 해변과 산을 좋아하고, 평균 예산은 10만 루피이며, 방문하고 싶은 버킷리스트가 있고, 어린이 친화적인 명소보다는 역사와 문화 체험을 선호함)를 활용하여 매번 사용자에게 다시 묻지 않고 맞춤형 서비스를 제공할 수 있습니다.</p><p>단기 메모리(채팅 기록)는 즉각적인 컨텍스트와 연속성을 제공하는 반면, 장기 메모리는 에이전트가 필요할 때 활용할 수 있는 더 넓은 컨텍스트를 제공합니다. 대부분의 고급 AI 에이전트 프레임워크는 이 두 가지를 모두 지원합니다. 최근 대화를 추적하여 컨텍스트를 유지관리합니다. <em>그리고</em> 장기 저장소에서 정보를 조회하거나 해당 저장소에 저장하는 메커니즘을 제공합니다. 단기 메모리를 관리하여 컨텍스트 윈도우 내에서 유지되도록 하고, 장기 메모리를 관리하여 에이전트가 이전 상호작용과 페르소나를 기반으로 답변을 확립할 수 있도록 합니다.</p><h2>컨텍스트 엔지니어링에서 메모리와 RAG</h2><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/46635aa11ceff89b8d6a26ac3e22da52407d82f3-1600x900.png" style="width: 100%;" /><p><strong><em>실제로 AI 에이전트에 유용한 장기 메모리를 부여하는 방법은 무엇일까요?</em></strong></p><p>장기 메모리를 위한 대표적인 접근 방식 중 하나는 <strong><em>시맨틱 메모리</em></strong>로, <strong>retrieval-augmented generation(RAG)</strong>을 통해 구현되는 경우가 많습니다. 이는 LLM을 Elasticsearch와 같은 외부 지식 저장소 또는 벡터 지원 데이터 저장소와 결합하는 것을 의미합니다. LLM은 프롬프트나 기본 제공 학습에 포함된 정보 이상의 정보가 필요할 때 Elasticsearch를 대상으로 시맨틱 검색을 수행하고 가장 관련성이 높은 결과를 컨텍스트로 프롬프트에 삽입합니다. 이렇게 하면 모델의 유효 컨텍스트에 최근 대화(단기 메모리)뿐만 아니라 즉석에서 가져온 관련 장기 사실도 포함됩니다. 그런 다음 LLM은 자체 추론과 검색된 정보를 기반으로 답변을 구성하여 단기 메모리와 장기 메모리를 효과적으로 결합해 보다 정확하고 컨텍스트에 맞는 응답을 생성합니다.</p><p><strong>Elasticsearch</strong>를 사용하여 AI 에이전트의 장기 메모리를 구현할 수 있습니다. 다음은 Elasticsearch에서 컨텍스트 정보를 검색하여 장기 메모리에 저장하는 방법에 대한 개략적인 예시입니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/41ccbc7b5171e8170ac300139a963c0708816ba6-1600x900.png" style="width: 100%;" /><p>이렇게 하면 에이전트가 제한된 프롬프트에 모든 것을 저장하는 대신 관련 데이터를 검색하여 &#x27;기억&#x27;하기 때문에 <strong>다른 위험을 초래</strong>할 수 있습니다.</p><p><strong>Elasticsearch 또는 벡터 저장소와 함께 RAG를 사용하면 다음과 같이 여러 가지 이점이 있습니다.</strong></p><p>첫째, 학습 컷오프를 넘어 모델에 대한 <strong>지식을 확장합니다</strong>. 에이전트는 LLM이 알지 못할 수도 있는 최신 정보 또는 도메인별 데이터를 검색할 수 있습니다. 최근 이벤트나 전문 주제에 대한 질문이 있을 때 유용합니다.</p><p>둘째, 요청 시 컨텍스트를 검색하면 환각을 줄이는 데 도움이 됩니다. 특히 LLM은 귀하의 특정 사용 사례에 상대적인 독점적 또는 고도로 전문화된 데이터에 대해 훈련되지 않았기 때문에 환각에 노출될 가능성이 매우 높습니다. 최근 OpenAI 논문(<a href="https://arxiv.org/pdf/2509.04664">왜 언어 모델이 환각을 일으키는 이유(Why Language Models Hallucinate)</a>에서 강조된 것처럼 LLM이 평가를 통해 추측하거나 새로운 정보를 창조하는 대신, 이 모델은 Elasticsearch의 사실 참조를 통해 근거를 마련할 수 있습니다. 당연히 LLM은 벡터 저장소 내 데이터의 신뢰성에 의존하여 잘못된 정보를 진정으로 방지하고, 관련 데이터는 핵심 관련성 측정에 따라 검색됩니다.</p><p>셋째, RAG를 사용하면 에이전트가 프롬프트에 담을 수 있는 것보다 훨씬 더 큰 지식 기반을 활용할 수 있습니다. 긴 연구 논문이나 정책 문서와 같은 전체 문서를 컨텍스트 윈도우에 입력하여 과부하를 초래하거나 관련 없는 정보로 인해 모델의 추론이 <a href="https://www.elastic.co/search-labs/blog/agentic-memory-management-elasticsearch#context-poisoning">컨텍스트 오염</a>될 위험을 감수하는 대신, RAG는 <a href="https://www.elastic.co/search-labs/blog/chunking-strategies-elasticsearch">청킹</a> 방식을 사용합니다. 대용량 문서는 의미론적으로 유의미한 더 작은 조각으로 분할되며, 시스템은 쿼리와 가장 관련성이 높은 몇 개의 조각만 검색합니다. 이렇게 하면 모델은 지식이 풍부해 보이기 위한 수백만 개의 토큰으로 이루어진 컨텍스트가 필요하지 않고, 훨씬 더 큰 코퍼스에서 적절한 청크에만 액세스하기만 하면 됩니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/e6897356c9f0940e35a63d005e9cd20bc33e5dd7-1600x931.png" style="width: 100%;" /><p>LLM 컨텍스트 윈도우가 커짐에 따라(<a href="https://www.anthropic.com/news/1m-context">일부 모델은 이제 수십만, 심지어 수백만 개의 토큰을 지원함</a><em>)</em> RAG가 &#x27;사라졌는지&#x27;에 대한 논쟁이 제기되었다는 점에 주목할 필요가 있습니다. 모든 데이터를 프롬프트에 넣으면 안 되는 이유가 무엇일까요? 만약 여러분도 같은 생각을 하고 있다면, 제 동료인 제프리 렌기포(Jeffrey Rengifo)와 에두아르트 마틴(Eduard Martin)이 쓴 <a href="https://www.elastic.co/search-labs/blog/rag-vs-long-context-model-llm">더 긴 컨텍스트 ≠ 더 나은 것: RAG가 여전히 중요한 이유(Longer context ≠ better: Why RAG still matters)</a>라는 훌륭한 글을 참고해 보세요. 이렇게 하면 &#x27;쓰레기를 넣으면 쓰레기가 나온다&#x27;는 문제를 피할 수 있습니다. LLM은 노이즈를 처리하는 대신 중요한 몇 개의 청크에 집중할 수 있습니다.</p><p>즉, Elasticsearch 또는 벡터 저장소를 AI 에이전트 아키텍처에 통합하면 <strong>장기적인 메모리</strong>를 제공합니다. 에이전트는 지식을 외부 저장소에 저장하고 필요할 때 메모리 컨텍스트로 불러옵니다. 이는 각 사용자 쿼리 후에 에이전트가 Elasticsearch에서 관련 정보를 검색한 다음 LLM을 호출하기 전에 프롬프트에 상위 결과를 추가하는 <em>아키텍처</em>로 구현할 수 있습니다. 응답에 유용한 새로운 정보가 포함되어 있으면 장기 저장소에 다시 저장할 수도 있습니다(학습의 피드백 루프 생성). 이러한 검색 기반 메모리를 사용함으로써 에이전트는 모든 프롬프트에 알고 있는 모든 것을 집어넣을 필요 없이 정보를 유지하고 최신 상태로 유지합니다. 컨텍스트 윈도우가 <em>100만 토큰</em>을 지원한다고 하더라도 마찬가지입니다. 이 기술은 정보 검색과 생성형 AI의 강점을 결합한 컨텍스트 엔지니어링의 초석입니다. </p><p>다음은 세션 중 단기 메모리에 대한 LangGraph의 체크포인트 시스템을 사용하여 관리되는 인메모리 대화 상태의 예시입니다. (<a href="https://github.com/someshwaranM/elastic-context-engineering-short-term-long-term-memory">지원되는 컨텍스트 엔지니어링 앱</a>을 참조하세요.)</p><pre><code># Initialize chat memory (Note: This is in-memory only, not persistent)
memory = MemorySaver()

# Create a LangGraph agent
langgraph_agent = create_react_agent(model=llm, tools=tools, checkpointer=memory)

...
...
# Only process and display checkpoints if verbose mode is enabled
if args.verbose:
    # List all checkpoints that match a given configuration
    checkpoints = memory.list({"configurable": {"thread_id": "1"}})
    # Process the checkpoints
    process_checkpoints(checkpoints)</code></pre><p><strong>체크포인트</strong>를 저장하는 방법은 다음과 같습니다.</p><pre><code>Checkpoint:
Timestamp: 2025-12-30T09:19:41.691087+00:00
Checkpoint ID: 1f0e560a-c2fa-69ec-8001-14ee5373f9cf
User: Hi I'm Som, how are you? (Message ID: ad0a8415-5392-4a58-85ad-84154875bbf2)
Agent: Hi Som! I'm doing well, thank you! How about you? (Message ID: 
56d31efb-14e3-4148-806e-24a839799ece)
Agent:  (Message ID: lc_run--019b6e8e-553f-7b52-8796-a8b1fbb206a4-0)

Checkpoint:
Timestamp: 2025-12-30T09:19:40.350507+00:00
Checkpoint ID: 1f0e560a-b631-6a08-8000-7796d108109a
User: Hi I'm Som, how are you? (Message ID: ad0a8415-5392-4a58-85ad-84154875bbf2)
Agent: Hi Som! I'm doing well, thank you! How about you? (Message ID: 
56d31efb-14e3-4148-806e-24a839799ece)

Checkpoint:
Timestamp: 2025-12-30T09:19:40.349027+00:00
Checkpoint ID: 1f0e560a-b62e-6010-bfff-cbebe1d865f6</code></pre><p>장기 메모리의 경우, Elasticsearch에서 시맨틱 검색을 수행하여 체크포인트를 요약하고 색인한 후 벡터 임베딩을 사용하여 관련 이전 대화를 검색하는 방법은 다음과 같습니다.</p><pre><code>Functions: 
retrieve_from_elasticsearch() 

# Enhanced Elasticsearch retrieval with rank_window and verbose display
def retrieve_from_elasticsearch(query: str, k: int = 5, rank_window: int = None) -> tuple[List[Dict[str, Any]], str]:
    """
    Retrieve context from Elasticsearch with score-based ranking
    
    Args:
        query: Search query
        k: Number of results to return
        rank_window: Number of candidates to retrieve before ranking (default: args.rank_window)
        
    Returns:
        Tuple of (retrieved_documents, formatted_context_string)
    """
    if not es_client or not es_index_name:
        return [], "Elasticsearch is not available. Cannot search long-term memory."
    
    if rank_window is None:
        rank_window = args.rank_window
    
    try:
        # Check if index exists and has documents
        if not es_client.indices.exists(index=es_index_name):
            return [], "No previous conversations stored in long-term memory yet."
        
        # Get document count
        try:
            doc_count = es_client.count(index=es_index_name)["count"]
            if doc_count == 0:
                return [], "Long-term memory is empty. No previous conversations to search."
        except Exception as e:
            return [], f"Error checking memory: {str(e)}"
        
        # Generate embedding for the query
        try:
            query_embedding = embeddings.embed_query(query)
        except Exception as e:
            return [], f"Error generating embedding: {str(e)}"
        
        # Perform semantic search using kNN with rank_window
        try:
            search_body = {
                "knn": {
                    "field": "vector",
                    "query_vector": query_embedding,
                    "k": k,
                    "num_candidates": rank_window  # Retrieve more candidates, then rank top k
                },
                "_source": ["text", "content", "message_type", "timestamp", "thread_id"],
                "size": k
            }
            
            response = es_client.search(index=es_index_name, body=search_body)
            
            if not response.get("hits") or len(response["hits"]["hits"]) == 0:
                return [], "No relevant previous conversations found in long-term memory."
            
            # Extract documents with scores
            retrieved_docs = []
            for hit in response["hits"]["hits"]:
                source = hit["_source"]
                score = hit["_score"]
                retrieved_docs.append({
                    "content": source.get("content", source.get("text", "")),
                    "message_type": source.get("message_type", "unknown"),
                    "timestamp": source.get("timestamp", "unknown"),
                    "thread_id": source.get("thread_id", "unknown"),
                    "score": score
                })
            
            # Format context string
            context_parts = []
            for i, doc in enumerate(retrieved_docs, 1):
                context_parts.append(doc["content"])
            
            context_string = "\n\n".join(context_parts)
            
            # Verbose display
            if args.verbose:
                rich.print(f"\n[bold yellow]🔍 RETRIEVAL ANALYSIS[/bold yellow]")
                rich.print("="*80)
                rich.print(f"[blue]Query:[/blue] {query}")
                rich.print(f"[blue]Retrieved:[/blue] {len(retrieved_docs)} documents (from {rank_window} candidates)")
                rich.print(f"[blue]Total context length:[/blue] {len(context_string)} characters\n")
                
                for i, doc in enumerate(retrieved_docs, 1):
                    rich.print(f"[cyan]📄 Document {i} | Score: {doc['score']:.4f} | Type: {doc['message_type']}[/cyan]")
                    rich.print(f"[cyan]   Timestamp: {doc['timestamp']} | Thread: {doc['thread_id']}[/cyan]")
                    content_preview = doc['content'][:200] + "..." if len(doc['content']) > 200 else doc['content']
                    rich.print(f"[cyan]   Content: {content_preview}[/cyan]")
                    rich.print("-" * 80)
            
            return retrieved_docs, context_string
            
        except Exception as e:
            return [], f"Error searching memory: {str(e)}"
            
    except Exception as e:
        return [], f"Error accessing long-term memory: {str(e)}"</code></pre><p>이제 Elasticsearch에서 LangGraph의 체크포인트를 사용해 단기 메모리와 장기 메모리를 색인하고 가져오는 방법을 알아보았으니, 전체 대화를 색인하고 덤핑하는 것이 왜 위험한지 잠시 시간을 내어 살펴보겠습니다.</p><h2>컨텍스트 메모리를 관리하지 않을 경우의 위험성</h2><p>컨텍스트 엔지니어링과 단기 및 장기 메모리에 대해 많이 이야기하고 있으니, 에이전트의 메모리와 컨텍스트를 제대로 관리하지 않으면 어떤 일이 발생하는지 알아보겠습니다.</p><p>안타깝게도 AI의 컨텍스트가 지나치게 길어지거나 잘못된 정보를 포함할 경우 여러 가지 문제가 발생할 수 있습니다. 컨텍스트 윈도우가 커질수록 다음과 같은 <strong>새로운 오류 유형</strong>이 나타납니다.</p><ul><li><strong>컨텍스트 오염</strong></li><li><strong>컨텍스트 방해</strong></li><li><strong>컨텍스트 혼동</strong></li><li><strong>컨텍스트 충돌</strong></li><li><strong>컨텍스트 누출 및 지식 충돌</strong></li><li><strong>환각 및 잘못된 정보</strong></li></ul><p>컨텍스트 관리가 부실할 때 발생하는 이러한 문제점과 기타 위험 요소를 자세히 살펴보겠습니다.</p><h3>컨텍스트 오염</h3><p><em>컨텍스트 오염</em>이란 부정확하거나 해로운 정보가 컨텍스트에 유입되어 모델의 후속 출력에 악영향을 미치는 현상을 말합니다. 흔한 예로는 모델이 착각한 내용을 사실로 받아들여 대화 기록에 삽입하는 경우가 있습니다. 그러면 모델은 이후 응답에서 해당 오류를 바탕으로 더 나아가 오류를 증폭시킬 수 있습니다. 반복적인 에이전트 루프에서 잘못된 정보가 공유 컨텍스트(예: 에이전트의 작업 노트 요약)에 들어가면 그 정보는 계속해서 강화될 수 있습니다. </p><p><a href="https://storage.googleapis.com/deepmind-media/gemini/gemini_v2_5_report.pdf">DeepMind 연구진은 Gemini 2.5 보고서</a>(요약본은 <a href="https://www.dbreunig.com/2025/06/17/an-agentic-case-study-playing-pok%C3%A9mon-with-gemini.html">여기</a> 참조)에서 장시간 실행된 <em>포켓몬</em> 게임 에이전트에서 다음과 같은 현상을 관찰했습니다. 에이전트가 잘못된 게임 상태를 환각으로 인식하고, 그 환각이 에이전트의 <em>컨텍스트</em>(목표에 대한 메모리)에 기록되면, 에이전트는 불가능한 목표를 중심으로 <strong>비합리적인 전략</strong>을 세우고 결국 막히게 된다는 것입니다. 다시 말해 오염된 메모리는 에이전트를 잘못된 경로로 무한정 몰아넣을 수 있습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/3f2facf5aad67613ad557422e09ec23a66adc0ed-1600x1388.png" style="width: 100%;" /><p>컨텍스트 오염은 의도치 않게(실수로) 발생할 수도 있고, 악의적으로 발생할 수도 있습니다. 예를 들어 프롬프트 주입 공격을 통해 사용자나 제3자가 에이전트가 기억하고 따르는 숨겨진 지침이나 허위 사실을 몰래 삽입하는 경우가 있습니다.</p><p><strong>권장 대응책</strong></p><p><a href="https://www.wiz.io/academy/data-poisoning">Wiz</a>, <a href="https://zerlo.net/en/blog/what-is-llm-data-poisoning">Zerlo</a>, <a href="https://www.anthropic.com/research/small-samples-poison">Anthropic</a>의 사례를 바탕으로 컨텍스트 오염 방지 대책은 LLM의 프롬프트, 컨텍스트 윈도우 또는 검색 파이프라인에 잘못되거나 오해의 소지가 있는 정보가 유입되는 것을 막는 데 중점을 둡니다. 주요 단계는 다음과 같습니다.</p><ul><li>컨텍스트를 지속적으로 확인하세요. 시작 프롬프트뿐만 아니라 대화 내용이나 검색된 텍스트에서 의심스럽거나 유해한 내용이 있는지 모니터링하세요.</li><li>신뢰할 수 있는 출처를 사용하세요. 신뢰도에 따라 문서에 점수를 매기거나 레이블을 지정하여 시스템이 신뢰할 수 있는 정보를 우선시하고 점수가 낮은 데이터는 무시하도록 하세요.</li><li>비정상적인 데이터를 탐지하세요. 이상하거나 부적절하거나 조작된 콘텐츠를 감지하는 도구를 사용하여 모델이 사용하기 전에 제거하세요.</li><li>입력 및 출력을 필터링하세요. 유해하거나 오해의 소지가 있는 텍스트가 시스템에 쉽게 유입되거나 모델에 의해 반복되지 않도록 안전장치를 추가하세요.</li><li>모델을 정제된 데이터로 업데이트하세요. 시스템을 정기적으로 검증된 정보로 갱신하여 눈에 띄지 않게 침투한 불량 데이터를 차단하세요.</li><li>사람의 개입을 활용하세요(Human-in-the-loop). 중요한 출력물을 사람이 검토하거나 알려진 신뢰할 수 있는 출처와 비교하도록 하세요.</li></ul><p>간단한 사용자 습관도 도움이 됩니다. 긴 채팅을 초기화하고, 관련 정보만 공유하고, 복잡한 작업을 작은 단계로 나누고, 모델 외부에서 깔끔한 메모를 유지관리하는 것 등이 그 예입니다.</p><p>이러한 조치를 종합하면 LLM을 컨텍스트 오염으로부터 보호하고 출력의 정확성과 신뢰성을 유지하는 다층적인 방어 체계가 구축됩니다.</p><p>여기서 언급한 대응책이 없다면 에이전트는 공격자가 삽입한 지시(예: 이전 지침<em> </em>또는 사소한 사실 무시)를 기억하여 악의적인 출력을 생성할 수 있습니다.</p><h3>컨텍스트 방해</h3><p><em>컨텍스트 방해</em>는 컨텍스트가 너무 길어져 모델이 학습 중에 습득한 내용을 무시하고 컨텍스트에 과도하게 집중하는 경우를 말합니다. 극단적인 경우, 이는 <a href="https://en.wikipedia.org/wiki/Catastrophic_interference"><em>치명적인 망각과</em></a> 유사합니다. 즉, 모델이 기본 지식을 사실상 &#x27;망각&#x27;하고 눈앞에 놓인 정보에 지나치게 집착하게 되는 것입니다. 이전 연구에 따르면 LLM은 프롬프트가 극도로 길 때 종종 집중력을 잃는 것으로 나타났습니다.</p><p>예를 들어 Gemini 2.5 에이전트는 100만 토큰 윈도우를 지원했지만, 컨텍스트가 특정 지점(실험에서 10만 토큰 정도)을 넘어서자 새로운 솔루션을 제시하는 대신 <strong>과거의 행동을 반복하는 데 집착하기</strong> 시작했습니다. 말하자면, 에이전트는 방대한 과거 이력에 갇힌 것입니다. 기본 학습 지식을 활용해 새롭고 참신한 전략을 고안하는 대신, 이전 행동의 긴 로그(컨텍스트)를 계속 살펴보고 이를 모방했습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/e08e5b6d2e8ec7e3511d455985eed3d7fa6241e0-1352x636.png" style="width: 100%;" /><p>이는 역효과를 초래합니다. 우리는 모델이 추론을 돕기 위해 관련 컨텍스트를 사용하길 원하지, 모델의 사고 능력을 무시하길 원하지 않습니다. 특히, 방대한 데이터 윈도우를 가진 모델조차도 이러한 <a href="https://research.trychroma.com/context-rot"><em>컨텍스트 왜곡</em></a> 현상을 보입니다. 즉, 토큰이 추가될수록 성능이 불균일하게 저하됩니다. <em>주의력 예산</em>이 있는 것으로 보입니다. 작업 기억이 제한된 인간과 마찬가지로 LLM도 토큰에 집중할 수 있는 용량이 한정되어 있으며, 그 예산이 늘어날수록 정확도와 집중력이 떨어집니다.</p><p>이러한 문제를 완화하기 위해 청킹, 적절한 정보 엔지니어링, 정기적인 컨텍스트 요약, 점수화를 통한 응답의 정확성을 측정하는 평가 및 모니터링 기법을 사용하여 컨텍스트 방해를 방지할 수 있습니다.</p><p>이러한 방법은 모델이 관련 컨텍스트와 기본 학습에 기반을 두도록 하여 주의가 산만해질 위험을 줄이고 전반적인 추론 품질을 향상합니다.</p><h3>컨텍스트 혼동</h3><p><em>컨텍스트 혼동</em>이란 모델이 컨텍스트에 있는 불필요한 콘텐츠를 사용하여 품질이 낮은 응답을 생성하는 것을 말합니다. 대표적인 예로 에이전트에 사용할 수 있는 많은 도구나 API 정의를 제공하는 경우를 들 수 있습니다. 이러한 도구 중 상당수가 현재 작업과 관련이 없더라도, 모델은 컨텍스트에 있다는 이유만으로 부적절하게 사용하려고 시도할 수 있습니다. 실험 결과, 필요하지 않은 도구나 문서를 <em>더 많이</em> 제공하면 오히려 성능이 <em>저하</em>될 수 있다는 사실이 밝혀졌습니다. 에이전트가 잘못된 함수를 호출하거나 관련 없는 텍스트를 참조하는 등의 오류를 범하기 시작하는 것입니다. </p><p>한 사례에서 소형 <strong>Llama 3.1 8B</strong> 모델은 고려해야 할 도구가 46개 주어졌을 때는 작업에 실패했지만, 19개만 주어졌을 때는 성공했습니다. 컨텍스트 길이가 제한 내에 있었음에도 불구하고, 추가 도구는 혼동을 야기했습니다. 근본적인 문제는 프롬프트에 포함된 모든 정보가 모델에 의해 <em>처리</em>된다는 점입니다. 모델이 어떤 정보를 무시해야 하는지 알지 못하면, 그 정보가 모델의 출력에 원치 않는 영향을 미칠 수 있습니다. 관련 없는 정보가 모델의 주의를 분산시켜 잘못된 방향으로 이끌 수 있습니다(예를 들어 관련 없는 문서로 인해 에이전트가 질문과 다른 답변을 할 수 있습니다). 컨텍스트 혼동은 종종 모델이 관련 없는 컨텍스트를 통합하여 품질이 낮은 응답을 생성하는 형태로 나타납니다. 관련 연구 논문 <a href="https://arxiv.org/pdf/2411.15399">적을수록 낫다: 엣지 디바이스에서의 LLM 실행을 위한 함수 호출 최적화(Less is More: Optimizing Function Calling for LLM Execution on Edge Devices)</a>를 참조하세요.</p><p>이는 더 많은 컨텍스트가 항상 더 나은 것은 아니라는 점을 상기시켜 줍니다. 특히 관련성에 맞게 <strong>선별</strong>되지 않았다면 더욱 그렇습니다.</p><h3>컨텍스트 충돌</h3><p><em>컨텍스트 충돌</em>은 <strong>컨텍스트의 일부가 서로 모순되어</strong> 모델의 추론을 방해하는 내부 불일치를 일으킬 때 발생합니다. 에이전트가 충돌하는 여러 정보를 축적하는 경우 충돌이 발생할 수 있습니다. </p><p>예를 들어 에이전트가 두 개의 출처에서 데이터를 가져왔다고 가정해 보겠습니다. 하나는 <em>A 항공편이 오후 5시에 출발한다</em>고 하고, 다른 하나는 <em>A 항공편이 오후 6시에 출발한다</em>고 합니다. 두 사실이 모두 컨텍스트에 포함되면, 성능이 떨어지는 모델은 어느 것이 정확한지 알 수 없습니다. 혼란스러워하거나 부정확한 답변 또는 유사하지 않은 답변을 생성할 수 있습니다.</p><p>컨텍스트 충돌은 모델이 <strong>이전에 시도</strong>했던 답변이 나중에 추가된 정보와 함께 컨텍스트에 남아 있는 다중 턴 대화에서도 자주 발생합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/500d7a80dc8db1923f9b5ca84728eed64fa296f7-1316x580.png" style="width: 100%;" /><p>Microsoft와 Salesforce의 <a href="https://arxiv.org/pdf/2505.06120">연구에</a> 따르면 복잡한 쿼리를 여러 번의 챗봇 턴 대화로 나누어 (세부 정보를 점진적으로 추가하여) 답변할 경우, 모든 정보를 한 번에 제공하는 경우에 비해 최종 정확도가 크게 떨어지는 것으로 나타났습니다. 왜 그럴까요? 초기 턴에는 모델의 부분적이거나 부정확한 중간 답변이 포함되어 있고 이러한 답변이 컨텍스트에 남아 있기 때문입니다. 나중에 모델이 모든 정보를 가지고 답변을 시도할 때, 모델의 <em>메모리</em>에는 여전히 이러한 잘못된 답변이 포함되어 있으며, 이는 수정된 정보와 충돌하여 잘못된 방향으로 나아가게 합니다. 본질적으로 대화의 컨텍스트가 스스로 충돌하는 것입니다. 모델은 새로운 정보를 추가한 후에도 적용되지 않는 오래된 컨텍스트(이전 턴의 컨텍스트)를 의도치 않게 사용할 수 있습니다.</p><p>에이전트 시스템에서 컨텍스트 충돌은 특히 위험한데, 에이전트가 서로 다른 도구나 하위 에이전트의 출력을 결합할 수 있기 때문입니다. 이러한 출력이 서로 일치하지 않으면 통합된 컨텍스트가 일관성을 잃게 됩니다. 그러면 에이전트는 모순을 해결하려다 오류가 발생하거나 비합리적인 결과를 생성할 수 있습니다. 컨텍스트 충돌을 방지하려면 컨텍스트를 <strong>최신 상태로 유지하고 일관성을 보장</strong>해야 합니다.<strong> </strong>예를 들어 오래된 정보를 삭제하거나 업데이트하고, 일관성 검증을 거치지 않은 소스를 혼합해서 사용하지 않아야 합니다.</p><h3>컨텍스트 누출 및 지식 충돌</h3><p>여러 에이전트 또는 사용자가 메모리 저장소를 공유하는 시스템에서는 컨텍스트 간에 정보가 유출될 위험이 있습니다.</p><p>예를 들어 적절한 액세스 제어 없이 두 사용자의 데이터 임베딩이 동일한 벡터 데이터베이스에 저장된 경우, 사용자 A의 쿼리에 응답하는 에이전트가 실수로 사용자 B의 메모리 일부를 가져올 수 있습니다. 이러한 <strong><em>컨텍스트 간 누출</em></strong>은 개인 정보를 노출하거나 응답에 혼란을 초래할 수 있습니다.</p><p><a href="https://wtit.com/blog/2025/04/17/owasp-top-10-for-llm-applications-2025/">LLM 애플리케이션을 위한 OWASP Top 10</a>에 따르면 멀티테넌트 벡터 데이터베이스는 이러한 누출을 방지해야 합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/8f0ccf0b2f7bd6715c14aceee2deffb213d50bd9-1600x936.png" style="width: 100%;" /><p><a href="https://wtit.com/blog/2025/04/17/owasp-top-10-for-llm-applications-2025/">LLM08:2025 벡터 및 임베딩 약점</a>에 따르면<em>,</em> 일반적인 위험 중 하나는 컨텍스트 누출입니다.</p><blockquote><em>여러 사용자 또는 애플리케이션이 동일한 벡터 데이터베이스를 공유하는 멀티테넌트 환경에서는 사용자 또는 쿼리 간에 컨텍스트 누출 위험이 있습니다. 데이터 페더레이션 지식 충돌 오류는 여러 소스의 데이터가 서로 모순될 때 발생할 수 있습니다. 또한 LLM이 학습 과정에서 얻은 기존 지식을 검색 증강을 통해 얻은 새로운 데이터로 대체하지 못할 때도 이러한 오류가 발생할 수 있습니다.</em></blockquote><p>또 다른 측면은 LLM이 <strong>기본 제공 지식</strong>을 메모리의 새로운 정보로 재정의하는 데 어려움을 겪을 수 있다는 것입니다. 모델이 특정 사실에 기반하여 학습되었는데 검색된 컨텍스트가 그와 반대되는 내용을 담고 있다면 모델은 어떤 것을 신뢰해야 할지 혼란스러워할 수 있습니다. 적절한 설계가 없으면 에이전트가 컨텍스트를 혼동하거나 기존 지식을 새로운 증거로 업데이트하지 못하여 오래되거나 부정확한 답변을 내놓을 수 있습니다.</p><h3><strong>환각 및 잘못된 정보</strong></h3><p><em>환각</em>(LLM이 그럴듯하지만, 틀린 정보를 만드는 것)은 긴 설명이 필요 없는 잘 알려진 문제로, 메모리 관리가 제대로 이루어지지 않으면 더욱 심해질 수 있습니다. </p><p>에이전트의 메모리에 중요한 사실이 누락된 경우, 모델은 <strong>추측으로 그 공백을 채울 수 있으며</strong>, 만약 그 추측이 컨텍스트에 들어가게 되면(컨텍스트를 오염시키면서) 오류가 지속됩니다. </p><p>OWASP LLM 보안 보고서<a href="https://wtit.com/blog/2025/04/17/owasp-top-10-for-llm-applications-2025/"><strong>(LLM09:2025 잘못된 정보)</strong></a>는 잘못된 정보를 핵심 취약점으로 지적합니다. LLM은 확신에 찬 듯 보이지만 조작된 답변을 생성할 수 있으며, 사용자는 이를 과신할 수 있습니다. 장기 메모리가 불량하거나 오래된 에이전트는 메모리가 최신 상태로 유지되지 않으면 작년에는 사실이었지만 지금은 거짓인 정보를 확신에 차서 인용할 수 있습니다. </p><p>AI의 출력에 지나치게 의존하는 것(사용자 또는 에이전트 자체가 반복적인 과정에서)은 이러한 문제를 악화시킬 수 있습니다. 메모리에 저장된 정보를 아무도 검증하지 않으면 에이전트는 잘못된 정보를 축적하게 됩니다. 이것이 바로 RAG가 환각을 줄이는 데 자주 사용되는 이유입니다. RAG를 통해 권위 있는 출처를 검색함으로써 모델은 사실을 만들어낼 필요가 없습니다. 하지만 잘못된 문서(예: 잘못된 정보가 포함된 문서)를 검색하거나 초기 환각을 제거하지 않으면 시스템은 해당 잘못된 정보를 전체 작업에 전파할 수 있습니다. </p><p>결론적으로 메모리 관리에 실패하면 <strong>부정확하고 오해의 소지가 있는 출력</strong>으로 이어질 수 있으며, 이는 특히 위험 부담이 큰 경우(예: 금융이나 의료 분야에서 잘못된 조언) 심각한 피해를 초래할 수 있습니다. 에이전트는 컨텍스트에 있는 내용을 무조건 신뢰하는 것이 아니라 메모리 내용을 검증하거나 수정할 수 있는 메커니즘을 갖춰야 합니다.</p><p>요약하자면 AI 에이전트에 무한히 긴 메모리를 제공하거나 가능한 모든 것을 컨텍스트에 넣는 것은 <em>성공의 비결이 아닙니다</em>.</p><h2>LLM 애플리케이션에서의 메모리 관리 모범 사례</h2><p>위에서 언급한 문제점을 방지하고자 개발자와 연구자는 AI 시스템에서 <strong>컨텍스트와 메모리를 관리하는 여러 가지 모범 사례</strong>를 고안했습니다. 이러한 사례는 AI의 작업 컨텍스트를 간결하고 관련성 있으며 최신 상태로 유지하는 것을 목표로 합니다. 다음은 주요 전략 몇 가지와 그 효과를 보여주는 예시입니다.</p><h3>RAG: 타겟팅된 컨텍스트 사용</h3><p>RAG에 대한 내용은 이전 섹션에서 이미 많이 다루었습니다. 이 부분은 실용적인 핵심 사항을 간략하게 다시 한번 상기시켜 드리는 것입니다.</p><ul><li>대량 로딩이 아닌 대상 검색을 사용하세요. 전체 문서나 전체 대화 기록을 프롬프트에 입력하는 대신 가장 관련성이 높은 청크만 검색하세요.</li><li>RAG를 적절한 시기에 메모리를 불러오는 기능으로 활용하세요. 모든 정보를 턴 전반에 가져가지 말고, 필요할 때만 컨텍스트를 가져오세요.</li><li>관련성 인식 검색 전략을 선호하세요. 상위 k개 시멘틱 검색, 상호 순위 융합 또는 도구 구성 필터링과 같은 접근 방식은 노이즈를 줄이고 근거를 개선하는 데 도움이 됩니다.</li><li>컨텍스트 윈도우가 크다고 해서 RAG의 필요성이 없어지는 것은 아닙니다. 관련성이 높은 두 갱의 단락이 관련성이 낮은 20페이지 분량의 문서보다 거의 항상 더 효과적입니다.</li></ul><p>즉, RAG는 더 많은 컨텍스트를 추가하는 것이 아니라 적절한 컨텍스트를 추가하는 것입니다.</p><h3>도구 구성</h3><p><em>도구 구성</em>은 모델에 특정 작업에 실제로 필요한 도구만 제공하는 것을 의미합니다. 이 용어는 게임에서 유래했는데, 상황에 맞는 구성을 선택하는 것과 같습니다. 도구가 너무 많으면 속도가 느려지고, 잘못된 도구는 실패로 이어집니다. 연구 논문 <a href="https://arxiv.org/abs/2411.15399">적을수록 낫다(Less is more)</a>에 따르면 LM도 마찬가지로 동작합니다. 도구가 약 30개를 넘어서면 설명이 중복되어 모델이 혼란스러워집니다. 100개를 넘어서면 실패가 거의 확실해집니다. 이는 컨텍스트 윈도우 문제가 아니라 컨텍스트 혼동 문제입니다.</p><p>간단하면서도 효과적인 해결책은 <a href="https://arxiv.org/abs/2505.03275"><strong>RAG-MCP</strong></a>입니다. 모든 도구를 프롬프트에 나열하는 대신, 도구 설명을 벡터 데이터베이스에 저장하고 요청 시 가장 관련성이 높은 도구만 불러옵니다. 실제로 이렇게 하면 도구 구성이 간결하고 집중적으로 유지되고 프롬프트 표시 시간이 크게 단축되며 도구 선택 정확도가 최대 3배까지 향상될 수 있습니다.</p><p>소규모 모델일수록 이러한 한계에 더 빨리 부딪힙니다. 연구 결과에 따르면 80억 단위의 모델은 수십 개의 도구를 사용할 때는 제대로 작동하지 않지만, 도구 구성을 줄이면 성공하는 것으로 나타났습니다. 필요한 도구를 추론하여 동적으로 선택하는 방식(때로는 LLM을 먼저 사용하는 방식)은 성능을 44% 향상하면서 전력 소비와 지연 시간도 줄일 수 있습니다. 핵심은 대부분의 에이전트는 몇 개의 도구만 필요하지만, 시스템 규모가 커질수록 도구 구성과 RAG-MCP가 설계에서 가장 중요한 고려 사항이 된다는 것입니다.</p><h3>컨텍스트 가지치기: 채팅 기록 길이 제한</h3><p>대화가 여러 차례 이어지면 누적된 채팅 기록이 너무 커져서 컨텍스트 오버플로가 발생하거나 모델에 방해가 될 수 있습니다. </p><p><em>트리밍</em>이란 대화가 길어짐에 따라 중요도가 낮은 부분을 프로그램적으로 제거하거나 축약하는 것을 의미합니다. 간단한 방법으로는 특정 한계에 도달했을 때 가장 오래된 대화 내용을 삭제하고 최근 <em>N</em>개의 메시지만 남기는 것이 있습니다. 더 정교한 가지치기는 관련 없는 곁가지 이야기나 더 이상 필요하지 않은 이전 지시 사항을 제거할 수 있습니다. 목표는 <strong>컨텍스트 윈도우를 오래된 정보로 어지럽히지 않고 깔끔하게 유지</strong>하는 것입니다. </p><p>예를 들어 에이전트가 10턴 전에 하위 문제를 해결했고 그 이후로 넘어갔다면 더 이상 필요하지 않다고 가정하여 해당 기록 부분을 컨텍스트에서 삭제할 수 있습니다(더 이상 필요하지 않다고 가정). 많은 채팅 기반 구현이 이와 같은 방식을 사용합니다. 즉, 최근 메시지의 롤링 윈도우를 유지관리합니다. </p><p>트리밍은 대화의 앞부분이 요약되었거나 관련성이 없다고 판단되면 해당 부분을 &#x27;잊어버리는&#x27; 것처럼 간단하게 할 수 있습니다. 이렇게 하면 컨텍스트 오버플로 오류 위험과 <a href="https://www.elastic.co/search-labs/blog/agentic-memory-management-elasticsearch#context-distraction"><strong>컨텍스트 방해</strong></a>를 줄여 모델이 오래되거나 주제에서 벗어난 콘텐츠에 현혹되지 않도록 할 수 있습니다. 이는 사람이 한 시간짜리 대화의 모든 단어는 기억하지 못하지만 핵심 내용은 기억하는 것과 매우 유사합니다. </p><p>컨텍스트 가지치기가 헷갈린다면, 저자 드류 브루니그(Drew Breunig)가 <a href="https://www.dbreunig.com/2025/06/26/how-to-fix-your-context.html#tool-loadout:~:text=Provence%20is%20fast%2C%20accurate%2C%20simple%20to%20use%2C%20and%20relatively%20small%20%E2%80%93%20only%201.75%20GB.%20You%20can%20call%20it%20in%20a%20few%20lines%2C%20like%20so%3A">여기</a>서 강조했듯이, 질문 답변을 위한 가볍고(1.75GB), 효율적이며 정확한 컨텍스트 가지치기 도구인 Provence (`<a href="https://huggingface.co/naver/provence-reranker-debertav3-v1">naver/provence-reranker-debertav3-v1</a>`) 모델을 사용해 보시면 도움이 될 수 있습니다. 이 모델은 방대한 문서를 특정 쿼리에 가장 관련성이 높은 텍스트만 남기도록 다듬어 줍니다. 특정 간격으로 호출할 수도 있습니다.</p><p>컨텍스트 가지치기를 위해 코드에서 `provence-reranker` 모델을 호출하는 방법은 다음과 같습니다.</p><pre><code># Context pruning with Provence
def prune_with_provence(query: str, context: str, threshold: Optional[float] = None) -> str:
    """
    Prune context using Provence reranker model
    
    Args:
        query: User's query/question
        context: Original context to prune
        threshold: Relevance threshold (0-1) for Provence reranker.
                   If None, uses args.pruning_threshold.
                   0.1 = conservative (recommended, no performance drop)
                   0.3-0.5 = moderate to aggressive pruning
    
    Returns:
        Pruned context with only relevant sentences
    """
    if provence_model is None:
        return context
    
    if threshold is None:
        threshold = args.pruning_threshold
    
    try:
        # Use Provence's process method
        provence_output = provence_model.process(
            question=query,
            context=context,
            threshold=threshold,
            always_select_title=False,
            enable_warnings=False
        )
        
        # Extract pruned context from output
        pruned_context = provence_output.get('pruned_context', context)
        reranking_score = provence_output.get('reranking_score', 0.0)
        
        # Log statistics
        original_length = len(context)
        pruned_length = len(pruned_context)
        reduction_pct = ((original_length - pruned_length) / original_length * 100) if original_length > 0 else 0
        
        if args.verbose:
            rich.print(f"[cyan]📊 Pruning stats: {pruned_length}/{original_length} chars ({reduction_pct:.1f}% reduction, threshold={threshold:.2f}, rerank_score={reranking_score:.3f})[/cyan]")
        
        return pruned_context if pruned_context else context
        
    except Exception as e:
        rich.print(f"[yellow]⚠️ Error in Provence pruning: {str(e)}[/yellow]")
        rich.print(f"[yellow]⚠️ Falling back to original context[/yellow]")
        return context</code></pre><p>문장 관련성 점수 계산에는 Provence reranker 모델(`naver/provence-reranker-debertav3-v1`)을 사용합니다. 임계값 기반 필터링을 통해 관련성 임계값 이상의 문장만 남깁니다. 또한 가지치기가 실패할 경우 원래 컨텍스트으로 되돌아가는 폴백(fallback) 메커니즘을 도입했습니다. 마지막으로 상세 모드에서는 통계 로깅을 통해 감소율을 추적합니다.</p><h3>컨텍스트 요약: 이전 정보를 완전히 삭제하는 대신 축약</h3><p><em>요약</em>은 트리밍의 동반자입니다. 기록이나 지식 베이스가 너무 커지면 위의 코드에서 수행한 것처럼 LLM을 사용하여 중요한 요점에 대한 간략한 요약을 생성하고 앞으로 전체 콘텐츠 대신 이 요약을 사용할 수 있습니다.</p><p>예를 들어 AI 어시스턴트가 50턴 대화를 나눴다고 가정해 봅시다. 51턴 대화에서 50턴까지 대화를 한꺼번에 보내는 대신(용량 부족 문제 발생 가능성이 높음), 시스템은 1~40턴까지 대화만 처리하고 모델이 이를 요약하여 단락으로 작성하도록 한 다음, 다음 대화에서는 해당 요약본과 마지막 10턴까지의 대화 내용만 제공할 수 있습니다. 이렇게 하면 모델은 모든 세부 정보를 입력받지 않고도 어떤 내용이 논의되었는지 파악할 수 있습니다. 초기 챗봇 사용자는 &quot;지금까지 나눈 대화를 요약해 줄 수 있나요?&quot;라고 수동으로 질문하고, 요약 내용을 바탕으로 새로운 대화를 이어갔습니다. 이제는 이 작업을 자동화할 수 있습니다. 요약은 컨텍스트 윈도우 공간을 절약할 뿐만 아니라 불필요한 세부 정보를 제거하고 중요한 사실만 유지함으로써 <strong>컨텍스트 혼동/방해</strong>를 줄일 수 있습니다.</p><p>여기서는 OpenAI 모델(다른 LLM 모델도 사용 가능)을 활용하여 모든 관련 정보를 보존하면서 컨텍스트를 축약하고 중복 및 비효율성을 제거하는 방법을 설명합니다.<br/></p><pre><code># Context summarization
def summarize_context(query: str, context: str) -> str:
    """
    Summarize context using LLM to reduce duplication and focus on relevant information
    
    Args:
        query: User's query/question
        context: Context to summarize
        
    Returns:
        Summarized context
    """
    try:
        summary_prompt = f"""You are an expert at summarizing conversation context.

Your task: Analyze the provided conversation context and produce a condensed summary that fully answers or supports the user's specific question.

The summary must:
1. Preserve every fact, detail, and information that directly relates to the question
2. Eliminate redundancy and duplicate information
3. Maintain chronological flow when relevant
4. Focus on information that helps answer: "{query}"

Context to summarize:
{context}

Provide a concise summary that preserves all relevant information:"""

        summary = llm.invoke(summary_prompt).content
        
        if args.verbose:
            original_length = len(context)
            summary_length = len(summary)
            reduction_pct = ((original_length - summary_length) / original_length * 100) if original_length > 0 else 0
            rich.print(f"[cyan]📝 Summarization stats: {summary_length}/{original_length} chars ({reduction_pct:.1f}% reduction)[/cyan]")
        
        return summary
        
    except Exception as e:
        rich.print(f"[yellow]⚠️ Error in context summarization: {str(e)}[/yellow]")
        rich.print(f"[yellow]⚠️ Falling back to original context[/yellow]")
        return context</code></pre><p>중요한 것은 컨텍스트가 요약되면 (요약이 정확하다는 가정 하에) 모델이 사소한 세부 사항이나 과거 오류에 압도될 가능성이 줄어든다는 점입니다. </p><p>하지만 요약은 신중하게 해야 합니다. 요약이 잘못되면 중요한 세부 사항이 누락되거나 오류가 발생할 수도 있습니다. 요약은 본질적으로 모델에 대한 또 다른 프롬프트(&quot;이것을 요약하세요&quot;)이기 때문에 뉘앙스가 왜곡되거나 사라질 수 있습니다. 가장 좋은 방법은 점진적으로 요약하고 일부 표준적인 사실은 요약하지 않는 것입니다.</p><p>그럼에도 불구하고 이는 매우 유용한 것으로 입증되었습니다. <a href="https://storage.googleapis.com/deepmind-media/gemini/gemini_v2_5_report.pdf">Gemini 에이전트 시나리오</a>에서 약 10만 토큰마다 컨텍스트를 요약하는 것은 모델의 반복적인 경향을 상쇄하는 방법이었습니다. 요약은 대화 또는 데이터의 압축된 메모리 역할을 합니다. 개발자는 에이전트가 대화 기록이나 긴 문서에 대해 주기적으로 요약 함수(소규모 LLM 또는 전용 루틴)를 호출하도록 구현할 수 있습니다. 결과로 생성된 요약은 프롬프트의 원래 내용을 대체합니다. 이 전략은 컨텍스트를 제한적으로 유지하고 정보를 추출하는 데 널리 사용됩니다.</p><h3>컨텍스트 격리: 가능한 경우 컨텍스트 격리</h3><p>이는 복잡한 에이전트 시스템이나 다단계 워크플로우에서 더욱 중요합니다. 컨텍스트 세분화의 핵심 아이디어는 큰 작업을 각각 고유한 컨텍스트를 가진 더 작고 독립적인 작업으로 나누는 것입니다. 이렇게 하면 모든 것을 포함하는 하나의 거대한 컨텍스트가 누적되는 것을 방지할 수 있습니다. 각 하위 에이전트 또는 하위 작업은 특정 컨텍스트에 집중하여 문제 일부를 해결하고, 상위 에이전트, 감독자 또는 조정자가 결과를 통합합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/f2de68c3339883d7658e633af3948f29f427e6cf-1600x900.png" style="width: 100%;" /><p><a href="https://www.anthropic.com/engineering/multi-agent-research-system">Anthropic의 연구 전략은 여러 개의 하위 에이전트를 활용합니다</a>. 각 하위 에이전트는 고유한 컨텍스트 윈도우를 가지고 질문의 서로 다른 측면을 조사하며, 리드 에이전트는 이러한 하위 에이전트가 도출한 결과를 종합적으로 분석합니다. 이러한 병렬적인 모듈식 접근 방식 덕분에 어느 하나의 컨텍스트 윈도우도 과도하게 복잡해지지 않습니다. 또한 관련 없는 정보가 섞일 가능성을 줄이고, 각 스레드는 주제에서 벗어나지 않으며(컨텍스트 혼동 방지), 특정 하위 질문에 답할 때 불필요한 정보를 포함하지 않습니다. 마치 사고 과정 전체가 아닌 결과만 공유하는 별개의 독립적인 사고 스레드를 실행하는 것과 같습니다.</p><p>멀티 에이전트 시스템에서는 이러한 접근 방식이 필수적입니다. 에이전트 A가 작업 A를 처리하고 에이전트 B가 작업 B를 처리하는 경우, 정말 필요한 경우가 아니라면 어느 에이전트도 다른 에이전트의 전체 컨텍스트를 소비할 이유가 없습니다. 대신 에이전트는 필요한 정보만 교환할 수 있습니다. 예를 들어 에이전트 A는 감독 에이전트를 통해 에이전트 B에 조사 결과의 통합 요약을 전달할 수 있으며, 각 하위 에이전트는 자체적인 전용 컨텍스트 스레드를 유지관리합니다. 이러한 구성은 사람의 개입이 필요하지 않으며, 최소한의 제어된 컨텍스트 공유 기능을 갖춘 도구를 사용하는 감독 에이전트에 의존합니다.</p><p>그럼에도 불구하고 에이전트나 도구가 필요한 컨텍스트 중복을 최소화하면서 작동하도록 시스템을 설계하면 명확성과 성능을 크게 향상할 수 있습니다. 이를 <strong>AI를 위한 마이크로서비스</strong>라고 생각하면 됩니다. 각 구성 요소는 고유한 컨텍스트를 처리하고, 단일 컨텍스트 대신 제어된 방식으로 구성 요소 간에 메시지를 전달합니다. 이러한 모범 사례는 종종 함께 사용됩니다. 또한 이를 통해 사소한 기록을 트리밍하고 중요한 이전 메시지나 대화를 요약하며, 상세한 로그를 Elasticsearch에 저장하여 장기적인 컨텍스트를 유지하고 필요할 때 관련 정보를 검색할 수 있는 유연성을 확보할 수 있습니다.</p><p><a href="https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents#:~:text=While%20some%20models,to%20the%20LLM">앞서</a> 언급했듯이, 핵심 원칙은 컨텍스트는 제한적이고 귀중한 자원이라는 것입니다. 프롬프트의 모든 토큰은 제 역할을 다해야 하며, 출력의 질 향상에 기여해야 합니다. 메모리에 저장된 요소가 제 역할을 하지 못하고 더 나아가 혼동을 야기하는 경우 그 항목을 제거하거나 요약하거나 제외해야 합니다.</p><p>개발자로서 이제 코드를 프로그래밍하듯이 컨텍스트를 프로그래밍할 수 있습니다. 어떤 정보를 포함할지, 어떤 형식으로 표현할지, 언제 생략하거나 업데이트할지 결정할 수 있습니다. 이러한 방식을 따르면 LLM 에이전트가 앞서 설명한 오류 모드에 빠지지 않고 작업을 수행하는 데 필요한 컨텍스트를 제공할 수 있습니다. 결과적으로 에이전트는 기억할 정보는 기억하고, 기억할 필요 없는 정보는 잊고, 필요한 정보는 적시에 검색할 수 있게 됩니다.</p><h2>결론</h2><p>메모리는 에이전트에 추가하는 것이 아니라 설계하는 것입니다. 단기 메모리는 에이전트의 작업 임시 저장소 역할을 하고, 장기 메모리는 영구적인 지식 저장소 역할을 합니다. RAG는 이 둘을 연결하는 다리 역할을 하며, Elasticsearch와 같은 수동적인 데이터 저장소를 능동적인 재현 메커니즘으로 전환하여 출력을 안정적으로 유지하고 에이전트를 최신 상태로 유지합니다.</p><p>하지만 메모리는 양날의 검과도 같습니다. 컨텍스트를 방치하는 순간 오염, 방해, 혼동, 충돌이 발생하고 공유 시스템에서는 데이터 누출까지 일어날 수 있습니다. 그렇기 때문에 가장 중요한 메모리 작업은 &#x27;더 많이 저장&#x27;하는 것이 아니라 &#x27;더 잘 선별&#x27;하는 것입니다. 선택적으로 검색하고 공격적으로 정리하며, 신중하게 요약하고 업무에 꼭 필요한 경우가 아니라면 관련 없는 컨텍스트를 섞지 마세요.</p><p>실제로 훌륭한 컨텍스트 엔지니어링은 훌륭한 시스템 설계와 유사합니다. 즉, 작고 충분한 컨텍스트, 구성 요소 간의 제어된 인터페이스, 그리고 원시 상태와 모델이 실제로 인식해야 하는 정제된 상태를 명확하게 구분하는 것입니다. 제대로 구현하면 모든 것을 기억하는 에이전트를 만드는 것이 아니라, 적시에 적합한 이유로 적절한 정보를 기억하는 에이전트를 만들 수 있습니다.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[더 높은 처리량과 낮은 지연 시간: 성능이 크게 향상된 AWS의 Elastic Cloud Serverless]]></title>
            <description><![CDATA[Elasticsearch Serverless에 대한 AWS 인프라를 더 새롭고 빠른 하드웨어로 업그레이드했습니다. 획기적인 성능 향상이 어떻게 더 빠른 쿼리, 더 나은 확장, 더 저렴한 비용으로 이어지는지 확인해 보세요.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/elasticsearch-serverless-aws-performance-boost</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/elasticsearch-serverless-aws-performance-boost</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Pete Galeotti, Yuvraj Gupta, Rachel Forshee]]></dc:creator>
            <pubDate>Wed, 14 Jan 2026 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/3aa0b481994d2445ba979d3c79fff64c5ee6676a-720x420.jpg?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>Elastic Cloud Serverless는 이미 인프라 관리의 운영 부담 없이 효율적인 검색 및 AI 애플리케이션을 구축하고자 하는 개발자를 위한 최고의 솔루션입니다. 이제 서버리스 프로젝트의 성능을 완전히 새로운 수준으로 끌어올리고자 합니다.</p><p>AWS에서 실행되는 모든 <a href="https://www.elastic.co/cloud/serverless">Elastic Cloud Serverless</a> 프로젝트에 대한 주요 인프라 업그레이드를 완료하여 더 새롭고 빠른 하드웨어로 마이그레이션하였습니다. 해당 변경 사항이 모든 서버리스 프로젝트에 자동으로 적용되었습니다. 이 솔루션은 AWS에서 Elasticsearch, Elastic Observability 및 Elastic Security 서버리스 프로젝트에 <strong>더 높은 처리량과 더 낮은 지연 시간</strong>을 제공합니다.</p><h2><strong>개발자를 위한 주요 성능 혜택</strong></h2><p>새로운 AWS 하드웨어 인프라는 Elastic Cloud Serverless에서 수행하는 모든 작업을 뒷받침하여 애플리케이션의 속도와 반응성에 실질적인 이점을 제공합니다.</p><h3><strong>쿼리 지연 시간 감소 및 처리량 증가</strong></h3><p>향상된 하드웨어가 획기적으로 컴퓨팅 리소스의 속도를 높여 검색 쿼리가 그 어느 때보다 빠르게 처리됩니다.</p><ul><li><strong>검색 및 벡터 검색:</strong> 기존의 풀텍스트 쿼리를 실행하든, <a href="https://www.elastic.co/generative-ai">생성형 AI 및 검색 증강 생성(RAG) 애플리케이션</a>에 최첨단 벡터 검색을 사용하든, 대기 시간이 현저히 줄어듭니다. 내부 벤치마킹 결과 검색 지연 시간이 평균 35% 감소한 것으로 나타났습니다.</li><li><strong>더욱 빠른 색인:</strong> 데이터 수집 속도가 최적화되어 대용량 데이터와 복잡한 문서를 처리량을 높여 색인할 수 있습니다. 이는 거의 실시간 데이터 가시성이 필요한 애플리케이션에 매우 중요합니다. 내부 벤치마킹 결과, 색인 처리량이 평균 26% 증가했습니다.</li></ul><h3><strong>부하 상태에서도 일관된 성능</strong></h3><p>수요에 맞춰 실시간으로 동적으로 자동 확장되도록 설계된 Elastic Cloud Serverless는 작업 부하에 관계없이 대기 시간을 최소화합니다. 이번 하드웨어 업그레이드를 통해 확장이 더욱 향상되고 반응 속도도 빨라졌습니다.</p><ul><li><strong>급증하는 데이터를 손쉽게 처리:</strong> 사용자 트래픽이 갑자기 급증하거나 대규모 배치 데이터 인제스트에 직면하더라도, 새로운 인프라가 검색 및 색인 리소스를 보다 효율적으로 확장하여 낮은 지연 시간을 일관적으로 유지합니다.</li><li><strong>컴퓨팅-저장 공간 분리 최적화:</strong> 서버리스 아키텍처는 컴퓨팅과 저장 공간을 분리하여 워크로드가 독립적으로 확장될 수 있도록 함으로써 최적의 성능과 비용 효율성을 제공합니다. 더 빠른 하드웨어가 컴퓨팅 계층을 향상시켜 분리된 설계의 효율성을 극대화합니다.</li></ul><h2><strong>작동 원리: 내부 벤치마킹 결과</strong></h2><p>Elastic 엔지니어링 팀은 AWS 인프라 업그레이드의 영향을 정량화하기 위해 다양한 서버리스 워크로드를 대상으로 포괄적인 내부 벤치마킹을 수행했습니다. 이러한 워크로드는 사용 사례와 관계없이 애플리케이션 전반에서 기대할 수 있는 성능 향상에 대한 실증적 증거를 제공했습니다.</p><h3><strong>벤치마킹 접근법</strong></h3><p>개발자 경험과 애플리케이션 반응성에 직접적인 영향을 미치는 주요 지표인 응답 시간(지연 시간)과 검색 및 색인 작업 처리량을 집중적으로 테스트했습니다.</p><ul><li><strong>테스트를 마친 워크로드:</strong> 테스트에는 사용자 대면 애플리케이션에서 흔히 볼 수 있는 동시성이 높은 검색 작업, 복잡한 벡터 검색 쿼리, 통합 가시성 및 보안 사용 사례에 대한 대용량 데이터 수집/색인 등이 포함되었습니다. 그중에서도 테스트 방법론에서 Elastic의 벤치마킹 도구인 Rally에 대해 <a href="https://github.com/elastic/rally-tracks/tree/master">공개적으로</a> <a href="https://github.com/elastic/rally-tracks/tree/master">이용 가능한 데이터 세트</a>를 사용했습니다.<ul><li><a href="https://github.com/elastic/rally-tracks/tree/3bedd51/wikipedia"><code>wikipedia</code></a>: 위키피디아 텍스트 콘텐츠의 스냅샷에서 추출된 데이터 세트로, 일반 텍스트 검색 성능을 측정하는 데 사용됩니다.</li><li><a href="https://github.com/elastic/rally-tracks/tree/3bedd51/msmarco-passage-ranking"><code>MSMARCO-Passage-Ranking</code></a>: Microsoft의 Machine Reading Comprehension(MS MARCO)에서 파생된 데이터 세트로, 희소 벡터 필드에서 검색 성능을 측정합니다.</li><li><a href="https://github.com/elastic/rally-tracks/tree/3bedd51/openai_vector"><code>OpenAI_Vector</code></a>: BEIR의 NQ에서 파생된 데이터 세트로, OpenAI의 <code>text-embedding-ada-002</code> 모델이 생성한 임베딩으로 보강되어 조밀한 벡터 필드에서의 검색 성능을 측정하기 위한 것입니다.</li></ul></li><li><strong>측정:</strong> 구식 인프라와 신규 인프라의 성능을 비교했으며, 최악의 경우인 극한 지연 시간 성능과 초당 작업량을 파악하기 위해 99번째 백분위수(P99)에서의 지연 시간을 측정했습니다. 결과의 일관성을 확보하기 위해 하드웨어 프로필마다 각 트랙을 5회씩 실행했습니다.</li><li><strong>목표:</strong> 목표는 빠른 자동 확장 기간에도 전반적으로 일관되게 <strong>더 빠르고 예측 가능한 성능</strong>을 제공하는 인프라의 기능을 검증하는 것이었습니다.</li></ul><h3><strong>성능 데이터 요약</strong></h3><p>그 결과 효율성과 속도가 크게 향상되었음을 확인할 수 있었습니다. 이러한 이점은 사용자 응답 시간 단축과 운영 비용 절감으로 직결됩니다. 더 적은 컴퓨팅 리소스로 동일한 작업을 완료할 수 있기 때문입니다.</p><p>다음 표에는 정량적 개선 사항이 자세히 설명되어 있습니다. 높은 값은 처리량에 더 좋고, 낮은 값은 지연 시간에 더 좋습니다.</p><p><strong>벤치마크 결과 검색:</strong></p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">벤치마크</td><td style="border: 1px solid #ccc; padding: 4px;">비교</td><td style="border: 1px solid #ccc; padding: 4px;">구식 인프라</td><td style="border: 1px solid #ccc; padding: 4px;">새로운 인프라</td><td style="border: 1px solid #ccc; padding: 4px;">차이</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">'위키피디아'(일반 텍스트)</td><td style="border: 1px solid #ccc; padding: 4px;">검색 작업 처리량(초당 연산 수)</td><td style="border: 1px solid #ccc; padding: 4px;">729</td><td style="border: 1px solid #ccc; padding: 4px;">1107</td><td style="border: 1px solid #ccc; padding: 4px;">+52%</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">'위키피디아'(일반 텍스트)</td><td style="border: 1px solid #ccc; padding: 4px;">검색 작업 지연 시간(p99, ms)</td><td style="border: 1px solid #ccc; padding: 4px;">56</td><td style="border: 1px solid #ccc; padding: 4px;">35</td><td style="border: 1px solid #ccc; padding: 4px;">-37%</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">`MSMARCO-Passage-Ranking` (희소 벡터)</td><td style="border: 1px solid #ccc; padding: 4px;">검색 작업 처리량(초당 연산 수)</td><td style="border: 1px solid #ccc; padding: 4px;">22</td><td style="border: 1px solid #ccc; padding: 4px;">31</td><td style="border: 1px solid #ccc; padding: 4px;">+40%</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">`MSMARCO-Passage-Ranking` (희소 벡터)</td><td style="border: 1px solid #ccc; padding: 4px;">검색 작업 지연 시간(p99, ms)</td><td style="border: 1px solid #ccc; padding: 4px;">108</td><td style="border: 1px solid #ccc; padding: 4px;">67</td><td style="border: 1px solid #ccc; padding: 4px;">-38%</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">`OpenAI_Vector`(고밀도 벡터)</td><td style="border: 1px solid #ccc; padding: 4px;">검색 작업 처리량(초당 연산 수)</td><td style="border: 1px solid #ccc; padding: 4px;">475</td><td style="border: 1px solid #ccc; padding: 4px;">624</td><td style="border: 1px solid #ccc; padding: 4px;">+31%</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">`OpenAI_Vector`(고밀도 벡터)</td><td style="border: 1px solid #ccc; padding: 4px;">검색 작업 지연 시간(p99, ms)</td><td style="border: 1px solid #ccc; padding: 4px;">35</td><td style="border: 1px solid #ccc; padding: 4px;">22</td><td style="border: 1px solid #ccc; padding: 4px;">-37%</td>
                    </tr>
              </tbody>
            </table>
          <p><strong>색인 벤치마크 결과:</strong></p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">벤치마크</td><td style="border: 1px solid #ccc; padding: 4px;">비교</td><td style="border: 1px solid #ccc; padding: 4px;">구식 인프라</td><td style="border: 1px solid #ccc; padding: 4px;">새로운 인프라</td><td style="border: 1px solid #ccc; padding: 4px;">차이</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">'위키피디아'(일반 텍스트)</td><td style="border: 1px solid #ccc; padding: 4px;">검색 작업 처리량(초당 연산 수)</td><td style="border: 1px solid #ccc; padding: 4px;">2845</td><td style="border: 1px solid #ccc; padding: 4px;">3220</td><td style="border: 1px solid #ccc; padding: 4px;">+13%</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">'위키피디아'(일반 텍스트)</td><td style="border: 1px solid #ccc; padding: 4px;">검색 작업 지연 시간(p99, ms)</td><td style="border: 1px solid #ccc; padding: 4px;">1769</td><td style="border: 1px solid #ccc; padding: 4px;">1120</td><td style="border: 1px solid #ccc; padding: 4px;">-37%</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">`MSMARCO-Passage-Ranking` (희소 벡터)</td><td style="border: 1px solid #ccc; padding: 4px;">검색 작업 처리량(초당 연산 수)</td><td style="border: 1px solid #ccc; padding: 4px;">7087</td><td style="border: 1px solid #ccc; padding: 4px;">8900</td><td style="border: 1px solid #ccc; padding: 4px;">+26%</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">`MSMARCO-Passage-Ranking` (희소 벡터)</td><td style="border: 1px solid #ccc; padding: 4px;">검색 작업 지연 시간(p99, ms)</td><td style="border: 1px solid #ccc; padding: 4px;">824</td><td style="border: 1px solid #ccc; padding: 4px;">677</td><td style="border: 1px solid #ccc; padding: 4px;">-18%</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">`OpenAI_Vector`(고밀도 벡터)</td><td style="border: 1px solid #ccc; padding: 4px;">검색 작업 처리량(초당 연산 수)</td><td style="border: 1px solid #ccc; padding: 4px;">2972</td><td style="border: 1px solid #ccc; padding: 4px;">3187</td><td style="border: 1px solid #ccc; padding: 4px;">+7%</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">`OpenAI_Vector`(고밀도 벡터)</td><td style="border: 1px solid #ccc; padding: 4px;">검색 작업 지연 시간(p99, ms)</td><td style="border: 1px solid #ccc; padding: 4px;">2946</td><td style="border: 1px solid #ccc; padding: 4px;">2944</td><td style="border: 1px solid #ccc; padding: 4px;">0%</td>
                    </tr>
              </tbody>
            </table>
          <h2><strong>추가 보너스: 비용 절감</strong></h2><p>낮은 지연 시간으로 뛰어난 성능을 제공하는 데 중점을 두고 있지만, 새로운 하드웨어의 효율성은 Elasticsearch 프로젝트의 비용에도 직접적이고 긍정적인 영향을 미칩니다.</p><p><a href="https://www.elastic.co/pricing/serverless-search">Elasticsearch Serverless 요금제</a>는 사용량 기반이므로, 사용한 인제스트 및 검색 리소스에 대해서만 비용을 지불하면 됩니다. 더 빠른 신규 하드웨어는 효율성이 높기 때문에 대부분의 프로젝트에서 종종 더 적은 리소스를 사용하여 워크로드가 작업을 완료하게 되어 본질적인 비용 절감으로 이어집니다. 높은 금액을 지불하지 않고도 프리미엄 수준의 성능 향상을 얻을 수 있습니다. 최적화된 효율성이라고 할 수 있죠.</p><h2><strong>이것이 개발자에게 의미하는 바는 무엇일까요?</strong></h2><p>Elastic에서 이 인프라 업그레이드를 전적으로 관리하므로 손가락 하나 까딱하지 않아도 됩니다. 마이그레이션이나 구성 변경도 필요 없습니다. 이러한 개선 사항은 모든 AWS 기반 서버리스 프로젝트에 즉각적으로 자동 적용됩니다.</p><p>업그레이드의 효과:</p><ul><li><strong>더 빠른 애플리케이션 구축:</strong> 기본 검색 플랫폼이 사용자가 요구하는 속도를 제공한다는 것을 알기 때문에 기능 개발 속도에 집중할 수 있습니다.</li><li><strong>자신감 있는 혁신:</strong> 플랫폼이 최고의 성능으로 부하를 처리할 수 있다는 확신을 가지고 벡터 검색 및 관련성 순위와 같은 복잡한 AI 기능을 포함한 새로운 검색, 통합 가시성 및 보안 기능을 배포하세요.</li><li><strong>스택 간소화:</strong> 인프라 관리, 용량 계획 및 확장을 처리하는 완전 관리형 서비스를 사용하여 코드와 데이터에 집중할 수 있습니다.<br/></li></ul>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[LangGraph와 Elasticsearch로 휴먼 인 더 루프 에이전트 구축하기]]></title>
            <description><![CDATA[LangGraph와 Elasticsearch를 사용하여 인간이 의사결정 과정에 참여하여 맥락적 공백을 채우고 도구 호출을 실행 전에 검토하는 휴먼 인 더 루프 에이전트를 구축하는 방법을 알아보세요.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/human-in-the-loop-agents-langgraph-elasticsearch</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/human-in-the-loop-agents-langgraph-elasticsearch</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Jeffrey Rengifo]]></dc:creator>
            <pubDate>Mon, 05 Jan 2026 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/69634710b14adff494b51b2d57d119196ea6d644-838x532.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>이 문서에서는 <a href="https://langchain-ai.github.io/langgraph/">LangGraph</a>와 Elasticsearch를 결합하여 HITL(휴먼 인 더 루프) 애플리케이션을 구축하는 방법을 살펴봅니다. 이러한 접근 방식을 통해 AI 시스템은 사용자를 의사결정 과정에 직접 참여시켜 더욱 안정적이고 맥락에 맞는 상호작용을 할 수 있습니다. 컨텍스트 기반 시나리오를 사용해 실제 예제를 구현하여 LangGraph 워크플로우가 어떻게 Elasticsearch와 통합되어 데이터를 검색하고, 사용자 입력을 처리하고, 정제된 결과를 생성하는지를 보여드리겠습니다.</p><h2>요건</h2><ul><li>NodeJS 버전 18 이상</li><li>OpenAI API 키</li><li>Elasticsearch 8.x+ 배포</li></ul><h2>LangGraph를 생산 HITL 시스템에 사용하는 이유</h2><p><a href="https://www.elastic.co/search-labs/blog/ai-agent-workflow-finance-langgraph-elasticsearch">이전 문서</a>에서는 LLM과 <a href="https://docs.langchain.com/oss/javascript/langgraph/graph-api#conditional-edges">조건부 엣지</a>를 사용하여 자동으로 의사결정을 내리고 결과를 표시하는 RAG 시스템을 구축하는 데 있어 LangGraph와 그 이점에 대해 소개했습니다. 때로는 시스템이 처음부터 끝까지 자율적으로 작동하는 것이 아니라 사용자가 실행 루프 내에서 옵션을 선택하고 의사 결정을 내리기를 원할 때가 있습니다. 이 개념은 <strong><em>휴먼 인 더 루프</em></strong>라고 합니다.</p><h2>휴먼 인 더 루프 또는 인 더 루프</h2><p>이는 실제 사람이 AI 시스템과 상호 작용하여 더 많은 컨텍스트를 제공하고, 응답을 평가하고, 응답을 편집하고, 추가 정보를 요청하는 등의 작업을 수행할 수 있는 AI 개념입니다. 이는 규정 준수, 의사 결정 또는 콘텐츠 생성 등 오류 허용 오차가 낮은 시나리오에서 매우 유용하며, LLM 출력의 신뢰성을 개선하는 데 도움이 됩니다.</p><p>일반적인 예는 코딩 어시스턴트가 터미널에서 특정 명령을 실행할 권한을 요청하거나 코딩을 시작하기 전에 승인할 단계별 사고 과정을 보여 주는 경우입니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/4df9fe7383f0dc665a1b0866651f4f6a40a14aa8-499x215.png" style="width: 100%;" /><h2>Elasticsearch와 LangGraph의 상호 작용 방식</h2><p><a href="https://www.langchain.com/">LangChain</a>은 전체 텍스트 또는 시맨틱 검색을 실행하는 데 유용한 전체 텍스트 또는 시맨틱 검색을 실행하는 데 유용한 LangGraph 애플리케이션 내에서 Elasticsearch를 <a href="https://js.langchain.com/docs/integrations/vectorstores/elasticsearch/">벡터 저장소</a>로 사용하고 쿼리를 수행할 수 있게 해주며, LangGraph는 특정 워크플로우, 도구 및 상호 작용을 정의하는 데 사용됩니다. 또한 HITL을 사용자와의 추가적인 상호 작용 계층으로 추가합니다.</p><h2>실용적인 구현: 휴먼 인 더 루프</h2><p>변호사가 최근에 수임한 사건에 대해 궁금한 점이 있는 경우를 가정해 보겠습니다. 적절한 도구가 없다면 그는 법률 조항과 판례를 일일이 검색하고, 전문을 읽은 다음, 그것들이 자신의 상황에 어떻게 적용되는지 해석해야 할 것입니다. 그러나 LangGraph와 Elasticsearch를 사용하면 법률 판례 데이터베이스를 검색하고 변호사가 제공한 구체적인 세부 사항과 맥락을 통합한 판례 분석을 생성하는 시스템을 구축할 수 있습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/e95bf4ce115cbe9954ee5033776408bed485b8d2-1600x906.png" style="width: 100%;" /><p>변호사가 법률 질문을 제출하는 순간부터 워크플로우가 시작됩니다. 시스템은 Elasticsearch에서 벡터 검색을 수행하여 가장 관련성이 높은 판례를 검색한 후 자연어를 사용해 변호사가 선택할 수 있도록 제시합니다. 선택 후 LLM은 분석 초안을 생성하고 정보가 완전한지 확인합니다. 이 시점에서 워크플로우는 두 가지 경로 중 하나를 따를 수 있습니다. 모든 것이 명확하다면 최종 분석을 생성하기 위해 직접 진행하고, 그렇지 않다면 변호사에게 명확화를 요청하기 위해 일시 중지합니다. 누락된 컨텍스트가 제공되면 시스템은 설명을 고려하여 분석을 완료하고 반환합니다.</p><p>다음은 LangGraph에서 생성한 그래프로, 개발이 완료되었을 때 앱의 최종 모습을 보여줍니다. 각 노드는 도구 또는 기능을 나타냅니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/fcdc0dd53c436afc8e575d1f8fd3705b4dfe9a32-262x753.png" style="width: 100%;" /><h3>데이터 세트</h3><p>이 예제에 사용되는 <a href="https://github.com/Delacrobix/LangGraph-JS-Elasticsearch-human-in-the-loop/blob/main/dataset.json">데이터 세트</a>는 다음과 같습니다. 이 데이터 세트는 서비스 지연과 관련된 사례, 법원의 판결 이유 및 최종 결과를 설명하는 법적 판례 모음입니다.</p><pre><code>[
  {
    "pageContent": "Legal precedent: Case B - Service delay not considered breach. A consulting contract used term 'timely delivery' without specific dates. A three-week delay occurred but contract lacked explicit schedule. Court ruled no breach as parties had not defined concrete timeline and delay did not cause demonstrable harm.",
    "metadata": {
      "caseId": "CASE-B-2022",
      "contractType": "consulting agreement",
      "delayPeriod": "three weeks",
      "outcome": "no breach found",
      "reasoning": "no explicit deadline defined, no demonstrable harm",
      "keyTerms": "timely delivery, open terms, schedule definition",
      "title": "Case B: Delay Without Explicit Schedule"
    }
  },
  ...
]</code></pre><h3>데이터 수집 및 인덱스 설정</h3><p>인덱스 설정 및 데이터 수집 로직은 <a href="https://github.com/Delacrobix/LangGraph-JS-Elasticsearch-human-in-the-loop/blob/main/dataIngestion.ts">dataIngestion.ts</a> 파일에 정의되어 있으며, 여기서 인덱스 생성을 처리하는 함수를 선언합니다. 이 설정은 <a href="https://python.langchain.com/docs/integrations/vectorstores/elasticsearch/">Elasticsearch용 LangChain 벡터 저장소 인터페이스</a>와 호환됩니다.</p><p><em>참고: 매핑 설정은 <a href="https://github.com/Delacrobix/LangGraph-JS-Elasticsearch-human-in-the-loop/blob/main/dataIngestion.ts">dataIngestion.ts</a> 파일에도 포함되어 있습니다.</em></p><h3>패키지 설치 및 환경 변수 설정</h3><p>기본 설정으로 <a href="https://nodejs.org/en">Node.js</a> 프로젝트를 초기화합니다.</p><ul><li><strong>@elastic/elasticsearch: </strong>Node.js용 Elasticsearch 클라이언트입니다. 연결, 인덱스 생성 및 쿼리 실행에 사용됩니다.</li><li><strong>@langchain/community:</strong> ElasticVectorSearch 스토어를 비롯한 커뮤니티 지원 도구에 대한 통합을 제공합니다.</li><li><strong>@langchain/core:</strong> 체인, 프롬프트, 유틸리티와 같은 LangChain의 핵심 구성 요소입니다.</li><li><strong>@langchain/langgraph:</strong> 그래프 기반 오케스트레이션을 추가하여 노드, 엣지 및 상태 관리를 포함하는 워크플로우를 지원합니다.</li><li><strong>@langchain/openai: </strong>LangChain을 통해 OpenAI 모델(LLM 및 임베딩)에 대한 액세스를 제공합니다.</li><li><strong>dotenv:</strong>.env 파일에서 process.env로 환경 변수를 로드합니다.</li><li><strong>tsx: </strong>TypeScript 코드를 실행하는 데 유용한 도구입니다.</li></ul><p>콘솔에서 다음 명령어를 실행하여 모든 항목을 설치하세요.</p><pre><code>npm install @elastic/elasticsearch @langchain/community @langchain/core @langchain/langgraph @langchain/openai dotenv --legacy-peer-deps && npm install --save-dev tsx</code></pre><p><code>.env</code> 파일을 생성하여 환경 변수를 설정합니다.</p><pre><code>ELASTICSEARCH_ENDPOINT=
ELASTICSEARCH_API_KEY=
OPENAI_API_KEY=</code></pre><p>타입 안전성과 더 나은 개발자 경험을 제공하기 때문에 <a href="https://www.typescriptlang.org/">TypeScript</a>를 사용하여 코드를 작성할 것입니다. <a href="https://github.com/Delacrobix/LangGraph-JS-Elasticsearch-human-in-the-loop/blob/main/main.ts">main.ts</a>라는 이름의 TypeScript 파일을 만들고 다음 섹션의 코드를 삽입합니다.</p><h3>패키지 가져오기</h3><p><a href="https://github.com/Delacrobix/LangGraph-JS-Elasticsearch-human-in-the-loop/blob/main/main.ts">main.ts</a> 파일에서 필요한 모듈을 가져오고 환경 변수 구성을 초기화하는 것으로 시작합니다. 여기에는 핵심 LangGraph 구성 요소, OpenAI 모델 통합, Elasticsearch 클라이언트가 포함됩니다.</p><p>또한 <a href="https://github.com/Delacrobix/LangGraph-JS-Elasticsearch-human-in-the-loop/blob/main/dataIngestion.ts">DataingEstion.ts</a> 파일에서 다음을 가져옵니다.</p><ul><li><strong>ingestData: </strong>인덱스를 생성하고 데이터를 수집하는 함수입니다.</li><li><strong>Document 및 DocumentMetadata:</strong> 데이터셋 문서 구조를 정의하는 인터페이스입니다.</li></ul><h3>Elasticsearch 벡터 저장소 클라이언트, 임베딩 클라이언트 및 OpenAI 클라이언트</h3><p>이 코드는 벡터 저장소, 임베딩 클라이언트 및 OpenAI 클라이언트 하나를 초기화합니다.</p><pre><code>const VECTOR_INDEX = "legal-precedents";

const llm = new ChatOpenAI({ model: "gpt-4o-mini" });
const embeddings = new OpenAIEmbeddings({
  model: "text-embedding-3-small",
});

const esClient = new Client({
  node: process.env.ELASTICSEARCH_ENDPOINT,
  auth: {
    apiKey: process.env.ELASTICSEARCH_API_KEY ?? "",
  },
});

const vectorStore = new ElasticVectorSearch(embeddings, {
  client: esClient,
  indexName: VECTOR_INDEX,
});</code></pre><p>애플리케이션 워크플로우 상태 스키마는 노드 간 통신에 도움이 됩니다.</p><pre><code>const LegalResearchState = Annotation.Root({
  query: Annotation<string>(),
  analyzedConcepts: Annotation<string[]>(),
  precedents: Annotation<Document[]>(),
  selectedPrecedent: Annotation<Document | null>(),
  draftAnalysis: Annotation<string>(),
  ambiguityDetected: Annotation<boolean>(),
  userClarification: Annotation<string>(),
  finalAnalysis: Annotation<string>(),
});</code></pre><p>상태 객체에서는 사용자의 쿼리, 쿼리에서 추출된 개념, 검색된 법적 판례 및 감지된 모호성을 노드를 통해 전달합니다. 또한 사용자가 선택한 판례, 그 과정에서 생성된 분석 초안, 모든 설명이 완료된 후 최종 분석도 추적합니다.</p><h3>노드</h3><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/9bda47908dc0a73fdda222c8ba1382a1ef9dc77a-304x300.png" style="width: 100%;" /><p><strong>searchPrecedents: </strong>이 노드는 사용자의 입력에 따라 Elasticsearch 벡터 저장소에서 유사성 검색을 수행합니다. 이 기능은 일치하는 문서를 최대 5개까지 검색하여 사용자가 검토할 수 있도록 인쇄합니다.</p><pre><code>async function searchPrecedents(state: typeof LegalResearchState.State) {
  console.log(
    "📚 Searching for relevant legal precedents with query:\n",
    state.query
  );

  const results = await vectorStore.similaritySearch(state.query, 5);
  const precedents = results.map((d) => d as Document);

  console.log(`Found ${precedents.length} relevant precedents:\n`);

  for (let i = 0; i < precedents.length; i++) {
    const p = precedents[i];
    const m = p.metadata;
    console.log(
      `${i + 1}. ${m.title} (${m.caseId})\n` +
        `   Type: ${m.contractType}\n` +
        `   Outcome: ${m.outcome}\n` +
        `   Key reasoning: ${m.reasoning}\n` +
        `   Delay period: ${m.delayPeriod}\n`
    );
  }

  return { precedents };
}</code></pre><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/82afb46c4373b01daf7640a528c2ea54db0837b5-304x300.png" style="width: 100%;" /><p><strong>precedentSelection:</strong> 이 노드를 사용하면 사용자가 자연어를 사용하여 근접 검색에서 검색된 사용 사례 중 질문과 가장 잘 일치하는 사용 사례를 선택할 수 있습니다. 이 시점에서 애플리케이션은 <strong>워크플로우를 중단</strong>하고 사용자 입력을 기다립니다.</p><pre><code>function precedentSelection(state: typeof LegalResearchState.State) {
  console.log("\n⚖️  HITL #1: Human input needed\n");
  const question = "👨‍⚖️  Which precedent is most similar to your case? ";
  const userChoice = interrupt({ question });

  return { userChoice };
}</code></pre><p><strong>selectPrecedent:</strong> 이 노드는 검색된 문서 중 하나를 선택할 수 있도록 해석할 사용자 입력을 검색된 문서와 함께 전송합니다. LLM은 사용자의 자연어 입력으로부터 추론한 문서를 나타내는 숫자를 반환함으로써 이 작업을 수행합니다.</p><pre><code>async function selectPrecedent(state: typeof LegalResearchState.State) {
  const precedents = state.precedents || [];
  const userInput = (state as any).userChoice || "";

  const precedentsList = precedents
    .map((p, i) => {
      const m = p.metadata;
      return `${i + 1}. ${m.caseId}: ${m.title} - ${m.outcome}`;
    })
    .join("\n");

  const structuredLlm = llm.withStructuredOutput({
    name: "precedent_selection",
    schema: {
      type: "object",
      properties: {
        selected_number: {
          type: "number",
          description:
            "The precedent number selected by the lawyer (1-based index)",
          minimum: 1,
          maximum: precedents.length,
        },
      },
      required: ["selected_number"],
    },
  });

  const prompt = `
    The lawyer said: "${userInput}"

    Available precedents:
    ${precedentsList}

    Which precedent number (1-${precedents.length}) matches their selection?
  `;

  const response = await structuredLlm.invoke([
    {
      role: "system",
      content:
        "You are an assistant that interprets lawyer's selection and returns the corresponding precedent number.",
    },
    { role: "user", content: prompt },
  ]);

  const selectedIndex = response.selected_number - 1;
  const selectedPrecedent = precedents[selectedIndex] || precedents[0];

  console.log(`✅ Selected: ${selectedPrecedent.metadata.title}\n`);
  return { selectedPrecedent };
}</code></pre><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/714e840cc8dab48c436cd066a6fdb4b7dceccf13-428x260.png" style="width: 100%;" /><p><strong>createDraft: </strong>이 노드는 사용자가 선택한 판례를 기반으로 초기 법적 분석을 생성합니다. LLM을 사용하여 선택된 판례가 변호사의 질문에 어떻게 적용되는지 평가하고, 시스템이 진행하기에 충분한 정보를 가지고 있는지 결정합니다.</p><p>판례를 바로 적용할 수 있는 경우, 노드는 초안 분석 결과를 생성하고 올바른 경로를 따라 최종 노드로 이동합니다. LLM이 모호성, 예를 들어 정의되지 않은 계약 조건, 누락된 타임라인 세부 사항 또는 불명확한 조건을 감지하면 명확화가 필요함을 나타내는 플래그와 함께 제공되어야 하는 구체적인 정보 목록을 반환합니다. 이 경우 모호성으로 인해 그래프의 왼쪽 경로가 선택됩니다.</p><pre><code>async function createDraft(state: typeof LegalResearchState.State) {
  console.log("📝 Drafting initial legal analysis...\n");

  const precedent = state.selectedPrecedent;
  if (!precedent) return { draftAnalysis: "" };

  const m = precedent.metadata;

  const structuredLlm = llm.withStructuredOutput({
    name: "draft_analysis",
    schema: {
      type: "object",
      properties: {
        needs_clarification: {
          type: "boolean",
          description:
            "Whether the analysis requires clarification about contract terms or context",
        },
        analysis_text: {
          type: "string",
          description: "The draft legal analysis or the ambiguity explanation",
        },
        missing_information: {
          type: "array",
          items: { type: "string" },
          description:
            "List of specific information needed if clarification is required (empty if no clarification needed)",
        },
      },
      required: ["needs_clarification", "analysis_text", "missing_information"],
    },
  });

  const prompt = `
    Based on this precedent:
    Case: ${m.title}
    Outcome: ${m.outcome}
    Reasoning: ${m.reasoning}
    Key terms: ${m.keyTerms}

    And the lawyer's question: "${state.query}"

    Draft a legal analysis applying this precedent to the question.
    
    If you need more context about the specific contract terms, timeline details, 
    or other critical information to provide accurate analysis, set needs_clarification 
    to true and list what information is missing.
    
    Otherwise, provide the legal analysis directly.
  `;

  const response = await structuredLlm.invoke([
    {
      role: "system",
      content:
        "You are a legal research assistant that analyzes cases and identifies when additional context is needed.",
    },
    { role: "user", content: prompt },
  ]);

  let displayText: string;
  if (response.needs_clarification) {
    const missingInfoList = response.missing_information
      .map((info: string, i: number) => `${i + 1}. ${info}`)
      .join("\n");
    displayText = `AMBIGUITY DETECTED:\n${response.analysis_text}\n\nMissing information:\n${missingInfoList}`;
  } else {
    displayText = `ANALYSIS:\n${response.analysis_text}`;
  }

  console.log(displayText + "\n");

  return {
    draftAnalysis: displayText,
    ambiguityDetected: response.needs_clarification,
  };
}</code></pre><p>그래프가 취할 수 있는 두 가지 경로는 다음과 같습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/72dda52e1d84818c9f98437f2fa6ecb8e5b2f4ff-438x638.png" style="width: 100%;" /><p>왼쪽 경로에는 설명을 처리하는 추가 노드가 포함되어 있습니다.</p><p><strong>requestClarification:</strong> 이 노드는 초안 분석에 필수 컨텍스트가 부족하다고 시스템이 식별하면 두 번째 휴먼 인 더 루프 단계를 트리거합니다. 워크플로우가 중단되고 사용자에게 이전 노드에서 감지한 누락된 계약 세부 정보를 명확히 해달라는 메시지가 표시됩니다.</p><pre><code>function requestClarification(state: typeof LegalResearchState.State) {
  console.log("\n⚖️  HITL #2: Additional context needed\n");
  const userClarification = interrupt({
    question: "👨‍⚖️  Please provide clarification about your contract terms:",
  });
  return { userClarification };
}</code></pre><p><strong>generateFinalAnalysis: </strong>이 노드는 선택한 판례와 필요한 경우 사용자가 제공한 추가 컨텍스트를 결합하여 최종 법률 분석을 생성합니다. 이전 HITL 단계에서 수집한 설명을 사용하여, LLM은 판례의 추론, 사용자가 제공한 계약 세부 정보 및 위반이 발생했는지 여부를 결정하는 조건을 종합합니다.</p><p>이 노드는 법적 해석과 실용적인 권장 사항을 통합한 완전한 분석을 출력합니다.</p><pre><code>async function generateFinalAnalysis(state: typeof LegalResearchState.State) {
  console.log("📋 Generating final legal analysis...\n");

  const precedent = state.selectedPrecedent;
  if (!precedent) return { finalAnalysis: "" };

  const m = precedent.metadata;

  const prompt = `
    Original question: "${state.query}"
    
    Selected precedent: ${m.title}
    Outcome: ${m.outcome}
    Reasoning: ${m.reasoning}
    
    Lawyer's clarification: "${state.userClarification}"
    
    Provide a comprehensive legal analysis integrating:
    1. The selected precedent's reasoning
    2. The lawyer's specific contract context
    3. Conditions for breach vs. no breach
    4. Practical recommendations
  `;

  const response = await llm.invoke([
    {
      role: "system",
      content:
        "You are a legal research assistant providing comprehensive analysis.",
    },
    { role: "user", content: prompt },
  ]);

  const finalAnalysis = response.content as string;

  console.log(
    "\n" +
      "=".repeat(80) +
      "\n" +
      "⚖️  FINAL LEGAL ANALYSIS\n" +
      "=".repeat(80) +
      "\n\n" +
      finalAnalysis +
      "\n\n" +
      "=".repeat(80) +
      "\n"
  );

  return { finalAnalysis };
}</code></pre><p>그래프 작성:</p><pre><code>const workflow = new StateGraph(LegalResearchState)
  .addNode("analyzeQuery", analyzeQuery)
  .addNode("searchPrecedents", searchPrecedents)
  .addNode("precedentSelection", precedentSelection)
  .addNode("selectPrecedent", selectPrecedent)
  .addNode("createDraft", createDraft)
  .addNode("requestClarification", requestClarification)
  .addNode("generateFinalAnalysis", generateFinalAnalysis)
  .addEdge("__start__", "analyzeQuery")
  .addEdge("analyzeQuery", "searchPrecedents")
  .addEdge("searchPrecedents", "precedentSelection") // HITL #1
  .addEdge("precedentSelection", "selectPrecedent")
  .addEdge("selectPrecedent", "createDraft")
  .addConditionalEdges(
    "createDraft",
    (state: typeof LegalResearchState.State) => {
      // If ambiguity detected, request clarification (HITL #2)
      if (state.ambiguityDetected) return "needsClarification";
      // Otherwise, generate final analysis
      return "final";
    },
    {
      needsClarification: "requestClarification",
      final: "generateFinalAnalysis",
    }
  )
  .addEdge("requestClarification", "generateFinalAnalysis") // HITL #2
  .addEdge("generateFinalAnalysis", "__end__");</code></pre><p>그래프에서 조건부 엣지가 &quot;최종&quot; 경로를 선택하는 조건을 정의한다는 것을 볼 수 있습니다. 위에서 살펴본 바와 같이, 이제 결정은 초안 분석에서 추가 설명이 필요한 모호성을 발견했는지 여부에 따라 달라집니다.</p><p>모두 합쳐서 실행:</p><pre><code>await ingestData();

// Compile workflow
const app = workflow.compile({ checkpointer: new MemorySaver() });
const config = { configurable: { thread_id: "hitl-circular-thread" } };

await saveGraphImage(app);

// Execute workflow
const legalQuestion =
    "Does a pattern of repeated delays constitute breach even if each individual delay is minor?"; 

console.log(`⚖️  LEGAL QUESTION: "${legalQuestion}"\n`);

let currentState = await app.invoke({ query: legalQuestion }, config);

// Handle all interruptions in a loop
while ((currentState as any).__interrupt__?.length > 0) {
  console.log("\n💭 APPLICATION PAUSED WAITING FOR USER INPUT...");

  const interruptQuestion = (currentState as any).__interrupt__[0]?.value
    ?.question;
  const userChoice = await getUserInput(
    interruptQuestion || "👤 YOUR CHOICE: "
  );

  currentState = await app.invoke(
    new Command({ resume: userChoice }),
    config
  );
}</code></pre><h3>스크립트 실행:</h3><p>모든 코드가 할당되었으므로 터미널에 다음 명령어를 입력하여 <code>main.ts</code> 파일을 실행합니다.</p><pre><code>tsx main.ts</code></pre><p>스크립트가 실행되면, <strong>&quot;반복되는 지연 패턴이 각 개별 지연이 경미하더라도 침해에 해당하는가?&quot;</strong>라는 질문이 Elasticsearch로 전송되어 근접 검색을 수행하게 되며, 인덱스에서 얻은 결과가 표시됩니다. 앱은 쿼리와 일치하는 여러 관련 판례를 감지하여 실행을 일시정지하고 사용자에게 어떤 법적 판례가 가장 적합한지 명확히 설명하도록 요청합니다.</p><pre><code>📚 Searching for relevant legal precedents with query:
 Does a pattern of repeated delays constitute breach even if each individual delay is minor?
Found 5 relevant precedents:

1. Case H: Pattern of Repeated Delays (CASE-H-2021)
   Type: ongoing service agreement
   Outcome: breach found
   Key reasoning: pattern demonstrated failure to perform, cumulative effect
   Delay period: multiple instances

2. Case E: Minor Delay Quality Maintained (CASE-E-2022)
   Type: service agreement
   Outcome: minor breach only
   Key reasoning: delay minimal, quality maintained, termination unjustified
   Delay period: five days

3. Case A: Delay Breach with Operational Impact (CASE-A-2023)
   Type: service agreement
   Outcome: breach found
   Key reasoning: delay affected operations and caused financial harm
   Delay period: two weeks

4. Case B: Delay Without Explicit Schedule (CASE-B-2022)
   Type: consulting agreement
   Outcome: no breach found
   Key reasoning: no explicit deadline defined, no demonstrable harm
   Delay period: three weeks

5. Case C: Justified Delay External Factors (CASE-C-2023)
   Type: construction service
   Outcome: no breach found
   Key reasoning: external factors beyond control, force majeure applied
   Delay period: one month

⚖️  HITL #1: Human input needed

💭 APPLICATION PAUSED WAITING FOR USER INPUT...
👨‍⚖️  Which precedent is most similar to your case? </code></pre><p>이 애플리케이션의 흥미로운 점은 자연어를 사용하여 하나의 옵션을 선택할 수 있고, LLM이 사용자의 입력을 해석하여 올바른 선택을 결정할 수 있다는 점입니다. <strong>&quot;Case H&quot;</strong> 텍스트를 입력하면 어떻게 되는지 살펴봅시다.</p><pre><code>💭 APPLICATION PAUSED WAITING FOR USER INPUT...
👨‍⚖️  Which precedent is most similar to your case? Case H

✅ Selected: Case H: Pattern of Repeated Delays

📝 Drafting initial legal analysis...

AMBIGUITY DETECTED:
Based on Case H, a pattern of repeated delays can indeed constitute a breach of contract, even if each individual delay is minor. The outcome in Case H indicates that the cumulative effect of these minor delays led to a significant failure to perform the contractual obligations adequately. The reasoning emphasizes that consistent performance is critical in fulfilling the terms of a contract. Therefore, if the repeated delays create a situation where the overall performance is hindered, this pattern could be interpreted as a breach. However, the interpretation may depend on the specific terms of the contract at issue, as well as the expectations of performance set forth in that contract.

Missing information:
1. Specific contract terms regarding performance timelines
2. Details on the individual delays (duration, frequency)
3. Context on consequences of delays stated in the contract
4. Other parties' expectations or agreements related to performance


⚖️  HITL #2: Additional context needed


💭 APPLICATION PAUSED WAITING FOR USER INPUT...
👨‍⚖️  Please provide clarification about your contract terms:</code></pre><p>이 모델은 사용자의 설명을 받아 워크플로우에 통합하여 충분한 컨텍스트가 제공되면 최종 분석을 진행합니다. 이 단계에서는 이전에 감지된 모호성을 활용하여 초안 분석에서 법적 해석에 의미 있는 영향을 미칠 수 있는 누락된 계약 세부 사항을 강조 표시합니다. 이러한 &#x27;누락된 정보&#x27; 항목은 신뢰할 수 있는 최종 의견을 도출하기 전에 불확실성을 해소하기 위해 어떤 설명이 필요한지 판단하는 데 도움이 됩니다.</p><p>사용자는 다음 입력에 설명을 요청한 내용을 포함해야 합니다. <strong>&quot;계약서에는 기간 없이 ‘신속한 배송’이 요구됩니다. 6개월 동안 2-4일의 지연이 8번 발생했습니다. 3번의 고객 마감일을 놓쳐 50,000달러의 손실이 발생했습니다. 공급업체에 통보했지만 패턴은 계속되었습니다.&quot;</strong>로 시도해 보겠습니다.</p><pre><code>💭 APPLICATION PAUSED WAITING FOR USER INPUT...
👨‍⚖️  Please provide clarification about your contract terms: Contract requires "prompt delivery" without timelines. 8 delays of 2-4 days over 6 months. $50K in losses from 3 missed client deadlines. Vendor notified but pattern continued.

📋 Generating final legal analysis...

================================================================================
⚖️  FINAL LEGAL ANALYSIS
================================================================================

To analyze the question of whether a pattern of repeated minor delays constitutes a breach of contract, we need to combine insights from the selected precedent, the specifics of the lawyer's contract situation, conditions that typically govern breach versus non-breach, and practical recommendations for the lawyer moving forward.

### 1. Selected Precedent's Reasoning

The precedent case, referred to as Case H, found that a pattern of repeated delays amounted to a breach of contract. The court reasoned that even minor individual delays, when considered cumulatively, demonstrated a failure to perform as stipulated in the contract. The underlying rationale was that the cumulative effect of these minor delays could significantly undermine the purpose of the contract, which typically aims for timely performance and reliable delivery.

### 2. Lawyer's Specific Contract Context

In the lawyer's situation, the contract specified "prompt delivery" but did not provide a strict timeline. The vendor experienced 8 delays ranging from 2 to 4 days over a period of 6 months. These delays culminated in $50,000 in losses due to three missed client deadlines. The vendor was notified regarding these delays; however, the pattern of delays persisted.

Key considerations include:
- **Nature of the Obligations**: While “prompt delivery” does not define a strict timeline, it does imply an expectation for timely performance.
- **Material Impact**: The missed client deadlines indicate that these delays had a material adverse effect on the lawyer's ability to fulfill contractual obligations to third parties, likely triggering damages.

### 3. Conditions for Breach vs. No Breach

**Conditions for Breach**:
- **Pattern and Cumulative Effect**: Similar to the reasoning in Case H, evidence of a habitual pattern of delays can amount to a breach. Even if individual delays are minor, when combined, they may show a lack of diligence or reliability by the vendor.
- **Materiality**: The impact of these delays is crucial. If the cumulative delays adversely affect the contract's purpose or cause significant losses, this reinforces the case for a breach.
- **Notification and Opportunity to Cure**: The fact that the vendor was notified of the delays and failed to rectify the behavior can often be interpreted as a further indication of breach.

**Conditions for No Breach**:
- **Non-Material Delays**: If the delays did not affect the overall contractual performance or client obligations, this may lessen the likelihood of establishing a breach. However, given the risks and losses involved, this seems less relevant in this scenario.
- **Force Majeure or Justifiable Delays**: If the vendor could show that these delays were due to justify circumstances not within their control, it may potentially provide a defense against breach claims.

### 4. Practical Recommendations

1. **Assess Damages**: Document the exact nature of the financial losses incurred due to the missed deadlines to substantiate claims of damages.
  
2. **Gather Evidence**: Collect all communication regarding the delays, including any notifications sent to the vendor about the issues.

3. **Consider Breach of Contract Action**: Based on the precedent and accumulated delays, consider formalized communication to the vendor regarding a breach of contract claim, highlighting both the pattern and the impact of these repeated delays.

4. **Evaluate Remedies**: Depending upon the contract specifics, the lawyer may wish to pursue several remedies, including:
   - **Compensatory Damages**: For the financial losses due to missed deadlines.
   - **Specific Performance**: If timely delivery is critical and can still be enforced.
   - **Contract Termination**: Depending on the severity, terminating the contract and seeking replacements may be warranted.

5. **Negotiate Terms**: If continuing to work with the current vendor is strategic, the lawyer should consider renegotiating terms for performance guarantees or penalties for further delays.

6. **Future Contracts**: In future contracts, consider including explicit timelines and conditions for prompt delivery, as well as specified damages for delays to better safeguard against this issue.

By integrating the legal principles from the precedent with the specific context and conditions outlined, the lawyer can formulate a solid plan to address the repeated delays by the vendor effectively.</code></pre><p>이 출력은 워크플로우의 마지막 단계로, 모델이 선택한 판례(사례 H)와 변호사의 설명을 통합하여 완전한 법률 분석을 생성하는 과정을 보여줍니다. 이 시스템은 지연 패턴이 위반에 해당하는 이유를 설명하고, 이러한 해석을 뒷받침하는 요인을 간략하게 설명하며, 실질적인 권장 사항을 제공합니다. 전반적으로, 이 출력은 HITL 명확화가 어떻게 모호성을 해소하고 모델이 타당한 맥락에 맞는 법적 의견을 도출할 수 있도록 하는지를 보여줍니다.</p><h3>기타 실제 시나리오</h3><p>Elasticsearch, LangGraph, 휴먼 인 더 루프 등을 사용하는 이러한 종류의 애플리케이션은 다른 종류의 앱에서도 유용하게 사용될 수 있습니다.</p><ul><li><strong>도구 호출을 실행하기 전에 검토합니다</strong>. 예를 들어, 금융 거래에서는 주문이 입력되기 전에 인간이 매수/매도 주문을 승인합니다.</li><li><strong>필요에 따라 추가 매개변수를 제공합니다</strong>. 예를 들어, 고객 지원 분류 과정에서 AI가 고객 문제에 대해 여러 가지 가능한 해석을 제시할 때 상담원이 올바른 문제 범주를 선택하는 경우에 유용합니다.</li></ul><p>그리고 휴먼 인 더 루프가 판도를 바꾸는 요소가 될 많은 사용 사례들이 발견되어야 합니다.</p><h2>결론</h2><p>LangGraph와 Elasticsearch를 사용하면 자체적으로 결정을 내리고 선형 워크플로우로 작동하거나 한 경로 또는 다른 경로를 선택하는 조건을 가진 에이전트를 구축할 수 있습니다. 휴먼 인 더 루프를 통해 에이전트는 실제 사용자를 의사 결정 과정에 참여시켜 문맥상의 공백을 메우고 내결함성이 중요한 시스템에 대한 확인을 요청할 수 있습니다.</p><p>이 접근 방식의 장점 중 하나는 Elasticsearch 기능을 사용하여 대규모 데이터 세트를 필터링한 다음 LLM을 사용하여 사용자가 선택한 단일 문서를 가져올 수 있다는 것입니다. Elasticsearch만 사용하면 이 마지막 단계는 훨씬 더 까다로울 것입니다. 왜냐하면 인간이 자연어를 사용하여 결과를 언급할 수 있는 방법이 많기 때문입니다.</p><p>이 접근 방식은 전체 데이터 세트가 아닌 최종 결정을 내리는 데 필요한 정보만 LLM에 전송하므로 시스템의 속도와 토큰 효율을 유지합니다. 동시에 사용자의 의도를 매우 정확하게 감지하고 원하는 옵션이 선택될 때까지 반복합니다.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[ML을 활용하여 Streams에서 로그 구문 분석 자동화하기]]></title>
            <description><![CDATA[Streams에서 로그 형식 지문 인식을 활용한 자동화 실험을 통해 하이브리드 ML 접근 방식이 어떻게 로그 구문 분석 정확도 94%, 로그 분할 정확도 91%를 달성했는지 알아보세요.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/log-parsing-partitioning-automation-experiments-streams</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/log-parsing-partitioning-automation-experiments-streams</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Nastia Havriushenko]]></dc:creator>
            <pubDate>Fri, 02 Jan 2026 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/965c58f19742361160593c38fcaa8b2f4b0d6cc5-3838x2159.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>최신 통합 가시성 스택에서 다양한 데이터 제공자로부터 비정형 로그를 Elasticsearch와 같은 플랫폼으로 수집하는 것은 여전히 어려운 과제입니다. 수동으로 작성된 구문 분석 규칙에 의존하면 취약한 파이프라인이 생성되어 사소한 업스트림 코드 업데이트만으로도 구문 분석 실패와 색인되지 않은 않은 데이터가 발생합니다. 이러한 취약성은 확장성 문제로 인해 더욱 악화됩니다. 동적인 마이크로서비스 환경에서는 새로운 서비스가 지속적으로 추가됨에 따라 수동 규칙 유지관리는 운영상의 악몽으로 변할 수 있습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/9108ec303339dd091faa3c363c7cf5c228155f49-3840x2160.png" style="width: 100%;" /><p>목표는 로그 구문 분석(필드 추출)과 로그 분할(소스 식별)을 모두 처리할 수 있는 자동화되고 적응형 접근 방식으로 전환하는 것이었습니다. 대규모 언어 모델(LLM)이 코드 구문과 의미론적 패턴에 대한 내재된 이해를 통해 이러한 작업을 최소한의 인간 개입으로 자동화할 수 있다고 가정했습니다.</p><p><a href="http://elastic.co/elasticsearch/streams"><span style="text-decoration:underline">Streams</span></a>에서 이미 이 기능을 사용 가능하다는 기쁜 소식을 전해 드립니다!</p><h2>데이터 세트 설명</h2><p>PoC 목적으로 <strong><a href="https://github.com/logpai/loghub">Loghub</a> </strong>로그 컬렉션을 선택했습니다. 조사를 위해 다음 주요 분야에서 대표적인 샘플을 선정했습니다.</p><ul><li>분산 시스템: HDFS(Hadoop 분산 파일 시스템)와 Spark 데이터 세트를 사용했습니다. 여기에는 빅데이터 플랫폼에서 흔히 볼 수 있는 정보, 디버그, 오류 메시지가 섞여 있습니다.</li><li>서버 및 웹 애플리케이션: Apache 웹 서버 및 OpenSSH의 로그는 액세스, 오류 및 보안 관련 이벤트의 귀중한 소스를 제공했습니다. 이는 웹 트래픽을 모니터링하고 잠재적 위협을 탐지하는 데 매우 중요합니다.</li><li>운영 체제: Linux 및 Windows의 로그를 포함했습니다. 이러한 데이터 세트는 운영팀이 매일 접하는 일반적인 반정형 시스템 수준 이벤트를 나타냅니다.</li><li>모바일 시스템: 모바일 환경에서 발생하는 로그를 모델이 처리할 수 있는지 확인하기 위해 안드로이드 데이터 세트를 포함했습니다. 이러한 로그는 종종 상세한 내용을 담고 있으며 모바일 디바에스에서 발생하는 다양한 애플리케이션 및 시스템 수준의 활동을 기록합니다.</li><li>슈퍼컴퓨터: 고성능 컴퓨팅(HPC) 환경에서 성능을 테스트하기 위해 고도로 구조화된 로그와 특정 도메인 용어를 특징으로 하는 BGL (Blue Gene/L) 데이터 세트를 통합했습니다.</li></ul><p>Loghub 컬렉션의 주요 장점은 로그가 대부분 정제되지 않고 레이블이 지정되지 않아 노이즈가 많은 실시간 프로덕션 환경을 마이크로서비스 아키텍처로 미러링한다는 점입니다.</p><p>로그 예:</p><pre><code>[Sun Dec 04 20:34:21 2005] [notice] jk2_init() Found child 2008 in scoreboard slot 6
[Sun Dec 04 20:34:25 2005] [notice] workerEnv.init() ok /etc/httpd/conf/workers2.properties
[Mon Dec 05 11:06:51 2005] [notice] workerEnv.init() ok /etc/httpd/conf/workers2.properties
17/06/09 20:10:58 INFO output.FileOutputCommitter: Saved output of task 'attempt_201706092018_0024_m_000083_1138' to hdfs://10.10.34.11:9000/pjhe/test/1/_temporary/0/task_201706092018_0024_m_000083
17/06/09 20:10:58 INFO mapred.SparkHadoopMapRedUtil: attempt_201706092018_0024_m_000083_1138: Committed</code></pre><p>또한 가장 일반적인 도메인에서 추가 로그를 수집하기 위해 일반적인 웹 애플리케이션과 데이터베이스 설정으로 Kubernetes 클러스터를 생성했습니다.</p><p>일반적인 로그 필드의 예: 타임스탬프, 로그 레벨(정보, 경고, 오류), 소스, 메시지.</p><h2>LLM을 이용한 퓨샷 로그 구문 분석</h2><p>첫 번째 실험은 근본적인 질문에 초점을 맞췄습니다: <strong>LLM이 핵심 필드를 안정적으로 식별하고 이를 추출하기 위한 일관된 구문 분석 규칙을 생성할 수 있는가?</strong></p><p>모델에 원시 로그 샘플을 분석하고 정규식(Regex) 및 <a href="https://www.elastic.co/docs/explore-analyze/scripting/grok">Grok</a> 형식의 로그 구문 분석 규칙을 생성하도록 요청했습니다. 그 결과, 이 접근 방식은 잠재력이 많지만 구현에 상당한 어려움이 있음을 파악했습니다.</p><h3>높은 신뢰도 및 상황 인식</h3><p>초기 결과는 고무적이었습니다. LLM은 제공된 퓨샷 예시와 일치하는 구문 분석 규칙을 높은 신뢰도로 생성하는 뛰어난 능력을 보여주었습니다. 이 모델은 단순한 패턴 매칭 외에도 로그 소스를 정확하게 식별하고 이름을 지정할 수 있는 로그 이해 능력도 보여주었습니다(예: 상태 추적 앱, Nginx 웹 앱, Mongo 데이터베이스).</p><h3>입력 샘플의 &#x27;골디락스&#x27; 딜레마</h3><p>실험 결과, <strong>입력 샘플에 대한 극도의 민감성</strong> 때문에 모델의 견고성이 상당히 부족하다는 점이 금방 드러났습니다. 모델의 성능은 프롬프트에 포함된 특정 로그 예시에 따라 크게 변동했습니다. 로그 샘플에는 <em>충분히 다양한 </em>로그가 포함되어야 하는 로그 유사성 문제가 있음을 확인했습니다.</p><ul><li>너무 동질적인 경우(과적합)<strong>:</strong> 입력 로그가 너무 유사하면 LLM은 <strong>과도하게 세부화</strong>하는 경향이 있습니다. 스택 추적의 특정 Java 클래스 이름과 같은 가변 데이터를 템플릿의 정적 부분으로 처리합니다. 이로 인해 극히 일부 로그만 다루고 사용할 수 없는 필드를 추출하는 취약한 규칙이 생성됩니다.</li><li>너무 이질적인 경우(혼란): 반대로 샘플에 형식상의 편차가 심하거나, 더 심각하게는 진행률 표시줄, 메모리 테이블 또는 ASCII 아트와 같은 &#x27;엉터리 로그&#x27;가 포함되면 모델은 공통분모를 찾는 데 어려움을 겪습니다. 이 경우 복잡하고 불완전한 정규식을 생성하거나 전체 줄을 단일 메시지 블롭 필드로 과도하게 일반화하는 경우가 많습니다.</li></ul><h3>컨텍스트 윈도우 제약 조건</h3><p>또한 컨텍스트 윈도우 병목 현상에 직면했습니다. 입력 로그가 길거나, 이질적이거나, 추출 가능한 필드가 많을 경우, 모델의 출력 결과가 종종 저하되어 &quot;정리되지 않은&quot; 상태가 되거나 출력 컨텍스트 윈도우에 맞지 않을 정도로 길어졌습니다. 당연히 청킹이 이러한 문제를 해결하는 데 도움이 됩니다. 문자 기반 및 엔티티 기반 구분자를 사용하여 로그를 분할함으로써, 모델이 노이즈에 압도되지 않고 주요 필드를 추출하는 데 집중할 수 있도록 도울 수 있었습니다.</p><h3>일관성 및 표준화 격차</h3><p>모델이 규칙을 성공적으로 생성한 경우에도 다음과 같이 약간의 불일치가 발견되었습니다.</p><ul><li>서비스 이름 지정 변형: 모델은 동일한 엔티티에 대해 서로 다른 이름을 제안합니다(예: 실행마다 소스를 &#x27;Spark&#x27;, &#x27;Apache Spark&#x27; 및 &#x27;Spark Log Analytics&#x27;로 레이블 지정).</li><li>필드 이름 지정의 변형: 필드 이름이 표준화되지 않았습니다(예: <code>id</code> , <code>service.id</code> , <code>device.id</code>). 표준화된 <a href="https://www.elastic.co/docs/reference/ecs/ecs-field-reference">Elastic 필드 이름 지정</a>을 사용하여 이름을 정규화했습니다.</li><li>해상도 편차: 필드 추출의 해상도는 입력 로그가 서로 얼마나 유사한지에 따라 달라집니다.</li></ul><h2>로그 형식 지문</h2><p>로그 유사성 문제를 해결하기 위해 고성능 휴리스틱인 <strong>로그 형식 지문(LFF)</strong>을 도입합니다.</p><p>원시적이고 노이즈가 많은 로그를 LLM에 직접 입력하는 대신, 먼저 결정론적 변환을 적용하여 각 메시지의 기본 구조를 드러냅니다. 이 전처리 단계는 가변 데이터를 추상화하여 관련 로그를 그룹화할 수 있는 단순화된 &#x27;지문&#x27;을 생성합니다.</p><p>매핑 로직은 속도와 일관성을 보장하기 위해 다음과 같이 간단합니다.</p><ol><li>숫자 추상화: 모든 숫자 시퀀스(0-9)는 단일 &#x27;0&#x27;으로 대체됩니다.</li><li>텍스트 추상화: 공백이 있는 알파벳 문자 시퀀스는 단일 ‘a’로 대체됩니다.</li><li>공백 정규화: 모든 공백 시퀀스(공백, 탭, 줄바꿈)는 하나의 공백으로 축소됩니다.</li><li>기호 보존: 구두점 및 특수 문자(예: :, [, ], /)는 로그 구조를 나타내는 가장 강력한 지표인 경우가 많으므로 보존됩니다.</li></ol><p>로그 매핑 접근 방식을 소개합니다. 기본 매핑 패턴은 다음과 같습니다.</p><ul><li>길이에 상관없이 0-9자리 숫자 -&gt;&#x27;0&#x27;으로 대체.</li><li>모든 길이의 텍스트(공백이 있는 알파벳 문자) -&gt; &#x27;a&#x27;로 대체.</li><li>공백, 탭, 줄바꿈 -&gt; 하나의 공간으로 축소.</li></ul><p>이 매핑을 통해 로그를 어떻게 변환하는지에 대한 예를 살펴보겠습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/78fa2887486eb9417804354ee3bf2a4fdb0f6383-846x252.png" style="width: 100%;" /><p>그 결과, 다음과 같은 로그 마스크를 얻습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/ec439a3d3a25002498b97defcff733ea5ebc6b55-826x94.png" style="width: 100%;" /><p>처음 두 로그의 지문을 주목하세요. 타임스탬프, 소스 클래스, 메시지 내용이 다르더라도 접두사(<code>0/0/0 0:0:0 a a.a:</code>)는 동일합니다. 이러한 구조적 정렬을 통해 이러한 로그를 동일한 클러스터에 자동으로 버킷화할 수 있습니다.</p><p>하지만 세 번째 로그는 완전히 다른 지문을 생성합니다(<code>0-0-0...</code>). 이를 통해 LLM을 호출하기 <em>전에</em> 첫 번째 그룹과 알고리즘적으로 분리할 수 있습니다.</p><h2>보너스: ES|QL을 이용한 즉시 구현</h2><p>Discover에서 이 쿼리를 전달하기만 하면 됩니다.</p><pre><code>FROM loghub |
EVAL pattern = REPLACE(REPLACE(REPLACE(REPLACE(raw_message, "[ \t\n]+", " "), "[A-Za-z]+", "a"), "[0-9]+", "0"), "a( a)+", "a") |
STATS total_count = COUNT(), ratio = COUNT() / 2000.0, datasources=VALUES(filename), example=TOP(raw_message, 3, "desc") BY SUBSTRING(pattern, 0, 15) |
SORT total_count DESC |
LIMIT 100</code></pre><p><strong>쿼리 분석:</strong></p><p><strong>FROM</strong> loghub: 원시 로그 데이터를 포함하는 인덱스를 대상으로 합니다.</p><p><strong>EVAL</strong> 패턴 = …: 핵심 매핑 로직. REPLACE 함수를 연결하여 추상화(예: 숫자를 &#x27;0&#x27;으로, 텍스트를 &#x27;a&#x27;로 등)를 수행하고 결과를 &#x27;패턴&#x27; 필드에 저장합니다.</p><p><strong>STATS </strong>[column1 =] expression1, …<strong> BY </strong>SUBSTRING(pattern, 0, 15):</p><p>이는 클러스터링 단계입니다. 패턴의 처음 15자를 공유하는 로그를 그룹화하고 그룹당 총 로그 수, 로그 데이터 소스 목록, 패턴 접두사, 3개의 로그 예시와 같은 집계 필드를 만듭니다.</p><p><strong>SORT</strong> total_count DESC | <strong>LIMIT</strong> 100 : 가장 빈번한 상위 100개의 로그 패턴을 표시합니다</p><p>LogHub의 쿼리 결과는 아래와 같습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/b119498f124376c41d242a099bf9081fd6536be8-1600x394.png" style="width: 100%;" /><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/4dcfc0a5b7fa753497cc5def5ea3cd54449c0481-1600x719.png" style="width: 100%;" /><p>시각화 자료에서 볼 수 있듯이, 이 &#x27;LLM 없는&#x27; 접근 방식은 높은 정확도로 로그를 분할합니다. LogHub 레이블을 기준으로 16개 데이터 소스 중 10개를 완벽하게(&gt;90%) 클러스터링했고, 13개 소스에서는 과반수 클러스터링을 달성했습니다(&gt;60%). 이 모든 결과는 추가적인 데이터 정제, 전처리 또는 미세 조정 없이 얻어졌습니다.</p><p>로그 형식 지문은 <a href="https://www.elastic.co/docs/reference/aggregations/search-aggregations-bucket-categorize-text-aggregation">로그 패턴 분석</a>과 같은 정교한 ML 솔루션에 더해 실용적이고 영향력이 큰 대안을 제공합니다. 로그 관계에 대한 즉각적인 인사이트를 제공하고 대규모 로그 클러스터를 효과적으로 관리할 수 있습니다.</p><ul><li>원시적인 요소로서의 높은 활용도 </li></ul><p><a href="https://www.elastic.co/blog/getting-started-elasticsearch-query-language">ES|QL</a> 구현 덕분에 LFF는 빠른 데이터 진단/시각화를 위한 독립형 도구로, 그리고 대용량 사용 사례를 위한 로그 분석 파이프라인의 구성 요소로 모두 사용할 수 있습니다. </p><ul><li>유연성</li></ul><p>LFF는 쉽게 사용자 정의하고 확장하여 특정 패턴(예: 16진수 및 IP 주소)을 캡처할 수 있습니다.</p><ul><li>결정론적 안정성</li></ul><p>ML 기반 클러스터링 알고리즘과 달리, LFF 로직은 간단하고 결정론적입니다. 새로 들어오는 로그는 기존 로그 클러스터에 소급하여 영향을 미치지 않습니다.</p><ul><li>성능 및 mMemory</li></ul><p>최소한의 메모리만 필요하고, 학습이나 GPU도 필요 없으므로 실시간 고처리량 환경에 적합합니다.</p><h2>로그 형식 지문과 LLM 결합하기</h2><p>제안된 하이브리드 아키텍처를 검증하기 위해 각 실험에는 각 데이터 소스에서 무작위로 선택한 20%의 로그 하위 집합이 포함되었습니다. 이러한 제약 조건은 로그가 배치 처리되는 실제 프로덕션 환경을 시뮬레이션한 것으로, 로그가 하나의 거대한 과거 데이터 더미로 처리되는 환경과는 다릅니다.</p><p>목표는 LFF가 효과적인 압축 레이어로 작용함을 입증하는 것이었습니다. 또한 엄선된 소규모 샘플로부터 높은 커버리지를 갖는 구문 분석 규칙을 생성하고 이를 전체 데이터 세트에 성공적으로 일반화할 수 있음을 증명하고자 했습니다.</p><h2>실행 파이프라인</h2><p>데이터가 LLM에 도달하기 전에 필터링, 클러스터링, 계층화된 샘플링을 적용하는 다단계 파이프라인을 구현했습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/b3f46ea471760b406a32fc7d4bc74cc03faaced2-3840x1660.png" style="width: 100%;" /><p>1. 2단계 계층적 클러스터링</p><ul><li>하위 클래스(정확히 일치): 로그는 동일한 지문을 기준으로 집계됩니다. 하나의 하위 클래스에 있는 모든 로그는 정확히 동일한 형식 구조를 공유합니다.</li><li>이상값 정리. 전체 로그 볼륨의 5% 미만을 차지하는 하위 클래스는 모두 삭제합니다. 이렇게 하면 LLM이 주요 신호에 집중할 수 있고 노이즈나 잘못된 로그에 의해 방해받지 않게 됩니다.</li><li>메타클래스(접두사 일치): 나머지 하위 클래스는 형식 지문 일치의 첫 N 문자에 의해 메타클래스로 그룹화됩니다. 이 그룹화 전략은 어휘적으로 유사한 형식을 하나의 범주로 효과적으로 분할합니다. 데이터 소스를 알 수 없는 경우 로그 구문 분석에는 N=5를, 로그 분할에는 N=15를 선택했습니다.</li></ul><p>2. 계층화된 샘플링. 계층 구조 트리가 구축되면 LLM에 대한 로그 샘플을 구성합니다. 전략적 목표는 분산 범위를 최대화하고 토큰 사용량을 최소화하는 것입니다.</p><ul><li>더 넓은 메타클래스 내의 <em>각</em> 유효한 하위 클래스에서 대표적인 로그를 선택합니다.</li><li>너무 많은 하위 클래스의 예외적인 경우를 관리하기 위해 대상 윈도우 크기에 맞게 무작위 다운샘플링을 적용합니다.</li></ul><p>3. 규칙 생성 마지막으로, LLM에 각 메타클래스에 대해 제공된 샘플의 모든 로그에 맞는 정규식 구문 분석 규칙을 생성하도록 지시합니다. 본 PoC에서는 GPT-4o 미니 모델을 사용했습니다.</p><h2>실험 결과 및 관찰</h2><p>Loghub 데이터 세트에서 구문 분석 정확도 94%, 분할 정확도 91%를 달성했습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/49b2b6a1401dd1f33951da68e5a3fac37d0b5aaa-1600x1506.png" style="width: 100%;" /><p>위의 혼동 행렬은 로그 분할 결과를 보여줍니다. 세로축은 실제 데이터 소스를 나타내고 가로축은 예측된 데이터 소스를 나타냅니다. 히트맵 강도는 로그 볼륨에 대응하며, 밝은 타일은 더 많은 수를 나타냅니다. 대각선 정렬은 산포가 최소화된 상태에서 모델이 소스 귀속을 매우 정확하게 수행함을 보여줍니다.</p><h2>성과 벤치마크 인사이트:</h2><ul><li><strong>최적의 기준선:</strong> 카테고리당 <strong>30–40개 로그 샘플</strong>의 컨텍스트 윈도우가 &#x27;최적의 지점&#x27;으로 입증되었으며, 정규식과 Grok 패턴에서 모두 일관되게 강력한 구문 분을 생성했습니다.</li><li><strong>입력 최소화:</strong> 정규식 패턴에 대해 카테고리당 입력 크기를 10개의 로그로 늘렸을 때 구문 분석 성능이 2%만 저하하는 것을 확인했으며, 이는 다양성 기반 샘플링이 원시 볼륨보다 더 중요하다는 점을 입증합니다.</li></ul>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Jina 모델, 그 기능 및 Elasticsearch에서의 사용에 대한 소개]]></title>
            <description><![CDATA[Jina 멀티모달 임베딩, Reranker v3 및 시맨틱 임베딩 모델을 탐색하고, 이를 Elasticsearch에서 기본적으로 사용하는 방법을 알아보세요.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/jina-models-elasticsearch-guide</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/jina-models-elasticsearch-guide</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Scott Martens]]></dc:creator>
            <pubDate>Thu, 01 Jan 2026 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/407b4c862b51ebdfc7f26db4e25950a65caf1673-656x442.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>Elastic의 Jina는 애플리케이션과 비즈니스 프로세스 자동화를 위한 검색 기반 모델을 제공합니다. 이러한 모델은 Elasticsearch 애플리케이션과 혁신적인 AI 프로젝트에 AI를 도입하기 위한 핵심 기능을 제공합니다.</p><p>Jina 모델은 정보 처리, 정리, 검색을 지원하도록 설계된 세 가지 큰 카테고리로 나뉩니다.</p><ul><li>시맨틱 임베딩 모델</li><li>모델 순위 재지정</li><li>소규모 생성형 언어 모델</li></ul><h2>시맨틱 임베딩 모델</h2><p>시맨틱 임베딩의 핵심 아이디어는 AI 모델이 입력의 의미적 측면을 고차원 공간의 기하학적 형태로 표현하는 방법을 학습할 수 있다는 것입니다.</p><p>시맨틱 임베딩은 고차원 공간에 있는 점(엄밀히 말하면 <em>벡터</em>)으로 생각할 수 있습니다. 임베딩 모델은 일부 디지털 데이터(어떤 것이든 가능하지만 대부분 텍스트나 이미지)를 입력으로 받아 해당 고차원 점의 위치를 일련의 숫자 좌표로 출력하는 신경망입니다. 모델이 제대로 작동한다면 두 시맨틱 임베딩 사이의 거리는 해당 디지털 객체가 동일한 의미를 갖는 정도에 비례합니다.</p><p>검색 애플리케이션에서 이것이 얼마나 중요한지 이해하려면 &#x27;개&#x27;라는 단어와 &#x27;고양이&#x27;라는 단어에 대한 임베딩을 공간의 점으로 상상해 보세요.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/802cf9bbcb82180d3fc91009f9f62027eee8f031-615x615.png" style="width: 100%;" /><p>좋은 임베딩 모델은 &#x27;고양이&#x27;라는 단어에 대해 &#x27;개&#x27;보다 &#x27;고양이&#x27;에 훨씬 가까운 임베딩을 생성해야 하며, &#x27;개&#x27;는 거의 같은 의미이므로 &#x27;고양이&#x27;보다 &#x27;개&#x27;에 훨씬 가까운 임베딩을 가져야 합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/bce49daf9a31b8fb7ce1c6ef7ae4e8117a4e8b33-615x615.png" style="width: 100%;" /><p>모델이 다국어인 경우 &#x27;고양이&#x27;와 &#x27;개&#x27;의 번역에 대해 동일한 결과를 기대할 수 있습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/ce4d030385324526cbd7539140e0e634d939371c-615x615.png" style="width: 100%;" /><p>임베딩 모델은 사물 간 의미의 유사성 또는 유사성을 임베딩 간의 공간적 관계로 변환합니다. 위의 그림은 화면에서 볼 수 있도록 2차원으로만 표현했지만, 모델을 임베드하면 수십에서 수천 개의 차원을 가진 벡터가 만들어집니다. 이를 통해 전체 텍스트의 미묘한 의미를 인코딩할 수 있으며, 수천 단어 이상의 문서에 대해 수백 또는 수천 개의 차원을 가진 공간에 지점을 할당할 수 있습니다.</p><h2>멀티모달 임베딩</h2><p>멀티모달 모델은 시맨틱 임베딩의 개념을 텍스트 이외의 것, 특히 이미지로 확장합니다. 사진에 대한 임베딩은 사진에 대한 충실한 설명을 임베딩하는 것과 비슷할 것으로 예상합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/1ac6aef5b1423e5fe4853e8a547a74e66b0885c2-615x615.png" style="width: 100%;" /><p>시맨틱 임베딩은 다양한 용도를 가지고 있습니다. 무엇보다도 효율적인 분류기를 구축하고, 데이터 클러스터링을 수행하고, 데이터 중복 제거 및 데이터 다양성 조사와 같은 다양한 작업을 수행하는 데 사용할 수 있으며, 이는 모두 수작업으로 관리하기에는 너무 많은 데이터를 다루는 빅데이터 애플리케이션에 중요한 작업입니다.</p><p>임베딩의 가장 큰 직접적인 용도는 정보 검색입니다. Elasticsearch는 임베딩을 키로 사용하여 검색 객체를 저장할 수 있습니다. 쿼리는 임베딩 벡터로 변환되고 검색은 쿼리 임베딩에 가장 가까운 키가 있는 저장된 객체를 반환합니다.</p><p>전통적인 <em>벡터 기반 검색</em>(때때로 <em>희소 벡터 검색</em>이라고 함)이 문서와 쿼리에서 단어나 메타데이터를 기반으로 한 벡터를 사용하는 반면, <em>임베딩 기반 검색</em>(또한 <em>밀집 벡터 검색</em>이라고 함)은 단어가 아닌 AI가 평가한 의미를 사용합니다. 따라서 일반적으로 기존 검색 방법보다 훨씬 더 유연하고 정확합니다.</p><h2>Matryoshka 표현 학습</h2><p>임베딩의 차원 수와 임베딩에 포함된 숫자의 정밀도는 성능에 상당한 영향을 미칩니다. 매우 높은 차원의 공간과 매우 정밀한 숫자는 매우 상세하고 복잡한 정보를 표현할 수 있지만, 학습과 실행에 더 많은 비용이 드는 더 큰 AI 모델을 필요로 합니다. 벡터를 생성하려면 더 많은 저장 공간이 필요하고 벡터 사이의 거리를 계산하는 데 더 많은 컴퓨팅 사이클이 필요합니다. 시맨틱 임베딩 모델을 사용하는 것은 정밀도와 자원 소비 간의 중요한 절충을 포함합니다.</p><p>사용자의 유연성을 극대화하기 위해 Jina 모델은 <a href="https://arxiv.org/abs/2205.13147">마트료시카 표현 학습</a>이라는 기법으로 훈련됩니다. 이렇게 하면 모델이 가장 중요한 의미적 구분을 임베딩 벡터의 첫 번째 차원에 미리 로드하므로 상위 차원을 잘라내도 여전히 좋은 성능을 얻을 수 있습니다.</p><p>실제로 이는 Jina 모델 사용자가 임베딩에 원하는 차원 수를 선택할 수 있음을 의미합니다. 차원을 적게 선택하면 정밀도가 감소하지만, 성능 저하는 미미합니다. 대부분의 작업에서 Jina 모델의 성능 지표는 임베딩 크기를 50% 줄일 때마다 1~2% 감소하며, 크기가 약 95% 감소할 때까지 이러한 경향이 지속됩니다.</p><h2>비대칭 검색</h2><p>시맨틱 유사성은 일반적으로 대칭적으로 측정됩니다. &#x27;고양이&#x27;와 &#x27;개&#x27;를 비교할 때 얻는 값은 &#x27;개&#x27;와 &#x27;고양이&#x27;를 비교할 때 얻는 값과 동일합니다. 그러나 정보 검색에 임베딩을 사용할 때는 대칭을 깨고 검색 객체를 인코딩하는 방식과 다르게 쿼리를 인코딩하면 더 잘 작동합니다.</p><p>이는 임베딩 모델을 훈련하는 방식 때문입니다. 훈련 데이터에는 단어와 같은 동일한 요소가 다양한 맥락에서 나타나는 사례들이 포함되어 있으며, 모델은 요소 간의 맥락적 유사점과 차이점을 비교하여 의미를 학습합니다.</p><p>예를 들어 &#x27;동물&#x27;이라는 단어가 &#x27;고양이&#x27; 또는 &#x27;개&#x27;와 같은 문맥에서 많이 나타나지 않으므로 &#x27;동물&#x27;에 대한 임베딩이 &#x27;고양이&#x27; 또는 &#x27;개&#x27;와 특별히 가깝지 않을 수 있습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/9a33163405af6c71ee7f4ba8ebc86af39e295a69-615x615.png" style="width: 100%;" /><p>따라서 &#x27;동물&#x27;을 검색하면 목표와는 정반대로 고양이와 개에 관한 문서가 검색될 가능성이 줄어듭니다. 따라서 &#x27;동물&#x27;이 검색 대상일 때와 쿼리일 때를 다르게 인코딩합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/363992d4f0affba7937c0c8a9f82c9a531fcd3ba-615x615.png" style="width: 100%;" /><p><em>비대칭 검색</em>이란 쿼리에 다른 모델을 사용하거나, 저장된 데이터를 한 가지 방식으로 인코딩하고 쿼리를 다른 방식으로 인코딩하도록 임베딩 모델을 특별히 학습시키는 것을 의미합니다.</p><h2>멀티벡터 임베딩</h2><p>단일 임베딩은 인덱스 데이터베이스의 기본 프레임워크에 적합하기 때문에 정보 검색에 유용합니다. 즉, 검색 키로 단일 임베딩 벡터를 사용하여 검색 대상 객체를 저장합니다. 사용자가 문서 저장소를 쿼리하면 쿼리가 임베딩 벡터로 변환되고 쿼리 임베딩에 가장 가까운 키(고차원 임베딩 공간에서)를 가진 문서가 후보 일치 항목으로 검색됩니다.</p><p>멀티벡터 임베딩은 약간 다르게 작동합니다. 쿼리와 전체 저장된 객체를 나타내는 고정 길이 벡터를 생성하는 대신, 쿼리의 작은 부분을 나타내는 임베딩 시퀀스를 생성합니다. 구성 요소는 일반적으로 텍스트의 경우 토큰 또는 단어이며, 시각적 데이터의 경우 이미지 타일입니다. 이러한 임베딩은 해당 부분의 의미를 그 컨텍스트 내에서 반영합니다.</p><p>예를 들어 다음 문장을 생각해 보세요.</p><ul><li>그녀는 상냥한 마음씨를 가졌습니다.</li><li>그녀는 마음이 바뀌었습니다.</li><li>그녀는 심장 마비를 일으켰습니다.</li></ul><p>표면적으로는 매우 비슷해 보이지만 멀티벡터 모델은 &#x27;heart&#x27;의 각 인스턴스에 대해 매우 다른 임베딩을 생성하여 전체 문장의 맥락에서 각각이 어떻게 다른 의미를 갖는지를 표현합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/a33e60c8d8ee3d312bca8375ca2a8b0a0cd40ba9-615x615.png" style="width: 100%;" /><p>멀티벡터 임베딩을 통해 두 개체를 비교하려면 한 멀티벡터 임베딩의 각 부분을 다른 멀티벡터 임베딩의 각 부분과 비교하고 그 사이의 최소 거리를 합산하는 모따기 거리를 측정하는 경우가 많습니다. 아래에서 설명하는 Jina Rerankers를 포함한 다른 시스템들은 이들을 이들의 유사성을 평가하도록 특별히 훈련된 AI 모델에 입력합니다. 멀티벡터 임베딩은 단일 벡터 임베딩보다 훨씬 더 자세한 정보를 포함하기 때문에 일반적으로 두 접근 방식 모두 단일 벡터 임베딩을 비교하는 것보다 정밀도가 높습니다.</p><p>그러나 멀티벡터 임베딩은 색인에 적합하지 않습니다. 다음 섹션에서 설명된 <code>jina-colbert-v2</code> 모델과 같이 순위 재지정 작업에서 자주 사용됩니다.</p><h2>Jina 임베딩 모델</h2><h3>Jina 임베딩 v4</h3><p><a href="https://jina.ai/news/jina-embeddings-v4-universal-embeddings-for-multimodal-multilingual-retrieval/"><strong>jina-embeddings-v4</strong></a>는 38억(3.8x10⁹) 매개변수의 다국어 및 멀티모달 임베딩 모델로, 다양한 널리 사용되는 언어의 이미지와 텍스트를 지원합니다. 시각적 지식과 언어 지식을 활용하여 두 작업의 성능을 향상시키는 새로운 아키텍처를 사용하여 이미지 검색, 특히 <a href="https://huggingface.co/tasks/visual-document-retrieval">시각적 문서 검색</a>에서 탁월한 성능을 발휘합니다. 이는 차트, 슬라이드, 맵, 스크린샷, 페이지 스캔 및 다이어그램과 같은 이미지를 처리한다는 것을 의미합니다. 이러한 이미지는 일반적인 종류의 이미지로, 종종 중요한 내장 텍스트가 포함되어 있으며, 실제 장면의 사진으로 훈련된 컴퓨터 비전 모델의 범위를 벗어납니다.</p><p>컴팩트한 <a href="https://huggingface.co/docs/peft/en/package_reference/lora">로우랭크 적응(LoRA) 어댑터</a>를 사용하여 여러 가지 작업에 맞게 이 모델을 최적화했습니다. 이를 통해 메모리나 프로세싱의 추가 비용을 최소화하면서 여러 작업의 성능 저하 없이 단일 모델을 여러 작업에 특화하도록 훈련할 수 있습니다.</p><p>주요 기능은 다음과 같습니다:</p><ul><li>시각적 문서 검색의 최첨단 성능과 함께 다국어 텍스트 및 일반 이미지 성능은 훨씬 더 큰 모델을 능가합니다.</li><li>큰 입력 컨텍스트 크기 지원: 32,768토큰은 대략 두 줄짜리 영어 텍스트 80페이지에 해당하며, 20메가픽셀은 4,500 x 4,500픽셀 이미지에 해당합니다.</li><li>임베딩 크기는 최대 2048개에서 최소 128개까지 사용자가 선택할 수 있습니다. 경험적으로 이 임계값 이하에서는 성능이 급격히 저하되는 것으로 나타났습니다.</li><li>단일 임베딩과 멀티벡터 임베딩을 모두 지원합니다. 텍스트의 경우, 멀티벡터 출력은 각 입력 토큰에 대해 128차원 임베딩 하나로 구성됩니다. 이미지의 경우, 이미지를 커버하는 데 필요한 각 28x28 픽셀 타일에 대해 하나의 128차원 임베딩을 생성합니다.</li><li>비대칭 검색을 위한 최적화는 특별히 이를 위해 훈련된 LoRA 어댑터 한 쌍을 통해 이루어집니다.</li><li>시맨틱 유사도 계산에 최적화된 LoRA 어댑터입니다.</li><li>LoRA 어댑터를 통해서도 컴퓨터 프로그래밍 언어 및 IT 프레임워크를 특별히 지원합니다.</li></ul><p>광범위한 일반 검색, 자연어 이해 및 AI 분석 작업을 위한 범용 다목적 도구로 사용할 수 있도록 <code>jina-embeddings-v4</code>를 개발했습니다. 기능에 비해 비교적 작은 모델이지만 배포하는 데 상당한 리소스가 필요하며 클라우드 API를 통해 사용하거나 대용량 환경에서 사용하기에 가장 적합합니다.</p><h3>Jina 임베딩 v3</h3><p><a href="https://jina.ai/news/jina-embeddings-v3-a-frontier-multilingual-embedding-model/"><strong>jina-embeddings-v3</strong></a>는 6억 개 미만의 매개변수를 가진 소규모 고성능 다국어 텍스트 전용 임베딩 모델입니다. 최대 8192개의 텍스트 입력 토큰을 지원하며, 기본 1024개부터 최대 64개까지 사용자가 선택한 크기의 단일 벡터 임베딩을 출력합니다.</p><p>정보 검색 및 의미적 유사성뿐만 아니라 감정 분석 및 콘텐츠 조정과 같은 분류 작업, 뉴스 집계 및 추천과 같은 클러스터링 작업 등 다양한 텍스트 작업에 대해 <code>jina-embeddings-v3</code>를 학습시켰습니다. <code>jina-embeddings-v4</code>와 마찬가지로 이 모델은 다음 사용 범주에 특화된 LoRA 어댑터를 제공합니다.</p><ul><li>비대칭 검색</li><li>의미적 유사성</li><li>분류</li><li>클러스터링</li></ul><p><code>jina-embeddings-v3</code> 입력 컨텍스트 크기가 크게 줄어든 <code>jina-embeddings-v4</code> 모델보다 훨씬 작지만 운영 비용은 더 적게 듭니다. 그럼에도 불구하고 텍스트에만 해당되긴 하지만 성능 경쟁력이 매우 뛰어나고 많은 사용 사례에서 더 나은 선택입니다.</p><h3>Jina 코드 임베딩</h3><p>Jina의 특수 코드 임베딩 모델인 <a href="https://jina.ai/models/jina-code-embeddings-1.5b"><strong>jina-code-embeddings(0.5b 및 1.5b)</strong></a>는 15가지 프로그래밍 체계와 프레임워크, 그리고 컴퓨팅 및 정보기술 관련 영어 텍스트를 지원합니다. 각각 5억(0.5x10⁹) 및 15억(1.5x10⁹) 크기의 소규모 모델입니다. 두 모델 모두 최대 32,768개의 토큰 입력 컨텍스트 크기를 지원하며, 작은 모델의 경우 896개에서 64개까지, 큰 모델의 경우 1536개에서 128개까지 사용자가 출력 임베딩 크기를 선택할 수 있습니다.</p><p>이러한 모델은 <a href="https://arxiv.org/abs/2101.00190">접두사 튜닝</a>을 사용하여 LoRA 어댑터 대신 5가지 작업별 특화를 위한 비대칭 검색을 지원합니다.</p><ul><li><strong>코드-코드.</strong> 프로그래밍 언어 전반에서 유사한 코드를 검색합니다. 이는 코드 정렬, 코드 중복 제거, 이식 및 리팩토링 지원에 사용됩니다.</li><li><strong>자연어-코드.</strong> 코드를 검색하여 자연어 쿼리, 댓글, 설명 및 문서와 일치시킵니다.</li><li><strong>코드-자연어.</strong>코드를 문서 또는 기타 자연어 텍스트와 일치시킵니다.</li><li><strong>코드-코드 완성.</strong> 관련 코드를 제안하여 기존 코드를 완료하거나 향상시킵니다.</li><li><strong>기술 관련 Q&amp;A.</strong> 정보 기술에 관한 질문에 대한 자연어 답변을 식별합니다. 이는 기술 지원 사용 사례에 이상적입니다.</li></ul><p>이러한 모델은 컴퓨터 문서화 및 프로그래밍 자료와 관련된 작업에서 상대적으로 적은 컴퓨팅 비용으로 우수한 성능을 제공합니다. 개발 환경 및 코드 어시스턴트에 통합하는 데 적합합니다.</p><h3>Jina ColBERT v2</h3><p><a href="https://jina.ai/models/jina-colbert-v2"><strong>jina-colbert-v2</strong></a>는 5억 6천만 개의 매개변수를 가진 멀티벡터 텍스트 임베딩 모델입니다. 다국어 지원, 89개 언어의 자료를 사용하여 학습되었으며 다양한 임베딩 크기와 비대칭 검색을 지원합니다.</p><p>앞서 언급했듯이, 멀티벡터 임베딩은 색인하는 데에는 적합하지 않지만 다른 검색 전략의 결과 정확도를 높이는 데 매우 유용합니다. <code>jina-colbert-v2</code><strong></strong>를 사용하여 멀티벡터 임베딩을 미리 계산한 다음 쿼리 시 검색 후보의 순위를 재조정하는 데 사용할 수 있습니다. 이 접근 방식은 다음 섹션의 재순위 모델 중 하나를 사용하는 것보다 정확도는 떨어지지만 모든 쿼리와 후보 일치에 대해 전체 AI 모델을 호출하는 대신 저장된 멀티벡터 임베딩을 비교하기 때문에 훨씬 더 효율적입니다. 이는 순위 재지정 모델을 사용할 때의 지연 시간과 계산 부하가 너무 큰 사용 사례나 비교할 후보의 수가 순위 재지정 모델에 너무 많은 경우에 이상적으로 적합합니다.</p><p>이 모델은 입력 토큰당 하나씩 일련의 임베딩을 출력하며, 사용자는 128차원, 96차원 또는 64차원 임베딩의 토큰 임베딩을 선택할 수 있습니다. 후보 텍스트 매칭은 8,192개 토큰으로 제한됩니다. 쿼리는 비대칭적으로 인코딩되므로, 사용자는 텍스트가 쿼리인지 후보 일치인지 지정해야 하며, 쿼리 수를 32개의 토큰으로 제한해야 합니다.</p><h3>Jina CLIP v2</h3><p><a href="https://jina.ai/news/jina-clip-v2-multilingual-multimodal-embeddings-for-text-and-images/"><strong>jina-clip-v2</strong></a>는 9억 개의 매개변수를 가진 멀티모달 임베딩 모델로, 텍스트가 이미지의 내용을 설명할 경우 텍스트와 이미지가 서로 가까운 임베딩을 생성하도록 학습되었습니다. 주요 용도는 텍스처 쿼리를 기반으로 이미지를 검색하는 것이지만, 텍스트 전용 모델로도 고성능을 발휘하여 사용자 비용을 절감할 수 있습니다. 텍스트-텍스트 및 텍스트-이미지 검색을 위해 별도의 모델이 필요하지 않기 때문입니다.</p><p>이 모델은 8,192개의 토큰으로 구성된 텍스트 입력 컨텍스트를 지원하며, 이미지는 임베딩을 생성하기 전에 512x512 픽셀로 크기가 조정됩니다.</p><p>대조적 언어-이미지 사전 훈련(CLIP) 아키텍처는 훈련 및 운영이 쉽고 매우 컴팩트한 모델을 생성할 수 있지만, 몇 가지 근본적인 한계가 있습니다. 그들은 한 매체에서 얻은 지식을 다른 매체에서의 성능 향상에 사용할 수 없습니다. 한 매체에서 다른 매체로 사용하여 성능을 향상시킬 수 없습니다. 따라서 &#x27;개&#x27;와 &#x27;고양이&#x27;라는 단어가 &#x27;자동차&#x27;보다 의미상 서로 가깝다는 것은 알 수 있지만, 개 그림과 고양이 그림이 자동차 그림보다 더 관련이 있다는 것을 반드시 알지는 못합니다.</p><p>또한 <em>양식 격차</em>라는 문제도 있습니다. 개에 대한 텍스트 임베딩은 개 사진 임베딩보다 고양이에 대한 텍스트 임베딩에 더 가깝게 느껴질 가능성이 높습니다. 이러한 제한 때문에 CLIP을 텍스트-이미지 검색 모델 또는 텍스트 전용 모델로 사용하는 것이 좋지만, 단일 쿼리에서 둘을 혼합하지 않는 것이 좋습니다.</p><h2>모델 순위 재지정</h2><p>순위 재지정 모델은 하나 이상의 후보 일치 항목과 쿼리를 모델에 입력으로 받아 이를 직접 비교하여 훨씬 더 높은 정밀도의 일치 항목을 생성합니다.</p><p>원칙적으로, 각 쿼리를 저장된 각 문서와 비교하여 정보 검색을 위해 직접 순위 재지정 도구를 사용할 수 있지만, 이는 매우 계산 비용이 많이 들고 가장 작은 컬렉션을 제외하고는 비실용적입니다. 결과적으로, 순위 재지정 도구는 임베딩 기반 검색이나 다른 검색 알고리즘과 같은 다른 방법을 통해 찾은 비교적 짧은 후보 목록을 평가하는 데 사용되는 경향이 있습니다. 순위 재지정 모델은 하이브리드 및 연합 검색 체계에 이상적으로 적합합니다. 여기서 검색을 수행한다는 것은 쿼리가 별도의 검색 시스템으로 전송되어 각각 고유한 데이터 세트를 가지고 각기 다른 결과를 반환할 수 있음을 의미합니다. 다양한 결과를 하나의 고품질 결과로 병합하는 데 매우 효과적입니다.</p><p>임베딩 기반 검색은 저장된 모든 데이터를 재색인하고 검색 결과에 대한 사용자 기대치를 변경해야 하므로 상당한 노력이 필요합니다. 기존 검색 체계에 리랭커를 추가하면 AI의 많은 이점을 추가할 수 있으며, 전체 검색 솔루션을 다시 설계할 필요 없이 이를 달성할 수 있습니다.</p><h2>Jina 순위 재지정 모델</h2><h3>Jina 순위 재지정 m0</h3><p><a href="https://jina.ai/models/jina-reranker-m0/"><strong>jina-reranker-m0</strong></a>는 24억(2.4x10⁹)개의 매개변수를 가진 다중 모드 순위 재지정 도구로, 텍스트 쿼리와 텍스트 및/또는 이미지로 구성된 후보 매치를 지원합니다. 이 모델은 시각적 문서 검색의 선도적 모델로, PDF 저장소, 텍스트 스캔, 스크린샷 및 텍스트 또는 반구조화된 정보를 포함한 기타 컴퓨터 생성 또는 수정 이미지뿐만 아니라 텍스트 문서와 이미지로 구성된 혼합 데이터에도 이상적인 솔루션입니다.</p><p>이 모델은 단일 쿼리와 일치하는 후보를 입력받아 점수를 반환합니다. 동일한 쿼리를 다른 후보와 함께 사용하면 점수를 비교하여 순위를 매기는 데 사용할 수 있습니다. 쿼리 텍스트와 후보 텍스트 또는 이미지를 포함하여 최대 10,240개 토큰의 총 입력 크기를 지원합니다. 이미지를 덮는 데 필요한 28x28 픽셀 타일 하나하나가 입력 크기 계산을 위한 토큰으로 간주됩니다.</p><h3>Jina 순위 재지정 v3</h3><p><a href="https://jina.ai/models/jina-reranker-v3/"><strong>jina-reranker-v3</strong></a>는 비슷한 크기의 모델을 위한 최첨단 성능을 갖춘 6억 개의 매개변수 텍스트 순위 재지정 도구입니다. <code>jina-reranker-m0</code>와 달리, 단일 쿼리와 최대 64개의 후보 매칭 목록을 받아 순위 순서를 반환합니다. 쿼리와 모든 텍스트 후보를 포함하여 131,000개의 토큰으로 구성된 입력 컨텍스트가 있습니다.</p><h3>Jina 순위 재지정 v2</h3><p><a href="https://jina.ai/models/jina-reranker-v2"><strong>jina-reranker-v2-base-multilingual</strong></a>은 함수 호출 및 SQL 쿼리를 지원하도록 설계된 추가 기능을 갖춘 매우 컴팩트한 범용 순위 재지정 도구입니다. 3억 개 미만의 매개변수를 포함하며, 빠르고 효율적이며 정확한 다국어 텍스트 순위 재지정을 제공하며, 텍스트 쿼리에 맞는 SQL 테이블과 외부 함수 선택 지원도 추가하여 에이전트 사용 사례에 적합합니다.</p><h2>소규모 생성형 언어 모델</h2><p>생성형 언어 모델은 텍스트 또는 멀티미디어 입력을 받아 텍스트 출력으로 응답하는 OpenAI의 ChatGPT, Google Gemini, Anthropic의 Claude와 같은 모델입니다. <em>대규모</em> 언어 모델(LLM)과 <em>소규모</em> 언어 모델(SLM)을 구분하는 명확한 경계는 없지만, 최고급 LLM을 개발, 운영 및 사용하는 데 따르는 실질적인 문제는 잘 알려져 있습니다. 가장 잘 알려진 것들은 공개적으로 배포되지 않았기 때문에 우리는 그 크기를 추정할 수만 있지만, ChatGPT, Gemini 및 Claude는 1~3조(1~3x10¹²) 매개변수 범위 내에 있을 것으로 예상됩니다.</p><p>이러한 모델을 실행하는 것은, 심지어 공개적으로 이용 가능하더라도, 기존 하드웨어의 범위를 훨씬 넘어서며, 방대한 병렬 어레이로 구성된 최첨단 칩을 필요로 합니다. LLM에는 유료 API를 통해 액세스할 수 있지만, 이는 상당한 비용이 발생하고 높은 대기 시간을 가지며 데이터 보호, 디지털 주권 및 클라우드 재환원에 대한 요구 사항과 일치하기 어렵습니다. 또한 이 정도 규모의 모델을 교육하고 사용자 지정하는 데 드는 비용도 상당할 수 있습니다.</p><p>그 결과, 대형 LLM의 모든 기능은 부족할 수 있지만 저렴한 비용으로 특정 종류의 작업을 잘 수행할 수 있는 소규모 모델을 개발하기 위해 많은 연구가 진행되었습니다. 기업은 보통 특정 문제를 해결하기 위해 소프트웨어를 배포하며, AI 소프트웨어도 다르지 않습니다. 따라서 SLM 기반 솔루션이 LLM 기반 솔루션보다 나은 경우가 많습니다. 일반적으로 상용 하드웨어에서 실행할 수 있고, 더 빠르며 실행에 필요한 에너지를 덜 소비하고, 훨씬 더 쉽게 사용자 정의할 수 있습니다.</p><p>AI를 실용적인 검색 솔루션에 가장 효과적으로 도입할 수 있는 방법에 집중하면서 Jina의 SLM 제품군은 성장하고 있습니다.</p><h2>Jina SLMs</h2><h3>ReaderLM v2</h3><p><a href="https://jina.ai/models/ReaderLM-v2"><strong>ReaderLM-v2</strong></a>는 사용자가 제공한 JSON 스키마와 자연어 명령어에 따라 HTML을 Markdown 또는 JSON으로 변환하는 생성형 언어 모델입니다.</p><p>데이터 전처리 및 정규화는 디지털 데이터에 대한 효과적인 검색 솔루션을 개발하는 데 필수적인 부분이지만, 실제 세계의 데이터, 특히 웹에서 파생된 정보는 종종 혼란스럽고, 단순한 변환 전략은 매우 취약한 것으로 드러나는 경우가 많습니다. 대신, <code>ReaderLM-v2</code> 웹페이지의 DOM 트리 덤프의 혼란을 이해하고 유용한 요소를 강력하게 식별할 수 있는 지능형 AI 모델 솔루션을 제공합니다.</p><p>15억(1.5x10⁹)개의 매개변수로, 최첨단 LLM보다 세 자릿수 차이로 작지만 이 특정 작업에서는 동등한 수준의 성능을 발휘합니다.</p><h3>Jina VLM</h3><p><a href="https://jina.ai/models/jina-vlm"><strong>jina-vlm</strong></a>은 이미지에 대한 자연어 질문에 답하도록 훈련된 24억 (2.4x10⁹)개의 매개변수 생성형 언어 모델입니다. 그것은 스캔, 스크린샷, 슬라이드, 다이어그램 및 유사한 비자연적 이미지 데이터에 대한 질문에 답변하는 시각적 문서 분석을 매우 강력하게 지원합니다.</p><p>그 예는 다음과 같습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/adfa1420d079ca4fd5582eef4349b1265b378e76-950x500.png" style="width: 100%;" /><p>이미지 속 텍스트를 읽는 데도 매우 능숙합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/ea3956e7ad86f8e171841cab2c28c8b3498da1d4-1002x500.png" style="width: 100%;" /><p>하지만 <code>jina-vlm</code>의 진정한 강점은 정보성 이미지와 인공 이미지의 콘텐츠를 이해하는 것입니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/f68606f9d2d99e2cd616d4ff81db3574dc4e26a5-1020x700.png" style="width: 100%;" /><p>또는:</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/297e85e7e78f296388a02301e1e08fed70827423-1000x500.png" style="width: 100%;" /><p><code>jina-vlm</code> 자동 캡션 생성, 제품 설명, 이미지 대체 텍스트, 시각 장애인을 위한 접근성 애플리케이션에 적합합니다. 또한 검색 증강 생성(RAG) 시스템이 시각 정보를 사용하고 AI 에이전트가 사람의 도움 없이 이미지를 처리할 수 있는 가능성을 열어줍니다.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Elastic Agent Builder와 Strands Agents SDK로 시작하기]]></title>
            <description><![CDATA[Elastic Agent Builder를 활용해 에이전트를 생성하는 방법을 배우고, Strands Agents SDK로 오케스트레이션하는 A2A 프로토콜을 통해 해당 에이전트를 사용하는 방법을 알아보세요.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/agent-builder-a2a-strands-agents-guide</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/agent-builder-a2a-strands-agents-guide</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Jonathan Simon]]></dc:creator>
            <pubDate>Mon, 15 Dec 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/31b6a5c1c30dacbb4d5e58d1c566071e7143a0c8-1600x879.gif?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>AI 에이전트에 대한 아이디어가 있으신가요? 아마 데이터와 관련된 일일 겁니다. 에이전트가 유용한 행동을 시작하려면 결정을 내려야 하고, 그 결정을 제대로 내리려면 적절한 데이터가 필요하기 때문입니다.</p><p>Elastic Agent Builder를 사용하면 데이터 기반 AI 에이전트를 쉽게 구축할 수 있습니다. 이 블로그 게시물에서 그 방법을 알려드리겠습니다. Elastic에 저장된 데이터에 액세스하는 MCP 도구로 에이전트를 생성하는 데 필요한 모든 단계를 살펴본 후, Strands Agents SDK와 그의 Agent2Agent(A2A) 기능을 사용하여 에이전트를 운영해 보겠습니다. <a href="https://strandsagents.com/">Strands Agents SDK</a>는 멀티 에이전트 AI 개발 플랫폼으로, 최소한의 코드만으로 원하는 결과를 얻을 수 있는 에이전틱 앱을 구축할 수 있습니다.</p><p>RPS+ 게임을 플레이하는 AI 에이전트를 만들어 보세요. RPS+는 기존 가위바위보 게임에 새로운 변형을 더한 게임으로, 플레이어에게 몇 가지 추가 선택지를 제공합니다.</p><h2>필수 구성 요소</h2><p>이 블로그 게시물의 단계를 따라 진행하려면 다음과 같은 준비 사항이 필요합니다.</p><ul><li>로컬 컴퓨터에서 실행 가능한 텍스트 편집기<ul><li>이 블로그 게시물의 예제 지침에는 <a href="https://code.visualstudio.com/download">Visual Studio Code</a>를 사용합니다.</li></ul></li><li>로컬 컴퓨터에 설치된 <a href="https://www.python.org/downloads/">Python 3.10 이상</a></li></ul><h2>서버리스 프로젝트 만들기</h2><p>우선 Elastic Agent Builder가 포함된 Elasticsearch Serverless 프로젝트가 필요합니다.</p><p><a href="http://cloud.elastic.co/">cloud.elastic.co</a>로 이동해 새로운 Elasticsearch Serverless 프로젝트를 생성하세요.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/31b6a5c1c30dacbb4d5e58d1c566071e7143a0c8-1600x879.gif" style="width: 100%;" /><h2>인덱스 생성 및 데이터 추가</h2><p>다음으로 Elasticsearch 프로젝트에 몇 가지 데이터를 추가해 보겠습니다. Developer Tools를 열고 명령어를 실행하여, 새 인덱스를 만들고 일부 데이터를 삽입할 수 있습니다. 최상단 탐색 메뉴에서 Developer Tools를 선택하세요.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/f97d5af077504463155655a9e27c171a7f974f71-1600x879.jpg" style="width: 100%;" /><p>다음 PUT 명령어를 복사하고 Developer Tools 콘솔의 요청 입력 영역에 붙여넣으세요. 이 명령문은 “game-docs”라는 이름의 Elasticsearch 인덱스를 생성합니다.</p><pre><code>PUT /game-docs
{
  "mappings": {
    "properties": {
      "title": { "type": "text" },
      "content": { 
        "type": "text"
      },
      "filename": { "type": "keyword" },
      "last_modified": { "type": "date" }
    }
  }
}</code></pre><p>Developer Tools에서 해당 명령문 오른쪽에 표시되는 <strong>Send request</strong>(요청 전송) 버튼을 클릭하세요. Developer Tools의 응답 영역에서 <em>game-docs</em> 인덱스가 생성되었음을 확인하는 알림이 표시될 겁니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/be0555a1930e4d4f58b7ed8b669c9b702532ed17-1600x880.jpg" style="width: 100%;" /><p><em>game-docs</em>라는 이름의 인덱스는 현재 만들고 있는 게임의 데이터를 저장하기에 좋은 장소입니다. 그럼 <em>rps+-md</em>라는 이름의 문서를 이 인덱스에 추가하여, 게임에 필요한 모든 데이터를 저장해 보겠습니다. 다음 PUT 명령어를 복사하여 Developer Tools 콘솔에 붙여넣으세요.</p><pre><code>PUT /game-docs/_doc/rps+-md
{
  "title": "Rock Paper Scissors +",
  "content": "
# Game Name
RPS+

# Starting Prompt
Let's play RPS+ !
---
What do you choose?

# Game Objects
1. Rock 🪨 👊
2. Paper 📜 🖐
3. Scissors ✄ ✌️
4. Light ☼ 👍
5. Dark Energy ☄ 🫱

# Judgement of Victory
* Rock beats Scissors
  * because rocks break scissors
* Paper beats Rock
  * because paper covers rock
* Scissors beat Paper
  * because scissors cut paper
* Rock beats Light
  * because you can build a rock structure to block out light
* Paper beats Light
  * because knowledge stored in files and paper books helps us understand light
* Light beats Dark Energy
  * because light enables humans to lighten up and laugh in the face of dark energy as it causes the eventual heat death of the universe
* Light beats Scissors
  * because light is needed to use scissors safely
* Dark Energy beats Rock
  * because dark energy rocks more than rocks. It rocks rocks and everything else in its expansion of the universe
* Dark Energy beats Paper
  * because humans, with their knowledge stored in files and paper books, can't explain dark energy 
* Scissors beat Dark Energy
  * because a human running with scissors is darker than dark energy

# Invalid Input
I was hoping for an worthy opponent
  - but alas it appears that time has past
  - but alas there's little time for your todo list when [todo:fix this] is so vast

# Cancel Game
The future belongs to the bold. Goodbye..
",
  "filename": "RPS+.md",
  "last_modified": "2025-11-25T12:00:00Z"
}</code></pre><p>해당 명령문 옆에 있는 <strong>Send request</strong>(요청 전송) 버튼을 클릭하여 실행하고 <em>rps+-md</em> 문서를 game-docs 인덱스에 추가하세요.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/3c01d8a4602de5c33337457591a388a4a4e3fad3-1600x879.jpg" style="width: 100%;" /><p>이제 쿼리할 데이터가 준비되었습니다. Agent Builder를 사용하면 이 과정이 그 어느 때보다 간단해집니다.</p><p>최상단 탐색 메뉴에서 <strong>Agents</strong>(에이전트)를 선택하세요.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/82dbd2e9a439cabd5a5eea3d0ce005b87df0c3ea-1600x879.jpg" style="width: 100%;" /><p>그런 다음 기본 Elastic AI 에이전트에게 &quot;What data do I have?(내가 가진 데이터는 무엇인가요?)&quot;라고 묻기만 하면 됩니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/f7a2f39c9d1486bdf02d9e88a732b540ac2e2cd1-1600x872.gif" style="width: 100%;" /><p>Elastic AI 에이전트는 데이터를 평가하고 현재 보유한 데이터에 대한 간결한 설명을 제공합니다.</p><h2>도구 만들기</h2><p>Elastic에 데이터가 준비되었으니 이제 활용해 보겠습니다. Agent Builder는 에이전트가 작업에 필요한 올바른 컨텍스트를 갖출 수 있도록 데이터에 접근하게 도와주는 <a href="https://modelcontextprotocol.io/">MCP</a> 도구를 생성하는 기능을 내장하고 있습니다. 게임 데이터를 조회하는 간단한 도구를 만들어 보겠습니다.</p><p>Agent Builder 작업 메뉴를 클릭하세요.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/0e327c202674dda33bcc0e494d2b588fa8b32e4f-1600x879.png" style="width: 100%;" /><p>메뉴 옵션에서 <strong>View all tools</strong>(모든 도구 보기)를 선택하세요.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/1ebf58650e9fb56750d3f0b1700fab50b44f9bdf-1600x879.png" style="width: 100%;" /><p><strong>+ New Tool</strong>(새 도구)을 클릭하세요.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/6c03a7f28b99ac2d805f34f39948979893316a00-1600x879.png" style="width: 100%;" /><p><strong>Create Tool</strong>(도구 생성) 양식에서 도구 <strong>유형</strong>으로<strong> <a href="https://www.elastic.co/docs/explore-analyze/query-filter/languages/esql">ES|QL</a></strong>을 선택하고 다음 값을 입력하세요.</p><p><strong>Tool ID</strong>(도구 ID)에 다음을 입력하세요.</p><pre><code>example.get_game_docs</code></pre><p><strong>Description</strong>(설명)에 다음을 입력하세요.</p><pre><code>Get RPS+ doc from Elasticsearch game-docs index.</code></pre><p><strong>Configuration</strong>(구성)의 경우 <strong>ES|QL 쿼리</strong> 텍스트 영역에 다음 쿼리를 입력하세요.</p><pre><code>FROM game-docs | WHERE filename == "RPS+.md"</code></pre><p>완성된 <strong>Create tool</strong>(도구 생성) 양식은 다음과 같아야 합니다. <strong>Save</strong>(저장)를 클릭하여 도구를 만드세요.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/b6c93e344600f319b9d2c3030020cf2d171ac1c4-1600x1312.png" style="width: 100%;" /><p>이제 새로운 도구가 준비되었습니다. 도구는 단순히 보관해 두기보다는 가치 있게 활용해야겠죠. 새로 만든 사용자 지정 도구를 사용할 수 있는 에이전트를 만들어 보겠습니다.</p><h2>에이전트를 생성하고 도구를 할당하기</h2><p>Agent Builder를 사용하면 매우 간단하게 에이전트를 만들 수 있습니다. 몇 가지 세부 정보와 함께 에이전트 지침을 입력하기만 하면 됩니다. 이제 에이전트를 만들어보겠습니다.</p><p><strong>Manage agents</strong>(에이전트 관리)를 클릭하세요.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/53934b93db07187e251d4b321cb9ca647e2fd51b-1600x858.png" style="width: 100%;" /><p><strong> + New agent</strong>(새 에이전트)를 클릭하세요.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/fae3ad8f31e71a6dfd044e1daa025a4e280b4e68-1600x490.png" style="width: 100%;" /><p>다음 정보를 <strong>New Agent</strong>(새 에이전트) 양식에 입력하세요.</p><p><strong>Agent ID</strong>(에이전트 ID)에 아래 텍스트를 입력하세요.</p><pre><code>rps_plus_agent</code></pre><p><strong>Custom Instructions</strong>(사용자 지정 지침) 텍스트 영역에 다음 지침을 입력하세요.</p><pre><code>When prompted, if the prompt contains an integer, then select the corresponding numbered item in the list of "Game Objects" from your documents. Otherwise select a random game object. This is your chosen game object for a single round of the game.

# General Game Rules
* 2 players
    - the user: the person playing the game
    - you: the agent playing the game and serving as the game master
* Each player chooses a game object which will be compared and cause them to tie, win or lose.

# Start the game
1. This is the way each new game always starts. You make the first line of your response only the name of your chosen game object. 

2. The remainder of your response should be the "Starting Prompt" text from your documents and generate a list of "Game Objects" for the person playing the game to choose a game object from.  

# End of Game: The game ends in one of the following three outcomes:
1. Invalid Input: If the player responds with an invalid game object choice, respond with variations of the "Invalid Input" text from your documents and then end the game.

2. Tie: The game ends in a tie if the user chooses the same game object as your game object choice.

3. Win or Lose: The game winner is decided based on the "Judgement of Victory" conditions from your documents. Compare the user's game object choice and your game object choice and determine who chose the winning game object.

# Game conclusion
Respond with a declaration of the winner of the game by outputting the corresponding text in the "Judgement of Victory" section of your documents.</code></pre><p><strong>Display name</strong>(표시 이름)에 아래 텍스트를 입력하세요.</p><pre><code>RPS+ Agent</code></pre><p><strong>Display description</strong>(표시 설명)에 아리 텍스트를 입력하세요.</p><pre><code>An agent that plays the game RPS+</code></pre><p><strong>Tools</strong>(도구) 탭을 클릭하여 이전에 만든 사용자 지정 도구를 에이전트에 제공하세요.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/1778f64bc3a1b4004998dc3668ef7f666788e193-1600x1390.png" style="width: 100%;" /><p>앞서 만든 <em>example.get_game_docs</em> 도구만 선택하세요.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/7d734cd80161bcc058817482eb330ffcf1cb567b-1600x1363.png" style="width: 100%;" /><p><strong>Save</strong>(저장)를 클릭하여 새 에이전트를 만드세요.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/c0014faf605ce50c03679ed0d073bd9f3ae7234d-1600x468.png" style="width: 100%;" /><p>새 에이전트를 테스트해 보겠습니다. 에이전트 목록에서 원하는 에이전트와 채팅을 시작할 수 있는 편리한 링크가 있습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/b7d6943ad90a4f68691207caf66b81742e712145-1600x560.png" style="width: 100%;" /><p>&quot;start game(게임 시작)&quot;을 입력하기만 하면 게임이 시작됩니다. 잘 작동하는군요.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/984d008e4cc3f08cc1f101720673b0f7347c066c-1600x874.gif" style="width: 100%;" /><p>에이전트의 응답 상단에 게임 개체 선택 항목이 표시되는 것을 볼 수 있습니다. 이를 통해 에이전트의 선택을 확인하고 게임이 예상대로 작동하는지 확인할 수 있습니다. 하지만 상대방의 선택을 미리 알면 가위바위보 게임이 재미있지는 않습니다. 게임을 최종 형태로 다듬고 완성도를 높이기 위해 코드로 에이전트를 제어할 수 있는 에이전트 오케스트레이션 플랫폼을 사용할 수 있습니다.</p><p>이제 Strands Agents SDK가 등장합니다.</p><h2>Strands Agents SDK</h2><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/c195bba6ff2754f5d8fda174a0c1d247bc283710-456x156.png" style="width: 100%;" /><p>새로운 에이전트 개발 프레임워크를 사용해 보고 싶으시다면, <a href="https://strandsagents.com/latest/">Strands Agents SDK</a>가 시도해 볼 만한 가치가 있습니다. <a href="https://aws.amazon.com/blogs/opensource/introducing-strands-agents-an-open-source-ai-agents-sdk/">Strands Agents SDK는 AWS에서 오픈 소스 Python</a> 구현으로 출시되었으며(2025년 5월), 이제 <a href="https://dev.to/aws/strands-agents-now-speaks-typescript-a-side-by-side-guide-12b3">Typescript</a> 버전도 제공됩니다.</p><h2>Python에서 Strands Agents SDK 시작하기</h2><p>코딩 준비를 시작해 보죠. 이제 Strands Agents를 사용해 A2A 프로토콜로 <em>RPS+ 에이전트</em>를 제어하는 예제 앱을 복제하고 실행하는 과정을 빠르게 살펴보겠습니다. 플레이어가 선택을 한 후에 에이전트의 선택이 공개되도록 RPS+ 게임을 좀 더 세밀하게 조정해 보겠습니다. 결국 추측과 예상치 못한 결과가 바로 가위바위보와 같은 게임을 재미있게 만드는 요소이기 때문입니다.</p><p>로컬 컴퓨터에서 <a href="https://code.visualstudio.com/download">Visual Studio Code</a>를 열고 새 터미널을 여세요.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/2339cc37c89a3524f2b2a21684bc61dae958e1cf-915x460.jpg" style="width: 100%;" /><p>새로 열린 터미널에서 다음 명령어를 실행해 Elasticsearch Labs 리포지토리를 복제하세요.</p><pre><code>git clone https://github.com/elastic/elasticsearch-labs</code></pre><p>다음 <em>cd</em> 명령어를 실행하여 elasticsearch-labs 디렉토리로 이동하세요.</p><pre><code>cd elasticsearch-labs</code></pre><p>이제 다음 명령어를 실행하여 Visual Studio Code에서 리포지토리를 여세요.</p><pre><code>code .</code></pre><p>Visual Studio 파일 탐색기에서 <em>supporting-blog-content</em>와 <em>agent-builder-a2a-strands-agents</em> 폴더를 펼친 다음 <em>elastic_agent_builder_a2a_rps+.py</em> 파일을 여세요. Visual Studio Code에서 파일을 열면 다음과 같이 보입니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/d153b19e0e016c701576edb99ccab5af7c554f34-1484x1530.jpg" style="width: 100%;" /><p>다음은 텍스트 편집기에서 볼 수 있는 <em>elastic_agent_builder_a2a_rps+.py</em> 내용입니다:</p><pre><code>import asyncio
from dotenv import load_dotenv
from uuid import uuid4
import httpx
import os
import random
from a2a.client import A2ACardResolver, ClientConfig, ClientFactory
from a2a.types import Message, Part, Role, TextPart

DEFAULT_TIMEOUT = 60  # set request timeout to 1 minute


def create_message(*, role: Role = Role.user, text: str, context_id=None) -> Message:
    return Message(
        kind="message",
        role="user",
        parts=[Part(TextPart(kind="text", text=text))],
        message_id=uuid4().hex,
        context_id=context_id,
    )


async def main():
    load_dotenv()
    a2a_agent_host = os.getenv("ES_AGENT_URL")
    a2a_agent_key = os.getenv("ES_API_KEY")
    custom_headers = {"Authorization": f"ApiKey {a2a_agent_key}"}

    async with httpx.AsyncClient(
        timeout=DEFAULT_TIMEOUT, headers=custom_headers
    ) as httpx_client:
        # Get agent card
        resolver = A2ACardResolver(httpx_client=httpx_client, base_url=a2a_agent_host)
        agent_card = await resolver.get_agent_card(
            relative_card_path="/rps_plus_agent.json"
        )
        # Create client using factory
        config = ClientConfig(
            httpx_client=httpx_client,
            streaming=True,
        )
        factory = ClientFactory(config)
        client = factory.create(agent_card)
        # Use the client to communicate with the agent
        print("\nSending 'start game' message to Elastic A2A agent...")
        random_game_object = random.randint(1, 5)
        msg = create_message(text=f"start with game object {random_game_object}")
        async for event in client.send_message(msg):
            if isinstance(event, Message):
                context_id = event.context_id
                response_complete = event.parts[0].root.text
                # Get agent choice from the first line of the response
                parsed_response = response_complete.split("\n", 1)
                agent_choice = parsed_response[0]
                print(parsed_response[1])
        # User choice sent for game results from the agent
        prompt = input("Your Choice  : ")
        msg = create_message(text=prompt, context_id=context_id)
        async for event in client.send_message(msg):
            if isinstance(event, Message):
                print(f"Agent Choice : {agent_choice}")
                print(event.parts[0].root.text)


if __name__ == "__main__":
    asyncio.run(main())</code></pre><p>이 코드에서 어떤 일이 일어나고 있는지 살펴보도록 하겠습니다. <em><code>main()</code></em> 메서드에서 시작하여, 코드는 에이전트 URL과 API 키에 대한 환경 변수에 접근하는 것으로 시작합니다. 그런 다음 이러한 값을 사용하여 에이전트의 에이전트 카드를 가져오는 데 사용할 수 있는 <code><em>httpx</em> client</code>를 만듭니다. 그런 다음 클라이언트는 에이전트 카드 세부 정보를 사용하여 &#x27;게임 시작&#x27; 요청을 에이전트에게 보냅니다. 여기서 한 가지 흥미로운 점은 <code>&quot;start game&quot;</code> 요청의 일부로 <code>random_game_object</code> 값을 포함한다는 점입니다. 이 값은 Python 표준 라이브러리의 <em>random</em> 모듈을 사용하여 생성된 임의의 숫자입니다. 이렇게 하는 이유는 AI 에이전트를 가능하게 하는 강력한 LLM들이 무작위성에 능숙하지 않다는 것이 밝혀졌기 때문입니다. 하지만 걱정 마세요. Python이 해결해 주니까요.</p><p>이어서 코드 설명을 계속하자면, 에이전트가 &quot;start game&quot; 요청에 응답한 후, 코드가 에이전트의 게임 객체 선택을 추출하여 <em>agent_choice</em> 변수에 저장합니다. 나머지 응답 내용은 최종 사용자에게 텍스트로 표시됩니다. 그러면 사용자는 게임 객체를 선택하라는 메시지를 받게 되며, 해당 입력은 에이전트로 전송됩니다. 그런 다음 코드는 에이전트의 게임 객체 선택과 함께 게임 결과에 대한 에이전트의 최종 결정을 표시합니다.</p><h2>에이전트 URL과 API 키를 환경 변수로 설정하기</h2><p>예제 앱이 로컬 컴퓨터에서 실행되므로 Agent Builder 에이전트와 통신하기 위해서는 에이전트의 A2A URL과 API 키를 Strands Agents SDK에 제공해야 합니다. 예제 앱은 <em>.env</em>라는 파일을 사용하여 이러한 값을 저장합니다.</p><p><em>env.example</em> 파일의 복사본을 만들고 새 파일 이름을 <em>.env</em>로 지정하세요.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/25ead5f15a17dedb777132a082097cffb06cae4d-1600x843.jpg" style="width: 100%;" /><p>Elastic Agent Builder로 돌아가서 필요한 두 가지 값을 모두 얻을 수 있습니다.</p><p>페이지 오른쪽 상단의 Agent Builder 작업 메뉴에서 <strong>View all tools</strong>(모든 도구 보기)를 선택하세요.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/9c4f4e4a3bd76e11e0a182fa007a2f6aec7777b4-1600x880.jpg" style="width: 100%;" /><p>Tools(도구) 페이지 상단의 <strong>MCP Server</strong> 드롭다운을 클릭하고 <strong>Copy MCP Server URL</strong>(MCP 서버 URL 복사)를 선택하세요.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/6cde0de678bb6f81bef8a59deffb110ad6c6ce26-1600x882.jpg" style="width: 100%;" /><p><strong>MCP Server URL</strong>을 <em>.env</em> 파일에 붙여넣으세요. <strong>&lt;YOUR-ELASTIC-AGENT-BUILDER-URL&gt;</strong> 자리 표시자 값은 이 URL로 대체됩니다. 이제 URL을 한 가지 수정해야 합니다. 끝 부분의 텍스트 “mcp”를 “a2a”로 바꿔야 하는데, 이는 <a href="https://a2a-protocol.org/">A2A 프로토콜</a>이 Agent Strands SDK가 Elastic Agent Builder에서 실행 중인 에이전트와 통신하는 데 사용되기 때문입니다.</p><p>편집된 URL은 다음과 같이 표시될 것입니다.</p><pre><code>https://rps-game-project-12345a.kb.us-east-1.aws.elastic.cloud/api/agent_builder/a2a</code></pre><p>Elastic Cloud에 있는 동안 얻어야 하는 또 다른 값은 API 키입니다. 최상단 탐색 메뉴에서 <strong>Elasticsearch</strong>를 클릭하세요.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/651676b9be65178cdad50b5d24f26441c0bf3f97-1600x549.jpg" style="width: 100%;" /><p><strong>Copy API Key</strong>(API 키 복사) 버튼을 클릭해 API 키를 복사하세요.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/17f1e2ed5c7682630c71e75b0b09ffb1d9036210-1600x879.jpg" style="width: 100%;" /><p>이제 Visual Studio Code로 돌아가 <em>.env</em> 파일에 API 키를 붙여넣어 <strong>&lt;YOUR-ELASTIC-API-KEY&gt;</strong> 자리 표시자 텍스트를 대체하세요. <em>.env</em> 파일은 다음과 같이 표시될 것입니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/a357947e07f29c8c03382e00c7baedf04a399297-1600x286.jpg" style="width: 100%;" /><h2>예제 앱 실행하기</h2><p>Visual Studio Code에서 새 터미널을 여세요.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/33e1174c68ea1ed47c7fe62ab6a6da657c606f56-1413x711.jpg" style="width: 100%;" /><p>터미널에서 다음 <em>cd</em> 명령어를 실행하여 시작하세요.</p><pre><code>cd elasticsearch-labs/supporting-blog-content/agent-builder-a2a-strands-agents</code></pre><p>다음 명령어를 실행하여 Python 가상 환경을 생성하세요.</p><pre><code>python -m venv .venv</code></pre><p>사용 중인 컴퓨터의 운영 체제에 따라 다음 명령어를 실행해 가상 환경을 활성화하세요.</p><ul><li>macOS/Linux</li></ul><pre><code>source .venv/bin/activate</code></pre><ul><li>Windows</li></ul><pre><code>.venv\Scripts\activate</code></pre><p>이 예제 앱은 Strands Agents SDK를 사용하며, 이제 SDK를 설치할 차례입니다. 다음 명령어를 실행하여 Strands Agents SDK와 필요한 모든 Python 라이브러리 종속성을 설치하세요.</p><pre><code>pip install -r requirements.txt</code></pre><p>이제 실행 준비를 끝내고 카운트다운을 시작할 시간입니다. 이 앱을 시작할 준비가 되었으니, 뒤로 물러나세요. 다음 명령어를 사용하여 실행해 보겠습니다.</p><pre><code>python elastic_agent_builder_a2a_rps+.py</code></pre><p>이제 RPS+ 게임을 즐겨보세요. 정말 수고하셨습니다. 행운을 빕니다!</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/041df81fbf1776f09e1243af0a435c4c0af6aca1-1600x948.gif" style="width: 100%;" /><h2>관련성 있는 컨텍스트를 활용해 AI 앱을 구축하기</h2><p>이제 AI 에이전트를 구축할 수 있는 기술을 갖추게 되었습니다. 또한 Strands Agents SDK와 같은 에이전트 개발 프레임워크에서 A2A를 통해 Elastic Agent Builder 에이전트를 사용하는 것이 얼마나 쉬운지도 확인했습니다. <a href="https://cloud.elastic.co/registration?utm_source=agentic-ai-category&amp;utm_medium=search-labs&amp;utm_campaign=agent-builder">Elastic을 사용하여</a> 사용자 지정 데이터의 관련 컨텍스트와 연결된 AI 에이전트를 만들어 보세요.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Google MCP Toolbox for Databases, 새롭게 추가된 Elasticsearch 지원]]></title>
            <description><![CDATA[이제 Google MCP Toolbox for Databases에서 Elasticsearch 지원이 추가되었습니다. ES|QL 도구를 활용해 인덱스를 다양한 MCP 클라이언트와 안전하게 통합해 보세요.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/google-mcp-toolbox-elasticsearch-support</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/google-mcp-toolbox-elasticsearch-support</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Enrico Zimuel, Laurent Saint-Félix]]></dc:creator>
            <pubDate>Fri, 12 Dec 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/425a48691f436ed47c9bdfaf5d561ac122b2c472-1062x668.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>이 글에서는 <a href="https://github.com/elastic/elasticsearch">Elasticsearch</a>와 Google MCP Toolbox를 함께 사용해 Elasticsearch 인덱스에서 정보를 추출하는 간단한 도구를 만드는 방법을 단계별로 살펴봅니다.</p><p>최근 Elastic은 <a href="https://github.com/googleapis/genai-toolbox">Google MCP Toolbox for Databases</a> 오픈소스 프로젝트에 Elasticsearch를 데이터베이스로 사용할 수 있는 지원 기능을 추가했습니다.</p><p>이 새로운 기능을 통해 Google MCP Toolbox로 Elasticsearch에 연결하고 데이터와 직접 &quot;대화하듯&quot; 상호작용할 수 있습니다.</p><h2>Elasticsearch</h2><p>Elasticsearch 인스턴스가 실행 중이어야 합니다. <a href="https://www.elastic.co/cloud">Elastic Cloud</a>에서 무료 체험을 활성화하거나 <a href="https://github.com/elastic/start-local">start-local</a> 스크립트를 사용해 로컬에 설치할 수 있습니다.</p><pre><code>curl -fsSL https://elastic.co/start-local | sh</code></pre><p>이 명령을 실행하면 컴퓨터에 Elasticsearch와 Kibana가 설치되고 Google MCP Toolbox 설정에 사용할 API 키가 생성됩니다.</p><p>API 키는 이전 명령의 출력 결과로 표시되며 elastic-start-local 폴더 내 .env 파일에 저장됩니다.</p><h2>예제 데이터 세트 설치하기</h2><p>설치가 완료되면 사용자 이름 <em>elastic</em>과 start-local 스크립트로 생성된 비밀번호(.env 파일에 저장됨)를 사용해 Kibana에 로그인할 수 있습니다.</p><p>Kibana에서 제공되는 <strong>eCommerce orders</strong> 데이터 세트를 설치할 수 있습니다. 이 데이터 세트에는 <strong>kibana_sample_data_ecommerce</strong>라는 단일 인덱스가 포함되어 있으며 전자상거래 웹사이트의 4,675개 주문 정보를 담고 있습니다. 각 주문에는 다음과 같은 정보가 포함되어 있습니다.</p><ul><li>고객 정보(이름, ID, 생년월일, 이메일 등)</li><li>주문 날짜</li><li>주문 ID</li><li>제품(가격, 수량, ID, 카테고리, 할인 등을 포함한 전체 제품 목록)</li><li>SKU</li><li>총액(세전, 세후)</li><li>총 수량</li><li>지리 정보(도시, 국가, 대륙, 위치, 지역)</li></ul><p>샘플 데이터를 설치하려면 Kibana의 <strong>통합</strong> 페이지를 열고(상단 검색창에서 &quot;통합&quot;을 검색) &quot;Sample Data&quot;를 설치하세요. 자세한 내용은 다음 문서에서 확인할 수 있습니다: <a href="https://www.elastic.co/docs/explore-analyze/#gs-get-data-into-kibana">https://www.elastic.co/docs/explore-analyze/#gs-get-data-into-kibana</a></p><p>이 글의 목표는 Google MCP Toolbox를 설정해 Elasticsearch에 연결하고 자연어로 <strong>kibana_sample_data_ecommerce</strong> 인덱스와 손쉽게 상호작용하는 방법을 보여주는 것입니다.</p><h2>Google MCP Toolbox</h2><p>Google MCP Toolbox는 애플리케이션과 AI 에이전트가 데이터베이스와 안전하고 효율적으로 상호작용할 수 있도록 설계된 오픈 소스 MCP 서버입니다. 이전에는 &#x27;GenAI Toolbox for Databases&#x27;로 알려졌으나 <a href="https://www.anthropic.com/news/model-context-protocol">Model Context Protocol</a>(MCP)을 전면 지원하게 되면서 현재의 이름으로 변경되었습니다. 이 프로젝트의 목적은 연결 풀링, 인증, 통합 가시성 등과 같은 다양한 운영 관리 요소들을 백그라운드에서 처리하여 에이전트를 데이터베이스에 연결할 때 기존에 필요하던 번거로운 작업을 줄이는 데 있습니다.</p><p>Toolbox의 핵심은 개발자가 데이터베이스 상호작용을 캡슐화한 재사용 가능한 고수준 도구를 정의할 수 있도록 한다는 점입니다. 이러한 도구는 MCP 호환 클라이언트(예: AI 에이전트)에서 호출할 수 있으며 클라이언트가 저수준 SQL 쿼리를 구현하거나 데이터베이스 연결을 직접 관리할 필요가 없습니다. 이러한 접근 방식은 데이터베이스 인식 에이전트를 구축하는 데 필요한 보일러플레이트 코드(boilerplate code)의 양을 크게 줄여 단 몇 줄의 애플리케이션 로직만으로도 고급 데이터 처리 기능을 통합할 수 있게 합니다. 한 번 정의된 도구는 여러 에이전트, 프레임워크, 언어 간에 손쉽게 공유할 수도 있습니다(그림 1).</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/137cea290bb70ad5da21853f9a6358cef4cf7451-1248x1056.png" style="width: 100%;" /><p>Toolbox를 사용할 때의 큰 장점 중 하나는 기본으로 제공되는 보안 모델입니다. OAuth2와 OIDC 같은 인증 흐름을 기본적으로 지원하므로 개발자는 에이전트에서 민감한 데이터베이스 자격 증명을 직접 다루거나 저장하지 않아도 됩니다. 또한 이 플랫폼은 OpenTelemetry를 통해 메트릭과 트레이싱을 포함한 통합 가시성 기능을 제공하며 이는 디버깅, 모니터링 및 프로덕션 배포에 필수적입니다. 종합하면 MCP Toolbox는 MCP를 지원하는 모든 시스템에서 데이터와 상호작용하기 위한 통합되고 안전하며 확장 가능한 인터페이스 역할을 합니다.</p><h2>MCP Toolbox 설치 방법</h2><p>다음 명령어를 사용하여 Linux에서 MCP Toolbox 서버를 설치할 수 있습니다.</p><pre><code>export VERSION=0.21.0
curl -L -o toolbox https://storage.googleapis.com/genai-toolbox/v$VERSION/linux/amd64/toolbox
chmod +x toolbox</code></pre><p>macOS 또는 Windows에 설치하려면 <a href="https://googleapis.github.io/genai-toolbox/getting-started/introduction/#installing-the-server">여기</a>에 있는 자세한 안내를 따르세요.</p><h2>Toolbox를 Elasticsearch용으로 구성하기</h2><p>Elasticsearch용 MCP Toolbox를 구성하려면 다음과 같이 <strong>tools.yaml</strong> 파일을 생성해야 합니다.</p><pre><code>sources:
  my-cluster:
    kind: elasticsearch
    addresses:
      - http://localhost:9200
    apikey: <insert-here-api-key>

tools:
  customer-orders:
    kind: elasticsearch-esql
    source: my-cluster
    description: Get the orders made by a customer identified by name.
    query: |
    	FROM kibana_sample_data_ecommerce | WHERE MATCH(customer_full_name, ?name, {"operator": "AND"})
    parameters:
      - name: name
        type: string
        description: The customer name.

toolsets:
  elasticsearch-tools:
    - customer-orders</code></pre><p><strong>&lt;insert-here-api-key&gt;</strong> 값을 유효한 Elasticsearch API 키로 교체해야 합니다. 로컬에서 start-local로 Elasticsearch를 실행하고 있는 경우 생성된 .env 파일의 <strong>ES_LOCAL_API_KEY</strong> 변수에서 API 키를 확인할 수 있습니다. Elastic Cloud를 사용하는 경우 <a href="https://www.elastic.co/docs/deploy-manage/api-keys/elastic-cloud-api-keys">여기</a>에 설명된 절차를 따라 API 키를 생성할 수 있습니다.</p><p>이전 도구에는 Elasticsearch를 위한 다음 ES|QL 쿼리가 포함되어 있습니다:</p><pre><code>FROM kibana_sample_data_ecommerce | WHERE MATCH(customer_full_name, ?name)</code></pre><p>ES|QL에 익숙하지 않다면 간단히 이렇게 이해할 수 있습니다. ES|QL은 Elastic이 개발한 쿼리 언어로 SQL과 비슷한 방식으로 하나 이상의 인덱스를 대상으로 데이터를 검색할 수 있습니다. ES|QL에 대한 더 자세한 내용은 <a href="https://www.elastic.co/docs/reference/query-languages/esql">여기</a>에서 공식 문서를 통해 확인할 수 있습니다.</p><p>위의 쿼리는 <strong>?name</strong> 매개변수(물음표는 매개변수를 의미)를 사용해 지정된 고객 이름이 포함된 모든 주문을 <strong>kibana_sample_data_ecommerce</strong> 인덱스에서 검색합니다.</p><p>고객 이름은 앞서 작성한 YAML 구성에서 string 타입과 &#x27;The customer name&#x27; 설명으로 정의되었습니다.</p><p>이 도구를 사용하면 <em>고객인 Foo는 2025년 10월에 몇 건의 주문을 했나요?</em>와 같은 고객의 주문 관련 질문에 답할 수 있습니다.</p><p>도구와 그 매개변수에 대한 설명은 사용자의 자연어 요청에서 관련 정보를 추출하는 데 매우 중요합니다. 이러한 정보 추출은 대규모 언어 모델(LLM)의 <strong>함수 호출</strong> 기능을 통해 수행됩니다. 실제로 LLM은 필요한 정보를 얻기 위해 실행해야 할 함수(도구)와 해당 함수에 필요한 매개변수를 자동으로 식별할 수 있습니다.</p><p>함수 호출에 대한 자세한 내용은 Ashish Tiwari가 작성한 <a href="https://www.elastic.co/search-labs/blog/function-calling-with-elastic">OpenAI function calling with Elasticsearch(Elasticsearch와 함께 사용하는 OpenAI 함수 호출)</a>라는 글을 참고하세요.</p><h2>Toolbox 서버 실행하기</h2><p>앞서 작성한 tools.yaml 파일을 사용해 MCP Toolbox를 다음 명령어로 실행할 수 있습니다.</p><pre><code>./toolbox --tools-file tools.yaml --ui</code></pre><p><strong> -ui</strong> 매개변수를 사용하면 <a href="http://127.0.0.1:5000/ui">http://127.0.0.1:5000/ui</a>에서 웹 애플리케이션이 실행됩니다(그림 2).</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/3caf2339b632bafd5847af1ed8b33b518a25b8a2-1600x314.png" style="width: 100%;" /><p><strong>Tools</strong> &gt; <strong>customer-orders</strong>를 선택한 뒤 <strong>name</strong> 매개변수에 고객 이름(예: Gwen Sanders)을 입력하고 <strong>Run Tool</strong> 버튼을 클릭할 수 있습니다. 그림 3에 표시된 것과 같은 JSON 응답이 표시됩니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/b167e0142afb8919d9cedf6d0fa431d33d0e55f8-1600x933.png" style="width: 100%;" /><p>이로써 설정이 완료되며 MCP Toolbox는 <strong>customer-orders</strong> 도구를 실행해 Elasticsearch와 통신하고 ES|QL 쿼리를 실행할 수 있습니다.</p><h2>Gemini CLI와 함께 MCP Toolbox 사용하기</h2><p>데이터베이스용 MCP Toolbox와의 통신에는 어떤 MCP 클라이언트든 사용할 수 있습니다. 예를 들어 Gemini를 사용하기 위한 명령줄 도구인 <a href="https://github.com/google-gemini/gemini-cli">Gemini CLI</a>를 사용할 수 있습니다. Gemini CLI는 <a href="https://geminicli.com/docs/get-started/installation/">여기</a>에 안내된 지침에 따라 설치할 수 있습니다.</p><p>Gemini CLI에는 MCP Toolbox용으로 사전 구성된 확장 프로그램이 제공되며 <a href="https://github.com/gemini-cli-extensions/mcp-toolbox">gemini-cli-extensions/mcp-toolbox</a>에서 사용할 수 있습니다. 다음 명령어를 실행해 이 확장 프로그램을 설치할 수 있습니다.</p><pre><code>gemini extensions install https://github.com/gemini-cli-extensions/mcp-toolbox</code></pre><p>설치가 완료되면 MCP Toolbox용 tools.yaml 설정 파일을 저장한 디렉터리로 이동한 뒤 다음과 같이 Gemini CLI를 실행해야 합니다. 이는 Gemini CLI가 MCP Toolbox와 자동으로 구성되기 위해 필요한 단계입니다.</p><pre><code>gemini</code></pre><p>그림 4에 표시된 출력 결과를 확인할 수 있습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/0f22df6d3da13c1dc50dcb560414fa7c630eb9a7-1434x341.png" style="width: 100%;" /><p>다음 명령어로 MCP Toolbox가 연결되어 있는지 확인할 수 있습니다.</p><pre><code>/mcp list</code></pre><p><strong>mcp_toolbox</strong> 항목 아래에 <strong>customer-orders</strong> 도구가 나열된 것을 확인할 수 있습니다(그림 5).</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/97edbc40de9e44f469f6f3a09427532be167de0e-493x155.png" style="width: 100%;" /><p>MCP Toolbox가 Gemini CLI에 연결되었다면 이제 ”<em>고객인 Gwen Sanders의 주문 내역을 알려주세요</em>”와 같은 질문을 해볼 수 있습니다. 그러면 Gemini CLI가 mcp_toolbox 서버에서 customer-orders 도구를 실행할 수 있는 권한을 요청합니다(그림 6 참조).</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/c052f3b5e49436903b804280c0065f67ee02444b-1432x284.png" style="width: 100%;" /><p>확인되면 Gemini CLI는 MCP Toolbox에 요청을 보내 JSON 응답을 받은 다음 이를 바탕으로 최종 응답을 구성합니다(그림 7).</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/7ea5128f1705951c2757af6da4b456d394d4a080-1432x734.png" style="width: 100%;" /><p>Gemini CLI의 응답에는 Gwen Sanders가 2개 상품을 한 번만 주문했으며 총액은 132유로라는 내용이 표시됩니다.</p><h2>MCP Toolbox SDKs</h2><p>Google MCP Toolbox는 Go, Python, JavaScript로 작성된 프로그램에서 모든 기능에 접근할 수 있는 SDK도 제공합니다.</p><p>예를 들어 Python SDK는 다음의 GitHub 페이지에서 이용할 수 있습니다: <a href="https://github.com/googleapis/mcp-toolbox-sdk-python">https://github.com/googleapis/mcp-toolbox-sdk-python</a></p><p>MCP Toolbox에 연결하기 위해 간단한 에이전트를 만들어야 합니다. 이를 위해 다음 패키지를 설치해야 합니다.</p><pre><code>pip install toolbox-core
pip install google-adk</code></pre><p>그리고 아래 명령어로 새로운 에이전트 프로젝트를 생성합니다.</p><pre><code>adk create my_agent</code></pre><p>이 명령을 실행하면 <strong>agent.py</strong> 파일이 포함된 <strong>my_agent</strong>라는 새 디렉터리가 생성됩니다.</p><p>Toolbox에 연결하기 위해 <strong>my_agent/agent.py</strong>를 다음 내용으로 업데이트합니다.</p><pre><code>from google.adk import Agent
from google.adk.apps import App
from toolbox_core import ToolboxSyncClient

client = ToolboxSyncClient("http://127.0.0.1:5000")

root_agent = Agent(
    name='root_agent',
    model='gemini-2.5-flash',
    instruction="You are a helpful AI assistant designed to search information about a dataset of ecommerce orders.",
    tools=client.load_toolset(),
)

app = App(root_agent=root_agent, name="my_agent")</code></pre><p>Google API 키를 포함한 <strong>.env</strong> 파일을 생성합니다.</p><pre><code>echo 'GOOGLE_API_KEY="YOUR_API_KEY"' > my_agent/.env</code></pre><p>마지막으로 에이전트를 실행하여 결과를 확인할 수 있습니다. 에이전트를 실행하려면 아래 명령어를 입력합니다.</p><pre><code>adk run my_agent</code></pre><p>또는 웹 인터페이스로 제공할 수도 있습니다.</p><pre><code>adk web --port 8000</code></pre><p>두 경우 모두 Q&amp;A 인터페이스를 통해 MCP Toolbox와 상호작용할 수 있습니다. 예를 들어 이전 예시처럼 <em>고객인 Gwen Sanders의 주문 내역을 보여주세요</em>라고 질문할 수 있습니다.</p><p>다양한 SDK에 대한 자세한 내용은 <a href="https://googleapis.github.io/genai-toolbox/sdks/">이 문서 페이지</a>를 참고하세요.</p><h2>결론</h2><p>이번 글에서는 Google MCP Toolbox for Databases와 Elasticsearch를 통합하는 방법을 시연했습니다. 간단한 YAML 구성 파일만으로 ES|QL 언어를 활용해 자연어 질문을 Elasticsearch 쿼리로 변환하는 도구 세트를 정의할 수 있습니다.</p><p>또한 전자상거래 웹사이트의 주문 데이터를 포함하고 있는 kibana_sample_data_ecommerce 데이터 세트와 상호작용하는 방법을 시연했습니다. 이 설정 파일을 사용하면 MCP Toolbox 서버를 간단히 실행하고 모든 MCP 클라이언트에서 해당 서버에 연결할 수 있습니다.</p><p>마지막으로 Gemini CLI를 클라이언트로 사용해 MCP Toolbox for Databases에 연결하고 Elasticsearch에 저장된 전자상거래 데이터를 조회하는 과정을 시연했습니다. 특정 고객 이름을 기준으로 주문 정보를 조회하는 자연어 쿼리도 직접 실행해 보았습니다.</p><p>MCP 생태계가 계속 성장함에 따라 안전하고 프로덕션 환경에 바로 투입할 수 있는 인프라를 기반으로 한 경량 도구 정의 방식은 적은 노력으로도 점점 더 고도화되고 데이터에 정통한 에이전트를 구축할 수 있는 새로운 기회를 열어 줍니다. 로컬 환경에서 Elastic의 샘플 데이터 세트로 실험하는 경우나 대규모 애플리케이션에 검색 기능을 통합하는 경우에도 MCP Toolbox는 자연어를 통해 Elasticsearch 데이터와 상호작용할 수 있는 신뢰성 높고 확장 가능한 기반을 제공합니다.</p><p>에이전틱 AI 애플리케이션 개발에 대해 더 알고 싶다면 Anish Mathur와 Dana Juratoni가 작성한 <a href="https://search-labs-redesign.vercel.app/search-labs/blog/ai-agentic-workflows-elastic-ai-agent-builder">Building AI Agentic Workflows with Elasticsearch(Elasticsearch로 AI 에이전틱 워크플로우 구축하기)</a>라는 글을 참고해 보세요.</p><p>Google MCP Toolbox 관련 추가 정보는 <a href="https://googleapis.github.io/genai-toolbox/getting-started/introduction/">https://googleapis.github.io/genai-toolbox/getting-started/introduction/</a>에서 확인할 수 있습니다.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[판단 목록을 사용하여 검색 쿼리의 관련성을 평가합니다.]]></title>
            <description><![CDATA[Elasticsearch에서 확장 가능한 검색 테스트를 위해 검색 쿼리 관련성을 객관적으로 평가하고 리콜과 같은 성능 메트릭을 개선하기 위해 판단 목록을 구축하는 방법을 살펴보세요.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/judgment-lists-search-query-relevance-elasticsearch</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/judgment-lists-search-query-relevance-elasticsearch</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Jhon Guzmán]]></dc:creator>
            <pubDate>Thu, 11 Dec 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/25478d0ffb228afd5d65d82312998ec1c299c565-700x490.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>검색 엔진을 개발하는 개발자들은 종종 같은 문제를 겪습니다. 비즈니스 팀이 특정 검색 결과에 만족하지 못하는데, 기대했던 문서가 검색 결과 상단이 아니라 3~4번째에 표시되기 때문입니다.</p><p>그러나 모든 케이스를 수동으로 테스트할 수 없기 때문에, 한 가지 문제를 해결하면 실수로 다른 쿼리가 손상될 수 있습니다. 하지만 특정 쿼리를 변경했을 때 다른 쿼리에 파급 효과를 미치는지 사용자 또는 QA 팀이 어떻게 테스트할 수 있을까요? 더 중요한 것은 변경 사항이 실제로 쿼리를 개선했는지 어떻게 확인할 수 있을까요?</p><h2>체계적인 평가를 향해</h2><p>이때 판단 목록이 유용하게 작용합니다. 변경할 때마다 수동 및 주관적 테스트에 의존하는 대신, 비즈니스 사례와 관련된 고정된 쿼리 세트와 해당 결과를 정의할 수 있습니다.</p><p>이 세트가 기준이 됩니다. 변경 사항을 구현할 때마다 검색이 실제로 개선되었는지 여부를 평가합니다.</p><p>이 접근 방식의 가치는 다음과 같습니다:</p><ul><li><strong>불확실성 제거</strong>: 변경 사항이 다른 쿼리에 영향을 미치는지를 데이터가 알려줍니다.</li><li><strong>수동 테스트 중지</strong>: 판단 세트가 기록되면 테스트가 자동으로 진행됩니다.</li><li><strong>변경 지원</strong>: 변경의 이점을 뒷받침하는 명확한 메트릭을 표시할 수 있습니다.</li></ul><h2>판단 목록을 만드는 방법</h2><p>가장 쉽게 시작하는 방법 중 하나는 대표 검색어를 선택하고 관련 문서를 수동으로 고르는 것입니다. 이 목록을 작성할 때 두 가지 방법을 사용할 수 있습니다.</p><ul><li><strong>이진 판단:</strong> 쿼리와 연관된 각 문서에 <em>관련 있음</em>(보통 점수 1)과 <strong>관련 없음</strong>(0)이라는 간단한 태그가 붙습니다.</li><li><strong>등급별 판단:</strong> 여기서는 문서마다 다른 수준의 점수가 매겨집니다. 예를 들어, <a href="https://en.wikipedia.org/wiki/Likert_scale">Likert 척도</a>와 유사하게 0~4 척도를 설정해 0은 &#x27;전혀 관련 없음&#x27;, 4는 &#x27;매우 관련 있음&#x27;으로 정의하고, 그 사이에 &#x27;관련 있음&#x27;, &#x27;어느 정도 관련 있음&#x27;과 같은 단계를 둘 수 있습니다.</li></ul><p>이진 판단은 &#x27;이 문서가 결과에 포함되어야 하는가?&#x27;처럼 검색 의도에 명확한 한계가 있을 때 잘 작동합니다.</p><p>등급별 판단은 결과가 애매한 영역이 있을 때 더 유용합니다. 어떤 결과는 다른 결과보다 더 좋으므로 &#x27;매우 좋음&#x27;, &#x27;좋음&#x27;, &#x27;쓸모없음&#x27; 결과를 얻고, 결과의 순서와 사용자의 피드백을 중요시하는 메트릭을 사용할 수 있습니다. 그러나 등급 척도는 검토자마다 점수 수준을 다르게 사용해 판단의 일관성이 떨어질 수 있다는 단점도 갖고 있습니다. 또한 등급별 메트릭은 점수가 높으면 가중치를 더 많이 부여하기 때문에 작은 변화(예: 4점 대신 3점으로 평가)로도 검토자가 의도한 것보다 훨씬 더 큰 변화를 일으킬 수 있습니다. 이렇게 주관적인 요소가 추가되면 시간이 지남에 따라 등급별 판단이 더 복잡하고 관리하기 어려워집니다.</p><h2>문서를 직접 분류해야 하나요?</h2><p>반드시 그렇지는 않습니다. 판단 목록을 만드는 데는 여러 가지 방법이 있고 각각 고유한 장점과 단점이 있기 때문입니다.</p><ul><li><strong>명시적 판단:</strong> 여기서 중소기업은 각 쿼리/문서를 검토하고 관련성 여부(또는 방법)를 수동으로 결정합니다. 이는 품질과 제어 측면에서 장점을 제공하지만 확장성이 떨어집니다.</li><li><strong>암묵적 판단:</strong> 이 방법은 클릭, 이탈률, 구매 등 실제 사용자 행동을 기반으로 관련 문서를 추론합니다. 이 방식을 사용하면 데이터를 자동으로 수집할 수 있지만 편향적일 수 있습니다. 예를 들어, 사용자들은 관련성이 없더라도 검색 결과 상단에 있는 항목을 더 자주 클릭하는 경향이 있습니다.</li><li><strong>AI 생성 판단:</strong> 이 마지막 옵션은 모델(예: LLM)을 사용하여 쿼리와 문서를 자동으로 평가하며, 흔히 <a href="https://en.wikipedia.org/wiki/LLM-as-a-Judge">LLM 배심원단</a>이라고도 합니다. 빠르고 쉽게 확장할 수 있지만, 데이터의 품질은 사용 중인 모델의 품질과 LLM 학습 데이터가 비즈니스 <a href="http://interests.as/">관심사</a>와 얼마나 잘 부합하는지에 달려 있습니다. 인간의 평가와 마찬가지로, LLM 배심원단 역시 편향이나 불일치를 도입할 수 있으므로 신뢰할 수 있는 소규모의 판단과 비교하여 결과를 검증하는 것이 중요합니다. LLM 모델은 본질적으로 확률적이기 때문에 <a href="https://www.ibm.com/think/topics/llm-temperature">온도</a> 파라미터를 0으로 설정해도 같은 결과에 대해 서로 다른 등급을 부여하는 경우가 많습니다.</li></ul><p>다음은 판단 세트를 만드는 가장 좋은 방법을 선택하기 위한 몇 가지 권장 사항입니다.</p><ul><li>가격, 브랜드, 언어, 스타일, 제품 세부정보 등 사용자만 제대로 판단할 수 있는 일부 기능의 중요도를 결정합니다. 중요한 사항이라면 최소한 <em>판단 목록</em>의 일부에 대한 <strong>명시적인 판단</strong>이 필요합니다.</li><li>검색 엔진 트래픽이 이미 충분하여 클릭, 전환, 체류 시간 등의 지표를 활용해 사용 추세를 파악할 수 있는 경우, <strong>암묵적 판단</strong>을 사용하세요. 하지만 편향(예: 사용자는 하위 순위의 결과가 더 관련성이 높더라도 상위 순위의 결과를 더 자주 클릭하는 경향이 있음)을 방지하기 위해 명시적인 판단 기준과 대조하여 신중하게 해석해야 합니다.</li></ul><p>이를 해결하기 위해, 위치 편향 제거 기술이 클릭 데이터를 조정하거나 가중치를 조정하여 사용자의 진정한 관심을 더 잘 반영합니다. 몇 가지 접근 방식은 다음과 같습니다.</p><ul><li><strong>결과 순서 변경</strong>: 일부 사용자를 대상으로 검색 결과 순서를 변경하여 순위가 클릭에 미치는 영향을 추정합니다.</li><li><strong>클릭 모델</strong>에는<strong> </strong><a href="https://wiki.math.uwaterloo.ca/statwiki/index.php?title=a_Dynamic_Bayesian_Network_Click_Model_for_web_search_ranking">동적 베이지안 네트워크 <strong>DBN</strong></a>, <a href="https://rsrikant.com/papers/kdd10.pdf">사용자 브라우징 모델 <strong>UBM</strong></a>이 포함됩니다. 이러한 통계 모델은 스크롤, 체류 시간, 클릭 순서 및 결과 페이지로 돌아가는 등의 패턴을 사용하여 클릭이 단순한 위치가 아니라 실제 관심을 반영할 확률을 추정합니다.</li></ul><h2>예시: 영화 평점 앱</h2><h3>필수 구성 요소</h3><p>이 예시를 실행하려면 Elasticsearch 8.x 클러스터가 <a href="https://www.elastic.co/downloads/elasticsearch">로컬</a>이나 <a href="https://www.elastic.co/cloud/cloud-trial-overview">Elastic Cloud Hosted</a>(호스팅 또는 서버리스)를 실행하고, <a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis">REST API</a> 또는 Kibana에 접근할 수 있어야 합니다.</p><p>사용자가 영화에 대한 의견을 업로드하고 볼 영화를 검색할 수 있는 앱을 생각해 보세요. 사용자들이 직접 작성한 글이기 때문에 오타가 있거나, 표현 방식이 다양할 수 있습니다. 따라서 검색 엔진은 이러한 다양성을 해석하고 사용자에게 유용한 결과를 제공할 수 있어야 합니다.</p><p>전반적인 검색 동작에 영향을 주지 않고 쿼리를 반복적으로 수정할 수 있도록, 비즈니스 팀에서 가장 빈번하게 발생하는 검색어를 기반으로 다음과 같은 이진 판단 기준을 만들었습니다.</p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">쿼리</td><td style="border: 1px solid #ccc; padding: 4px;">DocID</td><td style="border: 1px solid #ccc; padding: 4px;">텍스트</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">디카프리오의 연기</td><td style="border: 1px solid #ccc; padding: 4px;">doc1</td><td style="border: 1px solid #ccc; padding: 4px;">디카프리오의 '레버넌트: 죽음에서 돌아온 자'에서의 연기는 놀라웠다.</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">디카프리오의 연기</td><td style="border: 1px solid #ccc; padding: 4px;">doc2</td><td style="border: 1px solid #ccc; padding: 4px;">인셉션에서는 레오나르도 디카프리오가 그의 가장 상징적인 연기를 보여줍니다.</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">디카프리오의 연기</td><td style="border: 1px solid #ccc; padding: 4px;">doc3</td><td style="border: 1px solid #ccc; padding: 4px;">브래드 피트는 이 범죄 스릴러에서 탄탄한 연기를 선보입니다.</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">디카프리오의 연기</td><td style="border: 1px solid #ccc; padding: 4px;">doc4</td><td style="border: 1px solid #ccc; padding: 4px;">놀라운 시각 효과와 함께 액션으로 가득한 모험을 경험할 수 있다.</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">눈물을 흘리게 하는 슬픈 영화</td><td style="border: 1px solid #ccc; padding: 4px;">doc5</td><td style="border: 1px solid #ccc; padding: 4px;">가슴 아픈 사랑과 상실에 대한 이야기로 몇 시간 동안 눈물을 흘렸습니다.</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">눈물을 흘리게 하는 슬픈 영화</td><td style="border: 1px solid #ccc; padding: 4px;">doc6</td><td style="border: 1px solid #ccc; padding: 4px;">휴지가 꼭 필요한 슬픈 영화</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">눈물을 흘리게 하는 슬픈 영화</td><td style="border: 1px solid #ccc; padding: 4px;">doc7</td><td style="border: 1px solid #ccc; padding: 4px;">웃음을 자아내는 경쾌한 코미디 영화</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">눈물을 흘리게 하는 슬픈 영화</td><td style="border: 1px solid #ccc; padding: 4px;">doc8</td><td style="border: 1px solid #ccc; padding: 4px;">액션과 흥분으로 가득 찬 SF 대작.</td>
                    </tr>
              </tbody>
            </table>
          <p>인덱스 생성:</p><pre><code>PUT movies
{
  "mappings": {
    "properties": {
      "text": {
        "type": "text"
      }
    }
  }
}</code></pre><p>대량 요청:</p><pre><code>POST /movies/_bulk
{ "index": { "_id": "doc1" } }
{ "text": "DiCaprio performance in The Revenant was breathtaking." }
{ "index": { "_id": "doc2" } }
{ "text": "Inception shows Leonardo DiCaprio in one of his most iconic roles." }
{ "index": { "_id": "doc3" } }
{ "text": "Brad Pitt delivers a solid performance in this crime thriller." }
{ "index": { "_id": "doc4" } }
{ "text": "An action-packed adventure with stunning visual effects." }
{ "index": { "_id": "doc5" } }
{ "text": "A heartbreaking story of love and loss that made me cry for hours." }
{ "index": { "_id": "doc6" } }
{ "text": "One of the saddest movies ever made -- bring tissues!" }
{ "index": { "_id": "doc7" } }
{ "text": "A lighthearted comedy that will make you laugh." }
{ "index": { "_id": "doc8" } }
{ "text": "A science-fiction epic full of action and excitement." }</code></pre><p>아래는 앱이 사용하는 Elasticsearch 쿼리입니다.</p><pre><code>GET movies/_search
{
 "query": {
   "match": {
     "text": {
       "query": "DiCaprio performance",
       "minimum_should_match": "100%"
     }
   }
 }
}</code></pre><h3>판단에서 지표로</h3><p>판단 목록은 그 자체로 많은 정보를 제공하지 않으며, 단지 쿼리 결과를 예상할 수 있을 뿐입니다. 그러나 검색 성능을 측정하기 위해 객관적인 메트릭을 계산할 때가 되면 판단 목록이 그 진가를 발휘합니다.</p><p>요즘 가장 많이 사용되는 메트릭은 다음과 같습니다.</p><ul><li><strong><a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/search-rank-eval#k-precision">정확도</a>: </strong>전체 검색 결과 중 실제 관련성이 있는 결과의 비율을 측정합니다.</li><li><strong><a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/search-rank-eval#k-recall">리콜</a>: </strong>검색 엔진이 찾은 x개의 결과 중에서 관련성 있는 결과의 비율을 측정합니다.</li><li><strong><a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/search-rank-eval#_discounted_cumulative_gain_dcg">할인 누적 이득(DCG):</a> </strong>결과 순위의 품질을 측정하며, 가장 관련성이 높은 결과가 최상위에 있어야 한다고 판단합니다.</li><li><a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/search-rank-eval#_mean_reciprocal_rank"><strong>평균 상호 순위(MRR):</strong></a> 첫 번째 관련 결과의 위치를 측정합니다. 목록에서 높은 위치에 있을수록 점수가 높아집니다.</li></ul><p>동일한 영화 평점 앱을 예로 들어 리콜 메트릭을 계산하여 쿼리에서 누락된 정보가 있는지 확인해 보겠습니다.</p><p>Elasticsearch에서는 <em>판단 목록</em>을 사용하여 <a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/search-rank-eval">순위 평가 API</a>를 통해 메트릭을 계산할 수 있습니다. 이 API는 판단 목록, 쿼리, 평가하려는 메트릭을 입력으로 받아 쿼리 결과와 판단 목록을 비교한 값을 반환합니다.</p><p>두 개의 쿼리에 대한 판단 목록을 실행해 보겠습니다.</p><pre><code>POST /movies/_rank_eval
{
 "requests": [
   {
     "id": "dicaprio-performance",
     "request": {
       "query": {
         "match": {
           "text": {
             "query": "DiCaprio performance",
             "minimum_should_match": "100%"
           }
         }
       }
     },
     "ratings": [
       {
         "_index": "movies",
         "_id": "doc1",
         "rating": 1
       },
       {
         "_index": "movies",
         "_id": "doc2",
         "rating": 1
       },
       {
         "_index": "movies",
         "_id": "doc3",
         "rating": 0
       },
       {
         "_index": "movies",
         "_id": "doc4",
         "rating": 0
       }
     ]
   },
   {
     "id": "sad-movies",
     "request": {
       "query": {
         "match": {
           "text": {
             "query": "sad movies that make you cry",
             "minimum_should_match": "100%"
           }
         }
       }
     },
     "ratings": [
       {
         "_index": "movies",
         "_id": "doc5",
         "rating": 1
       },
       {
         "_index": "movies",
         "_id": "doc6",
         "rating": 1
       },
       {
         "_index": "movies",
         "_id": "doc7",
         "rating": 0
       },
       {
         "_index": "movies",
         "_id": "doc8",
         "rating": 0
       }
     ]
   }
 ],
 "metric": {
   "recall": {
     "k": 10,
     "relevant_rating_threshold": 1
     }
 }
}</code></pre><p>&#x27;디카프리오&#x27; 쿼리와 &#x27;슬픈 영화&#x27; 쿼리, 두 가지 요청을 _rank_eval에 사용하겠습니다. 각 요청에는 쿼리와 해당 판단 목록(평가)이 포함됩니다. 평가에 포함되지 않은 문서는 판단이 없는 것으로 간주되므로 모든 문서에 등급을 매길 필요는 없습니다. 계산을 수행할 때 리콜은 평가에서 관련성이 있는 것으로 간주되는 문서인 &#x27;관련 세트&#x27;만 고려합니다.</p><p>이 경우, &#x27;디카프리오&#x27; 쿼리의 리콜은 1이고 &#x27;슬픈 영화&#x27;는 0입니다. 즉, 첫 번째 쿼리에서는 모든 관련 결과를 얻을 수 있었지만 두 번째 쿼리에서는 아무런 결과도 얻지 못했습니다. 따라서 평균 리콜은 0.5입니다.</p><pre><code>{
 "metric_score": 0.5,
 "details": {
   "dicaprio-performance": {
     "metric_score": 1,
     "unrated_docs": [],
     "hits": [
       {
         "hit": {
           "_index": "movies",
           "_id": "doc1",
           "_score": 2.4826927
         },
         "rating": 1
       },
       {
         "hit": {
           "_index": "movies",
           "_id": "doc2",
           "_score": 2.0780432
         },
         "rating": 1
       }
     ],
     "metric_details": {
       "recall": {
         "relevant_docs_retrieved": 2,
         "relevant_docs": 2
       }
     }
   },
   "sad-movies": {
     "metric_score": 0,
     "unrated_docs": [],
     "hits": [],
     "metric_details": {
       "recall": {
         "relevant_docs_retrieved": 0,
         "relevant_docs": 2
       }
     }
   }
 },
 "failures": {}
}</code></pre><p>쿼리의 모든 단어가 문서에 100% 포함되어야 한다고 요구하면 <strong>minimum_should_match</strong> 매개변수를 너무 엄격하게 적용해 관련성 있는 결과를 놓치고 있는 것일지도 모릅니다. 쿼리에서 단어가 하나만 발견되면 문서가 관련성이 있는 것으로 간주되도록 <strong>minimum_should_match</strong> 매개변수를 제거해 보겠습니다.</p><pre><code>POST /movies/_rank_eval
{
 "requests": [
   {
     "id": "dicaprio-performance",
     "request": {
       "query": {
         "match": {
           "text": {
             "query": "DiCaprio performance"
           }
         }
       }
     },
     "ratings": [
       {
         "_index": "movies",
         "_id": "doc1",
         "rating": 1
       },
       {
         "_index": "movies",
         "_id": "doc2",
         "rating": 1
       },
       {
         "_index": "movies",
         "_id": "doc3",
         "rating": 0
       },
       {
         "_index": "movies",
         "_id": "doc4",
         "rating": 0
       }
     ]
   },
   {
     "id": "sad-movies",
     "request": {
       "query": {
         "match": {
           "text": {
             "query": "sad movies that make you cry"
           }
         }
       }
     },
     "ratings": [
       {
         "_index": "movies",
         "_id": "doc5",
         "rating": 1
       },
       {
         "_index": "movies",
         "_id": "doc6",
         "rating": 1
       },
       {
         "_index": "movies",
         "_id": "doc7",
         "rating": 0
       },
       {
         "_index": "movies",
         "_id": "doc8",
         "rating": 0
       }
     ]
   }
 ],
 "metric": {
   "recall": {
     "k": 10,
     "relevant_rating_threshold": 1
     }
 }
}</code></pre><p>보시다시피, 두 쿼리 중 하나에서 <strong>minimum_should_match</strong> 매개 변수를 제거하면 두 쿼리 모두에서 평균 호출 횟수가 1이 됩니다.</p><pre><code>{
  "metric_score": 1,
  "details": {
    "dicaprio-performance": {
      "metric_score": 1,
      "unrated_docs": [],
      "hits": [
        {
          "hit": {
            "_index": "movies",
            "_id": "doc1",
            "_score": 2.0661702
          },
          "rating": 1
        },
        {
          "hit": {
            "_index": "movies",
            "_id": "doc3",
            "_score": 0.732218
          },
          "rating": 0
        },
        {
          "hit": {
            "_index": "movies",
            "_id": "doc2",
            "_score": 0.6271719
          },
          "rating": 1
        }
      ],
      "metric_details": {
        "recall": {
          "relevant_docs_retrieved": 2,
          "relevant_docs": 2
        }
      }
    },
    "sad-movies": {
      "metric_score": 1,
      "unrated_docs": [],
      "hits": [
        {
          "hit": {
            "_index": "movies",
            "_id": "doc7",
            "_score": 2.1307156
          },
          "rating": 0
        },
        {
          "hit": {
            "_index": "movies",
            "_id": "doc5",
            "_score": 1.3160692
          },
          "rating": 1
        },
        {
          "hit": {
            "_index": "movies",
            "_id": "doc6",
            "_score": 1.190063
          },
          "rating": 1
        }
      ],
      "metric_details": {
        "recall": {
          "relevant_docs_retrieved": 2,
          "relevant_docs": 2
        }
      }
    }
  },
  "failures": {}
}</code></pre><p>요약하면, minimum_should_match: 100% 절을 제거하면 두 쿼리 모두에 대해 완벽한 리콜을 얻을 수 있습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/24d055da4348c63827ba7046fe8cafb6f47cadd8-546x628.png" style="width: 100%;" /><p>됐습니다! 성공하셨죠?</p><p>그렇게 어렵지 않아요!</p><p>리콜을 개선함으로써 더 폭넓은 결과를 확보할 수 있게 됩니다. 그러나 각 조정에는 상충되는 부분이 있습니다. 따라서 다양한 메트릭을 사용해 변경 사항을 평가하는 완전한 테스트 케이스를 정의해야 합니다.</p><p>판단 목록과 메트릭을 사용하면 변경 사항을 적용할 때 근거 데이터가 생기므로, 감에 의존해 판단하는 일을 피할 수 있습니다. 검증은 더 이상 수동적이고 반복적이지 않으며, 한 가지 사용 사례뿐만 아니라 여러 사용 사례에서 변경 사항을 테스트할 수 있습니다. 번역:또한 A/B 테스트를 통해 어떤 설정이 사용자와 비즈니스 사례에 가장 적합한지 실제 환경에서 검증할 수 있어, 기술적 메트릭에서 출발해 실제 메트릭으로 다시 연결되는 선순환을 완성할 수 있습니다.</p><h2>판단 목록 사용에 대한 최종 권장 사항</h2><p>판단 목록을 활용하는 것은 단순히 측정하는 것뿐만 아니라, 자신 있게 반복할 수 있는 프레임워크를 만드는 것이기도 합니다. 이를 위해 다음 권장 사항을 따르세요:</p><ol><li><strong>작은 시작이라도, 시작하세요</strong>. 각각 50개의 판단 목록이 있는 10,000개의 쿼리가 필요하지 않습니다. 비즈니스에 가장 중요한 쿼리를 5~10개 식별하고 결과의 상단에 표시할 문서를 정의하기만 하면 됩니다. 이것으로 이미 기반이 갖춰집니다. 일반적으로 상위 쿼리와 결과가 없는 쿼리부터 시작하는 것이 좋습니다. Precision과 같이 쉽게 구성할 수 있는 메트릭으로 테스트를 시작한 후, 복잡성을 높여 나갈 수 있습니다.</li><li><strong>사용자 검증을 통해 타당성을 검증하세요.</strong> 프로덕션 환경에서 A/B 테스트를 통해 수치를 보완하세요. 이렇게 하면 메트릭에서 좋아 보이던 변경 사항이 실제 영향을 발휘하고 있는지 알 수 있습니다.</li><li><strong>목록을 계속 유지하세요.</strong> 비즈니스 사례는 진화할 것이며 중요한 쿼리도 진화할 것입니다. 정기적으로 판단을 업데이트하여 새로운 필요를 반영하세요.</li><li><strong>워크플로의 일부로 만드세요.</strong> 판단 목록을 개발 파이프라인에 통합하세요. 각 구성 변경, 동의어 또는 텍스트 분석이 기본 목록에 대해 자동으로 검증되는지 확인하세요.</li><li><strong>기술 지식을 전략과 연결하세요.</strong> 정확도나 리콜과 같은 기술적 메트릭을 측정하는 데서 멈추지 마세요. 평가 결과를 활용하여 비즈니스 성과에 전달하세요.</li></ol>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[LangGraph.js와 Elasticsearch를 사용하여 금융 AI 검색 워크플로우 구축]]></title>
            <description><![CDATA[LangGraph.js와 Elasticsearch를 사용하여 자연어 쿼리를 투자 및 시장 분석을 위한 동적인 조건부 필터로 전환하는 AI 기반 금융 검색 워크플로우를 구축하는 방법을 알아보세요.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/ai-agent-workflow-finance-langgraph-elasticsearch</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/ai-agent-workflow-finance-langgraph-elasticsearch</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Jeffrey Rengifo]]></dc:creator>
            <pubDate>Fri, 05 Dec 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/12b6057d84c6356267cd178a3c6c1a5c61123ece-2000x1256.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>AI 검색 애플리케이션을 구축할 때는 여러 작업, 데이터 검색 및 데이터 추출을 원활한 워크플로우로 조정하는 것이 중요합니다. LangGraph는 개발자가 노드 기반 구조를 통해 AI 에이전트를 오케스트레이션하여 이 프로세스를 간소화합니다. 이 포스트에서는 <a href="https://langchain-ai.github.io/langgraphjs/">LangGraph.js</a>를 사용하여 금융 솔루션을 구축해 보겠습니다.</p><h2>LangGraph란?</h2><p><a href="https://langchain-ai.github.io/langgraphjs/">LangGraph</a>는 AI 에이전트를 구축하고 워크플로우에서 이를 오케스트레이션하여 AI 지원 애플리케이션을 생성하는 프레임워크입니다. LangGraph에는 작업을 나타내는 함수를 선언하고 워크플로우의 노드로 할당할 수 있는 노드 아키텍처가 있습니다. 여러 노드가 상호작용을 한 결과는 그래프가 됩니다. LangGraph는 모듈형 및 조합 가능한 AI 시스템을 구축하기 위한 도구를 제공하는 광범위한 <a href="https://js.langchain.com/docs/introduction/">LangChain</a> 에코시스템의 일부입니다.</p><p>LangGraph가 유용한 이유를 더 잘 이해하기 위해 LangGraph를 사용하여 문제 상황을 해결해 보겠습니다.</p><h2>솔루션 개요</h2><p>벤처 캐피털 회사에서는 투자자들이 다양한 필터링 옵션을 갖춘 방대한 데이터베이스를 활용할 수 있지만, 기준을 결합하려고 하면 어렵고 시간이 오래 걸립니다. 이로 인해 일부 관련 스타트업이 투자 대상에서 제외될 수 있습니다. 결국 최적의 후보자를 찾는 데 많은 시간을 허비하거나, 심지어 기회를 놓치게 됩니다.</p><p>LangGraph와 Elasticsearch를 사용하면 자연어로 Elasticsearch 검색을 수행할 수 있어 사용자가 수십 개의 필터로 복잡한 요청을 수동으로 작성할 필요가 없습니다. 유연성을 높이기 위해 워크플로우는 사용자 입력에 따라 두 가지 쿼리 유형 중 하나를 자동으로 선택합니다.</p><ul><li><strong>투자 중심 쿼리</strong>: <a href="https://www.investopedia.com/articles/personal-finance/102015/series-b-c-funding-what-it-all-means-and-how-it-works.asp">펀딩 라운드</a>, 가치 평가 또는 <a href="https://www.investopedia.com/terms/r/revenue.asp">수익</a>과 같은 스타트업의 금융 및 자금 조달 측면을 대상으로 합니다. <em>예시:</em> &quot;800만~2500만 달러의 시리즈 A 또는 시리즈 B 펀딩을 받고 월 매출이 50만 달러 이상인 스타트업을 찾아주세요.&quot;</li><li><strong>시장 중심 쿼리</strong>: 이는 <a href="https://en.wikipedia.org/wiki/Vertical_market">산업 분야</a>, <a href="https://en.wikipedia.org/wiki/Target_market">지리적 시장</a> 또는 <a href="https://www.investopedia.com/terms/b/businessmodel.asp">비즈니스 모델</a>에 집중하여 특정 부문이나 지역의 기회를 식별하는 데 도움을 줍니다. <em>예시:</em> &quot;샌프란시스코, 뉴욕 또는 보스턴에서 핀테크 및 헬스케어 스타트업을 찾으주세요.&quot;</li></ul><p>쿼리를 견고하게 유지하기 위해 LLM이 <a href="https://www.elastic.co/docs/solutions/search/search-templates">검색 템플릿</a>을 작성하도록 하고, 전체 <a href="https://www.elastic.co/docs/explore-analyze/query-filter/languages/querydsl">DSL 쿼리</a> 대신 사용합니다. 이렇게 하면 항상 원하는 쿼리를 얻을 수 있으며, LLM은 매번 필요한 쿼리를 작성할 필요 없이 빈칸만 채우면 됩니다.</p><h2>시작에 필요한 사항</h2><ul><li>Elasticsearch APIKey</li><li>OpenAPI APIKey</li><li>Node 18 이상 버전</li></ul><h2>단계별 지침</h2><p>이 섹션에서는 앱이 어떻게 보일지 살펴보겠습니다. 이를 위해 <a href="https://www.typescriptlang.org/">TypeScript</a>를 사용하겠습니다. TypeScript는 JavaScript의 상위 집합으로, 정적 타입을 추가하여 코드의 안정성, 유지관리 용이성, 안전성을 높이고 기존 JavaScript와의 완벽한 호환성을 유지하면서도 오류를 조기에 발견할 수 있도록 합니다.</p><p>노드의 흐름은 다음과 같습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/b47d7f238c4964a6febc0de7fe5e68b186f539c3-363x555.png" style="width: 100%;" /><p>위 이미지는 LangGraph에서 생성되었으며, 노드 간의 실행 순서와 조건부 로직을 정의하는 워크플로우를 나타냅니다.</p><ul><li><strong>decideStrategy: </strong>LLM을 사용하여 사용자의 쿼리를 분석하고 두 가지 전문 검색 전략, 즉 투자 중심 또는 시장 중심 중 하나를 결정합니다.</li><li><strong>prepareInvestmentSearch: </strong>쿼리에서 필터 값을 추출하고 금융 및 자금 조달 관련 매개변수를 강조하는 사전 정의된 템플릿을 작성합니다.</li><li><strong>prepareMarketSearch</strong>: 필터 값을 추출할 뿐만 아니라 시장, 산업 및 지리적 컨텍스트를 강조하는 매개변수를 동적으로 구축합니다.</li><li><strong>executeSearch: </strong>구성된 쿼리를 검색 템플릿을 사용하여 Elasticsearch로 전송하고 일치하는 스타트업 문서를 검색합니다.</li><li><strong>visualizeResults: </strong>최종 결과를 자금 조달, 산업, 수익과 같은 주요 스타트업 속성을 보여주는 명확하고 읽기 쉬운 요약 형식으로 구성합니다.</li></ul><p>이 흐름에는 사용자의 입력에 따라 투자 또는 시장 검색 경로를 선택하는 &#x27;if&#x27; 문 역할을 하는 <a href="https://langchain-ai.github.io/langgraphjs/how-tos/branching/?h=conditional#how-to-create-branches-for-parallel-node-execution">조건부 분기</a>가 포함되어 있습니다. LLM에 의해 구동되는 이러한 결정 로직은 워크플로우를 적응적이고 컨텍스트에 맞게 만들어줍니다. 다음 섹션에서 이 메커니즘에 대해 더 자세히 살펴보겠습니다.</p><h3>LangGraph 상태</h3><p>각 노드를 개별적으로 보기 전에 노드가 어떻게 통신하고 데이터를 공유하는지 이해해야 합니다. 이를 위해 LangGraph를 사용하여 워크플로우 상태를 정의할 수 있습니다. 이는 노드 간에 전달될 공유 상태를 정의합니다.</p><p>상태는 워크플로우 전반에 걸쳐 중간 데이터를 저장하는 공유 컨테이너 역할을 합니다. 사용자의 자연어 쿼리로 시작하여 선택한 검색 전략, Elasticsearch에 준비된 매개변수, 검색 결과, 마지막으로 형식화된 출력을 유지합니다.</p><p>이 구조는 모든 노드가 상태를 읽고 업데이트할 수 있도록 하여 사용자 입력에서 최종 시각화까지 정보의 일관된 흐름을 보장합니다.</p><pre><code>const VCState = Annotation.Root({
  input: Annotation<string>(), // User's natural language query
  searchStrategy: Annotation<string>(), // Search strategy chosen by LLM
  searchParams: Annotation<any>(), // Prepared search parameters
  results: Annotation<any[]>(), // Search results
  final: Annotation<string>(), // Final formatted response
});</code></pre><h3>애플리케이션 설정</h3><p>이 섹션의 모든 코드는 <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/langgraph-js-elasticsearch">elasticsearch-labs 리포지토리</a>에서 확인할 수 있습니다.</p><p>앱이 위치할 폴더에서 터미널을 열고 다음 명령어로 Node.js 애플리케이션을 초기화합니다.</p><pre><code>npm init -y</code></pre><p>이제 이 프로젝트에 필요한 필수 종속성을 설치할 수 있습니다.</p><pre><code>npm install @elastic/elasticsearch @langchain/langgraph @langchain/openai @langchain/core dotenv zod && npm install --save-dev @types/node tsx typescript</code></pre><ul><li><strong><code>@elastic/elasticsearch</code></strong>: 데이터 수집 및 검색과 같은 Elasticsearch 요청을 처리하는 데 도움이 됩니다.</li><li><strong><code>@langchain/langgraph</code></strong>: 모든 LangGraph 도구를 제공하기 위한 JS 종속성입니다.</li><li><strong><code>@langchain/openai</code></strong>: LangChain을 위한 OpenAI LLM 클라이언트.</li><li>@langchain/core: 프롬프트 템플릿 등 LangChain 앱을 위한 기본 구성 요소를 제공합니다.</li><li><strong><code>dotenv</code></strong>: JavaScript에서 환경 변수를 사용하기 위한 필수 종속성입니다.</li><li><strong><code>zod</code></strong>: 유형 데이터에 대한 종속성입니다.</li></ul><p><code>@types/node</code> <code>tsx</code> <code>typescript</code> TypeScript 코드를 작성하고 실행할 수 있게 해줍니다.</p><p>이제 다음 파일을 생성합니다.</p><ul><li><code>elasticsearchSetup<a href="http://ingest.ts/">.ts</a></code>: 인덱스 매핑을 생성하고 JSON 파일에서 데이터 세트를 로드한 후 Elasticsearch에 데이터를 수집합니다.</li><li><a href="http://main.ts/"><code>main.ts</code></a>: LangGraph 애플리케이션을 포함합니다.</li><li><code>.env</code>: 환경 변수를 저장하기 위한 파일</li></ul><p><code>.env</code> 파일에 다음 환경 변수를 추가합시다:</p><pre><code>ELASTICSEARCH_ENDPOINT="your-endpoint-here"
ELASTICSEARCH_API_KEY="your-key-here"
OPENAI_API_KEY="your-key-here"</code></pre><p>OpenAPI APIKey는 코드에서 직접 사용되지 않으며, 대신 라이브러리 <code>@langchain/openai</code>에서 내부적으로 사용됩니다.</p><p>모든 매핑 생성, 검색 템플릿 생성 및 데이터 세트 수집과 관련된 로직은 <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/langgraph-js-elasticsearch/elasticsearchSetup.ts"><code>elasticsearchSetup.ts</code></a> 파일에서 확인할 수 있습니다. 다음 단계에서는 <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/langgraph-js-elasticsearch/main.ts"><code>main.ts</code></a> 파일에 초점을 맞추겠습니다. 또한 데이터 세트를 확인하여 <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/langgraph-js-elasticsearch/dataset.json"><code>dataset.json</code></a> 에서 데이터가 어떻게 보이는지 더 잘 이해할 수 있습니다.</p><h3>LangGraph 앱</h3><p><code>main.ts</code> 파일에서 LangGraph 애플리케이션을 통합하기 위해 필요한 몇 가지 종속성을 가져오겠습니다. 이 파일에는 노드 함수와 상태 선언도 포함해야 합니다. 그래프 선언은 다음 단계에서 <code>main</code> 메서드로 진행됩니다. <code>elasticsearchSetup.ts</code> 파일에는 향후 단계에서 노드 내에서 사용할 Elasticsearch 도우미가 포함됩니다.</p><pre><code>import { writeFileSync } from "node:fs";
import { StateGraph, Annotation, START, END } from "@langchain/langgraph";
import { ChatOpenAI } from "@langchain/openai";
import { z } from "zod";
import {
  esClient,
  ingestDocuments,
  createSearchTemplates,
  INDEX_NAME,
  INVESTMENT_FOCUSED_TEMPLATE,
  MARKET_FOCUSED_TEMPLATE,
  createIndex,
} from "./elasticsearchSetup.js";

const llm = new ChatOpenAI({ model: "gpt-4o-mini" });</code></pre><p>앞서 언급한 바와 같이 LLM 클라이언트는 사용자의 질문에 따라 Elasticsearch 검색 템플릿 매개변수를 생성하는 데 사용됩니다.</p><pre><code>async function saveGraphImage(app: any): Promise<void> {
  try {
    const drawableGraph = app.getGraph();
    const image = await drawableGraph.drawMermaidPng();
    const arrayBuffer = await image.arrayBuffer();

    const filePath = "./workflow_graph.png";
    writeFileSync(filePath, new Uint8Array(arrayBuffer));
    console.log(`📊 Workflow graph saved as: ${filePath}`);
  } catch (error: any) {
    console.log("⚠️  Could not save graph image:", error.message);
  }
}</code></pre><p>해당 메서드는 그래프 이미지를 png 형식으로 생성하고 <a href="https://mermaid.ink/">Mermaid.INK API</a>를 백그라운드에서 사용합니다. 이는 앱 노드가 스타일이 지정된 시각화와 어떻게 상호작용을 하는지 확인하려는 경우에 유용합니다.</p><h3>LangGraph 노드</h3><p>이제 각 노드를 자세히 살펴보겠습니다.</p><h3>decideSearchStrategy 노드</h3><p><code>decideSearchStrategy</code> 노드는 사용자 입력을 분석하여 투자 중심 검색을 수행할지 시장 중심 검색을 수행할지 결정합니다. 이 노드는 정형 출력 스키마(Zod로 정의됨)가 있는 LLM을 사용해 쿼리 유형을 분류합니다. 결정을 내리기 전에 집계를 사용하여 인덱스에서 사용 가능한 필터를 검색하여 모델이 산업, 위치 및 자금 조달 데이터에 대한 최신 컨텍스트를 갖출 수 있도록 합니다.</p><p>필터 가능한 값을 추출하여 LLM으로 전송하기 위해 <a href="https://www.elastic.co/docs/explore-analyze/query-filter/aggregations">집계</a> 쿼리를 사용하여 Elasticsearch 인덱스에서 직접 검색해 보겠습니다. 이 로직은 <code>getAvailableFilters</code> 이라는 메서드에 할당되어 있습니다.</p><pre><code>async function getAvailableFilters() {
  try {
    const response = await esClient.search({
      index: INDEX_NAME,
      size: 0,
      aggs: {
        industries: {
          terms: { field: "industry", size: 100 },
        },
        locations: {
          terms: { field: "location", size: 100 },
        },
        funding_stages: {
          terms: { field: "funding_stage", size: 20 },
        },
        business_models: {
          terms: { field: "business_model", size: 10 },
        },
        lead_investors: {
          terms: { field: "lead_investor", size: 100 },
        },
        funding_amount_stats: {
          stats: { field: "funding_amount" },
        },
      },
    });

    return response.aggregations;
  } catch (error) {
    console.error("❌ Error getting available filters:", error);
    return {};
  }
}</code></pre><p>위의 집계 쿼리로 다음과 같은 결과를 얻었습니다.</p><pre><code>{
  "industries": {
    "doc_count_error_upper_bound": 0,
    "sum_other_doc_count": 0,
    "buckets": [
      {
        "key": "logistics",
        "doc_count": 5
      },
      ...
    ]
  },
  "locations": {
    "doc_count_error_upper_bound": 0,
    "sum_other_doc_count": 0,
    "buckets": [
      {
        "key": "San Francisco, CA",
        "doc_count": 4
      },
      {
        "key": "New York, NY",
        "doc_count": 3
      },
      ...
    ]
  },
  "funding_stages": {
    "doc_count_error_upper_bound": 0,
    "sum_other_doc_count": 0,
    "buckets": [
      {
        "key": "Series A",
        "doc_count": 8
      },
      ...
    ]
  },
  "business_models": {
    "doc_count_error_upper_bound": 0,
    "sum_other_doc_count": 0,
    "buckets": [
      {
        "key": "B2B",
        "doc_count": 13
      },
      ...
    ]
  },
  "lead_investors": {
    "doc_count_error_upper_bound": 0,
    "sum_other_doc_count": 0,
    "buckets": [
      {
        "key": "Battery Ventures",
        "doc_count": 1
      },
      {
        "key": "Benchmark Capital",
        "doc_count": 1
      },
      ...
    ]
  },
  "funding_amount_stats": {
    "count": 20,
    "min": 4500000,
    "max": 35000000,
    "avg": 14075000,
    "sum": 281500000
  }
}</code></pre><p><a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/langgraph-js-elasticsearch/responses/aggregationsResponse.json">여기</a>에서 모든 결과를 확인하세요.</p><p>두 가지 전략 모두 하이브리드 검색을 사용하여 질문의 정형적인 부분(필터)과 보다 주관적인 부분(시맨틱)을 모두 탐지합니다. 다음은 <a href="https://www.elastic.co/docs/solutions/search/search-templates">검색 템플릿</a>을 사용한 두 쿼리의 예입니다.</p><pre><code>await esClient.putScript({
      id: INVESTMENT_FOCUSED_TEMPLATE,
      script: {
        lang: "mustache",
        source: `{
          "size": 5,
          "retriever": {
            "rrf": {
              "retrievers": [
                {
                  "standard": {
                    "query": {
                      "semantic": {
                        "field": "semantic_field",
                        "query": "{{query_text}}"
                      }
                    }
                  }
                },
                {
                  "standard": {
                    "query": {
                      "bool": {
                        "filter": [
                          {"terms": {"funding_stage": {{#join}}{{#toJson}}funding_stage{{/toJson}}{{/join}}}},
                          {"range": {"funding_amount": {"gte": {{funding_amount_gte}}{{#funding_amount_lte}},"lte": {{funding_amount_lte}}{{/funding_amount_lte}}}}},
                          {"terms": {"lead_investor": {{#join}}{{#toJson}}lead_investor{{/toJson}}{{/join}}}},
                          {"range": {"monthly_revenue": {"gte": {{monthly_revenue_gte}}{{#monthly_revenue_lte}},"lte": {{monthly_revenue_lte}}{{/monthly_revenue_lte}}}}}
                        ]
                      }
                    }
                  }
                }
              ],
              "rank_window_size": 100,
              "rank_constant": 20
            }
          }
        }`,
      },
    });</code></pre><p>자세히 설명된 쿼리는 <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/langgraph-js-elasticsearch/elasticsearchSetup.ts#L119"><code>elasticsearchSetup.ts</code></a> 파일에서 확인하세요. 다음 노드에서는 두 쿼리 중 어떤 쿼리를 사용할지 결정합니다.</p><pre><code>// Node 1: Decide search strategy using LLM
async function decideSearchStrategy(state: typeof VCState.State) {
  // Zod schema for specialized search strategy decision
  const SearchDecisionSchema = z.object({
    search_type: z
      .enum(["investment_focused", "market_focused"])
      .describe("Type of specialized search strategy to use"),
    reasoning: z
      .string()
      .describe("Brief explanation of why this search strategy was chosen"),
  });

  const decisionLLM = llm.withStructuredOutput(SearchDecisionSchema);

  // Get dynamic filters from Elasticsearch
  const availableFilters = await getAvailableFilters();

  const prompt = `Query: "${state.input}"
    Available filters: ${JSON.stringify(availableFilters, null, 2)}

    Choose between two specialized search strategies:
    
    - investment_focused: For queries about funding stages, funding amounts, monthly revenue, lead investors, financial performance
    
    - market_focused: For queries about industries, locations, business models, market segments, geographic markets
    
    Analyze the query intent and choose the most appropriate strategy.
  `;

  try {
    const result = await decisionLLM.invoke(prompt);
    console.log(
      `🤔 Search strategy: ${result.search_type} - ${result.reasoning}`
    );

    return {
      searchStrategy: result.search_type,
    };
  } catch (error: any) {
    console.error("❌ Error in decideSearchStrategy:", error.message);
    return {
      searchStrategy: "investment_focused",
    };
  }
}</code></pre><h3>prepareInvestmentSearch 및 prepareMarketSearch 노드</h3><p>두 노드 모두 <code>extractFilterValues</code>이라는 공유 도우미 함수를 사용하는데, 이 함수는 LLM을 활용하여 사용자 입력에 언급된 관련 필터(산업, 위치, 자금 조달 단계, 비즈니스 모델 등)를 식별합니다. 이 스키마를 사용하여 <a href="https://www.elastic.co/docs/solutions/search/search-templates">검색 템플릿</a>을 작성하고 있습니다.</p><pre><code>// Extract all possible filter values from user input
async function extractFilterValues(input: string) {
  const FilterValuesSchema = z.object({
    // Investment-focused filters
    funding_stage: z
      .array(z.string())
      .default([])
      .describe("Funding stage values mentioned in query"),
    funding_amount_gte: z
      .number()
      .default(0)
      .describe("Minimum funding amount in USD"),
    funding_amount_lte: z
      .number()
      .default(100000000)
      .describe("Maximum funding amount in USD"),
    lead_investor: z
      .array(z.string())
      .default([])
      .describe("Lead investor values mentioned in query"),
    monthly_revenue_gte: z
      .number()
      .default(0)
      .describe("Minimum monthly revenue in USD"),
    monthly_revenue_lte: z
      .number()
      .default(10000000)
      .describe("Maximum monthly revenue in USD"),
    industry: z
      .array(z.string())
      .default([])
      .describe("Industry values mentioned in query"),
    location: z
      .array(z.string())
      .default([])
      .describe("Location values mentioned in query"),
    business_model: z
      .array(z.string())
      .default([])
      .describe("Business model values mentioned in query"),
  });

  const extractorLLM = llm.withStructuredOutput(FilterValuesSchema);
  const availableFilters = await getAvailableFilters();

  const extractPrompt = `Extract ALL relevant filter values from: "${input}"
    Available options: ${JSON.stringify(availableFilters, null, 2)}
    Extract only values explicitly mentioned in the query. Leave fields empty if not mentioned.`;

  return await extractorLLM.invoke(extractPrompt);
}</code></pre><p>탐지된 의도에 따라 워크플로우는 다음 두 가지 경로 중 하나를 선택합니다.</p><p><strong>prepareInvestmentSearch:</strong> 자금 조달 단계, 자금 조달 금액, 투자자 및 갱신 정보 등 금융 지향적 검색 매개변수를 구축합니다. 전체 쿼리 템플릿은 <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/langgraph-js-elasticsearch/elasticsearchSetup.ts"><code>elasticsearchSetup.ts</code></a> 파일에서 확인할 수 있습니다.</p><pre><code>// Node 2A: Prepare Investment-Focused Search Parameters 
async function prepareInvestmentSearch(state: typeof VCState.State) {
  console.log(
    "💰 Preparing INVESTMENT-FOCUSED search parameters with financial emphasis..."
  );

  try {
    // Extract all filter values from input
    const values = await extractFilterValues(state.input);

    let searchParams: any = {
      template_id: INVESTMENT_FOCUSED_TEMPLATE,
      query_text: state.input,
      ...values,
    };

    return { searchParams };
  } catch (error) {
    console.error("❌ Error preparing investment-focused params:", error);
    return {
      searchParams: {},
    };
  }
}</code></pre><p><strong>prepareMarketSearch:</strong> 산업, 지역, 비즈니스 모델에 초점을 맞춘 시장 기반 매개변수를 생성합니다. 전체 쿼리는 <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/langgraph-js-elasticsearch/elasticsearchSetup.ts"><code>elasticsearchSetup.ts</code></a> 에서 확인하세요.</p><pre><code>// Node 2B: Prepare Market-Focused Search Parameters
async function prepareMarketSearch(state: typeof VCState.State) {
  console.log(
    "🔍 Preparing MARKET-FOCUSED search parameters with market emphasis..."
  );

  try {
    // Extract all filter values from input
    const values = await extractFilterValues(state.input);

    let searchParams: any = {
      template_id: MARKET_FOCUSED_TEMPLATE,
      query_text: state.input,
      ...values,
    };

    return { searchParams };
  } catch (error) {
    console.error("❌ Error preparing market-focused params:", error);
    return {};
  }
}</code></pre><h3>executeSearch 노드</h3><p>이 node는 상태에서 생성된 검색 매개변수를 가져와 먼저 <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-render-search-template">_render API</a> 를 사용하여 디버깅 목적으로 쿼리를 시각화한 후, 결과를 검색하기 위해 Elasticsearch로 요청을 보냅니다.</p><pre><code>// Node 3: Execute Search
async function executeSearch(state: typeof VCState.State) {
  const { searchParams } = state;

  try {
    // getting formed query from template for debugging
    const renderedTemplate = await esClient.renderSearchTemplate({
      id: searchParams.template_id,
      params: searchParams,
    });

    console.log(
      "📋 Complete query:",
      JSON.stringify(renderedTemplate.template_output, null, 2)
    );

    const results = await esClient.searchTemplate({
      index: INDEX_NAME,
      id: searchParams.template_id,
      params: searchParams,
    });

    return {
      results: results.hits.hits.map((hit: any) => hit._source),
    };
  } catch (error: any) {
    console.error(`❌ ${state.searchParams.search_type} search error:`, error);
    return { results: [] };
  }
}</code></pre><h3>visualizeResults 노드</h3><p>마지막으로, 이 node는 Elasticsearch 결과를 표시합니다.</p><pre><code>// Node 4: Visualize results
async function visualizeResults(state: typeof VCState.State) {
  const results = state.results || [];

  let formattedResults = `🎯 Found ${results.length} startups matching your criteria:\n\n`;

  results.forEach((startup: any, index: number) => {
    formattedResults += `${index + 1}. **${startup.company_name}**\n`;
    formattedResults += `   📍 ${startup.location} | 🏢 ${startup.industry} | 💼 ${startup.business_model}\n`;
    formattedResults += `   💰 ${startup.funding_stage} - $${(
      startup.funding_amount / 1000000
    ).toFixed(1)}M\n`;
    formattedResults += `   👥 ${startup.employee_count} employees | 📈 $${(
      startup.monthly_revenue / 1000
    ).toFixed(0)}K MRR\n`;
    formattedResults += `   🏦 Lead: ${startup.lead_investor}\n`;
    formattedResults += `   📝 ${startup.description}\n\n`;
  });

  return {
    final: formattedResults,
  };
}</code></pre><p>프로그래밍 방식으로 전체 그래프는 다음과 같습니다.</p><pre><code>  const workflow = new StateGraph(VCState)
    // Register nodes - these are the processing functions
    .addNode("decideStrategy", decideSearchStrategy)
    .addNode("prepareInvestment", prepareInvestmentSearch)
    .addNode("prepareMarket", prepareMarketSearch)
    .addNode("executeSearch", executeSearch)
    .addNode("visualizeResults", visualizeResults)
    // Define execution flow with conditional branching
    .addEdge(START, "decideStrategy") // Start with strategy decision
    .addConditionalEdges(
      "decideStrategy",
      (state: typeof VCState.State) => state.searchStrategy, // Conditional function
      {
        investment_focused: "prepareInvestment", // If investment focused -> RRF template preparation
        market_focused: "prepareMarket", // If market focused -> dynamic query preparation
      }
    )
    .addEdge("prepareInvestment", "executeSearch") // Investment prep -> execute
    .addEdge("prepareMarket", "executeSearch") // Market prep -> execute
    .addEdge("executeSearch", "visualizeResults") // Execute -> visualize
    .addEdge("visualizeResults", END); // End workflow</code></pre><p>보시다시피 앱이 다음에 실행할 &#x27;경로&#x27; 또는 &#x27;노드&#x27;를 결정하는 조건부 엣지가 있습니다. 이 기능은 워크플로우에서 여러 도구 중 하나를 선택하거나 인간이 개입하는 단계를 포함하는 등 분기 로직이 필요할 때 유용합니다.</p><p>LangGraph의 핵심 기능을 이해하면 다음과 같이 코드가 실행될 애플리케이션을 설정할 수 있습니다.</p><p><code>main</code> 메서드에 모든 것을 통합합니다. 여기서는 변수 워크플로우 아래의 모든 요소를 포함하는 그래프를 선언합니다.</p><pre><code>async function main() {
  await createIndex();
  await createSearchTemplates();
  await ingestDocuments();

  // Create the workflow graph with shared state
  const workflow = new StateGraph(VCState)
    // Register nodes - these are the processing functions
    .addNode("decideStrategy", decideSearchStrategy)
    .addNode("prepareInvestment", prepareInvestmentSearch)
    .addNode("prepareMarket", prepareMarketSearch)
    .addNode("executeSearch", executeSearch)
    .addNode("visualizeResults", visualizeResults)
    // Define execution flow with conditional branching
    .addEdge(START, "decideStrategy") // Start with strategy decision
    .addConditionalEdges(
      "decideStrategy",
      (state: typeof VCState.State) => state.searchStrategy, // Conditional function
      {
        investment_focused: "prepareInvestment", // If investment focused -> RRF template preparation
        market_focused: "prepareMarket", // If market focused -> dynamic query preparation
      }
    )
    .addEdge("prepareInvestment", "executeSearch") // Investment prep -> execute
    .addEdge("prepareMarket", "executeSearch") // Market prep -> execute
    .addEdge("executeSearch", "visualizeResults") // Execute -> visualize
    .addEdge("visualizeResults", END); // End workflow


  const app = workflow.compile();

  await saveGraphImage(app);

  const query =
    "Find startups with Series A or Series B funding between $8M-$25M and monthly revenue above $500K";

  const marketResult = await app.invoke({ input: query });
  console.log(marketResult.final);
}</code></pre><p>쿼리 변수는 가상의 검색창에 입력된 사용자 입력을 시뮬레이션합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/e8d76909eb2bc1bb62f3ca9a8b3e4b85fcec2893-1600x164.png" style="width: 100%;" /><p>&quot;800만~2500만 달러의 시리즈 A 또는 시리즈 B 펀딩을 받고 월 매출이 50만 달러 이상인 스타트업을 찾아주세요&quot;라는 자연어 문구에서 모든 필터가 추출됩니다.</p><p>마지막으로 다음과 같이 메인 메서드를 호출합니다.</p><pre><code>main().catch(console.error);</code></pre><h3>결과</h3><pre><code>🔍 Checking if index exists...
🏗️ Creating index...
✅ Index created successfully!
Ingesting documents...
✅ Documents ingested successfully!
✅ Investment-focused template created successfully!
✅ Market-focused template created successfully!

📊 Workflow graph saved as: ./workflow_graph.png

🔍 Query: "Find startups with Series A or Series B funding between $8M-$25M and monthly revenue above $500K"

🤔 Search strategy: investment_focused - The query specifically seeks profitable fintech startups with defined funding amounts and high monthly revenue, which aligns closely with financial performance metrics and investment-related criteria.

💰 Preparing INVESTMENT-FOCUSED search parameters with financial emphasis...

📋 Complete query: {
  "size": 5,
  "retriever": {
    "rrf": {
      "retrievers": [
        {
          "standard": {
            "query": {
              "semantic": {
                "field": "semantic_field",
                "query": "Find startups with Series A or Series B funding between $8M-$25M and monthly revenue above $500K"
              }
            }
          }
        },
        {
          "standard": {
            "query": {
              "bool": {
                "filter": [
                  {
                    "terms": {
                      "funding_stage": [
                        "Series A",
                        "Series B"
                      ]
                    }
                  },
                  {
                    "range": {
                      "funding_amount": {
                        "gte": 8000000,
                        "lte": 25000000
                      }
                    }
                  },
                  {
                    "terms": {
                      "lead_investor": []
                    }
                  },
                  {
                    "range": {
                      "monthly_revenue": {
                        "gte": 500000,
                        "lte": 0
                      }
                    }
                  }
                ]
              }
            }
          }
        }
      ],
      "rank_window_size": 100,
      "rank_constant": 20
    }
  }
}
🎯 Found 5 startups matching your criteria:

1. **TechFlow**
   📍 San Francisco, CA | 🏢 logistics | 💼 B2B
   💰 Series A - $8.0M
   👥 45 employees | 📈 $500K MRR
   🏦 Lead: Sequoia Capital
   📝 TechFlow optimizes supply chain operations using AI-powered route optimization and real-time tracking. Founded in 2023, shows remarkable growth with $500K monthly revenue.

2. **DataViz**
   📍 New York, NY | 🏢 enterprise software | 💼 B2B
   💰 Series A - $10.0M
   👥 42 employees | 📈 $450K MRR
   🏦 Lead: Battery Ventures
   📝 DataViz creates intuitive data visualization tools for enterprise customers. No-code platform allows business users to create dashboards without technical expertise.

3. **FinanceAI**
   📍 San Francisco, CA | 🏢 fintech | 💼 B2C
   💰 Series C - $25.0M
   👥 120 employees | 📈 $1200K MRR
   🏦 Lead: Tiger Global Management
   📝 FinanceAI provides AI-powered investment advisory services to retail investors. Uses machine learning to analyze market trends with over 100,000 active users.

4. **UrbanMobility**
   📍 New York, NY | 🏢 logistics | 💼 B2B2C
   💰 Series B - $15.0M
   👥 78 employees | 📈 $750K MRR
   🏦 Lead: Kleiner Perkins
   📝 UrbanMobility revolutionizes urban transportation through autonomous delivery drones and smart logistics hubs. Partners with major retailers for same-day delivery across Manhattan and Brooklyn.

5. **HealthTech Solutions**
   📍 Boston, MA | 🏢 healthcare | 💼 B2B
   💰 Series B - $18.0M
   👥 95 employees | 📈 $900K MRR
   🏦 Lead: General Catalyst
   📝 HealthTech Solutions develops medical devices and software for remote patient monitoring. Comprehensive telehealth platform reducing hospital readmissions by 30%.

✨  Done in 18.80s.</code></pre><p>전송된 입력에 대해 애플리케이션은 <strong>투자 중심</strong> 경로를 선택합니다. 그 결과, LangGraph 워크플로우에서 생성된 Elasticsearch 쿼리를 확인할 수 있으며, 이 쿼리는 사용자 입력에서 값과 범위를 추출합니다. 추출된 값이 적용된 쿼리가 Elasticsearch로 전송된 것을 볼 수 있으며, 마지막으로 <code>visualizeResults</code> 노드에 의해 형식이 지정된 결과를 확인할 수 있습니다.</p><p>이제 “샌프란시스코, 뉴욕 또는 보스턴에서 핀테크 및 헬스케어 스타트업을 찾아주세요”라는 쿼리를 사용하여 <strong>시장 중심</strong> 노드를 테스트해 보겠습니다.</p><pre><code>...

🔍 Query: Find fintech and healthcare startups in San Francisco, New York, or Boston

🤔 Search strategy: market_focused - The query is focused on finding fintech startups in San Francisco that are disrupting traditional banking and payment systems, which pertains to specific industries (fintech) and locations (San Francisco). Thus, a market-focused strategy is more appropriate.

🔍 Preparing MARKET-FOCUSED search parameters with market emphasis...

📋 Complete query: {
  "size": 5,
  "retriever": {
    "rrf": {
      "retrievers": [
        {
          "standard": {
            "query": {
              "semantic": {
                "field": "semantic_field",
                "query": "Find fintech and healthcare startups in San Francisco, New York, or Boston"
              }
            }
          }
        },
        {
          "standard": {
            "query": {
              "bool": {
                "filter": [
                  {
                    "terms": {
                      "industry": [
                        "fintech",
                        "healthcare"
                      ]
                    }
                  },
                  {
                    "terms": {
                      "location": [
                        "San Francisco, CA",
                        "New York, NY",
                        "Boston, MA"
                      ]
                    }
                  },
                  {
                    "terms": {
                      "business_model": []
                    }
                  }
                ]
              }
            }
          }
        }
      ],
      "rank_window_size": 50,
      "rank_constant": 10
    }
  }
}
🎯 Found 5 startups matching your criteria:

1. **FinanceAI**
   📍 San Francisco, CA | 🏢 fintech | 💼 B2C
   💰 Series C - $25.0M
   👥 120 employees | 📈 $1200K MRR
   🏦 Lead: Tiger Global Management
   📝 FinanceAI provides AI-powered investment advisory services to retail investors. Uses machine learning to analyze market trends with over 100,000 active users.

2. **CryptoWallet**
   📍 Miami, FL | 🏢 fintech | 💼 B2C
   💰 Series B - $16.0M
   👥 73 employees | 📈 $820K MRR
   🏦 Lead: Coinbase Ventures
   📝 CryptoWallet provides secure digital wallet solutions for cryptocurrency trading and storage. Multi-chain support with enterprise-grade security features.

...

✨  Done in 7.41s.</code></pre><h2>학습</h2><p>글을 쓰는 과정에서 배운 점은 다음과 같습니다.</p><ul><li>LLM에 필터의 정확한 값을 표시해야 합니다. 그렇지 않으면 사용자가 정확한 값을 입력해야 합니다. 카디널리티가 낮을 때는 이 방법이 괜찮지만, 카디널리티가 높을 때는 결과를 필터링할 메커니즘이 필요합니다.</li><li>검색 템플릿을 사용하면 LLM이 Elasticsearch 쿼리를 작성하게 하는 것보다 훨씬 더 일관되고 더 빠른 결과를 얻을 수 있습니다.</li><li>조건부 엣지는 여러 변형과 분기 경로를 갖춘 애플리케이션을 구축하는 강력한 메커니즘입니다.</li><li>정형 출력은 예측 가능하고 형식에 안전한 응답을 적용하므로 LLM으로 정보를 생성할 때 매우 유용합니다. 이렇게 하면 안정성이 향상되고 즉각적인 오해를 줄일 수 있습니다.</li></ul><p>하이브리드 검색을 통해 시맨틱 및 정형 검색을 결합하면 정확도와 컨텍스트 이해의 균형을 유지하면서 더 정확하고 관련성 높은 결과를 얻을 수 있습니다.</p><h2>결론</h2><p>이 예시에서는 LangGraph.js를 Elasticsearch와 결합하여 자연어 쿼리를 해석하고 금융 또는 시장 중심의 검색 전략을 선택할 수 있는 동적 워크플로우를 만듭니다. 이러한 접근 방식은 수동 쿼리의 복잡성을 줄이고 벤처 캐피털 분석가의 유연성과 정확성을 향상합니다.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[변수 제어를 통해 Kibana 대시보드 상호 작용성 향상]]></title>
            <description><![CDATA[Kibana 8.18+에서 변수 제어를 사용하여 Kibana 대시보드의 개별 시각화를 필터링하고 시간 간격을 조정하며 다양한 필드로 그룹화하는 방법을 알아보세요.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/kibana-dashboard-interactivity-variable-controls-overview</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/kibana-dashboard-interactivity-variable-controls-overview</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Teresa Alvarez Soler]]></dc:creator>
            <pubDate>Thu, 04 Dec 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/59aa3adffc8c759e42b961ef7d63719ce232893a-1348x830.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>이제 버전 8.18과 모든 9.x 시리즈부터 <strong>Kibana 대시보드에서 변수 제어를 사용할 수 있게 되어</strong> 기쁘게 생각합니다! 이 기능은 대시보드 사용자들 사이에서 가장 꾸준히 요청받은 추가 기능 중 하나였으며, 드디어 출시되었습니다 🎉 지난 몇 달간 <a href="https://www.elastic.co/docs/explore-analyze/dashboards/add-controls#add-variable-control">변수 제어</a>를 계속 확장하고 개선하였으며, 이제는 이를 위한 전용 블로그 포스팅을 만들게 되었습니다.</p><h2>변수 제어란 무엇인가요?</h2><p>이전에 Kibana 대시보드를 사용해 본 적이 있다면, 아마도 클래식 대시보드 제어를 알고 계실 것입니다. 데이터에서 값을 표시하여 몇 번의 클릭만으로 항목을 필터링할 수 있는 편리한 드롭다운입니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/b82c1b25a0b38661e5ce4552f763be487d5074aa-1600x701.png" style="width: 100%;" /><p>변수 제어는 겉보기에는 비슷해 보이지만, 독창적인 차별점이 있습니다. 대시보드의 모든 패널을 자동으로 필터링하는 대신, <a href="https://www.elastic.co/docs/explore-analyze/visualize/esorql">개별 시각화 내의 ES|QL 쿼리</a>에 직접 연결할 수 있습니다.</p><p>즉, <em>사용자</em>가 각 제어가 적용되는 위치를 결정할 수 있습니다. 더욱 좋은 점은, 시간 간격 조정, 분석 필드 전환, 시각화 매개변수 실시간 변경 등 다양한 창의적인 트릭에 활용할 수 있다는 것입니다. 기본적으로, 대시보드는 진정한 상호 작용 경험을 제공하여 인사이트를 더 빠르고 쉽게 얻을 수 있습니다.</p><h2>변수 제어 사용 사례</h2><p>좋습니다, 변수 제어는 유용한 것 같은데요. 그렇다면 실제로 무엇을 할 수 있을까요? 대시보드 수준을 높이는 몇 가지 예는 다음과 같습니다.</p><h3>선택한 시각화 필터링</h3><p><em>일부</em> 시각화는 필터링하고 다른 시각화는 그대로 두고 싶으신가요? 변수 제어를 사용하면 바로 그렇게 할 수 있습니다. 응답할 패널을 선택하고 시각화의 기반이 되는 ES|QL 쿼리에 연결하세요.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/efa367363830b03bc67028aceafe78c4b44e578f-1440x562.gif" style="width: 100%;" /><h3>다른 시간 간격을 선택하세요</h3><p>사용자에게 &#x27;5분&#x27;, &#x27;1시간&#x27;, &#x27;1일&#x27; 등 원하는 시간 버킷으로 전환할 수 있는 권한을 부여합니다. 미리 정의된 간격으로 변수 제어를 작성하고 시계열 쿼리에 연결합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/62aa9f4e728036f8c70213b76b1cf131f36f5b4d-1440x606.gif" style="width: 100%;" /><h3>함수 변경</h3><p>각 작업에 대해 여러 차트를 생성하는 대신, 대시보드 사용자가 최대값, 평균값, 다양한 백분위수 또는 다른 집계기를 선택할 수 있도록 합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/f6a2b4c73dc35efe462c2924a153d7b3fa3a7922-1436x606.gif" style="width: 100%;" /><h3>다양한 필드로 그룹화</h3><p>조사 중에는 데이터를 다양한 차원으로 세분화해야 할 때가 있습니다. 변수 제어를 통해 여러 &#x27;그룹별&#x27; 필드를 정의하고, 대시보드 사용자들이 자신만의 인사이트를 발견하는 데 가장 적합한 필드를 선택할 수 있게 할 수 있습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/fe8745a6fddccadba0666686b8ebc67fdaf64158-1438x606.gif" style="width: 100%;" /><h2>어떻게 만들 수 있나요?</h2><p>변수 제어를 만드는 가장 쉽게 (그리고 아마도 가장 즐겁게 만드는) 방법은 시각화의 <strong>ES|QL 쿼리 편집기</strong>에서 직접 만드는 것입니다. 쿼리를 입력하기 시작하고 자동 완성 메뉴를 사용하면 Kibana가 제어 구성을 훌륭히 도와줍니다.</p><p>하지만 변수 자체에서 시작하고 싶다면 <strong>패널 추가 → 제어 → 변수 제어</strong> 로 이동하여 제어를 만든 후 시각화에 변수를 추가할 수도 있습니다.</p><h3>예제 1: 다중 값 선택 기능을 갖춘 필터링 제어</h3><p>1. ES|QL 쿼리로 구동되는 시각화를 선택하고 WHERE 절에서 &#x27;제어 생성&#x27;을 클릭합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/46cb6f2a6775aee152d42eb5ee85170f1bdf26cb-1600x668.png" style="width: 100%;" /><p>2. 자동으로 변수 생성 플라이아웃으로 리디렉션되며, &#x27;쿼리에서 값 가져오기&#x27; 유형이 선택되고 변수 이름이 이미 입력되어 있습니다. 시각화 쿼리에서 작동하려면 컨트롤의 이름이 항상 &#x27;?...&#x27;로 시작해야 합니다.</p><p>대시보드에서 선택된 시간 범위에 따라 필드의 값을 가져오고 업데이트하려면 보통 이런 쿼리가 필요합니다.</p><pre><code>FROM <datasource_name>
| WHERE @timestamp <=?_tend and @timestamp >?_tstart
| STATS BY <field_name></code></pre><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/a2a72d4e3159923c6207908da9b4172e27cd5f81-1600x716.png" style="width: 100%;" /><p>3. 제어를 저장하면 대시보드 상단에 제어가 나타나고, 시각화 쿼리가 변수 제어 이름으로 업데이트됩니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/5fc434c8951889e9769652b675191711d126a685-1600x653.png" style="width: 100%;" /><p>4. 제어에 <a href="https://www.elastic.co/docs/explore-analyze/dashboards/add-controls#esql-multi-values-controls">다중 값 선택</a>을 추가하려면 쿼리에서 <code>MV_CONTAINS</code> 함수를 사용하고 2단계에서 제어 생성 시 &#x27;다중 선택 허용&#x27;을 선택해야 합니다(9.3부터 사용 가능).</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/1f237cea0a37cb25a7917a2a683707a269adae8e-1600x670.png" style="width: 100%;" /><h3>예제 2: 시간 간격 제어</h3><p>시계열을 구축하는 경우, 날짜 히스토그램 간격에 대한 변수 제어를 쉽게 추가할 수 있습니다.</p><p>1. 시계열에 대한 ES|QL 쿼리를 작성할 때 &#x27;제어 생성&#x27;을 클릭합니다. 간격 변수를 만들 때는 <code>BUCKET</code> 대신 <code>TBUCKET</code> 을(를) 사용하는 것이 좋습니다. &#x27;1시간&#x27;, &#x27;1일&#x27;과 같은 더 읽기 쉬운 간격을 허용하기 때문입니다. <code>TBUCKET</code> 에 곧 자동 옵션이 추가되어 시간 범위에 자동으로 적응할 수 있게 됩니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/b0ad53d790ff9bdd42db5e77477318319f423534-1600x664.png" style="width: 100%;" /><p>2. 드롭다운 메뉴에서 옵션을 채울 간격을 정의합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/f3bd83f530cfa4698c1a3b1ae60d08d0414043b5-1600x757.png" style="width: 100%;" /><p>3. 드롭다운 메뉴에서 다른 간격을 선택하고 시각화가 어떻게 변하는지 확인합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/0f928d9c70929f64926e065059188d140cd48943-1600x671.png" style="width: 100%;" /><h3>예제 3: 함수용 변수</h3><ol><li>&#x27;정적 값&#x27; 유형의 제어를 사용하여 변수를 생성하고 드롭다운 값에 함수 이름을 추가합니다. 함수를 대체하려면 &#x27;??...&#x27;로 시작하는 변수 이름을 사용하는 것이 중요합니다.</li></ol><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/531444237b7e152d3c8a6f3ca7e464f954f9e856-1600x663.png" style="width: 100%;" /><p>2. ES|QL 쿼리에 변수 이름을 포함합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/9858442abb26d8d266d464852871b139fde63b89-1600x665.png" style="width: 100%;" /><h3>예제 4: 필드를 위한 변수</h3><ol><li>&#x27;정적 값&#x27; 유형의 제어를 사용하여 원하는 필드 이름을 작성할 수 있습니다. 필드에서 작동하려면 &#x27;??...&#x27;로 시작하는 변수 이름을 사용하는 것이 중요합니다.</li></ol><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/33534c3df2fae024b25c28b4aed5d742e54202a2-1600x710.png" style="width: 100%;" /><p>2. 시각화 쿼리에서 원하는 위치에 변수를 참조합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/71cdf3e9df72c59d957628a3aa6e4aa9bd60d6d5-1600x676.png" style="width: 100%;" /><h2>Discover의 변수 제어</h2><p>변수 컨트롤은 단순한 대시보드 기능이 아닙니다. Discover의 ES|QL 편집기에서도 직접 사용할 수 있습니다. Discover에서 더 빠른 데이터 탐색 경험을 위한 제어를 생성하고, 이를 대시보드로 가져올 수 있으며, 그 반대로도 가능합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/fdddeec902d0bc746caed9276d01d7d48793dd85-1600x709.png" style="width: 100%;" /><h2>기술적 세부 사항</h2><p>이제 변수 제어에는 쿼리에서 참조할 수 있는 부분과 사용해야 하는 명명 접두사(값의 경우 &quot;?...&quot;, 필드 또는 함수의 경우 &quot;??...&quot;)와 같은 몇 가지 규칙이 있다는 것을 눈치채셨을 것입니다. 변수는 단순히 클라이언트 측에서 스트링 치환만 하는 것이 아니기 때문입니다. 실제로는 쿼리 언어 자체에서 1급 시민으로 처리됩니다(<a href="https://www.elastic.co/docs/solutions/search/agent-builder/tools/esql-tools#parameter-types">ES|QL에서 매개변수</a>로 알려짐).</p><p></p><p>이 디자인은 몇 가지 큰 장점을 제공합니다. 첫째, Kibana는 각 변수의 맥락을 이해할 수 있어, 자동으로 구성 설정을 생성하고 미리 채울 수 있습니다. 또한 훨씬 더 안전합니다. 언어가 변수 입력의 유효성을 엄격하게 검사하여 악의적인 주입을 방지하고, 이상이 있을 경우 우아하게 오류를 처리합니다. 또한 복잡한 유효성 검사 및 오류 처리를 클라이언트가 아닌 서버로 전환하여 성능과 안정성을 향상합니다. 성능에 대한 참고 사항을 말씀드리자면, 빠른 쿼리는 대시보드보다 먼저 로드되기 대문에 쿼리가 느리면 전체 대시보드 성능에 영향을 미칠 수 있으므로 빠른 쿼리를 포함하는 변수를 작성하는 것이 가장 좋습니다.</p><p>물론, 이 아키텍처에는 현재로서 몇 가지 <a href="https://www.elastic.co/docs/solutions/search/agent-builder/limitations-known-issues#esql-limitations">제약</a>이 있습니다. 변수는 아직 필터링에 &#x27;모두&#x27; 옵션을 지원하지 않으며, 현재 <code>LIKE</code><strong> </strong>또는 <code>FROM</code> (데이터 소스 전환용)과 같은 특정 연산자와 함께 사용할 수 없습니다. 좋은 소식은? 이러한 기능을 추가하기 위해 열심히 노력하고 있습니다.</p><h2>제어의 미래가 가져올 변화</h2><p>여기서 멈추지 않습니다! 주목하고 있는 개선 사항 일부는 다음과 같습니다.</p><p>✨ 대시보드 어디에나 컨트롤을 배치할 수 있는 기능</p><p>✨ 제어 체인 연결 - 한 제어의 출력이 다음 제어의 입력이 되는 것</p><p>✨ 변수에 대한 &#x27;모든&#x27; 선택과 같은 더 나은 선택 옵션</p><p>✨ 새로운 제어 유형(검색 유형 제어 및 데이터 소스용 변수)</p><p>✨ 그리고 많은 분들이 요청했던 삶의 질을 향상하는 추가 기능(예: 일반 제어 사전 필터링)</p><p>아이디어나 피드백이 있으시면 언제든지 알려주시기 바랍니다.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Elasticsearch에서 NVIDIA cuVS를 활용하여 벡터 색인화 속도 최대 12배 향상: GPU 가속화 챕터 2]]></title>
            <description><![CDATA[Elasticsearch가 GPU 가속 벡터 색인화와 NVIDIA cuVS로 어떻게 거의 12배 더 높은 색인화 처리량을 달성하는지 알아보세요.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/elasticsearch-gpu-accelerated-vector-indexing-nvidia</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/elasticsearch-gpu-accelerated-vector-indexing-nvidia</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Chris Hegarty, Hemant Malik, Corey Nolet, Manas Singh, Mithun Radhakrishnan, Mayya Sharipova, Lorenzo Dematte, Ben Frederickson]]></dc:creator>
            <pubDate>Wed, 03 Dec 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/08f7469a4daaf67b7c5999585aae179b6680c78d-896x746.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>올해 초 Elastic은 NVIDIA와의 <a href="https://ir.elastic.co/news/news-details/2025/Elastic-Brings-Enterprise-Data-to-NVIDIA-AI-Factories/default.aspx">협업</a>을 통해 Elasticsearch에 GPU 가속화를 도입하여 <a href="https://developer.nvidia.com/cuvs">NVIDIA cuVS</a>와 통합한다고 발표했으며, <a href="https://www.nvidia.com/en-us/on-demand/session/gtc25-S71286/">NVIDIA GTC의 세션</a>과 다양한 <a href="https://www.elastic.co/search-labs/blog/gpu-accelerated-vector-search-elasticsearch-nvidia">블로그</a>에 자세히 설명되어 있습니다. 이 게시물은 NVIDIA 벡터 검색 팀과의 공동 엔지니어링 활동에 대한 업데이트입니다.</p><h2>요약</h2><p>먼저 현황을 간략하게 설명하겠습니다. Elasticsearch는 강력한 벡터 데이터베이스로서, 풍부한 기능과 대규모 유사성 검색을 위한 뛰어난 성능을 제공하며 자리매김했습니다. 스칼라 양자화, Better Binary Quantization(<a href="https://www.elastic.co/search-labs/blog/better-binary-quantization-lucene-elasticsearch">BBQ</a>), <a href="https://www.elastic.co/blog/accelerating-vector-search-simd-instructions">SIMD</a> 벡터 연산, 그리고 <a href="https://www.elastic.co/search-labs/blog/diskbbq-elasticsearch-introduction">DiskBBQ</a>와 같은 디스크 효율적인 알고리즘을 통해 벡터 워크로드를 효율적이고 유연하게 관리할 수 있는 다양한 옵션을 제공합니다.</p><p>NVIDIA cuVS를 벡터 검색 작업을 위한 호출 가능한 모듈로 통합함으로써 벡터 색인화 성능과 효율성을 크게 향상해 대규모 벡터 워크로드를 보다 효과적으로 지원하는 것을 목표로 합니다.</p><h2>당면 과제</h2><p>고성능 벡터 데이터베이스를 구축하는 데 있어 매우 어려운 과제 중 하나는 벡터 인덱스, 즉 <a href="https://arxiv.org/abs/1603.09320">HNSW</a> 그래프를 구성하는 것입니다. 인덱스 구축은 각 벡터가 다른 수많은 벡터와 비교되기 때문에 수백만, 심지어 수십억 개의 산술 연산으로 빠르게 진행됩니다. 또한 압축 및 병합과 같은 인덱스 수명 주기 작업은 색인화의 전체 컴퓨팅 오버헤드를 더 증가시킬 수 있습니다. 데이터 볼륨과 관련 벡터 임베딩이 기하급수적으로 증가함에 따라 대규모 병렬 처리와 높은 처리량의 연산을 위해 구축된 가속 컴퓨팅 GPU는 이러한 워크로드를 처리하는 데 최적의 위치에 있습니다.</p><h2>Elasticsearch-GPU 플러그인 입력</h2><p><a href="https://developer.nvidia.com/cuvs">NVIDIA cuVS</a>는 GPU 가속 벡터 검색 및 데이터 클러스터링을 위한 오픈 소스 CUDA-X 라이브러리로, AI 및 추천 워크로드를 위한 빠른 인덱스 구축 및 임베딩 검색을 가능하게 합니다.</p><p>Elasticsearch는 커뮤니티가 개발하고 NVIDIA가 관리하는 오픈 소스 라이브러리인 <a href="https://mvnrepository.com/artifact/com.nvidia.cuvs/cuvs-java">cuvs-java</a>를 통해 cuVS를 사용합니다. cuvs-java 라이브러리는 경량이며, <a href="https://docs.rapids.ai/api/cuvs/nightly/c_api/">cuVS C API</a>를 기반으로 <a href="https://openjdk.org/projects/panama/">Panama</a> Foreign Function을 사용하여 cuVS 기능을 관용적인 Java 방식으로 노출하면서도 최신 기술과 뛰어난 성능을 유지합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/5f6daa1eb07f704a6707d9e6b7ccb81d0abaa8c9-566x419.png" style="width: 100%;" /><p>cuvs-java 라이브러리는 <a href="https://github.com/elastic/elasticsearch/pull/135545">새로운 Elasticsearch 플러그인</a>에 통합되어 있습니다. 따라서 GPU에서의 벡터 색인화는 동일한 Elasticsearch 노드와 프로세스에서 외부 코드나 하드웨어를 프로비저닝할 필요 없이 수행할 수 있습니다. 인덱스 구축 중에 cuVS 라이브러리가 설치되고 GPU가 존재하며 구성된 경우 Elasticsearch는 GPU를 사용하여 벡터 색인화 프로세스를 가속화합니다. 벡터는 GPU에 전달되어 <a href="https://arxiv.org/abs/2308.15136">CAGRA</a> 그래프를 구성합니다. 이 그래프는 HNSW 형식으로 변환되어 CPU에서 벡터를 즉시 검색할 수 있게 됩니다. 구축된 그래프의 최종 형식은 CPU에서 구축되는 것과 동일합니다. 이를 통해 Elasticsearch는 기본 하드웨어가 지원할 경우 높은 처리량의 벡터 색인화에 GPU를 활용할 수 있으며, 동시에 CPU 자원을 다른 작업(동시 검색, 데이터 처리 등)에 사용할 수 있습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/3ea255bd9bfd7983f78143c5eba999d2149d72be-671x356.png" style="width: 100%;" /><h2>인덱스 구축 가속화</h2><p>Elasticsearch에 GPU 가속화를 통합하는 과정에서 cuvs-java에 여러 가지 개선 사항이 적용되었으며, 특히 효율적인 데이터 입출력 및 함수 호출에 중점을 두었습니다. 핵심적인 개선 사항 중 하나는 <a href="https://github.com/rapidsai/cuvs/blob/2cf5fa7666d703dccbe655f8214656b0952bb69b/java/cuvs-java/src/main/java/com/nvidia/cuvs/CuVSMatrix.java">cuVSMatrix</a>를 사용하여 Java 힙, 오프힙 또는 GPU 메모리에 저장된 벡터를 투명하게 모델링하는 것입니다. 이를 통해 데이터가 메모리와 GPU 간에 효율적으로 이동하여 수십억 개의 벡터를 불필요하게 복사하는 것을 방지할 수 있습니다.</p><p>이러한 기본 제로 카피 추상화 덕분에 GPU 메모리로의 전송과 그래프 검색이 모두 직접 이루어질 수 있습니다. 색인화하는 동안 벡터는 먼저 Java 힙의 메모리에 버퍼링된 다음 GPU로 전송되어 CAGRA 그래프를 구성합니다. 그 후 그래프는 GPU에서 검색되어 HNSW 형식으로 변환된 후 디스크에 유지됩니다.</p><p>병합 시점에 벡터는 이미 디스크에 저장되어 있으므로 Java 힙을 완전히 우회합니다. 인덱스 파일은 메모리 맵핑 방식으로 처리되며 데이터는 GPU 메모리로 직접 전송됩니다. 또한 이 설계는 float32 또는 int8과 같은 다양한 비트 폭을 쉽게 수용할 수 있으며 다른 양자화 방식으로도 자연스럽게 확장됩니다.</p><h2>자, 그럼 성능은 어떨까요?</h2><p>수치를 살펴보기 전에, 약간의 배경 설명이 필요합니다. Elasticsearch에서 세그먼트 병합은 보통 색인 과정 중 백그라운드에서 자동으로 실행되기 때문에, 이를 단독으로 분리해 벤치마크하기가 어렵습니다. 재현 가능한 결과를 얻기 위해, 이번에는 강제 병합을 사용하여 통제된 실험 환경에서 세그먼트 병합을 명시적으로 트리거했습니다. 강제 병합은 백그라운드 병합과 동일한 기본 병합 작업을 수행하므로, 실제 색인 워크로드에서는 정확한 향상 폭이 다를 수 있지만, 성능 개선 효과를 가늠하는 유용한 지표로 볼 수 있습니다.</p><p>이제 수치를 확인해 보겠습니다.</p><p>초기 벤치마크 결과는 매우 희망적입니다. 로컬로 연결된 NVMe 저장 공간이 있는 AWS <code>g6.4xlarge</code> 인스턴스에서 벤치마크를 실행했습니다. Elasticsearch의 단일 노드는 기본 최적의 색인화 스레드 수(8개 - 물리적 코어당 1개)를 사용하고 <a href="https://www.elastic.co/docs/reference/elasticsearch/index-settings/merge">병합 스로틀링</a>을 비활성화하도록 구성되었습니다(빠른 NVMe 디스크에는 적용하기 어렵습니다).</p><p>데이터 세트에는 <a href="https://github.com/elastic/rally-tracks/blob/master/openai_vector/README.md">OpenAI Rally 벡터 트랙</a>에서 가져온 1,536차원의 260만 개 벡터를 사용했으며, <a href="https://github.com/elastic/elasticsearch/pull/137072">base64 스트링</a>으로 인코딩하고 float32 <em>hnsw</em>로 색인화했습니다. 모든 시나리오에서 구성된 그래프는 최대 95%의 재현율을 달성했습니다. 결과는 다음과 같습니다.</p><ul><li><strong>색인화 처리량:</strong> 인메모리 버퍼 플러시 중 그래프 구성을 GPU로 이동하여 처리량이 약 12배 증가합니다.</li><li><strong>강제 병합:</strong> 색인화가 완료된 후에도 GPU는 세그먼트 병합을 계속 가속화하여 강제 병합 단계의 속도를 최대 7배까지 높입니다.</li></ul><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/f60ea9ee5996e456f393ffd195ee7eada6e5a7c2-948x387.png" style="width: 100%;" /><ul><li><strong>CPU 사용량:</strong> 그래프 구성을 GPU로 오프로드하면 평균 및 최대 CPU 사용량이 많이 감소합니다. 아래 그래프는 색인화 및 병합 중 CPU 사용량을 보여주며, 이러한 작업이 GPU에서 실행될 때 GPU 작업량이 얼마나 낮아지는지를 강조합니다. GPU 색인화 중 CPU 사용률이 낮아지면 CPU 사이클이 확보되어 검색 성능 향상에 활용할 수 있습니다.</li></ul><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/5e680a5fc41700a877f3d8b2e5ce18ebd3f37a0b-1600x562.png" style="width: 100%;" /><ul><li><strong>재현율:</strong> CPU와 GPU 실행 간의 정확도는 사실상 동일하지만, GPU로 구축된 그래프는 재현율이 약간 더 높습니다.</li></ul><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/48a2b7758606bd321712b7d8378cd2640e652a4e-1384x544.png" style="width: 100%;" /><h2>가격 측면에서 비교</h2><p>앞선 비교에서는 의도적으로 동일한 하드웨어를 사용했으며 유일한 차이점은 색인 과정에서 GPU를 사용했는지 여부였습니다. 이 설정은 순수 연산 성능의 영향을 분리하여 파악하는 데 유용하지만 비용 관점에서의 비교도 가능합니다.</p><p>GPU 가속 구성과 거의 동일한 시간당 요금으로 CPU 전용 구성을 프로비저닝할 수 있습니다. 이 경우 비교 가능한 CPU 및 메모리 리소스가 약 두 배(32 vCPU(AMD EPYC), 64GB RAM)로 제공되며 색인 스레드 수를 16개로 두 배 늘릴 수 있습니다.</p><p>공정하고 일관된 비교를 위해 GPU를 명시적으로 비활성화한 상태로 AWS g6.8xlarge 인스턴스에서 CPU 전용 실험을 수행했습니다. 이를 통해 다른 모든 하드웨어 특성을 동일하게 유지하면서 GPU 가속과 CPU 전용 색인 간의 비용 대비 성능 트레이드오프를 평가할 수 있었습니다.</p><p>예상대로 더 강력한 CPU 인스턴스는 위 섹션의 벤치마크와 비교했을 때 향상된 성능을 보여줍니다. 그러나 이 더 강력한 CPU 인스턴스를 원래의 GPU 가속 결과와 비교해 보면 GPU는 여전히 상당한 성능 향상을 제공합니다. 색인화 처리량에서 <strong>약 5배</strong>, 강제 병합에서 <strong>약 6배</strong>의 성능 향상을 보이며, 동시에 최대 <strong>95%</strong>의 재현율을 달성하는 그래프를 구축할 수 있습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/8ffa58cae3ad175ef2932a351aeef4c34a1407b9-948x394.png" style="width: 100%;" /><h2>결론</h2><p>엔드 투 엔드 시나리오에서 NVIDIA cuVS를 사용한 GPU 가속화는 색인화 처리량을 거의 12배 향상하고 강제 병합 지연 시간을 7배 단축하는 동시에 CPU 사용률을 크게 낮춥니다. 이는 벡터 색인화 및 병합 워크로드가 GPU 가속화를 통해 상당한 성능 향상을 얻을 수 있음을 보여줍니다. 비용을 고려한 비교에서도 GPU 가속화는 색인화 처리량을 약 5배, 강제 병합 작업 속도를 약 6배 향상하는 등 상당한 성능 향상을 제공합니다.</p><p>GPU 가속 벡터 색인화는 현재 Elasticsearch 9.3의 기술 미리 보기로 계획되어 있으며, 2026년 초에 출시될 예정입니다.</p><p>더 많은 소식을 기대해 주세요.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[ES|QL 9.2: 스마트한 LOOKUP JOIN 및 시계열 지원]]></title>
            <description><![CDATA[Elasticsearch 9.2에서 ES|QL에 대한 세 가지 개별 업데이트, 즉 보다 표현력 있는 데이터 상관관계를 위한 향상된 LOOKUP JOIN, 시계열 분석을 위한 새로운 TS 명령, 집계를 위한 유연한 INLINE STATS 명령을 살펴보세요.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/esql-elasticsearch-9-2-multi-field-joins-ts-command</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/esql-elasticsearch-9-2-multi-field-joins-ts-command</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Tyler Perkins, Kostas Krikellas, Julian Kiryakov]]></dc:creator>
            <pubDate>Tue, 02 Dec 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/415894e21e7758c907d6e60d4efc94230349beef-2012x1164.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>10월에 출시된 Elasticsearch 9.2는 데이터를 그 어느 때보다 더 빠르고 유연하며 쉽게 분석할 수 있도록 중요한 개선 사항이 반영되었습니다. 이번 릴리즈의 핵심은 최종 사용자에게 더 큰 가치를 직접 제공하도록 설계된 파이프 쿼리 언어인 ES|QL의 중요한 기능 향상입니다.</p><p>ES|QL을 통해 데이터 분석 워크플로우를 혁신할 Elasticsearch 9.2의 기능을 살펴보겠습니다.</p><h2>데이터 상관 관계 혁신: 더 스마트하고 빠르며 유연한 조회 조인</h2><p>ES|QL의 <a href="https://www.elastic.co/docs/reference/query-languages/esql/commands/lookup-join">LOOKUP JOIN</a> 명령은 Elasticsearch 9.2에서 상당한 변화를 거쳐 효율성과 활용도가 크게 향상되었습니다. LOOKUP JOIN은 ES|QL 쿼리 결과 테이블의 데이터를 지정된 조회 모드 인덱스의 일치하는 레코드와 결합합니다. 조인 필드의 일치하는 값을 기준으로 조회 인덱스의 필드를 결과 테이블에 새로운 열로 추가합니다. 이전에는 데이터 조인이 단일 필드와 단순 동등성으로 제한되었지만, 이제는 그렇지 않습니다. 이러한 향상된 기능을 통해 복잡한 데이터 상관관계 시나리오를 손쉽게 처리할 수 있습니다.</p><p><strong>Lookup Join의 주요 개선 사항은 다음과 같습니다:</strong></p><ul><li><strong>다중 필드 조인:</strong> 여러 필드를 쉽게 조인할 수 있습니다. 예를 들어 <code>application_logs</code> 와(과)<code>service_registry</code> 을(를)<code>service_name</code>, <code>environment</code>, 버전을 기준으로 조인하려면 다음과 같이 합니다. <code>version:</code></li></ul><pre><code>FROM application_logs
| LOOKUP JOIN service_registry ON service_name, environment, version</code></pre><ul><li><strong>표현식으로 복잡한 조인 술어 활용(기술 미리 보기):</strong></li></ul><p>더 이상 단순한 등식에 국한되지 않습니다. LOOKUP JOIN을 사용하면 상관관계에 대한 <strong>여러 기준</strong>을 지정하고 ==, !=, &lt;, &gt;, &lt;=, &gt;= 등 다양한 <strong>이항 연산자</strong>를 사용할 수 있습니다. 즉, 고도로 정교한 조인 조건을 생성하여 데이터에 대해 훨씬 더 정교한 질문을 던질 수 있습니다.</p><p>예시 1: 서비스별 SLA 임계값을 사용하여 애플리케이션 메트릭 찾기</p><pre><code>FROM application_metrics
| LOOKUP JOIN sla_thresholds
      ON service_name == sla_service AND response_time > sla_response_time</code></pre><p>예시 2: 이 쿼리는 시간에 따라 변동하는 지역별 가격 정책을 기반으로 납부 금액을 계산합니다. 복잡한 날짜 범위와 등식 조건을 기반으로 세 개의 데이터 세트를 결합하여 최종 <code>due_amount</code>을(를) 계산합니다. 두 번째 LOOKUP JOIN은 <code>meter_readings</code> 인덱스의 <code>measurement_date</code> 필드와 <code>customers</code> 인덱스의 <code>region_id</code> 필드를 사용하여 <code>pricing_policies</code> 인덱스에 조인하고 특정 <code>region</code> 및 <code>measurement_date</code>에 대한 올바른 가격 정책을 찾습니다.</p><pre><code>FROM meter_readings
| LOOKUP JOIN customers
      ON meter_id
| LOOKUP JOIN pricing_policies
      ON
        region_id == region AND
          measurement_date >= policy_begin_date AND
          measurement_date < policy_end_date
| EVAL due_amount = (kwh_consumed * rate_per_kwh + base_charge) * (1 + tax_rate)
| EVAL period = policy_name
| KEEP customer_name, period, due_amount, measurement_date, kwh_consumed,
    rate_per_kwh, base_charge, tax_rate
| SORT measurement_date</code></pre><ul><li><strong>필터링된 조인의 대규모 성능 향상 </strong></li></ul><p>조회 테이블 조건을 사용하여 필터링되는 &#x27;확장 조인&#x27;의 성능을 개선했습니다. 확장 조인은 입력 행당 여러 개의 일치 항목을 생성하여 큰 중간 결과 세트를 생성할 수 있습니다. 이러한 행 중 상당수가 후속 필터에 의해 삭제되는 경우 상황은 더 악화됩니다. 9.2에서는 조회 데이터에 필터를 적용할 때 불필요한 행을 필터링하여 삭제될 행의 처리를 방지함으로써 이러한 조인을 최적화합니다. 때에 따라 이러한 조인은 최대 <strong>1,000배 더 빨라질</strong> 수 있습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/002c014ee29b1aaf9ddeb8c554bb76efe3ed180c-1572x954.png" style="width: 100%;" /><p>이러한 최적화는 조회가 초기에 많은 잠재적 일치 항목을 생성할 수 있는 &#x27;확장 조인&#x27;을 처리할 때 매우 중요합니다. 지능적으로 필터를 적용하면 관련 데이터만 처리되어 쿼리 실행 시간이 크게 단축되고 방대한 데이터 세트에 대한 실시간 분석이 가능해집니다. 즉, 매우 크거나 복잡한 조인 작업에서도 훨씬 빠르게 인사이트를 얻을 수 있습니다.</p><p><strong>LOOKUP JOIN 클러스터 간 검색(CCS) 호환성:</strong></p><p>LOOKUP JOIN이 8.19와 9.1 버전에서 정식 출시되었을 당시에는 클러스터 간 검색(CCS) 지원이 부족했습니다. 여러 클러스터에서 운영되는 조직의 경우 LOOKUP JOIN은 이제 9.2에서 CCS와 완벽하게 통합됩니다. 조인을 수행하려는 모든 원격 클러스터에 조회 인덱스를 추가하기만 하면 ES|QL이 자동으로 이러한 원격 조회 인덱스를 활용하여 원격 데이터와 조인합니다. 이를 통해 분산 데이터 분석이 간소화되고 전체 Elasticsearch 배포 환경에서 일관된 데이터 보강이 보장됩니다.</p><p>이러한 개선 사항을 통해 전례 없는 정밀도, 속도, 용이성으로 다양한 데이터 세트를 상호 연관시켜 복잡한 해결 방법이나 사전 처리 단계 없이 더 깊고 실행 가능한 인사이트를 발견할 수 있습니다.</p><h2>조회 인덱스용 Kibana Discover UX를 사용해 간편하게 데이터 보강</h2><p>데이터 보강은 간단해야 하며, 장애물이 되어서는 안 됩니다. Kibana의 Discover에서 조회 인덱스를 생성하고 관리하기 위해 새롭고 환상적인 사용자 환경을 도입했습니다.</p><p><strong>직관적인 워크플로우:</strong> Discover의 포괄적인 자동 완성 기능은 ES|QL 편집기에서 조회 인덱스와 조인 필드를 제안하여 프로세스를 안내해 주므로, 업로드한 데이터를 기존 인덱스와 매우 쉽게 연결할 수 있습니다. 존재하지 않는 조회 인덱스 이름을 입력하면 클릭 한 번으로 조회 편집기에 바로 액세스하여 인덱스를 생성할 수 있습니다. 기존 조회 인덱스 이름을 입력하면 다음과 같이 편집 옵션을 제안해 드립니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/d36fd6ffd6bc0bf8d31067f6266445c68d15c71c-1400x184.png" style="width: 100%;" /><p><strong>인라인 관리(CRUD):</strong> Discover에서 직접 인라인 편집 기능(생성, 읽기, 업데이트, 삭제)을 사용하여 참조 데이터 세트를 최신 상태로 유지하세요.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/a0e4aa66540b1f725c24ccb0519d978415073bb6-1453x842.png" style="width: 100%;" /><p><strong>간편한 파일 업로드: </strong>이제 CSV와 같은 파일을 Discover 내에서 직접 업로드하고 <code>LOOKUP JOIN</code>에서 즉시 사용할 수 있습니다. Kibana의 다른 영역을 오가며 컨텍스트를 전환할 필요가 없습니다!</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/0f5dc2c712af4c4cada50292a7c8b836eb02aa67-1600x748.png" style="width: 100%;" /><p>사용자 ID를 이름에 매핑하거나 비즈니스 메타데이터를 추가하거나 정적 참조 파일을 조인할 때, 이 기능은 데이터 보강을 대중화하고 모든 사용자가 조인 기능을 직접 사용할 수 있도록 합니다. 빠르고 간편하며 모든 것이 한 곳에서 가능합니다.</p><h2>컨텍스트 보존: INLINE STATS 소개 (기술 미리 보기)</h2><p>데이터 집계는 중요하지만, 때로는 집계된 데이터를 원본 데이터와 <em>함께</em> 확인해야 할 때가 있습니다. <strong>기술 미리 보기</strong> 기능으로 <a href="https://www.elastic.co/docs/reference/query-languages/esql/commands/inlinestats-by">인라인 통계</a>를 소개하게 되어 기쁩니다.</p><p><code>STATS</code> 명령은 입력 필드를 집계된 출력으로 대체하지만, <code>INLINE STATS</code> 명령은 원래 입력 필드를 그대로 유지하고 집계된 새 필드를 추가합니다. 이는 집계 <em>후</em> 원래 입력 필드에서 추가 작업을 수행할 수 있게 하여 보다 지속적이고 유연한 워크플로우를 제공합니다.</p><p>예를 들어 개별 비행 행을 유지하면서 평균 비행 거리를 계산하려면 다음을 수행합니다.</p><pre><code>FROM kibana_sample_data_flights
 | KEEP Carrier, Dest, DistanceMiles
 | INLINE STATS avgDist = ROUND(AVG(DistanceMiles))
       BY Dest
 | WHERE DistanceMiles > avgDist</code></pre><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/6eea943035e0ab371270084c504a06bb89f8b82b-1496x290.png" style="width: 100%;" /><p>이 쿼리에서는 그룹화 기준으로 사용된 해당 <code>Dest</code>(ination)을(를) 사용하여 각 행에 <code>avgDist</code>을(를) 추가한 다음, 항공편 정보 열이 남아 있으므로 평균보다 거리가 먼 항공편으로 결과를 필터링할 수 있습니다.</p><h2>ES|QL의 시계열 지원 (기술 미리 보기)</h2><p>Elasticsearch는 <a href="https://www.elastic.co/docs/manage-data/data-store/data-streams/time-series-data-stream-tsds">시계열 데이터 스트림</a>을 사용하여 메트릭을 저장합니다. <a href="https://www.elastic.co/docs/reference/query-languages/esql/commands/ts"><code>TS</code></a> 소스 명령을 통해 ES|QL에서 시계열 집계에 대한 지원을 추가하고 있습니다. 이 기능은 Elastic Cloud Serverless 및 9.2 기본 버전에서 기술 미리 보기로 제공됩니다.</p><p>시계열 분석은 주로 시간 버킷에 대한 메트릭 값을 하나 이상의 필터링 차원으로 분할하여 요약하는 집계 쿼리를 기반으로 합니다. 대부분의 집계 쿼리는 2단계 처리 방식을 사용하는데, (a) 시계열별 값을 요약하는 내부 집계 함수와 (b) 시계열 전반에 걸쳐 (a)의 결과를 결합하는 외부 집계 함수가 있습니다.</p><p><a href="https://www.elastic.co/docs/reference/query-languages/esql/commands/ts"><code>TS</code></a> 소스 명령어와 <a href="https://www.elastic.co/docs/reference/query-languages/esql/commands/stats-by"><code>STATS</code></a>을(를) 결합하면 시계열 데이터에 대한 쿼리를 간결하면서도 효과적으로 표현할 수 있습니다. 좀 더 구체적으로 호스팅 및 시간당 총 요청 비율을 계산하는 다음 예시를 살펴보겠습니다.</p><pre><code>TS my_metrics
| WHERE @timestamp > NOW() - 1 day
| STATS SUM(RATE(requests))
      BY host, TBUCKET(1h)</code></pre><p>이 경우 시계열 집계 함수 <code>RATE</code>이(가) 먼저 시계열 및 시간별로 평가됩니다. 그런 다음 <code>SUM</code>을(를) 사용하여 생성된 부분 집계를 결합하여 호스팅 및 시간당 최종 집계 값을 계산합니다.</p><p>사용 가능한 시계열 집계 함수 목록은 <a href="https://www.elastic.co/docs/reference/query-languages/esql/functions-operators/time-series-aggregation-functions">여기</a>에서 확인하세요. <a href="https://www.elastic.co/docs/manage-data/data-store/data-streams/time-series-data-stream-tsds#time-series-metric">카운터</a> 비율이 이제 지원되며, 이는 카운터 처리를 위한 가장 중요한 집계 함수라고 할 수 있습니다.</p><p><code>TS</code> 소스 명령은 <code>STATS</code>와(과) 결합하여 사용할 수 있도록 설계되었으며, 시계열 집계를 효율적으로 지원하도록 실행이 최적화되었습니다. 예를 들어 데이터는 <code>STATS</code>(으)로 이동하기 전에 정렬됩니다. <a href="https://www.elastic.co/docs/reference/query-languages/esql/commands/fork"><code>FORK</code></a> 또는 <a href="https://www.elastic.co/docs/reference/query-languages/esql/commands/inlinestats-by"><code>INLINE STATS</code></a>와(과) 같은 시계열 데이터 또는 그 순서를 보강하거나 변경할 수 있는 처리 명령은 현재 <code>TS</code>와(과) <code>STATS</code> 사이에 허용되지 않습니다. 이 제한은 향후 해제될 수 있습니다.</p><p><code>STATS</code> 표 형식 출력은 해당 명령을 사용하여 추가로 처리할 수 있습니다. 예를 들어 다음 쿼리는 호스팅 및 시간당 평균 <code>cpu_usage</code>와(과) 호스팅당 최대값의 비율을 계산합니다.</p><pre><code>TS my_metrics
| STATS avg_usage = AVG(AVG_OVER_TIME(cpu_usage))
      BY host, time_bucket = TBUCKET(1h)
| INLINE STATS max_avg_usage = MAX(avg_usage)
      BY host
| EVAL ratio = avg_usage / max_avg_usage
| KEEP host, time_bucket, ratio
| SORT host, time_bucket DESC</code></pre><p>시계열 데이터는 Lucene doc 값으로 구동되는 기본 열 형식 저장 공간 엔진에 저장됩니다. TS 명령은 ES|QL 컴퓨팅 엔진을 통해 벡터화된 쿼리 실행 기능을 추가합니다. 동급 <a href="https://www.elastic.co/docs/reference/query-languages/querydsl">DSL</a> 쿼리에 비해 쿼리 성능이 10배 이상 향상되는 경우가 많습니다. 향후 자세한 아키텍처 및 성능 분석을 제공할 예정이니 기대해 주시기 바랍니다.</p><h2>툴킷 확장: 새로운 ES|QL 함수</h2><div style="display:none">Unknown block type "block", specify a component for it in the `components.types` option</div><p><strong>스트링 조작: </strong>더욱 강력한 텍스트 및 URL 처리를 위해 <a href="https://www.elastic.co/docs/reference/query-languages/esql/functions-operators/string-functions#esql-contains">CONTAINS</a>, <a href="https://www.elastic.co/docs/reference/query-languages/esql/functions-operators/mv-functions#esql-mv_contains">MV_CONTAINS</a>, <a href="https://www.elastic.co/docs/reference/query-languages/esql/functions-operators/string-functions#esql-url_encode">URL_ENCODE</a>, <a href="https://www.elastic.co/docs/reference/query-languages/esql/functions-operators/string-functions#esql-url_encode_component">URL_ENCODE_COMPONENT</a>, <a href="https://www.elastic.co/docs/reference/query-languages/esql/functions-operators/string-functions#esql-url_decode">URL_DECODE</a>을(를) 제공합니다.</p><p><strong>시계열 및 위치 정보:</strong> 유연한 시간 버킷을 위한 <a href="https://www.elastic.co/docs/reference/query-languages/esql/functions-operators/grouping-functions#esql-tbucket">TBUCKET</a>, 벡터 연산을 위한 TO_DENSE_VECTOR, 고급 위치 기반 분석을 위한 <code>ST_GEOHASH</code>, <code>ST_GEOTILE</code>, <code>ST_GEOHEX</code>, <code>TO_GEOHASH</code>, <code>TO_GEOTILE</code>, <code>TO_GEOHEX</code> 와 같은 포괄적인 <a href="https://www.elastic.co/docs/reference/query-languages/esql/functions-operators/spatial-functions">위치 정보 함수</a> 세트를 제공합니다.</p><p><strong>날짜 형식:</strong> 가독성 높은 날짜 표현을 위해 <a href="https://www.elastic.co/docs/reference/query-languages/esql/functions-operators/date-time-functions#esql-day_name">DAY_NAME</a>, <a href="https://www.elastic.co/docs/reference/query-languages/esql/functions-operators/date-time-functions#esql-month_name">MONTH_NAME</a>을 제공합니다.</p><p>이러한 함수는 ES|QL 내에서 직접 데이터를 조작하고 분석할 수 있는 더욱 풍부한 도구 세트를 제공합니다.</p><h2>내부적인 개선을 통한 성능 및 효율성 향상</h2><p>Elasticsearch 9.2에는 강조된 기능 외에도 ES|QL 전반에 걸쳐 다양한 성능 최적화가 포함되어 있습니다. <a href="https://www.elastic.co/docs/reference/query-languages/esql/commands/where#like-and-rlike">RLIKE (LIST)</a> 함수가 여러 개의 유사한 RLIKE 쿼리를 대체하는 경우 푸시다운을 사용하여 속도를 높였습니다. <code>RLIKE</code> (LIST)를 사용하면 이러한 쿼리를 단일 Automaton로 병합하여 여러 Automaton 대신 하나의 Automaton를 적용할 수 있습니다. 또한 인덱스 정렬을 통해 키워드 필드를 더 빠르게 로딩하고 일반적인 쿼리를 최적화하여 ES|QL 쿼리를 그 어느 때보다 효율적으로 실행할 수 있도록 개선했습니다.</p><h2>오늘 바로 시작해 보세요!</h2><p>Elasticsearch 9.2는 ES|QL의 획기적인 도약을 의미하며, 데이터 분석 워크플로우에 전례 없는 지원과 유연성을 제공합니다. 이러한 새로운 기능을 살펴보고 그 차이를 경험해 보시기 바랍니다.</p><p>Elasticsearch 9.2의 모든 변경 사항과 향상된 기능에 대한 전체 목록은 <a href="https://www.elastic.co/guide/en/elasticsearch/reference/9.2/release-notes-9.2.0.html">공식 릴리즈 노트</a>에서 확인하세요. 쿼리 작업을 즐겨보세요!</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Elasticsearch로 ChatGPT 커넥터를 구축해 GitHub 문제 쿼리하기]]></title>
            <description><![CDATA[사용자 정의 ChatGPT 커넥터를 구축하고, 하이브리드 검색을 활용해 내부 GitHub 문제를 쿼리하는 Elasticsearch MCP 서버를 배포하는 방법을 알아보세요.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/chatgpt-connector-mcp-server-github-elasticsearch</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/chatgpt-connector-mcp-server-github-elasticsearch</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Tomás Murúa]]></dc:creator>
            <pubDate>Mon, 01 Dec 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/77a6fc1eb263a0eb16aac64f2ecaca5f4ac12ec2-966x568.gif?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>최근 OpenAI는 프로/비즈니스/엔터프라이즈 및 에듀 요금제에서 ChatGPT를 위한 <a href="https://help.openai.com/en/articles/11487775-connectors-in-chatgpt">사용자 정의 커넥터</a> 기능을 발표했습니다. Gmail, GitHub, 드롭박스 등에서 데이터를 활용하기 위한 기본 제공 커넥터 외에도 MCP 서버를 사용하여 사용자 정의 커넥터를 만들 수 있습니다.</p><p>맞춤형 커넥터를 사용하면 기존 ChatGPT 커넥터를 Elasticsearch와 같은 추가 데이터 소스와 결합하여 포괄적인 답변을 얻을 수 있습니다.</p><p>이문서에서는 내부 GitHub 문제와 풀 요청에 대한 정보를 포함하는 Elasticsearch 인덱스에 ChatGPT를 연결하는 <a href="https://modelcontextprotocol.io/docs/getting-started/intro">MCP</a> 서버를 구축합니다. 이 기능을 통해 Elasticsearch 데이터를 사용하여 자연어 쿼리에 응답할 수 있습니다.</p><p>Google Colab에서 <a href="https://gofastmcp.com/getting-started/welcome">FastMCP</a>를 사용하여 MCP 서버를 배포하고, ngrok을 통해 ChatGPT가 연결할 수 있는 공개 URL을 얻어 복잡한 인프라 설정을 간소화합니다.</p><p>MCP와 에코시스템에 대한 종합적인 개요는 <a href="https://www.elastic.co/search-labs/blog/mcp-current-state">MCP의 현재 상태</a>를 참조하세요.</p><h2>필수 구성 요소</h2><p>시작하려면 다음이 필요합니다.</p><ul><li>Elasticsearch 클러스터 (8.X 이상)</li><li>인덱스에 대한 읽기 권한이 있는 Elasticsearch API 키</li><li>Google 계정 (Google Colab용)</li><li>Ngrok 계정 (무료 요금제 사용 가능)</li><li>프로/엔터프라이즈/비즈니스 또는 에듀 요금제로 등록된 ChatGPT 계정</li></ul><h2>ChatGPT MCP 커넥터 요구사항 이해하기</h2><p>ChatGPT MCP 커넥터를 사용하려면 <code>search</code> 와 <code>fetch</code>, 두 가지를 구현해야 합니다. 자세한 내용은 <a href="https://platform.openai.com/docs/mcp#create-an-mcp-server">OpenAI 문서</a>를 참조하세요.</p><h3><a href="https://platform.openai.com/docs/mcp#search-tool">검색 툴</a></h3><p>사용자 쿼리를 기반으로 Elasticsearch 인덱스에서 관련 결과 목록을 반환합니다.</p><h4>수신 내용:</h4><ul><li>사용자의 자연어 쿼리가 포함된 단일 스트링입니다.</li><li>예: &#x27;Elasticsearch 마이그레이션과 관련된 문제 찾기&#x27;</li></ul><h4>반환값: </h4><ul><li>결과 객체 배열을 포함하는 <code>result</code> 키를 가진 객체입니다. 각 결과에는 다음이 포함됩니다.<ul><li><code>id</code> - 고유 문서 식별자</li><li><code>title</code> - 이슈 또는 PR 제목</li><li><code>url</code> - 문제/PR에 대한 링크</li></ul></li></ul><h4>구현에서:</h4><pre><code>return {
    "results": [
        {
            "id": "PR-612",
            "title": "Fix memory leak in WebSocket notification service",
            "url": "https://internal-git.techcorp.com/pulls/612"
        },
        # ... more results
    ]
}</code></pre><h3><a href="https://platform.openai.com/docs/mcp#fetch-tool">가져오기 도구</a></h3><p>특정 문서의 전체 내용을 가져옵니다.</p><h4>수신 내용:</h4><ul><li>검색 결과에서 Elasticsearch 문서 ID를 포함하는 단일 스트링</li><li>예: &#x27;PR-578의 세부 정보를 가져와&#x27;</li></ul><h4>반환값:</h4><ul><li>다음 항목을 포함한 완전한 문서 객체입니다.<ul><li><code>id</code> - 고유 문서 식별자</li><li><code>title</code> - 이슈 또는 PR 제목</li><li><code>text</code> - 전체 문제/PR 설명 및 세부 정보</li><li><code>url</code> - 문제/PR에 대한 링크</li><li><code>type</code> - 문서 유형(문제, pull_request)</li><li><code>status</code> - 현재 상태(오픈, 진행 중, 해결 완료)</li><li><code>priority</code> - 우선순위 수준(낮음, 중간, 높음, 긴급)</li><li><code>assignee</code> - 문제/PR 담당자</li><li><code>created_date</code> - 생성된 시점</li><li><code>resolved_date</code> - 해결된 시점(해당되는 경우)</li><li><code>labels</code> - 문서에 연결된 태그</li><li><code>related_pr</code> - 관련 풀 요청 ID</li></ul></li></ul><pre><code>return {
    "id": "PR-578",
    "title": "Security hotfix: Patch SQL injection vulnerabilities",
    "text": "Description: CRITICAL SECURITY FIX for ISSUE-1889. Patches SQL...",
    "url": "https://internal-git.techcorp.com/pulls/578",
    "type": "pull_request",
    "status": "closed",
    "priority": "critical",
    "assignee": "sarah_dev",
    "created_date": "2025-09-19",
    "resolved_date": "2025-09-19",
    "labels": "security, hotfix, sql",
    "related_pr": null
}</code></pre><p><strong>참고</strong>: 이 예에서는 모든 필드가 루트 레벨에 있는 플랫 구조를 사용합니다. OpenAI 요구 사항은 유연하며 중첩된 메타데이터 개체도 지원합니다.</p><h2>GitHub 이슈 및 PR 데이터 세트</h2><p>이 튜토리얼에서는 문제와 풀 요청이 포함된 내부 GitHub 데이터 집합을 사용합니다. 이는 ChatGPT를 통해 비공개 내부 데이터를 쿼리하는 시나리오입니다.</p><p>데이터 세트는 <a href="https://gist.github.com/TomasMurua/4e7bbdf7a7ebbdffaa663c43578d934a">여기</a>에서 찾을 수 있습니다. 그리고 <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-bulk">벌크 API</a>를 사용해 데이터의 색인을 업데이트할 것입니다.</p><p>이 데이터 세트에는 다음이 포함되어 있습니다.</p><ul><li>설명, 상태, 우선순위 및 담당자와 관련된 문제</li><li>코드 변경, 리뷰, 배포 정보가 포함된 풀 요청</li><li>문제와 PR 간의 관계 (예: PR-578이 ISSUE-1889를 수정함)</li><li>라벨, 날짜 및 기타 메타데이터</li></ul><h3>인덱스 매핑</h3><p>인덱스는 <a href="https://www.elastic.co/docs/manage-data/data-store/mapping">매핑</a>을 사용해 <a href="https://www.elastic.co/docs/explore-analyze/machine-learning/nlp/ml-nlp-elser">ELSER</a>와의 하이브리드 검색을 지원합니다. <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/semantic-text">text_semantic</a>은 시맨틱 검색에 사용되며, 다른 필드는 키워드 검색을 지원합니다.</p><pre><code>{
  "mappings": {
    "properties": {
      "id": {
        "type": "keyword"
      },
      "title": {
        "type": "text"
      },
      "text": {
        "type": "text"
      },
      "text_semantic": {
        "type": "semantic_text",
        "inference_id": ".elser-2-elasticsearch"
      },
      "url": {
        "type": "keyword"
      },
      "type": {
        "type": "keyword"
      },
      "status": {
        "type": "keyword"
      },
      "priority": {
        "type": "keyword"
      },
      "assignee": {
        "type": "keyword"
      },
      "created_date": {
        "type": "date",
        "format": "iso8601"
      },
      "resolved_date": {
        "type": "date",
        "format": "iso8601"
      },
      "labels": {
        "type": "keyword"
      },
      "related_pr": {
        "type": "keyword"
      }
    }
  }
}</code></pre><h2>MCP 서버 구축</h2><p>저희 MCP 서버는 더 나은 결과를 위해 하이브리드 검색을 사용하여 시맨틱과 텍스트 매칭을 결합하는 OpenAI 사양을 따르는 두 가지 도구를 구현합니다.</p><h3>검색 툴</h3><p><a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/reciprocal-rank-fusion">RRF</a>(상호 순위 융합)을 사용하여 의미론적 검색과 텍스트 매칭을 결합한 하이브리드 검색을 사용합니다.</p><pre><code>@mcp.tool()
    async def search(query: str) -> Dict[str, List[Dict[str, Any]]]:
        """
        Search for internal issues and PRs using hybrid search (semantic + text with RRF).
        Returns list with id, title, and url per OpenAI spec.
        """
        if not query or not query.strip():
            return {"results": []}

        logger.info(f"Searching for: '{query}'")

        try:
            # Hybrid search with RRF (Reciprocal Rank Fusion)
            response = es_client.search(
                index=ELASTICSEARCH_INDEX,
                size=10,
                source=["id", "title", "url", "type", "priority"],
                retriever={
                    "rrf": {
                        "retrievers": [
                            {
                                # Semantic search with ELSER
                                "standard": {
                                    "query": {
                                        "semantic": {
                                            "field": "text_semantic",
                                            "query": query
                                        }
                                    }
                                }
                            },
                            {
                                # Text search (BM25) for keyword matching
                                "standard": {
                                    "query": {
                                        "multi_match": {
                                            "query": query,
                                            "fields": [
                                                "title^3",
                                                "text^2",
                                                "assignee^2",
                                                "type",
                                                "labels",
                                                "priority"
                                            ],
                                            "type": "best_fields",
                                            "fuzziness": "AUTO"
                                        }
                                    }
                                }
                            }
                        ],
                        "rank_window_size": 50,
                        "rank_constant": 60
                    }
                }
            )

            results = []
            if response and 'hits' in response:
                for hit in response['hits']['hits']:
                    source = hit['_source']
                    results.append({
                        "id": source.get('id', hit['_id']),
                        "title": source.get('title', 'Unknown'),
                        "url": source.get('url', '')
                    })

            logger.info(f"Found {len(results)} results")
            return {"results": results}

        except Exception as e:
            logger.error(f"Search error: {e}")
            raise ValueError(f"Search failed: {str(e)}")</code></pre><h3>주요 사항:</h3><ul><li><strong>RRF를 사용한 하이브리드 검색:</strong> 시맨틱 검색(ELSER)과 텍스트 검색(BM25)을 결합하여 더 나은 결과를 제공합니다.</li><li><strong>다중 일치 쿼리:</strong> 부스팅(title^3, text^2, assignee^2)을 사용하여 <a href="https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-multi-match-query">여러 필드에 걸쳐 검색합니다</a>. 캐럿 기호(^)는 관련성 점수를 곱하여 콘텐츠보다 제목의 일치 항목에 우선순위를 부여합니다.</li><li><strong>퍼지 매칭:</strong> <a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/common-options#fuzziness"><code>fuzziness: AUTO</code></a>는 대략적인 매칭을 허용함으로써 오타와 맞춤법 오류를 처리합니다.</li><li><strong>상호 순위 결합(RRF) 파라미터 튜닝:</strong><ul><li><code>rank_window_size: 50</code> - 병합하기 전에 각 검색기(시맨틱 및 텍스트)에서 고려할 상위 결과의 수를 지정합니다.</li><li><code>rank_constant: 60</code> - 이 값은 개별 결과 집합의 문서가 최종 순위 결과에 미치는 영향을 결정합니다.</li></ul></li><li><strong>필수 필드만 반환: </strong> OpenAI 사양에 따라 <code>id</code>, <code>title</code>, <code>url</code> 만 반환되며, 불필요한 추가 필드를 노출하지 않습니다.</li></ul><h3>가져오기 도구</h3><p>문서가 있는 경우 문서 ID로 문서 세부 정보를 검색합니다.</p><pre><code>@mcp.tool()
    async def fetch(id: str) -> Dict[str, Any]:
        """
        Retrieve complete issue/PR details by ID.
        Returns id, title, text, url.
        """
        if not id:
            raise ValueError("ID is required")

        logger.info(f"Fetching: {id}")

        try:
            # Search by the 'id' field (not _id) since IDs are stored as a field
            response = es_client.search(
                index=ELASTICSEARCH_INDEX,
                body={
                    "query": {
                        "term": {
                            "id": id  # Search by your custom 'id' field
                        }
                    },
                    "size": 1
                }
            )

            if not response or not response['hits']['hits']:
                raise ValueError(f"Document with id '{id}' not found")

            hit = response['hits']['hits'][0]
            source = hit['_source']

            result = {
                "id": source.get('id', id),
                "title": source.get('title', 'Unknown'),
                "text": source.get('text', ''),
                "url": source.get('url', ''),
                "type": source.get('type', ''),
                "status": source.get('status', ''),
                "priority": source.get('priority', ''),
                "assignee": source.get('assignee', ''),
                "created_date": source.get('created_date', ''),
                "resolved_date": source.get('resolved_date', ''),
                "labels": source.get('labels', ''),
                "related_pr": source.get('related_pr', '')
            }

            logger.info(f"Fetched: {result['title']}")
            return result

        except Exception as e:
            logger.error(f"Fetch error: {e}")
            raise ValueError(f"Failed to fetch '{id}': {str(e)}")</code></pre><h3>주요 사항:</h3><ul><li><strong>문서 ID 필드로 검색:</strong> 사용자 정의 <code>id</code> 필드에서 용어 쿼리를 사용합니다.</li><li><strong>전체 문서 반환:</strong> 모든 콘텐츠가 포함된 전체 <code>text</code> 필드를 포함합니다.</li><li><strong>플랫 구조:</strong> 모든 필드가 루트 수준에 있으며, Elasticsearch의 문서 구조와 일치합니다.</li></ul><h2>Google Colab에 배포</h2><p>Google Colab을 사용하여 MCP 서버를 실행하고 ngrok을 사용해 외부에 공개함으로써 ChatGPT가 연결할 수 있도록 합니다.</p><h3>1단계: Google Colab 노트북 열기</h3><p>사전 구성된 <a href="https://github.com/elastic/elasticsearch-labs/tree/main/supporting-blog-content/elasticsearch-chatgpt-connector">Elasticsearch MCP for ChatGPT</a> 노트북에 액세스합니다.</p><h3>2단계: 자격 증명을 구성하세요</h3><p>세 가지 정보를 준비해야 합니다.</p><ul><li><strong>Elasticsearch URL:</strong> <a href="https://www.elastic.co/docs/deploy-manage/deploy/cloud-enterprise/connect-elasticsearch">Elasticsearch 클러스터 URL</a>입니다.</li><li><strong>Elasticsearch API 키:</strong> <a href="https://www.elastic.co/docs/deploy-manage/api-keys/elasticsearch-api-keys">API 키</a>로 인덱스에 대한 읽기 액세스 권한을 부여합니다.</li><li><strong>Ngrok 인증 토큰:</strong> <a href="https://ngrok.com/">ngrok</a>에서 무료로 제공하는 토큰입니다. ngrok을 사용하여 MCP URL을 인터넷에 노출하여 ChatGPT가 연결할 수 있도록 합니다.</li></ul><h4>ngrok 토큰 받기</h4><ol><li><a href="https://ngrok.com/">ngrok</a>에서 무료 계정 가입</li><li><a href="https://dashboard.ngrok.com/">ngrok 대시보드</a>로 이동</li><li>인증 토큰을 복사하세요</li></ol><h4>Google Colab에 시크릿 추가</h4><p>Google Colab 노트북에서:</p><ol><li>왼쪽 사이드바에서 <strong>키 아이콘</strong>을 클릭하여 <strong>시크릿</strong>을 엽니다.</li><li>이 세 가지 시크릿을 추가합니다.</li></ol><pre><code>ELASTICSEARCH_URL=https://your-cluster.elastic.com:443
ELASTICSEARCH_API_KEY=your-api-key
NGROK_TOKEN=your-ngrok-token</code></pre><p>3. 각 시크릿에 대한 노트북 접근 활성화</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/d5dd6ac19fe816a562c6351fdb0f11369da0e877-609x321.jpg" style="width: 100%;" /><h3>3단계: 노트북 실행</h3><ol><li><strong>런타임</strong>을 클릭한 다음 <strong>모두 실행</strong>을 클릭하여 모든 셀을 실행합니다.</li><li>서버가 시작될 때까지 기다립니다(약 30초).</li><li>공개 ngrok URL을 표시하는 출력을 찾습니다.</li></ol><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/f185100e8869624bc9e1c7b2b4eb32785e2d89e7-1189x283.png" style="width: 100%;" /><p>4. 출력은 다음과 같이 표시됩니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/e02e625e91ed9136454e4401b184575fb03a336e-1052x465.jpg" style="width: 100%;" /><h2>ChatGPT에 연결</h2><p>이제 MCP 서버를 ChatGPT 계정에 연결합니다.</p><ol><li>ChatGPT를 열고 <strong>설정</strong>으로 이동합니다.</li><li><strong>커넥터</strong>로 이동합니다.프로 계정을 사용하는 경우 커넥터에서 <a href="https://platform.openai.com/docs/guides/developer-mode">개발자 모드</a>를 사용 설정해야 합니다.</li></ol><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/32c02192912fc0e7e5a52e9399077ba7ae3b4901-739x715.png" style="width: 100%;" /><p><em>ChatGPT 엔터프라이즈 또는 비즈니스 버전을 사용하는 경우, 커넥터를 작업 환경에 게시해야 합니다.</em></p><p>3. <strong>만들기</strong>를 클릭합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/15c53e5ccc381108a9dc0052cca05bf0fc97679a-755x683.png" style="width: 100%;" /><p><em><strong>참고</strong>: 비즈니스, 엔터프라이즈, 에듀 작업 공간에서는 작업 공간 소유자, 관리자 및 해당 설정이 활성화된 사용자(엔터프라이즈/에듀)만 사용자 정의 커넥터를 추가할 수 있습니다. 일반 회원 역할을 가진 사용자는 사용자 정의 커넥터를 직접 추가할 수 없습니다.</em></p><p><em>소유자 또는 관리자 사용자가 커넥터를 추가하고 활성화하면 작업 공간의 모든 구성원이 이를 사용할 수 있습니다.</em></p><p>4. 필요한 정보를 입력하고 <code>/sse/</code>로 끝나는 ngrok URL을 입력합니다. &#x27;sse&quot;&#x27; 뒤의 &#x27;/&#x27;에 주의하세요. 이것 없이는 작동하지 않습니다.</p><ul><li><strong>이름:</strong> Elasticsearch MCP</li><li><strong>설명: </strong>GitHub 내부 정보를 검색하고 가져오기 위한 사용자 정의 MCP입니다.</li></ul><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/162a85705cc8ac48a3f2f665551d513e0719f93d-479x684.png" style="width: 100%;" /><p>5. <strong>생성</strong> 을 눌러 사용자 정의 MCP를 저장하세요.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/97eb5fb0a32b86bfadfb35561f698616f217c049-913x629.png" style="width: 100%;" /><p>서버가 실행 중이면 즉시 연결됩니다. Elasticsearch API 키가 서버에 구성되어 있으므로 추가 인증이 필요하지 않습니다.</p><h2>MCP 서버 테스트</h2><p>질문을 하기 전에 ChatGPT가 사용할 커넥터를 선택해야 합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/77a6fc1eb263a0eb16aac64f2ecaca5f4ac12ec2-966x568.gif" style="width: 100%;" /><h3>프롬프트 1: 문제 검색</h3><p>질문: &#x27;<strong>Elasticsearch 마이그레이션과 관련된 문제를 찾아&#x27;</strong>라고 요청하고 작업 도구 호출을 확인하세요.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/cfd781acbff8cd7c8095bbe29224f8b26d581f77-650x375.png" style="width: 100%;" /><p>ChatGPT가 사용자의 쿼리를 사용하여 <code>search</code> 도구를 호출합니다. 사용 가능한 도구를 찾고 Elasticsearch 도구를 호출할 준비를 하며, 도구에 대해 조치를 취하기 전에 사용자에게 확인하는 것을 볼 수 있습니다.</p><h4>도구 호출 요청:</h4><pre><code>{
  "query": "Elasticsearch migration issues"
}</code></pre><h4>도구 응답:</h4><pre><code>{
  "results": [
    {
      "id": "PR-598",
      "title": "Elasticsearch 8.x migration - Application code changes",
      "url": "https://internal-git.techcorp.com/pulls/598"
    },
    {
      "id": "ISSUE-1712",
      "title": "Migrate from Elasticsearch 7.x to 8.x",
      "url": "https://internal-git.techcorp.com/issues/1712"
    },
    {
      "id": "RFC-045",
      "title": "Design Proposal: Microservices Migration Architecture",
      "url": "https://internal-git.techcorp.com/rfcs/045"
    }
    // ... 7 more results
  ]
}</code></pre><p>ChatGPT는 결과를 처리하여 자연스러운 대화 형식으로 표시합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/9d5b6cff85c7e54ccc2584b8ae96d45495fae8c1-923x1352.png" style="width: 100%;" /><h3>비하인드 스토리</h3><h4>프롬프트: &#x27;Elasticsearch 마이그레이션 관련 문제를 찾아&#x27;</h4><p>1. ChatGPT 호출 <code>search(“Elasticsearch migration”)</code></p><p>2. Elasticsearch가 하이브리드 검색 수행</p><ul><li><strong>시맨틱 검색</strong>은 &#x27;업그레이드&#x27; 및 &#x27;<em>버전 호환성</em>&#x27;과 같은 개념을 이해합니다.</li><li><strong>텍스트 검색</strong>은 &#x27;<em>Elasticsearch</em>&#x27; 및 &#x27;마이그레이션&#x27;과 정확히 일치하는 결과를 찾습니다.</li><li><strong>RRF</strong>는 두 가지 접근 방식의 결과를 결합하고 순위를 매깁니다.</li></ul><p>3. <code>id</code>, <code>title</code>와 일치하는 상위 10개의 이벤트를 반환합니다, <code>url</code></p><p>4. ChatGPT는 &#x27;<em>ISSUE-1712: Elasticsearch 7.x에서 8.x로 마이그레이션</em>&#x27;을 가장 관련성 있는 결과로 식별합니다</p><h3>프롬프트 2: 전체 세부 정보 확인</h3><p>문의: <strong><em>&#x27;ISSUE-1889의 세부 정보를 보여줘&#x27;</em></strong></p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/5c0db5245535ce67a36056e61e135bddc97ce496-934x629.png" style="width: 100%;" /><p>ChatGPT는 사용자가 특정 문제에 대한 자세한 정보를 원한다는 것을 인식하고 <code>fetch</code> 도구를 호출한 후, 도구에 대해 조치를 취하기 전에 사용자에게 확인합니다.</p><h4>도구 호출 요청:</h4><pre><code>{
  "id": "ISSUE-1889"
}</code></pre><h4>도구 응답:</h4><pre><code>{
  "id": "ISSUE-1889",
  "title": "SQL injection vulnerability in search endpoint",
  "text": "Description: Security audit identified SQL injection vulnerability in /api/v1/search endpoint. User input from query parameter is not properly sanitized before being used in raw SQL query. Severity: HIGH - Immediate action required Affected Code: - File: services/search/query_builder.py - Line: 145-152 - Issue: String concatenation used instead of parameterized queries Investigation: - @security_team_alice: Confirmed exploitable with UNION-based injection - @sarah_dev: Checking all other endpoints for similar patterns - @john_backend: Found 3 more instances in legacy codebase Remediation: - Rewrite using SQLAlchemy ORM or parameterized queries - Add input validation and sanitization - Implement WAF rules as additional layer - Security regression tests Comments: - @tech_lead_mike: Stop all other work, this is P0 - @sarah_dev: PR-578 ready with fixes for all 4 vulnerable endpoints - @alex_devops: Deployed hotfix to production 2025-09-19 at 14:30 UTC - @security_team_alice: Verified fix, conducting full pentest next week Resolution: All vulnerable endpoints patched. Added pre-commit hooks to catch raw SQL queries. Security training scheduled for team.",
  "url": "https://internal-git.techcorp.com/issues/1889",
  "type": "issue",
  "status": "closed",
  "priority": "critical",
  "assignee": "sarah_dev",
  "created_date": "2025-09-18",
  "resolved_date": "2025-09-19",
  "labels": "security, vulnerability, bug, sql",
  "related_pr": "PR-578"
}</code></pre><p>ChatGPT는 정보를 종합하여 명확하게 제시합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/410f19f213e94fc4e3c47eeef6e04b69e0c86159-602x462.png" style="width: 100%;" /><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/54d8ffa117628a1e3afc317c3ab75d4f7731d7ab-767x1600.png" style="width: 100%;" /><h3>비하인드 스토리</h3><h4>프롬프트: &#x27;ISSUE-1889의 세부 정보를 가져와&#x27;</h4><ol><li>ChatGPT 호출 <code>fetch(“ISSUE-1889”)</code></li><li>Elasticsearch가 전체 문서를 검색합니다.</li><li>모든 필드가 루트 수준에 포함된 전체 문서를 반환합니다.</li><li>ChatGPT는 정보를 종합하고 적절한 출처를 제시하며 응답합니다.</li></ol><h2>결론</h2><p>이 문서에서는 전용 <strong>검색</strong> 및 <strong>가져오기</strong> MCP 도구를 사용하여 ChatGPT를 Elasticsearch에 연결하고, 비공개 데이터에 대한 자연어 쿼리를 가능하게 하는 사용자 정의 MCP 서버를 구축했습니다.</p><p>이 MCP 패턴은 자연어로 쿼리하려는 모든 Elasticsearch 인덱스, 문서, 제품, 로그 또는 기타 데이터에 적용할 수 있습니다.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[LangChain과 Elasticsearch를 사용하여 에이전트 RAG 어시스턴트 개발하기]]></title>
            <description><![CDATA[적응형 라우팅을 통해 기사에 대한 쿼리에 답변하는 LangChain과 Elasticsearch를 사용하여 에이전트 래그 뉴스 어시스턴트를 구축하는 방법을 알아보세요.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/agentic-rag-news-assistant-langchain-elasticsearch</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/agentic-rag-news-assistant-langchain-elasticsearch</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Kirti Sodhi]]></dc:creator>
            <pubDate>Fri, 28 Nov 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/d7213956309c6b5863627aca2ba95569b02fc4c7-720x420.jpg?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>이 블로그 게시물에서는 에이전트 RAG 워크플로우의 주요 기능과 일반적인 디자인 패턴에 대해 자세히 설명합니다. 또한 Elasticsearch를 벡터 저장소로 사용하고 LangChain을 사용하여 에이전트 RAG 프레임워크를 구성하는 실습 예제를 통해 이러한 워크플로우를 구현하는 방법을 보여줍니다. 마지막으로 이러한 아키텍처의 설계 및 구현과 관련된 모범 사례와 과제에 대해 간략하게 설명합니다. 이 <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/agentic-rag/agent_rag_news_assistant.ipynb">Jupyter 노트북을</a> 사용하여 간단한 에이전트 RAG 파이프라인을 만들 수 있습니다.</p><h2>에이전트 RAG 소개</h2><p>검색 증강 생성<a href="https://www.elastic.co/docs/solutions/search/rag">(RAG)</a>은 LLM 기반 애플리케이션의 초석이 되어 모델이 사용자 쿼리를 기반으로 관련 컨텍스트를 검색하여 최적의 답변을 제공할 수 있게 해줍니다. RAG 시스템은 사전 학습된 LLM 지식에 국한되지 않고 API 또는 데이터 저장소의 외부 정보를 활용하여 LLM 응답의 정확성과 컨텍스트를 향상시킵니다. 반면에 AI 에이전트는 자율적으로 작동하여 지정된 목표를 달성하기 위해 의사 결정을 내리고 조치를 취합니다.</p><p>에이전트 RAG는 검색 증강 생성과 에이전트 추론의 강점을 통합한 프레임워크입니다. RAG를 상담원의 의사 결정 프로세스에 통합하여 시스템이 동적으로 데이터 소스를 선택하고, 더 나은 컨텍스트 검색을 위해 쿼리를 구체화하고, 더 정확한 응답을 생성하고, 피드백 루프를 적용하여 출력 품질을 지속적으로 개선할 수 있도록 지원합니다.</p><h2>에이전트 RAG의 주요 기능</h2><p>에이전트 RAG 프레임워크는 기존 RAG 시스템보다 크게 발전한 것입니다. 고정된 검색 프로세스를 따르는 대신 실시간으로 결과를 계획, 실행 및 최적화할 수 있는 동적 에이전트를 활용합니다.</p><p>에이전트 RAG 파이프라인을 구별하는 몇 가지 주요 기능을 살펴보겠습니다:</p><ul><li><strong>동적 의사 결정</strong>: 에이전트 RAG는 추론 메커니즘을 사용하여 사용자의 의도를 파악하고 각 쿼리를 가장 관련성이 높은 데이터 소스로 라우팅하여 정확하고 맥락에 맞는 응답을 생성합니다.</li><li><strong>종합적인 쿼리 분석:</strong> 에이전틱 RAG는 하위 질문과 전반적인 의도를 포함하여 사용자 쿼리를 심층적으로 분석합니다. 쿼리 복잡성을 평가하고 가장 관련성이 높은 데이터 소스를 동적으로 선택하여 정보를 검색함으로써 정확하고 완전한 응답을 보장합니다.</li><li><strong>다단계 협업</strong>: 이 프레임워크는 전문 에이전트 네트워크를 통해 다단계 협업을 가능하게 합니다. 각 에이전트는 더 큰 목표의 특정 부분을 처리하며, 일관된 결과를 달성하기 위해 순차적으로 또는 동시에 작업합니다.</li><li><strong>자체 평가 메커니즘</strong>: 에이전트 RAG 파이프라인은 자체 반영을 사용하여 검색된 문서와 생성된 응답을 평가합니다. 검색된 정보가 쿼리에 완전히 부합하는지 확인한 다음 출력물의 정확성, 완전성, 사실적 일관성을 검토할 수 있습니다.</li><li><strong>외부 도구와의 통합</strong>: 이 워크플로는 외부 API, 데이터베이스 및 실시간 정보 소스와 상호 작용하여 최신 정보를 통합하고 진화하는 데이터에 동적으로 적응할 수 있습니다.</li></ul><h2>에이전트 RAG의 워크플로 패턴</h2><p>워크플로 패턴은 에이전트 AI가 안정적이고 효율적인 방식으로 LLM 기반 애플리케이션을 구성, 관리 및 오케스트레이션하는 방법을 정의합니다. 이러한 에이전트 워크플로우를 구현하기 위해 <a href="https://www.langchain.com/">LangChain</a>, <a href="https://www.langchain.com/langgraph">LangGraph</a>, <a href="https://www.crewai.com/">CrewAI</a>, <a href="https://www.llamaindex.ai/">LlamaIndex와</a> 같은 여러 프레임워크와 플랫폼을 사용할 수 있습니다.</p><ol><li><strong>순차적 검색 체인</strong>: 순차적 워크플로는 복잡한 작업을 단순하고 정돈된 단계로 나눕니다. 각 단계는 다음 단계의 입력을 개선하여 더 나은 결과로 이어집니다. 예를 들어 고객 프로필을 만들 때 한 상담원이 CRM에서 기본 세부 정보를 가져오고, 다른 상담원이 거래 데이터베이스에서 구매 내역을 검색한 다음, 최종 상담원이 이 정보를 결합하여 추천 또는 보고서를 위한 완전한 프로필을 생성할 수 있습니다.</li><li><strong>라우팅 검색 체인</strong>: 이 워크플로 패턴에서는 라우터 에이전트가 입력을 분석하여 가장 적합한 프로세스나 데이터 소스로 라우팅합니다. 이 접근 방식은 겹치는 부분이 최소화된 여러 데이터 원본이 존재하는 경우에 특히 효과적입니다. 예를 들어 고객 서비스 시스템에서 라우터 상담원은 기술 문제, 환불, 불만 등 들어오는 요청을 분류한 후 해당 부서로 라우팅하여 효율적으로 처리합니다.</li><li><strong>병렬 검색 체인</strong>: 이 워크플로 패턴에서는 여러 개의 독립적인 하위 작업이 동시에 실행되고 나중에 그 결과물을 집계하여 최종 응답을 생성합니다. 이 접근 방식은 처리 시간을 크게 단축하고 워크플로 효율성을 높입니다. 예를 들어 고객 서비스 병렬 워크플로우에서 한 상담원은 유사한 과거 요청을 검색하고 다른 상담원은 관련 지식창고 문서를 참조합니다. 그런 다음 애그리게이터는 이러한 출력을 결합하여 종합적인 해상도를 생성합니다.</li><li><strong>오케스트레이터 워커 체인</strong>: 이 워크플로는 독립적인 하위 작업을 활용한다는 점에서 병렬화와 유사점을 공유합니다. 그러나 중요한 차이점은 오케스트레이터 에이전트의 통합에 있습니다. 이 에이전트는 사용자 쿼리를 분석하여 런타임 중에 하위 작업으로 동적으로 분류하고 정확한 응답을 작성하는 데 필요한 적절한 프로세스 또는 도구를 식별하는 역할을 담당합니다.</li></ol><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/ece6fc2403f211556c93e99d5227bfb7053b0c31-1600x1047.png" style="width: 100%;" /><h2>에이전트 RAG 파이프라인을 처음부터 구축하기</h2><p>에이전트 RAG의 원리를 설명하기 위해 LangChain과 Elasticsearch를 사용해 워크플로우를 설계해 보겠습니다. 이 워크플로에서는 여러 상담원이 협업하여 쿼리를 분석하고 관련 정보를 검색하며 결과를 평가하고 일관된 응답을 생성하는 라우팅 기반 아키텍처를 채택하고 있습니다. 이 <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/agentic-rag/agent_rag_news_assistant.ipynb">Jupyter 노트북을</a> 참조하여 이 예제를 따라할 수 있습니다.</p><p>워크플로는 라우터 에이전트가 사용자의 쿼리를 분석하여 최적의 검색 방법, 즉 <code>vectorstore</code>, <code>websearch</code>, <code>composite</code> 중 하나를 선택하는 것으로 시작됩니다. 벡터스토어는 기존의 RAG 기반 문서 검색을 처리하고, 웹검색은 벡터스토어에 저장되지 않은 최신 정보를 가져오며, 여러 소스의 정보가 필요한 경우 이 두 가지를 결합하는 복합적인 접근 방식을 사용합니다.</p><p>문서가 적합하다고 판단되면 요약 에이전트가 명확하고 문맥에 맞는 답변을 생성합니다. 그러나 문서가 불충분하거나 관련성이 없는 경우 쿼리 재작성 에이전트가 검색을 개선하기 위해 쿼리를 다시 작성합니다. 그러면 수정된 쿼리가 라우팅 프로세스를 다시 시작하여 시스템이 검색을 개선하고 최종 결과를 향상시킬 수 있습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/ed8701a7f15558fbf2e967a884b3e770eccb826b-1256x1092.png" style="width: 100%;" /><h3>필수 구성 요소</h3><p>이 워크플로에서는 예제를 효과적으로 실행하기 위해 다음과 같은 핵심 구성 요소를 사용합니다:</p><ul><li>Python 3.10</li><li><a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/agentic-rag/agent_rag_news_assistant.ipynb">주피터 노트북</a></li><li>Azure OpenAI</li><li>Elasticsearch</li><li>LangChain</li></ul><p>계속 진행하기 전에 이 예제에 필요한 다음 환경 변수 집합을 구성하라는 메시지가 표시됩니다.</p><pre><code>AZURE_OPENAI_ENDPOINT="Add your azure openai endpoint"
AZURE_OPENAI_KEY="Add your azure openai key"
AZURE_OPENAI_DEPLOYMENT="gpt-4.1"
AZURE_OPENAI_API_VERSION="Add your azure openai api version"

ES_ENDPOINT = "Add your Elasticsearch ENDPOINT"
ES_API_KEY = "Add your Elasticsearch API KEY"</code></pre><h3>데이터 소스</h3><p>이 워크플로는 AG 뉴스 데이터 세트의 하위 집합을 사용하여 설명합니다. 이 데이터 세트는 국제, 스포츠, 비즈니스, 과학/기술 등 다양한 카테고리의 뉴스 기사로 구성되어 있습니다.</p><pre><code>dataset = load_dataset("ag_news", split="train[:1000]")
docs = [
    Document(
        page_content=sample["text"],
        metadata={"category": sample["label"]}
    )
    for sample in dataset
]</code></pre><p><a href="https://python.langchain.com/docs/integrations/vectorstores/elasticsearch/">ElasticsearchStore 모듈은</a> <code>langchain_elasticsearch</code> 에서 벡터 저장소로 활용됩니다. 검색을 위해 Elastic의 독점적인 임베딩 모델인 <a href="https://www.elastic.co/docs/explore-analyze/machine-learning/nlp/ml-nlp-elser">ELSER를</a> 사용하는 SparseVectorStrategy를 구현합니다. 벡터 저장소를 시작하기 전에 ELSER 모델이 Elasticsearch 환경에 올바르게 설치 및 배포되었는지 확인해야 합니다.</p><pre><code>elastic_vectorstore = ElasticsearchStore.from_documents(
    docs,
    es_url=ES_ENDPOINT,
    es_api_key=ES_API_KEY,
    index_name=index_name,
    strategy=SparseVectorStrategy(model_id=".elser_model_2"),
)

elastic_vectorstore.client.indices.refresh(index=index_name)</code></pre><p>웹 검색 기능은 LangChain 커뮤니티 도구의 <a href="https://python.langchain.com/api_reference/community/tools/langchain_community.tools.ddg_search.tool.DuckDuckGoSearchRun.html">DuckDuckGoSearchRun을</a> 사용하여 구현되어 시스템이 웹에서 실시간 정보를 효율적으로 검색할 수 있습니다. 보다 관련성 높은 결과를 제공할 수 있는 다른 검색 API를 사용하는 것도 고려해 볼 수 있습니다. 이 도구는 API 키 없이도 검색이 가능하기 때문에 선택되었습니다.</p><pre><code>duckduckgo = DuckDuckGoSearchRun(description= "A custom DuckDuckGo search tool for finding latest news stories.", verbose=True)
def websearch_retriever(query):
    results = duckduckgo.run(f"{query}")
    return results</code></pre><p>복합 검색기는 여러 소스를 조합해야 하는 쿼리를 위해 설계되었습니다. 웹에서 실시간 데이터를 검색하는 동시에 벡터 스토어에서 과거 뉴스를 참조하여 포괄적이고 맥락에 맞는 정확한 응답을 제공하는 데 사용됩니다.</p><pre><code>def composite_retriever(query):
    related_docs = vectorstore_retriever(query)
    related_docs += websearch_retriever(query)
    return related_docs</code></pre><h3>상담원 설정하기</h3><p>다음 단계에서는 이 워크플로 내에서 추론 및 의사 결정 기능을 제공하도록 LLM 에이전트를 정의합니다. 우리가 만들 LLM 체인에는 다음이 포함됩니다: <code>router_chain</code>, <code>grade_docs_chain</code>, <code>rewrite_query_chain</code>, 그리고 <code>summary_chain</code>.</p><p>라우터 에이전트는 LLM 어시스턴트를 사용하여 런타임에 주어진 쿼리에 가장 적합한 데이터 소스를 결정합니다. 채점 에이전트는 검색된 문서의 관련성을 평가합니다. 문서가 관련성이 있다고 판단되면 요약 에이전트로 전달되어 요약을 생성합니다. 그렇지 않으면 쿼리 재작성 에이전트가 쿼리를 재구성하여 라우팅 프로세스로 다시 보내 다른 검색을 시도합니다. <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/agentic-rag/agent_rag_news_assistant.ipynb">노트북의</a> LLM 체인 섹션에서 모든 에이전트에 대한 지침을 찾을 수 있습니다.</p><pre><code>class RouteQuery(BaseModel):
    datasource: Literal["vectorstore", "websearch", "composite"] = Field(
        ...,
        description="Choose to route the query to web search, vectorstore or composite."
    )

router_prompt = ChatPromptTemplate.from_template("""You are an assistant that decides the best data source for questions based on news articles.
Choose one of the following options:
- 'vectorstore': for general, background, or historical news articles.
- 'websearch': for recent discoveries, 'latest', 'current', or '2025' type queries.
- 'composite': when the question needs both historical and current knowledge on news articles.

Question: {query}

Return one word: 'vectorstore', 'websearch', or 'composite'.
""")
router_structured = llm.with_structured_output(RouteQuery)
router_chain: RunnableSequence = router_prompt | router_structured</code></pre><p><code>llm.with_structured_output</code> 은 모델의 출력이 <code>RouteQuery</code> 클래스의 BaseModel에 정의된 사전 정의된 스키마를 따르도록 제한하여 결과의 일관성을 보장합니다. 두 번째 줄은 <code>router_prompt</code> 과 <code>router_structured</code> 을 연결하여 <code>RunnableSequence</code> 을 구성하여 입력 프롬프트가 언어 모델에 의해 처리되어 구조화된 스키마 준수 결과를 생성하는 파이프라인을 형성합니다.</p><h3>그래프 노드 정의</h3><p>이 부분에는 시스템의 여러 구성 요소 간에 흐르는 데이터를 나타내는 그래프의 상태를 정의하는 작업이 포함됩니다. 이러한 상태를 명확하게 지정하면 워크플로우의 각 노드가 어떤 정보에 액세스하고 업데이트할 수 있는지 알 수 있습니다.</p><pre><code>class RAGState(TypedDict):
    query: str
    docs: List[Document]
    router: str
    summary: str
    self_reflection: bool
    retry_count: int = 0</code></pre><p>상태가 정의되면 다음 단계는 그래프의 노드를 정의하는 것입니다. 노드는 데이터에 대한 특정 연산을 수행하는 그래프의 기능 단위와 같습니다. 파이프라인에는 7개의 서로 다른 노드가 있습니다.</p><pre><code>def router(state: RAGState):
   router = router_chain.invoke({'query': state["query"]})
   logger.info(f"Router selected the datasource: {router.datasource}")
   logger.info(f"User query: {state['query']}")
   return {"router": router.datasource}

def vectorstore(state: RAGState):
   return {"docs": vectorstore_retriever(state["query"])}

def websearch(state: RAGState):
   return {"docs": websearch_retriever(state["query"])}

def composite(state: RAGState):
   return {"docs": composite_retriever(state["query"])}

def self_reflection(state: RAGState):
   evaluation = grade_docs_chain.invoke(
       {"query": state["query"], "docs": state["docs"]}
   )
   if evaluation.binary_score:
       logger.info(f"Self-reflection passed -- binary_score={evaluation.binary_score}")
   else:
       logger.info(f"Self-reflection failed -- binary_score={evaluation.binary_score}")

   return {
       "self_reflection": evaluation.binary_score,
   }

def query_rewriter(state: RAGState):
   retry_count = state.get("retry_count", 0) + 1
   new_query = rewrite_query_chain.invoke({"query": state["query"]})
   logger.info(f"Query rewritten: {new_query}, retry_count: {retry_count}")
   return {
       "query": new_query,
       "retry_count": retry_count,
   }

def summarize(state: RAGState):
   summary = summarize_chain.run(
       query=state["query"],
       docs=state["docs"],
   )
   return {"summary": summary}</code></pre><p><code>query_rewriter</code> 노드는 워크플로에서 두 가지 용도로 사용됩니다. 먼저, 자체 반영 에이전트가 평가한 문서가 불충분하거나 관련성이 없다고 판단되는 경우 검색을 개선하기 위해 <code>rewrite_query_chain</code> 을 사용하여 사용자 쿼리를 재작성합니다. 둘째, 쿼리가 재작성된 횟수를 추적하는 카운터 역할을 합니다.</p><p>노드가 호출될 때마다 워크플로 상태에 저장된 <code>retry_count</code> 이 증가합니다. 이 메커니즘은 워크플로우가 무한 루프에 빠지는 것을 방지합니다. <code>retry_count</code> 이 사전 정의된 임계값을 초과하면 시스템은 오류 상태, 기본 응답 또는 사용자가 선택한 기타 사전 정의된 조건으로 폴백할 수 있습니다.</p><h3>그래프 컴파일하기</h3><p>마지막 단계는 그래프의 가장자리를 정의하고 필요한 조건을 추가한 후 컴파일하는 것입니다. 모든 그래프는 워크플로우의 시작점 역할을 하는 지정된 시작 노드에서 시작해야 합니다. 그래프의 에지는 노드 간의 데이터 흐름을 나타내며 두 가지 유형이 있을 수 있습니다:</p><ul><li>직선 가장자리: 한 노드에서 다른 노드로의 직접적이고 무조건적인 흐름을 정의합니다. 첫 번째 노드가 작업을 완료할 때마다 워크플로는 직선을 따라 다음 노드로 자동 진행됩니다.</li><li>조건부 에지: 현재 상태 또는 노드의 계산 결과에 따라 워크플로를 분기할 수 있습니다. 다음 노드는 평가 결과, 라우팅 결정 또는 재시도 횟수 등의 조건에 따라 동적으로 선택됩니다.</li></ul><pre><code>graph.add_edge(START, "router")

def after_router(state: RAGState):
   route = state.get("router", None)
   if route == "vectorstore":
       return "vectorstore"
   elif route == "websearch":
       return "websearch"
   else:
       return "composite"

def after_self_reflection(state: RAGState):
   if state["self_reflection"]:
           return "summarize"
   return "query_rewriter"

def after_query_rewriter(state: RAGState):
   while state['retry_count'] <= 3:
           return "router"
   raise RuntimeError("Maximum retries (3) reached -- evaluation failed.")

graph.add_conditional_edges(
   "router",
   after_router,
   {
       "vectorstore": "vectorstore",
       "websearch": "websearch",
       "composite": "composite"
   }
)

graph.add_edge("vectorstore", "self_reflection")
graph.add_edge("websearch", "self_reflection")
graph.add_edge("composite", "self_reflection")
graph.add_conditional_edges(
   "self_reflection",
   after_self_reflection,
   {
       "summarize": "summarize",
       "query_rewriter": "query_rewriter"
   }
)
graph.add_conditional_edges("query_rewriter", after_query_rewriter, {"router": "router"})
graph.add_edge("summarize", END)
agent=graph.compile()</code></pre><p>이제 첫 번째 에이전트 RAG 파이프라인이 준비되었으며 컴파일된 에이전트를 사용하여 테스트할 수 있습니다.</p><pre><code>result = agent.invoke({"query": query1})
logger.info(f"\nFinal Summary:\n: {result['summary']}")</code></pre><h3>에이전트 RAG 파이프라인 테스트</h3><p>이제 아래와 같이 세 가지 유형의 쿼리를 사용하여 이 파이프라인을 테스트해 보겠습니다. 결과는 다를 수 있으며, 아래 예시는 한 가지 가능한 결과를 보여주는 것일 뿐입니다.</p><pre><code>query1="What are the latest AI models released this month?"
query2="What technological innovations are discussed in Sci/Tech news?"
query3="Compare a Sci/Tech article from the dataset with a current web article about AI trends."</code></pre><p>첫 번째 쿼리의 경우 라우터는 <code>websearch</code> 을 데이터 소스로 선택합니다. 쿼리가 자체 반영 평가에 실패하면 출력에 표시된 것처럼 쿼리 재작성 단계로 리디렉션됩니다.</p><pre><code>INFO     | __main__:router:11 - Router selected the datasource: websearch
INFO     | __main__:router:12 - User query: What are the latest AI models released this month?
Latest Singapore news, including the city state's relationships with Malaysia and Mahathir, China and Xi Jinping, and the rest of Southeast Asia. 3 days ago · The latest military news, insights and analysis from China. All the latest news, opinions and analysis on Hong Kong, China, Asia and around the world Latest news, in-depth features and opinion on Malaysia, covering politics, economy, society and the Asean member-nation's relationships with China, Singapore, and other Southeast Asian ... Oct 12, 2025 · Brics (an acronym for Brazil, Russia, India, China and South Africa) refers to an association of 10 leading emerging markets. The other member states are Egypt, Ethiopia, ...
INFO     | __main__:self_reflection:31 - Self-reflection failed -- binary_score=False
INFO     | __main__:query_rewriter:40 - Query rewritten: query='Which AI models have been officially released in June 2024?', retry_count: 1
INFO     | __main__:router:11 - Router selected the datasource: websearch
INFO     | __main__:router:12 - User query: query='Which AI models have been officially released in June 2024?'
Dream Machine is a text-to-video model created by Luma Labs and launched in June 2024 . It generates video output based on user prompts or still images. Dream Machine has been noted for its ability to realistically capture motion... Released in June 2023. In June 2024 , Baidu announced Ernie 4.0 Turbo. In April 2025, Ernie 4.5 Turbo and X1 Turbo were released . These models are optimized for faster response times and lower operational costs.[28][29]. The meaning of QUERY is question, inquiry. How to use query in a sentence. Synonym Discussion of Query. QUERY definition: 1. a question, often expressing doubt about something or looking for an answer from an authority.... Learn more. Query definition: a question; an inquiry.. See examples of QUERY used in a sentence.
INFO     | __main__:self_reflection:29 - Self-reflection passed -- binary_score=True
INFO     | __main__:<module>:2 - 
Final Summary:
: In June 2024, two AI models were officially released: Dream Machine, a text-to-video model launched by Luma Labs, and Ernie 4.0 Turbo, announced by Baidu, which is optimized for faster response times and lower operational costs.</code></pre><p>다음으로 두 번째 쿼리를 통해 <code>vectorstore</code> 검색이 사용되는 예시를 살펴봅니다.</p><pre><code>INFO     | __main__:router:11 - Router selected the datasource: vectorstore
INFO     | __main__:router:12 - User query: What technological innovations are discussed in Sci/Tech news?
INFO     | __main__:self_reflection:29 - Self-reflection passed -- binary_score=True
INFO     | __main__:<module>:2 - 
Final Summary:
: Recent Sci/Tech news highlights several technological innovations: NASA is collaborating with Silicon Valley firms to build a powerful Linux-based supercomputer to support theoretical research and shuttle engineering; new chromatin transfer techniques have enabled the cloning of cats; cybersecurity advancements are being discussed in relation to protecting personal technology; Princeton University scientists assert that existing technologies can be used immediately to stabilize global warming; and a set of GameBoy micro-games has been recognized for innovation in game design.</code></pre><p>최종 쿼리는 벡터스토어와 웹 검색을 모두 활용하는 복합 검색으로 진행됩니다.</p><pre><code>INFO     | __main__:router:11 - Router selected the datasource: composite
INFO     | __main__:router:12 - User query: Compare a Sci/Tech article from the dataset with a current web article about AI trends.
Atlas currently only available on macOS, built on Chromium with planned features like ad-blocking still in development. OpenAI's Atlas browser launched with bold promises of AI -powered web browsing, but early real-world testing reveals a different story. Career-long data are updated to end-of-2024 and single recent year data pertain to citations received during calendar year 2024. The selection is based on the top 100,000 scientists by c-score (with and without self-citations) or a percentile rank of 2% or above in the sub-field. In this article I list 45 AI tools across 21 different categories. After exploring all the available options in each category, I've carefully selected the best tools based on my personal experience. Reading a complex technical article ? Simply highlight confusing terminology and ask "what's this?" to receive instant explanations. compare browsers. Comparison showing traditional browser navigation versus OpenAI Atlas AI -powered workflows. After putting Gemini, ChatGPT, Grok, and DeepSeek through rigorous testing in October 2025, it's clear that there isn't one AI that reigns supreme across all categories.
INFO     | __main__:self_reflection:29 - Self-reflection passed -- binary_score=True
INFO     | __main__:<module>:2 - 
Final Summary:
: A Sci/Tech article from the dataset highlights NASA's development of robust artificial intelligence software for planetary rovers, aiming to make them more self-reliant and capable of decision-making during missions. In contrast, a current web article about AI trends focuses on the proliferation of AI-powered tools across various categories, including browsers like OpenAI Atlas, and compares leading models such as Gemini, ChatGPT, Grok, and DeepSeek, noting that no single AI currently excels in all areas. While the NASA article emphasizes specialized AI applications for autonomous robotics in space exploration, the current trends article showcases the broadening impact of AI across consumer and professional technologies, with ongoing competition and rapid innovation among major AI platforms.</code></pre><p>위의 워크플로우에서 에이전트 RAG는 사용자 쿼리에 대한 정보를 검색할 때 사용할 데이터 소스를 지능적으로 결정하여 응답의 정확성과 관련성을 개선합니다. 에이전트를 테스트하기 위해 추가 예제를 만들고 출력을 검토하여 흥미로운 결과가 나오는지 확인할 수 있습니다.</p><h2>에이전트 RAG 워크플로우 구축을 위한 모범 사례</h2><p>이제 에이전트 RAG의 작동 방식을 이해했으니 이러한 워크플로를 구축하기 위한 몇 가지 모범 사례를 살펴보겠습니다. 이 가이드라인을 따르면 시스템을 효율적이고 쉽게 유지 관리하는 데 도움이 됩니다.</p><ul><li><strong>폴백에 대비</strong>하세요: 워크플로우의 어느 단계에서든 장애가 발생할 경우를 대비하여 미리 대체 전략을 계획하세요. 여기에는 기본 답변 반환, 오류 상태 트리거 또는 대체 도구 사용이 포함될 수 있습니다. 이렇게 하면 시스템이 전체 워크플로우를 중단하지 않고도 장애를 원활하게 처리할 수 있습니다.</li><li><strong>포괄적인 로깅을 구현하세요</strong>: 재시도, 생성된 출력, 라우팅 선택, 쿼리 재작성 등 워크플로우의 각 단계에서 로깅을 구현해 보세요. 이러한 로그는 투명성을 개선하고 디버깅을 더 쉽게 하며 시간이 지남에 따라 프롬프트, 상담원 행동 및 검색 전략을 개선하는 데 도움이 됩니다.</li><li><strong>적절한 워크플로 패턴을 선택합니다</strong>: 사용 사례를 검토하여 필요에 가장 적합한 워크플로 패턴을 선택하세요. 단계별 추론에는 순차적 워크플로를, 독립적인 데이터 소스에는 병렬 워크플로를, 다중 도구 또는 복잡한 쿼리에는 오케스트레이터-작업자 패턴을 사용합니다.</li><li><strong>평가 전략을 통합하세요</strong>: 워크플로우의 여러 단계에 평가 메커니즘을 통합하세요. 여기에는 자기 반성 에이전트, 검색된 문서 채점, 자동화된 품질 검사 등이 포함될 수 있습니다. 평가는 검색된 문서가 관련성이 있는지, 답변이 정확한지, 복잡한 쿼리의 모든 부분이 해결되었는지 확인하는 데 도움이 됩니다.</li></ul><h2>도전 과제</h2><p>에이전트 RAG 시스템은 적응성, 정확성, 동적 추론 측면에서 상당한 이점을 제공하지만, 설계 및 구현 단계에서 해결해야 하는 특정 과제를 안고 있기도 합니다. 주요 과제 중 일부는 다음과 같습니다:</p><ul><li><strong>복잡한 워크플로</strong>: 더 많은 상담원과 의사 결정 포인트가 추가됨에 따라 전체 워크플로우가 점점 더 복잡해집니다. 이로 인해 런타임에 오류나 장애가 발생할 가능성이 높아질 수 있습니다. 가능하면 중복 상담원과 불필요한 의사 결정 지점을 제거하여 워크플로우를 간소화하는 데 우선순위를 두세요.</li><li><strong>확장성</strong>: 대규모 데이터 세트와 많은 쿼리 양을 처리하기 위해 에이전트 RAG 시스템을 확장하는 것은 어려울 수 있습니다. 효율적인 인덱싱, 캐싱 및 분산 처리 전략을 통합하여 규모에 맞게 성능을 유지하세요.</li><li><strong>오케스트레이션 및 컴퓨팅 오버헤드</strong>: 여러 에이전트가 포함된 워크플로를 실행하려면 고급 오케스트레이션이 필요합니다. 여기에는 병목 현상과 충돌을 방지하기 위한 신중한 스케줄링, 종속성 관리, 상담원 조정이 포함되며, 이 모든 것이 전반적인 시스템 복잡성을 가중시킵니다.</li><li><strong>평가의 복잡성</strong>: 각 단계마다 고유한 평가 전략이 필요하기 때문에 이러한 워크플로를 평가하는 데는 고유한 어려움이 있습니다. 예를 들어 RAG 단계에서는 검색된 문서의 관련성과 완전성을 평가해야 하며, 생성된 요약의 품질과 정확성을 확인해야 합니다. 마찬가지로 쿼리 재구성의 효과는 재작성된 쿼리가 검색 결과를 개선하는지 여부를 판단하기 위한 별도의 평가 로직이 필요합니다.</li></ul><h2>결론</h2><p>이 블로그 게시물에서는 에이전트 RAG의 개념을 소개하고 에이전트 AI의 자율 기능을 통합하여 기존 RAG 프레임워크를 개선하는 방법을 강조했습니다. 에이전트 RAG의 핵심 기능을 살펴보고 실제 예제를 통해 이러한 기능을 시연했으며, Elasticsearch를 벡터 저장소로 사용하고 LangChain을 에이전트 프레임워크를 생성하는 뉴스 어시스턴트를 구축했습니다.</p><p>또한 에이전트 RAG 파이프라인을 설계하고 구현할 때 고려해야 할 모범 사례와 주요 과제에 대해서도 논의했습니다. 이러한 인사이트는 개발자가 검색, 추론 및 의사 결정을 효과적으로 결합하는 강력하고 확장 가능하며 효율적인 에이전트 시스템을 만드는 데 도움을 주기 위한 것입니다.</p><h2>그럼 이제 무엇을 해야 할까요?</h2><p>우리가 구축한 워크플로우는 단순하여 개선과 실험을 위한 충분한 여지를 남겨두고 있습니다. 다양한 임베딩 모델을 실험하고 검색 전략을 개선하여 이를 개선할 수 있습니다. 또한 검색된 문서의 우선 순위를 다시 지정하는 에이전트를 통합하면 도움이 될 수 있습니다. 또 다른 탐색 영역은 에이전트 프레임워크에 대한 평가 전략을 개발하는 것으로, 특히 다양한 유형의 프레임워크에 적용 가능한 공통적이고 재사용 가능한 접근 방식을 식별하는 것입니다. 마지막으로, 더 크고 복잡한 데이터 세트에서 이러한 프레임워크를 실험해 보세요.</p><p>그동안 비슷한 실험을 해보신 경험이 있으시다면 공유해 주시면 감사하겠습니다! <a href="https://ela.st/slack">커뮤니티 Slack 채널이나</a> <a href="https://discuss.elastic.co/c/security">토론 포럼을</a> 통해 자유롭게 피드백을 제공하거나 소통하세요.</p><h2>리소스</h2><ul><li><a href="https://arxiv.org/abs/2310.11511">Self-RAG: 자기 성찰을 통한 검색, 생성 및 비평 학습하기</a></li><li><a href="https://arxiv.org/abs/2501.09136">에이전트 검색-증강 세대: 에이전트 RAG에 대한 설문 조사</a></li></ul>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[번거로움 없는 하이브리드 검색: 검색기를 사용한 하이브리드 검색 간소화]]></title>
            <description><![CDATA[선형 및 RRF 검색기를 위한 다중 필드 쿼리 형식을 사용해 Elasticsearch에서 하이브리드 검색을 간소화하는 방법을 살펴보고, Elasticsearch 인덱스에 대한 사전 지식 없이도 쿼리를 생성하는 방법을 알아보세요.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/hybrid-search-multi-field-query-retrievers-elasticsearch</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/hybrid-search-multi-field-query-retrievers-elasticsearch</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Mike Pellegrini]]></dc:creator>
            <pubDate>Thu, 27 Nov 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/4494f98ae3958bbdbc6171df9677fc4d65ec5640-1536x1024.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p><a href="https://www.elastic.co/what-is/hybrid-search">하이브리드 검색은</a> <a href="https://www.elastic.co/search-labs/blog/lexical-and-semantic-search-with-elasticsearch#lexical-search---sparse-retrieval">어휘 검색의</a> 정확성과 속도에 시맨틱 <a href="https://www.elastic.co/what-is/semantic-search">검색의</a> 자연어 기능을 결합한 강력한 검색 방식으로 널리 알려져 있습니다. 그러나 실제로 적용하는 것은 까다로울 수 있으며, 종종 인덱스에 대한 깊은 지식이 필요하고 간단한 구성이 아닌 장황한 쿼리를 구성해야 합니다. 이 블로그에서는 <a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/retrievers#multi-field-query-format">선형 및 RRF 검색기를 위한 다중 필드 쿼리 형식이</a> 어떻게 하이브리드 검색을 더 간단하고 접근하기 쉽게 만들어 일반적인 골칫거리를 없애고 그 모든 기능을 더 쉽게 활용할 수 있게 해주는지 살펴봅니다. 또한 다중 필드 쿼리 형식을 통해 인덱스에 대한 사전 지식 없이도 하이브리드 검색 쿼리를 수행할 수 있는 방법을 검토합니다.</p><h2>점수 범위 문제</h2><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/c8b49153c47d2cdc233c0d2e440db04711d48ca5-1600x1600.jpg" style="width: 100%;" /><p>하이브리드 검색이 어려운 주요 이유 중 하나인 다양한 점수 범위를 살펴보기 위해 무대를 설정해 보겠습니다. 우리의 오랜 친구 <a href="https://www.elastic.co/elasticon/conf/2016/sf/improved-text-scoring-with-bm25">BM25는</a> 무한한 점수를 만들어냅니다. 즉, BM25는 0에 가까운 점수부터 (이론적으로) 무한대까지 다양한 점수를 생성할 수 있습니다. 반대로 <code>dense_vector</code> 필드에 대한 쿼리는 0과 1 사이의 점수를 생성합니다. 이 문제를 더욱 악화시키는 <code>semantic_text</code> 은 임베딩을 색인하는 데 사용되는 필드 유형을 난독화하므로 색인 및 추론 엔드포인트 구성에 대한 자세한 지식이 없으면 쿼리의 점수 범위를 알기 어려울 수 있습니다. 이는 어휘 검색 결과와 의미 검색 결과를 통합하려고 할 때 문제가 되는데, 의미 검색 결과가 더 관련성이 높더라도 어휘 검색 결과가 의미 검색 결과보다 우선할 수 있기 때문입니다. 이 문제에 대한 일반적인 해결책은 결과를 인터리빙하기 전에 점수를 정규화하는 것입니다. 이를 위한 두 가지 도구, <a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/retrievers/linear-retriever">선형</a> 검색기와 <a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/retrievers/rrf-retriever">RRF</a> 검색기가 있습니다.<br/></p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/264481268c8b6ac259e3c257b85431b513f16672-1077x586.png" style="width: 100%;" /><p><strong>RRF</strong> 검색기는 문서 순위를 관련성 측정값으로 사용하고 점수를 버리는 <a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/reciprocal-rank-fusion">RRF 알고리즘을</a> 적용합니다. 점수는 고려되지 않으므로 점수 범위 불일치는 문제가 되지 않습니다.</p><p><strong>리니어</strong> 리트리버는 선형 조합을 사용하여 문서의 최종 점수를 결정합니다. 여기에는 문서에 대한 각 구성 요소 쿼리의 점수를 가져와서 정규화한 다음 합산하여 총 점수를 생성하는 작업이 포함됩니다. 수학적으로 이 연산은 다음과 같이 표현할 수 있습니다:</p><pre><code>Total Score = 𝚺(N(Sx))</code></pre><p>여기서 <code>N</code> 은 정규화 함수이고 SX 는 쿼리 X 의 점수입니다. 정규화 함수는 각 쿼리의 점수를 동일한 범위를 사용하도록 변환하기 때문에 여기서 핵심적인 역할을 합니다. 리니어 리트리버에 대해 자세히 알아보려면 <a href="https://www.elastic.co/search-labs/blog/linear-retriever-hybrid-search">여기를</a> 참조하세요.</p><h2>분석하기</h2><p>사용자는 이러한 도구를 사용하여 효과적인 하이브리드 검색을 구현할 수 있지만 색인에 대한 약간의 지식이 필요합니다. 두 개의 필드가 있는 인덱스를 쿼리하는 리니어 리트리버의 예를 살펴보겠습니다:</p><pre><code>PUT linear_retriever_example
{
  "mappings": {
    "properties": {
      "semantic_text_field": { <1>
        "type": "semantic_text",
        "inference_id": ".multilingual-e5-small-elasticsearch"
      },
      "text_field": { <2>
        "type": "text"
      }
    }
  }
}</code></pre><p>1. <code>semantic_text_field</code> 는 텍스트 임베딩 모델인 <a href="https://www.elastic.co/docs/explore-analyze/machine-learning/nlp/ml-nlp-e5">E5를</a> 사용하는 <code>semantic_text</code> 필드입니다.</p><p><code>text_field</code> 는 표준 <code>text</code> 필드입니다.</p><pre><code>GET linear_retriever_example/_search
{
  "retriever": {
    "linear": {
      "retrievers": [
        {
          "retriever": {
            "standard": {
              "query": {
                "match": { <1>
                  "semantic_text_field": "foo"
                }
              }
            }
          },
          "normalizer": "minmax"
        },
        {
          "retriever": {
            "standard": {
              "query": {
                "match": {
                  "text_field": "foo"
                }
              }
            }
          },
          "normalizer": "minmax"
        }
      ]
    }
  }
}</code></pre><p>1. <code>semantic_text</code> 필드에서 <code>match</code> 쿼리를 사용하며, <a href="https://www.elastic.co/search-labs/blog/semantic-search-match-knn-sparse-vector#we-made-match-happen-in-semantic-search!">Elasticsearch 8.18/9.0에서 지원이 추가되었습니다</a>.</p><p><br/>쿼리를 구성할 때 <code>semantic_text_field</code> 은 텍스트 임베딩 모델을 사용하므로 이 쿼리에 대한 모든 쿼리는 0에서 1 사이의 점수를 생성한다는 점을 염두에 두어야 합니다. 또한 <code>text_field</code> 은 표준 <code>text</code> 필드이므로 이 필드로 쿼리하면 무제한 점수가 생성된다는 점도 알아야 합니다. 적절한 연관성을 가진 결과 집합을 만들려면 쿼리 점수를 결합하기 전에 정규화하는 리트리버를 사용해야 합니다. 이 예에서는 각 쿼리의 점수를 0과 1 사이의 값으로 정규화하는 <code>minmax</code> 정규화와 함께 선형 검색기를 사용합니다.</p><p>이 예제의 쿼리 구성은 두 개의 필드만 관련되어 있기 때문에 매우 간단합니다. 그러나 더 많은 필드와 다양한 유형이 추가되면 매우 빠르게 복잡해질 수 있습니다. 이는 효과적인 하이브리드 검색 쿼리를 작성하려면 쿼리 대상 인덱스에 대한 심층적인 지식이 필요한 경우가 많으므로 구성 요소 쿼리 점수를 조합하기 전에 적절하게 정규화해야 한다는 것을 보여줍니다. 이는 하이브리드 검색의 광범위한 채택을 가로막는 장벽이 됩니다.</p><h3>쿼리 그룹화</h3><p>예제를 확장해 보겠습니다: 하나의 <code>text</code> 필드와 두 개의 <code>semantic_text</code> 필드를 쿼리하려면 어떻게 해야 할까요? 다음과 같이 쿼리를 작성할 수 있습니다:</p><pre><code>GET linear_retriever_example/_search
{
  "retriever": {
    "linear": {
      "retrievers": [
        {
          "retriever": {
            "standard": {
              "query": {
                "semantic": {
                  "field": "semantic_text_field_1",
                  "query": "foo"
                }
              }
            }
          },
          "normalizer": "minmax"
        },
        {
          "retriever": {
            "standard": {
              "query": {
                "semantic": {
                  "field": "semantic_text_field_2",
                  "query": "foo"
                }
              }
            }
          },
          "normalizer": "minmax"
        },
        {
          "retriever": {
            "standard": {
              "query": {
                "match": {
                  "text_field": "foo"
                }
              }
            }
          },
          "normalizer": "minmax"
        }
      ]
    }
  }
}</code></pre><p>겉으로 보기에는 좋아 보이지만 잠재적인 문제가 있습니다. 이제 <code>semantic_text</code> 필드 매치가 전체 점수의 ⅔를 차지합니다:</p><pre><code>Total Score = N(semantic_text_field_1 score) + N(semantic_text_field_2 score) + N(text_field score)</code></pre><p>이는 불균형한 점수를 만들기 때문에 원하지 않을 수도 있습니다. 필드가 3개만 있는 이 예제에서는 그 영향이 눈에 띄지 않을 수 있지만, 더 많은 필드가 쿼리되면 문제가 됩니다. 예를 들어, 대부분의 인덱스에는 시맨틱 필드보다 훨씬 더 많은 어휘 필드가 포함되어 있습니다(예 <code>dense_vector</code>, <code>sparse_vector</code>, 또는 <code>semantic_text</code>). 위의 패턴을 사용하여 어휘 필드 9개와 의미 필드 1개가 있는 인덱스를 쿼리한다면 어떨까요? 어휘 일치 항목이 점수의 90% 을 차지하여 의미론적 검색의 효과를 무색하게 만들었습니다.</p><p>이 문제를 해결하는 일반적인 방법은 쿼리를 어휘 및 의미 범주로 그룹화하고 두 범주에 균등하게 가중치를 부여하는 것입니다. 이렇게 하면 어느 한 카테고리가 총점을 지배하는 것을 방지할 수 있습니다.</p><p>이를 실천에 옮겨 보겠습니다. 이 예제에서 리니어 리트리버를 사용할 때 그룹화된 쿼리 접근 방식은 어떤 모습일까요?</p><pre><code>GET linear_retriever_example/_search
{
  "retriever": {
    "linear": {
      "retrievers": [
        {
          "retriever": {
            "linear": {
              "retrievers": [
                {
                  "retriever": {
                    "standard": {
                      "query": {
                        "semantic": {
                          "field": "semantic_text_field_1",
                          "query": "foo"
                        }
                      }
                    }
                  },
                  "normalizer": "minmax"
                },
                {
                  "retriever": {
                    "standard": {
                      "query": {
                        "semantic": {
                          "field": "semantic_text_field_2",
                          "query": "foo"
                        }
                      }
                    }
                  },
                  "normalizer": "minmax"
                }
              ]
            }
          },
          "normalizer": "minmax"
        },
        {
          "retriever": {
            "standard": {
              "query": {
                "match": {
                  "text_field": "foo"
                }
              }
            }
          },
          "normalizer": "minmax"
        }
      ]
    }
  }
}</code></pre><p>와, 점점 장황해지네요! 전체 쿼리를 살펴보기 위해 위아래로 여러 번 스크롤해야 했을 수도 있습니다! 여기서는 두 가지 수준의 정규화를 사용하여 쿼리 그룹을 생성합니다. 수학적으로는 다음과 같이 표현할 수 있습니다:</p><pre><code>Total Score = N(N(semantic_text_field_1 score) + N(semantic_text_field_2 score)) + N(text_field score)</code></pre><p>이 두 번째 정규화 수준은 <code>semantic_text</code> 필드와 <code>text</code> 필드에 대한 쿼리의 가중치가 균등하게 적용되도록 합니다. 이 예제에서는 어휘 필드가 하나뿐이므로 <code>text_field</code> 에 대한 두 번째 수준 정규화를 생략하여 <em>더</em> 자세한 설명을 생략했습니다.</p><p>이 쿼리 구조는 이미 다루기 힘든데다 세 개의 필드만 쿼리하고 있습니다. 더 많은 필드를 쿼리할수록 숙련된 검색 실무자라도 관리하기가 점점 더 어려워집니다.</p><h2>다중 필드 쿼리 형식</h2><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/dd04e1686076a574b717c1460acfe4eb79299208-1600x1600.jpg" style="width: 100%;" /><p>이 모든 것을 간소화하기 위해 Elasticsearch 8.19, 9.1 및 <a href="https://www.elastic.co/cloud/serverless">서버리스에서 선형</a> <a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/retrievers#multi-field-query-format">및 RRF 검색기에 대한 다중 필드 쿼리 형식을 추가했습니다.</a> 이제 위와 동일한 쿼리를 수행할 수 있습니다:</p><pre><code>GET linear_retriever_example/_search
{
  "retriever": {
    "linear": {
      "fields": [ "semantic_text_field_1", "semantic_text_field_2", "text_field" ],
      "query": "foo",
      "normalizer": "minmax"
    }
  }
}</code></pre><p>쿼리가 55줄에서 단 9줄로 줄어듭니다! Elasticsearch는 인덱스 매핑을 자동으로 사용합니다:</p><ul><li>쿼리되는 각 필드의 유형을 결정합니다.</li><li>각 필드를 어휘 또는 의미론적 범주로 그룹화합니다.</li><li>최종 점수에서 각 카테고리에 균등하게 가중치를 부여합니다.</li></ul><p>이를 통해 누구나 인덱스나 사용된 추론 엔드포인트에 대한 세부 정보를 몰라도 효과적인 하이브리드 검색 쿼리를 실행할 수 있습니다.</p><p>RRF를 사용하는 경우 순위가 관련성의 프록시로 사용되므로 <code>normalizer</code> 을 생략할 수 있습니다:</p><pre><code>GET rrf_retriever_example/_search
{
  "retriever": {
    "rrf": {
      "fields": [ "semantic_text_field_1", "semantic_text_field_2", "text_field" ],
      "query": "foo"
    }
  }
}</code></pre><h2>필드별 부스팅</h2><p>리니어 리트리버를 사용할 때 필드별 부스트를 적용하여 특정 필드에서 경기의 중요도를 조정할 수 있습니다. 예를 들어 <code>semantic_text</code> 필드 2개와 <code>text</code> 필드 2개 등 4개의 필드를 쿼리한다고 가정해 보겠습니다:</p><pre><code>GET linear_retriever_example/_search
{
  "retriever": {
    "linear": {
      "fields": [ "semantic_text_field_1", "semantic_text_field_2", "text_field_1", "text_field_2" ],
      "query": "foo",
      "normalizer": "minmax"
    }
  }
}</code></pre><p>기본적으로 각 필드는 해당 그룹(어휘 또는 의미)에서 동일하게 가중치가 부여됩니다. 점수 분석은 다음과 같습니다:</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/ba31c76189e3a1e5b1638437ccf0528aafec2598-1600x549.png" style="width: 100%;" /><p>즉, 각 필드는 총 점수의 25% %입니다.</p><p><code>field^boost</code> 구문을 사용하여 모든 필드에 필드별 부스트를 추가할 수 있습니다. <code>semantic_text_field_1</code> 와 <code>text_field_1</code> 에 2의 부스트를 적용해 보겠습니다:</p><pre><code>GET linear_retriever_example/_search
{
  "retriever": {
    "linear": {
      "fields": [ "semantic_text_field_1^2", "semantic_text_field_2", "text_field_1^2", "text_field_2" ]
      "query": "foo",
      "normalizer": "minmax"
    }
  }
}</code></pre><p>이제 점수 분석은 다음과 같습니다:</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/de20e555d52f914bf483a048d056f54f4fece757-1600x549.png" style="width: 100%;" /><p>각 쿼리 그룹은 여전히 동일하게 가중치가 적용되지만 이제 그룹 내 필드 가중치가 변경되었습니다:</p><ul><li><code>semantic_text_field_1</code> 시맨틱 쿼리 그룹 점수는 66%, 총 점수는 33% 입니다.</li><li><code>text_field_1</code> 는 어휘 쿼리 그룹 점수 66%, 총 점수 33% 입니다.</li></ul>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">ℹ️ 필드별 부스트가 적용되어도 총 점수 범위는 변경되지 않습니다. 이는 점수 정규화의 의도된 부작용으로, 어휘 및 의미론적 쿼리 점수가 서로 직접 비교 가능한 상태로 유지되도록 합니다.</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">ℹ️ 필드별 부스팅은 Elasticsearch 9.2+의 RRF 리트리버와 함께 사용할 수도 있습니다.</td>
                    </tr>
              </tbody>
            </table>
          <h3>와일드카드 해상도</h3><p><code>fields</code> 매개변수에 <code>*</code> 와일드카드를 사용하여 여러 필드를 일치시킬 수 있습니다. 위의 예를 계속 이어서, 이 쿼리는 기능적으로<code>emantic_text_field_1</code>, <code>semantic_text_field_2</code>, <code>text_field_1</code> 을 명시적으로 쿼리하는 것과 동일합니다:</p><pre><code>GET linear_retriever_example/_search
{
  "retriever": {
    "linear": {
      "fields": [ "semantic_text_field_*", "*_field_1" ],
      "query": "foo",
      "normalizer": "minmax"
    }
  }
}</code></pre><p><code>*_field_1</code> 패턴이 <code>text_field_1</code> 및 <code>semantic_text_field_1</code> 와 모두 일치한다는 점이 흥미롭습니다. 이는 자동으로 처리되며, 각 필드가 명시적으로 쿼리된 것처럼 쿼리가 실행됩니다. <code>semantic_text_field_1</code> 이 두 패턴과 모두 일치해도 괜찮습니다. 모든 필드 이름 일치 항목은 쿼리 실행 전에 중복이 제거됩니다.</p><p>와일드카드는 다양한 방법으로 사용할 수 있습니다:</p><ul><li>접두사 일치(예: <code>*_text_field</code>)</li><li>인라인 매칭(예: <code>semantic_*_field</code>)</li><li>접미사 일치(예: <code>semantic_text_field_*</code>)</li></ul><p><code>*_text_field_*</code> 와 같이 여러 와일드카드를 사용하여 위의 조합을 적용할 수도 있습니다.</p><h3>기본 쿼리 필드</h3><p>다중 필드 쿼리 형식을 사용하면 전혀 모르는 인덱스를 쿼리할 수도 있습니다. <code>fields</code> 매개변수를 생략하면 <a href="https://www.elastic.co/docs/reference/elasticsearch/index-settings/index-modules">index.query.default_field 인덱스 설정으로</a> 지정된 모든 필드를 쿼리합니다:</p><pre><code>GET linear_retriever_example/_search
{
  "retriever": {
    "linear": {
      "query": "foo",
      "normalizer": "minmax"
    }
  }
}</code></pre><p>기본적으로 <code>index.query.default_field</code> 은 <code>*</code> 으로 설정되어 있습니다. 이 와일드카드는 용어 쿼리를 지원하는 인덱스의 모든 필드 유형(대부분)으로 확인합니다. 예외는 있습니다:</p><ul><li><code>dense_vector</code> 필드</li><li><code>rank_vector</code> 필드</li><li>지오메트리 필드: <code>geo_point</code>, <code>shape</code></li></ul><p>이 기능은 타사에서 제공하는 색인에 대해 하이브리드 검색 쿼리를 수행하려는 경우에 특히 유용합니다. 다중 필드 쿼리 형식을 사용하면 간단한 방법으로 적절한 쿼리를 실행할 수 있습니다. <code>fields</code> 매개변수만 제외하면 해당되는 모든 필드가 쿼리됩니다.</p><h2>결론</h2><p>점수 범위 문제로 인해 효과적인 하이브리드 검색을 구현하기가 어려울 수 있으며, 특히 쿼리되는 인덱스나 사용 중인 추론 엔드포인트에 대한 인사이트가 제한적인 경우 더욱 그렇습니다. 선형 및 RRF 검색기를 위한 다중 필드 쿼리 형식은 자동화된 쿼리 그룹화 기반의 하이브리드 검색 방식을 간단하고 접근하기 쉬운 API로 패키징하여 이러한 어려움을 덜어줍니다. 필드별 부스팅, 와일드카드 해상도 및 기본 쿼리 필드와 같은 추가 기능을 통해 다양한 사용 사례를 포괄하는 기능을 확장할 수 있습니다.</p><h2>지금 바로 다중 필드 쿼리 형식을 사용해 보세요.</h2><p><a href="https://www.elastic.co/docs/deploy-manage/deploy/elastic-cloud/create-serverless-project">무료</a> 체험 판으로 완전 <a href="https://www.elastic.co/cloud/serverless">관리형 Elasticsearch 서버리스</a> 프로젝트에서 다중 필드 쿼리 형식의 선형 및 RRF 검색기를 확인해 보세요. 8.19 &amp; 9.1부터 스택 버전으로도 사용할 수 있습니다.</p><p>명령 한 번으로 로컬 환경에서 몇 분 안에 시작할 수 있습니다:</p><pre><code>curl -fsSL https://elastic.co/start-local | sh</code></pre><p></p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Elastic 에이전트 빌더와 GPT-OSS로 HR용 AI 에이전트 구축하기]]></title>
            <description><![CDATA[Elastic 에이전트 빌더와 GPT-OSS를 사용해 직원 인사 데이터에 대한 자연어 쿼리에 답변할 수 있는 AI 에이전트를 구축하는 방법을 알아보세요.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/build-an-ai-agent-hr-elastic-agent-builder-gpt-oss</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/build-an-ai-agent-hr-elastic-agent-builder-gpt-oss</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Tomás Murúa]]></dc:creator>
            <pubDate>Wed, 26 Nov 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/05d2d0513fff67d975f9223d75108aa9f50646bc-1600x914.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<h2>소개</h2><p>이 문서에서는 <a href="https://openai.com/index/introducing-gpt-oss/">GPT-OSS와</a> Elastic 에이전트 빌더를 사용해 HR용 AI 에이전트를 구축하는 방법을 설명합니다. 상담원은 OpenAI, Anthropic 또는 외부 서비스에 데이터를 보내지 않고도 질문에 답변할 수 있습니다.</p><p>LM Studio를 사용하여 GPT-OSS를 로컬로 서비스하고 이를 Elastic Agent Builder에 연결합니다.</p><p>이 글을 마치면 정보와 모델을 완벽하게 제어하면서 직원 데이터에 대한 자연어 질문에 답할 수 있는 맞춤형 AI 에이전트를 보유하게 됩니다.</p><h2>필수 구성 요소</h2><p>이 문서에는 다음이 필요합니다:</p><ul><li><a href="https://www.elastic.co/cloud">Elastic Cloud</a> 호스팅 9.2, 서버리스 또는 <a href="https://www.elastic.co/docs/deploy-manage/deploy/self-managed/local-development-installation-quickstart">로컬</a> 배포</li><li>32GB RAM이 장착된 머신 권장(GPT-OSS 20B의 경우 최소 16GB)</li><li><a href="https://lmstudio.ai/">LM Studio</a> 설치</li><li><a href="https://www.docker.com/products/docker-desktop/">도커 데스크톱</a> 설치</li></ul><h2>GPT-OSS를 사용하는 이유는 무엇인가요?</h2><p>로컬 LLM을 사용하면 자체 인프라에 배포하고 필요에 맞게 미세 조정할 수 있습니다. 이 모든 기능을 사용하면서 모델과 공유하는 데이터를 제어할 수 있으며, 물론 외부 제공업체에 라이선스 비용을 지불할 필요가 없습니다.</p><p>OpenAI는 개방형 모델 생태계를 위한 노력의 일환으로 2025년 8월 5일에 <a href="https://openai.com/index/introducing-gpt-oss/">GPT-OSS를 출시했습니다</a>.</p><p>20B 매개변수 모델은 다음을 제공합니다:</p><ul><li><strong>도구 사용 기능</strong></li><li><strong>효율적인 추론</strong></li><li><strong>OpenAI SDK 호환</strong></li><li><strong>에이전트 워크플로와 호환</strong></li></ul><p>벤치마크 비교:</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/29160e3345352088e8213297630882f252b00c47-1600x680.png" style="width: 100%;" /><h2>솔루션 아키텍처</h2><p>아키텍처는 전적으로 로컬 컴퓨터에서 실행됩니다. Elastic(Docker에서 실행)은 LM Studio를 통해 로컬 LLM과 직접 통신하며, Elastic 에이전트 빌더는 이 연결을 사용하여 직원 데이터를 쿼리할 수 있는 사용자 정의 AI 에이전트를 생성합니다.</p><p>자세한 내용은 이 <a href="https://www.elastic.co/docs/solutions/observability/connect-to-own-local-llm">문서를</a> 참조하세요.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/a4a886750ff25fa8bb7aefc7448161e52cf73ed3-1600x896.png" style="width: 100%;" /><h2>HR용 AI 에이전트 구축: 단계</h2><p>구현을 5단계로 나눠서 설명하겠습니다:</p><ol><li>로컬 모델로 LM 스튜디오 구성</li><li>Docker로 로컬 Elastic 배포</li><li>Elastic에서 OpenAI 커넥터 생성</li><li>Elasticsearch에 직원 데이터 업로드</li><li>AI 에이전트 빌드 및 테스트</li></ol><h2>1단계: GPT-OSS 20B로 LM Studio 구성하기</h2><p>LM Studio는 컴퓨터에서 로컬로 대규모 언어 모델을 실행할 수 있는 사용자 친화적인 애플리케이션입니다. OpenAI 호환 API 서버를 제공하므로 복잡한 설정 과정 없이 Elastic과 같은 도구와 쉽게 통합할 수 있습니다. 자세한 내용은 <a href="https://lmstudio.ai/docs/app">LM Studio 문서를</a> 참조하세요.</p><p>먼저 공식 웹사이트에서 <span class="unknown__pt__mark__51566e628851">LM Studio를</span> 다운로드하여 설치합니다. 설치가 완료되면 애플리케이션을 엽니다.</p><h3>LM Studio 인터페이스에서:</h3><ol><li>검색 탭으로 이동하여 &quot;GPT-OSS&quot;를 검색합니다.</li><li>OpenAI에서 <code>openai/gpt-oss-20b</code></li><li>다운로드를 클릭하세요.</li></ol><p>이 모델의 크기는 약 <strong>12.10GB입니다</strong>. 인터넷 연결 상태에 따라 다운로드하는 데 몇 분 정도 걸릴 수 있습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/5d01bc4dcb377b5259fc6b521fe2425a31b90ca4-1312x872.png" style="width: 100%;" /><h4>모델이 다운로드되면</h4><ol><li>로컬 서버 탭으로 이동합니다.</li><li>OPENAI/GPT-OSS-20B를 선택합니다.</li><li>기본 포트 1234 사용</li><li>오른쪽 패널에서 <strong>로드로 </strong>이동하여 컨텍스트 길이를 <strong>40K</strong> 이상으로 설정합니다.</li></ol><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/e546033f916381647b876815b2c1f1ae2a08365f-326x337.png" style="width: 100%;" /><p>5. 서버 시작을 클릭합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/28ee78a3caa84d14e04db3d42f30acbe4d4d005a-1312x872.png" style="width: 100%;" /><p>서버가 실행 중이면 이 메시지가 표시되어야 합니다.</p><pre><code>[LM STUDIO SERVER] Success! HTTP server listening on port 1234
[LM STUDIO SERVER] Supported endpoints:
[LM STUDIO SERVER] ->	GET  http://localhost:1234/v1/models
[LM STUDIO SERVER] ->	POST http://localhost:1234/v1/responses
[LM STUDIO SERVER] ->	POST http://localhost:1234/v1/chat/completions
[LM STUDIO SERVER] ->	POST http://localhost:1234/v1/completions
[LM STUDIO SERVER] ->	POST http://localhost:1234/v1/embeddings
Server started.</code></pre><h2>2단계: Docker로 로컬 Elastic 배포하기</h2><p>이제 Docker를 사용해 로컬에서 Elasticsearch와 Kibana를 설정하겠습니다. Elastic은 전체 설정 프로세스를 처리하는 편리한 스크립트를 제공합니다. 자세한 내용은 <a href="https://www.elastic.co/docs/deploy-manage/deploy/self-managed/local-development-installation-quickstart">공식 문서를</a> 참조하세요.</p><h3>시작-로컬 스크립트 실행</h3><p>터미널에서 다음 명령을 실행합니다:</p><pre><code>curl -fsSL https://elastic.co/start-local | sh</code></pre><p>이 스크립트는</p><ul><li>Elasticsearch와 Kibana 다운로드 및 구성하기</li><li>Docker Compose를 사용하여 두 서비스 모두 시작</li><li>30일 플래티넘 평가판 라이선스 자동 활성화</li></ul><h3>예상 출력</h3><p>다음 메시지가 표시될 때까지 기다렸다가 표시된 비밀번호와 API 키를 저장하세요. Kibana에 액세스하려면 이 키가 필요합니다:</p><pre><code>🎉 Congrats, Elasticsearch and Kibana are installed and running in Docker!
🌐 Open your browser at http://localhost:5601
   Username: elastic
   Password: KSUlOMNr
🔌 Elasticsearch API endpoint: http://localhost:9200
🔑 API key: cnJGX0pwb0JhOG00cmNJVklUNXg6cnNJdXZWMnM4bncwMllpQlFlUTlWdw==
Learn more at https://github.com/elastic/start-local</code></pre><h3>Kibana에 액세스</h3><p>브라우저를 열고 다음으로 이동합니다:</p><pre><code>http://localhost:5601</code></pre><p>터미널 출력에서 얻은 자격 증명을 사용하여 로그인합니다.</p><h3>상담원 빌더 사용</h3><p>Kibana에 로그인한 후, <strong>관리 </strong>&gt;<strong> AI </strong>&gt;<strong> 에이전트 빌더로 </strong>이동하여 에이전트 빌더를 활성화합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/92e104cb846c20d875865ded8a3d37f5c7daae9b-1491x1528.png" style="width: 100%;" /><h2>3단계: Elastic에서 OpenAI 커넥터 만들기</h2><p>이제 로컬 LLM을 사용하도록 Elastic을 구성하겠습니다.</p><h3>액세스 커넥터</h3><ol><li>Kibana에서</li><li><strong>프로젝트 설정으로</strong> 이동 &gt; <strong>관리</strong></li><li><strong>알림 및 인사이트에서</strong> <strong>커넥터를</strong>선택합니다.</li><li>커넥터 만들기를 클릭합니다.</li></ol><h3>커넥터 구성</h3><p>커넥터 목록에서 <strong>OpenAI를</strong> 선택합니다. LM Studio는 OpenAI SDK를 사용하므로 호환성이 뛰어납니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/5ac87042e086c7a2bd47a8039e646ec831f0dcc6-923x974.png" style="width: 100%;" /><p>이 값으로 필드를 채웁니다:</p><ul><li><strong>커넥터 이름: </strong>LM Studio - GPT-OSS 20B</li><li><strong>OpenAI 제공업체를 선택합니다: </strong>기타(OpenAI 호환 서비스)</li><li><strong>URL: </strong><code>http://host.docker.internal:1234/v1/chat/completions</code></li><li><strong>기본 모델: </strong>OPENAI/GPT-OSS-20B</li><li><strong>API 키:</strong> testkey-123(LM Studio 서버는 인증이 필요하지 않으므로 어떤 텍스트도 작동합니다.)</li></ul><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/2084ac32fcf1fb810c8b54ecab1c85a1e3e8905b-672x1302.png" style="width: 100%;" /><p>구성을 완료하려면 <strong>저장 &amp; 테스트를</strong> 클릭합니다.</p><p><strong>중요:</strong> 상담원 빌더가 제대로 작동하려면 &#x27;<strong>네이티브 함수 호출 사용</strong>&#x27;을 켜야 합니다. 이 기능을 활성화하지 않으면 <strong><code>No tool calls found in the response</code></strong> 오류가 발생합니다.</p><h3>연결 테스트</h3><p>Elastic은 자동으로 연결을 테스트합니다. 모든 것이 올바르게 구성되면 다음과 같은 성공 메시지가 표시됩니다:</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/f567d767f1969c4730c1daa92f651789dc3742ac-1042x812.png" style="width: 100%;" /><p>대응:</p><pre><code>{
  "status": "ok",
  "data": {
    "id": "chatcmpl-flj9h0hy4wcx4bfson00an",
    "object": "chat.completion",
    "created": 1761189456,
    "model": "openai/gpt-oss-20b",
    "choices": [
      {
        "index": 0,
        "message": {
          "role": "assistant",
          "content": "Hello! 👋 How can I assist you today?",
          "reasoning": "Just greet.",
          "tool_calls": []
        },
        "logprobs": null,
        "finish_reason": "stop"
      }
    ],
    "usage": {
      "prompt_tokens": 69,
      "completion_tokens": 23,
      "total_tokens": 92
    },
    "stats": {},
    "system_fingerprint": "openai/gpt-oss-20b"
  },
  "actionId": "ee1c3aaf-bad0-4ada-8149-118f52dad757"
}</code></pre><h2>4단계: 직원 데이터를 Elasticsearch에 업로드하기</h2><p>이제 <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/gpt-oss-with-elasticsearch/hr-employees-bulk.json">HR 직원 데이터 세트를</a> 업로드하여 상담원이 민감한 데이터로 어떻게 작업하는지 보여드리겠습니다. 이 구조로 가상의 데이터 집합을 생성했습니다.</p><h3>데이터 세트 구조</h3><pre><code>{
  "employee_id": "0f4dce68-2a09-4cb1-b2af-6bcb4821539b",
  "full_name": "Daffi Stiebler",
  "email": "lscutchings0@huffingtonpost.com",
  "date_of_birth": "1975-06-20T15:39:36Z",
  "hire_date": "2025-07-28T00:10:45Z",
  "job_title": "Physical Therapy Assistant",
  "department": "HR",
  "salary": "108455",
  "performance_rating": "Needs Improvement",
  "years_of_experience": 2,
  "skills": "Java",
  "education_level": "Master's Degree",
  "manager": "Carl MacGibbon",
  "emergency_contact": "Leigha Scutchings",
  "home_address": "5571 6th Park"
}</code></pre><h3>매핑으로 인덱스 만들기</h3><p>먼저 적절한 매핑으로 인덱스를 생성합니다. 일부 주요 필드에 <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/semantic-text">semantic_text</a> 필드를 사용하여 인덱스에 시맨틱 검색 기능을 사용할 수 있습니다.</p><pre><code>​​PUT hr-employees
{
  "mappings": {
    "properties": {
      "@timestamp": {
        "type": "date"
      },
      "employee_id": {
        "type": "keyword"
      },
      "full_name": {
        "type": "text",
        "copy_to": "employee_semantic"
      },
      "email": {
        "type": "keyword"
      },
      "date_of_birth": {
        "type": "date",
        "format": "iso8601"
      },
      "hire_date": {
        "type": "date",
        "format": "iso8601"
      },
      "job_title": {
        "type": "text",
        "copy_to": "employee_semantic"
      },
      "department": {
        "type": "text",
        "copy_to": "employee_semantic"
      },
      "salary": {
        "type": "double"
      },
      "performance_rating": {
        "type": "text",
        "copy_to": "employee_semantic"
      },
      "years_of_experience": {
        "type": "long"
      },
      "skills": {
        "type": "text",
        "copy_to": "employee_semantic"
      },
      "education_level": {
        "type": "text",
        "copy_to": "employee_semantic"
      },
      "manager": {
        "type": "text",
        "copy_to": "employee_semantic"
      },
      "emergency_contact": {
        "type": "keyword"
      },
      "home_address": {
        "type": "keyword"
      },
      "employee_semantic": {
        "type": "semantic_text"
      }
    }
  }
}</code></pre><h3>대량 API로 색인 생성</h3><p><a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/gpt-oss-with-elasticsearch/hr-employees-bulk.json">데이터 세트를</a> 복사하여 Kibana의 개발 도구에 붙여넣고 실행합니다:</p><pre><code>POST hr-employees/_bulk
{"index": {}}
{"employee_id": "57728b91-e5d7-4fa8-954a-2384040d3886", "full_name": "Filide Gane", "email": "vhallahan1@booking.com", "job_title": "Business Systems Development Analyst", "department": "Marketing", "salary": "$52330.27", "performance_rating": "Meets Expectations", "years_of_experience": 12, "skills": "Java", "education_level": "Bachelor's Degree", "date_of_birth": "2000-02-07T16:49:32Z", "hire_date": "2023-11-07T13:03:16Z", "manager": "Freedman Kings", "emergency_contact": "Vilhelmina Hallahan", "home_address": "75 Dennis Junction"}
{"index": {}}
{"employee_id": "...", ...}</code></pre><h3>데이터 확인</h3><p>쿼리를 실행하여 확인합니다:</p><pre><code>GET hr-employees/_search</code></pre><h2>5단계: AI 에이전트 빌드 및 테스트</h2><p>모든 구성이 완료되었으면 이제 Elastic 에이전트 빌더를 사용해 사용자 정의 AI 에이전트를 빌드할 차례입니다. 자세한 내용은 <a href="https://www.elastic.co/docs/solutions/search/agent-builder/get-started">Elastic 설명서를</a> 참조하세요.</p><h3>커넥터 추가</h3><p>새 에이전트를 생성하기 전에, 기본 커넥터가 <a href="https://www.elastic.co/docs/reference/kibana/connectors-kibana/elastic-managed-llm">Elastic Managed LLM이므로</a> <code>LM Studio - GPT-OSS 20B</code> 이라는 사용자 정의 커넥터를 사용하도록 에이전트 빌더를 설정해야 합니다. 이를 위해 <strong>프로젝트 설정</strong> &gt; <strong>관리</strong> &gt; <strong>GenAI 설정으로</strong> 이동한 다음, 생성한 프로젝트를 선택하고 <strong>저장을</strong> 클릭합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/11e830c3e2fb4c298b020c928fa5422f3397ba08-1600x1152.png" style="width: 100%;" /><h3>에이전트 빌더 액세스</h3><ol><li><strong>상담원으로</strong>이동</li><li><strong>새 상담원 만들기를</strong>클릭합니다.</li></ol><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/c1e60541563650163f972ac9088dc1ed1de759a7-1600x1054.png" style="width: 100%;" /><h3>상담원 구성하기</h3><p>새 상담원을 만들 때 필수 필드는 <strong>상담원 ID</strong>, <strong>표시 이름</strong> 및 <strong>표시 지침입니다</strong>.</p><p>하지만 시스템 프롬프트와 비슷하지만 사용자 지정 상담원을 위해 상담원이 어떻게 행동하고 툴과 상호 작용할지 안내하는 사용자 지정 지침과 같은 더 많은 사용자 지정 옵션이 있습니다. 레이블은 상담원, 아바타 색상 및 아바타 심볼을 정리하는 데 도움이 됩니다.</p><p>데이터 집합을 기반으로 상담원에게 선택한 것은<br/><br/><strong>상담원 ID입니다</strong>: <code>hr_assistant</code></p><p><strong>사용자 지정 지침:</strong></p><pre><code>You are an HR Analytics Assistant that helps answer questions about employee data.
When responding to queries:
- Provide clear, concise answers
- Include relevant employee details (name, department, salary, skills)
- Format monetary values with currency symbols
- Be professional and maintain data confidentiality</code></pre><p><br/>레이블: <code>Human Resources</code> 및 <code>GPT-OSS</code></p><p>표시 이름: <code>HR Analytics Assistant</code></p><p>설명을 표시합니다:</p><pre><code>A specialized AI assistant for Human Resources that helps analyze employee data, compensation, performance metrics, and talent management. Ask questions about employees, departments, salaries, or performance analytics.</code></pre><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/f94bb2bf08497e5e756ca76b30a3a51f42927756-1424x1217.png" style="width: 100%;" /><p>여기에 모든 데이터가 있으면 새 상담원 <strong>저장을</strong> 클릭할 수 있습니다.</p><h3>에이전트 테스트</h3><p>이제 직원 데이터에 대해 자연어 질문을 하면 GPT-OSS 20B가 의도를 파악하고 적절한 답변을 생성합니다.</p><h4>프롬프트:</h4><pre><code>Which employee is the one with the highest salary in the hr-employees index?</code></pre><h4>답변:</h4><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/94ad19f80b96304028a59f60beca51dfc9aecc8a-899x631.png" style="width: 100%;" /><p>에이전트 프로세스였습니다:</p><p>1. GPT-OSS 커넥터를 사용하여 질문 이해하기</p><p>2. (기본 제공 도구 또는 사용자 정의 <a href="https://www.elastic.co/docs/reference/query-languages/esql">ES|QL을</a> 사용하여) 적절한 Elasticsearch 쿼리를 생성합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/6f2961d0d1b97475f6dda300acee84da540938e6-844x466.png" style="width: 100%;" /><p>3. 일치하는 직원 기록 검색</p><p>4. 적절한 서식을 사용하여 자연어로 결과 표시</p><p>기존의 어휘 검색과 달리 GPT-OSS 기반 에이전트는 의도와 문맥을 이해하므로 정확한 필드 이름이나 쿼리 구문을 몰라도 정보를 쉽게 찾을 수 있습니다. 상담원의 사고 과정에 대한 자세한 내용은 이 <a href="https://www.elastic.co/search-labs/blog/ai-agent-builder-experiments-performance">문서를</a> 참조하세요.</p><h2>결론</h2><p>이 문서에서는 Elastic의 에이전트 빌더를 사용해 로컬에서 실행 중인 OpenAI GPT-OSS 모델에 연결하기 위해 사용자 정의 AI 에이전트를 구축했습니다. 이 아키텍처는 로컬 머신에 Elastic과 LLM을 모두 배포함으로써 외부 서비스로 정보를 전송하지 않고도 데이터를 완벽하게 제어하면서 생성형 AI 기능을 활용할 수 있게 해줍니다.</p><p>실험으로 GPT-OSS 20B를 사용했지만, 공식적으로 Elastic 에이전트 빌더에 권장되는 모델은 <a href="https://www.elastic.co/docs/solutions/search/agent-builder/models#recommended-models">여기를</a> 참조하세요. 고급 추론 기능이 필요한 경우, 로컬에서 실행하려면 더 높은 사양의 컴퓨터가 필요하지만 복잡한 시나리오에서 더 나은 성능을 발휘하는 <a href="https://huggingface.co/openai/gpt-oss-120b">120B 매개변수 변형도</a> 있습니다. 자세한 내용은 <a href="https://openai.com/open-models/">OpenAI 공식 문서를</a> 참조하세요.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[상위 Elastic 에이전트 빌더 프로젝트와 Cal Hacks 12.0에서 얻은 교훈]]></title>
            <description><![CDATA[Cal Hacks 12.0에서 최고의 Elastic 에이전트 빌더 프로젝트를 살펴보고 서버리스, ES|QL 및 에이전트 아키텍처에 대한 기술적 요점을 자세히 알아보세요.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/agent-builder-projects-learnings-cal-hacks-12-0</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/agent-builder-projects-learnings-cal-hacks-12-0</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[JD Armada]]></dc:creator>
            <pubDate>Tue, 25 Nov 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/8ba034a6f19b50521f541b8131756a8acdb52975-1280x960.jpg?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<a href="https://www.youtube.com/watch/GUI0WXNZduo">Watch video on YouTube</a><p>몇 주 전, 전 세계에서 2,000명 이상의 참가자가 모인 대규모 오프라인 해커톤 중 하나인 <a href="https://cal-hacks-12-0.devpost.com/">Cal Hacks 12.0을</a> 후원할 수 있는 놀라운 기회를 가졌습니다. 서버리스에서 Elastic 에이전트 빌더를 가장 잘 활용할 수 있는 전용 상금 트랙을 제공했는데, 그 반응은 놀라웠습니다. 단 36시간 만에 산불 인텔리전스 도구 구축부터 StackOverflow 유효성 검사기까지 에이전트 빌더를 창의적인 방식으로 사용한 29개의 제출물이 접수되었습니다.</p><p>인상적인 프로젝트 외에도 Cal Hacks 12.0의 경험은 Stack을 처음 접하는 개발자들의 빠르고 여과 없는 피드백이라는 또 다른 가치 있는 것을 얻게 해 주었습니다. 해커톤은 촉박한 일정, 사전 정보 부족, 예측할 수 없는 장애물(악명 높은 와이파이 중단 등)이 있는 독특한 압박 테스트입니다. 이를 통해 개발자 환경의 장점과 개선이 필요한 부분을 정확히 파악할 수 있습니다. 개발자들이 점점 더 LLM 기반 워크플로우를 통해 새로운 방식으로 Elastic Stack과 상호 작용함에 따라 이는 이제 더욱 중요해졌습니다. 이 블로그 게시물에서는 참가자들이 에이전트 빌더로 구축한 내용과 그 과정에서 배운 점을 자세히 살펴봅니다.</p><h2>수상 프로젝트</h2><h3>1등 에이전트 오버플로</h3><p>스택 오버플로가 LLM 및 에이전트 시대에 맞게 재구축되었습니다.</p><p><a href="https://devpost.com/software/agentoverflow">여기에서</a> 에이전트오버플로에 대해 자세히 알아보세요.</p><p>에이전트오버플로는 대부분의 AI 개발자가 직면하는 문제를 해결합니다: LLM이 환각을 일으키고, 채팅 기록이 사라지고, 개발자가 동일한 문제를 다시 해결하느라 시간을 낭비하는 등의 문제를 해결합니다.</p><p>에이전트오버플로는 실제 문제 해결 쌍을 캡처, 검증 및 재표출하여 개발자가 환각의 소용돌이를 끊고 더 빠르게 출시할 수 있도록 지원합니다.</p><h4>작동 방식:</h4><p><strong>1. JSON - &quot;솔루션 스키마&quot; 공유.</strong></p><p>Claude 공유에서 클릭 한 번으로 다음을 포함하는 구조화된 형식인 공유 솔루션 JSON을 스크랩, 추출 및 조립합니다:</p><ul><li>문제</li><li>컨텍스트</li><li>코드</li><li>태그</li><li>검증된 솔루션 단계.</li></ul><p>유효성 검사기(LAVA)가 구조를 검사하고 적용하면 사용자가 한 줄의 추가 컨텍스트를 추가하면 Elasticsearch 내에 저장되고 색인됩니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/45a3e96f4474050a855419628c2a7338bb12c706-1600x877.png" style="width: 100%;" /><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/2d65cb154d8ee32fc96ff17dfa5b0bf2636e3777-1600x1002.png" style="width: 100%;" /><p><strong>2. 솔루션 찾기</strong></p><p>문제가 발생하면 <code>Find Solution</code> 을 클릭하면 AgentOverflow가 현재 대화를 스크랩하여 쿼리를 작성하고 이를 사용하여 하이브리드 Elasticsearch 검색을 실행하여 결과를 표시합니다:</p><ul><li>순위가 매겨진 커뮤니티 검증 수정 사항</li><li>원래 문제를 해결한 정확한 안내 메시지</li></ul><p>이를 통해 개발자는 현재 세션을 빠르게 복사, 붙여넣기, 차단 해제할 수 있습니다.</p><p><strong>3. MCP - LLM을 위한 컨텍스트 주입</strong></p><p>MCP(모델 컨텍스트 프로토콜)를 통해 Elasticsearch 내에 저장된 구조화된 솔루션에 연결하면 별도의 노이즈 없이 런타임에 높은 신호 컨텍스트(코드, 로그, 구성, 이전 수정 사항)를 LLM에 공급할 수 있습니다.</p><p>에이전트 오버플로에서는 에이전트 빌더와 Elasticsearch를 사용해 관련 컨텍스트를 LLM에 주입하는 구조화된 메모리 계층으로 사용합니다. 이를 통해 수동적인 챗봇에서 상황 인식 문제 해결사로 변신합니다.</p><h3>준우승 MarketMind</h3><p>6개의 Elastic 에이전트가 제공하는 시장 에너지에 대한 실시간 해석 가능한 보기.</p><p>마켓마인드에 대한 자세한 내용은 <a href="https://devpost.com/software/marketmind-b6cy2q">여기를</a> 참조하세요.</p><p>MarketMind는 초보 트레이더에게 파편화된 시장 데이터를 명확한 실시간 신호로 변환하는 플랫폼을 제공함으로써 자리를 잡았습니다. 가격 움직임, 펀더멘털, 심리, 변동성을 여러 도구에 분산하는 대신 MarketMind는 이 모든 정보를 하나의 플랫폼에 통합하여 트레이더가 실행 가능한 인사이트를 얻을 수 있도록 도와줍니다. 이 프로젝트는 에이전트를 구축할 때 복잡한 ES|QL 쿼리를 사용하기도 했습니다.</p><h4>작동 방식:</h4><p><strong>1. 실시간 시장 데이터 수집</strong></p><p>MarketMind는 가격 움직임, 펀더멘털, 심리, 변동성, 위험 지표를 야후 파이낸스로부터 가져옵니다. 이 데이터는 수집되어 여러 개의 Elasticsearch 인덱스로 구성됩니다.</p><p><strong>2. 6명의 전문 에이전트가 시장을 분석합니다.</strong></p><p>에이전트 빌더로 구축된 각 에이전트는 시장의 다른 계층에 초점을 맞춥니다. 이들은 Elasticsearch 인덱스에서 읽고, 자체 도메인별 메트릭을 계산하고, 점수와 추론이 포함된 표준화된 JSON 출력을 생성합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/7d9716cca06a047a2b3584378b5c7e592a785ba1-1284x878.png" style="width: 100%;" /><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/5aac6a833347c0d2e596c02049ec4b4d3aae5cd7-794x764.png" style="width: 100%;" /><p><strong>3. 통합된 &#x27;시장 에너지&#x27; 모델로 신호 통합</strong></p><p>합산된 결과는 각 종목 주위에 빛나는 펄스로 표시되어 모멘텀이 형성되고 있는지, 리스크가 상승하고 있는지, 심리가 변화하고 있는지를 보여줍니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/46b3da8e3d528c5dd4e2829416c5446098acb3aa-744x718.png" style="width: 100%;" /><p><strong>4. 인사이트 시각화</strong></p><p>프론트엔드는 타입스크립트, SVG 물리 기반 비주얼, 라이브 캔들스틱 차트를 위한 <a href="https://github.com/chartjs">Chart.js를 사용하여</a> <a href="https://github.com/vercel/next.js">React와 Next.js로 구축되었습니다.</a> 이렇게 하면 원시 분석이 실시간으로 실행 가능한 피드백으로 전환됩니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/3f000c043117b77ed4127202be5a49c12e3682ba-1600x930.png" style="width: 100%;" /><h2>기타 흥미로운 프로젝트:</h2><p>다음은 스택의 여러 부분에서 Elastic을 사용한 다른 강력한 경쟁자들입니다:</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/76c49a853426844f475cd6b2a74999e60af20e8c-926x1080.png" style="width: 100%;" /><p><a href="https://cal-hacks-12-0.devpost.com/submissions/search?utf8=%E2%9C%93&amp;prize_filter%5Bprizes%5D%5B%5D=91882">여기에서</a> 트랙에 제출된 프로젝트의 전체 목록을 확인하세요.</p><h2>개발자로부터 배운 점</h2><ul><li><strong>에이전트 빌더는 사용자 친화적입니다:</strong></li></ul><p>대부분의 팀은 이전에 Elastic을 사용해 본 적이 없었지만 적은 지원으로도 신속하게 에이전트를 구축할 수 있었습니다. 더 많은 안내가 필요한 분들을 위해 워크숍을 열었지만, 대부분 데이터를 수집하고 해당 데이터에 대한 작업을 수행하는 에이전트를 구축할 수 있었습니다.</p><ul><li><strong>LLM은 <code>kNN</code> 쿼리에 탁월하지만 여전히 ES|QL 생성에 대한 지침이 필요합니다:</strong></li></ul><p>ChatGPT-5에 ES|QL 쿼리를 생성하도록 요청하면 ES|QL과 SQL이 혼합된 잘못된 정보가 반환되는 경우가 많았습니다. 마크다운 파일로 문서를 LLM에 공급하는 것은 실행 가능한 해결 방법인 것 같았습니다.</p><ul><li><strong>스냅샷 전용 ES|QL 함수가 문서로 유출되었습니다:</strong></li></ul><p>곧 출시될 <code>FIRST</code> 및 <code>LAST</code> 집계 함수는 의도치 않게 ES|QL 문서에 포함되었습니다. 이러한 문서를 ChatGPT에 제공했기 때문에 모델은 아직 서버리스에서 사용할 수 없는 기능임에도 불구하고 이러한 기능을 충실히 사용했습니다. 그룹의 피드백에 힘입어 엔지니어링 팀은 게시된 문서에서 기능을 제거하는 수정 사항을 신속하게 공개하고 병합했습니다<a href="https://github.com/elastic/elasticsearch/pull/137341">(PR #137341)</a>.</p><ul><li><strong>서버리스 관련 안내가 누락되었습니다:</strong></li></ul><p>한 팀이 조회 모드로 생성되지 않은 인덱스에서 <code>LOOKUP JOIN</code> 을 활성화하려고 시도했습니다. 오류 메시지는 서버리스에 존재하지 않는 명령을 쫓는다는 메시지를 보냈습니다. 이 사실을 제품 팀에 전달했고, 제품 팀은 즉시 서버리스와 관련된 실행 가능한 메시지에 대한 수정 사항을 공개했습니다. 장기적으로는 재색인 복잡성을 완전히 숨기는 것이 비전입니다<a href="https://github.com/elastic/elasticsearch-serverless/issues/4838">(이슈 #4838)</a>.</p><ul><li><strong>대면 이벤트의 가치:</strong></li></ul><p>온라인 해커톤도 훌륭하지만, 빌더와 어깨를 맞대고 디버깅할 때 얻을 수 있는 빠른 피드백을 따라올 수 있는 것은 없습니다. 다양한 사용 사례에서 에이전트 빌더를 통합하는 팀을 지켜보면서 ES|QL을 사용하는 개발자 경험을 개선할 수 있는 부분을 발견하고 비동기 채널을 통해 문제를 해결하는 것보다 훨씬 더 빠르게 문제를 해결했습니다.</p><h2>결론</h2><p>Cal Hacks 12.0은 주말 동안 멋진 데모를 보여줬을 뿐만 아니라 새로운 개발자들이 Elastic Stack과 어떻게 상호작용하는지에 대한 인사이트도 제공했습니다. 단 36시간 만에 팀들이 에이전트 빌더를 선택하고, Elasticsearch로 데이터를 수집하고, 멀티 에이전트 시스템을 설계하고, 다양한 방식으로 기능을 테스트하는 것을 보았습니다. 이번 행사를 통해 대면 이벤트가 중요한 이유를 다시 한 번 깨달았습니다. 빠른 피드백 루프, 실제 대화, 실습 디버깅을 통해 현재 개발자의 요구 사항을 파악하는 데 도움이 되었습니다. 저희가 배운 내용을 엔지니어링 팀에 다시 전달할 수 있게 되어 기쁩니다. 다음 해커톤에서 뵙겠습니다.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Elasticsearch에서 A2A 프로토콜과 MCP를 사용하여 LLM 에이전트 뉴스룸 만들기: 2부]]></title>
            <description><![CDATA[에이전트 협업을 위한 A2A 프로토콜과 Elasticsearch의 도구 액세스를 위한 MCP를 사용해 특화된 하이브리드 LLM 에이전트 뉴스룸을 구축하는 방법을 알아보세요.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/a2a-protocol-mcp-llm-agent-workflow-elasticsearch</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/a2a-protocol-mcp-llm-agent-workflow-elasticsearch</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Justin Castilla]]></dc:creator>
            <pubDate>Mon, 24 Nov 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/f87330e5d4ca961593b3cfb861ca850a4cc34186-1519x1173.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<h2>A2A 및 MCP: 작동 중인 코드</h2><p>이 글은 동일한 에이전트 내에서 A2A와 MCP 아키텍처를 모두 구현하여 두 프레임워크의 고유한 이점을 제대로 활용할 수 있는 이점을 설명한 &quot;Elasticsearch에서 A2A 프로토콜과 MCP를 사용하여 LLM 에이전트 뉴스룸 만들기!&quot; 글의 후속 글입니다. 직접 데모를 실행하려는 경우 <a href="https://github.com/justincastilla/elastic-newsroom">저장소를</a> 사용할 수 있습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/7196f004089127f83547b2e5dc3f663205cfcdce-1162x1600.png" style="width: 100%;" /><p>뉴스룸 에이전트가 A2A와 MCP를 사용하여 뉴스 기사를 제작하기 위해 어떻게 협업하는지 살펴보겠습니다. 에이전트가 작동하는 모습을 볼 수 있는 리포지토리는 <a href="https://github.com/justincastilla/elastic-newsroom">여기에서</a> 확인할 수 있습니다.</p><h3>1단계: 스토리 과제</h3><p><strong>뉴스 책임자</strong> (클라이언트 역할)가 스토리를 할당합니다:</p><pre><code>{
  "message_type": "task_request",
  "sender": "news_chief",
  "receiver": "reporter_agent",
  "payload": {
    "task_id": "story_renewable_energy_2024",
    "assignment": {
      "topic": "Renewable Energy Adoption in Europe",
      "angle": "Policy changes driving solar and wind expansion",
      "target_length": 1200,
      "deadline": "2025-09-30T18:00:00Z"
    }
  }
}</code></pre><h3>2단계: 리포터가 리서치 요청</h3><p><strong>리포터 에이전트는</strong> 배경 정보가 필요하다는 것을 인식하고 A2A를 통해 <strong>리포터 에이전트에게</strong> 위임합니다:</p><pre><code>{
  "message_type": "task_request",
  "sender": "reporter_agent",
  "receiver": "researcher_agent",
  "payload": {
    "task_id": "research_eu_renewable_2024",
    "parent_task_id": "story_renewable_energy_2024",
    "capability": "fact_gathering",
    "parameters": {
      "queries": [
        "EU renewable energy capacity 2024",
        "Solar installations growth Europe",
        "Wind energy policy changes 2024"
      ],
      "depth": "comprehensive"
    }
  }
}</code></pre><h3>3단계: 리포터가 아카이브 에이전트에게 기록 컨텍스트를 요청합니다.</h3><p><strong>리포터 에이전트는</strong> 역사적 맥락이 스토리를 강화할 수 있다는 것을 알고 있습니다. A2A를 <strong>통해</strong> (<a href="https://www.elastic.co/docs/solutions/search/elastic-agent-builder">Elastic의 A2A 에이전트로</a> 구동되는) 아카이브 에이전트에 위임하여 뉴스룸의 Elasticsearch 기반 기사 아카이브를 검색합니다:</p><pre><code>{
  "message_type": "task_request",
  "sender": "reporter_agent",
  "receiver": "archive_agent",
  "payload": {
    "task_id": "archive_search_renewable_2024",
    "parent_task_id": "story_renewable_energy_2024",
    "capability": "search_archive",
    "parameters": {
      "query": "European renewable energy policy changes and adoption trends over past 5 years",
      "focus_areas": ["solar", "wind", "policy", "Germany", "France"],
      "time_range": "2019-2024",
      "result_count": 10
    }
  }
}</code></pre><h3>4단계: 아카이브 에이전트가 MCP와 함께 Elastic A2A 에이전트 사용</h3><p><strong>아카이브 에이전트는</strong> Elastic의 A2A 에이전트를 사용하며, 이 에이전트는 다시 MCP를 사용해 Elasticsearch 도구에 액세스합니다. 이는 A2A가 에이전트 협업을 지원하는 동시에 MCP가 툴 액세스를 제공하는 하이브리드 아키텍처를 보여줍니다:</p><pre><code># Archive Agent using Elastic A2A Agent
async def search_historical_articles(self, query_params):
    # The Archive Agent sends a request to Elastic's A2A Agent
    elastic_response = await self.a2a_client.send_request(
        agent="elastic_agent",
        capability="search_and_analyze",
        parameters={
            "natural_language_query": query_params["query"],
            "index_pattern": "newsroom-articles-*",
            "filters": {
                "topics": query_params["focus_areas"],
                "date_range": query_params["time_range"]
            },
            "analysis_type": "trend_analysis"
        }
    )
    
    # Elastic's A2A Agent internally uses MCP tools:
    # - platform.core.search (to find relevant articles)
    # - platform.core.generate_esql (to analyze trends)
    # - platform.core.index_explorer (to identify relevant indices)
    
    return elastic_response</code></pre><p><strong>아카이브 에이전트는</strong> Elastic의 A2A 에이전트로부터 포괄적인 기록 데이터를 받아 리포터에게 반환합니다:</p><pre><code>{
  "message_type": "task_response",
  "sender": "archive_agent",
  "receiver": "reporter_agent",
  "payload": {
    "task_id": "archive_search_renewable_2024",
    "status": "completed",
    "archive_data": {
      "historical_articles": [
        {
          "title": "Germany's Energiewende: Five Years of Solar Growth",
          "published": "2022-06-15",
          "key_points": [
            "Germany added 7 GW annually 2020-2022",
            "Policy subsidies drove 60% of growth"
          ],
          "relevance_score": 0.94
        },
        {
          "title": "France Balances Nuclear and Renewables",
          "published": "2023-03-20",
          "key_points": [
            "France increased renewable target to 40% by 2030",
            "Solar capacity doubled 2021-2023"
          ],
          "relevance_score": 0.89
        }
      ],
      "trend_analysis": {
        "coverage_frequency": "EU renewable stories increased 150% since 2019",
        "emerging_themes": ["policy incentives", "grid modernization", "battery storage"],
        "coverage_gaps": ["Small member states", "offshore wind permitting"]
      },
      "total_articles_found": 47,
      "search_confidence": 0.91
    }
  }
}</code></pre><p>이 단계에서는 Elastic의 A2A 에이전트가 뉴스룸 워크플로우에 어떻게 통합되는지 보여드립니다. Archive 에이전트(뉴스룸 전용 에이전트)는 Elastic의 A2A 에이전트(타사 전문가)와 협력하여 Elasticsearch의 강력한 검색 및 분석 기능을 활용합니다. Elastic의 에이전트는 내부적으로 MCP를 사용하여 Elasticsearch 도구에 액세스하므로 에이전트 조정(A2A)과 도구 액세스(MCP)가 깔끔하게 분리되어 있습니다.</p><h3>5단계: 연구자가 MCP 서버 사용</h3><p><strong>연구원 에이전트는</strong> 여러 MCP 서버에 액세스하여 정보를 수집합니다:</p><pre><code># Researcher Agent using MCP to access tools
async def gather_facts(self, queries):
    results = []
    
    # Use News API MCP Server
    news_data = await self.mcp_client.invoke_tool(
        server="news_api_mcp",
        tool="search_articles",
        parameters={
            "query": queries[0],
            "date_range": "2024-01-01 to 2024-09-30",
            "sources": ["reuters", "bloomberg", "euractiv"]
        }
    )
    
    # Use Statistics MCP Server
    stats_data = await self.mcp_client.invoke_tool(
        server="eurostat_mcp",
        tool="get_energy_statistics",
        parameters={
            "metrics": ["solar_capacity_mw", "wind_capacity_mw"],
            "countries": ["EU27"],
            "year": 2024
        }
    )
    
    # Use Fact-Checking MCP Server
    verified_facts = await self.mcp_client.invoke_tool(
        server="factcheck_mcp",
        tool="verify_claims",
        parameters={
            "claims": [
                "EU solar capacity increased 40% in 2024",
                "Germany leads in wind installations"
            ]
        }
    )
    
    return self._synthesize_research(news_data, stats_data, verified_facts)</code></pre><h3>6단계: 연구원이 리포터에게 데이터를 반환합니다.</h3><p><strong>리서처 에이전트는</strong> A2A를 통해 종합적인 리서치를 다시 보냅니다:</p><pre><code>{
  "message_type": "task_response",
  "sender": "researcher_agent",
  "receiver": "reporter_agent",
  "payload": {
    "task_id": "research_eu_renewable_2024",
    "status": "completed",
    "research_data": {
      "key_facts": [
        {
          "claim": "EU solar capacity reached 260 GW in 2024",
          "source": "Eurostat Q3 2024 Report",
          "confidence": 0.95,
          "verified": true
        },
        {
          "claim": "Germany installed 12 GW of wind capacity in 2024",
          "source": "German Federal Network Agency",
          "confidence": 0.92,
          "verified": true
        }
      ],
      "statistics": {
        "solar_growth_rate": "35%",
        "wind_growth_rate": "28%"
      },
      "sources_count": 15
    }
  }
}</code></pre><h3>7단계: 기자가 기사 작성</h3><p><strong>리포터 에이전트는</strong> 리서치 데이터와 자체 LLM 기능을 사용하여 기사를 작성합니다. 글을 작성하는 동안 리포터는 스타일과 템플릿을 위해 MCP 서버를 사용합니다:</p><pre><code># Reporter Agent writing with MCP assistance
async def write_article(self, research_data, assignment):
    # Get style guidelines via MCP
    style_guide = await self.mcp_client.get_resource(
        server="newsroom_mcp",
        resource="style://editorial/ap_style_guide"
    )
    
    # Get article template via MCP
    template = await self.mcp_client.get_resource(
        server="newsroom_mcp",
        resource="template://articles/news_story"
    )
    
    # Generate article using LLM + research + style
    draft = await self.llm.generate(
        prompt=f"""
        Write a news article following these guidelines:
        {style_guide}
        
        Using this template:
        {template}
        
        Based on this research:
        {research_data}
        
        Assignment: {assignment}
        """
    )
    
    # Self-evaluate confidence in claims
    confidence_check = await self._evaluate_confidence(draft)
    
    return draft, confidence_check</code></pre><h3>8단계: 낮은 신뢰도로 인한 재조사 트리거</h3><p><strong>리포터 에이전트가</strong> 초안을 평가한 결과 한 클레임의 신뢰도가 낮다는 것을 발견했습니다. <strong>연구원 에이전트에게</strong> 또 다른 요청을 보냅니다:</p><pre><code>{
  "message_type": "collaboration_request",
  "sender": "reporter_agent",
  "receiver": "researcher_agent",
  "payload": {
    "request_type": "fact_verification",
    "claims": [
      {
        "text": "France's nuclear phase-down contributed to 15% increase in renewable capacity",
        "context": "Discussing policy drivers for renewable growth",
        "current_confidence": 0.45,
        "required_confidence": 0.80
      }
    ],
    "urgency": "high"
  }
}</code></pre><p><strong>연구원은</strong> 사실 확인 MCP 서버를 사용하여 클레임을 확인하고 업데이트된 정보를 반환합니다:</p><pre><code>{
  "message_type": "collaboration_response",
  "sender": "researcher_agent",
  "receiver": "reporter_agent",
  "payload": {
    "verified_claims": [
      {
        "original_claim": "France's nuclear phase-down contributed to 15% increase...",
        "verified_claim": "France's renewable capacity increased 18% in 2024, partially offsetting reduced nuclear output",
        "confidence": 0.88,
        "corrections": "Percentage was 18%, not 15%; nuclear phase-down is gradual, not primary driver",
        "sources": ["RTE France", "French Energy Ministry Report 2024"]
      }
    ]
  }
}</code></pre><h3>9단계: 리포터가 수정하여 편집자에게 제출합니다.</h3><p><strong>리포터는</strong> 확인된 사실을 통합하고 A2A를 통해 완성된 초안을 <strong>편집 에이전트에게</strong> 보냅니다:</p><pre><code>{
  "message_type": "task_request",
  "sender": "reporter_agent",
  "receiver": "editor_agent",
  "payload": {
    "task_id": "edit_renewable_story",
    "parent_task_id": "story_renewable_energy_2024",
    "content": {
      "headline": "Europe's Renewable Revolution: Solar and Wind Surge 30% in 2024",
      "body": "[Full article text...]",
      "word_count": 1185,
      "sources": [/* array of sources */]
    },
    "editing_requirements": {
      "check_style": true,
      "check_facts": true,
      "check_seo": true
    }
  }
}</code></pre><h3>10단계: MCP 도구를 사용한 편집자 리뷰</h3><p><strong>편집자 에이전트는</strong> 여러 MCP 서버를 사용하여 문서를 검토합니다:</p><pre><code># Editor Agent using MCP for quality checks
async def review_article(self, content):
    # Grammar and style check
    grammar_issues = await self.mcp_client.invoke_tool(
        server="grammarly_mcp",
        tool="check_document",
        parameters={"text": content["body"]}
    )
    
    # SEO optimization check
    seo_analysis = await self.mcp_client.invoke_tool(
        server="seo_mcp",
        tool="analyze_content",
        parameters={
            "headline": content["headline"],
            "body": content["body"],
            "target_keywords": ["renewable energy", "Europe", "solar", "wind"]
        }
    )
    
    # Plagiarism check
    originality = await self.mcp_client.invoke_tool(
        server="plagiarism_mcp",
        tool="check_originality",
        parameters={"text": content["body"]}
    )
    
    # Generate editorial feedback
    feedback = await self._generate_feedback(
        grammar_issues, 
        seo_analysis, 
        originality
    )
    
    return feedback</code></pre><p><strong>편집자가</strong> 글을 승인하고 앞으로 보냅니다:</p><pre><code>{
  "message_type": "task_response",
  "sender": "editor_agent",
  "receiver": "reporter_agent",
  "payload": {
    "status": "approved",
    "quality_score": 9.2,
    "minor_edits": [
      "Changed 'surge' to 'increased' in paragraph 3 for AP style consistency",
      "Added Oxford comma in list of countries"
    ],
    "approved_content": "[Final edited article]"
  }
}</code></pre><h3>11단계: CI/CD를 통해 게시자 게시</h3><p>마지막으로 <strong>프린터 에이전트는</strong> CMS 및 CI/CD 파이프라인을 위한 MCP 서버를 사용하여 승인된 문서를 게시합니다:</p><pre><code># Publisher Agent publishing via MCP
async def publish_article(self, content, metadata):
    # Upload to CMS via MCP
    cms_result = await self.mcp_client.invoke_tool(
        server="wordpress_mcp",
        tool="create_post",
        parameters={
            "title": content["headline"],
            "body": content["body"],
            "status": "draft",
            "categories": metadata["categories"],
            "tags": metadata["tags"],
            "featured_image_url": metadata["image_url"]
        }
    )
    
    post_id = cms_result["post_id"]
    
    # Trigger CI/CD deployment via MCP
    deploy_result = await self.mcp_client.invoke_tool(
        server="cicd_mcp",
        tool="trigger_deployment",
        parameters={
            "pipeline": "publish_article",
            "environment": "production",
            "post_id": post_id,
            "schedule": "immediate"
        }
    )
    
    # Track analytics
    await self.mcp_client.invoke_tool(
        server="analytics_mcp",
        tool="register_publication",
        parameters={
            "post_id": post_id,
            "publish_time": datetime.now().isoformat(),
            "story_id": metadata["story_id"]
        }
    )
    
    return {
        "status": "published",
        "post_id": post_id,
        "url": f"https://newsroom.example.com/articles/{post_id}",
        "deployment_id": deploy_result["deployment_id"]
    }</code></pre><p><strong>게시자는</strong> A2A를 통해 게시를 확인합니다:</p><pre><code>{
  "message_type": "task_complete",
  "sender": "printer_agent",
  "receiver": "news_chief",
  "payload": {
    "task_id": "story_renewable_energy_2024",
    "status": "published",
    "publication": {
      "url": "https://newsroom.example.com/articles/renewable-europe-2024",
      "published_at": "2025-09-30T17:45:00Z",
      "post_id": "12345"
    },
    "workflow_metrics": {
      "total_time_minutes": 45,
      "agents_involved": ["reporter", "researcher", "archive", "editor", "printer"],
      "iterations": 2,
      "mcp_calls": 12
    }
  }
}</code></pre><p>다음은 위에서 설명한 것과 동일한 에이전트를 사용하여 첨부된 리포지토리에 있는 A2A 워크플로우의 전체 시퀀스입니다.</p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">#</td><td style="border: 1px solid #ccc; padding: 4px;">에서</td><td style="border: 1px solid #ccc; padding: 4px;">To</td><td style="border: 1px solid #ccc; padding: 4px;">액션</td><td style="border: 1px solid #ccc; padding: 4px;">프로토콜</td><td style="border: 1px solid #ccc; padding: 4px;">설명</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">1</td><td style="border: 1px solid #ccc; padding: 4px;">사용자</td><td style="border: 1px solid #ccc; padding: 4px;">뉴스 수석</td><td style="border: 1px solid #ccc; padding: 4px;">스토리 할당</td><td style="border: 1px solid #ccc; padding: 4px;">HTTP POST</td><td style="border: 1px solid #ccc; padding: 4px;">사용자가 스토리 주제와 각도를 제출합니다.</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">2</td><td style="border: 1px solid #ccc; padding: 4px;">뉴스 수석</td><td style="border: 1px solid #ccc; padding: 4px;">내부</td><td style="border: 1px solid #ccc; padding: 4px;">스토리 만들기</td><td style="border: 1px solid #ccc; padding: 4px;">-</td><td style="border: 1px solid #ccc; padding: 4px;">고유 ID로 스토리 레코드 생성</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">3</td><td style="border: 1px solid #ccc; padding: 4px;">뉴스 수석</td><td style="border: 1px solid #ccc; padding: 4px;">리포터</td><td style="border: 1px solid #ccc; padding: 4px;">위임자 할당</td><td style="border: 1px solid #ccc; padding: 4px;">A2A</td><td style="border: 1px solid #ccc; padding: 4px;">A2A 프로토콜을 통해 스토리 할당 전송</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">4</td><td style="border: 1px solid #ccc; padding: 4px;">리포터</td><td style="border: 1px solid #ccc; padding: 4px;">내부</td><td style="border: 1px solid #ccc; padding: 4px;">할당 수락</td><td style="border: 1px solid #ccc; padding: 4px;">-</td><td style="border: 1px solid #ccc; padding: 4px;">내부적으로 스토어 할당</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">5</td><td style="border: 1px solid #ccc; padding: 4px;">리포터</td><td style="border: 1px solid #ccc; padding: 4px;">MCP 서버</td><td style="border: 1px solid #ccc; padding: 4px;">개요 생성</td><td style="border: 1px solid #ccc; padding: 4px;">MCP/HTTP</td><td style="border: 1px solid #ccc; padding: 4px;">기사 개요 및 연구 질문 생성</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">6a</td><td style="border: 1px solid #ccc; padding: 4px;">리포터</td><td style="border: 1px solid #ccc; padding: 4px;">연구원</td><td style="border: 1px solid #ccc; padding: 4px;">연구 요청</td><td style="border: 1px solid #ccc; padding: 4px;">A2A</td><td style="border: 1px solid #ccc; padding: 4px;">질문 보내기(6b와 병행)</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">6b</td><td style="border: 1px solid #ccc; padding: 4px;">리포터</td><td style="border: 1px solid #ccc; padding: 4px;">아카이브</td><td style="border: 1px solid #ccc; padding: 4px;">아카이브 검색</td><td style="border: 1px solid #ccc; padding: 4px;">A2A JSONRPC</td><td style="border: 1px solid #ccc; padding: 4px;">과거 기사 검색(6a와 병행)</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">7</td><td style="border: 1px solid #ccc; padding: 4px;">연구원</td><td style="border: 1px solid #ccc; padding: 4px;">MCP 서버</td><td style="border: 1px solid #ccc; padding: 4px;">연구 질문</td><td style="border: 1px solid #ccc; padding: 4px;">MCP/HTTP</td><td style="border: 1px solid #ccc; padding: 4px;">MCP를 통해 Anthropic을 사용하여 질문에 답하기</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">8</td><td style="border: 1px solid #ccc; padding: 4px;">연구원</td><td style="border: 1px solid #ccc; padding: 4px;">리포터</td><td style="border: 1px solid #ccc; padding: 4px;">리서치 반환</td><td style="border: 1px solid #ccc; padding: 4px;">A2A</td><td style="border: 1px solid #ccc; padding: 4px;">연구 답변 반환</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">9</td><td style="border: 1px solid #ccc; padding: 4px;">아카이브</td><td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch</td><td style="border: 1px solid #ccc; padding: 4px;">검색 색인</td><td style="border: 1px solid #ccc; padding: 4px;">ES REST API</td><td style="border: 1px solid #ccc; padding: 4px;">뉴스_아카이브 인덱스 쿼리</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">10</td><td style="border: 1px solid #ccc; padding: 4px;">아카이브</td><td style="border: 1px solid #ccc; padding: 4px;">리포터</td><td style="border: 1px solid #ccc; padding: 4px;">아카이브 반환</td><td style="border: 1px solid #ccc; padding: 4px;">A2A JSONRPC</td><td style="border: 1px solid #ccc; padding: 4px;">과거 검색 결과 반환</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">11</td><td style="border: 1px solid #ccc; padding: 4px;">리포터</td><td style="border: 1px solid #ccc; padding: 4px;">MCP 서버</td><td style="border: 1px solid #ccc; padding: 4px;">문서 생성</td><td style="border: 1px solid #ccc; padding: 4px;">MCP/HTTP</td><td style="border: 1px solid #ccc; padding: 4px;">리서치/아카이브 컨텍스트가 포함된 문서 생성</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">12</td><td style="border: 1px solid #ccc; padding: 4px;">리포터</td><td style="border: 1px solid #ccc; padding: 4px;">내부</td><td style="border: 1px solid #ccc; padding: 4px;">저장 초안</td><td style="border: 1px solid #ccc; padding: 4px;">-</td><td style="border: 1px solid #ccc; padding: 4px;">내부적으로 초안 저장</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">13</td><td style="border: 1px solid #ccc; padding: 4px;">리포터</td><td style="border: 1px solid #ccc; padding: 4px;">뉴스 수석</td><td style="border: 1px solid #ccc; padding: 4px;">초안 제출</td><td style="border: 1px solid #ccc; padding: 4px;">A2A</td><td style="border: 1px solid #ccc; padding: 4px;">완성된 초안 제출</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">14</td><td style="border: 1px solid #ccc; padding: 4px;">뉴스 수석</td><td style="border: 1px solid #ccc; padding: 4px;">내부</td><td style="border: 1px solid #ccc; padding: 4px;">업데이트 스토리</td><td style="border: 1px solid #ccc; padding: 4px;">-</td><td style="border: 1px solid #ccc; padding: 4px;">초안 저장, "draft_submitted로 상태 업데이트"</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">15</td><td style="border: 1px solid #ccc; padding: 4px;">뉴스 수석</td><td style="border: 1px solid #ccc; padding: 4px;">편집기</td><td style="border: 1px solid #ccc; padding: 4px;">초안 검토</td><td style="border: 1px solid #ccc; padding: 4px;">A2A</td><td style="border: 1px solid #ccc; padding: 4px;">검토를 위해 편집기로 자동 라우팅</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">16</td><td style="border: 1px solid #ccc; padding: 4px;">편집기</td><td style="border: 1px solid #ccc; padding: 4px;">MCP 서버</td><td style="border: 1px solid #ccc; padding: 4px;">기사 검토</td><td style="border: 1px solid #ccc; padding: 4px;">MCP/HTTP</td><td style="border: 1px solid #ccc; padding: 4px;">MCP를 통해 Anthropic을 사용하여 콘텐츠 분석</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">17</td><td style="border: 1px solid #ccc; padding: 4px;">편집기</td><td style="border: 1px solid #ccc; padding: 4px;">뉴스 수석</td><td style="border: 1px solid #ccc; padding: 4px;">반품 검토</td><td style="border: 1px solid #ccc; padding: 4px;">A2A</td><td style="border: 1px solid #ccc; padding: 4px;">편집 피드백 및 제안을 보냅니다.</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">18</td><td style="border: 1px solid #ccc; padding: 4px;">뉴스 수석</td><td style="border: 1px solid #ccc; padding: 4px;">내부</td><td style="border: 1px solid #ccc; padding: 4px;">스토어 리뷰</td><td style="border: 1px solid #ccc; padding: 4px;">-</td><td style="border: 1px solid #ccc; padding: 4px;">편집자 피드백 저장</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">19</td><td style="border: 1px solid #ccc; padding: 4px;">뉴스 수석</td><td style="border: 1px solid #ccc; padding: 4px;">리포터</td><td style="border: 1px solid #ccc; padding: 4px;">편집 적용</td><td style="border: 1px solid #ccc; padding: 4px;">A2A</td><td style="border: 1px solid #ccc; padding: 4px;">리포터에게 리뷰 피드백 전달</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">20</td><td style="border: 1px solid #ccc; padding: 4px;">리포터</td><td style="border: 1px solid #ccc; padding: 4px;">MCP 서버</td><td style="border: 1px solid #ccc; padding: 4px;">편집 적용</td><td style="border: 1px solid #ccc; padding: 4px;">MCP/HTTP</td><td style="border: 1px solid #ccc; padding: 4px;">피드백에 따라 문서 수정</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">21</td><td style="border: 1px solid #ccc; padding: 4px;">리포터</td><td style="border: 1px solid #ccc; padding: 4px;">내부</td><td style="border: 1px solid #ccc; padding: 4px;">업데이트 초안</td><td style="border: 1px solid #ccc; padding: 4px;">-</td><td style="border: 1px solid #ccc; padding: 4px;">수정 사항이 있는 초안 업데이트</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">22</td><td style="border: 1px solid #ccc; padding: 4px;">리포터</td><td style="border: 1px solid #ccc; padding: 4px;">뉴스 수석</td><td style="border: 1px solid #ccc; padding: 4px;">수정된 반환</td><td style="border: 1px solid #ccc; padding: 4px;">A2A</td><td style="border: 1px solid #ccc; padding: 4px;">수정된 문서 반환</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">23</td><td style="border: 1px solid #ccc; padding: 4px;">뉴스 수석</td><td style="border: 1px solid #ccc; padding: 4px;">내부</td><td style="border: 1px solid #ccc; padding: 4px;">업데이트 스토리</td><td style="border: 1px solid #ccc; padding: 4px;">-</td><td style="border: 1px solid #ccc; padding: 4px;">스토어 수정 초안, 상태 "수정됨"</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">24</td><td style="border: 1px solid #ccc; padding: 4px;">뉴스 수석</td><td style="border: 1px solid #ccc; padding: 4px;">게시자</td><td style="border: 1px solid #ccc; padding: 4px;">기사 게시</td><td style="border: 1px solid #ccc; padding: 4px;">A2A</td><td style="border: 1px solid #ccc; padding: 4px;">게시자에게 자동 라우팅</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">25</td><td style="border: 1px solid #ccc; padding: 4px;">게시자</td><td style="border: 1px solid #ccc; padding: 4px;">MCP 서버</td><td style="border: 1px solid #ccc; padding: 4px;">태그 생성</td><td style="border: 1px solid #ccc; padding: 4px;">MCP/HTTP</td><td style="border: 1px solid #ccc; padding: 4px;">태그 및 카테고리 만들기</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">26</td><td style="border: 1px solid #ccc; padding: 4px;">게시자</td><td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch</td><td style="border: 1px solid #ccc; padding: 4px;">색인 문서</td><td style="border: 1px solid #ccc; padding: 4px;">ES REST API</td><td style="border: 1px solid #ccc; padding: 4px;">기사를 뉴스_아카이브 인덱스에 색인화</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">27</td><td style="border: 1px solid #ccc; padding: 4px;">게시자</td><td style="border: 1px solid #ccc; padding: 4px;">파일 시스템</td><td style="border: 1px solid #ccc; padding: 4px;">마크다운 저장</td><td style="border: 1px solid #ccc; padding: 4px;">파일 I/O</td><td style="border: 1px solid #ccc; padding: 4px;">.md로 문서 저장 아티클의 파일</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">28</td><td style="border: 1px solid #ccc; padding: 4px;">게시자</td><td style="border: 1px solid #ccc; padding: 4px;">뉴스 수석</td><td style="border: 1px solid #ccc; padding: 4px;">게시 확인</td><td style="border: 1px solid #ccc; padding: 4px;">A2A</td><td style="border: 1px solid #ccc; padding: 4px;">성공 상태 반환</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">29</td><td style="border: 1px solid #ccc; padding: 4px;">뉴스 수석</td><td style="border: 1px solid #ccc; padding: 4px;">내부</td><td style="border: 1px solid #ccc; padding: 4px;">업데이트 스토리</td><td style="border: 1px solid #ccc; padding: 4px;">-</td><td style="border: 1px solid #ccc; padding: 4px;">스토리 상태를 "게시됨으로 업데이트"</td>
                    </tr>
              </tbody>
            </table>
          <h2>결론</h2><p>A2A와 MCP는 모두 최신 증강-LLM 인프라 패러다임에서 중요한 역할을 담당하고 있습니다. A2A는 복잡한 멀티 에이전트 시스템을 위한 유연성을 제공하지만 잠재적으로 휴대성이 떨어지고 운영 복잡성이 높아질 수 있습니다. MCP는 멀티에이전트 오케스트레이션을 처리하도록 설계되지는 않았지만 구현 및 유지 관리가 더 간편한 도구 통합을 위한 표준화된 접근 방식을 제공합니다.</p><p>선택은 이분법적이지 않습니다. 뉴스룸의 예시를 통해 알 수 있듯이, 가장 정교하고 효과적인 LLM 지원 시스템은 에이전트가 A2A 프로토콜을 통해 조정하고 전문화하면서 MCP 서버를 통해 도구와 리소스에 액세스하는 두 가지 접근 방식을 결합하는 경우가 많습니다. 이 하이브리드 아키텍처는 MCP의 표준화 및 에코시스템의 장점과 함께 멀티 에이전트 시스템의 조직적 이점을 제공합니다. 이는 선택의 여지가 전혀 없을 수도 있음을 시사합니다. 두 가지를 모두 표준 접근 방식으로 사용하기만 하면 됩니다.</p><p>특정 사용 사례에 적합한 결과를 만들기 위해 두 솔루션을 가장 잘 조합하여 테스트하고 결정하는 것은 개발자 또는 아키텍트의 몫입니다. 각 접근 방식의 강점, 한계, 적절한 적용 사례를 이해하면 보다 효과적이고 유지 관리가 용이하며 확장 가능한 AI 시스템을 구축할 수 있습니다.</p><p>디지털 뉴스룸, 고객 서비스 플랫폼, 리서치 어시스턴트 또는 기타 LLM 기반 애플리케이션을 구축하는 경우, 조정 요구 사항(A2A)과 도구 액세스 요구 사항(MCP)을 신중하게 고려하면 성공의 길로 들어설 수 있습니다.</p><h2>추가 리소스</h2><ul><li><strong>Elasticsearch 에이전트 빌더: </strong><a href="https://www.elastic.co/docs/solutions/search/elastic-agent-builder">https://www.elastic.co/docs/solutions/search/elastic-agent-builder</a></li><li><strong>A2A 사양:</strong> <a href="https://a2a-protocol.org/latest/specification/">https://a2a-protocol.org/latest/specification/</a></li><li><strong>A2A 및 MCP 통합:</strong> <a href="https://a2a-protocol.org/latest/topics/a2a-and-mcp/">https://a2a-protocol.org/latest/topics/a2a-and-mcp/</a></li><li><strong>모델 컨텍스트 프로토콜:</strong> <a href="https://modelcontextprotocol.io/">https://modelcontextprotocol.io</a></li></ul>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[맥락을 위한 검색 - 3부: 맥락 엔지니어링에서 하이브리드 검색의 힘]]></title>
            <description><![CDATA[컨텍스트 엔지니어링과 하이브리드 검색을 사용하여 집계, RBAC 및 비콘텐츠 신호로 AI 출력 정확도를 개선하는 방법을 알아보세요.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/context-engineering-hybrid-search-agentic-ai-accuracy</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/context-engineering-hybrid-search-agentic-ai-accuracy</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Woody Walton]]></dc:creator>
            <pubDate>Thu, 20 Nov 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/b82ff25242e4229cc20b218d9cc91c60cfd680bc-720x420.jpg?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>지금까지 하이브리드 검색<a href="https://www.elastic.co/search-labs/blog/context-engineering-hybrid-search-evolution-agentic-ai">(1부)</a>과 컨텍스트 엔지니어링<a href="https://www.elastic.co/search-labs/blog/context-engineering-llm-evolution-agentic-ai">(2부)</a>에 대해 살펴봤는데, 이제 이 두 가지가 어떻게 함께 작동하여 RAG 및 에이전트 AI 운영에 타겟팅된 컨텍스트를 제공하는 데 가장 큰 효과를 가져오는지 살펴보겠습니다.</p><h2>검색은 죽지 않았고, 단지 이동했을 뿐입니다.</h2><p>따라서 주로 텍스트 상자를 통해 문맥을 검색하고 반환된 정보(문맥)를 사용하여 직접 답변을 구성하는 방식에서 이제는 자연어를 사용하여 상담원에게 원하는 것을 말하면 자동으로 검색하여 답변을 작성하는 방식으로 전환했습니다. 기술 업계의 많은 사람들이 이러한 변화를 지적하며 &quot;검색은 죽었다&quot;고 선언하고 있지만(물론 SEO와 애드워즈 세계는 <a href="https://www.pewresearch.org/short-reads/2025/07/22/google-users-are-less-likely-to-click-on-links-when-an-ai-summary-appears-in-the-results/">확실히 변화하고</a> 있습니다 <a href="https://www.wired.com/story/goodbye-seo-hello-geo-brandlight-openai/">.</a> 누구세요?) 검색은 여전히 에이전트 운영에 절대적으로 중요하며, 지금은 대부분 도구를 통해 보이지 않는 곳에서 수행될 뿐입니다.</p><p>이전에는 사용자가 주관적인 관련성의 주요 중재자였습니다. 사용자마다 검색을 실행하는 이유가 다르고, 개인적인 경험에 따라 결과의 상대적 정확도가 달라집니다. 에이전트가 우리와 동일한(또는 더 나은) 결론에 도달할 수 있다고 믿으려면 에이전트가 액세스할 수 있는 컨텍스트 정보가 우리의 주관적인 의도에 최대한 가깝도록 보장해야 합니다. 우리는 그 목표를 향해 LLM을 제공하는 맥락을 설계해야 합니다!</p><h2>하이브리드 검색 검색을 통한 컨텍스트 생성</h2><p>1부에서 다시 한 번 말씀드리지만, Elastic의 하이브리드 검색은 기존 키워드 기반 검색의 강점(구문 유연성, 키워드 정밀도, 관련성 점수)과 벡터 유사성 검색의 의미론적 이해를 결합하고 다양한 재순위 지정 기술을 제공합니다. 이 시너지 효과(이 단어의 진정한 용도는 찾아볼 수 없습니다!) 를 사용하면 콘텐츠를 타겟팅하는 방식에 훨씬 더 미묘한 차이가 있는 쿼리를 통해 연관성이 높은 결과를 얻을 수 있습니다. 검색 단계 <em>중 하나로</em> 주관적 연관성을 적용할 수 있다는 것뿐만 아니라, 실제로는 1단계 검색에 다른 모든 모드와 함께 연관성 점수를 한 번에 포함할 수 있다는 것입니다.</p><h3>뛰어난 정확도 &amp; 효율성</h3><p>분산 검색, 검색 및 순위 재지정을 제공할 수 있는 데이터 플랫폼을 기본 컨텍스트 검색 엔진으로 사용하는 것은 매우 합리적입니다. 고급 쿼리 구문을 사용하여 주관적 의도의 누락된 구성 요소를 추가하고, 반환된 문맥 정보의 가치를 흐리게 하거나 방해할 수 있는 콘텐츠를 필터링할 수 있습니다. 사용 가능한 개별 구문 옵션 중에서 선택하거나 각 유형의 데이터를 가장 잘 이해하는 방식으로 타겟팅하는 단일 검색으로 모달리티를 결합한 다음 순위를 재조정하여 결합/재배열할 수 있습니다. 원하는 필드/값만 포함하도록 응답을 필터링하여 불필요한 데이터를 차단할 수 있습니다. 상담원 서비스에서는 이러한 타겟팅 유연성을 통해 컨텍스트를 검색하는 방식이 매우 정확한 툴을 구축할 수 있습니다.</p><h3>컨텍스트 세분화(집계 및 비콘텐츠 신호)</h3><p>집계는 도구가 컨텍스트 창에 제공하는 콘텐츠를 구성하는 데 특히 유용할 수 있습니다. 집계는 자연스럽게 반환된 컨텍스트 데이터의 형태에 대한 수치 기반 사실을 제공하므로, LLM이 더 쉽고 정확하게 추론할 수 있습니다. 집계는 계층적으로 중첩될 수 있기 때문에 LLM에 다단계 세부 정보를 쉽게 추가하여 보다 미묘한 차이를 파악할 수 있습니다. 집계는 컨텍스트 창 크기를 관리하는 데도 도움이 됩니다. 10만 개의 문서에 대한 쿼리 결과를 수백 개의 집계된 인사이트 토큰으로 쉽게 줄일 수 있습니다.</p><p>비콘텐츠 신호는 인기도, 신선도, 지리적 위치, 카테고리, 호스트 다양성, 가격대 등 결과의 추가적인 특성을 나타내는 데이터의 내재적 지표로, 현재 보고 있는 내용에 대한 더 큰 그림을 알려줍니다. 이러한 정보는 상담원이 수신한 컨텍스트의 중요도를 평가하는 데 유용할 수 있습니다. 몇 가지 간단한 예시를 통해 이를 가장 잘 설명할 수 있습니다:</p><ul><li><strong>최근에 게시된 인기 콘텐츠 강화하기</strong> - 문서에 대한 지식창고가 있다고 가정해 보세요. 사용자의 검색어와 관련된 문서를 찾고 싶지만, 최근 문서이면서 다른 사용자가 도움이 되었다고 판단한 문서(예: &quot;좋아요&quot; 수가 많은 문서)도 부스팅하고 싶을 수 있습니다. 이 시나리오에서는 하이브리드 검색을 사용하여 관련성 있는 문서를 찾은 다음 게시 날짜와 인기도를 조합하여 순위를 재조정할 수 있습니다.</li><li><strong>판매 및 재고 조정 기능이 있는 전자상거래 검색</strong> - 전자상거래 환경에서는 고객에게 검색어와 일치하는 제품을 표시하는 동시에 잘 팔리고 재고가 있는 제품을 홍보하고 싶을 수 있습니다. 또한 재고가 적은 제품의 순위를 낮춰 고객의 불만을 피할 수도 있습니다.</li><li><strong>버그 트래커에서 심각도가 높은 이슈 우선 순위 지정하기</strong> - 소프트웨어 개발팀의 경우 이슈를 검색할 때 심각도가 높고 우선 순위가 높으며 최근에 업데이트된 이슈를 먼저 표시하는 것이 중요합니다. &#x27;중요도&#x27; 및 &#x27;가장 많이 논의된&#x27; 등의 비신호를 사용하여 다양한 요소를 독립적으로 평가하여 가장 중요하고 활발하게 논의된 이슈가 맨 위에 표시되도록 할 수 있습니다.</li></ul><p>이러한 예제 쿼리 등은 함께 제공되는 Elasticsearch Labs <a href="https://github.com/elastic/elasticsearch-labs/tree/main/supporting-blog-content/you-know-for-context/">콘텐츠 페이지에서</a> 확인할 수 있습니다.</p><h3>보안 시행</h3><p>컨텍스트 엔지니어링을 위해 Elastic과 같은 검색 기반 속도 계층을 활용할 때의 중요한 장점은 기본 제공 보안 프레임워크입니다. Elastic의 플랫폼은 세분화된 역할 기반 액세스 제어(RBAC)와 속성 기반 액세스 제어(ABAC)를 통해 에이전트 및 생성 AI 작업에 제공되는 컨텍스트가 민감한 개인 보유 정보를 존중하고 보호하도록 보장합니다. 즉, 쿼리가 효율적으로 처리될 뿐만 아니라 요청을 시작한 상담원이나 사용자의 특정 권한에 따라 결과가 필터링됩니다.</p><p>에이전트는 인증된 사용자로 실행되므로 플랫폼에 내장된 보안 기능을 통해 보안이 암시적으로 적용됩니다:</p><ul><li><strong>세분화된 권한:</strong> 문서, 필드 또는 용어 수준에서 액세스 권한을 정의하여 AI 에이전트가 볼 권한이 있는 데이터만 받도록 하세요.</li><li><strong>역할 기반 액세스 제어(RBAC):</strong> 에이전트 또는 사용자에게 역할을 할당하여 정의된 책임에 따라 특정 데이터 세트 또는 기능에 대한 액세스 권한을 부여합니다.</li><li><strong>속성 기반 액세스 제어(ABAC):</strong> 데이터, 사용자 또는 환경의 속성을 기반으로 동적 액세스 정책을 구현하여 고도로 적응력이 뛰어나고 상황에 맞는 보안을 구현할 수 있습니다.</li><li><strong>문서 수준 보안(DLS) 및 필드 수준 보안(FLS):</strong> 이러한 기능은 검색된 문서 내에서도 승인된 부분만 볼 수 있도록 하여 민감한 정보가 노출되는 것을 방지합니다.</li><li><strong>엔터프라이즈 보안과 통합:</strong> 기존 ID 관리 시스템(예: LDAP, SAML, OIDC)과 원활하게 통합하여 조직 전체에 일관된 보안 정책을 적용할 수 있습니다.</li></ul><p>이러한 보안 조치를 컨텍스트 검색 메커니즘에 직접 통합함으로써 Elastic은 보안 게이트키퍼 역할을 수행하여 AI 에이전트가 정의된 데이터 경계 내에서 작동하도록 보장하고 무단 데이터 노출을 방지하며 데이터 개인 정보 보호 규정을 준수하도록 유지합니다. 이는 기밀 또는 독점 정보를 처리하는 에이전트 AI 시스템에 대한 신뢰를 구축하는 데 가장 중요한 요소입니다.</p><p>추가로, 엔터프라이즈 데이터 소스에서 통합 데이터 속도 계층을 사용하면 에이전트 도구가 생성하는 리포지토리의 예기치 않은 임시 쿼리 부하를 완화할 수 있습니다. 한 곳에서 모든 것을 거의 실시간으로 검색하고 보안 및 거버넌스 제어를 적용할 수 있습니다.</p><h2>하이브리드 검색 기반 도구</h2><p>컨텍스트 엔지니어링의 추구를 가속화하는 Elastic 플랫폼의 몇 가지 핵심 기능( <a href="https://www.elastic.co/blog/whats-new-elastic-9-2-0">계속 추가될</a> 예정)이 있습니다. 여기서 가장 중요한 것은 플랫폼이 AI 생태계가 발전함에 따라 유연하게 적응, 변경, 확장할 수 있는 다양한 방법을 제공한다는 점입니다.</p><h3>에이전트 빌더 소개</h3><p>Elastic <a href="https://www.elastic.co/elasticsearch/agent-builder">에이전트 빌더는</a> Elastic에 이미 저장되어 있는 데이터와 채팅할 수 있도록 구축된 에이전트 AI 도구 영역에 처음으로 진출한 제품입니다. 에이전트 빌더는 사용자가 Kibana 내에서 자신만의 에이전트와 도구를 생성하고 관리할 수 있는 채팅 인터페이스를 제공합니다. 기본 제공 MCP 및 A2A 서버, 프로그래밍 방식의 API, Elasticsearch 인덱스를 쿼리 및 탐색하고 자연어로부터 ES|QL 쿼리를 생성하기 위한 사전 구축된 시스템 도구 세트가 함께 제공됩니다. 에이전트 빌더를 사용하면 표현식 <a href="https://www.elastic.co/docs/reference/query-languages/esql">ES|QL</a> 쿼리 구문을 통해 에이전트에게 반환되는 컨텍스트 데이터를 타겟팅하고 조각하는 사용자 지정 도구를 만들 수 있습니다.</p><p>ES|QL은 하이브리드 검색을 어떻게 수행하나요? <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/semantic-text">핵심 기능은 semantic_text</a> 필드 유형과 <a href="https://www.elastic.co/docs/reference/query-languages/esql/commands/fork"></a><a href="https://www.elastic.co/docs/reference/query-languages/esql/commands/fuse">FORK/FUSE</a> 명령의 조합을 통해 수행됩니다(FUSE는 기본적으로 각 포크의 결과를 병합하는 데 <a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/reciprocal-rank-fusion">RRF를 사용합니다).</a> 다음은 가상의 제품 검색에 대한 간단한 예제입니다:</p><pre><code>FROM products
| FORK
  (MATCH description "high performance gaming laptop" | EVAL search_type = "bm25"),
  (MATCH description_semantic "high performance gaming laptop" | EVAL search_type = "semantic")
| FUSE 
| LIMIT 20
| KEEP product_name, description, _score, search_type</code></pre><p>위의 예제에서 각 FORK 브랜치에 포함된 <a href="https://www.elastic.co/docs/reference/query-languages/esql/commands/eval">EVAL</a> 절은 반드시 필요한 것은 아니며, 특정 검색 결과가 어떤 검색 방식에서 반환되었는지 추적하는 방법을 보여주기 위해 포함되었을 뿐입니다.</p><h3>템플릿 검색</h3><p>자체 외부 에이전트 도구를 Elastic 배포로 가리키고 싶다고 가정해 보겠습니다. 또한 ES|QL 대신 다단계 검색기를 사용하거나 개발한 기존 DSL 구문을 재사용하고 쿼리가 허용하는 입력, 검색 실행에 사용되는 구문 및 출력에 반환되는 필드를 제어할 수 있기를 원합니다. <a href="https://www.elastic.co/docs/solutions/search/search-templates">검색 템플릿을</a> 사용하면 일반적인 검색 패턴에 대해 미리 정의된 구조를 정의하여 데이터 검색의 효율성과 일관성을 개선할 수 있습니다. 이는 상용구 코드를 표준화하고 검색 로직의 빠른 반복을 가능하게 하므로 검색 API와 상호 작용하는 에이전트 도구에 특히 유용합니다. 이러한 요소 중 하나를 조정해야 하는 경우 검색 템플릿을 업데이트하기만 하면 변경 사항이 바로 적용됩니다. 에이전트 도구에서 작동하는 검색 템플릿의 예를 찾고 계신다면, 외부 MCP 서버에서 도구 호출 뒤에 검색 템플릿을 활용하는 Elasticsearch Labs 블로그 &#x27;<a href="https://www.elastic.co/search-labs/blog/mcp-intelligent-search">지능형 검색을 위한 MCP</a>&#x27;를 살펴보시기 바랍니다.</p><h3>통합 워크플로(FTW!)</h3><p>새로운 에이전트 AI 세계에서 가장 어려운 점 중 하나는 반자율적이고 자기 주도적인 &#x27;추론&#x27; 에이전트의 비결정적 특성입니다. 컨텍스트 엔지니어링은 에이전트 AI의 중요한 분야로, 에이전트가 생성할 수 있는 결론의 범위를 우리가 알고 있는 사실에 근거하여 좁히는 데 도움이 되는 기술입니다. 매우 정확하고 관련성이 높은 컨텍스트 창이 있더라도 (수치적 사실의 영역을 벗어나면) 상담원의 응답이 완전히 반복 가능하고 신뢰할 수 있다는 확신을 줄 수 있는 부분이 여전히 부족합니다.</p><p>상담원에게 동일한 요청을 여러 번 실행하면 응답에 약간의 차이가 <em>있을 뿐</em> <em>본질적으로</em> 동일한 답변이 나올 수 있습니다. 이는 보통 눈에 띄지 않을 정도로 단순한 쿼리의 경우 괜찮으며 컨텍스트 엔지니어링 기법을 사용하여 결과물을 구체화할 수 있습니다. 하지만 상담원에게 요청하는 작업이 복잡해짐에 따라 하나 이상의 하위 작업으로 인해 최종 결과가 약간 달라질 수 있는 변수가 발생할 가능성이 커지고 있습니다. 상담원 간 커뮤니케이션에 더 많이 의존하기 시작하면 이러한 차이는 더욱 심해질 것이며, 이러한 차이는 누적될 것입니다. 이는 상담원이 상호작용하는 툴이 컨텍스트 데이터를 정확하게 타겟팅할 수 있도록 매우 유연하고 조정이 가능해야 하며, 예상 출력 형식으로 응답해야 한다는 점을 다시 한 번 강조합니다. 또한 많은 사용 사례에서 에이전트와 툴의 상호 작용을 지시해야 할 필요가 있음을 나타내며, 바로 여기에서 워크플로우가 등장합니다!</p><p>Elastic은 곧 플랫폼의 핵심에 완전히 사용자 정의 가능한 워크플로우를 내장할 예정입니다. 이러한 워크플로는 상담원 및 툴과 양방향으로 작동할 수 있으므로 워크플로는 상담원 및 툴을 호출할 수 있고, 상담원 및 툴은 워크플로를 호출할 수 있게 됩니다. 이러한 기능이 모든 데이터가 있는 동일한 검색 AI 플랫폼에 완전히 통합되어 워크플로우를 혁신적으로 변화시킬 수 있는 잠재력은 매우 흥미롭습니다! 곧 출시됩니다!</p><h3>통합 메모리 뱅크로서의 Elastic</h3><p>실시간에 가까운 검색을 위해 만들어진 분산 데이터 플랫폼이기 때문에, Elastic은 에이전트 AI 시스템을 위한 장기 메모리 기능을 자연스럽게 수행합니다. 기본 제공되는 상담원 빌더 채팅 환경을 통해 단기 기억 및 채팅 기록을 추적하고 관리할 수도 있습니다. 그리고 전체 플랫폼이 API 우선이기 때문에, 에이전트의 컨텍스트 창을 압도할 수 있는 도구의 컨텍스트 출력을 유지(그리고 나중에 참조할 수 있도록)하기 위한 플랫폼으로 Elastic을 매우 쉽게 활용할 수 있습니다. 이 기술은 컨텍스트 엔지니어링 업계에서 &quot;<a href="https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents#:~:text=Agents%20can%20assemble%20understanding%20layer%20by%20layer%2C%20maintaining%20only%20what%27s%20necessary%20in%20working%20memory%20and%20leveraging%20note%2Dtaking%20strategies%20for%20additional%20persistence">메모 작성</a>&quot;이라고도 불립니다.</p><p>동일한 검색 플랫폼에서 단기 메모리와 장기 메모리를 모두 사용하면 많은 본질적인 이점을 얻을 수 있습니다. 채팅 기록과 지속적 문맥 반응을 향후 채팅 상호작용에 시맨틱 영향력의 일부로 사용하거나 위협 분석을 수행하거나 자주 반복되는 도구 호출에서 자동으로 생성되는 지속적 데이터 제품을 만들 수 있다고 상상해 보세요... 가능성은 무궁무진합니다!</p><h2>결론</h2><p>대규모 언어 모델의 등장으로 콘텐츠를 매칭하는 방식과 데이터를 조사하는 방법이 바뀌었습니다. 사람이 직접 조사하고, 맥락을 고려하고, 논리적 추론을 통해 질문에 답하는 현재의 세상에서 에이전트 AI를 통해 이러한 단계가 대부분 자동화되는 세상으로 빠르게 전환되고 있습니다. 생성된 답변을 신뢰할 수 있으려면 상담원이 답변을 <em></em> 생성할 때 <em>가장 관련성이 높은 모든</em> 정보(주관적 관련성 요소 포함)를 고려했다는 확신이 있어야 합니다. 에이전트 AI를 신뢰할 수 있게 만드는 기본 방법은 RAG 및 컨텍스트 엔지니어링 기술을 통해 추가 컨텍스트를 검색하는 도구를 기반으로 하는 것이지만, 이러한 도구가 <em>초기 검색을</em> 수행하는 방식은 응답의 정확성에 매우 중요할 수 있습니다.</p><p>Elastic Search AI 플랫폼은 정확성, 성능, 확장성 측면에서 에이전트 AI를 지원하는 여러 기본 제공 기능과 함께 하이브리드 검색의 유연성과 이점을 제공합니다. 즉, Elastic은 컨텍스트 엔지니어링의 여러 측면을 위한 환상적인 플랫폼입니다! 검색 플랫폼을 통해 컨텍스트 검색을 표준화함으로써 여러 측면에서 에이전트 도구 운영을 간소화하며, &#x27;느려야 빨리 간다&#x27;는 모순처럼 컨텍스트 생성 계층에서의 간소화는 더 빠르고 더 신뢰할 수 있는 에이전트 AI를 의미합니다.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[컨텍스트에 대한 이해 - 2부: 에이전트 AI와 컨텍스트 엔지니어링의 필요성]]></title>
            <description><![CDATA[에이전트 AI를 향한 LLM의 진화로 인해 RAG 컨텍스트 제한과 메모리 관리를 해결하기 위한 컨텍스트 엔지니어링의 필요성이 어떻게 증가하고 있는지 알아보세요.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/context-engineering-llm-evolution-agentic-ai</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/context-engineering-llm-evolution-agentic-ai</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Woody Walton]]></dc:creator>
            <pubDate>Tue, 18 Nov 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/6153ce399e5498841ef2ffa5363a3e2d2dea726d-720x420.jpg?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>LLM이 정보 검색의 기본 프로세스를 변화시킨 방식에 대한 (상당히 광범위한) <a href="https://www.elastic.co/search-labs/blog/context-engineering-hybrid-search-evolution-agentic-ai">배경</a> 지식을 바탕으로, 데이터 쿼리 방식도 어떻게 변화시켰는지 살펴봅시다.</p><h2>데이터와 상호 작용하는 새로운 방법</h2><p>제너레이티브(genAI) 및 에이전트 AI는 기존 검색과는 다른 방식으로 작업을 수행합니다. 과거에는 검색(&quot;구글에 검색해 볼게요...&quot;)을 통해 정보 조사를 시작했지만, 세대별 AI와 상담원 모두 채팅 인터페이스에 자연어를 입력하는 것이 시작 작업의 대부분을 차지합니다. 채팅 인터페이스는 LLM과의 토론으로, 의미론적 이해를 바탕으로 우리의 질문을 모든 종류의 정보에 대한 폭넓은 지식을 가진 오라클이 제공하는 것처럼 보이는 요약된 답변으로 바꿔줍니다. LLM의 진정한 매력은 드러나는 지식의 조각들을 하나로 묶어 일관성 있고 사려 깊은 문장을 만들어내는 능력입니다. 부정확하거나 완전히 환각적인 내용일지라도 <a href="https://en.wikipedia.org/wiki/Truthiness">진실성이</a> 담겨 있습니다.</p><p>우리가 익숙한 검색창은 우리가 <strong><em>직접</em></strong> 추론 에이전트였을 때 사용했던 RAG 엔진이라고 생각할 수 있습니다. 이제 인터넷 검색 엔진도 기존의 &#x27;사냥과 쪼기&#x27; 방식의 어휘 검색 경험을 AI 기반 개요로 전환하여 검색어에 대한 답변과 결과 요약을 제공함으로써 사용자가 개별 결과를 직접 클릭하고 평가할 필요가 없도록 돕고 있습니다.</p><h2>제너레이티브 AI &amp; RAG</h2><p>생성형 AI는 세상에 대한 의미론적 이해를 바탕으로 채팅 요청에 명시된 주관적 의도를 분석한 다음 추론 능력을 사용하여 즉석에서 전문가 답변을 생성합니다. 생성형 AI 상호작용에는 사용자의 입력/질문으로 시작하여 채팅 세션의 이전 대화를 추가 컨텍스트로 사용할 수 있으며, LLM에 추론 방법과 응답을 구성할 때 따라야 할 절차를 알려주는 지시 프롬프트 등 여러 부분이 있습니다. 안내 메시지는 &quot; &quot;5살짜리 아이처럼 설명해 주세요&quot;라는 단순한 안내에서 요청 처리 방법에 대한 완전한 분석으로 발전했습니다. 이러한 분류에는 종종 AI의 페르소나/역할, 생성 전 추론/내부 사고 과정, 객관적 기준, 제약 조건, 출력 형식, 대상에 대한 세부 사항을 설명하는 별도의 섹션과 예상 결과를 입증하는 데 도움이 되는 예시가 포함됩니다.</p><p>검색 증강 생성(RAG)은 사용자의 쿼리와 시스템 프롬프트 외에도 &quot;컨텍스트 창&quot;이라고 하는 추가 컨텍스트 정보를 제공합니다. RAG는 아키텍처에 중요한 추가 기능으로, LLM이 세계를 의미론적으로 이해하는 데 있어 누락된 부분을 알려주는 데 사용됩니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/5b9671d5d07d4caefde372bb3188000754a91eed-1470x746.png" style="width: 100%;" /><p>컨텍스트 창은 무엇을, 어디에, 얼마나 제공해야 하는지에 대해 다소 <a href="https://www.dbreunig.com/2025/06/22/how-contexts-fail-and-how-to-fix-them.html">까다로울</a> 수 있습니다. 물론 어떤 컨텍스트가 선택되는지도 매우 중요하지만, 제공된 컨텍스트의 신호 대 잡음비와 창 길이도 중요합니다.</p><h3>너무 적은 정보</h3><p>쿼리, 프롬프트 또는 컨텍스트 창에 너무 적은 정보를 제공하면 LLM이 응답을 생성할 올바른 의미적 컨텍스트를 정확하게 판단할 수 없기 때문에 착각이 발생할 수 있습니다. 문서 청크 크기의 벡터 유사성에도 문제가 있습니다. 짧고 간단한 질문이 벡터화된 지식창고에 있는 풍부하고 상세한 문서와 의미적으로 일치하지 않을 수 있습니다. <a href="https://medium.com/data-science/how-to-use-hyde-for-better-llm-rag-retrieval-a0aa5d0e23e8">가상의 문서 임베딩(HyDE)</a> 과 같은 쿼리 확장 기법이 개발되어 LLM을 사용하여 짧은 쿼리보다 더 풍부하고 표현력이 풍부한 가상의 답변을 생성할 수 있습니다. 물론 여기서 위험은 가상의 문서 자체가 올바른 맥락에서 더욱 멀어지게 하는 환각이라는 점입니다.</p><h3>너무 많은 정보</h3><p>우리 인간과 마찬가지로, 컨텍스트 창에 너무 많은 정보가 있으면 LLM이 중요한 부분이 무엇인지 압도당하고 혼란스러워할 수 있습니다. 컨텍스트 오버플로(또는 &quot;<a href="https://research.trychroma.com/context-rot">컨텍스트 썩</a>음&quot;)는 생성 AI 작업의 품질과 성능에 영향을 미치며, LLM의 &quot;주의 예산&quot;(작업 메모리)에 큰 영향을 미치고 여러 경쟁 토큰 간의 관련성을 희석시킵니다. &#x27;컨텍스트 썩음&#x27;의 개념에는 LLM이 중간 섹션의 콘텐츠보다 컨텍스트 창의 시작 또는 끝에 있는 콘텐츠를 선호하는 <a href="https://alexandrabarr.beehiiv.com/p/context-windows">위치 편향이</a> 있다는 관찰 결과도 포함됩니다.</p><h3>산만하거나 상충되는 정보</h3><p>컨텍스트 창이 커질수록 불필요하거나 상충되는 정보가 포함될 가능성이 높아져 LLM이 올바른 컨텍스트를 선택하고 처리하는 데 방해가 될 수 있습니다. 어떤 면에서는 가비지 인/가비지 아웃의 문제가 됩니다. 문서 결과 집합을 컨텍스트 창에 덤핑하는 것만으로도 LLM이 처리해야 할 정보가 너무 많지만, 컨텍스트가 어떻게 선택되었는지에 따라 상충되거나 관련 없는 정보가 스며들 가능성이 더 커질 수 있기 때문입니다.</p><h2>에이전틱 AI</h2><p>다뤄야 할 내용이 많다고 말씀드렸지만, 드디어 에이전트 AI 주제에 대해 이야기하게 되었습니다! 에이전트 AI는 사용자가 제공한 지식과 문맥 정보를 바탕으로 답변을 합성하는 제너레이티브 AI의 (이미 &#x27;레거시&#x27;라고 불러도 될까요?) 기능을 확장한 LLM 채팅 인터페이스의 매우 흥미로운 새로운 사용법입니다. 제너레이티브 AI가 더욱 성숙해지면서 처음에는 사람이 쉽게 확인/검증할 수 있는 지루하고 위험도가 낮은 활동으로 한정했던 작업을 LLM이 수행할 수 있는 일정 수준의 작업과 자동화가 가능하다는 것을 깨달았습니다. 짧은 기간 동안 초기 범위가 확장되어 이제 LLM 채팅 창은 AI 에이전트가 자율적으로 계획을 수립하고 실행하며 지정된 목표를 달성하기 위해 계획을 반복적으로 평가하고 조정하도록 하는 촉매제가 될 수 있습니다. 상담원은 LLM의 추론, 채팅 기록 및 사고 기억(있는 그대로)에 액세스할 수 있으며, 이러한 목표를 위해 활용할 수 있는 특정 도구도 마련되어 있습니다. 또한 최상위 에이전트가 각각 고유한 로직 체인, 명령어 세트, 컨텍스트 및 도구를 갖춘 여러 <a href="https://www.philschmid.de/the-rise-of-subagents">하위 에이전트의</a> 오케스트레이터로 기능할 수 있는 아키텍처도 등장하고 있습니다.</p><p>상담원은 대부분 자동화된 워크플로우의 시작점으로, 사용자와 채팅한 다음 &#x27;로직&#x27;을 사용하여 사용자의 질문에 답할 수 있는 도구를 결정할 수 있다는 점에서 자기 주도적입니다. 도구는 일반적으로 에이전트에 비해 수동적인 것으로 간주되며 한 가지 유형의 작업을 수행하도록 만들어집니다. 툴이 수행할 수 있는 작업의 <em>유형은</em> 무궁무진하지만(정말 흥미롭습니다!) 툴이 수행하는 주요 작업은 상담원이 워크플로우를 실행할 때 고려할 수 있도록 컨텍스트 정보를 수집하는 것입니다.</p><p>에이전트 AI는 아직 초기 단계의 기술로서 주의력 결핍 장애에 해당하는 LLM에 취약하며, 요청받은 작업을 쉽게 잊어버리고 업무에 전혀 포함되지 않은 다른 일을 하러 도망가는 경우가 많습니다. 겉보기에는 마술처럼 보이지만, LLM의 &#x27;추론&#x27; 능력은 여전히 시퀀스에서 다음으로 가능성이 높은 토큰을 예측하는 것을 기반으로 합니다. 추론(또는 언젠가는 인공 일반 지능(AGI)이)이 신뢰할 수 있고 신뢰할 수 있으려면 정확한 최신 정보가 주어졌을 때 우리가 기대하는 방식으로 추론할 수 있는지(그리고 우리가 미처 생각하지 못했던 것을 조금 더 제공할 수도 있는지) 검증할 수 있어야 합니다. 이를 위해서는 에이전트 아키텍처가 명확하게 커뮤니케이션하고(프로토콜), 워크플로와 제약 조건을 준수하며(가드레일), 작업의 현재 위치를 기억하고(상태), 사용 가능한 메모리 공간을 관리하고, 응답이 정확하고 작업 기준을 충족하는지 검증할 수 있는 기능이 필요합니다.</p><h2>내가 이해할 수 있는 언어로 대화하기</h2><p>새로운 개발 영역에서 흔히 그렇듯이(특히 LLM의 세계에서는 더욱 그렇습니다) 처음에는 에이전트 간 커뮤니케이션을 위한 여러 가지 접근 방식이 있었지만, 사실상의 표준인 <a href="https://modelcontextprotocol.io/docs/getting-started/intro">모델 컨텍스트 프로토콜(MCP)</a> 로 빠르게 수렴되었습니다. 모델 컨텍스트 프로토콜의 정의는 이름 그대로 <strong></strong> <strong>모델이</strong> <strong>컨텍스트</strong> 정보를 요청하고 수신하는 데 사용하는 프로토콜입니다. MCP는 LLM 에이전트가 외부 도구 및 데이터 소스에 연결할 수 있는 범용 어댑터 역할을 하며, 서로 다른 LLM 프레임워크와 도구가 쉽게 상호 운용될 수 있도록 API를 단순화하고 표준화합니다. 따라서 MCP는 에이전트가 목표를 달성하기 위해 자율적으로 수행하도록 에이전트에 제공되는 오케스트레이션 로직 및 시스템 프롬프트와 보다 격리된 방식으로 수행하도록 툴로 전송되는 작업(적어도 시작 에이전트와 관련해서는 격리된) 사이의 일종의 구심점 역할을 합니다.</p><p>이 에코시스템은 모든 방향이 새로운 개척지처럼 느껴질 정도로 모든 것이 새롭습니다. 에이전트 간 상호작용을 위한 유사한 프로토콜<a href="https://developers.googleblog.com/en/a2a-a-new-era-of-agent-interoperability/">(에이전트2에이전트(A2A)</a> natch!)과 에이전트 추론 메모리 개선<a href="https://venturebeat.com/ai/new-memory-framework-builds-ai-agents-that-can-handle-the-real-worlds">(ReasoningBank</a>), 현재 작업에 가장 적합한 MCP 서버 선택<a href="https://arxiv.org/abs/2505.03275">(RAG-MCP</a>), 입력 및 출력의 제로 샷 분류 및 패턴 감지 등의 의미 분석을 <a href="https://openai.github.io/openai-guardrails-python/">가드레일로</a> 사용하여 에이전트의 작업 허용 대상을 제어하기 위한 다른 프로젝트도 있습니다.</p><p>이러한 각 프로젝트의 기본 의도가 에이전트/genAI 컨텍스트 창에 반환되는 정보의 품질과 제어를 개선하는 것임을 눈치채셨나요? 에이전트 AI 생태계가 컨텍스트 정보를 더 잘 처리(제어, 관리 및 운영)할 수 있는 기능을 계속 개발하는 동안에도 에이전트가 밀링할 수 있는 <em>가장 관련성</em> 높은 컨텍스트 정보를 검색해야 할 필요성은 항상 존재할 것입니다.</p><h2>컨텍스트 엔지니어링에 오신 것을 환영합니다!</h2><p>제너레이티브 AI 용어에 익숙하다면 &#x27;프롬프트 엔지니어링&#x27;에 대해 들어보셨을 텐데요, 지금은 거의 유사 과학에 가깝다고 할 수 있습니다. 프롬프트 엔지니어링은 LLM이 응답을 생성할 때 사용할 동작을 사전에 설명하는 가장 효율적인 최선의 방법을 찾는 데 사용됩니다. &#x27;<a href="https://www.elastic.co/search-labs/blog/context-engineering-overview">컨텍스트 엔지니어링</a>&#x27;은 &#x27;프롬프트 엔지니어링&#x27; 기술을 에이전트 측면을 넘어 MCP 프로토콜의 도구 측면에서 사용 가능한 컨텍스트 소스 및 시스템까지 포함하도록 확장하며, 컨텍스트 관리, 처리 및 생성에 대한 광범위한 주제를 다룹니다:</p><ul><li><strong>컨텍스트 관리 </strong>- 장기 실행 및/또는 복잡한 상담원 워크플로 전반에서 상태 및 컨텍스트 효율성을 유지하는 것과 관련이 있습니다. 상담원의 목표를 달성하기 위한 작업 및 도구 호출의 반복적인 계획, 추적 및 오케스트레이션. 상담원이 작업할 수 있는 &#x27;주의 예산&#x27;이 제한되어 있기 때문에 컨텍스트 관리는 주로 컨텍스트 창을 세분화하여 전체 범위와 가장 중요한 컨텍스트(정확도 대비 회상률!)를 모두 포착하는 데 도움이 되는 기술과 관련이 있습니다. 압축, 요약, 이전 단계 또는 도구 호출의 컨텍스트를 유지하여 작업 메모리에 후속 단계의 추가 컨텍스트를 위한 공간을 확보하는 기술 등이 있습니다.</li><li><strong>컨텍스트 처리 </strong>- 에이전트가 모든 컨텍스트를 다소 일관된 방식으로 추론할 수 있도록 서로 다른 소스에서 얻은 컨텍스트를 통합, 정규화 또는 정제하는 논리적이고 대부분 프로그램적인 단계입니다. 기본 작업은 에이전트가 최대한 효율적으로 소비할 수 있는 모든 소스(프롬프트, RAG, 메모리 등)에서 컨텍스트를 만드는 것입니다. </li><li>컨텍스트 <strong>생성 </strong>- 컨텍스트 처리가 검색된 컨텍스트를 상담원이 사용할 수 있도록 만드는 것이라면 컨텍스트 생성은 상담원이 마음대로 추가 컨텍스트 정보를 요청하고 수신할 수 있는 기능을 제공하지만 제약 조건도 함께 제공합니다.</li></ul><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/4a8240e1eb078b2294b8d981b9caa8593589cac4-1600x900.png" style="width: 100%;" /><p>LLM 채팅 애플리케이션의 다양한 형태는 컨텍스트 엔지니어링의 높은 수준의 기능에 직접적으로(때로는 중복되는 방식으로) 매핑됩니다:</p><ul><li><strong>지침/시스템 프롬프트</strong> - 프롬프트는 생성(또는 에이전트) AI 활동이 사용자의 목표를 달성하기 위해 어떻게 사고를 유도할지에 대한 발판이 됩니다. 프롬프트는 그 자체로 컨텍스트이며, 단순한 톤의 지시가 아니라 사용자의 요청을 완전히 충족하는지 확인하기 위해 응답하기 전에 &#x27;단계별로 생각하기&#x27;, &#x27;심호흡하기&#x27; 등의 작업 실행 로직과 규칙이 포함되어 있는 경우가 많습니다. 최근 테스트에 따르면 마크업 언어는 프롬프트의 여러 부분을 구성하는 데 매우 효과적이지만 너무 모호한 것과 너무 구체적인 것 사이의 적절한 지점으로 지침을 조정하는 데에도 주의를 기울여야 합니다. 우리는 LLM이 올바른 맥락을 찾을 수 있도록 충분한 지침을 제공하되 너무 규범적이어서 예상치 못한 통찰력을 놓치지 않기를 원합니다.</li><li><strong>단기 메모리</strong> (상태/기록) - 단기 메모리는 본질적으로 사용자와 LLM 간의 채팅 세션 상호작용입니다. 이는 라이브 세션에서 컨텍스트를 구체화하는 데 유용하며, 나중에 검색하고 계속 사용할 수 있도록 저장할 수 있습니다. </li><li><strong>장기 기억</strong> - 장기 기억은 여러 세션에 걸쳐 유용한 정보로 구성되어야 합니다. 또한 RAG를 통해 액세스하는 도메인별 지식 기반뿐만 아니라, 최근 연구에서는 이전 에이전트/제너레이티브 AI 요청의 결과를 사용하여 현재 에이전트 상호 작용 내에서 학습하고 참조할 수 있습니다. 장기 기억 공간에서 가장 흥미로운 혁신 중 일부는 에이전트가 중단한 부분을 다시 시작할 수 있도록 상태를 <a href="https://steve-yegge.medium.com/introducing-beads-a-coding-agent-memory-system-637d7d92514a">저장하고 연결하는</a> 방식을 조정하는 것과 관련이 있습니다. </li><li><strong>구조화된 출력</strong> - 인지에는 노력이 필요하므로 추론 능력이 있더라도 (인간과 마찬가지로) LLM도 생각할 때 노력을 덜 들이고 싶어하며, 정의된 API나 프로토콜이 없는 경우 도구 호출에서 반환된 데이터를 읽는 방법에 대한 맵(스키마)이 있으면 매우 유용하다는 것은 놀라운 일이 아닐 수 없습니다. 에이전트 프레임워크의 일부로 <a href="https://platform.openai.com/docs/guides/structured-outputs?lang=javascript">구조화된 출력을</a> 포함하면 이러한 기계 간 상호 작용을 더 빠르고 안정적으로 수행할 수 있으며, 사고에 기반한 구문 분석이 덜 필요하게 됩니다.</li><li><strong>사용 가능한 도구</strong> - 도구는 추가 정보 수집(예: 엔터프라이즈 데이터 리포지토리에 대한 RAG 쿼리 발행 또는 온라인 API를 통한)부터 상담원을 대신하여 자동화된 작업 수행(예: 상담원의 요청 기준에 따라 호텔 객실 예약)까지 모든 종류의 작업을 수행할 수 있습니다. 도구는 자체 에이전트 처리 체인을 가진 하위 에이전트가 될 수도 있습니다. </li><li><strong>검색 증강 생성(RAG)</strong> - &quot;동적 지식 통합&quot;이라는 RAG에 대한 설명이 정말 마음에 듭니다. 앞서 설명한 것처럼 RAG는 학습할 때 LLM이 접근하지 못했던 추가 정보를 제공하거나 정답을 얻기 위해 가장 중요하다고 생각되는 아이디어, 즉 주관적인 질문과 가장 관련성이 높은 아이디어를 반복하는 기법입니다.</li></ul><h2>경이로운 우주의 힘, 아주 작은 생활 공간!</h2><p>에이전트 AI에는 탐험할 수 있는 흥미롭고 새로운 영역이 정말 많습니다! 여전히 해결해야 할 오래된 전통적인 데이터 검색 및 처리 문제도 많지만, 새로운 LLM 시대를 맞아 이제야 빛을 보게 된 완전히 새로운 종류의 문제도 있습니다. 현재 우리가 당면한 많은 문제는 제한된 작업 메모리 공간에 부담을 주지 않으면서도 LLM에 필요한 추가 컨텍스트 정보를 제공하는 컨텍스트 엔지니어링과 관련이 있습니다.</p><p>다양한 도구(및 다른 에이전트)에 액세스할 수 있는 반자율 에이전트의 유연성으로 인해 AI 구현을 위한 새로운 아이디어가 너무 많이 생겨나서 어떤 방식으로 조합할 수 있을지 가늠하기조차 어렵습니다. 현재 대부분의 연구는 컨텍스트 엔지니어링 분야에 속하며 더 많은 양의 컨텍스트를 처리하고 추적할 수 있는 메모리 관리 구조를 구축하는 데 중점을 두고 있는데, 이는 LLM이 실제로 해결하기를 원하는 심층 사고 문제는 복잡성이 증가하고 기억이 매우 중요한 장기적이고 다단계적인 사고 단계가 존재하기 때문입니다.</p><p>현장에서 진행 중인 많은 실험은 에이전트에게 최적의 작업 관리 및 도구 구성을 제공하기 위해 노력하고 있습니다. 상담원의 추론 체인에서 각 툴 호출은 해당 툴의 기능을 수행하기 위한 컴퓨팅과 제한된 컨텍스트 창에 미치는 영향 측면에서 누적 비용을 발생시킵니다. LLM 에이전트의 컨텍스트를 관리하는 최신 기술 중 일부는 장기 실행 작업에 대해 누적된 컨텍스트를 압축/요약하면 손실이 너무 <em>커지는</em>&#x27; 컨텍스트 붕괴&#x27;와<a href="https://venturebeat.com/ai/ace-prevents-context-collapse-with-evolving-playbooks-for-self-improving-ai">같은 의도하지 않은 연쇄 효과를 발생시켰습니다.</a> 원하는 결과는 간결하고 정확한 컨텍스트를 반환하는 도구로, 불필요한 정보가 소중한 컨텍스트 창 메모리 공간으로 유출되지 않도록 하는 것입니다.</p><h3>너무 많은/너무 많은 가능성</h3><p>도구/구성 요소를 재사용할 수 있는 유연성을 갖춘 업무 분리를 원하므로 특정 데이터 소스에 연결하기 위한 전용 에이전트 도구를 만드는 것이 좋습니다. 각 도구는 한 유형의 리포지토리, 한 유형의 데이터 스트림 또는 하나의 사용 사례 쿼리에 특화될 수 있습니다. 하지만 주의하세요: 시간/비용을 절약하고 무언가 가능하다는 것을 증명하기 위해 LLM을 연합 도구로 사용하고 싶은 강한 유혹이 있을 것입니다... 그러지 마세요, 저희도 <a href="https://www.elastic.co/pdf/elastic-distributed-not-federated-search.pdf">그 길을</a> 가본 적이 있습니다! 연합 쿼리는 들어오는 쿼리를 원격 리포지토리가 이해하는 구문으로 변환한 다음 여러 소스의 결과를 어떻게든 일관된 응답으로 합리화해야 하는 &#x27;범용 번역기&#x27; 같은 역할을 합니다. 페더레이션은 소규모에서는 <em></em> <em>잘</em> 작동하지만 대규모, 특히 데이터가 멀티모달인 경우 페더레이션은 너무 넓은 간격을 메우기 위해 시도합니다.</p><p>에이전트 세계에서는 에이전트가 페더레이터가 되고 도구(MCP를 통해)는 서로 다른 리소스에 수동으로 정의된 연결이 됩니다. 전용 도구를 사용하여 서로 연결되지 않은 데이터 원본에 접근하는 것은 쿼리별로 서로 다른 데이터 스트림을 동적으로 통합하는 강력하고 새로운 방법처럼 보일 수 있지만, 도구를 사용하여 여러 원본에 동일한 질문을 하는 것은 결국 해결되는 문제보다 더 많은 문제를 야기할 수 있습니다. 이러한 데이터 소스 각각은 그 아래에 서로 다른 유형의 리포지토리가 있을 가능성이 높으며, 그 안의 데이터를 검색, 순위 지정 및 보호하기 위한 고유한 기능을 갖추고 있습니다. 물론 리포지토리 간의 이러한 차이 또는 &quot;임피던스 불일치&quot;는 처리 부하를 증가시킵니다. 또한 상충되는 정보나 신호가 발생할 수 있는데, 점수 정렬 오류처럼 별것 아닌 것처럼 보이는 것이 반환된 컨텍스트의 중요성을 크게 떨어뜨리고 결국 생성된 응답의 관련성에 영향을 미칠 수 있습니다.</p><h3>컴퓨터에서도 컨텍스트 전환은 어렵습니다.</h3><p>에이전트를 임무에 파견할 때 가장 먼저 해야 할 일은 해당 에이전트가 액세스할 수 있는 모든 관련 데이터를 찾는 것입니다. 상담원이 연결한 각 데이터 소스가 서로 다르거나 분리된 응답으로 응답하는 경우 사람과 마찬가지로, 검색된 콘텐츠에서 중요한 맥락적 비트를 추출하는 것과 관련된 인지적 부하(정확히 같은 종류는 아니지만)가 발생할 수 있습니다. 여기에는 시간/계산이 필요하며, 에이전트 로직 체인에서 각각의 작은 부분이 합산됩니다. 따라서 <a href="https://blog.cloudflare.com/code-mode/">MCP에</a> 대해 논의되고 있는 것과 마찬가지로 대부분의 에이전트 툴은 알려진 입력 및 출력을 가진 격리된 함수, 즉 다양한 종류의 에이전트의 요구 사항을 지원하도록 조정된 API처럼 작동해야 한다는 결론에 도달하게 됩니다. 특히 자연어를 구조화된 구문으로 번역하는 작업과 같이 참조할 <a href="https://arxiv.org/html/2501.12372v5">스키마가</a> 있는 경우(실제로 RTFM!) 의미적 점들을 훨씬 더 잘 연결할 수 있다는 사실도 깨닫고 있습니다.</p><h2>7회 연장!</h2><p>지금까지 <a href="https://www.elastic.co/search-labs/blog/context-engineering-hybrid-search-evolution-agentic-ai">LLM이 데이터 검색 및 쿼리에 미치는 영향과</a> 채팅 창이 상담원 AI 경험으로 어떻게 발전하고 있는지에 대해 살펴보았습니다. 이 두 가지 주제를 종합하여 컨텍스트 엔지니어링에서 새로운 검색 및 검색 기능을 사용하여 결과를 개선하는 방법을 살펴봅시다. <a href="https://www.elastic.co/search-labs/blog/context-engineering-hybrid-search-agentic-ai-accuracy">3부: 컨텍스트 엔지니어링에서 하이브리드 검색의 힘</a>!</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Azure AKS 자동에 Elasticsearch를 배포하는 방법]]></title>
            <description><![CDATA[부분 관리형 Elasticsearch 설정 구성을 위해 AKS Automatic과 ECK를 사용하여 Azure에서 Kibana와 함께 Elasticsearch를 배포하는 방법을 알아보세요.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/elasticsearch-azure-aks-automatic-deployment</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/elasticsearch-azure-aks-automatic-deployment</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Eduard Martin]]></dc:creator>
            <pubDate>Fri, 14 Nov 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/4d821659a606e04390b09215e9a0d32eb01f0d1b-854x489.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>이 문서는 다양한 인프라를 사용하여 Elasticsearch를 설치하는 방법을 알아보는 시리즈의 일부입니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/0a65ca8b62fd8a42d7751b8f4bf057e33d877304-940x458.png" style="width: 100%;" /><p>ECK는 마켓플레이스 기반 Elastic Cloud 솔루션보다 훨씬 더 많은 노력이 필요하지만, Kubernetes 운영자가 시스템 오케스트레이션과 노드 확장을 처리하기 때문에 직접 VM을 배포하는 것보다 훨씬 더 자동화되어 있습니다.</p><p>이번에는 자동을 사용하여 Azure Kubernetes Service(AKS)로 작업해 보겠습니다. 다른 문서에서는 <a href="https://www.elastic.co/search-labs/blog/azure-elasticsearch-vm-deployment">Azure VM</a> 및 <a href="https://www.elastic.co/search-labs/blog/deploy-elasticsearch-azure-marketplace">Azure Marketplace를</a> 사용하는 방법에 대해 알아보세요.</p><h2>AKS 자동이란 무엇인가요?</h2><p><a href="https://learn.microsoft.com/en-us/azure/aks/intro-aks-automatic">Azure Kubernetes Service(AKS)는 </a>클러스터 설정을 자동으로 관리하고, 리소스를 동적으로 할당하고, 보안 모범 사례를 통합하는 동시에 Kubernetes 유연성을 유지하므로 개발자는 컨테이너 이미지에서 배포된 애플리케이션으로 몇 분 내에 전환할 수 있습니다.</p><p>AKS Automatic은 대부분의 클러스터 관리 오버헤드를 제거하고 단순성과 유연성 간의 균형을 잘 맞추고 있습니다. 올바른 선택은 사용 사례에 따라 다르지만, 계획이 있다면 더 쉽게 결정할 수 있습니다:</p><ul><li><strong>테스트 환경을 배포하세요: </strong>배포가 빠르고 간단하여 빠른 실험이나 단기간의 클러스터에 이상적입니다.</li><li><strong>엄격한 가상 머신, 스토리지 또는 네트워킹 요구 사항 없이 작업하세요: </strong>AKS 자동은 사전 정의된 기본값을 제공하므로 필요에 맞는 기본값을 사용하면 추가 구성이 필요하지 않습니다.</li><li><strong>Kubernetes로 처음 시작하세요: </strong>AKS Automatic은 클러스터 설정의 대부분을 처리함으로써 학습 곡선을 낮추고 팀이 애플리케이션에 집중할 수 있도록 합니다.</li></ul><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/2e4c09b8c5e0ce5e8ea9c369626a373b7030a5ba-854x489.png" style="width: 100%;" /><p>Elasticsearch의 경우, Elastic Stack의 Kubernetes 배포 오케스트레이션을 간소화하는 공식 Elastic Kubernetes 운영자인 ECK(Elastic <a href="https://www.elastic.co/docs/deploy-manage/deploy/cloud-on-k8s">Cloud on Kubernetes </a>)를 사용하겠습니다.</p><h2>AKS 자동 설정 방법</h2><p>1. <a href="https://azure.microsoft.com/">Microsoft Azure 포털에</a> 로그인합니다.</p><p>2.<strong>오른쪽 상단의  Cloud Shell</strong> 버튼()을 클릭하여 콘솔에 액세스하고 거기에서 AKS 클러스터를 배포합니다. 또는 <a href="https://learn.microsoft.com/en-us/azure/cloud-shell/overview">Azure Cloud Shell을</a> 사용할 수도 있습니다.</p><p><strong><em>튜토리얼을 진행하는 동안 프로젝트 ID를 자신의 것으로 업데이트하는 것을 잊지 마세요.</em></strong></p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/0aa60605777c0a6e3aef8faa4e54388c2cb582c8-624x495.png" style="width: 100%;" /><p><em>AKS를 열면 위의 스크린샷과 같은 화면이 나타납니다.</em></p><p>3. aks-preview Azure CLI 확장을 설치합니다. 이 미리 보기 버전에서는 클러스터 생성 시 <code>--sku automatic</code> 을 선택하면 AKS 자동 기능을 사용할 수 있습니다.</p><pre><code>az extension add --name aks-preview</code></pre><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/2e05ab67fc554c5fb5208683c179fdeaeadd95db-624x56.png" style="width: 100%;" /><p><em>이 메시지가 표시되면 AKS 확장 프로그램이 제대로 설치되었다는 뜻입니다.</em></p><p>4. <code>az feature register</code> 명령을 사용하여<a href="https://learn.microsoft.com/en-us/azure/azure-app-configuration/concept-feature-management"> 기능 플래그</a> 등록하기</p><pre><code>az feature register --namespace Microsoft.ContainerService --name AutomaticSKUPreview</code></pre><p><em>방금 생성한 기능 구독에 대한 세부 정보가 표시됩니다:</em></p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/3aca710c1f312ba91de461638e518386919ec722-801x138.png" style="width: 100%;" /><p>등록 상태가 &quot;등록<strong><em>중</em></strong>&quot;에서 &quot;<strong><em>등록됨</em></strong>&quot;으로 변경될 때까지 등록 상태를 확인합니다. 등록을 완료하는 데 몇 분 정도 걸릴 수 있습니다.</p><pre><code>az feature show --namespace Microsoft.ContainerService --name AutomaticSKUPreview</code></pre><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/186486b08ab8e1c372efaff50f10cbddeaf4e0cd-844x177.png" style="width: 100%;" /><p><code>az provider register</code> 을 실행하여 변경 사항을 전파합니다.</p><pre><code>az provider register --namespace Microsoft.ContainerService</code></pre><p>5. 리소스 그룹 만들기</p><p>리소스 그룹은 관리 및 배포할 Azure 리소스의 논리적 그룹입니다.</p><pre><code>az group create --name elastic-resource --location eastus</code></pre><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/d1952269e97d94f914020754bd02702f9eafd037-770x212.png" style="width: 100%;" /><p>6. 자동 조종 장치 클러스터를 생성합니다. 이름을 <strong><em>myAKSAutomaticCluster로 </em></strong>지정하고 방금 만든 리소스 그룹을 사용하겠습니다. 다음 VM 크기 중 하나에서 <strong><em>16개의 vCPU를</em></strong> 사용할 수 있는지 확인합니다: <a href="https://learn.microsoft.com/en-us/azure/virtual-machines/sizes/general-purpose/dpsv5-series">Standard_D4pds_v5</a>, <a href="https://learn.microsoft.com/en-us/azure/virtual-machines/sizes/general-purpose/dldsv5-series">Standard_D4lds_v5</a>, <a href="https://learn.microsoft.com/en-us/azure/virtual-machines/sizes/general-purpose/dadsv5-series">Standard_D4ads_v5</a>, <a href="https://learn.microsoft.com/en-us/azure/virtual-machines/sizes/general-purpose/ddsv5-series">Standard_D4ds_v5</a>, <a href="https://learn.microsoft.com/en-us/azure/virtual-machines/sizes/general-purpose/ddv5-series">Standard_D4d_v5</a>, <a href="https://learn.microsoft.com/en-us/azure/virtual-machines/sizes/general-purpose/ddv4-series">Standard_D4d_v4</a>, <a href="https://learn.microsoft.com/en-us/azure/virtual-machines/sizes/general-purpose/dsv3-series">Standard_DS3_v2</a>, AKS용 <a href="https://learn.microsoft.com/en-us/azure/virtual-machines/sizes/memory-optimized/dv2-dsv2-series-memory">Standard_DS12_v2</a> 리소스 할당에 사용할 수 있습니다.</p><pre><code>az aks create \
    --resource-group elastic-resource \
    --name myAKSAutomaticCluster \
    --sku automatic \
    --generate-ssh-keys</code></pre><p>*<em><code>MissingSubscriptionRegistration</code></em>오류가 <em>표시되면  누락된 구독이 있는 4단계로 돌아가세요. 예를 들어 <code>The subscription is not registered to use namespace &#x27;<strong>microsoft.insights</strong>&#x27;</code> 실행이 필요합니다. <code>az provider register --namespace Microsoft.Insights.</code></em></p><p>대화형 로그인을 따르세요:</p><p><em>&quot;az login&quot;을 실행하라는 메시지가 표시됩니다. 해당 명령을 실행한 다음 기다려야 합니다.</em></p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/14c0238f755fe6347519e69d3cb28c0fa52ec044-775x203.png" style="width: 100%;" /><p>7. 준비가 될 때까지 기다립니다. 생성하는 데 약 10분이 걸립니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/eb00c3ad18f884f47db6645b196808ebec07c1fc-797x177.png" style="width: 100%;" /><p>8. kubectl 명령줄 액세스를 구성한다.</p><pre><code>az aks get-credentials --resource-group elastic-resource --name myAKSAutomaticCluster</code></pre><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/5136d72a5d455345b0b6205bb232c4bdf7762998-793x52.png" style="width: 100%;" /><p><em>저희가 설치한 확장 프로그램은 AKS 자동을 활성화하고 있습니다.</em></p><p>9. 노드가 배포되었는지 확인합니다.</p><pre><code>kubectl get nodes</code></pre><p>금지된 오류 메시지가 표시되면 오류에서 사용자 ID를 복사합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/d6c021fa54f4db00d2d795f5ba9b5a93376d03cd-818x47.png" style="width: 100%;" /><p>10. AKS 액세스 제어에 사용자를 추가합니다.</p><p>AKS ID를 받습니다. 명령에서 출력을 복사합니다.</p><pre><code>az aks show --resource-group elastic-resource  --name myAKSAutomaticCluster --query id --output tsv</code></pre><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/22a1cdc538bd379812a752c6a368a0651000abb8-810x36.png" style="width: 100%;" /><p>AKS ID와 사용자의 대표 ID를 사용하여 역할 할당을 생성합니다.</p><pre><code>az role assignment create --role "Azure Kubernetes Service RBAC Cluster Admin" --assignee <PRINCIPAL_ID> --scope <AKS_ID></code></pre><p>11. 노드가 다시 배포되었는지 확인합니다.</p><pre><code>kubectl get nodes</code></pre><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/c9b3a5c1cc540ef732c3e7f60b0a973bdbd0b6fd-617x99.png" style="width: 100%;" /><p>12. Kubernetes(ECK) 운영자에 Elastic Cloud를 설치합니다.</p><pre><code># Install ECK Custom Resource Definitions
kubectl create -f https://download.elastic.co/downloads/eck/2.16.1/crds.yaml

# Install the ECK operator
kubectl apply -f https://download.elastic.co/downloads/eck/2.16.1/operator.yaml</code></pre><p>13. 기본값을 사용하여 단일 노드 Elasticsearch 인스턴스를 생성해 보겠습니다.</p><pre><code>cat <<EOF | kubectl apply -f -
apiVersion: elasticsearch.k8s.elastic.co/v1
kind: Elasticsearch
metadata:
  name: quickstart
spec:
  version: 9.0.0
  nodeSets:
  - name: default
    count: 1
    config:
      node.store.allow_mmap: false
EOF</code></pre><p>기본 AKS 머신의 <code>vm.max_map_count</code> 값이 너무 낮기 때문에 <code>nmap</code> 을 비활성화했습니다. 비활성화하는 것은 프로덕션에는 권장되지 않지만 <code>vm.max_map_count</code> 의 값을 늘리는 것은 권장됩니다. 자세한 방법은 <a href="https://www.elastic.co/docs/deploy-manage/deploy/cloud-on-k8s/virtual-memory">여기에서</a> 확인할 수 있습니다.</p><p>14. 또한 Kibana 단일 노드 클러스터를 배포해 보겠습니다. Kibana의 경우, 로드 밸런서를 추가하여 장치에서 Kibana에 접속하는 데 사용할 수 있는 외부 IP를 제공합니다.</p><pre><code>cat <<EOF | kubectl apply -f -
apiVersion: kibana.k8s.elastic.co/v1
kind: Kibana
metadata:
  name: quickstart
spec:
  version: 9.0.0
  http:
    service:
      spec:
        type: LoadBalancer
  count: 1
  elasticsearchRef:
    name: quickstart
EOF</code></pre><p>기본적으로 AKS 자동은 로드 밸런서를 공개로 구성하며, 메타데이터 주석을 설정하여 동작을 변경할 수 있습니다:</p><p><code>service.beta.kubernetes.io/azure-load-balancer-internal: &quot;true&quot;</code></p><p>15. 파드가 실행 중인지 확인합니다.</p><pre><code>kubectl get pods</code></pre><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/213a897176c0af6cea19c7c777cfaf8734e3ee6e-616x84.png" style="width: 100%;" /><p>16. 또한 <code>kubectl get elasticsearch</code> 및 <code>kubectl get kibana</code> 을 실행하여 Elasticsearch 버전, 노드 및 상태와 같은 보다 구체적인 통계를 확인할 수 있습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/c84988e725ef892eddd8fb7e5a03d58c35a8f9d6-470x62.png" style="width: 100%;" /><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/452dd03d314cd00c8a3c19e19862b968592a0435-415x62.png" style="width: 100%;" /><p>17. 서비스에 액세스합니다.</p><pre><code>kubectl get svc</code></pre><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/b2d3e8f368be22b89aa2ed4d4d514f97dd6cbabd-624x115.png" style="width: 100%;" /><p>그러면 EXTERNAL-IP 아래에 Kibana의 <a href="https://www.elastic.co/docs/deploy-manage/deploy/cloud-on-k8s/accessing-services">외부 URL이</a> 표시됩니다. 로드 밸런서가 프로비저닝하는 데 몇 분 정도 걸릴 수 있습니다. <strong><em>EXTERNAL-IP 값을 복사합니다.</em></strong></p><p>18. &#x27;elastic&#x27; 사용자의 Elasticsearch 비밀번호를 가져옵니다:</p><pre><code>kubectl get secret quickstart-es-elastic-user -o=jsonpath='{.data.elastic}' | base64 --decode</code></pre><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/bd74537f8c35c4e027c518913fdb0a0524621d56-624x31.png" style="width: 100%;" /><p>19. 브라우저를 통해 <strong>Kibana에 액세스합니다</strong>:</p><p>a. URL: https://&lt;EXTERNAL_IP&gt;:5601</p><p>b. 사용자 이름:elastic</p><p>c. 비밀번호: c44A295CaEt44D6xIzN6Zs5m(이전 단계부터)</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/a601112527d80721b292328ed8da58386d2837eb-463x503.png" style="width: 100%;" /><p>20. 브라우저에서 Elastic Cloud에 액세스하면 시작 화면이 표시됩니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/f91e71fa961d215a8d886601d1a9fc5c452ce329-1999x1256.png" style="width: 100%;" /><p>노드 변경 또는 크기 조정과 같이 Elasticsearch 클러스터 사양을 변경하려는 경우, 새 설정으로 YML 매니페스트를 다시 적용할 수 있습니다:</p><pre><code>cat <<EOF | kubectl apply -f -
apiVersion: elasticsearch.k8s.elastic.co/v1
kind: Elasticsearch
metadata:
  name: quickstart
spec:
  version: 9.0.0
  nodeSets:
    - name: default
      count: 2
      config:
        node.store.allow_mmap: false
      podTemplate:
        spec:
          containers:
            - name: elasticsearch
              resources:
                requests:
                  memory: 1.5Gi
                  cpu: 2
                limits:
                  memory: 1.5Gi
                  cpu: 2
EOF</code></pre><p>이 예제에서는 노드를 하나 더 추가하고 RAM과 CPU를 수정하겠습니다. 보시다시피 이제 <code>kubectl get elasticsearch</code> 에 2개의 노드가 표시됩니다:</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/4b139a476b50933d45d99e09479112817964f76a-624x60.png" style="width: 100%;" /><p>Kibana도 마찬가지입니다:</p><pre><code>cat <<EOF | kubectl apply -f -
apiVersion: kibana.k8s.elastic.co/v1
kind: Kibana
metadata:
  name: quickstart
spec:
  version: 9.0.0
  http:
    service:
      spec:
        type: LoadBalancer
  count: 1
  elasticsearchRef:
    name: quickstart
  podTemplate:
    spec:
      containers:
        - name: kibana
          env:
            - name: NODE_OPTIONS
              value: "--max-old-space-size=1024"
          resources:
            requests:
              memory: 0.5Gi
              cpu: 0.5
            limits:
              memory: 1Gi
              cpu: 1
EOF</code></pre><p>컨테이너 CPU/RAM 및 <a href="https://nodejs.org/">Node.js </a>메모리 사용량<a href="https://nodejs.org/api/cli.html#--max-old-space-sizesize-in-mib">(최대 이전 공간 크기)</a>도 조정할 수 있습니다.</p><p><a href="https://www.elastic.co/docs/deploy-manage/deploy/cloud-on-k8s/volume-claim-templates">기존 볼륨 클레임은 크기를 줄일 수 없다는</a> 점에 유의하세요. 업데이트를 적용한 후 운영자는 중단 시간을 최소화하면서 변경 작업을 수행합니다.</p><p>불필요한 비용을 피하려면 테스트가 끝나면 클러스터를 삭제하는 것을 잊지 마세요.</p><pre><code>az aks delete --name myAKSAutomaticCluster --resource-group elastic-resource</code></pre><h2>결론</h2><p>ECK와 함께 Azure AKS Automatic을 사용하면 운영 복잡성을 줄이고, 자동화된 확장 및 업데이트를 보장하며, Kubernetes의 유연성을 활용하는 등 Elasticsearch와 Kibana를 배포하기 위한 균형 잡힌 솔루션을 제공합니다. 이 접근 방식은 모든 인프라 세부 사항을 수동으로 관리하지 않고도 안정적이고 반복 가능하며 유지 관리가 가능한 배포 프로세스를 원하는 팀에 이상적이며 테스트 및 프로덕션 환경 모두에 실용적인 선택이 될 수 있습니다.</p><h2>다음 단계</h2><p>Kubernetes에 대해 자세히 알아보려면 여기에서 공식 문서를 확인하실 수 있습니다:</p><ul><li><a href="https://www.elastic.co/docs/deploy-manage/deploy/cloud-on-k8s">Kubernetes의 Elastic Cloud | Elastic Docs</a></li><li><a href="https://learn.microsoft.com/en-us/azure/aks/intro-aks-automatic">Azure Kubernetes Service(AKS) 자동 소개(미리 보기)(영문)</a></li><li><a href="https://azure.github.io/AKS/2024/05/22/aks-automatic">AKS 오토매틱 - AKS 엔지니어링 블로그</a></li></ul>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Elasticsearch에서 A2A 프로토콜과 MCP를 사용하여 LLM 에이전트 뉴스룸 만들기: 1부]]></title>
            <description><![CDATA[전문 LLM 에이전트가 협업하여 뉴스 기사를 조사, 작성, 편집, 게시하는 실제 뉴스룸 사례에서 A2A 프로토콜과 MCP의 개념을 살펴보세요.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/a2a-protocol-mcp-llm-agent-newsroom-elasticsearch</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/a2a-protocol-mcp-llm-agent-newsroom-elasticsearch</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Justin Castilla]]></dc:creator>
            <pubDate>Thu, 13 Nov 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/9f938d8e2f0fdf7509edf028816c48bdbc8b3fc7-1600x900.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<h2>소개</h2><p>현재의 LLM 지원 시스템은 단일 모델 애플리케이션을 넘어 전문 에이전트가 함께 협력하여 이전에는 불가능하다고 생각했던 작업을 수행하는 복잡한 네트워크로 빠르게 진화하고 있습니다. 이러한 시스템이 복잡해짐에 따라 상담원 커뮤니케이션과 툴 액세스를 지원하는 인프라가 개발의 주요 초점이 되고 있습니다. 이러한 요구를 해결하기 위해 두 가지 상호 보완적인 접근 방식이 등장했습니다: 바로 멀티 에이전트 조정을 위한 <strong>A2A(에이전트2에이전트)</strong> 프로토콜과 표준화된 도구 및 리소스 액세스를 위한 <strong>MCP(모델 컨텍스트 프로토콜)</strong> 입니다.</p><p>각각의 기능을 서로 조화롭게 사용할 때와 사용하지 않을 때를 이해하면 애플리케이션의 확장성, 유지보수성 및 효율성에 큰 영향을 미칠 수 있습니다. 이 문서에서는 전문 LLM 에이전트가 협업하여 뉴스 기사를 조사, 작성, 편집 및 게시하는 디지털 뉴스룸의 실제 사례에서 <strong>A2A의</strong> 개념과 구현을 살펴봅니다.</p><p>관련 리포지토리는 <a href="https://github.com/justincastilla/elastic-newsroom/tree/main">여기에서</a> 확인할 수 있으며, A2A의 구체적인 활용 사례는 섹션 5의 마지막 부분에서 살펴볼 것입니다.</p><h3>필수 구성 요소</h3><p><a href="https://github.com/justincastilla/elastic-newsroom/tree/main">리포지토리는</a> A2A 에이전트의 Python 기반 구현으로 구성되어 있습니다. Flask에는 API 서버가 제공되며, 로깅 및 UI 업데이트를 위한 메시지를 라우팅하는 이벤트 허브라는 사용자 정의 Python 메시징 서비스도 있습니다. 마지막으로, 뉴스룸 기능을 독립적으로 사용할 수 있는 React UI가 제공됩니다. 모든 것이 도커 이미지에 포함되어 있어 쉽게 구현할 수 있습니다. 컴퓨터에서 직접 서비스를 실행하려면 이러한 기술이 설치되어 있는지 확인해야 합니다:</p><p>언어 및 런타임</p><ul><li>Python 13.12 - 핵심 백엔드 언어</li><li>Node.js 18+ - 선택적 React UI</li></ul><p>핵심 프레임워크 및 SDKS:</p><ul><li>A2A SDK 0.3.8 - 상담원 조정 및 커뮤니케이션</li><li>Anthropic SDK - AI 생성을 위한 클로드 통합</li><li>Uvicorn - 에이전트 실행을 위한 ASGI 서버</li><li>FastMCP 2.12.5+ - MCP 서버 구현</li><li>React 18.2 - 프론트엔드 UI 프레임워크</li></ul><p>데이터 &amp; 검색</p><ul><li>Elasticsearch 9.1.1+ - 문서 색인 및 검색</li></ul><p>Docker 배포(선택 사항이지만 권장)</p><ul><li>Docker 28.5.1+</li></ul><h2>섹션 1: 에이전트2에이전트(A2A)란 무엇인가요?</h2><h3>정의 및 핵심 개념</h3><div style="display:none">Unknown block type "block", specify a component for it in the `components.types` option</div><p><strong>공식 사양:</strong> <a href="https://a2a-protocol.org/latest/specification/">https://a2a-protocol.org/latest/specification/</a></p><h3>기원과 진화</h3><p>에이전트 간 통신 또는 다중 에이전트 시스템의 개념은 <a href="https://en.wikipedia.org/wiki/Multi-agent_system">수십 년</a> 전의 분산 시스템, 마이크로 서비스 및 다중 에이전트 연구에 뿌리를 두고 있습니다. 분산형 인공 지능의 초기 연구는 협상, 조정, 협업이 가능한 에이전트의 토대를 마련했습니다. 이러한 초기 시스템은 대규모 <a href="https://www.jasss.org/5/1/7.html">소셜 시뮬레이션</a>, <a href="https://arxiv.org/html/2410.09403v1">학술 연구</a>, <a href="https://www.researchgate.net/publication/334765661_Generation_Expansion_Planning_Considering_Investment_Dynamic_of_Market_Participants_Using_Multi-agent_System">전력망 관리를</a> 위해 사용되었습니다.</p><p>LLM의 가용성이 시작되고 운영 비용이 절감되면서 멀티 에이전트 시스템은 Google과 광범위한 AI 연구 커뮤니티의 지원을 받아 &#x27;프로슈머&#x27; 시장에서 사용할 수 있게 되었습니다. 현재 에이전트2에이전트 시스템으로 알려진 A2A 프로토콜의 추가는 노력과 작업을 조정하는 여러 대규모 언어 모델 시대를 위해 특별히 설계된 최신 표준으로 발전했습니다.</p><p>A2A 프로토콜은 LLM이 연결되고 소통하는 상호작용 지점에 일관된 표준과 원칙을 적용하여 에이전트 간의 원활한 커뮤니케이션과 조정을 보장합니다. 이러한 표준화를 통해 서로 다른 기본 모델을 사용하는 서로 다른 개발자의 에이전트가 효과적으로 협업할 수 있습니다.</p><p>통신 프로토콜은 새로운 것이 아니며, 인터넷에서 이루어지는 거의 모든 디지털 거래에 널리 뿌리를 두고 있습니다. h <a href="https://www.elastic.co/search-labs">ttps://</a> www.elastic.co/search-labs 를 브라우저에 입력해 이 문서에 도달했다면 TCP/IP, HTTP 전송 및 DNS 조회 프로토콜이 모두 실행되어 일관된 브라우징 환경을 보장할 가능성이 높습니다.</p><h3>주요 특징</h3><p>A2A 시스템은 원활한 커뮤니케이션을 보장하기 위해 몇 가지 기본 원칙을 기반으로 구축되었습니다. 이러한 원칙을 기반으로 구축하면 서로 다른 LLM, 프레임워크 및 프로그래밍 언어를 기반으로 하는 다양한 에이전트가 모두 원활하게 상호 작용할 수 있습니다.</p><p>다음은 네 가지 주요 원칙입니다:</p><ul><li><strong>메시지 전달</strong>: 에이전트는 잘 정의된 속성과 형식을 갖춘 구조화된 메시지를 통해 커뮤니케이션합니다.</li><li><strong>조정</strong>: 상담원이 서로에게 작업을 위임하고 다른 상담원을 차단하지 않고 종속성을 관리하여 복잡한 워크플로우를 조율합니다.</li><li><strong>전문화</strong>: 각 상담원은 특정 도메인이나 기능에 집중하여 해당 분야의 전문가가 되어 해당 스킬셋을 기반으로 작업 완료를 제공합니다.</li><li><strong>분산 상태</strong>: 상태 및 지식이 중앙 집중화되지 않고 에이전트 간에 분산되며, 에이전트는 작업 상태 및 부분 반환(아티팩트)의 진행 상황을 서로 업데이트할 수 있습니다.</li></ul><h3>뉴스룸 실행 중인 예제</h3><p>저널리즘의 각기 다른 측면을 전문으로 하는 AI 에이전트로 운영되는 디지털 뉴스룸을 상상해 보세요:</p><ul><li><strong>뉴스 책임자</strong> (코디네이터/클라이언트): 스토리를 할당하고 워크플로우를 감독합니다.</li><li><strong>리포터 에이전트</strong>: 리서치 및 인터뷰에 기반한 기사 작성</li><li><strong>리서처 에이전트</strong>: 사실, 통계 및 배경 정보 수집</li><li><strong>아카이브 에이전트</strong>: Elasticsearch를 사용하여 과거 기사를 검색하고 동향을 파악합니다.</li><li><strong>에디터 에이전트</strong>: 품질, 스타일 및 SEO 최적화를 위해 기사를 검토합니다.</li><li><strong>게시자 에이전트</strong>: CI/CD를 통해 승인된 글을 블로그 플랫폼에 게시합니다.</li></ul><p>뉴스 책임자가 <em>재생 에너지 도입에</em> 관한 기사를 배정하면 기자는 통계를 수집하고, 편집자는 초안을 검토하고, 발행인은 최종 기사를 발행해야 합니다. 이러한 조정은 A2A 프로토콜을 통해 이루어집니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/cc0760dbd74c49b92fa00dafbb8c2e8740eb70b6-963x693.png" style="width: 100%;" /><h2>섹션 2: A2A 아키텍처 이해</h2><h3>클라이언트 에이전트 및 원격 에이전트 역할</h3><p>A2A 아키텍처에서 에이전트는 두 가지 주요 역할을 수행합니다. <strong>클라이언트 에이전트는</strong> 작업을 공식화하고 시스템의 다른 에이전트에게 전달할 책임이 있습니다. 이 정보를 사용하여 작업 위임에 대한 정보에 입각한 결정을 내릴 수 있도록 원격 상담원과 해당 상담원의 기능을 식별합니다. 클라이언트 에이전트는 전체 워크플로우를 조정하여 작업이 적절히 분배되고 시스템이 목표를 향해 진행되도록 합니다.</p><p>반면 <strong>원격 에이전트는</strong> 클라이언트로부터 위임받은 작업을 수행합니다. 요청에 대한 응답으로 정보를 제공하거나 특정 조치를 취하지만 독립적으로 작업을 시작하지는 않습니다. 원격 상담원은 자신에게 할당된 책임을 완수하기 위해 필요에 따라 다른 원격 상담원과 소통하여 전문화된 기능의 협업 네트워크를 만들 수도 있습니다.</p><p>뉴스룸에서는 뉴스 책임자가 클라이언트 에이전트 역할을 하고 기자, 연구원, 편집자, 발행인이 원격 에이전트로서 요청에 응답하고 서로 조율하는 역할을 합니다.</p><h3>A2A 핵심 기능</h3><p>A2A 프로토콜은 멀티 에이전트 협업을 가능하게 하는 몇 가지 기능을 정의합니다:</p><h4>1. 발견</h4><p>A2A 서버는 고객이 특정 작업에 언제, 어떻게 활용할 수 있는지 알 수 있도록 해당 기능을 알려야 합니다. 이는 상담원의 능력, 입력 및 출력을 설명하는 상담원 카드-JSON 문서를 통해 이루어집니다. 에이전트 카드는 일관되고 잘 알려진 엔드포인트(예: 권장되는 <code>/.well-known/agent-card.json</code> 엔드포인트)에서 제공되므로 클라이언트는 협업을 시작하기 전에 에이전트의 기능을 검색하고 조회할 수 있습니다.</p><p>다음은 Elastic의 사용자 정의 아카이브 에이전트에 대한 에이전트 카드 예시입니다 &quot;Archie Archivist&quot;. Elastic과 같은 소프트웨어 공급자는 A2A 에이전트를 호스팅하고 액세스를 위한 URL을 제공한다는 점에 유의하세요:</p><pre><code>{
  "name": "Archie Archivist",
  "description": "Helps find historical news documents in the Elasticsearch Index of archived news articles and content.",
  "url": "https://xxxxxxxxxxxxx-abc123.kb.us-central1.gcp.elastic.cloud/api/agent_builder/a2a/archive-agent",
  "provider": {
    "organization": "Elastic",
    "url": "https://elastic.co"
  },
  "version": "0.1.0",
  "protocolVersion": "0.3.0",
  "preferred_transport": "JSONRPC",
  "documentationURL": "https://www.elastic.co/docs/solutions/search/agent-builder/a2a-server"
  "capabilities": {
    "streaming": false,
    "pushNotifications": false,
    "stateTransitionHistory": false
  },
  "skills": [
    {
      "id": "platform.core.search",
      "name": "platform.core.search",
      "description": "A powerful tool for searching and analyzing data within your Elasticsearch cluster.",
      "inputModes": ["text/plain", "application/json"],
      "outputModes": ["text/plain", "application/json"]
    },
    {
      "id": "platform.core.index_explorer",
      "name": "platform.core.index_explorer",
      "description": "List relevant indices, aliases and datastreams based on a natural language query.",
      "inputModes": ["text/plain", "application/json"],
      "outputModes": ["text/plain", "application/json"]
    }
  ],
  "defaultInputModes": ["text/plain"],
  "defaultOutputModes": ["text/plain"]
}</code></pre><p>이 에이전트 카드는 Elastic의 아카이브 에이전트에 대한 몇 가지 중요한 측면을 보여줍니다. 에이전트는 자신을 &quot;Archie Archivist&quot; 로 식별하고 그 목적을 Elasticsearch 인덱스에서 과거 뉴스 문서를 찾는 데 도움을 주는 것으로 명확하게 명시합니다. 이 카드는 공급자(Elastic)와 프로토콜 버전(0.3.0)을 지정하여 다른 A2A 호환 에이전트와의 호환성을 보장합니다. 가장 중요한 것은 <code>skills</code> 배열에 강력한 검색 기능 및 지능형 인덱스 탐색 등 이 에이전트가 제공하는 특정 기능이 열거되어 있다는 점입니다. 각 스킬은 지원하는 입력 및 출력 모드를 정의하여 클라이언트가 이 상담원과 통신하는 방법을 정확히 이해할 수 있도록 합니다. 이 에이전트는 데이터 저장소에서 단순히 검색하는 것이 아니라 데이터 저장소와 대화할 수 있도록 기본 LLM 지원 도구와 API 엔드포인트 제품군을 제공하는 Elastic의 에이전트 빌더 서비스에서 파생되었습니다. Elasticsearch의 A2A 에이전트에 대한 액세스는 <a href="https://www.elastic.co/docs/solutions/search/agent-builder/a2a-server">여기에서</a> 확인할 수 있습니다.</p><h4>2. 협상</h4><p>고객과 상담원은 적절한 사용자 상호작용과 데이터 교환을 위해 텍스트, 양식, 아이프레임, 오디오/비디오 등 커뮤니케이션 방식에 대해 합의해야 합니다. 이 협상은 상담원 협업이 시작될 때 이루어지며 워크플로 전반에 걸쳐 상호 작용을 관리할 프로토콜을 설정합니다. 예를 들어 음성 기반 고객 서비스 상담원은 오디오 스트림을 통해 통신하도록 협상할 수 있고, 데이터 분석 상담원은 구조화된 JSON을 선호할 수 있습니다. 협상 프로세스를 통해 양 당사자는 각자의 역량과 당면한 업무의 요구 사항에 맞는 형식으로 정보를 효과적으로 교환할 수 있습니다.</p><p>위의 JSON 스니펫에 나열된 기능은 모두 입력 및 출력 스키마를 가지고 있으며, 이는 다른 에이전트에서 이 에이전트와 상호 작용하는 방법에 대한 기대치를 설정합니다.</p><h4>3. 작업 및 상태 관리</h4><p>클라이언트와 상담원은 작업 실행 전반에 걸쳐 작업 상태, 변경 사항 및 종속성을 전달할 수 있는 메커니즘이 필요합니다. 여기에는 작업 생성 및 할당부터 진행 상황 업데이트 및 상태 변경에 이르기까지 작업의 전체 수명 주기를 관리하는 것이 포함됩니다. 일반적인 상태에는 보류 중, 진행 중, 완료 또는 실패 상태가 포함됩니다. 또한 시스템은 작업 간의 종속성을 추적하여 종속 작업이 시작되기 전에 선행 작업이 완료되도록 해야 합니다. 오류 처리 및 재시도 로직도 필수 구성 요소로, 시스템이 장애로부터 원활하게 복구하고 주요 목표를 향해 계속 나아갈 수 있도록 합니다.</p><p>작업 메시지 예시:</p><pre><code>{
  "message_id": "msg_789xyz",
  "message_type": "task_request",
  "sender": "news_chief",
  "receiver": "researcher_agent",
  "timestamp": "2025-09-30T10:15:00Z",
  "payload": {
    "task_id": "task_456abc",
    "capability": "fact_gathering",
    "parameters": {
      "query": "renewable energy adoption rates in Europe 2024",
      "sources": ["eurostat", "iea", "ember"],
      "depth": "comprehensive"
    },
    "context": {
      "story_id": "story_123",
      "deadline": "2025-09-30T18:00:00Z",
      "priority": "high"
    }
  }
}</code></pre><p>이 예제 작업 메시지는 A2A 커뮤니케이션의 몇 가지 주요 측면을 보여줍니다.</p><ul><li><strong>메시지</strong> 구조에는 고유 메시지 식별자, 전송되는 메시지 유형, 발신자 및 수신자 식별, 추적 및 디버깅을 위한 타임스탬프 등의 메타데이터가 포함됩니다.</li><li><strong>페이로드에는</strong> 원격 에이전트에서 호출되는 기능을 지정하고 해당 기능을 실행하는 데 필요한 매개 변수를 제공하는 실제 작업 정보가 포함되어 있습니다.</li><li><strong>컨텍스트</strong> 섹션에서는 상담원이 리소스를 할당하고 작업 일정을 잡는 방법을 알려주는 마감일 및 우선 순위 수준을 포함하여 수신 상담원이 더 넓은 워크플로우를 이해하는 데 도움이 되는 추가 정보를 제공합니다.</li></ul><h4>4. 협업</h4><p>고객과 상담원은 역동적이면서도 구조화된 상호작용을 지원하여 상담원이 고객, 다른 상담원 또는 사용자에게 설명, 정보 또는 하위 작업을 요청할 수 <strong>있어야</strong> 합니다. 이를 통해 상담원은 초기 지침이 모호할 때 후속 질문을 하고, 더 나은 결정을 내리기 위해 추가 컨텍스트를 요청하고, 더 적절한 전문 지식을 갖춘 다른 상담원에게 하위 작업을 위임하고, 전체 작업을 진행하기 전에 피드백을 위한 중간 결과를 제공할 수 있는 협업 환경을 조성할 수 있습니다. 이러한 다방향 커뮤니케이션은 상담원이 고립된 채로 일하는 것이 아니라 지속적인 대화에 참여하여 더 나은 결과를 이끌어낼 수 있도록 합니다.</p><h3>분산형 P2P 커뮤니케이션</h3><p>A2A를 사용하면 여러 조직에서 에이전트를 호스팅할 수 있으며, 일부 에이전트는 사내에서 관리하고 다른 에이전트는 타사 서비스에서 제공하는 분산 커뮤니케이션이 가능합니다. 이러한 에이전트는 여러 클라우드 제공업체 또는 온프레미스 데이터 센터에 걸쳐 다양한 인프라에서 실행될 수 있습니다. 일부 에이전트는 GPT 모델, 다른 에이전트는 Claude, 다른 에이전트는 오픈 소스 대안으로 구동되는 등 서로 다른 기본 LLM을 사용할 수 있습니다. 에이전트는 데이터 주권 요건을 준수하거나 지연 시간을 줄이기 위해 여러 지역에 걸쳐 운영될 수도 있습니다. 이러한 다양성에도 불구하고 모든 에이전트는 정보 교환을 위한 공통 커뮤니케이션 프로토콜에 동의하여 구현 세부 사항에 관계없이 상호 운용성을 보장합니다. 이러한 분산 아키텍처는 시스템 구축 및 배포 방식에 유연성을 제공하여 조직이 특정 요구 사항에 가장 적합한 에이전트와 인프라를 혼합하여 사용할 수 있도록 합니다.</p><p>이것이 뉴스룸 애플리케이션의 최종 아키텍처입니다:</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/82e01a0d9746038eafd69d11177042b5390507ae-1600x838.png" style="width: 100%;" /><h2>섹션 3: 모델 컨텍스트 프로토콜(MCP)</h2><h3>정의 및 목적</h3><p>모델 컨텍스트 프로토콜(MCP)은 사용자 정의 도구, 리소스, 프롬프트 등 코드베이스에 추가된 추가 기능으로 개별 LLM을 개선하고 권한을 부여하기 위해 Anthropic에서 개발한 표준화된 프로토콜입니다. MCP는 언어 모델과 작업을 효과적으로 완료하는 데 필요한 외부 리소스 간의 범용 인터페이스를 제공합니다. 이 <a href="https://www.elastic.co/search-labs/blog/mcp-current-state">문서에서는</a> 사용 사례, 새로운 트렌드, Elastic의 자체 구현 사례를 통해 MCP의 현재 상태를 간략하게 설명합니다.</p><h3>핵심 MCP 개념</h3><p>MCP는 세 가지 주요 구성 요소로 구성된 클라이언트-서버 아키텍처에서 작동합니다:</p><ul><li><strong>클라이언트:</strong> MCP 서버에 연결하여 해당 기능에 액세스하는 애플리케이션(예: 클로드 데스크톱 또는 사용자 지정 AI 애플리케이션)입니다.</li><li><strong>서버</strong>: 리소스, 도구 및 프롬프트를 언어 모델에 노출하는 애플리케이션입니다. 각 서버는 특정 기능이나 데이터 소스에 대한 액세스를 제공하는 데 특화되어 있습니다.<ul><li><strong>도구</strong>: 데이터베이스 검색, 외부 API 호출 또는 데이터에 대한 실행 변환과 같은 작업을 수행하기 위해 모델이 호출할 수 있는 사용자 정의 함수입니다.</li><li><strong>리소스:</strong> 모델이 읽을 수 있는 데이터 소스, 동적 또는 정적 데이터로 제공되며 URI 패턴(REST 경로와 유사)을 통해 액세스됩니다.</li><li><strong>프롬프트: </strong>모델이 특정 작업을 수행하도록 안내하는 변수가 포함된 재사용 가능한 프롬프트 템플릿입니다.</li></ul></li></ul><h3>요청-응답 패턴</h3><p>MCP는 REST API와 유사한 친숙한 요청-응답 상호 작용 패턴을 따릅니다. 클라이언트(LLM)가 리소스를 요청하거나 도구를 호출하면 MCP 서버가 요청을 처리하고 결과를 반환하며, LLM은 이를 사용하여 작업을 계속 진행합니다. 주변 서버가 있는 이 중앙 집중식 모델은 피어 투 피어 에이전트 통신에 비해 더 간단한 통합 패턴을 제공합니다.</p><h3>뉴스룸의 MCP</h3><p>뉴스룸의 예에서 개별 에이전트는 MCP 서버를 사용하여 필요한 도구와 데이터에 액세스합니다:</p><ul><li><strong>연구원 에이전트</strong> 사용:<ul><li>뉴스 API MCP 서버(뉴스 데이터베이스에 액세스)</li><li>사실 확인 MCP 서버(신뢰할 수 있는 출처에 대한 클레임 확인)</li><li>학술 데이터베이스 MCP 서버(학술 논문 및 연구)</li></ul></li><li><strong>리포터 에이전트</strong> 사용:<ul><li>스타일 가이드 MCP 서버(뉴스룸 작성 표준)</li><li>템플릿 MCP 서버(문서 템플릿 및 형식)</li><li>이미지 라이브러리 MCP 서버(스톡 사진 및 그래픽)</li></ul></li><li><strong>에디터 에이전트</strong> 사용:<ul><li>문법 검사기 MCP 서버(언어 품질 도구)</li><li>표절 감지 MCP 서버(독창성 검증)</li><li>SEO 분석 MCP 서버(헤드라인 및 키워드 최적화)</li></ul></li><li><strong>게시자 에이전트</strong> 사용:<ul><li>CMS MCP 서버(콘텐츠 관리 시스템 API)</li><li>CI/CD MCP 서버(배포 파이프라인)</li><li>Analytics MCP 서버(추적 및 모니터링)</li></ul></li></ul><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/b67311e3b58b27f9eb1b42a7b1dbad47ef3be4ad-808x535.png" style="width: 100%;" /><h2><br/>섹션 4: 아키텍처 비교</h2><h3>A2A 사용 시기</h3><p>A2A 아키텍처는 <strong>진정한 멀티 에이전트 협업이 필요한 시나리오에서</strong> 탁월한 성능을 발휘합니다. 조율이 필요한 다단계 워크플로, 여러 순차적 또는 병렬 단계가 포함된 작업, 반복 및 개선이 필요한 워크플로, 체크포인트 및 검증이 필요한 프로세스의 경우 A2A의 이점을 크게 누릴 수 있습니다. 뉴스룸의 예에서 스토리 워크플로에서는 기자가 글을 작성해야 하지만 특정 사실에 대한 신뢰도가 낮으면 리서처에게 다시 전달한 다음 편집자에게 전달하고 마지막으로 게시자에게 전달해야 할 수도 있습니다.</p><p><strong>여러 영역에 걸친 도메인별 전문화는</strong> A2A의 또 다른 강력한 사용 사례입니다. 더 큰 작업을 수행하기 위해 다양한 분야의 여러 전문가가 필요하고 각 에이전트가 서로 다른 측면에 대한 심층적인 도메인 지식과 전문화된 추론 기능을 제공하는 경우 A2A는 이러한 연결을 만드는 데 필요한 조정 프레임워크를 제공합니다. 뉴스룸은 정보 수집을 전문으로 하는 연구원, 글쓰기를 전문으로 하는 기자, 품질 관리를 전문으로 하는 편집자 등 각기 다른 전문성을 가진 팀원들로 구성되어 있습니다.</p><p>에이전트의 자율적인 행동이 필요하기 때문에 A2A는 특히 유용합니다. <strong> 독립적인 의사 결정을 내리고, 변화하는 상황에 따라 능동적인 행동을 보이며, 워크플로 요구 사항에 동적으로 적응할</strong> 수 있는 상담원은 A2A 아키텍처에서 성공할 수 있습니다. 전문 기능의 수평적 확장은 또 다른 주요 이점입니다. 단일 마스터 오브 올 트레이드가 아닌 여러 전문 에이전트가 협력하여 작업하고 동일한 에이전트의 여러 인스턴스가 하위 작업을 비동기적으로 처리할 수 있습니다. 예를 들어, 뉴스룸에서 뉴스 속보를 작성할 때 여러 명의 리포터가 동시에 같은 기사를 다른 각도에서 작업할 수 있습니다.</p><p>마지막으로 진정한 멀티 에이전트 협업이 필요한 작업은 A2A에 이상적입니다. 여기에는 <a href="https://arxiv.org/abs/2404.18796">배심원 평가</a> 메커니즘, 합의 도출 및 투표 시스템, 최상의 결과에 도달하기 위해 <strong>다양한 관점이 필요한 협업적 문제 해결이</strong> 포함됩니다.</p><h3>MCP 사용 시기</h3><p>모델 컨텍스트 프로토콜은 단일 AI 모델의 기능을 확장할 때 이상적입니다. 단일 AI 모델이 여러 도구 및 데이터 소스에 액세스해야 하는 경우, MCP는 중앙 집중식 추론과 분산 도구 및 간단한 도구 통합을 결합한 완벽한 솔루션을 제공합니다. 뉴스룸 예시에서 리서처 에이전트(한 모델)는 뉴스 API, 사실 확인 서비스, 학술 데이터베이스 등 여러 데이터 소스에 액세스해야 하며, 모두 표준화된 MCP 서버를 통해 액세스할 수 있습니다.</p><p>도구 통합의 광범위한 공유와 재사용성이 중요한 경우 표준화된 도구 통합이 우선순위가 됩니다. MCP는 공통 통합을 위한 개발 시간을 크게 단축하는 사전 구축된 MCP 서버 에코시스템으로 빛을 발합니다. 단순성과 유지보수가 필요한 경우, MCP의 요청-응답 패턴은 개발자에게 친숙하고 분산 시스템보다 이해와 디버깅이 쉬우며 운영 복잡성이 낮습니다.</p><p>마지막으로, MCP는 소프트웨어 제공업체에서 시스템과의 원격 통신을 용이하게 하기 위해 제공하는 경우가 많습니다. 이러한 제공업체 제공 MCP 서버는 온보딩 및 개발 시간을 크게 단축하는 동시에 독점 시스템에 대한 표준화된 인터페이스를 제공하므로 맞춤형 API 개발보다 훨씬 간편하게 통합할 수 있습니다.</p><h3>둘 다 사용해야 하는 경우(A2A ❤️의 MCP)</h3><p>많은 정교한 시스템은 A2A와 <a href="https://a2a-protocol.org/latest/topics/a2a-and-mcp/">MCP 통합에 대한 A2A 문서에</a> 명시된 대로 A2A와 MCP를 결합하면 이점을 얻을 수 있습니다. 조정과 표준화가 모두 필요한 시스템은 하이브리드 접근 방식에 이상적인 후보입니다. A2A는 상담원 조정 및 워크플로 오케스트레이션을 처리하고 MCP는 개별 상담원에게 툴 액세스를 제공합니다. 뉴스룸의 예에서 에이전트는 A2A를 통해 조율하며, 워크플로는 리포터에서 리서처, 편집자, 퍼블리셔로 이동합니다. 하지만 각 에이전트는 전문화된 도구에 MCP 서버를 사용하므로 아키텍처가 깔끔하게 분리되어 있습니다.</p><p>각각 도구 액세스를 위해 MCP를 사용하는 여러 전문 에이전트는 A2A가 처리하는 에이전트 조정 계층과 MCP가 관리하는 도구 액세스 계층이 있는 일반적인 패턴을 나타냅니다. 이렇게 우려 사항을 명확하게 분리하면 시스템을 더 쉽게 이해하고 유지 관리할 수 있습니다.</p><p>두 가지 접근 방식을 결합하면 상당한 이점이 있습니다. 전문화, 자율성, 병렬 처리 등 멀티 에이전트 시스템의 조직적 이점을 누리는 동시에 툴 통합, 리소스 액세스 등 MCP의 표준화 및 에코시스템의 이점도 누릴 수 있습니다. 에이전트 조정(A2A)과 리소스 액세스(MCP)는 명확하게 분리되어 있으며, 중요한 점은 A2A는 API 액세스 같은 소규모 작업에는 필요하지 않으며, 멀티에이전트 오케스트레이션의 오버헤드 없이 MCP가 효율적으로 처리한다는 점입니다.</p><p><strong>FAQ: A2A와 MCP- 사용 사례</strong></p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">기능</td><td style="border: 1px solid #ccc; padding: 4px;">에이전트2에이전트(A2A)</td><td style="border: 1px solid #ccc; padding: 4px;">모델 컨텍스트 프로토콜(MCP)</td><td style="border: 1px solid #ccc; padding: 4px;">하이브리드(A2A + MCP)</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">주요 목표</td><td style="border: 1px solid #ccc; padding: 4px;">다중 에이전트 조정: 전문 에이전트로 구성된 팀이 복잡한 다단계 워크플로우에서 함께 작업할 수 있습니다.</td><td style="border: 1px solid #ccc; padding: 4px;">단일 에이전트 향상: 외부 도구, 리소스 및 데이터로 단일 LLM/에이전트의 기능을 확장합니다.</td><td style="border: 1px solid #ccc; padding: 4px;">결합된 힘: A2A는 팀의 워크플로우를 관리하고, MCP는 각 팀원에게 도구를 제공합니다.</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">뉴스룸 팀 예시</td><td style="border: 1px solid #ccc; padding: 4px;">워크플로 체인: 뉴스 책임자 → 기자 → 연구원 → 편집자 → 발행인. 이것이 바로 조정 레이어입니다.</td><td style="border: 1px solid #ccc; padding: 4px;">개별 에이전트의 도구: 리포터 에이전트가 스타일 가이드 서버 및 템플릿 서버에 액세스하는 경우(MCP를 통해). 이것이 도구 액세스 레이어입니다.</td><td style="border: 1px solid #ccc; padding: 4px;">전체 시스템: 리포터가 편집자(A2A)와 협력하고, 리포터는 이미지 라이브러리 MCP 서버를 사용하여 기사에 사용할 그래픽을 찾습니다.</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">사용 시기</td><td style="border: 1px solid #ccc; padding: 4px;">진정한 협업, 반복 및 개선이 필요하거나 여러 상담원이 전문 지식을 공유해야 하는 경우.</td><td style="border: 1px solid #ccc; padding: 4px;">단일 에이전트가 여러 도구 및 데이터 소스에 액세스해야 하거나 독점 시스템과의 표준화된 통합이 필요한 경우.</td><td style="border: 1px solid #ccc; padding: 4px;">멀티 에이전트 시스템의 조직적 이점과 MCP의 표준화 및 에코시스템 이점이 필요한 경우.</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">핵심 이점</td><td style="border: 1px solid #ccc; padding: 4px;">자율성과 확장성: 상담원은 독립적인 의사 결정을 내릴 수 있으며, 이 시스템을 통해 전문화된 기능을 수평적으로 확장할 수 있습니다.</td><td style="border: 1px solid #ccc; padding: 4px;">단순성 및 표준화: 중앙 집중식 추론으로 인해 디버깅 및 유지 관리가 더 쉬우며, 리소스를 위한 범용 인터페이스를 제공합니다.</td><td style="border: 1px solid #ccc; padding: 4px;">우려 사항을 명확하게 분리: 시스템을 더 쉽게 이해할 수 있습니다: A2A = 팀워크, MCP = 도구 액세스.</td>
                    </tr>
              </tbody>
            </table>
          <img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/ddf6a29b1107ac6a63e94ecef703abc561a29e1e-986x656.png" style="width: 100%;" /><h2>결론</h2><p>이 글은 데이터 및 도구에 대한 지원과 외부 액세스를 제공하기 위해 MCP 서버로 강화된 A2A 기반 에이전트의 구현을 다루는 두 편의 글 중 첫 번째 섹션입니다. 다음 글에서는 온라인 뉴스룸의 활동을 모방하기 위해 함께 작동하는 실제 코드를 살펴볼 것입니다. 두 프레임워크 모두 그 자체로 매우 훌륭하고 유연하지만, 함께 작업할 때 서로를 얼마나 보완하는지 알게 될 것입니다.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[맥락을 위한 검색 - 1부: 하이브리드 검색과 맥락 엔지니어링의 진화]]></title>
            <description><![CDATA[하이브리드 검색과 컨텍스트 엔지니어링이 어휘 기반에서 어떻게 진화하여 차세대 에이전트 AI 워크플로우를 지원하는지 살펴보세요.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/context-engineering-hybrid-search-evolution-agentic-ai</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/context-engineering-hybrid-search-evolution-agentic-ai</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Woody Walton]]></dc:creator>
            <pubDate>Wed, 12 Nov 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/7e59a0671aa9ea32d68188a693936a66ebf48625-1000x628.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<h2>새로운 에이전트 AI 세상</h2><p>다른 많은 사람들과 마찬가지로 저도 AI 기능이 발전하는 속도에 아찔함과 놀라움을 동시에 느낍니다. 대규모 언어 모델(LLM)과 벡터 검색을 통해 우리는 더 이상 키워드를 찾아 헤매지 않아도 되는 시맨틱 혁명을 맞이하게 되었습니다. 그런 다음 LLM은 채팅 인터페이스를 사용하여 자연어 요청을 방대한 지식 기반을 쉽게 사용할 수 있는 요약으로 변환하는 응답으로 변환하는 새로운 데이터 상호 작용 방법을 보여주었습니다. 우리는 지금 (이미!) 수신 요청을 의미론적으로 이해하고, 수행해야 할 단계를 추론한 다음, 해당 목표를 달성하기 위해 반복적으로 작업을 실행할 수 있는 도구를 선택할 수 있는 &#x27;에이전트 AI&#x27; 워크플로우의 형태로 자동화된 LLM 기반 로직의 시작을 알 수 있습니다.</p><p>에이전트 AI의 잠재력으로 인해 우리는 주로 &#x27;프롬프트 엔지니어링&#x27;을 사용하여 생성형 AI 상호작용을 형성하는 것에서 벗어나 에이전트 도구가 응답을 생성할 때 고려해야 하는 가장 관련성이 높고 효율적인 추가 정보를 얻을 수 있도록 돕는 방법, 즉 &#x27;맥락 엔지니어링&#x27;이 다음 개척 분야로 진화해야 합니다. 하이브리드 검색은 관련 컨텍스트를 표시하는 가장 강력하고 유연한 수단이며, Elastic의 검색 AI 플랫폼은 서비스 중인 데이터를 컨텍스트 엔지니어링에 활용할 수 있는 완전히 새로운 방법을 열어줍니다. 이 글에서는 LLM이 정보 검색의 세계를 어떻게 변화시켰는지 두 가지 각도에서 살펴본 다음, 더 나은 결과를 위해 어떻게 협력할 수 있는지에 대해 논의해 보겠습니다. 다뤄야 할 내용이 꽤 많습니다...</p><h2>1부: LLM이 검색을 바꾼 방법</h2><p>LLM이 정보에 액세스하고 검색하는 방식을 어떻게 변화시켰는지에 대한 관점에서 시작하겠습니다.</p><h3>어휘 유산</h3><p>우리는 모두 오랫동안 다소 제한적인 어휘 검색의 세계에서 (최선을 다해) 살아왔습니다. 검색은 새로운 프로젝트를 조사하거나 시작할 때마다 가장 먼저 찾는 도구로, 최근까지만 해도 어휘 검색 엔진이 이해할 수 있는 방식으로 쿼리를 표현하는 것은 전적으로 사용자의 몫이었습니다. 어휘 검색은 콘텐츠가 비정형인지 정형인지에 관계없이 문서 말뭉치에서 찾은 키워드에 어떤 형태의 쿼리 용어를 일치시키는 데 의존합니다. 어휘 검색이 문서를 히트로 반환하려면 해당 키워드와 일치하거나 동의어 목록이나 사전과 같이 개념적 연결을 위해 제어된 어휘가 있어야 합니다.</p><pre><code>POST my-index/_search
{
  "size": 10,
  "query": {
    "semantic": {
      "query": "machine learning applications",
      "field": "semantic-content-field"
    }
  }
}</code></pre><p><em>어휘 <a href="https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-multi-match-query">다중 일치</a> 쿼리</em>예제</p><p>적어도 검색 엔진은 관련성 점수가 있는 히트를 반환하는 기능이 있습니다. 검색 엔진은 색인된 데이터를 효과적으로 타겟팅할 수 있는 다양한 쿼리 구문 옵션과 사용자의 쿼리 구문 의도에 따라 결과를 점수화하는 기본 제공 관련성 알고리즘을 제공합니다. 검색 엔진은 수십 년간 발전해 온 관련성 순위 알고리즘의 이점을 활용하여 검색어와의 관련성에 따라 점수를 매기고 정렬된 결과를 제공할 수 있는 효율적인 데이터 검색 플랫폼이 되었습니다. SQL을 데이터 검색의 주요 방법으로 사용하는 데이터베이스 및 기타 시스템은 여기서 불리한 점이 있습니다. 데이터베이스 쿼리에는 관련성 개념이 없기 때문에 결과를 알파벳순 또는 숫자순으로 정렬하는 것이 최선입니다. 좋은 소식은 이러한 키워드로 모든 히트(리콜)를 얻을 수 있지만, 검색한 <em>이유</em> (정확도)에 비해 반드시 유용한 순서로 검색되는 것은 아니라는 점입니다. 이는 곧 살펴보겠지만 중요한 포인트입니다...</p><h3>(시맨틱) 용을 입력합니다.</h3><p>키워드 검색의 대안으로 정보를 벡터로 표현할 수 있는 가능성은 <a href="https://www.elastic.co/search-labs/blog/introduction-to-vector-search">꽤 오래전부터</a> 연구되어 왔습니다. 벡터는 용어와 가중치를 숫자로 표현하기 때문에 학습 도메인에서 용어가 서로 어떻게 연관되는지에 대한 언어 모델의 이해를 바탕으로 개념을 수학적으로 가깝게 만들 수 있기 때문에 키워드만 사용하는 콘텐츠 매칭 모드에서 벗어날 수 있다는 점에서 많은 가능성을 가지고 있습니다. 범용 벡터 검색이 오래 지연된 것은 모델이 대부분 특정 도메인에 국한되어 있고, 용어가 다양한 맥락에서 나타낼 수 있는 다양한 개념을 충분히 이해하기에 충분히 크지 않았기 때문이었습니다.</p><p>벡터 검색이 실용화되기 시작한 것은 몇 년 전, 훨씬 더 많은 양의 데이터를 학습할 수 있는 대규모 언어 모델(LLM)이 등장하면서( <a href="https://en.wikipedia.org/wiki/Transformer_(deep_learning_architecture)">트랜스포머와</a> <a href="https://en.wikipedia.org/wiki/Attention_(machine_learning)">주의력을</a> 사용해) LLM의 크기와 깊이 덕분에 벡터가 의미론적 의미를 실제로 포착할 수 있는 충분한 뉘앙스를 저장할 수 있게 되었을 때였습니다. 이해의 깊이가 갑자기 증가함에 따라 LLM은 이전에는 잠겨 있던 수많은 자연어 처리(NLP) 기능을 제공할 수 있게 되었으며, 가장 영향력 있는 기능은 아마도 지금까지의 시퀀스 내용을 바탕으로 시퀀스에서 가장 가능성이 높은 다음 용어를 추론하는 기능일 것입니다. 추론은 제너레이티브 AI에 인간에 가까운 텍스트 생성 능력을 부여하는 과정입니다. AI가 생성한 텍스트는 학습 데이터 내에서 용어가 어떻게 연관되어 있는지에 대한 LLM의 이해를 기반으로 하며, 요청의 문구를 사용하여 용어가 나타날 수 있는 다양한 문맥을 명확히 구분합니다.</p><p>생성형 AI는 마법과도 같지만, 품질과 정확성에서 오류를 일으키는 LLM에는 흔히 환각이라고 불리는 한계가 <em>있습니다</em>. 환각은 LLM이 사실에 근거한 답변을 할 수 있는 정보에 접근할 수 없거나 올바른 맥락으로 안내되지 않을 때 발생하므로, 도움이 되는 대신 자신감 있고 그럴듯하게 들리는 답변을 지어내게 됩니다. 그 원인 중 하나는 LLM이 다양한 정보의 넓은 도메인 내에서 언어 사용법을 학습하지만, 특정 시점에 학습을 중단해야 하므로 이해에 적시성 요소가 있어 모델이 학습을 중단한 시점까지만 정확한 정보를 알 수 있다는 점입니다. 환각의 또 다른 요인은 모델이 일반적으로 비공개 데이터(공개 인터넷에서 사용할 수 없는 데이터)에 대해 알지 못한다는 점이며, 이러한 데이터에 특정 용어와 명명법이 포함되어 있는 경우 특히 중요합니다.</p><h3>벡터 데이터베이스</h3><p>LLM은 텍스트 임베딩이라는 기술을 사용하여 콘텐츠를 모델 공간에 벡터화하는데, 이는 학습을 기반으로 모델의 세계관 내에 콘텐츠의 의미적 의미를 <a href="https://www.elastic.co/search-labs/blog/hybrid-search-multiple-embeddings">임베딩하거나</a> 매핑하는 것을 말합니다. 임베드할 콘텐츠를 준비하고 처리하는 데에는 <a href="https://www.elastic.co/search-labs/blog/chunking-strategies-elasticsearch">청킹과</a> 토큰화(및 <a href="https://www.kaggle.com/code/danishmahdi/subword-tokenization-bpe-wordpiece-and-unigram">하위 단어 토큰화</a>) 등 몇 가지 단계가 있습니다. 그 결과 일반적으로 벡터 공간 내에서 해당 콘텐츠의 의미에 대한 모델의 이해를 나타내는 고밀도 벡터 집합이 생성됩니다. 청킹은 임베딩을 생성하기 위한 모델의 처리 제약 조건에 콘텐츠를 맞추는 동시에 문장 및 단락 표시기와 같은 의미적 구성을 사용하여 관련 텍스트를 청크로 그룹화하기 위한 정확하지 않은 프로세스입니다.</p><p>청킹이 필요하면 개별 청크가 같은 문서의 다른 청크와 완전히 연결되지 않기 때문에 임베디드 문서에서 약간의 의미 손실이 발생할 수 있습니다. 신경망의 고유한 불투명성은 이러한 손실을 더욱 악화시킬 수 있습니다. LLM은 학습 중에 만들어진 용어와 개념 간의 연결이 비결정적이며 인간이 해석할 수 없는 진정한 &#x27;블랙박스&#x27;입니다. 이는 설명 가능성, 반복성, 무의식적 편견, 잠재적으로 신뢰와 정확성 상실 등의 문제로 이어집니다. 하지만 쿼리할 때 특정 키워드에 얽매이지 않고 아이디어를 의미적으로 연결할 수 있는 기능은 매우 강력합니다:</p><pre><code>POST my-index/_search 
{
  "size": 10, 
  "query": {
    "semantic": {
      "query": "machine learning applications",
      "field": "semantic-content-field"
    }
  }
} </code></pre><p><em> <a href="https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-semantic-query">시맨틱</a> 쿼리 예시</em></p><p>벡터 데이터베이스는 검색 엔진이 아니라 데이터베이스라는 점에서 고려해야 할 문제가 하나 더 있습니다! <a href="https://www.elastic.co/search-labs/blog/introduction-to-vector-search">벡터 유사성 검색이</a> 수행되면 쿼리 용어가 인코딩되어 모델의 벡터 공간 내에서 일련의 (임베딩) 좌표 집합을 찾습니다. 그런 다음 이러한 좌표를 과녁으로 사용하여 과녁에 &#x27;가장 가까운 이웃&#x27;인 문서를 찾습니다. 즉, 문서의 순위(또는 결과 내 배치)는 쿼리 좌표에서 해당 문서 좌표의 계산된 유사성 <em>거리에</em> 따라 결정됩니다. 어떤 방향으로 랭킹을 우선시해야 하며, 가능한 컨텍스트 중 사용자의 의도에 가장 가까운 컨텍스트는 무엇인가요? 제가 비유한 이미지는 영화 <a href="https://www.youtube.com/watch?v=x3h7xz558EY&amp;start=3&amp;end=86">스타게이트의</a> 한 장면으로, 교차하는 6개의 좌표점이 목적지(과녁)를 알려주지만 사용자의 주관적인 의도를 나타내는 출발점의 좌표인 &#x27;7번째 기호&#x27;를 모르면 목적지에 도달할 수 없는 상황입니다. 따라서 벡터의 상대적 순위가 계속 확장되고 차별화되지 않은 유사성 영역에 기반하는 대신, 표현 구문과 관련성 점수를 통해 쿼리의 주관적 의도를 고려하면 눈금이 매겨진 주관적 관련성의 <em>원통형과</em> 유사한 결과를 얻을 수 있습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/1ddfaae0c1496d08d7d30419e6d2aeaeacfc0ea2-1600x544.png" style="width: 100%;" /><p>LLM의 추론 기능은 쿼리에 대해 가장 가능성이 높은 컨텍스트를 <em>식별하는</em> 데 도움이 될 수 있지만, 문제는 <em>도움이 없으면</em> 수신 쿼리의 좌표는 모델이 원래 학습된 방식에 <em>의해서만</em> 결정될 수 있다는 점입니다.</p><p>어떤 면에서 벡터 유사도는 엄격한 키워드 검색과는 정반대의 극단이라고 할 수 있는데, 용어 불일치 문제를 극복할 수 있다는 것이 강점이지만 <a href="https://medium.com/data-science/vector-embeddings-are-lossy-heres-what-to-do-about-it-4f9a8ee58bb7">거의 결함에</a> 가깝다고 할 수 있습니다: LLM은 관련 개념을 구분하기보다는 통합하는 경향이 있습니다. 벡터 유사도는 콘텐츠를 의미론적으로 일치시키는 능력을 향상시키지만, 모델에서 충분히 명확하지 않은 정확한 키워드와 특정 세부 사항을 간과할 수 있기 때문에 정확성을 보장하지는 않습니다. 벡터 유사도 검색은 그 자체로도 강력하지만, 벡터 데이터베이스에서 검색한 결과를 다른 검색 방법의 결과와 연관시킬 수 있는 방법이 필요합니다.</p><h3>순위 재조정 기술</h3><p>이제 결과 집합의 점수를 다시 매기거나 통합된 순위 순서로 정규화하는 리랭킹이라는 일반적인 기법을 언급할 때입니다. 재랭크가 필요한 이유는 여러 소스의 결과 또는 순위/채점 메커니즘이 다른 검색 방법(또는 전혀 없는 SQL!) 때문일 수도 있고, 의미론적이지 않은 소스의 결과를 사용자의 쿼리에 의미론적으로 맞추기 위해 재랭크가 사용될 수도 있습니다. 재랭크는 2단계 작업으로, 어떤 <em>초기 검색</em> 방법(예를 들어 SQL, 어휘 검색, 벡터 검색)의 순서를 다른 채점 방법으로 다시 지정합니다.</p><p><a href="https://www.elastic.co/docs/solutions/search/ranking/learning-to-rank-ltr">학습을 통한 순위 지정(LTR)</a> 및 <a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/reciprocal-rank-fusion">상호 순위 융합(RRF</a> ) 등 여러 가지 접근 방식을 사용할 수 있습니다. LTR은 검색 결과 기능(좋아요, 평점, 클릭 등)을 캡처하고 이를 사용하여 결과를 점수화하고 부스트 또는 편향시키는 데 유용합니다. RRF는 다양한 쿼리 양식에서 반환된 결과를 병합하는 데 적합합니다(예 어휘 및 벡터 데이터베이스 검색)을 하나의 결과 목록으로 통합합니다. Elastic은 또한 <a href="https://www.elastic.co/search-labs/blog/linear-retriever-hybrid-search">선형 재순위화</a> 방법을 사용하여 점수를 조정할 수 있는 유연성도 제공합니다.</p><p>그러나 가장 효과적인 재순위 조정 기법 중 하나는 <a href="https://www.elastic.co/docs/solutions/search/ranking/semantic-reranking">시맨틱 재순위</a> 조정으로, LLM의 시맨틱 이해를 사용하여 쿼리와 결과의 벡터 임베딩을 함께 분석한 다음 관련성 점수/채점을 적용하여 최종 순위를 결정합니다. 물론 시맨틱 재랭크에는 재랭크 모델에 대한 연결이 필요하며, Elasticsearch는 기본 제공 모델(Elastic<a href="https://www.elastic.co/docs/explore-analyze/machine-learning/nlp/ml-nlp-rerank"></a> Rerank), <a href="https://www.elastic.co/docs/reference/elasticsearch/clients/eland/machine-learning">가져온 타사</a> <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-inference-put-cohere">모델 또는 Cohere나</a> <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-inference-put-googlevertexai">Google Vertex AI</a> 같은 외부 호스팅 서비스를 활용하는 <strong>재랭크</strong> 엔드포인트를 생성할 수 있는 <a href="https://www.elastic.co/docs/api/doc/elasticsearch/group/endpoint-inference">추론 API를</a> 제공합니다. 그런 다음 <a href="https://www.elastic.co/docs/solutions/search/retrievers-overview">검색</a> 쿼리 추상화 구문을 통해 다시 순위를 매길 수 있습니다:</p><pre><code>POST my-index/_search 
{
  "size": 10,
  "retriever": {
    "text_similarity_reranker": {
      "retriever": {
        "rrf": {
          "retrievers": [
            {
              "standard": {
                "query": {
                  "multi_match": {
                    "query": "machine learning applications",
                    "fields": ["title", "content"]
                  }
                }
              }
            },
            {
              "knn": {
                "field": "semantic-content-field",
                "k": 10,
                "num_candidates": 100,
                "query_vector_builder": {
                  "text_embedding": {
                    "model_id": "my-text-embedding-model",
                    "model_text": "machine learning applications"
                  }
                }
              }
            }
          ],
          "rank_window_size": 50,
          "rank_constant": 20
        }
      }
    },
    "field": "content",
    "inference_id": "my-reranker",
    "inference_text": "machine learning applications",
    "rank_window_size": 20
  }
}</code></pre><p><em>다단계 리트리버 순위 재조정 작업 예시</em></p><p>멋지지 않나요? 서로 다른 소스의 결과에 대해 재랭킹을 수행하여 모든 유형의 콘텐츠에 대한 의미론적 이해에 근접할 수 있습니다... 의미론적 재랭킹은 처리 시간뿐만 아니라 계산 비용이 많이 들 수 있으므로 제한된 수의 결과에 대해서만 실현 가능하게 수행할 수 있으므로 초기 결과를 검색하는 <em>방법이</em> 중요합니다.</p><h3>컨텍스트 검색 방법의 중요성</h3><p>주관적 의도는 결과의 정확성을 결정하고 관련성을 점수화할 때 중요한 요소입니다. 쿼리 수행에 대한 사용자의 의도를 고려할 수 있는 기능(유연한 구문 또는 2단계 재랭킹을 통해 표현됨)이 없으면 모델 공간 내에 이미 인코딩된 기존 컨텍스트 중에서 선택할 수 밖에 없습니다. 일반적으로 이러한 컨텍스트 부족 문제를 해결하는 방법은 <a href="https://en.wikipedia.org/wiki/Retrieval-augmented_generation">검색 증강 생성(RAG)</a>과 같은 기술을 사용하는 것입니다. RAG가 작동하는 방식은 상황에 맞는 데이터에 대한 사전 쿼리에서 반환된 추가 관련 용어를 포함하여 쿼리의 좌표를 효과적으로 이동하는 것입니다. 따라서 추가 컨텍스트를 제공하는 엔진과 <em>검색을</em> 수행하는 초기 방법이 컨텍스트의 정확성에 더욱 중요해집니다!</p><p>다양한 컨텍스트 검색 방법과 이러한 방법이 RAG 작업에 어떤 도움이 되거나 해가 되는지 살펴보겠습니다:</p><ul><li><strong>검색 엔진이 없는 하이브리드 검색 검색은 여전히 주관적인 연관성이 부족합니다.</strong> RAG를 제공하는 플랫폼이 주로 SQL 기반인 경우(대부분의 &#x27;데이터 레이크&#x27; 플랫폼 포함), 초기 검색 단계에서 정확도 점수가 부족합니다. 많은 데이터 레이크 플랫폼이 자체 버전의 하이브리드 검색(검색이 아닌)을 제공하며, 일반적으로 SQL 기반 검색과 벡터 데이터베이스 결과에 시맨틱 리랭크 및 RRF와 같은 리랭크 기법을 결합합니다. 단순 정렬은 주관적 순위를 매기기에는 분명히 불충분하지만, 2단계 시맨틱 재랭크 작업의 기초로 사용하더라도 1단계 검색으로서의 SQL은 검색 시 결과를 점수화하는 방법 없이 &#x27;상위 k&#x27; 히트에 대해서만 시맨틱 재랭크가 수행될 때 문제가 됩니다 - 실제로 <em>최상의</em> 결과가 상위 결과라고 보장할 수 있는 방법은 무엇일까요?</li><li><strong>벡터 유사성만으로는 RAG에 충분하지</strong> 않습니다. 이는 임베딩의 손실, 순진한 청킹 방법, 유사성 계산 방식, 주관적 의도라는 중요한 요소가 누락된 문제 등 복합적인 문제 때문이었습니다. RAG의 주요 목표 중 하나는 생성 AI의 상호작용을 객관적인 진실에 근거하여 환각을 방지하고 학습 중에 알지 못했던 개인 정보를 LLM에 알려주는 것입니다. RAG를 통해 제공되는 추가 컨텍스트를 사용하여 당면한 질문에 답하는 데 가장 중요한 연결과 세부 사항을 고려하도록 LLM을 제한하고 지시할 수 있습니다. 이를 위해서는 의미론적 접근 방식과 어휘적 접근 방식을 <em>모두</em> 사용해야 합니다.</li><li><strong>파일 기반 grep/레거시 RAG.</strong> 에이전트 AI <a href="https://www.nicolasbustamante.com/p/the-rag-obituary-killed-by-agents">세계에서는</a> 외부 검색 플랫폼이 아닌 RAG용 grep 및 정규식을 통해 로컬 파일에 액세스하는 크게 확대된 컨텍스트 창을 사용하는 것을 지적하는 의견도 있습니다. 훨씬 더 큰 컨텍스트 창을 사용할 수 있게 되면 LLM은 관련 정보를 수집하기 위해 단편적인 정보와 여러 검색 방법/플랫폼에 의존하지 않고 자신의 사고 공간 내에서 개념을 연결할 수 있게 될 것입니다. 이론적으로는 전체 문서가 문서 세그먼트보다 더 완전한 그림을 제공하지만, 이는 소규모 데이터 도메인(예: <a href="https://en.wikipedia.org/wiki/Vibe_coding">바이브코딩을</a> 위해 파일을 제공할 때)에서만 작동할 수 있으며, 그 경우에도 초기 검색 방식은 키워드만 일치하는 모든 문서를 스캔하는 것입니다.</li></ul><p><strong>검색은 검색 그 이상입니다</strong></p><p>검색 엔진은 가능한 한 빠르고 유연하게 쿼리를 수행하도록 특별히 설계되었습니다. 내부적으로는 다양한 종류의 데이터를 해당 데이터 유형에 맞는 방식으로 저장하고 검색하기 위해 특수 데이터 구조를 활용합니다. Elasticsearch는 비정형/전체 텍스트 어휘 검색(일치, 구문, 근접, 다중 일치), 빠른 키워드(정확히 일치) 검색 및 필터링, 숫자 범위, 날짜, IP 주소 등 거의 모든 유형의 데이터에 대해 최적화된 저장과 쿼리를 제공하며, 문서 구조를 저장하는 방식이 매우 유연합니다(예. 중첩되거나 평평해진 문서). Elasticsearch는 또한 희소 벡터 유형과 고밀도 벡터 유형을 모두 저장하고 쿼리할 수 있는 기본 벡터 데이터베이스이며, 검색 충실도를 유지하면서 벡터화된 콘텐츠와 관련된 속도, 확장성 및 비용을 개선하는 혁신적인 방법(예: <a href="https://www.elastic.co/search-labs/blog/better-binary-quantization-lucene-elasticsearch">Better Binary Quantization(BBQ)</a> &amp; <a href="https://www.elastic.co/search-labs/blog/diskbbq-elasticsearch-introduction">DiskBBQ</a>)을 계속 모색하고 있습니다. 또한 Elasticsearch 플랫폼은 기본으로 데이터 복원력과 고가용성을 제공하며, <a href="https://www.elastic.co/docs/deploy-manage/tools/snapshot-and-restore/searchable-snapshots">검색 가능한 스냅샷과</a> 같은 데이터 수명 주기 관리 기능을 통해 자주 액세스하지 않거나 장기 보존 데이터를 비용 효율적인 개체 스토리지에 보관하면서도 여전히 완벽하게 검색할 수 있습니다.</p><h3>하이브리드 검색은 모든 면에서 최고입니다.</h3><p><a href="https://www.elastic.co/what-is/hybrid-search">하이브리드 검색</a> (하이브리드 검색뿐만 아니라!) 는 기존 어휘 검색의 강점과 LLM의 의미론적 이해 및 벡터 유사도 검색을 결합합니다. 이러한 시너지를 통해 검색 엔진이 제공하는 유연한 쿼리 구문 옵션, 의도 중심 구문 옵션 및 관련성 점수, 멀티모달 데이터 검색, 필터링, 집계, 편향성 등 검색 엔진이 제공하는 모든 기능을 통해 <em>검색</em> 단계에서 관련성이 높은 결과를 타겟팅할 수 있습니다. <a href="https://www.elastic.co/docs/reference/query-languages/esql">ES|QL</a> 및 다단계 <a href="https://www.elastic.co/docs/solutions/search/retrievers-overview">검색기와</a> 같은 검색 구문을 사용하면 기존 검색과 시맨틱 검색, 필터, 여러 순위 재조정 기술을 하나의 요청에 유연하게 결합할 수 있습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/f25180cb430414b99ae553d3b8eb161dbccea4d4-1920x1080.png" style="width: 100%;" /><p>하이브리드 검색의 가장 큰 장점 중 하나는 쿼리가 여러 가지 데이터 유형에 대해 동시에 특수 구문을 사용할 수 있다는 점입니다. 이러한 다양한 쿼리 구문은 결과를 <em>찾는</em> 데만 사용할 수 있을 뿐만 아니라 결과에 <em>대한</em> 필터나 집계로도 사용할 수 있습니다. 예를 들어, 다른 구문과 자주 결합되는 가장 일반적인 쿼리 유형 중 하나는 <a href="https://www.elastic.co/docs/explore-analyze/geospatial-analysis">지리공간 분석입니다</a>. 특정 지점에서 지정된 거리 내의 지리적 좌표가 있는 결과를 쿼리하거나, 지역별 결과 집계 또는 구역 내/외의 이동을 추적하고 경고하기 위한 집계를 요청하는 등의 작업을 수행할 수 있습니다. 하이브리드 검색을 사용하면 구문을 유연하게 조합하여 가장 정확한 방식으로 결과를 타겟팅하고 컨텍스트에 가장 가까운 콘텐츠를 검색할 수 있습니다.</p><h2>인터미션</h2><p>이 첫 번째 파트에서는 벡터 검색이 데이터를 검색하는 방식을 어떻게 변화시켰는지 이야기하고, 데이터와 상호 작용하는 데 사용하는 쿼리 메커니즘에 LLM이 가져온 변화의 무대를 마련합니다. LLM이 맥락을 잃지 않고 이해할 수 있도록 여러 부분으로 나눠서 설명해야 한다고 가정해 보겠습니다... ;-) <a href="https://www.elastic.co/search-labs/blog/context-engineering-llm-evolution-agentic-ai">2부: 에이전트 AI와 컨텍스트 엔지니어링의 필요성에서</a> <em>이것이 중요한 이유에</em> 대해 자세히 알아보고, 3부에서는 하이브리드 검색에 대한 논의로 돌아가겠습니다.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Elasticsearch에서 구조화된 문서에 대한 재귀 청크 구성하기]]></title>
            <description><![CDATA[최적의 구조화된 문서 색인을 위해 청크 크기, 구분자 그룹, 사용자 정의 구분자 목록을 사용하여 Elasticsearch에서 재귀적 청크를 구성하는 방법을 알아보세요.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/recursive-chunking-structured-documents-elasticsearch</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/recursive-chunking-structured-documents-elasticsearch</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Daniel Rubinstein]]></dc:creator>
            <pubDate>Tue, 11 Nov 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/591872e31880768ca927507654a621addc0d124d-1600x960.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>8.16부터 사용자는 긴 문서를 의미론적 텍스트 필드로 수집할 때 사용되는 청킹 전략을 구성할 수 있습니다. 9.1 / 8.19 버전부터 정규식 목록을 사용하여 문서를 청크 처리하는 새로운 구성 가능한 재귀 청크 전략이 도입되었습니다. 청크의 목적은 긴 문서를 관련 콘텐츠를 캡슐화하는 섹션으로 분할하는 것입니다. 기존 전략은 단어/문장 단위로 텍스트를 분할하지만, 구조화된 형식으로 작성된 문서(예. 마크다운)에는 종종 일부 구분 문자열로 정의된 섹션 내에 관련 콘텐츠가 포함되어 있습니다(예 헤더). 이러한 유형의 문서에 대해 구조화된 문서의 형식을 활용하여 더 나은 청크를 만드는 재귀적 청크 전략을 소개합니다!</p><h2>재귀적 청킹이란 무엇인가요?</h2><p>재귀 청크는 패턴을 구분하는 제공된 섹션 목록을 반복하여 원하는 최대 청크 크기를 충족할 때까지 문서를 점진적으로 더 작은 세그먼트로 분할합니다.</p><h3>재귀 청킹은 어떻게 구성하나요?</h3><p>다음은 재귀 청킹을 위해 사용자가 설정할 수 있는 값입니다:</p><ul><li>(필수) <code>max_chunk_size</code>: 청크의 최대 단어 수입니다.</li><li>둘 중 하나입니다:<ul><li><code>separators</code>: 문서를 청크로 분할하는 데 사용할 정규식 문자열 패턴의 목록입니다.</li><li><code>separator_group</code>: 특정 유형의 문서에 사용하도록 Elastic에서 정의한 기본 구분 기호 목록에 매핑할 문자열입니다. 현재 <code>markdown</code> 및 <code>plaintext</code> 에서 사용할 수 있습니다.</li></ul></li></ul><h3>재귀 청크는 어떻게 작동하나요?</h3><p>입력 문서, <code>max_chunk_size</code> (단어로 측정), 구분 문자열 목록이 주어졌을 때 재귀 청킹을 수행하는 프로세스는 다음과 같습니다:</p><ol><li>입력 문서가 이미 최대 청크 크기 내에 있는 경우 전체 입력에 걸친 단일 청크를 반환합니다.</li><li>구분 기호의 발생 빈도에 따라 텍스트를 잠재적인 청크로 분할합니다. 각 잠재적 청크에 대해:<ol><li>잠재적 청크가 최대 청크 크기 이내인 경우 청크 목록에 추가하여 사용자에게 반환합니다.</li><li>그렇지 않으면 2단계부터 반복하여 잠재적 청크의 텍스트만 사용하고 목록의 다음 구분 기호를 사용하여 분할합니다. 더 이상 시도할 구분 기호가 남아 있지 않으면 문장 기반 청킹으로 돌아가세요.</li></ol></li></ol><h2>재귀 청킹 구성 예시</h2><p>청크 크기 외에도 재귀 청크의 주요 구성은 문서를 분할하는 데 사용할 구분 기호를 선택하는 것입니다. 어디서부터 시작해야 할지 잘 모르겠다면, Elasticsearch는 일반적인 사용 사례에 사용할 수 있는 몇 가지 기본 구분 기호 그룹을 제공합니다.</p><h3>구분 기호 그룹 활용</h3><p>구분 그룹을 사용하려면 청크 설정을 구성할 때 사용하려는 그룹의 이름을 입력하기만 하면 됩니다. 예를 들어</p><pre><code>"chunking_settings": {
    "strategy": "recursive",
    "max_chunk_size": 25,
    "separator_group": "plaintext"
}</code></pre><p>이렇게 하면 구분 기호 목록을 활용하는 재귀적 청크 전략이 제공됩니다 <code>[&quot;(?&lt;!\\n)\\n\\n(?!\\n)&quot;, &quot;(?&lt;!\\n)\\n(?!\\n)&quot;)]</code>. 이 방법은 일반적인 일반 텍스트 애플리케이션에서 잘 작동하며, 두 줄로 분할한 다음 한 줄로 분할합니다.</p><p>또한 구분 기호 목록을 활용할 수 있는 구분 기호 그룹 <code>markdown</code> 을 제공합니다:</p><pre><code>[
"\n# ",
       "\n## ",
       "\n### ",
       "\n#### ",
       "\n##### ",
       "\n###### ",
       "\n^(?!\\s*$).*\\n-{1,}\\n",
       "\n^(?!\\s*$).*\\n={1,}\\n"
]</code></pre><p>이 구분 기호 목록은 일반적인 마크다운 사용 사례에 적합하며, 6개의 제목 수준과 섹션 구분 문자를 각각 분할할 수 있습니다.</p><p>리소스(추론 엔드포인트/시맨틱 텍스트 필드)를 만들 때 당시의 구분 기호 그룹에 해당하는 구분 기호 목록이 구성에 저장됩니다. 나중에 구분 기호 그룹이 업데이트되더라도 이미 생성된 리소스의 동작은 변경되지 않습니다.</p><h3>사용자 지정 구분 기호 목록 활용</h3><p>미리 정의된 구분 기호 그룹 중 하나가 사용 사례에 적합하지 않은 경우 필요에 맞는 사용자 지정 구분 기호 목록을 정의할 수 있습니다. 구분 기호 목록 내에 정규식을 입력할 수 있습니다. 다음은 사용자 지정 구분 기호로 구성된 청크 설정의 예입니다:</p><pre><code>"chunking_settings": {
    "strategy": "recursive",
    "max_chunk_size": 25,
    "separators": ["\n\n", "\n", "<my-custom-separator>"]
}</code></pre><p>위의 청크 전략은 2개의 줄 바꿈 문자, 1개의 줄 바꿈 문자, 마지막으로 문자열 <code>“&lt;my-custom-separator&gt;”</code> 로 분할됩니다.</p><h2>재귀 청크의 실제 사용 예시</h2><p>재귀 청크가 실제로 작동하는 예를 살펴보겠습니다. 이 예에서는 상위 두 개의 헤더 수준을 사용하여 마크다운 문서를 분할하는 사용자 지정 구분 기호 목록과 함께 다음 청크 설정을 사용합니다:</p><pre><code>"chunking_settings": {
    "strategy": "recursive",
    "max_chunk_size": 25,
    "separators": ["\n# ", "\n## "]
}</code></pre><p>청크가 없는 간단한 마크다운 문서를 살펴보겠습니다:</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/3a5507f4a1288065097231548e5b18e240508785-1302x1446.png" style="width: 100%;" /><p>이제 위에서 정의한 청킹 설정을 사용하여 문서를 청킹해 보겠습니다:</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/a3313c4c40ff39b8dbcdd7c4878c723f088e6c1a-1600x1187.png" style="width: 100%;" /><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/79a2921943191ea631df94c9d465818ec8d3e738-1600x1206.png" style="width: 100%;" /><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/459e695cce7540267422396b9a62ff4ad35f61db-1600x1260.png" style="width: 100%;" /><p>참고: 각 청크(청크 3 제외)의 끝에 있는 줄 바꿈은 강조 표시되지 않지만 실제 청크 경계 내에 포함됩니다.</p><h3>지금 바로 리커시브 청크를 시작하세요!</h3><p>이 기능을 활용하는 방법에 대한 자세한 내용은 <span class="unknown__pt__mark__88e241676926">청크 설정 구성에</span> 대한 문서를 참조하세요.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Kibana의 Elasticsearch 쿼리 규칙 UI 소개]]></title>
            <description><![CDATA[Elasticsearch 쿼리 규칙 UI를 사용해 자연 순위에 영향을 주지 않고 Kibana에서 사용자 정의 가능한 규칙 세트를 사용해 검색 쿼리에서 문서를 추가하거나 제외하는 방법을 알아보세요.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/elasticsearch-query-rules-ui-introduction</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/elasticsearch-query-rules-ui-introduction</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Jhon Guzmán]]></dc:creator>
            <pubDate>Fri, 07 Nov 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/1fb10bd31c509cc9b9bb4f71f49970f140e6c36f-1600x945.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>검색 엔진의 역할은 관련성 높은 결과를 반환하는 것입니다. 그러나 세일 강조, 시즌 상품 우선순위 지정, 스폰서 상품 소개 등 그 이상의 비즈니스 요구사항이 있으며, 개발자가 검색 쿼리에서 이를 항상 수행할 수는 없습니다.</p><p>또한 이러한 사용 사례는 일반적으로 시간에 민감하며 일반적인 개발 단계(코드 브랜치를 생성한 다음 새 릴리스를 기다리는 과정)를 거치는 것은 시간이 많이 소요되는 프로세스입니다.</p><p>그렇다면 이 모든 프로세스를 API 호출만으로, 더 나아가 Kibana에서 클릭 몇 번으로 수행할 수 있다면 어떨까요?</p><h2>쿼리 규칙 UI</h2><p>Elasticsearch 8.10에는 <a href="https://www.elastic.co/blog/introducing-query-rules-elasticsearch-8-10"><strong>쿼리 규칙과</strong></a> <a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/retrievers/rule-retriever"><strong>규칙 검색기가</strong></a> 도입되었습니다. 규칙에 따라 자연 <a href="https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-pinned-query"><em>검색 결과의 순위에</em></a> 영향을 주지 않고 고정된 결과를 쿼리에 삽입하기 위해 고안된 도구입니다. 선언적이고 단순한 방식으로 결과 위에 비즈니스 로직을 추가할 뿐입니다.</p><p>쿼리 규칙의 몇 가지 일반적인 사용 사례는 다음과 같습니다:</p><ul><li><strong>프로모션 목록 또는 판매 강조 표시</strong>: 세일 중이거나 스폰서된 품목을 상단에 표시합니다.</li><li><strong>문맥 또는 지리적 위치에 따라 제외</strong>: 현지 규정으로 인해 특정 항목을 표시할 수 없는 경우 숨기기.</li><li><strong>주요 결과의 우선순위 지정</strong>: 자연 검색 순위와 관계없이 인기 검색어 또는 고정 검색어가 항상 상단에 표시되도록 합니다.</li></ul><p>인터페이스에 액세스하고 이러한 도구와 상호 작용하려면 Kibana 사이드 메뉴를 클릭하고 <strong></strong> <strong>관련성</strong>아래의 쿼리 규칙으로 이동해야 합니다:</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/242e33e89d1a07ffa0e76009c46b3a9236722741-458x1010.png" style="width: 100%;" /><p>쿼리 규칙 메뉴가 나타나면 <strong>첫 번째 규칙 집합 만들기를 클릭</strong>합니다:</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/30b3a91bbbf243d314cf38298e01ca5cff784430-1600x945.png" style="width: 100%;" /><p>다음으로 규칙 집합의 이름을 지정해야 합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/26c5462f88678867776f933b5655ca0df0d72a16-708x446.png" style="width: 100%;" /><p>각 규칙을 정의하는 양식에는 세 가지 주요 구성 요소가 있습니다:</p><ul><li><strong>기준</strong>: 조건: 규칙을 적용하기 위해 충족해야 하는 조건입니다. 예를 들어, &quot;쿼리 문자열 필드에 <em>크리스마스</em>값이 포함된 경우&quot; 또는 &quot;국가 필드가 <em>CO인</em>경우&quot;와 같은 예입니다.</li><li><strong>액션</strong>: 조건이 충족될 때 수행하려는 작업입니다. 고정(문서를 최상위 결과에 고정)하거나 제외(문서를 숨김)할 수 있습니다.</li><li><strong>메타데이터</strong>: 쿼리가 실행될 때 쿼리와 함께 제공되는 필드입니다. 여기에는 위치나 언어와 같은 사용자 정보뿐만 아니라 검색 데이터(쿼리 문자열)도 포함될 수 있습니다. 규칙 적용 여부를 결정하기 위해 기준에서 사용하는 값입니다.</li></ul><h2>예: 인기 품목</h2><p>다양한 품목이 있는 이커머스 사이트가 있다고 가정해 보겠습니다. 지표를 살펴보면 콘솔 카테고리에서 가장 많이 판매된 상품 중 하나는 &#x27;듀얼쇼크 4 무선 컨트롤러&#x27;로, 특히 사용자가 &#x27;PS4&#x27; 또는 &#x27;PlayStation 4&#x27; 키워드를 검색할 때 가장 많이 판매되는 상품임을 확인할 수 있습니다. 따라서 사용자가 해당 키워드를 검색할 때마다 이 제품을 결과 상단에 표시하기로 결정했습니다.</p><p>먼저 대량 API 요청을 사용하여 각 항목에 대한 문서를 색인해 보겠습니다:</p><pre><code>POST _bulk
{ "index": { "_index": "products", "_id": "1" } }
{ "id": "1", "name": "PlayStation 4 Slim 1TB", "category": "console", "brand": "Sony", "price": 1200 }
{ "index": { "_index": "products", "_id": "2" } }
{ "id": "2", "name": "DualShock 4 Wireless Controller", "category": "accessory", "brand": "Sony", "price": 250 }
{ "index": { "_index": "products", "_id": "3" } }
{ "id": "3", "name": "PlayStation 4 Camera", "category": "accessory", "brand": "Sony", "price": 200 }
{ "index": { "_index": "products", "_id": "4" } }
{ "id": "4", "name": "PlayStation 4 VR Headset", "category": "accessory", "brand": "Sony", "price": 900 }
{ "index": { "_index": "products", "_id": "5" } }
{ "id": "5", "name": "Charging Station for DualShock 4", "category": "accessory", "brand": "Sony", "price": 80 }</code></pre><p>쿼리에 개입하지 않으면 해당 항목은 일반적으로 네 번째 위치에 표시됩니다. 쿼리는 다음과 같습니다:</p><pre><code>GET products/_search
{
 "query": {
   "match": {
     "name": "PlayStation 4"
   }
 }
}</code></pre><p>결과는 다음과 같습니다.</p><pre><code>{
 "took": 1,
 "timed_out": false,
 "_shards": {
   "total": 1,
   "successful": 1,
   "skipped": 0,
   "failed": 0
 },
 "hits": {
   "total": {
     "value": 5,
     "relation": "eq"
   },
   "max_score": 0.6973252,
   "hits": [
     {
       "_index": "products",
       "_id": "3",
       "_score": 0.6973252,
       "_source": {
         "id": "3",
         "name": "PlayStation 4 Camera",
         "category": "accessory",
         "brand": "Sony",
         "price": 200
       }
     },
     {
       "_index": "products",
       "_id": "1",
       "_score": 0.6260078,
       "_source": {
         "id": "1",
         "name": "PlayStation 4 Slim 1TB",
         "category": "console",
         "brand": "Sony",
         "price": 1200
       }
     },
     {
       "_index": "products",
       "_id": "4",
       "_score": 0.6260078,
       "_source": {
         "id": "4",
         "name": "PlayStation 4 VR Headset",
         "category": "accessory",
         "brand": "Sony",
         "price": 900
       }
     },
     {
       "_index": "products",
       "_id": "2",
       "_score": 0.08701137,
       "_source": {
         "id": "2",
         "name": "DualShock 4 Wireless Controller",
         "category": "accessory",
         "brand": "Sony",
         "price": 250
       }
     },
     {
       "_index": "products",
       "_id": "5",
       "_score": 0.07893815,
       "_source": {
         "id": "5",
         "name": "Charging Station for DualShock 4",
         "category": "accessory",
         "brand": "Sony",
         "price": 80
       }
     }
   ]
 }
}</code></pre><p>이를 변경하는 쿼리 규칙을 만들어 보겠습니다. 먼저 다음과 같이 규칙 집합에 추가해 보겠습니다:</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/fdc42646fb3e76a09bca7d19047a76efe343f7a2-1600x650.png" style="width: 100%;" /><p>또는 이에 상응하는 <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-query-rules-put-ruleset">API 요청</a>:</p><pre><code>PUT _query_rules/my-rules
{
  "rules": [
    {
      "rule_id": "rule-1232",
      "type": "pinned",
      "criteria": [
        {
          "type": "exact",
          "metadata": "query_string",
          "values": [
            "PS4",
            "PlayStation 4"
          ]
        }
      ],
      "actions": {
        "docs": [
          {
            "_index": "products",
            "_id": "2"
          }
        ]
      }
    }
  ]
}</code></pre><p>쿼리에서 <strong>규칙 집합을 </strong>사용하려면 쿼리 규칙 유형을 사용해야 합니다. 이러한 종류의 쿼리는 크게 두 부분으로 구성됩니다:</p><pre><code>GET /products/_search
{
 "retriever": {
   "rule": {
     "retriever": {
       "standard": {
         "query": {
           "match": { "name": "PlayStation 4" }
         }
       }
     },
     "match_criteria": {
       "query_string": "PlayStation 4"
     },
     "ruleset_ids": ["my-rules"]
   }
 }
}</code></pre><ul><li><strong>match_criteria</strong>: 사용자의 쿼리와 비교하는 데 사용되는 메타데이터입니다. 이 예에서는 쿼리 문자열 필드에 &quot;PlayStation 4&quot; 값이 있을 때 규칙 집합이 활성화됩니다.</li><li><strong>쿼리</strong>: 검색 및 자연 검색 결과를 얻는 데 사용되는 실제 쿼리입니다.</li></ul><p>이렇게 하면 먼저 오가닉 쿼리를 실행한 다음 Elasticsearch가 규칙 세트의 규칙을 적용합니다:</p><pre><code>{
 "took": 17,
 "timed_out": false,
 "_shards": {
   "total": 1,
   "successful": 1,
   "skipped": 0,
   "failed": 0
 },
 "hits": {
   "total": {
     "value": 5,
     "relation": "eq"
   },
   "max_score": 1.7014122e+38,
   "hits": [
     {
       "_index": "products",
       "_id": "2",
       "_score": 1.7014122e+38,
       "_source": {
         "id": "2",
         "name": "DualShock 4 Wireless Controller",
         "category": "accessory",
         "brand": "Sony",
         "price": 250
       }
     },
     {
       "_index": "products",
       "_id": "3",
       "_score": 0.6973252,
       "_source": {
         "id": "3",
         "name": "PlayStation 4 Camera",
         "category": "accessory",
         "brand": "Sony",
         "price": 200
       }
     },
     {
       "_index": "products",
       "_id": "1",
       "_score": 0.6260078,
       "_source": {
         "id": "1",
         "name": "PlayStation 4 Slim 1TB",
         "category": "console",
         "brand": "Sony",
         "price": 1200
       }
     },
     {
       "_index": "products",
       "_id": "4",
       "_score": 0.6260078,
       "_source": {
         "id": "4",
         "name": "PlayStation 4 VR Headset",
         "category": "accessory",
         "brand": "Sony",
         "price": 900
       }
     },
     {
       "_index": "products",
       "_id": "5",
       "_score": 0.07893815,
       "_source": {
         "id": "5",
         "name": "Charging Station for DualShock 4",
         "category": "accessory",
         "brand": "Sony",
         "price": 80
       }
     }
   ]
 }
}</code></pre><h2>예: 사용자 기반 메타데이터</h2><p>쿼리 규칙의 또 다른 흥미로운 적용 사례는 메타데이터를 사용하여 사용자 또는 웹페이지의 문맥 정보를 기반으로 특정 문서를 표시하는 것입니다.</p><p>예를 들어, 사용자의 충성도 수준에 따라 숫자 값으로 표시되는 아이템이나 맞춤형 판매를 강조하고 싶다고 가정해 보겠습니다.</p><p>이 메타데이터를 쿼리에 직접 수집하여 해당 값이 특정 기준을 충족할 때 규칙이 활성화되도록 할 수 있습니다.</p><p>먼저, 충성도 수준이 높은 사용자만 볼 수 있는 문서를 색인화합니다:</p><pre><code>POST _bulk
{ "index": { "_index": "products", "_id": "6" } }
{ "id": "6", "name": "PlayStation Plus Deluxe Card - 12 months", "category": "membership", "brand": "Sony", "price": 300 }</code></pre><p>이제 동일한 규칙 집합 내에 새 규칙을 만들어 loyalty_level이 80 이상일 때 해당 항목이 결과 상단에 표시되도록 해 보겠습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/58de12dff93305440608f51465462fcc68653a08-1421x496.png" style="width: 100%;" /><p>규칙과 규칙 집합을 저장합니다.</p><p>다음은 이에 상응하는 REST 요청입니다:</p><pre><code>PUT _query_rules/my-rules
{
  "rules": [
    {
      "rule_id": "pin-premiun-user",
      "type": "pinned",
      "criteria": [
        {
          "type": "gte",
          "metadata": "loyalty_level",
          "values": [
            80
          ]
        }
      ],
      "actions": {
        "docs": [
          {
            "_index": "products",
            "_id": "6"
          }
        ]
      }
    }
  ]
}</code></pre><p>이제 쿼리를 실행할 때 메타데이터에 새 매개변수 <strong>loyalty_level을 </strong>포함시켜야 합니다. 규칙의 조건이 충족되면 새 문서가 결과 위에 표시됩니다.</p><p>예를 들어 충성도_레벨이 80인 쿼리를 전송하는 경우입니다:</p><pre><code>POST /products/_search
{
  "retriever": {
    "rule": {
      "retriever": {
        "standard": {
          "query": {
            "match": {
              "name": "PlayStation"
            }
          }
        }
      },
      "match_criteria": {
        "query_string": "PlayStation",
        "loyalty_level": 80
      },
      "ruleset_ids": ["my-rules"]
    }
  }
}</code></pre><p>결과 위에 로열티 문서가 표시됩니다:</p><pre><code>{
  "took": 31,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 4,
      "relation": "eq"
    },
    "max_score": 1.7014122e+38,
    "hits": [
      {
        "_index": "products",
        "_id": "6",
        "_score": 1.7014122e+38,
        "_source": {
          "id": "6",
          "name": "PlayStation Plus Deluxe Card - 12 months",
          "category": "membership",
          "brand": "Sony",
          "price": 300
        }
      },
      {
        "_index": "products",
        "_id": "3",
        "_score": 0.5054567,
        "_source": {
          "id": "3",
          "name": "PlayStation 4 Camera",
          "category": "accessory",
          "brand": "Sony",
          "price": 200
        }
      },
      {
        "_index": "products",
        "_id": "1",
        "_score": 0.45618832,
        "_source": {
          "id": "1",
          "name": "PlayStation 4 Slim 1TB",
          "category": "console",
          "brand": "Sony",
          "price": 1200
        }
      },
      {
        "_index": "products",
        "_id": "4",
        "_score": 0.45618832,
        "_source": {
          "id": "4",
          "name": "PlayStation 4 VR Headset",
          "category": "accessory",
          "brand": "Sony",
          "price": 900
        }
      }
    ]
  }
}</code></pre><p>아래의 경우 충성도 레벨이 70이므로 규칙이 충족되지 않으므로 해당 아이템이 상단에 표시되지 않아야 합니다:</p><pre><code>POST /products/_search
{
  "retriever": {
    "rule": {
      "retriever": {
        "standard": {
          "query": {
            "match": {
              "name": "PlayStation"
            }
          }
        }
      },
      "match_criteria": {
        "query_string": "PlayStation",
        "loyalty_level": 70
      },
      "ruleset_ids": ["my-rules"]
    }
  }
}</code></pre><p>결과는 다음과 같습니다:</p><pre><code>{
  "took": 7,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 4,
      "relation": "eq"
    },
    "max_score": 0.5054567,
    "hits": [
      {
        "_index": "products",
        "_id": "3",
        "_score": 0.5054567,
        "_source": {
          "id": "3",
          "name": "PlayStation 4 Camera",
          "category": "accessory",
          "brand": "Sony",
          "price": 200
        }
      },
      {
        "_index": "products",
        "_id": "1",
        "_score": 0.45618832,
        "_source": {
          "id": "1",
          "name": "PlayStation 4 Slim 1TB",
          "category": "console",
          "brand": "Sony",
          "price": 1200
        }
      },
      {
        "_index": "products",
        "_id": "4",
        "_score": 0.45618832,
        "_source": {
          "id": "4",
          "name": "PlayStation 4 VR Headset",
          "category": "accessory",
          "brand": "Sony",
          "price": 900
        }
      },
      {
        "_index": "products",
        "_id": "6",
        "_score": 0.3817649,
        "_source": {
          "id": "6",
          "name": "PlayStation Plus Deluxe Card - 12 months",
          "category": "membership",
          "brand": "Sony",
          "price": 300
        }
      }
    ]
  }
}</code></pre><h2>예: 즉시 제외</h2><p><strong>듀얼쇼크 4 무선 컨트롤러(ID 2)</strong> 를 일시적으로 사용할 수 없어 판매할 수 없다고 가정해 보겠습니다. 따라서 비즈니스 팀은 문서를 수동으로 삭제하거나 일부 데이터 프로세스가 시작될 때까지 기다리는 대신 그 동안 검색 결과에서 해당 문서를 제거하기로 결정합니다.</p><p>방금 인기 있는 항목에 적용한 것과 비슷한 프로세스를 사용하되 이번에는 <em>고정됨을</em> 선택하는 대신 <em>제외를</em> 선택합니다. 이 규칙은 일종의 블랙리스트 역할을 합니다. 쿼리가 실행될 때마다 제외가 작동하도록 기준을 <strong>항상으로</strong> 변경합니다.</p><p>규칙은 다음과 같이 표시되어야 합니다:</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/f10971e4f1bc9520105111adfa3a476581a27130-1600x623.png" style="width: 100%;" /><p>규칙과 규칙 집합을 저장하여 변경 사항을 적용합니다. 다음은 이에 상응하는 REST 요청입니다:</p><pre><code>PUT _query_rules/my-rules
{
  "rules": [
    {
      "rule_id": "rule-6358",
      "type": "pinned",
      "criteria": [
        {
          "type": "always"
        }
      ],
      "actions": {
        "docs": [
          {
            "_index": "products",
            "_id": "2"
          }
        ]
      }
    }
  ]
}</code></pre><p>이제 쿼리를 다시 실행하면 이전 규칙이 항목을 고정하는 것이었음에도 불구하고 해당 항목이 더 이상 결과에 없는 것을 볼 수 있습니다. 이는 <strong>제외가 고정 결과보다 우선하기</strong> 때문입니다.</p><pre><code>{
 "took": 6,
 "timed_out": false,
 "_shards": {
   "total": 1,
   "successful": 1,
   "skipped": 0,
   "failed": 0
 },
 "hits": {
   "total": {
     "value": 4,
     "relation": "eq"
   },
   "max_score": 2.205655,
   "hits": [
     {
       "_index": "products",
       "_id": "3",
       "_score": 2.205655,
       "_source": {
         "id": "3",
         "name": "PlayStation 4 Camera",
         "category": "accessory",
         "brand": "Sony",
         "price": 200
       }
     },
     {
       "_index": "products",
       "_id": "1",
       "_score": 1.9738505,
       "_source": {
         "id": "1",
         "name": "PlayStation 4 Slim 1TB",
         "category": "console",
         "brand": "Sony",
         "price": 1200
       }
     },
     {
       "_index": "products",
       "_id": "4",
       "_score": 1.9738505,
       "_source": {
         "id": "4",
         "name": "PlayStation 4 VR Headset",
         "category": "accessory",
         "brand": "Sony",
         "price": 900
       }
     },
     {
       "_index": "products",
       "_id": "5",
       "_score": 0.69247496,
       "_source": {
         "id": "5",
         "name": "Charging Station for DualShock 4",
         "category": "accessory",
         "brand": "Sony",
         "price": 80
       }
     }
   ]
 }
}</code></pre><h2>결론</h2><p><strong>쿼리 규칙을</strong> 사용하면 코드를 변경하지 않고도 관련성을 매우 쉽게 조정할 수 있습니다. 새로운 <strong>Kibana</strong> <strong>UI를 </strong>사용하면<strong> </strong>몇 초 만에 이러한 변경을 수행할 수 있으므로, 여러분과 여러분의 비즈니스 팀이 검색 결과를 더 잘 제어할 수 있습니다.</p><p>쿼리 규칙은 전자상거래 외에도 지원 포털에서 문제 해결 가이드를 강조 표시하고, 지식창고에 주요 내부 문서를 표시하고, 뉴스 사이트에서 속보를 홍보하고, 만료된 작업 또는 콘텐츠 목록을 필터링하는 등 다양한 시나리오에 활용할 수 있습니다. 사용자 역할이나 지역에 따라 제한된 자료를 숨기는 등의 규정 준수 규칙을 적용할 수도 있습니다.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Mastra와 Elasticsearch를 사용하여 시맨틱 리콜 기능을 갖춘 지식 에이전트 구축하기]]></title>
            <description><![CDATA[메모리 및 정보 검색을 위한 벡터 저장소로 Mastra와 Elasticsearch를 사용해 시맨틱 리콜 기능을 갖춘 지식 에이전트를 구축하는 방법을 알아보세요.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/knowledge-agent-semantic-recall-mastra-elasticsearch</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/knowledge-agent-semantic-recall-mastra-elasticsearch</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[JD Armada]]></dc:creator>
            <pubDate>Thu, 06 Nov 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/b8d51c2726d5573385c9246a7821d12ade4f1b0e-720x420.jpg?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p><a href="https://www.elastic.co/search-labs/blog/context-engineering-overview">컨텍스트 엔지니어링은</a> 안정적인 AI 에이전트 및 아키텍처를 구축하는 데 있어 점점 더 중요해지고 있습니다. 모델이 점점 더 개선됨에 따라 그 효과와 신뢰성은 학습된 데이터보다는 올바른 맥락에 얼마나 잘 근거를 두고 있는지에 따라 달라집니다. 가장 관련성 높은 정보를 적시에 검색하고 적용할 수 있는 에이전트가 정확하고 신뢰할 수 있는 결과물을 만들어낼 가능성이 훨씬 높습니다.</p><p>이 블로그에서는 <a href="https://mastra.ai/">Mastra를</a> 사용해 사용자가 말한 내용을 기억하고 나중에 관련 정보를 불러올 수 있는 지식 에이전트를 구축하는 데 Elasticsearch를 메모리 및 검색 백엔드로 사용하겠습니다. 동일한 개념을 실제 사용 사례로 쉽게 확장하여 지원 상담원이 과거의 대화와 해결 방법을 기억할 수 있어 특정 사용자에게 맞춤형 응답을 제공하거나 이전 컨텍스트를 기반으로 더 빠르게 해결책을 제시할 수 있다고 생각하면 됩니다.</p><p>여기를 따라 단계별로 구축하는 방법을 알아보세요. 길을 잃었거나 완성된 예제를 실행하고 싶다면 <a href="https://github.com/jdarmada/getting-started-mastra-elastic/tree/main">여기에서</a> 리포지토리를 확인하세요.</p><h2>마스트라란 무엇인가요?</h2><p>Mastra는 추론, 메모리 및 도구에 대한 교체 가능한 부품으로 AI 에이전트를 구축하기 위한 오픈 소스 TypeScript 프레임워크입니다. <a href="https://mastra.ai/docs/memory/semantic-recall">시맨틱 리콜</a> 기능을 통해 상담원은 메시지를 벡터 데이터베이스에 임베딩으로 저장하여 과거 상호작용을 기억하고 검색할 수 있습니다. 이를 통해 상담원은 장기적인 대화 맥락과 연속성을 유지할 수 있습니다. Elasticsearch는 효율적인 고밀도 벡터 검색을 지원하기 때문에 이 기능을 활성화하는 데 탁월한 벡터 저장소입니다. 시맨틱 리콜이 트리거되면 에이전트는 관련 과거 메시지를 모델의 컨텍스트 창으로 가져와서 모델이 검색된 컨텍스트를 추론 및 응답의 기초로 사용할 수 있도록 합니다.</p><h2>시작하기 위해 필요한 사항</h2><ul><li>노드 v18+</li><li>Elasticsearch(버전 8.15 이상)</li><li>Elasticsearch API 키</li><li><a href="https://help.openai.com/en/articles/4936850-where-do-i-find-my-openai-api-key">OpenAI API 키</a></li></ul><p>참고: 데모에서는 OpenAI 공급자를 사용하므로 이 공급자가 필요하지만, Mastra는 다른 AI SDK 및 커뮤니티 모델 공급자를 지원하므로 설정에 따라 쉽게 교체할 수 있습니다.</p><h2>Mastra 프로젝트 구축</h2><p>Mastra에 내장된 CLI를 사용하여 프로젝트의 스캐폴딩을 제공하겠습니다. 명령을 실행합니다:</p><pre><code>npm create mastra@latest</code></pre><p>다음과 같은 일련의 프롬프트가 표시됩니다:</p><p>1. 프로젝트 이름을 지정합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/2b9fe559e0276140dd539e24f916a73c60870405-620x84.png" style="width: 100%;" /><p>2. 이 기본값을 그대로 사용해도 되므로 비워두셔도 됩니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/e04729eb03bce8499e973e18c28642402340d0e5-852x68.png" style="width: 100%;" /><p>3. 이 프로젝트에서는 OpenAI에서 제공하는 모델을 사용합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/a86596a469a71bdf8bd99cbaf528d0f0cf7272c0-436x222.png" style="width: 100%;" /><p>4. 모든 환경 변수를 이후 단계에서 구성할 &#x27;.env&#x27; 파일에 저장하므로 &#x27;지금은 건너뛰기&#x27; 옵션을 선택합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/02b19ccc34af0bdacf52fd94b519d036540ca2e6-426x114.png" style="width: 100%;" /><p>5. 이 옵션을 건너뛸 수도 있습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/b3fe63d19d270bc2e0de1dd92033bf8b26750819-990x208.png" style="width: 100%;" /><p>초기화가 완료되면 다음 단계로 넘어갈 수 있습니다.</p><h3>종속성 설치</h3><p>다음으로 몇 가지 종속성을 설치해야 합니다:</p><pre><code>npm install ai @ai-sdk/openai @elastic/elasticsearch dotenv</code></pre><ul><li><code>ai</code> - 자바스크립트/타입스크립트에서 AI 모델, 프롬프트 및 워크플로를 관리할 수 있는 도구를 제공하는 핵심 AI SDK 패키지입니다. Mastra는 Vercel의 <a href="https://ai-sdk.dev/">AI SDK를</a> 기반으로 구축되었으므로 에이전트와의 모델 상호 작용을 활성화하려면 이 종속성이 필요합니다.</li><li><code>@ai-sdk/openai</code> - AI SDK를 OpenAI 모델(예: GPT-4, GPT-4o 등)에 연결하여 OpenAI API 키를 사용하여 API 호출을 가능하게 하는 플러그인입니다.</li><li><code>@elastic/elasticsearch</code> - <a href="https://www.elastic.co/docs/reference/elasticsearch/clients/javascript">Node.js용 공식 Elasticsearch 클라이언트</a>, 인덱싱, 검색, 벡터 작업을 위해 Elastic Cloud 또는 로컬 클러스터에 연결하는 데 사용됩니다.</li><li><code>dotenv</code> - .env에서 환경 변수를 로드합니다. 파일을 process.env에 추가합니다, API 키와 Elasticsearch 엔드포인트와 같은 자격 증명을 안전하게 삽입할 수 있습니다.</li></ul><h3>환경 변수 구성</h3><p>프로젝트 루트 디렉터리에 <code>.env</code> 파일이 없는 경우 이 파일을 만듭니다. 또는 <a href="https://github.com/jdarmada/getting-started-mastra-elastic/blob/main/.env.example">리포지토리에</a> 제공한 예제 <code>.env</code> 를 복사하여 이름을 바꿀 수 있습니다. 이 파일에서 다음 변수를 추가할 수 있습니다:</p><pre><code>ELASTICSEARCH_ENDPOINT="your-endpoint-here"
ELASTICSEARCH_API_KEY="your-key-here"
OPENAI_API_KEY="your-key-here"</code></pre><p>이것으로 기본 설정이 끝났습니다. 여기에서 이미 상담원 구축 및 오케스트레이션을 시작할 수 있습니다. 여기서 한 걸음 더 나아가 저장 및 벡터 검색 레이어로 Elasticsearch를 추가하겠습니다.</p><h2>벡터 저장소로 Elasticsearch 추가하기</h2><p><code>stores</code> 이라는 새 폴더를 만들고 그 안에 이 <a href="https://github.com/jdarmada/getting-started-mastra-elastic/blob/main/src/mastra/stores/elastic-store.ts">파일을</a> 추가합니다. Mastra와 Elastic이 공식 Elasticsearch 벡터 저장소 통합을 출시하기 전에 <a href="https://github.com/abhiaiyer91">Abhi Aiyer</a>(Mastra CTO)가 초기 프로토타입 클래스( <code>ElasticVector</code>)를 공유했습니다. 간단히 말해, Mastra의 메모리 추상화와 Elasticsearch의 고밀도 벡터 기능을 연결하여 개발자가 에이전트를 위한 벡터 데이터베이스로 Elasticsearch를 도입할 수 있습니다.</p><p>통합의 중요한 부분을 자세히 살펴보겠습니다:</p><h3>Elasticsearch 클라이언트 수집</h3><p>이 섹션에서는 <code>ElasticVector</code> 클래스를 정의하고 표준 배포와 서버리스 배포를 모두 지원하는 Elasticsearch 클라이언트 연결을 설정합니다.</p><pre><code>export interface ElasticVectorConfig extends ClientOptions {
    /**
     * Explicitly specify if connecting to Elasticsearch Serverless.
     * If not provided, will be auto-detected on first use.
     */
    isServerless?: boolean;
    
    /**
     * Maximum documents to count accurately when describing indices.
     * Higher values provide accurate counts but may impact performance on large indices.
     * 
     * @default 10000
     */
    maxCountAccuracy?: number;
}

export class ElasticVector extends MastraVector {
    private client: Client;
    private isServerless: boolean | undefined;
    private deploymentChecked: boolean = false;
    private readonly maxCountAccuracy: number;

    constructor(config: ElasticVectorConfig) {
        super();
        this.client = new Client(config);
        this.isServerless = config.isServerless;
        this.maxCountAccuracy = config.maxCountAccuracy ?? 10000;
    }
}</code></pre><ul><li><code>ElasticVectorConfig extends ClientOptions</code>: 이렇게 하면 모든 Elasticsearch 클라이언트 옵션(예: <code>node</code>, <code>auth</code>, <code>requestTimeout</code>)을 상속하고 사용자 정의 속성을 추가하는 새로운 구성 인터페이스가 생성됩니다. 즉, 사용자는 서버리스 전용 옵션과 함께 유효한 모든 Elasticsearch 구성을 전달할 수 있습니다.</li><li><code>extends MastraVector</code>: 이를 통해 <code>ElasticVector</code> 은 모든 벡터 스토어 통합이 준수하는 공통 인터페이스인 Mastra의 기본 <code>MastraVector</code> 클래스에서 상속할 수 있습니다. 이렇게 하면 Elasticsearch가 에이전트의 관점에서 다른 모든 Mastra 벡터 백엔드처럼 작동합니다.</li><li><code>private client: Client</code>: Elasticsearch JavaScript 클라이언트의 인스턴스를 보관하는 개인 속성입니다. 이렇게 하면 클래스가 클러스터와 직접 대화할 수 있습니다.</li><li><code>isServerless</code> 및 <code>deploymentChecked</code>: 이러한 속성은 함께 작동하여 서버리스 또는 표준 Elasticsearch 배포에 연결되어 있는지 여부를 감지하고 캐시합니다. 이 감지는 처음 사용할 때 자동으로 수행되거나 명시적으로 구성할 수 있습니다.</li><li><code>constructor(config: ClientOptions)</code>: 이 생성자는 구성 객체(Elasticsearch 자격 증명 및 선택적 서버리스 설정이 포함된)를 가져와서 <code>this.client = new Client(config)</code> 줄에서 클라이언트를 초기화하는 데 사용합니다.</li><li><code>super()</code>: 이것은 Mastra의 기본 생성자를 호출하므로 로깅, 유효성 검사 헬퍼 및 기타 내부 훅을 상속합니다.</li></ul><p>이 시점에서 Mastra는 다음과 같은 새로운 벡터 스토어가 있다는 것을 알고 있습니다. <code>ElasticVector</code></p><h3>배포 유형 감지</h3><p>인덱스를 생성하기 전에 어댑터는 표준 Elasticsearch를 사용 중인지 아니면 Elasticsearch 서버리스를 사용 중인지 자동으로 감지합니다. 서버리스 배포에서는 수동 샤드 구성을 허용하지 않기 때문에 이 점이 중요합니다.</p><pre><code>private async detectServerless(): Promise<boolean> {
    // Return cached result if already detected
    if (this.deploymentChecked) {
        return this.isServerless ?? false;
    }

    // Use explicit configuration if provided
    if (this.isServerless !== undefined) {
        this.deploymentChecked = true;
        this.logger?.info(
            `Using explicit deployment type: ${this.isServerless ? 'Serverless' : 'Standard'}`
        );
        return this.isServerless;
    }

    try {
        const info = await this.client.info();
        
        // Primary detection: build flavor (most reliable)
        const isBuildFlavorServerless = info.version?.build_flavor === 'serverless';
        
        // Secondary detection: tagline (fallback)
        const isTaglineServerless = info.tagline?.toLowerCase().includes('serverless') ?? false;
        
        this.isServerless = isBuildFlavorServerless || isTaglineServerless;
        this.deploymentChecked = true;
        
        this.logger?.info(
            `Auto-detected ${this.isServerless ? 'Serverless' : 'Standard'} Elasticsearch deployment`,
            { 
                buildFlavor: info.version?.build_flavor, 
                version: info.version?.number,
                detectionMethod: isBuildFlavorServerless ? 'build_flavor' : 'tagline'
            }
        );
        
        return this.isServerless;
    } catch (error) {
        this.logger?.warn(
            'Could not auto-detect deployment type, assuming Standard Elasticsearch. ' +
            'Set isServerless: true explicitly in config if using Serverless.',
            { error: error instanceof Error ? error.message : String(error) }
        );
        this.isServerless = false;
        this.deploymentChecked = true;
        return false;
    }
}</code></pre><p>무슨 일이 일어나고 있나요?</p><ul><li>먼저 구성에서 <code>isServerless</code> 을 명시적으로 설정했는지 확인합니다(자동 감지 건너뛰기).</li><li>Elasticsearch의 <code>info()</code> API를 호출하여 클러스터 정보를 가져옵니다.</li><li><code>build_flavor field</code> (서버리스 배포는 <code>serverless</code>)를 확인합니다.</li><li>빌드 플레이버를 사용할 수 없는 경우 태그 라인 확인으로 돌아가기</li><li>반복되는 API 호출을 방지하기 위해 결과 캐시</li><li>탐지에 실패하면 표준 배포로 기본 설정됩니다.</li></ul><p> 사용 예시:</p><pre><code>// Option 1: Auto-detect (recommended)
const vector = new ElasticVector({
    node: 'https://your-cluster.es.cloud',
    auth: { apiKey: 'your-api-key' }
});
// Detection happens automatically on first index operation

// Option 2: Explicit configuration (faster startup)
const vector = new ElasticVector({
    node: 'https://your-serverless.es.cloud',
    auth: { apiKey: 'your-api-key' },
    isServerless: true  // Skips auto-detection
});</code></pre><h3>Elasticsearch에서 &quot;메모리&quot; 저장소 만들기</h3><p>아래 함수는 임베딩을 저장하기 위한 Elasticsearch 인덱스를 설정합니다. 인덱스가 이미 존재하는지 확인합니다. 그렇지 않은 경우 임베딩 및 사용자 정의 유사성 메트릭을 저장할 <code>dense_vector</code> 필드가 포함된 아래 매핑을 사용하여 매핑을 생성합니다.</p><p>몇 가지 주의해야 할 사항:</p><ul><li><code>dimension</code> 매개변수는 각 임베딩 벡터의 길이로, 사용 중인 임베딩 모델에 따라 달라집니다. 이 경우, <code>1536</code> 크기의 벡터를 출력하는 OpenAI의 <code>text-embedding-3-small</code> 모델을 사용하여 임베딩을 생성하겠습니다. 이 값을 기본값으로 사용하겠습니다.</li><li>아래 매핑에 사용된 <code>similarity</code> 변수는 <code>metric</code> 매개변수의 값을 받아 선택한 거리 메트릭에 대해 Elasticsearch 호환 키워드로 변환하는 도우미 함수 c<code>onst similarity = this.mapMetricToSimilarity(metric)</code> 에서 정의됩니다.<ul><li>예를 들어: 예를 들어, Mastra는 <code>cosine</code>, <code>euclidean</code>, <code>dotproduct</code> 와 같은 벡터 유사성에 대한 일반적인 용어를 사용합니다. <code>euclidean</code> 메트릭을 Elasticsearch 매핑에 직접 전달하면, Elasticsearch는 <code>l2_norm</code> 키워드가 유클리드 거리를 나타낼 것으로 예상하기 때문에 오류가 발생합니다.</li></ul></li><li>서버리스 호환성: 서버리스 배포를 위한 샤드 및 복제본 설정은 Elasticsearch 서버리스에서 자동으로 관리되므로 코드에서 자동으로 생략됩니다.</li></ul><pre><code>async createIndex(params: CreateIndexParams): Promise<void> {
    const { indexName, dimension = 1536, metric = 'cosine' } = params;

    try {
        const exists = await this.client.indices.exists({ index: indexName });

        if (exists) {
            try {
                await this.validateExistingIndex(indexName, dimension, metric);
                this.logger?.info(`Index "${indexName}" already exists and is valid`);
                return;
            } catch (validationError) {
                throw new Error(
                    `Index "${indexName}" exists but does not match the required configuration: ${
                        validationError instanceof Error ? validationError.message : String(validationError)
                    }`
                );
            }
        }

        const isServerless = await this.detectServerless();
        const similarity = this.mapMetricToSimilarity(metric);

        const indexConfig: any = {
            index: indexName,
            mappings: {
                properties: {
                    vector: {
                        type: 'dense_vector',
                        dims: dimension,
                        index: true,
                        similarity: similarity,
                    },
                    metadata: {
                        type: 'object',
                        enabled: true,
                        dynamic: true, // Allows flexible metadata structures
                    },
                },
            },
        };

        // Only configure shards/replicas for non-serverless deployments
        // Serverless manages infrastructure automatically
        if (!isServerless) {
            indexConfig.settings = {
                number_of_shards: 1,
                number_of_replicas: 0, // Increase for production HA deployments
            };
        }

        await this.client.indices.create(indexConfig);

        this.logger?.info(
            `Created ${isServerless ? 'Serverless' : 'Standard'} Elasticsearch index "${indexName}"`,
            { dimension, metric, similarity }
        );
    } catch (error) {
        const errorMessage = error instanceof Error ? error.message : String(error);
        this.logger?.error(`Failed to create index "${indexName}": ${errorMessage}`);
        throw new Error(`Failed to create index "${indexName}": ${errorMessage}`);
    }
}</code></pre><h3>상호 작용 후 새 메모리 또는 노트 저장하기</h3><p>이 함수는 각 상호 작용 후에 생성된 새 임베딩을 메타데이터와 함께 가져온 다음 Elastic의 <code>bulk</code> API를 사용하여 인덱스에 삽입하거나 업데이트합니다. <code>bulk</code> API는 여러 개의 쓰기 작업을 단일 요청으로 그룹화하여 인덱싱 성능을 개선함으로써 에이전트의 메모리가 계속 증가함에 따라 업데이트가 효율적으로 유지되도록 합니다.</p><pre><code>async upsert(params: UpsertVectorParams): Promise<string[]> {
    const { indexName, vectors, metadata = [], ids } = params;

    try {
        // Generate unique IDs if not provided
        const vectorIds = ids || vectors.map((_, i) => 
            `vec_${Date.now()}_${i}_${Math.random().toString(36).substr(2, 9)}`
        );

        const operations = vectors.flatMap((vec, index) => [
            { index: { _index: indexName, _id: vectorIds[index] } },
            {
                vector: vec,
                metadata: metadata[index] || {},
            },
        ]);

        const response = await this.client.bulk({
            refresh: true,
            operations,
        });

        if (response.errors) {
            const erroredItems = response.items.filter((item: any) => item.index?.error);
            const erroredIds = erroredItems.map((item: any) => item.index?._id);
            const errorDetails = erroredItems.slice(0, 3).map((item: any) => ({
                id: item.index?._id,
                error: item.index?.error?.reason || item.index?.error,
                type: item.index?.error?.type
            }));
            
            const errorMessage = `Failed to upsert ${erroredIds.length}/${vectors.length} vectors`;
            console.error(`${errorMessage}. Sample errors:`, JSON.stringify(errorDetails, null, 2));
            this.logger?.error(errorMessage, { 
                failedCount: erroredIds.length, 
                totalCount: vectors.length,
                sampleErrors: errorDetails 
            });
            
            // Still return successfully inserted IDs
            const successfulIds = vectorIds.filter((id, idx) => 
                !erroredIds.includes(id)
            );
            
            if (successfulIds.length === 0) {
                throw new Error(`${errorMessage}. All operations failed. See logs for details.`);
            }
            
            return successfulIds;
        }

        this.logger?.info(`Successfully upserted ${vectors.length} vectors to "${indexName}"`);
        return vectorIds;
    } catch (error) {
        const errorMessage = error instanceof Error ? error.message : String(error);
        this.logger?.error(`Failed to upsert vectors to "${indexName}": ${errorMessage}`);
        throw new Error(`Failed to upsert vectors to "${indexName}": ${errorMessage}`);
    }
}</code></pre><h3>시맨틱 리콜을 위해 유사한 벡터 쿼리하기</h3><p>이 기능은 시맨틱 리콜 기능의 핵심입니다. 에이전트는 벡터 검색을 사용하여 인덱스 내에서 유사한 저장된 임베딩을 찾습니다.</p><pre><code>async query(params: QueryVectorParams<any>): Promise<QueryResult[]> {
    const { indexName, queryVector, topK = 10, filter, includeVector = false } = params;

    try {
        const knnQuery: any = {
            field: 'vector',
            query_vector: queryVector,
            k: topK,
            num_candidates: Math.max(topK * 10, 100), // Search more candidates for better recall
        };

        // Apply metadata filters if provided
        if (filter) {
            knnQuery.filter = this.buildElasticFilter(filter);
        }

        const sourceFields = ['metadata'];
        if (includeVector) {
            sourceFields.push('vector');
        }

        const response = await this.client.search({
            index: indexName,
            knn: knnQuery,
            size: topK,
            _source: sourceFields,
        });

        const results = response.hits.hits.map((hit: any) => ({
            id: hit._id,
            score: hit._score || 0,
            metadata: hit._source?.metadata || {},
            vector: includeVector ? hit._source?.vector : undefined,
        }));

        this.logger?.debug(`Query returned ${results.length} results from "${indexName}"`);
        return results;
    } catch (error) {
        const errorMessage = error instanceof Error ? error.message : String(error);
        this.logger?.error(`Failed to query vectors from "${indexName}": ${errorMessage}`);
        throw new Error(`Failed to query vectors from "${indexName}": ${errorMessage}`);
    }
}</code></pre><p>내부를 들여다보세요:</p><ul><li>Elasticsearch에서 <code>knn</code> API를 사용하여 <a href="https://www.elastic.co/docs/solutions/search/vector/knn">kNN</a> (k-nearest neighbors) 쿼리를 실행합니다.</li><li>입력 쿼리 벡터와 유사한 상위 K개의 벡터를 검색합니다.</li><li>선택적으로 메타데이터 필터를 적용하여 결과 범위를 좁힐 수 있습니다(예: 특정 카테고리 또는 시간 범위 내에서만 검색).</li><li>문서 ID, 유사도 점수, 저장된 메타데이터를 포함한 구조화된 결과를 반환합니다.</li></ul><h2>지식창고 만들기</h2><p>이제 <code>ElasticVector</code> 통합을 통해 Mastra와 Elasticsearch 간의 연결을 확인했으니, 지식 에이전트 자체를 생성해 보겠습니다.</p><p><code>agents</code> 폴더 안에 <code>knowledge-agent.ts</code> 이라는 파일을 만듭니다. 환경 변수를 연결하고 Elasticsearch 클라이언트를 초기화하는 것으로 시작할 수 있습니다.</p><pre><code>import { Agent } from '@mastra/core/agent';
import { Memory } from '@mastra/memory';
import { openai } from '@ai-sdk/openai';
import { Client } from '@elastic/elasticsearch';
import { ElasticVector } from '../stores/elastic-store';
import dotenv from "dotenv";

dotenv.config();

const ELASTICSEARCH_ENDPOINT = process.env.ELASTICSEARCH_ENDPOINT;
const ELASTICSEARCH_API_KEY = process.env.ELASTICSEARCH_API_KEY;

//Error check for undefined credentials
if (!ELASTICSEARCH_ENDPOINT || !ELASTICSEARCH_API_KEY) {
  throw new Error('Missing Elasticsearch credentials');
}

//Check to see if a connection can be established
const testClient = new Client({
  node: ELASTICSEARCH_ENDPOINT,
  auth: { 
    apiKey: ELASTICSEARCH_API_KEY 
  },
});

try {
  await testClient.ping();
  console.log('Connected to Elasticsearch successfully');
} catch (error: unknown) {
  if (error instanceof Error) {
    console.error('Failed to connect to Elasticsearch:', error.message);
  } else {
    console.error('Failed to connect to Elasticsearch:', error);
  }
  process.exit(1);
}
//Initialize the Elasticsearch vector store
const vectorStore = new ElasticVector({
  node: ELASTICSEARCH_ENDPOINT,
  auth: {
    apiKey: ELASTICSEARCH_API_KEY,
  },
//Optional: Explicitly set to true if using Elasticsearch Serverless to skip auto-detection and improve startup time
//isServerless: true,
});</code></pre><p>여기, 우리:</p><ul><li><code>dotenv</code> 을 사용하여 <code>.env</code> 파일에서 변수를 로드합니다.</li><li>Elasticsearch 자격 증명이 올바르게 주입되고 있는지 확인하면 클라이언트에 성공적으로 연결할 수 있습니다.</li><li><code>ElasticVector</code> 생성자에 Elasticsearch 엔드포인트와 API 키를 전달하여 앞서 정의한 벡터 저장소의 인스턴스를 생성합니다.</li><li>선택적으로 Elasticsearch 서버리스를 사용하는 경우 <code>isServerless: true</code> 을 지정합니다. 이렇게 하면 자동 감지 단계를 건너뛰고 시작 시간이 단축됩니다. 이 옵션을 생략하면 처음 사용할 때 어댑터가 자동으로 배포 유형을 감지합니다.</li></ul><p>다음으로 Mastra의 <code>Agent</code> 클래스를 사용하여 에이전트를 정의할 수 있습니다.</p><pre><code>export const knowledgeAgent = new Agent({
    name: 'KnowledgeAgent',
    instructions: 'You are a helpful knowledge assistant.',
    model: openai('gpt-4o'),
    memory: new Memory({

        vector: vectorStore,

        //embedder used to create embeddings for each message
        embedder: 'openai/text-embedding-3-small',

        //set semantic recall options
        options: {
            semanticRecall: {
                topK: 3, // retrieve 3 similar messages
                messageRange: 2, // include 2 messages before/after each match
                scope: 'resource',
            },
        },
    }),
});</code></pre><p>정의할 수 있는 필드는 다음과 같습니다:</p><ul><li><code>name</code> 및 <code>instructions</code>: 아이덴티티와 기본 기능을 부여합니다.</li><li><code>model</code>: <code>@ai-sdk/openai</code> 패키지를 통해 OpenAI의 <code>gpt-4o</code> 를 사용하고 있습니다.</li><li><code>memory</code>:<ul><li><code>vector</code>: Elasticsearch 저장소를 가리키므로 임베딩이 저장되고 거기에서 검색됩니다.</li><li><code>embedder</code>: 임베딩 생성에 사용할 모델</li><li><code>semanticRecall</code> 옵션은 리콜 작동 방식을 결정합니다:<ul><li><code>topK</code>: 검색할 의미적으로 유사한 메시지 수입니다.</li><li><code>messageRange</code>: 각 경기에 포함할 대화 분량입니다.</li><li><code>scope</code>: 메모리 경계를 정의합니다.</li></ul></li></ul></li></ul><p>거의 다 끝났습니다. 새로 생성된 에이전트를 Mastra 구성에 추가하기만 하면 됩니다. <a href="http://index.ts/"><code>index.ts</code></a> 라는 파일에서 지식 에이전트를 가져와서 <code>agents</code> 필드에 삽입합니다.</p><pre><code>export const mastra = new Mastra({
  agents: { knowledgeAgent },
  storage: new LibSQLStore({
    // stores observability, scores, ... into memory storage, if it needs to persist, change to file:../mastra.db
    url: ":memory:",
  }),
  logger: new PinoLogger({
    name: 'Mastra',
    level: 'info',
  }),
  telemetry: {
    // Telemetry is deprecated and will be removed in the Nov 4th release
    enabled: false, 
  },
  observability: {
    // Enables DefaultExporter and CloudExporter for AI tracing
    default: { enabled: true }, 
  },
});</code></pre><p>다른 필드에는 다음이 포함됩니다:</p><ul><li><code>storage</code>: 실행 기록, 통합 가시성 메트릭, 점수 및 캐시를 위한 Mastra의 내부 데이터 저장소입니다. Mastra 스토리지에 대한 자세한 내용은 <a href="https://mastra.ai/docs/server-db/storage">여기를</a> 참조하세요.</li><li><code>logger</code>: Mastra는 경량 구조화된 JSON 로거인 <a href="https://github.com/pinojs/pino">Pino를</a> 사용합니다. 상담원 시작 및 중지, 도구 호출 및 결과, 오류, LLM 응답 시간 등의 이벤트를 캡처합니다.</li><li><code>observability</code>: 상담원의 AI 추적 및 실행 가시성을 제어합니다. 추적합니다:<ul><li>각 추론 단계의 시작/종료</li><li>어떤 모델 또는 도구를 사용했는지.</li><li>입력 및 출력.</li><li>점수 및 평가</li></ul></li></ul><h3>Mastra Studio로 에이전트 테스트</h3><p>축하합니다! 여기까지 왔다면 이 에이전트를 실행하여 시맨틱 리콜 기능을 테스트할 준비가 된 것입니다. 다행히도 Mastra는 기본 제공 채팅 UI를 제공하므로 자체적으로 구축할 필요가 없습니다.</p><p>Mastra 개발 서버를 시작하려면 터미널을 열고 다음 명령을 실행합니다:</p><pre><code>npm run dev</code></pre><p>서버를 처음 번들링하고 시작하면 플레이그라운드의 주소가 제공됩니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/8b045f70008d26aec4d2e6b59d61085555b9c5b2-686x116.png" style="width: 100%;" /><p>이 주소를 브라우저에 붙여넣으면 마스트라 스튜디오로 이동합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/69bc80fe8486edd9e0cf91d87b39f465aeb23111-1600x438.png" style="width: 100%;" /><p><code>knowledgeAgent</code> 옵션을 선택하고 채팅을 시작합니다.</p><p>모든 것이 올바르게 연결되었는지 간단히 테스트하려면 &quot;팀은 10월의 판매 실적이 12% 증가했다고 발표했습니다%, 주로 기업 리뉴얼에 힘입은 것입니다.&quot;와 같은 정보를 입력합니다. 다음 단계는 미드 마켓 고객으로 범위를 넓히는 것입니다.&quot; 다음으로 새 채팅을 시작하고 &quot;다음에 어떤 고객 세그먼트에 집중해야 한다고 했나요?&quot;와 같은 질문을 하세요. 지식 상담원은 첫 번째 채팅에서 제공한 정보를 기억할 수 있어야 합니다. 다음과 같은 응답이 표시되어야 합니다:</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/da8ebddad89874023ed440a8f1ad2cb04ed043f4-1070x288.png" style="width: 100%;" /><p>이와 같은 응답을 보면 에이전트가 이전 메시지를 Elasticsearch에 임베딩으로 성공적으로 저장하고 나중에 벡터 검색을 사용하여 검색했다는 뜻입니다.</p><h3>상담원의 장기 기억 저장소 검사하기</h3><p>Mastra Studio의 상담원 구성에서 <code>memory</code> 탭으로 이동합니다. 이를 통해 상담원이 시간이 지남에 따라 학습한 내용을 확인할 수 있습니다. Elasticsearch에 포함되고 저장되는 모든 메시지, 응답, 상호 작용은 이 장기 기억의 일부가 됩니다. 과거 상호작용을 의미론적으로 검색하여 상담원이 이전에 학습한 정보나 컨텍스트를 빠르게 찾을 수 있습니다. 이는 기본적으로 에이전트가 시맨틱 리콜 중에 사용하는 것과 동일한 메커니즘이지만, 여기서 직접 검사할 수 있습니다. 아래 예시에서는 &#x27;판매&#x27;라는 용어를 검색하여 판매에 관한 내용이 포함된 모든 상호작용을 반환하고 있습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/3decaa0c332d288c5ae0b11c25f592c7d50c2f0f-1104x1320.png" style="width: 100%;" /><h2>결론</h2><p>Mastra와 Elasticsearch를 연결하면 컨텍스트 엔지니어링의 핵심 계층인 메모리를 에이전트에게 제공할 수 있습니다. 시맨틱 리콜을 통해 상담원은 시간이 지남에 따라 컨텍스트를 구축하여 학습한 내용을 기반으로 응답할 수 있습니다. 이는 보다 정확하고 안정적이며 자연스러운 상호작용을 의미합니다.</p><p>이 초기 통합은 시작에 불과합니다. 여기서 동일한 패턴으로 과거 티켓을 기억하는 지원 상담원, 관련 문서를 검색하는 내부 봇, 대화 중에 고객 세부 정보를 기억할 수 있는 AI 어시스턴트 등을 만들 수 있습니다. 또한, 가까운 시일 내에 이 페어링이 더욱 원활하게 이루어질 수 있도록 공식적인 Mastra 통합을 위해 노력하고 있습니다.</p><p>여러분이 다음에 무엇을 만들지 기대가 됩니다. 한 번 사용해보시고 <a href="https://mastra.ai/">Mastra와</a> 그 메모리 기능을 살펴보고 발견한 내용을 커뮤니티와 자유롭게 공유하세요.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Elasticsearch와 SigLIP-2로 산봉우리에 대한 멀티모달 검색 ]]></title>
            <description><![CDATA[SigLIP-2 임베딩과 Elasticsearch kNN 벡터 검색을 사용해 텍스트 대 이미지 및 이미지 대 이미지 다중 모드 검색을 구현하는 방법을 알아보세요. 프로젝트 초점: 에베레스트 트레킹에서 아마다블람 산 정상 사진 찾기.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/multimodal-search-siglip-2-elasticsearch</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/multimodal-search-siglip-2-elasticsearch</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Navneet Kumar]]></dc:creator>
            <pubDate>Tue, 04 Nov 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/ffcf93358a7c5dadcea82faf3de460bf060d003c-1600x1200.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>사진 앨범을 의미별로 검색하고 싶었던 적이 있나요? &quot;파란색 재킷을 입고 벤치에 앉아 있는 내 사진 보여줘&quot;, &quot;에베레스트산 사진 보여줘&quot;, &quot;사케와 초밥&quot; 등의 검색어를 사용해 보세요. 커피 한 잔(또는 좋아하는 음료)을 들고 계속 읽으세요. 이 블로그에서는 멀티모달 하이브리드 검색 애플리케이션을 구축하는 방법을 설명합니다. 멀티모달이란 앱이 단어뿐만 아니라 텍스트, 이미지, 오디오 등 다양한 종류의 입력을 이해하고 검색할 수 있다는 뜻입니다. 하이브리드란 키워드 매칭, kNN 벡터 검색, 지오펜싱과 같은 기술을 결합하여 더 선명한 결과를 제공하는 것을 의미합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/0ec6bbb45013846b59ee00d2bf73ee2182ee7392-1920x1080.gif" style="width: 100%;" /><p>이를 위해 Google의 SigLIP-2를 사용해 이미지와 텍스트 모두에 대한 벡터 임베딩을 생성하고 이를 Elasticsearch 벡터 데이터베이스에 저장합니다. 쿼리 시 텍스트 또는 이미지와 같은 검색 입력을 임베딩으로 변환하고 빠른 kNN 벡터 검색을 실행하여 결과를 검색합니다. 이 설정을 통해 텍스트 대 이미지 및 이미지 대 이미지 검색을 효율적으로 수행할 수 있습니다. 스트림릿 UI는 텍스트 기반 검색을 통해 앨범에서 일치하는 사진을 찾아서 볼 수 있을 뿐만 아니라 업로드된 이미지에서 산봉우리를 식별하고 사진 앨범에서 해당 산의 다른 사진을 볼 수 있는 프론트엔드를 제공함으로써 이 프로젝트에 활기를 불어넣었습니다.<br/>또한 검색 정확도를 개선하기 위해 취한 조치와 실용적인 팁과 요령에 대해서도 설명합니다. 더 자세히 살펴볼 수 있도록 <a href="https://github.com/navneet83/multimodal-mountain-peak-search">GitHub 리포지토리와</a> <a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/notebooks/multimodal_mountain_peak_search.ipynb">Colab 노트북을</a> 제공합니다.</p><h2>시작 방법</h2><p>이 블로그 게시물은 에베레스트 베이스캠프 트레킹에서 찍은 아마 다블람 산의 모든 사진을 보여 달라는 10살짜리 아이의 요청에 영감을 받아 작성했습니다. 사진첩을 훑어보면서 이름을 알 수 없는 다른 산봉우리도 몇 개 더 찾아달라는 요청을 받았습니다.</p><p>이를 통해 재미있는 컴퓨터 비전 프로젝트가 될 수 있겠다는 생각이 들었습니다. 우리가 달성하고자 했던 목표:</p><ul><li>이름으로 산봉우리 사진 찾기</li><li>이미지에서 산봉우리 이름을 맞추고 사진 앨범에서 비슷한 봉우리를 찾습니다.</li><li>컨셉 쿼리가 작동하도록 하기<em>(사람</em>, <em>강</em>, <em>기도 깃발</em> <em>등)</em></li></ul><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/e9d0d720a9b565d5b749bdc915068852d4f157ad-1200x1600.png" style="width: 100%;" /><h2>드림팀 구성: SigLIP-2, Elasticsearch &amp; Streamlit</h2><p>이 작업을 수행하려면 텍스트(&#x27;Ama Dablam&#x27;)와 이미지(내 앨범의 사진)를 모두 의미 있게 비교할 수 있는 벡터, 즉 동일한 벡터 공간으로 변환해야 한다는 것이 금방 분명해졌습니다. 이렇게 하면 검색은 &quot;가장 가까운 이웃을 찾는 것&quot;에 불과합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/20e6f8b7d4fa48414f407ec200adbe00ee28d517-1536x1024.png" style="width: 100%;" /><div style="display:none">Unknown block type "block", specify a component for it in the `components.types` option</div><p>최근 Google에서 출시한 <a href="https://huggingface.co/blog/siglip2"><strong>SigLIP-2가</strong></a> 여기에 잘 맞습니다. 작업별 교육( <strong>제로 샷</strong> 설정) 없이 임베딩을 생성할 수 있으며, 라벨이 없는 사진과 이름과 언어가 다른 봉우리라는 사용 사례에 적합하게 작동합니다. 텍스트 ↔ 이미지 매칭을 위해 학습되었기 때문에 쿼리 언어나 철자가 다르더라도 트레킹에서 찍은 산 사진과 짧은 텍스트 프롬프트가 임베딩으로 비슷하게 표시됩니다.</p><p>SigLIP-2는 강력한 속도 대비 품질 균형을 제공하고, 다양한 입력 해상도를 지원하며, CPU와 GPU 모두에서 실행됩니다. SigLIP-2는 기존 CLIP과 같은 이전 모델에 비해 야외 촬영에 더욱 견고하게 설계되었습니다. 테스트하는 동안 SigLIP-2는 일관되게 신뢰할 수 있는 결과를 생성했습니다. 또한 지원도 매우 잘 되어 있어 이 프로젝트의 확실한 선택이 될 것입니다.</p><p>다음으로 임베딩과 파워 검색을 저장할 벡터 데이터베이스가 필요합니다. 이미지 임베딩에 대한 코사인 kNN 검색을 지원할 뿐만 아니라 단일 쿼리에서 지오펜스 및 텍스트 필터를 적용할 수 있어야 합니다. Elasticsearch는 벡터(dense_vector 필드의 HNSW kNN)를 매우 잘 처리하고 텍스트, 벡터, 위치 기반 쿼리를 결합하는 하이브리드 검색을 지원하며 필터링과 정렬 기능을 기본으로 제공합니다. 또한 수평으로 확장할 수 있어 몇 장의 사진에서 수천 장으로 쉽게 늘릴 수 있습니다. 공식 <a href="https://www.elastic.co/docs/reference/elasticsearch/clients/python">Elasticsearch Python 클라이언트는</a> 배관을 단순하게 유지하며 프로젝트와 깔끔하게 통합됩니다. 마지막으로 검색 쿼리를 입력하고 결과를 볼 수 있는 경량 프론트엔드가 필요합니다. 파이썬 기반의 빠른 데모를 원한다면 Streamlit이 적합합니다. 파일 업로드, 반응형 이미지 그리드, 정렬 및 지오펜싱을 위한 드롭다운 메뉴 등 우리에게 필요한 기본 요소를 제공합니다. 로컬에서 쉽게 복제하고 실행할 수 있으며 Colab 노트북에서도 작동합니다.</p><h2>구현</h2><h3>Elasticsearch 인덱싱 설계 및 인덱싱 전략</h3><p>이 프로젝트에는 <code>peaks_catalog</code> 와 <code>photos</code> 의 두 가지 인덱스를 사용할 것입니다.</p><h4>Peaks_catalog 인덱스</h4><p>이 색인은 에베레스트 베이스캠프 트레킹 중에 볼 수 있는 주요 산봉우리를 간결하게 정리한 카탈로그 역할을 합니다. 이 색인에 포함된 각 문서는 에베레스트 산과 같은 하나의 산봉우리에 해당합니다. 각 산봉우리 문서에는 이름/별칭, 위도-경도 좌표(선택 사항), SigLIP-2 텍스트 프롬프트(+ 참조 이미지 옵션)를 혼합하여 구축한 단일 프로토타입 벡터가 저장됩니다.</p><p><strong>인덱스 매핑:</strong></p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">필드</td><td style="border: 1px solid #ccc; padding: 4px;">유형</td><td style="border: 1px solid #ccc; padding: 4px;">예</td><td style="border: 1px solid #ccc; padding: 4px;">목적/참고 사항</td><td style="border: 1px solid #ccc; padding: 4px;">벡터/인덱싱</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">id</td><td style="border: 1px solid #ccc; padding: 4px;">키워드</td><td style="border: 1px solid #ccc; padding: 4px;">아마다블람</td><td style="border: 1px solid #ccc; padding: 4px;">안정적인 슬러그/ID</td><td style="border: 1px solid #ccc; padding: 4px;">-</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">이름</td><td style="border: 1px solid #ccc; padding: 4px;">텍스트 + 키워드 하위 필드</td><td style="border: 1px solid #ccc; padding: 4px;">["아마다블람","아마다블람"]</td><td style="border: 1px solid #ccc; padding: 4px;">별칭/다국어 이름; 정확한 필터를 위한 names.raw</td><td style="border: 1px solid #ccc; padding: 4px;">-</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">latlon</td><td style="border: 1px solid #ccc; padding: 4px;">geo_point</td><td style="border: 1px solid #ccc; padding: 4px;">{"lat":27.8617,"lon":86.8614}</td><td style="border: 1px solid #ccc; padding: 4px;">위도/경도 조합의 피크 GPS 좌표(선택 사항)</td><td style="border: 1px solid #ccc; padding: 4px;">-</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">elev_m</td><td style="border: 1px solid #ccc; padding: 4px;">정수</td><td style="border: 1px solid #ccc; padding: 4px;">6812</td><td style="border: 1px solid #ccc; padding: 4px;">고도(선택 사항)</td><td style="border: 1px solid #ccc; padding: 4px;">-</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">text_embed</td><td style="border: 1px solid #ccc; padding: 4px;">dense_vector</td><td style="border: 1px solid #ccc; padding: 4px;">768</td><td style="border: 1px solid #ccc; padding: 4px;">이 피크에 대한 혼합 프로토타입(프롬프트 및 선택적으로 1~3개의 참조 이미지)</td><td style="border: 1px solid #ccc; padding: 4px;">index:true, 유사성:"코사인", index_옵션:{type:"hnsw", m:16, ef_construction:128}</td>
                    </tr>
              </tbody>
            </table>
          <p>이 색인은 주로 이미지에서 산봉우리를 식별하는 등 이미지 대 이미지 검색에 사용됩니다. 또한 이 인덱스를 사용하여 텍스트-이미지 검색 결과를 개선합니다.</p><p><code>peaks_catalog</code> 요약하면, &quot;어떤 산입니까?&quot; 라는 질문을 가장 가까운 이웃에 초점을 맞춘 문제로 변환하여 이미지 데이터의 복잡성에서 개념적 이해를 효과적으로 분리하는 것입니다.</p><p><strong>peaks_catalog 인덱스의 인덱싱 전략: </strong>EBC 트레킹 중 가장 눈에 띄는 봉우리 목록을 만드는 것부터 시작합니다. 각 봉우리에 대해 지리적 위치, 이름, 동의어, 고도를 <a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/data/peaks.yaml">yaml 파일에</a> 저장합니다. 다음 단계는 각 피크에 대한 <a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/scripts/embed_and_index_photos.py#L351">임베딩을 생성하여</a> <code>text_embed</code> 필드에 저장하는 것입니다. 강력한 임베딩을 생성하기 위해 다음 기술을 사용합니다:</p><ul><li>다음을 사용하여 텍스트 프로토타입을 만듭니다:<ul><li>봉우리 이름</li><li><a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/scripts/embed_and_index_photos.py#L301">프롬프트 앙상블</a> (여러 개의 다른 프롬프트를 사용하여 동일한 질문에 답하기) 등을 예로 들 수 있습니다:<ul><li>&quot;네팔 히말라야의 산봉우리 {name} 의 자연 사진&quot;</li><li>&quot;{name} 쿰부 지역의 랜드마크 봉우리, 고산 풍경&quot;</li><li>&quot;{name} 산 정상, 눈, 바위 능선&quot;</li></ul></li><li>선택적 <a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/scripts/embed_and_index_photos.py#L333">안티 콘셉트</a> (SigLIP-2에 일치하지 않을 대상을 알려줌): &#x27;그림, 일러스트, 포스터, 지도, 로고&#x27;에 대해 작은 벡터를 빼서 실제 사진에 편향되도록 합니다.</li></ul></li><li>피크의 참조 이미지가 제공된 경우 선택적으로 <a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/scripts/embed_and_index_photos.py#L388C13-L388C29">이미지 프로토타입을 생성합니다</a>.</li></ul><p>그런 다음 <a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/scripts/embed_and_index_photos.py#L392">텍스트와 이미지 프로토타입을 혼합하여</a> 최종 임베딩을 생성합니다. 마지막으로 모든 필수 필드가 포함된 문서가 <a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/scripts/embed_and_index_photos.py#L396">색인됩니다</a>:</p><pre><code>def l2norm(v: np.ndarray) -> np.ndarray:
    return v / (np.linalg.norm(v) + 1e-12)
def compute_blended_peak_vec(
        emb: Siglip2,
        names: List[str],
        peak_id: str,
        peaks_images_root: str,
        alpha_text: float = 0.5,
        max_images: int = 3,
) -> Tuple[np.ndarray, int, int, List[str]]:
    """
    Build blended vector for a single peak.

    Returns:
      vec           : np.ndarray (L2-normalized)
      found_count   : number of reference images discovered
      used_count    : number of references used (<= max_images)
      used_filenames: list of filenames used (for logging)
    """
    # 1) TEXT vector
    tv = embed_text_blend(emb, names)

    # 2) IMAGE refs: prefer folder by id; fallback to slug of the primary name
    root = Path(peaks_images_root)
    candidates = [root / peak_id]
    if names:
        candidates.append(root / slugify(names[0]))

    all_refs: List[Path] = []
    for c in candidates:
        if c.exists() and c.is_dir():
            all_refs = list_ref_images(c)
            if all_refs:
                break

    found = len(all_refs)
    used_list = all_refs[:max_images] if (max_images and found > max_images) else all_refs
    used = len(used_list)

    img_v = embed_image_mean(emb, used_list) if used_list else None

    # 3) Blend TEXT and IMAGE vectors, clamp alpha to [0,1]
    a = max(0.0, min(1.0, float(alpha_text)))
    vec = l2norm(tv if img_v is None else (a * tv + (1.0 - a) * img_v)).astype("float32")
    return vec, found, used, [p.name for p in used_list]</code></pre><p><code>peaks_catalog</code> 색인의 샘플 문서입니다:</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/bc05fbd0c4f8d721d5170c28a3884a9eda80bb7d-1210x1132.png" style="width: 100%;" /><h4>사진 색인</h4><p>이 기본 색인은 앨범의 모든 사진에 대한 자세한 정보를 저장합니다. 각 문서는 다음 정보를 포함하는 단일 사진을 나타냅니다:</p><ul><li>사진 앨범에서 사진의 상대 경로입니다. 검색 UI에서 일치하는 이미지를 보거나 이미지를 로드하는 데 사용할 수 있습니다.</li><li>사진의 GPS 및 시간 정보.</li><li>SigLIP-2에서 생성된 이미지 인코딩을 위한 고밀도 벡터입니다.</li><li><code>predicted_peaks</code> 를 사용하면 피크 이름을 기준으로 필터링할 수 있습니다.<br/><br/><strong>인덱스 매핑</strong></li></ul>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">필드</td><td style="border: 1px solid #ccc; padding: 4px;">유형</td><td style="border: 1px solid #ccc; padding: 4px;">예</td><td style="border: 1px solid #ccc; padding: 4px;">목적/참고 사항</td><td style="border: 1px solid #ccc; padding: 4px;">벡터 / 인덱싱</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">경로</td><td style="border: 1px solid #ccc; padding: 4px;">키워드</td><td style="border: 1px solid #ccc; padding: 4px;">데이터/이미지/IMG_1234.HEIC</td><td style="border: 1px solid #ccc; padding: 4px;">UI에서 썸네일/전체 이미지를 여는 방법</td><td style="border: 1px solid #ccc; padding: 4px;">-</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">clip_image</td><td style="border: 1px solid #ccc; padding: 4px;">dense_vector</td><td style="border: 1px solid #ccc; padding: 4px;">768</td><td style="border: 1px solid #ccc; padding: 4px;">SigLIP-2 이미지 임베딩</td><td style="border: 1px solid #ccc; padding: 4px;">index:true, 유사성:"코사인", index_옵션:{type:"hnsw", m:16, ef_construction:128}</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">예측된 피크</td><td style="border: 1px solid #ccc; padding: 4px;">키워드</td><td style="border: 1px solid #ccc; padding: 4px;">["아마다블람","푸모리"]</td><td style="border: 1px solid #ccc; padding: 4px;">인덱스 시점의 Top-K 추측(저렴한 UX 필터/패싯)</td><td style="border: 1px solid #ccc; padding: 4px;">-</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">gps</td><td style="border: 1px solid #ccc; padding: 4px;">geo_point</td><td style="border: 1px solid #ccc; padding: 4px;">{"lat":27.96,"lon":86.83}</td><td style="border: 1px solid #ccc; padding: 4px;">지리적 필터 사용</td><td style="border: 1px solid #ccc; padding: 4px;">-</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">shot_time</td><td style="border: 1px solid #ccc; padding: 4px;">date</td><td style="border: 1px solid #ccc; padding: 4px;">2023-10-18T09:41:00Z</td><td style="border: 1px solid #ccc; padding: 4px;">캡처 시간: 정렬/필터링</td><td style="border: 1px solid #ccc; padding: 4px;">-</td>
                    </tr>
              </tbody>
            </table>
          <p><strong>사진 색인 색인 전략: </strong>앨범의 각 사진에 대해 다음을 수행합니다:<br/><a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/scripts/embed_and_index_photos.py#L526">이미지 메타데이터에서 이미지</a> <code>shot_time</code> 및 <code>gps</code> 정보를 추출합니다.</p><ul><li><a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/scripts/embed_and_index_photos.py#L511">SigLIP-2 이미지 임베딩</a>: 이미지를 모델에 전달하고 벡터를 L2 정규화합니다. <code>clip_image</code> 필드에 임베딩을 저장합니다.</li><li><a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/scripts/embed_and_index_photos.py#L519">피크를 예측하여</a> <code>predicted_peaks</code> 필드에 저장합니다. 이를 위해 먼저 이전 단계에서 생성된 사진의 이미지 벡터를 가져온 다음 <code>peaks_catalog</code> 인덱스의 text_embed 필드에 대해 빠른 kNN 검색을 실행합니다. 상위 3~4개 봉우리는 유지하고 나머지는 무시합니다.</li><li>이미지 이름과 경로에 <a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/scripts/embed_and_index_photos.py#L509">해시를</a> 수행하여 <code>_id</code> 필드를 계산합니다. 이렇게 하면 여러 번 실행한 후에도 중복이 발생하지 않습니다.</li></ul><p>사진의 모든 필드를 결정한 후에는 <a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/scripts/embed_and_index_photos.py#L530">일괄</a> 색인을 사용하여 사진 문서를 일괄 색인합니다:</p><pre><code>def bulk_index_photos(
        es: Elasticsearch,
        images_root: str,
        photos_index: str = "photos",
        peaks_index: str = "peaks_catalog",
        topk_predicted: int = 5,
        batch_size: int = 200,
        refresh: str = "false",
) -> None:
    """Walk a folder of images, embed + enrich, and bulk index to Elasticsearch."""
    root = Path(images_root)
    if not root.exists():
        raise SystemExit(f"Images root not found: {images_root}")

    emb = Siglip2()
    batch: List[Dict[str, Any]] = []
    n_indexed = 0

    for p in iter_images(root):
        rel = relpath_within(root, p)
        _id = id_for_path(rel)

        # 1) Image embedding (and reuse it for predicted_peaks)
        try:
            with Image.open(p) as im:
                ivec = emb.image_vec(im.convert("RGB")).astype("float32")
        except (UnidentifiedImageError, OSError) as e:
            print(f"[skip] {rel} — cannot embed: {e}")
            continue

        # 2) Predict top-k peak names
        try:
            top_names = predict_peaks(es, ivec.tolist(), peaks_index=peaks_index, k=topk_predicted)
        except Exception as e:
            print(f"[warn] predict_peaks failed for {rel}: {e}")
            top_names = []

        # 3) EXIF enrichment (safe)
        gps = get_gps_decimal(str(p))
        shot = get_shot_time(str(p))

        # 4) Build doc and stage for bulk
        doc = {"path": rel, "clip_image": ivec.tolist(), "predicted_peaks": top_names}
        if gps:
            doc["gps"] = gps
        if shot:
            doc["shot_time"] = shot

        batch.append(
            {"_op_type": "index", "_index": photos_index, "_id": _id, "_source": doc}
        )

        # 5) Periodic flush
        if len(batch) >= batch_size:
            helpers.bulk(es, batch, refresh=refresh)
            n_indexed += len(batch)
            print(f"[photos] indexed {n_indexed} (last: {rel})")
            batch.clear()

    # Final flush
    if batch:
        helpers.bulk(es, batch, refresh=refresh)
        n_indexed += len(batch)
        print(f"[photos] indexed {n_indexed} total.")

    print("[done] photos indexing")</code></pre><p>사진 색인의 샘플 문서입니다:</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/1dc1406ac2a97440b6804838795b3c2205c4c6b2-1080x1234.png" style="width: 100%;" /><p>요약하자면, 사진 인덱스는 앨범에 있는 모든 사진을 빠르고 필터링이 가능하며 kNN으로 저장할 수 있는 저장소입니다. 매핑은 일부러 최소화하여 빠르게 검색하고, 깔끔하게 표시하고, 공간과 시간별로 결과를 분류할 수 있는 구조로만 구성했습니다. 이 인덱스는 두 가지 검색 사용 사례를 모두 지원합니다. 두 인덱스를 생성하는 Python 스크립트는 <a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/scripts/create_indices.py">여기에서</a> 찾을 수 있습니다.</p><p>아래의 Kibana 지도 시각화에서는 사진 앨범의 문서를 녹색 점으로, <code>peaks_catalog</code> 인덱스의 산봉우리를 빨간색 삼각형으로 표시하며, 녹색 점이 에베레스트 베이스캠프 트레킹 코스와 잘 정렬되어 있습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/1c75d0ed0ce53d28a94bf2f47a354e25581d2baf-1600x1402.png" style="width: 100%;" /><h2>검색 사용 사례</h2><p><strong>이름으로 검색(텍스트-이미지):</strong> 이 기능을 사용하면 텍스트 검색을 통해 산봉우리 사진(및 &#x27;기도 깃발&#x27;과 같은 추상적인 개념까지)을 찾을 수 있습니다. 이를 위해 텍스트 입력은 SigLIP-2를 사용하여 <a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/scripts/query_by_peak_name.py#L87C5-L87C20">텍스트 벡터로 변환됩니다</a>. 강력한 텍스트 벡터 생성을 위해 인덱스에서 텍스트 임베딩을 생성할 때와 동일한 전략을 <code>peaks_catalog</code> 사용합니다. <a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/scripts/query_by_peak_name.py#L104"></a> 즉, 텍스트 입력을 작은<a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/scripts/query_by_peak_name.py#L103"> 프롬프트</a> <a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/scripts/query_by_peak_name.py#L100"></a>앙상블과 결합하고, 작은 반개념 벡터를 뺀 다음 <a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/scripts/query_by_peak_name.py#L104">L2 정규화를</a> 적용하여 최종 쿼리 벡터를 생성하는 것입니다. 그런 다음 <code>photos.clip_image</code> 필드에서 kNN <a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/scripts/query_by_peak_name.py#L140">쿼리를</a> 실행하여 가장 가까운 이미지를 찾기 위해 코사인 유사성을 기반으로 가장 일치하는 상위 피크를 검색합니다. 선택적으로 지역 및 날짜 <a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/scripts/query_by_peak_name.py#L152">필터를</a> 적용하거나 <code>photos.predicted_peaks</code> 용어 필터를 쿼리의 일부로 적용하여 검색 결과의 연관성을 높일 수 있습니다(아래 쿼리 예시 참조). 이렇게 하면 트레킹에서 실제로 보이지 않는 유사 봉우리를 제외하는 데 도움이 됩니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/b5fac28ffdbedb820505365ca07df125cd01b939-946x370.png" style="width: 100%;" /><p><strong>지리적 필터를 사용한 Elasticsearch 쿼리:</strong></p><pre><code>POST photos/_search
{
  "knn": {
    "field": "clip_image",
    "query_vector": [ ... ],
    "k": 60,
    "num_candidates": 2000
  },
  "query": {
    "bool": {
      "filter": [
        { "geo_bounding_box": { "gps": { "top_left": "...", "bottom_right": "..." } } }
      ]
    }
  },
  "_source": ["path","predicted_peaks","gps","shot_time"]
}

Response (first two documents):
{
 "hits": {
   "total": {
     "value": 56,
     "relation": "eq"
   },
   "max_score": 0.5779596,
   "hits": [
     {
       "_index": "photos",
       "_id": "d01da3a1141981486c3493f6053c79e92a788463",
       "_score": 0.5779596,
       "_source": {
         "path": "IMG_2738.HEIC",
         "predicted_peaks": [
           "Pumori",
           "Kyajo Ri",
           "Khumbila",
           "Nangkartshang",
           "Kongde Ri"
         ],
         "gps": {
           "lat": 27.97116388888889,
           "lon": 86.82331111111111
         },
         "shot_time": "2023-11-03T08:07:13"
       }
     },
     {
       "_index": "photos",
       "_id": "c79d251f07adc5efaedc53561110a7fd78e23914",
       "_score": 0.5766071,
       "_source": {
         "path": "IMG_2761.HEIC",
         "predicted_peaks": [
           "Kyajo Ri",
           "Makalu",
           "Baruntse",
           "Cho Oyu",
           "Khumbila"
         ],
         "gps": {
           "lat": 27.975558333333332,
           "lon": 86.82515
         },
         "shot_time": "2023-11-03T08:51:08"
       }
     }
}</code></pre><p><strong>이미지로 검색(이미지 대 이미지):</strong> 이 기능을 사용하면 사진에서 산을 식별하고 사진 앨범 내에서 같은 산의 다른 이미지를 찾을 수 있습니다. 이미지가 업로드되면 SigLIP-2 이미지 인코더가 이미지를 처리하여 <a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/scripts/identify_from_picture_find_similar_peaks.py#L228">이미지 벡터를</a> 생성합니다. 그런 다음 <code>peaks_catalog.text_embed</code> 필드에서 <a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/scripts/identify_from_picture_find_similar_peaks.py#L234">kNN 검색을</a> 수행하여 가장 일치하는 피크 이름을 식별합니다. 그 후, 일치하는 피크 이름에서 <a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/scripts/identify_from_picture_find_similar_peaks.py#L257">텍스트 벡터를 생성하고</a> 사진 인덱스에서 또 다른 <a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/scripts/identify_from_picture_find_similar_peaks.py#L263">kNN 검색을</a> 수행하여 해당 사진을 찾습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/3a3d5635bee7a222b95529dd7f9fbee016381610-1226x550.png" style="width: 100%;" /><p><strong>Elasticsearch 쿼리:</strong></p><p>1단계: 일치하는 피크 이름 찾기</p><pre><code>GET peaks_catalog/_search
{
 "knn": {
   "field": "text_embed",
   "query_vector": [...image-vector... ],
   "k": 3,
   "num_candidates": 500
 },
 "_source": [
   "id",
   "names",
   "latlon",
   "text_embed"
 ]
}


Response (first two documents):
{
 "took": 2,
 "timed_out": false,
 "_shards": {
   "total": 1,
   "successful": 1,
   "skipped": 0,
   "failed": 0
 },
 "hits": {
   "total": {
     "value": 3,
     "relation": "eq"
   },
   "max_score": 0.58039916,
   "hits": [
     {
       "_index": "peaks_catalog",
       "_id": "pumori",
       "_score": 0.58039916,
       "_source": {
         "id": "pumori",
         "names": [
           "Pumori",
           "Pumo Ri"
         ],
         "latlon": {
           "lat": 28.01472,
           "lon": 86.82806
         },
         "text_embed": [
                  ... embeddings...
         ]
       }
     },
     {
       "_index": "peaks_catalog",
       "_id": "kyajo-ri",
       "_score": 0.57942784,
       "_source": {
         "id": "kyajo-ri",
         "names": [
           "Kyajo Ri",
           "Kyazo Ri"
         ],
         "latlon": {
           "lat": 27.909167,
           "lon": 86.673611
         },
         "text_embed": [
           ... embeddings...
         ]
       }
     }
   ]
 }
}</code></pre><p>2단계: <code>photos</code> 색인에서 검색을 수행하여 일치하는 사진을 찾습니다(텍스트-이미지 검색 사용 사례에 표시된 것과 동일한 쿼리):</p><pre><code>POST photos/_search
{
 "knn": {
   "field": "clip_image",
   "query_vector": [ ...image-vector... ],
   "k": 30,
   "num_candidates": 2000
 },
 "_source": [
   "path",
   "gps",
   "shot_time",
   "predicted_peaks",
   "clip_image"
 ],
 "query": {
   "bool": {
     "filter": [
       {
         "term": {
           "predicted_peaks": "Pumori"
         }
       }
     ]
   }
 }
}


Response (first two documents):
{
 "hits": {
   "total": {
     "value": 56,
     "relation": "eq"
   },
   "max_score": 0.5779596,
   "hits": [
     {
       "_index": "photos",
       "_id": "d01da3a1141981486c3493f6053c79e92a788463",
       "_score": 0.5779596,
       "_source": {
         "path": "IMG_2738.HEIC",
         "predicted_peaks": [
           "Pumori",
           "Kyajo Ri",
           "Khumbila",
           "Nangkartshang",
           "Kongde Ri"
         ],
         "gps": {
           "lat": 27.97116388888889,
           "lon": 86.82331111111111
         },
         "shot_time": "2023-11-03T08:07:13"
       }
     },
     {
       "_index": "photos",
       "_id": "c79d251f07adc5efaedc53561110a7fd78e23914",
       "_score": 0.5766071,
       "_source": {
         "path": "IMG_2761.HEIC",
         "predicted_peaks": [
           "Kyajo Ri",
           "Makalu",
           "Baruntse",
           "Cho Oyu",
           "Khumbila"
         ],
         "gps": {
           "lat": 27.975558333333332,
           "lon": 86.82515
         },
         "shot_time": "2023-11-03T08:51:08"
       }
     }
}</code></pre><h2>스트림라이트 UI</h2><p>모든 것을 하나로 모으기 위해 두 가지 검색 사용 사례를 모두 수행할 수 있는 간단한 Streamlit UI를 만들었습니다. 왼쪽 레일에는 스크롤 가능한 피크 목록( <code>photos.predicted_peaks</code> 에서 집계됨)이 체크박스와 미니맵/지리 필터와 함께 표시됩니다. 상단에는 <strong>이름으로 검색하기</strong> 상자와 <strong>사진 업로드에서 식별하기</strong> 버튼이 있습니다. 가운데 창에는 반응형 썸네일 그리드가 있어 kNN 점수, 예상 피크 배지, 캡처 시간을 보여줍니다. 각 이미지에는 전체 해상도 미리 보기를 위한 <strong>이미지 보기</strong> 버튼이 포함되어 있습니다.</p><p><strong>이미지를 업로드하여 검색합니다:</strong> 피크를 예측하고 사진 앨범에서 일치하는 피크를 찾습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/dca540cbf5279e6d6102c5a0c0351ddd4ac91cda-1600x1112.png" style="width: 100%;" /><p><strong>텍스트로 검색</strong>: 텍스트에서 앨범에서 일치하는 피크 찾기</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/b1e8618db746cd49ea4962d3dc73031387b975dd-1600x1166.png" style="width: 100%;" /><h2>결론</h2><p><strong><em>아마 </em></strong>다블람<em> 사진만</em> <em>볼 수 있나요?</em> 를 작고 작동하는 <strong>멀티모달 검색</strong> 시스템으로 전환했습니다. 우리는 원시 트레킹 사진을 찍어 <strong>SigLIP-2 임베딩으로</strong> 변환하고, <strong>Elasticsearch를</strong> 사용해 벡터를 통한 빠른 <strong>kNN과</strong> 간단한 지리적/시간 필터를 통해 <em>의미별로</em> 적합한 이미지를 표시했습니다. 그 과정에서 저희는 혼합된 프로토타입의 작은 <code>peaks_catalog</code> 인덱스(식별용)와 이미지 벡터 및 EXIF의 확장 가능한 <code>photos</code> 인덱스(검색용) 등 두 가지 인덱스를 사용하여 문제를 분리했습니다. 실용적이고 재현 가능하며 쉽게 확장할 수 있습니다.</p><p>튜닝을 원한다면 몇 가지 설정으로 조정할 수 있습니다:</p><ul><li><strong>쿼리 시간 설정:</strong> <code>k</code> (반환할 이웃 수) 및 <code>num_candidates</code> (최종 점수 산출 전 검색 범위). 이러한 설정은 <a href="https://www.elastic.co/search-labs/blog/elasticsearch-knn-and-num-candidates-strategies">여기</a> 블로그에서 설명합니다.</li><li><strong>인덱스 시간 설정:</strong> <code>m</code> (그래프 연결성) 및 <code>ef_construction</code> (빌드 시간 정확도 대 메모리). 쿼리의 경우 <code>ef_search</code> 을 너무 높게 설정하면 일반적으로 약간의 지연 시간 절충을 통해 더 나은 리콜을 얻을 수 있습니다. 이러한 설정에 대한 자세한 내용은 <a href="https://www.elastic.co/search-labs/blog/hnsw-graph">이 블로그를</a> 참조하세요.</li></ul><p>앞으로 <strong>멀티모달</strong> 및 <strong>다국어</strong> 검색을 위한 기본 모델/랭커가 곧 Elastic 생태계에 출시될 예정이므로 이미지/텍스트 검색과 하이브리드 랭킹이 더욱 강력해질 것입니다.<a href="https://ir.elastic.co/news/news-details/2025/Elastic-Completes-Acquisition-of-Jina-AI-a-Leader-in-Frontier-Models-for-Multimodal-and-Multilingual-Search/default.aspx?utm_source=chatgpt.com"> ir.elastic.co+1</a></p><p>직접 체험해보고 싶으신가요?</p><ul><li><strong>GitHub 리포지토리:</strong> <a href="https://github.com/navneet83/multimodal-mountain-peak-search"><em>https://github.com/navneet83/multimodal-mountain-peak-search</em></a></li><li><strong>Colab 빠른 시작:</strong> <a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/notebooks/multimodal_mountain_peak_search.ipynb">https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/notebooks/multimodal_mountain_peak_search.ipynb</a></li></ul><p>이것으로 우리의 여정은 끝났고 이제 돌아올 시간입니다. 도움이 되었기를 바라며, 이 기능을 중단(또는 개선)하신다면 어떤 점이 달라졌는지 알려주시기 바랍니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/d324d1e1472f1bfbd8f25747f57bdeeb9c7f16b2-1600x1200.png" style="width: 100%;" />]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[하이브리드 검색 재랭킹을 통한 다국어 임베딩 모델 관련성 향상]]></title>
            <description><![CDATA[Elasticsearch에서 Cohere의 재랭커와 하이브리드 검색을 사용해 E5 다국어 임베딩 모델 검색 결과의 정확도를 개선하는 방법을 알아보세요.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/multilingual-embedding-model-hybrid-search-reranking</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/multilingual-embedding-model-hybrid-search-reranking</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Quynh Nguyen]]></dc:creator>
            <pubDate>Mon, 03 Nov 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/d341b04acecc8eeec321f5404e1643447ecc8526-720x420.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<h2>소개</h2><p><a href="https://www.elastic.co/search-labs/blog/multilingual-embedding-model-deployment-elasticsearch">이 시리즈의 마지막 파트에서는</a> Elastic의 사전 학습된 E5 모델(그리고 Hugging Face의 다른 다국어 텍스트 임베딩 모델)을 배포하는 과정을 살펴보고, Elasticsearch와 Kibana를 사용해 텍스트 데이터에서 고밀도 벡터 임베딩을 생성하는 방법에 대해 알아보았습니다. 이 블로그에서는 이러한 임베딩의 결과를 살펴보고 다국어 모델을 활용할 때 얻을 수 있는 중요한 이점을 강조합니다.</p><p>이제 색인 <code>coco_multilingual</code> 을 만들었으므로 검색을 수행하면 참조할 수 있도록 &#x27;en&#x27; 필드가 있는 여러 언어로 된 문서가 표시됩니다:</p><pre><code># GET coco_multilingual/_search
    {
       "_index": "coco_multilingual",
       "_id": "WAiXQJYBgf6odR9bLohZ",
       "_score": 1,
       "_source": {
         "description": "Ein Parkmeßgerät auf einer Straße mit Autos",
         "en": "A row of parked cars sitting next to parking meters.",
         "language": "de",
         "vector_description": {...}
       }
     },
     . . .</code></pre><h2>영어로 검색 수행하기</h2><p>영어로 검색을 수행해보고 얼마나 잘 검색되는지 확인해 보겠습니다:</p><pre><code>GET coco_multi/_search
{
"size": 10,
"_source": [
  "description", "language", "en"
],
"knn": {
  "field": "vector_description.predicted_value",
  "k": 10,
  "num_candidates": 100,
  "query_vector_builder": {
    "text_embedding": {
      "model_id": ".multilingual-e5-small_linux-x86_64_search",
      "model_text": "query: kitty"
    }
  }
}
}</code></pre><pre><code>{
       "_index": "coco_multi",
       "_id": "JQiXQJYBgf6odR9b6Yz0",
       "_score": 0.9334303,
       "_source": {
         "description": "Eine Katze, die in einem kleinen, gepackten Koffer sitzt.",
         "en": "A brown and white cat is in a suitcase.",
         "language": "de"
       }
     },
      {
       "_index": "coco_multi",
       "_id": "3AiXQJYBgf6odR9bFod6",
       "_score": 0.9281012,
       "_source": {
         "description": "Una bambina che tiene un gattino vicino a una recinzione blu.",
         "en": "A little girl holding a kitten next to a blue fence.",
         "language": "it"
       }
     },
     . . .</code></pre><p>이 쿼리는 놀라울 정도로 단순해 보이지만, 내부적으로는 모든 언어의 모든 문서에서 &#x27;kitty&#x27;라는 단어가 포함된 숫자를 검색하고 있습니다. 그리고 벡터 검색을 수행하기 때문에 &#x27;kitty&#x27;와 관련이 있을 수 있는 모든 단어를 의미론적으로 검색할 수 있습니다: &quot;고양이&quot;, &quot;새끼 고양이&quot;, &quot;고양이&quot;, &quot;가토&quot;(이탈리아어), &quot;메오&quot;(베트남어), 고양이(한국어), 猫(중국어) 등이 있습니다. 그 결과, 검색어가 영어로 되어 있어도 다른 모든 언어로 된 콘텐츠도 검색할 수 있습니다. 예를 들어, 고양이(<code>ying on something</code> )를 검색하면 이탈리아어, 네덜란드어 또는 베트남어로 된 문서도 표시됩니다. 효율성에 대해 이야기해 보세요!</p><h2>다른 언어로 된 콘텐츠 검색 수행하기</h2><pre><code>GET coco_multi/_search
{  
 "size": 100,
 "_source": [
   "description", "language", "en"
 ],
 "knn": {
   "field": "vector_description.predicted_value",
   "k": 50,
   "num_candidates": 1000,
   "query_vector_builder": {
     "text_embedding": {
       "model_id": ".multilingual-e5-small_linux-x86_64_search",
       "model_text": "query: kitty lying on something"
     }
   }
 }
}</code></pre><pre><code>{
 "description": "A black kitten lays on her side beside remote controls.",
 "en": "A black kitten lays on her side beside remote controls.",
 "language": "en"
},
{
 "description": "un gattino sdraiato su un letto accanto ad alcuni telefoni ",
 "en": "A black kitten lays on her side beside remote controls.",
 "language": "it"
},
{
 "description": "eine Katze legt sich auf ein ausgestopftes Tier",
 "en": "a cat lays down on a stuffed animal",
 "language": "de"
},
{
 "description": "Một chú mèo con màu đen nằm nghiêng bên cạnh điều khiển từ xa.",
 "en": "A black kitten lays on her side beside remote controls.",
 "language": "vi"
}
. . .</code></pre><p>마찬가지로 한국어로 &#x27;고양이&#x27;로 키워드 검색을 수행하면 의미 있는 결과를 얻을 수 있습니다. 여기서 놀라운 점은 이 색인에는 한국어로 된 문서가 하나도 없다는 것입니다!</p><pre><code>GET coco_multi/_search
{
 "size": 100,
 "_source": [
   "description", "language", "en"
 ],
 "knn": {
   "field": "vector_description.predicted_value",
   "k": 50,
   "num_candidates": 1000,
   "query_vector_builder": {
     "text_embedding": {
       "model_id": ".multilingual-e5-small_linux-x86_64_search",
       "model_text": "query: 고양이"
     }
   }
 }
}</code></pre><pre><code> {
       {
         "description": "eine Katze legt sich auf ein ausgestopftes Tier",
         "en": "a cat lays down on a stuffed animal",
         "language": "de"
       }
     },
     {
       {
         "description": "Một con chó và con mèo đang ngủ với nhau trên một chiếc ghế dài màu cam.",
         "en": "A dog and cat lying  together on an orange couch. ",
         "language": "vi"
       }
     },</code></pre><p>임베딩 모델은 공유 의미 공간에서 의미를 나타내므로 색인된 캡션과 다른 언어로 쿼리해도 관련 이미지를 검색할 수 있습니다.</p><h2>하이브리드 검색 및 재랭킹으로 관련성 높은 검색 결과 얻기</h2><p>예상대로 관련 결과가 나타나서 기쁘게 생각합니다. 하지만 이커머스나 가장 적합한 상위 5~10개의 결과로 범위를 좁혀야 하는 RAG 애플리케이션과 같은 실제 환경에서는 재랭크 모델을 사용하여 가장 관련성이 높은 결과의 우선 순위를 지정할 수 있습니다.</p><p>여기서 베트남어로 &quot;고양이는 무슨 색인가요?&quot;라고 묻는 쿼리를 수행하면 많은 결과가 나오지만 상위 1, 2위가 가장 관련성이 높지 않을 수 있습니다.</p><pre><code>GET coco_multi/_search
{
 "size": 20,
 "_source": [
   "description",
   "language",
   "en"
 ],
 "knn": {
   "field": "vector_description.predicted_value",
   "k": 20,
   "num_candidates": 1000,
   "query_vector_builder": {
     "text_embedding": {
       "model_id": ".multilingual-e5-small_linux-x86_64_search",
       "model_text": "query: con mèo màu gì?"
     }
   }
 }
}</code></pre><p>결과에는 모두 고양이 또는 어떤 형태의 색상이 언급되어 있습니다:</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/33e1e887dbbdd1066cfedc7375f5e3b46538529e-859x847.png" style="width: 100%;" /><p>이제 개선해 봅시다! <a href="https://cohere.com/blog/rerank-3pt5">Cohere의</a>다국어 재랭크 모델을 통합하여 질문에 해당하는 추론을 개선해 보겠습니다.</p><pre><code>PUT _inference/rerank/cohere_rerank
{
 "service": "cohere",
 "service_settings": {
   "api_key": "your_api_key",
   "model_id": "rerank-v3.5"
 },
 "task_settings": {
   "top_n": 10,
   "return_documents": true
 }
}


GET coco_multi/_search
{
"size": 10,
"_source": [
  "description",
  "language",
  "en"
],
"retriever": {
  "text_similarity_reranker": {
    "retriever": {
      "rrf": {
        "retrievers": [
          {
            "knn": {
              "field": "vector_description.predicted_value",
              "k": 50,
              "num_candidates": 100,
              "query_vector_builder": {
                "text_embedding": {
                  "model_id": ".multilingual-e5-small_linux-x86_64_search",
                  "model_text": "query: con mèo màu gì?" // English: What color is the cat?
                }
              }
            }
          }
        ],
        "rank_window_size": 100,
        "rank_constant": 0
      }
    },
    "field": "description",
    "inference_id": "cohere_rerank",
    "inference_text": "con mèo màu gì?"
  }
}
}</code></pre><pre><code> {
       "_index": "coco_multi",
       "_id": "rQiYQJYBgf6odR9bBYyH",
       "_score": 1.5501487,
       "_source": {
         "description": "Hai cái điện thoại được đặt trên một cái chăn cạnh một con mèo con màu đen.",
         "en": "A black kitten lays on her side beside remote controls.",
         "language": "vi"
       }
     },
     {
       "_index": "coco_multi",
       "_id": "swiXQJYBgf6odR9b04uf",
       "_score": 1.5427427,
       "_source": {
         "description": "Một con mèo sọc nâu nhìn vào máy quay.", // Real translation: A brown striped cat looks at the camera 
         "en": "This cat is sitting on a porch near a tire.",
         "language": "vi"
       }
     },</code></pre><p>이제 최고의 결과를 통해 저희 애플리케이션은 새끼 고양이의 색이 검은색 또는 줄무늬가 있는 갈색이라고 자신 있게 대답할 수 있습니다. 여기서 더욱 흥미로운 점은 벡터 검색이 실제로 원본 데이터 세트의 영어 캡션에서 누락된 부분을 찾아냈다는 점입니다. 참조 영어 번역에서 갈색 줄무늬 고양이를 놓쳤음에도 불구하고 이를 찾아낼 수 있습니다. 이것이 바로 벡터 검색의 힘입니다.</p><h2>결론</h2><p>이 블로그에서는 다국어 임베딩 모델의 유용성과 Elasticsearch를 활용하여 모델을 통합하여 임베딩을 생성하고 하이브리드 검색 및 재랭커로 관련성과 정확도를 효과적으로 개선하는 방법을 살펴봤습니다. 원하는 언어와 데이터 세트에 <a href="https://www.elastic.co/docs/explore-analyze/machine-learning/nlp/ml-nlp-e5">대해 즉시 사용 가능한 E5 모델을 사용하여 자체 클라우드</a> <a href="https://cloud.elastic.co/registration?onboarding_token=vectorsearch&amp;cta=cloud-registration&amp;tech=trial&amp;plcmt=article%20content&amp;pg=search-labs">클러스터를</a> 생성하여 다국어 의미론적 검색을 사용해 볼 수 있습니다.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Elasticsearch에서 다국어 임베딩 모델 배포하기]]></title>
            <description><![CDATA[Elasticsearch에서 벡터 검색 및 언어 간 검색을 위한 e5 다국어 임베딩 모델을 배포하는 방법을 알아보세요.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/multilingual-embedding-model-deployment-elasticsearch</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/multilingual-embedding-model-deployment-elasticsearch</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Quynh Nguyen]]></dc:creator>
            <pubDate>Wed, 22 Oct 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/8f2aa7bebb6b2f701e274ba7282273f9ab4abed6-720x432.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<h2>소개</h2><p>전 세계 사용자가 있는 세계에서는 다국어 정보 검색(CLIR)이 매우 중요합니다. CLIR은 검색을 단일 언어로 제한하는 대신 <em>모든</em> 언어로 정보를 찾을 수 있도록 하여 사용자 경험을 개선하고 운영을 간소화합니다. 이커머스 고객이 자신의 언어로 상품을 검색하면 데이터를 미리 현지화할 필요 없이 적절한 결과가 표시되는 글로벌 시장을 상상해 보세요. 또는 학술 연구자들이 뉘앙스와 복잡성이 있는 논문을 모국어로 검색할 수 있으며, 출처가 다른 언어로 되어 있어도 검색할 수 있습니다.</p><p>다국어 텍스트 임베딩 모델을 사용하면 바로 그렇게 할 수 있습니다. 임베딩은 텍스트의 의미를 숫자 벡터로 표현하는 방법입니다. 이 벡터는 비슷한 의미를 가진 텍스트가 고차원 공간에서 서로 가깝게 위치하도록 설계되었습니다. 특히 다국어 텍스트 임베딩 모델은 여러 언어에서 동일한 의미를 가진 단어와 구문을 유사한 벡터 공간에 매핑하도록 설계되었습니다.</p><p>오픈 소스 다국어 E5와 같은 모델은 대개 대조 학습과 같은 기술을 사용하여 방대한 양의 텍스트 데이터를 학습합니다. 이 접근 방식에서 모델은 유사한 의미를 가진 텍스트 쌍(양의 쌍)과 서로 다른 의미를 가진 텍스트 쌍(음의 쌍)을 구별하는 방법을 학습합니다. 모델은 양성 쌍 간의 유사도는 최대화하고 음성 쌍 간의 유사도는 최소화하도록 생성하는 벡터를 조정하도록 학습됩니다. 다국어 모델의 경우 이 학습 데이터에는 서로 다른 언어로 된 텍스트 쌍이 포함되어 있어 모델이 여러 언어에 대한 공유 표현 공간을 학습할 수 있습니다. 이렇게 생성된 임베딩은 쿼리의 언어에 관계없이 텍스트 임베딩 간의 유사성을 사용하여 관련 문서를 찾는 교차 언어 검색을 비롯한 다양한 NLP 작업에 사용할 수 있습니다.</p><h2>다국어 벡터 검색의 이점</h2><ul><li><strong>뉘앙스</strong>: 벡터 검색은 키워드 매칭을 넘어 의미론적 의미를 포착하는 데 탁월합니다. 이는 언어의 맥락과 미묘한 차이를 이해해야 하는 작업에 매우 중요합니다.</li><li><strong>언어 간 이해</strong>: 쿼리와 문서가 서로 다른 어휘를 사용하는 경우에도 여러 언어에서 효과적으로 정보를 검색할 수 있습니다.</li><li><strong>연관성</strong>: 쿼리와 문서 간의 개념적 유사성에 초점을 맞춰 보다 관련성 높은 결과를 제공합니다.</li></ul><p>예를 들어, 여러 국가에서 소셜 미디어가 정치 담론에 미치는 영향( &quot;)을 연구하는 학계 연구자(&quot; )가 있다고 가정해 보겠습니다. 벡터 검색을 사용하면 &quot;l&#x27;impatto dei social media sul discorso politico&quot; (이탈리아어) 또는 &quot;ảnh hưởng của mạng xã hội đối với diễn ngôn chính trị&quot; (베트남어) 같은 검색어를 입력하면 관련 문서를 영문으로 찾을 수 있습니다, 스페인어 또는 기타 색인된 언어로 된 관련 문서를 찾아보세요. 벡터 검색은 정확한 키워드가 포함된 논문뿐만 아니라 소셜 미디어가 정치에 미치는 영향에 대한 <em>개념을</em> 논의하는 논문도 찾아내기 때문입니다. 이를 통해 연구의 폭과 깊이를 크게 향상시킬 수 있습니다.</p><h2>시작하기</h2><p>기본으로 제공되는 E5 모델을 사용하여 Elasticsearch를 사용하여 CLIR을 설정하는 방법은 다음과 같습니다. 여러 언어로 된 이미지 캡션이 포함된 <a href="https://huggingface.co/datasets/romrawinjp/multilingual-coco">오픈 소스 다국어 COCO 데이터셋을</a> 사용하여 두 가지 유형의 검색을 시각화해 보겠습니다:</p><ol><li>하나의 영어 데이터 세트에서 다른 언어로 된 쿼리 및 검색어, 그리고</li><li>여러 언어로 된 문서가 포함된 데이터 집합을 기반으로 여러 언어로 쿼리할 수 있습니다.</li></ol><p>그런 다음 하이브리드 검색과 재랭크의 힘을 활용하여 검색 결과를 더욱 개선할 것입니다.</p><h2>필수 구성 요소</h2><ul><li>Python 3.6+</li><li>Elasticsearch 8+</li><li>Elasticsearch Python 클라이언트: pip 설치 elasticsearch</li></ul><h2>데이터 세트</h2><p><a href="https://huggingface.co/datasets/romrawinjp/multilingual-coco">COCO 데이터 세트</a> 는 대규모 캡션 데이터 세트입니다. 데이터 세트의 각 이미지는 여러 언어로 캡션되어 있으며, 언어별로 여러 번역본을 사용할 수 있습니다. 데모용으로 각 번역을 개별 문서로 색인화하여 참조할 수 있도록 가장 먼저 제공되는 영어 번역과 함께 보여드리겠습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/d4f0632529c71a22fbdecf21c9f4f0bb64b8e69c-1600x567.png" style="width: 100%;" /><h3>1단계: 다국어 COCO 데이터 세트 다운로드</h3><p>블로그를 단순화하고 쉽게 따라갈 수 있도록 여기서는 간단한 API 호출을 통해 restval의 처음 100개 행을 로컬 JSON 파일에 로드합니다. 또는 허깅페이스의 라이브러리 데이터셋을 사용하여 전체 데이터셋 또는 데이터셋의 하위 집합을 로드할 수도 있습니다.</p><pre><code>import requests
import json
import os
### Download multilingual coco dataset into a json file (for easy viewing)
### Here we are retrieving first 100 rows for this example
### Alternatively, you can use `datasets` library from Hugging Face
url = "https://datasets-server.huggingface.co/rows?dataset=romrawinjp%2Fmultilingual-coco&config=default&split=restval&offset=0&length=100"
response = requests.get(url)


if response.status_code == 200:
   data = response.json()
   output_file = "multilingual_coco_sample.json" 
   ### Loading the downloaded content into a json file locally
   with open(output_file, "w", encoding="utf-8") as f:
       json.dump(data, f, indent=4, ensure_ascii=False)
   print(f"Data successfully downloaded and saved to {output_file}")
else:
   print(f"Failed to download data: {response.status_code}")
   print(response.text)</code></pre><p>데이터가 JSON 파일에 성공적으로 로드되면 다음과 비슷한 내용이 표시됩니다:</p><p><code>Data successfully downloaded and saved to multilingual_coco_sample.json</code></p><h3>2단계: (Elasticsearch를 시작하고) Elasticsearch에서 데이터 색인하기</h3><p>a) 로컬 Elasticsearch 서버를 시작합니다.</p><p>b) Elasticsearch 클라이언트를 시작합니다.</p><pre><code>from elasticsearch import Elasticsearch
from getpass import getpass


# Initialize Elasticsearch client
es = Elasticsearch(getpass("Host: "), api_key=getpass("API Key: "))


index_name = "coco"


# Create the index if it doesn't exist
if not es.indices.exists(index=index_name):
   es.indices.create(index=index_name, body=mapping)</code></pre><p>c) 인덱스 데이터</p><pre><code># Load the JSON data
with open('./multilingual_coco_sample.json', 'r') as f:
   data = json.load(f)


rows = data["rows"]
# List of languages to process
languages = ["en", "es", "de", "it", "vi", "th"]


# For each image, we will process each individual caption as its own document
bulk_data = []
for data in rows:
   row = data["row"]
   image = row.get("image")
   image_url = image["src"]


   # Process each language
   for lang in languages:
       # Skip if language not present in this row
       if lang not in row:
           continue


       # Get all descriptions for this language
 # along with first available English caption for reference
       descriptions = row[lang]
       first_eng_caption = row["en"][0]


       # Prepare bulk indexing data
       for description in descriptions:
           if description == "":
               continue
           # Add index operation
           bulk_data.append(
               {"index": {"_index": index_name}}
           )
           # Add document
           bulk_data.append({
               "language": lang,
               "description": description,
               "en": first_eng_caption,
               "image_url": image_url,
           })


# Perform bulk indexing
if bulk_data:
   try:
       response = es.bulk(operations=bulk_data)
       if response["errors"]:
           print("Some documents failed to index")
       else:
           print(f"Successfully bulk indexed {len(bulk_data)} documents")
   except Exception as e:
       print(f"Error during bulk indexing: {str(e)}")


print("Indexing complete!")</code></pre><p>데이터가 색인되면 다음과 비슷한 내용이 표시됩니다:</p><p><code>Successfully bulk indexed 4840 documents</code></p><p><code>Indexing complete!</code></p><h3>3단계: E5 학습된 모델 배포하기</h3><p>Kibana에서 스택 관리 &gt; <strong>학습된 모델</strong> 페이지로 이동하고 .multilingual-e5-small_linux-x86_64에 대한 <strong>배포를</strong> 클릭합니다. 옵션을 선택합니다. 이 E5 모델은 Linux-x86_64에 최적화된 소규모 다국어 버전으로, 즉시 사용할 수 있습니다. &#x27;배포&#x27;를 클릭하면 배포 설정 또는 vCPU 구성을 조정할 수 있는 화면이 표시됩니다. 간단하게 하기 위해 사용량에 따라 배포를 자동으로 확장하는 적응형 리소스를 선택한 기본 옵션을 사용하겠습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/95cd8f352425d1db2d04b00c3c88d1e71d1ef19a-1600x440.png" style="width: 100%;" /><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/1599d99949dda8267acc58f400a403a3af5373ef-1600x655.png" style="width: 100%;" /><p>선택적으로 다른 텍스트 임베딩 모델을 사용하려는 경우 사용할 수 있습니다. 예를 들어, BGE-M3를 사용하려면 <a href="https://www.elastic.co/docs/reference/elasticsearch/clients/eland/machine-learning#ml-nlp-pytorch">Elastic의 Eland Python 클라이언트를</a> 사용하여 HuggingFace에서 모델을 가져올 수 있습니다.</p><pre><code>export MODEL_ID="bge-m3"
export HUB_MODEL_ID="BAAI/bge-m3"
export CLOUD_ID={{CLOUD_ID}}
export ES_API_KEY={{API_KEY}}
docker run -it --rm docker.elastic.co/eland/eland \
eland_import_hub_model --cloud-id $CLOUD_ID --es-api-key $ES_API_KEY --hub-model-id $HUB_MODEL_ID --es-model-id $MODEL_ID --task-type text_embedding --start</code></pre><p>그런 다음 학습된 모델 페이지로 이동하여 가져온 모델을 원하는 구성으로 배포합니다.</p><h3>4단계: 배포된 모델을 사용하여 원본 데이터에 대한 임베딩을 벡터화하거나 생성합니다.</h3><p>임베딩을 생성하려면 먼저 텍스트를 가져와 추론 텍스트 임베딩 모델을 통해 실행할 수 있는 수집 파이프라인을 만들어야 합니다. 이 작업은 Kibana의 사용자 인터페이스 또는 Elasticsearch의 API를 통해 수행할 수 있습니다.</p><p><strong>Kibana 인터페이스를 통해 이 작업을 수행하려면</strong>, 학습된 모델을 배포한 후 <strong>테스트 </strong>버튼을 클릭합니다. 이렇게 하면 생성된 임베딩을 테스트하고 미리 볼 수 있습니다. <code>coco</code><strong> </strong>인덱스에 대한 새 데이터 보기를 만들고, 데이터 보기를 새로 만든 코코 데이터 보기로 설정하고, 필드를 임베딩을 생성할 필드이므로 <code>description</code> 로 설정합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/cfe189e13dc118d325e7fb90bdace0c912e29f51-1088x1600.png" style="width: 100%;" /><p>잘 작동합니다! 이제 수집 파이프라인을 생성하고 원본 문서를 재색인하고 파이프라인을 통과하여 임베딩이 포함된 새 인덱스를 생성할 수 있습니다. <strong>파이프라인 생</strong>성을 클릭하면 임베딩을 만드는 데 필요한 프로세서가 자동으로 채워지는 파이프라인 생성 프로세스를 안내합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/1e043c1c3279b66fbdf19c06b41e76e613043998-1600x1126.png" style="width: 100%;" /><p>마법사는 데이터를 수집하고 처리하는 동안 장애를 처리하는 데 필요한 프로세서를 자동으로 채울 수도 있습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/8804d31b4f869078e3b2245040bbb0ab1720a94a-1600x1084.png" style="width: 100%;" /><p>이제 수집 파이프라인을 만들어 보겠습니다. 파이프라인의 이름을 <code>coco_e5</code> 으로 지정합니다. 파이프라인이 성공적으로 생성되면, 마법사에서 원래 색인된 데이터를 새 색인으로 재색인하여 임베딩을 생성하는 데 즉시 파이프라인을 사용할 수 있습니다. <strong>색인 재생성을 </strong>클릭하여 프로세스를 시작합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/e34b1b18f5b24420d4581fe4d657c569926c2023-1600x1126.png" style="width: 100%;" /><h2>보다 복잡한 구성의 경우, Elasticsearch API를 사용할 수 있습니다.</h2><p>일부 모델의 경우 모델 학습 방식에 따라 임베딩을 생성하기 전에 실제 입력에 특정 텍스트를 미리 추가하거나 추가해야 할 수 있으며, 그렇지 않으면 성능이 저하될 수 있습니다.</p><p>예를 들어, e5의 경우 모델은 입력 텍스트가 &quot;passage: {content of passage}&quot;. 이를 위해 수집 파이프라인을 활용해 보겠습니다: 새로운 수집 파이프라인 <strong>벡터화_descriptions를</strong> 생성하겠습니다. 이 파이프라인에서는 임시 <code>temp_desc</code> 필드를 새로 만들고, &quot;passage: &quot;를 <code>description</code> 텍스트에 추가하고, 모델에서 <code>temp_desc</code> 을 실행하여 텍스트 임베딩을 생성한 다음 <code>temp_desc</code> 을 삭제합니다.</p><pre><code>PUT _ingest/pipeline/vectorize_descriptions
{
"description": "Pipeline to run the descriptions text_field through our inference text embedding model",
"processors": [
 {
   "set": {
     "field": "temp_desc",
     "value": "passage: {{description}}"
   }
 },
 {
   "inference": {     
"field_map": {
       "temp_desc": "text_field"
     },
     "model_id": ".multilingual-e5-small_linux-x86_64_search",
     "target_field": "vector_description"
   }
 },
 {
   "remove": {
     "field": "temp_desc"
   }
 }
]
}</code></pre><p>또한 생성된 벡터에 어떤 <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/dense-vector#dense-vector-quantization">양자화 유형을</a> 사용할지 지정할 수도 있습니다. 기본적으로 Elasticsearch는 <code>int8_hnsw</code> 을 사용하지만 여기서는 각 차원을 단일 비트 정밀도로 축소하는 <a href="https://www.elastic.co/search-labs/blog/better-binary-quantization-lucene-elasticsearch">Better Binary Quantization</a> (또는 <code>bqq_hnsw</code>)을 사용하려고 합니다. 이렇게 하면 메모리 사용량이 96% (또는 32배)로 줄어드는 대신 정확도는 더 높아집니다. 이 정량화 유형을 선택한 이유는 나중에 정확도 손실을 개선하기 위해 리랭커를 사용할 것이라는 것을 알고 있기 때문입니다.</p><p>이를 위해 <strong>coco_multi라는</strong> 새 인덱스를 생성하고 매핑을 지정합니다. 여기서 마법은 <strong>벡터_설명</strong>필드에 있으며, 여기서 인덱스_옵션의유형을 <strong>bbq_hnsw로</strong> 지정합니다.</p><pre><code>PUT coco_multi
{
 "mappings": {
   "properties": {
     "description": {
       "type": "text"
     },
     "en": {
       "type": "text"
     },
     "image_url": {
       "type": "keyword"
     },
     "language": {
       "type": "keyword"
     },
     "vector_description.predicted_value": {
       "type": "dense_vector",
       "dims": 384,
       "index": "true",
       "similarity": "cosine",
       "index_options": {
         "type": "bbq_hnsw" 
       }
     }
   }
 }
}</code></pre><p>이제 설명 필드를 &#x27;벡터화&#x27;하거나 임베딩을 생성하는 수집 파이프라인을 사용하여 원본 문서를 새 인덱스로 재색인할 수 있습니다.</p><pre><code>POST _reindex?wait_for_completion=false
{
 "source": {
   "index": "coco"
 },
 "dest": {
   "index": "coco_multilingual",
   "pipeline": "vectorize_descriptions"
 }
}</code></pre><p>여기까지입니다! 우리는 Elasticsearch와 Kibana로 다국어 모델을 성공적으로 배포했으며, Kibana 사용자 인터페이스 또는 Elasticsearch API를 통해 Elastic으로 데이터로 벡터 임베딩을 생성하는 방법을 단계별로 배웠습니다. 이 시리즈의 두 번째 파트에서는 다국어 모델 사용의 결과와 뉘앙스에 대해 살펴봅니다. 그 동안 자체 <a href="https://cloud.elastic.co/registration?onboarding_token=vectorsearch&amp;cta=cloud-registration&amp;tech=trial&amp;plcmt=article%20content&amp;pg=search-labs">클라우드 클러스터를 생성하여</a> 원하는 언어와 데이터 세트에서 <a href="https://www.elastic.co/docs/explore-analyze/machine-learning/nlp/ml-nlp-e5">즉시 사용 가능한 E5 모델을 사용하여 다국어 시맨틱 검색을</a> 사용해 볼 수 있습니다.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Elastic MCP 서버: 모든 AI 에이전트에 에이전트 빌더 도구 노출]]></title>
            <description><![CDATA[에이전트 빌더에 기본 제공되는 Elastic MCP 서버를 사용하여 개인 데이터 및 사용자 정의 도구에 대한 액세스를 통해 모든 AI 에이전트를 안전하게 확장하는 방법을 알아보세요.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/elastic-mcp-server-agent-builder-tools</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/elastic-mcp-server-agent-builder-tools</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Jedr Blaszyk, Joe McElroy]]></dc:creator>
            <pubDate>Mon, 20 Oct 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/ef5153551a1d14833c7f512fede554d1dfb31553-720x420.jpg?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>Elastic 에이전트 빌더는 Elasticsearch의 자체 데이터와 긴밀하게 통합되는 도구와 에이전트를 생성하기 위한 플랫폼입니다. 예를 들어, 내부 문서에 대해 시맨틱 검색을 수행하거나 통합 가시성 로그를 분석하거나 보안 경고를 쿼리하는 도구를 만들 수 있습니다.</p><p>하지만 대부분의 시간을 보내는 환경에 이러한 맞춤형 데이터 인식 도구를 도입할 때 진정한 마법이 일어납니다. 코드 편집기 에이전트가 조직의 비공개 지식창고에 안전하게 액세스할 수 있다면 어떨까요?</p><p>이것이 바로 <strong>모델 컨텍스트 프로토콜(MCP)</strong> 이 필요한 이유입니다. Elastic 에이전트 빌더는 플랫폼의 도구에 대한 액세스를 제공하는 기본 제공 MCP 서버와 함께 제공됩니다.</p><h2>Elastic 에이전트 빌더 MCP 서버를 사용하는 이유는 무엇인가요?</h2><p>AI 에이전트는 매우 강력하지만, 일반적으로 학습된 데이터와 공개 인터넷에서 적극적으로 검색할 수 있는 정보로 지식이 제한되어 있습니다. 그들은 회사의 내부 디자인 문서, 팀의 특정 배포 런북 또는 애플리케이션 로그의 고유한 구조에 대해 알지 못합니다.</p><p>문제는 AI 어시스턴트에게 필요한 전문 컨텍스트를 제공하는 것입니다. 이것이 바로 MCP가 해결하고자 하는 문제입니다. <strong>MCP는 AI 모델이나 에이전트가 외부 도구를 검색하고 사용할 수 있도록 하는 개방형 표준입니다.</strong></p><p>이를 가능하게 하기 위해 Elastic 에이전트 빌더는 기본적으로 기본 제공 MCP 서버를 통해 사용자 정의 도구를 노출합니다. 즉, Elastic 에이전트 빌더로 구축한 전문화된 데이터 인식 도구로 <strong>Cursor</strong>, <strong>VS Code</strong> 또는 <strong>Claude Desktop과</strong> 같은 모든 MCP 호환 클라이언트를 쉽게 연결할 수 있습니다.</p><h2>MCP를 사용해야 할 때(그리고 사용하지 말아야 할 때)</h2><p>Elastic 에이전트 빌더에는 다양한 통합 패턴을 지원하기 위한 여러 프로토콜이 포함되어 있습니다. 효과적인 AI 워크플로우를 구축하려면 올바른 것을 선택하는 것이 중요합니다.</p><ul><li><strong><a href="https://www.elastic.co/docs/solutions/search/agent-builder/mcp-server">MCP를 </a>사용하여</strong> 전문화된 도구로 AI 에이전트(예:<strong>커서</strong> 또는 <strong>VS 코드)를 보강할 수 있습니다.</strong> 개인 데이터에 대한 안전한 액세스를 통해 이미 사용 중인 어시스턴트를 개선하는 &quot;가져오기(&quot; ) 방식입니다. 도구만 MCP 서버를 통해 노출되며, Elastic의 에이전트는 이와는 별개입니다.</li><li><strong><a href="https://www.elastic.co/docs/solutions/search/agent-builder/a2a-server">A2A 프로토콜을 </a>사용하여</strong> 완전한 사용자 정의 Elastic 에이전트가 다른 자율 에이전트와 협업할 수 있도록 하세요(<a href="https://www.elastic.co/search-labs/blog/a2a-protocol-elastic-agent-builder-gemini-enterprise"><strong>Google의 Gemini Enterprise에서처럼).</strong></a> 상담원 간 위임은 각 상담원이 동료로서 문제를 해결하는 데 사용됩니다.</li><li><strong>처음부터 </strong>사용자 지정 애플리케이션을 구축할 때<a href="https://www.elastic.co/docs/solutions/search/agent-builder/kibana-api"><strong>에이전트 빌더 API를 사용하여 완전한 프로그래밍 방식으로 제어할 수 있습니다.</strong></a></li></ul><p>IDE를 벗어나지 않고도 내부 문서에서 답을 얻고자 하는 개발자에게 MCP는 완벽한 솔루션입니다.</p><h2>예: 에이전트 빌더 MCP 서버를 사용하는 커서의 사용자 지정 도구</h2><p>제가 매일 사용하는 실제 사례를 살펴보겠습니다. 먼저, 내부 엔지니어링 문서를 크롤링하여 <code>elastic-dev-docs</code> 이라는 Elasticsearch 인덱스로 색인화했습니다. 상담원 빌더에서 제공되는 일반적인 기본 제공 툴을 사용할 수도 있지만, 이 특정 지식창고를 쿼리하기 위한 자체 사용자 지정 툴을 만들겠습니다.</p><p>사용자 지정 도구를 구축하는 이유는 간단합니다: <strong>제어와 정밀도입니다</strong>. 이 접근 방식을 사용하면 <code>elastic-dev-docs</code> 인덱스에 대해 직접 빠른 시맨틱 쿼리를 실행할 수 있습니다. 당사는 정확히 어떤 인덱스를 대상으로 하고 데이터를 검색하는 방법을 완전히 제어할 수 있습니다.</p><p>이제 커서와 같은 AI 기반 코드 편집기에서 이 사용자 지정 지식창고를 사용하는 방법은 다음과 같습니다.</p><h3>1단계: 상담원 빌더에서 사용자 지정 지식창고 툴 만들기</h3><p>먼저 상담원 빌더에서 새 도구를 만듭니다. 명확하고 구체적인 도구 설명은 내부 Elastic 에이전트든 MCP를 통해 연결되는 Cursor와 같은 외부 도구든 모든 AI 에이전트가 올바른 작업을 위해 도구를 검색하고 선택하는 방식이기 때문에 중요합니다.</p><p>강력한 설명은 명시적이어야 합니다. 예를 들어 &quot;내부 엔지니어링 문서, 런북, 릴리즈 절차를 찾기 위해 elastic-dev-docs 인덱스에서 시맨틱 검색을 수행합니다.&quot;</p><p>이를 통해 특정 인덱스에 대해 시맨틱 검색을 수행하도록 도구를 구성했습니다. 저장하면 즉시 사용할 수 있습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/1eea079908fdf7cc72dbe81abd07ff51601a43d4-1472x1600.png" style="width: 100%;" /><p>외부에 연결하기 전에 UI에서 직접 테스트해 볼 수 있습니다. <strong>테스트</strong> 버튼을 클릭하여 매개변수를 수동으로 입력하여 LLM이 수행하는 작업을 에뮬레이션하고 결과를 검사하여 모든 것이 올바르게 작동하는지 확인하기만 하면 됩니다.</p><h3>2단계: 커서를 Elastic MCP 서버에 연결하기</h3><p>Elastic 에이전트 빌더는 안전한 MCP 엔드포인트를 통해 사용 가능한 모든 도구를 자동으로 노출합니다. Kibana 내의 도구 UI에서 고유한 서버 URL을 찾을 수 있습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/ba137be30f0eaa7f028b96bd8af4e2779c3f8a33-1600x589.png" style="width: 100%;" /><p>Cursor에 연결하려면 이 URL을 구성 파일에 인증용 Elastic API 키와 함께 추가하기만 하면<a href="https://www.elastic.co/docs/deploy-manage/api-keys/elasticsearch-api-keys">됩니다(ES API 키 생성 방법 알아보기)</a>. 모든 접근 제어 규칙을 준수하면서 사용자가 부여한 권한으로만 도구가 실행되도록 보장하기 때문에 권한 부여를 위해 API 키를 사용합니다.</p><p>Cursor의 <code>~/.cursor/mcp.json</code> 의 MCP 구성은 다음과 같습니다:</p><pre><code>{
  "mcpServers": {
    "elastic-agent-builder": {
      "command": "npx",
      "args": [
        "mcp-remote",
        "https://your-kibana.kb.company.io/api/agent_builder/mcp",
        "--header",
        "Authorization:${AUTH_HEADER}"
      ],
      "env": {
        "AUTH_HEADER": "ApiKey <ELASTIC_API_KEY>"
      }
    }
  }
}</code></pre><p>구성이 저장되면, Cursor에 Elastic 에이전트 빌더 MCP 서버 도구가 표시됩니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/d302c6d3609fbf14fd40e21b9e69e567bf12553f-1600x1002.png" style="width: 100%;" /><h3>3단계: 물어보세요!</h3><p>연결이 설정되면 이제 커서 상담원이 사용자 지정 도구를 호출하여 질문에 답하거나 코드 생성 프로세스를 안내할 수 있습니다.</p><p>구체적인 질문을 해보겠습니다:</p><p><em>&quot;탄력적 검색 조직의 엔지니어링 내부 문서에서 크롤러 서비스를 릴리즈하기 위한 조회 단계&quot;</em></p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/14f572730203c23615bb9dd38234bcb3b0f81155-1600x1468.png" style="width: 100%;" /><p>무대 뒤에서는 마법이 일어납니다:</p><ol><li>커서 상담원은 질문에 가장 적합한 방식으로 답변하는 방법을 결정한 후 <code>engineering_documentation_internal_search</code></li><li>자연어 쿼리로 도구를 호출합니다.</li><li>이 도구는 <code>elastic-dev-docs</code> 인덱스에 대해 시맨틱 검색을 실행하여 가장 관련성이 높은 최신 절차를 반환합니다.</li></ol><p>코드 편집기를 벗어나지 않고도 내부 문서를 기반으로 정확하고 신뢰할 수 있는 답변을 얻을 수 있습니다. 원활하고 강력한 경험을 제공합니다.</p><h2>빌드할 차례</h2><p>이제 Elastic 에이전트 빌더의 기본 제공 MCP 서버를 사용하여 개인 데이터에 대한 안전한 액세스로 AI 어시스턴트를 확장하는 방법을 살펴보셨습니다. 모델을 진정으로 유용하게 활용하려면 자신의 정보에 근거를 두는 것이 중요합니다.</p><p>핵심 단계를 다시 한 번 정리해 보았습니다:</p><ul><li>필요에 맞는 프로토콜 선택하기(MCP).</li><li>사용자 지정 지식창고 툴 구축하기</li><li>해당 도구를 커서와 같은 IDE 보조 도구에 연결합니다.</li></ul><p>더 이상 상담원과 툴이 가장 중요한 컨텍스트에서 분리될 필요가 없습니다. 이 가이드가 보다 효과적이고 데이터를 인식하는 워크플로를 만드는 데 도움이 되길 바랍니다. 행복한 건물!</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[사용자 행동 데이터에 기반한 판단 목록으로 Elasticsearch에서 LTR 모델 학습하기]]></title>
            <description><![CDATA[UBI 데이터를 사용해 판단 목록을 생성하여 Elasticsearch에서 학습 순위 지정(LTR) 모델의 학습을 자동화하는 방법을 알아보세요.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/training-learning-to-rank-models-elasticsearch-ubi-data</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/training-learning-to-rank-models-elasticsearch-ubi-data</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Alexander Dávila]]></dc:creator>
            <pubDate>Wed, 15 Oct 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/762bf09c28829d626d42c2cfadc719e1dd618d1b-1536x1024.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p><a href="https://www.elastic.co/docs/solutions/search/ranking/learning-to-rank-ltr"><strong><em>학습형 랭킹</em></strong></a> 모델을 사용할 때 가장 큰 과제는 모델을 학습시킬 고품질의 <a href="https://www.elastic.co/search-labs/blog/judgment-lists"><strong><em>판단 목록을</em></strong></a> 만드는 것입니다. 전통적으로 이 프로세스에는 쿼리-문서 관련성을 <strong><em>수동으로</em></strong> 평가하여 각 문서에 등급을 부여하는 작업이 포함됩니다. 이는 확장성이 떨어지고 유지 관리가 어려운 느린 프로세스입니다(수백 개의 항목이 있는 목록을 수작업으로 업데이트해야 한다고 상상해 보세요).</p><p>이제 이 학습 데이터를 생성하기 위해 검색 애플리케이션과의 실제 사용자 상호 작용을 사용할 수 있다면 어떨까요? <a href="https://www.elastic.co/search-labs/blog/elasticsearch-plugin-user-behavior-insights"><strong><em>UBI</em></strong></a> 데이터를 사용하면 바로 그렇게 할 수 있습니다. 검색, 클릭 및 기타 상호 작용을 캡처하고 사용하여 판단 목록을 생성할 수 있는 자동 시스템을 구축합니다. 이 프로세스는 수동 상호 작용보다 훨씬 쉽게 확장하고 반복할 수 있으며 더 나은 결과를 얻을 수 있습니다. 이 블로그에서는 Elasticsearch에 저장된 UBI 데이터를 쿼리하여 의미 있는 신호를 계산하여 <a href="https://www.elastic.co/search-labs/blog/elasticsearch-learning-to-rank-introduction"><strong><em>LTR</em></strong></a> 모델을 위한 학습 데이터 세트를 생성하는 방법을 살펴보겠습니다.</p><p><strong><em>전체 실험은 <a href="https://github.com/Alex1795/elastic-ltr-judgement_list-blog.git">여기에서</a></em></strong>확인할 수 있습니다<strong><em>.</em></strong></p><h2>UBI 데이터가 LTR 모델 학습에 유용한 이유</h2><p>UBI 데이터는 수동 주석에 비해 몇 가지 장점이 있습니다:</p><ul><li><strong>볼륨:</strong> UBI 데이터는 실제 상호 작용에서 생성되므로 수동으로 생성하는 것보다 훨씬 더 많은 데이터를 수집할 수 있습니다. 물론 이 데이터를 생성할 수 있는 충분한 트래픽이 있다는 전제입니다.</li><li><strong>실제 사용자 의도:</strong> 일반적으로 수동 판단 목록은 사용 가능한 데이터에 대한 전문가의 평가에서 비롯됩니다. 반면에 UBI 데이터는 실제 사용자 행동을 반영합니다. 즉, 관련성이 있어야 한다는 이론적 가정이 아니라 사용자가 실제로 콘텐츠와 상호작용하고 가치를 찾는 방식에 기반하기 때문에 검색 시스템의 정확도를 향상시킬 수 있는 더 나은 학습 데이터를 생성할 수 있습니다.</li><li><strong>지속적인 업데이트:</strong> 판단 목록은 시간이 지남에 따라 새로 고쳐야 합니다. UBI 데이터로 생성하면 최신 데이터로 업데이트된 판정 목록을 얻을 수 있습니다.</li><li><strong>비용 효율성:</strong> 판단 목록을 수동으로 작성하는 번거로움 없이 프로세스를 몇 번이고 효율적으로 반복할 수 있습니다.</li><li><strong>자연스러운 쿼리 분포</strong>: UBI 데이터는 실제 사용자 쿼리를 나타내므로 더 심층적인 변화를 이끌어낼 수 있습니다. 예를 들어, 사용자가 시스템에서 자연어를 사용하여 검색하나요? 그렇다면 시맨틱 검색 또는 하이브리드 검색 방식을 구현하는 것이 좋습니다.</li></ul><p>하지만 몇 가지 경고가 함께 제공됩니다:</p><ul><li><strong>편향성 증폭: </strong>인기 있는 콘텐츠는 더 많이 노출된다는 이유만으로 클릭을 받을 가능성이 더 높습니다. 따라서 인기 있는 항목이 증폭되어 더 나은 옵션이 사라질 수 있습니다.</li><li><strong>불완전한 커버리지: </strong>새 콘텐츠는 상호 작용이 부족하므로 결과에서 높은 순위를 차지하기 어려울 수 있습니다. 희귀 쿼리는 의미 있는 학습 데이터를 생성하기에 충분한 데이터 포인트가 부족할 수도 있습니다.</li><li><strong>계절적 변화:</strong> 시간이 지남에 따라 사용자 행동이 급격하게 변할 것으로 예상되는 경우, 과거 데이터로는 무엇이 좋은 결과인지 알 수 없을 수 있습니다.</li><li><strong>작업 모호성:</strong> 클릭 한 번으로 사용자가 원하는 것을 찾았다고 항상 보장할 수 있는 것은 아닙니다.</li></ul><h2>성적 계산</h2><h3>LTR 교육 성적</h3><p>LTR 모델을 학습시키려면 문서가 쿼리와 얼마나 연관성이 있는지를 수치로 표현해야 합니다. 저희 구현에서 이 숫자는 0.0에서 5.0+까지 연속 점수로 표시되며, 점수가 높을수록 관련성이 높다는 것을 의미합니다.</p><p>이 채점 시스템이 어떻게 작동하는지 보여드리기 위해 수동으로 만든 이 예시를 살펴보겠습니다:</p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">쿼리</td><td style="border: 1px solid #ccc; padding: 4px;">문서 콘텐츠</td><td style="border: 1px solid #ccc; padding: 4px;">등급</td><td style="border: 1px solid #ccc; padding: 4px;">설명</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">"최고의 피자 레시피"</td><td style="border: 1px solid #ccc; padding: 4px;">"단계별 사진이 포함된 정통 이탈리아 피자 도우 레시피"</td><td style="border: 1px solid #ccc; padding: 4px;">4.0</td><td style="border: 1px solid #ccc; padding: 4px;">연관성이 높고, 사용자가 찾고 있는 정확한 내용</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">"최고의 피자 레시피"</td><td style="border: 1px solid #ccc; padding: 4px;">"이탈리아 피자의 역사"</td><td style="border: 1px solid #ccc; padding: 4px;">1.0</td><td style="border: 1px solid #ccc; padding: 4px;">주제는 피자에 관한 것이지만 레시피는 아닙니다.</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">"최고의 피자 레시피"</td><td style="border: 1px solid #ccc; padding: 4px;">"초보자를 위한 빠른 15분 피자 레시피"</td><td style="border: 1px solid #ccc; padding: 4px;">3.0</td><td style="border: 1px solid #ccc; padding: 4px;">관련성이 있고 좋은 결과이지만 '최고의' 레시피라는 점에서는 다소 부족할 수 있습니다.</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">"최고의 피자 레시피"</td><td style="border: 1px solid #ccc; padding: 4px;">"자동차 관리 가이드"</td><td style="border: 1px solid #ccc; padding: 4px;">0.0</td><td style="border: 1px solid #ccc; padding: 4px;">전혀 관련이 없으며 쿼리와 전혀 관련이 없습니다.</td>
                    </tr>
              </tbody>
            </table>
          <p>여기에서 볼 수 있듯이 등급은 문서가 &#x27;최고의 피자 레시피&#x27;라는 샘플 쿼리와 얼마나 관련성이 있는지를 수치로 나타낸 것입니다. 이러한 점수를 통해 LTR 모델은 결과에서 어떤 문서가 더 높은 순위에 표시되어야 하는지 학습할 수 있습니다.</p><p>성적을 계산하는 방법은 교육 데이터 세트의 핵심입니다. 이를 위한 <a href="https://www.elastic.co/search-labs/blog/judgment-lists">여러 가지 접근 방식이</a> 있으며, 각 방식에는 고유한 장단점이 있습니다. 예를 들어, 관련성이 있으면 1점, 관련성이 없으면 0점이라는 이진 점수를 할당하거나 각 쿼리에 대해 결과 문서의 클릭 수를 계산할 수 있습니다.</p><p>이 블로그 게시물에서는 <strong><em>사용자 행동을 입력으로 고려하고 성적 번호를 출력으로 계산하는</em></strong> 다른 접근 방식을 사용할 것입니다. 또한 문서의 관련성에 관계없이 높은 결과가 더 많이 클릭되는 경향으로 인해 발생할 수 있는 편향도 수정할 예정입니다.</p><h2>성적 계산 - COEC 알고리즘</h2><p>COEC<a href="https://www.wsdm-conference.org/2010/proceedings/docs/p351.pdf">(예상 클릭 수 대비 클릭 수</a>) 알고리즘은 사용자 클릭 수로 평가 등급을 계산하는 방법론입니다.<br/>앞서 언급했듯이 사용자는 문서가 검색어와 가장 관련성이 높지 않더라도 높은 위치에 있는 결과를 클릭하는 경향이 있는데, 이를 <a href="https://eugeneyan.com/writing/position-bias/">위치 편향이라고</a> 합니다. COEC 알고리즘을 사용하는 핵심 아이디어는 모든 클릭이 똑같이 중요한 것은 아니며, 10번 위치에 있는 문서를 클릭하면 1번 위치에 있는 문서를 클릭하는 것보다 해당 문서가 쿼리와 훨씬 더 관련성이 높다는 것을 나타냅니다. COEC 알고리즘에 대한 연구 논문(위 링크)을 인용합니다:</p><p><em>&quot;검색 결과나 광고의 클릭률(CTR)은 결과의 위치에 따라 크게 감소한다는 것은 잘 알려진 사실입니다.&quot;</em></p><p>포지션 편향에 대한 자세한 내용은 <a href="https://www.researchgate.net/publication/200110550_An_experimental_comparison_of_click_position-bias_models">여기에서</a> 확인할 수 있습니다.</p><p>COEC 알고리즘으로 이 문제를 해결하려면 다음 단계를 따르세요:</p><p><strong>1. 게재 순위 기준선을 설정합니다:</strong> 각 검색 위치의 클릭률(CTR)을 1부터 10까지 계산합니다. 즉, 일반적으로 위치 1, 위치 2 등을 클릭하는 사용자의 비율을 파악합니다. 이 단계는 사용자의 자연스러운 위치 편향을 포착합니다.<br/><br/>다음을 사용하여 CTR을 계산합니다:</p><div class="katex">CTRp=CpIp</div><p>Where:</p><p>p = 위치. 1에서 10까지<br/>Cp = 모든 쿼리에서 p 위치에 있는 총 클릭 수(모든 문서)<br/>Ip = 총 노출 수입니다: 모든 쿼리에서 모든 문서가 위치 p에 노출된 횟수</p><p>여기서는 순위가 높을수록 더 많은 클릭을 얻을 것으로 예상합니다.</p><p><strong>2.</strong> <strong>예상 클릭 수(EC)를 계산합니다</strong>:</p><p>이 측정지표는 문서가 표시된 위치와 해당 위치에 대한 CTR을 기준으로 문서가 &#x27;받아야 하는&#x27; 클릭 수를 설정하며, 이를 통해 EC를 계산합니다:</p><div class="katex">EC (for a document) = qQdCTRpos(d,q)</div><p>Where:</p><p>Qd = 문서 d가 나타난 모든 쿼리<br/>pos(d,q)= 쿼리 q 결과에서 문서 d의 위치</p><p>3. <strong>실제 클릭 수 계산: </strong>문서가 표시된 모든 쿼리에서 문서가 받은 실제 총 클릭 수를 계산합니다(이하 <strong>A(d)</strong>라고 함).</p><p>4. <strong>COEC 점수를 계산합니다:</strong> 예상 클릭 수(EC(d))에 대한 실제 클릭 수(A(d))의 비율입니다:</p><div class="katex">COEC = A(d)EC(d)</div><p>이 메트릭은 이와 같은 위치 편향에 대해 정규화합니다:</p><ul><li>1.0점은 문서가 표시된 위치에서 예상한 대로 정확하게 수행되었음을 의미합니다.</li><li>1.0점 이상은 문서의 위치를 살펴봤을 때 예상보다 우수한 성능을 보였다는 의미입니다. 따라서 이 문서가 쿼리와 더 관련이 있습니다.</li><li>1.0 미만의 점수는 문서의 위치를 살펴볼 때 예상보다 실적이 좋지 않음을 의미합니다. 따라서 이 문서는 쿼리와 관련성이 낮습니다.</li></ul><p><strong><em>최종 결과는 검색 시스템과의 실제 상호 작용에서 추출한 위치 기반 기대치를 고려하여 사용자가 찾고 있는 것을 포착하는 등급 번호입니다.</em></strong></p><h2>기술적 구현</h2><p>LTR 모델을 학습시키기 위해 판단 목록을 만드는 스크립트를 만들 것입니다.</p><p>이 스크립트의 입력은 Elastic에서 색인된 UBI 데이터(쿼리 및 이벤트)입니다.</p><p>출력은 COEC 알고리즘을 사용하여 이러한 UBI 문서에서 생성된 CSV 파일의 판정 목록입니다. 이 판단 목록은 <a href="https://www.elastic.co/search-labs/blog/elasticsearch-learning-to-rank-introduction">이랜드와</a> 함께 관련 특징을 추출하고 LTR 모델을 훈련하는 데 사용할 수 있습니다.</p><h3>빠른 시작</h3><p>이 블로그의 샘플 데이터에서 판단 목록을 생성하려면 다음 단계를 따르세요:</p><p>1. 리포지토리를 복제합니다:</p><pre><code>git clone https://github.com/Alex1795/elastic-ltr-judgement_list-blog.git  
cd elastic-ltr-judgement_list-blog</code></pre><p>2. 필요한 라이브러리 설치</p><p>이 스크립트에는 다음 라이브러리가 필요합니다:</p><ul><li><em>판다: 판정</em> 목록 저장하기</li><li><em>엘라스틱서치</em>: Elastic 배포에서 UBI 데이터를 가져오려면 다음과 같이 하세요.</li></ul><p>파이썬 3.11도 필요합니다.</p><pre><code>pip install -r requirements.txt</code></pre><p>3. <a href="https://github.com/Alex1795/elastic-ltr-judgement_list-blog/blob/main/.env-example">.env 파일에서</a>Elastic 배포의 환경 변수를 업데이트합니다.</p><ul><li>ES_HOST</li><li>API_KEY</li></ul><p>환경 변수를 추가하려면 다음을 사용합니다:</p><pre><code>source .env</code></pre><p>4. ubi_queries, ubi_events 인덱스를 생성하고 샘플 데이터를 업로드합니다. setup.py 파일을 실행합니다:</p><pre><code>python setup.py</code></pre><p>5. 파이썬 스크립트를 실행합니다:</p><pre><code>python judgement_list-generator.py</code></pre><p>이 단계를 수행하면 다음과 같은 판단 목록이라는 새 파일이 표시됩니다:</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/2531090131ac9fe3e4e1d79de9d156fc47a7825a-782x531.png" style="width: 100%;" /><p>이 스크립트는 아래에 표시된 <strong>계산_관련성_등급()</strong> 함수를 사용하여 앞에서 설명한 COEC 알고리즘을 적용하여 성적을 계산합니다.</p><h2>데이터 아키텍처</h2><h3>Ubi 쿼리</h3><p>UBI 쿼리 색인에는 검색 시스템에서 실행된 쿼리에 대한 정보가 있습니다. 이 문서는 샘플 문서입니다:</p><pre><code>{
          "client_id": "client_002",
          "query": "italian pasta recipes",
          "query_attributes": {
            "search_type": "recipe",
            "category": "food",
            "cuisine": "italian"
          },
          "query_id": "q002",
          "query_response_id": "qr002",
          "query_response_object_ids": [
            "doc_011",
            "doc_012",
            "doc_013",
            "doc_014",
            "doc_015",
            "doc_016",
            "doc_017",
            "doc_018",
            "doc_019",
            "doc_020"
          ],
          "timestamp": "2024-08-14T11:15:00Z",
          "user_query": "italian pasta recipes"
        }</code></pre><p>여기에서 사용자(client_id), 쿼리 결과(query_response_object_ids), 쿼리 자체(timestamp, user_query)의 데이터를 볼 수 있습니다.</p><h3>Ubi 클릭 이벤트</h3><p>ubi_events 인덱스에는 사용자가 결과에서 문서를 클릭할 때마다 발생하는 데이터가 있습니다. 이 문서는 샘플 문서입니다:</p><pre><code>{
          "action_name": "click",
          "application": "recipe_search",
          "client_id": "client_001",
          "event_attributes": {
            "object": {
              "description": "Authentic Italian Pizza Dough Recipe with Step-by-Step Photos",
              "device": "desktop",
              "object_id": "doc_001",
              "position": {
                "ordinal": 1,
                "page_depth": 1
              },
              "user": {
                "city": "New York",
                "country": "USA",
                "ip": "192.168.1.100",
                "location": {
                  "lat": 40.7128,
                  "lon": -74.006
                },
                "region": "NY"
              }
            }
          },
          "message": "User clicked on document doc_001",
          "message_type": "click",
          "query_id": "q001",
          "timestamp": "2024-08-14T10:31:00Z",
          "user_query": "best pizza recipe"
        }</code></pre><h2>판결 목록 생성 스크립트</h2><h3>일반 스크립트 개요</h3><p>이 스크립트는 Elasticsearch에 저장된 쿼리 및 클릭 이벤트의 UBI 데이터를 사용하여 판단 목록 생성을 자동화합니다. 이러한 작업을 실행합니다:</p><ul><li>Elasticsearch에서 UBI 데이터를 가져와 처리합니다.</li><li>UBI 이벤트와 쿼리의 상관관계를 파악합니다.</li><li>각 포지션에 대한 CTR을 계산합니다.</li><li>각 문서에 대한 예상 클릭 수(EC)를 계산합니다.</li><li>각 문서에 대한 실제 클릭 수를 계산합니다.</li><li>각 쿼리-문서 쌍에 대한 COEC 점수를 계산합니다.</li><li>판정 목록을 생성하여 CSV 파일에 기록합니다.</li></ul><p>각 기능을 살펴보겠습니다:</p><h3>connect_to_elasticsearch()</h3><pre><code>def connect_to_elasticsearch(host, api_key):
    """Create and return Elasticsearch client"""
    try:
        es = Elasticsearch(
            hosts=[host],
            api_key=api_key,
            request_timeout=60
        )
        # Test the connection
        if es.ping():
            print(f"✓ Successfully connected to Elasticsearch at {host}")
            return es
        else:
            print("✗ Failed to connect to Elasticsearch")
            return None
    except Exception as e:
        print(f"✗ Error connecting to Elasticsearch: {e}")
        return None</code></pre><p>이 함수는 호스트와 API 키를 사용하여 Elasticsearch 클라이언트 객체를 반환합니다.</p><h3>FETCH_UBI_DATA()</h3><pre><code>def fetch_ubi_data(es_client: Elasticsearch, queries_index: str, events_index: str,
                   size: int = 10000) -> Tuple[List[Dict], List[Dict]]:
    """
    Fetch UBI queries and events data from Elasticsearch indices.

    Args:
        es_client: Elasticsearch client
        queries_index: Name of the UBI queries index
        events_index: Name of the UBI events index
        size: Maximum number of documents to fetch

    Returns:
        Tuple of (queries_data, events_data)
    """
    logger.info(f"Fetching data from {queries_index} and {events_index}")

    # Fetch queries with error handling
    try:
        queries_response = es_client.search(
            index=queries_index,
            body={
                "query": {"match_all": {}},
                "size": size
            }
        )
        queries_data = [hit['_source'] for hit in queries_response['hits']['hits']]
        logger.info(f"Fetched {len(queries_data)} queries")

    except Exception as e:
        logger.error(f"Error fetching queries from {queries_index}: {e}")
        raise

    # Fetch events (only click events for now) with error handling
    try:
        events_response = es_client.search(
            index=events_index,
            body={
                "query": {
                    "term": {"message_type.keyword": "CLICK_THROUGH"}
                },
                "size": size
            }
        )
        events_data = [hit['_source'] for hit in events_response['hits']['hits']]
        logger.info(f"Fetched {len(events_data)} click events")

    except Exception as e:
        logger.error(f"Error fetching events from {events_index}: {e}")
        raise

    logger.info(f"Data fetch completed successfully - Queries: {len(queries_data)}, Events: {len(events_data)}")

    return queries_data, events_data</code></pre><p>이 함수는 데이터 추출 계층으로, Elasticsearch와 연결하여 match_all 쿼리를 사용해 UBI 쿼리를 가져오고 &#x27;CLICK_THROUGH&#x27; 이벤트만 가져오도록 UBI 이벤트를 필터링합니다.</p><h3>process_ubi_data()</h3><pre><code>def process_ubi_data(queries_data: List[Dict], events_data: List[Dict]) -> pd.DataFrame:
    """
    Process UBI data and generate judgment list.

    Args:
        queries_data: List of query documents from UBI queries index
        events_data: List of event documents from UBI events index

    Returns:
        DataFrame with judgment list (qid, docid, grade, keywords)
    """
    logger.info("Processing UBI data to generate judgment list")

    # Group events by query_id
    clicks_by_query = {}
    for event in events_data:
        query_id = event['query_id']
        if query_id not in clicks_by_query:
            clicks_by_query[query_id] = {}

        # Extract clicked document info
        object_id = event['event_attributes']['object']['object_id']
        position = event['event_attributes']['object']['position']['ordinal']

        clicks_by_query[query_id][object_id] = {
            'position': position,
            'timestamp': event['timestamp']
        }

    judgment_list = []

    # Process each query
    for query in queries_data:
        query_id = query['query_id']
        user_query = query['user_query']
        document_ids = query['query_response_object_ids']

        # Get clicks for this query
        query_clicks = clicks_by_query.get(query_id, {})

        # Generate judgment for each document shown
        for doc_id in document_ids:
            grade = calculate_relevance_grade(doc_id, query_clicks, document_ids, queries_data, events_data)

            judgment_list.append({
                'qid': query_id,
                'docid': doc_id,
                'grade': grade,
                'query': user_query
            })

    df = pd.DataFrame(judgment_list)
    logger.info(f"Generated {len(df)} judgment entries for {df['qid'].nunique()} unique queries")

    return df</code></pre><p>이 함수는 판단 목록 생성을 처리합니다. UBI 이벤트와 쿼리를 연결하여 UBI 데이터 처리를 시작합니다. 그런 다음 각 문서-쿼리 쌍에 대해 계산_관련성_등급() 함수를 호출하여 판정 목록의 항목을 가져옵니다. 마지막으로 결과 목록을 판다 데이터 프레임으로 반환합니다.</p><h3>계산_관련성_등급()</h3><pre><code>def calculate_relevance_grade(document_id: str, clicks_data: Dict,
                              query_response_ids: List[str], all_queries_data: List[Dict] = None,
                              all_events_data: List[Dict] = None) -> float:
    """
    Calculate COEC (Click Over Expected Clicks) relevance score for a document.

    Args:
        document_id: ID of the document
        clicks_data: Dictionary of clicked documents with their positions for current query
        query_response_ids: List of document IDs shown in search results (ordered by position)
        all_queries_data: All queries data for calculating position CTR averages
        all_events_data: All events data for calculating position CTR averages

    Returns:
        COEC relevance score (continuous value, typically 0.0 to 5.0+)
    """

    # If no global data provided, fall back to simple position-based grading
    if all_queries_data is None or all_events_data is None:
        logger.warning("No global data provided, falling back to position-based grading")
        # Simple fallback logic
        if document_id in clicks_data:
            position = clicks_data[document_id]['position']
            if position > 3:
                return 4.0
            elif position >= 1 and position <= 3:
                return 3.0
        if document_id in query_response_ids:
            position = query_response_ids.index(document_id) + 1
            if position <= 5:
                return 2.0
            elif position >= 6 and position <= 10:
                return 1.0
        return 0.0

    # Calculate rank-aggregated click-through rates
    position_ctr_averages = {}
    position_impression_counts = {}
    position_click_counts = {}

    # Initialize counters
    for pos in range(1, 11):  # Positions 1-10
        position_impression_counts[pos] = 0
        position_click_counts[pos] = 0

    # Count impressions (every document shown contributes)
    for query in all_queries_data:
        for i, doc_id in enumerate(query['query_response_object_ids'][:10]):  # Top 10 positions
            position = i + 1
            position_impression_counts[position] += 1

    # Count clicks by position
    for event in all_events_data:
        if event.get('action_name') == 'click':
            position = event['event_attributes']['object']['position']['ordinal']
            if position <= 10:
                position_click_counts[position] += 1

    # Calculate average CTR per position
    for pos in range(1, 11):
        if position_impression_counts[pos] > 0:
            position_ctr_averages[pos] = position_click_counts[pos] / position_impression_counts[pos]
        else:
            position_ctr_averages[pos] = 0.0

    # Calculate expected clicks for this specific document
    expected_clicks = 0.0

    # Count how many times this document appeared at each position for any query
    for query in all_queries_data:
        if document_id in query['query_response_object_ids']:
            position = query['query_response_object_ids'].index(document_id) + 1
            if position <= 10:
                expected_clicks += position_ctr_averages[position]

    # Count total actual clicks for this document across all queries
    actual_clicks = 0
    for event in all_events_data:
        if (event.get('action_name') == 'click' and
                event['event_attributes']['object']['object_id'] == document_id):
            actual_clicks += 1

    # Calculate COEC score
    if expected_clicks > 0:
        coec_score = actual_clicks / expected_clicks
    else:
        coec_score = 0.0

    logger.debug(
        f"Document {document_id}: {actual_clicks} clicks / {expected_clicks:.3f} expected = {coec_score:.3f} COEC")

    return coec_score</code></pre><p>COEC 알고리즘을 구현하는 함수입니다. 각 위치에 대한 CTR을 계산한 다음 문서-쿼리 쌍에 대한 실제 클릭 수를 비교하고 마지막으로 각 위치에 대한 실제 COEC 점수를 계산합니다.</p><h3>GENERATE_JUDGEMENT_STATISTICS()</h3><pre><code>def generate_judgment_statistics(df: pd.DataFrame) -> Dict:
    """Generate statistics about the judgment list."""
    stats = {
        'total_judgments': len(df),
        'unique_queries': df['qid'].nunique(),
        'unique_documents': df['docid'].nunique(),
        'grade_distribution': df['grade'].value_counts().to_dict(),
        'avg_judgments_per_query': len(df) / df['qid'].nunique() if df['qid'].nunique() > 0 else 0,
        'queries_with_clicks': len(df[df['grade'] > 1]['qid'].unique()),
        'click_through_rate': len(df[df['grade'] > 1]) / len(df) if len(df) > 0 else 0
    }
    return stats</code></pre><p>심사 목록에서 총 쿼리 수, 총 고유 문서 수 또는 등급 분포와 같은 유용한 통계를 생성합니다. 이는 순전히 정보 제공용이며 결과 판정 목록은 변경되지 않습니다.</p><h2>결과 및 영향</h2><p>빠른 시작 섹션의 지침을 따르면 320개의 항목이 포함된 판정 목록이 포함된 결과 CSV 파일이 표시됩니다(리포지토리에서 <a href="https://github.com/Alex1795/elastic-ltr-judgement_list-blog/blob/main/judgment_list.csv">샘플 출력을</a> 확인할 수 있습니다). 이러한 필드를 사용합니다:</p><ul><li>qid: 쿼리의 고유 ID</li><li>docid: 결과 문서의 고유 식별자</li><li>성적: 쿼리-문서 쌍에 대해 계산된 성적입니다.</li><li>쿼리: 사용자 쿼리</li></ul><p> &quot;이탈리아 요리법&quot; 쿼리에 대한 결과를 살펴보겠습니다:</p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">qid</td><td style="border: 1px solid #ccc; padding: 4px;">docid</td><td style="border: 1px solid #ccc; padding: 4px;">학년</td><td style="border: 1px solid #ccc; padding: 4px;">쿼리</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Q1-이탈리아어 레시피</td><td style="border: 1px solid #ccc; padding: 4px;">레시피_파스타_베이직</td><td style="border: 1px solid #ccc; padding: 4px;">0.0</td><td style="border: 1px solid #ccc; padding: 4px;">이탈리안 레시피</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Q1-이탈리아어 레시피</td><td style="border: 1px solid #ccc; padding: 4px;">레시피_피자_마르게리타</td><td style="border: 1px solid #ccc; padding: 4px;">3.333333</td><td style="border: 1px solid #ccc; padding: 4px;">이탈리안 레시피</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Q1-이탈리아어 레시피</td><td style="border: 1px solid #ccc; padding: 4px;">레시피_리조또_가이드</td><td style="border: 1px solid #ccc; padding: 4px;">10.0</td><td style="border: 1px solid #ccc; padding: 4px;">이탈리안 레시피</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Q1-이탈리아어 레시피</td><td style="border: 1px solid #ccc; padding: 4px;">레시피_프랑스_크로아상</td><td style="border: 1px solid #ccc; padding: 4px;">0.0</td><td style="border: 1px solid #ccc; padding: 4px;">이탈리안 레시피</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Q1-이탈리아어 레시피</td><td style="border: 1px solid #ccc; padding: 4px;">레시피_스페인_파에야</td><td style="border: 1px solid #ccc; padding: 4px;">0.0</td><td style="border: 1px solid #ccc; padding: 4px;">이탈리안 레시피</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Q1-이탈리아어 레시피</td><td style="border: 1px solid #ccc; padding: 4px;">레시피_그리스_무사카</td><td style="border: 1px solid #ccc; padding: 4px;">1.875</td><td style="border: 1px solid #ccc; padding: 4px;">이탈리안 레시피</td>
                    </tr>
              </tbody>
            </table>
          <p>결과에서 &#x27;이탈리아 요리법&#x27;이라는 검색어를 확인할 수 있습니다:</p><ul><li>리조또 레시피는 예상보다 10배 더 많은 클릭을 받아 검색어에 가장 적합한 결과입니다.</li><li>피자 마르게리타도 훌륭한 결과물입니다.</li><li>(놀랍게도) 그리스 무사카도 좋은 결과를 얻었고, 결과에서 보이는 위치보다 더 좋은 성능을 보였습니다. 즉, 이탈리아 레시피를 찾는 일부 사용자가 대신 이 레시피에 관심을 갖게 된 것입니다. 이러한 사용자는 일반적으로 지중해 요리에 관심이 있을 수 있습니다. 결국, 이것이 우리에게 알려주는 것은 위에서 설명한 다른 두 가지 &#x27;더 나은&#x27; 경기에서도 좋은 결과를 보여줄 수 있다는 것입니다.</li></ul><h2>결론</h2><p>UBI 데이터를 사용하면 LTR 모델의 학습을 자동화하여 자체 사용자로부터 고품질의 판단 목록을 만들 수 있습니다. UBI 데이터는 검색 시스템이 어떻게 사용되고 있는지를 반영하는 빅 데이터 세트를 제공하며, COEC 알고리즘을 사용하여 성적을 생성함으로써 내재된 편향을 설명하는 동시에 사용자가 더 나은 결과라고 생각하는 것을 반영합니다. 여기에 설명된 방법을 실제 사용 사례에 적용하여 실제 사용 트렌드에 따라 진화하는 더 나은 검색 환경을 제공할 수 있습니다.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[AI 에이전트 평가: Elastic이 에이전트 프레임워크를 테스트하는 방법]]></title>
            <description><![CDATA[정확하고 검증 가능한 결과를 보장하기 위해 에이전트 시스템에 대한 변경 사항을 Elastic 사용자에게 릴리즈하기 전에 어떻게 평가하고 테스트하는지 알아보세요.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/ai-agent-evaluation-elastic</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/ai-agent-evaluation-elastic</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Susan Chang, Abhimanyu Anand]]></dc:creator>
            <pubDate>Mon, 13 Oct 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/e681aab6ee77dfef38dc95e36c8535dfe7bcc105-721x420.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<h2>소개</h2><p>Elastic Stack에는 곧 출시될<a href="https://www.elastic.co/search-labs/blog/ai-agentic-workflows-elastic-ai-agent-builder"> 에이전트 빌더의</a> Elastic AI 에이전트(현재 기술 프리뷰 중)와 <a href="https://www.elastic.co/docs/solutions/security/ai/attack-discovery">공격 탐색</a> (8.18 및 9.0+의<a href="https://www.elastic.co/blog/whats-new-elastic-security-9-0-0">GA)</a> 과 같은 많은 LLM 기반 에이전트 애플리케이션이 있으며, 더 많은 애플리케이션이 개발 중입니다. 개발 중은 물론 배포 후에도 이러한 질문에 답하는 것이 중요합니다:</p><ul><li>이러한 AI 애플리케이션의 응답 품질을 어떻게 평가할 수 있을까요?</li><li>변경을 하는 경우, 변경이 진정으로 개선된 것이며 사용자 경험의 저하를 초래하지 않는다고 어떻게 보장할 수 있을까요?</li><li>이러한 결과를 반복 가능한 방식으로 쉽게 테스트하려면 어떻게 해야 할까요?</li></ul><p>기존의 소프트웨어 테스트와 달리, 생성 AI 애플리케이션을 평가하려면 통계적 방법, 미묘한 정성적 검토, 사용자 목표에 대한 깊은 이해가 필요합니다.</p><p>이 문서에서는 Elastic 개발자 팀이 평가를 수행하고, 배포 전에 변경 사항의 품질을 보장하고, 시스템 성능을 모니터링하기 위해 사용하는 프로세스에 대해 자세히 설명합니다. 모든 변화가 증거에 의해 뒷받침되어 신뢰할 수 있고 검증 가능한 결과로 이어질 수 있도록 노력합니다. 이 프로세스의 일부는 오픈 소스 정신의 일부로서 투명성에 대한 우리의 약속을 반영하여 Kibana에 직접 통합되어 있습니다. 평가 데이터와 메트릭의 일부를 공개적으로 공유함으로써 커뮤니티의 신뢰를 증진하고 AI 에이전트를 개발하거나 제품을 활용하는 모든 사람에게 명확한 프레임워크를 제공하고자 합니다.</p><h2>제품 예시</h2><p>이 문서에서 사용된 방법은 공격 탐색 및 Elastic AI 에이전트와 같은 솔루션을 반복하고 개선하는 방법의 기초가 되었습니다. 두 가지에 대해 각각 간략하게 소개합니다:</p><h3>Elastic Security의 공격 탐색</h3><p>공격 탐색은 LLM을 사용해 Elastic에서 공격 시퀀스를 식별하고 요약합니다. 지정된 기간(기본 24시간)에 Elastic Security 경보가 주어지면, 공격 탐색의 에이전트 워크플로우가 자동으로 공격이 발생했는지 여부와 침해된 호스트 또는 사용자, 결론에 기여한 경보와 같은 중요한 정보를 찾아냅니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/20fabb47642dad7b588daaaa8c3a98de860ad01d-1251x758.png" style="width: 100%;" /><p></p><p>목표는 LLM 기반 솔루션이 최소한 사람만큼 좋은 결과물을 만들어내는 것입니다.</p><h3>Elastic AI 에이전트</h3><p><strong>Elastic 에이전트 빌더는</strong> 모든 검색 기능을 활용하는 상황 인식 AI 에이전트를 구축하기 위한 새로운 플랫폼입니다. 사용자가 대화형 상호 작용을 통해 데이터를 이해하고 데이터로부터 답을 얻을 수 있도록 설계된 사전 구축된 범용 에이전트인 <strong>Elastic AI 에이전트가</strong> 함께 제공됩니다.</p><p>에이전트는 Elasticsearch 또는 연결된 지식 기반 내에서 관련 정보를 자동으로 식별하고 사전 구축된 도구 모음을 활용하여 이들과 상호 작용함으로써 이를 달성합니다. 이를 통해 Elastic AI 에이전트는 단일 문서에 대한 간단한 질문(&amp;A)부터 여러 인덱스에 걸친 집계와 단일 또는 다단계 검색이 필요한 복잡한 요청까지 다양한 범위의 사용자 쿼리에 응답할 수 있습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/d29dee100bb8a17bb623acd745773a5164a1df4f-1600x1014.png" style="width: 100%;" /><h2>실험을 통한 개선 사항 측정</h2><p>AI 에이전트의 맥락에서 실험이란 잘 정의된 차원(예: 유용성, 정확성, 지연 시간)에서 성능을 개선하기 위해 설계된 시스템에 대한 구조적이고 테스트 가능한 변경을 의미합니다. 목표는 확실한 답을 찾는 것입니다: &quot;이 변경 사항을 병합하면 진정한 개선이 이루어지고 사용자 경험이 저하되지 않는다고 보장할 수 있는가?&quot;라는 질문에 확실히 답하는 것입니다.</p><p>저희가 수행하는 대부분의 실험에는 일반적으로 다음이 포함됩니다:</p><ul><li><strong>가설:</strong> 구체적이고 검증 가능한 주장. <em>예시:</em> &quot;공격 검색 도구에 대한 액세스 권한을 추가하면 보안 관련 쿼리의 정확도가 향상됩니다.&quot;</li><li><strong>성공 기준:</strong> &#x27;성공&#x27;의 의미를 정의하는 명확한 임계값을 설정하세요. <em>예시:</em> &quot;보안 데이터 집합의 정확도 점수 +5% 개선, 다른 곳에서는 성능 저하 없음.&quot;</li><li><strong>평가 계획:</strong> 성공 측정 방법(지표, 데이터 세트, 비교 방법)</li></ul><p>성공적인 실험은 체계적인 탐구 과정입니다. 사소한 프롬프트 조정부터 대대적인 아키텍처 변경에 이르기까지 모든 변경은 이 7단계에 따라 의미 있고 실행 가능한 결과를 보장합니다:</p><ul><li>1단계: 문제 식별</li><li>2단계: 지표 정의</li><li>3단계: 명확한 가설 수립하기</li><li>4단계: 평가 데이터 세트 준비</li><li>5단계: 실험 실행</li><li>6단계: 결과 분석 + 반복하기</li><li>7단계: 의사 결정 및 문서화</li></ul><p>이러한 단계의 예는 <em>그림 1에</em> 나와 있습니다. 다음 하위 섹션에서는 각 단계에 대해 설명하며, 다음 문서에서 각 단계의 기술적 세부 사항에 대해 자세히 설명할 예정입니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/3a9f5a3e92dd4922a795a19104c6e4ad8c98958d-2400x1352.png" style="width: 100%;" /><h2>실제 Elastic 예제를 통한 단계별 안내</h2><h3>1단계: 문제 식별</h3><p><em>이 변경으로 해결하고자 하는 문제는 정확히 무엇인가요?</em></p><p>공격 탐지 예시: 요약이 불완전하거나 양성 활동이 공격으로 잘못 플래그가 지정되는 경우(오탐): 간혹 요약이 불완전합니다.</p><p>Elastic AI 에이전트 예시: 특히 분석 쿼리에 대한 에이전트의 도구 선택이 최적이 아니며 일관성이 없어 종종 잘못된 도구가 선택되는 경우가 있습니다. 이는 결과적으로 토큰 비용과 지연 시간을 증가시킵니다.</p><h3>2단계: 지표 정의</h3><p><em>문제를 측정 가능하게 만들어 현재 상태와 변경 사항을 비교할 수 있도록 합니다.</em></p><p>일반적인 측정지표에는 <a href="https://developers.google.com/machine-learning/crash-course/classification/accuracy-precision-recall">정확도 및 회수율</a>, <a href="https://en.wikipedia.org/wiki/Semantic_similarity">의미적 유사성</a>, 사실성 등이 포함됩니다. 사용 사례에 따라 코드 검사를 사용하여 경고 ID 일치 또는 올바르게 검색된 URL과 같은 메트릭을 계산하거나 보다 자유로운 형식의 답변을 위해 LLM-as-judge와 같은 기법을 사용하여 메트릭을 계산합니다.</p><p>다음은 실험에 사용된 몇 가지 지표의 예시<em>(전부는 아님)</em>입니다:</p><p><strong>공격 탐지</strong></p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Metric</td><td style="border: 1px solid #ccc; padding: 4px;">설명</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">정밀도 & 리콜</td><td style="border: 1px solid #ccc; padding: 4px;">실제 출력과 예상 출력 간의 알림 ID를 일치시켜 탐지 정확도를 측정합니다.</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">유사성</td><td style="border: 1px solid #ccc; padding: 4px;">BERTScore를 사용하여 응답 텍스트의 의미적 유사성을 비교합니다.</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">사실성</td><td style="border: 1px solid #ccc; padding: 4px;">주요 IOC(타협 지표)가 존재하나요? MITRE 전술(업계 공격 분류)이 올바르게 반영되어 있나요?</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">공격 체인 일관성</td><td style="border: 1px solid #ccc; padding: 4px;">발견 횟수를 비교하여 공격의 과대 또는 과소 보고 여부를 확인합니다.</td>
                    </tr>
              </tbody>
            </table>
          <p><strong>Elastic AI 에이전트</strong></p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Metric</td><td style="border: 1px solid #ccc; padding: 4px;">설명</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">정밀도 & 리콜</td><td style="border: 1px solid #ccc; padding: 4px;">상담원이 사용자 쿼리에 답변하기 위해 검색한 문서/정보와 쿼리에 답변하는 데 필요한 실제 정보 또는 문서를 대조하여 정보 검색 정확도를 측정합니다.</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">사실성</td><td style="border: 1px solid #ccc; padding: 4px;">사용자 쿼리에 답변하는 데 필요한 핵심 사실이 존재하나요? 절차적 문의를 위한 사실관계가 올바른 순서로 정리되어 있나요?</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">응답 관련성</td><td style="border: 1px solid #ccc; padding: 4px;">응답에 사용자 쿼리와 관련이 없거나 주변적인 정보가 포함되어 있나요?</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">응답 완전성</td><td style="border: 1px solid #ccc; padding: 4px;">응답이 사용자 쿼리의 모든 부분에 대한 답변을 제공하나요? 응답에 근거 사실에 존재하는 모든 정보가 포함되어 있나요?</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">ES|QL 유효성 검사</td><td style="border: 1px solid #ccc; padding: 4px;">생성된 ES|QL이 구문적으로 올바른가요? 기능적으로 기준 데이터 ES|QL과 동일한가요?</td>
                    </tr>
              </tbody>
            </table>
          <h3>3단계: 명확한 가설 수립하기</h3><p><em>위에서 정의한 문제와 지표를 사용하여 명확한 성공 기준을 설정하세요.</em></p><p>Elastic AI 에이전트 예시:</p><ol><li><strong>특정 기능과 사용 사례를 명확하게 정의하기 위해 relevance_search 및 nl_search 도구의 설명을 변경하여</strong> 구현합니다.</li><li><strong></strong> <strong>도구 호출 정확도가</strong> <strong>25 %</strong> 향상될 것으로 예상됩니다 %.</li><li>다른 지표에 부정적인 영향을 미치지 않는지 확인하여 이것이 순 긍정적인지 확인할 것입니다. <strong>사실성과 완전성</strong>.</li><li><strong>정확한 도구 설명은 상담원이 다양한 쿼리 유형에 가장 적합한 검색 도구를 보다 정확하게 선택하고 적용하는 데 도움이 되어 잘못된 적용을 줄이고 전반적인 검색 효율성을 개선할</strong> 수 있기 때문입니다.</li></ol><h3>4단계: 평가 데이터 세트 준비</h3><p><em>시스템의 성능을 측정하기 위해 실제 시나리오를 캡처한 데이터 세트를 사용합니다.</em></p><p>수행하는 평가 유형에 따라 LLM에 공급되는 원시 데이터 등 다양한 유형의 데이터 형식이 필요할 수 있습니다(예 공격 발견을 위한 공격 시나리오) 및 예상 결과물입니다. 애플리케이션이 챗봇인 경우 입력은 사용자 쿼리일 수 있고 출력은 올바른 챗봇 응답, 검색했어야 하는 올바른 링크 등이 될 수 있습니다.</p><p>공격 발견 예시:</p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">10가지 새로운 공격 시나리오</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">오 마이 멀웨어 에피소드 8편(omymalware.com)</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">4가지 다중 공격 시나리오(처음 2개 카테고리의 공격을 결합하여 생성)</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">3가지 양성 시나리오</td>
                    </tr>
              </tbody>
            </table>
          <p>Elastic AI 에이전트 평가 데이터 세트 예제<a href="https://github.com/elastic/kibana/blob/main/x-pack/platform/packages/shared/onechat/kbn-evals-suite-onechat/evals/kb/kb.spec.ts">(Kibana 데이터 세트 링크)</a>:</p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">14 오픈 소스 데이터 세트를 사용하여 여러 소스를 KB 단위로 시뮬레이션하는 인덱스.</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">5가지 쿼리 유형(분석, 텍스트 검색, 하이브리드...)</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">7 쿼리 의도 유형(절차적 , 사실적 - 분류, 조사; ...)</td>
                    </tr>
              </tbody>
            </table>
          <h3>5단계: 실험 실행</h3><p>평가 데이터 세트에 대해 기존 에이전트와 수정된 버전 모두에서 응답을 생성하여 실험을 실행합니다. 사실 여부와 같은 지표를 계산합니다(2단계 참조).</p><p>2단계에서 요구되는 메트릭을 기반으로 다양한 평가를 혼합합니다:</p><ul><li>규칙 기반 평가(예 파이썬/타입스크립트를 사용하여 .json이 유효한지 확인)</li><li>LLM-as-judge(응답이 원본 문서와 사실적으로 일치하는지 별도의 LLM에게 문의)</li><li>뉘앙스 품질 검사를 위한 휴먼 인 더 루프 검토</li></ul><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/8648e75818d3291f0ac66f069438a500d42b8225-1600x1099.png" style="width: 100%;" /><h3>6단계: 결과 분석 + 반복하기</h3><p>이제 메트릭을 확보했으니 결과를 분석합니다. <em><span style="text-decoration:underline">결과가 3단계에서 정의한 성공 기준을 충족하더라도 변경 사항을 프로덕션에 병합하기 전에 인적 검토를</span></em> 거치고, 기준에 부합하지 않으면 문제를 반복하여 수정한 다음 새 변경 사항에 대한 평가를 실행합니다.</p><p>병합하기 전에 최적의 변경 사항을 찾으려면 몇 번의 반복이 필요할 것으로 예상됩니다. 커밋을 푸시하기 전에 로컬 소프트웨어 테스트를 실행하는 것과 마찬가지로, 오프라인 평가도 로컬 변경 사항 또는 여러 제안된 변경 사항으로 실행할 수 있습니다. 실험 결과, 종합 점수 및 시각화 저장을 자동화하여 분석을 간소화하는 데 유용합니다.</p><h3>7단계: 의사 결정 및 문서화</h3><p>의사 결정 프레임워크와 승인 기준에 따라 변경 사항 병합을 결정하고 실험을 문서화합니다. 의사 결정은 다면적이며 다른 데이터 세트에 대한 회귀 시나리오를 확인하거나 제안된 변경의 비용 편익을 평가하는 등 평가 데이터 세트 이외의 요소를 고려할 수 있습니다.</p><p>예시: 몇 번의 반복을 테스트하고 비교한 후 가장 높은 점수를 받은 변경 사항을 선택하여 제품 관리자 및 기타 관련 이해 관계자에게 보내 승인을 받습니다. 이전 단계의 결과를 첨부하여 결정을 내리는 데 도움이 되도록 합니다. 공격 탐색 측면에 대한 더 많은 예는 <a href="https://www.elastic.co/blog/elastic-security-generative-ai-features">Elastic Security의 생성형 AI 기능의 비하인드 스토</a>리를 참조하세요.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/74c80b8f34dce8ddd20873ecb2f553873587ed35-1600x618.png" style="width: 100%;" /><h2>결론</h2><p>이 블로그에서는 실험 워크플로우의 엔드투엔드 프로세스를 살펴보면서 Elastic 사용자에게 변경 사항을 릴리즈하기 전에 에이전트 시스템의 변경 사항을 평가하고 테스트하는 방법을 설명합니다. 또한 Elastic에서 에이전트 기반 워크플로우를 개선하는 몇 가지 예도 제공했습니다. 다음 블로그 게시물에서는 좋은 데이터 집합을 만드는 방법, 신뢰할 수 있는 메트릭을 설계하는 방법, 여러 메트릭이 관련된 경우 의사 결정을 내리는 방법 등 다양한 단계의 세부 사항에 대해 자세히 설명합니다.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[A2A 프로토콜을 통해 Elastic 에이전트를 Gemini Enterprise에 연결하기]]></title>
            <description><![CDATA[에이전트 빌더를 사용하여 사용자 정의 Elastic 에이전트를 A2A 프로토콜을 통해 Gemini Enterprise와 같은 외부 서비스에 노출하는 방법을 알아보세요.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/a2a-protocol-elastic-agent-builder-gemini-enterprise</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/a2a-protocol-elastic-agent-builder-gemini-enterprise</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Jedr Blaszyk, Valerio Arvizzigno, Joe McElroy]]></dc:creator>
            <pubDate>Thu, 09 Oct 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/5be8a425fab55dca2f9717d2e50812b0450fa625-1440x840.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p><strong>Elastic 에이전트 빌더는</strong> Elasticsearch에서 직접 데이터 기반 AI 에이전트를 생성하기 위한 일련의 기능입니다. <a href="https://www.elastic.co/search-labs/blog/series/context-aware-ai-agentic-workflows-with-elastic">시리즈의</a> 이전 게시물에서는 사용자 지정 상담원에게 복잡한 작업을 수행할 수 있는 툴을 제공하고 이들의 행동을 안내하는 일련의 사용자 지정 지침을 제공하는 방법에 대해 설명했습니다.</p><p>하지만 이미 사용하고 있는 애플리케이션 및 생산성 도구와 함께 사용자 지정 상담원을 사용하려면 어떻게 해야 할까요?</p><p>이것이 바로 <strong>에이전트 간(A2A) 프로토콜이</strong> 필요한 이유입니다. A2A는 상호 운용성을 위한 <a href="https://github.com/a2aproject/A2A">개방형 표준으로</a>, 서로 다른 플랫폼의 상담원들이 소통하고 협업할 수 있도록 합니다. 그리고 이를 Elastic 에이전트 빌더에 직접 구축했습니다.</p><p>오늘은 구축한 사용자 지정 상담원을 다른 서비스, 특히 <strong>Gemini Enterprise </strong>(이전의 Agentspace)에 노출하는 방법을 보여드리겠습니다.</p><h2>개방형 표준의 힘: A2A가 중요한 이유</h2><p>블로그 게시물 <a href="https://www.elastic.co/search-labs/blog/ai-agent-builder-elasticsearch">첫 번째 Elastic 에이전트에서</a> 시장 데이터에 안전하게 액세스할 수 있는 <em>금융 지원</em> 에이전트와 같은 사용자 정의 에이전트를 구축하는 방법을 보여드렸습니다. 그러나 작업을 재구축하지 않고 Gemini Enterprise와 같은 다른 환경에서 인사이트를 사용할 수 없다면 그 가치는 제한적입니다.</p><p>이러한 상호운용성 문제가 에이전트 AI의 발목을 잡고 있습니다. 에이전트는 여러 플랫폼에서 소통하기 위해 공통 언어가 필요하며, 이것이 바로 A2A 프로토콜의 역할입니다. 상담원과 직접 소통할 수 있는 표준 커뮤니케이션 계층을 제공할 뿐만 아니라 조직 전체의 전문 상담원이 협업하고 인사이트를 공유할 수 있는 미래를 열어줍니다.</p><p>이를 가능하게 하기 위해 Elastic 에이전트 빌더는 기본적으로 모든 에이전트에 대해 두 개의 표준 엔드포인트를 통해 A2A 프로토콜을 지원합니다:</p><ol><li>상담원 <strong>카드 엔드포인트(<code>GET {your-kibana-url}/api/agent_builder/a2a/{agentId}.json</code>) - </strong>사용자 지정 상담원의 명함 역할을 합니다. A2A 호환 서비스에 상담원에 대한 메타데이터(이름, 설명, 기능 등)를 제공합니다.</li><li><strong>A2A 프로토콜 엔드포인트 (<code>POST {your-kibana-url}/api/agent_builder/a2a/{agentId}</code>)</strong> - 통신 채널입니다. 다른 상담원이 여기로 요청을 보내면 상담원이 이를 처리하고 응답을 반환하며, 이 모든 과정은 <a href="https://a2a-protocol.org/latest/specification/">A2A 프로토콜 사양에</a> 따라 이루어집니다.</li></ol><h2>A2A 인스펙터로 에이전트 테스트하기</h2><p>에이전트를 프로덕션 시스템에 연결하기 전에 에이전트가 제대로 통신하고 있는지 확인하는 것이 좋습니다. 가장 쉬운 방법은 A2A 통합 테스트 및 디버깅을 위해 특별히 설계된 도구인 <strong>A2A 인스펙터를</strong> 사용하는 것입니다.</p><p>인스펙터를 실행하는 방법은 간단합니다. <a href="https://github.com/a2aproject/a2a-inspector">a2a-inspector</a> 리포지토리를 복제하고 README 지침에 따라 <a href="https://github.com/a2aproject/a2a-inspector?tab=readme-ov-file#3-run-the-application">애플리케이션을 실행할</a> 수 있습니다. 시작하면 기본적으로 <code>http://localhost:5001/</code> 에서 UI를 사용할 수 있습니다.</p><p>A2A 인스펙터를 상담원과 연결하려면 두 가지 주요 정보를 제공해야 합니다:</p><ul><li>상담원 카드 URL: 상담원을 설명하는 엔드포인트입니다. <a href="https://www.elastic.co/search-labs/blog/ai-agent-builder-elasticsearch">이전 게시물의 재무 도우미 상담</a>원의 경우 이 URL은 <code>{your-kibana-url}/api/agent_builder/a2a/financial_assistant.json</code> 입니다.</li><li>인증 헤더: 인증에 표준 API 키를 사용합니다.</li></ul><p>인스펙터의 UI에 이러한 세부 정보를 입력하면 바로 상담원과 연결하여 채팅을 시작할 수 있습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/7231c72bf30bed2a854f58658c1eca2843f43bfc-1600x1296.png" style="width: 100%;" /><p>이 간단한 검증을 통해 에이전트가 올바르게 구성되었으며 다음 단계로 넘어갈 준비가 되었다는 확신을 가질 수 있습니다.</p><h2>라이브 시작! Gemini Enterprise의 사용자 지정 에이전트</h2><p>이제 흥미로운 부분인 맞춤형 재무 설계사 에이전트를 Gemini Enterprise(이전의 Agentspace) 내에서 구현하는 것입니다. 이 통합은 <a href="https://console.cloud.google.com/marketplace/product/elastic-prod/elastic-ai-agent">Google Cloud Marketplace에서 사용할 수 있는 Elastic AI 에이전트에</a> 의해 구동됩니다.</p><p>연결되면 Gemini Enterprise는 A2A 프로토콜을 사용하여 상담원과 직접 통신합니다. 이제 사용자는 익숙한 환경을 벗어나지 않고도 사용자 정의 Elasticsearch 에이전트의 심층적인 데이터 기반 인사이트에 액세스할 수 있습니다. 상담원 목록에서 사용자 정의 Elastic 에이전트를 확인할 수 있습니다:</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/37a39e92ebf3d72c6c8014397cd8e846336173a4-1600x834.png" style="width: 100%;" /><p>Gemini Enterprise의 사용자가 다음과 같이 질문한다고 상상해 보세요:</p><p><em>&quot;시장 심리가 걱정됩니다. 어떤 고객이 악재로 인해 가장 위험에 처해 있는지 알려주실 수 있나요?</em>&quot;</p><p>뒤에서 Gemini Enterprise는 A2A 프로토콜을 통해 이 쿼리를 사용자 정의 Elastic 에이전트로 라우팅합니다. 그런 다음 상담원이 전문화된 도구를 사용하여 데이터를 쿼리하고 답변을 작성하여 다시 전송합니다. 최종 사용자에게는 매끄러운 경험이 제공됩니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/e5f126c1a27a51c6e69a767aa87c9f746b62e39c-1600x1044.png" style="width: 100%;" /><p>그리고 여기서 멈추지 않습니다! 이제 Elastic 에이전트로 검색된 답변을 다른 전문 에이전트를 트리거할 수 있는 다음 질문의 컨텍스트로 사용할 수 있습니다(예. 투자 플랫폼 에이전트를 통해 상장사에 대한 노출을 조정할 수 있습니다). 검색창을 떠나지 않고도 이 모든 것이 가능합니다.</p><p>A2A가 포함된 Gemini Enterprise에 배포된 Elastic 에이전트를 사용하면 사용자가 데이터 및 도구와 대화할 수 있는 단일 UI를 제공하여 액세스, 오케스트레이션, 워크플로우를 통합하여 AI, 검색, 엔터프라이즈 시스템 간의 마찰을 제거할 수 있으며, 모두 맥락에 맞게 사용할 수 있습니다. 사용자 입장에서는 도구 전환이 줄어들고 직관적이고 유능한 AI 비서가 더 많아진다는 뜻입니다. 조직에게는 일관된 거버넌스, 확장성 및 상호 운용성이 내장되어 있다는 의미입니다.</p><h2>구축할 차례</h2><p>이제 어디서나 Elastic 에이전트를 사용할 수 있는 도구가 생겼습니다. 개방형 A2A 프로토콜을 활용하면 데이터를 인식하는 맞춤형 상담원의 범위를 확장할 수 있습니다.</p><p>이 게시물에서는 주요 단계를 안내해 드렸습니다:</p><ul><li>A2A 에이전트 카드 및 프로토콜 엔드포인트를 통해 에이전트를 노출합니다.</li><li>A2A 인스펙터로 연결을 테스트합니다.</li><li>Google의 Gemini Enterprise와 같은 외부 서비스에 상담원을 실시간으로 통합합니다.</li></ul><p>상담원을 더 이상 격리할 필요가 없습니다. 여러분이 만든 강력하고 상호 연결된 시스템을 빨리 보고 싶어요. 행복한 건물!</p><p>가장 쉽게 시작할 수 있는 방법은 <a href="https://console.cloud.google.com/marketplace/product/elastic-prod/elastic-cloud?pli=1">Google Cloud Marketplace에서</a>Elastic Cloud 무료 체험판을 사용하는 것입니다.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Elasticsearch용 에이전트 AI 도구 개선 실험]]></title>
            <description><![CDATA[확장 가능한 RAG 최적화를 위해 선형 검색기, 하이브리드 검색, semantic_text를 결합하여 반복적인 실험을 통해 Elasticsearch의 AI 에이전트 워크플로우를 개선한 방법을 알아보세요.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/ai-agent-builder-experiments-performance</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/ai-agent-builder-experiments-performance</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Sean Story]]></dc:creator>
            <pubDate>Mon, 06 Oct 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/42903c869e034674b30bb36013345aaa97f6608b-1184x864.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>요즘 다른 모든 사람들과 마찬가지로 Elastic도 Chat, 에이전트, RAG에 올인하고 있습니다. 검색 부서에서는 최근 에이전트 빌더와 도구 레지스트리를 개발 중이며, 모두 Elasticsearch에서 데이터와 &#x27;채팅&#x27;하는 것을 간단하게 만들기 위한 것입니다.</p><p>이러한 노력의 &#x27;큰 그림&#x27;에 <a href="https://www.elastic.co/search-labs/blog/ai-agentic-workflows-elastic-ai-agent-builder">대해 자세히 알아보려면 Elasticsearch로 AI 에이전트 워크플로우 구축</a> <a href="https://www.elastic.co/search-labs/blog/ai-agent-builder-elasticsearch">블로그 또는 첫 번째 Elastic 에이전트를 읽어보세요: 단일 쿼리에서 AI 기반 채팅까지에서</a> 보다 실용적인 입문서를 읽어보세요.</p><p>하지만 이 블로그에서는 채팅을 시작할 때 가장 먼저 일어나는 일 중 하나를 조금 더 자세히 살펴보고 최근 개선된 몇 가지 사항을 안내해드리려고 합니다.</p><h2>여기서 무슨 일이 일어나고 있나요?</h2><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/25a24055a166d7d6ba81d80aa35cb97163662e23-1600x443.png" style="width: 100%;" /><p>Elasticsearch 데이터와 채팅할 때 기본 AI 에이전트가 이 표준 플로우를 안내합니다:</p><ol><li>프롬프트를 확인합니다.</li><li>해당 프롬프트에 대한 답변이 포함되어 있을 가능성이 높은 인덱스를 식별합니다.</li><li>프롬프트에 따라 해당 인덱스에 대한 쿼리를 생성합니다.</li><li>해당 쿼리로 해당 인덱스를 검색합니다.</li><li>결과를 종합합니다.</li><li>결과가 프롬프트를 해결할 수 있나요? 그렇다면 응답하세요. 그렇지 않다면 반복하되 다른 것을 시도하세요.</li></ol><p>검색 증강 세대(RAG)에 불과하기 때문에 너무 새롭지 않을 것입니다. 예상대로 응답의 품질은 초기 검색 결과의 관련성에 따라 크게 달라집니다. 따라서 응답 품질을 개선하기 위해 노력하면서 3단계에서 생성하고 4단계에서 실행하는 쿼리에 매우 세심한 주의를 기울이고 있습니다. 그리고 흥미로운 패턴을 발견했습니다.</p><p>첫 번째 응답이 &#x27;나쁨&#x27;인 경우가 종종 있었는데, 이는 쿼리를 잘못 실행했기 때문이 아니었습니다. 쿼리할 <em>인덱스를 잘못 선택했기</em> 때문입니다. 3단계와 4단계는 보통 2단계가 문제가 되지 않았습니다.</p><h2>우리가 뭘 하고 있었나요?</h2><p>초기 구현은 간단했습니다. 저희는 <code>_cat/indices</code> 을 통해 사용 가능한 모든 인덱스를 나열한 다음, 이 인덱스 중 사용자의 메시지/질문/프롬프트에 가장 적합한 인덱스를 식별하도록 LLM에 요청하는 도구(index_explorer라고 함)를 구축했습니다. 이 <a href="https://github.com/elastic/kibana/blob/0cc78184957fcd12110dabae50353392ea937508/x-pack/platform/packages/shared/onechat/onechat-genai-utils/tools/index_explorer.ts#L98-L113">원본 구현은 여기에서</a> 확인할 수 있습니다.</p><pre><code>You are an AI assistant for the Elasticsearch company.
based on a natural language query from the user, your task is to select up to ${limit} most relevant indices from a list of indices.

*The natural language query is:* ${nlQuery}

*List of indices:*
${indices.map((index) => `- ${index.index}`).join('\n')}

Based on those information, please return most relevant indices with your reasoning.
Remember, you should select at maximum ${limit} indices.</code></pre><p>얼마나 잘 작동했나요? 확실하지 않았습니다! 잘 작동하지 <em>않는</em> 사례는 분명 있었지만, 현재 상태를 정량화하는 것이 첫 번째 과제였습니다.</p><h2>기준선 설정</h2><h3>데이터에서 시작됩니다.</h3><p>우리에게 필요했던 것은 사용자 프롬프트와 기존 인덱스 세트가 주어졌을 때 올바른 인덱스를 선택하는 도구의 효율성을 측정하기 위한 골든 데이터 세트였습니다. 그런 데이터 세트가 없었기 때문에 저희가 직접 생성했습니다.</p><p>인정합니다: 이것이 &#x27;모범 사례&#x27;는 아니라는 것을 알고 있습니다. 하지만 때로는 자전거를 타는 것보다 앞으로 나아가는 것이 더 나을 때도 있습니다. <a href="https://www.elastic.co/about/our-source-code#progress-perfection">진행, 심플한 완벽함</a>.</p><p><a href="https://gist.github.com/seanstory/a08db2e149897da656db3a1ca72e17ac">이 프롬프트를</a> 사용하여 여러 다른 도메인에 대한 시드 인덱스를 생성했습니다. 그런 다음 생성된 각 도메인에 대해<a href="https://gist.github.com/seanstory/a280a85d067e61bfeb5911bf2654e6e2"> 이 프롬프트를</a> 사용하여 몇 가지 인덱스를 더 생성했습니다(여기서 목표는 하드 네거티브와 분류하기 어려운 예시로 LLM에 혼란을 심어주는 것입니다). 다음으로, 생성된 각 인덱스와 그 설명을 수동으로 편집했습니다. 마지막으로 <a href="https://gist.github.com/seanstory/44291b666c05a383136f6e36bb9106fa">이 프롬프트를</a> 사용하여 테스트 쿼리를 생성했는데, 다음과 같은 샘플 데이터가 남았습니다:</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/9d96d87e286eddbc012402b1ecccd57419a99253-1600x782.png" style="width: 100%;" /><p>와 같은 테스트 사례:</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/4c2e9ad941d98d7e66033bbc08c9b8060ec19097-1600x797.png" style="width: 100%;" /><h3>테스트 하네스 제작하기</h3><p>여기서부터의 과정은 매우 간단했습니다. 가능한 도구를 스크립트로 작성하세요:</p><ol><li>대상 Elasticsearch 클러스터로 클린 슬레이트를 설정하세요.</li><li>대상 데이터 세트에 정의된 모든 인덱스를 생성합니다.</li><li>각 테스트 시나리오에 대해 i<code>ndex_explorer</code> 도구를 실행합니다(편리하게도 <a href="https://www.elastic.co/docs/api/doc/kibana/operation/operation-post-agent-builder-tools-execute">도구 실행 API가</a> 있습니다).</li><li>결과 인덱스와 예상 인덱스를 비교하고 결과를 캡처합니다.</li><li>모든 테스트 시나리오를 완료한 후 결과를 표로 작성합니다.</li></ol><h3>설문조사에 따르면...</h3><p>초기 결과는 당연히 평범했습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/9c10679bcd6291edfa2a9ba42e7dd922aa483f0b-1216x806.png" style="width: 100%;" /><p>전반적으로 77.14% 올바른 인덱스를 식별하는 데 정확합니다. 그리고 이것은 모든 인덱스에 의미적으로 의미 있는 좋은 이름이 있는 &#x27;최상의 경우&#x27;의 시나리오입니다. PUT test2/_doc/foo {...}`를 해본 사람이라면 누구나 인덱스에 항상 의미 있는 이름이 있는 것은 아니라는 것을 알고 있습니다.</p><p>따라서 우리는 기준선을 가지고 있으며 개선의 여지가 많이 있음을 보여줍니다. 이제 과학이 필요한 시간입니다! 🧪</p><h2>실험</h2><h3>가설 1: 매핑이 도움이 될 것입니다.</h3><p>여기서 목표는 원래 프롬프트와 관련된 데이터를 포함할 인덱스를 식별하는 것입니다. 그리고 인덱스에 포함된 데이터를 가장 잘 설명하는 부분은 인덱스의 <em>매핑입니다</em>. 인덱스 콘텐츠의 샘플을 가져오지 않더라도 인덱스에 double 유형의 가격 필드가 있다는 것은 데이터가 판매할 상품을 나타낸다는 것을 의미합니다. 텍스트 유형의 작성자 필드는 일부 구조화되지 않은 언어 데이터를 의미합니다. 이 두 가지를 합치면 데이터가 책/이야기/시라는 것을 암시할 수 있습니다. 인덱스의 속성을 아는 것만으로도 많은 의미론적 단서를 얻을 수 있습니다. 그래서 로컬 브랜치에서 &#x27;.index_explorer`를 조정했습니다. 도구를 사용하여 인덱스의 전체 매핑(이름과 함께)을 LLM에 전송하여 결정을 내릴 수 있습니다. </p><p>결과(Kibana 로그에서 가져온):</p><pre><code>[2025-09-05T11:01:21.552-05:00][ERROR][plugins.onechat] Error: Error calling connector: event: error
data: {"error":{"code":"request_entity_too_large","message":"Received a content too large status code for request from inference entity id [.rainbow-sprinkles-elastic] status [413]","type":"error"}}


    at createInferenceProviderError (errors.ts:90:10)
    at convertUpstreamError (convert_upstream_error.ts:39:38)
    at handle_connector_response.ts:26:33
    at Observable.init [as _subscribe] (/Users/seanstory/Desktop/Dev/kibana/node_modules/rxjs/src/internal/observable/throwError.ts:123:68)...</code></pre><p>이 도구의 초기 개발자는 이러한 문제를 예상하고 있었습니다. 인덱스의 매핑은 정보를 얻을 수 있는 금광이지만, 상당히 장황한 JSON 블록이기도 합니다. 그리고 수많은 인덱스(평가 데이터 세트는 20개를 정의함)를 비교하는 현실적인 시나리오에서는 이러한 JSON 블롭이 합쳐집니다. 따라서 모든 옵션에 대한 인덱스 이름뿐만 아니라 각 옵션의 전체 매핑이 아닌 더 많은 컨텍스트를 LLM에 제공하여 결정에 도움을 주고자 합니다.</p><h3>가설 2: 절충안으로 &#x27;플랫화&#x27; 매핑(필드 목록) 사용</h3><p>우리는 인덱스 작성자가 의미론적으로 의미 있는 인덱스 이름을 사용한다는 가정에서 시작했습니다. 이 가정을 필드 이름으로도 확장하면 어떨까요? 이전 실험은 실패했는데, 그 이유는 JSON 매핑에 복잡한 메타데이터와 상용구가 많이 포함되어 있었기 때문입니다.</p><pre><code>     "description_text": {
          "type": "text",
          "fields": {
            "keyword": {
              "type": "keyword"
            }
          },
          "copy_to": [
            "description_semantic"
          ]
        },</code></pre><p>예를 들어, 위의 블록은 236자이며 Elasticsearch 매핑에서 단 하나의 필드만 정의합니다. 반면 &quot;description_text&quot; 문자열은 16자에 불과합니다. 이는 문자 수가 거의 15배 증가한 것이지만, 해당 필드가 사용 가능한 데이터에 대해 의미하는 바를 설명하는 데 있어 의미 있는 의미 개선은 없습니다. 모든 인덱스에 대한 매핑을 가져오되, LLM으로 보내기 전에 필드 이름 목록으로만 &#x27;플랫화&#x27;하면 어떨까요?</p><p>저희도 사용해 보았습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/112c2f447c11f154b5082725cd49b51d0a3c8a65-1214x804.png" style="width: 100%;" /><p>정말 멋지네요! 전반적으로 개선되었습니다. 하지만 더 잘할 수 있을까요?</p><h3>가설 3: 매핑 _meta의 설명</h3><p>추가 컨텍스트 없이 필드 이름만으로 그렇게 많은 점프가 발생했다면, 아마도 상당한 컨텍스트를 추가하는 것이 더 좋을 것입니다! 모든 인덱스에 반드시 설명을 첨부해야 하는 것은 아니지만, 매핑의 _meta 객체에 모든 종류의 인덱스 수준 메타데이터를 추가할 수 있습니다. 생성된 인덱스로 돌아가서 데이터 세트의 모든 인덱스에 대한 설명을 추가했습니다. 설명이 지나치게 길지 않다면 전체 매핑보다 적은 토큰을 사용하고 인덱스에 포함된 데이터에 대한 훨씬 더 나은 인사이트를 제공해야 합니다. 실험을 통해 이 가설을 검증했습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/32d2692ad4479d0e52d8ee723dcc5710a6ec90f3-1208x806.png" style="width: 100%;" /><p>소폭의 개선으로 이제 &gt;90% 전반적으로 정확해졌습니다.</p><h3>가설 4: 합이 부분보다 큼</h3><p>필드 이름을 사용하면 결과가 향상되었습니다. 설명을 통해 결과가 향상되었습니다. 따라서 설명과 필드 이름을 <em>모두 </em>활용하면 더 나은 결과를 얻을 수 있겠죠?</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/114cbb408ff16b136251d2265416bd5270380fe5-1208x794.png" style="width: 100%;" /><p>데이터는 &quot;아니오&quot;라고 응답했습니다(이전 실험에서 변경 사항 없음). 여기서 유력한 이론은 설명이 처음부터 인덱스 필드/매핑에서 생성되었기 때문에 이 두 컨텍스트 간에 서로 다른 정보가 충분하지 않아 결합할 때 &#x27;새로운&#x27; 것을 추가하는 데 도움이 되지 않는다는 것입니다. 또한 20개의 테스트 지수에 대해 전송하는 페이로드가 상당히 커지고 있습니다. 지금까지 우리가 따라온 사고방식은 확장할 수 없습니다. 사실, 지금까지의 실험 중 어떤 것도 수백, 수천 개의 인덱스가 있는 Elasticsearch 클러스터에서는 작동하지 않을 것이라고 믿을 만한 충분한 이유가 있습니다. 인덱스의 총 수가 증가함에 따라 LLM으로 전송되는 메시지 크기를 선형적으로 증가시키는 접근 방식은 일반화할 수 있는 전략이 아닐 수 있습니다.</p><p>우리에게 정말 필요한 것은 수많은 후보를 가장 관련성이 높은 옵션으로 좁히는 데 도움이 되는 접근 방식입니다....</p><p>여기에는 검색 문제가 있습니다.</p><h3>가설 5: 시맨틱 검색을 통한 선택</h3><p>인덱스 이름에 의미론적 의미가 있는 경우, 벡터로 저장하여 의미론적으로 검색할 수 있습니다.</p><p>인덱스의 필드 이름에 의미론적 의미가 있는 경우, 이를 벡터로 저장하고 의미론적으로 검색할 수 있습니다.</p><p>인덱스에 의미론적 의미가 있는 설명이 있는 경우, 이 역시 벡터로 저장하고 의미론적으로 검색할 수 있습니다.</p><p>오늘날 Elasticsearch 인덱스는 이러한 정보를 검색할 수 없지만(어쩌면 그렇게 해야 할지도 모릅니다!), 그 차이를 해결할 수 있는<a href="https://github.com/elastic/connectors/pull/3638"> 무언가를 함께 해킹하는</a> 것은 꽤나 사소한 일이었습니다. Elastic의 커넥터 프레임워크를 사용해 클러스터의 모든 인덱스에 대한 문서를 출력하는 커넥터를 구축했습니다. 출력 문서는 다음과 같은 형태가 됩니다:</p><pre><code> doc = {
                "_id": index_name,
                "index_name": index_name,
			"meta_description”: description,
"field_descriptions" = field_descriptions,
                "mapping": json.dumps(mapping),  
                "source_cluster": self.es_client.configured_host,
            }</code></pre><p>저는 이 문서들을 수동으로 매핑을 정의한 새 인덱스로 보냈습니다:</p><pre><code>{
   "mappings": {
       "properties": {
           "semantic_content": {
               "type": "semantic_text"
           },
           "index_name": {
               "type": "text",
               "copy_to": "semantic_content"
           },
           "mapping": {
               "type": "keyword",
               "copy_to": "semantic_content"
           },
           "source_cluster": {
               "type": "keyword"
           },
           "meta_description": {
               "type": "text",
               "copy_to": "semantic_content"
           },
           "field_descriptions": {
               "type": "text",
               "copy_to": "semantic_content"
           }
       }
   }
}</code></pre><p>이렇게 하면 의미론적 의미를 가진 다른 모든 필드가 청크업되어 색인되는 단일 semantic_content 필드가 생성됩니다. 이 인덱스를 검색하는 것은 사소한 일이 됩니다:</p><pre><code>GET indexed-indices/_search
{
 "query": {
   "semantic": {
     "field": "semantic_content",
     "query": "$query"
   }
 }
}</code></pre><p>수정된 <code>index_explorer</code> 도구는 이제 LLM에 요청할 필요 없이 주어진 쿼리에 대해 단일 임베딩을 요청하고 효율적인 벡터 검색 작업을 수행할 수 있으므로 <em>훨씬</em> 더 빨라졌습니다. 상위 히트를 선택한 지표로 삼은 결과 다음과 같은 결과를 얻었습니다:</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/06ef5d78040d064d3444793f636d527d9e19a869-1214x800.png" style="width: 100%;" /><p>이 접근 방식은 확장 가능합니다. 이 접근 방식은 효율적입니다. 하지만 이 접근 방식은 기준선보다 겨우 나은 수준입니다. 하지만 이는 놀라운 일이 아닙니다. 검색 접근 방식이 매우 순진하기 때문입니다. 뉘앙스가 없습니다. 인덱스의 이름과 설명이 인덱스에 포함된 임의의 필드 이름보다 더 많은 가중치를 가져야 한다는 인식이 없습니다. 동의어 일치보다 정확한 어휘 일치에 가중치를 부여하는 어포던스는 없습니다. 그러나 고도로 미묘한 쿼리를 작성하려면 현재 데이터에 대해 많은 것을 가정해야 합니다. 지금까지 인덱스와 필드 이름에 의미론적 의미가 있다는 큰 가정을 해 보았지만, 한 걸음 더 나아가서 <em>얼마나 많은</em> 의미를 가지고 있으며 서로 어떻게 연관되어 있는지 가정해 볼 필요가 있습니다. 이렇게 하지 않으면 최상의 일치 항목을 최고의 결과로 확실하게 식별할 수는 없지만, 상위 N개의 결과 중 어딘가에 최상의 일치 항목이 있다고 말할 수 있습니다. 우리는 의미론적 정보가 존재하는 맥락에서 의미론적 정보를 소비하고, 의미론적으로 구별되는 방식으로 자신을 표현할 수 있는 다른 개체와 비교하여 그 둘을 판단할 수 있는 무언가가 필요합니다. LLM처럼요.</p><h3>가설 6: 후보 세트 감소</h3><p>이 외에도 여러 가지 실험이 있었지만, 핵심적인 돌파구는 시맨틱 검색만으로 최적의 일치 항목을 고르려는 욕구를 버리고 대신 시맨틱 검색을 필터로 활용하여 LLM의 고려 대상에서 관련 없는 인덱스를 걸러내는 것이었습니다. <a href="https://gist.github.com/seanstory/d704443120e20f6c844db10e30066860">검색을</a> 위해 선형 검색기, 하이브리드 검색과 RRF, <code>semantic_text</code> 를 결합하여 상위 5개 일치하는 인덱스로 결과를 제한했습니다.</p><p>그런 다음 각 일치 항목에 대해 인덱스의 이름, 설명 및 필드 이름을 LLM용 메시지에 추가했습니다. 결과는 환상적이었습니다:</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/8fcabd78f591f90d6bc7c0e087d31317e4eef791-1206x804.png" style="width: 100%;" /><p>역대 실험 중 가장 높은 정확도! 또한 이 접근 방식은 총 인덱스 수에 비례하여 메시지 크기가 증가하지 않기 때문에 훨씬 더 확장성이 뛰어납니다.</p><h2>결과</h2><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/04d630797213dbb8bf567da41d1cdd5c7b4586c9-1600x521.png" style="width: 100%;" /><p>첫 번째 분명한 결과는 우리의 기준선을 개선할 <em>수</em> 있다는 것이었습니다. 지금 생각하면 당연해 보이지만 실험을 시작하기 전에는 <code>index_explorer</code> 도구를 완전히 버리고 검색 공간을 제한하기 위해 사용자의 명시적 설정에 의존해야 하는지에 대해 진지한 논의가 있었습니다. 여전히 실행 가능하고 유효한 옵션이지만, 이 연구는 이러한 사용자 입력을 사용할 수 없는 경우 인덱스 선택을 자동화하는 방향으로 나아갈 수 있는 유망한 경로가 있음을 보여줍니다.</p><p>다음으로 분명한 결과는 문제에 더 많은 설명 문자를 던지는 것만으로는 수익이 줄어든다는 것이었습니다. 이 연구 이전에는 <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/mapping-field-meta">필드 수준 메타데이터를</a> 저장하기 위해 Elasticsearch의 기능을 확장하는 데 투자해야 할지 고민하고 있었습니다. 현재 <code>meta</code> 값은 50자로 제한되어 있으며, 필드의 의미적 이해를 도출하기 위해서는 이 값을 늘려야 한다는 가정이 있었습니다. 이는 분명히 사실이 아니며, LLM은 필드 이름만으로 상당히 잘 작동하는 것 같습니다. 나중에 더 조사할 수 있지만 더 이상 시급하다고 생각하지 않습니다.</p><p>반대로, 이는 &#x27;검색 가능한&#x27; 인덱스 메타데이터의 중요성에 대한 명확한 증거가 되었습니다. 이 실험을 위해 저희는 인덱스 오브 인덱스를 해킹했습니다. 그러나 이것은 Elasticsearch에 직접 구축하거나, 관리할 API를 구축하거나, 최소한 관련 규칙을 수립하는 것을 검토할 수 있는 부분입니다. 여러 옵션을 검토하고 내부적으로 논의 중이니 계속 지켜봐 주시기 바랍니다.</p><p>마지막으로, 이러한 노력을 통해 시간을 들여 실험하고 데이터 기반 의사 결정을 내리는 것이 얼마나 가치 있는 일인지 확인했습니다. 실제로 에이전트 빌더 제품에 강력한 제품 내 평가 기능이 필요하다는 것을 재확인하는 데 도움이 되었습니다. 인덱스를 선택하는 도구만을 위한 전체 테스트 하네스를 구축해야 한다면, 고객은 반복적으로 조정할 때 사용자 지정 도구를 정성적으로 평가할 수 있는 방법이 반드시 필요합니다.</p><p>앞으로 무엇을 만들게 될지 기대가 되며, 여러분도 기대가 되시길 바랍니다!</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[AWS Marketplace에 Elasticsearch를 배포하는 방법]]></title>
            <description><![CDATA[이 단계별 가이드에서 AWS Marketplace의 Elastic Cloud Service를 사용하여 Elasticsearch를 설정하고 실행하는 방법을 알아보세요.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/aws-elasticsearch-service-set-up</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/aws-elasticsearch-service-set-up</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Eduard Martin]]></dc:creator>
            <pubDate>Fri, 03 Oct 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/a80776d2ef85db26f850d932339fac2d26b90278-1086x620.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>이 도움말에서는 Marketplace 제품을 사용하여 AWS에서 Elasticsearch를 배포하는 방법을 알아봅니다.</p><p>AWS의 인프라를 통해 모든 Elastic Stack 구성 요소의 배포와 오케스트레이션을 간소화하는 공식 관리형 Elasticsearch 서비스인 Elastic Cloud Service on AWS를 사용하려고 합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/f15814051b53b50bec38f9a9f515a1e6dc08a56c-884x440.png" style="width: 100%;" /><p>Elasticsearch와 AWS EC2를 설치하고 구성하는 방법을 배우고 싶으시다면 <a href="https://www.elastic.co/search-labs/blog/elasticsearch-on-aws-ec2-deployment-guide">이 블로그</a>를 참조하십시오.<br/></p><h2>AWS Marketplace는 무엇입니까?</h2><p><a href="https://aws.amazon.com/marketplace"><strong>Elastic on AWS Marketplace</strong></a>는 인프라 프로비저닝, 보인 및 확장을 AWS에서 처리하여 개발자는 검색 애플리케이션 구축에만 집중할 수 있는 완전 관리형 검색 및 분석 환경을 제공합니다. 이를 통해 팀은 AWS와의 기본 제공 통합을 통해 엔터프라이즈급 Elasticsearch 클러스터를 몇 분 안에 배포할 수 있습니다.</p><h2>Elastic을 AWS Marketplace에서 언제 사용해야 하나요?</h2><p>Elastic on AWS Marketplace는 관리형 서비스, 기본 제공 보안, 운영 부담 없이 원활한 AWS 통합을 통해 Elasticsearch를 배포하고자 하는 기존 AWS 인프라를 보유한 조직에 가장 적합합니다.</p><h2>AWS 마켓플레이스에서 Elastic Cloud를 설정하는 방법</h2><h3>1단계: AWS Marketplace에 액세스</h3><p>1. <a href="https://console.aws.com/">AWS</a>에 로그인합니다.</p><ul><li>검색 창에서 AWS 마켓플레이스를 검색합니다.</li></ul><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/fc9928f79482c2c01e33978c88d390a2bfa2a3bf-1600x340.png" style="width: 100%;" /><p>2. 왼쪽 탐색 패널에서 <strong>제품 검색</strong>을 클릭한 다음 Elasticsearch를 검색합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/ca4232271cb13ebfe33de406ecaec085033ec8a0-1454x760.png" style="width: 100%;" /><p>3. <strong>Elastic Cloud (Elasticsearch Service)</strong>를 클릭합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/e59aa276e55532f2ac3461d0ca983af4d41ad7a6-1600x611.png" style="width: 100%;" /><h3>2단계: 서비스 구독</h3><p>1. <strong>구매 옵션</strong>을 선택하거나 <strong>무료 체험</strong>을 클릭합니다.</p><p>2. <strong>요금제 세부 정보</strong>, <strong>이용 약관</strong> 및 <strong>구매 세부 정보</strong>를 검토합니다.</p><p>3. <strong>구독 </strong>버튼을 클릭합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/1c30d12b6b1061e76771d518011e522285f939f1-1600x290.png" style="width: 100%;" /><p>4. 이제 Elastic 계정을 설정해야 합니다. AWS에서 제공하는 단계를 따르세요.</p><p>a. &#x27;통합 사용&#x27; 버튼을 클릭합니다.</p><p>b. &#x27;로그인 또는 벤더 계정 만들기&#x27; 버튼을 클릭합니다.</p><p>c. &#x27;템플릿 시작&#x27; 버튼을 클릭합니다.</p><p>d. &#x27;소프트웨어 시작&#x27; 버튼을 클릭합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/879d9f0f01406e1955e1b38a2f6f2192ef040344-852x722.png" style="width: 100%;" /><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/fc59be78cf776aa12867f40810598419576cbd39-1600x1143.png" style="width: 100%;" /><h3>3단계. Elastic에서 새 계정 구성</h3><p>1. Elastic 계정을 생성하십시오</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/5efeaf0737062a55470b17b67651f220e12183f2-986x905.png" style="width: 100%;" /><p>2. 이메일 주소를 확인하십시오</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/e0cfaac278614e317ce278935040bfa5a58edd13-853x894.png" style="width: 100%;" /><p>3. 귀하의 이름과 회사 정보를 입력하십시오</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/d5658fe66c3b1bcced73e822eae006846f0ddd9e-997x903.png" style="width: 100%;" /><p>4. 간단한 Elastic 설문조사를 완료합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/a3f53c00dadfd22f7d739a920c87d5f387182833-892x805.png" style="width: 100%;" /><p>5. Elastic Cloud를 호스팅할 지역을 선택합니다. 기본적으로 실제 AWS 리전이 선택됩니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/c1dcdf3bf91c305821daaa25a60aa03be6454c1c-1207x1032.png" style="width: 100%;" /><p>6. Elastic이 배포될 때까지 기다립니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/50903e57ebea7cc47bdfabf4750b4ba2a7a91148-1370x1266.png" style="width: 100%;" /><p>7. 배포가 AWS 마켓플레이스 구독에 연결됩니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/3bb731e2d5de5053ccecb77e45dbdbcdaf294dba-1600x1288.png" style="width: 100%;" /><h2>구독 취소</h2><p>구독을 취소하시려면 다음 단계에 따르세요.</p><p>1. <a href="https://console.aws.com/">AWS 콘솔</a>로 이동합니다.</p><p>검색 창에서 AWS Marketplace를 검색합니다. <strong>AWS Marketplace</strong>를 클릭합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/aeed3d1e67b4cef91934de257a6fd6daa9737a12-1600x554.png" style="width: 100%;" /><p>2. <strong>Elastic Cloud 구독</strong>을 클릭합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/04e6cc41850226df223dbe2d1b0e4b45265f6c39-1600x564.png" style="width: 100%;" /><p>3. <strong>작업</strong> 버튼을 클릭한 다음 <strong>구독 취소</strong>를 클릭합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/e33d334ea6541c637a223de3ebd6209def75a6d3-1600x1039.png" style="width: 100%;" /><p>4. 취소를 확인한 후 <strong>예 </strong>버튼과<strong> 구독 취소</strong> 버튼을 클릭하십시오.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/b73b3187168adc7aefdd46f95be33c1bce3da1e4-1103x698.png" style="width: 100%;" /><p>5. 페이지 상단에 확인 메시지가 표시됩니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/c5ba66a23d535e866a8b458e5aca82c5f0b93037-1600x639.png" style="width: 100%;" /><h2>다음 단계</h2><p>Elastic Cloud 여정을 시작하려면 단일 배포와 세 개의 프로젝트가 포함된 <a href="https://aws.amazon.com/marketplace/pp/prodview-voru33wi6xs7k"> Elastic Cloud (Elasticsearch Service)</a> 7일 무료 체험을 시작하세요. AWS 계정에 로그인하고 &#x27;구매 옵션 보기&#x27;를 클릭하기만 하면 Elastic<a href="https://aws.amazon.com/marketplace/pp/prodview-voru33wi6xs7k"> Cloud(Elasticsearch Service)</a>에서 Elastic의 검색 AI 플랫폼을 즉시 사용해 볼 수 있습니다. 이 평가판은 인프라 관리 부담 없이 검색, 보안 및 통합 가시성 솔루션에 대한 전체 액세스를 제공합니다.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[AutoOps: 자체 관리형 Elasticsearch 관리 간소화를 위한 여정]]></title>
            <description><![CDATA[자체 관리형 Elasticsearch(온프레미스 또는 프라이빗 호스팅 환경)를 위한 AutoOps를 살펴보세요. AutoOps의 가치, 설정 방법, 그리고 제공되는 인사이트를 소개해 드립니다.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/elastic-autoops-self-managed-elasticsearch</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/elastic-autoops-self-managed-elasticsearch</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Ori Shafir]]></dc:creator>
            <pubDate>Wed, 01 Oct 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/4538ca4c6cdde99d88f1cead7515716fac217b92-1600x883.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>자체 관리형 Elasticsearch(온프레미스 또는 프라이빗 호스팅 환경)를 위한 AutoOps를 소개합니다. AutoOps는 Elasticsearch를 더 쉽게 관리할 수 있도록 도와줍니다. 이 블로그에서는 <em>기존 기술 기능 중심의 안내</em> 대신, DevOps 엔지니어 관점에서 AutoOps의 가치, 설정 방법, 제공되는 인사이트를 보여줍니다. AutoOps의 진가는 대규모 환경에서 Elasticsearch를 관리하는 일상 업무에서 가장 잘 드러납니다.</p><h2>1장: 배경 - 대규모 자체 관리의 복잡성</h2><p><strong>대규모 자체 관리 데이터 플랫폼의 운영은 복잡할 수 있습니다.</strong></p><p>어느 순간, 쿼리가 번개처럼 빨라집니다. 다음으로 수집이 지연되고 저장 비용이 급증합니다. 동물들이 새벽 3시에 호출할 수 있다는 점을 제외하면 기본적으로 동물원을 운영하는 것과 비슷합니다.</p><p>제 환경도 다르지 않습니다. 여러 클러스터, 무거운 클러스터 간 검색(CCS), 여러 부서에 걸쳐 수백 명의 사용자가 있습니다.</p><p>스택 모니터링을 일상 업무에 사용합니다. 그래프와 메트릭을 제공하지만 점들을 연결하려면 여전히 많은 전문 지식과 시간이 필요합니다. 병목 현상을 진단하거나 샤드 전략을 조정해야 할 시기를 파악하는 것은 여전히 오류가 발생하기 쉬운 수동 프로세스입니다. 많은 경우 문제가 가동 중단, 성능 저하 또는 예기치 않은 스토리지 급증을 초래할 때까지 눈에 띄지 않습니다.</p><h2>2장: AutoOps 알아보기</h2><p>그리고 발표가 있었습니다: 이제 자체 관리 클러스터(온프레미스 또는 프라이빗 호스팅 환경)에서 <strong><a href="https://www.elastic.co/platform/autoops">AutoOps를</a> 사용할 수 있습니다.</strong></p><p>.AutoOps는 오랫동안 Elastic Cloud 사용자가 배포를 보다 효율적으로 관리할 수 있도록 지원해 왔습니다. 이제 동일한 기능을 온프레미스 또는 프라이빗 클라우드 환경에서 실행되는 자체 관리형 클러스터(ECK, ECE, 독립형)에서도 라이선스 등급에 상관없이 무료로 활용할 수 있습니다.</p><p>AutoOps 피치는 매력적입니다:</p><ul><li>수집 병목 현상, 불균형한 샤드, 느린 쿼리 등에 대한 실시간 문제 감지</li><li>클러스터 구성에 맞춘 실행 가능한 권장 사항</li><li>리소스 최적화 인사이트를 통해 효율성 향상 및 낭비되는 비용 절감</li><li>경량 에이전트 설치로 간단한 설정 - 추가 인프라 필요 없음</li></ul><p>솔직히 &#x27;추가 인프라가 필요 없다&#x27;고 약속하는 모든 것이 제 관심을 끌었습니다.</p><h2>3장: 5분 안에 설정하기(예, 정말)</h2><p>저는 오후 시간을 차단하고 커피를 비축하며 긴 준비에 대비했습니다. 놀랍게도 5분밖에 걸리지 않았습니다:</p><ol><li>내 <a href="https://cloud.elastic.co/registration?onboarding_service_type=ccm-autoops">Elastic Cloud 계정에</a>로그인</li><li>에이전트 실행 방식을 결정했습니다: Elastic Cloud Kubernetes (ECK), Kubernetes, Docker 또는 Linux.</li><li>클러스터 URL 입력</li><li>단일 명령을 실행하여 경량 Metricbeat 에이전트를 설치했습니다.</li></ol><p>그게 다입니다. 클러스터가 연결되었습니다.</p><p>프로비저닝할 전용 모니터링 클러스터가 없습니다. 그리고 중요한 것은 AutoOps는 <strong>메트릭만 전송하므로</strong> 회사의 데이터는 자체 관리 환경에 보관된다는 것입니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/cb8e1ebc0f23a6db6b0e4300c9cdcd165623ca39-1184x734.png" style="width: 100%;" /><p><em>1단계: Elastic Cloud에 가입하기</em></p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/b208830db4d74feef700473ce07b052e0c72f48b-3446x1716.png" style="width: 100%;" /><p><em>2단계: 에이전트를 실행할 위치 선택</em></p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/850d8c0d6b9e018408969aa05b67dbd865a26d30-1184x734.png" style="width: 100%;" /><p><em>3단계: Elasticsearch 엔드포인트와 인증 방법 입력하기</em></p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/0a472889a277caabb9ecd3fb9cd57d3b3eac0100-1184x734.png" style="width: 100%;" /><p><em>4단계: 에이전트를 설치하는 간단한 명령어</em></p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/6f9a7283469a321a9c472d655cea11d9d0ea7b1e-1600x883.png" style="width: 100%;" /><p><em>몇 분 후 AutoOps에서 인사이트를 표시하기 시작합니다.</em></p><p>자세한 내용은 <a href="https://www.elastic.co/docs/deploy-manage/monitor/autoops/cc-connect-self-managed-to-autoops">AutoOps 온보딩 문서와</a> <a href="https://www.elastic.co/docs/deploy-manage/monitor/autoops/cc-cloud-connect-autoops-faq">자주</a> 묻는 질문을 참조하세요.</p><h2>4장: 첫 번째 인사이트, 첫 번째 승리</h2><p>몇 분 만에 AutoOps는 인사이트를 드러내기 시작하여 근본 원인 분석과 이를 해결하기 위한 명확한 단계를 제공했습니다.</p><p>첫 주 하이라이트가 포함되어 있습니다:</p><ul><li>너무 커진 ILM 정책에 연결되지 않은 인덱스에 플래그 지정</li><li>한 클러스터에는 과거 유지 관리 작업으로 인해 빈 노드 3개가 남아있었습니다.</li><li>일부 노드가 워터마크를 교차하고 있었고, 몇 개의 인덱스에 복제본이 누락되었습니다.</li><li>잘못 구성된 템플릿 발견</li><li>장기간 실행되는 검색을 정확히 찾아내어 정확한 취소 명령을 제안합니다.</li></ul><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/f7d16355938ffcb00127deaf833d7a4d0acdb188-1600x894.png" style="width: 100%;" /><p><em>클러스터가 인덱싱을 거부하는 것을 자동 운영이 감지했습니다.</em></p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/9d6372bc489e9954e6f78d81047d7a25feef1703-1600x894.png" style="width: 100%;" /><p><em>일부 인덱스가 복제본 없이 구성된 것을 AutoOps가 감지했습니다</em><br/>AutoOps 이전에는 이러한 문제를 해결하기 위해 더 많은 하드웨어를 투입했을 것입니다. 대신 자동 운영은 근본 원인을 바로 찾아냈고 수정하는 데 몇 분밖에 걸리지 않았습니다.</p><p>모니터링 시스템이 단순히 차트를 보여주는 데 그치지 않고 문제를 해결하는 방법을 알려주었습니다. AutoOps가 우리 집 Wi-Fi를 진단하는 데도 도움이 될 수 있는지 궁금해지기 시작했고 마침내 가족의 IT 부서에서 벗어나게 되었습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/eac09f1d730955453cd79900002b100bc741ad97-1600x894.png" style="width: 100%;" /><p><em>자동 운영은 샤드 크기를 모니터링하고 빈 샤드가 많을 때 알림을 보냅니다.</em></p><h2>5장: 내가 보는 것을 보는 지원</h2><p>지원 케이스를 처음 열었을 때, 저는 또 다른 이점을 깨달았습니다. Elastic Support 엔지니어가 제가 보고 있던 것과 똑같은 데이터와 권장 사항을 볼 수 있다는 것이었습니다.</p><p>지원을 협업으로 전환했습니다. 티켓을 주고받는 대신 Elasticsearch를 속속들이 알고 있는 팀원과 함께 일하는 것 같은 느낌이 들었습니다.</p><h2>6장: 대규모 운영</h2><p>AutoOps 이전에는 Elasticsearch를 확장하는 것이 과학, 본능, 부족의 지식이 뒤섞인 것처럼 느껴졌습니다.</p><p>이제 명확한 가시성과 권장 사항을 갖춘 데이터 기반이 되었습니다:</p><ul><li>리소스 사용률에 대한 가시성을 확보하여 오버프로비저닝 방지</li><li>균형 잡힌 성능을 위한 더 스마트한 샤드 할당 및 계층화 권장 사항</li><li>낭비되는 스토리지 및 하드웨어 비용을 줄이는 인덱스 사이징 인사이트</li><li>여러 클러스터에서 더 빠른 근본 원인 분석</li></ul><h2>7장: 다양한 클라우드 커넥티드 서비스 중 첫 번째 서비스</h2><p>AutoOps는 독립형 도구 그 이상입니다. 이는 자체 관리형 고객을 위한 새로운 <a href="https://www.elastic.co/docs/deploy-manage/cloud-connect"><strong>Cloud 커넥티드 서비스</strong></a> 세트의 첫 번째 서비스로, 자체 관리형 클러스터는 자체 환경에서 이러한 서비스를 설치하고 관리하는 데 따른 운영 오버헤드 없이 Elastic Cloud 서비스를 사용할 수 있습니다. 기능이 자동으로 롤아웃되므로 팀은 인프라 복잡성을 줄이면서 더 빠르게 개선된 기능을 사용할 수 있습니다.</p><p>다음은 탄력적 추론 서비스(EIS).</p><h2>마무리 의견</h2><p>대규모 자체 관리형 배포를 관리하는 것이 부담스러울 필요는 없습니다.</p><p>더 간단한 작업을 원하신다면 언제든지 일부 워크로드를 호스팅이든 서버리스든 <a href="https://cloud.elastic.co/registration?pg=global&amp;plcmt=nav&amp;cta=205352-primary">Elastic Cloud로</a> 이동하여 가장 손쉽게 Elasticsearch를 실행할 수 있습니다.</p><p>자체 관리형 Elasticsearch를 계속 운영하고 싶다면, 어떤 자체 관리형 클러스터든 <a href="https://cloud.elastic.co/registration?onboarding_service_type=ccm-autoops">Elastic Cloud의 AutoOps</a>에 연결하세요. 모든 Elastic 라이선스에서 무료로 이용할 수 있습니다.</p><h2>TLDR</h2><p>대규모 자체 관리형 Elasticsearch 클러스터를 실행하는 것은 복잡하고 시간이 많이 걸립니다. AutoOps는 별도의 인프라를 관리할 필요 없이 Elastic Support를 통해 실시간 문제 탐색, 실행 가능한 권장 사항, 공유 가시성을 제공합니다. 몇 분이면 설정이 완료되고 인사이트가 바로 표시됩니다.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[첫 번째 Elastic 에이전트: 단일 쿼리에서 AI 기반 채팅까지]]></title>
            <description><![CDATA[Elastic의 AI 에이전트 빌더를 사용해 전문화된 AI 에이전트를 생성하는 방법을 알아보세요. 이 블로그에서는 금융 AI 에이전트를 구축하는 방법을 소개합니다.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/ai-agent-builder-elasticsearch</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/ai-agent-builder-elasticsearch</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Jeff Vestal]]></dc:creator>
            <pubDate>Thu, 25 Sep 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/ca853555eaa213f10f1db8c0ab0a2bbacee97b88-1456x816.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>Elastic의 새로운 <a href="https://www.elastic.co/search-labs/blog/ai-agentic-workflows-elastic-ai-agent-builder">에이전트 빌더를</a> 사용하면 특정 비즈니스 도메인의 전문가 역할을 하는 전문화된 AI 에이전트를 생성할 수 있습니다. 이 기능은 단순한 대시보드와 검색창을 넘어 데이터를 수동적인 리소스에서 능동적인 대화 파트너로 탈바꿈시킵니다.</p><p>고객과의 미팅 전에 정보를 빠르게 파악해야 하는 재무 관리자가 있다고 상상해 보세요. 이제 수동으로 뉴스 피드를 검색하고 포트폴리오 대시보드를 상호 참조하는 대신 맞춤형 상담원에게 직접 질문할 수 있습니다. 이것이 바로 &quot;채팅 우선&quot; 접근 방식의 장점입니다. 관리자는 데이터에 직접 대화할 수 있는 라인을 통해 &quot;ACME Corp의 최신 뉴스는 무엇이며 고객의 보유 자산에 어떤 영향을 미칩니까?&quot; 같은 질문을 할 수 있습니다. 검색하면 몇 초 만에 종합적인 전문가 답변을 얻을 수 있습니다.</p><p>오늘날 금융 전문가를 구축하는 과정에서 데이터만큼이나 응용 분야도 다양합니다. 동일한 권한으로 위협을 찾아내는 사이버 보안 분석가, 장애를 진단하는 사이트 안정성 엔지니어, 캠페인을 최적화하는 마케팅 관리자를 만들 수 있습니다. 도메인에 관계없이 핵심 미션은 동일합니다. 데이터를 전문가와 대화할 수 있는 데이터로 전환하는 것입니다.</p><h2>0단계: 데이터 세트</h2><p>현재 저희 데이터 세트는 금융 계좌, 자산 현황, 뉴스 및 재무 보고서로 구성된 합성 금융 기반 데이터 세트입니다. 합성 데이터 세트이긴 하지만, 실제 금융 데이터 세트를 단순화한 버전을 재현한 것입니다.</p><p><code>financial_accounts</code>: 위험 프로필이 있는 고객 포트폴리오</p><p><code>financial_holdings</code>: 매수 내역이 있는 주식/ETF/채권 포지션</p><p><code>financial_asset_details</code>: 주식/ETF/채권에 대한 세부 정보</p><p><code>financial_news</code>: 감정 분석을 통해 AI가 생성한 시장 기사</p><p><code>financial_reports</code>: 기업 실적 및 애널리스트 노트</p><p>이 데이터 세트는 <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/your-first-elastic-agent/Your_First_Elastic_Agent.ipynb">여기에</a> 있는 첨부된 노트북을 따라 직접 로드할 수 있습니다.</p><h2>1단계: 기초 - ES|QL로서의 비즈니스 로직</h2><p>모든 AI 기술은 탄탄한 로직에서 시작됩니다. 재무 관리자 상담원에게 일반적인 질문에 답하는 방법을 가르쳐야 합니다: &quot;시장 심리가 걱정됩니다. 어떤 고객이 나쁜 소식으로 가장 위험에 처해 있는지 보여 주시겠습니까?&quot; 이 질문은 단순한 검색을 넘어서는 질문입니다. 이를 위해서는 시장 심리와 고객 포트폴리오의 상관관계를 파악해야 합니다.</p><p>부정적인 기사에 언급된 자산을 찾고, 해당 자산을 보유한 모든 고객을 식별하고, 해당 자산의 현재 시장 가치를 계산한 다음, 그 결과를 순위화하여 위험도가 가장 높은 자산의 우선순위를 정해야 합니다. 이러한 복잡한 다중 조인 분석은 고급 ES|QL 도구에 완벽한 작업입니다.</p><p>다음은 우리가 사용할 전체 쿼리입니다. 인상적으로 보이지만 개념은 간단합니다.</p><pre><code>FROM financial_news, financial_reports METADATA _index
        | WHERE sentiment == "negative"
        | WHERE coalesce(published_date, report_date) >= NOW() - TO_TIMEDURATION(?time_duration)
        | RENAME primary_symbol AS symbol
        | LOOKUP JOIN financial_asset_details ON symbol
        | LOOKUP JOIN financial_holdings ON symbol
        | LOOKUP JOIN financial_accounts ON account_id
        | WHERE account_holder_name IS NOT NULL
        | EVAL position_current_value = quantity * current_price.price
        | RENAME title AS news_title
        | KEEP
            account_holder_name, symbol, asset_name, news_title,
            sentiment, position_current_value, quantity, current_price.price,
            published_date, report_date
        | SORT position_current_value DESC
        | LIMIT 50</code></pre><h2>분석: 조인 및 가드레일</h2><p>이 쿼리에는 상담원 빌더를 만드는 두 가지 중요한 개념이 작용하고 있습니다.</p><h3>1. 조회 조인</h3><p>수년 동안 Elasticsearch에서 가장 많이 요청된 기능 중 하나는 공통 키를 기반으로 서로 다른 인덱스의 데이터를 조인하는 기능이었습니다. 이제 ES|QL을 사용하면 <code>LOOKUP JOIN</code> 에서 가능합니다.</p><p>새 쿼리에서는 먼저 부정적인 뉴스를 자산 세부 정보에 연결한 다음, 해당 자산을 고객 보유 자산에 연결하고 마지막으로 고객의 계정 정보에 연결하는 세 개의 <code>LOOKUP JOIN</code> 연쇄를 수행합니다. 이렇게 하면 하나의 효율적인 쿼리에서 4개의 서로 다른 인덱스로부터 놀랍도록 풍부한 결과를 얻을 수 있습니다. 즉, 모든 데이터를 미리 하나의 거대한 인덱스로 비정규화할 필요 없이 서로 다른 데이터 집합을 결합하여 통찰력 있는 단일 답변을 만들 수 있습니다.</p><h3>2. LLM 가드레일로서의 매개변수</h3><p>쿼리가 <code>?time_duration</code> 을 사용하는 것을 알 수 있습니다. 이는 단순한 변수가 아니라 AI를 위한 보호 장치입니다. LLM(대규모 언어 모델)은 쿼리 생성에 탁월하지만, 데이터를 자유롭게 사용할 수 있도록 허용하면 비효율적이거나 심지어 잘못된 쿼리가 발생할 수도 있습니다.</p><p>매개변수화된 쿼리를 생성하여 전문가가 이미 정의한 테스트되고 효율적이며 올바른 비즈니스 로직 내에서 LLM이 작동하도록 강제합니다. 이는 개발자들이 수년 동안 검색 템플릿을 사용하여 애플리케이션에 쿼리 기능을 안전하게 노출해 온 방식과 유사합니다. 에이전트는 이번 주 &quot;이번 주&quot; 같은 사용자의 요청을 해석하여 <code>time_duration</code> 매개 변수를 채울 수 있지만, 반드시 쿼리 구조를 사용하여 답변을 얻어야 합니다. 이를 통해 유연성과 제어의 완벽한 균형을 이룰 수 있습니다.</p><p>궁극적으로 이 쿼리를 통해 데이터를 이해하는 전문가가 자신의 지식을 도구로 캡슐화할 수 있습니다. 그러면 다른 사람, 즉 AI 에이전트는 이 도구를 사용하여 근본적인 복잡성에 대해 아무것도 모른 채 단일 매개변수만 제공하면 상관관계가 있는 결과를 얻을 수 있습니다.</p><h2>2단계: 기술 - 쿼리를 재사용 가능한 도구로 전환하기</h2><p>ES|QL 쿼리는 <strong>도구로</strong> 등록하기 전까지는 텍스트에 불과합니다. 상담원 빌더에서 도구는 단순히 저장된 쿼리 그 이상의 의미로, AI 상담원이 이해하고 사용할 수 있는 &quot;스킬(&quot; )을 의미합니다. 마법 같은 것은 저희가 제공하는 <strong>자연어 설명에</strong> 있습니다. 이 설명은 사용자의 질문을 기본 쿼리 로직에 연결하는 다리 역할을 합니다. 방금 작성한 쿼리를 등록해 보겠습니다.</p><h3>UI 경로</h3><p>Kibana에서 도구를 만드는 것은 간단한 과정입니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/a29c53a73b99af5972273c51218ea9004a9b0abb-1600x812.png" style="width: 100%;" /><p>1. <strong>상담원으로</strong>이동합니다.</p><ul><li><strong> 도구 </strong>또는 <strong>도구 관리를</strong> 클릭하고 <strong>새 도구</strong> 버튼을 클릭합니다.</li></ul><p>2. 다음 세부 정보를 입력하여 양식을 작성합니다:</p><ul><li><strong>도구 ID:</strong> <code>find_client_exposure_to_negative_news</code></li></ul><p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; i. 도구의 고유 ID입니다.</p><ul><li><strong>설명:</strong> &quot;부정적인 뉴스에 노출된 고객 포트폴리오를 찾습니다. 이 도구는 최근 뉴스와 보고서에서 부정적인 감정을 검색하고 관련 자산을 식별한 후 해당 자산을 보유한 모든 고객을 찾아냅니다. 포지션의 현재 시장가 기준으로 정렬된 목록을 반환하여 잠재적 위험이 가장 높은 포지션을 강조 표시합니다.&quot;</li></ul><p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; i. LLM은 이를 읽고 이 도구가 작업에 적합한지 여부를 결정합니다.</p><ul><li><strong>레이블</strong>: <code>retrieval</code> 및 <code>risk-analysis</code></li></ul><p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 레이블은 여러 도구를 그룹화하는 데 사용됩니다.</p><ul><li><strong>구성으로 이동합니다:</strong> 1단계의 전체 ES|QL 쿼리 붙여넣기</li></ul><p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; i. 상담원이 사용할 검색은 다음과 같습니다.</p><p>3. <strong>쿼리에서 매개변수 유추를</strong> 클릭합니다. UI에서 <code>?time_duration</code> 을 자동으로 찾을 수 있습니다. 상담원(및 다른 사용자)이 목적을 이해하는 데 도움이 되도록 각각에 대한 간단한 설명을 추가하세요.</p><ul><li><code>time_duration</code>: 부정적인 뉴스를 다시 검색할 수 있는 기간입니다. 형식은 &quot;X 시간&quot; 기본값은 8760시간입니다.</li></ul><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/deb422d97863f78dbe08bfa2e3c708d1f75166ff-1600x938.png" style="width: 100%;" /><p>4. 테스트해 보세요!</p><ul><li>저장 &amp; 테스트를 클릭합니다.</li></ul><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/57e768b88327821e70bd616744822f98fa367362-732x136.png" style="width: 100%;" /><ul><li>쿼리가 예상대로 작동하는지 테스트할 수 있는 새로운 플라이아웃이 표시됩니다.</li></ul><p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; i. <code>time_duration</code> 에서 원하는 범위를 입력합니다. 여기서는 &quot;8760 시간&quot;을 사용하고 있습니다.</p><ul><li>&#x27;제출&#x27;을 클릭하고 모든 것이 정상적으로 진행되면 JSON 응답이 표시됩니다. 예상대로 작동하는지 확인하려면 아래로 스크롤하여 <code>values</code> 개체를 확인합니다. 여기에서 실제 일치하는 문서가 반환됩니다.</li></ul><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/7e0c5171a4f7ffdfc1830f1a05a9acb987870b75-1600x722.png" style="width: 100%;" /><p>5. 오른쪽 상단의 &#x27;X&#x27;를 클릭하여 테스트 플라이아웃을 닫습니다. 이제 새 도구가 목록에 표시되며 상담원에게 배정할 준비가 되었습니다.</p><h3>API 경로</h3><p>자동화를 선호하거나 프로그래밍 방식으로 도구를 관리해야 하는 개발자의 경우, 한 번의 API 호출로 동일한 결과를 얻을 수 있습니다. <code>POST</code> 요청을 <code>/api/agent_builder/tools</code> 엔드포인트에 도구의 정의와 함께 보내면 됩니다.</p><pre><code>POST kbn://api/agent_builder/tools
{
  "id": "find_client_exposure_to_negative_news",
  "type": "esql",
  "description": "Finds client portfolio exposure to negative news. This tool scans recent news and reports for negative sentiment, identifies the associated asset, and finds all clients holding that asset. It returns a list sorted by the current market value of the position to highlight the highest potential risk.",
  "configuration": {
    "query": """
        FROM financial_news, financial_reports METADATA _index
        | WHERE sentiment == "negative"
        | WHERE coalesce(published_date, report_date) >= NOW() - TO_TIMEDURATION(?time_duration)
        | RENAME primary_symbol AS symbol
        | LOOKUP JOIN financial_asset_details ON symbol
        | LOOKUP JOIN financial_holdings ON symbol
        | LOOKUP JOIN financial_accounts ON account_id
        | WHERE account_holder_name IS NOT NULL
        | EVAL position_current_value = quantity * current_price.price
        | RENAME title AS news_title
        | KEEP
            account_holder_name, symbol, asset_name, news_title,
            sentiment, position_current_value, quantity, current_price.price,
            published_date, report_date
        | SORT position_current_value DESC
        | LIMIT 50
      """,
    "params": {
      "time_duration": {
        "type": "keyword",
        "description": """The timeframe to search back for negative news. Format is "X hours" DEFAULT TO 8760 hours """
      }
    }
  },
  "tags": [
    "retrieval",
    "risk-analysis"
  ]
}</code></pre><h2>3단계: 두뇌 - 사용자 지정 상담원 만들기</h2><p>재사용 가능한 스킬(도구)을 만들었습니다. 이제 실제로 사용할 페르소나, 즉 <strong>에이전트를</strong> 만들어야 합니다. 에이전트는 LLM, 액세스 권한을 부여한 특정 도구 세트, 그리고 가장 중요한 것은 에이전트의 성격, 규칙 및 목적을 정의하는 구성 요소 역할을 하는 <strong>사용자 지정 지침</strong> 세트의 조합입니다.</p><h3>프롬프트의 기술</h3><p>신뢰할 수 있는 전문 상담원을 만드는 데 있어 가장 중요한 부분은 바로 프롬프트입니다. 잘 만들어진 지침 세트는 일반 챗봇과 집중력 있는 전문 비서의 차이점입니다. 여기에서 가드레일을 설정하고, 출력을 정의하고, 에이전트에게 임무를 부여할 수 있습니다.</p><p><code>Financial Manager</code> 에이전트의 경우 다음 프롬프트를 사용합니다.</p><pre><code>You are a specialized Data Intelligence Assistant for financial managers, designed to provide precise, data-driven insights from information stored in Elasticsearch.

**Your Core Mission:**
- Respond accurately and concisely to natural language queries from financial managers.
- Provide precise, objective, and actionable information derived solely from the Elasticsearch data at your disposal.
- Summarize key data points and trends based on user requests.

**Reasoning Framework:**
1.  **Understand:** Deconstruct the user's query to understand their core intent.
2.  **Plan:** Formulate a step-by-step plan to answer the question. If you are unsure about the data structure, use the available tools to explore the indices first.
3.  **Execute:** Use the available tools to execute your plan.
4.  **Synthesize:** Combine the information from all tool calls into a single, comprehensive, and easy-to-read answer.

**Key Directives and Constraints:**
- **If a user's request is ambiguous, ask clarifying questions before proceeding.**
- **DO NOT provide financial advice, recommendations, or predictions.** Your role is strictly informational and analytical.
- Stay strictly on topic with financial data queries.
- If you cannot answer a query, state that clearly and offer alternative ways you might help *within your data scope*.
- All numerical values should be formatted appropriately (e.g., currency, percentages).

**Output Format:**
- All responses must be formatted using **Markdown** for clarity.
- When presenting structured data, use Markdown tables, lists, or bolding.

**Start by greeting the financial manager and offering assistance.**</code></pre><p>이 프롬프트가 효과적인 이유를 자세히 알아보세요:</p><ul><li><strong>이는 정교한 페르소나를 정의합니다: </strong>첫 번째 줄은 상담원을 &quot;전문 데이터 인텔리전스 어시스턴트(&quot; )로 즉시 설정하여 전문적이고 유능한 분위기를 조성합니다.</li><li><strong>추론 프레임워크를 제공합니다: </strong>상담원에게 &quot;이해, 계획, 실행 및 종합,&quot; 표준 운영 절차를 제공하도록 지시합니다. 이를 통해 복잡한 다단계 질문을 처리하는 능력이 향상됩니다.</li><li><strong>대화형 대화를 촉진합니다: </strong>&quot; 명확한 질문을&quot; 하라는 지시는 상담원을 더욱 강력하게 만듭니다. 모호한 요청에 대한 잘못된 가정을 최소화하여 보다 정확한 답변으로 이어질 수 있습니다.</li></ul><h3>UI 경로</h3><p>1. <strong>상담원으로 이동</strong>합니다.</p><ul><li><strong> 도구 </strong>또는 <strong>도구 관리를</strong> 클릭하고 <strong>새 도구</strong> 버튼을 클릭합니다.</li></ul><p>2. 기본 세부 정보를 입력합니다:</p><ul><li><strong>상담원 ID:</strong> <code>financial_assistant</code>.</li><li><strong>지침을 따르세요: </strong>위의 프롬프트를 복사합니다.</li><li><strong>레이블</strong>: <code>Finance</code>.</li><li><strong>표시 이름:</strong> <code>Financial Assistant</code>.</li><li><strong>디스플레이 설명: </strong><code>An assistant for analyzing and understanding your financial data</code>.</li></ul><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/18ea73f1cae620129c0afa0e7ba9e2a3390224a7-1600x1189.png" style="width: 100%;" /><p>3. 3. 상단으로 돌아가서 <strong>도구를</strong> 클릭합니다.</p><ul><li><code>find_client_exposure_to_negative_news</code> 도구 옆의 확인란을 선택합니다.</li></ul><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/0c1e4ecbbd51d0dd10c6e861dbe9a9ccddeb35f6-1600x149.png" style="width: 100%;" /><p>4. <strong>저장을</strong> 클릭합니다.</p><h3>API 경로</h3><p><code>/api/agent_builder/agents</code> 엔드포인트에 <code>POST</code> 요청을 통해 똑같은 에이전트를 만들 수 있습니다. 요청 본문에는 ID, 이름, 설명, 전체 지침, 상담원이 사용할 수 있는 도구 목록 등 모든 동일한 정보가 포함됩니다.</p><pre><code>POST kbn://api/agent_builder/agents
    {
      "id": "financial_assistant",
      "name": "Financial Assistant",
      "description": "An assistant for analyzing and understanding your financial data",
      "labels": [
        "Finance"
      ],
      "avatar_color": "#16C5C0",
      "avatar_symbol": "💰",
      "configuration": {
        "instructions": """You are a specialized Data Intelligence Assistant for financial managers, designed to provide precise, data-driven insights from information stored in Elasticsearch.

**Your Core Mission:**
- Respond accurately and concisely to natural language queries from financial managers.
- Provide precise, objective, and actionable information derived solely from the Elasticsearch data at your disposal.
- Summarize key data points and trends based on user requests.

**Reasoning Framework:**
1.  **Understand:** Deconstruct the user's query to understand their core intent.
2.  **Plan:** Formulate a step-by-step plan to answer the question. If you are unsure about the data structure, use the available tools to explore the indices first.
3.  **Execute:** Use the available tools to execute your plan.
4.  **Synthesize:** Combine the information from all tool calls into a single, comprehensive, and easy-to-read answer.

**Key Directives and Constraints:**
- **If a user's request is ambiguous, ask clarifying questions before proceeding.**
- **DO NOT provide financial advice, recommendations, or predictions.** Your role is strictly informational and analytical.
- Stay strictly on topic with financial data queries.
- If you cannot answer a query, state that clearly and offer alternative ways you might help *within your data scope*.
- All numerical values should be formatted appropriately (e.g., currency, percentages).

**Output Format:**
- All responses must be formatted using **Markdown** for clarity.
- When presenting structured data, use Markdown tables, lists, or bolding.

**Start by greeting the financial manager and offering assistance.**
""",
        "tools": [
          {
            "tool_ids": [
              "platform.core.search",
              "platform.core.list_indices",
              "platform.core.get_index_mapping",
              "platform.core.get_document_by_id",
              "find_client_exposure_to_negative_news"
            ]
          }
        ]
      }
    }</code></pre><h2>4단계: 보상 - 대화 나누기</h2><p>저희는 비즈니스 로직을 도구에 캡슐화하고 에이전트에서 사용할 수 있는 &quot;브레인&quot; 을 준비했습니다. 이제 이 모든 것이 한데 어우러질 때입니다. 이제 전문 상담원을 통해 데이터와 채팅을 시작할 수 있습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/5414cb6b7c41365acb0356a8bfe1140751ffd8db-1600x1014.png" style="width: 100%;" /><h3>UI 경로</h3><ol><li>Kibana에서 <strong>에이전트로 </strong>이동합니다.</li><li>채팅 창의 오른쪽 하단에 있는 드롭다운을 사용하여 기본 <strong>Elastic AI 에이전트에서</strong> 새로 생성된 <strong>재무 지원 </strong>에이전트로 전환하세요.</li><li>상담원이 전문 도구를 사용할 수 있는 질문을 하세요:<ol><li><em>시장 심리가 걱정됩니다. 어떤 고객이 나쁜 소식으로 인해 가장 위험에 처해 있는지 보여주시겠어요?</em></li></ol></li></ol><p>잠시 후 상담원이 완벽한 형식의 완전한 답변을 반환합니다. LLM의 특성상 답변의 형식이 약간 다를 수 있지만 이번 실행에서는 상담원이 반환했습니다:</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/17b4ed43d279f9e53ee9fe3d482d0b2ec359a083-1600x1088.png" style="width: 100%;" /><h3>방금 무슨 일이 있었나요? 에이전트의 추론</h3><p>상담원은 &quot;&quot; 답을 알고 있었습니다. 작업에 가장 적합한 도구를 선택하는 데 중점을 둔 다단계 계획을 실행했습니다. 그 사고 과정을 살펴보세요:</p><ul><li><strong>의도를 확인했습니다:</strong> &quot; 위험&quot; 및 &quot;부정적인 뉴스,&quot; 같은 질문의 키워드와 <code>find_client_exposure_to_negative_news</code> 도구의 설명이 일치했습니다.</li><li><strong>계획을 실행했습니다:</strong> 요청에서 기간을 추출하여 해당 전문 도구로 <strong>한 번만 호출합니다</strong>.</li><li><strong>작업 위임:</strong> 그런 다음 도구가 연쇄 조인, 값 계산 및 정렬과 같은 무거운 작업을 모두 수행했습니다.</li><li><strong>결과 종합:</strong> 마지막으로 에이전트는 프롬프트의 규칙에 따라 도구의 원시 데이터를 명확하고 사람이 읽을 수 있는 요약으로 포맷했습니다.</li></ul><p>생각을 확장하여 더 자세히 살펴보면 추측만 할 필요는 없습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/6a4da9262d3f88c60bfd8f8bf9b67c3b84e961ba-1600x607.png" style="width: 100%;" /><h3>API 경로</h3><p>동일한 대화를 프로그래밍 방식으로 시작할 수 있습니다. 입력 질문을 <code>converse</code> API 엔드포인트로 보내면 되며, <code>financial_manager</code> 의 <code>agent_id</code> 을 지정해야 합니다.</p><pre><code>POST kbn://api/agent_builder/converse
{
  "input": "Show me our largest positions affected by negative news",
  "agent_id": "financial_assistant"
}</code></pre><h2>개발자용 API와 통합하기</h2><p>Kibana UI는 에이전트 구축과 관리를 위한 환상적이고 직관적인 환경을 제공하지만, 오늘 보신 모든 것을 프로그래밍 방식으로도 수행할 수 있습니다. 에이전트 빌더는 일련의 API를 기반으로 구축되었으므로 이 기능을 자체 애플리케이션, CI/CD 파이프라인 또는 자동화 스크립트에 직접 통합할 수 있습니다.</p><p>작업하게 될 세 가지 핵심 엔드포인트는 다음과 같습니다:</p><ul><li><strong><code>/api/agent_builder/tools</code></strong>: 상담원이 사용할 수 있는 재사용 가능한 스킬을 만들고, 나열하고, 관리하기 위한 엔드포인트입니다.</li><li><strong><code>/api/agent_builder/agents</code></strong>: 중요한 지침 및 도구 할당을 포함하여 상담원 페르소나를 정의하기 위한 엔드포인트입니다.</li><li><strong><code>/api/agent_builder/converse</code></strong>: 상담원과 상호작용하고, 대화를 시작하고, 답변을 얻기 위한 엔드포인트입니다.</li></ul><p>이 튜토리얼의 모든 단계를 수행하기 위해 이러한 API를 사용하는 방법에 대한 완전한 실습 과정을 보려면 여기 GitHub <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/your-first-elastic-agent/Your_First_Elastic_Agent.ipynb"></a> 리포지토리에서 함께 <strong>제공되는 Jupyter Notebook을 확인하세요.</strong></p><h2>결론: 빌드할 차례</h2><p>먼저 ES|QL 쿼리를 가져와 재사용 가능한 스킬로 변환하는 것으로 시작했습니다. 그런 다음 전문화된 AI 에이전트를 구축하여 명확한 미션과 규칙을 부여하고 해당 기술을 강화했습니다. 그 결과 복잡한 질문을 이해하고 다단계 분석을 실행하여 정확한 데이터 기반 답변을 제공할 수 있는 정교한 어시스턴트가 탄생했습니다.</p><p>이 워크플로는 Elastic의 새로운 <strong>에이전트 빌더의</strong> 핵심입니다. 기술 전문가가 아닌 사용자도 UI를 통해 에이전트를 만들 수 있을 만큼 간단하면서도 개발자가 API를 기반으로 맞춤형 AI 기반 애플리케이션을 구축할 수 있을 만큼 미묘한 차이가 있도록 설계되었습니다. 가장 중요한 것은 사용자가 정의한 전문 로직에 따라 LLM을 자신의 데이터에 안전하게 연결하고 데이터와 채팅할 수 있다는 점입니다.</p><h2>에이전트를 사용하여 데이터와 채팅할 준비가 되셨나요?</h2><p>배운 내용을 확고히 하는 가장 좋은 방법은 직접 손을 더럽히는 것입니다. <a href="https://www.elastic.co/training/elastic-ai-agents-mcp"><strong>무료 대화형 실습 워크숍에서</strong></a> 오늘 논의한 모든 내용을 직접 체험해 보세요. 전용 샌드박스 환경에서 이 전체 흐름과 그 이상을 체험할 수 있습니다.</p><p>향후 블로그에서는 <code>Financial Assistant</code> 에이전트와 상호 작용하는 독립형 애플리케이션을 사용하는 방법과 이 모든 것을 가능하게 하는 <strong>모델 컨텍스트 프로토콜(MCP)</strong> 에 대해 자세히 살펴보겠습니다. 그리고 별도의 블로그에서 에이전트 빌더의 에이전트2에이전트 또는 A2A 프로토콜 개발에 대한 지원에 대해 설명할 예정입니다.</p><p>앞으로도 계속 지켜봐 주시고, 행복한 구축이 되시길 바랍니다!</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Elasticsearch로 AI 에이전트 워크플로우 구축하기]]></title>
            <description><![CDATA[하이브리드 검색을 사용해 에이전트가 추론하고 행동하는 데 필요한 컨텍스트를 제공하는 AI 에이전트 워크플로우를 구축하기 위한 프레임워크를 제공하는 Elasticsearch의 새로운 AI 계층인 에이전트 빌더에 대해 알아보세요.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/ai-agentic-workflows-elastic-ai-agent-builder</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/ai-agentic-workflows-elastic-ai-agent-builder</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Anish Mathur, Dana Juratoni]]></dc:creator>
            <pubDate>Tue, 23 Sep 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/d5d04bbcfc3925f48f3487ea4c7e0dd2205316d0-720x420.jpg?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>Elastic에서는 AI 어시스턴트, 고급 RAG, 벡터 데이터베이스 개선을 통해 LLM과 대화형 인터페이스에 컨텍스트를 제공해 왔습니다. 최근 AI 에이전트의 등장으로 관련 컨텍스트에 대한 필요성이 커지고 있으며, 영향력이 큰<strong> AI 에이전트에는 뛰어난 검색 기능이 필요하다는</strong> 사실을 알게 되었습니다. 그래서 Elasticsearch에서 데이터를 활용하는 AI 에이전트를 개발하는 데 도움이 되도록 설계된 새로운 기본 기능을 Elastic Stack에 구축했습니다. 이 여정의 진행 상황과 앞으로의 계획을 공유하고자 합니다.</p><h2>에이전트 빌더: 데이터 기반 AI 에이전트 구축을 위한 토대</h2><p>AI 에이전트의 약속은 간단합니다. 목표를 부여하면 작업을 완료한다는 것입니다. 하지만 개발자에게 현실은 복잡한 도전의 연속입니다. 첫째, 상담원은 환경에 대한 인식과 사용자 목표를 달성하기 위해 주어진 도구에 대한 인식이 뛰어나야 합니다. 그렇다면 다양한 기업 데이터에서 올바른 컨텍스트를 제공하는 것은 엄청난 과제입니다. 마지막으로, 이 모든 것은 계획, 실행, 학습할 수 있는 신뢰할 수 있는 추론 루프를 통해 조율되어야 합니다.</p><p>이를 해결하기 위해 개발자는 복잡하고 깨지기 쉬운 스택을 처음부터 새로 구축해야 합니다. 오늘날의 에이전트 아키텍처는 LLM, 벡터 데이터베이스, 메타데이터 저장소, 로깅 및 추적을 위한 별도의 시스템, 그리고 이 모든 것이 제대로 작동하는지 평가하는 방법 등 여러 가지 이질적인 조각들을 하나로 연결해야 합니다. 이는 복잡할 뿐만 아니라 비용이 많이 들고 오류가 발생하기 쉬우며 사용자가 요구하는 고품질의 신뢰할 수 있는 AI 시스템을 구축하기 어렵게 만듭니다.</p><p>그래서 저희는 더 간단하게 만들고자 합니다. 이를 위해, 저희의 접근 방식은 효과적인 컨텍스트 기반 에이전트의 필수 요소를 가져와 <strong>Elastic AI 에이전트 빌더라는</strong> 새로운 기능 세트를 통해 Elasticsearch의 핵심에 직접 통합하는 것입니다. 이 새로운 계층은 개방형 기본 요소 세트, 표준 기반 프로토콜, 데이터에 대한 안전한 액세스 등 Elasticsearch 기반 AI 에이전트를 생성하기 위한 모든 필수 구성 요소를 갖춘 프레임워크를 제공하므로 실제 데이터와 요구 사항에 맞는 에이전트 시스템을 구축할 수 있습니다:</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/1ee1e73dd3f485ce86294d39490c98ce2a3d9925-1238x1072.png" style="width: 100%;" /><p><strong>AI 경험 제공</strong>: 이것이 궁극적인 목표입니다. 검색 AI 플랫폼과 데이터를 기반으로 사용자 지정 채팅 인터페이스부터 LangChain과 같은 에이전트 프레임워크 또는 Salesforce와 같은 비즈니스 애플리케이션과의 통합에 이르기까지 모든 유형의 생성형 AI 애플리케이션을 구축할 수 있습니다.</p><p><strong>에이전트 제공 &amp; 도구</strong>: 플랫폼 위에 깔끔하고 단순한 추상화 계층을 노출합니다. 특정 요구 사항에 맞게 사용자 지정할 수 있는 상담원 및 도구와 직접 상호 작용합니다. 또한 강력한 API와 MCP 및 A2A와 같은 개방형 표준을 통해 플랫폼의 기능에 액세스할 수도 있습니다.</p><p><strong>검색 AI 플랫폼에서 사용 가능</strong>: 이 플랫폼은 구성 요소를 통합한 핵심 엔진입니다. 고급 벡터 데이터베이스, 에이전트 로직, 쿼리 구성, 보안 기능, 평가를 위한 추적 등 모든 것이 여기에 있으며, Elastic에서 관리하고 최적화합니다.</p><p><strong>데이터의 힘 활용하기</strong>: 훌륭한 상담원의 기본은 훌륭한 데이터입니다. Atlassian 플랫폼은 모든 엔터프라이즈 데이터에 대한 수집 또는 연합 액세스 기능으로 시작됩니다.</p><h2>플랫폼 내 에이전트 구축</h2><p>검색 AI 플랫폼에 통합된 에이전트 빌더는 에이전트 개발을 위한 완벽한 프레임워크를 제공합니다. 프로덕션급 AI 시스템 구축 및 배포의 중요한 측면을 해결하도록 설계된 5가지 핵심 요소를 기반으로 구축되었습니다. 에이전트가 목표를 정의하고, 도구가 기능을 제공하며, 개방형 표준이 상호 운용성을 보장하고, 평가가 투명성을 제공하고, 보안이 신뢰를 제공하는 방식을 세분화해 보겠습니다.</p><h3>상담원</h3><p>에이전트는 이 새로운 Elasticsearch 계층에서 가장 높은 수준의 빌딩 블록입니다. 에이전트는 달성할 목표, 실행에 사용할 수 있는 도구 세트 및 작동할 수 있는 데이터 소스를 정의합니다. 상담원은 대화형 상호작용에만 국한되지 않고 전체 워크플로, 작업 자동화 또는 사용자 대면 경험을 강화할 수 있습니다.</p><p>쿼리가 상담원에게 전달되면 구조화된 주기를 따릅니다:</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/627ad1744b629bbe27359325702f40d97e40d1f4-704x852.png" style="width: 100%;" /><ol><li>사용자의 입력과 목표 해석</li><li>실행에 적합한 도구와 인수를 선택합니다.</li><li>도구의 응답에 대한 이유</li><li>결과를 반환할지 아니면 추가 도구 호출을 계속할지 결정하세요.</li></ol><p>Elastic은 이 주기의 오케스트레이션, 컨텍스트 및 실행을 처리합니다. 개발자는 목표, 도구, 데이터 등 에이전트가 수행해야 할 <em>작업을</em> 정의하는 데 집중하고, 시스템은 추론과 워크플로우가 수행되는 <em>방식을</em> 관리합니다.</p><p><em>기본 에이전트</em></p><p>이 플랫폼을 기반으로 구축된 첫 번째 에이전트는 Kibana의 기본 대화형 에이전트로, 데이터와 즉시 상호 작용할 수 있는 기능을 제공합니다. 바로 사용할 수 있는 환경을 제공하는 동시에 완벽하게 확장 가능하며, 추가 구성 없이도 데이터와 즉시 상호 작용할 수 있습니다.</p><p>새로운 채팅 사용자 환경을 통해 또는 API를 통해 Kibana에서 직접 이 환경과 상호 작용할 수 있습니다.</p><p>API를 통해 기본 상담원을 쿼리하려면 한 번만 호출하면 됩니다:</p><pre><code>POST kbn://api/agent_builder/converse
{
    "input": "what is our top portfolio account?"
}</code></pre><p>대화가 상태 저장되므로 conversation_id 를 사용하여 상담원과 계속 대화하거나 전체 대화 기록을 검색할 수 있습니다:</p><pre><code>POST kbn://api/agent_builder/converse
{
    "input": "What about the second top?",
    "conversation_id": "ec757c6c-c3ed-4a83-8e2c-756238f008bb"
}

## get the full conversation
GET kbn://api/agent_builder/conversations/ec757c6c-c3ed-4a83-8e2c-756238f008bb</code></pre><p><em>맞춤형 상담원</em></p><p>개발자는 간단한 API를 통해 자신만의 사용자 지정 에이전트를 만들 수도 있습니다. 에이전트는 지침, 도구 및 데이터 액세스를 캡슐화하여 맞춤형 추론 엔진을 생성합니다.</p><p>사용자 지정 상담원을 만드는 것은 API 호출 한 번으로 간단합니다. 아래 샘플에서는 &#x27;구성&#x27; 필드에 지침이나 사용 가능한 도구 등 모든 주요 세부 정보가 들어 있는 예시를 보여 줍니다:</p><pre><code>POST kbn://api/agent_builder/agents
{
  "id": "custom_agent",
  "name": "My Custom Agent",
  "description": "Description of the custom agent",
  "configuration": {
      "instructions": "You are a log expert specialising in ...",
      "tools": 
...
   }
}</code></pre><p>에이전트가 생성되면 바로 쿼리할 수 있습니다:</p><pre><code>POST kbn://api/agent_builder/converse
{
    "input": "What news about DIA?",
    "agent_id": "custom_agent"
}</code></pre><p>이 접근 방식은 에이전트를 처음부터 구축해야 하는 복잡한 시스템에서 단순하고 선언적인 비즈니스 로직 단위로 전환하여 지능형 자동화를 더 빠르게 제공할 수 있도록 합니다.</p><p>전문 에이전트를 처음부터 구축하는 방법에 대해 자세히 알아보려면 자세한 단계별 가이드를 참조하세요: <a href="https://www.elastic.co/search-labs/blog/ai-agent-builder-elasticsearch">첫 번째 Elastic 에이전트: 단일 쿼리에서 AI 기반 채팅까지</a>.</p><h3>도구</h3><p>에이전트가 <em>무엇을</em> 달성할지 정의한다면 도구는 <em>어떻게</em> 달성할지 정의합니다.</p><p>도구는 에이전트가 정보를 실행 및 검색하거나 작업을 수행할 수 있도록 특정 Elastic 핵심 기능을 노출합니다. 도구에는 인덱스 가져오기 또는 매핑 가져오기와 같은 핵심 기능이나 자연어에서 ES|QL로의 고급 기능과 같은 고급 기능이 포함될 수 있습니다.</p><p>Elasticsearch는 일반적인 요구 사항에 최적화된 기본 도구 세트와 함께 제공됩니다. 하지만 진정한 유연성은 나만의 유연성을 만드는 데서 비롯됩니다. 도구를 정의함으로써 어떤 쿼리, 인덱스 및 필드를 ES|QL을 통해 에이전트에 노출할지 정확히 결정하여 속도, 정확성 및 보안을 정밀하게 제어할 수 있습니다.</p><p>새 도구를 등록하는 것도 API 호출 한 번으로 간단하게 할 수 있습니다. 특정 금융 자산에 대한 뉴스를 찾기 위해 <a href="https://www.elastic.co/search-labs/blog/esql-timeline-of-improvements">Elasticsearch 쿼리 언어(ES|QL)</a> 를 활용하는 도구를 만들 수 있습니다:</p><pre><code>POST kbn://api/agent_builder/tools
{
  "id": "news_on_asset",
  "type": "esql",
  "description": "Find news and reports about a particular asset where ...",
  "configuration": {
    "query": "FROM financial_news, financial_reports | where MATCH(company_symbol, ?symbol) OR MATCH(entities, ?symbol) | limit 5",
    "params": {
      "symbol": {
        "type": "keyword",
        "description": "The asset symbol"
      }
    }
  ...
  }
...
}</code></pre><p>등록한 후에는 새 도구를 사용자 지정 상담원에게 할당하여 선별된 기능을 추론하고 필요할 때마다 호출할 수 있도록 할 수 있습니다.</p><p>저희는 고객의 고유한 데이터 및 비즈니스 도메인에 기반하여 에이전트를 범용 에이전트에서 도메인별 전문가로 전환하는 ES|QL과 같이 고객의 특정 요구에 맞는 맞춤형 도구를 만들 수 있는 플랫폼을 제공합니다.</p><h3>개방형 표준 및 상호 운용성</h3><p>Elasticsearch 에이전트와 도구는 개방형 표준 API를 통해 노출되므로 에이전트 프레임워크의 광범위한 에코시스템 내에서 기본 블록으로 쉽게 통합할 수 있습니다. 우리의 접근 방식은 간단합니다: 블랙박스를 사용하지 않습니다. Elastic의 핵심 강점인 검색을 보완적인 기능 및 기타 에이전트 시스템과 결합하여 사용할 수 있기를 바랍니다.</p><p>이를 가능하게 하기 위해 저희는 API, 새로운 프로토콜, 개방형 표준을 통해 역량을 노출하고 있습니다.</p><p><em>모델 컨텍스트 프로토콜(MCP)</em></p><p><a href="https://www.elastic.co/search-labs/blog/model-context-protocol-elasticsearch">MCP(모델 컨텍스트 프로토콜)</a> 는 시스템 간 도구 연결을 위한 개방형 표준으로 빠르게 자리 잡고 있습니다. MCP를 지원함으로써 Elasticsearch는 대화형 AI를 데이터베이스, 인덱스 및 외부 API에 연결할 수 있습니다. Elastic Stack에 내장된 원격 MCP 서버를 통해 모든 MCP 호환 클라이언트는 Elastic의 도구에 액세스하고 이를 대규모 에이전트 워크플로우의 빌딩 블록으로 사용할 수 있습니다.</p><p>이것은 일방통행이 아닙니다. 또한 외부 MCP 서버에서 도구를 가져와서 Elasticsearch 내에서 사용할 수 있게 할 수도 있습니다. 곧 MCP 서버는 거의 모든 용도로 사용할 수 있게 될 것이며, 우리가 직접 만드는 것보다 훨씬 더 포괄적인 서버가 될 것입니다. Elastic은 대규모 검색 및 검색 기능을 제공하며, 이를 다른 플랫폼의 전문 기능과 결합하여 효과적인 에이전트를 구축할 수 있습니다.</p><p><em>에이전트 간(A2A)</em></p><p>또한 에이전트 간(A2A) 지원도 준비 중입니다. MCP가 툴을 연결하는 것이라면 A2A는 에이전트를 연결하는 것이 핵심입니다. A2A 서버를 사용하면 구축한 Elastic 에이전트가 다른 시스템의 에이전트와 직접 대화하여 컨텍스트를 공유하고, 작업을 위임하고, 워크플로우를 조정할 수 있습니다.</p><p>추론 계층에서의 상호 운용성이라고 생각하면 됩니다. Elastic 에이전트가 검색 및 검색을 처리한 다음 전문 지원팀이나 IT 에이전트에게 작업을 넘겨주고 결과를 원활하게 돌려받을 수 있습니다. 그 결과 각자가 가장 잘하는 일을 하는 협력 에이전트로 구성된 생태계가 탄생했습니다.</p><p>궁극적으로 MCP와 A2A를 채택함으로써, 더 광범위한 에이전트 에코시스템 전반에 걸쳐 개방형 통합을 보장하는 일류 시민으로서 Elasticsearch의 역할에 대한 우리의 약속을 강화할 수 있게 되었습니다.</p><h3>추적 및 평가</h3><p>검색이 상담원과 통합됨에 따라 효과적인 평가라는 과제가 중요해졌습니다. 실제 기업 환경에서 자신 있게 에이전트를 배포하려면 정확할 뿐만 아니라 효율적이고 신뢰할 수 있다는 확신이 있어야 합니다. 성능을 측정하고, 잘못된 응답을 진단하거나, 기준선을 개선하려면 어떻게 해야 하나요? 모든 것은 가시성에서 시작됩니다.</p><p>이것이 바로 처음부터 투명성을 위해 상담원 API를 설계한 이유입니다. 이 간단한 상담원 상호 작용을 생각해 보세요:</p><pre><code>POST kbn://api/agent_builder/converse
{
    "input": "what is our top portfolio account?"
}</code></pre><p>응답에는 최종 답변뿐만 아니라 상담원이 선택한 도구, 사용한 매개변수 및 각 단계의 결과를 자세히 설명하는 전체 실행 추적이 포함됩니다.</p><pre><code>{
  "conversation_id": "db5c0c8b-12bf-4928-a57e-d99129ad2fea",
  "steps": [
    {
      "type": "tool_call",
      "tool_call_id": "tooluse_Nfqr3mwtR92HTRIsTcGXZQ",
      "tool_id": ".index_explorer",
      "params": {
        "query": "indices containing portfolio data"
      },
      "results": [...]
    }
    // ... more steps ...
  ],
  "response": {
    "message": "Based on the information I've gathered...."
  }
}</code></pre><p>포괄적인 추적과 로깅은 지속적인 개선 루프에 필수적이며, 곧 이러한 에이전트 추적을 Elasticsearch에 직접 저장하고 볼 수 있게 됩니다. 더 좋은 점은 이러한 추적이 OpenTelemetry 프로토콜을 기반으로 구축되어 표준화되고 원하는 통합 가시성 플랫폼과 통합할 수 있도록 이식성이 보장된다는 것입니다.</p><p>이러한 수준의 세부 사항은 진정한 지속적인 개선 루프의 토대입니다. 포괄적인 테스트 제품군을 구축하고, 실패를 디버그하고, 실패 모드를 식별하여 회귀를 방지하고, 성공 패턴을 캡처하여 성능을 미세 조정할 수 있습니다. 궁극적으로 이러한 데이터 중심 접근 방식은 유망한 프로토타입을 생산 등급의 신뢰할 수 있는 AI 시스템으로 전환하는 데 핵심적인 역할을 합니다.</p><h3>보안</h3><p>에이전트와 툴의 기능이 향상됨에 따라 보안은 선택 사항이 아니라 기본이 되었습니다. API를 노출하고, 작업을 자동화하고, 워크플로우를 자동화하려면 엔터프라이즈 시스템을 신뢰할 수 있어야 합니다. 특히 상담원이 더 많은 워크플로를 자동화하기 시작하면서 이러한 워크플로를 보호하고 기업의 요구 사항을 충족할 수 있는 기능이 필수적입니다.</p><p>무엇보다도 이 기능은 API 호출을 위한 <a href="https://www.elastic.co/search-labs/blog/rag-and-rbac-integration">역할 기반 액세스 제어(RBAC)</a> 와 API 키 관리를 포함해 현재 Elastic에서 이미 사용 가능한 제어 기능을 그대로 계승합니다. 또한 MCP와 같은 새로운 프로토콜에도 동일한 제어 기능을 확장하고 있습니다. 즉, OAuth와 같은 표준을 지원할 뿐만 아니라 사용자 지정 인증 메커니즘을 연결할 수 있습니다.</p><p>저희의 목표는 조직이 요구하는 보안, 규정 준수 및 거버넌스 수준을 유지하면서 에이전트와 도구를 유연하게 실험할 수 있도록 하는 것입니다.</p><h2>다음 단계</h2><p>단순히 기능만 추가하는 것이 아니라 에이전트 컨텍스트 엔지니어링을 위해 Elasticsearch를 확장하고 있습니다. 앞으로도 이러한 원칙에 따라 발전해 나갈 계획입니다:</p><p>1. 오픈 소스 &amp; 표준에 대한 약속</p><p>오픈 소스 및 개방형 표준에 대한 당사의 노력은 이러한 기능이 외부 에이전트 프레임워크와 상호 운용성을 유지하도록 보장합니다. 데이터와 워크플로우를 항상 제어하면서 에코시스템 전반에서 에이전트를 연결, 확장 및 구성할 수 있습니다.</p><p>2. 컨텍스트의 가치</p><p>AI 에이전트의 가장 큰 자산은 컨텍스트입니다. 상담원이 검색 및 워크플로 작업을 수행할 때 컨텍스트를 관리하는 것은 어려운 작업일 수 있습니다. 저희는 Elastic의 핵심 강점을 활용하여 컨텍스트 엔지니어링을 해결함으로써 상담원이 항상 가장 관련성 높은 정보를 사용할 수 있도록 보장하고 있습니다.</p><p>3. 에이전트 데이터 스트림에 집중</p><p>앞으로 상담원은 상담원의 출력물(생성된 문서, 보고서, 시각화)과 상담원의 실행 추적(사고, 도구 호출, 메모리/컨텍스트)을 포함하여 점점 더 큰 데이터 소스가 될 것입니다. Elastic은 이러한 유형의 데이터를 처리하는 데 매우 적합하며, 이러한 데이터를 사용하여 분석, 평가 및 자동화된 개선 작업을 수행하는 것과 관련된 연구를 진행하고 있습니다.</p><p>4. 설계를 통한 보안 및 안전</p><p>AI 에이전트는 완전히 새로운 보안 및 안전 문제를 야기합니다. Elastic은 항상 보안 솔루션의 리더로서 엔터프라이즈급 가드레일, 액세스 제어, &quot;제로 트러스트&quot; 원칙을 지속적으로 구축해 왔습니다.</p><p>5. 플랫폼에 내장</p><p>AI 에이전트를 구축하기 위한 기능은 Elasticsearch 플랫폼에 내장되어 있습니다. 즉, 추적, 평가, 시각화 및 분석과 같은 플랫폼 수준의 기능을 모두 상담원에게 적용할 수 있습니다. 에이전트 실행을 기반으로 대시보드를 개발하려는 경우 - 이 기능이 기본으로 제공됩니다. 감정 분석을 사용하여 AI 상담원의 성과를 평가하고 싶다면 이 플랫폼을 통해 가능합니다. 이를 통해 AI 경험을 중심으로 완전한 라이프사이클을 구축할 수 있습니다.</p><p>Elastic의 목표는 완전히 통합되고 확장 가능하며 데이터에 기반한 대화형 AI와 자동화된 워크플로우를 구축할 수 있는 인터페이스를 제공하는 것입니다. 자세한 기술적 세부 사항과 진행 상황은 곧 공유될 예정입니다.</p><p>상담원 빌더는 현재 비공개 미리 보기로 제공됩니다. 액세스 권한을 요청하려면 <a href="https://www.elastic.co/contact?pg=global&amp;plcmt=nav&amp;cta=205352">당사에 문의</a> 하세요. 질문이나 피드백이 있으신가요? <a href="https://elasticstack.slack.com/archives/C09GRHEQ4AG"><strong>Slack 워크스페이스</strong></a> 또는 <a href="https://discuss.elastic.co/c/search/84"><strong>토론 포럼에서</strong></a> 개발자 커뮤니티와 소통하세요.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[코드로서의 Elastic Open Web Crawler]]></title>
            <description><![CDATA[변경 사항을 리포지토리에 푸시할 때마다 배포된 크롤러 인스턴스에 변경 사항이 자동으로 적용되도록 GitHub Actions를 사용하여 Elastic Open Crawler 구성을 관리하는 방법에 대해 알아보세요.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/elastic-open-crawler-config-as-code</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/elastic-open-crawler-config-as-code</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Gustavo Llermaly]]></dc:creator>
            <pubDate>Mon, 22 Sep 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/0ac41f55c85ad3f647cb0e0d750ed80bacd397f3-1036x581.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p><a href="https://github.com/elastic/crawler">Elastic Open Web Crawler와</a> CLI 기반 아키텍처를 사용하면 이제 버전이 관리되는 크롤러 구성과 로컬 테스트가 포함된 CI/CD 파이프라인을 매우 간단하게 구축할 수 있습니다.</p><p>기존에는 크롤러 관리가 수작업으로 이루어졌고 오류가 발생하기 쉬운 프로세스였습니다. UI에서 직접 구성을 편집하고 크롤링 구성 복제, 롤백, 버전 관리 등으로 어려움을 겪어야 했습니다. 크롤러 구성을 코드로 취급하면 소프트웨어 개발에서 기대하는 반복성, 추적성 및 자동화와 같은 이점을 제공함으로써 이 문제를 해결할 수 있습니다.</p><p>이 워크플로우를 사용하면 Elastic Web Crawler나 App Search Crawler와 같은 이전 Elastic Crawler에서는 훨씬 더 까다로웠던 롤백, 백업, 마이그레이션 작업을 위해 CI/CD 파이프라인에 Open Web Crawler를 더 쉽게 도입할 수 있습니다.</p><p>이 글에서는 그 방법을 알아보겠습니다:</p><ul><li>GitHub를 사용하여 크롤링 구성 관리하기</li><li>배포하기 전에 파이프라인을 테스트할 수 있는 로컬 설정이 있습니다.</li><li>메인 브랜치에 변경 사항을 푸시할 때마다 새로운 설정으로 웹 크롤러를 실행하도록 프로덕션 설정을 만듭니다.</li></ul><p>프로젝트 리포지토리는 <a href="https://github.com/llermaly/elastic-open-crawler-as-code"><strong><em>여기에서</em></strong></a>찾을 수 있습니다<em><strong>. </strong>이 글을 쓰는 현재, 저는 Elasticsearch 9.1.3과 Open Web Crawler 0.4.2를 사용하고 있습니다.</em></p><h2>필수 구성 요소</h2><ul><li>Docker 데스크톱</li><li>Elasticsearch 인스턴스</li><li>SSH 액세스 권한이 있는 가상 머신(예: AWS EC2) 및 Docker가 설치된 가상 머신</li></ul><h2>단계</h2><ol><li>폴더 구조</li><li>크롤러 구성</li><li>Docker-작성 파일(로컬 환경)</li><li>깃허브 액션</li><li>로컬 테스트</li><li>프로덕션에 배포</li><li>변경 및 다시 배포</li></ol><h2>폴더 구조</h2><p>이 프로젝트의 파일 구조는 다음과 같습니다:</p><pre><code>├── docker-compose.yml # Local elasticsearch + crawler
├── config/crawler-config.yml # Crawler config
├── .github/workflows/deploy.yml # GH Action to deploy changes
├── local.sh # Script to run our local crawler</code></pre><h2>크롤러 구성</h2><p><code>crawler-config.yml,</code> 아래에 다음을 입력합니다:</p><pre><code>output_sink: elasticsearch
output_index: web-crawl-index
max_crawl_depth: 1

elasticsearch:
  host: ${ES_HOST}
  api_key: ${ES_API_KEY}
     
domains:
  - url: https://web-scraping.dev
    seed_urls:
      - https://web-scraping.dev/product/1
      - https://web-scraping.dev/product/2
      - https://web-scraping.dev/product/3</code></pre><p>제품 모의 <a href="https://web-scraping.dev/products">사이트인 https://web-scraping.dev/products</a> 에서 크롤링됩니다. 처음 세 개의 제품 페이지만 크롤링합니다. <code>max_crawl_depth</code> 설정은 크롤러가 <code>seed_urls</code> 으로 정의된 페이지보다 더 많은 페이지를 발견하지 못하도록 하여 그 안의 링크를 열지 않도록 합니다.</p><p>Elasticsearch <code>host</code> 및 <code>api_key</code> 는 스크립트를 실행하는 환경에 따라 동적으로 채워집니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/f9831034e1c4ccb554d37bdd188f2824338355a0-890x624.png" style="width: 100%;" /><h2>Docker-작성 파일(로컬 환경)</h2><p>로컬 <code>docker-compose.yml,</code> 의 경우, 프로덕션에 배포하기 <strong><em>전에</em></strong> 크롤링 결과를 쉽게 시각화할 수 있도록 크롤러와 단일 Elasticsearch 클러스터 + Kibana를 배포합니다.</p><pre><code>services:
  es01:
    image: docker.elastic.co/elasticsearch/elasticsearch:9.1.3
    environment:
      - discovery.type=single-node
      - xpack.security.enabled=false
      - ES_JAVA_OPTS=-Xms1g -Xmx1g
    ports:
      - "9200:9200"
    networks: [esnet]
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:9200"]
      interval: 5s
      timeout: 5s
      retries: 10

  kibana:
    image: docker.elastic.co/kibana/kibana:9.1.3
    environment:
      - ELASTICSEARCH_HOSTS=http://es01:9200
    ports:
      - "5601:5601"
    networks: [esnet]
    depends_on: [es01]

  crawler:
    image: docker.elastic.co/integrations/crawler:0.4.2
    environment:
      - ES_HOST=http://es01:9200
      - CRAWLER_JRUBY_OPTS=--server
    container_name: crawler
    volumes:
      - ./config:/home/app/config
    networks: [esnet]
    entrypoint: ["/home/app/bin/crawler", "crawl", "/home/app/config/crawl-config-final.yml"]
    stdin_open: true
    tty: true

networks:
  esnet:
    driver: bridge</code></pre><p>크롤러가 Elasticsearch를 실행할 준비가 될 때까지 기다리는 방식에 주목하세요.</p><h2>깃허브 액션</h2><p>이제 새 설정을 복사하고 메인으로 푸시할 때마다 가상 머신에서 크롤러를 실행하는 GitHub 액션을 만들어야 합니다. 이렇게 하면 가상 머신에 수동으로 들어가서 파일을 업데이트하고 크롤러를 실행할 필요 없이 항상 최신 구성이 배포되어 있습니다. 가상 머신 공급자로 AWS EC2를 사용하겠습니다.</p><p>첫 번째 단계는 호스트(<code>VM_HOST</code>), 머신 사용자(<code>VM_USER</code>), SSH RSA 키(<code>VM_KEY</code>), Elasticsearch 호스트(<code>ES_HOST</code>), Elasticsearch API 키(<code>ES_API_KEY</code>)를 GitHub Action 시크릿에 추가하는 것입니다:</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/e1075bc54151b4b94eac2a6bd2682e9997e6c709-1106x707.png" style="width: 100%;" /><p>이렇게 하면 액션이 서버에 액세스하여 새 파일을 복사하고 크롤링을 실행할 수 있습니다.</p><p>이제 <code>.github/workflows/deploy.yml</code> 파일을 만들어 보겠습니다:</p><pre><code>name: Deploy

on:
  push:
    branches: [main]

jobs:
  Deploy:
    name: Deploy to EC2
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v5

      - name: Deploy crawler
        env:
          HOSTNAME: ${{ secrets.VM_HOST }}
          USER_NAME: ${{ secrets.VM_USER }}
          PRIVATE_KEY: ${{ secrets.VM_KEY }}
          ES_HOST: ${{ secrets.ES_HOST }}
          ES_API_KEY: ${{ secrets.ES_API_KEY }}
        run: |
          # Save private key
          echo "$PRIVATE_KEY" > private_key
          chmod 600 private_key

          # Generate final config locally
          envsubst < config/crawler-config.yml > config/crawl-config-final.yml

          # Copy the config folder to VM
          scp -o StrictHostKeyChecking=no -i private_key -r config ${USER_NAME}@${HOSTNAME}:~/config

          # SSH into VM and run crawler
          ssh -o StrictHostKeyChecking=no -i private_key ${USER_NAME}@${HOSTNAME} << EOF
            docker run --rm \
              -v ~/config:/config \
              docker.elastic.co/integrations/crawler:latest jruby \
              bin/crawler crawl /config/crawl-config-final.yml
          EOF</code></pre><p>이 작업은 크롤러 구성 파일에 변경 사항을 푸시할 때마다 다음 단계를 실행합니다:</p><ol><li>yml 구성에서 Elasticsearch 호스트 및 API 키 채우기</li><li>구성 폴더를 VM에 복사합니다.</li><li>SSH를 통해 VM에 연결</li><li>리포지토리에서 방금 복사한 구성으로 크롤링을 실행합니다.</li></ol><h2>로컬 테스트</h2><p>로컬에서 크롤러를 테스트하기 위해, Docker의 로컬 호스트에 Elasticsearch 호스트를 채우고 크롤링을 시작하는 bash 스크립트를 만들었습니다. <code>./local.sh</code> 을 실행하여 실행할 수 있습니다.</p><pre><code>#!/bin/bash

# Exit on any error
set -e

# Load environment variables
export ES_HOST="http://es01:9200"

# Generate final crawler config
envsubst < ./config/crawler-config.yml > ./config/crawl-config-final.yml

# Bring everything up
docker compose up --build</code></pre><p>Kibana 개발자 도구를 살펴보고<code> web-crawler-index</code> 이 올바르게 채워졌는지 확인해 보겠습니다:</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/18551635e8265866e389a9632c4e4540958e4468-990x723.png" style="width: 100%;" /><h2>프로덕션에 배포</h2><p>이제 가상 머신에 크롤러를 배포하고 서버리스 Elasticsearch 인스턴스로 로그 전송을 시작하는 메인 브랜치로 푸시할 준비가 되었습니다.</p><pre><code>git add .
git commit -m "First commit"
git push</code></pre><p>그러면 가상 머신 내에서 배포 스크립트를 실행하고 크롤링을 시작하는 GitHub 액션이 트리거됩니다.</p><p>GitHub 리포지토리로 이동하여 &quot;작업&quot; 탭을 방문하면 작업이 실행되었는지 확인할 수 있습니다:</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/67ba3a7164d7a8049fe5661264820826cb18ed64-667x325.png" style="width: 100%;" /><h2>변경 및 다시 배포</h2><p>눈에 띄는 점은 각 제품의 <code>price</code> 이 문서 본문 필드의 일부라는 점입니다. 가격을 별도의 필드에 저장하여 필터를 실행할 수 있도록 하는 것이 가장 이상적입니다.</p><p><code>crawler.yml</code> 파일에 이 변경 사항을 추가하여 <a href="https://github.com/elastic/crawler/blob/main/docs/features/EXTRACTION_RULES.md">추출 규칙을</a> 사용하여 <code>product-price</code> CSS 클래스에서 가격을 추출해 보겠습니다:</p><pre><code>output_sink: elasticsearch
output_index: web-crawl-index
max_crawl_depth: 1

elasticsearch:
  host: ${ES_HOST}
  api_key: ${ES_API_KEY}
     
  # Index ingest pipeline to process documents before indexing          
  pipeline_enabled: true
  pipeline: pricing-pipeline

domains:
  - url: https://web-scraping.dev
    seed_urls:
      - https://web-scraping.dev/product/1
      - https://web-scraping.dev/product/2
      - https://web-scraping.dev/product/3
    extraction_rulesets:
      - url_filters:
          - type: ends
            pattern: /product/*
        rules:
          - action: extract
            field_name: price
            selector: .product-price
            join_as: string
            source: html</code></pre><p>또한 가격에는 달러 기호(<code>$</code>)가 포함되어 있으며, 범위 쿼리를 실행하려면 이 기호를 제거해야 합니다. 이를 위해 수집 파이프라인을 사용할 수 있습니다. 위의 새 크롤러 구성 파일에서 이를 참조하고 있습니다:</p><pre><code>PUT _ingest/pipeline/pricing-pipeline
{
  "processors": [
    {
      "script": {
        "source": """
                ctx['price'] = ctx['price'].replace("$","")
            """
      }
    }
  ]
}</code></pre><p>프로덕션 Elasticsearch 클러스터에서 해당 명령을 실행할 수 있습니다. 개발의 경우, 임시적이므로 다음 서비스를 추가하여 <code>docker-compose.yml</code> 파일에 파이프라인 생성 부분을 만들 수 있습니다. 또한 크롤러 서비스에 <code>depends_on</code> 을 추가하여 파이프라인이 성공적으로 생성된 후에 시작되도록 했습니다.</p><pre><code> crawler:
    image: docker.elastic.co/integrations/crawler:0.4.2
    environment:
      - ES_HOST=http://es01:9200
      - CRAWLER_JRUBY_OPTS=--server
    container_name: crawler
    volumes:
      - ./config:/home/app/config
    networks: [esnet]
    entrypoint: ["/home/app/bin/crawler", "crawl", "/home/app/config/crawl-config-final.yml"]
    depends_on:
      pipeline-init:
        condition: service_completed_successfully
    stdin_open: true
    tty: true  


  pipeline-init:
    image: curlimages/curl:latest
    depends_on:
      es01:
        condition: service_healthy
    networks: [esnet]
    entrypoint: >
        sh -c "
        echo 'Creating ingest pipeline...';
        curl -s -X PUT http://es01:9200/_ingest/pipeline/pricing-pipeline \\
          -H 'Content-Type: application/json' \\
          -d '{\"processors\":[{\"script\":{\"source\":\"ctx.price = ctx.price.replace(\\\"$\\\", \\\"\\\")\"}}]}';
        echo 'Pipeline created!';
        "</code></pre><p>이제 <code>`./local.sh`</code> 을 실행하여 로컬에서 변경 사항을 확인해 보겠습니다:</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/dc1801599344a9f69f072b07ff828c4ba3815d7b-738x473.png" style="width: 100%;" /><p>훌륭합니다! 이제 변경 사항을 적용해 보겠습니다:</p><pre><code>git add crawler-config.yml
git commit -m "added price CSS selector"
git push</code></pre><p>모든 것이 제대로 작동하는지 확인하려면 프로덕션 Kibana를 확인하면 변경 사항이 반영되어 달러 기호가 없는 새 필드로 가격이 표시되어야 합니다.</p><h2>결론</h2><p>Elastic Open Web Crawler를 사용하면 크롤러를 코드로 관리할 수 있으므로 개발부터 배포까지 전체 파이프라인을 자동화하고 임시 로컬 환경을 추가하고 크롤링된 데이터에 대해 프로그래밍 방식으로 테스트하는 등 몇 가지 예를 들 수 있습니다.</p><p>공식 리포지토리를 복제하고 이 워크플로우를 사용하여 자체 데이터 색인화를 시작할 수 있습니다. <a href="https://www.elastic.co/search-labs/blog/semantic-search-open-crawler">이 문서에서</a> 크롤러가 생성한 인덱스에 대해 시맨틱 검색을 실행하는 방법을 알아볼 수도 있습니다.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Elasticsearch의 ES|QL 편집기 환경과 OpenSearch의 PPL 이벤트 분석기 비교]]></title>
            <description><![CDATA[ES|QL 에디터의 고급 기능이 어떻게 워크플로우를 가속화하는지, OpenSearch의 PPL 이벤트 분석기의 수동 방식과 직접 비교하여 알아보세요. 
]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/opensearch-vs-elasticsearch-ppl-esql</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/opensearch-vs-elasticsearch-ppl-esql</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Libby Lin, George Kobar]]></dc:creator>
            <pubDate>Thu, 18 Sep 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/f1ff4ff4a00b3e5b084d4116cea6cabc82a2d816-720x420.jpg?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>8.14부터 정식 버전으로 제공되는 <a href="https://www.elastic.co/blog/getting-started-elasticsearch-query-language">Elasticsearch 쿼리 언어</a> (ES|QL)는 검색, 통합 가시성 및 보안 조사를 위해 특별히 설계된 쿼리 언어와 엔진을 도입합니다. 기존 파이프 언어에서 많은 부분을 차용한 OpenSearch의 파이프 처리 언어(PPL)와 달리, ES|QL은 처음부터 세련미, 사용성, Kibana 플랫폼 전반의 원활한 통합에 중점을 두고 구축되었습니다.</p><p>이 블로그에서는 Elasticsearch 9.1의 ES|QL 편집기의 개발자 환경을 OpenSearch 3.2의 이벤트 분석기(줄여서 PPL)와 비교하여 살펴보겠습니다.</p><p>ES|QL 편집기는 지능형 자동 완성, 상황에 맞는 도움말, 추천 쿼리 및 클러스터 간 쿼리 지원을 제공하여 초급 사용자뿐만 아니라 전문가 수준의 사용자도 역량을 강화할 수 있습니다. ES|QL 저작을 위한 사려 깊은 설계는 예를 들어 최근 쿼리와 같은 통합 쿼리 검사 및 Kibana 워크플로우를 통한 전체적인 통합에서 더욱 잘 드러납니다.</p><p>반면 PPL은 자동 완성, 문맥 안내 및 분산 쿼리에 대한 지원이 부족하여 학습 곡선이 가파르고 시행착오가 더 많이 발생합니다.</p><h2>ES|QL을 더 쉽게 배우고 사용하기</h2><p>새로운 쿼리 언어를 시작하는 것은 종종 부담스럽게 느껴질 수 있습니다. <strong>Kibana Discover에</strong>직접<strong> </strong>내장된 ES|QL 편집기는 쿼리 생성 및 디버깅을 지원할 뿐만 아니라 사용자가 언어에 익숙해지고 편안해지는 속도를 가속화하여 이러한 프로세스를 간소화하도록 설계되었습니다. 에디터가 일상적인 작업의 마찰을 줄여주므로 구문과 시행착오에서 벗어나 문제 해결에 집중할 수 있습니다. 이러한 원칙과 이를 에디터에 통합한 방법에 대한 자세한 내용은 <a href="https://www.elastic.co/search-labs/blog/improving-esql-editor-experience-in-kibana">여기에서</a> 확인할 수 있습니다.</p><p>이 편집기 환경은 Discover에만 국한된 것이 아니라 재사용 가능한 코드 모듈로, 대시보드, Kibana 알림, Kibana 지도와 같은 <strong>Kibana의 다른 부분에도 통합하기</strong> 위해 노력하고 있습니다.</p><h3>지능형 자동 완성: 쿼리 생성 가속화</h3><p>ES|QL 편집기의 자동 완성 기능은 포괄적이며 호환 가능한 함수, 인수, 리터럴, 심지어 중첩된 함수에 대한 제안을 제공하며, PPL에서는 특히 부족한 기능입니다. 사실, <a href="https://www.elastic.co/search-labs/blog/esql-autocomplete-rebuilt">여기에</a> 설명된 대로 완전히 새롭게 재구축되었습니다.</p><p>유효성 검사는 <a href="https://www.elastic.co/search-labs/blog/improving-esql-editor-experience-in-kibana">여기에</a> 설명된 대로 사용자가 입력하는 대로 실행되며, 필드를 제안하고 오류를 사용자에게 알립니다. 이렇게 하면 사용자의 정신적 부담이 줄어들고 쿼리 작성 프로세스 초기에 오류를 방지할 수 있습니다.</p><p>예시: 이 중첩에는 필드와 호환 가능한 함수가 제안됩니다:</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/a4d7b2819c34fab31bced7873257b8932b623fba-1502x473.png" style="width: 100%;" /><p>PPL은 지원하지 않습니다:</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/6f3a89b1255b8a3a762022a2704fdd1c2987e5f9-1013x335.png" style="width: 100%;" /><p>지능형 자동 완성 기능이 호환 가능한 함수, 인수 및 중첩 함수를 안내해 주더라도 사용 가능한 옵션에 대해 더 깊이 이해하고 싶을 수 있습니다. 쿼리 개발을 명확히 하고 개선할 수 있도록 에디터 내에서 즉각적인 지원을 제공하는 ES|QL 에디터의 상황별 도움말은 바로 이 지점에서 매우 유용합니다.</p><h3>상황에 맞는 도움말을 손끝으로</h3><p>자동 완성으로 생성된 명령에 대한 추가 정보는 Ctrl-스페이스키를 클릭하면 확인할 수 있습니다. 해당 함수, 인수 또는 필드에 대한 세부 정보가 포함된 패널이 즉시 나타납니다. 이 가벼운 상호작용은 개발자가 에디터를 종료하거나 외부 문서를 검색할 필요 없이 적시 안내를 제공하여 개발자의 흐름을 유지합니다. 이렇게 하면 구문 조회에 낭비되는 시간이 줄어들고 일반적인 실수를 사전에 방지할 수 있습니다.</p><p>실제 모습은 다음과 같습니다:</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/2797f9b5e002dbd83c46475c4ed4dcdc86144a01-1343x522.gif" style="width: 100%;" /><p>PPL에는 이러한 수준의 임베디드 가이드가 없기 때문에 사용자는 외부 문서나 시행착오에 의존해야 합니다. 이러한 부재는 단순히 기능의 부재가 아니라 디자인 철학의 광범위한 차이를 강조합니다. ES|QL은 사용자의 데이터와 워크플로에 맞춰 조정되는 사려 깊고 컨텍스트 인식적인 경험을 우선시합니다. 쿼리의 복잡성이 커질수록 이러한 차이는 더욱 뚜렷해지며, ES|QL 편집기는 학습 및 프로덕션 사용 모두에 더욱 효율적이고 안정적인 환경을 제공합니다.</p><h3>데이터 컨텍스트를 인식하는 권장 쿼리</h3><p>ES|QL 편집기는 로그와 같이 작업 중인 데이터에 맞게 자동으로 조정되는 추천 쿼리를 제공합니다. 빈 편집기를 표시하는 대신 일반적인 사용 사례에 가장 적합한 시작점을 표시합니다. 추천 쿼리를 선택하면 즉시 사용할 수 있는 표준 쿼리가 생성되며 필요에 따라 더 세분화할 수 있습니다. 이 접근 방식은 특히 아직 전체 구문을 모르는 신규 사용자의 경우 쿼리 개발을 가속화합니다.</p><p>다음은 사용자가 &#x27;변경 지점 감지&#x27; 쿼리를 선택하는 예시입니다:</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/3284c9340d41298820fbf8c7702abad946b48248-925x370.gif" style="width: 100%;" /><p>이를 PPL 경험과 비교해 보세요:</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/5c3e59dadc6210aede3366bdd081887bcbae7a54-969x798.png" style="width: 100%;" /><p>반면 PPL은 기본적인 자동 완성 기능만 제공하므로 문맥이나 구조 없이 쿼리를 조합해야 합니다. 이러한 지침의 부재는 좌절과 시행착오로 이어질 수 있습니다.<br/>ES|QL 편집기의 데이터 인식 추천 쿼리를 사용하면 일상적인 작업을 처음부터 시작하거나 구문을 외우지 않아도 됩니다. 편집기는 인지 부하를 줄이고 오류를 방지하며, 쿼리 구성과 씨름하는 대신 문제 해결과 클러스터 간 검색 실행과 같은 더 광범위한 목표에 집중할 수 있게 해줍니다.</p><h2>직관적인 클러스터 간 쿼리</h2><p>ES|QL 편집기의 자동 완성 기능은 <a href="https://elastic.aiops.work/search-labs/blog/esql-cross-cluster-search">CCS를 사용하여</a> 여러 원격 클러스터로 작업하는 경우에도 여전히 우수합니다. 그 이유는 다음과 같습니다:</p><h3>클러스터 간에도 원활한 자동 완성 기능을 제공하는 ES|QL 편집기</h3><p>ES|QL 편집기의 자동 완성 기능은 클러스터 이름뿐만 아니라<strong> 로컬 및 원격 인덱스도</strong> 모두 지원합니다. <a href="https://www.elastic.co/search-labs/blog/esql-cross-cluster-search">여기서</a> 다룬 바와 같이, 이는 로컬 노드로 전송할 쿼리 계획을 검증 및 생성하고, 쿼리를 실행하고, 결과를 집계한 후 사용자에게 다시 전송하는 코디네이터 노드 아키텍처 덕분에 작동합니다. 전체 원격 클러스터 이름을 입력하지 않고 &quot;:&quot;를 입력하면 원격 인덱스에 대한 자동 완성 프로세스가 시작됩니다. 그리고 접두사에 국한되지 않습니다.</p><p>따라서 명명 규칙을 외우거나 컨텍스트를 전환하지 않고도 분산된 데이터 집합을 쉽게 검색하고 쿼리할 수 있습니다.</p><p>다음은 사용자가 원격 인덱스를 찾기 위해 &quot;clu:g&quot;만 입력하는 예제입니다:</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/bae1fbacb2320e4d07f41291ea57c9bcf15bf8a5-1092x523.gif" style="width: 100%;" /><p>이와는 대조적으로 PPL은 로컬 인덱스에 대한 기본 완성도만 제공하며, 제안은 접두사 일치로 제한됩니다. 원격 클러스터는 수동으로 입력해야 하므로 오류 발생 가능성이 높아지고 쿼리 생성 속도가 느려집니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/e38793c0cc7c6cc7dc0fd4779a3e24ffbb6e0838-1094x263.gif" style="width: 100%;" /><p>PPL은 로컬 인덱스에 대해서만 완성을 제공하며 제안은 접두사로 제한됩니다:</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/5310f824942f94485cace2558ea72c56a0971e22-862x197.png" style="width: 100%;" /><p>ES|QL은 음수 부호를 사용하여 직접 <a href="https://www.elastic.co/docs/solutions/search/cross-cluster-search#exclude-problematic-clusters">제외를 허용함으로써</a> 탐색에 참여하는 클러스터를 세밀하게 제어할 수 있습니다. 이 기능은 클러스터 간 조사 중에 특정 데이터 세트를 포함하거나 생략할 수 있는 하이브리드 환경에서 작업할 때 특히 유용합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/f23ca17f58fbf8e6d27419c028274cb91f30a549-937x78.png" style="width: 100%;" /><p>이러한 개선 사항은 클러스터 간 검색에서 마찰을 줄이는 데 중점을 둔 Elasticsearch의 광범위한 노력을 반영합니다. 분산 쿼리를 더 쉽게 구성하고 관리할 수 있게 해주는 ES|QL 편집기는 분석가와 개발자가 구문보다는 인사이트에 집중할 수 있게 해주는 반면, PPL은 이러한 부담을 사용자에게 더 많이 떠넘깁니다. 또한 ES|QL 편집기는 클러스터 간 쿼리 생성을 간소화할 뿐만 아니라 이러한 쿼리가 실행되는 방식을 검사하는 도구도 제공하여 여러 클러스터에서 투명성 및 성능 모니터링을 보장합니다.</p><h3>검사 도구를 사용하여 클러스터 간 검색 세부 정보 분석하기</h3><p>ES|QL 편집기에서 액세스할 수 있는 검사 도구는 모든 클러스터에서 쿼리 실행에 대한 명시적인 정보가 포함된 메타데이터를 제공하도록 설계되었습니다. 이 기능은 Kibana Discover에서 활성화되며 쿼리 검사기에서 직접 액세스할 수 있어 검색 진행 상황과 세부 정보를 분석할 수 있으며, 이는 특히 <strong>클러스터 간 검색</strong> <a href="https://www.elastic.co/docs/reference/query-languages/esql/esql-cross-clusters">(CCS)</a>에 매우 중요합니다. 이 기능을 사용하면 검색 진행 상황을 모니터링하고 분산된 데이터 세트에서 쿼리가 어떻게 수행되는지 파악할 수 있습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/5d1c204f70171526fff924c30ea8ad08121a0f8d-919x523.gif" style="width: 100%;" /><p>특히 복잡한 분산 검색의 경우 쿼리 실행에 대한 자세한 가시성을 통해 최적의 성능과 문제 해결을 보장할 수 있습니다.</p><p>개별 쿼리의 메커니즘을 이해하는 것 외에도, ES|QL 편집기는 전체 Kibana 플랫폼에 걸쳐 필수 기능을 심층적으로 내장함으로써 사용자 여정을 더욱 향상시켜 원활하고 중단 없는 워크플로우를 촉진합니다.</p><h2>ES|QL과 Kibana의 통합 쿼리 환경</h2><p>쿼리 기반 분석에서 가장 일반적인 마찰의 원인 중 하나는 컨텍스트 전환입니다. 이미 작성한 쿼리를 다시 불러와야 하는 경우가 종종 있습니다. 방해가 있을 때마다 집중력이 흐트러지고 조사 속도가 느려집니다. ES|QL 편집기는 Kibana 전체에 쿼리 기록을 통합하여 이 문제를 해결합니다.</p><h3>최근 쿼리</h3><p>ES|QL 편집기의 <a href="https://www.elastic.co/search-labs/blog/esql-piped-query-language-goes-ga">최근 쿼리</a> 기능은 과거 작업에 즉시 액세스할 수 있도록 하여 작업 흐름을 유지하는 데 도움이 됩니다. Discover의 ES|QL 편집기에서 최근 20개의 쿼리를 보고, 다시 실행하고, 별표를 표시할 수 있어 자주 사용하거나 복잡한 쿼리를 클릭 한 번으로 쉽게 찾을 수 있습니다. 이렇게 저장된 쿼리는 대시보드, 시각화, 알림, 지도와 통합되어 Kibana 전체에 적용되므로 현재 화면을 떠나거나 명령을 처음부터 다시 입력할 필요가 없습니다. 이를 통해 반복적인 작업을 줄이고 조사 속도를 높이며 오류 위험을 최소화할 수 있습니다.</p><p>예를 들어 사용자는 Discover의 ES|QL 편집기에서 최근 쿼리를 활용하고 별표를 표시할 수 있습니다:</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/eb0f9fe37b92dec421c394d31ae7d90afebe062e-1421x793.png" style="width: 100%;" /><p>최근 쿼리가 대시보드에 통합됩니다:</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/b84cd3a9bdec58812360d2aba4fc7713363ee3cc-1411x797.png" style="width: 100%;" /><p>PPL은 이에 필적할 만한 기능을 제공하지 않기 때문에 사용자는 수동 복사-붙여넣기나 외부 메모에 의존하여 쿼리를 재사용해야 합니다. 이 차이는 편의성 그 이상입니다. Kibana 에코시스템 내에서 진정한 통합 언어로 ES|QL을 구축하려는 Elastic의 전략이 반영된 것입니다. 최근 쿼리와 같은 기능을 통해 ES|QL 편집기는 일상적인 워크플로를 간소화할 뿐만 아니라 현재 기술 미리보기에서 고급 기능을 위한 기반을 마련하여 지속적으로 발전하는 환경을 보장합니다.</p><h2>결론</h2><p>ES|QL은 단순한 구문이 아니라 사용자가 데이터를 검색, 탐색, 분석하는 방식을 개선하기 위한 Elastic의 전략을 반영합니다. 지능형 자동 완성, 컨텍스트 인식 추천 쿼리, 편집기 내 안내, Inspect와 같은 도구를 통해 ES|QL 편집기는 학습을 가속화하고 오류를 줄이며 클러스터 간 분석과 같은 복잡한 워크플로를 간소화합니다. Kibana 전체에 통합되어 쿼리를 대시보드, 알림, 시각화에 원활하게 연결하여 중단 없는 워크플로우를 제공합니다.</p><p>요약하자면, ES|QL은 단순히 또 하나의 파이프 언어가 아니라, 데이터와 상호 작용하는 방식을 근본적으로 재정의하는 직관적인 UI와 결합된 세심하게 설계된 쿼리 엔진으로, 종종 순차적이고 안내가 부족한 OpenSearch PPL과는 확연히 대조되는 통합적이고 지능적이며 지속적으로 진화하는 경험을 제공합니다.</p><h2>다음 단계</h2><p>이 블로그는 ES|QL의 표면적인 부분만 다루고 있습니다. 다음 게시물에서는 OpenSearch PPL과의 비교를 자세히 살펴보고, <a href="https://www.elastic.co/docs/explore-analyze/dashboards/add-controls">컨트롤</a> (대시보드에서 이미 사용 가능), 다중 데이터 탐색 탭, 배경 검색, 더 풍부한 쿼리 기록 및 FUSE와 같은 지리적 공간, 시각화 및 곧 출시될 에디터 기능에 대해 살펴볼 예정입니다.</p><h2>지금 ES|QL 체험하기</h2><p><a href="https://www.elastic.co/docs/deploy-manage/deploy/elastic-cloud/create-serverless-project">무료 체험</a> 판으로 완전 관리형 <a href="https://www.elastic.co/cloud/serverless">Elasticsearch 서버리스</a> 프로젝트에서 ES|QL을 확인해 보세요. 8.11 버전에서도 사용할 수 있지만 <a href="https://www.elastic.co/blog/whats-new-elastic-9-1-0">8.19와 9.1에서</a> 가장 잘 사용할 수 있습니다.</p><p>명령 한 번으로 로컬 환경에서 몇 분 안에 시작할 수 있습니다:</p><pre><code>curl -fsSL https://elastic.co/start-local | sh</code></pre>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Elasticsearch Ruby 클라이언트를 위한 ES|QL 쿼리 빌더 소개]]></title>
            <description><![CDATA[최근 출시된 Elasticsearch Ruby 클라이언트용 ES|QL 쿼리 빌더를 사용하는 방법을 알아보세요. 루비 코드로 ES|QL 쿼리를 더 쉽게 작성할 수 있는 도구입니다.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/esql-query-builder-elasticsearch-ruby-client</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/esql-query-builder-elasticsearch-ruby-client</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Fernando Briano]]></dc:creator>
            <pubDate>Wed, 17 Sep 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/f8e1263ab53d356824a4fc539084151be80899db-720x420.jpg?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>최근에 Apache 2 라이선스에 따라 게시된 루비 보석( <a href="https://github.com/elastic/esql-ruby/"><code>elastic-esql</code></a>)을 출시했습니다. 이 보석을 사용하면 관용적 Ruby로 Elastic의 <a href="https://www.elastic.co/docs/explore-analyze/query-filter/languages/esql">ES|QL</a> 쿼리를 빌드한 다음 ES|QL 쿼리 API와 함께 사용할 수 있습니다. ES|QL을 사용하면 개발자가 쿼리를 통해 Elasticsearch에 저장된 데이터를 필터링, 변환, 분석할 수 있습니다. &quot;파이프&quot; ( <code>|</code> )를 사용하여 단계별로 데이터를 작업합니다. 이 보석은 대신 Ruby 함수를 사용하며, 이를 원래 객체에 연결하여 더 복잡한 쿼리를 만들 수 있습니다:</p><p><strong>ESQL:</strong></p><pre><code>FROM sample_data | LIMIT 2 | SORT @timestamp DESC</code></pre><p><strong>Ruby:</strong></p><pre><code>Elastic::ESQL.from('sample_data').limit(2).sort('@timestamp').descending</code></pre><h2>설치</h2><p>이 젬은 루비젬스에서 다음을 사용하여 설치할 수 있습니다:</p><pre><code>gem install elastic-esql</code></pre><p>또는 프로젝트의 젬파일에 추가할 수도 있습니다:</p><pre><code>gem 'elastic-esql'</code></pre><h2>사용법</h2><p>전체 쿼리를 한 번에 빌드하거나 <code>from</code> 또는 <code>row</code> 같은 소스 명령으로 쿼리 개체를 만든 다음 ES|QL 메서드를 체인으로 연결하여 빌드할 수 있습니다.</p><pre><code>query = Elastic::ESQL.from('sample_data')
query.limit(2).sort('@timestamp')</code></pre><p>겜은 <code>to_s</code> 메서드에서 코드를 ES|QL로 변환하므로 ES|QL 쿼리가 출력되거나 문자열로 캐스팅될 때 반환합니다:</p><pre><code>query = Elastic::ESQL.from('sample_data').limit(2).sort('@timestamp').descending
query.to_s
# => "FROM sample_data | LIMIT 2 | SORT @timestamp DESC"</code></pre><p>각 함수의 <code>!</code> 등가물을 사용하여 쿼리 객체를 인스턴스화하고 초기 상태를 변경할 수 있습니다:</p><pre><code>query = Elastic::ESQL.from('sample_data')
query.to_s
# => "FROM sample_data"
query.limit!(2).sort!('@timestamp')
query.to_s
# => "FROM sample_data | LIMIT 2 | SORT @timestamp"</code></pre><p>이 도구는 <code>enrich</code> 및 <code>sort</code> 과 같은 추가 단계를 ES|QL 함수에 연결하는 편리한 방법을 제공합니다. <code>Elastic::ESQL</code> 객체에서 <code>enrich</code> 을 호출하면 <code>on</code> 과 <code>with</code> 을 연결할 수 있습니다:</p><pre><code>esql.enrich!('policy').on('a').with({ name: 'language_name' })</code></pre><p><code>sort</code> 을 사용한 후 <code>desc</code>, <code>asc</code>, <code>nulls_first</code>, <code>nulls_last</code> 을 쿼리에 연결할 수도 있습니다:</p><pre><code>Elastic::ESQL.from('sample_data').sort('@timestamp').asc.to_s
# => 'FROM sample_data | SORT @timestamp ASC'

Elastic::ESQL.from('sample_data').sort('@timestamp').desc.nulls_first.to_s
# => 'FROM sample_data | SORT @timestamp DESC NULLS FIRST'</code></pre><p>또한 ES|QL 쿼리를 직접 작성하거나 아직 라이브러리에 추가되지 않은 기능을 사용하려는 경우 사용자 정의 문자열을 지원합니다. <code>custom</code> 은 쿼리 끝에 있는 문자열을 결합합니다. 파이프 문자를 추가하지 않고 함수에 전송되는 대로 추가합니다. 나머지 쿼리에는 공백 문자로 결합됩니다.</p><pre><code>esql = Elastic::ESQL.from('sample_data')
esql.custom('| MY_VALUE = "test value"').to_s
# => 'FROM sample_data | MY_VALUE = "test value"'</code></pre><p><code>custom</code> 함수를 연결할 수도 있습니다:</p><pre><code>esql.custom('| MY_VALUE = "test value"').custom('| ANOTHER, VALUE')
'FROM sample_data | MY_VALUE = "test value" | ANOTHER, VALUE'</code></pre><h2>루비 클라이언트와 함께 ES|QL 쿼리 빌더 사용하기</h2><p>쿼리 빌더는 쿼리 객체를 전송하여 <a href="https://github.com/elastic/elasticsearch-ruby">elasticsearch-ruby</a> 및 <code>esql.query</code> API와 함께 직접 사용할 수 있습니다:</p><pre><code>require 'elasticsearch'
require 'elastic/esql'

client = Elasticsearch::Client.new
index = 'sample_data'

query = Elastic::ESQL.from(index)
                     .sort('@timestamp')
                     .desc
                     .where('event_duration > 5000000')
                     .limit(3)
                     .eval({ duration_ms: 'ROUND(event_duration/1000000.0, 1)' })
client.esql.query(body: { query: query })</code></pre><p>Elasticsearch Ruby 클라이언트의 ES|QL 도우미와 함께 사용할 수도 있으며, <a href="https://www.elastic.co/search-labs/blog/esql-ruby-helper-elasticsearch">자세히 알아보세요</a>:</p><pre><code>require 'elasticsearch/helpers/esql_helper'

Elasticsearch::Helpers::ESQLHelper.query(client, query)</code></pre><h2>독립형 도구로서</h2><p>이 보석은 관용적인 방식으로 ES|QL 쿼리를 작성하는 독립형 도구로 설계되었습니다. 런타임 종속성이 없으므로 공식 Elasticsearch Ruby 클라이언트와 함께 사용하거나 단독으로 사용할 수 있습니다.</p><p>생성된 쿼리는 애플리케이션이 Elasticsearch API와 상호 작용하는 모든 방식(Ruby 여부에 관계없이)으로 <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-esql-query"><code>esql.query</code></a> API와 함께 사용할 수 있습니다. <code>elastic-esql</code> 으로 쿼리를 작성하면 생성된 문자열을 요청 본문에 <code>query</code> 매개변수로 API에 전송할 수 있습니다. </p><p>이전에 <a href="https://www.elastic.co/search-labs/blog/elasticsearch-ruby-tools">인기 있는 Ruby 도구와 함께 Elasticsearch를 사용하는</a> 방법에 대한 글을 쓴 적이 있습니다. 이 보석은 널리 사용되는 모든 Ruby 도구와 함께 ES|QL로 Elasticsearch를 쿼리하는 데 사용할 수 있습니다.</p><h2>결론</h2><p>이 라이브러리는 현재 개발 중이며 최종 API는 아직 완성되지 않았습니다. 현재 기술 프리뷰 버전으로 출시되었습니다. 현재 API 또는 일반적인 사용법에 대한 피드백이 있으시면 주저하지 마시고 <a href="https://github.com/elastic/esql-ruby/issues">새 이슈를 개설해</a> 주세요. 루비 ES|QL 쿼리 빌더에 대해 자세히 알아보려면 <a href="https://github.com/elastic/esql-ruby/?tab=readme-ov-file#ruby-esql-query-builder">README를</a> 참조하세요.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[벡터 검색 필터링: 관련성 유지]]></title>
            <description><![CDATA[쿼리와 가장 유사한 결과를 찾기 위해 벡터 검색을 수행하는 것만으로는 충분하지 않습니다. 검색 결과의 범위를 좁히기 위해 필터링이 필요한 경우가 많습니다. 이 문서에서는 Elasticsearch와 Apache Lucene에서 벡터 검색을 위한 필터링이 어떻게 작동하는지 설명합니다.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/vector-search-filtering</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/vector-search-filtering</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Carlos Delgado]]></dc:creator>
            <pubDate>Wed, 03 Sep 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/03b1dd4c7bda4fbabd8e374bc2e4f12d5be6ef5f-1600x1150.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>벡터 검색만으로는 관련 검색 결과를 찾을 수 없습니다. 검색 결과의 범위를 좁히고 관련 없는 결과를 걸러내는 데 도움이 되는 필터링 기준을 사용하는 것은 매우 일반적입니다.</p><p>벡터 검색에서 필터링이 작동하는 방식을 이해하면 성능과 회상률의 균형을 맞추는 데 도움이 될 뿐만 아니라 필터링 사용 시 벡터 검색의 성능을 높이는 데 사용되는 몇 가지 최적화를 알아볼 수 있습니다.</p><h2>왜 필터링할까요?</h2><p>벡터 검색은 대규모 데이터 세트에서 관련 정보를 찾는 방법을 혁신적으로 개선하여 검색어와 의미적으로 유사한 항목을 발견할 수 있게 해줍니다.</p><p>하지만 단순히 비슷한 아이템을 찾는 것만으로는 충분하지 않습니다. 특정 기준이나 속성에 따라 검색 결과의 범위를 좁혀야 하는 경우가 많습니다.</p><p>이커머스 스토어에서 제품을 검색하고 있다고 상상해 보세요. 순수한 벡터 검색은 시각적으로 유사한 상품을 보여줄 수 있지만 가격대, 브랜드, 재고 여부 또는 고객 평점을 기준으로 필터링할 수도 있습니다. 필터링이 없으면 유사한 상품이 너무 많이 표시되어 원하는 상품을 정확히 찾기 어렵습니다.</p><p>필터링을 통해 검색 결과를 정밀하게 제어할 수 있으므로 검색된 항목이 의미적으로 일치할 뿐만 아니라 필요한 모든 요건을 충족하는지 확인할 수 있습니다. 이를 통해 훨씬 더 정확하고 효율적이며 사용자 친화적인 검색 환경을 제공합니다.</p><p>다양한 데이터 유형에 걸쳐 효과적인 필터링을 사용하는 것이 다른 벡터 데이터베이스와의 주요 차이점 중 하나인 Elasticsearch와 Apache Lucene의 장점입니다.</p><h2>정확한 벡터 검색을 위한 필터링</h2><p>정확한 벡터 검색을 수행하는 방법에는 크게 두 가지가 있습니다:</p><ul><li>dense_vector 필드에 <code>flat</code> 인덱스 유형을 사용합니다. 따라서 <code>knn</code> 검색은 대략적인 검색이 아닌 정확한 검색을 사용합니다.</li><li>벡터 함수를 사용하여 점수를 계산하는 <a href="https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-script-score-query#vector-functions">스크립트_점수 쿼리를</a> 사용합니다. 모든 인덱스 유형에 사용할 수 있습니다.</li></ul><p>정확한 벡터 검색을 실행할 때는 모든 벡터가 쿼리와 비교됩니다. 이 시나리오에서는 필터를 통과한 벡터만 비교하면 되므로 필터링이 성능에 도움이 됩니다.</p><p>어쨌든 모든 벡터가 고려되므로 결과 품질에는 영향을 미치지 않습니다. 흥미롭지 않은 결과를 미리 필터링하여 작업 횟수를 줄일 수 있습니다.</p><p>적용된 필터로 인해 문서 수가 적은 경우 대략적인 검색 대신 정확한 검색을 실행하는 것이 더 효율적일 수 있으므로 이는 매우 중요합니다.</p><p>필터를 통과하는 문서가 1만 개 미만인 경우 정확한 검색을 사용하는 것이 좋습니다. <a href="https://www.elastic.co/search-labs/blog/better-binary-quantization-lucene-elasticsearch">BBQ</a> 인덱스가 비교에 훨씬 빠르므로 기준 인덱스가 10만 개 미만일 때는 정확한 검색을 사용하는 것이 좋습니다. 자세한 내용은 <a href="https://www.elastic.co/search-labs/blog/knn-exact-vs-approximate-search">이 블로그 게시물을</a> 확인하세요.</p><p>필터가 항상 매우 제한적인 경우에는 HNSW 기반 인덱스 유형 대신 <code>flat</code> 인덱스 유형을 사용하여 대략적인 검색 대신 정확한 검색에 초점을 맞춘 인덱싱을 고려할 수 있습니다. 자세한 내용은 <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/dense-vector#dense-vector-params">index_options의 속성을</a> 참조하세요.</p><h2>대략적인 벡터 검색을 위한 필터링</h2><p>근사 벡터 검색을 실행할 때는 결과 정확도와 성능을 맞바꿉니다. HNSW와 같은 벡터 검색 데이터 구조는 수백만 개의 벡터에서 대략적인 가장 가까운 이웃을 효율적으로 검색합니다. 계산 비용이 많이 드는 벡터 비교를 최소한으로 수행하여 가장 유사한 벡터를 검색하는 데 중점을 둡니다.</p><p>즉, 다른 필터링 속성은 벡터 데이터의 일부가 아닙니다. 용어 사전, 게시 목록, 문서 값 등 데이터 유형마다 이를 찾고 필터링하는 데 효율적인 자체 인덱싱 구조가 있습니다.</p><p>이러한 데이터 구조가 벡터 검색 메커니즘과 분리되어 있다면 벡터 검색에 필터링을 적용하려면 어떻게 해야 할까요? 벡터 검색 후 필터를 적용하거나(사후 필터링) 벡터 검색 전에 필터를 적용하는(사전 필터링) 두 가지 옵션이 있습니다.</p><p>각 옵션에는 장단점이 있습니다. 더 자세히 알아봅시다!</p><h3>사후 필터링</h3><p>사후 필터링은 벡터 검색이 완료된 후 필터를 적용합니다. 즉, 필터는 가장 유사한 벡터 결과 상위 k개를 찾은 후에 적용됩니다.</p><p>물론 결과에 필터를 적용한 후 잠재적으로 k보다 적은 결과를 얻을 수 있습니다. 물론 벡터 검색에서 더 많은 결과를 검색할 수 있지만(k 값이 높을수록) 필터를 적용한 후에도 k 이상의 결과를 얻을 수 있을지는 확신할 수 없습니다.</p><p>사후 필터링의 장점은 벡터 검색의 런타임 동작을 변경하지 않는다는 점입니다. 벡터 검색은 필터링을 인식하지 못합니다. 하지만 검색되는 최종 결과 수는 변경됩니다.</p><p>다음은 <a href="https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-knn-query">knn 쿼리를</a> 사용한 사후 필터링의 예입니다. 필터링 절이 knn 쿼리와 분리되어 있는지 확인합니다:</p><pre><code>{
  "query": {
    "bool": {
      "must": {
        "knn": {
          "field": "image-vector",
          "query_vector": [54, 10, -2],
          "k": 5,
          "num_candidates": 50
        }
      },
      "filter": {
        "term": {
          "file-type": "png"
        }
      }
    }
  }
}</code></pre><p><a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/filter-search-results#post-filter">포스트 필터를</a> 사용하여 KNN 검색에 포스트 필터링도 사용할 수 있습니다:</p><pre><code>{
  "knn": {
    "field": "image-vector",
    "query_vector": [54, 10, 2],
    "k": 5,
    "num_candidates": 50
  },
  "post_filter": {
    "term": {
      "file-type": "png"
    }
  }
}</code></pre><p>knn 검색에 명시적인 사후 필터 섹션을 사용해야 한다는 점에 유의하세요. 사후 필터를 사용하지 않는 경우 knn 검색은 <a href="https://www.elastic.co/docs/solutions/search/vector/knn#_combine_approximate_knn_with_other_features">사후 필터를 수행하는 대신 가장</a> 가까운 이웃 검색 결과를 다른 쿼리 또는 필터와 결합합니다.</p><h3>사전 필터링</h3><p>벡터 검색 전에 필터를 적용하면 먼저 필터를 충족하는 문서를 검색한 다음 해당 정보를 벡터 검색에 전달합니다.</p><p>Lucene은 <a href="https://github.com/apache/lucene/blob/7a60d7ce92392181e137361336e5196bd486cdd9/lucene/core/src/java/org/apache/lucene/util/BitSet.java">비트셋을</a> 사용하여 필터 조건을 충족하는 문서를 효율적으로 저장합니다. 그런 다음 벡터 검색은 조건을 충족하는 문서를 고려하여 HNSW 그래프를 탐색합니다. 결과에 후보를 추가하기 전에 유효한 문서의 비트 집합에 포함되어 있는지 확인합니다.</p><p>그러나 유효한 문서가 아니더라도 후보를 탐색하고 쿼리와 비교해야 합니다. HNSW의 효과는 그래프에서 벡터 간의 연결에 따라 달라지는데, 한 후보 탐색을 중단하면 이웃 후보도 건너뛸 수 있습니다.</p><p>주유소에 가기 위해 운전한다고 생각하세요. 주유소가 없는 도로를 버리면 목적지까지 갈 수 없을 가능성이 높습니다. 다른 길은 내가 원하는 길이 아닐 수도 있지만 목적지까지 <em>연결해</em> 줍니다. HNSW 그래프의 벡터도 마찬가지입니다!</p><p>따라서 사전 필터링을 적용하는 것이 필터를 적용하지 않는 것보다 성능이 떨어집니다. 검색에서 방문하는 <em>모든</em> 벡터에 대한 작업을 수행해야 하며 필터와 일치하지 않는 벡터는 버려야 합니다. 최고의 결과를 얻기 위해 더 많은 노력을 기울이고 더 많은 시간을 투자하고 있습니다.</p><p>다음은 Elasticsearch 쿼리 DSL에서 사전 필터링의 예입니다. 필터링 절이 이제 knn 섹션의 일부가 되었는지 확인합니다:</p><pre><code>{
  "knn": {
    "field": "image-vector",
    "query_vector": [54, 10, -2],
    "k": 5,
    "num_candidates": 50,
    "filter": {
      "term": {
        "file-type": "png"
      }
    }
  }
}</code></pre><p>사전 필터링은 <a href="https://www.elastic.co/docs/solutions/search/vector/knn#knn-search-filter-example">knn 검색과</a> <a href="https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-knn-query#knn-query-filtering">knn 쿼리</a> 모두에 사용할 수 있습니다:</p><pre><code>{
  "query": {
    "knn": {
      "field": "image-vector",
      "query_vector": [-5, 9, -12],
      "k": 5,
      "filter": {
        "term": {
          "file-type": "png"
        }
      }
    }
  }
}</code></pre><h4>사전 필터링 최적화</h4><p>사전 필터링의 성능을 보장하기 위해 적용할 수 있는 몇 가지 최적화가 있습니다.</p><p>필터가 매우 제한적인 경우 정확한 검색으로 전환할 수 있습니다. 비교할 벡터가 적을 때는 필터를 만족하는 소수의 문서에 대해 정확한 검색을 수행하는 것이 더 빠릅니다.</p><p>이것은 <a href="https://github.com/apache/lucene/blob/eb876b618da5d04c1ad14b04a48321638318493a/lucene/core/src/java/org/apache/lucene/search/AbstractKnnVectorQuery.java#L218">Lucene과</a> Elasticsearch에서 자동으로 적용되는 최적화입니다.</p><p>또 다른 최적화 방법은 필터를 만족하지 않는 벡터를 무시하는 것입니다. 대신 이 메서드는 필터를 통과한 필터링된 벡터의 이웃을 확인합니다. 이 접근 방식은 필터링된 벡터를 고려하지 않고 현재 경로에 연결된 벡터를 계속 탐색하므로 비교 횟수를 효과적으로 줄일 수 있습니다.</p><p>이 알고리즘은 ACORN-1이며, 그 과정은 <a href="https://www.elastic.co/search-labs/blog/filtered-hnsw-knn-search">이 블로그 게시물에</a> 자세히 설명되어 있습니다.</p><h2>문서 수준 보안을 사용한 필터링</h2><p><a href="https://www.elastic.co/docs/deploy-manage/users-roles/cluster-or-deployment-auth/controlling-access-at-document-field-level#document-level-security">DLS(문서 수준 보안)</a> 는 사용자 역할이 검색할 수 있는 문서를 지정하는 Elasticsearch 기능입니다.</p><p>DLS는 쿼리를 사용하여 수행됩니다. 역할에는 인덱스와 연결된 쿼리가 있을 수 있으며, 이 쿼리는 해당 역할에 속한 사용자가 인덱스에서 검색할 수 있는 문서를 효과적으로 제한합니다.</p><p>역할 쿼리는 필터로 사용되어 <a href="https://github.com/elastic/elasticsearch/blob/c3a1cb34294e902a9f46d7e840ea09965019f456/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/SecurityIndexReaderWrapper.java#L92">일치하는 문서를 검색하고</a> 비트셋으로 캐시됩니다. 그런 다음 이 비트셋은 기본 Lucene 리더를 래핑하는 데 사용되므로 쿼리에서 반환된 문서, 즉 인덱스에 존재하고 삭제되지 않은 문서만 <em>라이브</em>문서로 간주됩니다.</p><p>knn 쿼리를 수행하기 위해 리더에서 라이브 문서가 검색되므로 사용자가 사용할 <a href="https://github.com/apache/lucene/blob/a211d30097a8e3264d3ef073a054bd31eb847231/lucene/core/src/java/org/apache/lucene/search/AbstractKnnVectorQuery.java#L196">수 있는 문서만 고려됩니다.</a> 프리필터가 있는 경우 DLS 문서가 프리필터에 <a href="https://github.com/apache/lucene/blob/a211d30097a8e3264d3ef073a054bd31eb847231/lucene/core/src/java/org/apache/lucene/search/AbstractKnnVectorQuery.java#L204">추가됩니다</a>.</p><p>즉, DLS 필터링은 근사 벡터 검색을 위한 프리필터로 작동하며 성능에 미치는 영향과 최적화가 동일합니다.</p><p>정확한 검색을 사용하는 DLS는 필터를 적용하는 것과 동일한 이점이 있습니다. DLS에서 검색되는 문서가 적을수록 정확한 검색의 성능이 향상됩니다. DLS 역할이 매우 제한적인 경우 대략적인 검색 대신 정확한 검색을 사용하는 것도 고려할 수 있습니다.</p><h2>벤치마킹</h2><p>Elasticsearch에서는 벡터 검색 필터링이 효율적인지 확인하고자 합니다. 벡터 <a href="https://elasticsearch-benchmarks.elastic.co/#tracks/so_vector/nightly/default/90d">필터링에 대한 특정 벤치마크가</a> 있어 다양한 필터링을 통해 대략적인 벡터 검색을 수행하여 벡터 검색이 관련성 있는 결과를 최대한 빠르게 검색할 수 있도록 합니다.</p><p>ACORN-1 도입 시 <a href="https://elasticsearch-benchmark-analytics.elastic.co/app/dashboards#/view/43b63e80-5ba2-11ed-aede-a742809feed4?_g=(refreshInterval:(pause:!t,value:60000),time:(from:&#x27;2025-05-28T01:27:58.456Z&#x27;,to:&#x27;2025-06-30T13:53:26.430Z&#x27;))&amp;_a=()">개선</a> 사항을 확인하세요. 필터를 통과한 벡터가 2개% 뿐인 테스트의 경우 쿼리 지연 시간은 원래 지속 시간의 55% 로 줄어듭니다:</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/3eac3748a33376fc97d957364a5c1f5108d5c58b-1023x896.png" style="width: 100%;" /><h2>결론</h2><p>필터링은 검색의 필수적인 부분입니다. 벡터 검색에서 필터링이 제대로 작동하는지 확인하고 장단점과 최적화를 이해하는 것이 효율적이고 정확한 검색의 성패를 좌우합니다.</p><p>필터링은 벡터 검색의 성능에 영향을 미칩니다:</p><ul><li>필터링을 사용하면 정확한 검색이 더 빠릅니다. 필터링이 충분히 제한적인 경우 대략적인 검색 대신 정확한 검색을 사용하는 것이 좋습니다. 이것은 Elasticsearch의 자동 최적화입니다.</li><li>사전 필터링 사용 시 대략적인 검색 속도가 느려집니다. 사전 필터링을 사용하면 검색 속도가 느려지는 대신 필터와 일치하는 상위 k개의 결과를 얻을 수 있습니다.</li><li>사후 필터링은 필터를 적용할 때 필터를 통해 필터링될 수 있으므로 반드시 상위 k개의 결과를 검색하지는 않습니다.</li></ul><p>행복한 필터링!</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Elasticsearch 샤드와 복제본: 실용적인 가이드]]></title>
            <description><![CDATA[Elasticsearch 샤드와 복제본의 개념을 마스터하고 이를 최적화하는 방법을 알아보세요.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/elasticsearch-shards-and-replicas-guide</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/elasticsearch-shards-and-replicas-guide</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Piotr Przybyl]]></dc:creator>
            <pubDate>Thu, 14 Aug 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/7775c44f01f2516c4ff4cce6d6bbe9e7b2c38908-720x420.jpg?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>Elasticsearch는 확장성과 내결함성 문제를 해결하는 분산 시스템을 그 위에 구축하여 Lucene의 성능을 향상시킵니다. 또한 JSON 기반 REST API를 노출하여 다른 시스템과의 상호 운용성이 매우 간단합니다.</p><p>Elasticsearch와 같은 분산 시스템은 매우 복잡할 수 있으며, 성능과 안정성에 영향을 미칠 수 있는 많은 요인들이 존재합니다. <strong>샤드는</strong> Elasticsearch에서 가장 기본적인 개념 중 하나이며, 샤드의 작동 원리를 이해하면 Elasticsearch 클러스터를 효과적으로 관리할 수 있습니다.</p><p>이 문서에서는 기본 샤드와 복제 샤드가 무엇이며, Elasticsearch 클러스터에 미치는 영향과 다양한 요구 사항에 맞게 샤드를 조정할 수 있는 도구가 무엇인지 설명합니다.</p><h2>샤드 이해</h2><p>Elasticsearch 인덱스의 데이터는 엄청난 비율로 증가할 수 있습니다. 관리하기 쉽도록 모든 데이터는 인덱스에 보관되며, 인덱스는 여러 개의 <strong>샤드로</strong> 분할된 인덱스입니다. 각 Elasticsearch 샤드는 Apache Lucene 인덱스이며, 각 개별 Lucene 인덱스는 Elasticsearch 인덱스에 있는 문서의 하위 집합을 포함합니다. 이러한 방식으로 인덱스를 분할하면 리소스 사용량을 제어할 수 있습니다. Apache Lucene 인덱스의 문서 수 제한은 2,147,483,519개(2³¹ - 129개)입니다.</p><p>때로는 리밸런싱을 위해 노드 간에 인덱스를 이동해야 하는 경우가 있습니다. 이 프로세스는 시간과 리소스가 많이 소요될 수 있으므로 인덱스가 너무 커지지 않아야 복구 시간을 관리할 수 있습니다. 또한 인덱스는 지속적으로 병합해야 하는 Lucene 세그먼트로 구성되므로 세그먼트가 너무 커지지 않도록 하는 것이 중요합니다. 이러한 이유로 Elasticsearch는 인덱스 데이터를 <strong>기본 샤드라고</strong> 하는 관리하기 쉬운 작은 청크로 분할하여 여러 머신에 더 쉽게 분산할 수 있습니다. <strong>복제</strong> 샤드는 단순히 해당 기본 샤드의 정확한 복사본이며, 이 글의 뒷부분에서 복제 샤드의 기능을 살펴보겠습니다.</p><p>적절한 수의 샤드를 보유하는 것은 성능에 중요합니다. 따라서 미리 계획을 세우는 것이 현명합니다. 쿼리가 여러 샤드에서 병렬로 실행되는 경우, 단일 샤드로 구성된 인덱스보다 빠르게 실행되지만 각 샤드가 다른 노드에 있고 클러스터에 충분한 노드가 있는 경우에만 실행됩니다. 그러나 동시에 샤드는 인덱싱된 데이터와 클러스터 메타데이터 측면에서 메모리와 디스크 공간을 소비합니다. 샤드가 너무 많으면(오버샤딩이라고도 함) 쿼리, 인덱싱 요청 및 관리 작업 속도가 느려질 수 있으므로 적절한 균형을 유지하는 것이 중요합니다.</p><p>기본 샤드 수는 <strong>특정 인덱스 인스턴스에 대한</strong> 인덱스 생성 시 정의됩니다. 나중에 다른 수의 기본 샤드가 필요한 경우<strong> 크기 조정</strong><a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-indices-split">API인</a> 분할(기본 샤드 수 증가), <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-indices-shrink">축소</a> (기본 샤드 수 감소) 또는 <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-indices-clone">복제</a> (복제본에 대한 새로운 설정으로 동일한 수의 기본 샤드)를 사용할 수 있습니다. 이러한 작업은 Lucene 세그먼트를 복사하고 <strong>모든 문서의 전체 재색인을 피하며</strong>, 인덱스를 만들 때 인덱스의 설정으로 기본 및 복제 샤드 수를 설정할 수 있습니다:</p><pre><code>PUT /sensor
{
   "settings" : {
       "index" : {
           "number_of_shards" : 6,
           "number_of_replicas" : 2
       }
   }
}</code></pre><p>(샤드 또는 복제본 수를 지정하지 않으면 Elasticsearch 7.0 기준 기본값은 둘 다 1입니다). 인덱스의 데이터 양에 따라 이상적인 샤드 수를 결정해야 합니다. 일반적으로<a href="https://www.elastic.co/docs/deploy-manage/production-guidance/optimize-performance/size-shards">최적의 샤드에는 10~50GB의 데이터가 저장되어야 하며,</a> 샤드당 문서 수는 2억 개 미만이어야 합니다. 예를 들어, 하루에 약 300GB의 애플리케이션 로그가 누적될 것으로 예상되는 경우, 이를 호스팅할 충분한 양의 노드가 있다면 해당 인덱스에 약 10개의 샤드를 보유하는 것이 합리적입니다.</p><p>파편은 수명이 다하는 동안 다음과 같은 여러 상태를 거칠 수 있습니다:</p><ul><li><strong>초기화 중입니다:</strong> 샤드를 사용하기 전 초기 상태입니다.</li><li><strong>시작됨:</strong> 샤드가 활성화되어 요청을 받을 수 있는 상태입니다.</li><li><strong>재배치 중:</strong> 샤드가 다른 노드로 이동하는 중일 때 발생하는 상태입니다. 이는 특정 조건에서 필요할 수 있는데, 예를 들어 해당 노드의 디스크 공간이 부족한 경우입니다.</li><li><strong>할당되지 않음:</strong> 할당되지 않음: 할당되지 않은 샤드의 상태입니다. 예를 들어, 샤드를 호스팅하는 노드가 더 이상 클러스터에 속하지 않거나 <em>(NODE_LEFT)</em> 폐쇄 인덱스로 복원하는 경우 <em>(EXISTING_INDEX_RESTORED)</em>와 같이 이런 상황이 발생하면 사유가 제공됩니다.</li></ul><p>모든 샤드, 샤드의 상태 및 기타 메타데이터를 보려면 다음 요청을 사용하면 됩니다:</p><pre><code>GET _cat/shards</code></pre><p>특정 인덱스의 샤드를 보려면 URL에 인덱스 이름(예: sensor)을 추가하면 됩니다:</p><pre><code>GET _cat/shards/sensor</code></pre><p>이 명령은 다음 예제와 같은 출력을 생성합니다. 기본적으로 표시되는 열에는 인덱스의 이름, 이름(예 번호), 기본 샤드인지 복제본인지 여부, 샤드의 상태, 문서 수, 디스크의 크기, 샤드가 위치한 노드의 IP 주소와 노드 ID를 확인할 수 있습니다.</p><pre><code>sensor 5 p STARTED    0  283b 127.0.0.1 ziap
sensor 5 r UNASSIGNED                  
sensor 2 p STARTED    1 3.7kb 127.0.0.1 ziap
sensor 2 r UNASSIGNED                  
sensor 3 p STARTED    3 7.2kb 127.0.0.1 ziap
sensor 3 r UNASSIGNED                  
sensor 1 p STARTED    1 3.7kb 127.0.0.1 ziap
sensor 1 r UNASSIGNED                  
sensor 4 p STARTED    2 3.8kb 127.0.0.1 ziap
sensor 4 r UNASSIGNED                  
sensor 0 p STARTED    0  283b 127.0.0.1 ziap
sensor 0 r UNASSIGNED</code></pre><h2>복제본 이해</h2><p>각 샤드에는 데이터의 단일 사본이 포함되지만 인덱스에는 여러 개의 샤드 사본이 포함될 수 있습니다. 따라서 샤드에는 <strong>기본 샤드와</strong> <strong>복제본</strong>, 즉 복제본의 두 가지 유형이 있습니다. 기본 샤드의 각 복제본은 항상 다른 노드에 위치하므로 노드 장애 발생 시 데이터의 고가용성을 보장합니다. 복제본은 중복성과 데이터 손실 및 다운타임을 방지하는 역할 외에도 쿼리를 기본 샤드와 병렬로 처리하여 더 빠르게 처리함으로써 검색 성능을 향상시키는 데 도움이 될 수 있습니다.</p><p>기본 샤드와 복제본 샤드의 작동 방식에는 몇 가지 중요한 차이점이 있습니다. 둘 다 쿼리를 처리할 수 있지만 인덱싱 요청(예 인덱스에 데이터 추가)는 복제 샤드에 복제되기 전에 먼저 기본 샤드를 거쳐야 합니다. 위에서 언급했듯이 노드 연결이 끊기거나 하드웨어 장애 등으로 인해 기본 샤드를 사용할 수 없게 되면 복제본이 그 역할을 대신하도록 승격됩니다.</p><p>복제본은 노드 장애 발생 시 도움이 될 수 있지만, 인덱싱할 때 메모리, 디스크 공간, 컴퓨팅 성능을 소모하므로 복제본을 너무 많이 보유하지 않는 것이 중요합니다. 기본 샤드와 복제본의 또 다른 차이점은 인덱스가 생성된 후에는 기본 샤드의 수를 변경할 수 없지만, 복제본의 수는 인덱스 설정을 업데이트하여 언제든지 동적으로 변경할 수 있다는 점입니다.</p><p>복제본에서 고려해야 할 또 다른 요소는 사용 가능한 노드 수입니다. 복제본은 항상 기본 샤드와 다른 노드에 배치되는데, 동일한 노드에 동일한 데이터의 복사본이 두 개 있으면 노드에 장애가 발생할 경우 아무런 보호 기능을 제공하지 못하기 때문입니다. 따라서 시스템에서 <em>n개의</em> 복제본을 지원하려면 클러스터에 최소 <em>n + 1개의</em> 노드가 있어야 합니다. 예를 들어 클러스터에 노드가 2개 있고 인덱스가 6개의 복제본으로 구성된 경우, 복제본은 1개만 할당됩니다. 반면, 7개의 노드가 있는 시스템은 하나의 기본 샤드와 6개의 복제본을 완벽하게 처리할 수 있습니다.</p><h2>샤드 및 복제본 최적화</h2><p>기본 샤드와 복제본 샤드의 균형이 적절한 인덱스가 생성된 후에도 시간이 지남에 따라 인덱스 주변의 역학 관계가 변하기 때문에 이를 모니터링해야 합니다. 예를 들어 시계열 데이터를 다룰 때는 일반적으로 최근 데이터가 있는 인덱스가 오래된 데이터보다 더 활성화되어 있습니다. 이러한 인덱스를 조정하지 않으면 요구 사항이 매우 다름에도 불구하고 모두 동일한 양의 리소스를 소비하게 됩니다.</p><p>롤오버 인덱스 API는 최신 인덱스와 이전 인덱스를 분리하는 데 사용할 수 있습니다. 특정 임계값(디스크의 인덱스 크기, 문서 수 또는 기간 등)에 도달하면 자동으로 새 인덱스를 생성하도록 설정할 수 있습니다. 이 API는 샤드 크기를 제어하는 데도 유용합니다. 인덱스 생성 후에는 샤드 수를 쉽게 변경할 수 없기 때문에 롤오버 조건이 충족되지 않으면 샤드는 계속해서 데이터를 축적합니다. 자주 액세스하지 않는 오래된 인덱스의 경우, 인덱스 축소와 강제 병합은 메모리와 디스크 공간을 줄이는 두 가지 방법입니다. 전자는 인덱스의 샤드 수를 줄이고, 후자는 Lucene 세그먼트 수를 줄이며 삭제된 문서가 사용하는 공간을 확보합니다.</p><h2>기본 샤드와 복제본 샤드를 Elasticsearch의 기반으로 사용</h2><p>Elasticsearch는 방대한 양의 데이터를 위한 분산 저장, 검색 및 분석 플랫폼으로서 강력한 명성을 쌓아왔습니다. 그러나 이러한 규모로 운영할 때는 필연적으로 문제가 발생할 수밖에 없습니다. 따라서 기본 샤드와 복제본 샤드의 작동 방식을 이해하는 것은 플랫폼의 안정성과 성능을 최적화하는 데 도움이 될 수 있기 때문에 Elasticsearch에서 매우 중요하고 기본이 되는 이유입니다.</p><p>작동 방식과 최적화 방법을 아는 것은 보다 강력하고 성능이 우수한 Elasticsearch 클러스터를 달성하는 데 매우 중요합니다. 쿼리 응답이 느리거나 서비스 중단이 자주 발생하는 경우 이 지식이 이러한 장애물을 극복하는 열쇠가 될 수 있습니다.</p><p><a href="https://www.elastic.co/docs/deploy-manage/distributed-architecture/clusters-nodes-shards">클러스터, 노드 및</a> <a href="https://www.elastic.co/docs/deploy-manage/production-guidance/optimize-performance/size-shards"></a>샤드, <a href="https://www.elastic.co/docs/deploy-manage/distributed-architecture/shard-allocation-relocation-recovery">샤드 크기 조정 방법, 샤드 할당 및 복구에</a> 대해 자세히 알아보려면 Elasticsearch의 공식 설명서를 참조하세요.</p><p>이 주제는 <a href="https://youtu.be/sAySPSyL2qE">Elastic 커뮤니티 YouTube 채널에서</a>입문 과정으로도 제공됩니다.</p><p>마지막으로, 노드, 샤드 또는 복제본에 대해 걱정하고 싶지 않으시다면 <a href="https://www.elastic.co/docs/deploy-manage/deploy/elastic-cloud/serverless">Elastic Cloud Serverless를</a> 사용해 보세요. 이 Elastic Cloud 제품은 Elastic에서 완전히 관리하며 워크로드에 따라 확장할 수 있도록 자동화되어 있습니다. 무료 평가판을 통해 서버리스 접근 방식의 다른 이점에 대해 알아볼 수 있습니다.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Elasticsearch 인덱스의 필드를 표시하는 방법]]></title>
            <description><![CDATA[맵핑 및 _검색 API, 하위 필드, 합성 _소스 및 런타임 필드를 사용하여 Elasticsearch 인덱스의 필드를 표시하는 방법을 알아보세요.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/elasticsearch-index-show-fields</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/elasticsearch-index-show-fields</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[JD Armada]]></dc:creator>
            <pubDate>Wed, 06 Aug 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/23b96aaa1a38b1f4747b4a87695d816f24c0cf70-720x421.jpg?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>이 문서에서는 Elasticsearch 인덱스의 필드를 표시하는 방법에 대해 설명합니다. 이는 데이터 구조를 이해하고, 특정 필드를 식별하고, 문제를 해결하는 데 유용할 수 있습니다. 다음 주제를 다룰 예정입니다:</p><ol><li><code>_mapping</code> API를 사용하여 필드 정보 검색하기</li><li><code>_search</code> API를 사용하여 필드 값 표시</li><li>하위 필드 표시</li><li>Synthetic _source</li><li>런타임 필드</li></ol><h2>1. 맵핑 API를 사용하여 필드 정보 검색하기</h2><p><code>_mapping</code> API를 사용하면 인덱스 또는 여러 인덱스에 대한 매핑 정의를 검색할 수 있습니다. 여기에는 필드, 데이터 유형 및 기타 속성에 대한 정보가 포함됩니다. 특정 인덱스에 대한 매핑을 검색하려면 다음 요청을 사용하세요:</p><pre><code>GET /<index_name>/_mapping</code></pre><p>예를 들어 <code>my_index</code> 이라는 인덱스가 있는 경우 다음 요청으로 해당 인덱스의 매핑을 검색할 수 있습니다:</p><pre><code>GET /my_index/_mapping</code></pre><p>응답에는 필드 및 해당 속성에 대한 정보가 포함된 인덱스에 대한 매핑 정의가 포함됩니다.</p><p>특정 필드에 대한 매핑을 검색할 수도 있습니다. 매핑이 상당히 크고 특정 필드에만 집중하려는 경우 유용할 수 있습니다. 특정 필드의 매핑을 검색하려면 다음 요청을 사용하세요:</p><pre><code>GET /my_index/_mapping/field/my_field</code></pre><p>다음 요청에서와 같이 쉼표로 이름을 구분하여 여러 필드의 매핑을 검색할 수도 있습니다:</p><pre><code>GET /my_index/_mapping/field/my_field_1,my_field_2,my_field_3</code></pre><h2>2. search API를 사용하여 필드 값 표시하기</h2><p>Elasticsearch 인덱스의 필드 값을 표시하려면 <code>_search</code> API를 사용하면 됩니다. <code>_search</code> API는 반환되는 필드를 제어할 수 있는 다양한 방법을 제공하며, 두 가지 주요 방법은 다음과 같습니다:</p><ol><li><strong><code>_source</code></strong>: <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/mapping-source-field"><code>_source</code></a> 필드에는 수집 파이프라인이나 전처리 단계에 의해 변경된 사항을 포함하여 색인된 그대로의 원본 JSON 문서 본문이 포함되어 있습니다. 소스 문서의 특정 필드를 표시하려면 아래에서 설명하는 대로 소스 필터링을 구현합니다.</li><li><strong><code>fields</code></strong>: <a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/retrieve-selected-fields"><code>fields</code></a> 매개변수를 사용하면 색인 매핑을 기반으로 검색을 수행할 때 문서에서 특정 필드를 검색할 수 있습니다. <code>_source</code> 과 달리 <code>fields</code> 은 <code>_source</code> 을 참조하지 않고 저장된 필드, 문서 값 또는 런타임 필드의 값을 반환할 수도 있지만 문서 값이나 저장된 설정이 없는 표준 필드의 경우 <code>_source</code> 으로 되돌아갑니다. 이는 아래에서 살펴보겠지만 성능 등 많은 이점을 가져올 수 있습니다.</li></ol><h3>소스<code> </code>필드 사용</h3><p>기본적으로<code> _search</code> API는 색인된 원본 JSON 문서가 포함된 <code>_source</code> 필드를 반환합니다. 특정 필드를 표시하려면 검색 요청의 <code>_source </code>매개변수에 필터를 추가할 수 있으며, 이를 소스 필터링이라고 합니다.</p><p>다음은 <code>my_index</code> 인덱스에 있는 문서에 대한 <code>title </code>및 <code>author</code> 필드 값을 반환하는 검색 요청의 예입니다:</p><pre><code>GET /my_index/_search
{
  "query": {
    "match_all": {}
  },
  "_source": ["title", "author"]
}</code></pre><p>이 예제에서 <code>_source</code> 매개변수는 반환할 필드를 지정합니다.</p><p>더 많은 제어가 필요한 경우 <code>_source</code> 객체의 <code>includes</code> 및 <code>excludes </code>속성을 사용할 수 있습니다. 예를 들어 아래 쿼리는 최상위 수준 <code>title</code> 필드와 <code>author</code> 의 <code>author.description</code> 을 제외한 모든 하위 필드를 반환합니다.</p><pre><code>GET /my_index/_search
{
  "query": {
    "match_all": {}
  },
  "_source": {
     “includes”: [“title”, “author.*],
     “excludes”: [“author.description”]
  }
}</code></pre><p>이 예제에서는 <code>author.* </code>패턴을 사용하여 <code>author </code>객체의 모든 직접 하위 필드를 검색합니다. 그런 다음 <code>author.description </code>을 명시적으로 제외하여 다른 작성자 필드만 반환되도록 합니다. 이 경우에도 여전히 소스 JSON을 로드하고 구문 분석해야 하므로 성능이 향상되지는 않지만 네트워크를 통해 전송되는 응답의 크기를 줄일 수 있다는 점에 유의하세요.</p><h3>필드 매개변수 사용</h3><p><code>fields</code> 매개변수를 사용하여 검색 응답에 반환되는 필드를 필터링할 수 있습니다. <code>_source</code> 대신 <code>fields</code> 을 사용하면 다음과 같은 여러 가지 이점이 있습니다:</p><ul><li><strong>성능 개선: </strong><code>fields </code>은 전체 <code>_source</code> 을 로드할 필요 없이 <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/mapping-store">저장된 필드</a> 또는 <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/doc-values">문서 값에서</a> 직접 값을 반환할 수 있으므로 응답 페이로드 크기가 더 작아집니다.</li><li><strong>형식화된 출력:</strong> 표준 필드의 경우<code> fields</code> 은 <code>_source</code> 으로 되돌아가 값을 가져올 수 있지만, 인덱스 매핑을 확인하여 형식이 지정된 날짜와 같은 출력의 형식을 적절히 지정하여 집계 및 정렬에 사용되는 것과 일관성을 유지합니다.</li><li><strong>런타임 필드에 대한 액세스:</strong> <code>fields</code> 은 원본 <code>_source</code> 에 없는 런타임 필드를 반환할 수 있습니다.</li><li>더 많은 혜택은 <a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/retrieve-selected-fields#search-fields-param">여기에서</a> 확인할 수 있습니다.</li></ul><p>예를 들어 <code>my_index</code> 인덱스에서 <code>title</code> 및 <code>author</code> 필드만 반환하려면 다음 검색 요청을 사용할 수 있습니다:</p><pre><code>GET /my_index/_search
{
  "query": {
    "match_all": {}
  },
  "fields": ["title", "author"],
  "_source": false
}</code></pre><p>위의 쿼리에서는 소스 문서를 반환하지 않도록 <code>_source </code>필드를 false로 설정했습니다. 이렇게 하면 응답의 페이로드 크기를 크게 최소화할 수 있지만 <code>title</code> 및 <code>author</code> 필드가 <code>keyword </code>필드 유형이고 기본적으로 <code>doc_values</code> 이 활성화되어 있기 때문에 작동한다는 점을 기억하세요. 필드에 <code>doc_values</code> 가 활성화되어 있지 않고 <code>_source</code> 가 false로 설정되어 있으면, Elasticsearch는 이를 검색할 방법이 없으며 응답에서 건너뛰게 됩니다.</p><p><code>fields</code> 응답은 값이 하나만 있는 경우에도 항상 각 필드에 대한 값 배열을 반환한다는 점에 유의하세요. 이는 Elasticsearch에 전용 배열 유형이 없고 모든 필드에 여러 개의 값이 있을 수 있기 때문입니다. Elasticsearch의 배열에 대한 자세한 내용을 보려면 <a href="http://elastic.co/docs/reference/elasticsearch/mapping-reference/array">여기를</a> 클릭하세요.</p><h3>필드를 검색하는 다른 방법</h3><p><code>_source</code> 또는 <code>fields</code> 을 사용하여 필드를 검색하는 것이 권장되는 방법이지만, 특정 사용 사례에 따라 다음과 같은 다양한 방법을 사용할 수 있습니다:</p><p><strong>문서 값 필드:</strong> <code>_source</code> <code><a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/retrieve-selected-fields#docvalue-fields">docvalue_fields</a> </code>매개 변수를 사용하여 검색할 수 있습니다. 문서 값은 <code>_source</code> 과 동일한 필드 값을 저장하지만 정렬 및 집계에 최적화된 온디스크 데이터 구조로 저장합니다.</p><p><code>_source</code> 에 저장된 값과는 별개이므로 전체 <code>_source</code> 를 로드하지 않고도 특정 필드를 요청할 수 있습니다. 이 기능은 대규모 문서를 쿼리하지만 문서 값을 지원하는 작은 필드 몇 개만 필요한 경우에 유용합니다. <code>docvalue_fields </code>사용의 또 다른 사용 사례는 아래 예제에서 볼 수 있듯이 <code>date</code> 및 <code>numeric</code> 필드에 사용자 지정 서식을 사용하려는 경우입니다.</p><p><code>doc_values</code> 을 활성화한 필드 또는 <code>keyword</code>, <code>date</code>, 숫자 유형 및 <code>boolean</code> 과 같이 기본적으로 활성화된 필드 유형에 대해서만 작동하며, <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/text"><code>text</code></a> 또는 <a href="https://www.elastic.co/docs/reference/elasticsearch/plugins/mapper-annotated-text-usage"><code>annotated_text</code></a> 에는 작동하지 않습니다.</p><p>이 예에서는 <code>docvalue_fields</code> 매개변수를 사용하여 전체 <code>_source</code> 문서를 로드하지 않고 <code>title</code>, <code>author</code>, <code>published</code> 필드를 검색합니다:</p><pre><code>GET /my_index/_search
{
  "query": {
    "match_all": {}
  },
  "docvalue_fields": [
    "title",
    "author",
    {
      "field": "published",
      "format": "epoch_millis"
    }
  ],
  "_source": false
}</code></pre><p>이 쿼리가 실행되면 Elasticsearch는 각 문서에 대해 <code>_source </code>을 참조하는 대신 온디스크 컬럼형 저장소에서 직접 값을 가져옵니다. <code>published</code> 필드는 쿼리에 제공된 <code>format</code> 매개변수 덕분에 기본 형식이 아닌 <code>epoch_millis</code> 형식으로 반환됩니다.</p><p><strong>저장된 필드:</strong> 매핑에서 특정 필드를 <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/mapping-store">저장된</a> 것으로 명시적으로 표시한 경우 <code>stored_fields</code> 매개변수를 사용하여 해당 필드를 필터링할 수 있습니다. 특정 필드에 대해서만 가벼운 응답을 원하거나 나중에 검색할 수 있도록 의도적으로 저장한 필드에 대해 이 기능을 사용하면 유용합니다. <code>_source</code> 과 별도로 저장되므로 이 방법은 <code>_source</code> 을 로드할 필요가 없는 경우에도 유용합니다.</p><p>이 옵션은 기본적으로 꺼져 있으며 일반적으로 권장되지 않는다는 점에 유의하세요. 원본 소스 문서의 특정 하위 집합을 반환하려면 대신 소스 필터링을 사용하세요.</p><p>아래 예제 쿼리에서는 <code>stored_fields</code> 매개 변수를 사용하여 &quot;<code>store”: true</code>&quot; 인덱스 매핑 구성이 있는 <code>summary</code> 필드를 검색합니다.</p><pre><code>GET /my_index/_search
{
  "query": {
    "match_all": {}
  },
  "stored_fields": ["summary"]
}</code></pre><p>이 쿼리가 실행되면 Elasticsearch는 이 필드가 <code>”store”: true</code> 로 표시되어 있는지 확인하며, 이 필드를 찾지 못하면 필드를 완전히 건너뜁니다.</p><h2>3. 하위 필드 표시</h2><p>인덱스에 하위 필드가 포함된 경우 점 표기법을 사용하여 <code>fields</code> 매개변수에서 필드 경로를 지정할 수 있습니다. 하위 필드는 <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/nested">중첩된 필드 유형과</a> 다르다는 점에 유의하세요. 예를 들어 <code>address.city</code> 이라는 이름의 하위 필드가 있는 경우 다음과 같이 검색 응답에 포함할 수 있습니다:</p><pre><code>GET /my_index/_search
{
  "query": {
    "match_all": {}
  },
  "fields": ["title", "author", "address.city"],
  "_source": false
}</code></pre><p>이 예제에서는 검색 응답에 <code>title</code>, <code>author</code>, <code>address.city</code> 필드의 값이 포함됩니다.</p><h2>4. 합성 _소스</h2><p><code> _source</code> 사용 기능을 유지하면서 디스크 공간도 절약하려면 인덱스 매핑에 합성 <code>_source</code> 을 사용하는 옵션이 있습니다. <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/mapping-source-field#synthetic-source">합성 <code>_source</code></a> 은 <code>_source</code> 이 비활성화되어 있는 경우에도 Elasticsearch가 저장된 필드 및 문서 값과 같은 기존 데이터로부터 <code>_source</code> 을 재구성할 수 있도록 하는 기능입니다. 이렇게 하면 재구성이 즉시 이루어지므로 쿼리 시 속도가 약간 느려지는 대신 저장 공간을 많이 절약할 수 있습니다. 인덱스 설정에서 아래 값을 사용하여 이 기능을 사용하도록 설정합니다:</p><pre><code>PUT idx
{
  "settings": {
    "index": {
      "mapping": {
        "source": {
          "mode": "synthetic"
        }
      }
    }
  }
}</code></pre><p><code>_search</code> API를 사용할 때 전체 문서 표시, 소스 필터링, <code>_source</code> 를 사용할 수 있을 것으로 기대하는 Kibana와 같은 다른 기능 및 도구와의 호환성, 전체 <code>_source</code> 문서를 저장할 필요가 없는 것 등이 합성 <code>_source </code>사용의 몇 가지 이점입니다.</p><h2>5. 런타임 필드</h2><p>런타임 <a href="https://www.elastic.co/docs/manage-data/data-store/mapping/runtime-fields">필드를</a> 사용하면 쿼리 시 또는 런타임 블록 아래의 인덱스 매핑에서 스크립트 필드를 정의할 수 있습니다. 이러한 필드는 색인화되지 않으므로 런타임 필드를 추가해도 색인 크기가 증가하지는 않지만 <code>_source</code> 에 표시되지 않습니다. 매핑에 정의된 런타임 필드는 영구적이며 모든 쿼리에서 사용할 수 있는 반면, 쿼리 시점에 정의된 런타임 필드는 임시적이며 해당 검색 요청에서만 사용할 수 있습니다.</p><p>런타임 필드 사용의 주요 이점은 이미 수집한 후 문서에 필드를 추가할 수 있어 매핑 결정을 간소화할 수 있다는 점입니다. 런타임 필드는 문자열 서식 지정이나 점수 계산과 같이 원본 문서에는 없지만 스크립트를 사용하여 생성된 값으로 문서를 보강하는 데도 유용합니다.</p><p>또한 런타임 필드는 결과 집합의 모든 문서에 대해 스크립트를 실행해야 하므로 성능이 저하될 수 있다는 점도 유의할 필요가 있습니다. <a href="https://www.elastic.co/docs/manage-data/data-store/mapping/retrieve-runtime-field">런타임 필드를 검색하려면</a> <code>_search</code> API에서 <code>fields</code> 매개 변수를 사용할 수도 있습니다.</p><h2>결론</h2><p>Elasticsearch 인덱스의 필드를 표시하는 방법은 인덱스 매핑 또는 <code>_source</code> 을 사용하여 단순히 값을 검색하는 것부터 <code>fields</code>, <code>docvalue_fields</code> 또는 제어 및 효율성을 높이기 위한 런타임 필드를 사용하는 고급 방법까지 다양합니다. 검색 환경을 최적화하려면 다양한 방법 간의 장단점을 이해하는 것이 중요합니다. 페이로드를 최적화하든, 문서를 보강하든, 저장 공간을 절약하기 위해 합성 <code>_source</code> 을 사용하든, Elasticsearch는 필요한 데이터를 필요한 방식으로 찾을 수 있는 여러 가지 도구와 기능을 제공합니다. 이러한 기법은 데이터 구조를 이해하고, 특정 필드를 식별하고, 문제를 해결하는 데 도움이 될 수 있습니다.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[AI 기반 대시보드: 비전에서 Kibana까지]]></title>
            <description><![CDATA[이미지를 처리하기 위해 LLM을 사용해 대시보드를 생성하고 이를 Kibana 대시보드로 전환합니다.
]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/ai-powered-dashboards</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/ai-powered-dashboards</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Jeffrey Rengifo, Tomás Murúa]]></dc:creator>
            <pubDate>Wed, 16 Jul 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/eb60ceb2fbc3941745b21ae3357cbb6ea8fab18c-1443x811.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p><a href="https://www.elastic.co/kibana/kibana-lens">Kibana Lens를</a> 사용하면 대시보드를 매우 간단하게 끌어서 놓을 수 있지만, 수십 개의 패널이 필요한 경우에는 클릭 수가 늘어납니다. 대시보드를 스케치하고 스크린샷을 찍어 LLM이 전체 프로세스를 완료하도록 할 수 있다면 어떨까요?</p><p>이 글에서는 이를 실현하는 방법에 대해 설명합니다. 대시보드의 이미지를 가져와서 매핑을 분석한 다음 Kibana를 전혀 건드리지 않고도 대시보드를 생성하는 애플리케이션을 만들어 보겠습니다!</p><p><strong>단계</strong>:</p><ol><li><a href="https://www.elastic.co/search-labs/blog/ai-powered-dashboards#background-&amp;-application-workflow">배경 &amp; 애플리케이션 워크플로</a></li><li><a href="https://www.elastic.co/search-labs/blog/ai-powered-dashboards#prepare-data">데이터 준비</a></li><li><a href="https://www.elastic.co/search-labs/blog/ai-powered-dashboards#llm-configuration">LLM 구성</a></li><li><a href="https://www.elastic.co/search-labs/blog/ai-powered-dashboards#application-functions">애플리케이션 기능</a></li></ol><h2>배경 &amp; 애플리케이션 워크플로</h2><p>가장 먼저 떠오른 생각은 LLM이 전체 NDJSON 형식의 Kibana <a href="https://www.elastic.co/docs/explore-analyze/find-and-organize/saved-objects">저장 개체를</a> 생성한 다음 Kibana로 가져오도록 하는 것이었습니다.</p><p>몇 가지 모델을 사용해 보았습니다:</p><ul><li>Gemini 2.5 프로</li><li>GPT o3 / o4-미니 하이 / 4.1</li><li>클로드 4 소네트</li><li>Grok 3</li><li>딥씽크(딥씽크 R1)</li></ul><p>그리고 프롬프트는 간단하게 시작했습니다:</p><pre><code>You are an Elasticsearch Saved-Object generator (Kibana 9.0).
INPUTS
=====
1. PNG screenshot of a 4-panel dashboard (attached).
2. Index mapping (below) – trimmed down to only the fields present in the screenshot.
3. Example NDJSON of *one* metric visualization (below) for reference.

TASK
====
Return **only** a valid NDJSON array that recreates the dashboard exactly:
* 2 metric panels (Visits, Unique Visitors)
* 1 pie chart (Most used OS)
* 1 vertical bar chart (State Geo Dest)
* Use index pattern `kibana_sample_data_logs`.
* Preserve roughly the same layout (2×2 grid).
* Use `panelIndex` values 1-4 and random `id` strings.
* Kibana version: 9.0</code></pre><p>각 비주얼리제이션을 작성하는 방법에 대한 <a href="https://www.elastic.co/search-labs/blog/function-calling-with-elastic#:~:text=Few%2Dshot%20prompting%20involves%20providing%20examples%20of%20the%20types%20of%20queries%20you%20want%20it%20to%20return%2C%20which%20helps%20in%20increasing%20consistency.">몇 가지 예시와</a> 자세한 설명을 살펴봤지만 운이 없었습니다. 이 실험에 관심이 있으시다면 <a href="https://gist.github.com/TomasMurua/a78dc283e115624731beffc98984b70b">여기에서</a> 자세한 내용을 확인할 수 있습니다.</p><p>이 접근 방식을 사용한 결과, LLM에서 생성된 파일을 Kibana에 업로드하려고 할 때 이러한 메시지가 표시되었습니다:</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/2b599443b5613c9f0fc3235581614add5b4b3900-891x98.png" style="width: 100%;" /><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/d87ccfc033bc00ee8188c5cae18043fbca22784c-741x233.png" style="width: 100%;" /><p>이는 생성된 JSON이 유효하지 않거나 형식이 잘못되었음을 의미합니다. 가장 흔한 문제는 불완전한 NDJSON을 생성하거나, 매개변수를 착각하거나, 아무리 강제 적용을 시도해도 NDJSON 대신 일반 JSON을 반환하는 LLM이었습니다.</p><p><a href="https://www.elastic.co/docs/solutions/search/search-templates">검색 템플릿이</a> LLM 프리스타일보다 더 효과적이었다는 <a href="https://www.elastic.co/search-labs/blog/llm-functions-elasticsearch-intelligent-query">이 글에서 영감을</a> 받아 전체 NDJSON 파일을 생성하도록 요청하는 대신 템플릿을 LLM에 제공하고 코드에서 LLM이 제공한 매개변수를 사용하여 적절한 시각화를 만들기로 결정했습니다. 이 접근 방식은 실망스럽지 않았고 예측 가능하고 확장 가능하며 이제 코드가 무거운 작업을 수행하므로 LLM이 아닌 코드가 작업을 수행하게 되었습니다.</p><p>애플리케이션 워크플로우는 다음과 같습니다:</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/52c587cf0cf3517fdd4ee7ab95581dd4f2bce030-725x668.png" style="width: 100%;" /><p></p><p><em>간단하게 설명하기 위해 일부 코드는 생략하지만, 전체 애플리케이션의 작업 코드는 <a href="https://github.com/elastic/elasticsearch-labs/tree/main/supporting-blog-content/from-image-idea-to-kibana-dashboard-using-ai/from-image-idea-to-kibana-dashboard-using-ai.ipynb"><strong>이</strong></a> 노트북에서</em>찾을 수 있습니다.</p><h2>필수 구성 요소</h2><p>개발을 시작하기 전에 다음이 필요합니다:</p><ol><li>Python 3.8 이상</li><li><a href="https://docs.python.org/3/library/venv.html">Venv</a> Python 환경</li><li>엔드포인트 및 API 키와 함께 실행 중인 Elasticsearch 인스턴스</li><li>환경 변수 이름 OPENAI_API_KEY에 저장된 OpenAI API 키입니다:</li></ol><pre><code>export OPENAI_API_KEY="your-openai-api-key"</code></pre><h2>데이터 준비</h2><p>데이터의 경우, 간단하게 유지하면서 Elastic 샘플 웹 로그를 사용하겠습니다. <a href="https://www.elastic.co/docs/manage-data/ingest/sample-data#add-sample-data-sets">여기에서</a> 해당 데이터를 클러스터로 가져오는 방법을 알아보세요.</p><p>각 문서에는 애플리케이션에 요청을 보낸 호스트에 대한 세부 정보와 함께 요청 자체 및 응답 상태에 대한 정보가 포함되어 있습니다. 아래는 문서 예시입니다:</p><pre><code>{
    "agent": "Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24",
    "bytes": 8509,
    "clientip": "70.133.115.149",
    "extension": "css",
    "geo": {
        "srcdest": "US:IT",
        "src": "US",
        "dest": "IT",
        "coordinates": {
            "lat": 38.05134111,
            "lon": -103.5106908
        }
    },
    "host": "cdn.elastic-elastic-elastic.org",
    "index": "kibana_sample_data_logs",
    "ip": "70.133.115.149",
    "machine": {
        "ram": 5368709120,
        "os": "osx"
    },
    "memory": null,
    "message": "70.133.115.149 - - [2018-08-30T23:35:31.492Z] \"GET /styles/semantic-ui.css HTTP/1.1\" 200 8509 \"-\" \"Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24\"",
    "phpmemory": null,
    "referer": "http://twitter.com/error/john-phillips",
    "request": "/styles/semantic-ui.css",
    "response": 200,
    "tags": [
        "success",
        "info"
    ],
    "@timestamp": "2025-07-03T23:35:31.492Z",
    "url": "https://cdn.elastic-elastic-elastic.org/styles/semantic-ui.css",
    "utc_time": "2025-07-03T23:35:31.492Z",
    "event": {
        "dataset": "sample_web_logs"
    },
    "bytes_gauge": 8509,
    "bytes_counter": 51201128
}</code></pre><p>이제 방금 로드한 인덱스( <code>kibana_sample_data_logs</code>)의 매핑을 가져와 보겠습니다:</p><pre><code>INDEX_NAME = "kibana_sample_data_logs"

es_client = Elasticsearch(
    [os.getenv("ELASTICSEARCH_URL")],
    api_key=os.getenv("ELASTICSEARCH_API_KEY"),
)

result = es_client.indices.get_mapping(index=INDEX_NAME)
index_mappings = result[list(result.keys())[0]]["mappings"]["properties"]</code></pre><p>나중에 로드할 이미지와 함께 매핑을 전달하겠습니다.</p><h2>LLM 구성</h2><p><a href="https://python.langchain.com/docs/concepts/structured_outputs/">구조화된 출력을</a> 사용하여 이미지를 입력하고 함수에 전달해야 하는 정보가 포함된 JSON을 수신하여 JSON 객체를 생성하도록 LLM을 구성해 보겠습니다.</p><p>종속성을 설치합니다:</p><pre><code>pip install elasticsearch pydantic langchain langchain-openai -q</code></pre><p>Elasticsearch는 <a href="https://www.elastic.co/docs/manage-data/data-store/mapping">인덱스 매핑을</a> 검색하는 데 도움이 됩니다. Pydantic을 사용하면 파이썬으로 스키마를 정의한 다음 LLM에 따르도록 요청할 수 있으며, <a href="https://www.elastic.co/search-labs/integrations/langchain">LangChain은</a> LLM과 AI 도구를 더 쉽게 호출할 수 있도록 도와주는 프레임워크입니다.</p><p>LLM에서 원하는 출력을 정의하기 위해 Pydantic 스키마를 생성합니다. 이미지에서 알아야 할 것은 차트 유형, 필드, 비주얼리제이션 제목 및 대시보드 제목입니다:</p><pre><code>class Visualization(BaseModel):
    title: str = Field(description="The dashboard title")
    type: List[Literal["pie", "bar", "metric"]]
    field: str = Field(
        description="The field that this visualization use based on the provided mappings"
    )


class Dashboard(BaseModel):
    title: str = Field(description="The dashboard title")
    visualizations: List[Visualization]</code></pre><p>이미지 입력은 제가 방금 그린 대시보드를 보내드리겠습니다:</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/36441d7b5dc1f3ff2ac2a30710208d57ad41c716-1600x898.jpg" style="width: 100%;" /><p>이제 LLM 모델 호출과 이미지 로딩을 선언합니다. 이 함수는 생성하려는 대시보드의 이미지와 Elasticsearch 인덱스의 매핑을 수신합니다.</p><p><code>with_structured_output</code> 을 사용하면 Pydantic <code>Dashboard</code> 스키마를 LLM이 생성할 응답 객체로 사용할 수 있습니다. <a href="https://docs.pydantic.dev/latest/">Pydantic을</a> 사용하면 유효성 검사를 통해 데이터 모델을 정의할 수 있으므로 LLM 출력이 예상 구조와 일치하는지 확인할 수 있습니다.</p><p>이미지를 base64로 변환하여 입력으로 보내려면 <a href="https://www.base64-image.de/">온라인 변환기를</a> 사용하거나 <a href="https://www.geeksforgeeks.org/python-convert-image-to-string-and-vice-versa/">코드로</a> 변환할 수 있습니다.</p><pre><code>prompt = f"""
    You are an expert in analyzing Kibana dashboards from images for the version 9.0.0 of Kibana.

    You will be given a dashboard image and an Elasticsearch index mapping.

    Below are the index mappings for the index that the dashboard is based on.
    Use this to help you understand the data and the fields that are available.

    Index Mappings:
    {index_mappings}

    Only include the fields that are relevant for each visualization, based on what is visible in the image.
    """

message = [
    {
        "role": "user",
        "content": [
            {"type": "text", "text": prompt},
            {
                "type": "image",
                "source_type": "base64",
                "data": image_base64,
                "mime_type": "image/png",
            },
        ],
    }
]


try:
    llm = init_chat_model("gpt-4.1-mini")
    llm = llm.with_structured_output(Dashboard)
    dashboard_values = llm.invoke(message)

    print("Dashboard values generated by the LLM successfully")
    print(dashboard_values)
except Exception as e:
    print(f"Failed to analyze image and match fields: {str(e)}")</code></pre><p>LLM에는 이미 Kibana 대시보드에 대한 컨텍스트가 있으므로 프롬프트에서 모든 것을 설명할 필요는 없으며, Elasticsearch 및 Kibana와 함께 작동한다는 것을 잊지 않도록 하기 위한 몇 가지 세부 사항만 설명하면 됩니다.</p><p>프롬프트를 자세히 살펴 보겠습니다:</p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">섹션</td><td style="border: 1px solid #ccc; padding: 4px;">이유</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">귀하는 Kibana 버전 9.0.0의 이미지에서 Kibana 대시보드를 분석하는 전문가입니다.</td><td style="border: 1px solid #ccc; padding: 4px;">이를 강화하는 것이 바로 Elasticsearch이며, Elasticsearch 버전은 LLM이 오래되거나 유효하지 않은 매개변수를 착각할 가능성을 줄여줍니다.</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">대시보드 이미지와 Elasticsearch 인덱스 매핑이 제공됩니다.</td><td style="border: 1px solid #ccc; padding: 4px;">LLM의 잘못된 해석을 피하기 위해 이미지가 대시보드에 관한 것이라고 설명합니다.</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">다음은 대시보드의 기반이 되는 인덱스의 인덱스 매핑입니다. 이를 사용하여 데이터와 사용 가능한 필드를 이해하는 데 도움이 됩니다. 인덱스 매핑: {index_mappings}</td><td style="border: 1px solid #ccc; padding: 4px;">LLM이 유효한 필드를 동적으로 선택할 수 있도록 매핑을 제공하는 것이 중요합니다. 그렇지 않으면 여기에 매핑을 하드 코딩하여 너무 딱딱하게 만들거나 올바른 필드 이름이 포함된 이미지에 의존할 수 있는데, 이는 신뢰할 수 없습니다.</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">이미지에 표시되는 내용에 따라 각 시각화와 관련된 필드만 포함합니다.</td><td style="border: 1px solid #ccc; padding: 4px;">가끔 이미지와 관련이 없는 필드를 추가하려고 시도하기 때문에 이 기능을 추가해야 했습니다.</td>
                    </tr>
              </tbody>
            </table>
          <p>그러면 표시할 시각화 배열이 있는 객체가 반환됩니다:</p><pre><code>"Dashboard values generated by the LLM successfully
title=""Client, Extension, OS, and Response Keyword Analysis""visualizations="[
   "Visualization(title=""Count of Client IP",
   "type="[
      "metric"
   ],
   "field=""clientip"")",
   "Visualization(title=""Extension Keyword Distribution",
   "type="[
      "pie"
   ],
   "field=""extension.keyword"")",
   "Visualization(title=""Most Used OS",
   "type="[
      "bar"
   ],
   "field=""machine.os.keyword"")",
   "Visualization(title=""Response Keyword Distribution",
   "type="[
      "bar"
   ],
   "field=""response.keyword"")"
]</code></pre><h2>LLM 응답 처리</h2><p><strong> </strong>에서 샘플 2x2 패널 대시보드를 만든 다음 <a href="https://www.elastic.co/docs/api/doc/kibana/operation/operation-get-dashboards-dashboard">대시보드 가져오기 API를</a> 사용하여 JSON으로 내보낸 다음, 패널을 시각화 템플릿(파이, 막대, 메트릭)으로 저장하여 일부 매개 변수를 교체하여 질문에 따라 다른 필드로 새로운 시각화를 만들 수 있습니다.</p><p>템플릿 JSON 파일은 <a href="https://github.com/Delacrobix/elasticsearch-labs/tree/supporting-blog-content/from-image-idea-to-kibana-dashboard-using-ai/supporting-blog-content/from-image-idea-to-kibana-dashboard-using-ai/templates"><strong>여기에서</strong></a> 확인할 수 있습니다. 나중에 대체할 개체 값을 {<code>variable_name</code>}로 변경한 방법에 유의하세요.<br/></p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/ec7e1ac0cd8b470df13e60940162b56778acb386-315x234.png" style="width: 100%;" /><p>LLM이 제공한 정보를 바탕으로 어떤 템플릿을 사용하고 어떤 값을 대체할지 결정할 수 있습니다.</p><p><code>fill_template_with_analysis</code> 는 시각화의 JSON 템플릿, 제목, 필드, 그리드에 있는 시각화의 좌표 등 단일 패널에 대한 매개변수를 받습니다.</p><p>그런 다음 템플릿의 값을 바꾸고 최종 JSON 시각화를 반환합니다.</p><pre><code>def fill_template_with_analysis(
    template: Dict[str, Any],
    visualization: Visualization,
    grid_data: Dict[str, Any],
):
    template_str = json.dumps(template)
    replacements = {
	 "{visualization_id}": str(uuid.uuid4()),
        "{title}": visualization.title,
        "{x}": grid_data["x"],
        "{y}": grid_data["y"],
    }

    if visualization.field:
        replacements["{field}"] = visualization.field

    for placeholder, value in replacements.items():
        template_str = template_str.replace(placeholder, str(value))

    return json.loads(template_str)</code></pre><p>간단하게 하기 위해 LLM이 생성하기로 결정한 패널에 정적 좌표를 할당하고 위 이미지와 같은 2x2 그리드 대시보드를 생성합니다.</p><pre><code># Filling templates fields
panels = []    
grid_data = [
    {"x": 0, "y": 0},
    {"x": 12, "y": 0},
    {"x": 0, "y": 12},
    {"x": 12, "y": 12},
]


i = 0

for vis in dashboard_values.visualizations:
    for vis_type in vis.type:
        template = templates.get(vis_type, templates.get("bar", {}))
        filled_panel = fill_template_with_analysis(template, vis, grid_data[i])
        panels.append(filled_panel)
        i += 1</code></pre><p>LLM에서 결정한 시각화 유형에 따라 JSON 파일 템플릿을 선택하고 <code>fill_template_with_analysis</code> 을 사용하여 관련 정보를 바꾼 다음 새 패널을 나중에 대시보드를 만드는 데 사용할 배열에 추가합니다.</p><p>대시보드가 준비되면, <a href="https://www.elastic.co/docs/api/doc/kibana/operation/operation-post-dashboards-dashboard-id">대시보드 만들기 API를</a> 사용해 새 JSON 파일을 Kibana로 푸시하여 대시보드를 생성합니다:<br/></p><pre><code>try:
    dashboard_id = str(uuid.uuid4())

    # post request to create the dashboard endpoint
    url = f"{os.getenv('KIBANA_URL')}/api/dashboards/dashboard/{dashboard_id}"

    dashboard_config = {
        "attributes": {
            "title": dashboard_values.title,
            "description": "Generated by AI",
            "timeRestore": True,
            "panels": panels,  # Visualizations with the values generated by the LLM
            "timeFrom": "now-7d/d",
            "timeTo": "now",
        },
    }

    headers = {
        "Content-Type": "application/json",
        "kbn-xsrf": "true",
        "Authorization": f"ApiKey {os.getenv('ELASTICSEARCH_API_KEY')}",
    }

    requests.post(
        url,
        headers=headers,
        json=dashboard_config,
    )

    # Url to the generated dashboard
    dashboard_url = f"{os.getenv('KIBANA_URL')}/app/dashboards#/view/{dashboard_id}"

    print("Dashboard URL: ", dashboard_url)
    print("Dashboard ID: ", dashboard_id)

except Exception as e:
    print(f"Failed to create dashboard: {str(e)}")</code></pre><p>스크립트를 실행하고 대시보드를 생성하려면 콘솔에서 다음 명령을 실행합니다:</p><pre><code>python <file_name>.py</code></pre><p>최종 결과는 다음과 같습니다:</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/e909afbf0e47d9a6e0f7bd07dfb2efcfa5cf06ac-921x715.png" style="width: 100%;" /><h2>결론</h2><p>LLM은 텍스트를 코드로 변환하거나 이미지를 코드로 변환할 때 강력한 시각적 기능을 발휘합니다. 또한 대시보드 API를 사용하면 JSON 파일을 대시보드로 전환할 수 있으며, LLM과 몇 가지 코드를 사용하면 이미지를 Kibana 대시보드로 전환할 수 있습니다.</p><p>다음 단계는 다양한 그리드 설정, 대시보드 크기 및 위치를 사용하여 대시보드 시각적 요소의 유연성을 개선하는 것입니다. 또한 더 복잡한 시각화 및 시각화 유형에 대한 지원을 제공하면 이 애플리케이션에 유용한 추가 기능이 될 것입니다.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[고유한 패턴 공개 Elasticsearch의 중요 용어 집계에 대한 가이드]]></title>
            <description><![CDATA[중요 용어 집계를 사용하여 데이터에서 인사이트를 발견하는 방법을 알아보세요.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/significant-terms-aggregation-elasticsearch</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/significant-terms-aggregation-elasticsearch</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Alexander Dávila]]></dc:creator>
            <pubDate>Mon, 07 Jul 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/13adbc901837835bb56abf15e377127b017cfac8-1536x1024.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>Elasticsearch에서 <a href="https://www.elastic.co/docs/reference/aggregations/search-aggregations-bucket-significantterms-aggregation">유의미한 용어 집계는</a> <a href="https://www.elastic.co/docs/reference/aggregations/search-aggregations-bucket-terms-aggregation">가장 일반적인 용어를</a> 넘어 데이터 세트에서 통계적으로 비정상적인 값을 찾아냅니다. 이를 통해 가치 있는 인사이트와 명확하지 않은 패턴을 발견할 수 있습니다. 유의미한 용어 집계는 두 가지 유용한 매개변수가 포함된 응답을 제공합니다:</p><ul><li><strong>bg_count(백그라운드 카운트): </strong>상위 데이터 세트에서 발견된 문서 수입니다.</li><li><strong>doc_count:</strong> 결과 데이터 세트에서 발견된 문서 수</li></ul><p>예를 들어 휴대폰 판매 데이터 집합에서 다음과 같이 아이폰 16 판매량에서 중요한 용어를 찾을 수 있습니다:</p><pre><code>GET phone_sales_analysis/_search
{
 "size": 0,
 "query": {
   "term": {
     "phone_model": {
       "value": "iPhone 16"
     }
   }
 },
 "aggs": {
   "significant_cities": {
     "significant_terms": {
       "field": "city_region",
       "size": 1
     }
   }
 }
}</code></pre><p>그러면 응답이 돌아옵니다:</p><pre><code>{
 "aggregations": {
   "significant_cities": {
     "doc_count": 122,
     "bg_count": 424,
     "buckets": [
       {
         "key": "Houston",
         "doc_count": 12,
         "score": 0.1946481360617346,
         "bg_count": 14
       }

     ]
   }
 }
}</code></pre><p>휴스턴은 전체 데이터 세트에서 상위 10위 안에 드는 도시가 아니며, 아이폰 16의 상위 도시도 아닙니다. 그러나 유의미한 용어 집계 결과,<strong><em> 이 도시에서는</em></strong> 다른 데이터에 비해 아이폰 16이 불균형적으로 많이 구매되고 있는 것으로 나타났습니다. 숫자에 대해 자세히 알아봅시다:</p><ul><li><strong>최상위 레벨에서:</strong><ul><li><strong>doc_count: 122 - </strong>쿼리가 총 122개의 문서와 일치했습니다.</li><li><strong>bg_count: 424 - </strong>배경 세트(모든 영업 문서)에 424개의 문서가 포함되어 있습니다.</li></ul></li><li><strong>휴스턴 버킷에서:</strong><ul><li><strong>doc_count: 12 - </strong>휴스턴이 122개의 쿼리 결과 중 12개에 나타납니다.</li><li><strong>bg_count: 14 - </strong>휴스턴은 백그라운드 데이터 세트의 총 424개 문서 중 14개에 나타납니다.</li></ul></li></ul><p>이는 총 구매 424건 중 휴스턴에서 발생한 구매는 14건(전체 구매의 3.3%% )에 불과하다는 것을 의미합니다. 하지만 iPhone 16 판매량만 살펴보면 122건 중 12건이 휴스턴에서 발생했는데, 이는 전체 데이터 세트보다 3배 많은 9.8건% 이며, 이는 상당한 수치입니다!</p><p>다음은 비주얼라이제이션에서 이를 보여주는 모습입니다: 도시별_지역별 총 매출.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/f31335b0b7793650025f941820f238dd35bfb09f-1486x1066.png" style="width: 100%;" /><p>휴스턴에서 14개의 매출이 발생하여 데이터 세트에서 매출 기준으로 14번째로 높은 도시임을 알 수 있습니다.</p><p>이제 필터를 적용하여 iPhone 16 판매량만 확인하면 휴스턴에서 12개의 판매량이 발생하여 이 특정 모델에 대한 판매량이 가장 많은 두 번째 도시가 됩니다:</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/6516040db77e6c62af5541a74c723b18008ad3c6-1472x1038.png" style="width: 100%;" /><h2>중요한 용어 집계에 대한 이해</h2><p>Elastic 설명서에 따르면, <a href="https://www.elastic.co/docs/reference/aggregations/search-aggregations-bucket-significantterms-aggregation">중요한 용어 집계는</a> 다음과 같습니다:</p><p><em>&quot;(전경과 배경 세트 사이에서 측정된 인기도에서 상당한 변화를 겪은) 용어를 찾습니다.&quot;</em></p><p>즉, 통계 지표를 사용하여 데이터 하위 집합(전경 세트)에 있는 용어의 빈도와 상위 데이터 집합(배경 세트)에 있는 동일한 용어의 빈도를 비교합니다. 이렇게 하면 점수가 데이터에 용어가 얼마나 자주 나타나는지보다는 통계적 유의성을 반영합니다.</p><p>중요 용어 집계와 일반 용어 집계의 주요 차이점은 다음과 같습니다:</p><ul><li>중요 용어는 데이터의 하위 집합을 비교하는 반면, 용어 집계는 쿼리 결과 데이터 집합에서만 작동합니다.</li><li>용어 집계 결과는 데이터 집합에서 가장 일반적인 용어인 반면, 중요 용어의 결과는 데이터 집합을 고유하게 만드는 요소를 찾기 위해 일반적인 용어를 무시합니다.</li><li>중요한 용어는 집계라는 용어처럼 메모리가 아닌 디스크에서 데이터를 가져와야 하기 때문에 성능에 더 큰 영향을 미칠 수 있습니다.</li></ul><h2>실무 적용(소비자 행동 분석)</h2><h3>분석을 위한 데이터 준비</h3><p>이 분석을 위해 가격, 휴대폰 사양, 구매자의 인구통계학적 특성, 피드백을 포함한 합성 휴대폰 판매 데이터 세트를 생성했습니다. 또한 나중에 시맨틱 쿼리를 실행할 수 있도록 사용자의 피드백을 바탕으로 임베딩을 생성했습니다. Elasticsearch에서 바로 사용할 수 있는 <a href="https://huggingface.co/intfloat/multilingual-e5-small">다국어 e5 소형 모델을</a> 사용했습니다.</p><p></p><p>이 데이터 세트를 Elasticsearch에서 사용하려면:</p><ol><li>Kibana <a href="https://www.elastic.co/docs/manage-data/ingest/upload-data-files">데이터 파일</a> <a href="https://github.com/Alex1795/significant_terms_blog_dataset/blob/main/phone_sales_analysis_dataset.csv">업로드</a> 기능을 사용하여 CSV 파일( 여기에서 다운로드 가능)을 업로드합니다.</li><li><a href="https://www.elastic.co/search-labs/blog/chat-with-pdf-elastic-playground#upload-pdfs-to-kibana">이 블로그에</a> 표시된 것처럼 &quot;임베딩&quot;이라는 시맨틱 필드를 설정하여 <code>multilingual-e5-small model</code></li><li>필드 유형 기본값( <code>purchase_date</code> 및 <code>user_feedback)</code> 을 제외한 모든 필드의 키워드)을 사용하여 가져오기를 완료합니다. 여기에 제시된 쿼리를 그대로 실행할 수 있도록 인덱스 이름 <code>phone_sales_analysis</code> 을 추가해야 합니다.</li></ol><p>이 분석의 주요 초점은 &quot; <strong><em>&quot;아이폰 16 구매자와 다른 인구집단의 차이점은 무엇인가?</em></strong>&quot;를 알아보고 마케팅 목적으로 구매자를 세분화하기 위한 것입니다. </p><p>데이터 세트의 샘플 문서입니다:</p><pre><code>{
         "customer_type": "Returning",
         "user_feedback": "I have to say, quality is great for the price. The battery life is really good.",
         "upgrade_frequency": "2 years",
         "storage_capacity": "256GB",
         "occupation": "Technology & Data",
         "color": "Phantom Black",
         "gender": "Male",
         "price_paid": 899,
         "previous_brand_loyalty": "Mixed",
         "location_type": "Urban",
         "phone_model": "Samsung Galaxy S24",
         "city_region": "San Francisco Bay Area",
         "@timestamp": "2024-03-15T00:00:00.000-05:00",
         "income_bracket": "75000-100000",
         "purchase_channel": "Online",
         "feedback_sentiment": "positive",
         "education_level": "Bachelor",
         "embedding": "I have to say, quality is great for the price. The battery life is really good.",
         "customer_id": "C001",
         "purchase_date": "2024-03-15",
         "age": 34,
         "trade_in_model": "iPhone 13"
}</code></pre><h3>인구 통계학적 패턴 이해</h3><p>여기에서는 일반 사용자를 대상으로 분석을 실행하여 iPhone 16 사용자의 주요 용어 집계에서 얻은 흥미로운 결과와 비교해 보겠습니다.</p><h4>일반 패턴</h4><p>일반적인 구매 패턴을 이해하기 위해 다양한 분야의 모든 문서에 대한 데이터를 집계할 수 있습니다. 간단하게 설명하기 위해 휴대폰을 구매한 사람들의 직업을 살펴보는 데 초점을 맞추겠습니다. Elasticsearch에 요청하면 이 작업을 수행할 수 있습니다.</p><pre><code>GET phone_sales_analysis/_search
{
 "aggs": {
   "occupation_distribution": {
     "terms": {
       "size": 5,
       "field": "occupation"
     }
   }
 },
 "size": 0
}</code></pre><p>이를 통해 데이터 집합의 주요 직업군(레코드 수 기준)을 알 수 있습니다:</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/99136ddddd7abad5d74481158a04501b6915441b-1518x480.png" style="width: 100%;" /><h4>iPhone 16 사용자의 패턴</h4><p>iPhone 16을 구매한 사람들의 차이점을 이해하기 위해 다음과 같이 동일한 필드에 필터를 사용하여 용어 집계를 실행하여 쿼리에서 해당 사용자를 찾아보겠습니다:</p><pre><code>GET phone_sales_analysis/_search
{
  "query": {
    "term": {
      "phone_model": "iPhone 16"
    }
  },
  "aggs": {
    "occupation_distribution": {
      "terms": {
        "size": 5,
        "field": "occupation"
      }
    }
  },
  "size": 0
}</code></pre><p>따라서 iPhone 16 사용자의 주요 직업은 다음과 같습니다:</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/36ce86475beb03810c6ad81d7c776d1eec736654-1500x484.png" style="width: 100%;" /><p>iPhone 16 사용자는 다른 휴대폰 모델 사용자에 비해 직업 패턴이 다르다는 것을 알 수 있습니다. Kibana를 사용하여 결과를 쉽게 시각화해 보겠습니다:</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/b70ab05bc962a274e1617b6caf20575c489a62d8-1448x1128.png" style="width: 100%;" /><p></p><p>이 차트에서 iPhone 16의 추세가 전체 인구의 추세와 다르다는 것을 알 수 있습니다.</p><p>전체 분석을 건너뛰고 중요한 용어 한 가지를 집계하여 iPhone 16 사용자가 일반 인구와 다른 점을 확인할 수 있습니다:</p><pre><code>GET phone_sales_analysis/_search
{
  "query": {
    "term": {
      "phone_model": "iPhone 16"
    }
  },
  "aggs": {
    "occupation_distribution": {
      "significant_terms": {
        "size": 5,
        "field": "occupation"
      }
    }
  },
  "size": 0
}</code></pre><p>간단히 말해서 다음과 같은 응답을 받았습니다:</p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">아이폰 16의 직업 가치</td><td style="border: 1px solid #ccc; padding: 4px;">doc_count</td><td style="border: 1px solid #ccc; padding: 4px;">bg_count</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">직업_분포(최상위 수준)</td><td style="border: 1px solid #ccc; padding: 4px;">122</td><td style="border: 1px solid #ccc; padding: 4px;">424</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">의료 & 헬스케어 버킷</td><td style="border: 1px solid #ccc; padding: 4px;">45</td><td style="border: 1px solid #ccc; padding: 4px;">57</td>
                    </tr>
              </tbody>
            </table>
          <p>이 응답은 iPhone 16 사용자가 흔치 않은 (의미심장하게 읽히는!) 일반 인구 대비 의료 &amp; 헬스케어 분야 종사자 수입니다. 응답의 숫자가 무엇을 의미하는지 살펴보겠습니다:</p><ul><li><strong>최상위 레벨에서:</strong><ul><li><strong>doc_count: 122 - </strong>쿼리가 총 122개의 문서와 일치했습니다.</li><li><strong>bg_count: 424 - </strong>배경 세트(모든 영업 문서)에 424개의 문서가 포함되어 있습니다.</li></ul></li><li><strong>의료 &amp; 헬스케어 버킷에서:</strong><ul><li><strong>doc_count: 45 - </strong>&quot;의료 &amp; 헬스케어&quot; 122개의 쿼리 결과 중 45개에 나타납니다.</li><li><strong>bg_count: 57 - </strong>&quot;의료 &amp; 헬스케어&quot; 백그라운드 데이터 세트의 총 424개 문서 중 57개에 나타납니다.</li></ul></li></ul><p>424명의 구매자 중 57명이 의료 &amp; 헬스케어 분야에서 일하고 있으며, 13.44%인%. 하지만 iPhone 16 구매자를 살펴보면 122명 중 45명이 의료 &amp; 헬스케어 분야에서 일하고 있으며, 이는 36.88%인% 입니다. 즉, iPhone 16 사용자 중 의료( &amp; ) 분야에서 일하는 사람을 찾을 확률이 두 배나 높다는 뜻입니다!</p><p>동일한 분석을 다른 분야(연령, 위치, 소득 계층 등)에 적용하여 iPhone 16 사용자를 특별하게 만드는 요소에 대한 자세한 정보를 찾을 수 있습니다. </p><h3>소비자 세분화</h3><p>중요한 용어 집계를 사용하여 제품, 카테고리 및 고객 세그먼트 간의 관계 인사이트를 추출할 수 있습니다. 이를 위해 탐색하고자 하는 카테고리에 대한 상위 집계를 구축합니다. 또한 중요 용어와 일반 용어 하위 집계를 사용하여 해당 카테고리에 대한 흥미로운 인사이트를 찾고 해당 직종에 종사하는 대부분의 사람들이 사용하는 용어와 비교합니다.</p><p>예를 들어 일부 업무 분야의 사람들이 선호하는 것을 살펴봅시다:</p><ol><li>분석을 보다 명확하게 하기 위해 검색을 3개의 업무 분야로 제한해 보겠습니다: [&quot;관리 &amp; 지원&quot;, &quot;기술 &amp; 데이터&quot;, &quot;의료 &amp; 헬스케어&quot;]로 제한합니다.</li><li>집계 측면에서는 직업별 용어 집계부터 시작합니다.</li><li>휴대폰 모델별 용어라는 하나의 하위 집계를 추가하여 각 분야에서 일하는 사용자가 어떤 모델을 구매하는지 파악하세요.</li><li>두 번째 하위 집계 추가: 휴대폰 모델별 중요 용어 - 각 업무 분야에서 어떤 모델이 특별한지 찾아보세요.</li></ol><pre><code>GET phone_sales_analysis/_search
{
 "query": {
   "terms": {
     "occupation": [
       "Administrative & Support",
       "Technology & Data",
       "Medical & Healthcare"
     ]
   }
 },
 "aggs": {
   "occupations": {
     "terms": {
       "size": 15,
       "field": "occupation"
     },
     "aggs": {
       "general_models": {
         "terms": {
           "field": "phone_model"
         }
       },
       "significant_models": {
         "significant_terms": {
           "field": "phone_model"
         }
       }
     }
   }
 },
 "size": 0
}</code></pre><p>집계 결과를 자세히 분석해 보겠습니다:</p><p><strong>직업</strong> 관리 &amp; 지원</p><p><strong>용어 집계</strong></p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/a4ad519c9013867a3f4cee032160eadd8a47804a-1506x398.png" style="width: 100%;" /><p><strong>중요 용어 집계</strong></p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/e5604fa8036667bdfe733576a5e7c6153760dd3a-306x220.png" style="width: 100%;" /><p>이 표를 통해 이 직업의 추세와 전체 인구의 추세 사이에 큰 차이가 없다는 것을 유추할 수 있습니다.</p><p><strong>직업</strong> 기술 &amp; 데이터</p><p><strong>용어 집계</strong></p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/13b09039bb7d183276451007d2d69dc190b1d3c0-1508x836.png" style="width: 100%;" /><p></p><p><strong>중요 용어 집계</strong></p><p>총 문서 수 424</p><p>이 직업의 문서: 71</p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">휴대폰 모델</td><td style="border: 1px solid #ccc; padding: 4px;">doc_count (이 직업의 이 모델)</td><td style="border: 1px solid #ccc; padding: 4px;">bg_count (모든 문서에서 이 모델)</td><td style="border: 1px solid #ccc; padding: 4px;">% 모든 문서에서</td><td style="border: 1px solid #ccc; padding: 4px;">% 이 직종에서</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Google Pixel 8</td><td style="border: 1px solid #ccc; padding: 4px;">12</td><td style="border: 1px solid #ccc; padding: 4px;">22</td><td style="border: 1px solid #ccc; padding: 4px;">5.19%</td><td style="border: 1px solid #ccc; padding: 4px;">16.90%</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OnePlus 11</td><td style="border: 1px solid #ccc; padding: 4px;">9</td><td style="border: 1px solid #ccc; padding: 4px;">14</td><td style="border: 1px solid #ccc; padding: 4px;">3.30%</td><td style="border: 1px solid #ccc; padding: 4px;">12.68%</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OnePlus 12 Pro</td><td style="border: 1px solid #ccc; padding: 4px;">3</td><td style="border: 1px solid #ccc; padding: 4px;">3</td><td style="border: 1px solid #ccc; padding: 4px;">0.71%</td><td style="border: 1px solid #ccc; padding: 4px;">4.23%</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">구글 픽셀 8 프로</td><td style="border: 1px solid #ccc; padding: 4px;">9</td><td style="border: 1px solid #ccc; padding: 4px;">21</td><td style="border: 1px solid #ccc; padding: 4px;">4.95%</td><td style="border: 1px solid #ccc; padding: 4px;">12.68%</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">아무것도 전화 2</td><td style="border: 1px solid #ccc; padding: 4px;">5</td><td style="border: 1px solid #ccc; padding: 4px;">8</td><td style="border: 1px solid #ccc; padding: 4px;">1.89%</td><td style="border: 1px solid #ccc; padding: 4px;">7.04%</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">삼성 갤럭시 Z 폴드5</td><td style="border: 1px solid #ccc; padding: 4px;">4</td><td style="border: 1px solid #ccc; padding: 4px;">6</td><td style="border: 1px solid #ccc; padding: 4px;">1.42%</td><td style="border: 1px solid #ccc; padding: 4px;">5.63%</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OnePlus 12</td><td style="border: 1px solid #ccc; padding: 4px;">8</td><td style="border: 1px solid #ccc; padding: 4px;">20</td><td style="border: 1px solid #ccc; padding: 4px;">4.72%</td><td style="border: 1px solid #ccc; padding: 4px;">11.27%</td>
                    </tr>
              </tbody>
            </table>
          <p><strong>직업</strong> 의료 &amp; 헬스케어</p><p><strong>용어 집계</strong></p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/b008e996742fc0bb48dc6bacff17cfbc56cf0d73-1492x398.png" style="width: 100%;" /><p><strong>중요 용어 집계</strong></p><p>총 문서 수 424</p><p>이 직업의 문서: 57</p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">휴대폰 모델</td><td style="border: 1px solid #ccc; padding: 4px;">doc_count (이 직업의 이 모델)</td><td style="border: 1px solid #ccc; padding: 4px;">bg_count (모든 문서에서 이 모델)</td><td style="border: 1px solid #ccc; padding: 4px;">% 모든 문서에서</td><td style="border: 1px solid #ccc; padding: 4px;">% 이 직종에서</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">iPhone 16</td><td style="border: 1px solid #ccc; padding: 4px;">45</td><td style="border: 1px solid #ccc; padding: 4px;">122</td><td style="border: 1px solid #ccc; padding: 4px;">28.77%</td><td style="border: 1px solid #ccc; padding: 4px;">78.95%</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">아이폰 15 프로 맥스</td><td style="border: 1px solid #ccc; padding: 4px;">3</td><td style="border: 1px solid #ccc; padding: 4px;">13</td><td style="border: 1px solid #ccc; padding: 4px;">3.07%</td><td style="border: 1px solid #ccc; padding: 4px;">5.26%</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">iPhone 15</td><td style="border: 1px solid #ccc; padding: 4px;">7</td><td style="border: 1px solid #ccc; padding: 4px;">40</td><td style="border: 1px solid #ccc; padding: 4px;">9.43%</td><td style="border: 1px solid #ccc; padding: 4px;">12.28%</td>
                    </tr>
              </tbody>
            </table>
          <p>이 데이터가 우리에게 어떤 이야기를 들려주는지 살펴보겠습니다:</p><ul><li>의료 &amp; 의료 전문가들은 iPhone 16을 선호하며 일반적으로 Apple 휴대폰을 사용하는 경향이 매우 높습니다.</li><li>기술 &amp; 데이터 전문가들은 하이엔드 Android 휴대폰을 선호하지만 반드시 삼성 브랜드를 사용하지는 않습니다. 이 카테고리에서 아이폰도 상당한 추세를 보이고 있습니다.</li><li>관리 &amp; 지원 전문가는 삼성 및 Google 휴대폰을 선호하지만 강력하고 독특한 트렌드는 없습니다.</li></ul><h3>중요 용어 집계 및 하이브리드 검색</h3><p>하이브리드 검색은 텍스트 검색과 시맨틱 결과를 결합하여 향상된 검색 환경을 제공합니다. 이러한 맥락에서 중요한 용어 집계는 질문에 대한 답을 통해 문맥 인식 검색의 결과에 대한 인사이트를 제공할 수 있습니다: <strong>모든 문서와 비교했을 때 이 데이터 세트의 특별한 점은 무엇인가?</strong>이 기능을 설명하기 위해 사용자가 좋은 성능에 대해 이야기할 때 어떤 모델이 과도하게 대표되는지 살펴봅시다: </p><ul><li>필드 임베딩에 대해 &#x27;좋은 성능&#x27;이라는 입력에 가장 가까운 상위 사용자 피드백을 찾는 시맨틱 쿼리를 작성해 보겠습니다.</li><li>또한 사용자_피드백 텍스트 필드에 동일한 용어로 텍스트 검색을 사용할 것입니다.</li><li>또한 전체 데이터 세트보다 이러한 결과에서 더 자주 찾을 수 있는 휴대폰 모델을 찾기 위해 중요한 용어 쿼리를 추가할 예정입니다.<br/></li></ul><pre><code>GET phone_sales_analysis/_search
{
 "retriever": {
   "rrf": {
     "retrievers": [
       {
         "standard": {
           "query": {
             "bool": {
               "must": [
                 {
                   "match": {
                     "user_feedback": {
                       "query": "good performance",
                       "operator": "and"
                     }
                   }
                 }
               ]
             }
           }
         }
       },
       {
         "standard": {
           "query": {
             "semantic": {
               "field": "embedding",
               "query": "good performance"
             }
           }
         }
       }
     ],
    "rank_window_size": 20
   }
 },
 "aggs": {
   "Models": {
     "significant_terms": {
       "field": "phone_model"
     }
   }
 }
}</code></pre><p>일치하는 문서의 예를 살펴보겠습니다:</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/4cb488097a382f0c28c21540db4f593d23633473-1600x162.png" style="width: 100%;" /><p>이것이 저희가 받은 반응입니다:</p><pre><code>{
  "took": 388,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 20,
      "relation": "eq"
    },
    "max_score": 0.016393442,
    "hits": [...]
  },
  "aggregations": {
    "Models": {
      "doc_count": 20,
      "bg_count": 424,
      "buckets": [
        {
          "key": "iPhone 15",
          "doc_count": 5,
          "score": 0.4125,
          "bg_count": 40
        }
      ]
    }
  }
}</code></pre><p></p><p>이는 전체 문서 424개 중 아이폰 15가 40회(9.4% ) 발견된 반면, 의미 검색 &#x27;좋은 성능&#x27;과 일치하는 20개 문서에서는 5회(25% ) 발견되었다는 것을 알 수 있습니다. 따라서 결론을 도출 할 수 있습니다. iPhone 15는 우연히보다 좋은 성능에 대해 이야기 할 때 발견 될 가능성이 2.7 배 더 높습니다.</p><h2>결론</h2><p>중요 용어 집계는 데이터 집합을 문서 세계와 비교하여 데이터 집합의 고유한 세부 정보를 찾아낼 수 있습니다. 이를 통해 발생 횟수를 넘어 데이터에서 예상치 못한 관계를 발견할 수 있습니다. 예를 들어 매우 흥미로운 기능을 구현하는 다양한 사용 사례에 중요한 용어를 적용할 수 있습니다:</p><ul><li><a href="https://www.elastic.co/blog/significant-terms-aggregation#credit">사기 </a>탐지 작업 시 패턴 찾기 - 도난당한 신용 카드의 일반적인 거래를 식별합니다.</li><li>사용자 리뷰를 통한 브랜드 품질 인사이트 - 불균형적인 수의 악평이 있는 브랜드를 감지합니다.</li><li>잘못 분류된 문서 <a href="https://www.elastic.co/docs/reference/aggregations/search-aggregations-bucket-significantterms-aggregation#_use_on_free_text_fields">발견 </a>- 설명(중요 용어 집계)에서 해당 범주에 속하지 않는 단어를 사용하는 범주(용어 필터)에 속하는 문서를 발견합니다.</li></ul>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[JavaScript, Mastra, Elasticsearch로 에이전트 RAG 어시스턴트 구축하기]]></title>
            <description><![CDATA[JavaScript 에코시스템에서 AI 에이전트를 구축하는 방법 알아보기]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/agentic-rag</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/agentic-rag</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[JD Armada]]></dc:creator>
            <pubDate>Tue, 01 Jul 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/8132ed781c1ea5d46ca244182f421ed5c721f23b-1200x628.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <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://cdn.sanity.io/images/me0ej585/search-labs-import-testing/1c73bd1dc1b5fe54f025c7a2b7c322acc9122f3a-1999x1393.png" style="width: 100%;" /><p>참고: 이 블로그 게시물은 &quot;<a href="https://www.elastic.co/search-labs/blog/ai-agents-ai-sdk-elasticsearch">AI SDK와 Elastic으로 AI 에이전트 구축하기</a>&quot;를 기반으로 합니다. AI 에이전트를 처음 접하고 어떤 용도로 사용할 수 있는지 궁금하다면 여기서부터 시작하세요.<br/></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://cdn.sanity.io/images/me0ej585/search-labs-import-testing/443bdc00d84ed1dd49e9f9e431e86ca4b0892563-1999x977.png" style="width: 100%;" /><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><strong>프로젝트 발판 마련하기:</strong><ol><li><strong>백엔드(마스트라):</strong> 백엔드를 스캐폴드하고 에이전트 로직을 정의하려면 npx create mastra@latest를 사용하세요.</li><li><strong>프런트엔드(Vite + React):</strong> 상담원과 상호작용할 수 있는 프론트엔드 채팅 인터페이스를 구축하려면 npm create vite@latest를 사용하세요.</li></ol></li><li><strong>환경 변수 설정</strong><ol><li>환경 변수를 관리하려면 dotenv를 설치하세요.</li><li>.env 파일 만들기 파일을 열고 필요한 변수를 입력합니다.</li></ol></li><li><strong>Elasticsearch 설정</strong><ol><li>(로컬 또는 클라우드에서) Elasticsearch 클러스터를 스핀업합니다.</li><li>공식 Elasticsearch 클라이언트를 설치합니다.</li><li>환경 변수에 액세스할 수 있는지 확인합니다.</li><li>클라이언트에 연결을 설정합니다.</li></ol></li><li><strong>NBA 데이터를 Elasticsearch로 대량 수집하기</strong><ol><li>적절한 매핑으로 인덱스를 생성하여 집계를 활성화합니다.</li><li>CSV 파일에서 플레이어 게임 통계를 Elasticsearch 인덱스로 일괄 수집합니다.</li></ol></li><li><strong>Elasticsearch 집계 정의</strong><ol><li>특정 상대에 대한 과거 평균을 계산하기 위한 쿼리입니다.</li><li>특정 상대에 대한 시즌 평균을 계산하기 위한 쿼리입니다.</li></ol></li><li><strong>플레이어 비교 유틸리티 파일</strong><ol><li>도우미 기능과 Elasticsearch 집계를 통합합니다.</li></ol></li><li><strong>에이전트 구축하기</strong><ol><li>상담원 정의 및 시스템 프롬프트를 추가합니다.</li><li>zod를 설치하고 도구를 정의합니다.</li><li>CORS를 처리하기 위한 미들웨어 설정을 추가합니다.</li></ol></li><li><strong>프론트엔드 통합</strong><ol><li>AI-SDK의 useChat을 사용하여 상담원과 상호 작용합니다.</li><li>적절한 형식의 대화를 담을 수 있는 UI를 만듭니다.</li></ol></li><li><strong>애플리케이션 실행</strong><ol><li>백엔드(Mastra 서버)와 프론트엔드(React 앱)를 모두 시작하세요.</li><li>샘플 쿼리 및 사용법.</li></ol></li><li><strong>다음 단계 에이전트의 지능 향상</strong><ol><li>시맨틱 검색 기능을 추가하여 더욱 통찰력 있는 추천을 지원합니다.</li><li>검색 로직을 Elasticsearch MCP(모델 컨텍스트 프로토콜) 서버로 이동하여 동적 쿼리를 활성화합니다.</li></ol></li></ol><h3><strong>필수 구성 요소</strong></h3><ul><li><strong>Node.js 및 npm</strong>: 백엔드와 프론트엔드 모두 Node에서 실행됩니다. Node 18+ 및 npm v9+가 설치되어 있는지 확인합니다(Node 18+에 번들로 제공됨).</li><li><strong>Elasticsearch 클러스터:</strong> 로컬 또는 클라우드에 있는 활성 Elasticsearch 클러스터입니다.</li><li><strong>OpenAI API 키</strong>: <a href="https://platform.openai.com/api-keys">OpenAI 개발자 포털의</a> API 키 페이지에서 생성하세요.</li></ul><p></p><h3><strong>프로젝트 구조</strong></h3><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/1c0bde11ad0eead523a95e03b9b905aa776e3fd1-1420x934.png" style="width: 100%;" /><h4><strong>1단계: 프로젝트 발판 마련</strong></h4><ol><li>먼저 nba-ai-assistant-js 디렉토리를 생성하고 다음을 사용하여 내부로 이동합니다: </li></ol><pre><code>mkdir nba-ai-assistant-js && cd nba-ai-assistant-js</code></pre><p><strong>백엔드:</strong></p><ol><li>다음 명령과 함께 Mastra 생성 도구를 사용합니다: </li></ol><pre><code>npx create-mastra@latest</code></pre><p>2. 터미널에 몇 가지 메시지가 표시되는데, 첫 번째 메시지는 프로젝트 백엔드의 이름을 지정하는 것입니다:</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/de2725031ed6837db99a979efcdd0ece1e197dbb-608x84.png" style="width: 100%;" /><p>3. 다음으로, Mastra 파일을 저장하는 기본 구조를 유지하므로 <code>src/</code> 을 입력합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/88919d9ff1852126395e1fcd700ecb1b59aac63c-866x116.png" style="width: 100%;" /><p>4. 그런 다음 기본 LLM 공급자로 OpenAI를 선택합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/2328761e769f3ded134e5a21e8a0bf8f41e88f68-404x210.png" style="width: 100%;" /><p>5. 마지막으로 OpenAI API 키를 입력하라는 메시지가 표시됩니다. 지금은 건너뛰는 옵션을 선택하고 나중에<code> .env</code> 파일로 제공하겠습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/0662de9bd28758e377e4c63df8d08b479068ce63-444x120.png" style="width: 100%;" /><p><strong>프론트엔드:</strong></p><ol><li>다시 루트 디렉토리로 이동하여 다음 명령을 사용하여 <a href="https://vite.dev/guide/">Vite 생성 도구를</a> 실행합니다: <code>npm create vite@latest frontend -- --template react</code></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>민감한 키를 관리하기 위해 <code>dotenv</code> 패키지를 사용하여 .env에서 환경 변수를 로드합니다. 파일을 만듭니다. 백엔드 디렉토리로 이동하여 <code>dotenv</code> 을 설치합니다:</li></ol><pre><code>cd backend
npm install dotenv --save</code></pre><p>2. 백엔드 디렉토리에 적절한 변수를 입력할 수 있는 example.env 파일이 제공됩니다. 직접 생성하는 경우 다음 변수를 반드시 포함하세요:</p><pre><code># 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
</code></pre><p></p><p>참고: <code>.gitignore</code> 에 <code>.env</code> 을 추가하여 이 파일이 버전 관리에서 제외되었는지 확인하세요.</p><h4><strong>3단계: Elasticsearch 설정하기</strong></h4><p>먼저 활성 Elasticsearch 클러스터가 필요합니다. 두 가지 옵션이 있습니다:</p><ul><li><strong>옵션 A: Elasticsearch Cloud 사용</strong><ul><li><a href="https://cloud.elastic.co/registration">Elastic Cloud에</a>가입하기</li><li>새 배포 만들기</li><li>엔드포인트 URL 및 API 키(인코딩된) 가져오기</li></ul></li><li><strong>옵션 B: 로컬에서 Elasticsearch 실행하기</strong><ul><li>로컬에 Elasticsearch 설치 및 실행</li><li>엔드포인트로 http://localhost:9200 사용</li><li>API 키 생성</li></ul></li></ul><p></p><p><strong>백엔드에 Elasticsearch 클라이언트 설치하기:</strong></p><ol><li>먼저, 백엔드 디렉터리에 공식 Elasticsearch 클라이언트를 설치합니다:</li></ol><pre><code>npm install @elastic/elasticsearch</code></pre><p>2. 그런 다음 재사용 가능한 함수를 저장할 디렉터리 라이브러리를 만들고 그 안으로 이동합니다:</p><pre><code>mkdir lib && cd lib</code></pre><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><pre><code>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 });</code></pre><p>5. 이제 환경 변수를 사용하여 Elasticsearch 클라이언트를 초기화하고 연결을 확인합니다:</p><pre><code>//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();
</code></pre><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><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 소스에서 가져온 것이 아닙니다.)</li><li><a href="https://github.com/jdarmada/nba-ai-assistant-js/blob/main/backend/data/playerAndTeamInfo.js">playerAndTeamInfo.js</a> - 에이전트가 플레이어 및 팀 이름을 ID와 일치시킬 수 있도록 일반적으로 API 호출을 통해 제공되는 플레이어 및 팀 메타데이터를 대체합니다. 샘플 데이터를 사용하기 때문에 외부 API에서 가져오는 데 따른 오버헤드를 피하기 위해 에이전트가 참조할 수 있는 몇 가지 값을 하드코딩했습니다.</li></ul><p></p><p><strong>구현:</strong></p><ol><li><code>backend/lib</code> 디렉터리에서 <a href="https://github.com/jdarmada/nba-ai-assistant-js/blob/main/backend/lib/playerDataIngestion.js">플레이어데이터인입.js라는</a> 파일을 생성합니다.</li><li>가져오기를 설정하고, CSV 파일 경로를 확인하고, 구문 분석을 설정합니다. 다시 말하지만, ESM을 사용하고 있으므로 <code>__dirname</code> 을 재구성하여 샘플 CSV의 경로를 확인해야 합니다. 또한 <a href="http://node.js/">Node.js의</a> 기본 제공 모듈인 <code>fs</code> 및 <code>readline</code> 을 사용하여 주어진 CSV 파일을 한 줄씩 구문 분석합니다.</li></ol><pre><code>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');</code></pre><p>이렇게 하면 대량 수집 단계에 도달했을 때 CSV를 효율적으로 읽고 구문 분석할 수 있습니다.</p><p>3. 적절한 매핑으로 인덱스를 생성합니다. Elasticsearch는 <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/dynamic">동적 매핑을</a> 통해 필드 유형을 자동으로 추론할 수 있지만, 여기서는 각 통계가 숫자 필드로 처리되도록 명시적으로 처리하고자 합니다. 이 필드는 나중에 집계에 사용할 것이므로 중요합니다. 또한 득점, 리바운드 등과 같은 통계에는 <code>float </code>유형을 사용하여 소수점 값을 포함하도록 합니다. 마지막으로, 매핑 속성 <code>dynamic: &#x27;strict&#x27;</code> 을 추가하여 Elasticsearch가 인식할 수 없는 필드를 동적으로 매핑하지 않도록 하려고 합니다. <br/></p><pre><code>// 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;
    }
}
</code></pre><p>4. CSV 데이터를 Elasticsearch 인덱스에 대량 수집하는 기능을 추가합니다. 코드 블록 내부에서는 헤더 줄을 건너뜁니다. 그런 다음 각 줄 항목을 쉼표로 분할하고 문서 개체에 밀어 넣습니다. 이 단계는 또한 이들을 정리하고 올바른 유형인지 확인합니다. 다음으로, 문서를 인덱스 정보와 함께 bulkBody 배열에 밀어넣고, 이 배열은 Elasticsearch로의 대량 수집을 위한 페이로드 역할을 하게 됩니다.</p><pre><code>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`);
</code></pre><p>5. 그런 다음 <code>elasticClient.bulk()</code> 와 함께 Elasticsearch의 <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-bulk">대량 API를</a> 사용하여 단일 요청으로 여러 문서를 수집할 수 있습니다. 아래의 오류 처리는 수집에 실패한 문서 수와 수집에 성공한 문서 수를 알려주는 구조로 되어 있습니다.</p><pre><code>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) => {
                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);
    }
}
</code></pre><p>6. 아래 <code>main()</code> 함수를 실행하여 <code>createIndex()</code> 및 <code>bulkIngestCsv()</code> 함수를 순차적으로 실행합니다.</p><pre><code>// 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();
</code></pre><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><pre><code>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.' };
    }
}
</code></pre><p>3. 특정 상대를 상대로 한 플레이어의 시즌 평균을 계산하기 위해 과거 기록과 거의 동일한 쿼리를 사용합니다. 이 쿼리의 유일한 차이점은 <code>bool</code> 필터에 <code>game_date</code> 에 대한 추가 조건이 있다는 것입니다. <code>game_date</code> 필드는 현재 NBA 시즌 범위 내에 있어야 합니다. 이 경우 범위는 <code>2024-10-01</code> 에서 <code>2025-06-30</code> 사이입니다. 아래의 추가 조건은 다음 집계에서 이번 시즌의 게임만 격리되도록 합니다.<br/></p><pre><code>        {
                            range: {
                    //Range for this season, change to match current season
                                game_date: {
                                    gte: '2024-10-01',
                                    lte: '2025-06-30',
                                },
                            },
</code></pre><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 집계 로직을 에이전트가 사용하는 기본 도구를 강화하는 단일 함수로 통합하세요.<br/></p><pre><code>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];
}
</code></pre><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><strong>이름:</strong> 이름: 프런트엔드에서 상담원을 호출할 때 참조할 수 있는 이름을 지정합니다.</li><li><strong>지침/시스템 프롬프트: </strong>시스템 프롬프트는 상호 작용 중에 따라야 할 초기 컨텍스트와 규칙을 LLM에 제공합니다. 사용자가 채팅창을 통해 보내는 메시지와 비슷하지만, 이 메시지는 사용자가 입력하기 전에 먼저 표시됩니다. 이 역시 선택한 모델에 따라 달라집니다.</li><li><strong>모델:</strong> 사용할 LLM(Mastra는 OpenAI, Anthropic, 로컬 모델 등을 지원합니다).</li><li><strong>도구:</strong> 도구: 상담원이 호출할 수 있는 도구 기능의 목록입니다.</li><li><strong>메모리:</strong> (선택 사항) 상담원이 대화 기록 등을 기억하도록 하려는 경우 선택합니다. 간단하게 하기 위해 영구 메모리 없이 시작할 수 있지만, Mastra는 이를 지원합니다.</li></ul><p></p><pre><code>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 },
});
</code></pre><p><strong><br/>도구 정의</strong></p><ol><li><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> 파일로 이동합니다.</li><li>명령을 사용하여 Zod를 설치합니다:</li></ol><pre><code>npm install zod</code></pre><p>3. 도구 정의를 추가합니다. <code>comparePlayers.js</code> 파일 내의 함수를 에이전트가 이 도구를 호출할 때 사용할 기본 함수로 가져옵니다. Mastra의 <code>createTool()</code> 기능을 사용하여 <code>playerComparisonTool</code> 을 등록합니다. 필드에는 다음이 포함됩니다:</p><ul><li><code>id</code>: 상담원이 도구의 기능을 이해하는 데 도움이 되는 자연어 설명입니다.</li><li><code>input schema</code>: 도구의 입력 형태를 정의하기 위해 Mastra는 타입스크립트 스키마 유효성 검사 라이브러리인 <a href="https://zod.dev/">Zod</a> 스키마를 사용합니다. Zod는 에이전트가 올바른 구조의 입력을 입력하는지 확인하고 입력 구조가 일치하지 않으면 도구가 실행되지 않도록 방지하여 도움을 줍니다.</li><li><code>description</code>: 상담원이 언제 전화를 걸어 도구를 사용해야 하는지 이해하는 데 도움이 되는 자연어 설명입니다.</li><li><code>execute</code>: 도구가 호출될 때 실행되는 로직입니다. 저희의 경우 가져온 헬퍼 함수를 사용하여 성능 통계를 반환하고 있습니다.</li></ul><pre><code>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 } }) => {
        return await comparePlayersForNextMatchup(player1, player2);
      },
})</code></pre><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><code>origin: [&#x27;http://localhost:5173&#x27;]</code><ul><li>이 주소(Vite 기본 주소)에서만 요청을 허용합니다.</li></ul></li><li><code>allowMethods: [&quot;GET&quot;, &quot;POST&quot;]</code><ul><li>허용되는 HTTP 메서드입니다. 대부분의 경우 POST를 사용합니다.</li></ul></li><li><code>allowHeaders: [&quot;Content-Type&quot;, &quot;Authorization&quot;, &quot;x-mastra-client-type, &quot;x-highlight-request&quot;, &quot;traceparent&quot;],</code><ul><li>요청에 사용할 수 있는 사용자 정의 헤더를 결정합니다.</li></ul></li></ul><p></p><pre><code>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
</code></pre><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><pre><code>npm install @ai-sdk/react</code></pre><p>2. 같은 디렉토리에 있는 동안 에이전트가 생성하는 응답의 형식을 올바르게 지정할 수 있도록 React Markdown을 설치합니다.</p><pre><code>npm install react-markdown</code></pre><p>3. <code>useChat()</code> 을 구현합니다. 이 훅은 프론트엔드와 AI 에이전트 백엔드 간의 상호작용을 관리합니다. 메시지 상태, 사용자 입력, 상태를 처리하고 통합 가시성을 위한 라이프사이클 훅을 제공합니다. 전달되는 옵션에는 다음이 포함됩니다:</p><ul><li><code>api:</code> 이는 Mastra AI 에이전트의 엔드포인트를 정의합니다. 기본값은 포트 4111이며 스트리밍 응답을 지원하는 경로도 추가하려고 합니다.</li><li><code>onToolCall</code>: 상담원이 도구를 호출할 때마다 실행되며, 상담원이 어떤 도구를 호출하는지 추적하는 데 사용합니다.</li><li><code>onFinish</code>: 상담원이 전체 응답을 완료한 후에 실행됩니다. 스트리밍을 활성화하더라도 <code>onFinish</code> 은 각 청크가 아닌 전체 메시지가 수신된 후에 실행됩니다. 여기서는 토큰 사용량을 추적하는 데 사용하고 있습니다. 이는 LLM 비용을 모니터링하고 최적화할 때 유용할 수 있습니다.</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><pre><code>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 }) => {
            setToolsCalled((prev) => [...prev, toolCall.toolName]);
        },

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

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

    return (
        <div>
            <div className="agent-info">
                <h4 className="stats-title">What's My Agent Doing?</h4>

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

                    <div className="usage-stats">
                        <p>Prompt Token Usage: {promptTokenUsage}</p>
                        <p>Completion Token Usage: {completionTokenUsage}</p>
                        <p>Total Token Usage: {totalTokenUsage}</p>
                    </div>
                </div>
            </div>

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

            <form onSubmit={handleSubmit}>
                <input
                    type="text"
                    value={input}
                    onChange={handleInputChange}
                    placeholder="Input two players you want to compare."
                    className="input-box"
                />
                <button type="submit" disabled={status === 'streaming'}>
                    {status === 'streaming' ? 'Thinking...' : 'Send'}
                </button>
            </form>
        </div>
    );
}</code></pre><h4><strong>9단계: 애플리케이션 실행</strong></h4><p>축하합니다! 이제 애플리케이션을 실행할 준비가 되었습니다. 백엔드와 프론트엔드를 모두 시작하려면 다음 단계를 따르세요.</p><ol><li>터미널 창에서 루트 디렉토리에서 시작하여 백엔드 디렉토리로 이동하여 Mastra 서버를 시작합니다:</li></ol><pre><code>cd backend

npm run dev</code></pre><p>2. 다른 터미널 창에서 루트 디렉토리에서 시작하여 프론트엔드 디렉토리로 이동하고 React 앱을 시작합니다:</p><p></p><pre><code>cd frontend

npm run dev</code></pre><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>&quot;르브론 제임스와 스테픈 커리 비교&quot;</li><li>&quot;제이슨 테이텀과 루카 돈치치 중 누구를 선택해야 하나요?&quot;</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><strong>에이전트 RAG의 기본</strong> 사항과 AI 에이전트의 자율성과 RAG를 효과적으로 사용할 수 있는 도구를 결합하여 보다 미묘하고 역동적인 에이전트를 만드는 방법에 대해 알아보세요.</li><li>데이터 저장 기능과 강력한 기본 집계 기능을 갖춘 <strong>Elasticsearch가 </strong>어떻게 LLM의 지식 베이스로서 훌륭한 파트너가 되는지 알아보세요.</li><li>자바스크립트 에코시스템에서 개발자를 위해 이러한 에이전트 구축을 간소화하는 <strong>Mastra </strong>프레임워크와 그 방법을 소개합니다.</li></ul><p>농구광이든, AI 에이전트를 구축하는 방법을 모색 중이든, 저처럼 두 가지 모두에 관심이 있는 분이라면 이 블로그가 시작을 위한 몇 가지 기본 요소를 제공했으면 합니다. 전체 리포지토리는 <a href="https://github.com/jdarmada/nba-ai-assistant-js">GitHub에서</a> 사용할 수 있으며, 자유롭게 복제하고 수정할 수 있습니다. 이제 판타지 리그에서 우승하세요!</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Logstash의 루비 스크립팅]]></title>
            <description><![CDATA[Logstash 파이프라인에서 고급 데이터 변환을 위한 Logstash Ruby 필터 플러그인에 대해 알아보세요.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/ruby-scripting-logstash</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/ruby-scripting-logstash</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Dai Sugimori]]></dc:creator>
            <pubDate>Tue, 24 Jun 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/b1bcd63c033ccbde102c20ba3085f165f9289a71-1600x1000.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>Logstash는 여러 소스에서 데이터를 수집하고 변환하여 선택한 대상으로 전송하는 데이터 처리 파이프라인입니다. 필터 플러그인은 이 프로세스의 핵심으로, 데이터가 파이프라인을 통과할 때 특정 작업을 수행합니다.</p><p>Logstash에는 데이터 구문 분석, 보강, 수정과 같은 일반적인 작업을 위한 몇 가지 기본 제공 필터가 포함되어 있습니다. 그러나 때로는 이러한 표준 필터가 제공할 수 있는 범위를 넘어서는 사용자 지정 로직이 필요한 시나리오가 발생할 수 있습니다. 바로 이 부분에서 <a href="https://www.elastic.co/docs/reference/logstash/plugins/plugins-filters-ruby">루비 필터 플러그인이</a> 등장합니다.</p><p><strong>Ruby 필터 플러그인을 사용하면 Logstash 파이프라인 내에서 직접 사용자 정의 Ruby 코드를 실행할 수 있습니다.</strong> 표준 필터로 충분하지 않은 경우, 루비 필터를 사용하면 복잡한 데이터 변환을 처리하고, 사용자 지정 비즈니스 로직을 구현하거나, 외부 시스템과 통합할 수 있습니다.</p><p>이 블로그에서는 기본 사용법부터 고급 사용법까지 루비 필터를 사용하는 방법을 살펴봅니다.</p><h2>루비 필터는 언제 사용해야 하나요?</h2><p>Elastic의 컨설팅 아키텍트로서, 요즘은 최신 데이터 처리 엔진이 아니지만 데이터 처리 파이프라인에 Logstash를 사용하는 고객들을 자주 보게 됩니다. 복잡한 데이터 조작이나 사용자 지정 로직을 처리할 때 표준 필터의 한계로 인해 어려움을 겪는 경우가 많습니다. 이러한 경우 루비 필터를 사용하면 이러한 문제를 극복하는 데 도움이 될 수 있습니다.</p><p>Ruby 필터는 표준 Logstash 필터로 특정 요구 사항을 충족할 수 없을 때 유용합니다. 다음은 몇 가지 일반적인 사용 사례입니다:</p><ul><li><strong>심층 중첩 데이터 조작</strong>: 복잡한 JSON 구조, 배열 내의 배열을 수정하거나 콘텐츠에 따라 데이터를 동적으로 재구성합니다.</li><li><strong>고급 문자열 처리</strong>: 비정형 텍스트에서 정형 데이터 구문 분석 및 추출</li><li><strong>복잡한 비즈니스 로직 구현하기</strong>: 조건부 논리, 루프 또는 복잡한 계산이 필요한 사용자 지정 변환을 만듭니다.</li></ul><h2>기본 사용법</h2><p>루비 필터의 작동 원리를 이해하기 위해 간단한 예제부터 살펴보겠습니다.</p><h3>루비 필터 구성하기</h3><p>Logstash 파이프라인을 생성할 때, 구성 파일을 <code>/etc/logstash/conf.d</code> 디렉터리에 배치해야 합니다. 또는 Logstash를 수동으로 부팅할 때 <code>-f</code> 옵션을 사용하여 구성 파일의 경로를 지정하면 파이프라인을 쉽게 실험할 수 있습니다.</p><pre><code>$ ./bin/logstash -f /path/to/your_pipeline.conf</code></pre><p>구성 파일의 확장자는 <code>.conf</code> 여야 합니다.</p><p>Ruby 필터를 사용하려면 Logstash 파이프라인 구성(*.conf) 파일의 필터 섹션에 <code>ruby</code> 필터를 정의하세요. 다음은 기본적인 예입니다:</p><pre><code>filter {
  ruby {
    code => "
      event.set('new_field', 'Hello from Ruby!')
    "
  }
}</code></pre><p>이 인라인 Ruby 필터는 Logstash 구성 내에서 Ruby 필터 인스턴스를 정의합니다. <code>code</code> 매개변수는 이 필터가 처리하는 각 이벤트에 대해 Logstash가 실행할 인라인 Ruby 스크립트를 제공합니다. 해당 스크립트 내부에는 이벤트 자체를 나타내는 <code>event</code> 변수가 있습니다. 이벤트 객체에는 Logstash로 전송된 원본 데이터와 Logstash의 필터 단계에서 생성된 모든 추가 필드가 포함되어 있습니다. <code>event.get()</code> 및 <code>event.set()</code> 과 같은 Logstash 이벤트 API를 통해 이러한 필드에 액세스할 수 있습니다. 이 예제 코드에서 <code>event.set(&#x27;new_field&#x27;, &#x27;Hello from Ruby!&#x27;)</code> 는 <code>new_field</code> 이라는 새 필드를 문자열 값 <code>Hello from Ruby!</code> 으로 설정합니다. 필요에 따라 이 <code>code</code> 블록에 다른 코드를 추가할 수 있습니다.</p><p>이 <code>event</code> 객체는 키-값 유형의 데이터 컨테이너로 작동하지만 일반적인 Ruby의 해시 객체가 아닙니다. 이벤트 API에 대해 자세히 알아보려면 <a href="https://www.elastic.co/docs/reference/logstash/event-api">이 공식 문서를</a> 확인하세요.</p><h3>루비 스크립트 외부화</h3><p>간단한 변환의 경우 인라인 루비 코드가 편리합니다. 그러나 복잡한 로직이나 재사용 가능한 함수의 경우 코드를 외부 Ruby 스크립트로 옮기는 것이 좋습니다. 이렇게 하면 유지보수성이 향상되고 Logstash 파이프라인 구성이 깔끔하게 유지됩니다.</p><p>먼저 루비 스크립트를 만들어 <code>my_ruby_script.rb</code> 로 저장합니다. 스크립트는 이벤트를 처리하는 <code>filter</code> 메서드를 정의해야 합니다. 이 함수는 처리 중인 현재 이벤트를 나타내는 이벤트 객체를 인수로 받습니다. <code>filter</code> 메서드는 전송할 이벤트 배열을 반환해야 합니다. 이벤트를 삭제하려면 빈 배열을 반환합니다.</p><p>예를 들어 다음 스크립트는 <code>message</code> 필드를 읽고 길이를 계산한 다음 그 결과를 <code>message_length</code> 이라는 새 필드에 저장합니다.</p><pre><code>def register(params)
  # This method is called when the plugin is loaded.
  # You can use it to initialize any instance variables or perform setup tasks.
end

def filter(event)
  message = event.get('message')

  if message
    event.set('message_length', message.length)
  end

  return [event]
end</code></pre><p>그런 다음 <code>path</code> 옵션을 사용하여 스크립트를 참조하도록 루비 필터 구성을 설정합니다. 이렇게 하면 Logstash가 외부 스크립트를 로드하고 실행하도록 지시합니다. 외부 스크립트를 사용할 때는 파일이 존재하고 올바른 권한이 있는지 확인하세요.</p><pre><code>filter {
  ruby {
    path => "/path/to/my_ruby_script.rb"
  }
}</code></pre><p>이제 각 이벤트는 <code>my_ruby_script.rb</code> 의 <code>filter</code> 메서드로 전달되어 처리됩니다.</p><p>이 접근 방식을 사용하면 복잡한 로직을 보다 효과적으로 관리할 수 있으므로 Ruby 코드를 더 쉽게 테스트, 디버그 및 재사용할 수 있습니다.</p><h2>고급 사용 방법</h2><p>이 섹션에서는 Logstash에서 Ruby 필터를 사용하는 몇 가지 고급 예제를 살펴보겠습니다. 이 예제에서는 Ruby를 사용하여 데이터 변환을 수행하고, 이벤트를 보강하고, 사용자 지정 로직을 구현하는 방법을 보여드립니다.</p><h3>중첩된 데이터 구조 조작하기</h3><p>Logstash 이벤트는 Logstash가 처리하는 핵심 데이터 구조입니다. 배열 및 해시 같은 중첩된 데이터 구조를 포함하여 다양한 필드를 포함할 수 있습니다. 루비 필터를 사용하면 이러한 중첩 구조를 쉽게 조작할 수 있습니다.</p><p>루비 필터는 해시 및 배열과 같은 중첩된 데이터 구조를 처리할 수 있으므로 이러한 구조 내에서 필드를 수정하거나 추가할 수 있습니다. 이는 JSON과 같은 복잡한 데이터 형식을 다룰 때 유용합니다.</p><pre><code>input {
  generator {
    lines => [
      '{"nested": {"key1": "value1", "key2": "value2"}}'
    ]
    count => 1
    codec => "json"
    ecs_compatibility => "disabled"
  }
}

filter {
  ruby {
    code => "
      nested_data = event.get('nested')

      if nested_data.is_a?(Hash)
        nested_data['key3'] = 'value3'
        event.set('nested', nested_data)
      end
    "
  }
}

output {
  stdout { codec => rubydebug }
}</code></pre><p>이 예제에는 입력 데이터에 중첩된 JSON 객체가 포함되어 있습니다. 루비 필터는 새로운 키-값 쌍을 추가하여 중첩된 데이터를 수정합니다. 중첩된 데이터에 대한 이러한 유형의 조작은 표준 Logstash 필터로는 불가능하므로, 복잡한 데이터 구조에 편리한 옵션으로 Ruby 필터를 사용할 수 있습니다.</p><h3>단일 이벤트를 여러 이벤트로 분할</h3><p>루비 필터를 사용하여 단일 이벤트를 여러 개의 이벤트로 분할할 수도 있습니다. 여러 항목이 포함된 단일 이벤트가 있고 각 항목에 대해 별도의 이벤트를 만들려는 경우에 유용합니다.</p><p>Elasticsearch의 수집 파이프라인이나 Beats/Elastic Agent의 프로세서 모두 이벤트 분할을 지원하지 않습니다. 이것은 Logstash의 가장 강력한 사용 사례 중 하나입니다.</p><h4>분할 필터 사용</h4><p><code>split</code> 필터를 사용하여 지정된 필드를 기준으로 이벤트를 여러 개의 이벤트로 분할할 수 있습니다. 그러나 분할 중에 추가 변환이나 로직을 수행해야 하는 경우 분할 필터와 함께 루비 필터를 사용할 수 있습니다.</p><p>다음 예시에서는 한 줄의 XML 텍스트로 된 RSS 피드가 있습니다. 여기에는 여러 <code>&lt;item&gt;</code> 요소가 포함되어 있습니다. 루비 필터는 XML에서 <code>&lt;item&gt;</code> 요소를 추출하여 <code>items</code> 이라는 새 필드에 저장하는 데 사용됩니다. 그런 다음 분할 필터를 사용하여 <code>items</code> 필드를 기준으로 이벤트를 여러 개의 이벤트로 분할합니다.</p><pre><code>input {
  generator {
    lines => [
      '<rss version="2.0"><channel><title>Sample RSS</title><item><title>Article 1</title><link>http://example.com/1</link><description>Desc 1</description></item><item><title>Article 2</title><link>http://example.com/2</link><description>Desc 2</description></item></channel></rss>'
    ]
    count => 1
    codec => "plain"
    ecs_compatibility => "disabled"
  }
}

filter {
  xml {
    source => "message"
    target => "rss"
    store_xml => true
    force_array => false
  }
  ruby {
    code => "event.set('items', event.get('[rss][channel][item]')) if event.get('[rss][channel][item]')"
  }
  split {
    field => "items"
  }
  ruby {
    code => "
      item = event.get('items')
      event.set('title', item['title']) if item['title']
      event.set('link', item['link']) if item['link']
      event.set('description', item['description']) if item['description']
    "
  }
  mutate {
    remove_field => ["@timestamp", "@version", "sequence", "host", "event", "message", "rss", "items"]
  }
}

output {
  stdout { codec => rubydebug }
}</code></pre><p>다음과 같이 출력됩니다:</p><pre><code>{
          "title" => "Article 1",
           "link" => "http://example.com/1",
    "description" => "Desc 1"
}
{
          "title" => "Article 2",
           "link" => "http://example.com/2",
    "description" => "Desc 2"
}</code></pre><p>눈치채셨겠지만, 이 경우 <code>ruby</code> 필터는 필수 항목이 아닙니다. <code>split</code> 필터는 <code>items</code> 필드를 기준으로 이벤트를 여러 개의 이벤트로 분할하는 데 사용할 수 있으며, <code>mutate</code> 필터는 불필요한 필드를 제거하는 데 사용할 수 있습니다. 그러나 분할 중에 추가 변환이나 로직을 수행해야 하는 경우 루비 필터를 사용할 수 있습니다.</p><h4>인라인 루비 스크립트 사용</h4><p>또한 인라인 루비 스크립트를 사용하여 <code>event.clone</code> 메서드와 <code>new_event_block variable</code>, 예: <code>new_event_block.call(new_event)</code> 를 사용하여 단일 이벤트를 여러 개의 이벤트로 분할할 수도 있습니다. 이렇게 하면 데이터를 보존하면서 원래 이벤트를 기반으로 새 이벤트를 만들 수 있습니다.</p><p>다음은 루비 필터를 사용하여 단일 이벤트를 여러 개의 이벤트로 분할하는 방법의 예시입니다. 입력 및 출력은 이전 예제와 동일합니다.</p><pre><code>filter {
  xml {
    source => "message"
    target => "rss"
    store_xml => true
    force_array => false
  }
  ruby {
    code => "
      items = event.get('[rss][channel][item]')
      if items.is_a?(Array)
        items.each do |item|
          new_event = event.clone
          new_event.set('title', item['title'])
          new_event.set('link', item['link'])
          new_event.set('description', item['description'])
          new_event_block.call new_event
        end
        event.cancel
      elsif items.is_a?(Hash)
        event.set('title', items['title'])
        event.set('link', items['link'])
        event.set('description', items['description'])
      end
    "
  }
  mutate {
    remove_field => ["@timestamp", "@version", "sequence", "host", "event", "message", "rss", "items"]
  }
}</code></pre><h4>외부 루비 스크립트 사용</h4><p>외부 루비 스크립트를 사용하여 단일 이벤트를 여러 개의 이벤트로 분할할 수도 있습니다.</p><p>구성 파일입니다:</p><pre><code>filter {
  xml {
    source => "message"
    target => "rss"
    store_xml => true
    force_array => false
  }
  ruby {
    path => "path/to/ruby/split_event.rb"
  }
  mutate {
    remove_field => ["@timestamp", "@version", "sequence", "host", "event", "message", "rss", "items"]
  }
}</code></pre><p>루비 스크립트는 <code>split_event.rb</code> 로 외부화해야 합니다:</p><pre><code>def filter(event)
  items = event.get('[rss][channel][item]')
  events = []
  if items.is_a?(Array)
    items.each do |item|
      new_event = event.clone
      new_event.set('title', item['title'])
      new_event.set('link', item['link'])
      new_event.set('description', item['description'])
      events << new_event
    end
    return events
  elsif items.is_a?(Hash)
    event.set('title', items['title'])
    event.set('link', items['link'])
    event.set('description', items['description'])
    return [event]
  else
    return []
  end
end</code></pre><p><code>filter</code> 메서드는 이벤트 배열을 반환해야 한다는 점을 기억하세요. 들어오는 이벤트 객체를 복제하여 배열에 추가하여 여러 이벤트를 반환하거나 단일 이벤트를 하나의 요소가 있는 배열로 반환할 수 있습니다.</p><pre><code>return events
# or
# return [event]</code></pre><p>이렇게 하면 하나의 이벤트를 여러 개의 이벤트로 분할할 수 있습니다.</p><h3>외부 명령 실행 및 출력 구문 분석</h3><p>Logstash 실행 입력 플러그인을 사용하면 외부 명령을 실행할 수 있으며, 그 출력은 Logstash의 이벤트가 됩니다. 명령의 출력은 이벤트의 <code>message</code> 필드에 저장됩니다.</p><p>일반적으로 시스템 명령의 출력은 사람이 읽을 수 있지만, Logstash가 쉽게 구문 분석할 수 있는 JSON이나 기타 형식으로 구조화되어 있지 않습니다. 이를 처리하기 위해 루비 필터를 사용하여 출력을 구문 분석하고 거기서 정보를 추출할 수 있습니다.</p><p>다음은 <code>exec</code> 입력 플러그인을 사용하여 유닉스 계열 시스템에서 실행 중인 모든 프로세스를 나열하는 <code>ps -ef</code> 명령을 실행하는 예제입니다. 출력은 루비 필터로 파싱되어 각 프로세스에 대한 관련 정보를 추출합니다.</p><pre><code>input {
  exec {
    command => "ps -ef"
    interval => 60
  }
}

filter {
  ruby {
    code => '
      processes = []
      lines = event.get("message").split("\n")  
      lines.each_with_index do |line, index|
        # Skip header line and empty lines
        next if index == 0 || line.strip.empty?
        entry = nil
        
        # Use regex to match the ps -ef output format more flexibly
        # This pattern accounts for variable spacing and different time formats
        if line =~ /^\s*(\S+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\S+)\s+(\S+)\s+([\d:]+\.?\d*)\s+(.+)$/
          uid, pid, ppid, c, stime, tty, time, cmd = $1, $2, $3, $4, $5, $6, $7, $8
          
          entry = {
            "UID" => uid,
            "PID" => pid,
            "PPID" => ppid,
            "C" => c,
            "STIME" => stime,
            "TTY" => tty,
            "TIME" => time,
            "CMD" => cmd.strip
          }
        elsif line =~ /^\s*(\S+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(.+)$/
          # Fallback pattern for lines that might not match the exact format
          # Split the remaining part more carefully
          uid, pid, ppid, c, remainder = $1, $2, $3, $4, $5
          
          # Split remainder into STIME, TTY, TIME, CMD
          parts = remainder.strip.split(/\s+/, 4)
          if parts.length >= 4
            stime, tty, time, cmd = parts[0], parts[1], parts[2], parts[3]
            
            entry = {
              "UID" => uid,
              "PID" => pid,
              "PPID" => ppid,
              "C" => c,
              "STIME" => stime,
              "TTY" => tty,
              "TIME" => time,
              "CMD" => cmd
            }
          end
        end
        if entry && entry["UID"] == "0"
          original_line = line.strip
          entry["original_line"] = original_line if original_line.length > 0
          processes.push(entry)
        end
      end
      event.set("processes", processes)
      event.remove("message")
      event.remove("event")
    '
  }
}

output {
  stdout { codec => rubydebug }
}</code></pre><p>이 예에서는 <code>exec</code> 입력 플러그인을 사용하여 60초마다 <code>ps -ef</code> 명령을 실행합니다. 루비 필터는 출력을 처리하여 UID, PID, PPID, CPU 사용량(C), 시작 시간(STIME), TTY, 총 CPU 시간(TIME), 실행된 명령(CMD) 등 관련 필드를 추출합니다. 제 macOS 환경에서는 잘 작동하지만 시스템의 <code>ps -ef</code> 명령의 출력 형식과 일치하도록 정규식 패턴을 조정해야 할 수도 있습니다.</p><h3>기본 제공 라이브러리 사용</h3><p>루비 필터 플러그인을 사용하면 내장된 루비 라이브러리를 사용할 수 있어 다양한 작업에 매우 유용할 수 있습니다. 예를 들어 <code>json</code> 라이브러리를 사용하여 JSON 문자열을 구문 분석하거나 <code>date</code> 라이브러리를 사용하여 날짜를 조작할 수 있습니다.</p><p>다음은 <code>json</code> 라이브러리를 사용하여 필드에 저장된 JSON 문자열을 구문 분석하는 예제입니다:</p><pre><code>require 'json'

def filter(event)
  json_string = event.get('message')
  parsed_json = JSON.parse(json_string)
  event.set('parsed_json', parsed_json)
  return [event]
end</code></pre><p>매번 라이브러리가 필요하지 않도록 하려면 Ruby 필터 스크립트의 시작 부분에 <code>require</code> 문을 사용할 수 있도록 Ruby 코드를 외부화해야 합니다. 이렇게 하면 라이브러리가 한 번 로드되고 스크립트에서 사용할 수 있게 됩니다.</p><p>사용자 환경에서 어떤 라이브러리를 사용할 수 있는지 확인하려면 Ruby 필터에서 다음 코드를 실행하여 기본 제공 라이브러리를 나열할 수 있습니다:</p><pre><code>Gem.loaded_specs.sort_by { |name, _| name }.each do |name, spec|
  puts "#{name}: #{spec.version}"
end</code></pre><p><strong>참고: </strong>기본 제공 라이브러리는 Logstash에서 공식적으로 지원되지 않으며, 동작이 변경되거나 향후 버전에서 제공되지 않을 수 있습니다. 본인 책임하에 사용하세요.</p><h2>결론</h2><p>Logstash Ruby 필터를 사용하면 Logstash 파이프라인의 기능을 사용자 정의하고 확장할 수 있습니다. 이 글에서는 루비 필터 사용의 기본 사항을 다루고 고급 사용 예제를 제공했습니다.</p><p>루비 필터를 활용하면 사용자 지정 로직이나 고급 조작이 필요한 복잡한 데이터 처리 작업을 처리할 수 있습니다. 중첩된 데이터 구조로 작업하거나 이벤트를 분할하거나 복잡한/비구조화 텍스트를 구문 분석하여 구조화된 JSON으로 변환하는 등, Ruby 필터는 특정 요구 사항을 충족하는 유연성을 제공합니다.</p><p>이 가이드가 Logstash Ruby 필터의 잠재력을 최대한 활용할 수 있는 지식과 영감을 제공해 주었기를 바랍니다. 행복한 스크립팅!</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[GCP GKE Autopilot에 Elasticsearch를 배포하는 방법]]></title>
            <description><![CDATA[부분 관리형 Elasticsearch 설정 구성을 위해 GKE Autopilot과 ECK를 사용하여 GCP에 Elasticsearch 클러스터를 배포하는 방법을 알아보세요.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/eck-gke-autopilot</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/eck-gke-autopilot</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Eduard Martin]]></dc:creator>
            <pubDate>Thu, 19 Jun 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/d9c462fe63011356671479ccfedd435eec1ede52-1200x628.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>이 문서에서는 Autopilot을 사용하여 Google Cloud Kubernetes(GKE)에 Elasticsearch를 배포하는 방법을 알아보겠습니다.</p><p>Elasticsearch의 경우 <a href="https://www.elastic.co/docs/deploy-manage/deploy/cloud-on-k8s">Elastic Cloud on Kubernetes</a>(ECK)를 사용할 예정입니다. 이는 모든 Elastic Stack 구성 요소의 Kubernetes 배포 오케스트레이션을 단순화하는 공식 Elasticsearch Kubernetes 오퍼레이터입니다.</p><p>다양한 GCP 인프라에 Elasticsearch 클러스터를 배포하는 방법에 대해 자세히 알아보려면 <a href="https://www.elastic.co/search-labs/blog/elasticsearch-gpc-google-compute-engine">Google Cloud Compute</a> 및 <a href="https://www.elastic.co/search-labs/blog/deploy-elastic-gcp-marketplace">Google Cloud Marketplace</a> 시작하기 문서를 참조하세요.</p><h2>Elasticsearch 배포 작업</h2><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/56b54d718dcff9af9050873c41fdf738074851da-1428x582.png" style="width: 100%;" /><h3>GKE Autopilot이란 무엇입니까?</h3><p><a href="https://cloud.google.com/kubernetes-engine/docs/concepts/autopilot-overview?hl=es-419"><strong>Google Kubernetes Engine(GKE) Autopilot</strong></a>은 클러스터 구성, 노드 관리, 보안, 확장을 Google이 처리하고 개발자는 애플리케이션 배포에 집중할 수 있는 완전 관리형 Kubernetes 환경을 제공하며, 팀은 기본 제공되는 모범 사례를 통해 몇 분 안에 코드에서 프로덕션으로 전환할 수 있습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/bbf6de63fa0a199326352f521cb22654818799f6-1600x958.png" style="width: 100%;" /><h2>Google Cloud에서 ECK는 언제 사용하나요?</h2><p>Elastic Cloud on Kubernetes(ECK)는 전용 노드 역할, 고가용성 및 자동화와 같은 고급 기능을 갖춘 Elasticsearch를 배포하려는 기존 Kubernetes 인프라를 보유한 조직에 가장 적합합니다.</p><h2>Google Cloud에서 ECK를 설정하는 방법은 무엇인가요?</h2><p>1. <a href="https://console.cloud.google.com">Google Cloud 콘솔</a>에 로그인합니다.</p><p>2. <strong>오른쪽 상단</strong>의<strong> Cloud Shell</strong> 버튼을 클릭하여 콘솔에 접속하고 거기에서 GKE 클러스터를 배포합니다. 또는 <a href="https://cloud.google.com/cli">gcloud CLI</a>를 사용할 수도 있습니다.</p><p><strong><em>튜토리얼을 진행하는 동안 프로젝트 ID를 본인의 ID로 업데이트하는 것을 잊지 마세요.</em></strong></p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/999b03861d4fe44f360ab4c7e2616e1dc10cf182-1558x1248.png" style="width: 100%;" /><p>3. <a href="https://console.cloud.google.com/flows/enableapi?apiid=container.googleapis.com">Google Kubernetes Engine API</a>를 활성화합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/03827d3dc0e987c019e7747d33e7c01920047beb-911x246.png" style="width: 100%;" /><p><strong><em>다음</em></strong>을 클릭합니다.</p><p>이제 Kubernetes Engine API를 검색할 때 활성화된 것으로 표시되어야 합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/d5cd46f0333086bcb31b80cf9c08a469b449ec0f-640x250.png" style="width: 100%;" /><p>4. Cloud 셸에서 Autopilot 클러스터를 생성합니다. 이름을 autopilot-cluster-1로 지정하고 프로젝트의 ID를 autopilot-test로 바꿉니다.</p><pre><code>gcloud beta container --project "autopilot-test-457216" clusters create-auto "autopilot-cluster-1" --region "us-central1" --release-channel "regular" --tier "standard" --enable-ip-access --no-enable-google-cloud-access --network "projects/autopilot-test-457216/global/networks/default" --subnetwork "projects/autopilot-test-457216/regions/us-central1/subnetworks/default" --cluster-ipv4-cidr "/17" --binauthz-evaluation-mode=DISABLED</code></pre><p>5. 준비될 때까지 기다립니다. 생성하는 데 약 10분 정도 걸립니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/81fbc45ba56d0f16ba42724cb8ae45e60b327dbc-1581x258.png" style="width: 100%;" /><p>클러스터를 올바르게 설정하면 확인 메시지가 표시됩니다.</p><p>6. kubectl 명령줄 접근 권한을 구성합니다.</p><pre><code>gcloud container clusters get-credentials autopilot-cluster-1 --region us-central1 --project autopilot-test-457216</code></pre><p>다음과 같이 보셔야 합니다:</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/b0beba1ee00ce9029f586ee32693fc2aa58c7f65-3442x142.png" style="width: 100%;" /><p><em>autopilot-cluster-1에 대한 kubeconfig 항목이 생성되었습니다.</em></p><p>7. <a href="https://www.elastic.co/docs/deploy-manage/deploy/cloud-on-k8s">Elastic Cloud on Kubernetes</a>(ECK) 오퍼레이터를 설치합니다.</p><pre><code># Install ECK Custom Resource Definitions
kubectl create -f https://download.elastic.co/downloads/eck/2.16.1/crds.yaml

# Install the ECK operator
kubectl apply -f https://download.elastic.co/downloads/eck/2.16.1/operator.yaml</code></pre><p>8. 기본값으로 단일 노드 Elasticsearch 인스턴스를 생성해 보겠습니다.</p><p>다양한 설정에 대한 몇 가지 레시피를 확인하려면 <a href="https://www.elastic.co/docs/deploy-manage/deploy/cloud-on-k8s/recipes">이 링크</a>를 참조하세요.</p><p><code>storageClass</code>을(를) 지정하지 않으면 ECK는 기본값으로 설정된 <code>standard-rwo</code>을(를) 사용하며, GKE의 경우 <a href="https://cloud.google.com/kubernetes-engine/docs/how-to/persistent-volumes/gce-pd-csi-driver?cloudshell=true">Compute Engine 영구 디스크 CSI 드라이버</a>를 사용하여 1GB 볼륨을 생성한다는 점에 유의하세요.</p><pre><code>cat <<EOF | kubectl apply -f -
apiVersion: elasticsearch.k8s.elastic.co/v1
kind: Elasticsearch
metadata:
  name: quickstart
spec:
  version: 9.0.0
  nodeSets:
  - name: default
    count: 1
    config:
      node.store.allow_mmap: false
EOF</code></pre><p>기본 GKE 머신의 <code>vm.max_map_count</code> 값이 너무 낮기 때문에 <code>nmap</code>을(를) 비활성화했습니다. 프로덕션 환경에서는 비활성화하는 것을 권장하지 않으며, <code>vm.max_map_count</code> 값을 높이는 것이 좋습니다. 자세한 내용은 <a href="https://www.elastic.co/docs/deploy-manage/deploy/cloud-on-k8s/virtual-memory">여기</a>에서 확인할 수 있습니다.</p><p>9. 또한 Kibana 단일 노드 클러스터를 배포해 보겠습니다. Kibana의 경우 디바이스에서 Kibana에 접속하는 데 사용할 수 있는 외부 IP를 제공하는 LoadBalancer를 추가하겠습니다.</p><pre><code>cat <<EOF | kubectl apply -f -
apiVersion: kibana.k8s.elastic.co/v1
kind: Kibana
metadata:
  name: quickstart
spec:
  version: 9.0.0
  http:
    service:
      metadata:
        annotations:
          cloud.google.com/l4-rbs: "enabled"
      spec:
        type: LoadBalancer
  count: 1
  elasticsearchRef:
    name: quickstart
EOF</code></pre><p>다음과 같이 주석을 참고하세요. </p><p><code>cloud.google.com/l4-rbs: &quot;enabled&quot;</code></p><p><strong><em>이는 Autopilot에 공개 LoadBalancer를 제공하도록 지시하는 중요한 설정입니다. 설정하지 않으면 LoadBalancer가 내부에 있습니다.</em></strong></p><p>10. 파드가 실행 중인지 확인합니다</p><pre><code>kubectl get pods</code></pre><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/99da1270581a137683770efdb9c6e1577ec9fc01-3150x442.png" style="width: 100%;" /><p>11. Elasticsearch 버전, 노드 수 및 상태와 같은 더 구체적인 통계를 보려면 <code>run kubectl get elasticsearch</code> 및 <code>kubectl get kibana</code>을(를)사용할 수도 있습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/86234f307970fd5f78b8acd41496e8cc89ff82d3-3414x326.png" style="width: 100%;" /><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/29160286ccc88928734c8ea11b1923db8e85d49d-3142x318.png" style="width: 100%;" /><p>12. 서비스에 액세스합니다.</p><pre><code>kubectl get svc</code></pre><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/ed1a07314b84a99b4aa1fec3db4b9badeb9587ee-3446x610.png" style="width: 100%;" /><p>이렇게 하면 EXTERNAL-IP 아래에 Kibana의 외부 URL이 표시됩니다. LoadBalancer가 프로비저닝하는 데 몇 분 정도 걸릴 수 있습니다. <strong><em>EXTERNAL-IP의 값을 복사합니다.</em></strong></p><p>13. 다음과 같이 &#x27;elastic&#x27; 사용자의 Elasticsearch 비밀번호를 가져옵니다.</p><pre><code>kubectl get secret quickstart-es-elastic-user -o=jsonpath='{.data.elastic}' | base64 --decode</code></pre><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/ab5054219216ebc15fc0d96e27605aaf13b720c6-3448x210.png" style="width: 100%;" /><p>14. 다음과 같이 브라우저를 통해 <strong>Kibana에 액세스합니다</strong>.</p><ul><li>URL: https://&lt;EXTERNAL_IP&gt;:5601</li><li>Username:elastic</li><li>비밀번호: 28Pao50lr2GpyguX470L2uj5 (이전 단계에서 얻은 비밀번호)</li></ul><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/47cbe88dc14db64db3a256f3f7504cc86a843475-463x503.png" style="width: 100%;" /><p>15. 브라우저에서 액세스하면 시작 화면이 표시됩니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/a75c151c0144b7efe2b730698c0ed0156fa9b16a-1600x1005.png" style="width: 100%;" /><p>노드 변경이나 크기 조정과 같은 Elasticsearch 클러스터 사양을 변경하려면 새 설정으로 yml 매니페스트를 다시 적용하면 됩니다.</p><pre><code>cat <<EOF | kubectl apply -f -
apiVersion: elasticsearch.k8s.elastic.co/v1
kind: Elasticsearch
metadata:
  name: quickstart
spec:
  version: 9.0.0
  nodeSets:
    - name: default
      count: 2
      config:
        node.store.allow_mmap: false
      podTemplate:
        spec:
          containers:
            - name: elasticsearch
              resources:
                requests:
                  memory: 1.5Gi
                  cpu: 2
                limits:
                  memory: 1.5Gi
                  cpu: 2
EOF</code></pre><p>이 예시에서는 노드를 하나 더 추가하고 RAM과 CPU를 수정해 보겠습니다. 보시다시피 이제 <code>kubectl get elasticsearch</code>에는 2개의 노드가 표시됩니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/48b475622cc48890bff8105d151f2cbde28d7021-3418x298.png" style="width: 100%;" /><p>Kibana도 마찬가지입니다.</p><pre><code>cat <<EOF | kubectl apply -f -
apiVersion: kibana.k8s.elastic.co/v1
kind: Kibana
metadata:
  name: quickstart
spec:
  version: 9.0.0
  http:
    service:
      metadata:
        annotations:
          cloud.google.com/l4-rbs: "enabled"
      spec:
        type: LoadBalancer
  count: 1
  elasticsearchRef:
    name: quickstart
  podTemplate:
    spec:
      containers:
        - name: kibana
          env:
            - name: NODE_OPTIONS
              value: "--max-old-space-size=1024"
          resources:
            requests:
              memory: 0.5Gi
              cpu: 0.5
            limits:
              memory: 1Gi
              cpu: 1
EOF</code></pre><p>컨테이너 CPU/RAM 및 <a href="https://nodejs.org/">Node.js </a>메모리 사용량(<a href="https://nodejs.org/api/cli.html#--max-old-space-sizesize-in-mib">max-old-space-size</a>)을 조정할 수 있습니다.</p><p><a href="https://www.elastic.co/docs/deploy-manage/deploy/cloud-on-k8s/volume-claim-templates">기존 볼륨 클레임은 크기를 줄일 수 없다</a>는 점에 유의하세요. 업데이트를 적용한 후 오퍼레이터는 중단 시간을 최소화하면서 변경 사항을 적용할 것입니다.</p><p>불필요한 비용 발생을 방지하기 위해 테스트가 끝나면 클러스터를 삭제하는 것을 잊지 마세요.</p><pre><code>gcloud container clusters delete autopilot-cluster-1</code></pre><h2>다음 단계</h2><p>Kubernetes 및 Google Kubernetes Engine에 대해 더 자세히 알아보려면 다음 문서를 참조하세요.</p><ul><li><a href="https://www.elastic.co/docs/deploy-manage/deploy/cloud-on-k8s">Elastic Cloud on Kubernetes | Elastic Docs</a></li><li><a href="https://cloud.google.com/blog/products/containers-kubernetes/introducing-gke-autopilot">GKE Autopilot 소개 | Google Cloud 블로그</a></li><li><a href="https://cloud.google.com/kubernetes-engine/docs/concepts/autopilot-overview">Autopilot 개요 | Google Kubernetes Engine(GKE)</a></li></ul>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[더 스마트한 쿼리 환경을 위해 Elasticsearch와 함께 Azure LLM 함수 사용]]></title>
            <description><![CDATA[유연한 하이브리드 검색 결과를 제공하기 위해 Elasticsearch와 함께 Azure Gen AI LLM 함수를 사용하는 부동산 검색 앱의 예를 살펴보세요. GitHub 코드스페이스에서 예제 앱을 구성하고 실행하는 방법을 단계별로 확인하세요.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/azure-llm-functions-elasticsearch</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/azure-llm-functions-elasticsearch</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Jonathan Simon]]></dc:creator>
            <pubDate>Fri, 13 Jun 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/1431b90c7e00de06574c1e33c44a2e89296c824e-1200x628.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>정확성. 중요한 순간에는 매우 중요합니다. 특정 항목을 검색할 때는 정확성이 매우 중요합니다. 그러나 너무 정확한 쿼리는 결과를 반환하지 않는 경우도 있으므로, 잠재적으로 관련성이 있는 데이터를 추가로 찾기 위해 쿼리 범위를 유연하게 확장하는 것이 유리합니다.</p><p>이 블로그 게시물에서는 Elasticsearch와 Azure Open AI를 사용하여 매우 구체적인 부동산을 검색할 때 정확한 결과를 찾는 동시에 특정 일치 항목을 찾을 수 없는 경우에도 관련 결과를 제공하는 방법을 보여주는 예제 앱을 만드는 방법에 대해 설명합니다. 검색 템플릿과 함께 Elasticsearch 인덱스를 생성하는 데 필요한 모든 단계를 안내해드리겠습니다. 그런 다음 Azure OpenAI를 사용하여 사용자 쿼리를 받아 놀랍도록 사용자 정의된 결과를 생성할 수 있는 Elasticsearch 검색 템플릿 쿼리로 변환하는 앱을 만드는 방법에 대한 전체 프로세스를 설명합니다.</p><p>다음은 예제 부동산 검색 앱을 만드는 데 사용할 모든 리소스 목록입니다:</p><ul><li>Elasticsearch 색인 및 검색 템플릿</li><li>Azure OpenAI</li><li>Azure 맵 API</li><li><a href="https://github.com/jwilliams-elastic/msbuild-intelligent-query-demo/blob/main/VectorDBSetup.ipynb">코드스페이스 주피터 노트북</a></li><li>시맨틱 커널</li><li>블레이저 프론트엔드가 포함된 C# 앱</li></ul><h2>스마트 쿼리 워크플로</h2><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/0c4a7c835e06c514f158c00ab1055a7ba719a35f-1600x765.png" style="width: 100%;" /><p>이 워크플로는 LLM, LLM 도구, 검색을 결합하여 자연어 쿼리를 구조화되고 관련성 높은 검색 결과로 변환합니다:</p><ul><li><strong>LLM(대규모 언어 모델</strong> ) - 복잡한 사용자 쿼리를 해석하고 도구를 오케스트레이션하여 검색 의도를 추출하고 컨텍스트를 보강합니다.</li><li><strong>LLM 도구</strong> - 각 LLM 도구는 이 게시물을 위해 만든 C# 프로그램입니다. 세 가지 도구가 있습니다:<ul><li><em>매개변수 추출 도구</em>: 쿼리에서 침실, 욕실, 기능, 가격 등의 주요 속성을 가져옵니다.</li><li><em>지오코드 도구</em>: 공간 필터링을 위해 위치 이름을 위도/경도로 변환합니다.</li><li><em>검색 도구</em>: Elasticsearch 검색 템플릿을 쿼리 매개변수로 채우고 검색을 실행합니다. <strong>하이브리드 검색</strong> - 내장된 ML 추론을 통해 하이브리드 검색(전체 텍스트 + 고밀도 벡터)을 실행합니다. 이러한 계층화된 접근 방식은 최종 사용자에게 더욱 스마트하고 컨텍스트를 인식하는 쿼리 환경을 보장합니다.</li></ul></li></ul><h2>애플리케이션 아키텍처</h2><p>다음은 예제 앱의 시스템 아키텍처 다이어그램입니다. 코드스페이스 Jupyter 노트북을 사용해 Elastic Cloud와 상호 작용할 것입니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/2880afee184cd9270c0eb4310e51418e2339784d-936x452.png" style="width: 100%;" /><h2>필수 구성 요소</h2><div style="display:none">Unknown block type "block", specify a component for it in the `components.types` option</div><h2>코드스페이스에서 예제 앱 리포지토리 복제하기</h2><p>먼저 예제 애플리케이션의 코드를 복제하여 시작합니다. 애플리케이션을 복제하고 실행하는 방법을 제공하는 <a href="https://github.com/codespaces/">GitHub 코드스페이스에서</a> 이 작업을 수행할 수 있습니다. <strong>새 코드스페이스를 클릭</strong>합니다 .</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/b89cbec491659b6c8a0bb9551ed2629f7a37f9fd-1600x427.png" style="width: 100%;" /><p>그런 다음 <a href="https://github.com/jwilliams-elastic/msbuild-intelligent-query-demo">리포지토리</a> <strong>드롭다운에서</strong> <strong>jwilliams-elastic/msbuild-intelligent-query-demo</strong> 리포지토리를 선택하고 코드스페이스 생성을 클릭합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/43ea377554527af9578400f16cd2342bf8fff3a2-1600x1049.png" style="width: 100%;" /><h2>.env 만들기 파일</h2><p>우리는 Python Jupyter Notebook을 사용해 Elastic Cloud에 액세스하고 상호 작용할 것이며, 구성 파일에 저장된 구성 값을 사용해 그렇게 할 것입니다. 노트북의 구성 파일은 파일 이름이 <strong><em>.env여야</em></strong> 합니다. 를 클릭하시면 지금 바로 만들 수 있습니다.</p><ol><li>GitHub 코드스페이스에서 <strong>새 파일 </strong>버튼을 클릭하고 <strong><em>.env라는</em></strong>이름의 파일을 추가합니다.</li><li>새로 만든 <strong><em>.env</em></strong> 파일에 다음 내용을 추가합니다. 파일</li></ol><pre><code>ELASTIC_URL=
ELASTIC_API_KEY=</code></pre><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/84d4f327948858ba61db0001dd8cf780d42fe0a7-1600x875.gif" style="width: 100%;" /><p><em>보시다시피.env에</em> 추가해야 하는 몇 가지 누락된 값인<strong>ELASTIC_URL과</strong> <strong>ELASTIC_API_KEY가 있습니다.</strong> 파일입니다. 이제 예제 앱의 검색 기능을 구동할 백엔드 역할을 할 Elasticsearch 서버리스 프로젝트를 생성하여 이를 준비해 보겠습니다.</p><h2>Elastic 서버리스 프로젝트 생성</h2><ol><li><a href="http://cloud.elastic.co">cloud.elastic.co로</a> <strong>이동하여 새 서버리스 프로젝트 생성을 클릭합니다.</strong></li><li><strong>Elasticsearch </strong>솔루션을 보려면 <strong>다음을 클릭하세요.</strong></li><li><strong>벡터에 최적화됨을</strong>선택합니다.</li><li><strong>클라우드 공급자를</strong> <strong>Azure로</strong>설정</li><li><strong>서버리스 프로젝트 만들기를</strong>클릭합니다.</li><li>기본 탐색 메뉴에서 <strong>시작하기를</strong> 클릭하고 아래로 스크롤하여 <strong>연결 세부 정보를</strong>복사합니다.</li><li><strong>복사 </strong>버튼을 클릭하여 <strong>연결</strong> 세부 정보에서 <strong>Elasticsearch 엔드포인트를 복사합니다.</strong></li><li><strong><em>.env</em></strong> 업데이트 파일을 복사된 <strong></strong> <strong>Elasticsearch 엔드포인트로 설정합니다.</strong></li></ol><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/77a35e55446d396066b68cfd132d1543a07b81cc-1600x875.gif" style="width: 100%;" /><h2>Elastic API 키 생성</h2><ol><li>Elasticsearch <strong>시작하기</strong> 페이지를 열고,<strong>API 키</strong> 추가 섹션에서 <strong>새로 만들기를 클릭합니다.</strong></li><li>키 <strong>이름</strong>입력</li><li><strong> API 키 생성을</strong>클릭합니다.</li><li>복사 버튼을 클릭하여 API 키 값을 복사합니다.</li><li>다시 <strong>코드스페이스로</strong> 돌아가서 <strong><em>.env</em> </strong>파일을 열어 편집하고 복사한 값을 붙여넣어<strong> ELASTIC_API_KEY를</strong>설정합니다.</li></ol><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/20168cba493d8e2c0d9ae7704eb0ae707df58e4c-1600x875.gif" style="width: 100%;" /><h2>코드스페이스 노트북을 열고 라이브러리 종속성을 설치합니다.</h2><p>파일 탐색기에서 <strong><em>VectorDBSetup.ipynb</em></strong> 파일을 선택해 노트북을 엽니다. 노트북이 로드된 후,<a href="https://github.com/jwilliams-elastic/msbuild-intelligent-query-demo/blob/main/VectorDBSetup.ipynb?short_path=17c25d8#L40-L52"><strong>라이브러리 설치라는 </strong></a> <a href="https://github.com/jwilliams-elastic/msbuild-intelligent-query-demo/blob/main/VectorDBSetup.ipynb?short_path=17c25d8#L40-L52">제목의 노트북 섹션을</a><strong> 찾습니다. </strong>섹션 재생 버튼을 클릭합니다.</p><p>GitHub 코드스페이스에서 노트북을 처음 실행하는 경우, 코드스페이스 커널을 선택하고 Python 환경을 구성하라는 메시지가 표시됩니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/2272f70615dfb9dcbeb91f39b6dd5076213e24a5-1600x875.gif" style="width: 100%;" /><h2>코드스페이스 노트북을 사용하여 임포트를 정의하고 환경 변수를 로드하세요.</h2><p>노트북의 다음 <a href="https://github.com/jwilliams-elastic/msbuild-intelligent-query-demo/blob/main/VectorDBSetup.ipynb?short_path=17c25d8#L58-L104">섹션인 <strong>가져오기 정의 및 환경 변수 로드</strong></a> 섹션으로 이동합니다. 섹션 재생 버튼을 클릭합니다.</p><p>이 코드는 노트북에서 사용하는 Python 라이브러리를 가져오고 <em>.env </em>파일에서 환경 변수를 로드합니다.를 만들 수 있습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/aa9f9198ff452ac0c4ce33b00f253731dbee22c5-1600x875.gif" style="width: 100%;" /><h2>코드스페이스 노트북을 사용하여 Elastic ML 추론 엔드포인트 만들기</h2><p>다음 노트북 <a href="https://github.com/jwilliams-elastic/msbuild-intelligent-query-demo/blob/main/VectorDBSetup.ipynb?short_path=17c25d8#L112-L157">섹션인 <strong>ML 추론 엔드포인트 만들기로</strong></a> 이동합니다. 섹션 재생 버튼을 클릭합니다.</p><p>이렇게 하면 데이터에서 텍스트 임베딩을 생성하는 데 사용할 새로운 ML 추론 엔드포인트가 Elasticsearch 프로젝트에 생성됩니다. 텍스트 임베딩은 의미론적 검색을 강화하기 위해 Elasticsearch에 저장될 텍스트의 벡터 표현입니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/97c1afab3e64027ee5ae77f377d56ba406ae1765-1600x875.gif" style="width: 100%;" /><h2>코드스페이스 노트북을 사용하여 Elasticsearch 인덱스 만들기</h2><p>다음 노트북 <a href="https://github.com/jwilliams-elastic/msbuild-intelligent-query-demo/blob/main/VectorDBSetup.ipynb?short_path=17c25d8#L165-L224">섹션의 <strong>Elasticsearch 인덱스 생성으로</strong></a> 이동합니다. 섹션 재생 버튼을 클릭합니다.</p><p>이렇게 하면 예제 데이터와 ML 추론 엔드포인트를 통해 생성된 모든 관련 벡터 데이터를 저장할 Elasticsearch 인덱스가 생성됩니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/23a66283ee41239e24fb8455c3cd95641982ca6b-1600x875.gif" style="width: 100%;" /><h2>코드스페이스 노트북을 사용하여 Elasticsearch 검색 템플릿 만들기</h2><p><a href="https://github.com/jwilliams-elastic/msbuild-intelligent-query-demo/blob/main/VectorDBSetup.ipynb?short_path=17c25d8#L232-L384"><strong>검색 템플릿이라는 </strong></a>제목의 다음 노트북 <a href="https://github.com/jwilliams-elastic/msbuild-intelligent-query-demo/blob/main/VectorDBSetup.ipynb?short_path=17c25d8#L232-L384">섹션으로 계속</a> 진행합니다. 섹션 재생 버튼을 클릭합니다.</p><p>이렇게 하면 사용자의 검색 쿼리에서 파싱된 단어로 채워진 템플릿으로 예제 앱에서 사용할 <a href="https://www.elastic.co/kr/docs/solutions/search/search-templates">검색 템플릿이</a> 만들어집니다. 이를 통해 Elasticsearch 인덱스에서 데이터를 쿼리할 때 얼마나 구체적으로 쿼리할지 구성하고 제어할 수 있습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/4c001a3e4d1cca4cfb5c043fea92c7ccaf9cb64a-1600x875.gif" style="width: 100%;" /><h2>코드스페이스 노트북을 사용하여 Elasticsearch 인덱스로 데이터 수집하기</h2><p>노트북의 다음 <a href="https://github.com/jwilliams-elastic/msbuild-intelligent-query-demo/blob/main/VectorDBSetup.ipynb?short_path=17c25d8#L392-L450">섹션인 <strong>속성 데이터 수집으로</strong></a> 이동합니다. 섹션 실행 버튼을 클릭합니다.</p><p>이 코드 섹션을 실행하면 <em>properties.jsonl </em>파일에 포함된 예제 데이터가 일괄 로드됩니다. 몇 분 후 프로세스가 성공적으로 완료되었다는 확인 메시지가 표시됩니다. Elastic Cloud의 인덱스 <strong>관리</strong> 섹션으로 이동하여 인덱스에 예상 레코드가 포함되어 있는지 확인할 수 있습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/e9698c798541ccfc08143a939846597028e3c566-1600x875.gif" style="width: 100%;" /><h2>앱설정.json을 생성하여 C# 앱을 구성합니다.</h2><p>Elasticsearch 인덱스가 생성되고 데이터로 채워졌으므로 이제 Elastic 및 Azure Cloud와 함께 작동하도록 예제 앱을 구성할 준비가 되었습니다. C# 예제 앱은 <em>appsettings.json이라는</em> 파일을 사용하여 API 키와 같은 액세스 정보를 저장하고 로드합니다. 이제 코드스페이스의 편집기를 사용하여 <em>앱설정.json</em> 파일을 생성합니다.</p><p>1.<strong>홈파인더앱</strong> 폴더에 <em>앱설정.json을 생성합니다.</em></p><p>2. <em>앱설정.json</em> 파일에 다음 코드를 붙여넣습니다.</p><pre><code>{
 "ElasticSettings": {
   "Url": "",
   "ApiKey": "",
   "IndexName": "properties",
   "TemplateId": "properties-search-template"
 },
 "AzureOpenAISettings": {
   "Endpoint": "",
   "ApiKey": "",
   "DeploymentName": "gpt-4o"
 },
 "AzureMapsSettings": {
   "Url": "https://atlas.microsoft.com/geocode",
   "ApiKey": ""
 },
 "Logging": {
   "LogLevel": {
 	"Default": "Information",
 	"Microsoft.AspNetCore": "Warning"
   }
 },
 "AllowedHosts": "*"
}
</code></pre><p>3. 3.<strong>ElasticSettings</strong> 섹션에서 <strong>Url</strong> 및 <strong>ApiKey </strong>값을 찾습니다. <em>.env에서</em> 설정한 값과 동일한 값으로 설정합니다. 파일을 이전 단계에 추가합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/00dad25bacdea2adcbd1e6eca7658867a49b0d8c-1600x875.gif" style="width: 100%;" /><h2>Azure OpenAI 서비스 만들기</h2><p>예제 앱은 Azure OpenAI를 사용하여 앱 사용자의 쿼리를 구문 분석한 다음, 사용자가 검색하는 내용을 유연하게 전달하기 위해 검색 템플릿을 채워서 Elasticsearch에 요청을 제출할 것입니다.</p><ol><li>새 브라우저 탭을 열고 Azure 포털에서 <a href="https://portal.azure.com/#blade/Microsoft_Azure_ProjectOxford/CognitiveServicesHub/OpenAI">AI 파운드리 | Azure OpenAI로</a> 이동합니다. <strong>생성</strong>]을 클릭합니다.</li><li>만들기 양식에서 <strong>리소스 그룹을</strong> 선택합니다.</li><li><strong>이름</strong>입력</li><li><strong>요금제</strong>선택</li><li><strong>다음</strong>클릭</li><li><strong>네트워크</strong> 탭에서<strong> 다음을</strong>클릭합니다.</li><li><strong>태그</strong> 탭에서 <strong>다음을</strong>클릭합니다.</li><li><strong>검토 및 제출</strong> 탭에서 <strong>만들기를</strong>클릭합니다.</li><li>생성이 완료되면 <strong>리소스로 이동을</strong>클릭합니다.</li><li>왼쪽 탐색 메뉴에서 <strong>키 및 엔드포인트를</strong> 선택합니다.</li><li><strong>엔드포인트를</strong> 복사하여 코드스페이스 편집기가 열려 있는 브라우저 탭에서 생성한 <em>앱설정.json</em> 파일에 붙여넣습니다.</li><li>그런 다음 Azure OpenAI <strong>키 및 엔드포인트 페이지가</strong> 포함된 브라우저 탭으로 돌아갑니다. <strong>키 1의</strong> 복사 버튼을 클릭하고 복사한 값을 코드스페이스 편집기가 열려 있는 브라우저 탭에서 <em>앱설정.json</em> 파일에 다시 붙여넣습니다.</li></ol><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/6d49197302d110410dca0a53b6ae90237cf2dfd6-1600x875.gif" style="width: 100%;" /><h2>Azure Open AI 서비스에 gpt-4o 모델 배포 추가</h2><p>이제 Azure OpenAI 서비스가 실행 중이지만 예제 앱에 필요한 LLM 기능을 제공하려면 여전히 모델 배포가 필요합니다. 다양한 모델 중에서 선택할 수 있습니다. 생성한 <em>앱설정.json</em> 파일에 이미 지정되어 <em>있으므로 gpt-4o를</em> 배포해 보겠습니다.</p><p></p><ol><li><a href="https://ai.azure.com/resource/playground">Azure AI 파운드리로</a> 이동하여 <strong>배포 만들기를</strong>클릭합니다.</li><li><em>gpt-4o를</em> 검색하고 결과에서 선택합니다.</li><li><strong>확인을</strong> 클릭하여 선택합니다.</li><li><strong>배포를</strong> 클릭하여 모델을 배포합니다.</li></ol><p><em>gpt-4o</em> 모델을 성공적으로 배포했으면 왼쪽 탐색 메뉴에서 <strong>배포를</strong> <strong><em>선택하고 gpt-4o</em></strong> 배포가 성공 상태와 함께 나열되는지 <strong>확인할</strong> <strong>수</strong> 있습니다.<br/></p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/1b817ab67c05634e9c72777593b4d1a2c6c28191-1600x875.gif" style="width: 100%;" /><h2>Azure 맵 계정 만들기</h2><p>예제 앱의 사용자가 특정 지역의 부동산 매물을 검색할 수 있기를 바라지만, 너무 구체적일 필요는 없습니다. 누군가 지역 농산물 직판장 근처의 부동산을 검색하려는 경우 Azure Maps는 OpenAI LLM이 시장의 위도 및 경도 좌표를 가져오는 데 사용할 수 있는 서비스입니다. 그런 다음 좌표는 특정 위치 및 지리적 거리 고려 사항을 포함하는 사용자 쿼리에 대해 Elasticsearch로 전송되는 검색 템플릿 기반 요청에 포함될 수 있습니다.</p><ol><li><strong></strong> <a href="https://portal.azure.com/#browse/Microsoft.Maps%2Faccounts">Azure 맵 계정에서 만들기를 클릭합니다.</a></li><li><strong>리소스 그룹</strong>선택</li><li><strong>이름</strong>입력</li><li>라이선스 및 개인정보처리방침에 동의</li><li><strong>검토 및 생성을</strong>클릭합니다 .</li><li><strong>만들기를</strong>클릭합니다.</li><li>계정 생성이 완료되면 <strong>리소스로 이동을</strong>클릭합니다.</li><li>왼쪽 탐색 메뉴에서 <strong>인증을</strong> 클릭합니다.</li><li><strong>기본 키</strong> 값을 복사하여 코드스페이스 편집기가 포함된 브라우저 탭의 <em>앱설정.json</em> 파일의 <strong>AzureMapsSettings</strong> 섹션에 ApiKey의 <strong>값으로 붙여넣습니다.</strong></li></ol><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/90fadd48e366682e2bad91e32988f93c6354e126-1600x875.gif" style="width: 100%;" /><h2>예제 앱을 사용해 보세요.</h2><p>이제 재미있는 부분입니다. 이제 예제 앱을 실행할 차례입니다. 앱을 구동하는 데 필요한 Elastic Cloud 및 Azure Cloud 리소스와 함께 모든 구성 세부 정보가 준비되어 있습니다.</p><p>1. 코드스페이스 편집기에서 터미널 창을 엽니다.</p><p>2. 다음 명령을 사용하여 활성 디렉터리를 예제 앱 폴더로 변경합니다.<br/></p><pre><code>cd HomeFinderApp</code></pre><p>3. 3. 다음 <em>닷넷</em> 명령을 사용하여 앱을 실행합니다.</p><pre><code>dotnet run</code></pre><p>4. 4. <strong>브라우저에서 열기 </strong>버튼이 나타나면 클릭합니다.</p><p>5. 기본 검색을 테스트한 다음 사용자 지정 검색을 시도해 보세요. 검색 결과를 생성하기 위해 백엔드에서 수행되는 작업에 대한 자세한 내용을 보려면 <strong>도구 </strong> <strong>호출</strong>옆에 있는 표시 링크를 클릭하면 됩니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/821fe7b9446de5ed646d938cc9484a7ddad21030-1600x875.gif" style="width: 100%;" /><p><strong>보너스: </strong>GPT-4o를 실제로 테스트해보고 싶으시다면 다음 검색을 시도해보세요: <em>침실 30개 이상, 욕실 20개 이상, 수영장과 차고가 있고 해변 근처에 있는 20만 달러 미만의 디즈니 월드 플로리다 인근 숙소를 찾고 있습니다. </em>이 쿼리는 검색 도구를 여러 번 호출한 후 결과를 반환합니다.</p><h2>Elastic은 검색 AI를 위한 솔루션입니다</h2><p>실행 중인 앱은 검색 템플릿을 기본 데이터 소스로 사용하여 Elasticsearch를 통한 Gen AI LLM 가이드 검색의 예입니다. 예제 앱을 자유롭게 실험하고 사용자 지정하여 정확하면서도 유연한 검색 환경을 만들어 사용자가 원하는 것을 찾을 수 있도록 하세요.</p><p>읽어주셔서 감사합니다. <a href="https://cloud.elastic.co/registration">Elastic Cloud를</a> 사용해 보세요.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[MCP(모델 컨텍스트 프로토콜)의 현재 상태]]></title>
            <description><![CDATA[MCP, 프로젝트 업데이트, 기능, 보안 문제, 새로운 사용 사례, Elastic의 Elasticsearch MCP 서버를 다루는 방법에 대해 알아보세요.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/mcp-current-state</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/mcp-current-state</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[JD Armada]]></dc:creator>
            <pubDate>Thu, 12 Jun 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/02b8c8672ffa129e0ed91a92d6cab612a01d27f2-1200x628.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>최근 샌프란시스코에서 열린 <a href="https://mcpdevsummit.ai/">MCP 개발자 서밋에</a> 참석했는데, 모델 컨텍스트 프로토콜(MCP)이 AI 에이전트 및 컨텍스트가 풍부한 AI 애플리케이션의 기본 구성 요소로 빠르게 자리 잡고 있다는 것을 알 수 있었습니다. Elastic에서는 <a href="https://www.elastic.co/kr/elasticsearch/agent-builder">에이전트 빌더에서</a> 직접 MCP 서버를 노출함으로써 이러한 방향으로 나아가고 있으며, Elasticsearch를 모든 MCP 호환 에이전트를 위한 최고 수준의 컨텍스트 및 도구 제공자로 만들고 있습니다. 이 포스팅에서는 이벤트의 주요 업데이트, 새로운 사용 사례, MCP의 향후 예정 사항, 에이전트 빌더를 사용하여 에이전트가 MCP를 통해 Elasticsearch를 사용할 수 있도록 하는 방법에 대해 살펴보겠습니다.</p><h2>모델 컨텍스트 프로토콜(MCP)이란 무엇인가요?</h2><p>익숙하지 않은 분들을 위해 설명하자면, <a href="https://modelcontextprotocol.io/introduction">모델 컨텍스트 프로토콜은</a> 다양한 데이터 소스 및 도구에 AI 모델을 연결하는 구조화된 양방향 방법을 제공하여 보다 관련성 있고 정보에 입각한 응답을 생성할 수 있도록 하는 개방형 표준입니다. 일반적으로 &quot;<a href="https://modelcontextprotocol.io/introduction">AI 애플리케이션용 USB-C 포트</a>&quot;라고 불립니다.</p><p>다음은 양방향성을 강조하는 아키텍처 다이어그램입니다:</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/5eba1e59514eb58a5220bb92bb49e6328ee83cd7-674x466.png" style="width: 100%;" /><p>AI 애플리케이션을 확장하는 데 있어 주요 과제 중 하나는 각각의 새로운 데이터 소스에 대한 사용자 지정 통합을 구축해야 하는 것이므로, 이는 AI 실무자에게 중요한 변화입니다. MCP는 모델에 컨텍스트를 관리하고 제공하기 위한 지속 가능하고 재사용 가능한 아키텍처를 제공합니다. 모델에 구애받지 않고 서버에 구애받지 않으며 완전한 오픈 소스입니다.</p><p>MCP는 애플리케이션 간의 통합을 표준화하기 위한 일련의 API 사양 중 가장 최신 버전입니다. 과거에는 RESTful 서비스를 위한 OpenAPI, 데이터 쿼리를 위한 GraphQL, 마이크로 서비스 통신을 위한 gRPC를 사용했습니다. MCP는 이러한 구형 사양의 구조화된 엄격함을 공유할 뿐만 아니라 이를 제너레이티브 AI 설정에 적용하여 사용자 지정 커넥터 없이도 에이전트를 다른 시스템에 쉽게 연결할 수 있습니다. 여러 면에서 MCP는 HTTP가 웹에서 했던 일을 AI 에이전트에서 하는 것을 목표로 합니다. HTTP가 브라우저와 웹사이트 간의 통신을 표준화한 것처럼, MCP는 AI 에이전트가 주변 데이터 세계와 상호 작용하는 방식을 표준화하고자 합니다.</p><h2>MCP와 다른 에이전트 프로토콜 비교</h2><p>상담원 프로토콜 환경은 빠르게 확장되고 있으며, 상담원 상호 작용 방식을 정의하기 위해 경쟁하는 12개 이상의 새로운 표준이 등장하고 있습니다. LlamaIndex의 <a href="https://x.com/seldo">로리 보스는</a> 에이전트 간 대화에 초점을 맞춘 에이전트 간 프로토콜과 LLM에 구조화된 컨텍스트를 전달하는 데 초점을 맞춘 MCP와 같은 컨텍스트 지향 프로토콜의 두 가지 유형으로 대부분 분류할 수 있다고 설명합니다.</p><p>Google의 <a href="https://developers.googleblog.com/en/a2a-a-new-era-of-agent-interoperability/">A2A</a> (에이전트 간 프로토콜), Cisco와 IBM의 <a href="https://agentcommunicationprotocol.dev/introduction/welcome">ACP</a> (에이전트 통신 프로토콜), <a href="https://agoraprotocol.org/">아고라와</a> 같은 다른 인기 프로토콜은 에이전트 간 협상, 연합 구축, 심지어 탈중앙화된 신원 시스템을 가능하게 하는 것을 목표로 합니다. MCP는 상담원이 서로 대화하는 방식이 아니라 툴과 데이터에 액세스하는 방식에 초점을 맞추기 때문에 좀 더 실용적인 접근 방식을 취합니다(향후 다양한 방식으로 이를 지원할 수도 있지만).</p><p>현재 MCP를 차별화하는 요소는 견인력과 추진력입니다. 초창기 프론트엔드 프레임워크의 React와 마찬가지로 MCP도 틈새 문제에서 시작하여 현재는 가장 많이 채택되고 확장 가능한 에이전트 프로토콜 중 하나로 자리 잡았습니다.</p><h2>서밋 요약: MCP의 진화하는 우선순위</h2><p>이 서밋에는 Anthropic, Okta, OpenAI, AWS, GitHub 등의 기여자들이 연사로 참여했습니다. 이번 회담에서는 핵심 프로토콜 개선부터 실제 구현에 이르기까지 다양한 논의가 이루어졌으며, 즉각적인 우선순위와 장기적인 우선순위에 대한 윤곽이 드러났습니다. 이 강연은 초기 실험과 단순한 도구 호출에서 벗어나 MCP를 기반으로 신뢰할 수 있고 확장 가능한 모듈식 AI 시스템을 구축하는 것으로의 전환을 반영했습니다.</p><p>여러 연사가 MCP가 단순한 프로토콜 배관을 넘어 AI 네이티브 웹의 기반이 될 수 있는 미래에 대해 이야기했습니다. 자바스크립트를 통해 사용자가 웹 페이지를 클릭하고 상호 작용할 수 있었던 것처럼, MCP를 통해 에이전트가 우리를 대신하여 동일한 작업을 수행할 수 있습니다. 예를 들어, 이커머스에서는 사용자가 쇼핑을 위해 웹사이트를 수동으로 탐색하는 대신 상담원에게 로그인하여 특정 제품을 찾아 장바구니에 추가하고 결제하라고 간단히 지시할 수 있습니다.</p><p>페이팔은 이번 서밋에서 이러한 에이전트 커머스 경험을 가능하게 하는 새로운 에이전트 툴킷과 MCP 서버를 선보였습니다. MCP가 도구와 데이터 소스에 대한 안전하고 안정적인 액세스를 제공하므로 상담원은 웹을 읽는 데 그치지 않고 이를 기반으로 조치를 취할 수 있습니다. 오늘날 MCP는 이미 강력한 추진력을 갖춘 표준으로 자리 잡았으며, 향후에는 웹 전반에서 AI를 활용한 사용자 상호작용의 표준이 될 수 있습니다.</p><h2>MCP 프로젝트 업데이트: 운송, 도출 및 구조화된 툴링</h2><p>MCP의 핵심 기여자인 <a href="https://x.com/JeromeSwannack">제롬 스와낵은</a> 지난 6개월 동안 프로토콜 사양에 대한 몇 가지 업데이트를 공유했습니다. 이러한 변경의 주요 목표는 다음과 같습니다:</p><ol><li>스트리밍 가능한 HTTP를 추가하여 원격 MCP 활성화하기</li><li>발화 및 도구 출력 스키마를 추가하여 더욱 풍부한 상담원 상호작용 모델을 사용하려면 다음과 같이 하세요.</li></ol><p>MCP가 오픈 소스이기 때문에 개발자는 이미 스트리밍 가능한 HTTP와 같은 변경 사항을 구현할 수 있습니다. 유도 및 도구 출력 스키마는 현재 공개되지 않았으며 초안 단계에 있으며 발전할 수 있습니다.</p><p><strong>스트리밍 가능한 HTTP </strong><a href="https://modelcontextprotocol.io/specification/2025-03-26/basic/transports">(2025년 3월 26일 출시</a>)<strong>:</strong> 영향력 있는 기술 업데이트는 새로운 전송 메커니즘으로 스트리밍 가능한 HTTP를 도입한 것입니다. 이는 서버 전송 이벤트(SSE)를 확장성이 뛰어난 양방향 모델로 대체하여 단일 HTTP 연결을 통해 청크 전송 인코딩과 점진적 메시지 전달을 지원합니다. 이를 통해 AWS Lambda와 같은 클라우드 인프라에 MCP 서버를 배포하고 오래 지속되는 연결이나 폴링이 필요 없는 엔터프라이즈 네트워크 제약을 지원할 수 있습니다.</p><p><strong>유도 </strong><a href="https://modelcontextprotocol.io/specification/2025-06-18/client/elicitation">(06-18-2025에 출시</a>)<strong>:</strong> 도출을 통해 서버는 클라이언트로부터 컨텍스트를 구조화하는 방식에 대한 스키마를 정의할 수 있습니다. 기본적으로 서버는 필요한 사항과 기대하는 입력의 종류를 설명할 수 있습니다. 여기에는 몇 가지 의미가 있습니다: 서버 빌더의 경우 더 복잡한 에이전트 상호작용을 구축할 수 있습니다. 클라이언트 빌더의 경우 이러한 스키마에 맞게 조정되는 동적 UI를 구현할 수 있습니다. 그러나 사용자로부터 민감하거나 개인 식별이 가능한 정보를 추출하는 데 유도 기능을 사용해서는 안 됩니다. 개발자는 <a href="https://modelcontextprotocol.io/specification/draft/client/elicitation#security-considerations">모범 사례를</a> 따라 유도 프롬프트가 안전하고 적절하게 유지되도록 해야 하며, 특히 MCP가 성숙해짐에 따라 더욱 그렇습니다. 이는 이 글의 뒷부분에서 논의할 더 광범위한 보안 문제와 관련이 있습니다.</p><p><strong>도구 출력 스키마 </strong><a href="https://modelcontextprotocol.io/specification/draft/server/tools#output-schema">(06-18-2025에 출시</a>)<strong>: </strong>이 개념은 클라이언트와 LLM이 도구 출력 모양을 미리 알 수 있게 해줍니다. 도구 출력 스키마를 통해 개발자는 도구가 반환할 것으로 예상되는 내용을 설명할 수 있습니다. 이러한 스키마는 직접 도구 호출의 주요 한계 중 하나인 컨텍스트 창을 비효율적으로 사용하는 문제를 해결합니다. 컨텍스트 창은 LLM으로 작업할 때 가장 중요한 리소스 중 하나로 간주되며, 도구를 직접 호출하면 LLM의 컨텍스트에 완전히 푸시되는 원시 콘텐츠를 반환합니다. 도구 출력 스키마는 MCP 서버가 구조화된 데이터를 제공하도록 허용하여 토큰과 컨텍스트 창을 더 잘 활용할 수 있도록 도와줍니다. 다음은 일반적인 도구에 대한 몇 가지 <a href="https://modelcontextprotocol.io/specification/draft/server/tools#security-considerations">모범 사례입니다</a>.</p><p>이러한 새로운 업데이트와 향후 추가 기능을 통해 MCP는 더욱 모듈화되고 유형화되어 프로덕션에 바로 사용할 수 있는 에이전트 프로토콜이 될 것입니다.</p><h2>잘 사용되지 않는 전원 기능: 샘플링 및 루트</h2><p>MCP 사양이 새로운 것은 아니지만, 기조연설에서 샘플링과 루트가 모두 강조되었습니다. 이 두 가지 기본 요소는 현재 간과되고 잘 알려지지 않았지만 에이전트 간의 더욱 풍부하고 안전한 상호 작용에 크게 기여할 수 있습니다.</p><p><strong>샘플링 - 서버가 클라이언트에 완료를 요청할 수 있습니다: </strong><a href="https://modelcontextprotocol.io/docs/concepts/sampling">샘플링을</a> 사용하면 MCP 서버가 클라이언트 측 LLM에 완료를 요청할 수 있습니다. 이는 서버가 요청에 응답만 하는 것이 아니라 클라이언트 모델에 응답을 생성하도록 요청할 수 있는 프로토콜의 양방향 특성을 추가합니다. 이를 통해 클라이언트는 비용, 보안 및 MCP 서버가 사용하는 모델을 완벽하게 제어할 수 있습니다. 따라서 사전 구성된 모델이 있는 외부 MCP 서버를 사용하는 경우 서버가 이미 클라이언트에 연결된 모델을 프롬프트하기만 하면 되므로 자체 API 키를 제공하거나 해당 모델에 대한 자체 구독을 구성할 필요가 없습니다. 이를 통해 보다 복잡하고 인터랙티브한 상담원 동작이 가능해집니다.</p><p><strong>루트 - 리소스에 대한 범위 지정 액세스: </strong><a href="https://modelcontextprotocol.io/docs/concepts/roots">루트는</a> 클라이언트가 집중할 관련 리소스 및 작업 공간에 대해 서버에 알릴 수 있는 방법을 제공하기 위해 설계되었습니다. 이는 서버가 작동하는 범위를 설정하는 데 유용합니다. 루트는 &quot;<a href="https://modelcontextprotocol.io/docs/concepts/roots#how-roots-work">정보 제공을 위한 것이지 엄격하게 강제하는</a>것은 아니다&quot;라는 점에 유의하세요. 즉, MCP 서버나 에이전트에 대한 자격이나 권한을 정의하지 않습니다. 즉, 서버나 에이전트가 특정 도구를 실행하거나 쓰기 작업을 수행하는 것을 막기 위해 루트에만 의존할 수 없습니다. 루트를 사용하면 사용자 승인 메커니즘을 통해 클라이언트 측에서 권한을 처리해야 합니다. 또한 개발자는 루트에 의해 설정된 경계를 존중하고 <a href="https://modelcontextprotocol.io/docs/concepts/roots#best-practices">모범 사례를</a> 사용하도록 설계된 서버를 사용해야 한다는 점을 염두에 두어야 합니다.</p><h2>상담원을 위한 인증: OAuth 2.1 및 보호된 메타데이터</h2><p>이 섹션에서는 안전하지 않은 플로우를 제거하고 모범 사례를 통합한 OAuth 2.0의 최신 버전인 OAuth 2.1에 중점을 둡니다.</p><p>특히 보안과 확장성이 MCP가 에이전트와 툴을 연결하는 표준이 되는 데 걸림돌이 되는 주요 장애물로 여겨지는 만큼 OAuth 지원은 매우 기대되는 주제였습니다. <a href="https://x.com/aaronpk">Aaron Parecki</a> (Okta의 OAuth 2.1 편집자이자 ID 표준 전문가)는 MCP가 서버 개발자의 복잡성을 대부분 덜어주는 깔끔하고 확장 가능한 OAuth 흐름을 채택하는 방법에 대해 설명했습니다. 공식 OAuth 2.1 인증 사양은 최근 <a href="https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization">6-18-2025에</a> 최신 프로토콜 개정판에 게시되었습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/2ea159116fe5e03ff800f077adf16d6ca9f1c1d1-1594x1280.png" style="width: 100%;" /><p>이 구현에서는 OAuth 책임을 MCP 클라이언트와 서버로 나눌 수 있습니다. 대부분의 인증 흐름은 MCP 클라이언트에서 시작되고 처리되며, 마지막에 서버가 보안 토큰을 수신하고 확인하는 역할만 수행합니다. 이러한 분할은 개발자가 모든 연결을 구성할 필요 없이 여러 도구에서 인증하는 방법의 중요한 확장 문제를 해결하는 데 도움이 되며, MCP 서버 개발자가 OAuth 전문가가 될 필요가 없도록 보장합니다.</p><p>강연의 두 가지 주요 하이라이트는 다음과 같습니다:</p><ol><li><a href="https://datatracker.ietf.org/doc/rfc9728/"><strong>보호된 리소스 메타데이터</strong></a>: MCP 서버는 목적, 엔드포인트 및 인증 방법을 설명하는 JSON 파일을 게시할 수 있습니다. 이를 통해 클라이언트는 서버 URL만으로 OAuth 플로우를 시작할 수 있으므로 연결 프로세스가 간소화됩니다. 자세히 알아보세요: <a href="https://aaronparecki.com/2025/04/03/15/oauth-for-model-context-protocol">MCP에서 OAuth 수정하기</a></li><li><a href="https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-13"><strong>IDP 및 SSO 지원</strong></a>: 기업은 ID 공급업체를 통합하여 중앙에서 액세스를 관리할 수 있습니다. 이는 사용자 경험과 보안 모두에 도움이 됩니다. 사용자는 10개의 서로 다른 동의 화면을 클릭할 필요가 없으며 보안팀은 각 연결에 대한 가시성을 확보할 수 있습니다.</li></ol><p>OAuth 로직을 클라이언트로 푸시하고 서버의 메타데이터에 의존함으로써 MCP 에코시스템은 큰 병목 현상을 피할 수 있습니다. 이를 통해 MCP는 오늘날 프로덕션 환경에서 최신 API를 보호하는 방식과 더욱 밀접하게 연계됩니다.</p><p>추가 읽기: <a href="https://aaronparecki.com/oauth-2-simplified/">OAuth 2 간소화</a>.</p><h2>컴포저블 에코시스템의 보안 과제</h2><p>새로운 개발에는 새로운 공격 표면도 수반됩니다. Cisco의 Arjun Sambamoorthy는 MCP 환경의 몇 가지 주요 위협을 다음과 같이 나열합니다:</p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">위협</td><td style="border: 1px solid #ccc; padding: 4px;">설명</td><td style="border: 1px solid #ccc; padding: 4px;">해결 방법 & 모범 사례</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">프롬프트 주입 & 공구 중독</td><td style="border: 1px solid #ccc; padding: 4px;">LLM 시스템 컨텍스트 또는 도구 설명에 악성 프롬프트를 삽입하여 LLM이 파일 읽기 또는 데이터 유출과 같은 의도하지 않은 작업을 수행하도록 하는 방법입니다.</td><td style="border: 1px solid #ccc; padding: 4px;">MCP 스캔과 같은 도구를 사용하여 도구 메타데이터를 확인합니다. 설명과 매개변수를 프롬프트에 포함하기 전에 유효성을 검사합니다. 마지막으로 고위험 도구에 대한 사용자 승인을 구현하는 것을 고려하세요. 자세한 내용은 표 아래의 추가 읽기 목록에서 OWASP 프롬프트 주입 가이드를 참조하세요.</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">샘플링 공격</td><td style="border: 1px solid #ccc; padding: 4px;">MCP의 맥락에서 샘플링은 MCP 서버가 LLM에 대한 즉각적인 인젝션 공격을 수행할 수 있는 문을 열어줍니다.</td><td style="border: 1px solid #ccc; padding: 4px;">신뢰할 수 없는 서버에 대한 샘플링을 사용하지 않도록 설정하고 샘플링 요청에 대한 인적 승인을 추가하는 것을 고려하세요.</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">악성 MCP 서버</td><td style="border: 1px solid #ccc; padding: 4px;">현재 MCP 서버 모음에서는 안전을 보장하기 위해 모든 서버를 일일이 검사하기 어렵습니다. 로그 서버는 사용자의 데이터를 은밀하게 수집하여 악의적인 공격자에게 노출시킬 수 있습니다.</td><td style="border: 1px solid #ccc; padding: 4px;">신뢰할 수 있는 레지스트리 또는 내부 목록의 MCP 서버에만 연결합니다. 샌드박싱을 사용하여 컨테이너에서 타사 서버를 실행하세요.</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">악성 MCP 설치 도구</td><td style="border: 1px solid #ccc; padding: 4px;">명령줄 설치 프로그램과 스크립트는 MCP 서버나 도구를 빠르게 구현하는 데 편리하지만, 확인되지 않은 손상된 코드가 설치될 수 있습니다.</td><td style="border: 1px solid #ccc; padding: 4px;">샌드박스 환경에 설치하고 패키지 서명의 유효성을 검사하세요. 확인되지 않은 출처의 자동 업데이트는 절대 하지 마세요.</td>
                    </tr>
              </tbody>
            </table>
          <p>이러한 문제를 해결하기 위해 Arjun은 모든 검증을 처리할 수 있는 신뢰할 수 있는 MCP 레지스트리(자세한 내용은 아래 읽기 목록의 상위 두 항목을 참조하세요)와 이 <a href="https://github.com/slowmist/MCP-Security-Checklist">보안 체크리스트를</a> 사용할 것을 제안합니다.</p><p>추가 읽기:</p><ul><li><a href="https://modelcontextprotocol.io/specification/2025-06-18/basic/security_best_practices">공식 MCP 보안 모범 사례</a></li><li><a href="https://owasp.org/www-project-top-10-for-large-language-model-applications/">OWASP LLM 애플리케이션 상위 10위</a></li><li><a href="https://hiddenlayer.com/innovation-hub/">히든레이어 위협 연구</a></li><li><a href="https://github.com/invariantlabs-ai/mcp-scan">MCP 스캔</a></li><li><a href="https://genai.owasp.org/llmrisk/llm01-prompt-injection/">OWASP 프롬프트 주입 가이드</a></li></ul><h2>다음 단계 레지스트리, 거버넌스 및 에코시스템</h2><p>중앙 집중식 MCP 레지스트리가 개발 중이며 이번 서밋에서 가장 지속적으로 논의된 주제 중 하나였습니다. 현재 서버 에코시스템은 파편화, 낮은 신뢰도 및 검색 가능성으로 어려움을 겪고 있습니다. 특히 메타데이터가 불완전하거나 스푸핑될 수 있는 탈중앙화된 생태계에서는 개발자가 MCP 서버를 찾고, 그 기능을 확인하고, 안전하게 설치하는 것이 어렵습니다.</p><p>중앙 집중식 레지스트리는 신뢰할 수 있는 정보 소스 역할을 하고 검색 가능성을 개선하며 서버 메타데이터의 무결성을 보장하고 악성 도구 설치 위험을 줄임으로써 이러한 문제점을 직접 해결합니다.</p><p>MCP 레지스트리의 목표는 다음과 같습니다:</p><ul><li>서버 메타데이터(서버의 기능, 인증, 설치 및 호출 방법)에 대한 단일 소스 제공</li><li>불완전한 타사 레지스트리와 파편화를 제거하여 서버가 등록을 원할 때 인터넷의 다른 모든 레지스트리를 업데이트할 필요가 없도록 합니다.</li><li>앞서 언급한 메타데이터가 포함된 CLI 도구와 server.json 파일을 포함하는 서버 등록 플로우를 제공합니다.</li></ul><p>더 큰 희망은 신뢰할 수 있는 레지스트리가 생태계를 안전하게 확장하여 개발자가 자신 있게 새로운 도구를 만들고 공유할 수 있도록 돕는 것입니다.</p><p>거버넌스는 앤트로픽이 가장 중요하게 생각하는 또 다른 문제였습니다. MCP는 개방적이고 커뮤니티 주도로 유지되어야 하지만 거버넌스 모델을 확장하는 것은 여전히 진행 중인 작업이라는 점을 분명히 했습니다. 현재 해당 분야에서 도움을 구하고 있으며, 오픈소스 프로토콜 거버넌스에 대한 경험이 있는 분이라면 누구든 연락해 달라고 요청하고 있습니다. 이것은 제가 언급하고 싶었던 다른 주제로 이어집니다. 행사 내내 연사들은 생태계는 내부 개발자들의 기여가 있어야만 성장할 수 있다고 강조했습니다. MCP를 새로운 웹 표준으로 만들고 다른 인기 있는 에이전트 프로토콜과 차별화하기 위해서는 집중적인 노력이 필요합니다.</p><h2>실제 세계에서의 MCP: 사례 연구 및 데모</h2><p>여러 조직에서 이미 MCP가 실제 애플리케이션에서 어떻게 사용되고 있는지 공유했습니다:</p><ul><li><strong>페이팔 - 에이전트 커머스를 위한 MCP 서버: </strong>PayPal은 사용자의 쇼핑 경험을 근본적으로 바꿀 수 있는 새로운 <a href="https://github.com/paypal/agent-toolkit/">에이전트 툴킷과</a> MCP 서버를 선보였습니다. 소셜 미디어를 뒤져 상품을 찾고, 가격을 비교하고, 결제하는 대신 PayPal MCP 서버에 연결된 상담원과 채팅하여 이러한 모든 작업을 처리할 수 있습니다.<br/></li><li><strong>EpicAI.pro - 자비스:</strong> MCP의 발전으로 실제 자비스형 비서가 점점 더 가까워지고 있습니다. 아이언맨 영화에 익숙하지 않은 분들을 위해 설명하자면, 자비스는 자연어를 사용하고, 다중 모드 입력에 응답하며, 응답 대기 시간이 없고, 사용자의 요구를 사전에 예측하고, 자동으로 통합을 관리하고, 기기와 위치 간에 컨텍스트 전환이 가능한 AI 어시스턴트입니다. 자비스를 실제 로봇 비서로 상상한다면, MCP는 자비스에게 &#x27;손&#x27; 또는 복잡한 작업을 처리할 수 있는 능력을 부여합니다.<br/></li><li><strong>Postman - <a href="https://www.postman.com/explore/mcp-generator">MCP 서버 생성기</a>: </strong>다양한 API 요청을 선택하고 장바구니에 담아 전체 장바구니를 MCP 서버로 다운로드할 수 있는 API 요청에 대한 장바구니 환경을 제공합니다.<br/></li><li><strong>블룸버그 - </strong>블룸버그는 엔터프라이즈 GenAI 개발의 주요 병목 현상을 해결했습니다. 약 10,000명의 엔지니어를 보유한 이 회사는 팀 전체에 걸쳐 도구와 에이전트를 통합할 수 있는 표준화된 방법이 필요했습니다. MCP를 통해 내부 툴을 상담원이 통합 인터페이스에서 쉽게 호출할 수 있는 원격 우선의 모듈식 구성 요소로 전환했습니다. 이를 통해 엔지니어는 조직 전체에 툴을 기여할 수 있었고, AI 팀은 사용자 지정 통합 대신 에이전트 구축에 집중할 수 있었습니다. 이제 블룸버그는 확장 가능하고 안전한 에이전트 워크플로우를 지원하여 MCP 에코시스템과의 완전한 상호운용성을 확보할 수 있습니다. 블룸버그는 어떤 공개 자료도 연결하지 않았지만, 정상 회담에서 공개적으로 발표한 내용은 이렇습니다.<br/></li><li><strong>블록 - </strong>블록은 MCP를 사용하여 직원들이 엔지니어링, 영업, 마케팅 등의 업무를 자동화할 수 있도록 지원하는 내부 AI 에이전트인 <a href="https://github.com/block/goose?tab=readme-ov-file">Goose를</a> 구동합니다. 이들은 매일 사용하는 시스템과 자연어 상호 작용을 지원하기 위해 Git, Snowflake, Jira 및 Google Workspace와 같은 도구를 위한 60개 이상의 MCP 서버를 구축했습니다. 이제 Block의 직원들은 코드를 작성하지 않고도 Goose를 사용하여 데이터 쿼리, 사기 탐지, 인시던트 관리, 내부 프로세스 탐색 등의 작업을 수행할 수 있습니다. MCP는 블록이 단 2개월 만에 여러 직무에 걸쳐 AI 도입을 확장할 수 있도록 지원했습니다.<br/></li><li><strong>AWS - <a href="https://github.com/awslabs/mcp">AWS MCP 서버</a>: </strong>AWS는 주사위 굴리기를 시뮬레이션하고, 과거 주사위 굴림을 추적하고, 스트리밍 가능한 HTTP를 사용하여 결과를 반환하는 재미있는 던전 앤 드래곤 테마의 MCP 서버를 선보였습니다. 이 경량 예시는 Lambda 및 Fargate와 같은 AWS 도구와 인프라를 사용하여 MCP 서버를 구축하고 배포하는 것이 얼마나 쉬운지를 강조합니다. 또한 MCP 서버와 상호 작용하는 멀티모달 에이전트를 구축하기 위한 오픈 소스 툴킷인 <a href="https://aws.amazon.com/blogs/opensource/introducing-strands-agents-an-open-source-ai-agents-sdk/">Strands SDK도</a> 소개했습니다.</li></ul><h2>Elastic 에이전트 빌더의 MCP 지원</h2><p>데이터 위에 직접 에이전트를 구축하는 가장 쉬운 방법인 <a href="https://www.elastic.co/kr/search-labs/blog/elastic-ai-agent-builder-context-engineering-introduction">Elastic 에이전트 빌더를</a> 사용해 지금 바로 MCP 실험을 시작할 수 있습니다. 에이전트 빌더를 사용하면 Elasticsearch 기반 도구를 MCP 호환 에이전트에 노출할 수 있으며, 다음과 같은 강력한 기본 제공 도구가 이미 포함되어 있습니다:</p><ul><li><code>platform.core.search</code> - 전체 Elasticsearch 쿼리 DSL을 사용하여 검색을 실행합니다.</li><li><code>platform.core.list_indices</code> - Elasticsearch 내에서 사용 가능한 모든 인덱스를 나열합니다(에이전트가 어떤 데이터가 존재하는지 검색하는 데 도움이 됨).</li><li><code>platform.core.get_index_mapping</code> - 특정 인덱스에 대한 필드 매핑을 검색합니다(상담원이 데이터의 형태와 유형을 이해하는 데 도움이 됨).</li><li><code>platform.core.get_document_by_id</code> - ID로 특정 문서 가져오기(정확한 검색을 위해)</li></ul><p>이러한 도구만 있으면 신뢰할 수 있는 AI 에이전트 구축의 핵심인 엔터프라이즈 수준의 검색 및 정확도로 에이전트를 무장시킬 수 있습니다.</p><p>에이전트 빌더를 더욱 강력하게 만드는 것은 애플리케이션의 요구 사항에 맞는 사용자 지정 도구를 정의하고 노출할 수 있는 기능입니다. 이는 상담원이 매번 해당 로직을 다시 발견할 필요 없이 특정 인덱스에 대해 특정 유형의 검색을 수행하도록 하려는 독단적이거나 반복적인 워크플로우에 특히 유용합니다. 동일한 결론에 도달하기 위해 계획과 추론에 토큰을 사용하는 대신, 그 의도를 도구에 직접 인코딩하여 에이전트가 더 빠르고 안정적이며 비용 효율적으로 작업할 수 있습니다.</p><p>다음은 에이전트 빌더 UI 내에서 ES|QL을 사용하는 사용자 지정 도구 정의의 예입니다:</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/c1f6405a374b707e8e6fa36b9e21db5f3c7cd127-1376x864.png" style="width: 100%;" /><p>사용자 지정 도구를 정의한 후에는 <code>Manage MCP</code> 드롭다운을 클릭하고 MCP 서버 URL을 복사하여 MCP를 사용하여 해당 도구(기본 제공 기본 도구 포함)를 노출할 수 있습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/9f39588525ca2643475de557ea54a6bcf5c150f6-1282x616.png" style="width: 100%;" /><p>이제 이 MCP 엔드포인트를 MCP를 사용하는 모든 클라이언트로 가져와서 에이전트 빌더에 연결하고 사용 가능한 모든 도구에 대한 액세스 권한을 부여할 수 있습니다. 자세한 내용은 <a href="https://www.elastic.co/kr/search-labs/blog/elastic-ai-agent-builder-context-engineering-introduction">상담원 빌더에</a> 대한 소개를 참조하세요.</p><h2>결론</h2><p>MCP 개발자 서밋은 MCP가 이러한 AI 에이전트가 서로 상호 작용하고 주변 데이터 세계와 상호 작용하는 방식을 형성하고 있음을 분명히 했습니다. 에이전트를 엔터프라이즈 데이터에 연결하든 완전 자율 에이전트를 설계하든 MCP는 표준화되고 구성 가능한 통합 방법을 제공하여 대규모로 빠르게 유용해지고 있습니다. 전송 프로토콜과 보안 패턴에서 레지스트리와 거버넌스에 이르기까지 MCP 생태계는 빠르게 성숙하고 있습니다. MCP는 계속해서 개방적이고 커뮤니티 주도로 운영될 예정이므로 지금 바로 개발자가 MCP의 진화를 주도할 수 있습니다.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[하이브리드 검색 재조명: Elasticsearch의 리니어 리트리버를 소개합니다!]]></title>
            <description><![CDATA[리니어 리트리버가 가중치 점수와 최소 최대 정규화를 활용하여 보다 정확하고 일관된 랭킹을 위해 하이브리드 검색을 향상시키는 방법을 알아보고 그 사용법을 알아보세요.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/linear-retriever-hybrid-search</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/linear-retriever-hybrid-search</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <pubDate>Wed, 28 May 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/7a07e27e3095463ff93b4cb7f8a0cf3b8e44eab0-1777x1000.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p><a href="https://www.elastic.co/kr/search-labs/blog/elasticsearch-retrievers-ga-8.16.0">이전 블로그</a> 게시물에서 복잡한 랭킹 파이프라인을 생성할 수 있도록 처음부터 다시 설계된 검색어 프레임워크를 소개했습니다. 또한 상호 순위 융합(RRF) 검색기가 서로 다른 쿼리의 결과를 병합하여 하이브리드 검색을 가능하게 하는 방법도 살펴봤습니다. RRF는 구현하기 쉽지만, 실제 점수를 무시하고 순전히 상대적인 순위에만 초점을 맞춘다는 한계가 있습니다. 따라서 미세 조정과 최적화가 어렵습니다.</p><h2>리니어 리트리버를 만나보세요!</h2><p>이 게시물에서는 하이브리드 검색을 지원하는 최신 기능인 <a href="https://www.elastic.co/kr/docs/solutions/search/retrievers-overview#retrievers-overview-types"><code>linear</code></a> <a href="https://www.elastic.co/kr/docs/solutions/search/retrievers-overview#retrievers-overview-types">리트리버를</a> 소개합니다! <code>rrf</code> 과 달리 <code>linear</code> 검색기는 문서와 일치하는 모든 쿼리에 대해 가중 합계를 계산합니다. 이 접근 방식은 결과 집합 내에서 각 문서의 상대적 중요도를 유지하면서 각 쿼리가 최종 점수에 미치는 영향을 정밀하게 제어할 수 있습니다. 그 결과, 하이브리드 검색을 보다 직관적이고 유연하게 미세 조정할 수 있는 방법을 제공합니다.</p><p>최종 점수가 계산될 리니어 리트리버를 정의합니다:</p><div class="katex">score = 5 * knn + 1.5 * bm25</div><p>간단합니다:</p><pre><code>GET linear_retriever_blog/_search
{
   "retriever": {
       "linear": {
           "retrievers": [
               {
                   "retriever": {
                       "knn": {
                          ...
                        }
                    },
                   "weight": 5
               },
                  {
                   "retriever": {
                       "standard": {
                          ...
                        }
                    },
                   "weight": 1.5
               },


           ]
        }
     }
}</code></pre><p>얼마나 간단하고 직관적인지 아시나요? (그리고 <code>rrf</code> 와 정말 비슷합니다!) 이 구성을 사용하면 상대 순위에만 의존하는 <code>rrf</code> 과 달리 각 쿼리 유형이 최종 순위에 기여하는 정도를 정밀하게 제어할 수 있습니다.</p><p>한 가지 주의할 점은 사용된 유사성 측정지표에 따라 <code>knn</code> 점수가 엄격하게 제한될 수 있다는 점입니다. 예를 들어 코사인 유사도 또는 단위 정규화된 벡터의 도트 곱을 사용하면 점수는 항상 <code>[0, 1]</code> 범위 내에 있습니다. 반면 <code>bm25</code> 점수는 예측 가능성이 낮고 범위가 명확하게 정의되어 있지 않습니다.</p><h2>점수 확장: kNN 대 BM25</h2><p>하이브리드 검색의 한 가지 문제점은 검색기마다 다른 척도로 점수를 산출한다는 점입니다. 예를 들어 다음 시나리오를 생각해 보세요:</p><p>쿼리 A 점수:</p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;">doc1</td><td style="border: 1px solid #ccc; padding: 4px;">doc2</td><td style="border: 1px solid #ccc; padding: 4px;">doc3</td><td style="border: 1px solid #ccc; padding: 4px;">doc4</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">knn</td><td style="border: 1px solid #ccc; padding: 4px;">0.347</td><td style="border: 1px solid #ccc; padding: 4px;">0.35</td><td style="border: 1px solid #ccc; padding: 4px;">0.348</td><td style="border: 1px solid #ccc; padding: 4px;">0.346</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">bm25</td><td style="border: 1px solid #ccc; padding: 4px;">100</td><td style="border: 1px solid #ccc; padding: 4px;">1.5</td><td style="border: 1px solid #ccc; padding: 4px;">1</td><td style="border: 1px solid #ccc; padding: 4px;">0.5</td>
                    </tr>
              </tbody>
            </table>
          <p>쿼리 B 점수:</p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;">doc1</td><td style="border: 1px solid #ccc; padding: 4px;">doc2</td><td style="border: 1px solid #ccc; padding: 4px;">doc3</td><td style="border: 1px solid #ccc; padding: 4px;">doc4</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">knn</td><td style="border: 1px solid #ccc; padding: 4px;">0.347</td><td style="border: 1px solid #ccc; padding: 4px;">0.35</td><td style="border: 1px solid #ccc; padding: 4px;">0.348</td><td style="border: 1px solid #ccc; padding: 4px;">0.346</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">bm25</td><td style="border: 1px solid #ccc; padding: 4px;">0.63</td><td style="border: 1px solid #ccc; padding: 4px;">0.01</td><td style="border: 1px solid #ccc; padding: 4px;">0.3</td><td style="border: 1px solid #ccc; padding: 4px;">0.4</td>
                    </tr>
              </tbody>
            </table>
          <p><code>kNN</code> 점수는 0에서 1 사이의 범위인 반면 <code>bm25</code> 점수는 매우 다양합니다. 이러한 차이로 인해 결과 결합을 위한 정적 최적 가중치를 설정하기가 까다롭습니다.</p><h2>구원의 정규화: MinMax 정규화 도구</h2><p>이 문제를 해결하기 위해 다음 공식을 사용하여 각 쿼리에 대해 독립적으로 점수를 <code>[0, 1]</code> 범위로 확장하는 <code>minmax</code> 정규화기(선택 사항)를 도입했습니다:</p><div class="katex">normalized_score = (score - min) / (max - min)</div><p>이렇게 하면 쿼리 결과 집합 내에서 각 문서의 상대적 중요도가 유지되므로 서로 다른 검색기의 점수를 쉽게 결합할 수 있습니다. 정규화를 사용하면 점수는 다음과 같이 됩니다:</p><p>쿼리 A 점수:</p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;">doc1</td><td style="border: 1px solid #ccc; padding: 4px;">doc2</td><td style="border: 1px solid #ccc; padding: 4px;">doc3</td><td style="border: 1px solid #ccc; padding: 4px;">doc4</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">knn</td><td style="border: 1px solid #ccc; padding: 4px;">0.347</td><td style="border: 1px solid #ccc; padding: 4px;">0.35</td><td style="border: 1px solid #ccc; padding: 4px;">0.348</td><td style="border: 1px solid #ccc; padding: 4px;">0.346</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">bm25</td><td style="border: 1px solid #ccc; padding: 4px;">1.00</td><td style="border: 1px solid #ccc; padding: 4px;">0.01</td><td style="border: 1px solid #ccc; padding: 4px;">0.005</td><td style="border: 1px solid #ccc; padding: 4px;">0.000</td>
                    </tr>
              </tbody>
            </table>
          <p>쿼리 B 점수:</p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;">doc1</td><td style="border: 1px solid #ccc; padding: 4px;">doc2</td><td style="border: 1px solid #ccc; padding: 4px;">doc3</td><td style="border: 1px solid #ccc; padding: 4px;">doc4</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">knn</td><td style="border: 1px solid #ccc; padding: 4px;">0.347</td><td style="border: 1px solid #ccc; padding: 4px;">0.35</td><td style="border: 1px solid #ccc; padding: 4px;">0.348</td><td style="border: 1px solid #ccc; padding: 4px;">0.346</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">bm25</td><td style="border: 1px solid #ccc; padding: 4px;">1.00</td><td style="border: 1px solid #ccc; padding: 4px;">0.000</td><td style="border: 1px solid #ccc; padding: 4px;">0.465</td><td style="border: 1px solid #ccc; padding: 4px;">0.645</td>
                    </tr>
              </tbody>
            </table>
          <p>이제 모든 점수는 <code>[0, 1]</code> 범위에 속하며, 절대 점수 대신 결과의 (쿼리 대비) 중요도를 파악하고 쿼리 간 일관성을 유지하므로 가중치 합계를 최적화하는 것이 훨씬 더 간단해졌습니다.</p><h2>리니어 리트리버 예시 </h2><p>이제 예제를 통해 위의 내용이 어떻게 보이는지, <code>linear</code> 리트리버가 <code>rrf</code> 의 몇 가지 단점을 어떻게 해결하는지 살펴보도록 하겠습니다. RRF는 상대적인 순위에만 의존하며 실제 점수 차이는 고려하지 않습니다. 예를 들어 다음과 같은 점수가 주어집니다:</p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;">doc1</td><td style="border: 1px solid #ccc; padding: 4px;">doc2</td><td style="border: 1px solid #ccc; padding: 4px;">doc3</td><td style="border: 1px solid #ccc; padding: 4px;">doc4</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">knn</td><td style="border: 1px solid #ccc; padding: 4px;">0.347</td><td style="border: 1px solid #ccc; padding: 4px;">0.35</td><td style="border: 1px solid #ccc; padding: 4px;">0.348</td><td style="border: 1px solid #ccc; padding: 4px;">0.346</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">bm25</td><td style="border: 1px solid #ccc; padding: 4px;">100</td><td style="border: 1px solid #ccc; padding: 4px;">1.5</td><td style="border: 1px solid #ccc; padding: 4px;">1</td><td style="border: 1px solid #ccc; padding: 4px;">0.5</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">RRF 점수</td><td style="border: 1px solid #ccc; padding: 4px;">0.03226</td><td style="border: 1px solid #ccc; padding: 4px;">0.03252</td><td style="border: 1px solid #ccc; padding: 4px;">0.03200</td><td style="border: 1px solid #ccc; padding: 4px;">0.03125</td>
                    </tr>
              </tbody>
            </table>
          <p>rrf는 문서의 순위를 매깁니다:</p><div class="katex">doc2 > doc1 > doc3 > doc4</div><p>그러나 doc1은 다른 문서보다 <code>bm25</code> 점수가 훨씬 높은데, <code>rrf</code> 은 상대적인 순위만 보기 때문에 이를 포착하지 못합니다. 정규화와 결합된 <code>linear</code> 리트리버는 점수와 그 차이를 모두 정확하게 설명하여 보다 의미 있는 순위를 생성합니다:</p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;">doc1</td><td style="border: 1px solid #ccc; padding: 4px;">doc2</td><td style="border: 1px solid #ccc; padding: 4px;">doc3</td><td style="border: 1px solid #ccc; padding: 4px;">doc4</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">knn</td><td style="border: 1px solid #ccc; padding: 4px;">0.347</td><td style="border: 1px solid #ccc; padding: 4px;">0.35</td><td style="border: 1px solid #ccc; padding: 4px;">0.348</td><td style="border: 1px solid #ccc; padding: 4px;">0.346</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">bm25</td><td style="border: 1px solid #ccc; padding: 4px;">1</td><td style="border: 1px solid #ccc; padding: 4px;">0.01</td><td style="border: 1px solid #ccc; padding: 4px;">0.005</td><td style="border: 1px solid #ccc; padding: 4px;">0</td>
                    </tr>
              </tbody>
            </table>
          <p>위에서 볼 수 있듯이 doc1의 높은 순위와 <code>score</code> 의 <code>bm25</code> 이 적절히 고려되어 최종 점수에 반영되었습니다. 또한 모든 점수는 이제 <code>[0, 1]</code> 범위 내에 있으므로 훨씬 더 직관적인 방식으로 비교하고 결합할 수 있으며 오프라인 최적화 프로세스도 구축할 수 있습니다.</p><h2>모든 것을 종합하기</h2><p>정규화를 통해 <code>linear</code> 검색기를 최대한 활용하려면 검색 요청은 다음과 같이 표시되어야 합니다:</p><pre><code>GET linear_retriever_blog/_search
{
   "retriever": {
       "linear": {
           "retrievers": [
               {
                   "retriever": {
                       "knn": {
                          ...
                        }
                    },
                   "weight": 5
               },
                  {
                   "retriever": {
                       "standard": {
                          ...
                        }
                    },
                   "weight": 1.5,
                   "normalizer": "minmax"
               },


           ]
       }
   }
}</code></pre><p>이 접근 방식은 <code>linear</code> 리트리버의 유연성과 직관적인 채점 방식을 유지하면서 MinMax 정규화를 통해 일관된 점수 확장을 보장하는 두 가지 장점을 결합한 것입니다.</p><p>모든 리트리버와 마찬가지로 <code>linear</code> 리트리버는 설명 기능, 일치 항목 강조 표시, 필드 축소 등을 지원하여 계층적 리트리버 트리의 모든 레벨에 통합할 수 있습니다.</p><h2>리니어 리트리버를 선택해야 하는 시기 및 리니어 리트리버가 차이를 만드는 이유</h2><p><code>linear</code> 리트리버:</p><ul><li>단순한 순위가 아닌 실제 점수를 활용하여 상대적 중요도를 유지합니다.</li><li>다양한 쿼리의 가중치 기여도를 사용하여 미세 조정할 수 있습니다.</li><li>정규화를 사용하여 일관성을 향상시켜 하이브리드 검색을 더욱 강력하고 예측 가능하게 만듭니다.</li></ul><h2>결론</h2><p><code>linear</code> 리트리버는 이미 Elasticsearch 서버리스와 8.18 및 9.0 릴리즈에서 사용할 수 있습니다! 더 많은 예제와 구성 매개변수는 문서에서도 확인할 수 있습니다. 직접 사용해보고 하이브리드 검색 환경을 개선하는 방법을 알아보세요. 여러분의 피드백을 기다리겠습니다. 즐거운 검색 되세요!</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Quepid를 사용하여 판정 목록을 생성하기]]></title>
            <description><![CDATA[Quepid에서 협업적인 인간 평가자 프로세스를 사용해 평가 목록을 만들고, 벤치마크를 사용하여 관련성을 조정하는 방법을 알아보세요.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/quepid-judgement-lists</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/quepid-judgement-lists</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Daniel Wrigley]]></dc:creator>
            <pubDate>Mon, 26 May 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/307c1774bd31f92bb4aa7b69e1a6796240465100-1600x914.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p><a href="https://www.elastic.co/search-labs/blog/judgment-lists">판단 목록을</a> 만드는 것은 검색 결과 품질을 최적화하는 데 중요한 단계이지만 복잡하고 어려운 작업일 수 있습니다. 판단 목록은 해당 결과에 대한 관련성 등급과 짝을 이루는 큐레이션된 검색어 집합으로, 테스트 컬렉션이라고도 합니다. 이 목록을 사용하여 계산된 메트릭은 검색 엔진의 성능을 측정하는 벤치마크 역할을 합니다. 판단 목록을 만드는 프로세스를 간소화하기 위해 <a href="https://opensourceconnections.com/">오픈소스 커넥션</a> 팀은 <a href="https://quepidapp.com/">Quepid를</a> 개발했습니다. 판단은 명시적일 수도 있고 사용자의 암묵적인 피드백을 기반으로 할 수도 있습니다. 이 블로그에서는 모든 평가 목록의 기초가 되는 명시적 평가를 효과적으로 수행할 수 있도록 큐피드에서 공동 작업 환경을 설정하는 방법을 안내합니다.</p><p>Quepid는 검색 품질 평가 프로세스에서 검색 팀을 지원합니다:</p><ul><li>쿼리 집합 구축</li><li>판단 목록 만들기</li><li>검색 품질 지표 계산</li><li>계산된 검색 품질 지표를 기반으로 다양한 검색 알고리즘/랭커를 비교하세요.</li></ul><p>블로그에서는 영화 대여점을 운영하며 검색 결과 품질을 개선하는 것이 목표라고 가정해 보겠습니다.</p><h2>필수 구성 요소</h2><p>이 블로그에서는 <a href="https://github.com/o19s/es-tmdb">es-tmdb 리포지토리의</a> 데이터와 매핑을 사용합니다. 데이터는 <a href="https://www.themoviedb.org/">영화 데이터베이스에서</a> 가져온 것입니다. 따라 하려면 매핑을 사용하여 tmdb라는 인덱스를 설정하고 데이터를 인덱싱하세요. 이를 위해 로컬 인스턴스를 설정하든 Elastic Cloud 배포를 사용하든 상관없으며 둘 다 잘 작동합니다. 이 블로그에서는 Elastic Cloud 배포를 가정합니다. 데이터 인덱싱 방법에 대한 정보는 <a href="https://github.com/o19s/es-tmdb/blob/master/README.md">es-tmdb 리포지토리의 README에서</a> 찾을 수 있습니다.</p><p>제목 필드에서 <code>rocky</code> 에 대한 간단한 일치 쿼리를 수행하여 검색할 데이터가 있는지 확인합니다:</p><pre><code>GET tmdb/_search
{
 "query": {
   "match": {
     "title": "rocky"
   }
 }
}</code></pre><p>8개의 결과가 표시되어야 합니다.</p><pre><code>{
 "took": 2,
 "timed_out": false,
 "_shards": {
   "total": 1,
   "successful": 1,
   "skipped": 0,
   "failed": 0
 },
 "hits": {
   "total": {
     "value": 8,
     "relation": "eq"
   }
…
}</code></pre><h2>Quepid에 로그인</h2><p><a href="https://github.com/o19s/quepid">Quepid는</a> 사용자가 검색 결과 품질을 측정하고 이를 개선하기 위해 오프라인 실험을 실행할 수 있는 도구입니다.</p><p>공개적으로 사용 가능한 무료 호스팅 버전( <a href="https://app.quepid.com">https://app.quepid.com</a>)을 사용하거나, 두 가지 방법으로 Quepid를 사용할 수 있습니다, 를 클릭하거나 액세스 권한이 있는 컴퓨터에서 Quepid를 설정하세요. 이 게시물은 무료 호스팅 버전을 사용한다고 가정합니다. 사용자 환경에서 Quepid 인스턴스를 설정하려면 <a href="https://github.com/o19s/quepid/wiki/Installation-Guide">설치 가이드를</a> 따르세요.</p><p>어떤 설정을 선택하든 아직 계정이 없는 경우 계정을 만들어야 합니다.</p><h2>Quepid 케이스를 설정하는 방법</h2><p>Quepid는 &quot;케이스를 중심으로 구성됩니다.&quot; 케이스는 연관성 튜닝 설정 및 검색 엔진과의 연결 설정 방법과 함께 쿼리를 저장합니다.</p><ul><li>처음 사용하는 경우 첫 번째 <strong>관련성 사례 만들기를</strong> 선택합니다.</li><li>복귀 사용자는 최상위 메뉴에서 <strong>관련성 사례를</strong> 선택하고 <strong>+ 사례 만들기를</strong> 클릭할 수 있습니다.</li></ul><p>예를 들어, &quot;영화 검색 기준,&quot; 기준 검색을 측정하고 개선하려는 경우와 같이 사례의 이름을 설명적으로 지정합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/8f9e480d9aae0d706cfc5371e41f19c706dd452a-594x251.png" style="width: 100%;" /><p><strong>계속을</strong> 선택하여 이름을 확인합니다.</p><p>다음으로 Quepid에서 검색 엔진으로 연결을 설정합니다. Quepid는 Elasticsearch를 비롯한 다양한 검색 엔진에 연결할 수 있습니다.</p><p>구성은 Elasticsearch 및 Quepid 설정에 따라 달라집니다. Quepid를 Elastic Cloud 배포에 연결하려면, Elastic Cloud 배포에 대해 CORS를 활성화 및 구성하고 API 키를 준비해야 합니다. 자세한 지침은 <a href="https://quepid-docs.dev.o19s.com/2/quepid/49/how-to-connect-quepid-to-elastic-cloud">Quepid 문서에 있는 해당 방법에</a> 나와 있습니다.</p><p>Elasticsearch 엔드포인트 정보(<code>https://YOUR_ES_HOST:PORT/tmdb/_search</code>)와 연결에 필요한 추가 정보( <strong>고급</strong> 구성 옵션의 Elastic Cloud 배포의 경우 API 키)를 입력하고, <strong>ping을</strong> 클릭하여 연결을 테스트한 후 <strong>계속을</strong> 선택하여 다음 단계로 이동합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/267339dfaecae2740eb2ee2739bdc971608bdb5f-588x1169.png" style="width: 100%;" /><p>이제 케이스에 표시할 필드를 정의합니다. 나중에 인간 평가자가 특정 쿼리에 대한 문서의 관련성을 평가하는 데 도움이 되는 모든 항목을 선택합니다.</p><p><code>title</code> 을 <em>제목 필드로</em> 설정하고 <code>_id</code> 을 <em>ID 필드로</em> 그대로 둔 다음 <code>overview, tagline, cast, vote_average, thumb:poster_path</code> 을 <em>추가 표시 필드에 추가합니다</em>. 마지막 항목은 결과에서 영화의 작은 썸네일 이미지를 표시하여 우리와 인간 평가자를 시각적으로 안내합니다.</p><p><strong>계속</strong> 버튼을 선택하여 표시 설정을 확인합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/02c58aae8c2ebb6d31f538b27462b4c65428fdc3-594x493.png" style="width: 100%;" /><p>마지막 단계는 케이스에 검색어를 추가하는 것입니다. 입력란에 <em>스타워즈</em>, <em>해리슨 포드</em>, <em>최고의 액션 영화라는</em> 세 가지 검색어를 하나씩 추가하고 <strong>계속을</strong> 클릭합니다.</p><p>케이스에는 실제 사용자 쿼리를 나타내고 다양한 유형의 쿼리를 설명하는 쿼리가 포함되어 있는 것이 이상적입니다. 현재로서는 <em>스타워즈는</em> 영화 제목에 대한 모든 쿼리를, <em>해리슨 포드는</em> 출연진에 대한 모든 쿼리를, <em>최고의 액션 영화는</em> 특정 장르의 영화를 검색하는 모든 쿼리를 나타내는 쿼리라고 상상할 수 있습니다. 이를 일반적으로 쿼리 집합이라고 합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/baccfe96766319aa7255e9bff08913ac87d1517f-595x326.png" style="width: 100%;" /><p>프로덕션 시나리오에서는 <a href="https://opensourceconnections.com/blog/2022/10/13/how-to-succeed-with-explicit-relevance-evaluation-using-probability-proportional-to-size-sampling/">확률 비례 크기 샘플링과</a> 같은 통계 기법을 적용하여 이벤트 추적 데이터에서 쿼리를 샘플링하고, 이렇게 샘플링된 쿼리를 Quepid로 가져와 빈도에 따라 헤드(빈번한 쿼리)와 테일(드문 쿼리)의 쿼리를 포함시킵니다. 즉, 빈번하지 않은 쿼리는 제외하지 않고 빈번한 쿼리에 편향성을 부여합니다.</p><p>마지막으로 <strong>마침을</strong> 선택하면 정의된 세 가지 쿼리가 표시되는 대소문자 인터페이스로 이동합니다.</p><h2>쿼리 및 정보 요구 사항</h2><p>판단 목록이라는 중요한 목표에 도달하기 위해서는 인간 평가자가 주어진 쿼리에 대한 검색 결과(일반적으로 문서)를 판단해야 합니다. 이를 쿼리/문서 쌍이라고 합니다.</p><p>때로는 쿼리를 보면 사용자가 원하는 것이 무엇인지 쉽게 알 수 있습니다. 쿼리( <code>harrison ford</code> )의 의도는 배우 해리슨 포드가 출연한 영화를 찾는 것입니다. <code>action</code>? 사용자의 의도가 액션 장르에 속하는 영화를 찾는 것이라고 말하고 싶을 수도 있습니다. 하지만 어떤 것일까요? 가장 최근의 것, 가장 인기 있는 것, 사용자 평가에 따른 최고의 것? 아니면 사용자가 &#x27;액션&#x27;이라는 제목의 모든 영화를 찾고 싶을까요? <a href="https://www.themoviedb.org/search/movie?query=Action">영화 데이터베이스에는 &#x27;액션&#x27;이라는 제목의 영화가 최소 12개(!)</a> 있으며, 제목에 붙은 느낌표의 개수가 주로 다릅니다.</p><p>의도가 불분명한 쿼리에 대해 두 사람의 평가자가 해석을 달리할 수 있습니다. 정보 필요를 입력합니다: <a href="https://en.wikipedia.org/wiki/Information_needs">정보</a> 욕구란 정보에 대한 의식적 또는 무의식적 욕구를 말합니다. 정보 요구 사항을 정의하면 인간 평가자가 쿼리에 대한 문서를 판단하는 데 도움이 되므로 판단 목록을 작성하는 과정에서 중요한 역할을 합니다. 전문 사용자 또는 주제별 전문가가 정보 요구 사항을 지정하는 데 적합합니다. 검색 결과가 충족해야 하는 것은 사용자의 요구이므로 사용자의 관점에서 정보 요구사항을 정의하는 것이 좋습니다.</p><p>&#x27;영화 검색 기준&#x27; 사례의 쿼리에 필요한 정보입니다:</p><ol><li><strong>스타워즈</strong>: 사용자가 스타워즈 프랜차이즈의 영화나 프로그램을 찾고자 합니다. 스타워즈에 관한 다큐멘터리가 관련성이 있을 수 있습니다.</li><li><strong>해리슨 포드</strong>: 사용자가 배우 해리슨 포드가 출연한 영화를 찾고자 합니다. 해리슨 포드가 내레이터와 같은 다른 역할을 맡은 영화도 관련성이 있을 수 있습니다.</li><li><strong>최고의 액션 영화</strong>: 사용자는 액션 영화, 가급적이면 사용자 평균 투표 수가 높은 영화를 찾고 싶어합니다.</li></ol><h2>Quepid에서 정보 요구를 정의하는 방법</h2><p>Quepid에서 정보 요구 사항을 정의하려면 케이스 인터페이스에 액세스하세요:</p><p>1. 쿼리(예:<em>스타 워즈</em>)를 열고 <em>메모 토글을</em>선택합니다.</p><p>2. 첫 번째 필드에 필요한 정보를 입력하고 두 번째 필드에 추가 메모를 입력합니다:</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/e01d3d5242a350d8797faa665eb3170039f5dfa2-1483x559.png" style="width: 100%;" /><p>3. <strong>저장을</strong> 클릭합니다.</p><p>소수의 쿼리의 경우 이 프로세스를 사용해도 괜찮습니다. 그러나 사례를 3개에서 100개 쿼리로 확장하는 경우(Quepid 사례는 보통 50~100개 쿼리 범위인 경우가 많습니다.) Quepid 외부(예: 스프레드시트)에서 정보 요구 사항을 정의한 다음 <strong>가져오기를</strong> 통해 업로드하고 <strong>정보 요구 사항을</strong> 선택해야 할 수 있습니다.</p><h2>Quepid에서 팀을 만들고 사례 공유하기</h2><p>협업적 판단은 관련성 평가의 품질을 향상시킵니다. 팀을 설정하려면 다음과 같이 하세요:</p><p>1. 1. 최상위 메뉴에서 <strong>Teams로</strong> 이동합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/797706e8d130b474a95d30b6fa22ecaf36f98c03-613x58.png" style="width: 100%;" /><p>2. <strong>새로 추가를</strong> 클릭하고 팀 이름(예: &quot;검색 관련성 평가자&quot;)을 입력한 다음 <strong>만들기를</strong> 클릭합니다.</p><p>3. 이메일 주소를 입력하고 <strong>사용자 추가를</strong> 클릭하여 구성원을 추가합니다.</p><p>4. 케이스 인터페이스에서 <strong>케이스 공유를</strong> 선택합니다.</p><p>5. 적절한 팀을 선택하고 확인합니다.</p><h2>Quepid에서 판단 목록 만들기</h2><p>Quepid의 Book을 사용하면 여러 평가자가 쿼리/문서 쌍을 체계적으로 평가할 수 있습니다. 생성하려면</p><p>1. 사건 인터페이스에서 <strong>판결로</strong> 이동하여 <strong>+ 책 만들기를</strong> 클릭합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/6a077f26225961150b7414463d7db04f090b68d6-896x365.png" style="width: 100%;" /><p>2. 설명이 포함된 이름으로 책을 구성하고, 팀에 할당하고, 채점 방법(예: DCG@10)을 선택하고, 채점 전략(단일 또는 복수 채점자)을 설정합니다. 책에 대해 다음 설정을 사용합니다:</p><ul><li><strong>이름</strong>: &quot;영화 검색 0-3 스케일&quot;</li><li><strong>이 책을 공유할 팀</strong>: 만든 팀과 함께 확인란을 선택합니다.</li><li><strong>득점자</strong>: DCG@10</li></ul><p>3. 3 <strong>. 책 만들기를 클릭</strong>합니다 .</p><p>이름은 설명적이며 검색 대상(&#x27;영화&#x27;)에 대한 정보와 심사 점수(&#x27;0~3&#x27;)를 포함합니다. 선택한 점수 DCG@10은 검색 지표가 계산되는 방식을 정의합니다. &#x27;DCG&#x27;는 <a href="https://en.wikipedia.org/wiki/Discounted_cumulative_gain">할인 누적</a> 수익의 약자이며 &#x27;@10&#x27;은 메트릭을 계산할 때 고려되는 상위 결과의 수입니다.</p><p>이 경우 정보 획득을 측정하고 이를 포지션 가중치와 결합하는 메트릭을 사용하고 있습니다. 사용 사례에 더 적합한 다른 검색 지표가 있을 수 있으며 올바른 지표를 <a href="https://opensourceconnections.com/blog/2020/02/28/choosing-your-search-relevance-metric">선택하는 것 자체가</a> 어려운 일입니다.</p><h2>쿼리/문서 쌍으로 목록 채우기</h2><p>연관성 평가를 위해 쿼리/문서 쌍을 추가하려면 다음 단계를 따르세요:</p><p>1. 케이스 인터페이스에서 &quot;판정으로 이동합니다.&quot;</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/d717c5b06ae6cb42ed2b9e771486a12f738a9890-1041x218.png" style="width: 100%;" /><p>2. 생성한 책을 선택합니다.</p><p>3. &quot; 책 채우기&quot; 를 클릭하고 &quot;책에 대한 쿼리/문서 쌍 새로 고침을 선택하여 확인합니다.&quot;</p><p>이 작업은 각 쿼리에 대한 상위 검색 결과를 기반으로 쌍을 생성하여 팀에서 평가할 수 있도록 준비합니다.</p><h2>인간 평가자 팀이 판단하도록 하세요. </h2><p>지금까지 완료된 단계는 상당히 기술적이고 관리적인 작업이었습니다. 이 필수적인 준비가 완료되었으니 이제 심사위원단에게 맡기면 됩니다. 기본적으로 심사위원의 임무는 주어진 쿼리에 대한 특정 문서의 관련성을 평가하는 것입니다. 이 프로세스의 결과는 판정된 쿼리 문서 쌍에 대한 모든 관련성 레이블이 포함된 판정 목록입니다. 다음으로 이 프로세스와 이에 대한 인터페이스에 대해 자세히 설명합니다.</p><h3>Human Rating 인터페이스 개요</h3><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/be3016091b49655dab3354d84e6dc638f3468390-1283x664.png" style="width: 100%;" /><p>Quepid의 인간 평가 인터페이스는 효율적인 평가를 위해 설계되었습니다.</p><ul><li><strong>쿼리:</strong> 검색어를 표시합니다.</li><li><strong>정보 필요:</strong> 사용자의 의도를 표시합니다.</li><li><strong>채점 가이드라인:</strong> 일관된 평가를 위한 지침을 제공합니다.</li><li><strong>문서 메타데이터:</strong> 문서에 대한 관련 세부 정보를 표시합니다.</li><li><strong>평가 버튼:</strong> 평가자가 해당 키보드 단축키를 사용하여 평가를 할당할 수 있습니다.</li></ul><h3>인간 평가 인터페이스 사용하기</h3><p>인간 평가자로서 저는 책 개요를 통해 인터페이스에 액세스합니다:</p><p>1. 케이스 인터페이스로 이동하여 <strong>판결을</strong> 클릭합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/d717c5b06ae6cb42ed2b9e771486a12f738a9890-1041x218.png" style="width: 100%;" /><p>2. <strong>더 많은 판단이 필요합니다!</strong> 를 클릭합니다.</p><p>시스템은 아직 평가되지 않았으며 추가 판단이 필요한 쿼리/문서 쌍을 표시합니다. 이는 도서의 선택 전략에 따라 결정됩니다:</p><ul><li><em>단일 평가자</em>: 쿼리/문서 쌍당 단일 평가.</li><li><em>여러 평가자</em>: 쿼리/문서 쌍당 최대 3개의 평가.</li></ul><h3>평가 쿼리/문서 쌍</h3><p>몇 가지 예를 살펴보겠습니다. 이 가이드를 따라가다 보면 다른 영화가 표시될 가능성이 높습니다. 그러나 평점 원칙은 동일하게 유지됩니다.</p><p>첫 번째 예는 영화 &#x27; <em>해리슨 포드</em>&#x27;에 대한 쿼리 &#x27;히어로즈&#x27;입니다:</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/c1e14b0d8b04dd579471932dbe4ff72ae5692a02-981x571.png" style="width: 100%;" /><p>먼저 쿼리를 살펴본 다음 필요한 정보를 확인한 다음 주어진 메타데이터를 기반으로 영화를 판단합니다.</p><p>이 영화는 해리슨 포드가 출연하기 때문에 검색어와 관련된 결과입니다. 주관적으로 최신 영화가 더 관련성이 높다고 생각할 수 있지만 이는 정보 요구 사항의 일부가 아닙니다. 따라서 이 문서의 등급은 3점 만점에 3점인 &#x27;완벽&#x27;으로 평가합니다.</p><p>다음 예는 영화 &#x27;포드 대 페라리&#x27;에 대한 쿼리 &#x27; <em>해리슨 포드</em>&#x27;입니다:</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/052af7894506d7a765af156ba8e26ceec3559973-981x789.png" style="width: 100%;" /><p>동일한 관행에 따라 쿼리, 정보 요구 사항, 그리고 문서의 메타데이터가 정보 요구 사항과 얼마나 잘 일치하는지 살펴봄으로써 이 쿼리/문서를 판단합니다.</p><p>이는 좋지 않은 결과입니다. 이 결과는 검색어 중 하나인 &#x27;ford&#x27;가 제목과 일치하기 때문일 수 있습니다. 하지만 해리슨 포드는 이 영화에서 다른 어떤 역할도 맡지 않았습니다. 따라서 이 문서의 등급은 0점인 &#x27;미흡&#x27;으로 평가합니다.</p><p>세 번째 예는 &#x27; <em>최고의 액션 영화</em>&#x27;라는 쿼리에 대한 영화 &#x27;액션 잭슨&#x27;입니다:</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/247ab862fbc7435537709f8c96619cb331133d09-985x606.png" style="width: 100%;" /><p>액션 영화처럼 보이므로 정보에 대한 요구는 적어도 부분적으로 충족됩니다. 그러나 투표 평균은 10점 만점에 5.4점입니다. 그래서 이 영화는 저희 컬렉션에서 최고의 액션 영화가 아닐 수도 있습니다. 따라서 심사위원으로서 저는 이 문서를 등급 척도에서 1점인 &#x27;보통&#x27;으로 평가합니다.</p><p>이 예는 특히 쿼리/문서 쌍을 Quepid로 평가하는 프로세스를 높은 수준에서 그리고 일반적으로 설명합니다.</p><h2>인간 평가자의 모범 사례</h2><p>표시된 예시를 보면 명확한 판단을 내리는 것이 간단해 보일 수 있습니다. 하지만 신뢰할 수 있는 인적 평가 프로그램을 구축하는 것은 쉬운 일이 아닙니다. 이 과정은 데이터의 품질을 쉽게 손상시킬 수 있는 문제들로 가득 차 있습니다:</p><ul><li>인간 평가자는 반복적인 작업으로 인해 피로를 느낄 수 있습니다.</li><li>개인적인 취향에 따라 판단이 왜곡될 수 있습니다.</li><li>도메인 전문 지식의 수준은 판사마다 다릅니다.</li><li>평가자는 종종 여러 가지 책임을 맡고 있습니다.</li><li>문서의 인지된 관련성이 쿼리에 대한 실제 관련성과 일치하지 않을 수 있습니다.</li></ul><p>이러한 요인으로 인해 일관성이 없고 품질이 낮은 판단이 내려질 수 있습니다. 하지만 걱정하지 마세요. 이러한 문제를 최소화하고 보다 강력하고 신뢰할 수 있는 평가 프로세스를 구축하는 데 도움이 되는 입증된 모범 사례가 있습니다:</p><ul><li><strong>일관된 평가:</strong> 쿼리, 정보 요구 사항, 문서 메타데이터를 순서대로 검토하세요.</li><li><strong>가이드라인을 참조하십시오:</strong> 채점 가이드라인을 사용하여 일관성을 유지하세요. 채점 가이드라인에는 언제 어떤 등급을 적용할지 예시를 들어 심사 과정을 설명할 수 있습니다. 첫 번째 판정 후 인간 평가자들과 체크인하는 것은 까다로운 엣지 사례와 추가 지원이 필요한 부분을 파악하는 데 좋은 관행으로 입증되었습니다.</li><li><strong>옵션을 활용합니다:</strong> 확실하지 않은 경우 &quot;나중에 판단하겠습니다&quot; 또는 &quot;알 수 없음,&quot; 필요한 경우 설명을 제공합니다.</li><li><strong>휴식을 취하세요:</strong> 규칙적인 휴식은 판단력을 유지하는 데 도움이 됩니다. 퀘피드는 인간 평가자가 심사를 마칠 때마다 색종이를 터뜨려 규칙적인 휴식을 취하도록 도와줍니다.</li></ul><p>이러한 단계를 수행하면 Quepid에서 판단 목록을 만드는 체계적이고 협업적인 접근 방식을 구축하여 검색 연관성 최적화 노력의 효율성을 높일 수 있습니다.</p><h2>다음 단계</h2><p>이제 어디로 가야 하나요? 판단 목록은 검색 결과 품질을 개선하기 위한 하나의 기초 단계에 불과합니다. 다음 단계는 다음과 같습니다:</p><h3>메트릭 계산 및 실험 시작</h3><p>판단 목록을 사용할 수 있게 되면 판단을 활용하고 <a href="https://opensourceconnections.com/blog/2020/02/28/choosing-your-search-relevance-metric/">검색 품질 지표를</a> 계산하는 것은 자연스러운 과정입니다. 판단이 가능한 경우 현재 사례에 대해 구성된 메트릭을 자동으로 계산합니다. 지표는 &#x27;채점자&#x27;로 구현되며, 지원되는 지표에 원하는 지표가 포함되지 않은 경우 직접 지표를 제공할 수 있습니다!</p><p>케이스 인터페이스로 이동하여 <strong>득점자 선택으로</strong> 이동하고 <em>DCG@10을</em> 선택한 다음 <strong>득점자 선택을</strong> 클릭하여 확인합니다. 이제 Quepid는 쿼리당 DCG@10을 계산하고 전체 쿼리의 평균을 계산하여 케이스의 검색 결과 품질을 정량화합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/0ff3b9918342071744d681a43d542102e927abd3-1163x551.png" style="width: 100%;" /><p>이제 검색 결과 품질이 정량화되었으므로 첫 번째 실험을 실행할 수 있습니다. 실험은 가설을 세우는 것에서 시작됩니다. 스크린샷의 세 가지 검색어를 평가한 후 살펴보면 검색 품질 지표 측면에서 세 검색어의 실적이 매우 다르다는 것을 알 수 있습니다. <em>&#x27;스타워즈&#x27;</em> 는 꽤 잘 수행되고 &#x27; <em>해리슨 포드</em> &#x27;도 괜찮아 보이지만 가장 큰 잠재력은 &#x27; <em>최고의 액션 영화</em>&#x27;에 있습니다.</p><p>이 쿼리를 확장하면 결과를 확인할 수 있으며, 문서가 일치하는 이유와 점수에 영향을 미치는 요인을 자세히 살펴볼 수 있습니다:</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/c45c6cae085a492198c0f8b7060a1a7204e3724e-1131x691.png" style="width: 100%;" /><p>&quot;쿼리 설명&quot;을 클릭하고 &quot;구문 분석&quot; 탭에 들어가면 쿼리가 <em>캐스팅</em>, <em>개요</em> 및 <em>제목의</em> 세 가지 필드에서 검색하는 DisjunctionMaxxQuery임을 확인할 수 있습니다:</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/4a1e1bb2a9cd28e9c48e0ba16357d17ed9d3a5cf-894x557.png" style="width: 100%;" /><p>일반적으로 검색 엔지니어는 검색 플랫폼에 대한 몇 가지 도메인별 정보를 알고 있습니다. 이 경우 <em>장르</em> 필드가 있다는 것을 알 수 있습니다. 이를 쿼리에 추가하여 검색 품질이 개선되는지 확인해 보겠습니다.</p><p>케이스 인터페이스에서 <strong>관련성 조정을</strong> 선택하면 열리는 <strong>쿼리 샌드박스를</strong> 사용합니다. 검색하는 <em>장르</em> 필드를 추가하여 탐색해 보세요:</p><pre><code>{
  "query": {
    "multi_match": {
      "query": "#$query##",
      "type": "best_fields",
      "fields": [
        "title^10",
        "overview",
        "cast",
        "genres"
      ]
    }
  }
}</code></pre><p>내 검색 다시 실행을 클릭하세요! 그리고 결과를 확인합니다. 변경되었나요? 안타깝게도 그렇지 않습니다. 이제 탐색할 수 있는 옵션이 많아졌습니다. 기본적으로 Elasticsearch가 제공하는 모든 쿼리 옵션이 있습니다:</p><ul><li>장르 필드의 필드 가중치를 높일 수 있습니다.</li><li>투표 평균에 따라 문서 등급을 높이는 기능을 추가할 수 있습니다.</li><li>장르가 많이 일치하는 경우에만 투표 평균에 따라 문서를 끌어올리는 더 복잡한 쿼리를 만들 수 있습니다.</li><li>…</li></ul><p>이러한 모든 옵션을 Quepid에서 탐색할 때 가장 좋은 점은 개선하려는 하나의 쿼리뿐만 아니라 모든 쿼리에 대한 효과를 정량화할 수 있다는 점입니다. 따라서 실적이 저조한 하나의 검색어를 개선하기 위해 다른 검색어의 검색 결과 품질을 희생할 수 없습니다. 빠르고 저렴하게 반복하고 위험 부담 없이 가설의 가치를 검증할 수 있으므로 오프라인 실험은 모든 검색 팀의 기본 역량으로 자리 잡았습니다.</p><h3>평가자 간 신뢰성 측정</h3><p>작업 설명, 정보 요구 사항, Quepid가 제공하는 것과 같은 인간 평가자 인터페이스가 있어도 인간 평가자는 동의하지 않을 수 있습니다.</p><p>의견 불일치 자체가 나쁜 것은 아니며, 오히려 의견 불일치를 측정하면 해결해야 할 문제가 드러날 수 있습니다. 관련성은 주관적일 수 있고, 쿼리가 모호할 수 있으며, 데이터가 불완전하거나 부정확할 수 있습니다. <a href="https://en.wikipedia.org/wiki/Fleiss%27_kappa">Fleiss의 카파는</a> 평가자 간의 합의에 대한 통계적 척도이며, Quepid에 사용할 수 있는 예제 노트북이 있습니다. 찾으려면 최상위 탐색 메뉴에서 <strong>노트북을</strong> <strong>선택하고 예제</strong> 폴더에서 <strong>Fleiss Kappa.ipynb 노트북을 선택합니다.</strong></p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/f0669ae96371368ef4d84bb28669560ef09d755c-624x61.png" style="width: 100%;" /><h2>결론</h2><p>가장 복잡한 검색 연관성 문제도 해결할 수 있도록 지원하고 계속 발전하고 있습니다. <a href="https://github.com/o19s/quepid/blob/main/CHANGELOG.md#800----2024-02-14">버전 8부터는 판단 생성</a> 프로세스를 확장하려는 팀에게 특히 유용한 AI 생성 판단을 지원합니다.</p><p>Quepid 워크플로우를 사용하면 확장 가능한 판단 목록을 효율적으로 생성하여 궁극적으로 사용자의 요구를 진정으로 충족하는 검색 결과를 얻을 수 있습니다. 판단 목록을 설정하면 검색 관련성을 측정하고, 개선 사항을 반복하며, 더 나은 사용자 경험을 제공하기 위한 강력한 기반을 마련할 수 있습니다.</p><p>앞으로 나아가면서 관련성 튜닝은 지속적인 과정이라는 점을 기억하세요. 판단 목록을 사용하면 진행 상황을 체계적으로 평가할 수 있지만, 실험, 메트릭 분석 및 반복적인 개선과 함께 사용할 때 가장 강력한 효과를 발휘합니다.</p><h2>추가 읽기</h2><ul><li>Quepid 문서:<ul><li><a href="https://quepid-docs.dev.o19s.com/2/quepid/32/relevancy-is-a-team-sport">관련성은 팀 스포츠입니다</a></li><li><a href="https://quepid-docs.dev.o19s.com/2/quepid/18/quepid-for-human-raters">인간 평가자를 위한 큐피드</a></li><li><a href="https://quepid-docs.dev.o19s.com/2/quepid/49/how-to-connect-quepid-to-elastic-cloud">Quepid를 Elastic Cloud에 연결하는 방법</a></li></ul></li><li><a href="https://github.com/o19s/quepid">퀘피드 깃허브 리포지토리</a></li><li><a href="https://opensourceconnections.com/blog/2020/07/07/meet-pete-the-e-commerce-search-product-manager/">이커머스 검색 개선에 관한 블로그 시리즈, Pete를 만나보세요.</a></li><li><a href="https://opensourceconnections.com/slack">관련성 슬랙</a>: #quepid 채널 가입하기</li></ul><p><strong> <a href="https://opensourceconnections.com/">오픈 소스 커넥션과</a></strong> 협력하여 검색 및 AI 기능을 혁신하고 팀이 이를 지속적으로 발전시킬 수 있도록 역량을 강화하세요. 전 세계 고객들이 검색 품질, 팀 역량, 비즈니스 성과에서 지속적으로 획기적인 개선을 달성하며 입증된 실적을 보유하고 있습니다. 자세한 내용은 <a href="https://opensourceconnections.com/contact/">지금</a> 바로 문의하세요.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Elasticsearch 인덱스에 필드 표시하기]]></title>
            <description><![CDATA[Elasticsearch 인덱스에서 필드를 표시하는 기술 살펴보기.
]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/displaying-fields-in-an-elasticsearch-index</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/displaying-fields-in-an-elasticsearch-index</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Kofi Bartlett]]></dc:creator>
            <pubDate>Mon, 26 May 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/fa386d7bbaeab6855e62897ace8d7dca91a060b4-720x420.jpg?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>이 문서에서는 Elasticsearch 인덱스에서 필드를 표시하는 방법에 대해 설명합니다. 이는 데이터 구조를 이해하고, 특정 필드를 식별하고, 문제를 해결하는 데 유용할 수 있습니다. 다음 주제를 다룰 예정입니다:</p><ol><li><a href="https://www.elastic.co/search-labs/blog/displaying-fields-in-an-elasticsearch-index#1.-using-the--mapping-api-to-retrieve-field-information"> <code>_mapping</code> API를 사용하여 필드 정보 검색하기</a></li><li><a href="https://www.elastic.co/search-labs/blog/displaying-fields-in-an-elasticsearch-index#2.-using-the--search-api-to-display-field-values"> API를 사용하여 필드 값 표시<code>_search</code></a></li><li><a href="https://www.elastic.co/search-labs/blog/displaying-fields-in-an-elasticsearch-index#3.-filtering-fields-using-the-fields-parameter">매개 변수를 사용하여 필드 필터링 <code>fields</code> </a></li><li><a href="https://www.elastic.co/search-labs/blog/displaying-fields-in-an-elasticsearch-index#4.-displaying-nested-fields">중첩된 필드 표시</a></li></ol><h2>1. 맵핑 API를 사용하여 필드 정보 검색하기</h2><p><code>_mapping</code> API를 사용하면 인덱스 또는 여러 <a href="https://opster.com/guides/elasticsearch/glossary/elasticsearch-index/">인덱스에</a> 대한 매핑 정의를 검색할 수 있습니다. 여기에는 필드, 데이터 유형 및 기타 속성에 대한 정보가 포함됩니다. 특정 인덱스에 대한 매핑을 검색하려면 다음 요청을 사용하세요:</p><pre><code>GET /<index_name>/_mapping</code></pre><p>예를 들어 <code>my_index</code> 이라는 인덱스가 있는 경우 다음 요청으로 해당 인덱스의 매핑을 검색할 수 있습니다:</p><pre><code>GET /my_index/_mapping</code></pre><p>응답에는 필드 및 해당 속성에 대한 정보가 포함된 인덱스에 대한 매핑 정의가 포함됩니다.</p><p>특정 필드에 대한 매핑을 검색할 수도 있습니다. 매핑이 상당히 크고 특정 필드에만 집중하려는 경우 유용할 수 있습니다. 특정 필드의 매핑을 검색하려면 다음 요청을 사용하세요:</p><pre><code>GET /my_index/_mapping/field/my_field</code></pre><p>다음 요청에서와 같이 쉼표로 이름을 구분하여 여러 필드의 매핑을 검색할 수도 있습니다:</p><pre><code>GET /my_index/_mapping/field/my_field_1,my_field_2,my_field_3</code></pre><h2>2. search API를 사용하여 필드 값 표시하기</h2><p>Elasticsearch 인덱스의 필드 값을 표시하려면 <code>_search</code> API를 사용하면 됩니다. 기본적으로 <code>_search</code> API는 색인된 원본 JSON 문서가 포함된 <code>_source</code> 필드를 반환합니다. 특정 필드만 표시하려면 검색 요청에 <code>_source</code> 매개변수를 사용하면 됩니다.</p><p>다음은 <code>my_index</code> 인덱스에 있는 문서에 대한 <code>title</code> 및 <code>author</code> 필드 값을 반환하는 검색 요청의 예입니다:</p><pre><code>GET /my_index/_search
{
  "query": {
    "match_all": {}
  },
  "_source": ["title", "author"]
}</code></pre><p>이 예제에서 <code>_source</code> 매개변수는 반환할 필드를 지정합니다.</p><h2>3. fields 매개변수를 사용하여 필드 필터링하기</h2><p><code>fields</code> 매개변수를 사용하여 검색 응답에 반환되는 필드를 필터링할 수도 있습니다. 특정 필드만 필요하고 응답의 크기를 줄이려는 경우 유용할 수 있습니다. <code>fields</code> 매개변수는 필드 이름 또는 와일드카드 패턴의 배열을 허용합니다.</p><p>예를 들어 <code>my_index</code> 색인에 있는 문서에 대해 <code>title</code> 및 <code>author</code> 필드만 반환하려면 다음 검색 요청을 사용할 수 있습니다:</p><pre><code>GET /my_index/_search
{
  "query": {
    "match_all": {}
  },
  "fields": ["title", "author"],
  "_source": false
}</code></pre><p>소스 문서를 반환하지 않으려면 <code>_source</code> 매개 변수를 false로 설정해야 합니다.</p><p><code>text</code> 데이터 유형이 있는 모든 필드를 반환하려면 다음과 같은 와일드카드 패턴을 사용할 수 있습니다:</p><pre><code>GET /my_index/_search
{
  "query": {
    "match_all": {}
  },
  "fields": ["*.text"],
  "_source": false
}</code></pre><h2>4. 중첩된 필드 표시</h2><p>인덱스에 중첩 필드가 포함된 경우, 점 표기법을 사용하여 <code>fields</code> 매개변수에서 중첩 필드 경로를 지정할 수 있습니다. 예를 들어 <code>address.city</code> 이라는 이름의 중첩 필드가 있는 경우 다음과 같이 검색 응답에 포함할 수 있습니다:</p><pre><code>GET /my_index/_search
{
  "query": {
    "match_all": {}
  },
  "fields": ["title", "author", "address.city"],
  "_source": false
}</code></pre><p>이 예제에서는 검색 응답에 <code>title</code>, <code>author</code>, <code>address.city</code> 필드의 값이 포함됩니다.</p><h2>결론</h2><p>결론적으로, Elasticsearch 인덱스에서 필드를 표시하려면 <code>_mapping</code> API를 사용하여 필드 정보를 검색하고 <code>_search</code> API를 사용하여 필드 값을 표시할 수 있습니다. <code>_source</code> 또는 <code>fields</code> 매개변수를 사용하여 검색 응답에 반환된 필드를 필터링하고 점 표기법을 사용하여 중첩된 필드를 표시할 수 있습니다. 이러한 기술은 데이터의 구조를 이해하고, 특정 필드를 식별하고, 문제를 해결하는 데 도움이 될 수 있습니다.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[벡터 데이터베이스로서의 Spring AI와 Elasticsearch]]></title>
            <description><![CDATA[Spring AI와 Elasticsearch를 사용하여 프로덕션에 바로 사용할 수 있는 RAG 앱을 구축하고, 벡터 데이터베이스를 활용해 LLM을 독점 데이터와 통합하는 방법을 알아보세요.
]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/spring-ai-elasticsearch-application</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/spring-ai-elasticsearch-application</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Philipp Krenn, Laura Trotta]]></dc:creator>
            <pubDate>Tue, 20 May 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/0771fb5b3d9234697cb868cd7d9d1b840000bf29-1280x720.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>이제 <strong>Spring AI를</strong> 정식으로 사용할 수 있으며, 첫 번째 <a href="https://spring.io/blog/2025/05/20/spring-ai-1-0-GA-released">안정적인 릴리스 1.0을</a> <a href="https://mvnrepository.com/artifact/org.springframework.ai/spring-ai-core">Maven Central에서</a> 다운로드할 수 있습니다. 좋아하는 <a href="https://www.elastic.co/what-is/large-language-models">LLM과</a> <a href="https://www.elastic.co/elasticsearch/vector-database">벡터 데이터베이스를</a> 사용하여 완벽한 AI 애플리케이션을 구축하는 데 바로 사용해 보세요. 또는 최종 애플리케이션을 사용하여 <a href="https://github.com/xeraa/rag-with-java-springai-elasticsearch">리포지토리로</a> 바로 이동하세요.</p><a href="https://www.youtube.com/watch/ms2xRO8B1YU">Watch video on YouTube</a><h2>스프링 AI란 무엇인가요?</h2><p>AI 분야의 급속한 발전에 힘입어 상당한 개발 기간을 거쳐 이제 Java로 구현된 종합적인 AI 엔지니어링 솔루션인 <strong>Spring AI 1.0을</strong> 사용할 수 있게 되었습니다. 이번 릴리스에는 AI 엔지니어를 위한 필수적인 새 기능이 다수 포함되어 있습니다.</p><p>Java와 Spring은 이러한 AI 물결에 뛰어들 수 있는 최적의 위치에 있습니다. 수많은 기업이 Spring Boot를 기반으로 업무를 운영하고 있으며, 이를 통해 이미 수행 중인 업무에 AI를 매우 쉽게 연결할 수 있습니다. 기본적으로 번거로움 없이 비즈니스 로직과 데이터를 이러한 AI 모델에 바로 연결할 수 있습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/328d7c51e1c145e94ea1e73ee9ff91836d3b180e-1600x773.png" style="width: 100%;" /><p>Spring AI는 다음과 같은 <a href="https://docs.spring.io/spring-ai/reference/api/index.html">다양한 AI 모델과 기술을</a> 지원합니다:</p><ul><li><strong>이미지 모델</strong>: 텍스트 프롬프트가 주어진 이미지를 생성합니다.</li><li><strong>전사 모델</strong>: 오디오 소스를 가져와 텍스트로 변환합니다.</li><li><strong>임베딩 모델: </strong>임의의 데이터를 의미론적 유사성 검색에 최적화된 데이터 유형인 <a href="https://www.elastic.co/what-is/vector-embedding">벡터로</a> 변환합니다.</li><li><strong>채팅 모델:  </strong>익숙하실 겁니다! 어딘가에서 잠깐이라도 대화를 나눈 적이 있을 것입니다.</li></ul><p>채팅 모델은 AI 분야에서 가장 많은 관심을 받고 있는 분야이며, 당연히 훌륭합니다! 문서를 수정하거나 시를 쓰는 데 도움을 받을 수 있습니다. (아직은 농담을 부탁하지 마세요...) 멋진 기능이지만 몇 가지 문제가 있습니다.</p><h2>AI 과제에 대한 스프링 AI 솔루션</h2><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/2ebd68a90ebc73847df6ef7325936d4d06b35c8c-1600x900.jpg" style="width: 100%;" /><p>Spring AI에서 이러한 문제와 그 해결책을 살펴보세요.</p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;">문제</td><td style="border: 1px solid #ccc; padding: 4px;">해결책</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">일관성</td><td style="border: 1px solid #ccc; padding: 4px;">채팅 모델은 개방적이고 주의가 산만해지기 쉽습니다.</td><td style="border: 1px solid #ccc; padding: 4px;">전체 모양과 구조를 관리하기 위한 시스템 프롬프트를 제공할 수 있습니다.</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">메모리</td><td style="border: 1px solid #ccc; padding: 4px;">AI 모델에는 메모리가 없으므로 특정 사용자가 보낸 메시지와 다른 사용자의 메시지를 연관시킬 수 없습니다.</td><td style="border: 1px solid #ccc; padding: 4px;">대화의 관련 부분을 저장할 수 있는 메모리 시스템을 제공할 수 있습니다.</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">격리</td><td style="border: 1px solid #ccc; padding: 4px;">AI 모델은 고립된 작은 샌드박스에서 생활하지만, 필요할 때 호출할 수 있는 도구에 대한 액세스 권한을 부여하면 정말 놀라운 일을 할 수 있습니다.</td><td style="border: 1px solid #ccc; padding: 4px;">Spring AI는 AI 모델에 환경의 도구에 대해 알려주면 AI 모델이 사용자에게 호출을 요청할 수 있는 도구 호출을 지원합니다. 이 멀티 턴 상호 작용은 모두 투명하게 처리됩니다.</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">개인 데이터</td><td style="border: 1px solid #ccc; padding: 4px;">AI 모델은 똑똑하지만 전지전능하지는 않습니다! 그들은 여러분의 독점 데이터베이스에 무엇이 있는지 알지 못하며, 저희도 여러분이 알기를 원하지 않을 것이라고 생각합니다!</td><td style="border: 1px solid #ccc; padding: 4px;">기본적으로 강력한 문자열 연결 연산자를 사용하여 모델이 질문을 보기 전에 요청에 텍스트를 입력하는 방식으로 프롬프트를 채워서 응답을 알려야 합니다. 원하는 경우 배경 정보를 입력하세요. 어떤 것을 보내고 어떤 것을 보내지 말아야 하는지 어떻게 결정하나요? 벡터 저장소를 사용하여 관련 데이터만 선택하여 전송합니다. 이를 검색 증강 생성 또는 RAG라고 합니다.</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">환각</td><td style="border: 1px solid #ccc; padding: 4px;">AI 채팅 모델은 채팅을 좋아합니다! 그리고 때때로 그들은 너무 자신만만해서 무언가를 만들어낼 수 있습니다.</td><td style="border: 1px solid #ccc; padding: 4px;">합리적인 결과를 확인하려면 한 모델을 사용하여 다른 모델의 결과를 검증하는 평가를 사용해야 합니다.</td>
                    </tr>
              </tbody>
            </table>
          <p></p><p>물론 어떤 AI 애플리케이션도 고립된 섬은 없습니다. 오늘날의 최신 AI 시스템과 서비스는 다른 시스템 및 서비스와 통합되었을 때 가장 잘 작동합니다. <a href="https://modelcontextprotocol.io/introduction"><strong>모델 컨텍스트 프로토콜</strong></a>(<strong> </strong>)을 사용하면 작성된 언어에 관계없이 AI 애플리케이션을 다른 MCP 기반 서비스와 연결할 수 있습니다. 이 모든 것을 더 큰 목표를 향해 나아가는 <strong>에이전트 </strong>워크플로에 통합할 수 있습니다.</p><p>가장 좋은 점은? 기본적으로 모든 것을 위한 편리한 스타터 종속성이 <a href="https://start.spring.io"><strong>Spring Initializr에서</strong></a>제공되므로 모든 Spring Boot 개발자에게 익숙한 관용구 및 추상화를 기반으로 구축하면서 이 모든 작업을 수행할 수 있습니다<strong>.</strong></p><p>Spring AI는 사용자가 익히 알고 있고 기대하는 구성에 대한 관례적인 설정을 제공하는 편리한 Spring Boot 자동 구성을 제공합니다. 그리고 Spring AI는 Spring Boot의 액추에이터와 마이크로미터 프로젝트를 통해 통합 가시성을 지원합니다. 또한 GraalVM 및 가상 스레드와도 잘 작동하므로 확장성이 뛰어난 매우 빠르고 효율적인 AI 애플리케이션을 구축할 수 있습니다.</p><h2>왜 엘라스틱서치인가</h2><p>Elasticsearch는 전체 텍스트 검색 엔진이라는 것은 이미 알고 계실 겁니다. 그렇다면 이 프로젝트에 이 기술을 사용하는 이유는 무엇일까요? 벡터 <em>스토어이기도</em> 합니다! 데이터는 전문 옆에 있는 아주 좋은 데이터입니다. 다른 주목할 만한 장점도 있습니다:</p><ul><li>매우 간편한 설정</li><li>오픈소스</li><li>수평적 확장성</li><li>조직의 자유 형식 데이터 대부분은 이미 Elasticsearch 클러스터에 있을 것입니다.</li><li>완벽한 검색 엔진 기능</li><li><a href="https://docs.spring.io/spring-ai/reference/api/vectordbs/elasticsearch.html">Spring AI에 완전히</a> 통합되었습니다!</li></ul><p>모든 것을 고려할 때, Elasticsearch는 훌륭한 벡터 저장소를 위한 모든 상자를 확인하므로 이를 설정하고 애플리케이션 구축을 시작해 보겠습니다!</p><h2>Elasticsearch 시작하기</h2><p>데이터베이스에 호스팅된 데이터와 상호 작용하는 데 사용할 UI 콘솔인 Elasticsearch와 Kibana가 모두 필요합니다.</p><p>Docker 이미지와 <a href="http://elastic.co">Elastic.co 홈페이지의</a> 장점 덕분에 로컬 컴퓨터에서 모든 것을 사용해 볼 수 있습니다. 여기로 이동하여 아래로 스크롤하여 <code>curl</code> 명령을 찾아 실행한 후 셸에 바로 파이프합니다:</p><pre><code> curl -fsSL https://elastic.co/start-local | sh 
  ______ _           _   _      
 |  ____| |         | | (_)     
 | |__  | | __ _ ___| |_ _  ___ 
 |  __| | |/ _` / __| __| |/ __|
 | |____| | (_| \__ \ |_| | (__ 
 |______|_|\__,_|___/\__|_|\___|
-------------------------------------------------
🚀 Run Elasticsearch and Kibana for local testing
-------------------------------------------------
ℹ️  Do not use this script in a production environment
⌛️ Setting up Elasticsearch and Kibana v9.0.0...
- Generated random passwords
- Created the elastic-start-local folder containing the files:
  - .env, with settings
  - docker-compose.yml, for Docker services
  - start/stop/uninstall commands
- Running docker compose up --wait
[+] Running 25/26
 ✔ kibana_settings Pulled                                                 16.7s 
 ✔ kibana Pulled                                                          26.8s 
 ✔ elasticsearch Pulled                                                   17.4s                                                                     
[+] Running 6/6
 ✔ Network elastic-start-local_default             Created                 0.0s 
 ✔ Volume "elastic-start-local_dev-elasticsearch"  Created                 0.0s 
 ✔ Volume "elastic-start-local_dev-kibana"         Created                 0.0s 
 ✔ Container es-local-dev                          Healthy                12.9s 
 ✔ Container kibana_settings                       Exited                 11.9s 
 ✔ Container kibana-local-dev                      Healthy                21.8s 
🎉 Congrats, Elasticsearch and Kibana are installed and running in Docker!
🌐 Open your browser at http://localhost:5601
   Username: elastic
   Password: w1GB15uQ
🔌 Elasticsearch API endpoint: http://localhost:9200
🔑 API key: SERqaGlKWUJLNVJDODc1UGxjLWE6WFdxSTNvMU5SbVc5NDlKMEhpMzJmZw==
Learn more at https://github.com/elastic/start-local
➜  ~ </code></pre><p>이렇게 하면 Elasticsearch와 Kibana를 위한 Docker 이미지를 가져와 구성하기만 하면 몇 분 후에 연결 자격 증명이 완료되어 로컬 컴퓨터에서 실행할 수 있습니다.</p><p>또한 Elasticsearch 인스턴스와 상호 작용하는 데 사용할 수 있는 두 개의 서로 다른 URL이 있습니다. 메시지가 표시되면 브라우저에서 <a href="http://localhost:5601">http://localhost:5601</a> 을 가리킵니다.</p><p>로그인할 때 필요한 사용자 이름 <code>elastic</code> 및 비밀번호(위의 출력 예시에서는 각각 <code>elastic</code> 및 <code>w1GB15uQ</code>)도 콘솔에 인쇄되어 있습니다.</p><p></p><h2>앱 통합하기</h2><p><a href="https://start.spring.io">Spring 초기화</a> 페이지로 이동하여 다음 종속성을 사용하여 새 Spring AI 프로젝트를 생성합니다:</p><ul><li><code>Elasticsearch Vector Store</code></li><li><code>Spring Boot Actuator</code></li><li><code>GraalVM</code></li><li><code>OpenAI</code></li><li><code>Web</code></li></ul><p>가장 최신 버전인 Java(이 글을 쓰는 시점 기준으로 Java 24 이상)와 원하는 빌드 도구를 선택해야 합니다. 이 예제에서는 Apache Maven을 사용하고 있습니다.</p><p><code>Generate</code> 을 클릭한 다음 프로젝트의 압축을 풀고 원하는 IDE로 프로젝트를 가져옵니다. (저희는 IntelliJ IDEA를 사용하고 있습니다.)</p><p>먼저 Spring Boot 애플리케이션에 대한 연결 세부 정보를 지정해 보겠습니다. <code>application.properties,</code> 에 다음을 입력합니다:</p><pre><code>spring.elasticsearch.uris=http://localhost:9200
spring.elasticsearch.username=elastic
spring.elasticsearch.password=w1GB15uQ</code></pre><p>또한 Spring AI의 벡터 저장소 기능을 통해 데이터 구조 측면에서 Elasticsearch 측에 필요한 모든 것을 초기화할 수 있으므로 지정해 주세요:</p><pre><code>spring.ai.vectorstore.elasticsearch.initialize-schema=true</code></pre><p>이 데모에서는 <strong>OpenAI</strong>, 특히 <strong>임베딩 모델과</strong> <strong>채팅 모델을 </strong>사용하겠습니다( <a href="https://docs.spring.io/spring-ai/reference/api/embeddings.html#available-implementations">Spring AI가 지원하는</a> 한 원하는 서비스를 자유롭게 사용하세요).</p><p>임베딩 모델은 데이터를 Elasticsearch에 저장하기 전에 데이터의 임베딩을 생성하는 데 필요합니다. OpenAI가 작동하려면 <code>API key</code> 을 지정해야 합니다:</p><pre><code>spring.ai.openai.api-key=...</code></pre><p><code>SPRING_AI_OPENAI_API_KEY</code> 같은 환경 변수로 정의하여 소스 코드에 자격 증명을 숨기지 않도록 할 수 있습니다.</p><p>파일을 업로드할 예정이므로 서블릿 컨테이너에 업로드할 수 있는 데이터의 양을 사용자 지정해야 합니다:</p><pre><code>spring.servlet.multipart.max-file-size=20MB
spring.servlet.multipart.max-request-size=20MB</code></pre><p>거의 다 왔습니다! 코드 작성에 들어가기 전에 이것이 어떻게 작동하는지 미리 살펴봅시다.</p><p>컴퓨터에서 <a href="https://images-cdn.fantasyflightgames.com/filer_public/9f/aa/9faa23a3-9f71-4c77-865f-bba4aac8a258/runewars-revised-_rulebook.pdf">다음 파일</a> (보드 게임 규칙 목록)을 다운로드하여 <code>test.pdf</code> 으로 이름을 바꾸고 <code>~/Downloads/test.pdf</code> 에 넣었습니다.</p><p>파일은 <code>/rag/ingest</code> 엔드포인트로 전송됩니다(로컬 설정에 따라 경로를 변경하세요):</p><pre><code>http --form POST http://localhost:8080/rag/ingest path@/Users/jlong/Downloads/test.pdf</code></pre><p>몇 초 정도 걸릴 수 있습니다...</p><p>그 뒤에서 데이터는 데이터의 임베딩을 생성하는 OpenAI로 전송되고, 그 데이터는 벡터와 원본 텍스트 모두 Elasticsearch에 기록됩니다.</p><p>그 데이터와 그 안에 포함된 모든 임베딩이 바로 마법이 일어나는 곳입니다. 그런 다음 <code>VectorStore</code> 인터페이스를 사용하여 Elasticsearch를 쿼리할 수 있습니다.</p><p>전체 흐름은 다음과 같습니다:</p><ul><li>HTTP 클라이언트가 선택한 PDF를 Spring 애플리케이션에 업로드합니다.</li><li>Spring AI는 PDF에서 텍스트 추출을 처리하고 각 페이지를 800자 단위의 청크로 분할합니다.</li><li>OpenAI는 각 청크에 대한 벡터 표현을 생성합니다.</li><li>그런 다음 청크된 텍스트와 임베딩이 모두 Elasticsearch에 저장됩니다.</li></ul><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/9734adb2d7128e61c515d5855dfad6d3a326a4a1-1454x706.png" style="width: 100%;" /><p>마지막으로 쿼리를 발행합니다:</p><pre><code>http :8080/rag/query question=="where do you place the reward card after obtaining it?" </code></pre><p>관련 답변을 드리겠습니다:</p><pre><code>After obtaining a Reward card, you place it facedown under the Hero card of the hero who received it.
Found at page: 28 of the manual</code></pre><p>멋지네요! 이 모든 것이 어떻게 작동하나요?</p><ul><li>HTTP 클라이언트가 Spring 애플리케이션에 질문을 제출합니다.</li><li>Spring AI는 OpenAI로부터 질문의 벡터 표현을 가져옵니다.</li><li>이 임베딩을 통해 저장된 Elasticsearch 청크에서 유사한 문서를 검색하고 가장 유사한 문서를 검색합니다.</li><li>그런 다음 Spring AI는 질문과 검색된 컨텍스트를 OpenAI로 전송하여 LLM 답변을 생성합니다.</li><li>마지막으로 생성된 답변과 검색된 컨텍스트에 대한 참조를 반환합니다.</li></ul><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/3799de6e8cb13ce49b9e136cfe593263030231a8-1464x1050.png" style="width: 100%;" /><p>Java 코드를 자세히 살펴보고 실제로 어떻게 작동하는지 확인해 보겠습니다.</p><p>우선, <strong>메인</strong> 클래스: 모든 스프링 부트 애플리케이션에 기본으로 제공되는 표준 메인 클래스입니다.</p><pre><code>@SpringBootApplication
public class DemoApplication {
 	public static void main(String[] args) { 
     		SpringApplication.run(DemoApplication.class, args);
 	}
}</code></pre><p>볼 것이 없습니다. 계속 진행...</p><p>다음은 기본 HTTP 컨트롤러입니다:</p><pre><code>@RestController
class RagController {

   private final RagService rag;

   RagController(RagService rag) {
       this.rag = rag;
   }

   @PostMapping("/rag/ingest")
   ResponseEntity<?> ingestPDF(@RequestBody MultipartFile path) {
       rag.ingest(path.getResource());
       return ResponseEntity.ok().body("Done!");
   }

   @GetMapping("/rag/query")
   ResponseEntity<?> query(@RequestParam String question) {
       String response = rag.directRag(question);
       return ResponseEntity.ok().body(response);
   }
}</code></pre><p>컨트롤러는 단순히 파일 수집을 처리하기 위해 구축한 서비스를 호출하여 파일을 Elasticsearch 벡터 저장소에 쓰고, 동일한 벡터 저장소에 대한 쿼리를 용이하게 하기만 하면 됩니다.</p><p>서비스를 살펴보겠습니다:</p><pre><code>@Service
class RagService {

   private final ElasticsearchVectorStore vectorStore;

   private final ChatClient ai;

   RagService(ElasticsearchVectorStore vectorStore, ChatClient.Builder clientBuilder) {
       this.vectorStore = vectorStore;
       this.ai = clientBuilder.build();
   }

   void ingest(Resource path) {
       PagePdfDocumentReader pdfReader = new PagePdfDocumentReader(path);
       List<Document> batch = new TokenTextSplitter().apply(pdfReader.read());
       vectorStore.add(batch);
   }

  // TBD
}</code></pre><p>이 코드는 모든 수집을 처리합니다. 바이트 단위의 컨테이너인 Spring 프레임워크 <code>Resource</code> 가 주어지면, Spring AI의 <code>PagePdfDocumentReader</code> 를 사용하여 PDF 데이터( <code>.PDF</code> 파일로 추정 - 임의의 입력을 허용하기 전에 반드시 유효성을 검사해야 합니다!)를 읽은 다음 Spring AI의 <code>TokenTextSplitter</code> 를 사용하여 토큰화하고, 마지막으로 결과 <code>List&lt;Document&gt;</code>를 <code>VectorStore</code> 구현인 <code>ElasticsearchVectorStore</code> 에 추가합니다.</p><p><code>/rag/ingest</code> 엔드포인트로 파일을 전송한 후 브라우저를 열어 <code>localhost:5601</code> 으로 이동하고 왼쪽 사이드 메뉴에서 <code>Dev Tools</code> 으로 이동합니다. 여기에서 쿼리를 실행하여 Elasticsearch 인스턴스의 데이터와 상호 작용할 수 있습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/c85e522f02f8b2da7462cd428dc7e952c9692542-1600x1040.png" style="width: 100%;" /><p>다음과 같이 쿼리를 실행합니다:</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/00974a176cbce11e70fcab24fb4b3f9c6e205982-1600x1040.png" style="width: 100%;" /><p>이제 재미있는 부분은 사용자 쿼리에 대한 응답으로 데이터를 어떻게 다시 가져올 수 있을까요?</p><p>다음은 <code>directRag</code> 이라는 메서드에서 쿼리를 구현한 첫 번째 컷입니다.</p><pre><code>String directRag(String question) {
   // Query the vector store for documents related to the question
   List<Document> vectorStoreResult =
           vectorStore.doSimilaritySearch(SearchRequest.builder().query(question).topK(5)
                   .similarityThreshold(0.7).build());

   // Merging the documents into a single string
   String documents = vectorStoreResult.stream()
           .map(Document::getText)
           .collect(Collectors.joining(System.lineSeparator()));

   // Exit if the vector search didn't find any results
   if (documents.isEmpty()) {
       return "No relevant context found. Please change your question.";
   }

   // Setting the prompt with the context
   String prompt = """
           You're assisting with providing the rules of the tabletop game Runewars.
           Use the information from the DOCUMENTS section to provide accurate answers to the
           question in the QUESTION section.
           If unsure, simply state that you don't know.
          
           DOCUMENTS:
           """ + documents
           + """
           QUESTION:
           """ + question;


   // Calling the chat model with the question
   String response = ai
           .prompt()
           .user(prompt)
           .call()
           .content();

   return response +
           System.lineSeparator() +
           "Found at page: " +
           // Retrieving the first ranked page number from the document metadata
           vectorStoreResult.getFirst().getMetadata().get(PagePdfDocumentReader.METADATA_START_PAGE_NUMBER) +
           " of the manual";

}</code></pre><p>코드는 매우 간단하지만 여러 단계로 세분화해 보겠습니다:</p><ol><li><code>VectorStore</code> 을 사용하여 유사도 검색을 수행합니다.</li><li>모든 결과가 주어지면 기본 Spring AI <code>Document</code>를 가져와서 텍스트를 추출하여 모두 하나의 결과로 연결합니다.</li><li><code>VectorStore</code> 의 결과를 모델에게 보내고 모델에게 수행할 작업을 지시하는 프롬프트와 사용자의 질문을 함께 보냅니다. 응답을 기다렸다가 반환합니다.</li></ol><p></p><p>검색 증강 세대인 <strong>RAG입니다</strong>. 벡터 저장소의 데이터를 사용하여 모델이 수행하는 처리 및 분석에 정보를 제공한다는 아이디어입니다. 이제 어떻게 하는지 알았으니 더 이상 그럴 필요가 없기를 바랍니다! 어쨌든 이렇게는 안 됩니다: Spring AI의 <a href="https://docs.spring.io/spring-ai/reference/api/advisors.html">어드바이저가</a> 이 과정을 더욱 간소화해 드립니다.</p><p>어드바이저를 사용하면 애플리케이션과 벡터 스토어 사이에 추상화 계층을 제공하는 것 외에 주어진 모델에 대한 요청을 사전 및 사후 처리할 수 있습니다. 빌드에 다음 종속성을 추가하세요:<br/></p><pre><code><dependency>
   <groupId>org.springframework.ai</groupId>
   <artifactId>spring-ai-advisors-vector-store</artifactId>
</dependency></code></pre><p><code>advisedRag(String question)</code> 라는 메서드를 클래스에 추가합니다:</p><pre><code>String advisedRag(String question) {
   return this.ai
           .prompt()
           .user(question)
           .advisors(new QuestionAnswerAdvisor(vectorStore))
           .call()
           .content();
}</code></pre><p>모든 RAG 패턴 로직은 <code>QuestionAnswerAdvisor</code> 에 캡슐화되어 있습니다. 그 외의 모든 것은 <code>ChatModel</code> 으로 요청하는 것과 동일합니다! 멋지네요!</p><p><a href="https://github.com/xeraa/rag-with-java-springai-elasticsearch">전체 코드는 GitHub에서</a> 다운로드할 수 있습니다.</p><h2>결론</h2><p>이 데모에서는 Docker 이미지를 사용하고 로컬 머신에서 모든 작업을 수행했지만, 여기서는 프로덕션에 적합한 AI 시스템과 서비스를 구축하는 것이 목표입니다. 이를 실현하기 위해 할 수 있는 몇 가지 방법이 있습니다.</p><p>우선, <a href="https://docs.spring.io/spring-boot/reference/actuator/index.html#actuator">스프링 부트 액추에이터를</a> 추가하여 토큰 소비를 모니터링할 수 있습니다. 토큰은 모델에 대한 특정 요청의 복잡성(때로는 달러와 센트)에 대한 비용의 대리인입니다.</p><p>이미 클래스 경로에 Spring Boot 액추에이터가 있으므로 다음 프로퍼티를 지정하여 모든 메트릭을 표시하기만 하면 됩니다(멋진 <a href="http://micrometer.io">Micrometer.io</a> 프로젝트에서 캡처한):</p><pre><code>management.endpoints.web.exposure.include=*</code></pre><p>애플리케이션을 다시 시작합니다. 쿼리를 작성한 다음 http://localhost:8080/actuator/metrics <a href="http://localhost:8080/actuator/metrics">으로</a> 이동합니다. &quot;<code>token</code>&quot;를 검색하면 애플리케이션에서 사용 중인 토큰에 대한 정보를 확인할 수 있습니다. 계속 지켜보시기 바랍니다. 물론 <a href="https://docs.micrometer.io/micrometer/reference/implementations/elastic.html">Elasticsearch용 Micrometer의</a> 통합을 사용하여 이러한 메트릭을 푸시하고 Elasticsearch가 선택한 시계열 데이터베이스로 작동하도록 할 수도 있습니다!</p><p>그런 다음 Elasticsearch와 같은 데이터 저장소나 OpenAI 또는 기타 네트워크 서비스에 요청을 할 때마다 IO를 수행하게 되며, 종종 그 IO가 실행되는 스레드를 차단한다는 점을 고려해야 합니다. Java 21 이상 버전에는 확장성을 획기적으로 개선하는 논-블럭킹 <strong>가상 스레드가</strong> 제공됩니다. 다음으로 활성화합니다:<br/></p><pre><code>spring.threads.virtual.enabled=true</code></pre><p>마지막으로, 애플리케이션과 데이터가 잘 성장하고 확장할 수 있는 곳에서 호스팅하는 것이 좋습니다. 애플리케이션을 실행할 위치에 대해 이미 생각해 보셨겠지만, 데이터를 어디에 호스팅할 것인가? <a href="https://cloud.elastic.co/">Elastic Cloud를</a> 추천해도 될까요? 안전하고, 비공개적이며, 확장 가능하고, 다양한 기능을 제공합니다. 가장 좋아하는 부분은? 원하는 경우, 사용자가 아닌 Elastic이 호출기를 착용하는 서버리스 에디션을 받으실 수 있습니다!</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[올바른 방법으로 자바스크립트에서 Elasticsearch, 2부]]></title>
            <description><![CDATA[서버리스 환경에서 Elasticsearch Node.js 클라이언트를 실행하는 방법과 프로덕션 모범 사례를 학습하여 코딩 오류를 줄이는 방법을 알아보세요. ]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/how-to-use-elasticsearch-in-javascript-part-ii</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/how-to-use-elasticsearch-in-javascript-part-ii</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Jeffrey Rengifo]]></dc:creator>
            <pubDate>Mon, 19 May 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/70fb0ff949f6db9ac9b8a28ecb4329ab915ebf46-720x420.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <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><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-ii#production-best-practices">프로덕션 모범 사례</a><ul><li><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-ii#error-handling">오류 처리</a></li><li><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-ii#testing">테스팅</a></li></ul></li><li><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-ii#serverless-environments">서버리스 환경</a><ul><li><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></li><li><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></li></ul></li></ul><p><em>여기에서 예제와 함께 소스 코드를 확인할 수 <strong><a href="https://github.com/Delacrobix/JS-client-best-practices_article"></a>있습니다.</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><pre><code>const { errors } = require('@elastic/elasticsearch')
console.log(errors)</code></pre><p>검색 예제로 돌아가서 몇 가지 가능한 오류를 처리해 보겠습니다:</p><pre><code>app.get("/search/lexic", async (req, res) => {
 ....
  } 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,
    });
  }
});</code></pre><p><code>ResponseError</code> 특히 응답이 <code>4xx</code> 또는 <code>5xx</code> 인 경우 요청이 잘못되었거나 서버를 사용할 수 없음을 의미합니다.</p><p><strong>텍스트 유형 필드에 용어 쿼리를</strong>시도하는 등 잘못된 쿼리를 생성하여 이러한 유형의 오류를 테스트할 수 있습니다:</p><p>기본 오류입니다:</p><pre><code> {
    "success": false,
    "results": null,
    "error": "parsing_exception\n\tRoot causes:\n\t\tparsing_exception: [terms] query does not support [visit_details]"
}</code></pre><p>사용자 지정 오류: </p><pre><code>{
    "erroStatus": 400,
    "success": false,
    "results": null,
    "error": "Response error!, query malformed or server down; contact the administrator!"
}</code></pre><p>또한 각 오류 유형을 특정 방식으로 캡처하고 처리할 수도 있습니다. 예를 들어 <code>TimeoutError</code> 에 재시도 로직을 추가할 수 있습니다.</p><pre><code>app.get("/search/semantic", async (req, res) => {
    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.",
      });
    }
  }
});</code></pre><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><pre><code>"type": "module",
	"scripts": {
		"test": "ava"
	},
	"devDependencies": {
		"ava": "^5.0.0"
	}</code></pre><p>이제 <code>test.js</code> 파일을 생성하고 모의 클라이언트를 설치해 보겠습니다:</p><pre><code>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()
})</code></pre><p>이제 시맨틱 검색을 위한 모형을 추가합니다:</p><pre><code>function createSemanticSearchMock(query, indexName) {
  mock.add(
    {
      method: "POST",
      path: `/${indexName}/_search`,
      body: {
        query: {
          semantic: {
            field: "semantic_field",
            query: query,
          },
        },
      },
    },
    () => {
      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.",
              },
            },
          ],
        },
      };
    }
  );
}</code></pre><p>이제 코드에 대한 테스트를 생성하여 Elasticsearch 부분이 항상 동일한 결과를 반환하는지 확인할 수 있습니다:</p><pre><code>import test from 'ava';

test("performSemanticSearch must return formatted results correctly", async (t) => {
  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 > 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"
    );
  }
});</code></pre><p>테스트를 실행해 보겠습니다.</p><p><code>npm run test</code></p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/42feae845ae8eae03c37ad7ad114e8db35984812-1186x302.png" style="width: 100%;" /><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>스니핑을 끄고 모든 스니핑 관련 옵션을 무시합니다.</li><li>첫 번째 노드를 제외한 구성에서 전달된 모든 노드를 무시하고 모든 노드 필터링 및 선택 옵션을 무시합니다.</li><li>압축 및 `TLSv1_2_method`를 활성화합니다(Elastic Cloud에 대해 구성할 때와 동일).</li><li>모든 요청에 `elastic-api-version` HTTP 헤더를 추가합니다.</li><li>기본적으로 &#x27;가중치 연결 풀&#x27; 대신 &#x27;클라우드 연결 풀&#x27;을 사용합니다.</li><li>표준 MIME 유형을 위해 공급업체의 `content-type` 및 `accept` 헤더를 끕니다.</li></ul><p>서버리스 프로젝트를 연결하려면 서버모드: 서버리스 매개변수를 사용해야 합니다.</p><pre><code>const { Client } = require('@elastic/elasticsearch')
const client = new Client({
  node: 'ELASTICSEARCH_ENDPOINT',
  auth: { apiKey: 'ELASTICSEARCH_API_KEY' },
  serverMode: "serverless",
});</code></pre><h3>서비스형 함수(FaaS) 환경에서 클라이언트를 실행하는 방법</h3><p>이 예제에서는 Node.js 서버를 사용했지만 AWS 람다, GCP 실행 등과 같은 기능을 갖춘 서비스형 기능 환경을 사용하여 연결할 수도 있습니다.</p><pre><code>'use strict'

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

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

exports.handler = async function (event, context) {
  // use the client
}</code></pre><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><pre><code>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}`,
    },
  }
);</code></pre><p>이 엔드포인트는 /api 폴더에 있으며 서버 측에서 실행되므로 클라이언트는 검색어에 해당하는 &#x27;text&#x27; 매개변수만 제어할 수 있습니다.</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>
        </item>
        <item>
            <title><![CDATA[Elasticsearch 디스크 공간과 사용량을 최적화하는 방법]]></title>
            <description><![CDATA[클러스터 비용을 최적화하기 위해 Elasticsearch 디스크가 과도하게 사용된 경우와 디스크 용량이 충분히 활용되지 않는 경우를 예방하고 대응하는 방법을 알아보세요.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/optimize-elasticsearch-disk-space-and-usage</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/optimize-elasticsearch-disk-space-and-usage</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <pubDate>Fri, 16 May 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/5d41a078dd03e4480a0ff4e9591c8618b9bab4d0-720x420.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>디스크 관리는 모든 데이터베이스에서 중요하며 Elasticsearch도 예외는 아닙니다. 사용 가능한 디스크 공간이 충분하지 않으면 Elasticsearch는 노드에 샤드 할당을 중지합니다. 이렇게 하면 결국 클러스터에 데이터를 쓸 수 없게 되어 애플리케이션에서 데이터가 손실될 수 있는 잠재적 위험이 있습니다. 반면에 디스크 공간이 너무 많으면 필요한 것보다 더 많은 리소스에 대한 비용을 지불하는 것입니다.</p><h2>워터마크의 배경</h2><p>Elasticsearch 클러스터에는 사용 가능한 디스크 공간을 추적하는 데 도움이 되는 다양한 &quot;워터마크&quot; 임계값이 있습니다. 노드에서 디스크가 가득 차면 가장 먼저 통과해야 하는 임계값은 &#x27;디스크 부족 워터마크&#x27;입니다. 그러면 두 번째 임계값은 &quot;높은 디스크 워터마크 임계값&quot;이 됩니다. 마지막으로 &#x27;디스크 홍수 단계&#x27;에 도달하게 됩니다. 이 임계값을 통과하면 클러스터는 워터마크를 통과한 노드에 하나의 샤드(기본 또는 복제본)가 있는 모든 인덱스에 대한 쓰기를 차단합니다. 읽기(검색)는 계속 가능합니다.</p><h2>디스크가 너무 꽉 찬 경우(사용량 초과)를 방지하고 처리하는 방법</h2><p>Elasticsearch 디스크가 너무 꽉 찬 경우를 처리하는 방법에는 여러 가지가 있습니다:</p><ol><li>오래된 데이터를 <strong>삭제합니다</strong> <strong>:</strong> 일반적으로 데이터는 무기한 보관해서는 안 됩니다. 디스크가 너무 꽉 차는 것을 방지하고 해결하는 한 가지 방법은 데이터가 특정 수명에 도달하면 안정적으로 보관 및 삭제되도록 하는 것입니다. 이를 위한 한 가지 방법은 <a href="https://www.elastic.co/docs/manage-data/lifecycle/index-lifecycle-management">ILM을</a> 사용하는 것입니다.</li><li><strong>스토리지 용량을 추가합니다:</strong> 데이터를 삭제할 수 없는 경우, 성능에 부정적인 영향을 주지 않으면서 모든 데이터를 유지하기 위해 데이터 노드를 더 추가하거나 디스크 크기를 늘릴 수 있습니다. 클러스터에 스토리지 용량을 추가해야 하는 경우 스토리지 용량만 추가해야 하는지, 아니면 스토리지 용량과 RAM 및 CPU 리소스를 비례적으로 추가해야 하는지 고려해야 합니다(아래 <a href="https://www.elastic.co/search-labs/blog/optimize-elasticsearch-disk-space-and-usage#the-relationship-between-disk-size,-ram-and-cpu">디스크 크기, RAM 및 CPU 비율</a> 섹션 참조).</li></ol><h2>Elasticsearch 클러스터에 스토리지 용량을 추가하는 방법</h2><ol><li><strong>데이터 노드 수를 늘립니다: </strong>새 노드는 기존 노드와 크기가 같아야 하며 동일한 Elasticsearch 버전이어야 합니다.</li><li><strong>기존 노드의 크기를 늘립니다: </strong>클라우드 기반 환경에서는 일반적으로 기존 노드에서 디스크 크기와 RAM/CPU를 쉽게 늘릴 수 있습니다.</li><li><strong>디스크 크기만 늘리기: </strong>클라우드 기반 환경에서는 디스크 크기를 늘리는 것이 비교적 쉬운 경우가 많습니다.</li><li><a href="https://www.elastic.co/docs/deploy-manage/tools/snapshot-and-restore"><strong>스냅샷</strong> <strong>및</strong> <strong>복원</strong></a><strong>:</strong> 백업에서 자동화된 프로세스를 통해 요청 시 이전 데이터를 검색할 수 있도록 허용하려는 경우, 이전 인덱스를 스냅샷하고, 삭제하고, 스냅샷에서 요청 시 데이터를 임시로 복원할 수 있습니다. </li><li><strong>샤드당 복제본 수 줄이기:</strong> 데이터를 줄이는 또 다른 옵션은 각 샤드의 복제본 수를 줄이는 것입니다. 고가용성을 위해서는 샤드당 하나의 복제본을 사용하는 것이 좋지만, 데이터가 오래되면 복제본 없이도 작업할 수 있습니다. 일반적으로 데이터가 영구적이거나 필요한 경우 복원할 백업이 있는 경우 이 방법을 사용할 수 있습니다.</li><li><strong>알림 만들기:</strong> 향후 디스크가 가득 차는 것을 방지하고 선제적으로 대응하려면 디스크 사용량에 따라 디스크가 가득 차기 시작할 때 알려주는 알림을 만들어야 합니다. </li></ol><h2>디스크 용량이 제대로 활용되지 않는 경우를 방지하고 처리하는 방법</h2><p>디스크 용량을 제대로 활용하지 못하는 경우 클러스터의 스토리지 볼륨을 줄일 수 있는 다양한 옵션이 있습니다.</p><h3>Elasticsearch 클러스터의 스토리지 볼륨을 줄이는 방법</h3><p>클러스터의 스토리지 용량을 줄이는 방법에는 여러 가지가 있습니다.</p><p><strong>1. 데이터 노드 수 줄이기</strong></p><p>데이터 저장 공간을 줄이면서 RAM과 CPU 리소스도 같은 비율로 줄이려면 이 방법이 가장 쉬운 전략입니다. 불필요한 노드를 폐기하면 비용을 가장 크게 절감할 수 있습니다.</p><p>노드를 폐기하기 전에 노드를 폐기해야 합니다:</p><ul><li>해제할 노드가 마스터 노드로서 필요하지 않은지 확인합니다. 항상 마스터 노드 역할이 있는 노드가 3개 이상 있어야 합니다.</li><li>폐기할 노드에서 데이터 샤드를 마이그레이션합니다.</li></ul><p><strong>2. 기존 노드를 더 작은 노드로 교체</strong></p><p>노드 수를 더 줄일 수 없는 경우(일반적으로 3개가 최소 구성) 기존 노드의 크기를 줄일 수 있습니다. 샤드는 노드당 샤드 수에 따라 균형을 맞추기 때문에 모든 데이터 노드의 RAM 메모리와 디스크 크기가 동일한지 확인하는 것이 좋습니다.</p><p>그 과정은 다음과 같습니다:</p><ul><li>클러스터에 새롭고 작은 노드 추가하기</li><li>폐기할 노드에서 샤드를 멀리 마이그레이션합니다.</li><li>기존 노드 종료</li></ul><p><strong>3. 노드에서 디스크 크기 줄이기</strong></p><p>클러스터의 전체 RAM이나 CPU를 변경하지 않고 노드의 디스크 크기만 줄이려는 경우 각 노드의 디스크 크기를 줄일 수 있습니다. Elasticsearch 노드에서 디스크 크기를 줄이는 것은 결코 간단한 과정이 아닙니다.</p><p>가장 쉬운 방법은 일반적으로 다음과 같이 하는 것입니다:</p><ul><li>노드에서 샤드 마이그레이션</li><li>노드 중지</li><li>적절한 크기의 새 데이터 볼륨을 노드에 마운트합니다.</li><li>이전 디스크 볼륨의 모든 데이터를 새 볼륨으로 복사합니다.</li><li>이전 볼륨 A 분리</li><li>노드 시작 및 샤드를 다시 노드로 마이그레이션하기</li></ul><p>이 과정에서 노드의 여분의 샤드를 임시로 저장할 수 있는 충분한 용량이 다른 노드에 있어야 합니다. 많은 경우, 이 프로세스를 관리하는 데 드는 비용이 디스크 사용량의 잠재적 절감 효과를 초과할 수 있습니다. 따라서 노드를 원하는 디스크 크기의 새 노드로 완전히 교체하는 것이 더 간단할 수 있습니다(위의 &quot;기존 노드를 더 작은 노드로 교체하기&quot; 참조).</p><p>불필요한 리소스에 대한 비용을 지불할 때 리소스 활용을 최적화하면 비용을 확실히 줄일 수 있습니다.</p><h2>디스크 크기, RAM 및 CPU의 관계</h2><p>클러스터의 디스크 용량과 RAM의 이상적인 비율은 특정 사용 사례에 따라 달라집니다. 따라서 스토리지 용량 변경을 고려할 때는 현재 디스크/RAM/CPU 비율이 적절하게 균형을 이루고 있는지, 결과적으로 RAM/CPU도 같은 비율로 추가/축소해야 하는지 여부도 고려해야 합니다.</p><p>RAM 및 CPU 요구 사항은 <a href="https://opster.com/guides/elasticsearch/glossary/elasticsearch-indexing/">인덱싱</a> 활동의 양, 쿼리의 수와 유형, 검색 및 집계되는 데이터의 양에 따라 달라집니다. 이는 클러스터에 저장되는 데이터의 양에 비례하는 경우가 많으므로 디스크 크기와도 관련이 있어야 합니다.</p><p>디스크 용량과 RAM의 비율은 사용 사례에 따라 변경될 수 있습니다. 여기에서 몇 가지 예를 확인하세요:</p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;">인덱스 활동</td><td style="border: 1px solid #ccc; padding: 4px;">보존</td><td style="border: 1px solid #ccc; padding: 4px;">검색 활동</td><td style="border: 1px solid #ccc; padding: 4px;">디스크 용량</td><td style="border: 1px solid #ccc; padding: 4px;">RAM</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">엔터프라이즈 검색 앱</td><td style="border: 1px solid #ccc; padding: 4px;">중간 수준의 로그 수집</td><td style="border: 1px solid #ccc; padding: 4px;">Long</td><td style="border: 1px solid #ccc; padding: 4px;">빛</td><td style="border: 1px solid #ccc; padding: 4px;">2TB</td><td style="border: 1px solid #ccc; padding: 4px;">32GB</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">앱 모니터링</td><td style="border: 1px solid #ccc; padding: 4px;">집중적인 로그 수집</td><td style="border: 1px solid #ccc; padding: 4px;">짧은</td><td style="border: 1px solid #ccc; padding: 4px;">빛</td><td style="border: 1px solid #ccc; padding: 4px;">1TB</td><td style="border: 1px solid #ccc; padding: 4px;">32GB</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">전자상거래</td><td style="border: 1px solid #ccc; padding: 4px;">라이트 데이터 인덱싱</td><td style="border: 1px solid #ccc; padding: 4px;">무기한</td><td style="border: 1px solid #ccc; padding: 4px;">무거운</td><td style="border: 1px solid #ccc; padding: 4px;">500GB</td><td style="border: 1px solid #ccc; padding: 4px;">32GB</td>
                    </tr>
              </tbody>
            </table>
          <p><em>노드 머신의 구성을 수정하면 노드 다운타임이 발생할 수 있고 이미 과도하게 확장된 다른 노드로 샤드가 마이그레이션되지 않도록 해야 하므로 신중하게 수행해야 한다는 점을 기억하세요.</em></p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[올바른 방법으로 자바스크립트에서 Elasticsearch, 파트 1]]></title>
            <description><![CDATA[자바스크립트로 프로덕션에 사용할 수 있는 Elasticsearch 백엔드를 만드는 방법을 설명합니다.  

JavaScript와 함께 Elasticsearch를 사용하는 방법을 살펴보세요. 클라이언트/서버 모범 사례를 따르면서 Elasticsearch 문서를 쿼리하는 다양한 검색 엔드포인트를 가진 서버를 만드는 방법을 소개합니다.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/how-to-use-elasticsearch-in-javascript-part-i</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/how-to-use-elasticsearch-in-javascript-part-i</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Jeffrey Rengifo]]></dc:creator>
            <pubDate>Thu, 15 May 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/72200540ed258779d87e53a72ea189f8a138540c-1600x901.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>이 글은 JavaScript와 함께 Elasticsearch를 사용하는 방법을 다루는 시리즈의 첫 번째 글입니다. 이 시리즈에서는 JavaScript 환경에서 Elasticsearch를 사용하는 방법에 대한 기본 사항을 알아보고 검색 앱을 만드는 데 가장 적합한 기능과 모범 사례를 검토합니다. 끝으로 JavaScript를 사용하여 Elasticsearch를 실행하는 데 필요한 모든 것을 알게 될 것입니다.</p><p>이 첫 번째 파트에서는 검토할 내용을 살펴봅니다:</p><ul><li><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i#environment">환경</a><ul><li><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i#frontend,-backend,-or-serverless?">프론트엔드, 백엔드 또는 서버리스?</a></li><li><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i#connecting-the-client">클라이언트 연결하기</a></li></ul></li><li><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i#indexing-documents">문서 색인 생성</a><ul><li><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i#elasticsearch-client">Elasticsearch 클라이언트</a></li><li><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i#semantic-mappings">시맨틱 매핑</a></li><li><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i#bulk-helper">대량 도우미</a></li></ul></li><li><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i#searching-data">데이터 검색</a><ul><li><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></li><li><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></li><li><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></li></ul></li></ul><p><em>여기에서 예제와 함께 소스 코드를 확인할 수 <strong><a href="https://github.com/Delacrobix/JS-client-best-practices_article"></a>있습니다.</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://cdn.sanity.io/images/me0ej585/search-labs-import-testing/92ce6cfd923c8008fa44f617a58193642d9d5879-661x410.png" style="width: 100%;" /><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://cdn.sanity.io/images/me0ej585/search-labs-import-testing/a08769f08ec73fe57bf2e961cfdfbb1cdd57919d-972x429.png" style="width: 100%;" /><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><a href="https://www.npmjs.com/package/@elastic/elasticsearch"><strong><em>elastic/elasticsearch</em></strong></a>: 공식 Node.js 클라이언트입니다.</li><li><a href="https://www.npmjs.com/package/express"><strong><em>익스프레스</em></strong></a>: 경량 노드js 서버를 회전시켜 Elasticsearch를 노출할 수 있습니다.</li><li><a href="https://www.npmjs.com/package/split2"><strong><em>split2</em></strong></a>: 텍스트 줄을 스트림으로 분할합니다. 한 번에 한 줄씩 ndjson 파일을 처리하는 데 유용합니다.</li><li><a href="https://www.npmjs.com/package/dotenv"><strong><em>dotenv</em></strong></a>: .env를 사용하여 환경 변수를 관리할 수 있습니다. 파일</li></ul><p>.env 파일 만들기 파일을 열고 다음 줄을 추가합니다:</p><pre><code>ELASTICSEARCH_ENDPOINT="Your Elasticsearch endpoint"
ELASTICSEARCH_API_KEY="Your Elasticssearch API"</code></pre><p>이렇게 하면 <code>dotenv</code> 패키지를 사용하여 해당 변수를 가져올 수 있습니다.</p><p><code>server.js</code> 파일을 만듭니다:</p><pre><code>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, () => {
  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) => {
  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,
    });
  }
});</code></pre><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><pre><code>Server running on port 3000</code></pre><p>이제 엔드포인트 <code>/ping</code> 를 참조하여 Elasticsearch 클러스터의 상태를 확인해 보겠습니다.</p><pre><code>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"
    }
}</code></pre><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><pre><code>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 },
});</code></pre><h3>시맨틱 매핑</h3><p>동물 병원에 대한 데이터로 인덱스를 만들겠습니다. 주인과 반려동물의 정보, 방문 세부 정보를 저장합니다.</p><p>이름과 설명 등 전체 텍스트 검색을 실행하려는 데이터는 텍스트로 저장됩니다. 동물의 종이나 품종과 같은 카테고리의 데이터는 키워드로 저장됩니다.</p><p>또한 모든 필드의 값을 semantic_text 필드에 복사하여 해당 정보에 대해서도 의미론적 검색을 실행할 수 있도록 합니다.</p><pre><code>const INDEX_NAME = "vet-visits";

const createMappings = async (indexName, mapping) => {
  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",
    },
  },
});</code></pre><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><pre><code>{"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."}</code></pre><p>split2를 사용해 파일 줄을 스트리밍하는 동시에 대량 도우미가 이를 Elasticsearch로 전송합니다.</p><pre><code>const { createReadStream } = require("fs");
const split = require("split2");
 
const indexData = async (filePath, indexName) => {
  try {
    console.log(`Indexing data from ${filePath} into ${indexName}...`);

    const result = await esClient.helpers.bulk({
      datasource: createReadStream(filePath).pipe(split()),

      onDocument: () => {
        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);</code></pre><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><pre><code>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</code></pre><h2>Elasticsearch에서 데이터 검색</h2><p><code>server.js</code> 파일로 돌아가서 어휘, 시맨틱 또는 하이브리드 검색을 수행하기 위해 다양한 엔드포인트를 만들어 보겠습니다.</p><p>간단히 말해, 이러한 유형의 검색은 상호 배타적인 것이 아니라 답변해야 하는 질문의 종류에 따라 달라집니다.</p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">쿼리 유형</td><td style="border: 1px solid #ccc; padding: 4px;">사용 사례</td><td style="border: 1px solid #ccc; padding: 4px;">질문 예시</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">어휘 쿼리</td><td style="border: 1px solid #ccc; padding: 4px;">문제의 단어 또는 단어 어근이 색인 문서에 표시될 가능성이 높습니다. 질문과 문서 간의 토큰 유사성.</td><td style="border: 1px solid #ccc; padding: 4px;">파란색 스포츠 티셔츠를 찾고 있습니다.</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">시맨틱 쿼리</td><td style="border: 1px solid #ccc; padding: 4px;">질문의 단어는 문서에 표시되지 않을 가능성이 높습니다. 질문과 문서 간의 개념적 유사성.</td><td style="border: 1px solid #ccc; padding: 4px;">추운 날씨에 입을 옷을 찾고 있습니다.</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">하이브리드 검색</td><td style="border: 1px solid #ccc; padding: 4px;">문제에는 어휘 및/또는 의미 구성 요소가 포함되어 있습니다. 질문과 문서 간의 토큰 및 의미적 유사성.</td><td style="border: 1px solid #ccc; padding: 4px;">해변 결혼식에 입을 S 사이즈 드레스를 찾고 있습니다.</td>
                    </tr>
              </tbody>
            </table>
          <p>문제의 <strong><em>어휘 </em></strong>부분은 제목과 설명 또는 카테고리 이름의 일부일 가능성이 높으며, <strong><em>의미 </em></strong>부분은 해당 필드와 관련된 개념입니다. <strong><em>파란색은</em></strong> 카테고리 이름 또는 설명의 일부일 수 있으며, <strong><em>해변 결혼식은</em></strong> 그렇지 않을 가능성이 높지만 의미상 린넨 의류와 관련이 있을 수 있습니다.</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><pre><code>app.get("/search/lexic", async (req, res) => {
  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,
    });
  }
});</code></pre><p>테스트 대상: <strong><em>손톱 다듬기</em></strong></p><pre><code>curl http://localhost:3000/search/lexic?q=nail%20trimming</code></pre><p>답변:</p><pre><code>{
    "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"
                ]
            }
        }
    ]
}</code></pre><h3>시맨틱 쿼리(/search/semantic?q=&lt;query_term&gt;)</h3><p>시맨틱 검색은 어휘 검색과 달리 벡터 검색을 통해 검색어의 의미와 유사한 결과를 찾아냅니다.</p><p>시맨틱 검색 실습 튜토리얼은 <a href="https://www.elastic.co/demo-gallery/semantic-search">여기에서</a> 확인할 수 있습니다.</p><pre><code>app.get("/search/semantic", async (req, res) => {
  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,
    });
  }
});</code></pre><p>테스트 대상은 다음과 같습니다: <strong><em>누가 페디큐어를 받았나요?</em></strong></p><pre><code>curl http://localhost:3000/search/semantic?q=Who%20got%20a%20pedicure?</code></pre><p>답변:</p><pre><code>{
    "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"
                ]
            }
        }
    ]
}</code></pre><h3>하이브리드&lt;query_term&gt; 쿼리(/검색/하이브리드?q=)</h3><p>하이브리드 검색을 사용하면 시맨틱 검색과 어휘 검색을 결합하여 토큰별 검색의 정확성과 시맨틱 검색의 의미 근접성을 모두 얻을 수 있습니다.</p><pre><code>app.get("/search/hybrid", async (req, res) => {
  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,
    });
  }
});</code></pre><p>&quot;<strong><em>누가 페디큐어나 치과 치료를 받았나요?</em></strong>&quot;로 테스트합니다.&quot;</p><pre><code>curl http://localhost:3000/search/hybrid?q=who%20got%20a%20pedicure%20or%20dental%20treatment</code></pre><p>대응:</p><pre><code>{
    "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"
                ]
            }
        }
    ]
}</code></pre><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>
        </item>
        <item>
            <title><![CDATA[Elasticsearch 인덱스에서 복제본 수를 구성하는 방법]]></title>
            <description><![CDATA[Elasticsearch 인덱스에서 검색 성능을 개선하고 노드 장애에 대한 복원력을 제공하기 위해 number_of_replicas를 구성하는 방법을 알아보세요. 
]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/elasticsearch-index-number-of_replicas</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/elasticsearch-index-number-of_replicas</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <pubDate>Wed, 14 May 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/23b96aaa1a38b1f4747b4a87695d816f24c0cf70-720x421.jpg?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>Elasticsearch는 대량의 데이터를 처리하고 고가용성을 제공할 수 있는 분산 시스템으로 설계되었습니다. 이를 가능하게 하는 핵심 기능 중 하나는 <code>number_of_replicas</code> 설정으로 제어되는 인덱스 복제 개념입니다. 이 문서에서는 이 설정의 세부 사항과 그 의미, 올바르게 구성하는 방법에 대해 자세히 설명합니다.</p><h2>Elasticsearch에서 복제본의 역할</h2><p>Elasticsearch에서 인덱스는 여러 기본 샤드에 걸쳐 분할된 문서의 모음입니다. 각 기본 샤드는 독립적인 Apache Lucene 인덱스이며, 인덱스 내의 문서는 모든 기본 샤드에 분산되어 있습니다. 고가용성과 데이터 이중화를 보장하기 위해 Elasticsearch는 각 샤드에 복제본이라고 하는 하나 이상의 복사본을 가질 수 있도록 합니다.<br/><br/><code>number_of_replicas</code> 설정은 인덱스의 각 기본 샤드에 대해 Elasticsearch가 생성하는 복제 샤드(복사본)의 수를 제어합니다. 기본적으로 Elasticsearch는 각 기본 샤드에 대해 하나의 복제본을 생성하지만 시스템의 요구 사항에 따라 변경할 수 있습니다.</p><h2>수_오브_복제본 구성하기</h2><p><code>number_of_replicas</code> 설정은 인덱스 생성 시 구성하거나 나중에 업데이트할 수 있습니다. 인덱스 생성 중에 설정하는 방법은 다음과 같습니다:</p><pre><code>PUT /my_index
{
  "settings": {
    "number_of_replicas": 2
  }
}</code></pre><p>이 예제에서 Elasticsearch는 <code>my_index</code> 인덱스의 각 기본 샤드에 대해 두 개의 복제본을 생성합니다.</p><p>기존 인덱스에 대한 <code>number_of_replicas</code> 설정을 업데이트하려면 <code>_settings</code> API를 사용하면 됩니다:</p><pre><code>PUT /my_index/_settings
{
  "number_of_replicas": 3
}</code></pre><p>이 명령은 <code>my_index</code> 인덱스를 업데이트하여 각 기본 샤드에 대해 3개의 복제본을 갖도록 합니다.</p><h2>number_of_replicas 설정의 의미</h2><p><code>number_of_replicas</code> 설정은 Elasticsearch <a href="https://opster.com/guides/elasticsearch/glossary/elasticsearch-cluster/">클러스터의</a> 성능과 복원력에 상당한 영향을 미칩니다. 다음은 고려해야 할 몇 가지 핵심 사항입니다:</p><ol><li><strong>데이터 중복성 및 가용성:</strong> <code>number_of_replicas</code> 을 늘리면 각 샤드의 복사본을 더 많이 생성하여 데이터의 가용성이 향상됩니다. 노드에 장애가 발생해도 Elasticsearch는 나머지 <a href="https://opster.com/guides/elasticsearch/glossary/elasticsearch-node/">노드에</a> 있는 복제본 샤드의 데이터를 계속 제공할 수 있습니다.</li><li><strong>검색 성능:</strong> 복제본 샤드는 읽기 요청을 처리할 수 있으므로 복제본이 많으면 더 많은 샤드에 부하를 분산하여 검색 성능을 향상시킬 수 있습니다.</li></ol><div style="display:none">Unknown block type "block", specify a component for it in the `components.types` option</div><ol><li><strong>스토리지 요구 사항:</strong> 복제본이 많을수록 더 많은 저장 공간이 필요합니다. 클러스터에 추가 복제본을 저장할 수 있는 충분한 용량이 있는지 확인해야 합니다.</li><li><strong>노드 장애에 대한 복원력:</strong> <code>number_of_replicas</code> 은 클러스터의 노드 수를 고려하여 설정해야 합니다. <code>number_of_replicas</code> 이 노드 수보다 크면 클러스터는 데이터 손실 없이 여러 노드의 장애를 견딜 수 있습니다.</li></ol><h2>number_of_replicas 설정 모범 사례</h2><p>최적의 <code>number_of_replicas</code> 설정은 시스템의 특정 요구 사항에 따라 다릅니다. 그러나 다음은 몇 가지 일반적인 모범 사례입니다:</p><ul><li>단일 노드 클러스터의 경우, 복제본을 보관할 다른 노드가 없으므로 <code>number_of_replicas</code> 을 0으로 설정해야 합니다.</li><li>멀티노드 클러스터의 경우 데이터 중복성과 고가용성을 보장하려면 <code>number_of_replicas</code> 을 최소 1로 설정해야 합니다.</li><li>검색 성능이 우선순위라면 <code>number_of_replicas</code> 을 늘리는 것이 좋습니다. 하지만 쓰기 성능 및 스토리지 요구 사항과의 절충점을 염두에 두어야 합니다.</li><li>항상 클러스터에 추가 복제본을 저장할 수 있는 충분한 용량이 있는지 확인하세요.</li></ul>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[임베딩을 Elasticsearch 필드 유형에 매핑하기: semantic_text, dense_vector, sparse_vector]]></title>
            <description><![CDATA[semantic_text, dense_vector 또는 sparse_vector를 사용하는 방법과 시기, 그리고 임베딩 생성과의 관계에 대해 논의합니다.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/mapping-embeddings-to-elasticsearch-field-types</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/mapping-embeddings-to-elasticsearch-field-types</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <pubDate>Tue, 13 May 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/f98fdff837db55b466780c0bae672aa6f6c3a966-1200x628.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>정보 검색의 관련성과 정확성을 높이기 위한 임베딩의 사용은 지난 몇 년 동안 크게 증가했습니다. Elasticsearch와 같은 도구는 밀집 벡터, 희소 벡터, 시맨틱 텍스트와 같은 특수한 필드 유형을 통해 이러한 유형의 데이터를 지원하도록 발전해 왔습니다. 그러나 좋은 결과를 얻으려면 임베딩을 사용 가능한 Elasticsearch 필드 유형에 올바르게 매핑하는 방법을 이해하는 것이 필수적입니다: <code>semantic_text</code>, <code>dense_vector</code>, 및 <code>sparse_vector</code> 을 참조하세요.</p><p>이 문서에서는 이러한 필드 유형, 각 필드 유형이 언제 사용되는지, 색인 및 쿼리 중 임베딩 생성 및 사용 전략과 어떻게 연관되는지에 대해 설명합니다.</p><h2>고밀도 벡터 유형</h2><p>Elasticsearch의 <code>dense_vector</code> 필드 유형은 거의 모든 차원이 관련된 텍스트, 이미지, 오디오와 같은 데이터의 숫자 표현인 고밀도 벡터를 저장하는 데 사용됩니다. 이러한 벡터는 OpenAI, Cohere 또는 Hugging Face와 같은 플랫폼에서 제공하는 임베딩 모델을 사용하여 생성되며, 다른 문서와 정확한 용어를 공유하지 않더라도 데이터의 전체적인 의미적 의미를 포착하도록 설계되었습니다.</p><p>Elasticsearch에서 고밀도 벡터는 사용되는 모델에 따라 최대 4096개의 차원을 가질 수 있습니다. 예를 들어, 모든 MiniLM-L6-v2 모델은 384차원의 벡터를 생성하는 반면, OpenAI의 텍스트 임베딩-ada-002는 1536차원의 벡터를 생성합니다.</p><p><code>dense_vector</code> 필드는 사전 생성된 벡터를 사용하거나 사용자 정의 유사성 함수를 적용하거나 외부 모델과 통합하는 등 보다 강력한 제어가 필요한 경우 이러한 종류의 임베딩을 저장하는 기본 유형으로 일반적으로 채택됩니다.</p><h3>dense_vector 유형은 언제, 왜 사용하나요?</h3><p>고밀도 벡터는 문장, 단락 또는 전체 문서 간의 의미적 유사성을 포착하는 데 탁월합니다. 같은 용어가 아니더라도 텍스트의 전체적인 의미를 비교하는 것이 목표일 때 매우 효과적입니다.</p><p>고밀도 벡터 필드는 OpenAI, Cohere 또는 Hugging Face와 같은 플랫폼에서 제공하는 모델을 사용하는 외부 임베딩 생성 파이프라인이 이미 있고 이러한 벡터를 수동으로만 저장하고 쿼리하려는 경우에 이상적입니다. 이 유형의 필드는 임베딩 모델과의 호환성이 높고 생성 및 쿼리에서 완전한 유연성을 제공하므로 검색 중에 벡터를 생성, 색인 및 사용하는 방법을 제어할 수 있습니다.</p><p>또한 순위 로직을 조정해야 하는 경우를 위해 k-NN 또는 script_score와 같은 쿼리를 사용하여 다양한 형태의 시맨틱 검색을 지원합니다. 이러한 가능성으로 인해 고밀도 벡터는 검색 증강 세대(RAG), 추천 시스템, 유사도에 기반한 개인화된 검색과 같은 애플리케이션에 이상적입니다.</p><p>마지막으로 이 필드에서는 <code>cosineSimilarity</code>, <code>dotProduct</code> 또는 <code>l2norm</code> 와 같은 기능을 사용하여 관련성 로직을 사용자 지정하여 사용 사례의 필요에 따라 순위를 조정할 수 있습니다. </p><p>고밀도 벡터는 위에서 언급한 고급 사용 사례와 같은 유연성, 사용자 지정 및 호환성이 필요한 사용자에게 여전히 최고의 옵션입니다.</p><h3>고밀도 벡터 유형에 쿼리를 사용하는 방법은 무엇인가요?</h3><p><strong><code>dense_vector</code></strong> 로 정의된 필드에 대한 검색은 K-최근 이웃 쿼리를 사용합니다. 이 쿼리는 밀도 벡터가 쿼리 벡터에 가장 가까운 문서를 찾는 작업을 담당합니다. 다음은 고밀도 벡터 필드에 k-NN 쿼리를 적용하는 방법의 예시입니다:</p><pre><code>{
  "knn": {
    "field": "my_dense_vector",
    "k": 10,
    "num_candidates": 50,
    "query_vector": [/* vector generated by model */]
  }
}</code></pre><p>k-NN 쿼리 외에도 문서 점수를 사용자 정의할 필요가 있는 경우, 스크립트_스코어 쿼리를 사용하여 <strong>코사인 유사도, dotProduct 또는 l2norm과</strong> 같은 벡터 비교 함수와 결합하여 보다 제어된 방식으로 관련성을 계산할 수도 있습니다. 예시를 참조하세요:</p><pre><code>{
"script_score": {
    "query": { "match_all": {} },
    "script": {
      "source": "cosineSimilarity(params.query_vector,
'my_dense_vector') + 1.0",
      "params": {
        "query_vector": [/* vector */]
      }
    }
  }
}</code></pre><p>더 자세히 알아보고 싶으시다면 <a href="https://www.elastic.co/search-labs/blog/vector-search-set-up-elasticsearch">Elasticsearch에서 벡터 검색을 설정하는 방법</a>문서를 살펴보는 것을 추천합니다.</p><p></p><h2>희소 벡터 유형</h2><p><strong><code>sparse_vector</code></strong> 필드 유형은 대부분의 값이 0이고 일부 용어에만 가중치가 있는 숫자 표현인 스파스 벡터를 저장하는 데 사용됩니다. 이 유형의 벡터는 SPLADE 또는 ELSER(Elastic Learned Sparse EncodeR)와 같은 용어 기반 모델에서 흔히 볼 수 있습니다.</p><h3>희소 벡터 유형을 언제, 왜 사용해야 하나요?</h3><p>스파스 벡터는 의미론적 지능을 유지하면서 어휘를 보다 정밀하게 검색해야 할 때 이상적입니다. 토큰/값 쌍으로 텍스트를 표시하고 관련 가중치가 있는 가장 관련성이 높은 용어만 강조 표시하여 명확성, 제어 및 효율성을 제공합니다.</p><p>이 유형의 필드는 텍스트에서 상대적 중요도에 따라 각 토큰에 다른 가중치를 할당하는 ELSER 또는 SPLADE 모델과 같이 용어를 기반으로 벡터를 생성할 때 특히 유용합니다.</p><p>쿼리에서 특정 단어의 영향력을 제어하려는 경우 희소 벡터 유형을 사용하면 용어의 가중치를 수동으로 조정하여 결과의 순위를 최적화할 수 있습니다.</p><p>주요 이점으로는 문서가 관련성이 있는 것으로 간주된 이유를 명확하게 이해할 수 있으므로 검색의 투명성과 모든 차원을 저장하는 고밀도 벡터와 달리 0이 아닌 값을 가진 토큰만 저장되므로 저장 효율성이 높다는 점이 있습니다.</p><p>또한, 스파스 벡터는 하이브리드 검색 전략에서 이상적인 보완재이며, 밀도 벡터와 결합하여 어휘 정확도와 의미 이해를 결합할 수도 있습니다.</p><h3>희소 벡터 유형에 쿼리를 사용하는 방법은 무엇인가요?</h3><p><strong><code>sparse_vector</code></strong> 쿼리를 사용하면 토큰/값 형식의 쿼리 벡터를 기반으로 문서를 검색할 수 있습니다. 아래 쿼리 예시를 참조하세요:</p><pre><code>{
  "query": {
    "sparse_vector": {
      "field": "field_sparse",
      "query_vector": {
        "token1": 0.6,
        "token2": 0.2,
        "token3": 0.9
      }
    }
  }
}</code></pre><p>학습된 모델을 사용하려는 경우 쿼리 텍스트를 스파스 벡터로 자동 변환하는 추론 엔드포인트를 사용할 수 있습니다:</p><pre><code>{
  "query": {
    "sparse_vector": {
      "field": "field_sparse",
      "inference_id": "the inference ID to produce the token/weights",
      "query": "search text"
    }
  }
}</code></pre><p>이 주제를 더 자세히 알아보려면 <a href="https://www.elastic.co/search-labs/blog/sparse-vector-embedding">학습된 ML 모델을 사용한 희소 벡터 임베딩 이해를</a> 읽어보시기 바랍니다.</p><h2>시맨틱 텍스트 유형</h2><p><strong><code>semantic_text</code></strong> 필드 유형은 Elasticsearch에서 시맨틱 검색을 사용하는 가장 간단하고 직관적인 방법입니다. 추론 엔드포인트를 통해 인덱싱 및 쿼리 시점에 임베딩 생성을 자동으로 처리합니다. 즉, 벡터를 수동으로 생성하거나 저장하는 것에 대해 걱정할 필요가 없습니다.</p><h3>시맨틱 텍스트는 언제, 왜 사용해야 하나요?</h3><p><code>semantic_text</code> 필드는 벡터를 수동으로 처리할 필요 없이 최소한의 기술적 노력으로 시작하고 싶은 분들에게 이상적입니다. 이 필드에서는 임베딩 생성 및 벡터 검색 매핑과 같은 단계를 자동화하여 더 빠르고 편리하게 설정할 수 있습니다.</p><p><strong>매핑, 임베딩 생성 및 수집 파이프라인을 수동으로 구성해야</strong> <strong>하는 복잡성을</strong> 제거하므로 단순성과 추상화를 중시하는 경우 사용을 고려해야 합니다.<code>semantic_text</code> 추론 모델을 선택하기만 하면 나머지는 Elasticsearch가 알아서 처리합니다.</p><p>주요 장점으로는 인덱싱과 쿼리 중에 수행되는 <strong>자동 임베딩 생성과</strong> 선택한 추론 모델을 지원하도록 사전 구성되어 <strong>바로 사용할 수 있는 매핑이</strong> 있습니다.</p><p>또한 이 필드에서는 <strong>긴 텍스트의 자동 분할(텍스트 청킹)을 기본적으로 지원하여</strong> 큰 텍스트를 각각 임베딩된 작은 구절로 나눌 수 있으므로 검색 정확도가 향상됩니다. 이는 특히 시맨틱 검색의 기본 엔지니어링을 다루지 않고도 빠르게 가치를 제공하고자 하는 팀의 생산성을 크게 향상시킵니다.</p><p>하지만 <code>semantic_text</code> 은 속도와 간편함을 제공하지만 이 접근 방식에는 몇 가지 한계가 있습니다. 시장 표준 모델을 사용할 수 있으며, Elasticsearch에서 추론 엔드포인트로 사용할 수 있는 한 사용할 수 있습니다. 그러나 <code>dense_vector</code> 필드에서 가능한 것처럼 <strong>외부에서 생성된 임베딩은 지원하지 않습니다</strong>.</p><p>벡터 생성 방식을 더 잘 제어하고 싶거나, 자체 임베딩을 사용하거나, 고급 전략을 위해 여러 필드를 결합해야 하는 경우 <code>dense_vector</code> 및 <code>sparse_vector</code> 필드는 보다 맞춤화된 또는 도메인별 시나리오에 필요한 유연성을 제공합니다.</p><h3>시맨틱 텍스트 유형에 쿼리를 사용하는 방법</h3><p><strong><code>semantic_text</code></strong> 이전에는 임베딩 유형(밀도형 또는 희소형)에 따라 다른 쿼리를 사용해야 했습니다. 희소 필드에는 <code>sparse_vector</code> 쿼리가 사용되었고, <code>dense_vector</code> 필드에는 KNN 쿼리가 필요했습니다.</p><p>시맨틱 텍스트 유형에서는 쿼리 벡터를 자동으로 생성하고 색인된 문서의 임베딩과 비교하는 <a href="https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-semantic-query">시맨틱 쿼리를</a> 사용하여 검색을 수행합니다. <strong><code>semantic_text</code></strong> 유형을 사용하면 쿼리를 포함할 추론 엔드포인트를 정의할 수 있지만, 아무것도 지정하지 않으면 인덱싱 중에 사용된 것과 동일한 엔드포인트가 쿼리에 적용됩니다.</p><pre><code>{
  "query": {
    "semantic": {
      "field": "semantic_text_field",
      "query": "search text"
    }
  }
}</code></pre><p>자세한 내용은 <a href="https://www.elastic.co/search-labs/blog/semantic-search-simplified-semantic-text">Elasticsearch의 새로운 semantic_text 매핑 문서를 읽어보시기 바랍니다: 시맨틱 검색</a> 간소화.</p><h2>결론</h2><p>Elasticsearch에서 임베딩을 매핑하는 방법을 선택할 때는 벡터를 생성하는 방법과 벡터에 대해 필요한 제어 수준을 이해하는 것이 중요합니다. 시맨틱 텍스트 필드를 사용하면 자동 및 확장 가능한 시맨틱 검색이 가능하므로 많은 초기 사용 사례에 이상적입니다. 더 많은 제어, 미세 조정된 성능 또는 사용자 지정 모델과의 통합이 필요한 경우 고밀도 벡터 및 희소 벡터 필드는 필요한 유연성을 제공합니다.</p><p>이상적인 필드 유형은 사용 사례, 사용 가능한 인프라, 머신 러닝 스택의 성숙도에 따라 달라집니다. 가장 중요한 것은 Elastic이 현대적이고 적응력이 뛰어난 검색 시스템을 구축할 수 있는 도구를 제공한다는 점입니다.</p><h2>참고 자료</h2><ul><li><a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/semantic-text.html">시맨틱 텍스트 필드 유형</a></li><li><a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/sparse-vector.html">희소 벡터 필드 유형</a></li><li><a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/dense-vector.html">고밀도 벡터 필드 유형</a></li><li><a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-semantic-query.html">시맨틱 쿼리</a></li><li><a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-sparse-vector-query.html">희소 벡터 쿼리</a></li><li><a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/knn-search.html">kNN 검색</a></li><li><a href="https://www.elastic.co/search-labs/blog/semantic-search-simplified-semantic-text">Elasticsearch의 새로운 의미론적 텍스트 매핑: 시맨틱 검색 간소화</a></li><li><a href="https://www.elastic.co/search-labs/blog/sparse-vector-embedding">학습된 ML 모델을 사용한 스파스 벡터 임베딩 이해하기</a></li></ul>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[색인에서 Elasticsearch 필드 제외하기]]></title>
            <description><![CDATA[필드를 제외하도록 Elasticsearch를 구성하는 방법, 색인에서 필드를 제외하는 주요 이유, 따라야 할 모범 사례에 대해 알아보세요.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/excluding-elasticsearch-fields-from-indexing</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/excluding-elasticsearch-fields-from-indexing</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <pubDate>Mon, 12 May 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/3aa0b481994d2445ba979d3c79fff64c5ee6676a-720x420.jpg?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>Elasticsearch에서 인덱싱은 데이터를 쉽게 검색할 수 있는 방식으로 저장하고 구성하는 프로세스를 말합니다. 문서의 모든 필드를 색인하는 것이 경우에 따라 유용할 수 있지만, 특정 필드를 색인 대상에서 제외해야 하는 상황이 있을 수 있습니다. 이렇게 하면 성능을 개선하고, 스토리지 비용을 절감하고, Elasticsearch 인덱스의 전체 크기를 최소화하는 데 도움이 됩니다.</p><p>이 문서에서는 색인에서 필드를 제외하는 이유, 특정 필드를 제외하도록 Elasticsearch를 구성하는 방법, 그리고 그렇게 할 때 따라야 할 몇 가지 모범 사례에 대해 설명합니다.</p><h2>인덱싱에서 필드를 제외하는 이유</h2><ol><li><strong>성능: </strong>문서의 모든 필드를 색인하면 색인 시간이 길어지고 검색 성능이 저하될 수 있습니다. 검색이나 집계에 필요하지 않은 필드를 제외하면 Elasticsearch 클러스터의 전반적인 성능을 개선할 수 있습니다.</li><li><strong>저장소: </strong>필드 인덱싱은 저장 공간을 소모합니다. 검색이나 집계에 필요하지 않은 필드를 제외하면 Elasticsearch 클러스터의 저장 공간 요구 사항을 줄이는 데 도움이 될 수 있습니다.</li><li><strong>인덱스 크기: </strong>Elasticsearch 인덱스의 크기는 색인되는 필드의 수와 직접적으로 관련이 있습니다. 불필요한 필드를 제외하면 색인 크기를 최소화할 수 있어 검색 및 색인 성능이 향상될 수 있습니다.</li></ol><h2>필드를 제외하도록 Elasticsearch 구성하기</h2><p>Elasticsearch에서 필드를 색인되지 않도록 제외하려면 필드 매핑에서 &quot;index&quot; 속성을 사용하면 됩니다. &quot;index&quot; 속성을 &quot;false&quot;로 설정하면 Elasticsearch는 필드를 색인하지 않으며, 검색하거나 집계에 사용할 수 없게 됩니다.</p><p>다음은 Elasticsearch 매핑을 사용하여 필드를 색인에서 제외하는 방법의 예입니다:</p><pre><code>PUT /my_index
{
  "mappings": {
    "properties": {
      "field_to_exclude": {
        "type": "text",
        "index": false
      }
    }
  }
}</code></pre><p>이 예에서는 &quot;field_to_exclude&quot;라는 단일 필드를 사용하여 &quot;my_index&quot;라는 새 인덱스를 생성합니다. &quot;index&quot; 속성을 &quot;false&quot;로 설정하면, 이 필드를 색인하지 않도록 Elasticsearch에 지시하는 것입니다. 하지만 이 필드는 소스 문서에서 계속 사용할 수 있습니다.</p><h2>인덱싱에서 필드를 제외하는 모범 사례</h2><ol><li><strong>데이터 분석하기: </strong>인덱싱에서 필드를 제외하기 전에 데이터를 분석하고 검색 및 집계에 필요한 필드를 파악하는 것이 중요합니다. 이를 통해 제외할 필드에 대해 정보에 입각한 결정을 내릴 수 있습니다.</li><li><strong>변경 사항을 테스트합니다: </strong>인덱싱에서 필드를 제외할 때는 변경 사항을 테스트하여 검색 및 집계 기능이 여전히 예상대로 작동하는지 확인하는 것이 중요합니다. 이렇게 하면 예기치 않은 문제나 성능 문제를 방지하는 데 도움이 됩니다.</li><li><strong>성능 모니터링:</strong> 색인에서 필드를 제외시킨 후, Elasticsearch 클러스터의 성능을 모니터링하여 변경 사항이 원하는 효과를 가져왔는지 확인합니다. 이를 통해 필요한 추가 최적화를 파악하는 데 도움이 될 수 있습니다.</li><li><strong>소스 필터링 사용:</strong> Elasticsearch에 필드를 저장해야 하지만 검색 가능하거나 집계에 사용할 수 없도록 하려면 소스 필터링을 사용하는 것을 고려하세요. 이렇게 하면 _source 필드에 필드를 저장하되 인덱스에서 제외할 수 있습니다.</li></ol><h2>결론</h2><p>Elasticsearch에서 색인에서 필드를 제외하면 성능을 개선하고, 저장 비용을 절감하며, 전체 색인 크기를 최소화하는 데 도움이 될 수 있습니다. 데이터를 신중하게 분석하고 검색 및 집계에 필요한 필드를 이해하면 어떤 필드를 제외할지 정보에 입각한 결정을 내릴 수 있습니다. 항상 변경 사항을 테스트하고 Elasticsearch 클러스터의 성능을 모니터링하여 최적화가 원하는 효과를 가져오는지 확인하세요.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Elasticsearch의 문서에서 필드 삭제하기]]></title>
            <description><![CDATA[Elasticsearch 문서에서 필드를 삭제하는 방법을 알아보세요. Update API, 스크립트, 또는 reindex를 활용해 단일 항목 삭제 또는 일괄 삭제를 수행할 수 있습니다.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/elasticsearch-delete-field-from-document</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/elasticsearch-delete-field-from-document</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <pubDate>Fri, 09 May 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/89278eb7309b7f3018c61be2b514d1fd25b9564d-720x420.jpg?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>Elasticsearch에서는 문서에서 필드를 삭제하는 것이 일반적인 요구 사항입니다. 이 기능은 색인에서 불필요하거나 오래된 정보를 제거하려는 경우에 유용할 수 있습니다. 이 문서에서는 Elasticsearch의 문서에서 필드를 삭제하는 다양한 방법과 예제 및 단계별 지침에 대해 설명합니다. </p><h2>방법 1: 업데이트 API 사용</h2><p><a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/update-document">업데이트 API</a>를 사용하면 문서의 소스를 수정하는 스크립트를 제공하여 문서를 업데이트할 수 있습니다. 이 API를 사용하면 필드를 null로 설정하여 문서에서 필드를 삭제하실 수 있습니다. 다음은 이 작업을 수행하는 방법에 대한 단계별 안내입니다.</p><p>1. 업데이트하려는 문서의 인덱스, 문서 유형(Elasticsearch 6.x 이하를 사용하는 경우), 문서 ID를 식별합니다.</p><p>2. 필드를 null로 설정하거나 더 나아가 소스 문서에서 필드를 제거하는 스크립트와 함께 업데이트 API를 사용합니다. 다음 예는 &quot;my_index&quot; 인덱스에서 ID가 &quot;1&quot;인 문서에서 &quot;field_to_delete&quot; 필드를 삭제하는 방법을 보여 줍니다:</p><pre><code>POST /my_index/_update/1
{
  "script": "ctx._source.remove('field_to_delete')"
}</code></pre><p>3. 요청을 실행합니다. 성공하면 Elasticsearch는 문서가 업데이트되었음을 나타내는 응답을 반환합니다.</p><p>참고: 이 메서드는 지정된 문서에서 필드만 제거합니다. 이 필드는 인덱스의 매핑 및 기타 문서에 계속 존재합니다.</p><h2>방법 2: 수정된 소스를 사용하여 재색인</h2><p>인덱스의 모든 문서에서 필드를 삭제하려면, <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-reindex">재색인 API</a>를 사용하여 수정된 소스로 새 인덱스를 생성하면 됩니다. 이를 수행하는 방법은 다음과 같습니다.</p><p>1. 원래 인덱스와 동일한 설정 및 매핑을 사용하여 새 인덱스를 만듭니다. 인덱스 가져오기 API를 사용하여 원본 인덱스의 설정과 매핑을 검색할 수 있습니다.</p><p>2. 재색인 API를 사용하여 원본 인덱스에서 새 인덱스로 문서를 복사하는 동시에 소스에서 필드를 제거합니다. 다음 예는 &quot;my_index&quot; 인덱스의 모든 문서에서 &quot;field_to_delete&quot; 필드를 삭제하는 방법을 보여 줍니다:</p><pre><code>POST /_reindex
{
  "source": {
    "index": "my_index"
  },
  "dest": {
    "index": "new_index"
  },
  "script": {
    "source": "ctx._source.remove('field_to_delete')"
  }
}</code></pre><p><br/>3. 새 인덱스에 필드가 제거된 올바른 문서가 포함되어 있는지 확인합니다.</p><p>4. 모든 것이 정상으로 보이면 원래 인덱스를 삭제하고 필요한 경우 원래 인덱스 이름의 별칭을 새 인덱스에 추가할 수 있습니다.</p><h2>방법 3: 매핑을 업데이트하고 재색인</h2><p>매핑에서 필드를 삭제하고 인덱스의 모든 문서를 삭제하려면 매핑을 업데이트한 다음 문서를 다시 색인하면 됩니다. 방법은 다음과 같습니다:</p><p>1. 원래 인덱스와 동일한 설정으로 새 인덱스를 만듭니다.</p><p>2. 매핑 가져오기 API를 사용하여 원본 인덱스의 매핑을 검색합니다.</p><p>3. 삭제하려는 필드를 제거하여 매핑을 수정합니다.</p><p>4. 매핑 넣기 API를 사용하여 수정된 매핑을 새 인덱스에 적용합니다.</p><p>5. 방법 2에 설명된 대로 재색인 API를 사용하여 원래 색인에서 새 색인으로 문서를 복사합니다.</p><p>6. 새 인덱스에 필드가 제거된 올바른 문서가 포함되어 있고 해당 필드가 매핑에 없는지 확인합니다.</p><p>7. 모든 것이 정상으로 보이면 원본 인덱스를 삭제할 수 있으며, 필요한 경우 새 인덱스에 원본 인덱스 이름의 별칭을 추가할 수 있습니다.</p><h2>결론</h2><p>이 문서에서는 업데이트 API 사용, 수정된 소스로 재색인, 매핑 업데이트 및 재색인이라는 세 가지 방법으로 Elasticsearch의 문서에서 필드를 삭제하는 방법에 대해 설명했습니다. 각 방법에는 고유한 사용 사례와 장단점이 있으므로 요구 사항에 가장 적합한 방법을 선택하세요. 변경 사항을 프로덕션 환경에 적용하기 전에 항상 테스트하고 결과를 확인하는 것을 잊지 마세요.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Elasticsearch에서 두 인덱스를 조인하는 방법]]></title>
            <description><![CDATA[Elasticsearch에서 두 인덱스를 결합하기 위해 쿼리, Logstash elasticsearch 필터, enrich 프로세서 및 ES|QL이라는 용어를 사용하는 방법을 설명합니다.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/elasticsearch-join-two-indexes</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/elasticsearch-join-two-indexes</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Carly Richmond]]></dc:creator>
            <pubDate>Wed, 07 May 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/0d4736d10fa3e12e6233cd59993299c7bd48911b-680x450.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>Elasticsearch에서 두 인덱스를 조인하는 것은 기존 SQL 관계형 데이터베이스에서처럼 간단하지 않습니다. 그러나 Elasticsearch에서 제공하는 특정 기술과 기능을 사용하여 유사한 결과를 얻을 수 있습니다.</p><p>이전에는 많은 사람들이 서로 다른 인덱스를 결합하는 메커니즘으로 <a href="https://www.elastic.co/kr/docs/reference/elasticsearch/mapping-reference/nested"><code>nested</code> 필드 유형을</a> 사용했습니다. 그러나 비용이 많이 드는 쿼리와 Kibana의 불완전한 지원, 특히 Lens 시각화로 인해 제한적이었습니다.</p><p>이 문서에서는 다음 접근 방식에 초점을 맞춰 Elasticsearch에서 두 인덱스를 결합하는 프로세스를 자세히 설명합니다: </p><ol><li><code>terms</code> 쿼리 사용</li><li>수집 파이프라인에서 <code>enrich</code> 프로세서 사용</li><li>Logstash <code>elasticsearch</code> 필터 플러그인</li><li>ES|QL <code>ENRICH</code></li><li>ES|QL <code>LOOKUP JOIN</code></li></ol><h2>용어 쿼리 사용</h2><p><a href="https://www.elastic.co/kr/docs/reference/query-languages/query-dsl/query-dsl-terms-query">용어 쿼리는</a> Elasticsearch에서 두 인덱스를 결합하는 가장 효과적인 방법 중 하나입니다. 이 쿼리는 특정 필드에 하나 이상의 정확한 용어가 포함된 문서를 검색하는 데 사용됩니다. 여기서는 두 인덱스를 조인하는 데 사용하는 방법에 대해 설명합니다.</p><p>먼저 첫 번째 인덱스에서 필요한 데이터를 검색해야 합니다. 이는 간단한 GET 요청을 사용하여 <code>_source</code> 속성에서 값을 가져와서 수행할 수 있습니다.</p><pre><code># Simple GET request
GET first_index/_search</code></pre><p>첫 번째 인덱스의 데이터를 확보하면 이를 사용하여 두 번째 인덱스를 쿼리할 수 있습니다. <code>terms</code> 쿼리를 사용하여 일치시킬 필드와 값을 지정합니다.</p><p>다음은 그 예입니다:</p><pre><code>GET second_index/_search
{
  "query": {
    "terms": {
      "field_in_second_index": ["value1_from_first_index", "value2_from_first_index"]
    }
  }
}</code></pre><p><br/>이 예에서 <code>field_in_second_index</code> 은 두 번째 인덱스에서 첫 번째 인덱스의 값과 일치시키려는 필드입니다. <code>value1_from_first_index</code> 와 <code>value2_from_first_index</code> 는 두 번째 인덱스에서 일치시키려는 첫 번째 인덱스의 값입니다.</p><p><a href="https://www.elastic.co/kr/docs/reference/query-languages/query-dsl/query-dsl-terms-query#query-dsl-terms-lookup">용어 조회는 용어 조회라는</a> 기술을 사용하여 위의 두 단계를 한 번에 수행할 수 있는 기능도 지원합니다. Elasticsearch는 다른 인덱스에서 일치할 값을 투명하게 검색하는 작업을 처리합니다. 예를 들어 선수 목록이 포함된 팀 색인이 있는 경우입니다:</p><pre><code>PUT teams/_doc/team1
{
  "players":   ["john", "bill", "michael"]
}
PUT teams/_doc/team2
{
  "players":   ["aaron", "joe", "donald"]
}</code></pre><p>아래와 같이 팀1에서 플레이하는 모든 사람에 대한 사람 인덱스를 쿼리할 수 있습니다:</p><pre><code>GET people/_search?pretty
{
  "query": {
    "terms": {
        "name" : {
            "index" : "teams",
            "id" : "team1",
            "path" : "players"
        }
    }
  }
}</code></pre><p>위의 예에서, Elasticsearch는 팀 인덱스에서 id가 team1인 문서에서 선수 이름을 투명하게 검색합니다(즉, 다음과 같이). &quot;john&quot;, &quot;bill&quot;, &quot;michael&quot;)을 검색하여 이름 필드에 이러한 값이 포함된 모든 문서를 사람 색인에서 찾습니다.</p><p>궁금하신 분들을 위해 동등한 SQL 쿼리는 다음과 같습니다:</p><pre><code>SELECT p.* FROM people p
INNER JOIN teams t ON p.name = t.players</code></pre><h2>인리치 프로세서 사용</h2><p><a href="https://www.elastic.co/kr/docs/reference/enrich-processor/enrich-processor"><code>enrich</code> 프로세서는</a> Elasticsearch에서 두 개의 인덱스를 조인하는 데 사용할 수 있는 또 다른 강력한 도구입니다. 이 프로세서는 미리 정의된 보강 색인에서 데이터를 추가하여 수신 문서의 데이터를 보강합니다.</p><p>인리치 프로세서를 사용하여 두 인덱스를 결합하는 방법은 다음과 같습니다:</p><p>1. 먼저 인라이크 정책을 만들어야 합니다. 이 정책은 보강에 사용할 인덱스, 일치시킬 필드, 들어오는 문서를 보강하는 데 사용할 필드를 정의합니다.</p><p>다음은 그 예입니다:</p><pre><code>PUT _enrich/policy/my_enrich_policy
{
  "match": {
    "indices": "first_index",
    "match_field": "field_in_first_index",
    "enrich_fields": ["field_to_enrich"]
  }
}</code></pre><p>2. 정책이 생성되면 정책을 실행하여 새로 생성한 정책에서 색인 강화 인덱스를 생성해야 합니다:</p><pre><code>PUT _enrich/policy/my_enrich_policy/_execute</code></pre><p>이렇게 하면 강화하는 동안 사용될 새로운 숨겨진 강화 인덱스가 생성됩니다. 소스 인덱스의 크기에 따라 이 작업에는 다소 시간이 걸릴 수 있습니다. 다음 단계로 진행하기 전에 인라이크 정책이 완전히 구축되었는지 확인하세요.</p><p>3. 강화 정책이 구축된 후에는 수집 파이프라인에서 강화 프로세서를 사용하여 들어오는 문서의 데이터를 강화할 수 있습니다:</p><pre><code>PUT _ingest/pipeline/my_pipeline
{
  "processors": [
    {
      "enrich": {
        "policy_name": "my_enrich_policy",
        "field": "field_in_second_index",
        "target_field": "enriched_field"
      }
    }
  ]
}</code></pre><p>이 예에서 <code>field_in_second_index</code> 은 첫 번째 인덱스의 <code>match_field</code> 과 일치해야 하는 두 번째 인덱스의 필드입니다. <code>enriched_field</code> 는 첫 번째 인덱스의 <code>enrich_fields</code> 에서 보강된 데이터를 포함할 두 번째 인덱스의 새 필드입니다.</p><p>이 접근 방식의 한 가지 단점은 <code>first_index</code> 에서 데이터가 변경되면 보강 정책을 다시 실행해야 한다는 것입니다. 보강된 인덱스는 구축된 소스 인덱스에서 자동으로 업데이트되거나 동기화되지 않습니다. 하지만 <code>first_index</code> 이 비교적 안정적이라면 이 접근 방식이 잘 작동합니다.</p><h2>Logstash 엘라스틱서치 필터 플러그인</h2><p>Logstash를 사용하는 경우, 위에서 설명한 <code>enrich</code> 프로세서와 유사한 또 다른 옵션은 <code>elasticsearch</code> 필터 플러그인을 사용하여 지정된 쿼리를 기반으로 이벤트에 관련 필드를 추가하는 것입니다. Logstash 파이프라인의 구성은 <code>my-pipeline.conf</code> 과 같은 <code>.conf</code> 파일에 저장됩니다.</p><p>우리의 파이프라인이 <a href="https://www.elastic.co/kr/docs/reference/logstash/plugins/plugins-inputs-elasticsearch"><code>elasticsearch</code> 입력 플러그인을</a> 사용하여 Elasticsearch에서 로그를 가져오고, 선택 범위를 좁히기 위한 쿼리를 사용한다고 가정해 보겠습니다:</p><pre><code>input {
  # Read all documents from Elasticsearch matching the given query
  elasticsearch {
    hosts => "localhost"
    query => '{ "query": { "match": { "statuscode": 200 } }, "sort": [ "_doc" ] }'
  }
}</code></pre><p>주어진 인덱스의 정보로 이러한 메시지를 보강하려면 <code>filter</code> 섹션의 <a href="https://www.elastic.co/kr/docs/reference/logstash/plugins/plugins-filters-elasticsearch"><code>elasticsearch</code> 필터 플러그인을</a> 사용하여 로그를 보강할 수 있습니다:</p><pre><code>filter {
   elasticsearch {
      hosts => ["localhost"]
      index => "index_name"
      query => "type:start AND operation:%{[opid]}"
      fields => { "@timestamp" => "started" }
   }
}</code></pre><p>위의 코드는 인덱스 <code>index_name</code> 에서 <code>type</code> 이 시작이고 작업 필드가 지정된 <code>opid</code> 과 일치하는 문서를 찾은 다음 <code>@timestamp</code> 필드의 값을 <code>started</code> 라는 새 필드에 복사합니다 .</p><p>그런 다음 보강된 문서는 적절한 출력 소스(이 경우 <a href="https://www.elastic.co/kr/docs/reference/logstash/plugins/plugins-outputs-elasticsearch"><code>elasticsearch</code> 출력 플러그인을</a> 사용하여 Elasticsearch로)로 전송됩니다:</p><pre><code>output {
    elasticsearch {
        hosts => "localhost"
        data_stream => "true"
    }
}</code></pre><p>이미 Logstash를 사용하고 있는 경우, 이 옵션은 보강 로직을 한 곳에 통합하고 새로운 이벤트가 들어올 때 처리하는 데 유용할 수 있습니다. 그러나 그렇지 않은 경우 솔루션이 복잡해지고 실행 및 유지 관리해야 하는 또 다른 구성 요소가 추가됩니다.</p><h2>ES|QL 리치</h2><p>버전 8.14에서 정식 버전으로 출시된 <a href="https://www.elastic.co/kr/docs/explore-analyze/query-filter/languages/esql">ES|QL은</a> Elasticsearch에서 지원하는 파이핑 쿼리 언어로, 데이터를 필터링, 변환, 분석할 수 있게 해줍니다. ENRICH 처리 명령을 사용하면 강화 정책을 사용하여 기존 인덱스의 데이터를 추가할 수 있습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/03aadddc08afffff3f6526c9c052999c97fa09dd-1600x989.png" style="width: 100%;" /><p>원래의 인리치 프로세서 예제에서 동일한 정책 <code>my_enrich_policy</code> 을 사용하면 ES|QL 예제는 다음과 같이 됩니다:</p><pre><code>FROM first_index
| WHERE field_in_first_index IS NOT NULL
| ENRICH my_enrich_policy</code></pre><p>이 예제에서는 각각 <code>field_in_first_index</code> 및 <code>field_to_enrich</code> 인 일치 및 보강 필드를 재정의할 수도 있습니다:</p><pre><code>FROM first_index
| WHERE field_in_first_index IS NOT NULL
| ENRICH my_enrich_policy ON another_field_in_first_index WITH different_field_to_enrich</code></pre><p>강화 정책을 먼저 지정해야 한다는 분명한 한계가 있지만, ES|QL은 필요에 따라 필드를 조정할 수 있는 유연성을 제공합니다.</p><h2>ES|QL 조회 조인</h2><p>Elasticsearch 8.18에서는 <code>LOOKUP JOIN</code> 명령이라는 새로운 인덱스 조인 방법이 도입되었습니다. 이 명령은 조인의 오른쪽에 있는 새 <a href="https://www.elastic.co/kr/docs/reference/elasticsearch/index-settings/index-modules#index-mode-setting">조회 인덱스 모드를</a> 사용하여 SQL 스타일의 LEFT OUTER JOIN으로 작동합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/1d73495979c4d6bb675c4c966ea86d9a72dc1c48-510x605.png" style="width: 100%;" /><p>이전 예제를 다시 살펴보면 새 쿼리는 다음과 같으며, <code>match_field</code> 는 <code>first_index</code> 과 <code>second_index</code> 모두에 존재해야 합니다:</p><pre><code>FROM first_index
| WHERE field_in_first_index IS NOT NULL
| LOOKUP JOIN second_index ON match_field</code></pre><p>다른 접근 방식에 비해 조회 조인의 장점은 <code>enrich</code> 정책이 필요하지 않으므로 정책 설정과 관련된 추가 처리가 필요하지 않다는 점입니다. 이 글에서 설명하는 다른 접근 방식과 달리 자주 변경되는 보강 데이터로 작업할 때 유용합니다.</p><h2>결론</h2><p>결론적으로, Elasticsearch는 전통적인 조인 작업을 지원하지는 않지만, 유사한 결과를 얻기 위해 사용할 수 있는 다양한 기능을 제공합니다. 특히 다음을 사용하여 조인 작업을 수행하는 방법에 대해 설명했습니다:</p><ol><li><code>terms</code> 쿼리</li><li>수집 파이프라인의 <code>enrich</code> 프로세서</li><li>Logstash <code>elasticsearch</code> 필터 플러그인</li><li>ES|QL <code>ENRICH</code></li><li>ES|QL <code>LOOKUP JOIN</code></li></ol><p>이러한 방법에는 한계가 있으므로 특정 요구 사항과 데이터의 특성에 따라 신중하게 사용해야 한다는 점에 유의하세요.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Elasticsearch 채점 및 설명 API 이해하기]]></title>
            <description><![CDATA[설명 API로 검색 관련성을 감사하고 문서 순위를 개선하는 Elasticsearch 채점 메커니즘과 실용적인 채점 기능에 대해 알아보세요.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/elasticsearch-scoring-and-explain-api</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/elasticsearch-scoring-and-explain-api</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <pubDate>Mon, 05 May 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/a70c5403064d5bbceff66a17373332362227f13c-720x420.jpg?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>Elasticsearch는 인덱스의 각 문서에 대해 점수를 계산하여 빠르고 관련성 높은 검색 결과를 제공하는 강력한 검색 엔진입니다. 이 점수는 검색 결과의 순서를 결정하는 데 중요한 요소입니다. 이 문서에서는 Elasticsearch의 채점 메커니즘을 살펴보고 채점 프로세스를 이해하는 데 도움이 되는 Explain API를 살펴보겠습니다.</p><h2>Elasticsearch의 점수 매기기</h2><p>Elasticsearch는 기본적으로 BM25(실용적 채점 함수)라는 채점 모델을 사용합니다. 이 모델은 확률적 정보 검색 이론을 기반으로 하며 용어 빈도, 역 문서 빈도, 필드 길이 정규화 등의 요소를 고려합니다. 이러한 요소에 대해 간략히 살펴보겠습니다:</p><ol><li><strong>용어 빈도(TF):</strong> 문서에서 용어가 나타나는 횟수를 나타냅니다. 용어 빈도가 높을수록 용어와 문서 간의 관계가 더 강하다는 것을 나타냅니다.</li><li><strong>역 문서 빈도(IDF):</strong> 이 요소는 전체 문서 컬렉션에서 한 용어의 중요도를 측정합니다. 많은 문서에 등장하는 용어는 덜 중요한 것으로 간주하고, 적은 문서에 등장하는 용어는 더 중요한 것으로 간주합니다.</li><li><strong>필드 길이 정규화</strong>: 이 요소는 용어가 표시되는 필드의 길이를 설명합니다. 짧은 필드에서는 용어가 더 중요한 것으로 간주되므로 짧은 필드에 더 많은 가중치가 부여됩니다.</li></ol><h2>설명 API 사용</h2><p>Elasticsearch의 설명 API는 채점 프로세스를 이해하는 데 유용한 도구입니다. 특정 문서의 점수가 어떻게 계산되었는지에 대한 자세한 설명을 제공합니다. 설명 API를 사용하려면 다음 엔드포인트로 GET 요청을 보내야 합니다:</p><pre><code>GET /<index>/_explain/<document_id></code></pre><p>요청 본문에는 점수를 이해하고자 하는 쿼리를 입력해야 합니다. 다음은 한 가지 예입니다:</p><pre><code>{
  "query": {
    "match": {
      "title": "elasticsearch"
    }
  }
}</code></pre><p>설명 API의 응답에는 개별 요소(TF, IDF 및 필드 길이 정규화)와 최종 점수에 대한 기여도를 포함하여 채점 프로세스에 대한 자세한 분석이 포함됩니다. 다음은 샘플 응답입니다:</p><pre><code>{
  "_index": "example_index",
  "_type": "_doc",
  "_id": "1",
  "matched": true,
  "explanation": {
    "value": 1.2,
    "description": "weight(title:elasticsearch in 0) [PerFieldSimilarity], result of:",
    "details": [
      {
        "value": 1.2,
        "description": "score(doc=0,freq=1.0 = termFreq=1.0\n), product of:",
        "details": [
          {
            "value": 2.2,
            "description": "idf, computed as log(1 + (docCount - docFreq + 0.5) / (docFreq + 0.5)) from:",
            "details": [
              {
                "value": 1,
                "description": "docFreq",
                "details": []
              },
              {
                "value": 1,
                "description": "docCount",
                "details": []
              }
            ]
          },
          {
            "value": 0.5,
            "description": "tfNorm, computed as (freq * (k1 + 1)) / (freq + k1 * (1 - b + b * fieldLength / avgFieldLength)) from:",
            "details": [
              {
                "value": 1,
                "description": "termFreq=1.0",
                "details": []
              },
              {
                "value": 1.2,
                "description": "parameter k1",
                "details": []
              },
              {
                "value": 0.75,
                "description": "parameter b",
                "details": []
              },
              {
                "value": 1,
                "description": "avgFieldLength",
                "details": []
              },
              {
                "value": 1,
                "description": "fieldLength",
                "details": []
              }
            ]
          }
        ]
      }
    ]
  }
}</code></pre><p>이 예에서 응답은 점수 1.2가 IDF 값(2.2)과 tfNorm 값(0.5)의 곱이라는 것을 보여줍니다. 자세한 설명은 점수에 기여하는 요소를 이해하는 데 도움이 되며 검색 연관성을 미세 조정하는 데 유용할 수 있습니다.</p><h2>결론</h2><p>Elasticsearch 점수는 관련성 높은 검색 결과를 제공하는 데 있어 매우 중요한 요소입니다. 채점 메커니즘을 이해하고 설명 API를 사용하면 검색 결과에 영향을 미치는 요인에 대한 인사이트를 얻고 검색어를 최적화하여 관련성과 성능을 개선할 수 있습니다.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Elasticsearch의 색인 템플릿: 작성 가능한 템플릿을 사용하는 방법]]></title>
            <description><![CDATA[Elasticsearch에서 컴포저블 및 컴포넌트 인덱스 템플릿을 생성해 일관된 매핑을 보장하며 인덱스 구성을 자동화하는 방법을 알아보세요.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/index-composable-templates</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/index-composable-templates</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <pubDate>Fri, 02 May 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/510750708df50bf79463586a1bbf35bf94acfa30-1200x628.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>매핑, 설정 및 별칭을 통해 Elasticsearch 인덱스를 구성할 수 있습니다: </p><ul><li>매핑 정의는 데이터 스키마를 지정합니다.</li><li>설정에서 샤드 크기와 새로 고침 빈도를 설정합니다. </li><li>별칭은 인덱스에 대체 이름을 지정하는 데 사용됩니다.</li></ul><p>문서를 처음 색인하거나 색인 생성 API를 사용하여 빈 색인을 만들면 데이터 스키마 및 별칭 없이 기본 설정으로 색인이 생성됩니다. 이러한 기본값은 개발 및 테스트 환경에서는 잘 작동하지만 프로덕션 환경에 맞게 인덱스를 사용자 지정해야 할 수도 있습니다.</p><p>프로덕션 환경에서 기본 매핑 및 설정으로 작업하면 색인 및 검색 성능이 저하될 수 있습니다. 인덱스를 수동으로 인스턴스화하는 작업은 지루하고 시간이 많이 걸리는 과정입니다. 정교한 매핑 스키마와 사용자 정의 설정 및 별칭이 있는 경우 모든 환경에서 이러한 인덱스를 다시 생성하는 것은 특히 비현실적입니다.</p><p>다행히도 Elasticsearch는 <em>인덱스</em> <em>템플릿</em>형태로 인덱스를 생성할 때 미리 정의된 구성을 자동으로 적용할 수 있는 도구를 제공합니다.</p><h2>색인 템플릿</h2><p>인덱스 템플릿을 사용하면 사용자 정의 구성으로 인덱스를 만들 수 있습니다. 인덱스는 인스턴스화 중에 이러한 템플릿에서 설정된 수의 샤드 및 복제본 또는 필드 매핑과 같은 구성을 가져올 수 있습니다. 템플릿은 이름 패턴과 일부 구성으로 정의됩니다. 인덱스의 이름이 템플릿의 명명 패턴과 일치하면 템플릿에 정의된 구성으로 새 인덱스가 생성됩니다.</p><p>Elasticsearch는 버전 7.8에서 구성 가능한 템플릿으로 템플릿 기능을 업그레이드했습니다. 이 최신 버전은 이 문서에서 설명한 대로 훨씬 더 많은 재사용 가능한 인덱스 템플릿을 제공합니다.</p><h3>인덱스 템플릿의 종류</h3><p>색인 템플릿은 두 가지 범주로 분류할 수 있습니다:</p><ul><li><strong>색인 템플릿(또는 컴포저블 색인 템플릿)</strong>: 구성 가능한 인덱스 템플릿은 단독으로 존재하거나 하나 이상의 구성 요소 템플릿으로 구성될 수 있습니다(두 번째 범주 참조).</li><li><strong>컴포넌트 템플릿:</strong> 컴포넌트 템플릿은 필요한 구성을 정의하는 자체적으로 <em>재사용 가능한</em> 템플릿입니다. 일반적으로 컴포넌트 템플릿은 인덱스 템플릿과 연결될 것으로 예상됩니다. 각 컴포넌트 템플릿에는 하나 또는 여러 개의 인덱스 템플릿을 첨부할 수 있습니다. </li></ul><p>아래 이미지에서 볼 수 있듯이 인덱스 템플릿 A와 B는 서로 구성 요소 템플릿(이 경우 템플릿 3 하나만)을 공유합니다. 인덱스 템플릿은 하나 또는 여러 개의 구성 요소 템플릿으로 구성될 수 있으며, 각 구성 요소 템플릿은 하나 또는 여러 개의 인덱스 템플릿과 연결될 수 있습니다. 두 가지 유형의 템플릿은 모두 단독으로 존재할 수 있지만 컴포넌트 템플릿은 인덱스 템플릿에 첨부하지 않으면 아무 소용이 없습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/96c0aac29d3992e54a79be34e14cf909e0ca2ea9-1202x556.png" style="width: 100%;" /><p>일반적인 아이디어는 조직이 다양한 필요에 따라 사용할 수 있도록 구성 요소 템플릿 카탈로그를 개발하고(예: 개별 환경에 맞는 다양한 구성 요소 템플릿 지정), 구성 가능한 인덱스 템플릿을 통해 다양한 인덱스에 이를 첨부하는 것입니다.</p><h2>작성 가능한(색인) 템플릿을 만드는 방법</h2><p>Elasticsearch는 인덱스 템플릿을 관리하기 위한 _index_template 엔드포인트를 제공합니다. 사용자는 이 템플릿에서 인덱스 이름 패턴과 함께 필요한 모든 매핑, 설정 및 별칭을 제공합니다. 주문 생성 로직을 담당하는 마이크로서비스 애플리케이션 <em>고객 주문 서비스에</em> 대한 템플릿을 만드는 예제를 살펴보겠습니다. </p><p>와일드카드가 있는 패턴으로 표시되는 고객 주문에 대한 템플릿을 만들어야 한다고 가정해 보겠습니다: *orders. 이 템플릿에는 주문_날짜 필드, 샤드 및 복제본 번호와 같은 특정 매핑 및 설정이 있을 것으로 예상됩니다.</p><p>인덱스를 생성하는 동안 이 템플릿과 일치하는 모든 인덱스는 이 템플릿에 정의된 구성을 상속합니다. 예를 들어 검은_금요일_주문 인덱스에는 order_date 필드가 있고, 샤드는 5로 설정되며 복제본은 2로 설정됩니다. 이 외에도 이 템플릿에서 생성된 <em>모든</em> 인덱스는 단일 <a href="https://opster.com/guides/elasticsearch/glossary/elasticsearch-alias/">별칭</a> 이름을 상속받습니다! 주문으로 정의된 인덱스 패턴과 미리 정의된 날짜 형식 dd-MM-yyyy를 가진 단일 oder_date 필드로 구성된 매핑 스키마를 사용하여 이 orders_template을 만들어 보겠습니다. 아래 코드는 이 인덱스 템플릿을 만드는 방법을 보여줍니다.</p><pre><code>PUT _index_template/orders_template
{
  "index_patterns": ["*orders"],
  "priority": 300,
  "template": {
    "mappings": {
      "properties": {
        "order_date": {
          "type": "date",
          "format":"dd-MM-yyyy"
        }
      }
    },
    "settings":{
      "number_of_shards":5,
      "number_of_replicas":2
    },
    "aliases":{
      "all_orders":{}
    }
  }
}</code></pre><p>Kibana의 개발자 도구에서 이 쿼리를 실행하면 미리 정의된 매핑, 설정 및 별칭과 함께 *orders 인덱스 패턴으로 템플릿이 생성됩니다. index_patterns는 일치 패턴의 배열로, 이 패턴과 일치하는 인덱스는 템플릿 구성을 도출합니다. 다음을 실행하여 우리가 수행한 작업을 반복해야 하는 지속된 템플릿을 검색할 수 있습니다:</p><pre><code>GET _index_template/orders_template </code></pre><p>템플릿에 정의된 템플릿 속성을 만들 때 양수인 우선순위를 정의할 수도 있습니다. 모든 템플릿에는 우선순위가 정의되어 있으므로 다른 템플릿에서 충돌하는 변경 사항이 있을 경우 우선순위가 높은 값을 우선하여 이 값을 사용하여 해결됩니다. 아래에서 템플릿 우선 순위에 대해 자세히 알아보겠습니다.</p><h2>템플릿으로 색인 만들기</h2><p>이제 인덱스를 만들기 위한 청사진인 템플릿이 생겼으니 다음 단계는 인덱스를 만드는 것입니다. 인덱스 이름이 지정된 패턴과 일치하면 템플릿 구성이 자동으로 적용됩니다. 이를 증명하기 위해 아래 코드에서 볼 수 있듯이 검은 금요일_주문이라는 새로운 인덱스를 만들어 보겠습니다:</p><pre><code>PUT blackfriday_orders</code></pre><p>인덱스 이름(blackfriday_orders)이 템플릿에 정의된 명명 패턴과 일치하므로(예를 들어 *주문), 인덱스는 템플릿에서 파생된 모든 구성을 가져와야 합니다. 새로 생성된 인덱스를 검색하고 다음 코드를 실행하여 이것이 실제로 사실인지 확인해 보겠습니다:</p><pre><code>GET blackfriday_orders</code></pre><p>반환됩니다:</p><pre><code>{
  "blackfriday_orders" : {
    "aliases" : {
      "all_orders" : { }
    },
    "mappings" : {
      "properties" : {
        "order_date" : {
          "type" : "date",
          "format" : "dd-MM-yyyy"
        }
      }
    },
    "settings" : {
      "index" : {
         ...
        "number_of_shards" : "5",
        "number_of_replicas" : "2"
      }
    }
  }
}</code></pre><p>응답에서 알 수 있듯이 검은 금요일 주문의 구성은 템플릿에서 상속되었습니다. 템플릿 구성을 성공적으로 상속할 수 있는 다양한 인덱스 조합을 시도해 볼 수 있습니다:</p><pre><code>PUT blackfriday_orders
PUT americaorders
PUT cancelled--orders
PUT undefined101orders</code></pre><p>그러나 다음 인덱스는 이름이 패턴과 일치하지 않으므로 구성을 상속하지 않습니다:</p><pre><code>PUT blackfriday_orders2
PUT open_orders_
PUT allorders_total</code></pre><p>기억해야 할 한 가지 중요한 점은 템플릿에서 파생된 모든 인덱스는 동일한 별칭(이 경우 all_orders)을 사용한다는 것입니다. 이러한 별칭을 사용하면 여러 인덱스가 아닌 이 단일 별칭으로 간단히 쿼리할 수 있다는 이점이 있습니다.</p><pre><code>GET blackfriday_orders,americaorders,undefined101orders/_search
GET all_orders/_search 
{
  "query": {
    "range": {
      "order_date": {
        "gte": "01-12-2021",
        "lte": "31-12-2021"
      }
    }
  }
}</code></pre><p>주문에 대한 템플릿을 생성하는 동안 일치하는 인덱스는 모두 템플릿 구성을 채택할 것으로 예상됩니다. 일반적으로 팀에서는 자의든 타의든 여러 가지 이유로 템플릿을 몇 개 더 만들 수 있습니다. 즉, 인덱스 이름이 두 개의 다른 템플릿 패턴과 일치하는 경우가 있습니다! Elasticsearch는 이러한 템플릿에서 어떤 구성을 적용해야 할지 결정해야 합니다. 다행히도 템플릿 우선순위를 사용하면 이 딜레마를 해결할 수 있습니다.</p><h2>구성 요소 템플릿을 생성하는 방법</h2><p>이 글의 앞부분에서 인덱스 템플릿에 대해 알아보았습니다. 구성이 내장된 템플릿을 만들면 몇 가지 단점이 있는데, 그 중 하나는 다른 템플릿으로 구성을 내보낼 수 없다는 점입니다. 고객 관련 템플릿(*고객)과 같이 유사한 구성을 원할 경우 전체 템플릿을 다시 만들어야 할 수도 있습니다. 즉, 일반적인 조직에서는 수십 개를 만들 수 있습니다(환경에 따라 몇 개 더 만들 수도 있습니다).</p><p>항상 재사용 가능성을 염두에 두고 템플릿을 재설계하기 때문에 Elasticsearch는 재사용 가능성을 염두에 두고 템플릿을 재설계했습니다. 컴포넌트 템플릿이 이에 적합합니다. DevOps 출신이라면 각 환경에 대해 미리 설정된 구성으로 인덱스를 만들어야 하는 요구 사항이 있을 것입니다. 이러한 각 구성을 수동으로 적용하는 번거로움 대신 각 환경에 대한 컴포넌트 템플릿을 만들 수 있습니다.</p><p>구성 요소 템플릿은 더 많은 인덱스 템플릿을 구성하는 데 사용할 수 있는 재사용 가능한 구성 블록에 불과합니다. 컴포넌트 템플릿은 인덱스 템플릿과 클럽화하지 않으면 아무런 가치가 없습니다. 구성 요소 템플릿 엔드포인트를 통해 노출됩니다. 이 모든 것이 어떻게 결합되는지 살펴봅시다.</p><h3>인덱스 템플릿의 설정</h3><p>앞서 인덱스 템플릿에서 정의한 설정을 추출하여 컴포넌트 템플릿을 만들어 보겠습니다. 설정_컴포넌트_템플릿에는 기본 샤드당 2개의 복제본이 있는 5개의 기본 샤드가 있어야 합니다. 아래 코드 목록에서 볼 수 있듯이 첫 번째 단계는 이 구성으로 컴포넌트 템플릿을 선언하고 실행하는 것입니다.</p><pre><code>PUT _component_template/settings_component_template
{
  "template":{
    "settings":{
      "number_of_shards":5,
      "number_of_replicas":2
    }
  }
}</code></pre><p>위의 코드에서 볼 수 있듯이 _component_template 엔드포인트를 사용하여 컴포넌트 템플릿을 생성합니다. 요청 본문에는 템플릿 객체에 템플릿 정보가 들어 있습니다. 이제 색인 템플릿의 다른 곳에서 settings_component_template을 사용할 수 있습니다. 한 가지 주목할 만한 차이점은 이 템플릿은 인덱스 패턴을 정의하지 않고 일부 속성을 구성하는 코드 블록에 불과하다는 점입니다.</p><h3>매핑 템플릿</h3><p>같은 방법으로 다른 템플릿을 만들어 보겠습니다. 이번에는 앞서 독립형 인덱스 템플릿에서 정의했던 매핑 스키마를 추출해 보겠습니다. 아래 코드는 스크립트를 보여줍니다:</p><pre><code>PUT _component_template/mappings_component_template
{
  "template": {
    "mappings": {
      "properties": {
        "order_date": {
          "type": "date",
          "format":"dd-MM-yyyy"
        }
      }
    }
  }
}</code></pre><h3>별칭 템플릿</h3><p>동일한 흐름에 따라 별칭이 있는 컴포넌트 템플릿(두 개의 별칭(all_orders 및 sales_orders))을 가질 수도 있습니다:</p><pre><code>PUT _component_template/aliases_component_template
{
  "template": {
    "aliases": {
      "all_orders": {},
      "sales_orders":{}
    }
  }
}</code></pre><h3>작성 가능한 색인 템플릿</h3><p>이제 세 가지 컴포넌트 템플릿을 준비했으니 다음 단계는 이를 사용하는 것입니다. 예를 들어 christmas_orders에 대한 인덱스 템플릿을 사용하도록 허용하면 이 작업을 수행할 수 있습니다:</p><pre><code>PUT _index_template/composed_orders_template
{
  "index_patterns": [
    "*orders"
  ],
  "priority": 500,
  "composed_of": [
    "settings_component_template",
    "mappings_component_template",
    "aliases_component_template"
  ]
}</code></pre><p>composed_of 태그는 이 템플릿을 구성하는 모든 컴포넌트 템플릿의 모음입니다. 이 경우 설정, 매핑 및 별칭 컴포넌트 템플릿을 선택합니다. 또한 이 템플릿이 다른 템플릿보다 우선하도록 우선순위를 높이고 있습니다. 템플릿이 준비되면 *주문 패턴과 일치하는 모든 인덱스는 이 세 가지 구성 요소 템플릿의 구성을 상속받게 됩니다.</p><p>하지만 고객과 같은 새 템플릿을 만들려면 기존 템플릿(settings_component_template)과 새로 만든 별칭(aliases_component_template - 아래 참조) 템플릿 중 하나만 사용하여 만들 수 있습니다:</p><pre><code>PUT _component_template/aliases_component_template2
{
  "template": {
    "aliases": {
      "all_customers": {}
    }
  }
}</code></pre><p>인덱스 템플릿은 다음과 같습니다:</p><pre><code>PUT _index_template/composed_customers_template
{
  "index_patterns": [
    "*customers*"
  ],
  "priority": 200,
  "composed_of": [
    "settings_component_template",
    "aliases_component_template2"
  ]
}</code></pre><p>설정_컴포넌트_템플릿이 두 개의 다른 템플릿에서 (재)사용된 것을 보셨나요? 이것이 바로 컴포넌트 템플릿의 힘입니다.</p><h2>인덱스 템플릿 우선순위</h2><p>개발자가 기존 종목을 보지 않고 여러 개의 인덱스 템플릿을 만들 수 있습니다. 이러한 템플릿 각각에 우선순위를 설정하여 우선순위가 높은 템플릿이 사용되도록 하는 것이 중요합니다. 예를 들어, 다음 코드 스니펫에서 my_orders_template_1이 my_orders_template_2를 재정의합니다:</p><pre><code>PUT _index_template/my_orders_template_1
{
  "index_patterns": ["*orders"],
  "priority": 1000,
  "template": { ... }
}
PUT _index_template/my_orders_template2
{
  "index_patterns": ["*orders"],
  "priority": 300,
  "template": { ... }
}</code></pre><p>생성 중인 인덱스와 일치하는 템플릿이 여러 개 있는 경우, Elasticsearch는 일치하는 모든 템플릿의 모든 구성을 적용하지만 우선순위가 더 높은 모든 구성을 재정의합니다.</p><h2>템플릿의 우선 순위</h2><p>마지막으로 템플릿의 우선순위에 대해 궁금하실 텐데요, 구성 요소 템플릿에 정의된 구성이 기본 인덱스 템플릿 자체에 정의된 구성보다 우선하나요? 아니면 그 반대일까요? 몇 가지 규칙이 있습니다:</p><ul><li>즉, 명시적으로 구성하여 만든 인덱스는 모든 항목보다 우선합니다. 즉, 명시적으로 구성하여 인덱스를 만들면 템플릿에 의해 재정의되지 않을 것으로 기대하지 마세요.</li><li>레거시 템플릿(버전 7.8 이전에 만든 템플릿)은 작성 가능한 템플릿보다 우선 순위가 낮습니다.</li></ul><h2>요약</h2><ul><li>인덱스에는 매핑, 설정 및 별칭이 포함됩니다. 매핑은 필드 스키마를 정의하고, 설정은 샤드 및 복제본 수와 같은 인덱스 파라미터를 설정하며, 별칭은 인덱스에 대체 이름을 부여합니다.</li><li>템플릿을 사용하면 미리 정의된 구성으로 인덱스를 만들 수 있습니다. 특정 템플릿에 정의된 인덱스 패턴과 일치하는 이름으로 인덱스 이름을 지정하면 템플릿에 따라 해당 인덱스가 자동으로 구성됩니다.</li><li>Elasticsearch는 버전 7.8에서 구성 가능한 인덱스 템플릿을 도입했습니다. 구성 가능한 인덱스 템플릿을 사용하면 템플릿을 모듈화하고 버전을 관리할 수 있습니다.</li><li>컴포저블 템플릿은 하나 이상의 컴포넌트 템플릿으로 구성됩니다.</li><li>인덱스 템플릿에는 자체 구성도 정의할 수 있습니다.</li><li>컴포넌트 템플릿은 작성 가능한 인덱스 템플릿과 마찬가지로 미리 정의된 구성이 있는 재사용 가능한 템플릿입니다.</li><li>그러나 구성 요소 템플릿은 인덱스 템플릿의 일부가 되어야 하며, 인덱스 템플릿으로 &#x27;구성&#x27;되지 않으면 쓸모가 없습니다.</li><li>구성 요소 템플릿에는 인덱스 패턴이 정의되어 있지 않으므로 인덱스 템플릿의 일부가 될 것으로 &#x27;예상&#x27;되는 또 다른 이유입니다.</li><li>각 템플릿에는 양수인 우선순위가 있습니다. 숫자가 높을수록 해당 템플릿이 적용되는 우선 순위가 높아집니다.</li></ul>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[두 개의 필드로 Elasticsearch 검색]]></title>
            <description><![CDATA[멀티매치 쿼리, 부울 쿼리, 쿼리 시간 필드 부스팅 등 두 개의 필드로 검색하는 기술을 알아보세요.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/elasticsearch-search-by-two-fields</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/elasticsearch-search-by-two-fields</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <pubDate>Wed, 30 Apr 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/d5d04bbcfc3925f48f3487ea4c7e0dd2205316d0-720x420.jpg?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>Elasticsearch에서 여러 필드에 걸쳐 검색하는 것은 많은 애플리케이션에서 공통적으로 요구되는 사항입니다. 이 문서에서는 다중 일치 쿼리, 부울 쿼리, 쿼리 시간 필드 부스팅 등 두 가지 필드별로 검색을 수행하는 고급 기술을 살펴봅니다. 이러한 기술은 사용자에게 보다 정확하고 관련성 높은 검색 결과를 제공하는 데 도움이 됩니다.</p><h2>두 개의 필드로 검색을 수행하는 고급 기술</h2><h3>1. 멀티매치 쿼리</h3><p>다중 일치 쿼리를 사용하면 여러 필드에서 단일 쿼리 문자열을 검색할 수 있습니다. 이 기능은 두 필드 중 하나에 지정된 쿼리 문자열이 포함된 문서를 찾으려는 경우에 유용합니다. 다음은 &#x27;제목&#x27; 또는 &#x27;설명&#x27; 필드에서 &#x27;example&#x27;라는 용어를 검색하는 다중 검색 쿼리의 예입니다:</p><pre><code>{
  "query": {
    "multi_match": {
      "query": "example",
      "fields": ["title", "description"]
    }
  }
}</code></pre><h3>2. 부울 쿼리</h3><p>부울 쿼리를 사용하면 부울 논리를 사용하여 여러 쿼리를 결합할 수 있습니다. &quot;should&quot; 절을 사용하여 두 필드 중 하나에서 쿼리와 일치하는 문서를 검색할 수 있습니다. 다음은 &#x27;title&#x27; 및 &#x27;description&#x27; 필드에서 &#x27;example&#x27;라는 용어를 검색하는 부울 쿼리의 예입니다:</p><pre><code>{
  "query": {
    "bool": {
      "should": [
        {"match": {"title": "example"}},
        {"match": {"description": "example"}}
      ]
    }
  }
}</code></pre><h3>3. 쿼리 시간 필드 부스팅</h3><p>때로는 검색 중에 한 필드를 다른 필드보다 더 중요하게 생각하고 싶을 수도 있습니다. 쿼리 시점에 필드에 부스트 인자를 적용하여 이를 달성할 수 있습니다. 부스트 값이 높을수록 해당 필드에 더 많은 가중치가 부여되므로 최종 검색 점수에 영향을 미칠 가능성이 높아집니다. 다음은 &#x27;title&#x27; 필드에 부스트 팩터가 적용된 멀티매치 쿼리의 예입니다:</p><pre><code>{
  "query": {
    "multi_match": {
      "query": "example",
      "fields": ["title^3", "description"]
    }
  }
}</code></pre><p>이 예에서 &#x27;제목&#x27; 필드의 부스트 계수는 3으로, 검색 점수를 결정하는 데 있어 &#x27;설명&#x27; 필드보다 3배 더 중요합니다.</p><h3>4. 다양한 부스트 인자를 가진 쿼리 결합</h3><p>부울 쿼리를 사용하여 서로 다른 부스트 인자를 가진 여러 쿼리를 결합할 수도 있습니다. 이를 통해 검색 결과에서 각 필드의 중요도를 미세 조정할 수 있습니다. 다음은 &#x27;title&#x27; 및 &#x27;description&#x27; 필드에 서로 다른 부스트 인자를 적용한 부울 쿼리의 예입니다:</p><pre><code>{
  "query": {
    "bool": {
      "should": [
        {"match": {"title": {"query": "example", "boost": 3}}},
        {"match": {"description": {"query": "example", "boost": 1}}}
      ]
    }
  }
}</code></pre><p>이 예에서 &#x27;제목&#x27; 필드의 부스트 계수는 3이고 &#x27;설명&#x27; 필드의 부스트 계수는 1입니다.</p><h2>결론</h2><p>다중 일치 쿼리, 부울 쿼리, 쿼리 시간 필드 부스팅과 같은 고급 기술을 사용하여 Elasticsearch에서 두 필드를 기준으로 검색할 수 있습니다. 이러한 기술을 결합하면 사용자에게 더 정확하고 관련성 높은 검색 결과를 제공할 수 있습니다. 다양한 쿼리 조합과 부스트 인자를 실험하여 특정 사용 사례에 맞는 최적의 검색 구성을 찾아보세요.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[​​LangGraph와 Elasticsearch를 사용하여 RAG 워크플로우 구축하기]]></title>
            <description><![CDATA[효율적인 데이터 검색과 AI 기반 응답을 위한 RAG 워크플로우를 구축하기 위해 Elasticsearch로 LangGraph 검색 에이전트 템플릿을 구성하고 사용자 정의하는 방법을 알아보세요.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/build-rag-workflow-langgraph-elasticsearch</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/build-rag-workflow-langgraph-elasticsearch</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <pubDate>Fri, 25 Apr 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/17b7e7b336f73e232375d1add582ae5f6c52a279-1440x840.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p><a href="https://github.com/langchain-ai/retrieval-agent-template">LangGraph 검색 에이전트 템플릿은</a> LangGraph Studio에서 LangGraph를 사용하여 검색 기반 질문 답변 시스템을 쉽게 만들 수 있도록 LangChain에서 개발한 스타터 프로젝트입니다. 이 템플릿은 Elasticsearch와 원활하게 통합되도록 미리 구성되어 있어 개발자가 문서를 효율적으로 색인하고 검색할 수 있는 에이전트를 신속하게 구축할 수 있습니다.</p><p>이 블로그는 LangGraph Studio와 LangGraph CLI를 사용하여 LangChain 검색 에이전트 템플릿을 실행하고 커스터마이징하는 방법에 중점을 두고 있습니다. 이 템플릿은 Elasticsearch와 같은 다양한 검색 백엔드를 활용하여 검색 증강 생성(RAG) 애플리케이션을 구축하기 위한 프레임워크를 제공합니다.</p><p>에이전트 흐름을 사용자 정의하면서 Elastic으로 환경을 설정하고, 구성하고, 템플릿을 효율적으로 실행하는 방법을 안내해드립니다.</p><h2>필수 구성 요소</h2><p>계속 진행하기 전에 다음이 설치되어 있는지 확인하세요:</p><ul><li>Elasticsearch Cloud 배포 또는 온프레미스 Elasticsearch 배포(또는 Elastic Cloud에서 14일 <a href="https://www.elastic.co/kr/cloud/cloud-trial-overview">무료 체험판 </a>생성) - 버전 8.0.0 이상</li><li>Python 3.9+</li><li><a href="https://cohere.com/">Cohere</a> (이 가이드에서 사용됨), <a href="https://openai.com/">OpenAI</a> 또는 <a href="https://www.anthropic.com/claude">Anthropic/Claude와</a>같은 LLM 제공업체에 대한 액세스</li></ul><h2>LangGraph 앱 만들기</h2><h3>1. LangGraph CLI 설치</h3><pre><code>pip install --upgrade "langgraph-cli[inmem]"</code></pre><h3>2. 검색 에이전트 템플릿에서 LangGraph 앱 만들기</h3><pre><code>mkdir lg-agent-demo
cd lg-agent-demo
langgraph new lg-agent-demo </code></pre><p><em>사용 가능한 템플릿 목록에서 선택할 수 있는 대화형 메뉴가 표시됩니다. </em>아래와 같이 검색 에이전트에는 4개를, 파이썬에는 1개를 선택합니다:</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/6a41a41f95c2477c67810adc7be46d91faf06878-1600x407.png" style="width: 100%;" /><ul><li><strong>문제 해결</strong>: 오류 발생 시: &quot;urllib.error.URLError: &lt;urlopen 오류 [SSL: CERTIFICATE_VERIFY_FAILED] 인증서 확인 실패: 로컬 발급자 인증서(_ssl.c:1000)를 얻을 수 없음&gt; &quot;</li></ul><p>아래와 같이 파이썬의 인증서 설치 명령을 실행하여 문제를 해결하세요.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/83ec238136c41738457299fd42c83aff32eb5b97-1407x75.png" style="width: 100%;" /><h3>3. 설치 종속성</h3><p>새 LangGraph 앱의 루트에서 가상 환경을 만들고 <code>edit</code> 모드에서 종속성을 설치하여 로컬 변경 사항이 서버에서 사용되도록 합니다:</p><pre><code>#For Mac
python3 -m venv lg-demo
source lg-demo/bin/activate 
pip install -e .

#For Windows
python3 -m venv lg-demo
lg-demo\Scripts\activate 
pip install -e .</code></pre><h2>환경 설정</h2><h3>1. .environment 만들기 파일</h3><p><code>.env</code> 파일에는 앱이 선택한 LLM 및 검색 제공업체에 연결할 수 있도록 API 키와 구성이 저장되어 있습니다. 예제 구성을 복제하여 새 <code>.env</code> 파일을 생성합니다:</p><pre><code>cp .env.example .env</code></pre><h3>2. .env 파일</h3><p><code>.env</code> 파일은 기본 구성 세트와 함께 제공됩니다. 설정에 따라 필요한 API 키와 값을 추가하여 업데이트할 수 있습니다. 사용 사례와 관련이 없는 키는 변경하지 않거나 제거할 수 있습니다.</p><pre><code># To separate your traces from other applications
LANGSMITH_PROJECT=retrieval-agent

# LLM choice (set the API key for your selected provider):
ANTHROPIC_API_KEY=your_anthropic_api_key
FIREWORKS_API_KEY=your_fireworks_api_key
OPENAI_API_KEY=your_openai_api_key

# Retrieval provider (configure based on your chosen service):

## Elastic Cloud:
ELASTICSEARCH_URL=https://your_elastic_cloud_url
ELASTICSEARCH_API_KEY=your_elastic_api_key

## Elastic Local:
ELASTICSEARCH_URL=http://host.docker.internal:9200
ELASTICSEARCH_USER=elastic
ELASTICSEARCH_PASSWORD=changeme

## Pinecone:
PINECONE_API_KEY=your_pinecone_api_key
PINECONE_INDEX_NAME=your_pinecone_index_name

## MongoDB Atlas:
MONGODB_URI=your_mongodb_connection_string

# Cohere API key:
COHERE_API_KEY=your_cohere_api_key</code></pre><ul><li>예시 <code>.env</code> 파일(Elastic Cloud 및 Cohere 사용)</li></ul><p>아래는 이 블로그에서 설명한 대로 <strong>Elastic Cloud를</strong> 검색 공급자로, <strong>Cohere를</strong> LLM으로 사용하는 샘플 <code>.env</code> 구성입니다:</p><pre><code># To separate your traces from other applications
LANGSMITH_PROJECT=retrieval-agent
#Retrieval Provider
# Elasticsearch configuration
ELASTICSEARCH_URL=elastic-url:443
ELASTICSEARCH_API_KEY=elastic_api_key
# Cohere API key
COHERE_API_KEY=cohere_api_key</code></pre><p><em>참고: 이 가이드에서는 응답 생성과 임베딩 모두에 Cohere를 사용하지만 </em>, 사용 사례에 따라<em><strong>OpenAI</strong>, <strong>Claude</strong> 또는 로컬 LLM 모델과 </em>같은 다른 LLM 제공업체를 자유롭게 사용할 수 있습니다.&nbsp; 사용하려는 각 키가<em><code>.env</code></em>파일에 존재하고 올바르게 설정되어 있는지 확인하세요<em>.</em></p><h3>3. 구성 파일 -configuration.py 업데이트 </h3><p>적절한 API 키로 <code>.env</code> 파일을 설정한 후 다음 단계는 애플리케이션의 기본 모델 구성을 업데이트하는 것입니다. 구성을 업데이트하면 시스템에서 <code>.env</code> 파일에 지정한 서비스 및 모델을 사용하도록 합니다.</p><p>구성 파일로 이동합니다:</p><pre><code> cd src/retrieval_graph</code></pre><p><code>configuration.py</code> 파일에는 검색 에이전트가 세 가지 주요 작업에 사용하는 기본 모델 설정이 포함되어 있습니다:</p><ul><li><strong>임베딩 모델</strong> - 문서를 벡터 표현으로 변환합니다.</li><li><strong>쿼리 모델</strong> - 사용자의 쿼리를 벡터로 처리합니다.</li><li><strong>응답 모델</strong> - 최종 응답 생성</li></ul><p>기본적으로 이 코드는 <strong>OpenAI</strong> (예: <code>openai/text-embedding-3-small</code>) 및 <strong>Anthropic</strong> (예: <code>anthropic/claude-3-5-sonnet-20240620 and anthropic/claude-3-haiku-20240307</code>)의 모델을 사용합니다.<br/><br/>이 블로그에서는 Cohere 모델을 사용하도록 전환합니다. 이미 OpenAI 또는 Anthropic을 사용 중인 경우 변경할 필요가 없습니다.</p><h4>변경 예시(Cohere 사용):</h4><p><code>configuration.py</code> 을 열고 아래와 같이 모델 기본값을 수정합니다:</p><pre><code>…
 embedding_model: Annotated[
       str,
       {"__template_metadata__": {"kind": "embeddings"}},
   ] = field(
       default="cohere/embed-english-v3.0",
…
response_model: Annotated[str, {"__template_metadata__": {"kind": "llm"}}] = field(
       default="cohere/command-r-08-2024",
…
query_model: Annotated[str, {"__template_metadata__": {"kind": "llm"}}] = field(
       default="cohere/command-r-08-2024",
       metadata={</code></pre><h2>LangGraph CLI로 검색 에이전트 실행하기</h2><h3>1. LangGraph 서버를 시작합니다.</h3><pre><code>cd lg-agent-demo
langgraph dev</code></pre><p>이렇게 하면 LangGraph API 서버가 로컬에서 시작됩니다. 성공적으로 실행되면 다음과 같은 내용이 표시됩니다:</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/e3c3344b24651067e2d0892d870feca505b3be35-1494x542.png" style="width: 100%;" /><p>Open Studio UI URL.</p><p>두 가지 그래프를 사용할 수 있습니다:</p><ul><li><strong>검색 그래프</strong>: Elasticsearch에서 데이터를 검색하고 LLM을 사용하여 쿼리에 응답합니다.</li><li><strong>인덱서 그래프</strong>: 문서를 Elasticsearch로 색인하고 LLM을 사용하여 임베딩을 생성합니다.</li></ul><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/99b579ca44b2d13f61da7e1de5886ea5b9a5e16f-1600x1009.png" style="max-width:100%; margin-bottom:1em;" /><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/f54d182d2789b6a4d113b0c88063531ea0b6de5d-1600x1009.png" style="max-width:100%; margin-bottom:1em;" /><h3>2. 인덱서 그래프 구성</h3><ul><li>인덱서 그래프를 엽니다.</li><li>어시스턴트 관리를 클릭합니다.<ul><li><strong>&#x27;새 어시스턴트 추가&#x27;</strong>를 클릭하고 지정된 대로 사용자 세부 정보를 입력한 다음 창을 닫습니다.</li></ul></li></ul><pre><code>{"user_id": "101"}</code></pre><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/6f87958e40760ac9671639f4995962bcea090991-1600x1139.png" style="max-width:100%; margin-bottom:1em;" /><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/8f802f6b65b3f06d7d1a521c3f40385c6006413b-1422x1212.png" style="max-width:100%; margin-bottom:1em;" /><h3>3. 샘플 문서 색인 생성</h3><ul><li>다음 샘플 문서는 가상의 분기별 보고서인 NoveTech 조직에 대한 색인입니다:</li></ul><pre><code>[
  {    "page_content": "NoveTech Solutions Q1 2025 Report - Revenue: $120.5M, Net Profit: $18.2M, EPS: $2.15. Strong AI software launch and $50M government contract secured."
  },
  {
    "page_content": "NoveTech Solutions Business Highlights - AI-driven analytics software gained 15% market share. Expansion into Southeast Asia with two new offices. Cloud security contract secured."
  },
  {
    "page_content": "NoveTech Solutions Financial Overview - Operating expenses at $85.3M, Gross Margin 29.3%. Stock price rose from $72.5 to $78.3. Market Cap reached $5.2B."
  },
  {
    "page_content": "NoveTech Solutions Challenges - Rising supply chain costs impacting hardware production. Regulatory delays slowing European expansion. Competitive pressure in cybersecurity sector."
  },
  {
    "page_content": "NoveTech Solutions Future Outlook - Expected revenue for Q2 2025: $135M. New AI chatbot and blockchain security platform launch planned. Expansion into Latin America."
  },
  {
    "page_content": "NoveTech Solutions Market Performance - Year-over-Year growth at 12.7%. Stock price increase reflects investor confidence. Cybersecurity and AI sectors remain competitive."
  },
  {
    "page_content": "NoveTech Solutions Strategic Moves - Investing in R&D to enhance AI-driven automation. Strengthening partnerships with enterprise cloud providers. Focusing on data privacy solutions."
  },
  {
    "page_content": "NoveTech Solutions CEO Statement - 'NoveTech Solutions continues to innovate in AI and cybersecurity. Our growth strategy remains strong, and we foresee steady expansion in the coming quarters.'"
  }
]</code></pre><p>문서가 색인되면 아래와 같이 토론글에 삭제 메시지가 표시됩니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/fd3a1efd64cb54d54ea56ef5055249dd066d5708-1600x854.png" style="width: 100%;" /><h3>4. 검색 그래프 실행</h3><ul><li>검색 그래프로 전환합니다.</li><li>다음 검색어를 입력합니다:</li></ul><pre><code>What was NovaTech Solutions total revenue in Q1 2025?</code></pre><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/eb4d8ddfe0effd7e1868fba921b8ef13f7baf27a-1600x755.png" style="width: 100%;" /><p>시스템은 관련 문서를 반환하고 색인된 데이터를 기반으로 정확한 답변을 제공합니다.</p><h2>검색 에이전트 사용자 지정</h2><p>사용자 경험을 개선하기 위해 검색 그래프에 사용자 지정 단계를 도입하여 사용자가 다음 세 가지 질문을 예측할 수 있도록 했습니다. 이 예측은 다음을 기반으로 합니다:</p><ul><li>검색된 문서의 컨텍스트</li><li>이전 사용자 상호 작용</li><li>마지막 사용자 쿼리</li></ul><p>쿼리 예측 기능을 구현하려면 다음과 같은 코드 변경이 필요합니다:</p><h3>1. 그래프.py 업데이트</h3><ul><li><code>predict_query</code> 기능을 추가합니다:</li></ul><pre><code>async def predict_query(
   state: State, *, config: RunnableConfig
) -> dict[str, list[BaseMessage]]:
   logger.info(f"predict_query predict_querypredict_query predict_query predict_query predict_query")  # Log the query

   configuration = Configuration.from_runnable_config(config)
   prompt = ChatPromptTemplate.from_messages(
       [
           ("system", configuration.predict_next_question_prompt),
           ("placeholder", "{messages}"),
       ]
   )
   model = load_chat_model(configuration.response_model)
   user_query = state.queries[-1] if state.queries else "No prior query available"
   logger.info(f"user_query: {user_query}")
   logger.info(f"statemessage: {state.messages}")
   #human_messages = [msg for msg in state.message if isinstance(msg, HumanMessage)]

   message_value = await prompt.ainvoke(
       {
           "messages": state.messages,
           "user_query": user_query,  # Use the most recent query as primary input
           "system_time": datetime.now(tz=timezone.utc).isoformat(),
       },
       config,
   )

   next_question = await model.ainvoke(message_value, config)
   return {"next_question": [next_question]}</code></pre><ul><li><code>respond</code> 함수를 수정하여 메시지 대신 <strong><code>response</code></strong> 객체 를 반환하도록 합니다:</li></ul><pre><code>async def respond(
   state: State, *, config: RunnableConfig
) -> dict[str, list[BaseMessage]]:
   """Call the LLM powering our "agent"."""
   configuration = Configuration.from_runnable_config(config)
   # Feel free to customize the prompt, model, and other logic!
   prompt = ChatPromptTemplate.from_messages(
       [
           ("system", configuration.response_system_prompt),
           ("placeholder", "{messages}"),
       ]
   )
   model = load_chat_model(configuration.response_model)

   retrieved_docs = format_docs(state.retrieved_docs)
   message_value = await prompt.ainvoke(
       {
           "messages": state.messages,
           "retrieved_docs": retrieved_docs,
           "system_time": datetime.now(tz=timezone.utc).isoformat(),
       },
       config,
   )
   response = await model.ainvoke(message_value, config)
   # We return a list, because this will get added to the existing list
   return {"response": [response]}</code></pre><ul><li>그래프 구조를 업데이트하여 predict_query에 대한 새 노드와 에지를 추가합니다:</li></ul><pre><code>builder.add_node(generate_query)
builder.add_node(retrieve)
builder.add_node(respond)
builder.add_node(predict_query)
builder.add_edge("__start__", "generate_query")
builder.add_edge("generate_query", "retrieve")
builder.add_edge("retrieve", "respond")
builder.add_edge("respond", "predict_query")</code></pre><h3>2. prompts.py 업데이트</h3><ul><li><code>prompts.py</code> 에서 게리 예측을 위한 프롬프트를 작성합니다:</li></ul><pre><code>PREDICT_NEXT_QUESTION_PROMPT = """Given the user query and the retrieved documents, suggest the most likely next question the user might ask.

**Context:**
- Previous Queries:
{previous_queries}

- Latest User Query: {user_query}

- Retrieved Documents:
{retrieved_docs}

**Guidelines:**
1. Do not suggest a question that has already been asked in previous queries.
2. Consider the retrieved documents when predicting the next logical question.
3. If the user's query is already fully answered, suggest a relevant follow-up question.
4. Keep the suggested question natural and conversational.
5. Suggest at least 3 question

System time: {system_time}"""</code></pre><h3>3. configuration.py 업데이트</h3><ul><li><code>predict_next_question_prompt</code> 을 추가합니다:</li></ul><pre><code>predict_next_question_prompt: str = field(
       default=prompts.PREDICT_NEXT_QUESTION_PROMPT,
       metadata={"description": "The system prompt used for generating responses."},
   )</code></pre><h3>4. state.py 업데이트</h3><ul><li>다음 속성을 추가합니다:</li></ul><pre><code>response: Annotated[Sequence[AnyMessage], add_messages]
next_question : Annotated[Sequence[AnyMessage], add_messages]</code></pre><h3>5. 검색 그래프 다시 실행</h3><ul><li>다음 검색어를 다시 입력합니다:</li></ul><pre><code>What was NovaTech Solutions total revenue in Q1 2025?</code></pre><p>시스템은 입력을 처리하고 아래와 같이 사용자가 질문할 수 있는 세 가지 관련 질문을 예측합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/70e855a2e4edc0ba5a147588df0de30eb081d053-1600x777.png" style="width: 100%;" /><h2>결론</h2><p>LangGraph Studio와 CLI에 검색 에이전트 템플릿을 통합하면 몇 가지 주요 이점이 있습니다:</p><ul><li><strong>개발 가속화</strong>: 템플릿 및 시각화 도구는 검색 워크플로우의 생성 및 디버깅을 간소화하여 개발 시간을 단축합니다.</li><li><strong>원활한 배포</strong>: API 및 자동 확장에 대한 기본 지원으로 여러 환경에 걸쳐 원활한 배포를 보장합니다.</li><li><strong>간편한 업데이트:</strong> 워크플로우 수정, 새로운 기능 추가, 추가 노드 통합이 간단해 검색 프로세스를 쉽게 확장하고 개선할 수 있습니다.</li><li><strong>영구 메모리</strong>: 시스템이 상담원 상태와 지식을 유지하여 일관성과 안정성을 향상시킵니다.</li><li><strong>유연한 워크플로 모델링</strong>: 개발자는 특정 사용 사례에 맞게 검색 로직과 커뮤니케이션 규칙을 사용자 지정할 수 있습니다.</li><li><strong>실시간 상호작용 및 디버깅</strong>: 실행 중인 에이전트와 상호 작용하는 기능으로 효율적인 테스트 및 문제 해결이 가능합니다.</li></ul><p>이러한 기능을 활용하여 조직은 데이터 접근성과 사용자 경험을 향상시키는 강력하고 효율적이며 확장 가능한 검색 시스템을 구축할 수 있습니다.</p><p>이 프로젝트의 전체 소스 코드는 <a href="https://github.com/elastic/elasticsearch-labs/tree/main/supporting-blog-content/langraph-retrieval-agent-template-demo">GitHub에서</a> 확인할 수 있습니다.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[더 나은 바이너리 정량화(BBQ)를 사용 사례에 구현하는 방법]]></title>
            <description><![CDATA[사용 사례에서 더 나은 이진 정량화(BBQ)를 구현하는 이유와 그 방법을 살펴보세요.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/bbq-implementation-into-use-case</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/bbq-implementation-into-use-case</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Sachin Frayne]]></dc:creator>
            <pubDate>Wed, 23 Apr 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/66842055367cdd795532b01c167f2a4b03dc65e3-1200x628.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>벡터 검색은 텍스트에 대한 시맨틱 검색이나 이미지, 동영상 또는 오디오에 대한 유사도 검색을 구현할 때 기초를 제공합니다. 벡터 검색에서 벡터는 방대하고 때로는 느릴 수 있는 데이터를 수학적으로 표현한 것입니다. 더 나은 이진 양자화(이하 BBQ)는 벡터의 압축 방법으로 작동합니다. 벡터를 축소하여 검색 및 처리 속도를 높이면서 적합한 일치 항목을 찾을 수 있습니다. 이 글에서는 벡터를 자동으로 채점하는 정량화된 인덱스에만 사용할 수 있는 필드인 BBQ와 rescore_vector에 대해 설명합니다.</p><p>이 문서에 언급된 모든 전체 쿼리와 출력은 <a href="https://github.com/elastic/elasticsearch-labs/tree/main/supporting-blog-content/how-and-why-bbq">Elasticsearch Labs 코드 리포지토리에서</a> 확인할 수 있습니다.</p><h2>사용 사례에서 더 나은 이진 정량화(BBQ)를 구현하는 이유는 무엇인가요?</h2><blockquote>참고: BBQ의 수학적 원리에 대한 자세한 내용은 아래의 <a href="https://www.elastic.co/kr/search-labs/blog/bbq-implementation-into-use-case#further-learning">&#x27;추가 학습&#x27; 섹션을</a> 참조하세요. 이 블로그에서는 구현에 초점을 맞추고 있습니다.</blockquote><p>수학은 흥미롭지만, 벡터 검색의 정확성을 유지하려면 그 이유를 완전히 파악하는 것이 중요합니다. 현재의 벡터 검색 알고리즘으로는 데이터 읽기 속도에 제한이 있다는 것이 밝혀졌기 때문에 궁극적으로 이것은 압축에 관한 것입니다. 따라서 해당 데이터를 모두 메모리에 넣을 수 있다면 스토리지에서 읽을 때보다 속도가 크게 향상됩니다<a href="https://sre.google/static/pdf/rule-of-thumb-latency-numbers-letter.pdf">(메모리는 SSD보다 약 200배 빠릅니다</a>).</p><p>몇 가지 유의해야 할 사항이 있습니다:</p><ul><li><a href="https://arxiv.org/pdf/1603.09320">HNSW</a> (계층 탐색 가능한 작은 세계)와 같은 그래프 기반 인덱스는 벡터 검색에 가장 빠릅니다.<ul><li>HNSW: 다층 그래프 구조를 구축하여 효율적인 고차원 유사도 검색을 가능하게 하는 근사 근사 이웃 검색 알고리즘입니다.</li></ul></li></ul><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/590f3b3c72a76023a38a0436cd9ff90a9f80e936-1964x1262.png" style="width: 100%;" /><ul><li>HNSW는 메모리 또는 최악의 경우 스토리지에서 데이터를 읽는 속도에 의해 근본적으로 속도가 제한됩니다.<ul><li>이상적으로는 저장된 모든 벡터를 메모리에 로드할 수 있어야 합니다.</li></ul></li><li>임베딩 모델은 일반적으로 부동 소수점 숫자당 4바이트의 플로트32 정밀도를 가진 벡터를 생성합니다.</li><li>마지막으로, 보유하고 있는 벡터 및/또는 치수의 수에 따라 모든 벡터를 저장하기 위한 메모리가 매우 빠르게 부족해질 수 있습니다.</li></ul><p>이를 당연하게 생각하면, 수백 또는 수천 개의 차원을 가진 수백만 또는 수십억 개의 벡터를 수집하기 시작하면 문제가 빠르게 발생한다는 것을 알 수 있습니다. &#x27;<a href="https://www.elastic.co/kr/search-labs/blog/bbq-implementation-into-use-case#approximate-numbers-on-the-compression-ratios">압축률에 대한 대략적인</a>수치&#x27;라는 제목의 섹션에서 대략적인 수치를 확인할 수 있습니다.</p><h2>시작하려면 무엇이 필요하나요?</h2><p>시작하려면 다음이 필요합니다:</p><ul><li>Elastic Cloud 또는 온프레미스를 사용하는 경우, 8.18 이상의 Elasticsearch 버전이 필요합니다. BBQ는 8.16에 도입되었지만, 이 글에서는 8.18에 도입된 <code>vector_rescore</code> 을 사용합니다.</li><li>또한 클러스터에 <a href="https://www.elastic.co/kr/guide/en/elasticsearch/reference/8.18/ml-settings.html">머신 러닝(ML) 노드가</a> 있는지 확인해야 합니다. (참고: 모델을 로드하려면 최소 4GB의 ML 노드가 필요하지만 전체 프로덕션 워크로드에는 훨씬 더 큰 노드가 필요할 수 있습니다.)</li><li>서버리스를 사용하는 경우 벡터에 최적화된 인스턴스를 선택해야 합니다.</li><li>또한 벡터 데이터베이스에 대한 기본 지식이 필요합니다. Elastic의 벡터 검색 개념에 아직 익숙하지 않으시다면 먼저 다음 리소스를 확인해보시는 것이 좋습니다:<ul><li><a href="https://www.elastic.co/kr/search-labs/blog/elastic-vector-database-practical-example">Elastic Vector 데이터베이스 탐색</a></li><li><a href="https://www.elastic.co/kr/blog/retrieval-augmented-generation-explained">검색 증강 생성의 핵심 아이디어</a></li></ul></li></ul><h2>더 나은 바이너리 정량화(BBQ) 구현</h2><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/4d388078495566f0527e931e0c2e38facdce83c6-1503x748.png" style="width: 100%;" /><p>이 블로그는 간결하게 유지하기 위해 기본 제공 기능을 사용할 수 있는 경우 이를 사용합니다. 이 경우, 머신 러닝 노드에서 Elasticsearch 내부에서 직접 실행되는 <a href="https://www.elastic.co/kr/guide/en/machine-learning/8.17/ml-nlp-e5.html"><code>.multilingual-e5-small</code></a> 벡터 임베딩 모델이 있습니다. <code>text_embedding</code> 모델을 원하는 임베더<a href="https://www.elastic.co/kr/guide/en/elasticsearch/reference/8.18/infer-service-openai.html">(OpenAI</a>, <a href="https://www.elastic.co/kr/guide/en/elasticsearch/reference/8.18/infer-service-google-ai-studio.html">Google AI Studio</a>, <a href="https://www.elastic.co/kr/guide/en/elasticsearch/reference/8.18/infer-service-cohere.html">Cohere</a> 등)로 대체할 수 있습니다. 선호하는 모델이 아직 통합되지 않은 경우, <a href="https://www.elastic.co/kr/guide/en/elasticsearch/reference/8.18/bring-your-own-vectors.html">자체 고밀도 벡터 임베딩을 가져올</a> 수도 있습니다.)</p><p>먼저, 주어진 텍스트에 대한 벡터를 생성하기 위해 추론 엔드포인트를 만들어야 합니다. 이 모든 명령은 Kibana <a href="https://www.elastic.co/kr/guide/en/kibana/8.18/console-kibana.html">개발자 도구 콘솔에서</a> 실행합니다. 이 명령은 <code>.multilingual-e5-small</code> 을 다운로드합니다. 아직 존재하지 않으면 엔드포인트가 설정되며, 실행하는 데 1분 정도 걸릴 수 있습니다. 예상 출력은 출력 폴더의 <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/how-and-why-bbq/Outputs/01-create-an-inference-endpoint-output.json">01-create-an-inference-endpoint-output.json</a> 파일에서 확인할 수 있습니다. </p><pre><code>PUT _inference/text_embedding/my_e5_model
{
  "service": "elasticsearch",
  "service_settings": {
    "num_threads": 1,
    "model_id": ".multilingual-e5-small",
    "adaptive_allocations": {
      "enabled": true,
      "min_number_of_allocations": 1
    }
  }
}</code></pre><p>반환되면 모델이 설정되고 다음 명령을 사용하여 모델이 예상대로 작동하는지 테스트할 수 있습니다. 예상 출력은 출력 폴더의 <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/how-and-why-bbq/Outputs/02-embed-text-output.json">02-embed-text-output.json</a> 파일에서 확인할 수 있습니다.</p><pre><code>POST _inference/text_embedding/my_e5_model
{
  "input": "my awesome piece of text"
}</code></pre><p>학습된 모델이 노드에 할당되지 않는 문제가 발생하면 모델을 수동으로 시작해야 할 수 있습니다.</p><pre><code>POST _ml/trained_models/.multilingual-e5-small/deployment/_start</code></pre><p>이제 임베딩 모델의 출력과 일치하도록 표준 텍스트 필드(<code>my_field</code>)와 384개의 차원을 가진 고밀도 벡터 필드(<code>my_vector</code>)의 두 가지 속성을 가진 새 매핑을 만들어 보겠습니다. 또한 <code>index_options.type to bbq_hnsw</code> 을 재정의합니다. 예상 출력은 출력 폴더의 <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/how-and-why-bbq/Outputs/03-create-byte-qauntized-index-output.json">03-create-byte-qauntized-index-output.json</a> 파일에서 확인할 수 있습니다.</p><pre><code>PUT bbq-my-byte-quantized-index
{
  "mappings": {
    "properties": {
      "my_field": {
        "type": "text"
      },
      "my_vector": {
        "type": "dense_vector",
        "dims": 384,
        "index_options": {
          "type": "bbq_hnsw"
        }
      }
    }
  }
}</code></pre><p>Elasticsearch가 벡터를 생성하도록 하기 위해 <a href="https://www.elastic.co/kr/guide/en/elasticsearch/reference/8.18/ingest.html">수집 파이프라인을</a> 사용할 수 있습니다. 이 파이프라인에는 엔드포인트(<code>model_id</code>), 벡터를 생성하려는 <code>input_field</code>, 벡터를 저장할 <code>output_field</code>, 이 세 가지가 필요합니다. 아래의 첫 번째 명령은 추론 <a href="https://www.elastic.co/kr/guide/en/elasticsearch/reference/current/inference-apis.html">서비스를 </a>내부적으로 사용하는 추론 수집 파이프라인을 만들고, 두 번째 명령은 파이프라인이 올바르게 작동하는지 테스트합니다. 예상 출력은 출력 폴더의 <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/how-and-why-bbq/Outputs/04-create-and-simulate-ingest-pipeline-output.json">04-create-and-simulate-ingest-pipeline-output.json</a> 파일에서 확인할 수 있습니다. </p><pre><code>PUT _ingest/pipeline/my_inference_pipeline
{
  "processors": [
    {
      "inference": {
        "model_id": "my_e5_model",
        "input_output": [
          {
            "input_field": "my_field",
            "output_field": "my_vector"
          }
        ]
      }
    }
  ]
}

POST _ingest/pipeline/my_inference_pipeline/_simulate
{
  "docs": [
    {
      "_source": {
        "my_field": "my awesome text field"
      }
    }
  ]
}</code></pre><p>이제 아래의 처음 두 명령어로 문서를 추가하고 세 번째 명령어로 검색이 제대로 작동하는지 테스트할 준비가 되었습니다. 예상 출력은 출력 폴더의 <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/how-and-why-bbq/Outputs/05-bbq-index-output.json">05-bbq-index-output.json</a> 파일에서 확인할 수 있습니다. </p><pre><code>PUT bbq-my-byte-quantized-index/_doc/1?pipeline=my_inference_pipeline
{
    "my_field": "my awesome text field"
}

PUT bbq-my-byte-quantized-index/_doc/2?pipeline=my_inference_pipeline
{
    "my_field": "some other sentence"
}

GET bbq-my-byte-quantized-index/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "knn": {
            "field": "my_vector",
            "query_vector_builder": {
              "text_embedding": {
                "model_id": "my_e5_model",
                "model_text": "my awesome search field"
              }
            },
            "k": 10,
            "num_candidates": 100
          }
        }
      ]
    }
  },
  "_source": [
    "my_field"
  ]
}</code></pre><p><a href="https://www.elastic.co/kr/search-labs/blog/better-binary-quantization-lucene-elasticsearch#lucene-benchmarking">이 게시물에서</a> 권장하는 바와 같이, 리스코어링과 오버샘플링은 압축의 이점을 활용하면서 높은 리콜 정확도를 유지하는 데 도움이 되므로 데이터 양이 많지 않은 경우 확장하는 것이 좋습니다. Elasticsearch 버전 8.18부터는 <a href="https://www.elastic.co/kr/guide/en/elasticsearch/reference/8.18/knn-search.html#dense-vector-knn-search-rescoring">rescore_vector를</a> 사용하여 이 작업을 수행할 수 있습니다. 예상 출력은 출력 폴더의 <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/how-and-why-bbq/Outputs/06-bbq-search-8-18-output.json">06-bbq-search-8-18-output.json</a> 파일에 있습니다.</p><pre><code>GET bbq-my-byte-quantized-index/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "knn": {
            "field": "my_vector",
            "query_vector_builder": {
              "text_embedding": {
                "model_id": "my_e5_model",
                "model_text": "my awesome search field"
              }
            },
            "rescore_vector": {
              "oversample": 3
            },
            "k": 10,
            "num_candidates": 100
          }
        }
      ]
    }
  },
  "_source": [
    "my_field"
  ]
}</code></pre><p>이 점수는 원시 데이터에 대해 얻을 수 있는 점수와 어떻게 비교될까요? 위의 모든 작업을 다시 수행하되 <code>index_options.type: hnsw</code> 을 사용하면 점수가 매우 비슷하다는 것을 알 수 있습니다. 예상 출력은 출력 폴더의 <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/how-and-why-bbq/Outputs/07-raw-vector-output.json">07-raw-vector-output.json</a> 파일에서 확인할 수 있습니다.</p><pre><code>PUT my-raw-vector-index
{
  "mappings": {
    "properties": {
      "my_field": {
        "type": "text"
      },
      "my_vector": {
        "type": "dense_vector",
        "dims": 384,
        "index_options": {
          "type": "hnsw"
        }
      }
    }
  }
}

PUT my-raw-vector-index/_doc/1?pipeline=my_inference_pipeline
{
    "my_field": "my awesome text field"
}

PUT my-raw-vector-index/_doc/2?pipeline=my_inference_pipeline
{
    "my_field": "some other sentence"
}

GET my-raw-vector-index/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "knn": {
            "field": "my_vector",
            "query_vector_builder": {
              "text_embedding": {
                "model_id": "my_e5_model",
                "model_text": "my awesome search field"
              }
            },
            "k": 10,
            "num_candidates": 100
          }
        }
      ]
    }
  },
  "_source": [
    "my_field"
  ]
}</code></pre><h2>압축률에 대한 대략적인 수치</h2><p>벡터 검색으로 작업할 때 저장 공간과 메모리 요구 사항은 금방 큰 문제가 될 수 있습니다. 다음 분석은 다양한 양자화 기법이 벡터 데이터의 메모리 사용량을 어떻게 획기적으로 줄이는지 보여줍니다.</p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">벡터(V)</td><td style="border: 1px solid #ccc; padding: 4px;">치수(D)</td><td style="border: 1px solid #ccc; padding: 4px;">원시(V x D x 4)</td><td style="border: 1px solid #ccc; padding: 4px;">int8 (V x (D x 1 + 4))</td><td style="border: 1px solid #ccc; padding: 4px;">int4 (V x (D x 0.5 + 4))</td><td style="border: 1px solid #ccc; padding: 4px;">bbq (V x (D x 0.125 + 4))</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">10,000,000</td><td style="border: 1px solid #ccc; padding: 4px;">384</td><td style="border: 1px solid #ccc; padding: 4px;">14.31GB</td><td style="border: 1px solid #ccc; padding: 4px;">3.61GB</td><td style="border: 1px solid #ccc; padding: 4px;">1.83GB</td><td style="border: 1px solid #ccc; padding: 4px;">0.58GB</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">50,000,000</td><td style="border: 1px solid #ccc; padding: 4px;">384</td><td style="border: 1px solid #ccc; padding: 4px;">71.53GB</td><td style="border: 1px solid #ccc; padding: 4px;">18.07GB</td><td style="border: 1px solid #ccc; padding: 4px;">9.13GB</td><td style="border: 1px solid #ccc; padding: 4px;">2.89GB</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">100,000,000</td><td style="border: 1px solid #ccc; padding: 4px;">384</td><td style="border: 1px solid #ccc; padding: 4px;">143.05GB</td><td style="border: 1px solid #ccc; padding: 4px;">36.14GB</td><td style="border: 1px solid #ccc; padding: 4px;">18.25GB</td><td style="border: 1px solid #ccc; padding: 4px;">5.77GB</td>
                    </tr>
              </tbody>
            </table>
          <h2>결론</h2><p>BBQ는 정확도 저하 없이 압축을 위해 벡터 데이터에 적용할 수 있는 최적화 기능입니다. 벡터를 비트로 변환하여 데이터를 효과적으로 검색하고 AI 워크플로우를 확장하여 검색을 가속화하고 데이터 저장 공간을 최적화할 수 있도록 지원합니다.</p><h2>추가 학습</h2><p>BBQ에 대해 더 자세히 알고 싶다면 다음 리소스를 확인하세요:</p><ul><li><a href="https://www.elastic.co/kr/search-labs/blog/better-binary-quantization-lucene-elasticsearch">루씬과 Elasticsearch의 이진 정량화(BBQ)</a></li><li><a href="https://www.elastic.co/kr/search-labs/blog/bit-vectors-elasticsearch-bbq-vs-pq">더 나은 바이너리 정량화(BBQ) 대 제품 정량화</a></li><li><a href="https://www.elastic.co/kr/search-labs/blog/optimized-scalar-quantization-elasticsearch">최적화된 스칼라 양자화: 더욱 향상된 바이너리 양자화</a></li><li><a href="https://www.youtube.com/watch?v=04NzMt2Nigc">더 나은 바이너리 정량화(BBQ): 바이트에서 BBQ로, 더 나은 벡터 검색의 비결, 벤 트렌트의 글</a></li></ul>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Elasticsearch 힙 크기 사용량 및 JVM 가비지 수집]]></title>
            <description><![CDATA[모범 사례와 힙 메모리 사용량이 너무 많거나 JVM 성능이 최적이 아닐 때 문제를 해결하는 방법을 포함해 Elasticsearch 힙 크기 사용량과 JVM 가비지 수집에 대해 살펴봅니다.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/elasticsearch-heap-size-jvm-garbage-collection</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/elasticsearch-heap-size-jvm-garbage-collection</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <pubDate>Tue, 22 Apr 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/b162c28623b9070fd1980bcd891b9dd1e868f2f0-720x421.jpg?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>힙 크기는 Elasticsearch 노드의 Java 가상 머신에 할당된 RAM의 양입니다.</p><p>버전 7.11부터 Elasticsearch는 기본적으로 노드의 역할과 총 메모리를 기반으로 JVM 힙 크기를 자동으로 설정합니다. 대부분의 프로덕션 환경에서는 기본 사이징을 사용하는 것이 좋습니다. 그러나 JVM 힙 크기를 수동으로 설정하려면 일반적으로 -Xms 및 -Xmx를 동일한 값으로 설정해야 하며, 최대 (대략) 31GB를 기준으로 총 사용 가능한 RAM의 50% 이 되어야 합니다.</p><p>힙 크기가 클수록 노드에 인덱싱 및 검색 작업을 위한 더 많은 메모리를 확보할 수 있습니다. 그러나 노드에는 캐싱을 위한 메모리도 필요하므로 50% 을 사용하면 둘 사이의 균형이 잘 유지됩니다. 프로덕션 환경에서도 이와 같은 이유로 Elasticsearch와 동일한 노드에서 다른 메모리 집약적인 프로세스를 사용하지 않아야 합니다.</p><p>일반적으로 힙 사용량은 톱니 모양 패턴을 따르며, 사용 중인 최대 힙의 약 30~70%(% ) 사이에서 진동합니다. 이는 가비지 수집 프로세스가 메모리를 다시 확보할 때까지 JVM이 힙 사용률을 꾸준히 증가시키기 때문입니다. 높은 힙 사용량은 가비지 수집 프로세스가 따라잡지 못할 때 발생합니다. 힙 사용량이 많다는 지표는 가비지 컬렉션이 힙 사용량을 약 30개% 로 줄일 수 없는 경우입니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/0a17a67cc589a3c1fbf9e918eadc119df7bd7619-858x278.png" style="width: 100%;" /><p>위 이미지에서 JVM 힙의 일반적인 톱니 모양을 볼 수 있습니다.</p><p>또한 가비지 컬렉션에는 젊은 가비지 컬렉션과 오래된 가비지 컬렉션의 두 가지 유형이 있음을 알 수 있습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/8df5c24c4894404de4617be7a13683c9027d607d-875x281.png" style="width: 100%;" /><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/e01eb2537310b052580411153b8eddc187d97687-890x264.png" style="width: 100%;" /><p>정상적인 JVM에서 가비지 컬렉션은 다음 조건을 이상적으로 충족해야 합니다:</p><ul><li>젊은 GC는 빠르게 처리됩니다(50ms 이내).</li><li>젊은 GC는 자주 실행되지 않습니다(약 10초).</li><li>이전 GC는 빠르게 처리됩니다(1초 이내).</li><li>이전 GC는 자주 실행되지 않습니다(10분에 한 번 이상).</li></ul><h3><strong>힙 메모리 사용량이 너무 많거나 JVM 성능이 최적이 아닌 경우 해결하는 방법</strong></h3><p>힙 메모리 사용량이 증가하는 데에는 여러 가지 이유가 있을 수 있습니다:</p><h4><strong>오버하딩</strong></h4><p>오버하딩에 대한 문서는 <a href="https://www.elastic.co/docs/deploy-manage/production-guidance/optimize-performance/size-shards#sizing-shard-guidelines">여기를</a> 참조하세요.</p><h4><strong>대규모 집계 크기</strong></h4><p>집계 크기가 커지는 것을 방지하려면 쿼리의 집계 버킷 수(크기)를 최소한으로 유지하세요.</p><pre><code>GET /_search
{
   "aggs" : {
       "products" : {
           "terms" : {
               "field" : "product",
               "size" : 5
                          }
       }
   }
}</code></pre><p>느린 쿼리 로깅(느린 로그)을 사용하고 다음을 사용하여 특정 인덱스에 구현할 수 있습니다.</p><pre><code>PUT /my_index/_settings
{
   "index.search.slowlog.threshold.query.warn": "10s",
   "index.search.slowlog.threshold.query.info": "5s",
   "index.search.slowlog.threshold.query.debug": "2s",
   "index.search.slowlog.threshold.query.trace": "500ms",
   "index.search.slowlog.threshold.fetch.warn": "1s",
   "index.search.slowlog.threshold.fetch.info": "800ms",
   "index.search.slowlog.threshold.fetch.debug": "500ms",
   "index.search.slowlog.threshold.fetch.trace": "200ms",
   "index.search.slowlog.level": "info"
}</code></pre><p>결과를 반환하는 데 시간이 오래 걸리는 쿼리는 리소스 집약적인 쿼리일 가능성이 높습니다.</p><h4><strong>과도한 벌크 인덱스 크기</strong></h4><p>대량의 요청을 전송하는 경우 힙 사용량이 많은 원인이 될 수 있습니다. 대량 인덱스 요청의 크기를 줄이세요.</p><h4><strong>매핑 문제</strong></h4><p>특히 &quot;fielddata: true&quot;를 사용하는 경우, 이는 JVM 힙의 주요 사용자가 될 수 있습니다.</p><h4><strong>힙 크기가 잘못 설정됨</strong></h4><p>힙 크기는 수동으로 정의할 수 있습니다:</p><p>환경 변수 설정하기:</p><pre><code>ES_JAVA_OPTS="-Xms2g -Xmx2g"</code></pre><p>Elasticsearch 구성 디렉터리에서 jvm.options 파일을 편집합니다:</p><pre><code>-Xms2g
-Xmx2g</code></pre><p>환경 변수 설정은 파일 설정보다 우선합니다.</p><p>설정을 적용하려면 노드를 다시 시작해야 합니다.</p><h4><strong>JVM 새 비율이 잘못 설정됨</strong></h4><p>Elasticsearch는 기본적으로 이 값을 설정하므로 일반적으로 이 값을 설정할 필요가 없습니다. 이 매개변수는 JVM에서 &quot;신세대&quot; 및 &quot;구세대&quot; 오브젝트에 사용할 수 있는 공간의 비율을 정의합니다.</p><p>오래된 GC가 매우 자주 발생하는 경우, Elasticsearch 구성 디렉터리의 jvm.options 파일에서 이 값을 구체적으로 설정해 볼 수 있습니다.</p><pre><code>-XX:NewRatio=3</code></pre><h3><strong>대규모 Elasticsearch 클러스터에서 힙 크기 사용량과 JVM 가비지 수집을 관리하기 위한 모범 사례는 무엇인가요?</strong></h3><p>대규모 Elasticsearch 클러스터에서 힙 크기 사용량과 JVM 가비지 수집을 관리하기 위한 모범 사례는 힙 크기를 사용 가능한 RAM의 최대 50% 로 설정하고 JVM 가비지 수집 설정이 특정 사용 사례에 최적화되도록 하는 것입니다. 클러스터가 최적으로 실행되고 있는지 확인하기 위해 힙 크기와 가비지 수집 메트릭을 모니터링하는 것이 중요합니다. 특히 JVM 힙 크기, 가비지 수집 시간, 가비지 수집 일시 중지를 모니터링하는 것이 중요합니다. 또한 쓰레기 수거 횟수와 쓰레기 수거에 소요되는 시간을 모니터링하는 것도 중요합니다. 이러한 메트릭을 모니터링하면 힙 크기나 가비지 수집 설정에 잠재적인 문제가 있는지 파악하고 필요한 경우 수정 조치를 취할 수 있습니다.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Elasticsearch에서 기본 샤드 수를 늘리는 방법]]></title>
            <description><![CDATA[Elasticsearch의 split 및 reindex API를 사용하여 기본 샤드 수를 늘려 최적의 샤드 확장을 구현하는 방법을 알아보세요.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/elasticsearch-increase-primary-shard-count</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/elasticsearch-increase-primary-shard-count</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <pubDate>Thu, 17 Apr 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/7034b76019a0cba52c25eda29fceb18afc96ed0b-720x420.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>기존 인덱스의 기본 샤드 수는 늘릴 수 없으므로, 기본 샤드 수를 늘리려면 인덱스를 다시 생성해야 합니다. 이러한 상황에서 일반적으로 사용되는 메서드는 _reindex API와 _split API의 두 가지입니다.</p><p>split API는 _reindex API보다 더 빠른 방법인 경우가 많습니다. 두 작업 전에 <strong>인덱싱을</strong> <strong>중지해야</strong> 하며, 그렇지 않으면 source_index와 target_index 문서 수가 달라집니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/aa0ae010c2f5691ca00440fb453ed6b47bacd24f-1200x628.png" style="width: 100%;" /><h2>방법 1 - 분할 API 사용</h2><p>분할 API는 설정을 복사하고 기존 인덱스를 매핑하여 원하는 기본 샤드 수로 새 인덱스를 생성하는 데 사용됩니다. 생성 시 원하는 기본 샤드 수를 설정할 수 있습니다. 분할 API를 구현하기 전에 다음 설정을 확인해야 합니다:</p><ol><li>소스 인덱스는 읽기 전용이어야 합니다. 즉, 인덱싱 프로세스를 중지해야 합니다.</li><li>대상 인덱스의 기본 샤드 수는 소스 인덱스의 기본 샤드 수의 배수여야 합니다. 예를 들어, 소스 인덱스에 기본 샤드가 5개인 경우, 대상 인덱스 기본 샤드를 10,15,20 등으로 설정할 수 있습니다.</li></ol><p>참고: 기본 샤드 번호만 변경해야 하는 경우, 재인덱스 API보다 훨씬 빠른 분할 API를 사용하는 것이 좋습니다.</p><h3>분할 API 구현하기</h3><p>테스트 인덱스를 만듭니다:</p><pre><code>POST test_split_source/_doc
{
  "test": "test"
}</code></pre><p>소스 인덱스는 읽기 전용이어야 분할할 수 있습니다:</p><pre><code>PUT test_split_source/_settings
{
  "index.blocks.write": true
}</code></pre><p>설정 및 매핑은 소스 인덱스에서 자동으로 복사됩니다:</p><pre><code>POST /test_split_source/_split/test_split_target
{
  "settings": {
    "index.number_of_shards": 3
  }
}</code></pre><p>진행 상황을 확인할 수 있습니다:</p><pre><code>GET _cat/recovery/test_split_target?v&h=index,shard,time,stage,files_percent,files_total</code></pre><p>설정과 매핑은 소스 인덱스에서 복사되므로 대상 인덱스는 읽기 전용입니다. 대상 인덱스에 대한 쓰기 작업을 활성화해 보겠습니다:</p><pre><code>PUT test_split_target/_settings
{
    "index.blocks.write": null
}</code></pre><p>원본 인덱스를 삭제하기 전에 소스 및 대상 인덱스 docs.count를 확인하세요:</p><pre><code>GET _cat/indices/test_split*?v&h=index,pri,rep,docs.count</code></pre><p>인덱스 이름과 별칭 이름은 같을 수 없습니다. 소스 인덱스를 삭제하고 소스 인덱스 이름을 대상 인덱스에 별칭으로 추가해야 합니다:</p><pre><code>DELETE test_split_source
PUT /test_split_target/_alias/test_split_source</code></pre><p><strong>test_split_source</strong> 별칭을 <strong>test_split_target</strong> 인덱스에 추가한 후 이를 테스트해야 합니다:</p><pre><code>GET test_split_source
POST test_split_source/_doc
{
  "test": "test"
}</code></pre><h2>방법 2 - 재인덱스 API 사용</h2><p>리인덱스 API로 새 인덱스를 생성하면 기본 샤드 개수를 얼마든지 지정할 수 있습니다. 원하는 수의 기본 샤드로 새 인덱스를 생성한 후, 소스 인덱스의 모든 데이터를 이 새 인덱스로 다시 색인할 수 있습니다.</p><p>분할 API 기능 외에도, 재인덱스 AP의 ingest_pipeline을 사용하여 데이터를 조작할 수 있습니다. 수집 파이프라인을 사용하면 필터에 맞는 지정된 필드만 쿼리를 사용하여 대상 인덱스로 색인됩니다. 간편한 스크립트를 사용하여 데이터 콘텐츠를 변경할 수 있으며, 여러 인덱스를 단일 인덱스로 병합할 수 있습니다.</p><h3>재색인 API 구현하기</h3><p>테스트 재인덱스를 만듭니다:</p><pre><code>POST test_reindex_source/_doc
{
    "test": "test"
}</code></pre><p>소스 인덱스에서 설정 및 매핑을 복사합니다:</p><pre><code>GET test_reindex_source</code></pre><p>설정, 매핑, 원하는 샤드 수로 대상 인덱스를 생성합니다:</p><pre><code>PUT test_reindex_target
{
  "mappings" : {},
  "settings": {
    "number_of_shards": 10,
    "number_of_replicas": 0,
    "refresh_interval": -1
  }
}</code></pre><p>*주: number_of_replicas: 0, refresh_interval: -1로 설정하면 재인덱싱 속도가 빨라집니다.</p><p>재색인 프로세스를 시작합니다. requests_per_second=-1 및 slices=auto를 설정하면 재인덱스 속도가 조정됩니다.</p><pre><code>POST _reindex?requests_per_second=-1&slices=auto&wait_for_completion=false
{
  "source": {
    "index": "test_reindex_source"
  },
  "dest": {
    "index": "test_reindex_target"
  }
}</code></pre><p>재인덱스 API를 실행하면 task_id를 볼 수 있습니다. 이를 복사하여 _tasks API로 확인합니다:</p><pre><code>GET _tasks/<task_id></code></pre><p>재색인 작업이 완료된 후 설정을 업데이트합니다:</p><pre><code>PUT test_reindex_target/_settings
{
  "number_of_replicas": 1,
  "refresh_interval": "1s"
}</code></pre><p>원본 인덱스를 삭제하기 전에 소스 인덱스와 대상 인덱스 docs.count를 확인하면 동일해야 합니다:</p><pre><code>GET _cat/indices/test_reindex_*?v&h=index,pri,rep,docs.count</code></pre><p>인덱스 이름과 별칭 이름은 같을 수 없습니다. 소스 인덱스를 삭제하고 소스 인덱스 이름을 대상 인덱스에 별칭으로 추가합니다:</p><pre><code>DELETE test_reindex_source
PUT /test_reindex_target/_alias/test_reindex_source</code></pre><p>test_split_source 별칭을 test_split_target 인덱스에 추가한 후 다음을 사용하여 테스트합니다:</p><pre><code>GET test_reindex_source</code></pre><h2>요약</h2><p>기존 인덱스의 기본 샤드 수를 늘리려면 새 인덱스에 대한 설정과 매핑을 다시 만들어야 합니다. 이를 위한 두 가지 주요 방법은 재색인 API와 분할 API입니다. 두 방법 중 하나를 사용하기 전에 활성 인덱싱을 중지해야 합니다.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Elasticsearch BBQ와 OpenSearch FAISS: 벡터 검색 성능 비교]]></title>
            <description><![CDATA[Elasticsearch BBQ와 OpenSearch FAISS의 성능 비교.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/elasticsearch-bbq-vs-opensearch-faiss</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/elasticsearch-bbq-vs-opensearch-faiss</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Ugo Sangiorgi]]></dc:creator>
            <pubDate>Tue, 15 Apr 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/796de4829e29566f1f3efa2482f5c3e54b31b1d6-1536x1024.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p><strong>이진 양자화를 통한 벡터 검색: BBQ가 포함된 Elasticsearch는 FAISS가 포함된 OpenSearch보다 5배 빠릅니다</strong>. Elastic은 특히 시맨틱 검색/벡터 검색 영역에서 Elasticsearch와 OpenSearch 간의 성능 차이를 명확히 해달라는 커뮤니티의 요청을 받았기 때문에 명확한 데이터 기반 비교를 제공하기 위해 이러한 성능 테스트를 수행했습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/7f4f8d08f7bee188423e4e65f0caefc7e34f0355-1600x681.png" style="width: 100%;" /><h2>이진 양자화 대결</h2><p>고차원 벡터를 원래 형태로 저장하는 것은 메모리 집약적일 수 있습니다. 양자화 기술은 이러한 벡터를 콤팩트한 표현으로 압축하여 메모리 사용량을 대폭 줄여줍니다. 그러면 검색이 압축된 공간에서 작동하므로 계산 복잡성이 줄어들고 특히 대규모 데이터 세트에서 검색 속도가 빨라집니다.</p><p>Elastic은 Lucene을 최고 성능의 벡터 엔진으로 만들기 위해 최선을 다하고 있습니다. 우리는 Lucene을 기반으로 Elasticsearch 8.16에서 <a href="https://www.elastic.co/kr/search-labs/blog/better-binary-quantization-lucene-elasticsearch">더 나은 이진 정량화</a> (BBQ)를 도입했고 8.18과 9.0에서 이를 더욱 발전시켰습니다. BBQ는 플로트32 차원을 비트로 축소하는 새로운 <a href="https://www.elastic.co/kr/search-labs/blog/optimized-scalar-quantization-elasticsearch">스칼라 양자화</a> 방식을 기반으로 구축되어 높은 순위 품질을 유지하면서 최대 95%의% 메모리 절감 효과를 제공합니다.</p><p>반면 OpenSearch는 여러 벡터 엔진, 즉 nmslib(현재는 더 이상 사용되지 않음), Lucene 및 FAISS를 사용합니다. <a href="https://www.elastic.co/kr/search-labs/blog/elasticsearch-opensearch-vector-search-performance-comparison">이전 블로그에서</a> 벡터 검색을 위해 Elasticsearch와 OpenSearch를 비교한 적이 있습니다. 세 가지 데이터 세트를 사용하여 두 제품에서 서로 다른 엔진 및 구성 조합을 테스트했습니다.</p><p>이 블로그에서는 현재 두 제품에서 사용할 수 있는 이진 양자화 알고리즘에 초점을 맞춥니다. <a href="https://github.com/elastic/rally-tracks/edit/master/openai_vector">openai_vector</a> Rally 트랙을 사용하여 BBQ와 함께 Elasticsearch를 테스트하고 <a href="https://opensearch.org/docs/latest/search-plugins/knn/knn-vector-quantization/#binary-quantization">FAISS의 이진 정량화를 통해 OpenSearch를 테스트했습니다.</a></p><p>주요 목표는 동일한 리콜 수준에서 두 솔루션의 성능을 평가하는 것이었습니다. <em>리콜이란</em> 무엇을 의미하나요? 리콜은 검색 시스템에서 얼마나 많은 관련 결과를 성공적으로 검색했는지를 측정하는 지표입니다.</p><p>이 평가에서는 recall@k가 특히 중요한데, 여기서 <em>k는</em> 고려되는 상위 결과의 수를 나타냅니다. 따라서 <strong>리콜@10</strong>, <strong>리콜@50 및 리콜@100은</strong> 각각 검색된 상위 10개, 50개 및 100개의 항목에서 얼마나 많은 실제 관련성 있는 결과가 나타나는지 측정합니다. 리콜은 0에서 1(또는 0% ~ 100% 정밀도)의 척도로 표시됩니다. 이는 리콜이 항상 1(100%)인 정확한 KNN이 아닌 근사 KNN(ANN)에 대해 이야기하고 있기 때문에 중요합니다.</p><p><em>k의</em> 각 값에 대해 최종 순위를 적용하기 전에 고려되는 후보의 수인 <em>n도 </em>지정했습니다. 즉, Recall@10, Recall@50 및 Recall@100의 경우 시스템은 먼저 이진 양자화 알고리즘을 사용하여 <em>n개의</em> 후보를 검색한 다음 상위 <em>k개의</em> 결과에 예상되는 관련 항목이 포함되어 있는지 여부를 판단하기 위해 순위를 매깁니다.</p><p><em>n을</em> 제어함으로써 효율성과 정확성 사이의 균형을 분석할 수 있습니다. 일반적으로 <em>n이</em> 높을수록 순위를 매길 수 있는 후보가 많아져 리콜률이 <strong>높아지지만</strong> 지연 시간이 <strong>길어지고</strong> 처리량도<strong> 감소합니다 </strong>. 반대로 <em>n이</em> 낮을수록 검색 속도가 빨라지지만 초기 세트에 관련 후보가 너무 적게 포함될 경우 검색 회수율이 떨어질 수 있습니다.</p><p>이 비교에서 Elasticsearch는 동일한 설정에서 OpenSearch보다 더 낮은 지연 시간과 더 높은 처리량을 보여주었습니다.</p><h2>방법론</h2><p>전체 구성과 함께 Terraform 스크립트, Kubernetes 매니페스트 및 특정 Rally 트랙은 이 <a href="https://github.com/elastic/elasticsearch-opensearch-vector-performance/tree/bbq">리포지토리에서</a> <a href="https://github.com/elastic/elasticsearch-opensearch-vector-performance/tree/bbq/rally-custom/custom_tracks/elasticsearch/openai_vector_bq"><em>openai_vector_bq</em></a> 아래에서 확인할 수 있습니다.</p><p>이전 벤치마크와 마찬가지로 다음과 같이 구성된 Kubernetes 클러스터를 사용했습니다:</p><ul><li>3개의 <code>e2-standard-32</code> 시스템(128GB RAM 및 32 CPU)을 갖춘 Elasticsearch 9.0용 노드 풀 1개</li><li>OpenSearch 2.19용 노드 풀 1개, <code>e2-standard-32</code> 머신 3대(128GB RAM 및 32개 CPU)</li><li>2개의 <code>e2-standard-4</code> 머신(16GB RAM 및 4개의 CPU)이 있는 Rally용 노드 풀 1개</li></ul><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/169bb6033f6eebfd1b177b3446bf916fde4ee5c5-1600x856.png" style="width: 100%;" /><p>하나의 Elasticsearch 클러스터 버전 9.0과 하나의 OpenSearch 클러스터 버전 2.19를 설정했습니다.</p><p>Elasticsearch와 OpenSearch 모두 정확히 동일한 설정으로 테스트했습니다. OpenAI의 텍스트 임베딩-ada-002 <a href="https://openai.com/blog/new-and-improved-embedding-model">모델을 사용해 생성된</a> <a href="https://huggingface.co/datasets/BeIR/nq">임베딩으로 강화된 NQ 데이터 세트의</a> 250만 개의 문서를 사용하는 <a href="https://github.com/elastic/rally-tracks/edit/master/openai_vector">openai_vector</a> Rally 트랙을 <a href="https://github.com/elastic/elasticsearch-opensearch-vector-performance/commit/b97d5d95c22c8cf862f2030964524bdd156a5da3">일부 수정하여 사용했습니다.</a></p><pre><code>{
  "source-file": "open_ai_corpus-initial-indexing.json.bz2",
  "document-count": 2580961,
  "compressed-bytes": 32076749416,
  "uncompressed-bytes": 90263571686
}</code></pre><p>검색 작업을 수행하기 위해 8개의 동시 클라이언트를 사용하여 다양한 리콜 수준(리콜@10, 리콜@50, 리콜@100)에서 측정된 지연 시간 및 처리량에 대한 결과 보고서입니다. 복제본 없이 단일 샤드를 사용했습니다.</p><p>다음과 같은 k-n-점수 조합을 실행했습니다. 10-2000-2000 또는 <em>k:10</em>, <em>n:2000</em> 및 <em>rescore:2000</em> 은 2000개의 결과에 대해 재점수(&#x27;과대 표본 계수&#x27; 1에 해당)를 적용하여 n개의 후보(2000개) 중 상위 k(10개)를 검색합니다. 각 검색은 워밍업으로 1000회의 검색으로 10.000회 실행되었습니다:</p><p></p><p><strong><span style="text-decoration:underline">Recall@10</span></strong></p><ul><li>10-40-40</li><li>10-50-50</li><li>10-100-100</li><li>10-200-200</li><li>10-500-500</li><li>10-750-750</li><li>10-1000-1000</li><li>10-1500-1500</li><li>10-2000-2000</li></ul><p><strong><span style="text-decoration:underline">Recall@50</span></strong></p><ul><li>50-150-150</li><li>50-200-200</li><li>50-250-250</li><li>50-500-500</li><li>50-750-750</li><li>50-1000-1000</li><li>50-1200-1200</li><li>50-1500-1500</li><li>50-2000-2000</li></ul><p><strong><span style="text-decoration:underline">Recall@100</span></strong></p><ul><li>100-200-200</li><li>100-250-250</li><li>100-300-300</li><li>100-500-500</li><li>100-750-750</li><li>100-1000-1000</li><li>100-1200-1200</li><li>100-1500-1500</li><li>100-2000-2000</li></ul><p>벤치마크를 복제하기 위해, rally-elasticsearch와 rally-opensearch 모두에 대한 Kubernetes 매니페스트에는 모든 관련 변수가 컨피그맵에 외부화되어 있으며, <a href="https://github.com/elastic/elasticsearch-opensearch-vector-performance/blob/bbq/k8s/rally-openai_vector-es-bq.yml">여기</a> (ES)와 <a href="https://github.com/elastic/elasticsearch-opensearch-vector-performance/blob/bbq/k8s/rally-openai_vector-os-bq.yml">여기</a> (OS)에서 확인할 수 있습니다. <em>search_ops</em> 매개변수는 k, n, rescore의 모든 조합을 테스트하도록 사용자 지정할 수 있습니다.</p><h3>OpenSearch Rally 구성</h3><p><code>/k8s/rally-openai_vector-os-bq.yml</code></p><pre><code>apiVersion: v1
kind: ConfigMap
metadata:
  name: rally-params-os
  labels:
    app: rally-opensearch
data:
  user-tags.json: |
    {
      "product": "OpenSearch",
      "product-version": "OpenSearch-2.19.0",
      "product-label": "OpenSearch-2.19-faiss",
      "benchmark-run": "19-feb-recall@100"
    }
  track-params.json: |
    {
      "mapping_type": "vectors-only-mapping-with-docid",
      "standalone_search_clients": 8,
      "standalone_search_iterations": 5000,
      "ann_threshold": 0,
      "vector_mode": "on_disk",
      "compression_level": "32x",
      "vector_method_name": "hnsw",
      "vector_method_engine": "faiss",
      "search_ops": [
        [100, 200, 200],
        [100, 250, 250],
        [100, 300, 300],
        [100, 500, 500],
        [100, 750, 750],
        [100, 1000, 1000],
        [100, 1200, 1200],
        [100, 1500, 1500],
        [100, 2000, 2000]
      ]
    }</code></pre><h3>검색 인덱스 구성</h3><p>그런 다음 컨피그맵의 변수가 인덱스 구성에 사용되며, 일부 매개변수는 변경되지 않은 상태로 유지됩니다. OpenSearch의 1비트 양자화는 <a href="https://opensearch.org/docs/latest/search-plugins/knn/knn-vector-quantization/#binary-quantization">압축 수준을 &quot;32배&quot;로 설정하여 구성</a>합니다.</p><p><code>index-vectors-only-mapping-with-docid-mapping.json</code></p><pre><code>{
  "settings": {
    {% if preload_pagecache %}
    "index.store.preload": [
      "vec", "vex", "vem", "veq", "veqm", "veb", "vebm"
    ],
    {% endif %}
    "index.number_of_shards": {{ number_of_shards | default(1) }},
    "index.number_of_replicas": {{ number_of_replicas | default(0) }},
    "index.knn": true,
    "index.knn.advanced.approximate_threshold": {{ ann_threshold | default(15000) }}
  },
  "mappings": {
    "dynamic": false,
    "properties": {
      "docid": {
        "type": "keyword"
      },
      "emb": {
        "type": "knn_vector",
        "dimension": 1536,
        "space_type": "innerproduct",
        "data_type": "float",
        "mode": {{ vector_mode | default("in_memory") | tojson }},
        "compression_level": {{ compression_level | default("32x") | tojson }},
        "method": {
          "name": {{ vector_method_name | default("hnsw") | tojson }},
          "engine": {{ vector_method_engine | default("faiss") | tojson }},
          "parameters": {
            "ef_construction": 100,
            "m": 16
          }
        }
      }
    }
  }
}</code></pre><h3>Elasticsearch Rally 구성</h3><p><code>/k8s/rally-openai_vector-es-bq.yml</code></p><pre><code>apiVersion: v1
kind: ConfigMap
metadata:
  name: rally-params-es
  labels:
    app: rally-elasticsearch
data:
  user-tags.json: |
    {
      "product": "Elasticsearch",
      "product-version": "Elasticsearch-9.0.0-ade01164",
      "product-label": "Elasticsearch-9.0-BBQ",
      "benchmark-run": "19-feb-recall@100"
    }
  track-params.json: |
    {
      "mapping_type": "vectors-only-mapping-with-docid",
      "standalone_search_clients": 8,
      "standalone_search_iterations": 5000,
      "vector_index_type": "bbq_hnsw",
      "search_ops": [
        [100, 200, 200],
        [100, 250, 250],
        [100, 300, 300],
        [100, 500, 500],
        [100, 750, 750],
        [100, 1000, 1000],
        [100, 1200, 1200],
        [100, 1500, 1500],
        [100, 2000, 2000]
      ]
    }</code></pre><h3>Elasticsearch 인덱스 구성</h3><p><code>index-vectors-only-mapping-with-docid-mapping.json</code></p><pre><code>{
  "settings": {
    {# non-serverless-index-settings-marker-start #}
    {%- if build_flavor != "serverless" or serverless_operator == true -%}
    {% if preload_pagecache %}
    "index.store.preload": [ "vec", "vex", "vem", "veq", "veqm", "veb", "vebm" ],
    {% endif %}
    "index.number_of_shards": {{ number_of_shards | default(1) }},
    "index.number_of_replicas": {{ number_of_replicas | default(0) }}
    {%- endif -%}
    {# non-serverless-index-settings-marker-end #}
  },
  "mappings": {
    "dynamic": false,
    "properties": {
      "docid": {
        "type": "keyword"
      },
      "emb": {
        "type": "dense_vector",
        "element_type": "float",
        "dims": 1536,
        "index": true,
        "similarity": "dot_product",
        "index_options": {
          "type": {{ vector_index_type | default("bbq_hnsw") | tojson }},
          "ef_construction": 100,
          "m": 16
        }
      }
    }
  }
}</code></pre><h2>결과</h2><p>결과를 해석하는 방법에는 여러 가지가 있습니다. 지연 시간과 처리량 모두에 대해 각 리콜 수준에 따라 단순화된 차트와 상세한 차트를 표시했습니다. 각 지표에 대해 &quot;높을수록 좋다&quot;고 생각하면 차이를 쉽게 알 수 있습니다. 그러나 지연 시간은 음수(낮을수록 좋음)인 반면 처리량은 양수입니다. 단순화된 차트에서는 <strong>(리콜/레이턴시) * 10000 </strong>(간단히 &#x27;속도&#x27;라고 함)과<strong> 리콜 * 처리량을</strong> 사용했으므로 두 지표 모두 속도가 빠르고 처리량이 많을수록 더 좋다는 의미입니다. 시작해 보겠습니다.</p><h3>리콜 @ 10 - 단순화</h3><p>이 수준의 리콜에서 Elasticsearch BBQ는 OpenSearch FAISS보다 최대 <strong>5배 </strong>(평균 3.9배) 빠르며 평균 <strong>3.2배 더 많은 처리량을</strong> 제공합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/d580ad53e8974bd3aa75957c413a0136c4e465c5-1600x681.png" style="width: 100%;" /><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/18edce667fe36ab95033264ef8df6f352dda2425-2044x866.png" style="width: 100%;" /><h4>리콜 @ 10 - 상세</h4><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/a7459b87e679f4ad963d0e2f1685499b40f6f050-1600x799.png" style="width: 100%;" /><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/c6036f55a13377654d296eb3148c7199e1965475-1600x799.png" style="width: 100%;" />
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;">작업</td><td style="border: 1px solid #ccc; padding: 4px;">latency.mean</td><td style="border: 1px solid #ccc; padding: 4px;">throughput.mean</td><td style="border: 1px solid #ccc; padding: 4px;">avg_recall</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch-9.0-BBQ</td><td style="border: 1px solid #ccc; padding: 4px;">10-100-100</td><td style="border: 1px solid #ccc; padding: 4px;">11.70</td><td style="border: 1px solid #ccc; padding: 4px;">513.58</td><td style="border: 1px solid #ccc; padding: 4px;">0.89</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch-9.0-BBQ</td><td style="border: 1px solid #ccc; padding: 4px;">10-1000-100</td><td style="border: 1px solid #ccc; padding: 4px;">27.33</td><td style="border: 1px solid #ccc; padding: 4px;">250.55</td><td style="border: 1px solid #ccc; padding: 4px;">0.95</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch-9.0-BBQ</td><td style="border: 1px solid #ccc; padding: 4px;">10-1500-1500</td><td style="border: 1px solid #ccc; padding: 4px;">35.93</td><td style="border: 1px solid #ccc; padding: 4px;">197.26</td><td style="border: 1px solid #ccc; padding: 4px;">0.95</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch-9.0-BBQ</td><td style="border: 1px solid #ccc; padding: 4px;">10-200-200</td><td style="border: 1px solid #ccc; padding: 4px;">13.33</td><td style="border: 1px solid #ccc; padding: 4px;">456.16</td><td style="border: 1px solid #ccc; padding: 4px;">0.92</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch-9.0-BBQ</td><td style="border: 1px solid #ccc; padding: 4px;">10-2000-2000</td><td style="border: 1px solid #ccc; padding: 4px;">44.27</td><td style="border: 1px solid #ccc; padding: 4px;">161.40</td><td style="border: 1px solid #ccc; padding: 4px;">0.95</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch-9.0-BBQ</td><td style="border: 1px solid #ccc; padding: 4px;">10-40-40</td><td style="border: 1px solid #ccc; padding: 4px;">10.97</td><td style="border: 1px solid #ccc; padding: 4px;">539.94</td><td style="border: 1px solid #ccc; padding: 4px;">0.84</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch-9.0-BBQ</td><td style="border: 1px solid #ccc; padding: 4px;">10-50-50</td><td style="border: 1px solid #ccc; padding: 4px;">11.00</td><td style="border: 1px solid #ccc; padding: 4px;">535.73</td><td style="border: 1px solid #ccc; padding: 4px;">0.85</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch-9.0-BBQ</td><td style="border: 1px solid #ccc; padding: 4px;">10-500-500</td><td style="border: 1px solid #ccc; padding: 4px;">19.52</td><td style="border: 1px solid #ccc; padding: 4px;">341.45</td><td style="border: 1px solid #ccc; padding: 4px;">0.93</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch-9.0-BBQ</td><td style="border: 1px solid #ccc; padding: 4px;">10-750-750</td><td style="border: 1px solid #ccc; padding: 4px;">22.94</td><td style="border: 1px solid #ccc; padding: 4px;">295.19</td><td style="border: 1px solid #ccc; padding: 4px;">0.94</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch-2.19-faiss</td><td style="border: 1px solid #ccc; padding: 4px;">10-100-100</td><td style="border: 1px solid #ccc; padding: 4px;">35.59</td><td style="border: 1px solid #ccc; padding: 4px;">200.61</td><td style="border: 1px solid #ccc; padding: 4px;">0.94</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch-2.19-faiss</td><td style="border: 1px solid #ccc; padding: 4px;">10-1000-1000</td><td style="border: 1px solid #ccc; padding: 4px;">156.81</td><td style="border: 1px solid #ccc; padding: 4px;">58.30</td><td style="border: 1px solid #ccc; padding: 4px;">0.96</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch-2.19-faiss</td><td style="border: 1px solid #ccc; padding: 4px;">10-1500-1500</td><td style="border: 1px solid #ccc; padding: 4px;">181.79</td><td style="border: 1px solid #ccc; padding: 4px;">42.97</td><td style="border: 1px solid #ccc; padding: 4px;">0.96</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch-2.19-faiss</td><td style="border: 1px solid #ccc; padding: 4px;">10-200-200</td><td style="border: 1px solid #ccc; padding: 4px;">47.91</td><td style="border: 1px solid #ccc; padding: 4px;">155.16</td><td style="border: 1px solid #ccc; padding: 4px;">0.95</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch-2.19-faiss</td><td style="border: 1px solid #ccc; padding: 4px;">10-2000-2000</td><td style="border: 1px solid #ccc; padding: 4px;">232.14</td><td style="border: 1px solid #ccc; padding: 4px;">31.84</td><td style="border: 1px solid #ccc; padding: 4px;">0.96</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch-2.19-faiss</td><td style="border: 1px solid #ccc; padding: 4px;">10-40-40</td><td style="border: 1px solid #ccc; padding: 4px;">27.55</td><td style="border: 1px solid #ccc; padding: 4px;">249.25</td><td style="border: 1px solid #ccc; padding: 4px;">0.92</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch-2.19-faiss</td><td style="border: 1px solid #ccc; padding: 4px;">10-50-50</td><td style="border: 1px solid #ccc; padding: 4px;">28.78</td><td style="border: 1px solid #ccc; padding: 4px;">245.14</td><td style="border: 1px solid #ccc; padding: 4px;">0.92</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch-2.19-faiss</td><td style="border: 1px solid #ccc; padding: 4px;">10-500-500</td><td style="border: 1px solid #ccc; padding: 4px;">79.44</td><td style="border: 1px solid #ccc; padding: 4px;">97.06</td><td style="border: 1px solid #ccc; padding: 4px;">0.96</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch-2.19-faiss</td><td style="border: 1px solid #ccc; padding: 4px;">10-750-750</td><td style="border: 1px solid #ccc; padding: 4px;">104.19</td><td style="border: 1px solid #ccc; padding: 4px;">75.49</td><td style="border: 1px solid #ccc; padding: 4px;">0.96</td>
                    </tr>
              </tbody>
            </table>
          <h3>리콜 @ 50 - 단순화</h3><p>이 수준의 리콜에서 Elasticsearch BBQ는 OpenSearch FAISS보다 <strong>최대 5배</strong> (평균 4.2배) 빠르며 평균 <strong>처리량이 3.9배(</strong> <strong> </strong>) 더 많습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/3189ffb330948b35854eeea9ae317d4846c14972-1600x681.png" style="width: 100%;" /><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/607383d674dcf0b8f94bfb1a450063f52fcbeb15-2060x876.png" style="width: 100%;" /><h4>상세 결과 - 리콜 @ 50%</h4><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/ebfffe0b776aad31dd03d315cfbf5aa098b41226-1600x789.png" style="width: 100%;" /><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/ae20cf870a65c400a2112bbad62eb56e244f549a-1600x799.png" style="width: 100%;" />
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;">작업</td><td style="border: 1px solid #ccc; padding: 4px;">지연 시간 평균</td><td style="border: 1px solid #ccc; padding: 4px;">처리량 평균</td><td style="border: 1px solid #ccc; padding: 4px;">평균 회수율</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch-9.0-BBQ</td><td style="border: 1px solid #ccc; padding: 4px;">50-1000-1000</td><td style="border: 1px solid #ccc; padding: 4px;">25.71</td><td style="border: 1px solid #ccc; padding: 4px;">246.44</td><td style="border: 1px solid #ccc; padding: 4px;">0.95</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch-9.0-BBQ</td><td style="border: 1px solid #ccc; padding: 4px;">50-1200-1200</td><td style="border: 1px solid #ccc; padding: 4px;">28.81</td><td style="border: 1px solid #ccc; padding: 4px;">227.85</td><td style="border: 1px solid #ccc; padding: 4px;">0.95</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch-9.0-BBQ</td><td style="border: 1px solid #ccc; padding: 4px;">50-150-150</td><td style="border: 1px solid #ccc; padding: 4px;">13.43</td><td style="border: 1px solid #ccc; padding: 4px;">362.90</td><td style="border: 1px solid #ccc; padding: 4px;">0.90</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch-9.0-BBQ</td><td style="border: 1px solid #ccc; padding: 4px;">50-1500-1500</td><td style="border: 1px solid #ccc; padding: 4px;">33.38</td><td style="border: 1px solid #ccc; padding: 4px;">202.37</td><td style="border: 1px solid #ccc; padding: 4px;">0.95</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch-9.0-BBQ</td><td style="border: 1px solid #ccc; padding: 4px;">50-200-200</td><td style="border: 1px solid #ccc; padding: 4px;">12.99</td><td style="border: 1px solid #ccc; padding: 4px;">406.30</td><td style="border: 1px solid #ccc; padding: 4px;">0.91</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch-9.0-BBQ</td><td style="border: 1px solid #ccc; padding: 4px;">50-2000-2000</td><td style="border: 1px solid #ccc; padding: 4px;">42.63</td><td style="border: 1px solid #ccc; padding: 4px;">163.68</td><td style="border: 1px solid #ccc; padding: 4px;">0.95</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch-9.0-BBQ</td><td style="border: 1px solid #ccc; padding: 4px;">50-250-250</td><td style="border: 1px solid #ccc; padding: 4px;">14.41</td><td style="border: 1px solid #ccc; padding: 4px;">373.21</td><td style="border: 1px solid #ccc; padding: 4px;">0.92</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch-9.0-BBQ</td><td style="border: 1px solid #ccc; padding: 4px;">50-500-500</td><td style="border: 1px solid #ccc; padding: 4px;">17.15</td><td style="border: 1px solid #ccc; padding: 4px;">341.04</td><td style="border: 1px solid #ccc; padding: 4px;">0.93</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch-9.0-BBQ</td><td style="border: 1px solid #ccc; padding: 4px;">50-750-750</td><td style="border: 1px solid #ccc; padding: 4px;">31.25</td><td style="border: 1px solid #ccc; padding: 4px;">248.60</td><td style="border: 1px solid #ccc; padding: 4px;">0.94</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch-2.19-faiss</td><td style="border: 1px solid #ccc; padding: 4px;">50-1000-1000</td><td style="border: 1px solid #ccc; padding: 4px;">125.35</td><td style="border: 1px solid #ccc; padding: 4px;">62.53</td><td style="border: 1px solid #ccc; padding: 4px;">0.96</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch-2.19-faiss</td><td style="border: 1px solid #ccc; padding: 4px;">50-1200-1200</td><td style="border: 1px solid #ccc; padding: 4px;">143.87</td><td style="border: 1px solid #ccc; padding: 4px;">54.75</td><td style="border: 1px solid #ccc; padding: 4px;">0.96</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch-2.19-faiss</td><td style="border: 1px solid #ccc; padding: 4px;">50-150-150</td><td style="border: 1px solid #ccc; padding: 4px;">43.64</td><td style="border: 1px solid #ccc; padding: 4px;">130.01</td><td style="border: 1px solid #ccc; padding: 4px;">0.89</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch-2.19-faiss</td><td style="border: 1px solid #ccc; padding: 4px;">50-1500-1500</td><td style="border: 1px solid #ccc; padding: 4px;">169.45</td><td style="border: 1px solid #ccc; padding: 4px;">46.35</td><td style="border: 1px solid #ccc; padding: 4px;">0.96</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch-2.19-faiss</td><td style="border: 1px solid #ccc; padding: 4px;">50-200-200</td><td style="border: 1px solid #ccc; padding: 4px;">48.05</td><td style="border: 1px solid #ccc; padding: 4px;">156.07</td><td style="border: 1px solid #ccc; padding: 4px;">0.91</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch-2.19-faiss</td><td style="border: 1px solid #ccc; padding: 4px;">50-2000-2000</td><td style="border: 1px solid #ccc; padding: 4px;">216.73</td><td style="border: 1px solid #ccc; padding: 4px;">36.38</td><td style="border: 1px solid #ccc; padding: 4px;">0.96</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch-2.19-faiss</td><td style="border: 1px solid #ccc; padding: 4px;">50-250-250</td><td style="border: 1px solid #ccc; padding: 4px;">53.52</td><td style="border: 1px solid #ccc; padding: 4px;">142.44</td><td style="border: 1px solid #ccc; padding: 4px;">0.93</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch-2.19-faiss</td><td style="border: 1px solid #ccc; padding: 4px;">50-500-500</td><td style="border: 1px solid #ccc; padding: 4px;">78.98</td><td style="border: 1px solid #ccc; padding: 4px;">97.82</td><td style="border: 1px solid #ccc; padding: 4px;">0.95</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch-2.19-faiss</td><td style="border: 1px solid #ccc; padding: 4px;">50-750-750</td><td style="border: 1px solid #ccc; padding: 4px;">103.20</td><td style="border: 1px solid #ccc; padding: 4px;">75.86</td><td style="border: 1px solid #ccc; padding: 4px;">0.96</td>
                    </tr>
              </tbody>
            </table>
          <h3>리콜 @ 100</h3><p>이 수준의 리콜에서 Elasticsearch BBQ는 OpenSearch FAISS보다 <strong>최대 5배 </strong>(평균 4.6배) 빠르며 평균 <strong>3.9배 더 많은 처리량을 </strong>제공합니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/d3f29db64cbde9956de1fa3ae64a75f15141a2bb-1600x681.png" style="width: 100%;" /><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/10aff7f8c61fdac895b9ba9c5342baf239ba3ffc-2072x864.png" style="width: 100%;" /><h4>상세 결과 - 리콜 @ 100</h4><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/8790fdf9512b850447f6875fb69969f6f1d4da5f-1600x799.png" style="width: 100%;" /><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/d47032fac6c288cd4eedd9f25001e417b2fa9d65-1600x787.png" style="width: 100%;" />
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;">작업</td><td style="border: 1px solid #ccc; padding: 4px;">latency.mean</td><td style="border: 1px solid #ccc; padding: 4px;">throughput.mean</td><td style="border: 1px solid #ccc; padding: 4px;">avg_recall</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch-9.0-BBQ</td><td style="border: 1px solid #ccc; padding: 4px;">100-1000-1000</td><td style="border: 1px solid #ccc; padding: 4px;">27.82</td><td style="border: 1px solid #ccc; padding: 4px;">243.22</td><td style="border: 1px solid #ccc; padding: 4px;">0.95</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch-9.0-BBQ</td><td style="border: 1px solid #ccc; padding: 4px;">100-1200-1200</td><td style="border: 1px solid #ccc; padding: 4px;">31.14</td><td style="border: 1px solid #ccc; padding: 4px;">224.04</td><td style="border: 1px solid #ccc; padding: 4px;">0.95</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch-9.0-BBQ</td><td style="border: 1px solid #ccc; padding: 4px;">100-1500-1500</td><td style="border: 1px solid #ccc; padding: 4px;">35.98</td><td style="border: 1px solid #ccc; padding: 4px;">193.99</td><td style="border: 1px solid #ccc; padding: 4px;">0.95</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch-9.0-BBQ</td><td style="border: 1px solid #ccc; padding: 4px;">100-200-200</td><td style="border: 1px solid #ccc; padding: 4px;">14.18</td><td style="border: 1px solid #ccc; padding: 4px;">403.86</td><td style="border: 1px solid #ccc; padding: 4px;">0.88</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch-9.0-BBQ</td><td style="border: 1px solid #ccc; padding: 4px;">100-2000-2000</td><td style="border: 1px solid #ccc; padding: 4px;">45.36</td><td style="border: 1px solid #ccc; padding: 4px;">159.88</td><td style="border: 1px solid #ccc; padding: 4px;">0.95</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch-9.0-BBQ</td><td style="border: 1px solid #ccc; padding: 4px;">100-250-250</td><td style="border: 1px solid #ccc; padding: 4px;">14.77</td><td style="border: 1px solid #ccc; padding: 4px;">433.06</td><td style="border: 1px solid #ccc; padding: 4px;">0.90</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch-9.0-BBQ</td><td style="border: 1px solid #ccc; padding: 4px;">100-300-300</td><td style="border: 1px solid #ccc; padding: 4px;">14.61</td><td style="border: 1px solid #ccc; padding: 4px;">375.54</td><td style="border: 1px solid #ccc; padding: 4px;">0.91</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch-9.0-BBQ</td><td style="border: 1px solid #ccc; padding: 4px;">100-500-500</td><td style="border: 1px solid #ccc; padding: 4px;">18.88</td><td style="border: 1px solid #ccc; padding: 4px;">340.37</td><td style="border: 1px solid #ccc; padding: 4px;">0.93</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch-9.0-BBQ</td><td style="border: 1px solid #ccc; padding: 4px;">100-750-750</td><td style="border: 1px solid #ccc; padding: 4px;">23.59</td><td style="border: 1px solid #ccc; padding: 4px;">285.79</td><td style="border: 1px solid #ccc; padding: 4px;">0.94</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch-2.19-faiss</td><td style="border: 1px solid #ccc; padding: 4px;">100-1000-1000</td><td style="border: 1px solid #ccc; padding: 4px;">142.90</td><td style="border: 1px solid #ccc; padding: 4px;">58.48</td><td style="border: 1px solid #ccc; padding: 4px;">0.95</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch-2.19-faiss</td><td style="border: 1px solid #ccc; padding: 4px;">100-1200-1200</td><td style="border: 1px solid #ccc; padding: 4px;">153.03</td><td style="border: 1px solid #ccc; padding: 4px;">51.04</td><td style="border: 1px solid #ccc; padding: 4px;">0.95</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch-2.19-faiss</td><td style="border: 1px solid #ccc; padding: 4px;">100-1500-1500</td><td style="border: 1px solid #ccc; padding: 4px;">181.79</td><td style="border: 1px solid #ccc; padding: 4px;">43.20</td><td style="border: 1px solid #ccc; padding: 4px;">0.96</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch-2.19-faiss</td><td style="border: 1px solid #ccc; padding: 4px;">100-200-200</td><td style="border: 1px solid #ccc; padding: 4px;">50.94</td><td style="border: 1px solid #ccc; padding: 4px;">131.62</td><td style="border: 1px solid #ccc; padding: 4px;">0.83</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch-2.19-faiss</td><td style="border: 1px solid #ccc; padding: 4px;">100-2000-2000</td><td style="border: 1px solid #ccc; padding: 4px;">232.53</td><td style="border: 1px solid #ccc; padding: 4px;">33.67</td><td style="border: 1px solid #ccc; padding: 4px;">0.96</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch-2.19-faiss</td><td style="border: 1px solid #ccc; padding: 4px;">100-250-250</td><td style="border: 1px solid #ccc; padding: 4px;">57.08</td><td style="border: 1px solid #ccc; padding: 4px;">131.23</td><td style="border: 1px solid #ccc; padding: 4px;">0.87</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch-2.19-faiss</td><td style="border: 1px solid #ccc; padding: 4px;">100-300-300</td><td style="border: 1px solid #ccc; padding: 4px;">62.76</td><td style="border: 1px solid #ccc; padding: 4px;">120.10</td><td style="border: 1px solid #ccc; padding: 4px;">0.89</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch-2.19-faiss</td><td style="border: 1px solid #ccc; padding: 4px;">100-500-500</td><td style="border: 1px solid #ccc; padding: 4px;">84.36</td><td style="border: 1px solid #ccc; padding: 4px;">91.54</td><td style="border: 1px solid #ccc; padding: 4px;">0.93</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch-2.19-faiss</td><td style="border: 1px solid #ccc; padding: 4px;">100-750-750</td><td style="border: 1px solid #ccc; padding: 4px;">111.33</td><td style="border: 1px solid #ccc; padding: 4px;">69.95</td><td style="border: 1px solid #ccc; padding: 4px;">0.94</td>
                    </tr>
              </tbody>
            </table>
          <h2>BBQ 개선 사항</h2><p>BBQ는 첫 출시 이후 많은 발전을 거듭해 왔습니다. 비교를 위해 8.16 버전에서 현재 버전과 함께 실행한 벤치마크를 포함시켰는데, 그 이후 리콜과 지연 시간이 어떻게 개선되었는지 확인할 수 있습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/45a0acfe5985bff28ccada76ec4eca190fe65f72-1600x799.png" style="width: 100%;" /><p>Elasticsearch 8.18과 9.0에서는 벡터를 정량화하기 위한 핵심 알고리즘을 다시 작성했습니다. 따라서 8.16의 BBQ도 좋았지만 최신 버전은 훨씬 더 좋습니다. 자세한 내용은 <a href="https://www.elastic.co/kr/search-labs/blog/optimized-scalar-quantization-elasticsearch">여기와</a> <a href="https://www.elastic.co/kr/search-labs/blog/scalar-quantization-optimization">여기에서</a> 확인할 수 있습니다. 즉, 모든 벡터는 최적화된 스칼라 사분위수를 통해 개별적으로 정량화됩니다. 그 결과, 사용자는 성능 저하 없이 벡터 검색의 정확도가 높아져 Elasticsearch의 벡터 검색이 더욱 강력해지는 이점을 누릴 수 있습니다.</p><h2>결론</h2><p>이 Elasticsearch BBQ와 OpenSearch FAISS 간의 성능 비교에서 Elasticsearch는 벡터 검색에서 OpenSearch보다 훨씬 뛰어난 성능을 발휘하여 다양한 수준의 리콜에서 평균적으로 최대 5배 빠른 쿼리 속도와 3.9배 높은 처리량을 달성했습니다.</p><p>주요 결과는 다음과 같습니다:</p><ul><li><strong>Recall@10</strong>: Elasticsearch BBQ는 OpenSearch FAISS에 비해 최대 5배(평균 3.9배) 빠르며 평균 3.2배 더 많은 처리량을 제공합니다.</li><li><strong>Recall@50</strong>: Elasticsearch BBQ는 OpenSearch FAISS에 비해 최대 5배(평균 4.2배) 빠르며 평균 3.9배 더 많은 처리량을 제공합니다.</li><li><strong>Recall@100</strong>: Elasticsearch BBQ는 OpenSearch FAISS에 비해 최대 5배(평균 4.6배) 빠르며 평균 3.9배 더 많은 처리량을 제공합니다.</li></ul><p>이러한 결과는 특히 고차원 벡터 검색 시나리오에서 Elasticsearch BBQ의 효율성과 성능 이점을 강조합니다. Elasticsearch 8.16에 도입된 더 나은 이진 양자화(BBQ) 기술은 높은 순위 품질을 유지하면서 상당한 메모리 절감(~95%)을 제공하므로 대규모 벡터 검색 애플리케이션에 탁월한 선택이 될 수 있습니다.</p><p>Elastic에서는 RAG(검색 증강 생성)를 비롯한 검색 및 검색 사용 사례를 위한 최고의 벡터 데이터베이스를 제공하기 위해 Apache Lucene과 Elasticsearch를 개선하기 위해 끊임없이 혁신하고 있습니다. <a href="https://www.elastic.co/kr/search-labs/blog/optimized-scalar-quantization-elasticsearch">최근의 발전으로</a> 성능이 크게 향상되어 이전보다 벡터 검색 속도가 빨라지고 공간 효율성이 높아졌으며, 이는 루씬 10에서 얻은 이점을 기반으로 합니다. 이 블로그는 이러한 혁신의 또 다른 예시입니다.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[클러스터 간에 서로 다른 버전의 Elasticsearch & 간에 데이터를 마이그레이션하는 방법]]></title>
            <description><![CDATA[Elasticsearch 버전과 클러스터 간에 데이터를 전송하는 방법을 살펴봅니다.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/elasticsearch-migrate-data-versions-clusters</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/elasticsearch-migrate-data-versions-clusters</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <pubDate>Mon, 14 Apr 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/01fde3b1d714f12bf8673140c9f2f940d443de31-1440x823.jpg?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>Elasticsearch 클러스터를 업그레이드하려는 경우, 별도의 새 클러스터를 생성하고 이전 클러스터에서 새 클러스터로 데이터를 전송하는 것이 더 쉬울 때가 있습니다. 따라서 사용자는 다운타임이나 데이터 손실의 위험 없이 모든 애플리케이션을 사용하여 새 클러스터에서 모든 데이터와 구성을 테스트할 수 있다는 이점을 누릴 수 있습니다.</p><p>이 접근 방식의 단점은 하드웨어를 일부 복제해야 하고 모든 데이터를 원활하게 전송하고 동기화하려고 할 때 문제가 발생할 수 있다는 점입니다.</p><p>한 데이터 센터에서 다른 데이터 센터로 애플리케이션을 마이그레이션해야 하는 경우에도 유사한 절차를 수행해야 할 수 있습니다.</p><p>이 문서에서는 Elasticsearch 클러스터 간에 데이터를 전송하는 세 가지 방법에 대해 자세히 설명합니다.</p><p><strong>Elasticsearch 클러스터 간에 데이터를 마이그레이션하는 방법은 무엇인가요?</strong></p><p>Elasticsearch 클러스터 간에 데이터를 전송하는 방법에는 3가지가 있습니다:</p><ol><li><a href="https://www.elastic.co/kr/search-labs/blog/elasticsearch-migrate-data-versions-clusters#1.-reindexing-data-from-a-remote-cluster">원격 클러스터에서 재색인</a></li><li><a href="https://www.elastic.co/kr/search-labs/blog/elasticsearch-migrate-data-versions-clusters#2.-transferring-data-using-snapshots">스냅샷을 사용하여 데이터 전송</a></li><li><a href="https://www.elastic.co/kr/search-labs/blog/elasticsearch-migrate-data-versions-clusters#3.-transferring-data-using-logstash">Logstash를 사용하여 데이터 전송</a></li></ol><p>일반적으로 스냅샷을 사용하는 것이 데이터를 전송하는 가장 빠르고 안정적인 방법입니다. 그러나 스냅샷은 동일하거나 상위 버전의 클러스터로만 복원할 수 있으며, 주 버전과 차이가 한 개 이상 나는 경우에는 복원할 수 없습니다. 즉, 6.x 스냅샷을 7.x 클러스터로 복원할 수는 있지만 8.x 클러스터로 복원할 수는 없습니다.</p><p>하나 이상의 주요 버전으로 늘려야 하는 경우, 색인을 다시 생성하거나 Logstash를 사용해야 합니다.</p><p>이제 Elasticsearch 클러스터 간에 데이터를 전송하는 세 가지 옵션 각각에 대해 자세히 살펴보겠습니다.</p><h2>1. 원격 클러스터에서 데이터 재색인하기</h2><p>재색인을 시작하기 전에 새 클러스터의 모든 인덱스에 대해 적절한 매핑을 설정해야 한다는 점을 기억하세요. 이렇게 하려면 적절한 매핑을 사용하여 직접 인덱스를 만들거나 인덱스 템플릿을 사용해야 합니다.</p><h3>원격에서 재색인 - 구성 필요</h3><p>원격에서 재색인하려면 데이터를 수신하는 클러스터의 elasticseearch.yml 파일에 아래 구성을 추가해야 하며, Linux 시스템에서는 일반적으로 여기에 위치합니다: /etc/elasticsearch/elasticsearch.yml. 추가할 구성은 다음과 같습니다:</p><pre><code>reindex.remote.whitelist: "192.168.1.11:9200"</code></pre><p>SSL을 사용하는 경우, 각 노드에 CA 인증서를 추가하고 elasticsearch.yml의 각 노드에 대한 명령에 다음을 포함해야 합니다:</p><pre><code>reindex.ssl.certificate_authorities: “/path/to/ca.pem”</code></pre><p>또는 모든 Elasticsearch 노드에 아래 줄을 추가하여 SSL 확인을 사용하지 않도록 설정할 수 있습니다. 그러나 이 방법은 이전 옵션만큼 안전하지 않으므로 권장하지 않습니다:</p><pre><code>reindex.remote.whitelist: "192.168.1.11:9200"
reindex.ssl.verification_mode: none
systemctl restart elasticsearch service </code></pre><p>모든 노드에서 이러한 수정 사항을 적용하고 롤링 재시작을 수행해야 합니다. 방법에 대한 자세한 내용은 <a href="https://www.elastic.co/kr/guide/en/elasticsearch/reference/8.17/restart-cluster.html#restart-cluster-rolling">가이드를</a> 참조하세요.</p><h3>재색인 명령</h3><p>elasticsearch.yml 파일에서 원격 호스트를 정의하고 필요한 경우 SSL 인증서를 추가한 후, 아래 명령으로 데이터 재색인을 시작할 수 있습니다:</p><pre><code>POST _reindex
{
  "source": {
    "remote": {
      "host": "http://192.168.1.11:9200",
      "username": "elastic",
      "password": "123456",
     "socket_timeout": "1m",
      "connect_timeout": "1m"

    },
    "index": "companydatabase"
  },
  "dest": {
    "index": "my-new-index-000001"
  }
}</code></pre><p>이 과정에서 시간 초과 오류가 발생할 수 있으므로 기본값에 의존하기보다는 시간 초과에 대한 넉넉한 값을 설정하는 것이 유용할 수 있습니다.</p><p>이제 원격에서 재색인할 때 발생할 수 있는 몇 가지 일반적인 오류를 살펴보겠습니다.</p><h3>원격에서 재색인할 때 흔히 발생하는 오류</h3><h4>1. 화이트리스트에 등재되지 않은 재인덱싱</h4><pre><code>{
  "error": {
    "root_cause": [
      {
        "type": "illegal_argument_exception",
        "reason": "[192.168.1.11:9200] not whitelisted in reindex.remote.whitelist"
      }
    ],
    "type": "illegal_argument_exception",
    "reason": "[192.168.1.11:9200] not whitelisted in reindex.remote.whitelist"
  },
  "status": 400
}</code></pre><p>이 오류가 발생하면 위에서 설명한 대로 Elasticsearch에서 원격 호스트 IP 주소 또는 노드 이름 DNS를 정의하지 않았거나 Elasticsearch 서비스를 다시 시작하는 것을 잊었다는 뜻입니다.</p><p>Elasticsearch 클러스터에 대해 이 문제를 해결하려면 모든 Elasticsearch 노드에 원격 호스트를 추가하고 Elasticsearch 서비스를 다시 시작해야 합니다.</p><h4>2. SSL 핸드셰이크 예외</h4><pre><code>{
  "error": {
    "root_cause": [
      {
        "type": "s_s_l_handshake_exception",
        "reason": "PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target"
      }
    ],
    "type": "s_s_l_handshake_exception",
    "reason": "PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target",
    "caused_by": {
      "type": "validator_exception",
      "reason": "PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target",
      "caused_by": {
        "type": "sun_cert_path_builder_exception",
        "reason": "unable to find valid certification path to requested target"
      }
    }
  },
  "status": 500
}</code></pre><p>이 오류는 위에서 설명한 대로 elasticsearch.yml에 reindex.ssl.certificate_authorities를 추가하는 것을 잊었음을 의미합니다. 추가하려면:</p><pre><code>#elasticsearch.yml
reindex.ssl.certificate_authorities: "/path/to/ca.pem"</code></pre><h2>2. 스냅샷을 사용하여 데이터 전송</h2><p>위에서 언급했듯이 스냅샷은 동일하거나 상위 버전의 클러스터로만 복원할 수 있으며, 주요 버전이 하나 이상 차이 나는 경우에는 절대로 복원할 수 없습니다.</p><p>하나 이상의 주요 버전으로 늘려야 하는 경우, 색인을 다시 생성하거나 Logstash를 사용해야 합니다.</p><p>스냅샷을 통해 데이터를 전송하려면 다음 단계가 필요합니다:</p><p>1단계. 첫 번째 Elasticsearch 클러스터에 리포지토리 플러그인 추가 - 스냅샷을 통해 클러스터 간에 데이터를 전송하려면 새 클러스터와 이전 클러스터 모두에서 리포지토리에 액세스할 수 있는지 확인해야 합니다. 일반적으로 AWS, Google, Azure와 같은 클라우드 스토리지 리포지토리가 가장 이상적입니다. 스냅샷을 찍으려면 <a href="https://www.elastic.co/kr/guide/en/elasticsearch/reference/current/snapshot-restore.html">가이드를</a> 참조하여 설명된 단계를 따르세요.</p><p>2단계. Elasticsearch 서비스를 다시 시작합니다(롤링 재시작).</p><p>3단계. 첫 번째 Elasticsearch 클러스터를 위한 리포지토리를 생성합니다.</p><p>4단계- 두 번째 Elasticsearch 클러스터에 리포지토리 플러그인을 추가합니다.</p><p>5단계- 두 번째 Elasticsearch 클러스터에 리포지토리를 읽기 전용으로 추가하기 - 첫 번째 Elasticsearch 클러스터를 생성할 때와 동일한 단계를 반복하여 리포지토리를 추가해야 합니다.</p><p>중요 참고: 두 번째 Elasticsearch 클러스터를 동일한 AWS S3 리포지토리에 연결할 때는 리포지토리를 읽기 전용 리포지토리로 정의해야 합니다:</p><pre><code>PUT _snapshot/my_s3_repository
{
  "type": "s3",
  "settings": {
    "bucket": "my-analytic-data",
    "endpoint": "s3.eu-de.cloud-object-storage.appdomain.cloud",
    "readonly": "true"
  }
}</code></pre><p>이는 동일한 스냅샷 리포지토리 내에서 Elasticsearch 버전이 혼합되는 위험을 방지하고자 하기 때문에 중요합니다.</p><p>6단계- 두 번째 Elasticsearch 클러스터로 데이터 복원 - 위의 단계를 수행한 후 데이터를 복원하여 새 클러스터로 전송할 수 있습니다. <a href="https://www.elastic.co/kr/guide/en/elasticsearch/reference/current/snapshot-restore.html">이 문서에</a> 설명된 단계에 따라 새 클러스터로 데이터를 복원하세요. </p><h2>3. Logstash를 사용하여 데이터 전송</h2><p>로그스태시로 데이터 전송을 시작하기 전에, 새 클러스터의 모든 인덱스에 대해 적절한 매핑을 설정해야 한다는 것을 기억하세요. 이렇게 하려면 인덱스를 직접 만들거나 인덱스 템플릿을 사용해야 합니다.</p><p>두 개의 Elasticsearch 클러스터 간에 데이터를 전송하려면 임시 Logstash 서버를 설정하고 이를 사용하여 두 클러스터 간에 데이터를 전송할 수 있습니다. 소규모 클러스터의 경우 2GB 램 인스턴스로 충분합니다. 대규모 클러스터의 경우 8GB RAM이 장착된 4코어 CPU를 사용할 수 있습니다.</p><p>Logstash 설치에 대한 안내는 <a href="https://www.elastic.co/kr/guide/en/logstash/current/installing-logstash.html">여기를 참조하세요</a>.</p><h3>한 클러스터에서 다른 클러스터로 데이터를 전송하기 위한 Logstash 구성</h3><p>클러스터 A에서 클러스터 B로 단일 인덱스를 복사하는 기본 구성은 다음과 같습니다:</p><pre><code>iinput
{
elasticsearch
      {
        hosts => ["192.168.1.11:9200"]
        index => "index_name"
       docinfo => true      
      }
}

output 
{
  elasticsearch {
        hosts => "https://192.168.1.12:9200"
        index => "index_name"
        
  }
}</code></pre><p>보안된 Elasticsearch의 경우, 아래 구성을 사용할 수 있습니다:</p><pre><code>input
{
  elasticsearch
      {
        hosts => ["192.168.1.11:9200"]
        index => "index_name"
        docinfo => true 
        user => "elastic"
        password => "elastic_password"
        ssl => true
        ssl_certificate_verification => false
            
      }
}

output 
{
  elasticsearch {
        hosts => "https://192.168.1.12:9200"
        index => "index_name"
        user => "elastic"
        password => "elastic_password"
        ssl => true
        ssl_certificate_verification => false
  }
}</code></pre><h3>인덱스 메타데이터</h3><p>위의 명령은 하나의 명명된 인덱스에 기록합니다. 여러 인덱스를 전송하고 인덱스 이름을 유지하려면 Logstash 출력에 다음 줄을 추가해야 합니다:</p><pre><code>index => "%{[@metadata][_index]}"</code></pre><p>또한 문서의 원래 ID를 유지하려면 추가해야 합니다:</p><pre><code>document_id => "%{[@metadata][_id]}"</code></pre><p>문서 ID를 설정하면 데이터 전송 속도가 상당히 느려지므로 필요한 경우에만 원본 ID를 보존하세요.</p><h2>업데이트 동기화</h2><p>위에서 설명한 모든 방법은 비교적 오랜 시간이 걸리며 프로세스가 완료될 때까지 기다리는 동안 원본 클러스터의 데이터가 업데이트될 수 있습니다.</p><p>데이터 전송 프로세스 중에 발생한 업데이트를 동기화할 수 있는 다양한 전략이 있으며, 이 프로세스를 시작하기 전에 이러한 문제에 대해 생각해 보아야 합니다. 특히 다음 사항을 고려해야 합니다:</p><ul><li>데이터 전송 프로세스 시작 이후 업데이트/추가된 데이터를 식별할 수 있는 방법(예: 데이터의 &#x27;last_update_time&#x27; 필드)은 무엇인가요?</li><li>마지막 데이터를 전송하는 데 어떤 방법을 사용할 수 있나요?</li><li>기록이 중복될 위험이 있나요? 사용 중인 메서드가 재색인 중에 문서 ID를 알려진 값으로 설정하지 않는 한 일반적으로 있습니다).</li></ul><p>업데이트 동기화를 활성화하는 다양한 방법은 아래에 설명되어 있습니다.</p><h3>1. 대기열 시스템 사용</h3><p>일부 수집/업데이트 시스템에서는 지난 x일 동안 수신한 데이터 수정 사항을 &#x27;재생&#x27;할 수 있는 대기열을 사용합니다. 이는 수행된 모든 변경 사항을 동기화할 수 있는 수단을 제공할 수 있습니다. </p><h3>2. 원격에서 재색인</h3><p>&quot;last_update_time&quot; &gt; x 일 전인 모든 항목에 대해 재색인 프로세스를 반복합니다. 재색인 요청에 &#x27;쿼리&#x27; 매개변수를 추가하여 이 작업을 수행할 수 있습니다.</p><h3>3. Logstash</h3><p>Logstash 입력에 쿼리를 추가하여 &quot;last_update_time&quot; &gt; x일 전인 모든 항목을 필터링할 수 있습니다. 그러나 이 프로세스는 문서_id를 설정하지 않은 경우 시계열이 아닌 데이터에 중복을 발생시킵니다.</p><h3>4. 스냅샷</h3><p>인덱스의 일부만 복원할 수는 없으므로 위에서 설명한 다른 데이터 전송 방법 중 하나(또는 스크립트)를 사용하여 데이터 전송 프로세스가 수행된 이후 발생한 변경 사항을 업데이트해야 합니다.</p><p>그러나 스냅샷 복원은 재색인/로그스토시보다 훨씬 빠른 프로세스이므로 스냅샷을 전송하는 동안 잠시 동안 업데이트를 일시 중단하여 문제를 완전히 피할 수 있습니다.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[구글 클라우드의 버텍스 AI 플랫폼에서 기본 접지를 위한 Elasticsearch 벡터 데이터베이스]]></title>
            <description><![CDATA[Google Cloud의 Vertex AI를 위한 최초의 써드파티 기본 접지 엔진인 Elasticsearch가 어떻게 기업 데이터에 Gemini 모델을 접지하여 맞춤형 GenAI 환경을 구축할 수 있는지 알아보세요.]]></description>
            <link>https://www.elastic.co/search-labs/kr/blog/elasticsearch-google-cloud-vertex-ai-native-grounding</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/kr/blog/elasticsearch-google-cloud-vertex-ai-native-grounding</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Valerio Arvizzigno]]></dc:creator>
            <pubDate>Wed, 09 Apr 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/d1f1c8860f1f0b989ad698a882f869de7284ab78-1200x628.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>Elastic은 이제 Elasticsearch 벡터 데이터베이스가 기본적으로 지원되는 정보 검색 엔진으로 Google Cloud의 Vertex AI 플랫폼에 통합되어 사용자가 Google Gemini 모델의 멀티모달 강점을 Elasticsearch의 고급 AI 기반 시맨틱 및 하이브리드 검색 기능과 함께 활용할 수 있게 되었음을 발표하게 되어 기쁘게 생각합니다.</p><p>이제 개발자는 통합된 여정 내에서 로우코드의 유연한 방식으로 개인 데이터에 기반한 채팅 경험을 제공하는 RAG 애플리케이션을 만들 수 있습니다. 고객과 내부 직원을 위한 AI 에이전트를 구축하든, 소프트웨어 내에서 LLM 생성을 활용하든, Vertex AI 플랫폼은 최소한의 구성으로 손쉽게 Elasticsearch 정확도를 제공합니다. 이러한 통합을 통해 프로덕션 사용 사례에서 Gemini 모델을 더 쉽고 빠르게 채택할 수 있으며, GenAI를 PoC에서 실제 시나리오로 끌어올릴 수 있습니다.</p><p>이 블로그에서는 원활한 데이터 기반을 마련하고 완전히 사용자 정의 가능한 GenAI 애플리케이션을 구축하기 위해 Elasticsearch를 Google Cloud의 Vertex AI 플랫폼과 통합하는 방법을 안내해드립니다. 방법을 알아보세요.</p><h2>Elasticsearch를 통해 데이터에 기반한 Google Cloud의 Vertex AI 및 Gemini 모델</h2><p>이제 GenAI 애플리케이션을 만들기 위해 버텍스 AI 서비스 및 도구를 활용하는 사용자는 새로운 &#x27;접지&#x27; 옵션에 액세스하여 자신의 개인 데이터를 자동으로 대화 상호작용으로 가져올 수 있습니다. Elasticsearch는 이제 이 기능의 일부이며 두 가지 모두에서 사용할 수 있습니다:</p><ul><li>생성 시점에 Google의 Gemini 모델을 직접 강화하는 Vertex AI <a href="https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/inference">LLM API</a>(선호);</li><li>버텍스 AI 에이전트 빌더 에코시스템에서 에이전트 경험을 구축하는 데 사용되는 <a href="https://cloud.google.com/generative-ai-app-builder/docs/grounded-gen">Grounded Generation API입니다</a>.</li></ul><p>이 통합을 통해 가장 많이 다운로드되고 배포된 <a href="https://www.elastic.co/kr/elasticsearch/vector-database">벡터 데이터베이스인</a> Elasticsearch는 내부 최종 고객 대면 채팅에서 필요한 곳이면 어디든 관련 엔터프라이즈 데이터를 가져올 수 있으며, 이는 GenAI를 비즈니스 프로세스에 실제로 도입하는 데 매우 중요한 역할을 합니다.</p><p>앞서 언급한 API를 통해 개발자는 이 새로운 파트너 기능을 코드에 적용할 수 있습니다. 그러나 신속한 엔지니어링 및 테스트는 애플리케이션 개발의 중요한 단계이며 초기 발견의 놀이터 역할을 합니다. 이를 지원하기 위해 Elasticsearch는 사용자가 Vertex AI Studio 콘솔 도구 내에서 쉽게 평가할 수 있도록 설계되었습니다.</p><p>아래 그림과 같이 UI의 &quot;사용자 정의 접지&quot; 탭에서 원하는 매개변수(검색할 인덱스, 검색할 문서 수, 원하는 검색 템플릿)로 Elastic 엔드포인트를 구성하는 몇 가지 간단한 단계만 거치면 됩니다(작동하려면 UI와 아래 코드 예제에서 &quot;ApiKey&quot; 라는 단어와 함께 API 키를 입력해야 합니다). 이제 개인 지식으로 생성할 준비가 되었습니다!</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/69b3d979d18fa90742d9397f57975c586edf5d9f-1003x710.gif" style="width: 100%;" /><h2>손쉽게 프로덕션에 사용할 수 있는 GenAI 애플리케이션</h2><p>Elastic과 Google Cloud는 개발자 우선의 포괄적이고 즐거운 경험을 제공하기 위해 노력하고 있습니다. LLM과 접지 생성 API 모두에서 기본적으로 Elastic에 연결하면 하나의 통합 호출로 접지하면서 불필요한 추가 API와 데이터 오케스트레이션을 피하고, Vertex AI에서 GAI 애플리케이션을 구축하는 동안 복잡성과 오버헤드를 줄일 수 있습니다.</p><p>두 시나리오에서 어떻게 작동하는지 살펴보겠습니다.</p><p>첫 번째 예제는 LLM API로 실행됩니다:</p><pre><code>curl -X POST \
  -H "Authorization: Bearer $(gcloud auth print-access-token)" \
  -H "Content-Type: application/json" \https://us-central1-aiplatform.googleapis.com/v1beta1/projects/<PROJECT_ID>/locations/us-central1/publishers/google/models/gemini-2.0-flash-001:generateContent \
  -d '
{
  "contents": [
    {
      "role": "user",
      "parts": [
        {
          "text": "What's my company car policy?"
        }
      ]
    }
  ],
  "tools": [{
    "retrieval": {
      "externalApi": {
        "api_spec": "ELASTIC_SEARCH",
    "endpoint": "https://<my-elastic-cluster>.gcp.elastic-cloud.com:9243",
    "apiAuth": {
      "apiKeyConfig": {
            "apiKeyString": "ApiKey <API_KEY>"
      }
    },
    "elasticSearchParams": {
      "index": "<my-index>",
      "searchTemplate": "<my-search-template>"
    }
      }
    }
  }]
}</code></pre><p>위의 예제에서 API의 <code>retrieval</code> 필드가 Gemini 2.0 Flash에 콘텐츠 생성을 요청하는 경우, 요청에 대한 검색 엔진을 컨텍스트에 맞게 설정할 수 있습니다. <code>api_spec</code> 을 &quot;ELASTIC_SEARCH&quot;로 설정하면 API 키와 클러스터 엔드포인트(요청을 Elastic 클러스터로 라우팅하는 데 필요), 데이터를 검색할 인덱스, 검색 로직에 사용할 검색 템플릿과 같은 추가 구성 매개변수를 사용할 수 있습니다.</p><p>마찬가지로 접지 생성 API를 사용하여 <code>groundingSpec</code> 파라미터를 설정하면 동일한 결과를 얻을 수 있습니다:</p><pre><code>curl -X POST -H "Authorization: Bearer $(gcloud auth print-access-token)" -H "Content-Type: application/json" https://us-discoveryengine.googleapis.com/v1alpha/projects/<PROJECT_ID>/locations/global:generateGroundedContent -d '
{
  "contents": [{
    "role": "user",
    "parts": [{
      "text": "What do I need to patch a hole in my drywall?"
    }]
  }],
  "groundingSpec": {
    "groundingSources": [{
      "elasticSource": {
        "endpoint": "https://<my-elastic-cluster>.gcp.elastic-cloud.com:9243",
        "index": "<my-index>",
        "searchTemplate": "<my-search-template",
        "apiKey": "projects/<PROJECT_ID>/secrets/api-key/versions/latest"
      }
    }]
  }
}
'</code></pre><p>두 가지 접근 방식 모두에서 응답은 쿼리를 지원하기 위해 Elasticsearch에서 찾은 가장 관련성이 높은 비공개 문서와 연결된 관련 데이터 소스로 답변을 제공합니다.</p><p>하지만 단순함을 특정 요구사항과 사용 사례를 충족하기 위한 개인화 부족과 혼동해서는 안 됩니다. 이를 염두에 두고 검색 구성을 사용자의 시나리오에 맞게 완벽하게 조정할 수 있도록 설계했습니다.</p><h2>손끝에서 완벽하게 사용자 지정 가능한 검색: 검색 템플릿</h2><p>검색 시나리오를 최대한 맞춤 설정할 수 있도록 Google은 Google Cloud와 협력하여 잘 알려진 <a href="https://www.elastic.co/kr/guide/en/elasticsearch/reference/current/search-template.html">검색 템플릿을</a> 기반으로 환경을 구축했습니다. Elasticsearch 검색 템플릿은 동적이고 재사용 가능하며 유지 관리가 가능한 검색 쿼리를 생성하기 위한 훌륭한 도구입니다. 쿼리 구조를 미리 정의하고 재사용할 수 있습니다. 개발 시간을 절약하고 오류 발생 가능성을 줄이기 때문에 다른 매개변수로 유사한 쿼리를 실행할 때 특히 유용합니다. 템플릿에는 변수에 대한 자리 표시자를 포함할 수 있으므로 다양한 검색 요구 사항에 맞게 쿼리를 동적으로 조정할 수 있습니다.</p><p>접지를 위해 Vertex AI API와 Elasticsearch를 사용하는 동안, 위의 코드 스니펫과 같이 원하는 검색 템플릿을 참조해야 하며, 여기서 검색 로직이 구현되어 Elasticsearch로 푸시됩니다. Elastic 파워 유저는 검색 접근 방식을 비동기적으로 관리, 구성 및 업데이트하고 특정 인덱스, 모델 및 데이터에 맞게 조정할 수 있으며, Vertex AI 사용자, 웹 앱 개발자 또는 AI 엔지니어는 접지 API에서 템플릿의 이름만 지정하면 됩니다.</p><p>이 설계를 통해 완벽한 사용자 정의가 가능하여 광범위한 Elasticsearch 검색 기능을 Google Cloud AI 환경에서 마음대로 사용할 수 있으며, Elastic에 익숙하지 않은 개발자도 모듈성, 투명성, 사용 편의성을 보장합니다.</p><p>BM25 검색, 시맨틱 검색 또는 이 둘의 하이브리드 접근 방식이 필요할 때마다(이미 <a href="https://www.elastic.co/kr/guide/en/elasticsearch/reference/current/retrievers-overview.html">검색기를</a> 사용해 보셨나요?)? 단일 검색 API 호출로 구성 가능한 검색 기술), 검색 템플릿에서 사용자 지정 로직을 정의하면 Vertex AI가 자동으로 이를 활용할 수 있습니다.</p><p>이는 벡터와 결과를 관리하기 위해 선택한 임베딩 및 재랭크 모델에도 적용됩니다. 사용 사례에 따라 Elastic의 ML 노드에서 모델을 호스팅하거나, 추론 API를 통해 타사 서비스 엔드포인트를 사용하거나, 온프레미스에서 로컬 모델을 실행할 수 있습니다. 검색 템플릿을 통해 이 작업을 수행할 수 있으며, 다음 섹션에서 그 방법을 살펴보겠습니다.</p><h2>참조 템플릿으로 시작한 다음 나만의 템플릿을 만들 수 있습니다.</h2><p>빠르게 시작할 수 있도록 초기 참조용으로 사용할 수 있는 호환 가능한 검색 템플릿 샘플 세트를 제공했으며, 이를 기반으로 사용자 지정 템플릿을 수정하고 구축할 수 있습니다:</p><ul><li>ELSER 모델을 사용한 시맨틱 검색(스파스 벡터 및 청킹)</li><li>e5 다국어 모델을 사용한 시맨틱 검색(고밀도 벡터 및 청킹)</li><li>버텍스 AI 텍스트 임베딩 모델을 사용한 하이브리드 검색</li></ul><p>이 <a href="https://github.com/elastic/elasticsearch-labs/tree/main/supporting-blog-content/Cloud-Vertex-AI/search-templates">GitHub 리포지토리에서</a> 찾을 수 있습니다.</p><p>한 가지 예를 들어 보겠습니다. 제품 카탈로그에 Google Cloud의 Vertex AI API를 사용하여 임베딩을 생성하는 것입니다. 먼저, 아래와 같이 Elasticsearch에서 검색 템플릿을 생성해야 합니다:</p><pre><code>PUT _scripts/google-template-knn
{
  "script": {
    "lang": "mustache",
    "source": {
      "_source": {
        "excludes": [ "title_embedding", "description_embedding", "images" ]
      },
        "size": "{{num_hits}}",
          "knn" : [
          { 
            "field": "description_embedding",
            "k": 5,
            "num_candidates": 10,
            "query_vector_builder": {
              "text_embedding": {
                "model_id": "googlevertexai_embeddings_004",
                "model_text": "{{query}}"
              }
            },
            "boost": 0.4
          },
          {
            "field": "title_embedding",
            "k": 5,
            "num_candidates": 10,
            "query_vector_builder": {
              "text_embedding": {
                "model_id": "googlevertexai_embeddings_004",
                "model_text": "{{query}}"
            }
          },
          "boost": 0.6
          }
          ]
    }  
  }
}</code></pre><p>이 예에서는 하나의 검색 내에서 두 개의 필드에 대해 KNN 검색을 실행합니다: <code>title_embedding</code> - 제품 이름이 포함된 벡터 필드와 제품 설명이 포함된 <code>description_embedding</code> 필드입니다.</p><p><code>excludes</code> 구문을 활용하여 처리 과정에서 노이즈를 유발하고 최종 답변의 품질에 영향을 줄 수 있는 불필요한 필드를 LLM에 반환하지 않도록 할 수 있습니다. 이 예제에서는 벡터와 이미지 URL이 포함된 필드를 제외했습니다.</p><p>벡터는 이전에 다음과 같이 정의된 Vertex AI 임베딩 API( <code>googlevertexai_embeddings_004</code>)에 대한 추론 엔드포인트를 통해 제출된 입력에 대해 쿼리 시점에 즉석에서 생성됩니다:</p><pre><code>PUT /_inference/text_embedding/googlevertexai_embeddings_004
{
    "service": "googlevertexai",
    "service_settings": {
        "service_account_json": "<your_service_account_key>",
        "model_id": "text-embedding-004",
        "location": "us-central1",
        "project_id": "<your_gcp_project>"
    }
}</code></pre><p>Elastic의 추론 API를 사용하는 방법에 대한 추가 정보는 <a href="https://www.elastic.co/kr/guide/en/elasticsearch/reference/current/inference-apis.html">여기에서</a> 확인할 수 있습니다.</p><p>이제 템플릿 검색을 테스트할 준비가 되었습니다:</p><pre><code>GET product-catalog-with-embeddings/_search/template
{
  "id": "google-template-knn",
  "params": {
    "query": "What do I need to patch a hole in my drywall?",
    "index_name": "product-catalog-with-embeddings",
    "num_hits": 3
  }
}</code></pre><p><code>params</code> 필드는 템플릿 스크립트에서 설정한 변수를 이중 중괄호 괄호로 대체합니다. 현재 Vertex AI LLM 및 Grounded Generation API는 다음과 같은 입력 변수를 Elastic에 전송할 수 있습니다:</p><ul><li>&quot;쿼리&quot; - 검색할 사용자 쿼리</li><li>&quot;index_name&quot; - 검색할 인덱스의 이름입니다.</li><li>&quot;num_hits&quot; - 최종 출력에서 검색할 문서 수</li></ul><p>다음은 샘플 출력입니다:</p><pre><code>{
        "_index": "product-catalog-with-embeddings",
        "_id": "9ZQCm5IBcrGI1ivqV-f_",
        "_score": 0.4925191,
        "_ignored": [
          "description.keyword",
          "images.keyword"
        ],
        "_source": {
          "description": "DAP Eclipse Rapid Wall Repair Patch is a new, revolutionary product solution for repairing drywall damage. No more waiting for spackling to dry or messy sanding. DAP Eclipse allows you to patch drywall damage and paint immediately, allowing you to finish your project faster. This all-in-1, mess free solution not only provides a permanent, long-lasting repair but also superior impact resistance for areas that may see reoccurring impact, such as behind a door.",
          "availability": "InStock",
          "model_id": "googlevertexai_embeddings_004",
          "title": "4 in. Eclipse Wall Repair Patch (2-Pack)",
          "url": "https://www.myDIYwebsite.com/p/DAP-4-in-Eclipse-Wall-Repair-Patch-2-Pack-7079809164/317967195",
          "price": 23.96,
          "product_id": 317967195,
          "currency": "USD",
          "brand": "DAP"
        }</code></pre><p>위의 쿼리는 바로 이전에 생성된 검색 템플릿을 참조할 때 Google Cloud의 Vertex AI가 Elasticsearch에서 백그라운드에서 실행하는 쿼리입니다. &quot;건식 벽체에 어떤 패치가 필요하나요?&quot;라고 질문하면 채팅 상담원이 일반적인 제안 대신 구체적인 제품을 추천해 드립니다!</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/8c8b1374da7e9b2c17df8758acb448eed5b74e2d-1473x913.png" style="width: 100%;" /><h2>Elastic과 Google Cloud를 통한 엔드투엔드 GenAI 여정</h2><p>Elastic은 Google Cloud와 협력하여 프로덕션에 바로 사용할 수 있는 엔드투엔드 GenAI 환경과 솔루션을 개발합니다. 방금 살펴본 바와 같이, Elastic은 벡터 검색 기능을 사용하여 원활하고 근거가 있는 Gemini 모델 프롬프트와 에이전트를 지원하는 Vertex AI 플랫폼의 UI와 SDK에 직접 통합된 최초의 ISV입니다. 또한, Elastic은 <a href="https://www.elastic.co/kr/guide/en/elasticsearch/reference/current/infer-service-google-vertex-ai.html">Vertex AI</a> 및 <a href="https://www.elastic.co/kr/guide/en/elasticsearch/reference/current/infer-service-google-ai-studio.html">Google AI Studio의</a>임베딩, 재순위 지정, 완성 모델과 통합되어 Google Cloud 환경을 벗어나지 않고도 벡터를 생성하고 순위를 지정하여 <a href="https://cloud.google.com/responsible-ai?hl=en">책임감 있는 AI </a>원칙을 보장합니다. 멀티모달 접근 방식을 지원함으로써 다양한 데이터 형식의 애플리케이션을 공동으로 촉진합니다.</p><p><a href="https://www.elastic.co/kr/search-labs/blog/vertex-ai-elasticsearch-playground-fast-rag-apps">플레이그라운드를</a> 통해 GenAI 검색 코드를 조정, 테스트 및 내보낼 수 있습니다.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/b1fc48dba1713cd01c4d7f3296587d0c4e6e7e0c-862x651.png" style="width: 100%;" /><p>하지만 검색 앱 구축에만 그치지 않습니다. Elastic은 Gemini 모델을 활용하여 <a href="https://www.elastic.co/kr/blog/elastic-google-vertex-ai-integration">Elastic AI 어시스턴트, 공격 탐색, 자동 가져오기 기능과</a> 같은 IT 운영을 강화함으로써 보안 분석가와 SRE가 저가치 작업에 대한 일상적인 피로를 줄이고 비즈니스 개선에 집중할 수 있도록 지원합니다. 또한 Elastic은 응답 시간, 토큰, 리소스와 같은 메트릭과 로그를 추적하여 최적의 성능을 보장하기 위해 <a href="https://www.elastic.co/kr/guide/en/integrations/current/gcp_vertexai.html">Vertex AI 사용량을 포괄적으로 모니터링할 수 있습니다.</a> 데이터 수집 및 임베딩 생성부터 하이브리드 검색을 통한 근거 마련에 이르기까지 전체 GenAI 라이프사이클을 함께 관리하며, LLM 기반 작업을 통해 GenAI 도구의 강력한 가시성 및 보안을 보장합니다.</p><h2>자세히 알아보고 사용해 보세요!</h2><p>이 기능을 사용해 보고 싶으신가요? 이 기능은 현재 구글 클라우드 프로젝트에서 정식 버전으로 출시되었습니다!</p><p>아직 시작하지 않으셨다면, Elastic Search AI Platform을 시작하고 기능을 살펴보는 가장 쉬운 방법 중 하나는 <a href="https://cloud.elastic.co/registration">무료 Elastic Cloud 체험판을</a> 사용하시거나 <a href="https://console.cloud.google.com/marketplace/product/elastic-prod/elastic-cloud?pli=1">Google Cloud Marketplace를</a> 통해 구독하는 것입니다.</p><p><em>이 게시물에 설명된 모든 기능의 출시와 시기는 Elastic의 단독 재량에 따라 결정됩니다. 현재 사용할 수 없는 기능이나 기능은 제때 또는 전혀 제공되지 않을 수 있습니다. Elastic, Elasticsearch 및 관련 상표는 미국 및 기타 국가에서 Elasticsearch N.V.의 상표, 로고 또는 등록 상표입니다. 기타 모든 회사 및 제품명은 해당 소유자의 상표, 로고 또는 등록 상표입니다.</em></p>]]></content:encoded>
        </item>
    </channel>
</rss>