前端 + 后端:商品搜索

This commit is contained in:
YunaiV
2019-04-24 21:47:43 +08:00
parent 68dadab873
commit b98e21e157
29 changed files with 770 additions and 125 deletions

View File

@@ -0,0 +1,9 @@
package cn.iocoder.mall.search.biz.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories;
@Configuration
@EnableElasticsearchRepositories(basePackages = "cn.iocoder.mall.search.biz.dao")
public class JPAConfiguration {
}

View File

@@ -0,0 +1,39 @@
package cn.iocoder.mall.search.biz.convert;
import cn.iocoder.mall.order.api.bo.CalcSkuPriceBO;
import cn.iocoder.mall.product.api.bo.ProductSpuDetailBO;
import cn.iocoder.mall.promotion.api.bo.PromotionActivityBO;
import cn.iocoder.mall.search.api.bo.ESProductBO;
import cn.iocoder.mall.search.biz.dataobject.ESProductDO;
import org.mapstruct.Mapper;
import org.mapstruct.Mappings;
import org.mapstruct.factory.Mappers;
import java.util.List;
@Mapper
public interface ProductSearchConvert {
ProductSearchConvert INSTANCE = Mappers.getMapper(ProductSearchConvert.class);
@Mappings({})
ESProductDO convert(ProductSpuDetailBO spu);
@Mappings({})
default ESProductDO convert(ProductSpuDetailBO spu, CalcSkuPriceBO calcSkuPrice) {
// Spu 的基础数据
ESProductDO product = this.convert(spu);
product.setOriginalPrice(calcSkuPrice.getOriginalPrice()).setBuyPrice(calcSkuPrice.getBuyPrice());
// 设置促销活动相关字段
if (calcSkuPrice.getTimeLimitedDiscount() != null) {
PromotionActivityBO activity = calcSkuPrice.getTimeLimitedDiscount();
product.setPromotionActivityId(activity.getId()).setPromotionActivityTitle(activity.getTitle())
.setPromotionActivityType(activity.getActivityType());
}
// 返回
return product;
}
List<ESProductBO> convert(List<ESProductDO> list);
}

View File

@@ -1,13 +1,67 @@
package cn.iocoder.mall.search.biz.dao;
import cn.iocoder.common.framework.util.CollectionUtil;
import cn.iocoder.common.framework.util.StringUtil;
import cn.iocoder.common.framework.vo.SortingField;
import cn.iocoder.mall.search.biz.dataobject.ESProductDO;
import org.elasticsearch.common.lucene.search.function.FunctionScoreQuery;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.query.functionscore.FunctionScoreQueryBuilder;
import org.elasticsearch.index.query.functionscore.ScoreFunctionBuilders;
import org.elasticsearch.search.sort.SortBuilders;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import static org.elasticsearch.index.query.QueryBuilders.matchQuery;
@Repository
public interface ProductRepository extends ElasticsearchRepository<ESProductDO, Integer> {
@Deprecated
ESProductDO findByName(String name);
default Page<ESProductDO> search(Integer cid, String keyword, Integer pageNo, Integer pageSize,
List<SortingField> sortFields) {
NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder()
.withPageable(PageRequest.of(pageNo - 1, pageSize));
// 筛选条件 cid
if (cid != null) {
nativeSearchQueryBuilder.withFilter(QueryBuilders.termQuery("cid", cid));
}
// 筛选
if (StringUtil.hasText(keyword)) {
FunctionScoreQueryBuilder.FilterFunctionBuilder[] functions = { // TODO 芋艿,分值随便打的
new FunctionScoreQueryBuilder.FilterFunctionBuilder(matchQuery("name", keyword),
ScoreFunctionBuilders.weightFactorFunction(10)),
new FunctionScoreQueryBuilder.FilterFunctionBuilder(matchQuery("sellPoint", keyword),
ScoreFunctionBuilders.weightFactorFunction(2)),
new FunctionScoreQueryBuilder.FilterFunctionBuilder(matchQuery("categoryName", keyword),
ScoreFunctionBuilders.weightFactorFunction(3)),
// new FunctionScoreQueryBuilder.FilterFunctionBuilder(matchQuery("description", keyword),
// ScoreFunctionBuilders.weightFactorFunction(2)), // TODO 芋艿,目前这么做,如果商品描述很长,在按照价格降序,会命中超级多的关键字。
};
FunctionScoreQueryBuilder functionScoreQueryBuilder = QueryBuilders.functionScoreQuery(functions)
.scoreMode(FunctionScoreQuery.ScoreMode.SUM)
.setMinScore(2F); // TODO 芋艿,需要考虑下 score
nativeSearchQueryBuilder.withQuery(functionScoreQueryBuilder);
} else {
nativeSearchQueryBuilder.withQuery(QueryBuilders.matchAllQuery());
}
// 排序
if (CollectionUtil.isEmpty(sortFields)) {
nativeSearchQueryBuilder.withSort(SortBuilders.scoreSort().order(SortOrder.DESC));
} else {
sortFields.forEach(sortField -> nativeSearchQueryBuilder.withSort(SortBuilders.fieldSort(sortField.getField())
.order(SortOrder.fromString(sortField.getOrder()))));
}
// 执行查询
return search(nativeSearchQueryBuilder.build());
}
}

View File

@@ -1,35 +1,40 @@
package cn.iocoder.mall.search.biz.service;
import cn.iocoder.common.framework.util.CollectionUtil;
import cn.iocoder.common.framework.vo.CommonResult;
import cn.iocoder.common.framework.vo.SortingField;
import cn.iocoder.mall.order.api.CartService;
import cn.iocoder.mall.order.api.bo.CalcSkuPriceBO;
import cn.iocoder.mall.product.api.ProductSpuService;
import cn.iocoder.mall.product.api.bo.ProductSpuDetailBO;
import cn.iocoder.mall.search.api.ProductSearchService;
import cn.iocoder.mall.search.api.bo.ESProductPageBO;
import cn.iocoder.mall.search.api.dto.ProductSearchPageDTO;
import cn.iocoder.mall.search.biz.convert.ProductSearchConvert;
import cn.iocoder.mall.search.biz.dao.ProductRepository;
import cn.iocoder.mall.search.biz.dataobject.ESProductDO;
import com.alibaba.dubbo.config.annotation.Reference;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import java.util.Comparator;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
@Service
@com.alibaba.dubbo.config.annotation.Service(validation = "true")
public class ProductSearchServiceImpl implements ProductSearchService {
private static final Integer REBUILD_FETCH_PER_SIZE = 2;
private static final Integer REBUILD_FETCH_PER_SIZE = 100;
@Autowired
private ProductRepository productRepository;
@Autowired
@Reference(validation = "true")
private ProductSpuService productSpuService;
@Autowired
@Reference(validation = "true")
private CartService cartService;
@Override
@@ -39,17 +44,12 @@ public class ProductSearchServiceImpl implements ProductSearchService {
int rebuildCounts = 0;
while (true) {
CommonResult<List<ProductSpuDetailBO>> result = productSpuService.getProductSpuDetailListForSync(lastId, REBUILD_FETCH_PER_SIZE);
Assert.isTrue(result.isError(), "获得商品列表必然成功");
Assert.isTrue(result.isSuccess(), "获得商品列表必然成功");
List<ProductSpuDetailBO> spus = result.getData();
rebuildCounts += spus.size();
// 存储到 ES 中
List<ESProductDO> products = spus.stream().map(new Function<ProductSpuDetailBO, ESProductDO>() {
@Override
public ESProductDO apply(ProductSpuDetailBO spu) {
return convert(spu);
}
}).collect(Collectors.toList());
List<ESProductDO> products = spus.stream().map(this::convert).collect(Collectors.toList());
productRepository.saveAll(products);
// 设置新的 lastId ,或者结束
if (spus.size() < REBUILD_FETCH_PER_SIZE) {
break;
@@ -66,14 +66,30 @@ public class ProductSearchServiceImpl implements ProductSearchService {
ProductSpuDetailBO.Sku sku = spu.getSkus().stream().min(Comparator.comparing(ProductSpuDetailBO.Sku::getPrice)).get();
// 价格计算
CommonResult<CalcSkuPriceBO> calSkuPriceResult = cartService.calcSkuPrice(sku.getId());
Assert.isTrue(calSkuPriceResult.isError(), String.format("SKU(%d) 价格计算不会出错", sku.getId()));
return new ESProductDO();
Assert.isTrue(calSkuPriceResult.isSuccess(), String.format("SKU(%d) 价格计算不会出错", sku.getId()));
// 拼装结果
return ProductSearchConvert.INSTANCE.convert(spu, calSkuPriceResult.getData());
}
@Override
public CommonResult searchPage(ProductSearchPageDTO searchPageDTO) {
return null;
public CommonResult<ESProductPageBO> searchPage(ProductSearchPageDTO searchPageDTO) {
checkSortFieldInvalid(searchPageDTO.getSorts());
// 执行查询
Page<ESProductDO> searchPage = productRepository.search(searchPageDTO.getCid(), searchPageDTO.getKeyword(),
searchPageDTO.getPageNo(), searchPageDTO.getPageSize(), searchPageDTO.getSorts());
// 转换结果
ESProductPageBO resultPage = new ESProductPageBO()
.setList(ProductSearchConvert.INSTANCE.convert(searchPage.getContent()))
.setTotal((int) searchPage.getTotalElements());
return CommonResult.success(resultPage);
}
private void checkSortFieldInvalid(List<SortingField> sorts) {
if (CollectionUtil.isEmpty(sorts)) {
return;
}
sorts.forEach(sortingField -> Assert.isTrue(ProductSearchPageDTO.SORT_FIELDS.contains(sortingField.getField()),
String.format("排序字段(%s) 不在允许范围内", sortingField.getField())));
}
}

View File

@@ -1,4 +1,4 @@
#
# es
spring:
data:
elasticsearch:
@@ -6,6 +6,7 @@ spring:
cluster-nodes: 192.168.88.10:9300
repositories:
enable: true
# dubbo
dubbo:
application:
@@ -16,4 +17,4 @@ dubbo:
port: -1
name: dubbo
scan:
base-packages: cn.iocoder.mall.search.service.biz
base-packages: cn.iocoder.mall.search.biz.service

View File

@@ -1,12 +1,15 @@
package cn.iocoder.mall.search.biz.dao;
import cn.iocoder.mall.search.biz.dataobject.ESProductDO;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import java.util.List;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
public class ProductRepositoryTest {
@@ -15,6 +18,7 @@ public class ProductRepositoryTest {
private ProductRepository productRepository;
@Test
@Ignore
public void testSave() {
// productRepository.deleteById(1);
ESProductDO product = new ESProductDO()
@@ -24,9 +28,23 @@ public class ProductRepositoryTest {
}
@Test
@Ignore
public void testFindByName() {
ESProductDO product = productRepository.findByName("锤子");
System.out.println(product);
}
@Test
public void testSearch() {
// Page<ESProductDO> page = productRepository.search(639, null, 1, 10);
// console(page.getContent());
// Page<ESProductDO> page = productRepository.search(null, "数据库Oracle", 1, 10);
// console(page.getContent());
}
private void console(List<ESProductDO> list) {
list.forEach(System.out::println);
}
}

View File

@@ -0,0 +1,27 @@
package cn.iocoder.mall.search.biz.service;
import cn.iocoder.mall.search.biz.dao.ProductRepository;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
public class ProductSearchServiceImplTest {
@Autowired
private ProductSearchServiceImpl productSearchService;
@Autowired
private ProductRepository productRepository;
@Test
public void testRebuild() {
int counts = productSearchService.rebuild().getData();
System.out.println("重建数量:" + counts);
System.out.println(productRepository.count());
}
}