1. 简介
1.1.什么是SpringDataElasticsearch
SpringDataElasticsearch(以后简称SDE)是Spring Data项目下的一个子模块。
查看 Spring Data的官网:http://projects.spring.io/spring-data/
Spring Data 的使命是给各种数据访问提供统一的编程接口,不管是关系型数据库(如MySQL),还是非关系数据库(如Redis),或者类似Elasticsearch这样的索引数据库。从而简化开发人员的代码,提高开发效率。
包含很多不同数据操作的模块:
Spring Data Elasticsearch的页面:https://projects.spring.io/spring-data-elasticsearch/
特征:
- 支持Spring的基于
@Configuration
的java配置方式,或者XML配置方式 - 提供了用于操作ES的便捷工具类**
ElasticsearchTemplate
**。包括实现文档到POJO之间的自动智能映射。 - 利用Spring的数据转换服务实现的功能丰富的对象映射
- 基于注解的元数据映射方式,而且可扩展以支持更多不同的数据格式
- 根据持久层接口自动生成对应实现方法,无需人工编写基本操作代码(类似mybatis,根据接口自动得到实现)。当然,也支持人工定制查询
2.配置SpringDataElasticsearch
我们在pom文件中,引入SpringDataElasticsearch的启动器:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
然后,只需要在resources下新建application.yml文件,引入elasticsearch的host和port即可:
spring:data:elasticsearch:cluster-name: elasticcluster-nodes: 192.168.150.101:9300,192.168.150.101:9301,192.168.150.101:9302
需要注意的是,SpringDataElasticsearch底层使用的不是Elasticsearch提供的RestHighLevelClient,而是TransportClient,并不采用Http协议通信,而是访问elasticsearch对外开放的tcp端口,我们之前集群配置中,设置的分别是:9301,9302,9300
另外,SpringBoot已经帮我们配置好了各种SDE配置,并且注册了一个ElasticsearchTemplate供我们使用。接下来一起来试试吧。
2.1.索引库操作
2.1.1.创建索引库
我们先创建一个测试类,然后注入ElasticsearchTemplate:
/*** @author 虎哥*/
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringElasticsearchTest {@Autowiredprivate ElasticsearchTemplate esTemplate;}
然后准备一个新的实体类,作为下面与索引库对应的文档:
package cn.itcast.es.pojo;public class Goods {private Long id;private String title; //标题private String category;// 分类private String brand; // 品牌private Double price; // 价格private String images; // 图片地址public Goods() {}public Goods(Long id, String title, String category, String brand, Double price, String images) {this.id = id;this.title = title;this.category = category;this.brand = brand;this.price = price;this.images = images;}// getter和setter略
}
下面是创建索引库的API示例:
@Test
public void testCreateIndex(){// 创建索引库,并制定实体类的字节码esTemplate.createIndex(Goods.class);
}
发现没有,创建索引库需要指定的信息,比如:索引库名、类型名、分片、副本数量、还有映射信息都没有填写,这是怎么回事呢?
实际上,与我们自定义工具类类似,SDE也是通过实体类上的注解来配置索引库信息的,我们需要在Goods上添加下面的一些注解:
@Document(indexName = "goods", type = "docs", shards = 3, replicas = 1)
public class Goods {@Idprivate Long id;@Field(type = FieldType.Text, analyzer = "ik_max_word")private String title; //标题@Field(type = FieldType.Keyword)private String category;// 分类@Field(type = FieldType.Keyword)private String brand; // 品牌@Field(type = FieldType.Double)private Double price; // 价格@Field(type = FieldType.Keyword, index = false)private String images; // 图片地址// 。。。略
}
几个用到的注解:
- @Document:声明索引库配置
- indexName:索引库名称
- type:类型名称,默认是“docs”
- shards:分片数量,默认5
- replicas:副本数量,默认1
- @Id:声明实体类的id
- @Field:声明字段属性
- type:字段的数据类型
- analyzer:指定分词器类型
- index:是否创建索引
2.1.2.创建映射
刚才的注解已经把映射关系也配置上了,所以创建映射只需要这样:
@Test
public void testMapping(){// 创建索引库,并制定实体类的字节码esTemplate.putMapping(Goods.class);
}
查看索引库:
3. 索引数据CRUD
SDE的索引数据CRUD并没有封装在ElasticsearchTemplate中,而是有一个叫做ElasticsearchRepository的接口:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9aLfTnqQ-1614864431921)(assets/1554378139181.png)]
我们需要自定义接口,继承ElasticsearchRespository:
package cn.itcast.es.repository;import cn.itcast.es.pojo.Goods;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;/*** @author 虎哥*/
public interface GoodsRepository extends ElasticsearchRepository<Goods, Long> {
}
3.1.创建索引数据
创建索引有单个创建和批量创建之分,先来看单个创建
@Autowired
private GoodsRepository goodsRepository;@Test
public void addDocument(){Goods goods = new Goods(1L, "小米手机9", " 手机","小米", 3499.00, "http://image.leyou.com/13123.jpg");// 添加索引数据goodsRepository.save(goods);
}
再来看批量创建:
@Test
public void addDocuments(){// 准备文档数据:List<Goods> list = new ArrayList<>();list.add(new Goods(1L, "小米手机7", "手机", "小米", 3299.00, "/13123.jpg"));list.add(new Goods(2L, "坚果手机R1", "手机", "锤子", 3699.00, "/13123.jpg"));list.add(new Goods(3L, "华为META10", "手机", "华为", 4499.00, "/13123.jpg"));list.add(new Goods(4L, "小米Mix2S", "手机", "小米", 4299.00, "/13123.jpg"));list.add(new Goods(5L, "荣耀V10", "手机", "华为", 2799.00, "/13123.jpg"));// 添加索引数据goodsRepository.saveAll(list);
}
通过elasticsearch-head查看:
3.2.查询索引数据
默认提供了根据id查询,查询所有两个功能:
根据id查询
@Test
public void testQueryById(){Optional<Goods> goodsOptional = goodsRepository.findById(3L);System.out.println(goodsOptional.orElse(null));
}
结果:
Item{id=3, title='华为META10', category='手机', brand='华为', price=4499.0, images='http://image.leyou.com/13123.jpg'}
查询所有:
@Test
public void testQueryAll(){Iterable<Goods> list = goodsRepository.findAll();list.forEach(System.out::println);
}
结果:
Item{id=2, title='坚果手机R1', category='手机', brand='锤子', price=3699.0, images='http://image.leyou.com/13123.jpg'}
Item{id=4, title='小米Mix2S', category='手机', brand='小米', price=4299.0, images='http://image.leyou.com/13123.jpg'}
Item{id=5, title='荣耀V10', category='手机', brand='华为', price=2799.0, images='http://image.leyou.com/13123.jpg'}
Item{id=1, title='小米手机7', category='手机', brand='小米', price=3299.0, images='http://image.leyou.com/13123.jpg'}
Item{id=3, title='华为META10', category='手机', brand='华为', price=4499.0, images='http://image.leyou.com/13123.jpg'}
3.3.自定义方法查询
GoodsRepository提供的查询方法有限,但是它却提供了非常强大的自定义查询功能:
只要遵循SpringData提供的语法,我们可以任意定义方法声明:
public interface GoodsRepository extends ElasticsearchRepository<Goods, Long> {/*** 根据价格区间查询* @param from 开始价格* @param to 结束价格* @return 符合条件的goods*/List<Goods> findByPriceBetween(Double from, Double to);
}
无需写实现,SDE会自动帮我们实现该方法,我们只需要用即可:
@Test
public void testQueryByPrice(){List<Goods> list = goodsRepository.findByPriceBetween(1000d, 4000d);list.forEach(System.out::println);
}
结果:
Item{id=2, title='坚果手机R1', category='手机', brand='锤子', price=3699.0, images='http://image.leyou.com/13123.jpg'}
Item{id=5, title='荣耀V10', category='手机', brand='华为', price=2799.0, images='http://image.leyou.com/13123.jpg'}
Item{id=1, title='小米手机7', category='手机', brand='小米', price=3299.0, images='http://image.leyou.com/13123.jpg'}
支持的一些语法示例:
Keyword | Sample | Elasticsearch Query String |
---|---|---|
And | findByNameAndPrice | {"bool" : {"must" : [ {"field" : {"name" : "?"}}, {"field" : {"price" : "?"}} ]}} |
Or | findByNameOrPrice | {"bool" : {"should" : [ {"field" : {"name" : "?"}}, {"field" : {"price" : "?"}} ]}} |
Is | findByName | {"bool" : {"must" : {"field" : {"name" : "?"}}}} |
Not | findByNameNot | {"bool" : {"must_not" : {"field" : {"name" : "?"}}}} |
Between | findByPriceBetween | {"bool" : {"must" : {"range" : {"price" : {"from" : ?,"to" : ?,"include_lower" : true,"include_upper" : true}}}}} |
LessThanEqual | findByPriceLessThan | {"bool" : {"must" : {"range" : {"price" : {"from" : null,"to" : ?,"include_lower" : true,"include_upper" : true}}}}} |
GreaterThanEqual | findByPriceGreaterThan | {"bool" : {"must" : {"range" : {"price" : {"from" : ?,"to" : null,"include_lower" : true,"include_upper" : true}}}}} |
Before | findByPriceBefore | {"bool" : {"must" : {"range" : {"price" : {"from" : null,"to" : ?,"include_lower" : true,"include_upper" : true}}}}} |
After | findByPriceAfter | {"bool" : {"must" : {"range" : {"price" : {"from" : ?,"to" : null,"include_lower" : true,"include_upper" : true}}}}} |
Like | findByNameLike | {"bool" : {"must" : {"field" : {"name" : {"query" : "?*","analyze_wildcard" : true}}}}} |
StartingWith | findByNameStartingWith | {"bool" : {"must" : {"field" : {"name" : {"query" : "?*","analyze_wildcard" : true}}}}} |
EndingWith | findByNameEndingWith | {"bool" : {"must" : {"field" : {"name" : {"query" : "*?","analyze_wildcard" : true}}}}} |
Contains/Containing | findByNameContaining | {"bool" : {"must" : {"field" : {"name" : {"query" : "**?**","analyze_wildcard" : true}}}}} |
In | findByNameIn(Collection<String>names) | {"bool" : {"must" : {"bool" : {"should" : [ {"field" : {"name" : "?"}}, {"field" : {"name" : "?"}} ]}}}} |
NotIn | findByNameNotIn(Collection<String>names) | {"bool" : {"must_not" : {"bool" : {"should" : {"field" : {"name" : "?"}}}}}} |
Near | findByStoreNear | Not Supported Yet ! |
True | findByAvailableTrue | {"bool" : {"must" : {"field" : {"available" : true}}}} |
False | findByAvailableFalse | {"bool" : {"must" : {"field" : {"available" : false}}}} |
OrderBy | findByAvailableTrueOrderByNameDesc | {"sort" : [{ "name" : {"order" : "desc"} }],"bool" : {"must" : {"field" : {"available" : true}}}} |
4.原生查询
如果觉得上述接口依然不符合你的需求,SDE也支持原生查询,这个时候还是使用ElasticsearchTemplate
而查询条件的构建是通过一个名为NativeSearchQueryBuilder
的类来完成的,不过这个类的底层还是使用的原生API中的QueryBuilders
、AggregationBuilders
、HighlightBuilders
等工具。
示例:
@Test
public void testNativeQuery(){// 原生查询构建器NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();// 1.1 source过滤queryBuilder.withSourceFilter(new FetchSourceFilter(new String[0], new String[0]));// 1.2搜索条件queryBuilder.withQuery(QueryBuilders.matchQuery("title", "小米手机"));// 1.3分页及排序条件queryBuilder.withPageable(PageRequest.of(0, 2,Sort.by(Sort.Direction.ASC, "price")));// 1.4高亮显示// queryBuilder.withHighlightBuilder(new HighlightBuilder().field("title"));// 1.5聚合queryBuilder.addAggregation(AggregationBuilders.terms("brandAgg").field("brand"));// 构建查询条件,并且查询AggregatedPage<Goods> result = esTemplate.queryForPage(queryBuilder.build(), Goods.class);// 2、解析结果:// 2.1分页结果long total = result.getTotalElements();int totalPages = result.getTotalPages();List<Goods> list = result.getContent();System.out.println("总条数 = " + total);System.out.println("总页数 = " + totalPages);list.forEach(System.out::println);// 2.2.聚合结果Aggregations aggregations = result.getAggregations();Terms terms = aggregations.get("brandAgg");terms.getBuckets().forEach(b -> {System.out.println("品牌 = " + b.getKeyAsString());System.out.println("count = " + b.getDocCount());});
}
上述查询不支持高亮结果,悲剧。
5.自定义结果处理器
要支持高亮,必须自定义结果处理器来实现,结果处理器是一个接口:
可以看到,处理器中的方法接受3个参数:
SearchResponse
:搜索的Response,原生查询中就见到过Class<T> clazz
:结果的实体类的字节码,本例中的是Goods.classPageable
:分页参数,就是我们定义的PageRequest
返回值一个:AggregatedPage<T>
,就是带聚合的分页结果
我们可以实现这个方法,在方法内部对响应处理,带上高亮结果:
package cn.itcast.mapper;import com.google.gson.Gson;
import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.lang3.StringUtils;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.core.SearchResultMapper;
import org.springframework.data.elasticsearch.core.aggregation.AggregatedPage;
import org.springframework.data.elasticsearch.core.aggregation.impl.AggregatedPageImpl;
import org.springframework.util.CollectionUtils;import java.util.ArrayList;
import java.util.List;
import java.util.Map;public class HighlightResultMapper implements SearchResultMapper {Gson gson = new Gson();@Overridepublic <T> AggregatedPage<T> mapResults(SearchResponse response, Class<T> clazz, Pageable pageable) {String scrollId = response.getScrollId();long total = response.getHits().getTotalHits();float maxScore = response.getHits().getMaxScore();List<T> list = new ArrayList<>();for (SearchHit hit : response.getHits()) {String source = hit.getSourceAsString();T t = gson.fromJson(source, clazz);// 处理高亮Map<String, HighlightField> highlightFields = hit.getHighlightFields();if (!CollectionUtils.isEmpty(highlightFields)) {for (Map.Entry<String, HighlightField> entry : highlightFields.entrySet()) {String fieldName = entry.getKey();String value = StringUtils.join(entry.getValue().getFragments());try {BeanUtils.setProperty(t, fieldName, value);} catch (Exception e) {e.printStackTrace();}}}list.add(t);}return new AggregatedPageImpl<>(list, pageable, total, response.getAggregations(), scrollId, maxScore);}
}
需要额外3个依赖:
<dependency><groupId>com.google.code.gson</groupId><artifactId>gson</artifactId>
</dependency>
<dependency><groupId>commons-beanutils</groupId><artifactId>commons-beanutils</artifactId><version>1.9.3</version>
</dependency>
<dependency><groupId>org.apache.commons</groupId><artifactId>commons-lang3</artifactId>
</dependency>
然后再次编写带高亮查询:
@Test
public void testNativeQueryAndHighlight() {// 原生查询构建器NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();// 1.1 source过滤queryBuilder.withSourceFilter(new FetchSourceFilter(new String[0], new String[0]));// 1.2搜索条件queryBuilder.withQuery(QueryBuilders.matchQuery("title", "小米手机"));// 1.3高亮显示queryBuilder.withHighlightFields(new HighlightBuilder.Field("title"));// 构建查询条件,并且自定义结果处理器AggregatedPage<Goods> result = esTemplate.queryForPage(queryBuilder.build(), Goods.class, new HighlightResultMapper());// 2.1分页结果long total = result.getTotalElements();int totalPages = result.getTotalPages();List<Goods> list = result.getContent();System.out.println("总条数 = " + total);System.out.println("总页数 = " + totalPages);list.forEach(System.out::println);
}
结果:
总条数 = 3
总页数 = 1
Item{id=1, title='<em>小米</em><em>手机</em>7', category='手机', brand='小米', price=3299.0, images='http://image.leyou.com/13123.jpg'}
Item{id=2, title='坚果<em>手机</em>R1', category='手机', brand='锤子', price=3699.0, images='http://image.leyou.com/13123.jpg'}
Item{id=4, title='<em>小米</em>Mix2S', category='手机', brand='小米', price=4299.0, images='http://image.leyou.com/13123.jpg'}
nse.getHits()) {
String source = hit.getSourceAsString();
T t = gson.fromJson(source, clazz);
// 处理高亮
Map<String, HighlightField> highlightFields = hit.getHighlightFields();
if (!CollectionUtils.isEmpty(highlightFields)) {
for (Map.Entry<String, HighlightField> entry : highlightFields.entrySet()) {
String fieldName = entry.getKey();
String value = StringUtils.join(entry.getValue().getFragments());
try {
BeanUtils.setProperty(t, fieldName, value);
} catch (Exception e) {
e.printStackTrace();
}
}
}
list.add(t);
}
return new AggregatedPageImpl<>(list, pageable, total, response.getAggregations(), scrollId, maxScore);
}
}
需要额外3个依赖:```xml
<dependency><groupId>com.google.code.gson</groupId><artifactId>gson</artifactId>
</dependency>
<dependency><groupId>commons-beanutils</groupId><artifactId>commons-beanutils</artifactId><version>1.9.3</version>
</dependency>
<dependency><groupId>org.apache.commons</groupId><artifactId>commons-lang3</artifactId>
</dependency>
然后再次编写带高亮查询:
@Test
public void testNativeQueryAndHighlight() {// 原生查询构建器NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();// 1.1 source过滤queryBuilder.withSourceFilter(new FetchSourceFilter(new String[0], new String[0]));// 1.2搜索条件queryBuilder.withQuery(QueryBuilders.matchQuery("title", "小米手机"));// 1.3高亮显示queryBuilder.withHighlightFields(new HighlightBuilder.Field("title"));// 构建查询条件,并且自定义结果处理器AggregatedPage<Goods> result = esTemplate.queryForPage(queryBuilder.build(), Goods.class, new HighlightResultMapper());// 2.1分页结果long total = result.getTotalElements();int totalPages = result.getTotalPages();List<Goods> list = result.getContent();System.out.println("总条数 = " + total);System.out.println("总页数 = " + totalPages);list.forEach(System.out::println);
}
结果:
总条数 = 3
总页数 = 1
Item{id=1, title='<em>小米</em><em>手机</em>7', category='手机', brand='小米', price=3299.0, images='http://image.leyou.com/13123.jpg'}
Item{id=2, title='坚果<em>手机</em>R1', category='手机', brand='锤子', price=3699.0, images='http://image.leyou.com/13123.jpg'}
Item{id=4, title='<em>小米</em>Mix2S', category='手机', brand='小米', price=4299.0, images='http://image.leyou.com/13123.jpg'}