跳至主要內容

Elasticsearch-复杂查询

Zenghr大约 9 分钟

Elasticsearch 复杂查询

前面,我们已经学习了 Elasticsearch 的基本 API 的操作,也在 SpringBoot 中集成了 Elasticsearch,使用 Repository 接口实现简单的 Crud 操作

因为 旧版本的 Repository 中的 search 方法被废弃了,所以我们要实现 复杂查询 只能使用 ElasticsearchRestTemplate

查询条件对象

对象描述
NativeSearchQueryBuilderSpring提供的一个查询条件构建器,帮助构建json格式的请求体
QueryBuilders提供了大量的静态方法,用于生成各种不同类型的查询对象,例如:词条、模糊、通配符等QueryBuilder对象
BoolQueryBuilder多条件查询对象,对应 Elasticsearch 中的 bool
MatchQueryBuilder构建分词查询条件对象,对应 Elasticsearch 中的 match
PageRequest用于构建分页请求
IndexCoordinate索引对象,通常在查询使用

数据准备

首先我们先准备接下来查询的 mapping 结构以及基本数据

mapping 结构

{
  "mappings": {
    "_doc": {
      "properties": {
        "_class": {
          "type": "keyword",
          "index": false,
          "doc_values": false
        },
        "brand": {
          "type": "keyword",
          "store": true
        },
        "id": {
          "type": "keyword"
        },
        "intro": {
          "type": "text",
          "store": true,
          "analyzer": "ik_max_word"
        },
        "price": {
          "type": "integer",
          "store": true
        },
        "title": {
          "type": "text",
          "store": true,
          "analyzer": "ik_max_word"
        }
      }
    }
  }
}

批量导入数据

{"create":{"_id": 1}}
{"id":1,"title":"Apple iPhone XR (A2108) 128GB 白色 移动联通电信4G手机 双卡双待","price":5299,"intro":"【iPhoneXR限时特惠!】6.1英寸视网膜显示屏,A12仿生芯片,面容识别,无线充电,支持双卡!选【换修无忧版】获 AppleCare 原厂服务,享只换不修!更有快速换机、保值换新、轻松月付!","brand":"Apple"}
{"create":{"_id": 2}}
{"id":2,"title":"Apple 2019款 Macbook Pro 13.3【带触控栏】八代i7 18G 256G RP645显卡 深空灰 苹果笔记本电脑 轻薄本 MUHN2CH/A","price":15299,"intro":"【八月精选】Pro2019年新品上市送三重好礼,现在购买领满8000减400元优惠神劵,劵后更优惠!","brand":"Apple"}
{"create":{"_id": 3}}
{"id":3,"title":"Apple iPad Air 3 2019年新款平板电脑 10.5英寸(64G WLAN版/A12芯片/Retina显示屏/MUUL2CH/A)金色","price":3788,"intro":"8月尊享好礼!买iPad即送蓝牙耳机!领券立减!多款产品支持手写笔!【新一代iPad,总有一款适合你】选【换修无忧版】获 AppleCare 原厂服务,享只换不修!更有快速换机、保值换新、轻松月付!","brand":"Apple"}
{"create":{"_id": 4}}
{"id":4,"title":"华为HUAWEI MateBook X Pro 2019款 英特尔酷睿i5 13.9英寸全面屏轻薄笔记本电脑(i5 8G 512G 3K 触控) 灰","price":7999,"intro":"3K全面屏开启无界视野;轻薄设计灵动有型,HuaweiShare一碰传","brand":"华为"}
{"create":{"_id": 5}}
{"id":5,"title":"华为 HUAWEI Mate20 X (5G) 7nm工艺5G旗舰芯片全面屏超大广角徕卡三摄8GB+256GB翡冷翠5G双模全网通手机","price":6199,"intro":"【5G双模,支持SA/NSA网络,7.2英寸全景巨屏,石墨烯液冷散热】5G先驱,极速体验。","brand":"华为"}
{"create":{"_id": 6}}
{"id":6,"title":"华为平板 M6 10.8英寸麒麟980影音娱乐平板电脑4GB+64GB WiFi(香槟金)","price":2299,"intro":"【华为暑期购】8月2日-4日,M5青春版指定爆款型号优惠100元,AI语音控制","brand":"华为"}
{"create":{"_id": 7}}
{"id":7,"title":"荣耀20 PRO DXOMARK全球第二高分 4800万四摄 双光学防抖 麒麟980 全网通4G 8GB+128GB 蓝水翡翠 拍照手机","price":3199,"intro":"白条6期免息!麒麟980,4800万全焦段AI四摄!荣耀20系列2699起,4800万超广角AI四摄!","brand":"荣耀"}
{"create":{"_id": 8}}
{"id":8,"title":"荣耀MagicBook Pro 16.1英寸全面屏轻薄性能笔记本电脑(酷睿i7 8G 512G MX250 IPS FHD 指纹解锁)冰河银","price":6199,"intro":"16.1英寸无界全面屏金属轻薄本,100%sRGB色域,全高清IPS防眩光护眼屏,14小时长续航,指纹一健开机登录,魔法一碰传高速传输。","brand":"荣耀"}
{"create":{"_id": 9}}
{"id":9,"title":"荣耀平板5 麒麟8核芯片 GT游戏加速 4G+128G 10.1英寸全高清屏影音平板电脑 WiFi版 冰川蓝","price":1549,"intro":"【爆款平板推荐】哈曼卡顿专业调音,10.1英寸全高清大屏,双喇叭立体环绕音,配置多重护眼,值得拥有!","brand":"荣耀"}
{"create":{"_id": 10}}
{"id":10,"title":"小米9 4800万超广角三摄 6GB+128GB全息幻彩蓝 骁龙855 全网通4G 双卡双待 水滴全面屏拍照智能游戏手机","price":2799,"intro":"限时优惠200,成交价2799!索尼4800万广角微距三摄,屏下指纹解锁!","brand":"小米"}
{"create":{"_id": 11}}
{"id":11,"title":"小米(MI)Pro 2019款 15.6英寸金属轻薄笔记本(第八代英特尔酷睿i7-8550U 16G 512GSSD MX250 2G独显) 深空灰","price":6899,"intro":"【PCIE固态硬盘、72%NTSC高色域全高清屏】B面康宁玻璃覆盖、16G双通道大内存、第八代酷睿I7处理器、专业级调校MX150","brand":"小米"}
{"create":{"_id": 12}}
{"id":12,"title":"联想(Lenovo)拯救者Y7000P 2019英特尔酷睿i7 15.6英寸游戏笔记本电脑(i7 9750H 16G 1T SSD GTX1660Ti 144Hz)","price":9299,"intro":"超大1T固态,升级双通道16G内存一步到位,GTX1660Ti电竞级独显,英特尔9代i7H高性能处理器,144Hz电竞屏窄边框!","brand":"联想"}

编写 domain 以及 Repository 接口

@Data
@Document(indexName = "product")
public class Product {
    @Id
    @Field(store = true, index = false, type = FieldType.Long)
    private Long id;

    @Field(analyzer = "ik_max_word", searchAnalyzer = "ik_max_word", store = true, type = FieldType.Text)
    private String title;

    @Field(store = true, type = FieldType.Integer)
    private Integer price;

    @Field(analyzer = "ik_max_word", searchAnalyzer = "ik_max_word", store = true, type = FieldType.Text)
    private String intro;

    @Field(store = true, type = FieldType.Keyword)
    private String brand;
}

Repository 接口

@Repository
public interface ProductRepository extends ElasticsearchRepository<Product, Long> {
}

基本分词查询

@Test
public void testQuery(){
    // 构建查询条件
    NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder();
    // 添加基本的分词查询
    MatchQueryBuilder matchQueryBuilder = QueryBuilders.matchQuery("title", "小米");
    // 生成查询对象 Query
    NativeSearchQuery queryBuilder = builder.withQuery(matchQueryBuilder).build();
	// 发起请求
    SearchHits<Product> hits = restTemplate.search(queryBuilder, Product.class);
    hits.forEach(System.out::println);
}

查询结果如下:

SearchHit{id='10', score=1.6231201, sortValues=[], content=Product(id=10, title=小米9 4800万超广角三摄 6GB+128GB全息幻彩蓝 骁龙855 全网通4G 双卡双待 水滴全面屏拍照智能游戏手机, price=2799, intro=限时优惠200,成交价2799!索尼4800万广角微距三摄,屏下指纹解锁!, brand=小米), highlightFields={}}
SearchHit{id='11', score=1.485091, sortValues=[], content=Product(id=11, title=小米(MI)Pro 201915.6英寸金属轻薄笔记本(第八代英特尔酷睿i7-8550U 16G 512GSSD MX250 2G独显) 深空灰, price=6899, intro=PCIE固态硬盘、72%NTSC高色域全高清屏】B面康宁玻璃覆盖、16G双通道大内存、第八代酷睿I7处理器、专业级调校MX150, brand=小米), highlightFields={}}

NativeSearchQueryBuilder :Spring提供的一个查询条件构建器,帮助构建json格式的请求体

NativeSearchQuery :NativeSearchQueryBuilder 构建具体的查询对象,实现 Query 接口,search方法中的 query 参数

QueryBuilders :用于生成各种不同类型的查询对象

分页查询

利用NativeSearchQueryBuilder可以方便的实现分页:

需求 - 查询商品中带 华为 关键字的所有商品,每页显示 2 条,根据价格排序

@Test
public void testNativeQuery(){
    // 构建查询条件
    NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder();
    // 添加基本的分词查询
    builder.withQuery(QueryBuilders.matchQuery("title", "华为"));

    // 初始化分页参数
    int page = 0;
    int size = 2;
    // 设置分页参数
    PageRequest pageable = PageRequest.of(page, size, Sort.Direction.DESC, "price");
    builder.withPageable(pageable);

    // 执行搜索,获取结果
    SearchHits<Product> products = restTemplate.search(builder.build(), Product.class, IndexCoordinates.of("product"));
    List<Product> list = products.stream()
        .map(SearchHit::getContent)
        .collect(Collectors.toList());
    // 构建分页
    Page<Product> productPage = new PageImpl<>(list, pageable, products.getTotalHits());
    // 打印总条数
    System.out.println(productPage.getTotalElements());
    // 打印总页数
    System.out.println(productPage.getTotalPages());
    // 每页大小
    System.out.println(productPage.getSize());
    // 当前页
    System.out.println(productPage.getNumber());
    productPage.forEach(System.out::println);
}

查询结果如下:

3
2
2
0
Product(id=4, title=华为HUAWEI MateBook X Pro 2019款 英特尔酷睿i5 13.9英寸全面屏轻薄笔记本电脑(i5 8G 512G 3K 触控), price=7999, intro=3K全面屏开启无界视野;轻薄设计灵动有型,HuaweiShare一碰传, brand=华为)
Product(id=5, title=华为 HUAWEI Mate20 X (5G) 7nm工艺5G旗舰芯片全面屏超大广角徕卡三摄8GB+256GB翡冷翠5G双模全网通手机, price=6199, intro=5G双模,支持SA/NSA网络,7.2英寸全景巨屏,石墨烯液冷散热】5G先驱,极速体验。, brand=华为)

可以发现,Elasticsearch中的分页是从第0页开始

提示

IndexCoordinates.of - 可以看到使用 search 方法中有个 IndexCoordinates 参数,这个参数是指定 查询的所有名称

PageRequest - 是用于构建分页请求,可以设置 分页参数、排序字段

排序

排序也可以通过NativeSearchQueryBuilder完成:

需求 - 根据价格排序

@Test
public void testSort() {
    // 构建查询条件
    NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder();
    // 添加基本的分词查询
    builder.withQuery(QueryBuilders.matchQuery("title", "华为"));

    // 排序
    builder.withSort(SortBuilders.fieldSort("price").order(SortOrder.DESC));
    SearchHits<Product> products = restTemplate.search(builder.build(), Product.class, IndexCoordinates.of("product"));
    products.forEach(System.out::println);
}

SortBuilders - 构建排序请求

聚合

需求 - 我们按照品牌brand进行分组:

@Test
public void testAgg(){
    NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder();
    // 1、添加一个新的聚合,聚合类型为terms,聚合名称为brands,聚合字段为brand
    builder.addAggregation(
        AggregationBuilders.terms("brands").field("brand"));
    // 2、查询
    SearchHits<Product> products = restTemplate.search(builder.build(), Product.class);

    // 3、解析
    Aggregations aggregations = products.getAggregations();
    Terms terms = aggregations.get("brands");
    // 3.2、获取桶
    if (terms.getBuckets().size() > 0) {
        // 3.3、遍历
        for (Terms.Bucket bk : terms.getBuckets()) {
            // 3.4、获取桶中的key,即品牌名称
            System.out.println(bk.getKeyAsString());
            // 3.5、获取桶中的文档数量
            System.out.println(bk.getDocCount());
        }
    }
}

输出结果如下:

Apple
3
华为
3
荣耀
3
小米
2
联想
1

关键API:

  • AggregationBuilders :聚合的构建工厂类。所有聚合都由这个类来构建,可以看看他的静态方法

我们看下页面的查询的JSON结果与Java类的对照关系:

6M22ns
6M22ns

嵌套聚合,求平均值

需求:根据品牌分组,并且计算它的平均售价

@Test
public void testSubAgg(){
    NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder();
    // 1、添加一个新的聚合,聚合类型为terms,聚合名称为brands,聚合字段为brand
    builder.addAggregation(
        AggregationBuilders.terms("brands").field("brand")
        .subAggregation(AggregationBuilders.avg("priceAvg").field("price")) // 在品牌聚合桶内进行嵌套聚合,求平均值
    );
    // 2、查询
    SearchHits<Product> products = restTemplate.search(builder.build(), Product.class);
    // 3、解析
    // 3.1、从结果中取出名为brands的那个聚合,
    Aggregations aggregations = products.getAggregations();
    Terms terms = aggregations.get("brands");
    // 3.2、获取桶
    if (terms.getBuckets().size() > 0) {
        // 3.3、遍历
        for (Terms.Bucket bucket : terms.getBuckets()) {
            // 3.4、获取桶中的key,即品牌名称  3.5、获取桶中的文档数量
            System.out.println(bucket.getKeyAsString() + ",共" + bucket.getDocCount() + "台");

            // 3.6.获取子聚合结果:
            Map<String, Aggregation> subMaps = bucket.getAggregations().asMap();
            ParsedAvg avg = (ParsedAvg) subMaps.get("priceAvg");
            System.out.println("平均售价:" + avg.getValue());
        }
    }

}

输出结果如下:

Apple,共3台
平均售价:8128.666666666667
华为,共3台
平均售价:5499.0
荣耀,共3台
平均售价:3649.0
小米,共2台
平均售价:4849.0
联想,共1台
平均售价:9299.0