一、建立商品索引库
首先要分析商品的参数和页面上需要展示的内容来分析商品索引库字段,搜索出来的大图上展示的是每个spu部分信息以及每个spu下的各个sku部分信息,接着开始分析分词字段的内容,用户搜索的时候输入的是商品的标题、分类或是品牌,所以分词字段由商品的这三个属性拼接而成,选用的是ik_max_word分词器,接着设计用于过滤的字段,一些不适合作为索引的字段设置为index = false,再加入一些商品三级分类字段,价格字段便于页面的展示,最后调用创建索引库的api方法。
@Data
@Document(indexName
= "goods", type
= "docs", shards
= 1, replicas
= 0)
public class Goods implements Serializable {
@Id
private Long id
;
@Field(type
= FieldType
.Text
, analyzer
= "ik_max_word")
private String all
;
private Long brandId
;
private Map
<String,Object> specs
;
@Field(type
= FieldType
.Keyword
, index
= false)
private String skus
;
@Field(type
= FieldType
.Keyword
, index
= false)
private String subTitle
;
private Long cid1
;
private Long cid2
;
private Long cid3
;
private Date createTime
;
private Set
<Long> price
;
}
下面很重要的一步就是往索引库中添加添加数据,需要远程调用商品微服务中的方法获取数据,获取到数据之后将Spu对象传入buildGoods方法中,每100个商品对象作为一个集合调用loadData方法,最后在loadData方法中调用存索引集合的api方法。
public Goods
buildGoods(Spu spu
){
List
<Category> categoryList
= categoryClient
.queryCategoryListByIds(Arrays
.asList(
spu
.getCid1(),spu
.getCid2(),spu
.getCid3()
));
if (CollectionUtils
.isEmpty(categoryList
)){
throw new LyException(ExceptionEnum
.CATEGORY_NOT_FOUND
);
}
List
<String> names
= categoryList
.stream().map(Category
::getName
).collect(Collectors
.toList());
Brand brand
= brandClient
.queryBrandById(spu
.getBrandId());
if (brand
== null
){
throw new LyException(ExceptionEnum
.BRAND_NOT_FOUND
);
}
String all
= spu
.getTitle() +
StringUtils
.join(names
," ") + brand
.getName();
List
<Sku> skuList
= goodsClient
.querySkusBySpuId(spu
.getId());
if (CollectionUtils
.isEmpty(skuList
)){
throw new LyException(ExceptionEnum
.GOODS_NOT_FOUND
);
}
List
<Map
<String,Object>> skus
= new ArrayList<>();
Set
<Long> priceList
= new HashSet<>();
for (Sku sku
:skuList
){
Map
<String,Object> map
= new HashMap<>();
map
.put("id",sku
.getId());
map
.put("title",sku
.getTitle());
map
.put("price",sku
.getPrice());
map
.put("image",StringUtils
.substringBefore(sku
.getImages(),","));
skus
.add(map
);
priceList
.add(sku
.getPrice());
}
List
<SpecParam> params
= specificationClient
.queryParamList(null
, spu
.getCid3(), true);
if (CollectionUtils
.isEmpty(params
)){
throw new LyException(ExceptionEnum
.SPEC_PARAM_NOT_FOUND
);
}
SpuDetail spuDetail
= goodsClient
.querySpuDetailBySpuId(spu
.getId());
if (spuDetail
== null
){
throw new LyException(ExceptionEnum
.GOODS_NOT_FOUND
);
}
Map
<Long,String> genericSpec
= JsonUtils
.parseMap(spuDetail
.getGenericSpec(),Long
.class,String
.class);
Map
<Long
,List
<String>> specialSpec
= JsonUtils
.nativeRead(spuDetail
.getSpecialSpec(), new TypeReference<Map
<Long
, List
<String>>>() {});
Map
<String,Object> specs
= new HashMap<>();
for (SpecParam param
:params
){
String key
= param
.getName();
Object value
= "";
if (param
.getGeneric()){
value
= genericSpec
.get(param
.getId());
if (param
.getNumeric() && StringUtils
.isNotBlank(param
.getSegments())){
value
= chooseSegment(value
.toString(),param
);
}
}else {
value
= specialSpec
.get(param
.getId());
}
specs
.put(key
,value
);
}
Goods goods
= new Goods();
goods
.setAll(all
);
goods
.setBrandId(spu
.getBrandId());
goods
.setCid1(spu
.getCid1());
goods
.setCid2(spu
.getCid2());
goods
.setCid3(spu
.getCid3());
goods
.setCreateTime(spu
.getCreateTime());
goods
.setId(spu
.getId());
goods
.setSubTitle(spu
.getSubTitle());
goods
.setAll(all
);
goods
.setPrice(priceList
);
goods
.setSkus(JSON
.toJSONString(skus
));
goods
.setSpecs(specs
);
return goods
;
}
public void loadData(){
int page
= 1;
int rows
= 100;
int size
= 0;
do {
PageResult
<Spu> result
= goodsClient
.querySpuByPage(
page
,rows
,true,null
);
List
<Spu> spuList
= result
.getItems();
if (CollectionUtils
.isEmpty(spuList
)){
break;
}
List
<Goods> goodsList
= spuList
.stream().map(
searchService
:: buildGoods
).collect(Collectors
.toList());
goodsRepository
.saveAll(goodsList
);
page
++;
size
= spuList
.size();
}while (size
== 100);
}
二、查询商品索引库
首先指定要检索的字段,过滤掉一些不用检索的字段将搜索的结果进行分页处理既有搜索条件又有过滤条件时要用BoolQuery的must方法来整合,对应的是buildBasicQuery方法最后对结果按分类和品牌进行聚合,规定商品分类存在而且数量是 1, 则可以聚合规格参数,因为分类不同的话它们之间的规格参数显然是不同的
public PageResult
<Goods> search(SearchRequest request
) {
Integer page
= request
.getPage() - 1;
Integer size
= request
.getSize();
NativeSearchQueryBuilder queryBuilder
= new NativeSearchQueryBuilder();
queryBuilder
.withSourceFilter(
new FetchSourceFilter(
new String[]{"id","subTitle","skus"}, null
));
queryBuilder
.withPageable(PageRequest
.of(page
,size
));
QueryBuilder basicQuery
= buildBasicQuery(request
);
queryBuilder
.withQuery(basicQuery
);
queryBuilder
.addAggregation(AggregationBuilders
.terms("category_agg").field("cid3"));
queryBuilder
.addAggregation(AggregationBuilders
.terms("brand_agg").field("brandId"));
AggregatedPage
<Goods> result
= template
.queryForPage(queryBuilder
.build(),Goods
.class);
if (result
== null
){
throw new LyException(ExceptionEnum
.GOODS_NOT_FOUND
);
}
Long total
= result
.getTotalElements();
Integer totalPage
= result
.getTotalPages();
List
<Goods> goodsList
= result
.getContent();
Aggregations aggs
= result
.getAggregations();
List
<Category> categories
= parseCategoryAgg(aggs
.get("category_agg"));
List
<Brand> brands
= parseBrandAgg(aggs
.get("brand_agg"));
List
<Map
<String, Object>> specs
= null
;
if (categories
!= null
&& categories
.size() == 1){
specs
= buildSpecificaitonAgg(categories
.get(0).getId(),basicQuery
);
}
return new SearchResult(total
,totalPage
,goodsList
,categories
,brands
,specs
);
}
private QueryBuilder
buildBasicQuery(SearchRequest request
) {
BoolQueryBuilder queryBuilder
= QueryBuilders
.boolQuery();
try {
NativeSearchQueryBuilder nativeSearchQueryBuilder
= new NativeSearchQueryBuilder();
nativeSearchQueryBuilder
.withSourceFilter(
new FetchSourceFilter(
new String[]{"id","subTitle","skus"}, null
));
nativeSearchQueryBuilder
.withQuery(QueryBuilders
.matchQuery("all", request
.getKey()));
nativeSearchQueryBuilder
.withIndices("goods").withTypes("docs");
NativeSearchQuery searchQuery
= nativeSearchQueryBuilder
.build();
long count
= template
.count(searchQuery
);
if (count
== 0){
return null
;
}
}catch (Exception e
){
return null
;
}
queryBuilder
.must(QueryBuilders
.matchQuery("all", request
.getKey()));
Map
<String, String> map
= request
.getFilter();
for (Map
.Entry
<String,String> entry
: map
.entrySet()){
String key
= entry
.getKey();
if ("cid3".equals(key
) || "brandId".equals(key
)){
}else {
key
= "specs."+key
+".keyword";
}
String value
= entry
.getValue();
queryBuilder
.filter(QueryBuilders
.termQuery(key
,value
));
}
return queryBuilder
;
}
三、展示搜索结果
用户输入手机这一大类之后
用户选中小米品牌的手机和后置摄像头像素之后
用户输入商品索引库中没有的关键词后
四、遇到的问题和解决途径
规格参数索引对象下的属性在聚合时忘记加keyword导致异常 queryBuilder.addAggregation(AggregationBuilders.terms(name).field(“specs.”+name+".keyword")); 是通过 通过观察下面的报错后解决的
java
.lang
.IllegalArgumentException
: Fielddata is disabled on text fields by
default. Set fielddata
=true on
[specs
.操作系统
] in order to load fielddata in memory by uninverting the inverted index
. Note that
this can however use significant memory
. Alternatively use a keyword field instead
.
没有用户搜索时输入商品索引库没有的内容没有处理,导致页面混乱
try {
NativeSearchQueryBuilder nativeSearchQueryBuilder
= new NativeSearchQueryBuilder();
nativeSearchQueryBuilder
.withSourceFilter(
new FetchSourceFilter(
new String[]{"id","subTitle","skus"}, null
));
nativeSearchQueryBuilder
.withQuery(QueryBuilders
.matchQuery("all", request
.getKey()));
nativeSearchQueryBuilder
.withIndices("goods").withTypes("docs");
NativeSearchQuery searchQuery
= nativeSearchQueryBuilder
.build();
long count
= template
.count(searchQuery
);
if (count
== 0){
return null
;
}
}catch (Exception e
){
return null
;
}
页面上的和vue有关的一些代码请教前端组的学姐后解决了,因为设计问题一些向后端发送的数据也通过字符串的拼接解决了