ElasticSearch--映射和分析

Posted 準提童子

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ElasticSearch--映射和分析相关的知识,希望对你有一定的参考价值。

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

分析analysis机制用于进行全文文本Full Text的分词,以建立供搜索用的倒排索引Inverted index.

上边做个总结,后续学习之后就明白了.先看一个查询现象:

在索引中有12个tweets,只有一个包含日期2014-09-15,但是我们看看下面查询中的total hits。

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  个结果 !
相互比较后会发现,为什么查询_all字段与查询date字段得到的结果不同呢?

是因为数据在_all字段和在date字段中的索引方式不同导致的.

1. 映射

现在通过_mpping查看文档的结构是如何被映射的:

GET /website/_mapping/blog
这个请求可以查看类型blog中的文档是被映射成为什么样的结构:

{
  "website": {
    "mappings": {
      "blog": {
        "properties": {
          "date": {
            "type": "date",
            "format": "yyyy/MM/dd HH:mm:ss||yyyy/MM/dd||epoch_millis"
          },
          "tags": {
            "type": "string"
          },
          "text": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "views": {
            "type": "long"
          }
        }
      }
    }
  }
}
可以看出,blog类型中的文档结构,每个字段都有其特定类型;

ES为对字段的数据类型进行猜测,动态生成了字段与数据类型的映射关系._all因为是默认字段,所以没有在此处显示,它是string类型.

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

每一种数据类型的字段会以不同的索引方式进行索引,确切值全文文本的索引方式存在很大的区别,这是区分搜索引擎和其他数据库的根本差异.

确切值的查询其实就是精确匹配,就像传统数据库中:SELECT * FROM table WHERE name="yuchen"  ,结果就是要么匹配要么不匹配.

而对于全文文本来说,并不是匹配与否二分这么简单,它是考虑的匹配程度如何.


例如下列需要,都是全文文本查询需要考虑的:

  • 一个针对"UK"的查询将返回涉及"United Kingdom"的文档

  • 一个针对"jump"的查询同时能够匹配"jumped""jumps""jumping"甚至"leap"

  • "johnny walker"也能匹配"Johnnie Walker""johnnie depp""Johnny Depp"

  • "fox news hunting"能返回有关hunting on Fox News的故事,而"fox hunting news"也能返回关于fox hunting的新闻故事。

为了满足上述需求,ES首先对文本进行分析,然后使用结果建立一个倒排索引

2. 倒排索引

ES使用一种倒排索引的结构来做快速的全文搜索.倒排索引由文本中出现的唯一的单词列表,和每个单词所在文本中的位置组成.

例如有两个文档,每个文档包含content字段的内容:

  1. The quick brown fox jumped over the lazy dog
  2. Quick brown foxes leap over lazy dogs in summer
为了创建倒排索引,先将每个文本切分为单独的单词term/token,把所有唯一词放入列表并排序:

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
查询结果是两个文档都匹配,但是第一个文档匹配了两个单词,第二个文档匹配了一个.所以就相关性来说,第一个文档相关性更高,排序在前.

但是,查询还有另外一些问题:

   1.  "Quick"和"quick"被认为是不同的单词,但是用户可能认为它们是相同的。(大小写转换)

   2. "fox"和"foxes"很相似,就像"dog"和"dogs"——它们都是同根词。(单复数转换)

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

为了解决这一问题,可以将单词标准化,同一为标准格式:

  1. "Quick"可以转为小写成为"quick"
  2. "foxes"可以被转为根形式"fox"。同理"dogs"可以被转为"dog"
  3. "jumped""leap"同义就可以只索引为单个词"jump"
这样标准化的过程称为分析analysis.

索引文档和请求字符串都要标准化到统一的格式,才能进行全文文本查询.

3. 分析

分析是这样一个过程:

首先,标记一个文本块为适用于倒排索引的单独的词terms;

然后,标准化这些词为标准形式,提高它们的可搜索性.

这个工作是由分析器analyzer完成的,一个分析器只是一个包,把三个功能放到一个包里:

字符过滤器Character filters

首先字符串经过字符过滤器,它们的工作是在标准化前处理字符串.字符过滤器能够去除html标记,或者转&为and

分词器Tokenizer

下一步,字符串被分词器标记为独立的词,一个简单的分词器可以根据空格或逗号将单词分开

标记过滤器Token Filter

每个词都依次通过标记过滤器,它可以修改词(将Quick转为小写),去掉词(停用词a and the),增加词(同义词 jump或leap)

ES提供很多开箱即用的字符过滤器,分词器,标记过滤器,可以组合以创建自定义的分析器,以应对不同的需求.

内置分析器

ES附带了一些预装的分析器,可以直接使用它们,下边列几个重要的分析器,用这个字符串来表现分析后的差异:

"Set the shape to semi-transparent by calling set_trans(5)"
标准分析器Standard analyzer

标准分析器是ES默认的分析器,对于分本分析,它对任何语言都适用.他跟据Unicode Consortium定义的单词边界word boundaries来切分文本,然后去掉大部分标点符号,最后把所有词转为小写.使用标准分析器的结果:

set, the, shape, to, semi, transparent, by, calling, set_trans, 5
简单分析器Simple analyzer

简单分析器将非单个字母的文本切分,然后把每个词转为小写.

set, the, shape, to, semi, transparent, by, calling, set, trans
空格分析器Whitespace analyzer

空格分析器依据空格切分文本,它不转换为小写

Set, the, shape, to, semi-transparent, by, calling, set_trans(5)
语言分析器Language analyzer

特定的语言分析器适用于很多语言,它们能够考虑到特定语言的特性,例如english analyzer自带一套停用词集,它可以在分析时去除它们,它还可以将英语单词的转为词根(term English words),因为它理解英语语法规则.

set, shape, semi, transpar, call, set_tran, 5

注意transparent,calling,set_trans被准为词根形式

查询字符串的处理

当我们索引一个文档,全文字段_all会被分析为单独的词来创建倒排索引,不过在全文字段搜索时,我们要让查询字符串经过同样的分析流程处理,以确保这些词在索引中存在.

查询全文字段,会对查询字符串进行分析流程处理;查询确切值字段,不对查询字符串进行分析处理,但是可以自己指定.

测试分析器

可以使用analyze API查看文本是如何被分析的,在查询字符串中指定要使用的分析器,被分析的文本作为请求体:

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

注意,GET请求与请求体之间不能有空格,响应:

{
   "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的值<ALPHANUM>会随着分析器变化,可先忽略.

analyze API对于理解ES索引的内在细节是很有用的工具,后边会继续谈论.

指定分析器:

当ES在文档中探测到新的字段时,将自动配置它为全文字符串full-text string字段,并用standard分析器分析.

你并不总是想这样做,你或许需要一个适合你的语言的分析器,有时候你想将string字段作为确切值匹配,不需要分析,例如ID,内部状态字段,标签等

为了达到这些,我们需要通过指定映射手动配置这些字段.

4. 自定义映射

为了能够把日期字段处理成日期,数字字段处理成数字,字符串字段处理成全文本或者确切值,ES需要知道每个字段包含了什么类型,这些类型和字段信息存储在映射中.

索引中每个文档都有一个类型type,每个类型拥有自己的映射mapping或模式定义schema definition.一个映射定义了字段类型,包括字段的数据类型,以及字段被ES处理的方式.

映射还用于设置关联到类型上的元数据.

ES支持以下简单字段类型:

类型 表示的数据类型
String string
Whole number byte, short, integer, long
Floating point float, double
Boolean boolean
Date date
当索引一个包含新字段的文档,ES使用动态映射猜测字段类型,根据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类型.

可以使用mapping API查看一个类型的映射,

GET /website/_mapping/blog
响应见第一节

错误的映射,例如把age映射为string,会造成查询结果混乱.要检查映射的数据类型,而不是假设映射是正确的.

自定义字段映射

尽管基本的字段数据类型已经能够满足很多需求,你需要为一些个别字段定义映射,尤其是string字段.

自定义映射可以是你做如下:

1.区分全文string字段和确切值string字段

2.使用特定语言分析器

3.优化一个字段,用作部分匹配

4.指定自定义的数据类型

5.以及更多

一个字段最重要的属性是type,对于除string字段外的大部分字段,除了type,你很少做其他的映射map.

{
    "number_of_clicks": {
        "type": "integer"
    }
}
因为对于string字段,需要考虑全文文本,它们的值在被索引前会先经过分析器,一个全文查询,查询字符串也需要经过分析器.

对于string字段,两个重要的属性是indexanalyzer.

index

index属性控制字符串以何种方式被索引,有三种取值:

解释
analyzed 首先分析这个字符串,然后索引。换言之,以全文形式索引此字段。
not_analyzed 索引这个字段,使之可以被搜索,但是精确索引指定值。不分析此字段。
no 不索引这个字段。这个字段不能为搜索到。
string字段默认index属性值是analyzed,如果我们映射为确切值,需要设置为not_analyzed

{
    "tag": {
        "type":     "string",
        "index":    "not_analyzed"
    }
}
其他的简单类型,如long,data等也接受index参数,但是其值只能是not_analyzed和no,因为它们的值不能被分析.

analyzer

对于analyzed的string字段,可以通过analyzer参数指定使用哪种分析器,在搜索和索引的时候都会使用.默认的,ES使用standard分析器,但可以指定一个内置的分析器来更改它,例如whitesapce,simple和english

{
    "tweet": {
        "type":     "string",
        "analyzer": "english"
    }
}
在自定义分析器章节,会详细的讲解如何定义以及使用自定义分析器.

更新映射

可以在第一次创建索引时,为一个类型指定映射,也可以在后边为一个新类型添加映射,或者为已存在的类型更新映射.

注意,尽管可以增加一个字段映射,但是不能改变一个已经存在的字段映射.如果一个字段的映射已经存在,那个字段的数据很可能已经被索引,如果改变字段的映射,那么被索引的数据就会出现错误,不能被正确的搜索.

我们可以更新映射,来增加一个字段,但是不能改变一个已经存在的字段从analyzd转为not_analyzed

演示两种指定映射的方式:

创建新的索引,并指定tweet字段使用english分析器:

PUT /gb 
{
  "mappings": {
    "tweet" : {
      "properties" : {
        "tweet" : {
          "type" :    "string",
          "analyzer": "english"
        },
        "date" : {
          "type" :   "date"
        },
        "name" : {
          "type" :   "string"
        },
        "user_id" : {
          "type" :   "long"
        }
      }
    }
  }
}
创建索引,在请求体中定义了映射.响应:

{
  "acknowledged": true
}

接下来,我们加一个新的not_analyzed文本字段tag在类型tweet的映射上.使用_maping后缀

PUT /gb/_mapping/tweet
{
  "properties" : {
    "tag" : {
      "type" :    "string",
      "index":    "not_analyzed"
    }
  }
}
注意到我们不需要再列出已经存在的字段,因为我们不能再改变它们,新的字段已经被加入到存在的映射中.

使用GET /gb/_mapping/tweet获取看:

{
  "gb": {
    "mappings": {
      "tweet": {
        "properties": {
          "date": {
            "type": "date",
            "format": "strict_date_optional_time||epoch_millis"
          },
          "name": {
            "type": "string"
          },
          "tag": {
            "type": "string",
            "index": "not_analyzed"
          },
          "tweet": {
            "type": "string",
            "analyzer": "english"
          },
          "user_id": {
            "type": "long"
          }
        }
      }
    }
  }
}

基于tweet类型现在的映射,可以通过analyze API测试字符串字段tweet和tag的映射不同,对比两个请求:

GET /gb/_analyze?field=tweet&text=Black-cats
响应:

{
  "tokens": [
    {
      "token": "black",
      "start_offset": 0,
      "end_offset": 5,
      "type": "<ALPHANUM>",
      "position": 0
    },
    {
      "token": "cat",
      "start_offset": 6,
      "end_offset": 10,
      "type": "<ALPHANUM>",
      "position": 1
    }
  ]
}
GET /gb/_analyze?field=tag&text=Black-cats
响应:

{
  "tokens": [
    {
      "token": "Black-cats",
      "start_offset": 0,
      "end_offset": 10,
      "type": "word",
      "position": 0
    }
  ]
}
tag字段是不能分析的,tweet字段是english分析器分析的.

可是这里有个疑问,当使用如下两个请求时,只是和上边相比,文本写到了请求体中,结果就不一样呢?

对tweet字段的请求是和上边一样的:

GET /gb/_analyze?field=tweet
{
  "text": "Black-cats" 
}
但是对tag的请求就不一样了:

GET /gb/_analyze?filed=tag
{
  "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
    }
  ]
}
不知为什么,什么原因呢?导致分析了,而且是cats

5. 复杂核心字段类型

除了前边提到的简单类型,JSON还有null,数组,对象,这些数据类型ES也都支持.

多值字段multivalue field

很有可能,我们希望tag字段包含不止一个标签,我们可以索引一个标签数组,来代替单一字符串.

{ "tag": [ "search", "nosql" ]}
对于数组,没有特殊的映射要求,任何字段可以包含零个,一个,或多个值,像全文文本一样可以被分析为多个词.

一个数组的所有值必须有相同的数据类型,如果通过索引数组创建一个新的字段,ES将使用数组的第一个值的数据类型决定新字段的type属性.


当从ES中获取一个文档,所有数组的顺序与索引文档时的一致,_source字段精确包括了索引的JSON文档.

然而数组被索引为多值字段类型,用于被查询,它们是无序的,在搜索的时候不能指定"第一个元素","最后一个元素"等.而是把数组看为一个值集合(bag of value).

空字段empty field

数组可以是为空的,这与有零个值是等价的,事实上在lucene中没有存储null的方式,所以一个存有null的字段被认为是空字段.

这三个字段都被认为是空字段,不被索引.

"null_value":               null,
"empty_array":              [],
"array_with_null_value":    [ null ]
多层对象multilevel objects

最后讨论的自然的JSON数据类型是对象object.

内置对象Inner Objects经常被嵌入到一个实体,或者另外一个对象中,比如,我们可以不使用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可以动态的发现新的对象字段,并且映射它们的类型为object,并在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" }
              }
            }
          }
        }
      }
    }
  }
}
在实际操作时,没有返回对象字段的type属性,不知原因:

{
  "gb": {
    "mappings": {
      "tw": {
        "properties": {
          "tweet": {
            "type": "string",
            "analyzer": "english"
          },
          "user": {
            "properties": {
              "age": {
                "type": "long"
              },
              "gender": {
                "type": "string"
              },
              "id": {
                "type": "string"
              },
              "name": {
                "properties": {
                  "first": {
                    "type": "string"
                  },
                  "full": {
                    "type": "string"
                  },
                  "last": {
                    "type": "string"
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}

内部对象是怎样被索引的:

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]
}
内置字段与名字相关,区分两个字段中相同的名字,可以使用全路径,例如user.name.first,甚至类型的名字加在前边:tweet.user.name.first

内置对象数组

最后考虑一个包含内置对象的数组被索引,例如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}与{Mary White}的相关性已经丢失,因为每个多值字段只是一个数值集,不是排序的数组.
对于问题:"Is there a follower who is 26 years old?"

我们不能得到精确的回答.

关联Correlated内置数组有能力回答这样的问题,我们称之为嵌套对象nested objects,后续会学习到.

以上是关于ElasticSearch--映射和分析的主要内容,如果未能解决你的问题,请参考以下文章

ElasticSearch权威指南学习(映射和分析)

小烨收藏ElasticSearch权威指南-映射和分析

elasticSearch-mappings(映射,分析)

python 接受带有字段名称,类型和分析器的YAML文件,并生成Elasticsearch JSON映射文件

SpringBoot整合Elasticsearch之索引,映射,文档,搜索的基本操作案例分析

SpringBoot整合Elasticsearch之索引,映射,文档,搜索的基本操作案例分析