1 Elastic Stack简介

什么是ELK Stack / Elastic Stack?

ELK是三个开源项目的首字母缩写,这三个项目分别是:Elasticsearch、LogstashKibanaimage.png 2015年,ELK Stack中加入了一系列轻量型的单一功能数据采集器,并把它们叫做Beats。 加入Beats后,ELK Stack这个名字并没有变成BELK、BLEK等等,而是更名为Elastic Stack

为什么需要日志系统?

  • 一般我们需要进行日志分析时,直接在日志文件中使用grep、awk指令就可以获得自己想要的信息。但在大型系统中,此方法效率低下,面临的问题包括日志量太大如何归档、文本搜索太慢怎么办、如何多维度查询等。
  • 为了应对大型系统的日志分析,我们需要集中化的日志管理,将所有服务器上的日志收集汇总。常见解决思路是建立集中式日志收集系统,将所有节点上的日志统一收集、管理、访问。
  • 一般大型系统是一个分布式部署的架构,不同的服务模块部署在不同的服务器上。问题出现时,我们需要根据问题暴露的关键信息,定位到具体的服务器和服务模块。构建一套集中式日志系统,可以提高定位问题的效率。
  • 一个完整的集中式日志系统,需要包含以下几个主要特点:
    • 收集 - 能够采集多种来源的日志数据
    • 传输 - 能够稳定的把日志数据传输到中央系统
    • 存储 - 如何存储日志数据
    • 分析 - 可以支持UI分析
    • 警告 - 能够提供错误报告,监控机制
  • Elastic Stack概述
    • Elastic Stack能够安全可靠地获取任何来源、任何格式的数据,然后实时地对数据进行搜索、分析和可视化,是目前主流的日志系统。
    • Elastic Stack包括Elasticsearch、Logstash、KibanaBeats

Elasticsearch

  1. - Elasticsearch是个开源分布式搜索和分析引擎

Logstash

  - Logstash是一个开源的服务器端数据处理管道,在将数据索引到Elasticsearch之前**同时从多个来源采集数据**,并**对数据进行充实和转换**。
  - Logstash一般工作方式为C/S架构,客户端安装在需要收集日志的主机上,服务端负责将收到的各节点日志进行过滤、修改等操作然后发送到Elasticsearch。

Kibana

  - Kibana是一款适用于Elasticsearch的数据可视化和管理工具,可以提供实时的直方图、线形图、饼状图和地图

Beats

  - Beats包含一系列轻量型的单一功能数据采集器,如Filebeat(搜集文件数据)、Topbeat(搜集系统、进程和文件系统级别的 CPU 和内存使用情况等数据)等
  - Beats可以代替Logstash向Elasticsearch传数据
  • Elastic Stack的工作原理
    1. 利用Logstash或Beats,可以将原始数据从多个来源(包括日志、系统指标和网络应用程序等)输入到ES中。数据在ES索引之前会经过Logstash的数据采集过程,包括解析、标准化和充实原始数据。
    2. ES接收到数据后,将为ES构建倒排索引,索引构建完成之后,用户便可针对他们的数据运行复杂的查询,并使用聚合来检索自身数据的复杂汇总。
    3. 在Kibana中,用户可以基于自己的数据创建强大的可视化仪表板,并对Elastic Stack进行管理。

2 全文搜索

  • 全文搜索(full-text search)定义

    • 全文检索是指计算机索引程序通过扫描文章中的每一个词,对每一个词建立一个索引,指明该词在文章中出现的次数和位置
    • 当用户查询时,检索程序就根据事先建立的索引进行查找,并将查找的结果反馈给用户的检索方式。
    • 全文搜索的过程类似于通过字典中的检索字表查字的过程
  • 全文搜索的应用

    • Google、百度等搜索网站都是根据网页中的关键字生成索引。我们在搜索时输入关键字,搜索网站会将该关键字即索引匹配到的所有网页返回;
    • 在项目中对应用日志的搜索也属于全文搜索。
  • 全文搜索与关系型数据库

    • 对于非结构化的数据文本,关系型数据库搜索不是能很好的支持
    • 一般传统数据库,全文检索都实现的很鸡肋,因为一般也没人用数据库存文本字段。
    • 进行全文检索需要扫描整个表,如果数据量大的话即使对SQL的语法进行优化,也收效甚微。

3 Elasticsearch简介

  • Elasticsearch概述

    • Elaticsearch简称为ES,是由Java开发的开源的高扩展的分布式搜索引擎,适用于包括文本、数字、地理空间、结构化和非结构化数据等在内的所有类型的数据,是整个Elastic Stack技术栈的核心。
    • ES提供强大且全面的 REST API 集合,这些 API 可用来执行各种任务,例如检查集群的运行状况、针对索引执行CRUD和搜索操作,以及执行诸如筛选和聚合等高级搜索操作。
    • ES的特点有:分布式、零配置、自动发现、索引自动分片、索引副本机制、RESTful风格接口、多数据源、自动搜索负载等。
  • ES的用途

Elasticsearch在速度和可扩展性方面都表现出色,而且还能够索引多种类型的内容,这意味着其可用于多种用例:应用程序搜索、网站搜索、日志处理和分析、应用程序性能监测、地理空间数据分析和可视化、安全分析、业务分析

  • ES的特点

速度快

  • 由于Elasticsearch是在Lucene基础上构建而成的,所以在全文本搜索方面表现十分出色
  • Elasticsearch同时还是一个近实时的搜索平台,这意味着从文档索引操作到文档变为可搜索状态之间的延时很短,一般只有一秒。因此,Elasticsearch非常适用于对时间有严苛要求的用例,例如安全分析和基础设施监测。

具有分布式的本质特征

  • Elasticsearch中存储的文档(属于同一个索引的文档)分布在不同的容器中,这些容器称为分片,可以进行复制以提供数据冗余副本,以防发生硬件故障。
  • Elasticsearch的分布式特性使得它可以扩展至数百台(甚至数千台)服务器,并处理PB量级的数据。

包含一系列广泛的功能

  • 除了速度、可扩展性和弹性等优势以外,Elasticsearch还有大量强大的内置功能(如数据汇总和索引生命周期管理),可以方便用户更加高效地存储和搜索数据。

Elastic Stack简化了数据采集、可视化和报告过程

  • 通过与Beats和Logstash进行集成,用户能够在向Elasticsearch中索引数据前轻松地获取和处理数据。
  • 同时,Kibana不仅可针对Elasticsearch数据提供实时可视化,同时还提供UI界面以便用户快速访问应用程序性能监测 (APM)、日志和基础设施指标等数据。
  • ES应用实例

GitHub

  • 2013 年初抛弃了Solr,采取Elasticsearch来做PB级的搜索。
  • GitHub 使用Elasticsearch搜索20TB的数据,包括13亿文件和1300亿行代码。

维基百科

  • 以Elasticsearch为基础的核心搜索架构

SoundCloud

  • SoundCloud使用Elasticsearch为1.8亿用户提供即时而精准的音乐搜索服务。

百度

  • 目前广泛使用Elasticsearch作为文本数据分析

新浪

  • 使用Elasticsearch分析处理32亿条实时日志。

阿里

  • 使用Elasticsearch构建日志采集和分析体系

4 核心概念

1 Elasticsearch与MySQL对比

  • 对照关系表 | Elasticsearch | MySQL | | —- | —- | | 文档 | 行 | | 文档中的字段 | 列 | | 索引 | 表 | | 映射 | 表结构 |
  • ES的索引与MySQL的索引
    • MySQL中的索引是为了优化查询而建立的数据结构,没有索引也能完成查询
    • ES专门用于全文查询,因此索引是ES的关键,查询也是基于索引进行的

2 文档(Document)与字段(Field)

  • 文档概述

    • 文档相当于数据库表的行
    • ES中的数据是一个个文档,文档以JSON格式存储
    • 一个文档是一个可被索引的基础信息单元,也就是一条数据
  • 字段

    • 字段相当于数据库表的列
    • 字段是**key:value**格式,ES执行全文搜索时是针对字段的值来进行的

3 索引(Index)

  • 索引概述
    • 一个索引就是一个具有相似特征的文档的集合,比如说,我们可以有一个客户数据的索引、一个产品目录的索引、一个订单数据的索引等
    • 索引由小写字母组成的名字来标识

当要对索引中的文档进行索引、搜索、更新和删除的时候,都要使用到索引的名字

  • ES使用了一种名为倒排索引的数据结构,这一结构的设计可以允许十分快速地进行全文本搜索(full-text search)
  • ES会为每个文档构建倒排索引,这样用户便可以近实时地对文档数据进行搜索
  • 正排索引

实例

  • 现有两个文档,分别是[my name is cui yichen]和[my name is qiu xuejing]
  • 建立正排索引如下 | id(key, index) | file(value) | | —- | —- | | 1001 | my name is cui yichen | | 1002 | my name is qiu xuejing |

正排索引的特点

  • 将key作为主键建立正排索引,之后就可以根据key快速查询key相关联的file
  • 关系数据库中的索引基本都是正排索引

正排索引进行全文检索的问题

  • 当需要进行全文检索时就无法利用正排索引了,而是需要遍历所有file进行模糊查询,资源消耗极大
  • 例如,当我们想查询包括name的file时,只能遍历上述两个文档的全部内容
  • 倒排索引
    • 倒排索引会列出在所有文档中出现的每个特有词汇,并且可以找到包含每个词汇的全部文档。

实例

keyword(key) id(value)
name 1001, 1002
cui 1001
yichen 1001

倒排索引特点

  • 倒排索引为文章中的关键字建立索引,与正排索引正好相反
  • 根据倒排索引查询id,再根据id查询关联的文章,这样就可以实现快速的全文检索

4 映射(Mapping)

  • 映射概述

    • ES中的索引类似于MySQL中的表,而映射类似于MySQL中的表结构
    • 映射对ES处理数据的方式和规则做了一些限制,如:文档某个字段的数据类型、默认值、分析器、是否被索引等等,这些都是映射里面可以设置的
    • 按照最优规则处理数据对性能提高很大,因此才需要建立映射,并且需要思考如何建立映射才能。
  • 映射中字段可以设置的属性

  1. type

表示字段中值的类型,常用类型如下
String:字符串类型,分以下两种

  • text(默认):字段值将被分词后作为倒排索引的关键字
  • keyword:完整的字段值作为倒排索引的关键字

Numerical:数值类型,分为以下两类

  • 基本数据类型:long、integer、short、byte、double、float、half_float
  • 浮点数的高精度类型:scaled_float

Date:日期类型
Array:数组类型
Object:对象

  1. index

表示字段是否索引,默认为true,因此不进行任何映射配置时所有字段都会被索引。

  • true:字段会被索引,可以用来进行搜索
  • false:字段不会被索引,不能用来搜索

5 Elasticsearch集群

1 集群简介

  • 集群概述

    • 一个运行中的ES实例称为一个节点,而集群是由一个或者多个拥有相同cluster.name配置的节点组成, 它们共同承担数据和负载的压力(每个节点持有几乎不同的分片)
    • 有节点加入集群中或者从集群中移除节点时(水平伸缩)集群将会自动重新平均分布所有的数据
    • 集群由ES管理,使得集群中的一切操作对用户来说是透明的。
  • 主节点与数据节点

    • ES集群中存在两类节点,分别是主节点(master node)和数据节点(data node)

主节点可读可写、数据节点只可读不可写

和Redis集群一样,Redis集群主可读可写、从可读不可写

  • 主节点将负责管理集群范围内的所有变更,例如增加、删除索引,或者增加、删除节点等,但是主节点并不需要涉及到文档级别的变更和搜索等操作
  • 分配给节点的分片包括主分片和副本,如果分配的是副本,分片过程还包含从主分片复制数据的过程。整个分配分片的过程是由集群中的master节点完成的。
  • 一个节点可以既作为主节点又作为数据节点
  • 用户可以将请求发送到集群中的任何节点 ,包括主节点。每个节点都知道任意文档所处的位置,并且能够将请求直接转发到存储我们所需文档的节点。
  • 无论用户将请求发送到哪个节点,它都能负责从各个包含用户所需文档的节点收集回数据,并将最终结果返回給客户端。

2 分片(Shards)

类似于Kafka分区的概念

  • 索引完整存储在单机存在的问题

    • 一个索引可以存储超出单个节点硬件限制的大量数据。比如,一个具有10亿文档数据的索引占据1TB的磁盘空间。这就可能导致任一节点都可能没有这样大的磁盘空间
    • 单个节点处理针对庞大索引的搜索请求响应太慢
  • 分片概述

    • 为了解决单机存储索引所导致的问题,ES提供了将索引划分成多份的能力,每一份就称之为分片。有了分片后,就可以将一个索引放到一个集群中。
    • 每个分片本身也是一个功能完善且独立的“索引”,因此分片可以被放置到集群中的任何节点上
    • 一个分片怎样分布、搜索怎样执行、怎样汇总结果等,都是完全由ES管理的,对于用户来说是透明的,无需过分关心。
  • ES分片后的工作流程

    • 实际上一个分片就是一个Lucene索引,一个ES索引则是分片的集合
    • 当ES在索引中搜索的时候,其会发送查询到每一个属于ES索引的分片(Lucene 索引),然后合并每个分片的结果到一个全局的结果集

image.png

  • 分片的好处
    • 允许水平 分割/扩展 内容容量。
    • 允许在分片之上进行分布式的、并行的操作,进而提高性能/吞吐量。

3 副本(Replicas)

类似Kafka中分区的副本,但是Kafka中客户端不能与的副本交互,但是ES中客户端可以与副本交互

  • 副本概述
    • 在一个网络 / 云的环境里,失败随时都可能发生,某个分片/节点可能不知怎么的就处于离线状态或者由于某些原因消失了
    • 这种情况下,有一个故障转移机制是非常有必要的。因此,ES允许创建分片的一份或多份拷贝,这些拷贝叫做副本
    • 一个分片可以被复制0次或多次。一旦复制了,索引就有了主分片(作为复制源的原来的分片)和复制分片(主分片的拷贝)之别。
    • 为了提高可用性,副本不应与原/主要(original/primary)分片置于同一节点,如下

image.png

  • 副本的好处
    • 在分片/节点不可用的情况下,提供了高可用性
    • 扩展了搜索量/吞吐量,因为搜索可以在所有的副本上并行运行

4 分片路由

  • 分片路由公式
    • 当索引一个文档的时候,文档会被存储到一个主分片中,ES如何知道一个文档应该存放到哪个分片中呢?首先这肯定不会是随机的,否则将来要获取文档的时候我们就不知道从何处寻找了。
    • 实际上,分片路由是根据下面这个公式决定

shard = hash(routing) % number_of_primary_shards

  - **routing是一个可变值,默认是文档的 _id **,也可以设置成一个自定义的值。
  - routing通过hash函数生成一个数字,然后这个数字再除以number_of_primary_shards (主分片的数量)后得到余数
  - 这个这个分布在0到number_of_primary_shards - 1之间的余数,就是我们所寻求的文档所在分片的位置
  • 自定义routing

    • 所有的文档API都接受一个叫做routing的路由参数 ,通过这个参数我们可以自定义文档到分片的映射
    • 一个自定义的路由参数可以用来确保所有相关的文档(例如所有属于同一个用户的文档)都被存储到同一个分片中
  • 索引创建后分片数量不可变

分片路由公式解释了为什么我们要在创建索引的时候就确定好主分片的数量并且永远不会改变这个数量,因为如果数量变化了,那么所有之前路由的值都会无效,文档也就再也找不到了。

5 查询路由 / 协调节点

不使用routing查询
在查询的时候因为不知道要查询的数据具体在哪个分片上,所以整个过程分为2个步骤

  1. 分发:请求到达协调节点后,协调节点将查询请求分发到每个分片上。
  2. 聚合:协调节点搜集到每个分片上查询结果,在将查询的结果进行排序,之后给用户返回结果。

协调节点(coordinating node)的功能

  • 诸如搜索请求或批量索引请求之类的请求可能涉及保存在不同数据节点上的数据。 例如,搜索请求在两个阶段中执行(query 和 fetch),这两个阶段由接收客户端请求的节点 - 协调节点协调。
  • 在请求阶段,协调节点将请求转发到保存数据的数据节点。 每个数据节点在本地执行请求并将其结果返回给协调节点。 在收集fetch阶段,协调节点将每个数据节点的结果汇集为单个全局结果集。
  • 每个节点都隐式地是一个协调节点。 这意味着将所有三个node.master,node.data和node.ingest设置为false的节点作为仅用作协调节点,无法禁用该节点。 结果,这样的节点需要具有足够的内存和CPU以便处理收集阶段。

使用routing查询
查询的时候,可以直接根据给定的routing信息定位到某个分片查询,不需要查询所有的分片
比如进行用户查询时,如果routing设置为userid的话,就可以直接找到特定分片,效率提升很多。

请求可以发送到集群任意节点

  • 当我们需要查询某个索引时,因为每个节点都是协调节点,因此都知道集群中任一文档的位置,所以我们可以发送请求到集群中的任一节点,收到请求的节点会直接将请求转发到需要的节点上。
  • 当发送请求的时候, 为了扩展负载,更好的做法是轮询集群中所有的节点

6 动态水平扩容

  • 水平扩容的时机

    • 主分片和副本的数量可以在索引创建的时候指定
    • 在索引创建之后,可以在任何时候动态地改变副本的数量,但不能改变主分片的数量
  • 动态水平扩容

    • 主分片的数目在索引创建时就已经确定了下来。实际上,这个数目定义了这个索引能够存储的最大数据量(实际大小取决于数据、硬件和使用场景),因此写操作没办法进行动态水平扩容。
    • 读操作(即搜索)可以同时被主分片或副本分片处理,而副本是可以在索引创建后新增的,因此读操作可以实现动态水平扩容。
    • 拥有越多的副本分片时,也将拥有越高的吞吐量。当然,如果只是在相同节点数目的集群上增加更多的副本分片并不能提高性能,因为每个分片从节点上获得的资源会变少,此时需要增加更多的硬件资源来提升吞吐量。

7 应对故障

通过一个实例来了解ES应对故障的方式

初始状态
image.png

  • 初始时集群有3个节点,有3个主分片,每个主分片有2个副本分片

关闭节点1
image.png

  • 我们关闭的节点是一个主节点,而集群必须拥有一个主节点来保证正常工作,所以发生的第一件事情就是选举一个新的主节点:Node 3 。
  • 在我们关闭Node 1的同时也失去了主分片1和2,并且在缺失主分片的时候索引也不能正常工作。 如果此时来检查集群的状况,我们看到的状态将会为red:不是所有主分片都在正常工作(但是该状态持续时间太短,在可视化界面很难捕捉到)。
  • 幸运的是,在其它节点上存在着这两个主分片的完整副本, 所以新的主节点立即将这些分片在Node 2和Node 3上对应的副本分片提升为主分片, 此时集群的状态将会为yellow。这个提升主分片的过程是瞬间发生的,如同按下一个开关一般。

为什么集群的状态是yellow而不是green?

  • 虽然我们拥有所有的三个主分片,但是ES设置了每个主分片需要对应2份副本分片,而此时只存在一份副本分片,所以集群不能为green的状态
  • 如果我们重新启动Node 1,集群可以将缺失的副本分片再次进行分配,那么集群的状态也将恢复成之前的状态。
  • 如果Node 1依然拥有着之前的分片,它将尝试去重用它们,同时仅从主分片复制发生了修改的数据文件。和之前的集群相比,只是Master节点切换了。

image.png

6 Elasticsearch执行流程

1 写流程

  • 新建索引删除请求都是写操作, 必须在主分片上完成之后才能被复制到相关的副本分片

新建、索引和删除文档所需要的步骤顺序
image.png

  1. 客户端向Node 1发送新建、索引或者删除请求
  2. 节点使用文档的_id确定文档属于分片0。请求会被转发到Node 3,因为分片0的主分片目前被分配在Node 3上。
  3. Node 3在主分片上执行请求。如果成功,它将请求并行转发到Node 1和Node 2的副本分片上。一旦所有的副本分片都报告成功, Node 3将向协调节点Node 1报告成功,协调节点向客户端报告成功。
  • 在客户端收到成功响应时,文档变更已经在主分片和所有副本分片执行完成,变更是安全的。

2 读流程

  • 我们可以从主分片或者从其它任意副本分片检索文档

从主分片或者副本分片检索文档的步骤顺序
image.png

  1. 客户端向Node 1发送获取请求。
  2. 节点使用文档的_id来确定文档属于分片 0。

分片0的副本分片存在于所有的三个节点上。 根据负载均衡,Node 1将请求转发到Node 2。

  1. Node 2将文档返回给Node 1 ,然后Node 1将文档返回给客户端。
  • 在处理读取请求时,协调节点在每次请求的时候都会通过轮询所有的副本分片来达到负载均衡
  • 在文档被检索时,已经被索引的文档可能已经存在于主分片上但是还没有复制到副本分片。在这种情况下,副本分片可能会报告文档不存在,但是主分片可能成功返回文档。
  • 一旦索引请求成功返回给用户,文档在主分片和副本分片都是可用的。

3 更新流程

  • 部分更新一个文档结合了先前说明的读取和写入流程

部分更新一个文档的步骤如下
image.png

  1. 客户端向Node 1发送更新请求
  2. Node 1将请求转发到主分片所在的Node 3。
  3. Node 3从主分片检索文档,修改_source字段中的JSON ,并且尝试重新索引主分片的文档。如果文档已经被另一个进程修改,它会重试步骤3,超过retry_on_conflict次后放弃。
  4. 如果Node 3成功地更新文档,它将新版本的文档并行转发到Node 1和Node 2上的副本分片,重新建立索引。一旦所有副本分片都返回成功,Node 3向协调节点也返回成功,协调节点向客户端返回成功。
  • 当主分片把更改转发到副本分片时,它不会转发更新请求。相反,它转发完整文档的新版本
  • 这些更改将会异步转发到副本分片,并且不能保证它们以发送它们相同的顺序到达。如果Elasticsearch仅转发更改请求,则可能以错误的顺序应用更改,导致得到损坏的文档

4 多文档操作流程

  • mget和bulk API的模式类似于单文档模式。区别在于协调节点知道每个文档存在于哪个分片中,它将整个多文档请求分解成每个分片的多文档请求,并且将这些请求并行转发到每个参与节点。
  • 协调节点一旦收到来自每个节点的应答,就将每个节点的响应收集整理成单个响应,返回给客户端

用单个mget请求取回多个文档所需的步骤如下
image.png

  1. 客户端向Node 1发送mget请求。
  2. Node 1 为每个分片构建多文档获取请求,然后并行转发这些请求到托管在每个所需的主分片或者副本分片的节点上。一旦收到所有答复, Node 1 构建响应并将其返回给客户端。

bulk API允许在单个批量请求中执行多个创建、索引、删除和更新请求,bulk API 按如下步骤执行
image.png

  1. 客户端向Node 1发送 bulk 请求。
  2. Node 1 为每个节点创建一个批量请求,并将这些请求并行转发到每个包含主分片的节点主机。
  3. 主分片一个接一个按顺序执行每个操作。当每个操作成功时,主分片并行转发新文档(或删除)到副本分片,然后执行下一个操作。 一旦所有的副本分片报告所有操作成功,该节点将向协调节点报告成功,协调节点将这些响应收集整理并返回给客户端。

7 Elasticsearch核心原理

1 倒排索引

  • 倒排索引(也称为反向索引)概述
    • 传统的数据库每个字段存储单个值,但这对全文检索并不够。文本字段中的每个单词需要被搜索,对数据库意味着需要单个字段有索引多值的能力。
    • 最好的支持是一个字段索引多个值需求的数据结构是倒排索引,适用于快速的全文搜索

Elasticsearch使用的就是倒排索引的结构

  • 正向索引与倒排索引
    • 见其名,知其意,有倒排索引,肯定会对应有正向索引。
    • 所谓的正向索引,就是搜索引擎会将待搜索的文件都对应一个文件ID将文件ID和文件中的关键字进行对应,形成K-V对

image.png

  • 搜索时需要遍历每个索引,然后根据搜索关键字找到对应的文件ID,进而找到文件

    关系数据库中使用的索引也都是正向索引

  • 互联网上收录在搜索引擎中的文档的数目是个天文数字,这样的索引结构根本无法满足实时返回排名结果的要求。

  • 所以,搜索引擎会将正向索引重新构建为倒排索引,即把文件ID对应到关键词的映射转换为关键词到文件ID的映射,每个关键词都对应着一系列的文件, 这些文件中都出现这个关键词

image.png

  • 倒排索引实例
    • 一个倒排索引由文档中所有不重复词的列表构成,对于其中每个词,有一个包含它的文档列表。

例如,假设我们有两个文档,每个文档的 content 域包含如下内容:

  - `The quick brown fox jumped over the lazy dog `
  - `Quick brown foxes leap over lazy dogs in summer `
  • 为了创建倒排索引,我们首先将每个文档的 content 域拆分成单独的词(我们称它为词条或tokens),创建一个包含所有不重复词条的排序列表,然后列出每个词条出现在哪个文档。结果如下所示:

image.png

  • 现在,如果我们想搜索quick brown,我们只需要查找包含每个词条的文档

image.png
两个文档都匹配,但是第一个文档比第二个匹配度更高。如果我们使用仅计算匹配词条数量的简单相似性算法,那么我们可以说,对于我们查询的相关性来讲,第一个文档比第二个文档更佳。

但是,我们目前的倒排索引有一些问题:

  1. Quick和quick以独立的词条出现,然而用户可能认为它们是相同的词。
  2. fox和foxes非常相似,就像dog和dogs;他们有相同的词根。
  3. jumped和leap,尽管没有相同的词根,但他们的意思很相近。他们是同义词。
  • 使用前面的索引搜索+Quick +fox(+ 前缀表明这个词必须存在)不会得到任何匹配文档。只有同时出现Quick和fox的文档才满足这个查询条件,但是第一个文档包含quick fox ,第二个文档包含 Quick foxes 。用户可以合理的期望两个文档与查询匹配。
  • 如果我们将词条规范为标准模式,那么我们可以找到与用户搜索的词条不完全一致,但具有足够相关性的文档。如:
    • Quick小写化为quick
    • foxes词干提取变为词根的格式fox;类似的,dogs为提取为dog
    • jumped和leap是同义词,可以索引为相同的单词jump
  • 现在索引看上去像这样:

image.png

  • 到目前位置我们搜索+Quick +fox仍然 会失败,因为在我们的索引中已经没有Quick了。此时可以对搜索的字符串使用与 content 域相同的标准化规则,这样+Quick +fox会变成查询+quick +fox,这样两个文档都会匹配!
  • 分词和标准化的过程称为分析。分析非常重要,因为只能搜索在索引中出现的词条,所以索引文本和查询字符串必须标准化为相同的格式

2 文档搜索

  • 早期的全文检索会为整个文档集合建立一个很大的倒排索引并将其写入到磁盘。 一旦新的索引就绪,旧的就会被其替换,这样最近的变化便可以被检索到。

  • 倒排索引被写入磁盘后是不可改变的。不变性有重要的价值:

    • 不需要锁

如果索引不更新,就不需要担心多进程同时修改数据的问题。

  • 一旦索引被读入内核的文件系统缓存,便会留在哪里

由于其不变性。只要文件系统缓存中还有足够的空间,那么大部分读请求会直接请求内存,而不会命中磁盘,这提供了很大的性能提升。

  • 其它缓存(像filter缓存),在索引的生命周期内始终有效。它们不需要在每次数据改变时被重建,因为数据不会变化。
  • 写入单个大的倒排索引允许数据被压缩,减少磁盘I/O和需要被缓存到内存的索引的使用量。
  • 当然,一个不变的索引也有不好的地方

主要事实是它是不可变的。如果你需要让一个新的文档可被搜索,就需要重建整个索引。这要么对一个索引所能包含的数据量造成了很大的限制,要么对索引可被更新的频率造成了很大的限制。

3 动态更新索引

  • 如何在保留不变性的前提下实现倒排索引的更新?答案是用更多的索引。

通过增加新的补充索引来反映新近的修改,而不是直接重写整个倒排索引。每一个倒排索引都会被轮流查询到,从最早的索引开始查询完后再对结果进行合并。

  • Elasticsearch基于Lucene,其引入了按段(per-segment)搜索的概念,每一段本身就是一个倒排索引

索引在Lucene中除表示所有段的集合外,还增加了提交点的概念:一个列出了所有已知段的文件
image.png

  • 按段搜索会以如下流程执行:

    1. 新文档被收集到内存索引缓存

    image.png

    1. 不时地,缓存被提交(commit):
      • 一个新的段,即一个追加的倒排索引被写入磁盘
      • 一个新的包含新段名字的提交点被写入磁盘
      • 磁盘进行同步:所有在文件系统缓存中等待的写入都刷新到磁盘,以确保它们被写入物理文件
    2. 新的段被开启,让它包含的文档可见以被搜索
    3. 内存缓存被清空,等待接收新的文档

image.png

  • 当一个查询被触发,所有已知的段按顺序被查询。词项统计会对所有段的查询结果进行聚合,以保证每个词和每个文档的关联都被准确计算。 这种方式可以用相对较低的成本将新文档添加到索引。
  • 段是不可改变的,所以既不能从把文档从旧的段中移除,也不能修改旧的段来进行反映文档的更新。取而代之的是,每个提交点会包含一个.del文件,文件中会列出这些被删除文档的段信息。
  • 当一个文档被“删除”时,它实际上只是在.del文件中被标记删除。一个被标记删除的文档仍然可以被查询匹配到, 但它会在最终结果被返回前从结果集中移除。
  • 文档更新也是类似的操作方式:当一个文档被更新时,旧版本文档被标记删除,文档的新版本被索引到一个新的段中。可能两个版本的文档都会被一个查询匹配到,但被删除的那个旧版本文档在结果集返回前就已经被移除。

4 近实时搜索

  • 随着按段搜索的发展,一个新的文档从索引到可被搜索的延迟显著降低了,新文档在几分钟之内即可被检索

  • 这样还是不够快,磁盘在这里成为了瓶颈

    • 提交一个新的段到磁盘需要一个fsync来确保段被物理性地写入磁盘,这样在断电的时候就不会丢失数据
    • 但是fsync操作代价很大,如果每次索引一个文档都去执行一次的话会造成很大的性能问题。
    • 我们需要的是一个更轻量的方式来使一个文档可被搜索,这意味着fsync要从整个过程中被移除。
    • 在Elasticsearch和磁盘之间是文件系统缓存。 像之前描述的一样, 在内存索引缓冲区中的文档会被写入到一个新的段中。 但是这里新段会被先写入到文件系统缓存,这一步代价会比较低稍后再被刷新到磁盘,这一步代价比较高。不过只要文件已经在缓存中, 就可以像其它文件一样被打开和读取了。

image.png

  • Lucene允许新段被写入和打开,使其包含的文档在未进行一次完整提交时便对搜索可见

这种方式比进行一次提交代价要小得多,并且在不影响性能的前提下可以被频繁地执行。
image.png

  • 在Elasticsearch中,写入和打开一个新段的轻量的过程叫做refresh。默认情况下每个分片会每秒自动refresh一次。这就是为什么我们说Elasticsearch是近实时搜索:文档的变化并不是立即对搜索可见,但会在一秒之内变为可见
  • 这些行为可能会对新用户造成困惑:他们索引了一个文档然后尝试搜索它,但却没有搜到。这个问题的解决办法是用refreshAPI执行一次手动刷新:/users/_refresh

    尽管刷新是比提交轻量很多的操作,它还是会有性能开销。当写测试的时候,手动刷新很有用,但是不要在生产环境下每次索引一个文档都去手动刷新。相反,你的应用需要意识到Elasticsearch的近实时的性质,并接受它的不足

  • 并不是所有的情况都需要每秒刷新。可能你正在使用Elasticsearch索引大量的日志文件,你可能想优化索引速度而不是近实时搜索,可以通过设置refresh_interval,降低每个索引的刷新频率

    { 
      "settings": { 
          "refresh_interval": "30s" 
      } 
    }
    
    • refresh_interval可以在既存索引上进行动态更新。 在生产环境中,当你正在建立一个大的新索引时,可以先关闭自动刷新,待开始使用该索引时,再把它们调回来
      # 关闭自动刷新 
      PUT /users/_settings 
      { "refresh_interval": -1 } 
      # 每一秒刷新 
      PUT /users/_settings 
      { "refresh_interval": "1s" }