ElasticSearch企业级应用实战
1. 初识elasticsearch
1.1.了解ES
1.1.1.elasticsearch的作用
elasticsearch是一款非常强大的开源搜索引擎,具备非常多强大功能,可以帮助我们从海量数据中快速找到需要的内容
例如:
在GitHub搜索代码

在电商网站搜索商品

在百度搜索答案

在打车软件搜索附近的车

1.1.2.ELK技术栈
elasticsearch结合kibana、Logstash、Beats,也就是elastic stack(ELK)。被广泛应用在日志数据分析、实时监控等领域:

而elasticsearch是elastic stack的核心,负责存储、搜索、分析数据。

1.1.3.elasticsearch和lucene
elasticsearch底层是基于lucene来实现的。
Lucene是一个Java语言的搜索引擎类库,是Apache公司的顶级项目,由DougCutting于1999年研发。官网地址:https://lucene.apache.org/ 。

elasticsearch的发展历史:
- 2004年Shay Banon基于Lucene开发了Compass
- 2010年Shay Banon 重写了Compass,取名为Elasticsearch。

1.1.4.为什么不是其他搜索技术?
目前比较知名的搜索引擎技术排名:

虽然在早期,Apache Solr是最主要的搜索引擎技术,但随着发展elasticsearch已经渐渐超越了Solr,独占鳌头:

1.1.5.总结
什么是elasticsearch?
- 一个开源的分布式搜索引擎,可以用来实现搜索、日志统计、分析、系统监控等功能
什么是elastic stack(ELK)?
- 是以elasticsearch为核心的技术栈,包括beats、Logstash、kibana、elasticsearch
什么是Lucene?
- 是Apache的开源搜索引擎类库,提供了搜索引擎的核心API
1.2.倒排索引
倒排索引的概念是基于MySQL这样的正向索引而言的。
1.2.1.正向索引
那么什么是正向索引呢?例如给下表(tb_goods)中的id创建索引:

如果是根据id查询,那么直接走索引,查询速度非常快。
但如果是基于title做模糊查询,只能是逐行扫描数据,流程如下:
1)用户搜索数据,条件是title符合"%手机%"
2)逐行获取数据,比如id为1的数据
3)判断数据中的title是否符合用户搜索条件
4)如果符合则放入结果集,不符合则丢弃。回到步骤1
逐行扫描,也就是全表扫描,随着数据量增加,其查询效率也会越来越低。当数据量达到数百万时,就是一场灾难。
正向索引在搜索场景中存在的问题
- 模糊查询,可能会破坏最左匹配原则,从而导致索引失效,走全表扫描,效率低
- 无法查询关联内容,比如搜索引擎搜索小米手机,可能会在后续词条中出现小米耳机、小米骑车等关联内容,这是数据库模糊查询做不到的
1.2.2.倒排索引
倒排索引中有两个非常重要的概念:
- 文档(
Document
):用来搜索的数据,其中的每一条数据就是一个文档。例如一个网页、一个商品信息
- 词条(
Term
):对文档数据或用户搜索数据,利用某种算法分词,得到的具备含义的词语就是词条。例如:我是中国人,就可以分为:我、是、中国人、中国、国人这样的几个词条
创建倒排索引是对正向索引的一种特殊处理,流程如下:
- 将每一个文档的数据利用算法分词,得到一个个词条
- 创建表,每行数据包括词条、词条所在文档id、位置等信息
- 因为词条唯一性,可以给词条创建索引,例如hash表结构索引
如图:

倒排索引的搜索流程如下(以搜索”华为手机”为例):
1)用户输入条件"华为手机"
进行搜索。
2)对用户输入内容分词,得到词条:华为
、手机
。
3)拿着词条在倒排索引中查找,可以得到包含词条的文档id:1、2、3。
4)拿着文档id到正向索引中查找具体文档。
如图:

虽然要先查询倒排索引,再查询倒排索引,但是无论是词条、还是文档id都建立了索引,查询速度非常快!无需全表扫描。
1.2.3.正向和倒排
那么为什么一个叫做正向索引,一个叫做倒排索引呢?
是不是恰好反过来了?
那么两者方式的优缺点是什么呢?
正向索引:
- 优点:
- 可以给多个字段创建索引
- 根据索引字段搜索、排序速度非常快
- 缺点:
- 根据非索引字段,或者索引字段中的部分词条查找时,只能全表扫描。
倒排索引:
- 优点:
- 缺点:
- 只能给词条创建索引,而不是字段
- 无法根据字段做排序
1.3.es的一些概念
elasticsearch中有很多独有的概念,与mysql中略有差别,但也有相似之处。
1.3.1.文档和字段
elasticsearch是面向文档(Document)存储的,可以是数据库中的一条商品数据,一个订单信息。文档数据会被序列化为json格式后存储在elasticsearch中:

而Json文档中往往包含很多的字段(Field),类似于数据库中的列。
1.3.2.索引和映射
索引(Index),就是相同类型的文档的集合。
例如:
- 所有用户文档,就可以组织在一起,称为用户的索引;
- 所有商品的文档,可以组织在一起,称为商品的索引;
- 所有订单的文档,可以组织在一起,称为订单的索引;

因此,我们可以把索引当做是数据库中的表。
数据库的表会有约束信息,用来定义表的结构、字段的名称、类型等信息。因此,索引库中就有映射(mapping),是索引中文档的字段约束信息,类似表的结构约束。
1.3.3.mysql与elasticsearch
我们统一的把mysql与elasticsearch的概念做一下对比:
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 |
是不是说,我们学习了elasticsearch就不再需要mysql了呢?
并不是如此,两者各自有自己的擅长支出:
因此在企业中,往往是两者结合使用:
- 对安全性要求较高的写操作,使用mysql实现
- 对查询性能要求较高的搜索需求,使用elasticsearch实现
- 两者再基于某种方式,实现数据的同步,保证一致性

1.4.安装es、kibana
通过docker
的方式来进行安装
1.4.1.安装
创建网络
因为需要部署ElasticSearch
和Kibana
,因此需要创建一个网络来保证这两个容器互联
1
| docker network create es-kibana
|
部署单点es
安装ElasticSearch 7.12.1
版本的镜像
下载镜像
1
| docker pull elasticsearch:7.12.1
|
运行
1 2 3 4 5 6 7 8 9 10 11 12
| docker run -d \ --restart=always \ --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
:端口映射配置
--restart=always
:开机自启
测试
在浏览器输入http://虚拟机ip:9200
即可看到es
的响应结果

部署Kibana
Kibana
提供了一个ElasticSearch
的可视化界面
下载镜像
1
| docker pull kibana:7.12.1
|
运行
1 2 3 4 5 6 7
| docker run -d \ --restart=always \ --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
:端口映射配置
测试
在浏览器输入http://虚拟机ip:5601
即可看到对应的界面

kibana中提供了一个DevTools界面:
这个界面中可以编写DSL来操作elasticsearch。并且对DSL语句有自动补全功能。
1.4.2.分词器
ik分词器就是一个标准的中文分词器。它可以根据定义的字典对域进行分词,并且支持用户配置自己的字典,所以它除了可以按通用的习惯分词外,我们还可以定制化分词
在线安装(网络情况不好可能会失败,因为需要从github下载文件)
1 2 3 4 5 6 7 8 9 10
| # 进入容器内部 docker exec -it es /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 es
|
离线安装
先手动下载对应的安装包,然后将其手动上传到es的插件目录
查看elasticsearch的数据卷目录
1
| docker volume inspect es-plugins
|

说明plugins目录被挂载到了:/var/lib/docker/volumes/es-plugins/_data
这个目录中。
将ik分词器解压缩包上传至该挂载目录
重启容器,查看日志
1 2 3 4
| #重启容器 docker restart es #查看日志 docker logs -f es
|
查看插件是否安装
1 2 3 4 5 6 7 8
| # 进入容器内部 docker exec -it es /bin/bash
# 查看插件列表 elasticsearch-plugin list
#退出 exit
|
测试分词效果
IK分词器包含两种模式:
ik_smart
:最少切分
ik_max_word
:最细切分
1 2 3 4 5
| GET /_analyze { "analyzer": "ik_max_word", "text": "我们是中国人" }
|

1.4.3.总结
分词器的作用是什么?
- 创建倒排索引时对文档分词
- 用户搜索时,对输入的内容分词
IK分词器有几种模式?
- ik_smart:智能切分,粗粒度
- ik_max_word:最细切分,细粒度
2.RestAPI
ES官方提供了各种不同语言的客户端,用来操作ES。这些客户端的本质就是组装DSL语句,通过http请求发送给ES。官方文档地址:https://www.elastic.co/guide/en/elasticsearch/client/index.html
其中的Java Rest Client又包括两种:
- Java Low Level Rest Client
- Java High Level Rest Client

我们学习的是Java HighLevel Rest Client客户端API
2.0.导入Demo工程
2.0.1.导入数据
首先导入数据库数据:
数据结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 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;
|
2.0.2.导入项目
然后导入项目代码:
项目结构如图:

2.0.3.mapping映射分析
创建索引库,最关键的是mapping映射,而mapping映射要考虑的信息包括:
- 字段名
- 字段数据类型
- 是否参与搜索
- 是否需要分词
- 如果分词,分词器是什么?
其中:
- 字段名、字段数据类型,可以参考数据表结构的名称和类型
- 是否参与搜索要分析业务来判断,例如图片地址,就无需参与搜索
- 是否分词呢要看内容,内容如果是一个整体就无需分词,反之则要分词
- 分词器,我们可以统一使用ik_max_word
来看下酒店数据的索引库结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| 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" } } } }
|
几个特殊字段说明:
- location:地理坐标,里面包含精度、纬度
- all:一个组合字段,其目的是将多字段的值 利用copy_to合并,提供给用户搜索
地理坐标说明:

copy_to说明:

2.0.4.初始化RestClient
在elasticsearch提供的API中,与elasticsearch一切交互都封装在一个名为RestHighLevelClient的类中,必须先完成这个对象的初始化,建立与elasticsearch的连接。
分为三步:
1)引入es的RestHighLevelClient依赖:
1 2 3 4
| <dependency> <groupId>org.elasticsearch.client</groupId> <artifactId>elasticsearch-rest-high-level-client</artifactId> </dependency>
|
2)因为SpringBoot默认的ES版本是7.6.2,所以我们需要覆盖默认的ES版本:
1 2 3 4
| <properties> <java.version>1.8</java.version> <elasticsearch.version>7.12.1</elasticsearch.version> </properties>
|
3)初始化RestHighLevelClient:
初始化的代码如下:
1 2 3
| RestHighLevelClient client = new RestHighLevelClient(RestClient.builder( HttpHost.create("http://192.168.150.101:9200") ));
|
这里为了单元测试方便,我们创建一个测试类HotelIndexTest,然后将初始化的代码编写在@BeforeEach方法中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| 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(); } }
|
2.1.创建索引库
2.1.1.代码解读
创建索引库的API如下:

代码分为三步:
- 1)创建Request对象。因为是创建索引库的操作,因此Request是CreateIndexRequest。
- 2)添加请求参数,其实就是DSL的JSON参数部分。因为json字符串很长,这里是定义了静态字符串常量MAPPING_TEMPLATE,让代码看起来更加优雅。
- 3)发送请求,client.indices()方法的返回值是IndicesClient类型,封装了所有与索引库操作有关的方法。
2.1.2.完整示例
在hotel-demo的cn.itcast.hotel.constants包下,创建一个类,定义mapping映射的JSON字符串常量:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| 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" + "}"; }
|
在hotel-demo中的HotelIndexTest测试类中,编写单元测试,实现创建索引:
1 2 3 4 5 6 7 8 9
| @Test void createHotelIndex() throws IOException { CreateIndexRequest request = new CreateIndexRequest("hotel"); request.source(MAPPING_TEMPLATE, XContentType.JSON); client.indices().create(request, RequestOptions.DEFAULT); }
|
2.2.删除索引库
删除索引库的DSL语句非常简单:
与创建索引库相比:
- 请求方式从PUT变为DELTE
- 请求路径不变
- 无请求参数
所以代码的差异,注意体现在Request对象上。依然是三步走:
- 1)创建Request对象。这次是DeleteIndexRequest对象
- 2)准备参数。这里是无参
- 3)发送请求。改用delete方法
在hotel-demo中的HotelIndexTest测试类中,编写单元测试,实现删除索引:
1 2 3 4 5 6 7
| @Test void testDeleteHotelIndex() throws IOException { DeleteIndexRequest request = new DeleteIndexRequest("hotel"); client.indices().delete(request, RequestOptions.DEFAULT); }
|
2.3.判断索引库是否存在
判断索引库是否存在,本质就是查询,对应的DSL是:
因此与删除的Java代码流程是类似的。依然是三步走:
- 1)创建Request对象。这次是GetIndexRequest对象
- 2)准备参数。这里是无参
- 3)发送请求。改用exists方法
1 2 3 4 5 6 7 8 9
| @Test void testExistsHotelIndex() throws IOException { GetIndexRequest request = new GetIndexRequest("hotel"); boolean exists = client.indices().exists(request, RequestOptions.DEFAULT); System.err.println(exists ? "索引库已经存在!" : "索引库不存在!"); }
|
2.4.总结
JavaRestClient操作elasticsearch的流程基本类似。核心是client.indices()方法来获取索引库的操作对象。
索引库操作的基本步骤:
- 初始化RestHighLevelClient
- 创建XxxIndexRequest。XXX是Create、Get、Delete
- 准备DSL( Create时需要,其它是无参)
- 发送请求。调用RestHighLevelClient#indices().xxx()方法,xxx是create、exists、delete
3.RestClient操作文档
为了与索引库操作分离,我们再次参加一个测试类,做两件事情:
- 初始化RestHighLevelClient
- 我们的酒店数据在数据库,需要利用IHotelService去查询,所以注入这个接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| 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 { @Autowired private IHotelService hotelService;
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(); } }
|
3.1.新增文档
我们要将数据库的酒店数据查询出来,写入elasticsearch中。
3.1.1.索引库实体类
数据库查询后的结果是一个Hotel类型的对象。结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| @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; }
|
与我们的索引库结构存在差异:
- longitude和latitude需要合并为location
因此,我们需要定义一个新的类型,与索引库结构吻合:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| 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(); } }
|
3.1.2.语法说明
新增文档的DSL语句如下:
1 2 3 4 5
| POST /{索引库名}/_doc/1 { "name": "Jack", "age": 21 }
|
对应的java代码如图:

可以看到与创建索引库类似,同样是三步走:
- 1)创建Request对象
- 2)准备请求参数,也就是DSL中的JSON文档
- 3)发送请求
变化的地方在于,这里直接使用client.xxx()的API,不再需要client.indices()了。
3.1.3.完整代码
我们导入酒店数据,基本流程一致,但是需要考虑几点变化:
- 酒店数据来自于数据库,我们需要先查询出来,得到hotel对象
- hotel对象需要转为HotelDoc对象
- HotelDoc需要序列化为json格式
因此,代码整体步骤如下:
- 1)根据id查询酒店数据Hotel
- 2)将Hotel封装为HotelDoc
- 3)将HotelDoc序列化为JSON
- 4)创建IndexRequest,指定索引库名和id
- 5)准备请求参数,也就是JSON文档
- 6)发送请求
在hotel-demo的HotelDocumentTest测试类中,编写单元测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @Test void testAddDocument() throws IOException { Hotel hotel = hotelService.getById(61083L); HotelDoc hotelDoc = new HotelDoc(hotel); String json = JSON.toJSONString(hotelDoc);
IndexRequest request = new IndexRequest("hotel").id(hotelDoc.getId().toString()); request.source(json, XContentType.JSON); client.index(request, RequestOptions.DEFAULT); }
|
3.2.查询文档
3.2.1.语法说明
查询的DSL语句如下:
非常简单,因此代码大概分两步:
不过查询的目的是得到结果,解析为HotelDoc,因此难点是结果的解析。完整代码如下:

可以看到,结果是一个JSON,其中文档放在一个_source
属性中,因此解析就是拿到_source
,反序列化为Java对象即可。
与之前类似,也是三步走:
- 1)准备Request对象。这次是查询,所以是GetRequest
- 2)发送请求,得到结果。因为是查询,这里调用client.get()方法
- 3)解析结果,就是对JSON做反序列化
3.2.2.完整代码
在hotel-demo的HotelDocumentTest测试类中,编写单元测试:
1 2 3 4 5 6 7 8 9 10 11 12
| @Test void testGetDocumentById() throws IOException { GetRequest request = new GetRequest("hotel", "61082"); GetResponse response = client.get(request, RequestOptions.DEFAULT); String json = response.getSourceAsString();
HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class); System.out.println(hotelDoc); }
|
3.3.删除文档
删除的DSL为是这样的:
与查询相比,仅仅是请求方式从DELETE变成GET,可以想象Java代码应该依然是三步走:
- 1)准备Request对象,因为是删除,这次是DeleteRequest对象。要指定索引库名和id
- 2)准备参数,无参
- 3)发送请求。因为是删除,所以是client.delete()方法
在hotel-demo的HotelDocumentTest测试类中,编写单元测试:
1 2 3 4 5 6 7
| @Test void testDeleteDocument() throws IOException { DeleteRequest request = new DeleteRequest("hotel", "61083"); client.delete(request, RequestOptions.DEFAULT); }
|
3.4.修改文档
3.4.1.语法说明
修改我们讲过两种方式:
- 全量修改:本质是先根据id删除,再新增
- 增量修改:修改文档中的指定字段值
在RestClient的API中,全量修改与新增的API完全一致,判断依据是ID:
- 如果新增时,ID已经存在,则修改
- 如果新增时,ID不存在,则新增
这里不再赘述,我们主要关注增量修改。
代码示例如图:

与之前类似,也是三步走:
- 1)准备Request对象。这次是修改,所以是UpdateRequest
- 2)准备参数。也就是JSON文档,里面包含要修改的字段
- 3)更新文档。这里调用client.update()方法
3.4.2.完整代码
在hotel-demo的HotelDocumentTest测试类中,编写单元测试:
1 2 3 4 5 6 7 8 9 10 11 12
| @Test void testUpdateDocument() throws IOException { UpdateRequest request = new UpdateRequest("hotel", "61083"); request.doc( "price", "952", "starName", "四钻" ); client.update(request, RequestOptions.DEFAULT); }
|
3.5.批量导入文档
案例需求:利用BulkRequest批量将数据库数据导入到索引库中。
步骤如下:
3.5.1.语法说明
批量处理BulkRequest,其本质就是将多个普通的CRUD请求组合在一起发送。
其中提供了一个add方法,用来添加其他请求:

可以看到,能添加的请求包括:
- IndexRequest,也就是新增
- UpdateRequest,也就是修改
- DeleteRequest,也就是删除
因此Bulk中添加了多个IndexRequest,就是批量新增功能了。示例:

其实还是三步走:
- 1)创建Request对象。这里是BulkRequest
- 2)准备参数。批处理的参数,就是其它Request对象,这里就是多个IndexRequest
- 3)发起请求。这里是批处理,调用的方法为client.bulk()方法
我们在导入酒店数据时,将上述代码改造成for循环处理即可。
3.5.2.完整代码
在hotel-demo的HotelDocumentTest测试类中,编写单元测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| @Test void testBulkRequest() throws IOException { List<Hotel> hotels = hotelService.list();
BulkRequest request = new BulkRequest(); for (Hotel hotel : hotels) { HotelDoc hotelDoc = new HotelDoc(hotel); request.add(new IndexRequest("hotel") .id(hotelDoc.getId().toString()) .source(JSON.toJSONString(hotelDoc), XContentType.JSON)); } client.bulk(request, RequestOptions.DEFAULT); }
|
3.6.小结
文档操作的基本步骤:
- 初始化RestHighLevelClient
- 创建XxxRequest。XXX是Index、Get、Update、Delete、Bulk
- 准备参数(Index、Update、Bulk时需要)
- 发送请求。调用RestHighLevelClient#.xxx()方法,xxx是index、get、update、delete、bulk
- 解析结果(Get时需要)
4.RestClient查询文档
4.1.查询分类
常见的查询类型包括:
4.2.快速入门
我们以match_all查询为例
4.2.1.发起查询请求

代码解读:
这里关键的API有两个,一个是request.source()
,其中包含了查询、排序、分页、高亮等所有功能:

另一个是QueryBuilders
,其中包含match、term、function_score、bool等各种查询:

4.2.2.解析响应
响应结果的解析:

elasticsearch返回的结果是一个JSON字符串,结构包含:
hits
:命中的结果
total
:总条数,其中的value是具体的总条数值
max_score
:所有结果中得分最高的文档的相关性算分
hits
:搜索结果的文档数组,其中的每个文档都是一个json对象
_source
:文档中的原始数据,也是json对象
因此,我们解析响应结果,就是逐层解析JSON字符串,流程如下:
SearchHits
:通过response.getHits()获取,就是JSON中的最外层的hits,代表命中的结果
SearchHits#getTotalHits().value
:获取总条数信息
SearchHits#getHits()
:获取SearchHit数组,也就是文档数组
SearchHit#getSourceAsString()
:获取文档结果中的_source,也就是原始的json文档数据
4.2.3.完整代码
完整代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| @Test void testMatchAll() throws IOException { SearchRequest request = new SearchRequest("hotel"); request.source() .query(QueryBuilders.matchAllQuery()); SearchResponse response = client.search(request, RequestOptions.DEFAULT);
handleResponse(response); }
private void handleResponse(SearchResponse response) { SearchHits searchHits = response.getHits(); long total = searchHits.getTotalHits().value; System.out.println("共搜索到" + total + "条数据"); SearchHit[] hits = searchHits.getHits(); for (SearchHit hit : hits) { String json = hit.getSourceAsString(); HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class); System.out.println("hotelDoc = " + hotelDoc); } }
|
4.2.4.小结
查询的基本步骤是:
创建SearchRequest对象
准备Request.source(),也就是DSL。
① QueryBuilders来构建查询条件
② 传入Request.source() 的 query() 方法
发送请求,得到结果
解析结果(参考JSON结果,从外到内,逐层解析)
4.2.match查询
全文检索的match和multi_match查询与match_all的API基本一致。差别是查询条件,也就是query的部分。
因此,Java代码上的差异主要是request.source().query()中的参数了。同样是利用QueryBuilders提供的方法:
而结果解析代码则完全一致,可以抽取并共享。
完整代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13
| @Test void testMatch() throws IOException { SearchRequest request = new SearchRequest("hotel"); request.source() .query(QueryBuilders.matchQuery("all", "如家")); SearchResponse response = client.search(request, RequestOptions.DEFAULT); handleResponse(response);
}
|
4.3.精确查询
精确查询主要是两者:
与之前的查询相比,差异同样在查询条件,其它都一样。
查询条件构造的API如下:
4.4.布尔查询
布尔查询是用must、must_not、filter等方式组合其它查询,代码示例如下:

可以看到,API与其它查询的差别同样是在查询条件的构建,QueryBuilders,结果解析等其他代码完全不变。
完整代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| @Test void testBool() throws IOException { SearchRequest request = new SearchRequest("hotel"); BoolQueryBuilder boolQuery = QueryBuilders.boolQuery(); boolQuery.must(QueryBuilders.termQuery("city", "杭州")); boolQuery.filter(QueryBuilders.rangeQuery("price").lte(250));
request.source().query(boolQuery); SearchResponse response = client.search(request, RequestOptions.DEFAULT); handleResponse(response);
}
|
4.5.排序、分页
搜索结果的排序和分页是与query同级的参数,因此同样是使用request.source()来设置。
对应的API如下:

完整代码示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| @Test void testPageAndSort() throws IOException { int page = 1, size = 5;
SearchRequest request = new SearchRequest("hotel"); request.source().query(QueryBuilders.matchAllQuery()); request.source().sort("price", SortOrder.ASC); request.source().from((page - 1) * size).size(5); SearchResponse response = client.search(request, RequestOptions.DEFAULT); handleResponse(response);
}
|
4.6.高亮
高亮的代码与之前代码差异较大,有两点:
- 查询的DSL:其中除了查询条件,还需要添加高亮条件,同样是与query同级。
- 结果解析:结果除了要解析_source文档数据,还要解析高亮结果
4.6.1.高亮请求构建
高亮请求的构建API如下:

上述代码省略了查询条件部分,但是大家不要忘了:高亮查询必须使用全文检索查询,并且要有搜索关键字,将来才可以对关键字高亮。
完整代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @Test void testHighlight() throws IOException { SearchRequest request = new SearchRequest("hotel"); request.source().query(QueryBuilders.matchQuery("all", "如家")); request.source().highlighter(new HighlightBuilder().field("name").requireFieldMatch(false)); SearchResponse response = client.search(request, RequestOptions.DEFAULT); handleResponse(response);
}
|
4.6.2.高亮结果解析
高亮的结果与查询的文档结果默认是分离的,并不在一起。
因此解析高亮的代码需要额外处理:

代码解读:
- 第一步:从结果中获取source。hit.getSourceAsString(),这部分是非高亮结果,json字符串。还需要反序列为HotelDoc对象
- 第二步:获取高亮结果。hit.getHighlightFields(),返回值是一个Map,key是高亮字段名称,值是HighlightField对象,代表高亮值
- 第三步:从map中根据高亮字段名称,获取高亮字段值对象HighlightField
- 第四步:从HighlightField中获取Fragments,并且转为字符串。这部分就是真正的高亮字符串了
- 第五步:用高亮的结果替换HotelDoc中的非高亮结果
完整代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| private void handleResponse(SearchResponse response) { SearchHits searchHits = response.getHits(); long total = searchHits.getTotalHits().value; System.out.println("共搜索到" + total + "条数据"); SearchHit[] hits = searchHits.getHits(); for (SearchHit hit : hits) { 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); } }
|
5.旅游案例
下面,我们通过黑马旅游的案例来实战演练下之前学习的知识。
我们实现四部分功能:
- 酒店搜索和分页
- 酒店结果过滤
- 我周边的酒店
- 酒店竞价排名
启动我们提供的hotel-demo项目,其默认端口是8089,访问http://localhost:8090,就能看到项目页面了:

5.1.酒店搜索和分页
案例需求:实现黑马旅游的酒店搜索功能,完成关键字搜索和分页
5.1.1.需求分析
在项目的首页,有一个大大的搜索框,还有分页按钮:

点击搜索按钮,可以看到浏览器控制台发出了请求:

请求参数如下:

由此可以知道,我们这个请求的信息如下:
- 请求方式:POST
- 请求路径:/hotel/list
- 请求参数:JSON对象,包含4个字段:
- key:搜索关键字
- page:页码
- size:每页大小
- sortBy:排序,目前暂不实现
- 返回值:分页查询,需要返回分页结果PageResult,包含两个属性:
total
:总条数
List<HotelDoc>
:当前页的数据
因此,我们实现业务的流程如下:
- 步骤一:定义实体类,接收请求参数的JSON对象
- 步骤二:编写controller,接收页面的请求
- 步骤三:编写业务实现,利用RestHighLevelClient实现搜索、分页
5.1.2.定义实体类
实体类有两个,一个是前端的请求参数实体,一个是服务端应该返回的响应结果实体。
1)请求参数
前端请求的json结构如下:
1 2 3 4 5 6
| { "key": "搜索关键字", "page": 1, "size": 3, "sortBy": "default" }
|
因此,我们在cn.itcast.hotel.pojo
包下定义一个实体类:
1 2 3 4 5 6 7 8 9 10 11
| package cn.itcast.hotel.pojo;
import lombok.Data;
@Data public class RequestParams { private String key; private Integer page; private Integer size; private String sortBy; }
|
2)返回值
分页查询,需要返回分页结果PageResult,包含两个属性:
total
:总条数
List<HotelDoc>
:当前页的数据
因此,我们在cn.itcast.hotel.pojo
中定义返回结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| package cn.itcast.hotel.pojo;
import lombok.Data;
import java.util.List;
@Data public class PageResult { private Long total; private List<HotelDoc> hotels;
public PageResult() { }
public PageResult(Long total, List<HotelDoc> hotels) { this.total = total; this.hotels = hotels; } }
|
5.1.3.定义controller
定义一个HotelController,声明查询接口,满足下列要求:
- 请求方式:Post
- 请求路径:/hotel/list
- 请求参数:对象,类型为RequestParam
- 返回值:PageResult,包含两个属性
Long total
:总条数
List<HotelDoc> hotels
:酒店数据
因此,我们在cn.itcast.hotel.web
中定义HotelController:
1 2 3 4 5 6 7 8 9 10 11 12
| @RestController @RequestMapping("/hotel") public class HotelController {
@Autowired private IHotelService hotelService; @PostMapping("/list") public PageResult search(@RequestBody RequestParams params){ return hotelService.search(params); } }
|
5.1.4.实现搜索业务
我们在controller调用了IHotelService,并没有实现该方法,因此下面我们就在IHotelService中定义方法,并且去实现业务逻辑。
1)在cn.itcast.hotel.service
中的IHotelService
接口中定义一个方法:
1 2 3 4 5 6
|
PageResult search(RequestParams params);
|
2)实现搜索业务,肯定离不开RestHighLevelClient,我们需要把它注册到Spring中作为一个Bean。在cn.itcast.hotel
中的HotelDemoApplication
中声明这个Bean:
1 2 3 4 5 6
| @Bean public RestHighLevelClient client(){ return new RestHighLevelClient(RestClient.builder( HttpHost.create("http://192.168.150.101:9200") )); }
|
3)在cn.itcast.hotel.service.impl
中的HotelService
中实现search方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| @Override public PageResult search(RequestParams params) { try { SearchRequest request = new SearchRequest("hotel"); String key = params.getKey(); if (key == null || "".equals(key)) { request.source().query(QueryBuilders.matchAllQuery()); } else { request.source().query(QueryBuilders.matchQuery("all", key)); }
int page = params.getPage(); int size = params.getSize(); request.source().from((page - 1) * size).size(size);
SearchResponse response = client.search(request, RequestOptions.DEFAULT); return handleResponse(response); } catch (IOException e) { throw new RuntimeException(e); } }
private PageResult handleResponse(SearchResponse response) { SearchHits searchHits = response.getHits(); long total = searchHits.getTotalHits().value; SearchHit[] hits = searchHits.getHits(); List<HotelDoc> hotels = new ArrayList<>(); for (SearchHit hit : hits) { String json = hit.getSourceAsString(); HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class); hotels.add(hotelDoc); } return new PageResult(total, hotels); }
|
5.2.酒店结果过滤
需求:添加品牌、城市、星级、价格等过滤功能
5.2.1.需求分析
在页面搜索框下面,会有一些过滤项:

传递的参数如图:
包含的过滤条件有:
- brand:品牌值
- city:城市
- minPrice~maxPrice:价格范围
- starName:星级
我们需要做两件事情:
- 修改请求参数的对象RequestParams,接收上述参数
- 修改业务逻辑,在搜索条件之外,添加一些过滤条件
5.2.2.修改实体类
修改在cn.itcast.hotel.pojo
包下的实体类RequestParams:
1 2 3 4 5 6 7 8 9 10 11 12 13
| @Data public class RequestParams { private String key; private Integer page; private Integer size; private String sortBy; private String city; private String brand; private String starName; private Integer minPrice; private Integer maxPrice; }
|
5.2.3.修改搜索业务
在HotelService的search方法中,只有一个地方需要修改:requet.source().query( … )其中的查询条件。
在之前的业务中,只有match查询,根据关键字搜索,现在要添加条件过滤,包括:
- 品牌过滤:是keyword类型,用term查询
- 星级过滤:是keyword类型,用term查询
- 价格过滤:是数值类型,用range查询
- 城市过滤:是keyword类型,用term查询
多个查询条件组合,肯定是boolean查询来组合:
- 关键字搜索放到must中,参与算分
- 其它过滤条件放到filter中,不参与算分
因为条件构建的逻辑比较复杂,这里先封装为一个函数:

buildBasicQuery的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| private void buildBasicQuery(RequestParams params, SearchRequest request) { BoolQueryBuilder boolQuery = QueryBuilders.boolQuery(); String key = params.getKey(); if (key == null || "".equals(key)) { boolQuery.must(QueryBuilders.matchAllQuery()); } else { boolQuery.must(QueryBuilders.matchQuery("all", key)); } if (params.getCity() != null && !params.getCity().equals("")) { boolQuery.filter(QueryBuilders.termQuery("city", params.getCity())); } if (params.getBrand() != null && !params.getBrand().equals("")) { boolQuery.filter(QueryBuilders.termQuery("brand", params.getBrand())); } if (params.getStarName() != null && !params.getStarName().equals("")) { boolQuery.filter(QueryBuilders.termQuery("starName", params.getStarName())); } if (params.getMinPrice() != null && params.getMaxPrice() != null) { boolQuery.filter(QueryBuilders .rangeQuery("price") .gte(params.getMinPrice()) .lte(params.getMaxPrice()) ); } request.source().query(boolQuery); }
|
5.3.我周边的酒店
需求:我附近的酒店
5.3.1.需求分析
在酒店列表页的右侧,有一个小地图,点击地图的定位按钮,地图会找到你所在的位置:
并且,在前端会发起查询请求,将你的坐标发送到服务端:
我们要做的事情就是基于这个location坐标,然后按照距离对周围酒店排序。实现思路如下:
- 修改RequestParams参数,接收location字段
- 修改search方法业务逻辑,如果location有值,添加根据geo_distance排序的功能
5.3.2.修改实体类
修改在cn.itcast.hotel.pojo
包下的实体类RequestParams:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| package cn.itcast.hotel.pojo;
import lombok.Data;
@Data public class RequestParams { private String key; private Integer page; private Integer size; private String sortBy; private String city; private String brand; private String starName; private Integer minPrice; private Integer maxPrice; private String location; }
|
5.3.3.距离排序API
我们以前学习过排序功能,包括两种:
我们只讲了普通字段排序对应的java写法。地理坐标排序只学过DSL语法,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| GET /indexName/_search { "query": { "match_all": {} }, "sort": [ { "price": "asc" }, { "_geo_distance" : { "FIELD" : "纬度,经度", "order" : "asc", "unit" : "km" } } ] }
|
对应的java代码示例:

5.3.4.添加距离排序
在cn.itcast.hotel.service.impl
的HotelService
的search
方法中,添加一个排序功能:

完整代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| @Override public PageResult search(RequestParams params) { try { SearchRequest request = new SearchRequest("hotel"); buildBasicQuery(params, request);
int page = params.getPage(); int size = params.getSize(); request.source().from((page - 1) * size).size(size);
String location = params.getLocation(); if (location != null && !location.equals("")) { request.source().sort(SortBuilders .geoDistanceSort("location", new GeoPoint(location)) .order(SortOrder.ASC) .unit(DistanceUnit.KILOMETERS) ); }
SearchResponse response = client.search(request, RequestOptions.DEFAULT); return handleResponse(response); } catch (IOException e) { throw new RuntimeException(e); } }
|
5.3.5.排序距离显示
重启服务后,测试我的酒店功能:

发现确实可以实现对我附近酒店的排序,不过并没有看到酒店到底距离我多远,这该怎么办?
排序完成后,页面还要获取我附近每个酒店的具体距离值,这个值在响应结果中是独立的:

因此,我们在结果解析阶段,除了解析source部分以外,还要得到sort部分,也就是排序的距离,然后放到响应结果中。
我们要做两件事:
- 修改HotelDoc,添加排序距离字段,用于页面显示
- 修改HotelService类中的handleResponse方法,添加对sort值的获取
1)修改HotelDoc类,添加距离字段
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| 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; private Object distance;
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(); } }
|
2)修改HotelService中的handleResponse方法

1 2 3 4 5
| Object[] sortValues = hit.getSortValues(); if(sortValues.length>0){ Object sortValue = sortValues[0]; hotelDoc.setDistance(sortValue); }
|
重启后测试,发现页面能成功显示距离了:

5.4.酒店竞价排名
需求:让指定的酒店在搜索结果中排名置顶
5.4.1.需求分析
要让指定酒店在搜索结果中排名置顶,效果如图:

页面会给指定的酒店添加广告标记。
那怎样才能让指定的酒店排名置顶呢?
我们之前学习过的function_score查询可以影响算分,算分高了,自然排名也就高了。而function_score包含3个要素:
- 过滤条件:哪些文档要加分
- 算分函数:如何计算function score
- 加权方式:function score 与 query score如何运算
这里的需求是:让指定酒店排名靠前。因此我们需要给这些酒店添加一个标记,这样在过滤条件中就可以根据这个标记来判断,是否要提高算分。
比如,我们给酒店添加一个字段:isAD,Boolean类型:
这样function_score包含3个要素就很好确定了:
- 过滤条件:判断isAD 是否为true
- 算分函数:我们可以用最简单暴力的weight,固定加权值
- 加权方式:可以用默认的相乘,大大提高算分
因此,业务的实现步骤包括:
给HotelDoc类添加isAD字段,Boolean类型
挑选几个你喜欢的酒店,给它的文档数据添加isAD字段,值为true
修改search方法,添加function score功能,给isAD值为true的酒店增加权重
5.4.2.修改HotelDoc实体
给cn.itcast.hotel.pojo
包下的HotelDoc类添加isAD字段:

5.4.3.添加广告标记
接下来,我们挑几个酒店,添加isAD字段,设置为true:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| POST /hotel/_update/1902197537 { "doc": { "isAD": true } } POST /hotel/_update/2056126831 { "doc": { "isAD": true } } POST /hotel/_update/1989806195 { "doc": { "isAD": true } } POST /hotel/_update/2056105938 { "doc": { "isAD": true } }
|
5.4.4.添加算分函数查询
接下来我们就要修改查询条件了。之前是用的boolean 查询,现在要改成function_socre查询。
function_score查询结构如下:

对应的JavaAPI如下:

我们可以将之前写的boolean查询作为原始查询条件放到query中,接下来就是添加过滤条件、算分函数、加权模式了。所以原来的代码依然可以沿用。
修改cn.itcast.hotel.service.impl
包下的HotelService
类中的buildBasicQuery
方法,添加算分函数查询:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| private void buildBasicQuery(RequestParams params, SearchRequest request) { BoolQueryBuilder boolQuery = QueryBuilders.boolQuery(); String key = params.getKey(); if (key == null || "".equals(key)) { boolQuery.must(QueryBuilders.matchAllQuery()); } else { boolQuery.must(QueryBuilders.matchQuery("all", key)); } if (params.getCity() != null && !params.getCity().equals("")) { boolQuery.filter(QueryBuilders.termQuery("city", params.getCity())); } if (params.getBrand() != null && !params.getBrand().equals("")) { boolQuery.filter(QueryBuilders.termQuery("brand", params.getBrand())); } if (params.getStarName() != null && !params.getStarName().equals("")) { boolQuery.filter(QueryBuilders.termQuery("starName", params.getStarName())); } if (params.getMinPrice() != null && params.getMaxPrice() != null) { boolQuery.filter(QueryBuilders .rangeQuery("price") .gte(params.getMinPrice()) .lte(params.getMaxPrice()) ); }
FunctionScoreQueryBuilder functionScoreQuery = QueryBuilders.functionScoreQuery( boolQuery, new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{ new FunctionScoreQueryBuilder.FilterFunctionBuilder( QueryBuilders.termQuery("isAD", true), ScoreFunctionBuilders.weightFactorFunction(10) ) }); request.source().query(functionScoreQuery); }
|