小烨收藏ElasticSearch权威指南-映射和分析
Posted SureData数烨数据
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了小烨收藏ElasticSearch权威指南-映射和分析相关的知识,希望对你有一定的参考价值。
映射和分析
当摆弄索引里面的数据时,我们发现一些奇怪的事情。一些事情看起来被打乱了:在我们的索引中有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 !
View in Sense
为什么在 _all
字段查询日期返回所有推文,而在 date
字段只查询年份却没有返回结果?为什么我们在 _all
字段和 date
字段的查询结果有差别?
推测起来,这是因为数据在 _all
字段与 data
字段的索引方式不同。所以,通过请求 gb
索引中 tweet
类型的_映射_(或模式定义),让我们看一看 Elasticsearch 是如何解释我们文档结构的:
GET /gb/_mapping/tweet
View in Sense
这将得到如下结果:
{ "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
字段)的字段和代表 全文 的字段。这个区别非常重要——它将搜索引擎和所有其他数据库区别开来。
精确值 VS 全文
Elasticsearch 中的数据可以概括的分为两类:精确值和全文。
另一方面,全文 是指文本数据(通常以人类容易识别的语言书写),例如一个推文的内容或一封邮件的内容。
全文通常是指非结构化的数据,但这里有一个误解:自然语言是高度结构化的。问题在于自然语言的规则是复杂的,导致计算机难以正确解析。例如,考虑这条语句:
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 使用一种称为 倒排索引 的结构,它适用于快速的全文搜索。一个倒排索引由文档中所有不重复词的列表构成,对于其中每个词,有一个包含它的文档列表。
例如,假设我们有两个文档,每个文档的 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
,这样两个文档都会匹配!
这非常重要。你只能搜索在索引中出现的词条,所以索引文本和查询字符串必须标准化为相同的格式。
分词和标准化的过程称为 分析 , 我们会在下个章节讨论。
分析与分析器
分析 包含下面的过程:
首先,将一块文本分成适合于倒排索引的独立的 词条 ,
之后,将这些词条统一化为标准格式以提高它们的“可搜索性”,或者 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)"
标准分析器
标准分析器是Elasticsearch默认使用的分析器。它是分析各种语言文本最常用的选择。它根据 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
已经变为词根格式。
什么时候使用分析器
当我们 索引 一个文档,它的全文域被分析成词条以用来创建倒排索引。 但是,当我们在全文域 搜索 的时候,我们需要将查询字符串通过 相同的分析过程 ,以保证我们搜索的词条格式与索引中的词条格式一致。
全文查询,理解每个域是如何定义的,因此它们可以做 正确的事:
当你查询一个 全文 域时, 会对查询字符串应用相同的分析器,以产生正确的搜索词条列表。
当你查询一个 精确值 域时,不会分析查询字符串, 而是搜索你指定的精确值。
现在你可以理解在 开始章节 的查询为什么返回那样的结果:
date
域包含一个精确值:单独的词条 `2014-09-15`。_all
域是一个全文域,所以分词进程将日期转化为三个词条: `2014`, `09`, 和 `15`。
当我们在 _all
域查询 2014`,它匹配所有的12条推文,因为它们都含有 `2014
:
GET /_search?q=2014 # 12 results
View in Sense
当我们在 _all
域查询 2014-09-15`,它首先分析查询字符串,产生匹配 `2014`, `09`, 或 `15
中 任意 词条的查询。这也会匹配所有12条推文,因为它们都含有 2014
:
GET /_search?q=2014-09-15 # 12 results !
View in Sense
当我们在 date
域查询 `2014-09-15`,它寻找 精确日期,只找到一个推文:
GET /_search?q=date:2014-09-15 # 1 result
View in Sense
当我们在 date
域查询 `2014`,它找不到任何文档,因为没有文档含有这个精确日志:
GET /_search?q=date:2014 # 0 results !
View in Sense
测试分析器
有些时候很难理解分词的过程和实际被存储到索引中的词条,特别是你刚接触 Elasticsearch。为了理解发生了什么,你可以使用 analyze
API 来看文本是如何被分析的。在消息体里,指定分析器和要分析的文本:
GET /_analyze { "analyzer": "standard", "text": "Text to analyze" }
View in Sense
结果中每个元素代表一个单独的词条:
{ "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 过滤器。
analyze
API 是一个有用的工具,它有助于我们理解Elasticsearch索引内部发生了什么,随着深入,我们会进一步讨论它。
指定分析器
当Elasticsearch在你的文档中检测到一个新的字符串域 ,它会自动设置其为一个全文 字符串
域,使用 标准
分析器对它进行分析。
你不希望总是这样。可能你想使用一个不同的分析器,适用于你的数据使用的语言。有时候你想要一个字符串域就是一个字符串域--不使用分析,直接索引你传入的精确值,例如用户ID或者一个内部的状态域或标签。
要做到这一点,我们必须手动指定这些域的映射。
映射
为了能够将时间域视为时间,数字域视为数字,字符串域视为全文或精确值字符串, Elasticsearch 需要知道每个域中数据的类型。这个信息包含在映射中。
如 数据输入和输出 中解释的, 索引中每个文档都有 类型 。每种类型都有它自己的 映射 ,或者 模式定义 。映射定义了类型中的域,每个域的数据类型,以及Elasticsearch如何处理这些域。映射也用于配置与类型有关的元数据。
我们会在 类型和映射 详细讨论映射。本节,我们只讨论足够让你入门的内容。
核心简单域类型
Elasticsearch 支持 如下简单域类型:
字符串:
string
整数 :
byte
,short
,integer
,long
浮点数:
float
,double
布尔型:
boolean
日期:
date
当你索引一个包含新域的文档--之前未曾出现-- Elasticsearch 会使用 动态映射 ,通过JSON中基本数据类型,尝试猜测域类型,使用如下规则:
JSON type |
域 type |
布尔型: |
|
整数: |
|
浮点数: |
|
字符串,有效日期: |
|
字符串: |
|
这意味着如果你通过引号( "123"
)索引一个数字,它会被映射为 string
类型,而不是 long
。但是,如果这个域已经映射为 long
,那么 Elasticsearch 会尝试将这个字符串转化为 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" } } } } } }
错误的映射,例如 将 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" } }
其他简单类型(例如 long
, double
, date
等)也接受 index
参数,但有意义的值只有 no
和 not_analyzed
, 因为它们永远不会被分析。
analyzer
对于 analyzed
字符串域,用 analyzer
属性指定在搜索和索引时使用的分析器。默认, Elasticsearch 使用 standard
分析器, 但你可以指定一个内置的分析器替代它,例如whitespace
、 simple
和 `english`:
{ "tweet": { "type": "string", "analyzer": "english" } }
在 自定义分析器 ,我们会展示怎样定义和使用自定义分析器。
更新映射
当你首次 创建一个索引的时候,可以指定类型的映射。你也可以使用 /_mapping
为新类型(或者为存在的类型更新映射)增加映射。
尽管你可以 增加_ 一个存在的映射,你不能 _修改 存在的域映射。如果一个域的映射已经存在,那么该域的数据可能已经被索引。如果你意图修改这个域的映射,索引的数据可能会出错,不能被正常的搜索。
我们可以更新一个映射来添加一个新域,但不能将一个存在的域从 analyzed
改为 not_analyzed
。
为了描述指定映射的两种方式,我们先删除 gd
索引:
DELETE /gb
View in Sense
然后创建一个新索引,指定 tweet
域使用 english
分析器:
PUT /gb { "mappings": { "tweet" : { "properties" : { "tweet" : { "type" : "string", "analyzer": "english" }, "date" : { "type" : "date" }, "name" : { "type" : "string" }, "user_id" : { "type" : "long" } } } } }
View in Sense
|
通过消息体中指定的 |
稍后,我们决定在 tweet
映射增加一个新的名为 tag
的 not_analyzed
的文本域,使用 _mapping
:
PUT /gb/_mapping/tweet { "properties" : { "tag" : { "type" : "string", "index": "not_analyzed" } } }
View in Sense
注意,我们不需要再次列出所有已存在的域,因为无论如何我们都无法改变它们。新域已经被合并到存在的映射中。
测试映射
你可以使用 analyze
API 测试字符串域的映射。比较下面两个请求的输出:
GET /gb/_analyze { "field": "tweet", "text": "Black-cats" } GET /gb/_analyze { "field": "tag", "text": "Black-cats" }
View in Sense
|
消息体里面传输我们想要分析的文本。 |
tweet
域产生两个词条 black
和 cat
, tag
域产生单独的词条 Black-cats
。换句话说,我们的映射正常工作。
复杂核心域类型
除了我们提到的简单标量数据类型, JSON 还有 null
值,数组,和对象,这些 Elasticsearch 都是支持的。
多值域
很有可能,我们希望 tag
域 包含多个标签。我们可以以数组的形式索引标签:
{ "tag": [ "search", "nosql" ]}
对于数组,没有特殊的映射需求。任何域都可以包含0、1或者多个值,就像全文域分析得到多个词条。
这暗示 数组中所有的值必须是相同数据类型的 。你不能将日期和字符串混在一起。如果你通过索引数组来创建新的域,Elasticsearch 会用数组中第一个值的数据类型作为这个域的 类型
。
当你从 Elasticsearch 得到一个文档,每个数组的顺序和你当初索引文档时一样。你得到的 _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" } } }
内部对象的映射
Elasticsearch 会动态 监测新的对象域并映射它们为 对象
,在 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 文档是由一组键值对列表组成的。为了能让 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] }
内部域 可以通过名称引用(例如, first
)。为了区分同名的两个域,我们可以使用全 路径 (例如, user.name.first
) 或 type
名加路径( tweet.user.name.first
)。
在前面简单扁平的文档中,没有 user
和 user.name
域。Lucene 索引只有标量和简单值,没有复杂数据结构。
内部对象数组
最后,考虑包含 内部对象的数组是如何被索引的。 假设我们有个 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 对象,可以回答上面的查询,我们稍后会在嵌套对象中介绍它。
一码不扫,
何以扫天下?
以上是关于小烨收藏ElasticSearch权威指南-映射和分析的主要内容,如果未能解决你的问题,请参考以下文章