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/
微服搭建
依赖pom.xml
<!-- SpringDataES依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
配置application.yml
spring:
application:
name: search
data:
elasticsearch:
cluster-name: my-application # es配置中的集群名字
cluster-nodes: 192.168.169.140:9300
cluster-name:Elasticsearch的集群节点名称,这里需要和Elasticsearch集群节点名称保持一致
cluster-nodes:Elasticsearch节点通信地址,端口是9300
启动类
@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);
}
}
model层
映射索引库
都是elasticsearch包
import lombok.Data;
import lombok.NoArgsConstructor;
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;
/**
* Title:映射索引库的javabean
* Description:
* @author WZQ
* @version 1.0.0
* @date 2020/3/5
*/
@Document(indexName = "skuinfo",type = "docs") // 索引库名
@Data
@NoArgsConstructor
public class SkuInfo implements Serializable {
//商品id,同时也是商品编号
@Id
private Long id;
//SKU名称
/**
* Field注解属性
* type = FieldType.Text:类型,Text文本,适用分词
* index = true:添加数据的时候,是否分词
* analyzer = "ik_smart":创建索引分词器
* store = false:是否存储
* searchAnalyzer = "ik_smart":搜索的时候是否使用分词
*/
@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;
//类目名称
/**
* FieldType.Keyword:不分词
*/
@Field(type = FieldType.Keyword)
private String categoryName;
//品牌名称
@Field(type = FieldType.Keyword)
private String brandName;
//规格
private String spec;
//规格参数
private Map<String,Object> specMap; // 数据不同地方,可以用map存数据
}
数据导入
查询数据库数据或是独立出来search微服务,则可以利用fegin调用其他微服务的方法查询出来数据,转化为索引映射javaBean,再把数据导入ES,数据太多的话,建议分页查询数据库数据
dao层
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import org.springframework.stereotype.Repository;
// 给索引映射javabean:skuinfo、该javabean的主键类型:long。自定义
@Repository
public interface SkuEsMapper extends ElasticsearchRepository<SkuInfo, Long> {
}
service层
public interface SkuService {
/***
* 导入SKU数据到ES
*/
void importSku();
}
@Service
public class SkuServiceImpl implements SkuService {
// 这里例子是利用fegin,也可以该微服务对数据库查询,根据业务
@Resource
private SkuFeign skuFeign;
@Resource
private SkuEsMapper skuEsMapper;
/**
* 导入sku数据到es
*/
@Override
public void importSku(){
//根据业务拿到数据库数据
//feign调用goods微服务
ResponseResult<List<TbSku>> skuListResult = skuFeign.findByStatus("1");
//将json数据转成对应的索引映射类,fastjson
//字段名字匹配得上就可以转
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);
}
// es通用mapper方法批量存入到es索引库中
skuEsMapper.saveAll(skuInfos);
}
}
controller层:
@RestController
@RequestMapping(value = "/search")
@CrossOrigin
public class SkuController {
@Resource
private SkuService skuService;
/**
* 导入数据
* @return
*/
@GetMapping("/import")
public ResponseResult<Void> importData(){
skuService.importSku();
return new ResponseResult<Void>(true, StatusCode.OK,"导入数据到索引库中成功!");
}
}
启动类添加:
@EnableElasticsearchRepositories(basePackages = "search.dao")// dao路径
kibana查看数据或者elasticsearch-head
安装如下:
docker pull mobz/elasticsearch-head:5
docker run -di --name elasticsearch-head -p 9100:9100 mobz/elasticsearch-head:5
http://192.168.169.140:9100/
数据搜索
对应DSL方法查询
must多条件
match标准单个关键词
term过滤
prefix前缀
等等
service层实现:
@Service
public class SkuServiceImpl implements SkuService {
// ElasticsearchTemplate可以实现对索引库的增删改查
@Resource
private ElasticsearchTemplate esTemplate;
@Override
public Map<String, Object> search(Map<String, String> searchMap) {
//创建查询对象 构建对象
NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
//获取关键字的值
//这里可以不止关键字,多条件也行,判断加条件进去
String keywords = null;
if (searchMap != null && searchMap.size() > 0){
// 不为空
if (!StringUtils.isEmpty(searchMap.get("keywords"))){
keywords = searchMap.get("keywords");
//条件一:设置关键词查询条件
//match、term等对应dsl语句
//域名,值
nativeSearchQueryBuilder.withQuery(QueryBuilders.matchQuery("name", keywords));
}
}
//条件二:设置分组条件addAggregation-->group by 商品分类
//categoryName是域名,代码自定义skuCategorygroup组名
nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms("skuCategorygroup").field("categoryName").size(50));
//构建查询对象
NativeSearchQuery query = nativeSearchQueryBuilder.build();
//执行查询
//带上索引映射类
AggregatedPage<SkuInfo> skuPage = esTemplate.queryForPage(query, SkuInfo.class);
//获取分组结果,对应分组名
StringTerms stringTermsCategory = (StringTerms) skuPage.getAggregation("skuCategorygroup");
List<String> categoryList = getStringsCategoryList(stringTermsCategory);
//获取每页显示个数和当前页码,一起返回
Pageable pageable = query.getPageable();
int pageSize = pageable.getPageSize();
int pageNumber = pageable.getPageNumber()+1; //从0开始
//返回结果
Map<String, Object> resultMap = new HashMap<>();
resultMap.put("categoryList", categoryList); // 分组结果,几种类型
resultMap.put("rows", skuPage.getContent()); // 数据
resultMap.put("total", skuPage.getTotalElements()); // 总记录数
resultMap.put("totalPages", skuPage.getTotalPages()); // 总页数
resultMap.put("pageSize", pageSize); // 每页显示个数
resultMap.put("pageNumber", pageNumber); // 当前页码
return resultMap;
}
/**
* 获取分类列表数据
* @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;
}
}
controller层:
/**
* 搜索
* @param searchMap
* @return
*/
@PostMapping
public Map<String, Object> search(@RequestBody(required = false) Map<String, String> searchMap){
return skuService.search(searchMap);
}
上图是规格的索引存储格式,真实数据在spechMap.规格名字.keyword中,所以找数据也是按照如下格式去找:
spechMap.key值.keyword
域是map类型的数据,这样可以拿到key对应的value值
拿到map数据要:spechMap.keyword
搜索条件
重点
搜索模板
@Service
public class ServiceImpl implements Service {
// ElasticsearchTemplate可以实现对索引库的增删改查
@Resource
private ElasticsearchTemplate esTemplate;
// 搜索条件和返回数据类型根据业务定义
@Override
public Map<String, Object> search(Map<String, String> searchMap) {
//创建查询对象 构建对象
NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
//判断前端条件是否为空
//多条件多个,组合条件得用bool
//带上条件,看业务
nativeSearchQueryBuilder.withQuery(QueryBuilders.matchQuery(域名字符串, 前端searchMap中的关键词));
//设置分组查询addAggregation-->SQL对应group by
//这里的分组是拿查询出来的数据统计该域的种类并返回
//可多个,可抽取分组查询方法,多个都是多个addAggregation,换域名组名
//域名字符串看kibana,可以是值中带值,比如值是Map类型:spec.mapkey
nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms(组名字符串).field(域名字符串).size(100)); // 默认10条数据,size添加分组的数据数
nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms(组名字符串).field(域名字符串).size(100)); // 可多个,多个的话最好抽取方法出来
//构建查询对象
NativeSearchQuery query = nativeSearchQueryBuilder.build();
//执行查询
//带上索引映射类
AggregatedPage<索引映射类> page = esTemplate.queryForPage(query, 索引映射类.class);
//获取分组结果,对应分组名
StringTerms stringTermsCategory = (StringTerms) page.getAggregation(组名字符串);
List<String> list = getStringsList(stringTermsCategory);
//返回结果
Map<String, Object> resultMap = new HashMap<>();
resultMap.put("List", list); // 分组结果,几种类型
resultMap.put("rows", page.getContent()); // 数据
resultMap.put("total", page.getTotalElements()); // 总记录数
resultMap.put("totalPages", page.getTotalPages()); // 总页数
return resultMap;
}
/**
* 获取分组后的列表数据
* 一般String类型
* @param stringTerms
* @return
*/
private List<String> getStringsList(StringTerms stringTerms) {
List<String> list = new ArrayList<>();
if (stringTerms != null) {
for (StringTerms.Bucket bucket : stringTerms.getBuckets()) {
String keyAsString = bucket.getKeyAsString();//分组的值
list.add(keyAsString);
}
}
return list;
}
}
对应dsl语句,field指域名
标准match搜索
可分词搜索,前提是索引映射类上对应的变量域名有searchAnalyzer = “ik_smart”,支持搜索使用分词器,否则只是普通的模糊搜索like。
nativeSearchQueryBuilder.withQuery(QueryBuilders.matchQuery(域名字符串, 关键词));
过滤term搜索
不分词精确匹配
nativeSearchQueryBuilder.withQuery(QueryBuilders.termQuery(域名字符串, 关键词));
组合bool搜索
bool组合过滤可以用来合并多个过滤条件查询结果的布尔逻辑,它包含一下操作符:
- must : 多个查询条件的完全匹配,相当于 and。
- must_not : 多个查询条件的相反匹配,相当于 not。
- should : 至少有一个查询条件匹配, 相当于 or。
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
// 中文分词,如果多个中文分词关键词得用must
if (!StringUtils.isEmpty(主关键词)) {
boolQueryBuilder.must(QueryBuilders.matchQuery("域名", 主关键词));
}
// 多关键词可用map,或者dto
if (!StringUtils.isEmpty(关键词1)) {
boolQueryBuilder.must(QueryBuilders.termQuery("域名1", 关键词1));
}
if (!StringUtils.isEmpty(关键词2)) {
boolQueryBuilder.must(QueryBuilders.termQuery("域名2", 关键词2));
}
// ...rangeQuery
// 构建过滤查询
// withQuery只能一个
nativeSearchQueryBuilder.withQuery(boolQueryBuilder);
另一个方式:
PS说明: 以上,我们建议使用filter ,它的搜索效率要优于must.可以参考官方文档说明:
https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-bool-query.html
如果多个中文分词关键词的话得用must(matchQuery),一个一下就用filter。
//设置主关键字查询,中文分词
nativeSearchQueryBuilder.withQuery(QueryBuilders.matchQuery("域名", 主关键词));
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
if (!StringUtils.isEmpty(关键词1)) {
boolQueryBuilder.filter(QueryBuilders.termQuery("域名1", 关键词1));
}
if (!StringUtils.isEmpty(关键词2)) {
boolQueryBuilder.filter(QueryBuilders.termQuery("域名2", 关键词2));
}
//...rangeQuery
//构建过滤查询
nativeSearchQueryBuilder.withFilter(boolQueryBuilder);
区间range搜索
最好结合bool组合搜索,一般是数字大小,范围
- gt:>
- gte:>=
- lt:<
- lte:<=
// 区间1
boolQueryBuilder.filter(QueryBuilders.rangeQuery("数字域名").gte(前端int类型参数小);
boolQueryBuilder.filter(QueryBuilders.rangeQuery("数字域名").lte(前端int类型参数大);
// 区间2
boolQueryBuilder.filter(QueryBuilders.rangeQuery("数字域名").from(前端int类型参数小, true).to(前端int类型参数大, true));
// 单个以上,大于或小于选一个
boolQueryBuilder.filter(QueryBuilders.rangeQuery("数字域名").lte(前端int类型参数);
分组搜索
group by,可额外添加到搜索,拿到查询数据后该域名的种类。
nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms(组名字符串1).field("域名1").size(100));
nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms(组名字符串2).field("域名2").size(100));
//...
List<String> list1 = getStringsList(page, 组名字符串1);
List<String> list2 = getStringsList(page, 组名字符串2);
/**
* 获取分组后的列表数据
* 一般是String类型
* page是查询后的返回数据
*/
private List<String> getStringsList(AggregatedPage<索引映射类> page, String 组名字符串) {
StringTerms stringTermsCategory = (StringTerms) page.getAggregation(组名字符串);
List<String> list = new ArrayList<>();
if (stringTerms != null) {
for (StringTerms.Bucket bucket : stringTerms.getBuckets()) {
String keyAsString = bucket.getKeyAsString(); //分组的值
list.add(keyAsString);
}
}
return list;
}
这样会多次请求es,效率不高,改进如下,分组的合并一起请求
//多个域一起分组,以后多域用这个方法
//修改了getAggregations().get(组名字符串)
/**
* 获取分组后的列表数据
* 一般是String类型
* page是查询后的返回数据
*/
private List<String> getStringsList(AggregatedPage<索引映射类> page, String 组名字符串) {
StringTerms stringTermsCategory = (StringTerms) page.getAggregations().get(组名字符串);
List<String> list = new ArrayList<>();
if (stringTerms != null) {
for (StringTerms.Bucket bucket : stringTerms.getBuckets()) {
String keyAsString = bucket.getKeyAsString(); //分组的值
list.add(keyAsString);
}
}
return list;
}
分页查询
//写在所有条件最后
//构建分页查询
int pageNum = 1; // 默认第一页
if (!StringUtils.isEmpty(searchMap.get("pageNum"))) {
try {
// 前端传过来的当前页
pageNum = Integer.parseInt(searchMap.get("pageNum"));
} catch (NumberFormatException e) {
e.printStackTrace();
pageNum=1;
}
}
int pageSize = 8; // 一页多少条数据,一般后台固定
nativeSearchQueryBuilder.withPageable(PageRequest.of(pageNum - 1, pageSize));
//构建查询对象
NativeSearchQuery query = nativeSearchQueryBuilder.build();
排序查询
//构建排序查询
String sortRule = searchMap.get("sortRule"); //排序的规则,升序还是降序DESC、ASC
String sortField = searchMap.get("sortField"); //前端传过来的指定排序域名
if (!StringUtils.isEmpty(sortRule) && !StringUtils.isEmpty(sortField)) {
nativeSearchQueryBuilder.withSort(SortBuilders.fieldSort(sortField).order(sortRule.equals("DESC") ? SortOrder.DESC : SortOrder.ASC));
}
//也可以这样
nativeSearchQueryBuilder.withSort(new FieldSortBuilder(sortField).order(SortOrder.valueOf(sortRule)));
//构建查询对象
NativeSearchQuery query = nativeSearchQueryBuilder.build();
高亮设置
某些数据需要高亮显示,比如含有关键词的语句中关键词变色
SkuInfo是对应的索引映射类
数据操作实现类
import com.alibaba.fastjson.JSON;
import com.changgou.model.es.SkuInfo;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.common.text.Text;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.core.SearchResultMapper;
import org.springframework.data.elasticsearch.core.aggregation.AggregatedPage;
import org.springframework.data.elasticsearch.core.aggregation.impl.AggregatedPageImpl;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* Title:获取es搜索后的数据
* Description:数据进行操作返回给前端
* SkuInfo是对应的索引映射类
* @author WZQ
* @version 1.0.0
* @date 2020/3/6
*/
public class SearchResultMapperImpl implements SearchResultMapper {
@Override
public <T> AggregatedPage<T> mapResults(SearchResponse response, Class<T> clazz, Pageable pageable) {
//存放修改后的数据
List<T> content = new ArrayList<>();
//如果没有结果返回为空
if (response.getHits() == null || response.getHits().getTotalHits() <= 0) {
return new AggregatedPageImpl<T>(content);
}
// 遍历数据
for (SearchHit searchHit : response.getHits()) {
String sourceAsString = searchHit.getSourceAsString();
// 默认拿到的是非高亮数据,指定索引映射类
SkuInfo skuInfo = JSON.parseObject(sourceAsString, SkuInfo.class);
Map<String, HighlightField> highlightFields = searchHit.getHighlightFields();
//跟条件设置的域名一致
HighlightField highlightField = highlightFields.get("域名");
//有高亮则读取高亮的值
if (highlightField != null && highlightField.getFragments() != null) {
StringBuilder stringBuilder = new StringBuilder();
for (Text text : highlightField.getFragments()) {
stringBuilder.append(text.toString());
}
// 数据中指定域替换成高亮数据,默认非高亮的
skuInfo.setName(stringBuilder.toString());
}
content.add((T) skuInfo);
}
/**
* 1.携带高亮数据的内容
* 2.分页对象信息
* 3.搜索数据的总数
*/
return new AggregatedPageImpl<T>(content, pageable, response.getHits().getTotalHits(), response.getAggregations(), response.getScrollId());
}
}
serive中修改:
//高亮配置
HighlightBuilder.Field field = new HighlightBuilder.Field("域名");
//样式前缀
field.preTags("<em style=\"color:red;\">");
//后缀
field.postTags("</em>");
//碎片长度 关键词数据的长度,不超过设置的长度给到前端
field.fragmentSize(100);
//添加到条件
nativeSearchQueryBuilder.withHighlightFields(field); // 参数是可变数组,可多个传进去
//构查询对象
NativeSearchQuery query = nativeSearchQueryBuilder.build();
//记得修改成这个方法,带上实现类
AggregatedPage<SkuInfo> skuPage = esTemplate.queryForPage(query, SkuInfo.class, new SearchResultMapperImpl());