1.前言
本文主要讲解,springBoot 使用spring-boot-starter-data-elasticsearch方式整合Elasticsearch
文末附源码
什么是Spring Data ElasticSearch
Spring Data ElasticSearch 基于 spring data API 简化 elasticSearch操作,将原始操作elasticSearch的客户端API进行封装 。Spring Data为Elasticsearch项目提供集成搜索引擎。Spring Data Elasticsearch POJO的关键功能区域为中心的模型与Elastichsearch交互文档和轻松地编写一个存储库数据访问层。
这里需要注意一下版本
官方版本对照:传送门
优点:
- 封装了很多通用方法以及注解式操作简化了开发流程。
- 与springBoot 集成更快速。
缺点:
- elasticsearch官方更新的版本速度太快,而springboot速度明显略慢。
- elasticsearch随着版本升级API略有变化,spring-data-elasticsearch容易遇到版本不兼容问题。
2.入门使用实例
开发环境:
- springboot版本:2.3.0.RELEASE
- elasticSearch版本: 7.2.0
- spring data elasticsearch :4.0.0
官网文档4.0版本client使用介绍:传送门
2.1.pom依赖
创建springBoot项目,添加整合elasticsearch依赖
<!--spring boot 整合 elasticsearch -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
2.2.配置application.properties
server.port=8091spring.elasticsearch.rest.uris=http://47.114.56.113:9200
spring.elasticsearch.rest.username=elastic
spring.elasticsearch.rest.password=1234567
2.3.测试实体类
创建一个员工的实体类
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
/*** 员工实体类** @author 程序员小强*/
@Document(indexName = "employee_info", shards = 2, replicas = 2)
public class EmployeeInfo {
@Idprivate Long id;/*** 工号*/@Field(name = "job_no")private String jobNo;/*** 姓名*/@Field(name = "name")private String name;/*** 英文名*/@Field(name = "english_name")private String englishName;/*** 工作岗位*/private String job;/*** 性别*/private Integer sex;/*** 年龄*/private Integer age;/*** 薪资*/private BigDecimal salary;/*** 入职时间*/@Field(name = "job_day",format= DateFormat.date_time)private Date jobDay;/*** 备注*/private String remark;//省略get set
}
注:注解说明
Document注解
@Persistent
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({
ElementType.TYPE})
public @interface Document {
String indexName();//索引库的名称,个人建议以项目的名称命名@DeprecatedString type() default "";//类型,7.x之后以废弃short shards() default 1;//默认分区数short replicas() default 1;//每个分区默认的备份数String refreshInterval() default "1s";//刷新间隔String indexStoreType() default "fs";//索引文件存储类型boolean createIndex() default true; //是否创建索引VersionType versionType() default VersionType.EXTERNAL; //版本
}
@Field
注解
可以通过@Field
注解来进行详细的指定,如果无特殊需求,那么只需要添加@Document即可。
常见@Field,用于定义别名,或者针对日期属性进行格式化设定。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Documented
@Inherited
public @interface Field {
@AliasFor("name")String value() default ""; //属性别名@AliasFor("value")String name() default ""; //属性别名FieldType type() default FieldType.Auto; //属性类型,默认自动根据参数类型自动属性的类型boolean index() default true; //默认情况下分词DateFormat format() default DateFormat.none; //时间格式化String pattern() default "";boolean store() default false; //默认情况下不存储原文String searchAnalyzer() default ""; //指定字段搜索时使用的分词器String indexAnalyzer() default "";//指定字段建立索引时指定的分词器String[] ignoreFields() default {
};//忽略某些字段//以下是一些不常用的设置boolean includeInParent() default false;String[] copyTo() default {
};int ignoreAbove() default -1;boolean coerce() default true;boolean docValues() default true;boolean ignoreMalformed() default false;IndexOptions indexOptions() default IndexOptions.none;boolean indexPhrases() default false;IndexPrefixes[] indexPrefixes() default {
};boolean norms() default true;String nullValue() default "";int positionIncrementGap() default -1;Similarity similarity() default Similarity.Default;TermVector termVector() default TermVector.none;double scalingFactor() default 1;int maxShingleSize() default -1;
}
2.4创建xxRepository
import com.example.elasticsearch.bean.EmployeeInfo;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;/*** @author 程序员小强*/
//泛型的参数分别是实体类型和主键类型
public interface EmployeeInfoRepository extends ElasticsearchRepository<EmployeeInfo, Long> {
}
2.5创建Service接口
/*** @author 程序员小强*/
public interface EmployeeInfoService {
void deleteIndex(String index);void save(EmployeeInfo docBean);void saveAll(List<EmployeeInfo> list);Iterator<EmployeeInfo> findAll();
}
/*** @author 程序员小强*/
@Service
public class EmployeeInfoServiceImpl implements EmployeeInfoService {
@Resourceprivate EmployeeInfoRepository elasticRepository;@Resourceprivate ElasticsearchRestTemplate elasticsearchTemplate;@Overridepublic void deleteIndex(String index) {
elasticsearchTemplate.deleteIndex(index);}@Overridepublic void save(EmployeeInfo docBean) {
elasticRepository.save(docBean);}@Overridepublic void saveAll(List<EmployeeInfo> list) {
elasticRepository.saveAll(list);}@Overridepublic Iterator<EmployeeInfo> findAll() {
return elasticRepository.findAll().iterator();}
}
2.6创建测试controller
@RestController
@RequestMapping("/employeeInfo")
public class EmployeeElasticController {
@Autowiredprivate EmployeeInfoService employeeInfoService;@RequestMapping("/batchSave")public String init() throws Exception {
List<EmployeeInfo> list = new ArrayList<>();SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");list.add(new EmployeeInfo(1001L, "2001", "张三", "zhangsan", "Java", 1, 19, new BigDecimal("12500.01"), simpleDateFormat.parse("2019-09-10"), "备注"));list.add(new EmployeeInfo(1002L, "2002", "李四", "lisi", "PHP", 1, 18, new BigDecimal("11600.01"), simpleDateFormat.parse("2019-09-10"), "备注"));list.add(new EmployeeInfo(1003L, "2003", "王五", "wangwu", "C++", 1, 20, new BigDecimal("9900.01"), simpleDateFormat.parse("2019-09-10"), "备注"));list.add(new EmployeeInfo(1004L, "2004", "赵六", "zhaoliu", "Java Leader", 1, 20, new BigDecimal("20000.01"), simpleDateFormat.parse("2019-09-10"), "备注"));list.add(new EmployeeInfo(1005L, "2005", "小五", "xiaowu", "H5", 1, 17, new BigDecimal("10600.01"), simpleDateFormat.parse("2019-09-10"), "备注"));list.add(new EmployeeInfo(1006L, "2006", "小六", "xaioliu", "web", 1, 20, new BigDecimal("12600.01"), simpleDateFormat.parse("2019-09-10"), "备注"));list.add(new EmployeeInfo(1007L, "2007", "小七", "xiaoqi", "app", 1, 22, new BigDecimal("20000.01"), simpleDateFormat.parse("2019-09-10"), "备注"));list.add(new EmployeeInfo(1008L, "2008", "小八", "xaioba", "Java", 1, 21, new BigDecimal("11000.01"), simpleDateFormat.parse("2019-09-10"), "备注"));list.add(new EmployeeInfo(1009L, "2009", "小九", "xiaojiu", "Java", 1, 20, new BigDecimal("14000.01"), simpleDateFormat.parse("2019-09-10"), "备注"));list.add(new EmployeeInfo(1010L, "2010", "大十", "dashi", "Java", 1, 20, new BigDecimal("13000.01"), simpleDateFormat.parse("2019-09-10"), "备注"));employeeInfoService.batchSaveOrUpdate(list);return "success -> " + list.size();}@GetMapping("/listAll")public Iterator<EmployeeInfo> all() {
return employeeInfoService.findAll();}}
3.更多查询
简介
spring data elsaticsearch提供了三种构建查询模块的方式:
- 基本的增删改查:继承spring data提供的接口就默认提供
- 接口中声明方法:**无需实现类。**spring data根据方法名,自动生成实现类,方法名必须符合一定的规则
接口只要继承 ElasticsearchRepository 类即可。默认会提供很多实现,比如 CRUD 和搜索相关的实现。类似于 JPA 读取数据。
支持的默认方法有:
count(), findAll(), findOne(ID), delete(ID), deleteAll(), exists(ID), save(DomainObject), save(Iterable)。
接口的命名是遵循规范的。常用命名规则如下:
表格内容摘自官网(官方文档:传送门)
关键字 | 方法命名 | Elasticsearch 查询DSL语法示例 |
---|---|---|
And | findByNameAndPrice | { “query” : { “bool” : { “must” : [ { “query_string” : { “query” : “?”, “fields” : [ “name” ] } }, { “query_string” : { “query” : “?”, “fields” : [ “price” ] } } ] } }} |
Or | findByNameOrPrice | { “query” : { “bool” : { “should” : [ { “query_string” : { “query” : “?”, “fields” : [ “name” ] } }, { “query_string” : { “query” : “?”, “fields” : [ “price” ] } } ] } }} |
Is | findByName | { “query” : { “bool” : { “must” : [ { “query_string” : { “query” : “?”, “fields” : [ “name” ] } } ] } }} |
Not | findByNameNot | { “query” : { “bool” : { “must_not” : [ { “query_string” : { “query” : “?”, “fields” : [ “name” ] } } ] } }} |
Between | findByPriceBetween | { “query” : { “bool” : { “must” : [ {“range” : {“price” : {“from” : ?, “to” : ?, “include_lower” : true, “include_upper” : true } } } ] } }} |
LessThan | findByPriceLessThan | { “query” : { “bool” : { “must” : [ {“range” : {“price” : {“from” : null, “to” : ?, “include_lower” : true, “include_upper” : false } } } ] } }} |
LessThanEqual | findByPriceLessThanEqual | { “query” : { “bool” : { “must” : [ {“range” : {“price” : {“from” : null, “to” : ?, “include_lower” : true, “include_upper” : true } } } ] } }} |
GreaterThan | findByPriceGreaterThan | { “query” : { “bool” : { “must” : [ {“range” : {“price” : {“from” : ?, “to” : null, “include_lower” : false, “include_upper” : true } } } ] } }} |
GreaterThanEqual | findByPriceGreaterThan | { “query” : { “bool” : { “must” : [ {“range” : {“price” : {“from” : ?, “to” : null, “include_lower” : true, “include_upper” : true } } } ] } }} |
Before | findByPriceBefore | { “query” : { “bool” : { “must” : [ {“range” : {“price” : {“from” : null, “to” : ?, “include_lower” : true, “include_upper” : true } } } ] } }} |
After | findByPriceAfter | { “query” : { “bool” : { “must” : [ {“range” : {“price” : {“from” : ?, “to” : null, “include_lower” : true, “include_upper” : true } } } ] } }} |
Like | findByNameLike | { “query” : { “bool” : { “must” : [ { “query_string” : { “query” : “?*”, “fields” : [ “name” ] }, “analyze_wildcard”: true } ] } }} |
StartingWith | findByNameStartingWith | { “query” : { “bool” : { “must” : [ { “query_string” : { “query” : “?*”, “fields” : [ “name” ] }, “analyze_wildcard”: true } ] } }} |
EndingWith | findByNameEndingWith | { “query” : { “bool” : { “must” : [ { “query_string” : { “query” : “*?”, “fields” : [ “name” ] }, “analyze_wildcard”: true } ] } }} |
Contains/Containing | findByNameContaining | { “query” : { “bool” : { “must” : [ { “query_string” : { “query” : “?”, “fields” : [ “name” ] }, “analyze_wildcard”: true } ] } }} |
In (when annotated as FieldType.Keyword) | findByNameIn(Collectionnames) | { “query” : { “bool” : { “must” : [ {“bool” : {“must” : [ {“terms” : {“name” : ["?","?"]}} ] } } ] } }} |
In | findByNameIn(Collectionnames) | { “query”: {“bool”: {“must”: [{“query_string”:{“query”: “”?" “?”", “fields”: [“name”]}}]}}} |
NotIn (when annotated as FieldType.Keyword) | findByNameNotIn(Collectionnames) | { “query” : { “bool” : { “must” : [ {“bool” : {“must_not” : [ {“terms” : {“name” : ["?","?"]}} ] } } ] } }} |
NotIn | findByNameNotIn(Collectionnames) | {“query”: {“bool”: {“must”: [{“query_string”: {“query”: “NOT(”?" “?”)", “fields”: [“name”]}}]}}} |
Near | findByStoreNear | Not Supported Yet ! |
True | findByAvailableTrue | { “query” : { “bool” : { “must” : [ { “query_string” : { “query” : “true”, “fields” : [ “available” ] } } ] } }} |
False | findByAvailableFalse | { “query” : { “bool” : { “must” : [ { “query_string” : { “query” : “false”, “fields” : [ “available” ] } } ] } }} |
OrderBy | findByAvailableTrueOrderByNameDesc | { “query” : { “bool” : { “must” : [ { “query_string” : { “query” : “true”, “fields” : [ “available” ] } } ] } }, “sort”:[{“name”:{“order”:“desc”}}] } |
按照接口的命名方法示例
/*** @author 程序员小强*/
public interface EmployeeInfoRepository extends ElasticsearchRepository<EmployeeInfo, Long> {
/*** 精确查找* 方法名规则:finByxxx** @param name* @return 员工数据集*/List<EmployeeInfo> findByName(String name);/*** AND 语句查询** @param name* @param age* @return 员工数据集*/List<EmployeeInfo> findByNameAndAge(String name, Integer age);/*** OR 语句查询** @param name* @param age* @return 员工数据集*/List<EmployeeInfo> findByNameOrAge(String name, Integer age);/*** 分页查询员工信息** @param name* @param page* @return 员工数据集* 注:等同于下面代码 @Query("{\"bool\" : {\"must\" : {\"term\" : {\"name\" : \"?0\"}}}}")*/Page<EmployeeInfo> findByName(String name, Pageable page);/*** NOT 语句查询** @param name* @param page* @return 员工数据集*/Page<EmployeeInfo> findByNameNot(String name, Pageable page);/*** LIKE 语句查询** @param name* @param page* @return 员工数据集*/Page<EmployeeInfo> findByNameLike(String name, Pageable page);}
4.高级查询
Data ElasticSearch 支持了一些常见的查询
但是一些高级查询呢?可以使用类组装DSL语法支持
/*** 聚合查询-groupBy* 聚合所有的年龄*/@Testpublic void groupByAge() {
//1.构建查询对象NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms("groupByAge").field("age").size(30));SearchHits<EmployeeInfo> search = elasticsearchRestTemplate.search(nativeSearchQueryBuilder.build(), EmployeeInfo.class);Aggregations aggregations = search.getAggregations();//解析聚合分组后结果数据ParsedLongTerms parsedLongTerms = aggregations.get("groupByAge");//groupBy后的年龄集List<String> ageList = parsedLongTerms.getBuckets().stream().map(Terms.Bucket::getKeyAsString).collect(Collectors.toList());System.out.println(ageList);}
/*** 分页查询* 带参数*/
@Test
public void listPageMatch() {
int pageNo = 1;int pageSize = 5;NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();nativeSearchQueryBuilder.withQuery(QueryBuilders.matchQuery("name", "小"));//注:Pageable类中 pageNum需要减1,如果是第一页 数值为0Pageable pageable = PageRequest.of(pageNo - 1, pageSize);nativeSearchQueryBuilder.withPageable(pageable);SearchHits<EmployeeInfo> searchHitsResult = elasticsearchRestTemplate.search(nativeSearchQueryBuilder.build(), EmployeeInfo.class);//7.获取分页数据SearchPage<EmployeeInfo> searchPageResult = SearchHitSupport.searchPageFor(searchHitsResult, pageable);System.out.println("分页查询");System.out.println(String.format("totalPages:%d, pageNo:%d, size:%d", searchPageResult.getTotalPages(), pageNo, pageSize));System.out.println(JSON.toJSONString(searchPageResult.getSearchHits(), SerializerFeature.PrettyFormat));
}
5.源码地址
传送门
关注程序员小强公众号更多编程趣事,知识心得与您分享
?关注“程序员小强”发送关键字“elasticSearch”到公众号获取相关篇