商品搜索架构文档
概述
用户端搜索商品,可按以下条件进行搜索:
商品名称关键字、分类、品牌、参数和价格范围
商品列表可以按照以下条件进行排序显示:
默认排序(搜索优先级)、销量、价格和好评率
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内容结构如下:
cat | List<SearchSelector> |
---|---|
selected_cat | List<SearchSelector> |
brand | List<SearchSelector> |
prop | List<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
}
]
}
}
类图展示
代码展示
构建搜索条件
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;
}