ElasticSearch的使用
概念
ElasticSearch是一个基于Lucene的搜索搜索引擎。它提供了一个分布式多用户能力的全文搜索引擎,基于RESTful web接口。Elasticsearch是用Java开发的,并作为Apache许可条款下的开放源码发布,是当前流行的企业级搜索引擎。设计用于云计算中,能够达到实时搜索,稳定,可靠,快速,安装使用方便。
同类产品 solr
直奔主题,整体思路
使用目的: 作为门户网站的分布式项目,其中的搜索栏,如果使用模糊搜索,不仅效率低,且对数据库造成较大的压力,千万级的数据无法在短时间内搜索,用户体验不好,所以必须使用ElasticSearch来帮助我们开发搜索模块
思路如下 图所示:
开发商品模块接口,目的: 通过数据库查询,获取商品信息,届时通过OpenFeign声明接口,给search模块微服务调用,用来初始化ES中的index索引
1. 商品微服务的接口开发
a. 正常接口开发: 使用的是MybatisPlus去连接Mysql的数据库,在对应的表,mall_goods , mall_goods_brand , mall_goods_cat 通过三表的连查去查找所有审核通过的商品信息
b. 使用OpenFeign的接口声明,去声明接口,给其他需要的微服务调用
goods微服务中的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</depedency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</depedency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</depedency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</depedency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</depedency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</depedency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.0</version>
</depedency>
<dependency>
<groupId>com.fengmi</groupId>
<artifactId>fengmi-entity</artifactId>
<version>1.0-SNAPSHOT</version>
</depedency>
goods微服务中的mapper层
public interface MallGoodsMapper extends BaseMapper<MallGoods> {
public List<MallGoods> findAllGoodsAudited();
}
goods微服务中的mapper层映射文件
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.fengmi.mapper.MallGoodsMapper">
<resultMap id="findAllGoodsAuditedMap" type="mallgoods">
<id column="spu_id" property="spuId"/>
<result column="goods_name" property="goodsName"/>
<result column="price" property="price"/>
<result column="album_pics" property="albumPics"/>
<association property="mallGoodsBrand" javaType="mallGoodsBrand">
<id column="brandId" property="id"/>
<result column="brandName" property="name"/>
</association>
<association property="cat1" javaType="MallGoodsCat">
<id column="cat1Id" property="id"/>
<result column="cat1Name" property="name"/>
</association>
<association property="cat2" javaType="MallGoodsCat">
<id column="cat2Id" property="id"/>
<result column="cat2Name" property="name"/>
</association>
<association property="cat3" javaType="MallGoodsCat">
<id column="cat3Id" property="id"/>
<result column="cat3Name" property="name"/>
</association>
</resultMap>
<select id="findAllGoodsAudited" resultMap="findAllGoodsAuditedMap">
SELECT
goods.spu_id,
goods.goods_name,
goods.price,
goods.album_pics,
goods.brand_id AS brandId,
brand.name AS brandName,
goods.category1_id AS cat1Id,
cat1.name AS cat1Name,
goods.category2_id AS cat2Id,
cat2.name AS cat2Name,
goods.category3_id AS cat3Id,
cat3.name AS cat3Name
FROM mall_goods goods
LEFT JOIN mall_goods_brand brand ON goods.brand_id = brand.id
LEFT JOIN mall_goods_cat cat1 ON goods.category1_id = cat1.id
LEFT JOIN mall_goods_cat cat2 ON goods.category2_id = cat2.id
LEFT JOIN mall_goods_cat cat3 ON goods.category3_id = cat3.id
WHERE goods.audit_status = 1;
</select>
</mapper>
goods微服务中的service层
public interface IMallGoodsService extends IService<MallGoods> {
public List<MallGoods> findAllGoodsInfo();
}
@Service
public class MallGoodsServiceImpl extends ServiceImpl<MallGoodsMapper, MallGoods> implements IMallGoodsService {
@Override
public List<MallGoods> findAllGoodsInfo() {
return this.baseMapper.findAllGoodsAudited();
}
}
goods微服务中的controller层
@RestController
@RequestMapping("/goods")
public class MallGoodsController {
@Autowired
private IMallGoodsService goodsService;
@GetMapping("findAllGoodsInfo")
public List<MallGoods> findAllGoodsInfo() {
return goodsService.findAllGoodsInfo();
}
}
goods微服务进行OpenFeign的接口声明
这里需要创建一个模块,专门用于OpenFeign的接口声明,注意分包!!!
import com.fengmi.goods.MallGoods;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import java.util.List;
@FeignClient("fengmi-goods")
@RequestMapping("goods")
public interface GoodsApi {
@GetMapping("findAllGoodsInfo")
public List<MallGoods> findAllGoodsInfo();
}
2. search模块微服务的接口开发
这一块微服务主要分两个步骤:
a. 初始化elasticsearch的索引和域 => 这需要查询到数据库中的数据(调用以上接口即可),通过ElasticsearchRestTemplate中的 save()方法去初始化
b. 查询操作 => 需要绑定查询的条件,如bool复合查询,highlight高亮显示,aggs分组,filters过滤等,这需要熟练使用ElasticSearchRestTemplate的Api
2.1 开发所需依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>com.fengmi</groupId>
<artifactId>fengmi-entity</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.fengmi</groupId>
<artifactId>fengmi-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
2.2 引导类中需要扫描
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableDiscoveryClient
@EnableFeignClients(basePackages = {"com.fengmi.api.goods"})
public class SearchApp {
public static void main(String[] args) {
SpringApplication.run(SearchApp.class,args);
}
}
2.3 初始化ElasticSearch的接口
service 的接口
public interface ISearchService {
public ResultVO initEs();
public PageResultVO<ESGoods> searchAndPageByES(SearchDTO searchDTO);
}
service的实现
明确
所谓的elasticsearch初始化主要分两个步骤:
- 删除之前可能存在的索引,对应Api => ElasticSearchRestTemplate.deleteIndex(对应的实体Class),之后再通过设计的实体去创建索引
- 查询数据库中的数据,然后遍历转换为设置的索引实体类,之后使用save()方法导入数据
至此,索引,域创建完毕,数据导入完毕
实体类的设计对于其中字段要明确三个维度:
- 要不要分词,通过 type 去确认字段的类型,如果type确认字段是 TEXT 那么就可以通过 analyzer去确定分词的类型是 ik_max_word 或者 ik_smart ;如果字段的类型设置为了Keyword,表示这个字段是不使用分词的,就是其本身
- 要不要建立索引,通过 index 属性来设置,一般的字段都是需要建立索引的,但如图片地址等一些 不会用到的搜索条件,我们可以不建立索引
- 要不要储存,通过 store 属性来设置,一般来说我们设计的字段都是需要储存的
package com.fengmi.entity;
import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.DateFormat;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
import java.util.Date;
@Data
@Document(indexName = "es-goods", shards = 1, replicas = 0)
public class ESGoods {
@Id
private Long spuId;
@Field(type = FieldType.Text, analyzer = "ik_max_word",store=true)
private String goodsName;
@Field(type=FieldType.Long,index=true,store=true)
private Long brandId;
@Field(type = FieldType.Keyword,index=true,store=true)
private String brandName;
@Field(type=FieldType.Long,index=true,store=true)
private Long cid1id;
@Field(type = FieldType.Keyword,index=true,store=true)
private String cat1name;
@Field(type=FieldType.Long,index=true,store=true)
private Long cid2id;
@Field(type = FieldType.Keyword,index=true,store=true)
private String cat2name;
@Field(type=FieldType.Long,index=true,store=true)
private Long cid3id;
@Field(type = FieldType.Keyword,index=true,store=true)
private String cat3name;
@Field(type=FieldType.Date,index=true,store=true,format = DateFormat.date_time)
private Date createTime;
@Field(type=FieldType.Double,index=true,store=true)
private Double price;
@Field(type = FieldType.Keyword,index = false,store=true)
private String smallPic;
}
- 实体类设计完毕,下面开始做es初始化的工作,代码如下:
@Service
public class SearchService implements ISearchService {
@Autowired
private GoodsApi goodsApi;
@Autowired
private ElasticsearchRestTemplate restTemplate;
@Override
public ResultVO initEs() {
restTemplate.deleteIndex(ESGoods.class);
restTemplate.createIndex(ESGoods.class);
restTemplate.putMapping(ESGoods.class);
List<MallGoods> allGoodsInfo = goodsApi.findAllGoodsInfo();
if (allGoodsInfo == null || allGoodsInfo.size() < 0) {
return new ResultVO(false, "初始化失败");
}
List<ESGoods> goodsList = allGoodsInfo.stream().map(spu -> {
ESGoods esGoods = new ESGoods();
esGoods.setSpuId(spu.getSpuId());
esGoods.setBrandId(spu.getMallGoodsBrand().getId());
esGoods.setBrandName(spu.getMallGoodsBrand().getName());
esGoods.setCid1id(spu.getCat1().getId());
esGoods.setCat1name(spu.getCat1().getName());
esGoods.setCid2id(spu.getCat2().getId());
esGoods.setCat2name(spu.getCat2().getName());
esGoods.setCid3id(spu.getCat3().getId());
esGoods.setCat3name(spu.getCat3().getName());
esGoods.setCreateTime(new Date());
esGoods.setGoodsName(spu.getGoodsName());
esGoods.setPrice(spu.getPrice().doubleValue());
String albumPics = spu.getAlbumPics();
if (!StringUtils.isEmpty(albumPics)) {
String[] split = albumPics.split(",");
if (split != null && split.length > 0) {
esGoods.setSmallPic(spu.getAlbumPics());
}
}
return esGoods;
}).collect(Collectors.toList());
restTemplate.save(goodsList);
return new ResultVO(true, "elasticsearch初始化成功");
}
}
提供初始化的controller接口
@RestController
@RequestMapping("search")
public class SearchController {
@Autowired
private ISearchService searchService;
@RequestMapping("initES")
public ResultVO initES() {
return searchService.initEs();
}
}
至此,初始化的工作进行完毕
2.4 使用ElasticSearch查询接口开发
接下来就是重头戏了,使用elasticsearch来作为查询的工作
2.4.1 思路
- 先做基本查询的功能,需要多个域匹配,所以我们采用复合查询bool中的should来操作
- 基本查询完成之后进行分页的操作,可以通过设置分页来完成
- 对输入的关键字进行设置,采用高亮设置,以实现查询
- 分组,聚合查询,完成品牌和种类列表的实现
- 过滤,这里必须使用过滤,对上述查询的结果进行过滤的操作不会影响分数!
- 排序,对结果集添加排序的操作,这里实现价格的排序,其他方法是一致的
明确要求:
- 查询时,首先是要匹配几个域的,分别是: 商品的名称goodsName,商品的品牌brandName,商品三级分类的名称cat3name,而这几个域只要有一个匹配上就展示,这样能搜索到的商品就更多
- 域的分词,goodsName采用 ik_max_word分词,这样也能搜索到更多的产品,以提升用户的购买率,品牌和商品三级分类则可以不需要分词
- 以上要求组合起来可以得到:需要使用复合查询,should条件拼接
- 顺带可以将分页也处理了,只需添加分页设置即可
2.4.2 基本分页查询
public PageResultVO<ESGoods> searchAndPageByES(SearchDTO searchDTO) {
if (searchDTO == null) {
return new PageResultVO<>(false, "参数不合法");
}
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
if (!StringUtils.isEmpty(searchDTO.getKeyword())) {
MatchQueryBuilder goodsName = QueryBuilders.matchQuery("goodsName", searchDTO.getKeyword());
TermQueryBuilder brandName = QueryBuilders.termQuery("brandName", searchDTO.getKeyword());
TermQueryBuilder cat3name = QueryBuilders.termQuery("cat3name", searchDTO.getKeyword());
boolQueryBuilder.should(goodsName).should(brandName).should(cat3name);
}
if (searchDTO.getPage() <= 0 || searchDTO.getSize() <= 0) {
searchDTO.setPage(1);
searchDTO.setSize(5);
}
PageRequest pageRequest = PageRequest.of(searchDTO.getPage() - 1, searchDTO.getSize());
NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder()
.withQuery(boolQueryBuilder)
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class SearchDTO {
private String keyword;
private Integer page = 1;
private Integer size = 5;
private String brandNameFilter;
private String cat3NameFilter;
private String sortField;
}
- 响应类:ResultVO 和PageResultVO
@Data
@AllArgsConstructor
@NoArgsConstructor
@RequiredArgsConstructor
public class ResultVO {
@NonNull
private boolean success;
@NonNull
private String msg;
private Object data ;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
@RequiredArgsConstructor
public class PageResultVO<T> {
private Long total;
private Integer pages;
private List<T> data;
@NonNull
private boolean success;
@NonNull
private String msg;
}
2.4.3 高亮设置
对于查询的关键字keyword可以设置高亮显示,这样的用户体验会更好
@Override
public PageResultVO<ESGoods> searchAndPageByES(SearchDTO searchDTO) {
if (searchDTO == null) {
return new PageResultVO<>(false, "参数不合法");
}
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
if (!StringUtils.isEmpty(searchDTO.getKeyword())) {
MatchQueryBuilder goodsName = QueryBuilders.matchQuery("goodsName", searchDTO.getKeyword());
TermQueryBuilder brandName = QueryBuilders.termQuery("brandName", searchDTO.getKeyword());
TermQueryBuilder cat3name = QueryBuilders.termQuery("cat3name", searchDTO.getKeyword());
boolQueryBuilder.should(goodsName).should(brandName).should(cat3name);
}
if (searchDTO.getPage() <= 0 || searchDTO.getSize() <= 0) {
searchDTO.setPage(1);
searchDTO.setSize(5);
}
PageRequest pageRequest = PageRequest.of(searchDTO.getPage() - 1, searchDTO.getSize());
NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder()
.withQuery(boolQueryBuilder)
.withPageable(pageRequest)
.withHighlightBuilder(getHighlightBuilder("goodsName"))
SearchHits<ESGoods> search = restTemplate.search(nativeSearchQueryBuilder.build(), ESGoods.class);
PageResult pageResultVO = new PageResult();
pageResultVO.setTotal(search.getTotalHits());
List<SearchHit<ESGoods>> searchHits = search.getSearchHits();
List<ESGoods> esGoodsList = searchHits.stream().map(hit -> {
ESGoods esGoods = hit.getContent();
Map<String, List<String>> highlightFields = hit.getHighlightFields();
highlightFields.forEach((k, v) -> {
if (v != null && v.size() > 0) {
if ("goodsName".equals(k)) {
esGoods.setGoodsName(v.get(0));
}
}
});
return esGoods;
}).collect(Collectors.toList());
pageResultVO.setData(esGoodsList);
return pageResultVO;
}
private HighlightBuilder getHighlightBuilder(String... fields) {
HighlightBuilder highlightBuilder = new HighlightBuilder();
for (String field : fields) {
highlightBuilder.field(field);
}
highlightBuilder.requireFieldMatch(false);
highlightBuilder.preTags("<span style=\"color:red\">");
highlightBuilder.postTags("</span>");
highlightBuilder.fragmentSize(800000);
highlightBuilder.numOfFragments(0);
return highlightBuilder;
}
}
2.4.4 聚合统计品牌和分类信息
回忆一下在kibana中我们是怎么做聚合的,是与query同级,使用aggs来聚合,设置聚合组名,聚合桶只能使用term不分词,设置field字段对应的域
那么在代码中使用聚合也是在与查询同级
public PageResultVO<ESGoods> searchAndPageByES(SearchDTO searchDTO) {
if (searchDTO == null) {
return new PageResultVO<>(false, "参数不合法");
}
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
if (!StringUtils.isEmpty(searchDTO.getKeyword())) {
MatchQueryBuilder goodsName = QueryBuilders.matchQuery("goodsName", searchDTO.getKeyword());
TermQueryBuilder brandName = QueryBuilders.termQuery("brandName", searchDTO.getKeyword());
TermQueryBuilder cat3name = QueryBuilders.termQuery("cat3name", searchDTO.getKeyword());
boolQueryBuilder.should(goodsName).should(brandName).should(cat3name);
}
if (searchDTO.getPage() <= 0 || searchDTO.getSize() <= 0) {
searchDTO.setPage(1);
searchDTO.setSize(5);
}
PageRequest pageRequest = PageRequest.of(searchDTO.getPage() - 1, searchDTO.getSize());
TermsAggregationBuilder brandNameTermsAggregationBuilder = AggregationBuilders.terms("brandAgg").field("brandName").size(30);
TermsAggregationBuilder cat3nameTermsAggregationBuilder = AggregationBuilders.terms("cat3Agg").field("cat3name").size(30);
NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder()
.withQuery(boolQueryBuilder)
.withPageable(pageRequest)
.withHighlightBuilder(getHighlightBuilder("goodsName"))
.addAggregation(brandNameTermsAggregationBuilder)
.addAggregation(cat3nameTermsAggregationBuilder);
SearchHits<ESGoods> search = restTemplate.search(nativeSearchQueryBuilder.build(), ESGoods.class);
PageResult pageResultVO = new PageResult();
pageResultVO.setTotal(search.getTotalHits());
List<SearchHit<ESGoods>> searchHits = search.getSearchHits();
List<ESGoods> esGoodsList = searchHits.stream().map(hit -> {
ESGoods esGoods = hit.getContent();
Map<String, List<String>> highlightFields = hit.getHighlightFields();
highlightFields.forEach((k, v) -> {
if (v != null && v.size() > 0) {
if ("goodsName".equals(k)) {
esGoods.setGoodsName(v.get(0));
}
}
});
return esGoods;
}).collect(Collectors.toList());
Aggregations aggregations = search.getAggregations();
Terms brandName = aggregations.get("brandAgg");
Terms cat3name = aggregations.get("cat3Agg");
List<? extends Terms.Bucket> brandNameBuckets = brandName.getBuckets();
List<? extends Terms.Bucket> cat3nameBuckets = cat3name.getBuckets();
List<String> brandNameList = brandNameBuckets.stream().map(item -> (String) item.getKey()).collect(Collectors.toList());
pageResultVO.setBrandNameList(brandNameList);
List<String> cat3NameList = cat3nameBuckets.stream().map(item -> (String) item.getKey()).collect(Collectors.toList());
pageResultVO.setCat3NameList(cat3NameList);
pageResultVO.setData(esGoodsList);
return pageResultVO;
}
private HighlightBuilder getHighlightBuilder(String... fields) {
HighlightBuilder highlightBuilder = new HighlightBuilder();
for (String field : fields) {
highlightBuilder.field(field);
}
highlightBuilder.requireFieldMatch(false);
highlightBuilder.preTags("<span style=\"color:red\">");
highlightBuilder.postTags("</span>");
highlightBuilder.fragmentSize(800000);
highlightBuilder.numOfFragments(0);
return highlightBuilder;
}
2.4.5 过滤查询,通过聚合出的品牌名称和分类名称过滤
为什么要使用过滤而非在复合条件中添加?
关键字: 分数
使用复合条件查询对结果的分数会有影响,就会影响到排序,页面图片的改变就会变大,这里我们希望是不影响结果的分数,那么只能使用过滤的方式来实现
回忆一下在kibana中怎么使用过滤的:
过滤在bool复合查询中,使用filters与bool同级,使用term或者terms来设置过滤的字段,该字段是不分词的
所以在代码中使用过滤也是在bool中组合,而非与查询同级
public PageResultVO<ESGoods> searchAndPageByES(SearchDTO searchDTO) {
if (searchDTO == null) {
return new PageResultVO<>(false, "参数不合法");
}
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
if (!StringUtils.isEmpty(searchDTO.getKeyword())) {
MatchQueryBuilder goodsName = QueryBuilders.matchQuery("goodsName", searchDTO.getKeyword());
TermQueryBuilder brandName = QueryBuilders.termQuery("brandName", searchDTO.getKeyword());
TermQueryBuilder cat3name = QueryBuilders.termQuery("cat3name", searchDTO.getKeyword());
boolQueryBuilder.should(goodsName).should(brandName).should(cat3name);
}
if (searchDTO.getPage() <= 0 || searchDTO.getSize() <= 0) {
searchDTO.setPage(1);
searchDTO.setSize(5);
}
PageRequest pageRequest = PageRequest.of(searchDTO.getPage() - 1, searchDTO.getSize());
TermsAggregationBuilder brandNameTermsAggregationBuilder = AggregationBuilders.terms("brandAgg").field("brandName").size(30);
TermsAggregationBuilder cat3nameTermsAggregationBuilder = AggregationBuilders.terms("cat3Agg").field("cat3name").size(30);
if (!StringUtils.isEmpty(searchDTO.getBrandNameFilter())) {
TermQueryBuilder brandNameTermQueryBuilder = QueryBuilders.termQuery("brandName", searchDTO.getBrandNameFilter());
boolQueryBuilder.filter(brandNameTermQueryBuilder);
}
if (!StringUtils.isEmpty(searchDTO.getCat3NameFilter())) {
TermQueryBuilder cat3namebrandNameTermQueryBuilder = QueryBuilders.termQuery("cat3name", searchDTO.getCat3NameFilter());
boolQueryBuilder.filter(cat3namebrandNameTermQueryBuilder);
}
NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder()
.withQuery(boolQueryBuilder)
.withPageable(pageRequest)
.withHighlightBuilder(getHighlightBuilder("goodsName"))
.addAggregation(brandNameTermsAggregationBuilder)
.addAggregation(cat3nameTermsAggregationBuilder);
SearchHits<ESGoods> search = restTemplate.search(nativeSearchQueryBuilder.build(), ESGoods.class);
PageResult pageResultVO = new PageResult();
pageResultVO.setTotal(search.getTotalHits());
List<SearchHit<ESGoods>> searchHits = search.getSearchHits();
List<ESGoods> esGoodsList = searchHits.stream().map(hit -> {
ESGoods esGoods = hit.getContent();
Map<String, List<String>> highlightFields = hit.getHighlightFields();
highlightFields.forEach((k, v) -> {
if (v != null && v.size() > 0) {
if ("goodsName".equals(k)) {
esGoods.setGoodsName(v.get(0));
}
}
});
return esGoods;
}).collect(Collectors.toList());
Aggregations aggregations = search.getAggregations();
Terms brandName = aggregations.get("brandAgg");
Terms cat3name = aggregations.get("cat3Agg");
List<? extends Terms.Bucket> brandNameBuckets = brandName.getBuckets();
List<? extends Terms.Bucket> cat3nameBuckets = cat3name.getBuckets();
List<String> brandNameList = brandNameBuckets.stream().map(item -> (String) item.getKey()).collect(Collectors.toList());
pageResultVO.setBrandNameList(brandNameList);
List<String> cat3NameList = cat3nameBuckets.stream().map(item -> (String) item.getKey()).collect(Collectors.toList());
pageResultVO.setCat3NameList(cat3NameList);
pageResultVO.setData(esGoodsList);
return pageResultVO;
}
private HighlightBuilder getHighlightBuilder(String... fields) {
HighlightBuilder highlightBuilder = new HighlightBuilder();
for (String field : fields) {
highlightBuilder.field(field);
}
highlightBuilder.requireFieldMatch(false);
highlightBuilder.preTags("<span style=\"color:red\">");
highlightBuilder.postTags("</span>");
highlightBuilder.fragmentSize(800000);
highlightBuilder.numOfFragments(0);
return highlightBuilder;
}
2.4.6 设置排序
对于排序,我们可以通过页面原型发现,这里需要做的是对商品价格的排序
在查询的DTO中插入排序的字段String sortFilter,通过对传入的字段是asc还是desc来决定排序方式
这里需要注意的是:可能前端未传入排序方式,那么此时就不需要在查询中添加排序
回忆一下在kibana中我们是怎么使用排序的:
排序sort,和aggs聚合一样与query同级,设置排序的字段,order中设置排序方式,asc为升序,desc为降序
所以在代码中的查询中添加排序条件
- 由此给出全部的查询在kibana中的查询语句,供参考和对比
GET es-goods/_search
{
"query": {
"bool": {
"should": [
{
"match": {
"goodsName": "抢购"
}
},
{
"term": {
"brandName": {
"value": "良品铺子"
}
}
},
{
"term": {
"cat3name": {
"value": ""
}
}
}
],
"filter": [
{
"term": {
"brandName": "百草味"
}
}
]
}
},
"aggs": {
"brandName_agg": {
"terms": {
"field": "brandName",
"size": 10
}
},
"cat3Name_agg":{
"terms": {
"field": "cat3name",
"size": 10
}
}
},
"sort": [
{
"price": {
"order": "desc"
}
}
]
}
@Override
public PageResultVO<ESGoods> searchAndPageByES(SearchDTO searchDTO) {
if (searchDTO == null) {
return new PageResultVO<>(false, "参数不合法");
}
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
if (!StringUtils.isEmpty(searchDTO.getKeyword())) {
MatchQueryBuilder goodsName = QueryBuilders.matchQuery("goodsName", searchDTO.getKeyword());
TermQueryBuilder brandName = QueryBuilders.termQuery("brandName", searchDTO.getKeyword());
TermQueryBuilder cat3name = QueryBuilders.termQuery("cat3name", searchDTO.getKeyword());
boolQueryBuilder.should(goodsName).should(brandName).should(cat3name);
}
if (searchDTO.getPage() <= 0 || searchDTO.getSize() <= 0) {
searchDTO.setPage(1);
searchDTO.setSize(5);
}
PageRequest pageRequest = PageRequest.of(searchDTO.getPage() - 1, searchDTO.getSize());
TermsAggregationBuilder brandNameTermsAggregationBuilder = AggregationBuilders.terms("brandAgg").field("brandName").size(30);
TermsAggregationBuilder cat3nameTermsAggregationBuilder = AggregationBuilders.terms("cat3Agg").field("cat3name").size(30);
if (!StringUtils.isEmpty(searchDTO.getBrandNameFilter())) {
TermQueryBuilder brandNameTermQueryBuilder = QueryBuilders.termQuery("brandName", searchDTO.getBrandNameFilter());
boolQueryBuilder.filter(brandNameTermQueryBuilder);
}
if (!StringUtils.isEmpty(searchDTO.getCat3NameFilter())) {
TermQueryBuilder cat3namebrandNameTermQueryBuilder = QueryBuilders.termQuery("cat3name", searchDTO.getCat3NameFilter());
boolQueryBuilder.filter(cat3namebrandNameTermQueryBuilder);
}
FieldSortBuilder price = null;
if (!StringUtils.isEmpty(searchDTO.getSortField()) && "desc".equals(searchDTO.getSortField())) {
price = SortBuilders.fieldSort("price").order(SortOrder.DESC);
}
if (!StringUtils.isEmpty(searchDTO.getSortField()) && "asc".equals(searchDTO.getSortField())) {
price = SortBuilders.fieldSort("price").order(SortOrder.ASC);
}
NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder()
.withQuery(boolQueryBuilder)
.withPageable(pageRequest)
.withHighlightBuilder(getHighlightBuilder("goodsName"))
.addAggregation(brandNameTermsAggregationBuilder)
.addAggregation(cat3nameTermsAggregationBuilder);
if (price != null){
nativeSearchQueryBuilder.withSort(price);
}
SearchHits<ESGoods> search = restTemplate.search(nativeSearchQueryBuilder.build(), ESGoods.class);
PageResult pageResultVO = new PageResult();
pageResultVO.setTotal(search.getTotalHits());
List<SearchHit<ESGoods>> searchHits = search.getSearchHits();
List<ESGoods> esGoodsList = searchHits.stream().map(hit -> {
ESGoods esGoods = hit.getContent();
Map<String, List<String>> highlightFields = hit.getHighlightFields();
highlightFields.forEach((k, v) -> {
if (v != null && v.size() > 0) {
if ("goodsName".equals(k)) {
esGoods.setGoodsName(v.get(0));
}
}
});
return esGoods;
}).collect(Collectors.toList());
Aggregations aggregations = search.getAggregations();
Terms brandName = aggregations.get("brandAgg");
Terms cat3name = aggregations.get("cat3Agg");
List<? extends Terms.Bucket> brandNameBuckets = brandName.getBuckets();
List<? extends Terms.Bucket> cat3nameBuckets = cat3name.getBuckets();
List<String> brandNameList = brandNameBuckets.stream().map(item -> (String) item.getKey()).collect(Collectors.toList());
pageResultVO.setBrandNameList(brandNameList);
List<String> cat3NameList = cat3nameBuckets.stream().map(item -> (String) item.getKey()).collect(Collectors.toList());
pageResultVO.setCat3NameList(cat3NameList);
pageResultVO.setData(esGoodsList);
return pageResultVO;
}
private HighlightBuilder getHighlightBuilder(String... fields) {
HighlightBuilder highlightBuilder = new HighlightBuilder();
for (String field : fields) {
highlightBuilder.field(field);
}
highlightBuilder.requireFieldMatch(false);
highlightBuilder.preTags("<span style=\"color:red\">");
highlightBuilder.postTags("</span>");
highlightBuilder.fragmentSize(800000);
highlightBuilder.numOfFragments(0);
return highlightBuilder;
}
至此,一个ElasticSearch的基本使用完成,你学废了么?
|