电商项目实战(架构六)——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

posted @ 2019-12-04 17:00  不浪小生  阅读(9938)  评论(2编辑  收藏  举报