直奔主题,整体思路ElasticSearch是一个基于Lucene的搜索搜索引擎。它提供了一个分布式多用户能力的全文搜索引擎,基于RESTful web接口。Elasticsearch是用Java开发的,并作为Apache许可条款下的开放源码发布,是当前流行的企业级搜索引擎。设计用于云计算中,能够达到实时搜索,稳定,可靠,快速,安装使用方便。
同类产品 solr
使用目的: 作为门户网站的分布式项目,其中的搜索栏,如果使用模糊搜索,不仅效率低,且对数据库造成较大的压力,千万级的数据无法在短时间内搜索,用户体验不好,所以必须使用ElasticSearch来帮助我们开发搜索模块
思路如下 图所示:
1. 商品微服务的接口开发开发商品模块接口,目的: 通过数据库查询,获取商品信息,届时通过OpenFeign声明接口,给search模块微服务调用,用来初始化ES中的index索引
goods微服务中的依赖a. 正常接口开发: 使用的是MybatisPlus去连接Mysql的数据库,在对应的表,mall_goods , mall_goods_brand , mall_goods_cat 通过三表的连查去查找所有审核通过的商品信息
b. 使用OpenFeign的接口声明,去声明接口,给其他需要的微服务调用
goods微服务中的mapper层org.springframework.boot spring-boot-starter-web com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery com.alibaba.cloud spring-cloud-starter-alibaba-nacos-config org.springframework.boot spring-boot-starter-actuator com.alibaba druid-spring-boot-starter 1.1.10 mysql mysql-connector-java com.baomidou mybatis-plus-boot-starter 3.4.0 com.fengmi fengmi-entity 1.0-SNAPSHOT
public interface MallGoodsMapper extends baseMappergoods微服务中的mapper层映射文件{ // 查询所有审核通过的商品 public List findAllGoodsAudited(); }
goods微服务中的service层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;
public interface IMallGoodsService extends IService{ // 查询所有审核通过上线的商品信息 public List findAllGoodsInfo(); }
@Service public class MallGoodsServiceImpl extends ServiceImplgoods微服务中的controller层implements IMallGoodsService { @Override public List findAllGoodsInfo() { return this.baseMapper.findAllGoodsAudited(); } }
@RestController
@RequestMapping("/goods")
public class MallGoodsController {
@Autowired
private IMallGoodsService goodsService;
@GetMapping("findAllGoodsInfo")
public List 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 findAllGoodsInfo();
}
2. search模块微服务的接口开发
2.1 开发所需依赖这一块微服务主要分两个步骤:
a. 初始化elasticsearch的索引和域 => 这需要查询到数据库中的数据(调用以上接口即可),通过ElasticsearchRestTemplate中的 save()方法去初始化
b. 查询操作 => 需要绑定查询的条件,如bool复合查询,highlight高亮显示,aggs分组,filters过滤等,这需要熟练使用ElasticSearchRestTemplate的Api
2.2 引导类中需要扫描org.springframework.boot spring-boot-starter-data-elasticsearch com.alibaba.cloud spring-cloud-starter-alibaba-nacos-config com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-actuator com.fengmi fengmi-entity 1.0-SNAPSHOT com.fengmi fengmi-api 1.0-SNAPSHOT
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableDiscoveryClient
@EnableFeignClients(basePackages = {"com.fengmi.api.goods"}) // 扫描才能调用openfeign的接口哦
public class SearchApp {
public static void main(String[] args) {
SpringApplication.run(SearchApp.class,args);
}
}
2.3 初始化ElasticSearch的接口
service 的接口
public interface ISearchService {
// 初始化elasticsearch 看这里!!!!
public ResultVO initEs();
// 使用es查询并分页
public PageResultVO 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; // 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; // 品牌id
@Field(type = FieldType.Keyword,index=true,store=true)
private String brandName; //品牌名称
@Field(type=FieldType.Long,index=true,store=true)
private Long cid1id; // 1级分类id
@Field(type = FieldType.Keyword,index=true,store=true)
private String cat1name; // 1级分类名称
@Field(type=FieldType.Long,index=true,store=true)
private Long cid2id; // 2级分类id
@Field(type = FieldType.Keyword,index=true,store=true)
private String cat2name; // 2级分类名称
@Field(type=FieldType.Long,index=true,store=true)
private Long cid3id; // 3级分类id
@Field(type = FieldType.Keyword,index=true,store=true)
private String cat3name; // 3级分类名称
@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; // 价格,spu默认的sku的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);
// 远程调用goodsApi 查询商品信息
List allGoodsInfo = goodsApi.findAllGoodsInfo();
if (allGoodsInfo == null || allGoodsInfo.size() < 0) {
return new ResultVO(false, "初始化失败");
}
// 遍历 将实体类的映射设置到索引库
List goodsList = allGoodsInfo.stream().map(spu -> {
ESGoods esGoods = new ESGoods();
// id
esGoods.setSpuId(spu.getSpuId());
// brand 品牌信息
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());
// 保存到es中
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查询接口开发至此,初始化的工作进行完毕
2.4.1 思路接下来就是重头戏了,使用elasticsearch来作为查询的工作
- 先做基本查询的功能,需要多个域匹配,所以我们采用复合查询bool中的should来操作
- 基本查询完成之后进行分页的操作,可以通过设置分页来完成
- 对输入的关键字进行设置,采用高亮设置,以实现查询
- 分组,聚合查询,完成品牌和种类列表的实现
- 过滤,这里必须使用过滤,对上述查询的结果进行过滤的操作不会影响分数!
- 排序,对结果集添加排序的操作,这里实现价格的排序,其他方法是一致的
- 先看看原型界面
2.4.2 基本分页查询明确要求:
- 查询时,首先是要匹配几个域的,分别是: 商品的名称goodsName,商品的品牌brandName,商品三级分类的名称cat3name,而这几个域只要有一个匹配上就展示,这样能搜索到的商品就更多
- 域的分词,goodsName采用 ik_max_word分词,这样也能搜索到更多的产品,以提升用户的购买率,品牌和商品三级分类则可以不需要分词
- 以上要求组合起来可以得到:需要使用复合查询,should条件拼接
- 顺带可以将分页也处理了,只需添加分页设置即可
public PageResultVOsearchAndPageByES(SearchDTO searchDTO) { // 非空判断 if (searchDTO == null) { return new PageResultVO<>(false, "参数不合法"); } // 从es中查询 // 1. 多重匹配查询 BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery(); // 2. 条件匹配,需要分词,匹配商品名称,从条件dto中获取,先做非空判断,keyword为空是查所有 if (!StringUtils.isEmpty(searchDTO.getKeyword())) { MatchQueryBuilder goodsName = QueryBuilders.matchQuery("goodsName", searchDTO.getKeyword()); // 3. 条件匹配,不分词,匹配品牌名称 TermQueryBuilder brandName = QueryBuilders.termQuery("brandName", searchDTO.getKeyword()); // 4. 条件匹配 , 不分词, 匹配3级分类名称 TermQueryBuilder cat3name = QueryBuilders.termQuery("cat3name", searchDTO.getKeyword()); // 5. 使用should匹配,只要有一个符合就可以,这样符合电商的性质 boolQueryBuilder.should(goodsName).should(brandName).should(cat3name); } // 6. 设置分页信息,防止恶意攻击 if (searchDTO.getPage() <= 0 || searchDTO.getSize() <= 0) { searchDTO.setPage(1); searchDTO.setSize(5); } PageRequest pageRequest = PageRequest.of(searchDTO.getPage() - 1, searchDTO.getSize()); // 7. 创建并设置查询对象 NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder() // 设置查询条件 .withQuery(boolQueryBuilder) }
- searchDTO类
@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 PageResultVO2.4.3 高亮设置{ private Long total; // 总记录数 private Integer pages; // 总页数 private List data; // 每页数据 @NonNull private boolean success; // 响应标志位 @NonNull private String msg; // 响应信息 }
对于查询的关键字keyword可以设置高亮显示,这样的用户体验会更好
@Override
public PageResultVO searchAndPageByES(SearchDTO searchDTO) {
// 非空判断
if (searchDTO == null) {
return new PageResultVO<>(false, "参数不合法");
}
// 从es中查询
// 1. 多重匹配查询
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
// 2. 条件匹配,需要分词,匹配商品名称,从条件dto中获取,先做非空判断
if (!StringUtils.isEmpty(searchDTO.getKeyword())) {
MatchQueryBuilder goodsName = QueryBuilders.matchQuery("goodsName", searchDTO.getKeyword());
// 3. 条件匹配,不分词,匹配品牌名称
TermQueryBuilder brandName = QueryBuilders.termQuery("brandName", searchDTO.getKeyword());
// 4. 条件匹配 , 不分词, 匹配3级分类名称
TermQueryBuilder cat3name = QueryBuilders.termQuery("cat3name", searchDTO.getKeyword());
// 5. 使用should匹配,只要有一个符合就可以,这样符合电商的性质
boolQueryBuilder.should(goodsName).should(brandName).should(cat3name);
}
// 6. 设置分页信息,防止恶意攻击
if (searchDTO.getPage() <= 0 || searchDTO.getSize() <= 0) {
searchDTO.setPage(1);
searchDTO.setSize(5);
}
PageRequest pageRequest = PageRequest.of(searchDTO.getPage() - 1, searchDTO.getSize());
// 7. 创建并设置查询对象
NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder()
// 设置查询条件
.withQuery(boolQueryBuilder)
// 设置分页条件
.withPageable(pageRequest)
// 设置高亮
.withHighlightBuilder(getHighlightBuilder("goodsName"))
// 8. 查询
SearchHits search = restTemplate.search(nativeSearchQueryBuilder.build(), ESGoods.class);
// 9. 设置总条数
PageResult pageResultVO = new PageResult();
pageResultVO.setTotal(search.getTotalHits());
// 10. 获取命中对象
List> searchHits = search.getSearchHits();
// 11. 遍历,处理每页数据,和高亮
List esGoodsList = searchHits.stream().map(hit -> {
// 获取内容
ESGoods esGoods = hit.getContent();
// 设置高亮信息
Map> highlightFields = hit.getHighlightFields();
highlightFields.forEach((k, v) -> {
// v 即是每个信息中高亮的数组属性,做非空判断
if (v != null && v.size() > 0) {
// 如果k与域名相同,那么才设置高亮
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); //如果要多个字段高亮,这项要为false
highlightBuilder.preTags(""); //高亮设置
highlightBuilder.postTags("");
//下面这两项,如果你要高亮如文字内容等有很多字的字段,必须配置,不然会导致高亮不全,文章内容缺失等
highlightBuilder.fragmentSize(800000); //最大高亮分片数
highlightBuilder.numOfFragments(0); //从第一个分片获取高亮片段
return highlightBuilder;
}
}
2.4.4 聚合统计品牌和分类信息
回忆一下在kibana中我们是怎么做聚合的,是与query同级,使用aggs来聚合,设置聚合组名,聚合桶只能使用term不分词,设置field字段对应的域
那么在代码中使用聚合也是在与查询同级
public PageResultVO2.4.5 过滤查询,通过聚合出的品牌名称和分类名称过滤searchAndPageByES(SearchDTO searchDTO) { // 非空判断 if (searchDTO == null) { return new PageResultVO<>(false, "参数不合法"); } // 从es中查询 // 1. 多重匹配查询 BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery(); // 2. 条件匹配,需要分词,匹配商品名称,从条件dto中获取,先做非空判断 if (!StringUtils.isEmpty(searchDTO.getKeyword())) { MatchQueryBuilder goodsName = QueryBuilders.matchQuery("goodsName", searchDTO.getKeyword()); // 3. 条件匹配,不分词,匹配品牌名称 TermQueryBuilder brandName = QueryBuilders.termQuery("brandName", searchDTO.getKeyword()); // 4. 条件匹配 , 不分词, 匹配3级分类名称 TermQueryBuilder cat3name = QueryBuilders.termQuery("cat3name", searchDTO.getKeyword()); // 5. 使用should匹配,只要有一个符合就可以,这样符合电商的性质 boolQueryBuilder.should(goodsName).should(brandName).should(cat3name); } // 6. 设置分页信息,防止恶意攻击 if (searchDTO.getPage() <= 0 || searchDTO.getSize() <= 0) { searchDTO.setPage(1); searchDTO.setSize(5); } PageRequest pageRequest = PageRequest.of(searchDTO.getPage() - 1, searchDTO.getSize()); // 6.1设置聚合 // terms设置组名,field设置对应域,size设置桶的个数 TermsAggregationBuilder brandNameTermsAggregationBuilder = AggregationBuilders.terms("brandAgg").field("brandName").size(30); TermsAggregationBuilder cat3nameTermsAggregationBuilder = AggregationBuilders.terms("cat3Agg").field("cat3name").size(30); // 7. 创建并设置查询对象 NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder() // 设置查询条件 .withQuery(boolQueryBuilder) // 设置分页条件 .withPageable(pageRequest) // 设置高亮 .withHighlightBuilder(getHighlightBuilder("goodsName")) // 设置聚合 .addAggregation(brandNameTermsAggregationBuilder) .addAggregation(cat3nameTermsAggregationBuilder); // 8. 查询 SearchHits search = restTemplate.search(nativeSearchQueryBuilder.build(), ESGoods.class); // 9. 设置总条数 PageResult pageResultVO = new PageResult(); pageResultVO.setTotal(search.getTotalHits()); // 10. 获取命中对象 List > searchHits = search.getSearchHits(); // 11. 遍历,处理每页数据,和高亮 List esGoodsList = searchHits.stream().map(hit -> { // 获取内容 ESGoods esGoods = hit.getContent(); // 设置高亮信息 Map > highlightFields = hit.getHighlightFields(); highlightFields.forEach((k, v) -> { // v 即是每个信息中高亮的数组属性,做非空判断 if (v != null && v.size() > 0) { // 如果k与域名相同,那么才设置高亮 if ("goodsName".equals(k)) { esGoods.setGoodsName(v.get(0)); } } }); return esGoods; }).collect(Collectors.toList()); // 12. 获取聚合信息 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 brandNameList = brandNameBuckets.stream().map(item -> (String) item.getKey()).collect(Collectors.toList()); pageResultVO.setBrandNameList(brandNameList); List 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); //如果要多个字段高亮,这项要为false highlightBuilder.preTags(""); //高亮设置 highlightBuilder.postTags(""); //下面这两项,如果你要高亮如文字内容等有很多字的字段,必须配置,不然会导致高亮不全,文章内容缺失等 highlightBuilder.fragmentSize(800000); //最大高亮分片数 highlightBuilder.numOfFragments(0); //从第一个分片获取高亮片段 return highlightBuilder; }
为什么要使用过滤而非在复合条件中添加?
关键字: 分数
使用复合条件查询对结果的分数会有影响,就会影响到排序,页面图片的改变就会变大,这里我们希望是不影响结果的分数,那么只能使用过滤的方式来实现
回忆一下在kibana中怎么使用过滤的: 过滤在bool复合查询中,使用filters与bool同级,使用term或者terms来设置过滤的字段,该字段是不分词的 所以在代码中使用过滤也是在bool中组合,而非与查询同级
public PageResultVO2.4.6 设置排序searchAndPageByES(SearchDTO searchDTO) { // 非空判断 if (searchDTO == null) { return new PageResultVO<>(false, "参数不合法"); } // 从es中查询 // 1. 多重匹配查询 BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery(); // 2. 条件匹配,需要分词,匹配商品名称,从条件dto中获取,先做非空判断 if (!StringUtils.isEmpty(searchDTO.getKeyword())) { MatchQueryBuilder goodsName = QueryBuilders.matchQuery("goodsName", searchDTO.getKeyword()); // 3. 条件匹配,不分词,匹配品牌名称 TermQueryBuilder brandName = QueryBuilders.termQuery("brandName", searchDTO.getKeyword()); // 4. 条件匹配 , 不分词, 匹配3级分类名称 TermQueryBuilder cat3name = QueryBuilders.termQuery("cat3name", searchDTO.getKeyword()); // 5. 使用should匹配,只要有一个符合就可以,这样符合电商的性质 boolQueryBuilder.should(goodsName).should(brandName).should(cat3name); } // 6. 设置分页信息,防止恶意攻击 if (searchDTO.getPage() <= 0 || searchDTO.getSize() <= 0) { searchDTO.setPage(1); searchDTO.setSize(5); } PageRequest pageRequest = PageRequest.of(searchDTO.getPage() - 1, searchDTO.getSize()); // 6.1设置聚合 TermsAggregationBuilder brandNameTermsAggregationBuilder = AggregationBuilders.terms("brandAgg").field("brandName").size(30); TermsAggregationBuilder cat3nameTermsAggregationBuilder = AggregationBuilders.terms("cat3Agg").field("cat3name").size(30); // 6.2 设置过滤,过滤不会影响分数,过滤在bool中!!!! 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); } // 7. 创建并设置查询对象 NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder() // 设置查询条件,这里面已经设置了过滤了 .withQuery(boolQueryBuilder) // 设置分页条件 .withPageable(pageRequest) // 设置高亮 .withHighlightBuilder(getHighlightBuilder("goodsName")) // 设置聚合 .addAggregation(brandNameTermsAggregationBuilder) .addAggregation(cat3nameTermsAggregationBuilder); // 8. 查询 SearchHits search = restTemplate.search(nativeSearchQueryBuilder.build(), ESGoods.class); // 9. 设置总条数 PageResult pageResultVO = new PageResult(); pageResultVO.setTotal(search.getTotalHits()); // 10. 获取命中对象 List > searchHits = search.getSearchHits(); // 11. 遍历,处理每页数据,和高亮 List esGoodsList = searchHits.stream().map(hit -> { // 获取内容 ESGoods esGoods = hit.getContent(); // 设置高亮信息 Map > highlightFields = hit.getHighlightFields(); highlightFields.forEach((k, v) -> { // v 即是每个信息中高亮的数组属性,做非空判断 if (v != null && v.size() > 0) { // 如果k与域名相同,那么才设置高亮 if ("goodsName".equals(k)) { esGoods.setGoodsName(v.get(0)); } } }); return esGoods; }).collect(Collectors.toList()); // 12. 从查询结果中获取聚合信息 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 brandNameList = brandNameBuckets.stream().map(item -> (String) item.getKey()).collect(Collectors.toList()); pageResultVO.setBrandNameList(brandNameList); List 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); //如果要多个字段高亮,这项要为false highlightBuilder.preTags(""); //高亮设置 highlightBuilder.postTags(""); //下面这两项,如果你要高亮如文字内容等有很多字的字段,必须配置,不然会导致高亮不全,文章内容缺失等 highlightBuilder.fragmentSize(800000); //最大高亮分片数 highlightBuilder.numOfFragments(0); //从第一个分片获取高亮片段 return highlightBuilder; }
对于排序,我们可以通过页面原型发现,这里需要做的是对商品价格的排序
在查询的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 searchAndPageByES(SearchDTO searchDTO) {
// 非空判断
if (searchDTO == null) {
return new PageResultVO<>(false, "参数不合法");
}
// 从es中查询
// 1. 多重匹配查询
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
// 2. 条件匹配,需要分词,匹配商品名称,从条件dto中获取,先做非空判断
if (!StringUtils.isEmpty(searchDTO.getKeyword())) {
MatchQueryBuilder goodsName = QueryBuilders.matchQuery("goodsName", searchDTO.getKeyword());
// 3. 条件匹配,不分词,匹配品牌名称
TermQueryBuilder brandName = QueryBuilders.termQuery("brandName", searchDTO.getKeyword());
// 4. 条件匹配 , 不分词, 匹配3级分类名称
TermQueryBuilder cat3name = QueryBuilders.termQuery("cat3name", searchDTO.getKeyword());
// 5. 使用should匹配,只要有一个符合就可以,这样符合电商的性质
boolQueryBuilder.should(goodsName).should(brandName).should(cat3name);
}
// 6. 设置分页信息,防止恶意攻击
if (searchDTO.getPage() <= 0 || searchDTO.getSize() <= 0) {
searchDTO.setPage(1);
searchDTO.setSize(5);
}
PageRequest pageRequest = PageRequest.of(searchDTO.getPage() - 1, searchDTO.getSize());
// 6.1设置聚合
TermsAggregationBuilder brandNameTermsAggregationBuilder = AggregationBuilders.terms("brandAgg").field("brandName").size(30);
TermsAggregationBuilder cat3nameTermsAggregationBuilder = AggregationBuilders.terms("cat3Agg").field("cat3name").size(30);
// 6.2 设置过滤,过滤不会影响分数,过滤在bool中!!!!
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);
}
// 6.3 设置排序 , 排序与query同级
// 提出变量,按条件赋值
FieldSortBuilder price = null;
if (!StringUtils.isEmpty(searchDTO.getSortField()) && "desc".equals(searchDTO.getSortField())) {
// 不为空,且前端传过来的是desc ,则按降序排列
price = SortBuilders.fieldSort("price").order(SortOrder.DESC);
}
if (!StringUtils.isEmpty(searchDTO.getSortField()) && "asc".equals(searchDTO.getSortField())) {
// 不为空,且前端传过来的是desc ,则按降序排列
price = SortBuilders.fieldSort("price").order(SortOrder.ASC);
}
// 7. 创建并设置查询对象
NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder()
// 设置查询条件
.withQuery(boolQueryBuilder)
// 设置分页条件
.withPageable(pageRequest)
// 设置高亮
.withHighlightBuilder(getHighlightBuilder("goodsName"))
// 设置聚合
.addAggregation(brandNameTermsAggregationBuilder)
.addAggregation(cat3nameTermsAggregationBuilder);
// 7.1 判断排序条件是否存在
if (price != null){
// 存在则加上排序
nativeSearchQueryBuilder.withSort(price);
}
// 8. 查询
SearchHits search = restTemplate.search(nativeSearchQueryBuilder.build(), ESGoods.class);
// 9. 设置总条数
PageResult pageResultVO = new PageResult();
pageResultVO.setTotal(search.getTotalHits());
// 10. 获取命中对象
List> searchHits = search.getSearchHits();
// 11. 遍历,处理每页数据,和高亮
List esGoodsList = searchHits.stream().map(hit -> {
// 获取内容
ESGoods esGoods = hit.getContent();
// 设置高亮信息
Map> highlightFields = hit.getHighlightFields();
highlightFields.forEach((k, v) -> {
// v 即是每个信息中高亮的数组属性,做非空判断
if (v != null && v.size() > 0) {
// 如果k与域名相同,那么才设置高亮
if ("goodsName".equals(k)) {
esGoods.setGoodsName(v.get(0));
}
}
});
return esGoods;
}).collect(Collectors.toList());
// 12. 从查询结果中获取聚合信息
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 brandNameList = brandNameBuckets.stream().map(item -> (String) item.getKey()).collect(Collectors.toList());
pageResultVO.setBrandNameList(brandNameList);
List 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); //如果要多个字段高亮,这项要为false
highlightBuilder.preTags(""); //高亮设置
highlightBuilder.postTags("");
//下面这两项,如果你要高亮如文字内容等有很多字的字段,必须配置,不然会导致高亮不全,文章内容缺失等
highlightBuilder.fragmentSize(800000); //最大高亮分片数
highlightBuilder.numOfFragments(0); //从第一个分片获取高亮片段
return highlightBuilder;
}
至此,一个ElasticSearch的基本使用完成,你学废了么?



