概要

Elasticsearch让索引创建变得非常简单,只要索引一条新的数据,索引会自动创建出来,但随着数据量的增加,我们开始有了索引优化和搜索优化的需求之后,就会发现自动创建的索引在某些方面不能非常完美的适应我们的需求,我们开始考虑手动创建适合我们业务需求的索引。

索引的CRUD

为了更好地贴切我们的业务数据需求,我们开始更精细的管理我们的索引。

创建索引

创建索引的语法示例如下:

PUT /music
{
  "settings": {
    "number_of_shards": 3,
    "number_of_replicas": 1
  },
  "mappings": {
    "children": {
      "properties": {
          "name": {
            "type": "text",
            "fields": {
              "keyword": {
                "type": "keyword",
                "ignore_above": 256
              }
            }
          }
      }
    }
  }
}

settings内的参数

  • number_of_shards:每个索引的primary shard数量,索引创建后不可修改。
  • number_of_replicas: 每个索引的replica shard的数量,可以随时修改。

mappings内的参数

  • type: 6.3.1版本只允许设置一个type
  • properties:类型映射具体信息,索引文档的字段名称,类型,分词器都在里面指定。

默认Elasticsearch是允许自动创建索引的,生产环境上为了避免自动索引可能出现的隐患,可以禁止自动创建索引,修改elasticsearch.yml配置文件即可:

action.auto_create_index: false

修改索引

可以单独修改setting部分和mapping部分,修改setting部分示例如下:

PUT /music/_settings
{
    "number_of_replicas": 2
}

如果要修改mapping信息,如给索引新增字段length、likes、content,示例如下:

PUT /music/_mapping/children
{
  "properties": {
    "length": {
        "type": "long"
      },
    "likes": {
        "type": "long"
      },
    "content": {
      "type": "text",
      "fields": {
        "keyword": {
          "type": "keyword",
          "ignore_above": 256
        }
      }
    }
  }
}

删除索引

DELETE /music
DELETE /music,content
DELETE /music*
DELETE /_all
DELETE /*

如上命令均可删除索引,但此操作一定要慎重,反复确认后再操作,误删的后果不可想像,建议删除操作一定要设置操作权限,另外Elasticsearch可以设置只限定索引名称进行删除,不允许通配符或_all删除大量的索引,作如下设置即可:

action.destructive_requires_name: true

误删索引的后果非常严重,请在操作权限上加把锁,宁可麻烦也不要误删。

查看索引信息

GET /music
GET /music/_settings
GET /music/_mapping

三条命令可以查看索引的完整信息,只查setting信息,只查mapping信息。

分词器设置

analysis是索引设置中非常重要的一部分,默认的分词器我们前面有介绍,有兴趣可以翻一下。我们可以为索引单独配置特有的分词器,或者自定义分词器。

修改分词器设置

例如,我们为music索引创建一个新的分词器,叫做music_std,启用英文停用词列表:

PUT /music
{
  "settings": {
    "analysis": {
      "analyzer": {
        "music_std": {
          "type": "standard",
          "stopwords": "_english_"
        }
      }
    }
  }
}

此命令只能在创建时候执行,已经存在的索引执行会报错。

我们对music索引进行分词器测试:

GET /music/_analyze
{
  "analyzer": "music_std",
  "text": "get up brightly early in the morning"
}

测试结果是"in","the"这两个词已经被正确的移除掉了。

自定义分词器

Elasticsearch对分词器的应用设置得非常灵活,用户可以根据自己的需求灵活定制字符过滤器、分词器、词单元过滤器来创建自定义的分词器。

文档的分词过程包含以下几步:

  • 字符过滤器

对字符串进行预处理,如HTML标签清洗<span>Love</span> --> Love,I & you --> I and you等等。

  • 分词器

把字符串切分成单个的词条,如英文的按空格和标点切分,中文的按词语切分,针对不同的语言,有不同的分词器,有相对简单的标准分词器,也有特别复杂的中文分词器,里面包含了非常复杂的切分逻辑如:

I Love you --> I/Love/you

我和我的祖国 --> 我/和/我的/祖国

  • Token过滤器
    将分词器得到的词条进一步的处理,如改变词条(英文词干提取loves --> love),删除无实际意义的词条(英文的a, and, this,中文的"的","了","吗"),增加词条(补充同义词)

如果我们自定义分词器,可以从这三个组件入手,可以自行替换。我们举一个示例:

PUT /music
{
  "settings": {
    "analysis": {
      "char_filter": {
        "&_to_and": {
          "type": "mapping",
          "mappings": ["&=> and"]
        }
      },
      "filter": {
        "my_stopwords": {
          "type": "stop",
          "stopwords": ["the", "a"]
        }
      },
      "analyzer": {
        "my_analyzer": {
          "type": "custom",
          "char_filter": ["html_strip", "&_to_and"],
          "tokenizer": "standard",
          "filter": ["lowercase", "my_stopwords"]
        }
      }
    }
  }
}

上面示例中我们自定义的分词器有如下特点:

  • 字符过滤器:把&转换成and,并加上html_strip处理html文本
  • token过滤器:将"the","a"作为停用词,全部改成小写

我们对这个分词器进行测试:

GET /music/_analyze
{
  "text": "you & me the love, <a>, HAHA!!",
  "analyzer": "my_analyzer"
}

响应的结果:

{
  "tokens": [
    {
      "token": "you",
      "start_offset": 0,
      "end_offset": 3,
      "type": "<ALPHANUM>",
      "position": 0
    },
    {
      "token": "and",
      "start_offset": 4,
      "end_offset": 5,
      "type": "<ALPHANUM>",
      "position": 1
    },
    {
      "token": "me",
      "start_offset": 6,
      "end_offset": 8,
      "type": "<ALPHANUM>",
      "position": 2
    },
    {
      "token": "love",
      "start_offset": 13,
      "end_offset": 17,
      "type": "<ALPHANUM>",
      "position": 4
    },
    {
      "token": "haha",
      "start_offset": 24,
      "end_offset": 28,
      "type": "<ALPHANUM>",
      "position": 5
    }
  ]
}

可以看到,"the"作为停用词被移除了,&变成了"and",html标签&lt;a&gt;移除了,HAHA小写处理后得到haha。

自定义分词器后,如果需要应用在索引上,需要将它绑定到具体的字段上:

PUT /music/_mapping/children
{
  "properties": {
    "content": {
      "type": "text",
      "analyzer": "my_analyzer"
    }
  }
}

后面只要有新的文档进行索引,在content字段上都会使用我们自定义的分词器。

映射对象

root object

映射对象信息是一组JSON结构,最顶层的叫根对象(root object),包括内容如下:

  • properties: 索引中每个字段的映射信息。
  • metadata:各种元数据信息,以下划线开头,如_id,_source,_type。
  • settings:设置项信息,如analyzer。
  • 其他settings:比如include_in_all

properties

主要是指文档字段和属性最重要的三个设置:

  • type: 数据类型,如text、date、long等。
  • index: 该字段是否需要全文搜索(analyzed),或精准搜索(not_analyzed)或是不支持搜索(no)。
  • analyzer: 文档索引和搜索时的分词器。

例如节选了以下properties信息:

{
  "music": {
    "mappings": {
      "children": {
        "properties": {
          "author": {
            "type": "text",
            "analyzer": "english"
          }
        }
      }
    }
  }
}

_source

_source字段存储的内容包含文档的JSON字符串,_source字段在写入磁盘前会被压缩。

_source存储的内容才是我们真正关心的数据,我们可以更加方便的完成这些事:

  • 查询的时候可以一次性拿到完整的document,不需要先拿document id,再发送一次请求拿document
  • partial update基于_source实现
  • reindex时,直接基于_source实现,不需要从数据库(或者其他外部存储)查询数据再修改
  • 可以基于_source定制返回field
  • debug query更容易,因为可以直接看到_source

_all

建立索引时将所有field拼接在一起,作为一个_all field ,没指定任何field进行搜索时,就是搜索_all field,一般轻量搜索中用得比较多。

如果不需要_all field,可以设置成禁用:

PUT /music/_mapping/children
{
  "_all": {"enabled": false}
}

也可以指定某些field不加入_all field

PUT /music/_mapping/children
{
  "properties": {
    "author": {
      "type": "text",
      "include_in_all": false
    }
  }
}

metadata

文档标识主要的几个字段:

  • _id:文档ID
  • _type:类型名称,6.x以后一个索引只会有一个type
  • _index: 文档所在的索引名称

这三个字段是用来标识一个独一无二的文档所在的位置信息,从这三个字段我们基本上可以定位出来该文档存储在哪个shard中。

动态映射

dynamic属性

Elasticsearch索引文档时,如果JSON结构出现新的字段,Elasticsearch会根据dynamic mapping规则来识别字段的数据类型,并自动增加新的字段,如果我们对文档的JSON结构有较严格的规定,这种自动增加字段的行为,就不是我们期望的操作,我们可以为properties设置dynamic属性来决定这种行为:

  • true: 动态添加新的字段
  • false:忽略新的字段
  • strict: 遇到新的字段,抛出异常

这个dynamic参数可以在任何一层的object中使用,如:

PUT /music
{
  "mappings": {
    "children": {
      "dynamic": "strict",
      "properties": {
        "name": {
          "type": "text"
        },
        "address": {
          "type": "object",
          "dynamic": "true"
        }
      }
    }
  }
}

如果children下面遇到新字段,就会抛出异常
如果address内部对象中遇到新字段,会动态创建该字段

示例:

# address内部对象增加两个新字段
PUT /music/children/1
{
  "name":"sunshine",
  "address": {
    "province": "gd",
    "city": "sz"
  }
}

创建成功,响应如下:

{
  "_index": "music",
  "_type": "children",
  "_id": "1",
  "_version": 1,
  "result": "created",
  "_shards": {
    "total": 2,
    "successful": 1,
    "failed": 0
  },
  "_seq_no": 0,
  "_primary_term": 1
}
# children下直接增加新字段author
PUT /music/children/1
{
  "name":"sunshine",
  "author":"Johnny Cash"
}

创建失败,报错响应如下:

{
  "error": {
    "root_cause": [
      {
        "type": "strict_dynamic_mapping_exception",
        "reason": "mapping set to strict, dynamic introduction of [author] within [children] is not allowed"
      }
    ],
    "type": "strict_dynamic_mapping_exception",
    "reason": "mapping set to strict, dynamic introduction of [author] within [children] is not allowed"
  },
  "status": 400
}

定制dynamic mapping策略

Elasticsearch在运行中遇到新增的字段时,会根据动态映射模板为新的字段定义类型,但字段类型是根据首次遇到的字段值来定义的,可能会出现误判的情况。

我们先举一个反例,假设我们有一个新增的字段remark,里面的内容是"2019-12-17",是一个日期格式的内容,Elasticsearch会把这个note字段设置成日期格式,但remark字段第二条数据过来的却是"Comment Submit",这只是一段文本,remark字段已经是日期格式了,第二条保存就会抛出异常。

针对日期检测,我们可以选择关闭,如下:

PUT /music
{
    "mappings": {
        "children": {
            "date_detection": false
        }
    }
}

但我们针对Long类型,Boolean类型的,同样有这种情况,逐一关闭可行性不高,为此我们需要使用动态模板配置。

动态模板

使用动态模板(dynamic template),我们可以通过字段名称或数据类型来应用不同的映射,来定制自己的模板。

例如我们使用字段名称后缀的方式:

PUT /music
{
    "mappings": {
        "children": {
            "dynamic_templates": [
                { "en": {
                      "match":              "*_en", 
                      "match_mapping_type": "string",
                      "mapping": {
                          "type":           "text",
                          "analyzer":       "english"
                      }
                }}
            ]
        }

    }
}

这个含义是如果字段以_en结尾,那么类型为text,analyzer为english,否则类型为string,analyzer为standard。

测试内容:

PUT /music/children/1
{
  "content": "you are my sunshine"
}

PUT /music/children/2
{
  "content_en": "you are my sunshine"
}

理论上content使用standard分词器,4个单词均可被索引到,content_en字段使用english分词器,are作为停用词会被移除掉。

对索引进行搜索可知:

GET /music/children/_search
{
  "query": {
    "match": {
      "content_en": "are"
    }
  }
}

结果是空

{
  "took": 1,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": 0,
    "max_score": null,
    "hits": []
  }
}

使用其他的关键词,或使用content字段,搜索均能出结果,符合预期。

小建议

以上只是动态映射模板的一个小案例,真实生产环境中文档的复杂度远高于此,对文档的结构而言,优先手动创建索引,明确每个字段的含义和数据类型,其次再做通用的动态映射模板,但也需要定时检查索引下的数据类型,以防出现意外情况。

小结

本篇主要介绍索引的相关知识,包含索引的CRUD、自定义分词器、映射对象的知识,最后简单介绍了映射模板的配置,实际生产如果有乃至动态模板配置,肯定远比这个复杂,这里仅作抛砖引玉,谢谢。

专注Java高并发、分布式架构,更多技术干货分享与心得,请关注公众号:Java架构社区
可以扫左边二维码添加好友,邀请你加入Java架构社区微信群共同探讨技术
Java架构社区