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 {@Autowiredprivate ShopEsMapper shopEsMapper;@Autowiredprivate ElasticsearchRestTemplate elasticsearchRestTemplate;@Overridepublic 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*/@Overridepublic 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.keywordnativeSearchQueryBuilder.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<=500if (!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));// 排序操作// 获取排序的字段 和要排序的规则// priceString sortField = searchMap.get("sortField");// DESC ASCString 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 获取 规格的分组结果 列表数据mapTerms 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;}@Overridepublic 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;}@Overridepublic boolean deleteIndex() {Boolean aBoolean = elasticsearchRestTemplate.deleteIndex(ShopInfo.class);System.out.println("删除索引是否成功:" + aBoolean);return aBoolean;}private Map<String, Set<String>> getStringSetMap(Terms stringTermsSpec) {// key :规格的名称// value :规格名称对应的选项的多个值集合setMap<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()) {//品牌的名称 huaweiString 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 {@Overridepublic <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.深度分页的IDString scrollId = response.getScrollId();return new AggregatedPageImpl<T>(content, pageable, totalHits, aggregations, scrollId);}@Overridepublic <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 {@Autowiredprivate ShopSearchService shopSearchService;@Autowiredprivate ElasticsearchRestTemplate elasticsearchRestTemplate;@Testvoid createIndex() {shopSearchService.createIndex();}@Testvoid deleteIndex() {shopSearchService.deleteIndex();}@Testvoid saveData() {// 如果没有索引在执行的时候自动会创建索引shopSearchService.importEs();}/*** 参数有:* keywords* category* brand* spec_* price* pageNum* pageSize* sortField* sortRule*/@Testvoid 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}