电商项目实战(架构六)——Elasticsearch实现商品搜索
一、前言
Elasticsearch是一个分布式、可扩展、实时的搜索与数据分析引擎,它能从一开始就赋予你的数据以搜索、分析和探索的能力,可用于全文搜索和数据实时统计。
二、框架
Elasticsearch的安装和使用
1、下载Elasticsearch6.2.2压缩包,下载地址:https://www.elastic.co/cn/downloads/past-releases/elasticsearch-6-2-2
2、安装中文分词插件,解压后,在cmd命令框中进入到bin目录下,执行命令:elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v6.2.2/elasticsearch-analysis-ik-6.2.2.zip
3、运行bin目录下的elasticsearch.bat,启动elasticsearch
4、下载kibana,作为访问elasticsearch的客户端,下载地址:https://artifacts.elastic.co/downloads/kibana/kibana-6.2.2-windows-x86_64.zip,解压后进入bin目录,打开文件kibana.bat,启动Kibana用户界面
5、访问http://localhost:5601打开用户界面
Spring Data Elasticsearch
1、常用注解
@Document(表示映射到Elasticsearch文档上的领域对象)
public @interface Document{ //索引库名次,mysql中数据库的概念 String indexName(); //文档类型,mysql中表的概念 String type() default ""; //默认分片数 short shards() default 5; //默认副本数量 short replicas default 1; }
@Id(表示是文档的id,文档可以认为是mysql中表字段的概念)
public @interface Id{ }
@Field
public @interface Field{ //文档中字段的类型 FieldType type() default FieldType.Auto; //是否建立倒排索引 boolean index() default true; //是否进行存储 boolean store() deafult false; //分词器名次 String analyzer() default ""; } //为文档自动指定元数据类型 public enum FieldType{ Text, //会进行分词并建了索引的字符类型 Integer, Long, Date, Float, Double, Boolean, Object, Auto, //自动判断字段类型 Nested, //嵌套对象类型 Ip, Attachment, Keyword //不会进行分词建立索引的类型 }
三、建表
商品信息表:pms_product
字段解释:id(商品信息表id),brand_id(品牌id),product_category_id(商品分类id),feight_template_id(运费模板id),product_attribute_category_id(规格属性类别id),name(商品名称),pic(商品主图),product_sn(货号),publish_status(是否上架 0->下架 1->上架),recommand_status(是否推荐 0->否 1->是),verify_status(审核状态 0->待审核 1->审核通过 2->审核拒绝),sale_count(销量),unit(单位),min_price(最低价),max_price(最高价),market_price(市场价),description(商品描述),stock_total(库存总数),weight(重量(单位默认为克)),album_pics(画册图片,限制为5张,以逗号分割,主图在第一位),detail_title(商品详情标题),detail_sub_title(商品详情副标题),detail_html(商品详情富文本)
商品规格属性分类表:pms_product_attribute_category
字段解释:id(商品规格属性类别表id),name(类别名称),attribute_count(类别下属性数量)
商品规格属性表:pms_product_attribute
字段解释:id(商品规格属性表id),product_attribute_category_id(规格属性类别id),name(规格属性名称),type(类型 0->规格属性 1->参数)
商品规格属性值表:pms_product_attribute_value
字段解释:id(商品规格属性值id),product_attribute_id(商品规格属性id),value(属性值)
四、整合Elasticsearch,实现商品搜索
1、在pom.xml文件中添加相关依赖
<!--Elasticsearch相关依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-elasticsearch</artifactId> </dependency>
2、修改application.yml文件
spring: #连接elasticsearch data: elasticsearch: repositories: enabled: true cluster-nodes: 127.0.0.1:9300 #es的连接地址及端口号 cluster-name: elasticsearch #es集群的名称
3、新建elasticsearch.document和elasticsearch.repository包
4、在document包下新建商品文档对象EsProduct
package com.zzb.test.admin.elasticsearch.document; import org.springframework.data.annotation.Id; import org.springframework.data.elasticsearch.annotations.Document; import org.springframework.data.elasticsearch.annotations.Field; import org.springframework.data.elasticsearch.annotations.FieldType; import java.io.Serializable; import java.math.BigDecimal; import java.util.List; /** * 商品信息 * Created by zzb on 2019/12/3 17:09 */ @Document(indexName = "pms", type = "product", shards = 1, replicas = 0) public class EsProduct implements Serializable { private static final long serialVersionUID = -1L; @Id private Long id; private Long brandId; private Long productCategoryId; private Long productAttributeCategoryId; private String unit; private BigDecimal minPrice; private BigDecimal maxPrice; private BigDecimal marketPrice; private String description; private BigDecimal stockTotal; private BigDecimal weight; @Field(type = FieldType.Keyword) private String productSn; @Field(analyzer = "ik_max_word", type = FieldType.Text) private String name; @Field(analyzer = "ik_max_word", type = FieldType.Text) private String detailTitle; @Field(analyzer = "ik_max_word", type = FieldType.Text) private String detailSubTitle; @Field(analyzer = "ik_max_word", type = FieldType.Text) private String keyword; @Field(type = FieldType.Nested) //嵌套对象类型 private List<EsProductAttributeValue> attrValueList; public String getKeyword() { return keyword; } public void setKeyword(String keyword) { this.keyword = keyword; } public Long getBrandId() { return brandId; } public void setBrandId(Long brandId) { this.brandId = brandId; } public Long getProductCategoryId() { return productCategoryId; } public void setProductCategoryId(Long productCategoryId) { this.productCategoryId = productCategoryId; } public Long getProductAttributeCategoryId() { return productAttributeCategoryId; } public void setProductAttributeCategoryId(Long productAttributeCategoryId) { this.productAttributeCategoryId = productAttributeCategoryId; } public String getUnit() { return unit; } public void setUnit(String unit) { this.unit = unit; } public BigDecimal getMinPrice() { return minPrice; } public void setMinPrice(BigDecimal minPrice) { this.minPrice = minPrice; } public BigDecimal getMaxPrice() { return maxPrice; } public void setMaxPrice(BigDecimal maxPrice) { this.maxPrice = maxPrice; } public BigDecimal getMarketPrice() { return marketPrice; } public void setMarketPrice(BigDecimal marketPrice) { this.marketPrice = marketPrice; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public BigDecimal getStockTotal() { return stockTotal; } public void setStockTotal(BigDecimal stockTotal) { this.stockTotal = stockTotal; } public BigDecimal getWeight() { return weight; } public void setWeight(BigDecimal weight) { this.weight = weight; } public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getProductSn() { return productSn; } public void setProductSn(String productSn) { this.productSn = productSn; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getDetailTitle() { return detailTitle; } public void setDetailTitle(String detailTitle) { this.detailTitle = detailTitle; } public String getDetailSubTitle() { return detailSubTitle; } public void setDetailSubTitle(String detailSubTitle) { this.detailSubTitle = detailSubTitle; } public List<EsProductAttributeValue> getAttrValueList() { return attrValueList; } public void setAttrValueList(List<EsProductAttributeValue> attrValueList) { this.attrValueList = attrValueList; } }
5、在document包下新建商品文档对象内的嵌套对象EsProductAttributeValue
package com.zzb.test.admin.elasticsearch.document; import org.springframework.data.elasticsearch.annotations.Field; import org.springframework.data.elasticsearch.annotations.FieldType; import java.io.Serializable; /** * 商品属性 * Created by zzb on 2019/12/3 17:36 */ public class EsProductAttributeValue implements Serializable { private static final long serialVersionUID = 1L; //属性值id private Long id; //属性id private Long productAttributeId; //属性值 @Field(type = FieldType.Keyword) private String value; @Field(type = FieldType.Keyword) private String name; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public Long getProductAttributeId() { return productAttributeId; } public void setProductAttributeId(Long productAttributeId) { this.productAttributeId = productAttributeId; } public String getValue() { return value; } public void setValue(String value) { this.value = value; } public String getName() { return name; } public void setName(String name) { this.name = name; } }
6、在repository包下新建操作Elasticsearch的接口继承ElasticsearchRepository
package com.zzb.test.admin.elasticsearch.repository; import com.zzb.test.admin.elasticsearch.document.EsProduct; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; /** * 操作Elasticsearch的接口 * Created by zzb on 2019/12/4 10:54 */ public interface EsProductRepository extends ElasticsearchRepository<EsProduct,Long> { /** * 搜索查询 * @param name * @param detailTitle * @param keyword * @param page * @return */ Page<EsProduct> findByKeyword(String name, String detailTitle, String keyword,Pageable page); }
7、在service包下新建Elasticsearch商品搜索Service类EsProductService
package com.zzb.test.admin.service; import com.zzb.test.admin.elasticsearch.document.EsProduct; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import java.util.List; /** * Elasticsearch商品搜索的Service * Created by zzb on 2019/12/4 11:00 */ public interface EsProductService { /** * 从数据库中导入商品到ES * @return */ int importAll(); /** * 根据id删除商品 * @param id */ void delete(Long id); /** * 根据id创建商品 * @param id * @return */ EsProduct create(Long id); /** * 批量删除 * @param ids */ void deletes(List<Long> ids); /** * 根据关键字搜索 * @param keyword * @param pageNum * @param pageSize * @return */ Page<EsProduct> searchPage(String keyword, Integer pageNum,Integer pageSize); }
8、在impl包下创建其实现类EsProductServiceImpl
package com.zzb.test.admin.service.impl; import com.zzb.test.admin.dao.EsProductDao; import com.zzb.test.admin.elasticsearch.document.EsProduct; import com.zzb.test.admin.elasticsearch.repository.EsProductRepository; import com.zzb.test.admin.service.EsProductService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.CollectionUtils; import java.util.ArrayList; import java.util.Iterator; import java.util.List; /** * EsProductService接口的实现类 * Created by zzb on 2019/12/4 11:06 */ @Service @Transactional public class EsProductServiceImpl implements EsProductService { private static final Logger logger = LoggerFactory.getLogger(EsProductServiceImpl.class); @Autowired private EsProductDao esProductDao; @Autowired private EsProductRepository esProductRepository; @Override public int importAll() { List<EsProduct> esProductList = esProductDao.getProductEs(null); Iterable<EsProduct> iterable = esProductRepository.saveAll(esProductList); Iterator<EsProduct> iterator = iterable.iterator(); logger.info("导入ES数据{}:",iterator); int count = 0; while (iterator.hasNext()) { count++; iterator.next(); } return count; } @Override public void delete(Long id) { logger.info("删除ES中的商品{}:",id); esProductRepository.deleteById(id); } @Override public EsProduct create(Long id) { List<EsProduct> esProducts = esProductDao.getProductEs(id); if (CollectionUtils.isEmpty(esProducts)) { return null; } EsProduct esProduct = esProducts.get(0); logger.info("导入ES单条商品{}:",esProduct); return esProductRepository.save(esProduct); } @Override public void deletes(List<Long> ids) { if (!CollectionUtils.isEmpty(ids)) { List<EsProduct> esProductList = new ArrayList<>(); ids.forEach(id->{ EsProduct esProduct = new EsProduct(); esProduct.setId(id); esProductList.add(esProduct); }); logger.info("批量删除ES中的商品{}:",esProductList); esProductRepository.deleteAll(esProductList); } } @Override public Page<EsProduct> searchPage(String keyword, Integer pageNum, Integer pageSize) { Pageable pageable = PageRequest.of(pageNum,pageSize); return esProductRepository.findByKeyword(keyword,keyword,keyword,pageable); } }
9、在dao包下新建操作数据库接口EsProductDao和映射xml文件EsProductDao.xml
package com.zzb.test.admin.dao; import com.zzb.test.admin.elasticsearch.document.EsProduct; import java.util.List; /** * Elasticsearch商品搜索dao * Created by zzb on 2019/12/4 11:20 */ public interface EsProductDao { List<EsProduct> getProductEs(Long id); }
<?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.zzb.test.admin.dao.EsProductDao"> <select id="getProductEs" resultType="com.zzb.test.admin.elasticsearch.document.EsProduct" parameterType="java.lang.Long"> SELECT DISTINCT p.id id, p.product_sn productSn, p.brand_id brandId, pb.brand_name brandName, p.product_category_id productCategoryId, p.pic pic, p. NAME NAME, p.detail_title detailTitle, p.min_price minPrice, p.recommand_status recommandStatus, p.stock_total stockTotal, p.sort sort FROM pms_product p LEFT JOIN pms_brand pb ON pb.id = p.brand_id LEFT JOIN pms_product_attribute_category ppac ON ppac.id = p.product_attribute_category_id LEFT JOIN pms_product_attribute pa ON pa.product_attribute_category_id = ppac.id LEFT JOIN pms_product_attribute_value pav ON pa.id = pav.product_attribute_id WHERE p.del_status = 0 AND p.publish_status = 1 <if test="id!=null"> AND p.id=#{id} </if> </select> </mapper>
10、在controller包下新建控制器EsProductController
package com.zzb.test.admin.controller; import com.zzb.test.admin.common.CommonPage; import com.zzb.test.admin.common.CommonResult; import com.zzb.test.admin.elasticsearch.document.EsProduct; import com.zzb.test.admin.service.EsProductService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.stereotype.Controller; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.*; import java.util.List; /** * ES搜索商品Controller * Created by zzb on 2019/12/4 14:06 */ @Controller @Api(tags = "EsProductController",description = "ES商品搜索") public class EsProductController { @Autowired private EsProductService esProductService; @ApiOperation("从数据库导入ES商品数据") @RequestMapping(value = "/esProduct/importAll",method = RequestMethod.POST) @ResponseBody public CommonResult<Integer> importAll(){ int count = esProductService.importAll(); return CommonResult.success(count); } @ApiOperation("根据id删除商品") @RequestMapping(value = "/esProduct/delete/{id}",method = RequestMethod.POST) @ResponseBody public CommonResult deleteById(@PathVariable Long id){ esProductService.delete(id); return CommonResult.success("删除成功"); } @ApiOperation("批量删除商品") @RequestMapping(value = "/esProduct/deletes",method = RequestMethod.POST) @ResponseBody public CommonResult deleteById(List<Long> ids){ esProductService.deletes(ids); return CommonResult.success("删除成功"); } @ApiOperation("根据id创建商品") @RequestMapping(value = "/esProduct/create",method = RequestMethod.POST) @ResponseBody public CommonResult create(Long id){ EsProduct esProduct = esProductService.create(id); if (StringUtils.isEmpty(esProduct)) { return CommonResult.failed("创建失败"); } return CommonResult.success("创建成功"); } @ApiOperation("搜索商品") @RequestMapping(value = "/esProduct/search",method = RequestMethod.GET) @ResponseBody public CommonResult<CommonPage<EsProduct>> search(@RequestParam(required = false) String keyword, @RequestParam(required = false, defaultValue = "0") Integer pageNum, @RequestParam(required = false, defaultValue = "5") Integer pageSize){ Page<EsProduct> esProductPage = esProductService.searchPage(keyword,pageNum,pageSize); return CommonResult.success(CommonPage.restPage(esProductPage)); } }
11、修改common包下分页结果解析类CommonPage
package com.zzb.test.admin.common; import com.github.pagehelper.PageInfo; import org.springframework.data.domain.Page; import java.util.List; /** * mybatis分页封装 * Created by zzb on 2019/11/15 12:27 */ public class CommonPage<T> { private Integer pageNum; private Integer pageSize; private Integer totalPage; private Long total; private List<T> list; /** * 将PageHelper分页后的list转为分页信息 * @param list * @param <T> * @return */ public static <T> CommonPage<T> restPage(List<T> list){ CommonPage<T> result = new CommonPage<>(); PageInfo<T> pageInfo = new PageInfo<>(list); result.setPageNum(pageInfo.getPageNum()); result.setPageSize(pageInfo.getPageSize()); result.setTotal(pageInfo.getTotal()); result.setList(pageInfo.getList()); return result; } /** * 将SpringData分页后的list转为分页信息 * @param pageInfo * @param <T> * @return */ public static <T> CommonPage<T> restPage(Page pageInfo){ CommonPage<T> result = new CommonPage<>(); result.setPageNum(pageInfo.getNumber()); result.setPageSize(pageInfo.getSize()); result.setTotalPage(pageInfo.getTotalPages()); result.setList(pageInfo.getContent()); return result; } public Integer getPageNum() { return pageNum; } public void setPageNum(Integer pageNum) { this.pageNum = pageNum; } public Integer getPageSize() { return pageSize; } public void setPageSize(Integer pageSize) { this.pageSize = pageSize; } public Integer getTotalPage() { return totalPage; } public void setTotalPage(Integer totalPage) { this.totalPage = totalPage; } public Long getTotal() { return total; } public void setTotal(Long total) { this.total = total; } public List<T> getList() { return list; } public void setList(List<T> list) { this.list = list; } }
五、添加数据
在数据库中给商品相关表添加数据
六、测试
1、访问http://localhost:10077/swagger-ui.html
2、访问登录接口,获取访问权限
3、访问接口/esProduct/importAll,将数据库中数据导入到elasticsearch
4、访问接口/esProduct/search,查询导入的数据
5、访问接口/esProduct/delete/{id},将id为1的商品从elasticsearch中移除
再次访问接口/esProduct/search,进行查看是否移除成功
项目github地址:https://github.com/18372561381/shoptest