《Elasticsearch检索引擎学习指南》第六章 映射和分析

参考资料

精确值VS全文

ES中的数据可以概括的分为两类:精确值和全文
精确值就是其字面意思。全文是指文本数据。(全文通常指非结构化的数据,但这里有一个误解:自然语言是高度结构化的。问题在于自然语言的规则是复杂的,导致计算机难以正确解析。)

精确值很容易查询。结果只有两种,是与否。
查询全文数据要微妙许多。不是要求匹配与否,而是要知道匹配程度有多少,也就是要知道文档与查询的相关性如何。

我们很少会对全文类型的域做精确匹配。相反,我们希望在文本类型的域中搜索。不仅如此,我们还希望搜索能够理解我们的意图:

  • 搜索UK,会返回包含United Kingdom的文档
  • 搜索jump,会匹配jumped,jumps甚至leap

为了促进这类在全文域中的查询,ES首先分析文档,之后根据结果创建倒排索引

倒排索引

ES使用一种称为倒排索引的结构,它适用于快速的全文搜索。一个倒排索引由文档中所有不重复词的列表构成,对于其中每个词,有一个包含它的文档列表。

例如,假设我们有两个文档,每个文档的content域包含如下内容:

  1. The quick brown fox jumped over the lazy dog
  2. Quick brown foxes leap over lazy dogs in summer

为了创建倒排索引,首先将每个文档的content域拆分成单独的词(我们称之为词条(Term)或tokens),创建一个包含所有不重复词条的排序列表,然后列出每个词条出现在哪个文档。如下所示:

倒排索引
现在,如果我们想搜索 quick brown ,我们只需要查找包含每个词条的文档:
在这里插入图片描述
两个文档都匹配,但是第一个文档比第二个匹配度更高。如果我们使用仅计算匹配词条数量的简单相似性算法,那么,我们可以说,对于我们查询的相关性来讲,第一个文档比第二个文档更佳。

但是,目前的倒排索引有一些问题:

  • Quick和quick独立出现,实际上它们可能是相同的
  • fox和foxes的单复数形式,dog和dogs
  • jump和leap属于近义词

使用前面的索引搜索 +Quick +fox 不会得到任何匹配文档。(记住,+ 前缀表明这个词必须存在。)只有同时出现 Quick 和 fox 的文档才满足这个查询条件,但是第一个文档包含 quick fox ,第二个文档包含 Quick foxes 。

如果我们将此条规范为标准模式,那么可以找到与用户搜索的词条完全不一致,但具有足够相关性的文档。例如

  • Quick 可以小写化为 quick 。
  • foxes 可以 词干提取 --变为词根的格式-- 为 fox 。类似的, dogs 可以为提取为 dog 。
  • jumped 和 leap 是同义词,可以索引为相同的单词 jump 。

之后的索引如下:
在这里插入图片描述
这还远远不够。精确搜索仍然会失败(如 +Quick +fox,因为在我们的索引中,已经没有 Quick 了 )。但是,如果我们对搜索的字符串使用与 content 域相同的标准化规则,会变成查询 +quick +fox ,这样两个文档都会匹配!

NOTE:这非常重要,只能搜索在索引中存在的词条,索引文本和查询字符串必须标准化为相同的格式。

分词与标准化的过程称为分析,就是下面的内容。

分析与分析器

分析包含下面的过程:

  • 首先,将一块文本分成适合于倒排索引的独立的词条
  • 之后,将这些词条统一化为标准格式以提高它们的可搜索性,或者recall

分析器执行上面的工作。分析器实际上是将三个功能封装:

  • 字符过滤器
    首先,字符串按顺序通过每个字符过滤器。他们的任务是在分词前整理字符串。一个字符过滤器可以用来去掉HTML或者将& 转换为and
  • 分词器
    其次,字符串被分词器分为单个的词条。一个简单的分词器遇到空格和标点的时候,可能会将文本拆分成词条
  • Token过滤器
    最后,词条按顺序通过每个token过滤器。这个过程可能会改变词条(标准化),删除词条(例如 a,and ,the等无用词),或者增加词条(例如,像jump和leap这种同义词)

ES提供了开箱即用的字符过滤器、分词器和token过滤器。这些可以组合起来形成自定义的分析器以用于不同的目的。

内置分析器

但是,ES还附带了可以直接使用的预包装的分析器。接下来会列出最重要的分析器。以一个字符串作为处理对象,比较几个分析器的处理。

Set the shape to semi-transparent by calling set_trans(5)
  • 标准分析器
    标准分析器是ES默认使用的分析器。它是分析各种文本语言最常用的选择。它根据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 已经变为词根格式。

什么时候使用分析器

当我们索引一个文档,它的全文域被分析成词条以用来创建倒排索引。但是,当我们在全文域搜索的时候,我们需要将查询字符串通过相同的分析过程,以保证我们搜索的词条格式与索引中的词条格式一致。
全文查询,理解每个域是如何定义的,因此它们可以做正确的事:

  • 当查询一个全文域时,会对查询字符串应用相同的分析器,以产生正确的索引词条列表。
  • 当查询一个精确值域时,不会分析查询字符串,而是搜索指定的精确值。

测试分析器

有些时候很难理解分词的过程和实际被存储到索引中的词条,特别是刚接触ES。为了理解发生了什么,可以使用analyze API来看文本是如何被分析的。在消息体里,指定分析器和要分析的文本:

GET /_analyze
{
  "analyzer": "standard",
  "text": "Text to analyze"
}

结果中每个元素代表一个单独的词条:

{
   "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 过滤器

指定分析器

当ES在文档中检测到一个新的字符串域,它会自动设置其为一个全文字符串域,使用标准分析器对它进行分析。
如果对于一个字符串域不使用分析,直接索引传入的精确值,例如用户ID或者一个内部的状态域或标签。要做到这些,必须手动指定这些域的映射。

映射

为了能够将时间域视为时间,数字域视为数字,字符串域视为全文或精确值字符串,ES需要知道每个域中数据的类型。这个信息包含在映射中。
索引中每个文档都有类型。每种类型都有它自己的映射,或者模式定义。映射定义了类型中的域,每个域的数据类型,以及ES如何处理这些域。映射也用于配置与类型有关的元数据。

核心简单域类型

ES支持如下简单域类型:

  • 字符串 stirng
  • 整数 byte short integer long
  • 浮点数 float double
  • 布尔型 boolean
  • 日期 date
    当索引一个包含新域的文档–之前未曾出现–ES会使用动态映射,通过JSON中基本数据类型,尝试猜测域类型。
    NOTE:这意味着如果你通过引号( “123” )索引一个数字,它会被映射为 string 类型,而不是 long 。但是,如果这个域已经映射为 long ,那么 Elasticsearch 会尝试将这个字符串转化为 long ,如果无法转化,则抛出一个异常。

查看映射

通过/_mapping ,我们可以查看ES在一个或多个索引中的一个或多个类型的映射。如下取得索引 gb 中类型 tweet 的映射:

GET /gb/_mapping/tweet

ES根据索引的文档,为域(称为 属性 )动态生成的映射

{
   "gb": {
      "mappings": {
         "tweet": {
            "properties": {
               "date": {
                  "type": "date",
                  "format": "strict_date_optional_time||epoch_millis"
               },
               "name": {
                  "type": "string"
               },
               "tweet": {
                  "type": "string"
               },
               "user_id": {
                  "type": "long"
               }
            }
         }
      }
   }
}

TIP:
错误的映射,例如 将 age 域映射为 string 类型,而不是 integer ,会导致查询出现令人困惑的结果。

检查一下!而不是假设你的映射是正确的。

自定义域映射

尽管在很多情况下基本数据类型已经够用,但经常需要为单独域自定义映射,特别是字符串域。自定义映射允许执行下面的操作:

  • 全文字符串域和精确值字符串域的区别
  • 使用特定语言分析器
  • 优化域以适应部分匹配
  • 指定自定义数据格式
  • 还有更多
    域的重要属性是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"
    }
}

NOTE:
其他简单类型(例如 long , double , date 等)也接受 index 参数,但有意义的值只有 no 和 not_analyzed , 因为它们永远不会被分析。

analyzer

对于analyzed字符串域,用analyzer属性指定在搜索和索引时使用的分析器。默认,ES使用standard分析器,但可以指定一个内置的分析器替代它,例如whitespace、simple和english:

{
    "tweet": {
        "type":     "string",
        "analyzer": "english"
    }
}

自定义分析器,可以看到更多定义和使用自定义分析器的细节。

更新映射

当首次创建一个索引的时候,可以指定类型的映射。也可以使用/_mapping 为新类型(或者为存在的类型更新映射)增加映射。
==NOTE:==尽管可以增加一个存在的映射,但不能修改存在的域映射。如果一个域的映射已经存在,那么该域的数据可能已经被索引。如果意图修改这个域的映射,索引的数据可能会出错,不能被正常的搜索。
我们可以更新一个映射来添加一个新域,但不能将一个存在的域从analyzed改为not_analyzed。
为了描述指定映射的两种方式,先删除gd索引:

DELETE /gb

然后创建一个新索引,指定tweet域使用english分析器:

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"
    }
  }
}

注意,我们不需要再次列出所有已存在的域,因为无论如何我们都无法改变它们。新域已经被合并到存在的映射中。

测试映射

可以使用analyze API测试字符串域的映射。比较下面两个请求的输出:

GET /gb/_analyze
{
  "field": "tweet",
  "text": "Black-cats" 
}

GET /gb/_analyze
{
  "field": "tag",
  "text": "Black-cats" 
}

tweet 域产生两个词条 black 和 cat , tag 域产生单独的词条 Black-cats 。换句话说,我们的映射正常工作。

复杂核心域类型

除了之前提到的简单标量数据类型,JSON还有null值,数组和对象,这些ES都是支持的。

多值域

很有可能,希望tag域包含多个标签。可以使用数组的形式索引标签:

{ "tag": [ "search", "nosql" ]}

对于数组,没有特殊的映射需求。任何域都可以包含多个值。就像全文域分析得到多个词条。
这意味着数组中所有的值必须是相同数据类型。不能将日期和字符串混在一起。如果通过索引数组来创建新的域。ES会用数组中第一个值的数据类型作为这个域的类型。
==NOTE:==当从ES得到一个文档,每个数组的顺序和当初索引文档时一样。得到的_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"
        }
    }
}

内部对象的映射

ES会动态监测新的对象域并映射它们为对象,在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文档是由一组键值对列表组成的。为了能让ES有效地索引内部类,它把我们的文档转化为这样:

{
    "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 对象,可以回答上面的查询,我们稍后会在嵌套对象中介绍它。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值