组件整合之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=外交部:望

 

posted @ 2022-01-03 16:14  残城碎梦  阅读(602)  评论(0编辑  收藏  举报