Elasticsearch简介

简介篇

Elasticsearch简介_第1张图片

全文搜索引擎是什么

全文搜索引擎是目前广泛应用的主流搜索引擎。它的工作原理是计算机索引程序通过扫描文章中的每一个词,对每一个词建立一个索引,指明该词在文章中出现的次数和位置,当用户查询时,检索程序就根据事先建立的索引进行查找,并将查找的结果反馈给用户的检索方式。这个过程类似于通过字典中的检索字表查字的过程。 从定义中我们已经可以大致了解全文检索的思路了,为了更详细的说明,我们先从生活中的数据说起。

我们生活中的数据总体分为两种:结构化数据非结构化数据

  • 结构化数据: 指具有固定格式或有限长度的数据,如数据库,元数据等。
  • 非结构化数据: 非结构化数据又可称为全文数据,指不定长或无固定格式的数据,如邮件,word文档等。

当然有的地方还会有第三种:半结构化数据,如XML,HTML等,当根据需要可按结构化数据来处理,也可抽取出纯文本按非结构化数据来处理。

根据两种数据分类,搜索也相应的分为两种:结构化数据搜索和非结构化数据搜索。

对于结构化数据,我们一般都是可以通过关系型数据库(mysql,oracle等)的 table 的方式存储和搜索,也可以建立索引。 对于非结构化数据,也即对全文数据的搜索主要有两种方法:顺序扫描法全文检索

顺序扫描:通过文字名称也可了解到它的大概搜索方式,即按照顺序扫描的方式查询特定的关键字。 例如给你一张报纸,让你找到该报纸中“RNG”的文字在哪些地方出现过。你肯定需要从头到尾把报纸阅读扫描一遍然后标记出关键字在哪些版块出现过以及它的出现位置。

这种方式无疑是最耗时的最低效的,如果报纸排版字体小,而且版块较多甚至有多份报纸,等你扫描完你的眼睛也差不多了。

全文搜索:对非结构化数据顺序扫描很慢,我们是否可以进行优化?把我们的非结构化数据想办法弄得有一定结构不就行了吗?将非结构化数据中的一部分信息提取出来,重新组织,使其变得有一定结构,然后对此有一定结构的数据进行搜索,从而达到搜索相对较快的目的。这种方式就构成了全文检索的基本思路。这部分从非结构化数据中提取出的然后重新组织的信息,我们称之索引

还以读报纸为例,我们想关注最近英雄联盟S8全球总决赛的新闻,假如都是 RNG 的粉丝,如何快速找到 RNG 新闻的报纸和版块呢?全文搜索的方式就是,将所有报纸中所有版块中关键字进行提取,如"EDG","RNG","FW","战队","英雄联盟"等。然后对这些关键字建立索引,通过索引我们就可以对应到该关键词出现的报纸和版块。注意区别目录搜索引擎。

为什么要用全文搜索引擎

之前,有同事问我,为什么要用搜索引擎?我们的所有数据在数据库里面都有,而且 Oracle、SQL Server 等数据库里也能提供查询检索或者聚类分析功能,直接通过数据库查询不就可以了吗?确实,我们大部分的查询功能都可以通过数据库查询获得,如果查询效率低下,还可以通过建数据库索引,优化SQL等方式进行提升效率,甚至通过引入缓存来加快数据的返回速度。如果数据量更大,就可以分库分表来分担查询压力。

那为什么还要全文搜索引擎呢?我们主要从以下几个原因分析:

  • 数据类型 全文索引搜索支持非结构化数据的搜索,可以更好地快速搜索大量存在的任何单词或单词组的非结构化文本。 例如 Google,百度类的网站搜索,它们都是根据网页中的关键字生成索引,我们在搜索的时候输入关键字,它们会将该关键字即索引匹配到的所有网页返回;还有常见的项目中应用日志的搜索等等。对于这些非结构化的数据文本,关系型数据库搜索不是能很好的支持。
  • 索引的维护 一般传统数据库,全文检索都实现的很鸡肋,因为一般也没人用数据库存文本字段。进行全文检索需要扫描整个表,如果数据量大的话即使对SQL的语法优化,也收效甚微。建立了索引,但是维护起来也很麻烦,对于 insert 和 update 操作都会重新构建索引。

什么时候使用全文搜索引擎:

  • 搜索的数据对象是大量的非结构化的文本数据。
  • 文件记录量达到数十万或数百万个甚至更多。
  • 支持大量基于交互式文本的查询。
  • 需求非常灵活的全文搜索查询。
  • 对高度相关的搜索结果的有特殊需求,但是没有可用的关系数据库可以满足。
  • 对不同记录类型、非文本数据操作或安全事务处理的需求相对较少的情况。

Elasticsearch是什么

Elasticsearch是一个高度可扩展的、开源的、基于 Lucene 的全文搜索和分析引擎。它允许您快速,近实时地存储,搜索和分析大量数据,并支持多租户。 Elasticsearch也使用Java开发并使用 Lucene 作为其核心来实现所有索引和搜索的功能,但是它的目的是通过简单的 RESTful API 来隐藏 Lucene 的复杂性,从而让全文搜索变得简单。

不过,Elasticsearch 不仅仅是 Lucene 和全文搜索,我们还能这样去描述它:

  • 分布式的实时文件存储,每个字段都被索引并可被搜索
  • 分布式的实时分析搜索引擎
  • 可以扩展到上百台服务器,处理PB级结构化或非结构化数据

而且,所有的这些功能被集成到一个服务里面,你的应用可以通过简单的RESTful API、各种语言的客户端甚至命令行与之交互。

我们能用Elasticsearch做什么

数字、文本、地理位置、结构化数据、非结构化数据。适用于所有数据类型。全文本搜索只是全球众多公司利用 Elasticsearch 解决各种挑战的冰山一角。查看直接依托 Elastic Stack 所构建解决方案的完整列表。

Elasticsearch简介_第2张图片

版本问题

Elasticsearch 目前有三个常用的稳定的主版本:2.x,5.x,6.x(排除 0.x 和 1.x)公司目前的Elasticsearch版本是 6.7.1,后续的演示都会基于此版本

Lucene/Solr/ElasticSearch

现在主流的搜索引擎大概就是:Lucene,Solr,ElasticSearch。

Elasticsearch简介_第3张图片

它们的索引建立都是根据倒排索引的方式生成索引。

Lucene

Lucene是一个Java全文搜索引擎,完全用Java编写。Lucene不是一个完整的应用程序,而是一个代码库和API,可以很容易地用于向应用程序添加搜索功能。

Lucene通过简单的API提供强大的功能:

可扩展的高性能索引

  • 在现代硬件上超过150GB /小时
  • 小RAM要求 - 只有1MB堆
  • 增量索引与批量索引一样快
  • 索引大小约为索引文本大小的20-30%

强大,准确,高效的搜索算法

  • 排名搜索 - 首先返回最佳结果
  • 许多强大的查询类型:短语查询,通配符查询,邻近查询,范围查询等
  • 现场搜索(例如标题,作者,内容)
  • 按任何字段排序
  • 使用合并结果进行多索引搜索
  • 允许同时更新和搜索
  • 灵活的分面,突出显示,连接和结果分组
  • 快速,内存效率和错误容忍的建议
  • 可插拔排名模型,包括矢量空间模型和Okapi BM25
  • 可配置存储引擎(编解码器)

跨平台解决方案

  • 作为Apache许可下的开源软件提供 ,允许您在商业和开源程序中使用Lucene
  • 100%-pure Java
  • 可用的其他编程语言中的实现是索引兼容的

Apache软件基金会 在Apache软件基金会提供的开源软件项目的Apache社区的支持。

但是Lucene只是一个框架,要充分利用它的功能,需要使用JAVA,并且在程序中集成Lucene。需要很多的学习了解,才能明白它是如何运行的,熟练运用Lucene确实非常复杂。

Solr

Apache Solr是一个基于名为Lucene的Java库构建的开源搜索平台。它以用户友好的方式提供Apache Lucene的搜索功能。作为一个行业参与者近十年,它是一个成熟的产品,拥有强大而广泛的用户社区。它提供分布式索引,复制,负载平衡查询以及自动故障转移和恢复。如果它被正确部署然后管理得好,它就能够成为一个高度可靠,可扩展且容错的搜索引擎。很多互联网巨头,如Netflix,eBay,Instagram和亚马逊(CloudSearch)都使用Solr,因为它能够索引和搜索多个站点。

主要功能列表包括:

  • 全文搜索
  • 突出
  • 分面搜索
  • 实时索引
  • 动态群集
  • 数据库集成
  • NoSQL功能和丰富的文档处理(例如Word和PDF文件)

ElasticSearch

Elasticsearch是一个开源(Apache 2许可证),是一个基于Apache Lucene库构建的RESTful搜索引擎。

Elasticsearch是在Solr之后几年推出的。它提供了一个分布式,多租户能力的全文搜索引擎,具有HTTP Web界面(REST)和无架构JSON文档。Elasticsearch的官方客户端库提供Java,Groovy,PHP,Ruby,Perl,Python,.NET和Javascript。

分布式搜索引擎包括可以划分为分片的索引,并且每个分片可以具有多个副本。每个Elasticsearch节点都可以有一个或多个分片,其引擎也可以充当协调器,将操作委派给正确的分片。

Elasticsearch可通过近实时搜索进行扩展。其主要功能之一是多​​租户。

主要功能列表包括:

  • 分布式搜索
  • 多租户
  • 分析搜索
  • 分组和聚合

Elasticsearch vs Solr

由于Lucene的复杂性,一般很少会考虑它作为搜索的第一选择,排除一些公司需要自研搜索框架,底层需要依赖Lucene。所以这里我们重点分析 Elasticsearch 和 Solr。

Elasticsearch vs. Solr。哪一个更好?他们有什么不同?你应该使用哪一个?

Elasticsearch简介_第4张图片

历史比较

Apache Solr是一个成熟的项目,拥有庞大而活跃的开发和用户社区,以及Apache品牌。Solr于2006年首次发布到开源,长期以来一直占据着搜索引擎领域,并且是任何需要搜索功能的人的首选引擎。它的成熟转化为丰富的功能,而不仅仅是简单的文本索引和搜索; 如分面,分组,强大的过滤,可插入的文档处理,可插入的搜索链组件,语言检测等。

Solr 在搜索领域占据了多年的主导地位。然后,在2010年左右,Elasticsearch成为市场上的另一种选择。那时候,它远没有Solr那么稳定,没有Solr的功能深度,没有思想分享,品牌等等。

Elasticsearch虽然很年轻,但它也自己的一些优势,Elasticsearch 建立在更现代的原则上,针对更现代的用例,并且是为了更容易处理大型索引和高查询率而构建的。此外,由于它太年轻,没有社区可以合作,它可以自由地向前推进,而不需要与其他人(用户或开发人员)达成任何共识或合作,向后兼容,或任何其他更成熟的软件通常必须处理。

因此,它在Solr之前就公开了一些非常受欢迎的功能(例如,接近实时搜索,英文:Near Real-Time Search)。从技术上讲,NRT搜索的能力确实来自Lucene,它是 Solr 和 Elasticsearch 使用的基础搜索库。具有讽刺意味的是,因为 Elasticsearch 首先公开了NRT搜索,所以人们将NRT搜索与Elasticsearch 联系在一起,尽管 Solr 和 Lucene 都是同一个 Apache 项目的一部分,因此,人们会首先期望 Solr 具有如此高要求的功能。

特征差异比较

这两个搜索引擎都是流行的,先进的的开源搜索引擎。它们都是围绕核心底层搜索库 - Lucene构建的 - 但它们又是不同的。像所有东西一样,每个都有其优点和缺点,根据您的需求和期望,每个都可能更好或更差。Solr和Elasticsearch都在快速发展,所以,话不多说,先来看下它们的差异清单:

特征 Solr/SolrCloud Elasticsearch
社区和开发者 Apache 软件基金和社区支持 单一商业实体及其员工
节点发现 Apache Zookeeper,在大量项目中成熟且经过实战测试 Zen内置于Elasticsearch本身,需要专用的主节点才能进行分裂脑保护
碎片放置 本质上是静态,需要手动工作来迁移分片,从Solr 7开始 - Autoscaling API允许一些动态操作 动态,可以根据群集状态按需移动分片
高速缓存 全局,每个段更改无效 每段,更适合动态更改数据
分析引擎性能 非常适合精确计算的静态数据 结果的准确性取决于数据放置
全文搜索功能 基于Lucene的语言分析,多建议,拼写检查,丰富的高亮显示支持 基于Lucene的语言分析,单一建议API实现,高亮显示重新计算
DevOps支持 尚未完全,但即将到来 非常好的API
非平面数据处理 嵌套文档和父-子支持 嵌套和对象类型的自然支持允许几乎无限的嵌套和父-子支持
查询DSL JSON(有限),XML(有限)或URL参数 JSON
索引/收集领导控制 领导者安置控制和领导者重新平衡甚至可以节点上的负载 不可能
机器学习 内置 - 在流聚合之上,专注于逻辑回归和学习排名贡献模块 商业功能,专注于异常和异常值以及时间序列数据

这里了解更多。

综合比较

另外,我们在从以下几个方面来分析下:

  • 近几年的流行趋势 我们查看一下这两种产品的Google搜索趋势。谷歌趋势表明,与 Solr 相比,Elasticsearch具有很大的吸引力,但这并不意味着Apache Solr已经死亡。虽然有些人可能不这么认为,但Solr仍然是最受欢迎的搜索引擎之一,拥有强大的社区和开源支持。

Elasticsearch简介_第5张图片

  • 安装和配置 与Solr相比,Elasticsearch易于安装且非常轻巧。此外,您可以在几分钟内安装并运行Elasticsearch。 但是,如果Elasticsearch管理不当,这种易于部署和使用可能会成为一个问题。基于JSON的配置很简单,但如果要为文件中的每个配置指定注释,那么它不适合您。 总的来说,如果您的应用使用的是JSON,那么Elasticsearch是一个更好的选择。否则,请使用Solr,因为它的schema.xml和solrconfig.xml都有很好的文档记录。
  • 社区 Solr拥有更大,更成熟的用户,开发者和贡献者社区。ES虽拥有的规模较小但活跃的用户社区以及不断增长的贡献者社区。 Solr是真正的开源社区代码。任何人都可以为Solr做出贡献,并且根据优点选出新的Solr开发人员(也称为提交者)。Elasticsearch在技术上是开源的,但在精神上却不那么重要。任何人都可以看到来源,任何人都可以更改它并提供贡献,但只有Elasticsearch的员工才能真正对Elasticsearch进行更改。 Solr贡献者和提交者来自许多不同的组织,而Elasticsearch提交者来自单个公司。
  • 成熟度 Solr更成熟,但ES增长迅速,我认为它稳定。
  • 文档 Solr在这里得分很高。它是一个非常有据可查的产品,具有清晰的示例和API用例场景。 Elasticsearch的文档组织良好,但它缺乏好的示例和清晰的配置说明。

总结

那么,到底是Solr还是Elasticsearch? 有时很难找到明确的答案。无论您选择Solr还是Elasticsearch,首先需要了解正确的用例和未来需求。总结他们的每个属性。

记住:

  • 由于易于使用,Elasticsearch在新开发者中更受欢迎。但是,如果您已经习惯了与Solr合作,请继续使用它,因为迁移到Elasticsearch没有特定的优势。
  • 如果除了搜索文本之外还需要它来处理分析查询,Elasticsearch是更好的选择。
  • 如果需要分布式索引,则需要选择Elasticsearch。对于需要良好可伸缩性和性能的云和分布式环境,Elasticsearch是更好的选择。
  • 两者都有良好的商业支持(咨询,生产支持,整合等)
  • 两者都有很好的操作工具,尽管Elasticsearch因其易于使用的API而更多地吸引了DevOps人群,因此可以围绕它创建一个更加生动的工具生态系统。
  • Elasticsearch在开源日志管理用例中占据主导地位,许多组织在Elasticsearch中索引它们的日志以使其可搜索。虽然Solr现在也可以用于此目的,但它只是错过了这一想法。
  • Solr仍然更加面向文本搜索。另一方面,Elasticsearch 通常用于过滤和分组 - 分析查询工作负载 - 而不一定是文本搜索。Elasticsearch 开发人员在 Lucene 和 Elasticsearch 级别上投入了大量精力使此类查询更高效(降低内存占用和CPU使用)。因此,对于不仅需要进行文本搜索,而且需要复杂的搜索时间聚合的应用程序,Elasticsearch是一个更好的选择。
  • Elasticsearch更容易上手,一个下载和一个命令就可以启动一切。Solr传统上需要更多的工作和知识,但Solr最近在消除这一点上取得了巨大的进步,现在只需努力改变它的声誉。
  • 在性能方面,它们大致相同。我说“大致”,因为没有人做过全面和无偏见的基准测试。对于95%的用例,任何一种选择在性能方面都会很好,剩下的5%需要用它们的特定数据和特定的访问模式来测试这两种解决方案。
  • 从操作上讲,Elasticsearch使用起来比较简单 - 它只有一个进程。Solr在其类似Elasticsearch的完全分布式部署模式SolrCloud中依赖于Apache ZooKeeper。ZooKeeper是超级成熟,超级广泛使用等等,但它仍然是另一个活跃的部分。也就是说,如果您使用的是Hadoop,HBase,Spark,Kafka或其他一些较新的分布式软件,您可能已经在组织的某个地方运行ZooKeeper。
  • 虽然Elasticsearch内置了类似ZooKeeper的组件Xen,但ZooKeeper可以更好地防止有时在Elasticsearch集群中出现的可怕的裂脑问题。公平地说,Elasticsearch开发人员已经意识到这个问题,并致力于改进Elasticsearch的这个方面。
  • 如果您喜欢监控和指标,那么使用Elasticsearch,您将会进入天堂。这个东西比新年前夜在时代广场可以挤压的人有更多的指标!Solr暴露了关键指标,但远不及Elasticsearch那么多。

核心原理篇

Elasticsearch是一个近乎实时(NRT)的搜索平台。这意味着从索引文档到可搜索文档的时间有一点延迟(通常是一秒)。通常有集群,节点,分片,副本等概念。

基础术语/概念

当我们开始使用 Elasticsearch 时,我们必须理解其中的一些重要的概念。这些概念的理解对于以后我们使用 Elastic 栈是非常重要的。在今天的这篇文章里,我们先来介绍一下在 Elastic 栈中最重要的一些概念。

首先,我们来看下一下如下的这个图:

Elasticsearch简介_第6张图片

cluster

Cluster 也就是集群的意思。Elasticsearch 集群由一个或多个节点组成,可通过其集群名称进行标识。通常这个 Cluster 的名字是可以在 Elasticsearch 里的配置文件中设置的。在默认的情况下,如我们的 Elasticsearch 已经开始运行,那么它会自动生成一个叫做 “elasticsearch” 的集群。我们可以在 config/elasticsearch.yml 里定制我们的集群的名字:

Elasticsearch简介_第7张图片

一个 Elasticsearch 的集群就像是下面的一个布局:

Elasticsearch简介_第8张图片

带有 NGINX 代理及 Balancer 的架构图是这样的:

Elasticsearch简介_第9张图片

我们可以通过:

GET _cluster/state

来获取整个 cluster 的状态。这个状态只能被 master node 所改变。上面的接口返回的结果是:

    {
      "cluster_name": "elasticsearch",
      "compressed_size_in_bytes": 1920,
      "version": 10,
      "state_uuid": "rPiUZXbURICvkPl8GxQXUA",
      "master_node": "O4cNlHDuTyWdDhq7vhJE7g",
      "blocks": {},
      "nodes": {...},
      "metadata": {...},
      "routing_table": {...},
      "routing_nodes": {...},
      "snapshots": {...},
      "restore": {...},
      "snapshot_deletions": {...}
    }
复制代码

node

单个 Elasticsearch 实例。 在大多数环境中,每个节点都在单独的盒子或虚拟机上运行。一个集群由一个或多个 node 组成。在测试的环境中,我可以把多个 node 运行在一个 server 上。在实际的部署中,大多数情况还是需要一个 server 上运行一个 node。

根据 node 的作用,可以分为如下的几种:

  • master-eligible:可以作为主 node。一旦成为主 node,它可以管理整个 cluster 的设置及变化:创建,更新,删除 index;添加或删除 node;为 node 分配 shard
  • data:数据 node
  • ingest: 数据接入(比如 pipepline)
  • machine learning (Gold/Platinum License)

一般来说,一个 node 可以具有上面的一种或几种功能。我们可以在命令行或者 Elasticsearch 的配置文件(Elasticsearch.yml)来定义.

标题

| Node类型 | 配置参数 | 默认值 | | ---- | ---- | ---- | ---- | | master-eligible | node.master | true | | data | node.data | true | | ingest | node.ingest | true | | machine learning | node.ml | true |

你也可以让一个 node 做专有的功能及角色。如果上面 node 配置参数没有任何配置,那么我们可以认为这个 node 是作为一个 coordination node。在这种情况下,它可以接受外部的请求,并转发到相应的节点来处理。针对 master node,有时我们需要设置 cluster.remote.connect: false。

在实际的使用中,我们可以把请求发送给 data 节点,而不能发送给 master 节点。

我们可以通过对 config/elasticsearch.yml 文件中配置来定义一个 node 在集群中的角色:

Elasticsearch简介_第10张图片

在有些情况中,我们可以通过设置 node.voting_only 为 true 从而使得一个 node 在 node.master 为真的情况下,只作为参加 voting 的功能,而不当选为 master node。这种情况为了避免脑裂情况发生。它通常可以使用一个 CPU 性能较低的 node 来担当。

在一个集群中,我们可以使用如下的一个命令来获取当前可以进行 vote 的所有 master-eligible 节点:

GET /_cluster/state?filter_path=metadata.cluster_coordination.last_committed_config

你可能获得类似如下列表的结果:

    {
      "metadata" : {
        "cluster_coordination" : {
          "last_committed_config" : [
            "Xe6KFUYCTA6AWRpbw84qaQ",
            "OvD79L1lQme1hi06Ouiu7Q",
            "e6KF9L1lQUYbw84CTAemQl"
          ]
        }
      }
    }
复制代码

在整个 Elastic 的架构中,Data Node 和 Cluster 的关系表述如下:

Elasticsearch简介_第11张图片

上面的定义适用于 Elastic Stack 7.9 发布版以前。在 Elastic Stack 7.9 之后,有了新的改进。

建议

集群中设置3台以上的节点作为master节点,这些节点只负责成为主节点,维护整个集群的状态。

再根据数据量设置一批data节点,这些节点只负责存储数据,后期提供建立索引和查询索引的服务,这样的话如果用户请求比较频繁,这些节点的压力也会比较大。

所以在集群中建议再设置一批client节点,这些节点只负责处理用户请求,实现请求转发,负载均衡等功能。

  • master节点:普通服务器即可(CPU 内存 消耗一般)
  • data节点:主要消耗磁盘,内存
  • coordination节点:普通服务器即可(如果要进行分组聚合操作的话,建议这个节点内存也分配多一点)

节点和节点之间通过Zen Discovery模块实现自动发现功能,这个后面会详细介绍到。

document

Elasticsearch 是面向文档的,这意味着你索引或搜索的最小数据单元是文档。文档在 Elasticsearch 中有一些重要的属性:

  • 它是独立的。文档包含字段(名称)及其值。
  • 它可以是分层的。可以将其视为文档中的文档。字段的值可以很简单,就像位置字段的值可以是字符串一样。它还可以包含其他字段和值。例如,位置字段可能包含城市和街道地址。
  • 结构灵活。你的文档不依赖于预定义的架构。例如,并非所有事件都需要描述值,因此可以完全省略该字段。但它可能需要新的字段,例如位置的纬度和经度。

文档通常是数据的 JSON 表示形式。JSON over HTTP 是与 Elasticsearch 进行通信的最广泛使用的方式,它是我们在本书中使用的方法。例如,你的聚会网站中的事件可以在以下文档中表示:

            {
              "name": "Elasticsearch Denver",
              "organizer": "Lee",
              "location": "Denver, Colorado, USA"
            }
复制代码

很多人认为 document 相比较于关系数据库,它相应于其中每个 record。

Elasticsearch简介_第12张图片

type

类型是文档的逻辑容器,类似于表是行的容器。 你将具有不同结构(模式)的文档放在不同类型中。 例如,你可以使用一种类型来定义聚合组,并在人们聚集时为事件定义另一种类型。 每种类型的字段定义称为映射。 例如,name 将映射为字符串,但 location 下的 geolocation 字段将映射为特殊的 geo_point 类型。 每种字段的处理方式都不同。 例如,你在名称字段中搜索单词,然后按位置搜索组以查找位于你居住地附近的组。

很多人认为 Elasticsearch 是 schema-less 的。大家都甚至认为 Elasticsearch 中的数据库是不需要 mapping 的。其实这是一个错误的概念。schema-less 在 Elasticsearch 中正确的理解是,我们不需要事先定义一个类型关系数据库中的 table 才使用数据库。在 Elasticsearch 中,我们可以不开始定义一个 mapping,而直接写入到我们指定的 index 中。这个 index 的 mapping 是动态生成的 (当然我们也可以禁止这种行为)。其中的数据项的每一个数据类型是动态识别的。比如时间,字符串等,虽然有些的数据类型还是需要我们手动调整,比如 geo_point 等地理位置数据。另外,它还有一个含义,同一个 type,我们在以后的数据输入中,可能增加新的数据项,从而生产新的 mapping。这个也是动态调整的。

Elasticsearch 具有 schema-less 的能力,这意味着无需显式指定如何处理文档中可能出现的每个不同字段即可对文档建立索引。 启用动态映射后,Elasticsearch 自动检测并向索引添加新字段。 这种默认行为使索引和浏览数据变得容易-只需开始建立索引文档,Elasticsearch 就会检测布尔值,浮点数和整数值,日期和字符串并将其映射到适当的 Elasticsearch 数据类型。

由于一些原因,在 Elasticsearch 6.0 以后,一个 Index 只能含有一个 type。这其中的原因是:相同 index 的不同映射 type 中具有相同名称的字段是相同; 在 Elasticsearch 索引中,不同映射 type 中具有相同名称的字段在 Lucene 中被同一个字段支持。在默认的情况下是 _doc。在未来8.0的版本中,type 将被彻底删除。

index

在 Elasticsearch 中,索引是文档的集合。

Elasticsearch简介_第13张图片

 每个 Index 一个或许多的 documents 组成,并且这些 document 可以分布于不同的 shard 之中。

Elasticsearch简介_第14张图片

很多人认为 index 类似于关系数据库中的 database。这中说法是有些道理,但是并不完全相同。其中很重要的一个原因是,在Elasticsearch 中的文档可以有 object 及 nested 结构。一个 index 是一个逻辑命名空间,它映射到一个或多个主分片,并且可以具有零个或多个副本分片。

每当一个文档进来后,根据文档的 id 会自动进行 hash 计算,并存放于计算出来的 shard 实例中,这样的结果可以使得所有的 shard 都比较有均衡的存储,而不至于有的 shard 很忙。

shard_num = hash(_routing) % num_primary_shards

在默认的情况下,上面的 _routing 既是文档的 _id。如果有 routing 的参与,那么这些文档可能只存放于一个特定的 shard,这样的好处是对于一些情况,我们可以很快地综合我们所需要的结果而不需要跨 node 去得到请求。比如针对 join 的数据类型。

从上面的公式我们也可以看出来,我们的 shard 数目是不可以动态修改的,否则之后也找不到相应的 shard 号码了。必须指出的是,replica 的数目是可以动态修改的。

shard

由于 Elasticsearch 是一个分布式搜索引擎,因此索引通常会拆分为分布在多个节点上的称为分片的元素。 Elasticsearch 自动管理这些分片的排列。 它还根据需要重新平衡分片,因此用户无需担心细节。

一个索引可以存储超出单个结点硬件限制的大量数据。比如,一个具有10亿文档的索引占据1TB的磁盘空间,而任一节点都没有这样大的磁盘空间;或者单个节点处理搜索请求,响应太慢。

为了解决这个问题,Elasticsearch 提供了将索引划分成多份的能力,这些份就叫做分片(shard)。当你创建一个索引的时候,你可以指定你想要的分片(shard)的数量。每个分片本身也是一个功能完善并且独立的“索引”,这个“索引”可以被放置到集群中的任何节点上。 分片之所以重要,主要有两方面的原因:

  • 允许你水平分割/扩展你的内容容量
  • 允许你在分片(潜在地,位于多个节点上)之上进行分布式的、并行的操作,进而提高性能/吞吐量

有两种类型的分片:primary shard 和 replica shard。

  • Primary shard: 每个文档都存储在一个Primary shard。 索引文档时,它首先在 Primary shard上编制索引,然后在此分片的所有副本上(replica)编制索引。索引可以包含一个或多个主分片。 此数字确定索引相对于索引数据大小的可伸缩性。 创建索引后,无法更改索引中的主分片数。

  • Replica shard: 每个主分片可以具有零个或多个副本。 副本是主分片的副本,有两个目的:

    1. 增加故障转移:如果主要故障,可以将副本分片提升为主分片。即使你失去了一个 node,那么副本分片还是拥有所有的数据
    2. 提高性能:get 和 search 请求可以由主 shard 或副本 shard 处理。

默认情况下,每个主分片都有一个副本,但可以在现有索引上动态更改副本数。我们可以通过如下的方法来动态修改副本数:

    PUT my_index/_settings
    {
        "number_of_replicas": 2
    }
复制代码

永远不要在与其主分片相同的节点上启动副本分片。在最新的 Elasticsearch 集群设计中,我们可以使用 auto_expand_replica 这个配置来让 Elasticsearch 自动决定有多少个 replica。当我们有一个节点时,通过这个设置,我们可能会得到 0 个 replica 从而保证整个集群的健康状态。

通常一个 shard 可以存储许多文档。在实际的使用中,增加副本 shard 的数量,可以提高搜索的速度,这是因为更多的 shard 可以帮我们进行同时进行搜索。但是副本 shard 数量的增多,也会影响数据写入的速度。在很多的情况下,在大批量数据写入的时候,我们甚至可以把 replica 的数量设置为 0。详细阅读可以参考文章 “Elasticsearch:如何提高 Elasticsearch 数据摄入速度”。增加 primary shard 的数量可以提高数据的写入速度,这是因为有更多的 shard 可以帮我们同时写入数据。可能有很多的开发者认为是不是 shard 的数量越多越好啊?oversharding 是 Elasticsearch 用户经常会遇到的一个问题。许多小的 shard 会消耗很多的资源,这是因为每一个 shard 其实对应的是一个 Lucene 的 index。一个 shard 通常可以存储几十个 G 的数据。如果你需要更多的 shard,你可以创建更多的索引从而使得它容易扩展,比如针对一些时序数据,我们可以为它们每天或者每个星期创建新的索引使用 Split API 来时一个大的索引增加 shard 的数量。一个 shard 的性能会随着它的大下而改变:

Elasticsearch简介_第15张图片

如上图所示,我们建议 50G 为索引的大小以求得最好的性能。在我们实际的 Beats 的使用中,默认的 ILM 索引大小就是 50G。

下面的图表示的是一个 index 有5个 shard 及1个 replica

Elasticsearch简介_第16张图片

 这些 shard 分布于不同的物理机器上:

Elasticsearch简介_第17张图片

我们可以为每个 index 设置相应的 shard 数值:

    curl -XPUT http://localhost:9200/another_user?pretty -H 'Content-Type: application/json' -d 
    {
        "settings" : {
            "index.number_of_shards" : 2,
            "index.number_of_replicas" : 1
        }
    }
复制代码

比如在上面的 REST 接口中,我们为 another_user 这个 index 设置了2个 shards,并且有一个 replica。一旦设置好 primary shard 的数量,我们就不可以修改了。这是因为 Elasticsearch 会依据每个 document 的 id 及 primary shard 的数量来把相应的 document 分配到相应的 shard 中。如果这个数量以后修改的话,那么每次搜索的时候,可能会找不到相应的 shard。

我们可以通过如下的接口来查看我们的 index 中的设置:

curl -XGET http://localhost:9200/twitter/_settings?pretty

上面我们可以得到 twitter index 的设置信息:

    {
      "twitter" : {
        "settings" : {
          "index" : {
            "creation_date" : "1565618906830",
            "number_of_shards" : "1",
            "number_of_replicas" : "1",
            "uuid" : "rwgT8ppWR3aiXKsMHaSx-w",
            "version" : {
              "created" : "7030099"
            },
            "provided_name" : "twitter"
          }
        }
      }
    }
复制代码

replica

默认情况下,Elasticsearch 为每个索引创建一个主分片和一个副本。这意味着每个索引将包含一个主分片,每个分片将具有一个副本。

分配多个分片和副本是分布式搜索功能设计的本质,提供高可用性和快速访问索引中的文档。主副本和副本分片之间的主要区别在于只有主分片可以接受索引请求。副本和主分片都可以提供查询请求。

在上图中,我们有一个 Elasticsearch 集群,由默认分片配置中的两个节点组成。 Elasticsearch 自动排列分割在两个节点上的一个主分片。有一个副本分片对应于每个主分片,但这些副本分片的排列与主分片的排列完全不同。

请允许我们澄清一下:请记住,number_of_shards 值与索引有关,而不是与整个群集有关。此值指定每个索引的分片数(不是群集中的主分片总数)。

我们可以通过如下的接口来获得一个 index 的健康情况:

http://localhost:9200/_cat/indices/twitter

上面的接口可以返回如下的信息:

Elasticsearch简介_第18张图片

更进一步的查询,我们可以看出:

如果一个 index 显示的是红色,表面这个 index 至少有一个 primary shard 没有被正确分配,并且有的 shard 及其相应的 replica 已经不能正常访问。 如果是绿色,表明 index 的每一个 shard 都有备份 (replica),并且其备份也成功复制在相应的 replica shard 之中。如果其中的一个 node 坏了,相应的另外一个 node 的 replica 将起作用,从而不会造成数据的丢失。

shard 健康

红色:集群中未分配至少一个主分片
黄色:已分配所有主副本,但未分配至少一个副本
绿色:分配所有分片
复制代码

小结

我们假设有一个集群由三个节点组成(Node1 , Node2 , Node3)。 它有两个主分片(P0 , P1),每个主分片有两个副本分片(R0 , R1)。相同分片的副本不会放在同一节点,所以我们的集群看起来如下图所示 “有三个节点和一个索引的集群”。

Elasticsearch简介_第19张图片

类似于关系型数据库:数据库集群,假如有个用户表,我担心数据量过大,我新建了多个用户表(即 Shard),将用户信息数据切分成多份,然后根据某种规则分到这些用户表中,我又担心某个表会出现异常造成数据丢失,我又将每个表分别备份了一次(即 Replica )。

副本是乘法,越多越浪费,但也越保险。分片是除法,分片越多,单分片数据就越少也越分散。

另外,我们可以画一个对比图来类比传统关系型数据库:

  • 关系型数据库 -> Databases(库) -> Tables(表) -> Rows(行) -> Columns(列)。
  • Elasticsearch -> Indeces(索引) -> Types(类型) -> Documents(文档) -> Fields(属性)。

Elasticsearch集群可以包含多个索引(indices)(数据库),每一个索引可以包含多个类型 (Types)(表),每一个类型包含多个文档(documents)(行),然后每个文档包含多个字段(Fields)(列)。

虽然这么类比,但是毕竟是两个差异化的产品,而且上面也说过在以后的版本中类型 (Types) 可能会被删除,所以一般我们创建索引都是一个种类对应一个索引。生鲜就创建商品的索引,生活用品就创建生活用品的索引,而不会说创建一个商品的索引,里面既包含生鲜的类型,又包含生活用品的类型。

Zen Discovery

ES目前主要推荐的自动发现机制,有如下几种:

  1. Azure classic discovery 插件方式,多播
  2. EC2 discovery 插件方式,多播
  3. Google Compute Engine (GCE) discovery 插件方式,多播
  4. Zen discovery 默认实现,多播/单播

Zen discovery是内建的、默认的、用于Elasticsearch的发现模块。它提供了单播和基于文件的发现,可以通过插件扩展到支持云环境和其他形式的发现。

Zen Discovery 是与其他模块集成的,例如,节点之间的所有通信都使用 transport模块完成。某个节点通过 发现机制 找到其他节点是使用 Ping 的方式实现的。

发现机制 负责发现集群中的节点,以及选择主节点。每次集群状态发生更改时,集群中的其他节点都会知道状态(具体方式取决于使用的是哪一种发现机制)。

这里额外介绍下单播,多播,广播的定义和区别,方便我们更好的理解发现机制。

单播,多播,广播的区别:

  • 单播(unicast) :网络节点之间的通信就好像是人们之间的对话一样。如果一个人对另外一个人说话,那么用网络技术的术语来描述就是“单播”,此时信息的接收和传递只在两个节点之间进行。例如,你在收发电子邮件、浏览网页时,必须与邮件服务器、Web服务器建立连接,此时使用的就是单播数据传输方式。
  • 多播(multicast) :“多播”也可以称为“组播”,多播”可以理解为一个人向多个人(但不是在场的所有人)说话,这样能够提高通话的效率。因为如果采用单播方式,逐个节点传输,有多少个目标节点,就会有多少次传送过程,这种方式显然效率极低,是不可取的。如果你要通知特定的某些人同一件事情,但是又不想让其他人知道,使用电话一个一个地通知就非常麻烦。多播方式,既可以实现一次传送所有目标节点的数据,也可以达到只对特定对象传送数据的目的。多播在网络技术的应用并不是很多,网上视频会议、网上视频点播特别适合采用多播方式。
  • 广播(broadcast) :可以理解为一个人通过广播喇叭对在场的全体说话,这样做的好处是通话效率高,信息一下子就可以传递到全体,广播是不区分目标、全部发送的方式,一次可以传送完数据,但是不区分特定数据接收对象。

上面列举的发现机制中, Zen Discovery 是 ES 默认内建发现机制。它提供单播多播的发现方式,并且可以扩展为通过插件支持云环境和其他形式的发现。所以我们接下来重点介绍下 Zen Discovery是如何在Elasticsearch中使用的。

集群是由相同cluster.name的节点组成的。

  • 在同一台机器上启动了第二个节点时,只要它和第一个节点有同样的 cluster.name 配置,它就会自动发现集群并加入到其中。
  • 在不同机器上启动节点的时候,为了加入到同一集群,你需要配置一个可连接到的单播主机列表。

单播主机列表通过discovery.zen.ping.unicast.hosts来配置。这个配置在 elasticsearch.yml 文件中:

discovery.zen.ping.unicast.hosts: ["host1", "host2:port"]
复制代码

具体的值是一个主机数组或逗号分隔的字符串。每个值应采用host:porthost的形式(其中port默认为设置transport.profiles.default.port,如果未设置则返回transport.tcp.port)。请注意,必须将IPv6主机置于括号内。此设置的默认值为127.0.0.1,[:: 1]

Elasticsearch 官方推荐我们使用 **单播 **代替 组播。而且 Elasticsearch 默认被配置为使用 单播 发现,以防止节点无意中加入集群。只有在同一台机器上运行的节点才会自动组成集群。

虽然 组播 仍然作为插件提供, 但它应该永远不被使用在生产环境了,否则你得到的结果就是一个节点意外的加入到了你的生产环境,仅仅是因为他们收到了一个错误的 组播 信号。对于 组播 本身并没有错,组播会导致一些愚蠢的问题,并且导致集群变的脆弱(比如,一个网络工程师正在捣鼓网络,而没有告诉你,你会发现所有的节点突然发现不了对方了)。

使用单播,你可以为 Elasticsearch 提供一些它应该去尝试连接的节点列表。当一个节点联系到单播列表中的成员时,它就会得到整个集群所有节点的状态,然后它会联系 master 节点,并加入集群。

这意味着你的单播列表不需要包含你的集群中的所有节点,它只是需要足够的节点,当一个新节点联系上其中一个并且说上话就可以了。如果你使用 master 候选节点作为单播列表,你只要列出三个就可以了。

选举主节点

作为 ping 过程的一部分,一个集群的主节点需要是被选举或者加入进来的(即选举主节点也会执行ping,其他的操作也会执行ping)。这个过程是自动执行的。通过配置discovery.zen.ping_timeout来控制节点加入某个集群或者开始选举的响应时间(默认3s)。

在这段时间内有3个 ping 会发出。如果超时,重新启动 ping 程序。在网络缓慢时,3秒时间可能不够,这种情况下,需要慎重增加超时时间,增加超时时间会减慢选举进程。

一旦节点决定加入一个存在的集群,它会发出一个加入请求给主节点,这个请求的超时时间由discovery.zen.join_time控制,默认是 ping 超时时间(discovery.zen.ping_timeout)的20倍。

当主节点停止或者出现问题,集群中的节点会重新 ping 并选举一个新节点。有时一个节点也许会错误的认为主节点已死,所以这种 ping 操作也可以作为部分网络故障的保护性措施。在这种情况下,节点将只从其他节点监听有关当前活动主节点的信息。

如果discovery.zen.master_election.ignore_non_master_pings设置为true时(默认值为false),node.masterfalse的节点不参加主节点的选举,同时选票也不包含这种节点。

通过设置node.masterfalse,可以将节点设置为非备选主节点,永远没有机会成为主节点。

discovery.zen.minimum_master_nodes设置了最少有多少个备选主节点参加选举,同时也设置了一个主节点需要控制最少多少个备选主节点才能继续保持主节点身份。如果控制的备选主节点少于discovery.zen.minimum_master_nodes个,那么当前主节点下台,重新开始选举。

discovery.zen.minimum_master_nodes必须设置一个恰当的备选主节点值(quonum,一般设置 为备选主节点数/2+1),尽量避免只有两个备选主节点,因为两个备选主节点quonum应该为2,那么如果一个节点出现问题,另一个节点的同意人数最多只能为1,永远也不能选举出新的主节点,这时就发生了脑裂现象。

脑裂

如果集群中选举出多个Master节点,使得数据更新时出现不一致,这种现象称之为脑裂。简而言之集群中不同的节点对于 Master的选择出现了分歧,出现了多个Master竞争。

一般而言脑裂问题可能有以下几个原因造成:

  • 网络问题: 集群间的网络延迟导致一些节点访问不到Master,认为Master 挂掉了,而master其实并没有宕机,而选举出了新的Master,并对Master上的分片和副本标红,分配新的主分片。
  • 节点负载: 主节点的角色既为Master又为Data,访问量较大时可能会导致 ES 停止响应(假死状态)造成大面积延迟,此时其他节点得不到主节点的响应认为主节点挂掉了,会重新选取主节点。
  • 内存回收: 主节点的角色既为Master又为Data,当Data节点上的ES进程占用的内存较大,引发JVM的大规模内存回收,造成ES进程失去响应。

如何避免脑裂:我们可以基于上述原因,做出优化措施:

  • 适当调大响应超时时间,减少误判。通过参数 discovery.zen.ping_timeout 设置节点ping超时时间,默认为 3s,可以适当调大。
  • 选举触发,我们需要在候选节点的配置文件中设置参数 discovery.zen.munimum_master_nodes 的值。这个参数表示在选举主节点时需要参与选举的候选主节点的节点数,默认值是 1,官方建议取值(master_eligibel_nodes/2)+1,其中 master_eligibel_nodes 为候选主节点的个数。这样做既能防止脑裂现象的发生,也能最大限度地提升集群的高可用性,因为只要不少于 discovery.zen.munimum_master_nodes 个候选节点存活,选举工作就能正常进行。当小于这个值的时候,无法触发选举行为,集群无法使用,不会造成分片混乱的情况。
  • 角色分离,即是上面我们提到的候选主节点和数据节点进行角色分离,这样可以减轻主节点的负担,防止主节点的假死状态发生,减少对主节点宕机的误判。

No master block

对于一个可以正常充分运作的集群来说,必须拥有一个活着的主节点和正常数量(discovery.zen.minimum_master_nodes个)活跃的备选主节点。discovery.zen.no_master_block设置了没有主节点时限制的操作。它又两个可选参数

  • all:所有操作均不可做,读写、包括集群状态的读写api,例如获得索引配置(index settings),putMapping,和集群状态(cluster state)api
  • write:默认为write,写操作被拒绝执行,基于最后一次已知的正常的集群状态可读,这也许会读取到已过时的数据。

discovery.zen.no_master_block,对于节点相关的基本api,这个参数是无效的,如集群统计信息(cluster stats),节点信息(node info),节点统计信息(node stats)。对这些api的请求不会被阻止,并且可以在任何可用节点上运行。

集群故障检测

有两个故障检测进程在集群的生命周期中一直运行。一个是主节点的,ping集群中所有的其他节点,检查他们是否活着。另一种是每个节点都ping主节点,确认主节点是否仍在运行或者是否需要重新启动选举程序。

使用discovery.zen.fd前缀设置来控制故障检测过程,配置如下:

配置 描述
discovery.zen.fd.ping_interval 节点多久ping一次,默认1s
discovery.zen.fd.ping_timeout 等待响应时间,默认30s
discovery.zen.fd.ping_retries 失败或超时后重试的次数,默认3

集群状态更新

主节点是唯一一个能够更新集群状态的节点。主节点一次处理一个群集状态更新,应用所需的更改并将更新的群集状态发布到群集中的所有其他节点。当其他节点接收到状态时,先确认收到消息,但是不应用最新状态。如果主节点在规定时间(discovery.zen.commit_timeout ,默认30s)内没有收到大多数节点(discovery.zen.minimum_master_nodes)的确认,集群状态更新不被通过。

一旦足够的节点响应了更新的消息,新的集群状态(cluster state)被提交并且会发送一条消息给所有的节点。这些节点开始在内部应用新的集群状态。在继续处理队列中的下一个更新之前,主节点等待所有节点响应,直到超时(discovery.zen.publish_timeout,默认设置为30秒)。上述两个超时设置都可以通过集群更新设置api动态更改。

读写流程

创建索引的时候,我们通过Mapping 映射定义好索引的基本结构信息,接下来我们肯定需要往 ES 里面新增业务文档数据了,例如用户,日志等业务数据。新增的业务数据,我们根据 Mapping 来生成对应的倒排索引信息 。

我们一直说,Elasticsearch是一个基于Apache Lucene 的开源搜索引擎。Elasticsearch的搜索高效的原因并不是像Redis那样重依赖内存的,而是通过建立特殊的索引数据结构--倒排索引实现的。由于它的使用场景:处理PB级结构化或非结构化数据,数据量大且需要持久化防止断电丢失,所以 Elasticsearch 的数据和索引存储是依赖于服务器的硬盘。这也是为什么我们在ES性能调优的时候可以将使用SSD硬盘存储作为其中一个优化项来考虑。

倒排索引的概念,我相信大家都已经知道了,这里就不在赘述,倒排索引可以说是Elasticsearch搜索高效和支持非结构化数据检索的主要原因了,但是倒排索引被写入磁盘后是不可改变 的:它永远不会修改

段和提交点

Elasticsearch简介_第20张图片

索引文档以段的形式存储在磁盘上,何为?索引文件被拆分为多个子文件,则每个子文件叫作, 每一个段本身都是一个倒排索引,并且段具有不变性,一旦索引的数据被写入硬盘,就不可再修改。在底层采用了分段的存储模式,使它在读写时几乎完全避免了锁的出现,大大提升了读写性能。

段被写入到磁盘后会生成一个提交点,提交点是一个用来记录所有提交后段信息的文件。一个段一旦拥有了提交点,就说明这个段只有读的权限,失去了写的权限。相反,当段在内存中时,就只有写的权限,而不具备读数据的权限,意味着不能被检索。

的概念提出主要是因为:在早期全文检索中为整个文档集合建立了一个很大的倒排索引,并将其写入磁盘中。如果索引有更新,就需要重新全量创建一个索引来替换原来的索引。这种方式在数据量很大时效率很低,并且由于创建一次索引的成本很高,所以对数据的更新不能过于频繁,也就不能保证时效性。

段被设定为不可修改具有一定的优势也有一定的缺点,优势主要表现在:

  • 不需要锁。如果你从来不更新索引,你就不需要担心多进程同时修改数据的问题。
  • 一旦索引被读入内核的文件系统缓存,便会留在哪里,由于其不变性。只要文件系统缓存中还有足够的空间,那么大部分读请求会直接请求内存,而不会命中磁盘。这提供了很大的性能提升。
  • 其它缓存(像filter缓存),在索引的生命周期内始终有效。它们不需要在每次数据改变时被重建,因为数据不会变化。
  • 写入单个大的倒排索引允许数据被压缩,减少磁盘 I/O 和 需要被缓存到内存的索引的使用量。

段的不变性的缺点如下:

  • 当对旧数据进行删除时,旧数据不会马上被删除,而是在.del文件中被标记为删除。而旧数据只能等到段更新时才能被移除,这样会造成大量的空间浪费。
  • 若有一条数据频繁的更新,每次更新都是新增新的标记旧的,则会有大量的空间浪费。
  • 每次新增数据时都需要新增一个段来存储数据。当段的数量太多时,对服务器的资源例如文件句柄的消耗会非常大。
  • 在查询的结果中包含所有的结果集,需要排除被标记删除的旧数据,这增加了查询的负担。

写流程

上面说过 ES 的索引的不变性,还有段和提交点的概念。那么它的具体实现细节和写入磁盘的过程是怎样的呢?

Elasticsearch简介_第21张图片

1.用户创建了一个新文档,新文档被写入内存中。
2.当达到默认的时间(1秒钟)或者内存的数据达到一定量时,会触发一次刷新(Refresh),将内存中的数据生成到一个新的段上并缓存到文件缓存系统上而不是直接被刷到磁盘。这是因为,提交一个新的段到磁盘需要一个 fsync 来确保段被物理性地写入磁盘,这样在断电的时候就不会丢失数据。但是 fsync 操作代价很大;如果每次索引一个文档都去执行一次的话会造成很大的性能问题,但是这里新段会被先写入到文件系统缓存,这一步代价会比较低。
3.新的段被写入到文件缓存系统,这时内存缓存被清空。在文件缓存系统会存在一个未提交的段。虽然新段未被提交(刷到磁盘),但是文件已经在缓存中了, 此时就可以像其它文件一样被打开和读取了。
4.到目前为止索引的段还未被刷新到磁盘,如果没有用 fsync 把数据从文件系统缓存刷(flush)到硬盘,我们不能保证数据在断电甚至是程序正常退出之后依然存在。Elasticsearch 增加了一个 translog ,或者叫事务日志,在每一次对 Elasticsearch 进行操作时均进行了日志记录。如上图所示,一个文档被索引之后,就会被添加到内存缓冲区,并且同时追加到了 translog。
5.每隔一段时间,更多的文档被添加到内存缓冲区和追加到事务日志(translog),之后新段被不断从内存缓存区被写入到文件缓存系统,这时内存缓存被清空,但是事务日志不会。随着 translog 变得越来越大,达到一定程度后索引被刷新,在刷新(flush)之后,段被全量提交,一个提交点被写入硬盘,并且事务日志被清空。

translog 提供所有还没有被刷到磁盘的操作的一个持久化纪录。当 Elasticsearch 启动的时候, 它会从磁盘中使用最后一个提交点去恢复已知的段,并且会重放 translog 中所有在最后一次提交后发生的变更操作。 translog 也被用来提供实时 CRUD 。当你试着通过ID查询、更新、删除一个文档,它会在尝试从相应的段中检索之前, 首先检查 translog 任何最近的变更。这意味着它总是能够实时地获取到文档的最新版本。

Elasticsearch简介_第22张图片

段合并

由于自动刷新流程每秒会创建一个新的段 ,这样会导致短时间内的段数量暴增。而段数目太多会带来较大的麻烦。 每一个段都会消耗文件句柄、内存和cpu运行周期。更重要的是,每个搜索请求都必须轮流检查每个段然后合并查询结果,所以段越多,搜索也就越慢。

Elasticsearch通过在后台定期进行段合并来解决这个问题。小的段被合并到大的段,然后这些大的段再被合并到更大的段。段合并的时候会将那些旧的已删除文档从文件系统中清除。被删除的文档不会被拷贝到新的大段中。合并的过程中不会中断索引和搜索。

Elasticsearch简介_第23张图片

段合并在进行索引和搜索时会自动进行,合并进程选择一小部分大小相似的段,并且在后台将它们合并到更大的段中,这些段既可以是未提交的也可以是已提交的。合并结束后老的段会被删除,新的段被 flush 到磁盘,同时写入一个包含新段且排除旧的和较小的段的新提交点,新的段被打开可以用来搜索。

段合并的计算量庞大, 而且还要吃掉大量磁盘 I/O,段合并会拖累写入速率,如果任其发展会影响搜索性能。Elasticsearch在默认情况下会对合并流程进行资源限制,所以搜索仍然有足够的资源很好地执行。

更新和删除流程

删除流程

由于不可修改,所以对于删除操作,不会把文档从旧的段中移除而是通过新增一个.del文件,文件中会列出这些被删除文档的段信息。这个被标记删除的文档仍然可以被查询匹配到, 但它会在最终结果被返回前从结果集中移除。

更新流程

不能修改旧的段来进行反映文档的更新,其实更新相当于是删除和新增这两个动作组成。会将旧的文档在.del文件中标记删除,然后文档的新版本被索引到一个新的段中。可能两个版本的文档都会被一个查询匹配到,但被删除的那个旧版本文档在结果集返回前就会被移除。

查询流程

查询我们最简单的方式可以分为两种:

  • 根据ID查询doc
  • 根据query(搜索词)去查询匹配的doc
public TopDocs search(Query query, int n);
public Document doc(int docID);
复制代码

根据ID去查询具体的doc的流程是:

  • 检索内存的Translog文件
  • 检索硬盘的Translog文件
  • 检索硬盘的Segement文件

根据query去匹配doc的流程是:

  • 同时去查询内存和硬盘的Segement文件

Elasticsearch简介_第24张图片

从上面所讲的写入流程,我们就可以知道:Get(通过ID去查Doc是实时的),Query(通过query去匹配Doc是近实时的)

  • 因为segement文件是每隔一秒才生成一次的

Elasticsearch查询又分可以为三个阶段:

  • QUERY_AND_FETCH(查询完就返回整个Doc内容)

  • QUERY_THEN_FETCH(先查询出对应的Doc id ,然后再根据Doc id 匹配去对应的文档)

  • DFS_QUERY_THEN_FETCH(先算分,再查询)

「这里的分指的是 词频率和文档的频率(Term Frequency、Document Frequency)众所周知,出现频率越高,相关性就更强」

Elasticsearch简介_第25张图片

一般我们用得最多的就是QUERY_THEN_FETCH,第一种查询完就返回整个Doc内容(QUERY_AND_FETCH)只适合于只需要查一个分片的请求。

QUERY_THEN_FETCH总体的流程流程大概是:

  • 客户端请求发送到集群的某个节点上。集群上的每个节点都是coordinate node(协调节点)
  • 然后协调节点将搜索的请求转发到所有分片上(主分片和副本分片都行)
  • 每个分片将自己搜索出的结果(doc id)返回给协调节点,由协调节点进行数据的合并、排序、分页等操作,产出最终结果。
  • 接着由协调节点根据 doc id 去各个节点上拉取实际document 数据,最终返回给客户端。

Query Phase阶段时节点做的事:

  • 协调节点向目标分片发送查询的命令(转发请求到主分片或者副本分片上)
  • 数据节点(在每个分片内做过滤、排序等等操作),返回doc id给协调节点

Fetch Phase阶段时节点做的是:

  • 协调节点得到数据节点返回的doc id,对这些doc id做聚合,然后将目标数据分片发送抓取命令(希望拿到整个Doc记录)
  • 数据节点按协调节点发送的doc id,拉取实际需要的数据返回给协调节点

主流程我相信大家也不会太难理解,说白了就是:由于Elasticsearch是分布式的,所以需要从各个节点都拉取对应的数据,然后最终统一合成给客户端

只是Elasticsearch把这些活都干了,我们在使用的时候无感知而已。

分页查询

查询阶段

在初始 查询阶段 时, 查询会广播到索引中每一个分片拷贝(主分片或者副本分片)。 每个分片在本地执行搜索并构建一个匹配文档的 优先队列

优先队列

一个 优先队列 仅仅是一个存有 top-n 匹配文档的有序列表。优先队列的大小取决于分页参数 fromsize 。例如,如下搜索请求将需要足够大的优先队列来放入100条文档。

GET /_search
{
    "from": 90,
    "size": 10
}
复制代码

这个查询阶段的过程如图 Figure 14, “查询过程分布式搜索” 所示。

Elasticsearch简介_第26张图片

Figure 14. 查询过程分布式搜索

查询阶段包含以下三个步骤:

  1. 客户端发送一个 search 请求到 Node 3Node 3 会创建一个大小为 from + size 的空优先队列。
  2. Node 3 将查询请求转发到索引的每个主分片或副本分片中。每个分片在本地执行查询并添加结果到大小为 from + size 的本地有序优先队列中。
  3. 每个分片返回各自优先队列中所有文档的 ID 和排序值给协调节点,也就是 Node 3 ,它合并这些值到自己的优先队列中来产生一个全局排序后的结果列表。

当一个搜索请求被发送到某个节点时,这个节点就变成了协调节点。 这个节点的任务是广播查询请求到所有相关分片并将它们的响应整合成全局排序后的结果集合,这个结果集合会返回给客户端。

第一步是广播请求到索引中每一个节点的分片拷贝。就像 document GET requests 所描述的, 查询请求可以被某个主分片或某个副本分片处理, 这就是为什么更多的副本(当结合更多的硬件)能够增加搜索吞吐率。 协调节点将在之后的请求中轮询所有的分片拷贝来分摊负载。

每个分片在本地执行查询请求并且创建一个长度为 from + size 的优先队列—也就是说,每个分片创建的结果集足够大,均可以满足全局的搜索请求。 分片返回一个轻量级的结果列表到协调节点,它仅包含文档 ID 集合以及任何排序需要用到的值,例如 _score

协调节点将这些分片级的结果合并到自己的有序优先队列里,它代表了全局排序结果集合。至此查询过程结束

取回阶段

查询阶段标识哪些文档满足搜索请求,但是我们仍然需要取回这些文档。这是取回阶段的任务, 正如 Figure 15, “分布式搜索的取回阶段” 所展示的。

Elasticsearch简介_第27张图片

Figure 15. 分布式搜索的取回阶段

分布式阶段由以下步骤构成:

  1. 协调节点辨别出哪些文档需要被取回并向相关的分片提交多个 GET 请求。
  2. 每个分片加载并 丰富 文档,如果有需要的话,接着返回文档给协调节点。
  3. 一旦所有的文档都被取回了,协调节点返回结果给客户端。

协调节点首先决定哪些文档 确实 需要被取回。例如,如果我们的查询指定了 { "from": 90, "size": 10 } ,最初的90个结果会被丢弃,只有从第91个开始的10个结果需要被取回。这些文档可能来自和最初搜索请求有关的一个、多个甚至全部分片。

协调节点给持有相关文档的每个分片创建一个 multi-get request ,并发送请求给同样处理查询阶段的分片副本。

分片加载文档体-- _source 字段—​如果有需要,用元数据和 search snippet highlighting 丰富结果文档。 一旦协调节点接收到所有的结果文档,它就组装这些结果为单个响应返回给客户端。

深分页(Deep Pagination)

先查后取的过程支持用 fromsize 参数分页,但是这是 有限制的 。 要记住需要传递信息给协调节点的每个分片必须先创建一个 from + size 长度的队列,协调节点需要根据 number_of_shards * (from + size) 排序文档,来找到被包含在 size 里的文档。

取决于你的文档的大小,分片的数量和你使用的硬件,给 10,000 到 50,000 的结果文档深分页( 1,000 到 5,000 页)是完全可行的。但是使用足够大的 from 值,排序过程可能会变得非常沉重,使用大量的CPU、内存和带宽。因为这个原因,我们强烈建议你不要使用深分页。

实际上, “深分页” 很少符合人的行为。当2到3页过去以后,人会停止翻页,并且改变搜索标准。会不知疲倦地一页一页的获取网页直到你的服务崩溃的罪魁祸首一般是机器人或者web spider。

如果你 确实 需要从你的集群取回大量的文档,你可以通过用 scroll 查询禁用排序使这个取回行为更有效率。

游标查询

scroll 查询 可以用来对 Elasticsearch 有效地执行大批量的文档查询,而又不用付出深度分页那种代价。

游标查询允许我们 先做查询初始化,然后再批量地拉取结果。 这有点儿像传统数据库中的 cursor

游标查询会取某个时间点的快照数据。 查询初始化之后索引上的任何变化会被它忽略。 它通过保存旧的数据文件来实现这个特性,结果就像保留初始化时的索引 视图 一样。

深度分页的代价根源是结果集全局排序,如果去掉全局排序的特性的话查询结果的成本就会很低。 游标查询用字段 _doc 来排序。 这个指令让 Elasticsearch 仅仅从还有结果的分片返回下一批结果。

启用游标查询可以通过在查询的时候设置参数 scroll 的值为我们期望的游标查询的过期时间。 游标查询的过期时间会在每次做查询的时候刷新,所以这个时间只需要足够处理当前批的结果就可以了,而不是处理查询结果的所有文档的所需时间。 这个过期时间的参数很重要,因为保持这个游标查询窗口需要消耗资源,所以我们期望如果不再需要维护这种资源就该早点儿释放掉。 设置这个超时能够让 Elasticsearch 在稍后空闲的时候自动释放这部分资源。

GET /old_index/_search?scroll=1m 
{
    "query": { "match_all": {}},
    "sort" : ["_doc"], 
    "size":  1000
}
复制代码
** 保持游标查询窗口一分钟。
** 关键字 _doc 是最有效的排序顺序。

这个查询的返回结果包括一个字段 _scroll_id, 它是一个base64编码的长字符串 。 现在我们能传递字段 _scroll_id_search/scroll 查询接口获取下一批结果:

GET /_search/scroll
{
    "scroll": "1m", 
    "scroll_id" : "cXVlcnlUaGVuRmV0Y2g7NTsxMDk5NDpkUmpiR2FjOFNhNnlCM1ZDMWpWYnRROzEwOTk1OmRSamJHYWM4U2E2eUIzVkMxalZidFE7MTA5OTM6ZFJqYkdhYzhTYTZ5QjNWQzFqVmJ0UTsxMTE5MDpBVUtwN2lxc1FLZV8yRGVjWlI2QUVBOzEwOTk2OmRSamJHYWM4U2E2eUIzVkMxalZidFE7MDs="
}
复制代码

这个游标查询返回的下一批结果。 尽管我们指定字段 size 的值为1000,我们有可能取到超过这个值数量的文档。 当查询的时候, 字段 size 作用于单个分片,所以每个批次实际返回的文档数量最大为 size * number_of_primary_shards

注意游标查询每次返回一个新字段 _scroll_id。每次我们做下一次游标查询, 我们必须把前一次查询返回的字段 _scroll_id 传递进去。 当没有更多的结果返回的时候,我们就处理完所有匹配的文档了。

倒排索引

正排索引:从文档里搜索一个关键字(文档->关键字)。优点:易维护。缺点:搜索的耗时太长。

倒排索引:从关键字搜索文档(关键字->文档)。优点:搜索耗时短。缺点:不易维护。

基础术语/概念

在进入下文之前,先描述几个前置概念。

term

在 ES 中,关键词被称为 term

postings list

举个例子,{静夜思, 望庐山瀑布}

是 "前" 这个 term 所对应列表。在 ES 中,这些被描述为所有包含特定 term 文档的 id 的集合。由于整型数字 integer 可以被高效压缩的特质,integer 是最适合放在 postings list 作为文档的唯一标识的,ES 会对这些存入的文档进行处理,转化成一个唯一的整型 id。

再说下这个 id 的范围,在存储数据的时候,在每一个 shard 里面,ES 会将数据存入不同的 segment,这是一个比 shard 更小的分片单位,这些 segment 会定期合并。在每一个 segment 里面都会保存最多 2^31 个文档,每个文档被分配一个唯一的 id,从0到(2^31)-1。

Elasticsearch简介_第28张图片

上面所描述的倒排索引,仅仅是一个很粗糙的模型。真的要在实际生产中使用,当然还差的很远。在实际生产场景中,比如 ES 最常用的日志分析,日志内容进行分词之后,可以得到多少的 term?那么如何快速的在海量 term 中查询到对应的 term 呢?遍历一遍显然是不现实的。

term dictionary

于是乎就有了 term dictionary,ES 为了能快速查找到 term,将所有的 term 排了一个序,二分法查找。是不是感觉有点眼熟,这不就是 MySQL 的索引方式的,直接用 B+树建立索引词典指向被索引的数据。

term index

但是问题又来了,你觉得 Term Dictionary 应该放在哪里?肯定是放在内存里面吧?磁盘 io 那么慢。就像 MySQL 索引就是存在内存里面了。

但是如果把整个 term dictionary 放在内存里面会有什么后果呢?

内存爆了...

别忘了,ES 默认可是会对全部 text 字段进行索引,必然会消耗巨大的内存,为此 ES 针对索引进行了深度的优化。在保证执行效率的同时,尽量缩减内存空间的占用。

于是乎就有了 term index。

Term index 从数据结构上分类算是一个“Trie 树”,也就是我们常说的字典树。这是一种专门处理字符串匹配的数据结构,用来解决在一组字符串集合中快速查找某个字符串的问题。

这棵树不会包含所有的 term,它包含的是 term 的一些前缀(这也是字典树的使用场景,公共前缀)。通过 term index 可以快速地定位到 term dictionary 的某个 offset,然后从这个位置再往后顺序查找。就想右边这个图所表示的。(怎么样,像不像我们查英文字典,我们定位 S 开头的第一个单词,或者定位到 Sh 开头的第一个单词,然后再往后顺序查询)

term index/term dictionary 设计中的优化点

lucene 在这里还做了两点优化:

  1. term dictionary 在磁盘上面是分 block 保存的,一个 block 内部利用公共前缀压缩,比如都是 Ab 开头的单词就可以把 Ab 省去。
  2. term index 在内存中是以 FST(finite state transducers 有穷状态转换器)的数据结构保存的。

FST 有两个优点:

  1. 空间占用小,通过对词典中单词前缀和后缀的重复利用,压缩了存储空间查询速度快。
  2. O(len(str)) 的查询时间复杂度。

FST的理论比较复杂,本文不细讲,延伸阅读:www.shenyanchao.cn/blog/2018/1…

OK,现在我们能得到 lucene 倒排索引大致是个什么样子的了。

单词-索引,单词-词典,倒排-列表三部分组成

Elasticsearch简介_第29张图片

postings list 设计中的优化点

原生的 Postings List 有两个痛点:

  1. 如何压缩以节省磁盘的空间(FOR压缩进行优化)
  2. 如何在内存中快速求交并集(Roaring Bitmaps算法进行优化)

先来聊聊压缩。

我们来简化下 Lucene 要面对的问题,假设有这样一个数组:

[73, 300, 302, 332, 343, 372]

如何把它进行尽可能的压缩?

Lucene 里,数据是按 Segment 存储的,每个 Segment 最多存 65536 个文档 ID, 所以文档 ID 的范围,从 0 到 2^16-1,所以如果不进行任何处理,那么每个元素都会占用 2 bytes ,对应上面的数组,就是 6 * 2 = 12 bytes.

怎么压缩呢?

压缩,就是尽可能降低每个数据占用的空间,同时又能让信息不失真,能够还原回来。

Step 1:Delta-encode —— 增量编码

我们只记录元素与元素之间的增量,于是数组变成了:

[73, 227, 2, 30, 11, 29]

Step 2:Split into blocks —— 分割成块

Lucene里每个块是 256 个文档 ID,这样可以保证每个块,增量编码后,每个元素都不会超过 256(1 byte).

为了方便演示,我们假设每个块是 3 个文档 ID:

[73, 227, 2], [30, 11, 29]

Step 3:Bit packing —— 按需分配空间

对于第一个块,[73, 227, 2],最大元素是227,需要 8 bits,好,那我给你这个块的每个元素,都分配 8 bits的空间。

但是对于第二个块,[30, 11, 29],最大的元素才30,只需要 5 bits,那我就给你每个元素,只分配 5 bits 的空间,足矣。

这一步,可以说是把吝啬发挥到极致,精打细算,按需分配。

以上三个步骤,共同组成了一项编码技术,Frame Of Reference(FOR):

Elasticsearch简介_第30张图片

Roaring bitmaps

接着来聊聊 Posting List 的第二个痛点 —— 如何快速求交并集(intersections and unions)。

在 Lucene 中查询,通常不只有一个查询条件,比如我们想搜索:

  • 含有“生存”相关词语的文档
  • 文档发布时间在最近一个月
  • 文档发布者是平台的特约作者

这样就需要根据三个字段,去三棵倒排索引里去查,当然,磁盘里的数据,上一节提到过,用了 FOR 进行压缩,所以我们要把数据进行反向处理,即解压,才能还原成原始的文档 ID,然后把这三个文档 ID 数组在内存中做一个交集。

即使没有多条件查询, Lucene 也需要频繁求并集,因为 Lucene 是分片存储的。

同样,我们把 Lucene 遇到的问题,简化成一道算法题。

假设有下面三个数组:

[64, 300, 303, 343]

[73, 300, 302, 303, 343, 372]

[303, 311, 333, 343]

求它们的交集。

Option 1: Integer 数组

直接用原始的文档 ID ,可能你会说,那就逐个数组遍历一遍吧,遍历完就知道交集是什么了。

其实对于有序的数组,用跳表(skip table)可以更高效,这里就不展开了,因为不管是从性能,还是空间上考虑,Integer 数组都不靠谱,假设有100M 个文档 ID,每个文档 ID 占 2 bytes,那已经是 200 MB,而这些数据是要放到内存中进行处理的,把这么大量的数据,从磁盘解压后丢到内存,内存肯定撑不住。

Option 2: Bitmap

假设有这样一个数组:

[3,6,7,10]

那么我们可以这样来表示:

[0,0,1,0,0,1,1,0,0,1]

看出来了么,对,我们用 0 表示角标对应的数字不存在,用 1 表示存在。

这样带来了两个好处:

  • 节省空间:既然我们只需要0和1,那每个文档 ID 就只需要 1 bit,还是假设有 100M 个文档,那只需要 100M bits = 100M * 1/8 bytes = 12.5 MB,比之前用 Integer 数组 的 200 MB,优秀太多
  • 运算更快:0 和 1,天然就适合进行位运算,求交集,「与」一下,求并集,「或」一下,一切都回归到计算机的起点

Option 3: Roaring Bitmaps

细心的你可能发现了,bitmap 有个硬伤,就是不管你有多少个文档,你占用的空间都是一样的,之前说过,Lucene Posting List 的每个 Segement 最多放 65536 个文档ID,举一个极端的例子,有一个数组,里面只有两个文档 ID:

[0, 65535]

用 Bitmap,要怎么表示?

[1,0,0,0,….(超级多个0),…,0,0,1]

你需要 65536 个 bit,也就是 65536/8 = 8192 bytes,而用 Integer 数组,你只需要 2 * 2 bytes = 4 bytes

呵呵,死板的 bitmap。可见在文档数量不多的时候,使用 Integer 数组更加节省内存。

我们来算一下临界值,很简单,无论文档数量多少,bitmap都需要 8192 bytes,而 Integer 数组则和文档数量成线性相关,每个文档 ID 占 2 bytes,所以:

8192 / 2 = 4096

当文档数量少于 4096 时,用 Integer 数组,否则,用 bitmap.

Elasticsearch简介_第31张图片

这里补充一下 Roaring bitmaps 和 之前讲的 Frame Of Reference 的关系。

Frame Of Reference 是压缩数据,减少磁盘占用空间,所以当我们从磁盘取数据时,也需要一个反向的过程,即解压,解压后才有我们上面看到的这样子的文档ID数组:[73, 300, 302, 303, 343, 372] ,接着我们需要对数据进行处理,求交集或者并集,这时候数据是需要放到内存进行处理的,我们有三个这样的数组,这些数组可能很大,而内存空间比磁盘还宝贵,于是需要更强有力的压缩算法,同时还要有利于快速的求交并集,于是有了Roaring Bitmaps 算法。

另外,Lucene 还会把从磁盘取出来的数据,通过 Roaring bitmaps 处理后,缓存到内存中,Lucene 称之为 filter cache。

Elasticsearch 的索引设计思路

将磁盘里的东西尽量搬进内存,减少磁盘随机读取次数 (同时也利用磁盘顺序读特性),结合各种压缩算法,用及其苛刻的态度使用内存。

所以,对于使用 Elasticsearch 进行索引时需要注意:

  • 不需要索引的字段,一定要明确定义出来,因为默认是自动建索引的
  • 同样的道理,对于 String 类型的字段,不需要 analysis 的也需要明确定义出来,因为默认也是会 analysis 的
  • 选择有规律的 ID 很重要,随机性太大的 ID(比如 Java 的 UUID) 不利于查询

分词器

众所周知,世界上有这么多的语言,那Elasticsearch怎么切分这些词呢? ,Elasticsearch内置了一些分词器

  • Standard Analyzer 。按词切分,将词小写
  • Simple Analyzer。按非字母过滤(符号被过滤掉),将词小写
  • WhitespaceAnalyzer。按照空格切分,不转小写
  • ....等等等

Elasticsearch分词器主要由三部分组成:

  • Character Filters(文本过滤器,去除HTML)
  • Tokenizer(按照规则切分,比如空格)
  • TokenFilter(将切分后的词进行处理,比如转成小写)

显然,Elasticsearch是老外写的,内置的分词器都是英文类的,而我们用户搜索的时候往往搜的是中文,现在中文分词器用得最多的就是IK

实践篇

安装部署

配置项

配置分词器

API

运维类API

管理类API

#查看Elasticsearch信息

GET /
复制代码

创建一个索引以及文档 在关系型数据库中,我们通常需要有专用的语句来生产相应的数据库,表,让后才可以让我们插入相应的记录,但是针对 Elasticsearch 来说,这个不是必须的

不指定主键ID

POST twitter/_doc
{
  "user": "GB",
  "uid": 1,
  "city": "Shenzhen",
  "province": "Guangdong",
  "country": "China"
}
复制代码

指定主键ID

POST twitter/_doc/1
{
  "user": "GB",
  "uid": 1,
  "city": "Shenzhen",
  "province": "Guangdong",
  "country": "China"
}
复制代码

在上面创建文档的过程中,我们并没有像其他 RDMS 系统一样,在输入文档之前需要定义各个字段的类型及长度等。当我们建立一个索引的第一个文档时,如果你没有创建它的schema,那么Elasticsearch会根据所输入字段的数据进行猜测它的数据类型,比如上面的 user 被被认为是 text 类型,而 uid 将被猜测为整数类型。这种方式我们称之为 schema on write,也即当我们写入第一个文档时,Elasticsearch 会自动帮我们创建相应的 schema。在 Elasticsearch 的术语中,mapping 被称作为 Elasticsearch 的数据 schema。一旦一个索引的某个字段的类型被确定下来之后,那么后续导入的文档的这个字段的类型必须是和之前的是一致,否则写入将导致错误。

Elasticsearch 中的数据类型: text:全文搜索字符串 keyword:用于精确字符串匹配和聚合 date 及 date_nanos:格式化为日期或数字日期的字符串 byte, short, integer, long:整数类型 boolean:布尔类型 float,double,half_float:浮点数类型

删除索引

DEL twitter,twitter2
复制代码

判断一个索引是否存在

HEAD twitter
复制代码

查询所有索引

GET _cat/indices?v
复制代码

查询单个索引下的所有文档

GET twitter/_search
复制代码

查询所有索引下的所有文档

GET /_search
复制代码

根据主键查询文档

GET twitter/_doc/1001
复制代码

全量修改文档 我们使用 PUT 的这个方法,每次修改一个文档时,我们需要把文档的每一项都要写出来

PUT twitter/_doc/1
{
   "user": "GB",
   "uid": 1,
   "city": "北京",
   "province": "北京",
   "country": "中国",
   "location":{
     "lat":"29.084661",
     "lon":"111.335210"
   }
}
复制代码

局部修改文档 只需要传入需要修改的字段即可

POST twitter/_doc/1/_update
{
  "doc":{
      "user": "TB"
  }
}
复制代码

删除文档

DELETE twitter/_doc/1
复制代码

判断一个文档是否存在

HEAD twitter/_doc/1
复制代码

查询类API

基本条件查询

GET twitter/_search
{
  "query": {
    "match": {
      "address": "朝阳"
    }
  }
}
复制代码

多条件and查询

GET twitter/_search
{
    "query": {
        "bool": {
            "must": [
                {
                    "match": {
                        "address": "朝阳"
                    }
                },
                {
                    "match": {
                        "age": 40
                    }
                }
            ]
        }
    }
}
复制代码

多条件or查询

GET twitter/_search
{
    "query": {
        "bool": {
            "should": [
                {
                    "match": {
                        "address": "朝阳"
                    }
                },
                {
                    "match": {
                        "age": 40
                    }
                }
            ]
        }
    }
}
复制代码

范围查询

GET twitter/_search
{
    "query": {
        "bool": {
            "should": [
                {
                    "match": {
                        "category": "小米"
                    }
                },
                {
                    "match": {
                        "category": "华为"
                    }
                }
            ],
            "filter": {
                "range": {
                    "price": {
                        "gt": 4000
                    }
                }
            }
        }
    }
}
复制代码

分页查询

GET twitter/_search
{
    "query": {
        "match": {
            "title": "手机"
        }
    },
    "from": 0,
    "size": 10,
    "_source": [
        "title",
        "price"
    ],
    "sort": {
        "price": {
            "order": "desc"
        }
    }
}
复制代码

精确匹配搜索

GET twitter/_search
{
    "query": {
        "match_phrase": {
            "category": "小华"
        }
    }
}
复制代码

文档-高亮显示

GET twitter/_search
{
    "query": {
        "match": {
            "category": "小华"
        }
    },
    "highlight": {
        "fields": {
            "category": {}
        }
    }
}
复制代码

文档-分组查询

GET twitter/_search
{
    "aggs": { //分组操作
        "price_avg": { //分组名称
            "avg": { //分组
                "field": "price" //分组字段
            }
        }
    },
    "size": 0
}
复制代码

文档-平均值

GET {{esHost}}/shopping/_search
{
    "aggs": { //分组操作
        "price_group": { //分组名称
            "terms": { //分组
                "field": "price" //分组字段
            }
        }
    },
    "size": 0
}
复制代码

fuzzy模糊搜索(错别字搜索)

在实际的搜索中,我们有时候会打错字,从而导致搜索不到。在 Elasticsearch 中,我们可以使用fuzziness属性来进行模糊查询,从而达到搜索有错别字的情形。

match 查询具有 “fuziness” 属性。它可以被设置为 “0”, “1”, “2”或 “auto”。“auto” 是推荐的选项,它会根据查询词的长度定义距离。在实际的使用中,当我们使用 auto 时,如果字符串的长度大于5,那么 funziness 的值自动设置为2,如果字符串的长度小于2,那么 fuziness 的值自动设置为 0。Fuzzy query返回包含与搜索词相似的词的文档,以 Levenshtein编辑距离 测量。编辑距离是将一个术语转换为另一个术语所需的一个字符更改的次数。 这些更改可以包括:

更改字符(box→fox)
删除字符(black→lack)
插入字符(sic→sick)
转置两个相邻字符(act→cat)
复制代码

为了找到相似的词,模糊查询会在指定的编辑距离内创建搜索词的所有可能变化或扩展的集合。 查询然后返回每个扩展的完全匹配。 例子 我们首先输入如下的一个文档到 fuzzyindex 索引中:

    PUT fuzzyindex/_doc/1
    {
      "content": "I like blue sky"
    }
复制代码

如果这个时候,我们进行如下的搜索:

    GET fuzzyindex/_search
    {
      "query": {
        "match": {
          "content": "ski"
        }
      }
    }
复制代码

那么是没有任何被搜索到的结果,这是因为 “I like blue sky" 里分词后没有 ski 这个词。这个时候,如果我们使用如下的搜索:

    GET fuzzyindex/_search
    {
      "query": {
        "match": {
          "content": {
            "query": "ski",
            "fuzziness": "1"
          }
        }
      }
    }
复制代码

那么显示的结果是:

    {
      "took" : 18,
      "timed_out" : false,
      "_shards" : {
        "total" : 1,
        "successful" : 1,
        "skipped" : 0,
        "failed" : 0
      },
      "hits" : {
        "total" : {
          "value" : 1,
          "relation" : "eq"
        },
        "max_score" : 0.19178805,
        "hits" : [
          {
            "_index" : "fuzzyindex",
            "_type" : "_doc",
            "_id" : "1",
            "_score" : 0.19178805,
            "_source" : {
              "content" : "I like blue sky"
            }
          }
        ]
      }
    }
复制代码

显然是找到我们需要的结果了。这是因为 sky 和 ski 时间上是只差别一个字母。

同样,如果我们选用“auto”选项看看:

    GET fuzzyindex/_search
    {
      "query": {
        "match": {
          "content": {
            "query": "ski",
            "fuzziness": "auto"
          }
        }
      }
    }
复制代码

它显示的结果和上面的是一样的。也可以进行匹配。

如果我们进行如下的匹配:

    GET fuzzyindex/_search
    {
      "query": {
        "match": {
          "content": {
            "query": "bxxe",
            "fuzziness": "auto"
          }
        }
      }
    }
复制代码

那么它不能匹配任何的结果,但是,如果我们进行如下的搜索:

    GET fuzzyindex/_search
    {
      "query": {
        "match": {
          "content": {
            "query": "bxxe",
            "fuzziness": "2"
          }
        }
      }
    }
复制代码

我们也可以使用如下的格式:

    GET /_search
    {
        "query": {
            "fuzzy": {
                "content": {
                    "value": "bxxe",
                    "fuzziness": "2"
                }
            }
        }
    }
复制代码

那么它可以显示搜索的结果,这是因为我们能够容许两个编辑的错误。

我们接着再做一个试验:

    GET fuzzyindex/_search
    {
      "query": {
        "match": {
          "content": {
            "query": "bluo ski",
            "fuzziness": 1
          }
        }
      }
    }
复制代码

上面显示的结果是:

    {
      "took" : 17,
      "timed_out" : false,
      "_shards" : {
        "total" : 1,
        "successful" : 1,
        "skipped" : 0,
        "failed" : 0
      },
      "hits" : {
        "total" : {
          "value" : 1,
          "relation" : "eq"
        },
        "max_score" : 0.40754962,
        "hits" : [
          {
            "_index" : "fuzzyindex",
            "_type" : "_doc",
            "_id" : "1",
            "_score" : 0.40754962,
            "_source" : {
              "content" : "I like blue sky"
            }
          }
        ]
      }
    }
复制代码

在上面的搜索中 “bluo ski”,这个词语有两个错误。我们想,是不是超过了我们定义的 "funziness": 1。其实不是的。 fuziness 为1,表示是针对每个词语而言的,而不是总的错误的数值。

在 Elasticsearch 中,有一个单独的 fuzzy 搜索,但是这个只针对一个 term 比较有用。其功能和上面的是差不多的:

    GET fuzzyindex/_search
    {
      "query": {
        "fuzzy": {
          "content": {
            "value": "ski",
            "fuzziness": 1
          }
        }
      }
    }
复制代码

上面的搜索返回的结果是:

    {
      "took" : 0,
      "timed_out" : false,
      "_shards" : {
        "total" : 1,
        "successful" : 1,
        "skipped" : 0,
        "failed" : 0
      },
      "hits" : {
        "total" : {
          "value" : 1,
          "relation" : "eq"
        },
        "max_score" : 0.19178805,
        "hits" : [
          {
            "_index" : "fuzzyindex",
            "_type" : "_doc",
            "_id" : "1",
            "_score" : 0.19178805,
            "_source" : {
              "content" : "I like blue sky"
            }
          }
        ]
      }
复制代码

你可能感兴趣的:(#,ElasticStack,elasticsearch)