适用于Java开发人员的Elasticsearch:Java的Elasticsearch

本文是我们学院课程的一部分,该课程的标题为Java开发人员的Elasticsearch教程

在本课程中,我们提供了一系列教程,以便您可以开发自己的基于Elasticsearch的应用程序。 我们涵盖了从安装和操作到Java API集成和报告的广泛主题。 通过我们简单易懂的教程,您将能够在最短的时间内启动并运行自己的项目。 在这里查看

1.简介

在本教程上半部分,我们仅通过命令行工具通过利用其大量RESTful API来掌握与Elasticsearch建立有意义的对话的技能。 这是非常少的知识,但是,当您开发Java / JVM应用程序时,您将需要比命令行更好的选择。 幸运的是, Elasticsearch在这一领域提供了多种产品。

在本教程的这一部分中,我们将学习如何通过本地Java API与Elasticsearch进行通信。 我们的方法是编写代码并在几个Java应用程序上工作,使用Apache Maven进行构建管理,使用出色的Spring Framework进行依赖关系接线和控制反转 ,并使用出色的JUnit / AssertJ作为测试支架。

2.使用Java客户端API

从早期版本开始, Elasticsearch随每个发行版一起分发专用的Java客户端API ,也称为传输客户端。 它谈到了Elasticsearch本机传输协议,因此施加了这样的约束:客户端库的版本应至少与您使用的Elasticsearch发行版的主要版本匹配(理想情况下,客户端应具有完全相同的版本)。

当我们使用Elasticsearch版本5.2.0 ,将相应的客户端版本依赖项添加到我们的pom.xml文件中是有意义的。

<dependency>
    <groupId>org.elasticsearch.client</groupId>
    <artifactId>transport</artifactId>
    <version>5.2.0</version>
</dependency>

由于我们选择了Spring Framework来驱动我们的应用程序,因此实际上我们唯一需要的就是传输客户端配置。

@Configuration
public class ElasticsearchClientConfiguration {
    @Bean(destroyMethod = "close")
    TransportClient transportClient() throws UnknownHostException  {
        return new PreBuiltTransportClient(
            Settings.builder()-
                .put(ClusterName.CLUSTER_NAME_SETTING.getKey(), "es-catalog")
                .build()
            )
            .addTransportAddress(new InetSocketTransportAddress(
                InetAddress.getByName("localhost"), 9300));
    }
}

PreBuiltTransportClient遵循构建器模式 (与我们很快将要看到的大多数类一样)来构造TransportClient实例,一旦存在,我们就可以使用Spring Framework支持的注入技术来访问它:

@Autowired private TransportClient client;

CLUSTER_NAME_SETTING值得我们注意:它应该与我们要连接的Elasticsearch集群的名称完全匹配,在本例中为es-catalog

太好了,我们已经初始化了传输客户端,那么该如何处理呢? 本质上,传输客户端公开了很多方法(遵循流畅的界面样式),以打开对Java代码中所有Elasticsearch API的访问。 要迈出第一步,应该注意的是,传输客户端在常规API和管理API之间有明确的分隔。 后者可以通过在传输客户端实例上调用admin()方法获得。

在翻开袖子弄脏手之前,有必要提到, Elasticsearch Java API设计为完全异步的,因此它们围绕两个关键抽象: ActionFuture<?>ListenableActionFuture<?> 。 实际上, ActionFuture<?>只是一个普通的Java Future <?> ,其中添加了一些少数方法,请继续关注。 另一方面, ListenableActionFuture<?>是更强大的抽象,具有执行回调并将执行结果通知调用方的能力。

选择一种样式完全取决于您的应用程序需求,因为这两种样式都有其优缺点。 事不宜迟,让我们继续前进,确保我们的Elasticsearch集群运行状况良好并已准备就绪。

final ClusterHealthResponse response = client
    .admin()
    .cluster()
    .health(
        Requests
            .clusterHealthRequest()
            .waitForGreenStatus()
            .timeout(TimeValue.timeValueSeconds(5))
    )
    .actionGet();

assertThat(response.isTimedOut())
    .withFailMessage("The cluster is unhealthy: %s", response.getStatus())
    .isFalse();

该示例非常简单明了。 我们要做的是向Elasticsearch集群查询其状态,同时明确要求最多等待5 seconds以使状态变为green (如果不是这种情况)。 在client.admin().cluster().health(...)client.admin().cluster().health(...)返回ActionFuture<?>所以我们必须调用actionGet方法之一来获取响应。

这是使用Elasticsearch Java API的另一种稍有不同的方式,这次使用了prepareXxx方法家族。

final ClusterHealthResponse response = client
    .admin()
    .cluster()
    .prepareHealth()
    .setWaitForGreenStatus()
    .setTimeout(TimeValue.timeValueSeconds(5))
    .execute()
    .actionGet();

assertThat(response.isTimedOut())
    .withFailMessage("The cluster is unhealthy: %s", response.getStatus())
    .isFalse();

尽管这两个代码段均导致绝对相同的结果,但后一个代码段是在链的末尾调用client.admin().cluster().prepareHealth().execute()方法,该方法返回ListenableActionFuture<?> 。 在这个例子中,它并没有太大的区别,但是请牢记这一点,因为我们将看到更多有趣的用例,其中这样的细节实际上会改变游戏规则。

最后,最后但并非最不重要的一点是,任何API的异步特性(并且Elasticsearch Java API也不例外)假定对该操作的调用将花费一些时间,并且调用者有责任决定如何处理该操作。 到目前为止,我们仅在ActionFuture<?>实例上调用actionGet ,它将有效地将异步执行转换为阻塞(或ActionFuture<?> ,同步)调用。 此外,我们没有在同意放弃之前等待执行完成的时间方面指定期望。 我们可以做得更好,在本节的其余部分中,我们将解决这两点。

一旦我们的Elasticsearch集群状态全部变为green ,就该创建一些索引了,就像我们在本教程的上一部分中所做的一样,但是这次仅使用Java API。 创建catalog索引之前,最好确保catalog索引尚不存在。

final IndicesExistsResponse response = client
    .admin()
    .indices()
    .prepareExists("catalog")
    .get(TimeValue.timeValueMillis(100));
		
if (!response.isExists()) {
    ...
}

请注意,在上面的代码段中,我们提供了完成操作的显式超时get(TimeValue.timeValueMillis(100)) ,这实际上是execute().actionGet(TimeValue.timeValueMillis(100))的快捷方式。

对于catalog索引设置和映射类型,我们将使用与本教程上半部分相同的JSON文件catalog-index.json 。 我们将遵循Apache Maven约定将其放置在src/test/resources文件夹中。

@Value("classpath:catalog-index.json") 
private Resource index;

幸运的是, Spring Framework大大简化了对类路径资源的注入,因此我们在这里不需要做太多事情就可以访问catalog-index.json内容并将其直接提供给Elasticsearch Java API。

try (final ByteArrayOutputStream out = new ByteArrayOutputStream()) {
    Streams.copy(index.getInputStream(), out);
			
    final CreateIndexResponse response = client
        .admin()
        .indices()
        .prepareCreate("catalog")
        .setSource(out.toByteArray())
        .setTimeout(TimeValue.timeValueSeconds(1))
        .get(TimeValue.timeValueSeconds(2));
	
    assertThat(response.isAcknowledged())
        .withFailMessage("The index creation has not been acknowledged")
        .isTrue();		
}

该代码块说明了利用setSource方法调用来处理Elasticsearch Java API的另一种方法。 简而言之,我们只是以不透明的Blob(或字符串)的形式自己提供请求有效负载,并将其按原样发送到Elasticsearch节点。 但是,我们可以改用纯Java数据结构,例如:

final CreateIndexResponse response = client
    .admin()
    .indices()
    .prepareCreate("catalog")
    .setSettings(...)
    .setMapping("books", ...)
    .setMapping("authors", ...)
    .setTimeout(TimeValue.timeValueSeconds(1))
    .get(TimeValue.timeValueSeconds(2));

好的,到此,我们将结束传输客户端管理API并切换到文档和搜索API,因为您通常会使用这些API。 我们记得, Elasticsearch说的是JSON,因此我们必须以某种方式使用Java将书籍和作者转换为JSON表示形式。 实际上, Elasticsearch Java API通过支持对名为XContent的内容的通用抽象来提供XContent ,例如:

final XContentBuilder source = JsonXContent
    .contentBuilder()
    .startObject()
    .field("title", "Elasticsearch: The Definitive Guide. ...")
    .startArray("categories")
        .startObject().field("name", "analytics").endObject()
        .startObject().field("name", "search").endObject()
        .startObject().field("name", "database store").endObject()
    .endArray()
    .field("publisher", "O'Reilly")
    .field("description", "Whether you need full-text search or ...")
    .field("published_date", new LocalDate(2015, 02, 07).toDate())
    .field("isbn", "978-1449358549")
    .field("rating", 4)
    .endObject();

具有文档表示形式后,我们可以将其发送给Elasticsearch进行索引。 为了兑现承诺,这次我们希望采用真正的异步方式,而不是等待响应,而是以ActionListener<IndexResponse>的形式提供通知回调。

client
    .prepareIndex("catalog", "books")
    .setId("978-1449358549")
    .setContentType(XContentType.JSON)
    .setSource(source)
    .setOpType(OpType.INDEX)
    .setRefreshPolicy(RefreshPolicy.WAIT_UNTIL)
    .setTimeout(TimeValue.timeValueMillis(100))
    .execute(new ActionListener() {
        @Override
	  public void onResponse(IndexResponse response) {
	      LOG.info("The document has been indexed with the result: {}", 
		    response.getResult());
        }
				
        @Override
        public void onFailure(Exception ex) {
            LOG.error("The document has been not been indexed", ex);
        }
    });

很好,所以我们在books有了第一个文件! 那authors呢? 提醒一下,这本书有多个作者,因此是使用文档批量索引的绝佳时机。

final XContentBuilder clintonGormley = JsonXContent
    .contentBuilder()
    .startObject()
    .field("first_name", "Clinton")
    .field("last_name", "Gormley")
    .endObject();
		
final XContentBuilder zacharyTong = JsonXContent
    .contentBuilder()
    .startObject()
    .field("first_name", "Zachary")
    .field("last_name", "Tong")
    .endObject();

XContent部分很清楚,坦白地说,您可能永远都不会使用这种选项,而是希望对真实的类进行建模,并使用一种出色的Java库来自动进行JSON转换。 但是以下片段非常有趣。

final BulkResponse response = client
    .prepareBulk()
    .add(
        Requests
            .indexRequest("catalog")
            .type("authors")
            .id("1")
            .source(clintonGormley)
            .parent("978-1449358549")
            .opType(OpType.INDEX)
    )
    .add(
        Requests
            .indexRequest("catalog")
            .type("authors")
            .id("2")
            .source(zacharyTong)
            .parent("978-1449358549")
            .opType(OpType.INDEX)
    )
    .setRefreshPolicy(RefreshPolicy.WAIT_UNTIL)
    .setTimeout(TimeValue.timeValueMillis(500))
    .get(TimeValue.timeValueSeconds(1));
		
assertThat(response.hasFailures())
    .withFailMessage("Bulk operation reported some failures: %s", 
        response.buildFailureMessage())
    .isFalse();

我们正在单批发送两个针对authors集合的索引请求。 您可能想知道这个parent("978-1449358549")含义,要回答这个问题,我们必须回想起, booksauthors是使用父母/子女关系建模的。 因此,在这种情况下, parent键是对books各个父文档的引用(通过_id属性)。

做得好,所以我们知道如何使用索引以及如何使用Elasticsearch传输客户端Java API对文档建立索引。 现在是搜索时间!

final SearchResponse response = client
    .prepareSearch("catalog")
    .setTypes("books")
    .setSearchType(SearchType.DFS_QUERY_THEN_FETCH)
    .setQuery(QueryBuilders.matchAllQuery())
    .setFrom(0)
    .setSize(10)
    .setTimeout(TimeValue.timeValueMillis(100))
    .get(TimeValue.timeValueMillis(200));

assertThat(response.getHits().hits())
    .withFailMessage("Expecting at least one book to be returned")
    .isNotEmpty();

可以提出的最简单的搜索标准是匹配所有文档,这就是我们在上面的摘录中所做的(请注意,我们明确将返回的结果数限制为10文档)。

幸运的是, Elasticsearch Java API以QueryBuildersQueryBuilder类的形式全面实现了Query DSL ,因此编写(和维护)复杂的查询非常容易。 作为练习,我们将构建与本教程的上一部分相同的复合查询:

final QueryBuilder query = QueryBuilders
    .boolQuery()
        .must(
            QueryBuilders
                .rangeQuery("rating")
                .gte(4)
        )
        .must(
            QueryBuilders
                .nestedQuery(
                    "categories", 
                    QueryBuilders.matchQuery("categories.name", "analytics"),
                    ScoreMode.Total
                )
            )
        .must(
            QueryBuilders
                .hasChildQuery(
                    "authors", 
                    QueryBuilders.termQuery("last_name", "Gormley"),
                    ScoreMode.Total
                )
    );

该代码看起来漂亮,简洁,易于阅读。 如果您热衷于使用Java编程语言的静态导入功能,则查询看起来会更加紧凑。

final SearchResponse response = client
    .prepareSearch("catalog")
    .setTypes("books")
    .setSearchType(SearchType.DFS_QUERY_THEN_FETCH)
    .setQuery(query)
    .setFrom(0)
    .setSize(10)
    .setFetchSource(
        new String[] { "title", "publisher" }, /* includes */ 
        new String[0] /* excludes */
    )
    .setTimeout(TimeValue.timeValueMillis(100))
    .get(TimeValue.timeValueMillis(200));

assertThat(response.getHits().hits())
    .withFailMessage("Expecting at least one book to be returned")
    .extracting("sourceAsString", String.class)
    .hasOnlyOneElementSatisfying(source -> {
        assertThat(source).contains("Elasticsearch: The Definitive Guide.");
    });

为了使两个版本的查询保持相同,我们还通过setFetchSource方法提示搜索请求,我们只对返回文档源的title和Publisher属性感兴趣。

好奇的读者可能想知道如何将聚合与搜索请求一起使用。 这是一个非常好的话题,所以让我们先讨论一下。 与Query DSL一起Elasticsearch Java API还提供了聚合DSL ,围绕AggregationBuildersAggregationBuilder类展开。 例如,这就是我们可以通过publisher属性构建存储桶聚合的方法。

final AggregationBuilder aggregation = AggregationBuilders
    .terms("publishers")
    .field("publisher")
    .size(10);

定义好聚合之后,我们可以使用addAggregation方法调用将它们注入搜索请求中,如下面的代码片段所示:

final SearchResponse response = client
    .prepareSearch("catalog")
    .setTypes("books")
    .setSearchType(SearchType.DFS_QUERY_THEN_FETCH)
    .setQuery(QueryBuilders.matchAllQuery())
    .addAggregation(aggregation)
    .setFrom(0)
    .setSize(10)
    .setTimeout(TimeValue.timeValueMillis(100))
    .get(TimeValue.timeValueMillis(200));

final StringTerms publishers = response.getAggregations().get("publishers");
assertThat(publishers.getBuckets())
    .extracting("keyAsString", String.class)
    .contains("O'Reilly");

汇总的结果在响应中可用,并且可以通过引用汇总名称(例如本例中的publishers来检索。 但是要谨慎并谨慎使用正确的聚合类型,以免以ClassCastException的形式出现意外。 因为已经定义了发布者聚合来将术语分组到存储桶中,所以我们可以安全地将其从响应转换为StringTerms类实例。

3.使用Java Rest客户端

与使用Elasticsearch Java客户端API相关的缺点之一是要求与您正在运行的Elasticsearch版本(独立版本或集群版本)二进制兼容。

幸运的是,自5.0.0分支的第一个版本发布以来, Elasticsearch在表上带来了另一个选择: Java REST client 。 它使用HTTP协议来倾诉Elasticsearch通过调用它的RESTful API端点,是无视的版本Elasticsearch (从字面上看,它是兼容所有Elasticsearch版本)。

应当指出的是,尽管Java REST客户端相当低级,并且使用起来不像Java客户端API那样方便,但实际上并非如此。 但是,出于很多原因,人们可能更喜欢使用Java REST客户端而不是Java客户端APIElasticsearch进行通信,因此值得进行自己的讨论。 首先,让我们将相应的依赖项包含到我们的Apache Maven pom.xml文件中。

<dependency>
    <groupId>org.elasticsearch.client</groupId>
    <artifactId>rest</artifactId>
    <version>5.2.0</version>
</dependency>

从配置的角度来看,我们只需要通过调用RestClient.builder方法来构造RestClient的实例。

@Configuration
public class ElasticsearchClientConfiguration {
    @Bean(destroyMethod = "close")
    RestClient transportClient() {
        return RestClient
            .builder(new HttpHost("localhost", 9200))
            .setRequestConfigCallback(new RequestConfigCallback() {
                  @Override
                  public Builder customizeRequestConfig(Builder builder) {
                      return builder
                          .setConnectTimeout(1000)
                          .setSocketTimeout(5000);
                  }
            })
            .build();
    }
}

我们在这里取得了一些进步,但是请特别注意正确超时的配置,因为Java REST客户端没有提供(至少目前)基于每个请求级别指定超时的方法。 这样,我们可以使用Spring Framework为我们提供的相同接线技术,在任何地方注入RestClient实例:

@Autowired private RestClient client;

为了公平地比较Java客户端APIJava REST客户端 ,我们将剖析上一部分中看到的几个示例,并通过检查Elasticsearch集群运行状况来确定阶段。

@Test
public void esClusterIsHealthy() throws Exception {
    final Response response = client
        .performRequest(HttpGet.METHOD_NAME, "_cluster/health", emptyMap());

    final Object json = defaultConfiguration()
        .jsonProvider()
        .parse(EntityUtils.toString(response.getEntity()));
		
    assertThat(json, hasJsonPath("$.status", equalTo("green")));
}

确实,差异是显而易见的。 您可能会猜到, Java REST客户端实际上是更通用,知名和受人尊敬的Apache Http Client库的一个瘦包装。 响应以字符串或字节数组的形式返回,调用者有责任将其转换为JSON并提取必要的数据。 为了在测试断言中处理该问题,我们已经启用了出色的JsonPath库,但是您可以在这里自由选择。

一组performRequest方法是使用Java REST客户端 API进行同步(或阻止)通信的典型方法。 另外,还有一类performRequestAsync方法,应该在完全异步的流中使用。 在下一个示例中,我们将使用其中之一来将文档编books

用Java语言表示类似于JSON的结构的最简单方法是使用普通的旧Map<String, Object>如下面的代码片段所示。

final Map<String, Object> source = new LinkedHashMap<>();
source.put("title", "Elasticsearch: The Definitive Guide. ...");
source.put("categories", 
    new Map[] {
        singletonMap("name", "analytics"),
        singletonMap("name", "search"),
        singletonMap("name", "database store")
    }
);
source.put("publisher", "O'Reilly");
source.put("description", "Whether you need full-text search or ...");
source.put("published_date", "2015-02-07");
source.put("isbn", "978-1449358549");
source.put("rating", 4);

现在,我们需要将此Java结构转换为有效的JSON字符串。 这样做有很多方法,但是我们将利用json-smart库,因为它已经可以作为JsonPath库的传递依赖项使用

final HttpEntity payload = new NStringEntity(JSONObject.toJSONString(source), 
    ContentType.APPLICATION_JSON);

准备好有效负载后,没有什么可以阻止我们调用Elasticsearch的 Indexing API将一本书添加到books集合中。

client.performRequestAsync(
    HttpPut.METHOD_NAME, 
    "catalog/books/978-1449358549",
    emptyMap(),
    payload,
    new ResponseListener() {
        @Override
        public void onSuccess(Response response) {
            LOG.info("The document has been indexed successfully");
        }
				
        @Override
        public void onFailure(Exception ex) {
            LOG.error("The document has been not been indexed", ex);
        }
    });

这次我们决定不等待响应,而是提供一个回调( ResponseListener实例),以保持流真正异步。 最后,最好了解执行或多或少切合实际的搜索请求并解析结果所需的内容。

如您所料, Java REST客户端不提供围绕Query DSL的任何流畅的API,因此我们不得不再回退一次Map<String, Object>以便构建搜索条件。

final Map<String, Object> authors = new LinkedHashMap<>();
authors.put("type", "authors");
authors.put("query", 
    singletonMap("term",
        singletonMap("last_name", "Gormley")
    )
);
		
final Map<String, Object> categories = new LinkedHashMap<>();
categories.put("path", "categories");
categories.put("query",
    singletonMap("match", 
        singletonMap("categories.name", "search")
    )
);
		
final Map<String, Object> query = new LinkedHashMap<>();
query.put("size", 10);
query.put("_source", new String[] { "title", "publisher" });
query.put("query", 
    singletonMap("bool",
        singletonMap("must", new Map[] {
            singletonMap("range",
                singletonMap("rating", 
                    singletonMap("gte", 4)
                )
            ),
            singletonMap("has_child", authors),
            singletonMap("nested", categories)
        })
    )
);

公开解决问题需要付出的代价是编写许多繁琐且容易出错的代码。 在这方面, Java客户端API的一致性和简洁性确实产生了巨大的差异。 您可能会争辩说,实际上可能有人依赖更简单,更安全的技术,例如数据传输对象值对象 ,或者甚至具有带有占位符的JSON搜索查询模板,但重点是Java REST客户端在此提供了一些帮助。时刻。

final HttpEntity payload = new NStringEntity(JSONObject.toJSONString(query), 
    ContentType.APPLICATION_JSON);

final Response response = client
    .performRequest(HttpPost.METHOD_NAME, "catalog/books/_search", 
        emptyMap(), payload);

final Object json = defaultConfiguration()
    .jsonProvider()
    .parse(EntityUtils.toString(response.getEntity()));

assertThat(json, hasJsonPath("$.hits.hits[0]._source.title", 
    containsString("Elasticsearch: The Definitive Guide.")));

在此处添加的内容不多,只需查阅格式的Search API文档,然后从响应中提取您感兴趣的详细信息,就像我们通过声明_sourcetitle property所做的那样。

到此,我们结束了关于Java REST client的讨论。 坦率地说,与选择Java生态系统所具有的通用HTTP客户端之一相比,使用它是否有任何好处还不清楚。 确实,这确实是一个令人担忧的问题,但是请记住, Java REST客户端Elasticsearch系列的新成员,希望我们很快就会看到很多激动人心的功能。

4.使用测试套件

随着我们的应用程序变得越来越复杂和分散,正确的测试变得前所未有的重要。 多年来, Elasticsearch提供了卓越的测试工具 ,以简化严重依赖其搜索和分析功能的应用程序的测试。 更具体地说,您的项目中可能需要两种测试:

  • 单元测试 :那些正在独立测试单个单元(例如fe类)的测试,通常不需要具有正在运行的Elasticsearch节点或集群。 这些测试由ESTestCaseESTokenStreamTestCase支持。
  • 集成测试 :这些测试正在测试完整的流程,通常需要至少一个运行的Elasticsearch节点(或集群,以强调更实际的场景)。 这些测试由ESIntegTestCaseESSingleNodeTestCaseESBackCompatTestCase

让我们再翻一次袖子,学习如何使用Elasticsearch提供的测试支架来开发我们自己的测试套件。 我们将从声明依赖关系开始,仍然使用Apache Maven

<dependency>
    <groupId>org.apache.lucene</groupId>
    <artifactId>lucene-test-framework</artifactId>
    <version>6.4.0</version>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>org.elasticsearch.test</groupId>
    <artifactId>framework</artifactId>
    <version>5.2.0</version>
    <scope>test</scope>
</dependency>

尽管这不是绝对必要的,但我们还将显式依赖项添加到JUnit ,将其版本提高到4.12

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
    <exclusions>
        <exclusion>
            <groupId>org.hamcrest</groupId>
            <artifactId>hamcrest-core</artifactId>
        </exclusion>
    </exclusions>
</dependency>

我们在这里需要提请注意: Elasticsearch测试框架对依赖项异常敏感,确保您的应用程序不会陷入每个Java开发人员都熟知的jar hell的问题Elasticsearch测试框架所做的一项预检查是确保classpath中没有重复的类。 通常,您可能会在此过程中使用其他出色的测试库,但如果您的Elasticsearch测试用例突然开始无法通过初始化阶段,则很可能是由于检测到jar地狱问题,因此必须进行一些排除。

还有一件事,很可能您需要在测试运行期间通过将tests.security.manager属性设置为false来关闭安全管理器。 可以通过将-Dtests.security.manager=false参数直接传递给JVM或使用Apache Maven插件配置来完成。

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>2.19.1</version>
    <configuration>
        <argLine>-Dtests.security.manager=false</argLine>
    </configuration>
</plugin>

太棒了,所有前提条件都得到了解释,我们都准备开始开发第一个测试用例。 适用于Elasticsearch的上下文中的单元测试对于测试您自己的分析器令牌生成 令牌过滤器字符过滤器非常有用。 在这方面,我们没有做太多事情,但是集成测试是一个截然不同的故事。 让我们看看如何启动具有3节点的Elasticsearch集群。

@ClusterScope(numDataNodes = 3)
public class ElasticsearchClusterTest extends ESIntegTestCase {
}

……从字面上看,就是这样。 当然,尽管群集已启动,但它没有索引或未预先配置的内容。 让我们添加一些测试背景,以使用相同的catalog-index.json文件创建catalog索引及其映射类型。

@Before
public void setUpCatalog() throws IOException {
    try (final ByteArrayOutputStream out = new ByteArrayOutputStream()) {
        Streams.copy(getClass().getResourceAsStream("/catalog-index.json"), 
            out);
			
        final CreateIndexResponse response = admin()
		.indices()
            .prepareCreate("catalog")
            .setSource(out.toByteArray())
            .get();
			
        assertAcked(response);
        ensureGreen("catalog");
    }
}

如果您已经识别出此代码,那是因为我们使用的是之前了解的相同传输客户端! 如果您需要Java REST客户端实例, Elasticsearch测试脚手架会在client()admin()方法之后为您提供该功能,并与getRestClient()一起提供。 每次测试运行后清理集群都是一件好事,幸运的是,我们可以使用cluster()方法来访问几个非常有用的操作,例如:

@After
public void tearDownCatalog() throws IOException, InterruptedException {
    cluster().wipeIndices("catalog");
}

总体而言, Elasticsearch测试工具的目标是两个目标:简化最常见的任务(我们已经看到了client()admin()cluster()实际运行)以及轻松进行验证,声明或期望(例如, ensureGreen(...)assertAcked(...) )。 官方文档有专门的部分介绍了辅助方法断言,因此请看一看。

首先,空索引中应该没有文档,因此我们的第一个测试用例将明确声明这一事实。

@Test
public void testEmptyCatalogHasNoBooks() {
    final SearchResponse response = client()
        .prepareSearch("catalog")
        .setTypes("books")
        .setSearchType(SearchType.DFS_QUERY_THEN_FETCH)
        .setQuery(QueryBuilders.matchAllQuery())
        .setFetchSource(false)
        .get();

    assertNoSearchHits(response);	
}

一个简单,但是创建真实文档呢? Elasticsearch测试框架具有多种有用的方法,可以为大多数类型生成随机值。 我们可以利用它来创建一本书,将其添加到图书catalog索引中并对其发出查询。

@Test
public void testInsertAndSearchForBook() throws IOException {
    final XContentBuilder source = JsonXContent
        .contentBuilder()
	  .startObject()
        .field("title", randomAsciiOfLength(100))
        .startArray("categories")
            .startObject().field("name", "analytics").endObject()
            .startObject().field("name", "search").endObject()
            .startObject().field("name", "database store").endObject()
        .endArray()
        .field("publisher", randomAsciiOfLength(20))
        .field("description", randomAsciiOfLength(200))
        .field("published_date", new LocalDate(2015, 02, 07).toDate())
        .field("isbn", "978-1449358549")
        .field("rating", randomInt(5))
        .endObject();
		
    index("catalog", "books", "978-1449358549", source);
    refresh("catalog");
		
    final QueryBuilder query = QueryBuilders
        .nestedQuery(
            "categories", 
            QueryBuilders.matchQuery("categories.name", "analytics"),
            ScoreMode.Total
        );
    	
    final SearchResponse response = client()
        .prepareSearch("catalog")
        .setTypes("books")
        .setSearchType(SearchType.DFS_QUERY_THEN_FETCH)
        .setQuery(query)
        .setFetchSource(false)
        .get();

    assertSearchHits(response, "978-1449358549");		
}

如您所见,除了categories之外,大多数书籍属性都是随机生成的,因此我们可以通过它们可靠地进行搜索。

Elasticsearch测试支持提供了许多有趣的机会,不仅可以测试成功的结果,而且可以模拟现实的集群行为和错误的条件(这里的internalCluster()提供的辅助方法非常有用)。 对于像Elasticsearch这样的复杂分布式系统,此类测试的价值是无价的,因此请利用可用的选项来确保部署到生产中的代码健壮并能够应对故障。 举个简单的例子,我们可以在运行搜索请求时关闭随机数据节点,并断言它们仍在处理中。

@Test
public void testClusterNodeIsDown() throws IOException {
    internalCluster().stopRandomDataNode();
        
    final SearchResponse response = client()
        .prepareSearch("catalog")
        .setTypes("books")
        .setSearchType(SearchType.DFS_QUERY_THEN_FETCH)
        .setQuery(QueryBuilders.matchAllQuery())
        .setFetchSource(false)
        .get();

    assertNoSearchHits(response);
}

我们只是简单介绍了Elasticsearch测试工具的功能。 希望您在组织中实践测试驱动的开发,并且我们所研究的示例可以很好地为您提供起点。

5。结论

在本教程的这一部分中,我们学习了Elasticsearch开箱即用提供的两种Java客户端API: Transport客户端REST客户端 。 您可能会发现很难选择要使用哪种Java客户端API,但是总的来说,它高度依赖于应用程序。 在大多数情况下, 传输客户端是最佳选择,但是,如果您的项目仅使用几个Elasticsearch API(或功能的非常有限的子集),则REST客户端可能是一个更好的选择。 另外,我们不要忘记Java REST客户端是相当新的,并且肯定会在将来的版本中进行改进,因此请密切注意它。

当我们剖析传输客户端时 ,已经指出了它的完全异步性质。 尽管这绝对是一件好事,但我们已经看到它基于回调(更确切地说是侦听器),这可能会Swift导致称为回调hell的问题。 强烈建议尽早解决此问题(幸运的是,有很多库和可用的替代品,例如RxJava 2Project ReactorJava 9也在赶上)。

最后但并非最不重要的一点是,我们浏览了Elasticsearch的 测试工具 ,并有机会认识到它为Java / JVM开发人员提供的巨大帮助。

6.接下来

在接下来的部分 ,即教程的最后一部分 ,我们将讨论围绕Elasticsearch的出色项目的生态系统。 希望您再一次对Elasticsearch的功能感到惊讶,为您打开它的适用性的新视野。

所有项目的完整源代码都可以下载: elasticsearch-client-restelasticsearch-testingelasticsearch-client-java

翻译自: https://www.javacodegeeks.com/2017/03/elasticsearch-java-developers-elasticsearch-java.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值