SpringBoot2.2.0.RELEASE整合Elasticsearch6.8.3
SpringBoot是2.2.0.RELEASE
,elasticsearch是6.8.3
使用依赖spring-boot-starter-data-elasticsearch
使用ElasticSearchRepository
操作
1、导入依赖
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.0.RELEASE</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>spring-boot-elasticsearch5</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-boot-elasticsearch5</name>
<description>spring-boot-elasticsearch5</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.58</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2、配置文件
spring:
elasticsearch:
rest:
uris: http://192.168.94.186:9200
3、创建索引的实体类
package com.example.search.entity;
import lombok.*;
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.Map;
/**
* 1.创建索引
* 2.创建类型
* 3.创建文档
* 4.字段的映射(是否分词,是否索引,是否存储,数据类型是什么,分词器是什么)
* indexName 指定创建的索引的名称
* type :指定索引中的类型
*/
@Getter
@Setter
@Data
@AllArgsConstructor
@NoArgsConstructor
@Document(indexName = "shop_info", type = "docs")
public class ShopInfo implements Serializable {
@Id
@Field(type = FieldType.Text)
private Long id;
@Field(type = FieldType.Text, analyzer = "ik_smart")
private String name;
@Field(type = FieldType.Double)
private Double price;
@Field(type = FieldType.Keyword)
private String categoryName;
@Field(type = FieldType.Keyword)
private String brandName;
@Field(type = FieldType.Keyword)
private String spec;
// -ES能够自动存储未提交创建字段信息的数据
// 目的:未指定时ES为了可以更好的支持聚合和查询功能,所以默认创建了两种
// 对于未提前指定类型的字段,使用以下默认规则
// [字段](text) #分词不聚合
// [字段].keyword(keyword) #聚合不分词
private Map<String, Object> specMap;
}
package com.example.search.entity;
import lombok.*;
import java.util.Map;
/**
* 查询数据的封装
*/
@Getter
@Setter
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ShopVO {
private Long id;
private String name;
private Double price;
private String categoryName;
private String brandName;
private String spec;
private Map<String, Object> specMap;
}
4、ShopEsMapper
package com.example.search.dao;
import com.example.search.entity.ShopInfo;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import org.springframework.stereotype.Component;
@Component
public interface ShopEsMapper extends ElasticsearchRepository<ShopInfo, Long> {
}
5、Service
package com.example.search.service;
import java.util.Map;
public interface ShopSearchService {
/**
* 1.查询符合条件的shop的数据
* 2.调用spring data elasticsearch的API导入到ES中
*/
void importEs();
/**
* 进行查询
*
* @param searchMap
* @return
*/
Map search(Map<String, String> searchMap);
/**
* 创建索引
*/
boolean createIndex();
/**
* 删除索引
*/
boolean deleteIndex();
}
package com.example.search.service.impl;
import com.alibaba.fastjson.JSON;
import com.example.search.dao.ShopEsMapper;
import com.example.search.entity.ShopInfo;
import com.example.search.entity.ShopVO;
import com.example.search.service.ShopSearchService;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.sort.SortBuilders;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;
import org.springframework.data.elasticsearch.core.aggregation.AggregatedPage;
import org.springframework.data.elasticsearch.core.query.NativeSearchQuery;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.*;
@Service
public class ShopSearchServiceImpl implements ShopSearchService {
@Autowired
private ShopEsMapper shopEsMapper;
@Autowired
private ElasticsearchRestTemplate elasticsearchRestTemplate;
@Override
public void importEs() {
// 1.查询符合条件的shop的数据
// 这里正常是从数据库中查询数据
// 现在只是测试,只添加两条数据
List<ShopVO> shopVOList = new ArrayList<>();
Map<String, Object> specMap = new HashMap<>();
specMap.put("颜色", "白色");
specMap.put("内存", "64G");
specMap.put("硬盘", "1T");
specMap.put("待机", "8h");
shopVOList.add(new ShopVO(1L, "华为手机", 2000.0, "手机", "华为", "{\"内存\":\"64G\",\"颜色\":\"白色\"}", specMap));
shopVOList.add(new ShopVO(2L, "小米电脑", 3000.0, "电脑", "小米", "{\"硬盘\":\"1T\",\"颜色\":\"金色\"}", specMap));
// 将shopVO的列表转换成es中的ShopInfo的列表
List<ShopInfo> shopInfoList = JSON.parseArray(JSON.toJSONString(shopVOList), ShopInfo.class);
// 2.调用spring data elasticsearch的API导入到ES中
shopEsMapper.saveAll(shopInfoList);
}
/**
* @param searchMap
*/
@Override
public Map search(Map<String, String> searchMap) {
// 1.获取到关键字
String keywords = searchMap.get("keywords");
// 2.判断是否为空,如果为空给一个默认值:华为
// 查询所有
// SELECT * FROM shop WHERE name LIKE '%手机%';
if (StringUtils.isEmpty(keywords)) {
keywords = "华为";
}
// 3.创建查询构建对象
NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
// 4.设置查询的条件
// SELECT categoryName FROM shop WHERE name LIKE '%手机%' GROUP BY categoryName;
// 4.1商品分类的列表展示: 按照商品分类的名称来分组
// terms:指定分组的一个别名
// field:指定要分组的字段名
// size:指定查询结果的数量,默认是10个
nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms("shopCategoryGroup").field("categoryName.keyword").size(50));
// 4.2商品的品牌的列表展示,按照商品品牌来进行分组
// SELECT brandName FROM shop WHERE name LIKE '%手机%' GROUP BY brandName;
nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms("shopBrandGroup").field("brandName.keyword").size(100));
// 4.3商品的规格的列表展示,按照商品的规格的字段spec进行分组
// SELECT spec FROM shop WHERE name LIKE '%手机%' GROUP BY spec;
// 规则要求字段是一个keyword类型的,spec.keyword
nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms("shopSpecGroup").field("spec.keyword").size(500));
// 4.4设置高亮的字段,设置前缀和后缀
// 设置高亮的字段,针对商品的名称进行高亮
nativeSearchQueryBuilder.withHighlightFields(new HighlightBuilder.Field("name"));
// 设置前缀和后缀
nativeSearchQueryBuilder.withHighlightBuilder(new HighlightBuilder().preTags("<em style=\"color:red\">").postTags("</em>"));
// 匹配查询:先分词,再查询,主条件查询
// 参数1:指定要搜索的字段
// 参数2:要搜索的值(先分词,再搜索)
// 从单个字段搜索数据
// nativeSearchQueryBuilder.withQuery(QueryBuilders.matchQuery("name", keywords));
// 从多个字段中搜索数据,参数1为关键字,后面的参数为所有的字段
nativeSearchQueryBuilder.withQuery(QueryBuilders.multiMatchQuery(keywords, "name", "categoryName", "brandName"));
//========================过滤查询开始=====================================
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
// 4.4 过滤查询的条件设置
// 商品分类的条件
String category = searchMap.get("category");
if (!StringUtils.isEmpty(category)) {
boolQueryBuilder.filter(QueryBuilders.termQuery("categoryName", category));
}
// 4.5 过滤查询的条件设置
// 商品品牌的条件
String brand = searchMap.get("brand");
if (!StringUtils.isEmpty(brand)) {
boolQueryBuilder.filter(QueryBuilders.termQuery("brandName", brand));
}
//4.6 过滤查询的条件设置
// 规格条件
if (searchMap != null) {
//{ spec_网络:"电信4G",spec_顔色:"黑色"}
for (String key : searchMap.keySet()) {
if (key.startsWith("spec_")) {
//截取规格的名称
boolQueryBuilder.filter(QueryBuilders.termQuery("specMap." + key.substring(5) + ".keyword", searchMap.get(key)));
}
}
}
// 4.7 过滤查询的条件设置
// 价格区间的过滤查询
// 0-500 3000-*
String price = searchMap.get("price");
if (!StringUtils.isEmpty(price)) {
//获取值 按照- 切割
String[] split = price.split("-");
//过滤范围查询
//0<=price<=500
if (!split[1].equals("*")) {
boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").from(split[0], true).to(split[1], true));
} else {
boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").gte(split[0]));
}
}
//过滤查询
nativeSearchQueryBuilder.withFilter(boolQueryBuilder);
//========================过滤查询结束=====================================
// 分页查询
// 第一个参数:指定当前的页码 注意: 如果是第一页 数值为0
// 第二个参数:指定当前的页的显示的行
String pageNum1 = searchMap.get("pageNum");
Integer pageNum = Integer.valueOf(pageNum1);
String pageSize1 = searchMap.get("pageSize");
Integer pageSize = Integer.valueOf(pageSize1);
nativeSearchQueryBuilder.withPageable(PageRequest.of(pageNum - 1, pageSize));
// 排序操作
// 获取排序的字段 和要排序的规则
// price
String sortField = searchMap.get("sortField");
// DESC ASC
String sortRule = searchMap.get("sortRule");
if (!StringUtils.isEmpty(sortField) && !StringUtils.isEmpty(sortRule)) {
// 执行排序
nativeSearchQueryBuilder.withSort(SortBuilders.fieldSort(sortField).order(sortRule.equalsIgnoreCase("ASC") ? SortOrder.ASC : SortOrder.DESC));
// nativeSearchQueryBuilder.withSort(SortBuilders.fieldSort(sortField).order(SortOrder.valueOf(sortRule)));
}
// 5.构建查询对象(封装了查询的语法)
NativeSearchQuery nativeSearchQuery = nativeSearchQueryBuilder.build();
//6.执行查询
AggregatedPage<ShopInfo> shopInfos = elasticsearchRestTemplate.queryForPage(nativeSearchQuery, ShopInfo.class, new SearchResultMapperImpl());
// 6.2 获取聚合分组结果 获取商品分类的列表数据
Terms stringTermsCategory = (Terms) shopInfos.getAggregation("shopCategoryGroup");
List<String> categoryList = getStringsCategoryList(stringTermsCategory);
//6.3 获取 品牌分组结果 列表数据
Terms stringTermsBrand = (Terms) shopInfos.getAggregation("shopBrandGroup");
List<String> brandList = getStringsBrandList(stringTermsBrand);
//6.4 获取 规格的分组结果 列表数据map
Terms stringTermsSpec = (Terms) shopInfos.getAggregation("shopSpecGroup");
Map<String, Set<String>> specMap = getStringSetMap(stringTermsSpec);
//7.获取结果
// 返回map
//当前的页的集合
List<ShopInfo> content = shopInfos.getContent();
//总页数
int totalPages = shopInfos.getTotalPages();
//总记录数
long totalElements = shopInfos.getTotalElements();
Map<String, Object> resultMap = new HashMap<>();
//商品分类的列表数据
resultMap.put("categoryList", categoryList);
//商品品牌的列表数据
resultMap.put("brandList", brandList);
//商品规格的列表数据展示
resultMap.put("specMap", specMap);
resultMap.put("rows", content);
resultMap.put("total", totalElements);
resultMap.put("totalPages", totalPages);
resultMap.put("pageNum", pageNum);
resultMap.put("pageSize", pageSize);
return resultMap;
}
@Override
public boolean createIndex() {
// 创建索引,会根据ShopInfo类的@Document注解信息来创建
Boolean aBoolean = elasticsearchRestTemplate.createIndex(ShopInfo.class);
// 配置映射,会根据ShopInfo类中的id、Field等字段来自动完成映射
Boolean aBoolean1 = elasticsearchRestTemplate.putMapping(ShopInfo.class);
System.out.println("创建索引是否成功:" + (aBoolean && aBoolean1));
return aBoolean && aBoolean1;
}
@Override
public boolean deleteIndex() {
Boolean aBoolean = elasticsearchRestTemplate.deleteIndex(ShopInfo.class);
System.out.println("删除索引是否成功:" + aBoolean);
return aBoolean;
}
private Map<String, Set<String>> getStringSetMap(Terms stringTermsSpec) {
// key :规格的名称
// value :规格名称对应的选项的多个值集合set
Map<String, Set<String>> specMap = new HashMap<String, Set<String>>();
Set<String> specValues = new HashSet<String>();
if (stringTermsSpec != null) {
// 1. 获取分组的结果集
for (Terms.Bucket bucket : stringTermsSpec.getBuckets()) {
//2.去除结果集的每一行数据()
// {"手机屏幕尺寸":"5.5寸","网络":"电信4G","颜色":"白","测试":"s11","机身内存":"128G","存储":"16G","像素":"300万像素"}
String keyAsString = bucket.getKeyAsString();
System.out.println("keyAsString:" + keyAsString);
//3.转成JSON 对象 map key :规格的名称 value:规格名对应的选项的单个值
Map<String, String> map = JSON.parseObject(keyAsString, Map.class);
for (Map.Entry<String, String> stringStringEntry : map.entrySet()) {
//规格名称:手机屏幕尺寸
String key = stringStringEntry.getKey();
//规格的名称对应的单个选项值 5.5寸
String value = stringStringEntry.getValue();
//先从原来的specMap中 获取 某一个规格名称 对应的规格的选项值集合
specValues = specMap.get(key);
if (specValues == null) {
specValues = new HashSet<>();
}
specValues.add(value);
//4.提取map中的值放入到返回的map中
specMap.put(key, specValues);
}
}
}
return specMap;
}
private List<String> getStringsBrandList(Terms stringTermsBrand) {
List<String> brandList = new ArrayList<>();
if (stringTermsBrand != null) {
for (Terms.Bucket bucket : stringTermsBrand.getBuckets()) {
//品牌的名称 huawei
String keyAsString = bucket.getKeyAsString();
brandList.add(keyAsString);
}
}
return brandList;
}
/**
* 获取分组结果 商品分类的分组结果
*
* @param stringTermsCategory
* @return
*/
private List<String> getStringsCategoryList(Terms stringTermsCategory) {
List<String> categoryList = new ArrayList<>();
if (stringTermsCategory != null) {
for (Terms.Bucket bucket : stringTermsCategory.getBuckets()) {
String keyAsString = bucket.getKeyAsString();
//就是商品分类的数据
categoryList.add(keyAsString);
}
}
return categoryList;
}
}
package com.example.search.service.impl;
import com.alibaba.fastjson.JSON;
import com.example.search.entity.ShopInfo;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.common.text.Text;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.aggregations.Aggregations;
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;
/**
* 自定义结果集映射 ()
* 目的: 获取高亮的数据
*/
public class SearchResultMapperImpl implements SearchResultMapper {
@Override
public <T> AggregatedPage<T> mapResults(SearchResponse response, Class<T> clazz, Pageable pageable) {
//1.创建一个当前页的记录集合对象
List<T> content = new ArrayList<>();
if (response.getHits() == null || response.getHits().getTotalHits() <= 0) {
return new AggregatedPageImpl<T>(content);
}
//搜索到的结果集
for (SearchHit searchHit : response.getHits()) {
//每一个行的数据 json的 数据
String sourceAsString = searchHit.getSourceAsString();
ShopInfo skuInfo = JSON.parseObject(sourceAsString, ShopInfo.class);
//key :高亮的字段名 value 就是该字段的高亮的数据集合
Map<String, HighlightField> highlightFields = searchHit.getHighlightFields();
HighlightField highlightField = highlightFields.get("name");
//有高亮的数据
if (highlightField != null) {
//有高亮的数据
StringBuffer buffer = new StringBuffer();
//取高亮的数据
for (Text text : highlightField.getFragments()) {
//高亮的数据 华为 胀奸 5寸 联通2G 白 <em style='color=red>'显示</em> 32G 16G 300万像素
String string = text.string();
buffer.append(string);
}
//有高亮的数据
skuInfo.setName(buffer.toString());
}
content.add((T) skuInfo);
}
//2.创建分页的对象 已有
//3.获取总个记录数
long totalHits = response.getHits().getTotalHits();
//4.获取所有聚合函数的结果
Aggregations aggregations = response.getAggregations();
//5.深度分页的ID
String scrollId = response.getScrollId();
return new AggregatedPageImpl<T>(content, pageable, totalHits, aggregations, scrollId);
}
@Override
public <T> T mapSearchHit(SearchHit searchHit, Class<T> aClass) {
return null;
}
}
6、启动类
package com.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories;
@SpringBootApplication
@EnableElasticsearchRepositories(basePackages = "com.example.search.dao")
public class SpringBootElasticsearch5Application {
public static void main(String[] args) {
SpringApplication.run(SpringBootElasticsearch5Application.class, args);
}
}
7、测试
package com.example;
import com.example.search.entity.ShopInfo;
import com.example.search.service.ShopSearchService;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@SpringBootTest
class SpringBootElasticsearch5ApplicationTests {
@Autowired
private ShopSearchService shopSearchService;
@Autowired
private ElasticsearchRestTemplate elasticsearchRestTemplate;
@Test
void createIndex() {
shopSearchService.createIndex();
}
@Test
void deleteIndex() {
shopSearchService.deleteIndex();
}
@Test
void saveData() {
// 如果没有索引在执行的时候自动会创建索引
shopSearchService.importEs();
}
/**
* 参数有:
* keywords
* category
* brand
* spec_
* price
* pageNum
* pageSize
* sortField
* sortRule
*/
@Test
void search() {
Map<String, String> map = new HashMap<>();
map.put("pageNum", "1");
map.put("pageSize", "1");
Map resultMap = shopSearchService.search(map);
log.info(resultMap.toString());
}
}
7.1 插入数据测试
插入数据前不需要先建立索引,在执行插入的时候会自动建立。
7.2 搜索测试
2022-06-24 15:06:07.992 INFO 15828 --- [ main] SpringBootElasticsearch5ApplicationTests : {total=1, categoryList=[手机], totalPages=1, specMap={颜色=[白色], 内存=[64G]}, pageSize=1, brandList=[华为], rows=[ShopInfo(id=1, name=<em style="color:red">华</em><em style="color:red">为</em>手机, price=2000.0, categoryName=手机, brandName=华为, spec={"内存":"64G","颜色":"白色"}, specMap={硬盘=1T, 颜色=白色, 内存=64G, 待机=8h})], pageNum=1}