组件整合之Elasticsearch
ES在7.0版本开始将废弃TransportClient,8.0版本开始将完全移除TransportClient,取而代之的是High Level REST Client。
Java High Level REST Client 为高级别的Rest客户端,基于低级别的REST客户端,增加了编组请求JSON串,解析响应JSON串等相关API,使用的版本需要和ES服务端的版本保持一致,否则会有版本问题。
Java High Level REST Client
Javadoc:https://www.elastic.co/guide/en/elasticsearch/client/java-rest/index.html
maven配置:
<!-- es rest client-->
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>7.10.0</version>
</dependency>
spring-data-elasticsearch
spring-data-elasticsearch内部是对Java High Level REST Client 的再封装,有以下特征:
- 支持Spring的基于@Configuration的java配置方式,或者XML配置方式
- 提供了用于操作ES的便捷工具类ElasticsearchTemplate。包括实现文档到POJO之间的自动智能映射。
- 利用Spring的数据转换服务实现的功能丰富的对象映射
- 基于注解的元数据映射方式,而且可扩展以支持更多不同的数据格式
- 根据持久层接口自动生成对应实现方法,无需人工编写基本操作代码(类似mybatis,根据接口自动得到实现)。当然,也支持人工定制查询
pom配置
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring.version>5.2.13.RELEASE</spring.version>
<jackson.version>2.11.4</jackson.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
<!--Jackson-->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>${jackson.version}</version>
</dependency>
<!--spring-data-elasticsearch - start -->
<!--使用的spring版本需要是-->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-elasticsearch</artifactId>
<version>3.2.13.RELEASE</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.22</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.11.1</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.11.1</version>
</dependency>
<!--spring-data-elasticsearch - end -->
<!--测试框架-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${spring.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.5</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<!--因为没有web.xml,所以打成war包的时候需要忽略掉,否则会提示没有web.xml-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>2.6</version>
<configuration>
<failOnMissingWebXml>false</failOnMissingWebXml>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
注意:尽量保证spring-data-elasticsearch依赖里的spring版本与项目的spring版本保持一致,否则可能会有一些奇奇怪怪的问题。
这里我使用的spring-data-elasticsearch依赖的版本是5.2.13.RELEASE,我项目使用的spring版本就是5.2.13.RELEASE,保持一致。
es集群配置
注:我是在自己电脑上搭建了es集群。
@Configuration
public class EsConfig {
@Bean
public Client elasticsearchClient() throws UnknownHostException {
/**
* 配置选项
*/
Settings.Builder builder = Settings.builder();
//设置ES的集群名称
builder.put("cluster.name", "my-application");
//自动嗅探整个集群的状态,把集群中其他ES节点的ip添加到本地的客户端列表中。
//设了选项后,一般你不用手动设置集群里所有集群的ip到连接客户端,它会自动帮你添加,并且自动发现新加入集群的机器
builder.put("client.transport.sniff", true);
Settings esSettings = builder.build();
/**
* 这里的连接方式指的是没有安装x-pack插件
* 1. java客户端的方式是以tcp协议在9300端口上进行通信
* 2. http客户端的方式是以http协议在9200端口上进行通信
*/
TransportAddress[] transportAddressArr = new TransportAddress[3];
transportAddressArr[0] = new TransportAddress(InetAddress.getByName("127.0.0.1"), 9300);
transportAddressArr[1] = new TransportAddress(InetAddress.getByName("127.0.0.1"), 9301);
transportAddressArr[2] = new TransportAddress(InetAddress.getByName("127.0.0.1"), 9302);
return new PreBuiltTransportClient(esSettings).addTransportAddresses(transportAddressArr);
}
@Bean
public ElasticsearchTemplate elasticsearchTemplate(Client elasticsearchClient) {
return new ElasticsearchTemplate(elasticsearchClient);
}
}
实体类及注解
首先我们准备好实体类:
public class Sku implements Serializable {
private Long id;
//标题
private String title;
//分类
private String category;
//品牌
private String brand;
//价格
private Double price;
//图片地址
private String images;
}
Spring Data通过注解来声明字段的映射属性,有下面的三个注解:
- @Document 作用在类,标记实体类为文档对象,一般有四个属性
- indexName:对应索引库名称
- type:对应在索引库中的类型
- shards:分片数量,默认5
- replicas:副本数量,默认1(即每个分片有几个副本)
- @Id 作用在成员变量,标记一个字段作为id主键
- @Field 作用在成员变量,标记为文档的字段,并指定字段映射属性
- type:字段类型,取值是枚举:FieldType
- index:是否索引,布尔类型,默认是true
- store:是否存储,布尔类型,默认是false
- analyzer:分词器名称:ik_max_word
Spring Data Elasticsearch - Reference Documentation
示例:
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
import java.io.Serializable;
@Document(indexName = "sku",type = "docs", shards = 3, replicas = 2)
public class Sku implements Serializable {
@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(index = false, type = FieldType.Keyword)
private String images;
//必须加上无参构造树,否则在查询结果映射会报无法转换成Sku
public Sku() {
}
public Sku(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;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getCategory() {
return category;
}
public void setCategory(String category) {
this.category = category;
}
public String getBrand() {
return brand;
}
public void setBrand(String brand) {
this.brand = brand;
}
public Double getPrice() {
return price;
}
public void setPrice(Double price) {
this.price = price;
}
public String getImages() {
return images;
}
public void setImages(String images) {
this.images = images;
}
@Override
public String toString() {
return "Sku{" +
"id=" + id +
", title='" + title + '\'' +
", category='" + category + '\'' +
", brand='" + brand + '\'' +
", price=" + price +
", images='" + images + '\'' +
'}';
}
}
测试创建索引
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {RootConfig.class, WebConfig.class})
@WebAppConfiguration
public class EsTest {
@Autowired
private ElasticsearchTemplate elasticsearchTemplate;
@Test
public void testCreate() {
// 创建索引,会根据Sku类的@Document注解信息来创建
elasticsearchTemplate.createIndex(Sku.class);
// 配置映射,会根据Sku类中的id、Field等字段来自动完成映射
elasticsearchTemplate.putMapping(Sku.class);
}
}
增删改操作
Spring Data 的强大之处,就在于你不用写任何DAO处理,自动根据方法名或类的信息进行CRUD操作。只要你定义一个接口,然后继承Repository提供的一些子接口,就能具备各种基本的CRUD功能。
编写 SkuRepository:
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface SkuRepository extends ElasticsearchRepository<Sku,Long> {
}
需要在配置类中添加@EnableElasticsearchRepositories(basePackages = {"com.spring.es.repository"}),扫描到我们的Repository
增加
@Autowired
private SkuRepository skuRepository;
@Test
public void testAdd() {
Sku item = new Sku(1L, "华为手机Mate40", " 手机",
"华为", 4999.00, "http://image.huawei.com/13123.jpg");
skuRepository.save(item);
}
修改
id存在就是修改,否则就是插入。
@Autowired
private SkuRepository skuRepository;
@Test
public void testAdd() {
Sku item = new Sku(1L, "小米手机7777", " 手机",
"小米", 9499.00, "http://image.leyou.com/13123.jpg");
skuRepository.save(item);
}
批量新增
@Autowired
private SkuRepository skuRepository;
@Test
public void indexList() {
List<Sku> list = new ArrayList();
list.add(new Sku(2L, "坚果手机R1", " 手机", "锤子", 3699.00, "http://image.dsl.com/123.jpg"));
list.add(new Sku(3L, "华为META10", " 手机", "华为", 4499.00, "http://image.dsl.com/3.jpg"));
// 接收对象集合,实现批量新增
skuRepository.saveAll(list);
}
删除操作
@Autowired
private SkuRepository skuRepository;
@Test
public void testDelete() {
skuRepository.deleteById(1L);
}
根据id查询
@Autowired
private SkuRepository skuRepository;
@Test
public void testQuery(){
Optional<Sku> optional = skuRepository.findById(2L);
System.out.println(optional.get());
}
查询全部,并按照价格降序排序
@Autowired
private SkuRepository skuRepository;
@Test
public void testFind() {
// 查询全部,并按照价格降序排序
Iterable<Sku> items = this.skuRepository.findAll(Sort.by(Sort.Direction.DESC, "price"));
items.forEach(item->{
System.out.println(item);
});
}
输出结果:
Sku{id=3, title='华为META10', category=' 手机', brand='华为', price=4499.0, images='http://image.dsl.com/3.jpg'}
Sku{id=2, title='坚果手机R1', category=' 手机', brand='锤子', price=3699.0, images='http://image.dsl.com/123.jpg'}
自定义方法
Spring Data 的另一个强大功能,是根据方法名称自动实现功能。
比如:你的方法名叫做:findByTitle,那么它就知道你是根据title查询,然后自动帮你完成,无需写实现类。
当然,方法名称要符合一定的约定:
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}}}}} |
Like | findByPriceAfter | {"bool" : {"must" : {"range" : {"price" : {"from" : ?,"to" : null,"include_lower" : true,"include_upper" : true}}}}} |
After | 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}}}} |
例如,我们来按照价格区间查询,定义这样的一个方法:
@Repository
public interface SkuRepository extends ElasticsearchRepository<Sku,Long> {
/**
* 根据价格区间查询
* @param price1
* @param price2
* @return
*/
List<Sku> findByPriceBetween(double price1, double price2);
}
然后添加一些测试数据:
@Test
public void indexList() {
List<Sku> list = new ArrayList<>();
list.add(new Sku(1L, "小米手机7", "手机", "小米", 3299.00, "http://image.dsl.com/13124.jpg"));
list.add(new Sku(2L, "坚果手机R1", "手机", "锤子", 3699.00, "http://image.dsl.com/13125.jpg"));
list.add(new Sku(3L, "华为META10", "手机", "华为", 4499.00, "http://image.dsl.com/13126.jpg"));
list.add(new Sku(4L, "小米Mix2S", "手机", "小米", 4299.00, "http://image.dsl.com/13127.jpg"));
list.add(new Sku(5L, "荣耀V10", "手机", "华为", 2799.00, "http://image.dsl.com/13128.jpg"));
// 接收对象集合,实现批量新增
skuRepository.saveAll(list);
}
不需要写实现类,然后我们直接去运行:
@Test
public void queryByPriceBetween(){
List<Sku> list = this.skuRepository.findByPriceBetween(2000.00, 3500.00);
for (Sku sku : list) {
System.out.println("sku = " + sku);
}
}
输出结果:
sku = Sku{id=5, title='荣耀V10', category='手机', brand='华为', price=2799.0, images='http://image.dsl.com/13128.jpg'}
sku = Sku{id=1, title='小米手机7', category='手机', brand='小米', price=3299.0, images='http://image.dsl.com/13124.jpg'}
虽然基本查询和自定义方法已经很强大了,但是如果是复杂查询(模糊、通配符、词条查询等)就显得力不从心了。此时,我们只能使用原生查询。
高级查询
基本查询
先看看基本玩法:
@Test
public void testBaseQuery(){
// 词条查询
MatchQueryBuilder queryBuilder = QueryBuilders.matchQuery("title", "小米");
// 执行查询
Iterable<Sku> skuList = this.skuRepository.search(queryBuilder);
skuList.forEach(sku->{
System.out.println(sku);
});
}
QueryBuilders提供了大量的静态方法,用于生成各种不同类型的查询对象,例如:词条、模糊、通配符等QueryBuilder对象。
自定义查询
先来看最基本的match query:
@Test
public void testNativeQuery() {
// 构建查询条件
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
// 添加基本的分词查询
queryBuilder.withQuery(QueryBuilders.matchQuery("title", "小米"));
// 执行搜索,获取结果
Page<Sku> itemPage = this.skuRepository.search(queryBuilder.build());
// 打印总条数
System.out.println(itemPage.getTotalElements());
// 打印总页数
System.out.println(itemPage.getTotalPages());
itemPage.forEach(sku -> {
System.out.println(sku);
});
}
NativeSearchQueryBuilder:Spring提供的一个查询条件构建器,帮助构建json格式的请求体。
Page<item>:默认是分页查询,因此返回的是一个分页的结果对象,包含属性:
- totalElements:总条数
- totalPages:总页数
- Iterator:迭代器,本身实现了Iterator接口,因此可直接迭代得到当前页的数据
- 其它属性。
分页查询
利用NativeSearchQueryBuilder可以方便的实现分页:
@Test
public void testNativeQuery2(){
// 构建查询条件
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
// 添加基本的分词查询
queryBuilder.withQuery(QueryBuilders.termQuery("category", "手机"));
// 初始化分页参数
int page = 0;
int size = 3;
// 设置分页参数
queryBuilder.withPageable(PageRequest.of(page, size));
// 执行搜索,获取结果
Page<Sku> items = this.skuRepository.search(queryBuilder.build());
// 打印总条数
System.out.println(items.getTotalElements());
// 打印总页数
System.out.println(items.getTotalPages());
// 每页大小
System.out.println(items.getSize());
// 当前页
System.out.println(items.getNumber());
items.forEach(item->{
System.out.println(item);
});
}
注意:Elasticsearch中的分页是从第0页开始。
排序
排序也通用通过NativeSearchQueryBuilder完成:
@Test
public void testSort(){
// 构建查询条件
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
// 添加基本的分词查询
queryBuilder.withQuery(QueryBuilders.termQuery("category", "手机"));
// 排序
queryBuilder.withSort(SortBuilders.fieldSort("price").order(SortOrder.DESC));
// 执行搜索,获取结果
Page<Sku> items = this.skuRepository.search(queryBuilder.build());
// 打印总条数
System.out.println(items.getTotalElements());
items.forEach(item->{
System.out.println(item);
});
}
聚合查询
聚合为桶
桶就是分组,比如这里我们按照品牌brand进行分组:
@Test
public void testAgg(){
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
// 不查询任何结果
queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{""}, null));
// 1、添加一个新的聚合,聚合类型为terms,聚合名称为brands,聚合字段为brand
queryBuilder.addAggregation(
AggregationBuilders.terms("brands").field("brand"));
// 2、查询,需要把结果强转为AggregatedPage类型
AggregatedPage<Sku> aggPage = (AggregatedPage<Sku>) skuRepository.search(queryBuilder.build());
// 3、解析
// 3.1、从结果中取出名为brands的那个聚合,
// 因为是利用String类型字段来进行的term聚合,所以结果要强转为StringTerm类型
StringTerms agg = (StringTerms) aggPage.getAggregation("brands");
// 3.2、获取桶
List<StringTerms.Bucket> buckets = agg.getBuckets();
// 3.3、遍历
for (StringTerms.Bucket bucket : buckets) {
// 3.4、获取桶中的key,即品牌名称
System.out.println(bucket.getKeyAsString());
// 3.5、获取桶中的文档数量
System.out.println(bucket.getDocCount());
}
}
输出如下:
华为
2
小米
2
锤子
1
嵌套聚合,求平均值
@Test
public void testSubAgg(){
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
// 不查询任何结果
queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{""}, null));
// 1、添加一个新的聚合,聚合类型为terms,聚合名称为brands,聚合字段为brand
queryBuilder.addAggregation(
AggregationBuilders.terms("brands").field("brand")
.subAggregation(AggregationBuilders.avg("priceAvg").field("price")) // 在品牌聚合桶内进行嵌套聚合,求平均值
);
// 2、查询,需要把结果强转为AggregatedPage类型
AggregatedPage<Sku> aggPage = (AggregatedPage<Sku>) this.skuRepository.search(queryBuilder.build());
// 3、解析
// 3.1、从结果中取出名为brands的那个聚合,
// 因为是利用String类型字段来进行的term聚合,所以结果要强转为StringTerm类型
StringTerms agg = (StringTerms) aggPage.getAggregation("brands");
// 3.2、获取桶
List<StringTerms.Bucket> buckets = agg.getBuckets();
// 3.3、遍历
for (StringTerms.Bucket bucket : buckets) {
// 3.4、获取桶中的key,即品牌名称 3.5、获取桶中的文档数量
System.out.println(bucket.getKeyAsString() + ",共" + bucket.getDocCount() + "台");
// 3.6.获取子聚合结果:
InternalAvg avg = (InternalAvg) bucket.getAggregations().asMap().get("priceAvg");
System.out.println("平均售价:" + avg.getValue());
}
}
API说明
我们先来看一个例子:
@Autowired
private ElasticsearchTemplate elasticsearchTemplate;
@Test
public void testQuery(){
NativeSearchQuery nativeSearchQuery = new NativeSearchQueryBuilder()
//查询条件
.withQuery(QueryBuilders.queryStringQuery("华为").defaultField("title"))
//分页
.withPageable(PageRequest.of(0, 5))
//排序
.withSort(SortBuilders.fieldSort("id").order(SortOrder.DESC))
//高亮字段显示
.withHighlightFields(new HighlightBuilder.Field("华为"))
.build();
List<Sku> skuEntities = elasticsearchTemplate.queryForList(nativeSearchQuery, Sku.class);
skuEntities.forEach(item -> System.out.println(item.toString()));
}
- NativeSearchQuery :是spring data中的查询条件;
- NativeSearchQueryBuilder :用于建造一个NativeSearchQuery查询对象;
- QueryBuilders :设置查询条件,是ES中的类;
- SortBuilders :设置排序条件;
- HighlightBuilder :设置高亮显示;
NativeSearchQuery
这是一个原生的查询条件类,用来和ES的一些原生查询方法进行搭配,实现一些比较复杂的查询。
下面是NativeSearchQuery的一些内部属性,基本上都是ES的一些内部对象:
//查询条件,查询的时候,会考虑关键词的匹配度,并按照分值进行排序
private QueryBuilder query;
//查询条件,查询的时候,不考虑匹配程度以及排序这些事情
private QueryBuilder filter;
//排序条件的builder
private List<SortBuilder> sorts;
private final List<ScriptField> scriptFields = new ArrayList<>();
private CollapseBuilder collapseBuilder;
private List<FacetRequest> facets;
private List<AbstractAggregationBuilder> aggregations;
//高亮显示的builder
private HighlightBuilder highlightBuilder;
private HighlightBuilder.Field[] highlightFields;
private List<IndexBoost> indicesBoost;
QueryBuilders
QueryBuilders是ES中的查询条件构造器。下面结合一些具体的查询场景,分析其常用方法。
假设ES中已经有title为 “总裁关心浦东开发开放” 的数据;
ik_smart分词结果:
{
"tokens": [
{
"token": "总裁",
"start_offset": 3,
"end_offset": 6,
"type": "CN_WORD",
"position": 1
},
{
"token": "关心",
"start_offset": 6,
"end_offset": 8,
"type": "CN_WORD",
"position": 2
},
{
"token": "浦东",
"start_offset": 8,
"end_offset": 10,
"type": "CN_WORD",
"position": 3
},
{
"token": "开发",
"start_offset": 10,
"end_offset": 12,
"type": "CN_WORD",
"position": 4
},
{
"token": "开放",
"start_offset": 12,
"end_offset": 14,
"type": "CN_WORD",
"position": 5
}
]
}
1、精确查询
(1)指定字符串作为关键词查询,关键词支持分词
//查询title字段中,包含 ”开发”、“开放" 这个字符串的document;相当于把"浦东开发开放"分词了,再查询;
QueryBuilders.queryStringQuery("开发开放").defaultField("title");
//不指定feild,查询范围为所有feild
QueryBuilders.queryStringQuery("青春");
//指定多个feild
QueryBuilders.queryStringQuery("青春").field("title").field("content");
(2)以关键字“开发开放”,关键字不支持分词
QueryBuilders.termQuery("title", "开发开放")
QueryBuilders.termsQuery("fieldName", "fieldlValue1","fieldlValue2...")
(3)以关键字“开发开放”,关键字支持分词
QueryBuilders.matchQuery("title", "开发开放")
QueryBuilders.multiMatchQuery("fieldlValue", "fieldName1", "fieldName2", "fieldName3")
2、模糊查询
模糊,是指查询关键字与目标关键字可以模糊匹配。
(1)左右模糊查询,其中fuzziness的参数作用是在查询时,es动态的将查询关键词前后增加或者删除一个词,然后进行匹配
QueryBuilders.fuzzyQuery("title", "开发开放").fuzziness(Fuzziness.ONE)
(2)前缀查询,查询title中以“开发开放”为前缀的document;
QueryBuilders.prefixQuery("title", "开发开放")
(3)通配符查询,支持*和?,?表示单个字符;注意不建议将通配符作为前缀,否则导致查询很慢
QueryBuilders.wildcardQuery("title", "开*放")
QueryBuilders.wildcardQuery("title", "开?放")
在分词的情况下,针对fuzzyQuery、prefixQuery、wildcardQuery不支持分词查询,即使有这种document数据,也不一定能查出来,因为分词后,不一定有“开发开放”这个词。
查询关键词
查询关键词 | 开发开放 | 放 | 开 |
queryStringQuery | 查询目标中含有开发、开放、开发开放的 | 无 | 无 |
matchQuery | 同queryStringQuery | 无 | 无 |
termQuery | 无结果,因为它不支持分词 | 无 | 无 |
prefixQuery | 无结果,因为它不支持分词 | 无 | 有,目标分词中以”开“开头的 |
fuzzyQuery | 无结果,但是与fuzziness参数有关系 | 无 | 无 |
wildcardQuery | 开发开放*无结果 | 开*,有 | 放*,无 |
3、范围查询
//闭区间查询
QueryBuilders.rangeQuery("fieldName").from("fieldValue1").to("fieldValue2");
//开区间查询,默认是true,也就是包含
QueryBuilders.rangeQuery("fieldName").from("fieldValue1").to("fieldValue2").includeUpper(false).includeLower(false);
//大于
QueryBuilders.rangeQuery("fieldName").gt("fieldValue");
//大于等于
QueryBuilders.rangeQuery("fieldName").gte("fieldValue");
//小于
QueryBuilders.rangeQuery("fieldName").lt("fieldValue");
//小于等于
QueryBuilders.rangeQuery("fieldName").lte("fieldValue");
4、多个关键字组合查询boolQuery()
QueryBuilders.boolQuery()
QueryBuilders.boolQuery().must();//文档必须完全匹配条件,相当于and
QueryBuilders.boolQuery().mustNot();//文档必须不匹配条件,相当于not
QueryBuilders.boolQuery().should();//至少满足一个条件,这个文档就符合should,相当于or
示例:
public void testBoolQuery() {
NativeSearchQuery nativeSearchQuery = new NativeSearchQueryBuilder()
.withQuery(QueryBuilders.boolQuery()
.should(QueryBuilders.termQuery("title", "开发"))
.should(QueryBuilders.termQuery("title", "青春"))
.mustNot(QueryBuilders.termQuery("title", "潮头"))
)
.withSort(SortBuilders.fieldSort("id").order(SortOrder.DESC))
.withPageable(PageRequest.of(0, 50))
.build();
List<ArticleEntity> articleEntities = elasticsearchTemplate.queryForList(nativeSearchQuery, ArticleEntity.class);
articleEntities.forEach(item -> System.out.println(item.toString()));
}
以上是查询title分词中,包含“开发”或者“青春”,但不能包含“潮头”的document;
也可以多个must组合。
SortBuilders排序
上述示例中,我们使用了排序条件:
//按照id字段降序
.withSort(SortBuilders.fieldSort("id").order(SortOrder.DESC))
注意排序时,有个坑,就是在以id排序时,比如降序,结果可能并不是我们想要的。因为根据id排序,es实际上会根据_id进行排序,但是_id是string类型的,排序后的结果会与整型不一致。
建议:在创建es的索引mapping时,将es的id和业务的id分开,比如业务id叫做myId
@Id
@Field(type = FieldType.Long, store = true)
private Long myId;
@Field(type = FieldType.Text, store = true, analyzer = "ik_smart")
private String title;
@Field(type = FieldType.Text, store = true, analyzer = "ik_smart")
private String content;
这样,后续排序可以使用myId进行排序。
分页
@Test
public void testPage() {
NativeSearchQuery nativeSearchQuery = new NativeSearchQueryBuilder()
.withQuery(QueryBuilders.matchQuery("title", "青春"))
.withSort(SortBuilders.fieldSort("myId").order(SortOrder.DESC))
.withPageable(PageRequest.of(0, 50))
.build();
AggregatedPage<ArticleEntity> page = elasticsearchRestTemplate.queryForPage(nativeSearchQuery, ArticleEntity.class);
List<ArticleEntity> articleEntities = page.getContent();
articleEntities.forEach(item -> System.out.println(item.toString()));
}
注意,如果不指定分页参数,es默认只显示10条。
高亮显示
查询title字段中的关键字,并高亮显示:
@Test
public void test() {
String preTag = "<font color='#dd4b39'>";
String postTag = "</font>";
NativeSearchQuery nativeSearchQuery = new NativeSearchQueryBuilder()
.withQuery(QueryBuilders.matchQuery("title", "开发"))
.withPageable(PageRequest.of(0, 50))
.withSort(SortBuilders.fieldSort("id").order(SortOrder.DESC))
.withHighlightFields(new HighlightBuilder.Field("title").preTags(preTag).postTags(postTag))
.build();
AggregatedPage<ArticleEntity> page = elasticsearchTemplate.queryForPage(nativeSearchQuery, ArticleEntity.class,
new SearchResultMapper() {
@Override
public <T> AggregatedPage<T> mapResults(SearchResponse response, Class<T> clazz, Pageable pageable) {
List<ArticleEntity> chunk = new ArrayList();
for (SearchHit searchHit : response.getHits()) {
if (response.getHits().getHits().length <= 0) {
return null;
}
ArticleEntity article = new ArticleEntity();
article.setMyId(Long.valueOf(searchHit.getSourceAsMap().get("id").toString()));
article.setContent(searchHit.getSourceAsMap().get("content").toString());
HighlightField title = searchHit.getHighlightFields().get("title");
if (title != null) {
article.setTitle(title.fragments()[0].toString());
}
chunk.add(article);
}
if (chunk.size() > 0) {
return new AggregatedPageImpl<>((List<T>) chunk);
}
return null;
}
@Override
public <T> T mapSearchHit(SearchHit searchHit, Class<T> type) {
return null;
}
});
List<ArticleEntity> articleEntities = page.getContent();
articleEntities.forEach(item -> System.out.println(item.toString()));
}
结果:
title=勇立潮头——总裁关心浦东<font color='#dd4b39'>开发</font>开放40, content=外交部:望