实现搜索的功能。
实现搜索功能:
添加搜索库
(1)首先分析,输入搜索字段之后,页面会显示的数据,把需要的数据封装成一个类。然后创建相应的类。goods。主要是第一张图。
对分类和品牌进行的聚合,也主要是给第二张图进行渲染。
@Document(indexName = "goods", type = "docs", shards = 1, replicas = 0)
public class Goods {
@Id
private Long id; // spuId
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String all; // 所有需要被搜索的信息,包含标题,分类,甚至品牌
@Field(type = FieldType.Keyword, index = false)
private String subTitle;// 卖点
private Long brandId;// 品牌id
private Long cid1;// 1级分类id
private Long cid2;// 2级分类id
private Long cid3;// 3级分类id
private Date createTime;// 创建时间
private List<Long> price;// 价格
@Field(type = FieldType.Keyword, index = false)
private String skus;// List<sku>信息的json结构
private Map<String, Object> specs;// 可搜索的规格参数,key是参数名,值是参数值
}
在这里思考我们需要的数据,然后我们需要哪些服务。然后写这些接口
第一:分批查询spu的服务,已经写过。
第二:根据spuId查询sku的服务,已经写过
第三:根据spuId查询SpuDetail的服务,已经写过
第四:根据商品分类id,查询商品分类名称,没写过
第五:根据商品品牌id,查询商品的品牌,没写过
第六:规格参数接口
(2)将mysql数据库中的数据按照每页查出来,以一页为单位进行循环。将每页的数据集合转换成goods类型的集合List<.Goods>。然后在存储到elasticsearch中。
public interface GoodsRepository extends
ElasticsearchRepository<Goods, Long> {
}
导入数据其实就是查询数据,然后把查询到的Spu转变为Goods来保存,因此我们先编写一个SearchService,然后在里面定义一个方法, 把Spu转为Goods,执行this.goodsRepository.saveAll(goodsList)
将结果保存在Elasticsearch中去。
blic void test(){
int page = 1;
int rows = 100;
do{
this.elasticsearchTemplate.createIndex(Goods.class);
this.elasticsearchTemplate.putMapping(Goods.class);
// 分页查询spu,获取分页数据,page:表示当前页。
PageResult<SpuBo> result = this.goodsClient.querySpuBoByPage(null, null, page, rows);
// 获取当前页的数据
List<SpuBo> items = result.getItems();
// 处理List<SpuBO> ==> List<Goods>
List<Goods> goodsList = items.stream().map(spuBo -> {
try {
return this.searchService.buildGoods(spuBo);
} catch (IOException e) {
e.printStackTrace();
}
return null;
}).collect(Collectors.toList());
// 执行新增数据的方法
this.goodsRepository.saveAll(goodsList);
rows = items.size();
page++;
}while(rows==100);
其中将spu转换成goods代码如下:下面的反序列化:是将字符串转换成对象。
public Goods buildGoods(Spu spu) throws IOException {
Goods goods = new Goods();
// 根据分类的id查询分类名称
List<String> names = this.categoryClient.queryNamesByIds(Arrays.asList(spu.getCid1(), spu.getCid2(), spu.getCid3()));
// 根据品牌id 查询品牌
Brand brand = brandClient.queryBrandById(spu.getBrandId());
// 根据spuId查询所有的sku
List<Sku> skus = this.goodsClient.querySkusBySpuId(spu.getId());
// 初始化一个价格的集合,收集所有sku 的价格
ArrayList<Long> prices = new ArrayList<>();
// 收集sku必要字段信息
ArrayList<Map<String, Object>> skuMapList = new ArrayList<>();
skus.forEach(sku -> {
prices.add(sku.getPrice());
HashMap<String, Object> map = new HashMap<>();
map.put("id", sku.getId());
map.put("title", sku.getTitle());
map.put("price", sku.getPrice());
// 获取sku中的图片,数据库的中图片可能是多张,多张是以","分割,所以也以逗号来切割图片数组,获取第一张图片
map.put("image", StringUtils.isBlank(sku.getImages()) ? "" : StringUtils.split(sku.getImages(),",")[0]);
skuMapList.add(map);
});
// 根据spu中的cid3,查询所有的搜索规格参数
List<SpecParam> params = this.specificationClient.queryParams(null, spu.getCid3(), null, true);
// 根据spuId 查询spuDetail
SpuDetail spuDetail = this.goodsClient.querySpuDetailBySpuId(spu.getId());
// 把通用的规格参数值,进行反序列化
Map<String,Object> genericSpecMap = MAPPER.readValue(spuDetail.getGenericSpec(), new TypeReference<Map<String, Object>>() {});
// 把特殊的规格的参数值,进行反序列化
Map<String,List<Object>> specialSpecMap = MAPPER.readValue(spuDetail.getSpecialSpec(), new TypeReference<Map<String, List<Object>>>() {});
HashMap<String, Object> specs = new HashMap<>();
params.forEach(param -> {
// 判断规格参数的类型,是否是通用的规格参数类型
if (param.getGeneric()){
// 如果是通用类型的参数,从genericSpecMap获取规格参数值
String value = genericSpecMap.get(param.getId().toString()).toString();
// 判断是否是数值类型,如果是数值类型,应该返回一个区间
if (param.getNumeric()){
value = chooseSegment(value, param);
}
specs.put(param.getName(), value);
} else {
// 特殊规格的参数类型
List<Object> value = specialSpecMap.get(param.getId().toString());
specs.put(param.getName(), value);
}
});
goods.setId(spu.getId());
goods.setCid1(spu.getCid1());
goods.setCid2(spu.getCid2());
goods.setCid3(spu.getCid3());
goods.setBrandId(spu.getBrandId());
goods.setCreateTime(spu.getCreateTime());
goods.setSubTitle(spu.getSubTitle());
// 拼接all字段,需要分类名称以及品牌名称
goods.setAll(spu.getTitle() + " " + StringUtils.join(names," ") + " " + brand.getId());
// 获取spu下的所有的sku的价格
goods.setPrice(prices);
// 获取spu下的所有sku,并转化成json字符串
goods.setSkus(MAPPER.writeValueAsString(skuMapList));
// 获取所有查询的规格参数
goods.setSpecs(specs);
return goods;
}
查询
(3)。聚合的结果集转换成Terms,StringTerms,LongTerms。才能获取桶的集合。
例如:LongTerms terms = (LongTerms)aggregation;
terms.getBuckets();
下面需要注意的是:
(1)首先// 添加分类和品牌的聚合。其中categoryAggName:是聚合的名称,而field:是按照什么进行聚合。
queryBuilder.addAggregation(AggregationBuilders.terms(categoryAggName).field("cid3"));
queryBuilder.addAggregation(AggregationBuilders.terms(brandAggName).field("brandId"));
(2)然后通过下面来进行解析
// 获取聚合结果集并解析
List<Map<String, Object>> categories = getCategoryAggResult(goodsPage.getAggregation(categoryAggName));
List<Brand> brands = getBrandAggResult(goodsPage.getAggregation(brandAggName));
/**
* 根据搜索参数查询
* @param searchRequest
* @return
*/
public SearchResult search(SearchRequest searchRequest) {
if (StringUtils.isBlank(searchRequest.getKey())){
return null;
}
// 自定义查询构建器
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
// 添加查询条件
QueryBuilder basicQuery = QueryBuilders.matchQuery("all", searchRequest.getKey()).operator(Operator.AND);
// BoolQueryBuilder basicQuery = buildBoolQueryBuilder(searchRequest);
queryBuilder.withQuery(basicQuery);
//queryBuilder.withQuery(QueryBuilders.matchQuery("all", searchRequest.getKey()).operator(Operator.AND));
// 添加分页,分页页码从0开始
queryBuilder.withPageable(PageRequest.of(searchRequest.getPage()-1, searchRequest.getSize()));
// 添加结果集过滤,否则将返回一堆没用的数据,影响查询的效率。
queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{"id","skus","subTitle"},null));
// 添加分类和品牌的聚合。其中categoryAggName:是聚合的名称,而field:是按照什么进行聚合。
String categoryAggName = "categories";
String brandAggName = "brands";
queryBuilder.addAggregation(AggregationBuilders.terms(categoryAggName).field("cid3"));
queryBuilder.addAggregation(AggregationBuilders.terms(brandAggName).field("brandId"));
// 执行查询,获取结果集
//Page<Goods> goodsPage = this.goodsRepository.search(queryBuilder.build());
AggregatedPage<Goods> goodsPage = (AggregatedPage<Goods>)this.goodsRepository.search(queryBuilder.build());
// 获取聚合结果集并解析
List<Map<String, Object>> categories = getCategoryAggResult(goodsPage.getAggregation(categoryAggName));
List<Brand> brands = getBrandAggResult(goodsPage.getAggregation(brandAggName));
// 判断是否是一个分类,只有是一个分类是才做参数聚合
List<Map<String, Object>> specs = null;
if(!CollectionUtils.isEmpty(categories) && categories.size() == 1){
// 对规格参数进行聚合
specs = getParamAggResult((Long)categories.get(0).get("id"),basicQuery);
}
//getTotalElements:查询到的数据的总条数。getContent:查询到的数据
return new SearchResult(goodsPage.getTotalElements(), goodsPage.getTotalPages(), goodsPage.getContent(),categories,brands,specs);
}
(3)对结果聚合结果进行,解析,只有一个分类才做参数聚合,下面是对规格参数的聚合方法:
private List<Map<String, Object>> getParamAggResult(Long cid, QueryBuilder basicQuery) {
//自定义查询对象构建
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
//添加基本查询条件
queryBuilder.withQuery(basicQuery);
//查询要聚合的规格参数 gid:组id generic:通用的还是特殊的。 cid:表示分类的id
List<SpecParam> params = this.specificationClient.queryParams(null, cid, null, true);
//添加规格参数的聚合 。此时的keyword是分词还是不需要分词。
params.forEach(param -> {
queryBuilder.addAggregation(AggregationBuilders.terms(param.getName()).field("specs." + param.getName() + ".keyword"));
});
//添加结果集过滤
queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{},null));
//执行聚合
AggregatedPage<Goods> goodsPage = (AggregatedPage<Goods>)this.goodsRepository.search(queryBuilder.build());
List<Map<String, Object>> specs = new ArrayList<>();
// 解析聚合结果集 key- 聚合名称(规格参数) value-聚合参数
Map<String, Aggregation> aggregationMap = goodsPage.getAggregations().asMap();
for (Map.Entry<String, Aggregation> entry : aggregationMap.entrySet()) {
// 初始化一个map {k: 规格参数名 options: 聚合的规格参数}
HashMap<String, Object> map = new HashMap<>();
map.put("k",entry.getKey());
// 初始化一个options集合,收集桶中的key
ArrayList<String> options = new ArrayList<>();
// 获取聚合
StringTerms terms = (StringTerms) entry.getValue();
// 获取桶集合
terms.getBuckets().forEach(bucket -> {
options.add(bucket.getKeyAsString());
});
map.put("options",options);
specs.add(map);
}
return specs;
}
下一篇: 数据库设计--树形结构设计