Elastic Stack应用宝典
上QQ阅读APP看书,第一时间看更新

2.2 文档字段

文档字段(Field)可以理解为文档的一个结构化特征。由于它在实现上是JSON的属性,所以有些文献中也将文档字段称为文档属性。由于Elasticsearch接口的请求参数和返回结果都是JSON格式的,所以为了避免因名称引起混淆,本书对JSON属性的叫法进行一些约定。本书后续章节将统一称文档中的JSON属性为字段,而称请求中的JSON属性为参数。由于接口返回结果中的JSON属性代表的是文档的字段或字段运算的结果,所以出现在返回结果中的JSON属性也统一称为字段。对于其他情况,均称JSON中的属性为属性。在后续章节学习到Logstash和Beats组件时也会面临这个问题,本书会在相应章节再给出新的约定。

由于文档的具体内容都以字段为单元保存,所以字段决定了文档将以什么样的方式存储和索引。索引文档和存储文档是两个不同的概念,索引文档是将文档编入倒排索引,而存储文档则是将文档在物理上保存起来。

2.2.1 字段索引

由于文档中的数据分散在各个字段中,所以索引文档肯定都是针对文档字段进行的。一份文档一般会有多个字段,所以倒排索引一般是多个相互关联的倒排索引。前面所说的索引文档应该是以字段为单位对文档做索引,而并非以整个文档内容做索引。为了叙述上的方便,本书后续章节将无区别的使用索引文档和索引文档字段。

在默认情况下,文档的所有字段都会创建倒排索引。这可以通过字段的index参数来设置,其默认值为true,即字段会被编入索引。在编入索引时,一般不会将字段值整体编入。对于text类型的字段来说,它们会被解析为词项后再以词项为单位编入索引。编入索引的信息包括文档ID、词项在字段中出现的频率、词项在字段中的次序、词项在字段中的起止偏移量等信息。文档ID是词项来源文档的编号,是文档的惟一标识,可用于存在性检索。文档ID由文档中的元字段_id保存,本书后续将无区别地使用这两种称谓,它们都代表文档的惟一标识。词项在字段中出现的频率一般称为词频(Term Frequency),它可以反映检索结果的相关性,词项在文本中出现的频率越高与检索的相关性也就越高。词项在字段中的次序(以下简称词序)记录的是某一词项在所有词项中的次序,主要用于短语查询(Phrase Query)。词项在字段中的起止偏移量(以下简称词项偏移量),给出了词项在字段中的实际位置,一般用于高亮检索结果。

例如,在“Elasticsearch is a search engine”中,search这个词项在整个文本中只出现了1次,所以它的词频为1;而search之前还有3个词项,所以词序为3(词序起始位置为0);最后,由于search在整个短语中的位置是第19~24之间,所以偏移量为[19,25)。为了体验这些内容,可借助_analyze接口看一下文本分析的结果,如示例2-4所示:

示例2-4 使用_analyze接口请求分析文本

在示例2-4中,analyzer参数指明了使用的分析器为standard,即标准分析器;text则指明了需要做分析的文本数据。在Kibana控制台执行示例2-4中的请求,返回结果中会看到文本分析的结果如下:

示例2-5 使用_analyze接口分析文本结果

默认情况下,除text类型的字段会保存文档ID、词频、词序以外,其余类型字段均只保存文档ID。用户可以在映射字段时通过index_option参数来设置,它的可选值为docs、freqs、positions、offsets,编入索引的信息依次增加,具体含义如下:

● docs:只有文档ID会被编入索引;

● freqs:文档ID、词频会被编入索引;

● positions:文档ID、词频和词序会被编入索引;

● offsets:文档ID、词频、词序和偏移量都会被编入索引。

由此也可以看出,尽管在默认情况下所有的字段都会被索引,但是这些字段的原始值是不会被编入索引中的。这意味着用户可以通过某一字段的词项检索到文档,但并不能直接取到这个字段的原始值。因为字段的索引最多只包含上述四项内容,并不包含字段原始值。这似乎有些令人费解,如果是这样检索数据还有什么意义呢?

2.2.2 字段存储

为什么字段原始值不会被编入索引呢?这显然还是出于对性能与效率的考量。还是以“Elasticsearch is a search engine”这段文本为例,文档分析后会提取出5个词项并编入索引。如果这5个词项都在索引中保留字段的原始值,那么这段文本就要被保留5次。而对于很多文档来说,它们的文本内容要比这大得多,如果都保留下来这对于存储空间的浪费将是十分惊人的。

那么是不是字段的原始值就完全丢失了呢?别担心,尽管单个字段的原始值不会被保存,但索引提供了一个叫_source的字段用于存储整个文档的原始值。_source字段有一个特性,那就是这个字段在默认情况下是不会被索引的,但是每个查询默认都会带着_source字段返回。如果确定不需要使用_source字段保存源文档,也可以在创建索引通过映射类型的_source参数将其关闭,如示例2-6所示:

示例2-6 关闭_source字段

在示例2-6中,“PUT /users”是创建索引接口的请求,请求体中的mappings参数用于设置映射关系,_source则是控制_source字段的开关。不推荐关闭_source字段通常,因为_source字段与以下一些功能相关联:

● 使用update、update_by_query更新文档,使用reindex重新索引文档;

● 运行时高亮检索结果;

● 在不同的Elasticsearch实例间重新索引文档;

● 使用源文档对检索和聚集做debug。

关闭_source字段后,上述功能也将无法使用,所以在考虑关闭_source字段时要权衡清楚。通常关闭_source字段的主要原因是出于节省存储空间,Elastic官方建议如果单纯只是考虑节省存储空间可以通过修改index.codec提高压缩效率,具体请参见第3章中有关索引配置的介绍。

_source字段保存的源文档信息是在索引文档时以JSON形式传递过来的最原始文档,这在查看文档时比较直观方便,但如果需要使用文档中某一字段值做进一步运算时就比较麻烦了。例如在检索出文档后再根据某一字段值进行排序,类似的情况还有聚集查询中的取字段极值、平均值等统计数据,这在第6章中有比较详细的介绍。针对这种情况,Elasticsearch还提供了另外一种机制保存字段值,这就是文档值(Doc Value)机制。文档值机制存储的信息与_source字段基本相同,但它的存储结构是面向列的,类似于传统关系型数据库中的表结构。换句话说,_source字段将源文档揉在一起保存,而文档值则将它们按字段分别保存在不同的列中;_source保存的是最原始的文档信息,而文档值则是经过一定分析处理的数据。所以文档值相当于把文档中的结构化数据以结构化的方式存储起来了,而对于非结构化的文本数据则不能使用文档值机制。所以在默认情况下,所有非text类型的字段都支持文档值机制,并且都是开启的。而对于text类型的字段,由于它本身就不是结构化数据,所以到目前为止还不支持文档值机制。字段的文档值机制可以通过字段的doc_values参数开关,例如在示例2-7中,就将users索引的age属性的文档值关闭了:

示例2-7 关闭字段文档值与开启字段fielddata机制

对于text类型的字段来说,Elasticsearch提供了另外一种称为fielddata的机制来处理相似场景的问题。fielddata机制与文档值机制虽然在效果上类似,但在实现上则完全不同。文档值机制的数据结构保存在硬盘中,而fielddata机制则是在内存中构建数据结构,所以使用fielddata机制有可能导致JVM内存溢出。不仅如此,fielddata机制保存的也不是字段原始值,而是通过遍历倒排索引建立文档与它所包含词项的对应关系。具体来说,Elasticsearch会在首次对字段进行聚集、排序等请求时,遍历所有倒排索引并在内存中构建起文档与词项之间的对应关系。在默认情况下,text字段的fielddata机制是关闭的,可以通过在映射字段时修改fielddata参数开启。例如在示例2-6中,address字段的fielddata就被开启了。在开启fielddata机制前要考虑清楚,因为这种机制显然非常消耗资源,而且使用text类型字段做聚集、排序也往往不是合理的需求。即便是真的有这样的需求,也可以通过字段多数据类型来开启文档值机制,而尽量不要使用fielddata机制。

除了使用_source字段、文档值和fielddata机制以外,在字段映射时还可以通过store参数将字段修改为true,以使索引单独保存这个字段。通常情况下,如果文档本身十分庞大,而一些字段又会经常单独使用,那么这样的字段就可以设置为单独存储。例如对于书名和书的内容来说,书的内容要比书名长得多,而书名在许多情况下又需要单独使用,如果每次需要取书名时都需要将书的内容也返回就有点太浪费了。在这种情况下就可以使用store参数,将书名设置为单独存储,然后就可以使用stored_fields单独检索这些字段了。在第4章第4.3节的查询接口中,提供了只返回存储字段的方法。

2.2.3 字段参数

除了前面介绍的type、doc_value、fielddata、store等参数,文档字段还有许多可以使用的配置参数。表2-2列出了在字段定义时所有可用的参数。

表2-2 字段参数

(续)

其中,copy_to参数可以将字段值复制到另外一个字段,这样就可以将相关字段值复制到同一个字段中,而在需要做跨字段检索时就可以使用这个字段了。

这些参数并不是在所有字段上都可以使用,在表格的第二列中给出了它们适用的字段类型。本小节不打算对这些参数做进一步讲解,会在具体应用时给出更详细的说明。

2.2.4 元字段

文档字段可以分为两类:一类是元字段(Meta-field);另一类是用户定义的业务字段。元字段不需要用户定义,在任一文档中都存在,例如在前面提及的文件ID字段_id就是一个元字段。在名称上它们有一个显著的特征,就是它们都以下划线“_”开头。在学习这些字段时,要从字段索引和字段存储两方面理解它们。有些元字段只是为了存储,它们会出现在文档检索的结果中,但却不能通过这个字段本身做检索,比如_source字段。而有些元字段则只是为了索引,它会创建一个索引出来,用户可以在这个索引上检索文档,但这个字段却不会出现最终的检索结果中,如_all字段。此外,也并不是所有元字段都是默认开启的,有些元字段是需要在索引中配置开启才可使用的。表2-3列出了文档所有元字段。

表2-3 元字段(Meta-field)

(续)

上述一些元字段在前面已经介绍过,但有一些现在理解起来会比较困难。这里先按类别尝试讲解一下,更具体解释和应用会在后续章节中介绍。

第一类,标识相关元字段:

这类元字段主要用于标识当前文档,包括_id、_uid、_index、_type。_id和_uid都是文档的标识符,在版本6之前_id仅在映射类型内惟一,而_uid由_type和_id组成并在索引内惟一;但在6.0.0版之后,映射类型在索引内仅有一个,所以_uid已经被废止,而_id则在索引内惟一。_id字段就是第2.2.1节中介绍编入索引信息中的文档ID,它本身也会被索引,所以可以通过_id字段检索到文档。更准确的说,_id应该是在同一索引的同一分片内惟一。

第二类,源文档相关字段:

_source字段就是第2.2.2节介绍字段存储时提到的存储源文档的元字段,源文档所有信息都会保存在这个字段中,但这个字段不会被索引。_size字段保存了源文档长度,但需要安装mapper_size插件,并在设置索引映射类型时通过将_size参数设置为true开启这个功能。

第三类,索引相关字段:

这类元字段是关于如何创建索引的字段,它们一般只创建索引而不会存储,也不会出现在检索结果中。例如,_all字段是将文档所有字段的词项以空格分隔连接起来创建大索引,这样用户在检索时就不用指明根据哪一个字段检索,这在不知道检索内容位于哪一个字段时非常有用。但在6.0.0版中_all字段已经被废止,而建议使用字段参数copy_to。_field_names字段用于给所有有值的字段名称做索引,这可以用于检索某一字段是否存在。_ignored字段则用于给所有被忽略的字段创建索引,被忽略的字段一般是在字段格式错误而ignore_malformed参数又设置为true时发生。

第四类,路由:

在默认情况下,文档会根据_id字段的值将文档路由到不同的分片,通过在添加文档时设置routing参数可以修改文档路由。而_routing字段就是当前文档路由的值,可以在文档检索时通过_routing字段检索文档。有关路由与分片的相关内容,请参考本章2.4.2节。

2.2.5 字段限制

在索引中定义太多字段会导致映射爆炸,进而导致内存不足甚至系统崩溃。这个问题在使用动态映射时比较常见,动态映射就是预先不定义索引映射关系,而在添加文档时动态确定字段名称和类型。如果每次文档添加到索引时都包含新字段,这些字段将最终体现在索引的映射中。为了防范这种情况的发生,Elasticsearch引入了一些参数可以限制字段的数量。比如,index.mapping.total_fields.limit参数定义了索引中最大字段数,它限制了字段、对象以及字段别名最大值,默认值是1000;index.mapping.depth.limit参数则定义了嵌套对象的最大深度,默认值是20;index.mapping.nested_fields.limit参数定义了索引中嵌套字段的最大数量,默认值为50。这些限制出于安全角度出发,但在某些特定应用中可能会限制了需求,可以通过在创建索引修改这些参数满足需求。