elasticsearch中字符串类型text vs keyword的选择

参考:https://www.phpmianshi.com/?id=201

关于ES字符串类型的选择

ElasticSearch 5.0以后,string类型有重大变更,移除了string类型,string字段被拆分成两种新的数据类型: text用于全文搜索的,而keyword用于关键词搜索。

ElasticSearch字符串将默认被同时映射成text和keyword类型,将会自动创建下面的动态映射(dynamic mappings):

{
    "foo": {
        "type": "text",
        "fields": {
            "keyword": {
                "type": "keyword",
                "ignore_above": 256
            }
        }
    }
}

这就是造成部分字段还会自动生成一个与之对应的“.keyword”字段的原因。

Text vs. keyword

 

text:通常用于基于文本的相关性搜索。全文本字段可以分词,即在索引执行之前通过一个分词器将字符串转换为单词列表。分词操作使得es可以在全文本字段上搜索单词。支持模糊、精确查询、不支持聚合,全文本字段不用于排序,很少用于聚合等操作。

keyword:关键字是个精准值,通常用于过滤、排序、参与聚合等。关键字不参与分词通常搜索keyword是按照整体搜索,所以创建keyword字段的索引时是不进行分词的,比如:邮政编码、手机号码、身份证等。keyword字段通常 用于过滤、排序、聚合等。支持模糊、精确查询 支持聚合

相关优化

1. 5.0以后对于数值型字段采用了block k-d tree索引结构,这个是为了优化范围查询,但是如果要精确匹配的话就蛋疼了。导致数字类型的字段,比如status一类不同值不多,每个值对应的文档比较多的情况下,查询会比较缓慢。 改用keyword字段来索引就快了。如果你同时要做 range 的话,可以用子字段来实现

 

2. 在ES5.x里,一定要注意数值类型是否需要做范围查询,看似数值,但其实只用于Term或者Terms这类精确匹配的,应该定义为keyword类型。典型的例子就是索引web日志时常见的HTTP Status code。

 

3. 如果RangeQuery的结果集很大,并且还需要和其他结果集更小的查询条件做AND的,应该升级到ES5.4+,该版本在底层引入的indexOrDocValuesQuery,可以极大提升该场景下RangeQuery的查询速度。

 

ES2.x和5.x的数值类型分别是如何索引的

ES5.x之前用到的Lucene版本,实际上只能够索引文本类型的数据,表面上被定义为数值类型的字段,在暗地里都被转换成了字符串,编排成了倒排索引。例如:

 

2

[doc3, doc5, doc10 ...]

5

[doc1, doc3, doc9 ... ]

...

...

90

[doc2, doc3, doc8 ...]

99

[doc3, doc5, doc20 ...]

这种结构对于精确的数值查询速度还是比较快的,直接从倒排索引根据查找的term拿到postings list就好了。 但类似 range: [50, 100] 这样的范围查找就比较麻烦了,Lucene在找到对应的term后,只能将其转换成类似这样 50 OR 51 OR 52 ... OR 100 的Bool查询。可想而知,这个多条件OR查询开销很高,执行很慢。所以Lucene在创建索引的时候,会自动产生一些类似50x75这样的特殊Term,指向包含在该范围的文档列表,从而可以将查询优化成类似50x75 OR 76x99 OR 100 这种形式。但是这种优化在字段的不同值很多,查询范围很大的时候,依然很无力。 因此早期版本的Lucene和ES的范围查询性能一直被诟病。

Lucene从6.0开始引入了Block k-d tree来重新设计数值类型的索引结构,其目标是让数值型数据索引的结构更紧凑,搜索速度更快。这种数据结构是为多维数值字段设计的,可以高效的用于诸如地理位置这类数据的快速过滤,但同样适用于单维度的数值型。

Block k-d tree的基本概念和Lucene实现

即k-dimensional tree,常用来作空间划分及近邻搜索,是二叉空间划分树的一个特例。通常,对于维度为k,数据点数为N的数据集,k-d tree适用于N≫2k的情形。

基本思想就是将一个N维的数值空间,不断选定包含值最多的维度做2分切割,反复迭代,直到切分出来的空间单元 cell 包含的值数量小于某个数值。 对于单维度的数据,实际上就是简单的对所有值做一个排序,然后反复从中间做切分,生成一个类似于B-tree这样的结构。和传统的B-tree不同的是,他的叶子结点存储的不是单值,而是一组值的集合,也就是是所谓的一个Block。每个Block内部包含的值数量控制在512- 1024个,保证值的数量在block之间尽量均匀分布。 其数据结构大致看起来是这样的:

Lucene将这颗B-tree的非叶子结点部分放在内存里,而叶子结点紧紧相邻存放在磁盘上。当作range查询的时候,内存里的B-tree可以帮助快速定位到满足查询条件的叶子结点块在磁盘上的位置,之后对叶子结点块的读取几乎都是顺序的。

要注意一点,不是简单的将拿到的所有块合并就可以得到想要的docID结果集,因为查询的上下边界不一定刚好落在两端block的上下边界上。 所以如果需要拿到range filter的结果集,就要对于两端的block内的docid做扫描,将他们的值和range的上下边界做比较,挑选出match的docid集合。

 

Queries/filters执行的先后顺序及结果合并是怎样做的

ES的Queries/filters执行顺序比较复杂,并非按照Query里条件的排列顺序来挨个执行;也不是某些人想象的那样,每个filter/Query都独立执行,拿到各自的结果集以后,再做结果集的合并。 在elasticsearch-query-execution-order 这篇博客里对这个主题做了比较详细的介绍。

简单来说,ES会先通过调用每个查询的cost()函数估算一下该查询的代价,然后选择代价最小的查询作为起点,在其圈定的docid集合上生成一个迭代器。然后反复迭代,根据和其他条件之间是AND还是OR的关系,再去决定结果集合并的方式。

 

这个结果集的迭代,以及合并,就是上面链接里提到的nextdoc()和advance()等操作。 比较复杂的地方是这些操作根据数据类型的不同和查询类型的不同,ES都有针对性的进行操作优化,同样的操作有些可能是在内存中进行,有些则可能直接在磁盘上进行。

以最常见的keyword字段做TermQuery为例,其cost就是Term Frequency,这个值可以直接从倒排索引读取。 Frequency越高的Term,其postings list就越长,迭代起来的代价就越高。 所以如果对多个TermQuery做AND合并,就会选择Frequency最低的Term,以其postings list为起点做迭代(nextdoc)。 Postings list是按照docid顺序存放的,并且在数据结构上还增加了跳表来加快advance()操作。因此多个postings list的合并可以直接操作磁盘上的数据而不会引起过多的随机IO,加上ES5.0以后对于索引数据采取了mmap file的方式访问,热数据读取引发的磁盘IO愈发的少。 这也是为什么5.1.1之后取消了TermQuery的cache,因为在跳表和OS page cache的加持下,直接合并磁盘上的postings list已经非常快了。 取消对其cache后,可以减少构造cache的开销,并且将宝贵的cache空间留给代价更高的filter,一定程度上可以提升ES整体性能。

 

 

相关文章:https://elasticsearch.cn/article/446

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值