第六章
搜索解决方案-Elasticsearch
优就业.JAVA教研室
学习目标
- Elasticsearch、Kibana环境配置
- DSL语句讲解
- 商品索引库导入讲解
- 商品关键词搜索实现
- 分类统计搜索
一、Elasticsearch 安装
ElasticSearch是一个基于Lucene的搜索服务器。它提供了一个分布式多用户能力的全文搜索引擎,基于RESTfulweb接口。ElasticSearch是用Java开发的,并作为Apache许可条款下的开放源码发布,是当前流行的企业级搜索引擎。设计用于云计算中,能够达到实时搜索,稳定,可靠,快速,安装使用方便。构建在全文检索开源软件Lucene之上的Elasticsearch,不仅能对海量规模的数据完成分布式索引与检索,还能提供数据聚合分析。据国际权威的数据库产品评测机构DBEngines的统计,在2016年1月,Elasticsearch已超过Solr等,成为排名第一的搜索引擎类应用
简之:ElasticSearch是基于Restfull乐准的高扩展高可用的实时数据分析的全文搜索工具。
基本概念
此处需要注意:关于类型type在不同版本是有所变化的
Elasticsearch 官网提出的近期版本对 type 概念的演变情况如下:
在 5.X 版本中,一个 index 下可以创建多个 type;
在 6.X 版本中,一个 index 下只能存在一个 type;
在 7.X 版本中,直接去除了 type 的概念,就是说 index 不再会有 type。
主要目的就是 为了保持 Elasticsearch “一切为了搜索” 的宗旨
由于我们之前已经使用过elasticsearch了,这里不再对它进行过多的介绍了,直接下载安装,本章节将采用Docker安装,不过在市面上还有很多采用linxu安装,关于linux安装,已经提供了安装手册,这里就不讲了。
我们之前已经使用过elasticsearch了,这里不再对它进行介绍了,直接下载安装,本章节将采用Docker安装,不过在市面上还有很多采用linxu安装,关于linux安装,已经提供了安装手册,这里就不讲了。
(1)docker镜像下载
docker pull elasticsearch:7.7.0
注意:建议还原镜像备份文件
docker load -i elasticsearch7.7.tar
(2)安装es容器
因为elasticsearch在启动的时候会进行一些检查,比如最多打开的文件的个数以及虚拟内存区域数量等等,所以我们需要先进行系统调优,需要在虚拟机下设置max_map_count
,否则elasticsearch容器会启动不起来
查看max_map_count
的值 默认是65530
cat /proc/sys/vm/max_map_count
重新设置max_map_count
的值
sysctl -w vm.max_map_count=262144
然后执行运行容器命令,如下
docker run -di --name dym_es -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" -e "ES_JAVA_OPTS=-Xms512m -Xmx512m" elasticsearch:7.7.0
命令解释:
9200端口(Web管理平台端口) 9300(服务默认端口)
-e ES_JAVA_OPTS="-Xms512m -Xmx512m"
是设置占用内存大小,一般线上检索服务器-ES服务器32G,这里就小一点测试
-e "discovery.type=single-node"
这个是设置单节点运行
(3)开启远程连接及跨域配置
将elasticsearch容器内部的/usr/share/elasticsearch/config/elasticsearch.yml配置文件拷贝到虚拟机的root目录下,编辑增加以下命令。
拷贝命令:
docker cp dym_es:/usr/share/elasticsearch/config/elasticsearch.yml /root/elasticsearch.yml
编辑elasticsearch.yml
cluster.name: my-application
http.cors.enabled: true
http.cors.allow-origin: "*"
network.host: 0.0.0.0
其中:
cluster.name:自定义集群名称。
network.host:当前es节点绑定的ip地址,默认127.0.0.1,如果需要开放对外访问这个属性必须设置。http.cors.enabled: true:此步为允许elasticsearch跨域访问,默认是false。
http.cors.allow-origin: "*"
:表示跨域访问允许的域名地址(*表示任意)。
删除之前的es容器,并重新执行运行容器命令
docker rm -f dym_es
docker run -di --name dym_es -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" -e "ES_JAVA_OPTS=-Xms512m -Xmx512m" -v /root/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml elasticsearch:7.7.0
浏览器输入地址访问:`http://192.168.188.128:9200/
小提示:如果想让容器开启重启,可以执行下面命令
docker update --restart=always dym_es
二、IK分词器安装
(1)安装ik分词器
IK分词器下载地址https://github.com/medcl/elasticsearch-analysis-ik/releases
上传分词器压缩文件到linux服务器,拷贝文件到ES容器
docker cp /root/elasticsearch-analysis-ik-7.7.0.zip dym_es:/usr/share/elasticsearch/plugins/elasticsearch-analysis-ik-7.7.0.zip
进入容器安装ik分词器
docker exec -it dym_es /bin/bash
cd /usr/share/elasticsearch/plugins/
在plugins目录下安装ik分词器,ik分词器需要和es版本一致
使用上传的插件压缩包安装:
mkdir ik
mv elasticsearch-analysis-ik-7.7.0.zip ik/
cd ik
unzip elasticsearch-analysis-ik-7.7.0.zip
也可以基于网络安装:
elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.7.0/elasticsearch-analysis-ik-7.7.0.zip
退出容器并重启
docker restart dym_es
测试安装成功,地址栏输入:
http://192.168.188.128:9200/_cat/plugins
IK分词器有两种分词模式:ik_max_word和ik_smart模式。
1、ik_max_word
会将文本做最细粒度的拆分,比如会将“中华人民共和国人民大会堂”拆分为“中华人民、中华、华人、人民共和国、人民、共和国、大会堂、大会、会堂等词语。
2、ik_smart 会做最粗粒度的拆分,比如会将“中华人民共和国人民大会堂”拆分为中华人民共和国、人民大会堂。
两种分词器使用的最佳实践是:索引时用ik_max_word,在搜索时用ik_smart。 即:索引时最大化的将文章内容分词,搜索时更精确的搜索到想要的结果。
三、 Kibana使用-掌握DSL语句
我们这里需要一个更专业的工具实现对日志的实时分析,也就是我们接下来要讲的kibana。
Kibana 是一款开源的数据分析和可视化平台,它是 Elastic Stack 成员之一,设计用于和 Elasticsearch 协作。您可以使用 Kibana 对 Elasticsearch 索引中的数据进行搜索、查看、交互操作。您可以很方便的利用图表、表格及地图对数据进行多元化的分析和呈现。
Kibana 可以使大数据通俗易懂。它很简单,基于浏览器的界面便于您快速创建和分享动态数据仪表板来追踪 Elasticsearch 的实时数据变化。
搭建 Kibana 非常简单。您可以分分钟完成 Kibana 的安装并开始探索 Elasticsearch 的索引数据 — 没有代码、不需要额外的基础设施。
3.1 Kibana下载安装
我们项目中不再使用linux,直接使用Docker,所有这里就不演示在windows的下载安装了。
(1)镜像下载
docker pull kibana:7.7.0
为了节省时间,建议还原虚拟机备份文件
docker load -i kibana7.7.tar
(2)安装kibana容器
执行如下命令,开始安装kibana容器
docker run -di --link dym_es:elasticsearch --name kibana --restart=always -p 5601:5601 kibana:7.7.0
restart=always:每次服务都会重启,也就是开启启动
5601:5601:端口号
(3)访问测试
访问http://192.168.188.128:5601
如下:
3.2 Kibana使用
3.2.1 配置索引
此处我们选择 图中所示:自己的数据
选择上图的设置图标
接下来就可以和es进行交互了
3.2.2 索引管理
要使用Kibana,您必须至少配置一个索引(但是要求Elasticsearch中要有数据)。索引用于标识Elasticsearch索引以运行搜索和分析。它们还用于配置字段等。下面我们看如何管理查看索引。
点击 设置-选择D -Manager space
选择 Index Patterns
创建一个索引
没有数据目前不能建立索引
后面添加过索引可以通过如下方法查看添加的索引
输入索引规则,点击下一步
点击创建
域的每个标题选项分别代表如下意思:
也可以直接查看 选择 Elasticsearch-Index Managerment 查看创建的索引
3.2.3 DSL语句使用
3.2.3.1 Query DSL结构化查询介绍
Query DSL是一个Java开源框架用于构建类型安全的SQL查询语句。采用API代替传统的拼接字符串来构造查询语句。目前Querydsl支持的平台包括JPA,JDO,SQL,Java Collections,RDF,Lucene,Hibernate Search。elasticsearch提供了一整套基于JSON的查询DSL语言来定义查询。 Query DSL当作是一系列的抽象的查询表达式树(AST)特定查询能够包含其它的查询,(如 bool ), 有些查询能够包含过滤器(如 constant_score), 还有的可以同时包含查询和过滤器 (如 filtered). 都能够从ES支持查询集合里面选择任意一个查询或者是从过滤器集合里面挑选出任意一个过滤器, 这样的话,我们就可以构造出任意复杂(maybe 非常有趣)的查询了。
3.2.3.2 测试分词
(1)ik_smart
POST _analyze
{
"analyzer": "ik_smart",
"text": "中华人民共和国人民大会堂"
}
(2) ik_max_word
POST _analyze
{
"analyzer": "ik_max_word",
"text": "中华人民共和国人民大会堂"
}
四、数据导入ES
4.1 SpringData Elasticsearch介绍
4.1.1 SpringData介绍
Spring Data是一个用于简化数据库访问,并支持云服务的开源框架。其主要目标是使得对数据的访问变得方便快捷,并支持map-reduce框架和云计算数据服务。 Spring Data可以极大的简化JPA的写法,可以在几乎不用写实现的情况下,实现对数据的访问和操作。除了CRUD外,还包括如分页、排序等一些常用的功能。
Spring Data的官网:https://spring.io/projects/spring-data
4.1.2 SpringData ES介绍
Spring Data ElasticSearch 基于 spring data API 简化 elasticSearch操作,将原始操作elasticSearch的客户端API 进行封装 。Spring Data为Elasticsearch项目提供集成搜索引擎。Spring Data Elasticsearch POJO的关键功能区域为中心的模型与Elastichsearch交互文档和轻松地编写一个存储库数据访问层。 官方网站:https://spring.io/projects/spring-data-elasticsearch
4.2 搜索工程搭建
创建搜索微服务工程,dongyimai-search-service,该工程主要提供搜索服务以及索引数据的更新操作。
(1)API工程搭建
首先创建search的API工程,在dongyimai-service-api中创建dongyimai-search-service-api,如下图:
pom.xml如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>dongyimai-service-api</artifactId>
<groupId>com.offcn</groupId>
<version>0.0.1</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>dongyimai-search-service-api</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<!--goods API依赖-->
<dependency>
<groupId>com.offcn</groupId>
<artifactId>dongyimai-sellergoods-serivce-api</artifactId>
<version>1.0</version>
</dependency>
<!--SpringDataES依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
</dependencies>
</project>
(2)搜索微服务搭建
在dongyimai-service中搭建dongyimai-search-service微服务,并进行相关配置。
pom.xml配置
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>dongyimai-service</artifactId>
<groupId>com.offcn</groupId>
<version>0.0.1</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>dongyimai-search-service</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>com.offcn</groupId>
<artifactId>dongyimai-search-service-api</artifactId>
<version>1.0</version>
</dependency>
</dependencies>
</project>
application.yml配置
server:
port: 9005
spring:
application:
name: search
elasticsearch:
rest:
uris: 192.168.188.128:9200 #此处配置elasticsearch的访问地址
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:8761/eureka
instance:
prefer-ip-address: true
feign:
hystrix:
enabled: true
#超时配置
ribbon:
ReadTimeout: 300000
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 10000
(3)启动类
创建SearchApplication作为搜索微服务工程的启动类,代码如下:
@SpringBootApplication(exclude = {DruidDataSourceAutoConfigure.class, DataSourceAutoConfiguration.class})
@EnableEurekaClient
public class SearchApplication {
public static void main(String[] args) {
SpringApplication.run(SearchApplication.class,args);
}
}
分别创建对应的包,dao、service、controller,如下图:
4.3 数据导入
现在需要将数据从数据库中查询出来,然后将数据导入到ES中。
数据导入流程如下:
1.请求search服务,调用数据导入地址
2.根据注册中心中的注册的goods服务的地址,使用Feign方式查询所有已经审核的Sku
3.使用SpringData Es将查询到的Sku集合导入到ES中
4.3.1 文档映射Bean创建
搜索商品的时候,会根据如下属性搜索数据,并且不是所有的属性都需要分词搜索,我们创建JavaBean,将JavaBean数据存入到ES中要以搜索条件和搜索展示结果为依据,部分关键搜索条件分析如下:
1.可能会根据商品名称搜索,而且可以搜索商品名称中的任意一个词语,所以需要分词
2.可能会根据商品分类搜索,商品分类不需要分词
3.可能会根据商品品牌搜索,商品品牌不需要分词
4.可能会根据商品商家搜索,商品商家不需要分词
5.可能根据规格进行搜索,规格时一个键值对结构,用Map
根据上面的分析,我们可以在dongyimai-search-service-api工程中创建com.offcn.search.pojo.SkuInfo,如下
@Document(indexName = "skuinfo")
public class SkuInfo implements Serializable {
//商品id,同时也是商品编号
@Id
private Long id;
//SKU名称
@Field(type = FieldType.Text, analyzer = "ik_smart")
private String title;
//商品价格,单位为:元
@Field(type = FieldType.Double)
private BigDecimal price;
//库存数量
private Integer num;
//商品图片
private String image;
//商品状态,1-正常,2-下架,3-删除
private String status;
//创建时间
private Date createTime;
//更新时间
private Date updateTime;
//是否默认
private String isDefault;
//goodsId
private Long goodsId;
//类目ID
private Long categoryId;
//类目名称
// @Field(type = FieldType.Keyword)
private String category;
//品牌名称
// @Field(type = FieldType.Keyword)
private String brand;
//规格
private String spec;
//规格参数
private Map<String,Object> specMap;
//对应的get、set方法省略
....
}
4.3.2 搜索审核通过Sku
修改dongyimai-sellergoods-service微服务,添加搜索审核通过的Sku,供search微服务调用。
下面都是针对goods微服务的操作。
修改ItemService接口,添加根据状态查询Sku方法,代码如下:
/**
* 根据状态查询SKU列表
*/
List<Item> findByStatus(String status);
修改ItemServiceImpl,添加根据状态查询Sku实现方法,代码如下:
@Override
public List<Item> findByStatus(String status) {
QueryWrapper<Item> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("status",status);
return this.list(queryWrapper);
}
修改com.offcn.sellergoods.controller.ItemController,添加根据审核状态查询Sku方法,代码如下:
/***
* 根据审核状态查询Sku
* @param status
* @return
*/
@GetMapping("/status/{status}")
public Result<List<Item>> findByStatus(@PathVariable String status){
List<Item> list = itemService.findByStatus(status);
return new Result<List<Item>>(true,StatusCode.OK,"查询成功",list);
}
访问地址:http://localhost:9001/item/status/1
4.3.3 Sku导入ES实现
(1) Feign配置
修改dongyimai-sellergoods-service-api工程,在com.offcn.sellergoods.feign.ItemFeign上添加findSkuList方法,代码如下:
@FeignClient(value="dym-sellergoods")
public interface ItemFeign {
/***
* 根据审核状态查询Sku
* @param status
* @return
*/
@GetMapping("/item/status/{status}")
Result<List<Item>> findByStatus(@PathVariable String status);
}
(2) Dao创建
修改dongyimai-search-service工程,创建com.dongyimai.search.dao.SkuEsMapper,该接口主要用于索引数据操作,主要使用它来实现将数据导入到ES索引库中,代码如下:
@Repository
public interface SkuEsMapper extends ElasticsearchRepository<SkuInfo,Long> {
}
(3) 服务层创建
修改dongyimai-search-service工程,创建com.dongyimai.search.service.SkuService,代码如下:
public interface SkuService {
/***
* 导入SKU数据
*/
void importSku();
}
修改dongyimai-search-service工程,创建com.offcn.search.service.impl.SkuServiceImpl,实现Sku数据导入到ES中,代码如下:
@Service
public class SkuServiceImpl implements SkuService {
@Autowired
private SkuEsMapper skuEsMapper;
@Autowired
private ItemFeign itemFeign;
@Override
public void importSku() {
//调用商品微服务,获取sku商品数据
Result<List<Item>> result = itemFeign.findByStatus("1");
//把返回的结果返回搜索实体类数据集合(采用深克隆技术,把Item对象属性值复制到SkuInfo对象)
List<SkuInfo> skuInfoList = JSON.parseArray(JSON.toJSONString(result.getData()), SkuInfo.class);
//遍历sku集合
for (SkuInfo skuInfo : skuInfoList) {
//获取规格json字符串,转换为map对象
Map<String,Object> map = JSON.parseObject(skuInfo.getSpec(), Map.class);
//关联设置到specMap
skuInfo.setSpecMap(specMap);
}
//保存sku集合数据到es
skuEsMapper.saveAll(skuInfoList);
}
}
(4)控制层配置
修改dongyimai-search-service工程,在com.offcn.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包扫描,代码如下:
@SpringBootApplication(exclude={DataSourceAutoConfiguration.class})
@EnableEurekaClient
@EnableFeignClients(basePackages = "com.offcn.sellergoods.feign")
@EnableElasticsearchRepositories(basePackages = "com.offcn.search.dao")
public class SearchApplication {
public static void main(String[] args) {
SpringApplication.run(SearchApplication.class,args);
}
}
注意如果出现错误:
解决办法,修改application.yml增加如下配置:
spring:
main:
allow-bean-definition-overriding: true
(6)测试
调用http://localhost:9005/search/import进行测试
打开Kibana可以看到如下数据:
http://192.168.188.128:5601
GET /skuinfo/_search
五、关键字搜索
我们先使用SpringDataElasticsearch实现一个简单的搜索功能,先实现根据关键字搜索,从上面搜索图片可以看得到,每次搜索的时候,除了关键字外,还有可能有品牌、分类、规格等,后台接收搜索条件使用Map接收比较合适。
5.1 服务层实现
修改search服务的com.offcn.search.service.SkuService,添加搜索方法,代码如下:
/***
* 搜索
* @param searchMap
* @return
*/
Map search(Map<String, String> searchMap);
修改search服务的com.dongyimai.search.service.impl.SkuServiceImpl,添加搜索实现方法,代码如下:
@Autowired
private ElasticsearchRestTemplate elasticsearchRestTemplate;
public Map search(Map<String, String> searchMap) {
//1.获取关键字的值
String keywords = searchMap.get("keywords");
if (StringUtils.isEmpty(keywords)) {
keywords = "华为";//赋值给一个默认的值
}
//2.创建查询对象 的构建对象
NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
//3.设置查询的条件
//使用:QueryBuilders.matchQuery("title", keywords) ,搜索华为 ---> 华 为 二字可以拆分查询,
//使用:QueryBuilders.matchPhraseQuery("title", keywords) 华为二字不拆分查询
nativeSearchQueryBuilder.withQuery(QueryBuilders.matchQuery("title", keywords));
//4.构建查询对象
NativeSearchQuery query = nativeSearchQueryBuilder.build();
//5.执行查询
SearchHits<SkuInfo> searchHits = elasticsearchRestTemplate.search(query, SkuInfo.class);
//对搜索searchHits集合进行分页封装
SearchPage<SkuInfo> skuPage = SearchHitSupport.searchPageFor(searchHits, query.getPageable());
//遍历取出查询的商品信息
List<SkuInfo> skuList=new ArrayList<>();
for (SearchHit<SkuInfo> searchHit :skuPage.getContent()) { // 获取搜索到的数据
SkuInfo content = (SkuInfo) searchHit.getContent();
SkuInfo skuInfo = new SkuInfo();
//把一个对象属性值复制到另外一个对象:两个对象属性名称必须一致,属性类型必须相同
BeanUtils.copyProperties(content, skuInfo);
skuList.add(skuInfo);
}
//6.返回结果
Map resultMap = new HashMap<>();
resultMap.put("rows", skuList);//获取所需SkuInfo集合数据内容
resultMap.put("total",searchHits.getTotalHits());//总记录数
resultMap.put("totalPages", skuPage.getTotalPages());//总页数
return resultMap;
}
注意: ElasticSearchTemplate更多是对ESRepository的补充,里面提供了一些更底层的方法,更加适用于查询数据
5.2 控制层实现
修改com.offcn.search.controller.SkuController,在控制层调用Service层即可,代码如下:
/**
* 搜索
* @param searchMap
* @return
*/
@PostMapping
public Map search(@RequestBody(required = false) Map searchMap){
return skuService.search(searchMap);
}
5.3 测试
使用Postman工具,输入http://localhost:9005/search
选中POST提交
六、 分类统计
6.1 分类统计分析
看下面的SQL语句,我们在执行搜索的时候,第1条SQL语句是执行搜,第2条语句是根据分类名字分组查看有多少分类,大概执行了2个步骤就可以获取数据结果以及分类统计,我们可以发现他们的搜索条件完全一样。
-- 查询所有
SELECT * FROM tb_item WHERE title LIKE '%手机%';
-- 根据分类名字分组查询
SELECT category FROM tb_item WHERE title LIKE '%手机%' GROUP BY category;
我们每次执行搜索的时候,需要显示商品分类名称,这里要显示的分类名称其实就是符合搜素条件的所有商品的分类集合,我们可以按照上面的实现思路,使用ES根据分组名称做一次分组查询即可实现。
6.2 分类分组统计实现
修改search微服务的com.offcn.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();
// 设置分组的条件 terms后表示分组查询后的列名,注意搜索字段后面加.keyword
nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms("skuCategorygroup").field("category.keyword"));
//3.设置查询的条件
//使用:QueryBuilders.matchQuery("title", keywords) ,搜索华为 ---> 华 为 二字可以拆分查询,
//使用:QueryBuilders.matchPhraseQuery("title", keywords) 华为二字不拆分查询
nativeSearchQueryBuilder.withQuery(QueryBuilders.matchQuery("title", keywords));
//4.构建查询对象
NativeSearchQuery query = nativeSearchQueryBuilder.build();
//5.执行查询
SearchHits<SkuInfo> searchHits = elasticsearchRestTemplate.search(query, SkuInfo.class);
//对搜索searchHits集合进行分页封装
SearchPage<SkuInfo> skuPage = SearchHitSupport.searchPageFor(searchHits, query.getPageable());
//获取分组结果
Terms terms = searchHits.getAggregations().get("skuCategorygroup");
// 获取分类名称集合
List<String> categoryList = new ArrayList<>();
if (terms != null) {
for (Terms.Bucket bucket : terms.getBuckets()) {
String keyAsString = bucket.getKeyAsString();// 分组的值(分类名称)
categoryList.add(keyAsString);
}
}
//遍历取出查询的商品信息
List<SkuInfo> skuList=new ArrayList<>();
for (SearchHit<SkuInfo> searchHit :skuPage.getContent()) { // 获取搜索到的数据
SkuInfo content = (SkuInfo) searchHit.getContent();
SkuInfo skuInfo = new SkuInfo();
BeanUtils.copyProperties(content, skuInfo);
skuList.add(skuInfo);
}
//6.返回结果
Map resultMap = new HashMap<>();
resultMap.put("rows", skuList);//获取所需SkuInfo集合数据内容
resultMap.put("total",searchHits.getTotalHits());//总记录数
resultMap.put("totalPages", skuPage.getTotalPages());//总页数
resultMap.put("categoryList",categoryList);
return resultMap;
}
添加的代码如下:
注意分组搜索要加keyword,因为数据存储结构是keyword类型
具体查看数据结构可以使用如下指令:
GET /skuinfo/_mapping
6.3 测试
请求http://localhost:9005/search
6.4 代码优化
如上,可以将获取分组的代码进行提取,如下代码所示:
/**
* 获取分类列表数据
* @param terms
* @return
**/
private List<String> getStringsCategoryList(Terms terms) {
List<String> categoryList = new ArrayList<>();
if (terms != null) {
for (Terms.Bucket bucket : terms.getBuckets()) {
String keyAsString = bucket.getKeyAsString();// 分组的值(分类名称)
categoryList.add(keyAsString);
}
}
return categoryList;
}
在search方法中进行调用:
// 获取分类名称集合
List<String> categoryList = this.getStringsCategoryList(terms);
整体代码如下:
package com.offcn.search.service.impl;
import com.alibaba.fastjson.JSON;
import com.offcn.entity.Result;
import com.offcn.search.dao.SkuEsMapper;
import com.offcn.search.pojo.SkuInfo;
import com.offcn.search.service.SkuService;
import com.offcn.sellergoods.feign.SkuFeign;
import com.offcn.sellergoods.pojo.Item;
import org.apache.commons.lang.StringUtils;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.elasticsearch.core.*;
import org.springframework.data.elasticsearch.core.query.NativeSearchQuery;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Service
public class SkuServiceImpl implements SkuService {
@Autowired
private SkuEsMapper skuEsMapper;
@Autowired
private SkuFeign skuFeign;
/***
* 导入SKU数据
*/
@Override
public void importSku() {
//调用商品微服务,获取sku商品数据
Result<List<Item>> result = skuFeign.findByStatus("1");
//把数据转换为搜索实体类数据
List<SkuInfo> skuInfoList = JSON.parseArray(JSON.toJSONString(result.getData()), SkuInfo.class);
//遍历sku集合
for (SkuInfo skuInfo : skuInfoList) {
//获取规格
Map<String, Object> specMap = JSON.parseObject(skuInfo.getSpec());
//关联设置到specMap
skuInfo.setSpecMap(specMap);
}
//保存sku集合数据到es
skuEsMapper.saveAll(skuInfoList);
}
@Autowired
private ElasticsearchRestTemplate elasticsearchRestTemplate;
public Map search(Map<String, String> searchMap) {
//1.获取关键字的值
String keywords = searchMap.get("keywords");
if (StringUtils.isEmpty(keywords)) {
keywords = "华为";//赋值给一个默认的值
}
//2.创建查询对象 的构建对象
NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
// 设置分组的条件 terms后表示分组查询后的列名
nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms("skuCategorygroup").field("category.keyword"));
//3.设置查询的条件
//使用:QueryBuilders.matchQuery("title", keywords) ,搜索华为 ---> 华 为 二字可以拆分查询,
//使用:QueryBuilders.matchPhraseQuery("title", keywords) 华为二字不拆分查询
nativeSearchQueryBuilder.withQuery(QueryBuilders.matchQuery("title", keywords));
//4.构建查询对象
NativeSearchQuery query = nativeSearchQueryBuilder.build();
//5.执行查询
SearchHits<SkuInfo> searchHits = elasticsearchRestTemplate.search(query, SkuInfo.class);
//对搜索searchHits集合进行分页封装
SearchPage<SkuInfo> skuPage = SearchHitSupport.searchPageFor(searchHits, query.getPageable());
//获取分组结果
Terms terms = searchHits.getAggregations().get("skuCategorygroup");
// 获取分类名称集合
List<String> categoryList = this.getStringsCategoryList(terms);
//遍历取出查询的商品信息
List<SkuInfo> skuList=new ArrayList<>();
for (SearchHit<SkuInfo> searchHit :skuPage.getContent()) { // 获取搜索到的数据
SkuInfo content = (SkuInfo) searchHit.getContent();
SkuInfo skuInfo = new SkuInfo();
BeanUtils.copyProperties(content, skuInfo);
skuList.add(skuInfo);
}
//6.返回结果
Map resultMap = new HashMap<>();
resultMap.put("rows", skuList);//获取所需SkuInfo集合数据内容
resultMap.put("total",searchHits.getTotalHits());//总记录数
resultMap.put("totalPages", skuPage.getTotalPages());//总页数
resultMap.put("categoryList",categoryList);
return resultMap;
}
/**
* 获取分类列表数据
* @param terms
* @return
**/
private List<String> getStringsCategoryList(Terms terms) {
List<String> categoryList = new ArrayList<>();
if (terms != null) {
for (Terms.Bucket bucket : terms.getBuckets()) {
String keyAsString = bucket.getKeyAsString();// 分组的值(分类名称)
categoryList.add(keyAsString);
}
}
return categoryList;
}
}