# elasticsearch-demo **Repository Path**: jiangyifan/elasticsearch-demo ## Basic Information - **Project Name**: elasticsearch-demo - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 1 - **Created**: 2021-05-05 - **Last Updated**: 2023-09-05 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # elasticsearch-demo # 第1章 Elasticsearch概述 ## 1.1 开篇 ### Elasticsearch 是什么 Elaticsearch,简称为 ES, ES 是一个开源的高扩展的分布式全文搜索引擎,它可以近乎实时的存储、检索数据,它可以从RESTful Web服务接口访问,并使用模式少JSON(JavaScript对象符号)文档来存储数据。它是基于Java编程语言,并使用Lucene作为其核心来实现所有索引和搜索的功能,这使Elasticsearch能够在不同的平台上运行。使用户能够以非常快的速度来搜索非常大的数据量。 ### ElasticSearch可以做什么 1. 它可以进行快速进行全文搜索 2. 可扩展性: 支持PB级别数据的存储,以及成百上千的集群 3. 大数据处理, 数十亿行日志的聚合处理 4. 通过相关度(_score)对所有内容搜索 基于各项元素(从词频或新近度到热门度等)对搜索结果进行排序。将这些内容与功能进行混搭,以优化向用户显示结果的方式。 5. 弹性处理. 故障转移, 跨集群复制 ### 为什么要使用 Elasticsearch? 系统中的数据, 随着业务的发展,时间的推移, 将会非常多, 而业务中往往采用模糊查询进行数据的搜索, 而模糊查询会导致查询引擎放弃索引,导致系统查询数据时都是全表扫描,在百万级别的数据库中,查询效率是非常低下的,而我们使用 ES 做一个全文索引,将经常查询的系统功能的某些字段,比如说电商系统的商品表中商品名,描述、价格还有 id 这些字段我们放入 ES 索引库里,可以提高查询速度。 ### 全文搜索引擎 Google,百度类的网站搜索,它们都是根据网页中的关键字生成索引,我们在搜索的时候输入关键字,它们会将该关键字即索引匹配到的所有网页返回;还有常见的项目中应用日志的搜索等等。对于这些非结构化的数据文本,关系型数据库搜索不是能很好的支持。 一般传统数据库,全文检索都实现的很鸡肋,因为一般也没人用数据库存文本字段。进行全文检索需要扫描整个表,如果数据量大的话即使对 SQL 的语法优化,也收效甚微。建立了索引,但是维护起来也很麻烦,对于 insert 和 update 操作都会重新构建索引。 基于以上原因可以分析得出,在一些生产环境中,使用常规的搜索方式,性能是非常差的: 搜索的数据对象是大量的非结构化的文本数据。 文件记录量达到数十万或数百万个甚至更多。 支持大量基于交互式文本的查询。 需求非常灵活的全文搜索查询。 对高度相关的搜索结果的有特殊需求,但是没有可用的关系数据库可以满足。 对不同记录类型、非文本数据操作或安全事务处理的需求相对较少的情况。为了解决结构化数据搜索和非结构化数据搜索性能问题,我们就需要专业,健壮,强大的全文搜索引擎 。 这里说到的全文搜索引擎指的是目前广泛应用的主流搜索引擎。它的工作原理是计算机索引程序通过扫描文章中的每一个词,对每一个词建立一个索引,指明该词在文章中出现的次数和位置,当用户查询时,检索程序就根据事先建立的索引进行查找,并将查找的结果反馈给用户的检索方式。这个过程类似于通过字典中的检索字表查字的过程。 ### Elasticsearch 应用案例 GitHub: 2013 年初,抛弃了 Solr,采取 Elasticsearch 来做 PB 级的搜索。 “GitHub 使用Elasticsearch 搜索 20TB 的数据,包括 13 亿文件和 1300 亿行代码”。 维基百科:启动以 Elasticsearch 为基础的核心搜索架构 百度:目前广泛使用Elasticsearch作为文本数据分析,采集百度所有服务器上的各类指标数据及用户自定义数据,通过对各种数据进行多维分析展示,辅助定位分析实例异常或业务层面异常。目前覆盖百度内部 20 多个业务线(包括云分析、网盟、预测、文库、直达号、钱包、 风控等),单集群最大 100 台机器, 200 个 ES 节点,每天导入 30TB+数据。 新浪:使用 Elasticsearch 分析处理 32 亿条实时日志。 阿里:使用 Elasticsearch 构建日志采集和分析体系。 Stack Overflow:解决 Bug 问题的网站,全英文,编程人员交流的网站。 ## 1.2 相关技术 ### **Lucene是什么** Lucene全文检索框架 Apache Lucene将写入索引的所有信息组织成一种倒排索引(Inverted Index)的结构之中,该结构是种将词项映射到文档的数据结构。其工作方式与传统的关系数据库不同,大致来说倒排索引是面向词项而不是面向文档的。且Lucene索引之中还存储了很多其他的信息,如词向量等等,每个Lucene都是由多个段构成的,每个段只会被创建一次但会被查询多次,段一旦创建就不会再被修改。多个段会在段合并的阶段合并在一起,何时合并由Lucene的内在机制决定,段合并后数量会变少,但是相应的段本身会变大。段合并的过程是非常消耗I/O的,且与之同时会有些不再使用的信息被清理掉。在Lucene中,将数据转化为倒排索引,将完整串转化为可用于搜索的词项的过程叫做分析。文本分析由分析器(Analyzer)来执行,分析其由分词器(Tokenizer),过滤器(Filter)和字符映射器(Character Mapper)组成,其各个功能显而易见。 Lucene 是 Apache 软件基金会 Jakarta 项目组的一个子项目,提供了一个简单却强大的应用程式接口,能够做全文索引和搜寻。在 Java 开发环境里 Lucene 是一个成熟的免费开源工具。就其本身而言, Lucene 是当前以及最近几年最受欢迎的免费 Java 信息检索程序库。但 Lucene 只是一个提供全文搜索功能类库的核心工具包,而真正使用它还需要一个完善的服务框架搭建起来进行应用。 目前市面上流行的搜索引擎软件,主流的就两款: Elasticsearch 和 Solr,这两款都是基于 Lucene 搭建的,可以独立部署启动的搜索引擎服务软件。由于内核相同,所以两者除了服务器安装、部署、管理、集群以外,对于数据的操作 修改、添加、保存、查询等等都十分类似。 1)Lucene只是一个库。想要使用它,你必须使用Java来作为开发语言并将其直接集成到你的应用中,更糟糕的是,Lucene非常复杂,你需要深入了解检索的相关知识来理解它是如何工作的。 2)Elasticsearch也使用Java开发并使用Lucene作为其核心来实现所有索引和搜索的功能,但是它的目的是通过简单的RESTful API来隐藏Lucene的复杂性,从而让全文搜索变得简单。 ### **Elk架构** “ELK”是三个开源项目的首字母缩写,这三个项目分别是:Elasticsearch、Logstash 和 Kibana。Elasticsearch 是一个搜索和分析引擎。Logstash 是服务器端数据处理管道,能够同时从多个来源采集数据,转换数据,然后将数据发送到诸如 Elasticsearch 等“存储库”中。Kibana 则可以让用户在 Elasticsearch 中使用图形和图表对数据进行可视化。 # 第2章 Elasticsearch入门 ## 2.1 核心概念 ### 索引Index 一个索引就是一个拥有几分相似特征的文档的集合,比如说,你可以有一个客户数据的索引,另一个产品目录的索引,还有一个订单数据的索引。你可以把索引看成关系型数据库的表。 一个索引由一个名字来标识(必须全部是小写字母),并且当我们要对这个索引中的文档进行索引、搜索、更新和删除(CRUD)的时候,都要使用到这个名字。 索引实际上是指向一个或者多个物理'分片'的逻辑命名空间。Elasticsearch可以把索引存放在一台机器或者分散在多台服务器上,每个索引有一或多个分片(shard),每个分片可以有多个副本(replica)。 ### 类型Type 在一个索引中,你可以定义一种或多种类型。 一个类型是你的索引的一个逻辑上的分类/分区,其语义完全由你来定。通常,会为具有一组共同字段的文档定义一个类型。不同的版本,类型发生了不同的变化。 | 版本 | Type | | ---- | ---- | | 5.x | 支持多种 type | | 6.x | 只能有一种 type | | 7.x | 默认不再支持自定义索引类型(默认类型为: _doc)| ### 文档Document 它是指最顶层或者根对象, 这个根对象被序列化成 JSON 并存储到 Elasticsearch 中,指定了唯一 ID,可以自动生成也可以指定 文档由多个字段组成,一个文档是一个可被索引的基础信息单元,也就是一条数据,可搜索的最小单位 比如:你可以拥有某一个客户的文档,某一个产品的一个文档,当然,也可以拥有某个订单的一个文档。文档以 JSON(Javascript Object Notation)格式来表示,而 JSON 是一个到处存在的互联网数据交互格式。 在一个 index/type 里面,你可以存储任意多的文档 文档格式灵活,不需要预先规定格式`类似MYSQL里面的CHAR\VARCHAR类型` , 可以自己指定,也可以 ES 自动推算,支持数组、跟嵌套 ![img](https://i.loli.net/2021/05/05/oX96VQINMKkzb7A.png) 文档元数据 ![img](https://i.loli.net/2021/05/05/HrblQmTZotJLuCe.png) ### 字段Field 相当于是数据表的字段,对文档数据根据不同属性进行的分类标识。 #### 数据类型 核心数据类型 text 和 keyword text和keyword都是string类型,选择区分很简单,需要进行分词用text,不需要并且进行排序或聚合的可以用keyword。 数值数据类型 long,integer,short,byte,double,float,half_float,scaled_float 日期数据类型 date 布尔数据类型 boolean 二进制数据类型 binary 范围数据类型 integer_range,float_range,long_range,double_range,date_range 复杂数据类型 对象数据类型 object 用于单个JSON对象 嵌套数据类型 nested 用于JSON对象数组 地理数据类型 地理位置数据类型 geo_point 纬度/经度积分 地理形状数据类型 geo_shape 用于多边形等复杂形状 专业数据类型 IP数据类型 ip 用于IPv4和IPv6地址 完成数据类型 completion 提供自动完成建议 令牌计数数据类型 token_count 计算字符串中令牌的数量 mapper-murmur3 murmur3 在索引时计算值的哈希并将其存储在索引中 mapper-annotated-text annotated-text 索引包含特殊标记的文本(通常用于标识命名实体) 渗滤器类型 接受来自query-dsl的查询 join 数据类型 为同一索引内的文档定义父/子关系 ![字段数据类型](https://i.loli.net/2021/05/05/SwbmGTpLcUPVXEk.png) ### **设置setting** setting可以理解为管理这个index的一些重要属性的,比如分片(shard)和副本(replica),它决定这个索引库最终的配置形态 number_of_shards: 是设置的分片数,设置之后无法更改! refresh_interval: 是设置es缓存的刷新时间,如果写入较为频繁,但是查询对实时性要求不那么高的话,可以设置高一些来提升性能。可以更改 number_of_replicas : 是设置该索引库的副本数,建议设置为1以上。 ### 映射Mapping mapping可以理解为关系型数据库的表结构,指定字段的类型。 mapping 是处理数据的方式和规则方面做一些限制,如:某个字段的数据类型、默认值、分析器、是否被索引等等。并且需要思考如何建立映射才能对性能更好。作用如下 - 定义索引中的字段名称 - 定义字段的数据类型,例如字符串,数字,布尔… - 字段,倒排索引的相关配置,(Analyzed or Not Analyzed,Analyzer) Mapping 会把 JSON 文档映射称 Lucene 所需的扁平格式 #### Dynamic Mapping - 在写入文档的时候,如果索引不存在,会自动创建索引 - Dynamic Mapping 的机制,使得我们无需手动定义 Mappings。Elasticsearch 会自动根据文档信息,推算出字段的类型 - 但是会有时候推算不对。例如地理位置信息 - 当类型如果设置不对时,会导致一些功能无法正常运行,例如 Range 查询 #### 能否更改 Mapping 的字段类型 - 新增字段 - Dynamic 设置为 true 时,一定有新增字段的文档写入,Mapping 也同时被更新 - Dynamic 设为 false,Mapping 不会被更新,自增字段的数据无法被索引,但是信息会出现在_source 中 - Dynamic 设置成 Strict 文档写入失败 - 对已有字段,一旦已经有数据写入,就不在支持修改字段定义 - Luene 实现的倒排索引,一旦生成后,就不允许修改 如果希望改变字段类型,必须 Reindex API,重建索引 如果修改了字段的数据类型,会导致已被索引的属于无法被搜索,但是如果是增加新的字段,就不会有这样的影响 ![Dynamic Mappings 设置](https://i.loli.net/2021/05/05/fbxUdPSQH97igO6.png) ### 分片Shards 一个索引可以存储超出单个节点硬件限制的大量数据。比如,一个具有 10 亿文档数据的索引占据 1TB 的磁盘空间,而任一节点都可能没有这样大的磁盘空间。 或者单个节点处理搜索请求,响应太慢。为了解决这个问题,**Elasticsearch 提供了将索引划分成多份的能力,每一份就称之为分片。**当你创建一个索引的时候,你可以指定你想要的分片的数量。每个分片本身也是一个功能完善并且独立的“索引”,这个“索引”可以被放置到集群中的任何节点上。 一个 分片 是一个底层的 工作单元,它仅保存了全部数据中的一部分,一个分片是一个 Lucene 的实例。分片是数据的容器,文档保存在分片内,分片又被分配到集群内的各个节点里。我们的文档被存储和索引到分片内,但是应用程序是直接与索引而不是与分片进行交互 至于一个分片怎样分布,它的文档怎样聚合和搜索请求,是完全由 Elasticsearch 管理的,对于作为用户的你来说,这些都是透明的,无需过分关心。 一个Elasticsearch 索引 是分片的集合,当 Elasticsearch 在索引中搜索的时候, 他发送查询到每一个属于索引的分片 #### 主分片 索引内任意一个文档都归属于一个主分片,所以主分片的数目决定着索引能够保存的最大数据量 主分片最大能够存储 Integer.MAX_VALUE - 128 个文档 在索引建立的时候就已经确定了主分片数,但是副本分片数可以随时修改。 所有新近被索引的文档都将会保存在主分片上,然后被并行的复制到对应的副本分片上 我不知道这个索引将来会变得多大,并且过后我也不能更改索引的大小,所以为了保险起见,还是给它设为 1000 个分片吧… -- 一个新手的话 一个分片并不是没有代价的。记住: - 一个分片的底层即为一个 Lucene 索引,会消耗一定文件句柄、内存、以及 CPU 运转。 - 每一个搜索请求都需要命中索引中的每一个分片,如果每一个分片都处于不同的节点还好, 但如果多个分片都需要在同一个节点上竞争使用相同的资源就有些糟糕了。 - 用于计算相关度的词项统计信息是基于分片的。如果有许多分片,每一个都只有很少的数据会导致很低的相关度。 #### 副本Replicas 在一个网络 / 云的环境里,失败随时都可能发生,在某个分片/节点不知怎么的就处于离线状态,或者由于任何原因消失了,这种情况下,有一个故障转移机制是非常有用并且是强烈推荐的。为此目的, Elasticsearch 允许你创建分片的一份或多份拷贝,这些拷贝叫做复制分片(副本)。 复制分片之所以重要,有两个主要原因: 1.副本分片的主要目的就是为了故障转移 在分片/节点失败的情况下,提供了高可用性。因为这个原因,注意到复制分片从不与原/主要(original/primary)分片置于同一节点上是非常重要的。 扩展你的搜索量/吞吐量,因为搜索可以在所有的副本上并行运行。 总之,每个索引可以被分成多个分片。一个索引也可以被复制 0 次(意思是没有复制)或多次。一旦复制了,每个索引就有了主分片(作为复制源的原来的分片)和复制分片(主分片的拷贝)之别。 分片和复制的数量可以在索引创建的时候指定。在索引创建之后,你可以在任何时候动态地改变复制的数量,但是你事后不能改变分片的数量。 默认情况下,Elasticsearch 中的每个索引被分片 1 个主分片和 1 个复制,这意味着,如果你的集群中至少有两个节点,你的索引将会有 1 个主分片和另外 1 个复制分片(1 个完全拷贝),这样的话每个索引总共就有 2 个分片, 我们需要根据索引需要确定分片个数。 一个副本分片只是一个主分片的拷贝。副本分片作为硬件故障时保护数据不丢失的冗余备份,并为搜索和返回文档等读操作提供服务。 2.通过副本进行负载均衡 搜索性能取决于最慢的节点的响应时间,所以尝试均衡所有节点的负载是一个好想法。 如果我们只是增加一个节点而不是两个,最终我们会有两个节点各持有一个分片,而另一个持有两个分片做着两倍的工作。 读操作——搜索和返回数据——可以同时被主分片 或 副本分片所处理,所以当你拥有越多的副本分片时,也将拥有越高的吞吐量 ![img](https://i.loli.net/2021/05/05/4Iiu6WE5RHOcthX.png) ## 2.2 数据结构 ![img](https://i.loli.net/2021/05/05/GbUTfn36YDKWQla.png) 我们通过与MySQL的对比, 来快速了解Elasticsearch数据架构 ![img](https://img-blog.csdnimg.cn/20191020114233693.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2FzZHJ0MTI1ODl3dG8x,size_16,color_FFFFFF,t_70) 如图所示: 1. 其中数据库对应ES中的Index(索引库) 2. 而Table对应Type, 7.x以后'type'概念删除了, 可以理解一个Index库只存储一个种类型的数据 3. 数据库中的一条记录 => ES中的一个文档记录document 4. 数据库表中的字段 => ES 中的Field字段 5. 数据库表中的字段类型映射 => ES中的mapping映射 根据最新的ElasticSearch7.x中已经将Types移除了,并且在日常的使用中,我建议最好把一个索引(index)当做数据库的一张表来使用,类型(type)除了必要的情况,最好无视它,将它和索引库名设置一样即可。 这里顺便再来说下创建索引库的结构。我们知道在关系型数据库中需要创建表才能添加数据,但是在ElasticSearch中可以直接插入数据,它会根据你的第一条数据来自动创建索引库的结构, 但是这种在很多情况下是不符合我们要求的。如果我们想自己进行创建的话,那么就有必要了解一下index的setting和mapping了。 ### 倒排索引 Elasticsearch 使用一种称为 倒排索引 的结构,它适用于快速的全文搜索。一个倒排索引由文档中所有不重复词的列表构成,对于其中每个词,有一个包含它的文档列表。 假设我们MySQL 有两条记录,对应ES中的两个文档,每个记录的 content 字段包含如下内容: doc1: 天空中有只小鸟 doc2: 天空中有只大雁 为了创建倒排索引,我们首先将每个文档的 content 字段(通过分词器,分析器)拆分成单独的词(我们称它为 词条 或 tokens ),创建一个包含所有不重复词条的排序列表,然后列出每个词条出现在哪个文档。结果如下所示: ![image-20210505095421381](https://i.loli.net/2021/05/05/Rh4PpwEday5uYL9.png) 现在,如果我们想搜索 天空小鸟 ,我们只需要查找包含每个词条的文档: ![image-20210505095519040](https://i.loli.net/2021/05/05/FUqO1ZTPL7HM4mE.png) 两个文档都匹配,但是第一个文档比第二个匹配度更高。如果我们使用仅计算匹配词条数量的简单 相似性算法 ,那么,我们可以说,对于我们查询的相关性来讲,第一个文档比第二个文档更佳。 # 第3章 Elasticsearch进阶 ## 3.1 系统架构 ![es亿级数据检索优化,三秒返回突破性能瓶颈_lucene](https://i.loli.net/2021/05/05/r93OqpScFBbaVGQ.jpg) ### 集群cluster 一个集群由一个或多个共享相同的群集名称的节点组成。它们共同承担数据和负载的压力。当有节点加入集群中或者从集群中移除节点时,集群将会重新平均分布所有的数据。 每个群集有一个单独的主节点,这是由程序自动选择,如果当前主节点失败,程序会自动选择其他节点作为主节点。 当一个节点被选举成为主节点时, 它将负责管理集群范围内的所有变更,例如增加、 删除索引,或者增加、删除节点等。 而主节点并不需要涉及到文档级别的变更和搜索等操作,所以当集群只拥有一个主节点的情况下,即使流量的增加它也不会成为瓶颈。 任何节点都可以成为主节点。 作为用户,我们可以将请求发送到集群中的任何节点 ,包括主节点。 每个节点都知道 任意文档所处的位置,并且能够将我们的请求直接转发到存储我们所需文档的节点。 无论我们将请求发送到哪个节点,它都能负责从各个包含我们所需文档的节点收集回数据,并将最终结果返回給客户端。 Elasticsearch 对这一切的管理都是透明的。 ### 节点node 一个节点属于一个集群。通常情况下一个服务器有一个节点,但有时候为了测试方便,一台服务器也可以有多个节点。在启动时,一个节点将使用广播来发现具有相同群集名称的现有群集,并将尝试加入该群集。**节点属性根据elasticsearch.yml的一些配置来决定!其中master和datanode是必不可少的,其他的可以按照情况来进行添加!为了防止脑裂以及后续维护,建议将节点属性分离!** master节点的职责主要包括集群、节点和索引的管理,不负责文档级别的管理; data节点:配置文件中node.data属性为true(默认为true),就有资格被设置成data节点,data节点主要用于执行数据相关的操作。比如文档的CRUD 客户端节点:配置文件中node.master属性和node.data属性均为false。该节点不能作为master节点,也不能作为data节点。可以作为客户端节点,用于响应用户的请求,把请求转发到其他节点 每个节点默认都起到 **Coorinating Node** 的职责 > 造成脑裂的原因主要是网络分区(这个词之前在讲CAP理论时就已经出现过了)。由于网络故障或者集群节点之间的通信链路有问题,导致原本的一个集群被物理分割成为两个甚至多个小的、独立运作的集群,这些小集群各自会选举出自己的主节点,并同时对外提供服务。网络分区恢复后,这些小集群再度合并为一个集群,就出现了多个活动的主节点。 > > 另外,主节点假死也有可能造成脑裂。由于当前主节点暂时无响应(如负载过高、频繁GC等)导致其向其他节点发送心跳信号不及时,其他节点认为它已经宕机,就触发主节点的重新选举。新的主节点选举出来后,假死的主节点又复活,就出现了两个主节点。 Elasticsearch内部提供了一个rest接口用于查看集群内部的健康状况: /_cluster/health 这里的status有3种状态 - Green - 主分片跟副本都正常分配 - Yellow - 主分片全部分配正常,有副本分片问能正常分配 - Red - 有主分片未能分配 ## 3.2 进阶概念 ### 单节点集群 我们在包含一个空节点的集群内创建名为 users 的索引,为了演示目的,我们将分配 3个主分片和一份副本(每个主分片拥有一个副本分片)。 ``` PUT http://127.0.0.1:1001/users { "settings" : { "number_of_shards" : 3, "number_of_replicas" : 1 } } ``` 集群现在是拥有一个索引的单节点集群。所有 3 个主分片都被分配在 node-1 。 ![img](https://i.loli.net/2021/05/05/dXZgMNJCvtBRD7c.png) 3 个主分片正常。3 个副本分片都是 Unassigned,它们都没有被分配到任何节点。 在同 一个节点上既保存原始数据又保存副本是没有意义的,因为一旦失去了那个节点,我们也将丢失该节点 上的所有副本数据。当前集群是正常运行的,但存在丢失数据的风险。 ### 故障转移 当集群中只有一个节点在运行时,意味着会有一个单点故障问题——没有冗余。 幸运的是,我们只需再启动一个节点即可防止数据丢失。当你在同一台机器上启动了第二个节点时,只要它和第一个节点有同样的 cluster.name 配置,它就会自动发现集群并加入到其中。但是在不同机器上启动节点的时候,为了加入到同一集群,你需要配置一个可连接到的单播主机列表。之所以配置为使用单播发现,以防止节点无意中加入集群。只有在同一台机器上运行的节点才会自动组成集群。 如果启动了第二个节点,集群将会拥有两个节点 : 所有主分片和副本分片都已被分配 。 ![img](https://i.loli.net/2021/05/05/eFx2nqUlPpITbGa.png) 表示所有 6 个分片(包括 3 个主分片和 3 个副本分片)都在正常运行。 第二个节点加入到集群后, 3 个副本分片将会分配到这个节点上——每个主分片对应一个副本分片。这意味着当集群内任何一个节点出现问题时,我们的数据都完好无损。所有新近被索引的文档都将会保存在主分片上,然后被并行的复制到对应的副本分片上。这就保证了我们 既可以从主分片又可以从副本分片上获得文档。 ### 水平扩容 怎样为我们的正在增长中的应用程序按需扩容呢?当启动了第三个节点,我们的集群将会拥有三个节点的集群 : 为了分散负载而对分片进行重新分配 。 ![img](https://i.loli.net/2021/05/05/TfOgPMW68qunQod.png) Node 1 和 Node 2 上各有一个分片被迁移到了新的 Node 3 节点,现在每个节点上都拥有 2 个分片, 而不是之前的 3 个。 这表示每个节点的硬件资源(CPU, RAM, I/O)将被更少的分片所共享,每个分片 的性能将会得到提升。 分片是一个功能完整的搜索引擎,它拥有使用一个节点上的所有资源的能力。 我们这个拥有 6 个分 片(3 个主分片和 3 个副本分片)的索引可以最大扩容到 6 个节点,每个节点上存在一个分片,并且每个 分片拥有所在节点的全部资源。 **但是如果我们想要扩容超过 6 个节点怎么办呢?** 主分片的数目在索引创建时就已经确定了下来。实际上,这个数目定义了这个索引能够存储 的最大数据量。(实际大小取决于你的数据、硬件和使用场景。) 但是,读操作——搜索和返回数据——可以同时被主分片 或 副本分片所处理,所以当你拥有越多的副本分片时,也将拥有越高的吞吐量。 在运行中的集群上是可以动态调整副本分片数目的,我们可以按需伸缩集群。让我们把副本数从默认的 1 增加到 2。 ![img](https://i.loli.net/2021/05/05/lLKuMTNV8CyQ1AB.png) 这意味着我们可以将集群扩容到 9 个节点,每个节点上一个分片。相比原来 3 个节点时,集群搜索性能可以提升 3 倍。 当然,如果只是在相同节点数目的集群上增加更多的副本分片并不能提高性能,因为每个分片从节点上获得的资源会变少。 你需要增加更多的硬件资源来提升吞吐量。 ### 应对故障 我们关闭第一个节点,这时集群的状态为:关闭了一个节点后的集群。 ![img](https://i.loli.net/2021/05/05/efYJVnUIh3K9yPE.png) 我们关闭的节点是一个主节点。而集群必须拥有一个主节点来保证正常工作,所以发生的第一件事情就是选举一个新的主节点: Node 2 。在我们关闭 Node 1 的同时也失去了主分片 1 和 2 ,并且在缺失主分片的时候索引也不能正常工作。 如果此时来检查集群的状况,我们看到的状态将会为 red :不是所有主分片都在正常工作。 幸运的是,在其它节点上存在着这两个主分片的完整副本, 所以新的主节点立即将这些分片在 Node 2 和 Node 3 上对应的副本分片提升为主分片, 此时集群的状态将会为yellow。这个提升主分片的过程是瞬间发生的,如同按下一个开关一般。 为什么我们集群状态是 yellow 而不是 green 呢? 虽然我们拥有所有的三个主分片,但是同时设置了每个主分片需要对应 2 份副本分片,而此时只存在一份副本分片。 所以集群不能为 green 的状态,不过我们不必过于担心:如果我们同样关闭了 Node 2 ,我们的程序 依然 可以保持在不丢任何数据的情况下运行,因为Node 3 为每一个分片都保留着一份副本。 集群可以将缺失的副本分片再次进行分配,那么集群的状态也将恢复成之前的状态。 如果 Node 1 依然拥有着之前的分片,它将尝试去重用它们,同时仅从主分片复制发生了修改的数据文件。和之前的集群相比,只是 Master 节点切换了。 > master 选举流程: > > Elasticsearch的选主是ZenDiscovery模块负责的,主要包含Ping(节点之间通过这个RPC来发现彼此) > 和Unicast(单播模块包含-一个主机列表以控制哪些节点需要ping通)这两部分。 > > 对所有可以成为master的节点(node master: true)根据nodeId字典排序,每次选举每个节点都把自 > 己所知道节点排一次序,然后选出第一个(第0位)节点,暂且认为它是master节点。 > > 如果对某个节点的投票数达到一定的值(可以成为master节点数n/2+1)并且该节点自己也选举自己, > 那这个节点就是master。否则重新选举一直到满足上述条件。 > > master节点的职责主要包括集群、节点和索引的管理,不负责文档级别的管理;data节点可以关闭http > 功能。 ### 路由计算 & 分片控制 当索引一个文档的时候,文档会被存储到一个主分片中。Elasticsearch 如何知道一个文档应该存放到哪个分片中呢? 当我们创建文档时,它如何决定这个文档应当被存储在分片 1 还是分片 2 中呢?首先这肯定不会是随机的,否则将来要获取文档的时候我们就不知道从何处寻找了。实际上,这个过程是根据下面这个公式决定的: ``` shard = hash(routing) % number_of_primary_shards ``` routing 是一个可变值,默认是文档的 _id ,也可以设置成一个自定义的值。 routing 通过hash 函数生成一个数字,然后这个数字再除以 number_of_primary_shards (主分片的数量)后得到余数 。这个分布在 0 到 number_of_primary_shards-1 之间的余数,就是我们所寻求的文档所在分片的位置。 ![img](https://i.loli.net/2021/05/05/LbRGoVtuiK896Xz.png) 这就解释了为什么我们要在创建索引的时候就确定好主分片的数量并且永远不会改变这个数量:因为如果数量变化了,那么所有之前路由的值都会无效,文档也再也找不到了。 所有的文档API ( get . index . delete 、 bulk , update以及 mget )都接受一个叫做routing 的路由参数,通过这个参数我们可以自定义文档到分片的映射。一个自定义的路由参数可以用来确保所有相关的文档—一例如所有属于同一个用户的文档——都被存储到同一个分片中。 ### 分片控制( 主分片和副本分片如何交互) 我们可以发送请求到集群中的任一节点。每个节点都有能力处理任意请求。每个节点都知道集群中任一文档位置,所以可以直接将请求转发到需要的节点上。在下面的例子中,如果将所有的请求发送到Node 1001,我们将其称为协调节点coordinating node。 ![img](https://i.loli.net/2021/05/05/EjLoNnUI4D5c72T.png) 当发送请求的时候, 为了扩展负载,更好的做法是轮询集群中所有的节点。 ### 数据写流程 新建、索引和删除请求都是写操作, 必须在主分片上面完成之后才能被复制到相关的副本分片。 ![img](https://i.loli.net/2021/05/05/x1jv6mKe3bhUzul.png) 以下是在主副分片和任何副本分片上面 成功新建,索引和删除文档所需要的步骤顺序: 1. 客户端向 `Node 2` 发送新建、索引或者删除请求。 2. 节点使用文档的 `_id` 确定文档属于分片 0 。请求会被转发到 `Node 1`,因为分片 0 的主分片目前被分配在 `Node 1` 上。 3. `Node 1` 在主分片上面执行请求。如果成功了,它将请求并行转发到 `Node 2` 和 `Node 3` 的副本分片上。一旦所有的副本分片都报告成功, `Node 1` 将向协调节点报告成功,协调节点向客户端报告成功。 在客户端收到成功响应时,文档变更已经在主分片和所有副本分片执行完成,变更是安全的。 ### 数据读流程 ![取回单个文档](https://i.loli.net/2021/05/05/vsdmO2ukB1wqXZT.png) 以下是从主分片或者副本分片检索文档的步骤顺序: 1、客户端向 `Node 1` 发送获取请求。 2、节点使用文档的 `_id` 来确定文档属于分片 `0` 。分片 `0` 的副本分片存在于所有的三个节点上。 在这种情况下,它将请求转发到 `Node 2` 。 3、`Node 2` 将文档返回给 `Node 1` ,然后将文档返回给客户端。 在处理读取请求时,协调结点在每次请求的时候都会通过轮询所有的副本分片来达到负载均衡。 在文档被检索时,已经被索引的文档可能已经存在于主分片上但是还没有复制到副本分片。 在这种情况下,副本分片可能会报告文档不存在,但是主分片可能成功返回文档。 一旦索引请求成功返回给用户,文档在主分片和副本分片都是可用的。 ### 更新流程 ![局部更新文档](https://i.loli.net/2021/05/05/ZNjKmG28zkLYFwE.png) 以下是部分更新一个文档的步骤: 1. 客户端向 `Node 1` 发送更新请求。 2. 它将请求转发到主分片所在的 `Node 3` 3. `Node 3` 从主分片检索文档,修改 `_source` 字段中的 JSON ,并且尝试重新索引主分片的文档。 如果文档已经被另一个进程修改,它会重试步骤 3 ,超过 `retry_on_conflict` 次后放弃 4. 如果 `Node 3` 成功地更新文档,它将新版本的文档并行转发到 `Node 1` 和 `Node 2` 上的副本分片,重新建立索引。 一旦所有副本分片都返回成功, `Node 3` 向协调节点也返回成功,协调节点向客户端返回成功 ### 批量操作流程 `mget` 和 `bulk` API 的模式类似于单文档模式。区别在于协调节点知道每个文档存在于哪个分片中。 它将整个多文档请求分解成 *每个分片* 的多文档请求,并且将这些请求并行转发到每个参与节点。 **使用** `mget` **取回多个文档** ![“使用 `mget` 取回多个文档”](https://i.loli.net/2021/05/05/yaU4fvuI95hDwdV.png) 以下是使用单个 `mget` 请求取回多个文档所需的步骤顺序: 1. 客户端向 `Node 1` 发送 `mget` 请求。 2. `Node 1` 为每个分片构建多文档获取请求,然后并行转发这些请求到托管在每个所需的主分片或者副本分片的节点上。一旦收到所有答复, `Node 1` 构建响应并将其返回给客户端。 **使用** `bulk` **修改多个文档** ![“使用 `bulk` 修改多个文档”](https://i.loli.net/2021/05/05/tsnkbo3Lgw45fpl.png) `bulk` API 按如下步骤顺序执行: 1. 客户端向 `Node 1` 发送 `bulk` 请求。 2. `Node 1` 为每个节点创建一个批量请求,并将这些请求并行转发到每个包含主分片的节点主机。 3. 主分片一个接一个按顺序执行每个操作。当每个操作成功时,主分片并行转发新文档(或删除)到副本分片,然后执行下一个操作。 一旦所有的副本分片报告所有操作成功,该节点将向协调节点报告成功,协调节点将这些响应收集整理并返回给客户端。 ### 倒排索引 正排索引 - 目录结构: - 书的目录就是书的索引 ![img](https://i.loli.net/2021/05/05/3GvC4E9IFDp6fcX.png) 倒排索引: ![img](https://i.loli.net/2021/05/05/1RFToijrOuIeUAm.png) 图书和索引引擎的类比: - 图书 - 正排索引 - 目录页 - 倒排索引 - 索引页 - 搜索引擎 - 正排索引 - 文档 id 到文档内容和单词的关联 - 倒排索引 - 单词到文档 id 的关系 ![img](https://i.loli.net/2021/05/05/o4sDyEXcFM9kfJC.png) #### 倒排索引的核心组成 倒排索引包含两个部分: - 单词词典(Term Dictionary) ,记录所有文档的单词,记录单词到倒排列表的关联关系 单词词典比较大,可以通过 B + 树 或者 哈希拉链法实现,以满足高性能的插入与查询 - 倒排列表(Postion List)- 记录了单词对应的文档结合,由倒排索引项组成 倒排索引 文档 ID 词频 TF - 单词在文档中的分词的位置。用于语句搜索(phrase query) 偏移(Offset) - 记录单词的开始结束时间,实现高亮显示 ![img](https://i.loli.net/2021/05/05/kUygOo1LtcWxelH.png) --- 分片是Elasticsearch最小的工作单元。但是究竟什么是一个分片,它是如何工作的? 传统的数据库每个字段存储单个值,但这对全文检索并不够。文本字段中的每个单词需要被搜索,对数据库意味着需要单个字段有索引多值的能力。最好的支持是一个字段多个值需求的数据结构是倒排索引。 - Elasticsearch 的 JSON 文档中的每个字段,都有自己的倒排索引 - 可以指定对某些字段不做索引 - 优点:节省储存空间 - 缺点:字段无法被搜索 倒排索引原理 Elasticsearch使用一种称为倒排索引的结构,它适用于快速的全文搜索。 见其名,知其意,有倒排索引,肯定会对应有正向索引。正向索引(forward index),反向索引(inverted index)更熟悉的名字是倒排索引。 所谓的正向索引,就是搜索引擎会将待搜索的文件都对应一个文件ID,搜索时将这个ID和搜索关键字进行对应,形成K-V对,然后对关键字进行统计计数。(统计??下文有解释) ![img](https://i.loli.net/2021/05/05/3cKsjU6qiZdBEaG.png) 但是互联网上收录在搜索引擎中的文档的数目是个天文数字,这样的索引结构根本无法满足实时返回排名结果的要求。所以,搜索引擎会将正向索引重新构建为倒排索引,即把文件ID对应到关键词的映射转换为关键词到文件ID的映射,每个关键词都对应着一系列的文件,这些文件中都出现这个关键词。 ![img](https://i.loli.net/2021/05/05/n2jaSok6tFOThCb.png) #### 倒排索引的例子 一个倒排索引由文档中所有不重复词的列表构成,对于其中每个词,有一个包含它的文档列表。例如,假设我们有两个文档,每个文档的content域包含如下内容: ``` 1. The quick brown fox jumped over the lazy dog 2. Quick brown foxes leap over lazy dogs in summer ``` 为了创建倒排索引,我们首先将每个文档的content域拆分成单独的词(我们称它为词条或tokens ),创建一个包含所有不重复词条的排序列表,然后列出每个词条出现在哪个文档。结果如下所示: ![img](https://i.loli.net/2021/05/05/AQn2fcKFRO5jZ3G.png) 现在,如果我们想搜索 quick brown ,我们只需要查找包含每个词条的文档: ![img](https://i.loli.net/2021/05/05/8M9rApz13FOnDhd.png) 两个文档都匹配,但是第一个文档比第二个匹配度更高。如果我们使用仅计算匹配词条数量的简单相似性算法,那么我们可以说,对于我们查询的相关性来讲,第一个文档比第二个文档更佳。 但是,我们目前的倒排索引有一些问题: Quick和quick以独立的词条出现,然而用户可能认为它们是相同的词。 fox和foxes非常相似,就像dog和dogs;他们有相同的词根。 jumped和leap,尽管没有相同的词根,但他们的意思很相近。他们是同义词。 使用前面的索引搜索+Quick +fox不会得到任何匹配文档。(记住,+前缀表明这个词必须存在)。 只有同时出现Quick和fox 的文档才满足这个查询条件,但是第一个文档包含quick fox ,第二个文档包含Quick foxes 。 我们的用户可以合理的期望两个文档与查询匹配。我们可以做的更好。 如果我们将词条规范为标准模式,那么我们可以找到与用户搜索的词条不完全一致,但具有足够相关性的文档。例如: Quick可以小写化为quick。 foxes可以词干提取变为词根的格式为fox。类似的,dogs可以为提取为dog。 jumped和leap是同义词,可以索引为相同的单词jump 。 现在索引看上去像这样: ![img](https://i.loli.net/2021/05/05/RYjPl9MC6xAsUIH.png) 这还远远不够。我们搜索+Quick +fox 仍然会失败,因为在我们的索引中,已经没有Quick了。但是,如果我们对搜索的字符串使用与content域相同的标准化规则,会变成查询+quick +fox,这样两个文档都会匹配!分词和标准化的过程称为分析,这非常重要。你只能搜索在索引中出现的词条,所以索引文本和查询字符串必须标准化为相同的格式。 ### 分片内部原理 *分片*, 并将它 描述成最小的 *工作单元* 。但是究竟什么 *是* 一个分片,它是如何工作的? - 为什么搜索是 *近* 实时的? - 为什么文档的 CRUD (创建-读取-更新-删除) 操作是 *实时* 的? - Elasticsearch 是怎样保证更新被持久化在断电时也不丢失数据? - 为什么删除文档不会立刻释放空间? #### 文档搜索 必须解决的第一个挑战是如何使文本可被搜索。 传统的数据库每个字段存储单个值,但这对全文检索并不够。文本字段中的每个单词需要被搜索,对数据库意味着需要单个字段有索引多值(这里指单词)的能力。 最好的支持 *一个字段多个值* 需求的数据结构是倒排索引。 倒排索引包含一个有序列表,列表包含所有文档出现过的不重复个体,或称为 *词项* ,对于每一个词项,包含了它所有曾出现过文档的列表。 早期的全文检索会为整个文档集合建立一个很大的倒排索引并将其写入到磁盘。 一旦新的索引就绪,旧的就会被其替换,这样最近的变化便可以被检索到。 倒排索引被写入磁盘后是不可改变的:它永远不会修改。 1. 不需要锁。如果你从来不更新索引,你就不需要担心多进程同时修改数据的问题。 2. 一旦索引被读入内核的文件系统缓存,便会留在哪里,由于其不变性。只要文件系统缓存中还有足够的空间,那么大部分读请求会直接请求内存,而不会命中磁盘。这提供了很大的性能提升。 3. 其它缓存(像filter缓存),在索引的生命周期内始终有效。它们不需要在每次数据改变时被重建,因为数据不会变化。 4. 写入单个大的倒排索引允许数据被压缩,减少磁盘IO和需要被缓存到内存的索引的使用量。 当然,一个不变的索引也有不好的地方。主要事实是它是不可变的! 你不能修改它。如果你需要让一个新的文档可被搜索,你需要重建整个索引。这要么对一个索引所能包含的数据量造成了很大的限制,要么对索引可被更新的频率造成了很大的限制。 #### 动态更新索引 下一个需要被解决的问题是怎样在保留不变性的前提下实现倒排索引的更新?答案是: 用更多的索引。通过增加新的补充索引来反映新近的修改,而不是直接重写整个倒排索引。每一个倒排索引都会被轮流查询到,从最早的开始查询完后再对结果进行合并。 Elasticsearch基于Lucene,这个java库引入了按段搜索的概念。每一段本身都是一个倒排索引,但索引在 Lucene 中除表示所有段的集合外,还增加了提交点的概念—一个列出了所有已知段的文件。 当一个查询被触发,所有已知的段按顺序被查询。词项统计会对所有段的结果进行聚合,以保证每个词和每个文档的关联都被准确计算。 这种方式可以用相对较低的成本将新文档添加到索引。 #### 删除和更新 段是不可改变的,所以既不能从把文档从旧的段中移除,也不能修改旧的段来进行反映文档的更新。 取而代之的是,每个提交点会包含一个 `.del` 文件,文件中会列出这些被删除文档的段信息。 当一个文档被 “删除” 时,它实际上只是在 `.del` 文件中被 *标记* 删除。一个被标记删除的文档仍然可以被查询匹配到, 但它会在最终结果被返回前从结果集中移除。 文档更新也是类似的操作方式:当一个文档被更新时,旧版本文档被标记删除,文档的新版本被索引到一个新的段中。 可能两个版本的文档都会被一个查询匹配到,但被删除的那个旧版本文档在结果集返回前就已经被移除。 #### 近实时搜索 随着按段(per-segment)搜索的发展,一个新的文档从索引到可被搜索的延迟显著降低了。新文档在几分钟之内即可被检索,但这样还是不够快。磁盘在这里成为了瓶颈。提交(Commiting)一个新的段到磁盘需要一个fsync来确保段被物理性地写入磁盘,这样在断电的时候就不会丢失数据。但是fsync操作代价很大;如果每次索引一个文档都去执行一次的话会造成很大的性能问题。 我们需要的是一个更轻量的方式来使一个文档可被搜索,这意味着fsync要从整个过程中被移除。在Elasticsearch和磁盘之间是文件系统缓存。像之前描述的一样,在内存索引缓冲区中的文档会被写入到一个新的段中。但是这里新段会被先写入到文件系统缓存—这一步代价会比较低,稍后再被刷新到磁盘—这一步代价比较高。不过只要文件已经在缓存中,就可以像其它文件一样被打开和读取了。 Lucene允许新段被写入和打开,使其包含的文档在未进行一次完整提交时便对搜索可见。这种方式比进行一次提交代价要小得多,并且在不影响性能的前提下可以被频繁地执行。 在 Elasticsearch 中,写入和打开一个新段的轻量的过程叫做refresh。默认情况下每个分片会每秒自动刷新一次。这就是为什么我们说 Elasticsearch是近实时搜索:文档的变化并不是立即对搜索可见,但会在一秒之内变为可见。 这些行为可能会对新用户造成困惑:他们索引了一个文档然后尝试搜索它,但却没有搜到。这个问题的解决办法是用refresh API执行一次手动刷新:/usersl_refresh 尽管刷新是比提交轻量很多的操作,它还是会有性能开销。当写测试的时候,手动刷新很有用,但是不要在生产环境下每次索引一个文档都去手动刷新。相反,你的应用需要意识到Elasticsearch 的近实时的性质,并接受它的不足。 ![img](https://i.loli.net/2021/05/05/WifU21ORSaTyts4.png) ![clipboard.png](https://i.loli.net/2021/05/05/k8RPT5NlSiategb.png) (1)当分片所在的节点接收到来自协调节点的请求后,会将请求写入到 MemoryBuffffer,然后定时(默认是每隔 1 秒)写入到 Filesystem Cache,这个从 MomeryBuffffer 到 Filesystem Cache 的过程就叫做 refresh; (2)当然在某些情况下,存在 Momery Buffffer 和 Filesystem Cache 的数据可能会丢失,ES 是通过translog 的机制来保证数据的可靠性的。其实现机制是接收到请求后,同时也会写入到 translog 中 , 当 Filesystem cache 中的数据写入到磁盘中时,才会清除掉,这个过程叫做 flflush; (3)在 flflush 过程中,内存中的缓冲将被清除,内容被写入一个新段,段的 fsync将创建一个新的提交点,并将内容刷新到磁盘,旧的 translog 将被删除并开始一个新的 translog。 (4)flflush 触发的时机是定时触发(默认 30 分钟)或者 translog 变得太大(默认为 512M)时;补充:关于 Lucene 的 Segement: (1)Lucene 索引是由多个段组成,段本身是一个功能齐全的倒排索引。 (2)段是不可变的,允许 Lucene 将新的文档增量地添加到索引中,而不用从头重建索引。 (3)对于每一个搜索请求而言,索引中的所有段都会被搜索,并且每个段会消耗CPU 的时钟周、文件句柄和内存。这意味着段的数量越多,搜索性能会越低。 (4)为了解决这个问题,Elasticsearch 会合并小段到一个较大的段,提交新的合并段到磁盘,并删除那些旧的小段。 ![img](https://i.loli.net/2021/05/11/2ZvYuMqIGmQLW7j.png) #### 段合并 Lucene Index 原理 - 在 Lucene 中,单个倒排索引文件被称为 Segment。Segment 是自包含的,不可变更的。 多个 Segments 汇总在一起,称为 Lucene 的 Index,其对应的就是 ES 中的 Shard - 当有新文档写入时,并且执行 Refresh,就会生成一个新 Segment。 Lucene 中有一个文 件,用来记录所有 Segments 信息,叫做 Commit Point。查询时会同时查询所有 Segments,并且对结果汇总。 - 删除的文档信息,保存在 “.del” 文件中,查 询后会进行过滤。 - Segment 会定期 Merge,合并成一个,同时删 除已删除文档 ![img](https://i.loli.net/2021/05/05/WVnCxK196XPOsRp.png) 由于自动刷新流程每秒会创建一个新的段,这样会导致短时间内的段数量暴增。而段数目太多会带来较大的麻烦。每一个段都会消耗文件句柄、内存和 cpu运行周期。更重要的是,每个搜索请求都必须轮流检查每个段;所以段越多,搜索也就越慢。 Elasticsearch通过在后台进行段合并来解决这个问题。小的段被合并到大的段,然后这些大的段再被合并到更大的段。 段合并的时候会将那些旧的已删除文档从文件系统中清除。被删除的文档(或被更新文档的旧版本)不会被拷贝到新的大段中。 启动段合并不需要你做任何事。进行索引和搜索时会自动进行。 一、当索引的时候,刷新(refresh)操作会创建新的段并将段打开以供搜索使用。 二、合并进程选择一小部分大小相似的段,并且在后台将它们合并到更大的段中。这并不会中断索引和搜索。 三、一旦合并结束,老的段被删除 合并大的段需要消耗大量的 I/O 和 CPU 资源,如果任其发展会影响搜索性能。 Elasticsearch在默认情况下会对合并流程进行资源限制,所以搜索仍然有足够的资源很好地执行。 #### **Translog 有多安全?** translog 的目的是保证操作不会丢失。这引出了这个问题: Translog 有多安全? 在文件被 `fsync` 到磁盘前,被写入的文件在重启之后就会丢失。默认 translog 是每 5 秒被 `fsync` 刷新到硬盘, 或者在每次写请求完成之后执行(e.g. index, delete, update, bulk)。这个过程在主分片和复制分片都会发生。最终, 基本上,这意味着在整个请求被 `fsync` 到主分片和复制分片的translog之前,你的客户端不会得到一个 200 OK 响应。 在每次请求后都执行一个 fsync 会带来一些性能损失,尽管实践表明这种损失相对较小(特别是bulk导入,它在一次请求中平摊了大量文档的开销)。 但是对于一些大容量的偶尔丢失几秒数据问题也并不严重的集群,使用异步的 fsync 还是比较有益的。比如,写入的数据被缓存到内存中,再每5秒执行一次 `fsync` 。 ### 文档分析 分析包含下面的过程: 1. 将一块文本分成适合于倒排索引的独立的词条。 2. 将这些词条统一化为标准格式以提高它们的“可搜索性”,或者recall。 分析器执行上面的工作。分析器实际上是将三个功能封装到了一个包里: - 字符过滤器:首先,字符串按顺序通过每个 字符过滤器 。他们的任务是在分词前整理字符串。一个字符过滤器可以用来去掉 HTML,或者将 & 转化成 and。 - 分词器:其次,字符串被分词器分为单个的词条。一个简单的分词器遇到空格和标点的时候,可能会将文本拆分成词条。 - Token 过滤器:最后,词条按顺序通过每个 token 过滤器 。这个过程可能会改变词条(例如,小写化Quick ),删除词条(例如, 像 a, and, the 等无用词),或者增加词条(例如,像jump和leap这种同义词) #### 内置分析器 Elasticsearch还附带了可以直接使用的预包装的分析器。接下来我们会列出最重要的分析器。为了证明它们的差异,我们看看每个分析器会从下面的字符串得到哪些词条: ``` "Set the shape to semi-transparent by calling set_trans(5)" ``` - 标准分析器 标准分析器是Elasticsearch 默认使用的分析器。它是分析各种语言文本最常用的选择。它根据Unicode 联盟定义的单词边界划分文本。删除绝大部分标点。最后,将词条小写。它会产生: ``` set, the, shape, to, semi, transparent, by, calling, set_trans, 5 ``` - 简单分析器 简单分析器在任何不是字母的地方分隔文本,将词条小写。它会产生: ``` set, the, shape, to, semi, transparent, by, calling, set, trans ``` - 空格分析器 空格分析器在空格的地方划分文本。它会产生: ``` Set, the, shape, to, semi-transparent, by, calling, set_trans(5) ``` - 语言分析器 特定语言分析器可用于很多语言。它们可以考虑指定语言的特点。例如,英语分析器附带了一组英语无用词(常用单词,例如and或者the ,它们对相关性没有多少影响),它们会被删除。由于理解英语语法的规则,这个分词器可以提取英语单词的词干。 英语分词器会产生下面的词条: ``` set, shape, semi, transpar, call, set_tran, 5 ``` 注意看transparent、calling和 set_trans已经变为词根格式。 #### 分析器使用场景 当我们索引一个文档,它的全文域被分析成词条以用来创建倒排索引。但是,当我们在全文域搜索的时候,我们需要将查询字符串通过相同的分析过程,以保证我们搜索的词条格式与索引中的词条格式一致。 全文查询,理解每个域是如何定义的,因此它们可以做正确的事: - 当你查询一个全文域时,会对查询字符串应用相同的分析器,以产生正确的搜索词条列表。 - 当你查询一个精确值域时,不会分析查询字符串,而是搜索你指定的精确值。 #### Anaiysis 与 Analyzer - Analysis - 文本分析是吧全文本转换成一系列的单词(term /token)的过程,也叫分词 - Analysis 是通过 Analyzer 来实现的 - 可使用 Elasticesearch 内置的分析器 或者按需求定制化分析器 - 除了在数据写入时转换词条,匹配 Query 语句时候也需要用相同的分析器会查询语句进行分析 #### Analyzer 的组成 - 分词器是专门处理分词的组件,Analyzer 由三部分组成 - Character Filters (针对原始文本处理,例如去除 html) - Tokenizer(按照规则切分为单词) - Token Filter (将切分的单词进行加工,小写,删除 stopwords,增加同义语) ![img](https://i.loli.net/2021/05/05/AR5D62vxMg4BZU1.png) #### Elastocsearch 的内置分词器 - Standard Analyzer - 默认分词器,按词切分,小写处理 - Simple Analyzer - 按照非字母切分(符号被过滤),小写处理 - Stop Analyzer - 小写处理,停用词过滤(the ,a,is) - Whitespace Analyzer - 按照空格切分,不转小写 - Keyword Analyzer - 不分词,直接将输入当做输出 - Patter Analyzer - 正则表达式,默认 \W+ ### 文档控制 #### 文档冲突 当我们使用index API更新文档,可以一次性读取原始文档,做我们的修改,然后重新索引整个文档。最近的索引请求将获胜:无论最后哪一个文档被索引,都将被唯一存储在 Elasticsearch 中。如果其他人同时更改这个文档,他们的更改将丢失。 很多时候这是没有问题的。也许我们的主数据存储是一个关系型数据库,我们只是将数据复制到Elasticsearch中并使其可被搜索。也许两个人同时更改相同的文档的几率很小。或者对于我们的业务来说偶尔丢失更改并不是很严重的问题。 但有时丢失了一个变更就是非常严重的。试想我们使用Elasticsearch 存储我们网上商城商品库存的数量,每次我们卖一个商品的时候,我们在 Elasticsearch 中将库存数量减少。有一天,管理层决定做一次促销。突然地,我们一秒要卖好几个商品。假设有两个web程序并行运行,每一个都同时处理所有商品的销售。 web_1 对stock_count所做的更改已经丢失,因为 web_2不知道它的 stock_count的拷贝已经过期。结果我们会认为有超过商品的实际数量的库存,因为卖给顾客的库存商品并不存在,我们将让他们非常失望。 变更越频繁,读数据和更新数据的间隙越长,也就越可能丢失变更。在数据库领域中,有两种方法通常被用来确保并发更新时变更不会丢失: - 悲观并发控制:这种方法被关系型数据库广泛使用,它假定有变更冲突可能发生,因此阻塞访问资源以防止冲突。一个典型的例子是读取一行数据之前先将其锁住,确保只有放置锁的线程能够对这行数据进行修改。 - 乐观并发控制:Elasticsearch 中使用的这种方法假定冲突是不可能发生的,并且不会阻塞正在尝试的操作。然而,如果源数据在读写当中被修改,更新将会失败。应用程序接下来将决定该如何解决冲突。例如,可以重试更新、使用新的数据、或者将相关情况报告给用户。 #### 乐观并发控制 Elasticsearch是分布式的。当文档创建、更新或删除时,新版本的文档必须复制到集群中的其他节点。Elasticsearch也是异步和并发的,这意味着这些复制请求被并行发送,并且到达目的地时也许顺序是乱的。Elasticsearch需要一种方法确保文档的旧版本不会覆盖新的版本。 当我们之前讨论index , GET和DELETE请求时,我们指出每个文档都有一个_version(版本号),当文档被修改时版本号递增。Elasticsearch使用这个version号来确保变更以正确顺序得到执行。如果旧版本的文档在新版本之后到达,它可以被简单的忽略。 我们可以利用version号来确保应用中相互冲突的变更不会导致数据丢失。我们通过指定想要修改文档的 version号来达到这个目的。如果该版本不是当前版本号,我们的请求将会失败。 ### deep paging问题 deep paging简单来说,就是搜索的特别深,比如总共有60000条数据,现在有3个primary shard,每个shard上分20000条,每页是10条数据,这个时候你要搜索到第1000页,实际上要拿到的是10001-10010,该怎么拿呢? 请求首先可能是打到一个不包含这个index的shard的node上,这个node就是一个coordinate node,这个coordinate node就会将搜索请求转发到index的三个shard所在的node上去。 要搜索60000条数据中的第1000页,实际上每个shard都要将内部的20000条数据中的第1-10010条数据拿出来,不是10条,是10010条数据,3个shard每个shard都返回10010条数据给coordinate node,coordinate node会收到总共30030条数据,然后排序取到所需的那10条数据,其实就是我们要的最后的第1000页的10条数据。 举个例子,现在有60个带编号的球(从1到60),我现在随机给他们放到三个篮子里面(他们在篮子里面已经排好序了),现在我要取出第10-12个球,那我是不是应该先把各个篮子里面前12个球取出来放到一起(篮子里面的球是随机放的,无规律),共计36个球,然后汇总进行排序后,在这个结果中取出第10-12个球!!! #### 缺点 搜索过深的时候就需要在coordinate node上保存大量的数据,还要进行大量数据的排序,排序之后再取出对应的那一页,所以这个过程,既消耗网络宽带,耗费内存,还消耗cpu。这就是deep paging的性能问题,我们应该尽量避免出现这种deep paging操作。 #### 解决方案 为了解决上面的问题,elasticsearch提出了一个scroll滚动的方式,这个滚动的方式原理就是通过每次查询后,返回一个scroll_id。根据这个scroll_id 进行下一页的查询。可以把这个scroll_id理解为通常关系型数据库中的游标。但是,这种scroll方式的缺点是不能够进行反复查询,也就是说,只能进行下一页,不能进行上一页。 经过分析,如果数据达到了50000条以上,那么用户基本上是不会考虑每条都去看的,用户需要的是最后对数据分析处理后的结果。而如果小于50000条的时候我们可以使用from size的方式进行分页的查询。那么这种方式存在是为了什么情景呢。应该是为了分批次的检索所有数据。 https://blog.csdn.net/gwd1154978352/article/details/82943037 ### filter执行原理深度剖析 https://blog.csdn.net/gwd1154978352/article/details/83786721 ### 精准全文检索 https://blog.csdn.net/gwd1154978352/article/details/83826267 # api ``` # 索引-创建 PUT /shopping GET _cat # 查看所有索引 GET _cat/indices?v # 查看单个索引 GET /shopping # 删除索引 DELETE /shopping # 索引映射 PUT /shopping/_mapping {"properties":{"category":{"type":"keyword"},"images":{"type":"keyword"},"price":{"type":"double"},"title":{"type":"text"}}} # 查看映射 GET /shopping/_mapping # 文档-创建(Put & Post) POST /shopping/_doc {"title":"小米手机","category":"小米","images":"http://www.xxxx.com/xm.jpg","price":3999} PUT /shopping/_create/1 {"title":"苹果手机","category":"苹果","images":"http://www.xxxx.com/pg.jpg","price":8888} # 文档查询-主键查询 GET /shopping/_doc/1 # 查看索引下所有文档数据 GET /shopping/_search # 文档全量修改 会将原有的数据内容覆盖 POST /shopping/_doc/1 {"title":"华为手机","category":"华为","images":"http://www.gulixueyuan.com/hw.jpg","price":1999} # 文档局部修改 POST /shopping/_update/1 {"doc":{"title":"小米手机","category":"小米"}} # 文档删除 DELETE /shopping/_doc/1 # 条件查询-查询排序 GET /shopping/_search {"query":{"match_all":{}},"sort":{"price":{"order":"desc"}}} # 多条件查询-小米牌子,价格为3999元 #bool:must(and),should(or),must_not(not)和filter GET /shopping/_search {"query":{"bool":{"must":[{"match":{"category":"小米"}},{"match":{"price":3999}}]}}} GET /shopping/_search {"query":{"bool":{"should":[{"match":{"category":"小米"}},{"match":{"category":"华为"}}]}}} # gt lt gte lte GET /shopping/_search {"query":{"bool":{"filter":[{"range":{"price":{"gt":3000}}}]}}} # 全文检索 GET /shopping/_search {"query":{"match":{"category":"小米"}}} GET /_analyze {"analyzer":"ik_max_word","text":"小米手机"} GET /shopping/_search {"query":{"match_phrase":{"title":"小米"}}} # 精确匹配 GET /shopping/_search {"query":{"term":{"category":"小米"}}} # 聚合查询 GET /shopping/_search {"size":0,"aggs":{"price_group":{"terms":{"field":"price"}}}} # 平均值 GET /shopping/_search {"size":0,"aggs":{"price_avg":{"avg":{"field":"price"}},"price_max":{"max":{"field":"price"}}}} # 高亮查询 GET movie/_search {"query":{"match":{"title":"肖申克"}},"highlight":{"fields":{"title":{}},"pre_tags":[""],"post_tags":[""]}} ```