Day 6 商品搜索
这里会总结构建项目过程中遇到的问题、主要流程,以及一些个人思考!!
学习方法:
1 github源码 + 文档 + 官网
2 内容复现 ,实际操作
项目源码同步更新到github 欢迎大家star~ 后期会更新并上传前端项目
搜索相关实体
在ES中存储的商品实体类与数据库中的商品实体类不同,且商品的搜索条件和搜索结果都有相应的实体类。
Q: 为什么这两种实体类不同?//todo
/**
* 在ES中存储的商品实体类
*/
@Document(indexName = "goods",createIndex = false)
@Data
public class GoodsES implements Serializable {
@Field
private Long id; // 商品id
@Field
private String goodsName; // 商品名称
@Field
private String caption; // 副标题
@Field
private BigDecimal price; // 价格
@Field
private String headerPic; // 头图
@Field
private String brand; // 品牌名称
@CompletionField
private List<String> tags; // 关键字
@Field
private List<String> productType; // 类目名
@Field
private Map<String,List<String>> specification; // 规格,键为规格项,值为规格值
}
/**
* 商品搜索条件
*/
@Data
public class GoodsSearchParam implements Serializable {
private String keyword; // 关键字
private String brand; // 品牌名
private Double highPrice; //最高价
private Double lowPrice; //最低价
private Map<String,String> specificationOption; // 规格map, 键:规格名,值:规格值
private String sortFiled; //排序字段 NEW:新品 PRICE:价格
private String sort; //排序方式 ASC:升序 DESC:降序
private Integer page; //页码
private Integer size; //每页条数
}
/**
* 商品搜索结果
*/
@Data
public class GoodsSearchResult implements Serializable {
private Page<GoodsES> goodsPage; // 页面商品信息
private GoodsSearchParam goodsSearchParam; // 搜索条件回显
private Set<String> brands; // 和商品有关的品牌列表
private Set<String> productType; // 和商品有关的类别列表
// 和商品有关的规格列表,键:规格名,值:规格集合
private Map<String, Set<String>> specifications;
}
创建创建商品索引
Kinbana是一个es的开源分析与可视化平台
PUT /goods
{
"settings": {
"number_of_shards": 5,
"number_of_replicas": 1,
"analysis": {
"analyzer": {
"ik_pinyin": {
"tokenizer": "ik_smart",
"filter": "pinyin_filter"
},
"tag_pinyin": {
"tokenizer": "keyword",
"filter": "pinyin_filter"
}
},
"filter": {
"pinyin_filter": {
"type": "pinyin",
"keep_joined_full_pinyin": true,
"keep_original": true,
"remove_duplicated_term": true
}
}
}
},
"mappings": {
"properties": {
"id": {
"type": "long",
"index": true
},
"goodsName": {
"type": "text",
"index": true,
"analyzer": "ik_pinyin",
"search_analyzer": "ik_pinyin"
},
"caption": {
"type": "text",
"index": true,
"analyzer": "ik_pinyin",
"search_analyzer": "ik_pinyin"
},
"tags": {
"type": "completion",
"analyzer": "tag_pinyin",
"search_analyzer": "tag_pinyin"
},
"price": {
"type": "double",
"index": true
},
"headerPic": {
"type": "keyword",
"index": true
},
"brand": {
"type": "keyword",
"index": true
},
"productType": {
"type": "keyword",
"index": true
},
"specification":{
"properties": {
"specificationName":{
"type": "keyword",
"index": true
},
"specificationOption":{
"type": "keyword",
"index": true
}
}
}
}
}
}
创建搜索模块
# 端口号
server:
port: 8004
# 日志格式
logging:
pattern:
console: '%d{HH:mm:ss.SSS} %clr(%-5level) --- [%-15thread] %cyan(%-50logger{50}):%msg%n'
# Nacos
spring:
application:
name: shopping_search_customer_api
cloud:
nacos:
discovery:
server-addr: 192.168.66.100:8848
dubbo:
application:
#项目名字
name: shopping_search_customer_api
protocol:
name: dubbo
port: -1
registry:
# 注册地址
address: nacos://192.168.66.100:8848
# 端口号
server:
port: 9008
# 日志格式
logging:
pattern:
console: '%d{HH:mm:ss.SSS} %clr(%-5level) --- [%-15thread] %cyan(%-50logger{50}):%msg%n'
spring:
# elasticsearch
elasticsearch:
uris: http://192.168.66.100:9200
application:
name: shopping_search_service #服务名
cloud:
nacos:
discovery:
server-addr: 192.168.66.100:8848 # 注册中心地址
dubbo:
application:
name: shopping_search_service # 项目名
serialize-check-status: DISABLE
check-serializable: false
protocol:
name: dubbo # 通讯协议
port: -1 # 端口号,-1表示自动扫描可用端口。
registry:
address: nacos://192.168.66.100:8848 # 注册中心
服务的启动类需要配置分页插件
@EnableDiscoveryClient
@EnableDubbo
@RefreshScope
@SpringBootApplication(exclude= {DataSourceAutoConfiguration.class})
public class ShoppingSearchServiceApplication {
public static void main(String[] args) {
SpringApplication.run(ShoppingSearchServiceApplication.class, args);
}
// 分页插件
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
编写搜索相关的接口
public interface SearchService {
/**
* 自动补齐关键字
* @param keyword 被补齐的词
* @return 补齐的关键词集合
*/
List<String> autoSuggest(String keyword);
/**
* 搜索商品
* @param goodsSearchParam 搜索条件
* @return 搜索结果
*/
GoodsSearchResult search(GoodsSearchParam goodsSearchParam);
/**
* 向ES同步数据库中的商品数据
* @param goodsDesc 商品详情
*/
void syncGoodsToES(GoodsDesc goodsDesc);
/**
* 删除ES中的商品数据
* @param id 商品id
*/
void delete(Long id);
}
查询详情
/**
* 商品详情
*/
@Data
public class GoodsDesc implements Serializable {
private Long id; // 商品id
private String goodsName; // 商品名称
private String caption; // 副标题
private BigDecimal price; // 价格
private String headerPic; // 头图
private Boolean isMarketable; // 是否上架
private String introduction; // 商品介绍
private Brand brand; // 品牌
private ProductType productType1; // 一级类目
private ProductType productType2; // 二级类目id
private ProductType productType3; // 三级类目id
private List<GoodsImage> images; // 商品图片
private List<Specification> specifications; // 商品规格
}
在商品服务Mapper中添加
List<GoodsDesc> findAll();
编写mapper映射xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.bootscoder.shopping_goods_service.mapper.GoodsMapper">
<insert id="addGoodsSpecificationOption">
INSERT INTO boots_goods_specification_option VALUES (#{gid},#{optionId})
</insert>
<delete id="deleteGoodsSpecificationOption" parameterType="long">
DELETE FROM boots_goods_specification_option WHERE gid = #{gid}
</delete>
<update id="putAway">
UPDATE boots_goods SET isMarketable = #{isMarketable} WHERE id = #{id}
</update>
<resultMap id="goodsMapper" type="com.bootscoder.shopping_common.pojo.Goods">
<id property="id" column="bid"></id>
<result property="goodsName" column="goodsName"></result>
<result property="caption" column="caption"></result>
<result property="price" column="price"></result>
<result property="headerPic" column="headerPic"></result>
<result property="isMarketable" column="isMarketable"></result>
<result property="introduction" column="introduction"></result>
<result property="brandId" column="brandId"></result>
<result property="productType1Id" column="productType1Id"></result>
<result property="productType2Id" column="productType2Id"></result>
<result property="productType3Id" column="productType3Id"></result>
<collection property="images" column="bid" ofType="com.bootscoder.shopping_common.pojo.GoodsImage">
<id property="id" column="imageId"></id>
<result property="imageTitle" column="imageTitle"></result>
<result property="imageUrl" column="imageUrl"></result>
</collection>
<collection property="specifications" column="bid" ofType="com.bootscoder.shopping_common.pojo.Specification">
<id property="id" column="specificationId"></id>
<result property="specName" column="specName"></result>
<result property="productTypeId" column="productTypeId"></result>
<collection property="specificationOptions" column="specificationId" ofType="com.bootscoder.shopping_common.pojo.SpecificationOption">
<id property="id" column="optionId"></id>
<result property="optionName" column="optionName"></result>
</collection>
</collection>
</resultMap>
<select id="findById" parameterType="long" resultMap="goodsMapper">
SELECT
boots_goods.id AS bid,
boots_goods.goodsName AS goodsName,
boots_goods.caption AS caption,
boots_goods.price AS price,
boots_goods.headerPic AS headerPic,
boots_goods.introduction AS introduction,
boots_goods.isMarketable AS isMarketable,
boots_goods.brandId AS brandId,
boots_goods.productType1Id AS productType1Id,
boots_goods.productType2Id AS productType2Id,
boots_goods.productType3Id AS productType3Id,
boots_goods_image.id AS imageId,
boots_goods_image.imageTitle AS imageTitle,
boots_goods_image.imageUrl AS imageUrl,
boots_specification.id AS specificationId,
boots_specification.specName AS specName,
boots_specification.productTypeId AS productTypeId,
boots_specification_option.id AS optionId,
boots_specification_option.optionName AS optionName
FROM
boots_goods
LEFT JOIN boots_goods_image ON boots_goods.id = boots_goods_image.goodsId
LEFT JOIN boots_goods_specification_option ON boots_goods.id = boots_goods_specification_option.gid
LEFT JOIN boots_specification_option ON boots_goods_specification_option.optionId = boots_specification_option.id
LEFT JOIN boots_specification ON boots_specification_option.specId = boots_specification.id
WHERE
boots_goods.id = #{gid}
</select>
<resultMap id="goodsDescMapper" type="com.bootscoder.shopping_common.pojo.GoodsDesc">
<id property="id" column="bid"></id>
<result property="goodsName" column="goodsName"></result>
<result property="caption" column="caption"></result>
<result property="price" column="price"></result>
<result property="headerPic" column="headerPic"></result>
<result property="isMarketable" column="isMarketable"></result>
<result property="introduction" column="introduction"></result>
<association property="brand" column="brandId" javaType="com.bootscoder.shopping_common.pojo.Brand">
<id property="id" column="brandId"></id>
<result property="name" column="brandName"></result>
</association>
<association property="productType1" column="type1Id" javaType="com.bootscoder.shopping_common.pojo.ProductType">
<id property="id" column="type1Id"></id>
<result property="name" column="type1Name"></result>
<result property="level" column="type1Level"></result>
<result property="parentId" column="type1parentId"></result>
</association>
<association property="productType2" column="type2Id" javaType="com.bootscoder.shopping_common.pojo.ProductType">
<id property="id" column="type2Id"></id>
<result property="name" column="type2Name"></result>
<result property="level" column="type2Level"></result>
<result property="parentId" column="type2parentId"></result>
</association>
<association property="productType3" column="type3Id" javaType="com.bootscoder.shopping_common.pojo.ProductType">
<id property="id" column="type3Id"></id>
<result property="name" column="type3Name"></result>
<result property="level" column="type3Level"></result>
<result property="parentId" column="type3parentId"></result>
</association>
<collection property="images" column="bid" ofType="com.bootscoder.shopping_common.pojo.GoodsImage">
<id property="id" column="imageId"></id>
<result property="imageTitle" column="imageTitle"></result>
<result property="imageUrl" column="imageUrl"></result>
</collection>
<collection property="specifications" column="bid" ofType="com.bootscoder.shopping_common.pojo.Specification">
<id property="id" column="specificationId"></id>
<result property="specName" column="specName"></result>
<result property="productTypeId" column="productTypeId"></result>
<collection property="specificationOptions" column="specificationId" ofType="com.bootscoder.shopping_common.pojo.SpecificationOption">
<id property="id" column="optionId"></id>
<result property="optionName" column="optionName"></result>
</collection>
</collection>
</resultMap>
<select id="findAll" resultMap="goodsDescMapper">
SELECT
boots_goods.id bid,
boots_goods.goodsName goodsName,
boots_goods.caption caption,
boots_goods.price price,
boots_goods.headerPic headerPic,
boots_goods.introduction introduction,
boots_goods.isMarketable isMarketable,
boots_goods.brandId brandId,
boots_brand.`name` brandName,
type1.id type1Id,
type1.`name` type1Name,
type1.level type1Level,
type1.parentId type1parentId,
type2.id type2Id,
type2.`name` type2Name,
type2.level type2Level,
type2.parentId type2parentId,
type3.id type3Id,
type3.`name` type3Name,
type3.level type3Level,
type3.parentId type3parentId,
boots_goods_image.id imageId,
boots_goods_image.imageTitle imageTitle,
boots_goods_image.imageUrl imageUrl,
boots_specification.id specificationId,
boots_specification.specName specName,
boots_specification.productTypeId productTypeId,
boots_specification_option.id optionId,
boots_specification_option.optionName optionName
FROM
boots_goods,
boots_goods_image,
boots_brand,
boots_specification,
boots_specification_option,
boots_goods_specification_option,
boots_product_type AS type1,
boots_product_type AS type2,
boots_product_type AS type3
WHERE boots_goods.id = boots_goods_specification_option.gid
AND boots_goods_specification_option.optionId = boots_specification_option.id
AND boots_specification_option.specId = boots_specification.id
AND boots_goods.brandId = boots_brand.id
AND boots_goods.id = boots_goods_image.goodsId
AND boots_goods.productType1Id = type1.id
AND boots_goods.productType2Id = type2.id
AND boots_goods.productType3Id = type3.id
</select>
<select id="findDesc" resultMap="goodsDescMapper">
SELECT
boots_goods.id bid,
boots_goods.goodsName goodsName,
boots_goods.caption caption,
boots_goods.price price,
boots_goods.headerPic headerPic,
boots_goods.introduction introduction,
boots_goods.isMarketable isMarketable,
boots_goods.brandId brandId,
boots_brand.`name` brandName,
type1.id type1Id,
type1.`name` type1Name,
type1.level type1Level,
type1.parentId type1parentId,
type2.id type2Id,
type2.`name` type2Name,
type2.level type2Level,
type2.parentId type2parentId,
type3.id type3Id,
type3.`name` type3Name,
type3.level type3Level,
type3.parentId type3parentId,
boots_goods_image.id imageId,
boots_goods_image.imageTitle imageTitle,
boots_goods_image.imageUrl imageUrl,
boots_specification.id specificationId,
boots_specification.specName specName,
boots_specification.productTypeId productTypeId,
boots_specification_option.id optionId,
boots_specification_option.optionName optionName
FROM
boots_goods,
boots_goods_image,
boots_brand,
boots_specification,
boots_specification_option,
boots_goods_specification_option,
boots_product_type AS type1,
boots_product_type AS type2,
boots_product_type AS type3
WHERE boots_goods.id = boots_goods_specification_option.gid
AND boots_goods_specification_option.optionId = boots_specification_option.id
AND boots_specification_option.specId = boots_specification.id
AND boots_goods.brandId = boots_brand.id
AND boots_goods.id = boots_goods_image.goodsId
AND boots_goods.productType1Id = type1.id
AND boots_goods.productType2Id = type2.id
AND boots_goods.productType3Id = type3.id
AND boots_goods.id = #{id}
</select>
</mapper>
修改商品服务接口和实现类
提问:
//todo
- mapper 和Service 层的内容有什么不同? 为什么这样设计
- 商品实体和详情有何区别?为什么这样设计
测试查询所有商品详情
@SpringBootTest
class ShoppingGoodsServiceApplicationTests {
@Autowired
GoodsService goodsService;
@Test
void contextLoads() {
System.out.println(goodsService.findAll());
}
}
编写分词方法
在向ES添加数据时,我们需要将数据库的一些字段进行分词作为商品的关键词,方便编写补齐关键词功能,在搜索服务接口实现类编写分词方法:
这里,关键字是 商品名称 + 副标题的分词结果
@DubboService
@Service
public class SearchServiceImpl implements SearchService {
@Autowired
private ElasticsearchClient client;
@Autowired
private GoodsESRepository goodsESRepository;
@Autowired
private ElasticsearchTemplate template;
/**
* 分词
*
* @param text 被分词的文本
* @param analyzer 分词器
* @return 分词结果
*/
@SneakyThrows // 抛出已检查异常
public List<String> analyze(String text, String analyzer) {
// 分词请求
AnalyzeRequest request = AnalyzeRequest.of(a ->
a.index("goods").analyzer(analyzer).text(text)
);
// 发送分词请求,获取相应结果
AnalyzeResponse response = client.indices().analyze(request);
// 处理相应结果
List<String> words = new ArrayList(); // 分词结果集合
List<AnalyzeToken> tokens = response.tokens();
for (AnalyzeToken token : tokens) {
String term = token.token();// 分出的词
words.add(term);
}
return words;
}
}
在测试时,发现JAVA项目无法连接ES,这是由于ES默认不允许远程访问。kibana由于和ES在同一台服务器下所以可以访问,JAVA程序在开发电脑中所以无法访问。我们修改es的配置文件,开启远程访问功能:
-
打开ES配置文件
vim /usr/local/elasticsearch/config/elasticsearch.yml
-
添加如下内容
# 单体ES环境 discovery.type: single-node # 允许所有路径访问 network.host: 0.0.0.0
-
重启ES和kibana。
ps -ef | grep elasticsearch
kill -9 id
./elasticsearch -d
-d 后台运行
分词测试成功
@Test
void contextLoads() {
List<String> analyze = searchService.analyze("我爱敲代码,我爱读书", "ik_pinyin");
System.out.println(analyze);
}
向ES中同步数据
在搜索服务模块编写操作ES商品文档的Repository接口
//todo 这个接口的作用是什么?
@Repository
public interface GoodsESRepository extends ElasticsearchRepository<GoodsES,Long> {
}
在搜素服务模块编写同步数据的方法
// 向ES同步商品数据
@Override
public void syncGoodsToES(GoodsDesc goodsDesc) {
// 将商品详情对象转为GoodsES对象
GoodsES goodsES = new GoodsES();
goodsES.setId(goodsDesc.getId());
goodsES.setGoodsName(goodsDesc.getGoodsName());
goodsES.setCaption(goodsDesc.getCaption());
goodsES.setPrice(goodsDesc.getPrice());
goodsES.setHeaderPic(goodsDesc.getHeaderPic());
goodsES.setBrand(goodsDesc.getBrand().getName());
// 类型集合
List<String> productType = new ArrayList();
productType.add(goodsDesc.getProductType1().getName());
productType.add(goodsDesc.getProductType2().getName());
productType.add(goodsDesc.getProductType3().getName());
goodsES.setProductType(productType);
// 规格集合
Map<String,List<String>> map = new HashMap();
List<Specification> specifications = goodsDesc.getSpecifications();
// 遍历规格
for (Specification specification : specifications) {
// 规格项集合
List<SpecificationOption> options = specification.getSpecificationOptions();
// 规格项名集合
List<String> optionStrList = new ArrayList();
for (SpecificationOption option : options) {
optionStrList.add(option.getOptionName());
}
map.put(specification.getSpecName(),optionStrList);
}
goodsES.setSpecification(map);
// 关键字
List<String> tags = new ArrayList();
tags.add(goodsDesc.getBrand().getName()); //品牌名是关键字
tags.addAll(analyze(goodsDesc.getGoodsName(),"ik_smart"));//商品名分词后为关键词
tags.addAll(analyze(goodsDesc.getCaption(),"ik_smart"));//副标题分词后为关键词
goodsES.setTags(tags);
// 将GoodsES对象存入ES
goodsESRepository.save(goodsES);
}
测试同步方法
@SpringBootTest
class ShoppingSearchServiceApplicationTests {
@DubboReference
private GoodsService goodsService;
@Test
void testSyncGoodsToES(){
List<GoodsDesc> goods = goodsService.findAll();
for (GoodsDesc goodsDesc : goods) {
// 如果商品是上架状态
if (goodsDesc.getIsMarketable()){
searchService.syncGoodsToES(goodsDesc);
}
}
}
}
先启动商品服务,再执行测试类,同步所有商品。
注:同步成功后需要注释测试类,否则启动搜索服务前必须启动商品服务。因为测试类中注入了另一个服务
GET /goods/_search
{
"query": {
"match_all": {}
}
}
编写es的简单查询语句可以看到成功查到 --> 即同步成功
商品搜索自动补全(关键字)
源码中有不少函数式接口 nice
//todo 了解什么是函数式接口
// 自动补齐
@SneakyThrows
@Override
public List<String> autoSuggest(String keyword) {
// 1.自动补齐查询条件
Suggester suggester = Suggester.of(
s -> s.suggesters("prefix_suggestion", FieldSuggester.of(
fs -> fs.completion(
cs -> cs.skipDuplicates(true)
.size(10)
.field("tags")
)
)).text(keyword)
);
// 2.自动补齐查询
SearchResponse<Map> response = client.search(s -> s.index("goods")
.suggest(suggester), Map.class);
// 3.处理查询结果
Map resultMap = response.suggest();
List<Suggestion> suggestionList = (List) resultMap.get("prefix_suggestion");
Suggestion suggestion = suggestionList.get(0);
List<CompletionSuggestOption> resultList = suggestion.completion().options();
List<String> result = new ArrayList();
for (CompletionSuggestOption completionSuggestOption : resultList) {
String text = completionSuggestOption.text();
result.add(text);
}
return result;
}
在higress 中配置网关
测试自动补齐成功
编写商品搜索功能
@DubboService
@Service
public class SearchServiceImpl implements SearchService {
@Autowired
private ElasticsearchClient client;
@Autowired
private GoodsESRepository goodsESRepository;
@Autowired
private ElasticsearchTemplate template;
/**
* 分词
*
* @param text 被分词的文本
* @param analyzer 分词器
* @return 分词结果
*/
@SneakyThrows
public List<String> analyze(String text, String analyzer) {
// 创建分词请求
AnalyzeRequest request = AnalyzeRequest.of(a -> a.index("goods").analyzer(analyzer).text(text));
// 发送分词请求
AnalyzeResponse response = client.indices().analyze(request);
// 处理分词结果
List<String> words = new ArrayList();
List<AnalyzeToken> tokens = response.tokens();
for (AnalyzeToken token : tokens) {
String term = token.token();
words.add(term);
}
return words;
}
// 自动补齐
@SneakyThrows
@Override
public List<String> autoSuggest(String keyword) {
// 1.自动补齐查询条件
Suggester suggester = Suggester.of(
s -> s.suggesters("prefix_suggestion", FieldSuggester.of(
fs -> fs.completion(
cs -> cs.skipDuplicates(true)
.size(10)
.field("tags")
)
)).text(keyword)
);
// 2.自动补齐查询
SearchResponse<Map> response = client.search(s -> s.index("goods")
.suggest(suggester), Map.class);
// 3.处理查询结果
Map resultMap = response.suggest();
List<Suggestion> suggestionList = (List) resultMap.get("prefix_suggestion");
Suggestion suggestion = suggestionList.get(0);
List<CompletionSuggestOption> resultList = suggestion.completion().options();
List<String> result = new ArrayList();
for (CompletionSuggestOption completionSuggestOption : resultList) {
String text = completionSuggestOption.text();
result.add(text);
}
return result;
}
@Override
public GoodsSearchResult search(GoodsSearchParam goodsSearchParam) {
// 1.构造ES搜索条件
NativeQuery nativeQuery = buildQuery(goodsSearchParam);
// 2.搜索
SearchHits<GoodsES> search = template.search(nativeQuery, GoodsES.class);
// 3.将查询结果封装为Mybatis-plus的Page对象
// 3.1 将SearchHits对象转为List
List<GoodsES> content = new ArrayList();
for (SearchHit<GoodsES> goodsESSearchHit : search) {
GoodsES goodsES = goodsESSearchHit.getContent();
content.add(goodsES);
}
// 3.2 将List转为Mybatis-plus的Page对象
Page<GoodsES> page = new Page();
page.setCurrent(goodsSearchParam.getPage()) // 当前页
.setSize(goodsSearchParam.getSize()) // 每页条数
.setTotal(search.getTotalHits()) // 总条数
.setRecords(content); // 结果集
// 4.封装查询结果
GoodsSearchResult result = new GoodsSearchResult();
// 4.1 封装商品
result.setGoodsPage(page);
// 4.2 封装查询参数
result.setGoodsSearchParam(goodsSearchParam);
// 4.3 封装查询面板
buildSearchPanel(goodsSearchParam,result);
return result;
}
/**
* 封装查询面板,即根据查询条件,找到查询结果关联度前20名的商品进行封装
* @param goodsSearchParam 查询条件对象
* @param goodsSearchResult 查询结果对象
*/
public void buildSearchPanel(GoodsSearchParam goodsSearchParam,GoodsSearchResult goodsSearchResult){
// 1.构造查询条件
goodsSearchParam.setPage(1);
goodsSearchParam.setSize(20);
goodsSearchParam.setSort(null);
goodsSearchParam.setSortFiled(null);
NativeQuery nativeQuery = buildQuery(goodsSearchParam);
// 2.搜索
SearchHits<GoodsES> search = template.search(nativeQuery, GoodsES.class);
// 3.将结果封装为List对象
List<GoodsES> content = new ArrayList();
for (SearchHit<GoodsES> goodsESSearchHit : search) {
GoodsES goodsES = goodsESSearchHit.getContent();
content.add(goodsES);
}
// 4.遍历集合,封装查询面板
// 商品相关的品牌列表
Set<String> brands = new HashSet();
// 商品相关的类型列表
Set<String> productTypes = new HashSet();
// 商品相关的规格列表
Map<String,Set<String>> specifications = new HashMap();
for (GoodsES goodsES : content) {
// 获取品牌
brands.add(goodsES.getBrand());
// 获取类型
List<String> productType = goodsES.getProductType();
productTypes.addAll(productType);
// 获取规格
Map<String, List<String>> specification = goodsES.getSpecification();
Set<Map.Entry<String, List<String>>> entries = specification.entrySet();
for (Map.Entry<String, List<String>> entry : entries) {
// 规格名
String key = entry.getKey();
// 规格值
List<String> value = entry.getValue();
// 如果specifications有该规格,则像规格中添加规格项,如果没有该规格,新增键值对
if (!specifications.containsKey(key)){
specifications.put(key,new HashSet(value));
}else {
specifications.get(key).addAll(value);
}
}
}
goodsSearchResult.setBrands(brands);
goodsSearchResult.setProductType(productTypes);
goodsSearchResult.setSpecifications(specifications);
}
/**
* 构造搜索条件
*
* @param goodsSearchParam 查询条件对象
* @return 搜索条件对象
*/
public NativeQuery buildQuery(GoodsSearchParam goodsSearchParam) {
// 1.创建复杂查询条件对象
NativeQueryBuilder nativeQueryBuilder = new NativeQueryBuilder();
BoolQuery.Builder builder = new BoolQuery.Builder();
// 2.如果查询条件有关键词,关键词可以匹配商品名、副标题、品牌字段;否则查询所有商品
if (!StringUtils.hasText(goodsSearchParam.getKeyword())){
MatchAllQuery matchAllQuery = new MatchAllQuery.Builder().build();
builder.must(matchAllQuery._toQuery());
}else{
String keyword = goodsSearchParam.getKeyword();
MultiMatchQuery keywordQuery = MultiMatchQuery.of(q ->q.query(keyword).fields("goodsName","caption","brand"));
builder.must(keywordQuery._toQuery());
}
// 3.如果查询条件有品牌,则精准匹配品牌
String brand = goodsSearchParam.getBrand();
if (StringUtils.hasText(brand)){
TermQuery brandQuery = TermQuery.of(q -> q.field("brand").value(brand));
builder.must(brandQuery._toQuery());
}
// 4.如果查询条件有价格,则匹配价格
Double highPrice = goodsSearchParam.getHighPrice();
Double lowPrice = goodsSearchParam.getLowPrice();
if (highPrice != null && highPrice != 0){
RangeQuery lte = RangeQuery.of(q -> q.field("price").lte(JsonData.of(highPrice)));
builder.must(lte._toQuery());
}
if (lowPrice != null && lowPrice != 0){
RangeQuery gte = RangeQuery.of(q -> q.field("price").gte(JsonData.of(lowPrice)));
builder.must(gte._toQuery());
}
// 5.如果查询条件有规格项,则精准匹配规格项
Map<String, String> specificationOptions = goodsSearchParam.getSpecificationOption();
if (specificationOptions != null && specificationOptions.size() > 0){
Set<Map.Entry<String, String>> entries = specificationOptions.entrySet();
for (Map.Entry<String, String> entry : entries) {
String key = entry.getKey();
String value = entry.getValue();
if (StringUtils.hasText(key)){
TermQuery termQuery = TermQuery.of(q->q.field("specification."+key+".keyword").value(value));
builder.must(termQuery._toQuery());
}
}
}
nativeQueryBuilder.withQuery(builder.build()._toQuery());
// 6.添加分页条件
PageRequest pageable = PageRequest.of(goodsSearchParam.getPage()-1, goodsSearchParam.getSize());
nativeQueryBuilder.withPageable(pageable);
// 7.如果查询条件有排序,则添加排序条件
String sortFiled = goodsSearchParam.getSortFiled();
String sort = goodsSearchParam.getSort();
if (StringUtils.hasText(sort) && StringUtils.hasText(sortFiled)){
Sort sortParam = null;
// 新品的正序是ID的倒序
if (sortFiled.equals("NEW")){
if (sort.equals("ASC")){
sortParam = Sort.by(Sort.Direction.DESC,"id");
}
if (sort.equals("DESC")){
sortParam = Sort.by(Sort.Direction.ASC,"id");
}
}
if (sortFiled.equals("PRICE")){
if (sort.equals("ASC")){
sortParam = Sort.by(Sort.Direction.ASC,"price");
}
if (sort.equals("DESC")){
sortParam = Sort.by(Sort.Direction.DESC,"price");
}
}
nativeQueryBuilder.withSort(sortParam);
}
// 8.返回封装好的搜索条件对象
return nativeQueryBuilder.build();
}
@Override
public void syncGoodsToES(GoodsDesc goodsDesc) {
// 将商品详情数据转为GoodsES对象
GoodsES goodsES = new GoodsES();
goodsES.setId(goodsDesc.getId());
goodsES.setGoodsName(goodsDesc.getGoodsName());
goodsES.setCaption(goodsDesc.getCaption());
goodsES.setPrice(goodsDesc.getPrice());
goodsES.setHeaderPic(goodsDesc.getHeaderPic());
goodsES.setBrand(goodsDesc.getBrand().getName());
// 商品类型集合
List<String> productType = new ArrayList();
productType.add(goodsDesc.getProductType1().getName());
productType.add(goodsDesc.getProductType2().getName());
productType.add(goodsDesc.getProductType3().getName());
goodsES.setProductType(productType);
// 商品规格集合
Map<String, List<String>> map = new HashMap();
List<Specification> specifications = goodsDesc.getSpecifications();
// 遍历规格集合
for (Specification specification : specifications) {
// 规格项
List<SpecificationOption> specificationOptions = specification.getSpecificationOptions();
// 拿到规格项名
List<String> optionStrList = new ArrayList();
for (SpecificationOption option : specificationOptions) {
optionStrList.add(option.getOptionName());
}
map.put(specification.getSpecName(), optionStrList);
}
goodsES.setSpecification(map);
// 关键字
List<String> tags = new ArrayList();
tags.add(goodsDesc.getBrand().getName()); // 品牌名是关键字
tags.addAll(analyze(goodsDesc.getGoodsName(), "ik_smart")); // 商品名分词后是关键词
tags.addAll(analyze(goodsDesc.getCaption(), "ik_smart")); // 副标题分词后是关键词
goodsES.setTags(tags);
// 将GoodsES对象存入ES
goodsESRepository.save(goodsES);
}
@Override
public void delete(Long id) {
goodsESRepository.deleteById(id);
}
}
//奇怪这里没有图片,怀疑是因为没有同步的ES
同步后修改成功!(ps: 这里还存在一个bug ,//todo 仍然无法添加没有规格项的商品,会报空指针异常)
根据id查询商品详情
访问成功
管理员操作后同步到ES
@DubboService
public class GoodsServiceImpl implements GoodsService {
@Autowired
private GoodsMapper goodsMapper;
@Autowired
private GoodsImageMapper goodsImageMapper;
@DubboReference
private SearchService searchService;
@Override
public void add(Goods goods) {
// 插入商品数据
goodsMapper.insert(goods);
// 插入图片数据
Long goodsId = goods.getId(); // 获取商品主键
List<GoodsImage> images = goods.getImages(); // 商品图片
for (GoodsImage image : images) {
image.setGoodsId(goodsId); // 给图片设置商品id
goodsImageMapper.insert(image); //插入图片
}
// 插入商品_规格项数据
// 1.获取规格
List<Specification> specifications = goods.getSpecifications();
// 2.获取规格项
List<SpecificationOption> options = new ArrayList(); //规格项集合
// 遍历规格,获取规格中的所有规格项
for (Specification specification : specifications) {
options.addAll(specification.getSpecificationOptions());
}
// 3.遍历规格项,插入商品_规格项数据
for (SpecificationOption option : options) {
goodsMapper.addGoodsSpecificationOption(goodsId,option.getId());
}
// 将商品数据同步到es中
GoodsDesc goodsDesc = findDesc(goodsId);
searchService.syncGoodsToES(goodsDesc);
}
@Override
public void update(Goods goods) {
// 删除旧图片数据
Long goodsId = goods.getId(); // 商品id
QueryWrapper<GoodsImage> queryWrapper = new QueryWrapper();
queryWrapper.eq("goodsId",goodsId);
goodsImageMapper.delete(queryWrapper);
// 删除旧规格项数据
goodsMapper.deleteGoodsSpecificationOption(goodsId);
// 插入商品数据
goodsMapper.updateById(goods);
// 插入图片数据
List<GoodsImage> images = goods.getImages(); // 商品图片
for (GoodsImage image : images) {
image.setGoodsId(goodsId); // 给图片设置商品id
goodsImageMapper.insert(image); //插入图片
}
// 插入商品_规格项数据
// 1.获取规格
List<Specification> specifications = goods.getSpecifications();
// 2.获取规格项
List<SpecificationOption> options = new ArrayList(); //规格项集合
// 遍历规格,获取规格中的所有规格项
for (Specification specification : specifications) {
options.addAll(specification.getSpecificationOptions());
}
// 3.遍历规格项,插入商品_规格项数据
for (SpecificationOption option : options) {
goodsMapper.addGoodsSpecificationOption(goodsId,option.getId());
}
// 将商品数据同步到es中
GoodsDesc goodsDesc = findDesc(goodsId);
searchService.syncGoodsToES(goodsDesc);
}
@Override
public void putAway(Long id, Boolean isMarketable) {
goodsMapper.putAway(id,isMarketable);
// 上架时数据同步到ES,下架时删除ES数据
if (isMarketable){
GoodsDesc goodsDesc = findDesc(id);
searchService.syncGoodsToES(goodsDesc);
}else {
searchService.delete(id);
}
}
}
这里存在耦合 goods 和search类
优化商品服务
rocketmq:
# nameserver地址
name-server: 192.168.66.100:9876
producer:
# 生产组
group: my_group1
# 发送消息超时时间
send-message-timeout: 3000
出现问题:nacos服务挂了
tmd 重启服务器后发现,nacos好了,但是Higress 挂了……tmd 神经病
我就奇了怪了,感觉应该是我安装的Higress 有点问题…… 出于一个测试版吧应该,感觉不是很稳定
不然按理来说这些环境的配置应该是很稳定才对,这样才会极大的减轻开发人员的负担 --> 后期考虑放到云服务器上了