商城-商品搜索(搜索微服务搭建、商品搜索数据导入ES、关键词搜索)

SpringData Elasticsearch介绍

SpringData介绍

Spring Data是一个用于简化数据库访问,并支持云服务的开源框架。其主要目标是使得对数据的

访问变得方便快捷,并支持map-reduce框架和云计算数据服务。 Spring Data可以极大的简化JPA

的写法,可以在几乎不用写实现的情况下,实现对数据的访问和操作。除了CRUD外,还包括如分

页、排序等一些常用的功能。

Spring Data的官网:http://projects.spring.io/spring-data/

SpringData ES介绍

Spring Data ElasticSearch 基于 spring data API 简化 elasticSearch操作,将原始操elasticSearch

的客户端API 进行封装 。Spring Data为Elasticsearch项目提供集成搜索引擎。

Spring Data Elasticsearch POJO的关键功能区域为中心的模型与Elastichsearch交互文档和轻松地

编写一个存储库数据访问层。

官方网站:http://projects.spring.io/spring-data-elasticsearch/

搜索工程搭建

创建搜索微服务工程,changgou-service-search,该工程主要提供搜索服务以及索引数据的更新操

作。

(1)API工程搭建

首先创建search的API工程,在changgou-service-api中创建changgou-service-search-api,

如下图:

pom.xml:

<dependencies>
        <!--goods API依赖-->
        <dependency>
            <groupId>com.changgou</groupId>
            <artifactId>changgou-service-goods-api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <!--SpringDataES依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
        </dependency>
</dependencies>

(2)搜索微服务搭建

在changgou-service中搭建changgou-service-search微服务,并进行相关配置。

pom.xml:

<dependencies>
        <!--依赖search api-->
        <dependency>
            <groupId>com.changgou</groupId>
            <artifactId>changgou-service-search-api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
</dependencies>

application.yml配置

server:
  port: 18085
spring:
  application:
    name: search
  data:
    elasticsearch:
      cluster-name: my-application
      cluster-nodes: 192.168.2.132:9300
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:7001/eureka
  instance:
    prefer-ip-address: true
feign:
  hystrix:
    enabled: true
#超时配置
ribbon:
  ReadTimeout: 300000

hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 10000

文档中端口号是,注意改成18085

server:
  port: 18086

配置说明:

connection-timeout:服务连接超时时间
socket-connect:HTTP请求超时时间
ribbon.ReadTimeout: Feign请求读取数据超时时间
timeoutInMilliseconds:feign连接超时时间
cluster-name:Elasticsearch的集群节点名称,这里需要和Elasticsearch集群节点名称保持一致
cluster-nodes:Elasticsearch节点通信地址

(3)启动类

创建SearchApplication作为搜索微服务工程的启动类,代码如下:

package com.changgou.search;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

@SpringBootApplication(exclude={DataSourceAutoConfiguration.class})
@EnableEurekaClient
public class SearchApplication {

    public static void main(String[] args) {
        /**
         * Springboot整合Elasticsearch 在项目启动前设置一下的属性,防止报错
         * 解决netty冲突后初始化client时还会抛出异常
         * availableProcessors is already set to [12], rejecting [12]
         ***/
        System.setProperty("es.set.netty.runtime.available.processors", "false");
        SpringApplication.run(SearchApplication.class,args);
    }
}

分别创建对应的包,dao、service、controller,如下图:

---------------------------------------------------------------------------------------------------------------------------------

数据导入:

现在需要将数据从数据库中查询出来,然后将数据导入到ES中。

------------------------------------------------------------------------------------------------------------------------------

文档映射Bean创建

搜索商品的时候,会根据如下属性搜索数据,并且不是所有的属性都需要分词搜索,我们创建

JavaBean,将JavaBean数据存入到ES中要以搜索条件和搜索展示结果为依据,部分关键搜索条

件分析如下:

1.可能会根据商品名称搜素,而且可以搜索商品名称中的任意一个词语,所以需要分词
2.可能会根据商品分类搜索,商品分类不需要分词
3.可能会根据商品品牌搜索,商品品牌不需要分词
4.可能会根据商品商家搜索,商品商家不需要分词
5.可能根据规格进行搜索,规格时一个键值对结构,用Map

根据上面的分析,我们可以在changgou-service-search-api工程中创建

com.changgou.search.pojo.SkuInfo,如下

package com.changgou.search.pojo;

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.util.Date;
import java.util.Map;

@Document(indexName = "skuinfo",type = "docs")
public class SkuInfo implements Serializable {
    //商品id,同时也是商品编号
    @Id
    private Long id;

    //SKU名称
    @Field(type = FieldType.Text, analyzer = "ik_smart")
    private String name;

    //商品价格,单位为:元
    @Field(type = FieldType.Double)
    private Long price;

    //库存数量
    private Integer num;

    //商品图片
    private String image;

    //商品状态,1-正常,2-下架,3-删除
    private String status;

    //创建时间
    private Date createTime;

    //更新时间
    private Date updateTime;

    //是否默认
    private String isDefault;

    //SPUID
    private Long spuId;

    //类目ID
    private Long categoryId;

    //类目名称
    @Field(type = FieldType.Keyword)
    private String categoryName;

    //品牌名称
    @Field(type = FieldType.Keyword)
    private String brandName;

    //规格
    private String spec;

    //规格参数
    private Map<String,Object> specMap;

    //--------------------------getter、setter-------------------------------
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Long getPrice() {
        return price;
    }

    public void setPrice(Long price) {
        this.price = price;
    }

    public Integer getNum() {
        return num;
    }

    public void setNum(Integer num) {
        this.num = num;
    }

    public String getImage() {
        return image;
    }

    public void setImage(String image) {
        this.image = image;
    }

    public String getStatus() {
        return status;
    }

    public void setStatus(String status) {
        this.status = status;
    }

    public Date getCreateTime() {
        return createTime;
    }

    public void setCreateTime(Date createTime) {
        this.createTime = createTime;
    }

    public Date getUpdateTime() {
        return updateTime;
    }

    public void setUpdateTime(Date updateTime) {
        this.updateTime = updateTime;
    }

    public String getIsDefault() {
        return isDefault;
    }

    public void setIsDefault(String isDefault) {
        this.isDefault = isDefault;
    }

    public Long getSpuId() {
        return spuId;
    }

    public void setSpuId(Long spuId) {
        this.spuId = spuId;
    }

    public Long getCategoryId() {
        return categoryId;
    }

    public void setCategoryId(Long categoryId) {
        this.categoryId = categoryId;
    }

    public String getCategoryName() {
        return categoryName;
    }

    public void setCategoryName(String categoryName) {
        this.categoryName = categoryName;
    }

    public String getBrandName() {
        return brandName;
    }

    public void setBrandName(String brandName) {
        this.brandName = brandName;
    }

    public String getSpec() {
        return spec;
    }

    public void setSpec(String spec) {
        this.spec = spec;
    }

    public Map<String, Object> getSpecMap() {
        return specMap;
    }

    public void setSpecMap(Map<String, Object> specMap) {
        this.specMap = specMap;
    }
}

------------------------------------------------------------------------------------------------------------------------------

搜索审核通过Sku

修改changgou-service-goods微服务,添加搜索审核通过的Sku,供search微服务调用。下面都是

针对goods微服务的操作。

修改SkuService接口,添加根据状态查询Sku方法,代码如下:

/**
 * 根据状态查询SKU列表
 */
List<Sku> findByStatus(String status);

修改SkuServiceImpl,添加根据状态查询Sku实现方法,代码如下:

/***
 * 根据状态查询SKU列表
 * @return
 */
@Override
public List<Sku> findByStatus(String status) {
    Sku sku = new Sku();
    sku.setStatus(status);
    return skuMapper.select(sku);
}

修改com.changgou.goods.controller.SkuController,添加根据审核状态查询Sku方法,代码如下:

/***
 * 根据审核状态查询Sku
 * @param status
 * @return
 */
@GetMapping("/status/{status}")
public Result<List<Sku>> findByStatus(@PathVariable String status){
    List<Sku> list = skuService.findByStatus(status);
    return new Result<List<Sku>>(true,StatusCode.OK,"查询成功",list);
}

------------------------------------------------------------------------------------------------------------------------------

Sku导入ES实现

(1) Feign配置

修改changgou-service-goods-api工程,在com.changgou.goods.feign.SkuFeign上添加findSkuList

方法,代码如下:

package com.changgou.goods.feign;

import com.changgou.goods.pojo.Sku;
import entity.Result;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import java.util.List;

@FeignClient(name="goods")
@RequestMapping(value = "/sku")
public interface SkuFeign {

    /***
     * 根据审核状态查询Sku
     * @param status
     * @return
     */
    @GetMapping("/status/{status}")
    Result<List<Sku>> findByStatus(@PathVariable String status);

}

(2) Dao创建

修改changgou-service-search工程,创建com.changgou.search.dao.SkuEsMapper,该接口主要用

于索引数据操作,主要使用它来实现将数据导入到ES索引库中,代码如下:

package com.changgou.search.dao;

import com.changgou.search.pojo.SkuInfo;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface SkuEsMapper extends ElasticsearchRepository<SkuInfo,Long> {
}

注意文档有笔误,写成了Sku,会报错,应该是SkuInfo

(3) 服务层创建

修改changgou-service-search工程,创建com.changgou.search.service.SkuService,代码如下:

public interface SkuService {

    /***
     * 导入SKU数据
     */
    void importSku();
}

修改changgou-service-search工程,创建com.changgou.search.service.impl.SkuServiceImpl,实

现Sku数据导入到ES中,代码如下:

@Service
public class SkuServiceImpl implements SkuService {

    @Autowired
    private SkuFeign skuFeign;

    @Autowired
    private SkuEsMapper skuEsMapper;

    /**
     * 导入sku数据到es
     */
    @Override
    public void importSku(){
        //调用changgou-service-goods微服务
        Result<List<Sku>> skuListResult = skuFeign.findByStatus("1");
        //将数据转成search.Sku
        List<SkuInfo> skuInfos  =  JSON.parseArray(JSON.toJSONString(skuListResult.getData()),SkuInfo.class);
        /*for(SkuInfo skuInfo:skuInfos){
            Map<String, Object> specMap= JSON.parseObject(skuInfo.getSpec()) ;
            skuInfo.setSpecMap(specMap);
        }*/
        skuEsMapper.saveAll(skuInfos);
    }
}

(4)控制层配置

修改changgou-service-search工程,在com.changgou.search.controller.SkuController类中添加如

下方法调用上述导入方法,代码如下:

@RestController
@RequestMapping(value = "/search")
@CrossOrigin
public class SkuController {

    @Autowired
    private SkuService skuService;

    /**
     * 导入数据
     * @return
     */
    @GetMapping("/import")
    public Result search(){
        skuService.importSku();
        return new Result(true, StatusCode.OK,"导入数据到索引库中成功!");
    }
}

(5)修改启动类

启动类中需要开启Feign客户端,并且需要添加ES包扫描,代码如下:

package com.changgou.search;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories;

@SpringBootApplication(exclude={DataSourceAutoConfiguration.class})
@EnableEurekaClient
@EnableFeignClients(basePackages = "com.changgou.goods.feign")
@EnableElasticsearchRepositories(basePackages = "com.changgou.search.dao")
public class SearchApplication {

    public static void main(String[] args) {
        /**
         * Springboot整合Elasticsearch 在项目启动前设置一下的属性,防止报错
         * 解决netty冲突后初始化client时还会抛出异常
         * availableProcessors is already set to [12], rejecting [12]
         ***/
        System.setProperty("es.set.netty.runtime.available.processors", "false");
        SpringApplication.run(SearchApplication.class,args);
    }
}

(6)测试:

启动注册中心、goods微服务、search微服务

http://localhost:7001/

http://localhost:18085/search/import

http://192.168.2.132:9100/

需要用map处理下,生成动态域:

package com.changgou.search.service.impl;

import com.alibaba.fastjson.JSON;
import com.changgou.goods.feign.SkuFeign;
import com.changgou.goods.pojo.Sku;
import com.changgou.search.dao.SkuEsMapper;
import com.changgou.search.pojo.SkuInfo;
import com.changgou.search.service.SkuService;
import entity.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Map;

@Service
public class SkuServiceImpl implements SkuService {

    @Autowired
    private SkuFeign skuFeign;

    @Autowired
    private SkuEsMapper skuEsMapper;

    /**
     * 导入sku数据到es
     */
    @Override
    public void importSku(){
        //调用changgou-service-goods微服务
        Result<List<Sku>> skuListResult = skuFeign.findByStatus("1");
        //将数据转成search.Sku
        List<SkuInfo> skuInfos = JSON.parseArray(JSON.toJSONString(skuListResult.getData()),SkuInfo.class);
        for(SkuInfo skuInfo:skuInfos){
            Map<String, Object> specMap= JSON.parseObject(skuInfo.getSpec()) ;
            skuInfo.setSpecMap(specMap);
        }
        skuEsMapper.saveAll(skuInfos);
    }
}

重新启动search微服务,删掉之前生成的索引

重新导入es:http://localhost:18085/search/import

---------------------------------------------------------------------------------------------------------------------------------

关键字搜索

我们先使用SpringDataElasticsearch实现一个简单的搜索功能,先实现根据关键字搜索,从上面搜

索图片可以看得到,每次搜索的时候,除了关键字外,还有可能有品牌、分类、规格等,后台接收

搜索条件使用Map接收比较合适。

老师后来讲用GetMapping,因为可能直接从浏览器请求

/**
 * 搜索
 * @param searchMap
 * @return
 */
@GetMapping
public Map search(@RequestParam Map<String,String> searchMap){
    return  skuService.search(searchMap);
}


/***
* 搜索
* @param searchMap
* @return
*/
Map search(Map<String, String> searchMap);


@Autowired
private ElasticsearchTemplate esTemplate;

public Map search(Map<String, String> searchMap) {

    //1.获取关键字的值
    String keywords = searchMap.get("keywords");

    if (StringUtils.isEmpty(keywords)) {
        keywords = "华为";//赋值给一个默认的值
    }
    //2.创建查询对象 的构建对象
    NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();

    //3.设置查询的条件

    nativeSearchQueryBuilder.withQuery(QueryBuilders.matchQuery("name", keywords));

    //4.构建查询对象
    NativeSearchQuery query = nativeSearchQueryBuilder.build();

    //5.执行查询
    AggregatedPage<SkuInfo> skuPage = esTemplate.queryForPage(query, SkuInfo.class);


    //6.返回结果
    Map resultMap = new HashMap<>();
    resultMap.put("rows", skuPage.getContent());
    resultMap.put("total", skuPage.getTotalElements());
    resultMap.put("totalPages", skuPage.getTotalPages());

    return resultMap;
}

搜索过程类比JDBC过程分析

搜索商品数据条件封装

关键词搜索测试:

http://localhost:18085/search?keywords=华为

---------------------------------------------------------------------------------------------------------------------------------

分类统计

看下面的SQL语句,我们在执行搜索的时候,第1条SQL语句是执行搜,第2条语句是根据分类名字

分组查看有多少分类,大概执行了2个步骤就可以获取数据结果以及分类统计,我们可以发现他们

的搜索条件完全一样。

-- 查询所有
SELECT * FROM tb_sku WHERE name LIKE '%手机%';
-- 根据分类名字分组查询
SELECT category_name FROM  tb_sku WHERE name LIKE '%手机%' GROUP BY category_name;

我们每次执行搜索的时候,需要显示商品分类名称,这里要显示的分类名称其实就是符合搜素条件

的所有商品的分类集合,我们可以按照上面的实现思路,使用ES根据分组名称做一次分组查询即

可实现。

修改search微服务的com.changgou.search.service.impl.SkuServiceImpl类,整体代码如下:

public Map search(Map<String, String> searchMap) {

    //1.获取关键字的值
    String keywords = searchMap.get("keywords");

    if (StringUtils.isEmpty(keywords)) {
        keywords = "华为";//赋值给一个默认的值
    }
    //2.创建查询对象 的构建对象
    NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();

    //3.设置查询的条件

    //---------------分组查询补充的代码---------------------------------------------
    // 4.1 商品分类的列表展示: 按照商品分类的名称来分组
    //terms  指定分组的一个别名
    //field 指定要分组的字段名
    // size 指定查询结果的数量 默认是10个
    nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms("skuCategorygroup").field("categoryName").size(50));
    //---------------分组查询补充的代码---------------------------------------------


    nativeSearchQueryBuilder.withQuery(QueryBuilders.matchQuery("name", keywords));

    //4.构建查询对象
    NativeSearchQuery query = nativeSearchQueryBuilder.build();

    //5.执行查询
    AggregatedPage<SkuInfo> skuPage = esTemplate.queryForPage(query, SkuInfo.class);


    //---------------分组查询补充的代码---------------------------------------------
    // 获取聚合结果  获取商品分类的列表数据
    StringTerms stringTermsCategory = (StringTerms) skuPage.getAggregation("skuCategorygroup");
    
    List<String> categoryList = new ArrayList<>();
    if (stringTermsCategory != null) {
        for (StringTerms.Bucket bucket : stringTermsCategory.getBuckets()) {
            String keyAsString = bucket.getKeyAsString();
            System.out.println(keyAsString);//就是商品分类的数据
            categoryList.add(keyAsString);
        }
    }
    //---------------分组查询补充的代码---------------------------------------------

    //6.返回结果
    Map resultMap = new HashMap<>();
    //---------------分组查询补充的代码---------------------------------------------
    resultMap.put("categoryList", categoryList);
    //---------------分组查询补充的代码---------------------------------------------
    resultMap.put("rows", skuPage.getContent());
    resultMap.put("total", skuPage.getTotalElements());
    resultMap.put("totalPages", skuPage.getTotalPages());

    return resultMap;
}

修改好重启搜索微服务,测试

http://localhost:18085/search?keywords=华为

---------------------------------------------------------------------------------------------------------------------------------

代码优化

可以将获取分组的代码进行提取,如下代码所示:

......

List<String> categoryList = getStringsCategoryList(stringTermsCategory);

/**
 * 获取分类列表数据
 *
 * @param stringTerms
 * @return
 */
private List<String> getStringsCategoryList(StringTerms stringTerms) {
    List<String> categoryList = new ArrayList<>();
    if (stringTerms != null) {
        for (StringTerms.Bucket bucket : stringTerms.getBuckets()) {
            String keyAsString = bucket.getKeyAsString();//分组的值
            categoryList.add(keyAsString);
        }
    }
    return categoryList;
}

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ZHOU_VIP

您的鼓励将是我创作最大的动力!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值