微服务商城系统(六)商品搜索 SpringBoot 整合 Elasticsearch

一、Elasticsearch 和 IK 分词器的安装

     Elasticsearch 是一个 开源的 高扩展的 分布式全文检索引擎,它可以近乎实时地存储、检索数据;本身扩展性很好,可以扩展到上百台服务器,处理 PB 级别的数据。 Elasticsearch 使用 Java开发,并使用 Lucene 作为其核心,来实现所有索引和搜索的功能,但是,它的目的 是通过简单的 RESTful API 来隐藏 Lucene 的复杂性,从而让全文搜索变得简单。

  • Elasticsearch 的安装

     采用 Docker 安装 Elasticsearch。

(1)docker 镜像下载

docker pull elasticsearch:5.6.8

在这里插入图片描述
(2)安装 es 容器

docker run -di --name=changgou_elasticsearch -p 9200:9200  -p 9300:9300

    9200 端口是 Web 管理平台端口;9300 是服务默认端口。
在这里插入图片描述

(3)开启远程连接
    上面完成安装后,es 并不能正常使用,elasticsearch 从 5 版本以后默认不开启远程连接,程序直接连接会报如下错误:
failed to load elasticsearch nodes : org.elasticsearch.client.transport.NoNodeAvailableException: None of the configured nodes are available: [{#transport#-1}{5ttLpMhkRjKLkvoY7ltUWg}{192.168.211.132}{192.168.211.132:9300}]

     我们需要修改es配置开启远程连接。

登录容器:

docker exec -it changgou_elasticsearch /bin/bash

查看目录结构:
在这里插入图片描述
进入 config 目录,修改 elasticsearch.yml 文件:
在这里插入图片描述
     重启后发现启动失败了,这与我们刚才修改的配置有关,因为elasticsearch 在启动的时候会进行一些检查,比如最多打开的文件的个数 以及 虚拟内存区域数量 等等,如果进行了配置,意味着需要打开更多的文件以及虚拟内存,所以我们还需要系统调优。
     执行 vi /etc/security/limits.conf ,追加内容 ( nofile 是单个进程允许打开的最大文件个数, soft nofile 是软限制, hard nofile是硬限制 )
在这里插入图片描述

     执行 vi /etc/sysctl.conf,追加内容 (限制一个进程可以拥有的 VMA 虚拟内存区域 的数量 )

vm.max_map_count=655360

执行 sysctl -p 修改内核参数马上生效
在这里插入图片描述
     重新启动虚拟机,再次启动容器,发现已经可以启动并远程访问。
    
(4)跨域配置
     修改 elasticsearch/config 下的配置文件:elasticsearch.yml,增加以下三句命令,并重启:

http.cors.enabled: true
http.cors.allow-origin: "*"
network.host: 192.168.211.132

     其中,http.cors.enabled: true为允许 elasticsearch 跨域访问,默认是false。 http.cors.allow-origin: "*"表示 跨域访问允许的域名地址(*表示任意)。
    
     重启:docker restart changgou_elasticsearch
     如果想让容器开启重启,执行 docker update --restart=always changgou_elasticsearch
    
在这里插入图片描述
     注意,地址栏中默认的是 localhost,需要改成虚拟机的 IP 地址喔。
    
     IK 分词器 是一个开源的,基于 Java 语言开发的 轻量级的 中文分词工具包。

  • IK 分词器安装

(1)安装 ik 分词器

IK分词器下载地址 https://github.com/medcl/elasticsearch-analysis-ik/releases

将 ik分词器 上传到服务器上,然后解压,并改名字为 ik

unzip elasticsearch-analysis-ik-5.6.8.zip
mv elasticsearch ik

将 ik 目录拷贝到 docker 容器的 plugins 目录下

docker cp ./ik changgou_elasticsearch:/usr/share/elasticsearch/plugins

(2)IK 分词器测试
访问:http://192.168.211.132:9200/_analyze?analyzer=ik_smart&pretty=true&text=我是程序员
1559427846075

访问:http://192.168.211.132:9200/_analyze?analyzer=ik_max_word&pretty=true&text=我是程序员 最细分词:
在这里插入图片描述
     如果想要自定义分词器,需要修改 IKAnalyzer.cfg.xml 配置文件,添加自定义分词文件,如果想要自定义停用词汇,则在该配置文件里添加停用词汇。

二、Kibana 使用

     上面使用的是 elasticsearch-head 插件(默认端口是 9100 ) 实现数据查找的,但是它的功能比较单一,我们需要一个更专业的工具实现对日志的实时分析,也就是我们接下来要讲的 Kibana 。
    Kibana 是一款开源的数据分析和可视化平台,它是 Elastic Stack 成员之一, 用于和 Elasticsearch 协作,相当于 Elasticsearch 的数据分析工具。 可以使用 Kibana 对 Elasticsearch 索引中的数据进行搜索、查看、交互操作。 可以很方便的利用图表、表格及地图对数据进行多元化的分析和呈现。
     Kibana 可以使大数据通俗易懂。它很简单,基于浏览器的界面便于快速创建和分享动态数据仪表板来追踪 Elasticsearch 的实时数据变化。
     搭建 Kibana 非常简单,可以分分钟完成 Kibana 的安装并开始探索 Elasticsearch 的索引数据 — 没有代码、不需要额外的基础设施。

  • Kibana下载安装

(1)镜像下载

docker pull docker.io/kibana:5.6.8

(2)安装 kibana 容器

docker run -it -d -e ELASTICSEARCH_URL=http://192.168.211.132:9200 --name kibana --restart=always -p 5601:5601 kibana:5.6.8

参数说明:
① ELASTICSEARCH_URL=http://192.168.211.132:9200:是指链接的 ES 地址
② restart=always:每次服务都会重启,也就是开启启动
③ 5601:5601:端口号

(3)访问测试

访问 http://192.168.211.132:5601 如下:

1559533771948

  • Kibana 使用

    • 配置索引
           要使用 Kibana,必须至少配置一个索引。索引用于标识 Elasticsearch 索引,以运行搜索和分析。它们还用于配置字段,输入索引后,点击 create,会展示出当前配置的索引的域信息:
      在这里插入图片描述
    • 数据搜索

     Discover 为 数据搜索 部分,可以对日志信息进行搜索操作。

1554501163624

    可以使用 Discover 实现数据搜索过滤和搜索条件显示以及关键词搜索:
在这里插入图片描述

比如,还可以把数据 以各种表的形式展示:
在这里插入图片描述
比如,绘制个饼状图:
在这里插入图片描述

三、数据导入 Elasticsearch

1、SpringData Elasticsearch 介绍

     Spring Data 是一个 用于简化数据库访问,并支持云服务的开源框架。 其主要目标是使得对数据的访问变得方便快捷,并支持 map-reduce 框架和云计算数据服务。 Spring Data 可以极大的简化 JPA 的写法,可以在几乎不用写实现的情况下,实现对数据的访问和操作。除了 CRUD 外,还包括如分页、排序等一些常用的功能。

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

     Spring Data ElasticSearch 基于 spring data API ,简化 ElasticSearch 操作,将原始操作 elasticSearch 的客户端 API 进行封装 。 Spring Data 为 Elasticsearch 项目提供集成搜索引擎。Spring Data Elasticsearch POJO的关键功能区域为中心的模型与 Elastichsearch 交互文档和轻松地编写一个存储库数据访问层。

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

    

2、搜索工程搭建

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

(1)API 工程搭建

     首先创建 search 的 API 工程,在 changgou-service-api 中创建changgou-service-search-api,导入依赖:

<!--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>

     因为是对商品的搜索(而且是针对 sku 进行搜索),所以应该向索引库中导入 sku 的数据,需要用到 goods 的 feign 调用。
    
(2)搜索微服务搭建
     在 changgou-service 中搭建 changgou-service-search 微服务,导入依赖:

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

application.yml配置

server:
  port: 18085
spring:
  application:
    name: search
  data:
    elasticsearch:
      cluster-name: my-application
      cluster-nodes: 192.168.211.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

配置说明:

  • connection-timeout:服务连接超时时间
  • socket-connect:HTTP请求超时时间
  • ribbon.ReadTimeout: Feign 请求读取数据超时时间
  • timeoutInMilliseconds:Feign 连接超时时间
  • cluster-name:Elasticsearch 的集群节点名称,这里需要和 Elasticsearch 集群节点名称保持一致
  • cluster-nodes:Elasticsearch 节点通信地址
        
    (3)启动类
        创建 SearchApplication 作为搜索微服务工程的启动类:
@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。
    

3、数据导入

在这里插入图片描述
数据导入流程如下:

  • 请求 search 服务
  • 根据注册中心中的注册的 goods 服务的地址,使用 Feign 方式查询所有已经审核的 Sku
  • 使用 SpringData Elasticsearch 将查询到的 Sku集合导入到 Elasticsearch 中
        

实现过程:

  • 创建一个 JavaBean,名为 skuInfo ,在该 JavaBean 中添加索引库映射配置
  • 创建 Feign,实现查询所有 sku 集合
  • 在 搜索微服务中调用 Feign,查询所有 sku 集合,并将 sku 集合转换成 skuinfo
  • 调用 dao(继承 ElasticsearchRepostory 接口),实现数据导入到 Elasticsearch 中
        

(1)文档映射 Bean 创建

     搜索商品的时候,不是所有的属性都需要分词搜索,我们创建 JavaBean,将 JavaBean 数据存入到 Elasticsearch 中,要以 搜索条件 和 搜索展示结果 为依据,部分关键搜索条件分析如下:

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

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

@Document(indexName = "skuinfo",type = "docs")
public class SkuInfo implements Serializable {
    
    

    // 商品id,同时也是商品编号
    @Id
    private Long id;

    /**
     * SKU名称
     * type=Field.Type.Text,支持分词
     * analyzer = "ik_smart",创建索引的分词器
     * index=true,表示开启分词(默认)
     * store=false,表示不存储(默认)
     */
   
    @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;

	 /**
     * type = FieldType.Keyword 不支持分词
     */
    // 类目名称
    @Field(type = FieldType.Keyword)
    private String categoryName;

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

    // 规格
    private String spec;

    // 规格参数
    private Map<String,Object> specMap;
   
   // getter、setter 方法略
}

     document 是 Elaticsearch 中的最小数据单元,一个 document 可以是一条商品数据,也可以是一条订单数据,通常用 JSON 格式表示,一个 document 里面有多个 field,每个 field 就是一个数据字段。属性上如果不加 @Field,是会自动添加域的。

(2) 搜索 Sku
     从数据导入的流程图中可以看出来,查询 sku 需要调用 goods 服务的 findAll 方法:
在这里插入图片描述

需要为它在 changgou-service-goods-api 包中提供 Feign 调用:

@FeignClient(value="goods")
@RequestMapping("/sku")
public interface SkuFeign {
    
    
    /**
     * 查询 sku 全部数据
     * @return
     */
    @GetMapping("/findAll")
    Result<List<Sku>> findAll();
  }

接下来,提供 dao :

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

在 changgou-service-search 中 通过 service 层调用 feign 和 这个 SkuEsMapper :

public class SkuServiceImpl implements SkuService {
    
    

    @Autowired
    SkuEsMapper skuEsMapper;
    @Autowired
    SkuFeign skuFeign;

    /**
     * 导入索引库
     */
    @Override
    public void importData() {
    
    

        // Feign 调用,查询 List<Sku>
        Result<List<Sku>> skus = skuFeign.findAll();

        // 将 List<Sku> 转化成 List<SkuInfo>
        List<SkuInfo> skuInfos = JSON.parseArray(JSON.toJSONString(skus.getData()), SkuInfo.class);

        // 调用 dao 实现数据批量导入
        skuEsMapper.saveAll(skuInfos);
    }
}

因为用到了 Feign 和 SkuEsMapper 的调用,所以需要在启动类上添加 @EnableFeignClients 和 @EnableElasticsearchRepositories 注解:
在这里插入图片描述

提供控制层:

@RestController
@RequestMapping("/search")
@CrossOrigin
public class SkuController {
    
    
    
    @Autowired
    private SkuService skuService;
    
    @GetMapping("/import")
    public Result importData(){
    
    

	   // 数据导入
       skuService.importData();
       return new Result(true, StatusCode.OK,"导入数据到索引库成功!");
    }
}

运行结果:
在这里插入图片描述
在这里插入图片描述
现在有一个问题,我们设计的前端展示页面里,有 “显示屏尺寸”、“摄像头参数” :
在这里插入图片描述

而我们导入到索引库里的数据是这样的:
在这里插入图片描述
     我们需要提供 规格 和 对应的 域,之前在 SkuInfo 类中提供了 Map 类型的属性 specMap, spec 是 String 类型,我们把它转换成 Map,把它的 key 都提取出来,作为域的名称。在 importData 方法中添加:

 // 遍历当前 skuInfos,获取列表规格列表
    for (SkuInfo skuInfo : skuInfos) {
    
    
        Map<String, Object> specMap = JSON.parseObject(skuInfo.getSpec(), Map.class);
        
        // 将域存入 Map<String, Object>,key 就会生成动态域,
        // 域的名字为 key,值为 value
        skuInfo.setSpecMap(specMap);
    } 

这时删除 skuinfo 索引,重新运行,可以看到:
在这里插入图片描述
     这样和前端里的规格栏是相应的。
    

四、关键字搜索

     我们先使用 SpringDataElasticsearch 实现一个简单的搜索功能,先实现 根据关键字搜索,从上面搜索图片可以看得到,每次搜索的时候,除了关键字外,还有可能有品牌、分类、规格等,后台接收搜索条件 使用 Map 接收比较合适。
    
(1)Service 层
修改 search 服务的 com.changgou.search.service.SkuService类,添加搜索方法:

Map search(Map<String, String> searchMap);

     在 SkuServiceImpl 实现类中添加 ElasticsearchTemplat 类型属性,用来实现索引库的增删改查,适用于高级搜索:

   @Autowired
    ElasticsearchTemplate elasticsearchTemplate;

实现:

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

    // 搜索条件构建对象
    NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder();

    if(searchMap!=null && searchMap.size()>0){
    
    

        // 根据关键词搜索
        String keyWords = searchMap.get("keywords");

        if(!StringUtils.isEmpty(keyWords)){
    
    
            builder.withQuery(
                    QueryBuilders.queryStringQuery(keyWords).field("name"));
        }

    }

    // 第二个参数需要搜索的结果类型(页面展示的是集合数据)
    // AggregatedPage<SkuInfo> 是对结果集的封装
    AggregatedPage<SkuInfo> page = elasticsearchTemplate.queryForPage(
            builder.build(), SkuInfo.class);

    // 获取数据结果集
    List<SkuInfo> contents = page.getContent();

    // 获取总记录数
    long totalNums = page.getTotalElements();

    // 获取总页数
    int totalPages = page.getTotalPages();

    // 封装 Map 存储数据作为结果
    Map<String,Object> resultMap=new HashMap<String,Object>();
    resultMap.put("rows",contents);
    resultMap.put("totalNums",totalNums);
    resultMap.put("totalPages", totalPages);
    return resultMap;
}

(2)控制层
在 com.changgou.search.controller.SkuController 增加方法:

    @GetMapping
    public Map search(@RequestParam(required = false) Map<String,String> searchMap) throws Exception{
    
    
        return  skuService.search(searchMap);
    }

测试一下,访问 http://localhost:18085/search,运行结果:
在这里插入图片描述

在这里插入图片描述

五、分类统计

在这里插入图片描述
     可以看到,这个前端页面中第一栏 “分类”,如果选择的分类不同,那么后续展示的也应该是不同的,比如,配件就不需要网络制式。所以,我们有必要对分类进行分组,先根据分类进行分组,因为对于不同的分类,页面展示的商品也是不同的。先实现根据分类查询商品的功能。
     先看看如果是 SQL 语句,在执行搜索的时候,第 1 条 SQL 语句是执行搜索,第 2 条语句是根据 分类名字 分组查看有多少分类。

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

     每次执行 搜索 的时候,需要显示商品分类名称,这里要显示的分类名称,其实就是符合搜素条件的所有商品的分类集合,我们可以按照上面的实现思路,使用 ES 根据分组名称,做一次分组查询。

(1) 分类分组统计实现
修改 SkuServiceImpl 类代码,search 方法变为:

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

        // 搜索条件构建对象
        NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder();

        // 如果关键词不为空
        if (searchMap != null && searchMap.size() > 0) {
    
    

            // 根据关键词搜索
            String keyWords = searchMap.get("keywords");

            if (!StringUtils.isEmpty(keyWords)) {
    
    
                builder.withQuery(
                        QueryBuilders.queryStringQuery(keyWords).field("name"));
            }
        }
        // 第二个参数需要搜索的结果类型(页面展示的是集合数据)
        // AggregatedPage<SkuInfo> 是对结果集的封装
        AggregatedPage<SkuInfo> page = elasticsearchTemplate.queryForPage(
                builder.build(), SkuInfo.class);


        /** 根据分类进行分组查询 */
        // 添加聚合操作
        builder.addAggregation(AggregationBuilders.terms("skuCategory").field("categoryName.keyword"));

        AggregatedPage<SkuInfo> page1 = elasticsearchTemplate.queryForPage(
                builder.build(), SkuInfo.class);

        // 因为可以根据多个域分组,所以先获取集合,再获取指定域的数据
        // 这时获取到的是 {电脑,手机配件,电视}
        StringTerms stringTerms = page1.getAggregations().get("skuCategory");

        List<String> categoryList=new ArrayList<>();
        for(StringTerms.Bucket bucket:stringTerms.getBuckets()){
    
    
            String categoryName=bucket.getKeyAsString();
            categoryList.add(categoryName);
        }


        /** 根据分类进行分组查询 */


        // 获取数据结果集
        List<SkuInfo> contents = page.getContent();

        // 获取总记录数
        long totalNums = page.getTotalElements();

        // 获取总页数
        int totalPages = page.getTotalPages();

        // 封装 Map 存储数据作为结果
        Map<String, Object> resultMap = new HashMap<String, Object>();
        resultMap.put("rows", contents);
        resultMap.put("totalNums", totalNums);
        resultMap.put("totalPages", totalPages);

        resultMap.put("categoryList",categoryList);
        return resultMap;
    }

运行结果:
在这里插入图片描述

六、总结

(1)Elasticsearch 是一个 高扩展的 分布式全文检索引擎,它可以近乎实时地存储、检索数据。MySQL 通过 数据库 存储数据,Elasticsearch 通过 索引库 存储索引。
    
(2)IK 分词器 是一个基于 Java 开发的 轻量级 中文分词工具包。
    
(3)Kibana 相当于 Elasticsearch 的数据分析工具,在浏览器里 看数据很直观(比如各种图表)。在 kibana 的配置文件中,会指向 Elasticsearch 的地址。
    
(4)Spring Data ElasticSearch 是 对 操作 elasticSearch 的客户端 API 进行的封装。
    
(5)因为搜索商品是对 sku 进行搜索,所以需要向 Elasticsearch 索引库中导入 sku 数据。需要提供 SkuInfo (是个 Java Bean) ,做索引库映射配置。然后把从 goods 商品微服务中查询到的 sku 数据,转化成 skuInfo, 调用继承了 ElasticsearchRepostory 接口的 skuEsMapper 的saveAll 方法,即可实现数据导入到 Elasticsearch。
     skuInfo 类上使用 @Document 注解,并声明索引名和类型,它的属性使用 @Field 注解,并声明是否开启分词和存储。skuInfo 对象的属性就对应域,一个个对象就对应一个个记录。
    
(6)使用 ElasticsearchTemplat 类,实现索引库的增删改查,适用于高级搜索。需要先使用 NativeSearchQueryBuilder 构建条件,然后调用具体查询语句,比如,本文中根据分类名称 categoryName 进行分组查询。

猜你喜欢

转载自blog.csdn.net/weixin_41750142/article/details/114383724
今日推荐