赞
踩
elasticsearch是一款非常强大的开源搜索引擎,具备非常多强大功能,可以帮助我们从海量数据中快速找到需要的内容
我们会从下面几个角度来讲解ElasticSearch:
首先我们先来简单介绍一下ElasticSearch
我们首先来简单介绍一下ES:
我们给出ELK的组成部分:
我们给出一张结构图来表示ELK的整体结构:
我们如果需要学习ES,那么首先就需要了解ES的核心机制——倒排索引
我们首先来介绍正向索引:
首先我们还需要了解倒排索引的一些关键字:
那么我们再来介绍倒排索引:
我们可以对两者做出一个简单的比较:
正向索引优点:可以给多个字段创建索引;根据索引字段搜索、排序速度非常快
正向索引缺点:根据非索引字段,或者索引字段中的部分词条查找时,只能全表扫描。
倒排索引优点:根据词条搜索、模糊搜索时,速度非常快
倒排索引缺点:只能给词条创建索引,而不是字段;无法根据字段做排序
我们来介绍一些ES中的核心概念:
索引实际上对标MySQL的数据库,一个索引就是一个具体的数据库
映射实际上对标MySQL的约束信息,用于对索引进行一定条件的限制
通俗来讲:索引就是就是相同类型的文档的集合,映射是索引中文档的字段约束信息
我们将ES和MySQL进行一个简单的对比,我们会发现两者结构上非常相似:
MySQL | Elasticsearch | 说明 |
---|---|---|
Table | Index | 索引(index),就是文档的集合,类似数据库的表(table) |
Row | Document | 文档(Document),就是一条条的数据,类似数据库中的行(Row),文档都是JSON格式 |
Column | Field | 字段(Field),就是JSON文档中的字段,类似数据库中的列(Column) |
Schema | Mapping | Mapping(映射)是索引中文档的约束,例如字段类型约束。类似数据库的表结构(Schema) |
SQL | DSL | DSL是elasticsearch提供的JSON风格的请求语句,用来操作elasticsearch,实现CRUD |
而在实际使用上,两者有不同的特点:
此外两者还可以结合使用:
既然要使用ES,那么我们首先需要下载ES:
docker network create es-net
- # 导入数据
- docker load -i es.tar
- docker run -d \
- --name es \
- -e "ES_JAVA_OPTS=-Xms512m -Xmx512m" \
- -e "discovery.type=single-node" \
- -v es-data:/usr/share/elasticsearch/data \
- -v es-plugins:/usr/share/elasticsearch/plugins \
- --privileged \
- --network es-net \
- -p 9200:9200 \
- -p 9300:9300 \
- elasticsearch:7.12.1
-
- # 命令解释:
-
- # - `-e "cluster.name=es-docker-cluster"`:设置集群名称
- # - `-e "http.host=0.0.0.0"`:监听的地址,可以外网访问
- # - `-e "ES_JAVA_OPTS=-Xms512m -Xmx512m"`:内存大小
- # - `-e "discovery.type=single-node"`:非集群模式
- # - `-v es-data:/usr/share/elasticsearch/data`:挂载逻辑卷,绑定es的数据目录
- # - `-v es-logs:/usr/share/elasticsearch/logs`:挂载逻辑卷,绑定es的日志目录
- # - `-v es-plugins:/usr/share/elasticsearch/plugins`:挂载逻辑卷,绑定es的插件目录
- # - `--privileged`:授予逻辑卷访问权
- # - `--network es-net` :加入一个名为es-net的网络中
- # - `-p 9200:9200`:端口映射配置
然后我们还需要去部署一个kibana,kibana可以给我们提供一个elasticsearch的可视化界面,便于我们学习:
- # 导入数据
- docker load -i kibana.tar
- docker run -d \
- --name kibana \
- -e ELASTICSEARCH_HOSTS=http://es:9200 \
- --network=es-net \
- -p 5601:5601 \
- kibana:7.12.1
-
- # 命令解释:
-
- # - `--network es-net` :加入一个名为es-net的网络中,与elasticsearch在同一个网络中
- # - `-e ELASTICSEARCH_HOSTS=http://es:9200"`:设置elasticsearch的地址,因为kibana已经与elasticsearch在一个网络,因此可以用容器名直接访问elasticsearch
- # - `-p 5601:5601`:端口映射配置
最后还需要一个IK分词器,它可以帮助我们去完成中文的分词功能:
- # 进入容器内部
- docker exec -it elasticsearch /bin/bash
-
- # 在线下载并安装
- ./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.12.1/elasticsearch-analysis-ik-7.12.1.zip
-
- #退出
- exit
- #重启容器
- docker restart elasticsearch
我们首先来介绍对ES索引库的操作
我们首先需要去介绍ES索引库的Mapping:
我们下面来一一介绍:
TYPE名称 | TYPE含义 |
---|---|
text | 字符串(可以被划分,可分词的文本) |
keyword | 字符串(不可被划分,精确值,例如:品牌、国家、ip地址) |
long、integer、short、byte、double、float | 常见数值类型 |
boolean | 布尔值 |
date | 日期值 |
object | 对象 |
我们给出一个简单举例:
- {
- "age": 21,// 类型为 integer;参与搜索,因此需要index为true;无需分词器
- "weight": 52.1,
- "isMarried": false,
- "info": "河南师范大学",// 类型为字符串,需要分词,因此是text;参与搜索,因此需要index为true;分词器可以用ik_smart
- "email": "zy@hsd.cn",// 类型为字符串,但是不需要分词,因此是keyword;不参与搜索,因此需要index为false;无需分词器
- "score": [99.1, 99.5, 98.9],
- "name": {
- "firstName": "云",
- "lastName": "赵"
- }
- }
这里我们统一使用Kibana编写DSL的方式来演示
下面我们来介绍创建索引库的说明,架构和案例:
- /*
- - 请求方式:PUT
- - 请求路径:/索引库名,可以自定义
- - 请求参数:mapping映射
- */
-
- /* 架构 */
-
- PUT /索引库名称
- {
- "mappings": {
- "properties": {
- "字段名":{
- "type": "text",
- "analyzer": "ik_smart"
- },
- "字段名2":{
- "type": "keyword",
- "index": "false"
- },
- "字段名3":{
- "properties": {
- "子字段": {
- "type": "keyword"
- }
- }
- },
- }
- }
- }
-
- /* 案例 */
-
- PUT /qiuluo
- {
- "mappings": {
- "properties": {
- "info":{
- "type": "text",
- "analyzer": "ik_smart"
- },
- "email":{
- "type": "keyword",
- "index": "falsae"
- },
- "name":{
- "properties": {
- "firstName": {
- "type": "keyword"
- }
- }
- }
- }
- }
- }
首先我们给出一个简单案例:
- /*
- - 请求方式:GET
- - 请求路径:/索引库名
- - 请求参数:无
- */
-
- /* 架构 */
-
- GET /索引库名
-
- /* 案例 */
-
- GET /qiuluo
我们需要注意索引库是无法修改已存在的结构的,但是可以对索引库进行新增操作:
- /*
- - 请求方式:PUT
- - 请求路径:/索引库名/_mapping
- - 请求参数:修改内容
- */
-
- /* 架构 */
-
- PUT /索引库名/_mapping
- {
- "properties": {
- "新字段名":{
- "type": "integer"
- }
- }
- }
删除索引库和前两个的语法基本相似:
- /*
- - 请求方式:DELETE
- - 请求路径:/索引库名
- - 请求参数:无
- */
-
- /* 架构 */
-
- DELETE /索引库名
下面我们来介绍ES的文档操作
其中在索引库和文档之间原本还有一层Type:
这里我们统一使用Kibana编写DSL的方式来演示
我们同样直接给出具体的解释和代码:
- /*
- - 请求方式:POST
- - 请求路径:/索引库名/_doc/文档id
- - 请求参数:具体的字段值和存储值
- */
-
- /* 模板 */
-
- POST /索引库名/_doc/文档id
- {
- "字段1": "值1",
- "字段2": "值2",
- "字段3": {
- "子属性1": "值3",
- "子属性2": "值4"
- },
- // ...
- }
-
- /* 举例 */
-
- POST /qiuluo/_doc/1
- {
- "info": "河南师范大学",
- "email": "zy@hsd.cn",
- "name": {
- "firstName": "云",
- "lastName": "赵"
- }
- }
我们同样以DSL语句书写查询文档代码:
- /*
- - 请求方式:GET
- - 请求路径:/索引库名/_doc/文档id
- - 请求参数:无
- */
-
- /* 模板 */
-
- GET /{索引库名称}/_doc/{id}
-
- /* 举例 */
-
- GET /qiuluo/_doc/1
删除文档的格式和查询文档的格式基本相同:
- /*
- - 请求方式:DELETE
- - 请求路径:/索引库名/_doc/文档id
- - 请求参数:无
- */
-
- /* 模板 */
-
- DELETE /{索引库名}/_doc/id值
-
- /* 举例 */
-
- DELETE /qiuluo/_doc/1
我们修改文档大致分为两种:全量修改和增量修改:
- /*
- 全量修改的具体步骤:
- - 根据指定的id删除文档,新增一个相同id的文档
-
- - 请求方式:PUT
- - 请求路径:/索引库名/_doc/文档id
- - 请求参数:全部字段内容
- */
-
- /* 模板 */
-
- PUT /{索引库名}/_doc/文档id
- {
- "字段1": "值1",
- "字段2": "值2",
- // ... 略
- }
-
- /* 举例 */
-
- PUT /qiuluo/_doc/1
- {
- "info": "河南师范大学",
- "email": "zy@hsd.cn",
- "name": {
- "firstName": "云",
- "lastName": "赵"
- }
- }
-
- /*
- 增量修改的具体步骤:
- - 修改文档中的部分字段
-
- - 请求方式:POST
- - 请求路径:/索引库名/_update/文档id
- - 请求参数:只修改需要修改的部分
- */
-
- /* 模板 */
-
- POST /{索引库名}/_update/文档id
- {
- "doc": {
- "字段名": "新的值",
- }
- }
-
- /* 举例 */
-
- POST /heima/_update/1
- {
- "doc": {
- "email": "ZhaoYun@hsd.cn"
- }
- }
下面我们在IDEA上使用API去完成ES的使用
我们在使用ES之前需要先完成几项准备工作:
- CREATE TABLE `tb_hotel` (
- `id` bigint(20) NOT NULL COMMENT '酒店id',
- `name` varchar(255) NOT NULL COMMENT '酒店名称;例:7天酒店',
- `address` varchar(255) NOT NULL COMMENT '酒店地址;例:航头路',
- `price` int(10) NOT NULL COMMENT '酒店价格;例:329',
- `score` int(2) NOT NULL COMMENT '酒店评分;例:45,就是4.5分',
- `brand` varchar(32) NOT NULL COMMENT '酒店品牌;例:如家',
- `city` varchar(32) NOT NULL COMMENT '所在城市;例:上海',
- `star_name` varchar(16) DEFAULT NULL COMMENT '酒店星级,从低到高分别是:1星到5星,1钻到5钻',
- `business` varchar(255) DEFAULT NULL COMMENT '商圈;例:虹桥',
- `latitude` varchar(32) NOT NULL COMMENT '纬度;例:31.2497',
- `longitude` varchar(32) NOT NULL COMMENT '经度;例:120.3925',
- `pic` varchar(255) DEFAULT NULL COMMENT '酒店图片;例:/img/1.jpg',
- PRIMARY KEY (`id`)
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
- /*
- 下述是我们需要插入的数据,我们需要对其分析并简单了解,其具体思路不再解释
-
- 我们需要介绍几个新的内容:
-
- 1. geo_point
- 属于type的一种,表示地理坐标类型,里面包含精度、纬度
- geo_point属于由两个数组成的一个点;而geo_shape是由多个geo_point所组成的一条线或一个区域
-
- 2. all
- 一个组合字段,其目的是将多字段的值 利用copy_to合并,提供给用户搜索
- all字段在最后进行标明,但在前面的某些字段中我们采用了copy_to字段,后面跟上了all表示将该字段值拷贝一份到all中
- 也就是说all这个字段是由name,brand,city等多个字段连接起来的,这点是为了帮助我们在后面的按词条快速查询时方便
-
- */
-
- PUT /hotel
- {
- "mappings": {
- "properties": {
- "id": {
- "type": "keyword"
- },
- "name":{
- "type": "text",
- "analyzer": "ik_max_word",
- "copy_to": "all"
- },
- "address":{
- "type": "keyword",
- "index": false
- },
- "price":{
- "type": "integer"
- },
- "score":{
- "type": "integer"
- },
- "brand":{
- "type": "keyword",
- "copy_to": "all"
- },
- "city":{
- "type": "keyword",
- "copy_to": "all"
- },
- "starName":{
- "type": "keyword"
- },
- "business":{
- "type": "keyword"
- },
- "location":{
- "type": "geo_point"
- },
- "pic":{
- "type": "keyword",
- "index": false
- },
- "all":{
- "type": "text",
- "analyzer": "ik_max_word"
- }
- }
- }
- }
- <!--
- 我们在IDEA中引用ES去进行操作必定需要借助工具,而这里我们需要借用RestHighLevelClient去完成ES的操作,所以我们需要先去引入依赖
- -->
-
- <!--引入es的RestHighLevelClient依赖-->
- <dependency>
- <groupId>org.elasticsearch.client</groupId>
- <artifactId>elasticsearch-rest-high-level-client</artifactId>
- </dependency>
-
- <!--因为SpringBoot默认的ES版本是7.6.2,所以我们需要覆盖默认的ES版本-->
- <properties>
- <java.version>1.8</java.version>
- <elasticsearch.version>7.12.1</elasticsearch.version>
- </properties>
- // 这里仅是一个代码展示
-
- RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(// 可以看作固定生成语句
- HttpHost.create("http://192.168.150.101:9200")// 这里需要给出ES的链接地址,给出你的虚拟机ES端口
- ));
- package cn.itcast.hotel;
-
- import org.apache.http.HttpHost;
- import org.elasticsearch.client.RestHighLevelClient;
- import org.junit.jupiter.api.AfterEach;
- import org.junit.jupiter.api.BeforeEach;
- import org.junit.jupiter.api.Test;
-
- import java.io.IOException;
-
- public class HotelIndexTest {
- private RestHighLevelClient client;
-
- // 执行前运行
- @BeforeEach
- void setUp() {
- this.client = new RestHighLevelClient(RestClient.builder(
- HttpHost.create("http://192.168.150.101:9200")
- ));
- }
-
- // 执行后运行
- @AfterEach
- void tearDown() throws IOException {
- this.client.close();
- }
- }
我们首先需要定义出具体的JSON数据内容:
- package cn.itcast.hotel.constants;
-
- public class HotelConstants {
- public static final String MAPPING_TEMPLATE = "{\n" +
- " \"mappings\": {\n" +
- " \"properties\": {\n" +
- " \"id\": {\n" +
- " \"type\": \"keyword\"\n" +
- " },\n" +
- " \"name\":{\n" +
- " \"type\": \"text\",\n" +
- " \"analyzer\": \"ik_max_word\",\n" +
- " \"copy_to\": \"all\"\n" +
- " },\n" +
- " \"address\":{\n" +
- " \"type\": \"keyword\",\n" +
- " \"index\": false\n" +
- " },\n" +
- " \"price\":{\n" +
- " \"type\": \"integer\"\n" +
- " },\n" +
- " \"score\":{\n" +
- " \"type\": \"integer\"\n" +
- " },\n" +
- " \"brand\":{\n" +
- " \"type\": \"keyword\",\n" +
- " \"copy_to\": \"all\"\n" +
- " },\n" +
- " \"city\":{\n" +
- " \"type\": \"keyword\",\n" +
- " \"copy_to\": \"all\"\n" +
- " },\n" +
- " \"starName\":{\n" +
- " \"type\": \"keyword\"\n" +
- " },\n" +
- " \"business\":{\n" +
- " \"type\": \"keyword\"\n" +
- " },\n" +
- " \"location\":{\n" +
- " \"type\": \"geo_point\"\n" +
- " },\n" +
- " \"pic\":{\n" +
- " \"type\": \"keyword\",\n" +
- " \"index\": false\n" +
- " },\n" +
- " \"all\":{\n" +
- " \"type\": \"text\",\n" +
- " \"analyzer\": \"ik_max_word\"\n" +
- " }\n" +
- " }\n" +
- " }\n" +
- "}";
- }
然后我们就可以在我们先前定义的测试类中进行ES操作:
- @Test
- void createHotelIndex() throws IOException {
- // 1.创建Request对象(创建是CreateIndexRequest)
- CreateIndexRequest request = new CreateIndexRequest("hotel");// 参数是索引名称
-
- // 2.准备请求的参数:DSL语句(对request的source属性进行设置)
- request.source(MAPPING_TEMPLATE, XContentType.JSON);// 第一个参数是具体JSON,第二个参数是第一个参数类型
-
- // 3.发送请求(client.indices()是一个针对索引的对象,里面封装了各种索引方法)
- client.indices().create(request, RequestOptions.DEFAULT);// 第一个参数是请求,第二个是默认格式
- }
我们直接给出对应的代码展示:
- @Test
- void testDeleteHotelIndex() throws IOException {
- // 1.创建Request对象(创建是DeleteIndexRequest)
- DeleteIndexRequest request = new DeleteIndexRequest("hotel");// 参数是索引名称
-
- // 2.发送请求
- client.indices().delete(request, RequestOptions.DEFAULT);
- }
我们这里获得索引库并判断该索引是否存在:
- @Test
- void testExistsHotelIndex() throws IOException {
- // 1.创建Request对象(创建是GetIndexRequest)
- GetIndexRequest request = new GetIndexRequest("hotel");
-
- // 2.发送请求(这里调用的请求是exists判断该索引是否存在,最后返回一个boolean值用于判断)
- boolean exists = client.indices().exists(request, RequestOptions.DEFAULT);
-
- // 3.输出
- System.err.println(exists ? "索引库已经存在!" : "索引库不存在!");
- }
我们其实可以将ES的DSL操作和IDEA的操作做一个简单对比:
我们可以注意到:
因而我们可以给出具体的流程:
下面我们来介绍IDEA的文档操作
我们首先需要准备一个对应的实体类:
- // 这次我们主要是针对hotel旅馆进行一个文档信息的填充
- // 我们在MySQL数据库中存放了相对应的数据,我们首先给出对应实体类
-
- @Data
- @TableName("tb_hotel")
- public class Hotel {
- @TableId(type = IdType.INPUT)
- private Long id;
- private String name;
- private String address;
- private Integer price;
- private Integer score;
- private String brand;
- private String city;
- private String starName;
- private String business;
- private String longitude;
- private String latitude;
- private String pic;
- }
-
- // 但是我们需要注意:我们之前索引库中我们存在一个location属性是将longitude和latitude结合起来形成一个坐标
- // 因而我们需要一个DTO来完成信息封装
-
- package cn.itcast.hotel.pojo;
-
- import lombok.Data;
- import lombok.NoArgsConstructor;
-
- @Data
- @NoArgsConstructor
- public class HotelDoc {
- private Long id;
- private String name;
- private String address;
- private Integer price;
- private Integer score;
- private String brand;
- private String city;
- private String starName;
- private String business;
- private String location;
- private String pic;
-
- public HotelDoc(Hotel hotel) {
- this.id = hotel.getId();
- this.name = hotel.getName();
- this.address = hotel.getAddress();
- this.price = hotel.getPrice();
- this.score = hotel.getScore();
- this.brand = hotel.getBrand();
- this.city = hotel.getCity();
- this.starName = hotel.getStarName();
- this.business = hotel.getBusiness();
- this.location = hotel.getLatitude() + ", " + hotel.getLongitude();
- this.pic = hotel.getPic();
- }
- }
我们同样提前准备一个简单的测试类:
- package cn.itcast.hotel;
-
- import cn.itcast.hotel.pojo.Hotel;
- import cn.itcast.hotel.service.IHotelService;
- import org.junit.jupiter.api.AfterEach;
- import org.junit.jupiter.api.BeforeEach;
- import org.junit.jupiter.api.Test;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.boot.test.context.SpringBootTest;
-
- import java.io.IOException;
- import java.util.List;
-
- @SpringBootTest
- public class HotelDocumentTest {
- // 这里是hotel的服务层,我们调用其方法来获得mysql的相关数据来填充进ES的文档中
- @Autowired
- private IHotelService hotelService;
-
- // RestHighLevelClient
- private RestHighLevelClient client;
-
- // RestHighLevelClient封装
- @BeforeEach
- void setUp() {
- this.client = new RestHighLevelClient(RestClient.builder(
- HttpHost.create("http://192.168.150.101:9200")
- ));
- }
-
- // RestHighLevelClient释放资源
- @AfterEach
- void tearDown() throws IOException {
- this.client.close();
- }
- }
我们首先给出一个简单的案例展示:
- POST /{索引库名}/_doc/1
- {
- "name": "Jack",
- "age": 21
- }
- @Test
- void testAddDocument() throws IOException {
- // 1.准备Request对象(这里是IndexRequest,Index类似于新添的概念)
- IndexRequest request = new IndexRequest("indexName").id(1);// 第一个参数索引名称,后面id跟的是文档id
- // 2.准备Json文档
- request.source("{\"name\":\"jack\",\"age\":21}", XContentType.JSON);// 同样是对应的数据信息
- // 3.发送请求
- client.index(request, RequestOptions.DEFAULT);// 这里文档我们直接采用client调用方法即可,index就是新添操作
- }
然后我们再针对MySQL数据库信息将其挪移到ES中:
- @Test
- void testAddDocument() throws IOException {
-
- // 这里都是Spring和MyBatisPlus的内容
-
- // 1.根据id查询酒店数据
- Hotel hotel = hotelService.getById(61083L);
- // 2.转换为文档类型
- HotelDoc hotelDoc = new HotelDoc(hotel);
- // 3.将HotelDoc转json
- String json = JSON.toJSONString(hotelDoc);
-
- // 这里和前面的内容完全相同,只是内容进行了包装和更换
-
- // 1.准备Request对象
- IndexRequest request = new IndexRequest("hotel").id(hotelDoc.getId().toString());
- // 2.准备Json文档
- request.source(json, XContentType.JSON);
- // 3.发送请求
- client.index(request, RequestOptions.DEFAULT);
- }
我们首先给出对应的DSL语句:
GET /hotel/_doc/{id}
然后我们给出对应的Java代码:
- @Test
- void testGetDocumentById() throws IOException {
- // 1.准备Request(这里是GetRequest,Get就是获得)
- GetRequest request = new GetRequest("hotel", "61082");// 第一个参数是索引名称,第二个参数是id
-
- // 2.发送请求,得到响应
- GetResponse response = client.get(request, RequestOptions.DEFAULT);// get方法获得response
-
- // 3.解析响应结果
- String json = response.getSourceAsString();// 我们通过response获得对应的数据
-
- // 4.将数据解析获得结果
- HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
- System.out.println(hotelDoc);
- }
我们同样给出DSL语句:
DELETE /hotel/_doc/{id}
然后我们给出对应的Java代码:
- @Test
- void testDeleteDocument() throws IOException {
- // 1.准备Request
- DeleteRequest request = new DeleteRequest("hotel", "61083");
-
- // 2.发送请求
- client.delete(request, RequestOptions.DEFAULT);
- }
修改文档同样可以划分为两种:
我们直接给出增强修改对应的Java代码:
- @Test
- void testUpdateDocument() throws IOException {
- // 1.准备Request
- UpdateRequest request = new UpdateRequest("hotel", "61083");
-
- // 2.准备请求参数(注意:这里采用了doc而不是source)
- request.doc(
- "price", "952",
- "starName", "四钻"
- );
-
- // 3.发送请求
- client.update(request, RequestOptions.DEFAULT);
- }
下面我们将会利用BulkRequest批量将数据库数据导入到索引库中:
- @Test
- void testBulkRequest() throws IOException {
- // 批量查询酒店数据
- List<Hotel> hotels = hotelService.list();
-
- // 1.创建Request
- // BulkRequest实际上只是一个request集合体,它可以添加多种其他类型的request
- // 包括IndexRequest新增,UpdateRequest修改,DeleteRequest删除三种request请求并统一处理
- BulkRequest request = new BulkRequest();
-
- // 2.准备参数,添加多个新增的Request
- for (Hotel hotel : hotels) {
- // 2.1.转换为文档类型HotelDoc
- HotelDoc hotelDoc = new HotelDoc(hotel);
- // 2.2.创建新增文档的Request对象
- request.add(new IndexRequest("hotel")
- .id(hotelDoc.getId().toString())
- .source(JSON.toJSONString(hotelDoc), XContentType.JSON));
- }
- // 3.发送请求
- client.bulk(request, RequestOptions.DEFAULT);
- }
我们其实可以将ES的DSL操作和IDEA的操作做一个简单对比:
我们可以注意到同样划分为三步:
因而我们可以给出具体的流程:
在前面的章节其实只是完成了ES的一个数据储存功能,但ES的核心功能是数据快速检索查询
Elasticsearch提供了基于JSON的DSL来定义查询,大致有以下几种查询方式:
我们再给出一个基本查询模板:
- GET /indexName/_search
- {
- "query": {
- "查询类型": {
- "查询条件": "条件值"
- }
- }
- }
查询所有的关键字是"match_all",无查询条件:
- // 查询所有
- GET /indexName/_search
- {
- "query": {
- "match_all": {
- }
- }
- }
首先我们需要了解全文检索查询的基础流程:
其中全文检索查询可以大致分为两种:
我们分别给出全文检索模板:
- // match查询:仅一个字段,一个匹配内容
-
- GET /indexName/_search
- {
- "query": {
- "match": {
- "FIELD": "TEXT"// FIELD为对应的字段名称,TEXT为查询内容
- }
- }
- }
-
- // multi_match查询:多字段查询,任意一个字段符合条件就算符合查询条件
- GET /indexName/_search
- {
- "query": {
- "multi_match": {
- "query": "TEXT", // TEXT为查询内容
- "fields": ["FIELD1", " FIELD12"] // FIELD1,2,3,均为查询字段
- }
- }
- }
我们同时给出一个简单案例:
- // 下述两个全文检索含义相同
-
- // match查询:仅一个字段,一个匹配内容
-
- GET /hotel/_search
- {
- "query": {
- "match": {
- "all": "外滩" // 这里仅针对all字段进行"外滩"的检索,但是all字段是由多个字段copy_to产生的
- }
- }
- }
-
- // multi_match查询:多字段查询,任意一个字段符合条件就算符合查询条件
-
- GET /hotel/_search
- {
- "query": {
- "multi_match": {
- "query": "外滩",
- "fields": ["brand", "name","business"] // 这三个字段均使用了copy_to至all字段,固两个查询含义相同
- }
- }
- }
-
- // 但是查询字段越多其速度越慢,所以match查询的速度是要远高于multi_match的
精确查询一般是查找keyword、数值、日期、boolean等类型字段,我们一般会将精准查询分为两部分:
首先我们先来介绍term查询:
- // 模板
-
- GET /indexName/_search
- {
- "query": {
- "term": { // 表示精准查询
- "FIELD": { // 字段名
- "value": "VALUE" // 查找字段内容
- }
- }
- }
- }
-
- // 案例
-
- GET /hotel/_search
- {
- "query": {
- "term": {
- "city": {
- "value": "北京" // 表示查找地点在北京的宾馆
- }
- }
- }
- }
下面我们再来介绍range查询:
- // 模板
-
- GET /indexName/_search
- {
- "query": {
- "range": {
- "FIELD": { // 这里更换字段名
- "gte": 10, // 这里的gte代表大于等于,gt则代表大于
- "lte": 20 // lte代表小于等于,lt则代表小于
- }
- }
- }
- }
-
- // 案例
-
- GET /hotel/_search
- {
- "query": {
- "range": {
- "price": {
- "gte": 200,
- "lte": 1000 // 这里表示寻找价格在200~1000之间的宾馆
- }
- }
- }
- }
所谓的地理坐标查询,其实就是根据经纬度查询,地理查询通常被分为两方面:
我们首先来介绍矩形范围查询:
- // geo_bounding_box查询
-
- GET /indexName/_search
- {
- "query": {
- "geo_bounding_box": {
- "FIELD": { // 这里的FIELD需要修改为字段名
- "top_left": { // 左上点
- "lat": 31.1,
- "lon": 121.5
- },
- "bottom_right": { // 右下点
- "lat": 30.9,
- "lon": 121.7
- }
- }
- }
- }
- }
下面我们再来介绍附近范围查询:
- // 模板
-
- GET /indexName/_search
- {
- "query": {
- "geo_distance": { // 表示附近范围查询,其实就是圆形查询
- "distance": "15km", // 半径
- "FIELD": "31.21,121.5" // 圆心(前面的FIELD需要修改为具体字段名,表示进行匹配)
- }
- }
- }
-
- // 案例
-
- GET /hotel/_search
- {
- "query": {
- "geo_distance": {
- "distance": "15km",
- "Location": "31.21,121.5" // 圆心修改为Location的值,当距离小于15km时匹配成功
- }
- }
- }
最后我们介绍一下复合查询:
复合查询通常被分为两种情况:
那么在正式介绍复合查询之前,我们需要先去了解一下文档相关性算分:
- // 文档相关性算法:档结果会根据与搜索词条的关联度打分(_score),返回结果时按照分值降序排列
-
- [
- {
- "_score" : 17.850193,
- "_source" : {
- "name" : "虹桥如家酒店真不错",
- }
- },
- {
- "_score" : 12.259849,
- "_source" : {
- "name" : "外滩如家酒店真不错",
- }
- },
- {
- "_score" : 11.91091,
- "_source" : {
- "name" : "迪士尼如家酒店真不错",
- }
- }
- ]
而这种算分机制是由系统去控制,目前所延用的算分机制为BM25算分机制:
那么我们接下来就可以去了解算分函数查询:
- // 我们平时所得出的score也许并不能完全满足我们的正常需求
- // 例如:百度的广告通常会覆盖掉得分高的空间而被安排到最上层
-
- /*
-
- function score 查询中包含四部分内容:
-
- - **原始查询**条件:query部分,基于这个条件搜索文档,并且基于BM25算法给文档打分,**原始算分**(query score)
- - **过滤条件**:filter部分,符合该条件的文档才会重新算分
- - **算分函数**:符合filter条件的文档要根据这个函数做运算,得到的**函数算分**(function score),有四种函数
- - weight:函数结果是常量
- - field_value_factor:以文档中的某个字段值作为函数结果
- - random_score:以随机数作为函数结果
- - script_score:自定义算分函数算法
- - **运算模式**:算分函数的结果、原始查询的相关性算分,两者之间的运算方式,包括:
- - multiply:相乘
- - replace:用function score替换query score
- - 其它,例如:sum、avg、max、min
-
- */
-
- /*
-
- function score的运行流程如下:
-
- - 1)根据**原始条件**查询搜索文档,并且计算相关性算分,称为**原始算分**(query score)
- - 2)根据**过滤条件**,过滤文档
- - 3)符合**过滤条件**的文档,基于**算分函数**运算,得到**函数算分**(function score)
- - 4)将**原始算分**(query score)和**函数算分**(function score)基于**运算模式**做运算,得到最终结果,作为相关性算分。
-
- */
-
- // 我们给出一个简单模板去解释上述内容
-
- GET /indexName/_search
- {
- "query": {
- "function_score": {
- "query": { .... }, // 原始查询,可以是任意条件
- "functions": [ // 算分函数
- {
- "filter": { // 满足的条件
- "term": { // 这里假设采用精准查询匹配
- "FIELD": "TEXT" // 需要满足FIELD字段为TEXT
- }
- },
- "weight": N // 算分权重为N
- }
- ],
- "boost_mode": "???" // 原始权重和算分权重的算法:有相加,乘法等
- }
- }
- }
-
- // 我们举一个简单的例子
-
- GET /hotel/_search
- {
- "query": {
- "function_score": {
- "query": {
- "match":{
- "all":"外滩"
- }
- }, // 原始查询,我们这里查询在all字段中带"外滩"的内容
- "functions": [ // 算分函数,下面是内容函数
- {
- "filter": { // 满足的条件,品牌必须是如家
- "term": {
- "brand": "如家"
- }
- },
- "weight": 2 // 算分权重为2
- }
- ],
- "boost_mode": "sum" // 加权模式,求和
- }
- }
- }
最后我们还需要介绍一个布尔查询:
- // 布尔查询其实就是简单的&&,||查询
-
- /*
-
- 布尔查询是一个或多个查询子句的组合,每一个子句就是一个**子查询**。子查询的组合方式有:
-
- - must:必须匹配每个子查询,类似“与”
- - should:选择性匹配子查询,类似“或”
- - must_not:必须不匹配,**不参与算分**,类似“非”
- - filter:必须匹配,**不参与算分**
-
- */
-
- /*
-
- 需要注意的是,搜索时,参与**打分的字段越多,查询的性能也越差**。因此这种多条件查询时,建议这样做:
-
- - 搜索框的关键字搜索,是全文检索查询,使用must查询,参与算分
- - 其它过滤条件,采用filter查询。不参与算分
-
- */
-
- // 我们首先给出一个模板
-
- GET /indexName/_search
- {
- "query": {
- "bool": { // 表示开启bool复合查询
- "must": [ // must必须满足
- {"term": {"FIELD": "TEXT" }}
- ],
- "should": [// 选择性匹配子查询,类似“或”
- {"term": {"FIELD": "TEXT" }},
- {"term": {"FIELD": "TEXT" }}
- ],
- "must_not": [// 不能满足,
- { "range": { "FIELD": { "lte": TEXT } }}
- ],
- "filter": [// 必须满足,但不参与到算分项目中
- { "range": {"FIELD": { "gte": TEXT } }}
- ]
- }
- }
- }
-
- // 我们给出一个简单案例:
-
- GET /hotel/_search
- {
- "query": {
- "bool": {
- "must": [// 必须在上海
- {"term": {"city": "上海" }}
- ],
- "should": [// 品牌必须是皇冠或华美达其中一种
- {"term": {"brand": "皇冠假日" }},
- {"term": {"brand": "华美达" }}
- ],
- "must_not": [// 价格不能低于500
- { "range": { "price": { "lte": 500 } }}
- ],
- "filter": [// 得分必须高于45
- { "range": {"score": { "gte": 45 } }}
- ]
- }
- }
- }
对于GET获得的结果我们还可以对其进行简单处理,其中大致包括有:
ES默认是根据相关度算分来排序,但是也支持自定义方式对搜索结果排序,大致分为两种:
我们首先来介绍普通字段排序:
- // 普通字段包括有:keyword、数值、日期类型排序
-
- // 模板
- // 排序条件是一个数组,也就是可以写多个排序条件。按照声明的顺序,当第一个条件相等时,再按照第二个条件排序,以此类推
- GET /indexName/_search
- {
- "query": {
- "match_all": {}
- },
- "sort": [
- {
- "FIELD": "desc" // 排序字段、排序方式ASC、DESC
- }
- ]
- }
-
- // 案例
-
- GET /hotel/_search
- {
- "query": {
- "match_all": {}
- },
- "sort": [
- {
- "score"::"desc"
- }
- ,{
- "price": "asc"
- }
- ]
- }
我们再来介绍地理坐标排序:
- // 地理坐标排序:指定一个坐标,作为目标点,计算每一个文档中,指定字段的坐标 到目标点的距离是多少,根据距离排序
-
- // 模板
-
- GET /indexName/_search
- {
- "query": {
- "match_all": {}
- },
- "sort": [
- {
- "_geo_distance" : {
- "FIELD" : "纬度,经度", // 文档中geo_point类型的字段名、目标坐标点
- "order" : "asc", // 排序方式
- "unit" : "km" // 排序的距离单位
- }
- }
- ]
- }
-
- // 案例
-
- GET /hotel/_search
- {
- "query": {
- "match_all": {}
- },
- "sort": [
- {
- "_geo_distance" : {
- "location" : "31,121", // 坐标在(31,121)附近的酒店按距离排序
- "order" : "asc", // 排序方式
- "unit" : "km" // 排序的距离单位
- }
- }
- ]
- }
elasticsearch 默认情况下只返回top10的数据,如果希望返回更多只能采用分页模式,分页被划分为两种:
我们首先来介绍基本分页:
- // 分页主要依赖两个参数:from和size,类似于mysql中的`limit ?, ?`
- // - from:从第几个文档开始
- // - size:总共查询几个文档
-
- // 模板
-
- GET /indexName/_search
- {
- "query": {
- "match_all": {}
- },
- "from": ?, // 分页开始的位置,默认为0
- "size": ?, // 期望获取的文档总数
- "sort": [
- {"price": "asc"} // 排序方式
- ]
- }
-
- // 案例
-
- GET /hotel/_search
- {
- "query": {
- "match_all": {}
- },
- "from": 0, // 分页开始的位置,默认为0
- "size": 10, // 期望获取的文档总数
- "sort": [
- {"price": "asc"}
- ]
- }
然后我们再来介绍深度分页:
- // 首先我们需要去了解一个思想,假设我们获取990~1000的数据,那么我们需要先去查询0~1000的数据然后去截取990~1000
-
- GET /hotel/_search
- {
- "query": {
- "match_all": {}
- },
- "from": 990, // 分页开始的位置,默认为0
- "size": 10, // 期望获取的文档总数
- "sort": [
- {"price": "asc"}
- ]
- }
-
- // 如果是单点查询,那么我们可以只查找数据并排序截取就可以了
- // 但如果集群查询,我们并非说只获取每个节点的TOP200就可以了,因为排序未定,我们需要获取每个节点的TOP1000再重新排序获取
- // 就会导致所查询数据过多导致查询缓慢,ES服务器压力较大,因此elasticsearch会禁止from+ size 超过10000的请求
-
- // 针对深度分页,ES提供了两种解决方案,[官方文档]:
- // - search after:分页时需要排序,原理是从上一次的排序值开始,查询下一页数据。官方推荐使用的方式。
- // - scroll:原理将排序后的文档id形成快照,保存在内存。官方已经不推荐使用。
我们首先介绍一下高亮:
高亮显示的实现分为两步:
<em>
标签<em>
标签编写CSS样式我们来简单学习一下高亮:
- // 模板
-
- GET /indexName/_search
- {
- "query": {
- "match": {
- "FIELD": "TEXT" // 查询条件,高亮一定要使用全文检索查询
- }
- },
- "highlight": {
- "fields": { // 指定要高亮的字段
- "FIELD": {
- "pre_tags": "<em>", // 用来标记高亮字段的前置标签
- "post_tags": "</em>" // 用来标记高亮字段的后置标签
- }
- }
- }
- }
-
- // 案例
-
- GET /hotel/_search
- {
- "query": {
- "match": {
- "all": "如家" // 查询条件,高亮一定要使用全文检索查询
- }
- },
- "highlight": {
- "fields": { // 指定要高亮的字段
- "name": {
- "require_field_match":"false",// 默认无法高亮,需要添加属性
- "pre_tags": "<em>", // 用来标记高亮字段的前置标签
- "post_tags": "</em>" // 用来标记高亮字段的后置标签
- }
- }
- }
- }
-
- /*
-
- **注意:**
-
- - 高亮是对关键字高亮,因此**搜索条件必须带有关键字**,而不能是范围这样的查询。
- - 默认情况下,**高亮的字段,必须与搜索指定的字段一致**,否则无法高亮
- - 如果要对非搜索字段高亮,则需要添加一个属性:required_field_match=false
-
- */
下面我们来使用Java代码去操作ES完成数据搜索
我们首先来简单学习一下使用流程:
- @Test
- void testMatchAll() throws IOException {
- // 1.准备Request
- SearchRequest request = new SearchRequest("hotel");// 查询统一使用SearchRequest的请求
- // 2.准备DSL
- request.source()
- .query(QueryBuilders.matchAllQuery());
- // request.source()的方法中包含了所有方法:query,sort,size,from,highlighter等
- // query():代表查询条件,利用QueryBuilders.matchAllQuery()构建一个match_all查询的DSL
- // QueryBuilders:其中包含match、term、function_score、bool等各种查询
-
- // 3.发送请求
- SearchResponse response = client.search(request, RequestOptions.DEFAULT);
-
- }
- private void handleResponse(SearchResponse response) {
- // 4.解析响应
- // 通过response.getHits()获取,就是JSON中的最外层的hits,代表命中的结果
- SearchHits searchHits = response.getHits();
-
- // 4.1.获取总条数
- // `SearchHits#getTotalHits().value`:获取总条数信息
- long total = searchHits.getTotalHits().value;
- System.out.println("共搜索到" + total + "条数据");
-
- // 4.2.文档数组
- // `SearchHits#getHits()`:获取SearchHit数组,也就是文档数组
- SearchHit[] hits = searchHits.getHits();
-
- // 4.3.遍历
- for (SearchHit hit : hits) {
- // 获取文档source
- // `SearchHit#getSourceAsString()`:获取文档结果中的_source,也就是原始的json文档数据
- String json = hit.getSourceAsString();
-
- // 反序列化
- HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
- System.out.println("hotelDoc = " + hotelDoc);
- }
- }
- @Test
- void testMatchAll() throws IOException {
- // 1.准备Request
- SearchRequest request = new SearchRequest("hotel");
- // 2.准备DSL
- request.source()
- .query(QueryBuilders.matchAllQuery());
- // 3.发送请求
- SearchResponse response = client.search(request, RequestOptions.DEFAULT);
-
- // 4.解析响应
- handleResponse(response);
- }
-
- private void handleResponse(SearchResponse response) {
- // 4.解析响应
- SearchHits searchHits = response.getHits();
- // 4.1.获取总条数
- long total = searchHits.getTotalHits().value;
- System.out.println("共搜索到" + total + "条数据");
- // 4.2.文档数组
- SearchHit[] hits = searchHits.getHits();
- // 4.3.遍历
- for (SearchHit hit : hits) {
- // 获取文档source
- String json = hit.getSourceAsString();
- // 反序列化
- HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
- System.out.println("hotelDoc = " + hotelDoc);
- }
- }
-
- /*
- 整体步骤:
-
- 1. 创建SearchRequest对象
-
- 2. 准备Request.source(),也就是DSL。
-
- ① QueryBuilders来构建查询条件
-
- ② 传入Request.source() 的 query() 方法
-
- 3. 发送请求,得到结果
-
- 4. 解析结果(参考JSON结果,从外到内,逐层解析)
-
- */
我们首先来介绍match查询:
- // 全文检索的match和multi_match查询与match_all的API基本一致,Java代码上的差异主要是request.source().query()中的参数
-
- // match查询
-
- @Test
- void testMatch() throws IOException {
- // 1.准备Request
- SearchRequest request = new SearchRequest("hotel");
- // 2.准备DSL
- request.source()// 这里第一个参数是字段,第二个参数是val匹配值
- .query(QueryBuilders.matchQuery("all", "如家"));// QueryBuilders的matchQuery
- // 3.发送请求
- SearchResponse response = client.search(request, RequestOptions.DEFAULT);
- // 4.解析响应
- handleResponse(response);
-
- }
-
- // MultiMatch查询
-
- @Test
- void testMultiMatch() throws IOException {
- // 1.准备Request
- SearchRequest request = new SearchRequest("hotel");
- // 2.准备DSL
- request.source()// 这里第一个参数是val匹配值,后面均为字段
- .query(QueryBuilders.mutilMatchQuery("如家","brand","name","business"));// mutilMatchQuery方法
- // 3.发送请求
- SearchResponse response = client.search(request, RequestOptions.DEFAULT);
- // 4.解析响应
- handleResponse(response);
-
- }
下面我们来介绍精准查询:
- // 精准查询主要分为:term和range,和之前几乎相同,只是采用的API不同
-
- // term查询
-
- @Test
- void testMatch() throws IOException {
- // 1.准备Request
- SearchRequest request = new SearchRequest("hotel");
- // 2.准备DSL
- request.source()// 这里第一个参数是字段,第二个参数是val匹配值
- .query(QueryBuilders.termQuery("city", "北京"));// QueryBuilders的termQuery
- // 3.发送请求
- SearchResponse response = client.search(request, RequestOptions.DEFAULT);
- // 4.解析响应
- handleResponse(response);
-
- }
-
- // range查询
-
- @Test
- void testMatch() throws IOException {
- // 1.准备Request
- SearchRequest request = new SearchRequest("hotel");
- // 2.准备DSL
- request.source()// rangeQuery获得对应QueryBuilders,后面采用gte和lte设置条件
- .query(QueryBuilders.rangeQuery("price").gte(100).lte(150));
- // 3.发送请求
- SearchResponse response = client.search(request, RequestOptions.DEFAULT);
- // 4.解析响应
- handleResponse(response);
-
- }
下面我们来介绍布尔查询:
- // 布尔查询是用must、must_not、filter等方式组合其它查询
-
- @Test
- void testBool() throws IOException {
- // 1.准备Request
- SearchRequest request = new SearchRequest("hotel");
- // 2.准备DSL
- // 2.1.准备BooleanQuery(由于内容过多,我们提前创建并对其设置,最后添加入request即可)
- BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
- // 2.2.添加term(注意:内部使用的仍是Query,BooleanQuery仅仅是将这些Query结合起来)
- boolQuery.must(QueryBuilders.termQuery("city", "杭州"));
- // 2.3.添加range(注意:内部使用的仍是Query,BooleanQuery仅仅是将这些Query结合起来)
- boolQuery.filter(QueryBuilders.rangeQuery("price").lte(250));
-
- request.source().query(boolQuery);// 注入
- // 3.发送请求
- SearchResponse response = client.search(request, RequestOptions.DEFAULT);
- // 4.解析响应
- handleResponse(response);
-
- }
下面我们同时来介绍排序和分页两个操作:
- // 搜索结果的排序和分页是与query同级的参数,因此同样是使用request.source()来设置
-
- @Test
- void testPageAndSort() throws IOException {
- // 页码,每页大小
- int page = 1, size = 5;
-
- // 1.准备Request
- SearchRequest request = new SearchRequest("hotel");
- // 2.准备DSL
- // 2.1.query
- request.source().query(QueryBuilders.matchAllQuery());// 查询所有
- // 2.2.排序 sort
- request.source().sort("price", SortOrder.ASC); // 按price排序,升序
- // 2.3.分页 from、size
- request.source().from((page - 1) * size).size(5);// 分页查询
- // 3.发送请求
- SearchResponse response P= client.search(request, RequestOptions.DEFAULT);
- // 4.解析响应
- handleResponse(response);
-
- }
最后我们介绍一下高亮查询:
- // 高亮查询必须使用全文检索查询,并且要有搜索关键字,将来才可以对关键字高亮
-
- // - 查询的DSL:其中除了查询条件,还需要添加高亮条件,同样是与query同级。
- // - 结果解析:结果除了要解析_source文档数据,还要解析高亮结果
-
- // 首先我们处理请求问题
- @Test
- void testHighlight() throws IOException {
- // 1.准备Request
- SearchRequest request = new SearchRequest("hotel");
- // 2.准备DSL
- // 2.1.query
- request.source().query(QueryBuilders.matchQuery("all", "如家"));// 全文检索查询
- // 2.2.高亮
- request.source().highlighter(new HighlightBuilder().field("name").requireFieldMatch(false));
- // 3.发送请求
- SearchResponse response = client.search(request, RequestOptions.DEFAULT);
- // 4.解析响应
- handleResponse(response);
-
- }
-
- // 我们还需要对高亮结果进行解析
- private void handleResponse(SearchResponse response) {
- // 4.解析响应
- SearchHits searchHits = response.getHits();
- // 4.1.获取总条数
- long total = searchHits.getTotalHits().value;
- System.out.println("共搜索到" + total + "条数据");
- // 4.2.文档数组
- SearchHit[] hits = searchHits.getHits();
- // 4.3.遍历
- for (SearchHit hit : hits) {
- // 获取文档source
- String json = hit.getSourceAsString();
- // 反序列化
- HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
- // 获取高亮结果
- Map<String, HighlightField> highlightFields = hit.getHighlightFields();
- if (!CollectionUtils.isEmpty(highlightFields)) {
- // 根据字段名获取高亮结果
- HighlightField highlightField = highlightFields.get("name");
- if (highlightField != null) {
- // 获取高亮值
- String name = highlightField.getFragments()[0].string();
- // 覆盖非高亮结果
- hotelDoc.setName(name);
- }
- }
- System.out.println("hotelDoc = " + hotelDoc);
- }
- }
下面我们来学习ES的数据聚合
首先我们需要先来了解数据聚合:
ES的聚合通常被分为三种:
注意:参加聚合的字段必须是keyword、日期、数值、布尔类型
首先我们先来了解基本的桶聚合:
我们给出一个桶聚合的案例展示:
- GET /hotel/_search
- {
- "size": 0, // 设置size为0,结果中不包含文档,只包含聚合结果
- "aggs": { // 表示开始定义聚合
- "brandAgg": { // 聚合名称,自定义即可
- "terms": { // 聚合的类型,按照品牌值聚合,所以选择term(TermAggregation聚合)
- "field": "brand", // 参与聚合的字段,会根据brand品牌进行聚合
- "size": 20 // 希望获取的聚合结果数量(默认情况为10,修改可展示数据数量)
- }
- }
- }
- }
默认情况下,Bucket聚合会统计Bucket内的文档数量,记为_count,并且按照_count降序排序,但是我们可以进行修改:
- GET /hotel/_search
- {
- "size": 0,
- "aggs": {
- "brandAgg": {
- "terms": {
- "field": "brand",
- "order": { // 我们可以在aggs的对应聚合名称内设置order来修改排序方式
- "_count": "asc" // 按照_count升序排列
- },
- "size": 20
- }
- }
- }
- }
我们同样可以采用数据搜索的方式来限制聚合的范围大小:
- // 很多情况下,我们并非需要聚合所有的数据,而是聚合满足一定条件的数据,那么我们就需要设置限制条件
-
- GET /hotel/_search
- {
- // 实际上就是采用最简单的query方法来限制条件
- "query": {
- "range": {
- "price": {
- "lte": 200 // 只对200元以下的文档聚合
- }
- }
- },
- "size": 0,
- // 这里仍旧采用aggs即可
- "aggs": {
- "brandAgg": {
- "terms": {
- "field": "brand",
- "size": 20
- }
- }
- }
- }
我们再来介绍一下度量聚合:
度量聚合通常会分为四种情况:
我们这里同样给出一个简单案例展示:
- GET /hotel/_search
- {
- // 设置展示数据,设置为0,不允许出聚合数据外的数据展示
- "size": 0,
- // 第一层数据聚合,这层会按brand进行分组并且聚合展示
- "aggs": {
- "brandAgg": {
- "terms": {
- "field": "brand",
- "size": 20
- },
- // 第二层数据聚合,实际上也是管道聚合!!!
- "aggs": { // 是brands聚合的子聚合,也就是分组后对每组分别计算
- "score_stats": { // 聚合名称
- "stats": { // 聚合类型,这里stats可以计算min、max、avg等(也可以单写min、max、avg其中一种)
- "field": "score" // 聚合字段,这里是score
- }
- }
- }
- }
- }
- }
下面我们来用Java代码来实现数据聚合
我们下面会从两方面分别将DSL语句和Java语句进行对比分析:
我们将通过一个简单的数据聚合案例来介绍具体API使用:
下面我们来具体实现其效果:
- /*
-
- 相关前端请求信息:
- - 请求方式:`POST`
- - 请求路径:`/hotel/filters`
- - 请求参数:`RequestParams`,与搜索文档的参数一致
- - 返回值类型:`Map<String, List<String>>`
- */
- @PostMapping("filters")
- public Map<String, List<String>> getFilters(@RequestBody RequestParams params){
- return hotelService.getFilters(params);
- }
- Map<String, List<String>> filters(RequestParams params);
- @Override
- public Map<String, List<String>> filters(RequestParams params) {
- try {
- // 1.准备Request
- SearchRequest request = new SearchRequest("hotel");
- // 2.准备DSL
- // 2.1.query
- buildBasicQuery(params, request);
- // 2.2.设置size
- request.source().size(0);
- // 2.3.聚合
- buildAggregation(request);
- // 3.发出请求
- SearchResponse response = client.search(request, RequestOptions.DEFAULT);
- // 4.解析结果
- Map<String, List<String>> result = new HashMap<>();
- Aggregations aggregations = response.getAggregations();
- // 4.1.根据品牌名称,获取品牌结果
- List<String> brandList = getAggByName(aggregations, "brandAgg");
- result.put("品牌", brandList);
- // 4.2.根据品牌名称,获取品牌结果
- List<String> cityList = getAggByName(aggregations, "cityAgg");
- result.put("城市", cityList);
- // 4.3.根据品牌名称,获取品牌结果
- List<String> starList = getAggByName(aggregations, "starAgg");
- result.put("星级", starList);
-
- return result;
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- }
-
- private void buildAggregation(SearchRequest request) {
- request.source().aggregation(AggregationBuilders
- .terms("brandAgg")
- .field("brand")
- .size(100)
- );
- request.source().aggregation(AggregationBuilders
- .terms("cityAgg")
- .field("city")
- .size(100)
- );
- request.source().aggregation(AggregationBuilders
- .terms("starAgg")
- .field("starName")
- .size(100)
- );
- }
-
- private List<String> getAggByName(Aggregations aggregations, String aggName) {
- // 4.1.根据聚合名称获取聚合结果
- Terms brandTerms = aggregations.get(aggName);
- // 4.2.获取buckets
- List<? extends Terms.Bucket> buckets = brandTerms.getBuckets();
- // 4.3.遍历
- List<String> brandList = new ArrayList<>();
- for (Terms.Bucket bucket : buckets) {
- // 4.4.获取key
- String key = bucket.getKeyAsString();
- brandList.add(key);
- }
- return brandList;
- }
最后我们来介绍ES和MySQL数据同步的具体实现
首先我们需要明白为什么要实现数据同步:
数据同步的实现具体来说有三种方式:
但是不同的方式存在有不同的优缺点:
在ES和MySQL的数据同步问题上异步方式在一定程度优于同步方式且我们之前已经学习过MQ,所以这里采用MQ实现数据同步
在实现数据同步之前我们先来简单介绍一下具体项目内容:
下面让我们来逐步完成MQ数据同步操作:
- <!--注意:在hotel-admin、hotel-demo中引入rabbitmq的依赖-->
-
- <!--amqp-->
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-amqp</artifactId>
- </dependency>
- // 为了保证两个服务的交换机机制相同,我们在两个服务中都声明以下类,采用常量去定义具体交换机和队列以及key
-
- package cn.itcast.hotel.constatnts;
-
- public class MqConstants {
- /**
- * 交换机
- */
- public final static String HOTEL_EXCHANGE = "hotel.topic";
- /**
- * 监听新增和修改的队列
- */
- public final static String HOTEL_INSERT_QUEUE = "hotel.insert.queue";
- /**
- * 监听删除的队列
- */
- public final static String HOTEL_DELETE_QUEUE = "hotel.delete.queue";
- /**
- * 新增或修改的RoutingKey
- */
- public final static String HOTEL_INSERT_KEY = "hotel.insert";
- /**
- * 删除的RoutingKey
- */
- public final static String HOTEL_DELETE_KEY = "hotel.delete";
- }
- // 在hotel-demo中声明即可
-
- package cn.itcast.hotel.config;
-
- import cn.itcast.hotel.constants.MqConstants;
- import org.springframework.amqp.core.Binding;
- import org.springframework.amqp.core.BindingBuilder;
- import org.springframework.amqp.core.Queue;
- import org.springframework.amqp.core.TopicExchange;
- import org.springframework.context.annotation.Bean;
- import org.springframework.context.annotation.Configuration;
-
- @Configuration
- public class MqConfig {
- // 声明交换机
- @Bean
- public TopicExchange topicExchange(){
- return new TopicExchange(MqConstants.HOTEL_EXCHANGE, true, false);
- }
-
- // 声明队列
- @Bean
- public Queue insertQueue(){
- return new Queue(MqConstants.HOTEL_INSERT_QUEUE, true);
- }
-
- // 声明队列
- @Bean
- public Queue deleteQueue(){
- return new Queue(MqConstants.HOTEL_DELETE_QUEUE, true);
- }
-
- // 声明绑定关系
- @Bean
- public Binding insertQueueBinding(){
- return BindingBuilder.bind(insertQueue()).to(topicExchange()).with(MqConstants.HOTEL_INSERT_KEY);
- }
-
- // 声明绑定关系
- @Bean
- public Binding deleteQueueBinding(){
- return BindingBuilder.bind(deleteQueue()).to(topicExchange()).with(MqConstants.HOTEL_DELETE_KEY);
- }
- }
- // 在hotel-admin的mysql操作中顺便发送MQ信息
-
- @PostMapping
- public void saveHotel(@RequestBody Hotel hotel){
- // 服务层具体实现
- hotelService.save(hotel);
- // 我们主要看这部分,MQ的信息发送(这里仅发送id为了节省MQ内存)
- rabbitTemplate.convertAndSend(MqConstants.HOTEL_EXCHANGE,OTEL_INSERT_KEY,hotel.getId());
- }
-
- @PostMapping
- public void updateHotel(@RequestBody Hotel hotel){
- // 服务层具体实现
- hotelService.update(hotel);
- // 由于ES的新增和更新相同,所以这里采用同一个key
- rabbitTemplate.convertAndSend(MqConstants.HOTEL_EXCHANGE,OTEL_INSERT_KEY,hotel.getId());
- }
-
- @PostMapping
- public void deleteHotel(@PathVariable Long id){
- // 服务层具体实现
- hotelService.delete(hotel);
- // 这里发送不同的key,进入不同Listener
- rabbitTemplate.convertAndSend(MqConstants.HOTEL_EXCHANGE,OTEL_DELETE_KEY,id);
- }
- // 首先在hotel-demo的`cn.qiuluo.hotel.service`包下的`IHotelService`中新增新增、删除业务
-
- void deleteById(Long id);
-
- void insertById(Long id);
-
- // 给hotel-demo中的`cn.qiuluo.hotel.service.impl`包下的HotelService中实现业务
-
- @Override
- public void deleteById(Long id) {
- try {
- // 1.准备Request
- DeleteRequest request = new DeleteRequest("hotel", id.toString());
- // 2.发送请求
- client.delete(request, RequestOptions.DEFAULT);
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- }
-
- @Override
- public void insertById(Long id) {
- try {
- // 0.根据id查询酒店数据
- Hotel hotel = getById(id);
- // 转换为文档类型
- HotelDoc hotelDoc = new HotelDoc(hotel);
-
- // 1.准备Request对象
- IndexRequest request = new IndexRequest("hotel").id(hotel.getId().toString());
- // 2.准备Json文档
- request.source(JSON.toJSONString(hotelDoc), XContentType.JSON);
- // 3.发送请求
- client.index(request, RequestOptions.DEFAULT);
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- }
-
- // 监听类书写
-
- package cn.qiuluo.hotel.mq;
-
- import cn.qiuluo.hotel.constants.MqConstants;
- import cn.qiuluo.hotel.service.IHotelService;
- import org.springframework.amqp.rabbit.annotation.RabbitListener;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.stereotype.Component;
-
- @Component
- public class HotelListener {
-
- @Autowired
- private IHotelService hotelService;
-
- /**
- * 监听酒店新增或修改的业务
- * @param id 酒店id
- */
- @RabbitListener(queues = MqConstants.HOTEL_INSERT_QUEUE)
- public void listenHotelInsertOrUpdate(Long id){
- hotelService.insertById(id);
- }
-
- /**
- * 监听酒店删除的业务
- * @param id 酒店id
- */
- @RabbitListener(queues = MqConstants.HOTEL_DELETE_QUEUE)
- public void listenHotelDelete(Long id){
- hotelService.deleteById(id);
- }
- }
这篇文章中详细介绍了ES以及关于ES的相关API展示,希望能为你带来帮助
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。