elasticsearch - 分词,优化,别名替换
filter,仅仅只是按照搜索条件过滤出需要的数据而已,不计算任何相关度分数,对相关度没有任何影响,不需要按照相关度分数进行排序,同时还有内置的自动cache最常使用filter的数据
query,会去计算每个document相对于搜索条件的相关度,并按照相关度进行排序,而且无法cache结果
使用validate来对语句进行验证
如果对一个string field进行排序,结果往往不准确,因为分词后是多个单词,再排序就不是我们想要的结果了
通常解决方案是,将一个string field建立两次索引,一个分词,用来进行搜索;一个不分词,用来进行排序
relevance score算法,简单来说,就是计算出,一个索引中的文本,与搜索文本,他们之间的关联匹配程度
Elasticsearch使用的是 term frequency/inverse document frequency算法,简称为TF/IDF算法
Term frequency:搜索文本中的各个词条在field文本中出现了多少次,出现次数越多,就越相关
Inverse document frequency:搜索文本中的各个词条在整个索引的所有文档中出现了多少次,出现的次数越多,就越不相关
Field-length norm:field长度,field越长,相关度越弱
搜索的时候,要依靠倒排索引;排序的时候,需要依靠正排索引,看到每个document的每个field,然后进行排序,所谓的正排索引,其实就是doc values
在建立索引的时候,一方面会建立倒排索引,以供搜索用;一方面会建立正排索引,也就是doc values,以供排序,聚合,过滤等操作使用
doc values是被保存在磁盘上的,此时如果内存足够,os会自动将其缓存在内存中,性能还是会很高;如果内存不足够,os会将其写入磁盘上
query phase:
搜索请求发送到某一个coordinate node,会构建一个priority queue,长度以paging操作from和size为准,如果不加from和size,就默认搜索前10条,按照_score排序,coordinate node将请求转发到所有shard,每个shard本地搜索,并构建一个本地的priority queue,各个shard将自己的priority queue返回给coordinate node,并构建一个全局的priority queue。如果from + size 的值太大,就会需要过多的资源。
fetch phase:
query phase结束后会返回给coordinate node一些doc id等信息,之后coordinate node再查询需要的数据,将其再发往对应的shard上,将获取的结果在返回给客户端。
preference:
决定了哪些shard会被用来执行搜索操作
_primary, _primary_first, _local, _only_node:xyz, _prefer_node:xyz, _shards:2,3
两个document排序,field值相同;不同的shard上,可能排序不同;每次请求轮询打到不同的replica shard上;每次页面上看到的搜索结果的排序都不一样。这就是bouncing result,也就是跳跃的结果。搜索的时候,是轮询将搜索请求发送到每一个replica shard(primary shard),但是在不同的shard上,可能document的排序不同。解决方案就是将preference设置为一个字符串,比如说user_id,让每个user每次搜索的时候,都使用同一个 shard去执行,就不会看到bouncing results了
timeout:限定在一定时间内,将部分获取到的数据直接返回,避免查询耗时过长
routing:document文档路由,_id路由,routing=user_id,这样的话可以让同一个user对应的数据到一个shard上去
search_type:
default:query_then_fetch
dfs_query_then_fetch,可以提升revelance sort精准度
scroll搜索:
如果一次性要查出来比如10万条数据,那么性能会很差,此时一般会采取用scoll滚动查询,一批一批的查,直到所有数据都查询完处理完,使用scoll滚动搜索,可以先搜索一批数据,然后下次再搜索一批数据,以此类推,直到搜索出全部的数据来
scoll搜索会在第一次搜索的时候,保存一个当时的视图快照,之后只会基于该旧的视图快照提供数据搜索,如果这个期间数据变更,是不会让用户看到的,采用基于_doc进行排序的方式,性能较高,每次发送scroll请求,我们还需要指定一个scoll参数,指定一个时间窗口,每次搜索请求只要在这个时间窗口内能完成就可以了
获得的结果会有一个scroll_id,下一次再发送scoll请求的时候,必须带上这个scoll_id
scroll,看起来挺像分页的,但是其实使用场景不一样。分页主要是用来一页一页搜索,给用户看的;scoll主要是用来一批一批检索数据,让系统进行处理的
手动创建索引:
PUT /my_index
{
"settings": { ... any settings ... },
"mappings": {
"type_one": { ... any mappings ... },
"type_two": { ... any mappings ... },
...
}
}
修改索引
PUT /my_index/_settings
{
"number_of_replicas": 1
}
删除索引
DELETE /my_index
DELETE /index_one,index_two
DELETE /index_*
DELETE /_all
elasticsearch.yml
action.destructive_requires_name: true
分词器:
默认的分词器 : standard
standard tokenizer:以单词边界进行切分
standard token filter:什么都不做
lowercase token filter:将所有字母转换为小写
stop token filer(默认被禁用):移除停用词,比如a the it等等
修改分词器的设置:
启用english停用词token filter
定制化自己的分词器
手动创建的索引下建立type,使用自己定义的分词器:
type,是一个index中用来区分类似的数据的,类似的数据,但是可能有不同的fields,而且有不同的属性来控制索引建立、分词器
field的value,在底层的lucene中建立索引的时候,全部是opaque bytes类型,不区分类型的
lucene是没有type的概念的,在document中,实际上将type作为一个document的field来存储,即_type,es通过_type来进行type的过滤和筛选
一个index中的多个type,实际上是放在一起存储的,因此一个index下,不能有多个type重名,而类型或者其他设置不同的,因为那样是无法处理的
root object:
就是某个type对应的mapping json,包括了properties,metadata(_id,_source,_type),settings(analyzer),其他settings(比如include_in_all)
properties: type,index,analyzer
_source:
(1)查询的时候,直接可以拿到完整的document,不需要先拿document id,再发送一次请求拿document
(2)partial update基于_source实现
(3)reindex时,直接基于_source实现,不需要从数据库(或者其他外部存储)查询数据再修改
(4)可以基于_source定制返回field
(5)debug query更容易,因为可以直接看到_source
如果不需要上述好处,可以禁用_source
_all
将所有field打包在一起,作为一个_all field,建立索引。没指定任何field进行搜索时,就是使用_all field在搜索。
也可以在field级别设置include_in_all field,设置是否要将field的值包含在_all field中
标识性metadata:_index,_type,_id
定制dynamic策略
true:遇到陌生字段,就进行dynamic mapping
false:遇到陌生字段,就忽略
strict:遇到陌生字段,就报错
定制dynamic mapping策略
默认会按照一定格式识别date,比如yyyy-MM-dd。但是如果某个field先过来一个2017-01-01的值,就会被自动dynamic mapping成date,后面如果再来一个"hello world"之类的值,就会报错。可以手动关闭某个type的date_detection,如果有需要,自己手动指定某个field为date类型。
定制自己的dynamic mapping template(type level)
title没有匹配到任何的dynamic模板,默认就是standard分词器,不会过滤停用词,is会进入倒排索引,用is来搜索是可以搜索到的
title_en匹配到了dynamic模板,就是english分词器,会过滤停用词,is这种停用词就会被过滤掉,用is来搜索就搜索不到了
定制自己的default mapping template(index level):
重建索引
一个field的设置是不能被修改的,如果要修改一个Field,那么应该重新按照新的mapping,建立一个index,然后将数据批量查询出来,重新用bulk api写入index中,批量查询的时候,建议采用scroll api,并且采用多线程并发的方式来reindex数据,每次scoll就查询指定日期的一段数据,交给一个线程即可
有些数据是2017-10-10这种日期格式的,所以title这种field被自动映射为了date类型,实际上它应该是string类型的
之后插入数据格式不正确会报错
如果此时想修改title的类型,是不可能的,唯一的办法,就是进行reindex,也就是说,重新建立一个索引,将旧索引的数据查询出来,再导入新索引,如果说旧索引的名字,是old_index,新索引的名字是new_index,终端java应用,已经在使用old_index在操作了,这个时候给java应用一个别名,这个别名是指向旧索引的,java应用先使用。先用goods_index alias来操作,此时实际指向的是旧的my_index
新建一个index,调整其title的类型为string
再使用scroll api将数据批量查询出来
采用bulk api将scoll查出来的一批数据,批量写入新索引
重复这两个操作。。。
将tmp_index alias切换到my_index_new上去,java应用会直接通过index别名使用新的索引中的数据,java应用程序不需要停机,零提交,高可用
再通过别名来先查询也是可以的。
倒排索引,是适合用于进行搜索的
(1)包含这个关键词的document list
(2)包含这个关键词的所有document的数量:IDF(inverse document frequency)
(3)这个关键词在每个document中出现的次数:TF(term frequency)
(4)这个关键词在这个document中的次序
(5)每个document的长度:length norm
(6)包含这个关键词的所有document的平均长度
倒排索引不可变的好处
(1)不需要锁,提升并发能力,避免锁的问题
(2)数据不变,一直保存在os cache中,只要cache内存足够
(3)filter cache一直驻留在内存,因为数据不变
(4)可以压缩,节省cpu和io开销
倒排索引不可变的坏处:每次都要重新构建整个索引
document的写入原理:
es底层是Lucene,会将一个index分成多个segment,每个segment都会存放数据。
数据首先会被写入buffer,之后commit point阶段,是将buffer中的数据写入到index segment中,每次commit point时,如果出现删除操作,会有一个.del文件,标记了哪些segment中的哪些document被标记为deleted了,搜索的时候,会依次查询所有的segment,从旧的到新的,比如被修改过的document,在旧的segment中,会标记为deleted,在新的segment中会有其新的数据,等待在os cache中的index segment被fsync强制刷到磁盘上,供搜索使用,同时buffer也会被清空。
如果是更新操作,会将现有的document标记为deleted状态,然后把新来的document插入index segment中,
NRT:
数据写入buffer,每隔一定时间,buffer中的数据被写入segment文件,但是先写入os cache,只要segment写入os cache,那就直接打开供search使用,不立即执行commit,数据写入os cache,并被打开供搜索的过程,叫做refresh,默认是每隔1秒refresh一次。也就是说,每隔一秒就会将buffer中的数据写入一个新的index segment file,先写入os cache中。所以,es是近实时的,数据写入到可以被搜索,默认是1秒。
POST /my_index/_refresh,可以手动refresh,一般不需要手动执行
如果时效性要求,比较低,只要求一条数据写入es,一分钟以后才让我们搜索到就可以了,那么就可以调整refresh interval
PUT /my_index
{
"settings": {
"refresh_interval": "30s"
}
}
数据写入buffer缓冲同时也会写入translog日志文件,每隔一秒钟,buffer中的数据被写入新的segment file,并进入os cache,此时segment被打开并供search使用,buffer被清空,新的segment不断添加,buffer不断被清空,而translog中的数据不断累加,当translog长度达到一定程度的时候,commit操作发生: buffer中的所有数据写入一个新的segment,并写入os cache,打开供使用,buffer被清空,一个commit ponit被写入磁盘,标明了所有的index segment,filesystem cache中的所有index segment file缓存数据,被fsync强行刷到磁盘上,现有的translog被清空,创建一个新的translog
fsync+清空translog,就是flush,默认每隔30分钟flush一次,或者当translog过大的时候,也会flush
POST /my_index/_flush,一般不手动flush
translog:数据每隔5秒被fsync一次到磁盘上。在一次增删改操作之后,当fsync在primary shard和replica shard都成功之后,那次增删改操作才会成功
但是这种在一次增删改时强行fsync translog可能会导致部分操作比较耗时,也可以允许部分数据丢失,设置异步fsync translog
PUT /my_index/_settings
{
"index.translog.durability": "async",
"index.translog.sync_interval": "5s"
}
每秒一个segment file,文件过多,而且每次search都要搜索所有的segment,很耗时,默认会在后台执行segment merge操作,在merge的时候,被标记为deleted的document也会被彻底物理删除
每次merge操作的执行流程
(1)选择一些有相似大小的segment,merge成一个大的segment
(2)将新的segment flush到磁盘上去
(3)写一个新的commit point,包括了新的segment,并且排除旧的那些segment
(4)将新的segment打开供搜索
(5)将旧的segment删除
POST /my_index/_optimize?max_num_segments=1,尽量不要手动执行