上篇解释了 ES 的基本概念和分词器。Elastic Search (ES)Java 入门实操(1)下载安装、概念-CSDN博客
Elastic Search(ES)Java 入门实操(3)数据同步-CSDN博客
这篇主要演示 Java 整合 ES进行数据搜索。
ES 实现搜索接口
首先根据 MySQL 字段,在 ES 创建索引。
create table mydb
(
id int auto_increment comment '序号'
primary key,
title varchar(20) not null comment '标题',
content text not null comment '内容',
cretateTime datetime default CURRENT_TIMESTAMP not null comment '创建时间',
updateTime datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
isDelete tinyint default 0 not null comment '是否删除'
)
comment '文章' collate = utf8mb4_unicode_ci;
PUT article_1
{
"aliases": { #别名
"article": {}
},
"mappings": {
"properties": {
"title": {
"type": "text", #字段类型
"analyzer": "ik_max_word",#插入时分词方式
"search_analyzer": "ik_smart", #查询时分词方式
"fields": { #字段配置,子字段
"keyword": {
"type": "keyword", #精确匹配
"ignore_above": 256 #超过 256 字符就忽略查询
}
}
},
"content": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"createTime": {
"type": "date"
},
"updateTime": {
"type": "date"
},
"isDelete": {
"type": "keyword"
}
}
}
}
引入 spring-data-elasticsearch 依赖
需要注意版本号一定要兼容
Spring Data Elasticsearch - Reference Documentation
这里使用的是 7.17版本,所以选择相近的 4.4.x版本的 依赖,同样的 springboot 的版本也得严格对应。Maven Repository: org.springframework.data » spring-data-elasticsearch » 4.4.7 (mvnrepository.com)
下面的报错就是版本不对,需要把springboot 改为 2.7+,
java.lang.NoSuchFieldError: INDEX_CONTENT_TYPE
<!-- https://mvnrepository.com/artifact/org.springframework.data/spring-data-elasticsearch -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-elasticsearch</artifactId>
<version>4.4.7</version>
</dependency>
启动 springboot,有调用的日志
给 ES 索引创建实体类
/**
* ES 实体类
* document 注解是将Java 对象映射到 Elasticsearch 索引和类型中
*/
@Document(indexName = "article")
@Data
public class ArticleEsDto implements Serializable {
private static final String DATE_TIME_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'";
/**
* id
* 需要打上 id 注解,指定 ES 中存储的 id 是唯一字段
* 如果新增是不传入,则 ES 会自动生成
*/
@Id
private long id;
/**
* 标题
*/
private String title;
/**
* 内容
*/
private String content;
/**
* 创建时间
*/
@Field(index = false, store = true,type = FieldType.Date,format = {},pattern = DATE_TIME_PATTERN)
private Date createTime;
/**
* 更新时间
* Field:这是一个MyBatis-Plus注解,用于标注该字段在数据库表中的对应关系。
* 其中,index = false表示该字段不在数据库表的索引中,
* store = true表示该字段在数据库表的存储中,
* type = FieldType.Date表示该字段的类型为Date,
* format = {}表示该字段的格式为空,
* pattern = DATE_TIME_PATTERN表示该字段的日期时间格式为DATE_TIME_PATTERN。
*/
@Field(index = false, store = true,type = FieldType.Date,format = {},pattern = DATE_TIME_PATTERN)
private Date updateTime;
/**
* 是否删除
*/
private Integer isDelete;
private static final long serialVersionUID = 1L;
}
第一种方式
elasticsearch Respository,新建类继承该类,默认提供了简单的增删改查方法,多用于可以预期的,相对不复杂的查询
@NoRepositoryBean
public interface CrudRepository<T, ID> extends Repository<T, ID> {
<S extends T> S save(S entity);
<S extends T> Iterable<S> saveAll(Iterable<S> entities);
Optional<T> findById(ID id);
boolean existsById(ID id);
Iterable<T> findAll();
Iterable<T> findAllById(Iterable<ID> ids);
long count();
void deleteById(ID id);
void delete(T entity);
void deleteAllById(Iterable<? extends ID> ids);
void deleteAll(Iterable<? extends T> entities);
void deleteAll();
}
/**
* ES 的控制层
* 继承 ElasticsearchRepository 即可
*/
public interface ArticleEsDao extends ElasticsearchRepository<ArticleEsDto, Long> {
// 这里可以扩展一些自定义方法
// 例如:根据标题模糊查询
List<ArticleEsDto> findByTitle(String title);
}
新增测试
//注入接口
@Resource
private ArticleEsDao articleEsDao;
//测试新增
@Test
void EsTest1(){
//创建实体对象并添加属性
ArticleEsDto articleEsDto = new ArticleEsDto();
articleEsDto.setId(1L);
articleEsDto.setTitle("青花瓷");
articleEsDto.setContent("天青色等烟雨而我在等你");
articleEsDto.setCreateTime(new Date());
articleEsDto.setUpdateTime(new Date());
articleEsDto.setIsDelete(0);
//调用方法保存
articleEsDao.save(articleEsDto);
System.out.println(articleEsDto.getId());
}
dev tools 查看
GET article/_search/
自定义方法测试
我们在上面创建接口的时候创建了一个根据标题查询的方法
@Test
void EsTest2(){
List<ArticleEsDto> articleEsDtos = articleEsDao.findByTitle("青花瓷");
System.out.println(articleEsDtos);
}
第二种方式
spring 默认提供了操作 ES 的客户端对象 ElasticSearchRestTemplate,同样提供了增删改查,更加灵活,适用于更加复杂的操作,返回结果更加完整,但是需要自己解析。
@Resource
private ElasticsearchRestTemplate elasticsearchRestTemplate;
提示,在编写查询条件以及处理数据时,可以先在 Dev Tools 中执行一下查询,没问题之后再进行代码层面的条件编写。
查询 DSL
官方文档:Query and filter context | Elasticsearch Guide [8.14] | Elastic
查询模式:Boolean query | Elasticsearch Guide [8.14] | Elastic
GET /_search
{
"query": {
"bool": { //组合条件
"must": [ //必须匹配
{ "match": // 模糊匹配{ "title": "Search" }},
{ "match": { "content": "Elasticsearch" }}
],
"filter": [ //
{ "term": //精确匹配 { "status": "published" }},
{ "range": //范围匹配 { "publish_date": { "gte": "2015-01-01" }}}
]
}
}
}
除了 must ,filter,还有 must_not,必须不存在才能匹配,should,至少有多少个条件相符才匹配,同时还有一个参数 minimum_should_match 满足最小的条件数,比如 1,至少满足一个条件才能查询到,比如标题和描述存在一个就可以返回结果。
POST _search
{
"query": {
"bool" : {
"must" : {
"term" : { "user.id" : "kimchy" }
},
"filter": {
"term" : { "tags" : "production" }
},
"must_not" : {
"range" : {
"age" : { "gte" : 10, "lte" : 20 }
}
},
"should" : [
{ "term" : { "tags" : "env1" } },
{ "term" : { "tags" : "deployed" } }
],
"minimum_should_match" : 1,
"boost" : 1.0
}
}
}
需要注意的是,通过模糊查询之后,查询结果中有一个参数是 max_score,表示这条数据和搜索条件的最高匹配度。
在 Java 中编写查询条件以及处理查询的数据。
主要使用到的 API
//查询条件构造器
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
//排序条件构造器
SortBuilder<?> sortBuilder = SortBuilders.scoreSort();
//分页
PageRequest pageRequest = PageRequest.of((int) current, (int) pageSize);
//组合查询条件
NativeSearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(boolQueryBuilder)
.withPageable(pageRequest).withSorts(sortBuilder).build();
//调用 elasticsearchRestTemplate 查询
SearchHits<PostEsDTO> searchHits = elasticsearchRestTemplate.search(searchQuery, PostEsDTO.class);
完整代码
/**
* 从 es 查询数据
*/
@Service
public class ArticleEsManager {
@Resource
private ArticleMapper articleMapper;
@Resource
private ElasticsearchRestTemplate elasticsearchRestTemplate;
public Page<Article> searchByEs(ArticleQueryRequest articleQueryRequest) {
//提取查询参数
Long id = articleQueryRequest.getId();
String searchText = articleQueryRequest.getSearchText();
String content = articleQueryRequest.getContent();
String title = articleQueryRequest.getTitle();
//设置分页参数,起始页为 0
int current = articleQueryRequest.getCurrent() -1 ;
int pageSize = articleQueryRequest.getPageSize();
String sortField = articleQueryRequest.getSortField();
String sortOrder = articleQueryRequest.getSortOrder();
//构建布尔查询,创建 boolqueryBuilder,用于后续查询条件构建
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
//过滤查询条件
//过滤掉被逻辑删除的 term 是精确匹配
boolQueryBuilder.filter(QueryBuilders.termQuery("isDelete",0));
//id必须精确匹配
if(id!=null){
boolQueryBuilder.filter(QueryBuilders.termQuery("id",id));
}
//模糊查询,按照查询词(关键词)检索
if(StringUtils.isNotEmpty(searchText)){
boolQueryBuilder.should(QueryBuilders.matchQuery("title",searchText));
boolQueryBuilder.should(QueryBuilders.matchQuery("content",searchText));
//设置至少多少匹配才进行查询
boolQueryBuilder.minimumShouldMatch(1);
}
//根据标题检索
if(StringUtils.isNotEmpty(title)){
boolQueryBuilder.should(QueryBuilders.matchQuery("title",title));
boolQueryBuilder.minimumShouldMatch(1);
}
//根据内容检索
if(StringUtils.isNotEmpty(content)){
boolQueryBuilder.should(QueryBuilders.matchQuery("content",searchText));
//设置至少多少匹配才进行查询
boolQueryBuilder.minimumShouldMatch(1);
}
//进行排序
//对查询结果的分数进行排序
SortBuilder<?> sortBuilder = SortBuilders.scoreSort();
if (StringUtils.isNotEmpty(sortField) && StringUtils.isNotEmpty(sortOrder)){
sortBuilder = SortBuilders.fieldSort(sortField);
sortBuilder.order(CommonConstant.SORT_ORDER_ASC.equals(sortOrder) ? SortOrder.ASC:SortOrder.DESC);
}
//分页
PageRequest pageRequest = PageRequest.of(current, pageSize);
//构造查询
NativeSearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(boolQueryBuilder)
.withPageable(pageRequest).withSorts(sortBuilder).build();
//调用 elasticsearchRestTemplate 执行查询
SearchHits<ArticleEsDto> searchHits = elasticsearchRestTemplate.search(searchQuery, ArticleEsDto.class);
System.out.println(searchHits);
Page<Article> page = new Page<>();
page.setTotal(searchHits.getTotalHits());
//新建集合存储文章
List<Article> resourceList = new ArrayList<>();
//处理结果,判断是否有搜索结果
if(searchHits.hasSearchHits()){
//获取结果列表
List<SearchHit<ArticleEsDto>> searchHits1 = searchHits.getSearchHits();
System.out.println(searchHits1);
//获取结果的id,使用id在数据库中查询
List<Long> ids = searchHits1.stream().map(searchHit -> searchHit.getContent().getId()).collect(Collectors.toList());
List<Article> articles = articleMapper.selectBatchIds(ids);
//将查询结果与数据库查询结果进行匹配
if (CollectionUtils.isNotEmpty(articles)) {
//将查询结果与数据库查询结果进行匹配
Map<Long, List<Article>> collect = articles.stream().collect(Collectors.groupingBy(Article::getId));
//遍历文章id
articles.forEach(articleId -> {
if(collect.containsKey(articleId)){
resourceList.add(collect.get(articleId).get(0));
}else{
// 从 es 清空 db 已物理删除的数据
String delete = elasticsearchRestTemplate.delete(String.valueOf(articleId), ArticleEsDto.class);
}
});
}
}
page.setRecords(resourceList);
return page;
}
}
成功查询到数据
完整代码
/**
* 从 es 查询数据
*/
@Service
public class ArticleEsManager {
@Resource
private ArticleMapper articleMapper;
@Resource
private ElasticsearchRestTemplate elasticsearchRestTemplate;
public Page<Article> searchByEs(ArticleQueryRequest articleQueryRequest) {
//提取查询参数
Long id = articleQueryRequest.getId();
String searchText = articleQueryRequest.getSearchText();
String content = articleQueryRequest.getContent();
String title = articleQueryRequest.getTitle();
//设置分页参数,起始页为 0
int current = articleQueryRequest.getCurrent() -1 ;
int pageSize = articleQueryRequest.getPageSize();
String sortField = articleQueryRequest.getSortField();
String sortOrder = articleQueryRequest.getSortOrder();
//构建布尔查询,创建 boolqueryBuilder,用于后续查询条件构建
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
//过滤查询条件
//过滤掉被逻辑删除的 term 是精确匹配
boolQueryBuilder.filter(QueryBuilders.termQuery("isDelete",0));
//id必须精确匹配
if(id!=null){
boolQueryBuilder.filter(QueryBuilders.termQuery("id",id));
}
//模糊查询,按照查询词(关键词)检索
if(StringUtils.isNotEmpty(searchText)){
boolQueryBuilder.should(QueryBuilders.matchQuery("title",searchText));
boolQueryBuilder.should(QueryBuilders.matchQuery("content",searchText));
//设置至少多少匹配才进行查询
boolQueryBuilder.minimumShouldMatch(1);
}
//根据标题检索
if(StringUtils.isNotEmpty(title)){
boolQueryBuilder.should(QueryBuilders.matchQuery("title",title));
boolQueryBuilder.minimumShouldMatch(1);
}
//根据内容检索
if(StringUtils.isNotEmpty(content)){
boolQueryBuilder.should(QueryBuilders.matchQuery("content",searchText));
//设置至少多少匹配才进行查询
boolQueryBuilder.minimumShouldMatch(1);
}
//进行排序
//对查询结果的分数进行排序
SortBuilder<?> sortBuilder = SortBuilders.scoreSort();
if (StringUtils.isNotEmpty(sortField) && StringUtils.isNotEmpty(sortOrder)){
sortBuilder = SortBuilders.fieldSort(sortField);
sortBuilder.order(CommonConstant.SORT_ORDER_ASC.equals(sortOrder) ? SortOrder.ASC:SortOrder.DESC);
}
//分页
PageRequest pageRequest = PageRequest.of(current, pageSize);
//构造查询
NativeSearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(boolQueryBuilder)
.withPageable(pageRequest).withSorts(sortBuilder).build();
//调用 elasticsearchRestTemplate 执行查询
SearchHits<ArticleEsDto> searchHits = elasticsearchRestTemplate.search(searchQuery, ArticleEsDto.class);
System.out.println(searchHits);
Page<Article> page = new Page<>();
page.setTotal(searchHits.getTotalHits());
//新建集合存储文章
List<Article> resourceList = new ArrayList<>();
//处理结果,判断是否有搜索结果
if(searchHits.hasSearchHits()){
//获取结果列表
List<SearchHit<ArticleEsDto>> searchHits1 = searchHits.getSearchHits();
System.out.println(searchHits1);
//获取结果的id,使用id在数据库中查询
List<Long> ids = searchHits1.stream().map(searchHit -> searchHit.getContent().getId()).collect(Collectors.toList());
List<Article> articleList = articleMapper.selectBatchIds(ids);
//将查询结果与数据库查询结果进行匹配
if (CollectionUtils.isNotEmpty(articleList)) {
//将查询结果与数据库查询结果进行匹配
Map<Long, List<Article>> collect = articleList.stream().collect(Collectors.groupingBy(Article::getId));
//遍历文章id
ids.forEach(articleId -> {
if(collect.containsKey(articleId)){
resourceList.add(collect.get(articleId).get(0));
}else{
// 从 es 清空 db 已物理删除的数据
String delete = elasticsearchRestTemplate.delete(String.valueOf(articleId), ArticleEsDto.class);
}
});
}
}
//设置到分页里
page.setRecords(resourceList);
return page;
}