乐趣区

关于elasticsearch:来聊一聊-ElasticSearch-最新版的-Java-客户端

可能不少小伙伴都留神到了,从 ElasticSearch7.17 这个版本开始,原先的 Java 高级客户端
Java High Level REST Client 废除了,不反对了。诚实说,ElasticSearch 算是我用过的所有 Java 工具中,更新最为激进的一个了,在 Es7 中废除了 TransportClient,7.17 又废除了 TransportClient,那么当初用啥呢?当初的客户端叫做 Elasticsearch Java API Client。

始终偷懒抉择忽视 Elasticsearch Java API Client,不过最近工作中用到了,所以还是整篇文章和小伙伴们简略梳理一下 Elasticsearch Java API Client 的玩法。

上面的介绍我次要从索引操作和文档操作两个方面来给大家介绍。

不过须要跟大家强调的是,ElasticSearch 的 Java 客户端想要用的 6,必须要相熟 ElasticSearch 的查问脚本,大家平时在工作中遇到 Es 相干的问题,我也都是倡议先在 Kibana 中把操作脚本写好,而后再翻译成 Java 代码,或者间接拷贝到 Java 代码中, 十分不倡议上来就整 Java 代码 ,那样很容易出错。

如果你对 Es 的操作不相熟,松哥录了收费的视频教程,大家能够参考:

  • https://www.bilibili.com/video/BV1ft4y1e7tq/

不想看视频,也能够在微信公众号后盾回复 es,有文档教程。

1. Elasticsearch Java API Client

Elasticsearch Java API Client 是 Elasticsearch 的官网 Java API,这个客户端为所有 Elasticsearch APIs 提供强类型的申请和响应。

这里跟大家解释下什么是强类型的申请和响应:因为所有的 Elasticsearch APIs 实质上都是一个 RESTful 格调的 HTTP 申请,所以当咱们调用这些 Elasticsearch APIs 的时候,能够就当成一般的 HTTP 接口来看待,例如应用 HttpUrlConnection 或者 RestTemplate 等工具来间接调用,如果应用这些工具间接调用,就须要咱们本人组装 JSON 参数,而后本人解析服务端返回的 JSON。而强类型的申请和响应则是零碎把申请参数封装成一个对象了,咱们调用对象中的办法去设置就能够了,不须要本人手动拼接 JSON 参数了,申请的后果零碎也会封装成一个对象,不须要本人手动去解析 JSON 参数了。

小伙伴们看一下上面这个例子,我想查问 books 索引中,书名中蕴含 Java 关键字的图书:

public class EsDemo02 {public static void main(String[] args) throws IOException {URL url = new URL("http://localhost:9200/books/_search?pretty");
        HttpURLConnection con = (HttpURLConnection) url.openConnection();
        con.setRequestMethod("GET");
        con.setRequestProperty("content-type","application/json;charset=utf-8");
        // 容许输入流 / 容许参数
        con.setDoOutput(true);
        // 获取输入流
        OutputStream out = con.getOutputStream();
        String params = "{\n" +
                "\"query\": {\n" +
                "\"term\": {\n" +
                "\"name\": {\n" +
                "\"value\": \"java\"\n" +
                "}\n" +
                "}\n" +
                "}\n" +
                "}";
        out.write(params.getBytes());
        if (con.getResponseCode() == 200) {BufferedReader br = new BufferedReader(new InputStreamReader(con.getInputStream()));
            String str = null;
            while ((str = br.readLine()) != null) {System.out.println(str);
            }
            br.close();}
    }
}

小伙伴们看到,这就是一个一般的 HTTP 申请,申请参数就是查问的条件,这个条件是一个 JSON 字符串,须要咱们本人组装,申请的返回值也是一个 JSON 字符串,这个 JSON 字符串也须要咱们本人手动去解析,这种能够算是弱类型的申请和响应。

Elasticsearch Java API Client 具备如下个性:

  • 为所有 Elasticsearch APIs 提供强类型的申请和响应。
  • 所有 API 都有阻塞和异步版本。
  • 应用构建器模式,在创立简单的嵌套构造时,能够编写简洁而可读的代码。
  • 通过应用对象映射器(如 Jackson 或任何实现了 JSON-B 的解析器),实现应用程序类的无缝集成。
  • 将协定解决委托给一个 http 客户端,如 Java Low Level REST Client,它负责所有传输级的问题。HTTP 连接池、重试、节点发现等等由它去实现。

对于第三点,松哥吐槽一句,的确简洁,然而可读性一般般吧。

另外还有两点须要留神:

  • Elasticsearch Java 客户端是向前兼容的,即该客户端反对与 Elasticsearch 的更大或相等的主要版本进行通信。
  • Elasticsearch Java 客户端只向后兼容默认的发行版本,并且没有做出保障。

好了,那就不废话了,开整吧。

2. 引入 Elasticsearch Java API Client

首先须要咱们加依赖,对 JDK 的版本要求是 1.8,咱们须要增加如下两个依赖:

<dependency>
  <groupId>co.elastic.clients</groupId>
  <artifactId>elasticsearch-java</artifactId>
  <version>8.5.1</version>
</dependency>

<dependency>
  <groupId>com.fasterxml.jackson.core</groupId>
  <artifactId>jackson-databind</artifactId>
  <version>2.12.3</version>
</dependency>

如果是 Spring Boot 我的项目,就不必增加第二个依赖了,因为 Spring Boot 的 Web 中默认曾经加了这个依赖了,然而 Spring Boot 个别须要额定增加上面这个依赖,呈现这个起因是因为从 JavaEE 过渡到 JakartaEE 时衍生进去的一些问题,这里我就不啰嗦了,咱们间接加依赖即可:

<dependency>
  <groupId>jakarta.json</groupId>
  <artifactId>jakarta.json-api</artifactId>
  <version>2.0.1</version>
</dependency>

3. 建设连贯

接下来咱们须要用咱们的 Java 客户端和 ElasticSearch 之间建设连贯,建设连贯的形式如下:

RestClient restClient = RestClient.builder(new HttpHost("localhost", 9200)).build();
ElasticsearchTransport transport = new RestClientTransport(restClient, new JacksonJsonpMapper());
ElasticsearchClient client = new ElasticsearchClient(transport);

小伙伴们看到,这里一共有三个步骤:

  1. 首先创立一个低级客户端,这个其实松哥之前的视频中和大家讲过低级客户端的用法,这里就不再赘述。
  2. 接下来创立一个通信 Transport,并利用 JacksonJsonpMapper 做数据的解析。
  3. 最初创立一个阻塞的 Java 客户端。

下面这个是创立了一个阻塞的 Java 客户端,当然咱们也能够创立非阻塞的 Java 客户端,如下:

RestClient restClient = RestClient.builder(new HttpHost("localhost", 9200)).build();
ElasticsearchTransport transport = new RestClientTransport(restClient, new JacksonJsonpMapper());
ElasticsearchAsyncClient client = new ElasticsearchAsyncClient(transport);

只有第三步和后面的不一样,其余都一样。

利用阻塞的 Java 客户端操作 Es 的时候会产生阻塞,也就是必须等到 Es 给出响应之后,代码才会继续执行;非阻塞的 Java 客户端则不会阻塞前面的代码执行,非阻塞的 Java 客户端个别通过回调函数解决申请的响应值。

有时候,咱们可能还须要和 Es 之间建设 HTTPS 连贯,那么须要在后面代码的根底之上,再套上一层 SSL,如下:

String fingerprint = "<certificate fingerprint>";
SSLContext sslContext = TransportUtils
    .sslContextFromCaFingerprint(fingerprint); 
BasicCredentialsProvider credsProv = new BasicCredentialsProvider(); 
credsProv.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(login, password)
);
RestClient restClient = RestClient
    .builder(new HttpHost(host, port, "https")) 
    .setHttpClientConfigCallback(hc -> hc
        .setSSLContext(sslContext) 
        .setDefaultCredentialsProvider(credsProv)
    )
    .build();
ElasticsearchTransport transport = new RestClientTransport(restClient, new JacksonJsonpMapper());
ElasticsearchClient client = new ElasticsearchClient(transport);

好了,对于建设连贯,差不多就这些点。

4. 索引操作

Elasticsearch Java API Client 中最大的特色就是建造者模式 +Lambda 表达式。例如,我想创立一个索引,形式如下:

@Test
public void test99() throws IOException {
    RestClient restClient = RestClient.builder(new HttpHost("localhost", 9200)).build();
    ElasticsearchTransport transport = new RestClientTransport(restClient, new JacksonJsonpMapper());
    ElasticsearchClient client = new ElasticsearchClient(transport);
    CreateIndexResponse createIndexResponse = client.indices().create(
            c ->
                    c.index("javaboy_books")
                            .settings(s ->
                                    s.numberOfShards("3")
                                            .numberOfReplicas("1"))
                            .mappings(m ->
                                    m.properties("name", p -> p.text(f -> f.analyzer("ik_max_word")))
                                            .properties("birthday", p -> p.date(d -> d.format("yyyy-MM-dd"))))
                            .aliases("books_alias", f -> f.isWriteIndex(true)));
    System.out.println("createResponse.acknowledged() =" + createIndexResponse.acknowledged());
    System.out.println("createResponse.index() =" + createIndexResponse.index());
    System.out.println("createResponse.shardsAcknowledged() =" + createIndexResponse.shardsAcknowledged());
}

小伙伴们看到,这里都是建造者模式和 Lambda 表达式,办法名称其实都很好了解(前提是你得相熟 ElasticSearch 操作脚本),例如:

  • index 办法示意设置索引名称
  • settings 办法示意配置 setting 中的参数
  • numberOfShards 示意索引的分片数
  • numberOfReplicas 示意配置索引的正本数
  • mapping 示意配置索引中的映射规定
  • properties 示意配置索引中的具体字段
  • text 办法示意字段是 text 类型的
  • analyzer 示意配置字段的分词器
  • aliases 示意配置索引的别名

反正这里的办法都是见名知义的,下面这个就相似于上面这个申请:

PUT javaboy_books
{
  "settings": {
    "number_of_replicas": 1,
    "number_of_shards": 3
  },
  "mappings": {
    "properties": {
      "name":{
        "type": "text",
        "analyzer": "ik_max_word"
      },
      "birthday":{
        "type": "date",
        "format": "yyyy-MM-dd"
      }
    }
  },
  "aliases": {"xxxx":{}
  }
}

小伙伴们在写的时候,脑子里要先有上面这个脚本,而后 Java 办法能够棘手拈来了。

最终创立好的索引如下图:

有的小伙伴可能感觉调这一大堆办法太啰里啰唆了,来个简略的,间接上 JSON,那也不是不能够,如下:

@Test
public void test98() throws IOException {
    RestClient restClient = RestClient.builder(new HttpHost("localhost", 9200)).build();
    ElasticsearchTransport transport = new RestClientTransport(restClient, new JacksonJsonpMapper());
    ElasticsearchClient client = new ElasticsearchClient(transport);
    StringReader json = new StringReader("{\n" +
            "\"settings\": {\n" +
            "\"number_of_replicas\": 1,\n" +
            "\"number_of_shards\": 3\n" +
            "},\n" +
            "\"mappings\": {\n" +
            "\"properties\": {\n" +
            "\"name\":{\n" +
            "\"type\": \"text\",\n" +
            "\"analyzer\": \"ik_max_word\"\n" +
            "},\n" +
            "\"birthday\":{\n" +
            "\"type\": \"date\",\n" +
            "\"format\": \"yyyy-MM-dd\"\n" +
            "}\n" +
            "}\n" +
            "},\n" +
            "\"aliases\": {\n" +
            "\"xxxx\":{\n" +
            "\n" +
            "}\n" +
            "}\n" +
            "}");
    CreateIndexResponse createIndexResponse = client.indices().create(
            c ->
                    c.index("javaboy_books").withJson(json));
    System.out.println("createResponse.acknowledged() =" + createIndexResponse.acknowledged());
    System.out.println("createResponse.index() =" + createIndexResponse.index());
    System.out.println("createResponse.shardsAcknowledged() =" + createIndexResponse.shardsAcknowledged());
}

这是间接把 JSON 参数给拼接进去,就不须要一堆建造者 +Lambda 了。

如果你想删除索引呢?如下:

@Test
public void test06() throws IOException {
    RestClient restClient = RestClient.builder(new HttpHost("localhost", 9200)).build();
    ElasticsearchTransport transport = new RestClientTransport(restClient, new JacksonJsonpMapper());
    ElasticsearchClient client = new ElasticsearchClient(transport);
    // 删除一个索引
    DeleteIndexResponse delete = client.indices().delete(f ->
            f.index("my-index")
    );
    System.out.println("delete.acknowledged() =" + delete.acknowledged());
}

这个示意删除一个名为 my-index 的索引。

好了,对于索引的操作我就说这两点。

可能有的小伙伴会说,ElasticSearch 中创立索引能够配置很多参数你都没讲。在我看来,哪些很多参数其实跟这个 Java API 没有多大关系,只有你会写查问脚本,就天然懂得 Java API 中该调用哪个办法,退一万步讲,你会脚本,不懂 Java API 的办法,那么就像下面那样,间接把你的 JSON 拷贝过去,作为 Java API 的参数即可。

5. 文档操作

5.1 增加文档

先来看文档的增加操作。

如下示意我想给一个名为 books 的索引中增加一个 id 为 890 的书:

@Test
public void test07() throws IOException {
    RestClient restClient = RestClient.builder(new HttpHost("localhost", 9200)).build();
    ElasticsearchTransport transport = new RestClientTransport(restClient, new JacksonJsonpMapper());
    ElasticsearchClient client = new ElasticsearchClient(transport);
    Book book = new Book();
    book.setId(890);
    book.setName("深刻了解 Java 虚拟机");
    book.setAuthor("xxx");
    // 增加一个文档
    // 这是一个同步申请,申请会卡在这里
    IndexResponse response = client.index(i -> i.index("books").document(book).id("890"));
    System.out.println("response.result() =" + response.result());
    System.out.println("response.id() =" + response.id());
    System.out.println("response.seqNo() =" + response.seqNo());
    System.out.println("response.index() =" + response.index());
    System.out.println("response.shards() =" + response.shards());
}

增加胜利之后,返回的 IndexResponse 对象其实就是对上面这个 JSON 的封装:

当初咱们只须要调用相应的办法,就能够获取到 JSON 相干的属性了。

5.2 删除文档

如下示意删除 books 索引中 id 为 891 的文档:

@Test
public void test09() {
    RestClient restClient = RestClient.builder(new HttpHost("localhost", 9200)).build();
    ElasticsearchTransport transport = new RestClientTransport(restClient, new JacksonJsonpMapper());
    ElasticsearchAsyncClient client = new ElasticsearchAsyncClient(transport);
    client.delete(d -> d.index("books").id("891")).whenComplete((resp, e) -> {System.out.println("resp.result() =" + resp.result());
    });
}

删除这里我用了异步非阻塞的客户端来给小伙伴们演示的,异步非阻塞的话,就应用 whenComplete 办法解决回调就行了,里边有两个参数,一个是失常状况下返回的对象,另外一个则是出错时候的异样。

5.3 查问文档

最初,就是查问了。这应该是大家日常开发中应用较多的性能项了,不过我还是后面的态度,查问的要害不在 Java API,而在于你对 ElasticSearch 脚本的把握水平。

所以我这里举个简略的例子,小伙伴们大抵理解下 Java API 的办法即可:

@Test
public void test01() throws IOException {
    RestClient restClient = RestClient.builder(new HttpHost("localhost", 9200)).build();
    ElasticsearchTransport transport = new RestClientTransport(restClient, new JacksonJsonpMapper());
    ElasticsearchClient client = new ElasticsearchClient(transport);
    SearchRequest request = new SearchRequest.Builder()
            // 去哪个索引里搜寻
            .index("books")
            .query(QueryBuilders.term().field("name").value("java").build()._toQuery())
            .build();
    SearchResponse<Book> search = client.search(request, Book.class);
    System.out.println("search.toString() =" + search.toString());
    long took = search.took();
    System.out.println("took =" + took);
    boolean b = search.timedOut();
    System.out.println("b =" + b);
    ShardStatistics shards = search.shards();
    System.out.println("shards =" + shards);
    HitsMetadata<Book> hits = search.hits();
    TotalHits total = hits.total();
    System.out.println("total =" + total);
    Double maxScore = hits.maxScore();
    System.out.println("maxScore =" + maxScore);
    List<Hit<Book>> list = hits.hits();
    for (Hit<Book> bookHit : list) {System.out.println("bookHit.source() =" + bookHit.source());
        System.out.println("bookHit.score() =" + bookHit.score());
        System.out.println("bookHit.index() =" + bookHit.index());
    }
}

下面这个例子是一个 term 查问,查问 books 索引中书名 name 中蕴含 java 关键字的图书,等价于上面这个查问:

GET books/_search
{
  "query": {
    "term": {
      "name": {"value": "java"}
    }
  }
}

如果心愿可能对查问关键字分词之后查问,那么能够应用 match 查问,如下:

@Test
public void test03() throws IOException {
    RestClient restClient = RestClient.builder(new HttpHost("localhost", 9200)).build();
    ElasticsearchTransport transport = new RestClientTransport(restClient, new JacksonJsonpMapper());
    ElasticsearchClient client = new ElasticsearchClient(transport);
    SearchResponse<Book> search = client.search(s -> {s.index("books")
                .query(q -> {
                    q.match(m -> {m.field("name").query("美术计算机");
                        return m;
                    });
                    return q;
                });
        return s;
    }, Book.class);
    System.out.println("search.toString() =" + search.toString());
    long took = search.took();
    System.out.println("took =" + took);
    boolean b = search.timedOut();
    System.out.println("b =" + b);
    ShardStatistics shards = search.shards();
    System.out.println("shards =" + shards);
    HitsMetadata<Book> hits = search.hits();
    TotalHits total = hits.total();
    System.out.println("total =" + total);
    Double maxScore = hits.maxScore();
    System.out.println("maxScore =" + maxScore);
    List<Hit<Book>> list = hits.hits();
    for (Hit<Book> bookHit : list) {System.out.println("bookHit.source() =" + bookHit.source());
        System.out.println("bookHit.score() =" + bookHit.score());
        System.out.println("bookHit.index() =" + bookHit.index());
    }
}

为了让小伙伴们看到这个 Java 客户端的不同用法,下面两个查问的例子,我别离应用了结构查问申请和建造者 +Lambda 的形式。

match 查问就调用 match 办法就行了,设置查问关键字即可,这个查问等价于上面这个查问:

GET books/_search
{
  "query": {
    "match": {"name": "美术计算机"}
  }
}

如果你感觉这种调用各种办法拼接参数的形式不习惯,那么也能够间接上 JSON,如下:

@Test
public void test04() throws IOException {
    RestClient restClient = RestClient.builder(new HttpHost("localhost", 9200)).build();
    ElasticsearchTransport transport = new RestClientTransport(restClient, new JacksonJsonpMapper());
    ElasticsearchClient client = new ElasticsearchClient(transport);
    String key = "java";
    StringReader sr = new StringReader("{\n" +
            "\"query\": {\n" +
            "\"term\": {\n" +
            "\"name\": {\n" +
            "\"value\": \"" + key + "\"\n"+"      }\n"+"    }\n"+"  }\n"+"}");
    SearchRequest request = new SearchRequest.Builder()
            .withJson(sr)
            .build();
    SearchResponse<Book> search = client.search(request, Book.class);
    System.out.println("search.toString() =" + search.toString());
    long took = search.took();
    System.out.println("took =" + took);
    boolean b = search.timedOut();
    System.out.println("b =" + b);
    ShardStatistics shards = search.shards();
    System.out.println("shards =" + shards);
    HitsMetadata<Book> hits = search.hits();
    TotalHits total = hits.total();
    System.out.println("total =" + total);
    Double maxScore = hits.maxScore();
    System.out.println("maxScore =" + maxScore);
    List<Hit<Book>> list = hits.hits();
    for (Hit<Book> bookHit : list) {System.out.println("bookHit.source() =" + bookHit.source());
        System.out.println("bookHit.score() =" + bookHit.score());
        System.out.println("bookHit.index() =" + bookHit.index());
    }
}

能够看到,间接把查问的 JSON 参数传进来也是能够的。这样咱们就能够先在 Kibana 中写好脚本,而后间接将脚本拷贝到 Java 代码中来执行就行了。

好啦,对于 Es 中新的 Java 客户端,我就和大家说这么多,最初再强调一下,这其实不是重点,玩 Es 的重点是把 Es 的各种查问参数搞懂,那么 Java 代码其实就是棘手拈来的事了。

最初,如果大家对 Es 不相熟,能够看看松哥录的这个收费视频教程:

  • https://www.bilibili.com/video/BV1ft4y1e7tq/
退出移动版