文章目录
一、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=我是程序员
访问: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 如下:
-
Kibana 使用
-
- 配置索引
要使用 Kibana,必须至少配置一个索引。索引用于标识 Elasticsearch 索引,以运行搜索和分析。它们还用于配置字段,输入索引后,点击 create,会展示出当前配置的索引的域信息:
- 配置索引
-
- 数据搜索
Discover 为 数据搜索 部分,可以对日志信息进行搜索操作。
可以使用 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 进行分组查询。