elasticsearch ~ 从入门到入坑。

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 的概念做一下对比。

MySQLElasticsearch说明
TableIndex索引(index),就是文档的集合,类似数据库的表(table)。
RowDocument文档(Document),就是一条条的数据,类似数据库中的行(Row),文档都是 JSON 格式。
ColumnField字段(Field),就是 JSON 文档中的字段,类似数据库中的列(Column)。
SchemaMappingMapping(映射)是索引中文档的约束,例如字段类型约束。类似数据库的表结构(Schema)。
SQLDSLDSL 是 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 的数据搜索功能。我们会分别使用 DSLRestClient 实现搜索。



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=inTF(词条频率)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)=inlog(n+0.51+(Nn+0.5)) fi+ki(1b+b(avgdldl)fi

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

在这里插入图片描述
小结:elasticsearch 会根据词条和文档的相关度做打分,算法有两种。

  • TF-IDF 算法 ~ 在 elasticsearch 5.0 之前。会随着词频增加而越来越大。

  • BM25 算法 ~ elasticsearch 5.1 版本后采用的算法。会随着词频增加而增大,但增长曲线会趋于水平。



1.5.2. 算分函数查询 ~ function score query。

根据相关度打分是比较合理的需求,但合理的不一定是产品经理需要的

以百度为例,你搜索的结果中,并不是相关度越高排名越靠前,而是谁掏的钱多排名就越靠前。如图。

在这里插入图片描述
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-umL1fubK-1680272802425)(assets/image-20210721191144560.png)]
要想认为控制相关性算分,就需要利用 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. 小结。

查询的基本步骤是。

  1. 创建 SearchRequest 对象。

  2. 准备 Request.source(),也就是 DSL。

    ① QueryBuilders 来构建查询条件。

    ② 传入 Request.source() 的 query() 方法。

  3. 发送请求,得到结果。

  4. 解析结果(参考 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.implHotelServiceImplgetList(); 方法中,添加一个排序功能。

完整代码。


    @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;
    }



elasticsearch 拼音分词器 & 自动补全。

《elasticsearch 拼音分词器 & 自动补全。》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

lyfGeek

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值