6000+字讲透ElasticSearch 索引设计

ElasticSearch 索引设计

在MySQL中数据库设计非常重要,同样在ES中数据库设计也是非常重要的

概述

我们创建索引就像创建表结构一样,必须非常慎重的,索引如果创建不好后面会出现各种各样的问题

索引设计的重要性

索引创建后,索引的分片只能通过_split和_shrink接口对其进行成倍的增加和缩减

主要是因为es的数据是通过_routing分配到各个分片上面的,所以本质上是不推荐去改变索引的分片数量的,因为这样都会对数据进行重新的移动。

还有就是索引只能新增字段,不能对字段进行修改和删除,缺乏灵活性,所以每次都只能通过_reindex重建索引了,还有就是一个分片的大小以及所以分片数量的多少严重影响到了索引的查询和写入性能,所以可想而知,设计一个好的索引能够减少后期的运维管理和提高不少性能,所以前期对索引的设计是相当的重要的。

基于时间的Index设计

Index设计时要考虑的第一件事,就是基于时间对Index进行分割,即每隔一段时间产生一个新的Index

这样设计的目的

因为现实世界的数据是随着时间的变化而不断产生的,切分管理可以获得足够的灵活性和更好的性能

240510bk-1.png

如果数据都存储在一个Index中,很难进行扩展和调整,因为Elasticsearch中Index的某些设置在创建时就设定好了,是不能更改的,比如Primary Shard的个数。

而根据时间来切分Index,则可以实现一定的灵活性,既可以在数据量过大时及时调整Shard个数,也可以及时响应新的业务需求。

大多数业务场景下,客户对数据的请求都会命中在最近一段时间上,通过切分Index,可以尽可能的避免扫描不必要的数据,提高性能。

时间间隔

根据上面的分析,自然是时间越短越能保持灵活性,但是这样做就会导致产生大量的Index,而每个Index都会消耗资源来维护其元信息的,因此需要在灵活性、资源和性能上做权衡

常见的间隔有小时、天、周和月:先考虑总共要存储多久的数据,然后选一个既不会产生大量Index又能够满足一定灵活性的间隔,比如你需要存储6个月的数据,那么一开始选择“周”这个间隔就会比较合适。

考虑业务增长速度:假如业务增长的特别快,比如上周产生了1亿数据,这周就增长到了10亿,那么就需要调低这个间隔来保证有足够的弹性能应对变化。

如何实现分割

切分行为是由客户端(数据的写入端)发起的,根据时间间隔与数据产生时间将数据写入不同的Index中,为了易于区分,会在Index的名字中加上对应的时间标识

创建新Index这件事,可以是客户端主动发起一个创建的请求,带上具体的Settings、Mappings等信息,但是可能会有一个时间错位,即有新数据写入时新的Index还没有建好,Elasticsearch提供了更优雅的方式来实现这个动作,即Index Template

分片设计

所谓分片设计,就是如何设定主分片的个数

看上去只是一个数字而已,也许在很多场景下,即使不设定也不会有问题(ES7默认是1个主分片一个副本分片),但是如果不提前考虑,一旦出问题就可能导致系统性能下降、不可访问、甚至无法恢复,换句话说,即使使用默认值,也应该是通过足够的评估后作出的决定,而非拍脑袋定的。

限制分片大小

单个Shard的存储大小不超过30GB

Elastic专家根据经验总结出来大家普遍认为30GB是个合适的上限值,实践中发现单个Shard过大(超过30GB)会导致系统不稳定。

其次,为什么不能超过30GB?主要是考虑Shard Relocate过程的负载,我们知道,如果Shard不均衡或者部分节点故障,Elasticsearch会做Shard Relocate,在这个过程中会搬移Shard,如果单个Shard过大,会导致CPU、IO负载过高进而影响系统性能与稳定性。

评估分片数量

单个Index的Primary Shard个数 = k * 数据节点个数

在保证第一点的前提下,单个Index的Primary Shard个数不宜过多,否则相关的元信息与缓存会消耗过多的系统资源,这里的k,为一个较小的整数值,建议取值为1,2等,整数倍的关系可以让Shard更好地均匀分布,可以充分的将请求分散到不同节点上。

小索引设计

对于很小的Index,可以只分配1~2个Primary Shard的

有些情况下,Index很小,也许只有几十、几百MB左右,那么就不用按照第二点来分配了,只分配1~2个Primary Shard是可以,不用纠结。

使用索引模板

就是把已经创建好的某个索引的参数设置(settings)和索引映射(mapping)保存下来作为模板,在创建新索引时,指定要使用的模板名,就可以直接重用已经定义好的模板中的设置和映射

Elasticsearch基于与索引名称匹配的通配符模式将模板应用于新索引,也就是说通过索引进行匹配,看看新建的索引是否符合索引模板,如果符合,就将索引模板的相关设置应用到新的索引,如果同时符合多个索引模板呢,这里需要对参数priority进行比较,这样会选择priority大的那个模板进行创建索引。

在创建索引模板时,如果匹配有包含的关系,或者相同,则必须设置priority为不同的值,否则会报错,索引模板也是只有在新创建的时候起到作用,修改索引模板对现有的索引没有影响,同样如果在索引中设置了一些设置或者mapping都会覆盖索引模板中相同的设置或者mapping

索引模板的用途

索引模板一般用在时间序列相关的索引中。

也就是说, 如果你需要每间隔一定的时间就建立一次索引,你只需要配置好索引模板,以后就可以直接使用这个模板中的设置,不用每次都设置settings和mappings.

创建索引模板

COPYPUT _index_template/logstash-village
{
  "index_patterns": [
    "logstash-village-*"  // 可以通过"logstash-village-*"来适配创建的索引
  ],
  "template": {
    "settings": {
      "number_of_shards": "3", //指定模板分片数量
      "number_of_replicas": "2"  //指定模板副本数量
    },
    "aliases": {
      "logstash-village": {}  //指定模板索引别名
    },
    "mappings": {   //设置映射
      "dynamic": "strict", //禁用动态映射
      "properties": {
        "@timestamp": {
          "type": "date",
           "format": "strict_date_optional_time||epoch_millis||yyyy-MM-dd HH:mm:ss"
        },
        "@version": {
          "doc_values": false,
          "index": "false",
          "type": "integer"
        },
        "name": {
          "type": "keyword"
        },
        "province": {
          "type": "keyword"
        },
        "city": {
          "type": "keyword"
        },
        "area": {
          "type": "keyword"
        },
        "addr": {
          "type": "text",
          "analyzer": "ik_smart"
        },
        "location": {
          "type": "geo_point"
        },
        "property_type": {
          "type": "keyword"
        },
        "property_company": {
          "type": "text",
          "analyzer": "ik_smart"
        },
        "property_cost": {
          "type": "float"
        },
        "floorage": {
          "type": "float"
        },
        "houses": {
          "type": "integer"
        },
        "built_year": {
          "type": "integer"
        },
        "parkings": {
          "type": "integer"
        },
        "volume": {
          "type": "float"
        },
        "greening": {
          "type": "float"
        },
        "producer": {
          "type": "keyword"
        },
        "school": {
          "type": "keyword"
        },
        "info": {
          "type": "text",
          "analyzer": "ik_smart"
        }
      }
    }
  }
}




模板参数

下面是创建索引模板的一些参数



参数名称    参数介绍

index_patterns  必须配置,用于在创建期间匹配索引名称的通配符(*)表达式数组
template  可选配置,可以选择包括别名、映射或设置配置
composed_of  可选配置,组件模板名称的有序列表。组件模板按指定的顺序合并,这意味着最后指定的组件模板具有最高的优先级
priority  可选配置,创建新索引时确定索引模板优先级的优先级。选择具有最高优先级的索引模板。如果未指定优先级,则将模板视为优先级为0(最低优先级)
version  可选配置,用于外部管理索引模板的版本号
_meta  可选配置,关于索引模板的可选用户元数据,可能有任何内容
映射配置

上面我们配置了映射模板,但是我们用到了映射,下面我们说下映射






什么是映射

在创建索引时,可以预先定义字段的类型(映射类型)及相关属性

数据库建表的时候,我们DDL依据一般都会指定每个字段的存储类型,例如:varchar、int、datetime等,目的很明确,就是更精确的存储数据,防止数据类型格式混乱,在Elasticsearch中也是这样,创建索引的时候一般也需要指定索引的字段类型,这种方式称为映射(Mapping)

被动创建(动态映射)

此时字段和映射类型不需要事先定义,只需要存在文档的索引,当向此索引添加数据的时候当遇到不存在的映射字段,ES会根据数据内容自动添加映射字段定义。

动态映射规则

使用动态映射的时候,根据传递请求数据的不同会创建对应的数据类型

数据类型  Elasticsearch 数据类型
null  不添加任何字段
true或者false  boolean类型
浮点数据  float类型
integer数据  long类型
object  object类型
array  取决于数组中的第一个非空值的类型。
string  如果此内容通过了日期格式检测,则会被认为是date数据类型 如果此值通过了数值类型检测则被认为是double或者long数据类型 带有关键字子字段会被认为一个text字段
禁止动态映射

一般生产环境下需要禁用动态映射,使用动态映射可能出现以下问题

造成集群元数据一直变更,导致不稳定;

可能造成数据类型与实际类型不一致;

如何禁用动态映射,动态mapping的dynamic字段进行配置,可选值及含义如下

true:支持动态扩展,新增数据有新的字段属性时,自动添加对于的mapping,数据写入成功
false:不支持动态扩展,新增数据有新的字段属性时,直接忽略,数据写入成功
strict:不支持动态扩展,新增数据有新的字段时,报错,数据写入失败
主动创建(显示映射)

动态映射只能保证最基础的数据结构的映射

所以很多时候我们需要对字段除了数据结构定义更多的限制的时候,动态映射创建的内容很可能不符合我们的需求,所以可以使用PUT {index}/mapping来更新指定索引的映射内容。

映射类型

我们要创建映射必须还要知道映射类型,否则就会走默认的映射类型,下面我们看看常用的映射类型

准备工作

我们先创建一个用于测试映射类型的索引

COPYPUT mapping_demo

字符串类型

字符串类型是我们最常用的类型之一,我们操作的时候字符串类型可以被设置为以下几种类型

text

当一个字段是要被全文搜索的,比如Email内容、产品描述,应该使用text类型,text类型会被分词

设置text类型以后,字段内容会被分词,在生成倒排索引以前,字符串会被分析器分成一个一个词项,text类型的字段不用于排序,很少用于聚合

keyword

keyword类型不会被分词,常用于关键字搜索,比如姓名、email地址、主机名、状态码和标签等

如果字段需要进行过滤(比如查姓名是张三发布的博客)、排序、聚合,keyword类型的字段只能通过精确值搜索到,常常被用来过滤、排序和聚合

两者区别

它们的区别在于text会对字段进行分词处理而keyword则不会进行分词

也就是说如果字段是text类型,存入的数据会先进行分词,然后将分完词的词组存入索引,而keyword则不会进行分词,直接存储,这样划分数据更加节省内存。

使用案例

我们先创建一个映射,name是keyword类型,描述是text类型的

COPYPUT mapping_demo/_mapping
{
  "properties": {
    "name": {
        "type": "keyword"
     },
      "city": {
        "type": "text",
        "analyzer": "ik_smart"
     }
  }
}
插入数据

COPYPUT mapping_demo/_doc/1
{
  "name":"北京小区",
  "city":"北京市昌平区回龙观街道"
}




对于keyword的name字段进行精确查询



COPYGET mapping_demo/_search
{
  "query": {
    "term": {
      "name": "北京小区"
    }
  }
}




对于text的city进行模糊查询

COPYGET mapping_demo/_search
{
  "query": {
    "term": {
      "city": "北京市"
    }
  }
}
数字类型

数字类型也是我们最常用的类型之一,下面我们看下数字类型的使用

类型    取值范围

long  -263 ~ 263
integer  -231 ~ 231
short  -215 ~ 215
byte  -27 ~ 27
double  64位的双精度 IEEE754 浮点类型
float  32位的双精度 IEEE754 浮点类型
half_float  16位的双精度 IEEE754 浮点类型
scaled_float  缩放类型的浮点类型
注意事项

在满足需求的情况下,优先使用范围小的字段,字段长度越小,索引和搜索的效率越高。

日期类型

JSON表示日期

JSON没有表达日期的数据类型,所以在ES里面日期只能是下面其中之一



格式化的日期字符串,比如:"2015-01-01" or "2015/01/01 12:10:30"

用数字表示的从新纪元开始的毫秒数

用数字表示的从新纪元开始的秒数(epoch_second)

注意点:毫秒数的值是不能为负数的,如果时间在1970年以前,需要使用格式化的日期表达






ES如何处理日期

在ES的内部,时间会被转换为UTC时间(如果声明了时区)并使用从新纪元开始的毫秒数的长整形数字类型的进行存储,在日期字段上的查询,内部将会转换为使用长整形的毫秒进行范围查询,根据与字段关联的日期格式,聚合和存储字段的结果将转换回字符串

注意点:日期最终都会作为字符串呈现,即使最开始初始化的时候是利用JSON文档的long声明的

默认日期格式

日期的格式可以被定制化的,如果没有声明日期的格式,它将会使用默认的格式:



COPY"strict_date_optional_time||epoch_millis"

这意味着它将会接收带时间戳的日期,它将遵守strict_date_optional_time限定的格式(yyyy-MM-dd'T'HH:mm:ss.SSSZ 或者 yyyy-MM-dd)或者毫秒数



日期格式示例

COPYPUT mapping_demo/_mapping
{
  "properties": {
    "datetime": {
        "type": "date",
        "format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis"
     }
  }
}
# 添加数据
PUT mapping_demo/_doc/2
{
  "name":"河北区",
  "city":"河北省小区",
  "datetime":"2022-02-21 11:35:42"
}
日期类型参数

下面表格里的参数可以用在date字段上面
参数    说明

doc_values  该字段是否按照列式存储在磁盘上以便于后续进行排序、聚合和脚本操作,可配置 true(默认)或 false
format  日期的格式
locale  解析日期中时使用了本地语言表示月份时的名称和/或缩写,默认是 ROOT locale
ignore_malformed  如果设置为true,则奇怪的数字就会被忽略,如果是false(默认)奇怪的数字就会导致异常并且该文档将会被拒绝写入。需要注意的是,如果在脚本参数中使用则该属性不能被设置
index  该字段是否能快速的被查询,默认是true。date类型的字段只有在doc_values设置为true时才能被查询,尽管很慢。
null_value  替代null的值,默认是null
on_script_error  定义在脚本中如何处理抛出的异常,fail(默认)则整个文档会被拒绝索引,continue:继续索引
script  如果该字段被设置,则字段的值将会使用该脚本产生,而不是直接从source里面读取。
store  true or false(默认)是否在 _source 之外在独立存储一份
布尔类型

boolean类型用于存储文档中的true/false

范围类型

顾名思义,范围类型字段中存储的内容就是一段范围,例如年龄30-55岁,日期在2020-12-28到2021-01-01之间等



类型范围

es中有六种范围类型:



integer_range

float_range

long_range

double_range

date_range

ip_range

使用实例

COPYPUT mapping_demo/_mapping
{
  "properties": {
    "age_range": {
        "type": "integer_range"
     }
  }
}


# 指定年龄范围,可以使用 gt、gte、lt、lte。

PUT mapping_demo/_doc/3
{
  "name":"张三",
  "age_range":{
    "gt":20,
    "lt":30
  }
}
分词器

什么是分词器

分词器的主要作用将用户输入的一段文本,按照一定逻辑,分析成多个词语的一种工具

顾名思义,文本分析就是把全文本转换成一系列单词(term/token)的过程,也叫分词,在 ES 中,Analysis 是通过分词器(Analyzer) 来实现的,可使用 ES 内置的分析器或者按需定制化分析器。

举一个分词简单的例子:比如你输入 Mastering Elasticsearch,会自动帮你分成两个单词,一个是 mastering,另一个是 elasticsearch,可以看出单词也被转化成了小写的。

240510bk-2.png

分词器构成

分词器是专门处理分词的组件,分词器由以下三部分组成:

character filter
接收原字符流,通过添加、删除或者替换操作改变原字符流



例如:去除文本中的html标签,或者将罗马数字转换成阿拉伯数字等,一个字符过滤器可以有零个或者多个



tokenizer

简单的说就是将一整段文本拆分成一个个的词



例如拆分英文,通过空格能将句子拆分成一个个的词,但是对于中文来说,无法使用这种方式来实现,在一个分词器中,有且只有一个tokenizeer

token filters

将切分的单词添加、删除或者改变

例如将所有英文单词小写,或者将英文中的停词a删除等,在token filters中,不允许将token(分出的词)的position或者offset改变,同时,在一个分词器中,可以有零个或者多个token filters。

分词顺序


240510bk-3.png


同时 Analyzer 三个部分也是有顺序的,从图中可以看出,从上到下依次经过 Character Filters,Tokenizer 以及 Token Filters,这个顺序比较好理解,一个文本进来肯定要先对文本数据进行处理,再去分词,最后对分词的结果进行过滤。



测试分词

可以通过_analyzerAPI来测试分词的效果,我们使用下面的html过滤分词

COPYPOST _analyze
{
    "text":"<b>hello world<b>"  # 输入的文本
    "char_filter":["html_strip"], # 过滤html标签
    "tokenizer":"keyword", #原样输出
}
什么时候分词

文本分词会发生在两个地方:

创建索引:当索引文档字符类型为text时,在建立索引时将会对该字段进行分词。
搜索:当对一个text类型的字段进行全文检索时,会对用户输入的文本进行分词。






创建索引时指定分词器

如果设置手动设置了分词器,ES将按照下面顺序来确定使用哪个分词器

先判断字段是否有设置分词器,如果有,则使用字段属性上的分词器设置

如果设置了analysis.analyzer.default,则使用该设置的分词器

如果上面两个都未设置,则使用默认的standard分词器

字段指定分词器

为addr属性指定分词器,这里我们使用的是中文分词器



COPYPUT my_index
{
  "mappings": {
    "properties": {
     "info": {
        "type": "text",
        "analyzer": "ik_smart"
       }
    }
  }
}
设置默认分词器

COPYPUT my_index
{
  "settings": {
    "analysis": {
      "analyzer": {
        "default":{
          "type":"simple"
        }
      }
    }
  }
}


搜索时指定分词器

在搜索时,通过下面参数依次检查搜索时使用的分词器,这样我们的搜索语句就会先分词,然后再来进行搜索



搜索时指定analyzer参数

创建mapping时指定字段的search_analyzer属性

创建索引时指定setting的analysis.analyzer.default_search

查看创建索引时字段指定的analyzer属性

如果上面几种都未设置,则使用默认的standard分词器。

指定analyzer

搜索时指定analyzer查询参数



COPYGET my_index/_search
{
  "query": {
    "match": {
      "message": {
        "query": "Quick foxes",
        "analyzer": "stop"
      }
    }
  }
}
指定字段analyzer

COPYPUT my_index
{
  "mappings": {
    "properties": {
      "title":{
        "type":"text",
        "analyzer": "whitespace",
        "search_analyzer": "simple"
      }
    }
  }
}




指定默认default_seach

COPYPUT my_index
{
  "settings": {
    "analysis": {
      "analyzer": {
        "default":{
          "type":"simple"
        },
        "default_seach":{
          "type":"whitespace"
        }
      }
    }
  }
}
内置分词器

es在索引文档时,会通过各种类型 Analyzer 对text类型字段做分析

不同的 Analyzer 会有不同的分词结果,内置的分词器有以下几种,基本上内置的 Analyzer 包括 Language Analyzers 在内,对中文的分词都不够友好,中文分词需要安装其它 Analyzer



分析器  描述  分词对象  结果
standard  标准分析器是默认的分析器,如果没有指定,则使用该分析器。它提供了基于文法的标记化(基于 Unicode 文本分割算法,如 Unicode 标准附件 # 29所规定) ,并且对大多数语言都有效。The 2 QUICK Brown-Foxes jumped over the lazy dog’s bone.  [ the, 2, quick, brown, foxes, jumped, over, the, lazy, dog’s, bone ]
simple  简单分析器将文本分解为任何非字母字符的标记,如数字、空格、连字符和撇号、放弃非字母字符,并将大写字母更改为小写字母。The 2 QUICK Brown-Foxes jumped over the lazy dog’s bone.  [ the, quick, brown, foxes, jumped, over, the, lazy, dog, s, bone ]
whitespace  空格分析器在遇到空白字符时将文本分解为术语  The 2 QUICK Brown-Foxes jumped over the lazy dog’s bone.  [ The, 2, QUICK, Brown-Foxes, jumped, over, the, lazy, dog’s, bone. ]
stop  停止分析器与简单分析器相同,但增加了删除停止字的支持。默认使用的是 _english_ 停止词。The 2 QUICK Brown-Foxes jumped over the lazy dog’s bone.  [ quick, brown, foxes, jumped, over, lazy, dog, s, bone ]
keyword  不分词,把整个字段当做一个整体返回  The 2 QUICK Brown-Foxes jumped over the lazy dog’s bone.  [The 2 QUICK Brown-Foxes jumped over the lazy dog’s bone.]
pattern  模式分析器使用正则表达式将文本拆分为术语。正则表达式应该匹配令牌分隔符,而不是令牌本身。正则表达式默认为 w+ (或所有非单词字符)。The 2 QUICK Brown-Foxes jumped over the lazy dog’s bone.  [ the, 2, quick, brown, foxes, jumped, over, the, lazy, dog, s, bone ]
多种西语系 arabic, armenian, basque, bengali, brazilian, bulgarian, catalan, cjk, czech, danish, dutch, english等等  一组旨在分析特定语言文本的分析程序。
    

IK中文分词器

IKAnalyzer

IKAnalyzer是一个开源的,基于java的语言开发的轻量级的中文分词工具包

从2006年12月推出1.0版开始,IKAnalyzer已经推出了3个大版本,在 2012 版本中,IK 实现了简单的分词歧义排除算法,标志着 IK 分词器从单纯的词典分词向模拟语义分词衍化



中文分词器算法

中文分词器最简单的是ik分词器,还有jieba分词,哈工大分词器等



分词器    描述    分词对象    结果

ik_smart    ik分词器中的简单分词器,支持自定义字典,远程字典    学如逆水行舟,不进则退    [学如逆水行舟,不进则退]

ik_max_word    ik_分词器的全量分词器,支持自定义字典,远程字典    学如逆水行舟,不进则退    [学如逆水行舟,学如逆水,逆水行舟,逆水,行舟,不进则退,不进,则,退]

ik_smart

原始内容

COPY传智教育的教学质量是杠杠的

测试分词

COPYGET _analyze
{
  "analyzer": "ik_smart",
  "text": "传智教育的教学质量是杠杠的"
}


ik_max_word

原始内容

COPY传智教育的教学质量是杠杠的

测试分词

COPYGET _analyze
{
  "analyzer": "ik_max_word",
  "text": "传智教育的教学质量是杠杠的"
}




作者:陈烨123


欢迎关注微信公众号 :java知路