Elasticsearch是一个实时的分布式搜索分析引擎,底层基于Lucene实现。它提供了一个分布式多用户能力的全文搜索引擎,并且客户端可以使用标准的RESTful进行访问。Elasticsearch是用Java开发的,并作为Apache许可条款下的开放源码发布,是当前流行的企业级搜索引擎。设计用于云计算中,能够达到实时搜索,稳定,可靠,快速,安装使用方便。
传统的搜索做法主要以是基于系统或应用的查询功能进行查找,或者使用数据库的模糊查询等机制来完成。这些方式相对简单,对于少量数据来说操作性比较方便。但对于海量数据,性能就会急剧下降导致不能及时响应,且不易于扩展。而使用搜索引擎的好处在于,它可以存储非结构化的数据,进行相关的排序、过滤以及分词等功能,快速检索我们需要的数据信息。
可以从官网下载最新版本: https://www.elastic.co/cn/products/
根据不同的系统平台下载相应的压缩包。
下载完成后解压到任意目录即可,下面对elasticsearch目录进行简要说明。
目录 | 说明 |
---|---|
bin | 运行目录,包含elasticsearch启动程序等 |
config | 配置目录,存放elsticsearch的相关配置文件 |
data | 存放ES节点数据的目录 |
jdk | 从7.0开始自带JDK运行环境 |
lib | 运行时所需的类库 |
logs | 存放elasticsearch运行时产生的日志 |
modules | 模块目录 |
plugins | 插件目录 |
进入bin录下,运行elasticsearch程序即可。
启动完成后可以在浏览器输入http://localhost:9200访问,elasticsearch默认端口为9200。
如果能访问到以上的JSON信息表示elasticsearch正常运行。
在进行内容检索时,通常会用到中文检索,那么可以额外为es添加中文分词器,比较常见的中文分词插件如:analysis-ik、庖丁解牛等。(注意:分词器要对应ES的版本)
安装analysis-ik,打开终端并进入ES的目录,执行以下命令:
./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.0.0/elasticsearch-analysis-ik-7.0.0.zip
ik支持两种分词模式:
在config目下有一个核心配置文件elasticsearch.yml,这个文件主要用于elasticsearch的集群配置、节点配置、IP地址以及端口等信息的设置。
下面主要讲解一些常用的配置:
属性 | 说明 |
---|---|
cluster.name | 集群名称,在一个网段中的所有节点都设置为同一个集群名称时,会自动加入集群中 |
node.name | 当前节点名称 |
node.master | 是否参与master选举 |
node.data | 是否为数据节点 |
path.data | 存储数据的路径,默认开启 |
path.logs | 存储日志的路径 |
network.host | 当前节点对外暴露的IP地址 |
http.port | 对外服务的http端口,默认9200 |
http.cors.enabled | 启用跨域支持 |
http.cors.allow-origin | 允许访问的域名 |
discovery.seed_hosts | 用于设置集群发现 |
cluster.initial_master_nodes | 指定需要成为mater的所有节点的name或者ip |
如果ES需要开放外网访问,需要在elasticsearch.yml中添加network.host和discovery.seed_hosts配置。
ES原生支持http协议并提供了丰富的Restful API。可以使用各种基于http协议的客户端软件来访问,如:curl、postman等工具,也可以使用官方提供的Kibana来访问ES,这里以postman为例。
ElasticSearch中的一个Index(索引)类似于关系型数据库的一个库,它是最顶级的元素,在ES中可以创建多个Index,类似于创建了多个数据库。内部结构使用一个"_index"字段来标识索引的名称。
{
"_index" : "users",
...
}
PUT /index
字段 | 说明 |
---|---|
acknowledged | acknowledged值为true表示创建成功 |
shards_acknowledged | true表示已启用数据分片 |
index | 创建的索引名称 |
GET /index
字段 | 说明 |
---|---|
aliases | 索引别名 |
mappings | 文档字段映射(这里暂时没有创建) |
settings | 索引的一些设置,包括创建时间,主节点分数量和副本数量,唯一标识符,版本号等设置 |
POST /index/_close
POST /index/_open
DELETE /index
GET /_cat/inclus
响应内容包含了es中所有的index的信息。我们看下users这个索引,其中yellow表示当前索引主分片可用,但副本不一定都可用。green表示当前索引在集群中健康良好。open表示当前索引是打开的。当然还包括uuid和索引容量等等其他信息。
Type表示文档映射类型。不太确切地说,Type有点类似于数据库中的表。在早期的版本中,一个index下面可以创建多个type,但是从6.0开始,ES将type进行弱化,一个index中只能有一个type。在创建index时会包含一个个“_doc”这个默认的type。
{
"_index": "usersinfo",
"_type": "_doc"
...
}
在ES中,Mapping用于映射文档的结构,例如文档中的每一个字段名以及数据类型都在Mapping中进行描述。在mapping里面有一个"properties"属性,文档的所有字段映射都放在properties中,例如下面的文档映射中包含age、birthplace、name三个字段。
{
"users": {
"mappings": {
"properties": {
"age": {
"type": "integer",
"index": false
},
"birthplace": {
"type": "text",
"analyzer": "ik_max_word"
},
"name": {
"type": "text",
"analyzer": "ik_max_word"
}
}
}
}
}
PUT /index/_mapping
{
"properties": {
"name": {
"type": "text",
"analyzer": "ik_max_word"
},
"age": {
"type": "integer",
"index": false
},
"birthplace": {
"type": "text",
"analyzer": "ik_max_word"
}
}
}
示例:
说明:
type:用于指定字段的类型,ES也提供了众多的数据类型共我们选择,下面列举一些常用的数据类型。
类型 | 说明 |
---|---|
text | 字符串,支持分词、全文检索、模糊、精确查询。不支持聚合、排序操作。长度无限制,适合大字段存储。 |
keyword | 字符串,不分词,不支持全文搜索,所以只能是使用精确匹配进行查询。但直接索引、模糊、精确匹配、聚合、排序操作。长度为32766个UTF-8类型的字符。 |
integer、long、short、byte | 整数类型 |
double、float | 浮点类型 |
boolean | 逻辑类型 |
date | 日期类型,指定日期是还可以指定日期的格式化,如:{“type”:“date”, “format”:“yyyy-MM-dd”} |
binary | 二进制类型 |
array | 数组类型 |
object | 对象类型 |
nested | 嵌套类型 |
range | 范围类型 |
… | … |
index:表示是否给当前字段设置分词,true为设置,false为不设置。
analyzer:用于设置分词器。ES内置很多的分词器,例如standard 分词器、simple 分词器、Whitespace 分词器等等,如果不指定分词器,默认则使用standard 分词器。而例子中使用的是自行安装的ik中文分词器。
GET /index/_mapping
示例:
注意:查看mapping时时使用GET请求。
PUT /index
{
"mappings": {
"properties": {
"name": {
"type": "text",
"analyzer": "ik_max_word"
},
"age": {
"type": "integer"
},
"birthplace": {
"type": "text",
"analyzer": "ik_max_word"
}
}
}
}
示例:
Elasticsearch是面向文档的,因此文档在ES中是搜索的最小单位,而文档通常都会序列化成JSON的格式保存在ES中。一个文档由多个字段组成,每个字段拥有自己的类型,并且还有一个唯一的ID。在ES中有一个"_source"字段,这个字段就是用于存储文档的原始json数据。
{
"_index": "users",
...
"_source": {
"name": "张三",
"age": 22,
"birthplace": "珠海"
}
}
在设置好mapping之后,我们就可以在index中添加Document,类似于在数据库中插入一条记录。
PUT /index/_doc/id
{
"name": "小明",
"age": 21,
"birthplace": "广州"
}
示例:
说明:_doc是默认也是每个index唯一的type,添加记录时需要加上。“1”表示给这条document记录自定义一个id(相当于主键),成功添加后将响应如下结果:
字段 | 说明 |
---|---|
_index | 前操作的索引 |
_type | 默认的文档类型,也就是_doc |
_id | 自定义的文档唯一标识 |
_version | 表示当前记录的版本号 |
_result | 执行的结果,created表示创建了一条记录 |
_shard | 分片信息 |
_seq_no | 每个文档对应一个递增的序列号,当执行任何写操作,如create、update、delete等操作时,都会生成一个_seq_no |
_primary_term | 主要用来恢复数据时处理当多个文档的_seq_no一样时的冲突。每当重新分片或集群中的主节点选举时,_primary_term都会递增 |
当然,创建文档时也可以不指定id,此时ES将使用uuid作为唯一标识。需要注意的是,如果不指定id,必须使用POST请求。
POST /index/_doc
{
"name": "小明",
"age": 21,
"birthplace": "广州"
}
在响应结果中可以看到"_id"字段使用了uuid串作为唯一标识。
GET /index/_doc/id
示例:
说明:"_source"字段的内容就是具体的文档信息
PUT /index/_doc/id
{
"name": "王小明",
"age": 21,
"birthplace": "深圳"
}
示例:
响应结果:
注意响应体中的“result”字段,此时为updated,表示更新了一条记录。
DELETE /index/_doc/id
示例:
响应结果:
此时在响应体中的“result”字段的内容为deleted,表示删除了一条记录。
文档检索可以说是ES中最常用和最重要的功能了。ES中对文档搜索的方式有两种,一种是使用参数查询,查询参数可以使用简单的查询字符串作为参数并将其放在请求的url后面通过”?“号进行提交。另一种是将查询参数放入请求体中,而在请求体中通常使用ES提供的DSL查询语句进行检索,因此也成为DSL查询。在使用搜索时,会在请求的url中都会使用"_search"关键字进行检索。
GET /index/_doc/_search
示例:
返回的结果中hits表示查询的命中率,如下:
URI查询是在请求的URL中使用“q”作为请求参数,将需要检索的字段名和对应的值提交给ES。
GET /index/_search?q=field:value
示例:
响应结果:
Query DSL又叫查询表达式,是一种非常灵活又富有表现力的查询语言,采用JSON接口的方式实现的查询。
term表示精确匹配,搜索前不会再对搜索的内容进行分词,因此我们搜索的词必须是文档分词集合中的一个。
GET /index/_search
{
"query" : {
"term" : {
"birthplace" : "广州"
}
}
}
示例:
响应结果:
说明:首先创建文档信息时,ik中文分词器将birthplace字段的内容进行了分词。例如“广东省广州市”会分成“广东”、“广东省”、“广州”、“广州市”等等一系列的分词信息,因此在使用term进行检索的时候,只要输入的内容完全符合这些分词中的任意一个,都能成功匹配。但如果输入内容是“广东广州”,由于分词器并没有对齐进行这样的分词,因此是匹配不到结果的。
示例:
响应结果:
terms用于在一个字段中精确匹配多个搜索词,值是一个数组。
GET /index/_search
{
"query" : {
"terms" : {
"birthplace" : ["广州","深圳"]
}
}
}
示例:
响应结果:
match查询会先对搜索的内容进行分词,然后再对分词结果进行匹配。
GET /index/_search
{
"query" : {
"match" : {
"birthplace" : "广州"
}
}
}
示例:
响应结果:
说明:match会对输入的内容进行分词处理,这是和term最根本的区别。在上面的劣质中由于输入的是“广州”这个词,因此得到了包含出生地为广州的用户信息。但如果将搜索条件改为“广东广州”,那么在进行检索前会将其分成“广东”和“广州”,所以搜索条件中出生地只要包含“广东”或者是“广州”的用户都会被搜索到。
示例:
响应结果:
因此会发现,含有“深圳”用户信息的也会被搜索出来,因为“广东省深圳市”中也包含了“广东”这个词。
前面的term或者是match都是针对一个字段进行检索,而multi_match用于检索多个字段。
GET /index/_search
{
"query" : {
"multi_match" : {
"query" : "广州",
"fields" : ["name","birthplace"]
}
}
}
示例:
响应结果:
从结果看出,在name和birthplace字段中只要包含“广州”这个词的都会被检索到。
range表示范围查询,例如在文档中检索年龄大于等于20并且小于等于23的用户信息。
GET /index/_search
{
"query": {
"range": {
"age": {
"gte": 20,
"lte": 23
}
}
}
}
说明:
关键字 | 说明 |
---|---|
gte | “>=” |
lte | “<=” |
gt | “>” |
lt | “<” |
示例:
响应结果:
GET /index/_search
{
"query": {
"wildcard" : {
"name" : "张*"
}
}
}
说明:
符号 | 说明 |
---|---|
* | 匹配任何字符 |
? | 匹配零个或多个字符 |
示例:
响应结果:
bool查询通常用来组合多个子句进行检索,也可以理解为是一种组合查询。它主要包括must、must_not、filter、should这四种子句构成,并且这四种子句是可选的,不要求全部出现。
GET /index/_search
{
"query": {
"bool":{
"must":{
...
},
"must_not":{
...
},
"filter":{
...
},
"should":{
...
}
}
}
}
说明:
子句 | 说明 |
---|---|
must | 检索的结果必须满足must子句 |
must_not | 检索的结果必须不满足must_not子句 |
filter | 检索的结果必须满足filter子句,(注意:filter子句不会计算分值) |
should | 检索的结果可能满足should子句,也就是说子句中没有出现must或filter,但有一个或多个should子句,只要满足一个即可 |
示例1:
响应结果:
示例2:
这个示例中将使用filter,前面的DSL查询都是基于query的,query查询关注是检索出来的文档是否匹配这个查询,并且它还有一个具体的评分“_score”,评分越高关注度也就越高,在评分之后才会返回具体的文档内容。而filter查询则不一样,filter查询关注的是这个文档是否匹配,但它不会去计算任何分值,也不关心返回结果的排序问题,因此会提高查询效率。另外,ES还会自动缓存过滤查询的内容,在性能上也会有所提高。
响应结果:
从检索的记过看出,filter并不会对检索的文档进行评分,"_score"字段的值都为0。
GET /index/_search
{
"size": 0,
"aggs": {
"group_by_birthplace": {
"terms": {
"field": "birthplace"
}
}
}
}
其中,group_by_birthplace为分组名称,可自定命名,tems、fileld指定根据哪个属性进行分组,此案例根据birthplace属性进行分组(聚合)查询
示例:
响应结果:
GET /index/_search
{
"size": 0,
"aggs": {
"tj_by_age": {
"stats": {
"field": "age"
}
}
}
}
其中,tj_by_age是自定义名称,stats函数用于统计计算(类似还有max、min、avg等函数),field指定根据age属性进行统计计算。
示例:
结果:
ES支持使用对检索的结果进行排序,并且支持多个字段的排序。
GET /index/_search
{
"query": {
"match" : {
"birthplace": "广州"
}
},
"sort": {
"age": {"order":"desc"}
}
}
说明:
sort的值是一个数组,如果需要对多个字段排序,需要使用“[]”括起来。如:
{
"query": {
"match" : {
"birthplace": "广州"
}
},
"sort": [
{"age": {"order":"desc"}},
{"_id": {"order":"desc"}}
]
}
注意:如果多个字段使用了不同的排序,则使用第一个字段的排序。
示例:
响应结果:
GET /index/_search
{
"query": {
"match" : {
"birthplace": "广东"
}
},
"form": 0,
"size": 2
}
说明:form表示从第几条开始取,size表示取多少条。
示例:
响应结果:
可以ES中使用highlight来指定需要高亮的字段,在响应结果中,会使用""的HTML标记包裹搜索词。
GET /index/_search
{
"query": {
"match" : {
"birthplace": "广东"
}
},
"highlight": {
"fields" : {
"birthplace" : {}
}
}
}
示例:
响应结果:
上面的例子只列出一些常见的API操作,在ES中还有大量的API,如果需要深入学习,可以参考官方文档。
参考资料:
https://www.elastic.co/guide/en/elasticsearch/reference/7.0/docs.html
ES客户端与服务端通讯主要由两种方式,一种是Transport,它是基于TCP协议进行通讯,另一种则使用RESTFull API,基于http协议。在新的版本中官方推荐使用RESTFull API。而RESTFull API也分为low level(低级别)和high level(高级别),下面的案例主要讲解high level的使用。
Rest-High-Level-Client是官方提供的高级别的Restful API,使用前需添加elasticsearch-rest-high-level-client依赖。
<dependency>
<groupId>org.elasticsearch.clientgroupId>
<artifactId>elasticsearch-rest-high-level-clientartifactId>
<version>7.3.1version>
dependency>
要使用high level的客户端,首先必须构建RestHighLevelClient实例,如下:
RestHighLevelClient client = new RestHighLevelClient(
RestClient.builder(
new HttpHost("localhost", 9200, "http")));
...
client.close();
如果需要访问ES集群时,可添加多个HttpHost来配置每个ES节点的地址、端口以及访问协议。
RestHighLevelClient client = new RestHighLevelClient(
RestClient.builder(
new HttpHost("localhost", 9200, "http"),
new HttpHost("localhost", 9201, "http")));
...
client.close();
使用CreateIndexRequest对象创建索引,index为索引名称。CreateIndexResponse为响应对象,它的isAcknowledged方法返回一个boolean值,true为创建成功,false则失败。
public void createIndex(String index){
try(RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(
new HttpHost("localhost", 9200, "http")))){
//创建索引
CreateIndexRequest request = new CreateIndexRequest(index);
CreateIndexResponse response = client.indices().create(request, RequestOptions.DEFAULT);
boolean acknowledged = response.isAcknowledged();
System.out.println(acknowledged);
}catch(IOException e){
e.printStackTrace();
}
}
测试:
@Test
public void testCreateIndex(){
IndexDemo indexDemo = new IndexDemo();
indexDemo.createIndex("users_info");
}
使用CloseIndexRequest对象关闭索引。
public void closeIndex(String index){
try(RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(
new HttpHost("localhost", 9200, "http")))){
//关闭索引
CloseIndexRequest request = new CloseIndexRequest(index);
AcknowledgedResponse response = client.indices().close(request, RequestOptions.DEFAULT);
boolean acknowledged = response.isAcknowledged();
System.out.println(acknowledged);
}catch(IOException e){
e.printStackTrace();
}
}
测试:
@Test
public void testCloseIndex(){
IndexDemo indexDemo = new IndexDemo();
indexDemo.closeIndex("users_info");
}
使用OpenIndexRequest打开索引。
public void openIndex(String index){
try(RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(
new HttpHost("localhost", 9200, "http")))){
//打开索引
OpenIndexRequest request = new OpenIndexRequest(index);
OpenIndexResponse response = client.indices().open(request, RequestOptions.DEFAULT);
boolean acknowledged = response.isAcknowledged();
System.out.println(acknowledged);
}catch(IOException e){
e.printStackTrace();
}
}
测试:
@Test
public void testOpenIndex(){
IndexDemo indexDemo = new IndexDemo();
indexDemo.openIndex("users_info");
}
使用DeleteIndexRequest对象删除索引。
public void deleteIndex(String index){
try(RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(
new HttpHost("localhost", 9200, "http")))){
//删除索引
DeleteIndexRequest request = new DeleteIndexRequest(index);
AcknowledgedResponse response = client.indices().delete(request, RequestOptions.DEFAULT);
boolean acknowledged = response.isAcknowledged();
System.out.println(acknowledged);
}catch(IOException e){
e.printStackTrace();
}
}
测试:
@Test
public void testDeleteIndex(){
IndexDemo indexDemo = new IndexDemo();
indexDemo.deleteIndex("users_info");
}
使用PutMappingRequest为Index添加mapping。构建mapping有几种方式,可以使用json字符串或者使用map集合,也可以使用XContentBuilder来构建。下面分别使用这几种方式创建如下的mapping结构。
{
"properties": {
"name": {
"type": "text",
"analyzer": "ik_max_word"
},
"age": {
"type": "integer"
},
"birthplace": {
"type": "text",
"analyzer": "ik_max_word"
}
}
}
使用JSON字符串构建mapping:
public void createMapping(String index){
try(RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(
new HttpHost("localhost", 9200, "http")))){
PutMappingRequest request = new PutMappingRequest(index);
request.source("{\n" +
" \"properties\": {\n" +
" \"name\": {\n" +
" \"type\": \"text\",\n" +
" \"analyzer\": \"ik_max_word\"\n" +
" },\n" +
" \"age\": {\n" +
" \"type\": \"integer\"\n" +
" },\n" +
" \"birthplace\": {\n" +
" \"type\": \"text\",\n" +
" \"analyzer\": \"ik_max_word\"\n" +
" }\n" +
" }\n" +
"}", XContentType.JSON);
AcknowledgedResponse response = client.indices().putMapping(request, RequestOptions.DEFAULT);
boolean acknowledged = response.isAcknowledged();
System.out.println(acknowledged);
}catch(IOException e){
e.printStackTrace();
}
}
使用Map集合构建mapping:
public void createMapping(String index){
try(RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(
new HttpHost("localhost", 9200, "http")))){
PutMappingRequest request = new PutMappingRequest(index);
Map<String, Object> jsonMap = new HashMap<>();
Map<String, Object> properties = new HashMap<>();
Map<String, Object> userName = new HashMap<>();
userName.put("type", "text");
userName.put("analyzer", "ik_max_word");
Map<String, Object> age = new HashMap<>();
age.put("type", "integer");
Map<String, Object> birthplace = new HashMap<>();
birthplace.put("type", "text");
birthplace.put("analyzer", "ik_max_word");
properties.put("name", userName);
properties.put("age", age);
properties.put("birthplace", birthplace);
jsonMap.put("properties", properties);
request.source(jsonMap);
AcknowledgedResponse response = client.indices().putMapping(request, RequestOptions.DEFAULT);
boolean acknowledged = response.isAcknowledged();
System.out.println(acknowledged);
}catch(IOException e){
e.printStackTrace();
}
}
使用XContentBuilder构建mapping:
public void createMapping(String index){
try(RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(
new HttpHost("localhost", 9200, "http")))){
PutMappingRequest request = new PutMappingRequest(index);
XContentBuilder builder = XContentFactory.jsonBuilder();
builder.startObject();
{
builder.startObject("properties");
{
builder.startObject("name");
{
builder.field("type", "text");
builder.field("analyzer", "ik_max_word");
}
builder.endObject();
builder.startObject("age");
{
builder.field("type", "integer");
}
builder.endObject();
builder.startObject("birthplace");
{
builder.field("type", "text");
builder.field("analyzer", "ik_max_word");
}
builder.endObject();
}
builder.endObject();
}
builder.endObject();
request.source(builder);
AcknowledgedResponse response = client.indices().putMapping(request, RequestOptions.DEFAULT);
boolean acknowledged = response.isAcknowledged();
System.out.println(acknowledged);
}catch(IOException e){
e.printStackTrace();
}
}
测试:
@Test
public void testCreateMapping(){
MappingDemo demo = new MappingDemo();
demo.createMapping("users_info");
}
使用GetMappingsRequest查看Mapping信息,执行后返回的是GetMappingsResponse。
public void getMapping(String index){
try(RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(
new HttpHost("localhost", 9200, "http")))){
GetMappingsRequest request = new GetMappingsRequest();
request.indices(index);
GetMappingsResponse response = client.indices().getMapping(request, RequestOptions.DEFAULT);
Map<String, MappingMetaData> allMappings = response.mappings();
MappingMetaData indexMapping = allMappings.get(index);
Map<String, Object> mapping = indexMapping.sourceAsMap();
for (String key : mapping.keySet()) {
System.out.println(key + " : " + mapping.get(key));
}
}catch(IOException e){
e.printStackTrace();
}
测试:
@Test
public void testGetMapping(){
MappingDemo demo = new MappingDemo();
demo.getMapping("users_info");
}
使用IndexRequest对象创建文档,执行后返回IndexResponse。
/**
* 创建文档
* @param index 索引名称
* @param id 文档唯一标识
* @param jsonMap 文档信息
*/
public void createDocument(String index, String id, Map<String, Object> jsonMap){
try(RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(
new HttpHost("localhost", 9200, "http")))){
IndexRequest request = new IndexRequest(index).id(id).source(jsonMap);
IndexResponse response = client.index(request, RequestOptions.DEFAULT);
System.out.println(response.getId());
}catch (IOException e){
e.printStackTrace();
}
}
测试:
@Test
public void testCreateDocument(){
Map<String, Object> user1 = new HashMap<>();
user1.put("name", "张三");
user1.put("age", "20");
user1.put("birthplace", "广东省广州市");
Map<String, Object> user2 = new HashMap<>();
user2.put("name", "李四");
user2.put("age", "21");
user2.put("birthplace", "广东省广州市");
Map<String, Object> user3 = new HashMap<>();
user3.put("name", "王五");
user3.put("age", "22");
user3.put("birthplace", "广东省深圳市");
Map<String, Object> user4 = new HashMap<>();
user4.put("name", "赵六");
user4.put("age", "23");
user4.put("birthplace", "广东省深圳市");
Map<String, Object> user5 = new HashMap<>();
user5.put("name", "陈广州");
user5.put("age", "24");
user5.put("birthplace", "广东省东莞市");
DocumentDemo demo = new DocumentDemo();
demo.createDocument("users_info", "1", user1);
demo.createDocument("users_info", "2", user2);
demo.createDocument("users_info", "3", user3);
demo.createDocument("users_info", "4", user4);
demo.createDocument("users_info", "5", user5);
}
查看文档使用GetReuqest对象,执行后返回GetResponse对象。
/**
* 查看文档
* @param index 索引名称
* @param id 文档唯一标识
*/
public void getDocument(String index, String id){
try(RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(
new HttpHost("localhost", 9200, "http")))){
GetRequest request = new GetRequest(index, id);
GetResponse response = client.get(request, RequestOptions.DEFAULT);
Map<String, Object> sourceMap = response.getSource();
for(String field : sourceMap.keySet()){
System.out.println(field + " : " + sourceMap.get(field));
}
}catch (IOException e){
e.printStackTrace();
}
}
测试:
@Test
public void testGetDocument(){
DocumentDemo demo = new DocumentDemo();
demo.getDocument("users_info", "1");
}
是该文档使用UpdateRequest对象,执行后返回UpdateResponse。
/**
* 修改文档
* @param index 索引名称
* @param id 文档唯一标识
* @param jsonMap 文档信息
*/
public void updateDocument(String index, String id, Map<String, Object> jsonMap){
try(RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(
new HttpHost("localhost", 9200, "http")))){
UpdateRequest request = new UpdateRequest(index, id).doc(jsonMap);
UpdateResponse response = client.update(request, RequestOptions.DEFAULT);
System.out.println(response.status().getStatus());
}catch (IOException e){
e.printStackTrace();
}
}
测试:
@Test
public void testUpdateDocument(){
Map<String, Object> user = new HashMap<>();
user.put("name", "张三三");
DocumentDemo demo = new DocumentDemo();
demo.updateDocument("users_info", "1", user);
}
删除文档使用DeleteRequest对象,执行后返回DeleteResponse.
/**
* 删除文档
* @param index 索引名称
* @param id 文档唯一标识
*/
public void deleteDocument(String index, String id){
try(RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(
new HttpHost("localhost", 9200, "http")))){
DeleteRequest request = new DeleteRequest(index, id);
DeleteResponse response = client.delete(request, RequestOptions.DEFAULT);
System.out.println(response.status().getStatus());
}catch (IOException e){
e.printStackTrace();
}
}
测试:
@Test
public void testDeleteDocument(){
DocumentDemo demo = new DocumentDemo();
demo.deleteDocument("users_info", "3");
}
文档检索使用SearchRquest对象发送请求,同时使用QueryBuilders来构建不同的检索策略,执行后返回SearchResponse。
使用term查询:
ES中检索使用SearchRquest对象,执行后返回SearchResponse。
/**
* term查询
* @param index 索引
* @param field 检索字段
* @param value 检索内容
*/
public void termQuery(String index, String field, String value){
try(RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(
new HttpHost("localhost", 9200, "http")))){
SearchRequest request = new SearchRequest(index);
SearchSourceBuilder builder = new SearchSourceBuilder();
builder.query(QueryBuilders.termQuery(field, value));
request.source(builder);
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//返回命中结果
SearchHits sh = response.getHits();
//遍历命中结果中的每一条目
for (SearchHit hit : sh.getHits()) {
//输出文档内容,可以是一个Map,也可以是一个json字符串
//System.out.println(hit.getSourceAsMap());
System.out.println(hit.getSourceAsString());
}
}catch (IOException e){
e.printStackTrace();
}
}
测试:
@Test
public void testTermQuery(){
SearchDemo demo = new SearchDemo();
demo.termQuery("users_info","birthplace", "广州");
}
使用terms查询:
/**
* terms查询
* @param index 索引
* @param field 检索字段
* @param values 检索字段的多个内容
*/
public void termsQuery(String index, String field, String...values){
try(RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(
new HttpHost("localhost", 9200, "http")))){
SearchRequest request = new SearchRequest(index);
SearchSourceBuilder builder = new SearchSourceBuilder();
builder.query(QueryBuilders.termsQuery(field, values));
request.source(builder);
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
SearchHits sh = response.getHits();
for (SearchHit hit : sh.getHits()) {
System.out.println(hit.getSourceAsString());
}
}catch (IOException e){
e.printStackTrace();
}
}
测试:
@Test
public void testTermsQuery(){
SearchDemo demo = new SearchDemo();
demo.termsQuery("users_info","birthplace", "广州", "深圳");
}
使用match查询:
/**
* match查询
* @param index 索引
* @param field 检索字段
* @param value 检索内容
*/
public void matchQuery(String index, String field, String value){
try(RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(
new HttpHost("localhost", 9200, "http")))){
SearchRequest request = new SearchRequest(index);
SearchSourceBuilder builder = new SearchSourceBuilder();
builder.query(QueryBuilders.matchQuery(field, value));
request.source(builder);
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
SearchHits sh = response.getHits();
for (SearchHit hit : sh.getHits()) {
System.out.println(hit.getSourceAsString());
}
}catch (IOException e){
e.printStackTrace();
}
}
测试:
@Test
public void testMatchQuery(){
SearchDemo demo = new SearchDemo();
demo.matchQuery("users_info","birthplace", "广州");
}
使用multi_match查询:
/**
* multi match查询
* @param index 索引
* @param value 检索内容
* @param fields 检索的多个字段
*/
public void multiMatchQuery(String index, String value, String...fields){
try(RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(
new HttpHost("localhost", 9200, "http")))){
SearchRequest request = new SearchRequest(index);
SearchSourceBuilder builder = new SearchSourceBuilder();
builder.query(QueryBuilders.multiMatchQuery(value, fields));
request.source(builder);
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
SearchHits sh = response.getHits();
for (SearchHit hit : sh.getHits()) {
System.out.println(hit.getSourceAsString());
}
}catch (IOException e){
e.printStackTrace();
}
}
测试:
@Test
public void testMultiMatchQuery(){
SearchDemo demo = new SearchDemo();
demo.multiMatchQuery("users_info", "广州","name", "birthplace");
}
使用Range查询:
/**
* 范围查询
* @param index 索引
* @param field 检索字段
* @param gteValue >=于某个范围的值
* @param lteValue <=于某个范围的值
*/
public void rangeQuery(String index, String field, int gteValue, int lteValue){
try(RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(
new HttpHost("localhost", 9200, "http")))){
SearchRequest request = new SearchRequest(index);
SearchSourceBuilder builder = new SearchSourceBuilder();
builder.query(QueryBuilders.rangeQuery(field).gte(gteValue).lte(lteValue));
request.source(builder);
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
SearchHits sh = response.getHits();
for (SearchHit hit : sh.getHits()) {
System.out.println(hit.getSourceAsString());
}
}catch (IOException e){
e.printStackTrace();
}
}
测试:
@Test
public void testRangeQuery(){
SearchDemo demo = new SearchDemo();
demo.rangeQuery("users_info", "age",20, 23);
}
使用Bool查询:
/**
* bool查询
* @param index 索引
* @param mustField 检索时必须包含的字段
* @param mustNotField 检索时必须不包含的字段
* @param value 检索内容
*/
public void boolQuery(String index, String mustField, String mustNotField, String value) {
try(RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(
new HttpHost("localhost", 9200, "http")))){
SearchRequest request = new SearchRequest(index);
SearchSourceBuilder builder = new SearchSourceBuilder();
builder.query(QueryBuilders.boolQuery()
.must(QueryBuilders.matchQuery(mustField, value))
.mustNot(QueryBuilders.termQuery(mustNotField, value)));
request.source(builder);
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
SearchHits sh = response.getHits();
for (SearchHit hit : sh.getHits()) {
System.out.println(hit.getSourceAsString());
}
}catch (IOException e){
e.printStackTrace();
}
}
测试:
@Test
public void testBoolQuery(){
SearchDemo demo = new SearchDemo();
demo.boolQuery("users_info", "birthplace","name", "广州");
}
分页使用SearchSourceBuilder提供的from和size方法即可。
/**
* 分页
* @param index 索引
* @param field 检索字段
* @param value 检索内容
*/
public void pageQuery(String index, String field, String value){
try(RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(
new HttpHost("localhost", 9200, "http")))){
SearchRequest request = new SearchRequest(index);
SearchSourceBuilder builder = new SearchSourceBuilder();
builder.query(QueryBuilders.matchQuery(field, value));
builder.from(0).size(2);
request.source(builder);
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
SearchHits sh = response.getHits();
for (SearchHit hit : sh.getHits()) {
System.out.println(hit.getSourceAsString());
}
}catch (IOException e){
e.printStackTrace();
}
}
测试:
@Test
public void testPageQuery(){
SearchDemo demo = new SearchDemo();
demo.pageQuery("users_info","birthplace", "广州");
}
排序使用SearchSourceBuilder提供的的sort方法即可。需要注意的是,使用了分词的字段是不能被排序的,因为分析器将字符串拆分成了很多词汇单元。
/**
* 排序
* @param index 索引
* @param field 检索字段
* @param value 检索内容
* @param sortField 需要排序的字段
*/
public void sortQuery(String index, String field, String value, String sortField){
try(RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(
new HttpHost("localhost", 9200, "http")))){
SearchRequest request = new SearchRequest(index);
SearchSourceBuilder builder = new SearchSourceBuilder();
builder.query(QueryBuilders.matchQuery(field, value));
builder.sort(sortField, SortOrder.ASC);
request.source(builder);
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
SearchHits sh = response.getHits();
for (SearchHit hit : sh.getHits()) {
System.out.println(hit.getSourceAsString());
}
}catch (IOException e){
e.printStackTrace();
}
}
测试:
@Test
public void testSortQuery(){
SearchDemo demo = new SearchDemo();
demo.sortQuery("users_info","birthplace", "广州", "age");
}
高亮显示需要使用HighlightBuilder,它可以为多个字段进行高亮显示。因此我们可以先编写一个HighlightUtils的工具类,用于创建HighlightBuilder,并设置好需要高亮的字段以及高亮的类型。
HighlightUtils.java
public class HighlightUtils {
/**
*
* @param fields 需要高亮的字段,HighlightBuilder可以对多个字段进行高亮显示
* @return
*/
public static HighlightBuilder createHighlightBuilder(String...fields){
//创建一个HighlightBuilder
HighlightBuilder highlightBuilder = new HighlightBuilder();
for (String field : fields) {
//指定需要高亮显示的字段
HighlightBuilder.Field highlightField = new HighlightBuilder.Field(field);
//设置高亮的类型,unified为荧光笔
highlightField.highlighterType("unified");
//将高亮字段(highlightField)添加到highlightBuilder中
highlightBuilder.field(highlightField);
}
return highlightBuilder;
}
}
示例:
/**
* 高亮显示
* @param index 索引
* @param field 检索字段,同时也是需要高亮显示的字段
* @param value 检索内容
*/
public void highlightQuery(String index, String field, String value) {
try(RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(
new HttpHost("localhost", 9200, "http")))){
SearchRequest request = new SearchRequest(index);
//通过HighlightUtils构建HighlightBuilder对象
HighlightBuilder highlightBuilder = HighlightUtils.createHighlightBuilder(field);
//创建SearchSourceBuilder,并将highlightBuilder设置到SearchSourceBuilder中
SearchSourceBuilder builder = new SearchSourceBuilder();
//将highlightBuilder设置到SearchSourceBuilder中
builder.highlighter(highlightBuilder);
builder.query(QueryBuilders.matchQuery(field, value));
request.source(builder);
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
SearchHits sh = response.getHits();
for (SearchHit hit : sh.getHits()) {
//从命中的记录中获取高亮字段的Map集合
Map<String, HighlightField> highlightFields = hit.getHighlightFields();
//循环map获取高亮字段信息
for (String key : highlightFields.keySet()) {
String highlightValue = highlightFields.get(key).fragments()[0];
System.out.println(highlightValue);
}
}
}catch (IOException e){
e.printStackTrace();
}
}
测试:
@Test
public void testHighlightQuery(){
HighlightDemo demo = new HighlightDemo();
demo.highlightQuery("users_info", "birthplace", "广州");
}
以上只是列出常用API的使用,如果想了解更多细节,请参阅官方帮助文档。
参考资料:
https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/java-rest-high.html
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-elasticsearchartifactId>
dependency>
spring:
elasticsearch:
rest:
# 指定连接主机地址以及端口,多个地址使用逗号隔开
uris: http://127.0.0.1:9200
# 连接超时时间
connection-timeout: 2s
# 连接账号(如果有提供)
# username: xxx
# 连接密码
# password: xxx
创建实体类用于映射Document,并在实体类中使用相关注解进行标识。注解说明如下:
@Document注解:标记实体类为文档对象,一般有两个属性
@Mapping注解:文档映射
@Id注解:标指定当前字段作为id主键
@Field注解:标记为文档的字段,并指定字段映射属性
示例:
//在使用ElasticsearchRestTemplate时,可以将createIndex设置为false
@Document(indexName = "users", createIndex = false)
@Mapping
public class Users {
@Id
private String id;
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String name;
@Field(type = FieldType.Integer)
private Integer age;
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String birthplace;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public String getBirthplace() {
return birthplace;
}
public void setBirthplace(String birthplace) {
this.birthplace = birthplace;
}
}
我们可以通过注入ElasticsearchRestTemplate来操作Elasticsearch。
@Autowired
private ElasticsearchRestTemplate template;
示例:
创建index
//指定映射的实体,会根据@Document的indexName自动创建索引
template.indexOps(User.class).create();
检查索引是否存在
template.indexOps(User.class).exists()
创建Mapping
//先根据User类的注解配置创建Document对象
Document doc = template.indexOps(User.class).createMapping(User.class);
//为索引添加mapping
template.indexOps(User.class).putMapping(doc);
删除index
//指定映射的实体,会根据@Document的indexName删除索引
template.indexOps(User.class).delete();
添加文档
Users user = new Users();
user.setId("1001");
user.setName("张三");
user.setAge(21);
user.setBirthplace("广东省广州市");
template.save(user);
修改文档
//创建文档对象
Document doc = Document.create();
//设置index
doc.setIndex("users");
//设置id
doc.setId("1");
//设置需要更新的字段和值
doc.put("name", "张三");
doc.put("age", 20);
UpdateQuery updateQuery = UpdateQuery.builder(doc.getId()).withDocument(doc).build();
template.update(updateQuery, IndexCoordinates.of(doc.getIndex()));
根据ID删除文档
template.delete("1001", Users.class);
根据ID查询文档
Users user = template.get("1001", Users.class);
查询所有文档
NativeSearchQuery query = new NativeSearchQueryBuilder().build();
SearchHits<Users> hits = template.search(query, Users.class);
分页
NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder();
builder.withPageable(PageRequest.of(0,2));
SearchHits<Users> hits = template.search(builder.build(), Users.class);
排序
NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder();
FieldSortBuilder sortBuilder = SortBuilders.fieldSort("age").order(SortOrder.DESC);
builder.withSort(sortBuilder);
SearchHits<Users> hits = template.search(builder.build(), Users.class);
文档检索
文档检索可以通过QueryBuilders构建不同的查询方式来进行检索,例如:
term查询:
NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder();
builder.withQuery(QueryBuilders.termQuery("birthplace", "广州"));
SearchHits<Users> hits = template.search(builder.build(), Users.class);
math查询:
NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder();
builder.withQuery(QueryBuilders.matchQuery("birthplace", "广州"));
SearchHits<Users> hits = template.search(builder.build(), Users.class);
高亮
NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder();
builder.withQuery(QueryBuilders.termQuery("birthplace", "广州"))
.withHighlightBuilder(new HighlightBuilder()
.field("birthplace")
.preTags("")
.postTags(""));
SearchHits<Users> hits = template.search(builder.build(), Users.class);
hits.forEach(hit -> System.out.println(hit.getContent().getId()+ " , " + hit.getContent().getAge() + " , " + hit.getContent().getName() + " , " + hit.getHighlightField("birthplace")));
除了ElasticsearchRestTemplate,还可以使用spring提供的ElasticsearchRepository接口,它继承自 ElasticsearchCrudRepository接口,默认提供了相关的CRUD操作。并且在ElasticsearchRepository接口中扩展了文档的检索以及分页检索等方法,非常的便利。
public interface ElasticsearchRepository<T, ID> extends ElasticsearchCrudRepository<T, ID> {
<S extends T> S index(S entity);
<S extends T> S indexWithoutRefresh(S entity);
Iterable<T> search(QueryBuilder query);
Page<T> search(QueryBuilder query, Pageable pageable);
Page<T> search(SearchQuery searchQuery);
Page<T> searchSimilar(T entity, String[] fields, Pageable pageable);
void refresh();
Class<T> getEntityClass();
}
我们只需要声明一个接口继承ElasticsearchRepository。接口的泛型参数分别指定操作的实体和ID的类型。
public interface UserRepository extends ElasticsearchRepository<Users, String> {
}
并将UserRepository注入到相应的类中
@Autowired
private UserRepository userRepository;
示例:
1.添加文档
Users user = new Users();
user.setId("1001");
user.setName("张三");
user.setAge(21);
user.setBirthplace("广东省广州市");
userRepository.save(user);
当index不存在时会自动创建并完成mapping映射
2.根据ID获取文档
Optional<Users> optional = userRepository.findById("1001");
Users user = optional.get();
3.根据ID删除文档
userRepository.deleteById("1001");
4.查询所有文档
Iterable<Users> it = userRepository.findAll();
5.文档检索
文档检索同样使用QueryBuilders构建不同的查询方式来进行检索,例如:
term查询:
Iterable<Users> iterable = userRepository.search(QueryBuilders.termQuery("birthplace", "广州"));
match查询:
Iterable<Users> iterable = userRepository.search(QueryBuilders.matchQuery("birthplace", "广州"));
5.分页
Pageable pageable = PageRequest.of(0, 2);
Iterable<Users> iterable = userRepository.findAll(pageable);
6.排序
Iterable<Users> iterable = userRepository.findAll(Sort.by(Sort.Order.desc("age")));
7.自定义方法
当ElasticsearchRepository默认提供的方法不能满足业务需求时,我们也可以在自定义的接口中编写检索方法,只要方法名符合Spring的约定,也就是说方法名中只要包含符合约定的关键字,那么就能自动帮我们使用ES的对应Json查询字符串进行检索。
约定示例:
关键字 | 方法名示例 | 对应的ES查询字符串 |
---|---|---|
And | findByNameAndPrice | {“bool” : {“must” : [ {“field” : {“name” : “?”}}, {“field” : {“price” : “?”}} ]}} |
Or | findByNameOrPrice | {“bool” : {“should” : [ {“field” : {“name” : “?”}}, {“field” : {“price” : “?”}} ]}} |
Is | findByName | {“bool” : {“must” : {“field” : {“name” : “?”}}}} |
Not | findByNameNot | {“bool” : {“must_not” : {“field” : {“name” : “?”}}}} |
Between | findByPriceBetween | {“bool” : {“must” : {“range” : {“price” : {“from” : ?,“to” : ?,“include_lower” : true,“include_upper” : true}}}}} |
LessThanEqual | findByPriceLessThan | {“bool” : {“must” : {“range” : {“price” : {“from” : null,“to” : ?,“include_lower” : true,“include_upper” : true}}}}} |
GreaterThanEqual | findByPriceGreaterThan | {“bool” : {“must” : {“range” : {“price” : {“from” : ?,“to” : null,“include_lower” : true,“include_upper” : true}}}}} |
Before | findByPriceBefore | {“bool” : {“must” : {“range” : {“price” : {“from” : null,“to” : ?,“include_lower” : true,“include_upper” : true}}}}} |
After | findByPriceAfter | {“bool” : {“must” : {“range” : {“price” : {“from” : ?,“to” : null,“include_lower” : true,“include_upper” : true}}}}} |
Like | findByNameLike | {“bool” : {“must” : {“field” : {“name” : {“query” : “?*”,“analyze_wildcard” : true}}}}} |
StartingWith | findByNameStartingWith | {“bool” : {“must” : {“field” : {“name” : {“query” : “?*”,“analyze_wildcard” : true}}}}} |
EndingWith | findByNameEndingWith | {“bool” : {“must” : {“field” : {“name” : {“query” : “*?”,“analyze_wildcard” : true}}}}} |
Contains/Containing | findByNameContaining | {“bool” : {“must” : {“field” : {“name” : {“query” : “?”,“analyze_wildcard” : true}}}}} |
In | findByNameIn( Collection names) | {“bool” : {“must” : {“bool” : {“should” : [ {“field” : {“name” : “?”}}, {“field” : {“name” : “?”}} ]}}}} |
NotIn | findByNameNotIn( Collection names) | {“bool” : {“must_not” : {“bool” : {“should” : {“field” : {“name” : “?”}}}}}} |
True | findByAvailableTrue | {“bool” : {“must” : {“field” : {“available” : true}}}} |
False | findByAvailableFalse | {“bool” : {“must” : {“field” : {“available” : false}}}} |
OrderBy | findByAvailableTrueOrderByNameDesc | {“sort” : [{ “name” : {“order” : “desc”} }],“bool” : {“must” : {“field” : {“available” : true}}}} |
… | … | … |
示例:
public interface UserRepository extends ElasticsearchRepository<Users, String> {
/**
* 根据出生地检索
* @param birthplace
* @return
*/
List<Users> findByBirthplace(String birthplace);
}
测试:
@Test
void testCustomMethod(){
List<Users> users = userRepository.findByBirthplace("广东");
users.forEach(user -> System.out.println(user.getBirthplace()));
}
8.自定义查询条件
我们还可以使用@Query注解来自定义Json条件查询字符串
示例:
public interface UserRepository extends ElasticsearchRepository<Users, String> {
/**
* 自定义方法
* @param birthplace
* @return
*/
List<Users> findByBirthplace(String birthplace);
/**
* 自定义检索条件
* @param birthplace
* @return
*/
@Query("{\"match\" : {\"birthplace\" : \"?0\"}}}")
List<Users> listUser(String birthplace);
}
测试:
@Test
void testCustomQuery(){
List<Users> users = userRepository.listUser("广东");
users.forEach(user -> System.out.println(user.getName()));
}