RestHighLevelClient
客户端介绍
在 elasticsearch 官网中提供了各种语言的客户端:
选择 Java REST Client
选择 Java High Level Rest Client 版本,这里有使用的API
es依赖
<dependency>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch</artifactId>
<version>6.2.4</version>
</dependency>
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-client</artifactId>
<version>6.2.4</version>
</dependency>
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>6.2.4</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.70</version>
</dependency>
索引库及映射
创建索引库的同时创建type及其映射关系,但是这些操作不建议使用java客户端完成,原因如下:
-
索引库和映射往往是初始化时完成,不需要频繁操作,不如提前配置好
-
官方提供的创建索引库及映射API非常繁琐,需要通过字符串拼接json结构:
request.mapping( "{\n" + " \"properties\": {\n" + " \"message\": {\n" + " \"type\": \"text\"\n" + " }\n" + " }\n" + "}", XContentType.JSON);
因此,这些操作建议还是使用 Rest 风格API去实现。
以这样一个商品数据为例来创建索引库:
新增实体类:
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Item {
private Long id;
private String title; //标题
private String category;// 分类
private String brand; // 品牌
private Double price; // 价格
private String images; // 图片地址
}
- id:可以认为是主键,将来判断数据是否重复的标示,不分词,可以使用keyword类型
- title:搜索字段,需要分词,可以用text类型
- category:商品分类,这个是整体,不分词,可以使用keyword类型
- brand:品牌,与分类类似,不分词,可以使用keyword类型
- price:价格,这个是double类型
- images:图片,用来展示的字段,不搜索,index为false,不分词,可以使用keyword类型
映射配置:
PUT /item
{
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1
},
"mappings": {
"_doc": {
"properties": {
"id": {
"type": "keyword"
},
"title": {
"type": "text",
"analyzer": "ik_max_word"
},
"category": {
"type": "keyword"
},
"brand": {
"type": "keyword"
},
"images": {
"type": "keyword",
"index": false
},
"price": {
"type": "double"
}
}
}
}
}
查看添加结果
文档操作
初始化http客户端:RestHighLevelClient
完成任何操作都需要通过 RestHighLevelClient 客户端
入门示例
@RunWith(SpringRunner.class)
@SpringBootTest
class EsDemoApplicationTests {
RestHighLevelClient client;
/**
* 初始化连接
*/
@Before
void init() {
//初始化:高级客户端
client = new RestHighLevelClient(RestClient.builder(
new HttpHost("192.168.85.135", 9201, "http"),
new HttpHost("192.168.85.135", 9202, "http"),
new HttpHost("192.168.85.135", 9203, "http")
));
}
@After
void close() throws IOException {
client.close();
}
}
应用级案例
application.yml 配置文件
# es集群名称
elasticsearch.clusterName=single-node-cluster
# es用户名
elasticsearch.userName=elastic
# es密码
elasticsearch.password=elastic
# es 是否启用用户密码
elasticsearch.passwdEnabled=true
# es host ip 地址(集群):本次使用的是单机模式
elasticsearch.hosts=43.142.243.124:9200
# es 请求方式
elasticsearch.scheme=http
# es 连接超时时间
elasticsearch.connectTimeOut=1000
# es socket 连接超时时间
elasticsearch.socketTimeOut=30000
# es 请求超时时间
elasticsearch.connectionRequestTimeOut=500
# es 连接保持活跃时间(ms)
elasticsearch.keepAliveStrategy=180000
# es 最大连接数
elasticsearch.maxConnectNum=100
# es 每个路由的最大连接数
elasticsearch.maxConnectNumPerRoute=100
es连接配置类
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestClientBuilder;
import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.ArrayList;
import java.util.List;
/**
* restHighLevelClient 客户端配置类
*/
@Slf4j
@Data
@Configuration
@ConfigurationProperties(prefix = "elasticsearch")
public class ElasticsearchConfig {
// es host ip 地址(集群)
private String hosts;
// es用户名
private String userName;
// es密码
private String password;
// es 是否启用用户密码
private boolean passwdEnabled;
// es 请求方式
private String scheme;
// es集群名称
private String clusterName;
// es 连接超时时间
private int connectTimeOut;
// es socket 连接超时时间
private int socketTimeOut;
// es 请求超时时间
private int connectionRequestTimeOut;
// es 连接保持活跃时间
private int keepAliveStrategy;
// es 最大连接数
private int maxConnectNum;
// es 每个路由的最大连接数
private int maxConnectNumPerRoute;
@Bean(name = "restHighLevelClient")
public RestHighLevelClient restHighLevelClient() {
// 拆分地址。单节点配一个地址即可
List<HttpHost> hostLists = new ArrayList<>();
hosts = hosts.replace("http://", "");
String[] hostList = hosts.split(",");
for (String addr : hostList) {
String host = addr.split(":")[0];
String port = addr.split(":")[1] == null ? "9200": addr.split(":")[1];
hostLists.add(new HttpHost(host, Integer.parseInt(port), scheme));
}
// 转换成 HttpHost 数组
HttpHost[] httpHost = hostLists.toArray(new HttpHost[]{});
// 构建连接对象
RestClientBuilder builder = RestClient.builder(httpHost);
// 连接延时配置
builder.setRequestConfigCallback(requestConfigBuilder -> {
requestConfigBuilder
.setConnectTimeout(connectTimeOut)
.setSocketTimeout(socketTimeOut)
.setConnectionRequestTimeout(connectionRequestTimeOut);
return requestConfigBuilder;
});
builder.setHttpClientConfigCallback(httpClientBuilder -> {
httpClientBuilder
// 连接数配置
.setMaxConnTotal(maxConnectNum)
.setMaxConnPerRoute(maxConnectNumPerRoute)
// 连接保持活跃时间配置
.setKeepAliveStrategy((HttpRequest, HttpResponse) -> keepAliveStrategy);
// 设置用户名、密码
if (passwdEnabled) {
CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
credentialsProvider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(userName, password));
httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider);
}
return httpClientBuilder;
});
return new RestHighLevelClient(builder);
}
}
注:
-
KeepAliveStrategy :
HTTP 规范没有确定一个持久连接可能或应该保持活动多长时间。
一些HTTP服务器使用非标准的头部信息 Keep-Alive 来告诉客户端它们想在服务器端保持连接活动的周期秒数。
如果这个信息可用,HttpClient 就会利用这个它。
如果头部信息 Keep-Alive 在响应中不存在,HttpClient 假设连接无限期的保持活动。
然而许多现实中的 HTTP 服务器配置了在特定不活动周期之后丢掉持久连接来保存系统资源,往往这是不通知客户端的。
创建索引及映射
索引的 mappings
mapping_test.json
{
"properties": {
"brandName": {
"type": "keyword"
},
"categoryName": {
"type": "keyword"
},
"createTime": {
"type": "date",
"format": "yyyy-MM-dd HH:mm:ss"
},
"id": {
"type": "long"
},
"price": {
"type": "double"
},
"saleNum": {
"type": "integer"
},
"status": {
"type": "integer"
},
"stock": {
"type": "integer"
},
"spec": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart"
},
"title": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart"
}
}
}
import com.example.test.service.es.IndexTestService;
import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest;
import org.elasticsearch.action.support.master.AcknowledgedResponse;
import org.elasticsearch.client.IndicesClient;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.client.indices.CreateIndexRequest;
import org.elasticsearch.client.indices.CreateIndexResponse;
import org.elasticsearch.client.indices.GetIndexRequest;
import org.elasticsearch.client.indices.GetIndexResponse;
import org.elasticsearch.cluster.metadata.MappingMetaData;
import org.elasticsearch.common.xcontent.XContentType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Map;
/**
* 索引服务类
*/
@Service
public class IndexTestServiceImpl implements IndexTestService {
@value("classpath:json/mapping_test.json")
private resoure mappingTest;
@Autowired
RestHighLevelClient restHighLevelClient;
// 分片数的配置名
private String shardNumName = "number_of_shards";
// 副本数的配置名
private String replicaNumName = "number_of_replicas";
// 索引名
private String index = "goods"
@Override
public boolean indexCreate() throws Exception {
// 1、创建 创建索引request 参数:索引名
CreateIndexRequest indexRequest = new CreateIndexRequest(index);
// 2、设置索引的settings
indexRequest.settings(Settings.builder().put(shardNumName, 3).put(replicaNumName, 1));
// 3、设置索引的mappings(表结构)
String mappingJson = IOUtils.toString(mappingTest.getInputStream(), Charset.forName("UTF-8"));
indexRequest.mapping(mappingJson, XContentType.JSON);
// 4、 设置索引的别名
// 5、 发送请求
// 5.1 同步方式发送请求
// 请求服务器
IndicesClient indicesClient = restHighLevelClient.indices();
CreateIndexResponse response = indicesClient.create(indexRequest, RequestOptions.DEFAULT);
return response.isAcknowledged();
}
/**
* 获取表结构
* GET goods/_mapping
*/
@Override
public Map<String, Object> getMapping(String indexName) throws Exception {
IndicesClient indicesClient = restHighLevelClient.indices();
// 创建get请求
GetIndexRequest request = new GetIndexRequest(indexName);
// 发送get请求
GetIndexResponse response = indicesClient.get(request, RequestOptions.DEFAULT);
// 获取表结构
Map<String, MappingMetaData> mappings = response.getMappings();
Map<String, Object> sourceAsMap = mappings.get(indexName).getSourceAsMap();
return sourceAsMap;
}
/**
* 删除索引库
*/
@Override
public boolean indexDelete(String indexName) throws Exception {
IndicesClient indicesClient = restHighLevelClient.indices();
// 创建delete请求方式
DeleteIndexRequest deleteIndexRequest = new DeleteIndexRequest(indexName);
// 发送delete请求
AcknowledgedResponse response = indicesClient.delete(deleteIndexRequest, RequestOptions.DEFAULT);
return response.isAcknowledged();
}
/**
* 判断索引库是否存在
*/
@Override
public boolean indexExists(String indexName) throws Exception {
IndicesClient indicesClient = restHighLevelClient.indices();
// 创建get请求
GetIndexRequest request = new GetIndexRequest(indexName);
// 判断索引库是否存在
boolean result = indicesClient.exists(request, RequestOptions.DEFAULT);
return result;
}
}
新增文档:index
文档:https://www.elastic.co/guide/en/elasticsearch/client/java-rest/6.8/java-rest-high-document-index.html
示例:
// 新增文档
@Test
void add() throws IOException {
// 准备文档
Item item = new Item(1L, "小米手机9", "手机",
"小米", 3499.00, "http://image.leyou.com/13123.jpg");
// 将对象转换为Json
String json = JSON.toJSONString(item);
// 创建索引请求 参数为: 索引库名 类型名 文档ID
IndexRequest request = new IndexRequest("item", "_doc", item.getId().toString());
// 将Json格式的数据放入到请求中
request.source(json, XContentType.JSON);
// 发送请求
IndexResponse response = client.index(request);
// 打印结果
System.out.println("结果为:" + response);
}
响应:
response = IndexResponse[index=item,type=docs,id=1,version=1,result=created,seqNo=0,primaryTerm=1,shards={"total":2,"successful":2,"failed":0}]
查看文档:get
示例:
// 根据ID获取文档
@Test
void get() throws IOException {
// 创建get请求对象 参数为: 索引库名 类型名 文档ID
GetRequest request = new GetRequest("item","_doc","1");
// 发送请求
GetResponse response = client.get(request);
// 解析结果 结果为Json
String source = response.getSourceAsString();
// 将Json数据转换为对象 参数为: Json字符串 类的字节码
Item item = JSON.parseObject(source, Item.class);
// 打印结果
System.out.println(item);
}
更新文档:update
示例:
// 根据ID更新文档
@Test
void update() throws IOException{
// 准备文档
Item item = new Item(1L, "小米手机9", "手机",
"小米", 3699.00, "http://image.leyou.com/13123.jpg");
// 将对象转换为Json
String json = JSON.toJSONString(item);
// 创建Update请求对象 参数为: 索引库名 类型名 文档ID
UpdateRequest request = new UpdateRequest("item","_doc","1");
// 将Json格式的数据放入到请求中
request.doc(json,XContentType.JSON);
// 发送请求
UpdateResponse response = client.update(request);
// 打印结果
System.out.println("结果为:" + response);
}
删除文档:delete
示例:
// 根据ID删除文档
@Test
void delete() throws IOException {
// 创建Delete请求对象 参数为: 索引库名 类型名 文档ID
DeleteRequest request = new DeleteRequest("item","_doc","1");
// 发送请求
DeleteResponse response = client.delete(request);
// 打印结果
System.out.println("结果为:" + response);
}
批量新增:bulk
示例:
// 批量插入
@Test
void bulkInsert() throws IOException {
// 准备文档数据:
List<Item> list = new ArrayList<>();
list.add(new Item(1L, "小米手机7", "手机", "小米", 3299.00, "http://image.leyou.com/13123.jpg"));
list.add(new Item(2L, "坚果手机R1", "手机", "锤子", 3699.00, "http://image.leyou.com/13123.jpg"));
list.add(new Item(3L, "华为META10", "手机", "华为", 4499.00, "http://image.leyou.com/13123.jpg"));
list.add(new Item(4L, "小米Mix2S", "手机", "小米", 4299.00, "http://image.leyou.com/13123.jpg"));
list.add(new Item(5L, "荣耀V10", "手机", "华为", 2799.00, "http://image.leyou.com/13123.jpg"));
// 创建批量新增请求
BulkRequest request = new BulkRequest();
// 遍历集合
for (Item item : list) {
// 将索引请求添加到批量请求对象中
request.add(new IndexRequest("item", "_doc", item.getId().toString())
.source(JSON.toJSONString(item), XContentType.JSON));
}
// 发送请求
BulkResponse response = client.bulk(request);
// 打印结果
System.out.println("结果为:" + response);
}
文档搜索:search
查询所有:matchAllQuery
示例:
/**
* 查询文档
*/
@Test
public void searchDoc() throws IOException {
// 创建查询请求对象 指定查询的索引名称
SearchRequest request = new SearchRequest("item");
// 指定查询的源
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
// 构建查询条件
QueryBuilder query = QueryBuilders.matchAllQuery(); // 查询的是所有的文档
// 添加查询条件
sourceBuilder.query(query);
// 添加查询源
request.source(sourceBuilder);
// 发送请求
SearchResponse response = client.search(request);
// 分析响应结果
// 返回命中的数据对象
SearchHits searchHits = response.getHits();
// 获取命中的文档个数
long totalHits = searchHits.totalHits;
System.out.println("命中的文档个数为: " + totalHits);
// 获取命中的数据
SearchHit[] hits = searchHits.getHits();
// 遍历数组
for (SearchHit hit : hits) {
String sourceAsString = hit.getSourceAsString();
// 转换成对象
Item item = JSON.parseObject(sourceAsString, Item.class);
System.out.println(item);
}
}
上面的代码中,搜索条件是通过 sourceBuilder.query(QueryBuilders.matchAllQuery())来添加的。这个 query() 方法接受的参数是: QueryBuilder 接口类型。
QueryBuilder 接口提供了很多实现类,分别对应不同类型的查询,例如:term查询、match查询、range查询、boolean查询等。若要使用各种不同查询,只需传递不同的参数给 sourceBuilder.query() 方法即可。而这些实现类并不需要去 new ,官方提供了 QueryBuilders 工厂来构建各种实现类
关键字搜索:matchQuery
其实搜索类型的变化,仅仅是利用QueryBuilders构建的查询对象不同而已,其他代码基本一致:
// 使用match查询,参数为 1 查询的字段 2 查询的关键字
QueryBuilder query = QueryBuilders.matchQuery("title","小米");
关键字完全匹配:termQuery
// 使用term查询,参数为 1 查询的字段 2 查询的关键字
QueryBuilder query = QueryBuilders.termQuery("title","小米手机");
范围查询:rangeQuery
支持下面的范围关键字:
方法 | 说明 |
---|---|
gt(Object from) | 大于 |
gte(Object from) | 大于等于 |
lt(Object from) | 小于 |
lte(Object from) | 小于等于 |
示例:
// 使用rangeQuery查询,参数为 查询的字段
QueryBuilder query = QueryBuilders.rangeQuery("price").gte(2000).lt(4000); // 参数为:查询的字段 后面是链式的调用
响应:
item = Item(id=2, title=坚果手机R1, category=手机, brand=锤子, price=3699.0,
images=http://image.leyou.com/13123.jpg)
item = Item(id=5, title=荣耀V10, category=手机, brand=华为, price=2799.0,
images=http://image.leyou.com/13123.jpg)
item = Item(id=1, title=小米手机7, category=手机, brand=小米, price=3299.0,
images=http://image.leyou.com/13123.jpg)
过滤 :fetchSource
默认情况下,索引库中所有数据都会返回,如果想只返回部分字段,可以通过fetchSource来控制。
示例:
/**
* 查询文档
*/
@Test
public void searchDoc() throws IOException {
// 创建查询请求对象 指定查询的索引名称
SearchRequest request = new SearchRequest("item");
// 指定查询的源
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
// 构建查询条件
// QueryBuilder query = QueryBuilders.matchAllQuery(); // 查询的是所有的文档
// 使用match查询,参数为 1 查询的字段 2 查询的关键字
// QueryBuilder query = QueryBuilders.matchQuery("title","小米");
// 使用term查询,参数为 1 查询的字段 2 查询的关键字
// QueryBuilder query = QueryBuilders.termQuery("title","小米手机");
// 使用rangeQuery查询,参数为 1 查询的字段
QueryBuilder query = QueryBuilders.rangeQuery("price").gte(3000).lte(4000);
// 添加查询条件
sourceBuilder.query(query);
// 添加过滤
String[] includes = {"id", "title", "price"};
String[] excludes = {};
sourceBuilder.fetchSource(includes, excludes);
// 添加查询源
request.source(sourceBuilder);
// 发送请求
SearchResponse response = client.search(request);
// 分析响应结果
// 返回命中的数据对象
SearchHits searchHits = response.getHits();
// 获取命中的文档个数
long totalHits = searchHits.totalHits;
System.out.println("命中的文档个数为: " + totalHits);
// 获取命中的数据
SearchHit[] hits = searchHits.getHits();
// 遍历数组
for (SearchHit hit : hits) {
String sourceAsString = hit.getSourceAsString();
// 转换成对象
Item item = JSON.parseObject(sourceAsString, Item.class);
System.out.println(item);
}
}
排序:sort
示例:
// 排序
sourceBuilder.sort("price", SortOrder.DESC);
分页:from、size
示例:
// 分页
int current = 1;
int size = 2;
int start = (current - 1) * size;
sourceBuilder.from(start);
sourceBuilder.size(size);
// 搜索
SearchResponse response = client.search(request);
高亮:HighlightBuilder
示例:
@Test
public void testHighlight() throws IOException{
// 创建搜索对象
SearchRequest request = new SearchRequest();
// 指定查询的源
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
// 添加查询条件,通过QueryBuilders获取各种查询
sourceBuilder.query(QueryBuilders.matchQuery("title", "小米手机"));
// 高亮
HighlightBuilder highlightBuilder = new HighlightBuilder() //创建高亮构建器对象
.field("title") // 指定高亮字段
.preTags("<em style='color:red'>") // 添加高亮前缀
.postTags("</em>"); // 添加高亮后缀
sourceBuilder.highlighter(highlightBuilder);
request.source(sourceBuilder);
// 获取结果
SearchResponse response = client.search(request);
SearchHits hits = response.getHits();
SearchHit[] hitList = hits.getHits();
for (SearchHit hit : hitList) {
// 获取高亮结果
Map<String, HighlightField> fields = hit.getHighlightFields();
// 取出标题
HighlightField titleField = fields.get("title");
// 拼接为字符串
Text[] fragments = titleField.fragments();
String title = fragments[0].string();
// 获取其它字段,并转换成对象
Item item = JSON.parseObject(hit.getSourceAsString(), Item.class);
// 覆盖title
item.setTitle(title);
System.out.println(item);
}
}
关键代码:
- 查询条件中添加高亮字段:
- new HighlightBuilder() :创建高亮构建器
- .field(“title”) :指定高亮字段
- .preTags(“”) 和 .postTags(“”) :指定高亮的前置和后置标签
- 解析高亮结果:
- hit.getHighlightFields(); 获取高亮结果
聚合:aggregation
再来试试聚合,以brand字段来聚合,看看有哪些品牌,每个品牌有多少数量。
聚合关键是弄清楚这几点:
- 聚合的字段是什么
- 聚合的类型是什么
- 给聚合起个名
与查询类似,聚合条件通过 sourceBuilder.aggregation() 方法来设置,而参数是一个接口:
- AggregationBuilder ,这个接口也有大量的实现类,代表不同的聚合种类。
同样也不需要自己去new,官方提供了一个工厂帮助创建实例:
示例:
/**
* 聚合
*/
@Test
public void testAgg() throws IOException{
// 创建搜索对象
SearchRequest request = new SearchRequest();
// 指定查询的源
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
// 添加查询条件,通过QueryBuilders获取各种查询
sourceBuilder.query(QueryBuilders.matchAllQuery());
// 添加排序
sourceBuilder.sort("price", SortOrder.ASC);
// 配置size为0,因为不需要数据,只要聚合结果
sourceBuilder.size(0);
// 添加聚合
sourceBuilder.aggregation(AggregationBuilders.terms("brandAgg").field("brand"));
request.source(sourceBuilder);
// 获取结果
SearchResponse response = client.search(request);
// 获取聚合结果
Aggregations aggregations = response.getAggregations();
// 获取某个聚合
Terms terms = aggregations.get("brandAgg");
// 获取桶
for (Terms.Bucket bucket : terms.getBuckets()) {
// 获取key,这里是品牌名称
System.out.println("品牌 : " + bucket.getKeyAsString());
// 获取docCount,就是数量
System.out.println("count: " + bucket.getDocCount());
}
}
响应:
品牌 : 华为
count: 2
品牌 : 小米
count: 2
品牌 : 锤子
count: 1
还可以在聚合中添加子聚合
示例:
/**
* 聚合
*/
@Test
public void testAgg() throws IOException{
// 创建搜索对象
SearchRequest request = new SearchRequest();
// 指定查询的源
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
// 添加查询条件,通过QueryBuilders获取各种查询
sourceBuilder.query(QueryBuilders.matchAllQuery());
// 添加排序
sourceBuilder.sort("price", SortOrder.ASC);
// 配置size为0,因为不需要数据,只要聚合结果
sourceBuilder.size(0);
// 添加聚合
TermsAggregationBuilder termsAggregationBuilder = AggregationBuilders.terms("brandAgg").field("brand");
// 添加子聚合
termsAggregationBuilder.subAggregation(AggregationBuilders.avg("avgPrice").field("price"));
sourceBuilder.aggregation(termsAggregationBuilder);
request.source(sourceBuilder);
// 获取结果
SearchResponse response = client.search(request);
// 获取聚合结果
Aggregations aggregations = response.getAggregations();
// 获取某个聚合
Terms terms = aggregations.get("brandAgg");
// 获取桶
for (Terms.Bucket bucket : terms.getBuckets()) {
// 获取key,这里是品牌名称
System.out.println("品牌 : " + bucket.getKeyAsString());
// 获取docCount,就是数量
System.out.println("count: " + bucket.getDocCount());
// 获取子聚合
Avg avgPrice = bucket.getAggregations().get("avgPrice");
System.out.println("均价:" + avgPrice.getValue());
}
}
响应:
品牌 : 华为
count: 2
均价:3649.0
品牌 : 小米
count: 2
均价:3799.0
品牌 : 锤子
count: 1
均价:3699.0
Spring Data Elasticsearch
Spring Data Elasticsearch 是Spring提供的elasticsearch组件
Spring Data Elasticsearch 简介
Spring Data Elasticsearch(以后简称SDE)是Spring Data项目下的一个子模块。
查看 Spring Data的官网:https://spring.io/projects/spring-data
Spring Data 的使命是给各种数据访问提供统一的编程接口,不管是关系型数据库(如MySQL),还是
非关系数据库(如Redis),或者类似Elasticsearch这样的索引数据库。从而简化开发人员的代码,提
高开发效率。
包含很多不同数据操作的模块:
Spring Data Elasticsearch的页面:https://spring.io/projects/spring-data-elasticsearch
特征:
- 支持Spring的基于 @Configuration 的Java配置方式,或者XML配置方式
- 提供了用于操作ES的便捷工具类 ElasticsearchTemplate 。包括实现文档到POJO之间的自动智能映射。
- 利用Spring的数据转换服务实现的功能丰富的对象映射
- 基于注解的元数据映射方式,而且可扩展以支持更多不同的数据格式
- 根据持久层接口自动生成对应实现方法,无需人工编写基本操作代码(类似mybatis,根据接口自动得到实现)。当然,也支持人工定制查询
版本关系:
注意:如果使用Spring Boot 2.3.X版本,Spring Data Elasticsearch 使用的是4.0.X,在4版本后很多API与ElasticSearch 6.X兼容性不是很好,所以要将Spring Boot的版本控制在2.1.X-2.2.X。
配置 Spring Data Elasticsearch
官方文档:https://docs.spring.io/spring-data/elasticsearch/docs/3.0.1.RELEASE/reference/html/#reference
集成步骤:
-
依赖
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.3.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-elasticsearch</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.70</version> </dependency> </dependencies>
-
配置文件中添加 ES 地址
Spring Data Elasticsearch 已经配置好了各种SDE配置,并且注册了一个 ElasticsearchTemplate 以供使用
ElasticsearchTemplate 底层使用的不是 Elasticsearch 提供的 RestHighLevelClient,而是 TransportClient,并不采用 Http 协议通信,而是访问 elasticsearch 对外开放的 tcp 端口,所以这里设置的端口是:9300 ,而不是9200
spring: data: elasticsearch: # ES 集群名称 cluster-name: elasticsearch # 这里使用的是TransportClient 连接的是TCP端口 cluster-nodes: localhost:9300,localhost:9301,localhost:9302
索引库操作(ElasticsearchTemplate)
创建索引库
添加测试类,这里需要注意 SpringBoot 2.1.X 的测试类上需要添加 @RunWith(SpringRunner.class)
,注入ElasticsearchTemplate
@RunWith(SpringRunner.class)
@SpringBootTest
public class EsDemoApplicationTests {
@Autowired
private ElasticsearchTemplate template;
}
准备一个实体类
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Document(indexName = "goods",type = "_doc",shards = 3, replicas = 1)
public class Goods {
@Id
private Long id;
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String title; //标题
@Field(type = FieldType.Keyword)
private String category;// 分类
@Field(type = FieldType.Keyword)
private String brand; // 品牌
@Field(type = FieldType.Double)
private Double price; // 价格
@Field(type = FieldType.Keyword, index = false)
private String images; // 图片地址
}
用到的注解的说明:
- @Document:声明索引库配置
- indexName:索引库名称
- shards:分片数量,默认5
- replicas:副本数量,默认1
- @Id:声明实体类的id
- @Field:声明字段属性
- type:字段的数据类型
- analyzer:指定分词器类型
- index:是否创建索引
创建索引库
/**
* 创建索引
*/
@Test
void testCreateIndex(){
boolean b = template.createIndex(Goods.class);
System.out.println("结果为:"+b);
}
若不加@Document注解,直接运行会报错,如下所示:
加@Document注解再次运行测试,可以成功创建索引,看一下索引信息
创建映射
@Id 和 @Filed 注解用于配置映射关系,使用 putMapping() 方法即可创建映射:
/**
* 创建类型映射
*/
@Test
public void testCreateMapping() {
boolean b = template.putMapping(Goods.class);
System.out.println("结果为:" + b);
}
查看索引信息
新增文档
@Autowired
private ElasticsearchTemplate template;
@Test
public void add(){
Goods goods = new Goods(1L,"小米手机10Pro","手机","小米",5999.00,"/images/123.jpg");
IndexQuery query = new IndexQuery();
query.setObject(goods);
String index = template.index(query);
System.out.println(index);
}
ElasticsearchRepository 接口
ElasticsearchRepository
封装了基本的CRUD方法,可以通过继承 ElasticsearchRepository
来使用:
public interface GoodsRepository extends ElasticsearchRepository<Goods,Long> {
}
ElasticsearchRepository<T, ID>
中的T对应类型,ID对应主键类型。
新增和更新文档:save
新增单个文档
// 保存文档
@Test
public void testSave() {
Goods item = new Goods(6L, "小米手机10Pro", " 手机", "小米", 4699.00, "http://image.leyou.com/13123.jpg");
goodsRepository.save(item);
}
更新文档
// 更新文档
@Test
public void testUpdate() {
Goods item = new Goods(6L, "小米手机10Pro", " 手机", "小米", 4699.00, "http://image.leyou.com/13123.jpg");
goodsRepository.save(item);
}
批量新增
@Test
// 批量保存文档
public void addDocuments() {
// 准备文档数据:
List<Goods> list = new ArrayList<>();
list.add(new Goods(1L, "小米手机7", "手机", "小米", 3299.00, "/13123.jpg"));
list.add(new Goods(2L, "坚果手机R1", "手机", "锤子", 3699.00, "/13123.jpg"));
list.add(new Goods(3L, "华为META10", "手机", "华为", 4499.00, "/13123.jpg"));
list.add(new Goods(4L, "小米Mix2S", "手机", "小米", 4299.00, "/13123.jpg"));
list.add(new Goods(5L, "荣耀V10", "手机", "华为", 2799.00, "/13123.jpg"));
// 添加索引数据
goodsRepository.saveAll(list);
}
查看文档:findById
根据id查询
// 根据id查询
@Test
public void testQueryById(){
Optional<Goods> goodsOptional = goodsRepository.findById(3L);
System.out.println(goodsOptional.orElse(null));
}
删除文档:deleteById
根据id删除
// 删除文档
@Test
public void testDelete(){
goodsRepository.deleteById(6L);
}
查询所有:findAll
// 查询所有
@Test
public void testQueryAll(){
Iterable<Goods> list = goodsRepository.findAll();
list.forEach(System.out::println);
}
自定义查询
GoodsRepository提供的查询方法有限,但是它却提供了非常强大的自定义查询功能:只要遵循SpringData提供的语法,就可以任意定义方法声明:
public interface GoodsRepository extends ElasticsearchRepository<Goods, Long> {
/**
* 根据价格区间查询
* @param from 开始价格
* @param to 结束价格
* @return 符合条件的goods
*/
List<Goods> findByPriceBetween(double from, double to);
List<Goods> findByTitle(String title);
List<Goods> findByBrand(String brand);
}
使用实例:
// 范围查询
@Test
public void testConditionSearch(){
List<Goods> list = goodsRepository.findByPriceBetween(3000, 4000);
list.forEach(System.out::println);
}
@Test
public void testTitle(){
// List<Goods> goods = goodsRepository.findByBrand("米");
List<Goods> goods = goodsRepository.findByBrand("小米");
goods.forEach(System.out::println);
}
支持的一些语法示例:
Keyword | Sample | Elasticsearch Query String |
---|---|---|
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(Collectionnames) |
{"bool" : {"must" : {"bool" : {"should" : [ {"field" : {"name" : "?"}}, {"field" : {"name" : "?"}} ]}}}} |
NotIn |
findByNameNotIn(Collectionnames) |
{"bool" : {"must_not" : {"bool" : {"should" : {"field" : {"name" : "?"}}}}}} |
Near |
findByStoreNear |
Not Supported Yet ! |
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}}}} |
条件查询:matchQuery
@Test
public void testSearch(){
// QueryBuilder query = QueryBuilders.matchAllQuery();
QueryBuilder query = QueryBuilders.matchQuery("title","小米");
Iterable<Goods> goods = goodsRepository.search(query);
goods.forEach(System.out::println);
}
分页查询:search
@Test
public void testPage(){
QueryBuilder query = QueryBuilders.matchAllQuery();
// 设置分页 page是从0开始
PageRequest pageable = PageRequest.of(0, 2, Sort.by(Sort.Direction.DESC, "price"));
Page<Goods> goodsPage = goodsRepository.search(query, pageable);
System.out.println("总数:" + goodsPage.getTotalElements());
List<Goods> goods = goodsPage.getContent();
goods.forEach(System.out::println);
}
ElasticsearchTemplate 接口
SDE也支持使用 ElasticsearchTemplate 进行原生查询,而查询条件的构建是通过一个名为 NativeSearchQueryBuilder 的类来完成的,不过这个类的底层还是使用的原生API中的 QueryBuilders 、 AggregationBuilders 、 HighlightBuilders 等工具。
高亮
要支持高亮,必须自定义结果处理器来实现
import com.itheima.es.entity.Goods;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.aggregations.Aggregations;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.core.SearchResultMapper;
import org.springframework.data.elasticsearch.core.aggregation.AggregatedPage;
import org.springframework.data.elasticsearch.core.aggregation.impl.AggregatedPageImpl;
import java.util.ArrayList;
import java.util.List;
public class GoodsSearchResultMapper implements SearchResultMapper {
@Override
public <T> AggregatedPage<T> mapResults(SearchResponse response, Class<T> clazz, Pageable pageable) {
SearchHits searchHits = response.getHits();
long total = searchHits.getTotalHits();
float maxScore = searchHits.getMaxScore();
// 定义content
List<T> content = new ArrayList<>();
SearchHit[] hits = searchHits.getHits();
// 遍历文档
for (SearchHit hit : hits) {
// 获取json格式数据
String sourceAsString = hit.getSourceAsString();
// 转换称为对象
Goods goods = JSON.parseObject(sourceAsString, Goods.class);
// 解析高亮字段
String title = hit.getHighlightFields().get("title").fragments()[0].string();
// 替换原有的title
goods.setTitle(title);
content.add((T) goods);
}
Aggregations aggregations = response.getAggregations();
String scrollId = response.getScrollId();
return new AggregatedPageImpl(content,pageable,total,aggregations,scrollId,maxScore);
}
}
查询时需要传入自定义结果处理器
/**
* 查询结果高亮处理
*/
@Test
public void testHighlight() {
// 构建查询条件
QueryBuilder queryBuilder = QueryBuilders.matchQuery("title", "小米");
// 定义高亮条件
HighlightBuilder.Field field = new HighlightBuilder.Field("title")
.preTags("<em style='color:red'>")
.postTags("</em>");
// 构建查询条件并设置高亮
SearchQuery query = new NativeSearchQueryBuilder()
.withQuery(queryBuilder)
.withHighlightFields(field)
.build();
AggregatedPage<Goods> aggregatedPage = template.queryForPage(query, Goods.class, new GoodsSearchResultMapper());
List<Goods> goods = aggregatedPage.getContent();
goods.forEach(System.out::println);
}
查看结果:
聚合
示例:
/**
* 聚合
*/
@Test
void testAgg() {
// 针对品牌字段做分组
AbstractAggregationBuilder agg = AggregationBuilders.terms("brandAgg").field("brand");
// 添加子聚合来实现平均值的计算
agg.subAggregation(AggregationBuilders.avg("priceAvg").field("price"));
SearchQuery query = new NativeSearchQueryBuilder()
.withQuery(QueryBuilders.matchAllQuery())
.addAggregation(agg)
.build();
AggregatedPage<Goods> aggregatedPage = template.queryForPage(query, Goods.class);
// 获取到品牌的聚合
Terms brandAgg = (Terms) aggregatedPage.getAggregation("brandAgg");
for (Terms.Bucket bucket : brandAgg.getBuckets()) {
System.out.println("品牌:" + bucket.getKeyAsString());
System.out.println("数量:" + bucket.getDocCount());
Avg priceAvg = bucket.getAggregations().get("priceAvg");
System.out.println("均价:" + priceAvg.getValue());
}
}
结果:
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/124050.html