更多优质内容
请关注公众号

  Elasticsearch入门基础系列(四) ES如何定义字段类型、Mapping映射 和 分词器介绍-阿沛IT博客

正文内容

Elasticsearch入门基础系列(四) ES如何定义字段类型、Mapping映射 和 分词器介绍

栏目:数据库 系列:Elasticsearch 发布时间:2024-08-03 16:20 浏览量:505

官方文档:

https://www.elastic.co/guide/en/elasticsearch/reference/7.17/explicit-mapping.html


一、Mapping是什么

mapping是一个type下的文档的元数据,相当于mysql中的字段属性和数据类型。

一个mapping包括元字段(meta-fields)和用户字段(fields或properties)。

前者包括如_index/_type/_id/_source等字段。后者是用户自定义的字段。


可以通过一下语句查看一个type下的mapping。


注意:ES 7将 type 的概念给移除了或者说ES 7中一个索引只能有一个type,因此type对用户来说事透明的,因此我们可以认为 Mapping 就是一份描述一个文档的元数据。


ES5的写法

GET /index名/_mapping/type名


ES7的写法

GET /index名/_mapping
{
    "zbp_index": {
        "mappings": {
            "properties": {
                "desc": {
                    "type": "text",
                    "fields": {
                        "keyword": {
                            "type": "keyword",
                            "ignore_above": 256
                        }
                    }
                },
                "description": {
                    "type": "text",
                    "fields": {
                        "keyword": {
                            "type": "keyword",
                            "ignore_above": 256
                        }
                    }
                },
                "name": {
                    "type": "text",
                    "fields": {
                        "keyword": {
                            "type": "keyword",
                            "ignore_above": 256
                        }
                    }
                },
                "price": {
                    "type": "long"
                },
                "producer": {
                    "type": "text",
                    "fields": {
                        "keyword": {
                            "type": "keyword",
                            "ignore_above": 256
                        }
                    }
                },
                "tags": {
                    "type": "text",
                    "fields": {
                        "keyword": {
                            "type": "keyword",
                            "ignore_above": 256
                        }
                    }
                }
            }
        }
    }
}


ES7的一个索引最多只能有一个type,因此查看index 的 _mapping就是查看index的type的_mapping。


查看某个字段的mapping

GET /my-index/_mapping/field/employee-id    // 查看employee-id字段的类型




二、动态映射 和 静态映射

在不手动创建index的情况下直接插入一个文档会自动创建index,同时自动创建该index的mapping,这种mapping方式称为动态映射(dynamic mapping)。


如果手动创建index,并带上自定义的mapping,这种方式称为静态映射或显式映射(explicit mapping)。

PUT /my-index    // 创建my-index这个映射,并且指定mappings
{
  "mappings": {
    "properties": {
      "age":    { "type": "integer" },  
      "email":  { "type": "keyword"  }, 
      "name":   { "type": "text"  }     
    }
  }
}


可以使用 PUT /index/_mapping 接口为一个已存在的index添加某些字段的映射。

PUT /my-index/_mapping
{
  "properties": {
    "employee-id": {
      "type": "keyword",
      "index": false    // index为false表示该字段不会假如倒排索引,搜索时也不能根据employee-字段进行搜索
    }
  }
}


不能对已经存在的字段更改其字段类型,也不能更改它的字段名。因为不同的字段类型决定了写入和搜索时对该字段使用的存储和分词处理方式,中途更改字段类型会导致更改前的文档和更改后的新文档的写入和分词行为不一致,从而导致搜索结果不正确。


如果实在想要更改一个字段的类型,需要创建一个新index,重新在该index中指定所有原有字段的mapping,并将原index的文档写入到新index。


如果实在想更改一个字段的字段名,可以创建一个类型为 alias 的字段,字段名为新字段名,并将其指向原字段。这样的话,旧字段名和新字段名可以同时生效。


mapping中不同的字段类型决定了搜索时,对该字段使用的搜索方式。ES的搜索方式有两种:精确匹配(exact value)和全文检索(full text)。


对于一个类型是date的create_time字段,ES会在写入阶段将字段值的整体(不分词)作为key存到倒排索引中。当查询 create_time=2022-06-05时,会采用精确匹配到倒排索引检索,必须字段值和搜索值完全一致才能匹配成功。


对于一个类型为text的name字段,ES会在写入阶段将字段值的多个分词作为key存到倒排索引中。当查询 name=robbin bob 时,则会对搜索值进行分词,并一一到倒排索引匹配。



三、字段类型


简单字段类型包括 text、keyword、date、long、double、boole或者ip。复杂字段类型包括 object 或 nested类型。此外还有一些特殊字段类型如geo_point, geo_shape, 或 completion。


下面介绍几种重要的字段类型:


1、text类型

text类型会用于全文检索,需要经过分词器处理,text类型不能用于排序且不适合用于聚合。

PUT my_index
{
  "mappings": {
    "properties": {
      "full_name": {
        "type":  "text"
      }
    }
  }
}


2、keyword类型

keyword类型仅用于过滤、排序或聚合,对keyword类型的字段进行搜索时,会走精确匹配,而非全文检索,因此keyword类型字段存储时会将整个字段值放入倒排索引,而非对其分词放入倒排索引。

PUT my_index
{
  "mappings": {
    "properties": {
      "tags": {
        "type":  "keyword"
      }
    }
  }
}


一般来说,我们可能会对一个字段指定多种字段类型(multi-fields)。例如对于一个string类型的字段,我们希望对其指定text类型用于全文检索,也希望对其指定keyword类型用于排序和聚合。此时可以这样做:

PUT my_index
{
  "mappings": {
    "properties": {
      "city": {
        "type": "text",    // 指定text类型
        "fields": {        // 使用fields参数可以指定第二个类型
          "raw": {         // 新类型的别名是city.raw
            "type":  "keyword"  // 第二类型是keyword
          }
        }
      }
    }
  }
}

PUT my_index/_doc/1
{
  "city": "New York"
}

PUT my_index/_doc/2
{
  "city": "York"
}

GET my_index/_search
{
  "query": {
    "match": {
      "city": "york"     // 使用city字段进行全文检索
    }
  },
  "sort": {
    "city.raw": "asc"   // 使用city.raw进行排序和聚合
  },
  "aggs": {
    "Cities": {
      "terms": {
        "field": "city.raw" 
      }
    }
  }
}


keyword的其他变种:constant_keyword 和 wildcard。

keyword、constant_keyword 和 wildcard都是不分词就存入到倒排索引的,仅用于排序、聚合和term查询(精确匹配查询)。

keywrod适合存一些结构化的内容,例如Id、email、IP地址、状态码、标签之类的。

constant_keyword只用来存内容完全相同的字段。

例如:

PUT logs-debug
{
  "mappings": {
    "properties": {
      "@timestamp": {
        "type": "date"
      },
      "message": {
        "type": "text"
      },
      "level": {
        "type": "constant_keyword",
        "value": "debug"        // level字段的值就只能是
      }
    }
  }
}
POST logs-debug/_doc
{
  "date": "2019-12-12",
  "message": "Starting up Elasticsearch",
  "level": "debug"
}

POST logs-debug/_doc
{   // 会为level默认填充debug值
  "date": "2019-12-12",
  "message": "Starting up Elasticsearch"
}

wildcard类型专门用来存储无结构的大段文本,并支持使用正则表达式查询,例如一段邮件内容,一段http请求报文。

如果存储一个内容很多的文本请用wildcard,否则请用text,后者查询更快、插入更快、占用空间更少。

PUT my-index-000001
{
  "mappings": {
    "properties": {
      "my_wildcard": {
        "type": "wildcard"
      }
    }
  }
}

PUT my-index-000001/_doc/1
{
  "my_wildcard" : "This string can be quite lengthy"
}

GET my-index-000001/_search
{
  "query": {
    "wildcard": {
      "my_wildcard": {
        "value": "*quite*lengthy" // 使用正则表达式查询
      }
    }
  }
}


3、date类型

date类型可以是以下3种形式:

2015-01-01 或 2015/01/01 12:10:30;

秒为单位的整型;

毫秒为单位的长整型;


无论是哪种形式,date类型都会被转为一个毫秒为单位的长整型存储在ES底层,然而呈现出来的格式一直都会是字符串格式(比如返回给客户端时)。

PUT my_index
{
  "mappings": {
    "properties": {
      "date": {
        "type":   "date",
        "format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis"    // 这里同时指定了3种格式
      }
    }
  }
}


4、boolean类型

Boolean类型不仅接受 true 或 false,还能接受字符串 "true"和"false"。

false值:false, "false", "";

true值:true, "true"


5、binary类型

binary类型接受一个经过base64编码的binary数据(经过base64编码后会变成一个字符串)。该类型的字段不放入倒排索引也不能搜索。base64编码的binary值不能嵌入换行符\n。

PUT my_index
{
  "mappings": {
    "properties": {
      "name": {
        "type": "text"
      },
      "blob": {
        "type": "binary"
      }
    }
  }
}

PUT my_index/_doc/1
{
  "name": "Some binary blob",
  "blob": "U29tZSBiaW5hcnkgYmxvYg==" 
}


6、range类型

range类型是一个范围类型,包括:integer_range、float_range、long_range、double_range、date_range和ip_range。


下面是一个官方例子:

PUT range_index
{
  "settings": {
    "number_of_shards": 2
  },
  "mappings": {
    "properties": {
      "expected_attendees": {
        "type": "integer_range"
      },
      "time_frame": {
        "type": "date_range", 
        "format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis"
      }
    }
  }
}

PUT range_index/_doc/1?refresh
{
  "expected_attendees" : { 
    "gte" : 10,
    "lte" : 20
  },
  "time_frame" : { 
    "gte" : "2015-10-31 12:00:00", 
    "lte" : "2015-11-01"
  }
}


查询示例:

GET range_index/_search
{
  "query" : {
    "term" : {
      "expected_attendees" : {
        "value": 12
      }
    }
  }
}

结果
{
  "took": 13,
  "timed_out": false,
  "_shards" : {
    "total": 2,
    "successful": 2,
    "skipped" : 0,
    "failed": 0
  },
  "hits" : {
    "total" : {
        "value": 1,
        "relation": "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "range_index",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 1.0,
        "_source" : {
          "expected_attendees" : {
            "gte" : 10, "lte" : 20
          },
          "time_frame" : {
            "gte" : "2015-10-31 12:00:00", "lte" : "2015-11-01"
          }
        }
      }
    ]
  }
}
GET range_index/_search
{
  "query" : {
    "range" : {
      "time_frame" : { 
        "gte" : "2015-10-31",
        "lte" : "2015-11-01",
        "relation" : "within"     // relation支持WITHIN, CONTAINS, INTERSECTS。
      }
    }
  }
}
结果
{
  "took": 13,
  "timed_out": false,
  "_shards" : {
    "total": 2,
    "successful": 2,
    "skipped" : 0,
    "failed": 0
  },
  "hits" : {
    "total" : {
        "value": 1,
        "relation": "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "range_index",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 1.0,
        "_source" : {
          "expected_attendees" : {
            "gte" : 10, "lte" : 20
          },
          "time_frame" : {
            "gte" : "2015-10-31 12:00:00", "lte" : "2015-11-01"
          }
        }
      }
    ]
  }
}


7、Object类型

一个object类型的字段可以包含内部object,内部object还可以继续包含内部object。

PUT my_index/_doc/1
{ 
  "region": "US",
  "manager": { 
    "age":     30,
    "name": { 
      "first": "John",
      "last":  "Smith"
    }
  }
}

一个object类型在ES底层会被存储为一个简单的、平展的key-value键值对列表,上面的文档会在ES底层会被存储为类似如下形式:

{
  "region":             "US",
  "manager.age":        30,
  "manager.name.first": "John",
  "manager.name.last":  "Smith"
}


而该文档的mapping如下:

PUT my_index
{
  "mappings": {
    "properties": { 
      "region": {
        "type": "keyword"
      },
      "manager": { 
        "properties": {
          "age":  { "type": "integer" },
          "name": { 
            "properties": {
              "first": { "type": "text" },
              "last":  { "type": "text" }
            }
          }
        }
      }
    }
  }
}


8、nested类型

nested类型是一种特殊的object类型,准确的来说应该是数组包着多个object类型。但是需要显式的声明某个字段的类型为nested,否则使用dynamic mapping的话,ES仍然会将object数组作为object类型解析。


举个例子:

PUT my_index/_doc/1
{
  "group" : "fans",
  "user" : [ 
    {
      "first" : "John",
      "last" :  "Smith"
    },
    {
      "first" : "Alice",
      "last" :  "White"
    }
  ]
}


上面这个例子,假设没有显式的对user字段设置为nested类型,那么在ES底层,user字段会被作为object类型进行存储,具体的行为是将user从行式存储转为列式存储的key-value键值对列表,上面的文档在ES底层会被存储为类似如下形式,user.first和user.last会被展平为多值字段:

{
  "group" :        "fans",
  "user.first" : [ "alice", "john" ],
  "user.last" :  [ "smith", "white" ]
}


按照这种方式存储会存在一个问题,即一个文档的user数组中,每条object内部字段的联系会被丢失,例如本例中,alice和white的关联关系会被丢失,因此假设我想查询user.first=Alice而且user.last=Smith,按理说是不可能查询到的,然而结果却返回了id=1的文档。原因就是ES底层如果按照object类型存储,会将该文档的user数组中所有object的user.first汇聚到一个数组,user.last也汇聚到一个数组(如上面代码所示),现在user.first=Alice而且user.last=Smith就相当于是按 Alice in user.first 而且 Smith in user.last,当然会匹配成功。

POST my_index/_search
{
  "query": {
    "bool": {
      "must": [
        { "match": { "user.first": "Alice" }},
        { "match": { "user.last":  "Smith" }}
      ]
    }
  }
}

结果
{
  "took": 2,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 1,
      "relation": "eq"
    },
    "max_score": 0.5753642,
    "hits": [
      {
        "_index": "es_example",
        "_type": "_doc",
        "_id": "1",
        "_score": 0.5753642,
        "_source": {
          "group": "fans",
          "user": [
            {
              "first": "John",
              "last": "Smith"
            },
            {
              "first": "Alice",
              "last": "White"
            }
          ]
        }
      }
    ]
  }
}


这种情况下,我们就需要使用nested类型而非object类型。

和object类型不同,ES底层会将nested类型的数组中每条object作为一个独立的隐式文档来存储,如此一来就能保证该object内每个字段之间的关联性。

如下所示:

PUT my_index
{
  "mappings": {
    "properties": {
      "user": {
        "type": "nested"
      }
    }
  }
}

PUT my_index/_doc/1
{
  "group" : "fans",
  "user" : [
    {
      "first" : "John",
      "last" :  "Smith"
    },
    {
      "first" : "Alice",
      "last" :  "White"
    }
  ]
}

GET my_index/_search
{
  "query": {
    "nested": {    // 指明要以nested类型的方式来查询
      "path": "user",     // 你要查询的nested对象路径
      "query": {  // query是你希望在path这个nested对象执行的查询
        "bool": {
          "must": [
            { "match": { "user.first": "Alice" }},
            { "match": { "user.last":  "Smith" }} 
          ]
        }
      }
    }
  }
}

GET my_index/_search
{
  "query": {
    "nested": {
      "path": "user",
      "query": {
        "bool": {
          "must": [
            { "match": { "user.first": "Alice" }},
            { "match": { "user.last":  "White" }} 
          ]
        }
      },
      "inner_hits": { 
        "highlight": {
          "fields": {
            "user.first": {}
          }
        }
      }
    }
  }
}

第一个查询返回值为空,第二个查询返回id=1的文档。更多有关nested类型的查询语法,可以参考:

https://www.elastic.co/guide/en/elasticsearch/reference/7.4/query-dsl-nested-query.html


关于字段类型的注意点:

一个index如果定义过多的字段可能会引起内存爆炸,例如使用动态mapping时,如果每次添加一个文档都会带来一个或一些新的字段,这些字段都会被记录到index 的 mapping中,严重的话会导致内存溢出。为了避免这种情况,可以使用如下参数做限制。

index.mapping.total_fields.limit    // 限制一个index的总字段数,默认值是1000
index.mapping.depth.limit        // 限制一个字段的最大深度,默认20
index.mapping.nested_fields.limit    // 一个索引允许出现的nested类型字段的最多个数,默认50
index.mapping.nested_objects.limit    // 一个文档中nested类型字段数组下的object的最多个数,默认10000


9、alias类型

如果希望一个字段有多个字段名,可以使用alias类型。

PUT trips
{
  "mappings": {
    "properties": {
      "distance": {
        "type": "long"
      },
      "route_length_miles": {
        "type": "alias",
        "path": "distance" 
      },
      "transit_mode": {
        "type": "keyword"
      }
    }
  }
}

GET _search
{
  "query": {
    "range" : {
      "route_length_miles" : {
        "gte" : 39
      }
    }
  }
}

alias一般用在用户想改字段名的场景。

alias类型字段的目标必须是一个已存在的具体字段,而且如果目标是一个nested内的字段,则alias字段的层级必须要与目标字段层级相同。

alias类型字段只是一个字段别名,它指向一个已有字段,但本身不会在ES底层多存储一份,因此如果想用_source属性指定只返回alias类型字段时会失败(一个字段都不返回)。

GET /_search
{
  "query" : {
    "match_all": {}
  },
  "_source": "route_length_miles"    // 失败,一个字段都不返回
}


10、Arrays类型

在ES中,array类型不需要一个专门的属性值去指定(Object类型其实也是),任何字段都可以包含0到多个值(即任何字段都可以是array类型,一个单值字符串类型的字段 和 一个字符串数组类型的字段,ES都会看成是字符串类型的字段,不区分它是不是数组)。前提是数组中元素的数据类型必须一致。

例如:

// 指定"订单编号"这个字段为text类型
{
    "properties":{
        "order_no":{
            "type":"text",
            "analyzer":"standard"
        }    
    }
}

// 我可以往这个order_no字段插入数组类型的值
POST /order/1
{
    "order_no":["No_1000","No_1001"]
}


四、字段操作


1. 往原有Index中添加新字段

往原有Index中添加新字段可以不用新建Index,直接用_mapping方法更改原Index的mapping即可。例如下面的例子就往“my-index-000001”这个Index中添加了一个“employee-id”字段,类型为keyword,index为false表示该字段不会建立倒排索引,不能用于搜索。

PUT /my-index-000001/_mapping { "properties": { "employee-id": { "type": "keyword", "index": false } } }


2. 更改Index字段的Mapping参数(包括为一个字段新增分词器、过滤器等)

官网中列出的以下Mapping参数可以通过 _mapping API更改。

https://www.elastic.co/guide/en/elasticsearch/reference/7.17/mapping-params.html


初次之外字段的其他的mapping参数更改以及字段类型更改都不能通过用 _mapping API 对原Index进行更改来实现,只能新建Index,也就是reindex(如果对原Index的已有字段进行了类型更改和不支持的mapping参数更改,会导致已存在于Index中的旧数据由于沿用旧mapping规则而无法被搜索到,而新插入的数据可以被搜索到;对字段重命名同理,也是会导致旧数据无法被搜索到,新数据可以,因为旧数据还在旧字段名下的倒排索引中)。


更多更改mapping的参考资料:

https://blog.csdn.net/laoyang360/article/details/121528491


3. reindex 重新索引

如果我们定义好了一个索引的mapping映射,之后又想修改某个字段的字段类型,但是ES是不能允许修改的,因此已经写入索引的字段值已经按照之前定义的字段类型进行存储。此时我们就需要reindex,它可以将一个索引内的所有数据拷贝到另一个索引。 


具体操作如下:

已有索引A,A的字段x是keyword类型,但是我想修改字段x的类型为text。此时需要新增一个索引 A2,并定义A2的字段x为text类型。然后对 A 和 A2进行 reindex,A的数据就会写入到索引A2。之后我们再移除索引A,并对A2索引重命名为A,此时就完成了对字段x的字段类型变更。


reindex是默认是同步API,如果数据量太大,http响应会等待很久。可以设置为异步执行。 


我在reindex的时候遇到一个报错:

circuit_breaking_exception

原因是每批reindex的数量太大,而我的机器内存比较少,所以内存不足。ES默认每批1000个文档进行reindex。我将它改为了500就好了。 

 

{
  "source": {
    "index": "product",
    "size":500
  },
  "dest": {
    "index": "product_copy"
  }
}

其他参考博客:

https://www.jianshu.com/p/0bd1e950353f

http://t.zoukankan.com/gmhappy-p-11864054.html



五、元数据字段


1、_source 【指定字段是否存储到原始文档】

这个参数可以用到查询语句和创建index语句中。

这里我们要了解一个document写入一个节点的时候是怎么存储的,首先该document会保留一份没有被处理过的原始文档,原始文档保存着_source规定的所有该document的字段(好吧,其实_source就是这份原始文档),然后document被分词保存到一份倒排索引,现在我们就有2份数据了。

当我们查询时,先在倒排索引找到document对应的id,再根据原始id到原始文档中找到document的_source字段返回给客户端。

我们可以在创建Index的时候(也就是创建mapping的时候)指定_source是否开启(默认是开启的),以及_source只存储哪些字段,排除哪些字段。

PUT my-index-000001
{
  "mappings": {
    "_source": {
      "enabled": false
    }
  }
}

表示这个Index的原始文档中不保存任何字段。好处是节省磁盘空间,但坏处要原因超过好处。


坏处如下:

1、_search方法只能查到id,查不到文档的其他任何用户自定义的字段。

2、update、update_by_query和reindex 这3个API不再起作用,换句话说,你原始文档的内容本来就为空,再更改document没啥用(_source为false的情况下,update之后倒排索引会更新,这意味着update之后,你仍可以通过搜索这些字段得到正确的搜索结果)。

基本上我们不会将 _source 置为false。除非你只查id。

PUT logs
{
  "mappings": {
    "_source": {
      "includes": [
        "*.count",
        "meta.*"
      ],
      "excludes": [
        "meta.description",
        "meta.other.*"
      ]
    }
  }
}

PUT logs/_doc/1
{
  "requests": {
    "count": 10,
    "foo": "bar" // X
  },
  "meta": {
    "name": "Some metric",
    "description": "Some metric description", // X
    "other": {
      "foo": "one", // X
      "baz": "two" // X
    }
  }
}

GET logs/_search
{
  "query": {
    "match": {
      "meta.other.foo": "one"  // 仍可搜到
    }
  }
}

打了X的字段,都不会保留在_source原始文档中。

关于 _source 和 store 的区别可以看看:

https://blog.csdn.net/ITWANGBOIT/article/details/104982759


2、_routing 【路由】

这个参数可以用在创建index语句中。查询和写入数据可以使用routing参数指定一个值,ES跟用这个值计算出它对应的shard分片。

routing参数的值决定了文档写入ES时,该文档会被存到哪个shard分片中。默认routing值的是该文档的_id值。

PUT my-index-000001/_doc/1?routing=user1&refresh=true 
{
  "title": "This is a document"
}

GET my-index-000001/_doc/1?routing=user1

// 同上
GET my-index-000001/_search
{
  "query": {
    "terms": {
      "_routing": [ "user1" ] 
    }
  }
}


例子中的文档写入ES时是根据 "user1" 这个值来路由的(这个值是哪个字段不重要,即使这个值不属于任何一个字段,ES也能根据这个值计算出它位于哪个shard),也就是说 routing值=user1 的文档会被放到同一个shard中。

搜索时如果指定了routing=user1,那么ES就只会在 值为user1 对应的那个shard去搜索,而不会将请求分发到所有的shard。

GET my-index-000001/_search?routing=user1,user2 
{
  "query": {
    "match": {
      "title": "document"
    }
  }
}

这个例子意思是,只在“与user1,user2相关的shard”上查找title包含“document”的文档。


下面这个例子是在创建index时要求每次写入文档时都必须指定routing值,否则会报错。

PUT my-index-000002
{
  "mappings": {
    "_routing": {
      "required": true 
    }
  }
}

// 这个PUT API没有指定routing,因此ES会报错
PUT my-index-000002/_doc/1 
{
  "text": "No routing value provided"
}

一般来说,我们会指定一个字段的值作为routing值,而不会说这次写入使用字段A的值作为routing值,下次写入使用字段B的值作为routing的值,这样数据路由是错乱的。


更多元数据字段后续待补充,具体文档在:

https://www.elastic.co/guide/en/elasticsearch/reference/7.17/mapping-fields.html


五、分词器简介


一个标准的分词器(analyzer)包含三个部分,character filters、tokenizers和token filters。


character filters (字符过滤器)可以按照需求对字符进行过滤,比较基本的功能如:将html标签去除。分词器可能有零个或多个 字符过滤器,它们按顺序应用。


tokenizers (分词器)接收字符流,将其分解为单独的 tokens(通常是单个单词),并输出 tokens 流。

例如,whitespace 分词器在看到任何空格时将文本分解为tokens。 它会将文本“Quick brown fox!”转换为terms[Quick, brown, fox!]。tokenizer分词器还负责记录每个词条的顺序或位置,以及该词条所代表的原始单词的开始和结束字符偏移量。分词器必须具有正好一个 tokenizer。


token filter (token 过滤器) 接收token流并可以添加、删除或更改token 。

例如,一个 lowercase 标记过滤器将所有标记转换为小写;一个 stop 标识过滤器从标识流中删除常用词(停用词),如 the/a/an; synonym token过滤器将同义词引入token流。

token 过滤器不允许更改每个token 的位置或字符偏移量。


分词器可能有零个或多个 token filters,它们按顺序应用。


多种类的分词器

ES中有各种各样的分词器,每种分词器的行为都是不同的。例如一个 English analyzer (英文分词器)的行为如下。

在写入阶段,English analyzer 会将一个英文句子按这种方式转为单词。

"The QUICK brown foxes jumped over the lazy dog!"

[ quick, brown, fox, jump, over, lazi, dog ]

其中包括了对单复数的转换,时态转换和同义词转换,并且去掉 the、an和a等常用词,并将这些单词写入倒排索引。


指定分词器

在创建type的mapping时,可以为字段手动指定分词器。例如,下面的例子为title这个字段指定了standard 分词器,如果没有指定分词器,则默认使用standard分词器。

PUT my_index
{
  "mappings": {
    "properties": {
      "title": {
        "type":     "text",
        "analyzer": "standard"
      }
    }
  }
}

一般来说,当用户搜索时,如果某个字段的搜索方式是全文检索而不是精确匹配,则ES会用该字段mapping中对应的分词器对搜索值进行相同的处理再检索。这样可以保证搜索词和文档字段值的处理行为是一致的,而不会因为分词方式不同导致搜索不到的情况。


搜索阶段的分词使用哪种分词器取决于以下优先级:

查询语句中指定的分词器 > mapping中search_analyzer属性指定的分词器 > mapping中analyzer属性指定的分词器 > index配置中 default_search 和 default 配置指定的分词器 > standard分词器


我们可以使用_analyze 接口手动查看某个分词器是如何对一个句子进行分词的:

POST _analyze
{
  "analyzer": "whitespace",   // 使用whitespace分词器对下面句子分词测试
  "text":     "The quick brown fox."
}

POST _analyze
{
  "tokenizer": "standard",    // 使用standard分词器对下面句子分词,并用lowercase和asciifolding过滤器进行单词过滤
  "filter":  [ "lowercase", "asciifolding" ],
  "text":      "Is this déja vu?"
}

GET my_index/_analyze     // 使用my_index索引中my_text这个字段的分词器对“Is this déjà vu?”进行分词测试
{
  "field": "my_text", 
  "text":  "Is this déjà vu?"
}


用户可以自定义一个分词器。如下所示:

PUT my_index
{
  "settings": {
    "analysis": {
      "analyzer": {
        "std_folded": { 
          "type": "custom",
          "tokenizer": "standard",
          "filter": [
            "lowercase",
            "asciifolding"
          ]
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "my_text": {
        "type": "text",
        "analyzer": "std_folded" 
      }
    }
  }
}

GET my_index/_analyze 
{
  "analyzer": "std_folded", 
  "text":     "Is this déjà vu?"
}

GET my_index/_analyze 
{
  "field": "my_text", 
  "text":  "Is this déjà vu?"
}


1、对my_index这个索引定义一个名为std_folded的分词器(type=custom表示采用自定义分词器),该分词器的 tokenizer 部分采用 standard分词器,token过滤器部分采用lowercase和asciifolding。


2、在mapping中指定my_text这个字段的类型为text类型(text类型可以采用全文检索),并使用 std_folded分词器进行分词。

如果想查看ES7的所有分词器,可以查看官方文档

https://www.elastic.co/guide/en/elasticsearch/reference/7.4/analysis-analyzers.html

 


六、Mapping参数详解(只看几个主要的)


1、analyzer 【分词器】 和 search-analyzer【查询时起作用的分词器】

只有text类型的字段才能使用analyzer参数,因为只有text类型才支持分词。

analyzer参数无法通过_mapping API进行修改,只能reindex修改。

但是search-analyzer可以通过_mapping API进行修改,修改的时候记得把该字段已有的"analyzer" 和"type" 带上

例子:

{
    "mappings":{
       "properties":{
          "title": {
             "type":"text",
             "analyzer":"my_analyzer",  // 写入的时候用 my_analyzer分词
             "search_analyzer":"my_stop_analyzer", //查询的时候用 my_stop_analyzer分词器对用户传入的内容分词
             "search_quote_analyzer":"my_analyzer" 
         }
      }
   }
}

例如:

PUT my-index-000001
{
  "settings": {
    "analysis": {
      "filter": {
        "autocomplete_filter": {
          "type": "edge_ngram",
          "min_gram": 1,
          "max_gram": 20
        }
      },
      "analyzer": {
        "autocomplete": { 
          "type": "custom",
          "tokenizer": "standard",
          "filter": [
            "lowercase",
            "autocomplete_filter"
          ]
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "text": {
        "type": "text",
        "analyzer": "autocomplete", 
        "search_analyzer": "standard" 
      }
    }
  }
}

PUT my-index-000001/_doc/1
{
  "text": "Quick Brown Fox"  // 走的analyzer的分词器,得到[ q, qu, qui, quic, quick, b, br, bro, brow, brown, f, fo, fox ]
}

GET my-index-000001/_search
{
  "query": {
    "match": {
      "text": {
        "query": "Quick Br", // 走的search_analyzer分词器,得到[ quick, br ]
        "operator": "and"
      }
    }
  }
}


2、boost 【权重提升参数】

PUT my-index-000001
{
    "mappings": {
        "properties": {
            "title": {
                "type": "text",
                "boost": 2
            },
            "content": {
                "type": "text"
            }
        }
    }
}

如此一来,title查询时的权重就是content的两倍,会影响到查询结果的分数。

查询_search API中也可以使用boost参数指定权重。

PS:mapping中的boosting参数在ES 5 中被废弃。boosting现在只在_search中起作用。


3、doc_values 【增加正排索引】

如果字段使用了这个参数,那么这个字段不仅会存到倒排索引中,也会多存一份到正排索引中。

PS:text和annotated_text类型的字段不支持doc_values参数,其他字段(如keyword、date、数值等不会被分词的类型)都支持doc_values并默认开启。

例如,某个字段类型为keyword,内容为:

你好呀我的朋友


那么他会存一份到倒排索引(意思是根据“你好呀我的朋友”找到 _id=1)

terms(分词)    |    _id
你好呀我的朋友        1


也会存一份到正排索引(意思是根据 _id=1 找到  “你好呀我的朋友”)

_id    |    terms(分词)
1          你好呀我的朋友

这样做的目的是方便这个字段做聚合、排序(这依赖正排索引)。如果你可以确定某个字段不会用作排序或聚合,那么你可以将这个字段的doc_values置为false。


下面是示例:

PUT my-index-000001
{
    "mappings": {
        "properties": {
            "status_code": {
                "type": "keyword"
            },
            "session_id": {
                "type": "keyword",
                "doc_values": false
            }
        }
    }
}

PS:无法为wildcard类型的字段禁用doc_values。

无论是否开启doc_values,都不会影响原始文档(_source)。

doc_values和_source本质上都是正排索引,区别在于,_source的每条数据都包含所有字段值,而doc_values中的正排索引的每条数据只包含设置了doc_values=true的字段,也就是每个doc_values正排索引只包含1个字段,字段和字段间是独立的。


4、dynamic 【是否允许字段被写入时自动mapping】

PUT my-index-000001
{
  "mappings": {
    "dynamic": false, 
    "properties": {
      "user": { 
        "properties": {
          "name": {
            "type": "text"
          },
          "social_networks": {
            "dynamic": true, 
            "properties": {}
          }
        }
      }
    }
  }
}

这里指定了最外层dynamic为false,因此如果我插入一个文档,该文档包含了一个未在mapping中定义的字段age,那么age会插入失败。

但是我插入一个 user.social_networks.abc 字段是可以成功的。


5、format 【格式化】

在ES的文档中,date日期类型一般会被展示为字符串,因此可以指定format参数对日期格式化。

PUT my-index-000001
{
  "mappings": {
    "properties": {
      "date": {
        "type":   "date",
        "format": "yyyy-MM-dd"
      }
    }
  }
}

更多format格式参考:https://www.elastic.co/guide/en/elasticsearch/reference/7.17/mapping-date-format.html


6、fields 【单字段多mapping设置】

为一个字段定义多mapping设置有两种用法。

用法1:为一个字段设置不同的字段类型。

例如下面例子中我为city设定了两个类型。text类型可用于分词搜索、keyword可用于精确搜索、排序和聚合。

PUT my-index-000001
{
  "mappings": {
    "properties": {
      "city": {
        "type": "text", // 主mapping(父mapping)
        "fields": {
          "raw": {   // 子mapping 1
            "type":  "keyword"
          }
        }
      }
    }
  }
}

PUT my-index-000001/_doc/1
{
  "city": "New York"
}

PUT my-index-000001/_doc/2
{
  "city": "York"
}

GET my-index-000001/_search
{
  "query": {
    "match": {
      "city": "york"  // 对city查询属于分词查询
    }
  },
  "sort": {
    "city.raw": "asc" // 对 city.raw排序和聚合
  },
  "aggs": {
    "Cities": {
      "terms": {
        "field": "city.raw" 
      }
    }
  }
}


用法2:一个字段的多个mapping的字段类型相同,但分词器或过滤器不同,使该字段可以使用不同的搜索行为进行搜索。

fields中的子mapping是不继承父mapping的,意思是说,父mapping可能用的A分词器和B过滤器,但子mapping可以指定自己的分词器和过滤器,如果不指定就是用的标准分词器。

例如:下面的例子中,text字段的父mapping使用标准分词器,子mapping使用english分词器。

PUT my-index-000001
{
  "mappings": {
    "properties": {
      "text": { 
        "type": "text",
        "fields": {
          "english": { 
            "type":     "text",
            "analyzer": "english"
          }
        }
      }
    }
  }
}

PUT my-index-000001/_doc/1
{ "text": "quick brown fox" } 

PUT my-index-000001/_doc/2
{ "text": "quick brown foxes" } 

GET my-index-000001/_search
{
  "query": {
    "multi_match": {
      "query": "quick brown foxes",
      "fields": [ 
        "text",
        "text.english"
      ],
      "type": "most_fields" 
    }
  }
}

同时使用text和text.english这两种分词器搜索“quick brown foxes”(也就是说,用户提供的查询内容会被 标准分词器分词 并到text倒排索引中检索,也会被english分词器分词并到text.english倒排索引中检索,只要两者有其中之一能搜到就算搜索成功),两者共同影响分数。

PS:为一个字段A增加一个字段类型A2可以使用 _mapping API 实现而无需reindx的。但是更改A已有的字段类型或者更改已有类型的分词器、过滤器等mapping参数是不行的,只能reindex。


7、enabled 【是否将字段放到倒排索引】

如果我们想把一个字段只存储到原始文档(即_source中),而不用根据该字段进行搜索(即不存到倒排索引中),就可以在创建index时对该字段指定enabled为false。

PUT my-index-000001
{
  "mappings": {
    "properties": {
      "user_id": {
        "type":  "keyword"
      },
      "last_updated": {
        "type": "date"
      },
      "session_data": { 
        "type": "object",
        "enabled": false  // session_data字段不放入倒排索引中
      }
    }
  }
}

PUT my-index-000001/_doc/session_1
{
  "user_id": "kimchy",
  "session_data": { // Any arbitrary data can be passed to the session_data field as it will be entirely ignored
    "arbitrary_object": {
      "some_array": [ "foo", "bar", { "baz": 2 } ]
    }
  },
  "last_updated": "2015-12-06T18:20:22"
}

PUT my-index-000001/_doc/session_2
{
  "user_id": "jpountz",
  "session_data": "none",  // The session_data will also ignore values that are not JSON objects
  "last_updated": "2015-12-06T18:22:13"
}

我们会发现,虽然mapping中我们指定 session_data 得是一个object类型,但是我们PUT了一个“none”字符串也没报错,这是因为enabled = false使得ES会忽略session_data值的内容。


下面这个例子表示,索引里的所有字段都不保存到倒排索引,只保存到原始文档_source,也意味着,无法根据任何字段进行搜索。

PUT my-index-000001
{
  "mappings": {
    "enabled": false 
  }
}

enabled参数只能设置到字段的最高层级mapping。

enabled无法通过_mapping API进行更改。


8、fielddata 【对某个字段将倒排索引加载到内存中转为正排索引】

极其不建议对字段开启fielddata,因为会消耗大量内存,导致性能问题。

我们一般开启fielddata的原因是为了需要正排索引以方便聚合和排序。但我们完全可以使用设置多字段类型的方案,一个字段既设置text类型,也设置keyword类型来解决。


9、ignore_above 【字段值超过了多少长度就不存入到倒排索引中】

如果一个字段设置了 ignore_above = 20,插入一个document时该字段如果超过20字符,那么该document的该字段就不会被添加到倒排索引中,但是该document的其他字段还是可以成功添加到倒排索引中的。

例子:

PUT my-index-000001
{
  "mappings": {
    "properties": {
      "message": {
        "type": "keyword",
        "ignore_above": 20 
      }
    }
  }
}

PUT my-index-000001/_doc/1 
{
  "message": "Syntax error"  // 写入成功
}

PUT my-index-000001/_doc/2 
{
  "message": "Syntax error with some long stacktrace"  // 这个文档写入成功,但是message这个字段没有写入到倒排索引
}

GET my-index-000001/_search 
{  // 查询结果值包含第一条
  "aggs": {
    "messages": {
      "terms": {
        "field": "message"
      }
    }
  }
}

ignore_above这个mapping参数可以通过_mapping API进行修改和新增。


10、ignore_malformed 【忽略字段类型错误】

如果你mapping定义一个字段A的类型是整数,但是用户插入的是一个字符串,此时整个文档会插入失败然后报错。

如果你希望忽略这种错误,可以对这个字段开启ignore_malformed。

PUT my-index-000001
{
  "mappings": {
    "properties": {
      "number_one": {
        "type": "integer",
        "ignore_malformed": true
      },
      "number_two": {
        "type": "integer"
      }
    }
  }
}

PUT my-index-000001/_doc/1
{// text写入成功,number_one写入倒排索引失败
  "text":       "Some text value",
  "number_one": "foo" 
}

PUT my-index-000001/_doc/2
{ // 整条document插入失败
  "text":       "Some text value",
  "number_two": "foo" 
}


ignore_malformed只对以下字段类型有效:


ignore_malformed也可以通过_mapping API进行修改。

我们可以在setting中设置index.mapping.ignore_malformed为true来让ignore_malformed对所有字段生效(那些不适用ignore_malformed参数的字段类型如text、keyword、object会自动忽略这个设置)。

PUT my-index-000001
{
  "settings": {
    "index.mapping.ignore_malformed": true 
  },
  "mappings": {
    "properties": {
      "number_one": { 
        "type": "byte"
      },
      "number_two": {
        "type": "integer",
        "ignore_malformed": false // 会覆盖settings的ignore_malformed
      }
    }
  }
}

如何处理哪些被忽略过的错误字段,我们要处理掉这些字段或者说这些文档,因为这些字段是空的,在倒排索引中也不存在,搜索就没有意义。

我们可以用exists、term或terms语句查询_ignored这个特殊字段来查到有哪些文档有这样的错误字段。可以参考:

https://www.elastic.co/guide/en/elasticsearch/reference/7.17/mapping-ignored-field.html

另外,如果你对一个字段开启了ignore_malformed,但是你插入数据时往这个字段插入了一个JSON对象,此时ignore_malformed会失效,整个document都无法写入成功。


11、normalizer 【keyword标准化】

normalizer是专门作用于keyword类型字段的分析转化器,normalizer对于keyword字段的作用相当于analyzer对于text类型字段的作用。

不同的地方在于,normalizer转化器处理后的结果只有一个token,什么叫1个token?

例如,一个text类型字段,它的值是“Hello Bob! How are you?",经过analyzer转化之后会得到 "hello", "bob", "how", "are", "you"共5个token,并将这5个token存入到倒排索引,每个token都是倒排索引中的一行。

而对于一个keyword类型字段,它的值同样是“Hello Bob! How are you?",经过normalizer转化之后会得到 ”hello bob! how are you?“这1个token存入到倒排索引。

由于normalizer只能发出一个 token。 因此,它们没有 tokenizer,只接受 char filters 和 token filters 这种基于每个字符的过滤器。 例如,允许使用 lowercase 过滤器,但不允许使用 stemming filter(词干过滤器)。

下面是一个例子:

PUT my_index
{
  "settings": {
    "analysis": {
      "char_filter": {
        "quote": {
          "type": "mapping",
          "mappings": [
            "« => \"",
            "» => \""
          ]
        }
      },
      "normalizer": {
        "my_normalizer": {
          "type": "custom",
          "char_filter": [
            "quote"
          ],
          "filter": [
            "lowercase",
            "asciifolding"
          ]
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "foo": {
        "type": "keyword",
        "normalizer": "my_normalizer"
      }
    }
  }
}

在上面 normalizer 的定义中,我们使用了一个 char filter 把 « 及 » 字符转换为引号 "。同时它也对字母进行小写及 asciifolding。我们现在插入如下的一个文档来进行展示:

PUT my_index/_doc/1 { "foo": "«açaí à la Carte»" }


根据我们上面定义的 normalizer, foo 经过normalizer处理后得出仅有一个 token:"acai a la carte”,这是因为 « 及 » 字符转换为引号 ",而 “açaí à la Carte” 经过 asciifolding 过滤器后,变为 acai a la Carte。再经过 lowercase 的过滤器,它就变为 "acai a la carte”。


针对上面的索引,我们可以进行如下的搜索:

PUT my_index/_doc/1
{
  "foo": "«açaí à la Carte»"
}

更多有关 normalizer 的介绍可以查看:

https://www.elastic.co/guide/en/elasticsearch/reference/7.17/normalizer.html


12、norms【是否对某字段开启score计算】

如果你觉得某个字段查询时不必计算分数(尤其是只用来聚合、排序和精确匹配的字段),就可以对该字段的norms设为false(默认是开启的),这样可以节省更多磁盘空间。

GET my_index/_search
{
  "query": {
    "match": {
      "foo": "\"acai a la carte\"" // 这段内容也同样会经过normalizer处理后才开始搜索
    }
  }
}

norms参数可以通过_mapping API 关闭,但是不能通过_mapping API开启。(文档说未来该参数可能会移除)


13、properties 【Object和nested类型字段的子属性】

14、null_value

15、store

16、position_increment_gap

17、copy_to





更多内容请关注微信公众号
zbpblog微信公众号

如果您需要转载,可以点击下方按钮可以进行复制粘贴;本站博客文章为原创,请转载时注明以下信息

张柏沛IT技术博客 > Elasticsearch入门基础系列(四) ES如何定义字段类型、Mapping映射 和 分词器介绍

热门推荐
推荐新闻