技术原理
索引文档
索引文档分为单个文档和多个文档。
单个文档
新建单个文档所需要的步骤顺序:
- 客户端向 Node 1 发送新建、索引或者删除请求。
- 节点使用文档的 _id 确定文档属于分片 0 。请求会被转发到 Node 3,因为分片 0 的主分片目前被分配在 Node 3 上。
- Node 3 在主分片上面执行请求。如果成功了,它将请求并行转发到 Node 1 和 Node 2 的副本分片上。一旦所有的副本分片都报告成功, Node 3 将向协调节点报告成功,协调节点向客户端报告成功。
多个文档
使用 bulk 修改多个文档步骤顺序:
- 客户端向 Node 1 发送 bulk 请求。
- Node 1 为每个节点创建一个批量请求,并将这些请求并行转发到每个包含主分片的节点主机。
- 主分片一个接一个按顺序执行每个操作。当每个操作成功时,主分片并行转发新文档(或删除)到副本分片,然后执行下一个操作。 一旦所有的副本分片报告所有操作成功,该节点将向协调节点报告成功,协调节点将这些响应收集整理并返回给客户端。
索引文档的实现机制
写操作的关键点
在考虑或分析一个分布式系统的写操作时,一般需要从下面几个方面考虑:
- 可靠性:或者是持久性,数据写入系统成功后,数据不会被回滚或丢失。
- 一致性:数据写入成功后,再次查询时必须能保证读取到最新版本的数据,不能读取到旧数据。
- 原子性:一个写入或者更新操作,要么完全成功,要么完全失败,不允许出现中间状态。
- 隔离性:多个写入操作相互不影响。
- 实时性:写入后是否可以立即被查询到。
- 性能:写入性能,吞吐量到底怎么样。
Elasticsearch作为分布式系统,通过如何保证的呢?
- 可靠性:由于Lucene的设计中不考虑可靠性,在Elasticsearch中通过Replica和TransLog两套机制保证数据的可靠性。
- 一致性:Lucene中的Flush锁只保证Update接口里面Delete和Add中间不会Flush,但是Add完成后仍然有可能立即发生Flush,导致Segment可读。这样就没法保证Primary和所有其他Replica可以同一时间Flush,就会出现查询不稳定的情况,这里只能实现最终一致性。
- 原子性:Add和Delete都是直接调用Lucene的接口,是原子的。当部分更新时,使用Version和锁保证更新是原子的。
- 隔离性:仍然采用Version和局部锁来保证更新的是特定版本的数据。
- 实时性:使用定期Refresh Segment到内存,并且Reopen Segment方式保证搜索可以在较短时间(比如1秒)内被搜索到。通过将未刷新到磁盘数据记入TransLog,保证对未提交数据可以通过ID实时访问到。
- 性能:性能是一个系统性工程,所有环节都要考虑对性能的影响,在Elasticsearch中,在很多地方的设计都考虑到了性能,一是不需要所有Replica都返回后才能返回给用户,只需要返回特定数目的就行;二是生成的Segment现在内存中提供服务,等一段时间后才刷新到磁盘,Segment在内存这段时间的可靠性由TransLog保证;三是TransLog可以配置为周期性的Flush,但这个会给可靠性带来伤害;四是每个线程持有一个Segment,多线程时相互不影响,相互独立,性能更好;五是系统的写入流程对版本依赖较重,读取频率较高,因此采用了versionMap,减少热点数据的多次磁盘IO开销。Lucene中针对性能做了大量的优化。
Elasticsearch的写
Elasticsearch采用多Shard方式,通过配置routing规则将数据分成多个数据子集,每个数据子集提供独立的索引和搜索功能。当写入文档的时候,根据routing规则,将文档发送给特定Shard中建立索引。这样就能实现分布式了。
此外,Elasticsearch整体架构上采用了一主多副的方式:
每个Index由多个Shard组成,每个Shard有一个主节点和多个副本节点,副本个数可配。但每次写入的时候,写入请求会先根据_routing规则选择发给哪个Shard,Index Request中可以设置使用哪个Filed的值作为路由参数,如果没有设置,则使用Mapping中的配置,如果mapping中也没有配置,则使用_id作为路由参数,然后通过_routing的Hash值选择出Shard(在OperationRouting类中),最后从集群的Meta中找出出该Shard的Primary节点。
请求接着会发送给Primary Shard,在Primary Shard上执行成功后,再从Primary Shard上将请求同时发送给多个Replica Shard,请求在多个Replica Shard上执行成功并返回给Primary Shard后,写入请求执行成功,返回结果给客户端。
这种模式下,写入操作的延时就等于latency = Latency(Primary Write) + Max(Replicas Write)。只要有副本在,写入延时最小也是两次单Shard的写入时延总和,写入效率会较低,但是这样的好处也很明显,避免写入后,单机或磁盘故障导致数据丢失,在数据重要性和性能方面,一般都是优先选择数据,除非一些允许丢数据的特殊场景。
采用多个副本后,避免了单机或磁盘故障发生时,对已经持久化后的数据造成损害,但是Elasticsearch里为了减少磁盘IO保证读写性能,一般是每隔一段时间(比如5分钟)才会把Lucene的Segment写入磁盘持久化,对于写入内存,但还未Flush到磁盘的Lucene数据,如果发生机器宕机或者掉电,那么内存中的数据也会丢失,这时候如何保证?
对于这种问题,Elasticsearch学习了数据库中的处理方式:增加CommitLog模块,Elasticsearch中叫TransLog
。
在每一个Shard中,写入流程分为两部分,先写入Lucene,再写入TransLog。
写入请求到达Shard后,先写Lucene文件,创建好索引,此时索引还在内存里面,接着去写TransLog,写完TransLog后,刷新TransLog数据到磁盘上,写磁盘成功后,请求返回给用户。这里有几个关键点:
- 一是和数据库不同,数据库是先写CommitLog,然后再写内存,而Elasticsearch是先写内存,最后才写TransLog,一种可能的原因是Lucene的内存写入会有很复杂的逻辑,很容易失败,比如分词,字段长度超过限制等,比较重,为了避免TransLog中有大量无效记录,减少recover的复杂度和提高速度,所以就把写Lucene放在了最前面。
- 二是写Lucene内存后,并不是可被搜索的,需要通过Refresh把内存的对象转成完整的Segment后,然后再次reopen后才能被搜索,一般这个时间设置为1秒钟,导致写入Elasticsearch的文档,最快要1秒钟才可被从搜索到,所以Elasticsearch在搜索方面是NRT(Near Real Time)近实时的系统。
- 三是当Elasticsearch作为NoSQL数据库时,查询方式是GetById,这种查询可以直接从TransLog中查询,这时候就成了RT(Real Time)实时系统。
- 四是每隔一段比较长的时间,比如30分钟后,Lucene会把内存中生成的新Segment刷新到磁盘上,刷新后索引文件已经持久化了,历史的TransLog就没用了,会清空掉旧的TransLog。
数据更新
Update流程:
Lucene中不支持部分字段的Update,所以需要在Elasticsearch中实现该功能,具体流程如下:
- 收到Update请求后,从Segment或者TransLog中读取同id的完整Doc,记录版本号为V1。
- 将版本V1的全量Doc和请求中的部分字段Doc合并为一个完整的Doc,同时更新内存中的VersionMap。获取到完整Doc后,Update请求就变成了Index请求。 加锁。
- 再次从VersionMap中读取该id的最大版本号V2,如果VersionMap中没有,则从Segment或者TransLog中读取,这里基本都会从VersionMap中获取到。
检查版本是否冲突(V1==V2),如果冲突,则回退到开始的“Update doc”阶段,重新执行。如果不冲突,则执行最新的Add请求。 - 在Index Doc阶段,首先将Version + 1得到V3,再将Doc加入到Lucene中去,Lucene中会先删同id下的已存在doc id,然后再增加新Doc。写入Lucene成功后,将当前V3更新到VersionMap中。
- 释放锁,部分更新的流程就结束了。
查询文档
单个文档
从主分片或者副本分片检索文档的步骤顺序:
- 客户端向 Node 1(Master) 发送获取请求。
- 节点使用文档的 _id 来确定文档属于分片 0(根据_routering确认文档所在的Shard) 。分片 0 的副本分片存在于所有的三个节点上。 在这种情况下,它将请求转发到 Node 2 。
- Node 2 将文档返回给 Node 1 ,然后将文档返回给客户端。
在处理读取请求时,协调结点在每次请求的时候都会通过轮询所有的副本分片来达到负载均衡。
在文档被检索时,已经被索引的文档可能已经存在于主分片上但是还没有复制到副本分片。 在这种情况下,副本分片可能会报告文档不存在,但是主分片可能成功返回文档。 一旦索引请求成功返回给用户,文档在主分片和副本分片都是可用的。
多个文档
使用 mget 取回多个文档的步骤顺序:
使用单个 mget 请求取回多个文档所需的步骤顺序:
- 客户端向 Node 1(Master) 发送 mget 请求。
- Node 1 为每个分片构建多文档获取请求,然后并行转发这些请求到托管在每个所需的主分片或者副本分片的节点上。一旦收到所有答复, Node 1 构建响应并将其返回给客户端。
文档读取过程详解
所有的搜索系统一般都是两阶段查询:
- 第一阶段查询到匹配的DocID,
- 第二阶段再查询DocID对应的完整文档,这种在Elasticsearch中称为query_then_fetch。
除了一阶段,两阶段外,还有一种三阶段查询的情况。搜索里面有一种算分逻辑是根据TF(Term Frequency)和DF(Document Frequency)计算基础分,但是Elasticsearch中查询的时候,是在每个Shard中独立查询的,每个Shard中的TF和DF也是独立的,虽然在写入的时候通过_routing保证Doc分布均匀,但是没法保证TF和DF均匀,那么就有会导致局部的TF和DF不准的情况出现,这个时候基于TF、DF的算分就不准。
为了解决这个问题,Elasticsearch中引入了DFS查询,比如DFS_query_then_fetch,会先收集所有Shard中的TF和DF值,然后将这些值带入请求中,再次执行query_then_fetch,这样算分的时候TF和DF就是准确的,类似的有DFS_query_and_fetch。这种查询的优势是算分更加精准,但是效率会变差。
另一种选择是用BM25代替TF/DF模型。
第一阶段
todo
第二阶段
- 在初始查询阶段时,查询会广播到索引中每一个分片拷贝(主分片或者副本分片)。 每个分片在本地执行搜索并构建一个匹配文档的大小为 from + size 的优先队列。
- 每个分片返回各自优先队列中 所有文档的 ID 和排序值 给协调节点,它合并这些值到自己的优先队列中来产生一个全局排序后的结果列表。
- 接下来就是 取回阶段,协调节点辨别出哪些文档需要被取回并向相关的分片提交多个 GET 请求。每个分片加载并丰富文档,如果有需要的话,接着返回文档给协调节点。一旦所有的文档都被取回了,协调节点返回结果给客户端。
二阶段查询为例来介绍查询流程:
Client Node
1.Get Remove Cluster Shard
判断是否需要跨集群访问,如果需要,则获取到要访问的Shard列表。
2.Get Search Shard Iterator
获取当前Cluster中要访问的Shard,和上一步中的Remove Cluster Shard合并,构建出最终要访问的完整Shard列表。
这一步中,会根据Request请求中的参数从Primary Node和多个Replica Node中选择出一个要访问的Shard。
3.For Every Shard:Perform
遍历每个Shard,对每个Shard执行后面逻辑。
4.Send Request To Query Shard
将查询阶段请求发送给相应的Shard。
5.Merge Docs
上一步将请求发送给多个Shard后,这一步就是异步等待返回结果,然后对结果合并。这里的合并策略是维护一个Top N大小的优先级队列,每当收到一个shard的返回,就把结果放入优先级队列做一次排序,直到所有的Shard都返回。
翻页逻辑也是在这里,如果需要取Top 30~ Top 40的结果,这个的意思是所有Shard查询结果中的第30到40的结果,那么在每个Shard中无法确定最终的结果,每个Shard需要返回Top 40的结果给Client Node,然后Client Node中在merge docs的时候,计算出Top 40的结果,最后再去除掉Top 30,剩余的10个结果就是需要的Top 30~ Top 40的结果。
上述翻页逻辑有一个明显的缺点就是每次Shard返回的数据中包括了已经翻过的历史结果,如果翻页很深,则在这里需要排序的Docs会很多,比如Shard有1000,取第9990到10000的结果,那么这次查询,Shard总共需要返回1000 * 10000,也就是一千万Doc,这种情况很容易导致OOM。
另一种翻页方式是使用search_after,这种方式会更轻量级,如果每次只需要返回10条结构,则每个Shard只需要返回search_after之后的10个结果即可,返回的总数据量只是和Shard个数以及本次需要的个数有关,和历史已读取的个数无关。这种方式更安全一些,推荐使用这种。
如果有aggregate,也会在这里做聚合,但是不同的aggregate类型的merge策略不一样,具体的可以在后面的aggregate文章中再介绍。
6.Send Request To Fetch Shard
选出Top N个Doc ID后发送给这些Doc ID所在的Shard执行Fetch Phase,最后会返回Top N的Doc的内容。
Query Phase
接下来我们看第一阶段查询的步骤:
1.Create Search Context
创建Search Context,之后Search过程中的所有中间状态都会存在Context中,这些状态总共有50多个,具体可以查看DefaultSearchContext或者其他SearchContext的子类。
2.Parse Query
解析Query的Source,将结果存入Search Context。这里会根据请求中Query类型的不同创建不同的Query对象,比如TermQuery、FuzzyQuery等,最终真正执行TermQuery、FuzzyQuery等语义的地方是在Lucene中。
这里包括了dfsPhase、queryPhase和fetchPhase三个阶段的preProcess部分,只有queryPhase的preProcess中有执行逻辑,其他两个都是空逻辑,执行完preProcess后,所有需要的参数都会设置完成。
由于Elasticsearch中有些请求之间是相互关联的,并非独立的,比如scroll请求,所以这里同时会设置Context的生命周期。
同时会设置lowLevelCancellation是否打开,这个参数是集群级别配置,同时也能动态开关,打开后会在后面执行时做更多的检测,检测是否需要停止后续逻辑直接返回。
3.Get From Cache
判断请求是否允许被Cache,如果允许,则检查Cache中是否已经有结果,如果有则直接读取Cache,如果没有则继续执行后续步骤,执行完后,再将结果加入Cache。
4.Add Collectors
Collector主要目标是收集查询结果,实现排序,对自定义结果集过滤和收集等。这一步会增加多个Collectors,多个Collector组成一个List。
- FilteredCollector:先判断请求中是否有Post Filter,Post Filter用于Search,Agg等结束后再次对结果做Filter,希望Filter不影响Agg结果。如果有Post Filter则创建一个FilteredCollector,加入Collector List中。
- PluginInMultiCollector:判断请求中是否制定了自定义的一些Collector,如果有,则创建后加入Collector List。
- MinimumScoreCollector:判断请求中是否制定了最小分数阈值,如果指定了,则创建MinimumScoreCollector加入Collector List中,在后续收集结果时,会过滤掉得分小于最小分数的Doc。
- EarlyTerminatingCollector:判断请求中是否提前结束Doc的Seek,如果是则创建EarlyTerminatingCollector,加入Collector List中。在后续Seek和收集Doc的过程中,当Seek的Doc数达到Early Terminating后会停止Seek后续倒排链。
- CancellableCollector:判断当前操作是否可以被中断结束,比如是否已经超时等,如果是会抛出一个TaskCancelledException异常。该功能一般用来提前结束较长的查询请求,可以用来保护系统。
- EarlyTerminatingSortingCollector:如果Index是排序的,那么可以提前结束对倒排链的Seek,相当于在一个排序递减链表上返回最大的N个值,只需要直接返回前N个值就可以了。这个Collector会加到Collector List的头部。EarlyTerminatingSorting和EarlyTerminating的区别是,EarlyTerminatingSorting是一种对结果无损伤的优化,而EarlyTerminating是有损的,人为掐断执行的优化。
- TopDocsCollector:这个是最核心的Top N结果选择器,会加入到Collector List的头部。TopScoreDocCollector和TopFieldCollector都是TopDocsCollector的子类,TopScoreDocCollector会按照固定的方式算分,排序会按照分数+doc id的方式排列,如果多个doc的分数一样,先选择doc id小的文档。而TopFieldCollector则是根据用户指定的Field的值排序。
5.lucene::search
这一步会调用Lucene中IndexSearch的search接口,执行真正的搜索逻辑。每个Shard中会有多个Segment,每个Segment对应一个LeafReaderContext,这里会遍历每个Segment,到每个Segment中去Search结果,然后计算分数。
搜索里面一般有两阶段算分,第一阶段是在这里算的,会对每个Seek到的Doc都计算分数,为了减少CPU消耗,一般是算一个基本分数。这一阶段完成后,会有个排序。然后在第二阶段,再对Top 的结果做一次二阶段算分,在二阶段算分的时候会考虑更多的因子。二阶段算分在后续操作中。
具体请求,比如TermQuery、WildcardQuery的查询逻辑都在Lucene中,后面会有专门文章介绍。
6.rescore
根据Request中是否包含rescore配置决定是否进行二阶段排序,如果有则执行二阶段算分逻辑,会考虑更多的算分因子。二阶段算分也是一种计算机中常见的多层设计,是一种资源消耗和效率的折中。
Elasticsearch中支持配置多个Rescore,这些rescore逻辑会顺序遍历执行。每个rescore内部会先按照请求参数window选择出Top window的doc,然后对这些doc排序,排完后再合并回原有的Top 结果顺序中。
7.suggest::execute()
如果有推荐请求,则在这里执行推荐请求。如果请求中只包含了推荐的部分,则很多地方可以优化。推荐不是今天的重点,这里就不介绍了,后面有机会再介绍。
8.aggregation::execute()
如果含有聚合统计请求,则在这里执行。Elasticsearch中的aggregate的处理逻辑也类似于Search,通过多个Collector来实现。在Client Node中也需要对aggregation做合并。aggregate逻辑更复杂一些,就不在这里赘述了,后面有需要就再单独开文章介绍。
上述逻辑都执行完成后,如果当前查询请求只需要查询一个Shard,那么会直接在当前Node执行Fetch Phase。
- ExplainFetchSubPhase
- DocValueFieldsFetchSubPhase
- ScriptFieldsFetchSubPhase
- FetchSourceSubPhase
- VersionFetchSubPhase
- MatchedQueriesFetchSubPhase
- HighlightPhase
- ParentFieldSubFetchPhase
除了系统默认的8种外,还有通过插件的形式注册自定义的功能,这些SubPhase中最重要的是Source和Highlight,Source是加载原文,Highlight是计算高亮显示的内容片断。
上述多个SubPhase会针对每个Doc顺序执行,可能会产生多次的随机IO,这里会有一些优化方案,但是都是针对特定场景的,不具有通用性。
Fetch Phase执行完后,整个查询流程就结束了。
Fetch Phase
Elasticsearch作为搜索系统时,或者任何搜索系统中,除了Query阶段外,还会有一个Fetch阶段,这个Fetch阶段在数据库类系统中是没有的,是搜索系统中额外增加的阶段。搜索系统中额外增加Fetch阶段的原因是搜索系统中数据分布导致的,在搜索中,数据通过routing分Shard的时候,只能根据一个主字段值来决定,但是查询的时候可能会根据其他非主字段查询,那么这个时候所有Shard中都可能会存在相同非主字段值的Doc,所以需要查询所有Shard才能不会出现结果遗漏。同时如果查询主字段,那么这个时候就能直接定位到Shard,就只需要查询特定Shard即可,这个时候就类似于数据库系统了。另外,数据库中的二级索引又是另外一种情况,但类似于查主字段的情况,这里就不多说了。
基于上述原因,第一阶段查询的时候并不知道最终结果会在哪个Shard上,所以每个Shard中管都需要查询完整结果,比如需要Top 10,那么每个Shard都需要查询当前Shard的所有数据,找出当前Shard的Top 10,然后返回给Client Node。如果有100个Shard,那么就需要返回100 * 10 = 1000个结果,而Fetch Doc内容的操作比较耗费IO和CPU,如果在第一阶段就Fetch Doc,那么这个资源开销就会非常大。所以,一般是当Client Node选择出最终Top N的结果后,再对最终的Top N读取Doc内容。通过增加一点网络开销而避免大量IO和CPU操作,这个折中是非常划算的。
Fetch阶段的目的是通过DocID获取到用户需要的完整Doc内容。这些内容包括了DocValues,Store,Source,Script和Highlight等,具体的功能点是在SearchModule中注册的,系统默认注册的有:
第三阶段
todo
使用相关性进行搜索
在进行相关性搜索前先了解下面问题:
- Lucene和EIasticsearch内部打分是如何运作的
- 提升特定查询或字段的得分
- 使用解释的API接口来理解词频、逆文档频率、相关性得分
- 通过重新计算文档子集的得分来减少评分操作的性能影响
- 使用function—score查询,获取终极的打分能力
- 字段数据的缓存,以及它是如何影响实例的
文档的评分使用的是TF-IDF或者BM25。
其他评分算法:
- 随机性分歧(Divergencefromrandomness),即DFR相似度;
- 基于信息的(lnformation),即IB相似度;
- LMDirichIet相似度;
- LMJeIinekMercer相似度。
ElasticSearch如何给文档评分
有两种配置评分:
- 索引字段评分
- ElasticSearch配置评分
索引字段评分-使用内置评分算法
PUT /mall_user
{"mappings": {"properties": {"name": {"type": "text","similarity": "BM25", "fields": {"raw": {"type": "keyword"}}},"age": {"type": "integer"},"birth": {"type": "date","format": "yyyy-MM-dd"}}}
}
在字段similarity配置内置的评分算法。
索引字段评分-自定义算法:
PUT /mall_user
{"settings": {"similarity":{"customSimilarity":{"type":"BM25","k1":1.2,"b":0.75,"discount_overlaps":false}}},"mappings": {"properties": {"name": {"type": "text","similarity": "customSimilarity", "fields": {"raw": {"type": "keyword"}}},"age": {"type": "integer"},"birth": {"type": "date","format": "yyyy-MM-dd"}}}
}
在setting.similarity参数定义评分算法,并且在字段的similarity指定自定义的评分算法。
BM25有3种主要的设置,即k1、b和discount_overlaps:
- k1:k1控制对于得分而言词频(词条出现在文档里的频繁程度,或者是TF)的重要性。
- b:b是介于0到1之间的数值,它控制了文档篇幅对于得分的影响程度。
- discount_overlaps:discount_overlaps的设置告知Elasticsearch,在某个字段中,多个分词出现在同一个位置,是否应该影响长度的标准化。默认值是true.
默认情况下,k1被设置为1.2,而b被设置为0.75。
如果希望全局修改评分算法,在elasticsearch.yml配置文件中配置:
index.similarity.default.type: BM25
boosting
boosting是一个可以用来修改文档的相关性的程序。
boosting有两种类型,当索引或者查询文档的时候,可以提升一篇文档的得分。在索引期间修改的文档boosting是存储在索引中的,修改boosting值唯一的方法是重新索引这篇文档。鉴于此,我们当然建议用户使用查询期间的boosting,因为这样更为灵活,并允许用户改变主意,在不重新索引数据的前提下改变字段或者词条的重要性。
在索引期间boost文档是不推荐的。
1.在索引期间配置boost
PUT /mall_user
{"mappings": {"properties": {"name": {"type": "text","boost": 3.0, "fields": {"raw": {"type": "keyword"}}},"age": {"type": "integer"},"birth": {"type": "date","format": "yyyy-MM-dd"}}}
}
在设置该索引的映射后,任何自动索引的文档就拥有一个boost值,运用于name字段的词条中。再次强调一下,请记住这个boost的值是固定的(fixed),也就是说如果决定修改这个值,你必须重新索引文档。
在索引期间boost文档是不推荐的,原因:
- 精度丢失:boost的值是以低精度的数值存储在Lucene的内部索引结构中。只有一个字节用于存储浮点型数值,所以计算文档的最终得分时可能会丢失精度。
- boost是运用于一个词条的。因此,在被boost的字段中如果匹配上了多个词条,就意味着多次的boost,将会进一步增加字段的权重。
2.查询期间boosting
当进行搜索的时候,有几种方法进行boosting。如果使用基本的match、multi_match、simple_query_string或query_string查询,就可以基于每个词条或者每个字段来控制boost.几乎所有的Elasticsearch查询类型都支持boosting.如果这个还不够灵活,那么可以通过function_score查询,以更为精细的方式来控制boosting.
开始查询前先创建一个索引:
PUT /mall_goods_test
{"mappings": {"properties": {"title": {"type": "text","fields": {"raw": {"type": "keyword"}}},"attribute":{"type":"text"},"desc":{"type":"text"},"price":{"type": "double"}}}
}POST /mall_goods_test/_doc/1
{"title":"Apple iPhone 15 Pro Max - 全新旗舰智能手机","attribute":"256GB Pro","desc":"iPhone 15 Pro Max采用了全新的设计语言,机身采用航空级铝合金打造,正面是一块6.7英寸的超视网膜XDR显示屏,分辨率高达4K,显示效果惊艳。同时,这款手机还支持120Hz的高刷新率,让您在浏览网页、玩游戏时更加流畅。","price": 4999
}POST /mall_goods_test/_doc/2
{"title":"Apple iPhone 15 Pro Max - 全新旗舰智能手机","attribute":"512GB Pro Max","desc":"iPhone 15 Pro Max采用了全新的设计语言,机身采用航空级铝合金打造,正面是一块6.7英寸的超视网膜XDR显示屏,分辨率高达4K,显示效果惊艳。同时,这款手机还支持120Hz的高刷新率,让您在浏览网页、玩游戏时更加流畅。","price": 6999
}POST /mall_goods_test/_doc/3
{"title":"Apple iPhone 15 Pro Max - 全新旗舰智能手机","attribute":"1TB Pro Max","desc":"iPhone 15 Pro Max采用了全新的设计语言,机身采用航空级铝合金打造,正面是一块6.7英寸的超视网膜XDR显示屏,分辨率高达4K,显示效果惊艳。同时,这款手机还支持120Hz的高刷新率,让您在浏览网页、玩游戏时更加流畅。","price": 12999
}
match查询:
POST /mall_goods_test/_search
{"query": {"bool": {"should":[{"match": {"title": "iPhone 1TB"}},{"match": {"attribute": {"query": "iPhone 1TB","boost": 3.0}}}]}}
}
增加boost参数,默认1.0。在搜索中,我们认为attribute 的描述更加符合搜索的需求,通过设置boost参数实现。
当使用bool或and/or/not组合多个查询时,boost查询才有意义。
multi_match查询:
multi_match和match类似,多个字段指定同一个boost值。
POST /mall_goods_test/_search
{"query": {"multi_match": {"query": "iPhone 1TB","fields": ["title","attribute"],"boost": 3.0}}
}
或者使用^为各个字段指定boost值:
POST /mall_goods_test/_search
{"query": {"multi_match": {"query": "iPhone 1TB","fields": ["title^0.75","attribute^3"]}}
}
title:设置boost为0.75
attribute:设置boost为3
query_string 查询:
match、multi_match查询是为字段指定boost值,query_string可以为搜索文本中的某个词设置boost。
POST /mall_goods_test/_search
{"query": {"query_string": {"query": "iPhone 256GB^3"}}
}
认为256比iPhone关键词相关性更加高,为256设置更高的boost。
explain查看评分的详细信息:
POST /mall_goods_test/_search
{"query": {"query_string": {"query": "iPhone 256GB^3"}},"explain": true
}
256GB boost对评分的贡献比较大。
_explain查看为何没有匹配文档:
POST /mall_goods_test/_doc/1/_explain
{"query":{"match": {"title": "Huawei"}}
}
输出:
{"_index" : "mall_goods_test","_type" : "_doc","_id" : "1","matched" : false,"explanation" : {"value" : 0.0,"description" : "no matching term","details" : [ ]}
}
查看_id=1的文档为何没有匹配“Huawei”,从结果明显看出是不匹配。
function_score 打分,更加自由的打分。todo
Elasticsearch Function Score
1.Lucene和EIasticsearch内部打分是如何运作的?
确定文档和查询相关程度的过程被称为打分(scoring)。
Bulk请求
Elasticsearch写入请求类型
Elasticsearch中的写入请求类型,主要包括下列几个:Index(Create),Update,Delete和Bulk,其中前3个是单文档操作,后一个Bulk是多文档操作,其中Bulk中可以包括Index(Create),Update和Delete。
在6.0.0及其之后的版本中,前3个单文档操作的实现基本都和Bulk操作一致,甚至有些就是通过调用Bulk的接口实现的。估计接下来几个版本后,Index(Create),Update,Delete都会被当做Bulk的一种特例化操作被处理。这样,代码和逻辑都会更清晰一些。
下面,我们就以Bulk请求为例来介绍写入流程。
红色:Client Node。
绿色:Primary Node。
蓝色:Replica Node。
Client Node
1.Ingest Pipeline
在这一步可以对原始文档做一些处理,比如HTML解析,自定义的处理,具体处理逻辑可以通过插件来实现。在Elasticsearch中,由于Ingest Pipeline会比较耗费CPU等资源,可以设置专门的Ingest Node,专门用来处理Ingest Pipeline逻辑。
如果当前Node不能执行Ingest Pipeline,则会将请求发给另一台可以执行Ingest Pipeline的Node。
2.Auto Create Index
判断当前Index是否存在,如果不存在,则需要自动创建Index,这里需要和Master交互。也可以通过配置关闭自动创建Index的功能。
3.Set Routing
设置路由条件,如果Request中指定了路由条件,则直接使用Request中的Routing,否则使用Mapping中配置的,如果Mapping中无配置,则使用默认的_id字段值。
在这一步中,如果没有指定id字段,则会自动生成一个唯一的_id字段,目前使用的是UUID。
4.Construct BulkShardRequest
由于Bulk Request中会包括多个(Index/Update/Delete)请求,这些请求根据routing可能会落在多个Shard上执行,这一步会按Shard挑拣Single Write Request,同一个Shard中的请求聚集在一起,构建BulkShardRequest,每个BulkShardRequest对应一个Shard。
5.Send Request To Primary
这一步会将每一个BulkShardRequest请求发送给相应Shard的Primary Node。
Primary Node
Primary 请求的入口是在PrimaryOperationTransportHandler的messageReceived.
1.Index or Update or Delete
循环执行每个Single Write Request,对于每个Request,根据操作类型(CREATE/INDEX/UPDATE/DELETE)选择不同的处理逻辑。
其中,Create/Index是直接新增Doc,Delete是直接根据_id删除Doc,Update会稍微复杂些,我们下面就以Update为例来介绍。
2.Translate Update To Index or Delete
这一步是Update操作的特有步骤,在这里,会将Update请求转换为Index或者Delete请求。首先,会通过GetRequest查询到已经存在的同_id Doc(如果有)的完整字段和值(依赖_source字段),然后和请求中的Doc合并。同时,这里会获取到读到的Doc版本号,记做V1。
3.Parse Doc
这里会解析Doc中各个字段。生成ParsedDocument对象,同时会生成uid Term。在Elasticsearch中,_uid = type # _id,对用户,_Id可见,而Elasticsearch中存储的是_uid。这一部分生成的ParsedDocument中也有Elasticsearch的系统字段,大部分会根据当前内容填充,部分未知的会在后面继续填充ParsedDocument。
4.Update Mapping
Elasticsearch中有个自动更新Mapping的功能,就在这一步生效。会先挑选出Mapping中未包含的新Field,然后判断是否运行自动更新Mapping,如果允许,则更新Mapping。
5.Get Sequence Id and Version
由于当前是Primary Shard,则会从SequenceNumber Service获取一个sequenceID和Version。SequenceID在Shard级别每次递增1,SequenceID在写入Doc成功后,会用来初始化LocalCheckpoint。Version则是根据当前Doc的最大Version递增1。
6.Add Doc To Lucene
这一步开始的时候会给特定_uid加锁,然后判断该_uid对应的Version是否等于之前Translate Update To Index步骤里获取到的Version,如果不相等,则说明刚才读取Doc后,该Doc发生了变化,出现了版本冲突,这时候会抛出一个VersionConflict的异常,该异常会在Primary Node最开始处捕获,重新从“Translate Update To Index or Delete”开始执行。
如果Version相等,则继续执行,如果已经存在同id的Doc,则会调用Lucene的UpdateDocument(uid, doc)接口,先根据uid删除Doc,然后再Index新Doc。如果是首次写入,则直接调用Lucene的AddDocument接口完成Doc的Index,AddDocument也是通过UpdateDocument实现。
这一步中有个问题是,如何保证Delete-Then-Add的原子性,怎么避免中间状态时被Refresh?答案是在开始Delete之前,会加一个Refresh Lock,禁止被Refresh,只有等Add完后释放了Refresh Lock后才能被Refresh,这样就保证了Delete-Then-Add的原子性。
Lucene的UpdateDocument接口中就只是处理多个Field,会遍历每个Field逐个处理,处理顺序是invert index,store field,doc values,point dimension,后续会有文章专门介绍Lucene中的写入。
7.Write Translog
写完Lucene的Segment后,会以key-value的形式写TransLog,Key是_id,Value是Doc内容。当查询的时候,如果请求是GetDocByID,则可以直接根据_id从TransLog中读取到,满足NoSQL场景下的实时性要去。
需要注意的是,这里只是写入到内存的TransLog,是否Sync到磁盘的逻辑还在后面。
这一步的最后,会标记当前SequenceID已经成功执行,接着会更新当前Shard的LocalCheckPoint。
8.Renew Bulk Request
这里会重新构造Bulk Request,原因是前面已经将UpdateRequest翻译成了Index或Delete请求,则后续所有Replica中只需要执行Index或Delete请求就可以了,不需要再执行Update逻辑,一是保证Replica中逻辑更简单,性能更好,二是保证同一个请求在Primary和Replica中的执行结果一样。
9.Flush Translog
这里会根据TransLog的策略,选择不同的执行方式,要么是立即Flush到磁盘,要么是等到以后再Flush。Flush的频率越高,可靠性越高,对写入性能影响越大。
10.Send Requests To Replicas
这里会将刚才构造的新的Bulk Request并行发送给多个Replica,然后等待Replica的返回,这里需要等待所有Replica返回后(可能有成功,也有可能失败),Primary Node才会返回用户。如果某个Replica失败了,则Primary会给Master发送一个Remove Shard请求,要求Master将该Replica Shard从可用节点中移除。
这里,同时会将SequenceID,PrimaryTerm,GlobalCheckPoint等传递给Replica。
发送给Replica的请求中,Action Name等于原始ActionName + [R],这里的R表示Replica。通过这个[R]的不同,可以找到处理Replica请求的Handler。
11.Receive Response From Replicas
Replica中请求都处理完后,会更新Primary Node的LocalCheckPoint。
Replica Node
Replica 请求的入口是在ReplicaOperationTransportHandler的messageReceived.
1.Index or Delete
根据请求类型是Index还是Delete,选择不同的执行逻辑。这里没有Update,是因为在Primary Node中已经将Update转换成了Index或Delete请求了。
2.Parse Doc
3.Update Mapping
以上都和Primary Node中逻辑一致。
4.Get Sequence Id and Version
Primary Node中会生成Sequence ID和Version,然后放入ReplicaRequest中,这里只需要从Request中获取到就行。
5.Add Doc To Lucene
由于已经在Primary Node中将部分Update请求转换成了Index或Delete请求,这里只需要处理Index和Delete两种请求,不再需要处理Update请求了。比Primary Node会更简单一些。
6.Write Translog
7.Flush Translog
以上都和Primary Node中逻辑一致。
数据分片
查询
Elasticsearch查询的详细过程是复杂的,而它的核心部分就是倒排索引。这里将概述查询的过程,包括分片的运用、主从节点的交互以及数据排序和结果汇总。
-
查询分发
当一个查询到达Elasticsearch时,它首先被发送到一个协调节点(coordinating node)。这个节点负责解析查询,确定需要从哪些分片(shards)检索数据,并将查询请求转发到这些分片所在的节点。
-
主从节点交互
在Elasticsearch中,每个索引被分为多个分片,并且每个分片可以有一个或多个副本。副本分为主副本(primary shard)和从副本(replica shard)。查询可以在主分片或任何一个从分片上执行,这有助于分散读取负载并提高查询性能。协调节点通常会根据分片的当前负载情况来决定发送查询请求给主分片或某个从分片。
-
本地查询处理
查询被发送到负责存储相关分片的节点后,每个节点上的Elasticsearch进程会执行实际的查询操作。查询操作通常分为两个阶段:
-
查询阶段(Query Phase):在这一阶段中,Elasticsearch会检查查询条件并从倒排索引中查找匹配的文档ID。由于Elasticsearch使用的是倒排索引,它可以非常高效地进行文本搜索。
-
取回阶段(Fetch Phase):一旦文档ID被查找到,Elasticsearch就会进入取回阶段,此时会根据需要从存储中检索文档的完整内容。如果查询包括了排序或聚合,还可能需要在这个阶段对结果进行进一步处理。
-
-
结果排序与剪枝
如果查询请求指定了排序条件,各个节点需要在本地对查询结果进行排序。每个节点只返回最顶端的结果(例如,如果请求指定了
size: 10
,则每个节点只返回排名前10的结果)给协调节点,这样可以避免回传大量不必要的数据,提高效率。 -
结果汇总
协调节点收到所有涉及的节点返回的结果后,会进行最终的排序和汇总。然后协调节点会将最终的查询结果返回给客户端。
-
缓存
Elasticsearch还有一个查询缓存机制,它会缓存经常被执行的过滤器查询的结果。这能够使得相同或相似的后续查询更快地返回结果。
查询复杂性可以根据查询中涉及的文档数量、查询类型(如文本搜索、范围查询等)、是否有聚合操作等因素有很大的不同。尽管Elasticsearch旨在使查询尽可能快速,但一些复杂的查询还是会因为涉及大量的数据处理和传输而耗费较多时间。对于真正的性能要求,通常需要结合监控、索引优化、查询优化等一系列措施来确保系统的查询响应效率。
分析器
什么是分析
分析(analysis)是在文档被发送并加入倒排索引之前,Elasticsearch在其主体上进行的操作。在文档被加入索引之前,Elasticsearch让每个被分析字段经过一系列的处理步骤。
- 字符过滤:使用字符过滤器转变字符。
- 文本切分为分词:将文本切分为单个或多个分词。
- 分词过滤:使用分词过滤器转变每个分词。
- 分词索引:将这些分词存储到索引中。
使用到ElasticSearch的字符过滤器、分词器、分词过滤器和分词索引,它们便组成了分析器(analyzer)。
字符过滤器
字符过滤器将特定的字符序列转变为其他的字符序列。可以用于将HTML从文本中剥离,或者是将任意数量的字符转化为其他字符(也许是将"i love u 2”缩写的短消息纠正为"i love you too”),或者将“&”替换为“and”等。
分词器
分词器是从文本片段生成的,可能会产生任意数量(甚至是0)的分词(token)。例如,在英文中一个通用的分词是标准分词器,它根据空格、换行和破折号等其他字符,将文本分割为分词。
例如将字符串“share your experience with NoSql and big data technologies"分解为分词share、your、experience、with、NoSql、and、big、data 和 technologies。
分词过滤器
分词器文本块被转换为分词,将会对每个分词运用分词过滤器(tokenfilter)。分词过滤器可以将一个分词作为输人,然后根据需要进行修改,添加或者是删除。
最为有用的和常用的分词过滤器是小写分词过滤器,它将输人的分词变为小写,确保在搜索词条"nosql"的时候,可以发现关于“nosql"的聚会。分词可以经过多于1个的分词过滤器,每个过滤器对分词进行不同的操作,将数据塑造为最佳的形式,便于之后的索引。
分词索引
当经历了零个或者多个分词过滤器,它们将被发送到Lucene进行文档的索引。这些分词组成了倒排索引。
配置分析器
配置分析器方式:
- 当创建索引的时候,为特定的索引进行设置。
- 在Elasticsearch的配置文件中,设置全局的分析器。
通常来说,出于灵活性的考虑,在创建索引或者是配置映射的时候,指定分析器是更简单的。
创建索引配置分析器
分为两种,一种是索引全局分析器,另一种某个字段自定分析器
PUT /mall_order_test
{"settings": {"number_of_shards": 1,"number_of_replicas": 1,"index":{# 分析器"analysis": {"analyzer": {# 设置分析器名称"myCustomAnalyzer":{#定制化类型"token":"custom",# 字符过滤器"char_filter":["myCustomCharFilter"],# 分词器"tokenizer":"myCustomTokenizer",# 分词过滤器"filter":["myCustomFilter1","myCustomFilter2"],}}},# 定义字符过滤器"char_filter":{"myCustomCharFilter":{# 类型为映射(把字符转成其他字符)"type":"mapping","mappings":["ph=>f","u=>you"]}}# 分词器"tokenizer":{"myCustomTokenizer":{# 分词器为 letter"type":"letter"}},# 分词过滤器"filter":{"myCustomFilter1":{# 小写分词过滤器"type":"lowercase"},"myCustomFilter2":{# kstem就行词干处理"type":"kstem"}}}}, "mappings":{"properties":{"orderTile":{"type":"text",#指定分词器,es内置有多种analyzer:whitespace、standard、simple、stop"analyzer": "myCustomAnalyzer" }}}
}
如果想一个字段不被任何分析处理,需要增加not_analyzed(7.x之前) 或者 type=keyword(7.x之后):
PUT /mall_order_test
{"mappings":{"properties":{"orderTile":{"type":"keyword"}}}
}
用多字段类型来存储分析方式不同的文本
通常情况下,可以同时搜索字段分析后的文本和原始、未经分析的文本,是非常有用的。
PUT /mall_user
{"mappings": {"properties": {"name":{"type": "text", "analyzer": "standard","fields": {"raw":{# keyword 即不分词"type":"keyword"}}}}}
}
ElasticSearch配置分析器
index:analysis:analyer:myCustomAnalyzer:type: customchar_filter: [customCharFilter]tokenizer: customTokenizerfilter: [customFilter1,customFilter2]char_filter:customCharFilter:type: mappingmappings: ["pf=>f","u=>you"] tokenizer:customTokenizer:type: letterfilter:customFilter1:type: lowercase customFilter2:type: kstem
内置分析器
一个分析器包括一个可选的字符过滤器、一个单个分词器、0个或多个分词过滤器。
standard-标准分析器
当没有指定分析器的时候,标准分析器(standard analyzer)是文本的默认分析器。
它综合了对大多欧洲语言来说合理的默认模块,包括标准分词器、标准分词过滤器、小写转换分词过滤器和停用词分词过滤器。
simple-简单分析器
简单分析器(simpleanalyzer)就是那么简单!它只使用了小写转换分词器,这意味着在非字母处进行分词,并将分词自动转变为小写。
简单分析器对于亚洲语言来说效果不佳,因为亚洲语言不是根据空白来分词,所以请仅仅针对欧洲语言使用它。
whitespace-空白分析器
空白分析器(whitespaceanalyzer)什么事情都不做,只是根据空白将文本切分为若干分词,非常简单!
stop-停用词分析器
停用词分析器(analyzer)和简单分析器的行为很相像,只是在分词流中额外地过滤了停用词。
keyword-关键词分析器
关键词分析器(keywordanalyzer)将整个字段当作一个单独的分词。请记住,最好是将index设置指定为notanalyzed(7.x只有改为 type=keyword方式),而不是在映射中使用关键词分析器。
pattern-模式分析器
模板分析器(pattern analyzer)允许你指定一个分词切分的模式。但是,由于可能无论如何都要指定模式,通常更有意义的做法是使用定制分析器,组合现有的模式分词器和所需的分词过滤器。
Elasticsearch 的 Pattern Analyzer 是一个基于正则表达式的分词器(tokenizer),它允许用户使用正则表达式来定义如何将文本拆分为词(token)。下面举一个 Pattern Analyzer 的使用例子。
假设我们想要对一段日志数据进行分析,这些日志数据的格式大致如下:
127.0.0.1 - - [10/Oct/2000:13:55:36 -0700] "GET /apache_pb.gif HTTP/1.0" 200 2326
我们可能对从这些日志中提取出如 IP 地址、日期时间、请求方法、请求资源、响应状态码和响应大小等信息感兴趣。下面是如何在 Elasticsearch 中定义一个索引,使用 Pattern Analyzer 来实现这一目标。
PUT /log_index
{"settings": {"analysis": {"analyzer": {"log_analyzer": {"type": "pattern","pattern": "\\s+", // 使用空白字符做为分词分隔符"lowercase": false // 不将词条转为小写}}}},"mappings": {"properties": {"log": {"type": "text","analyzer": "log_analyzer","fielddata": true // 由于后续可能需要对 log 字段进行聚合操作,需要开启 fielddata}}}
}
在这个例子中,我们定义了一个名为 log_analyzer
的分词器,其类型为 pattern
。我们指定了一个简单的正则表达式作为 pattern
,它用于匹配一个或多个空白字符(\\s+
),以此来分割日志的每一部分。我们还设置了 lowercase
为 false
来保持文本的原始大小写,因为对于日志分析,区分大小写可能是重要的。
创建了索引和相应的 Pattern Analyzer 之后,就可以开始索引数据了:
POST /log_index/_doc/1
{"log": "127.0.0.1 - - [10/Oct/2000:13:55:36 -0700] \"GET /apache_pb.gif HTTP/1.0\" 200 2326"
}
接着,你可以通过执行搜索查询或聚合操作来分析日志数据:
GET /log_index/_search
{"query": {"match": {"log": "GET"}}
}
这个搜索将会返回包含 GET
请求方法的所有日志条目。
请注意,为简单起见,这个例子只用了空格来分割日志条目,并没有详细地去解析每个字段,如 IP 地址等。在实际应用中,你可能需要一个更复杂的正则表达式来准确提取和分析每个感兴趣的部分。此外,对于复杂的日志分析,可能需要定义更详细的字段映射来区分不同的日志组成部分,如分别用不同字段存储 IP 地址、日期时间等。
snowball-雪球分析器
雪球分析器(snowbal1analyzer)除了使用标准的分词器和分词过滤器(和标准分析器一样),也使用了小写分词过滤器和停用词过滤器。它还使用了雪球词干器对文本进行词干提取。
内置分词器
standard-标准分词器
标准分词器(standardtokenizer)是一个基于语法的分词器,对于大多数欧洲语言来说是不错的。它还处理了Unicode文本的切分,不过分词默认的最大长度是255。它也移除了逗号和句号这样的标点符号。
POST _analyze
{"tokenizer": "standard","text": "The intersecting buckets e.g A&C are labelled using a combination of the two filter names with a default separator of &. "
}
keyword-关键词分词器
关键词分词器(keyword tokenizer)是一种简单的分词器,将整个文本作为单个的分词,提供给分词过滤器。只想应用分词过滤器,而不做任何分词操作时,它可能非常有用。
letter-字母分词器
字母分词器(letter tokenizer)根据非字母的符号,将文本切分成分词。例如,对于句子"share your experience with NoSql & big data technologies”分词是share、your、experience、with、NoSql、big、data、technologies,因为&、空格和句号都不是字母:
POST _analyze
{"tokenizer": "letter","text": "share your experience with NoSql & big data technologies "
}
lowercase-小写分词器
小写分词器(lowercase tokenizer)结合了常规的字母分词器和小写分词过滤器(如你所想,它将整个分词转化为小写)的行为。通过一个单独的分词器来实现的主要原因是,一次进行两项操作会获得更好的性能。
POST _analyze
{"tokenizer": "lowercase","text": "I am man. "
}
分词结果是i、am、man,I转换成i。
whitespace-空白分词器
空白分词器(whitespace tokenizer)通过空白来分隔不同的分词,空白包括空格、制表符、换行等。请注意,这种分词器不会删除任何标点符号。
POST _analyze
{"tokenizer": "whitespace","text": "share your experience with NoSql & big data technologies"
}
分词结果也包含“&”。
pattern-模式分词器
模式分词器(pattern tokenizer)允许指定一个任意的模式,将文本切分为分词。被指定的模式应该匹配间隔符号。
uax_url_emall - 电子邮件分词器
在处理英语单词的时候,标准分词器是非常好的选择。但是,当下存在不少以网站地址和电子邮件地址结束的文本。标准分析器可能在你未注意的地方对其进行了切分。
1.电子邮件分词
POST _analyze
{"tokenizer": "uax_url_email","text": "address is joy@gmail.com"
}
分词结果:address 、 is 、 joy@gmail.com
2.网址分词
POST _analyze
{"tokenizer": "uax_url_email","text": "domain is https://www.baidu.com"
}
分词结果:domain、is、https://www.baidu.com?s=iPhone
path_hierarchy - 路径层次分词器
路径层次分词器(path hierarchy tokenizer)允许以特定的方式索引文件系统的路径,这样在搜索时,共享同样路径的文件将被作为结果返回。例如,假设有一个文件名想要索引,看上去是这样的/usr/local/var/log/elasticsearch.logo路径层次分词器将其切分为:
POST _analyze
{"tokenizer": "path_hierarchy","text": "/usr/local/var/log/elasticsearch.logo"
}
结果:
{"tokens" : [{"token" : "/usr","start_offset" : 0,"end_offset" : 4,"type" : "word","position" : 0},{"token" : "/usr/local","start_offset" : 0,"end_offset" : 10,"type" : "word","position" : 0},{"token" : "/usr/local/var","start_offset" : 0,"end_offset" : 14,"type" : "word","position" : 0},{"token" : "/usr/local/var/log","start_offset" : 0,"end_offset" : 18,"type" : "word","position" : 0},{"token" : "/usr/local/var/log/elasticsearch.logo","start_offset" : 0,"end_offset" : 37,"type" : "word","position" : 0}]
}
分词过滤器
standard-标准分词过滤器
不要认为标准分词过滤器(standard token filter)进行了什么复杂的计算,实际上它什么事情也没做!在更老版本的Lucene中,它用于去除单词结尾的"'s”字符,还有不必要的句点字符,但是现在这些都被其他的分词过滤器和分词器处理掉了。
注意:标准分词过滤器在 7.x之后被移除。
POST _analyze
{"text": "I am man.","filter": [{"type": "standard"}]
}
lowercase - 小写分词过滤器
小写分词过滤器(lowercase token filter)只是做了这件事:将任何经过的分词转换为小写。
POST _analyze
{"text": "I am man.","filter": [{"type": "lowercase"}]
}
输出:
{"tokens" : [{"token" : "i am man.","start_offset" : 0,"end_offset" : 9,"type" : "word","position" : 0}]
}
创建索引时指定分词过滤器:
PUT /mall_user
{"settings": {"index": {"analysis":{"analyzer":{"customLowercaseAnalyzer":{"type":"custom","tokenizer":"standard","filter":["lowercase","stop"]}}}}},"mappings": {"properties": {"name": {"type": "text","analyzer": "customLowercaseAnalyzer","fields": {"raw": {"type": "keyword"}}},"age": {"type": "integer"},"birth": {"type": "date","format": "yyyy-MM-dd"}}}
}
查询name的词条:
# 写入数据
POST /mall_user/_doc/1
{"name":"i am Jay","age":"20","birth":"2002-01-01"
}# 分析词条
POST /mall_user/_doc/1/_termvectors
{"fields": ["name"]
}
由于使用了lowercase分词过滤器,Jay会被转换成jay,通过Jay或者jay都可以进行搜索。
length - 长度分词过滤器
长度分词过滤器(length token filter)将长度超出最短和最长限制范围的单词过滤掉。举个例子,如果将min设置为2,并将max设置为5,任何小于2个字符和任何大于5个字符的分词将会被移除。
POST _analyze
{"text": "I am man.","tokenizer": "standard","filter": [{"type": "length","min": 2,"max": 5}]
}
max:参数表示最大分词长度,默认为Integer.MAX_VALUE,就是2147483647。
min:则表示最小长度,默认为0
输出结果:
{"tokens" : [{"token" : "am","start_offset" : 2,"end_offset" : 4,"type" : "<ALPHANUM>","position" : 1},{"token" : "man","start_offset" : 5,"end_offset" : 8,"type" : "<ALPHANUM>","position" : 2}]
}
I和符号.长度不在2-5之间都被过滤。
创建索引时指定分词过滤器:
PUT /mall_user
{"settings": {"index": {"analysis": {"filter":{"customLengthFilter":{"type":"length","min":2,"max":"5"}},"analyzer": {"customLengthAnaylyzer": {"type": "custom","tokenizer": "standard","filter": ["customLengthFilter"]}}}}},"mappings": {"properties": {"name": {"type": "text","analyzer": "customLengthAnaylyzer","fields": {"raw": {"type": "keyword"}}},"age": {"type": "integer"},"birth": {"type": "date","format": "yyyy-MM-dd"}}}
}
写入数据:
POST /mall_user/_doc/1
{"name":"i am Jay","age":"20","birth":"2002-01-01"
}
查询name的词条:
POST /mall_user/_doc/1/_termvectors
{"fields": ["name"]
}
输出结果:
{"_index" : "mall_user","_type" : "_doc","_id" : "1","_version" : 1,"found" : true,"took" : 0,"term_vectors" : {"name" : {"field_statistics" : {"sum_doc_freq" : 2,"doc_count" : 1,"sum_ttf" : 2},"terms" : {"Jay" : {"term_freq" : 1,"tokens" : [{"position" : 2,"start_offset" : 5,"end_offset" : 8}]},"am" : {"term_freq" : 1,"tokens" : [{"position" : 1,"start_offset" : 2,"end_offset" : 4}]}}}}
}
从结果看,i 长度不在2-5之间,已被过滤。
查询:
# 查询 name 包含 i 的信息,输出为空
POST /mall_user/_search
{"query": {"match": {"name": "i"}}
}# 查询 name 包含 Jay 的信息,找到对应的记录
POST /mall_user/_search
{"query": {"match": {"name": "Jay"}}
}
stop - 停用词分词过滤器
停用词分词过滤器(stop token filter)将停用词从分词流中移除。对于英文而言,这意味着停用词列表中的所有分词都将每会被完全移除。可以添加指定一个待移除单词的列表。
POST _analyze
{"text": "I am man.","tokenizer": "standard","filter": [{"type": "stop","stopwords":["am"]}]
}
stopwords
:自定义的待移除单词的列表。
输出:
{"tokens" : [{"token" : "I","start_offset" : 0,"end_offset" : 1,"type" : "<ALPHANUM>","position" : 0},{"token" : "man","start_offset" : 5,"end_offset" : 8,"type" : "<ALPHANUM>","position" : 2}]
}
从结果来看,已经移除am单词。
创建索引时指定分词过滤器:
PUT /mall_user
{"settings": {"index": {"analysis": {"filter":{"customStopFilter":{"type":"stop","stopwords":["am"]}},"analyzer":{"customStopAnalyzer":{"type":"custom","tokenizer":"standard","filter":["customStopFilter"]}}}}},"mappings": {"properties": {"name": {"type": "text","analyzer": "customStopAnalyzer", "fields": {"raw": {"type": "keyword"}}},"age": {"type": "integer"},"birth": {"type": "date","format": "yyyy-MM-dd"}}}
}
除stopwords
参数还,也可以使用stopwords_path
参数指定停用词文件。
写入数据:
POST /mall_user/_doc/1
{"name":"am Jay","age":"20","birth":"2002-01-01"
}
查询分词信息:
POST /mall_user/_doc/1/_termvectors
{"fields": ["name"]
}
只有Jay分词。
使用am词搜索:
POST /mall_user/_search
{"query": {"match": {"name": "am"}}
}
搜索结果为空。
reverse - 反转分词过滤器
反转分词过滤器(reverse token filter)允许处理一个分词流,并反转每个分词。
使用反转分词过滤器测试文本:
POST _analyze
{"text": "I am man.","tokenizer": "standard","filter": ["reverse"]
}
创建索引时指定分析器:
PUT /mall_user
{"settings": {"index": {"analysis":{"analyzer":{"customReverseAnalyzer":{"type":"custom","tokenizer":"standard","filter":["reverse"]}}}}},"mappings": {"properties": {"name": {"type": "text","analyzer": "customReverseAnalyzer","fields": {"raw": {"type": "keyword"}}},"age": {"type": "integer"},"birth": {"type": "date","format": "yyyy-MM-dd"}}}
}
写入数据并且查看name词条和搜索:
#写入数据
POST /mall_user/_doc/1
{"name":"i am Jay","age":"20","birth":"2002-01-01"
}# 查询name的词条
POST /mall_user/_doc/1/_termvectors
{"fields": ["name"]
}# 搜索
POST /mall_user/_search
{"query": {"match": {"name": "Jay"}}
}
unique - 唯一分词过滤器
唯一分词过滤器(unique token filter)只保留唯一的分词,它保留第一个匹配分词的元数据,而将其后出现的重复删除:
POST _analyze
{"text": "foo bar foo bar","tokenizer": "standard","filter": ["unique"]
}
创建索引时指定唯一分词过滤器:
PUT /mall_user
{"settings": {"index": {"analysis":{"analyzer":{"customUniqueAnalyzer":{"type":"custom","tokenizer":"standard","filter":["unique"]}}}}},"mappings": {"properties": {"name": {"type": "text","analyzer": "customUniqueAnalyzer","fields": {"raw": {"type": "keyword"}}},"age": {"type": "integer"},"birth": {"type": "date","format": "yyyy-MM-dd"}}}
}
查看name词条:
# 写入数据
POST /mall_user/_doc/1
{"name":"foo bar foo bar","age":"20","birth":"2002-01-01"
}# 查看name词条
POST /mall_user/_doc/1/_termvectors
{"fields": ["name"]
}
synonym - 同义词分词过滤器
同义词分词过滤器(synonym token filter)在分词流中的同样位移处,使用关键词的同义词取代原分词。
例如文本“i have a automobile”,automobile 的同义词为car。
POST _analyze
{"text": "i have a automobile","tokenizer": "standard","filter": [{"type":"synonym","synonyms":["automobile=>car"]} ]
}
输出结果没有automobile,已替换成car。
创建索引时使用同义词:
PUT /mall_user
{"settings": {"index": {"analysis":{"filter":{"customSynonymFilter":{"type":"synonym","synonyms":["automobile=>car"]}},"analyzer":{"customSynonymAnalyzer":{"type":"custom","tokenizer":"standard","filter":["customSynonymFilter"]}}}}},"mappings": {"properties": {"name": {"type": "text","analyzer": "customSynonymAnalyzer","fields": {"raw": {"type": "keyword"}}},"age": {"type": "integer"},"birth": {"type": "date","format": "yyyy-MM-dd"}}}
}
写入数据和测试name的词条:
POST /mall_user/_doc/1
{"name":"i have a automobile","age":"20","birth":"2002-01-01"
}# 查看词条
POST /mall_user/_doc/1/_termvectors
{"fields": ["name"]
}# 使用car 或者 automobile搜索 都可以搜索到结果
POST /mall_user/_search
{"query": {"match": {"name": "automobile"}}
}
POST /mall_user/_search
{"query": {"match": {"name": "car"}}
}
ngram - N元语法分词过滤器
什么是N元语法?
N元语法(ngram)是Elasticsearch中更为独特的分词方式。N元语法是将一个单词切分为多个子单词。
N元语法具体表现为1元语法、2元语法等。
1元语法(1-ngram)分词“automobile”,结果时a、u、t、o、m、o、b、i、l、e。
2元语法(2-ngram)分词“automobile”,结果时au、ut、to、om、mo、ob、bi、il、le。
由此可以看出N元语法是按字符的顺序往后截取N个字符。
使用N元语法分析文本:
# 1元语法
POST _analyze
{"text": "automobile","tokenizer": "standard","filter": [{"type":"ngram","min_gram":1,"max_gram":1} ]
}#2元语法
POST _analyze
{"text": "automobile","tokenizer": "standard","filter": [{"type":"ngram","min_gram":2,"max_gram":2} ]
}
min_gram
:最小分隔的大小;
max_gram
:最大分隔的大小;
max_gram
和 min_gram
的差值需要在index.max_ngram_diff
之间。
如果min_gram=1、max_gram=2,automobile分割成:a、u、t、o、m、o、b、i、l、e、au、ut、to、om、mo、ob、bi、il、le。
创建索引时使用N元语法:
PUT /mall_user
{"settings": {"index": {"analysis":{"filter":{"customNgramFilter":{"type":"ngram","min_gram":1,"max_gram":1}},"analyzer":{"customNgramAnalyzer":{"type":"custom","tokenizer":"standard","filter":["customNgramFilter"]}}}}},"mappings": {"properties": {"name": {"type": "text","analyzer": "customNgramAnalyzer","fields": {"raw": {"type": "keyword"}}},"age": {"type": "integer"},"birth": {"type": "date","format": "yyyy-MM-dd"}}}
}
写入数据并且查看name词条、通过name查询:
# 写入数据
POST /mall_user/_doc/1
{"name":"automobile","age":"20","birth":"2002-01-01"
}# 查看词条
POST /mall_user/_doc/1/_termvectors
{"fields": ["name"]
}# 查询name类似a的文档,能查询到
POST /mall_user/_search
{"query": {"match": {"name": "a"}}
}
edge_ngram - 侧边N元语法
侧边N元语法是普通N元语法切分的一种变体,仅仅从左边的边缘开始构建N元语法。
例如“automobile”,从左边“a”的边缘开始截取N个词。
POST _analyze
{"text": "automobile","tokenizer": "standard","filter": [{"type":"edge_ngram","min_gram":2,"max_gram":4} ]
}
输出au、aut、auto,此时已经注意到超过max_ngram部分不会分割(意味着不能搜索)。
创建索引时使用侧边N元语法:
PUT /mall_user
{"settings": {"index": {"analysis":{"filter":{"customEdgeNgramFilter":{"type":"edge_ngram","min_gram":2,"max_gram":4}},"analyzer":{"customEdgeNgramAnalyzer":{"type":"custom","tokenizer":"standard","filter":["customEdgeNgramFilter"]}}}}},"mappings": {"properties": {"name": {"type": "text","analyzer": "customEdgeNgramAnalyzer","fields": {"raw": {"type": "keyword"}}},"age": {"type": "integer"},"birth": {"type": "date","format": "yyyy-MM-dd"}}}
}
写入数据、查看name词条和根据name查询数据:
#写入数据
POST /mall_user/_doc/1
{"name":"automobile","age":"20","birth":"2002-01-01"
}#查询name词条
POST /mall_user/_doc/1/_termvectors
{"fields": ["name"]
}# 根据name查询
# au、aut、auto 可以查询到数据
# mobile 字符是不能搜索到数据的,超过max_gram没有分割
POST /mall_user/_search
{"query": {"match": {"name": "au"}}
}
shingle - 滑动窗口分词过滤器
滑动窗口分词过滤器(shingles),和N元语法以及侧边N元语法沿用了同样的方式。滑动窗口分词过滤器基本上是分词级别的N元语法,而不是字符级别的N元语法。
例如“i has a automobile”分割成:i、i has、has、has a、a、a automobile:
POST _analyze
{"text": "i has a automobile","tokenizer": "standard","filter": [{"type":"shingle","min_shingle_size":2,"max_shingle_size":2} ]
}
创建索引时使用滑动窗口分词过滤器:
PUT /mall_user
{"settings": {"index": {"analysis":{"filter":{"customShingleFilter":{"type":"shingle","min_shingle_size":2,"max_shingle_size":2}},"analyzer":{"customShingleAnalyzer":{"type":"custom","tokenizer":"standard","filter":["customShingleFilter"]}}}}},"mappings": {"properties": {"name": {"type": "text","analyzer": "customShingleAnalyzer","fields": {"raw": {"type": "keyword"}}},"age": {"type": "integer"},"birth": {"type": "date","format": "yyyy-MM-dd"}}}
}
写入数据、查询name的词条、使用name查询:
# 写入数据
POST /mall_user/_doc/1
{"name":"i has a automobile","age":"20","birth":"2002-01-01"
}# 查看name的词条
POST /mall_user/_doc/1/_termvectors
{"fields": ["name"]
}# 根据name搜索
POST /mall_user/_search
{"query": {"match": {"name": "has"}}
}
提取词干-词干分词过滤器
提取词干是将单词缩减到基本或词根的形式。在搜索的时候,这种处理是非常方便的,因为这意味着用户可以匹配单词的复数,以及有同样词根的单词(因此名字称为“提取词干")。
提取词干的算法有snowball过滤器、porter_stem过滤器、kstem过滤器。他们表现基本一致,不过在提取词干有多激进的方面有一些细微的差别。这里的“激进",是指相对于不激进的词干提取器,更为激进的词干提取器会砍掉单词更多的部分。
snowball过滤器:
POST _analyze
{"text": "administrations administrators Administrate","tokenizer": "standard","filter": [{"type":"snowball"} ]
}
porter_stem 过滤器:
POST _analyze
{"text": "administrations administrators Administrate","tokenizer": "standard","filter": [{"type":"porter_stem"} ]
}
kstem过滤器:
POST _analyze
{"text": "administrations administrators Administrate","tokenizer": "standard","filter": [{"type":"kstem"} ]
}
输出结果对比:
算法 | administrations | administrators | Administrate |
---|---|---|---|
snowball | administr | administr | Administr |
porter_stem | administr | administr | Administr |
kstem | administration | administrator | Administrate |
除此之外,还可以使用词典提取词干
有的时候,算法词干提取会以一种奇怪的方式来提取单词的词干,因为它们并不理解基层的语言。正因为此,存在更为精确的方式来提取词干,那就是使用单词字典。在Elasticsearch中,可以使用hunspell
分词过滤器,结合一个字典,来处理词干。
基于此,词干提取的质量就和所用字典的质量是直接相关的。词干提取器只能处理字典里存在的单词。
当创建一个hunspell分析器的时候,字典文件应该是在名为hunspell的目录里,并且hunspell目录和elasticsearch.yml处于同一个目录中。在hunspell目录中,每种语言的字典是一个以其关联地区命名的目录。
使用分析API分析文本
当跟踪信息是如何在Elasticsearch索引中存储的时候,使用分析API来测试分析的过程是十分有用的。API允许你向Elasticsearch发送任何文本,指定所使用的分析器、分词器或者分词过滤器,然后获取分析后的分词。使用标准分析分析了文本"share your experience with NoSql & big data technologies"
1.使用内置的分析器分析
POST _analyze
{"text": ["share your experience with NoSql & big data technologies"],"analyzer": "standard"
}
输出结果:
{"tokens" : [{"token" : "share","start_offset" : 0,"end_offset" : 5,"type" : "<ALPHANUM>","position" : 0},{"token" : "your","start_offset" : 6,"end_offset" : 10,"type" : "<ALPHANUM>","position" : 1},{"token" : "experience","start_offset" : 11,"end_offset" : 21,"type" : "<ALPHANUM>","position" : 2},{"token" : "with","start_offset" : 22,"end_offset" : 26,"type" : "<ALPHANUM>","position" : 3},{"token" : "nosql","start_offset" : 27,"end_offset" : 32,"type" : "<ALPHANUM>","position" : 4},{"token" : "big","start_offset" : 35,"end_offset" : 38,"type" : "<ALPHANUM>","position" : 5},{"token" : "data","start_offset" : 39,"end_offset" : 43,"type" : "<ALPHANUM>","position" : 6},{"token" : "technologies","start_offset" : 44,"end_offset" : 56,"type" : "<ALPHANUM>","position" : 7}]
}
分析API中最为重要的输出是token键。输出是一组这样映射的列表,代表了处理后的分词(实际上,这些分词将会被写人到索引中)。
2.通过组合即时使用分析器分析
POST _analyze
{"text": ["share your experience with NoSql & big data technologies"],"char_filter": {"type": "mapping","mappings": ["&=>and"]},"tokenizer": {"type": "whitespace"},"filter": [# 转小写{"type": "lowercase"},# 翻转字符{"type": "reverse"}]
}
输出:
{"tokens" : [{"token" : "share","start_offset" : 0,"end_offset" : 5,"type" : "word","position" : 0},{"token" : "your","start_offset" : 6,"end_offset" : 10,"type" : "word","position" : 1},{"token" : "experience","start_offset" : 11,"end_offset" : 21,"type" : "word","position" : 2},{"token" : "with","start_offset" : 22,"end_offset" : 26,"type" : "word","position" : 3},{"token" : "nosql","start_offset" : 27,"end_offset" : 32,"type" : "word","position" : 4},{"token" : "and","start_offset" : 33,"end_offset" : 34,"type" : "word","position" : 5},{"token" : "big","start_offset" : 35,"end_offset" : 38,"type" : "word","position" : 6},{"token" : "data","start_offset" : 39,"end_offset" : 43,"type" : "word","position" : 7},{"token" : "technologies","start_offset" : 44,"end_offset" : 56,"type" : "word","position" : 8}]
}
3.使用现有索引字段分析器分析
POST /mall_order_test/_analyze
{"text": "share your experience with NoSql & big data technologies","field":"orderTitle"
}
使用 mall_order_test 索引中orderTitle字段使用的分词器分析text文本。
查询索引文档的分词信息
通过_termvectors
查询已经索引的文档orderTitle字段的分词词条(操作消耗比较大):
POST /mall_order_test/_doc/sZNUsIwBSLqPVZ3tlB7z/_termvectors?pretty=true
{"fields":["orderTitle"]
}
输出:
{"_index" : "mall_order_test","_type" : "_doc","_id" : "sZNUsIwBSLqPVZ3tlB7z","_version" : 8,"found" : true,"took" : 0,"term_vectors" : {# orderTitle 词条信息"orderTitle" : {"field_statistics" : {# 该字段中所有词条的文档频率之和"sum_doc_freq" : 9,# 包含该字段的文档数量"doc_count" : 1,# 字段中所有词条频率之和"sum_ttf" : 9},"terms" : {# 16000 词条的结果数据"16000" : {# 词条在该字段中出现的次数"term_freq" : 1,"tokens" : [{"position" : 7,"start_offset" : 20,"end_offset" : 25}]},"iphone15" : {"term_freq" : 1,"tokens" : [{"position" : 0,"start_offset" : 0,"end_offset" : 8}]},"max" : {"term_freq" : 1,"tokens" : [{"position" : 2,"start_offset" : 13,"end_offset" : 16}]},"pro" : {"term_freq" : 1,"tokens" : [{"position" : 1,"start_offset" : 9,"end_offset" : 12}]},"元" : {"term_freq" : 1,"tokens" : [{"position" : 8,"start_offset" : 25,"end_offset" : 26}]},"单" : {"term_freq" : 1,"tokens" : [{"position" : 4,"start_offset" : 17,"end_offset" : 18}]},"订" : {"term_freq" : 1,"tokens" : [{"position" : 3,"start_offset" : 16,"end_offset" : 17}]},"金" : {"term_freq" : 1,"tokens" : [{"position" : 5,"start_offset" : 18,"end_offset" : 19}]},"额" : {"term_freq" : 1,"tokens" : [{"position" : 6,"start_offset" : 19,"end_offset" : 20}]}}}}
}
快照和恢复
Elasticsearch 的快照和恢复功能允许用户备份集群中的数据并在需要时恢复。快照可以是手动触发的,也可以通过自动化机制进行定期备份。以下是有关如何配置和使用 Elasticsearch 自动快照和恢复机制的步骤:
快照仓库注册
在配置自动快照之前,首先需要定义一个快照仓库。Elasticsearch 支持多种类型的仓库,包括文件系统(FS)、Amazon S3、Google Cloud Storage、Azure Blob Storage 等。仓库需要在集群中预先定义好,如下所示:
PUT /_snapshot/my_backup_repository
{"type": "fs","settings": {"location": "/mount/backups/my_backup","compress": true}
}
在上述例子中,注册了一个文件系统类型的仓库,名为 my_backup_repository
,并指定了备份文件的位置和压缩选项。
创建自动快照策略
通过 Elasticsearch 的 SLM(Snapshot Lifecycle Management)功能,你可以定期自动创建快照。你需要创建一个快照生命周期策略,如下所示:
PUT /_slm/policy/daily_snapshots
{"schedule": "0 30 1 * * ?", "name": "<my-snap-{now/d}>","repository": "my_backup_repository","config": {"indices": ["*"],"ignore_unavailable": true,"include_global_state": false},"retention": {"expire_after": "30d","min_count": 5,"max_count": 50}
}
这个例子中定义了一个名为 daily_snapshots
的策略,它会在每天凌晨 1:30(服务器时间)自动创建一个快照。快照会被保存在前面注册的 my_backup_repository
仓库中。策略还定义了保留策略,以控制保存快照的时间长短。
恢复快照
若要恢复先前创建的快照,可以使用以下命令:
POST /_snapshot/my_backup_repository/my-snap-2023.04.06_01:30/_restore
{"indices": "index_1,index_2","ignore_unavailable": true,"include_global_state": true
}
上面的命令会从 my_backup_repository
仓库中恢复名为 my-snap-2023.04.06_01:30
的快照,包括 index_1
和 index_2
这两个索引。
监控和管理快照
你可以通过 Elasticsearch 的各种 API 来监控和管理快照和快照策略。例如,检查快照状态:
GET /_snapshot/my_backup_repository/my-snap-2023.04.06_01:30
或者获取当前生命周期策略的状态:
GET /_slm/policy
上面介绍了 Elasticsearch 自动快照和恢复机制的基本步骤。请注意,一切配置和操作都要考虑到你的实际环境和需求。Elasticsearch 的版本和配置可能会影响具体步骤和可用功能。