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/
特征:
@Configuration
的java配置方式,或者XML配置方式ElasticsearchTemplate
**。包括实现文档到POJO之间的自动智能映射。我们在pom文件中,引入SpringDataElasticsearch的启动器:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-elasticsearchartifactId>
dependency>
然后,只需要在resources下新建application.yml文件,引入elasticsearch的host和port即可:
spring:
data:
elasticsearch:
cluster-name: elastic
cluster-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供我们使用。接下来一起来试试吧。
我们先创建一个测试类,然后注入ElasticsearchTemplate:
/**
* @author 虎哥
*/
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringElasticsearchTest {
@Autowired
private 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 {
@Id
private 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; // 图片地址
// 。。。略
}
几个用到的注解:
刚才的注解已经把映射关系也配置上了,所以创建映射只需要这样:
@Test
public void testMapping(){
// 创建索引库,并制定实体类的字节码
esTemplate.putMapping(Goods.class);
}
查看索引库:
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> {
}
创建索引有单个创建和批量创建之分,先来看单个创建
@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查看:
默认提供了根据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'}
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 |
{"bool" : {"must" : {"bool" : {"should" : [ {"field" : {"name" : "?"}}, {"field" : {"name" : "?"}} ]}}}} |
NotIn |
findByNameNotIn(Collection |
{"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}}}} |
如果觉得上述接口依然不符合你的需求,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());
});
}
上述查询不支持高亮结果,悲剧。
要支持高亮,必须自定义结果处理器来实现,结果处理器是一个接口:
可以看到,处理器中的方法接受3个参数:
SearchResponse
:搜索的Response,原生查询中就见到过Class clazz
:结果的实体类的字节码,本例中的是Goods.classPageable
:分页参数,就是我们定义的PageRequest返回值一个:AggregatedPage
,就是带聚合的分页结果
我们可以实现这个方法,在方法内部对响应处理,带上高亮结果:
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();
@Override
public <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.gsongroupId>
<artifactId>gsonartifactId>
dependency>
<dependency>
<groupId>commons-beanutilsgroupId>
<artifactId>commons-beanutilsartifactId>
<version>1.9.3version>
dependency>
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-lang3artifactId>
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='小米手机7', category='手机', brand='小米', price=3299.0, images='http://image.leyou.com/13123.jpg'}
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'}
nse.getHits()) {
String source = hit.getSourceAsString();
T t = gson.fromJson(source, clazz);
// 处理高亮
Map
if (!CollectionUtils.isEmpty(highlightFields)) {
for (Map.Entry
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
com.google.code.gson
gson
commons-beanutils
commons-beanutils
1.9.3
org.apache.commons
commons-lang3
然后再次编写带高亮查询:
@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='小米手机7', category='手机', brand='小米', price=3299.0, images='http://image.leyou.com/13123.jpg'}
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'}