目录
一、Elasticsearch 介绍
1.1 简介
Elasticsearch 是一个开源的搜索引擎,建立在一个全文搜索引擎库 Apache Lucene™ 基础之上。 Lucene 可以说是当下最先进、高性能、全功能的搜索引擎库—无论是开源还是私有。
但是 Lucene 仅仅只是一个库。为了充分发挥其功能,你需要使用 Java 并将 Lucene 直接集成到应用程序中。 更糟糕的是,您可能需要获得信息检索学位才能了解其工作原理。Lucene 非常复杂。
Elasticsearch 也是使用 Java 编写的,它的内部使用 Lucene 做索引与搜索,但是它的目的是使全文检索变得简单, 通过隐藏 Lucene 的复杂性,取而代之的提供一套简单一致的 RESTful API。
然而,Elasticsearch 不仅仅是 Lucene,并且也不仅仅只是一个全文搜索引擎。 它可以被下面这样准确的形容:
- 一个分布式的实时文档存储,每个字段 可以被索引与搜索
- 一个分布式实时分析搜索引擎
- 能胜任上百个服务节点的扩展,并支持 PB 级别的结构化或者非结构化数据
Elasticsearch 将所有的功能打包成一个单独的服务,这样你可以通过程序与它提供的简单的 RESTful API 进行通信, 可以使用自己喜欢的编程语言充当 web 客户端,甚至可以使用命令行(去充当这个客户端)。
1.2 原理和应用
1.2.1 先了解一下Lucene的整体框架
Lucene作为一个全文检索引擎,其具有如下突出的优点:
(1)索引文件格式独立于应用平台。Lucene定义了一套以8位字节为基础的索引文件格式,使得兼容系统或者不同平台的应用能够共享建立的索引文件。
(2)在传统全文检索引擎的倒排索引的基础上,实现了分块索引,能够针对新的文件建立小文件索引,提升索引速度。然后通过与原有索引的合并,达到优化的目的。
(3)优秀的面向对象的系统架构,使得对于Lucene扩展的学习难度降低,方便扩充新功能。
(4)设计了独立于语言和文件格式的文本分析接口,索引器通过接受Token流完成索引文件的创立,用户扩展新的语言和文件格式,只需要实现文本分析的接口。
(5)已经默认实现了一套强大的查询引擎,用户无需自己编写代码即可使系统可获得强大的查询能力,Lucene的查询实现中默认实现了布尔操作、模糊查询(Fuzzy Search[11])、分组查询等等。
全文检索大体分两个过程,索引创建(Indexing)和搜索索引(Search)。
索引创建:将现实中所有的结构化和非结构化数据提取信息,创建索引的过程。
搜索索引:就是得到用户的查询请求,搜索创建的索引,然后返回结果的过程。
1.2.2 ES核心概念
集群 (Cluster):集群是一个或多个节点(服务器)的集合,它们一起保存您的整个数据并提供跨所有节点的联合索引和搜索功能。集群由唯一名称标识,默认情况下为“elasticsearch”。此名称很重要,因为只有将节点设置为按其名称加入集群时,该节点才能成为集群的一部分。
节点 (Node):节点是集群的一部分,存储数据并参与集群的索引和搜索功能的单个服务器。就像集群一样,节点由名称标识,默认情况下,该名称是在启动时分配给节点的随机通用唯一标识符 (UUID)。如果您不想要默认值,您可以定义任何您想要的节点名称。
索引 (Index):索引是具有某种相似特征的文档的集合。例如,您可以有一个客户数据索引、另一个产品目录索引和另一个订单数据索引。索引由名称(必须全部小写)标识,该名称用于在对其中的文档执行索引、搜索、更新和删除操作时引用索引。
类型 (Type):一种类型曾经是索引的逻辑类别/分区,允许您在同一索引中存储不同类型的文档,例如,一种类型用于用户,另一种类型用于博客文章。不再可能在索引中创建多个类型,并且在以后的版本中将删除整个类型的概念。
文档 (Document):文档是可以被索引的基本信息单元。例如,您可以为单个客户创建一个文档,为单个产品创建另一个文档,以及为单个订单创建另一个文档。本文档以JSON(JavaScript 对象表示法)表示,这是一种普遍存在的互联网数据交换格式。
分片 (Shard):因为ES是分布式架构,类似于HDFS的存储方式,所以数据被打散存储在集群的多个节点上,一个分片实际上就是底层Lucene的一个索引,这里说的分片指的是ES中的主分片(因为还有副本分片一说),分片的方式是ES自动完成,用户可以指定分片的数量,主分片一旦指定就不能修改,因为ES打散数据的方式是和索引创建时指定的主分片数量有关(路由算法公式:shard=hash(routing)%number_of_primary_shards,进行文档分配),后期改变会导致分片中的数据不可搜索。
副本 (Replia):副本就是分片的一个拷贝,不仅能提高自身容灾,另外,请求量很大的情况下,副本可以分担主Shard压力,承担查询功能。副本个数还以在创建完索引后灵活调整
1.2.3 ES实现写入和读取的原理
(1)ES写数据过程
1)客户端随机选择一个node发送请求过去,这个node就是coordinating node(协调节点)
2)coordinating node,对document进行路由,将请求转发给对应的node(有primary shard)
3)实际的node上的primary shard处理请求,然后将数据同步到replica node
4)coordinating node,如果发现primary node和所有replica node都搞定之后,就返回响应结果给客户端
在写primary shard的过程中同时还要持久到本地 :
1)先写入buffer,在buffer里的时候数据是搜索不到的;同时将数据写入translog日志文件
2)如果buffer快满了,或者到一定时间,就会将buffer数据refresh到一个新的segment file中,但是此时数据不是直接进入segment file的磁盘文件的,而是先进入os cache的。这个过程就是refresh。
每隔1秒钟,es将buffer中的数据写入一个新的segment file,每秒钟会产生一个新的磁盘文件,segment file,这个segment file中就存储最近1秒内buffer中写入的数据
但是如果buffer里面此时没有数据,那当然不会执行refresh操作咯,每秒创建换一个空的segment file,如果buffer里面有数据,默认1秒钟执行一次refresh操作,刷入一个新的segment file中
操作系统里面,磁盘文件其实都有一个东西,叫做os cache,操作系统缓存,就是说数据写入磁盘文件之前,会先进入os cache,先进入操作系统级别的一个内存缓存中去
只要buffer中的数据被refresh操作,刷入os cache中,就代表这个数据就可以被搜索到了
为什么叫es是准实时的?NRT,near real-time,准实时。默认是每隔1秒refresh一次的,所以es是准实时的,因为写入的数据1秒之后才能被看到。
可以通过es的restful api或者java api,手动执行一次refresh操作,就是手动将buffer中的数据刷入os cache中,让数据立马就可以被搜索到。
只要数据被输入os cache中,buffer就会被清空了,因为不需要保留buffer了,数据在translog里面已经持久化到磁盘去一份了
3)只要数据进入os cache,此时就可以让这个segment file的数据对外提供搜索了
4)重复1~3步骤,新的数据不断进入buffer和translog,不断将buffer数据写入一个又一个新的segment file中去,每次refresh完buffer清空,translog保留。随着这个过程推进,translog会变得越来越大。当translog达到一定长度的时候,就会触发commit操作。
buffer中的数据,倒是好,每隔1秒就被刷到os cache中去,然后这个buffer就被清空了。所以说这个buffer的数据始终是可以保持住不会填满es进程的内存的。
每次一条数据写入buffer,同时会写入一条日志到translog日志文件中去,所以这个translog日志文件是不断变大的,当translog日志文件大到一定程度的时候,就会执行commit操作。
5)commit操作发生第一步,就是将buffer中现有数据refresh到os cache中去,清空buffer
6)将一个commit point写入磁盘文件,里面标识着这个commit point对应的所有segment file
7)强行将os cache中目前所有的数据都fsync到磁盘文件中去
translog日志文件的作用是什么?就是在你执行commit操作之前,数据要么是停留在buffer中,要么是停留在os cache中,无论是buffer还是os cache都是内存,一旦这台机器死了,内存中的数据就全丢了。
所以需要将数据对应的操作写入一个专门的日志文件,translog日志文件中,一旦此时机器宕机,再次重启的时候,es会自动读取translog日志文件中的数据,恢复到内存buffer和os cache中去。
commit操作:1、写commit point;2、将os cache数据fsync强刷到磁盘上去;3、清空translog日志文件
8)将现有的translog清空,然后再次重启启用一个translog,此时commit操作完成。默认每隔30分钟会自动执行一次commit,但是如果translog过大,也会触发commit。整个commit的过程,叫做flush操作。我们可以手动执行flush操作,就是将所有os cache数据刷到磁盘文件中去。
不叫做commit操作,flush操作。es中的flush操作,就对应着commit的全过程。我们也可以通过es api,手动执行flush操作,手动将os cache中的数据fsync强刷到磁盘上去,记录一个commit point,清空translog日志文件。
9)translog其实也是先写入os cache的,默认每隔5秒刷一次到磁盘中去,所以默认情况下,可能有5秒的数据会仅仅停留在buffer或者translog文件的os cache中,如果此时机器挂了,会丢失5秒钟的数据。但是这样性能比较好,最多丢5秒的数据。也可以将translog设置成每次写操作必须是直接fsync到磁盘,但是性能会差很多。
其实es第一是准实时的,数据写入1秒后可以搜索到;可能会丢失数据的,你的数据有5秒的数据,停留在buffer、translog os cache、segment file os cache中,有5秒的数据不在磁盘上,此时如果宕机,会导致5秒的数据丢失。
如果你希望一定不能丢失数据的话,你可以设置个参数,官方文档,百度一下。每次写入一条数据,都是写入buffer,同时写入磁盘上的translog,但是这会导致写性能、写入吞吐量会下降一个数量级。本来一秒钟可以写2000条,现在你一秒钟只能写200条,都有可能。
10)如果是删除操作,commit的时候会生成一个.del文件,里面将某个doc标识为deleted状态,那么搜索的时候根据.del文件就知道这个doc被删除了
11)如果是更新操作,就是将原来的doc标识为deleted状态,然后新写入一条数据
12)buffer每次refresh一次,就会产生一个segment file,所以默认情况下是1秒钟一个segment file,segment file会越来越多,此时会定期执行merge
13)每次merge的时候,会将多个segment file合并成一个,同时这里会将标识为deleted的doc给物理删除掉,然后将新的segment file写入磁盘,这里会写一个commit point,标识所有新的segment file,然后打开segment file供搜索使用,同时删除旧的segment file。
es里的写流程,有4个底层的核心概念,refresh、flush、translog、merge
当segment file多到一定程度的时候,es就会自动触发merge操作,将多个segment file给merge成一个segment file。
(2)ES读数据过程
查询,GET某一条数据,写入了某个document,这个document会自动给你分配一个全局唯一的id,即doc id,同时也是根据doc id进行hash路由到对应的primary shard上面去。也可以手动指定doc id,比如用订单id,用户id。
你可以通过doc id来查询,会根据doc id进行hash,判断出来当时把doc id分配到了哪个shard上面去,从那个shard去查询
1)客户端发送请求到任意一个node,成为coordinate node
2)coordinate node对document进行路由,将请求转发到对应的node,此时会使用round-robin随机轮询算法,在primary shard以及其所有replica shard中随机选择一个,让读请求负载均衡
3)接收请求的node返回document给coordinate node
4)coordinate node返回document给客户端
(3)ES搜索数据过程
1)客户端发送请求到一个coordinate node
2)协调节点将搜索请求转发到所有的shard对应的primary shard或replica shard也可以
3)query phase:每个shard将自己的搜索结果(其实就是一些doc id),返回给协调节点,由协调节点进行数据的合并、排序、分页等操作,产出最终结果
4)fetch phase:接着由协调节点,根据doc id去各个节点上拉取实际的document数据,最终返回给客户端
(4)搜索的底层原理,倒排索引,用表格说明传统数据库和倒排索引的区别(自上而下)
关系型数据库(比如Mysql) | 非关系型数据库(Elasticsearch) |
---|---|
数据库 DB | 索引 Index |
表 Table | 类型 Type |
数据行 Row | 文档 Document |
数据列 Column | 字段 Field |
约束 Schema | 映射 Mapping |
二、Elasticsearch 的使用
2.1 安装服务端
2.1.1 前期准备,不做会报错
- 调整虚拟机最大并发连接
$:vi /etc/sysctl.conf
vm.max_map_count=655360
$:sysctl -p
- 修改文件限制
$:vi /etc/security/limits.conf
root soft nofile 65536
root hard nofile 65536
* soft nofile 65536
* hard nofile 65536
$:reboot
2.1.2 先安装Java的JDK环境(此处省略,自行查找安装文档)
2.1.3 通过官网下载 Elasticsearch
Download Elasticsearch | Elastic
下载完解压的目录,我使用的是5.3的版本,现在最新版已更新到了8.2
2.1.4 配置
配置文件在根目录的config文件夹里,里面 elasticsearch.yml 是基本配置文件,logging.yml 是日志配置文件。
这里主要说一下是否允许 IP:9200 访问,network.host 配置默认是192.168.0.1 改成0.0.0.0是允许所有IP访问
# Set the bind address to a specific IP (IPv4 or IPv6):
#
network.host: 0.0.0.0
#
# Set a custom port for HTTP:
#
http.port: 9200
2.1.6 设置启动文件的所属用户权限
由于 ES服务端 不允许root用户执行启动文件,所以需要用 useradd 增加一个执行用户 user,再通过 chown -R user:user * 设置整个根目录及子目录文件的所属用户为新建用户 user,再进入通过 su user 切换用户后执行 ./bin/elasticsearch 启动,要守护进程运行在命令行中指定 -d
2.1.7 演示启动后的效果
浏览器上访问 ip:9200 显示结果为下图则说明启动成功
若使用可视化工具可以访问GitHub - mobz/elasticsearch-head: A web front end for an elastic search cluster
到此搜索引擎就已经安装好了, 下面是使用过程。
2.2 PHP通过客户端操作ES库
2.2.1 composer安装客户端
composer require elasticsearch/elasticsearch
2.2.2 使用客户端操作ES库
(1)建立客户端链接
<?php
require_once './vendor/autoload.php';
use Elasticsearch\ClientBuilder;
//建立客户端链接
$client = ClientBuilder::create()
->setHosts(['localhost:9200'])
->build();
//获取客户端详情
$response = $client->info();
echo "<pre/>";
print_r($response);
(2) 索引管理
ㆍ建立索引
$params = [
'index' => 'my_index', //索引名称
'body' => [
'settings' => [
'number_of_shards' => 3, //分片节点数量
'number_of_replicas' => 2 //副本节点数量
],
'mappings' => [
'my_type' => [ //索引类型
'_source' => [ //该字段是mapping里的一个元数据字段
'enabled' => true
],
'properties' => [ //字段名称
'first_name' => [
'type' => 'string', //字段类型
'analyzer' => 'standard' //标准分析器
],
'age' => [
'type' => 'integer' //字段类型
]
]
]
]
]
];
// 创建索引
$response = $client->indices()->create($params);
echo "<pre/>";
print_r($response);
ㆍ删除索引
$params = ['index' => 'my_index'];
$response = $client->indices()->delete($params);
ㆍ修改索引参数
$params = [
'index' => 'my_index',
'body' => [
'settings' => [
'number_of_replicas' => 0, //副本节点数量
'refresh_interval' => -1, //刷新间隔,-1代表不刷新,单位s
]
]
];
$response = $client->indices()->putSettings($params);
ㆍ获取索引当前配置参数
$params = ['index' => 'my_index'];
$response = $client->indices()->getSettings($params);
ㆍ允许你更改或增加一个索引的映射
$params = [
'index' => 'my_index',
'type' => 'my_type2',
'body' => [
'my_type2' => [
'_source' => [
'enabled' => true
],
'properties' => [
'first_name' => [
'type' => 'string',
'analyzer' => 'standard'
],
'age' => [
'type' => 'integer'
]
]
]
]
];
// Update the index mapping
$client->indices()->putMapping($params);
ㆍ获取索引和类型的映射细节
//获取全部索引和类型的映射
$response = $client->indices()->getMapping();
//获取某一个索引和类型的映射
$params = ['index' => 'my_index'];
$response = $client->indices()->getMapping($params);
(3) 索引文档操作
ㆍ添加索引文档
$params = [
'index' => 'my_index',
'type' => 'my_type',
'id' => '1', //可以不设置ID值,让系统自动生成
'body' => [
'first_name' => '张',
'age' => '30',
]
];
$response = $client->index($params);
ㆍ批量建立索引文档
for($i = 0; $i < 100; $i++) {
$params['body'][] = [
'index' => [
'_index' => 'my_index',
'_type' => 'my_type',
]
];
$params['body'][] = [
'my_field' => 'my_value',
'second_field' => 'some more values'
];
}
$response = $client->bulk($params);
ㆍ获取文档
$params = [
'index' => 'my_index',
'type' => 'my_type',
'id' => '1'
];
$response = $client->get($params);
ㆍ修改文档
$params = [
'index' => 'my_index',
'type' => 'my_type',
'id' => '1',
'body' => [
'doc' => [
'first_name' => '李',
'age' => '29',
]
]
];
$response = $client->update($params);
ㆍ删除文档
$params = [
'index' => 'my_index',
'type' => 'my_type',
'id' => 'my_id'
];
$response = $client->delete($params);
(4) 搜索操作
ㆍMatch 查询、分页、排序、条件过滤和高亮
$params = [
'index' => 'my_index',
'type' => 'my_type', //index 和 type 是可以模糊匹配的,比如 m*
'from' => 0, //分页参数,偏移量
'size' => 10, //分页参数,每页条数
'body' => [
'query'=>[
'bool'=>[
'must'=>[ //must里的条件必须全部为true才能返回,should则是包含的条件里有一个条件为true就返回
['match'=>['first_name' => '李']], //匹配查询,match_phrase短语匹配
],
'filter'=>[
[ //这里注意多个筛选条件时,term外面还有一层[]
'term' => [//过滤条件匹配
'age' => 30
]
],
[
'range'=>[ //过滤范围取值
'age'=>[
'gte'=>29, //大于等于29
]
]
]
],
]
],
'highlight' => [ //高亮显示
'pre_tags' => ["<font color='red'>"],
'post_tags' => ["</font>"],
'fields' => [
"name" => new \stdClass()
]
],
'sort' => [ //排序
'age' => 'desc'
]
]
];
$results = $client->search($params);
ㆍScrolling(游标)查询
$params = [
"scroll" => "30s", // how long between scroll requests. should be small!
"size" => 50, // how many results *per shard* you want back
"index" => "my_index",
"body" => [
"query" => [
"match_all" => new \stdClass()
]
]
];
$response = $client->search($params);