ElasticSearch的映射和分析

映射和分析

映射(mapping)机制用于进行字段类型确认,将每个字段匹配为一种确定的数据类型( string , number , booleans , date 等)。

分析(analysis)机制用于进行 全文文本(Full Text)的分词,以建立供搜索用的反向索引。

让我们看看Elasticsearch在对 megacorp索引中的 employee类型进行 mapping后是如何解读我们的文档结构:

Elasticsearch为对字段类型进行猜测,动态生成了字段和类型的映射关系。返回的信息显示了 date 字段被识别为 date 类型。 _all 因为是默认字段所以没有在此显示,不过我们知道它是 string 类型。

date 类型的字段和 string 类型的字段的索引方式是不同的,因此导致查询结果的不同。

每一种核心数据类型(strings, numbers, booleans及dates)在Elasticsearch中他们是被区别对待的。

但是更大的区别在于 确切值 (exact values)(比如 string 类型)及 全文文本 (full text)之间。

确切值和全文文本

Elasticsearch中的数据可以大致分为两种类型:确切值 及 全文文本 。

扫描二维码关注公众号,回复: 5689503 查看本文章

确切值是确定的,正如它的名字一样。比如一个date或用户ID,也可以包含更多的字符串比如username或email地址等。确切值 "Foo" 和 "foo" 就并不相同。确切值 2014 和 2014-09-15 也不相同。

全文文本,从另一个角度来说是文本化的数据(常常以人类的语言书写),比如一片推文(Twitter的文章)或邮件正文等。

确切值是很容易查询的,因为结果是二进制的 -- 要么匹配,要么不匹配。

而对于全文数据的查询来说,却有些微妙。我们不会去询问 这篇文档是否匹配查询要求? 。 但是,我们会询问 这篇文档和查询的匹配程度如何? 。换句话说,对于查询条件,这篇文档的 相关性 有多高?我们很少确切的匹配整个全文文本。我们想在全文中查询 包含 查询文本的部分。我们还期望搜索引擎能理解我们的意图,如:

• 一个针对 "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

Doc1

Doc2

Quick

 

X

The

X

 

brown

X

X

dog

X

 

dogs

 

X

quick

X

 

....

....

...

现在,如果我们想搜索 "quick brown" ,我们只需要找到每个词在哪个文档中出现既可:

Term

Doc_1

Doc_2

brown

X

X

quick

X

 

两个文档都匹配,但是第一个比第二个有更多的匹配项。 如果我们加入简单的 相似度算法(similarity algorithm),计算匹配单词的数目,这样我们就可以说第一个文档比第二个匹配度更高——对于我们的查询具有更多相关性。

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

1. "Quick" 和 "quick" 被认为是不同的单词,但是用户可能认为它们是相同的。

2. "dog" 和 "dogs" ——它们都是同根词。

3. "jumped" 和 "leap" 不是同根词,但意思相似——它们是同义词。

上面的索引中,搜索 "+Quick +fox" 不会匹配任何文档(记住,前缀 + 表示单词必须匹配到)。

但是第一个文档包含 "quick fox" 且第二个文档包含 "Quick foxes" 。

如果我们将词为统一为标准格式,这样就可以找到不是确切匹配查询,但是足以相似从而可以关联的文档。例如:

1. "Quick" 可以转为小写成为 "quick" 。

2. "foxes" 可以被转为根形式 ""fox 。同理 "dogs" 可以被转为 "dog" 。

3. "jumped" 和 "leap" 同义就可以只索引为单个词 "jump"

但我们还未成功。我们的搜索 "+Quick +fox" 依旧 失败,因为 "Quick" 的确切值已经不在索引里,不过,如果我们使用相同的标准化规则处理查询字符串的 content 字段,查询将变成 "+quick +fox" ,这样就可以匹配到两个文档。

这个表征化(断词)和标准化的过程叫做 分词(analysis)

分析和分析器

分词(analysis)是这样一个过程:

• 首先,表征化(断词)一个文本块为适用于倒排索引单独的 词(term)

• 然后标准化这些词为标准形式,提高它们的“可搜索性”或“查全率”

这个工作是 分词器(analyzer)完成的。一个 分词器(analyzer)包装了三个功能:

字符过滤器(character filter):字符过滤器 用来 整理 一个尚未被分词的字符串。例如,如果我们的文本是HTML格式的,它会包含像 <p> 或者 <div> 这样的HTML标签,这些标签是我们不想索引的。我们可以使用 html清除 字符过滤器 来移除掉所有的HTML标签,并且像把 &Aacute; 转换为相对应的Unicode字符 Á 这样,转换HTML实体。

一个分析器可能有0个或者多个字符过滤器。

分词器(tokenizer):用来将文本表征化(断词)为独立的词。一个分析器 必须 有一个唯一的分词器。 分词器把字符串分解成单个词条或者词汇单元

表征过滤(token filters)(或称为断词过滤)每个词都通过所有 表征过滤(token filters),它可以修改词(例如将 "Quick" 转为小写),去掉词(例如停用词像 "a" 、 "and"``"the" 等等),或者增加词(例如同义词像 "jump" 和 "leap" )

Elasticsearch提供很多开箱即用的字符过滤器,分词器和表征过滤器。这些可以组合来创建自定义的分析器以应对不同的需求。

经过分词,作为结果的 词单元流 会按照指定的顺序通过指定的词单元过滤器 。词单元过滤器可以修改、添加或者移除表征词。我们已经提到过 lowercase stop 词过滤器 ,但是在 Elasticsearch 里面还有很多可供选择的表征词过滤器。 词干过滤器 把单词 遏制 为 词干。 ascii_folding 过滤器移除变音符,把一个像 "très" 这样的词转换为 "tres" 。 ngramedge_ngram 词单元过滤器 可以产生 适合用于部分匹配或者自动补全的词单元。

内建的分词器

除了自定义分词器外,ElasticSearch自带了一些预装的分词器可以直接使用。例如:标准分词器(ES默认的分词器),简单分词器,空格分词器,语言分词器等;

分词器的使用

当我们索引一个文档时,全文字段会被分词器分割成单独的词来创建倒排索引。

当我们在全文字段搜索时,我们要输入的查询字符串也同样会按照相同的分词器进行分割处理,以确保这些词在索引中存在。

当你查询全文(full text)字段,查询将使用相同的分析器来分析查询字符串,以产生正确的词列表。

当你查询一个 确切值(exact value)字段,查询将不分析查询字符串,但是你可以自己指定。

GET /_search?q=2014 # 12 个结果

GET /_search?q=2014-09-15 # 还是 12 个结果 !

GET /_search?q=date:2014-09-15 # 1 一个结果

GET /_search?q=date:2014 # 0 个结果 !

为什么全日期的查询返回所有的相关结果,而针对 date 字段进行年度查询却什么都不返回?

为什么我们的结果因查询 _all 字段(译者注:默认所有字段中进行查询)或 date 字段而变得不同?

想必是因为我们的数据在 _all 字段的索引方式和在 date 字段的索引方式不同而导致。

date 字段包含一个确切值:单独的一个词 "2014-09-15" 。 * _all 字段是一个全文字段,所以分析过程将日期转为三个词: "2014" 、 "09" 和 "15" 。

当我们在 _all 字段查询 2014 ,它一个匹配到12条推文,因为这些推文都包含词 2014;

当我们在 _all 字段中查询 2014-09-15 ,首先分析查询字符串,产生匹配 任一词 2014 、 09 或 15 的查询语句,它依旧匹配12个推文,因为它们都包含词 2014 。

当我们在 date 字段中查询 2014-09-15 ,它查询一个 确切的日期,然后只找到一条推文。

当我们在 date 字段中查询 2014 ,没有找到文档,因为没有文档包含那个确切的日期。

测试分词器

token 是一个实际被存储在索引中的词。 position 指明词在原文本中是第几个出现的。 start_offset 和 end_offset 表示词在原文本中占据的位置。

指定分析器

当Elasticsearch在你的文档中探测到一个新的字符串字段,它将自动设置它为全文 string 字段并用 standard分词器分析。

你不可能总是想要这样做。也许你想使用一个更适合这个数据的语言分析器。或者,你只想把字符串字段当作一个普通的字段——不做任何分析,只存储确切值,就像字符串类型的用户ID或者内部状态字段或者标签。

为了达到这种效果,我们必须通过 映射(mapping)人工设置这些字段。

映射

索引中每个文档都有一个 类型(type)。 每个类型拥有自己的 映射(mapping)一个映射定义了字段类型,每个字段的数据类型以及字段被Elasticsearch处理的方式。映射还用于设置关联到类型上的元数据。

核心简单字段类型

类型

表示的数据类型

字符串

keyword

整型

byte,short,integer,long

浮点型

float,double

布尔型

boolean

文本型

text

日期

date

当你索引一个包含新字段的文档,Elasticsearch将使用动态映射猜测字段类型,这类型来自于JSON的基本数据类型。

创建包含 mappings 的索引mytest,自定义的映射在请求体中指定。

我们可以使用 _mapping 后缀在 user的映射中增加一个新的 not_analyzed 类型的文本字段:tag

注意到我们不再需要列出所有的已经存在的字段,因为我们没法修改他们。我们的新字段已经被合并至存在的那个映射中。

复合核心字段类型

除了简单的标量类型,JSON还有 null 值,数组和对象,所有这些Elasticsearch都支持:

多值字段

我们想让 tag 字段包含多个字段,这非常有可能发生。我们可以索引一个标签数组来代替单一字符串

对于数组不需要特殊的映射。任何一个字段可以包含零个、一个或多个值,同样对于全文字段将被分析并产生多个词。

这意味着 数组中所有值必须为同一类型。你不能把日期和字符窜混合。如果你创建一个新字段,这个

字段索引了一个数组,Elasticsearch将使用第一个值的类型来确定这个新字段的类型。

当你从Elasticsearch中取回一个文档,任何一个数组的顺序和你索引它们的顺序一致。你取回的 _source 字段的顺序同样与索引它们的顺序相同。

然而,数组是做为多值字段被 索引的,它们没有顺序。在搜索阶段你不能指定“第一个值”或者“最后一个值”。

空字段

当然数组可以是空的。这等价于有零个值。事实上,Lucene没法存放 null 值,所以一个 null 值的字段被认为是空字段。这四个字段将被识别为空字段而不被索引:

"empty_string": "",

"null_value": null,

"empty_array": [],

"array_with_null_value": [ null ]

内部对象(又称多层对象)

内部对象(inner objects)经常用于嵌入一个实体或对象里的另一个地方。例如

检索出这条数据,如下:

对 user 和 name 字段的映射与 _source类型自己很相似。事实上, type 映射只是 object 映射的一种特殊类型,我们将 object 称为根对象 。它与其他对象一模一样,除非它有一些特殊的顶层字段,比如 _source , _all 等等。

内部对象是怎样被索引的

Lucene不支持内部对象。Lucene文档由key-value的平面列表组成。为了使Elasticsearch能够有效地索引内层对象,ES将我们的文档转换成这样的内容:

{

"user.age": [27],

"user.birthday": [1990-09-09],

"user.name.full": [Zhang, Haitao],

"user.name.first": [Haitao],

"user.name.last": [Zhang]

"user.tag":[search,Elastic,ES]

}

内部对象可以用名字来称呼,为了区分具有相同名称的两个字段,我们可以使用完整路径例如:

user.name.first

在上边的简单扁平文档中,没有字段称为“user”,也没有字段称为“user.name”.Lucene值索引简单字段类型,不会识别和索引复杂的数据结构。

请求体查询

简单查询语句(lite)是一种有效的命令行 自组织模式查询。但是,如果你想要善用搜索,你必须使用请求体查询(request body search )API。之所以这么称呼,是因为大多数的参数以JSON格式所容纳的并不是查询字符串。

请求体查询(下文简称查询),并不仅仅用来处理查询,而且还可以高亮返回结果中的片段,并且给出帮助你的用户找寻最好结果的相关数据建议。

空查询

以最简单的 search API开始,空查询将会返回索引中所有的文档。

同字符串查询一样,你可以查询一个,多个或 _all 索引(indices)或类型(types)

携带内容的 GET 请求?

任何一种语言(特别是js)的HTTP库都不允许 GET 请求中携带交互数据。

Elasticsearch倾向于使用 GET 提交查询请求,因为这个词相比 POST 来说,能更好的描述这种行为。 然而,因为携带交互数据的 GET 请求并不被广泛支持,所以 search API同样支持 POST 请求。

结构化查询 Query DSL

结构化查询是一种灵活的,多表现形式的查询语言。 Elasticsearch在一个简单的JSON接口中用结构化查询来展现Lucene绝大多数能力。 你应当在你的产品中采用这种方式进行查询。它使得你的查询更加灵活,精准,易于阅读并且易于debug。

使用结构化查询,你需要传递 query 参数:

空查询 - {} - 在功能上等同于使用 match_all 查询子句,正如其名字一样,匹配所有的文档:

查询子句——match(匹配查询)

一个查询子句一般使用这种结构:

{

    QUERY_NAME: {

    ARGUMENT: VALUE,

    ARGUMENT: VALUE,...

    }

}

可以使用 match 查询子句(匹配查询)用来找寻在 last_name字段中找寻包含 Smith的成员

查询与过滤

上节提到了结构化查询语句,事实上我们可以使用两种结构化语句: 结构化查询(Query DSL)和结

构化过滤(Filter DSL)。 查询与过滤语句非常相似,但是它们由于使用目的不同而稍有差异。

一条过滤语句会询问每个文档的字段值是否包含着特定值。如:

  1. 是否 created 的日期范围在 2013 到 2014 ?
  2. 是否 status 字段中包含单词 "published" ?
  3. 是否 lat_lon 字段中的地理位置与目标点相距不超过10km ?

一条查询语句与过滤语句相似,但问法不同:

查询语句会询问每个文档的字段值与特定值的匹配程度如何?

查询语句的典型用法是为了找到文档。一条查询语句会计算每个文档与查询语句的相关性,会给出一个相关性评分 _score ,并且 按照相关性对匹配到的文档进行排序。 这种评分方式非常适用于一个没有完全配置结果的全文本搜索。

性能差异

使用过滤语句得到的结果集 -- 一个简单的文档列表,快速匹配运算并存入内存是十分方便的, 每个文档仅需要1个字节。这些缓存的过滤结果集与后续请求的结合使用是非常高效的。

查询语句不仅要查找相匹配的文档,还需要计算每个文档的相关性,所以一般来说查询语句要比 过滤语句更耗时,并且查询结果也不可缓存。

幸亏有了倒排索引,一个只匹配少量文档的简单查询语句在百万级文档中的查询效率会与一条经过缓存 的过滤语句旗鼓相当,甚至略占上风。 但是一般情况下,一条经过缓存的过滤查询要远胜一条查询语句的执行效率。

过滤语句的目的就是缩小匹配的文档结果集,所以需要仔细检查过滤条件。

什么时候使用过滤语句

原则上来说,使用查询语句做全文本搜索或其他需要进行相关性评分的时候,剩下的全部用过滤语句

最常用的查询过滤语句

term 过滤

term 主要用于精确匹配哪些值,比如数字,日期,布尔值或 not_analyzed 的字符串(未经分析的文本数据类型)

terms  过滤

terms 跟 term 有点类似,但 terms 允许指定多个匹配条件。 如果某个字段指定了多个值,那么文档需要一起去做匹配:

range  过滤

range 过滤允许我们按照指定范围查找一批数据:

范围操作符包含:

gt <-----> 大于 ,      gte <-----> 大于等于,      lt <----->  小于,        lte  <----->  小于等于

exists 和 missing 过滤

exists 和 missing 过滤可以用于查找文档中是否包含指定字段或没有某个字段,类似于SQL语句中的 IS_NULL 条件。这两个过滤只是针对已经查出一批数据来,但是想区分出某个字段是否存在的时候使用。

bool  过滤

bool 过滤可以用来合并多个过滤条件查询结果的布尔逻辑,它包含一下操作符:

must  <---->  多个查询条件的完全匹配, 相当于 and

must_not  <---->  多个查询条件的相反匹配, 相当于 not

should  <---->  至少有一个查询条件匹配, 相当于 or

match_all 查询

即空查询,是没有查询条件下的默认查询语句,可以查询到所有文档。(见上文:结构化查询 Query DSL);

match  查询

查询是一个标准查询,不管你需要全文本查询还是精确查询基本上都要用到它。

如果你使用 match 查询一个全文本字段,它会在真正查询之前用分析器先分析 match 一下查询字符。如果用 match 下指定了一个确切值,在遇到数字,日期,布尔值或者 not_analyzed 的字符串时,它将为你搜索你给定的值。(具体实例见上文:结构化查询 Query DSL);

multi_match 查询

multi_match 查询允许你做 match 查询的基础上同时搜索多个字段:

bool  查询  (对比bool过滤)

查询与过滤条件的合并

查询语句和过滤语句可以放在各自的上下文中。 在 ElasticSearch API 中我们会看到许多带有 query 或 filter的语句。 这些语句既可以包含单条 query 语句,也可以包含一条 filter 子句。 换句话说,这些语句需要首先创建一个 query 或 filter 的上下文关系。

复合查询语句可以加入其他查询子句,复合过滤语句也可以加入其他过滤子句。 通常情况下,一条查询语句需要过滤语句的辅助,全文本搜索除外。

所以说,查询语句可以包含过滤子句,反之亦然。 以便于我们切换 query 或 filter 的上下文。这就要求我们在读懂需求的同时构造正确有效的语句。

验证查询

查询语句可以变得非常复杂,特别是与不同的分析器和字段映射相结合后,就会有些难度。

validate API 可以验证一条查询语句是否合法。

理解错误信息

想知道语句非法的具体错误信息,需要加上 explain 参数:

相关性介绍

我们曾经讲过,默认情况下,返回结果是按相关性倒序排列的。 但是什么是相关性? 相关性如何计算?每个文档都有相关性评分,用一个相对的浮点数字段 _score 来表示 -- _score 的评分越高,相关性越高。

查询语句会为每个文档添加一个 _score 字段。评分的计算方式取决于不同的查询类型 -- 不同的查询语句用于不同的目的: fuzzy 查询会计算与关键词的拼写相似程度, terms 查询会计算 找到的内容与关键词组成部分匹配的百分比,但是一般意义上我们说的全文本搜索是指计算内容与关键词的类似程度。

ElasticSearch的相似度算法被定义为 TF/IDF,即检索词频率/反向文档频率,包括一下内容:

检索词频率:

检索词在该字段出现的频率,出现频率越高,相关性也越高。

反向文档频率:

每个检索词在索引中出现的频率。频率越高,相关性越低。检索词出现在多数文档中会比出现在少数文档中的权重更低, 即检验一个检索词在文档中的普遍重要性。

字段长度准则:

字段的长度是多少?长度越长,相关性越低。 检索词出现在一个短的 title 要比同样的词出现在一个长的 content 字段的相关性高。

相关性排序

默认情况下,结果集会按照相关性进行排序 -- 相关性越高。

为了使结果可以按照相关性进行排序,我们需要一个相关性的值。在ElasticSearch的查询结果中, 相关性分值会用 _score 字段来给出一个浮点型的数值,所以默认情况下,结果集以 _score 进行倒序排列。

有时,即便如此,你还是没有一个有意义的相关性分值。比如,以下语句返回所有tweets中 user_id 是否 包含值 1,过滤语句与 _score 没有关系,但是有隐含的查询条件 match_all 为所有的文档的 _score 设值为 1 。 也就相当于所有的文档相关性是相同的。

如果多条查询子句被合并为一条复合查询语句,比如 bool 查询,则每个查询子句计算得出的评分会被合并到总的相关性评分中。

当调试一条复杂的查询语句时,想要理解相关性评分 _score 是比较困难的。ElasticSearch 在 每个查询语句中都有一个explain参数,将 explain 设为 true 就可以得到更详细的信息。

注: 输出 explain 结果代价是十分昂贵的,它只能用作调试工具 --千万不要用于生产环境。

排序方式

字段值排序

对结果集按照时间,年龄等排序,这也是最常见的情形,将最新的文档排列靠前。 我们使用 sort 参数进行排序:

首先,在每个结果中增加了一个 sort 字段,它所包含的值是用来排序的。在这个例子当中用的是age字段。

其次就是 _score 和 max_score 字段都为 null 。计算 _score 是比较消耗性能的, 而且通常主要用作排序-- 我们不是用相关性进行排序的时候,就不需要统计其相关性。 如果你想强制计算其相关性,可以设置 track_scores 为 true 。_score 默认以倒序排列。

多级排序

如果我们想要合并一个查询语句,并且展示所有匹配的结果集使用第一排序是 age,第二排序是 _score :

排序是很重要的。结果集会先用第一排序字段来排序,当用用作第一字段排序的值相同的时候, 然后再用第二字段对第一排序值相同的文档进行排序,以此类推。排序字段要求是数值。

数据字段

当你对一个字段进行排序时,ElasticSearch 需要进入每个匹配到的文档得到相关的值。 倒排索引在用于搜索时是非常卓越的,但却不是理想的排序结构。

  1. 当搜索的时候,我们需要用检索词去遍历所有的文档。
  2. 当排序的时候,我们需要遍历文档中所有的值,我们需要做反倒序排列操作。

为了提高排序效率,ElasticSearch 会将所有字段的值加载到内存中,这就叫做"数据字段"

ElasticSearch将所有字段数据加载到内存中并不是匹配到的那部分数据。 而是索引下所有文档中的

值,包括所有类型。

将所有字段数据加载到内存中是因为从硬盘反向倒排索引是非常缓慢的。尽管你这次请求需要的是某些文档中的部分数据, 但你下个请求却需要另外的数据,所以将所有字段数据一次性加载到内存中是十分必要的。

ElasticSearch中的字段数据常被应用到以下场景:

• 对一个字段进行排序

• 对一个字段进行聚合

• 某些过滤,比如地理位置过滤

• 某些与字段相关的脚本计算

毫无疑问,这会消耗掉很多内存,值得庆幸的是,内存不足是可以通过横向扩展解决的,我们可以增加更多的节点到集群。

猜你喜欢

转载自blog.csdn.net/zhtzh312/article/details/88863168
今日推荐