Elasticsearch---读书笔记(4)

164 阅读11分钟

二、 基础知识

6. 映射和分析

当摆弄索引里面的数据时,我们发现一些奇怪的事情。一些事情看起来被打乱了:在我们的索引中有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 !

为什么在 _all 字段查询日期返回所有推文,而在 date 字段只查询年份却没有返回结果?为什么我们在 _all 字段和 date 字段的查询结果有差别?

推测起来,这是因为数据在 all 字段与 date 字段的索引方式不同。所以,通过请求 gb 索引中 tweet 类型的_映射(或模式定义),让我们看一看 Elasticsearch 是如何解释我们文档结构的:

GET /gb/_mapping/tweet

这将得到如下结果:

{
   "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 的索引方式有稍许不同。

6.1. 精确值 VS 全文

  • Elasticsearch 中的数据可以概括的分为两类:精确值全文

  • 精确值 如它们听起来那样精确。例如日期或者用户 ID

  • 但字符串也可以表示精确值,例如用户名或邮箱地址。对于精确值来讲,Foo 和 foo 是不同的,2014 和 2014-09-15 也是不同的。

  • 全文 是指文本数据(通常以人类容易识别的语言书写),例如一个推文的内容或一封邮件的内容


  • 精确值很容易查询。结果是二进制的:要么匹配查询,要么不匹配。这种查询很容易用 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 应该返回关于猎狐的故事。

6.2. 倒排索引

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

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

  1. The quick brown fox jumped over the lazy dog
  2. 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 ,这样两个文档都会匹配!

6.3. 分析与分析器

分析 包含下面的过程:

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

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

  • 字符过滤器

    首先,字符串按顺序通过每个 字符过滤器 。他们的任务是在分词前整理字符串。一个字符过滤器可以用来去掉HTML,或者将 & 转化成 and

  • 分词器

    其次,字符串被 分词器 分为单个的词条。一个简单的分词器遇到空格和标点的时候,可能会将文本拆分成词条。

  • Token 过滤器

    最后,词条按顺序通过每个 token 过滤器 。这个过程可能会改变词条(例如,小写化 Quick ),删除词条(例如, 像 a, and, the 等无用词),或者增加词条(例如,像 jump 和 leap 这种同义词)。

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

6.3.1. 内置分析器

  • 词条:
"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 已经变为词根格式

6.3.2. 什么时候使用分析器

  • 当查询的时候,需要进行一次相同的分析过程,保证我们搜索的词语和索引种的词条格式一致

  • date 域包含一个精确值:单独的词条 2014-09-15

  • _all 域是一个全文域,所以分词进程将日期转化为三个词条: 2014, 09, 和 15

当我们在 _all 域查询 2014,它匹配所有的12条推文,因为它们都含有 2014 :

GET /_search?q=2014              # 12 results

当我们在 all 域查询 2014-09-15,它首先分析查询字符串,产生匹配 2014, 09, 或 15 中 _任意 词条的查询。这也会匹配所有12条推文,因为它们都含有 2014 :

GET /_search?q=2014-09-15        # 12 results !

当我们在 date 域查询 2014-09-15,它寻找 精确 日期,只找到一个推文:

GET /_search?q=date:2014-09-15   # 1  result

当我们在 date 域查询 2014,它找不到任何文档,因为没有文档含有这个精确日志:

GET /_search?q=date:2014         # 0  results !

6.3.3. 测试分析器

有些时候很难理解分词的过程和实际被存储到索引中的词条,特别是你刚接触Elasticsearch。为了理解发生了什么,你可以使用 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
      }
   ]
}

6.3.4. 指定分析器

  • 当Elasticsearch在你的文档中检测到一个新的字符串域,它会自动设置其为一个全文 字符串 域,使用 标准 分析器对它进行分析。

6.4. 映射

  • 为了能够将时间域视为时间,数字域视为数字,字符串域视为全文或精确值字符串, Elasticsearch 需要知道每个域中数据的类型。这个信息包含在映射中。 Elasticsearch 支持如下简单域类型:

6.4.1. 核心简单域类型

  • 字符串: string
  • 整数 : byteshortintegerlong
  • 浮点数: floatdouble
  • 布尔型: boolean
  • 日期: date

当你索引一个包含新域的文档—​之前未曾出现-- Elasticsearch 会使用 动态映射 ,通过JSON中基本数据类型,尝试猜测域类型,使用如下规则:

JSON type域 type
布尔型: true 或者 falseboolean
整数: 123long
浮点数: 123.45double
字符串,有效日期: 2014-09-15date
字符串: foo barstring

6.4.2. 查看映射

6.4.3. 自定义域映射

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

  • 全文字符串域和精确值字符串域的区别
  • 使用特定语言分析器
  • 优化域以适应部分匹配
  • 指定自定义数据格式
  • 还有更多

域最重要的属性是 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"
    }
}

analyzer

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