《Elasticsearch检索引擎学习指南》第六章 映射和分析
参考资料
精确值VS全文
ES中的数据可以概括的分为两类:精确值和全文
精确值就是其字面意思。全文是指文本数据。(全文通常指非结构化的数据,但这里有一个误解:自然语言是高度结构化的。问题在于自然语言的规则是复杂的,导致计算机难以正确解析。)
精确值很容易查询。结果只有两种,是与否。
查询全文数据要微妙许多。不是要求匹配与否,而是要知道匹配程度有多少,也就是要知道文档与查询的相关性如何。
我们很少会对全文类型的域做精确匹配。相反,我们希望在文本类型的域中搜索。不仅如此,我们还希望搜索能够理解我们的意图:
- 搜索UK,会返回包含United Kingdom的文档
- 搜索jump,会匹配jumped,jumps甚至leap
为了促进这类在全文域中的查询,ES首先分析文档,之后根据结果创建倒排索引
倒排索引
ES使用一种称为倒排索引的结构,它适用于快速的全文搜索。一个倒排索引由文档中所有不重复词的列表构成,对于其中每个词,有一个包含它的文档列表。
例如,假设我们有两个文档,每个文档的content域包含如下内容:
- The quick brown fox jumped over the lazy dog
- Quick brown foxes leap over lazy dogs in summer
为了创建倒排索引,首先将每个文档的content域拆分成单独的词(我们称之为词条(Term)或tokens),创建一个包含所有不重复词条的排序列表,然后列出每个词条出现在哪个文档。如下所示:
现在,如果我们想搜索 quick brown ,我们只需要查找包含每个词条的文档:
两个文档都匹配,但是第一个文档比第二个匹配度更高。如果我们使用仅计算匹配词条数量的简单相似性算法,那么,我们可以说,对于我们查询的相关性来讲,第一个文档比第二个文档更佳。
但是,目前的倒排索引有一些问题:
- Quick和quick独立出现,实际上它们可能是相同的
- fox和foxes的单复数形式,dog和dogs
- jump和leap属于近义词
使用前面的索引搜索 +Quick +fox 不会得到任何匹配文档。(记住,+ 前缀表明这个词必须存在。)只有同时出现 Quick 和 fox 的文档才满足这个查询条件,但是第一个文档包含 quick fox ,第二个文档包含 Quick foxes 。
如果我们将此条规范为标准模式,那么可以找到与用户搜索的词条完全不一致,但具有足够相关性的文档。例如
- Quick 可以小写化为 quick 。
- foxes 可以 词干提取 --变为词根的格式-- 为 fox 。类似的, dogs 可以为提取为 dog 。
- jumped 和 leap 是同义词,可以索引为相同的单词 jump 。
之后的索引如下:
这还远远不够。精确搜索仍然会失败(如 +Quick +fox,因为在我们的索引中,已经没有 Quick 了 )。但是,如果我们对搜索的字符串使用与 content 域相同的标准化规则,会变成查询 +quick +fox ,这样两个文档都会匹配!
NOTE
:这非常重要,只能搜索在索引中存在的词条,索引文本和查询字符串必须标准化为相同的格式。
分词与标准化的过程称为分析,就是下面的内容。
分析与分析器
分析包含下面的过程:
- 首先,将一块文本分成适合于倒排索引的独立的词条
- 之后,将这些词条统一化为标准格式以提高它们的可搜索性,或者recall
分析器执行上面的工作。分析器实际上是将三个功能封装:
- 字符过滤器
首先,字符串按顺序通过每个字符过滤器。他们的任务是在分词前整理字符串。一个字符过滤器可以用来去掉HTML或者将& 转换为and - 分词器
其次,字符串被分词器分为单个的词条。一个简单的分词器遇到空格和标点的时候,可能会将文本拆分成词条 - Token过滤器
最后,词条按顺序通过每个token过滤器。这个过程可能会改变词条(标准化),删除词条(例如 a,and ,the等无用词),或者增加词条(例如,像jump和leap这种同义词)
ES提供了开箱即用的字符过滤器、分词器和token过滤器。这些可以组合起来形成自定义的分析器以用于不同的目的。
内置分析器
但是,ES还附带了可以直接使用的预包装的分析器。接下来会列出最重要的分析器。以一个字符串作为处理对象,比较几个分析器的处理。
Set the shape to semi-transparent by calling set_trans(5)
- 标准分析器
标准分析器是ES默认使用的分析器。它是分析各种文本语言最常用的选择。它根据Unicode联盟定义的单词边界划分文本 。删除绝大部分标点。最后,将词条小写。它会产生
set,the,shape,to,semi,transparent,by,calling,set_trans,5
- 简单分析器
在任何非字母的地方分割文本,将词条小写。产生
set,the,shape,to,semi,transparent,by,calling,set,trans
- 空格分析器
空格分析器在空格的地方划分文本。它会产生
Set, the, shape, to, semi-transparent, by, calling, set_trans(5)
- 语言分析器
特定语言分析器可用于 很多语言。它们可以考虑指定语言的特点。例如, 英语 分析器附带了一组英语无用词(常用单词,例如 and 或者 the ,它们对相关性没有多少影响),它们会被删除。 由于理解英语语法的规则,这个分词器可以提取英语单词的 词干 。
英语 分词器会产生下面的词条:
set, shape, semi, transpar, call, set_tran, 5
注意看 transparent、 calling 和 set_trans 已经变为词根格式。
什么时候使用分析器
当我们索引一个文档,它的全文域被分析成词条以用来创建倒排索引。但是,当我们在全文域搜索的时候,我们需要将查询字符串通过相同的分析过程,以保证我们搜索的词条格式与索引中的词条格式一致。
全文查询,理解每个域是如何定义的,因此它们可以做正确的事:
- 当查询一个全文域时,会对查询字符串应用相同的分析器,以产生正确的索引词条列表。
- 当查询一个精确值域时,不会分析查询字符串,而是搜索指定的精确值。
测试分析器
有些时候很难理解分词的过程和实际被存储到索引中的词条,特别是刚接触ES。为了理解发生了什么,可以使用analyze API来看文本是如何被分析的。在消息体里,指定分析器和要分析的文本:
GET /_analyze
{
"analyzer": "standard",
"text": "Text to analyze"
}
结果中每个元素代表一个单独的词条:
{
"tokens": [
{
"token": "text",
"start_offset": 0,
"end_offset": 4,
"type": "<ALPHANUM>",
"position": 1
},
{
"token": "to",
"start_offset": 5,
"end_offset": 7,
"type": "<ALPHANUM>",
"position": 2
},
{
"token": "analyze",
"start_offset": 8,
"end_offset": 15,
"type": "<ALPHANUM>",
"position": 3
}
]
}
token 是实际存储到索引中的词条。 position 指明词条在原始文本中出现的位置。 start_offset 和 end_offset 指明字符在原始字符串中的位置。
每个分析器的 type 值都不一样,可以忽略它们。它们在Elasticsearch中的唯一作用在于keep_types token 过滤器。
指定分析器
当ES在文档中检测到一个新的字符串域,它会自动设置其为一个全文字符串域,使用标准分析器对它进行分析。
如果对于一个字符串域不使用分析,直接索引传入的精确值,例如用户ID或者一个内部的状态域或标签。要做到这些,必须手动指定这些域的映射。
映射
为了能够将时间域视为时间,数字域视为数字,字符串域视为全文或精确值字符串,ES需要知道每个域中数据的类型。这个信息包含在映射中。
索引中每个文档都有类型。每种类型都有它自己的映射,或者模式定义。映射定义了类型中的域,每个域的数据类型,以及ES如何处理这些域。映射也用于配置与类型有关的元数据。
核心简单域类型
ES支持如下简单域类型:
- 字符串 stirng
- 整数 byte short integer long
- 浮点数 float double
- 布尔型 boolean
- 日期 date
当索引一个包含新域的文档–之前未曾出现–ES会使用动态映射,通过JSON中基本数据类型,尝试猜测域类型。
NOTE:这意味着如果你通过引号( “123” )索引一个数字,它会被映射为 string 类型,而不是 long 。但是,如果这个域已经映射为 long ,那么 Elasticsearch 会尝试将这个字符串转化为 long ,如果无法转化,则抛出一个异常。
查看映射
通过/_mapping ,我们可以查看ES在一个或多个索引中的一个或多个类型的映射。如下取得索引 gb 中类型 tweet 的映射:
GET /gb/_mapping/tweet
ES根据索引的文档,为域(称为 属性 )动态生成的映射
{
"gb": {
"mappings": {
"tweet": {
"properties": {
"date": {
"type": "date",
"format": "strict_date_optional_time||epoch_millis"
},
"name": {
"type": "string"
},
"tweet": {
"type": "string"
},
"user_id": {
"type": "long"
}
}
}
}
}
}
TIP:
错误的映射,例如 将 age 域映射为 string 类型,而不是 integer ,会导致查询出现令人困惑的结果。
检查一下!而不是假设你的映射是正确的。
自定义域映射
尽管在很多情况下基本数据类型已经够用,但经常需要为单独域自定义映射,特别是字符串域。自定义映射允许执行下面的操作:
- 全文字符串域和精确值字符串域的区别
- 使用特定语言分析器
- 优化域以适应部分匹配
- 指定自定义数据格式
- 还有更多
域的重要属性是type。对于不是string的域,一般只需要设置type:
{
"number_of_clicks": {
"type": "integer"
}
}
默认,string类型域会被认为包含全文。就是说,它们的值在索引前,会通过一个分析器,针对于这个域的查询在搜索前也会经过一个分析器。
string域映射的两个最重要属性是index和analyzer。
index
index属性控制怎样索引字符串。它可以是下面三个值:
- analyzed
首先分析字符串,然后索引它。换句话说,以全文索引这个域。 - not_analyzed
索引这个域,所以它能够被搜索,但索引的是精确值。不会对它进行分析。 - no
不索引这个域。这个域不会被搜索到。
string域index属性默认是analyzed。如果想映射这个字段为一个精确值,需要设置它为not_analyzed:
{
"tag": {
"type": "string",
"index": "not_analyzed"
}
}
NOTE:
其他简单类型(例如 long , double , date 等)也接受 index 参数,但有意义的值只有 no 和 not_analyzed , 因为它们永远不会被分析。
analyzer
对于analyzed字符串域,用analyzer属性指定在搜索和索引时使用的分析器。默认,ES使用standard分析器,但可以指定一个内置的分析器替代它,例如whitespace、simple和english:
{
"tweet": {
"type": "string",
"analyzer": "english"
}
}
在自定义分析器,可以看到更多定义和使用自定义分析器的细节。
更新映射
当首次创建一个索引的时候,可以指定类型的映射。也可以使用/_mapping 为新类型(或者为存在的类型更新映射)增加映射。
==NOTE:==尽管可以增加一个存在的映射,但不能修改存在的域映射。如果一个域的映射已经存在,那么该域的数据可能已经被索引。如果意图修改这个域的映射,索引的数据可能会出错,不能被正常的搜索。
我们可以更新一个映射来添加一个新域,但不能将一个存在的域从analyzed改为not_analyzed。
为了描述指定映射的两种方式,先删除gd索引:
DELETE /gb
然后创建一个新索引,指定tweet域使用english分析器:
PUT /gb
{
"mappings": {
"tweet" : {
"properties" : {
"tweet" : {
"type" : "string",
"analyzer": "english"
},
"date" : {
"type" : "date"
},
"name" : {
"type" : "string"
},
"user_id" : {
"type" : "long"
}
}
}
}
}
稍后,我们决定在tweet映射增加一个名为tag的not_analyzed的文本域,使用_mapping:
PUT /gb/_mapping/tweet
{
"properties" : {
"tag" : {
"type" : "string",
"index": "not_analyzed"
}
}
}
注意,我们不需要再次列出所有已存在的域,因为无论如何我们都无法改变它们。新域已经被合并到存在的映射中。
测试映射
可以使用analyze API测试字符串域的映射。比较下面两个请求的输出:
GET /gb/_analyze
{
"field": "tweet",
"text": "Black-cats"
}
GET /gb/_analyze
{
"field": "tag",
"text": "Black-cats"
}
tweet 域产生两个词条 black 和 cat , tag 域产生单独的词条 Black-cats 。换句话说,我们的映射正常工作。
复杂核心域类型
除了之前提到的简单标量数据类型,JSON还有null值,数组和对象,这些ES都是支持的。
多值域
很有可能,希望tag域包含多个标签。可以使用数组的形式索引标签:
{ "tag": [ "search", "nosql" ]}
对于数组,没有特殊的映射需求。任何域都可以包含多个值。就像全文域分析得到多个词条。
这意味着数组中所有的值必须是相同数据类型。不能将日期和字符串混在一起。如果通过索引数组来创建新的域。ES会用数组中第一个值的数据类型作为这个域的类型。
==NOTE:==当从ES得到一个文档,每个数组的顺序和当初索引文档时一样。得到的_source域,包含与索引的一模一样的JSON文档。但是,数组是以多值域 索引的—可以搜索,但是无序的。 在搜索的时候,你不能指定 “第一个” 或者 “最后一个”。 更确切的说,把数组想象成 装在袋子里的值 。
空域
当然,数组可以为空。这相当于存在零值。事实上,在Lucene中是不能存储null值的,所以我们认为存在null值的域为空域。
下面三种域被认为是空的,它们将不会被索引:
"null_value": null,
"empty_array": [],
"array_with_null_value": [ null ]
多层级对象
最后一个JSON原生数据类是对象–在其他语言中称为哈希,哈希map,字典或者关联数组。
内部对象经常用于嵌入一个实体或对象到其它对象中。例如,与其在tweet文档中包含user_name和user_id域,我们也可以这样写:
{
"tweet": "Elasticsearch is very flexible",
"user": {
"id": "@johnsmith",
"gender": "male",
"age": 26,
"name": {
"full": "John Smith",
"first": "John",
"last": "Smith"
}
}
}
内部对象的映射
ES会动态监测新的对象域并映射它们为对象,在properties属性下列出内部域:
{
"gb": {
"tweet": {
"properties": {
"tweet": { "type": "string" },
"user": {
"type": "object",
"properties": {
"id": { "type": "string" },
"gender": { "type": "string" },
"age": { "type": "long" },
"name": {
"type": "object",
"properties": {
"full": { "type": "string" },
"first": { "type": "string" },
"last": { "type": "string" }
}
}
}
}
}
}
}
}
user和name域的映射结构与tweet类型的相同。事实上,type映射只是一种特殊的对象映射,称之为根对象。除了它有一些文档元数据的特殊顶级域,例如_source和_all域,它和其他对象一样。
内部对象是如何索引的
Lucene不理解内部对象。Lucene文档是由一组键值对列表组成的。为了能让ES有效地索引内部类,它把我们的文档转化为这样:
{
"tweet": [elasticsearch, flexible, very],
"user.id": [@johnsmith],
"user.gender": [male],
"user.age": [26],
"user.name.full": [john, smith],
"user.name.first": [john],
"user.name.last": [smith]
}
内部对象数组
最后,考虑包含内部对象的数组是如何被索引的。假如有一个followers数组:
{
"followers": [
{ "age": 35, "name": "Mary White"},
{ "age": 26, "name": "Alex Jones"},
{ "age": 19, "name": "Lisa Smith"}
]
}
这个文档会像我们之前描述的那样被扁平化处理,结果如下所示:
{
"followers.age": [19, 26, 35],
"followers.name": [alex, jones, lisa, smith, mary, white]
}
{age: 35} 和 {name: Mary White} 之间的相关性已经丢失了,因为每个多值域只是一包无序的值,而不是有序数组。这足以让我们问,“有一个26岁的追随者?”
但是我们不能得到一个准确的答案:“是否有一个26岁 名字叫 Alex Jones 的追随者?”
相关内部对象被称为 nested 对象,可以回答上面的查询,我们稍后会在嵌套对象中介绍它。