Spring Data Elasticsearch基于Spring Data API简化 Elasticsearch 操作,将原始操作Elasticsearch 的客户端API进行封装。Spring Data为Elasticsearch 项目提供集成搜索引擎。Spring Data Elasticsearch POJO的关键功能区域为中心的模型与Elastichsearch交互文档和轻松地编写一个存储索引库数据访问层。
spring-data-elasticsearch与ES、SpringBoot的对应关系
Spring Data通过注解来声明字段的映射属性,有下面的三个注解:
@Document
作用在类,标记实体类为文档对象,一般有两个属性
indexName:对应索引库名称
type:对应在索引库中的类型
shards:分片数量,默认5
replicas:副本数量,默认1@Id
作用在成员变量,标记一个字段作为id主键@Field
作用在成员变量,标记为文档的字段,并指定字段映射属性:
type:字段类型,取值是枚举:FieldType
index:是否索引,布尔类型,默认是true
store:是否存储,布尔类型,默认是false
analyzer:分词器名称
本文使用的是:
spring-boot(2.3.12.RELEASE)
spring-data-elasticsearch(2.3.12.RELEASE)
- 导入依赖
spring-data-elasticsearch
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
全部pom
<?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.3.12.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>ES-test</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>ES-test</name>
<description>ES-test</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</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>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.8.2</version>
</dependency>
<!-- junit 单元测试 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
- 配置文件
##单机版连接
##spring.elasticsearch.rest.uris=http://127.0.0.1:9200
##集群版连接
spring.elasticsearch.rest.uris= http://127.0.0.1:9200,http://127.0.0.1:9201,http://127.0.0.1:9202
spring.elasticsearch.rest.read-timeout= 30
spring.elasticsearch.rest.connection-timeout= 30
- 创建索引
请求地址:http://localhost:9200/blog
请求类型:PUT
请求体:
{
"settings": {
"number_of_shards": 5,
"number_of_replicas": 1
},
"mappings": {
"properties": {
"id":{
"type":"long"
},
"title": {
"type": "text"
},
"content": {
"type": "text"
},
"author":{
"type": "text"
},
"category":{
"type": "keyword"
},
"createTime": {
"type": "date",
"format":"yyyy-MM-dd HH:mm:ss.SSS||yyyy-MM-dd'T'HH:mm:ss.SSS||yyyy-MM-dd HH:mm:ss||epoch_millis"
},
"updateTime": {
"type": "date",
"format":"yyyy-MM-dd HH:mm:ss.SSS||yyyy-MM-dd'T'HH:mm:ss.SSS||yyyy-MM-dd HH:mm:ss||epoch_millis"
},
"status":{
"type":"integer"
},
"serialNum": {
"type": "keyword"
}
}
}
}
- 编写实体类
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.DateFormat;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
import java.util.Date;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Document(indexName = "blog", shards = 1, replicas = 1)
public class Blog {
//此项作为id,不会写到_source里边。
@Id
private Long blogId;
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String title;
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String content;
@Field(type = FieldType.Text)
private String author;
//博客所属分类。
@Field(type = FieldType.Keyword)
private String category;
//0: 未发布(草稿) 1:已发布 2:已删除
@Field(type = FieldType.Integer)
private int status;
//序列号,用于给外部展示的id
@Field(type = FieldType.Keyword)
private String serialNum;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss.SSS")
@Field(type= FieldType.Date, format= DateFormat.custom, pattern="yyyy-MM-dd HH:mm:ss.SSS")
private Date createTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss.SSS")
@Field(type=FieldType.Date, format=DateFormat.custom, pattern="yyyy-MM-dd HH:mm:ss.SSS")
private Date updateTime;
}
- 编写Dao
import com.example.estest.bean.Blog;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
/**
* 自定义的dao.类似于mybatis的dao,在此处可以自定义方法
*/
public interface BlogRepository extends ElasticsearchRepository<Blog, Long> {
}
- 增删改查
import com.example.estest.bean.Blog;
import com.example.estest.dao.BlogRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.elasticsearch.core.SearchHit;
import org.springframework.web.bind.annotation.*;
import java.util.*;
/**
* 测试ES
*/
@RestController
@RequestMapping("crud")
public class CrudController {
@Autowired
private BlogRepository blogRepository;
//高亮查询需要
@Autowired
private RestHighLevelClient client;
/**
* 添加单个文档
* @return
*/
@GetMapping("addDocument")
public Blog addDocument() {
Long id = 1L;
Blog blog = new Blog();
blog.setBlogId(id);
blog.setTitle("我是一个大标题" + id);
blog.setContent("这是添加单个文档的实例" + id);
blog.setAuthor("wf");
blog.setCategory("ElasticSearch");
blog.setCreateTime(new Date());
blog.setStatus(1);
blog.setSerialNum(id.toString());
blog.setUpdateTime(new Date());
return blogRepository.save(blog);
}
/**
* 查找指定文档
* @param id
* @return
*/
@GetMapping("findById")
public Blog findById(Long id) {
return blogRepository.findById(id).get();
}
/**
* 添加多个文档
* @param count
* @return
*/
@GetMapping("addDocuments")
public Object addDocuments(Integer count) {
List<Blog> blogs = new ArrayList<>();
for (int i = 1; i <= count; i++) {
Long id = (long)i;
Blog blog = new Blog();
blog.setBlogId(id);
blog.setTitle("我是一个大标题" + id);
blog.setContent("这是博客内容" + id);
blog.setAuthor("wf");
blog.setCategory("ElasticSearch");
blog.setCreateTime(new Date());
blog.setStatus(1);
blog.setSerialNum(id.toString());
blogs.add(blog);
}
return blogRepository.saveAll(blogs);
}
/**
* 修改单个文档,相当于是覆盖,没有设置的内容为null,也会一同替换
* @return
*/
@GetMapping("editDocument")
public Blog editDocument(Long id) {
Blog blog = new Blog();
blog.setBlogId(id);
blog.setTitle("Spring Data ElasticSearch学习教程" + id);
blog.setContent("这是修改单个文档的实例" + id);
return blogRepository.save(blog);
}
/**
* 删除指定文档
* @param id
* @return
*/
@GetMapping("deleteDocument")
public String deleteDocument(Long id) {
blogRepository.deleteById(id);
return "success";
}
/**
* 删除所有文档
* @return
*/
@GetMapping("deleteDocumentAll")
public String deleteDocumentAll() {
blogRepository.deleteAll();
return "success";
}
/**
* 查询全部文档
* @return
*/
@GetMapping("queryAll")
public Object queryAll() {
return blogRepository.findAll();
}
/**
* 分页查询文档
*
* 注意:
* 这里的分页和mysql的分页有所不同,此处分页只需要传递页码即可,无需计算起始位置
* 第一页的页码为 0, 第二页为 1,第三页为 2,以此类推
*
*/
@GetMapping("findByPageable/{page}/{size}")
public List<Blog> findByPageable(@PathVariable("page") Integer page, @PathVariable("size") Integer size){
//设置排序(排序方式,正序还是倒序,排序的 id)
Sort sort = Sort.by(Sort.Direction.DESC,"blogId");
//设置查询分页
PageRequest pageRequest = PageRequest.of(page, size,sort);
//分页查询
Page<Blog> productPage = blogRepository.findAll(pageRequest);
System.out.println("总页数:"+productPage.getTotalPages());
System.out.println("分页参数信息:"+productPage.getPageable());
System.out.println("总条数:"+productPage.getTotalElements());
System.out.println("当前页码:"+productPage.getNumber());
System.out.println("单页展示条数:"+productPage.getNumberOfElements());
System.out.println("排序字段及方式:"+productPage.getSort());
System.out.println("当前集合总数:"+productPage.getSize());
return productPage.getContent();
}
/**
* 高亮查询+分页+排序
* @param keyword 本次搜索的值
* @param pageNum 分页页码
* @param pageSize 单页展示条数
* @return
*/
@GetMapping("list/{keyword}/{pageNum}/{pageSize}")
public List<Map<String, Object>> list(@PathVariable("keyword") String keyword, @PathVariable("pageNum") Integer pageNum, @PathVariable("pageSize") Integer pageSize ) {
// 从blog索引中查出来并指定title,content字段高亮
List<Map<String, Object>> list = highLightSearch("blog", "title", "content", keyword, pageNum, pageSize);
return list;
}
/**
* 高亮搜索+分页+按照指定字段排序
*
* @param indexName 索引名称
* @param field 需要匹配的字段
* @param field2 需要匹配的字段
* @param keyword 匹配的值
* @param pageNum 分页页码
* @param pageSize 单页展示条数
* @return 返回替换原本内容后的数据
*/
public List<Map<String, Object>> highLightSearch(String indexName, String field, String field2, String keyword, Integer pageNum, Integer pageSize) {
List<Map<String, Object>> resList = new ArrayList<>();
// 1. 创建 SearchRequest并执行索引名称,名称可以指定多个
SearchRequest searchRequest = new SearchRequest(indexName);
// 2. 创建 SearchSourceBuilder
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
//避免匹配空字符串
if("".equals(keyword)){
//设置需要匹配的字段及匹配的值
searchSourceBuilder.query(QueryBuilders.matchAllQuery());
}else{
//设置需要匹配的字段及匹配的值
searchSourceBuilder.query(QueryBuilders.multiMatchQuery(keyword, field, field2));
//嵌套对象匹配查询
// QueryBuilder orderQuery = QueryBuilders.nestedQuery("userInformationId",
// QueryBuilders.boolQuery()
// .filter(QueryBuilders.matchQuery("userInformationId.ID", userId)),
// ScoreMode.None);
//
// QueryBuilder queryBuilder = QueryBuilders.boolQuery().must(orderQuery);
// searchSourceBuilder.postFilter(queryBuilder);
}
// 设置分页参数
searchSourceBuilder.from((pageNum - 1) * pageSize);
searchSourceBuilder.size(pageSize);
searchSourceBuilder.timeout(new TimeValue(60, TimeUnit.SECONDS));
//4.按照score正序排列
// searchSourceBuilder.sort(SortBuilders.scoreSort().order(SortOrder.ASC));
//5.按照id倒序排列(score会失效返回NaN)
searchSourceBuilder.sort(SortBuilders.fieldSort("createTime").order(SortOrder.ASC));
// 设置高亮规则
HighlightBuilder highlightBuilder = new HighlightBuilder();
highlightBuilder.field(field).field(field2);
//设置高亮代码
highlightBuilder.preTags("<span style='color:red'>");
highlightBuilder.postTags("</span>");
searchSourceBuilder.highlighter(highlightBuilder);
// 4. 将 SearchSourceBuilder 添加到 SearchRequest
searchRequest.source(searchSourceBuilder);
try {
// 5. 搜索
SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);
TotalHits totalHits = response.getHits().getTotalHits();
System.out.println("总记录条数"+totalHits.value);
// 6. 查询到的文档
org.elasticsearch.search.SearchHit[] hits = response.getHits().getHits();
for (org.elasticsearch.search.SearchHit hit : hits) {
// 获取到全部字段高亮部分
Map<String, HighlightField> highlightFields = hit.getHighlightFields();
// 用高亮后的结果取代原来字段
Map<String, Object> source = hit.getSourceAsMap();
if (highlightFields.get(field) != null) {
// 获取 指定field部分高亮出的片段
Text[] fragments = highlightFields.get(field).fragments();
// 拼装
StringBuilder builder = new StringBuilder();
for (Text text : fragments) {
builder.append(text);
}
source.put(field, builder.toString());
}
if (highlightFields.get(field2) != null) {
// 获取 指定field部分高亮出的片段
Text[] fragments2 = highlightFields.get(field2).fragments();
// 拼装
StringBuilder builder2 = new StringBuilder();
for (Text text2 : fragments2) {
builder2.append(text2);
}
source.put(field2, builder2.toString());
}
// 将这条记录加入结果集
resList.add(source);
}
} catch (IOException e) {
System.out.println("===执行搜索失败===");
e.printStackTrace();
}
return resList;
}
}
- 自定义查询方法及高亮查询
跟Spring Data JPA类似,spring data elsaticsearch提供了自定义方法的查询方式,
在Repository接口中自定义方法,spring data根据方法名,自动生成实现类,但是方法名必须符合一定的规则
,
在自定义的Dao中编写如下方法:
import com.example.estest.bean.Blog;
import org.springframework.data.elasticsearch.annotations.Highlight;
import org.springframework.data.elasticsearch.annotations.HighlightField;
import org.springframework.data.elasticsearch.annotations.HighlightParameters;
import org.springframework.data.elasticsearch.core.SearchHit;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import java.util.List;
/**
* 自定义的dao.类似于mybatis的dao,在此处可以自定义方法
*/
public interface BlogRepository extends ElasticsearchRepository<Blog, Long> {
/**
* 自定义根据title搜索查询,并高亮显示title属性
* 此处高亮使用的是,默认的 <em></em> 斜体标签
*
* @param title 模糊查询的 title 值
* @return
*/
@Highlight(fields = {
@HighlightField(name = "title")
})
List<SearchHit<Blog>> findByTitle(String title);
/**
* 根据content模糊查询
* 此处高亮使用的是,自定义的span标签
*
* @param content 模糊查询的 title 值
* @return
*/
@Highlight(fields = {
@HighlightField(name = "content")
}, parameters = @HighlightParameters(
preTags = "<span style='color:red'>",
postTags = "</span>"
))
List<SearchHit<Blog>> findByContent(String content);
/**
* 根据title,content模糊查询
* 此处高亮使用的是,自定义的span标签
*
* @param content 模糊查询的 title 值
* @return
*/
@Highlight(
fields = {
@HighlightField(name = "title"),
@HighlightField(name = "content")
},
parameters = @HighlightParameters(
preTags = "<span style='color:red'>",
postTags = "</span>"
))
List<SearchHit<Blog>> findByTitleAndContent(String title, String content);
}
编写接口:
import com.example.estest.bean.Blog;
import com.example.estest.dao.BlogRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.elasticsearch.core.SearchHit;
import org.springframework.web.bind.annotation.*;
import java.util.*;
/**
* 测试ES
*/
@RestController
@RequestMapping("crud")
public class CrudController {
@Autowired
private BlogRepository blogRepository;
……
此处省略上文接口
……
/**
* 默认高亮标签模糊查询 title
* @param title
* @return
*/
@GetMapping("highlightQueryTitle/{title}")
public List<SearchHit<Blog>> highlightQueryTitle(@PathVariable("title") String title) {
return blogRepository.findByTitle(title);
}
/**
* 自定义高亮标签查询 content
* @param content
* @return
*/
@GetMapping("highlightQueryContent/{content}")
public List<SearchHit<Blog>> highlightQueryContent(@PathVariable("content") String content) {
return blogRepository.findByContent(content);
}
/**
* 自定义高亮标签查询 content 和 title
* @param content
* @return
*/
@GetMapping("highlightQueryTitleAndContent/{title}/{content}")
public List<SearchHit<Blog>> highlightQueryTitleAndContent(@PathVariable("title") String title,@PathVariable("content") String content) {
return blogRepository.findByTitleAndContent(title,content);
}
}
- 高亮查询效果
- 自定义DSL查询
跟JPA一样,Spring Data ElasticSearch可以使用@Query自定义语句进行查询。
但Spring Data ElasticSearch不能通过冒号指定参数(比如:title),只能用问号加序号,比如?0)
在自定义的Dao中编写如下方法:
import com.example.estest.bean.Blog;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.annotations.Highlight;
import org.springframework.data.elasticsearch.annotations.HighlightField;
import org.springframework.data.elasticsearch.annotations.HighlightParameters;
import org.springframework.data.elasticsearch.annotations.Query;
import org.springframework.data.elasticsearch.core.SearchHit;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import org.springframework.data.repository.query.Param;
import java.util.List;
/**
* 自定义的dao.类似于mybatis的dao,在此处可以自定义方法
*/
public interface BlogRepository extends ElasticsearchRepository<Blog, Long> {
………………
………此处省略前文内容………
………………
/**
* 根据标题和内筒查询
* @param title
* @param content
* @return
*/
@Query("{\"bool\":{\"must\":[{\"match\":{\"title\":\"?0\"}}," +
"{\"match\":{\"content\":\"?1\"}}]}}")
List<Blog> findByTitleAndContentCustom(@Param("title") String title, @Param("content") String content);
/**
* 根据标题和内容分页查询
* @param title
* @param content
* @param pageable
* @return
*/
@Query("{\"bool\":{\"must\":[{\"match\":{\"title\":\"?0\"}}," +
"{\"match\":{\"content\":\"?1\"}}]}}")
Page<Blog> findByTitleAndContentCustom(@Param("title") String title, @Param("content") String content,Pageable pageable);
}
编写接口:
import com.example.estest.bean.Blog;
import com.example.estest.dao.BlogRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.elasticsearch.core.SearchHit;
import org.springframework.web.bind.annotation.*;
import java.util.*;
/**
* 测试ES
*/
@RestController
@RequestMapping("crud")
public class CrudController {
@Autowired
private BlogRepository blogRepository;
………………
………此处省略前文内容………
………………
/**
* 根据标题和内筒查询
* @param title
* @param content
* @return
*/
@GetMapping("listByTitleAndContent/{title}/{content}")
public List<Blog> listByTitleAndContent(@PathVariable("title") String title,@PathVariable("content") String content) {
return blogRepository.findByTitleAndContentCustom(title, content);
}
/**
* 根据标题和内容分页查询
* @param title
* @param content
* @return
*/
@GetMapping("pageByTitleAndContent/{title}/{content}")
public Page<Blog> pageByTitleAndContent(@PathVariable("title") String title,@PathVariable("content") String content) {
PageRequest pageRequest = PageRequest.of(0, 2);
return blogRepository.findByTitleAndContentCustom(title, content, pageRequest);
}
}
- 测试