跳到主要内容

商品搜索架构文档

概述

  1. 用户端搜索商品,可按以下条件进行搜索:

    商品名称关键字、分类、品牌、参数和价格范围

  2. 商品列表可以按照以下条件进行排序显示:

    默认排序(搜索优先级)、销量、价格和好评率

API

API地址

https://{buyer-api-domain}/buyer/goods/search

请求方式

GET

请求参数

请求参数用GoodsSearchDTO实体类进行接收,参数内容说明如下:

参数参数含义示例
pageNo分页页数1
pageSize分页每页数量20
keyword搜索关键字手机
category商品分类491
brand商品品牌151
price价格区间10_100
sort排序值格式为:关键字_排序;示例:def_asc
prop参数信息格式为:参数名_参数值;示例:型号_G402
sellerId商家ID店铺内部搜索商品时使用
shopCatId店铺分组ID店铺内部搜索商品时使用
point_disable是否开启积分兑换 0:否,1:是积分商品页面搜索商品时使用
recommend是否为推荐商品 0:否,1:是搜索推荐商品时使用

重点参数说明:

sort:排序值,格式为:关键字_排序,可按正序和倒序排序,允许上传的字段有限制,只允许上传如下字段:

默认正序def_asc
默认倒序def_desc
价格正序price_asc
价格倒序price_desc
销量正序buynum_asc
销量倒序buynum_desc
好评率正序grade_asc
好评率倒序grade_desc

返回参数

返回参数为GoodsSearchResultVO实体类,共包含两部分数据

  • WebPage<GoodsSearchLine> goodsData:商品分页列表数据
  • Map selectorData:商品选择器数据

返回参数说明:

GoodsSearchLine内容如下:

参数参数含义
goodsId商品ID
name商品名称
thumbnail商品缩略图
small商品小图
discountPrice商品优惠价格(预留字段,暂时未用到)
price商品价格
buyCount购买数量
commentNum评论数
grade商品好評率
sellerId商家ID
sellerName商家名称
selfOperated是否为自营店铺商品 0:否,1:是
pointDisable积分兑换是否开启 0:否,1:是
exchangePoint兑换商品所需的积分
goodsType商品类型 NORMAL:普通商品,VIRTUAL:虚拟商品

Map selectorData内容结构如下:

catList<SearchSelector>
selected_catList<SearchSelector>
brandList<SearchSelector>
propList<PropSelector>

SearchSelector内容结构如下:

name名称
url链接
isSelected是否被选中(true&false)
value选择值
otherOptions其它选项

PropSelector内容结构如下:

key参数名称
value参数值集合,类型为List<SearchSelector>

返回参数json示例:

{
"goods_data":{
"data":[
{
"goods_id":"273",
"name":"索尼(SONY)PS4 Slim Pro 正版游戏软件 实体光盘 GTA5 猎车手5",
"thumbnail":"http://javashop-statics.oss-cn-beijing.aliyuncs.com/demo/66F34A1B0F8241709D5EA4677D5ABC89.jpg_300x300",
"small":"http://javashop-statics.oss-cn-beijing.aliyuncs.com/demo/66F34A1B0F8241709D5EA4677D5ABC89.jpg_400x400",
"discount_price":0,
"price":298,
"buy_count":1,
"comment_num":0,
"grade":100,
"seller_id":"1",
"seller_name":"平台自营",
"self_operated":1,
"exchange_point":0,
"point_disable":0,
"goods_type":"NORMAL"
},
{
"goods_id":"1689100487747145729",
"name":"商品展示",
"thumbnail":"https://javashop-statics.oss-cn-beijing.aliyuncs.com/test/normal/9B8AD5DB1A0A42C09CAD0A7436FD95F9.jpg_300x300",
"small":"https://javashop-statics.oss-cn-beijing.aliyuncs.com/test/normal/9B8AD5DB1A0A42C09CAD0A7436FD95F9.jpg_400x400",
"discount_price":0,
"price":100,
"buy_count":6,
"comment_num":6,
"grade":100,
"seller_id":"1681937776303042562",
"seller_name":"是的是的是",
"self_operated":0,
"exchange_point":0,
"point_disable":0,
"goods_type":"NORMAL"
}
],
"page_no":1,
"page_size":50,
"data_total":21
},
"selector_data":{
"selected_cat":[
{
"name":"数码家电",
"url":null,
"is_selected":false,
"value":"491",
"other_options":[
{
"name":"数码家电",
"url":null,
"is_selected":false,
"value":"491",
"other_options":null
},
{
"name":"食品饮料",
"url":null,
"is_selected":false,
"value":"1",
"other_options":null
}
]
}
],
"cat":[
{
"name":"家电",
"url":null,
"is_selected":false,
"value":"493",
"other_options":null
},
{
"name":"数码",
"url":null,
"is_selected":false,
"value":"492",
"other_options":null
}
],
"prop":[
{
"key":"型号",
"value":[
{
"name":"G610 青轴",
"url":null,
"is_selected":false,
"value":"G610 青轴",
"other_options":null
},
{
"name":"华硕RT-AC66U B1",
"url":null,
"is_selected":false,
"value":"华硕RT-AC66U B1",
"other_options":null
}
]
},
{
"key":"类型",
"value":[
{
"name":"无线鼠标",
"url":null,
"is_selected":false,
"value":"无线鼠标",
"other_options":null
},
{
"name":"有线鼠标",
"url":null,
"is_selected":false,
"value":"有线鼠标",
"other_options":null
}
]
},
{
"key":"连接类型",
"value":[
{
"name":"有线",
"url":null,
"is_selected":false,
"value":"有线",
"other_options":null
}
]
}
],
"brand":[
{
"name":"美的",
"url":"http://javashop-statics.oss-cn-beijing.aliyuncs.com/demo/BE90E762AE18430D95A862F2C38A1A39.jpeg",
"is_selected":false,
"value":"162",
"other_options":null
},
{
"name":"小米",
"url":"http://javashop-statics.oss-cn-beijing.aliyuncs.com/demo/66DC1A272FE143038CE1DE6E966ECA8A.jpeg",
"is_selected":false,
"value":"170",
"other_options":null
}
]
}
}

类图展示

image-20230817145104145

代码展示

构建搜索条件

com.enation.app.javashop.service.goodssearch.impl.GoodsSearchManagerImpl#createQuery

protected SearchSourceBuilder createQuery(GoodsSearchDTO goodsSearch) {
String keyword = goodsSearch.getKeyword();
Long cat = goodsSearch.getCategory();
Long brand = goodsSearch.getBrand();
String price = goodsSearch.getPrice();
Integer pointDisable = goodsSearch.getPointDisable();
Long sellerId = goodsSearch.getSellerId();
Long shopCatId = goodsSearch.getShopCatId();

//sourceBuilder 是用来构建查询条件
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.trackTotalHits(true);
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();

// 关键字检索
if (!StringUtil.isEmpty(keyword)) {
//名字搜索的定义,后面会用到
Map<String, Float> fields = new HashMap<>();
fields.put("name", QueryStringQueryBuilder.DEFAULT_BOOST);
fields.put("name.keyword", QueryStringQueryBuilder.DEFAULT_BOOST);
QueryStringQueryBuilder queryString = new QueryStringQueryBuilder(keyword).field("name").fields(fields);
queryString.defaultOperator(Operator.AND);
queryString.analyzer(EsSettings.IK_SMART);
boolQueryBuilder.must(queryString);
}
// 品牌搜素
if (brand != null) {
boolQueryBuilder.must(QueryBuilders.termQuery("brand", brand));
}
// 按店铺搜索
if (sellerId != null) {
boolQueryBuilder.must(QueryBuilders.termQuery("sellerId", sellerId));
}
//是否推荐商品
if (goodsSearch.getRecommend() != null) {
boolQueryBuilder.must(QueryBuilders.termQuery("recommend", goodsSearch.getRecommend()));
}
// 分类检索
if (cat != null) {
CategoryDO category = categoryManager.getModel(cat);
if (category == null) {
throw new ServiceException("", "该分类不存在");
}
boolQueryBuilder.must(QueryBuilders.wildcardQuery("categoryPath", HexUtils.encode(category.getCategoryPath()) + "*"));
}

// 固定条件:查询审核已通过的商品
boolQueryBuilder.must(QueryBuilders.termQuery("isAuth", "1"));
// 固定条件:查询已上架的商品
boolQueryBuilder.must(QueryBuilders.termQuery("marketEnable", "1"));
// 固定条件:查询未删除的商品
boolQueryBuilder.must(QueryBuilders.termQuery("disabled", "1"));

// 参数检索
String prop = goodsSearch.getProp();
if (!StringUtil.isEmpty(prop)) {
String[] propArray = prop.split(Separator.SEPARATOR_PROP);
for (String p : propArray) {
String[] onpropAr = p.split(Separator.SEPARATOR_PROP_VLAUE);
String name = onpropAr[0];
String value = onpropAr[1];
boolQueryBuilder.must(QueryBuilders.nestedQuery("params", QueryBuilders.termQuery("params.name", name), ScoreMode.None));
boolQueryBuilder.must(QueryBuilders.nestedQuery("params", QueryBuilders.termQuery("params.value", value), ScoreMode.None));
}
}
// 卖家分组 查询
if (ObjectUtil.isNotEmpty(shopCatId)) {
ShopCatDO shopCat = shopCatClient.getModel(shopCatId);
if (shopCat == null) {
throw new ServiceException("", "该分组不存在");
}
boolQueryBuilder.must(QueryBuilders.wildcardQuery("shopCatPath", HexUtils.encode(shopCat.getCatPath()) + "*"));
}
//价格搜索
if (!StringUtil.isEmpty(price)) {
String[] pricear = price.split(Separator.SEPARATOR_PROP_VLAUE);
double min = StringUtil.toDouble(pricear[0], 0.0);
double max = Integer.MAX_VALUE;

if (pricear.length == 2) {
max = StringUtil.toDouble(pricear[1], Double.MAX_VALUE);
}
boolQueryBuilder.must(QueryBuilders.rangeQuery("price").from(min).to(max).includeLower(true).includeUpper(true));
}
// 按是否开启积分兑换进行搜索 0:否,1:是
if (pointDisable != null) {
boolQueryBuilder.must(QueryBuilders.termQuery("pointDisable", pointDisable));
}

sourceBuilder.query(boolQueryBuilder);

//排序
String sortField = goodsSearch.getSort();
String sortId = "priority";
SortOrder sort = SortOrder.DESC;
if (!StringUtil.isEmpty(sortField)) {
Map<String, String> sortMap = SortContainer.getSort(sortField);
sortId = sortMap.get("id");
// 如果是默认排序 --默认排序根据 商品优先级排序
if ("def".equals(sortId)) {
sortId = "priority";
}
if ("buynum".equals(sortId)) {
sortId = "buyCount";
}
if ("desc".equals(sortMap.get("def_sort"))) {
sort = SortOrder.DESC;
} else {
sort = SortOrder.ASC;
}
}
sourceBuilder.sort(sortId, sort);
//好平率
if ("grade".equals(sortId)) {
sourceBuilder.sort("commentNum", SortOrder.DESC);
sourceBuilder.sort("buyCount", SortOrder.DESC);
}
//如果不是默认排序 则在原有搜索结果基础上加上商品优先级排序
if (!"priority".equals(sortId)) {
//商品优先级
sourceBuilder.sort("priority", SortOrder.DESC);
}
return sourceBuilder;
}

搜索商品

com.enation.app.javashop.service.goodssearch.impl.GoodsSearchManagerImpl#searchGoodsAndSelector

public GoodsSearchResultVO searchGoodsAndSelector(GoodsSearchDTO goodsSearch) {
//返回结果
GoodsSearchResultVO goodsSearchResult = new GoodsSearchResultVO();
Long pageNo = goodsSearch.getPageNo();
Long pageSize = goodsSearch.getPageSize();
try {
SearchSourceBuilder searchSourceBuilder = this.createQuery(goodsSearch);
//如果不为空 则表示关键词搜索
if (!StringUtil.isEmpty(goodsSearch.getKeyword())) {
//搜索关键字消息
GoodsSearchMessage goodsSearchMessage = new GoodsSearchMessage(goodsSearch.getKeyword());
this.messageSender.send(goodsSearchMessage);
}
//设置分页信息 设置是否按查询匹配度排序
searchSourceBuilder.from((pageNo.intValue() - 1) * pageSize.intValue()).size(pageSize.intValue()).explain(true);
//分类
AggregationBuilder categoryTermsBuilder = AggregationBuilders.terms("categoryAgg").field("categoryId").size(Integer.MAX_VALUE);
//品牌
AggregationBuilder brandTermsBuilder = AggregationBuilders.terms("brandAgg").field("brand").size(Integer.MAX_VALUE);
//参数
AggregationBuilder valuesBuilder = AggregationBuilders.terms("valueAgg").field("params.value").size(Integer.MAX_VALUE);

AggregationBuilder paramsNameBuilder = AggregationBuilders.terms("nameAgg").field("params.name").subAggregation(valuesBuilder).size(Integer.MAX_VALUE);
if(i18n){
AggregationBuilder langBuilder = AggregationBuilders.terms("langAgg").field("params.lang").size(Integer.MAX_VALUE);
paramsNameBuilder.subAggregation(langBuilder);
}//endif
AggregationBuilder avgBuild = AggregationBuilders.nested("paramsAgg", "params").subAggregation(paramsNameBuilder);
searchSourceBuilder.aggregation(categoryTermsBuilder);
searchSourceBuilder.aggregation(brandTermsBuilder);
searchSourceBuilder.aggregation(avgBuild);
// Elasticsearch 搜索
ElasticSearchResult elasticSearchResult = ElasticOperationUtil.search(jestClient, elasticJestConfig.getIndexName() + "_" + EsSettings.GOODS_INDEX_NAME, searchSourceBuilder.toString());
if (elasticSearchResult == null) {
return goodsSearchResult;
}
List<GoodsSearchLine> searchLines = elasticSearchResult.getSourceAsObjectList(GoodsSearchLine.class, false);

if (i18n) {
//根据当前用户选择的语言显示商品名称
Locale currentLocale = LocaleContext.getCurrentLocale();
String tag = currentLocale.toLanguageTag();

searchLines = searchLines.stream().map(item -> item.getLanguages().stream()
.filter(tar -> Objects.equals(tar.getLang(), tag))
.findFirst()
.map(tar -> {
item.setName(tar.getGoodsName());
return item;
}).orElse(item))
.collect(Collectors.toList());
}//endif

//商品分页数据
WebPage webPage = new WebPage(pageNo, elasticSearchResult.getTotal(), pageSize, searchLines);

//选择器数据:分类、品牌、参数
Map<String, Object> selectorMap = new HashMap<>(16);
MetricAggregation agg = elasticSearchResult.getAggregations();
//分类
TermsAggregation categoryAgg = agg.getTermsAggregation("categoryAgg");
List<TermsAggregation.Entry> categoryBuckets = categoryAgg.getBuckets();
List<CategoryVO> allCatList = this.categoryManager.getByParentId(0L, true);
List<SearchSelector> catDim = SelectorUtil.createCatSelector(categoryBuckets, allCatList, goodsSearch.getCategory());
selectorMap.put("cat", catDim);

String catPath = null;
if (goodsSearch.getCategory() != null) {
CategoryDO cat = categoryManager.getModel(goodsSearch.getCategory());
String path = cat.getCategoryPath();
catPath = path.replace("|", Separator.SEPARATOR_PROP_VLAUE).substring(0, path.length() - 1);
}
//已经选择的分类
List<SearchSelector> selectedCat = CatUrlUtils.getCatDimSelected(allCatList, catPath);
selectorMap.put("selected_cat", selectedCat);
//品牌
TermsAggregation brandAgg = agg.getTermsAggregation("brandAgg");
List<TermsAggregation.Entry> brandBuckets = brandAgg.getBuckets();
List<BrandDO> brandList = brandManager.getAllBrands();
List<SearchSelector> brandDim = SelectorUtil.createBrandSelector(brandBuckets, brandList);
selectorMap.put("brand", brandDim);
//参数
FilterAggregation paramsAgg = agg.getFilterAggregation("paramsAgg");
TermsAggregation nameTerms = paramsAgg.getTermsAggregation("nameAgg");
List<PropSelector> paramDim = SelectorUtil.createParamSelector(nameTerms);
selectorMap.put("prop", paramDim);

goodsSearchResult.setGoodsData(webPage);
goodsSearchResult.setSelectorData(selectorMap);
} catch (Exception e) {
goodsSearchResult.setGoodsData(new WebPage(pageNo, 0L, pageSize, new ArrayList()));
goodsSearchResult.setSelectorData(new HashMap());
e.printStackTrace();
}
return goodsSearchResult;
}