Neil Zhu,简书ID Not_GOD,University AI 创始人 & Chief Scientist,致力于推进世界人工智能化进程。制定并实施 UAI 中长期增长战略和目标,带领团队快速成长为人工智能领域最专业的力量。
作为行业领导者,他和UAI一起在2014年创建了TASA(中国最早的人工智能社团), DL Center(深度学习知识中心全球价值网络),AI growth(行业智库培训)等,为中国的人工智能人才建设输送了大量的血液和养分。此外,他还参与或者举办过各类国际性的人工智能峰会和活动,产生了巨大的影响力,书写了60万字的人工智能精品技术内容,生产翻译了全球第一本深度学习入门书《神经网络与深度学习》,生产的内容被大量的专业垂直公众号和媒体转载与连载。曾经受邀为国内顶尖大学制定人工智能学习规划和教授人工智能前沿课程,均受学生和老师好评。
类型表示类似的文档的类。类型包含一个名字——例如 user
或者 blogpost
—— 和一个 mapping
。映射,就像数据库的模式一样,描述了对应类型的文档可能拥有的字段或者属性,每个字段的数据类型——如 string
integer
或者 date
—— 还有这些字段会被 Lucene 如何索引和存储。
在 what is a document? 中,我们说类型就像关系型数据库中的表。尽管这样的类比在开始熟悉 elasticsearch 时便于理解,但是我们这里还是更加细化对类型究竟是什么以及在 Lucene 之上如何实现的认知。
Lucene 如何看待文档
Lucene 中的文档包含 字段-值 对的简单列表。字段必须有至少一个值,但是任何的字段都可以包含多个值。类似地,单个字符串值可能会被分析过程转换成多个值。Lucene 并不管这些值是字符串或者数字或者日期——所有值都会被看做是 opaque bytes。
当我们在 Lucene 中索引文档时,每个字段的值被加入到关联字段的倒排索引中。可选择的是,原始值可能也会不作变化就存储起来,这样以后就可以对这些信息进行检索了。
类型如何实现
Elasticsearch 类型在这个简单的基础上实现的。索引可能有多个类型,每个有自己的映射,不同类型的文档可以存放在同一个索引中。
因为 Lucene 并没有文档类型的概念,每个文档的类型名会使用一个称为 _type
的元数据字段进行存放。当我们搜索特定类型的文档时,Elasticsearch 可以轻易地在 _type
字段上使用过滤器限制在该类型文档上进行检索。
Lucene 同样也没有映射的概念。映射是 Elasticsearch 用来映射复杂的 JSON 文档到简单的 Lucene 期待接受的扁平文档的层。
例如,在 user
类型中的 name
的映射可能声明这个字段是 string
字段,并且他的值在索引到倒排索引 name
中之前必须使用 whitespace
分析器进行分析:
"name": {
"type": "string",
"analyzer": "whitespace"
}
避免类型的出错
不同类型的文档可以被加入同样的索引中也带来了一些意料之外的复杂性。
假设我们在索引中有两个类型:blog_en
对英文博客,blog_es
对西班牙文博客。两个类型都有一个 title
字段,但是一个采用的是 english
分析器,另一个则是 spanish
分析器。
下面的查询就会产生一个问题:
GET /_search
{
"query": {
"match": {
"title": "The quick brown fox"
}
}
}
我们在两个类型中搜索 title
字段。查询字符串需要被分析,但是使用哪一个分析器呢,english
还是 spanish
?它会使用首先发现的 title
所使用的分析器,所以这对于有些文档就是正确的,而对其他一些文档就是错误的。
我们可以避免这个问题,通过对字段采取不同的命名——例如,title_en
和 title_es
——或者显式地包含类型的名字在字段名字中,分开查询每个字段:
GET /_search
{
"query": {
"multi_match": {
"query": "The quick brown fox",
"fields": [ "blog_en.title", "blog_es.title" ]
}
}
}
-
multi_match
查询执行一个match
查询在多个字段上,并对结果进行合并
我们新的查询就会使用 english
分析器在字段 blog_en.title
上,而使用 spanish
分析器在字段 blog_es.title
上,合并的结果会得到一个总体的相关分数。
这个解决方案可以在两个字段都有相同的数据类型的时候起作用,但是如何你索引下面两个文档到同一个索引中时:
- 类型:user
{ "login": "john_smith" }
- 类型:event
{ "login": "2014-06-01" }
Lucene 不管一个字段包含字符串另一个字段包含一个日期。它很乐意从两个字段索引字节值。
然而,如果我们试着去按照 event.login
字段排序,Elasticsearch 需要将 login 字段的值载入到内存中。正如我们在 Fielddata 所说,他会载入在这个索引下的所有文档而不顾其类型。
所以,他会尝试或者按照字符串或者日期来载入这些值,取决于它首先看到的 login
字段。这个会得到无法预期的结果或者出错。
为了确保你不会出现这样的冲突,建议确保在一个索引中的每个类型的拥有同样名字的这些字段按照同样的映射方式进行配置。
什么是文档?
在大多数应用中的大多数实体或者对象可以被序列化为一个 JSON 对象,包含 key 和 value。key 就是字段或者属性的名称,value 可以是一个字符串、数字、布尔值、另一个对象、值的数组或者其他特定类型的(如表示日期的字符串或者表示位置的对象):
{
"name": "John Smith",
"age": 42,
"confirmed": true,
"join_date": "2014-06-01",
"home": {
"lat": 51.5,
"lon": 0.1
},
"accounts": [
{
"type": "facebook",
"id": "johnsmith"
},
{
"type": "twitter",
"id": "johnsmith"
}
]
}
我们互换地使用对象或者文档,这两者是一个东西。然而,还是有个差异。对象就是一个 JSON 对象——类似于我们熟知的 hash、hashmap、dictionary 或者 associative array。对象可能包含其他的对象。在 Elasticsearch 中,术语 文档 有一个特定的含义——文档代表最顶层、根对象,序列化成 JSON 并在 Elasticsearch 中以唯一个 ID 进行存储。
字段数据
关于 Elasticsearch 内部的视角。尽管我们没有展示任何新技术,字段数据是一个我们会反复提到的重要的话题,这也是你需要注意的事项。
当你对一个字段进行排序时,Elasticsearch 需要获得每个已经匹配的文档相应字段的值。倒排索引,在搜索的时候的表现很好,但是在进行排序并非理想的选择:
- 搜索的时候,我们需要将一个 term 映射到文档的列表
- 排序的时候,我们需要将一个 文档映射到其 term上,换言之,我们需要“翻转”倒排索引。
为了让排序更加高效,Elasticsearch 将那个想要进行排序的字段所有的值都载入到了内存中。这个就称为字段数据。
Elasticsearch 不会仅仅载入匹配了特定查询的文档的值。它会载入在索引中每个文档的值,不管对应的
type
Elasticsearch 载入所有值到内存中的原因就是从磁盘翻转索引非常缓慢。即使你可能需要当前请求的部分文档的值,你可能会需要对下一个请求获取另外的文档的值,所以将这些值一次性载入到内存中也很合理了。
字段数据用在了 Elasticsearch 中的好几个地方:
- 对一个字段进行排序
- 对字段进行聚合
- 特定的过滤器(例如,地理位置过滤器)
- 关联的 script
显然,这样会消耗很多内存,特别是较大字符串的字段,其值包含众多唯一的值——就像邮件的正文。幸运的是,内存不够的问题可以通过水平扩展来解决,增加更多的节点。
现在,所有你需要知道的就是字段数据是什么,注意可能会出现的消耗内存的问题。后面,我们会告诉你如何确定字段数据使用的内存的数量,如果去限制其可用的内存,如何去提前载入来提升用户体验过。