1 Elasticsearch 简介
1.1 什么是 Elasticsearch?
Elasticsearch是一个基于 Apache Lucene 的开源搜索引擎。无论在开源还是专有领域,Lucene 可以被认为是迄今为止最先进、性能最好的、功能最全的搜索引擎库。
特点:
- 分布式的实时文件存储,每个字段都被索引并可被搜索
- 分布式的实时分析搜索引擎--做不规则查询
- 可以扩展到上百台服务器,处理PB级结构化或非结构化数据
Elasticsearch 也使用 Java 开发并使用 Lucene 作为其核心来实现所有索引和搜索的功能,但是它的目的是通过简单的 RESTful API 来隐藏 Lucene 的复杂性,从而让全文搜索变得简单。
1.2 Elasticsearch 应用场景
- 全文检索(全部字段)
- 模糊查询(搜索)
- 数据分析(提供分析语法,例如聚合)
1.3 Elasticsearch 使用案例
- 2013 年初,GitHub 抛弃了 Solr,采取 ElasticSearch 来做 PB 级的搜索:GitHub 使用 ElasticSearch 搜索 20TB 的数据,包括 13亿文件和 1300 亿行代码
- 维基百科启动以 Elasticsearch 为基础的核心搜索架构 SoundCloud:SoundCloud 使用 ElasticSearch 为 1.8 亿用户提供即时而精准的音乐搜索服务
- 百度目前广泛使用 ElasticSearch 作为文本数据分析,采集百度所有服务器上的各类指标数据及用户自定义数据,通过对各种数据进行多维分析展示,辅助定位分析实例异常或业务层面异常:目前覆盖百度内部 20 多个业务线(包括 casio、云分析、网盟、预测、文库、直达号、钱包、风控等),单集群最大 100 台机器,200 个 ES 节点,每天导入 30TB+ 数据
- 新浪使用 ES 分析处理 32 亿条实时日志
- 阿里使用 ES 构建挖财自己的日志采集和分析体系
1.4 同类产品
Solr、ElasticSearch、Hermes:
- Solr、ES
- 源自搜索引擎,侧重搜索与全文检索
- 数据规模从几百万到千万不等,数据量过亿的集群特别少
- 有可能存在个别系统数据量过亿,但这并不是普遍现象
- Hermes
- 一个基于大索引技术的海量数据实时检索分析平台,侧重数据分析
- 数据规模从几亿到万亿不等,最小的表也是千万级别
- 在 腾讯 17 台 TS5 机器,就可以处理每天 450 亿的数据(每条数据 1kb 左右),数据可以保存一个月之久
Solr、ES 区别:
- Solr 利用 Zookeeper 进行分布式管理,而 Elasticsearch 自身带有分布式协调管理功能
- Solr 支持更多格式的数据,而 Elasticsearch 仅支持 JSON 文件格式
- Solr 官方提供的功能更多,而 Elasticsearch 本身更注重于核心功能,高级功能多有第三方插件提供
- Solr 在传统的搜索应用中表现好于 Elasticsearch,但在处理实时搜索应用时效率明显低于 Elasticsearch-----附近的人
2 Elasticsearch 安装部署
2.1 单机部署
解压
[djm@hadoop102 software]$ tar -vzxf elasticsearch-6.3.1.tar.gz -C /opt/module/
设置访问 IP
vim config/elasticsearch.yml
network.host: 0.0.0.0
[max virtual memory areas vm.max_map_count [65530] is too low, increase to at least [262144]
sysctl -w vm.max_map_count=262144
启动和停止
# 前台启动
[djm@hadoop102 bin]$ ./elasticsearch
# 后台启动
[djm@hadoop102 bin]$ ./elasticsearch -d
2.2 集群部署
修改 elasticserach.yml 文件
#集群名称
cluster.name: my-application
#节点名称(不能重复)
node.name: node-2
#指定了该节点可能成为 master 节点,还可以是数据节点
node.master: true
node.data: true
#数据的默认存放路径(自定义)
path.data: /opt/module/elasticsearch-6.3.1/data
#日志的默认存放路径
path.logs: /opt/module/elasticsearch-6.3.1/logs
#允许外网访问
network.host: 0.0.0.0
#对外提供服务的端口
http.port: 9200
#9300为集群服务的端口
transport.tcp.port: 9300
#集群个节点IP地址,也可以使用域名,需要各节点能够解析
discovery.zen.ping.unicast.hosts: ["hadoop102", "hadoop103", "hadoop104"]
#为了避免脑裂,集群节点数最少为 半数+1
discovery.zen.minimum_master_nodes: 2
解压 head、node
[djm@hadoop102 ~]$ tar -zxvf node-v9.9.0-linux-x64.tar.gz
[djm@hadoop102 ~]$ unzip elasticsearch-head-master.zip
配置 Node 环境变量
#NODE_HOME
export NODE_HOME=/opt/module/node-v9.9.0
export PATH=$PATH:$NODE_HOME/bin
测试环境变量
[djm@hadoop102 ~]$ node -v
[djm@hadoop102 ~]$ npm -v
安装 grunt,进入到 head 目录
[djm@hadoop102 elasticsearch-head-master]$ npm install -g grunt-cli
[djm@hadoop102 elasticsearch-head-master]$ npm install
修改 elasticsearch.yml 文件,添加如下内容:
http.cors.enabled: true
http.cors.allow-origin: "*"
修改 Gruntfile.js,找到 connect 属性,新增 hostname: ’*’
connect: {
server: {
options: {
hostname: '*',
port: 9100,
base: '.',
keepalive: true
}
}
}
启动
# 前台启动
[djm@hadoop102 elasticsearch-head-master]$ grunt server
# 后台启动
[djm@hadoop102 elasticsearch-head-master]$ nohup grunt server &exit
2.3 Elasticsearch 操作工具
- PostMan
- Linux 命令行
- Kibana 的 Dev Tools
- Cerebro 插件
3 Elasticsearch API
3.1 Mapping
定义数据库中的表的结构的定义,通过 mapping 来控制索引存储数据的设置
- 定义Index下的字段名(Field Name)
- 定义字段的类型,比如数值型、字符串型、布尔型等
- 定义倒排索引相关的配置,比如 documentId、记录 position、打分等
获取 mapping
GET /djm/_mapping
Dynamic Mapping
JSON类型 | es 类型 |
---|---|
null | 忽略 |
boolean | boolean |
浮点类型 | float |
整数 | long |
object | object |
array | 由第一个非 null 值的类型决定 |
string | 匹配为日期设为 date 类型(默认开启),数字设为 float 或 long 类型(默认关闭),设为 text 类型,并附带keyword的子字段 |
mapping 中的字段类型一旦设定后,禁止修改
dynamic设置
- true:允许自动新增字段(默认的配置)
- False:不允许自动新增字段,但是文档可以正常写入,无法对字段进行查询操作
- strict:文档不能写入(如果写入会报错)
PUT my_index
{
"mappings": {
"doc": {
"dynamic": false,
"properties": {
"user": {
"properties": {
"name": {
"type": "text"
},
"social_networks": {
"dynamic": true,
"properties": {}
}
}
}
}
}
}
}
copy_to 将该字段的值复制到目标字段,实现 _all 的作用,不会出现在 _source 中,只用来搜索
PUT my_index
{
"mappings": {
"doc": {
"properties": {
"frist_name": {
"type": "text",
"copy_to": "full_name"
},
"last_name": {
"type": "text",
"copy_to": "full_name"
},
"full_name": {
"type": "text"
}
}
}
}
}
Index 属性,控制当前字段是否可以被索引,默认为 true
PUT my_index
{
"mappings": {
"doc": {
"properties": {
"cookie": {
"type": "text",
"index": false
}
}
}
}
}
Index_options 用于控制倒排索引记录的内容
- docs:只记录 docid
- freqs:记录 docid 和 term frequencies(词频)
- position:记录 docid、term frequencies、term position
- Offsets:记录 docid、term frequencies、term position、character offsets
3.2 Document
索引一个文档
PUT {index}/{type}/{id}
{
"": ""
}
使用自己的 ID
PUT /{index}/{type}/{id}
{
"field": "value",
...
}
例如我们的索引叫做 website,类型叫做 blog,我们选择的 ID 是123,那么这个索引请求就像这样:
PUT /website/blog/123
{
"title": "My first blog entry",
"text": "Just trying this out...",
"date": "2014/01/01"
}
响应指出请求的索引已经被成功创建,这个索引中包含_index、_type和_id元数据,以及一个新元素:_version
每个文档都有版本号,每当文档发生变化时都会使_version发生变化
自增 ID
POST /website/blog/
{
"title": "My second blog entry",
"text": "Still trying this out...",
"date": "2014/01/01"
}
检索文档
GET /website/blog/123?pretty
- pretty:表示美化 json
检索文档的一部分
#_source只包含title、text字段
GET /website/blog/123?_source=title,text
#仅返回_source
GET /website/blog/123/_source
检索多个文档
POST /_mget
{
"docs": [
{
"_index": "website",
"_type": "blog",
"_id": 2
},
{
"_index": "website",
"_type": "pageviews",
"_id": 1,
"_source": "views"
}
]
}
如果检索的文档在同一个 _index 中(甚至在同一个 _type 中),可以在 URL 中定义一个默认的 _index 或者_index/_type
POST /website/blog/_mget
{
"ids": [
"2",
"1"
]
}
更新文档
POST /website/blog/123
{
"title": "My first blog entry",
"text": "I am starting to get the hang of this...",
"date": "2014/01/02"
}
局部更新
POST /website/blog/123/_update
{
"doc": {
"tags": [
"testing"
],
"views": 0
}
}
删除文档
DELETE /website/blog/123
批量插入
POST test_search_index/doc/_bulk
{
"index": {
"_id": 1
}
}
{
"username": "alfred way",
"job": "java engineer",
"age": 18,
"birth": "1991-12-15",
"isMarried": false
}
{
"index": {
"_id": 2
}
}
{
"username": "alfred",
"job": "java senior engineer and java specialist",
"age": 28,
"birth": "1980-05-07",
"isMarried": true
}
{
"index": {
"_id": 3
}
}
{
"username": "lee",
"job": "java and ruby engineer",
"age": 22,
"birth": "1985-08-07",
"isMarried": false
}
{
"index": {
"_id": 4
}
}
{
"username": "lee junior way",
"job": "ruby engineer",
"age": 23,
"birth": "1986-08-07",
"isMarried": false
}
3.3 Search API(URI)
GET /_search #查询所有索引文档
GET /my_index/_search #查询指定索引文档
GET /my_index1,my_index2/_search #多索引查询
GET /my_index/_search?q=user:alfred #指定字段查询
GET /my_index/_search?q=keyword&df=user&sort=age:asc&from=4&size=10&timeout=1s
- q:指定查询的语句
- df:指定默认查询的字段,如果不指定,查询全部字段
- sort:排序
- timeout:指定超时时间,默认不超时
- form、size:用于分页
term 与 phrase
- term 相当于单词查询
- phrase 相当于词语查询
泛查询
- 即不指定默认查询字段的查询
指定字段
- name:alfread
Group
- 使用括号指定匹配的规则
布尔操作符
- AND(&&)、OR(||)、NOT(!),例如 name:(tom NOT lee) 表示 name 字段中可以包含 tom 但一定不包含 lee
- +、-分别对应 must 和 must_not,例如 name:(tom +lee -alfred) 表示 name 字段中,一定包含 lee,一定不包含 alfred,可以包含 tom
范围查询
- age:[1 TO 10] #1<=age<=10
- age:[1 TO 10} #1<=age<10
- age:[1 TO ] #1<=age
- age:[* TO 10] #age<=10
- gae:>=1 #age>=1
- age:(>=1&&<=10) #1<=age<=10
- age:(+>=1 +<=10) #1<=age<=10
通配符查询
- ?:1 个字符,例如 name:t?m
- *:0 或多个字符,例如 name:tom*
正则表达式
- name:/[mb]oat/
模糊匹配 fuzzy query
- name:roam~1 匹配与 roam 差一个字母的词,比如 from、roams
近似度查询 proximity search
- “fox quick”~5 以 term 为单位进行差异比较,比如 qucik fox、qucik brown for
3.4 Search API(Request Body Search)
Match Query 对字段作全文检索,最基本和常用的查询类型
GET test_search_index/_search
{
"profile": true,
"query": {
"match": {
"username": "alfred way"
}
}
}
通过 operator 参数可以控制单词间的匹配关系,可选项为 or 和 and
GET test_search_index/_search
{
"query": {
"match": {
"username": {
"query": "alfred way",
"operator": "and"
}
}
}
}
4 Elasticsearch 数据结构
核心数据类型
- 字符串型: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
复杂数据类型
- 数组类型:array
- 对象类型:object
- 嵌套类型:nested object
地理位置数据类型
- geo_point(点)
- geo_shape(形状)
专用类型
- 记录IP地址:ip
- 实现自动补全 completion
- 记录分词数:token_count
1、创建 mapping
PUT my_index
{
"mappings": {
"doc": {
"properties": {
"username": {
"type": "text",
"fields": {
"pinyin": {
"type": "text"
}
}
}
}
}
}
}
2、创建文档
PUT my_index1/doc/1
{
"username": "haha heihei"
}
3、查询
GET my_index/_search
{
"query": {
"match": {
"username.pinyin": "haha"
}
}
}
Dynamic Mapping
PUT /test_index/doc/1
{
"username": "alfred",
"age": 1
}
age自动识别为long类型,username识别为text类型
PUT test_index/doc/1
{
"username": "samualz",
"age": 14,
"birth": "1991-12-15",
"year": 18,
"tags": [
"boy",
"fashion"
],
"money": "100.1"
}
birth自动识别为日期。。。。
自定义日期识别格式
PUT my_index
{
"mappings": {
"doc": {
"dynamic_date_formats": [
"yyyy-MM-dd",
"yyyy/MM/dd"
]
}
}
}
关闭日期自动识别
PUT my_index
{
"mappings": {
"doc": {
"date_detection": false
}
}
}
字符串是数字时,默认不会自动识别为整形,因为字符串中出现数字时完全合理的,Numeric_datection 可以开启字符串中数字的自动识别
PUT my_index
{
"mappings": {
"doc": {
"numeric_datection": true
}
}
}
5 Elasticsearch 原理
5.1 Elasticsearch 数据存储
5.1.1 Elasticsearch 存储方式
面向文档:
Elasticsearch 是面向文档的,这意味着它可以存储整个对象或文档,它在存储的时候还会对文档进行索引,使文档的内容可以被搜索,在 Elasticsearch 中,可以对文档进行索引、搜索、排序、过滤,这就是 Elasticsearch 能够执行复杂的全文检索的原因。
JSON:
Elasticsearch 使用 JSON,作为文档序列化格式,JSON 目前已成为 NoSQL 领域的标准格式,简洁容易阅读,并且对 JSON 做索引比在表结构中做索引容易的多。
5.1.2 Elasticsearch 存储结构
[图片上传失败...(image-575404-1572113601393)]
POST djm/doc
{
"name": "zhangsan",
"age": 10
}
- _index:文档所在索引名称
- _type:文档所在类型名称
- _id:文档唯一id
- _uid:组合id,由_type和_id组成(6.x后,_type不再起作用,同-_id)
- _source:文档的原始Json数据,包括每个字段的内容
- _all:将所有字段内容整合起来,默认禁用(用于对所有字段内容检索)
名称解释:
1、索引 Index
- 一个 Index 就是一个拥有几分相似特征的 document 的集合
- 一个 Index 包含很多文档,一个 Index 就代表了一类 document
- 比如说建立一个 productIndex,里面可能存放了所有的商品数据,所有的 productDocument
2、类型 type
- 相当于数据表
3、字段 field
- 相当于数据表的字段
4、文档 document
- 一个文档是一个可被索引的基本信息单元
5.2 Elasticsearch 搜索原理
5.2.1 正排索引和倒排索引
正排索引:记录文档Id到文档内容、单词的关联关系
倒排索引:记录单词到文档id的关联关系
5.2.2 分词
分词是指将文本转换成一系列单词(term or token)的过程,也可以叫做文本分析
Elasticsearch 自带的分词器
分词器(Analyzer) | 特点 |
---|---|
Standard(es默认) | 支持多语言,按词切分并做小写处理 |
Simple | 按照非字母切分,小写处理 |
Whitespace | 按照空格来切分 |
Stop | 去除语气助词,如the、an、的、这等 |
Keyword | 不分词 |
Pattern | 正则分词,默认\w+,即非字词符号做分割符 |
Language | 常见语言的分词器(30+) |
5.2.3 IK 分词器
将 ik 解压到 elasticsearch-6.3.1\plugins 并重命名为 analysis-ik,即可加载 ik 分词器
IK 提供了两个分词算法 ik_smart 、ik_max_word,其中 ik_smart 为最少切分,ik_max_word 为最细粒度划分
#Request
POST _analyze
{
"tokenizer": "ik_smart",
"text": "我是程序员"
}
#Response
{
"tokens": [
{
"token": "我",
"start_offset": 0,
"end_offset": 1,
"type": "CN_CHAR",
"position": 0
},
{
"token": "是",
"start_offset": 1,
"end_offset": 2,
"type": "CN_CHAR",
"position": 1
},
{
"token": "程序员",
"start_offset": 2,
"end_offset": 5,
"type": "CN_WORD",
"position": 2
}
]
}
#Request
POST _analyze
{
"tokenizer": "ik_max_word",
"text": "我是程序员"
}
#Response
{
"tokens": [
{
"token": "我",
"start_offset": 0,
"end_offset": 1,
"type": "CN_CHAR",
"position": 0
},
{
"token": "是",
"start_offset": 1,
"end_offset": 2,
"type": "CN_CHAR",
"position": 1
},
{
"token": "程序员",
"start_offset": 2,
"end_offset": 5,
"type": "CN_WORD",
"position": 2
},
{
"token": "程序",
"start_offset": 2,
"end_offset": 4,
"type": "CN_WORD",
"position": 3
},
{
"token": "员",
"start_offset": 4,
"end_offset": 5,
"type": "CN_CHAR",
"position": 4
}
]
}
5.3 集群操作原理
一个节点就是一个 Elasticsearch 实例,而一个集群是由多个节点组成,它们具有相同的集群名称,它们协同工作,分享数据和负载,当加入新的节点或者删除一个节点时,集群就会感知到,并平衡数据
5.3.1 属于介绍
节点 node
- 一个节点是集群中的一个服务器,作为集群的一部分,它存储数据,参与集群的索引和搜索
- 一个节点也有一个名字标识,默认是一个随机的漫威角色的名字,在启动的时候被确定
- 一个节点可以通过配置集群名字的方式加入集群,默认所有节点都会被加入到 ElasticSearch 集群
分片和复制 shards & replias
- 一个索引可以存储超出单个结点硬件限制的大量数据;比如,一个具有 10 亿文档的索引占据 1TB 的磁盘空间,而任一节点都没有这样大的磁盘空间,或者单个节点处理搜索请求,响应太慢, 为了解决这个问题,es 提供了将索引划分成多份的能力,这些份就叫做分片
- 在一个网络/云的环境里,失败随时都可能发生,在某个分片/节点不知怎么的就处于离线状态,或者由于任何原因消失了,这种情况下,有一个故障转移机制是非常有用并且是强烈推荐的,为此,es 允许你创建分片的一份或多份拷贝,这些拷贝叫做复制分片,或者直接叫复制
集群健康有三种状态:
颜色 | 意义 |
---|---|
green | 所有主要分片和复制分片都可用 |
yellow | 所有主要分片可用,但不是所有复制分片都可用 |
red | 不是所有的主要分片都可用 |
故障转移
- 在单一节点上运行意味着有单点故障的风险——没有数据备份,幸运的是,要防止单点故障,我们唯一需要做的就是启动另一个节点
- 第二个节点已经加入集群,三个复制分片也已经被分配了——分别对应三个主分片,这意味着在丢失任意一个节点的情况下依旧可以保证数据的完整性
- 文档的索引将首先被存储在主分片中,然后并发复制到对应的复制节点上,这可以确保我们的数据在主节点和复制节点上都可以被检索
5.3.2 路由
当你索引一个文档,它被存储在单独一个主分片上,Elasticsearch 是如何知道文档属于哪个分片的呢?当你创建一个新文档,它是如何知道是应该存储在分片 1 还是分片 2 上的呢?
进程不能是随机的,因为我们将来要检索文档
算法决定:
shard = hash(routing) % number_of_primary_shards
routing 值是一个任意字符串,它默认是 _id 但也可以自定义
为什么主分片的数量只能在创建索引时定义且不能修改?
如果主分片的数量在未来改变了,所有先前的路由值就失效了,文档也就永远找不到了
所有的文档 API(get、index、delete、bulk、update、mget)都接收一个 routing 参数,它用来自定义文档到分片的映射,自定义路由值可以确保所有相关文档——例如属于同一个人的文档——被保存在同一分片上
5.3.3 操作数据节点工作流程
[图片上传失败...(image-4aef1e-1572113601393)]
- 客户端给 Node1 发送新建、索引或删除请求
- 节点使用文档的 _id 确定文档属于分片 0,它转发请求到Node3,分片 0 位于这个节点上
- Node3 在主分片上执行请求,如果成功,它转发请求到相应的位于 Node1 和 Node2 的复制节点上,当所有的复制节点报告成功,Node3 报告成功到请求的节点,请求的节点再报告给客户端
5.3.4 检索流程
[图片上传失败...(image-f07c-1572113601393)]
- 客户端给 Node1 发送 GET 请求
- 节点使用文档的 _id 确定文档属于分片 0,分片 0 对应的复制分片在三个节点上都有,此时,它转发请求到 Node 2
- Node2 返回文档给 Node 1 然后返回给客户端
对于读请求,为了平衡负载,请求节点会为每个请求选择不同的分片——它会循环所有分片副本
可能的情况是,一个被索引的文档已经存在于主分片上却还没来得及同步到复制分片上,这时复制分片会报告文档未找到,主分片会成功返回文档,一旦索引请求成功返回给用户,文档则在主分片和复制分片都是可用的