少女祈祷中...

Elasticsearch

基于java编写的Lucence的一层封装

基本概念

Elasticsearch是面向文档的,关系行数据库

  • 索引(indices):对应mysql中的数据库
  • types: 对应表
  • documents: 对应mysql中行
  • fields: 对应mysql中的字段

物理设计
elasticsearch 在后台把每个索引划分成多个分片,每份分片可以在集群中的不同服务器间迁移

逻辑设计
一个索引类型中,包含多个文档,比如说文档1,文档2。 当我们索引一篇文档时,可以通过这样的一个顺序找到 它: 索引 ▷ 类型 ▷ 文档ID ,通过这个组合我们就能索引到某个具体的文档。 注意 : ID不必是整数,实际上它是个字符串。

文档

文档就是一条一条的数据
elasticsearch 是面向文档的,那么就意味着索引和搜索数据的最小单位是文档,elasticsearch 中,文档有几个 重要属性 :

  • 自我包含,一篇文档同时包含字段和对应的值,也就是同时包含 key:value!

  • 可以是层次型的,一个文档中包含自文档,复杂的逻辑实体就是这么来的!其实就是个JSON对象

  • 灵活的结构,文档不依赖预先定义的模式,我们知道关系型数据库中,要提前定义字段才能使用,在 elasticsearch 中,对于字段是非常灵活的,有时候,我们可以忽略该字段,或者动态的添加一个新的字段。

    尽管我们可以随意的新增或者忽略某个字段,但是,每个字段的类型非常重要,比如一个年龄字段类型,可以是字符串也可以是整形。因为 elasticsearch 会保存字段和类型之间的映射及其他的设置。这种映射具体到每个映射的每种类型,这也是为什么在 elasticsearch 中,类型有时候也称为映射类型。

类型

​ 类型是文档的逻辑容器,就像关系型数据库一样,表格是行的容器。 类型中对于字段的定义称为映射,比如 name 映 射为字符串类型。 我们说文档是无模式的,它们不需要拥有映射中所定义的所有字段,比如新增一个字段,那么 elasticsearch 是怎么做的呢?elasticsearch会自动的将新字段加入映射,但是这个字段的不确定它是什么类型,elasticsearch就开始猜,如果这个值是18,那么elasticsearch会认为它是整形。 但是elasticsearch也可能猜不对, 所以最安全的方式就是提前定义好所需要的映射,这点跟关系型数据库殊途同归了,先定义好字段,然后再使用。

总之类型就是数据字段对应的数据类型的映射

索引

​ 索引是映射类型的容器,elasticsearch 中的索引是一个非常大的文档集合。索引存储了映射类型的字段和其他设置。 然后它们被存储到了各个分片上了。 我们来研究下分片是如何工作的。

物理设计 :节点和分片如何工作

​ 一个集群至少有一个节点,而一个节点就是一个 elasricsearch 进程,节点可以有多个索引默认的,如果你创建索引,那么索引将会有个5个分片 ( primary shard ,又称主分片 ) 构成的,每一个主分片会有一个副本 ( replica shard ,又称复制分片 )

下图是一个有3个节点的集群,可以看到主分片和对应的复制分片都不会在同一个节点内,这样有利于某个节点挂掉 了,数据也不至于丢失。
实际上,一个分片是一个 Lucene 索引,一个包含倒排索引的文件目录,倒排索引的结构使得elasticsearch在不扫描全部文档的情况下,就能告诉你哪些文档包含特定的关键字

倒排索引

​ elasticsearch 使用的是一种称为倒排索引的结构,采用Lucene倒排索引作为底层。这种结构适用于快速的全文搜索, 一个索引由文档中所有不重复的列表构成,对于每一个词,都有一个包含它的文档列表。 例如,现在有两个文档, 每个文档包含如下内容:

Study every day, good good up to forever # 文档1包含的内容
To forever, study every day, good good up # 文档2包含的内容

​ 为了创建倒排索引,我们首先要将每个文档拆分成独立的词(或称为词条或者tokens),然后创建一个包含所有不重复的词条的排序列表,然后列出每个词条出现在哪个文档 :


现在,我们试图搜索 to forever,只需要查看包含每个词条的文档

两个文档都匹配,但是第一个文档比第二个匹配程度更高。如果没有别的条件,现在,这两个包含关键字的文档都将返回。

​ 再来看一个示例,比如我们通过博客标签来搜索博客文章。那么倒排索引列表就是这样的一个结构 :


如果要搜索含有 python 标签的文章,那相对于查找所有原始数据而言,查找倒排索引后的数据将会快的多。只需要 查看标签这一栏,然后获取相关的文章ID即可。完全过滤掉无关的所有数据,提高效率!

​ 在 elasticsearch 中, 索引这个词被频繁使用,这就是术语的使用。在 elasticsearch 中,索引被分为多个分片,每份分片是一个 Lucene 的索引。所以 一个 elasticsearch 索引是由多个Lucene索引组成的。

基本操作

IK分词器

分词:即把一段中文或者别的内容划分成一个个的关键字,我们在搜索时候会把自己的信息进行分词,是因为数据库中或者索引库中的数据也会进行分词,然后进行一个匹配操作,默认的中文分词是将每个字看成一个词,比如 “我爱大数据” 会被分为”我”,”爱”,”大”, “数”,”据”,这显然是不符合要求的,所以我们需要安装中文分词器 ik 来解决这个问题。

IK提供了两个分词算法:ik_smart 和 ik_max_word ,其中 ik_smart 为最少切分,ik_max_word 为 最细粒度划分

安装:https://github.com/medcl/elasticsearch-analysis-ik/
安装对应版本并放入plugin目录下

使用:

  • ik_smart模式:

    GET _analyze
    {
    "analyzer":"ik_smart",
    "text":"中国共产党"
    }

    效果:

  • ik_max_word模式

    GET _analyze
    {
    "analyzer":"ik_max_word",
    "text":"中国共产党"
    }

    效果:

Rest风格

        什么是 Rest 风格呢?

一种软件架构风格,而不是标准,只是提供了一组设计原则和约束条件。它主要用于客户端和服务器交互类的软件。基于这个风格设计的软件可以更简洁,更有层次,更易于实现缓存等机制

        其中 基本 的 Rest 命令说明:

method url地址 描述
PUT localhost:9200/索引名称/类型名称/文档id 创建文档(指定文档 id )
POST localhost:9200/索引名称/类型名称 创建文档(随机文档 id )
POST localhost:9200/索引名称/类型名称/文档id/_update 修改文档
DELETE localhost:9200/索引名称/类型名称/文档id 删除文档
GET localhost:9200/索引名称/类型名称/文档id 通过文档id查询文档
POST localhost:9200/索引名称/类型名称/_search 查询所有数据

关于索引的基本操作

基础测试

  1. 创建一个索引

PUT /索引名/类型名/文档id{请求id}

// 命令解释 
// PUT 创建命令 test1 索引 type1 类型 1 id
PUT /test1/type1/1
{
"name":"大数据梦想家",
"age":21
}

如下图所示:返回结果 (是以REST ful 风格返回的 ):
那么 name 这个字段用不用指定类型呢。毕竟我们关系型数据库 是需要指定类型的啊

字段类型

elasticsearch 常见的字段类型如下:

  • 字符串类型

textkeyword

  • 数值类型

long, integer, short, byte, double, float, half_float, scaled_float

  • 日期类型

date

  • 布尔值类型

boolean

  • 二进制类型

binary

  • 等等

PUT新增设置索引规则(设置字段与数据类型)

PUT /test2
{
"mappings": {
"properties": {
"name":{
"type":"text"
},
"age":{
"type":"long"
},
"birthday":{
"type":"date"
}
}
}
}

输出如下,说明创建成功了

{
"acknowledged" : true,
"shards_acknowledged" : true,
"index" : "test2"
}

GET 查看规则信息
现在我们来尝试使用一下 GET 命令,请求具体的信息!
可以发现通过GET请求,我们能够详细获取到该索引下具体的信息,其中包含字段类型。那上面示例中字段类型是我自己定义的,那么我们不定义类型会是什么情况呢?
我们首先发起一个PUT请求,创建一个新的索引 test3,并添加一条数据

PUT /test3/_doc/1
{
"name":"大数据梦想家",
"age":21,
"birthday":"2000-02-06"
}

然后通过GET请求,可以发现非常的智能。但是如果我们的文档字段类型没有指定,那么es就会给我们默认配置的字段类型!
UPDATE修改
那如果我们想要修改文档里的字段信息呢?我们可以选择 UPDATE 也可以 选择 PUT进行覆盖
例如我可以像下图中的例子,将之前test3索引中的1号文档中的 name 字段修改后,重复提交,发现更新成功,但是注意 version 版本号已经变成了2但是注意这种方法有弊端,如果我们在PUT的过程中,遗漏了字段,那么数据就会被新数据覆盖!所以,修改数据不建议使用PUT覆盖的方式!
我们使用 POST 命令,在 id 后面跟 _update ,要修改的内容放到 doc 文档(属性)中即可。可以发现此时更新之后的version变成了3。所以,一旦索引被创建了之后,所有的修改都可以通过版本号看到变化。
DELETE删除
结论: 通过 DELETE 命令实现删除,根据请求判断是删除索引还是删除文档记录!因此,使用 RESTFUL 风格是我们学习ES值得推荐使用的

其他命令

我们可以通过GET _cat/health来获取集群的一个健康状态除了看集群的健康信息,
通过命令GET _cat/indices?v,我们可以获取到当前索引的很多信息,返回值包括所有索引的状态健康情况,分片,数据储存大小等等

关于文档的基本操作

接下来我们学习关于文档的基本操作,首先先重新创建一个新的索引,并添加一些数据

PUT /alice/user/1
{
"name":"爱丽丝",
"age":21,
"desc":"在最美的年华,做最好的自己!",
"tags":["技术宅","温暖","思维活跃"]
}

PUT /alice/user/2
{
"name":"张三",
"age":23,
"desc":"法外狂徒",
"tags":["渣男","交友"]
}

PUT /alice/user/3
{
"name":"路人甲",
"age":24,
"desc":"不可描述",
"tags":["靓仔","网游"]
}

接下来就可以进行文档的基本操作了!

简单查询

通过 GET 命令,我们可以搜索到指定 id 的文档信息
GET alice/user/1
当然这是简单的搜索,下面我们来看一下 es 如何做条件查询
条件查询_search?q=
我们可以通过如下命令,来进行条件查询
GET alice/user/_search?q=name:张三

我们看一下结果 返回并不是 数据本身,是给我们了一个 hits ,还有 _score得分,就是根据算法算出和查询条件匹配度高的分就越高。
我们在以某度为例的搜索引擎上进行搜索也是一样的道理,权重越高网站的位置就越靠前!
但我们一般使用不会直接加条件去查询,更多的会用到下面要介绍到的复杂操作搜索
复杂操作搜索 select( 排序,分页,高亮,模糊查询,精准查询!)
为了方便测试,我又执行下面的命令,往Alice索引下添加了2个文档

PUT /alice/user/4
{
"name":"爱丽丝学Java",
"age":25,
"desc":"技术成就自我!",
"tags":["思维敏捷","喜欢学习"]
}

PUT /alice/user/5
{
"name":"爱丽丝学Python",
"age":26,
"desc":"人生苦短,我用Python!",
"tags":["好学","勤奋刻苦"]
}

现在我们来构建一个查询:

GET alice/user/_search
{
"query":{
"match": {
"name": "爱丽丝"
}
}
}

默认的话,es会查询出文档的所有字段,如果我们只想要部分的字段,就可以像下面所展示的demo进行查询:

GET alice/user/_search
{
"query":{
"match": {
"name": "爱丽丝"
}
},
"_source":["name","desc"]
}

如上例所示,在查询中,通过 _source 来控制仅返回 name 和 desc 属性。页面返回的查询结果如下:一般的,我们推荐使用构建查询,以后在与程序交互时的查询等也是使用构建查询方式处理查询条件,因为该方式可以构建更加复杂的查询条件,也更加一目了然。

排序查询

我们说到排序,有人就会想到:正序或倒序。那么我们先来根据age字段倒序查询:

GET alice/user/_search
{
"query":{
"match": {
"name": "爱丽丝"
}
},
"sort": [
{
"age":
{
"order": "desc"
}

}
]
}

查询返回的结果如下:同理,如果我们想要正序查询,只需要将desc换成了asc即可。

GET alice/user/_search
{
"query":{
"match": {
"name": "爱丽丝"
}
},
"sort": [
{
"age":
{
"order": "asc"
}

}
],
"from":0,
"size":1
}

查询结果如下:
注意:在排序的过程中,只能使用可排序的属性进行排序。那么可以排序的属性有哪些呢?

  • 数字
  • 日期
  • ID

其他都不行!

分页查询

学到这里,我们也可以看到,我们的查询条件越来越多,开始仅是简单查询,慢慢增加条件查询,增加排序,对返回结果进行限制。所以,我们可以说,对 于 elasticsearch 来说,所有的查询条件都是可插拔的。比如说,我们在查询中,仅对返回结果进行限制:

GET alice/user/_search
{
"query":
{"match_all": {}
},
"from":0, # 从第n条开始
"size":4 # 返回n条数据
}

分页查询类似于我们SQL中的 limit 语句。在 es 中我们想要实现这样的效果只需要用 from 指定 从第几条数据开始,size指定返回多少条数据即可。

布尔查询

must (and)
我们上面已经讲过了通过构建查询的方法去做模糊查询,那我们如果想多条件查询,例如查询name为alice,并且age是25岁,那该如何查询呢?
我们通过在 bool属性内使用 must 来作为查询条件!看结果,是不是 有点像and的感觉,里面的条件需要都满足!

GET alice/user/_search
{
"query":{
"bool": {
"must":[
{
"match":{
"name":"爱丽丝"
}
},
{
"match":{
"age":25
}
}
]
}
}
}

查询结果如下should (or)
那么我要查询name为爱丽丝或 age 为 25 的呢?
我们只需要将boolean属性内的must值换成should 即可,这就有点相当于 or 的感觉

GET alice/user/_search
{
"query":{
"bool": {
"should":[
{
"match":{
"name":"爱丽丝"
}
},
{
"match":{
"age":25
}
}
]
}
}
}

查询结果如下
must_not (not)
那现在我想要查询年龄不是 25 的 数据,只需要将boolean的属性值换成must_not即可

GET alice/user/_search
{
"query":{
"bool": {
"must_not":[
{
"match":{
"age":25
}
}
]
}
}
}

查询结果如下:Fitter
那如果查询 name 为爱丽丝,age 大于 24 的数据,需要使用到filter进行过滤。

GET alice/user/_search
{
"query":{
"bool":{
"must": [
{
"match": {
"name": "爱丽丝"
}
}
],
"filter": [
{
"range": {
"age": {
"gt": 24
}
}
}
]
}
}
}

查询结果如下,可以发现只有age为25 和 26的两条数据这里就用到了 filter 条件过滤查询,过滤条件的范围用 range 表示,其余操作如下 :

  • gt 表示大于
  • gte 表示大于等于
  • lt 表示小于
  • lte 表示小于等于

那现在要查询,例如 age 在24到26之间的数据该如何查询?

GET alice/user/_search
{
"query":{
"bool":{
"filter": [
{
"range": {
"age": {
"gte": 24,
"lte": 26
}
}
}
]
}
}
}

查询结果:

短语检索

为了方便测试,我们再加入几条文档数据:

PUT /alice/user/6
{
"name":"大数据老K",
"age":25,
"desc":"技术成就自我!",
"tags":["男","学习","技术"]
}

PUT /alice/user/7
{
"name":"Python女侠",
"age":26,
"desc":"人生苦短,我用Python!",
"tags":["靓女","勤奋学习","善于交际"]
}

例如现在需要查询tags中包含“男”的数据

GET alice/user/_search
{
"query":{
"match":{
"tags":"男"
}
}
}

查询结果如下:

匹配多个标签
既然按照标签检索,那么,能不能写多个标签呢?

GET alice/user/_search
{
"query":{
"match":{
"tags":"男 学习"
}
}
}

此时我们可以观察返回的结果,可以发现只要满足一个标签就能返回这个数据了

精确查询

term查询是直接通过倒排索引指定的词条进程精确查找的!
关于分词:

  • term ,不经过分词,直接查询精确的值
  • match,会使用分词器解析!(先分析文档,然后再通过分析的文档进行查询!)

说到分词器解析,就不得不提到两种数据类型:text和keyword。下面我们就来做个测试:

// 创建一个索引,并指定类型
PUT testdb
{
"mappings": {
"properties": {

"name":{
"type": "text"
},
"desc":{
"type":"keyword"
}
}
}
}

// 插入数据
PUT testdb/_doc/1
{
"name":"爱丽丝学大数据name",
"desc":"爱丽丝学大数据desc"
}

PUT testdb/_doc/2
{
"name":"爱丽丝学大数据name2",
"desc":"爱丽丝学大数据desc2"
}

上述中testdb索引中,字段name在被查询时会被分析器进行分析后匹配查询。而属于keyword类型不会被分析器处理。
我们来验证一下:

GET _analyze
{
"analyzer": "keyword",
"text": "爱丽丝学大数据 name"
}

查询结果:是不是没有被分析~就是简单的一个字符串啊。再测试一下:

GET _analyze
{
"analyzer": "standard",
"text": "爱丽丝学大数据 name"
}

查询结果:然后我们可以得出结论:keyword 字段类型不会被分析器分析
下面我们用前面添加的2条数据做过测试:
先精准查询text类型的字段

GET testdb/_search         // text 会被分析器分析 查询
{
"query": {
"term": {
"name": "爱"
}
}
}

查询结果,2条数据都能匹配到然后用standard类型做精准测试

GET testdb/_search          // keyword 不会被分析所以直接查询 
{
"query": {
"term": {
"desc": "爱丽丝学大数据desc"
}
}
}

查询结果,只有1条数据能匹配到

查找多个精确值

为了方便测试,我们再添加如下数据:

PUT testdb/_doc/3
{
"t1":"22",
"t2":"2021-03-01"
}

PUT testdb/_doc/4
{
"t1":"33",
"t2":"2021-03-01"
}

然后进行查询

GET testdb/_search
{
"query": {
"bool":{
"should": [
{
"term": {
"t1":"22"
}
},
{
"term": {
"t1":"33"
}
}
]
}
}
}

查询结果:可以发现2条数据也都能查到,证明就算是term精确查询,也能够查询多个值。
当然,除了 bool 查询之外,下面这种方式也同样是可以的。

GET testdb/_doc/_search
{
"query":{
"terms":{
"t1":["22","33"]
}
}
}

下面要介绍的功能,就是经常被搜索引擎用到的“高亮显示”!

高亮显示

我们可以通过highlight属性,来对我们查询的结果的指定字段做高亮显示!

GET alice/user/_search
{
"query":{
"match": {
"name": "爱丽丝"
}
},
"highlight":{
"fields": {
"name": {}
}
}
}

观察返回的结果,我们可以发现搜索相关的结果,被加上了高亮标签现在效果看到了,那我们有没有办法自定义样式呢?
答案当然是可以的,我们需要在pre_tags中定义标签的前缀,post_tags中定义后缀!

GET alice/user/_search
{
"query":{
"match": {
"name": "爱丽丝"
}
},
"highlight":{
"pre_tags": "<b class='key' style='color:red'>",
"post_tags": "</b>",
"fields": {
"name": {}
}
}
}

查询结果:

SpringBoot集成ES

官方文档:https://www.elastic.co/guide/en/elasticsearch/client/java-api-client/current/getting-started-java.html

依赖

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

使用springboot

\<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>

注意版本要对应

这里我使用最新的用法
配置

package com.yyjccc.eslearn.config;

import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.json.jackson.JacksonJsonpMapper;
import co.elastic.clients.transport.ElasticsearchTransport;
import co.elastic.clients.transport.rest_client.RestClientTransport;
import lombok.Data;
import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@Data
public class ElasticsearchConfig {

@Value("${spring.data.es.url}")
private String serverUrl;

@Bean
public ElasticsearchClient restClient(){
RestClient restClient = RestClient
.builder(HttpHost.create(serverUrl))
.build();

ElasticsearchTransport transport = new RestClientTransport(
restClient, new JacksonJsonpMapper());

ElasticsearchClient esClient = new ElasticsearchClient(transport);
return esClient;
}

}

索引

创建索引

@Autowired
private ElasticsearchClient esClient;
@Test
void contextLoads() throws IOException {
CreateIndexRequest createIndexRequest=new CreateIndexRequest.Builder().index("test").build();
CreateIndexResponse createIndexResponse = esClient.indices().create(createIndexRequest);
System.out.println(createIndexResponse);
}

判断索引是否存在

void hasIndex() throws IOException {
ExistsRequest indexRequest= new ExistsRequest.Builder().index("test").build();
BooleanResponse exists = esClient.indices().exists(indexRequest);
System.out.println(exists);
}

删除索引

void DeleteIndex() throws IOException {
DeleteIndexRequest indexRequest=new DeleteIndexRequest.Builder().index("test").build();
DeleteIndexResponse delete = esClient.indices().delete(indexRequest);
System.out.println(delete);
}

文档

实体类

package com.yyjccc.eslearn.entity;

import lombok.AllArgsConstructor;
import lombok.Data;


@Data
@AllArgsConstructor
public class User {
private String name;
private int age;
}

创建文档数据

void addDoc() throws IOException {
User yyjccc = new User("yyjccc", 18);
IndexRequest indexRequest= new IndexRequest.Builder<>().index("test")
.id("1")
.timeout(new Time.Builder().time("1s").build())
.document(yyjccc)
.build();
//document()设置内容
//index()设置索引名称
//id ()设置插入数据的id
//timeout()设置超时时间
IndexResponse index = esClient.index(indexRequest);
//result 操作结果
System.out.println(index.toString());
System.out.println(index.result());


}

判断是否存在文档

void hasDoc() throws IOException {
ExistsSourceRequest sourceRequest=new ExistsSourceRequest.Builder()
.index("test")
.id("2")
.build();

BooleanResponse booleanResponse = esClient.existsSource(sourceRequest);
System.out.println(booleanResponse);
}

获取文档内容

void getDoc() throws IOException {
GetRequest getRequest=new GetRequest.Builder()
.index("test")
.id("1")
.build();


GetResponse<User> userGetResponse = esClient.get(getRequest, User.class);
System.out.println(userGetResponse.source());
}

注意实体类User必须是标准的JavaBean
跟新文档数据

void updateDoc() throws IOException {
User user = new User("yyj",18);
UpdateRequest updateRequest=new UpdateRequest.Builder()
.id("1")
.index("test")
.doc(user)

.build();
UpdateResponse update = esClient.update(updateRequest, User.class);
System.out.println(update);
}

删除文档

void deleteDoc() throws IOException {
DeleteRequest build = new DeleteRequest.Builder()
.index("test")
.id("2")
.build();
DeleteResponse delete = esClient.delete(build);
System.out.println(delete);
}

批量操作

void bulkDoc() throws IOException {
List<BulkOperation> operations=new ArrayList<>();
List<User> userList=new ArrayList<>();
userList.add(new User("uuu",18));
userList.add(new User("yanyongju",18));
userList.add(new User("youyou",18));
for (int i = 0; i <userList.size(); i++) {
CreateOperation createOperation =((CreateOperation.Builder)new CreateOperation.Builder()
.id(""+(i+2)))
.document(userList.get(i))
.build();
BulkOperation build = (BulkOperation) new BulkOperation.Builder()
.create(createOperation)
.build();
operations.add(build);
}


BulkRequest bulkRequest = new BulkRequest.Builder()
.index("test")
.operations(operations)
.build();
BulkResponse bulk = esClient.bulk(bulkRequest);
System.out.println(bulk.errors());
}

查询

void searchDoc() throws IOException {
MatchQuery matchQuery=new MatchQuery.Builder()
.query("y")
.field("name")
.build();

MatchAllQuery matchAllQuery=new MatchAllQuery.Builder()
.queryName("name")
.build();

TermQuery termQuery=new TermQuery.Builder()
.field("age")
.value(18)
.build();

Query query=new Query.Builder()
.matchAll(matchAllQuery)
//.term(termQuery)
.build();

SearchRequest searchRequest=new SearchRequest.Builder()
.index("test")
.query(query)
.from(0)
.size(10)
.build();
//分页精准匹配
SearchResponse<User> search = esClient.search(searchRequest, User.class);

System.out.println(search);
}