Статьи

Elasticsearch для разработчиков Java: Elasticsearch от Java

Эта статья является частью нашего курса Академии под названием Elasticsearch Tutorial для разработчиков Java .

В этом курсе мы предлагаем серию руководств, чтобы вы могли разрабатывать свои собственные приложения на основе Elasticsearch. Мы охватываем широкий спектр тем, от установки и эксплуатации до интеграции Java API и создания отчетов. С нашими простыми учебными пособиями вы сможете запустить и запустить собственные проекты за минимальное время. Проверьте это здесь !

1. Введение

В предыдущей части руководства мы овладели навыками налаживания содержательных бесед с Elasticsearch , используя многочисленные API-интерфейсы RESTful , используя только инструменты командной строки. Это очень полезные знания, однако, когда вы разрабатываете приложения Java / JVM, вам потребуются лучшие параметры, чем командная строка. К счастью, у Elasticsearch есть несколько предложений в этой области.

В этой части руководства мы узнаем, как общаться с Elasticsearch с помощью нативных API-интерфейсов Java. Наш подход к этому будет заключаться в написании кода и работе с несколькими Java-приложениями с использованием Apache Maven для управления сборкой, потрясающей Spring Framework для разветвления зависимостей и инверсии управления , а также великолепного JUnit / AssertJ в качестве основы для тестирования.

2. Использование Java Client API

Начиная с ранних версий, Elasticsearch распространяет выделенный клиентский API Java с каждым выпуском, также известным как транспортный клиент. Он использует собственный транспортный протокол Elasticsearch и, как таковой, налагает ограничение на то, что версия клиентской библиотеки должна как минимум соответствовать основной версии дистрибутива Elasticsearch, которую вы используете (в идеале, клиент должен иметь точно такую ​​же версию).

Поскольку мы используем Elasticsearch версии 5.2.0 , имеет смысл добавить соответствующую версию клиента в наш файл pom.xml .

1
2
3
4
5
<dependency>
    <groupId>org.elasticsearch.client</groupId>
    <artifactId>transport</artifactId>
    <version>5.2.0</version>
</dependency>

Поскольку мы выбрали Spring Framework для работы с нашим приложением, буквально единственное, что нам нужно, — это конфигурация транспортного клиента.

01
02
03
04
05
06
07
08
09
10
11
12
13
@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, для доступа к нему:

1
@Autowired private TransportClient client;

CLUSTER_NAME_SETTING заслуживает нашего внимания: он должен точно соответствовать названию кластера Elasticsearch , к которому мы подключаемся, в нашем случае это es-catalog .

Отлично, у нас инициализирован наш транспортный клиент, так что мы можем с ним сделать? По сути, транспортный клиент предоставляет целую кучу методов (следуя свободному стилю интерфейса ), чтобы открыть доступ ко всем API Elasticsearch из кода Java. Чтобы сделать шаг вперед, следует отметить, что у транспортного клиента есть явное разделение между обычными API и API администратора. Последнее доступно путем вызова метода admin() на экземпляре транспортного клиента.

Прежде чем закатывать рукава и пачкать руки, необходимо упомянуть, что Java-API Elasticsearch спроектированы так, чтобы быть полностью асинхронными и поэтому сосредоточены вокруг двух ключевых абстракций: ActionFuture<?> И ListenableActionFuture<?> . Фактически, ActionFuture<?> — это просто старая Java Future <?> С добавлением нескольких горсток методов, следите за обновлениями. С другой стороны, ListenableActionFuture<?> Является более мощной абстракцией с возможностью принимать обратные вызовы и уведомлять вызывающую сторону о результате выполнения.

Выбор одного стиля над другим полностью продиктован потребностями ваших приложений, так как оба имеют свои плюсы и минусы. Без лишних слов давайте продолжим и убедимся, что наш кластер Elasticsearch здоров и готов к работе.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
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(...) ActionFuture<?> client.admin().cluster().health(...) возвращает ActionFuture<?> Поэтому мы должны вызвать один из методов actionGet чтобы получить ответ.

Вот еще один, немного другой способ использования Java API prepareXxx , на этот раз с использованием семейства методов prepareXxx .

01
02
03
04
05
06
07
08
09
10
11
12
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 (и Java API Elasticsearch не является исключением) предполагает, что вызов операции займет некоторое время, и вызывающая сторона сама решит, как с этим справиться. До сих пор мы использовали просто вызов actionGet для экземпляра ActionFuture<?> , ActionFuture<?> эффективно преобразует асинхронное выполнение в блокирующий (или, иначе говоря, синхронный) вызов. Более того, мы не указали ожидания в отношении того, как долго мы будем соглашаться ждать завершения исполнения, прежде чем сдаваться. Мы могли бы добиться большего, чем в этом, и в оставшейся части этого раздела мы рассмотрим оба этих пункта.

После того, как наш статус кластера Elasticsearch станет green , пришло время создать некоторые индексы, так же, как мы делали в предыдущей части руководства, но на этот раз только с использованием API Java. Было бы неплохо убедиться, что индекс catalog еще не существует до его создания.

1
2
3
4
5
6
7
8
9
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 , который мы использовали в предыдущей части руководства. Мы собираемся поместить его в папку src/test/resources , следуя соглашениям Apache Maven .

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

К счастью, Spring Framework значительно упрощает внедрение ресурсов classpath, поэтому нам не нужно много делать, чтобы получить доступ к catalog-index.json и catalog-index.json его непосредственно в Elasticsearch Java API.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
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 к API-интерфейсам Java setSource помощью вызова метода setSource . В двух словах, мы просто предоставляем полезную нагрузку запроса самостоятельно в виде непрозрачного большого двоичного объекта (или строки), и она будет отправлена ​​на один или несколько узлов Elasticsearch . Однако вместо этого мы могли бы использовать чистые структуры данных Java, например:

1
2
3
4
5
6
7
8
9
final CreateIndexResponse response = client
    .admin()
    .indices()
    .prepareCreate("catalog")
    .setSettings(...)
    .setMapping("books", ...)
    .setMapping("authors", ...)
    .setTimeout(TimeValue.timeValueSeconds(1))
    .get(TimeValue.timeValueSeconds(2));

Хорошо, с этим мы собираемся завершить API администрирования транспортного клиента и переключиться на API документов и поиска, так как именно они будут использоваться в большинстве случаев. Как мы помним, Elasticsearch говорит на JSON, поэтому нам нужно каким-то образом преобразовать книги и авторов в представление JSON с помощью Java. Фактически, Elasticsearch Java API помогает в этом, поддерживая общую абстракцию над контентом с именем XContent , например:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
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> .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
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 ? Ну, так же, как напоминание, у рассматриваемой книги есть больше чем один автор, таким образом, это — прекрасная возможность использовать массовую индексацию документа.

01
02
03
04
05
06
07
08
09
10
11
12
13
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 . Но следующий фрагмент действительно интересен.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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") и чтобы ответить на этот вопрос, мы должны вспомнить, что books и authors моделируются с использованием отношений родитель / ребенок. Таким образом, parent ключ в этом случае является ссылкой (посредством свойства _id ) на соответствующий родительский документ в коллекции books .

Отлично, поэтому мы знаем, как работать с индексами и как индексировать документы, используя Java-интерфейсы API клиента транспорта Elasticsearch. Сейчас время поиска!

01
02
03
04
05
06
07
08
09
10
11
12
13
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 имеет полноценную реализацию Query DSL в форме классов QueryBuilders и QueryBuilder поэтому написание (и сопровождение) сложных запросов исключительно легко. В качестве упражнения мы собираемся создать тот же составной запрос, который мы создали в предыдущей части урока:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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, запрос будет выглядеть еще более компактным.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
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 , вращающиеся вокруг классов AggregationBuilders и AggregationBuilder . Например, именно так мы могли бы построить агрегированное агрегирование по свойству publisher .

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

Определив агрегаты, мы могли бы внедрить их в поисковый запрос, используя addAggregation метода addAggregation как показано в фрагменте кода ниже:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
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 один из ответа к StringTerms класса StringTerms .

3. Использование Java Rest Client

Одним из недостатков, связанных с использованием клиентского API-интерфейса Elasticsearch, является требование совместимости двоичного кода с используемой версией Elasticsearch (автономной или кластерной).

К счастью, начиная с первого выпуска ветки 5.0.0 , Elasticsearch предлагает еще один вариант: клиент Java REST . Он использует протокол HTTP для связи с Elasticsearch , вызывая его конечные точки API RESTful и не обращая внимания на версию Elasticsearch (буквально, он совместим со всеми версиями Elasticsearch ).

Однако следует отметить, что Java REST-клиент довольно низкого уровня и не так удобен в использовании, как Java-клиент API , в действительности это далеко не так. Однако есть немало причин, по которым можно предпочесть использовать Java REST-клиент по сравнению с Java-клиентским API для взаимодействия с Elasticsearch, поэтому его стоит обсудить самостоятельно. Для начала, давайте включим соответствующую зависимость в наш файл Apache Maven pom.xml .

1
2
3
4
5
<dependency>
    <groupId>org.elasticsearch.client</groupId>
    <artifactId>rest</artifactId>
    <version>5.2.0</version>
</dependency>

С точки зрения конфигурации нам нужно только RestClient экземпляр RestClient , вызвав метод RestClient.builder .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
@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 не предоставляет способ (по крайней мере, на данный момент) указать их для каждого уровня запроса. При этом мы можем внедрить экземпляр RestClient любом месте, используя те же методы подключения, которые любезно предоставляет нам Spring Framework :

1
@Autowired private RestClient client;

Чтобы провести справедливое сравнение между API клиента Java и клиентом Java REST , мы собираемся проанализировать пару примеров, которые мы рассмотрели в предыдущем разделе, и описать этап, проверив работоспособность кластера Elasticsearch .

01
02
03
04
05
06
07
08
09
10
11
@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 является типичным способом для синхронного (или блокирующего) взаимодействия с использованием клиентского API Java REST . Альтернативно, существует семейство методов performRequestAsync которые должны использоваться в полностью асинхронных потоках. В следующем примере мы собираемся использовать один из них для индексации документа в коллекцию books .

Простейшим способом представления JSON- подобной структуры в языке Java является использование простого старого Map<String, Object> как показано на фрагменте кода ниже.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
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 .

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

При наличии полезной нагрузки ничто не мешает нам вызывать API индексации Elasticsearch для добавления книги в коллекцию books .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
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 не предоставляет никаких свободно распространяемых API-интерфейсов вокруг Query DSL, поэтому нам придется вернуться к Map<String, Object> еще раз, чтобы построить критерии поиска.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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)
        })
    )
);

Цена, которую нужно заплатить, решая проблему открыто, — это много громоздкого и подверженного ошибкам кода для написания. В этом отношении непротиворечивость и лаконичность клиентского API Java действительно имеет огромное значение. Вы можете утверждать, что в действительности можно полагаться на гораздо более простые и безопасные методы, такие как объект передачи данных , объекты значений или даже иметь шаблоны поисковых запросов JSON с заполнителями, но дело в том, что клиент REST Java предлагает небольшую помощь в момент.

01
02
03
04
05
06
07
08
09
10
11
12
13
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.")));

Добавлять здесь особо нечего, просто ознакомьтесь с документацией API поиска по формату и извлеките из ответа подробности, которые вас интересуют, как мы делаем, _source документа _source .

На этом мы завершим наше обсуждение клиента Java REST . Честно говоря, вам может показаться неясным, есть ли какие-либо преимущества от его использования по сравнению с выбором одного из универсальных HTTP- клиентов, которыми богата Java-экосистема. Действительно, это серьезная проблема, но имейте в виду, что клиент Java REST является совершенно новым дополнением к семейству Elasticsearch, и, мы надеемся, очень скоро мы увидим множество интересных функций.

4. Использование комплекта для тестирования

Поскольку наши приложения становятся все более сложными и распределенными, надлежащее тестирование становится таким же важным, как никогда раньше В течение многих лет Elasticsearch предоставляет превосходные средства для тестирования , чтобы упростить тестирование приложений, которые в значительной степени зависят от его функций поиска и аналитики. Более конкретно, есть два типа тестов, которые могут вам понадобиться в ваших проектах:

  • Модульные тесты : они тестируют отдельные модули (например, классы fe) изолированно и, как правило, не требуют наличия узлов или кластеров Elasticsearch . Эти виды тестов поддерживаются ESTestCase и ESTokenStreamTestCase .
  • Интеграционные тесты : они тестируют полные потоки и обычно требуют как минимум один работающий узел Elasticsearch (или кластер, чтобы подчеркнуть более реалистичные сценарии). Эти виды тестов поддерживаются ESIntegTestCase , ESSingleNodeTestCase и ESBackCompatTestCase .

Давайте еще раз закручиваем рукава и узнаем, как использовать испытательные леса, предоставляемые Elasticsearch, для разработки наших собственных тестовых пакетов. Мы собираемся начать с объявления наших зависимостей, все еще используя для этого Apache Maven .

01
02
03
04
05
06
07
08
09
10
11
12
13
<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 .

01
02
03
04
05
06
07
08
09
10
11
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 — убедиться, что в пути к классам нет повторяющихся классов. Нередко по пути вы можете использовать другие превосходные библиотеки тестирования, но если ваши тестовые примеры Elasticsearch внезапно начинают не проходить фазу инициализации, это очень вероятно из-за обнаруженных проблем с адской койкой и некоторых исключений.

И еще одна вещь, весьма вероятно, что вам может потребоваться отключить менеджер безопасности во время тестовых прогонов, установив tests.security.manager свойства tests.security.manager значение false . Это можно сделать либо путем передачи аргумента -Dtests.security.manager=false непосредственно в JVM, либо с помощью конфигурации плагина Apache Maven .

1
2
3
4
5
6
7
8
<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 , очень полезны для тестирования ваших собственных анализаторов , токенизаторов , фильтров токенов и символов . Мы мало что сделали в этом отношении, но интеграционные тесты — это совсем другая история. Давайте посмотрим, что нужно для раскрутки кластера Elasticsearch с 3 узлами.

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

И … буквально, это так. Конечно, хотя кластер работает, он не имеет никаких предварительно настроенных индексов или чего-либо еще. Давайте добавим тестовый фон для создания индекса catalog и его типов отображения, используя тот же файл catalog-index.json .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
@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");
    }
}

Если вы уже знаете этот код, то это потому, что мы используем тот же транспортный клиент, о котором мы уже знали! Elasticsearch test scaffolding предоставляет его вам за методы client() или admin() вместе с getRestClient() на случай, если вам нужен экземпляр клиента Java REST . Было бы хорошо очищать кластер после каждого запуска теста, к счастью, мы можем использовать метод cluster() чтобы получить доступ к паре очень полезных операций, например:

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

В целом, тестовое использование Elasticsearch направлено на две цели: упростить наиболее распространенные задачи (мы уже видели client() , admin() , cluster() в действии) и легко выполнять проверку, утверждения или ожидания (например, ensureGreen(...) , assertAcked(...) ). Официальная документация имеет специальные разделы, в которых рассматриваются вспомогательные методы и утверждения, поэтому, пожалуйста, посмотрите.

Начнем с того, что в пустом индексе не должно быть документов, поэтому в нашем первом тестовом примере этот факт будет явно подтвержден.

01
02
03
04
05
06
07
08
09
10
11
12
@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 книг и выдачи запросов к ней.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@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 , ценность таких тестов бесценна, поэтому, пожалуйста, используйте доступные опции, чтобы гарантировать, что код, развернутый в рабочей среде , является надежным и устойчивым к сбоям. В качестве быстрого примера, мы могли бы закрыть узел случайных данных при выполнении поисковых запросов и утверждать, что они все еще обрабатываются.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
@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. Выводы

В этой части руководства мы узнали о двух типах клиентских API Java, которые Elasticsearch предлагает из коробки: транспортный клиент и REST-клиент . Вам может быть трудно сделать выбор того, какой API-интерфейс Java-клиента предпочтительнее использовать, но в целом это сильно зависит от приложения. В большинстве случаев транспортный клиент является наилучшим вариантом, однако, если в вашем проекте используется всего пара API-интерфейсов Elasticsearch (или очень ограниченное подмножество его функций), лучшим вариантом может быть клиент REST . Кроме того, мы не должны забывать, что клиент Java REST довольно нов и будет улучшаться в будущих выпусках, так что следите за этим.

Пока мы анализировали транспортный клиент , было высказано мнение о его полностью асинхронной природе. Хотя это хорошая вещь во всех отношениях, мы видели, что она основана на обратных вызовах (точнее, слушателях), которые могут быстро привести к проблеме, известной как ад обратного вызова . Настоятельно рекомендуется решить эту проблему на раннем этапе (к счастью, существует довольно много библиотек и альтернатив, таких как RxJava 2 и Project Reactor , с Java 9 тоже наверстывает упущенное).

И последнее, но не менее важное: мы просмотрели тестовый набор Elasticsearch и получили возможность оценить отличную помощь, которую он предоставляет разработчикам Java / JVM.

6. Что дальше

В следующей части , последней части урока, мы поговорим об экосистеме потрясающих проектов, сосредоточенных вокруг Elasticsearch . Надеемся, вы еще раз будете поражены возможностями и возможностями Elasticsearch , открывающими для себя новые горизонты его применимости.

Полный исходный код для всех проектов доступен для скачивания: эластичный поиск-клиент-отдых , эластичный поиск-тестирование , эластичный поиск-клиент-java