1. Spring Data概述
Spring Data是spring提供的一套连接各种第三方数据源的框架集,它支持连接很多第三方数据源,例如:
数据库
redis
ElasticSearch
MongoDB等
包括数据库在内,很多第三方数据都可以使用SpringData操作,非常方便。
2. Spring Data Elasticsearch
上面章节介绍了Spring Data可以连接很多第三方数据源,其中ES就是Spring Data可以连接的对象。原生情况下,我们需要使用socket来连接ES获得响应,再解析响应,代码量非常大,我们现在可以使用Spring Data提供的封装,连接ES,方便快捷。
转到knows-search模块:
下面我们添加Spring Data ES的依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-elasticsearch</artifactId> </dependency>
application.properties:
# 搜索微服务端口 server.port=8066 # 搜索微服务名称 spring.application.name=search-service # 定位ES的位置 spring.elasticsearch.rest.uris=http://localhost:9200 # 设置日志门槛,显示ES的操作信息 logging.level.cn.tedu.knows.search=debug # 还需要进一步设置才能使输出日志更清晰 logging.level.org.elasticsearch.client.RestClient=debug
SpringBoot启动类无需配置!
3. 实现基本操作
操作ES需要类:首先定义一个对应ES数据的类型,创建一个vo包,包中定义Item(商品)类代码如下:
package cn.tedu.knows.search.vo; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import lombok.experimental.Accessors; 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; @Data // lombok @Accessors(chain = true) // 链式赋值(连续set方法) @AllArgsConstructor // 全参构造 @NoArgsConstructor // 无参构造 //指定当前类对象对应哪个ES中的索引 //如果索引不存在,会自动创建 @Document(indexName = "items") public class Item { // 表示当前ES索引的id列 @Id private Long id; // 需要分词的属性使用Text类型,并指定分词器 @Field(type = FieldType.Text,analyzer = "ik_smart", searchAnalyzer = "ik_smart") private String title; //商品名称 // 不需要分词的属性使用Keyword类型,不用写分词器 @Field(type = FieldType.Keyword) private String category;//分类 @Field(type = FieldType.Keyword) private String brand; //品牌 @Field(type = FieldType.Double) private Double price; //价格 //不会使用图片地址查询,设置index = false表示当前属性不会创建索引,节省空间,因为这个属性不会被查询 @Field(type = FieldType.Keyword,index = false) private String images; //图片地址 // /upload/2021/08/19/abc.jpg }
这个类中所有属性均配置了对应ES的属性和类型,下面我们就可以使用这个类操作ES了。
创建一个包repository,创建一个接口ItemRepository:
@Repository //将实现类的对象存到Spring容器中 //ElasticsearchRepository实现基本的增删改查 public interface ItemRepository extends ElasticsearchRepository<Item,Long> { }
这个接口和Mybatis Plus中Mapper接口继承的BaseMapper类似,会自动提供基本的增删改查方法。下面进行测试,测试类代码如下:
package cn.tedu.knows.search; import cn.tedu.knows.search.repository.ItemRepository; import cn.tedu.knows.search.vo.Item; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import javax.annotation.Resource; import java.util.ArrayList; import java.util.List; import java.util.Optional; @SpringBootTest class KnowsSearchApplicationTests { @Resource ItemRepository itemRepository; @Test void contextLoads() { //实例化对象,新增一个商品 Item item = new Item() .setId(1L) .setTitle("罗技激光无线游戏鼠标") .setCategory("鼠标") .setBrand("罗技") .setImages("/1.jpg") .setPrice(285.0); //调用Spring Data提供的方法进行新增,save表示新增 itemRepository.save(item); System.out.println("ok"); } //按id查询 @Test public void getId(){ //Optional表示装着Item的盒子 Optional<Item> optional = itemRepository.findById(1L); //通过get进行查询 System.out.println(optional.get()); } //批量新增 @Test public void addAll(){ List<Item> list = new ArrayList<>(); list.add(new Item(2L,"罗技机械无线游戏键盘","键盘","罗技",360.0,"/2.jpg")); list.add(new Item(3L,"雷蛇激光有线游戏鼠标","鼠标","雷蛇",488.0,"/3.jpg")); list.add(new Item(4L,"罗技降噪蓝牙竞技耳机","耳机","罗技",378.0,"/4.jpg")); list.add(new Item(5L,"华为静音办公有线鼠标","鼠标","华为",220.0,"/5.jpg")); list.add(new Item(6L,"雷蛇竞技机械无线键盘","键盘","雷蛇",425.0,"/6.jpg")); itemRepository.saveAll(list); System.out.println("ok"); } //全查 @Test public void getAll(){ //Iterable是List的父接口 Iterable<Item> list = itemRepository.findAll(); for(Item item : list){ System.out.println(item); } } } //上面都是基本操作,不需要我们自己写接口中的方法
上面进行了单增、单查、批量增和全查的操作,下面进行自定义的查询。
Spring Data支持编写方法名表达操作,会自动按方法名的表达生成实现代码,这是它的一大优势!
在ItemRepository接口编写方法:
// Spring Data框架连接数据源,可以通过方法名来表达操作含义 // 根据商品的title属性执行模糊查询 Iterable<Item> queryItemsByTitleMatches(String title);
测试代码:
// 下面要完成一些条件查询,需要调用ItemRepository接口中编写的方法 // 商品标题模糊匹配 @Test public void queryByTitle(){ Iterable<Item> items=itemRepository.queryItemsByTitleMatches("无线"); for(Item item: items){ System.out.println(item); } }
相当于运行了下面的指令:
### 单条件搜索 POST http://localhost:9200/items/_search Content-Type: application/json { "query": {"match": { "title": "无线" }} }
多属性条件查询:在ItemRepository接口编写方法:
// 根据商品的title和brand执行模糊查询 Iterable<Item> queryItemsByTitleMatchesAndBrandMatches(String title,String brand);
测试类:
// 测试多条件查询 @Test public void queryByTitleBrand(){ Iterable<Item> items=itemRepository .queryItemsByTitleMatchesAndBrandMatches( "游戏","罗技" ); for(Item item: items){ System.out.println(item); } }
实际执行的请求:
### 多字段搜索 POST http://localhost:9200/items/_search Content-Type: application/json { "query": { "bool": { "must": [ { "match": { "title": "游戏" }}, { "match": { "brand": "罗技"}} ] } } }
排序查询:在ItemRepository接口编写方法:
// 排序查询:按照价格降序查询标题或者品牌匹配的商品 Iterable<Item> queryItemsByTitleMatchesOrBrandMatchesOrderByPriceDesc(String title,String brand);
测试代码:
// 测试排序 @Test public void order(){ Iterable<Item> items = itemRepository.queryItemsByTitleMatchesOrBrandMatchesOrderByPriceDesc("游戏","罗技"); for(Item item:items){ System.out.println(item); } }
实际运行的请求:
### 多字段搜索 POST http://localhost:9200/items/_search Content-Type: application/json { "query": { "bool": { "should": [ { "match": { "title": "游戏" }}, { "match": { "brand": "罗技"}} ] } },"sort":[{"price":"desc"}] }
添加分页查询功能::在ItemRepository接口编写方法
// 分页查询 Page相当于PageHelper,Pageable规定第几页包含多少行,相当于PageInfo Page<Item> queryItemsByTitleMatchesOrBrandMatchesOrderByPriceDesc(String title, String brand, Pageable pageable);
测试:
//分页查询 @Test public void page(){ int pageNum=1; int pageSize=2; //PageRequest.of返回Pageable,Pageable页数从第0页开始 Page<Item> page=itemRepository.queryItemsByTitleMatchesOrBrandMatchesOrderByPriceDesc( "游戏","罗技",PageRequest.of(pageNum-1,pageSize)); //page实现了Iterable接口 for(Item item:page){ System.out.println(item); } }
根据条件分页查询
//Repository接口定义 public interface BookInfoEsMapper extends ElasticsearchRepository<BookInfoEs, String> { Page<BookInfoEs> findBookInfoEsByTitleOrIsbnOrAuthor(String title, String isbn, String author, Pageable pageable); }
@Autowired private BookInfoEsMapper bookInfoEsMapper; @Test public void test() { //调用 // Repository层根据findBookInfoEsByTitleOrIsbnOrAuthor 方法名自动识别条件 Pageable pageable = PageRequest.of(0,10); Page<BookInfoEs> list = bookInfoEsMapper.findBookInfoEsByTitleOrIsbnOrAuthor("现代", "", "", pageable); System.out.println(list); System.out.println("总条数:" +list.getTotalElements()); System.out.println("总页数:" +list.getTotalPages()); }
再来一种 根据条件分页查询
@Autowired private ElasticsearchOperations elasticsearchOperations; @Test public void test() { //构造条件 BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery(); //must:必须满足的条件 //should:非必须满足的条件 boolQueryBuilder.should(QueryBuilders.matchQuery("title", "现代")); //wildcardQuery: 通配符查询 boolQueryBuilder.should(QueryBuilders.wildcardQuery("id", "*122*")); boolQueryBuilder.should(QueryBuilders.matchQuery("author", "")); Pageable pageable = PageRequest.of(0, 10); NativeSearchQuery build = new NativeSearchQueryBuilder() .withQuery(boolQueryBuilder) .withPageable(pageable) .build(); SearchHits<BookInfoEs> search = elasticsearchOperations.search(build, BookInfoEs.class); System.out.println("检索后的总分页数目为:" + search.getTotalHits()); List<SearchHit<BookInfoEs>> searchHits = search.getSearchHits(); System.out.println(searchHits); }
基于RestHighLevelClient
批量操作
import org.elasticsearch.client.RestHighLevelClient; import org.elasticsearch.action.update.UpdateRequest; import org.elasticsearch.action.bulk.BulkItemResponse; import org.elasticsearch.action.bulk.BulkRequest; import org.elasticsearch.action.bulk.BulkResponse; import org.elasticsearch.client.RequestOptions; import org.elasticsearch.rest.RestStatus; @Autowired private RestHighLevelClient client; BulkRequest bulkRequest = new BulkRequest(); for(int i = 0; i < 100; i++){ //添加、删除、更新文档,操作索引都有相关类就不一一举例了 UpdateRequest updateRequest = new UpdateRequest(indexName, IndexConst.DEFAULT_TYPE_NAME, docId); updateRequest.doc(docObject.toJSONString(), XContentType.JSON); bulkRequest.add(updateRequest); } if (!bulkRequest.requests().isEmpty()) { BulkResponse bulkResponse = client.bulk(bulkRequest, RequestOptions.DEFAULT); BulkItemResponse[] items = bulkResponse.getItems(); for (BulkItemResponse item : items) { if (item.status() != RestStatus.OK) { log.error(JSONObject.toJSONString(item)); } } }
4. ElasticSearch操作文档
### 创建 index PUT http://localhost:9200/questions
### 删除一个Index DELETE http://localhost:9200/questions
### 设置index中的文档属性采用ik分词 ### type=text的才能分词,analyzer表示分词器,根据分词器对text内容进行分词,建立索引 ### search_analyzer表示搜索内容的分词器,一般与上面的分词器相同,建立索引 ### _mapping配合properties用来设置属性 ### 注意下面的换行,这里是回车并换行,有严格格式要求,必须这样书写 POST http://localhost:9200/questions/_mapping Content-Type: application/json { "properties": { "title": { "type": "text", "analyzer": "ik_max_word", "search_analyzer": "ik_max_word" }, "content": { "type": "text", "analyzer": "ik_max_word", "search_analyzer": "ik_max_word" } } }
### questions中添加文档 ### POST一般为新增或修改的意思,_create表示创建文档,/1中的1表示文档id,为真正的id ### 每执行一次请求必须通过###来分割,既是分隔符,也是注释符 POST http://localhost:9200/questions/_create/1 Content-Type: application/json { "id":1, "title":"Java基本数据类型有哪些", "content":"面时候为啥要问基本类型这么简单问题呀,我们要如何回答呢?" } ### questions 中添加文档 POST http://localhost:9200/questions/_create/2 Content-Type: application/json { "id":2, "title":"int类型的范围", "content":"为啥要了解int类型的范围呢?" } ### questions 中添加文档 POST http://localhost:9200/questions/_create/3 Content-Type: application/json { "id":3, "title":"常用集合类有哪些", "content":"为啥企业经常问集合呀?该如何回复呢" } ### questions 中添加文档 POST http://localhost:9200/questions/_create/4 Content-Type: application/json { "id":4, "title":"线程的run方法和start方法有啥区别", "content":"run方法可以执行线程的计算过程, start也可以执行线程的计算过程,用途一样么?" }
### 更新questions索引中的文档 ### 此处POST是更新的意思,表示对文档4进行更新 POST http://localhost:9200/questions/_doc/4/_update Content-Type: application/json { "doc": { "title": "Java线程的run方法和start方法有啥区别" } }
### 删除questions中的一个文档,DELETE表示删除 DELETE http://localhost:9200/questions/_doc/2
### 查询数据,GET表示查询 GET http://localhost:9200/questions/_doc/4
### 分词搜索 单属性模糊查询 查询分词索引,按照输出得分(_score:查询内容占整个内容的比例)由高到低排序 POST http://localhost:9200/questions/_search Content-Type: application/json { "query": { "match": {"title": "类型" } } }
### 多字段搜索 多属性模糊查询 格式固定 ### bool表示真假,should表示或,must表示与 ### 查询的内容也会分词 POST http://localhost:9200/questions/_search Content-Type: application/json { "query": { "bool": { "should": [ { "match": { "title": "java类型" }}, { "match": { "content": "java类型"}} ] } } }
5. ElasticSearch原生API操作工具类
最后附上自己写的一个请求工具类(使用这个不需要引入spring-data-es的jar包了,是依靠es自带的http请求操作)
import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.stereotype.Component; import org.springframework.util.MultiValueMap; import org.springframework.web.client.RestTemplate; import javax.annotation.PostConstruct; import java.util.Base64; /** * @author: yh * @Description: restTemplate工具类 * @date: 2022/8/4 15:43 */ @Component public class RestTemplateUtil { @Value("${spring.elasticsearch.rest.uris}") private String host; @Value("${spring.elasticsearch.rest.username}") private String userName; @Value("${spring.elasticsearch.rest.password}") private String passWord; /** * 认证的Authorization */ private static String authentication; /** * 查询ES POST-url */ public static String SEARCH_INDEX_URL = null; /** * 多个搜索API-url */ public static String MSEARCH_INDEX_URL = null; /** * 查询文档-url */ public static String GET_DOC_ID = null; /** * 新建文档-url */ public static String CREATE_DOC = null; /** * 更新文档 POST-url */ public static String UPDATE_INDEX_DOC = null; /** * 按查询更新API-url */ public static String UPDATE_BY_QUERY = null; /** * 创建ES索引,PUT请求-url */ public static String CREATE_INDEX = null; /** * 批量增删改API POST-url */ public static String BULK_URL = null; /** * 根据文档id查询文档,也可以判断文档是否存在-url */ public static String QUERY_INDEX_DOC = null; /** * Multi get (mget) API-url */ public static String INDEX_MGET = null; /** * 删除索引(危险) delete请求 */ public static String DELETE_INDEX = null; @PostConstruct public void initProperty() { //{esIndex}:操作索引名称,{id}:操作文档id SEARCH_INDEX_URL = "http://" + host + "/{esIndex}/_search"; MSEARCH_INDEX_URL = "http://" + host + "/{esIndex}/_msearch"; GET_DOC_ID = "http://" + host + "/{esIndex}/_doc/{id}"; CREATE_DOC = "http://" + host + "/{esIndex}/_create/{id}"; UPDATE_INDEX_DOC = "http://" + host + "/{esIndex}/_doc/{id}/_update"; UPDATE_BY_QUERY = "http://" + host + "/{esIndex}/_update_by_query"; CREATE_INDEX = "http://" + host + "/{esIndex}"; BULK_URL = "http://" + host + "/{esIndex}/_bulk"; QUERY_INDEX_DOC = "http://" + host + "/{esIndex}/_doc/{id}"; INDEX_MGET = "http://" + host + "/{esIndex}/_mget"; DELETE_INDEX = "http://" + host + "/{esIndex}"; authentication = "Basic " + Base64.getEncoder().encodeToString((userName + ":" + passWord).getBytes()); } private static RestTemplate restTemplate; @Autowired public void setRestTemplate(RestTemplate restTemplate) { RestTemplateUtil.restTemplate = restTemplate; } /** * POST请求 * * @param url 请求路径 * @param jsonStr 参数 * @return String * @author yh * @date 2022/8/4 */ public static String postForObject(String url, String jsonStr) { //设置header信息 HttpHeaders requestHeaders = new HttpHeaders(); requestHeaders.setContentType(MediaType.APPLICATION_JSON); requestHeaders.set("authorization", authentication); HttpEntity<String> requestEntity = new HttpEntity<>(jsonStr, requestHeaders); return restTemplate.postForObject(url, requestEntity, String.class); } /** * ndjson格式数据请求 * * @param ndjson 一种数据格式 * @param url url * @return String * @author yh * @date 2022/8/5 */ public static String postForNdjson(String url, String ndjson) { if (StringUtils.isBlank(ndjson)) { return "{}"; } HttpHeaders headers = new HttpHeaders(); headers.add("Content-Type", "application/x-ndjson"); headers.set("authorization", authentication); HttpEntity<String> request = new HttpEntity<>(ndjson, headers); return restTemplate.postForObject(url, request, String.class); } /** * get查询es * * @param url 请求路径 * @return String * @author yh * @date 2022/8/5 */ public static String getForString(String url) { if (StringUtils.isBlank(url)) { return "{}"; } HttpHeaders headers = new HttpHeaders(); headers.set("authorization", authentication); HttpEntity<MultiValueMap<String, Object>> request = new HttpEntity(null, headers); return restTemplate.exchange(url, HttpMethod.GET, request, String.class).getBody(); // return restTemplate.getForObject(url, String.class); } /** * PUT请求 * * @param url 请求路径 * @param jsonStr body参数 * @author yh * @date 2022/8/5 */ public static void putForVoid(String url, String jsonStr) { //设置header信息 HttpHeaders requestHeaders = new HttpHeaders(); requestHeaders.setContentType(MediaType.APPLICATION_JSON); requestHeaders.set("authorization", authentication); HttpEntity<String> requestEntity = new HttpEntity<>(jsonStr, requestHeaders); restTemplate.exchange(url, HttpMethod.PUT, requestEntity, String.class); // restTemplate.put(url, JSONObject.parse(json)); } /** * delete请求 * * @param url 请求路径 * @author yh * @date 2022/8/11 */ public static void deleteForVoid(String url) { restTemplate.delete(url); } }
需要注意的是_bulk批量操作时,换行符的使用(_bulk操作ES)
/** * 不同系统的换行符 * @date 2022/8/11 */ private String newLine = System.getProperty("line.separator");
还有一点注意的是:当操作es索引时,索引不存在就会返回404,不做配置的话会让程序直接抛出异常终止运行,我们希望状态码返回404时,走创建索引的逻辑,这时候就需要把RestTemplate相关的状态码加入白名单
import org.springframework.http.HttpStatus; import org.springframework.http.client.ClientHttpResponse; import org.springframework.web.client.DefaultResponseErrorHandler; import java.io.IOException; import java.util.ArrayList; import java.util.List; /** * 功能:捕获RestTemplate异常 * * @author yh * @date 2022/8/10 */ public class RtErrorHandler extends DefaultResponseErrorHandler { @Override public boolean hasError(ClientHttpResponse response) throws IOException { return super.hasError(response); } @Override public void handleError(ClientHttpResponse response) throws IOException { HttpStatus statusCode = HttpStatus.resolve(response.getRawStatusCode()); // 白名单。白名单上的异常则不处理,直接返回 List<HttpStatus> donotDeal = new ArrayList<>(); // 404不要抛异常 donotDeal.add(HttpStatus.NOT_FOUND); // donotDeal.add(HttpStatus.BAD_REQUEST); // 非白名单则处理 if (!donotDeal.contains(statusCode)) { super.handleError(response); } } }
import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.client.RestTemplate; /** * @author: yh * @Description: * @date: 2022/8/5 10:32 */ @Configuration public class RestConfig { /** * 定义restTemplate请求处理 * @param builder 可用于配置和创建RestTemplate的生成器。提供方便的方法来注册转换器、错误处理程序和UriTemplateHandlers。 * @author yh * @date 2022/8/10 * @return RestTemplate */ @Bean public RestTemplate restTemplate(RestTemplateBuilder builder){ RestTemplate build = builder.build(); build.setErrorHandler(new RtErrorHandler()); return build; } }