ElasticSearch快速入门——下篇(在Java代码中操作ElasticSearch、JavaRestClient、操作索引库、操作文档、DSL查询、JavaRestClient查询、数据聚合)


阅读本文前可以先阅读我的另一篇博文: ElasticSearch快速入门——上篇(认识ElasticSearch、安装ElasticSearch、安装kibana、IK分词器、ElasticSearch中的基本概念、索引库操作、文档操作)

10. JavaRestClient

ElasticSearch 为我们提供了 Java 客户端,名为 JavaRestClient,为什么带有 Rest 关键字呢,因为这个客户端本质上就是帮我们发送 Restful 风格的 Http 请求


Elasticsearch 目前的最新版本是 8.x,其 Java 客户端有很大变化,特别是 API 方面,8.x 版本的 API 大都是响应式编程、与 lambda 表达式相关的 API

由于大多数企业使用的还是 8 以下的版本,所以我们选择使用早期的 JavaRestClient 客户端(虽然已被标记为过时)来学习

官方文档地址:Elasticsearch Clients

在这里插入图片描述

10.1 导入依赖

引入 ElasticSearch 的 RestHighLevelClient 依赖

<dependency>
    <groupId>org.elasticsearch.client</groupId>
    <artifactId>elasticsearch-rest-high-level-client</artifactId>
</dependency>

10.2 指定版本

如果是 SpringBoot 项目,会有一个默认的 ElasticSearch 版本,我们需要覆盖默认的 ElasticSearch 版本

在父工程的 pom.xml 文件中添加以下属性

<properties>
    <elasticsearch.version>7.17.18</elasticsearch.version>
</properties>

10.3 编写测试类

编写一个测试类,测试能否成功连接 ElasticSearch

import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

public class ElasticSearchTests {

    private RestHighLevelClient restHighLevelClient;

    @Test
    public void testConnect() {
        System.out.println(restHighLevelClient);
    }

    @BeforeEach
    public void setUp() {
        restHighLevelClient = new RestHighLevelClient(RestClient.builder(
                new HttpHost("127.0.0.1", 9200, "http")
        ));
    }

    @AfterEach
    public void tearDown() throws Exception {
        restHighLevelClient.close();
    }

}

11. 创建索引库时如何编写 Mapping 映射

创建索引库前需要先准备好索引库的 Mapping 映射,我们以商品数据为例,分析该如何编写 Mapping 映射

我们要实现商品搜索,那么索引库的字段肯定要满足页面搜索的需求,但是我们不能照搬商品表的结构作为 Mapping 映射的结构,因为商品表里面有非常多的字段,但这些字段不一定是索引库所需要的,如果直接照搬商品表的结构作为 Mapping 映射的结构,会增加 ElasticSearch 的存储负担

编写 Mapping 字段,需要结合我们的业务逻辑,我们以商品搜索页面为例来分析

在这里插入图片描述

以下是商品表的结构

在这里插入图片描述

根据搜索逻辑,我们可以判断出表中的哪些字段需要保存到 ElasticSearch 中,哪些字段参与搜索

在这里插入图片描述

根据分析,我们可以得到以下创建索引库的语句

PUT /shopping_mall
{
  "mappings": {
    "properties": {
      "id": {
        "type": "keyword"
      },
      "name": {
        "type": "text",
        "analyzer": "ik_smart"
      },
      "price": {
        "type": "integer"
      },
      "image": {
        "type": "keyword",
        "index": false
      },
      "category": {
        "type": "keyword"
      },
      "brand": {
        "type": "keyword"
      },
      "sold": {
        "type": "integer"
      },
      "commentCount": {
        "type": "integer",
        "index": false
      },
      "isAD": {
        "type": "boolean"
      },
      "updateTime": {
        "type": "date"
      }
    }
  }
}

12. JavaRestClient 操作索引库

12.1 新增索引库

新增索引库的语法如下

在这里插入图片描述

其中的 indices 是 index 的复数形式,indices 方法会返回一个对象,该对象中包含了操作索引库的所有方法


具体的 Java 代码如下

private static final String MAPPING_TEMPLATE = """
        {
          "mappings": {
            "properties": {
              "id": {
                "type": "keyword"
              },
              "name": {
                "type": "text",
                "analyzer": "ik_smart"
              },
              "price": {
                "type": "integer"
              },
              "image": {
                "type": "keyword",
                "index": false
              },
              "category": {
                "type": "keyword"
              },
              "brand": {
                "type": "keyword"
              },
              "sold": {
                "type": "integer"
              },
              "commentCount": {
                "type": "integer",
                "index": false
              },
              "isAD": {
                "type": "boolean"
              },
              "updateTime": {
                "type": "date",
                "format": ["yyyy-MM-dd HH:mm:ss"]
              }
            }
          }
        }""";

@Test
public void testCreateIndex() throws IOException {
    // 1.创建 CreateIndexRequest 对象
    CreateIndexRequest createIndexRequest = new CreateIndexRequest("shopping_mall");

    // 2.指定请求参数,其中 MAPPING_TEMPLATE 是一个静态常量字符串,内容是 JSON 格式的请求
    createIndexRequest.source(MAPPING_TEMPLATE, XContentType.JSON);

    // 3.发起请求
    restHighLevelClient.indices().create(createIndexRequest, RequestOptions.DEFAULT);
}

12.2 查询索引库

查询索引库的语法如下

在这里插入图片描述


具体的 Java 代码

注意:GetIndexRequest 类的包名为 org.elasticsearch.client.indices.GetIndexRequest

@Test
public void testGetIndex() throws IOException {
    // 1.创建 GetIndexRequest 对象
    GetIndexRequest shoppingMall = new GetIndexRequest("shopping_mall");

    // 2.发起请求
    boolean exists = restHighLevelClient.indices().exists(shoppingMall, RequestOptions.DEFAULT);
    System.err.println("exists = " + exists);
}

12.3 删除索引库

删除索引库的语法如下

在这里插入图片描述


具体的 Java 代码如下

@Test
public void testDeleteIndex() throws IOException {
    // 1.创建 DeleteIndexRequest 对象
    DeleteIndexRequest shoppingMall = new DeleteIndexRequest("shopping_mall");

    // 2.发起请求
    AcknowledgedResponse acknowledgedResponse = restHighLevelClient.indices().delete(shoppingMall, RequestOptions.DEFAULT);
    System.err.println("acknowledged = " + acknowledgedResponse.isAcknowledged());
}

12.4 查看当前有哪些索引库

在 kibana 提供的 Dev Tools 控制台中

GET _cat/indices?v

13. JavaRestClient 操作文档

我们新建一个名为 ElasticSearchDocumentTests 的测试类,在这个类中进行文档的 CRUD 操作

import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;

public class ElasticSearchDocumentTests {

    private RestHighLevelClient restHighLevelClient;

    @BeforeEach
    public void setUp() {
        restHighLevelClient = new RestHighLevelClient(RestClient.builder(
                new HttpHost("127.0.0.1", 9200, "http")
        ));
    }

    @AfterEach
    public void tearDown() throws Exception {
        restHighLevelClient.close();
    }

}

13.1 新增文档

新增文档的 Java API 如下

在这里插入图片描述


具体的 Java 代码

private String sourceString = """
        {
          "id": "1",
          "name": "小米11",
          "price": 3999,
          "image": "https://img.alicdn.com/imgextra/i4/O1CN01LX6jqs1E5W8Y8X1qG_!!6000000001648-2-tps-200-200.png",
          "category": "手机",
          "brand": "小米",
          "sold": 100,
          "commentCount": 100,
          "isAD": true,
          "updateTime": "2021-01-01"
        }""";

@Test
public void testCreateDocument() throws IOException {
    // 1.准备 IndexRequest 对象
    IndexRequest indexRequest = new IndexRequest("shopping_mall").id("1");

    // 2.准备请求参数
    indexRequest.source(sourceString, XContentType.JSON);

    // 3.发送请求
    IndexResponse indexResponse = restHighLevelClient.index(indexRequest, RequestOptions.DEFAULT);
    System.err.println("新增文档操作的返回结果 = " + indexResponse.getResult());
}

注意:

  • 在实际业务中,会有一个专门的实体类用于往索引库中新增文档
  • 将实体类转换成 JSON 格式的字符串可以使用 Alibaba 提供的 fastjson2 工具

13.2 查询文档

查询文档包含查询和解析响应结果两部分,对应的 Java API 如下

在这里插入图片描述


具体的 Java 代码

@Test
public void testGetDocument() throws IOException {
    // 1.准备 GetRequest 对象
    GetRequest getRequest = new GetRequest("shopping_mall", "1");

    // 2.发送请求
    GetResponse getResponse = restHighLevelClient.get(getRequest, RequestOptions.DEFAULT);
    System.err.println("getResponse = " + getResponse);
    System.err.println("source = " + getResponse.getSourceAsString());
}

13.3 删除文档

删除文档的 Java API 如下

在这里插入图片描述


具体的 Java 代码

@Test
public void testDeleteDocument() throws IOException {
    // 1.准备 DeleteRequest 对象
    DeleteRequest deleteRequest = new DeleteRequest("shopping_mall", "1");

    // 2.发送请求
    DeleteResponse deleteResponse = restHighLevelClient.delete(deleteRequest, RequestOptions.DEFAULT);
    System.err.println("deleteResponse = " + deleteResponse.getResult());
}

13.4 修改文档

修改文档数据有两种方式:

  1. 方式一:全量更新,再次写入 id 一样的文档,就会制除旧文档,添加新文档,与新增的 Java API 一致(新增时返回的结果为 created ,全量更新时返回的结果为 updated)
  2. 方式二:局部更新,只更新指定部分字段

局部更新的 Java API 如下

在这里插入图片描述


具体的 Java 代码

@Test
public void testUpdateDocument() throws IOException {
    // 1.准备 UpdateRequest 对象
    UpdateRequest updateRequest = new UpdateRequest("shopping_mall", "1");

    // 2.准备请求参数
    HashMap<String, Object> stringObjectHashMap = new HashMap<>();
    stringObjectHashMap.put("sold", 101);
    updateRequest.doc(stringObjectHashMap, XContentType.JSON);

    // 3.发送请求
    UpdateResponse updateResponse = restHighLevelClient.update(updateRequest, RequestOptions.DEFAULT);
    System.err.println("局部更新文档操作的返回结果 = " + updateResponse.getResult());
}

13.5 文档批处理(可以进行不同类型的文档操作)

批处理代码流程与之前类似,不过构建请求会用到一个名为 BulkRequest 的类来封装普通的 CRUD 请求

具体的 Java API 如下

在这里插入图片描述


具体的 Java 代码

@Test
public void testBulk() throws IOException {
    // 1.准备 BulkRequest 对象
    BulkRequest bulkRequest = new BulkRequest();

    // 2.准备请求参数
    // 2.1 新增文档
    HashMap<String, Object> indexMap = new HashMap<>();
    indexMap.put("sold", 102);
    bulkRequest.add(new IndexRequest("shopping_mall").id("1").source(indexMap, XContentType.JSON));
    // 2.2 修改文档
    HashMap<String, Object> updateMap = new HashMap<>();
    updateMap.put("commentCount", 103);
    bulkRequest.add(new UpdateRequest("shopping_mall", "1").doc(updateMap, XContentType.JSON));

    // 3.发起 bulk 请求
    BulkResponse bulkResponse = restHighLevelClient.bulk(bulkRequest, RequestOptions.DEFAULT);
    System.err.println("批量处理文档操作的返回结果如下");
    Arrays.stream(bulkResponse.getItems()).forEach(item -> System.err.println(item.getResponse()));
}

13.6 利用 JavaRestClient 批量新增文档

在批量新增文档前,我们先准备一些数据

13.6.1 建表

首先,创建一个名为 item 的表,建表语句如下

/*
 Navicat Premium Data Transfer

 Source Server         : localhost
 Source Server Type    : MySQL
 Source Server Version : 80034 (8.0.34)
 Source Host           : localhost:3306
 Source Schema         : blog

 Target Server Type    : MySQL
 Target Server Version : 80034 (8.0.34)
 File Encoding         : 65001

 Date: 25/08/2024 01:59:24
*/

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for item
-- ----------------------------
DROP TABLE IF EXISTS `item`;
CREATE TABLE `item`  (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '商品id',
  `name` varchar(200) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL COMMENT 'SKU名称',
  `price` int NOT NULL DEFAULT 0 COMMENT '价格(分)',
  `stock` int UNSIGNED NOT NULL COMMENT '库存数量',
  `image` varchar(200) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL COMMENT '商品图片',
  `category` varchar(200) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL COMMENT '类目名称',
  `brand` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL COMMENT '品牌名称',
  `spec` varchar(200) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL COMMENT '规格',
  `sold` int NULL DEFAULT 0 COMMENT '销量',
  `comment_count` int NULL DEFAULT 0 COMMENT '评论数',
  `isAD` tinyint(1) NULL DEFAULT 0 COMMENT '是否是推广广告,true/false',
  `status` int NULL DEFAULT 2 COMMENT '商品状态 1-正常,2-下架,3-删除',
  `create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `creater` bigint NULL DEFAULT NULL COMMENT '创建人',
  `updater` bigint NULL DEFAULT NULL COMMENT '修改人',
  PRIMARY KEY (`id`) USING BTREE,
  INDEX `status`(`status` ASC) USING BTREE,
  INDEX `updated`(`update_time` ASC) USING BTREE,
  INDEX `category`(`category` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 100002672305 CHARACTER SET = utf8mb3 COLLATE = utf8mb3_general_ci COMMENT = '商品表' ROW_FORMAT = COMPACT;

SET FOREIGN_KEY_CHECKS = 1;

13.6.2 导入数据

运行 SQL 文件,导入数据,共有 88476 条数据(如果需要 SQL 文件,可以私聊我)

13.6.3 编写实体类

编写 service 类前,需要先导入 MyBatisPlus 的 Maven 依赖

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.7</version>
</dependency>
Item.java
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;

import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;

@TableName("item")
public class Item implements Serializable {

    @Serial
    private static final long serialVersionUID = 1L;

    /**
     * 商品id
     */
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    /**
     * SKU名称
     */
    private String name;

    /**
     * 价格(分)
     */
    private Integer price;

    /**
     * 库存数量
     */
    private Integer stock;

    /**
     * 商品图片
     */
    private String image;

    /**
     * 类目名称
     */
    private String category;

    /**
     * 品牌名称
     */
    private String brand;

    /**
     * 规格
     */
    private String spec;

    /**
     * 销量
     */
    private Integer sold;

    /**
     * 评论数
     */
    private Integer commentCount;

    /**
     * 是否是推广广告,true/false
     */
    @TableField("isAD")
    private Boolean isAD;

    /**
     * 商品状态 1-正常,2-下架,3-删除
     */
    private Integer status;

    /**
     * 创建时间
     */
    private LocalDateTime createTime;

    /**
     * 更新时间
     */
    private LocalDateTime updateTime;

    /**
     * 创建人
     */
    private Long creater;

    /**
     * 修改人
     */
    private Long updater;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getPrice() {
        return price;
    }

    public void setPrice(Integer price) {
        this.price = price;
    }

    public Integer getStock() {
        return stock;
    }

    public void setStock(Integer stock) {
        this.stock = stock;
    }

    public String getImage() {
        return image;
    }

    public void setImage(String image) {
        this.image = image;
    }

    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 String getSpec() {
        return spec;
    }

    public void setSpec(String spec) {
        this.spec = spec;
    }

    public Integer getSold() {
        return sold;
    }

    public void setSold(Integer sold) {
        this.sold = sold;
    }

    public Integer getCommentCount() {
        return commentCount;
    }

    public void setCommentCount(Integer commentCount) {
        this.commentCount = commentCount;
    }

    public Boolean getIsAD() {
        return isAD;
    }

    public void setIsAD(Boolean AD) {
        isAD = AD;
    }

    public Integer getStatus() {
        return status;
    }

    public void setStatus(Integer status) {
        this.status = status;
    }

    public LocalDateTime getCreateTime() {
        return createTime;
    }

    public void setCreateTime(LocalDateTime createTime) {
        this.createTime = createTime;
    }

    public LocalDateTime getUpdateTime() {
        return updateTime;
    }

    public void setUpdateTime(LocalDateTime updateTime) {
        this.updateTime = updateTime;
    }

    public Long getCreater() {
        return creater;
    }

    public void setCreater(Long creater) {
        this.creater = creater;
    }

    public Long getUpdater() {
        return updater;
    }

    public void setUpdater(Long updater) {
        this.updater = updater;
    }

    @Override
    public String toString() {
        return "Item{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", price=" + price +
                ", stock=" + stock +
                ", image='" + image + '\'' +
                ", category='" + category + '\'' +
                ", brand='" + brand + '\'' +
                ", spec='" + spec + '\'' +
                ", sold=" + sold +
                ", commentCount=" + commentCount +
                ", isAD=" + isAD +
                ", status=" + status +
                ", createTime=" + createTime +
                ", updateTime=" + updateTime +
                ", creater=" + creater +
                ", updater=" + updater +
                '}';
    }

}
ItemDocument.java
import java.time.LocalDateTime;

/**
 * 索引库实体类
 */
public class ItemDocument {
    
    private Long id;

    private String name;

    private Integer price;

    private Integer stock;

    private String image;

    private String category;

    private String brand;

    private Integer sold;

    private Integer commentCount;

    private Boolean isAD;

    private LocalDateTime updateTime;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getPrice() {
        return price;
    }

    public void setPrice(Integer price) {
        this.price = price;
    }

    public Integer getStock() {
        return stock;
    }

    public void setStock(Integer stock) {
        this.stock = stock;
    }

    public String getImage() {
        return image;
    }

    public void setImage(String image) {
        this.image = image;
    }

    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 Integer getSold() {
        return sold;
    }

    public void setSold(Integer sold) {
        this.sold = sold;
    }

    public Integer getCommentCount() {
        return commentCount;
    }

    public void setCommentCount(Integer commentCount) {
        this.commentCount = commentCount;
    }

    public Boolean getIsAD() {
        return isAD;
    }

    public void setIsAD(Boolean AD) {
        isAD = AD;
    }

    public LocalDateTime getUpdateTime() {
        return updateTime;
    }

    public void setUpdateTime(LocalDateTime updateTime) {
        this.updateTime = updateTime;
    }

    @Override
    public String toString() {
        return "ItemDocument{" +
                "id='" + id + '\'' +
                ", name='" + name + '\'' +
                ", price=" + price +
                ", stock=" + stock +
                ", image='" + image + '\'' +
                ", category='" + category + '\'' +
                ", brand='" + brand + '\'' +
                ", sold=" + sold +
                ", commentCount=" + commentCount +
                ", isAD=" + isAD +
                ", updateTime=" + updateTime +
                '}';
    }

}

13.6.4 编写配置文件

编写配置文件前,先导入 MySQL 连接驱动的 Maven 依赖

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

application.yaml

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/blog?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: 123456

编写完配置文件后,在项目的启动类上添加 @MapperScan 注解,指定 Mapper 所在的包

@MapperScan("cn.edu.scau.mapper")

13.6.5 编写 Mapper 类和 Service 类

ItemMapper.java

import cn.edu.scau.pojo.Item;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;

public interface ItemMapper extends BaseMapper<Item> {

}

ItemService.java

import cn.edu.scau.pojo.Item;
import com.baomidou.mybatisplus.extension.service.IService;

public interface ItemService extends IService<Item> {

}

ItemServiceImpl.java

import cn.edu.scau.mapper.ItemMapper;
import cn.edu.scau.pojo.Item;
import cn.edu.scau.service.ItemService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;


@Service
public class ItemServiceImpl extends ServiceImpl<ItemMapper, Item> implements ItemService {

}

完成上述工作后,编写一个测试类,检查 ItemServiceImpl 类能否正常工作

import cn.edu.scau.service.ItemService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public class ItemServiceTests {

    @Autowired
    private ItemService itemService;

    @Test
    public void test() {
        System.out.println(itemService.getById(317578L));
    }

}

13.6.6 批量新增文档

新增文档前,需要先引入 PageHelper 和 fastjson2 的 Maven 依赖

PageHelper

<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
    <version>2.1.0</version>
    <exclusions>
        <exclusion>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
        </exclusion>
    </exclusions>
</dependency>

fastjson2

<dependency>
    <groupId>com.alibaba.fastjson2</groupId>
    <artifactId>fastjson2</artifactId>
    <version>2.0.50</version>
</dependency>

由于数据库中有 87476 条数据,肯定不能一次性全部导入 ElasticSearch ,需要分批次导入

import cn.edu.scau.pojo.Item;
import cn.edu.scau.pojo.ItemDocument;
import cn.edu.scau.service.ItemService;
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.github.pagehelper.PageHelper;
import org.apache.http.HttpHost;
import org.elasticsearch.action.bulk.BulkRequest;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.xcontent.XContentType;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.List;


@SpringBootTest
public class BulkInsertDocumentTests {

    private RestHighLevelClient restHighLevelClient;

    @Autowired
    private ItemService itemService;

    @Test
    public void testBulkInsertDocument() throws Exception {
        int pageNumber = 1;
        int pageSize = 500;
        while (true) {
            // 1.准备文档数据
            QueryWrapper<Item> queryWrapper = new QueryWrapper<>();
            queryWrapper.lambda().eq(Item::getStatus, 1);
            PageHelper.startPage(pageNumber, pageSize);
            List<Item> itemList = itemService.list(queryWrapper);

            if (itemList == null || itemList.isEmpty()) {
                return;
            }

            // 2.准备 BulkRequest 对象
            BulkRequest bulkRequest = new BulkRequest();

            // 3.准备请求参数
            ItemDocument itemDocument;
            for (Item item : itemList) {
                itemDocument = new ItemDocument();
                BeanUtils.copyProperties(item, itemDocument);
                bulkRequest.add(new IndexRequest("shopping_mall")
                        .id(item.getId().toString())
                        .source(JSON.toJSONString(itemDocument), XContentType.JSON));
            }

            // 4.发送请求
            restHighLevelClient.bulk(bulkRequest, RequestOptions.DEFAULT);

            // 5.翻页
            pageNumber++;
        }
    }

    @BeforeEach
    public void setUp() {
        restHighLevelClient = new RestHighLevelClient(RestClient.builder(
                new HttpHost("127.0.0.1", 9200, "http")
        ));
    }

    @AfterEach
    public void tearDown() throws Exception {
        restHighLevelClient.close();
    }

}

批量新增文档总耗时 34 秒 2 毫秒(耗时较慢,可以使用多线程 + 线程池的方案进行优化),我们来验证一下数据是否已经全部导入成功

返回的结果

{
  "count" : 88476,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  }
}

item 表中共有 88476 条数据,其中有一条数据的 status 字段为 2 ,表示该商品已下架,再加上我们新增的小米手机数据,所以总共是 88476 条数据

13.6.7 可能遇到的问题

如果你在批量新增文档时遇到了以下错误,是因为 ElasticSearch 拒绝了执行该次批量操作请求

{“error”:{“root_cause”:[{“type”:“es_rejected_execution_exception”,“reason”:“rejected execution of coordinating operation [coordinating_and_primary_bytes=0, replica_bytes=0, all_bytes=0, coordinating_operation_bytes=54034916, max_coordinating_and_primary_bytes=53687091]”}],“type”:“es_rejected_execution_exception”,“reason”:“rejected execution of coordinating operation [coordinating_and_primary_bytes=0, replica_bytes=0, all_bytes=0, coordinating_operation_bytes=54034916, max_coordinating_and_primary_bytes=53687091]”},“status”:429}
at org.elasticsearch.client.RestClient.convertResponse(RestClient.java:347)
at org.elasticsearch.client.RestClient.performRequest(RestClient.java:313)
at org.elasticsearch.client.RestClient.performRequest(RestClient.java:288)
at org.elasticsearch.client.RestHighLevelClient.performClientRequest(RestHighLevelClient.java:2699)
at org.elasticsearch.client.RestHighLevelClient.internalPerformRequest(RestHighLevelClient.java:2171)
… 74 more

因为本次批量操作所需的内存大小超过了配置的最大限制,也就是说,ElasticSearch 为了防止内存溢出或资源耗尽,拒绝了该次请求


解决方法:减小分页查询时每一页的数据条数

14. DSL(Domain SpecificLanguage) 查询

前面我们都是根据 id 来查询文档的,这种查询方式满足不了我们的业务要求,比如想京东这样的电商网站,我们搜索商品的时候,搜索条件往往比较复杂,为了实现复杂搜索,我们需要一种新的搜索方式

ElasticSearch 为我们提供了 DSL 查询来实现复杂搜索

14.1 快速入门

DSL:Domain Specific Language,以 JSON 格式来定义查询条件,以下是一个示例

在这里插入图片描述

大家看到上面的请求,是不是一下就懵了,怎么这么复杂

不用担心,这是最终形态,我们在学习的时候肯定是一步一步拆分,从易到难进行学习


DSL 查询可以分为两大类:

  • 叶子查询(Leaf query clauses):一般是在特定的字段里查询特定值,属于简单查询,很少单独使用
  • 复合查询(Compound query clauses):以逻辑方式组合多个叶子查询或者更改叶子查询的行为方式

查询以后,还可以对查询的结果做处理,包括:

  • 排序:按照 1 个或多个字段值做排序
  • 分页:根据 from 和 size 做分页,类似于 MySQL 的分页
  • 高亮:对搜索结果中的关键字添加特殊样式,使其更加醒目
  • 聚合:对搜索结果做数据统计以形成报表

基于 DSL 的查询语法如下

在这里插入图片描述

在这里插入图片描述


我们先在 Dev Tools 中进行一个 DSL 查询

GET /shopping_mall/_search
{
  "query": {
    "match_all": {}
  }
}

查询结果

{
  "took" : 0,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "shopping_mall",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 1.0,
        "_source" : {
          "sold" : 102,
          "commentCount" : 103
        }
      }
    ]
  }
}

以下是对查询结果的解释

在这里插入图片描述

  • Elastic Search 单次查询涉及的数据量默认不能超过一万
  • Elastic Search 单次查询默认只会返回 10 条数据

14.2 叶子查询

叶子查询还可以进一步细分,常见的有:

  • 全文检索(fulltext)查询:利用分词器对用户输入的内容分词,然后去词条列表中匹配,例如:
    • match_query
    • multi_match_query
  • 精确查询:不对用户输入内容分词,直接精确匹配,一般是查找keyword、数值、日期、布尔等类型,例如:
    • ids
    • range
    • term
  • 地理(geo)查询:用于搜索地理位置,搜索方式很多,例如(地理查询可用于实现搜索附近的人、搜索附近的车等功能):
    • geo_distance
    • geo_bounding_box

14.2.1 全文检索查询

参与全文检索查询的字段最好也是可以分词的(类型为 text 的字段),这样才可以找到与用户搜索内容匹配度更高的文档

14.2.1.1 match 查询

match 查询:全文检索查询的一种,会对用户输入内容分词,然后去倒排索引库检索,语法如下

在这里插入图片描述

示例

GET /shopping_mall/_search
{
  "query": {
    "match": {
      "name": "脱脂牛奶"
    }
  }
}
14.2.1.2 multi_match 查询

multi_match:与 match 查询类似,只不过允许同时查询多个字段,语法如下(参与查询字段越多,查询性能越差

在这里插入图片描述

示例

GET /shopping_mall/_search
{
  "query": {
    "multi_match": {
      "query": "脱脂牛奶",
      "fields": ["name"]
    }
  }
}

14.2.2 精确查询

精确查询,英文是 Term-level query,顾名思义,词条级别的查询,也就是说不会对用户输入的搜索条件再分词,而是将搜索条件作为一个词条,与搜索的字段内容精确值匹配

精确查询适用于查找 keyword、数值、日期、boolean 类型的字段,例如 id、price、城市、地名、人名等作为一个整体才有含义的字段


精确查询主要有三种:

  1. term 查询
  2. range 查询
  3. id 查询
14.2.2.1 term 查询

term 查询的语法如下

在这里插入图片描述

示例

GET /shopping_mall/_search
{
  "query": {
    "term": {
      "brand": {
        "value": "德亚"
      }
    }
  }
}
14.2.2.2 range 查询

range 查询的语法如下

在这里插入图片描述

示例(价格以分为单位)

GET /shopping_mall/_search
{
  "query": {
    "range": {
      "price": {
        "gte": 500000,
        "lte": 1000000
      }
    }
  }
}
14.2.2.3 id 查询

示例

GET /shopping_mall/_search
{
  "query": {
    "ids": {
      "values": [
        "613359",
        "613360"
      ]
    }
  }
}

14.3 复合查询(布尔查询)

叶子查询是比较简单的单字段查询,在真实的业务场景下,往往都会有比较复杂的组合条件查询,这个时候就需要使用复合查询了

复合查询大致可以分为两类:

  • 第一类:基于逻辑运算组合叶子查询,实现组合条件,例如
    • bool
  • 第二类:基于某种算法修改查询时的文档相关性算分,从而改变文档排名,例如:
    • function_score
    • dis_max

布尔查询是一个或多个查询子句的组合,子查询的组合方式有:

  • must:必须匹配每个子查询,类似与
  • should:选择性匹配子查询,类似或
  • must_not:必须不匹配,不参与算分,类似非
  • filter:必须匹配,不参与算分

布尔查询的语法如下

在这里插入图片描述

我们来做一个小案例:搜索"智能手机",但品牌必须是华为,而且价格必须在 [900, 1599] 区间内

GET /shopping_mall/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "name": "智能手机"
          }
        }
      ],
      "filter": [
        {
          "term": {
            "brand": "华为"
          }
        },
        {
          "range": {
            "price": {
              "gte": 90000,
              "lte": 159900
            }
          }
        }
      ]
    }
  }
}

14.4 排序和分页

14.4.1 排序

Elastic Search 支持对搜索结果排序,默认是根据相关度算分(_score)来排序,也可以指定字段排序,可以排序的字段类型有:keyword 类型、数值类型、地理坐标类型、日期类型等

排序的语法如下

  • 如果有多个字段参与排序,会先按照第一个字段进行排序
  • 如果第一个字段相同,再按照第二个字段进行排序,以此类推

在这里插入图片描述

我们来做一个小案例:搜索商品,按照销量排序,销量一样则按照价格升序

GET /shopping_mall/_search
{
  "query": {
    "match_all": {}
  },
  "sort": [
    {
      "sold": {
        "order": "desc"
      }
    },
    {
      "price": {
        "order": "asc"
      }
    }
  ]
}

14.4.2 分页

Elastic Search 默认情况下只返回 top10 的数据,如果要查询更多数据,需要修改分页参数,Elastic Search 中通过修改 from、size 参数来控制要返回的分页结果:

  • from:从第几个文档开始
  • size:总共查询几个文档

分页的语法如下

在这里插入图片描述

我们来做一个小案例:查询出销量排名前 10 的商品,销量一样时按照价格升序

GET /shopping_mall/_search
{
  "query": {
    "match_all": {}
  },
  "from": 0,
  "size": 10,
  "sort": [
    {
      "sold": {
        "order": "desc"
      }
    },
    {
      "price": {
        "order": "asc"
      }
    }
  ]
}

14.5 深度分页问题

ElasticSearch 的数据一般会采用分片存储,也就是把一个索引中的数据分成 N 份,存储到不同节点上,查询数据时需要汇总各个分片的数据

假如我们要查第 100 页的数据,每页查 10 条,那 ElasticSearch 是如何找到前 1000 名中的最后 10 名的呢?

要找到前 1000 名的最后 10 名,需要先知道前 1000 名是谁,整体的思路有两步:

  1. 对数据进行排序
  2. 找出 [991, 1000] 名

那前 1000 个如何找呢(假如现在有四个分片),是不是我每个分片找 250 个就行了呢?实际上不能这么操作,因为数据是混乱地保存在不同的分片上的,不能保证每个分片的前 250 名加起来就是总的前 1000 名

举个通俗的例子,学校有 10 个班级,现在要找到年级前十名,是选出每个班的第一名就可以了吗,显然不是的,因为每个班级中每个学生都有学得好的和学得差的,班级的整体水平不一样,有可能某个班级的第一名在另一个班级中是垫底的

那我要怎么找到年级的前十名呢,最简单的做法就是对所有学生的成绩进行统计,找出前十名,但这种做法的效率可能不是很高,其实我们只需要找出每个班级的前十名,接着将每个班级前十名的学生汇总在一起,再找出这些汇总学生的前十名即可(就算是最极端的情况,即年级前十名都在同一个班级,也能正确地找出年级前十名)

ElasticSearch 的分页也是基于这个思想,要找出前 1000 名的数据,需要从每个分片中取出前 1000 名的数据,汇总后进行排序,找到真正的前 1000 名的数据

在这里插入图片描述

在这里插入图片描述

其实这种做法在数据量较大的情况下也有一定的问题,比如说我要查第 1 万页的数据,每页查 10 条,也就是要找前 10 万条数据,再找出排名在 [99990, 100000] 之间的数据

根据上述思想,就需要从每个分片中分别取出前 10 万条数据,汇总在一起,再筛选出真正的排名在 [99900, 100000] 之间的数据,这个数据量是非常恐怖的,很有可能导致内存直接炸裂


针对深度分页,Elastic Search 提供了两种解决方案(官方文档:search after):

  1. search after:分页时需要排序,原理是从上一次的排序值开始,查询下一页数据(官方推荐使用的方式)
  2. scroll:原理将排序数据形成快照,保存在内存,官方已经不推荐使用

search after 模式

  • 优点:没有查询上限,支持深度分页
  • 缺点:只能从前往后逐页查询,不能随机翻页
  • 应用场景:数据迁移、手机滚动查询

那传统分页的应用场景有哪些呢?

百度搜索就是传统分页的一个典型例子,这个时候,有同学就有疑惑了,像百度这样的搜索引擎,ElasticSearch 中不是存储了数以千万计的数据吗,百度搜索是怎么解决深度分页问题的呢?

实际上,百度搜索对分页做了限制,最多只能跳转到 77 页,也就是说,你最多只能得到几百条数据

事实上,我们平时在用百度搜索的时候,一般只会看前三页的内容,很少会查看后面的数据,所以说,针对深度分页问题,最简单的解决方法就是直接对页码做一个限制,为页码制定一个上限(很多软件都是这么解决深度分页问题的)


ElasticSearch 对传统分页的页码和每页多少条数据也做了一个限制(也就是 from 字段和 size 字段),from 字段和 size 字段组合起来所需要用到的数据不能超过 1 万条,我们可以做一个测试

在这里插入图片描述

14.6 高亮显示

高亮显示:在搜索结果中把搜索关键字突出显示

我们在用百度等搜索引擎时,我们的搜索条件会在搜索结果中高亮显示(一般是标为红色),那具体是怎么样实现的呢,其实在搜索结果中,我们的搜索条件会被一个 <em></em> 标签包裹起来

在这里插入图片描述

那前端怎么知道我要给哪些内容添加高亮呢,前端工程师最多只能控制页面的排版,不能提前知道数据的内容

其实,添加标签这一步是由 ElasticSearch 完成的,ElasticSearch 为我们提供了高亮显示搜索词的功能


高亮显示的语法如下

在这里插入图片描述

示例

GET /shopping_mall/_search
{
  "query": {
    "match": {
      "name": "脱脂牛奶"
    }
  },
  "highlight": {
    "fields": {
      "name": {
        "pre_tags": "<em>",
        "post_tags": "</em>"
      }
    }
  }
}

返回的结果如下

如果不指定标签,默认使用 <em></em> 标签


搜索的完整语法

在这里插入图片描述

15. JavaRestClient 查询

15.1 快速入门

数据搜索的 Java 代码我们分为两部分:

  1. 构建请求并发起请求
  2. 解析查询结果

在这里插入图片描述

在这里插入图片描述

具体的 Java 代码如下

import cn.edu.scau.pojo.ItemDocument;
import com.alibaba.fastjson2.JSON;
import org.apache.http.HttpHost;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.io.IOException;

public class SearchTests {

    private RestHighLevelClient restHighLevelClient;

    @Test
    public void testMatchAll() throws IOException {
        // 1.创建 SearchRequest 对象
        SearchRequest searchRequest = new SearchRequest("shopping_mall");

        // 2.配置 request 参数
        searchRequest.source().query(QueryBuilders.matchAllQuery());

        // 3.发送请求
        SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);

        // 4.解析结果
        SearchHits searchHits = searchResponse.getHits();
        // 4.1 获取总记录数
        long totalHits = searchHits.getTotalHits().value;
        System.err.println("总记录数 = " + totalHits);
        // 4.2 获取命中的数据
        for (SearchHit hit : searchHits) {
            // 4.2.1 获取 source 结果
            String sourceAsString = hit.getSourceAsString();
            // 4.2.2 转换为 ItemDocument 对象
            ItemDocument itemDocument = JSON.parseObject(sourceAsString, ItemDocument.class);
            System.err.println("itemDocument = " + itemDocument);
        }
    }

    @BeforeEach
    public void setUp() {
        restHighLevelClient = new RestHighLevelClient(RestClient.builder(
                new HttpHost("127.0.0.1", 9200, "http")
        ));
    }

    @AfterEach
    public void tearDown() throws Exception {
        restHighLevelClient.close();
    }

}

15.2 构造查询条件

在 JavaRestAPI 中,所有类型的 query 查询条件都是由 QueryBuilders 来构建的

在这里插入图片描述

全文检索的查询条件构造的 API 如下:

在这里插入图片描述

精确检索的查询条件构造的 API 如下:

在这里插入图片描述

布尔查询的查询条件构造 API 如下:

在这里插入图片描述

了解如何构造查询条件之后,我们来做一个小案例:利用 JavaRestClient 实现搜索功能,条件如下:

  • 搜索关键字为脱脂牛奶
  • 品牌必须为德亚
  • 价格必须低于 300 元

具体的 Java 代码

import cn.edu.scau.pojo.ItemDocument;
import com.alibaba.fastjson2.JSON;
import org.apache.http.HttpHost;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.io.IOException;

public class SearchTests {

    private RestHighLevelClient restHighLevelClient;

    @Test
    public void testSearch() throws IOException {
        // 1.创建 SearchRequest 对象
        SearchRequest searchRequest = new SearchRequest("shopping_mall");

        // 2.组织 DSL 参数
        searchRequest.source().query(QueryBuilders.boolQuery()
                .must(QueryBuilders.matchQuery("name", "脱脂牛奶"))
                .filter(QueryBuilders.termQuery("brand", "德亚"))
                .filter(QueryBuilders.rangeQuery("price").lt(30000))
        );

        // 3.发送请求
        SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);

        // 4.解析结果
        parseSearchResponse(searchResponse);
    }

    private static void parseSearchResponse(SearchResponse searchResponse) {
        SearchHits searchHits = searchResponse.getHits();
        // 4.1 获取总记录数
        long totalHits = searchHits.getTotalHits().value;
        System.err.println("总记录数 = " + totalHits);
        // 4.2 获取命中的数据
        for (SearchHit hit : searchHits) {
            // 4.2.1 获取 source 结果
            String sourceAsString = hit.getSourceAsString();
            // 4.2.2 转换为 ItemDocument 对象
            ItemDocument itemDocument = JSON.parseObject(sourceAsString, ItemDocument.class);
            System.err.println("itemDocument = " + itemDocument);
        }
    }

    @BeforeEach
    public void setUp() {
        restHighLevelClient = new RestHighLevelClient(RestClient.builder(
                new HttpHost("127.0.0.1", 9200, "http")
        ));
    }

    @AfterEach
    public void tearDown() throws Exception {
        restHighLevelClient.close();
    }

}

15.3 排序和分页

与 query 类似,排序和分页参数都是基于 request.source() 来设置的

在这里插入图片描述

具体的 Java 代码如下

@Test
public void testSortAndPage() throws IOException {
    // 0.模拟前端传递过来的分页参数
    int pageNumber = 1;
    int pageSize = 10;

    // 1.创建 SearchRequest 对象
    SearchRequest searchRequest = new SearchRequest("shopping_mall");

    // 2.组织 DSL 参数
    // 2.1 query条件
    searchRequest.source().query(QueryBuilders.matchAllQuery());
    // 2.2 分页
    searchRequest.source().from((pageNumber - 1) * pageSize).size(pageSize);
    // 2.3 排序
    searchRequest.source().sort("price", SortOrder.DESC);

    // 3.发送请求
    SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);

    // 4.解析结果
    parseSearchResponse(searchResponse);
}

private static void parseSearchResponse(SearchResponse searchResponse) {
    SearchHits searchHits = searchResponse.getHits();
    // 4.1 获取总记录数
    long totalHits = searchHits.getTotalHits().value;
    System.err.println("总记录数 = " + totalHits);
    // 4.2 获取命中的数据
    for (SearchHit hit : searchHits) {
        // 4.2.1 获取 source 结果
        String sourceAsString = hit.getSourceAsString();
        // 4.2.2 转换为 ItemDocument 对象
        ItemDocument itemDocument = JSON.parseObject(sourceAsString, ItemDocument.class);
        System.err.println("itemDocument = " + itemDocument);
    }
}

15.4 高亮显示

高亮显示的条件构造 API 如下

在这里插入图片描述

在这里插入图片描述

具体的 Java 代码

@Test
public void testHighlight() throws IOException {
    // 1.创建 SearchRequest 对象
    SearchRequest searchRequest = new SearchRequest("shopping_mall");

    // 2.组织 DSL 参数
    // 2.1 query条件
    searchRequest.source().query(QueryBuilders.matchQuery("name", "脱脂牛奶"));
    // 2.2 高亮条件
    searchRequest.source().highlighter(SearchSourceBuilder.highlight()
            .field("name")
            .preTags("<em>")
            .postTags("</em>")
    );

    // 3.发送请求
    SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);

    // 4.解析结果
    parseSearchResponse(searchResponse);
}

private static void parseSearchResponse(SearchResponse searchResponse) {
    SearchHits searchHits = searchResponse.getHits();
    // 4.1 获取总记录数
    long totalHits = searchHits.getTotalHits().value;
    System.err.println("总记录数 = " + totalHits);
    // 4.2 获取命中的数据
    for (SearchHit hit : searchHits) {
        // 4.2.1 获取 source 结果
        String sourceAsString = hit.getSourceAsString();
        // 4.2.2 转换为 ItemDocument 对象
        ItemDocument itemDocument = JSON.parseObject(sourceAsString, ItemDocument.class);

        // 4.3 处理高亮结果
        Map<String, HighlightField> highlightFields = hit.getHighlightFields();
        if (highlightFields != null && !highlightFields.isEmpty()) {
            HighlightField highlightField = highlightFields.get("name");
            String name = highlightField.getFragments()[0].toString();
            itemDocument.setName(name);
        }

        System.err.println("itemDocument = " + itemDocument);
    }
}

16. 数据聚合

ElasticSearch 不仅可以做数据的存储和搜索,还可以做海量数据的分析和运算,这种分析和运算的功能,被称为数据聚合

16.1 聚合的分类

聚合(aggregations)可以实现对文档数据的统计、分析、运算,常见的聚合有三类:

  • 桶(Bucket)聚合:用来对文档做分组
    • TermAggregation:按照文档字段值分组
    • Date Histogram:按照日期阶梯分组,例如一周为一组,或者一月为一组
  • 度量(Metric)聚合:用以计算一些值,比如最大值、最小值、平均值等
    • Avg:求平均值
    • Max:求最大值
    • Min:求最小值
    • Stats:同时求 max、min、avg、sum 等
  • 管道(pipeline)聚合:以其它聚合的结果为基础做聚合

注意事项:
参与聚合的字段必须是不能分词的字段,例如 keyword、数值、日期、布尔等类型的字段

16.2 DSL 实现聚合

16.2.1 无条件的聚合

我们来做一个小案例:统计所有商品中共有哪些商品分类,其实就是以分类(category)字段对数据分组,category 值一样的放在同一组,属于 Bucket 聚合中的 Term 聚合

DSL 的语法如下

在这里插入图片描述

示例

GET /shopping_mall/_search
{
  "query": {
    "match_all": {}
  },
  "size": 0,
  "aggs": {
    "categoryAggregation": {
      "terms": {
        "field": "category",
        "size": 20
      }
    }
  }
}

返回的结果

{
  "took" : 38,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 10000,
      "relation" : "gte"
    },
    "max_score" : null,
    "hits" : [ ]
  },
  "aggregations" : {
    "categoryAggregation" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [
        {
          "key" : "休闲鞋",
          "doc_count" : 20612
        },
        {
          "key" : "牛仔裤",
          "doc_count" : 19611
        },
        {
          "key" : "老花镜",
          "doc_count" : 16222
        },
        {
          "key" : "拉杆箱",
          "doc_count" : 14347
        },
        {
          "key" : "手机",
          "doc_count" : 10101
        },
        {
          "key" : "真皮包",
          "doc_count" : 3064
        },
        {
          "key" : "拉拉裤",
          "doc_count" : 1706
        },
        {
          "key" : "牛奶",
          "doc_count" : 1296
        },
        {
          "key" : "曲面电视",
          "doc_count" : 1219
        },
        {
          "key" : "硬盘",
          "doc_count" : 298
        }
      ]
    }
  }
}

16.2.2 有条件的聚合

默认情况下,Bucket 聚合是对索引库的所有文档做聚合,我们可以限定要聚合的文档范围,只要添加 query 条件即可

例如,我想知道价格高于 3000 元的手机品牌有哪些

在这里插入图片描述

示例

GET /shopping_mall/_search
{
  "query": {
    "bool": {
      "filter": [
        {
          "term": {
            "category": "手机"
          }
        },
        {
          "range": {
            "price": {
              "gt": 300000
            }
          }
        }
      ]
    }
  },
  "size": 0,
  "aggs": {
    "brandAggregation": {
      "terms": {
        "field": "brand",
        "size": 20
      }
    }
  }
}

16.2.3 度量聚合

除了对数据分组(Bucket)以外,我们还可以对每个 Bucket 内的数据进一步做数据计算和统计

例如:我想知道手机有哪些品牌,每个品牌的价格最小值、最大值、平均值

在这里插入图片描述

GET /shopping_mall/_search
{
  "query": {
    "term": {
      "category": "手机"
    }
  },
  "size": 0,
  "aggs": {
    "brandAggregation": {
      "terms": {
        "field": "brand",
        "size": 20
      },
      "aggs": {
        "priceStatistics": {
          "stats": {
            "field": "price"
          }
        }
      }
    }
  }
}

16.3 Java 客户端实现聚合

聚合三要素:

  1. 聚合类型
  2. 聚合名称
  3. 聚合字段

我们以查询品牌为例

在这里插入图片描述

在这里插入图片描述

具体的 Java 代码

Terms 类所在的包为 org.elasticsearch.search.aggregations.bucket.terms.Terms

@Test
public void testAggregation() throws IOException {
    // 1.创建 SearchRequest 对象
    SearchRequest searchRequest = new SearchRequest("shopping_mall");

    // 2.组织 DSL 参数
    // 2.1 分页
    searchRequest.source().size(0);
    // 2.2 聚合条件
    String brandAggregationName = "brandAggregationName";
    searchRequest.source().aggregation(
            AggregationBuilders.terms(brandAggregationName).field("brand").size(20)
    );

    // 3.发送请求
    SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);

    // 4.解析结果
    Aggregations aggregations = searchResponse.getAggregations();
    // 4.1 根据聚合名称获取对应的聚合结果
    Terms brandTerms = aggregations.get(brandAggregationName);
    // 4.2 获取buckets
    List<? extends Terms.Bucket> buckets = brandTerms.getBuckets();
    // 4.3 遍历获取每一个bucket
    for (Terms.Bucket bucket : buckets) {
        System.out.println("brand = " + bucket.getKeyAsString());
        System.out.println("docCount = " + bucket.getDocCount());
    }
}

注意事项:

  • aggregations.get(brandAggregationName) 方法返回的是顶层的 Aggregation 接口,由于 aggregations.get(String name) 是一个统一的 API ,也就是说,设计者在设计这个 API 的时候不知道使用者会使用哪种聚合,所以使用顶层的 Aggregation 接口来接收
  • Aggregation 接口有很多的子子孙孙,代表不同类型的聚合,我们使用的 Terms 接口实现了 Aggregation 接口,Terms 接口中有获取桶的方法
  • 不同类型的聚合,结果也大不相同,我们使用的是 term 分组聚合,所以才能得到桶,如果不是使用 term 分组聚合,结果中不会有桶,所以顶层接口中不会有获取桶的方法

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

聂 可 以

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值