映射和分析
当摆弄索引里面的数据时,我们发现一些奇怪的事情。一些事情看起来被打乱了:在我们的索引中有12条推文,其中只有一条包含日期 2014-09-15 ,但是看一看下面查询命中的 总数 (total):
GET /_search?q=2014 # 12 results
GET /_search?q=2014-09-15 # 12 results !
GET /_search?q=date:2014-09-15 # 1 result
GET /_search?q=date:2014 # 0 results !
为什么在 _all 字段查询日期返回所有推文,而在 date 字段只查询年份却没有返回结果?为什么我们在 _all 字段和 date 字段的查询结果有差别?
推测起来,这是因为数据在 all 字段与 data 字段的索引方式不同。所以,通过请求 gb 索引中 tweet 类型的映射_(或模式定义),让我们看一看 Elasticsearch 是如何解释我们文档结构的:
GET /gb/_mapping/tweet
这将得到如下结果:
{
“gb”: {
“mappings”: {
“tweet”: {
“properties”: {
“date”: {
“type”: “date”,
“format”: “strict_date_optional_time||epoch_millis”
},
“name”: {
“type”: “string”
},
“tweet”: {
“type”: “string”
},
“user_id”: {
“type”: “long”
}
}
}
}
}
}
基于对字段类型的猜测, Elasticsearch 动态为我们产生了一个映射。这个响应告诉我们 date 字段被认为是 date 类型的。由于 _all 是默认字段,所以没有提及它。但是我们知道 _all 字段是 string 类型的。
所以 date 字段和 string 字段 索引方式不同,因此搜索结果也不一样。这完全不令人吃惊。你可能会认为 核心数据类型 strings、numbers、Booleans 和 dates 的索引方式有稍许不同。没错,他们确实稍有不同。
但是,到目前为止,最大的差异在于 代表 精确值 (它包括 string 字段)的字段和代表 全文 的字段。这个区别非常重要——它将搜索引擎和所有其他数据库区别开来。
本地演示
11
12
13
14
15
16
至于为什么这样,因为他们索引的类型不同所以效果不同,具体请看之前的博客,有提到过这一点。即term查询方式虽然表达的是查询精确值,但是它的意思是要根据索引类型,如果索引类型是精确值,查询的结果才对应。之前的疑惑解决。如果索引类型是text,即全文,那么即使查询方式是term它也会被分词和倒排索引,只有我们手动让他not_analyzed,才可以有相同的结果。(当然最主要的终结原因是索引类型)
精确值 VS 全文
Elasticsearch 中的数据可以概括的分为两类:精确值和全文。
精确值 如它们听起来那样精确。例如日期或者用户 ID,但字符串也可以表示精确值,例如用户名或邮箱地址。对于精确值来讲,Foo 和 foo 是不同的,2014 和 2014-09-15 也是不同的。
另一方面,全文 是指文本数据(通常以人类容易识别的语言书写),例如一个推文的内容或一封邮件的内容。
Note
全文通常是指非结构化的数据,但这里有一个误解:自然语言是高度结构化的。问题在于自然语言的规则是复杂的,导致计算机难以正确解析。例如,考虑这条语句:
May is fun but June bores me.
它指的是月份还是人?
精确值很容易查询。结果是二进制的:要么匹配查询,要么不匹配。这种查询很容易用 SQL 表示:
WHERE name = “John Smith”
AND user_id = 2
AND date > “2014-09-15”
查询全文数据要微妙的多。我们问的不只是“这个文档匹配查询吗”,而是“该文档匹配查询的程度有多大?”换句话说,该文档与给定查询的相关性如何?
我们很少对全文类型的域做精确匹配。相反,我们希望在文本类型的域中搜索。不仅如此,我们还希望搜索能够理解我们的 意图 :
• 搜索 UK ,会返回包含 United Kindom 的文档。
• 搜索 jump ,会匹配 jumped , jumps , jumping ,甚至是 leap 。
• 搜索 johnny walker 会匹配 Johnnie Walker , johnnie depp 应该匹配 Johnny Depp 。
• fox news hunting 应该返回福克斯新闻( Foxs News )中关于狩猎的故事,同时, fox hunting news 应该返回关于猎狐的故事。
为了促进这类在全文域中的查询,Elasticsearch 首先 分析 文档,之后根据结果创建 倒排索引 。在接下来的两节,我们会讨论倒排索引和分析过程。
总结
Elasticsearch对于全文这种类型的文档,会对他进行分析,尽量让相关度高的结果在前面,非常人性化。即:
对于精确值类型,查询的时候可以类似SQL语句中那样值是什么查出来的就表达的是什么。
对于全文类型,查询的时候会在文本域中进行分析,然后对分析的结果进行倒排索引,最后查出来的值是经过分析处理后相关性高在前的排序结果。
倒排索引
Elasticsearch 使用一种称为 倒排索引 的结构,它适用于快速的全文搜索。一个倒排索引由文档中所有不重复词的列表构成,对于其中每个词,有一个包含它的文档列表。
例如,假设我们有两个文档,每个文档的 content 域包含如下内容:
- The quick brown fox jumped over the lazy dog
- Quick brown foxes leap over lazy dogs in summer
为了创建倒排索引,我们首先将每个文档的 content 域拆分成单独的 词(我们称它为 词条 或 tokens ),创建一个包含所有不重复词条的排序列表,然后列出每个词条出现在哪个文档。结果如下所示:
Term Doc_1 Doc_2
Quick | | X
The | X |
brown | X | X
dog | X |
dogs | | X
fox | X |
foxes | | X
in | | X
jumped | X |
lazy | X | X
leap | | X
over | X | X
quick | X |
summer | | X
the | X |
现在,如果我们想搜索 quick brown ,我们只需要查找包含每个词条的文档:
Term Doc_1 Doc_2
brown | X | X
quick | X |
Total | 2 | 1
两个文档都匹配,但是第一个文档比第二个匹配度更高。如果我们使用仅计算匹配词条数量的简单 相似性算法 ,那么,我们可以说,对于我们查询的相关性来讲,第一个文档比第二个文档更佳。
但是,我们目前的倒排索引有一些问题:
• Quick 和 quick 以独立的词条出现,然而用户可能认为它们是相同的词。
• fox 和 foxes 非常相似, 就像 dog 和 dogs ;他们有相同的词根。
• jumped 和 leap, 尽管没有相同的词根,但他们的意思很相近。他们是同义词。
使用前面的索引搜索 +Quick +fox 不会得到任何匹配文档。(记住,+ 前缀表明这个词必须存在。)只有同时出现 Quick 和 fox 的文档才满足这个查询条件,但是第一个文档包含 quick fox ,第二个文档包含 Quick foxes 。
我们的用户可以合理的期望两个文档与查询匹配。我们可以做的更好。
如果我们将词条规范为标准模式,那么我们可以找到与用户搜索的词条不完全一致,但具有足够相关性的文档。例如:
• Quick 可以小写化为 quick 。
• foxes 可以 词干提取 –变为词根的格式– 为 fox 。类似的, dogs 可以为提取为 dog 。
• jumped 和 leap 是同义词,可以索引为相同的单词 jump 。
现在索引看上去像这样:
Term Doc_1 Doc_2
brown | X | X
dog | X | X
fox | X | X
in | | X
jump | X | X
lazy | X | X
over | X | X
quick | X | X
summer | | X
the | X | X
这还远远不够。我们搜索 +Quick +fox 仍然 会失败,因为在我们的索引中,已经没有 Quick 了。但是,如果我们对搜索的字符串使用与 content 域相同的标准化规则,会变成查询 +quick +fox ,这样两个文档都会匹配!
Note
这非常重要。你只能搜索在索引中出现的词条,所以索引文本和查询字符串必须标准化为相同的格式。
分词和标准化的过程称为 分析 , 我们会在下个章节讨论。
总结
文档分析后倒排索引的结果去匹配查询的字符串若相关度高,会排序在前返回。
分析过程包括分词和标准化。
倒排索引可以这样想:
以前传统数据库我们是拿查询的字符串去找数据库中的数据,倒排索引是对文档进行分析,最后统计结果取匹配查询的字符串相关度是否高。
分析与分析器
分析与分析器
分析 包含下面的过程:
• 首先,将一块文本分成适合于倒排索引的独立的 词条 ,
• 之后,将这些词条统一化为标准格式以提高它们的“可搜索性”,或者 recall
分析器执行上面的工作。 分析器 实际上是将三个功能封装到了一个包里:
字符过滤器
首先,字符串按顺序通过每个 字符过滤器 。他们的任务是在分词前整理字符串。一个字符过滤器可以用来去掉HTML,或者将 & 转化成 and
。
分词器
其次,字符串被 分词器 分为单个的词条。一个简单的分词器遇到空格和标点的时候,可能会将文本拆分成词条。
Token 过滤器
最后,词条按顺序通过每个 token 过滤器 。这个过程可能会改变词条(例如,小写化 Quick ),删除词条(例如, 像 a,
and,
the 等无用词),或者增加词条(例如,像 jump 和 leap 这种同义词)。
Elasticsearch提供了开箱即用的字符过滤器、分词器和token 过滤器。 这些可以组合起来形成自定义的分析器以用于不同的目的。我们会在 自定义分析器 章节详细讨论。
内置分析器
但是, Elasticsearch还附带了可以直接使用的预包装的分析器。 接下来我们会列出最重要的分析器。为了证明它们的差异,我们看看每个分析器会从下面的字符串得到哪些词条:
“Set the shape to semi-transparent by calling set_trans(5)”
标准分析器 standard
标准分析器是Elasticsearch默认使用的分析器。它是分析各种语言文本最常用的选择。它根据 Unicode 联盟 定义的 单词边界 划分文本。删除绝大部分标点。最后,将词条小写。它会产生
set, the, shape, to, semi, transparent, by, calling, set_trans, 5
简单分析器 simple
简单分析器在任何不是字母的地方分隔文本,将词条小写。它会产生
set, the, shape, to, semi, transparent, by, calling, set, trans
空格分析器 whitespace
空格分析器在空格的地方划分文本。它会产生
Set, the, shape, to, semi-transparent, by, calling, set_trans(5)
语言分析器 例如english
特定语言分析器可用于 很多语言。它们可以考虑指定语言的特点。例如, 英语 分析器附带了一组英语无用词(常用单词,例如 and 或者 the ,它们对相关性没有多少影响),它们会被删除。 由于理解英语语法的规则,这个分词器可以提取英语单词的 词干 。
英语 分词器会产生下面的词条:
set, shape, semi, transpar, call, set_tran, 5
注意看 transparent、
calling 和 set_trans 已经变为词根格式。
什么时候使用分析器
当我们 索引 一个文档,它的全文域被分析成词条以用来创建倒排索引。 但是,当我们在全文域 搜索 的时候,我们需要将查询字符串通过 相同的分析过程 ,以保证我们搜索的词条格式与索引中的词条格式一致。
全文查询,理解每个域是如何定义的,因此它们可以做 正确的事:
• 当你查询一个 全文 域时, 会对查询字符串应用相同的分析器,以产生正确的搜索词条列表。
• 当你查询一个 精确值 域时,不会分析查询字符串, 而是搜索你指定的精确值。
现在你可以理解在 开始章节 的查询为什么返回那样的结果:
• date 域包含一个精确值:单独的词条 2014-09-15
。
• _all 域是一个全文域,所以分词进程将日期转化为三个词条: 2014
, 09
, 和 15
。
当我们在 _all 域查询 2014,它匹配所有的12条推文,因为它们都含有
2014 :
GET /_search?q=2014 # 12 results
当我们在 _all 域查询 2014-09-15,它首先分析查询字符串,产生匹配
2014,
09, 或
15 中 任意 词条的查询。这也会匹配所有12条推文,因为它们都含有 2014 :
GET /_search?q=2014-09-15 # 12 results !
当我们在 date 域查询 2014-09-15
,它寻找 精确 日期,只找到一个推文:
GET /_search?q=date:2014-09-15 # 1 result
当我们在 date 域查询 2014
,它找不到任何文档,因为没有文档含有这个精确日志:
GET /_search?q=date:2014 # 0 results !
测试分析器
有些时候很难理解分词的过程和实际被存储到索引中的词条,特别是你刚接触 Elasticsearch。为了理解发生了什么,你可以使用 analyze API 来看文本是如何被分析的。在消息体里,指定分析器和要分析的文本:
GET /_analyze
{
“analyzer”: “standard”,
“text”: “Text to analyze”
}
结果中每个元素代表一个单独的词条:
{
“tokens”: [
{
“token”: “text”,
“start_offset”: 0,
“end_offset”: 4,
“type”: “”,
“position”: 1
},
{
“token”: “to”,
“start_offset”: 5,
“end_offset”: 7,
“type”: “”,
“position”: 2
},
{
“token”: “analyze”,
“start_offset”: 8,
“end_offset”: 15,
“type”: “”,
“position”: 3
}
]
}
token 是实际存储到索引中的词条。 position 指明词条在原始文本中出现的位置。 start_offset 和 end_offset 指明字符在原始字符串中的位置。
Tip
每个分析器的 type 值都不一样,可以忽略它们。它们在Elasticsearch中的唯一作用在于keep_types token 过滤器。
analyze API 是一个有用的工具,它有助于我们理解Elasticsearch索引内部发生了什么,随着深入,我们会进一步讨论它。
17
指定分析器
当Elasticsearch在你的文档中检测到一个新的字符串域 ,它会自动设置其为一个全文 字符串 域,使用 标准 分析器对它进行分析。
你不希望总是这样。可能你想使用一个不同的分析器,适用于你的数据使用的语言。有时候你想要一个字符串域就是一个字符串域–不使用分析,直接索引你传入的精确值,例如用户ID或者一个内部的状态域或标签。
要做到这一点,我们必须手动指定这些域的映射。
总结
如果我们不手动映射,字符串类型值默认在文档中是全文类型的,使用标准分析器分析,然后倒排索引匹配查询返回相关度高的文档。
这是Elasticsearch的默认分析配置,当然分析器除了默认分析器还有其他不同种类,可以根据效果进行选择。
映射
为了能够将时间域视为时间,数字域视为数字,字符串域视为全文或精确值字符串, Elasticsearch 需要知道每个域中数据的类型。这个信息包含在映射中。
如 数据输入和输出 中解释的, 索引中每个文档都有 类型 。每种类型都有它自己的 映射 ,或者 模式定义 。映射定义了类型中的域,每个域的数据类型,以及Elasticsearch如何处理这些域。映射也用于配置与类型有关的元数据。
我们会在 类型和映射 详细讨论映射。本节,我们只讨论足够让你入门的内容。
核心简单域类型
Elasticsearch 支持 如下简单域类型:
• 字符串: string
• 整数 : byte, short, integer, long
• 浮点数: float, double
• 布尔型: boolean
• 日期: date
当你索引一个包含新域的文档–之前未曾出现– Elasticsearch 会使用 动态映射 ,通过JSON中基本数据类型,尝试猜测域类型,使用如下规则:
JSON type
域 type
布尔型: true 或者 false
boolean
整数: 123
long
浮点数: 123.45
double
字符串,有效日期: 2014-09-15
date
字符串: foo bar
string
Note
这意味着如果你通过引号( “123” )索引一个数字,它会被映射为 string 类型,而不是 long 。但是,如果这个域已经映射为 long ,那么 Elasticsearch 会尝试将这个字符串转化为 long ,如果无法转化,则抛出一个异常。
即,如果你不自己手动写映射,那么你put了一个”123”这样的值,elasticsearch会默认它是text字符串类型的,索引的时候会被分析,如果手动映射为long那么才是你想要的效果。
查看映射
通过 /_mapping ,我们可以查看 Elasticsearch 在一个或多个索引中的一个或多个类型的映射 。在 开始章节 ,我们已经取得索引 gb 中类型 tweet 的映射:
GET /gb/_mapping/tweet
Elasticsearch 根据我们索引的文档,为域(称为 属性 )动态生成的映射。
{
“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 Don’t index this field at all不索引这个域。这个域不会被搜索到。
string 域 index 属性默认是 analyzed 。如果我们想映射这个字段为一个精确值,我们需要设置它为 not_analyzed :
{
“tag”: {
“type”: “string”,
“index”: “not_analyzed”
}
}
Note
其他简单类型(例如 long , double , date 等)也接受 index 参数,但有意义的值只有 no 和 not_analyzed , 因为它们永远不会被分析。
analyzer
对于 analyzed 字符串域,用 analyzer 属性指定在搜索和索引时使用的分析器。默认, Elasticsearch 使用 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”
}
}
}
}
}
通过消息体中指定的 mappings 创建了索引。
稍后,我们决定在 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 。换句话说,我们的映射正常工作。
总结
- 对于不是text类型的字段,Elasticsearch不会默认对他们分析,所以只需要指定正确他们的字段类型type即可。
- 对于是text类型的字段,Elasticsearch默认对他们进行分析,且使用的是标准分析器。如果希望其他的效果,可以对该字段的索引中index 和 analyzer属性进行配置。
index 属性控制怎样索引字符串。
analyzed 首先分析字符串,然后索引它。换句话说,以全文索引这个域。
not_analyzed 索引这个域,所以可以搜索到它,但索引指定的精确值。不对它进行分析。
no Don’t index this field at all不索引这个域。这个域不会被搜索到。
默认处理中text字段的index属性就是analyzed,如果我们希望它匹配查询的精确值而不是分词查询,可以吧index设为not_analyzed
. Analyzer 指定了如果分析此text字段使用的分析器类型
标准分析器standard,简单分析器simple,空格分析器whitespace,语言分析器,例如英语english
但是如果一个映射已经存在,不能更改,只能在一开始建立映射的时候或是第一次添加该字段的值之前指定该字段的索引类型,因为如果更改了数据类型,数字long可能存入不了date类型种。
第一次建立索引:
在决定使用解析器前可以先测试是不是你要的效果:
然后谨慎建立,因为一旦建立就改不了咯~!
18
插入一条数据后想要插入一个新字段,在新字段赋值前增加索引:
19
20-26
查看映射:
可以测试了: