Spring Boot - 数据库集成06 - 集成ElasticSearch
Spring boot 集成 ElasticSearch
文章目录
- Spring boot 集成 ElasticSearch
- 一:前置工作
- 1:项目搭建和依赖导入
- 2:客户端连接相关构建
- 3:实体类相关注解配置说明
- 二:客户端client相关操作说明
- 1:检索流程
- 1.1:构建请求request和源source
- 1.2:查询建造器queryBuilder构建
- 1.3:请求建造器加入到请求源,请求源加入到请求中
- 1.4:结果hits和highlight分析
- 2:各种查询构造器说明
- 2.1:MatchQueryBuilder
- 2.2:RegexQueryBuilder
- 2.3:IdsQueryBuilder
- 2.4:MatchPhraseQueryBuilder
- 2.5:MatchPhrasePrefixQueryBuilder
- 2.6:MultiMatchQueryBuilder
- 2.7:TermQueryBuilder
- 2.8:FuzzyQueryBuilder
- 2.9:RangeQueryBuilder
- 2.10:WildcardQueryBuilder
- 2.11:BoolQueryBuilder
- 2.12:其他的QueryBuilder
- 3:聚合建造器说明
- 3.1:构造聚合条件
- 3.2:将聚合条件交给builder,builder交给源,封装request
- 3.3:结果解析和处理
- 三:repo & template
- 1:ElasticsearchRepository
- 1.1:repo介绍
- 1.2:自定义方法命名规范
- 2:ElasticsearchRestTemplate
- 2.1:template介绍
- 2.2:template查询操作
- 2.2.1:入参特殊情况处理【包括异常入参】
- 2.2.2:构建查询条件Criteria
- 2.2.3:构建高亮条件
- 2.2.4:查询(也可能是其他的方法,不一定是search)
- 2.2.5:结果封装
- 2.3:创建索引和删除索引
- 3:使用实例
Elasticsearch是面向文档型数据库,一条数据在这里就是一个文档,用 JSON作为文档序列化的格式
{
"name" : "John",
"sex" : "Male",
"age" : 25,
"birthDate": "1990/05/01",
"about" : "I love to go rock climbing",
"interests": [ "sports", "music" ]
}
ES和传统的关系型数据库相关术语对比如下:
关系型数据库 | ES |
---|---|
数据库(Database) | 索引(index) |
表(table) | 类型(Type)[es6.0.0废弃] |
行(row) | 文档(document) |
列(column) | 字段(field) |
表结构(schema) | 映射(mapping) |
索引 | 反向索引 |
SQL | 查询DSL |
Select * from table | Get http://… |
update table set… | Put http://… |
delete | Delete http://… |
一:前置工作
1:项目搭建和依赖导入
spring data
->spring-boot-starter-data-elasticsearch
->repo & template
transport & elasticsearch-rest-high-level-client
->client
<dependencies>
<!-- spring boot启动器 -->
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- spring boot web启动器 -->
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- spring boot 测试启动器 -->
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- commons-lang3工具类 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!-- spring data elasticsearch -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
<!-- es 高阶客户端 -->
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
</dependency>
<!-- es 低阶客户端 -->
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>transport</artifactId>
</dependency>
</dependencies>
spring:
elasticsearch:
rest:
uris: http://localhost:9200,http://localhost:9201,http://localhost:9202 # 集群地址
connection-timeout: 5s # 连接超时时间
read-timeout: 30s # 读取超时时间
max-connections: 100 # 最大连接数
max-connections-per-route: 20 # 每个路由的最大连接数
password: es_password
2:客户端连接相关构建
package com.cui.es_demo.config;
import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.elasticsearch.client.ClientConfiguration;
import org.springframework.data.elasticsearch.client.RestClients;
import java.util.Arrays;
/**
* @author cui haida
* 2025/1/29
*/
@Configuration
@ConfigurationProperties(prefix = "spring.elasticsearch.rest")
public class ElasticSearchClientConfig {
/**
* 集群地址
*/
private String uris;
/**
* 用户名和密码
*/
private String username;
private String password;
@Bean
public RestHighLevelClient restHighLevelClient() {
// 创建连接, 指定url和用户名密码
ClientConfiguration build = ClientConfiguration.builder()
.connectedTo(uris)
.withBasicAuth(username, password)
.build();
return RestClients.create(build).rest();
}
}
3:实体类相关注解配置说明
package com.cui.es_demo.entity;
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.time.LocalDateTime;
/**
* 实体类相关注解介绍
* - @Document:指定索引库的名称,以及分片数、副本数、刷新间隔、是否允许创建索引
* - @Id:指定主键
* - @Field:指定字段的名称,以及字段的分词器,以及字段的存储类型,以及字段的分词器
* - @JsonFormat:指定日期格式(注意日期类型字段不要用java.util.Date类型,要用java.time.LocalDate或java.time.LocalDateTime类型)
* @author cui haida
* 2025/1/30
*/
@NoArgsConstructor
@AllArgsConstructor
@Data
// 指定对应的索引名称,, 主分片数 = 3, 副本数 = 1, 刷新间隔 = 1s, 允许创建索引
@Document(indexName = "person", shards = 3, replicas = 1, refreshInterval = "1s", createIndex = true)
public class Person {
@Id
private String id;
@Field(type = FieldType.Keyword)
private String name;
@Field(type = FieldType.Keyword)
private String age;
// text类型,并使用IK最粗粒度的分词器
// 检索时的分词器采用的是最细粒度的IK分词器
@Field(type = FieldType.Text, analyzer = "ik_smart", searchAnalyzer = "ik_max_word")
private String address;
// 注意日期格式的特殊处理
// 指定格式化和时区
@Field(type = FieldType.Date, format = DateFormat.custom, pattern = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime createTime;
}
二:客户端client相关操作说明
对索引相关的操作都不推荐在这里执行,建议直接使用Kibana
文档的更新和插入操作建议使用repo & template方式
这里只介绍查询相关操作
1:检索流程
1.1:构建请求request和源source
// 声明查询请求对象
SearchRequest client = new SearchRequest();
// 指定使用的索引(数据库)
client.indices(index);
// 声明searchSourceBuilder源对象
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
// ======================== 源对象其他常见设置 ===========================
// 1. 排序和浅显分页
// from & size 默认都是-1,也就是有多少显示多少
// from-size浅分页适合数据量不大的情况(官网推荐是数据少于10000条),可以跳码进行查询
searchSourceBuilder.from(0);
searchSourceBuilder.size(9999);
// 2. 排序, date、float 等类型添加排序, text类型的字段不允许排序
searchSourceBuilder
.sort("age", SortOrder.DESC)
.sort("name", SortOrder.ASC);
1.2:查询建造器queryBuilder构建
// =============== 以boolean为例 ==================
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery()
.must(QueryBuilders.termQuery("name.keyword", "王五"))
.mustNot(QueryBuilders.idsQuery().addIds("1", "2", "3"))
// 可以进行bool嵌套
.must(QueryBuilders.boolQuery()
.must(QueryBuilders.matchQuery("name", "张三"))
)
.boost(2.0f);
// 加入对应的builder到searchSourceBuilder中
searchSourceBuilder.query(boolQueryBuilder);
// ============ 如果有高亮设置 ===============
HighlightBuilder highlightBuilder = new HighlightBuilder();
highlightBuilder.preTags("<font style='color: red'>");
highlightBuilder.postTags("</font>");
highlightBuilder.field("name").field("age");
//同一字段中存在多个高亮值 设置都高亮
highlightBuilder.requireFieldMatch(true);
// 高亮属性中加入高亮配置器
searchSourceBuilder.highlighter(highlightBuilder);
1.3:请求建造器加入到请求源,请求源加入到请求中
searchSourceBuilder.query(xxxQueryBuilder);
// 封装request,指定要request的索引index,指定source -> searchSourceBuilder
SearchRequest request = new SearchRequest(index).source(searchSourceBuilder);
// 通过client.search获取响应
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
1.4:结果hits和highlight分析
List<Map<String, Object>> list = new ArrayList<>();
SearchResponse searchResponse = this.client.search(client, RequestOptions.DEFAULT);
if (searchResponse == null) {
log.warn("没有返回值");
return;
}
// todo: 然后从这个searchResponse解析各种的值 -> 根据返回结构
int failedShards = searchResponse.getFailedShards();
System.out.println("失败的分片数:" + failedShards);
int successfulShards = searchResponse.getSuccessfulShards();
System.out.println("成功的分片数:" + successfulShards);
RestStatus status = searchResponse.status();
System.out.println("状态:" + status);
//解析高亮数据
SearchHits hits = searchResponse.getHits();
System.out.println(hits.getMaxScore());
for (SearchHit hit : hits) {
System.out.println("fields: " + hit.getFields());
System.out.println("index is:" + hit.getIndex());
System.out.println("document field is: " + hit.getDocumentFields());
System.out.println("metadata field is: " + hit.getMetadataFields());
System.out.println("score is: " + hit.getScore());
System.out.println("-----------------");
//原始数据,不包含高亮的数据
Map<String, Object> sourceMap = hit.getSourceAsMap();
//高亮数据,拿到高亮字段name
Map<String, HighlightField> highlightFields = hit.getHighlightFields();
HighlightField highlightTitle = highlightFields.get("name");
//将原始数据的name替换
if (highlightTitle != null) {
Text[] fragments = highlightTitle.getFragments();
if (fragments != null && fragments.length > 0) {
sourceMap.replace("name", fragments[0].toString());
}
}
list.add(sourceMap);//循环将数据添加入列表
}
list.forEach(System.out::println);
package com.cui.es_demo.client;
import lombok.extern.slf4j.Slf4j;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.text.Text;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.rest.RestStatus;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* 客户端相关操作
*
* @author cui haida
* 2025/1/30
*/
@Service
@Slf4j
public class ClientDemo {
//
private final RestHighLevelClient client;
public ClientDemo(RestHighLevelClient restHighLevelClient) {
this.client = restHighLevelClient;
}
/**
* 主要介绍客户端查询操作
*/
public void searchTest(String[] args) {
// 声明查询请求,同时声明作用的索引(数据库)
SearchRequest request = new SearchRequest();
request.indices("person");
// 声明searchSourceBuilder源对象
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
// ======================== 源对象其他常见设置 ===========================
// 1. 排序和浅显分页
// from & size 默认都是-1,也就是有多少显示多少
// from-size浅分页适合数据量不大的情况(官网推荐是数据少于10000条),可以跳码进行查询
searchSourceBuilder.from(0);
searchSourceBuilder.size(9999);
// 2. 排序, date、float 等类型添加排序, text类型的字段不允许排序
searchSourceBuilder
.sort("age", SortOrder.DESC)
.sort("name", SortOrder.ASC);
// 构造查询建造器,根据不同的业务需求,进行不同的查询
// =============== 以boolean为例 ==================
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery()
.must(QueryBuilders.termQuery("name.keyword", "王五"))
.mustNot(QueryBuilders.idsQuery().addIds("1", "2", "3"))
// 可以进行bool嵌套
.must(QueryBuilders.boolQuery()
.must(QueryBuilders.matchQuery("name", "张三"))
)
.boost(2.0f);
// 请求构造器加入源中
searchSourceBuilder.query(boolQueryBuilder);
// ============ 如果有高亮设置 ===============
HighlightBuilder highlightBuilder = new HighlightBuilder();
highlightBuilder.preTags("<font style='color: red'>");
highlightBuilder.postTags("</font>");
highlightBuilder.field("name").field("age");
//同一字段中存在多个高亮值 设置都高亮
highlightBuilder.requireFieldMatch(true);
// 高亮属性中加入源中
searchSourceBuilder.highlighter(highlightBuilder);
// 进行查询
// 1. 设置查询源(source -> request)
request.source(searchSourceBuilder);
// 2. 通过search方法进行查询
List<Map<String, Object>> list = new ArrayList<>();
try {
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 结果处理
System.out.println("response.getHits().getTotalHits() = " + response.getHits().getTotalHits());
// 然后从这个searchResponse解析各种的值 -> 根据返回结构
int failedShards = response.getFailedShards();
System.out.println("失败的分片数:" + failedShards);
int successfulShards = response.getSuccessfulShards();
System.out.println("成功的分片数:" + successfulShards);
RestStatus status = response.status();
System.out.println("状态:" + status);
// 解析高亮数据
SearchHits hits = response.getHits();
System.out.println(hits.getMaxScore());
for (SearchHit hit : hits) {
System.out.println("fields: " + hit.getFields());
System.out.println("index is:" + hit.getIndex());
System.out.println("document field is: " + hit.getDocumentFields());
System.out.println("metadata field is: " + hit.getMetadataFields());
System.out.println("score is: " + hit.getScore());
System.out.println("-----------------");
// 原始数据,不包含高亮的数据
Map<String, Object> sourceMap = hit.getSourceAsMap();
// 高亮数据,拿到高亮字段name
Map<String, org.elasticsearch.search.fetch.subphase.highlight.HighlightField> highlightFields = hit.getHighlightFields();
HighlightField highlightTitle = highlightFields.get("name");
// 将原始数据的name替换
if (highlightTitle != null) {
Text[] fragments = highlightTitle.getFragments();
if (fragments != null && fragments.length > 0) {
sourceMap.replace("name", fragments[0].toString());
}
}
// 循环将数据添加入列表
list.add(sourceMap);
}
list.forEach(System.out::println);
} catch (Exception e) {
e.printStackTrace();
}
}
}
2:各种查询构造器说明
2.1:MatchQueryBuilder
MatchQueryBuilder matchQueryBuilder = QueryBuilders.matchQuery("name", "李四")
// 李和四必须都出现在name字段中才可以, operator可以是OR
.operator(Operator.AND)
// 匹配度必须是 >= 75%才行
.minimumShouldMatch("75%")
// 是否忽略数据类型转换异常
.lenient(true);
2.2:RegexQueryBuilder
// 张姓开头的doc
RegexpQueryBuilder regexpQueryBuilder =
QueryBuilders.regexpQuery("name", "张*").caseInsensitive(true);
2.3:IdsQueryBuilder
String[] ids = new String[]{"1", "2", "3"};
IdsQueryBuilder idsQueryBuilder =
QueryBuilders.idsQuery().addIds(ids);
2.4:MatchPhraseQueryBuilder
MatchPhraseQueryBuilder match =
QueryBuilders.matchPhraseQuery("name", "李四").slop(2);
2.5:MatchPhrasePrefixQueryBuilder
MatchPhrasePrefixQueryBuilder matchPhrasePrefixQueryBuilder =
QueryBuilders.matchPhrasePrefixQuery("name", "张").maxExpansions(1);
2.6:MultiMatchQueryBuilder
String[] fieldNames = new String[]{"name", "age"};
String searchText = "老坛";
MultiMatchQueryBuilder multiMatchQueryBuilder = QueryBuilders.multiMatchQuery(searchText, fieldNames)
// 最多数量匹配
.type(MultiMatchQueryBuilder.Type.MOST_FIELDS)
// 使用and操作符和minimum_should_match参数来减少相关度低的文档数量
.operator(Operator.AND);
2.7:TermQueryBuilder
TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery("name.keyword", "李四");
TermsQueryBuilder termsQueryBuilder = QueryBuilders.termsQuery("name.keyword", "李四光", "李四");
2.8:FuzzyQueryBuilder
FuzzyQueryBuilder fuzzyQueryBuilder = QueryBuilders.fuzzyQuery("name", "李四");
2.9:RangeQueryBuilder
RangeQueryBuilder ageRangeFilter = QueryBuilders.rangeQuery("age")
// greater than 12
.gt(12)
// less than 17
.lt(17);
// 默认是true包含头尾,设置false去掉头尾
RangeQueryBuilder ageRangeFilter2 = QueryBuilders.rangeQuery("age")
.from(12)
.to(17)
// 不包含最后一个元素
.includeLower(false)
// 包含第一个元素
.includeUpper(true);
2.10:WildcardQueryBuilder
// wildcard 通配符查询, 支持*,匹配任何字符序列, 包括空,避免*
String queryString = "Lc*dd";
WildcardQueryBuilder wildcardQueryBuilder = QueryBuilders.wildcardQuery("name", queryString);
2.11:BoolQueryBuilder
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery()
.must(QueryBuilders.termQuery("name.keyword", "王五"))
.mustNot(QueryBuilders.idsQuery().addIds("1", "2", "3"))
// 可以进行bool嵌套
.must(QueryBuilders.boolQuery()
.must(QueryBuilders.matchQuery("name", "张三"))
)
.boost(2.0f);
searchSourceBuilder.query(boolQueryBuilder);
2.12:其他的QueryBuilder
// 16: 其他查询
// moreLikeThisQuery: 实现基于内容推荐, 支持实现一句话相似文章查询
// percent_terms_to_match:匹配项(term)的百分比,默认是0.3
// min_term_freq:一篇文档中一个词语至少出现次数,小于这个值的词将被忽略,默认是2
// max_query_terms:一条查询语句中允许最多查询词语的个数,默认是25
// stop_words:设置停止词,匹配时会忽略停止词
// min_doc_freq:一个词语最少在多少篇文档中出现,小于这个值的词会将被忽略,默认是无限制
// max_doc_freq:一个词语最多在多少篇文档中出现,大于这个值的词会将被忽略,默认是无限制
// min_word_len:最小的词语长度,默认是0
// max_word_len:最多的词语长度,默认无限制
// boost_terms:设置词语权重,默认是1
// boost:设置查询权重,默认是1
// analyzer:设置使用的分词器,默认是使用该字段指定的分词器
QueryBuilder queryBuilder = QueryBuilders.moreLikeThisQuery(new String[]{"王"})
// 一篇文档中一个词语至少出现次数,小于这个值的词将被忽略,默认是2
.minTermFreq(1)
// 一条查询语句中允许最多查询词语的个数,默认是25
.maxQueryTerms(3);
// 查询条件
searchSourceBuilder.query(matchQueryBuilder)
// 后置过滤器,id, exists, term, range
.postFilter(QueryBuilders.existsQuery("tag"));
3:聚合建造器说明
3.1:构造聚合条件
TermsAggregationBuilder aggregation =
AggregationBuilders.terms(等于的值).field(字段名称) // 进行分桶操作
.subAggregation(AggregationBuilders.avg(桶的名字,随便起的).field(根据xx字段分桶)) // 对每一个分桶,进行子聚合
3.2:将聚合条件交给builder,builder交给源,封装request
// 声明源
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
// 将查询建造器放入源,将高亮信息放入源,将聚合信息放入源
searchSourceBuilder.query(boolQueryBuilder);
searchSourceBuilder.highlighter(highlightBuilder);
searchSourceBuilder.aggregation(aggregation);
// 将源放入请求request
SearchRequest request = new SearchRequest(index).source(searchSourceBuilder);
// 进行查询
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
3.3:结果解析和处理
// 结果解析
List<Object> ans = handleResult(response);
// 结果处理
for (Object o : ans) {
System.out.println(o.toString());
}
// todo: 结果解析封装
/**
* 聚合查询
*/
public void aggTest() {
// 声明查询请求,同时声明作用的索引(数据库)
SearchRequest request = new SearchRequest();
request.indices("person");
// 声明searchSourceBuilder源对象
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
// ======================== 源对象其他常见设置 ===========================
// 1. 排序和浅显分页
// from & size 默认都是-1,也就是有多少显示多少
// from-size浅分页适合数据量不大的情况(官网推荐是数据少于10000条),可以跳码进行查询
searchSourceBuilder.from(0);
searchSourceBuilder.size(9999);
// 2. 排序, date、float 等类型添加排序, text类型的字段不允许排序
searchSourceBuilder
.sort("age", SortOrder.DESC)
.sort("name", SortOrder.ASC);
// 构造查询建造器,根据不同的业务需求,进行不同的查询
// =============== 以boolean为例 ==================
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery()
.must(QueryBuilders.termQuery("name.keyword", "王五"))
.mustNot(QueryBuilders.idsQuery().addIds("1", "2", "3"))
// 可以进行bool嵌套
.must(QueryBuilders.boolQuery()
.must(QueryBuilders.matchQuery("name", "张三"))
)
.boost(2.0f);
// 请求构造器加入源中
searchSourceBuilder.query(boolQueryBuilder);
// ============ 如果有高亮设置 ===============
HighlightBuilder highlightBuilder = new HighlightBuilder();
highlightBuilder.preTags("<font style='color: red'>");
highlightBuilder.postTags("</font>");
highlightBuilder.field("name").field("age");
//同一字段中存在多个高亮值 设置都高亮
highlightBuilder.requireFieldMatch(true);
// 高亮属性中加入源中
searchSourceBuilder.highlighter(highlightBuilder);
TermsAggregationBuilder aggregation =
// 进行分桶操作
AggregationBuilders.terms("Jone").field("name")
// 对每一个分桶,进行子聚合
.subAggregation(AggregationBuilders.avg("age_avg").field("age"));
// 将聚合结果放入源中
searchSourceBuilder.aggregation(aggregation);
// 进行查询
// 1. 设置查询源(source -> request)
request.source(searchSourceBuilder);
// 2. 通过search方法进行查询
List<Map<String, Object>> list = new ArrayList<>();
try {
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 结果处理
System.out.println("response.getHits().getTotalHits() = " + response.getHits().getTotalHits());
// 然后从这个searchResponse解析各种的值 -> 根据返回结构
int failedShards = response.getFailedShards();
System.out.println("失败的分片数:" + failedShards);
int successfulShards = response.getSuccessfulShards();
System.out.println("成功的分片数:" + successfulShards);
RestStatus status = response.status();
System.out.println("状态:" + status);
// 解析高亮数据
SearchHits hits = response.getHits();
System.out.println(hits.getMaxScore());
for (SearchHit hit : hits) {
System.out.println("fields: " + hit.getFields());
System.out.println("index is:" + hit.getIndex());
System.out.println("document field is: " + hit.getDocumentFields());
System.out.println("metadata field is: " + hit.getMetadataFields());
System.out.println("score is: " + hit.getScore());
System.out.println("-----------------");
// 原始数据,不包含高亮的数据
Map<String, Object> sourceMap = hit.getSourceAsMap();
// 高亮数据,拿到高亮字段name
Map<String, org.elasticsearch.search.fetch.subphase.highlight.HighlightField> highlightFields = hit.getHighlightFields();
HighlightField highlightTitle = highlightFields.get("name");
// 将原始数据的name替换
if (highlightTitle != null) {
Text[] fragments = highlightTitle.getFragments();
if (fragments != null && fragments.length > 0) {
sourceMap.replace("name", fragments[0].toString());
}
}
// 循环将数据添加入列表
list.add(sourceMap);
}
list.forEach(System.out::println);
} catch (Exception e) {
e.printStackTrace();
}
}
三:repo & template
1:ElasticsearchRepository
1.1:repo介绍
ElasticsearchRepository接口封装了Document的CRUD操作,我们直接定义接口继承它即可。
ElasticsearchRepository接口的源码
package org.springframework.data.elasticsearch.repository;
import java.io.Serializable;
import org.elasticsearch.index.query.QueryBuilder;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.core.query.SearchQuery;
import org.springframework.data.repository.NoRepositoryBean;
@NoRepositoryBean
public interface ElasticsearchRepository<T, ID extends Serializable> extends ElasticsearchCrudRepository<T, ID> {
<S extends T> S index(S entity);
Iterable<T> search(QueryBuilder query);
Page<T> search(QueryBuilder query, Pageable pageable);
Page<T> search(SearchQuery searchQuery);
Page<T> searchSimilar(T entity, String[] fields, Pageable pageable);
void refresh();
Class<T> getEntityClass();
}
CrudRepository源码
package org.springframework.data.repository;
import java.util.Optional;
/**
* Interface for generic CRUD operations on a repository for a specific type.
*
* @author Oliver Gierke
* @author Eberhard Wolff
*/
@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();
// delete相关
void deleteById(ID id);
void delete(T entity);
void deleteAll(Iterable<? extends T> entities);
void deleteAll();
}
PagingAndSortingRepository源码
@NoRepositoryBean
public interface PagingAndSortingRepository<T, ID> extends Repository<T, ID> {
Iterable<T> findAll(Sort sort);
Page<T> findAll(Pageable pageable);
}
可见ElasticsearchRepository为我们封装好了很多常用的方法,我们可以在使用的时候直接调用这些方法进行操作
1.2:自定义方法命名规范
和所有的Spring Data一样,都支持指定格式的函数声明,对于这些函数,不用写具体的实现,而是可以直接调用
SpringData会通过动态代理的方式,帮我们生成基础的CRUD方法
自定义方法命名规范
findBy[fieldName]
:根据指定的单个条件进行等值查询;findBy[fieldName]And[fieldName]And[...]
:根据指定的多条件进行and查询;findBy[fieldName]Or[fieldName]Or[...]
:根据指定的多条件进行or查询;findBy[fieldName]Equals
:根据指定的单个条件进行等值查询;findBy[fieldName]In
:对指定的单个字段进行in查询,入参为一个列表;findBy[fieldName]Like
:对指定的单个字段进行like模糊查询;findBy[fieldName]NotNull
:查询指定字段不为空的数据;findBy[fieldName]GreaterThan
:对指定的单个字段进行]范围查询;findBy[fieldName]GreaterThanEqual
:对指定的单个字段进行]=范围查询;findBy[fieldName]LessThan
:对指定的单个字段进行[范围查询;findBy[fieldName]LessThanEqual
:对指定的单个字段进行[=范围查询;Page[...] findBy[...]
:根据指定的条件进行分页查询;countBy[fieldName]
:根据指定的条件字段进行计数统计;findTop[n]By[fieldName]
:根据指定字段做等值查询,并返回前n条数据;findBy[fieldName]Between
:根据指定字段进行between范围查询;findDistinctBy[fieldName]
:根据指定的单个条件进行去重查询;findFirstBy[fieldName]
:根据指定的单个条件进行等值查询(只返回满足条件的第一个数据);findBy[fieldName1]OrderBy[fieldName2]
:根据第一个字段做等值查询,并根据第二个字段做排序;
各种方法名开头含义
- 以
get、find、read、query、stream
开头,代表是查询数据的方法; - 以
count
开头,代表是计数统计的方法; - 以
delete、remove
开头,代表是删除数据的方法; - 以
exists
开头,代表是判断是否存在的方法; - 以
search
开头,代表是全文搜索的方法; - 以
update
开头,代表是修改数据的方法;
方法名开头后面跟的关键字含义,以find开头的方法为例:
By
:表示当前方法生成的查询语句,会根据By后面的逻辑来组成;FirstBy
:表示当前方法生成的语句,只会返回符合条件的第一条数据;DistinctBy
:表示当前方法生成的语句,会对符合条件的数据去重;TopBy
:表示当前方法生成的语句,只会返回符合条件的前N条数据;[实体类名称]By
:表示当前方法生成的语句,只会返回一条数据;[实体类名称]sBy
:表示当前方法生成的语句,会返回多条数据;AllBy
:表示当前方法生成的语句,会返回多条或所有数据;DistinctFirstBy
:表示当前方法生成的语句,只会返回去重后的第一条数据;DistinctTopBy
:表示当前方法生成的语句,只会返回去重后的前N条数据;
方法名开头跟的关键字之后跟的是字段名[
fieldName
], 字段名称后面可以接的关键字如下
在这些关键字之后,都是跟具体的字段名(实体类的属性名),字段名称后面可以接的关键字如下(同样以find为例):
Or
:表示当前查询方法有多个条件,多个条件之间为“或者”关系;And
:表示当前查询方法有多个条件,多个条件之间为“并且”关系;OrderBy
:表示当前查询会涉及到排序,后面需要跟一个排序字段;Between
:表示当前方法为between范围查询;GreaterThan
:表示当前方法为>查询;GreaterThanEqual
:表示当前方法为>=查询;LessThan
:表示当前方法为 < 查询;LessThanEqual
:表示当前方法为<=查询;After
:和GreaterThan差不多,相当于查询指定数值之后的数据;Before
:和LessThan差不多,查询指定条件之前的数据;Containing
:查询某字段中包含指定字符的数据;Empty
:表示当前方法会查询指定字段为空的数据,与之含义类似的还有Null、Exists;Equals
:表示当前方法会根据指定字段做等值查询;Is
:和Equals差不多;In
:表示当前方法为in多值匹配查询;Like
:表示当前方法为like模糊查询;Not
:可以和上述大多数关键字组合,带有Not的则含义相反,如NotEmpty表示不为空;
2:ElasticsearchRestTemplate
2.1:template介绍
template提供了众多模板方法,只要我们编写好对应的条件,然后调用模板方法即可
2.2:template查询操作
下面是操作流程【查询为例】
2.2.1:入参特殊情况处理【包括异常入参】
2.2.2:构建查询条件Criteria
// 1:构建查询条件
Criteria criteria = new Criteria()
// 条件一:xxxx
.and(new Criteria("字段名称").contains(条件中的内容))
// 条件二:xxx
.and(new Criteria("字段名称").is(条件中的内容));
// 2:封装进入到CriteriaQuery
CriteriaQuery filter = new CriteriaQuery(criteria);
// 3:设置分页信息【可能没有,看业务】
CriteriaQuery criteriaQuery = filter.setPageable(PageRequest.of(pageIndex, pageSize));
2.2.3:构建高亮条件
// 1:声明高亮构造器对象
HighlightBuilder highlightBuilder = new HighlightBuilder();
// todo: 设置高亮领域
// todo: 前置后置标签
// 2:构建高亮query
HighlightQuery highlightQuery = new HighlightQuery(highlightBuilder);
// 3:将高亮query封装进criteriaQuery中
criteriaQuery.setHighlightQuery(highlightQuery);
2.2.4:查询(也可能是其他的方法,不一定是search)
elasticsearchRestTemplate.search(查询条件,返回信息的实体.class);
2.2.5:结果封装
// 对结果进行封装
2.3:创建索引和删除索引
对于索引的创建和删除,建议还是直接使用Kibana进行相关的操作,如果要使用template,可以输入对应的索引名称进行创建
template会到实体中找到@Document(indexName = "xxx")
对应的信息,根据这个注解的配置拿到settings对应的信息配置
然后根据实体类标注的字段类型和分析器类型创建对应的mappings对应的信息配置
package com.example.es_demo.pojo;
import com.alibaba.fastjson.annotation.JSONField;
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.time.LocalDateTime;
/**
* <p>
* 功能描述:实体类
* </p>
*
* @author cui haida
* @date 2024/01/28/8:20
*/
@NoArgsConstructor
@AllArgsConstructor
@Data
@Document(indexName = "mytest") // 指定对应的索引名称
public class MyTest {
@Id
@JSONField(serialize = false)
private String id;
@Field(type = FieldType.Keyword)
private String name;
@Field(type = FieldType.Keyword)
private String age;
// text类型,并使用IK最粗粒度的分词器,检索时的分词器采用的是最细粒度的IK分词器
@Field(type = FieldType.Text, analyzer = "ik_smart", searchAnalyzer = "ik_max_word")
private String address;
// 注意日期格式的特殊处理
@Field(type = FieldType.Date, format = DateFormat.custom, pattern = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime createTime;
}
public String create(String indexName) {
IndexOperations indexOperations =
elasticsearchRestTemplate.indexOps(IndexCoordinates.of(indexName));
if (indexOperations.exists()) {
return "索引已存在";
}
indexOperations.create();
return "索引创建成功";
}
public String delete(String indexName) {
IndexOperations indexOperations =
elasticsearchRestTemplate.indexOps(IndexCoordinates.of(indexName));
indexOperations.delete();
return "索引删除成功";
}
3:使用实例
package com.cui.es_demo.client;
import com.cui.es_demo.client.repo.PersonRepository;
import com.cui.es_demo.entity.PageResponse;
import com.cui.es_demo.entity.Person;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;
import org.springframework.data.elasticsearch.core.SearchHit;
import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.data.elasticsearch.core.query.Criteria;
import org.springframework.data.elasticsearch.core.query.CriteriaQuery;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
/**
* @author cui haida
* 2025/1/30
*/
@Service
@Slf4j
public class TemplateDemo {
// es repo -> 这里提供了Spring Data的基本方法,可以直接使用
@Autowired
PersonRepository personRepository;
// template
@Autowired
ElasticsearchRestTemplate elasticsearchRestTemplate;
// =========== repo测试 =============
// 就是直接调用对应的方法,只要语法满足上面说的Spring Data语法就可以
public void saveAll(List<Person> orders) {
personRepository.saveAll(orders);
}
public void deleteById(Integer id) {
personRepository.deleteById(id);
}
public void updateById(Person person) {
personRepository.save(person);
}
public Person findById(Integer id) {
return (Person) personRepository.findById(id).orElse(null);
}
public PageResponse<Person> findAll(Integer pageIndex, Integer pageSize) {
Page<Person> page = personRepository.findAll(PageRequest.of(pageIndex, pageSize));
PageResponse<Person> pageResponse = new PageResponse<Person>();
pageResponse.setTotal(page.getTotalElements());
pageResponse.setResult(page.getContent());
return pageResponse;
}
// =============== template测试 ==============
// 1:构造查询条件
// 2:调用template.xxx(条件)
// 3:结果处理(封装)
public PageResponse<Person> findList(Person person, Integer pageIndex, Integer pageSize) {
// 1:构建查询条件
Criteria criteria = new Criteria()
// 条件一:文档中的orderDesc字段要包含查询条件的orderDesc字段
.and(new Criteria("orderDesc").contains(person.getAddress()))
// 条件二:文档中的orderNo字段要等于查询条件的orderNo字段
.and(new Criteria("orderNo").is(person.getId()));
// 封装进入到CriteriaQuery
CriteriaQuery filter = new CriteriaQuery(criteria);
// 设置分页信息
CriteriaQuery criteriaQuery = filter.setPageable(PageRequest.of(pageIndex, pageSize));
// 2:查询 -> elasticsearchRestTemplate.search(查询条件,返回信息的实体.class)
SearchHits<Person> searchHits = elasticsearchRestTemplate.search(criteriaQuery, Person.class);
// 3:结果封装
List<Person> result = searchHits.get().map(SearchHit::getContent).collect(Collectors.toList());
PageResponse<Person> pageResponse = new PageResponse<>();
pageResponse.setTotal(searchHits.getTotalHits());
pageResponse.setResult(result);
return pageResponse;
}
public PageResponse<Person> findHighlight(Person person, Integer pageIndex, Integer pageSize) {
// 0:特殊情况的处理
if (person == null) {
PageResponse<Person> pageResponse = new PageResponse<Person>();
pageResponse.setTotal(0L);
pageResponse.setResult(new ArrayList<>());
return pageResponse;
}
// 1:构建查询条件
Criteria criteria = new Criteria()
// 条件一:文档中的orderDesc字段要包含查询条件的orderDesc字段
.and(new Criteria("id").contains(person.getId()))
// 条件二:文档中的orderNo字段要等于查询条件的orderNo字段
.and(new Criteria("address").is(person.getAddress()));
// 封装进入到CriteriaQuery
CriteriaQuery filter = new CriteriaQuery(criteria);
// 设置分页信息
CriteriaQuery criteriaQuery = filter.setPageable(PageRequest.of(pageIndex, pageSize));
// 3:查询 -> elasticsearchRestTemplate.search(查询条件,返回信息的实体.class)
SearchHits<Person> searchHits = elasticsearchRestTemplate.search(criteriaQuery, Person.class);
// 4:结果封装
// 对每一个都设置对应的高亮字段信息
List<Person> result = searchHits.get().map(SearchHit::getContent).collect(Collectors.toList());
// 封装页码信息
PageResponse<Person> pageResponse = new PageResponse<>();
pageResponse.setTotal(searchHits.getTotalHits());
pageResponse.setResult(result);
return pageResponse;
}
}