欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

ES五| Elasticsearch的映射(字段类型)和分析

程序员文章站 2022-03-02 15:28:42
...

免责声明:本专栏的内容都是在读了《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 字段包含:

  1. The quick brown fox jumped over the lazy dog
  2. 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), 计算匹配单词的数目, 这样我们就可以说第一个文档比第二个匹配度更高——对于我们的查询具有更多相关性。

但是在我们的倒排索引中还有些问题:

  1.  "Quick" 和 "quick" 被认为是不同的单词, 但是用户可能认为它们是相同的。
  2.  "fox" 和 "foxes" 很相似, 就像 "dog" 和 "dogs" ——它们都是同根词。
  3. "jumped" 和 "leap" 不是同根词, 但意思相似——它们是同义词。

上面的索引中, 搜索 "+Quick +fox" 不会匹配任何文档( 记住, 前缀 + 表示单词必须匹配到) 。 只有 "Quick" 和 "fox" 都在同一文档中才可以匹配查询, 但是第一个文档包含 "quick fox" 且第二个文档包含 "Quick foxes" 。 ( 译者注: 这段真啰嗦, 说白了就是单复数和同义词没法匹配)

用户可以合理地希望两个文档都能匹配查询, 我们也可以做得更好。如果我们将词为统一为标准格式, 这样就可以找到不是确切匹配查询, 但是足以相似从而可以关联的文档。 例如:

  1.  "Quick" 可以转为小写成为 "quick" 。
  2. "foxes" 可以被转为根形式 "fox" 。 同理 "dogs" 可以被转为 "dog" 。
  3. "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)时, 我们要让查询字符串经过同样的分析流程处理, 以确保这些词在索引中存在。

  1. 当你查询全文(full text)字段, 查询将使用相同的分析器来分析查询字符串, 以产生正确的词列表。
  2. 当你查询一个确切值(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中被索引的。