<?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[Jeffrey Rengifo - Elasticsearch Labs]]></title>
    <description><![CDATA[Articles and tutorials from the Search team at Elastic]]></description>
    <copyright><![CDATA[© 2026. Elasticsearch B.V. All Rights Reserved]]></copyright>
    <image>
      <title><![CDATA[Jeffrey Rengifo - Elasticsearch Labs]]></title>
      <url>https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1121c0bf0e8a6e65/6a88da6340a1841030ef456f/search-labs-thumbnail.png</url>
      <link>https://www.elastic.co/jp/search-labs/author/jeffrey-rengifo</link>
    </image>
    <link>https://www.elastic.co/jp/search-labs/author/jeffrey-rengifo</link>
    <atom:link href="https://www.elastic.co/jp/search-labs/rss/author/jeffrey-rengifo.xml" rel="self" type="application/rss+xml"/>
    <language><![CDATA[jp]]></language>
    <lastBuildDate>Mon, 28 Sep 2026 04:52:01 GMT</lastBuildDate>
  <item>
    <title><![CDATA[Elasticsearchの検索再現率を測定・改善する方法：ハイブリッド検索で0.43から0.75へ]]></title>
    <description><![CDATA[Elasticsearchにおける検索再現率を測定および改善する方法を学びましょう。BM25の語彙検索とJina AIのベクトル埋め込みを組み合わせ、rank_eval APIを使用して実際の数値で改善効果を検証します。]]></description>
    <content:encoded><![CDATA[<p><a href="https://www.elastic.co/blog/practical-bm25-part-1-how-shards-affect-relevance-scoring-in-elasticsearch">BM25ランキングアルゴリズム</a>を用いた<a href="https://www.elastic.co/docs/solutions/search/full-text">語彙検索</a>は、低コストで高速であり、幅広いクエリに対して非常に効果的です。しかし盲点があります。それは、ドキュメントとトークンを共有しないクエリです。この記事では、BM25が足りないところを正確に測定します。Elasticsearchの<a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/search-rank-eval"> ランキング評価API</a>（<code>rank_eval</code>）を使用し、<a href="https://www.elastic.co/docs/explore-analyze/elastic-inference/eis">Elastic Inference Service</a>（EIS）を介して<a href="https://www.elastic.co/search-labs/es/blog/jina-embeddings-v3-elastic-inference-service">Jina AIの埋め込みを追加することでそのギャップを埋めます。</a>再現率スコアが <code>0.43</code> から <code>0.75</code> に上がるのを見て、その理由がわかるでしょう。</p><h2>リコールとは何ですか？</h2><p><a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/search-rank-eval#k-recall">再現率</a>は、<code>0</code>から<code>1</code>のスケールで、ユーザーが実際に欲しいドキュメントが検索結果にどれだけ含まれているかを測定します。クエリで3点の製品が表示されるはずなのに、検索結果の上位10位に2つしか表示されない場合は、そのクエリの再現率は<code>recall@10 = 0.67</code>となります。これは集合ベースの指標であり、その<em>k</em>件の結果内の関連ドキュメントの位置は考慮されません。10番目の位置にある関連ドキュメントは、1番目の位置にある関連ドキュメントと同じものとして扱われます。再現率が高いということは、関連性の高い検索結果を見逃さないということです。</p><p>
</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt5ffd147b13705680/6a170a6fe8fbce11a539fc22/b13af2a5d0ca055535d8bfe3dfe4b3d1093ee6da-1457x796.png" alt="Recall@10の計算方法を示すベン図です。すべての関連文書とBM25によって検索された上位10件の結果の重複を示し、Recall@10のスコアは0.40となります。" /><p>この図は2つの集合を示しています。1つは関連するすべてのドキュメント（左側）、もう1つはBM25が実際に取得したドキュメント（上位10件、右側）です。再現率に影響する交差部分、<code>prod_1</code>と<code>prod_2</code>が見つかり、<code>prod_3</code>、<code>prod_4</code>、<code>prod_6</code>は完全に見落とされました。結果：<code>Recall@10 = 2/5 = </code><strong><code>0.40</code></strong>。</p><h2>要件</h2><p>再現率がどのように機能するのかをよりよく理解するために、本題に入りましょう。このデモンストレーションではPythonを使用します。付属のノートブック（<a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/relevance-tuning-improving-recall-adding-vectors/notebook.ipynb">notebook.ipynb</a>）で一緒に進めることができます。すべてのコードブロックは、実行準備が整ったセルです。</p><p>提供されたコードでは、以下を使用しています。</p><ul><li><p>Elasticsearch 9.3以降</p></li><li><p>Python 3.10+</p></li></ul>pip install elasticsearch pandas plotly python-dotenv<ul><li><p>Elasticsearchの認証情報が含まれる<code>.env</code>ファイル</p></li></ul>ELASTICSEARCH_URL=https://your-cluster-url
ELASTICSEARCH_API_KEY=your-api-key<h2>データセット</h2><p>靴、電子機器、工具など、さまざまなカテゴリーにわたる1,000点の製品を掲載した製品カタログを使用します。</p><p>各ドキュメントには4つのフィールドがあります。</p><p>フィールド</p><p>タイプ</p><p>`タイトル`</p><p>テキスト</p><p>`description`</p><p>テキスト</p><p>`ブランド`</p><p>キーワード</p><p>`category`</p><p>キーワード</p><p>データセットは <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/relevance-tuning-improving-recall-adding-vectors/dataset.csv"><code>dataset.csv</code></a>から読み込まれます。</p><h2>語彙検索の力と限界</h2><p>BM25は、Elasticsearchおよびほとんどの検索エンジンのデフォルトのランキングアルゴリズムです。ドキュメントは、クエリ用語がどれだけ頻繁に出現するかによってスコアリングされます。これは、ドキュメントの長さと、インデックス全体でのそれらの用語の頻度に合わせて調整されます。さらに、小文字への正規化、語幹抽出、ストップワード除去といった<a href="https://www.elastic.co/docs/reference/text-analysis/analyzer-reference">アナライザー</a>も搭載されています。「running shoes」というクエリは、「Running Shoes」と一致し、おそらく「run」とも一致します。</p><p>これは、多くの種類のクエリに対して有効です。</p><ul><li><p>「running shoes」はタイトルにこれらのトークンを含む商品を即座にマッチングします。</p></li><li><p>「Bluetoothスピーカー」は、トークンが逐語的に表示されるため、携帯オーディオ製品として表示されます。</p></li></ul><p>結果は決定論的で説明可能です。つまり、検索クエリに含まれる用語がドキュメント中に含まれているからこそ、そのドキュメントは上位にランク付けされるのです。関連性のデバッグは単純明快です。</p><h3>問題が発生する箇所</h3><p>では、同じカタログに対してこれらのクエリを試してみましょう。</p><ul><li><p><strong>「スキンケアルーティン」：</strong>どの製品名にも「ルーティン」という単語は含まれません。BM25は、「スキンケア」で部分一致することができますが、フェイス美容液、ボディオイル、モイスチャライザーは、「ビタミンC」、「レチノール」、または「ブライトニング」のような用語を使用して説明されており、いずれもクエリと重複しません。完全なスキンケアルーティンを形成する製品は、共通のトークンを持たずにインデックス全体に散らばっています。</p></li></ul>ID: B06XX6DS3P, Score: 9.0552, Title: Replenix Retinol Smooth + Tighten Body Lotion - Collagen-Boosting, Regenerating Anti-Aging Body Cream, Reduces Appearance of Stretch Marks, 6.7 oz.

  ID: B08XMPKJ1L, Score: 5.2699, Title: Bio-Oil Skincare Body Oil (Natural) Serum for Scars and Stretchmarks, Face and Body Moisturizer Hydrates Skin, with Organic Jojoba Oil and Vitamin E, For All Skin Types, 6.7 oz

  ID: B01CY764KQ, Score: 5.0057, Title: Nike Up Or Down Men Deodorant - Pack of 2 | Long-Lasting Fragrance, Body Spray Combo for Men | Deodorant for Active Living | Nike Men's Deo Set | Ultimate Odor Protection | Grooming Essentials | Signature Nike Scent | High-Performance Men's Deodorant<ul><li><p><strong>「ペット用旅行用品」：</strong>これはユースケースのグループ分けであり、製品カテゴリではありません。犬用スリングキャリア、ペット用カーシート、旅行用クレートはどれも関連性のあるアイテムですが、それらの説明文は「旅行用品」というよりは、携帯性、安全性、快適性について述べています。BM25は「ペット」と大まかに一致しますが、旅行固有の商品を他のペットカタログと区別するシグナルはありません。</p></li></ul>ID: B0BVV7BKTW, Score: 7.4371, Title: Large Foldable Travel Duffel Bag with Shoes Compartment

ID: B07TNPHYNV, Score: 6.6455, Title: 40 Pieces Christmas Bronze Jingle Bells Craft Small Bells

ID: B08R8FRW53, Score: 6.6335, Title: CUBY Dog and Cat Sling Carrier
ID: B08QMCQYGM, Score: 6.5259, Title: YTFGGY Whiteboard Pinstripe Tape 6 Rolls 1/8"
ID: B0CP3LQSWM, Score: 6.2994, Title: Portable Dog Water Bottle 32 Oz<p>これは <strong>再現率の問題</strong>です。関連するドキュメントはインデックスに存在しますが、ユーザーの言葉と文書の単語が十分に一致しないため、BM25はそれらを見つけることができません。</p><p>同義語を追加することは、既知の事例に役立ちますが、ユーザーが意図を表現するすべての方法を列挙することはできません。そこでベクトルが役立ちます。</p><h2>リコールを測定すべき理由</h2><p>問題を解決する前に、まずそれを定量化する必要があります。</p><p><a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/search-rank-eval#k-recall"><strong>Recall@k</strong></a>は、ユーザーが実際に求めているドキュメントのうち、検索結果に表示されるドキュメントの数を測定します。正式には：</p>Recall@k = (relevant documents found in top k) / (total relevant documents)<p><a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/search-rank-eval#k-precision"><strong>Precision@k</strong></a>は、上位k件の結果のうち、実際にどれだけ関連性があるかを測定します。</p>Precision@k = (relevant documents in top k) / k<p>高精度とは、得られる結果が良好であることを意味します。電子商取引においては、関連商品を見落とすこと（再現率の低さ）は、多少不完全な結果を表示すること（精度の低さ）よりも深刻です。なぜなら、商品が表示されないということは、販売機会の損失を意味するからです。</p><p>Elasticsearchの<code>rank_eval</code> APIは、両方を体系的に測定できます。それぞれに評価されたドキュメントを含むクエリのリストを提供すると、Elasticsearchがすべてのクエリのメトリックを計算します。</p><h2>評価の設定</h2><p><code>rank_eval</code> APIには<strong>評価データセット</strong>が必要です。これは、クエリとそれに関連するドキュメントのマッピング、および関連性グレード（0 = 関連性なし、1 = 関連性あり、2 = 非常に関連性あり）で構成されます。</p><p>ノートブックでは、これは<a href="https://www.elastic.co/docs/solutions/search/ranking/learning-to-rank-ltr#learning-to-rank-judgement-list">判断リスト</a>です。</p>judgments = [
    # Query 1: "running shoes" BM25 handles well (tokens appear in product titles) 
    {"query_id": "q1", "doc_id": "B09NQJFRW6", "grade": 2, "query": "running shoes"},
    {"query_id": "q1", "doc_id": "B08JMD4LMM", "grade": 2, "query": "running shoes"},
    {"query_id": "q1", "doc_id": "B08VRJ6F2Q", "grade": 2, "query": "running shoes"},
    {"query_id": "q1", "doc_id": "B07S8NRRWR", "grade": 2, "query": "running shoes"},
    {"query_id": "q1", "doc_id": "B01HD620I8", "grade": 2, "query": "running shoes"},
    {"query_id": "q1", "doc_id": "B07DX86321", "grade": 2, "query": "running shoes"},
    {"query_id": "q1", "doc_id": "B0968YVLQ8", "grade": 1, "query": "running shoes"},
    {"query_id": "q1", "doc_id": "B093QJ39ZS", "grade": 1, "query": "running shoes"},
    {"query_id": "q1", "doc_id": "B096FGSC39", "grade": 1, "query": "running shoes"},
    {"query_id": "q1", "doc_id": "B01GVQWVV2", "grade": 1, "query": "running shoes"},

    # Query 2: "skincare routine" intent-based, "routine" never appears in product titles
    {"query_id": "q2", "doc_id": "B08XMPKJ1L", "grade": 2, "query": "skincare routine"},
    {"query_id": "q2", "doc_id": "B0BN3WQB92", "grade": 2, "query": "skincare routine"},
    {"query_id": "q2", "doc_id": "B0BT7B7P5T", "grade": 2, "query": "skincare routine"},
    {"query_id": "q2", "doc_id": "B00NPA2WEY", "grade": 2, "query": "skincare routine"},
    {"query_id": "q2", "doc_id": "B06XX6DS3P", "grade": 1, "query": "skincare routine"},
    {"query_id": "q2", "doc_id": "B07PDRD1KT", "grade": 1, "query": "skincare routine"},
    {"query_id": "q2", "doc_id": "B074J7869B", "grade": 1, "query": "skincare routine"},
    {"query_id": "q2", "doc_id": "B08JV31QW4", "grade": 1, "query": "skincare routine"},
    {"query_id": "q2", "doc_id": "B00K3TVJMQ", "grade": 1, "query": "skincare routine"},

    # Query 3: "study desk setup" intent-based, products are desks/stands/organizers
    {"query_id": "q3", "doc_id": "B08CS35J2T", "grade": 2, "query": "study desk setup"},
    {"query_id": "q3", "doc_id": "B09B3LFDXJ", "grade": 2, "query": "study desk setup"},
    {"query_id": "q3", "doc_id": "B07W58LMND", "grade": 1, "query": "study desk setup"},
    {"query_id": "q3", "doc_id": "B0CHYDX91L", "grade": 1, "query": "study desk setup"},

    # Query 4: "pet travel accessories" use-case grouping, products are carriers/crates/seats
    {"query_id": "q4", "doc_id": "B08R8FRW53", "grade": 2, "query": "pet travel accessories"},
    {"query_id": "q4", "doc_id": "B01MYUYX33", "grade": 2, "query": "pet travel accessories"},
    {"query_id": "q4", "doc_id": "B003C5RKE4", "grade": 2, "query": "pet travel accessories"},
    {"query_id": "q4", "doc_id": "B09GF8GBF6", "grade": 1, "query": "pet travel accessories"},
    {"query_id": "q4", "doc_id": "B0CP3LQSWM", "grade": 1, "query": "pet travel accessories"},
]<p>この組み合わせは意図的なものです。 <code>q1</code>はBM25が適切に処理するクエリ（製品タイトル内の正確なトークン）であり、 <code>q2</code> 、 <code>q3</code> 、 <code>q4</code>はユーザーの意図が特定の製品キーワードではなく概念として表現されるインテントベースのクエリです。</p><h2>BM25ベースライン再現率の測定</h2><p>まず、Elasticsearchクライアントを設定し、生のテキストデータをインデックス化します。</p>import os
import json
import pandas as pd
import plotly.graph_objects as go
from elasticsearch import Elasticsearch, helpers
from dotenv import load_dotenv

load_dotenv()

es = Elasticsearch(
    os.getenv("ELASTICSEARCH_URL"),
    api_key=os.getenv("ELASTICSEARCH_API_KEY")
)

INDEX_NAME = "ecommerce-products"<p>次にBM25の<code>rank_eval</code>リクエストを作成しましょう。リスト内の各リクエストは、クエリとその評価を組み合わせたものです。</p>judgments_df = pd.DataFrame(judgments)

bm25_requests = []
for query_id, query_text in (
    judgments_df[["query_id", "query"]].drop_duplicates().values
):
    relevant_docs = judgments_df[judgments_df["query_id"] == query_id]
    ratings = [
        {"_index": INDEX_NAME, "_id": row["doc_id"], "rating": row["grade"]}
        for _, row in relevant_docs.iterrows()
    ]

    bm25_requests.append({
        "id": query_id,
        "request": {
            "query": {
                "multi_match": {
                    "query": query_text,
                    "fields": ["title", "description"]
                }
            }
        },
        "ratings": ratings,
    })

bm25_eval = {
    "requests": bm25_requests,
    "metric": {"recall": {"k": 10, "relevant_rating_threshold": 1}},
}

bm25_result = es.rank_eval(index=INDEX_NAME, body=bm25_eval)
print("BM25 Recall@10:", bm25_result.body["metric_score"])<p>次の結果が得られます。</p>BM25 Recall@10: 0.43<p><code>0.43</code> は、4つのクエリすべてで、BM25が見つけるべきドキュメントの43%しか見つけられないということです。不足している点は、意図に基づくクエリに集中している。「スキンケアルーティン」という検索では、「ルーティン」が製品タイトルに含まれていないため、フェイス美容液やボディオイルが見つかりません。また、「ペット用旅行用品」という検索では、旅行用品ではなく携帯性や安全性の観点から説明されているキャリーケースやクレートが見つからない一方で、関連性のないペット用品が検索結果に表示されます。</p><p>これがベースラインです。これで、破るべき数値ができました。</p><h2>Jina埋め込みを用いたベクトル検索機能の追加</h2><p><a href="https://www.elastic.co/docs/solutions/search/vector"><code>Vector search</code></a> はドキュメントとクエリを高次元ベクトルとしてエンコードします。高次元ベクトルとは、数百または数千の数値から構成されるベクトルの一種で、各数値はそれが表すデータの特定の特徴をエンコードします。意味が似ているドキュメントは、たとえ共通の単語が一つもなくても、ベクトル空間上では互いに近い位置に配置されます。「ジム器具」と「ダンベルセット」は、概念が関連しているため、近くに配置されるでしょう。私がベクトルデータベースとしてElasticsearchを選んだ理由は、ハイブリッド検索をサポートしており、セマンティックな理解とキーワードの精度をすぐに実現できるからです。</p><p><a href="https://www.elastic.co/docs/explore-analyze/elastic-inference/eis">EIS</a>には、<a href="https://www.elastic.co/docs/api/doc/elasticsearch/group/endpoint-inference">推論API</a>を通じてモデル埋め込みを標準でサポートする機能が含まれています。</p><h3>ステップ1：Jina埋め込みv5を推論エンドポイントとして使用する</h3>INFERENCE_ENDPOINT_ID = ".jina-embeddings-v5-text-small"<p>クラスターにGPUリソースがある場合（Elastic CloudおよびElasticsearch 9.3以降で利用可能）、埋め込みはGPU上で生成されます。これはCPU推論よりも大幅に高速であり、従来、大規模な環境でベクトル処理を高価にしていたパフォーマンスのトレードオフを解消します。</p><p>なぜJina埋め込みが特に重要なのでしょうか。<a href="https://www.elastic.co/search-labs/blog/jina-embeddings-v5-text">JINA-embeddings-v5-text</a>は、32,000トークンのコンテキストウィンドウを持ち、タスク固有の <a href="https://arxiv.org/abs/2106.09685">Low-Rank Adaptation (LoRA) アダプター</a>をサポートする多言語モデル（119言語以上に対応）です。短い商品説明であれば、そのままでも十分に機能します。<code>jina-embeddings-v5-text</code>モデルの詳細については<a href="https://huggingface.co/jinaai/jina-embeddings-v5-text-small">こちらを</a>ご覧ください。</p><h3>ステップ2：セマンティックフィールドを持つインデックスを作成</h3>index_mappings = {
    "mappings": {
        "properties": {
            "title": {"type": "text", "copy_to": "semantic_field"},
            "description": {"type": "text", "copy_to": "semantic_field"},
            "brand": {"type": "keyword"},
            "category": {"type": "keyword"},
            "semantic_field": {
                "type": "semantic_text",
                "inference_id": INFERENCE_ENDPOINT_ID,
            },
        }
    }
}

if not es.indices.exists(index=INDEX_NAME):
    es.indices.create(index=INDEX_NAME, body=index_mappings)
    print(f"Created index: {INDEX_NAME}")<p>ここで重要なのは <a href="https://www.elastic.co/docs/solutions/search/semantic-search/semantic-search-semantic-text"><code>semantic_text</code></a> フィールドタイプです。これは<a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/dense-vector"><code>dense_vector</code></a>よりも高レベルの抽象化です。推論エンドポイントを指定すると、Elasticsearchが埋め込みの生成を自動的に行います。</p><p><a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/copy-to"><code>copy_to</code></a>プロパティが<code>title</code>と<code>description</code>にある場合、両方のフィールドのコンテンツが<a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/semantic-text"><code>semantic_field</code></a>に流れ込んで埋め込まれるため、単一のベクトルで製品の完全な表現を捉えることができます。</p><h3>ステップ3：製品をインデックス化する</h3>def bulk_index(products, index_name):
    actions = []
    for product in products:
        doc_id = product.get("_id")
        source = {k: v for k, v in product.items() if k != "_id"}
        action = {"_index": index_name, "_source": source}
        if doc_id:
            action["_id"] = doc_id
        actions.append(action)

    success, failed = helpers.bulk(es, actions, raise_on_error=False)
    if failed:
        for error in failed:
            print(f"Error: {error}")
    else:
        print(f"Successfully indexed {success} documents")

bulk_index(products, INDEX_NAME)<p>インデックス時には、Elasticsearchは各文書の推論エンドポイントを呼び出し、その結果得られた埋め込みを<code>semantic_field</code>に保存します。余計なコードは必要ありません。</p><h2>ハイブリッド検索：BM25とベクトルおよびRRFの組み合わせ</h2><p>ベクトルを追加すると再現率は向上しますが、ベクトルのみを使用すると、完全一致クエリの精度が低下するリスクがあります。「ランニングシューズ」は、依然として逐語的に一致するものを最優先にランク付けするべきです。ハイブリッド検索は、その精度を維持するために、語彙要素を意図的に保持します。</p><p>ハイブリッド<a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/reciprocal-rank-fusion">逆順位融合</a>（RRF）検索は、両方の長所を保持します。</p><ul><li><p>BM25は正確およびほぼ正確なクエリを高精度で処理します。</p></li><li><p>セマンティック検索は、意図に基づく多言語クエリを高い再現率で処理します。</p></li><li><p>RRFは2つのランキングリストを1つのランキングに統合します。</p></li></ul><p>RRF式は、各結果リストのランクに基づいて各文書にスコアを割り当てます。</p>score = sum(1 / (rank_constant + rank))<p>両方のリストで上位に表示されるドキュメントほど、統合スコアは高くなります。<code>rank_constant</code>は、より低いランクの文書に与えられる重みを制御します。</p>hybrid_requests = []

for query_id, query_text in (
    judgments_df[["query_id", "query"]].drop_duplicates().values
):
    relevant_docs = judgments_df[judgments_df["query_id"] == query_id]
    ratings = [
        {"_index": INDEX_NAME, "_id": row["doc_id"], "rating": row["grade"]}
        for _, row in relevant_docs.iterrows()
    ]

    hybrid_requests.append({
        "id": query_id,
        "request": {
            "retriever": {
                "rrf": {
                    "retrievers": [
                        {
                            "standard": {
                                "query": {
                                    "multi_match": {
                                        "query": query_text,
                                        "fields": ["title", "description"],
                                    }
                                }
                            }
                        },
                        {
                            "standard": {
                                "query": {
                                    "match": {
                                        "semantic_field": {"query": query_text}
                                    }
                                }
                            }
                        },
                    ],
                    "rank_window_size": 50,
                    "rank_constant": 5,
                }
            }
        },
        "ratings": ratings,
    })

hybrid_eval = {
    "requests": hybrid_requests,
    "metric": {"recall": {"k": 10, "relevant_rating_threshold": 1}},
}

hybrid_result = es.rank_eval(index=INDEX_NAME, body=hybrid_eval)
print("Hybrid Recall@10:", hybrid_result.body["metric_score"])<p>次の結果が得られます。</p>Hybrid Recall@10: 0.75<p>ハイブリッドはBM25（<code>0.43</code>）よりも大幅に改善されており、「ランニングシューズ」のような完全一致クエリに対しても精度を維持します。</p><h2>結果：ビフォーアフター</h2><p>3つのアプローチの完全な比較は次のとおりです。</p>methods = {
    "BM25 (Lexical)": bm25_requests,
    "Hybrid (BM25 + Vectors)": hybrid_requests,
}

recall_metric = {"recall": {"k": 10, "relevant_rating_threshold": 1}}

comparison_data = []
for method_name, requests in methods.items():
    result = es.rank_eval(
        index=INDEX_NAME,
        body={"requests": requests, "metric": recall_metric}
    )
    comparison_data.append({
        "method": method_name,
        "recall@10": result.body["metric_score"]
    })

comparison_df = pd.DataFrame(comparison_data)
print(comparison_df.to_string(index=False))<p>次の結果が得られます。</p><p>メソッド</p><p>Recall@10</p><p>BM25（語彙）</p><p>0.43</p><p>ハイブリッド（BM25 + ベクター）</p><p>0.75</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt5a1d72b57056fe64/6a170a71c1e8a56c58f882ab/e49f6c10516b0a48a0ad75962c6590ee07311407-700x500.png" alt="BM25語彙検索と、BM25とベクトルを組み合わせたハイブリッド検索のRecall@10を比較した棒グラフ。ハイブリッド検索の方がはるかに高いリコール率を達成していることがわかります。" /><p>クエリごとに分解すると次のようになります。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt871347f754c866d0/6a170a73839dfa40abdcfeb4/40e36dcb7b34cbf4649c512bcb60cef60f1778a6-700x500.png" alt="グループ化された棒グラフ。4つの製品クエリにおけるBM25語彙検索とハイブリッド検索のRecall@10を比較し、各クエリでハイブリッド検索が一貫して語彙検索を上回ることを示しています。" /><h2>まとめ</h2><p>この記事を通して、BM25の語彙検索は、ユーザーが正確なクエリを入力する場合には信頼性が高いものの、キーワードではなく意図に基づいて検索する場合は再現率が低下することがわかりました。<code>rank_eval</code>を用いて、そのギャップを実数で測定するための再現可能な基準を確立しました。そこから、Jina埋め込みを利用した<code>semantic_text</code>フィールドを追加し、評価を再度実行しました。結果：ハイブリッド検索により、<code>0.43</code>から<code>0.75</code>への再現率が向上し、正確な一致クエリでの精度を維持しました。ただし、実際の差はクエリの組み合わせによって異なります。</p><p>このパターンは、この例を超えてスケールします。ユーザーの実際のクエリから判断を収集し、<code>rank_eval</code>をベースラインとして実行し、<code>semantic_text</code>を追加して再度測定します。何がどれだけ改善したかが正確にわかるでしょう。</p><h2>今後の見通し</h2><ul><li><p>再現率とベクトル検索についてさらに詳しく：<a href="https://www.elastic.co/search-labs/blog/recall-vector-search-quantization">再現率とベクトル検索の量子化</a>（Jeff Vestal著）</p></li><li><p>上位結果の精度をさらに向上させるために再ランキング機能を追加</p></li><li><p><a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/rrf.html">Elasticsearchハイブリッド検索のドキュメント</a>をご覧ください。</p></li><li><p><a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/search-rank-eval.html"><code>rank_eval</code></a><a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/search-rank-eval.html"> API</a>について詳しくはこちら</p></li></ul>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/elasticsearch-relevance-tuning-improve-recall</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/elasticsearch-relevance-tuning-improve-recall</guid>
    <category><![CDATA[ハイブリッド検索]]></category>
    <category><![CDATA[Vector Database]]></category>
    <dc:creator><![CDATA[Jeffrey Rengifo]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt37c9d2971b5a2db3/6a170a75cf4f254223b2d149/492c9b5432a2b9e40cebb3b60f0df019a8c7bf6d-1280x720.png" length="0" type="image/png"/>
    <pubDate>Mon, 04 May 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[TypeScriptを使用したElasticsearch MCPサーバーの作成]]></title>
    <description><![CDATA[TypeScriptとClaude Desktopを使用してElasticsearch MCPサーバーを作成する方法を学びます。]]></description>
    <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><p><a href="https://www.elastic.co/docs/solutions/search/agent-builder/mcp-server">Elasticsearch 9.2+用Elastic Agent Builder MCPサーバー</a></p></li><li><p><a href="https://github.com/elastic/mcp-server-elasticsearch?tab=readme-ov-file#elasticsearch-mcp-server">旧バージョン向けのElasticsearch MCPサーバー（Python）</a></p></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>モデルを用いて要約と引用を生成し、Claude DesktopをMCPクライアントおよびUIとして活用してユーザーのクエリを受け取り、応答を提供します。最終的には、エンジニアが組織内の技術文書全体からベストプラクティスを発見し、統合するのに役立つ内部ナレッジアシスタントが完成します。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltad9133cb083ad352/6a170c19b0367d411e72bd5b/ec5771a874cf9740d4cac6888622cbe8cd6aede7-1999x1133.png" alt="TypeScriptとClaude Desktopを使用してElastic MCPサーバーを作成します。" /><h2>要件：</h2><ul><li><p>Node.js 20 +</p></li><li><p>Elasticsearch</p></li><li><p>OpenAI APIキー</p></li><li><p>Claude Desktop</p></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>は多数あり、それぞれに特徴や制限があります。簡便性と普及度を考慮し、今回はMCPクライアントとして<a href="https://claude.ai/download">Claude Desktop</a>を使用します。これは、ユーザーが自然言語で質問できるチャットインターフェースとして機能し、MCPサーバーが公開しているツールを自動的に呼び出して、文書を検索し、要約を生成します。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt06fd7a02042094e1/6a170c1b14b2700024e3c651/66eb0b11473347b6cf2d85718251eeac38d6249d-1999x1491.png" alt="「Coffee and Claude time? How can I help you today?」というメモが表示されたClaude 4.5 Sonnetのページ" /><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><p><a href="https://www.elastic.co/search-labs/blog/elastic-mcp-server-typescript-claude#configure-mcp-server-for-elasticsearch">Elasticsearch 用の MCP サーバーを設定してください。</a></p></li><li><p><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></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/elastic-mcp-server-typescript-claude#test-it-out">試してみてください。</a></p></li></ol><h3>Elasticsearch MCPサーバーを設定してください</h3><p>まず、Nodeアプリケーションを初期化します。</p>npm init -y<p>これで <code>package.json</code>ファイルが作成され、このアプリケーションに必要な依存関係のインストールを開始できます。</p>npm install @elastic/elasticsearch @modelcontextprotocol/sdk openai zod &amp;&amp; npm install --save-dev ts-node @types/node typescript<ul><li><p><strong>@elastic/elasticsearch</strong> はElasticsearchのNode.jsライブラリにアクセスするためのものです。</p></li><li><p><strong>@modelcontextprotocol/sdk</strong>は、MCPサーバーの作成と管理、ツールの登録、MCPクライアントとの通信処理を行うためのコアツールを提供します。</p></li><li><p><strong>openai</strong>は、OpenAIのモデルと対話し、要約や自然言語による対応を生成することができます。</p></li><li><p><a href="https://zod.dev/"><strong>zod</strong></a>は、各ツールの入出力データの構造化スキーマの定義と検証に役立ちます。</p></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>{
    "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"]
}<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>// 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";<p>また、ElasticsearchとOpenAIの呼び出しを処理するようにクライアントを初期化します。</p>const openai = new OpenAI({
  apiKey: OPENAI_API_KEY,
});

const _client = new Client({
  node: ELASTICSEARCH_ENDPOINT,
  auth: {
    apiKey: ELASTICSEARCH_API_KEY,
  },
});<p>実装をより堅牢にし、構造化された入出力を保証するために、 <a href="https://zod.dev/"><code>zod</code></a>を使用してスキーマを定義します。これにより、ランタイムでデータを検証し、エラーを早期に捕捉し、ツールの対応をプログラムで処理しやすくすることができます。</p>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&lt;typeof DocumentSchema&gt;;
type SearchResult = z.infer&lt;typeof SearchResultSchema&gt;;<p>構造化出力の詳細については、<a href="https://www.elastic.co/search-labs/blog/structured-outputs-elasticsearch-guide">こちらを</a>ご覧ください。</p><p>それでは、MCPサーバーを初期化しましょう。</p>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",
});<h4>MCPツールの定義</h4><p>すべての設定が完了したので、MCPサーバーによって公開されるツールの作成を開始できます。このサーバーは2つのツールを公開します。</p><ul><li><p><strong><code>search_docs</code></strong><strong>：</strong>Elasticsearchで文書を全文検索で検索します。</p></li><li><p><strong><code>summarize_and_cite</code></strong><strong>：</strong>以前に取得した文書から情報を要約・統合し、ユーザーの質問に答えます。このツールは、出典となる文書を参照する引用も追加します。</p></li></ul><p>これらのツールを組み合わせることで、シンプルな「検索してから要約」ワークフローが構築されます。一方のツールが関連文書を取得し、もう一方のツールがその文書を使用して要約と引用を含む回答を生成します。</p><h4>ツールの応答形式</h4><p>各ツールは任意の入力パラメータを受け入れることができますが、以下の構造で応答する必要があります。</p><ul><li><p><strong>Content：</strong> これは非構造化形式でのツールの応答です。このフィールドは通常、テキスト、画像、音声、リンク、または埋め込みを返すために使用されます。この用途では、ツールによって生成された情報を含む整形済みテキストを返すために使用されます。</p></li><li><p><strong>structuredContent：</strong>これは、各ツールの結果を構造化された形式で提供するために使用されるオプションの戻り値です。これはプログラム上の目的に役立ちます。このMCPサーバーでは使用されていませんが、他のツールを開発したり、結果をプログラムで処理したりする場合に便利です。</p></li></ul><p>その構造を念頭に置いて、各ツールについて詳しく見ていきましょう。</p><h4>Search_docsツール</h4><p>このツールは、Elasticsearchインデックスで<a href="https://www.elastic.co/docs/solutions/search/full-text">全文検索</a>を実行し、ユーザークエリに基づいて最も関連性の高いドキュメントを取得します。主要な一致をハイライトし、関連性スコアを素早くまとめてくれます。</p>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 }) =&gt; {
    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) =&gt; {
        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) =&gt;
            `[${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,
      };
    }
  }
);<p><a href="https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-fuzzy-query"><em><code>fuzziness</code></em></a><em><code>: “AUTO”</code></em> は <em> 分析対象のトークンの長さに応じて誤字許容度を調整するように設定しています。また </em>、<em> タイトルフィールドで一致が発生したドキュメントのスコアを上げるtitle^2も設定しました。</em>。</p><h4>summarize_and_citeツール</h4><p>このツールは、前回の検索で取得したドキュメントに基づいて要約を生成します。OpenAIの <code>gpt-4o-mini</code>モデルを使用して、ユーザーの質問に答えるために最も関連性の高い情報を統合し、検索結果に直接基づく回答を提供します。要約に加えて、使用したソースドキュメントの引用情報（メタデータ）も返します。</p>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 }) =&gt; {
    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) =&gt;
            `[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) =&gt; ({
        id: r.id,
        title: r.title,
        tags: r.tags,
        relevance_score: r.score,
      }));

      const citationText = citations
        .map(
          (c: any, i: number) =&gt;
            `[${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,
      };
    }
  }
);<p>最後に、<a href="https://github.com/modelcontextprotocol/typescript-sdk?tab=readme-ov-file#stdio">stdio</a>を使用してサーバーを起動する必要があります。つまり、MCPクライアントは、標準の入出力ストリームを読み書きすることでサーバーと通信します。stdioは最もシンプルな転送オプションで、クライアントによってサブプロセスとして立ち上げられるローカルMCPサーバーに適しています。ファイルの最後に以下のコードを追加します。</p>const transport = new StdioServerTransport();
server.connect(transport);<p>次に、以下のコマンドを使用してプロジェクトをコンパイルします。</p>npx tsc index.ts --target ES2022 --module node16 --moduleResolution node16 --outDir ./dist --strict --esModuleInterop<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>{
  "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"
      }
    }
  }
}<p><code>args</code>値は、 <code>dist</code>フォルダ内のコンパイル済みファイルを指す必要があります。また、設定ファイル内の環境変数も、コード内で定義されているものと全く同じ名前で設定する必要があります。</p><h3>試してみる</h3><p>各ツールを実行する前に、「<strong>検索とツール</strong>」をクリックして、ツールが有効になっていることを確認します。ここでは、それぞれを有効または無効にすることもできます。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt395a7337021f9820/6a170c1c67045bb74d45c228/172981c2a54adabc70d5819013c3007670935605-1999x1002.png" alt="「Good afternoon, Jeff. How can I help you today?」というメモが表示されたClaude 4.5 Sonnetのページ" /><p>最後に、Claude DesktopのチャットからMCPサーバーをテストして、質問を始めましょう。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf4ac458dc0206271/6a170c1e66c4f91328f8c072/03654c0f8c53c714f801fba8b25747071179209b-1999x1353.png" alt="ユーザーがClaude Desktopチャットで、認証方法と役割ベースのアクセス制御に関するドキュメントを検索するようリクエストし、それに対するClaudeの回答。" /><p>「<strong>認証方法とRBACに関する文書を検索する</strong>」という質問に対して、<code>search_docs</code> ツールが実行され、以下の結果が返されます。</p>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.<p>回答は「素晴らしい！認証方法とロールベースのアクセス制御に関する関連文書を5件見つけました。見つかった内容は以下のとおりです。」</p><p>ツール呼び出しは、対応ペイロードの一部としてソース文書を返します。これらの文書は、後に引用を生成するために使用されます。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltbaf48a9468770ce2/6a170c21509168bffee1bb14/25ff4c7e9563d99752f95540dafdc7fd211a66e3-800x530.gif" alt="Claude 4.5 Sonnetのページ。認証方法とRBACに関する5つの関連文書がスクロールして表示されます。" /><p>また、複数のツールを一度のインタラクションで連結することも可能です。この場合、Claude Desktopはユーザーの質問を分析し、まず<code>search_docs</code>を呼び出して関連文書を取得し、次にその結果を<code>summarize_and_cite</code>に渡して最終的な回答を生成する必要があると判断します。これらすべては、ユーザーからの個別のプロンプトを必要とせずに実行されます。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blta46ff45274e64192/6a170c230c4857a91501aac1/e6a8a46acb4236e77058f18bcd2f0737b5882c05-1999x1101.png" alt="Claude Desktopチャットに、「Jeff returns」というメモと、新しいユーザーからの質問「What are the main recommendations to improve authentication and access control across our systems? Include references.」が添えられている。" /><p>この場合、「<strong>システム全体の認証とアクセス制御を改善するための主な推奨事項は何ですか？参考文献を含めてください。</strong>」というクエリに対して、以下の結果が得られました。</p>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.<p>前のステップと同様に、この質問に対する各ツールの回答を確認できます。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt8f633c518e708a99/6a170c25ab7f082991db9ed6/cb606d356b2f7d5e4878a5eff71bc881869ac0ee-800x585.gif" alt="Claude Desktopチャットページ、スクロールテキストには各ツールからの対応が含まれ、システム全体の認証とアクセス制御を改善するための主な推奨事項に関する質問への回答が含まれている。" /><p><em>注：各ツールの使用を承認するかを確認するサブメニューが表示された場合は、</em><em><strong>「常に許可」</strong></em><em>または</em><em><strong>「一度だけ許可」</strong></em><em>を選択します。</em></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt6627ee0bff1862df/6a170c266f7f040f6f91488c/aea942ba9b0037526ea215bec65690f1a5c3099c-1522x250.png" alt="ユーザーが選択できるClaude Desktop「常に許可」と「一度だけ許可」のオプション。" /><h2>まとめ</h2><p>MCPサーバーは、ローカルとリモートの両方のアプリケーションのLLMツールの標準化に向けた重要な一歩です。完全な互換性の実現にはまだ取り組んでいますが、その方向へ急速に進んでいます。</p><p>この記事では、ElasticsearchをLLM搭載アプリケーションに接続するカスタムMCPサーバーをTypeScriptで構築する方法を学びました。当サーバーは2つのツールを提供しています。1つはQuery DSLを使用して関連文書を取得するためのツール<code>search_docs</code>、もう1つはOpenAIモデルとクライアントUIとしてのClaude Desktopを使用して引用付きの要約を生成するためのツール<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>
    <link>https://www.elastic.co/search-labs/blog/elastic-mcp-server-typescript-claude</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/elastic-mcp-server-typescript-claude</guid>
    <category><![CDATA[エージェント型AI]]></category>
    <category><![CDATA[統合]]></category>
    <dc:creator><![CDATA[Jeffrey Rengifo]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt5600198cb47666a5/6a170c28509168ce3ae1bb18/0bb24c05fff391f42070c2883182ea6fe9cb9680-1280x720.png" length="0" type="image/png"/>
    <pubDate>Fri, 27 Mar 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Elasticsearch Inference APIとHugging Faceモデルを組み合わせて使用]]></title>
    <description><![CDATA[推論エンドポイントを使用してElasticsearchをHugging Faceモデルに接続する方法と、セマンティック検索とチャット補完機能を備えた多言語ブログ推奨システムを構築する方法を学びましょう。]]></description>
    <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://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt9094997548bd70f8/6a170d6a839dfa0ad6dcff54/7ddadf1976421a860a7d62087239adb9150d808b-1999x1388.png" alt="複数の小規模言語モデルを、x軸にモデルサイズ（数十億個のパラメータ）、y軸に勝率（パーセント）でプロットした散布図。SmolLM3-3Bは効率性の傾向において上位に位置し、同規模の他のモデルよりも高い勝率を示しています。" /><h2>要件</h2><ul><li><p><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>クイックスタートを使うこともできます。</p></li><li><p><strong>Python 3.12：</strong>Pythonは<a href="https://www.python.org/">こちら</a>からダウンロードしてください。</p></li><li><p><strong>Hugging Face</strong><a href="https://huggingface.co/docs/hub/en/security-tokens">アクセストークン</a>。</p></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://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf217b7b7db4e1e6c/6a170d6ca929cf8022ae0a3b/1dfbc2323438feaaa42e13ab242dd1f7166f74aa-1200x676.png" alt="Elasticsearchインデックスがセマンティック検索結果を推論エンドポイントに送り込み、そこから記事のレコメンデーション結果が返されるフロー図。" /><p>この記事では、コンパクトなサイズと強力な多言語推論能力・ツール呼び出し能力を組み合わせた<strong>SmolLM3-3B</strong>の性能を検証します。検索クエリに基づいて、一致するすべてのコンテンツ（英語とスペイン語）をLLMに送信し、検索クエリと結果に基づいたカスタムメイドの説明を含むおすすめ記事のリストを生成します。</p><p>AIによる推奨生成システムを備えた記事サイトのUIは次のようになります。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt20e69b9a06fecd65/6a170d6e839dfa6f97dcff58/8d3b86b212f28ff279f2da67a33e6134039f0e4e-1999x949.png" alt="AIによるおすすめ生成システムを備えた記事サイトのUI。3つの例がリストされており、テキストは英語、タイトルは英語またはスペイン語のいずれかで表示される。" /><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>を使用するには、2つの重要な要素（Hugging Face APIキーと実行中のHugging FaceエンドポイントURL）が必要です。下の画像のように表示されるはずです。</p>PUT _inference/chat_completions/hugging-face-smollm3-3b
{
    "service": "hugging_face",
    "service_settings": {
        "api_key": "hugging-face-access-token", 
        "url": "url-endpoint" 
    }
}<p>Hugging FaceのElasticsearchにおける推論エンドポイントは、 <code>text_embedding</code>, <code>completion</code>, <code>chat_completion</code>, と <code>rerank</code>の異なるタスクタイプをサポートしています。このブログ記事では、検索結果とシステムプロンプトに基づいて会話形式のレコメンデーションをモデルに生成させる必要があるため、<code>chat_completion</code> を使用します。このエンドポイントを使用すると、Elasticsearch APIを使用してElasticsearchから直接チャットの完了を簡単に実行できます。</p>POST _inference/chat_completion/hugging-face-smollm3-3b/_stream
{
  "messages": [
      { "role": "user", "content": "&lt;user prompt&gt;" }
  ]
}<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://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blta7bdfa43d6766324/6a170d6fb339d59e5476a039/b816e9fba1fe172687bf58f5143fb1f838c1077f-549x331.png" alt="「smoll3」にフィルタリングされたモデルカタログのインターフェースビュー。テキスト生成「smollm3‑3b」という名前の1つのモデル、vLLM、GPU 1× NVIDIA L4、定価$ 0.8 と、すべての Hugging Faceモデルに検索を拡張する提案のメモが表示。" /><p><strong>SmolLM3-3B</strong>モデルを選択します。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltdb0a2e6ffd7deb20/6a170d710c48574b7401aafc/610d3aba0429f3666c2df3616d513eb6a4397c0c-502x478.png" alt="SmolLM3-3Bモデルのエンドポイントを作成するためのインターフェース。モデル名、「Hugging Faceによって検証済み」という注記、エンドポイント名フィールド、実行中のレプリカ1つあたり1時間0.80ドルのコスト、cURLオプション、および「エンドポイントの作成」ボタンが表示。" /><p>ここから、Hugging FaceのエンドポイントURLを取得します。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt25714021711ed6ff/6a170d72c1e8a54853f88336/025094ddb2cfbd1f0f216a5ec4e119b0f4fa2c42-646x328.png" alt="「smollm3-3b-pnz」という名前のHugging Face推論エンドポイントのダッシュボードビュー。緑色の実行ステータス、アクティブなレプリカ1つ、過去1時間のリクエスト数0、ナビゲーションタブ、表示されているエンドポイントURLが表示。" /><p>Elasticsearch <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-inference-put-hugging-face">Hugging Faceの推論エンドポイントのドキュメント</a>で述べられているように、テキスト生成にはOpenAI APIと互換性のあるモデルが必要です。そのため、<code>/v1/chat/completions</code>のサブパスをHugging FaceのエンドポイントURLに追加する必要があります。最終的な結果は次のようになります。</p>https://j2g31h0futopfkli.us-east-1.aws.endpoints.huggingface.cloud/v1/chat/completions<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>fine-grained</em>（本番環境に推奨。特定のリソースへのアクセスのみを提供）、<em>read</em>（読み取り専用アクセス用）、<em>write</em>（読み取りおよび書き込みアクセス用）の3つから選択できます。このチュートリアルでは、推論エンドポイントを呼び出すだけでよいので、readトークンで十分です。次のステップのために、このキーを保存しておいてください。</p><h4>Elasticsearch推論エンドポイントの設定</h4><p>まず、Elasticsearch Pythonクライアントを宣言します。</p>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"]
)<p>次に、Hugging Faceモデルを使用するElasticsearch推論エンドポイントを作成します。このエンドポイントを使用すると、ブログ記事とモデルに渡されたプロンプトに基づいて応答を生成できます。</p>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"],
            },
        },
    )<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>// 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."
  }<h4>Elasticsearch マッピング</h4><p>データセットが定義されたので、ブログ記事の構造に適切にフィットするデータスキーマを作成する必要があります。Elasticsearchにデータを格納するために以下の<a href="https://www.elastic.co/docs/manage-data/data-store/mapping">インデックスマッピング</a>が使用されます。</p>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)<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>フィールドには2つのサブフィールドが含まれています。<code>original</code>サブフィールドは、記事の元の言語に応じて英語またはスペイン語でタイトルを格納し、<code>translated_title</code>サブフィールドはスペイン語の記事にのみ存在し、元のタイトルの英語訳が含まれています。</p><h3>データの取り込み</h3><p>以下のコードスニペットは<a href="https://www.elastic.co/docs/reference/elasticsearch/clients/javascript/bulk_examples">bulk API</a>を使用してブログ投稿データセットをElasticsearchに取り込みます。</p>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)}")<p>Elasticsearchに記事を取り込んだので、次に<code>semantic_text</code>フィールドに対して検索できる関数を作成する必要があります:</p>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 []<p>推論エンドポイントを呼び出す関数も必要です。この場合、<strong><code>chat_completion</code></strong>タスクタイプを使用してエンドポイントを呼び出し、ストリーミング応答を取得します。</p>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"]) &gt; 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)}"<p>ここで、 <code>chat_completions</code> 推論エンドポイントと推薦エンドポイントを合わせてセマンティック検索関数を呼び出し、カードに割り当てられるデータを生成する関数を書くことができます。</p>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<p>最後に、情報を抽出して出力できるようにフォーマットする必要があります。</p>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 &lt;think&gt; tags
        cleaned_text = re.sub(
            r"&lt;think&gt;.*?&lt;/think&gt;", "", 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 + "┘")<p>セキュリティブログの投稿について質問して、これをテストしてみましょう。</p>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)<p>ここでは、ワークフローによって生成されたコンソール内のカードを確認できます。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt4aa221a08a51aeb3/6a170d7460084be1413c45d6/730d35212594bb3db30447c3ea7e2a92857287b7-1999x1515.png" alt="セクションタイトル「Recommended Articles」。認証システムの脆弱性、移行リスク、REST API v2のパフォーマンスと認証の改善、通知システムの変更、新しいAPIの完全ガイドなど、5つの記事の要約がボックスで表示。" /><p>すべてのヒットとLLMの対応を含む完全な結果を<a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/elasticsearch-inference-api-and-hugging-face/results.md">このファイル</a>でご覧いただけます。</p><p>「Security and vulnerabilities」に関連する記事をクエリしています。この質問は、Elasticsearchに保存されているドキュメントに対する検索クエリとして使用されます。取得された結果はモデルに渡され、モデルはその内容に基づいてレコメンデーションを生成します。ご覧の通り、このモデルは読者がクリックする動機付けとなる魅力的な短いテキストを非常にうまく生成しています。</p><h2>まとめ</h2><p>この例では、ElasticsearchとHugging Faceを組み合わせて、AIアプリケーション向けの高速で効率的な集中型システムを構築する方法を示します。Hugging Faceの豊富なモデルカタログにより、このアプローチでは手作業を削減し、柔軟性を確保できます。特にSmolLM3-3Bを使用すると、コンパクトな多言語モデルでも、セマンティック検索と組み合わせることで有意義な推論とコンテンツ生成を実現できることがわかります。これらのツールを組み合わせることで、インテリジェントなコンテンツ分析と多言語アプリケーションを構築するための、拡張性が高く効果的な基盤を提供できます。</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/hugging-face-elasticsearch-inference-api</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/hugging-face-elasticsearch-inference-api</guid>
    <category><![CDATA[エージェント型AI]]></category>
    <category><![CDATA[統合]]></category>
    <dc:creator><![CDATA[Jeffrey Rengifo]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt5f961af4cb26ec97/6a170d767d8d6790c770e790/1417d6ff033712206c9bd4bcc22074ee3437ce96-1999x1125.png" length="0" type="image/png"/>
    <pubDate>Mon, 23 Mar 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[LangGraph.jsとElasticsearchを使用して金融AI検索ワークフローを構築]]></title>
    <description><![CDATA[LangGraph.jsとElasticsearchを使用して、自然言語クエリを投資や市場分析のための動的な条件付きフィルターに変換するAIを活用した金融検索ワークフローを構築する方法を学びます。]]></description>
    <content:encoded><![CDATA[<p>AI検索アプリケーションの構築では、多くの場合、複数のタスク、データ取得、データ抽出をシームレスなワークフローに調整する必要があります。LangGraphは、開発者がnodeベースの構造を使用して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を使用することで、自然言語を用いてフィルターで検索することが可能となり、ユーザーが手動で複雑なリクエストを何十ものフィルターで構築する必要がなくなります。柔軟性を高めるために、ワークフローはユーザーの入力に基づいて2つのクエリタイプを自動的に決定します。</p><ul><li><p><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>「シリーズAまたはシリーズBの資金調達額が800万ドル～2,500万ドルで、月間収益が50万ドルを超えるスタートアップを探してください。」</p></li><li><p><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>「サンフランシスコ、ニューヨーク、ボストンのフィンテックおよびヘルスケアのスタートアップ企業を探してください」</p></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><p>Elasticsearch APIキー</p></li><li><p>OpenAPI APIキー</p></li><li><p>Node 18以降</p></li></ul><h2>ステップ別のガイド</h2><p>このセクションでは、アプリがどのように見えるかを見てみましょう。<a href="https://www.typescriptlang.org/">TypeScript</a>はJavaScriptのスーパーセットで、静的な型を追加することでコードの信頼性を高め、保守性を向上させ、エラーを早期に発見して安全性を高めます。既存のJavaScriptとの完全な互換性を保ちながら、これを実現します。</p><p>ノードのフローは次のようになります。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt90db8f03f372608c/6a170986dc55de6e16e00d93/b47d7f238c4964a6febc0de7fe5e68b186f539c3-363x555.png" alt="" /><p>上記の画像はLangGraphによって生成されたもので、ノード間の実行順序と条件付きロジックを定義するワークフローを表しています。</p><ul><li><p><strong>decideStrategy：</strong>LLMを用いてユーザーのクエリを分析し、投資重視か市場重視の2つの専門的な検索戦略のどちらかを判断します。</p></li><li><p><strong>prepareInvestmentSearch：</strong>クエリからフィルター値を抽出し、財務および資金調達関連のパラメータを強調した定義済みテンプレートを構築します。</p></li><li><p><strong>prepareMarketSearch</strong> : フィルター値も抽出しますが、市場、業界、地理的コンテキストを重視したパラメータを動的に構築します。</p></li><li><p><strong>executeSearch：</strong>検索テンプレートを使用して構築されたクエリをElasticsearchに送信し、一致するスタートアップドキュメントを取得します。</p></li><li><p><strong>visualizeResults：</strong>最終結果を、資金、業界、収益などの主要なスタートアップ属性を示す明確で読みやすい要約にフォーマットします。</p></li></ul><p>このフローには「if」ステートメントとして機能する<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>const VCState = Annotation.Root({
  input: Annotation&lt;string&gt;(), // User's natural language query
  searchStrategy: Annotation&lt;string&gt;(), // Search strategy chosen by LLM
  searchParams: Annotation&lt;any&gt;(), // Prepared search parameters
  results: Annotation&lt;any[]&gt;(), // Search results
  final: Annotation&lt;string&gt;(), // Final formatted response
});<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>npm init -y<p>これで、このプロジェクトに必要な依存関係をインストールできます。</p>npm install @elastic/elasticsearch @langchain/langgraph @langchain/openai @langchain/core dotenv zod &amp;&amp; npm install --save-dev @types/node tsx typescript<ul><li><p><strong><code>@elastic/elasticsearch</code></strong>: Elasticsearchのデータインジェストや検索などのリクエストを処理するのに役立ちます。</p></li><li><p><strong><code>@langchain/langgraph</code></strong>: すべてのLangGraphツールを提供するためのJS依存関係。</p></li><li><p><strong><code>@langchain/openai</code></strong>: LangChain用のOpenAI LLMクライアント。</p></li><li><p>@langchain/core：プロンプトテンプレートなど、LangChainアプリのコアとなる基本的な構成要素を提供します。</p></li><li><p><strong><code>dotenv</code></strong>:JavaScriptで環境変数を使用するために必要な依存関係。</p></li><li><p><strong><code>zod</code></strong>：型データへの依存関係。</p></li></ul><p><code>@types/node</code> <code>tsx</code> <code>typescript</code> により、TypeScriptコードを記述して実行できるようになります。</p><p>次に、以下のファイルを作成します。</p><ul><li><p><code>elasticsearchSetup</code><a href="http://ingest.ts/"><code>.ts</code></a>: Elasticsearchのマッピングを作成し、JSONファイルからデータを取り込み、Elasticsearchにデータを取り込みます。</p></li><li><p><a href="http://main.ts/"><code>main.ts</code></a>: LangGraphアプリケーションが含まれます。</p></li><li><p><code>.env</code>：環境変数を格納するファイル</p></li></ul><p><code>.env</code>ファイルに以下の環境変数を追加します。</p>ELASTICSEARCH_ENDPOINT="your-endpoint-here"
ELASTICSEARCH_API_KEY="your-key-here"
OPENAI_API_KEY="your-key-here"<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>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" });<p>前述のように、LLMクライアントは、ユーザーの質問に基づいてElasticsearch検索テンプレートパラメーターを生成するために使用されます。</p>async function saveGraphImage(app: any): Promise&lt;void&gt; {
  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);
  }
}<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>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 {};
  }
}<p>上記の集約クエリを用いると、以下の結果が得られます。</p>{
  "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
  }
}<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>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
            }
          }
        }`,
      },
    });<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>ファイルに詳細が記載されているクエリを確認します。次のノードでは、2つのクエリのどちらを使用するかが決定されます。</p>// 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",
    };
  }
}<h3>prepareInvestmentSearchノードとprepareMarketSearchノード</h3><p>どちらのノードも共有ヘルパー関数<code>extractFilterValues</code>を使用します。この関数はLLMを活用して、業界、場所、資金調達段階、ビジネスモデルなど、ユーザーの入力に記載されている関連フィルターを識別します。このスキーマを使用して<a href="https://www.elastic.co/docs/solutions/search/search-templates">検索テンプレート</a>を構築します。</p>// 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);
}<p>検出された意図に応じて、ワークフローは2つのパスのいずれかを選択します。</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>// 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: {},
    };
  }
}<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>// 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 {};
  }
}<h3>executeSearchノード</h3><p>このノードは、生成された検索パラメータを状態から取得し、最初にElasticsearchに送信します。次に、<a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-render-search-template">_render API</a>を使用してデバッグの目的でクエリを視覚化し、次に結果を取得するためのリクエストを送信します。</p>// 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) =&gt; hit._source),
    };
  } catch (error: any) {
    console.error(`❌ ${state.searchParams.search_type} search error:`, error);
    return { results: [] };
  }
}<h3>visualizeResultsノード</h3><p>最後に、このnodeはElasticsearchの結果を表示します。</p>// 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) =&gt; {
    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,
  };
}<p>プログラム的には、グラフ全体は次のようになります。</p>  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) =&gt; state.searchStrategy, // Conditional function
      {
        investment_focused: "prepareInvestment", // If investment focused -&gt; RRF template preparation
        market_focused: "prepareMarket", // If market focused -&gt; dynamic query preparation
      }
    )
    .addEdge("prepareInvestment", "executeSearch") // Investment prep -&gt; execute
    .addEdge("prepareMarket", "executeSearch") // Market prep -&gt; execute
    .addEdge("executeSearch", "visualizeResults") // Execute -&gt; visualize
    .addEdge("visualizeResults", END); // End workflow<p>ご覧のとおり、アプリが次にどの「パス」またはノードを実行するかを決定する条件付きエッジがあります。この特徴は、ワークフローに分岐ロジックが必要な場合、例えば複数のツールから選択する場合や、人間が関与するステップを含む場合に有用です。</p><p>LangGraph のコア機能を理解したら、コードが実行されるアプリケーションをセットアップできます。</p><p>すべてを<code>main</code>メソッドで組み合わせ、ここではすべての要素をワークフロー変数下のグラフとして宣言します。</p>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) =&gt; state.searchStrategy, // Conditional function
      {
        investment_focused: "prepareInvestment", // If investment focused -&gt; RRF template preparation
        market_focused: "prepareMarket", // If market focused -&gt; dynamic query preparation
      }
    )
    .addEdge("prepareInvestment", "executeSearch") // Investment prep -&gt; execute
    .addEdge("prepareMarket", "executeSearch") // Market prep -&gt; execute
    .addEdge("executeSearch", "visualizeResults") // Execute -&gt; 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);
}<p>クエリ変数は、仮想の検索バーに入力されたユーザー入力をシミュレートします。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltba7189d5f4e63403/6a1709880e2e49cc3041a076/e8d76909eb2bc1bb62f3ca9a8b3e4b85fcec2893-1600x164.png" alt="" /><p>「シリーズAまたはシリーズBの資金調達額が800万ドル～2,500万ドルで、月間収益が50万ドルを超えるスタートアップを探してください。」という自然言語フレーズから、すべてのフィルターが抽出されます。</p><p>最後にmainメソッドを呼び出します。</p>main().catch(console.error);<h3>成果</h3>🔍 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.<p>送信された入力に対して、アプリケーションは<strong>投資に重点を置いた</strong>パスを選択し、その結果、ユーザー入力から値と範囲を抽出するLangGraphワークフローによって生成されたElasticsearchクエリを確認できます。また、抽出された値が適用された状態でElasticsearchに送信されたクエリと、最後に<code>visualizeResults</code>ノードによって結果がフォーマットされた結果も確認できます。</p><p>次に、<strong>市場重視</strong>のノードを、クエリ「サンフランシスコ、ニューヨーク、ボストンのフィンテックおよびヘルスケアのスタートアップ企業を探してください」を使用してテストしてみましょう。</p>...

🔍 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.<h2>学び</h2><p>執筆の過程で次のことを学びました。</p><ul><li><p>LLMにフィルターの正確な値を表示する必要があります。そうしないと、ユーザーが正確な値を入力することになります。カーディナリティが低い場合はこのアプローチで問題ありませんが、カーディナリティが高い場合は結果をフィルタリングする何らかのメカニズムが必要です。</p></li><li><p>検索テンプレートを使用すると、LLMにElasticsearchクエリを記述させるよりも結果の一貫性が大幅に向上し、速度も速くなります。</p></li><li><p>条件付きエッジは、複数のバリアントと分岐パスを持つアプリケーションを構築するための強力なメカニズムです。</p></li><li><p>構造化された出力は、予測可能でタイプセーフな応答を強制するため、LLMを使用して情報を生成する場合に非常に役立ちます。これにより、信頼性が向上し、プロンプトの誤解が減少します。</p></li></ul><p>ハイブリッド検索を通じてセマンティック検索と構造化検索を組み合わせることで、精度とコンテキスト理解のバランスを保ちながら、より適切で関連性の高い結果が生成されます。</p><h2>まとめ</h2><p>この例では、LangGraph.jsとElasticsearchを組み合わせて、自然言語クエリをElasticsearchで検索し、金融と市場のいずれかワークフローを焦点を当てた検索戦略をワークフローで決定できる動的なワークフローを作成します。このアプローチにより、手動クエリ作成の複雑さが軽減され、ベンチャーキャピタルアナリストの柔軟性と精度が向上します。</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/ai-agent-workflow-finance-langgraph-elasticsearch</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/ai-agent-workflow-finance-langgraph-elasticsearch</guid>
    <category><![CDATA[AI]]></category>
    <category><![CDATA[エージェント型AI]]></category>
    <dc:creator><![CDATA[Jeffrey Rengifo]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt013eba5d152f11f3/6a1709892b835f6784f4b1a6/12b6057d84c6356267cd178a3c6c1a5c61123ece-2000x1256.png" length="0" type="image/png"/>
    <pubDate>Fri, 05 Dec 2025 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[AI搭載ダッシュボード：ビジョンからKibanaへ]]></title>
    <description><![CDATA[LLM を使用してイメージを処理し、Kibana ダッシュボードに変換してダッシュボードを生成します。
]]></description>
    <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><p><a href="https://www.elastic.co/search-labs/blog/ai-powered-dashboards#background-&amp;-application-workflow">背景とアプリケーションのワークフロー</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/ai-powered-dashboards#prepare-data">データを準備する</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/ai-powered-dashboards#llm-configuration">LLM構成</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/ai-powered-dashboards#application-functions">アプリケーション機能</a></p></li></ol><h2>背景とアプリケーションのワークフロー</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><p>ジェミニ 2.5 プロ</p></li><li><p>GPT o3 / o4-ミニハイ / 4.1</p></li><li><p>クロード 4つのソネット</p></li><li><p>グロク3</p></li><li><p>ディープシーク（ディープシンク R1）</p></li></ul><p>プロンプトについては、次のように単純なものから始めました。</p>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<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://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt9ea005966a783057/6a1707d266c4f90e4ef8bf88/2b599443b5613c9f0fc3235581614add5b4b3900-891x98.png" alt="" /><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt5e5632d6d95b998c/6a1707d3a6c2b9441de79661/d87ccfc033bc00ee8188c5cae18043fbca22784c-741x233.png" alt="" /><p>これは、生成された JSON が無効であるか、形式が間違っていることを意味します。最も一般的な問題は、LLM が不完全な NDJSON を生成したり、パラメータを幻覚させたり、あるいは、どれだけ強制しようとしても NDJSON ではなく通常の JSON を返したりすることでした。</p><p><a href="https://www.elastic.co/search-labs/blog/llm-functions-elasticsearch-intelligent-query">この記事</a>（<a href="https://www.elastic.co/docs/solutions/search/search-templates">検索テンプレートが</a>LLM フリースタイルよりもうまく機能した）に触発され、完全な NDJSON ファイルを生成するように要求するのではなく、テンプレートを LLM に提供し、コード内で LLM によって提供されたパラメータを使用して適切な視覚化を作成することにしました。このアプローチは期待を裏切らず、予測可能で拡張可能です。LLM ではなくコードが重い処理を実行するようになったためです。</p><p>アプリケーションのワークフローは次のようになります。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt9f7738a4c7ddd0cd/6a1707d52b835f0a25f4b166/52c587cf0cf3517fdd4ee7ab95581dd4f2bce030-725x668.png" alt="" /><p></p><p><em>簡潔にするために一部のコードは省略しますが、完全なアプリケーションの動作コードは</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"><em><strong>この</strong></em></a><em>ノートブックに記載されています。</em></p><h2>要件</h2><p>開発を始める前に、次のものが必要です。</p><ol><li><p>Python 3.8以上</p></li><li><p><a href="https://docs.python.org/3/library/venv.html">Venv</a> Python環境</p></li><li><p>実行中のElasticsearchインスタンス、そのエンドポイント、APIキー</p></li><li><p>環境変数名 OPENAI_API_KEY に保存された OpenAI API キー:</p></li></ol>export OPENAI_API_KEY="your-openai-api-key"<h2>データを準備する</h2><p>データについては、シンプルさを保ち、Elastic のサンプル Web ログを使用します。<a href="https://www.elastic.co/docs/manage-data/ingest/sample-data#add-sample-data-sets">ここで、</a>そのデータをクラスターにインポートする方法を学習できます。</p><p>各ドキュメントには、アプリケーションにリクエストを発行したホストの詳細と、リクエスト自体とその応答ステータスに関する情報が含まれています。以下にサンプル文書を示します。</p>{
    "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
}<p>ここで、先ほどロードしたインデックス<code>kibana_sample_data_logs</code>のマッピングを取得しましょう。</p>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"]<p>後で読み込むイメージと一緒にマッピングを渡します。</p><h2>LLM構成</h2><p><a href="https://python.langchain.com/docs/concepts/structured_outputs/">構造化出力</a>を使用して画像を入力し、JSON オブジェクトを生成するために関数に渡す必要がある情報を含む JSON を受け取るように LLM を構成しましょう。</p><p>依存関係をインストールします。</p>pip install elasticsearch pydantic langchain langchain-openai -q<p>Elasticsearch は<a href="https://www.elastic.co/docs/manage-data/data-store/mapping">インデックス マッピングの</a>取得に役立ちます。Pydantic を使用すると、Python でスキーマを定義して LLM に従うように要求することができ、 <a href="https://www.elastic.co/search-labs/integrations/langchain">LangChain は</a>LLM と AI ツールの呼び出しを容易にするフレームワークです。</p><p>LLM から必要な出力を定義するために、Pydantic スキーマを作成します。画像からわかる必要があるのは、グラフの種類、フィールド、視覚化タイトル、ダッシュボード タイトルです。</p>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]<p>画像入力には、先ほど描いたダッシュボードを送信します。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt7870f6421986d11d/6a1707d78b73cb3408189fa3/36441d7b5dc1f3ff2ac2a30710208d57ad41c716-1600x898.jpg" alt="" /><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>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)}")<p>LLM にはすでに Kibana ダッシュボードに関するコンテキストがあるため、プロンプトですべてを説明する必要はなく、Elasticsearch と Kibana で動作していることを忘れないようにするための詳細のみを説明します。</p><p>プロンプトを分解してみましょう:</p><p>セクション</p><p>理由</p><p>あなたは、Kibana バージョン 9.0.0 の画像から Kibana ダッシュボードを分析するエキスパートです。</p><p>これを Elasticsearch と Elasticsearch バージョンで強化することで、LLM が古い/無効なパラメータを幻覚する可能性を減らします。</p><p>ダッシュボード イメージと Elasticsearch インデックス マッピングが提供されます。</p><p>LLM による誤った解釈を避けるために、この画像はダッシュボードに関するものであることを説明します。</p><p>以下は、ダッシュボードのベースとなるインデックスのインデックス マッピングです。これを使用すると、使用可能なデータとフィールドを理解するのに役立ちます。インデックス マッピング: {index_mappings}</p><p>LLM が有効なフィールドを動的に選択できるようにマッピングを提供することが重要です。そうしないと、ここでのマッピングをハードコードすることになり、厳しすぎることになります。あるいは、正しいフィールド名を含むイメージに依存することになりますが、これは信頼できません。</p><p>画像に表示されている内容に基づいて、各視覚化に関連するフィールドのみを含めます。</p><p>画像に関係のないフィールドを追加しようとすることがあるため、この強化を追加する必要がありました。</p><p>これにより、表示する視覚化の配列を含むオブジェクトが返されます。</p>"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"")"
]<h2>LLM応答の処理</h2><p>私たちはサンプルの 2x2 パネル ダッシュボードを作成し、 <a href="https://www.elastic.co/docs/api/doc/kibana/operation/operation-get-dashboards-dashboard">Get a 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>} に変更したことに注意してください。
</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc55d69d84a08e668/6a1707d8a2929903acd00fb8/ec7e1ac0cd8b470df13e60940162b56778acb386-315x234.png" alt="" /><p>LLM が提供した情報を使用して、どのテンプレートを使用し、どの値を置き換えるかを決定できます。</p><p><code>fill_template_with_analysis</code> 視覚化の JSON テンプレート、タイトル、フィールド、グリッド上の視覚化の座標など、単一のパネルのパラメータを受け取ります。</p><p>次に、テンプレートの値を置き換えて、最終的な JSON 視覚化を返します。</p>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)<p>簡単にするために、LLM が作成することを決定したパネルに割り当てる静的座標があり、上の画像のように 2x2 グリッド ダッシュボードが生成されます。</p># 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<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 にプッシュし、ダッシュボードを生成します。
</p>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)}")<p>スクリプトを実行してダッシュボードを生成するには、コンソールで次のコマンドを実行します。</p>python &lt;file_name&gt;.py<p>最終結果は次のようになります。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt5ceffed004153a4f/6a1707d9a929cf9147ae0901/e909afbf0e47d9a6e0f7bd07dfb2efcfa5cf06ac-921x715.png" alt="" /><h2>まとめ</h2><p>LLM は、テキストをコード化したり、画像をコード化したりするときに、強力な視覚機能を発揮します。ダッシュボード API を使用すると、JSON ファイルをダッシュボードに変換することも可能で、LLM といくつかのコードを使用して、画像を Kibana ダッシュボードに変換することもできます。</p><p>次のステップは、さまざまなグリッド設定、ダッシュボードのサイズ、位置を使用して、ダッシュボードのビジュアルの柔軟性を向上させることです。また、より複雑な視覚化と視覚化タイプのサポートを提供することも、このアプリケーションにとって便利な追加機能となるでしょう。</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/ai-powered-dashboards</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/ai-powered-dashboards</guid>
    <category><![CDATA[Kibana]]></category>
    <category><![CDATA[AI]]></category>
    <dc:creator><![CDATA[Jeffrey Rengifo,Tomás Murúa]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt41727cbee6155a68/6a1707dbb0367dd2fd72bc86/eb60ceb2fbc3941745b21ae3357cbb6ea8fab18c-1443x811.png" length="0" type="image/png"/>
    <pubDate>Wed, 16 Jul 2025 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[JavaScript で Elasticsearch を正しく使う方法、パート II]]></title>
    <description><![CDATA[本番環境でのベストプラクティスと、コーディングエラーを減らすためにServerless環境でElasticsearch Node.jsクライアントを実行する方法についてご覧ください。 ]]></description>
    <content:encoded><![CDATA[<p>これは、JavaScript での Elasticsearch シリーズの第 2 部です。<a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i">最初の部分では、</a>環境を正しくセットアップし、Node.js クライアントを構成し、データをインデックスして検索する方法を学びました。この第 2 部では、実稼働のベスト プラクティスを実装し、サーバーレス環境で Elasticsearch <a href="http://node.js">Node.js</a>クライアントを実行する方法を学習します。</p><p>以下を確認します:</p><ul><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-ii#production-best-practices">制作のベストプラクティス</a></p><ul><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-ii#error-handling">エラー処理</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-ii#testing">テスト</a></p></li></ul></li><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-ii#serverless-environments">サーバーレス環境</a></p><ul><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-ii#running-the-client-on-elastic-serverless">Elastic Serverlessでクライアントを実行する</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-ii#running-the-client-on-function-as-a-service-environment">Function-as-a-Service 環境でクライアントを実行する</a></p></li></ul></li></ul><p><a href="https://github.com/Delacrobix/JS-client-best-practices_article"><em><strong>ここで</strong></em></a><em> 例付きのソースコードを確認できます</em><em><strong> 。</strong></em></p><h2>制作のベストプラクティス</h2><h3>Elasticsearchにおけるエラー処理。</h3><p>Node.js の Elasticsearch クライアントの便利な機能は、Elasticsearch で発生する可能性のあるエラーのオブジェクトを公開し、さまざまな方法で検証して処理できることです。</p><p><a href="https://www.elastic.co/docs/reference/elasticsearch/clients/javascript/connecting#client-error-handling">すべてを表示する</a>には、次のコマンドを実行します。</p>const { errors } = require('@elastic/elasticsearch')
console.log(errors)<p>検索の例に戻り、起こりうるエラーのいくつかを処理してみましょう。</p>app.get("/search/lexic", async (req, res) =&gt; {
 ....
  } catch (error) {
    if (error instanceof errors.ResponseError) {
      let errorMessage =
        "Response error!, query malformed or server down, contact the administrator!";

      if (error.body.error.type === "parsing_exception") {
        errorMessage = "Query malformed, make sure mappings are set correctly";
      }

      res.status(error.meta.statusCode).json({
        erroStatus: error.meta.statusCode,
        success: false,
        results: null,
        error: errorMessage,
      });
    }

    res.status(500).json({
      success: false,
      results: null,
      error: error.message,
    });
  }
});<p><code>ResponseError</code> 特に、回答が<code>4xx</code>または<code>5xx</code>場合に発生します。これは、要求が正しくないか、サーバーが利用できないことを意味します。</p><p>このタイプのエラーは<strong>、テキストタイプのフィールドで用語クエリを実行するなど、間違ったクエリを生成することでテストできます。</strong></p><p>デフォルトエラー:</p> {
    "success": false,
    "results": null,
    "error": "parsing_exception\n\tRoot causes:\n\t\tparsing_exception: [terms] query does not support [visit_details]"
}<p>カスタマイズされたエラー: </p>{
    "erroStatus": 400,
    "success": false,
    "results": null,
    "error": "Response error!, query malformed or server down; contact the administrator!"
}<p>各タイプのエラーを特定の方法でキャプチャして処理することもできます。たとえば、 <code>TimeoutError</code>に再試行ロジックを追加できます。</p>app.get("/search/semantic", async (req, res) =&gt; {
    try {
  ...
  } catch (error) {
    if (error instanceof errors.TimeoutError) {


     // Retry logic...

      res.status(error.meta.statusCode).json({
        erroStatus: error.meta.statusCode,
        success: false,
        results: null,
        error:
          "The request took more than 10s after 3 retries. Try again later.",
      });
    }
  }
});<h3>テスト</h3><p>テストはアプリの安定性を保証する上で重要です。Elasticsearch から分離された方法でコードをテストするには、クラスターを作成するときにライブラリ<a href="https://github.com/elastic/elasticsearch-js-mock">elasticsearch-js-mock を</a>使用できます。</p><p>このライブラリを使用すると、実際のクライアントと非常によく似たクライアントをインスタンス化できますが、クライアントの HTTP レイヤーのみをモックのレイヤーに置き換え、残りは元のレイヤーと同じにすることで、構成に応答します。</p><p>自動テスト用に、モック ライブラリと<a href="https://github.com/avajs/ava">AVA</a>をインストールします。</p><p><code>npm install @elastic/elasticsearch-mock</code></p><p><code>npm install --save-dev ava</code></p><p>テストを実行するために<code>package.json</code>ファイルを構成します。次のようになっていることを確認してください:</p>"type": "module",
	"scripts": {
		"test": "ava"
	},
	"devDependencies": {
		"ava": "^5.0.0"
	}<p>それでは、 <code>test.js</code>ファイルを作成し、モッククライアントをインストールしましょう。</p>const { Client } = require('@elastic/elasticsearch')
const Mock = require('@elastic/elasticsearch-mock')

const mock = new Mock()
const client = new Client({
  node: 'http://localhost:9200',
  Connection: mock.getConnection()
})<p>次に、セマンティック検索のモックを追加します。</p>function createSemanticSearchMock(query, indexName) {
  mock.add(
    {
      method: "POST",
      path: `/${indexName}/_search`,
      body: {
        query: {
          semantic: {
            field: "semantic_field",
            query: query,
          },
        },
      },
    },
    () =&gt; {
      return {
        hits: {
          total: { value: 2, relation: "eq" },
          hits: [
            {
              _id: "1",
              _score: 0.9,
              _source: {
                owner_name: "Alice Johnson",
                pet_name: "Buddy",
                species: "Dog",
                breed: "Golden Retriever",
                vaccination_history: ["Rabies", "Parvovirus", "Distemper"],
                visit_details:
                  "Annual check-up and nail trimming. Healthy and active.",
              },
            },
            {
              _id: "2",
              _score: 0.7,
              _source: {
                owner_name: "Daniel Kim",
                pet_name: "Mochi",
                species: "Rabbit",
                breed: "Mixed",
                vaccination_history: [],
                visit_details:
                  "Nail trimming and general health check. No issues.",
              },
            },
          ],
        },
      };
    }
  );
}<p>これで、コードのテストを作成し、Elasticsearch 部分が常に同じ結果を返すことを確認できるようになりました。</p>import test from 'ava';

test("performSemanticSearch must return formatted results correctly", async (t) =&gt; {
  const indexName = "vet-visits";
  const query = "Which pets had nail trimming?";

  createSemanticSearchMock(query, indexName);

  async function performSemanticSearch(esClient, q, indexName = "vet-visits") {
    try {
      const result = await esClient.search({
        index: indexName,
        body: {
          query: {
            semantic: {
              field: "semantic_field",
              query: q,
            },
          },
        },
      });

      return {
        success: true,
        results: result.hits.hits,
      };
    } catch (error) {
      if (error instanceof errors.TimeoutError) {
        return {
          success: false,
          results: null,
          error: error.body.error.reason,
        };
      }

      return {
        success: false,
        results: null,
        error: error.message,
      };
    }
  }

  const result = await performSemanticSearch(esClient, query, indexName);

  t.true(result.success, "The search must be successful");
  t.true(Array.isArray(result.results), "The results must be an array");

  if (result.results.length &gt; 0) {
    t.true(
      "_source" in result.results[0],
      "Each result must have a _source property"
    );
    t.true(
      "pet_name" in result.results[0]._source,
      "Results must include the pet_name field"
    );
    t.true(
      "visit_details" in result.results[0]._source,
      "Results must include the visit_details field"
    );
  }
});<p>テストを実行してみましょう。</p><p><code>npm run test</code></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt36304e286146f362/6a170559d7c02237b2de638f/42feae845ae8eae03c37ad7ad114e8db35984812-1186x302.png" alt="" /><p>完了です！これからは、外部要因ではなくコードに100%重点を置いてアプリをテストできます。</p><h2>サーバーレス環境</h2><h3>Elastic Serverlessでクライアントを実行する方法</h3><p>クラウドまたはオンプレミスでの Elasticsearch の実行について説明しましたが、Node.js クライアントは<a href="https://www.elastic.co/guide/en/serverless/current/intro.html">Elastic Cloud Serverless</a>への接続もサポートしています。</p><p>Elastic Cloud Serverless を使用すると、Elastic がインフラストラクチャを内部で処理するためインフラストラクチャについて心配する必要がないプロジェクトを作成でき、インデックスを作成するデータとそのデータにアクセスする期間のみを考慮すれば済みます。</p><p>使用の観点から見ると、Serverless はコンピューティングとストレージを切り離し、<a href="https://www.elastic.co/search-labs/blog/elasticsearch-serverless-tier-autoscaling">検索</a>と<a href="https://www.elastic.co/search-labs/blog/elasticsearch-ingest-autoscaling">インデックス作成</a>の両方に自動スケーリング機能を提供します。これにより、実際に必要なリソースのみを増やすことができます。</p><p>クライアントは、Serverless に接続するために次の調整を行います。</p><ul><li><p>スニッフィングをオフにし、スニッフィング関連のオプションを無視します</p></li><li><p>最初のノードを除いて、config で渡されたすべてのノードを無視し、ノードのフィルタリングと選択のオプションも無視します。</p></li><li><p>圧縮と `TLSv1_2_method` を有効にします（Elastic Cloud 用に構成した場合と同じ）</p></li><li><p>すべてのリクエストに `elastic-api-version` HTTP ヘッダーを追加します</p></li><li><p>デフォルトでは `WeightedConnectionPool` ではなく `CloudConnectionPool` を使用します</p></li><li><p>標準の MIME タイプを優先して、ベンダーの `content-type` および `accept` ヘッダーをオフにします。</p></li></ul><p>サーバーレス プロジェクトを接続するには、パラメーター serverMode: serverless を使用する必要があります。</p>const { Client } = require('@elastic/elasticsearch')
const client = new Client({
  node: 'ELASTICSEARCH_ENDPOINT',
  auth: { apiKey: 'ELASTICSEARCH_API_KEY' },
  serverMode: "serverless",
});<h3>Function-as-a-Service環境でクライアントを実行する方法</h3><p>この例では Node.js サーバーを使用しましたが、AWS lambda、GCP Run などの機能を備えた Function-as-a-Service 環境を使用して接続することもできます。</p>'use strict'

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

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

exports.handler = async function (event, context) {
  // use the client
}<p>もう 1 つの例は、同じくサーバーレスである Vercel などのサービスに接続することです。これを実行する方法の<a href="https://github.com/elastic/elasticsearch-js/blob/main/docs/examples/proxy/README.md">完全な例</a>を確認できますが、<a href="https://github.com/elastic/elasticsearch-js/blob/main/docs/examples/proxy/api/search.js">検索エンドポイント</a>の最も重要な部分は次のようになります。</p>const response = await client.search(
  {
    index: INDEX,
    // You could directly send from the browser
    // the Elasticsearch's query DSL, but it will
    // expose you to the risk that a malicious user
    // could overload your cluster by crafting
    // expensive queries.
    query: {
      match: { field: req.body.text },
    },
  },
  {
    headers: {
      Authorization: `ApiKey ${token}`,
    },
  }
);<p>このエンドポイントは /api フォルダーにあり、サーバー側から実行されるため、クライアントは検索用語に対応する「テキスト」パラメータのみを制御できます。</p><p>Function-as-a-Service を使用する意味は、24 時間 365 日稼働するサーバーとは異なり、関数は関数を実行するマシンのみを起動し、関数が終了するとマシンは休止モードになり、消費するリソースが少なくなることです。</p><p>この構成は、アプリケーションがあまり多くのリクエストを受け取らない場合には便利ですが、そうでない場合はコストが高くなる可能性があります。また、<a href="https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtime-environment.html">関数のライフサイクル</a>と実行時間 (場合によっては数秒しかないこともあります) も考慮する必要があります。</p><h2>まとめ</h2><p>この記事では、実稼働環境で非常に重要なエラーの処理方法を学びました。また、Elasticsearch サービスをモックしながらアプリケーションをテストする方法についても説明しました。これにより、クラスターの状態に関係なく信頼性の高いテストが提供され、コードに集中できるようになります。</p><p>最後に、Elastic Cloud Serverless と Vercel アプリケーションの両方をプロビジョニングして、完全にサーバーレスなスタックを立ち上げる方法を示しました。</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-ii</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-ii</guid>
    <category><![CDATA[JavaScript]]></category>
    <category><![CDATA[基本]]></category>
    <dc:creator><![CDATA[Jeffrey Rengifo]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc58be329ffebcd60/6a17043e47d49c0bc62d88ab/70fb0ff949f6db9ac9b8a28ecb4329ab915ebf46-720x420.png" length="0" type="image/png"/>
    <pubDate>Mon, 19 May 2025 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[JavaScript で Elasticsearch を正しく使う方法、パート 1]]></title>
    <description><![CDATA[JavaScript で本番環境対応の Elasticsearch バックエンドを作成する方法を説明します。  

ElasticsearchをJavaScriptで使用して、クライアント/サーバーのベストプラクティスに従ってElasticsearchドキュメントにクエリを実行するために、さまざまな検索エンドポイントを持つサーバーを作成する方法をご覧ください。]]></description>
    <content:encoded><![CDATA[<p>これは、JavaScript で Elasticsearch を使用する方法を説明するシリーズの最初の記事です。このシリーズでは、JavaScript 環境で Elasticsearch を使用する方法の基本を学習し、検索アプリを作成するための最も関連性の高い機能とベスト プラクティスを確認します。最後には、JavaScript を使用して Elasticsearch を実行するために必要なすべてのことを理解できるようになります。</p><p>この最初の部分では、次の点を確認します。</p><ul><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i#environment">環境</a></p><ul><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i#frontend,-backend,-or-serverless?">フロントエンド、バックエンド、それともサーバーレス？</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i#connecting-the-client">クライアントの接続</a></p></li></ul></li><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i#indexing-documents">文書のインデックス作成</a></p><ul><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i#elasticsearch-client">Elasticsearchクライアント</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i#semantic-mappings">セマンティックマッピング</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i#bulk-helper">バルクヘルパー</a></p></li></ul></li><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i#searching-data">データの検索</a></p><ul><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i#lexical-query-(/search/lexic?q=%3Cquery-term%3E)">語彙クエリ</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i#semantic-query-(/search/semantic?q=%3Cquery-term%3E)">セマンティッククエリ</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i#hybrid-query-(/search/hybrid?q=%3Cquery-term%3E)">ハイブリッドクエリ</a></p></li></ul></li></ul><p><a href="https://github.com/Delacrobix/JS-client-best-practices_article"><em><strong>ここで</strong></em></a><em> 例付きのソースコードを確認できます</em><em><strong> 。</strong></em></p><h3>Elasticsearch Node.js クライアントとは何ですか?</h3><p><a href="https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/index.html">Elasticsearch Node.js クライアント</a>は、Elasticsearch API からの HTTP REST 呼び出しを JavaScript に配置する JavaScript ライブラリです。これにより、処理が容易になり、ドキュメントのインデックス作成などのタスクを一括で簡素化するヘルパーが利用できるようになります。</p><h2>環境</h2><h3>フロントエンド、バックエンド、それともサーバーレス？</h3><p>JavaScript クライアントを使用して検索アプリを作成するには、Elasticsearch クラスターとクライアントを実行する JavaScript ランタイムという少なくとも 2 つのコンポーネントが必要です。</p><p>JavaScript クライアントはすべての Elasticsearch ソリューション (クラウド、オンプレミス、サーバーレス) をサポートしており、クライアントがすべてのバリエーションを内部で処理するため、ソリューション間に大きな違いはありません。そのため、どれを使用するかについて心配する必要はありません。</p><p>ただし、JavaScript ランタイムは<strong>ブラウザ</strong>から<strong>直接ではなく、サーバーから実行する必要があります。</strong></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltd3ec469c83e3a71a/6a17e3d5445de91da44d00b6/92ce6cfd923c8008fa44f617a58193642d9d5879-661x410.png" alt="JavaScript環境でのElasticsearch。" /><p>これは、ブラウザから Elasticsearch を呼び出すと、ユーザーがクラスター API キー、ホスト、クエリ自体などの機密情報を取得する可能性があるためです。Elasticsearch では<strong>、クラスターをインターネットに直接公開せず</strong>、このすべての情報を抽象化する中間層を使用して、ユーザーがパラメータのみを確認できるようにすることを推奨しています。このトピックの詳細については、<a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/es-security-principles.html#security-protect-cluster-traffic">ここ を</a>ご覧ください。</p><p>次のようなスキーマを使用することをお勧めします。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt4d7f215f2e70230a/6a17e3d6fbc5f83de6491a13/a08769f08ec73fe57bf2e961cfdfbb1cdd57919d-972x429.png" alt="Elasticsearch Node.jsクライアントをセットアップする。" /><p>この場合、クライアントは検索用語とサーバーの認証キーのみを送信し、サーバーはクエリと Elasticsearch との通信を完全に制御します。</p><h3>クライアントの接続</h3><p>まず、<a href="https://www.elastic.co/search-labs/tutorials/install-elasticsearch/elastic-cloud">次の手順</a>に従って API キーを作成します。</p><p>前の例に従って、シンプルな Express サーバーを作成し、Node.JS サーバーからのクライアントを使用してそのサーバーに接続します。</p><p>NPM を使用してプロジェクトを初期化し、Elasticsearch クライアントと<a href="https://expressjs.com/">Express をインストールします。</a>後者は、Node.js でサーバーを起動するためのライブラリです。Express を使用すると、HTTP 経由でバックエンドと対話できます。</p><p>プロジェクトを初期化しましょう:</p><p><code>npm init -y</code></p><p>依存関係をインストールします:</p><p><code>npm install @elastic/elasticsearch express split2 dotenv</code></p><p>詳しく説明しましょう:</p><ul><li><p><a href="https://www.npmjs.com/package/@elastic/elasticsearch"><em><strong>@elastic/elasticsearch</strong></em></a> : 公式Node.jsクライアントです</p></li><li><p><a href="https://www.npmjs.com/package/express"><em><strong>express</strong></em></a> : 軽量なNode.jsサーバーを立ち上げてElasticsearchを公開できるようになります</p></li><li><p><a href="https://www.npmjs.com/package/split2"><em><strong>split2</strong></em></a> : テキスト行をストリームに分割します。ndjsonファイルを1行ずつ処理するのに便利です</p></li><li><p><a href="https://www.npmjs.com/package/dotenv"><em><strong>dotenv</strong></em></a> : .env を使用して環境変数を管理できるようにしますファイル</p></li></ul><p>.envを作成するプロジェクトのルートにあるファイルを作成し、次の行を追加します。</p>ELASTICSEARCH_ENDPOINT="Your Elasticsearch endpoint"
ELASTICSEARCH_API_KEY="Your Elasticssearch API"<p>この方法では、 <code>dotenv</code>パッケージを使用してこれらの変数をインポートできます。</p><p><code>server.js</code>ファイルを作成します:</p>const express = require("express");
const bodyParser = require("body-parser");
const { Client } = require("@elastic/elasticsearch");
 
require("dotenv").config(); //environment variables setup

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


const app = express();

app.listen(PORT, () =&gt; {
  console.log("Server running on port", PORT);
});
app.use(bodyParser.json());


let esClient = new Client({
  node: ELASTICSEARCH_ENDPOINT,
  auth: { apiKey: ELASTICSEARCH_API_KEY },  
});

app.get("/ping", async (req, res) =&gt; {
  try {
    const result = await esClient.info();

    res.status(200).json({
      success: true,
      clusterInfo: result,
    });
  } catch (error) {
    console.error("Error getting Elasticsearch info:", error);

    res.status(500).json({
      success: false,
      clusterInfo: null,
      error: error.message,
    });
  }
});<p>このコードは、ポート 3000 をリッスンし、認証用の API キーを使用して Elasticsearch クラスターに接続する基本的な Express.js サーバーをセットアップします。これには、GET リクエストを介してアクセスすると、Elasticsearch クライアントの<code>.info()</code>メソッドを使用して Elasticsearch クラスターに基本情報を照会する /ping エンドポイントが含まれています。</p><p>クエリが成功した場合は、クラスター情報が JSON 形式で返され、それ以外の場合はエラー メッセージが返されます。サーバーは、JSON リクエスト本体を処理するために body-parser ミドルウェアも使用します。</p><p>ファイルを実行してサーバーを起動します。</p><p><code>node server.js</code></p><p>答えは次のようになるはずです:</p>Server running on port 3000<p>それでは、エンドポイント<code>/ping</code>を参照して、Elasticsearch クラスターのステータスを確認しましょう。</p>curl http://localhost:3000/ping
{
    "success": true,
    "clusterInfo": {
        "name": "instance-0000000000",
        "cluster_name": "61b7e19eec204d59855f5e019acd2689",
        "cluster_uuid": "BIfvfLM0RJWRK_bDCY5ldg",
        "version": {
            "number": "9.0.0",
            "build_flavor": "default",
            "build_type": "docker",
            "build_hash": "112859b85d50de2a7e63f73c8fc70b99eea24291",
            "build_date": "2025-04-08T15:13:46.049795831Z",
            "build_snapshot": false,
            "lucene_version": "10.1.0",
            "minimum_wire_compatibility_version": "8.18.0",
            "minimum_index_compatibility_version": "8.0.0"
        },
        "tagline": "You Know, for Search"
    }
}<h2>文書のインデックス作成</h2><p>接続すると、セマンティック検索用の<a href="https://www.elastic.co/search-labs/blog/semantic-search-simplified-semantic-text">semantic_text</a>やフルテキストクエリ用の text などのマッピングを使用してドキュメントのインデックスを作成できます。これら 2 つのフィールド タイプを使用すると、<a href="https://www.elastic.co/what-is/hybrid-search">ハイブリッド検索</a>も実行できます。</p><p>マッピングを生成し、ドキュメントをアップロードするために、新しい<code>load.js</code>ファイルを作成します。</p><h3>Elasticsearchクライアント</h3><p>まずクライアントをインスタンス化して認証する必要があります。</p>const { Client } = require("@elastic/elasticsearch");

const ELASTICSEARCH_ENDPOINT = "cluster/project_endpoint";
const ELASTICSEARCH_API_KEY = "apiKey";

const esClient = new Client({
  node: ELASTICSEARCH_ENDPOINT,
  auth: { apiKey: ELASTICSEARCH_API_KEY },
});<h3>セマンティックマッピング</h3><p>動物病院に関するデータを含むインデックスを作成します。飼い主様、ペット様、訪問の詳細に関する情報を保存します。</p><p>名前や説明など、全文検索を実行するデータはテキストとして保存されます。動物の種や品種などのカテゴリのデータは、キーワードとして保存されます。</p><p>さらに、すべてのフィールドの値を semantic_text フィールドにコピーして、その情報に対してもセマンティック検索を実行できるようにします。</p>const INDEX_NAME = "vet-visits";

const createMappings = async (indexName, mapping) =&gt; {
  try {
    const body = await esClient.indices.create({
      index: indexName,
      body: {
        mappings: mapping,
      },
    });

    console.log("Index created successfully:", body);
  } catch (error) {
    console.error("Error creating mapping:", error);
  }
};

await createMappings(INDEX_NAME, {
  properties: {
    owner_name: {
      type: "text",
      copy_to: "semantic_field",
    },
    pet_name: {
      type: "text",
      copy_to: "semantic_field",
    },
    species: {
      type: "keyword",
      copy_to: "semantic_field",
    },
    breed: {
      type: "keyword",
      copy_to: "semantic_field",
    },
    vaccination_history: {
      type: "keyword",
      copy_to: "semantic_field",
    },
    visit_details: {
      type: "text",
      copy_to: "semantic_field",
    },
    semantic_field: {
      type: "semantic_text",
    },
  },
});<h3>バルクヘルパー</h3><p>クライアントのもう 1 つの利点は、<a href="https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/client-helpers.html#bulk-helper">一括ヘルパーを</a>使用してインデックスを一括で作成できることです。バルク ヘルパーを使用すると、同時実行、再試行、関数を通過して成功または失敗した各ドキュメントの処理などを簡単に処理できます。</p><p>このヘルパーの魅力的な機能は、ストリームを操作できることです。この機能を使用すると、ファイル全体をメモリに保存して Elasticsearch に一度に送信するのではなく、ファイルを 1 行ずつ送信できます。</p><p>Elasticsearch にデータをアップロードするには、プロジェクトのルートに data.ndjson というファイルを作成し、以下の情報を追加します (または、<a href="https://github.com/Delacrobix/JS-client-best-practices_article/blob/main/data.ndjson">ここ</a>からデータセットを含むファイルをダウンロードすることもできます)。</p>{"owner_name":"Alice Johnson","pet_name":"Buddy","species":"Dog","breed":"Golden Retriever","vaccination_history":["Rabies","Parvovirus","Distemper"],"visit_details":"Annual check-up and nail trimming. Healthy and active."}
{"owner_name":"Marco Rivera","pet_name":"Milo","species":"Cat","breed":"Siamese","vaccination_history":["Rabies","Feline Leukemia"],"visit_details":"Slight eye irritation, prescribed eye drops."}
{"owner_name":"Sandra Lee","pet_name":"Pickles","species":"Guinea Pig","breed":"Mixed","vaccination_history":[],"visit_details":"Loss of appetite, recommended dietary changes."}
{"owner_name":"Jake Thompson","pet_name":"Luna","species":"Dog","breed":"Labrador Mix","vaccination_history":["Rabies","Bordetella"],"visit_details":"Mild ear infection, cleaning and antibiotics given."}
{"owner_name":"Emily Chen","pet_name":"Ziggy","species":"Cat","breed":"Mixed","vaccination_history":["Rabies","Feline Calicivirus"],"visit_details":"Vaccination update and routine physical."}
{"owner_name":"Tomás Herrera","pet_name":"Rex","species":"Dog","breed":"German Shepherd","vaccination_history":["Rabies","Parvovirus","Leptospirosis"],"visit_details":"Follow-up for previous leg strain, improving well."}
{"owner_name":"Nina Park","pet_name":"Coco","species":"Ferret","breed":"Mixed","vaccination_history":["Rabies"],"visit_details":"Slight weight loss; advised new diet."}
{"owner_name":"Leo Martínez","pet_name":"Simba","species":"Cat","breed":"Maine Coon","vaccination_history":["Rabies","Feline Panleukopenia"],"visit_details":"Dental cleaning. Minor tartar buildup removed."}
{"owner_name":"Rachel Green","pet_name":"Rocky","species":"Dog","breed":"Bulldog Mix","vaccination_history":["Rabies","Parvovirus"],"visit_details":"Skin rash, antihistamines prescribed."}
{"owner_name":"Daniel Kim","pet_name":"Mochi","species":"Rabbit","breed":"Mixed","vaccination_history":[],"visit_details":"Nail trimming and general health check. No issues."}<p>バルク ヘルパーがファイル行を Elasticsearch に送信する間、split2 を使用してファイル行をストリーミングします。</p>const { createReadStream } = require("fs");
const split = require("split2");
 
const indexData = async (filePath, indexName) =&gt; {
  try {
    console.log(`Indexing data from ${filePath} into ${indexName}...`);

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

      onDocument: () =&gt; {
        return {
          index: { _index: indexName },
        };
      },
      onDrop(doc) {
        console.error("Error processing document:", doc);
      },
    });

    console.log("Bulk indexing successful elements:", result.items.length);
  } catch (error) {
    console.error("Error indexing data:", error);
    throw error;
  }
};

await indexData("./data.ndjson", INDEX_NAME);<p>上記のコードは.ndjsonを読み取りますファイルを 1 行ずつ読み込み、 <code>helpers.bulk</code>メソッドを使用して各 JSON オブジェクトを指定された Elasticsearch インデックスに一括インデックスします。<code>createReadStream</code>と<code>split2</code>を使用してファイルをストリーミングし、各ドキュメントのインデックス メタデータを設定し、処理に失敗したドキュメントをログに記録します。完了すると、正常にインデックスが作成されたアイテムの数を記録します。</p><p><code>indexData</code>関数を使用する代わりに、Kibana を使用して UI 経由でファイルを直接アップロードし、<a href="https://www.elastic.co/docs/manage-data/ingest/upload-data-files">データ ファイルのアップロード UI を使用することもできます。</a></p><p>ファイルを実行して、ドキュメントを Elasticsearch クラスターにアップロードします。</p><p><code>node load.js</code></p>Creating mappings for index vet-visits...
Index created successfully: { acknowledged: true, shards_acknowledged: true, index: 'vet-visits' }
Indexing data from ./data.ndjson into vet-visits...
Bulk indexing completed. Total documents: 10, Failed: 0<h2>Elasticsearchでのデータ検索</h2><p><code>server.js</code>ファイルに戻って、語彙検索、セマンティック検索、ハイブリッド検索を実行するためのさまざまなエンドポイントを作成します。</p><p>簡単に言えば、これらのタイプの検索は相互に排他的ではありませんが、回答する必要がある質問の種類によって異なります。</p><p>クエリタイプ</p><p>使用事例</p><p>例題</p><p>語彙クエリ</p><p>質問内の単語または語根は、索引文書に表示される可能性があります。質問とドキュメント間のトークンの類似性。</p><p>青いスポーツTシャツを探しています。</p><p>セマンティッククエリ</p><p>質問内の単語は文書には表示されない可能性があります。質問とドキュメント間の概念的な類似性。</p><p>寒い季節用の服を探しています。</p><p>ハイブリッド検索</p><p>質問には語彙や意味の要素が含まれています。質問とドキュメント間のトークンと意味の類似性。</p><p>ビーチでの結婚式用にSサイズのドレスを探しています。</p><p>質問の<em><strong>語彙</strong></em>部分はタイトルや説明、またはカテゴリ名の一部である可能性が高く、<em><strong>意味</strong></em>部分はそれらの分野に関連する概念です。<em><strong>青は</strong></em>おそらくカテゴリ名または説明の一部であり、<em><strong>ビーチウェディングは</strong></em>そうではないかもしれませんが、意味的にはリネンの衣服に関連している可能性があります。</p><h3>語彙クエリ (/search/lexic?q=&lt;query_term&gt;)</h3><p>語彙検索 (フルテキスト検索とも呼ばれる) とは、トークンの類似性に基づいて検索することを意味します。つまり、分析後、検索内のトークンを含むドキュメントが返されます。</p><p>語彙検索の実践チュートリアルは、<a href="https://www.elastic.co/demo-gallery/lexical-search">こちらで</a>ご覧いただけます。</p>app.get("/search/lexic", async (req, res) =&gt; {
  const { q } = req.query;

  const INDEX_NAME = "vet-visits";

  try {
    const result = await esClient.search({
      index: INDEX_NAME,
      size: 5,
      body: {
        query: {
          multi_match: {
            query: q,
            fields: ["owner_name", "pet_name", "visit_details"],
          },
        },
      },
    });

    res.status(200).json({
      success: true,
      results: result.hits.hits
    });
  } catch (error) {
    console.error("Error performing search:", error);

    res.status(500).json({
      success: false,
      results: null,
      error: error.message,
    });
  }
});<p><em><strong>爪切り</strong></em>でテストする</p>curl http://localhost:3000/search/lexic?q=nail%20trimming<p>答え：</p>{
    "success": true,
    "results": [
        {
            "_index": "vet-visits",
            "_id": "-RY6RJYBLe2GoFQ6-9n9",
            "_score": 2.7075968,
            "_source": {
                "pet_name": "Mochi",
                "owner_name": "Daniel Kim",
                "species": "Rabbit",
                "visit_details": "Nail trimming and general health check. No issues.",
                "breed": "Mixed",
                "vaccination_history": []
            }
        },
        {
            "_index": "vet-visits",
            "_id": "8BY6RJYBLe2GoFQ6-9n9",
            "_score": 2.560356,
            "_source": {
                "pet_name": "Buddy",
                "owner_name": "Alice Johnson",
                "species": "Dog",
                "visit_details": "Annual check-up and nail trimming. Healthy and active.",
                "breed": "Golden Retriever",
                "vaccination_history": [
                    "Rabies",
                    "Parvovirus",
                    "Distemper"
                ]
            }
        }
    ]
}<h3>セマンティッククエリ (/search/semantic?q=&lt;query_term&gt;)</h3><p>セマンティック検索は、語彙検索とは異なり、ベクトル検索を通じて検索用語の意味に類似した結果を見つけます。</p><p>セマンティック検索の実践チュートリアルは、<a href="https://www.elastic.co/demo-gallery/semantic-search">こちらで</a>ご覧いただけます。</p>app.get("/search/semantic", async (req, res) =&gt; {
  const { q } = req.query;

  const INDEX_NAME = "vet-visits";

  try {
    const result = await esClient.search({
      index: INDEX_NAME,
      size: 5,
      body: {
        query: {
          semantic: {
            field: "semantic_field",
            query: q
          },
        },
      },
    });

    res.status(200).json({
      success: true,
      results: result.hits.hits,
    });
  } catch (error) {
    console.error("Error performing search:", error);

    res.status(500).json({
      success: false,
      results: null,
      error: error.message,
    });
  }
});<p>テスト対象:<em><strong>誰がペディキュアをしましたか?</strong></em></p>curl http://localhost:3000/search/semantic?q=Who%20got%20a%20pedicure?<p>答え：</p>{
    "success": true,
    "results": [
        {
            "_index": "vet-visits",
            "_id": "-RY6RJYBLe2GoFQ6-9n9",
            "_score": 4.861466,
            "_source": {
                "owner_name": "Daniel Kim",
                "pet_name": "Mochi",
                "species": "Rabbit",
                "breed": "Mixed",
                "vaccination_history": [],
                "visit_details": "Nail trimming and general health check. No issues."
            }
        },
        {
            "_index": "vet-visits",
            "_id": "8BY6RJYBLe2GoFQ6-9n9",
            "_score": 4.7152824,
            "_source": {
                "pet_name": "Buddy",
                "owner_name": "Alice Johnson",
                "species": "Dog",
                "visit_details": "Annual check-up and nail trimming. Healthy and active.",
                "breed": "Golden Retriever",
                "vaccination_history": [
                    "Rabies",
                    "Parvovirus",
                    "Distemper"
                ]
            }
        },
        {
            "_index": "vet-visits",
            "_id": "9RY6RJYBLe2GoFQ6-9n9",
            "_score": 1.6717153,
            "_source": {
                "pet_name": "Rex",
                "owner_name": "Tomás Herrera",
                "species": "Dog",
                "visit_details": "Follow-up for previous leg strain, improving well.",
                "breed": "German Shepherd",
                "vaccination_history": [
                    "Rabies",
                    "Parvovirus",
                    "Leptospirosis"
                ]
            }
        },
        {
            "_index": "vet-visits",
            "_id": "9xY6RJYBLe2GoFQ6-9n9",
            "_score": 1.5600781,
            "_source": {
                "pet_name": "Simba",
                "owner_name": "Leo Martínez",
                "species": "Cat",
                "visit_details": "Dental cleaning. Minor tartar buildup removed.",
                "breed": "Maine Coon",
                "vaccination_history": [
                    "Rabies",
                    "Feline Panleukopenia"
                ]
            }
        },
        {
            "_index": "vet-visits",
            "_id": "-BY6RJYBLe2GoFQ6-9n9",
            "_score": 1.2696637,
            "_source": {
                "pet_name": "Rocky",
                "owner_name": "Rachel Green",
                "species": "Dog",
                "visit_details": "Skin rash, antihistamines prescribed.",
                "breed": "Bulldog Mix",
                "vaccination_history": [
                    "Rabies",
                    "Parvovirus"
                ]
            }
        }
    ]
}<h3>ハイブリッド クエリ (/search/hybrid?q=&lt;query_term&gt;)</h3><p>ハイブリッド検索により、セマンティック検索と語彙検索を組み合わせることができるため、両方の長所を活用できます。つまり、トークンによる検索の精度と、セマンティック検索の意味の近似性の両方が得られます。</p>app.get("/search/hybrid", async (req, res) =&gt; {
  const { q } = req.query;

  const INDEX_NAME = "vet-visits";

  try {
    const result = await esClient.search({
      index: INDEX_NAME,
      body: {
        retriever: {
          rrf: {
            retrievers: [
              {
                standard: {
                  query: {
                    bool: {
                      must: {
                         multi_match: {
             query: q,
            fields: ["owner_name", "pet_name", "visit_details"],
          },
                      },
                    },
                  },
                },
              },
              {
                standard: {
                  query: {
                    bool: {
                      must: {
                        semantic: {
                          field: "semantic_field",
                          query: q,
                        },
                      },
                    },
                  },
                },
              },
            ],
          },
        },
        size: 5,
      },
    });

    res.status(200).json({
      success: true,
      results: result.hits.hits,
    });
  } catch (error) {
    console.error("Error performing search:", error);

    res.status(500).json({
      success: false,
      results: null,
      error: error.message,
    });
  }
});<p>「<em><strong>ペディキュアや歯科治療を受けた人はいますか？」</strong></em>という質問をしてテストします。</p>curl http://localhost:3000/search/hybrid?q=who%20got%20a%20pedicure%20or%20dental%20treatment<p>対応：</p>{
    "success": true,
    "results": [
        {
            "_index": "vet-visits",
            "_id": "9xY6RJYBLe2GoFQ6-9n9",
            "_score": 0.032522473,
            "_source": {
                "pet_name": "Simba",
                "owner_name": "Leo Martínez",
                "species": "Cat",
                "visit_details": "Dental cleaning. Minor tartar buildup removed.",
                "breed": "Maine Coon",
                "vaccination_history": [
                    "Rabies",
                    "Feline Panleukopenia"
                ]
            }
        },
        {
            "_index": "vet-visits",
            "_id": "-RY6RJYBLe2GoFQ6-9n9",
            "_score": 0.016393442,
            "_source": {
                "pet_name": "Mochi",
                "owner_name": "Daniel Kim",
                "species": "Rabbit",
                "visit_details": "Nail trimming and general health check. No issues.",
                "breed": "Mixed",
                "vaccination_history": []
            }
        },
        {
            "_index": "vet-visits",
            "_id": "8BY6RJYBLe2GoFQ6-9n9",
            "_score": 0.015873017,
            "_source": {
                "pet_name": "Buddy",
                "owner_name": "Alice Johnson",
                "species": "Dog",
                "visit_details": "Annual check-up and nail trimming. Healthy and active.",
                "breed": "Golden Retriever",
                "vaccination_history": [
                    "Rabies",
                    "Parvovirus",
                    "Distemper"
                ]
            }
        },
        {
            "_index": "vet-visits",
            "_id": "9RY6RJYBLe2GoFQ6-9n9",
            "_score": 0.015625,
            "_source": {
                "pet_name": "Rex",
                "owner_name": "Tomás Herrera",
                "species": "Dog",
                "visit_details": "Follow-up for previous leg strain, improving well.",
                "breed": "German Shepherd",
                "vaccination_history": [
                    "Rabies",
                    "Parvovirus",
                    "Leptospirosis"
                ]
            }
        },
        {
            "_index": "vet-visits",
            "_id": "8xY6RJYBLe2GoFQ6-9n9",
            "_score": 0.015384615,
            "_source": {
                "pet_name": "Luna",
                "owner_name": "Jake Thompson",
                "species": "Dog",
                "visit_details": "Mild ear infection, cleaning and antibiotics given.",
                "breed": "Labrador Mix",
                "vaccination_history": [
                    "Rabies",
                    "Bordetella"
                ]
            }
        }
    ]
}<h2>まとめ</h2><p>このシリーズの最初の部分では、クライアント/サーバーのベストプラクティスに従って、環境を設定し、さまざまな検索エンドポイントを持つサーバーを作成し、Elasticsearch ドキュメントをクエリする方法を説明しました。シリーズの<a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i">パート 2</a>では、本番環境のベスト プラクティスと、サーバーレス環境で Elasticsearch Node.js クライアントを実行する方法について学習します。</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i</guid>
    <category><![CDATA[JavaScript]]></category>
    <category><![CDATA[基本]]></category>
    <dc:creator><![CDATA[Jeffrey Rengifo]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt16d00c8a548b32e8/6a17e3d8fbc5f8c740491a19/72200540ed258779d87e53a72ea189f8a138540c-1600x901.png" length="0" type="image/png"/>
    <pubDate>Thu, 15 May 2025 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Ollama を推論 API と共に使用する]]></title>
    <description><![CDATA[Inference API を使用して Ollama を Elasticsearch と統合する方法を学びます。]]></description>
    <content:encoded><![CDATA[<p>この記事では、Ollama を使用してローカル モデルを Elasticsearch 推論モデルに接続し、Playground を使用してドキュメントに質問する方法を学習します。</p><p>Elasticsearch を使用すると、ユーザーは Open <a href="https://www.elastic.co/jp/guide/en/elasticsearch/reference/current/inference-apis.html">Inference API</a>を使用して LLM に接続でき、Amazon Bedrock、Cohere、Google AI、Azure AI Studio、HuggingFace などのプロバイダーをサービスとしてサポートします。</p><p><a href="https://ollama.com">Ollama は</a>、独自のインフラストラクチャ (ローカル マシン/サーバー) を使用して LLM モデルをダウンロードおよび実行できるツールです。<a href="https://ollama.com/library">ここでは</a>、Ollama と互換性のある利用可能なモデルのリストを見つけることができます。</p><p>Ollama は、各モデルをセットアップする方法の違いや、モデル関数にアクセスするための API の作成方法について心配することなく、さまざまなオープンソース モデルをホストしてテストしたい場合に最適なオプションです。Ollama がすべてを処理します。</p><p>Ollama API は OpenAI API と互換性があるため、推論モデルを簡単に統合し、Playground を使用して RAG アプリケーションを作成できます。</p><h2>要件</h2><ol><li><p>エラスティックサーチ 8.17</p></li><li><p>キバナ 8.17</p></li><li><p>Python</p></li></ol><h2>ステップ</h2><ol><li><p><a href="https://www.elastic.co/jp/search-labs/blog/ollama-with-inference-api#setting-up-ollama-llm-server">Ollama LLMサーバーのセットアップ</a></p></li><li><p><a href="https://www.elastic.co/jp/search-labs/blog/ollama-with-inference-api#creating-mappings">マッピングの作成</a></p></li><li><p><a href="https://www.elastic.co/jp/search-labs/blog/ollama-with-inference-api#indexing-data">データのインデックス作成</a></p></li><li><p><a href="https://www.elastic.co/jp/search-labs/blog/ollama-with-inference-api#asking-questions-using-playground">Playgroundを使って質問する</a></p></li></ol><h2>Ollama LLMサーバーのセットアップ</h2><p>Ollama を使用して、LLM サーバーを設定し、Playground インスタンスに接続します。以下のことが必要です:</p><ul><li><p>Ollamaをダウンロードして実行します。</p></li><li><p>ngrokを使用して、インターネット経由でOllamaをホストするローカルWebサーバーにアクセスします。</p></li></ul><h3>Ollamaをダウンロードして実行する</h3><p>Ollama を使用するには、まず<a href="https://ollama.com/download">ダウンロードする</a>必要があります。Ollama は Linux、Windows、macOS をサポートしているので、<a href="https://ollama.com/download">ここからお使いの OS と互換性のある Ollama バージョンをダウンロードしてください。</a>Ollama がインストールされると、サポートされている LLM の<a href="https://ollama.com/library">リスト</a>からモデルを選択できます。この例では、汎用多言語モデルである<a href="https://ollama.com/library/llama3.2">llama3.2</a>モデルを使用します。セットアップ プロセスでは、Ollama のコマンド ライン ツールを有効にします。ダウンロードが完了したら、次の行を実行できます。</p>ollama pull llama3.2<p>出力は次のようになります:</p>pulling manifest
pulling dde5aa3fc5ff... 100% ▕█████████████████████████████████████████████████████████████████████████████████████████▏ 2.0 GB
pulling 966de95ca8a6... 100% ▕█████████████████████████████████████████████████████████████████████████████████████████▏ 1.4 KB
pulling fcc5a6bec9da... 100% ▕█████████████████████████████████████████████████████████████████████████████████████████▏ 7.7 KB
pulling a70ff7e570d9... 100% ▕█████████████████████████████████████████████████████████████████████████████████████████▏ 6.0 KB
pulling 56bb8bd477a5... 100% ▕█████████████████████████████████████████████████████████████████████████████████████████▏   96 B
pulling 34bb5ab01051... 100% ▕█████████████████████████████████████████████████████████████████████████████████████████▏  561 B
verifying sha256 digest
writing manifest
success<p>インストールしたら、次のコマンドでテストできます。</p>ollama run llama3.2<p>質問してみましょう:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc12f240920e897f5/6a17f39425daab32a508a367/ad1eff81c1b04d2a747c3afd0ecbc215e5bd96fd-800x501.gif" alt="Ollamaを実行して質問する" /><p>モデルを実行すると、Ollama はポート「11434」でデフォルトで実行される API を有効にします。<a href="https://github.com/ollama/ollama/blob/main/docs/api.md">公式ドキュメント</a>に従って、その API にリクエストを送信してみましょう。</p>curl http://localhost:11434/api/generate -d '{                                          
  "model": "llama3.2",               
  "prompt": "What is the capital of France?"
}' <p>私たちが受け取った回答は次のとおりです。</p>{"model":"llama3.2","created_at":"2024-11-28T21:48:42.152817532Z","response":"The","done":false}
{"model":"llama3.2","created_at":"2024-11-28T21:48:42.251884485Z","response":" capital","done":false}
{"model":"llama3.2","created_at":"2024-11-28T21:48:42.347365913Z","response":" of","done":false}
{"model":"llama3.2","created_at":"2024-11-28T21:48:42.446837322Z","response":" France","done":false}
{"model":"llama3.2","created_at":"2024-11-28T21:48:42.542367394Z","response":" is","done":false}
{"model":"llama3.2","created_at":"2024-11-28T21:48:42.644580384Z","response":" Paris","done":false}
{"model":"llama3.2","created_at":"2024-11-28T21:48:42.739865362Z","response":".","done":false}
{"model":"llama3.2","created_at":"2024-11-28T21:48:42.834347518Z","response":"","done":true,"done_reason":"stop","context":[128006,9125,128007,271,38766,1303,33025,2696,25,6790,220,2366,18,271,128009,128006,882,128007,271,3923,374,279,6864,315,9822,30,128009,128006,78191,128007,271,791,6864,315,9822,374,12366,13],"total_duration":6948567145,"load_duration":4386106503,"prompt_eval_count":32,"prompt_eval_duration":1872000000,"eval_count":8,"eval_duration":684000000}<p><em>このエンドポイントの特定の応答はストリーミングであることに注意してください。</em></p><h3>ngrokを使用してエンドポイントをインターネットに公開する</h3><p>エンドポイントはローカル環境で動作するため、Elastic Cloud インスタンスなどの別のポイントからインターネット経由でアクセスすることはできません。<a href="https://ngrok.com">ngrok を</a>使用すると、パブリック IP を提供するポートを公開できます。ngrok でアカウントを作成し、公式の<a href="https://dashboard.ngrok.com/get-started/setup">セットアップ ガイド</a>に従ってください。</p><p>ngrok エージェントをインストールして構成したら、Ollama が使用しているポートを公開できます。</p>ngrok http 11434 --host-header="localhost:11434"<p><em>注: ヘッダー</em><em><code>--host-header="localhost:11434"</code></em>は<em>、リクエスト内の「Host」ヘッダーが「localhost:11434」と一致することを保証します。</em></p><p>このコマンドを実行すると、ngrok と Ollama サーバーがローカルで実行されている限り機能するパブリック リンクが返されます。</p>Session Status                online                                                                                                                                                                              
Account                       xxxx@yourEmailProvider.com (Plan: Free)                                                                                                                                             
Version                       3.18.4                                                                                                                                                                              
Region                        United States (us)                                                                                                                                                                  
Latency                       561ms                                                                                                                                                                               
Web Interface                 http://127.0.0.1:4040                                                                                                                                                               
Forwarding                    https://your-ngrok-url.ngrok-free.app -&gt; http://localhost:11434                                                                                                                   


Connections                   ttl     opn     rt1     rt5     p50     p90                                                                                                                                         
                              0       0       0.00    0.00    0.00    0.00                                                ```<p>「転送」では、ngrok が URL を生成したことがわかります。後で使用するために保存します。</p><p>ngrok によって生成された URL を使用して、エンドポイントへの HTTP リクエストを再度実行してみましょう。</p>curl https://your-ngrok-endpoint.ngrok-free.app/api/generate -d '{                                          
  "model": "llama3.2",               
  "prompt": "What is the capital of France?"
}'<p>応答は前のものと同様になるはずです。</p><h2>マッピングの作成</h2><h3>ELSERエンドポイント</h3><p>この例では、 <a href="https://www.elastic.co/jp/guide/en/elasticsearch/reference/current/put-inference-api.html">Elasticsearch 推論 API を使用して推論エンドポイントを作成します</a>。さらに、 <a href="https://www.elastic.co/jp/guide/en/machine-learning/current/ml-nlp-elser.html">ELSER</a>を使用して埋め込みを生成します。</p>PUT _inference/sparse_embedding/medicines-inference
{
  "service": "elasticsearch",
  "service_settings": {
    "num_allocations": 1,
    "num_threads": 1,
    "model_id": ".elser_model_2_linux-x86_64"
  }
}<p>この例では、2 種類の薬を販売する薬局があるとします。</p><ul><li><p>処方箋が必要な医薬品。</p></li><li><p>処方箋を必要としない医薬品。</p></li></ul><p>この情報は各薬剤の説明欄に含まれます。</p><p>LLM はこのフィールドを解釈する必要があるため、次のデータ マッピングを使用します。</p>PUT medicines
{
  "mappings": {
    "properties": {
      "name": {
        "type": "text",
        "copy_to": "semantic_field"
      },
      "semantic_field": {
        "type": "semantic_text",
        "inference_id": "medicines-inference"
      },
      "text_description": {
        "type": "text",
        "copy_to": "semantic_field"
      }
    }
  }
}<p>フィールド<code>text_description</code>は説明のプレーンテキストが格納され、 <a href="https://www.elastic.co/jp/guide/en/elasticsearch/reference/current/semantic-text.html">semantic_text</a>フィールド タイプである<code>semantic_field</code>には ELSER によって生成された埋め込みが格納されます。</p><p>プロパティ<a href="https://www.elastic.co/jp/guide/en/elasticsearch/reference/current/copy-to.html">copy_to は</a>、フィールド名と<code>text_description</code>の内容をセマンティック フィールドにコピーし、それらのフィールドの埋め込みが生成されます。</p><h2>データのインデックス作成</h2><p>ここで、 <a href="https://www.elastic.co/jp/guide/en/elasticsearch/reference/current/docs-bulk.html">_bulk API</a>を使用してデータをインデックス化してみましょう。</p>POST _bulk
{"index":{"_index":"medicines"}}
{"id":1,"name":"Paracetamol","text_description":"An analgesic and antipyretic that does NOT require a prescription."}
{"index":{"_index":"medicines"}}
{"id":2,"name":"Ibuprofen","text_description":"A nonsteroidal anti-inflammatory drug (NSAID) available WITHOUT a prescription."}
{"index":{"_index":"medicines"}}
{"id":3,"name":"Amoxicillin","text_description":"An antibiotic that requires a prescription."}
{"index":{"_index":"medicines"}}
{"id":4,"name":"Lorazepam","text_description":"An anxiolytic medication that strictly requires a prescription."}
{"index":{"_index":"medicines"}}
{"id":5,"name":"Omeprazole","text_description":"A medication for stomach acidity that does NOT require a prescription."}
{"index":{"_index":"medicines"}}
{"id":6,"name":"Insulin","text_description":"A hormone used in diabetes treatment that requires a prescription."}
{"index":{"_index":"medicines"}}
{"id":7,"name":"Cold Medicine","text_description":"A compound formula to relieve flu symptoms available WITHOUT a prescription."}
{"index":{"_index":"medicines"}}
{"id":8,"name":"Clonazepam","text_description":"An antiepileptic medication that requires a prescription."}
{"index":{"_index":"medicines"}}
{"id":9,"name":"Vitamin C","text_description":"A dietary supplement that does NOT require a prescription."}
{"index":{"_index":"medicines"}}
{"id":10,"name":"Metformin","text_description":"A medication used for type 2 diabetes that requires a prescription."}<p>対応：</p>{
   "errors": false,
   "took": 34732020848,
   "items": [
 	{
   	"index": {
     	"_index": "medicines",
     	"_id": "mYoeMpQBF7lnCNFTfdn2",
     	"_version": 1,
     	"result": "created",
     	"_shards": {
       	"total": 2,
       	"successful": 2,
       	"failed": 0
     	},
     	"_seq_no": 0,
     	"_primary_term": 1,
     	"status": 201
   	}
 	},
 	{
   	"index": {
     	"_index": "medicines",
     	"_id": "mooeMpQBF7lnCNFTfdn2",
     	"_version": 1,
     	"result": "created",
     	"_shards": {
       	"total": 2,
       	"successful": 2,
       	"failed": 0
     	},
     	"_seq_no": 1,
     	"_primary_term": 1,
     	"status": 201
   	}
 	},
 	{
   	"index": {
     	"_index": "medicines",
     	"_id": "m4oeMpQBF7lnCNFTfdn2",
     	"_version": 1,
     	"result": "created",
     	"_shards": {
       	"total": 2,
       	"successful": 2,
       	"failed": 0
     	},
     	"_seq_no": 2,
     	"_primary_term": 1,
     	"status": 201
   	}
 	},
 	{
   	"index": {
     	"_index": "medicines",
     	"_id": "nIoeMpQBF7lnCNFTfdn2",
     	"_version": 1,
     	"result": "created",
     	"_shards": {
       	"total": 2,
       	"successful": 2,
       	"failed": 0
     	},
     	"_seq_no": 3,
     	"_primary_term": 1,
     	"status": 201
   	}
 	},
 	{
   	"index": {
     	"_index": "medicines",
     	"_id": "nYoeMpQBF7lnCNFTfdn2",
     	"_version": 1,
     	"result": "created",
     	"_shards": {
       	"total": 2,
       	"successful": 2,
       	"failed": 0
     	},
     	"_seq_no": 4,
     	"_primary_term": 1,
     	"status": 201
   	}
 	},
 	{
   	"index": {
     	"_index": "medicines",
     	"_id": "nooeMpQBF7lnCNFTfdn2",
     	"_version": 1,
     	"result": "created",
     	"_shards": {
       	"total": 2,
       	"successful": 2,
       	"failed": 0
     	},
     	"_seq_no": 5,
     	"_primary_term": 1,
     	"status": 201
   	}
 	},
 	{
   	"index": {
     	"_index": "medicines",
     	"_id": "n4oeMpQBF7lnCNFTfdn2",
     	"_version": 1,
     	"result": "created",
     	"_shards": {
       	"total": 2,
       	"successful": 2,
       	"failed": 0
     	},
     	"_seq_no": 6,
     	"_primary_term": 1,
     	"status": 201
   	}
 	},
 	{
   	"index": {
     	"_index": "medicines",
     	"_id": "oIoeMpQBF7lnCNFTfdn2",
     	"_version": 1,
     	"result": "created",
     	"_shards": {
       	"total": 2,
       	"successful": 2,
       	"failed": 0
     	},
     	"_seq_no": 7,
     	"_primary_term": 1,
     	"status": 201
   	}
 	},
 	{
   	"index": {
     	"_index": "medicines",
     	"_id": "oYoeMpQBF7lnCNFTfdn2",
     	"_version": 1,
     	"result": "created",
     	"_shards": {
       	"total": 2,
       	"successful": 2,
       	"failed": 0
     	},
     	"_seq_no": 8,
     	"_primary_term": 1,
     	"status": 201
   	}
 	},
 	{
   	"index": {
     	"_index": "medicines",
     	"_id": "oooeMpQBF7lnCNFTfdn2",
     	"_version": 1,
     	"result": "created",
     	"_shards": {
       	"total": 2,
       	"successful": 2,
       	"failed": 0
     	},
     	"_seq_no": 9,
     	"_primary_term": 1,
     	"status": 201
   	}
 	}
   ]
 }<h2>Playgroundを使って質問する</h2><p><a href="https://www.elastic.co/jp/guide/en/kibana/current/playground.html">Playground は</a>、Elasticsearch インデックスと LLM プロバイダーを使用して RAG システムをすばやく作成できる Kibana ツールです。詳細については、この<a href="https://www.elastic.co/jp/search-labs/blog/playground-connectors-data-chat">記事</a>をお読みください。</p><h3>ローカルLLMをPlaygroundに接続する</h3><p>まず、作成したパブリック URL を使用するコネクタを作成する必要があります。Kibana で、 <strong>「検索」&gt;「プレイグラウンド」</strong>に移動し、「LLM に接続」をクリックします。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt22148eabfabf6d3f/6a17f3963e9e459f97ba15c6/1854f0808f8150e359fe62ba5d901d32a88d477c-1600x867.png" alt="地元のLLMをOllamaのPlaygroundに接続する" /><p>このアクションにより、Kibana インターフェースの左側にメニューが表示されます。そこで、「OpenAI」をクリックします。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt6e28194d9012f141/6a17f39725daab500a08a36b/c83d3c4d7035a518124ad7d22b38764db57b6800-933x1007.png" alt="コネクタを選択: Open AI Ollama" /><p>これで、OpenAI コネクタの設定を開始できます。</p><p>「コネクタ設定」に移動し、OpenAI プロバイダーの場合は「その他 (OpenAI 互換サービス)」を選択します。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt9984dce6f78a7c08/6a17f3990b0bed0b7add36c4/ecfcdc4b575c309bd55b4e61ca0ddb348aa84f64-917x268.png" alt="Ollama を Inference API で使用するためのコネクタ設定を行う" /><p>次に、他のフィールドを設定しましょう。この例では、モデル名を「medicines-llm」とします。URLフィールドには、ngrokによって生成されたURL（/v1/chat/completions）を入力します。「デフォルトモデル」フィールドで、「llama3.2」を選択します。API キーは使用しないので、任意のテキストを入力して続行します。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt8a24b93a39d380fb/6a17f39b96142a15c7eb1c3c/5d3b5027c8096cbe49fb740d70aa24e849611a9d-916x688.png" alt="設定を追加する" /><p>「保存」をクリックし、「データ ソースの追加」をクリックしてインデックス医薬品を追加します。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt4f107a54d5be25f9/6a17f39d4b055deb9d432338/525113da59e902c8235f62bde8fb62371a63e11b-1579x753.png" alt="Playground を使用してドキュメントに質問するためのデータ ソースを追加します" /><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt03fb36fe05dcdbf5/6a17f39ebe608602f40048be/96138de0bbe2c2ac619f64889d3487df62739ca4-466x805.png" alt="クエリデータを追加する" /><p>素晴らしい！これで、RAG エンジンとしてローカルで実行している LLM を使用して Playground にアクセスできるようになりました。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltb1b27580107259b3/6a17f3a096142abefceb1c40/cfb48b33c70f4534ab77eb01f58008237f65e6f4-1600x851.png" alt="プレイグラウンドでモデル設定を選択する" /><p>テストする前に、エージェントにさらに具体的な指示を追加し、モデルに送信されるドキュメントの数を 10 に増やして、回答にできるだけ多くのドキュメントが含まれるようにします。コンテキスト フィールドは<code>semantic_field</code>になり、copy_to プロパティにより、薬剤の名前と説明が含まれます。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt4fbc97dc87c6fc62/6a17f3a1e8fbce052c3a1aa0/0c57c9c0e1a0e7b58fffdd3ef81d67d41e2990c4-580x806.png" alt="Elastic PlaygroundのMoel設定" /><p>さて、質問しましょう：<em><strong>処方箋なしでクロナゼパムを購入できますか？</strong></em>そして何が起こるか見てみましょう:</p><p>予想通り、正しい答えが得られました。</p><h3>今後の見通し</h3><p>次のステップは、独自のアプリケーションを作成することです。Playground は、マシン上で実行し、ニーズに合わせてカスタマイズできる Python のコード スクリプトを提供します。たとえば、 <a href="https://fastapi.tiangolo.com/">FastAPI</a>サーバーの背後に配置して、UI で使用される QA 医薬品チャットボットを作成します。</p><p>このコードは、Playground の右上のセクションにある<em><strong>[コードの表示]</strong></em>ボタンをクリックすると見つかります。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt42fe193b8aa08830/6a17f3a33e9e4569e8ba15ca/816bfd0e5f936ad65dbe719d5df10714e550a40b-380x121.png" alt="コード表示ボタン" /><p>また、<em><strong>エンドポイントと API キー</strong></em>を使用して、コードで必要な<code>ES_API_KEY</code>環境変数を生成します。</p><p>この特定の例では、コードは次のようになります。</p>## Install the required packages
## pip install -qU elasticsearch openai
import os
from elasticsearch import Elasticsearch
from openai import OpenAI
es_client = Elasticsearch(
    "https://your-deployment.us-central1.gcp.cloud.es.io:443",
    api_key=os.environ["ES_API_KEY"]
)
openai_client = OpenAI(
    api_key=os.environ["OPENAI_API_KEY"],
)
index_source_fields = {
    "medicines": [
        "semantic_field"
    ]
}
def get_elasticsearch_results():
    es_query = {
        "retriever": {
            "standard": {
                "query": {
                    "nested": {
                        "path": "semantic_field.inference.chunks",
                        "query": {
                            "sparse_vector": {
                                "inference_id": "medicines-inference",
                                "field": "semantic_field.inference.chunks.embeddings",
                                "query": query
                            }
                        },
                        "inner_hits": {
                            "size": 2,
                            "name": "medicines.semantic_field",
                            "_source": [
                                "semantic_field.inference.chunks.text"
                            ]
                        }
                    }
                }
            }
        },
        "size": 3
    }
    result = es_client.search(index="medicines", body=es_query)
    return result["hits"]["hits"]
def create_openai_prompt(results):
    context = ""
    for hit in results:
        inner_hit_path = f"{hit['_index']}.{index_source_fields.get(hit['_index'])[0]}"
        ## For semantic_text matches, we need to extract the text from the inner_hits
        if 'inner_hits' in hit and inner_hit_path in hit['inner_hits']:
            context += '\n --- \n'.join(inner_hit['_source']['text'] for inner_hit in hit['inner_hits'][inner_hit_path]['hits']['hits'])
        else:
            source_field = index_source_fields.get(hit["_index"])[0]
            hit_context = hit["_source"][source_field]
            context += f"{hit_context}\n"
    prompt = f"""
  Instructions:
  - You are an assistant specializing in answering questions about the sale of medicines.
  - Answer questions truthfully and factually using only the context presented.
  - If you don't know the answer, just say that you don't know, don't make up an answer.
  - You must always cite the document where the answer was extracted using inline academic citation style [], using the position.
  - Use markdown format for code examples.
  - You are correct, factual, precise, and reliable.
  Context:
  {context}
  """
    return prompt
def generate_openai_completion(user_prompt, question):
    response = openai_client.chat.completions.create(
        model="gpt-3.5-turbo",
        messages=[
            {"role": "system", "content": user_prompt},
            {"role": "user", "content": question},
        ]
    )
    return response.choices[0].message.content
if __name__ == "__main__":
    question = "my question"
    elasticsearch_results = get_elasticsearch_results()
    context_prompt = create_openai_prompt(elasticsearch_results)
    openai_completion = generate_openai_completion(context_prompt, question)
    print(openai_completion)<p>Ollama で動作させるには、OpenAI クライアントを OpenAI サーバーではなく Ollama サーバーに接続するように変更する必要があります。OpenAI の例と互換性のあるエンドポイントの完全なリストについては、こちらをご覧ください。</p>openai_client = OpenAI(
    # you can use http://localhost:11434/v1/ if running this code locally.
    base_url='https://your-ngrok-url.ngrok-free.app/v1/',
    # required but ignored
    api_key='ollama',
)<p>また、補完メソッドを呼び出すときにモデルを llama3.2 に変更します。</p>def generate_openai_completion(user_prompt, question):
    response = openai_client.chat.completions.create(
        model="llama3.2",
        messages=[
            {"role": "system", "content": user_prompt},
            {"role": "user", "content": question},
        ]
    )
    return response.choices[0].message.content<p>質問を追加しましょう:<em><strong>処方箋なしでクロナゼパムを購入できますか?</strong></em>Elasticsearch クエリへ:</p>def get_elasticsearch_results():
    es_query = {
        "retriever": {
            "standard": {
                "query": {
                    "nested": {
                        "path": "semantic_field.inference.chunks",
                        "query": {
                            "sparse_vector": {
                                "inference_id": "medicines-inference",
                                "field": "semantic_field.inference.chunks.embeddings",
                                "query": "Can I buy Clonazepam without a prescription?"
                            }
                        },
                        "inner_hits": {
                            "size": 2,
                            "name": "medicines.semantic_field",
                            "_source": [
                                "semantic_field.inference.chunks.text"
                            ]
                        }
                    }
                }
            }
        },
        "size": 3
    }
    result = es_client.search(index="medicines", body=es_query)
    return result["hits"]["hits"]<p>また、いくつかの出力を伴う完了呼び出しでは、質問のコンテキストの一部として Elasticsearch の結果が送信されていることを確認できます。</p>if __name__ == "__main__":
    question = "Can I buy Clonazepam without a prescription?"
    elasticsearch_results = get_elasticsearch_results()
    context_prompt = create_openai_prompt(elasticsearch_results)
    print("========== Context Prompt START ==========")
    print(context_prompt)
    print("========== Context Prompt END ==========")
    print("========== Ollama Completion START ==========")
    openai_completion = generate_openai_completion(context_prompt, question)
    print(openai_completion)
    print("========== Ollama Completion END ==========")<p>ではコマンドを実行してみましょう</p><p><code>pip install -qU elasticsearch openai</code></p><p><code>python main.py</code></p><p>次のような画面が表示されます。</p>========== Context Prompt START ==========
  Instructions:
  - You are an assistant specializing in answering questions about the sale of medicines.
  - Answer questions truthfully and factually using only the context presented.
  - If you don't know the answer, just say that you don't know, don't make up an answer.
  - You must always cite the document where the answer was extracted using inline academic citation style [], using the position.
  - Use markdown format for code examples.
  - You are correct, factual, precise, and reliable.
  Context:
  Clonazepam
 ---
An antiepileptic medication that requires a prescription.A nonsteroidal anti-inflammatory drug (NSAID) available WITHOUT a prescription.
 ---
IbuprofenAn anxiolytic medication that strictly requires a prescription.
 ---
Lorazepam


========== Context Prompt END ==========
========== Ollama Completion START ==========
No, you cannot buy Clonazepam over-the-counter (OTC) without a prescription [1]. It is classified as a controlled substance in the United States due to its potential for dependence and abuse. Therefore, it can only be obtained from a licensed healthcare provider who will issue a prescription for this medication.
========== Ollama Completion END ==========<h2>まとめ</h2><p>この記事では、Ollama などのツールを Elasticsearch 推論 API および Playground と組み合わせて使用した場合の、その強力さと汎用性について説明します。</p><p>いくつかの簡単な手順を実行するだけで、LLM を使用したチャット機能を備えた RAG アプリケーションが、コストゼロで独自のインフラストラクチャで稼働するようになりました。これにより、さまざまなタスクのさまざまなモデルにアクセスできるだけでなく、リソースと機密情報をより細かく制御できるようになります。</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/ollama-with-inference-api</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/ollama-with-inference-api</guid>
    <category><![CDATA[AI]]></category>
    <dc:creator><![CDATA[Jeffrey Rengifo]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltd9c8eb0fc946920e/6a17f3a46864a4b2fbb688f0/399b9ef527be633845fb6505b68132cc03bc9e09-1150x628.png" length="0" type="image/png"/>
    <pubDate>Fri, 14 Feb 2025 00:00:00 GMT</pubDate>
  </item>
  </channel>
</rss>