ElasticSearch 的概念解析以及使用方式

楔子

ElasticSearch(后续简称 ES)在企业中的使用可以说是非常广泛了,那么 ES 到底是什么呢?我们学习 ES 能做到哪些事情呢?下面来了解一下。

ES 是一款高性能的分布式搜索引擎,当然里面出现的高性能、分布式已经是见怪不怪了,因此我们的重点是在搜索引擎上面。提到搜索引擎肯定不陌生,像百度、谷歌,它们都提供了自己的搜索引擎,我们每天都会在上面查找各种各样的信息。因此:通过输入指定的关键字(关键词)来获取与之相关的信息,这个过程称之为搜索。并且搜索是不分场合的,除了百度、谷歌提供的搜索引擎之外,我们还可以在各种 app 上搜索,比如你在京东 app 上输入小提琴,那么点击确认之后会给你返回与小提琴有关的商品信息,这也是搜索;而支持搜索的工具便是搜索引擎,它负责根据用户输入的关键字匹配出与之相关的信息,然后返回给用户,所以搜索引擎就是支持用户搜索的一个工具。

而支持搜索的工具有很多种,其实说白了只要是支持字符串匹配的都可以,但能否满足不同的业务场景、以及保证高级别的搜索效率就两说了。

使用数据库做搜索会怎么样

显然数据库是支持搜索的,毕竟它是专门用来存储数据的,其中也包含了数据分析。比如数据库中有一张表负责存储商品信息,我要查询里面所有名字包含 "凉鞋" 的商品对应的 id,那么就可以这么做:

select product_id from product 
where product_name like '%凉鞋%';

很明显这么做是正确的,但是要拿数据库来做搜索引擎则是不合适的。因为由于业务场景的不同,会带来两个问题:

  • 假设一个商品的名称不是 "...凉鞋...",而是 "...凉拖鞋...",这个时候该商品就选不到了,但它也是需要被选出来的;再比如用户想搜索 "榨汁机",但是不小心输入成了 "榨汁鸡",这个时候也没办法搜索的。所以这种情况下,无法通过对关键词进行切分,来获取更多的结果。
  • 如果我们不是按商品名称、而是按商品描述进行搜索,这个时候还会产生效率上的问题。因为某些字段的内容会非常长,数千甚至上万个是字符也是常见的,这个时候要查询内部是否包含关键词所需要扫描的文本量就会非常大,并且该字段的每一行记录都需要扫描。如果一张表里面有千万条记录,那么这个耗时会非常恐怖。

因此用数据库实现搜索是不靠谱的,性能会非常差。

什么是全文检索和 Lucene

我们说数据库不适合专门用于搜索,那么我们应该选择谁呢?不用想,肯定是 ES,否则就没有必要介绍了。只不过在具体介绍 ES 之前,我们还需要做一些前戏,来聊一聊什么是全文检索。

首先全文检索(或者说全文搜索)也是一种搜索,只不过它和数据库中使用 like 不同,全文检索使用了倒排索引的技术。所以全文检索分为两步:

  • 索引创建:从数据中提取信息,建立倒排索引
  • 搜索索引:根据用户的查询去搜索索引,然后返回索引对应的结果

直接说的话不容易理解,我们举例说明,假设数据库中有一张表 game:

我想搜寻 type 中包含 "校园" 或者 "爱情" 的记录,这个时候显然需要全表扫描,假设库里面有一千万条记录,那么我们就需要扫描一千万次,且每次扫描的范围都是全部的字符。如果用户指定了多个关键词,还不能拆分、对单独的部分检索。

那么倒排索引是如何做到的呢?首先我们这里查询的是 type 字段,那么就对 type 字段的每一个文本进行拆分,得到多个关键词,然后再获取包含指定关键词的记录的 id。光说不好理解,我们直接基于上面的 type 字段建立倒排索引,看看结果如何吧。

通过对文本进行拆分,我们看到 type 字段包含 "亲情" 的有 id 为 1、2、3 的记录,包含 "夏日" 的有 id 为 2、3 的记录,所以每一个关键词都和包含该关键词的记录的 id 做了一个映射。然后搜索的时候,同样也会对关键词、或者说要搜索内容进行拆分,得到更多的关键词,然后去匹配。假设我们想要根据 "校园爱情" 进行查找,那么会拆分成 "校园" 和 "爱情",然后直接就能得到 1、3、4、5,根据 id 直接查找就可以了。

之前是逐行遍历去确定记录,现在是先根据关键词来确定 id,而构建的关键词到 id 之间的映射便是倒排索引。所以我们也可以发现,并不是说使用了 ES 之后就不需要数据库了,因为数据库表的字段可能非常多,我们不会对每一个字段都建立倒排索引。而是只针对那些需要通过关键词匹配的字段,将该字段的每一行都拆分成一个个的关键词,然后再把所有的关键词组合起来,建立它们到 id 之间的映射(倒排索引)。

因此在建立倒排索引后,行数反而会增多(如果大部分词都不一样的话),比如原来的数据有 100 万行,但是拆分出来的关键词有 200 万个,那么在建立倒排索引之后也会有 200 万行。但我们不可能真的搜索 200 万次,有可能我们搜索一次就能找到对应的 id 了,因为在倒排索引中匹配的是关键词。当然搜索一次是理想情况,也可能是十次、一百次,因此就需要设计一个好的搜索算法以及合适的数据组织结构来使得查询次数最小化,而算法如何设计显然不是我们需要操心的。并且在倒排索引中进行关键词匹配也和数据库的 like 不一样,前者只需要匹配单词即可,效率要比后者高很多。

以上便是全文检索以及倒排索引,还是很好理解的。然后再来说说 Lucene,其实 Lucene 就是一个 Jar 包,里面封装了很多建立倒排索引、以及搜索相关的算法。如果你使用 Java 语言的话,那么只需要引入这个 Jar、然后基于 Lucene 提供的 api 进行开发即可。通过 Lucene 我们就可以对已有的数据建立索引,Lucene 会在本地磁盘上面给我们组织数据的索引结构。

什么是 ElasticSearch

了解了上面的内容之后,再来了解 ES 就简单多了。我们说 Lucene 它封装了类似于搜索引擎的功能,但它是部署在单机上面的,如果我们的数据量非常大、需要多机存储的话该怎么办呢。首先我们能想到的是就把数据分散开存储在多机上,然后每台机器各有一个 Lucene。

上面的做法看似解决了数据量的问题,但其实背后还有很多缺陷。首先数据分散在多台机器上,这些数据要怎么切分?当我们在搜索的时候,如果数据存在多台机器上,那么是不是每台机器都需要访问?这么做是不是很麻烦呢?数据一旦分散在多台机器上,那么如何保证建立高性能的索引?以及数据的不丢失要如何保证,系统的高可用性要如何保证。因此任何框架,如果需要多机部署,那么之间就应该具备相互通信的功能,相互协调,彼此作为一个整体、像单机一样对外提供服务。

所以 ES 就应运而生,它是基于 Lucene 实现的一个搜索引擎,同样使用 Java 语言编写。但是通过 ES 可以让全文搜索变得更加简单,因为 Lucene 需要你有比较深的检索相关的知识,比较复杂,而 ES 将这种复杂隐藏了起来,让用户可以通过 RESTful API 进行查询。不仅如此,ES 不仅仅是为了检索方便而封装的 Lucene,它还解决了分布式的问题。因为 Lucene 只是一个库,如果想支持多机部署,那么你需要额外做很多的工作。而 ES 把这些全部解决了,比如:

  • ES 具有分布式的文件存储,每个字段都可以被索引、被搜索
  • 自动维护数据在多个节点之间的分布,以及索引的建立、搜索请求的执行
  • 自动维护数据的冗余副本,一个节点宕掉了,不会造成数据的丢失
  • 可以轻松的扩展到上百台服务器,处理 PB 级结构化或非结构化数据
  • 除了 Lucene 的检索功能,ES 还封装了更多的高级功能,比如聚合分析、基于地理位置的搜索等等,可以让我们快速的开发应用,如果要基于原生的 Lucene 实现是很困难的

因此 ES 的概念我们就说完了,说白了 ES 就是一个基于 Lucene 实现的搜索引擎,并且支持高可用、可伸缩、分布式。每个节点之上都部署一个 ES,多个节点共同对外提供服务,至于节点之间如何协调 ES 内部已经帮我们做好了,无需我们关心。

此外,虽然我们一直说 ES 是一个搜索引擎,但其实 ES 不仅可以用来搜索,还可以用来做数据分析。比如电商网站通过 ES 选取 "百褶裙" 销量最高的十个商家,新闻网站通过 ES 选取访问量最高的几篇文章等等,显然此时在获取数据的同时也伴随着数据分析。因此 ES 是一个分布式的搜索和数据分析引擎,能够进行全文检索、结构化检索、数据分析,以及对海量数据进行接近实时的处理。

当然相信很多人都听过 ELK,专门用来搭建日志分析平台,其中 E 就是我们这里的 ElasticSearch,L 是 Logstash,K 是 Kibana。Logstash 是用来做数据采集的,ElasticSearch 负责数据分析,Kibana 负责数据可视化,我们后面也会涉及到 ELK。

总结一下 ES 的特点:

  • 可以组成大型分布式集群,处理 PB 级数据,服务大公司;在可以运行在单机或者组成小型分布式集群,服务小公司。
  • ES 不是什么新技术,主要是将全文检索、数据分析以及分布式这些现有的技术合并在了一起,才形成了独一无二的 ES。
  • 对用户而言是开箱即用的,非常简单,作为中小型的应用,直接三分钟部署一下 ES 就可以作为生产环境的系统来用了。
  • 数据库的功能面对很多领域是不够用的,一些特殊的功能,像全文检索、同义词处理、相关度排名,复杂数据分析,海量数据的近实时处理等等,这些数据库是不支持的,而 ES 作为一个补充提供了数据库不具备的功能。

ElasticSearch 核心概念

1)Near Realtime(NRT):近实时,从写入数据到数据可以被搜索,整个过程所需要的时间非常短(大概 1 秒),并且基于 ES 执行搜索和分析同样可以达到秒级。

2)Cluster:集群,包含多个节点,当然也可以只包含一个节点。

3)Node:集群中的一个节点,每个节点都有一个名称(默认随机分配),节点的名称还是比较重要的,尤其是在执行运维管理操作的时候。

4)Index:索引,对应 MySQL 的数据库。

5)Type:类型,对应 MySQL 的表。

6)Document:文档,对应 MySQL 表中的一条记录,ES 的一个 Document 就类似于一条 JSON 数据。当然每条 JSON 数据可以有多个字段,然后字段在 ES 中被称为 Field,对应 MySQL 中的 Column。显然一个 Type 下的 Document,都有着相同的 Field。

7)shard:单台机器无法存储大量数据,ES 可以将一个索引中的数据切分为多个 shard,分布在多台服务器上存储。有了 shard 就可以横向扩展,存储更多的数据,让搜索和操作分布到多台服务器上去执行,提升吞吐性能。每个 shard 都是一个 Lucene Index,说白了就是 Index 的一个切片。

8)replica:任何一个服务器都有可能因为故障而宕机,造成 shard 丢失,因此可以为每一个 shard 创建多个 replica 副本。replica 可以在 shard 故障时提供备用服务,保证数据不丢失,此外多个 replica 还可以提升搜索操作的吞吐量和性能。默认情况下,每个 Index 会被分成 5 个 shard(建立索引时设置,设置之后不能修改),被称为 primary shard,每个 primary shard 默认会有一个 replica shard(可以随时修改)。简单说的话,每个 Index 会被分成 5 个 shard,每个 shard 会有一个 replica。当然啦,在 7.x 之前每个 Index 默认有 5 个 shard,但从 7.x 开始每个 Index 默认只有 1 个 shard。

因此在概念上,ES 和关系型数据库还是有一些共同之处的。

需要注意的是,随着 ES 的发展,Type 的概念逐渐在弱化,因为全文索引的目的是建立关键词到 id 的映射,所以 Type 和全文索引的概念是冲突的。在 ES 6.x 中,已经规定一个 index 下只能包含一个 Type,而到 ES 7.x 时,Type 的概念就被完全移除了。

下面来安装 ES,这里我采用的是阿里云服务器,操作系统是 CentOS 7。由于 ES 是基于 Java 语言编写的,所以在安装 ES 之前要先安装 Jdk,而 Jdk 的安装比较简单,这里我就不演示了,总之版本不低于 1.8 即可。

然后去 ES 官网下载相应的安装包,这里我下载的是最新版 8.11.3,然后上传到服务器,并解压到 /opt 目录中。

安装成功,然后看一下 ES 的主目录,是不是很熟悉呢。所有 Java 编写的大数据组件都是类似的,每个目录作用如下:

  • bin 目录放一些启动脚本、以及用于命令行操作的脚本;
  • config 目录放一些配置文件;
  • lib 目录存放程序依赖的 jar 包;
  • logs 目录负责存放日志文件;
  • modules 目录存放功能模块;
  • plugins 目录存放一些插件。

然后我们看到 ES 还内置了一个 JDK,所以即使当前的系统没有安装 Java 环境,也是没关系的。

下面我们启动 ES,不过启动之前需要修改一下配置文件 config/elasticsearch.yml。

# ES 默认只允许本机访问,将其修改为 0.0.0.0
network.host: 0.0.0.0
# 端口默认为 9200
http.port: 9200
# 是否需要用户名密码,这里改成 false
xpack.security.enabled: false
# 是否开启 SSL 认证,这里将 enabled 给改成 false
# 否则只允许 https 请求,而 http 请求会被拒绝
xpack.security.http.ssl:
  enabled: true
  keystore.path: certs/http.p12

然后再创建用户,因为 ES 要求不能以 root 用户启动,因此我们要创建一个用户,并赋予它相关权限。

# 创建一个组 es
groupadd es
# 创建一个用户 es,并关联到组 es 中
useradd es -g es
# 赋予它 ES 目录的操作权限
chown es:es /opt/elasticsearch-8.11.3/ -R

下面切换用户,进入 ES 目录中,输入 bin/elasticsearch 启动 ES。如果你配置了环境变量,那么直接输入 elasticsearch 就行。

但如果你启动时发现报了下面这个错,那么说明空间不足。

此时应该修改 config/jvm.options 配置文件。

# 设置 JVM 的初始内存为 1G,此值可以与 -Xmx 相同
# 避免每次垃圾回收完成后 JVM 重新分配内存
-Xms1g
# 设置 JVM 最大可用内存为 1G
-Xmx1g

然后再来启动 ES,默认是以前台启动的。但如果你发现输出一堆日志信息后,进程又退出了,并且最后输出了 ERROR: Elasticsearch exited unexpectedly, with exit code 78。那么你需要切换回 root 用户,然后执行如下命令:

sysctl -w vm.max_map_count=262144

然后再打开 /etc/security/limits.conf,并在里面追加如下内容。

es hard nofile 65536
es soft nofile 65536

这里的 es 就是刚才创建用户,如果你创建的用户不叫 es,那么记得修改。

完事之后,再切换回 es 用户,再次启动,会发现启动成功。然后我们测试一下,浏览器中输入 http://ip:9200 ,看看能否返回内容。

返回了一条 JSON,我们说 ES 中的 Document(文档)就类似于一条 JSON,其字段就是 Field。然后里面的 name 字段表示节点名称,cluster_name 表示集群名称,这些都可以通过配置文件 elasticsearch.yml 进行修改,至于其它字段就见名知意了。

到目前为止,整个 ES 算是启动成功了,但目前是前台启动,我们需要改成后台启动。

bin/elasticsearch -d

只需要在结尾加一个 -d 即可。

通过 HTTP 操作 ElasticSearch

ES 支持客户端通过标准的 HTTP 请求来访问数据,并且 API 遵循 Restful 风格。

索引操作

我们说过,ES 的索引就类似于 MySQL 的数据库,因此我们首先要创建索引。

向服务器发送 PUT 请求,http://ip:9200/girls ,便会创建一个名为 girls 的索引,同时服务器会返回如下内容。

{
    "acknowledged": true,
    "shards_acknowledged": true,
    "index": "girls"
}

acknowledged 表示响应结果,true 表示添加成功;shards_acknowledged 表示分片结果,true 表示分片成功;index 表示添加成功之后的索引名称。注意:索引的分片数量默认为 1,在 7.x 之前默认为 5。

如果我们添加了一个已存在的索引,那么会返回错误信息。

{
    "error": {
        "root_cause": [
            {
                "type": "resource_already_exists_exception",
                "reason": "index [girls/f9NmNv3ERL-0qY9GZbivcQ] already exists",
                "index_uuid": "f9NmNv3ERL-0qY9GZbivcQ",
                "index": "girls"
            }
        ],
        "type": "resource_already_exists_exception",
        "reason": "index [girls/f9NmNv3ERL-0qY9GZbivcQ] already exists",
        "index_uuid": "f9NmNv3ERL-0qY9GZbivcQ",
        "index": "girls"
    },
    "status": 400
}

错误信息非常全面,当然,如果你不确定要添加的索引是否存在,那么可以查询一下。通过发送 GET 请求 http://ip:9200/girls ,便可查询指定的索引是否存在。

{
    "girls": {
        "aliases": {},   
        "mappings": {},  
        "settings": {
            "index": { 
                "routing": {
                    "allocation": {
                        "include": {
                            "_tier_preference": "data_content"
                        }
                    }
                },
                "number_of_shards": "1",
                "provided_name": "girls",
                "creation_date": "1702639706945",
                "number_of_replicas": "1",
                "uuid": "f9NmNv3ERL-0qY9GZbivcQ",
                "version": {
                    "created": "8500003"
                }
            }
        }
    }
}

里面有几个关键字段,number_of_shards 表示分片数量,我们说过,ES 会将 Index 切分成多份,每一份叫做一个 shard,从 7.x 开始默认只有一个 shard。number_of_replicas 表示副本数量,每个 shard 可以配置多个副本,保证多可用,默认副本数为 1。creation_date 表示创建时间,uuid 表示索引的唯一表示。

shard 也被称为主分片,replica 也被称为副本分片,不过我个人还是习惯称它们为分片和副本。

但如果索引不存在呢?会返回什么呢?

{
    "error": {
        "root_cause": [
            {
                "type": "index_not_found_exception",
                "reason": "no such index [not_exists_index]",
                "resource.type": "index_or_alias",
                "resource.id": "not_exists_index",
                "index_uuid": "_na_",
                "index": "not_exists_index"
            }
        ],
        "type": "index_not_found_exception",
        "reason": "no such index [not_exists_index]",
        "resource.type": "index_or_alias",
        "resource.id": "not_exists_index",
        "index_uuid": "_na_",
        "index": "not_exists_index"
    },
    "status": 404
}

返回的依旧是包含错误信息的 JSON,里面的字段解释了错误的原因。

然后除了查询单个索引之外,也可以查询全部的索引,通过 GET http://ip:9200/_cat/indices 进行查看,里面的 _cat 表示查看,indices 表示索引,整体含义就是查看当前 ES 服务器的所有索引。

这里会以一个二维表的形式返回,第一行是字段,剩余行是具体的索引记录,当然我们这里只有一个索引。这些字段的含义如下:

  • health:当前服务器健康状态,green 表示集群完整,yellow 表示单点正常、集群不完整,red 表示单点不正常。
  • status:索引的状态,是打开还是关闭。
  • index:索引的名称。
  • uuid:索引的唯一编号。
  • pri:分片(shard)的数量。
  • rep:副本(replica)的数量。
  • docs.count:可用文档(Document)的数量。
  • docs.deleted:文档删除状态(逻辑删除)。
  • store.size:分片和副本整体占用的空间大小。
  • pri.store.size:分片占用的空间大小。
  • dataset.size:主分片占用的空间大小。

说完了添加索引和查询索引,再来看看如何删除索引。相信删除的过程不用我说你也知道,发一个 DELETE 请求即可。

{
    "acknowledged": true
}

返回的 JSON 里面只包含一个 acknowledged 字段,为 true 表示删除成功。如果删除一个不存在的索引,那么会报错,比如这里再次删除 girls。

{
    "error": {
        "root_cause": [
            {
                "type": "index_not_found_exception",
                "reason": "no such index [girls]",
                "resource.type": "index_or_alias",
                "resource.id": "girls",
                "index_uuid": "_na_",
                "index": "girls"
            }
        ],
        "type": "index_not_found_exception",
        "reason": "no such index [girls]",
        "resource.type": "index_or_alias",
        "resource.id": "girls",
        "index_uuid": "_na_",
        "index": "girls"
    },
    "status": 404
}

因此基于返回值我们很容易判断出,操作是否出现错误,以及错误原因是什么。

关于索引操作,再总结一下。

以上就是索引相关的操作,还是比较简单的。

文档操作

索引已经创建好了(刚才的 girls 索引已经删除了,我又重新创建了),接下来我们来创建文档,并添加数据。这里的文档可以类比为关系型数据库中的表数据,添加的数据格式为 JSON 格式。另外我们知道在关系型数据库中,必须先定义好表、指定好字段才可以使用,但在 ES 中则不需要。因为 ES 对字段的处理是非常灵活的,我们可以忽略某个字段,或者新增一个字段。

PUT http://123.57.183.166:9200/girls/_doc/satori

{
    "name": "古明地觉",
    "age": 17,
    "gender": "female",
    "address": "地灵殿"
}

添加文档是 PUT /index/_doc/document_id,注意 API,格式为 /索引/_doc/文档id。一个 document 就是一条 JSON 数据,每个 document 都有一个唯一的 id,这个 id 可以通过路径参数指定。返回响应如下:

{
    "_index": "girls",
    "_id": "satori",
    "_version": 1,
    "result": "created",
    "_shards": {
        "total": 2,
        "successful": 1,
        "failed": 0
    },
    "_seq_no": 0,
    "_primary_term": 1
}

解释一下里面的字段:

  • _index:标识文档位于哪个索引中
  • _id:每个文档都有一个唯一 id
  • _version:版本号,当对文档执行增删改操作时,该文档的版本号会自增 1
  • result:这里的 created 表示创建成功
  • _shards.total:分片的总数
  • _shards.successful:创建成功的分片数
  • _shards.failed:创建失败的分片数

到这里应该有人发现了,如果文档 id 每次都需要手动指定,未免有点麻烦。因此 ES 也可以帮我们生成文档 id,做法如下:

POST http://123.57.183.166:9200/girls/_doc

{
    "name": "芙兰朵露",
    "age": 400,
    "gender": "female",
    "address": "红魔馆"
}

这里我们没有指定文档 id,那么 ES 会自动为我们生成,返回响应如下。

{
    "_index": "girls",
    "_id": "FSVabowBlTaHOP7AMAey",
    "_version": 1,
    "result": "created",
    "_shards": {
        "total": 2,
        "successful": 1,
        "failed": 0
    },
    "_seq_no": 1,
    "_primary_term": 1
}

注意这里的请求,如果手动指定了文档 id,那么 POST 和 PUT 请求均可。但如果没有指定文档 id,那么只能使用 POST 请求。

目前我们已经在 girls 这个 index 下面创建了两个 document,它们的 id 分别是 "satori" 和 "9GMLbowB6tXz2L1KQfb5",那么便可通过如下方式进行查询。

  • GET /girls/_doc/satori
  • GET /girls/_doc/9GMLbowB6tXz2L1KQfb5

响应内容如下:

{
    "_index": "girls",
    "_id": "satori",
    "_version": 1,
    "_seq_no": 0,
    "_primary_term": 1,
    "found": true,
    "_source": {
        "name": "古明地觉",
        "age": 17,
        "gender": "female",
        "address": "地灵殿"
    }
}

{
    "_index": "girls",
    "_id": "FSVabowBlTaHOP7AMAey",
    "_version": 1,
    "_seq_no": 1,
    "_primary_term": 1,
    "found": true,
    "_source": {
        "name": "芙兰朵露",
        "age": 400,
        "gender": "female",
        "address": "红魔馆"
    }
}

里面的 _source 字段的值,就是我们添加的文档数据。

说完了添加和查询,再来看看修改,比如我想将文档 id 为 'satori' 的文档中的 name 字段的值,改为 '古明地恋',要怎么做呢?

POST /girls/_doc/satori

{
    "name": "古明地恋",
    "age": 17,
    "gender": "female",
    "address": "地灵殿"
}

非常简单,直接改即可。就像新建文档一样,输入相同的 URL,会将旧的文档数据替换掉,并且版本号会自增 1。ES 服务器返回数据如下:

{
    "_index": "girls",
    "_id": "satori",
    "_version": 2,
    "result": "updated",
    "_shards": {
        "total": 2,
        "successful": 1,
        "failed": 0
    },
    "_seq_no": 2,
    "_primary_term": 1
}

我们看到 _version 从初始的 1 变成了 2,并且此时的 result 不再是 created,而是 updated,表示更新。并且在更新的过程中,字段数量可以和之前不一样,也就是说我们可以新增字段、删除字段,非常灵活。

并且由于指定了文档 id,所以 POST 请求和 PUT 均可。这里我们将另一个文档也给改了,并调整一下字段。

PUT /girls/_doc/FSVabowBlTaHOP7AMAey

{
    "name": "芙兰朵露·斯卡雷特",
    "年龄": 400,
    "gender": "female",
    "address": "东方红魔馆"
}

ES 服务器返回数据如下:

{
    "_index": "girls",
    "_id": "FSVabowBlTaHOP7AMAey",
    "_version": 2,
    "result": "updated",
    "_shards": {
        "total": 2,
        "successful": 1,
        "failed": 0
    },
    "_seq_no": 3,
    "_primary_term": 1
}

注意:修改数据必须要指定文档 id,如果不指定的话,那么会生成一个不重复的新的文档 id,此时就是新增文档。如果想修改文档,那么必须手动指定它的 id,从而实现替换。然后再看一下里面的 _seq_no 字段,你可以认为 ES 内部有一个全局的计数器,只要有文档发生增删改,这个计数器就会自增 1,而 _seq_no 保存了当前时刻的全局计数器的值。

我们查询一下,看看 ES 服务端返回的数据。

{
    "_index": "girls",
    "_id": "satori",
    "_version": 2,
    "_seq_no": 2,
    "_primary_term": 1,
    "found": true,
    "_source": {
        "name": "古明地恋",
        "age": 17,
        "gender": "female",
        "address": "地灵殿"
    }
}

{
    "_index": "girls",
    "_id": "FSVabowBlTaHOP7AMAey",
    "_version": 2,
    "_seq_no": 3,
    "_primary_term": 1,
    "found": true,
    "_source": {
        "name": "芙兰朵露·斯卡雷特",
        "年龄": 400,
        "gender": "female",
        "address": "东方红魔馆"
    }
}

数据发生了变化,并且此时字段可以自由调整,如果是关系型数据库,那么表的字段是不能发生变化的。所以在 ES 中,Type 的概念已经不存在了,因为它和全局索引的设计理念是冲突的,不过 Index、Document、Field 还是可以和关系型数据库的 Database、Row、Column 对应的。

说完了,查询、添加、修改,最后来看看删除,删除很简单,直接通过 DELETE 请求即可,DELETE /girls/_doc/satori,返回数据如下:

{
    "_index": "girls",
    "_id": "satori",
    "_version": 3,
    "result": "deleted",
    "_shards": {
        "total": 2,
        "successful": 1,
        "failed": 0
    },
    "_seq_no": 4,
    "_primary_term": 1
}

因为涉及到增删改,所以当前文档的 _version 自增 1,并且 result 为 deleted,表示删除。如果文档不存在,那么 result 字段的值就是 not_found。注意:文档被删除后,不会立即从磁盘上移除,只是被标记成已删除(逻辑删除)。如果我们查询一个已被删除(或者不存在)的文档,会返回如下信息:

{
    "_index": "girls",
    "_id": "satori",
    "found": false
}

所以在获取文档时,可以先通过 found 字段判断是否存在,如果存在,再通过 _source 字段拿到具体的文档记录。

我们总结一下,文档操作的 API。

在添加文档时,如果在路径参数中没有指定文档 id,即 /index/_doc,那么必须用 POST 请求,此时 ES 会自动为新增的文档生成一个不重复的 id。如果添加文档时,在路径参数中指定了文档 id,即 /index/_doc/document_id,那么可以用 PUT 也可以用 POST,并且当指定的文档 id 已存在时,还能实现修改的效果。所以文档的新增和修改实际上用的是同一套接口,至于到底是新增还是修改,就看到返回的响应中 result 字段的值是 "created" 还是 "updated"。

查询文档时,API 为 GET /index/_doc/document_id,然后通过返回响应中的 found 字段,可以判断文档是否存在。如果是 true,那么代表该文档存在,否则不存在。

最后是删除,API 为 DELETE /index/_doc/document_id,如果要删除文档存在,那么返回响应中的 result 字段为 "deleted",否则为 "not_found"。事实上在大部分情况下,我们并不关心删除的文档是否存在,直接删就完事了,反正文档不存在时啥事也没有。正如添加文档时,很多时候我们也不关心文档是否存在,如果存在,那么替换掉就完事了。

同时查询和删除多个文档

我们上面的文档操作,针对的都是单个文档,如果想同时查询和删除多个文档,要怎么做呢?这里我提前生成好了几个文档。

通过 POST /index/_search 即可查询指定索引下的全部文档,在 ES 中,将具有特殊意义的路径参数都带上了一个下划线作为前缀,用于区分。比如这里的 _search,就表示返回 Index 下所有的 document。

{
    "took": 4,
    "timed_out": false,
    "_shards": {
        "total": 1,
        "successful": 1,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": {
            "value": 4,
            "relation": "eq"
        },
        "max_score": 1.0,
        "hits": [
            {
                "_index": "girls",
                "_id": "g1",
                "_score": 1.0,
                "_source": {
                    "name": "g1",
                    "salary": 9000
                }
            },
            {
                "_index": "girls",
                "_id": "g2",
                "_score": 1.0,
                "_source": {
                    "name": "g2",
                    "salary": 9000
                }
            },
            {
                "_index": "girls",
                "_id": "g3",
                "_score": 1.0,
                "_source": {
                    "name": "g3",
                    "salary": 9000
                }
            },
            {
                "_index": "girls",
                "_id": "g4",
                "_score": 1.0,
                "_source": {
                    "name": "g4",
                    "salary": 10000
                }
            }
        ]
    }
}

当然,有时我们也不一定获取全部文档,特别是当 Index 下的文档非常多的时候,会很耗时。更多的情况是,我们在筛选文档时会给出一个过滤条件,比如我要获取 salary 等于 9000 的文档。

POST /girls/_search

{
    "query": {
        "match": {
            "salary": 9000
        }
    }
}

此时就会返回 salary 字段等于 9000 的文档,响应如下:

{
    "took": 16,
    "timed_out": false,
    "_shards": {
        "total": 1,
        "successful": 1,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": {
            "value": 3,
            "relation": "eq"
        },
        "max_score": 1.0,
        "hits": [
            {
                "_index": "girls",
                "_id": "g1",
                "_score": 1.0,
                "_source": {
                    "name": "g1",
                    "salary": 9000
                }
            },
            {
                "_index": "girls",
                "_id": "g2",
                "_score": 1.0,
                "_source": {
                    "name": "g2",
                    "salary": 9000
                }
            },
            {
                "_index": "girls",
                "_id": "g3",
                "_score": 1.0,
                "_source": {
                    "name": "g3",
                    "salary": 9000
                }
            }
        ]
    }
}

如果想删除多个文档也很简单:

POST /girls/_delete_by_query

{
    "query": {
        "match": {
            "salary": 9000
        }
    }
}

表示删除内部的 salary 字段等于 9000 的文档。

目前出现的 API 有点多,我们总结一下:

索引和单个文档的操作没什么难度,我们需要重点关注的是文档的批量操作,也就是像关系型数据库的 WHERE 语句那样,指定查询条件,筛选出满足条件的文档。而查询条件,可以通过传递一个 JSON 来指定,它同时适用于批量查询和批量删除,所以下面我们重点来看看这个查询条件怎么写。

posted @ 2019-11-21 11:25  古明地盆  阅读(2096)  评论(0编辑  收藏  举报