Elasticsearch 是什么?
The Elastic Stack, 包括 Elasticsearch、 Kibana、 Beats 和 Logstash(也称为 ELK Stack)。能够安全可靠地获取任何来源、任何格式的数据,然后实时地对数据进行搜索、分析和可视化。
Elaticsearch,简称为 ES, ES 是一个开源的高扩展的分布式全文搜索引擎, 是整个 ElasticStack 技术栈的核心。它可以近乎实时的存储、检索数据;本身扩展性很好,可以扩展到上百台服务器,处理 PB 级别的数据。
elastic
英 [ɪˈlæstɪk] 美 [ɪˈlæstɪk]
n. 橡皮圈(或带);松紧带
adj. 橡皮圈(或带)的;有弹性的;有弹力的;灵活的;可改变的;可伸缩的
全文搜索引擎
Google,百度类的网站搜索,它们都是根据网页中的关键字生成索引,我们在搜索的时候输入关键字,它们会将该关键字即索引匹配到的所有网页返回;还有常见的项目中应用日志的搜索等等。对于这些非结构化的数据文本,关系型数据库搜索不是能很好的支持。
一般传统数据库,全文检索都实现的很鸡肋,因为一般也没人用数据库存文本字段。进行全文检索需要扫描整个表,如果数据量大的话即使对 SQL 的语法优化,也收效甚微。建立了索引,但是维护起来也很麻烦,对于 insert 和 update 操作都会重新构建索引。
基于以上原因可以分析得出,在一些生产环境中,使用常规的搜索方式,性能是非常差的:
官方网址
官方文档
Elasticsearch 7.8.0下载页面
正排索引(传统)
id | content |
---|---|
1001 | my name is zhang san |
1002 | my name is li si |
倒排索引
keyword | id |
---|---|
name | 1001, 1002 |
zhang | 1001 |
Elasticsearch 是面向文档型数据库,一条数据在这里就是一个文档。 为了方便大家理解,我们将 Elasticsearch 里存储文档数据和关系型数据库 MySQL 存储数据的概念进行一个类比
ES 里的 Index 可以看做一个库,而 Types 相当于表, Documents 则相当于表的行。这里 Types 的概念已经被逐渐弱化, Elasticsearch 6.X 中,一个 index 下已经只能包含一个type, Elasticsearch 7.X 中, Type 的概念已经被删除了
首先,当我们对记录进行修改时,es会把数据同时写到内存缓存区和translog中。而这个时候数据是不能被搜索到的,只有数据形成了segmentFile,才会被搜索到。默认情况下,es每隔一秒钟执行一次refresh,可以通过参数index.refresh_interval来修改这个刷新间隔或者搜索时加上?refresh=wait_for强制刷新,但是会造成刷新频次过高会造成性能下降,表示如果1秒内有请求立即更新并可见,执行refresh主要做三件事:
1、所有在内存缓冲区中的文档被写入到一个新的segment中,但是没有调用fsync,因此内存中的数据可能丢失;
2、segment被打开使得里面的文档能够被搜索到;
3、清空内存缓冲区;
translog的相当于事务日志,记录着所有对Elasticsearch的操作记录,也是对Elasticsearch的一种备份。因为并不是写到segment就表示数据落到磁盘了,实际上segment是存储在系统缓存(page cache)中的,只有达到一个周期或者数据量达到一定值,才会flush到磁盘上。这个时候如果系统内存中的segment丢失,是可以通过translog来恢复的。这个flush过程主要做了三件事:
1、往磁盘里写入commit point信息。
2、文件系统中的segment,fsync到磁盘。
3、清空translog文件。
translog可以保证缓存中的segment的恢复,但translog也不是实时也磁盘的,也就是说,内存中的translog丢了的话,也会有丢失数据的可能。所以translog也要进行flush。translog的flush主要有三个条件:
1、可以设置是否在某些操作之后进行强制flush,比如索引的删除或批量请求之后。
2、translog大小超过512mb或者超过三十分钟会强制对segment进行flush,随后会强制对translog进行flush,这种情况缓存中的translog在flush之后会被清空。
3、默认5s,会强制对translog进行flush。最小值可配置100ms。
6.3版本显示保留translog文件的最长持续时间。默认为12h。
参考官网:Elasticsearch Guide | Elastic
refresh,flush 和fsync的区别
1.refresh是将缓冲队列buffer里数据刷入文件缓冲系统生成索引文件segement,该segement数据才能被查询到,保证查询属性可见
2.flush是将索引文件segement数据持久化到硬盘(触发机制是translog文件超过512mb或者30分钟强强制刷新segement)
3.fsyncd是将translog 持久化到硬盘(每5秒执行一次) 写入translog其实也是在内存中。translog 和segement持久化到硬盘是两回事。
持久化的translog文件中存的是所有索引成segement的数据但还未持久化到硬盘的内容,一旦segement持久化到硬盘translog会清空。
参考:https://elasticsearch.cn/question/3847
Elasticsearch是一个建立在全文搜索引擎库Apache Lucene 基础上的分布式搜索引擎,Lucene最早的版本是2000年发布的,距今已经18年,是当今最先进,最高效的全功能开源搜索引擎框架。
Lucene中包含了四种基本数据类型,分别是:
上述四种类型在Elasticsearch中同样存在,意思也一样。
Lucene中存储的索引主要分为三种类型:
倒排索引是lucene的核心索引类型,采用链表的数据结构,倒排索引中的key就是一个term,value就是以doc_id形成的链表结构。
Term Doc_1 Doc_2
-------------------------
Quick | | X
The | X |
brown | X | X
dog | X |
dogs | | X
fox | X |
foxes | | X
in | | X
jumped | X |
lazy | X | X
leap | | X
over | X | X
quick | X |
summer | | X
the | X |
------------------------
现在,如果我们想搜索 quick brown ,我们只需要查找包含每个词条的文档:
Term Doc_1 Doc_2
-------------------------
brown | X | X
quick | X |
------------------------
Total | 2 | 1
这里分别匹配到了doc1和doc2,但是doc1匹配度要高于doc2。
倒排索引中的value有四种存储类型:
正排索引类似关系型数据库的存储模式,作用是通过doc_id和field_name可以快速定位到指定doc的特定字段值。DocValues的key是doc_id+field_name,value是field_value。
ES默认会对所有字段进行正排索引,但并不是所有字段都需要DocValues。所以合理配置DocValues可以节省存储空间。DocValues的使用场景一般是:需要针对某个Field排序、聚合、过滤和script。
存储的是Document的完整信息,包括所有field_name和field_value。Store的key是doc_id,value是field_name+field_value。对于上诉中需要聚合和排序的Field并没有开启DocValues的情况,依然可以实现排序和聚合,会从Store中获取要排序聚合的字段值。
Lucene本身不支持分布式,Elasticsearch通过_routing实现分布式的架构。我们可以通过_routing来实现不同doc分布在不同的Shard上,我们可以自己定义也可以系统自动分配。对于指定_routing的插入和查询,性能上会更好。
Lucene中没有主键索引,并且id是在Segment中唯一。那么Elasticsearch是如何实现doc_id唯一?Elasticsearch中有个系统字段_id,来决定doc唯一。_id是在用户可见层度上的id值,实际上Elasticsearch内部会把_id存储成_uid(_uid =index_type + '#' + _id)。_uid只会存储倒排和原文,目的就是为了通过id可以快速索引到doc。这里需要注意的是,在Elasticsearch6.x版本以后,一个index只支持一个type,这也就意味着_id和_uid概念几乎相同。以后的版本中,Elasticsearch会取消type。
Elasticsearch通过_version字段来保证文档的一致性。更多关于文档的一致性和锁机制的参考:ElasticSearch干货(一):锁机制
Elasticsearch通过_source字段来存储doc原文。这个字段非常重要。Lucene的update是覆盖,是不支持针对doc中特定字段进行修改的。但Elasticsearch支持对特定字段的修改,就是基于_source字段实现的。关于Elasticsearch的update详细内容,参考:ElasticSearch干货(二):index、create、update区别
Elasticsearch通过_field_names字段来判断doc中是否存在某个字段。_field_names的存储形式为倒排,可以快速判断出是否包含某个field_name。
Lucene中Segment一旦创建不可修改。那么Elasticsearch如何实现实时修改并索引数据的呢?详细参考:ElasticSearch原理(三):写入流程
关于原理和索引先介绍到这里,主要这里还是聚焦与如何实现几个关键需求?
默认情况下,es每隔一秒钟执行一次refresh,可以通过参数index.refresh_interval来修改这个刷新间隔或者搜索时加上?refresh=wait_for强制刷新,但是会造成刷新频次过高会造成性能下降,表示如果1秒内有请求立即更新并可见。另外由于没有生成segment,也就是说不能通过索引来获取,但是可以通过直接get by id来获取单条记录。注意:在并发写入的时候,特别是updateByQuery的时候,很容易出现复写或者是丢失的情况,坑很多,所以最好要频繁地去更新同一个索引的同一条数据。
ES的搜索是分2个阶段进行的,即Query阶段和Fetch阶段。 Query阶段比较轻量级,通过查询倒排索引,获取满足查询结果的文档ID列表。 而Fetch阶段比较重,需要将每个shard的结果取回,在协调结点进行全局排序。 通过From+size这种方式分批获取数据的时候,随着from加大,需要全局排序并丢弃的结果数量随之上升,性能越来越差。
1、精确查询
在elasticsearch 中输入查询条件,一般会匹配到很多结果,是因为analyzer的存在:
Each element in the result represents a single term:
{ "tokens": [ { "token": "text", "start_offset": 0, "end_offset": 4, "type": "
", "position": 1 }, { "token": "to", "start_offset": 5, "end_offset": 7, "type": " ", "position": 2 }, { "token": "analyze", "start_offset": 8, "end_offset": 15, "type": " ", "position": 3 } ] }
必须要将字段设置为not_analyzed 才可以,如下:
PUT /my_store { "mappings" : { "products" : { "properties" : { "productID" : { "type" : "string", "index" : "not_analyzed" } } } } }
1、must、should
GET /test_index/_search
{
"query": {
"bool": {
"must": { "match": { "name": "tom" }},
"should": [
{ "match": { "hired": true }},
{ "bool": {
"must": { "match": { "personality": "good" }},
"must_not": { "match": { "rude": true }}
}}
],
"minimum_should_match": 1
}
}
}
在es中,使用组合条件查询是其作为搜索引擎检索数据的一个强大之处,在前几篇中,简单演示了es的查询语法,但基本的增删改查功能并不能很好的满足复杂的查询场景,比如说我们期望像mysql那样做到拼接复杂的条件进行查询该如何做呢?es中有一种语法叫bool,通过在bool里面拼接es特定的语法可以做到大部分场景下复杂条件的拼接查询,也叫复合查询
首先简单介绍es中常用的组合查询用到的关键词,
filter:过滤,不参与打分
must:如果有多个条件,这些条件都必须满足 and与
should:如果有多个条件,满足一个或多个即可 or或
must_not:和must相反,必须都不满足条件才可以匹配到 !非
发生 描述
must
该条款(查询)必须出现在匹配的文件,并将有助于得分。
filter
子句(查询)必须出现在匹配的文档中。然而不像 must查询的分数将被忽略。Filter子句在过滤器上下文中执行,这意味着评分被忽略,子句被考虑用于高速缓存。
should
子句(查询)应该出现在匹配的文档中。如果 bool查询位于查询上下文中并且具有mustor filter子句,则bool即使没有should查询匹配,文档也将匹配该查询 。在这种情况下,这些条款仅用于影响分数。如果bool查询是过滤器上下文 或者两者都不存在,must或者filter至少有一个should查询必须与文档相匹配才能与bool查询匹配。这种行为可以通过设置minimum_should_match参数来显式控制 。
must_not
子句(查询)不能出现在匹配的文档中。子句在过滤器上下文中执行,意味着评分被忽略,子句被考虑用于高速缓存。因为计分被忽略,0所有文件的分数被返回。
3、模糊查询
前缀查询:匹配包含具有指定前缀的项(not analyzed)的字段的文档。前缀查询对应 Lucene 的 PrefixQuery 。
案例
GET /_search
{ "query": {
"prefix" : { "user" : { "value" : "ki", "boost" : 2.0 } }
}
}
正则表达式查询:egexp (正则表达式)查询允许您使用正则表达式进行项查询。有关支持的正则表达式语言的详细信息,请参阅正则表达式语法。第一个句子中的 “项查询” 意味着 Elasticsearch 会将正则表达式应用于由该字段生成的项,而不是字段的原始文本。注意: regexp (正则表达式)查询的性能很大程度上取决于所选的正则表达式。匹配一切像 “.*” ,是非常慢的,使用回顾正则表达式也是如此。如果可能,您应该尝试在正则表达式开始之前使用长前缀。通配符匹配器 “.*?+” 将主要降低性能。
案例
GET /_search
{
"query": {
"regexp":{
"name.first":{
"value":"s.*y",
"boost":1.2
}
}
}
}
‘
通配符查询:匹配与通配符表达式具有匹配字段的文档(not analyzed)。支持的通配符是 “*”,它匹配任何字符序列(包括空字符);还有 “?”,它匹配任何单个字符。请注意,此查询可能很慢,因为它需要迭代多个项。为了防止极慢的通配符查询,通配符项不应以通配符 “*” 或 “?” 开头。通配符查询对应 Lucene 的 WildcardQuery 。
案例
GET /_search
{
"query": {
"wildcard" : { "user" : { "value" : "ki*y", "boost" : 2.0 } }
}
}
###模糊查询数据量越大效率越低,当查询内容较多,数据量较大时建议将该字段设置成text进行分词,然后通过match进行匹配。
默认情况下,返回的结果是按照 相关性 进行排序的——最相关的文档排在最前。 在本章的后面部分,我们会解释 相关性 意味着什么以及它是如何计算的, 不过让我们首先看看 sort 参数以及如何使用它。
为了按照相关性来排序,需要将相关性表示为一个数值。在 Elasticsearch 中, 相关性得分 由一个浮点数进行表示,并在搜索结果中通过 _score 参数返回, 默认排序是 _score 降序。
有时,相关性评分对你来说并没有意义。例如,下面的查询返回所有 user_id 字段包含 1 的结果:
GET /_search
{
"query" : {
"bool" : {
"filter" : {
"term" : {
"user_id" : 1
}
}
}
}
}
里没有一个有意义的分数:因为我们使用的是 filter (过滤),这表明我们只希望获取匹配 user_id: 1 的文档,并没有试图确定这些文档的相关性。 实际上文档将按照随机顺序返回,并且每个文档都会评为零分。
1.1、按照字段的值排序
在这个案例中,通过时间来对 tweets 进行排序是有意义的,最新的 tweets 排在最前。 我们可以使用 sort 参数进行实现:
GET /_search
{
"query" : {
"bool" : {
"filter" : { "term" : { "user_id" : 1 }}
}
},
"sort": { "date": { "order": "desc" }}
}
1.2、多级排序
假定我们想要结合使用 date 和 _score 进行查询,并且匹配的结果首先按照日期排序,然后按照相关性排序:
GET /_search
{
"query" : {
"bool" : {
"must": { "match": { "tweet": "manage text search" }},
"filter" : { "term" : { "user_id" : 2 }}
}
},
"sort": [
{ "date": { "order": "desc" }},
{ "_score": { "order": "desc" }}
]
}
排序条件的顺序是很重要的。结果首先按第一个条件排序,仅当结果集的第一个 sort 值完全相同时才会按照第二个条件进行排序,以此类推。
多级排序并不一定包含 _score 。你可以根据一些不同的字段进行排序, 如地理距离或是脚本计算的特定值。
1.3、字段多值的排序
一种情形是字段有多个值的排序, 需要记住这些值并没有固有的顺序;一个多值的字段仅仅是多个值的包装,这时应该选择哪个进行排序呢?
对于数字或日期,你可以将多值字段减为单值,这可以通过使用 min 、 max 、 avg 或是 sum 排序模式 。 例如你可以按照每个 date 字段中的最早日期进行排序,通过以下方法:
"sort": {
"dates": {
"order": "asc",
"mode": "min"
}
}
"浅"分页可以理解为简单意义上的分页。它的原理很简单,就是查询前20条数据,然后截断前10条,只返回10-20的数据。这样其实白白浪费了前10条的查询。
GET test_dev/_search
{
"query": {
"bool": {
"filter": [
{
"term": {
"age": 28
}
}
]
}
},
"size": 10,
"from": 20,
"sort": [
{
"timestamp": {
"order": "desc"
},
"_id": {
"order": "desc"
}
}
]
}
其中,from定义了目标数据的偏移值,size定义当前返回的数目。默认from为0,size为10,即所有的查询默认仅仅返回前10条数据。
在这里有必要了解一下from/size的原理:
因为es是基于分片的,假设有5个分片,from=100,size=10。则会根据排序规则从5个分片中各取回100条数据数据,然后汇总成500条数据后选择最后面的10条数据。
做过测试,越往后的分页,执行的效率越低。总体上会随着from的增加,消耗时间也会增加。而且数据量越大,就越明显!
es默认的from+size的分页方式返回的结果数据集不能超过1万点,超过之后返回的数据越多性能就越低;
这是因为es要计算相似度排名,需要排序整个整个结果集,假设我们有一个index它有5个shard,现在要读取1000到1010之间的这10条数据,es内部会在每个shard上读取1010条数据,然后返回给计算节点,这里有朋友可能问为啥不是10条数据而是1010条呢?这是因为某个shard上的10条数据,可能还没有另一个shard上top10之后的数据相似度高,所以必须全部返回,然后在计算节点上,重新对5050条数据进行全局排序,最后在选取top 10出来,这里面排序是非常耗时的,所以这个数量其实是指数级增长的,到后面分页数量越多性能就越下降的厉害,而且大量的数据排序会占用jvm的内存,很有可能就OOM了,这也是为什么es默认不允许读取超过1万条数据的原因。
from+size查询在10000-50000条数据(1000到5000页)以内的时候还是可以的,但是如果数据过多的话,就会出现深分页问题。
为了解决上面的问题,elasticsearch提出了一个scroll滚动的方式。
scroll 类似于sql中的cursor,使用scroll,每次只能获取一页的内容,然后会返回一个scroll_id。根据返回的这个scroll_id可以不断地获取下一页的内容,所以scroll并不适用于有跳页的情景。
GET test_dev/_search?scroll=5m
{
"query": {
"bool": {
"filter": [
{
"term": {
"age": 28
}
}
]
}
},
"size": 10,
"from": 0,
"sort": [
{
"timestamp": {
"order": "desc"
},
"_id": {
"order": "desc"
}
}
]
}
然后我们可以通过数据返回的_scroll_id读取下一页内容,每次请求将会读取下10条数据,直到数据读取完毕或者scroll_id保留时间截止:
GET _search/scroll
{
"scroll_id": "DnF1ZXJ5VGhlbkZldGNoBQAAAAAAAJZ9Fnk1d......",
"scroll": "5m"
}
注意:请求的接口不再使用索引名了,而是 _search/scroll,其中GET和POST方法都可以使用。
scroll删除
根据官方文档的说法,scroll的搜索上下文会在scroll的保留时间截止后自动清除,但是我们知道scroll是非常消耗资源的,所以一个建议就是当不需要了scroll数据的时候,尽可能快的把scroll_id显式删除掉。
清除指定的scroll_id:
DELETE _search/scroll/DnF1ZXJ5VGhlbkZldGNo.....
清除所有的scroll:
DELETE _search/scroll/_all
scroll 的方式,官方的建议不用于实时的请求(一般用于数据导出),因为每一个 scroll_id 不仅会占用大量的资源,而且会生成历史快照,对于数据的变更不会反映到快照上。
search_after 分页的方式是根据上一页的最后一条数据来确定下一页的位置,同时在分页请求的过程中,如果有索引数据的增删改查,这些变更也会实时的反映到游标上。但是需要注意,因为每一页的数据依赖于上一页最后一条数据,所以无法跳页请求。
为了找到每一页最后一条数据,每个文档必须有一个全局唯一值,官方推荐使用 _uid 作为全局唯一值,其实使用业务层的 id 也可以。
GET test_dev/_search
{
"query": {
"bool": {
"filter": [
{
"term": {
"age": 28
}
}
]
}
},
"size": 20,
"from": 0,
"sort": [
{
"timestamp": {
"order": "desc"
},
"_id": {
"order": "desc"
}
}
]
}
使用sort返回的值搜索下一页:
GET test_dev/_search
{
"query": {
"bool": {
"filter": [
{
"term": {
"age": 28
}
}
]
}
},
"size": 10,
"from": 0,
"search_after": [
1541495312521,
"d0xH6GYBBtbwbQSP0j1A"
],
"sort": [
{
"timestamp": {
"order": "desc"
},
"_id": {
"order": "desc"
}
}
]
}
只是整合记录少有原创