本文主要内容:
1、精确值和全文搜索
2、创建倒排索引
3、内置分析器、分词测试
4、基本数据类型
5、自定义映射
6、更新映射(只能增加)
7、复杂数据类型
精确值和全文
Elasticsearch 中的数据可以概括的分为两类:精确值和全文。
精确值 如它们听起来那样精确。例如日期或者用户 ID,但字符串也可以表示精确值,例如用户名或邮箱地址。对于精确值来讲,Foo 和 foo 是不同的,2014 和 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
。
我们的用户可以合理的期望两个文档与查询匹配。我们可以做的更好。
如果我们将词条规范为标准模式(analyzer:'standard'
),那么我们可以找到与用户搜索的词条不完全一致,但具有足够相关性的文档。例如:
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 ,它们对相关性没有多少影响),它们会被删除。 由于理解英语语法的规则,这个分词器可以提取英语单词的 词干 。
english分词器会产生下面的词条:
set, shape, semi, transpar, call, set_tran, 5
分词测试:
GET /gb/_analyze
{
"field": "tweet",
"text": "Black-cats"
}
{
"tokens": [
{
"token": "black",
"start_offset": 0,
"end_offset": 5,
"type": "<ALPHANUM>",
"position": 0
},
{
"token": "cats",
"start_offset": 6,
"end_offset": 10,
"type": "<ALPHANUM>",
"position": 1
}
]
}
GET /gb/_analyze
{
"field": "name",
"text": "Black-cats"
}
{
"tokens": [
{
"token": "Black-cats",
"start_offset": 0,
"end_offset": 10,
"type": "word",
"position": 0
}
]
}
基本域数据类型
Elasticsearch 支持如下基本域数据类型:
字符串: string
整数 : byte
, short
, integer
, long
浮点数: float
, double
布尔型: boolean
日期: date
当你索引一个包含新域的文档–之前未曾出现– Elasticsearch 会使用 动态映射
(即自动推测类型) 。
注意:
ElasticSearch 5.X以上的版本已经不支持string类型,而是改为text和keyword,前者支持分词,后者则是精确匹配不支持分词
自定义域映射
尽管在很多情况下基本域数据类型已经够用,但你经常需要为单独域自定义映射 ,特别是字符串域。自定义映射允许你执行下面的操作:
- 全文字符串域和精确值字符串域的区别
- 使用特定语言分析器
- 优化域以适应部分匹配
- 指定自定义数据格式
- 还有更多
域最重要的属性是 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"
}
}
对于 analyzed
字符串域,用 analyzer
属性指定在搜索和索引时使用的分析器。默认, Elasticsearch 使用 standard
分析器, 但你可以指定一个内置的分析器替代它:
{
"tweet": {
"type": "string",
"analyzer": "english"
}
}
更新映射:
当你首次创建一个索引的时候,可以指定类型的映射。你也可以使用 /_mapping
为新类型(或者为存在的类型更新映射)增加映射。
尽管你可以 增加_ 一个存在的映射,你不能 _修改 存在的域映射。如果一个域的映射已经存在,那么该域的数据可能已经被索引。如果你意图修改这个域的映射,索引的数据可能会出错,不能被正常的搜索。
我们可以更新一个映射来添加一个新域,但不能将一个存在的域从 analyzed
改为 not_analyzed
。
创建一个索引,消息体里指定mapping映射:
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"
}
}
}
注意:
我们不需要再次列出所有已存在的域,因为无论如何我们都无法改变它们。新域已经被合并到存在的映射中。
复杂核心域类型
除了我们提到的简单标量数据类型, JSON 还有 null 值,数组,和对象,这些 Elasticsearch 都是支持的。
1、多值域
很有可能,我们希望 tag 域 包含多个标签。我们可以以数组的形式索引标签:
{ "tag": [ "search", "nosql" ]}
对于数组,没有特殊的映射需求。任何域
都可以包含0、1或者多个值,就像全文域分析得到多个词条。
这暗示 数组中所有的值必须是相同数据类型的 。你不能将日期和字符串混在一起。如果你通过索引数组来创建新的域,Elasticsearch 会用数组中第一个值的数据类型作为这个域的 类型 。
2、空域
当然,数组可以为空。 这相当于存在零值。 事实上,在 Lucene
中是不能存储 null
值的,所以我们认为存在 null
值的域为空域。
下面三种域被认为是空的,它们将不会被索引:
"null_value": null,
"empty_array": [],
"array_with_null_value": [ null ]
3、多层次对象
我们讨论的最后一个 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"
}
}
}
4、内部对象的映射
{
"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]
}
内部对象数组
最后,考虑包含 内部对象的数组是如何被索引的。 假设我们有个 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
对象,可以回答上面的查询。