大数据时代系统和业务每分每秒都产生成千上万的数据,其存储一定是不能通过关系型数据库了,当然因为数据的持久性也不能存储到内存型Nosql数据库Redis中,我们通常会将这些数据存储在能够不丢失数据的非关系型数据库中,这个技术选型有很多,例如HBase、Cassandra,这里我们暂不关心其数据存储,留待日后讨论,我们关注的是另一件事,如何能在分布式的数据库中进行PB级的数据检索,目前市场上较为成熟的解决方案中间件就是ElasticSearch,本文将从使用背景开始对ElasticSearch进行全面讨论:
- 什么是全文检索:全文检索实现方式、倒排索引
- ElasticSearch的概念和实现:索引创建和文档添加
- ElasticSearch集群:节点分析、发现机制、选举机制
- ElasticSearch工作流程:索引CRSUD过程
- ElasticSearch存储原理及策略:分段存储及段不变性、索引操作策略
- ElasticSearch检索:分词器、查询方式以及分页查询
适合人群:不了解ElasticSearch的新手,对ElasticSearch的实现机制感兴趣的技术人员
本文的全部内容来自我个人在ElasticSearch学习过程中整理的博客,是该博客专栏的精华部分。在书写过程中过滤了流程性的上下文,例如部署环境、配置文件等,而致力于向读者讲述其中的核心部分,如果读者有意对过程性内容深入探究,可以移步MaoLinTian的Blog,在这篇索引目录里找到答案。
什么是全文检索
有因才有果,先了解下为什么使用全文检索,才能最终料到ElasticSearch。本节从以下三个方面来进行讨论:全文检索的应用场景、基本概念、实现思路。
全文检索基本概念
因为我们的数据世界存在不规则的数据,而我们又需要对这些数据进行快速检索,所以和关系数据库类似,需要创建索引,这就引出了全文检索的概念。
数据分类
先来了解一个前置概念:数据类型的分类,数据的来源按数据类型可分为:结构化数据、半结构化数据、非结构化数据。
- 结构化数据:一般是从内部数据库和外部开放数据库接口中获得,一般存储产品业务运营数据以及用户操作的结果数据,比如注册用户数、下单量、完单量等数据。这类数据格式规范,典型代表就是关系数据库中的数据,可以用二维表来存储,有固定字段数,每个字段有固定的数据类型(数字、字符、日期等),每个字节长度相对固定。这类数据易于维护管理,同时对于查询、展示和分析而言也是最为方便的一类数据格式。
- 半结构化数据:应用的点击日志以及一些用户行为数据,通常指日志数据、xml、json等格式输出的数据,格式较为规范,一般是纯文本数据,需要对数据格式进行解析,才能用于查询或分析数据。每条记录预定义规范,但是每条记录包含信息不同,字段数不同,字段名和字段类型不同,或者还包含着嵌套的格式。
- 非结构化数据:指非纯文本类数据,没有标准格式,无法直接解析相应值,常见的非结构化数据有富文本、图片、声音、视频等数据。一般将非结构化数据存放在文件系统中,数仓中记录数据的信息,如标题、摘要、创建时间等,方便进行索引查询
对于结构化数据来说我们的查询、展示和分析很方便,强大的SQL语句和规范的表结构让这一点很容易做到,但是对于非结构化数据来说并不容易,尤其是进行查询的时候,如果给你一推文件,让你找出包含某个字符串的所有文件是很难实现的,一个两个还可以目测,多了就不行了。所以如何快速定位到满足查询条件的文档(数据)呢?
非结构化查询方式
要想实现上述需求,我们可以按照非结构化数据的处理思路来看:一般将非结构化数据存放在文件系统中,数仓中记录数据的信息,如标题、摘要、创建时间等,方便进行索引查询,也就是从文件数据提取数据标签,描述这个文件是做什么的,这样我们搜索的时候只要搜索这些标签即可找到目标的数据文件。
全文检索
以上非结构化查询方式抽象而言可以理解为将非结构化数据中的一部分信息提取出来,重新组织,使其变得有一定结构,然后对此有一定结构的数据进行搜索,从而达到搜索相对较快的目的。这部分从非结构化数据中提取出的然后重新组织的信息,我们称之索引,这种先建立索引,再对索引进行搜索的过程就叫全文检索(Full-text Search)。虽然创建索引的过程也是非常耗时的,但是索引一旦创建就可以多次使用,全文检索主要处理的是查询,所以耗时间创建索引是值得的。
这里说到我们将非结构化数据提取出来使其变得有结构,那么有人可能会问:如果我们把这些结构化标签存储在MySQL里不行么,还有这篇blog聊的ElasticSearch什么事儿呢?事实上MySQL也提供全文检索能力,但是没有ElasticSearch好用,这里简单解释一下为什么不用关系型数据库处理索引:
- 数据结构不固定:这个是最关键的一点,关系型数据库扩展性很差,如果需要添加一个字段,那么首先要改表,其次如果有存储过程这些的话还需要全都改一遍,不好维护,而非结构化数据的数据标签【索引】是不固定的。
- 不满足高并发读写:web2.0时代,需要依据用户个性化需要高并发读写,关系型数据库读还可以,写就很难做到了。例如论坛这样的站点, 网站的用户并发性非常高,往往达到每秒上万次读写请求,对于传统关系型数据库来说,硬盘I/O是一个很大的瓶颈。
- 不满足高效访问:海量数据高效率存储和访问, 网站每天产生的数据量是巨大的,对于关系型数据库来说,在一张包含海量数据的表中查询,效率是非常低的,因为关系型数据库导出充斥着锁和事务。
- 不满足高可拓展性和高可用性:关系型数据库很难进行横向扩展,当一个应用系统的用户量和访问量与日俱增的时候,数据库却没有办法像web server和app server那样简单的通过添加更多的硬件和服务节点来扩展性能和负载能力。
之后我们在详细聊到ElasticSearch再去讨论它是如何做到满足以上几个要求的。
全文检索应用场景
对于数据量大、数据结构不固定的数据可采用全文检索方式搜索,比如百度、Google等搜索引擎、论坛站内搜索、电商网站站内搜索等,总而言之,只要用到搜索的地方,都可以使用全文检索进行搜索,
全文检索实现思路
全文检索的整体实现思路如下图所示,左侧部分为创建索引,右侧部分为查询索引:
上图的执行流程说明如下:
- 左侧绿色部分表示索引过程,对要搜索的原始内容进行索引构建一个索引库,索引过程包括:确定原始内容即要搜索的内容—>采集文档—>创建文档—>分析文档—>索引文档
- 右侧红色部分表示搜索过程,从索引库中搜索内容,搜索过程包括:用户通过搜索界面传参—>创建查询—>执行搜索、从索引库搜索—>渲染搜索结果
我们来具体看下索引库的基本创建过程和查询过程。
创建索引
对文档索引的过程,将用户要搜索的文档内容进行索引,索引存储在索引库(index)中,也就是左侧的绿色流程:
- 确定原始内容即要搜索的内容:确定我们的需求也即对什么数据进行分析,这里就是这个存放了诸多文档的文件夹
- 采集文档:手段有很多,网络爬虫,数据读取,文件IO读取,我们这里用的就是文件IO读取
- 创建文档:获取原始内容的目的是为了索引,在索引前需要将原始内容创建成文档(Document),文档中包括一个一个的域(Field),域中存储内容。这里我们可以将磁盘上的一个文件当成一个document,Document中包括一些Field(file_name文件名称、file_path文件路径、file_size文件大小、file_content文件内容)
每个文档都有一个唯一的编号,就是文档id。 - 分析文档:将原始内容创建为包含域(Field)的文档(document)后,需要再对域中的内容进行分析,分析的过程是经过对原始文档提取单词、将字母转为小写、去除标点符号、去除停用词等过程生成最终的语汇单元,可以将语汇单元理解为一个一个的单词,例如:
Lucene is a Java full-text search engine.
分析后得到的语汇单元:lucene、java、full、text、search、engine。每个单词叫做一个Term,不同的域中拆分出来的相同的单词是不同的term。term中包含两部分一部分是文档的域名,另一部分是单词的内容,例如{"FileName":"springmvc"}
就是域FileName上的一个term,这样我们在该域上进行检索的时候,只要检索springmvc就能找到当前文档。 - 索引文档:对所有文档分析得出的语汇单元进行索引,索引的目的是为了搜索,最终要实现只搜索被索引的语汇单元从而找到Document(文档),注意:创建索引是对语汇单元索引,通过词语找文档,这种索引的结构叫倒排索引结构。传统方法是根据文件找到该文件的内容,在文件内容中匹配搜索关键字,这种方法是顺序扫描方法,数据量大、搜索慢。倒排索引结构是根据内容(词语)找文档,如下图
这里的词典就是term的集合,每个term【域和关键词的组合】会索引一连串满足条件的文档id,检索时通过term检索可以找到这串id,进而找到满足条件的文档集合。
实际上以上过程就是将非结构化的一些数据进行结构化提取并索引的过程。
查询索引
查询索引也是搜索的过程。搜索就是用户输入关键字,从索引(index)中进行搜索的过程。根据关键字搜索索引,根据索引找到对应的文档,从而找到要搜索的内容(这里指磁盘上的文件),也就是右侧红色的部分:
- 用户通过搜索界面键入关键词:全文检索系统提供用户搜索的界面供用户提交搜索的关键字,搜索完成展示搜索结果,例如我们常用的百度就是这个原理。
- 创建查询:用户输入查询关键字执行搜索之前需要先构建一个查询对象,查询对象中可以指定查询要搜索的Field文档域、查询关键字等,查询对象会生成具体的查询语法,例如term为
{"FileName":"springmvc"}
- 执行搜索:根据查询语法在倒排索引词典表中分别找出对应搜索词的索引,从而找到索引所链接的文档链表
- 渲染搜索结果:以一个友好的界面将查询结果展示给用户,用户根据搜索结果找自己想要的信息,为了帮助用户很快找到自己的结果,提供了很多展示的效果,比如搜索结果中将关键字高亮显示,百度提供的快照等
以上部分就是全文检索如何实现索引库的查询
全流程举例
按照如上的流程来模拟一遍。提出如下需求:给出一组doc文件,如下图所示,用于建立索引和搜索,找到指定的文档:要求找出所有文件名包含丑字的文件。
那么按照需求我们来看下如何实现,创建文档的相关索引,按照流程处理如下:
- 确定原始内容即要搜索的内容:确认要搜索的是文档的文件名,所以只给文件名进行索引即可。需要说明的是当前这个例子比较简单,基本通过目测就可以实现,就是举个例子,实际上文档成千上万的时候,索引非常有用。
- 采集文档:我们这里可以通过将文件通过IO读取到磁盘即可。
- 创建文档:创建一个文档对象,在文档对象下我们可以创建一个FieldName为Name的域,Value值即为文件名,例如以上六个文档对象里都会包含一个文件名域,拿第一个文档对象举例:
"FieldName":"tml超级帅"
- 分析文档:将文件名进行处理,如果按照标准的分词方法可以举例如下:tml超级帅分词时分为**:tml、有、点、帅** ,标准的中文是单字切分的。
- 索引文档:索引文档就是建立term【某个域下的某个关键词】和文档的关系,倒排索引结构的创建,例如这里就是:例如term:
"FieldName":"丑"
—>很丑.txt[文档id为5]–>tml介于帅和丑之间.txt[文档id为1],挂载了两篇文档
查询文档过程是从索引库中搜索内容,按照流程处理过程如下:
- 用户通过搜索界面传参:选定好域Name:FieldName,关键词value:丑
- 创建查询:创构造一个term查询结构:
"FieldName":"丑"
- 执行搜索:找到所有包含丑关键词的文档,共两篇,返回文档id:1、5
- 渲染搜索结果:通过文档id找到对应文档,返回结果,呈现给用户
以上就是全文检索的实现逻辑,了解了实现逻辑后,其实大家就知道,一定有人会写一些工具至少是类库来加速这个过程。
ElasticSearch的概念和实现
熟悉全文检索概念的话知道其实早期实现方式是用Lucene的,什么是Lucene呢?Lucene是一个开放源代码的全文检索引擎工具包,但它不是一个完整的全文检索引擎,而是一个全文检索引擎的架构,提供了完整的查询引擎和索引引擎,部分文本分析引擎(英文与德文两种西方语言)。Lucene的目的是为软件开发人员提供一个简单易用的工具包,以方便的在目标系统中实现全文检索的功能,或者是以此为基础建立起完整的全文检索引擎。在Java开发环境里Lucene是一个成熟的免费开源工具。一句话概括,Lucene就是一组实现全文检索的Jar包。而ElasticSearch就是基于Lucene实现的工业级全文检索引擎
什么是ElasticSearch
Elaticsearch,简称为es, es是一个开源的高扩展的分布式全文检索引擎,它可以近乎实时的存储、检索数据;本身扩展性很好,可以扩展到上百台服务器,处理PB[1024TB]级别的数据。es也使用Java开发并使用Lucene作为其核心来实现所有索引和搜索的功能,但是它的目的是通过简单的RESTful API来隐藏Lucene的复杂性,从而让全文搜索变得简单:
- Elasticsearch 自身带有分布式协调管理功能, 仅支持json文件格式,Elasticsearch处理实时搜索应用时效率很高
以上的所有特点都标注了Elasticsearch实质上是一款高效的分布式的全文搜索引擎
ElasticSearch基本术语
ElasticSearch是一个面向文档的搜索引擎,这意味着它可以存储整个对象或文档(document)。然而它不仅仅是存储,还会索引(index)每个文档的内容使之可以被搜索。在Elasticsearch中,你可以对文档(而非成行成列的数据)进行索引、搜索、排序、过滤。Elasticsearch比传统关系型数据库如下:
类别 | 库 | 表 | 行 | 列 |
关系型数据库 | Databases | Tables | Rows | Columns |
ElasticSearch | Indices | Types | Docements | Fields |
Elasticsearch具备接近实时 NRT,Elasticsearch是一个接近实时的搜索平台。这意味着,从索引一个文档直到这个文档能够被搜索到有一个轻微的延迟,通常1秒以内,关于这个延迟后边我们会详细讨论到。
Elasticsearch 概念 | 解释 | Lucene概念 |
索引 | 索引index就是相似特征文档的集合。例如产品目录索引、订单索引。一个索引由一个名字来标识(必须全部是小写字母),当对索引中的文档进行CRSUD的时候都需要指定索引编码。一个集群中可以定义任意多的索引 | 索引 |
类型 | 一个索引中可以定义一种或多种类型type。一个类型是索引的一个逻辑上的分类/分区,通常,会为具有一组共同字段的文档定义一个类型。ES7.X新版本中移除了这个属性,恒定为_doc,这么做是为了和Lucene一致 | |
字段 | 相当于数据的字段Field,对文档数据根据不同属性进行的分类标识 | 字段域 |
映射 | mapping是处理数据的方式和规则的一些限制,如某个字段的**数据类型(type)、分析器(analyzer)、是否被索引(index)、是否存储(store)**等等 | 字段域属性配置集合 |
文档 | 一个文档是一个可被索引的基础信息单元。比如,你可以拥有某一个客户的文档,某一个产品的一个文档,当然,也可以拥有某个订单的一个文档。文档以JSON(Javascript Object Notation)格式来表示,而JSON是一个到处存在的互联网数据交互格式。在一个index/type里面,你可以存储任意多的文档。 | 文档 |
ElasticSearch服务节点
中间件可以直接从官网 ElasticSearch的官方地址下载到软件,本质上ElasticSearch就是一个服务器,服务器上存放了索引数据,看项目结构也可以看出,实质上是一个tomcat的典型服务器目录:
用户可以通过向服务器发起请求进行一系列操作, 其中9300为tcp的通讯接口【代码接口tcp调用使用】,9200为RestFul风格通讯接口【Http调用访问】。
创建索引
上面提到过可以通过http请求和tcp请求来实现,创建索引也是如此,通过http请求即可创建索引并设置相关mapping,注意mapping在创建后只能新增字段,不能修改已有的字段属性,其中关于分片和复制在下一小节具体讨论:
PUT http://127.0.0.1:9200/tml-userinfo 请求体body { "settings": { "number_of_shards": 5, //分片数 "number_of_replicas": 1 //复制数 }, "mappings": { "properties": { "id": { "type": "long", "store": true, "index": "true" }, "title": { "type": "text", //数据类型 "store": true, //是否存储 "index": "true", //是否索引 "analyzer": "standard" //分词器类别 }, "content": { "type": "text", "store": true, "index": "true", "analyzer": "standard" } } } }
返回信息如下证明索引创建成功
{ "acknowledged": true, "shards_acknowledged": true, "index": "tml-userinfo" }
添加文档
同样的添加文档也是通过发http请求即可做到:
POST localhost:9200/tml-userinfo/_doc/1 请求体: { "id":1101, "title":"我是第一个集群数据", "content":"它提供了一个分布式多用户能力的全文搜索引擎,基于RESTful web接口" }
返回值:
{ "_index": "tml-userinfo", "_type": "_doc", "_id": "1", "_version": 1, "result": "created", "_shards": { "total": 2, "successful": 2, "failed": 0 }, "_seq_no": 0, "_primary_term": 1 }
在索引中的数据展示如下图所示