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:18085/search/import
需要用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;
}