ElasticSearch
官方文档 https://www.elastic.co

es介绍

1.ElasticSearch
ElasticSearch 是一个基于分布式搜索引擎的开源项目,主要用于全文搜索、结构化搜索以及分析。它在大数据场景下有广泛应用,特别是在需要处理复杂查询和数据检索的环境中。
elasticsearch是elastic stack的核心,负责存储、搜索、分析数据。
elasticsearch底层是基于lucene来实现的。
2.elastic stack(ELK)
- 是以elasticsearch为核心的技术栈,包括beats、Logstash、kibana、elasticsearch
3.Lucene
- 是Apache的开源搜索引擎类库,提供了搜索引擎的核心API
4.Elk技术
elasticsearch结合kibana、Logstash、Beats,也就是elastic stack(ELK)。被广泛应用在日志数据分析、实时监控等领域:
es概念

索引
正向索引
那么什么是正向索引呢?例如给下表(tb_goods)中的id创建索引:

如果是根据id查询,那么直接走索引,查询速度非常快。
但如果是基于title做模糊查询,只能是逐行扫描数据,逐行扫描,也就是全表扫描,随着数据量增加,其查询效率也会越来越低。当数据量达到数百万时,就是一场灾难。
倒排索引
倒排索引中有两个非常重要的概念:
- 文档(
Document):用来搜索的数据,其中的每一条数据就是一个文档。一个商品信息,mysql的一行记录
- 词条(
Term):对文档数据或用户搜索数据,利用某种算法分词,得到的具备含义的词语就是词条。例如:我是中国人,就可以分为:我、是、中国人、中国、国人这样的几个词条

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

虽然要先查询倒排索引,再查询倒排索引,但是无论是词条、还是文档id都建立了索引,查询速度非常快!无需全表扫描。

一个字段-一个词条-非聚集索引和正向索引蛮像的
对比
概念区别:
- 正向索引是最传统的,根据id索引的方式。但根据词条查询时,必须先逐条获取每个文档,然后判断文档中是否包含所需要的词条,是根据文档找词条的过程。
- 而倒排索引则相反,是先找到用户要搜索的词条,根据词条得到保护词条的文档的id,然后根据id获取文档。是根据词条找文档的过程。
优缺点:
正向索引:
- 优点:
- 可以给多个字段创建索引
- 根据索引字段搜索、排序速度非常快
- 缺点:
- 根据非索引字段,或者索引字段中的部分词条查找时,只能全表扫描。
倒排索引:
- 优点:
- 缺点:
- 只能给词条创建索引,而不是字段
- 无法根据字段做排序
Es数据概念
1.文档和字段
一个文档就像数据库里的一条数据,字段就像数据库里的列
elasticsearch是面向文档(Document)存储的,可以是数据库中的一条商品数据,一个订单信息。文档数据会被序列化为json格式后存储在elasticsearch中:

2.索引和隐射
索引就像数据库里的表,映射就像数据库中定义的表结构
索引(Index),就是相同类型的文档的集合【类似mysql中的表】
例如:
- 所有用户文档,就可以组织在一起,称为用户的索引;
- 所有商品的文档,可以组织在一起,称为商品的索引;
- 所有订单的文档,可以组织在一起,称为订单的索引;

因此,我们可以把索引当做是数据库中的表。
数据库的表会有约束信息,用来定义表的结构、字段的名称、类型等信息。因此,索引库中就有映射(mapping),是索引中文档的字段约束信息,类似表的结构约束。
3.mysql和es
各自长处:
es安装
1.安装es、kibana、分词器
分词器的作用是什么?
- 创建倒排索引时对文档分词
- 用户搜索时,对输入的内容分词
IK分词器有几种模式?
- ik_smart:智能切分,粗粒度
- ik_max_word:最细切分,细粒度
IK分词器如何拓展词条?如何停用词条?
- 利用config目录的IkAnalyzer.cfg.xml文件添加拓展词典和停用词典
- 在词典中添加拓展词条或者停用词条
2.部署es
因为我们还需要部署kibana容器,因此需要让es和kibana容器互联。这里先创建一个网络:
1
| docker network create es-net
|
加载镜像
这里我们采用elasticsearch的7.12.1版本的镜像,这个镜像体积非常大,接近1G。不建议大家自己pull。
课前资料提供了镜像的tar包:

大家将其上传到虚拟机中,然后运行命令加载即可:
注意:同理还有kibana的tar包也需要这样做。
运行docker命令,部署单点es:
1 2 3 4 5 6 7 8 9 10 11
| 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:端口映射配置
在浏览器中输入:http://192.168.194.131/:9200 即可看到elasticsearch的响应结果:

3.部署kibana
kibana可以给我们提供一个elasticsearch的可视化界面,便于我们学习。
创建网络后,导入kibana压缩包,然后创建并启动相应容器。【和前面部署单点es一样做法】
再运行docker命令,部署kibana
1 2 3 4 5 6
| 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:端口映射配置
kibana启动一般比较慢,需要多等待一会,可以通过命令:
查看运行日志,当查看到下面的日志,说明成功:


kibana左侧中提供了一个DevTools界面:

这个界面中可以编写DSL来操作elasticsearch。并且对DSL语句有自动补全功能。
4.安装IK分词器
1 2 3 4 5 6 7 8 9 10 11
| 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
|
.离线安装ik插件
安装插件需要知道elasticsearch的plugins目录位置,而我们用了数据卷挂载,因此需要查看elasticsearch的数据卷目录,通过下面命令查看:
1
| docker volume inspect es-plugins
|
显示结果:
1 2 3 4 5 6 7 8 9 10 11
| [ { "CreatedAt": "2022-05-06T10:06:34+08:00", "Driver": "local", "Labels": null, "Mountpoint": "/var/lib/docker/volumes/es-plugins/_data", "Name": "es-plugins", "Options": null, "Scope": "local" } ]
|
说明plugins目录被挂载到了:/var/lib/docker/volumes/es-plugins/_data这个目录中。
解压缩分词器安装包
下面我们需要把课前资料中的ik分词器解压缩,重命名为ik

3)上传到es容器的插件数据卷中
也就是/var/lib/docker/volumes/es-plugins/_data:

重启容器
1 2 3 4
| 4、重启容器 docker restart es # 查看es日志 docker logs -f es
|
5.测试
IK分词器包含两种模式:
ik_smart:最少切分
ik_max_word:最细切分
在kibana的Dev tools中输入以下代码:
”analyzer“ 就是选择分词器模式
1 2 3 4 5 6
| GET /_analyze { "analyzer": "ik_max_word", "text": "黑马程序员学习java太棒了" }
|
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 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
| { "tokens" : [ { "token" : "黑马", "start_offset" : 0, "end_offset" : 2, "type" : "CN_WORD", "position" : 0 }, { "token" : "程序员", "start_offset" : 2, "end_offset" : 5, "type" : "CN_WORD", "position" : 1 }, { "token" : "程序", "start_offset" : 2, "end_offset" : 4, "type" : "CN_WORD", "position" : 2 }, { "token" : "员", "start_offset" : 4, "end_offset" : 5, "type" : "CN_CHAR", "position" : 3 }, { "token" : "学习", "start_offset" : 5, "end_offset" : 7, "type" : "CN_WORD", "position" : 4 }, { "token" : "java", "start_offset" : 7, "end_offset" : 11, "type" : "ENGLISH", "position" : 5 }, { "token" : "太棒了", "start_offset" : 11, "end_offset" : 14, "type" : "CN_WORD", "position" : 6 }, { "token" : "太棒", "start_offset" : 11, "end_offset" : 13, "type" : "CN_WORD", "position" : 7 }, { "token" : "了", "start_offset" : 13, "end_offset" : 14, "type" : "CN_CHAR", "position" : 8 } ] }
|
6.扩展词词典
随着互联网的发展,“造词运动”也越发的频繁。出现了很多新的词语,在原有的词汇列表中并不存在。比如:“奥力给”,“白嫖” 等。
所以我们的词汇也需要不断的更新,IK分词器提供了扩展词汇的功能。
)打开IK分词器config目录:

在IKAnalyzer.cfg.xml配置文件内容添加:
1 2 3 4 5 6 7 8
| <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd"> <properties> <comment>IK Analyzer 扩展配置</comment> <entry key="ext_dict">ext.dic</entry> </properties>
|
新建一个 ext.dic,可以参考config目录下复制一个配置文件进行修改
白嫖 奥力给
重启elasticsearch

日志中已经成功加载ext.dic配置文件
测试效果:
1 2 3 4 5
| GET /_analyze { "analyzer": "ik_max_word", "text": "传智播客Java就业超过90%,奥力给!" }
|
注意当前文件的编码必须是 UTF-8 格式,严禁使用Windows记事本编辑
7.停用词词典
在互联网项目中,在网络间传输的速度很快,所以很多语言是不允许在网络上传递的,如:关于宗教、政治等敏感词语,那么我们在搜索时也应该忽略当前词汇。
IK分词器也提供了强大的停用词功能,让我们在索引时就直接忽略当前的停用词汇表中的内容。
1 2 3 4 5 6 7 8 9
| <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd"> <properties> <comment>IK Analyzer 扩展配置</comment> <entry key="ext_dict">ext.dic</entry> <entry key="ext_stopwords">stopword.dic</entry> </properties>
|
在 stopword.dic 添加停用词
重启elasticsearch
1
| GET /_analyze { "analyzer": "ik_max_word", "text": "我是真的会谢Java就业率超过95%,大帅逼都点赞白嫖,奥力给!" }
|
索引库(表)操作
1. Mapping映射属性
索引库就类似数据库表,mapping映射就类似表的结构。
我们要向es中存储数据,必须先创建“库”和“表”。
mapping是对索引库中文档的约束,常见的mapping属性包括:

1 2 3 4 5 6 7 8 9 10 11 12 13
| { "age": 21, "weight": 52.1, "isMarried": false, "info": "真相只有一个!", "email": "zy@itcast.cn", "score": [99.1, 99.5, 98.9], "name": { "firstName": "柯", "lastName": "南" } }
|

2. 索引库的CRUD
CRUD简单描述:
- 创建索引库:PUT /索引库名
- 查询索引库:GET /索引库名
- 删除索引库:DELETE /索引库名
- 修改索引库(添加字段):PUT /索引库名/_mapping
这里统一使用Kibana编写DSL的方式来演示。
文档操作
1. 文档的CRUD
1.新增文档
1 2 3 4 5 6 7 8 9 10 11
| POST /索引库名/_doc/文档id { "字段1": "值1", "字段2": "值2", "字段3": { "子属性1": "值3", "子属性2": "值4" }, // ... }
|
1 2 3 4 5 6 7 8 9 10 11 12
| POST /heima/_doc/1#索引库 { //
|

在名为 heima 的索引库中创建或更新一条文档,其 ID 为 1。其中,/_doc/1 表示该文档的 ID 是 1,文档内容则包含一些个人信息
如果索引库中不存在 ID 为 1 的文档,它会新建该文档;如果存在,则会更新该文档。
数据存储在名为 heima 的索引库中。索引库相当于数据库中的表。
Elasticsearch 自动为字段生成映射(mapping),它会根据字段的内容自动推断字段的类型。比如:
"info" 和 "email" 会被映射为字符串类型(text 或 keyword)。
"name" 作为一个嵌套对象,会被识别为对象类型,firstName 和 lastName 可能也会被映射为字符串类型。
2.查询文档
根据rest风格,新增是post,查询应该是get,不过查询一般都需要条件,这里我们把文档id带上。
1 2 3
| GET /{索引库名称}/_doc/{id}
GET /{索引库名称}/_search
|

3.删除文档
删除使用DELETE请求,同样,需要根据id进行删除:
1 2
| DELETE /{索引库名}/_doc/id值
|

4.修改文档
修改有两种方式:
- 全量修改:直接覆盖原来的文档
- 增量修改:修改文档中的部分字段
全量修改
全量修改是覆盖原来的文档,其本质是:
注意:如果根据id删除时,id不存在,第二步的新增也会执行,也就从修改变成了新增操作了。
1 2 3 4 5 6 7 8
| PUT /{索引库名}/_doc/文档id { "字段1": "值1", "字段2": "值2", // ... 略 }
|
1 2 3 4 5 6 7 8 9 10
| PUT /heima/_doc/1 { "info": "黑马程序员高级Java讲师", "email": "zy@itcast.cn", "name": { "firstName": "云", "lastName": "赵" } }
|
增量修改
增量修改是只修改指定id匹配的文档中的部分字段。
1 2 3 4 5 6 7
| POST /{索引库名}/_update/文档id { "doc": { "字段名": "新的值", } }
|
1 2 3 4 5 6 7
| POST /heima/_update/1 { "doc": { "email": "ZhaoYun@itcast.cn" } }
|
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
1.Api操作索引库
JavaRestClient操作elasticsearch的流程基本类似。核心是client.indices()方法来获取索引库的操作对象。
索引库操作的基本步骤:【可以根据发送请求那步的第一个参数,发过来判断需要创建什么XXXXRequest】
- 初始化RestHighLevelClient
- 创建XxxIndexRequest。XXX是Create、Get、Delete
- 准备DSL( Create时需要,其它是无参)
- 发送请求。调用RestHighLevelClient#indices().xxx()方法,xxx是create、exists、delete
2.Mapping映射分析
根据MySQL数据库表结构(建表语句),去写索引库结构JSON。表和索引库一一对应
注意:地理坐标、组合字段。索引库里的地理坐标是一个字段:坐标:维度,精度 。copy_to组合字段作用是供用户查询(输入关键字可以查询多个字段)
创建索引库,最关键的是mapping映射,而mapping映射要考虑的信息包括:
1 2 3 4 5
| 字段名 字段数据类型 是否参与搜索 * 是否需要分词 * 如果分词,分词器是什么? *
|
1 2 3 4
| 字段名、字段数据类型,可以参考数据表结构的名称和类型 是否参与搜索要分析业务来判断,例如图片地址,就无需参与搜索 是否分词呢要看内容,内容如果是一个整体就无需分词,反之则要分词 分词器,我们可以统一使用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 51
| 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说明:

3.初始化RestClient
在elasticsearch提供的API中,与elasticsearch一切交互都封装在一个名为RestHighLevelClient的类中,必须先完成这个对象的初始化,建立与elasticsearch的连接。
导入依赖
1 2 3 4 5 6
| 引入es的RestHighLevelClient依赖:
<dependency> <groupId>org.elasticsearch.client</groupId> <artifactId>elasticsearch-rest-high-level-client</artifactId> </dependency>
|
因为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>
|
初始化RestHighLevelClient:这里一般在启动类或者配置类里注入该Bean,用于告诉Java 访问ES的ip地址
1 2 3 4 5 6
| @Bean public RestHighLevelClient client(){ return 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
| 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(); } }
|
4.Spring 索引库 Crud
1.创建索引库
代码分为三步:
- 1)创建Request对象。因为是创建索引库的操作,因此Request是CreateIndexRequest。
- 2)添加请求参数,其实就是DSL的JSON参数部分。因为json字符串很长,这里是定义了静态字符串常量MAPPING_TEMPLATE,让代码看起来更加优雅。
- 3)发送请求,client.indices()方法的返回值是IndicesClient类型,封装了所有与索引库操作有关的方法。
创建索引库的API如下:

在hotel-demo的cn.itcast.hotel.constants包下,创建一个类,定义mapping映射的JSON字符串常量:
HotelConstants –MAPPING_TEMPLATE
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.删除索引库
三步走:
- 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); }
|
3. 查询索引库
三步走:
- 1)创建Request对象。这次是GetIndexRequest对象
- 2)准备参数。这里是无参
- 3)发送请求。改用exists方法
判断索引库是否存在,本质就是查询,对应的DSL是:
GET /hotel
1 2 3 4 5 6 7 8 9 10
| @Test void testExistsHotelIndex() throws IOException { GetIndexRequest request = new GetIndexRequest("hotel"); boolean exists = client.indices().exists(request, RequestOptions.DEFAULT); System.err.println(exists ? "索引库已经存在!" : "索引库不存在!"); }
|
API操作文档
这里更多的是先读取Mysql中的数据,然后再存进ES中。
文档操作的基本步骤:【可以根据发送请求那步的第一个参数,发过来判断需要创建什么XXXXRequest】
- 初始化RestHighLevelClient
- 创建XxxRequest。XXX是Index、Get、Update、Delete、Bulk
- 准备参数(Index、Update、Bulk时需要)
- 发送请求。调用RestHighLevelClient#.xxx()方法,xxx是index、get、update、delete、bulk
- 解析结果(Get时需要)
文档CRUD
1.批量导入文档
三步走:
- 1)创建Request对象。这里是BulkRequest
- 2)准备参数。批处理的参数,就是其它Request对象,这里就是多个IndexRequest
- 3)发起请求。这里是批处理,调用的方法为client.bulk()方法
案例需求:利用BulkRequest批量将数据库数据导入到索引库中。
步骤如下:
- 利用mybatis-plus查询酒店数据
- 将查询到的酒店数据(Hotel)转换为文档类型数据(HotelDoc)
- 利用JavaRestClient中的BulkRequest批处理,实现批量新增文档
语法说明:
批量处理BulkRequest,其本质就是将多个普通的CRUD请求组合在一起发送。
其中提供了一个add方法,用来添加其他请求:

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

我们在导入酒店数据时,将上述代码改造成for循环处理即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| @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); }
|
2.批量 新增文档
四步走:
- 0)创建索引库实体类
- 1)创建Request对象
- 2)准备请求参数,也就是DSL中的JSON文档
- 3)发送请求 (注意:这里直接使用client.xxx()的API,不再需要**client.indices()**了)
我们要将数据库的酒店数据查询出来,写入elasticsearch中。
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
| @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(); } }
|
新增代码
新增文档的DSL语句如下:
1 2 3 4 5
| POST /{索引库名}/_doc/1 { "name": "Jack", "age": 21 }
|
对应的java代码如图:

我们导入酒店数据,基本流程一致,但是需要考虑几点变化:
- 酒店数据来自于数据库,我们需要先查询出来,得到hotel对象
- hotel对象需要转为HotelDoc对象
- HotelDoc需要序列化为json格式
在hotel-demo的HotelDocumentTest测试类中,编写单元测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| @Test void testAddDocument() 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)); request.add(new IndexRequest("xxx")...) } client.bulk(request, RequestOptions.DEFAULT); }
|
3.查询文档
查询文档是根据id查询的,所以没有批量查询
三步走:
- 1)准备Request对象。这次是查询,所以是GetRequest
- 2)发送请求,得到结果。因为是查询,这里调用client.get()方法
- 3)解析结果,就是对JSON做反序列化
查询的DSL语句如下:
非常简单,因此代码大概分两步:
不过查询的目的是得到结果,解析为HotelDoc,因此难点是结果的解析。完整代码如下:

可以看到,结果是一个JSON,其中文档放在一个_source属性中,因此解析就是拿到_source,使用工具反序列化为Java对象即可。
在hotel-demo的HotelDocumentTest测试类中,编写单元测试:
1 2 3 4 5 6 7 8 9 10 11 12 13
| @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); }
|
4.批量删除文档
三步走:
- 1)准备Request对象,因为是删除,这次是DeleteRequest对象。要指定索引库名和id
- 2)准备参数,无参
- 3)发送请求。因为是删除,所以是client.delete()方法
删除的DSL为是这样的:
在hotel-demo的HotelDocumentTest测试类中,编写单元测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| @Test void testDeleteDocument() throws IOException { List<Hotel> list = hotelService.list(); BulkRequest request = new BulkRequest(); for (Hotel hotel : list) { HotelDoc hotelDoc =new HotelDoc(hotel); request.add(new DeleteRequest("hotel") .id(hotel.getId().toString())); } client.bulk(request,RequestOptions.DEFAULT); }
|
5.批量修改文档
三步走:
- 1)准备Request对象。这次是修改,所以是UpdateRequest
- 2)准备参数。也就是JSON文档,里面包含要修改的字段
- 3)更新文档。这里调用client.update()方法
修改有两种方式:
- 全量修改:本质是先根据id删除,再新增
- 增量修改:修改文档中的指定字段值
在RestClient的API中,全量修改与新增的API完全一致,判断依据是ID:
- 如果新增时,ID已经存在,则修改
- 如果新增时,ID不存在,则新增
只演示增量修改:

在hotel-demo的HotelDocumentTest测试类中,编写单元测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| @Test void testUpdateDocument() throws IOException { List<Hotel> list = hotelService.list(); BulkRequest request = new BulkRequest(); for (Hotel hotel : list) { HotelDoc hotelDoc =new HotelDoc(hotel); request.add(new UpdateRequest("hotel",hotel.getId().toString()) .doc( "price", "952", "starName", "四钻" )); } client.bulk(request,RequestOptions.DEFAULT); }
|
Es搜索引擎
DSL查询
1.dsl查询分类
Elasticsearch提供了基于JSON的DSL(Domain Specific Language)来定义查询。常见的查询类型包括:
查询所有:查询出所有数据,一般测试用。例如:match_all
全文检索(full text)查询:利用分词器对用户输入内容分词,然后去倒排索引库中匹配。例如:
- match_query
- multi_match_query
精确查询:根据精确词条值查找数据,一般是查找keyword、数值、日期、boolean等类型字段。例如:
地理(geo)查询:根据经纬度查询。例如:
- geo_distance
- geo_bounding_box
复合(compound)查询:复合查询可以将上述各种查询条件组合起来,合并查询条件。例如:
查询的语法基本一致:
1 2 3 4 5 6 7 8 9
| GET /indexName/_search { "query": { "查询类型": { "查询条件": "条件值" } } }
|
我们以查询所有为例,其中:
1 2 3 4 5 6 7 8
| GET /indexName/_search { "query": { "match_all": { } } }
|
其它查询无非就是查询类型、查询条件的变化。
2.全文检索
match和multi_match的区别是什么?
- match:根据一个字段查询【推荐:使用copy_to构造all字段】
- multi_match:根据多个字段查询,参与查询字段越多,查询性能越差
注:搜索字段越多,对查询性能影响越大,因此建议采用copy_to,然后单字段查询的方式。
全文检索查询的基本流程如下:
- 对用户搜索的内容做分词,得到词条
- 根据词条去倒排索引库中匹配,得到文档id
- 根据文档id找到文档,返回给用户
比较常用的场景包括:
例如京东:

因为是拿着词条去匹配,因此参与搜索的字段也必须是可分词的text类型的字段。
常见的全文检索查询包括:
- match查询:单字段查询
- multi_match查询:多字段查询,任意一个字段符合条件就算符合查询条件
match查询语法如下:
1 2 3 4 5 6 7 8 9
| GET /indexName/_search { "query": { "match": { "FIELD": "TEXT" } } }
|
match查询示例:

mulit_match语法如下:
1 2 3 4 5 6 7 8 9
| GET /indexName/_search { "query": { "multi_match": { "query": "TEXT", "fields": ["FIELD1", " FIELD12"] } } }
|

3.精准查询
精准查询类型:
- term查询:根据词条精确匹配,一般搜索keyword类型、数值类型、布尔类型、日期类型字段
- range查询:根据数值范围查询,可以是数值、日期的范围
精确查询一般是查找keyword、数值、日期、boolean等类型字段。所以不会对搜索条件分词。常见的有:
- term:根据词条精确值查询
- range:根据值的范围查询
1.term查找
1 2 3 4 5 6 7 8 9 10 11 12
| GET /indexName/_search { "query": { "term": { "FIELD": { "value": "VALUE" } } } }
|
示例:
当我搜索的是精确词条时,能正确查询出结果:

但是,当我搜索的内容不是词条,而是多个词语形成的短语时,反而搜索不到:

2.range查询
范围查询,一般应用在对数值类型做范围过滤的时候。比如做价格范围过滤。
1 2 3 4 5 6 7 8 9 10 11 12 13
| GET /indexName/_search { "query": { "range": { "FIELD": { "gte": 10, "lte": 20 } } } }
|

4.地理坐标查询
所谓的地理坐标查询,其实就是根据经纬度查询,官方文档:https://www.elastic.co/guide/en/elasticsearch/reference/current/geo-queries.html
见的使用场景包括:
- 携程:搜索我附近的酒店
- 滴滴:搜索我附近的出租车
- 微信:搜索我附近的人
附近的酒店:

附近的车:

矩形范围查询
很少有业务有这种需求
矩形范围查询,也就是geo_bounding_box查询,查询坐标落在某个矩形范围的所有文档:

查询时,需要指定矩形的左上、右下两个点的坐标,然后画出一个矩形,落在该矩形内的都是符合条件的点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| GET /indexName/_search { "query": { "geo_bounding_box": { "FIELD": { "top_left": { "lat": 31.1, "lon": 121.5 }, "bottom_right": { "lat": 30.9, "lon": 121.7 } } } } }
|
附近(圆形)查询
附近查询,也叫做距离查询(geo_distance):查询到指定中心点小于某个距离值的所有文档。
换句话来说,在地图上找一个点作为圆心,以指定距离为半径,画一个圆,落在圆内的坐标都算符合条件:

1 2 3 4 5 6 7 8 9 10
| GET /indexName/_search { "query": { "geo_distance": { "distance": "15km", "FIELD": "31.21,121.5" } } }
|
例子
我们先搜索陆家嘴附近15km的酒店:

经纬度开始的15km内的酒店
5.复合查询
复合(compound)查询:复合查询可以将其它简单查询组合起来,实现更复杂的搜索逻辑。常见的有两种:
- fuction score:算分函数查询,可以控制文档相关性算分,控制文档排名
- bool query:布尔查询,利用逻辑关系组合多个其它的查询,实现复杂搜索
1.符合查询归纳
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
| GET /hotel/_search { "query": { "function_score": { "query": { // 原始查询,可以是任意条件 "bool": { //bool 查询 must:要求必须匹配的条件。在这里,文档的 city 字段必须是 "上海" 才会被返回。 "must": [ {"term": {"city": "上海" }} ], "should": [//should:是可选的条件,但匹配这些条件的文档会获得更高的评分。在这里,如果文档的 brand 是 "皇冠假日" 或 "华美达",它的评分会增加。 {"term": {"brand": "皇冠假日" }}, {"term": {"brand": "华美达" }} ],///must_not:要求不匹配的条件。这里的条件是不允许 price 字段小于等于 500 的文档。
"must_not": [ { "range": { "price": { "lte": 500 } }} ],//must_not:要求不匹配的条件。这里的条件是不允许 price 字段小于等于 500 的文档。
"filter": [ { "range": {"score": { "gte": 45 } }} ] } }, "functions": [ // 算分函数 { "filter": { // 满足的条件,品牌必须是如家【品牌是如家的才加分,这里是加分条件】 "term": { "brand": "如家" } }, "weight": 2 // 算分权重为2 } ], "boost_mode": "sum" // 加权模式,求和 } } }
|
6.相关性算法分析
省
elasticsearch会根据词条和文档的相关度做打分,算法由两种:
- TF-IDF算法
- BM25算法,elasticsearch5.1版本后采用的算法
当我们利用match查询时,文档结果会根据与搜索词条的关联度打分(_score),返回结果时按照分值降序排列。
例如,我们搜索 “虹桥如家”,结果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| [ { "_score" : 17.850193, "_source" : { "name" : "虹桥如家酒店真不错", } }, { "_score" : 12.259849, "_source" : { "name" : "外滩如家酒店真不错", } }, { "_score" : 11.91091, "_source" : { "name" : "迪士尼如家酒店真不错", } } ]
|
在elasticsearch中,早期使用的打分算法是TF-IDF算法,公式如下:

在后来的5.1版本升级中,elasticsearch将算法改进为BM25算法,公式如下:

TF-IDF算法有一各缺陷,就是词条频率越高,文档得分也会越高,单个词条对文档影响较大。而BM25则会让单个词条的算分有一个上限,曲线更加平滑:

7.算分函数查询
在搜索出来的结果的分数基础上,再手动与指定的数字进行一定运算来改变算分,从而改变结果的排序。
function score query定义的三要素是什么?
- 过滤条件:哪些文档要加分
- 算分函数:如何计算function score
- 加权方式:function score 与 query score如何运算
根据相关度打分是比较合理的需求,但合理的不一定是产品经理需要的。
以百度为例,你搜索的结果中,并不是相关度越高排名越靠前,而是谁掏的钱多排名就越靠前。如图:

要想认为控制相关性算分,就需要利用elasticsearch中的function 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)基于运算模式做运算,得到最终结果,作为相关性算分。
需求:给“如家”这个品牌的酒店排名靠前一些
翻译一下这个需求,转换为之前说的四个要点:
- 原始条件:不确定,可以任意变化
- 过滤条件:brand = “如家”
- 算分函数:可以简单粗暴,直接给固定的算分结果,weight
- 运算模式:比如求和
因此最终的DSL语句如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| GET /hotel/_search { "query": { "function_score": { "query": { .... }, "functions": [ { "filter": { "term": { "brand": "如家" } }, "weight": 2 } ], "boost_mode": "sum" } } }
|
测试,在未添加算分函数时,如家得分如下:

添加了算分函数后,如家得分就提升了:

8.布尔查询
省
设置查询结果
搜索结果种类
排序
1.地址坐标排序
地理坐标排序略有不同。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| GET /indexName/_search { "query": { "match_all": {} }, "sort": [ { "_geo_distance" : { "FIELD" : "纬度,经度", "order" : "asc", "unit" : "km" } } ] }
|
这个查询的含义是:
- 指定一个坐标,作为目标点
- 计算每一个文档中,指定字段(必须是geo_point类型)的坐标 到目标点的距离是多少
- 根据距离排序
示例
需求描述:实现对酒店数据按照到你的位置坐标的距离升序排序
提示:获取你的位置的经纬度的方式:https://lbs.amap.com/demo/jsapi-v2/example/map/click-to-get-lnglat/
假设我的位置是:31.034661,121.612282,寻找我周围距离最近的酒店。

2.分页
3.高亮
数据聚合
RestClient查询文档
文档的查询同样适用昨天学习的 RestHighLevelClient对象,基本步骤包括:
- 1)准备Request对象
- 2)准备请求参数
- 3)发起请求
- 4)解析响应
快速入门
查询的基本步骤是:
创建SearchRequest对象
准备Request.source(),也就是DSL。
① QueryBuilders来构建查询条件
② 传入Request.source() 的 query() 方法
发送请求,得到结果
解析结果(参考JSON结果,从外到内,逐层解析)

第一步,创建SearchRequest对象,指定索引库名
第二步,利用request.source()构建DSL,DSL中可以包含查询、分页、排序、高亮等
query():代表查询条件,利用QueryBuilders.matchAllQuery()构建一个match_all查询的DSL
第三步,利用client.search()发送请求,得到响应
这里关键的API有两个,一个是request.source(),其中包含了查询、排序、分页、高亮等所有功能:

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

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

elasticsearch返回的结果是一个JSON字符串,结构包含:
因此,我们解析响应结果,就是逐层解析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
| @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); } }
|
条件查询
1.全文检索查询
全文检索的match和multi_match查询与match_all的API基本一致。差别是查询条件,也就是query的部分。

1 2 3 4 5 6 7 8 9 10 11 12
| @Test void testMatch throw Ioexception{ SearchRequest request = new SearchRequest("hotel"); request.source().query(QueryBuilders.matchQuery("all","如家")); SearchResponse response = client.search(request, RequestOptions.DEFAULT); handleResponse(response);
}
|
2.精准查找
精确查询主要是两者:
与之前的查询相比,差异同样在查询条件,其它都一样。

1 2 3 4 5 6 7 8 9 10 11
| @Test void testMatch throw Ioexception{ SearchRequest request = new SearchRequest("hotel"); request.source().query(QueryBuilders.termQuery("city","杭州")); SearchResponse response = client.search(request, RequestOptions.DEFAULT); handleResponse(response);
}
|
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
| @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); } }
|
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 20
| @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);
}
|
5.算分函数
6.设置搜索结果
7.高亮
8.聚合
自动补全
Es Mysql数据同步
Es集群
//项目用到自然会讲解
实际使用
1.kafka配和更新
具体就是拿到新增的分值-然后传到es
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
| public class ArticleIncrHandleListener {
@Autowired RedisTemplate redisTemplate; @Autowired RestHighLevelClient restHighLevelClient; @KafkaListener(topics = HotArticleConstants.HOT_ARTICLE_CONSUMER_QUEUE) public void handle(String message) { ArticleVisitStreamMess mess= JSON.parseObject(message, ArticleVisitStreamMess.class); savemess(mess);
log.info("收到消息:{}", message); }
private void savemess(ArticleVisitStreamMess mess) { int score=mess.getLike()*5+mess.getComment()*2+mess.getView(); redisTemplate.opsForZSet().add(HotArticleConstants.HOT_ARTICLE_REDIS_QUEUE,mess.getMuicid(),score);
UpdateRequest updateRquest=new UpdateRequest(); updateRquest.index("online_music_index").id(mess.getMuicid()).doc(mess);
Map<String, Object> params = new HashMap<>(); params.put("view", mess.getView()); params.put("comment", mess.getComment()); params.put("like", mess.getLike());
Script script = new Script(ScriptType.INLINE, "painless", "ctx._source.comments += params.comment;" + "ctx._source.views += params.view;" + "ctx._source.likes += params.like;", params); updateRquest.script(script); try { restHighLevelClient.update(updateRquest, RequestOptions.DEFAULT); } catch (Exception e) { e.printStackTrace(); }
} }
|
2.页面的附加展示和推荐
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 54 55 56 57 58 59 60 61 62 63 64 65 66 67
| @Autowired RestHighLevelClient restHighLevelClient;
@Override public R show(OnlineMusic onlineMusic) {
Set<String> set = redisTemplate.opsForZSet().reverseRange(HotArticleConstants.HOT_ARTICLE_REDIS_QUEUE, onlineMusic.getPage() * 10 - 10, onlineMusic.getPage() * 10);
OnlineMusicDoc doc = new OnlineMusicDoc(onlineMusic); GeoPoint geoPoint = new GeoPoint(doc.getLocation()); SearchRequest request = new SearchRequest("online_music_index"); if(doc.getLocation().isEmpty()) {
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); searchSourceBuilder.query(QueryBuilders.termsQuery("music", set));
request.source(searchSourceBuilder); try { SearchResponse search = restHighLevelClient.search(request, RequestOptions.DEFAULT); }catch(Exception e) { e.printStackTrace(); }
}else {
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder(); sourceBuilder.sort(SortBuilders.geoDistanceSort("location", geoPoint) .order(SortOrder.ASC) .unit(org.elasticsearch.common.unit.DistanceUnit.KILOMETERS)); sourceBuilder.query(QueryBuilders.termsQuery("music", set)); request.source(sourceBuilder); } List<Map<String, Object>> resultList = new ArrayList<>(); try { SearchResponse search = restHighLevelClient.search(request, RequestOptions.DEFAULT);
SearchHit[] hits = search.getHits().getHits(); for (SearchHit hit : hits) { Map<String, Object> sourceAsMap = hit.getSourceAsMap(); resultList.add(sourceAsMap); }
} catch (Exception e) { e.printStackTrace(); }
return R.success(resultList); }
|