elasticsearch ~ 从入门到入坑。
文章目录
分布式搜索引擎 01 – elasticsearch 基础。
0. 学习目标。
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. 为什么不是其他搜索技术?
目前比较知名的搜索引擎技术排名。
https://db-engines.com/en/ranking/search+engine
虽然在早期,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. 正向和倒排。
那么为什么一个叫做正向索引,一个叫做倒排索引呢?
-
正向索引
是最传统的,根据 id 索引的方式。但根据词条查询时,必须先逐条获取每个文档,然后判断文档中是否包含所需要的词条,是**
根据文档找词条的过程**。 -
而倒排索引则相反,是先找到用户要搜索的词条,根据词条得到保护词条的文档的 id,然后根据 id 获取文档。是根据词条找文档的过程。
是不是恰好反过来了?
那么两者方式的优缺点是什么呢?
正向索引。
-
优点。
- 可以给多个字段创建索引。
- 根据索引字段搜索、排序速度非常快。
-
缺点。
- 根据非索引字段,或者索引字段中的部分词条查找时,只能全表扫描。
倒排索引。
优点。
- 根据词条搜索、模糊搜索时,速度非常快。
缺点。
-
只能给词条创建索引,而不是字段。
-
无法根据字段做排序。
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:擅长海量数据的搜索、分析、计算。
因此在企业中,往往是两者结合使用。
-
对安全性要求较高的写操作,使用 mysql 实现。
-
对查询性能要求较高的搜索需求,使用 elasticsearch 实现。
-
两者再基于某种方式,实现数据的同步,保证一致性。
1.4. 安装 es、kibana。
1.4.1. 安装。
1. 部署单点 es。
1.1. 创建网络。
因为我们还需要部署 kibana 容器,因此需要让 es 和 kibana 容器互联。这里先创建一个网络。
docker network create es-net
1.2. 加载镜像。
这里我们采用 elasticsearch 的 7.12.1 版本的镜像,这个镜像体积非常大,接近1G。不建议大家自己 pull。
上传到虚拟机中,然后运行命令加载即可。
docker load -i es.tar
同理还有 kibana
的 tar 包也需要这样做。
1.3. 运行。
运行 docker 命令,部署单点 es。
$ sudo docker run -d \
--name elasticsearch \
--net es-net \
-p 9200:9200 \
-p 9300:9300 \
-e "discovery.type=single-node" \
-e "ES_JAVA_OPTS=-Xms512m -Xmx512m" \
-v es-data:/usr/share/elasticsearch/data \
-v es-plugins:/usr/share/elasticsearch/plugins \
--privileged \
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 端口。-p 9300:9300
:端口映射配置。es 节点互联端口。
docker: Error response from daemon: driver failed programming external connectivity on endpoint kibana (b819727f0cdc3aa21fa114463099750d9bbc50e503e2a83fce69ccdf71613988): (iptables failed: iptables --wait -t nat -A DOCKER -p tcp -d 0/0 --dport 5601 -j DNAT --to-destination 172.19.0.3:5601 ! -i br-d88f3e3bb288: iptables: No chain/target/match by that name.
(exit status 1)).
重启 docker。
在浏览器中输入:http://192.168.142.168:9200 即可看到 elasticsearch 的响应结果。
2. 部署 kibana。
kibana 可以给我们提供一个 elasticsearch 的可视化界面,便于我们学习。
2.1. 部署。
运行 docker 命令,部署 kibana。
$ sudo docker run -d \
--name kibana \
--net es-net \
-p 5601:5601 \
-e ELASTICSEARCH_HOSTS=http://elasticsearch:9200 \
kibana:7.12.1
-
--network es-net
:加入一个名为 es-net 的网络中,与 elasticsearch 在同一个网络中。 -
-e ELASTICSEARCH_HOSTS=http://elasticsearch:9200"
:elasticsearch 容器的名称。设置 elasticsearch 的地址,因为 kibana 已经与 elasticsearch 在一个网络,因此可以用容器名直接访问 elasticsearch。 -
-p 5601:5601
:端口映射配置。
kibana 启动一般比较慢,需要多等待一会,可以通过命令。
docker logs -f kibana
查看运行日志,当查看到下面的日志,说明成功。
[geek@localhost ~]$ sudo docker logs -f kibana
{"type":"log","@timestamp":"2023-04-04T06:23:47+00:00","tags":["info","plugins-service"],"pid":7,"message":"Plugin \"osquery\" is disabled."}
{"type":"log","@timestamp":"2023-04-04T06:23:47+00:00","tags":["warning","config","deprecation"],"pid":7,"message":"Support for setting server.host to \"0\" in kibana.yml is deprecated and will be removed in Kibana version 8.0.0. Instead use \"0.0.0.0\" to bind to all interfaces."}
{"type":"log","@timestamp":"2023-04-04T06:23:47+00:00","tags":["warning","config","deprecation"],"pid":7,"message":"Config key [monitoring.cluster_alerts.email_notifications.email_address] will be required for email notifications to work in 8.0.\""}
{"type":"log","@timestamp":"2023-04-04T06:23:47+00:00","tags":["info","plugins-system"],"pid":7,"message":"Setting up [100] plugins: [taskManager,licensing,globalSearch,globalSearchProviders,banners,code,usageCollection,xpackLegacy,telemetryCollectionManager,telemetry,telemetryCollectionXpack,kibanaUsageCollection,securityOss,share,newsfeed,mapsLegacy,kibanaLegacy,translations,legacyExport,embeddable,uiActionsEnhanced,esUiShared,expressions,charts,bfetch,data,home,observability,apmOss,console,consoleExtensions,searchprofiler,painlessLab,grokdebugger,management,indexPatternManagement,advancedSettings,fileUpload,savedObjects,visualizations,visTypeMetric,visTypeVislib,visTypeVega,visTypeMarkdown,visTypeTagcloud,visTypeTable,visTypeTimelion,features,licenseManagement,watcher,canvas,tileMap,regionMap,visTypeXy,graph,timelion,dashboard,dashboardEnhanced,visualize,visTypeTimeseries,inputControlVis,discover,discoverEnhanced,savedObjectsManagement,spaces,security,savedObjectsTagging,maps,lens,reporting,lists,encryptedSavedObjects,dataEnhanced,dashboardMode,cloud,upgradeAssistant,snapshotRestore,enterpriseSearch,fleet,indexManagement,rollup,remoteClusters,crossClusterReplication,indexLifecycleManagement,beatsManagement,transform,ingestPipelines,eventLog,actions,alerts,triggersActionsUi,stackAlerts,ml,securitySolution,case,infra,monitoring,logstash,apm,uptime]"}
{"type":"log","@timestamp":"2023-04-04T06:23:47+00:00","tags":["info","plugins","taskManager"],"pid":7,"message":"TaskManager is identified by the Kibana UUID: 9ba02435-1e96-4063-a854-bb977a68fab6"}
{"type":"log","@timestamp":"2023-04-04T06:23:48+00:00","tags":["warning","plugins","security","config"],"pid":7,"message":"Generating a random key for xpack.security.encryptionKey. To prevent sessions from being invalidated on restart, please set xpack.security.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command."}
{"type":"log","@timestamp":"2023-04-04T06:23:48+00:00","tags":["warning","plugins","security","config"],"pid":7,"message":"Session cookies will be transmitted over insecure connections. This is not recommended."}
{"type":"log","@timestamp":"2023-04-04T06:23:48+00:00","tags":["warning","plugins","reporting","config"],"pid":7,"message":"Generating a random key for xpack.reporting.encryptionKey. To prevent sessions from being invalidated on restart, please set xpack.reporting.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command."}
{"type":"log","@timestamp":"2023-04-04T06:23:48+00:00","tags":["warning","plugins","reporting","config"],"pid":7,"message":"Chromium sandbox provides an additional layer of protection, but is not supported for Linux CentOS 8.3.2011\n OS. Automatically setting 'xpack.reporting.capture.browser.chromium.disableSandbox: true'."}
{"type":"log","@timestamp":"2023-04-04T06:23:48+00:00","tags":["warning","plugins","encryptedSavedObjects"],"pid":7,"message":"Saved objects encryption key is not set. This will severely limit Kibana functionality. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command."}
{"type":"log","@timestamp":"2023-04-04T06:23:48+00:00","tags":["warning","plugins","fleet"],"pid":7,"message":"Fleet APIs are disabled because the Encrypted Saved Objects plugin is missing encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command."}
{"type":"log","@timestamp":"2023-04-04T06:23:48+00:00","tags":["warning","plugins","actions","actions"],"pid":7,"message":"APIs are disabled because the Encrypted Saved Objects plugin is missing encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command."}
{"type":"log","@timestamp":"2023-04-04T06:23:48+00:00","tags":["warning","plugins","alerts","plugins","alerting"],"pid":7,"message":"APIs are disabled because the Encrypted Saved Objects plugin is missing encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command."}
{"type":"log","@timestamp":"2023-04-04T06:23:48+00:00","tags":["info","plugins","monitoring","monitoring"],"pid":7,"message":"config sourced from: production cluster"}
{"type":"log","@timestamp":"2023-04-04T06:23:48+00:00","tags":["info","savedobjects-service"],"pid":7,"message":"Waiting until all Elasticsearch nodes arecompatible with Kibana before starting saved objects migrations..."}
{"type":"log","@timestamp":"2023-04-04T06:23:48+00:00","tags":["info","savedobjects-service"],"pid":7,"message":"Starting saved objects migrations"}
{"type":"log","@timestamp":"2023-04-04T06:23:49+00:00","tags":["info","savedobjects-service"],"pid":7,"message":"[.kibana_task_manager] INIT -> CREATE_NEW_TARGET. took: 39ms."}
{"type":"log","@timestamp":"2023-04-04T06:23:49+00:00","tags":["info","savedobjects-service"],"pid":7,"message":"[.kibana] INIT -> CREATE_NEW_TARGET. took: 97ms."}
{"type":"log","@timestamp":"2023-04-04T06:23:49+00:00","tags":["info","savedobjects-service"],"pid":7,"message":"[.kibana_task_manager] CREATE_NEW_TARGET -> MARK_VERSION_INDEX_READY. took: 926ms."}
{"type":"log","@timestamp":"2023-04-04T06:23:49+00:00","tags":["info","savedobjects-service"],"pid":7,"message":"[.kibana] CREATE_NEW_TARGET -> MARK_VERSION_INDEX_READY. took: 924ms."}
{"type":"log","@timestamp":"2023-04-04T06:23:50+00:00","tags":["info","savedobjects-service"],"pid":7,"message":"[.kibana_task_manager] MARK_VERSION_INDEX_READY -> DONE. took: 82ms."}
{"type":"log","@timestamp":"2023-04-04T06:23:50+00:00","tags":["info","savedobjects-service"],"pid":7,"message":"[.kibana_task_manager] Migration completed after 1047ms"}
{"type":"log","@timestamp":"2023-04-04T06:23:50+00:00","tags":["info","savedobjects-service"],"pid":7,"message":"[.kibana] MARK_VERSION_INDEX_READY -> DONE. took: 70ms."}
{"type":"log","@timestamp":"2023-04-04T06:23:50+00:00","tags":["info","savedobjects-service"],"pid":7,"message":"[.kibana] Migration completed after 1091ms"}
{"type":"log","@timestamp":"2023-04-04T06:23:50+00:00","tags":["info","plugins-system"],"pid":7,"message":"Starting [100] plugins: [taskManager,licensing,globalSearch,globalSearchProviders,banners,code,usageCollection,xpackLegacy,telemetryCollectionManager,telemetry,telemetryCollectionXpack,kibanaUsageCollection,securityOss,share,newsfeed,mapsLegacy,kibanaLegacy,translations,legacyExport,embeddable,uiActionsEnhanced,esUiShared,expressions,charts,bfetch,data,home,observability,apmOss,console,consoleExtensions,searchprofiler,painlessLab,grokdebugger,management,indexPatternManagement,advancedSettings,fileUpload,savedObjects,visualizations,visTypeMetric,visTypeVislib,visTypeVega,visTypeMarkdown,visTypeTagcloud,visTypeTable,visTypeTimelion,features,licenseManagement,watcher,canvas,tileMap,regionMap,visTypeXy,graph,timelion,dashboard,dashboardEnhanced,visualize,visTypeTimeseries,inputControlVis,discover,discoverEnhanced,savedObjectsManagement,spaces,security,savedObjectsTagging,maps,lens,reporting,lists,encryptedSavedObjects,dataEnhanced,dashboardMode,cloud,upgradeAssistant,snapshotRestore,enterpriseSearch,fleet,indexManagement,rollup,remoteClusters,crossClusterReplication,indexLifecycleManagement,beatsManagement,transform,ingestPipelines,eventLog,actions,alerts,triggersActionsUi,stackAlerts,ml,securitySolution,case,infra,monitoring,logstash,apm,uptime]"}
{"type":"log","@timestamp":"2023-04-04T06:23:52+00:00","tags":["listening","info"],"pid":7,"message":"Server running at http://0.0.0.0:5601"}
{"type":"log","@timestamp":"2023-04-04T06:23:53+00:00","tags":["info","http","server","Kibana"],"pid":7,"message":"http server running at http://0.0.0.0:5601"}
{"type":"log","@timestamp":"2023-04-04T06:23:53+00:00","tags":["info","plugins","watcher"],"pid":7,"message":"Your basic license does not support watcher.Please upgrade your license."}
{"type":"log","@timestamp":"2023-04-04T06:23:53+00:00","tags":["info","plugins","crossClusterReplication"],"pid":7,"message":"Your basic license does not support crossClusterReplication. Please upgrade your license."}
{"type":"log","@timestamp":"2023-04-04T06:23:53+00:00","tags":["info","plugins","monitoring","monitoring","kibana-monitoring"],"pid":7,"message":"Startingmonitoring stats collection"}
{"type":"log","@timestamp":"2023-04-04T06:23:54+00:00","tags":["warning","plugins","reporting"],"pid":7,"message":"Enabling the Chromium sandbox provides an additional layer of protection."}
此时,在浏览器输入地址访问:http://192.168.142.186:5601,即可看到结果。
management - dev tools
GET _search
{
"query": {
"match_all": {}
}
}
测试 es 是否连接。
GET /
{
"name" : "a5520c185ef4",
"cluster_name" : "docker-cluster",
"cluster_uuid" : "PKEq6eeFSQe4XQijZPwUCg",
"version" : {
"number" : "7.12.1",
"build_flavor" : "default",
"build_type" : "docker",
"build_hash" : "3186837139b9c6b6d23c3200870651f10d3343b7",
"build_date" : "2021-04-20T20:56:39.040728659Z",
"build_snapshot" : false,
"lucene_version" : "8.8.0",
"minimum_wire_compatibility_version" : "6.8.0",
"minimum_index_compatibility_version" : "6.0.0-beta1"
},
"tagline" : "You Know, for Search"
}
2.2. DevTools。
kibana 中提供了一个 DevTools 界面。
这个界面中可以编写 DSL 来操作 elasticsearch。并且对 DSL 语句有自动补全功能。
3. 安装 IK 分词器。
GET /_analyze
{
"analyzer": "standard",
"text": "Geek 学习 Java 太棒了。"
}
{
"tokens" : [
{
"token" : "geek",
"start_offset" : 0,
"end_offset" : 4,
"type" : "<ALPHANUM>",
"position" : 0
},
{
"token" : "学",
"start_offset" : 5,
"end_offset" : 6,
"type" : "<IDEOGRAPHIC>",
"position" : 1
},
{
"token" : "习",
"start_offset" : 6,
"end_offset" : 7,
"type" : "<IDEOGRAPHIC>",
"position" : 2
},
{
"token" : "java",
"start_offset" : 8,
"end_offset" : 12,
"type" : "<ALPHANUM>",
"position" : 3
},
{
"token" : "太",
"start_offset" : 13,
"end_offset" : 14,
"type" : "<IDEOGRAPHIC>",
"position" : 4
},
{
"token" : "棒",
"start_offset" : 14,
"end_offset" : 15,
"type" : "<IDEOGRAPHIC>",
"position" : 5
},
{
"token" : "了",
"start_offset" : 15,
"end_offset" : 16,
"type" : "<IDEOGRAPHIC>",
"position" : 6
}
]
}
3.1. 在线安装 ik 插件(较慢)。
https://github.com/medcl/elasticsearch-analysis-ik/releases
# 进入容器内部。
docker exec -it elasticsearch /bin/bash
# 在线下载并安装。
[geek@localhost ~]$ sudo docker exec -it elasticsearch /bin/bash
[root@a5520c185ef4 elasticsearch]# ls
LICENSE.txt NOTICE.txt README.asciidoc bin config data jdk lib logs modules plugins
[root@a5520c185ef4 elasticsearch]# ./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.12.1/elasticsearch-analysis-ik-7.12.1.zip
-> Installing https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.12.1/elasticsearch-analysis-ik-7.12.1.zip
-> Downloading https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.12.1/elasticsearch-analysis-ik-7.12.1.zip
[=================================================] 100%??
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@ WARNING: plugin requires additional permissions @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
* java.net.SocketPermission * connect,resolve
See http://docs.oracle.com/javase/8/docs/technotes/guides/security/permissions.html
for descriptions of what these permissions allow and the associated risks.
Continue with installation? [y/N]y
-> Installed analysis-ik
-> Please restart Elasticsearch to activate any plugins installed
[root@a5520c185ef4 elasticsearch]#
# 退出。
exit
# 重启容器。
docker restart elasticsearch
3.2. 离线安装 ik 插件(推荐)。
1)查看数据卷目录。
安装插件需要知道 elasticsearch 的 plugins 目录位置,而我们用了数据卷挂载,因此需要查看 elasticsearch 的数据卷目录,通过下面命令查看。
[geek@localhost ~]$ sudo docker volume inspect es-plugins
[sudo] password for geek:
[
{
"CreatedAt": "2023-04-04T14:01:25+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
这个目录中。
2)解压缩分词器安装包。
下面我们需要把资料中的 ik 分词器解压缩,重命名为 ik。
3)上传到 es 容器的插件数据卷中。
也就是 /var/lib/docker/volumes/es-plugins/_data
。
4)重启容器。
# 4、重启容器。
docker restart elasticsearch
# 查看 es 日志。
docker logs -f elasticsearch
5)测试。
IK 分词器包含两种模式。
-
ik_smart
:最少切分。 -
ik_max_word
:最细切分。
GET /_analyze
{
"analyzer": "ik_smart",
"text": "Geek 学习 Java 太棒了。"
}
结果。
{
"tokens" : [
{
"token" : "geek",
"start_offset" : 0,
"end_offset" : 4,
"type" : "ENGLISH",
"position" : 0
},
{
"token" : "程序员",
"start_offset" : 5,
"end_offset" : 8,
"type" : "CN_WORD",
"position" : 1
},
{
"token" : "学习",
"start_offset" : 8,
"end_offset" : 10,
"type" : "CN_WORD",
"position" : 2
},
{
"token" : "java",
"start_offset" : 11,
"end_offset" : 15,
"type" : "ENGLISH",
"position" : 3
},
{
"token" : "太棒了",
"start_offset" : 16,
"end_offset" : 19,
"type" : "CN_WORD",
"position" : 4
}
]
}
GET /_analyze
{
"analyzer": "ik_max_word",
"text": "Geek程序员学习Java太棒了。"
}
{
"tokens" : [
{
"token" : "geek",
"start_offset" : 0,
"end_offset" : 4,
"type" : "ENGLISH",
"position" : 0
},
{
"token" : "程序员",
"start_offset" : 4,
"end_offset" : 7,
"type" : "CN_WORD",
"position" : 1
},
{
"token" : "程序",
"start_offset" : 4,
"end_offset" : 6,
"type" : "CN_WORD",
"position" : 2
},
{
"token" : "员",
"start_offset" : 6,
"end_offset" : 7,
"type" : "CN_CHAR",
"position" : 3
},
{
"token" : "学习",
"start_offset" : 7,
"end_offset" : 9,
"type" : "CN_WORD",
"position" : 4
},
{
"token" : "java",
"start_offset" : 9,
"end_offset" : 13,
"type" : "ENGLISH",
"position" : 5
},
{
"token" : "太棒了",
"start_offset" : 13,
"end_offset" : 16,
"type" : "CN_WORD",
"position" : 6
},
{
"token" : "太棒",
"start_offset" : 13,
"end_offset" : 15,
"type" : "CN_WORD",
"position" : 7
},
{
"token" : "了",
"start_offset" : 15,
"end_offset" : 16,
"type" : "CN_CHAR",
"position" : 8
}
]
}
3.3. 扩展词词典。
随着互联网的发展,“造词运动”也越发的频繁。出现了很多新的词语,在原有的词汇列表中并不存在。比如:“奥力给”,“传智播客” 等。
GET /_analyze
{
"analyzer": "ik_max_word",
"text": "传智教育的课程可以白嫖,传智播客 Java 就业率超过 90%,奥力给!"
}
{
"tokens" : [
{
"token" : "传",
"start_offset" : 0,
"end_offset" : 1,
"type" : "CN_CHAR",
"position" : 0
},
{
"token" : "智",
"start_offset" : 1,
"end_offset" : 2,
"type" : "CN_CHAR",
"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" : "可以",
"start_offset" : 7,
"end_offset" : 9,
"type" : "CN_WORD",
"position" : 5
},
{
"token" : "白",
"start_offset" : 9,
"end_offset" : 10,
"type" : "CN_CHAR",
"position" : 6
},
{
"token" : "嫖",
"start_offset" : 10,
"end_offset" : 11,
"type" : "CN_CHAR",
"position" : 7
},
{
"token" : "传",
"start_offset" : 12,
"end_offset" : 13,
"type" : "CN_CHAR",
"position" : 8
},
{
"token" : "智",
"start_offset" : 13,
"end_offset" : 14,
"type" : "CN_CHAR",
"position" : 9
},
{
"token" : "播",
"start_offset" : 14,
"end_offset" : 15,
"type" : "CN_CHAR",
"position" : 10
},
{
"token" : "客",
"start_offset" : 15,
"end_offset" : 16,
"type" : "CN_CHAR",
"position" : 11
},
{
"token" : "java",
"start_offset" : 17,
"end_offset" : 21,
"type" : "ENGLISH",
"position" : 12
},
{
"token" : "就业率",
"start_offset" : 22,
"end_offset" : 25,
"type" : "CN_WORD",
"position" : 13
},
{
"token" : "就业",
"start_offset" : 22,
"end_offset" : 24,
"type" : "CN_WORD",
"position" : 14
},
{
"token" : "率",
"start_offset" : 24,
"end_offset" : 25,
"type" : "CN_CHAR",
"position" : 15
},
{
"token" : "超过",
"start_offset" : 25,
"end_offset" : 27,
"type" : "CN_WORD",
"position" : 16
},
{
"token" : "90",
"start_offset" : 28,
"end_offset" : 30,
"type" : "ARABIC",
"position" : 17
},
{
"token" : "奥",
"start_offset" : 32,
"end_offset" : 33,
"type" : "CN_CHAR",
"position" : 18
},
{
"token" : "力",
"start_offset" : 33,
"end_offset" : 34,
"type" : "CN_CHAR",
"position" : 19
},
{
"token" : "给",
"start_offset" : 34,
"end_offset" : 35,
"type" : "CN_CHAR",
"position" : 20
}
]
}
所以我们的词汇也需要不断的更新,IK 分词器提供了扩展词汇的功能。
1)打开 IK 分词器 config 目录。
/usr/share/elasticsearch/plugins/ik/config/ext_dict.dic
<?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"></entry>
<!-- 用户可以在这里配置自己的扩展停止词字典。 -->
<entry key="ext_stopwords"></entry>
<!-- 用户可以在这里配置远程扩展字典。 -->
<!-- <entry key="remote_ext_dict">words_location</entry> -->
<!-- 用户可以在这里配置远程扩展停止词字典。 -->
<!-- <entry key="remote_ext_stopwords">words_location</entry> -->
</properties>
2)在 IKAnalyzer.cfg.xml 配置文件内容添加。
<?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_dict.dic</entry>
<!-- 用户可以在这里配置自己的扩展停止词字典。 -->
<entry key="ext_stopwords">ext_stop_words.dic</entry>
<!-- 用户可以在这里配置远程扩展字典。 -->
<!-- <entry key="remote_ext_dict">words_location</entry> -->
<!-- 用户可以在这里配置远程扩展停止词字典。 -->
<!-- <entry key="remote_ext_stopwords">words_location</entry> -->
</properties>
3)新建一个 ext_dict.dic,可以参考 config 目录下复制一个配置文件进行修改。
传智教育
奥力给
4)重启 elasticsearch 。
docker restart elasticsearch
# 查看日志。
docker logs -f elasticsearch
日志中已经成功加载 ext.dic 配置文件。
5)测试效果。
GET /_analyze
{
"analyzer": "ik_max_word",
"text": "传智教育的课程可以白嫖,传智播客 Java 就业率超过 90%,奥力给!"
}
{
"tokens" : [
{
"token" : "传",
"start_offset" : 0,
"end_offset" : 1,
"type" : "CN_CHAR",
"position" : 0
},
{
"token" : "智",
"start_offset" : 1,
"end_offset" : 2,
"type" : "CN_CHAR",
"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" : "可以",
"start_offset" : 7,
"end_offset" : 9,
"type" : "CN_WORD",
"position" : 5
},
{
"token" : "白嫖",
"start_offset" : 9,
"end_offset" : 11,
"type" : "CN_WORD",
"position" : 6
},
{
"token" : "传智播客",
"start_offset" : 12,
"end_offset" : 16,
"type" : "CN_WORD",
"position" : 7
},
{
"token" : "java",
"start_offset" : 17,
"end_offset" : 21,
"type" : "ENGLISH",
"position" : 8
},
{
"token" : "就业率",
"start_offset" : 22,
"end_offset" : 25,
"type" : "CN_WORD",
"position" : 9
},
{
"token" : "超过",
"start_offset" : 25,
"end_offset" : 27,
"type" : "CN_WORD",
"position" : 10
},
{
"token" : "90",
"start_offset" : 28,
"end_offset" : 30,
"type" : "ARABIC",
"position" : 11
},
{
"token" : "奥力给",
"start_offset" : 32,
"end_offset" : 35,
"type" : "CN_WORD",
"position" : 12
}
]
}
注意当前文件的编码必须是 UTF-8 格式,严禁使用 Windows 记事本编辑。
3.4. 停用词词典。
在互联网项目中,在网络间传输的速度很快,所以很多语言是不允许在网络上传递的,如:关于宗教、政治等敏感词语,那么我们在搜索时也应该忽略当前词汇。
IK 分词器也提供了强大的停用词功能,让我们在索引时就直接忽略当前的停用词汇表中的内容。
1)IKAnalyzer.cfg.xml 配置文件内容添加。
<?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>
3)在 stopword.dic 添加停用词。
万维网
4)重启 elasticsearch 。
# 重启服务。
docker restart elasticsearch
docker restart kibana
# 查看日志。
docker logs -f elasticsearch
日志中已经成功加载 stopword.dic 配置文件。
5)测试效果。
4. 部署 es 集群。
部署 es 集群可以直接使用 docker-compose 来完成,不过要求你的 Linux 虚拟机至少有 4G 的内存空间。
首先编写一个 docker-compose 文件,内容如下。
version: '2.2'
services:
es01:
image: docker.elastic.co/elasticsearch/elasticsearch:7.12.1
container_name: es01
environment:
- node.name=es01
- cluster.name=es-docker-cluster
- discovery.seed_hosts=es02,es03
- cluster.initial_master_nodes=es01,es02,es03
- bootstrap.memory_lock=true
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
ulimits:
memlock:
soft: -1
hard: -1
volumes:
- data01:/usr/share/elasticsearch/data
ports:
- 9200:9200
networks:
- elastic
es02:
image: docker.elastic.co/elasticsearch/elasticsearch:7.12.1
container_name: es02
environment:
- node.name=es02
- cluster.name=es-docker-cluster
- discovery.seed_hosts=es01,es03
- cluster.initial_master_nodes=es01,es02,es03
- bootstrap.memory_lock=true
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
ulimits:
memlock:
soft: -1
hard: -1
volumes:
- data02:/usr/share/elasticsearch/data
networks:
- elastic
es03:
image: docker.elastic.co/elasticsearch/elasticsearch:7.12.1
container_name: es03
environment:
- node.name=es03
- cluster.name=es-docker-cluster
- discovery.seed_hosts=es01,es02
- cluster.initial_master_nodes=es01,es02,es03
- bootstrap.memory_lock=true
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
ulimits:
memlock:
soft: -1
hard: -1
volumes:
- data03:/usr/share/elasticsearch/data
networks:
- elastic
volumes:
data01:
driver: local
data02:
driver: local
data03:
driver: local
networks:
elastic:
driver: bridge
Run docker-compose
to bring up the cluster:
docker-compose up
1.4.2. 分词器。
1.4.3. 总结。
分词器的作用是什么?
-
创建倒排索引时对文档分词。
-
用户搜索时,对输入的内容分词。
IK 分词器有几种模式?
-
ik_smart:智能切分,粗粒度。
-
ik_max_word:最细切分,细粒度。
IK 分词器如何拓展词条?如何停用词条?
-
利用 config 目录的 IkAnalyzer.cfg.xml 文件添加拓展词典和停用词典。
-
在词典中添加拓展词条或者停用词条。
2. 索引库操作。
索引库就类似数据库表,mapping 映射就类似表的结构。
我们要向 es 中存储数据,必须先创建“库”和“表”。
2.1.mapping 映射属性。
mapping 是对索引库中文档的约束,常见的 mapping 属性包括。
- type:字段数据类型,常见的简单类型有。
- 字符串:text(可分词的文本)、keyword(精确值,例如:品牌、国家、ip 地址)。
- 数值:long、integer、short、byte、double、float。
- 布尔:boolean。
- 日期:date。
- 对象:object。
- index:是否创建索引,默认为 true。
- analyzer:使用哪种分词器。
- properties:该字段的子字段。
例如下面的 json 文档。
{
"age": 21,
"weight": 52.1,
"isMarried": false,
"info": "Java 讲师",
"email": "YifanLiGeek@gmail.com",
"score": [
99.1,
99.5,
98.9
],
"name": {
"firstName": "云",
"lastName": "赵"
}
}
对应的每个字段映射(mapping)。
- age:类型为 integer;参与搜索,因此需要 index 为 true;无需分词器。
- weight:类型为 float;参与搜索,因此需要 index 为 true;无需分词器。
- isMarried:类型为 boolean;参与搜索,因此需要 index 为 true;无需分词器。
- info:类型为字符串,需要分词,因此是 text;参与搜索,因此需要 index 为 true;分词器可以用 ik_smart。
- email:类型为字符串,但是不需要分词,因此是 keyword;不参与搜索,因此需要 index 为 false;无需分词器。
- score:虽然是数组,但是我们只看元素的类型,类型为 float;参与搜索,因此需要 index 为 true;无需分词器。
- name:类型为 object,需要定义多个子属性。
- name.firstName;类型为字符串,但是不需要分词,因此是 keyword;参与搜索,因此需要 index 为 true;无需分词器。
- name.lastName;类型为字符串,但是不需要分词,因此是 keyword;参与搜索,因此需要 index 为 true;无需分词器。
2.2. 索引库的 CRUD。
这里我们统一使用 Kibana 编写 DSL 的方式来演示。
2.2.1. 创建索引库和映射。
基本语法。
- 请求方式:PUT。
- 请求路径:/索引库名,可以自定义。
- 请求参数:mapping 映射。
格式。
PUT /索引库名称。
{
"mappings": {
"properties": {
"字段名": {
"type": "text",
"analyzer": "ik_smart"
},
"字段名2": {
"type": "keyword",
"index": "false"
},
"字段名3": {
"properties": {
"子字段": {
"type": "keyword"
}
}
},
// ... 略。
}
}
}
示例。
PUT /geek
{
"mappings": {
"properties": {
"info": {
"type": "text",
"analyzer": "ik_smart"
},
"email": {
"type": "keyword",
"index": false
},
"name": {
"properties": {
"firstName": {
"type": "keyword"
}
}
},
// ... 略。
}
}
}
{
"acknowledged" : true,
"shards_acknowledged" : true,
"index" : "geek"
}
2.2.2. 查询索引库。
基本语法。
-
请求方式:GET
-
请求路径:/索引库名。
-
请求参数:无。
格式。
GET /索引库名。
示例。
GET /geek
{
"geek" : {
"aliases" : { },
"mappings" : {
"properties" : {
"email" : {
"type" : "keyword",
"index" : false
},
"info" : {
"type" : "text",
"analyzer" : "ik_smart"
},
"name" : {
"properties" : {
"firstName" : {
"type" : "keyword"
}
}
}
}
},
"settings" : {
"index" : {
"routing" : {
"allocation" : {
"include" : {
"_tier_preference" : "data_content"
}
}
},
"number_of_shards" : "1",
"provided_name" : "geek",
"creation_date" : "1680595225438",
"number_of_replicas" : "1",
"uuid" : "Nl9fY2dmTjWw-LIRvZG5XA",
"version" : {
"created" : "7120199"
}
}
}
}
}
2.2.3. 修改索引库。
倒排索引结构虽然不复杂,但是一旦数据结构改变(比如改变了分词器),就需要重新创建倒排索引,这简直是灾难。因此索引库一旦创建,无法修改 mapping。
虽然无法修改 mapping 中已有的字段,但是却允许添加新的字段到 mapping 中,因为不会对倒排索引产生影响。
语法说明。
PUT /索引库名/_mapping
{
"properties": {
"新字段名": {
"type": "integer"
}
}
}
示例。
PUT /geek/_mapping
{
"properties": {
"age" : {
"type": "integer"
}
}
}
{
"acknowledged" : true
}
GET /geek
{
"geek" : {
"aliases" : { },
"mappings" : {
"properties" : {
"age" : {
"type" : "integer"
},
"email" : {
"type" : "keyword",
"index" : false
},
"info" : {
"type" : "text",
"analyzer" : "ik_smart"
},
"name" : {
"properties" : {
"firstName" : {
"type" : "keyword"
}
}
}
}
},
"settings" : {
"index" : {
"routing" : {
"allocation" : {
"include" : {
"_tier_preference" : "data_content"
}
}
},
"number_of_shards" : "1",
"provided_name" : "geek",
"creation_date" : "1680595225438",
"number_of_replicas" : "1",
"uuid" : "Nl9fY2dmTjWw-LIRvZG5XA",
"version" : {
"created" : "7120199"
}
}
}
}
}
PUT /geek/_mapping
{
"properties": {
"age" : {
"type": "long"
}
}
}
{
"error" : {
"root_cause" : [
{
"type" : "illegal_argument_exception",
"reason" : "mapper [age] cannot be changed from type [integer] to [long]"
}
],
"type" : "illegal_argument_exception",
"reason" : "mapper [age] cannot be changed from type [integer] to [long]"
},
"status" : 400
}
2.2.4. 删除索引库。
语法。
-
请求方式:DELETE
-
请求路径:/索引库名。
-
请求参数:无。
格式。
DELETE /索引库名
在 kibana 中测试。
2.2.5. 总结。
索引库操作有哪些?
- 创建索引库:PUT /索引库名。
- 查询索引库:GET /索引库名。
- 删除索引库:DELETE /索引库名。
- 添加字段:PUT /索引库名/_mapping
3. 文档操作。
3.1. 新增文档。
语法。
POST /索引库名/_doc/文档 id
{
"字段1": "值1",
"字段2": "值2",
"字段3": {
"子属性1": "值3",
"子属性2": "值4"
}
// ...
}
示例。
如果不写 id,会随机生成 id。
POST /geek/_doc
{
"info": "黑马程序员 Java 讲师",
"email": "YifanLiGeek@gmail.com",
"name": {
"firstName": "云",
"lastName": "赵"
}
}
响应。
{
"_index" : "geek",
"_type" : "_doc",
"_id" : "G9pQS4cBQ8HD8n5qqZ_O",
"_version" : 1,
"result" : "created",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 0,
"_primary_term" : 1
}
加 id。
POST /geek/_doc/1
{
"info": "黑马程序员 Java 讲师",
"email": "YifanLiGeek@gmail.com",
"name": {
"firstName": "云",
"lastName": "赵"
}
}
{
"_index" : "geek",
"_type" : "_doc",
"_id" : "1",
"_version" : 1,
"result" : "created",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 1,
"_primary_term" : 1
}
3.2. 查询文档。
根据 rest 风格,新增是 post,查询应该是 get,不过查询一般都需要条件,这里我们把文档 id 带上。
语法。
GET /{索引库名称}/_doc/{id}
通过 kibana 查看数据。
GET /geek/_doc/1
查看结果。
{
"_index" : "geek",
"_type" : "_doc",
"_id" : "1",
"_version" : 1,
"_seq_no" : 1,
"_primary_term" : 1,
"found" : true,
"_source" : {
"info" : "黑马程序员 Java 讲师",
"email" : "YifanLiGeek@gmail.com",
"name" : {
"firstName" : "云",
"lastName" : "赵"
}
}
}
3.3. 删除文档。
删除使用 DELETE 请求,同样,需要根据 id 进行删除。
语法。
DELETE /{索引库名}/_doc/id
示例。
# 根据 id 删除数据。
DELETE /geek/_doc/1
3.4. 修改文档。
修改有两种方式。
-
全量修改:直接覆盖原来的文档。
-
增量修改:修改文档中的部分字段。
3.4.1. 全量修改。
全量修改是覆盖原来的文档,其本质是。
-
根据指定的 id 删除文档。
-
新增一个相同 id 的文档。
注意:如果根据 id 删除时,id 不存在,第二步的新增也会执行,也就从修改变成了新增操作了。
语法。
PUT /{索引库名}/_doc/文档 id
{
"字段1": "值1",
"字段2": "值2"
// ... 略。
}
示例。
PUT /geek/_doc/1
{
"info": "黑马程序员高级 Java 讲师",
"email": "zy@itcast.cn",
"name": {
"firstName": "云",
"lastName": "赵"
}
}
{
"_index" : "geek",
"_type" : "_doc",
"_id" : "1",
"_version" : 2,
"result" : "updated",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 2,
"_primary_term" : 1
}
id 不存在时 -> created。
PUT /geek/_doc/2
{
"info": "黑马程序员高级 Java 讲师",
"email": "zy@itcast.cn",
"name": {
"firstName": "云",
"lastName": "赵"
}
}
{
"_index" : "geek",
"_type" : "_doc",
"_id" : "2",
"_version" : 1,
"result" : "created",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 3,
"_primary_term" : 1
}
3.4.2. 增量修改。
增量修改是只修改指定 id 匹配的文档中的部分字段。
语法。
POST /{索引库名}/_update/文档 id
{
"doc": {
"字段名": "新的值"
}
}
示例。
POST /geek/_update/1
{
"doc": {
"email": "lyfGeek@gmail.com"
}
}
{
"_index" : "geek",
"_type" : "_doc",
"_id" : "1",
"_version" : 3,
"result" : "updated",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 4,
"_primary_term" : 1
}
3.5. 总结。
文档操作有哪些?
- 创建文档:POST /{索引库名}/_doc/文档 id { json 文档 }
- 查询文档:GET /{索引库名}/_doc/文档 id
- 删除文档:DELETE /{索引库名}/_doc/文档 id
- 修改文档。
- 全量修改:PUT /{索引库名}/_doc/文档 id { json 文档 }
- 增量修改:POST /{索引库名}/_update/文档 id {“doc”: {字段}}
4. 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。
4.0. 导入 Demo 工程。
4.0.1. 导入数据。
首先导入数据库数据。
数据结构如下。
CREATE SCHEMA `elasticsearch_demo` DEFAULT CHARACTER SET utf8mb4 ;
CREATE TABLE `elasticsearch_demo`.`hotel` (
`id` BIGINT NOT NULL COMMENT '酒店 id。',
`name` VARCHAR(255) NULL COMMENT '名称。',
`address` VARCHAR(255) NULL COMMENT '地址。',
`price` INT NULL COMMENT '价格。',
`score` INT(2) NULL COMMENT '评分。',
`brand` VARCHAR(45) NULL COMMENT '品牌。',
`city` VARCHAR(45) NULL COMMENT '所在城市。',
`star_name` VARCHAR(45) NULL COMMENT '酒店星级。1 星 ~ 5 星,1 钻 ~ 5 钻。',
`business` VARCHAR(255) NULL COMMENT '商圈。',
`latitude` VARCHAR(45) NULL COMMENT '纬度。',
`longitude` VARCHAR(45) NULL COMMENT '经度。',
`pic` VARCHAR(255) NULL COMMENT '图片。',
PRIMARY KEY (`id`))
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8mb4
COMMENT = '酒店。';
INSERT INTO `hotel` VALUES (36934, '7天连锁酒店(上海宝山路地铁站店)', '静安交通路40号', 336, 37, '7天酒店', '上海', '二钻', '四川北路商业区', '31.251433', '121.47522', 'https://m.tuniucdn.com/fb2/t1/G1/M00/3E/40/Cii9EVkyLrKIXo1vAAHgrxo_pUcAALcKQLD688AAeDH564_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (38609, '速8酒店(上海赤峰路店)', '广灵二路126号', 249, 35, '速8', '上海', '二钻', '四川北路商业区', '31.282444', '121.479385', 'https://m.tuniucdn.com/fb2/t1/G2/M00/DF/96/Cii-TFkx0ImIQZeiAAITil0LM7cAALCYwKXHQ4AAhOi377_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (38665, '速8酒店上海中山北路兰田路店', '兰田路38号', 226, 35, '速8', '上海', '二钻', '长风公园地区', '31.244288', '121.422419', 'https://m.tuniucdn.com/fb2/t1/G2/M00/EF/86/Cii-Tlk2mV2IMZ-_AAEucgG3dx4AALaawEjiycAAS6K083_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (38812, '7天连锁酒店(上海漕溪路地铁站店)', '徐汇龙华西路315弄58号', 298, 37, '7天酒店', '上海', '二钻', '八万人体育场地区', '31.174377', '121.442875', 'https://m.tuniucdn.com/fb2/t1/G2/M00/E0/0E/Cii-TlkyIr2IEWNoAAHQYv7i5CkAALD-QP2iJwAAdB6245_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (39106, '7天连锁酒店(上海莘庄地铁站店)', '闵行莘庄镇七莘路299号', 348, 41, '7天酒店', '上海', '二钻', '莘庄工业区', '31.113812', '121.375869', 'https://m.tuniucdn.com/fb2/t1/G2/M00/D8/11/Cii-T1ku2zGIGR7uAAF1NYY9clwAAKxZAHO8HgAAXVN368_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (39141, '7天连锁酒店(上海五角场复旦同济大学店)', '杨浦国权路315号', 349, 38, '7天酒店', '上海', '二钻', '江湾、五角场商业区', '31.290057', '121.508804', 'https://m.tuniucdn.com/fb2/t1/G2/M00/C7/E3/Cii-T1knFXCIJzNYAAFB8-uFNAEAAKYkQPcw1IAAUIL012_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (45845, '上海西藏大厦万怡酒店', '虹桥路100号', 589, 45, '万怡', '上海', '四钻', '徐家汇地区', '31.192714', '121.434717', 'https://m.tuniucdn.com/fb3/s1/2n9c/48GNb9GZpJDCejVAcQHYWwYyU8T_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (45870, '上海临港豪生大酒店', '新元南路555号', 896, 45, '豪生', '上海', '四星级', '滴水湖临港地区', '30.871729', '121.81959', 'https://m.tuniucdn.com/fb3/s1/2n9c/2F5HoQvBgypoDUE46752ppnQaTqs_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (46829, '上海浦西万怡酒店', '恒丰路338号', 726, 46, '万怡', '上海', '四钻', '上海火车站地区', '31.242977', '121.455864', 'https://m.tuniucdn.com/fb3/s1/2n9c/x87VCoyaR8cTuYFZmKHe8VC6Wk1_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (47066, '上海浦东东站华美达酒店', '施新路958号', 408, 46, '华美达', '上海', '四钻', '浦东机场核心区', '31.147989', '121.759199', 'https://m.tuniucdn.com/fb3/s1/2n9c/2pNujAVaQbXACzkHp8bQMm6zqwhp_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (47478, '速8酒店(上海松江中心店)', '松江荣乐东路677号', 428, 35, '速8', '上海', '二钻', '佘山、松江大学城', '31.016712', '121.261606', 'https://m.tuniucdn.com/filebroker/cdn/res/07/36/073662e1718fccefb7130a9da44ddf5c_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (56201, '上海齐鲁万怡大酒店', '东方路838号', 873, 44, '万怡', '上海', '四星级', '浦东陆家嘴金融贸易区', '31.226031', '121.525801', 'https://m.tuniucdn.com/fb2/t1/G6/M00/52/B6/Cii-TF3eXKeIJeN7AASiKHbTtx4AAGRegDSBzMABKJA111_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (56214, '上海浦东华美达大酒店', '新金桥路18号', 830, 45, '华美达', '上海', '四星级', '浦东金桥地区', '31.244916', '121.590752', 'https://m.tuniucdn.com/fb3/s1/2n9c/3jtXiuMKZEXJAuKuAkc47yLCjUBt_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (56227, '上海圣淘沙万怡酒店', '南桥镇南桥路1号', 899, 45, '万怡', '上海', '四星级', '奉贤开发区', '30.910917', '121.456525', 'https://m.tuniucdn.com/fb2/t1/G6/M00/52/B9/Cii-U13eXSiIdJjXAARSA6FywFYAAGRnwHvy1AABFIb158_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (56392, '上海银星皇冠假日酒店', '番禺路400号', 809, 47, '皇冠假日', '上海', '五星级', '徐家汇地区', '31.202768', '121.429524', 'https://m.tuniucdn.com/fb3/s1/2n9c/37ucQ38K3UFdcRqntJ8M5dt884HR_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (56852, '上海财大豪生大酒店', '武东路188号', 592, 46, '豪生', '上海', '五钻', '江湾/五角场商业区', '31.304182', '121.492936', 'https://m.tuniucdn.com/fb3/s1/2n9c/2jGHezLZvPZqC9cBGesbP5vAhCXi_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (56912, '上海华凯华美达广场酒店', '月华路9号', 747, 40, '华美达', '上海', '四钻', '奉贤开发区', '30.814382', '121.464521', 'https://m.tuniucdn.com/fb3/s1/2n9c/45iaCNCuZavJTxwTLskhVKzwynLD_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (56977, '上海五角场华美达大酒店', '黄兴路1888号', 499, 40, '华美达', '上海', '三钻', '江湾/五角场商业区', '31.292932', '121.519759', 'https://m.tuniucdn.com/fb3/s1/2n9c/26VREqAQdaGFvJdAJALVtjxcNMpL_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (60214, '上海金茂君悦大酒店', '世纪大道88号(54楼办理入住)', 699, 46, '君悦', '上海', '五星级', '浦东陆家嘴金融贸易区', '31.235152', '121.506082', 'https://m.tuniucdn.com/fb3/s1/2n9c/7Azm3jvGUHuXe3eS1DrixAWVTXY_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (60223, '上海希尔顿酒店', '静安华山路250号', 2688, 37, '希尔顿', '上海', '五星级', '静安寺地区', '31.219306', '121.445427', 'https://m.tuniucdn.com/filebroker/cdn/res/92/10/9210e74442aceceaf6e196d61fc3b6b1_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (60359, '上海外高桥皇冠假日酒店', '杨高北路1000号', 3299, 46, '皇冠假日', '上海', '五星级', '浦东外高桥地区', '31.338944', '121.590611', 'https://m.tuniucdn.com/fb3/s1/2n9c/VcKUM9zUSiVgDhFioc6mWQoX9ES_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (60363, '上海新世界丽笙大酒店', '南京西路88号', 1341, 46, '丽笙', '上海', '五星级', '人民广场地区', '31.23462', '121.47327', 'https://m.tuniucdn.com/fb3/s1/2n9c/2j31b7X3YzGkf4Li3phS6TG1mtwm_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (60398, '上海复旦皇冠假日酒店', '邯郸路199号', 924, 47, '皇冠假日', '上海', '五星级', '江湾/五角场商业区', '31.295382', '121.502537', 'https://m.tuniucdn.com/fb3/s1/2n9c/2H1Gk8LHaBWZfYvR6NYYcGTvACmL_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (60487, '上海外滩茂悦大酒店', '黄浦路199号', 689, 44, '君悦', '上海', '五星级', '外滩地区', '31.245409', '121.492969', 'https://m.tuniucdn.com/fb3/s1/2n9c/2Swp2h1fdj9zCUKsk63BQvVgKLTo_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (60522, '上海嘉豪淮海国际豪生酒店', '汾阳路1号', 425, 45, '豪生', '上海', '四钻', '淮海路/新天地地区', '31.215497', '121.456297', 'https://m.tuniucdn.com/fb3/s1/2n9c/38UBi4QYuaF8jN94CxQ7tb7tjtmZ_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (60916, '上海绿地万怡酒店', '沪宜公路3101号', 328, 45, '万怡', '上海', '四钻', '嘉定新城', '31.368523', '121.258567', 'https://m.tuniucdn.com/fb3/s1/2n9c/3VLwG9tTQQnp3M3MTeMTdx9nas9B_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (60922, '上海虹桥祥源希尔顿酒店', '红松东路1116号', 1108, 45, '希尔顿', '上海', '五钻', '虹桥地区', '31.18746', '121.395312', 'https://m.tuniucdn.com/fb3/s1/2n9c/tQRqDTFkHnHzMZiDKjcGV81ekvc_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (60935, '上海虹口三至喜来登酒店', '四平路59号', 1899, 46, '喜来登', '上海', '五星级', '四川北路商业区', '31.2579', '121.487954', 'https://m.tuniucdn.com/fb3/s1/2n9c/3C3gxLxLjVwnkxJwJm8rd3f38kcd_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (61075, '上海雅居乐万豪酒店', '西藏中路555号', 1152, 46, '万豪', '上海', '五钻', '人民广场地区', '31.236681', '121.473529', 'https://m.tuniucdn.com/fb3/s1/2n9c/3FoT16PkXavKsssvktVvVq5Si6Cr_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (61083, '上海滴水湖皇冠假日酒店', '自由贸易试验区临港新片区南岛1号', 971, 44, '皇冠假日', '上海', '五钻', '滴水湖临港地区', '30.890867', '121.937241', 'https://m.tuniucdn.com/fb3/s1/2n9c/312e971Rnj9qFyR3pPv4bTtpj1hX_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (309208, '北京王府井希尔顿酒店', '王府井东街8号', 1679, 46, '希尔顿', '北京', '五钻', '天安门/王府井地区', '39.914539', '116.413392', 'https://m.tuniucdn.com/fb2/t1/G6/M00/52/10/Cii-TF3ePt2IX9UEAALb6VYBSmoAAGKMgGsuW8AAtwB147_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (394559, '北京东方君悦大酒店', '长安街1号东方广场', 686, 45, '君悦', '北京', '五星级', '天安门/王府井地区', '39.909635', '116.414621', 'https://m.tuniucdn.com/fb3/s1/2n9c/3mFqcNSh7eEo9yc3Rw2P5HDNTdDe_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (394617, '北京宝辰饭店', '建国门内大街甲18号', 418, 44, '豪生', '北京', '四星级', '北京站/建国门地区', '39.905768', '116.428153', 'https://m.tuniucdn.com/fb3/s1/2n9c/NEYa6EfDHuhhb19Ct85WBbkKHZU_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (394796, '北京新云南皇冠假日酒店', '东北三环西坝河太阳宫桥东北角云南大厦', 485, 46, '皇冠假日', '北京', '五星级', '国展中心地区', '39.972409', '116.434698', 'https://m.tuniucdn.com/fb3/s1/2n9c/dfP8K782eTsohQWSRdkd7St9LA2_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (395434, '北京希尔顿酒店', '东三环北路东方路1号', 350, 45, '希尔顿', '北京', '五星级', '燕莎/朝阳公园商业区', '39.952703', '116.462387', 'https://m.tuniucdn.com/fb3/s1/2n9c/3fwNbKGhk6XCrkdVyxwhC5uGpLVy_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (395702, '北京首都机场希尔顿酒店', '首都机场3号航站楼三经路1号', 222, 46, '希尔顿', '北京', '五钻', '首都机场/新国展地区', '40.048969', '116.619566', 'https://m.tuniucdn.com/fb2/t1/G6/M00/52/10/Cii-U13ePtuIMRSjAAFZ58NGQrMAAGKMgADZ1QAAVn_167_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (395787, '北京海航大厦万豪酒店', '霄云路甲26号', 1302, 46, '万豪', '北京', '五钻', '燕莎/朝阳公园商业区', '39.959861', '116.467363', 'https://m.tuniucdn.com/fb3/s1/2n9c/3zFiWi2C9SmbcQwCZgJFQC9ahvs5_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (395799, '北京国际艺苑皇冠假日酒店', '王府井大街48号', 636, 44, '皇冠假日', '北京', '五星级', '天安门/王府井地区', '39.918994', '116.411277', 'https://m.tuniucdn.com/fb2/t1/G6/M00/52/10/Cii-U13ePvyIahjPAAMykV278aEAAGKOQO9e4UAAzKp283_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (395815, '北京明豪华美达酒店', '天竺镇府前一街13号', 558, 46, '华美达', '北京', '四钻', '首都机场/新国展地区', '40.062832', '116.580678', 'https://m.tuniucdn.com/fb2/t1/G6/M00/52/13/Cii-U13eP2mIKCwvAAODTZXT-fAAAGKVAA9taIAA4Nl245_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (396189, '北京朝阳悠唐皇冠假日酒店', '三丰北里3号', 944, 46, '皇冠假日', '北京', '五钻', '三里屯/工体/东直门地区', '39.92129', '116.43847', 'https://m.tuniucdn.com/fb3/s1/2n9c/tT6ipLain1ZovR5gnQ7tJ4KKym5_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (396471, '维也纳酒店(北京花园路店)', '海淀北太平庄花园路甲17号', 381, 36, '维也纳', '北京', '三钻', '马甸、安贞地区', '39.970837', '116.365244', 'https://m.tuniucdn.com/filebroker/cdn/res/17/00/1700926908bae6ba3e5ef96de7b7d4cc_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (396506, '北京金隅喜来登酒店', '北三环东路36号', 357, 47, '喜来登', '北京', '五星级', '马甸/安贞地区', '39.967163', '116.4099', 'https://m.tuniucdn.com/fb3/s1/2n9c/29FW2WtGzzUtPhWR1LKxcFZAVa9P_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (413460, '7天连锁酒店(北京天坛店)', '东城天坛东里甲48号', 753, 38, '7天酒店', '北京', '二钻', '前门、崇文门商贸区', '39.875786', '116.421987', 'https://m.tuniucdn.com/fb2/t1/G2/M00/C7/D8/Cii-T1knCK6IWTtxAAI0plLButMAAKYTAJu-woAAjS-422_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (413476, '7天连锁酒店(北京南站店)', '丰台马家堡西路22号', 478, 37, '7天酒店', '北京', '二钻', '永定门、南站、大红门、南苑地区', '39.845363', '116.372327', 'https://m.tuniucdn.com/fb2/t1/G1/M00/26/B7/Cii-U1knCtaISM4VAAHkEQd-mrAAAKw0ALVCwEAAeQp741_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (414168, '7天连锁酒店(北京西直门店)', '西城平安里西大街翠花街育幼胡同甲20-22号', 419, 37, '7天酒店', '北京', '二钻', '西单、金融街地区', '39.931338', '116.364982', 'https://m2.tuniucdn.com/filebroker/cdn/res/bc/66/bc666859edf4fc072a8006c66758058d_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (414481, '7天连锁酒店(北京团结湖地铁站店)', '朝阳团结湖北里9号楼', 525, 36, '7天酒店', '北京', '二钻', '燕莎、三里屯商业区', '39.928457', '116.466132', 'https://m.tuniucdn.com/fb2/t1/G1/M00/38/2D/Cii9EFkv2-uIPTaBAALX6P-rbdUAALPpwHv4ykAAtgA277_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (414698, '7天连锁酒店(北京798艺术区店)', '朝阳北京市朝阳区酒仙桥北路(798艺术区北门)彩虹路6号-电通创意广场大院内', 553, 37, '7天酒店', '北京', '二钻', '望京、酒仙桥、798地区', '39.990671', '116.498452', 'https://m.tuniucdn.com/fb2/t1/G2/M00/D8/11/Cii-T1ku2zqIN7SiAAEdvT6RrjUAAKxZQKFooYAAR3V090_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (415600, '如家酒店(北京朝阳北路传媒大学褡裢坡地铁站店)', '三间房乡褡裢坡村青年沟西侧558号', 259, 47, '如家', '北京', '二钻', '传媒大学/管庄地区', '39.923212', '116.560023', 'https://m.tuniucdn.com/fb3/s1/2n9c/3NezpxNZWQMdNXibwbMkQuAZjDyJ_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (415659, '7天连锁酒店(北京紫竹桥店)', '海淀北洼路甲3号', 781, 42, '7天酒店', '北京', '二钻', '西直门及北京展览馆地区', '39.936138', '116.302405', 'https://m.tuniucdn.com/fb2/t1/G1/M00/3C/22/Cii9EVkxPMqIZJz-AAIh0esETAIAALXbgNQkH8AAiHp053_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (416121, '如家酒店(北京西客站北广场店)', '莲花池东路120-2号6层', 275, 43, '如家', '北京', '二钻', '北京西站/丽泽商务区', '39.896449', '116.317382', 'https://m.tuniucdn.com/fb3/s1/2n9c/42DTRnKbiYoiGFVzrV9ZJUxNbvRo_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (416260, '7天连锁酒店(北京通州八里桥店)', '永顺镇西马庄小区', 534, 38, '7天酒店', '北京', '二钻', '果园环岛、通州区', '39.915443', '116.631871', 'https://m.tuniucdn.com/fb2/t1/G2/M00/DF/5A/Cii-TlkxkeGIKM0oAAGOb64RvToAALBAAH9Fg8AAY6H201_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (416268, '和颐酒店(北京传媒大学财满街店)', '朝阳路高井176号', 524, 46, '和颐', '北京', '三钻', '国贸地区', '39.918277', '116.53015', 'https://m.tuniucdn.com/fb2/t1/G6/M00/52/13/Cii-TF3eP5GIJIOLAAUwsIVCxdAAAGKXgK5a0IABTDI239_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (416307, '速8酒店(北京新国展首都机场后沙峪店)', '后沙峪镇裕民大街32号', 350, 39, '速8', '北京', '二钻', '首都机场/新国展地区', '40.099019', '116.543655', 'https://m.tuniucdn.com/fb2/t1/G6/M00/52/3E/Cii-TF3eRgGIHCkKAAP_ATvriiQAAGL0AIoLtUAA_8Z513_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (432335, '7天连锁酒店(上海北外滩国际客运中心地铁站店)', '唐山路145号', 249, 35, '7天酒店', '上海', '二钻', '北外滩地区', '31.252585', '121.498753', 'https://m2.tuniucdn.com/filebroker/cdn/res/c1/ba/c1baf64418437c56617f89840c6411ef_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (433576, '如家酒店(上海南京路步行街店)', '南京东路480号保安坊内', 379, 44, '如家', '上海', '二钻', '人民广场地区', '31.236454', '121.480948', 'https://m.tuniucdn.com/fb2/t1/G6/M00/52/BA/Cii-U13eXVaIQmdaAAWxgzdXXxEAAGRrgNIOkoABbGb143_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (434082, '如家酒店·neo(上海外滩城隍庙小南门地铁站店)', '复兴东路260号', 392, 44, '如家', '上海', '二钻', '豫园地区', '31.220706', '121.498769', 'https://m.tuniucdn.com/fb2/t1/G6/M00/52/B6/Cii-U13eXLGIdHFzAAIG-5cEwDEAAGRfQNNIV0AAgcT627_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (441836, '如家酒店(北京国展三元桥店)', '西坝河东里36号', 458, 47, '如家', '北京', '二钻', '国展中心地区', '39.966238', '116.450142', 'https://m.tuniucdn.com/fb2/t1/G6/M00/52/39/Cii-TF3eRTGITp1UAAYIilRD7skAAGLngIuAnQABgii479_w200_h200_c1_t0.png');
INSERT INTO `hotel` VALUES (485775, '如家酒店(上海闵行华东师范大学吴泾店)', '吴泾镇宝秀路977号', 161, 45, '如家', '上海', '二钻', '交大/闵行经济开发区', '31.047135', '121.46224', 'https://m.tuniucdn.com/fb3/s1/2n9c/V8pz15CkiMX5xYJRmbbp5zkKWJ8_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (489756, '7天连锁酒店(北京平谷店)', '文化北街4-16号', 544, 40, '7天酒店', '北京', '二钻', '平谷城区', '40.14308', '117.111554', 'https://m2.tuniucdn.com/filebroker/cdn/res/2e/b4/2eb4edb22ddb981307d8570beb1d746d_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (517915, '如家酒店·neo(深圳草埔地铁站店)', '布吉路1036号', 159, 44, '如家', '深圳', '二钻', '田贝/水贝珠宝城', '22.583191', '114.118499', 'https://m.tuniucdn.com/fb3/s1/2n9c/228vhBCQmFRFWQBYX1cgoFQb6x58_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (527938, '深圳好日子皇冠假日酒店', '福华一路28号', 590, 46, '皇冠假日', '深圳', '五星级', '会展中心/CBD', '22.537153', '114.053529', 'https://m.tuniucdn.com/fb3/s1/2n9c/b6Ztz5jn4MngK3Hzfxuu9JGsjrm_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (541619, '如家酒店(上海莘庄地铁站龙之梦商业广场店)', '莘庄镇莘浜路172号', 149, 44, '如家', '上海', '二钻', '莘庄工业区', '31.105797', '121.37755', 'https://m.tuniucdn.com/fb3/s1/2n9c/3mKs3jETvJDj3dDdkRB9UyLLvPna_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (542068, '上海茂业华美达广场酒店', '沪南路938号', 646, 40, '华美达', '上海', '三钻', '浦东新国际博览中心', '31.182761', '121.554106', 'https://m.tuniucdn.com/fb3/s1/2n9c/2139uDFUZ2VKxrathwSeeE4DwyFU_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (546869, '深圳彭年万丽酒店', '嘉宾路2002号(毗邻金光华购物广场)', 701, 46, '万丽', '深圳', '五钻', '罗湖口岸/火车站', '22.540989', '114.122665', 'https://m.tuniucdn.com/fb3/s1/2n9c/gwAbqEXFUpjUBmnxUfK89p3zBBT_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (581859, '维也纳酒店(北京丰益桥店)(原申晨酒店)', '丰台丰管路8号', 648, 45, '维也纳', '北京', '三钻', '北京西站、丽泽商务区', '39.857707', '116.312482', 'https://m2.tuniucdn.com/filebroker/cdn/res/97/43/97438481b9e79abad429e5c30d7f303f_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (584697, '7天连锁酒店(深圳华强赛格广场店)', '华强南路3024号赛格苑1栋', 362, 36, '7天酒店', '深圳', '二钻', '华强北商业区', '22.539831', '114.087899', 'https://m.tuniucdn.com/fb2/t1/G2/M00/D8/11/Cii-T1ku2zmIcP4sAAEw8iuLXFgAAKxZQH7HVYAATEK972_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (593228, '7天连锁酒店(北京颐和园店)', '海淀厢红旗路功德寺桥北侧', 730, 38, '7天酒店', '北京', '二钻', '香山、八大处风景区', '40.003959', '116.256718', 'https://m2.tuniucdn.com/filebroker/cdn/res/55/84/55841f502c5a711e66dd5454b64f559b_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (598591, '上海丽昂豪生大酒店', '金新路99号', 529, 47, '豪生', '上海', '四钻', '浦东金桥地区', '31.252496', '121.600085', 'https://m.tuniucdn.com/fb3/s1/2n9c/2KfPPyPx9rWyVXif2CUuxv61Nryc_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (607915, '汉庭酒店(深圳皇岗店)', '滨河大道6033号海滨广场国皇大厦3楼', 313, 42, '汉庭', '深圳', '二钻', '皇岗口岸/福田口岸', '22.528101', '114.064221', 'https://m.tuniucdn.com/fb3/s1/2n9c/qMyCJVYuW21nsCeEBt8CMfmEhra_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (608374, '如家酒店(上海浦东机场龙东大道合庆店)', '东川公路5863号', 160, 45, '如家', '上海', '二钻', '浦东机场核心区', '31.237662', '121.718556', 'https://m.tuniucdn.com/fb3/s1/2n9c/LUYxGGV4pzjKeN5a69K4deU8JD8_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (609023, '如家酒店·neo(上海外高桥保税区北地铁站店)', '花山路1209号', 266, 45, '如家', '上海', '二钻', '浦东外高桥地区', '31.351148', '121.585606', 'https://m.tuniucdn.com/fb3/s1/2n9c/3cJ6KTfms9cfEnME8WRkQQBXBkYm_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (609372, '豪派特华美达广场酒店(深圳北站店)', '民治街道梅龙路与民旺路交汇处', 498, 45, '华美达', '深圳', '四钻', '深圳北站地区', '22.620501', '114.033874', 'https://m.tuniucdn.com/fb3/s1/2n9c/3G5TnUCPbjGYHAVWfvuixw8bs69t_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (624417, '深圳君悦酒店', '宝安南路1881号', 442, 47, '君悦', '深圳', '五钻', '万象城/京基100', '22.537247', '114.111182', 'https://m.tuniucdn.com/fb2/t1/G6/M00/45/EA/Cii-TF3ZpVmIVDJ9AAXvJftz_AgAAFrrQKbI4oABe89086_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (628327, '如家酒店·neo(深圳罗湖口岸国贸地铁站店)', '人民南路2011号', 223, 45, '如家', '深圳', '二钻', '罗湖口岸/火车站', '22.536734', '114.118336', 'https://m.tuniucdn.com/fb3/s1/2n9c/2rKHmQWHYiY8GZA3xBHpFKCLZwZo_w200_h200_c1_t0.png');
INSERT INTO `hotel` VALUES (629023, '和颐酒店(北京十里河欢乐谷店)', '十八里店乡周家庄288号', 390, 47, '和颐', '北京', '四钻', '劲松/潘家园地区', '39.853354', '116.483437', 'https://m.tuniucdn.com/fb3/s1/2n9c/28hnDdqn5uzuzCKYkw2x4pYmunXM_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (629729, '7天连锁酒店(上海张江高科园区店)', '浦东新区蔡伦路103号', 267, 36, '7天酒店', '上海', '二钻', '浦东张江地区', '31.196154', '121.62071', 'https://m2.tuniucdn.com/filebroker/cdn/res/d9/61/d961508a10865b9b29c033064f31b913_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (635963, '深圳龙岗珠江皇冠假日酒店', '龙岗中心城龙翔大道9009号珠江广场', 737, 46, '皇冠假日', '深圳', '五星级', '龙岗中心区/大运新城', '22.722941', '114.250002', 'https://m.tuniucdn.com/fb2/t1/G6/M00/45/EA/Cii-U13ZpWGIasKjAAY1SNE36KMAAFrrwMNoAwABjVg973_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (636080, '深圳大中华喜来登酒店', '福华路一号大中华国际交易广场', 1556, 47, '喜来登', '深圳', '五星级', '会展中心/CBD', '22.535567', '114.062005', 'https://m.tuniucdn.com/fb3/s1/2n9c/3hQRTmAUW9PegTjxMiEfYwh2HnKp_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (644417, '上海外高桥喜来登酒店', '自由贸易试验区基隆路28号(二号门内)', 2419, 46, '喜来登', '上海', '五钻', '浦东外高桥地区', '31.350989', '121.588751', 'https://m.tuniucdn.com/fb3/s1/2n9c/1Rrtg9n7PdMEivVDhsehbJBrEre_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (648219, '上海金桥红枫万豪酒店', '新金桥路15号', 891, 47, '万豪', '上海', '五钻', '浦东金桥地区', '31.244061', '121.591153', 'https://m.tuniucdn.com/fb2/t1/G6/M00/52/B6/Cii-TF3eXKuIR_a0AAUx-Xd2JLQAAGRfACSpvUABTIR560_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (657192, '深圳宝安中天美景华美达酒店', '新桥街道万丰社区中心路7-1号', 498, 45, '华美达', '深圳', '四钻', '深圳国际会展中心商圈', '22.716473', '113.826391', 'https://m.tuniucdn.com/fb2/t1/G6/M00/45/EA/Cii-U13ZpVeIRbhTAAOzGZSDtlcAAFrrQEWM-AAA7Mx626_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (659496, '维也纳酒店(深圳国王店)', '龙华新区龙华龙观西路95号', 714, 37, '维也纳', '深圳', '三钻', '深圳北站地区', '22.65892', '114.006817', 'https://m2.tuniucdn.com/filebroker/cdn/res/b4/76/b476cacc575a7ff237128ba2fd63923a_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (670673, '北京龙城华美达酒店', '昌平路319号', 506, 45, '华美达', '北京', '四钻', '回龙观/天通苑地区', '40.084219', '116.304313', 'https://m.tuniucdn.com/fb3/s1/2n9c/T3WruZV3S4MfcxdD1HFVhZjaBLW_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (672207, '和颐至尊酒店(北京望京798店)', '酒仙桥北路9号荧屏里4号楼', 579, 44, '和颐', '北京', '四钻', '望京/酒仙桥/798地区', '39.98835', '116.491217', 'https://m.tuniucdn.com/fb3/s1/2n9c/2y56zwK8kd2tBuRUyF7XeJ2ucvWM_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (684720, '和颐酒店(深圳罗湖口岸火车地铁站店)', '沿河南路1064号', 208, 47, '和颐', '深圳', '四钻', '罗湖口岸/火车站', '22.533753', '114.122491', 'https://m.tuniucdn.com/fb3/s1/2n9c/2LFgB2iFawKKoGADwzhW6jpCSaJT_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (696948, '上海新园华美达广场酒店', '漕宝路509号', 1290, 45, '华美达', '上海', '四星级', '光大会展中心/漕河泾地区', '31.163802', '121.405618', 'https://m.tuniucdn.com/fb2/t1/G6/M00/52/B6/Cii-TF3eXHuIE57sAAZx8iP6rMIAAGRbgAH09gABnIK621_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (706343, '速8酒店(北京西客站北广场店)', '丰台莲花池东路126号', 268, 39, '速8', '北京', '二钻', '北京西站、丽泽商务区', '39.896623', '116.315586', 'https://m.tuniucdn.com/fb2/t1/G2/M00/E3/46/Cii-TlkzMXWIL0sAAAGG8a3YwiwAALJlgG-r5YAAYcJ067_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (707357, '维也纳酒店(上海南站光大店)', '徐汇桂林路46号(钦州南路路口往南100米)', 3288, 36, '维也纳', '上海', '三钻', '光大会展中心', '31.156297', '121.419948', 'https://m2.tuniucdn.com/filebroker/cdn/res/f7/13/f713f8f98d777d8d53aafefb37a79ef6_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (711837, '速8酒店(北京立水桥店)', '朝阳安立路3号1幢3层', 268, 36, '速8', '北京', '二钻', '亚运村、奥体中心地区', '40.043717', '116.410962', 'https://m2.tuniucdn.com/filebroker/cdn/res/b3/87/b3876eaf16af62521cf6fb474504b8ca_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (727679, '7天连锁酒店(深圳步行街老街地铁站二店)', '罗湖区东门中路2216号华佳广场12-14楼(东门天桥东头)', 742, 40, '7天酒店', '深圳', '二钻', '东门商业区', '22.54585', '114.122227', 'https://m.tuniucdn.com/fb2/t1/G1/M00/39/99/Cii9EFkwVMKIP_mCAAI3fOHlS1wAALSDAMeO2MAAjeU309_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (728180, '如家酒店(深圳宝安西乡地铁站店)', '西乡大道298-7号(富通城二期公交站旁)', 184, 43, '如家', '深圳', '二钻', '宝安体育中心商圈', '22.569693', '113.860186', 'https://m.tuniucdn.com/fb3/s1/2n9c/FHdugqgUgYLPMoC4u4rdTbAPrVF_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (728415, '如家酒店·neo(深圳东门步行街晒布地铁站店)', '晒布路67号', 152, 46, '如家', '深圳', '二钻', '东门商业区', '22.550183', '114.120771', 'https://m.tuniucdn.com/fb2/t1/G6/M00/25/57/Cii-U13PFNWISSnQAAEpTtoilsQAAEVWgEvur8AASlm647_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (728461, '汉庭酒店(深圳会展中心店)', '新洲路世纪工艺品广场307栋', 258, 44, '汉庭', '深圳', '二钻', '皇岗口岸/福田口岸', '22.518026', '114.046061', 'https://m.tuniucdn.com/fb2/t1/G6/M00/25/56/Cii-TF3PFKOIPl0JAANm4ge6DdMAAEVTQK2SP8AA2b6365_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (728604, '如家酒店·neo(深圳南山地铁站南山市场店)', '南新路顺富街18号化州大厦', 198, 43, '如家', '深圳', '二钻', '科技园', '22.525561', '113.920058', 'https://m.tuniucdn.com/fb2/t1/G6/M00/25/57/Cii-TF3PFLmIDGWiAAPHkaNTuOIAAEVVQBGazAAA8ep611_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (730454, '7天连锁酒店(深圳万象城店)', '罗湖区嘉宾路4025号城市天地广场内', 552, 43, '7天酒店', '深圳', '二钻', '罗湖口岸/火车站', '22.537078', '114.113733', 'https://m.tuniucdn.com/fb2/t1/G2/M00/C8/4C/Cii-Tlknhz2IFnYNAAInF2jEK14AAKbcQNB5M8AAicv660_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (730569, '7天连锁酒店(深圳梅林卓越城店)', '孖岭地铁口C出口左手边,青年学院对面', 269, 39, '7天酒店', '深圳', '二钻', '莲花山/梅林', '22.568701', '114.068464', 'https://m.tuniucdn.com/fb2/t1/G2/M00/D8/0B/Cii-Tlku2dOISeGXAAAUw3MvPrIAAKxYALB2VAAABTb555_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (730968, '7天连锁酒店(深圳宝安地铁站店)', '宝安区宝安25区创业二路步行街金麒麟服装广场4栋', 314, 36, '7天酒店', '深圳', '二钻', '宝安中心区/前海', '22.568162', '113.900968', 'https://m.tuniucdn.com/fb2/t1/G2/M00/D8/0B/Cii-T1ku2cqIWTOOAAAS_bvRI5UAAKxXwILCFYAABMV285_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (751035, '7天连锁酒店(上海自贸区北门地铁站店)(原外高桥地铁北站店)', '花山路706号', 328, 39, '7天酒店', '上海', '二钻', '浦东外高桥地区', '31.348029', '121.576896', 'https://m.tuniucdn.com/fb2/t1/G1/M00/3A/21/Cii-U1kwxUCINXaHAAGmh7z6qRAAALUdwMKGREAAaaf928_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (844350, '7天连锁酒店(深圳世界之窗店)', '南山区白石洲沙河街2号金三角大厦', 769, 36, '7天酒店', '深圳', '二钻', '华侨城', '22.540501', '113.968858', 'https://m.tuniucdn.com/fb2/t1/G2/M00/D8/11/Cii-TFku2zmIOdjCAAAQJpLFhEEAAKxZQIEvQ0AABA-920_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (2316304, '如家酒店(深圳双龙地铁站店)', '龙岗街道龙岗墟社区龙平东路62号', 135, 45, '如家', '深圳', '二钻', '龙岗中心区/大运新城', '22.730828', '114.278337', 'https://m.tuniucdn.com/fb3/s1/2n9c/4AzEoQ44awd1D2g95a6XDtJf3dkw_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (2329005, '维也纳酒店(深圳华之沙店)', '福强路新洲九街28号', 651, 39, '维也纳', '深圳', '三钻', '皇岗口岸/福田口岸', '22.524835', '114.048214', 'https://m2.tuniucdn.com/filebroker/cdn/res/88/f0/88f05cd11990ef39ae187886c76f40a5_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (2351601, '深圳蛇口希尔顿南海酒店', '望海路1177号', 509, 47, '希尔顿', '深圳', '五钻', '深圳湾口岸/蛇口', '22.479373', '113.916013', 'https://m.tuniucdn.com/fb2/t1/G6/M00/45/EA/Cii-TF3ZpXOIfa6fAAJjiUOiuYgAAFrtgDtgpQAAmOh799_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (2359697, '如家酒店(北京上地安宁庄东路店)', '清河小营安宁庄东路18号20号楼', 420, 46, '如家', '北京', '二钻', '上地产业园/西三旗', '40.041322', '116.333316', 'https://m.tuniucdn.com/fb3/s1/2n9c/2wj2f8mo9WZQCmzm51cwkZ9zvyp8_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (5865979, '北京丽都皇冠假日酒店', '将台路6号', 1168, 47, '皇冠假日', '北京', '五钻', '望京/酒仙桥/798地区', '39.978133', '116.478642', 'https://m.tuniucdn.com/fb3/s1/2n9c/Yo4xL3RUsYUnDDc5QcQWj7sCrUX_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (5870456, '上海宝华万豪酒店', '广中西路333号', 922, 47, '万豪', '上海', '五钻', '大宁国际商业区', '31.279371', '121.446327', 'https://m.tuniucdn.com/fb2/t1/G6/M00/52/BA/Cii-U13eXVqIZXDFAAUC_xbrQDAAAGRrwPRyOcABQMX057_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (5871652, '上海圣诺亚皇冠假日酒店', '金沙江路1699号', 770, 46, '皇冠假日', '上海', '五钻', '长风公园地区', '31.232346', '121.377709', 'https://m.tuniucdn.com/fb3/s1/2n9c/J4sP7qRSHa9rFYnKTW75ZPB393M_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (5872067, '崇明金茂凯悦酒店', '陈家镇揽海路799弄', 1024, 46, '凯悦', '上海', '五钻', '崇明岛/长兴岛/横沙岛', '31.466563', '121.799671', 'https://m.tuniucdn.com/fb3/s1/2n9c/fsKrbnNsmSsYnNLmhh3ZvVjZ5cA_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (5873072, '速8酒店(上海火车站北广场店)', '闸北芷江西路796号', 190, 41, '速8', '上海', '二钻', '上海火车站地区', '31.255579', '121.452903', 'https://m2.tuniucdn.com/filebroker/cdn/res/96/6d/966d6596e6cb7b48c9cc1d7da79b57c8_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (7094829, '汉庭酒店(深圳华强南店)', '松岭路9号(南园小学对面)', 215, 40, '汉庭', '深圳', '二钻', '华强北商业区', '22.536842', '114.094316', 'https://m.tuniucdn.com/fb3/s1/2n9c/3WDhaZZ9yALHw8yNiU6HJyrdC3u5_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (189429168, '7天连锁酒店(北京大兴黄村清源路地铁站店)', '清源西路55号', 392, 38, '7天酒店', '北京', '二钻', '大兴农业生态观光区', '39.743751', '116.321676', 'https://m.tuniucdn.com/fb2/t1/G1/M00/4F/25/Cii9EFk3LmOIFtnDAAHm5kdIiM8AAL1FQM8kG0AAeb-418_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (197487869, '如家酒店(北京第二外国语大学南门双桥地铁站店)', '三间房乡新房村1号', 321, 47, '如家', '北京', '二钻', '传媒大学/管庄地区', '39.90635', '116.565528', 'https://m.tuniucdn.com/fb3/s1/2n9c/ZkgDAs8tTMvgFHdVPpikNqENEn1_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (197488318, '速8酒店(北京立水桥地铁南站店)', '朝阳北苑路18号院3号楼4层', 344, 36, '速8', '北京', '二钻', '亚运村、奥体中心地区', '40.043689', '116.414138', 'https://m.tuniucdn.com/fb2/t1/G1/M00/36/4D/Cii9EVkvP72IYYjgAAF7yZeWV-wAALMQACOARMAAXvh983_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (197492277, '速8酒店(北京平谷兴谷环岛店)', '平谷平谷大街31号', 614, 39, '速8', '北京', '二钻', '平谷城区', '40.159255', '117.12401', 'https://m.tuniucdn.com/fb2/t1/G1/M00/38/D5/Cii9EFkwFCiII79zAAHKsXy_LAoAALQuQEmEZ4AAcrJ339_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (197492479, '如家酒店(北京顺义中心地铁站店)', '光明南大街14号', 306, 45, '如家', '北京', '二钻', '顺义温泉休闲区', '40.124783', '116.65751', 'https://m.tuniucdn.com/fb3/s1/2n9c/2hNBSjmMTk6JQ2o8ixr5s3ioevhB_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (197496182, '和颐酒店(北京团结湖地铁站店)', '团结湖路9号楼', 341, 44, '和颐', '北京', '三钻', '燕莎/朝阳公园商业区', '39.930731', '116.466602', 'https://m.tuniucdn.com/fb3/s1/2n9c/2gK41VpMb4AwyNkwQEkfFo83uTUU_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (197496980, '速8酒店(北京温都水城王府店)', '昌平北七家镇平西府村(温都水城东200米)', 585, 39, '速8', '北京', '二钻', '小汤山温泉度假区', '40.10144', '116.380641', 'https://m.tuniucdn.com/fb2/t1/G2/M00/C7/CB/Cii-T1km_5eICnpJAAHOWN1GylMAAKYJwF0Hp8AAc5w000_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (197516492, '速8酒店(北京南苑东高地店)', '丰台南大红门路东营房15号', 651, 39, '速8', '北京', '二钻', '永定门、南站、大红门、南苑地区', '39.78996', '116.42081', 'https://m.tuniucdn.com/fb2/t1/G1/M00/3B/D8/Cii-U1kxKGWIQlaxAAIdkjkSALkAALXDQMFbTsAAh2q158_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (197835483, '7天连锁酒店(深圳大学桃园店)', '南山区桃园西路160号', 431, 36, '7天酒店', '深圳', '二钻', '科技园', '22.532576', '113.916362', 'https://m.tuniucdn.com/fb2/t1/G1/M00/38/40/Cii9EFkv4XKIQN85AAFUcDrkXe0AALPvwPRn08AAVSI037_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (197837109, '如家酒店·neo(深圳龙岗大道布吉地铁站店)', '布吉镇深惠路龙珠商城', 127, 43, '如家', '深圳', '二钻', '布吉/深圳东站', '22.602482', '114.123284', 'https://m.tuniucdn.com/fb2/t1/G6/M00/25/58/Cii-TF3PFZOIA7jwAAKInGFN4xgAAEVbAGeP4AAAoi0485_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (198323591, '汉庭酒店(深圳北站龙华汽车站店)', '龙华新区建辉路2号', 209, 46, '汉庭', '深圳', '二钻', '深圳北站地区', '22.671313', '114.02784', 'https://m.tuniucdn.com/fb3/s1/2n9c/2dkB2HzbaBUJ7adZZfZaeS9JCvjP_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (200208940, '上海浦东喜来登由由公寓', '浦建路38号', 3168, 45, '喜来登', '上海', '五钻', '浦东新国际博览中心', '31.208553', '121.518552', 'https://m.tuniucdn.com/fb3/s1/2n9c/m3Nrm37Yx6YV4NwqRvSYnFRNSGk_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (200210163, '7天连锁酒店(上海徐家汇宜山路地铁站店)', '徐汇钦州北路78号', 219, 35, '7天酒店', '上海', '二钻', '光大会展中心', '31.180615', '121.422916', 'https://m.tuniucdn.com/fb2/t1/G2/M00/DF/96/Cii-Tlkx0TOIGtOzAAEe_xcDxeIAALCZQJyxf4AAR8X941_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (200214437, '上海浦东机场华美达广场酒店', '浦东机场启航路1100号', 600, 45, '华美达', '上海', '四星级', '浦东机场核心区', '31.160969', '121.799086', 'https://m.tuniucdn.com/fb3/s1/2n9c/2D2gbXDgrMx76uWfwzmoWpmSCCXx_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (200214538, '上海绿地万豪酒店', '江滨路99号(打浦路底)', 720, 43, '万豪', '上海', '五星级', '打浦桥地区', '31.192103', '121.47298', 'https://m.tuniucdn.com/fb3/s1/2n9c/268jVMuWdYok5ehGFhQ2QNhBhUhs_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (200214715, '上海浦东喜来登由由大酒店', '浦建路38号', 2489, 45, '喜来登', '上海', '五星级', '浦东新国际博览中心', '31.208739', '121.518305', 'https://m.tuniucdn.com/fb3/s1/2n9c/36t2KUGs4h5YgYSaLSkr5pMXLM54_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (200214824, '7天连锁酒店(上海陆家嘴八佰伴店)', '崂山路689号', 249, 36, '7天酒店', '上海', '二钻', '浦东陆家嘴金融贸易区', '31.220656', '121.525127', 'https://m.tuniucdn.com/fb2/t1/G1/M00/3D/95/Cii9EVkx0JCIRDFfAAIMnl8npiYAALaawHMG2kAAgy2536_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (200215226, '上海颖奕皇冠假日酒店', '博园路6555号', 907, 45, '皇冠假日', '上海', '五钻', '嘉定新城', '31.272533', '121.19179', 'https://m.tuniucdn.com/fb3/s1/2n9c/3Uyfi2aBRETE1K5PChiLVZCwtDLF_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (200215286, '上海万豪虹桥大酒店', '虹桥路2270号', 910, 46, '万豪', '上海', '五星级', '虹桥地区', '31.191529', '121.375577', 'https://m.tuniucdn.com/fb2/t1/G6/M00/52/B6/Cii-TF3eXK6IBQoRAAbgs1dyxJwAAGRfAPXbPQABuDL314_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (200215365, '如家酒店(上海虹桥漕河泾古北店)', '虹梅路2971号', 189, 44, '如家', '上海', '二钻', '虹桥地区', '31.180968', '121.392415', 'https://m.tuniucdn.com/fb3/s1/2n9c/2WPfVp6auQkYoHzAdSbxwHAtQFfa_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (200215548, '速8酒店(上海虹桥枢纽九亭中心路店)', '松江九亭镇中心路128号', 198, 39, '速8', '上海', '二钻', '七宝古镇', '31.119363', '121.322768', 'https://m.tuniucdn.com/fb2/t1/G1/M00/42/40/Cii-U1kziVOIGTw-AAGRMfcIwJwAALi6ACRnUsAAZFJ536_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (200216665, '维也纳酒店(上海奉贤南桥中心店)', '奉贤沪杭公路1758-8号', 1289, 38, '维也纳', '上海', '三钻', '奉贤开发区', '30.934646', '121.451449', 'https://m.tuniucdn.com/fb2/t1/G2/M00/DC/A8/Cii-T1kw5leIQAA3AAFzNYtL4loAAK9OQOyk4sAAXNN152_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (219484323, '7天连锁酒店(深圳观澜章阁店)', '宝安区章阁村桂月路章阁市场汇龙峰景一期A栋2-4层', 725, 40, '7天酒店', '深圳', '二钻', '观澜', '22.746493', '114.023', 'https://m.tuniucdn.com/fb2/t1/G1/M00/3C/8C/Cii-U1kxXqaIeqeuAAHwF4GDmOcAALYFQI9zMMAAfAv100_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (223318960, '和颐酒店(深圳深南大道华强路地铁站店)', '深南中路2081号', 637, 46, '和颐', '深圳', '四钻', '华强北商业区', '22.540313', '114.088611', 'https://m.tuniucdn.com/fb3/s1/2n9c/2M7am7D8rPTeTQAhxqBeMSANaqGr_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (233036941, '7天连锁酒店(上海东林寺店)', '朱泾镇文商路79号', 218, 37, '7天酒店', '上海', '二钻', '金山枫泾古镇地区', '30.895912', '121.160238', 'https://m.tuniucdn.com/fb2/t1/G4/M00/35/13/Cii_J1zr5PyIY3acAAFCnHJPxLUAAGX-ABvcIMAAUK0087_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (234719711, '如家酒店·neo(北京朝阳北路十里堡地铁站店)', '朝阳北路八里庄南里26号', 378, 47, '如家', '北京', '二钻', '国贸地区', '39.922472', '116.501118', 'https://m.tuniucdn.com/fb3/s1/2n9c/2rHdXNCmycnUxw99AniFC25ZDSfJ_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (234719728, '速8酒店(北京房山城关店)', '房山城关镇城隍庙街10号(原房山老公安局)', 392, 47, '速8', '北京', '二钻', '', '39.705216', '115.981904', 'https://m.tuniucdn.com/fb2/t1/G1/M00/3F/66/Cii9EFkyeImIB3ZVAAHcTtTFt4oAALdsgICDO0AAdxm378_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (235196477, '和颐酒店(北京总部基地店)', '丰葆路106号', 379, 45, '和颐', '北京', '四钻', '总部基地/丰台体育中心/南宫地区', '39.815383', '116.291012', 'https://m.tuniucdn.com/fb3/s1/2n9c/3J7Hcvwt5xZJL3NkS4wPJ6csmFb9_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (339777429, '上海嘉定喜来登酒店', '菊园新区嘉唐公路66号', 1286, 44, '喜来登', '上海', '五钻', '嘉定新城', '31.394595', '121.245773', 'https://m.tuniucdn.com/fb3/s1/2n9c/2v2fKuo5bzhunSBC1n1E42cLTkZV_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (339952837, '如家酒店(北京良乡西路店)', '良乡西路7号', 159, 46, '如家', '北京', '二钻', '房山风景区', '39.73167', '116.132482', 'https://m.tuniucdn.com/fb3/s1/2n9c/3Dpgf5RTTzrxpeN5y3RLnRVtxMEA_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (343341695, '和颐酒店(北京中关村软件园店)', '东北旺西路8号中关村软件园一期9号楼', 245, 47, '和颐', '北京', '四钻', '上地产业园/西三旗', '40.044663', '116.29607', 'https://m.tuniucdn.com/fb3/s1/2n9c/3hSkPeWRQ3VK1heRQpHzJNMTanQz_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (365011120, '和颐酒店(北京石景山万达广场店)', '鲁谷东街甲26号', 505, 47, '和颐', '北京', '四钻', '公主坟/五棵松/石景山游乐园地区', '39.895479', '116.240386', 'https://m.tuniucdn.com/fb3/s1/2n9c/3iwohdQzyZP9azUkYAwTFj7WzBwd_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (368343863, '如家酒店(上海金桥博兴路地铁站店)', '博兴路1119号', 218, 45, '如家', '上海', '二钻', '浦东金桥地区', '31.266272', '121.593829', 'https://m.tuniucdn.com/fb3/s1/2n9c/w5ERtGJEmdgdgy5qtLPatR1xfm4_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (368701368, '深圳大中华希尔顿酒店', '福田深南大道1003号', 1666, 46, '希尔顿', '深圳', '五钻', '会展中心/CBD', '22.539313', '114.069763', 'https://m.tuniucdn.com/fb3/s1/2n9c/4EnHseZ73LXdFJY7DSdJ8xqAcjXe_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (1393017952, '汉庭酒店(深圳宝安松岗地铁站店)', '松岗镇河滨北路12号盛华大厦', 166, 47, '汉庭', '深圳', '二钻', '松岗商业中心区', '22.768912', '113.83325', 'https://m.tuniucdn.com/fb3/s1/2n9c/4NehRjdHyZDKxTjAxTYv27FHq8LJ_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (1400304687, '如家酒店(深圳横岗地铁站新马商贸城店)', '龙岗大道横岗段4004号', 149, 43, '如家', '深圳', '二钻', '龙岗中心区/大运新城', '22.642629', '114.202799', 'https://m.tuniucdn.com/fb2/t1/G6/M00/25/5A/Cii-TF3PFkiIb27dAAEqdDcKl3YAAEViQGVWY0AASqM960_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (1406627919, '深圳中洲万豪酒店', '海德一道88号中洲控股中心A座', 204, 47, '万豪', '深圳', '五钻', '海岸城/后海', '22.517293', '113.933785', 'https://m.tuniucdn.com/fb3/s1/2n9c/3wsinQAcuWtCdmv1yxauVG2PSYpC_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (1418471719, '上海宝龙丽笙酒店', '金海路2449弄2号', 860, 46, '丽笙', '上海', '五钻', '浦东金桥地区', '31.26571', '121.650132', 'https://m.tuniucdn.com/fb3/s1/2n9c/3myGUurFCriEVMGPy9yYMPFdb9Zh_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (1455383931, '如家酒店(深圳宝安客运中心站店)', '西乡河西金雅新苑34栋', 169, 45, '如家', '深圳', '二钻', '宝安商业区', '22.590272', '113.881933', 'https://m.tuniucdn.com/fb3/s1/2n9c/2w9cbbpzjjsyd2wRhFrnUpBMT8b4_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (1457521002, '7天连锁酒店(深圳西丽茶光地铁站店)', '珠光北路280号天下湘军1楼', 517, 39, '7天酒店', '深圳', '二钻', '大学城/西丽动物园', '22.576187', '113.956166', 'https://m.tuniucdn.com/fb2/t1/G2/M00/E3/E0/Cii-Tlkzdl6IfQYfAAHCgNVDe1sAALK6gPBDhQAAcKY242_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (1463484295, '上海和平豪生酒店', '沪南公路2653-2号', 650, 41, '豪生', '上海', '四钻', '周浦康桥地区', '31.146478', '121.568218', 'https://m.tuniucdn.com/fb3/s1/2n9c/ZxM9gWHqj657ndRsHw4j4p3CQ5k_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (1514269829, '如家酒店(上海东川路碧江商业广场店)', '东川路2645号', 218, 45, '如家', '上海', '二钻', '交大/闵行经济开发区', '31.008875', '121.402813', 'https://m.tuniucdn.com/fb3/s1/2n9c/R92UunuCRXiG826G9Ptu7orqs7b_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (1557882030, '维也纳酒店(深圳东门老街店)', '罗湖区东门新园路37号益德楼', 336, 43, '维也纳', '深圳', '三钻', '东门商业区', '22.549413', '114.118866', 'https://m.tuniucdn.com/fb2/t1/G1/M00/45/83/Cii9EFk0opCIPl9CAAKHl3Egm6oAALoKwLNWlwAAoev470_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (1557997004, '上海五角场凯悦酒店', '国定东路88号', 1104, 46, '凯悦', '上海', '五钻', '江湾/五角场商业区', '31.300645', '121.51918', 'https://m.tuniucdn.com/fb3/s1/2n9c/3a3Zz9cDgbJEEJ1GcXzKhTh21YqK_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (1565094427, '上海国际旅游度假区万怡酒店', '秀浦路3999弄17号', 713, 45, '万怡', '上海', '四钻', '迪士尼度假区', '31.132913', '121.63464', 'https://m.tuniucdn.com/fb3/s1/2n9c/KPBUPunPDETYWg8WaJDSmiZC65z_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (1584362548, '如家酒店(上海浦东国际旅游度假区御桥地铁站店)', '御青路315-317号', 339, 44, '如家', '上海', '二钻', '周浦康桥地区', '31.15719', '121.572392', 'https://m.tuniucdn.com/fb3/s1/2n9c/2ybd3wqdoBtBeKcPxmyso9y1hNXa_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (1630005459, '7天连锁酒店(深圳地王大厦红桂路店)(原红桂路店)', '罗湖区宝安南路2078号深港豪苑(与红桂路交汇处)', 143, 39, '7天酒店', '深圳', '二钻', '', '22.550341', '114.10965', 'https://m.tuniucdn.com/fb2/t1/G2/M00/EA/18/Cii-T1k1KaGIIkQVAAD4fD_T3FcAALTtABiCJ8AAPiU164_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (1637944903, '速8酒店北京后海店', '西城北京市西城区德胜门内大街兴华胡同五福里2号', 213, 39, '速8', '北京', '二钻', '后海', '39.934452', '116.38184', 'https://m.tuniucdn.com/fb2/t1/G1/M00/48/0C/Cii9EVk1JNuILdBWAAHv5O89TjMAALrFgJ8bwcAAe_8197_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (1649956165, '上海南青华美达酒店', '华夏东路811号', 299, 47, '华美达', '上海', '四钻', '迪士尼度假区', '31.195206', '121.664791', 'https://m.tuniucdn.com/fb3/s1/2n9c/2RHmQgTpte3UVSDJ5KbqobbZGRnE_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (1714520967, '速8酒店(北京安华桥黄寺大街店)', '黄寺大街12号院16号楼', 559, 43, '速8', '北京', '二钻', '马甸、安贞地区', '39.962742', '116.388431', 'https://m.tuniucdn.com/fb2/t1/G1/M00/4A/21/Cii-U1k1o-uIdcUZAAIbmIKVlKAAALtvQGBb6kAAhuw170_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (1725781423, '上海三迪华美达酒店', '广富林路600弄7号', 690, 43, '华美达', '上海', '四钻', '佘山/松江大学城', '31.058023', '121.246536', 'https://m.tuniucdn.com/fb3/s1/2n9c/NoHym6tuKwVazxy33wRNTNuQWd2_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (1734220581, '汉庭酒店(深圳大鹏佳兆业店)', '大鹏街道新园街12号', 378, 43, '汉庭', '深圳', '二钻', '较场尾/大鹏所城', '22.592661', '114.475167', 'https://m.tuniucdn.com/fb3/s1/2n9c/3nWzyWt63gtwPzRf5xbHvwKM27vU_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (1754929048, '上海环球港凯悦酒店', '宁夏路718号', 1336, 45, '凯悦', '上海', '五钻', '中山公园商业区', '31.232041', '121.412492', 'https://m.tuniucdn.com/fb2/t1/G6/M00/45/B6/Cii-U13ZY-CIF-8MAAXkwQoY7FIAAFpQgEE1bgABeTZ750_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (1762661667, '深圳佳兆业万豪酒店', '棕榈大道33号', 223, 46, '万豪', '深圳', '五钻', '较场尾/大鹏所城', '22.569193', '114.459325', 'https://m.tuniucdn.com/fb2/t1/G6/M00/45/EC/Cii-TF3ZqE-IYM0NAAY6GIHLZNsAAFr2QDhR8EABjow444_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (1765008760, '如家酒店(北京西直门北京北站店)', '西直门北大街49号', 356, 44, '如家', '北京', '二钻', '西直门/北京展览馆地区', '39.945106', '116.353827', 'https://m.tuniucdn.com/fb3/s1/2n9c/4CLwbCE9346jYn7nFsJTQXuBExTJ_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (1880614409, '上海崇明由由喜来登酒店', '揽海路2888号', 2198, 45, '喜来登', '上海', '五钻', '崇明岛/长兴岛/横沙岛', '31.462167', '121.823103', 'https://m.tuniucdn.com/fb3/s1/2n9c/21gDCGgRT3xFqCd3FxBh633j6Qsu_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (1889333146, '如家酒店(北京西客站丽泽桥店)', '西三环南路44号-218', 459, 47, '如家', '北京', '二钻', '北京西站/丽泽商务区', '39.869638', '116.313075', 'https://m.tuniucdn.com/fb3/s1/2n9c/kG5corYUDC7U1qE8RAY6xCVnGxq_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (1908594080, '上海建工浦江皇冠假日酒店', '陈行公路3701号', 843, 46, '皇冠假日', '上海', '五钻', '浦江镇地区', '31.090063', '121.489728', 'https://m.tuniucdn.com/fb2/t1/G6/M00/45/B7/Cii-U13ZZDWIePrGAAPyImW93N0AAFpRgMmj4MAA_I6005_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (1913922369, '上海中建万怡酒店', '蟠文路333号', 889, 47, '万怡', '上海', '四钻', '虹桥机场/国家会展中心', '31.185504', '121.287709', 'https://m.tuniucdn.com/fb3/s1/2n9c/39Afm5Bxgd784eMeFB5DrcsPnhT_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (1916002015, '上海苏宁环球万怡酒店', '丹巴路99号', 689, 45, '万怡', '上海', '四钻', '长风公园地区', '31.22292', '121.379912', 'https://m.tuniucdn.com/fb3/s1/2n9c/svpYHdmVDck91NqAhjtngcXth2G_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (1928731731, '上海康桥万豪酒店', '康新公路4499号', 811, 46, '万豪', '上海', '五钻', '迪士尼度假区', '31.119187', '121.618966', 'https://m.tuniucdn.com/fb3/s1/2n9c/3inpPxTnvRjMCEB39K9FuHaXohYw_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (1931442052, '深圳机场凯悦酒店', '宝安国际机场地面交通中心(GTC)18号出口', 291, 47, '凯悦', '深圳', '五钻', '宝安机场商圈', '22.622498', '113.812341', 'https://m.tuniucdn.com/fb2/t1/G6/M00/45/EC/Cii-TF3ZqVeIJcTPAAUq_Ou_CrcAAFr4gKufPUABSsU446_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (1931602865, '深圳联投东方万怡酒店', '松岗东方大道46号', 688, 45, '万怡', '深圳', '五钻', '松岗商业中心区', '22.760746', '113.856961', 'https://m.tuniucdn.com/fb2/t1/G6/M00/45/EC/Cii-U13ZqVyIOjdpAARI9aeBh-IAAFr4gOQh7oABEkN297_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (1937347815, '北京望京凯悦酒店', '广顺南大街8号院2号楼', 617, 46, '凯悦', '北京', '五钻', '望京/酒仙桥/798地区', '39.991546', '116.476288', 'https://m.tuniucdn.com/fb3/s1/2n9c/2gLT4ZgJ8ZuS7sSmXzYoCXnV248p_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (1942938880, '北京乐多港万豪酒店', '城南街道南口路29号', 227, 45, '万豪', '北京', '五钻', '昌平城区/十三陵度假区', '40.23264', '116.188888', 'https://m.tuniucdn.com/fb3/s1/2n9c/3mBWaZeaqq54E7kX2n7g9b2CZX6q_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (1942992995, '上海嘉定凯悦酒店', '裕民南路1366号', 758, 46, '凯悦', '上海', '五钻', '嘉定新城', '31.352298', '121.263314', 'https://m.tuniucdn.com/fb2/t1/G6/M00/53/2D/Cii-U13edkqIfZhLAAJEW25WIF4AAGVxQIg38sAAkRz517_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (1951709780, '深圳同泰万怡酒店', '福海街道宝安大道 6259号', 617, 48, '万怡', '深圳', '五钻', '深圳国际会展中心商圈', '22.678611', '113.805695', 'https://m.tuniucdn.com/fb3/s1/2n9c/3oUfktphxMAWq9hUxD9uqdjRdZGB_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (1973839294, '深圳湾万怡酒店', '科技南路16号', 508, 47, '万怡', '深圳', '五钻', '科技园', '22.531101', '113.950615', 'https://m.tuniucdn.com/fb3/s1/2n9c/8C9QscRsvTWCx92wt17GAegEMFn_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (1975922994, '如家酒店·neo(深圳南山海岸城南油地铁站店)', '南商路84-6号', 238, 44, '如家', '深圳', '二钻', '海岸城/后海', '22.513566', '113.9291', 'https://m.tuniucdn.com/fb2/t1/G6/M00/25/5E/Cii-TF3PGD-IQ0FcAAFIZC82AnkAAEVvAKdj4YAAUh8638_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (1989806195, '深圳博林天瑞喜来登酒店', '留仙大道4088号', 1369, 48, '喜来登', '深圳', '五钻', '大学城/西丽动物园', '22.582918', '113.97219', 'https://m.tuniucdn.com/fb3/s1/2n9c/4Rx55fZoneUeKbE3TCRSPB6WQ6bw_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (1996823660, '上海紫竹万怡酒店', '紫星路588号3幢', 642, 46, '万怡', '上海', '四钻', '交大/闵行经济开发区', '31.02118', '121.465186', 'https://m.tuniucdn.com/fb2/t1/G6/M00/53/2F/Cii-TF3edraIPzK9AAH_p8vdHKoAAGV3AJgSVEAAf-_019_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (1997830708, '和颐至尚酒店(北京上地软件园店)', '清河小营西路48号汇苑仁和大厦一层', 753, 47, '和颐', '北京', '四钻', '上地产业园/西三旗', '40.034623', '116.323925', 'https://m.tuniucdn.com/fb3/s1/2n9c/2sKjxS1hFYyBFVKVBqo2x2hSFvGj_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (2003479905, '上海榕港万怡酒店', '新松江路1277号', 798, 46, '万怡', '上海', '四钻', '佘山/松江大学城', '31.038198', '121.210178', 'https://m.tuniucdn.com/fb3/s1/2n9c/2GM761BYH8k15qkNrJrja3cwfr2D_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (2009548883, '和颐至尚酒店(北京首都机场新国展店)', '府前二街6号', 611, 46, '和颐', '北京', '三钻', '首都机场/新国展地区', '40.063953', '116.576829', 'https://m.tuniucdn.com/fb3/s1/2n9c/43zCTomkMSkUfZByZxn77YH2XidJ_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (2011785622, '北京世园凯悦酒店', '阜康南路1号院1号楼A', 558, 47, '凯悦', '北京', '五星级', '延庆休闲度假区', '40.440732', '115.963259', 'https://m.tuniucdn.com/fb3/s1/2n9c/uhGcQze3zZQxe4avSU8BysgYVvx_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (2022598930, '上海宝华喜来登酒店', '南奉公路3111弄228号', 2899, 46, '喜来登', '上海', '五钻', '奉贤开发区', '30.921659', '121.575572', 'https://m.tuniucdn.com/fb2/t1/G6/M00/45/BD/Cii-TF3ZaBmIStrbAASnoOyg7FoAAFpYwEoz9oABKe4992_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (2022881967, '深圳盐田凯悦酒店', '海景二路1025号1-6层、30-50层', 650, 47, '凯悦', '深圳', '五钻', '盐田区政府/沙头角', '22.551323', '114.23781', 'https://m.tuniucdn.com/fb3/s1/2n9c/2RFMLSujkczEn1HoybD6dUpN9pzr_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (2031683181, '和颐至尚酒店(北京雍和宫和平里店)', '小黄庄1区15号', 299, 47, '和颐', '北京', '四钻', '马甸/安贞地区', '39.962361', '116.412931', 'https://m.tuniucdn.com/fb3/s1/2n9c/4Xqm5BN9pZTamwmYS3eLxL417YYt_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (2045052822, '深圳国际会展中心皇冠假日酒店', '展云路6号', 675, 47, '皇冠假日', '深圳', '五钻', '深圳国际会展中心商圈', '22.686581', '113.777655', 'https://m.tuniucdn.com/fb3/s1/2n9c/4DGZygQpE4iSpcBDCoXJvjNr4oiR_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (2048042240, '北京大兴希尔顿酒店', '高米店南里18号楼', 1283, 48, '希尔顿', '北京', '五钻', '大兴北京新机场地区', '39.76875', '116.339199', 'https://m.tuniucdn.com/fb3/s1/2n9c/3B32F8zSU2CJCWzs1hoH2o4WcquR_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (2048047291, '北京新青海喜来登酒店', '丽泽金融商务区凤凰嘴北路与金中都西路交叉口西营街8号院1号楼', 723, 47, '喜来登', '北京', '五钻', '北京西站/丽泽商务区', '39.864026', '116.322505', 'https://m.tuniucdn.com/fb3/s1/2n9c/4DPQMu5sMM7XR1mvcjoqtWngc7TF_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (2048050570, '汉庭酒店(深圳坪山火车站店)', '新和路127-2号', 436, 47, '汉庭', '深圳', '二钻', '坪山高铁站商圈', '22.700753', '114.339089', 'https://m.tuniucdn.com/fb3/s1/2n9c/2nXN2bWjfoqoTkPwHvLJQPYz17qD_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (2048671293, '汉庭酒店(深圳观澜五和大道店)', '观湖街道五和大道327号', 234, 43, '汉庭', '深圳', '二钻', '观澜', '22.684459', '114.07708', 'https://m.tuniucdn.com/fb3/s1/2n9c/2JrQi83S9qgDEkXqWpe5iyi44Uh2_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (2051661320, '汉庭酒店(深圳罗湖口岸万象城二店)', '桂园街道宝安南路1050号嘉宾花园C栋', 667, 47, '汉庭', '深圳', '三钻', '万象城/京基100', '22.540352', '114.112668', 'https://m.tuniucdn.com/fb3/s1/2n9c/34FRP7HLPhvKZP1a6tXu4XrJeiaw_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (2056105938, '北京通州北投希尔顿酒店', '新华东街289号2号楼', 1068, 48, '希尔顿', '北京', '五钻', '果园环岛/通州区', '39.908805', '116.659748', 'https://m.tuniucdn.com/fb3/s1/2n9c/NGKdpec3tZJNUUNWJ5pd67Cp5AY_w200_h200_c1_t0.png');
INSERT INTO `hotel` VALUES (2056126831, '上海虹桥金臣皇冠假日酒店', '申长路630弄1-3 号', 2488, 48, '皇冠假日', '上海', '五钻', '虹桥机场/国家会展中心', '31.19036', '121.31535', 'https://m.tuniucdn.com/fb3/s1/2n9c/PvFh4Vzc84xXhm5N41F6AqdAqyJ_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (2056132395, '深圳深铁皇冠假日酒店', '深南大道9819号', 340, 47, '皇冠假日', '深圳', '五钻', '科技园', '22.538923', '113.944794', 'https://m.tuniucdn.com/fb3/s1/2n9c/eBLtrED2uJs7yURWfjnWge9dT1P_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (2056298828, '上海中优城市万豪酒店', '沪南公路7688弄1号', 1200, 45, '万豪', '上海', '五钻', '南汇/野生动物园', '31.030053', '121.662943', 'https://m.tuniucdn.com/fb3/s1/2n9c/2gBATEyysyQWmw3wZL863HGdqjaq_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (2058250574, '深圳湾万丽酒店', '粤海街道高新区社区科技南路18号', 351, 47, '万丽', '深圳', '五钻', '科技园', '22.531674', '113.951882', 'https://m.tuniucdn.com/fb3/s1/2n9c/2YWUpZsvPVkRiKgdPg95LJxaFmB6_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (2060510277, '北京金隅八达岭皇冠假日酒店', '妫水北街1号1-14幢', 1026, 44, '皇冠假日', '北京', '五钻', '延庆休闲度假区', '40.476483', '115.97481', 'https://m.tuniucdn.com/fb3/s1/2n9c/3Dzq2KxgiQbmb1sbc5iK6xqpVuFr_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (2060618247, '汉庭酒店(深圳海岸城店)', '粤海街道后海社区后海第二统建楼商业裙楼第二层B', 562, 49, '汉庭', '深圳', '二钻', '海岸城/后海', '22.507276', '113.931251', 'https://m.tuniucdn.com/fb3/s1/2n9c/TBoXdgEx5Yjc2HobeC3fPWWnSJi_w200_h200_c1_t0.jpg');
INSERT INTO `hotel` VALUES (2062643512, '深圳国际会展中心希尔顿酒店', '展丰路80号', 285, 46, '希尔顿', '深圳', '五钻', '深圳国际会展中心商圈', '22.705335', '113.77794', 'https://m.tuniucdn.com/fb3/s1/2n9c/2SHUVXNrN5NsXsTUwcd1yaHKbrGq_w200_h200_c1_t0.jpg');
4.0.2. 导入项目。
4.0.3. mapping 映射分析。
创建索引库,最关键的是 mapping 映射,而 mapping 映射要考虑的信息包括。
- 字段名。
- 字段数据类型。
- 是否参与搜索。
- 是否需要分词。
- 如果分词,分词器是什么?
其中。
-
字段名、字段数据类型,可以参考数据表结构的名称和类型。
-
是否参与搜索要分析业务来判断,例如图片地址,就无需参与搜索。
-
是否分词呢要看内容,内容如果是一个整体就无需分词,反之则要分词。
-
分词器,我们可以统一使用 ik_max_word。
来看下酒店数据的索引库结构。
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",
"copy_to": "all"
},
"location": {
"type": "geo_point"
},
"pic": {
"type": "keyword",
"index": false
},
"all": {
"type": "text",
"analyzer": "ik_max_word"
}
}
}
}
几个特殊字段说明。
-
location:地理坐标,里面包含精度、纬度。
-
all:一个组合字段,其目的是将多字段的值利用 copy_to 合并,提供给用户搜索。
-
地理坐标说明。
ES 中支持两种地理坐标数据类型。
geo_point:由纬度(latitude)和经度(longitude)确定的一个点。例如:“32.8752345, 120.2981576”。
geo_shape:由多个 geo_point 组成的复杂几何图形。例如一条直线,“LINESTRING (-77.03653 38.897676, -77.009051 38.889939)”。
- copy_to 说明。
字段拷贝可以使用copy_to属性将当前字段拷贝到指定字段。示例:
"all": {
"type": "text",
"analyzer": "ik_max_word"
},
"brand": {
"type": "keyword",
"copt_to": "all"
}
4.0.4. 初始化 RestClient。
在 elasticsearch 提供的 API 中,与 elasticsearch 一切交互都封装在一个名为 RestHighLevelClient 的类中,必须先完成这个对象的初始化,建立与 elasticsearch 的连接。
分为三步。
1)引入 es 的 RestHighLevelClient 依赖。
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
</dependency>
2)因为 SpringBoot 默认的 ES 版本是7.6.2,所以我们需要覆盖默认的 ES 版本。
<properties>
<java.version>1.8</java.version>
<elasticsearch.version>7.12.1</elasticsearch.version>
</properties>
3)初始化 RestHighLevelClient。
初始化的代码如下。
this.restHighLevelClient = new RestHighLevelClient(RestClient.builder(
HttpHost.create("http://192.168.142.186:9200")
));
这里为了单元测试方便,我们创建一个测试类 HotelIndexTest,然后将初始化的代码编写在 @BeforeEach 方法中。
package com.geek.elasticsearchgeek;
import com.geek.elasticsearchgeek.constant.IConstant;
import org.apache.http.HttpHost;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.client.indices.CreateIndexRequest;
import org.elasticsearch.common.xcontent.XContentType;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.IOException;
/**
* @author geek
*/
public class HotelIndexTest {
private RestHighLevelClient restHighLevelClient;
@BeforeEach
void setUp() {
this.restHighLevelClient = new RestHighLevelClient(RestClient.builder(
HttpHost.create("http://192.168.142.186:9200")
));
}
@Test
void testInit() {
System.out.println("restHighLevelClient = " + this.restHighLevelClient);
}
@Test
void testIndices() {
// 创建 Request 对象。
CreateIndexRequest createIndexRequest = new CreateIndexRequest("hotel");
// 请求参数。MAPPING_TEMPLATE 是静态常量字符串,内容的创建索引库的 SDL 语句。
createIndexRequest.source(IConstant.MAPPING_SOURCE, XContentType.JSON);
// 发起请求。
try {
this.restHighLevelClient.indices().create(createIndexRequest, RequestOptions.DEFAULT);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
// @Test
// public void testIndices1() {
// // 创建 Request 对象。
// org.elasticsearch.action.admin.indices.create.CreateIndexRequest createIndexRequest = new org.elasticsearch.action.admin.indices.create.CreateIndexRequest("hotel1");
// // 请求参数。MAPPING_TEMPLATE 是静态常量字符串,内容的创建索引库的 SDL 语句。
// createIndexRequest.source(IConstant.MAPPING_SOURCE, XContentType.JSON);
// // 发起请求。
// try {
// this.restHighLevelClient.indices().create(createIndexRequest, RequestOptions.DEFAULT);
// } catch (IOException e) {
// throw new RuntimeException(e);
// }
// }
@AfterEach
void close() {
try {
this.restHighLevelClient.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
4.1. 创建索引库。
4.1.1. 代码解读。
创建索引库的 API 如下。
代码分为三步。
-
1)创建 Request 对象。因为是创建索引库的操作,因此 Request 是 CreateIndexRequest。
-
2)添加请求参数,其实就是 DSL 的 JSON 参数部分。因为 json 字符串很长,这里是定义了静态字符串常量 MAPPING_TEMPLATE,让代码看起来更加优雅。
-
3)发送请求,client.indices() 方法的返回值是 IndicesClient 类型,封装了所有与索引库操作有关的方法。
4.1.2. 完整示例。
在 hotel-demo 的 com.geek.elasticsearchgeek.constant 包下,创建一个类,定义 mapping 映射的 JSON 字符串常量。
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",
"copy_to": "all"
},
"location": {
"type": "geo_point"
},
"pic": {
"type": "keyword",
"index": false
},
"all": {
"type": "text",
"analyzer": "ik_max_word"
}
}
}
}
package com.geek.elasticsearchgeek.constant;
/**
* @author geek
*/
public interface IConstant {
String MAPPING_SOURCE = "{\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" +
" \"copy_to\": \"all\"\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" +
"}\n";
}
在 hotel-demo 中的 HotelIndexTest 测试类中,编写单元测试,实现创建索引。
@Test
void testIndices() {
// 创建 Request 对象。
CreateIndexRequest createIndexRequest = new CreateIndexRequest("hotel");
// 请求参数。MAPPING_TEMPLATE 是静态常量字符串,内容的创建索引库的 SDL 语句。
createIndexRequest.source(IConstant.MAPPING_SOURCE, XContentType.JSON);
// 发起请求。
try {
this.restHighLevelClient.indices().create(createIndexRequest, RequestOptions.DEFAULT);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
4.2. 删除索引库。
删除索引库的 DSL 语句非常简单。
DELETE /hotel
与创建索引库相比。
-
请求方式从 PUT 变为 DELTE。
-
请求路径不变。
-
无请求参数。
所以代码的差异,注意体现在 Request 对象上。依然是三步走。
-
1)创建 Request 对象。这次是 DeleteIndexRequest 对象。
-
2)准备参数。这里是无参。
-
3)发送请求。改用 delete 方法。
在 hotel-demo 中的 HotelIndexTest 测试类中,编写单元测试,实现删除索引。
@Test
void testDeleteHotelIndex() throws IOException {
// 1. 创建 Request 对象。
DeleteIndexRequest deleteIndexRequest = new DeleteIndexRequest("hotel");
// 2. 发送请求。
this.restHighLevelClient.indices().delete(deleteIndexRequest, RequestOptions.DEFAULT);
}
4.3. 判断索引库是否存在。
判断索引库是否存在,本质就是查询,对应的 DSL 是。
GET /hotel
因此与删除的 Java 代码流程是类似的。依然是三步走。
-
1)创建 Request 对象。这次是 GetIndexRequest 对象。
-
2)准备参数。这里是无参。
-
3)发送请求。改用 exists 方法。
@Test
void testIndexExists() {
// 创建 Request 对象。
GetIndexRequest getIndexRequest = new GetIndexRequest("hotel");
// 发送请求。
boolean exists = false;
try {
exists = this.restHighLevelClient.indices().exists(getIndexRequest, RequestOptions.DEFAULT);
} catch (IOException e) {
throw new RuntimeException(e);
}
// 输出。
System.out.println("exists = " + exists);
}
4.4. 总结。
JavaRestClient 操作 elasticsearch 的流程基本类似。核心是 client.indices(); 方法来获取索引库的操作对象。
索引库操作的基本步骤。
-
初始化 RestHighLevelClient。
-
创建 XxxIndexRequest。XXX 是 Create、Get、Delete。
-
准备 DSL(Create 时需要,其它是无参)。
-
发送请求。调用 RestHighLevelClient#indices().xxx(); 方法,xxx 是 create、exists、delete。
5. RestClient 操作文档。
为了与索引库操作分离,我们再次参加一个测试类,做两件事情。
-
初始化 RestHighLevelClient。
-
我们的酒店数据在数据库,需要利用 IHotelService 去查询,所以注入这个接口。
package com.geek.elasticsearchgeek;
import com.geek.elasticsearchgeek.hotel.service.IHotelService;
import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.io.IOException;
/**
* @author geek
*/
@SpringBootTest
public class HotelDocumentTest {
@Autowired
private IHotelService hotelService;
private RestHighLevelClient restHighLevelClient;
@BeforeEach
void setUp() {
this.restHighLevelClient = new RestHighLevelClient(RestClient.builder(
HttpHost.create("http://192.168.142.186:9200")
));
}
@AfterEach
void tearDown() throws IOException {
this.restHighLevelClient.close();
}
}
5.1. 新增文档。
我们要将数据库的酒店数据查询出来,写入 elasticsearch 中。
5.1.1. 索引库实体类。
数据库查询后的结果是一个 Hotel 类型的对象。结构如下。
package com.geek.elasticsearchgeek.hotel.pojo;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
/**
* @author geek
*/
@Data
@TableName("hotel")
public class Hotel implements Serializable {
@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。
因此,我们需要定义一个新的类型,与索引库结构吻合。
package com.geek.elasticsearchgeek.hotel.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author geek
*/
@Data
@AllArgsConstructor
@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();
}
}
GET /hotel/_doc/38812
{
"_index" : "hotel",
"_type" : "_doc",
"_id" : "38812",
"_version" : 1,
"_seq_no" : 0,
"_primary_term" : 1,
"found" : true,
"_source" : {
"address" : "徐汇龙华西路315弄58号",
"brand" : "7天酒店",
"business" : "八万人体育场地区",
"city" : "上海",
"id" : 38812,
"location" : "31.174377, 121.442875",
"name" : "7天连锁酒店(上海漕溪路地铁站店)",
"pic" : "https://m.tuniucdn.com/fb2/t1/G2/M00/E0/0E/Cii-TlkyIr2IEWNoAAHQYv7i5CkAALD-QP2iJwAAdB6245_w200_h200_c1_t0.jpg",
"price" : 298,
"score" : 37,
"starName" : "二钻"
}
}
5.1.2. 语法说明。
新增文档的 DSL 语句如下。
POST /{索引库名}/_doc/1
{
"name": "Jack",
"age": 21
}
对应的 java 代码如图。
可以看到与创建索引库类似,同样是三步走。
-
1)创建 Request 对象。
-
2)准备请求参数,也就是 DSL 中的 JSON 文档。
-
3)发送请求。
变化的地方在于,这里直接使用 client.xxx(); 的 API,不再需要 client.indices(); 了。
5.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 测试类中,编写单元测试。
@Test
void testAddDocument() {
// 根据 id 查询酒店数据。
Hotel hotel = this.hotelService.getById(38812L);
System.out.println("hotel = " + hotel);
// 转换为文档类型。
HotelDoc hotelDoc = new HotelDoc(hotel);
System.out.println("hotelDoc = " + hotelDoc);
// 将 HotelDoc 转 json。
String toJSONString = JSONObject.toJSONString(hotelDoc);
// 准备 Request 对象。
IndexRequest indexRequest = new IndexRequest("hotel")
.id(hotelDoc.getId().toString());
// 准备 Json 文档。
indexRequest.source(toJSONString, XContentType.JSON);
// 发送请求。
try {
this.restHighLevelClient.index(indexRequest, RequestOptions.DEFAULT);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
5.2. 查询文档。
5.2.1. 语法说明。
查询的 DSL 语句如下。
GET /hotel/_doc/{id}
非常简单,因此代码大概分两步。
-
准备 Request 对象。
-
发送请求。
不过查询的目的是得到结果,解析为 HotelDoc,因此难点是结果的解析。完整代码如下。
可以看到,结果是一个 JSON,其中文档放在一个 _source
属性中,因此解析就是拿到 _source
,反序列化为 Java 对象即可。
与之前类似,也是三步走。
-
1)准备 Request 对象。这次是查询,所以是 GetRequest。
-
2)发送请求,得到结果。因为是查询,这里调用 client.get()方法。
-
3)解析结果,就是对 JSON 做反序列化。
5.2.2. 完整代码。
在 hotel-demo 的 HotelDocumentTest 测试类中,编写单元测试。
@Test
void testGetDocumentById() {
// 准备 Request。
GetRequest getRequest = new GetRequest("hotel", "38812");
// 发送请求,得到响应。
GetResponse response = null;
try {
response = this.restHighLevelClient.get(getRequest, RequestOptions.DEFAULT);
} catch (IOException e) {
throw new RuntimeException(e);
}
// 解析响应结果。
String json = response.getSourceAsString();
HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
System.out.println("hotelDoc = " + hotelDoc);
}
5.3. 删除文档。
删除的 DSL 是这样的。
DELETE /hotel/_doc/{id}
与查询相比,仅仅是请求方式从 DELETE 变成 GET,可以想象 Java 代码应该依然是三步走。
-
1)准备 Request 对象,因为是删除,这次是 DeleteRequest 对象。要指定索引库名和 id。
-
2)准备参数,无参。
-
3)发送请求。因为是删除,所以是 client.delete(); 方法。
在 hotel-demo 的 HotelDocumentTest 测试类中,编写单元测试。
@Test
void testDeleteDocument() {
// 准备 Request。
DeleteRequest request = new DeleteRequest("hotel", "38812");
// 发送请求。
try {
this.restHighLevelClient.delete(request, RequestOptions.DEFAULT);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
5.4. 修改文档。
5.4.1. 语法说明。
修改我们讲过两种方式。
-
全量修改:本质是先根据 id 删除,再新增。
-
增量修改:修改文档中的指定字段值。
在 RestClient 的 API 中,全量修改与新增的 API 完全一致,判断依据是 ID。
-
如果新增时,ID 已经存在,则修改。
-
如果新增时,ID 不存在,则新增。
这里不再赘述,我们主要关注增量修改。
代码示例如图。
与之前类似,也是三步走。
-
1)准备 Request 对象。这次是修改,所以是 UpdateRequest
-
2)准备参数。也就是 JSON 文档,里面包含要修改的字段。
-
3)更新文档。这里调用 client.update(); 方法。
5.4.2. 完整代码。
在 hotel-demo 的 HotelDocumentTest 测试类中,编写单元测试。
@Test
void testUpdateDocument() {
// 准备 Request。
UpdateRequest updateRequest = new UpdateRequest("hotel", "61083");
// 准备请求参数。
updateRequest.doc(
"price", "952",
"starName", "四钻"
);
// 发送请求。
try {
this.restHighLevelClient.update(updateRequest, RequestOptions.DEFAULT);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
5.5. 批量导入文档。
案例需求:利用 BulkRequest 批量将数据库数据导入到索引库中。
步骤如下。
-
利用 mybatis-plus 查询酒店数据。
-
将查询到的酒店数据(Hotel)转换为文档类型数据(HotelDoc)。
-
利用 JavaRestClient 中的 BulkRequest 批处理,实现批量新增文档。
5.5.1. 语法说明。
批量处理 BulkRequest,其本质就是将多个普通的 CRUD 请求组合在一起发送。
其中提供了一个 add 方法,用来添加其他请求。
可以看到,能添加的请求包括。
-
IndexRequest,也就是新增。
-
UpdateRequest,也就是修改。
-
DeleteRequest,也就是删除。
因此 Bulk 中添加了多个 IndexRequest,就是批量新增功能了。示例。
其实还是三步走。
-
1)创建 Request 对象。这里是 BulkRequest
-
2)准备参数。批处理的参数,就是其它 Request 对象,这里就是多个 IndexRequest
-
3)发起请求。这里是批处理,调用的方法为 client.bulk()方法。
我们在导入酒店数据时,将上述代码改造成 for 循环处理即可。
5.5.2. 完整代码。
在 hotel-demo 的 HotelDocumentTest 测试类中,编写单元测试。
@Test
void testBulkRequest() {
// 批量查询酒店数据。
List<Hotel> hotelList = this.hotelService.list();
// 创建 Request。
BulkRequest bulkRequest = new BulkRequest();
// 准备参数,添加多个新增的 Request。
for (Hotel hotel : hotelList) {
// 转换为文档类型 HotelDoc。
HotelDoc hotelDoc = new HotelDoc(hotel);
// 创建新增文档的 Request 对象。
bulkRequest.add(new IndexRequest("hotel")
.id(hotelDoc.getId().toString())
.source(JSON.toJSONString(hotelDoc), XContentType.JSON));
}
// 发送请求。
try {
this.restHighLevelClient.bulk(bulkRequest, RequestOptions.DEFAULT);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
5.6. 小结。
文档操作的基本步骤。
-
初始化 RestHighLevelClient。
-
创建 XxxRequest。XXX 是 Index、Get、Update、Delete、Bulk。
-
准备参数(Index、Update、Bulk 时需要)。
-
发送请求。调用 RestHighLevelClient#.xxx()方法,xxx 是 index、get、update、delete、bulk。
-
解析结果(Get 时需要)。
分布式搜索引擎 02。
在昨天的学习中,我们已经导入了大量数据到 elasticsearch 中,实现了 elasticsearch 的数据存储功能。但 elasticsearch 最擅长的还是搜索和数据分析。
所以今天,我们研究下 elasticsearch 的数据搜索功能。我们会分别使用 DSL 和 RestClient 实现搜索。
0. 学习目标。
1. DSL 查询文档。
elasticsearch 的查询依然是基于 JSON 风格的 DSL 来实现的。
1.1. DSL 查询分类。
Elasticsearch 提供了基于 JSON 的 DSL(Domain Specific Language)来定义查询。常见的查询类型包括。
-
查询所有:查询出所有数据,一般测试用。例如:match_all。
-
全文检索(full text)查询:利用分词器对用户输入内容分词,然后去倒排索引库中匹配。例如。
- match_query
- multi_match_query
-
精确查询:根据精确词条值查找数据,一般是查找 keyword、数值、日期、boolean 等类型字段。例如。
- ids
- range
- term
-
地理(geo)查询:根据经纬度查询。例如。
- geo_distance
- geo_bounding_box
-
复合(compound)查询:复合查询可以将上述各种查询条件组合起来,合并查询条件。例如。
- bool
- function_score
查询的语法基本一致。
GET /indexName/_search
{
"query": {
"查询类型": {
"查询条件": "条件值"
}
}
}
我们以查询所有为例,其中。
-
查询类型为 match_all。
-
没有查询条件。
// 查询所有。
GET /indexName/_search
{
"query": {
"match_all": {
}
}
}
其它查询无非就是查询类型、查询条件的变化。
1.2. 全文检索查询。
1.2.1. 使用场景。
全文检索查询的基本流程如下。
-
对用户搜索的内容做分词,得到词条。
-
根据词条去倒排索引库中匹配,得到文档 id。
-
根据文档 id 找到文档,返回给用户。
比较常用的场景包括。
-
商城的输入框搜索。
-
百度输入框搜索。
例如京东。
因为是拿着词条去匹配,因此参与搜索的字段也必须是可分词的 text 类型的字段。
1.2.2. 基本语法。
常见的全文检索查询包括。
-
match 查询:单字段查询。
-
multi_match 查询:多字段查询,任意一个字段符合条件就算符合查询条件。
match 查询语法如下。
GET /indexName/_search
{
"query": {
"match": {
"FIELD": "TEXT"
}
}
}
mulit_match 语法如下。
GET /indexName/_search
{
"query": {
"multi_match": {
"query": "TEXT",
"fields": [
"FIELD1",
" FIELD12"
]
}
}
}
1.2.3. 示例。
match 查询示例。
multi_match 查询示例。
可以看到,两种查询结果是一样的,为什么?
因为我们将 brand、name、business 值都利用 copy_to 复制到了 all 字段中。因此你根据三个字段搜索,和根据 all 字段搜索效果当然一样了。
但是,搜索字段越多,对查询性能影响越大,因此建议采用 copy_to,然后单字段查询的方式。
1.2.4. 总结。
match 和 multi_match 的区别是什么?
-
match:根据一个字段查询。
-
multi_match:根据多个字段查询,参与查询字段越多,查询性能越差。
1.3. 精准查询。
精确查询一般是查找 keyword、数值、日期、boolean 等类型字段。所以不会对搜索条件分词。常见的有。
-
term:根据词条精确值查询。
-
range:根据值的范围查询。
1.3.1. term 查询。
因为精确查询的字段搜是不分词的字段,因此查询的条件也必须是不分词的词条。查询时,用户输入的内容跟自动值完全匹配时才认为符合条件。如果用户输入的内容过多,反而搜索不到数据。
语法说明。
// term 查询。
GET /indexName/_search
{
"query": {
"term": {
"FIELD": {
"value": "VALUE"
}
}
}
}
示例。
当我搜索的是精确词条时,能正确查询出结果。
但是,当我搜索的内容不是词条,而是多个词语形成的短语时,反而搜索不到。
1.3.2. range 查询。
范围查询,一般应用在对数值类型做范围过滤的时候。比如做价格范围过滤。
基本语法。
// range 查询。
GET /indexName/_search
{
"query": {
"range": {
"FIELD": {
"gte": 10,
// 这里的 gte 代表大于等于,gt 则代表大于。
"lte": 20
// lte 代表小于等于,lt 则代表小于。
}
}
}
}
示例。
1.3.3. 总结。
精确查询常见的有哪些?
-
term 查询:根据词条精确匹配,一般搜索 keyword 类型、数值类型、布尔类型、日期类型字段。
-
range 查询:根据数值范围查询,可以是数值、日期的范围。
1.4. 地理坐标查询。
所谓的地理坐标查询,其实就是根据经纬度查询,官方文档:https://www.elastic.co/guide/en/elasticsearch/reference/current/geo-queries.html。
常见的使用场景包括。
-
携程:搜索我附近的酒店。
-
滴滴:搜索我附近的出租车。
-
微信:搜索我附近的人。
根据经纬度查询。
geo_bounding_box:查询 geo_point 值落在某个矩形范围的所有文档。
geo_distance:查询到指定中心点小于某个距离值的所有文档。
1.4.1. 矩形范围查询。
矩形范围查询,也就是 geo_bounding_box 查询,查询坐标落在某个矩形范围的所有文档。
查询时,需要指定矩形的左上、右下两个点的坐标,然后画出一个矩形,落在该矩形内的都是符合条件的点。
语法如下。
// geo_bounding_box 查询。
GET /indexName/_search
{
"query": {
"geo_bounding_box": {
"FIELD": {
"top_left": {
// 左上点。
"lat": 31.1,
"lon": 121.5
},
"bottom_right": {
// 右下点。
"lat": 30.9,
"lon": 121.7
}
}
}
}
}
这种并不符合“附近的人”这样的需求,所以我们就不做了。
1.4.2. 附近查询。
附近查询,也叫做距离查询(geo_distance):查询到指定中心点小于某个距离值的所有文档。
换句话来说,在地图上找一个点作为圆心,以指定距离为半径,画一个圆,落在圆内的坐标都算符合条件。
语法说明。
// geo_distance 查询。
GET /indexName/_search
{
"query": {
"geo_distance": {
"distance": "15km",
// 半径。
"FIELD": "31.21,121.5"
// 圆心。
}
}
}
示例。
我们先搜索陆家嘴附近 15km 的酒店。
发现共有 23 家酒店。
然后把半径缩短到 3 公里。
可以发现,搜索到的酒店数量减少到了 1 家。
1.5. 复合查询。
复合(compound)查询:复合查询可以将其它简单查询组合起来,实现更复杂的搜索逻辑。常见的有两种。
-
fuction score:算分函数查询,可以控制文档相关性算分,控制文档排名。
-
bool query:布尔查询,利用逻辑关系组合多个其它的查询,实现复杂搜索。
1.5.1. 相关性算分。
当我们利用 match 查询时,文档结果会根据与搜索词条的关联度打分(_score),返回结果时按照分值降序排列。
例如,我们搜索"虹桥如家",结果如下。
[
{
"_score": 17.850193,
"_source": {
"name": "虹桥如家酒店真不错"
}
},
{
"_score": 12.259849,
"_source": {
"name": "外滩如家酒店真不错"
}
},
{
"_score": 11.91091,
"_source": {
"name": "迪士尼如家酒店真不错"
}
}
]
在 elasticsearch 中,早期使用的打分算法是 TF-IDF 算法,公式如下。
T F (词条频率) = ( 词条出现次数 文档中词条总数 ) TF(词条频率)= \left( 词条出现次数 \over 文档中词条总数 \right) TF(词条频率)=(文档中词条总数词条出现次数)
- TF-IDF 算法。
I D F (逆文档频率) = L o g ( 文档总数 包含词条的文档总数 ) IDF(逆文档频率) = Log \left( 文档总数 \over 包含词条的文档总数 \right) IDF(逆文档频率)=Log(包含词条的文档总数文档总数)
s c o r e = ∑ i n T F ( 词条频率 ) ∗ I D F ( 逆文档频率 ) score = \sum_i^n TF(词条频率) * IDF(逆文档频率) score=i∑nTF(词条频率)∗IDF(逆文档频率)
在后来的 5.1 版本升级中,elasticsearch 将算法改进为 BM25 算法,公式如下。
s c o r e ( Q , D ) = ∑ i n l o g ( 1 + ( N − n + 0.5 ) n + 0.5 ) ⋅ ( f i f i + k i ⋅ ( 1 − b + b ⋅ ( d l a v g d l ) ) score(Q, D) = \sum_i^n log \left( 1 + ( N - n + 0.5 ) \over n + 0.5 \right) · \left( f_i \over f_i + k_i · (1 - b + b · \left( dl \over avgdl \right) \right) score(Q,D)=i∑nlog(n+0.51+(N−n+0.5))⋅ fi+ki⋅(1−b+b⋅(avgdldl)fi
TF-IDF 算法有一各缺陷,就是词条频率越高,文档得分也会越高,单个词条对文档影响较大。而 BM25则会让单个词条的算分有一个上限,曲线更加平滑。
小结:elasticsearch 会根据词条和文档的相关度做打分,算法有两种。
-
TF-IDF 算法 ~ 在 elasticsearch 5.0 之前。会随着词频增加而越来越大。
-
BM25 算法 ~ elasticsearch 5.1 版本后采用的算法。会随着词频增加而增大,但增长曲线会趋于水平。
1.5.2. 算分函数查询 ~ function score query。
根据相关度打分是比较合理的需求,但合理的不一定是产品经理需要的。
以百度为例,你搜索的结果中,并不是相关度越高排名越靠前,而是谁掏的钱多排名就越靠前。如图。
要想认为控制相关性算分,就需要利用 elasticsearch 中的 function score 查询了。
1)语法说明。
{
"query": {
"function_score": {
"query": {
"match": {
"all": "外滩"
}
},
"functions": [
{
"filter": {
"term": {
"id": "1"
}
},
"weight": 10
}
],
"boost_mode": "multiply"
}
}
}
function score 查询中包含四部分内容。
-
原始查询条件:query 部分,基于这个条件搜索文档,并且基于 BM25 算法给文档打分,原始算分(query score)。
-
过滤条件:filter 部分,符合该条件的文档才会重新算分。
-
算分函数:符合 filter 条件的文档要根据这个函数做运算,得到的函数算分(function score),有四种函数。
- weight:函数结果是常量。
- field_value_factor:以文档中的某个字段值作为函数结果。
- random_score:以随机数作为函数结果。
- script_score:自定义算分函数算法。
-
加权模式:boost_mode。定义 function score 与 query 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)基于运算模式做运算,得到最终结果,作为相关性算分。
因此,其中的关键点是。
-
过滤条件:决定哪些文档的算分被修改。
-
算分函数:决定函数算分的算法。
-
运算模式:决定最终算分结果。
2)示例。
需求:给“如家”这个品牌的酒店排名靠前一些。
翻译一下这个需求,转换为之前说的四个要点。
-
原始条件:不确定,可以任意变化。
-
过滤条件:brand = “如家”。
-
算分函数:可以简单粗暴,直接给固定的算分结果,weight。
-
运算模式:比如求和。
因此最终的 DSL 语句如下。
GET /hotel/_search
{
"query": {
"function_score": {
// 原始查询,可以是任意条件。
"query": {
....
},
// 算分函数。
"functions": [
{
// 满足的条件,品牌必须是如家。
"filter": {
"term": {
"brand": "如家"
}
},
// 算分权重为 2。
"weight": 2
}
],
// 加权模式,求和。
"boost_mode": "sum"
}
}
}
测试,在未添加算分函数时,如家得分如下。
GET /hotel/_search
{
"query": {
"function_score": {
"query": {
"match": {
"all": "外滩"
}
}
}
}
}
{
"took" : 12,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 3,
"relation" : "eq"
},
"max_score" : 5.9968843,
"hits" : [
{
"_index" : "hotel",
"_type" : "_doc",
"_id" : "60487",
"_score" : 5.9968843,
"_source" : {
"address" : "黄浦路199号",
"brand" : "君悦",
"business" : "外滩地区",
"city" : "上海",
"id" : 60487,
"location" : "31.245409, 121.492969",
"name" : "上海外滩茂悦大酒店",
"pic" : "https://m.tuniucdn.com/fb3/s1/2n9c/2Swp2h1fdj9zCUKsk63BQvVgKLTo_w200_h200_c1_t0.jpg",
"price" : 689,
"score" : 44,
"starName" : "五星级"
}
},
{
"_index" : "hotel",
"_type" : "_doc",
"_id" : "432335",
"_score" : 4.8872695,
"_source" : {
"address" : "唐山路145号",
"brand" : "7天酒店",
"business" : "北外滩地区",
"city" : "上海",
"id" : 432335,
"location" : "31.252585, 121.498753",
"name" : "7天连锁酒店(上海北外滩国际客运中心地铁站店)",
"pic" : "https://m2.tuniucdn.com/filebroker/cdn/res/c1/ba/c1baf64418437c56617f89840c6411ef_w200_h200_c1_t0.jpg",
"price" : 249,
"score" : 35,
"starName" : "二钻"
}
},
{
"_index" : "hotel",
"_type" : "_doc",
"_id" : "434082",
"_score" : 3.8139162,
"_source" : {
"address" : "复兴东路260号",
"brand" : "如家",
"business" : "豫园地区",
"city" : "上海",
"id" : 434082,
"location" : "31.220706, 121.498769",
"name" : "如家酒店·neo(上海外滩城隍庙小南门地铁站店)",
"pic" : "https://m.tuniucdn.com/fb2/t1/G6/M00/52/B6/Cii-U13eXLGIdHFzAAIG-5cEwDEAAGRfQNNIV0AAgcT627_w200_h200_c1_t0.jpg",
"price" : 392,
"score" : 44,
"starName" : "二钻"
}
}
]
}
}
添加了算分函数后,如家得分就提升了。
GET /hotel/_search
{
"query": {
"function_score": {
"query": {
"match": {
"all": "外滩"
}
},
"functions": [
{
"filter": {
"term": {
"brand": "如家"
}
},
"weight": 10
}
]
}
}
}
默认 multiply。
“_score” : 3.8139162, 乘以 10 后变为 38.139162。
{
"took" : 4,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 3,
"relation" : "eq"
},
"max_score" : 38.13916,
"hits" : [
{
"_index" : "hotel",
"_type" : "_doc",
"_id" : "434082",
"_score" : 38.13916,
"_source" : {
"address" : "复兴东路260号",
"brand" : "如家",
"business" : "豫园地区",
"city" : "上海",
"id" : 434082,
"location" : "31.220706, 121.498769",
"name" : "如家酒店·neo(上海外滩城隍庙小南门地铁站店)",
"pic" : "https://m.tuniucdn.com/fb2/t1/G6/M00/52/B6/Cii-U13eXLGIdHFzAAIG-5cEwDEAAGRfQNNIV0AAgcT627_w200_h200_c1_t0.jpg",
"price" : 392,
"score" : 44,
"starName" : "二钻"
}
},
{
"_index" : "hotel",
"_type" : "_doc",
"_id" : "60487",
"_score" : 5.9968843,
"_source" : {
"address" : "黄浦路199号",
"brand" : "君悦",
"business" : "外滩地区",
"city" : "上海",
"id" : 60487,
"location" : "31.245409, 121.492969",
"name" : "上海外滩茂悦大酒店",
"pic" : "https://m.tuniucdn.com/fb3/s1/2n9c/2Swp2h1fdj9zCUKsk63BQvVgKLTo_w200_h200_c1_t0.jpg",
"price" : 689,
"score" : 44,
"starName" : "五星级"
}
},
{
"_index" : "hotel",
"_type" : "_doc",
"_id" : "432335",
"_score" : 4.8872695,
"_source" : {
"address" : "唐山路145号",
"brand" : "7天酒店",
"business" : "北外滩地区",
"city" : "上海",
"id" : 432335,
"location" : "31.252585, 121.498753",
"name" : "7天连锁酒店(上海北外滩国际客运中心地铁站店)",
"pic" : "https://m2.tuniucdn.com/filebroker/cdn/res/c1/ba/c1baf64418437c56617f89840c6411ef_w200_h200_c1_t0.jpg",
"price" : 249,
"score" : 35,
"starName" : "二钻"
}
}
]
}
}
boost_mode 改为 sum,_score 为 “_score” : 13.813916。
GET /hotel/_search
{
"query": {
"function_score": {
"query": {
"match": {
"all": "外滩"
}
},
"functions": [
{
"filter": {
"term": {
"brand": "如家"
}
},
"weight": 10
}
],
"boost_mode": "sum"
}
}
}
{
"took" : 1,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 3,
"relation" : "eq"
},
"max_score" : 13.813916,
"hits" : [
{
"_index" : "hotel",
"_type" : "_doc",
"_id" : "434082",
"_score" : 13.813916,
"_source" : {
"address" : "复兴东路260号",
"brand" : "如家",
"business" : "豫园地区",
"city" : "上海",
"id" : 434082,
"location" : "31.220706, 121.498769",
"name" : "如家酒店·neo(上海外滩城隍庙小南门地铁站店)",
"pic" : "https://m.tuniucdn.com/fb2/t1/G6/M00/52/B6/Cii-U13eXLGIdHFzAAIG-5cEwDEAAGRfQNNIV0AAgcT627_w200_h200_c1_t0.jpg",
"price" : 392,
"score" : 44,
"starName" : "二钻"
}
},
{
"_index" : "hotel",
"_type" : "_doc",
"_id" : "60487",
"_score" : 6.9968843,
"_source" : {
"address" : "黄浦路199号",
"brand" : "君悦",
"business" : "外滩地区",
"city" : "上海",
"id" : 60487,
"location" : "31.245409, 121.492969",
"name" : "上海外滩茂悦大酒店",
"pic" : "https://m.tuniucdn.com/fb3/s1/2n9c/2Swp2h1fdj9zCUKsk63BQvVgKLTo_w200_h200_c1_t0.jpg",
"price" : 689,
"score" : 44,
"starName" : "五星级"
}
},
{
"_index" : "hotel",
"_type" : "_doc",
"_id" : "432335",
"_score" : 5.8872695,
"_source" : {
"address" : "唐山路145号",
"brand" : "7天酒店",
"business" : "北外滩地区",
"city" : "上海",
"id" : 432335,
"location" : "31.252585, 121.498753",
"name" : "7天连锁酒店(上海北外滩国际客运中心地铁站店)",
"pic" : "https://m2.tuniucdn.com/filebroker/cdn/res/c1/ba/c1baf64418437c56617f89840c6411ef_w200_h200_c1_t0.jpg",
"price" : 249,
"score" : 35,
"starName" : "二钻"
}
}
]
}
}
3)小结。
function score query 定义的三要素是什么?
-
过滤条件:哪些文档要加分。
-
算分函数:如何计算 function score。
-
加权方式:function score 与 query score 如何运算。
1.5.3. 布尔查询 ~ Boolean Query。
布尔查询是一个或多个查询子句的组合,每一个子句就是一个子查询。子查询的组合方式有。
-
must:必须匹配每个子查询,类似“与”。
-
should:选择性匹配子查询,类似“或”。
-
must_not:必须不匹配,不参与算分,类似“非”。
-
filter:必须匹配,不参与算分。
比如在搜索酒店时,除了关键字搜索外,我们还可能根据品牌、价格、城市等字段做过滤。
每一个不同的字段,其查询的条件、方式都不一样,必须是多个不同的查询,而要组合这些查询,就必须用 bool 查询了。
需要注意的是,搜索时,参与打分的字段越多,查询的性能也越差。因此这种多条件查询时,建议这样做。
-
搜索框的关键字搜索,是全文检索查询,使用 must 查询,参与算分。
-
其它过滤条件,采用 filter 查询。不参与算分。
1)语法示例。
GET /hotel/_search
{
"query": {
"bool": {
"must": [
{
"term": {
"city": "上海"
}
}
],
"should": [
{
"term": {
"brand": "皇冠假日"
}
},
{
"term": {
"brand": "华美达"
}
}
],
"must_not": [
{
"range": {
"price": {
"lte": 500
}
}
}
],
"filter": [
{
"range": {
"score": {
"gte": 45
}
}
}
]
}
}
}
2)示例。
需求:搜索名字包含“如家”,价格不高于 400,在坐标 31.21,121.5 周围 10km 范围内的酒店。
分析。
-
名称搜索,属于全文检索查询,应该参与算分。放到 must 中。
-
价格不高于 400,用 range 查询,属于过滤条件,不参与算分。放到 must_not 中。
-
周围 10km 范围内,用 geo_distance 查询,属于过滤条件,不参与算分。放到 filter 中。
GET /hotel/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"name": "如家"
}
}
],
"must_not": [
{
"range": {
"price": {
"gte": 400,
"lte": 1000
}
}
}
],
"filter": [
{
"geo_distance": {
"distance": 100,
"distance_unit": "km",
"location": {
"lat": 31.21,
"lon": 121.5
}
}
}
]
}
}
}
默认提示写法是错误的。
{
"error" : {
"root_cause" : [
{
"type" : "parsing_exception",
"reason" : "[geo_distance] query doesn't support multiple fields, found [distance_unit] and [location]",
"line" : 26,
"col" : 25
}
],
"type" : "x_content_parse_exception",
"reason" : "[26:25] [bool] failed to parse field [filter]",
"caused_by" : {
"type" : "parsing_exception",
"reason" : "[geo_distance] query doesn't support multiple fields, found [distance_unit] and [location]",
"line" : 26,
"col" : 25
}
},
"status" : 400
}
GET /hotel/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"name": "如家"
}
}
],
"must_not": [
{
"range": {
"price": {
"gte": 400,
"lte": 1000
}
}
}
],
"filter": [
{
"geo_distance": {
"distance": "10km",
"location": {
"lat": 31.21,
"lon": 121.5
}
}
}
]
}
}
}
{
"took" : 66,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 3,
"relation" : "eq"
},
"max_score" : 1.716568,
"hits" : [
{
"_index" : "hotel",
"_type" : "_doc",
"_id" : "433576",
"_score" : 1.716568,
"_source" : {
"address" : "南京东路480号保安坊内",
"brand" : "如家",
"business" : "人民广场地区",
"city" : "上海",
"id" : 433576,
"location" : "31.236454, 121.480948",
"name" : "如家酒店(上海南京路步行街店)",
"pic" : "https://m.tuniucdn.com/fb2/t1/G6/M00/52/BA/Cii-U13eXVaIQmdaAAWxgzdXXxEAAGRrgNIOkoABbGb143_w200_h200_c1_t0.jpg",
"price" : 379,
"score" : 44,
"starName" : "二钻"
}
},
{
"_index" : "hotel",
"_type" : "_doc",
"_id" : "434082",
"_score" : 1.4689932,
"_source" : {
"address" : "复兴东路260号",
"brand" : "如家",
"business" : "豫园地区",
"city" : "上海",
"id" : 434082,
"location" : "31.220706, 121.498769",
"name" : "如家酒店·neo(上海外滩城隍庙小南门地铁站店)",
"pic" : "https://m.tuniucdn.com/fb2/t1/G6/M00/52/B6/Cii-U13eXLGIdHFzAAIG-5cEwDEAAGRfQNNIV0AAgcT627_w200_h200_c1_t0.jpg",
"price" : 392,
"score" : 44,
"starName" : "二钻"
}
},
{
"_index" : "hotel",
"_type" : "_doc",
"_id" : "1584362548",
"_score" : 1.4178693,
"_source" : {
"address" : "御青路315-317号",
"brand" : "如家",
"business" : "周浦康桥地区",
"city" : "上海",
"id" : 1584362548,
"location" : "31.15719, 121.572392",
"name" : "如家酒店(上海浦东国际旅游度假区御桥地铁站店)",
"pic" : "https://m.tuniucdn.com/fb3/s1/2n9c/2ybd3wqdoBtBeKcPxmyso9y1hNXa_w200_h200_c1_t0.jpg",
"price" : 339,
"score" : 44,
"starName" : "二钻"
}
}
]
}
}
3)小结。
bool 查询有几种逻辑关系?
-
must:必须匹配的条件,可以理解为“与”。
-
should:选择性匹配的条件,可以理解为“或”。
-
must_not:必须不匹配的条件,不参与打分。
-
filter:必须匹配的条件,不参与打分。
2. 搜索结果处理。
搜索的结果可以按照用户指定的方式去处理或展示。
2.1. 排序。
elasticsearch 默认是根据相关度算分(_score)来排序,但是也支持自定义方式对搜索结果排序。可以排序字段类型有:keyword 类型、数值类型、地理坐标类型、日期类型等。
2.1.1. 普通字段排序。
keyword、数值、日期类型排序的语法基本一致。
语法。
GET /indexName/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"FIELD": "desc"
// 排序字段、排序方式 ASC、DESC。
}
]
}
GET /indexName/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"_geo_distance": {
"FIELD": "经度、纬度",
"order": "asc",
"unit": "km"
}
}
]
}
排序条件是一个数组,也就是可以写多个排序条件。按照声明的顺序,当第一个条件相等时,再按照第二个条件排序,以此类推。
示例。
需求描述:酒店数据按照用户评价(score)降序排序,评价相同的按照价格(price)升序排序。
GET /hotel/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"score": {
"order": "desc"
}
},
{
"price": "asc"
}
]
}
2.1.2. 地理坐标排序。
地理坐标排序略有不同。
语法说明。
GET /indexName/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"_geo_distance": {
"FIELD": {
"纬度": ,
"经度":
}
// 文档中 geo_point 类型的字段名、目标坐标点。
"order": "asc",
// 排序方式。
"unit": "km"
// 排序的距离单位。
}
}
]
}
这个查询的含义是。
- 指定一个坐标,作为目标点。
- 计算每一个文档中,指定字段(必须是 geo_point 类型)的坐标 到目标点的距离是多少。
- 根据距离排序。
示例。
需求描述:实现对酒店数据按照到你的位置坐标的距离升序排序。
提示:获取你的位置的经纬度的方式:https://lbs.amap.com/demo/jsapi-v2/example/map/click-to-get-lnglat/
假设我的位置是:31.034661,121.612282,寻找我周围距离最近的酒店。
GET /hotel/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"_geo_distance": {
"location": {
"lat": 31.034661,
"lon": 121.612282
},
"order": "asc",
"unit": "km"
}
}
]
}
2.2. 分页。
elasticsearch 默认情况下只返回 top10的数据。而如果要查询更多数据就需要修改分页参数了。elasticsearch 中通过修改 from、size 参数来控制要返回的分页结果。
-
from:从第几个文档开始。
-
size:总共查询几个文档。
类似于 MySQL 中的 limit ?, ?
。
2.2.1. 基本的分页。
分页的基本语法如下。
GET /hotel/_search
{
"query": {
"match_all": {}
},
"from": 0, // 分页开始的位置,默认为 0。
"size": 10, // 期望获取的文档总数。
"sort": [
{
"price": "asc"
}
]
}
2.2.2. 深度分页问题。
现在,我要查询 990~1000 的数据,查询逻辑要这么写。
GET /hotel/_search
{
"query": {
"match_all": {}
},
"from": 990,
// 分页开始的位置,默认为 0。
"size": 10,
// 期望获取的文档总数。
"sort": [
{
"price": "asc"
}
]
}
这里是查询 990 开始的数据,也就是 第 990 ~ 第 1000 条 数据。
不过,elasticsearch 内部分页时,必须先查询 0~1000 条,然后截取其中的 990 ~ 1000的这10条。
查询 TOP1000,如果 es 是单点模式,这并无太大影响。
但是 elasticsearch 将来一定是集群,例如我集群有 5 个节点,我要查询 TOP1000 的数据,并不是每个节点查询 200 条就可以了。
因为节点 A 的 TOP200,在另一个节点可能排到 10000 名以外了。
因此要想获取整个集群的 TOP1000,必须先查询出每个节点的 TOP1000,汇总结果后,重新排名,重新截取 TOP1000。
那如果我要查询 9900~10000 的数据呢?是不是要先查询 TOP10000 呢?那每个节点都要查询 10000 条?汇总到内存中?
当查询分页深度较大时,汇总数据过多,对内存和 CPU 会产生非常大的压力,因此 elasticsearch 会禁止 from+ size 超过 10000 的请求。
GET /hotel/_search
{
"query": {
"match_all": {}
},
"from": 9991,
"size": 10
}
{
"error" : {
"root_cause" : [
{
"type" : "illegal_argument_exception",
"reason" : "Result window is too large, from + size must be less than or equal to: [10000] but was [10001]. See the scroll api for a more efficient way to request large data sets. This limit can be set by changing the [index.max_result_window] index level setting."
}
],
"type" : "search_phase_execution_exception",
"reason" : "all shards failed",
"phase" : "query",
"grouped" : true,
"failed_shards" : [
{
"shard" : 0,
"index" : "hotel",
"node" : "-uA2eH37RRi_Ix3VqR3Sww",
"reason" : {
"type" : "illegal_argument_exception",
"reason" : "Result window is too large, from + size must be less than or equal to: [10000] but was [10001]. See the scroll api for a more efficient way to request large data sets. This limit can be set by changing the [index.max_result_window] index level setting."
}
}
],
"caused_by" : {
"type" : "illegal_argument_exception",
"reason" : "Result window is too large, from + size must be less than or equal to: [10000] but was [10001]. See the scroll api for a more efficient way to request large data sets. This limit can be set by changing the [index.max_result_window] index level setting.",
"caused_by" : {
"type" : "illegal_argument_exception",
"reason" : "Result window is too large, from + size must be less than or equal to: [10000] but was [10001]. See the scroll api for a more efficient way to request large data sets. This limit can be set by changing the [index.max_result_window] index level setting."
}
}
},
"status" : 400
}
针对深度分页,ES 提供了两种解决方案,官方文档。
-
search after:分页时需要排序,原理是从上一次的排序值开始,查询下一页数据。官方推荐使用的方式。
-
scroll:原理将排序后的文档 id 形成快照,保存在内存。官方已经不推荐使用。
2.2.3. 小结。
分页查询的常见实现方案以及优缺点。
-
from + size
。- 优点:支持随机翻页。
- 缺点:深度分页问题,默认查询上限(from + size)是10000。
- 场景:百度、京东、谷歌、淘宝这样的随机翻页搜索。
-
after search
。- 优点:没有查询上限(单次查询的 size 不超过10000)。
- 缺点:只能向后逐页查询,不支持随机翻页。
- 场景:没有随机翻页需求的搜索,例如手机向下滚动翻页。
-
scroll
。- 优点:没有查询上限(单次查询的 size 不超过10000)。
- 缺点:会有额外内存消耗,并且搜索结果是非实时的。
- 场景:海量数据的获取和迁移。从 ES7.1开始不推荐,建议用 after search 方案。
2.3. 高亮。
2.3.1. 高亮原理。
什么是高亮显示呢?
我们在百度,京东搜索时,关键字会变成红色,比较醒目,这叫高亮显示。
高亮显示的实现分为两步。
-
1)给文档中的所有关键字都添加一个标签,例如
<em></em>
标签。 -
2)页面给
<em></em>
标签编写 CSS 样式。
2.3.2. 实现高亮。
高亮的语法。
GET /hotel/_search
{
"query": {
"match": {
// 查询条件,高亮一定要使用全文检索查询。
"FIELD": "TEXT"
}
},
"highlight": {
// fields 可指定多个。
"fields": {
// 指定要高亮的字段。
"FIELD": {
// 用来标记高亮字段的前置标签。
"pre_tags": "<em>",
// 用来标记高亮字段的后置标签。
"post_tags": "</em>"
}
}
}
}
注意。
-
高亮是对关键字高亮,因此搜索条件必须带有关键字,而不能是范围这样的查询。
-
默认情况下,高亮的字段,必须与搜索指定的字段一致,否则无法高亮。
-
如果要对非搜索字段高亮,则需要添加一个属性:required_field_match=false。
示例。
2.4. 总结。
查询的 DSL 是一个大的 JSON 对象,包含下列属性。
- query:查询条件。
- from 和 size:分页条件。
- sort:排序条件。
- highlight:高亮条件。
示例。
3. RestClient 查询文档。
文档的查询同样适用昨天学习的 RestHighLevelClient 对象,基本步骤包括。
-
1)准备 Request 对象。
-
2)准备请求参数。
-
3)发起请求。
-
4)解析响应。
3.1. 快速入门。
我们以 match_all 查询为例。
3.1.1. 发起查询请求。
代码解读。
-
第一步,创建
SearchRequest
对象,指定索引库名。 -
第二步,利用
request.source();
构建 DSL,DSL 中可以包含查询、分页、排序、高亮等。query();
:代表查询条件,利用QueryBuilders.matchAllQuery();
构建一个 match_all 查询的 DSL。
-
第三步,利用
client.search();
发送请求,得到响应。
这里关键的 API 有两个,一个是 request.source();
,其中包含了查询、排序、分页、高亮等所有功能。
另一个是 QueryBuilders
,其中包含 match、term、function_score、bool 等各种查询。
3.1.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 文档数据。
3.1.3. 完整代码。
完整代码如下。
package com.geek.elasticsearchgeek;
import com.alibaba.fastjson.JSON;
import com.geek.elasticsearchgeek.hotel.pojo.HotelDoc;
import org.apache.http.HttpHost;
import org.apache.lucene.search.TotalHits;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import java.io.IOException;
/**
* @author geek
*/
@SpringBootTest
public class HotelDocumentSearchTest {
private RestHighLevelClient restHighLevelClient;
@BeforeEach
void setUp() {
this.restHighLevelClient = new RestHighLevelClient(RestClient.builder(HttpHost.create("http://192.168.142.186:9200")));
}
@Test
void testMatchAll() {
// 准备 Request。
SearchRequest searchRequest = new SearchRequest("hotel");
// 准备 DSL。
searchRequest.source()
.query(QueryBuilders.matchAllQuery());
// 发送请求。
SearchResponse searchResponse = null;
try {
searchResponse = this.restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
System.out.println("searchResponse = " + searchResponse);
} catch (IOException e) {
throw new RuntimeException(e);
}
// 解析响应。
handleResponse(searchResponse);
}
private void handleResponse(SearchResponse searchResponse) {
// 解析响应。
SearchHits searchHits = searchResponse.getHits();
System.out.println("searchHits = " + searchHits);
// 获取总条数。
TotalHits totalHits = searchHits.getTotalHits();
System.out.println("totalHits = " + totalHits);
long total = searchHits.getTotalHits().value;
System.out.println("共搜索 " + total + " 条数据。");
// 文档数组。
SearchHit[] hits = searchHits.getHits();
// 遍历。
for (SearchHit hit : hits) {
// 获取文档 source。
String hitSourceAsString = hit.getSourceAsString();
// 反序列化。
HotelDoc hotelDoc = JSON.parseObject(hitSourceAsString, HotelDoc.class);
System.out.println("hotelDoc = " + hotelDoc);
}
}
@AfterEach
void tearDown() throws IOException {
this.restHighLevelClient.close();
}
}
3.1.4. 小结。
查询的基本步骤是。
-
创建 SearchRequest 对象。
-
准备 Request.source(),也就是 DSL。
① QueryBuilders 来构建查询条件。
② 传入 Request.source() 的 query() 方法。
-
发送请求,得到结果。
-
解析结果(参考 JSON 结果,从外到内,逐层解析)。
3.2.match 查询。
全文检索的 match 和 multi_match 查询与 match_all 的 API 基本一致。差别是查询条件,也就是 query 的部分。
GET /hotel/_search
{
"query": {
"match_all": {}
}
}
GET /hotel/_search
{
"query": {
"match": {
"all": "如家"
}
}
}
GET /hotel/_search
{
"query": {
"multi_match": {
"query": "如家",
"fields": ["name", "brand"]
}
}
}
因此,Java 代码上的差异主要是 request.source().query(); 中的参数了。同样是利用 QueryBuilders 提供的方法。
searchRequest.source()
.query(QueryBuilders.matchQuery("all", "如家"));
searchRequest.source()
.query(QueryBuilders.multiMatchQuery("如家", "name", "business"));
// 条件查询。
QueryBuilders.termQuery("city", "武汉");
// 范围查询。
QueryBuilders.rangeQuery("price").gt(100).lt(150);
而结果解析代码则完全一致,可以抽取并共享。
完整代码如下。
@Test
void testMatch() {
// 准备 Request。
SearchRequest searchRequest = new SearchRequest("hotel");
// 准备 DSL。
searchRequest.source()
.query(QueryBuilders.matchQuery("all", "如家"));
// 发送请求。
SearchResponse searchResponse = null;
try {
searchResponse = this.restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
System.out.println("searchResponse = " + searchResponse);
} catch (IOException e) {
throw new RuntimeException(e);
}
// 解析响应。
handleResponse(searchResponse);
}
3.3. 精确查询。
精确查询主要是两者。
-
term:词条精确匹配。
-
range:范围查询。
与之前的查询相比,差异同样在查询条件,其它都一样。
查询条件构造的 API 如下。
// 条件查询。
QueryBuilders.termQuery("city", "武汉");
// 范围查询。
QueryBuilders.rangeQuery("price").gt(100).lt(150);
3.4. 布尔查询。
布尔查询是用 must、must_not、filter 等方式组合其它查询,代码示例如下。
可以看到,API 与其它查询的差别同样是在查询条件的构建,QueryBuilders,结果解析等其他代码完全不变。
完整代码如下。
@Test
void testBoolean() {
// 准备 Request。
SearchRequest searchRequest = new SearchRequest("hotel");
// 准备 DSL。
// 准备 BooleanQuery。
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
// 添加 term。
boolQueryBuilder.must(QueryBuilders.termQuery("city", "杭州"));
// 添加 range。
boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").lte(250));
searchRequest.source().query(boolQueryBuilder);
// 发送请求。
SearchResponse response = null;
try {
response = this.restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
} catch (IOException e) {
throw new RuntimeException(e);
}
System.out.println("response = " + response);
// 解析响应。
handleResponse(response);
}
3.5. 排序、分页。
搜索结果的排序和分页是与 query 同级的参数,因此同样是使用 request.source()来设置。
对应的 API 如下。
完整代码示例。
@Test
void testPageAndSort() {
// 页码,每页大小。
int page = 1, size = 5;
// 准备 Request。
SearchRequest searchRequest = new SearchRequest("hotel");
// 准备 DSL。
// query。
searchRequest.source().query(QueryBuilders.matchAllQuery());
// 排序 sort。
searchRequest.source().sort("price", SortOrder.ASC);
// 分页 from、size。
searchRequest.source().from((page - 1) * size).size(size);
// 发送请求。
SearchResponse searchResponse = null;
try {
searchResponse = this.restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
} catch (IOException e) {
throw new RuntimeException(e);
}
// 解析响应。
handleResponse(searchResponse);
}
3.6. 高亮。
高亮的代码与之前代码差异较大,有两点。
- 查询的 DSL:其中除了查询条件,还需要添加高亮条件,同样是与 query 同级。
- 结果解析:结果除了要解析_source 文档数据,还要解析高亮结果。
3.6.1. 高亮请求构建。
高亮请求的构建 API 如下。
上述代码省略了查询条件部分,但是大家不要忘了:高亮查询必须使用全文检索查询,并且要有搜索关键字,将来才可以对关键字高亮。
完整代码如下。
@Test
void testHighlight() {
// 准备 Request。
SearchRequest searchRequest = new SearchRequest("hotel");
// 准备 DSL。
// query。
searchRequest.source().query(QueryBuilders.matchQuery("all", "如家"));
// 高亮。
searchRequest.source().highlighter(new HighlightBuilder().field("name").requireFieldMatch(false));
// 发送请求。
SearchResponse searchResponse = null;
try {
searchResponse = this.restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
} catch (IOException e) {
throw new RuntimeException(e);
}
System.out.println("searchResponse = " + searchResponse);
// 解析响应。
handleResponseHighlight(searchResponse);
}
3.6.2. 高亮结果解析。
高亮的结果与查询的文档结果默认是分离的,并不在一起。
因此解析高亮的代码需要额外处理。
代码解读。
- 第一步:从结果中获取 source。hit.getSourceAsString();,这部分是非高亮结果,json 字符串。还需要反序列为 HotelDoc 对象。
- 第二步:获取高亮结果。hit.getHighlightFields(),返回值是一个 Map,key 是高亮字段名称,值是 HighlightField 对象,代表高亮值。
- 第三步:从 map 中根据高亮字段名称,获取高亮字段值对象 HighlightField。
- 第四步:从 HighlightField 中获取 Fragments,并且转为字符串。这部分就是真正的高亮字符串了。
- 第五步:用高亮的结果替换 HotelDoc 中的非高亮结果。
完整代码如下。
private void handleResponseHighlight(SearchResponse searchResponse) {
// 解析响应。
SearchHits searchHits = searchResponse.getHits();
// 获取总条数。
TotalHits totalHits = searchHits.getTotalHits();
System.out.println("totalHits = " + totalHits);
long total = totalHits.value;
System.out.println(" ~ handleResponseHighlight; ~ 共搜索到 " + total + " 条数据");
// 文档数组。
SearchHit[] hits = searchHits.getHits();
System.out.println("hits = " + Arrays.toString(hits));
// 遍历。
for (SearchHit hit : hits) {
// 获取文档 source。
String sourceAsString = hit.getSourceAsString();
System.out.println("sourceAsString = " + sourceAsString);
// 反序列化。
HotelDoc hotelDoc = JSON.parseObject(sourceAsString, HotelDoc.class);
System.out.println("hotelDoc = " + hotelDoc);
// 获取高亮结果。
Map<String, HighlightField> highlightFields = hit.getHighlightFields();
System.out.println("highlightFields = " + highlightFields);
if (CollectionUtils.isEmpty(highlightFields)) {
continue;
}
// 根据字段名获取高亮结果。
HighlightField highlightField = highlightFields.get("name");
System.out.println("highlightField = " + highlightField);
if (highlightField != null) {
// 获取高亮值。
Text[] highlightFieldFragments = highlightField.getFragments();
System.out.println("highlightFieldFragments = " + Arrays.toString(highlightFieldFragments));
Text highlightFieldFragmentText0 = highlightFieldFragments[0];
System.out.println("highlightFieldFragmentText0 = " + highlightFieldFragmentText0);
String highlightFieldFragmentText0String = highlightFieldFragmentText0.string();
System.out.println("highlightFieldFragmentText0String = " + highlightFieldFragmentText0String);
String highlightFieldFragmentText0ToString = highlightFieldFragmentText0.toString();
System.out.println("highlightFieldFragmentText0ToString = " + highlightFieldFragmentText0ToString);
// 覆盖非高亮结果。
hotelDoc.setName(highlightFieldFragmentText0String);
}
System.out.println("hotelDoc = " + hotelDoc);
}
}
4. 黑马旅游案例。
下面,我们通过黑马旅游的案例来实战演练下之前学习的知识。
我们实现四部分功能。
-
酒店搜索和分页。
-
酒店结果过滤。
-
我周边的酒店。
-
酒店竞价排名。
启动我们提供的 hotel-demo 项目,其默认端口是 8089,访问 http://localhost:8090,就能看到项目页面了。
4.1. 酒店搜索和分页。
案例需求:实现黑马旅游的酒店搜索功能,完成关键字搜索和分页。
4.1.1. 需求分析。
在项目的首页,有一个大大的搜索框,还有分页按钮。
点击搜索按钮,可以看到浏览器控制台发出了请求。
请求参数如下。
由此可以知道,我们这个请求的信息如下。
- 请求方式:POST。
- 请求路径:/hotel/list。
- 请求参数:JSON 对象,包含 4 个字段。
- key:搜索关键字。
- page:页码。
- size:每页大小。
- sortBy:排序,目前暂不实现。
- 返回值:分页查询,需要返回分页结果 PageResult,包含两个属性。
total
:总条数。List<HotelDoc>
:当前页的数据。
因此,我们实现业务的流程如下。
-
步骤一:定义实体类,接收请求参数的 JSON 对象。
-
步骤二:编写 controller,接收页面的请求。
-
步骤三:编写业务实现,利用 RestHighLevelClient 实现搜索、分页。
4.1.2. 定义实体类。
实体类有两个,一个是前端的请求参数实体,一个是服务端应该返回的响应结果实体。
1)请求参数。
前端请求的 json 结构如下。
{
"key": "搜索关键字",
"page": 1,
"size": 3,
"sortBy": "default"
}
因此,我们在 com.geek.elasticsearchgeek.hotel.dto
包下定义一个实体类。
package com.geek.elasticsearchgeek.hotel.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* @author geek
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class RequestParams implements Serializable {
private String key;
private Integer page;
private Integer size;
private String sortBy;
}
2)返回值。
分页查询,需要返回分页结果 PageResult,包含两个属性。
-
total
:总条数。 -
List<HotelDoc>
:当前页的数据。
因此,我们在 com.geek.elasticsearchgeek.hotel.dto
中定义返回结果。
package com.geek.elasticsearchgeek.hotel.dto;
import com.geek.elasticsearchgeek.hotel.pojo.HotelDoc;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.List;
/**
* @author geek
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PageResult implements Serializable {
private Long total;
private List<HotelDoc> hotelDocList;
}
4.1.3. 定义 controller。
定义一个 HotelController,声明查询接口,满足下列要求。
- 请求方式:Post
- 请求路径:/hotel/list
- 请求参数:对象,类型为 RequestParams
- 返回值:PageResult,包含两个属性。
Long total
:总条数。List<HotelDoc> hotels
:酒店数据。
因此,我们在 com.geek.elasticsearchgeek.hotel.controller
中定义 HotelController。
package com.geek.elasticsearchgeek.hotel.controller;
import com.geek.elasticsearchgeek.hotel.dto.PageResult;
import com.geek.elasticsearchgeek.hotel.dto.RequestParams;
import com.geek.elasticsearchgeek.hotel.service.IHotelService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author geek
*/
@Slf4j
@RestController
@RequestMapping("/hotel")
public class HotelController {
@Autowired
private IHotelService hotelService;
@RequestMapping("/list")
public PageResult getList(@RequestBody RequestParams requestParams) {
return this.hotelService.getList(requestParams);
}
}
4.1.4. 实现搜索业务。
我们在 controller 调用了 IHotelService,并没有实现该方法,因此下面我们就在 IHotelService 中定义方法,并且去实现业务逻辑。
1)在 com.geek.elasticsearchgeek.hotel.service
中的 IHotelService
接口中定义一个方法。
package com.geek.elasticsearchgeek.hotel.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.geek.elasticsearchgeek.hotel.dto.PageResult;
import com.geek.elasticsearchgeek.hotel.dto.RequestParams;
import com.geek.elasticsearchgeek.hotel.pojo.Hotel;
/**
* @author geek
*/
public interface IHotelService extends IService<Hotel> {
/**
* 根据关键字搜索酒店信息。
*
* @param requestParams
* @return
*/
PageResult getList(RequestParams requestParams);
}
2)实现搜索业务,肯定离不开 RestHighLevelClient,我们需要把它注册到 Spring 中作为一个 Bean。在com.geek.elasticsearchgeek.hotel.context
中的 BeanIoC
中声明这个 Bean。
package com.geek.elasticsearchgeek.hotel.context;
import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author geek
*/
@Configuration
public class BeanIoC {
@Bean
public RestHighLevelClient restHighLevelClient() {
return new RestHighLevelClient(
RestClient.builder(
HttpHost.create("http://192.168.142.186:9200")
)
);
}
}
3)在 com.geek.elasticsearchgeek.hotel.service.impl
中的 HotelServiceImpl
中实现 getList(); 方法。
package com.geek.elasticsearchgeek.hotel.service.impl;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.geek.elasticsearchgeek.hotel.dto.PageResult;
import com.geek.elasticsearchgeek.hotel.dto.RequestParams;
import com.geek.elasticsearchgeek.hotel.mapper.IHotelMapper;
import com.geek.elasticsearchgeek.hotel.pojo.Hotel;
import com.geek.elasticsearchgeek.hotel.pojo.HotelDoc;
import com.geek.elasticsearchgeek.hotel.service.IHotelService;
import org.apache.lucene.search.TotalHits;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* @author geek
*/
@Service
public class HotelServiceImpl extends ServiceImpl<IHotelMapper, Hotel> implements IHotelService {
@Autowired
private RestHighLevelClient restHighLevelClient;
@Override
public PageResult getList(RequestParams requestParams) {
// 准备 Request。
SearchRequest searchRequest = new SearchRequest("hotel");
// 准备 DSL。
// query。
String key = requestParams.getKey();
if (StringUtils.hasLength(key)) {
searchRequest.source().query(QueryBuilders.matchAllQuery());
} else {
searchRequest.source().query(QueryBuilders.matchQuery("all", key));
}
// 分页。
Integer page = requestParams.getPage();
Integer size = requestParams.getSize();
searchRequest.source().from((page - 1) * size).size(size);
// 发送请求。
SearchResponse searchResponse = null;
try {
searchResponse = this.restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
} catch (IOException e) {
throw new RuntimeException(e);
}
System.out.println("searchResponse = " + searchResponse);
// 解析响应。
return handleResponse(searchResponse);
}
private PageResult handleResponse(SearchResponse searchResponse) {
// 解析响应。
SearchHits searchHits = searchResponse.getHits();
// 获取总条数。
TotalHits totalHits = searchHits.getTotalHits();
System.out.println("totalHits = " + totalHits);
long total = totalHits.value;
System.out.println(" ~ handleResponseHighlight; ~ 共搜索到 " + total + " 条数据");
// 文档数组。
SearchHit[] hits = searchHits.getHits();
System.out.println("hits = " + Arrays.toString(hits));
// 遍历。
List<HotelDoc> hotelDocList = new ArrayList<>();
for (SearchHit hit : hits) {
// 获取文档 source。
String sourceAsString = hit.getSourceAsString();
System.out.println("sourceAsString = " + sourceAsString);
// 反序列化。
HotelDoc hotelDoc = JSON.parseObject(sourceAsString, HotelDoc.class);
System.out.println("hotelDoc = " + hotelDoc);
boolean add = hotelDocList.add(hotelDoc);
System.out.println(" ~ add ~ " + add + "\r\n ~ hotelDoc = " + hotelDoc);
}
return new PageResult((long) hotelDocList.size(), hotelDocList);
}
}
4.2. 酒店结果过滤。
需求:添加品牌、城市、星级、价格等过滤功能。
4.2.1. 需求分析。
在页面搜索框下面,会有一些过滤项。
传递的参数如图。
包含的过滤条件有。
-
brand:品牌值。
-
city:城市。
-
minPrice~maxPrice:价格范围。
-
starName:星级。
我们需要做两件事情。
-
修改请求参数的对象 RequestParams,接收上述参数。
-
修改业务逻辑,在搜索条件之外,添加一些过滤条件。
4.2.2. 修改实体类。
修改在 com.geek.elasticsearchgeek.hotel.pojo
包下的实体类 RequestParams。
package com.geek.elasticsearchgeek.hotel.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* @author geek
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class RequestParams implements Serializable {
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;
}
4.2.3. 修改搜索业务。
在 HotelService 的 search(); 方法中,只有一个地方需要修改:requet.source().query( … ); 其中的查询条件。
在之前的业务中,只有 match 查询,根据关键字搜索,现在要添加条件过滤,包括。
-
品牌过滤:是 keyword 类型,用 term 查询。
-
星级过滤:是 keyword 类型,用 term 查询。
-
价格过滤:是数值类型,用 range 查询。
-
城市过滤:是 keyword 类型,用 term 查询。
多个查询条件组合,肯定是 boolean 查询来组合。
-
关键字搜索放到 must 中,参与算分。
-
其它过滤条件放到 filter 中,不参与算分。
因为条件构建的逻辑比较复杂,这里先封装为一个函数。
buildBasicQuery 的代码如下。
private void buildBasicQuery(RequestParams requestParams, SearchRequest searchRequest) {
// 构建 BooleanQuery。
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
// 关键字搜索。
String key = requestParams.getKey();
if (key == null || "".equals(key)) {
boolQueryBuilder.must(QueryBuilders.matchAllQuery());
} else {
boolQueryBuilder.must(QueryBuilders.matchQuery("all", key));
}
// 条件过滤。
// 城市条件。
if (requestParams.getCity() != null && !"".equals(requestParams.getCity())) {
// filter 不参与算分。
boolQueryBuilder.filter(QueryBuilders.termQuery("city", requestParams.getCity()));
}
// 品牌条件。
if (requestParams.getBrand() != null && !"".equals(requestParams.getBrand())) {
boolQueryBuilder.filter(QueryBuilders.termQuery("brand", requestParams.getBrand()));
}
// 星级条件。
if (requestParams.getStarName() != null && !"".equals(requestParams.getStarName())) {
boolQueryBuilder.filter(QueryBuilders.termQuery("starName", requestParams.getStarName()));
}
// 价格。
if (requestParams.getMinPrice() != null && requestParams.getMaxPrice() != null) {
boolQueryBuilder.filter(QueryBuilders
.rangeQuery("price")
.gte(requestParams.getMinPrice())
.lte(requestParams.getMaxPrice())
);
}
// 放入 source。
searchRequest.source().query(functionScoreQueryBuilder);
}
4.3. 我附近的酒店。
需求:我附近的酒店。
4.3.1. 需求分析。
在酒店列表页的右侧,有一个小地图,点击地图的定位按钮,地图会找到你所在的位置。
并且,在前端会发起查询请求,将你的坐标发送到服务端。
我们要做的事情就是基于这个 location 坐标,然后按照距离对周围酒店排序。实现思路如下。
-
修改 RequestParams 参数,接收 location 字段。
-
修改 search 方法业务逻辑,如果 location 有值,添加根据 geo_distance 排序的功能。
4.3.2. 修改实体类。
修改在 com.geek.elasticsearchgeek.hotel.dto
包下的实体类 RequestParams。
package com.geek.elasticsearchgeek.hotel.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* @author geek
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class RequestParams implements Serializable {
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;
}
4.3.3. 距离排序 API。
我们以前学习过排序功能,包括两种。
-
普通字段排序。
-
地理坐标排序。
我们只讲了普通字段排序对应的 java 写法。地理坐标排序只学过 DSL 语法,如下。
GET /indexName/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"price": "asc"
},
{
"_geo_distance": {
"FIELD": "纬度,经度",
"order": "asc",
"unit": "km"
}
}
]
}
对应的 java 代码示例。
4.3.4. 添加距离排序。
在 com.geek.elasticsearchgeek.hotel.service.impl
的 HotelServiceImpl
的 getList
(); 方法中,添加一个排序功能。
完整代码。
@Override
public PageResult getList(RequestParams requestParams) {
// 准备 Request。
SearchRequest searchRequest = new SearchRequest("hotel");
log.info(" ~ HotelServiceImpl ~ getList; ~ searchRequest ~ {}" + "\r\n ~ [requestParams ~ {}]",
searchRequest, requestParams);
// 准备 DSL。
// query。
buildBasicQuery(requestParams, searchRequest);
// 分页。
Integer page = requestParams.getPage();
Integer size = requestParams.getSize();
searchRequest.source().from((page - 1) * size).size(size);
// 地理排序。
String location = requestParams.getLocation();
System.out.println("location = " + location);
if (location != null && !"".equals(location)) {
searchRequest.source().sort(
SortBuilders
.geoDistanceSort("location", new GeoPoint(location))
.order(SortOrder.ASC)
.unit(DistanceUnit.KILOMETERS)
);
}
// 发送请求。
SearchResponse searchResponse = null;
try {
searchResponse = this.restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
} catch (IOException e) {
throw new RuntimeException(e);
}
log.info(" ~ HotelServiceImpl ~ getList; ~ searchResponse ~ {}" + "\r\n ~ searchRequest ~ {}" + "\r\n ~ [requestParams ~ {}]",
searchResponse, JSONObject.toJSONString(searchRequest), requestParams);
System.out.println("searchResponse = " + JSONObject.toJSONString(searchResponse));
// 解析响应。
return handleResponse(searchResponse);
}
4.3.5. 排序距离显示。
重启服务后,测试我的酒店功能。
发现确实可以实现对我附近酒店的排序,不过并没有看到酒店到底距离我多远,这该怎么办?
排序完成后,页面还要获取我附近每个酒店的具体距离值,这个值在响应结果中是独立的。
因此,我们在结果解析阶段,除了解析 source 部分以外,还要得到 sort 部分,也就是排序的距离,然后放到响应结果中。
我们要做两件事。
-
修改 HotelDoc,添加排序距离字段,用于页面显示。
-
修改 HotelService 类中的 handleResponse 方法,添加对 sort 值的获取。
1)修改 HotelDoc 类,添加距离字段。
package com.geek.elasticsearchgeek.hotel.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* @author geek
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class HotelDoc implements Serializable {
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 方法。
private PageResult handleResponse(SearchResponse searchResponse) {
// 解析响应。
SearchHits searchHits = searchResponse.getHits();
// 获取总条数。
TotalHits totalHits = searchHits.getTotalHits();
System.out.println("totalHits = " + totalHits);
long total = totalHits.value;
System.out.println(" ~ handleResponseHighlight; ~ 共搜索到 " + total + " 条数据");
// 文档数组。
SearchHit[] hits = searchHits.getHits();
System.out.println("hits = " + Arrays.toString(hits));
// 遍历。
List<HotelDoc> hotelDocList = new ArrayList<>();
for (SearchHit hit : hits) {
// 获取文档 source。
String sourceAsString = hit.getSourceAsString();
System.out.println("sourceAsString = " + sourceAsString);
// 反序列化。
HotelDoc hotelDoc = JSON.parseObject(sourceAsString, HotelDoc.class);
System.out.println("hotelDoc = " + hotelDoc);
// 获取排序值。
Object[] sortValues = hit.getSortValues();
if (sortValues.length > 0) {
hotelDoc.setDistance(sortValues[0]);
}
boolean add = hotelDocList.add(hotelDoc);
System.out.println(" ~ add ~ " + add + "\r\n ~ hotelDoc = " + hotelDoc);
}
return new PageResult((long) hotelDocList.size(), hotelDocList);
}
重启后测试,发现页面能成功显示距离了。
4.4. 酒店竞价排名。
需求:让指定的酒店在搜索结果中排名置顶。
4.4.1. 需求分析。
要让指定酒店在搜索结果中排名置顶,效果如图。
页面会给指定的酒店添加广告标记。
那怎样才能让指定的酒店排名置顶呢?
我们之前学习过的 function_score 查询可以影响算分,算分高了,自然排名也就高了。而 function_score 包含3个要素。
-
过滤条件:哪些文档要加分。
-
算分函数:如何计算 function score。
-
加权方式:function score 与 query score 如何运算。
这里的需求是:让指定酒店排名靠前。因此我们需要给这些酒店添加一个标记,这样在过滤条件中就可以根据这个标记来判断,是否要提高算分。
比如,我们给酒店添加一个字段:isAD,Boolean 类型。
-
true:是广告。
-
false:不是广告。
这样 function_score 包含3个要素就很好确定了。
-
过滤条件:判断 isAD 是否为 true。
-
算分函数:我们可以用最简单暴力的 weight,固定加权值。
-
加权方式:可以用默认的相乘,大大提高算分。
因此,业务的实现步骤包括。
-
给 HotelDoc 类添加 advetise 字段,Boolean 类型。
-
挑选几个你喜欢的酒店,给它的文档数据添加 advetise 字段,值为 true。
-
改 search 方法,添加 function score 功能,给 advetise 值为 true 的酒店增加权重。
4.4.2. 修改 HotelDoc 实体。
给 com.geek.elasticsearchgeek.hotel.pojo
包下的 HotelDoc 类添加 bAdvertise 字段。
package com.geek.elasticsearchgeek.hotel.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* @author geek
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class HotelDoc implements Serializable {
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;
private Boolean bAdvertise ;
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();
}
}
4.4.3. 添加广告标记。
接下来,我们挑几个酒店,添加 bAdvertise 字段,设置为 true。
POST /hotel/_update/2048050570
{
"doc": {
"bAdvertise": true
}
}
4.4.4. 添加算分函数查询。
接下来我们就要修改查询条件了。之前是用的 boolean 查询,现在要改成 function_socre 查询。
function_score 查询结构如下。
对应的 JavaAPI 如下。
我们可以将之前写的 boolean 查询作为原始查询条件放到 query 中,接下来就是添加过滤条件、算分函数、加权模式了。所以原来的代码依然可以沿用。
修改 com.geek.elasticsearchgeek.hotel.service.impl
包下的 HotelService
类中的 buildBasicQuery
方法,添加算分函数查询。
private void buildBasicQuery(RequestParams requestParams, SearchRequest searchRequest) {
// 构建 BooleanQuery。
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
// 关键字搜索。
String key = requestParams.getKey();
if (key == null || "".equals(key)) {
boolQueryBuilder.must(QueryBuilders.matchAllQuery());
} else {
boolQueryBuilder.must(QueryBuilders.matchQuery("all", key));
}
// 条件过滤。
// 城市条件。
if (requestParams.getCity() != null && !"".equals(requestParams.getCity())) {
// filter 不参与算分。
boolQueryBuilder.filter(QueryBuilders.termQuery("city", requestParams.getCity()));
}
// 品牌条件。
if (requestParams.getBrand() != null && !"".equals(requestParams.getBrand())) {
boolQueryBuilder.filter(QueryBuilders.termQuery("brand", requestParams.getBrand()));
}
// 星级条件。
if (requestParams.getStarName() != null && !"".equals(requestParams.getStarName())) {
boolQueryBuilder.filter(QueryBuilders.termQuery("starName", requestParams.getStarName()));
}
// 价格。
if (requestParams.getMinPrice() != null && requestParams.getMaxPrice() != null) {
boolQueryBuilder.filter(QueryBuilders
.rangeQuery("price")
.gte(requestParams.getMinPrice())
.lte(requestParams.getMaxPrice())
);
}
// 算分控制。
FunctionScoreQueryBuilder functionScoreQueryBuilder =
QueryBuilders.functionScoreQuery(
// 原始查询,相关性算分的查询。
boolQueryBuilder,
// function score 的数组。
new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{
// 其中的一个 function score 元素。
new FunctionScoreQueryBuilder.FilterFunctionBuilder(
// 过滤条件。
QueryBuilders.termQuery("bAdvertise", true),
// 算分函数。
ScoreFunctionBuilders.weightFactorFunction(10)
)
});
// 放入 source。
searchRequest.source().query(functionScoreQueryBuilder);
}
分布式搜索引擎 03。
0. 学习目标。
1. 数据聚合。
聚合(aggregations) 可以让我们极其方便的实现对数据的统计、分析、运算。例如。
-
什么品牌的手机最受欢迎?
-
这些手机的平均价格、最高价格、最低价格?
-
这些手机每月的销售情况如何?
实现这些统计功能的比数据库的 sql 要方便的多,而且查询速度非常快,可以实现近实时搜索效果。
1.1. 聚合的种类。
聚合常见的有三类。
-
桶(Bucket) 聚合:用来对文档做分组。
- TermAggregation:按照文档字段值分组,例如按照品牌值分组、按照国家分组。
- Date Histogram:按照日期阶梯分组,例如一周为一组,或者一月为一组。
-
度量(Metric) 聚合:用以计算一些值,比如:最大值、最小值、平均值等。
- Avg:求平均值。
- Max:求最大值。
- Min:求最小值。
- Stats:同时求 max、min、avg、sum 等。
-
管道(pipeline) 聚合:其它聚合的结果为基础做聚合。
**注意。**参加聚合的字段必须是 keyword、日期、数值、布尔类型。
参与聚合的字段类型。
-
keyword。
-
数值。
-
日期。
-
布尔。
1.2. DSL 实现聚合。
现在,我们要统计所有数据中的酒店品牌有几种,其实就是按照品牌对数据分组。此时可以根据酒店品牌的名称做聚合,也就是 Bucket 聚合。
1.2.1. Bucket 聚合语法。
语法如下。
GET /hotel/_search
{
// 设置 size 为 0,结果中不包含文档,只包含聚合结果。
"size": 0,
// 定义聚合。
"aggs": {
// 给聚合起个名字。
"brandAgg": {
// 聚合的类型,按照品牌值聚合,所以选择 term。
"terms": {
// 参与聚合的字段。
"field": "brand",
// 希望获取的聚合结果数量。
"size": 20,
// 聚合结果排序方式。
"order": "asc"
}
}
}
}
结果如图。
1.2.2. 聚合结果排序。
默认情况下,Bucket 聚合会统计 Bucket 内的文档数量,记为 _count,并且按照 _count 降序排序。
我们可以指定 order 属性,自定义聚合的排序方式。
GET /hotel/_search
{
"size": 0,
"aggs": {
"brandAgg": {
"terms": {
"field": "brand",
// 按照_count 升序排列。
"order": {
"_count": "asc"
},
"size": 20
}
}
}
}
1.2.3. 限定聚合范围。
默认情况下,Bucket 聚合是对索引库的所有文档做聚合,但真实场景下,用户会输入搜索条件,因此聚合必须是对搜索结果聚合。那么聚合必须添加限定条件。
我们可以限定要聚合的文档范围,只要添加 query 条件即可。
GET /hotel/_search
{
"query": {
"range": {
"price": {
// 只对 200 元以下的文档聚合。
"lte": 200
}
}
},
"size": 0,
"aggs": {
"brandAgg": {
"terms": {
"field": "brand",
"size": 20
}
}
}
}
{
"took" : 1,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 17,
"relation" : "eq"
},
"max_score" : null,
"hits" : [ ]
},
"aggregations" : {
"brandAgg" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "如家",
"doc_count" : 13
},
{
"key" : "速8",
"doc_count" : 2
},
{
"key" : "7天酒店",
"doc_count" : 1
},
{
"key" : "汉庭",
"doc_count" : 1
}
]
}
}
}
这次,聚合得到的品牌明显变少了。
1.2.4.Metric 聚合语法。
上节课,我们对酒店按照品牌分组,形成了一个个桶。现在我们需要对桶内的酒店做运算,获取每个品牌的用户评分的 min、max、avg 等值。
这就要用到 Metric 聚合了,例如 stat 聚合:就可以获取 min、max、avg 等结果。
语法如下。
GET /hotel/_search
{
"size": 0,
"aggs": {
"brandAgg": {
"terms": {
"field": "brand",
"size": 20
},
// 是 brands 聚合的子聚合,也就是分组后对每组分别计算。
"aggs": {
// 聚合名称。
"score_stats": {
// 聚合类型,这里 stats 可以计算 min、max、avg 等。
"stats": {
// 聚合字段,这里是 score。
"field": "score"
}
}
}
}
}
}
这次的 score_stats 聚合是在 brandAgg 的聚合内部嵌套的子聚合。因为我们需要在每个桶分别计算。
另外,我们还可以给聚合结果做个排序,例如按照每个桶的酒店平均分做排序。
1.2.5. 小结。
aggs 代表聚合,与 query 同级,此时 query 的作用是?
- 限定聚合的的文档范围。
聚合必须的三要素。
-
聚合名称。
-
聚合类型。
-
聚合字段。
聚合可配置属性有。
-
size:指定聚合结果数量。
-
order:指定聚合结果排序方式。
-
field:指定聚合字段。
1.3. RestAPI 实现聚合。
1.3.1. API 语法。
聚合条件与 query 条件同级别,因此需要使用 request.source(); 来指定聚合条件。
package com.geek.elasticsearchgeek;
import org.apache.http.HttpHost;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import java.io.IOException;
/**
* @author geek
*/
@SpringBootTest
public class ElasticsearchAggregationTest {
private RestHighLevelClient restHighLevelClient;
@BeforeEach
void setUp() {
this.restHighLevelClient = new RestHighLevelClient(RestClient.builder(HttpHost.create("http://192.168.142.186:9200")));
}
@Test
void testElasticsearchAggregation() {
// 准备 Request。
SearchRequest searchRequest = new SearchRequest("hotel");
// 准备 DSL。
// 设置 size。
searchRequest.source().size(10);
SearchSourceBuilder searchSourceBuilder =
searchRequest.source()
.aggregation(
AggregationBuilders
.terms("brandAgg")
.field("brand")
.size(20)
);
System.out.println("searchRequest = " + searchRequest);
System.out.println("searchSourceBuilder = " + searchSourceBuilder);
// 发出请求。
SearchResponse searchResponse = null;
try {
searchResponse = this.restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
} catch (IOException e) {
throw new RuntimeException(e);
}
System.out.println("searchResponse = " + searchResponse);
// 解析结果。
}
@AfterEach
void tearDown() throws IOException {
this.restHighLevelClient.close();
}
}
聚合条件的语法。
聚合的结果也与查询结果不同,API 也比较特殊。不过同样是 JSON 逐层解析。
1.3.2. 业务需求。
需求:搜索页面的品牌、城市等信息不应该是在页面写死,而是通过聚合索引库中的酒店数据得来的。
分析。
目前,页面的城市列表、星级列表、品牌列表都是写死的,并不会随着搜索结果的变化而变化。但是用户搜索条件改变时,搜索结果会跟着变化。
例如:用户搜索“东方明珠”,那搜索的酒店肯定是在上海东方明珠附近,因此,城市只能是上海,此时城市列表中就不应该显示北京、深圳、杭州这些信息了。
也就是说,搜索结果中包含哪些城市,页面就应该列出哪些城市;搜索结果中包含哪些品牌,页面就应该列出哪些品牌。
如何得知搜索结果中包含哪些品牌?如何得知搜索结果中包含哪些城市?
使用聚合功能,利用 Bucket 聚合,对搜索结果中的文档基于品牌分组、基于城市分组,就能得知包含哪些品牌、哪些城市了。
因为是对搜索结果聚合,因此聚合是限定范围的聚合,也就是说聚合的限定条件跟搜索文档的条件一致。
查看浏览器可以发现,前端其实已经发出了这样的一个请求。
请求参数与搜索文档的参数完全一致。
返回值类型就是页面要展示的最终结果。
结果是一个 Map 结构。
-
key 是字符串,城市、星级、品牌、价格。
-
value 是集合,例如多个城市的名称。
1.3.3. 业务实现。
在 com.geek.elasticsearchgeek.hotel.controller
包的 HotelController
中添加一个方法,遵循下面的要求。
-
请求方式:
POST
。 -
请求路径:
/hotel/filters
。 -
请求参数:
RequestParams
,与搜索文档的参数一致。 -
返回值类型:
Map<String, List<String>>
。
代码。
package com.geek.elasticsearchgeek.hotel.controller;
import com.geek.elasticsearchgeek.hotel.dto.PageResult;
import com.geek.elasticsearchgeek.hotel.dto.RequestParams;
import com.geek.elasticsearchgeek.hotel.service.IHotelService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Map;
/**
* @author geek
*/
@Slf4j
@RestController
@RequestMapping("/hotel")
public class HotelController {
@Autowired
private IHotelService hotelService;
@RequestMapping("/list")
public PageResult getList(@RequestBody RequestParams requestParams) {
PageResult hotelServiceList = this.hotelService.getList(requestParams);
log.info(" ~ HotelController ~ getList; ~ hotelServiceList ~ {}" + "\r\n ~ [requestParams ~ {}]",
hotelServiceList, requestParams);
return hotelServiceList;
}
@RequestMapping("/filters")
public Map<String, List<String>> getFilters(@RequestBody RequestParams requestParams) {
Map<String, List<String>> map = this.hotelService.getFilters(requestParams);
log.info(" ~ HotelController ~ getFilters; ~ map ~ {}" + "\r\n ~ [requestParams ~ {}]",
map,requestParams);
return map;
}
}
这里调用了 IHotelService 中的 getFilters(); 方法,尚未实现。
在 com.geek.elasticsearchgeek.hotel.service.IHotelService
中定义新方法。
package com.geek.elasticsearchgeek.hotel.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.geek.elasticsearchgeek.hotel.dto.PageResult;
import com.geek.elasticsearchgeek.hotel.dto.RequestParams;
import com.geek.elasticsearchgeek.hotel.pojo.Hotel;
import java.util.List;
import java.util.Map;
/**
* @author geek
*/
public interface IHotelService extends IService<Hotel> {
/**
* 根据关键字搜索酒店信息。
*
* @param requestParams
* @return
*/
PageResult getList(RequestParams requestParams);
/**
* elasticsearch 聚合。
*
* @param requestParams
* @return
*/
Map<String, List<String>> getFilters(RequestParams requestParams);
}
在 com.geek.elasticsearchgeek.hotel.service.impl.HotelServiceImpl
中实现该方法。
/**
* @param requestParams
* @return
*/
@Override
public Map<String, List<String>> getFilters(RequestParams requestParams) {
// 准备 Request。
SearchRequest searchRequest = new SearchRequest("hotel");
// 准备 DSL。
// query。
buildBasicQuery(requestParams, searchRequest);
// 设置 size。
searchRequest.source().size(0);
// 聚合。
buildAggregation(searchRequest);
// 发出请求。
SearchResponse searchResponse = null;
try {
searchResponse = this.restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
} catch (IOException e) {
throw new RuntimeException(e);
}
log.info(" ~ HotelServiceImpl ~ getFilters; ~ searchResponse ~ {}" + "\r\n ~ searchRequest ~ {}" + "\r\n ~ [requestParams ~ {}]",
searchResponse, searchRequest, requestParams);
// 解析结果。
Map<String, List<String>> map = new HashMap<>();
Aggregations aggregations = searchResponse.getAggregations();
// 根据品牌名称,获取品牌结果。
List<String> brandList = getAggregationByName(aggregations, "brandAgg");
map.put("品牌", brandList);
// 根据品牌名称,获取品牌结果。
List<String> cityList = getAggregationByName(aggregations, "cityAgg");
map.put("城市", cityList);
// 根据品牌名称,获取品牌结果。
List<String> starList = getAggregationByName(aggregations, "starAgg");
map.put("星级", starList);
return map;
}
private void buildAggregation(SearchRequest searchRequest) {
searchRequest.source()
.aggregation(AggregationBuilders
.terms("brandAgg")
.field("brand")
.size(100)
);
searchRequest.source()
.aggregation(AggregationBuilders
.terms("cityAgg")
.field("city")
.size(100)
);
searchRequest.source()
.aggregation(AggregationBuilders
.terms("starAgg")
.field("starName")
.size(100)
);
}
private List<String> getAggregationByName(Aggregations aggregations, String aggName) {
// 根据聚合名称获取聚合结果。
Terms terms = aggregations.get(aggName);
// 获取 buckets。
List<? extends Terms.Bucket> termsBuckets = terms.getBuckets();
// 遍历。
List<String> list = new ArrayList<>();
for (Terms.Bucket bucket : termsBuckets) {
//获取 key。
String key = bucket.getKeyAsString();
list.add(key);
}
return list;
}