ES五| Elasticsearch的映射(字段类型)和分析
免责声明:本专栏的内容都是在读了《Elasticsearch权威指南( 中文版)》书籍后整理的读后感及部分内容笔记,具体内容请查阅原著内容。
引言
映射(mapping)机制用于进行字段类型确认, 将每个字段匹配为一种确定的数据类型( string , number , booleans , date 等)。
分析(analysis)机制用于进行全文文本(Full Text)的分词, 以建立供搜索用的反向索引。
确切值(Exact values) 和 全文文本(Full text)
Elasticsearch中的数据可以大致分为两种类型:确切值 及 全文文本。
确切值是确定的, 正如它的名字一样。 比如一个date或用户ID, 也可以包含更多的字符串比如username或email地址。确切值 "Foo" 和 "foo" 就并不相同。 确切值 2014 和 2014-09-15 也不相同。
全文文本, 从另一个角度来说是文本化的数据(常常以人类的语言书写), 比如一篇推文(Twitter的文章)或邮件正文。全文文本常常被称为 非结构化数据 , 其实是一种用词不当的称谓, 实际上自然语言是高度结构化的。
确切值是很容易查询的, 因为结果是二进制的 -- 要么匹配, 要么不匹配。 下面的查询很容易。以SQL表达:
#SQL语句中查找确切值
WHERE name = "John Smith"
AND user_id = 2
AND date > "2014-09-15"
而对于全文数据的查询来说, 却有些微妙。 我们不会去询问 这篇文档是否匹配查询要求? 。 但是,我们会询问 这篇文档和查询的匹配程度如何? 。 换句话说, 对于查询条件, 这篇文档的相关性有多
高?我们很少确切的匹配整个全文文本。 我们想在全文中查询包含查询文本的部分。 不仅如此,我们还期望搜索引擎能理解我们的意图:
- 一个针对 "UK" 的查询将返回涉及 "United Kingdom" 的文档;
- 一个针对 "jump" 的查询同时能够匹配 "jumped" , "jumps" , "jumping" 甚至 "leap";
为了方便在全文文本字段中进行这些类型的查询, Elasticsearch首先对文本分析(analyzes),然后使用结果建立一个倒排索引。
倒排索引
Elasticsearch使用一种叫做倒排索引(inverted index)的结构来做快速的全文搜索。 倒排索引由在文档中出现的唯一的单词列表, 以及对于每个单词在文档中的位置组成。
例如, 我们有两个文档, 每个文档 content 字段包含:
- The quick brown fox jumped over the lazy dog
- Quick brown foxes leap over lazy dogs in summer
为了创建倒排索引, 我们首先切分每个文档的 content 字段为单独的单词( 我们把它们叫做词(terms)或者表征(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 |
两个文档都匹配, 但是第一个比第二个有更多的匹配项。 如果我们加入简单的相似度算法(similarity algorithm), 计算匹配单词的数目, 这样我们就可以说第一个文档比第二个匹配度更高——对于我们的查询具有更多相关性。
但是在我们的倒排索引中还有些问题:
- "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" , 这样就可以匹配到两个文档。
这个标记化和标准化的过程叫做分词(analysis)。
分析和分析器
分析(analysis)是这样一个过程:首先, 标记化一个文本块为适用于倒排索引单独的词(term)然后标准化这些词为标准形式, 提高它们的“可搜索性”或“查全率”这个工作是分析器(analyzer)完成的。 一个分析器(analyzer)只是一个包装用于将三个功能放到一个包里:
字符过滤器
首先字符串经过字符过滤器(character filter), 它们的工作是在标记化前处理字符串。 字符过滤器能够去除HTML标记, 或者转换 "&" 为 "and" 。分词器下一步, 分词器(tokenizer)被标记化成独立的词。 一个简单的分词器(tokenizer)可以根据空格或逗号将单词分开( 译者注: 这个在中文中不适用) 。标记过滤最后, 每个词都通过所有标记过滤(token filters), 它可以修改词( 例如将 "Quick" 转为小写) , 去掉词( 例如停用词像 "a" 、 "and" 、 "the" 等等) , 或者增加词( 例如同义词像 "jump" 和 "leap" )Elasticsearch提供很多开箱即用的字符过滤器, 分词器和标记过滤器。 这些可以组合来创建自定义的分析器以应对不同的需求。
内建的分析器
不过, Elasticsearch还附带了一些预装的分析器, 你可以直接使用它们。 下面我们列出了最重要的几个分析器, 来演示这个字符串分词后的表现差异:
"Set the shape to semi-transparent by calling set_trans(5)"
标准分析器
标准分析器是Elasticsearch默认使用的分析器。 对于文本分析, 它对于任何语言都是最佳选择。 它根据Unicode Consortium的定义的单词边界(word boundaries)来切分文本, 然后去掉大部分标点符号。 最后, 把所有词转为小写。 产生的结果为:
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)
语言分析器
特定语言分析器适用于很多语言。 它们能够考虑到特定语言的特性。 例如, english 分析器自带一套英语停用词库——像 and 或 the 这些与语义无关的通用词。 这些词被移除后, 因为语法规则的存在, 英语单词的主体含义依旧能被理解( 译者注: stem English words 这句不知道该如何翻译, 查了字典, 我理解的大概意思应该是将英语语句比作一株植物, 去掉无用的枝叶, 主干依旧存在, 停用词好比枝叶, 存在与否并不影响对这句话的理解。 ) 。english 分析器将会产生以下结果:
set, shape, semi, transpar, call, set_tran, 5
注意 "transparent" 、 "calling" 和 "set_trans" 是如何转为词干的。
当分析器被使用
当我们索引(index)一个文档, 全文字段会被分析为单独的词来创建倒排索引。 不过, 当我们在全文字段搜索(search)时, 我们要让查询字符串经过同样的分析流程处理, 以确保这些词在索引中存在。
- 当你查询全文(full text)字段, 查询将使用相同的分析器来分析查询字符串, 以产生正确的词列表。
- 当你查询一个确切值(exact value)字段, 查询将不分析查询字符串, 但是你可以自己指定。
当Elasticsearch在你的文档中探测到一个新的字符串字段, 它将自动设置它为全文 string 字段并用 standard 分析器分析,通过映射(mapping)人工设置这些字段。
映射
Elasticsearch需要知道每个字段里面都包含了什么类型。 这些类型和字段的信息存储( 包含) 在映射( mapping) 中。
核心简单字段类型
Elasticsearch支持以下简单字段类型:
类型 | 表示的数据类型 |
String | string |
Whole number | byte , short , integer , long |
Floating point | float , double |
Boolean | boolean |
Date | date |
当你索引一个包含新字段的文档(之前没有的字段),Elasticsearch将使用动态映射猜测字段类型, 这类型来自于JSON的基本数据类型, 使用以下规则:
JSON type | Field type |
Boolean: true or false | "boolean" |
Whole number: 123 | "long" |
Floating point: 123.45 | "double" |
String, valid date: "2014-09-15" | "date" |
String: "foo bar" | "string" |
注意:这意味着, 如果你索引一个带引号的数字—— "123" , 它将被映射为 "string" 类型, 而不是 "long" 类型。 然而, 如果字段已经被映射为 "long" 类型, Elasticsearch将尝试转换字符串为long, 并在转换失败时会抛出异常。
查看映射
我们可以使用 _mapping 后缀来查看Elasticsearch中的映射。 在本章开始我们已经找到索引 gb 类型 tweet 中的映射:
GET /gb/_mapping/tweet
这展示给了我们字段的映射( 叫做属性(properties)) , 这些映射是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"
}
}
}
}
}
}
错误的映射, 例如把 age 字段映射为 string 类型而不是 integer 类型, 会造成查询结果混乱。
复合核心字段类型
除了之前提到的简单的标量类型, JSON还有 null 值, 数组和对象, 所有这些Elasticsearch都支持:
多值字段
我们想让 tag 字段包含多个字段, 这非常有可能发生。 我们可以索引一个标签数组来代替单一字符串:
{ "tag": [ "search", "nosql" ]}
对于数组不需要特殊的映射。 任何一个字段可以包含零个、 一个或多个值, 同样对于全文字段将被分析并产生多个词。言外之意, 这意味着数组中所有值必须为同一类型。 你不能把日期和字符窜混合。 如果你创建一个新字段, 这个字段索引了一个数组, Elasticsearch将使用第一个值的类型来确定这个新字段的类型。
当你从Elasticsearch中取回一个文档, 任何一个数组的顺序和你索引它们的顺序一致。你取回的 _source 字段的顺序同样与索引它们的顺序相同。然而, 数组是做为多值字段被索引的, 它们没有顺序。 在搜索阶段你不能指定“第一个值”或者“最后一个值”。 倒不如把数组当作一个值集合(bag of values)。
空字段
当然数组可以是空的。 这等价于有零个值。 事实上, Lucene没法存放 null 值, 所以一个 null 值的字段被认为是空字段。这四个字段将被识别为空字段而不被索引:
"empty_string": "",
"null_value": null,
"empty_array": [],
"array_with_null_value": [ null ]
多层对象
我们需要讨论的最后一个自然JSON数据类型是对象(object)——在其它语言中叫做hash、hashmap、 dictionary 或者 associative array.
内部对象(inner objects)经常用于在另一个对象中嵌入一个实体或对象。 例如, 做为在 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"
}
}
}
Elasticsearch 会动态的检测新对象的字段, 并且映射它们为 object 类型, 将每个字段加到properties 字段下:
{
"gb": {
"tweet": { <1>
"properties": {
"tweet": { "type": "string" },
"user": { <2>
"type": "object",
"properties": {
"id": { "type": "string" },
"gender": { "type": "string" },
"age": { "type": "long" },
"name": { <3>
"type": "object",
"properties": {
"full": { "type": "string" },
"first": { "type": "string" },
"last": { "type": "string" }
}
}
}
}
}
}
}
}
对 user 和 name 字段的映射与 tweet 类型自己很相似。 事实上, type 映射只是 object 映射的一种特殊类型, 我们将 object 称为根对象。 它与其他对象一模一样, 除非它有一些特殊的顶层字段, 比如 _source , _all 等等。
内部对象是怎样被索引的
Lucene 并不了解内部对象。 一个 Lucene 文件包含一个键-值对应的扁平表单。 为了让Elasticsearch 可以有效的索引内部对象, 将文件转换为以下格式:
{
"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]
}
内部栏位可被归类至name, 例如 "first" 。 为了区别两个拥有相同名字的栏位, 我们可以使用完整路径, 例如 "user.name.first" 或甚至 类型 名称加上路径: "tweet.user.name.first" 。
内部对象数组
一个包含内部对象的数组如何索引。 我们有个数组如下所示:
{
"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} 之间的关联会消失, 因每个多值的栏位会变成一个值集合,而非有序的阵列。
总结
映射(mapping)机制用于进行字段类型确认, 将每个字段匹配为一种确定的数据类型( string , number , booleans , date 等)。分析(analysis)机制用于进行全文文本(Full Text)的分词, 以建立供搜索用的反向索引。通过这两者可以知道数据是如何在Elasticsearch中被索引的。