<?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[Piotr Przybyl - 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[Piotr Przybyl - Elasticsearch Labs]]></title>
      <url>https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1121c0bf0e8a6e65/6a88da6340a1841030ef456f/search-labs-thumbnail.png</url>
      <link>https://www.elastic.co/jp/search-labs/author/piotr-przybyl</link>
    </image>
    <link>https://www.elastic.co/jp/search-labs/author/piotr-przybyl</link>
    <atom:link href="https://www.elastic.co/jp/search-labs/rss/author/piotr-przybyl.xml" rel="self" type="application/rss+xml"/>
    <language><![CDATA[jp]]></language>
    <lastBuildDate>Sun, 20 Sep 2026 18:42:21 GMT</lastBuildDate>
  <item>
    <title><![CDATA[Elasticsearch のシャードとレプリカ: 実践ガイド]]></title>
    <description><![CDATA[Elasticsearch のシャードとレプリカの概念を習得し、それらを最適化する方法を学習します。]]></description>
    <content:encoded><![CDATA[<p>Elasticsearch は、スケーラビリティとフォールト トレランスの問題に対処する分散システムを Lucene 上に構築することで、Lucene のパワーを強化します。また、JSON ベースの REST API も公開されているため、他のシステムとの相互運用性が非常に簡単になります。</p><p>Elasticsearch のような分散システムは非常に複雑になる可能性があり、パフォーマンスと安定性に影響を与える要因が多数あります。<strong>シャードは</strong>Elasticsearch の最も基本的な概念の 1 つであり、その仕組みを理解することで Elasticsearch クラスターを効果的に管理できるようになります。</p><p>この記事では、プライマリ シャードとレプリカ シャードとは何か、それらが Elasticsearch クラスターに与える影響、さまざまな需要に合わせてそれらを調整するためのツールについて説明します。</p><h2>破片を理解する</h2><p>Elasticsearch インデックス内のデータは膨大な量にまで増大する可能性があります。管理しやすいように、すべてのデータはインデックスに保存され、インデックスはいくつかの<strong>シャード</strong>に分割されます。各 Elasticsearch シャードは Apache Lucene インデックスであり、個々の Lucene インデックスには Elasticsearch インデックス内のドキュメントのサブセットが含まれています。このようにインデックスを分割すると、リソースの使用量を制御できます。Apache Lucene インデックスには、2,147,483,519 (2³¹ - 129) ドキュメントの制限があります。</p><p>場合によっては、再バランス調整のためにインデックスをノード間で移動する必要があります。このプロセスは時間とリソースの両方を大量に消費する可能性があるため、インデックスが大きくなりすぎないようにする必要があります。これにより、回復時間を管理しやすい状態に保つことができます。さらに、インデックスは常に結合する必要がある Lucene セグメントで構成されているため、セグメントが大きくなりすぎないことが重要です。これらの理由から、Elasticsearch はインデックス データを<strong>プライマリ シャード</strong>と呼ばれるより小さく管理しやすいチャンクに分割し、複数のマシン間でより簡単に分散できるようにします。<strong>レプリカ</strong>シャードは、対応するプライマリ シャードの正確なコピーであり、その機能についてはこの記事の後半で説明します。</p><p>適切な数のシャードを持つことはパフォーマンスにとって重要です。したがって、事前に計画を立てるのが賢明です。クエリが異なるシャード間で並列に実行されると、各シャードが異なるノードに配置され、クラスター内に十分なノードがある場合に限り、単一のシャードで構成されたインデックスよりも高速に実行されます。ただし同時に、シャードはインデックス化されたデータとクラスター メタデータの両方に関して、メモリとディスク領域を消費します。シャードが多すぎると（オーバーシャーディングとも呼ばれます）、クエリ、インデックス要求、および管理操作が遅くなる可能性があるため、適切なバランスを維持することが重要です。</p><p>プライマリ シャードの数は、<strong>特定のインデックス インスタンスの</strong>インデックス作成時に定義されます。後でプライマリ シャードの数を変更する必要がある場合は、<strong>サイズ変更 API を</strong>使用できます (<a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-indices-split">分割</a>(プライマリ シャードを増やす)、<a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-indices-shrink">縮小</a>(プライマリ シャードを減らす)、または<a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-indices-clone">複製</a>(レプリカの新しい設定でプライマリ シャードの数は同じ))。これらの操作は、Lucene セグメントをコピーし、<strong>すべてのドキュメントの完全な再インデックスを回避します</strong>。インデックスを作成するときに、インデックスの設定としてプライマリ シャードとレプリカ シャードの数を設定できます。</p>PUT /sensor
{
   "settings" : {
       "index" : {
           "number_of_shards" : 6,
           "number_of_replicas" : 2
       }
   }
}<p>(シャードまたはレプリカの数を指定しない場合、Elasticsearch 7.0 以降、両方のデフォルト値は 1 になります)。理想的なシャードの数は、インデックス内のデータ量に基づいて決定する必要があります。一般的に、<a href="https://www.elastic.co/docs/deploy-manage/production-guidance/optimize-performance/size-shards">最適なシャードは 10 ～ 50 GB のデータを保持し、シャードあたりのドキュメント数は 2 億未満である必要があります</a>。たとえば、1 日に約 300 GB のアプリケーション ログが蓄積されると予想される場合、それらをホストするのに十分な数のノードがあれば、そのインデックスに約 10 個のシャードを持つことが妥当です。</p><p>シャードは、その存続期間中に、次のようなさまざまな状態を経る可能性があります。</p><ul><li><p><strong>初期化中:</strong>シャードが使用される前の初期状態。</p></li><li><p><strong>開始済み:</strong>シャードがアクティブでリクエストを受信できる状態。</p></li><li><p><strong>再配置中:</strong>シャードが別のノードに移動されている途中に発生する状態。これは、たとえば、ノードのディスク容量が不足している場合など、特定の状況下では必要になることがあります。</p></li><li><p><strong>未割り当て:</strong>割り当てに失敗したシャードの状態。これが発生すると理由が提供されます。たとえば、シャードをホストしているノードがクラスター内になくなった場合<em>(NODE_LEFT)</em> 、または閉じたインデックスに復元された場合<em>(EXISTING_INDEX_RESTORED) などです。</em></p></li></ul><p>すべてのシャード、その状態、およびその他のメタデータを表示するには、次のリクエストを使用できます。</p>GET _cat/shards<p>特定のインデックスのシャードを表示するには、URL にインデックスの名前を追加します (例: sensor)。</p>GET _cat/shards/sensor<p>このコマンドは、次の例のような出力を生成します。デフォルトでは、表示される列にはインデックスの名前、名前（つまりシャードの数、プライマリ シャードかレプリカか、シャードの状態、ドキュメント数、ディスク上のサイズ、シャードが配置されているノードの IP アドレスとノード ID が表示されます。</p>sensor 5 p STARTED    0  283b 127.0.0.1 ziap
sensor 5 r UNASSIGNED                  
sensor 2 p STARTED    1 3.7kb 127.0.0.1 ziap
sensor 2 r UNASSIGNED                  
sensor 3 p STARTED    3 7.2kb 127.0.0.1 ziap
sensor 3 r UNASSIGNED                  
sensor 1 p STARTED    1 3.7kb 127.0.0.1 ziap
sensor 1 r UNASSIGNED                  
sensor 4 p STARTED    2 3.8kb 127.0.0.1 ziap
sensor 4 r UNASSIGNED                  
sensor 0 p STARTED    0  283b 127.0.0.1 ziap
sensor 0 r UNASSIGNED<h2>レプリカを理解する</h2><p>各シャードにはデータのコピーが 1 つ含まれますが、インデックスにはシャードの複数のコピーが含まれる場合があります。したがって、シャードには、<strong>プライマリ シャード</strong>とコピー、または<strong>レプリカ</strong>の 2 種類があります。プライマリ シャードの各レプリカは常に異なるノードに配置されるため、ノード障害が発生した場合でもデータの高可用性が確保されます。冗長性とデータ損失やダウンタイムの防止の役割に加えて、レプリカはクエリをプライマリ シャードと並行して処理できるため、検索パフォーマンスが向上し、処理速度が向上します。</p><p>プライマリ シャードとレプリカ シャードの動作にはいくつかの重要な違いがあります。どちらもクエリを処理できますが、インデックスリクエスト（つまりインデックスにデータを追加するなどの処理は、レプリカ シャードに複製される前に、まずプライマリ シャードを通過する必要があります。前述のように、プライマリ シャードが使用できなくなった場合 (たとえば、ノードの切断やハードウェア障害などにより)、レプリカが昇格してその役割を引き継ぎます。</p><p>レプリカはノード障害の際に役立ちますが、インデックス作成時にメモリ、ディスク容量、計算能力を消費するため、レプリカを多くしすぎないことが重要です。プライマリ シャードとレプリカのもう 1 つの違いは、インデックスの作成後はプライマリ シャードの数を変更できないのに対し、レプリカの数はインデックス設定を更新することでいつでも動的に変更できることです。</p><p>レプリカに関して考慮すべきもう 1 つの要素は、利用可能なノードの数です。同じノードに同じデータのコピーが 2 つあると、ノードに障害が発生した場合に保護が提供されないため、レプリカは常にプライマリ シャードとは異なるノードに配置されます。その結果、システムが<em>n 個の</em>レプリカをサポートするには、クラスター内に少なくとも<em>n + 1 個の</em>ノードが必要になります。たとえば、クラスター内に 2 つのノードがあり、インデックスが 6 つのレプリカで構成されている場合、割り当てられるレプリカは 1 つだけです。一方、7 つのノードを持つシステムは、1 つのプライマリ シャードと 6 つのレプリカを完全に処理できます。</p><h2>シャードとレプリカの最適化</h2><p>プライマリ シャードとレプリカ シャードの適切なバランスを持つインデックスが作成された後でも、インデックスの周囲のダイナミクスは時間の経過とともに変化するため、これらを監視する必要があります。たとえば、時系列データを扱う場合、最近のデータを持つインデックスは、一般的に古いデータを持つインデックスよりもアクティブになります。これらのインデックスを調整しないと、要件が大きく異なるにもかかわらず、すべて同じ量のリソースを消費することになります。</p><p>ロールオーバー インデックス API を使用すると、新しいインデックスと古いインデックスを分離できます。特定のしきい値（ディスク上のインデックスのサイズ、ドキュメントの数、または年齢）に達すると、新しいインデックスを自動的に作成するように設定できます。この API は、シャードのサイズを制御するのにも役立ちます。インデックス作成後はシャードの数を簡単に変更できないため、ロールオーバー条件が満たされない限り、シャードにはデータが蓄積され続けます。アクセス頻度が低い古いインデックスの場合、インデックスの縮小と強制マージは、メモリとディスクのフットプリントを削減する 2 つの異なる方法です。前者はインデックス内のシャードの数を減らし、後者は Lucene セグメントの数を減らし、削除されたドキュメントによって使用されていたスペースを解放します。</p><h2>Elasticsearchの基盤となるプライマリシャードとレプリカシャード</h2><p>Elasticsearch は、膨大な量のデータのための分散ストレージ、検索、分析プラットフォームとして高い評価を得ています。しかし、このような規模で事業を展開する場合、必然的に課題が生じます。そのため、プライマリ シャードとレプリカ シャードの仕組みを理解することは、Elasticsearch にとって非常に重要かつ基本的なことであり、プラットフォームの信頼性とパフォーマンスを最適化するのに役立ちます。</p><p>これらがどのように機能し、どのように最適化するかを知ることは、より堅牢でパフォーマンスの高い Elasticsearch クラスターを実現するために重要です。クエリ応答が遅くなったり、頻繁に停止したりする場合は、この知識がこれらの障害を克服する鍵となる可能性があります。</p><p><a href="https://www.elastic.co/docs/deploy-manage/distributed-architecture/clusters-nodes-shards">クラスター、ノード、シャード、シャードの</a><a href="https://www.elastic.co/docs/deploy-manage/production-guidance/optimize-performance/size-shards">サイズ設定方法</a>、<a href="https://www.elastic.co/docs/deploy-manage/distributed-architecture/shard-allocation-relocation-recovery">シャードの割り当てと回復の</a>詳細については、Elasticsearch の公式ドキュメントを参照してください。</p><p>このトピックは、 <a href="https://youtu.be/sAySPSyL2qE">Elastic コミュニティ YouTube チャンネルの入門コースとしてもご利用いただけます。</a></p><p>最後に、ノード、シャード、レプリカについて心配したくない場合は、 <a href="https://www.elastic.co/docs/deploy-manage/deploy/elastic-cloud/serverless">Elastic Cloud Serverless を</a>試してみてください。この Elastic Cloud オファリングは Elastic によって完全に管理され、ワークロードに合わせて自動的に拡張されます。無料トライアルを利用すると、サーバーレス アプローチのその他の利点を理解するのに役立ちます。</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/elasticsearch-shards-and-replicas-guide</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/elasticsearch-shards-and-replicas-guide</guid>
    <category><![CDATA[基本]]></category>
    <dc:creator><![CDATA[Piotr Przybyl]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt71dd92d939d383a2/6a17e9d53e03d769c44f2cc7/7775c44f01f2516c4ff4cce6d6bbe9e7b2c38908-720x420.jpg" length="0" type="image/jpeg"/>
    <pubDate>Thu, 14 Aug 2025 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[モックと実際のElasticsearchを使用してJavaコードをテストする]]></title>
    <description><![CDATA[モックとテストコンテナを使用してElasticsearchの自動テストを作成する方法を学びます]]></description>
    <content:encoded><![CDATA[<p>この記事では、Elasticsearch を外部システム依存関係として使用してソフトウェアをテストする 2 つの方法を紹介し、説明します。モックを使用したテストと統合テストについて説明し、それらの実際の違いを示し、各スタイルでどこに向かうべきかについてヒントを提供します。</p><h2>システムの信頼性を測る優れたテスト</h2><p>優れたテストとは、IT システムの作成および保守のプロセスに関わるすべての人の信頼を高めるテストです。テストは、クールであったり、高速であったり、コード カバレッジを人為的に増やしたりするためのものではありません。テストは次のことを確認する上で重要な役割を果たします。</p><ul><li><p>私たちが提供したいものは、本番環境で機能するようになります。</p></li><li><p>システムは要件と契約を満たしています。</p></li><li><p>将来的には後退は起こりません。</p></li><li><p>開発者 (および関係する他のチーム メンバー) は、自分たちが作成したものが機能すると確信しています。</p></li></ul><p>もちろん、これはテストがクールで高速でなかったり、コード カバレッジを増加できなかったりすることを意味するものではありません。テスト スイートをより速く実行できればできるほど、良い結果が得られます。テスト スイートの全体的な期間を短縮することを目指す場合、自動テストによって得られる信頼性、保守性、および自信を犠牲にすべきではありません。</p><p>優れた自動テストにより、さまざまなチーム メンバーの自信が向上します。</p><ul><li><p>開発者: 開発者は、自分が行っている作業が機能することを確認できます (作業中のコードが自分のマシンから送信される前でも)。</p></li><li><p>品質保証チーム: 手動でテストする量が少なくなります。</p></li><li><p>システム オペレーターと SRE: システムの導入と保守が容易になるため、安心できます。</p></li></ul><p>最後になりましたが、システムのアーキテクチャについてです。私たちは、システムが整理され、保守が容易で、アーキテクチャがクリーンで目的を果たしている場合を好みます。しかし、時には「この方がテストしやすい」という言い訳のために、あまりにも多くのものを犠牲にしているアーキテクチャを目にすることがあります。テストが非常に容易であることは何も悪いことではありません。システムが、その存在を正当化するニーズに応えるのではなく、テストができるように主に書かれている場合にのみ、尻尾が犬を振るという状況が発生します。</p><h2>2種類のテスト: モックと依存関係</h2><p>テストを確認する方法、および分類する方法は数多くあります。この投稿では、テストを分割する際の 1 つの側面、つまりモック (またはスタブ、フェイクなど) の使用と実際の依存関係の使用のみに焦点を当てます。私たちの場合、依存関係は Elasticsearch です。</p><p>モックを使用したテストは、外部依存関係を開始する必要がなく、すべてがメモリ内でのみ行われるため、非常に高速です。自動テストにおけるモックとは、実際の依存関係を使用せずにプログラムの一部をテストするために、実際のオブジェクトの代わりに偽のオブジェクトを使用することです。これが、それらが必要な理由であり、高速検出ネットテストでそれらが優れている理由です。入力の検証。たとえば、リクエスト内の負の数値が許可されていないことを確認するためだけに、データベースを起動して呼び出す必要はありません。</p><p>ただし、モックを導入すると、次のようないくつかの影響が生じます。</p><ul><li><p>すべてをいつでも簡単にモックできるわけではないので、モックはシステムのアーキテクチャに影響を与えます (時には大きな影響があり、時にはそれほど大きくない影響もあります)。</p></li><li><p>モック上で実行されるテストは高速かもしれませんが、模倣するシステムを詳細に反映するモックは通常無料では提供されないため、そのようなテストの開発にはかなり時間がかかります。システムの仕組みを知っている人が、適切な方法でモックを作成する必要があります。この知識は、実際の経験やドキュメントの学習などから得られます。</p></li><li><p>モックは維持される必要があります。システムが外部の依存関係に依存していて、この依存関係をアップグレードする必要がある場合、依存関係を模倣するモックも、破壊的変更、ドキュメント化された変更、ドキュメント化されていない変更（これもシステムに影響を与える可能性があります）をすべて反映して更新されるようにする必要があります。これは、依存関係をアップグレードしたいが、(モックのみを使用した) テスト スイートではテストされたすべてのケースが確実に機能するという確信が得られない場合に特に困難になります。</p></li><li><p>モックではなくシステムの開発とテストに努力が向けられるようにするには、規律が必要です。</p></li></ul><p>これらの理由から、多くの人は正反対の方向に進むことを主張しています。つまり、モック (またはスタブなど) を決して使用せず、実際の依存関係のみに依存するということです。このアプローチは、デモや、システムが小さく、膨大なカバレッジを生成するテスト ケースが少数しかない場合に非常にうまく機能します。このようなテストには、統合テスト (大まかに言うと、システムの一部を実際の依存関係に対してチェックする) やエンドツーエンド テスト (実際の依存関係をすべて同時に使用し、システムの動作をすべてのエンドでチェックしながら、システムが使用可能かつ成功であると定義するユーザー ワークフローを再生する) などがあります。このアプローチを使用することの明らかな利点は、依存関係に関する想定と、それらを作業中のシステムにどのように統合するかについても (多くの場合は意図せずに) 検証できることです。</p><p>ただし、テストで実際の依存関係のみを使用する場合は、次の点を考慮する必要があります。</p><ul><li><p>一部のテスト シナリオでは、実際の依存関係は必要ありません (例: リクエストの静的不変条件を検証する場合)。</p></li><li><p>このようなテストは通常、フィードバックを待つのに時間がかかりすぎるため、開発者のマシンでスイート全体で実行されることはありません。</p></li><li><p>CI マシンではより多くのリソースが必要となり、時間とリソースを無駄にしないように調整するのにさらに時間がかかる可能性があります。</p></li><li><p>テストデータを使用して依存関係を初期化するのは簡単ではないかもしれません。</p></li><li><p>実際の依存関係を持つテストは、大規模なリファクタリング、移行、または依存関係のアップグレードの前にコードを封鎖するのに最適です。</p></li><li><p>これらは不透明なテストである可能性が高く、つまり、テスト対象のシステムの内部については詳細に説明されず、結果に注意が払われます。</p></li></ul><h2>スイートスポット：両方のテストを使用する</h2><p>システムを 1 種類のテストだけでテストするのではなく、適切な場合には両方の種類のテストを活用し、両方のテストの使用率を向上させることができます。</p><ul><li><p>モックベースのテストの方がはるかに高速なので、最初にモックベースのテストを実行し、すべてが成功した場合にのみ、その後でより遅い依存関係テストを実行します。</p></li><li><p>外部依存関係が実際には必要のないシナリオでは、モックを選択します。モックに時間がかかりすぎる場合は、そのためだけにコードを大幅に変更し、外部依存関係に依存します。</p></li><li><p>意味がある限り、両方のアプローチを使用してコードをテストしても問題はありません。</p></li></ul><h2>SystemUnderTestの例</h2><p>次のセクションでは、<a href="https://github.com/pioorg/testing-elasticsearch">ここに</a>ある例を使用します。これは Java 21 で記述された小さなデモ アプリケーションで、ビルド ツールとして Maven を使用し、Elasticsearch クライアントに依存し、Elasticsearch の最新の追加機能である<a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/esql.html">ES|QL</a> (Elastic の新しい手続き型クエリ言語) を使用しています。Java がプログラミング言語でない場合でも、これから説明する概念を理解し、それを自分のスタックに適用すれば問題ありません。実際のコード例を使用すると、特定の事柄を説明しやすくなります。</p><p><code>BookSearcher</code>は、データの検索と分析（この場合は書籍）の処理に役立ちます（<a href="https://www.elastic.co/search-labs/blog/esql-queries-to-java-objects">以前の投稿の 1 つ</a>で示したように）。</p><ul><li><p>唯一の依存関係として、バージョン<code>8.15.x</code>の Elasticsearch が必要です ( <code>isCompatibleWithBackend()</code>を参照)。たとえば、コードに前方互換性があるかどうか不明であり、後方互換性がないことは確実であるためです。運用中の Elasticsearch を新しいバージョンにアップグレードする前に、まずテストでそれをアップグレードして、テスト対象システムの動作が同じままであることを確認します。</p></li><li><p>これを使用して、特定の年に出版された書籍の数を検索できます ( <code>numberOfBooksPublishedInYear</code>参照)。</p></li><li><p>また、データセットを分析して、特定の 2 年間に最も多く出版された 20 人の著者を見つける必要がある場合にも、これを使用できます ( <code>mostPublishedAuthorsInYears</code>参照)。</p></li></ul>public class BookSearcher {

    private final ElasticsearchClient esClient;

    public BookSearcher(ElasticsearchClient esClient) {
        this.esClient = esClient;
        if (!isCompatibleWithBackend()) {
            throw new UnsupportedOperationException("This is not compatible with backend");
        }
    }

    private boolean isCompatibleWithBackend() {
        try (ResultSet rs = esClient.esql().query(ResultSetEsqlAdapter.INSTANCE, """
            show info
            | keep version
            | dissect version "%{major}.%{minor}.%{patch}"
            | keep major, minor
            | limit 1""")) {
            if (!rs.next()) {
                throw new RuntimeException("No version found");
            }
            return rs.getInt(1) == 8 &amp;&amp; rs.getInt(2) == 15;
        } catch (SQLException | IOException e) {
            throw new RuntimeException(e);
        }
    }

    public int numberOfBooksPublishedInYear(int year) {
        try (ResultSet rs = esClient.esql().query(ResultSetEsqlAdapter.INSTANCE, """
            from books
            | where year == ?
            | stats published = count(*) by year
            | limit 1000""", year)) {

            if (rs.next()) {
                return rs.getInt("published");
            }
        } catch (SQLException | IOException e) {
            throw new RuntimeException(e);
        }
        return 0;
    }


    public List&lt;MostPublished&gt; mostPublishedAuthorsInYears(int minYear, int maxYear) {
        assert minYear &lt;= maxYear;
        String query = """
            from books
            | where year &gt;= ? and year &lt;= ?
            | stats first_published = min(year), last_published = max(year), times = count (*) by author
            | eval years_published = last_published - first_published
            | sort years_published desc
            | drop years_published
            | limit 20
            """;

        try {
            Iterable&lt;MostPublished&gt; published = esClient.esql().query(
                ObjectsEsqlAdapter.of(MostPublished.class),
                query,
                minYear,
                maxYear);

            List&lt;MostPublished&gt; mostPublishedAuthors = new ArrayList&lt;&gt;();
            for (MostPublished mostPublished : published) {
                mostPublishedAuthors.add(mostPublished);
            }
            return mostPublishedAuthors;
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public record MostPublished(
        String author,
        @JsonProperty("first_published") int firstPublished,
        @JsonProperty("last_published") int lastPublished,
        int times
    ) {
        public MostPublished {
            assert author != null;
            assert firstPublished &lt;= lastPublished;
            assert times &gt; 0;
        }
    }
}
<h2>まずはモックでテストする</h2><p>テストで使用するモックを作成するには、Java エコシステムで非常に人気のあるモック ライブラリである<a href="https://site.mockito.org/">Mockito を</a>使用します。</p><p>各テストの前にモックをリセットするには、次のように開始します。</p>public class BookSearcherMockingTest {

    ResultSet mockResultSet;
    ElasticsearchClient esClient;
    ElasticsearchEsqlClient esql;

    @BeforeEach
    void setUpMocks() {
        mockResultSet = mock(ResultSet.class);
        esClient = mock(ElasticsearchClient.class);
        esql = mock(ElasticsearchEsqlClient.class);

    }
}
<p>前に述べたように、モックを使用してすべてを簡単にテストできるわけではありません。しかし、できること（そしておそらくそうすべきこと）もあります。現時点では Elasticsearch のサポートされているバージョンは<code>8.15.x</code>であることを確認してみましょう (将来的には、システムが将来のバージョンと互換性があることが確認されたら、範囲を拡張する可能性があります)。</p>@Test
void canCreateSearcherWithES_8_15() throws SQLException, IOException{
    // when
    when(esClient.esql()).thenReturn(esql);
    when(esql.query(eq(ResultSetEsqlAdapter.INSTANCE), anyString())).thenReturn(mockResultSet);
    when(mockResultSet.next()).thenReturn(true).thenReturn(false);
    when(mockResultSet.getInt(1)).thenReturn(8);
    when(mockResultSet.getInt(2)).thenReturn(15);

    // then
    Assertions.assertDoesNotThrow(() -&gt; new BookSearcher(esClient));
}
<p>同様に (単に異なるマイナー バージョンを返すことによって)、 <code>BookSearcher</code>が<code>8.16.x</code>とまだ動作しないことを確認できます。これは、互換性があるかどうかわからないためです。</p>@Test
void cannotCreateSearcherWithoutES_8_15() throws SQLException, IOException {
    // when
    when(esClient.esql()).thenReturn(esql);
    when(esql.query(eq(ResultSetEsqlAdapter.INSTANCE), anyString())).thenReturn(mockResultSet);
    when(mockResultSet.next()).thenReturn(true).thenReturn(false);
    when(mockResultSet.getInt(1)).thenReturn(8);
    when(mockResultSet.getInt(2)).thenReturn(16);

    // then
    Assertions.assertThrows(UnsupportedOperationException.class, () -&gt; new BookSearcher(esClient));
}
<p>ここで、実際の Elasticsearch に対してテストするときに同様のことを実現する方法を見てみましょう。このため、 <a href="https://java.testcontainers.org/modules/elasticsearch/">Testcontainers の Elasticsearch モジュール</a>を使用します。このモジュールの要件は 1 つだけで、Docker コンテナーを実行するため、Docker にアクセスする必要があります。ある角度から見ると、Testcontainers は単に Docker コンテナを操作する方法に過ぎませんが、Docker Desktop (または同様のもの)、CLI、またはスクリプトで操作する代わりに、使い慣れたプログラミング言語でニーズを表現できます。これにより、イメージの取得、コンテナの起動、テスト後のガベージコレクション、ファイルのコピー、コマンドの実行、ログの調査などがテスト コードから直接実行できるようになります。</p><p>スタブは次のようになります。</p>@Testcontainers
public class BookSearcherIntTest {

    static final String ELASTICSEARCH_IMAGE = "docker.elastic.co/elasticsearch/elasticsearch:8.15.0";
    static final JacksonJsonpMapper JSONP_MAPPER = new JacksonJsonpMapper();

    RestClientTransport transport;
    ElasticsearchClient client;

    @Container
    ElasticsearchContainer elasticsearch = new ElasticsearchContainer(ELASTICSEARCH_IMAGE);

    @BeforeEach
    void setupClient() {
        transport = // setup transport here
        client = new ElasticsearchClient(transport);
    }

    @AfterEach
    void closeClient() throws IOException {
        if (transport != null) {
            transport.close();
        }
    }

}
<p>この例では、 <a href="https://java.testcontainers.org/test_framework_integration/junit_5/">Testcontainers の JUnit 統合を</a><code>@Testcontainers</code>および<code>@Container</code>と利用しています。つまり、テストの前に Elasticsearch を起動したり、テスト後に停止したりする必要はありません。必要なのは、各テストの前にクライアントを作成し、各テストの後にクライアントを閉じることだけです (大規模なテスト スイートに影響を及ぼす可能性のあるリソース リークを回避するため)。</p><p>非静的フィールドに<code>@Container</code>注釈を付けるということは、テストごとに新しいコンテナが開始されることを意味します。そのため、古いデータやコンテナの状態のリセットについて心配する必要がありません。ただし、多くのテストではこのアプローチがうまく機能しない可能性があるため、次の投稿のいずれかで代替案と比較する予定です。</p><p><strong>注記：</strong></p><code>docker.elastic.co</code> (Elastic の公式 Docker イメージ リポジトリ) を利用することで、Docker hub の制限を超えてしまうことを回避できます。最大限の互換性を確保するために、テスト環境と本番環境で同じバージョンの依存関係を使用することをお勧めします。また、バージョンを正確に選択することをお勧めします。このため、Elasticsearch イメージには<code>latest</code>タグがありません。<h2>テストでElasticsearchに接続する</h2><p><a href="https://www.elastic.co/guide/en/elasticsearch/client/java-api-client/current/index.html">Elasticsearch Java クライアントは</a>、セキュリティと SSL/TLS が有効になっている場合でも、テスト コンテナーで実行されている Elasticsearch に接続できます (バージョン 8.x ではこれがデフォルトであるため、コンテナー宣言でセキュリティに関連するものを指定する必要はありませんでした)。実稼働環境で使用している Elasticsearch でも TLS と何らかのセキュリティが有効になっていると仮定すると、統合テストの設定を実稼働環境のシナリオにできるだけ近づけ、テストでそれらを無効にしないことが推奨されます。</p><p>コンテナがフィールドまたは変数<code>elasticsearch</code>に割り当てられていると仮定して、接続に必要なデータを取得する方法:</p><ul><li><p><code>elasticsearch.getHost()</code> コンテナが実行されているホストが提供されます (ほとんどの場合、おそらく<code>"localhost"</code>になりますが、設定によっては別の名前になる場合があるため、これをハードコードしないでください。そのため、ホストは常に動的に取得する必要があります)。</p></li><li><p><code>elasticsearch.getMappedPort(9200)</code> コンテナ内で実行されている Elasticsearch に接続するために使用する必要があるホスト ポートを指定します (コンテナを起動するたびに外部ポートが異なるため、これも動的な呼び出しである必要があります)。</p></li><li><p>上書きされない限り、デフォルトのユーザー名とパスワードはそれぞれ<code>"elastic"</code>と<code>"changeme"</code>です。</p></li><li><p>コンテナのセットアップ中に SSL/TLS 証明書が指定されておらず、セキュリティで保護された接続が無効になっていない場合 (バージョン 8.x からのデフォルトの動作)、自己署名証明書が生成されます。それを信頼する（例：<a href="https://curl.se/docs/manpage.html#--cacert">cURL</a>と同様に、証明書は<code>elasticsearch.caCertAsBytes()</code> ( <code>Optional&lt;byte[]&gt;</code>を返します) を使用して取得できます。または、別の便利な方法として、 <code>createSslContextFromCa()</code>を使用して<code>SSLContext</code>を取得することもできます。</p></li></ul><p>全体的な結果は次のようになります。</p>BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider();
credentialsProvider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials("elastic", "changeme"));

// Create a low level rest client
RestClient restClient = RestClient.builder(new HttpHost(elasticsearch.getHost(), elasticsearch.getMappedPort(9200), "https"))
    .setHttpClientConfigCallback(httpClientBuilder -&gt;
        httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider)
            .setSSLContext(elasticsearch.createSslContextFromCa())
    )
    .build();

// The RestClientTransport is mainly for serialization/deserialization
RestClientTransport transport = new RestClientTransport(restClient, new JacksonJsonpMapper());

// The official Java API Client for Elasticsearch
ElasticsearchClient client = new ElasticsearchClient(transport);
<p><code>ElasticsearchClient</code>のインスタンスを作成する別の例は、<a href="https://github.com/pioorg/testing-elasticsearch/blob/e800b4b2ab3d706efcafb9a8182480e69e475b86/src/test/java/testing_elasticsearch/BookSearcherIntTest.java#L61">デモ プロジェクト</a>にあります。</p><p><strong>注記</strong>：</p>実稼働環境でクライアントを作成する場合は、<a href="https://www.elastic.co/guide/en/elasticsearch/client/java-api-client/current/connecting.html#_verifying_https_with_a_certificate_fingerprint">ドキュメント</a>を参照してください。<h2>最初の統合テスト</h2><p>Elasticsearch バージョン 8.15.x を使用して<code>BookSearcher</code>を作成できることを確認する最初のテストは次のようになります。</p>@Test
void canCreateClientWithContainerRunning_8_15() {
    Assertions.assertDoesNotThrow(() -&gt; new BookSearcher(client));
}
<p>ご覧のとおり、他に何も設定する必要はありません。Elasticsearch によって返されるバージョンをモックする必要はありません。必要なのは、Testcontainers によって開始された Elasticsearch の実際のインスタンスに接続されたクライアントを<code>BookSearcher</code>に提供することだけです。</p><h2>統合テストは内部をあまり考慮しない</h2><p>ちょっとした実験をしてみましょう。列インデックスを使用して結果セットからデータを抽出するのをやめ、列名に頼らなければならないと仮定します。そのため、メソッド<code>isCompatibleWithBackend</code>では、</p>return rs.getInt(1) == 8 &amp;&amp; rs.getInt(2) == 15;
<p>私たちは次のことをする予定です:</p>return rs.getInt("major") == 8 &amp;&amp; rs.getInt("minor") == 15;
<p>両方のテストを再実行すると、実際の Elasticsearch との統合テストが問題なく合格していることがわかります。ただし、 <code>rs.getInt(int)</code>ではなく<code>rs.getInt(String)</code>ような呼び出しをモックしたため、モックを使用したテストは機能しなくなりました。これらを合格させるには、テスト スイート内の他のユース ケースに応じて、代わりにモックを作成するか、両方をモックする必要があります。</p><h2>統合テストはハエを殺す大砲になり得る</h2><p>統合テストは、外部依存関係が必要ない場合でも、システムの動作を検証できます。ただし、このように使用すると、通常、実行時間とリソースが無駄になります。メソッド<code>mostPublishedAuthorsInYears(int minYear, int maxYear)</code>を見てみましょう。最初の 2 行は次のとおりです。</p>assert minYear &lt;= maxYear;
String query = // here goes the query
<p>最初のステートメントは、Elasticsearch (またはその他の外部依存関係) にまったく依存しない条件をチェックしています。したがって、 <code>minYear</code>が<code>maxYear</code>より大きい場合に例外がスローされることを確認するためだけにコンテナーを起動する必要はありません。</p><p>これを確認するには、高速でリソースを大量に消費しない単純なモック テストで十分です。モックを設定したら、次の操作を実行するだけです。</p>BookSearcher systemUnderTest = new BookSearcher(esClient);

Assertions.assertThrows(
    AssertionError.class,
    () -&gt; systemUnderTest.mostPublishedAuthorsInYears(2012, 2000)
);
<p><a href="https://github.com/pioorg/testing-elasticsearch/blob/e800b4b2ab3d706efcafb9a8182480e69e475b86/src/test/java/testing_elasticsearch/BookSearcherMockingTest.java#L89">このテスト ケース</a>では、モックする代わりに依存関係を開始するのは無駄です。この依存関係に対して意味のある呼び出しを行う機会がないためです。</p><p>ただし、 <code>String query = ...</code>で始まる動作、つまりクエリが正しく記述され、期待どおりの結果が得られることを検証するには、クライアント ライブラリが適切なリクエストと応答を送信でき、構文の変更がないため、統合テストを使用する方がはるかに簡単です。例:</p>@BeforeEach
void setupDataInContainer() {
    // here we initialise data in the Elasticsearch running in a container
}

@Test
void shouldGiveMostPublishedAuthorsInGivenYears() {
    var systemUnderTest = new BookSearcher(client);
    var list = systemUnderTest.mostPublishedAuthorsInYears(1800, 2010);
    Assertions.assertEquals("Beatrix Potter", list.get(12).author(), "Beatrix Potter was 13th most published author between 1800 and 2010");
}
<p>こうすることで、Elasticsearch (このバージョンまたは移行先の将来のバージョン) にデータをフィードするときに、クエリによって期待どおりの結果が正確に返されることが保証されます。つまり、データ形式は変更されず、クエリは引き続き有効であり、すべてのミドルウェア (クライアント、ドライバー、セキュリティなど) は引き続き機能します。モックを最新の状態に保つ必要はありません。例えば、互換性を確保するために必要な変更は、<code>8.15</code>はこれを変更します:</p>static final String ELASTICSEARCH_IMAGE = "docker.elastic.co/elasticsearch/elasticsearch:8.15.0";
<p>例えば、ES|QL の代わりに古き良き QueryDSL を使用してください。クエリから受け取る結果は (言語に関係なく) 同じになるはずです。</p><h2>必要に応じて両方のアプローチを使用する</h2><p>メソッド<code>mostPublishedAuthorsInYears</code>のケースは、1 つのメソッドを両方のメソッドを使用してテストできることを示しています。そしておそらくそうなるべきなのかもしれない。</p><ul><li><p>モックのみを使用するということは、モックを維持する必要があり、システムをアップグレードするときに自信がまったくないことを意味します。</p></li><li><p>統合テストのみを使用すると、まったく必要ないのに、かなりのリソースを無駄にすることになります。</p></li></ul><h2>まとめましょう</h2><ul><li><p>Elasticsearch ではモックテストと統合テストの両方を使用できます。</p></li><li><p>fast-detection-net としてモック テストを使用し、正常に合格した場合にのみ、依存関係を使用してテストを開始します (例: <code>./mvnw test '-Dtest=!TestInt*' &amp;&amp; ./mvnw test '-Dtest=TestInt*'</code>または<a href="https://maven.apache.org/surefire/maven-failsafe-plugin/">Failsafe</a>および<a href="https://maven.apache.org/surefire/maven-surefire-plugin/">Surefire</a>プラグインを使用)。</p></li><li><p>外部依存関係との統合があまり重要でない（またはスキップできる）システムの動作（「コード行」）をテストする場合は、モックを使用します。</p></li><li><p>統合テストを使用して、外部システムに関する想定と外部システムとの統合を検証します。</p></li><li><p>上記の点に従って、意味がある場合は、両方のアプローチを使用してテストすることを恐れないでください。</p></li></ul><p>バージョン (この場合は<code>8.15.x</code> ) について厳しすぎるというのは、言い過ぎではないでしょうか。バージョン タグだけを使用することもできますが、この投稿ではバージョン間で変更される可能性のある他のすべての機能を表すものとして機能していることに注意してください。</p><p><a href="https://www.elastic.co/search-labs/blog/automated-integration-tests-faster-elasticsearch">シリーズの次回の記事</a>では、テスト データ セットを使用して、テスト コンテナーで実行される Elasticsearch を初期化する方法について説明します。このブログに基づいて何かを構築した場合、または質問がある場合は、<a href="https://discuss.elastic.co/">ディスカッション フォーラム</a>や<a href="https://communityinviter.com/apps/elasticstack/elastic-community">コミュニティ Slack チャネル</a>でお知らせください。</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/tests-with-mocks-and-real-elasticsearch</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/tests-with-mocks-and-real-elasticsearch</guid>
    <category><![CDATA[Java]]></category>
    <dc:creator><![CDATA[Piotr Przybyl]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt7e4d003f09dfbe50/6a1709aba929cf810aae0957/b6bb727815ebdb844aeb36d5c44cdf3657f0e4bc-720x420.jpg" length="0" type="image/jpeg"/>
    <pubDate>Thu, 03 Oct 2024 00:00:00 GMT</pubDate>
  </item>
  </channel>
</rss>