<?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/cn/search-labs/author/piotr-przybyl</link>
    </image>
    <link>https://www.elastic.co/cn/search-labs/author/piotr-przybyl</link>
    <atom:link href="https://www.elastic.co/cn/search-labs/rss/author/piotr-przybyl.xml" rel="self" type="application/rss+xml"/>
    <language><![CDATA[cn]]></language>
    <lastBuildDate>Mon, 28 Sep 2026 08:28:29 GMT</lastBuildDate>
  <item>
    <title><![CDATA[Elasticsearch 分片和副本：实用指南]]></title>
    <description><![CDATA[掌握 Elasticsearch 分片和副本的概念，并学习如何优化它们。]]></description>
    <content:encoded><![CDATA[<p>Elasticsearch 在 Lucene 的基础上建立了一个分布式系统，解决了可扩展性和容错问题，从而增强了 Lucene 的功能。它还提供基于 JSON 的 REST 应用程序接口，使与其他系统的互操作性变得非常简单。</p><p>Elasticsearch 等分布式系统可能非常复杂，影响其性能和稳定性的因素很多。<strong>分片</strong>是 Elasticsearch 中最基本的概念之一，了解分片的工作原理将使您能够有效地管理 Elasticsearch 集群。</p><p>本文将解释什么是主分片和副本分片，它们对 Elasticsearch 集群的影响，以及有哪些工具可以调整它们以适应不同的需求。</p><h2>了解碎片</h2><p>Elasticsearch 索引中的数据可能会大量增长。为了便于管理，每条数据都保存在一个索引中，而索引是将一个索引分割成若干<strong>碎片</strong>。每个 Elasticsearch 分区都是一个 Apache Lucene 索引，每个单独的 Lucene 索引都包含 Elasticsearch 索引中文档的一个子集。以这种方式拆分索引可以控制资源使用量。Apache Lucene 索引的上限为 2,147,483,519 (2³¹ - 129) 个文档。</p><p>有时，出于重新平衡的目的，需要在节点间移动指数。由于这一过程需要大量时间和资源，因此索引不应过大，这有助于保持可控的恢复时间。此外，由于索引是由需要不断合并在一起的 Lucene 段组成的，因此段不能太大，这一点很重要。由于这些原因，Elasticsearch 将索引数据分割成更易于管理的小块（称为<strong>主分片</strong>），这些分片可以更方便地分布在多台计算机上。<strong>复制</strong>分区只是相应主分区的一个精确副本，我们将在本文稍后部分介绍它们的功能。</p><p>拥有适当数量的分片对性能非常重要。因此，提前制定计划是明智之举。当查询在不同分片上并行运行时，其执行速度要快于由单个分片组成的索引，但前提是每个分片位于不同的节点上，且集群中有足够多的节点。但与此同时，分片也会消耗内存和磁盘空间，包括索引数据和集群元数据。分片过多（也称为过度分片）会降低查询、索引请求和管理操作的速度，因此保持适当的平衡至关重要。</p><p>主分区的数量是在<strong>为特定索引实例</strong>创建索引时定义的。如果以后需要不同数量的主分片，可以使用<strong> 调整大小</strong><a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-indices-split">API</a> --拆分（更多的主分片）、<a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-indices-shrink">收缩</a>（更少的主分片）或<a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-indices-clone">克隆</a>（相同数量的主分片，并对副本进行新的设置）。创建索引时，可以<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-50GB 的数据</a>，每个分区的文件数少于 2 亿。例如，如果您预计一天内会积累约 300GB 的应用程序日志，那么在该索引中设置约 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 中添加索引名称，例如传感器：</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>每个分区只包含一份数据副本，而索引则可以包含多个分区副本。因此有两种分片，<strong>即主分片</strong>和副本或<strong>复制</strong> 分片。主分片的每个副本总是位于不同的节点上，这就确保了在节点发生故障时数据的高可用性。除了冗余及其在防止数据丢失和宕机方面的作用外，副本还可以帮助提高搜索性能，因为它允许查询与主分片并行处理，因此速度更快。</p><p>主分片和副本分片的行为方式存在一些重要差异。虽然两者都能处理查询、索引请求（即向索引添加数据）必须先经过主分片，然后才能复制到副本分片。如上所述，如果主分片不可用--例如，由于节点断开或硬件故障--副本就会被提升以接替其角色。</p><p>虽然复制可以在节点发生故障时提供帮助，但重要的是不要有太多的复制，因为它们会在编制索引时消耗内存、磁盘空间和计算能力。主分片和副本之间的另一个区别是，虽然主分片的数量在索引创建后无法更改，但副本的数量可以通过更新索引设置随时动态更改。</p><p>复制的另一个考虑因素是可用节点的数量。副本总是放在与主分片不同的节点上，因为如果节点发生故障，同一节点上的两个相同数据副本将无法提供保护。因此，一个系统要支持<em>n 个</em>副本，集群中至少需要有<em>n + 1 个</em>节点。例如，如果集群中有两个节点，而索引配置了六个副本，则只会分配一个副本。另一方面，拥有七个节点的系统完全可以处理一个主分片和六个副本。</p><h2>优化分片和副本</h2><p>即使在创建了主分片和副本分片平衡得当的索引后，也需要对这些分片进行监控，因为索引的动态会随着时间的推移而发生变化。例如，在处理时间序列数据时，最新数据的指数通常比旧数据的指数更活跃。如果不对这些指数进行调整，它们将消耗相同数量的资源，尽管它们的需求非常不同。</p><p>翻转索引 API 可用于区分新旧索引。可以对其进行设置，一旦达到某个阈值（磁盘上索引的大小、文档数量或年限），它就会自动创建新索引。该 API 对于控制分片大小也很有用。由于索引创建后无法轻易更改分片数量，因此如果不满足翻转条件，分片将继续积累数据。对于只需不经常访问的旧索引，缩小和强制合并索引是减少其内存和磁盘占用的两种不同方法。前者减少了索引中分片的数量，后者则减少了 Lucene 片段的数量，并释放了已删除文档的空间。</p><h2>作为 Elasticsearch 基础的主分片和副本分片</h2><p>Elasticsearch 作为适用于海量数据的分布式存储、搜索和分析平台，已经建立了良好的声誉。然而，在如此大规模的运作中，挑战将不可避免地出现。这就是为什么了解主分片和副本分片如何工作对 Elasticsearch 如此重要和基础的原因，因为这有助于优化平台的可靠性和性能。</p><p>了解它们如何工作以及如何优化它们，对于实现更强大、更高性能的 Elasticsearch 集群至关重要。如果您经常遇到查询响应迟缓或中断的情况，这些知识可能是克服这些障碍的关键。</p><p>请关注 Elasticsearch 的官方文档，了解有关<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>更多信息。</p><p>本主题还可作为入门课程在<a href="https://youtu.be/sAySPSyL2qE">Elastic Community YouTube 频道</a>上观看。</p><p>最后但并非最不重要的一点：如果你不想担心节点、分片或副本，可以试试<a href="https://www.elastic.co/docs/deploy-manage/deploy/elastic-cloud/serverless">Elastic Cloud Serverless</a>。该 Elastic 云产品由 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 作为外部系统依赖关系测试软件的两种方法。我们将介绍使用模拟测试和集成测试的测试方法，展示它们之间的一些实际区别，并就每种风格的测试方法给出一些提示。</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>系统运营商和自力更生者：由于系统更易于部署和维护，因此更加轻松。</p></li></ul><p>最后但并非最不重要的一点：系统架构。我们喜欢系统井井有条、易于维护、架构简洁并能实现其目的。不过，有时我们可能会看到这样一种架构，它为了所谓的 ""，这样更容易测试 "的借口而牺牲了太多" 。具有很强的可测试性并没有错--只有当编写系统的主要目的是为了可测试，而不是为了满足证明其存在合理性的需求时，我们才会看到 "狗尾续貂 "的情况。</p><h2>两种测试模拟&amp; 依赖关系</h2><p>可以从很多方面来看待测试，从而对其进行分类。在这篇文章中，我将只关注划分测试的一个方面：使用模拟（或存根、伪造或......）与使用真实依赖关系。在我们的案例中，依赖关系是 Elasticsearch。</p><p>使用 mock 的测试速度非常快，因为它们不需要启动任何外部依赖关系，而且一切都只在内存中进行。自动测试中的 "模拟 "是指使用假对象代替真对象，在不使用实际依赖关系的情况下测试程序的某些部分。这就是需要它们的原因，也是它们在任何快速检测网测试中大放异彩的原因，例如输入验证。例如，无需启动数据库并调用它来验证请求中的负数是否不允许。</p><p>然而，引入模拟器会产生一些影响：</p><ul><li><p>并不是每件事、每段时间都能轻松模拟，因此模拟会对系统架构产生影响（有时影响很大，有时影响不大）。</p></li><li><p>在模拟系统上运行的测试可能会很快，但开发此类测试可能需要相当长的时间，因为模拟系统通常不是免费提供的，而是要深入反映它们所模拟的系统。了解系统工作原理的人需要以正确的方式编写模拟，而这种知识可以来自实践经验、研究文档等。</p></li><li><p>需要对模拟进行维护。当你的系统依赖于一个外部依赖关系时，如果你需要升级这个依赖关系，就必须有人确保模仿这个依赖关系的 mock 也会随着所有变化而更新：破坏性的、有文档记录的和无文档记录的（这也会对我们的系统产生影响）。当你想升级一个依赖项，但你的测试套件（仅使用模拟）却无法让你确信所有的测试用例都能正常工作时，这种情况就会变得特别痛苦。</p></li><li><p>要确保将精力用于开发和测试系统，而不是模拟，这需要纪律。</p></li></ul><p>因此，很多人主张反其道而行之：永远不要使用 mock（或存根等），而要完全依赖真实的依赖关系。这种方法在演示或系统很小、只有少量测试用例、覆盖范围很大的情况下非常有效。这些测试可以是集成测试（粗略地说：根据一些真实的依赖关系来检查系统的一部分），也可以是端到端测试（同时使用所有真实的依赖关系，并在所有端点检查系统的行为，同时播放用户工作流，以确定系统的可用性和成功性）。使用这种方法的一个明显好处是，我们还（经常无意中）验证了我们对依赖关系的假设，以及我们如何将它们与我们正在开发的系统进行整合。</p><p>不过，当测试只使用真实依赖关系时，我们需要考虑以下几个方面：</p><ul><li><p>有些测试场景并不需要实际的依赖关系（如验证请求的静态不变性）。</p></li><li><p>此类测试通常不会在开发人员的机器上整套运行，因为等待反馈会耗费太多时间。</p></li><li><p>它们需要更多的 CI 机器资源，而且可能需要更多的时间来调整，以避免浪费时间&amp; 。</p></li><li><p>使用测试数据初始化依赖关系可能并非易事。</p></li><li><p>具有真实依赖关系的测试非常适合在重大重构、迁移或依赖关系升级之前对代码进行封锁。</p></li><li><p>它们更可能是不透明的测试，即不详细说明被测系统的内部结构，只关心测试结果。</p></li></ul><h2>甜蜜点：同时使用两种测试</h2><p>与其只用一种测试对系统进行测试，不如在合理的情况下同时使用两种测试，并尝试改进两种测试的使用。</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">之前的一篇文章</a>所示）。</p><ul><li><p>例如，我们不确定我们的代码是否向前兼容，也不确定它是否向后兼容，因此我们需要 Elasticsearch 的版本<code>8.15.x</code> 作为唯一的依赖关系（参见<code>isCompatibleWithBackend()</code> ）。在将生产中的 Elasticsearch 升级到更新的版本之前，我们应首先在测试中对其进行升级，以确保被测系统的行为保持不变。</p></li><li><p>我们可以用它来搜索某一年出版的图书数量（见<code>numberOfBooksPublishedInYear</code> ）。</p></li><li><p>当我们需要分析我们的数据集并找出两个给定年份之间发表论文最多的 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>，它只有一个要求：需要访问 Docker，因为它会为你运行 Docker 容器。从某种角度看，Testcontainers 只是操作 Docker 容器的一种方式，但您可以用自己熟悉的编程语言来表达自己的需求，而不是在 Docker 桌面（或类似工具）、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 和测试后停止 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>能够连接到运行在测试容器中的 Elasticsearch，即使安全和 SSL/TLS 已启用（这是 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 返回的版本，唯一需要做的就是为<code>BookSearcher</code> 提供一个连接到 Elasticsearch 真实实例的客户端，该实例已由 Testcontainers 为我们启动。</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> 方法。前两行如下</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>如果您决定例如使用传统的 QueryDSL 而不是 ES|QL：从查询中获得的结果（无论使用哪种语言）应该是一样的。</p><h2>必要时使用两种方法</h2><p><code>mostPublishedAuthorsInYears</code> 的情况说明，一种方法可以同时使用两种方法进行测试。也许还应该是这样。</p><ul><li><p>只使用模拟系统意味着我们必须维护模拟系统，并且在升级系统时没有信心。</p></li><li><p>只使用集成测试意味着我们浪费了大量资源，却根本不需要这些资源。</p></li></ul><h2>让我们回顾一下</h2><ul><li><p>可以在 Elasticsearch 中同时使用模拟测试和集成测试。</p></li><li><p>使用模拟测试作为快速检测网，只有当测试成功通过后，才启动带有依赖关系的测试（如使用<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>在测试系统行为（"行代码" ）时使用 mock，与外部依赖关系的整合并不重要（甚至可以省略）。</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>