<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
  <channel>
    <title><![CDATA[開発者エクスペリエンス - Elasticsearch Labs]]></title>
    <description><![CDATA[Articles and tutorials from the Search team at Elastic]]></description>
    <copyright><![CDATA[© 2026. Elasticsearch B.V. All Rights Reserved]]></copyright>
    <image>
      <title><![CDATA[開発者エクスペリエンス - Elasticsearch Labs]]></title>
      <url>https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1121c0bf0e8a6e65/6a88da6340a1841030ef456f/search-labs-thumbnail.png</url>
      <link>https://www.elastic.co/jp/search-labs/blog/category/developer-experience</link>
    </image>
    <link>https://www.elastic.co/jp/search-labs/blog/category/developer-experience</link>
    <atom:link href="https://www.elastic.co/jp/search-labs/rss/category/developer-experience.xml" rel="self" type="application/rss+xml"/>
    <language><![CDATA[jp]]></language>
    <lastBuildDate>Mon, 28 Sep 2026 11:56:50 GMT</lastBuildDate>
  <item>
    <title><![CDATA[Kibana Dashboards API：GA前に50以上のチームでテストされた、あらゆるパネルタイプに対応する安定した仕様]]></title>
    <description><![CDATA[Kibanaのダッシュボードをコードとして管理：Kibana APIとTerraformを使ってGitにコミットし、環境間で展開し、導入を自動化します。]]></description>
    <content:encoded><![CDATA[<p><a href="https://dashboardsapispec.kibana.dev/dashboards#tag/Dashboards">KibanaのDashboards APIとVisualizations API</a>は、Elastic 9.5で本番環境に対応しており、すべてのサブスクリプションティアで利用可能で、完全な下位互換性も備えています。ダッシュボードをJSONとして定義してGitにコミットし、継続的インテグレーションと継続的デプロイメント（CI/CD）パイプライン、<a href="https://registry.terraform.io/providers/elastic/elasticstack/latest/docs/resources/kibana_dashboard">Terraform</a>、またはすでにお持ちのツールを使って環境全体にデプロイします。<a href="https://www.elastic.co/search-labs/blog/kibana-dashboards-as-code-terraform-api">9.4のテクニカルプレビュー</a>期間中に50以上のチームがAPIをテストし、中には既に本番環境で運用しているチームもあります。バージョン9.5では<a href="https://dashboardsapispec.kibana.dev/tags.html">タグ</a>用の新しいエンドポイント（テクニカルプレビュー中）が追加され、<a href="https://dashboardsapispec.kibana.dev/markdowns.html">マークダウン</a>および<a href="https://dashboardsapispec.kibana.dev/links.html#tag/Links">リンク</a>パネルエンドポイントはElastic Cloud Serverlessで利用可能となり、9.6でリリース予定です。</p><h2>Kibana Dashboards APIにおける後方互換性の意味</h2><p>テクニカルプレビュー中はリリース間でAPIの仕様が変更される可能性があります。[1]現在はこれは該当しません。一般提供（GA）開始とは、以下のことを意味します。</p><ul><li><p><strong>完全な後方互換性。</strong>新しいフィールドやパネルの種類は今後追加されますが、既存のフィールドや動作は変更されません。今後、互換性を損なうような変更を行う場合は、非常に慎重に検討し、新しいメジャーバージョンでのみ導入します。</p></li><li><p><strong>本番環境での利用に対応し、完全にサポートさ。</strong>このAPIには、Elasticの完全なサポート保証が付いています。本番環境では、自動化された導入、環境の昇格、およびプログラムによるダッシュボード管理に安全にご利用いただけます。</p></li></ul><h2>タグ、マークダウン、リンクパネル用の新しいKibana APIエンドポイント</h2><p>Elastic 9.5では、<a href="https://dashboardsapispec.kibana.dev/tags.html"><strong>タグ</strong></a>用の新しいスタンドアロンエンドポイントも導入され、ダッシュボードの分類やフィルタリングが可能になりました。専用のCRUDエンドポイントによりプログラムによる管理が可能になり、複数の環境にわたって大規模にダッシュボードを整理しやすくなりました。	</p><p>新しい<a href="https://dashboardsapispec.kibana.dev/markdowns.html"><strong>マークダウン</strong></a>および<a href="https://dashboardsapispec.kibana.dev/links.html#tag/Links"><strong>リンク</strong></a>パネルエンドポイントは現在Serverlessで利用可能で、次回のスタックリリース（9.6）に搭載される予定です。</p><h2>Kibana Dashboards APIはどのようなパネル型をサポートしていますか？</h2><p>Dashboards APIは、9.5のすべての<em>値渡し</em>パネル（再利用のために保存されたライブラリパネルとは対照的に、ダッシュボードで直接定義されたもの）をサポートしています。サポートされているすべてのパネル型には、型指定され検証済みのスキーマがあります。</p><p><strong>パネル型</strong></p><p><strong>ステータス</strong></p><p>XYチャート</p><p>サポートあり</p><p>メトリクス</p><p>サポートあり</p><p>円グラフ</p><p>サポートあり</p><p>ゲージ</p><p>サポートあり</p><p>ヒートマップ</p><p>サポートあり</p><p>データ表</p><p>サポートあり</p><p>ツリーマップ</p><p>サポートあり</p><p>Discoverセッション</p><p>サポートあり</p><p>コントロール</p><p>サポートあり</p><p>マークダウン</p><p>サポートあり</p><p>リンク</p><p>サポートあり</p><p>MLパネル</p><p>サポートあり</p><p>オブザーバビリティパネル</p><p>サポートあり</p><p>マップ</p><p>まもなくリリース</p><p>Vega</p><p>まもなくリリース</p><h2>Kibanaのダッシュボードをコードとして管理する方法</h2><p>Dashboards APIを使用すると、完全なダッシュボード・アズ・コードのワークフローが実現できます。ダッシュボードをクリーンで差分比較可能なJSONとしてエクスポートし、それをGitにコミットして真の情報源とし、プルリクエストで変更内容を確認し、開発環境、ステージング環境、本番環境に同じ定義をデプロイできます。ダッシュボードをコードとして管理するようになったら、Gitを唯一の信頼できる情報源として扱います。UIで直接行った変更は、次回デプロイ時に上書きされます。</p><p>ダッシュボードをスペース、クラスター、またはステージ間で移動する際の主な課題は、ダッシュボードがデータビューやライブラリの視覚化などのオブジェクトをIDで参照することです。これらのIDは自動生成され、環境ごとに異なるため、ある環境からエクスポートされたダッシュボードが、別の環境には存在しないオブジェクトを指し示すことがあります。この問題を解決するには3つの方法があり、自動化の度合いが高い順に以下に示します。</p><ul><li><p><strong>Terraformを使用する。</strong><a href="https://registry.terraform.io/providers/elastic/elasticstack/latest/docs/resources/kibana_dashboard">Elastic Stack Terraformプロバイダー</a>は各リソースを追跡し、環境ごとにIDを自動マッピングするため、開発から本番環境へダッシュボードを移行する際に参照が一貫して保たれます。</p></li><li><p><strong>値渡しの</strong><a href="https://www.elastic.co/docs/explore-analyze/visualize/esorql"><strong>Elasticsearchクエリ言語（ES|QL）パネル</strong></a><strong>を定義する。</strong>パネルを構築する最もポータブルな方法は、ES|QLを使用してダッシュボードで直接可視化を定義することです。<a href="https://www.elastic.co/docs/explore-analyze/query-filter/languages/esql-kibana">ES|QL</a>クエリは、クエリ内で指定したインデックスからデータを読み取るため、パネルにはData viewやライブラリオブジェクトへの外部参照は含まれません。その結果、完全に自己完結型のポータブルダッシュボードとなります。</p></li><li><p><strong>一致するIDを割り当てる。</strong>Data viewやライブラリの視覚化など、保存済みのオブジェクトを参照する場合は、POST（IDを自動生成）ではなく、PUT（アップサート）を使用して、選択したIDでオブジェクトを作成してください。「logs-prod」のような人間が読みやすいIDを使うことで、環境を超えて再利用・認識しやすくなります。</p></li></ul><p>これらの移植性パターンとダッシュボード・アズ・コードのワークフロー全体に関する詳細な説明については、<a href="https://www.elastic.co/docs/explore-analyze/dashboards/manage-dashboards-as-code#dashboards-as-code-portability">「ダッシュボードをコードとして管理する」</a>ドキュメントをご覧ください。</p><h3>PUTメソッドを使用してDashboards APIでKibanaダッシュボードを作成</h3><p>ここでは、ダッシュボード名（service-health-overview）を使用してカスタムIDを割り当てるために、POSTではなくPUTを使用してメトリックパネルを含むダッシュボードを作成する簡単な例を示します。ライブラリに保存されたスタンドアロンの可視化を作成する場合も、同じロジックが適用されます。</p>PUT kbn:/api/dashboards/service-health-overview
{
  "title": "Service health overview",
  "description": "Key service metrics — managed via API",
  "tags": [
    "production",
    "sre-team"
  ],
  "panels": [
    {
      "type": "vis",
      "grid": {
        "x": 0,
        "y": 0,
        "w": 12,
        "h": 8
      },
      "config": {
        "title": "Error rate (5xx)",
        "type": "metric",
        "data_source": {
          "type": "esql",
          "query": "FROM logs-* | WHERE http.response.status_code &gt;= 500 | STATS error_rate=count(*) BY host.name"
        },
        "metrics": [
          {
            "type": "primary",
            "column": "count"
          }
        ]
      }
    }
  ]
}<h2>Kibana Dashboards APIロードマップ：マップ、Vega、スタンドアロンのエンドポイント</h2><p>APIの展開範囲を積極的に拡大していきます。次に、マップとVegaパネルのサポートを進め、それらに型付きスキーマを追加します。また、ダッシュボードのライフサイクルから切り離したDiscoverセッション（既存のダッシュボードパネルのサポートを超えて）、Vega、マップ、Annotations向けのスタンドアロンCRUDエンドポイントも構築しています。</p><p>スキーマ定義の詳細は、<a href="https://dashboardsapispec.kibana.dev/dashboards#tag/Dashboards">Dashboards APIドキュメント</a>をご覧ください。Terraformユーザー向けには<a href="https://registry.terraform.io/providers/elastic/elasticstack/latest/docs/resources/kibana_dashboard">Elastic Stack Terraformプロバイダー</a>が一般提供のDashboards APIをサポートしています。</p><h2>注</h2><ol><li><p>コアエンドポイントはテクニカルプレビュー版から変更されていません。9.4向けに構築した統合機能は、9.5でも動作します。唯一の破壊的変更は、ダッシュボード一覧と所要時間単位のフォーマットに影響を与える2つの小さな変更で、<a href="https://www.elastic.co/docs/release-notes/kibana/breaking-changes">こちら</a>に記載されています。</p></li></ol>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/dashboards-as-code-kibana-api</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/dashboards-as-code-kibana-api</guid>
    <category><![CDATA[Kibana]]></category>
    <category><![CDATA[開発者エクスペリエンス]]></category>
    <category><![CDATA[統合]]></category>
    <dc:creator><![CDATA[Teresa Alvarez Soler]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt8ed7e33de291f255/6a730619c8b7ac02b251f9d3/image1.png" length="0" type="image/png"/>
    <pubDate>Wed, 05 Aug 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Elastic Cloud ServerlessとElasticsearchの統合APIキーが登場]]></title>
    <description><![CDATA[Elasticがグローバルに分散されたIAMアーキテクチャでServerlessのコントロールプレーンとデータプレーンの認証を統合した方法をご紹介します。Cloud APIとElasticsearch APIに1つのAPIキーを使用できます。]]></description>
    <content:encoded><![CDATA[<p>あなたがサイト信頼性エンジニア（SRE）で、Elastic Cloud Serverlessプロジェクトの成長する製品群を担当していると想像してみてください。本番環境インフラのためのElastic Observability、セキュリティ運用センター（SOC）チームのためのElastic Security、そして顧客向けアプリケーションのためのElasticsearchです。各プロジェクトにそれぞれ固有のElasticsearch APIキーがあります。継続的インテグレーションと継続的デリバリー（CI/CD）パイプラインは、これらのプロジェクトをプロビジョニング・管理するために、別のCloud APIキーが必要です。四半期ごとにローテーションの日がやってきます。各プロジェクトを順番に確認し、新しいキーを作成し、Terraformの状態を更新し、パイプラインを再デプロイし、何も見落としがないことを祈ります。午前2時にインシデントが発生し、迅速にアクセス権を取り消す必要がある場合、どのキーがどのプロジェクト、どのサービスに属しているかを特定するために、認証情報が記載されたスプレッドシートを相互参照することになります。</p><p>今日では、こうした局面でもずっとシンプルになります。<strong>Elastic Cloud APIキー</strong>が、<strong>Elastic Cloud Serverless上で</strong><strong>Elasticsearch</strong>と<strong>Kibana</strong> APIに対する直接認証に使用できるようになりました。単一の認証情報を使用して、組織のリソースを管理したり、 Elasticsearchクエリ言語（ES|QL）クエリ、データ取り込み、アラートなどのデータ操作を実行したりできるようになりました。</p><p>当社がこれを構築した理由、グローバルに分散されたIDレイヤーをどのように設計して実現した方法、そしてこれがクロスプロジェクト検索の基盤をどのように築くのかを見ていきましょう。</p><h2>シークレット管理の負担</h2><p>信頼性の高いCI/CDパイプライン、GitOpsワークフロー、またはTerraformの自動化をデータプラットフォームに構築する際には、隠れたコストが伴います。それは、シークレットの無秩序な拡散です。</p><p>以前のモデルでは、開発者は断片的な認証プロセスに直面していました。</p><ul><li><p><strong>コントロールプレーン（Elastic Cloud APIキー）：</strong> <a href="https://www.elastic.co/docs/api/doc/cloud/">Elastic Cloud API</a>経由でプロジェクトの作成、ユーザーの招待、課金管理などを行う際に使用する組織スコープのキー。</p></li><li><p><strong>データプレーン（Elasticsearch APIキー）：</strong>特定のサーバーレスプロジェクト<em>内で</em>作成され、<a href="https://www.elastic.co/docs/api/doc/elasticsearch-serverless/">Elasticsearch</a>および<a href="https://www.elastic.co/docs/api/doc/serverless">Kibana</a> APIとやり取りするために使用されるプロジェクトスコープのキー。</p></li></ul><p>つまり、導入スクリプトがElastic Cloudに対して認証を行い、Serverlessプロジェクトをプロビジョニングし、その特定のプロジェクトから新しく作成されたElasticsearch APIキーを抽出し、その後、その<em>2番目のキー</em>を下流のアプリケーションまたは自動化ツールに注入する必要があったことになります。結果として複雑なパイプライン、断片化された監査ログ、認証情報漏洩のリスクの増加につながっていました。</p><h2>Elastic Cloud Serverlessでの統合認証</h2><p>このリリースにより、サーバーレスプロジェクトの分割はなくなりました。<strong>クラウド、Elasticsearch、Kibana APIs</strong>に対して明示的に認可されたElastic Cloud APIキーを作成できるようになりました。</p><ul><li><p><strong>以前：</strong>Elastic Cloud APIキーは、厳密にはコントロールプレーントークンでした。プロジェクトの作成、請求管理、ユーザー招待は可能だったが、プロジェクト内でElasticsearchやKibanaのAPIを呼び出せないという明確な制限がありました。データ操作には、常にプロジェクト固有の2つ目のキーが必要でした。</p></li><li><p><strong>現在：</strong>Elastic Cloud APIキーを作成する際に<strong>Cloud、Elasticsearch、Kibana API</strong>へのアクセスを選択することで、Serverlessの境界が解除され、そのAPIキーが真に統一された認証情報となります。組織のインフラを管理する能力を維持しながら、同時に任意の認可されたサーバーレスプロジェクトでデータをクエリ、取り込み、分析するためのネイティブアクセスを獲得できます。</p></li></ul><p>これを単一のElastic Cloud APIキーに統合することで、スコープ、監査、ローテーション、取り消しを1つのユニットとして行うことができる単一のIDが得られます。新しいプロジェクトのプロビジョニングであれ、ES|QLクエリの実行であれ、すべてのAPI呼び出しは監査ログに同じ認証情報で記録されるため、インシデント調査やコンプライアンスレビューの際に追跡できる単一の履歴が提供されます。認証情報のローテーションは、分離されたコントロールプレーンとデータプレーンのシークレット間での調整された更新ではなく、ワンステップの操作になります。また、役割の割り当てはプロジェクトごとに行われるため、1つのキーで複数のプロジェクトを横断的に管理でき、監視プロジェクトでのデータ取り込みを管理したり、セキュリティプロジェクトでクエリを実行したりすることが可能になり、プロジェクトごとに個別の認証情報を管理する手間が省けます。</p><p>重要なのは、<em>統一されているということ</em>は決して<em>全能である</em>ことを意味しない点です。<code>role_assignments</code> ペイロードを使用することで、統一されたキーを厳密に単一のプロジェクトと特定のロール（例: 読み取り専用）にスコープすることができ、認証情報が漏洩した場合でも、影響範囲を完全に制限することができます。開発者が退職した場合やアプリケーションが廃止された場合も、Elastic Cloudコンソールから単一のキーを取り消すことで、コントロールプレーンと関連するすべてのElasticsearchプロジェクトへのアクセスを即座に停止できます。</p><p><em>（注：Elastic Cloud Hosted/マネージド導入では、Cloud APIキーは依然としてコントロールプレーンのみを管理します。ホスト型スタックAPIへの対応は今後のリリースで予定されています。）</em></p><h2>ワークフローの自動化</h2><p>始めるのは簡単です。Elastic Cloudコンソールから完全に設定するか、<a href="https://www.elastic.co/docs/deploy-manage/api-keys/elastic-cloud-api-keys">Elastic Cloud API</a>を使って自動化できます。</p><p>UIの操作手順は変わりませんが、プロジェクトロールの割り当て時に<strong>Cloud、Elasticsearch、Kibana API</strong>へのアクセスを選択できるようになりました。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltda0a18945295aa84/6a1707bd509168fab4e1ba19/c4f802f130655290cd474b283001a954d14c3088-2801x1681.png" alt="Elastic Cloud画面にAPIキーのページが表示され、名前、有効期限、およびロール割り当てのフィールドを含むCreate API keyモーダルが開いています。" /><p>Elastic Cloud APIを使用してプログラムで統合キーを作成する方法を以下に示します。<code>application_roles</code>配列に注目してください。これが、Elasticsearch データプレーンへのキーのネイティブアクセスを許可します。</p>curl -X POST \
  -H "Content-Type: application/json" \
  -H "Authorization: ApiKey $EC_API_KEY" \
  "https://api.elastic-cloud.com/api/v1/users/auth/keys" \
  -d '{
    "description": "unified-automation-key",
    "expiration": "90d",
    "role_assignments": {
      "project": {
        "elasticsearch": [
          {
            "role_id": "elasticsearch-admin",
            "organization_id": "YOUR_ORG_ID",
            "all": false,
            "project_ids": ["YOUR_PROJECT_ID"],
            "application_roles": ["admin"]
          }
        ]
      }
    }
  }'<p>一度作成すると、このまったく同じキーを<code>Authorization: ApiKey</code>ヘッダーで<code>api.elastic-cloud.com</code>と特定のサーバーレスElasticsearchエンドポイントの両方に渡すだけです。</p><h2>内部構造：分散型IDレイヤーの構築</h2><p>Cloud APIキーをコントロールプレーンとデータプレーンの両方で使えるようにするのは、トークンを渡すほど単純ではありません。分散システムの根本的な課題を解決する必要があります。</p><p>歴史的に、Cloud APIキーは中央集権的なグローバルセキュリティクラスターに存在していました。これは、より高いレイテンシが許容されるコントロールプレーン操作には問題なく機能しますが、Elasticsearchのデータリクエストには超低レイテンシが求められます。すべての検索クエリやデータ取り込みリクエストを検証するために、地球を横断して中央コントロールプレーンまで往復する余裕はありません。</p><p>この問題を解決するため、グローバルに分散されたデータストアを基盤とする新しい認証アーキテクチャを導入しました。次のシーケンス図は、Elastic Cloud APIキーを使用してクライアントがElasticsearchクエリを送信する流れを示しています。グローバルコントロールプレーンへの往復なしに、認証がローカルリージョン内で完結することを示しています。Elasticsearchは認証を地域IAMサービスに委任します。このサービスはキーを検証し、グローバルに分散されたデータベースのローカルレプリカに照らしてロール割り当てを解決します。認可されると、Elasticsearchはクエリを実行し、結果をクライアントに返します。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf4fa84c3f33f88f7/6a1707baacf088989abe9a8e/3e38d7a862b9981523c5393c441b92eae13aeb90-2401x1351.webp" alt="結果を返す前に、Elasticsearch Serverless、地域のIAMサービス、分散データベースのレプリカを介して流れるCloud APIキーを含むクライアントのリクエストを示すシーケンス図。" /><h3>グローバルに分散された永続性</h3><p>Elastic Cloud APIキーとそれに関連付けられたロール定義は、中央集権型のセキュリティクラスターにのみ依存するのではなく、グローバルに分散された高可用性データベースに永続的に保存されるようになりました。このデータベースは、サーバーレスプロジェクトが実際に実行されるグローバルコントロールプレーンとリージョナルデータプレーン間で、アイデンティティおよびアクセス管理（IAM）データを同期します。</p><h3>地域IAMによるローカル検証</h3><p>クライアントがElastic Cloud APIキーを使用してElasticsearchにリクエストを送信した場合、そのリクエストはグローバルコントロールプレーンには返されません。代わりに、新しい地域IAMサービスにルーティングされます。ローカルデータベースのレプリカに対してキーを検証することで、認証がほぼゼロ遅延で行われ、グローバルなコントロールプレーンの障害から完全に隔離されることを保証します。</p><h3>動的ロールマッピング</h3><p>認証は戦いの半分に過ぎず、システムはリクエストを承認する必要もあります。地域IAMサービスは、クラウドレベルのロール割り当て（例：<code>application_roles</code>）をネイティブのElasticsearch権限に即座に変換します。Elasticsearch は、ローカルで<code>.security</code>インデックスを必要とすることなく、ローカルでリクエストを承認し、実行することができます。</p><h2>プロジェクト横断検索の基礎</h2><p>この分散型IDアーキテクチャは、Elastic Platformの将来を支える基本的な構成要素です。</p><p>IDとアクセス権限が統一され、グローバルに同期されたことで、異なるプロジェクト間で安全に身元情報をやり取りするために必要なフレームワークが整いました。これにより、Serverless向けの<strong>プロジェクト横断検索（CPS）</strong>機能が有効になります。</p><p>CPSを使用すると、セキュリティとオブザーバビリティのワークロードを組み合わせるなど、複数のリモートServerlessプロジェクトにまたがるデータを、あたかも1つのデータセットであるかのように簡単にクエリできるようになります。統一されたAPIキーを利用することで、システムはすべてのプロジェクトにわたるユーザーの権限を同時に自動的に評価できます。対象プロジェクトごとに複雑な信頼関係、証明書、重複した認証情報を設定する必要はありません。</p><h2>詳しくはこちら</h2><p>スタックを簡素化する準備はできていますか？</p><ul><li><p>スタックアクセスの割り当て方法については<a href="https://www.elastic.co/docs/deploy-manage/api-keys/elastic-cloud-api-keys">Elastic Cloud APIキーのドキュメント</a>をお読みください。</p></li><li><p>キー生成を自動化するには、<a href="https://www.elastic.co/docs/api/doc/cloud/operation/operation-create-api-key">「APIキーを作成する（Elastic Cloud API）」</a>のリファレンスを参照してください。</p></li><li><p>Elasticプラットフォーム全体で使用されているキーの種類を包括的に比較するには、 <a href="https://www.elastic.co/docs/deploy-manage/api-keys">Elastic APIキー</a>に関するドキュメントを参照してください。</p></li></ul><p><a href="https://cloud.elastic.co/registration">Elastic Cloud</a>での構築を今すぐ開始または継続しましょう。</p><h2>免責事項</h2><p>本記事に記述されているあらゆる機能ないし性能のリリースおよびタイミングは、Elasticの単独裁量に委ねられます。現時点で提供されていないあらゆる機能ないし性能は、すみやかに提供されない可能性、または一切の提供が行われない可能性があります。</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/elastic-cloud-api-keys-unified-serverless</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/elastic-cloud-api-keys-unified-serverless</guid>
    <category><![CDATA[Elastic Cloud Serverless]]></category>
    <category><![CDATA[開発者エクスペリエンス]]></category>
    <dc:creator><![CDATA[ Alex Chalkias]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt16ca1a6af7e5bab8/6a1707b7a6c2b900abe7965b/864e229f00eb2018084f13dd7f0e390e18383ed4-1980x1188.png" length="0" type="image/png"/>
    <pubDate>Mon, 20 Apr 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Elastic Workflowsを使用したKibanaのダッシュボード表示の監視]]></title>
    <description><![CDATA[Elastic Workflowsを使用して、Kibanaのダッシュボードのビューメトリクスを30分ごとに収集し、それらをElasticsearchにインデックスする方法を学びましょう。これにより、独自のデータ上にカスタム分析と可視化を構築できます。]]></description>
    <content:encoded><![CDATA[<p><a href="https://www.elastic.co/kibana">Kibana</a>は各ダッシュボードが何回閲覧されたかを追跡しますが、そのデータは組み込みのダッシュボードにはネイティブに公開されていません。この記事では、<strong>Elastic Workflows</strong>を使って30分ごとにそのデータを自動収集し、Elasticsearchにインデックス化して、その上に独自の分析を構築します。</p><p><a href="https://www.elastic.co/docs/explore-analyze/workflows">Elastic Workflows</a>はKibana内部に組み込まれた自動化エンジンで、シンプルなYAML構成を使用して多段階プロセスを定義できます。各ワークフローはスケジュールやイベント、または<a href="https://www.elastic.co/docs/explore-analyze/ai-features/elastic-agent-builder">Elastic Agent Builder</a>のツールとしてトリガーでき、各ステップでKibana APIを呼び出し、Elasticsearchをクエリし、データを変換できます。</p><p>具体的な例としてダッシュボードのビュー数を使用しますが、同じパターンがKibanaの保存済みオブジェクトAPIを通じて提供されるすべてのメトリックにも当てはまります。</p><h2>要件</h2><ul><li><p><a href="https://www.elastic.co/cloud">Elastic Cloud</a>または<a href="https://www.elastic.co/docs/deploy-manage/deploy/self-managed">セルフマネージド</a>クラスター（バージョン9.3を実行）</p></li><li><p><a href="https://www.elastic.co/docs/explore-analyze/workflows/get-started#workflows-prerequisites">ワークフローが有効</a>（詳細設定）</p></li></ul><h2>ステップ1：<a href="https://www.elastic.co/docs/explore-analyze/query-filter/tools/console">開発ツール</a>で生データを調べます</h2><p>何かを作る前に、どんなデータがあるのかを理解しましょう。Kibanaはほとんどの設定やメタデータを専用の内部インデックスに<a href="https://www.elastic.co/docs/api/doc/kibana/group/endpoint-saved-objects">保存済みオブジェクト</a>として保存しています。Kibanaがこの方法で追跡している項目の一つに、使用量カウンターと呼ばれる、特別な保存オブジェクトタイプを使ったダッシュボードの閲覧数があります。次のように、開発ツールから直接クエリできます。</p>GET kbn:/api/saved_objects/_find?type=usage-counter&amp;filter=usage-counter.attributes.domainId:"dashboard"%20and%20usage-counter.attributes.counterType:"viewed"&amp;per_page=10000<p>応答は下記のようになります。</p>{
  "page": 1,
  "per_page": 10000,
  "total": 1,
  "saved_objects": [
    {
      "type": "usage-counter",
      "id": "dashboard:346f3c64-ebca-484d-9d57-ec600067d596:viewed:server:20260310",
      "attributes": {
        "domainId": "dashboard",
        "counterName": "346f3c64-ebca-484d-9d57-ec600067d596",
        "counterType": "viewed",
        "source": "server",
        "count": 1
      },
      ...
    }
  ]<p><code>counterName</code>フィールドはダッシュボードIDであり、<code>count</code>はそのダッシュボードに対する当日中の累積閲覧数です。Kibanaは、1つのダッシュボードにつき1日に1つのカウンターオブジェクトを作成します。オブジェクトIDに日付サフィックス（...views:server:20260310）が表示されます。ユーザーがダッシュボードを開くにつれて、その数は一日を通して増加していきます。</p><p>この日常的なドキュメントモデルをインデックスで複製するのではなく、ワークフローの実行ごとに1つのドキュメントを作成します。各ドキュメントは、キャプチャの瞬間におけるその日のダッシュボードの累積ビュー数を記録します。</p><h2>ステップ2：宛先インデックスの作成</h2><p>ダッシュボードビューのスナップショットを格納するためのインデックスが必要です。次のコマンドは明示的なマッピングで作成し、後で集約や可視化ができるようにします。Dev Toolsで実行してください：</p>PUT dashboard-views
{
  "mappings": {
    "properties": {
      "captured_at": {
        "type": "date"
      },
      "dashboard_id": {
        "type": "keyword"
      },
      "dashboard_name": {
        "type": "keyword"
      },
      "view_count": {
        "type": "integer"
      }
    }
  }
}<p>IDと名前に<a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/keyword"><code>keyword</code></a>マッピングを使用すると、<a href="https://www.elastic.co/docs/explore-analyze/query-filter/aggregations">アグリゲーション</a>が可能になります。<code>view_count</code>のために<a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/number"><code>integer</code></a>を使用することは安全なデフォルト設定です。Kibanaはカウンターを毎日リセットするため、32ビットの制限（1日で20億回以上のビュー）に達することは現実的な懸念事項ではありません。数値演算も引き続きサポートしており、<a href="https://www.elastic.co/docs/reference/aggregations/search-aggregations-metrics-max-aggregation"><code>max</code></a>、<a href="https://www.elastic.co/docs/reference/aggregations/search-aggregations-metrics-avg-aggregation"><code>avg</code></a>、<a href="https://www.elastic.co/docs/reference/aggregations/search-aggregations-metrics-min-aggregation"><code>min</code></a>などが含まれます。</p><h2>ステップ3: ワークフローを作成する</h2><p><strong>Stack Management &gt; Workflows &gt; New Workflow</strong>に移動し、次のワークフローのYAML構成を貼り付けます。</p>name: dashboard-views-ingestion
triggers:
  - type: scheduled
    with:
      every: 30m

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

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

      - name: index_doc
        type: elasticsearch.request
        with:
          method: POST
          path: /dashboard-views/_doc
          body:
            dashboard_id: "{{ foreach.item.attributes.counterName }}"
            dashboard_name: "{{ steps.fetch_dashboard_name.output.attributes.title }}"
            view_count: "${{ foreach.item.attributes.count | plus: 0 }}"
            captured_at: "{{ execution.startedAt | date: '%Y-%m-%dT%H:%M:%SZ' }}"<p>次のセクションでは、ワークフローをステップごとに分解していきます。</p><h3>ワークフローの仕組み</h3><h4>トリガー</h4><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt7672aa533b4bc9ed/6a17dc5b420229d07c29f4d4/5670991d65c64ee833924225c2d375a1be868b13-325x162.png" alt=" スケジュールされたトリガー" /><p>ワークフローは30分ごとにスケジュールされたトリガーで実行されます。これにより、APIに負担をかけずに時系列データが得られます。</p><h4>ダッシュボードビューを取得</h4><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltab2a16f1f11304ea/6a17dc5d25daab26f608a117/66eaec147c3d01c524c67cf1c7f663ac56a3259d-812x215.png" alt=" ダッシュボードを取得" /><p><code>kibana.request</code>を使ってKibanaの保存済みオブジェクトAPIを呼び出します。認証の設定は不要です。ワークフローエンジンは実行コンテキストに基づいて適切なヘッダーを自動的に添付します。</p><h4>index_each_dashboard (foreach)</h4><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blte6b2611b0216555e/6a17dc5f445de95b584cffe5/aad45e8aed8dc81ded6260cd6199ff78dcffe3b4-1892x290.png" alt="各ダッシュボードをインデックス化" /><p>前のステップで返された <a href="https://www.elastic.co/docs/api/doc/kibana/group/endpoint-saved-objects"><code>saved_objects</code></a> 配列を反復します。各反復の現在の項目は <code>foreach.item</code>として利用可能です。ループ内では、各ダッシュボードごとに2つの入れ子手順を実行します。</p><p><strong>1. </strong><strong><code>fetch_dashboard_name</code></strong><strong>：</strong></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltb3733b3c24629abf/6a17dc60a292993fe3d02b75/db21ec5094b743018b9cd66c5052681f14c7d7e3-1999x431.png" alt="：ダッシュボード名を取得する" /><p>人間が読み取れるダッシュボードのタイトルを <code>GET /api/saved_objects/dashboard/{id}</code> を呼び出して解決します。<code>on-failure: continue: true</code> を追加することで、ダッシュボードが削除されてもビューカウンターが残っている場合、ループが継続して全体の実行が失敗しないようにします。</p><p><strong>2. </strong><strong><code>index_doc</code></strong><strong>：</strong></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt385c1f29d717c280/6a17dc62faa91353cb93c759/f49dd0c9f0817bb1e1e5d9f4a2b05d13ef331054-1999x626.png" alt=" Elasticsearchのリクエスト" /><p>各ドキュメントを<code>POST /dashboard-views/_doc</code>（明示的なIDなし）でインデックス化することで、Elasticsearchが自動でIDを生成します。これにより、毎回の実行時に新しいドキュメントが作成され、以前のスナップショットを上書きするのではなく、時間の経過とともにビューカウントの履歴が構築されます。</p><p>次の2点にご注意ください。</p><ul><li><p><code>captured_at</code>フィールドは日付フィルターを使ってタイムスタンプを<a href="https://www.iso.org/iso-8601-date-and-time-format.html">ISO 8601</a>としてフォーマットします。それなしでは、値はJavaScriptの日付文字列として出力され（例：<code>Tue Mar 10 2026 05:03:47 GMT+0000</code>）、Elasticsearchは日付としてマップしません。</p></li><li><p><code>view_count</code>は数値型を保持するために<code>${{ }}</code>構文を<code>| plus: 0</code>と共に用います。<code>{{ }}</code>を使うと文字列としてレンダリングされ、ダッシュボードでの計算操作ができなくなります。</p></li></ul><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt3b94bb6c22c0253e/6a17dc6425daab37a508a11b/6d48c8784d5df6192e8b5175e69dbab5098194bc-919x774.png" alt="" /><p><em>UIでは、各ワークフローステップを快適にデバッグできます。</em></p><h2>ステップ4：統計ダッシュボードを構築</h2><p>ワークフローが数回実行されてデータが収集されたら、dashboard-views Data viewを使用してKibanaで新しいダッシュボードを作成します。</p><p>まずは以下のパネルから始めましょう：</p><ul><li><p><strong>トップのダッシュボード（閲覧数別）：</strong> X軸に<code>dashboard_name</code>、Y軸に<code>last_value(view_count)</code>を持つ<a href="https://www.elastic.co/docs/explore-analyze/visualize/charts/bar-charts"><strong>棒グラフ</strong></a>を使用します。これはダッシュボードごとの現在の日次閲覧数を示しています。</p></li><li><p><strong>時間経過に伴うビュー：</strong>X軸に<code>captured_at</code>、Y軸に<code>last_value(view_count)</code>を用いた<a href="https://www.elastic.co/docs/explore-analyze/visualize/charts/line-charts"><strong>折れ線グラフ</strong></a>を使用し、<code>dashboard_name</code>で分類されます。各実行が新しいドキュメントを追加するため、重複を合計するのではなく、最後の値を使用して時間バケットごとのピークカウントを取得します。</p></li><li><p><strong>現在のスナップショット：</strong>すべてのダッシュボードの最新のビュー数を表示するには、最新の<code>captured_at</code> を含む<a href="https://www.elastic.co/docs/explore-analyze/visualize/charts/tables"><strong>データテーブル</strong></a>を使用します。</p></li></ul><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt18d0390e0526b215/6a17dc65a292991da7d02b79/e245b95f67daf76a2aaf4cb9df2c75ef4cfef582-1462x747.png" alt="" /><p>各ワークフローが新しいドキュメントを作成するため、時間範囲でフィルタリングして特定の期間のアクティビティを分析したり、週ごとの比較を行ったり、ダッシュボードのビューしきい値を下回ったときにアラートを設定したりすることができます。</p><h2><strong>まとめ</strong></h2><p>Elastic Workflowsは、ソース（Kibana API）と送信先（Elasticsearch）の両方がネイティブであるため、認証管理が全く必要なく、この種の定期的なデータ収集に適しています。ワークフローエンジンは<code>kibana.request</code>と<code>elasticsearch.request</code>のステップで認証を自動的に処理するため、記述するのはロジックだけです。</p><h2><strong>資料</strong></h2><ul><li><p><a href="https://www.elastic.co/docs/explore-analyze/workflows">Elastic Workflows</a></p></li><li><p><a href="https://www.elastic.co/docs/api/doc/kibana/">Kibana API</a></p></li></ul>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/monitor-kibana-dashboard-views-elastic-workflows</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/monitor-kibana-dashboard-views-elastic-workflows</guid>
    <category><![CDATA[開発者エクスペリエンス]]></category>
    <dc:creator><![CDATA[Gustavo Llermaly]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltef604bbb6dee6be0/6a17dc67a29299db23d02b7d/0ed94ce00962287b5507f45c92ecb60fdcbf2718-1280x720.png" length="0" type="image/png"/>
    <pubDate>Fri, 03 Apr 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Kubernetesでの依存関係管理]]></title>
    <description><![CDATA[Renovate CLIとArgoワークフローを使用してKubernetesの依存関係管理を効率化する方法。]]></description>
    <content:encoded><![CDATA[<p>以下は、Kubernetes、Argo Workflows、Argo Events、Renovate CLIを使用して、更新を自動化し、Common Vulnerabilities and Exposures（CVE）に迅速に対処し、何千ものリポジトリ全体で新しいパッケージバージョンを効率的に伝播するセルフホスト型の依存関係管理プラットフォームを構築した方法です。</p><h2><strong>Elasticでの依存関係管理</strong></h2><p>Elasticでは、プライベートとパブリックの両方で、数百、数千ものリポジトリを管理する必要があります。重大なCVEが発見された場合、どのリポジトリが脆弱であるかを即座に回答して対処する必要があります。どれくらい早くパッチを当てられるでしょうか。セキュリティとは別に、生産性に関する質問も出てきます。手動のタスクにあまり時間をかけずに、新しいパッケージバージョンのリリースを、それに依存するすべてのリポジトリに迅速に広めるにはどうすればよいでしょうか。</p><p>依存関係管理の方法を探す最初のきっかけは、<a href="https://www.elastic.co/blog/reducing-cves-in-elastic-container-images">CVE削減</a>のための自動更新を備えた安全な基盤を確立する必要性でした。依存関係管理に関するソリューションを慎重に検討した後、まずセルフホスト型のインフラストラクチャーの作業を開始しました。私たちは独自のKubernetesクラスターを使用して、Mend Renovate Community Self-Hostedを実行していました。ユーザーがセルフサービスでアクセスできる依存関係管理プラットフォームを提供するというアイデアがありました。</p><p>最初の実験が成功したため、より多くのチームが私たちのプラットフォームを導入し、日常のリポジトリのライフサイクルにおける更新やCVEパッチ適用に使用するようになりました。この展開は非常に速く、私たちはすぐにセルフマネージドインストールの限界に達しました。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc99617fc3eed538d/6a170ea9964cea459d08bc67/e14d9f98d4eccaa08a335d5bd23d88e5debbb344-1600x1103.png" alt="Elasticでの依存関係管理" /><h3><strong>課題：多数のリポジトリを持つ大規模な組織で依存関係管理プラットフォームを拡張するにはどうすればよいでしょうか。</strong></h3><p>当社の依存関係管理プラットフォームは、一度に1つのリポジトリを処理しており、シーケンシャルな処理モデルでは、当社の所有する多数のリポジトリに対応できませんでした。依存関係管理ツールの<strong>単一のインスタンス</strong>で拡大し続けるリポジトリのリストを処理するという概念に問題があることは、すでに認識していました。リポジトリはキュー内に留まり、場合によっては何時間も待機することがありました。リポジトリの50%以上は毎日処理されていませんでした。つまり、当社のリポジトリの50%以上がスキャン間でを24時間以上待っていたということです。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt0d205fd379e3c07a/6a170eab961e691e1fc4cfca/45ade5bda08f82bed0b3d0d3736cbd6f056e7a4e-1312x816.jpg" alt="依存関係管理の問題" /><p>大規模なリポジトリでは、コードベースのサイズが大きく、PRが複数オープンしているため、ボトルネックが大きくなります。GitHub Webhookイベントによりこのシーケンスが中断されました。スキャンのタイミングが予測できないため、Automergeの信頼性が低下しました。スキャンの頻度についてはユーザーと約束していましたが、それを果たすことができませんでした。</p><h3><strong>社内で構築するという決定：Elastic独自のスケールとセキュリティのニーズに対応</strong></h3><p>商用オプション、具体的には<strong>MendのRenovate Self-Hosted Enterprise Self-Hosted版</strong>も検討しましたが、Elastic社内ではいくつかの主要な取り組みが進行中でした。</p><p>社内プラットフォームを構築するという当社の決定は、Elastic の特定の譲れない要件を満たすには、徹底的にカスタマイズされたソリューションしかないという認識に基づいていました。</p><ol><li><p><strong>内部開発者プラットフォームへの投資：</strong>当時、私たちはすでに内部開発者プラットフォームに多額の投資を行っていました。それぞれのサービスをこれに適合させる方法について議論し、設計していました。つまり、依存関係管理プラットフォームの独自のルールと実践をテストドライブするというニーズがあり、それに加えて、新しいガイドラインが導入されることになり、イベントに先立ってプラットフォームを設計したいと考えていました。</p></li><li><p><strong>ネイティブ統合とワークフローのカスタマイズ：</strong>社内ツールや社内プロセスとの簡単な統合が必要で、例えば、Service Catalog（Backstage）を使用して構成をコードとして一元管理したいと考えていました。Backstageの使用に関しては、特定のニーズがあり、当社のプラットフォームと互換性を持たせたいと考えていました。したがって、Renovate Self-Hosted APIをBackstageの自動化と併用することは可能ですが、これでは当社の内部プロセスを完全にカバーすることはできません。</p></li><li><p><strong>Elastic特有の徹底したセキュリティ対策：</strong>当社の厳格なセキュリティコンプライアンスには、当社のエコシステムに合わせた特注のセキュリティメカニズムが必要でした。<a href="https://entro.security/blog/how-elastic-scaled-secrets-nhi-security-elastics-playbook-from-visibility-to-automation/">「非人間的アイデンティティ」の使用の強化</a>に取り組んでいました。このアクセス強化の仕組みにより、GitHub への認証を行う非標準の手段は、この内部実装をサポートしていない市販のツールでは機能しなくなります。当社のワークフローには、親子ワークフローの秘密の暗号化パターンを実装し、一時的な使い捨てのGitHubトークンを使用することが含まれていました。社内で構築することが、これらの独自のセキュリティレイヤーを組み込み、複雑なマルチクラウド環境全体の攻撃対象領域を最小限に抑える唯一の実用的な方法でした。</p></li></ol><h2><strong>解決策：依存関係管理のためのワークフローオーケストレーション</strong></h2><p>解決策の構築は、既に使用している依存関係管理ツールを基に構築し、それを置き換えたり他のソリューションを探したりするのではなく、その上に構築することから始まりました。その可能性の兆しはあり、その柔軟性は組織全体のさまざまなニーズにとって重要です。さまざまなソリューションを検討しましたが、最終的に決め手となったのは、カバーしなければならない大きくて時に特殊なニーズでした。私たちは、各リポジトリが独自に処理され、ボトルネックを解消して成長に備えられる、信頼性が高くスケーラブルな依存関係管理プラットフォームを構築することを決定しました。</p><p>プラットフォームは次の3つのコア原則に従って設計しました。</p><h3><strong>1. 並列処理</strong></h3><p>各リポジトリに独自の依存関係管理処理環境が与えられます。キューはなくなります。同時実行性は、消費するリソースの数によってのみ制限されます。また、GitHubでレート制限を受けないようにスマートな分散スケジューリングを適用しました。</p><h3><strong>2．セルフサービス可能</strong></h3><p>Service Catalog（Backstage）を使用して、新しいリポジトリを自動的にオンボードして管理します。独自のリソース定義を使用して、エンドユーザーにリポジトリの処理頻度を選択するオプション、スケジュールに割り当てるリソースの量、何らかの理由で処理をオンまたはオフにするオプションを提供します。ユーザーのニーズが進化し、新しいインストールに慣れてきたら、そのようにしてさらに多くのオプションを追加していく予定です。</p><h3><strong>3. シークレットのスコープと名前空間の分離の縮小</strong></h3><p>セキュリティを強化するために、各ワークフローの開始時に生成される一時的なGitHubトークンを依存関係管理ポッドに提供します。さらに、ワークロードを特定の名前空間に分離して、必要なシークレットのみが提供されるようにします。Kubernetes RBACを使用して、各依存関係管理ワークフローでアクセスできるシークレットを制御します。また、暗号化を使用して、親ワークフローから子ワークフローにGitHubトークンを伝播します。</p><p>Kubernetesを使用してプラットフォームを再構築し、Kubernetesのパワーを活用しました。Argo Workflowsはプロセスのロジックを強化し、Renovate CLIはリポジトリを一度に1つずつスキャンして処理するように設定されています。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt3548539ab52fbb79/6a170eac0c48573da601ab26/5560ed20e2bd9ecdd574a9c835126d12b24c332f-1600x1157.png" alt="Kubernetesにおける依存関係管理ワークフローの概要" /><p><strong>ここで素晴らしい点は</strong>、実績のあるオープンソースプロジェクトを独自の方法で使用し、すべてのプロジェクトに新しい実用的な例を提供すると同時に、開発速度を増幅し、チームのCVE削減を強化していることです。</p><h2><strong>依存関係管理アーキテクチャー：4つのマイクロサービス</strong></h2><p>このプラットフォームは、次の4つのカスタムビルドコンポーネントで構成されています。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt6451ff19da4db511/6a170eaec1e8a562e0f88378/2b3d4046c05bb261e45d40c59f864eb51fb9eaa9-1217x1600.png" alt="Kubernetesにおける依存関係管理のコンポーネント" /><h3><strong>ワークフローオペレーター（Go/Kubebuilder）</strong></h3><p>3つのカスタムリソース定義（CRD）を通じてワークフローライフサイクルを管理するKubernetesオペレーター：</p><ul><li><p><strong>RepoConfig CRD：</strong>リポジトリ設定のための信頼できる唯一の情報源。</p></li></ul><p>RepoConfigはオペレーターで次のように定義されています。</p>// RepoConfig is the Schema for the repoconfigs API
type RepoConfig struct {
	metav1.TypeMeta `json:",inline"`

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

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

	// status defines the observed state of RepoConfig
	// +optional
	Status RepoConfigStatus `json:"status,omitempty,omitzero"`
}<p>RepoConfigのインスタンスは次のようになります。</p>apiVersion: workflows.elastic.co/v1
kind: RepoConfig
metadata:
  generation: 3
  name: elastic-test-repo
  namespace: dependency-management-operator
spec:
  owner: group:my-team
  renovate:
    config:
      resourceGroup: SMALL
      runFrequency: 4h
    enabled: true
  repository: elastic/test-repo<ul><li><p><strong>親CRD：</strong>スケジュールされたスキャンのCronWorkflowを管理します。</p></li></ul><p>親コントローラの調整ループ内では、ワークフロー設定が作成され、最新の状態に保たれ、必要に応じて削除されることを確認します。</p><p>まず、ワークフローのグローバル設定を取得します。</p>func (r *ParentReconciler) reconcileSubResources(ctx context.Context, req ctrl.Request, parent *workflowsv1.Parent) error {
	logger := logf.FromContext(ctx)
	logger.Info("Reconcile SubResources for Parent", "name", req.NamespacedName)
	wfSet := workflowsettings.WorkflowSettings{
		RunFrequency:   parent.Spec.RunFrequency,
		ResourceGroups: "parent",
	}<p>mutexのconfigmapが最新であることを確認し、類似したワークフローが同時に動作しないようにします:</p>	cfMngr := resources.NewConfigMapManager(r.Client, r.Scheme, r.OperatorConfig.ParentNamespace)
	err := cfMngr.CreateOrUpdateSyncMutexConfigmap(ctx, fmt.Sprintf("%s%s", r.OperatorConfig.ResourcesPrefix, r.OperatorConfig.SyncMutexCfgMapName), strings.TrimPrefix(parent.Spec.Repository, "elastic/"), r.OperatorConfig.SemaphoreConcurrencyLimit)<p>そして、Cronワークフローとワークフローテンプレートを作成または更新する構造体であるワークフローマネージャーを作成します。</p>	wfMngr := resources.NewArgoWorkflowManager(r.Client,
		r.Scheme,
		curateResourceName(
			strings.ReplaceAll(parent.Spec.Repository, "/", "-"),
		),
		parent.Namespace,
		"parent-workflow",
		false).
		WithOrganization(r.OperatorConfig.GitHubOrg).
		WithRepoName(parent.Spec.Repository).
		Init(true, true).
		WithPrefix(r.OperatorConfig.ResourcesPrefix).
		WithWfTemplateName(r.OperatorConfig.ParentWorkflowTemplate).
		WithResources(wfSet.GetResourceCategory()).
		WithSchedule(wfSet.GetCronSchedule()).
		WithImagePullSecrets([]corev1.LocalObjectReference{{
			Name: r.OperatorConfig.WorkflowImagePullSecrets,
		}}).
		AddArgument(true, true, "extra_cli_args").
		SetArgument(true, false, "extra_cli_args", "none").
		AddTemplate(resources.NewParentDAGTemplateInstance()).
		AddTemplate(resources.NewWorkflowsTemplateInstance("check-child-workflows", r.OperatorConfig.WorkflowImagePullPolicy, r.OperatorConfig.WorkflowNodeSelector)).
		AddTemplate(resources.NewWorkflowsTemplateInstance("security", r.OperatorConfig.WorkflowImagePullPolicy, r.OperatorConfig.WorkflowNodeSelector)).
		AddTemplate(resources.NewWorkflowsTemplateInstance("submit-child-workflow", r.OperatorConfig.WorkflowImagePullPolicy, r.OperatorConfig.WorkflowNodeSelector))
	wfMngr.OverWriteCommand("submit-child-workflow", r.OperatorConfig.ChildNamespace)
	wfMngr.OverwriteWfTemplateName("parent-wftmpl")
	wfMngr.AddSynchronization(fmt.Sprintf("%s%s", r.OperatorConfig.ResourcesPrefix, r.OperatorConfig.SyncMutexCfgMapName), "{{workflow.parameters.repo_name}}")
	err = wfMngr.CreateOrUpdateCronWorkflow(ctx)
	if err != nil {
		return fmt.Errorf("failed to create or update cron workflow: %w", err)
	}
	err = wfMngr.CreateOrUpdateWorkflowTemplate(ctx)
	if err != nil {
		return fmt.Errorf("failed to create or update workflow template: %w", err)
	}
	return nil<ul><li><p><strong>子CRD：</strong>リポジトリごとのリソースを使用してWorkflowTemplatesを管理します。</p></li></ul><p>子コントローラにーは親と同様の調整義務がありますが、今回は親ワークフローによってトリガーされる子名前空間内のワークフローテンプレートを担当します。</p>func (r *ChildReconciler) reconcileSubResources(ctx context.Context, req ctrl.Request, child *workflowsv1.Child) error {
	logger := logf.FromContext(ctx)
	logger.Info("Reconcile SubResources for Child", "name", req.NamespacedName)
	wfSet := workflowsettings.WorkflowSettings{
		ResourceGroups: child.Spec.ResourceCategory,
	}
	wfMngr := resources.NewArgoWorkflowManager(r.Client,
		r.Scheme,
		curateResourceName(
			strings.ReplaceAll(child.Spec.Repository, "/", "-"),
		),
		child.Namespace,
		"runner",
		true).
		Init(false, true). // only manage workflow template
		WithPrefix(r.OperatorConfig.ResourcesPrefix).
		WithSuffix("-child-wftmpl").
		WithRepoName(child.Spec.Repository).
		WithOrganization(r.OperatorConfig.GitHubOrg).
		WithResources(wfSet.GetResourceCategory()). // will override resources of presets if set
		WithImagePullSecrets([]corev1.LocalObjectReference{{
			Name: r.OperatorConfig.WorkflowImagePullSecrets,
		}}).
		AddTemplate(resources.NewWorkflowsTemplateInstance("runner", r.OperatorConfig.WorkflowImagePullPolicy, r.OperatorConfig.WorkflowNodeSelector)).
		AddArgument(false, true, "repo_full_name").
		AddArgument(false, true, "repo_name").
		AddArgument(false, true, "encrypted_token").
		AddArgument(false, true, "extra_cli_args")
	wfMngr.OverWriteCommand("runner", r.OperatorConfig.ChildNamespace)
	err := wfMngr.CreateOrUpdateWorkflowTemplate(ctx)
	if err != nil {
		return fmt.Errorf("failed to create or update workflow template: %w", err)
	}
	return nil
}<img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blta735156ba6e370ef/6a170eaf7d8d6706fd70e7e4/7ac70492a1266ba02cb8afbafc5a486cb38a0edc-1600x1290.png" alt="Kubernetesにおける依存関係管理のワークフロー" /><p>マルチコントローラーパターンでは明確な分離が実現されます。RepoConfigコントローラーはオンボーディング／オフボーディングを処理し、親コントローラーはスケジュールを管理し、子コントローラーは実行テンプレートを処理します。</p><h3><strong>GitHuイベントゲートウェイ（Go）</strong></h3><p>GitHubのwebhookを受信し、署名を検証し、組織／リポジトリでフィルタリングし、Argo Eventsにルーティングするセキュアなwebhookプロキシです。依存関係ダッシュボードのインタラクション、PRイベント、パッケージの更新に対応する10個の異なるセンサーを構築しました。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt7e748ceb93c13a5a/6a170eb1a6c2b908f8e797b4/4828625456cbd6efa8020a20f10d23f294f98a02-1306x1600.png" alt="Kubernetesにおける依存関係ダッシュボードのインタラクション" /><p>このゲートウェイは、以下の方法でGitHub Appsとの統合を可能にします。</p><ul><li><p>セキュリティのため、受信したGitHub webhook署名を検証しています。</p></li><li><p>有効なイベントを、すべての関連ヘッダーと認証とともにArgo Events EventSourceに転送します。</p></li><li><p>また、EventSourceにAuthSecretを設定し、これを転送されるリクエストのBearerヘッダーとして提供します。</p></li><li><p>ログ、メトリクス、再試行ロジックを提供します。</p></li></ul><p>各GitHubイベントリクエストに対して様々な検証を行います。</p><p>以下のHTTP属性が存在することを確認します。</p>// ValidateRequestMethod checks if the request method is POST.
func ValidateRequestMethod(r *http.Request) error {
	if r.Method != http.MethodPost {
		return fmt.Errorf("method not allowed, only POST is accepted")
	}
	return nil
}

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

// ValidateUserAgent checks that the User-Agent header starts with GitHub-Hookshot/
func ValidateUserAgent(r *http.Request) error {
	userAgent := r.Header.Get("User-Agent")
	if !strings.HasPrefix(userAgent, "GitHub-Hookshot/") {
		return fmt.Errorf("invalid User-Agent")
	}
	return nil
}<p>また、各リクエストの署名とその構成も検証します。</p>// ValidateSignature verifies the GitHub webhook signature.
func ValidateSignature(r *http.Request, secret string) ([]byte, error) {
	payload, err := GitHub.ValidatePayload(r, []byte(secret))
	if err != nil {
		return nil, fmt.Errorf("invalid GitHub signature: %w", err)
	}
	return payload, nil
}

// ValidateAllowedOwner checks if the organization login is in the allowed organizations list.
func ValidateAllowedOwner(payload []byte, allowedGitHubOrganizations []string) (string, error) {
	var orgLogin string
	var payloadMap map[string]any
	if err := json.Unmarshal(payload, &amp;payloadMap); err == nil {
		if orgObj, ok := payloadMap["organization"].(map[string]any); ok {
			if login, ok := orgObj["login"].(string); ok {
				orgLogin = login
			} else if name, ok := orgObj["name"].(string); ok {
				orgLogin = name
			}
		}
	}
	if !slices.Contains(allowedGitHubOrganizations, orgLogin) {
		return orgLogin, fmt.Errorf("organization login not allowed")
	}
	return orgLogin, nil
}<p>最後に、イベントの種類に基づいてArgo Eventsにルーティングします。</p>	// Map eventType to Argo `EventSource` path
	var endpoint string
	switch eventType {
	case "push":
		endpoint = "/push"
	case "issues":
		endpoint = "/issues"
	case "pull_request":
		endpoint = "/pull-requests"
	default:
		slog.Info("Ignoring unhandled event type", "event_type", eventType, "delivery_id", deliveryID)
		w.WriteHeader(http.StatusOK)
		_,  = w.Write([]byte("ok"))
		return
	}
	forwardURL := h.config.ArgoEventSourceForwardURL + endpoint<p>Argo Events側では、10個のセンサーがArgo Events EventBusで新しいイベントを監視します。</p>apiVersion: argoproj.io/v1alpha1
kind: Sensor
metadata:
  name: {{ .Values.sensors.packageUpdateOnDefaultBranch.name }}
  namespace: {{ .Release.Namespace }}
spec:
  eventBusName: {{ .Values.eventBus.name }}<p>次に、スクリプトは各センサーのロジックを適用します。</p>script: |
          local e = event
          if not e or not e.body or not e.body.repository then
            return false
          end

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

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

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

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

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

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

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

          return false<h3><strong>Backstage Syncer（Go）</strong></h3><p>このプロセスでは、Service Catalog（Backstage）に対してRepository Real Resource Entitiesのポーリングを行い、それらをRepoConfig CRDに変換し、プラットフォームを設定変更と同期させます。変更は3分以内に適用されます。</p>repoMap := make(map[string]map[string]interface{})
			for i := range entities {
				entity := &amp;entities[i]
				if entity.Spec.Type != "GitHub-repository" {
					continue
				}

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

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

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

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

				workflowsMap := map[string]interface{}{
					"enabled":        workflowsWithDefaults.Enabled,
					"require_pr":     workflowsWithDefaults.RequirePr,
					"resource_group": string(workflowsWithDefaults.ResourceGroup),
					"run_frequency":  string(workflowsWithDefaults.RunFrequency),
				}
				repoMap[repoName] = map[string]interface{}{
					"renovate": workflowsMap,
					"owner":    entity.Spec.Owner,
				}
			}
			logger.Info("Fetched GitHub Repository data from Backstage", "repository_count", len(repoMap), "status_code", resp.StatusCode)<p>最後に、そのデータをRepoConfigインスタンスに書き込みます。</p><h3><strong>ワークフローベース（混合：JavaScript、Go、Helm）</strong></h3><p>基盤レイヤーには、Helmチャート、JavaScript設定、暗号化サポート付きのRenovate CLI用のGoラッパー、Alpineパッケージ用のカスタムAPKインデクサーが含まれています。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt4c1b5b840854ddf5/6a170eb47d8d67694e70e7e8/908d19278face3ce1119dbee9146c1264b6e2f30-1600x873.png" alt=" Kubernetesにおける依存関係管理の基礎コンポーネント" /><h2><strong>セルフサービス構成</strong></h2><p>チームはBackstageを通じてリポジトリを宣言的に設定します。</p>spec:
  renovate:
    enabled: true
    config:
      resourceGroup: LARGE      # SMALL | MEDIUM | LARGE  
      runFrequency: "0 */4 * * *"  # Every 4 hours<p>リソースグループはリポジトリサイズに基づいてCPUとメモリを割り当てます。</p><ul><li><p><strong>SMALL:</strong> 500m CPU、1Giメモリ。</p></li><li><p><strong>MEDIUM:</strong> 1000m CPU、2Giメモリ。</p></li><li><p><strong>LARGE:</strong> 2000m CPU、4Giメモリ。</p></li></ul><p>構成はバージョン管理され、監査可能で、自動的に適用されます。</p><h2><strong>親子パターン</strong></h2><p>実行モデルは親子ワークフローパターンを使用しています。</p><ul><li><p><strong>親ワークフロー：</strong>スケジュールに従って実行される軽量のCronワークフロー。シークレットを暗号化し、スキャンを実行するかどうかを決定し、子プロセスに構成を渡します。</p></li><li><p><strong>子ワークフロー：</strong>Renovate CLIが実行される一時的なポッド。リソースを動的に割り当て、シークレットを単独で復号し、完了後に終了します。</p></li></ul><p>この分離により、セキュリティ（親レベルで暗号化されたシークレット）、リソースの最適化（親は最小限のリソースを使用）、拡張性（子が並行して実行）が提供されます。</p><h2><strong>結果</strong></h2><h3><strong>パフォーマンスの変化</strong></h3><ul><li><p><strong>変更前：</strong>一度に1つのリポジトリで、一部のリポジトリは、おそらく1日以上、1日あたり1,000回未満のスキャンでも処理されませんでした。</p></li><li><p><strong>変更後：</strong>100以上の同時スキャン、通常は8,000件のスキャンと1日あたり最大10,000件の記録スキャン。制限は、当社が費やすリソースの量とGitHubのレート制限の扱い方のみです。</p></li></ul><h3><strong>費用対効果</strong></h3><p>奇妙に聞こえるかもしれませんが、1 日に8,000件のポッドを実行すると、同じ結果を達成するために 1 つの長時間実行ポッドを実行するよりもはるかに安価に同じ結果を得ることができます。</p><p>以前の設定では、単一のインスタンスを実行していて、調子が良い日には500～600回のスキャンを実行していました。同時に、異なる種類のリポジトリが同じポッドで実行されることから、最大のものに合わせてポッドのサイズを設定する必要がありました。そのサイズは、現在の8つのCPUと16GBのメモリを搭載する特大モデルよりもはるかに大きくなります。</p><p>現在の日次出力を満たすには、単一のポッドなら12日間実行する必要があります。それでは、12日間稼働する単一のポッドのコストを、毎日稼働する8,000個の「MEDIUM」サイズのポッドのコストと比較すると、新しい設計は同じスキャン出力に対してはるかに効率的です。</p><p>メトリック</p><p>シナリオ A（ワークフロー）</p><p>シナリオB（長時間実行される単一のポッド）</p><p>セットアップ</p><p>8,000ポッド（1 vCPU／2GB）</p><p>1 pod (8 vCPU / 16 GB)*</p><p>期間</p><p>それぞれ10分</p><p>12日間連続</p><p>総作業時間</p><p>1,333計算時間</p><p>288計算時間</p><p>総コスト</p><p>65.83ドル</p><p>$113.75</p><p>しかし、ワークロードのデフォルト設定が「SMALL」に設定されていることを考慮に入れましょう。大多数は0.5 CPUと1G RAMで正常に実行されており、規模を増やす変更する必要があるのはごく一部です。ワークロードの60％が「SMALL」で実行され、30％が「MEDIUM」で実行され、10％が「LARGE」で実行されている場合（これが実際に近い値です）に何が起こるかを見てみましょう。</p><p>メトリック</p><p>シナリオA（混合群）</p><p>シナリオB（長時間実行）</p><p>戦略</p><p>8,000ポッド（混合サイズ）</p><p>1 pod (8 vCPU / 16 GB)*</p><p>期間</p><p>それぞれ10分</p><p>12日間連続</p><p>総コスト</p><p>$52.66</p><p>$113.75</p><p>節約額</p><p>$61.09（54%安価）</p><p>—</p><p>同じ出力に対して、現在の設定ではるかに費用対効果に優れていることがわかります。</p><h3><strong>強化されたセキュリティ</strong></h3><ul><li><p>一時的なGitHubトークン（数分間の公開と数日間の公開の比較）。</p></li><li><p>ロールベースアクセス制御（RBAC）境界による名前空間の分離。</p></li><li><p>親ワークフロー内の保存時のシークレットの暗号化。</p></li><li><p>直接のVaultアクセスを削除。</p></li></ul><h3><strong>予測可能なパフォーマンス</strong></h3><p>スキャン頻度が保証されれば、最終的にサービスレベル目標（SLO）を設定できます。Automergeは確実に動作します。チームは、プラットフォームが約束どおりの成果をもたらすと信頼しています。</p><h2><strong>重要なアーキテクチャ上の決定</strong></h2><p>ここでは、プラットフォームの外観を形作る重要な設計上の決定事項をいくつか紹介します。</p><ul><li><p><strong>親子ワークフローが必要な理由</strong></p></li></ul><p><strong>多層防御</strong>戦略を実施するためにこのパターンを採用しました。価値の高い認証情報（GitHubアプリのシークレットなど）を専用のロックダウンされた名前空間に制限することで、<strong>RBAC</strong>を使用して、一時的な実行ポッドが機密データに恣意的にアクセスできないようにします。最近のサプライチェーンの脆弱性（例えば、<strong>"Shai Hulud"</strong>継続的インテグレーション/継続的デリバリー[CI/CD]攻撃）は、動的スクリプトを実行するランタイム環境を認証情報ストアから分離することの重要性を実証しています。</p><p>同時に、この分離により、<strong>きめ細かなリソースの最適化</strong>が可能になります。「親」ワークフローは軽量のオーケストレーターとして機能し、最小限のフットプリントで動作します。一方、「子」ワークフローは計算集約型の依存関係スキャンを処理します。この分離により、各レイヤーに個別の調整ロジックを適用し、スケジューリングとセキュリティインフラ（親）の管理制御を維持しながら、実行パラメータ（子）の制御をユーザーに許可することで、<strong>ライフサイクル管理</strong>を簡素化できます。</p><ul><li><p><strong>セルフサービス可能な理由</strong></p></li></ul><p>リポジトリ構成のボトルネックとなる当社のチームを排除することは重要な要件でした。私たちの使命は、多様なユースケースをサポートできるスケーラブルな<strong>セルフサービスプラットフォーム</strong>を設計することでした。リポジトリの膨大な量を考慮すると、すべての構成変更の<strong>ゲートキーパー</strong>として機能することは持続不可能であると認識しました。代わりに、私たちは「レール」（インフラストラクチャと<strong>ガードレール</strong>）を提供しながら、ユーザーが「列車」（実行とカスタマイズ）を運転できるようにするという支援の哲学を採用しました。こうした<strong>チームの自律性</strong>へのシフトが、ユーザーがシステムをそれぞれの具体的な業務ニーズに合わせてカスタマイズできるようにすることで、生産性を大幅に向上させると信じています。</p><ul><li><p><strong>Kubernetes Operatorパターンを選択した理由</strong></p></li></ul><p>上記で述べたように、基本的な設計原則は、プラットフォームが完全に<strong>セルフサービス可能</strong>であることを確保することでした。ユーザーの意図（スキャンの切り替え、スケジューリング頻度の調整、ランタイムリソース制限の調整など）を捉え、それらの変更を基盤となるワークフローに瞬時に反映する自動化されたメカニズムが必要でした。将来の要件を予測し、システムは簡単に<strong>拡張可能</strong>である必要もありました。</p><p>これを実現するために、カスタムの<strong>依存関係管理Kubernetes Operator</strong>を開発しました。設定のインターフェースとして<strong>CRD</strong>を使用することで、<strong>Kubernetesネイティブな調整ループ</strong>を確立しました。このオペレーターは、ユーザーが定義した望ましい状態を継続的に監視し、ワークフローインフラに必要な更新を自動的に管理します。これにより、<strong>イベント駆動型</strong>のシームレスな操作が保証され、プラットフォームのロジックが裏側ですべての複雑さを処理します。</p><ul><li><p><strong>GitHubイベントゲートウェイを設計する理由</strong></p></li></ul><p>プラットフォームの応答性を高めるために、<strong>イベントドリブンアーキテクチャー（EDA）</strong>の採用が不可欠でした。CronWorkflowsは信頼性の高いベースラインスケジュールを提供しましたが、ユーザーがダッシュボードから手動でスキャンをトリガーするなど<strong>アドホック実行</strong>を処理できる俊敏性が必要でした。これを達成するために、ペイロードの整合性を検証し、リクエストをインテリジェントにルーティングするための専用の<strong>インジェストゲートウェイ</strong>が必要でした。</p><p>既存のソリューション、特にArgoのネイティブGitHub EventSourceを評価しましたが、 <strong>運用上のオーバーヘッド</strong>や厳格な <strong>GitHub APIクォータ</strong>（例：リポジトリごとのwebhook制限）に関する重大なリスクを特定しました。結果として、これらの制限からインフラを切り離すためにカスタムゲートウェイを構築しました。</p><p>重要なのは、このゲートウェイが移行中に戦略的な<strong>トラフィック制御ポイント</strong>として機能したことです。これはスイッチとして機能し、レガシーシステムから新しいインフラストラクチャへの<strong>段階的かつ詳細なロールアウト</strong>（トラフィックの移行）を実行できるようになりました。これにより、数千のリポジトリのオンボーディングが「ビッグバン」的な切り替えではなく、制御されたリスクのないプロセスになることが保証されました。</p><p></p><h2><strong>教訓</strong></h2><p>私たちが学んだいくつかの教訓は <a href="https://www.elastic.co/about/our-source-code">Elasticソースコード</a>と密接に関連しています。</p><ol><li><p><strong>顧客第一：</strong>プラットフォームはユーザーのために構築されます。したがって、ユーザーのニーズを最優先にすることが重要です。これにより、プラットフォームは、ユーザーとの摩擦を減らし、プラットフォームの拡張を簡素化し、導入を容易にする、効率的に設計されたインフラストラクチャとアプリケーションに形作られます。</p></li><li><p><strong>空間と時間：</strong>時には、最も抵抗の少ない道が<strong>不安定な状況</strong>につながることがあります。当初、既存の順次処理モデルを最適化しようとしましたが、問題は解決されず、むしろ複雑さが増し、未解決の問題が増えただけでした。プラットフォームを並列処理で<strong>再構築</strong>するという大胆な決定には、多大な事前開発が必要でしたが、最終的には持続可能なプラットフォームの成長への道を切り開き、日々の面倒な管理業務を事実上排除しました。</p></li><li><p><strong>ITと依存関係：</strong>プラットフォームは単独では動作できません。その成功は、より広範なエコシステムとどれだけうまく統合できるかによって決まります。当社の場合、シームレスなサービスオンボーディングのための信頼できる情報源となるため、<strong>Backstage</strong>との統合は極めて重要でした。同様に、<strong>Artifactory</strong>に接続することで、プライベートパッケージの更新を効率的に管理できるようになります。他にも重要な統合は多々あります。</p></li><li><p><strong>進歩、シンプルな完璧さ：</strong>実装全体を通じて、当初の想定を継続的にプレッシャーテストし、新たな障壁が出現するたびにそれに適応しました。完璧主義に陥るのではなく、<strong>反復的なアプローチ</strong>を採用し、課題に一つずつ取り組み、実際の状況に合わせて移行戦略を調整しました。</p></li></ol><h2><strong>次のステップ</strong></h2><p>このプラットフォームの提供により、プラットフォームのUXと効率性の向上に役立つ、より有意義な作業が可能になります。いくつかの例を以下に示します。
</p><ul><li><p><strong>自動マージの採用を増やし、ガードレールを設定</strong></p></li></ul><p>自動マージ機能により、面倒な手動タスクが排除され、チームの速度が大幅に向上します。しかし、この速度の向上が安全性を犠牲にすることのないよう、厳格な<strong>ガードレールを</strong>確実に設置する必要があります。
</p><ul><li><p><strong>エンドユーザーエクスペリエンスに関するオブザーバビリティの向上</strong></p></li></ul><p>私たちのロードマップにおける重要な優先事項は、プラットフォームレベルだけでなく、特に<strong>エンドユーザー</strong>の視点からオブザーバビリティを高めることです。インフラの指標を捉えるのは簡単ですが、実際のユーザー体験を理解するにはより深い洞察が必要です。コアユーザー中心の重要業績評価指標（KPI）を定義し、テレメトリが<strong>エスカレート</strong>する前の摩擦点やパフォーマンスの問題を検出できるように取り組んでいます。</p><ul><li><p><strong>より広範な採用を促進するための障壁の除去</strong></p></li></ul><p>将来を見据えた当社の優先事項は、プラットフォームの採用を妨げている障壁を特定して取り除くことです。新しい統合の開発や特定の機能セットの展開が必要な場合でも、当社はデータ主導の計画に取り組んでいます。当社は拡張性を重視したプラットフォームの構築に成功しました。今後は、<strong>その潜在能力を最大限に引き出す</strong>ことに注力していきます。
</p><h2><strong>全体像</strong></h2><p>依存関係管理ワークフロープロジェクトは、より広範な原則を示しています。つまり、<strong>デフォルトの導入モデルを超えてオープンソースツールをスケールする必要がある場合、Kubernetesネイティブパターンが前進する道を提供する</strong>ということです。</p><p>以下を取り入れることで、</p><ul><li><p>構成用のCRD</p></li><li><p>ライフサイクル管理のためのオペレーター</p></li><li><p>応答性を高めるイベント駆動型アーキテクチャ</p></li><li><p>導入用のGitOps</p></li></ul><p>管理するリポジトリの数に関係なく拡張できるオーケストレーションを構築しました。1つのリポジトリをスキャンするパフォーマンスは、管理するリポジトリが100個でも1,000個でも同じです。</p><p>重要なCVEが発表されても、数時間ではなく数分で回答が得られます。それがボトルネックと競争優位の違いです。</p><h2><strong>謝辞</strong></h2><p>このプラットフォームは優れたオープンソースツールを基盤としています。</p><ul><li><p><strong>Kubebuilder：</strong>ワークフローをブートストラップして管理するKubernetes Operatorを起動するために使用したオープンソースフレームワーク。[<a href="https://github.com/kubernetes-sigs/kubebuilder">1</a>][<a href="https://book.kubebuilder.io/">2</a>]</p></li><li><p><strong>Backstage：</strong>Service Catalogを構築し、信頼できる情報源として使用するオープンソースフレームワーク。[<a href="https://github.com/backstage/backstage">1</a>][<a href="https://backstage.io/">2</a>]</p></li><li><p><strong>Argo WorkflowsとArgo Events：</strong>複雑なプロセスを調整し、イベントに基づいて動的な処理を追加するために使用したオープンソーススイート。[1][<a href="https://argo-workflows.readthedocs.io/en/stable/">2</a>][<a href="https://argoproj.github.io/argo-events/">3</a>][<a href="https://github.com/argoproj/argo-events">4</a>]</p></li><li><p><strong>Renovate CLI：</strong>リポジトリを処理するオープンソースの依存関係管理ツール。[<a href="https://github.com/renovatebot/renovate">1</a>][<a href="https://docs.renovatebot.com/getting-started/running/">2</a>]</p></li></ul><p>* AWS Fargateの料金モデルを単一ポッドのコストの基準として使用しました。ただし、当社のワークロードは必ずしもAWSで実行されているわけではなく、本格的なKubernetesクラスターで実行されています。</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/dependency-management-kubernetes</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/dependency-management-kubernetes</guid>
    <category><![CDATA[開発者エクスペリエンス]]></category>
    <dc:creator><![CDATA[Nikos Fotiou]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt6033995d6660d149/6a170eb5839dfa63f1dcff9b/00519840e6eec7101c1fb096afcae976ee0c454e-1280x720.png" length="0" type="image/png"/>
    <pubDate>Thu, 19 Feb 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Kibana に Elasticsearch クエリルール UI を導入]]></title>
    <description><![CDATA[Elasticsearch クエリ ルール UI を使用して、オーガニック ランキングに影響を与えずに Kibana のカスタマイズ可能なルールセットを使用して検索クエリにドキュメントを追加または除外する方法を学びます。]]></description>
    <content:encoded><![CDATA[<p>検索エンジンの役割は、関連性のある結果を返すことです。ただし、セールの強調、季節商品の優先、スポンサー商品の展示など、それ以上のビジネスニーズがあり、開発者は検索クエリでこれを常に実行できるとは限りません。</p><p>さらに、これらのユースケースは通常、時間に敏感であり、一般的な開発段階 (コード ブランチを作成してから新しいリリースを待つ) を実行するのは時間のかかるプロセスです。</p><p>では、このプロセス全体を API 呼び出しだけで、あるいは Kibana で数回クリックするだけで実行できたらどうなるでしょうか?</p><h2>クエリルールUI</h2><p>Elasticsearch 8.10 では、<a href="https://www.elastic.co/blog/introducing-query-rules-elasticsearch-8-10"><strong>クエリ ルール</strong></a>と<a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/retrievers/rule-retriever"><strong>ルール リトリーバー</strong></a>が導入されました。これらは、ルールに基づいてオーガニック検索結果のランキングに影響を与えずに、<a href="https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-pinned-query"><em>ピン留めされた結果を</em></a>クエリに挿入するように設計されたツールです。宣言的かつシンプルな方法で、結果の上にビジネス ロジックを追加するだけです。</p><p>クエリ ルールの一般的な使用例は次のとおりです。</p><ul><li><p><strong>プロモーション対象商品やセール品の強調表示</strong>: セール中の商品やスポンサー商品を上部に表示します。</p></li><li><p><strong>コンテキストまたは地理位置情報による除外</strong>: 地域の規制により表示が許可されていない場合は、特定のアイテムを非表示にします。</p></li><li><p><strong>主要な結果を優先する</strong>: オーガニックランキングに関係なく、人気のある検索や固定検索が常に上位に表示されるようにします。</p></li></ul><p>インターフェースにアクセスしてこれらのツールを操作するには、Kibana サイドメニューをクリックし、関連性の下にある<strong>クエリルール</strong>に移動する必要があります<strong>。</strong></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltac12541cddd58e36/6a170853a29299941cd00fc2/242e33e89d1a07ffa0e76009c46b3a9236722741-458x1010.png" alt="Elasticsearchの関連性に基づくクエリルールへのアクセス" /><p>クエリ ルール メニューが表示されたら、<strong>最初のルール セットの作成をクリックします。</strong></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltcc28329c0f3c3aa9/6a17085547d49c67e22d893b/30b3a91bbbf243d314cf38298e01ca5cff784430-1600x945.png" alt="Elasticsearchで最初のクエリルールルールセットを作成する" /><p>次に、ルールセットに名前を付ける必要があります。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltb37d271297a4f148/6a170856a29299782cd00fc6/26c5462f88678867776f933b5655ca0df0d72a16-708x446.png" alt="Elasticsearch でのクエリルールのルールセットの命名" /><p>各ルールを定義するフォームには、次の 3 つの主要コンポーネントがあります。</p><ul><li><p><strong>基準</strong>: ルールを適用するために満たす必要がある条件。たとえば、「query_string フィールドに値<em>Christmas</em>が含まれている場合」や「country フィールドに<em>CO の場合」などです。</em></p></li><li><p><strong>アクション</strong>: これは、条件が満たされたときに発生する動作です。ピン留め（ドキュメントを上位の結果に固定する）したり、除外（ドキュメントを非表示にする）したりできます。</p></li><li><p><strong>メタデータ</strong>: これらはクエリの実行時にクエリに付随するフィールドです。これらには、ユーザーの情報 (場所や言語など) や検索データ (query_string) を含めることができます。これらは、ルールを適用するかどうかを決定するための基準で使用される値です。</p></li></ul><h2>例: 人気商品</h2><p>さまざまな商品を扱う電子商取引サイトがあると想像してみましょう。指標をチェックすると、コンソール カテゴリで最も売れているアイテムの 1 つが「DualShock 4 ワイヤレス コントローラー」であることがわかります。特に、ユーザーが「PS4」または「PlayStation 4」というキーワードを検索した場合に多く見られます。そこで、ユーザーがこれらのキーワードを検索するたびに、この製品を結果の最上位に表示することにしました。</p><p>まず、Bulk API リクエストを使用して各アイテムのドキュメントをインデックス化します。</p>POST _bulk
{ "index": { "_index": "products", "_id": "1" } }
{ "id": "1", "name": "PlayStation 4 Slim 1TB", "category": "console", "brand": "Sony", "price": 1200 }
{ "index": { "_index": "products", "_id": "2" } }
{ "id": "2", "name": "DualShock 4 Wireless Controller", "category": "accessory", "brand": "Sony", "price": 250 }
{ "index": { "_index": "products", "_id": "3" } }
{ "id": "3", "name": "PlayStation 4 Camera", "category": "accessory", "brand": "Sony", "price": 200 }
{ "index": { "_index": "products", "_id": "4" } }
{ "id": "4", "name": "PlayStation 4 VR Headset", "category": "accessory", "brand": "Sony", "price": 900 }
{ "index": { "_index": "products", "_id": "5" } }
{ "id": "5", "name": "Charging Station for DualShock 4", "category": "accessory", "brand": "Sony", "price": 80 }<p>クエリに介入しない場合、アイテムは通常 4 番目の場所に表示されます。クエリは次のとおりです。</p>GET products/_search
{
 "query": {
   "match": {
     "name": "PlayStation 4"
   }
 }
}<p>そして結果はこちらです</p>{
 "took": 1,
 "timed_out": false,
 "_shards": {
   "total": 1,
   "successful": 1,
   "skipped": 0,
   "failed": 0
 },
 "hits": {
   "total": {
     "value": 5,
     "relation": "eq"
   },
   "max_score": 0.6973252,
   "hits": [
     {
       "_index": "products",
       "_id": "3",
       "_score": 0.6973252,
       "_source": {
         "id": "3",
         "name": "PlayStation 4 Camera",
         "category": "accessory",
         "brand": "Sony",
         "price": 200
       }
     },
     {
       "_index": "products",
       "_id": "1",
       "_score": 0.6260078,
       "_source": {
         "id": "1",
         "name": "PlayStation 4 Slim 1TB",
         "category": "console",
         "brand": "Sony",
         "price": 1200
       }
     },
     {
       "_index": "products",
       "_id": "4",
       "_score": 0.6260078,
       "_source": {
         "id": "4",
         "name": "PlayStation 4 VR Headset",
         "category": "accessory",
         "brand": "Sony",
         "price": 900
       }
     },
     {
       "_index": "products",
       "_id": "2",
       "_score": 0.08701137,
       "_source": {
         "id": "2",
         "name": "DualShock 4 Wireless Controller",
         "category": "accessory",
         "brand": "Sony",
         "price": 250
       }
     },
     {
       "_index": "products",
       "_id": "5",
       "_score": 0.07893815,
       "_source": {
         "id": "5",
         "name": "Charging Station for DualShock 4",
         "category": "accessory",
         "brand": "Sony",
         "price": 80
       }
     }
   ]
 }
}<p>これを変更するためのクエリ ルールを作成しましょう。まず、次のようにルールセットに追加しましょう。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1576d4f4a2e60548/6a170858cdacbfccb07d298d/fdc42646fb3e76a09bca7d19047a76efe343f7a2-1600x650.png" alt="Elasticsearchでクエリルールのルールセットを編集する方法" /><p>または同等の<a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-query-rules-put-ruleset">API リクエスト</a>:</p>PUT _query_rules/my-rules
{
  "rules": [
    {
      "rule_id": "rule-1232",
      "type": "pinned",
      "criteria": [
        {
          "type": "exact",
          "metadata": "query_string",
          "values": [
            "PS4",
            "PlayStation 4"
          ]
        }
      ],
      "actions": {
        "docs": [
          {
            "_index": "products",
            "_id": "2"
          }
        ]
      }
    }
  ]
}<p>クエリで<strong>ルールセット</strong>を使用するには、クエリ ルール タイプを使用する必要があります。この種のクエリは、主に次の 2 つの部分で構成されます。</p>GET /products/_search
{
 "retriever": {
   "rule": {
     "retriever": {
       "standard": {
         "query": {
           "match": { "name": "PlayStation 4" }
         }
       }
     },
     "match_criteria": {
       "query_string": "PlayStation 4"
     },
     "ruleset_ids": ["my-rules"]
   }
 }
}<ul><li><p><strong>match_criteria</strong> : ユーザーのクエリと比較するために使用されるメタデータです。この例では、query_string フィールドの値が「PlayStation 4」の場合にルールセットがアクティブになります。</p></li><li><p><strong>query</strong> : 検索してオーガニック検索結果を取得するために使用される実際のクエリ。</p></li></ul><p>この方法では、最初にオーガニッククエリを実行し、次に Elasticsearch がルールセットのルールを適用します。</p>{
 "took": 17,
 "timed_out": false,
 "_shards": {
   "total": 1,
   "successful": 1,
   "skipped": 0,
   "failed": 0
 },
 "hits": {
   "total": {
     "value": 5,
     "relation": "eq"
   },
   "max_score": 1.7014122e+38,
   "hits": [
     {
       "_index": "products",
       "_id": "2",
       "_score": 1.7014122e+38,
       "_source": {
         "id": "2",
         "name": "DualShock 4 Wireless Controller",
         "category": "accessory",
         "brand": "Sony",
         "price": 250
       }
     },
     {
       "_index": "products",
       "_id": "3",
       "_score": 0.6973252,
       "_source": {
         "id": "3",
         "name": "PlayStation 4 Camera",
         "category": "accessory",
         "brand": "Sony",
         "price": 200
       }
     },
     {
       "_index": "products",
       "_id": "1",
       "_score": 0.6260078,
       "_source": {
         "id": "1",
         "name": "PlayStation 4 Slim 1TB",
         "category": "console",
         "brand": "Sony",
         "price": 1200
       }
     },
     {
       "_index": "products",
       "_id": "4",
       "_score": 0.6260078,
       "_source": {
         "id": "4",
         "name": "PlayStation 4 VR Headset",
         "category": "accessory",
         "brand": "Sony",
         "price": 900
       }
     },
     {
       "_index": "products",
       "_id": "5",
       "_score": 0.07893815,
       "_source": {
         "id": "5",
         "name": "Charging Station for DualShock 4",
         "category": "accessory",
         "brand": "Sony",
         "price": 80
       }
     }
   ]
 }
}<h2>例: ユーザーベースのメタデータ</h2><p>クエリ ルールのもう 1 つの興味深い応用は、メタデータを使用して、ユーザーまたは Web ページからのコンテキスト情報に基づいて特定のドキュメントを表示することです。</p><p>たとえば、数値として表されるユーザーのロイヤルティ レベルに基づいて、アイテムやカスタマイズされたセールを強調表示したいとします。</p><p>これを実現するには、このメタデータをクエリに直接取り込んで、その値が特定の基準を満たしたときにルールがアクティブになるようにします。</p><p>まず、ロイヤルティ レベルの高いユーザーだけが閲覧できるドキュメントをインデックスします。</p>POST _bulk
{ "index": { "_index": "products", "_id": "6" } }
{ "id": "6", "name": "PlayStation Plus Deluxe Card - 12 months", "category": "membership", "brand": "Sony", "price": 300 }<p>ここで、同じルールセット内に新しいルールを作成し、loyalty_level が 80 以上の場合にアイテムが結果の上部に表示されるようにします。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt158578005df8c76d/6a17085aab7f086dc0db9de3/58de12dff93305440608f51465462fcc68653a08-1421x496.png" alt="Elasticsearchでクエリルールのルールセットを編集する方法" /><p>ルールとルールセットを保存します。</p><p>同等の REST リクエストは次のとおりです。</p>PUT _query_rules/my-rules
{
  "rules": [
    {
      "rule_id": "pin-premiun-user",
      "type": "pinned",
      "criteria": [
        {
          "type": "gte",
          "metadata": "loyalty_level",
          "values": [
            80
          ]
        }
      ],
      "actions": {
        "docs": [
          {
            "_index": "products",
            "_id": "6"
          }
        ]
      }
    }
  ]
}<p>ここで、クエリを実行するときに、メタデータに新しいパラメータ<strong>royality_level</strong>を含める必要があります。ルールの条件が満たされると、新しいドキュメントが結果の上部に表示されます。</p><p>たとえば、loyalty_level が 80 のクエリを送信する場合:</p>POST /products/_search
{
  "retriever": {
    "rule": {
      "retriever": {
        "standard": {
          "query": {
            "match": {
              "name": "PlayStation"
            }
          }
        }
      },
      "match_criteria": {
        "query_string": "PlayStation",
        "loyalty_level": 80
      },
      "ruleset_ids": ["my-rules"]
    }
  }
}<p>結果の上部にロイヤルティ ドキュメントが表示されます。</p>{
  "took": 31,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 4,
      "relation": "eq"
    },
    "max_score": 1.7014122e+38,
    "hits": [
      {
        "_index": "products",
        "_id": "6",
        "_score": 1.7014122e+38,
        "_source": {
          "id": "6",
          "name": "PlayStation Plus Deluxe Card - 12 months",
          "category": "membership",
          "brand": "Sony",
          "price": 300
        }
      },
      {
        "_index": "products",
        "_id": "3",
        "_score": 0.5054567,
        "_source": {
          "id": "3",
          "name": "PlayStation 4 Camera",
          "category": "accessory",
          "brand": "Sony",
          "price": 200
        }
      },
      {
        "_index": "products",
        "_id": "1",
        "_score": 0.45618832,
        "_source": {
          "id": "1",
          "name": "PlayStation 4 Slim 1TB",
          "category": "console",
          "brand": "Sony",
          "price": 1200
        }
      },
      {
        "_index": "products",
        "_id": "4",
        "_score": 0.45618832,
        "_source": {
          "id": "4",
          "name": "PlayStation 4 VR Headset",
          "category": "accessory",
          "brand": "Sony",
          "price": 900
        }
      }
    ]
  }
}<p>以下の場合、ロイヤルティ レベルが 70 であるため、ルールは満たされず、アイテムは上部に表示されません。</p>POST /products/_search
{
  "retriever": {
    "rule": {
      "retriever": {
        "standard": {
          "query": {
            "match": {
              "name": "PlayStation"
            }
          }
        }
      },
      "match_criteria": {
        "query_string": "PlayStation",
        "loyalty_level": 70
      },
      "ruleset_ids": ["my-rules"]
    }
  }
}<p>結果は次のとおりです。</p>{
  "took": 7,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 4,
      "relation": "eq"
    },
    "max_score": 0.5054567,
    "hits": [
      {
        "_index": "products",
        "_id": "3",
        "_score": 0.5054567,
        "_source": {
          "id": "3",
          "name": "PlayStation 4 Camera",
          "category": "accessory",
          "brand": "Sony",
          "price": 200
        }
      },
      {
        "_index": "products",
        "_id": "1",
        "_score": 0.45618832,
        "_source": {
          "id": "1",
          "name": "PlayStation 4 Slim 1TB",
          "category": "console",
          "brand": "Sony",
          "price": 1200
        }
      },
      {
        "_index": "products",
        "_id": "4",
        "_score": 0.45618832,
        "_source": {
          "id": "4",
          "name": "PlayStation 4 VR Headset",
          "category": "accessory",
          "brand": "Sony",
          "price": 900
        }
      },
      {
        "_index": "products",
        "_id": "6",
        "_score": 0.3817649,
        "_source": {
          "id": "6",
          "name": "PlayStation Plus Deluxe Card - 12 months",
          "category": "membership",
          "brand": "Sony",
          "price": 300
        }
      }
    ]
  }
}<h2>例: 即時除外</h2><p><strong>DualShock 4 ワイヤレス コントローラー (ID 2)</strong>が一時的に入手できず、販売できないとします。そのため、ビジネス チームは、ドキュメントを手動で削除したり、何らかのデータ処理が開始されるのを待ったりする代わりに、当面は検索結果からドキュメントを削除することにしました。</p><p>先ほど人気アイテムに適用したのと同様のプロセスを使用しますが、今回は<em>[Pinned]</em>ではなく<em>[Exclude]</em>を選択します。このルールは一種のブラックリストとして機能します。クエリが実行されるたびに除外が機能するように、条件を<strong>「常時」</strong>に変更します。</p><p>ルールは次のようになります。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt38564c0b7f4a6ee2/6a17085c1949f78692e7a989/f10971e4f1bc9520105111adfa3a476581a27130-1600x623.png" alt="Elasticsearch の即時除外ルールセットの例" /><p>変更を適用するには、ルールとルールセットを保存します。同等の REST リクエストは次のとおりです。</p>PUT _query_rules/my-rules
{
  "rules": [
    {
      "rule_id": "rule-6358",
      "type": "pinned",
      "criteria": [
        {
          "type": "always"
        }
      ],
      "actions": {
        "docs": [
          {
            "_index": "products",
            "_id": "2"
          }
        ]
      }
    }
  ]
}<p>ここで、クエリを再度実行すると、以前のルールではアイテムをピン留めするはずだったにもかかわらず、アイテムが結果に表示されなくなっていることがわかります。これは、<strong>除外がピン留め結果よりも優先される</strong>ためです。</p>{
 "took": 6,
 "timed_out": false,
 "_shards": {
   "total": 1,
   "successful": 1,
   "skipped": 0,
   "failed": 0
 },
 "hits": {
   "total": {
     "value": 4,
     "relation": "eq"
   },
   "max_score": 2.205655,
   "hits": [
     {
       "_index": "products",
       "_id": "3",
       "_score": 2.205655,
       "_source": {
         "id": "3",
         "name": "PlayStation 4 Camera",
         "category": "accessory",
         "brand": "Sony",
         "price": 200
       }
     },
     {
       "_index": "products",
       "_id": "1",
       "_score": 1.9738505,
       "_source": {
         "id": "1",
         "name": "PlayStation 4 Slim 1TB",
         "category": "console",
         "brand": "Sony",
         "price": 1200
       }
     },
     {
       "_index": "products",
       "_id": "4",
       "_score": 1.9738505,
       "_source": {
         "id": "4",
         "name": "PlayStation 4 VR Headset",
         "category": "accessory",
         "brand": "Sony",
         "price": 900
       }
     },
     {
       "_index": "products",
       "_id": "5",
       "_score": 0.69247496,
       "_source": {
         "id": "5",
         "name": "Charging Station for DualShock 4",
         "category": "accessory",
         "brand": "Sony",
         "price": 80
       }
     }
   ]
 }
}<h2>まとめ</h2><p><strong>クエリ ルールを</strong>使用すると、コードを変更することなく関連性を簡単に調整できます。新しい<strong>Kibana</strong> <strong>UI</strong>では、これらの変更を数秒で行うことができるため、お客様とビジネス チームは検索結果をより細かく制御できるようになります。</p><p>クエリ ルールは、電子商取引以外にも、サポート ポータルでトラブルシューティング ガイドを強調表示したり、ナレッジ ベースで重要な社内ドキュメントを表示したり、ニュース サイトで最新ニュースを宣伝したり、期限切れの求人やコンテンツの一覧を除外したりするなど、さまざまなシナリオで活用できます。ユーザーの役割や地域によって制限されたコンテンツを非表示にするなど、コンプライアンス ルールを適用することもできます。</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/elasticsearch-query-rules-ui-introduction</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/elasticsearch-query-rules-ui-introduction</guid>
    <category><![CDATA[基本]]></category>
    <category><![CDATA[開発者エクスペリエンス]]></category>
    <dc:creator><![CDATA[Jhon Guzmán]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt565ed0eb407e098d/6a17085d8b73cb363d189fb1/1fb10bd31c509cc9b9bb4f71f49970f140e6c36f-1600x945.png" length="0" type="image/png"/>
    <pubDate>Fri, 07 Nov 2025 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[MastraとElasticsearchを使用してセマンティックリコールを備えた知識エージェントを構築する]]></title>
    <description><![CDATA[メモリと情報検索用のベクトル ストアとして Mastra と Elasticsearch を使用して、セマンティック リコールを備えたナレッジ エージェントを構築する方法を学びます。]]></description>
    <content:encoded><![CDATA[<p><a href="https://www.elastic.co/search-labs/blog/context-engineering-overview">コンテキスト エンジニアリングは</a>、信頼性の高い AI エージェントとアーキテクチャの構築においてますます重要になっています。モデルがどんどん良くなるにつれて、その有効性と信頼性はトレーニングされたデータに依存するのではなく、適切なコンテキストにどれだけ適切に基づいているかに依存するようになります。最も関連性の高い情報を適切なタイミングで取得して適用できるエージェントは、正確で信頼できる出力を生成する可能性がはるかに高くなります。</p><p>このブログでは、 <a href="https://mastra.ai/">Mastra</a>を使用して、Elasticsearch をメモリおよび検索バックエンドとして使用し、ユーザーの発言を記憶し、後で関連情報を思い出すことができるナレッジ エージェントを構築します。これと同じ概念を実際のユースケースに簡単に拡張できます。サポート エージェントが過去の会話や解決策を記憶し、特定のユーザーへの応答をカスタマイズしたり、以前のコンテキストに基づいてより迅速に解決策を提示したりできると考えてください。</p><p>ここから手順に従って、ステップごとに構築する方法を確認してください。迷ってしまったり、完成した例を実行したいだけの場合は、<a href="https://github.com/jdarmada/getting-started-mastra-elastic/tree/main">ここにある</a>リポジトリを確認してください。</p><h2>マストラとは何ですか？</h2><p>Mastra は、推論、メモリ、ツールの交換可能なパーツを備えた AI エージェントを構築するためのオープンソースの TypeScript フレームワークです。<a href="https://mastra.ai/docs/memory/semantic-recall">セマンティック リコール</a>機能により、エージェントはメッセージをベクター データベースに埋め込みとして保存することで、過去のやり取りを記憶して取り出すことができます。これにより、エージェントは長期的な会話のコンテキストと継続性を維持できます。Elasticsearch は効率的な高密度ベクトル検索をサポートしているため、この機能を有効にするのに最適なベクトル ストアです。セマンティックリコールがトリガーされると、エージェントは関連する過去のメッセージをモデルのコンテキストウィンドウに引き出し、モデルが取得したコンテキストを推論と応答の基礎として使用できるようにします。</p><h2>始めるために必要なもの</h2><ul><li><p>ノード v18+</p></li><li><p>Elasticsearch（バージョン8.15以降）</p></li><li><p>Elasticsearch APIキー</p></li><li><p><a href="https://help.openai.com/en/articles/4936850-where-do-i-find-my-openai-api-key">OpenAI APIキー</a></p></li></ul><p>注: デモでは OpenAI プロバイダーを使用するため、これが必要になりますが、Mastra は他の AI SDK とコミュニティ モデル プロバイダーをサポートしているため、設定に応じて簡単に交換できます。</p><h2>Mastraプロジェクトの構築</h2><p>プロジェクトの足場を提供するために、Mastra の組み込み CLI を使用します。次のコマンドを実行します。</p>npm create mastra@latest<p>次のような一連のプロンプトが表示されます。</p><p>1. プロジェクトに名前を付けます。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt87f941f654d03827/6a16f7af67045b214d45bfa1/2b9fe559e0276140dd539e24f916a73c60870405-620x84.png" alt="Mastraアプリでプロンプトに名前を付ける" /><p>2. このデフォルト設定を維持することもできますし、空白のままにしておくこともできます。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt7dbb3d4f27435cac/6a16f7b0cdacbf29497d27de/e04729eb03bce8499e973e18c28642402340d0e5-852x68.png" alt="プロンプトファイルを保存する場所をmastraに伝える" /><p>3. このプロジェクトでは、OpenAI が提供するモデルを使用します。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1f654f6cb9397e94/6a16f7b2964cea899a08b942/a86596a469a71bdf8bd99cbaf528d0f0cf7272c0-436x222.png" alt="MastraでOpenAIが提供するモデルを選択する" /><p>4. すべての環境変数を、後のステップで設定する `.env` ファイルに保存するため、「今はスキップ」オプションを選択します。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltff106117521a3519/6a16f7b3c1e8a5031af880d8/02b19ccc34af0bdacf52fd94b519d036540ca2e6-426x114.png" alt="OpenAIキーについては今はスキップを選択" /><p>5. このオプションをスキップすることもできます。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltcda7d9c51c3878d7/6a16f7b450916809dbe1b892/b3fe63d19d270bc2e0de1dd92033bf8b26750819-990x208.png" alt="" /><p>初期化が完了したら、次のステップに進むことができます。</p><h3>依存関係のインストール</h3><p>次に、いくつかの依存関係をインストールする必要があります。</p>npm install ai @ai-sdk/openai @elastic/elasticsearch dotenv<ul><li><p><code>ai</code> - JavaScript/TypeScript で AI モデル、プロンプト、ワークフローを管理するためのツールを提供するコア AI SDK パッケージ。Mastra は Vercel の<a href="https://ai-sdk.dev/">AI SDK</a>上に構築されているため、エージェントとのモデルのインタラクションを有効にするにはこの依存関係が必要です。</p></li><li><p><code>@ai-sdk/openai</code> - AI SDK を OpenAI モデル (GPT-4、GPT-4o など) に接続し、OpenAI API キーを使用した API 呼び出しを可能にするプラグイン。</p></li><li><p><code>@elastic/elasticsearch</code> - <a href="https://www.elastic.co/docs/reference/elasticsearch/clients/javascript">Node.js 用の公式 Elasticsearch クライアント</a>、インデックス作成、検索、ベクター操作のために Elastic Cloud またはローカル クラスターに接続するために使用されます。</p></li><li><p><code>dotenv</code> - .envから環境変数を読み込みますファイルをprocess.envにコピーし、API キーや Elasticsearch エンドポイントなどの資格情報を安全に挿入できるようになります。</p></li></ul><h3>環境変数の設定</h3><p>プロジェクトのルート ディレクトリに<code>.env</code>ファイルが存在しない場合は作成します。あるいは、<a href="https://github.com/jdarmada/getting-started-mastra-elastic/blob/main/.env.example">リポジトリ</a>に提供されている例<code>.env</code>をコピーして名前を変更することもできます。このファイルでは、次の変数を追加できます。</p>ELASTICSEARCH_ENDPOINT="your-endpoint-here"
ELASTICSEARCH_API_KEY="your-key-here"
OPENAI_API_KEY="your-key-here"<p>これで基本的な設定は完了です。ここから、エージェントの構築とオーケストレーションを開始できます。さらに一歩進んで、Elasticsearch をストアおよびベクター検索レイヤーとして追加します。</p><h2>ベクターストアとしてElasticsearchを追加する</h2><p><code>stores</code>という新しいフォルダーを作成し、その中にこの<a href="https://github.com/jdarmada/getting-started-mastra-elastic/blob/main/src/mastra/stores/elastic-store.ts">ファイル</a>を追加します。Mastra と Elastic が公式の Elasticsearch ベクター ストア統合を出荷する前に、 <a href="https://github.com/abhiaiyer91">Abhi Aiyer</a> (Mastra CTO) が<code>ElasticVector</code>と呼ばれるこの初期のプロトタイプ クラスを共有しました。簡単に言えば、Mastra のメモリ抽象化を Elasticsearch の高密度ベクトル機能に接続することで、開発者はエージェントのベクトル データベースとして Elasticsearch を導入できるようになります。</p><p>統合の重要な部分を詳しく見てみましょう。</p><h3>Elasticsearchクライアントの取り込み</h3><p>このセクションでは、 <code>ElasticVector</code>クラスを定義し、標準デプロイメントとサーバーレスデプロイメントの両方をサポートする Elasticsearch クライアント接続を設定します。</p>export interface ElasticVectorConfig extends ClientOptions {
    /**
     * Explicitly specify if connecting to Elasticsearch Serverless.
     * If not provided, will be auto-detected on first use.
     */
    isServerless?: boolean;
    
    /**
     * Maximum documents to count accurately when describing indices.
     * Higher values provide accurate counts but may impact performance on large indices.
     * 
     * @default 10000
     */
    maxCountAccuracy?: number;
}

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

    constructor(config: ElasticVectorConfig) {
        super();
        this.client = new Client(config);
        this.isServerless = config.isServerless;
        this.maxCountAccuracy = config.maxCountAccuracy ?? 10000;
    }
}<ul><li><p><code>ElasticVectorConfig extends ClientOptions</code>: これにより、すべての Elasticsearch クライアント オプション ( <code>node</code> 、 <code>auth</code> 、 <code>requestTimeout</code>など) を継承し、カスタム プロパティを追加する新しい構成インターフェースが作成されます。つまり、ユーザーは、サーバーレス固有のオプションとともに、有効な Elasticsearch 構成を渡すことができるということです。</p></li><li><p><code>extends MastraVector</code>: これにより、 <code>ElasticVector</code> 、すべてのベクター ストア統合が準拠する共通インターフェースである Mastra の基本<code>MastraVector</code>クラスから継承できるようになります。これにより、エージェントの観点から見ると、Elasticsearch は他の Mastra ベクター バックエンドと同じように動作するようになります。</p></li><li><p><code>private client: Client</code>: これは、Elasticsearch JavaScript クライアントのインスタンスを保持するプライベート プロパティです。これにより、クラスはクラスターと直接通信できるようになります。</p></li><li><p><code>isServerless</code> および<code>deploymentChecked</code> : これらのプロパティは連携して、サーバーレスまたは標準の Elasticsearch デプロイメントに接続されているかどうかを検出し、キャッシュします。この検出は最初の使用時に自動的に行われますが、明示的に構成することもできます。</p></li><li><p><code>constructor(config: ClientOptions)</code>: このコンストラクターは、構成オブジェクト (Elasticsearch の資格情報とオプションのサーバーレス設定を含む) を受け取り、それを使用して<code>this.client = new Client(config)</code>行でクライアントを初期化します。</p></li><li><p><code>super()</code>: これは Mastra の基本コンストラクターを呼び出すため、ログ記録、検証ヘルパー、およびその他の内部フックを継承します。</p></li></ul><p>この時点で、Mastraは新しいベクターストアがあることを知っています。 <code>ElasticVector</code></p><h3>展開タイプの検出</h3><p>インデックスを作成する前に、アダプターは標準の Elasticsearch を使用しているか Elasticsearch Serverless を使用しているかを自動的に検出します。サーバーレス デプロイメントでは手動でのシャード構成が許可されないため、これは重要です。</p>private async detectServerless(): Promise&lt;boolean&gt; {
    // Return cached result if already detected
    if (this.deploymentChecked) {
        return this.isServerless ?? false;
    }

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

    try {
        const info = await this.client.info();
        
        // Primary detection: build flavor (most reliable)
        const isBuildFlavorServerless = info.version?.build_flavor === 'serverless';
        
        // Secondary detection: tagline (fallback)
        const isTaglineServerless = info.tagline?.toLowerCase().includes('serverless') ?? false;
        
        this.isServerless = isBuildFlavorServerless || isTaglineServerless;
        this.deploymentChecked = true;
        
        this.logger?.info(
            `Auto-detected ${this.isServerless ? 'Serverless' : 'Standard'} Elasticsearch deployment`,
            { 
                buildFlavor: info.version?.build_flavor, 
                version: info.version?.number,
                detectionMethod: isBuildFlavorServerless ? 'build_flavor' : 'tagline'
            }
        );
        
        return this.isServerless;
    } catch (error) {
        this.logger?.warn(
            'Could not auto-detect deployment type, assuming Standard Elasticsearch. ' +
            'Set isServerless: true explicitly in config if using Serverless.',
            { error: error instanceof Error ? error.message : String(error) }
        );
        this.isServerless = false;
        this.deploymentChecked = true;
        return false;
    }
}<p>何が起こっていますか：</p><ul><li><p>まず、構成で明示的に<code>isServerless</code>が設定されているかどうかを確認します (自動検出をスキップします)</p></li><li><p>Elasticsearch の<code>info()</code> API を呼び出してクラスター情報を取得します</p></li><li><p><code>build_flavor field</code>をチェックします (サーバーレス デプロイメントは<code>serverless</code>を返します)</p></li><li><p>ビルドフレーバーが利用できない場合はタグラインをチェックする</p></li><li><p>結果をキャッシュして、API 呼び出しの繰り返しを回避します。</p></li><li><p>検出に失敗した場合は標準展開をデフォルトとする</p></li></ul><p> 使用例:</p>// Option 1: Auto-detect (recommended)
const vector = new ElasticVector({
    node: 'https://your-cluster.es.cloud',
    auth: { apiKey: 'your-api-key' }
});
// Detection happens automatically on first index operation

// Option 2: Explicit configuration (faster startup)
const vector = new ElasticVector({
    node: 'https://your-serverless.es.cloud',
    auth: { apiKey: 'your-api-key' },
    isServerless: true  // Skips auto-detection
});<h3>Elasticsearchに「メモリ」ストアを作成する</h3><p>以下の関数は、埋め込みを保存するための Elasticsearch インデックスを設定します。インデックスがすでに存在するかどうかを確認します。そうでない場合は、埋め込みとカスタム類似度メトリックを格納するための<code>dense_vector</code>フィールドを含む以下のマッピングを使用して作成します。</p><p>注意すべき点:</p><ul><li><p><code>dimension</code>パラメータは各埋め込みベクトルの長さであり、使用している埋め込みモデルによって異なります。私たちの場合、サイズ<code>1536</code>のベクトルを出力する OpenAI の<code>text-embedding-3-small</code>モデルを使用して埋め込みを生成します。これをデフォルト値として使用します。</p></li><li><p>以下のマッピングで使用される<code>similarity</code>変数は、ヘルパー関数 c <code>onst similarity = this.mapMetricToSimilarity(metric)</code>から定義されます。この関数は、 <code>metric</code>パラメータの値を受け取り、選択された距離メトリックの Elasticsearch 互換キーワードに変換します。</p><ul><li><p>たとえば、Mastra では、ベクトルの類似性を表すために<code>cosine</code> 、 <code>euclidean</code> 、 <code>dotproduct</code>などの一般的な用語を使用します。メトリック<code>euclidean</code> Elasticsearch マッピングに直接渡すと、Elasticsearch はキーワード<code>l2_norm</code>がユークリッド距離を表すと想定するため、エラーが発生します。</p></li></ul></li><li><p>サーバーレス互換性: サーバーレスデプロイメントのシャードとレプリカの設定は Elasticsearch Serverless によって自動的に管理されるため、コードでは自動的に省略されます。</p></li></ul>async createIndex(params: CreateIndexParams): Promise&lt;void&gt; {
    const { indexName, dimension = 1536, metric = 'cosine' } = params;

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

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

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

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

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

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

        this.logger?.info(
            `Created ${isServerless ? 'Serverless' : 'Standard'} Elasticsearch index "${indexName}"`,
            { dimension, metric, similarity }
        );
    } catch (error) {
        const errorMessage = error instanceof Error ? error.message : String(error);
        this.logger?.error(`Failed to create index "${indexName}": ${errorMessage}`);
        throw new Error(`Failed to create index "${indexName}": ${errorMessage}`);
    }
}<h3>やりとりの後に新しい記憶やメモを保存する</h3><p>この関数は、各インタラクションの後に生成された新しい埋め込みをメタデータとともに取得し、Elastic の<code>bulk</code> API を使用してそれらをインデックスに挿入または更新します。<code>bulk</code> API は複数の書き込み操作を 1 つのリクエストにグループ化します。このインデックス作成パフォーマンスの向上により、エージェントのメモリが増加し続けても更新の効率が維持されます。</p>async upsert(params: UpsertVectorParams): Promise&lt;string[]&gt; {
    const { indexName, vectors, metadata = [], ids } = params;

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

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

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

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

        this.logger?.info(`Successfully upserted ${vectors.length} vectors to "${indexName}"`);
        return vectorIds;
    } catch (error) {
        const errorMessage = error instanceof Error ? error.message : String(error);
        this.logger?.error(`Failed to upsert vectors to "${indexName}": ${errorMessage}`);
        throw new Error(`Failed to upsert vectors to "${indexName}": ${errorMessage}`);
    }
}<h3>意味的想起のための類似ベクトルのクエリ</h3><p>この機能は、セマンティック リコール機能の中核です。エージェントはベクトル検索を使用して、インデックス内に保存されている類似の埋め込みを見つけます。</p>async query(params: QueryVectorParams&lt;any&gt;): Promise&lt;QueryResult[]&gt; {
    const { indexName, queryVector, topK = 10, filter, includeVector = false } = params;

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

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

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

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

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

        this.logger?.debug(`Query returned ${results.length} results from "${indexName}"`);
        return results;
    } catch (error) {
        const errorMessage = error instanceof Error ? error.message : String(error);
        this.logger?.error(`Failed to query vectors from "${indexName}": ${errorMessage}`);
        throw new Error(`Failed to query vectors from "${indexName}": ${errorMessage}`);
    }
}<p>内部構造:</p><ul><li><p>Elasticsearch の<code>knn</code> API を使用して<a href="https://www.elastic.co/docs/solutions/search/vector/knn">kNN</a> (k 近傍法) クエリを実行します。</p></li><li><p>入力クエリ ベクトルに類似する上位 K 個のベクトルを取得します。</p></li><li><p>オプションでメタデータ フィルターを適用して結果を絞り込む (例: 特定のカテゴリまたは時間範囲内のみを検索する)</p></li><li><p>ドキュメント ID、類似度スコア、保存されたメタデータを含む構造化された結果を返します。</p></li></ul><h2>知識エージェントの作成</h2><p><code>ElasticVector</code>統合を通じて Mastra と Elasticsearch の接続を確認したので、次は Knowledge Agent 自体を作成しましょう。</p><p>フォルダー<code>agents</code>内に、 <code>knowledge-agent.ts</code>というファイルを作成します。まず、環境変数を接続し、Elasticsearch クライアントを初期化します。</p>import { Agent } from '@mastra/core/agent';
import { Memory } from '@mastra/memory';
import { openai } from '@ai-sdk/openai';
import { Client } from '@elastic/elasticsearch';
import { ElasticVector } from '../stores/elastic-store';
import dotenv from "dotenv";

dotenv.config();

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

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

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

try {
  await testClient.ping();
  console.log('Connected to Elasticsearch successfully');
} catch (error: unknown) {
  if (error instanceof Error) {
    console.error('Failed to connect to Elasticsearch:', error.message);
  } else {
    console.error('Failed to connect to Elasticsearch:', error);
  }
  process.exit(1);
}
//Initialize the Elasticsearch vector store
const vectorStore = new ElasticVector({
  node: ELASTICSEARCH_ENDPOINT,
  auth: {
    apiKey: ELASTICSEARCH_API_KEY,
  },
//Optional: Explicitly set to true if using Elasticsearch Serverless to skip auto-detection and improve startup time
//isServerless: true,
});<p>ここでは、次の操作を行います。</p><ul><li><p><code>dotenv</code>を使用して、 <code>.env</code>ファイルから変数を読み込みます。</p></li><li><p>Elasticsearch の資格情報が正しく挿入されているかどうかを確認し、クライアントへの接続を正常に確立できるかどうかを確認します。</p></li><li><p>Elasticsearch エンドポイントと API キーを<code>ElasticVector</code>コンストラクターに渡して、先ほど定義したベクター ストアのインスタンスを作成します。</p></li><li><p>Elasticsearch Serverless を使用している場合は、オプションで<code>isServerless: true</code>を指定します。これにより、自動検出手順がスキップされ、起動時間が短縮されます。省略した場合、アダプタは最初の使用時にデプロイメント タイプを自動的に検出します。</p></li></ul><p>次に、Mastra の<code>Agent</code>クラスを使用してエージェントを定義します。</p>export const knowledgeAgent = new Agent({
    name: 'KnowledgeAgent',
    instructions: 'You are a helpful knowledge assistant.',
    model: openai('gpt-4o'),
    memory: new Memory({

        vector: vectorStore,

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

        //set semantic recall options
        options: {
            semanticRecall: {
                topK: 3, // retrieve 3 similar messages
                messageRange: 2, // include 2 messages before/after each match
                scope: 'resource',
            },
        },
    }),
});<p>定義できるフィールドは次のとおりです。</p><ul><li><p><code>name</code> そして<code>instructions</code> : アイデンティティと主な機能を与えます。</p></li><li><p><code>model</code>: <code>@ai-sdk/openai</code>パッケージを通じて OpenAI の<code>gpt-4o</code>を使用しています。</p></li><li><p><code>memory</code>:</p><ul><li><p><code>vector</code>: Elasticsearch ストアを指すので、埋め込みはそこから保存され、取得されます。</p></li><li><p><code>embedder</code>: 埋め込みを生成するためにどのモデルを使用するか</p></li><li><p><code>semanticRecall</code> オプションによってリコールの動作が決まります。</p><ul><li><p><code>topK</code>: 意味的に類似したメッセージを取得する数。</p></li><li><p><code>messageRange</code>: 各マッチにどの程度の会話を含めるか。</p></li><li><p><code>scope</code>: メモリの境界を定義します。</p></li></ul></li></ul></li></ul><p>もうすぐ終わりです。新しく作成したエージェントを Mastra 構成に追加するだけです。<a href="http://index.ts/"><code>index.ts</code></a>というファイルで、ナレッジ エージェントをインポートし、 <code>agents</code>フィールドに挿入します。</p>export const mastra = new Mastra({
  agents: { knowledgeAgent },
  storage: new LibSQLStore({
    // stores observability, scores, ... into memory storage, if it needs to persist, change to file:../mastra.db
    url: ":memory:",
  }),
  logger: new PinoLogger({
    name: 'Mastra',
    level: 'info',
  }),
  telemetry: {
    // Telemetry is deprecated and will be removed in the Nov 4th release
    enabled: false, 
  },
  observability: {
    // Enables DefaultExporter and CloudExporter for AI tracing
    default: { enabled: true }, 
  },
});<p>その他のフィールドには以下が含まれます。</p><ul><li><p><code>storage</code>: これは、実行履歴、観測性メトリック、スコア、キャッシュのための Mastra の内部データ ストアです。Mastra ストレージの詳細については、<a href="https://mastra.ai/docs/server-db/storage">こちらを</a>ご覧ください。</p></li><li><p><code>logger</code>: Mastra は、軽量の構造化 JSON ロガーである<a href="https://github.com/pinojs/pino">Pino</a>を使用します。エージェントの開始と停止、ツールの呼び出しと結果、エラー、LLM 応答時間などのイベントをキャプチャします。</p></li><li><p><code>observability</code>: エージェントの AI トレースおよび実行の可視性を制御します。追跡対象:</p><ul><li><p>各推論ステップの開始/終了。</p></li><li><p>使用されたモデルまたはツール。</p></li><li><p>入力と出力。</p></li><li><p>スコアと評価</p></li></ul></li></ul><h3>Mastra Studioでエージェントをテストする</h3><p>おめでとうございます！ここまで到達したら、このエージェントを実行し、そのセマンティックリコール能力をテストする準備が整いました。幸いなことに、Mastra には組み込みのチャット UI が用意されているため、独自に構築する必要はありません。</p><p>Mastra 開発サーバーを起動するには、ターミナルを開いて次のコマンドを実行します。</p>npm run dev<p>サーバーの初期バンドルと起動が完了すると、Playground のアドレスが提供されるはずです。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blte5f857fddc74ffc9/6a16f7b6a6c2b995d5e794c0/8b045f70008d26aec4d2e6b59d61085555b9c5b2-686x116.png" alt="プレイグラウンドのサーバーアドレス" /><p>このアドレスをブラウザに貼り付けると、Mastra Studio が表示されます。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc7fdda6ce46ce068/6a16f7b7b0367d4f7672bacf/69bc80fe8486edd9e0cf91d87b39f465aeb23111-1600x438.png" alt="プレイグラウンドのアドレスを貼り付けてMastra Studioにアクセスする" /><p><code>knowledgeAgent</code>のオプションを選択してチャットを開始してください。</p><p>すべてが正しく接続されているかどうかを確認するための簡単なテストでは、次のような情報を入力します。「チームは、主にエンタープライズ契約の更新により、10 月の売上実績が 12% 増加したと発表しました。次のステップは、中規模市場の顧客へのリーチを拡大することです。」次に、新しいチャットを開始して、「次に重点を置く必要があると言った顧客セグメントはどれですか?」などの質問をします。ナレッジエージェントは、最初のチャットで提供した情報を思い出せるはずです。次のような応答が表示されます。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltfec3266e81a7213b/6a16f7b92b835f6f70f4afe2/da8ebddad89874023ed440a8f1ad2cb04ed043f4-1070x288.png" alt="Mastra Studioでナレッジエージェントとチャット - エージェントは情報を思い出すことができます" /><p>このような応答が表示された場合、エージェントが以前のメッセージを Elasticsearch に埋め込みとして正常に保存し、後でベクトル検索を使用して取得したことを意味します。</p><h3>エージェントの長期記憶ストアの検査</h3><p>Mastra Studio のエージェント構成の<code>memory</code>タブに移動します。これにより、エージェントが時間の経過とともに何を学習したかを確認できます。Elasticsearch に埋め込まれて保存されるすべてのメッセージ、応答、およびやり取りは、この長期メモリの一部になります。過去のやり取りを意味的に検索して、エージェントが以前に学習した思い出の情報やコンテキストをすぐに見つけることができます。これは本質的には、エージェントがセマンティックリコール中に使用するメカニズムと同じものですが、ここではそれを直接検査できます。以下の例では、「sales」という用語を検索し、sales に関連する内容を含むすべてのインタラクションを取得しています。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blte428134d7bf2a43a/6a16f7bbb0367d185872bad3/3decaa0c332d288c5ae0b11c25f592c7d50c2f0f-1104x1320.png" alt="知識エージェントの長期記憶ストアを検査する方法" /><h2>まとめ</h2><p>Mastra と Elasticsearch を接続することで、コンテキスト エンジニアリングの重要なレイヤーであるメモリをエージェントに付与できます。セマンティックリコールを使用すると、エージェントは時間の経過とともにコンテキストを構築し、学習した内容に基づいて応答することができます。つまり、より正確で信頼性が高く、自然なやりとりが可能になります。</p><p>この早期の統合は単なる出発点にすぎません。ここで同じパターンを使用すると、過去のチケットを記憶しているサポートエージェント、関連ドキュメントを取得する内部ボット、会話の途中で顧客の詳細を思い出すことができる AI アシスタントなどが可能になります。当社は公式の Mastra 統合にも取り組んでおり、近い将来この組み合わせがさらにシームレスになる予定です。</p><p>次に何を構築するのか楽しみにしています。ぜひ試してみて、 <a href="https://mastra.ai/">Mastra</a>とそのメモリ機能を調べ、発見したことをコミュニティと自由に共有してください。</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/knowledge-agent-semantic-recall-mastra-elasticsearch</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/knowledge-agent-semantic-recall-mastra-elasticsearch</guid>
    <category><![CDATA[エージェント型AI]]></category>
    <category><![CDATA[開発者エクスペリエンス]]></category>
    <category><![CDATA[統合]]></category>
    <dc:creator><![CDATA[JD Armada]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt09afdbff05603865/6a16f7bd839dfabbf2dcfcb5/b8d51c2726d5573385c9246a7821d12ade4f1b0e-720x420.jpg" length="0" type="image/jpeg"/>
    <pubDate>Thu, 06 Nov 2025 00:00:00 GMT</pubDate>
  </item>
  </channel>
</rss>