袁庭新ES系列18节|Spring Data Elasticsearch高级

前言

这一章节袁老师将带领同学们来学习Spring Data Elasticsearch高级操作相关的内容。我们继续来探索SDE是如何将原始操作Elasticsearch的客户端API进行封装的,以及通过Spring Data Elasticsearch如何来操作ES。准备好了吗?我们继续来探索ES的内容。

一. 索引数据CRUD操作

SDE的索引数据CRUD操作并没有封装在ElasticsearchTemplate类中,封装在ElasticsearchRepository这个接口中。

在com.yx.respository包下自定义ProductRepository接口,并继承ElasticsearchRespository接口。

package com.yx.respository;
import com.yx.pojo.Product;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;

public interface ProductRepository extends ElasticsearchRepository<Product, Long> {

}

1.创建索引数据

创建索引数据时,有单个创建和批量创建之分。

1.先来看单个创建。在SpringDataESTests类中定义addDocument()方法。

@Autowired
private ProductRepository productRepository;

@Test
public void addDocument() {
    Product product = new Product(1L, "小米手机Mix", "手机", "小米", 2899.00, "http://image.yx.com/12479122.jpg");
    
    // 添加索引数据
    productRepository.save(product);
}

2.再来看批量创建。在SpringDataESTests类中定义addDocuments()方法。

@Test
public void addDocuments() {
    // 准备文档数据
    List<Product> list = new ArrayList<>();

    // 添加数据
    list.add(new Product(2L, "坚果手机R1", "手机", "锤子", 3699.00, "http://image.yx.com/12479122.jpg"));
    list.add(new Product(3L, "华为META20", "手机", "华为", 4499.00, "http://image.yx.com/12479122.jpg"));
    list.add(new Product(4L, "小米Pro", "手机", "小米", 4299.00, "http://image.yx.com/12479122.jpg"));
    list.add(new Product(5L, "荣耀V20", "手机", "华为", 2799.00, "http://image.yx.com/12479122.jpg"));

    // 添加索引数据
    productRepository.saveAll(list);
}

2.查询索引数据

2.1.根据id查询数据

ElasticsearchRepository接口中封装了根据id查询的findById(ID var1)方法。

1.在SpringDataESTests类中定义findById()方法。

@Test
public void findById() {
    Optional<Product> optional = productRepository.findById(1L);
    
    Product defaultProduct = new Product();
    defaultProduct.setTitle("默认商品数据");
    
    // orElse(T other)方法:如果Optional对象中封装的泛型为null,则将orElse()方法的参数作为返回值
    Product product = optional.orElse(defaultProduct);
    
    System.err.println(product);
}

2.运行findById()方法,输出结果见下:

Product(id=1, title=小米手机Mix, category=手机, brand=小米, price=2899.0, images=http://image.yx.com/12479122.jpg)

2.2.查询所有数据

ElasticsearchRepository接口中封装了查询所有数据的findAll()方法。

1.在SpringDataESTests类中定义findAll()方法。

@Test
public void findAll() {
    Iterable<Product> list = productRepository.findAll();
    
    list.forEach(System.err::println);
}

2.运行findAll()方法,输出结果见下:

Product(id=2, title=坚果手机R1, category=手机, brand=锤子, price=3699.0, images=http://image.yx.com/12479122.jpg)
Product(id=4, title=小米Pro, category=手机, brand=小米, price=4299.0, images=http://image.yx.com/12479122.jpg)
Product(id=5, title=荣耀V20, category=手机, brand=华为, price=2799.0, images=http://image.yx.com/12479122.jpg)
Product(id=1, title=小米手机Mix, category=手机, brand=小米, price=2899.0, images=http://image.yx.com/12479122.jpg)
Product(id=3, title=华为META20, category=手机, brand=华为, price=4499.0, images=http://image.yx.com/12479122.jpg)

3.自定义方法查询

3.1.存储库查询关键字

ElasticsearchRepository提供的查询方法有限,但是它却提供了非常强大的自定义查询功能。只要遵循Spring Data Elasticsearch提供的语法,我们可以任意定义方法声明。

关键字

示例

Elasticsearch查询字符串

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" : "?"}}}}

Betweend

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}}}}

3.2.自定义方法查询案例

1.在ProductRepository接口中定义根据商品的价格区间查询商品数据的findByPriceBetween()方法。

/**
* 根据商品的价格区间查询商品数据
* @param from 开始价格
* @param to 结束价格
* @return 符合条件的商品数据
*/
List<Product> findByPriceBetween(Double from, Double to);

2.无需写实现,SDE会自动帮我们实现该方法,我们只需要用即可。在SpringDataESTests类中定义findByPriceBetween()方法。

@Test
public void findByPriceBetween() {
    List<Product> products = productRepository.findByPriceBetween(3000d, 5000d);
    products.forEach(System.err::println);
}

3.运行findByPriceBetween()方法,输出结果见下:

Product(id=2, title=坚果手机R1, category= 手机, brand=锤子, price=3699.0, images=http://image.yx.com/12479122.jpg)
Product(id=4, title=小米Pro, category= 手机, brand=小米, price=4299.0, images=http://image.yx.com/12479122.jpg)
Product(id=3, title=华为META20, category= 手机, brand=华为, price=4499.0, images=http://image.yx.com/12479122.jpg)

二. 原生查询

1.原生查询介绍

如果觉得上述接口依然不符合你的需求,SDE也支持原生查询。这个时候还是使用ElasticsearchTemplate,而查询条件的构建是通过一个名为NativeSearchQueryBuilder的类来完成的,不过这个类的底层还是使用的原生API中的QueryBuilders、AggregationBuilders、HighlightBuilders等工具来实现的。

2.原生查询案例

需求描述:

  • 查询title中包含“手机”的商品;
  • 且以价格升序进行排序;
  • 进行分页查询:每页展示2条数据,查询第1页;
  • 最后对查询结果进行聚合分析:获取品牌及个数。

1.在SpringDataESTests类中定义nativeQuery()方法。

@Test
public void nativeQuery() {
    /* 1 查询结果 */
    
    // 1.1 原生查询构建器
    NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
    
    // 1.2 source过滤
    queryBuilder.withSourceFilter(new FetchSourceFilter(new String[0], new String[0]));
    
    // 1.3 搜索条件
    queryBuilder.withQuery(QueryBuilders.matchQuery("title", "手机"));
    
    // 1.4 分页及排序条件
    queryBuilder.withPageable(PageRequest.of(0, 2, Sort.by(Sort.Direction.ASC, "price")));
    
    // 1.5 高亮显示
    // TODO
    
    // 1.6 聚合
    queryBuilder.addAggregation(AggregationBuilders.terms("brandAgg").field("brand"));
    
    // 1.7 构建查询条件,并且查询
    AggregatedPage<Product> result = template.queryForPage(queryBuilder.build(), Product.class);

    /* 2 解析结果 */
    
    // 2.1 分页结果
    long total = result.getTotalElements();
    int totalPages = result.getTotalPages();
    List<Product> list = result.getContent();
    System.out.println("总条数:" + total);
    System.out.println("总页数:" + totalPages);
    System.out.println("数据:" + list);
    
    // 2.2 聚合结果
    Aggregations aggregations = result.getAggregations();
    // 导包org.elasticsearch.search.aggregations.bucket.terms.Terms
    Terms terms = aggregations.get("brandAgg");
    terms.getBuckets().forEach(b -> {
        System.out.println("品牌:" + b.getKeyAsString());
        System.out.println("count:" + b.getDocCount());
    });
}

注意:上述查询不支持高亮结果。

2.运行nativeQuery()方法,输出结果见下:

总条数:2
总页数:1
数据:[Product(id=1, title=小米手机Mix, category=手机, brand=小米, price=2899.0, images=http://image.yx.com/12479122.jpg), Product(id=2, title=坚果手机R1, category=手机, brand=锤子, price=3699.0, images=http://image.yx.com/12479122.jpg)]
品牌:小米
count:1
品牌:锤子
count:1

三. 高亮显示

1.高亮显示需求

需求描述:查询title中包含“小米手机”的商品,并将title中对应的分词通过<span style='color:red'></span>标签进行高亮修饰。

2.高亮显示实现

1.在com.yx.mapper包下自定义搜索结果映射ProductSearchResultMapper类。

package com.yx.mapper;
import com.google.gson.Gson;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.common.text.Text;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
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 java.util.ArrayList;
import java.util.List;
import java.util.Map;

/** 自定义查询结果映射,用于处理高亮显示 */
public class ProductSearchResultMapper implements SearchResultMapper {
    /**
	 * 完成查询结果映射。将_source取出,然后放入高亮的数据
 	 */
    @Override
    public <T> AggregatedPage<T> mapResults(SearchResponse searchResponse, Class<T> aClass, Pageable pageable) {
        // 记录总条数
        long totalHits = searchResponse.getHits().getTotalHits();
        
        // 记录列表(泛型) - 构建Aggregate使用
        List<T> list = new ArrayList<>();
        
        // 获取搜索结果(真正的的记录)
        SearchHits hits = searchResponse.getHits();
        
        for (SearchHit hit : hits) {
            if (hits.getHits().length <= 0) {
                return null;
            }
            
            // 将原本的JSON对象转换成Map对象
            Map<String, Object> map = hit.getSourceAsMap();
            
            // 获取高亮的字段Map
            Map<String, HighlightField> highlightFields = hit.getHighlightFields();
            
            for (Map.Entry<String, HighlightField> highlightField : highlightFields.entrySet()) {
                // 获取高亮的Key
                String key = highlightField.getKey();
                // 获取高亮的Value
                HighlightField value = highlightField.getValue();
                // 实际fragments[0]就是高亮的结果,无需遍历拼接
                Text[] fragments = value.getFragments();
                // 因为高亮的字段必然存在于Map中,就是key值
                map.put(key, fragments[0].toString());
            }
            
            // 把Map转换成对象
            Gson gson = new Gson();
            T item = gson.fromJson(gson.toJson(map), aClass);
            list.add(item);
        }
        
        // 返回的是带分页的结果
        return new AggregatedPageImpl<>(list, pageable, totalHits);
    }
}

2.高亮实现。重构nativeQuery()方法,使用自定义查询结果映射,来实现高亮显示。

@Test
public void nativeQuery() {
    /* 1 查询结果 */
    
    // 1.1 原生查询构建器
    NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
    
    // 1.2 source过滤
    queryBuilder.withSourceFilter(new FetchSourceFilter(new String[0], new String[0]));
    
    // 1.3 搜索条件
    queryBuilder.withQuery(QueryBuilders.matchQuery("title", "手机"));
    
    // 1.4 分页及排序条件
    queryBuilder.withPageable(PageRequest.of(0, 2, Sort.by(Sort.Direction.ASC, "price")));
    
    // 1.5 高亮显示
    HighlightBuilder.Field field = new HighlightBuilder.Field("title");
    field.preTags("<span style='color:red'>");
    field.postTags("</span>");
    queryBuilder.withHighlightFields(field);
    
    // 1.6 聚合
    queryBuilder.addAggregation(AggregationBuilders.terms("brandAgg").field("brand"));
    
    // 1.7 构建查询条件,并且查询
    // AggregatedPage<Product> result = template.queryForPage(queryBuilder.build(), Product.class);
    AggregatedPage<Product> result = template.queryForPage(queryBuilder.build(), Product.class, new ProductSearchResultMapper());

    /* 2 解析结果 */
    
    // 2.1 分页结果
    long total = result.getTotalElements();
    int totalPages = result.getTotalPages();
    List<Product> list = result.getContent();
    System.out.println("总条数:" + total);
    System.out.println("总页数:" + totalPages);
    System.out.println("数据:" + list);
    
    // 2.2 聚合结果
    /*
    Aggregations aggregations = result.getAggregations();
    // 导包org.elasticsearch.search.aggregations.bucket.terms.Terms
    Terms terms = aggregations.get("brandAgg");
    terms.getBuckets().forEach(b -> {
    System.out.println("品牌:" + b.getKeyAsString());
    System.out.println("count:" + b.getDocCount());
    });
	*/
}

3.运行nativeQuery()测试方法后,输出结果见下。

总条数:2
总页数:1
数据:[Product(id=1, title=小米<span style='color:red'>手机</span>Mix, category=手机, brand=小米, price=2899.0, images=http://image.yx.com/12479122.jpg), Product(id=2, title=坚果<span style='color:red'>手机</span>R1, category=手机, brand=锤子, price=3699.0, images=http://image.yx.com/12479122.jpg)]

四. 结语

Spring Data Elasticsearch基于spring data API大大简化了Elasticsearch的操作,从而简化开发人员的代码,提高开发效率。然后我们给大家介绍了使用Spring Data对Elasticsearch进行了增、删、改、查操作。

同学们,关于Elasticsearch的所有内容袁老师就给大家介绍到这里。还有很多前沿技术等着大家去探索,“路漫漫其修远昔,吾将上下而求索”。最后,祝愿各位小伙伴未来学业有成,一切如意!

今天的内容就分享到这里吧。关注「袁庭新」,干货天天都不断!

相关推荐

  1. 前端系列ES6-ES12语法

    2024-05-01 13:22:02       44 阅读
  2. ES高级用法:DeleteByQueryRequest

    2024-05-01 13:22:02       60 阅读
  3. ES实战-高级聚合

    2024-05-01 13:22:02       46 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-05-01 13:22:02       94 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-05-01 13:22:02       101 阅读
  3. 在Django里面运行非项目文件

    2024-05-01 13:22:02       82 阅读
  4. Python语言-面向对象

    2024-05-01 13:22:02       91 阅读

热门阅读

  1. react中解决 Capture Value 问题

    2024-05-01 13:22:02       35 阅读
  2. FAISS原理和使用总结

    2024-05-01 13:22:02       34 阅读
  3. H.264码流解析

    2024-05-01 13:22:02       30 阅读
  4. Nacos和Eureka有什么区别

    2024-05-01 13:22:02       32 阅读
  5. 指代消解原理

    2024-05-01 13:22:02       27 阅读
  6. Day41 HTTP编程

    2024-05-01 13:22:02       32 阅读
  7. 邦芒面试:面试时,如何展现卓越的口才

    2024-05-01 13:22:02       30 阅读
  8. 程序员商业模式画布

    2024-05-01 13:22:02       26 阅读