文章目录
- ES是什么
- 应用企业
- 应用场景
- 核心理论/概念
- ES原理
- Query then Fetch
- Query
- Fetch
- 潜在问题
- 深度分页
- Analysis (分析器)
- Character filter (字符过滤器)
- Tokenizer (分词器)
- Token filter (标记过滤器)
- demo
- 排序
- 主要名词介绍
- 主要操作流程
- 选举Master
- 写操作
- 读操作
- 数据操作逻辑
- 配置
- 相关API
- 易出问题的点
- 分页问题
- 由于lucene的原因,所有数据会做扁平化出来
- ES和Lucene相关概念对比
- 加载测试数据
- 处理并发读写操作
- 并发控制理论
- ES并发
- 相关概念
ES是什么
- 基于 Lucene 的搜索引擎,Lucene 被认为是迄今为止最先进、性能最好的、功能最全的搜索引擎库。
- 分布式的实时文档存储,每个字段 可以被索引与搜索
- 随时可用和按需扩容(水平扩容)
- 最受欢迎的企业搜索引擎,其次是 Apache Solr
- 能胜任上百个服务节点的扩展,并支持 PB 级别的结构化或者非结构化数据
- 对于使用者来说,不用担心主副问题,天然分布式保证数据安全
- Java 开发
- 不支持事物
- 由于需要分词、索引、merge,写速度慢
应用企业
- 维基百科:全文搜索,高亮关键字、搜索纠错
- StackOverflow:全文搜索,地理位置、more-like-this
- Github:1300亿行代码检索
应用场景
- 在线商店
- 日志或者交易数据 分析和挖掘
- 报价系统、价格变更系统:发生满足条件的变更通知关注者
- 大数据分析需求: kibana 统计分析数据
核心理论/概念
ES原理
Query then Fetch
Query
- 搜索数据请求发送到 Coordinating Node, 该节点会把请求随机的发送给分片(在所有主副分片中随机选择,比如 主分片设置3,副本分片设置为1时,会随机选择三个分片:[P|R]0, [P|R]1, [P][R]2)
- 在选中的分片中,根据 (from,size) 获得排序后的文档ID、排序值等数据(
0-> from+size
),返回给 Coordinating Node
Fetch
- 把所有数据,进行排序,获得 (from,size) 的数据ID
- 通过 multi get 请求,到对应分片获取详细信息
潜在问题
- 每个分片获得文档个数= from+size
- 最终协调节点需要处理:number_of_shard * (from+size)
- 深度分页必然导致性能问题,需要特别注意
- 相关性算分
- 每个分片都是基于自己的分片上的数据进行相关度算分。当数据少或者分布不均匀时,算分会不准确。所以在设置分片大小时,要特别注意
- 数据量不大时,设置分片数为1
- 数据量大时,保证数据均匀分布
- 通过
_search?search_type=dfs_query_then_fetch
进行全局完整的相关性算分,但需要耗费更多CPU和内存,执行效率低下
深度分页
- ES默认设置 10000 作为深度分页的界限。from+size>10000 会报错
- search_after
- 要求:排序唯一,所以一般把ID加入排序
- 每次只能向后查询
- 原理:每个分片都记录当前读取/消费的位置,比如查询 990-1000,每个分片都有前面已经被消费的数据的偏移位置(一个990),本次查询,只需要从每个分片读取10条数据,然后消费10条记录,调整各个分片的偏移位置
- Scroll API
- 原理: 第一次请求就创建一个快照。在生成快照后,插入的数据是无法查询到的
#search after
DELETE users
POST users/_doc
{"name":"user1","age":10}
POST users/_doc
{"name":"user2","age":11}
POST users/_doc
{"name":"user2","age":12}
POST users/_doc
{"name":"user2","age":13}
POST users/_count
POST users/_search
{
"size": 1,
"query": {
"match_all": {}
},
"sort": [
{"age": "desc"} ,
{"_id": "asc"}
]
}
# 每个文档都有 sort 字段,将数据作为 search_after的参数
POST users/_search
{
"size": 1,
"query": {
"match_all": {}
},
"search_after":
[
10,
"ZQ0vYGsBrR8X3IP75QqX"],
"sort": [
{"age": "desc"} ,
{"_id": "asc"}
]
}
#Scroll API
DELETE users
POST users/_doc
{"name":"user1","age":10}
POST users/_doc
{"name":"user2","age":20}
POST users/_doc
{"name":"user3","age":30}
POST users/_doc
{"name":"user4","age":40}
# 返回结果中会包含 scroll_id
POST /users/_search?scroll=5m
{
"size": 1,
"query": {
"match_all" : {
}
}
}
POST users/_doc
{"name":"user5","age":50}
# "scroll" : "1m" 表示 scroll_id有效期(重新计算有效期)
POST /_search/scroll
{
"scroll" : "1m",
"scroll_id" : "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAWAWbWdoQXR2d3ZUd2kzSThwVTh4bVE0QQ=="
}
Analysis (分析器)
通过执行 analyzer 实现,analyzer包含 Character Filter
,Tokenizer
, Token Filter
(有序经过上述三步处理).
更多查看
Character filter (字符过滤器)
- 接收原始数据,可在原始数据上增加/删除/修改字符.
- 把结果输出到
Tokenizer
. - 输入是Text,输出也是Text.
- 如把Text中的 (٠١٢٣٤٥٦٧٨٩) 转变成 (0123456789), 删除 HTML 的标签(如<b>)
Tokenizer (分词器)
- 对来自
Character filter
的输入数据(Text),进行分词处理,输出 Terms. - 输入是Text, 输出是Terms
Token filter (标记过滤器)
Token filter 从 Tokenizer 获得terms,可对terms执行的操作有,其操作对象是单个的term:
- 修改:如 大写变小写(Goods, goods)
- 删除:如 删除停顿符(stopwords)
- 添加:如 添加同义词
- 输入是 terms, 输出是 terms
demo
POST _analyze
{
"tokenizer": "standard",
"text": "<p>I'm so <b>happy</b>!</p>"
}
POST _analyze
{
"tokenizer": "standard",
"char_filter": [
"html_strip"
],
"text": "<p>I'm so <b>happy</b>!</p>"
}
POST _analyze
{
"tokenizer": "keyword",
"char_filter": [
"html_strip"
],
"text": "<p>I'm so <b>happy</b>!</p>"
}
POST _analyze
{
"tokenizer": "letter",
"text": "The 2 QUICK Brown-Foxes jumped over the lazy dog's bone."
}
POST _analyze
{
"tokenizer": "standard",
"filter": [ "lowercase", "asciifolding" ],
"text": "Is This Déja vu?"
}
排序
- 默认根据算分排序
- 可指定特定字段排序,支持多字段排序
- 默认不支持 Text 类型字段排序,Text 字段设置
fielddata: true
,就可支持排序 - 排序是针对原始字段,倒排无法发挥作用
- ES两种排序实现:
- Fielddata
- Doc Values(列式存储,对 Text 类型无效)
主要名词介绍
倒排索引
:Inverted index,索引表中的每一项都包括一个属性值和具有该属性值的各记录的地址。由于不是由记录来确定属性值,而是由属性值来确定记录的位置,因而称为倒排索引(inverted index)。带有倒排索引的文件我们称为倒排索引文件,简称倒排文件(inverted file)。倒排索引是ES中最核心的概念,是其可以进行快速搜索的原因集群
: Cluster,一个或者多个节点组成,他们有相同的 cluster.name节点
:Node,一个ES实例。集群中一个节点会被选为 主节点。主节点只管理集群级别的事物,不参与文档级别的变更或者搜索。每个节点都知道文档存在哪里
分片
:Shard,最小级别的工作单元,他只是保存了索引中所有数据的一部分,是一个数据的容器。主分片决定了可存储的索引的量。副本分片只是副本
。分片是一个Lucene Index, 最大可存储Document数为:2,147,483,519=Integer.MAX_VALUE - 128。
主分片
: Primary Shard, 简称Shard
- 数量在建索引时确定,不可修改
- 其不可修改原因:hash(routing) % number_of_shards, number_of_shards指主分片数
- 新建索引时,通过
index.number_of_shards
设置
副本分片
:Replica Shared, 简称Replica
- 新建时可设置,运用期可修改
- 新建索引或运行期间,通过
index.number_of_replicas
设置,表示每个主分片对应的副本分片数
索引
:Index,类似数据库名(database)
- 一个存储关联数据的地方
- 一个用来指向一个或者多个分片的逻辑命名空间
- 我们应用程序直接和索引通信,不和分片通信
文档的索引首先会存于主分片,然后存于复分片。成功后确保数据可在所有主复分片被检索。
类型
:Type,类似数据库表名(不太合适的类比).Deprecated in 6.0.0,7.X已经不支持了,只能是 _doc
Id
:文档id,可自定义或ES自动生成Mapping
:映射,定义字段type和相关设置CCR
:cross-cluster replication,跨集群复制,同网络数据迁移非常快速.单集群遇到灾难时,数据安全性没法保障。为了应对这个问题,采用CCR。CCR is active-passive,主集群负责读、写,复本集群只负责读
-
leader indices
:CCR跨集群复制的源索引。存在于远端,被复制到follower indices
-
follower index
:CCR跨集群复制的目标索引。存在于本地集群,复制leader indices
主要操作流程
选举Master
写操作
- 步骤
- Coordinating Node接收请求
- routing(ID) -> 主分片
- 数据校验(格式是否正确,id是否合法…),本地 indexing or deleting
- 通知 副分片 同步数据(
in-sync copies set
并行同步) - 所有副分片同步成功后,Coordinating Node告诉客户端操作完成
- 出现问题场景
- 主分片出现问题,
Master
会收到通知,Master
会选择一个副分片作为主分片 - 如果副分片出现问题,会通知
Master
把该分片从in-sync copies set
中删除,然后再通知client
-
Master
会启动新的分片,保证集群健康
读操作
- 步骤
- 协调节点
coordinating node
收到读请求, 解析请求,并把请求发送到相关的分片 - 从 分片副本集合
the shard replication group
(包含主副分片) 里面选择一个相关的分片获取有效数据, 采用 round robin算法(即轮询算法) - 发送分片级别请求到选定分片
- 协调节点合并请求结果,主要是 ID 及 排序信息等
- 根据ID获取数据
- 返回结果
- 读操作失败:会发送请求到另一个分片(来自
the shard replication group
的分片)。为了快速响应,Search
、Multi Search
、Bulk
、Multi Get
分片失败后,立马返回(快速失败,不会把请求发送到另一个分片)
数据操作逻辑
内存缓存(不可检索)
->未提交段(内存索引,可检索)
->已提交段(内存索引,可检索)
- 内存缓存 -> 未提交段:1s 会自动刷新一次,也可手动刷新
_refresh
。刷新后,内存缓存会被清除,事务日志会保留 - 未提交段 -> 已提交段:每30min,或事务日志过大,进行一次 Flush 到硬盘操作
- 当段较小时,未提交的段和已提交段之间可能会进行合并。对应命令:_optimize
- 索引和搜索一个文档之间是有一定延迟的。新文档会在1s后被搜索到。
- 事务日志:记录所有没有Flush到硬盘的操作
配置
- Xms Xmx 设置成相同的值, 如:
-e ES_JAVA_OPTS="-Xms16g -Xmx16g"
- Xmx不超过机器物理内存 50%, 以便给内核文件系统缓存留出足够物理内存
- 一般来讲,每个分片的大小在20GB-40GB
相关API
- update: 删除旧文档,索引新建文档,并不是我们认为的 update
- bulk: 单个操作失败,还是会执行其他操作,整个请求结果正常返回,每个操作都会有对应的操作结果(按请求排序)。如有需要,可判断结果做后续处理.
易出问题的点
分页问题
- 避免深度分页
- 分页逻辑:每次分页需要获得包括当前页及之前的所有数据,一起排序
- 处理深度分页方案:search after or scroll
由于lucene的原因,所有数据会做扁平化出来
- 原始数据
{
"followers": [
{ "age": 35, "name": "Mary White"},
{ "age": 26, "name": "Alex Jones"},
{ "age": 19, "name": "Lisa Smith"}
]
}
- Lucene保存数据
{
"followers.age": [19, 26, 35],
"followers.name": [alex, jones, lisa, smith, mary, white]
}
ES和Lucene相关概念对比
- Lucene索引是ES分片
- ES索引是分片的集合,索引的数据会存于多个分片
加载测试数据
- accounts.json, 格式如下:
{"index":{"_id":"1"}}
{"account_number":1,"balance":39225,"firstname":"Amber","lastname":"Duke","age":32,"gender":"M","address":"880 Holmes Lane","employer":"Pyrami","email":"amberduke@pyrami.com","city":"Brogan","state":"IL"}
{"index":{"_id":"6"}}
{"account_number":6,"balance":5686,"firstname":"Hattie","lastname":"Bond","age":36,"gender":"M","address":"671 Bristol Street","employer":"Netagy","email":"hattiebond@netagy.com","city":"Dante","state":"TN"}
{"index":{"_id":"13"}}
{"account_number":13,"balance":32838,"firstname":"Nanette","lastname":"Bates","age":28,"gender":"F","address":"789 Madison Street","employer":"Quility","email":"nanettebates@quility.com","city":"Nogal","state":"VA"}
- 导入数据
curl -H "Content-Type: application/json" -XPOST "localhost:9200/bank/_bulk?pretty&refresh" --data-binary "@accounts.json"
curl "localhost:9200/_cat/indices?v"
处理并发读写操作
并发控制理论
- 悲观并发控制:认为并发、冲突是大概率事件。要修改,先获得锁
- 乐观并发控制:认为并发、冲突是小概率事件。发生冲突,由业务自己决定如何处理
ES并发
- ES采用乐观锁
- 文档是不可变更的,更新文档时,源文档会被标记删除,同时新增一个新文档。
- 内部版本控制:
if_seq_no + if_primary_term
- 外部版本控制:
version + version_type=external
- Mysql + ES 搭配:Mysql提供事务保证,ES提供高性能搜索。Mysql同步数据到ES,可用Mysql version 或 last update 时间戳作为外部的version,实现冲突检测
DELETE products
PUT products
PUT products/_doc/1
{
"title":"iphone",
"count":100
}
GET products/_doc/1
PUT products/_doc/1?if_seq_no=1&if_primary_term=1
{
"title":"iphone",
"count":100
}
PUT products/_doc/1?version=30000&version_type=external
{
"title":"iphone",
"count":100
}
相关概念
- TF/IDF
- TF : 词频率
- IDF : 反转文档率
- 当根据 TF/IDF 算法获得得分跟实际得分有差距的原因:本地IDF和全局IDF有区别,数据越大,这个差距会越小。避免这种问题有两种方法(测试使用,实际场景数据量大,不需要):
- 为了避免这个问题,可以创建只有一个主分片的索引,那么本地IDF就是全局IDF。
- 查询加参数 ?search_type=dfs_query_then_fetch