Elastic Search学习系列06-索引管理
Table of Contents
1 创建一个索引
之前通过索引一篇文档创建了一个新的索引。这个索引采用的是默认的配置,新的字段通过动态映射的方式被添加到类型映射。现在我们需要对这个建立索引的过程做更多的控制:我们想要确保这个索引有数量适中的主分片,并且在我们索引任何数据 之前 ,分析器和映射已经被建立好。
为了达到这个目的,我们需要手动创建索引,在请求体里面传入设置或类型映射,如下所示:
PUT /my_index { "settings": { ... any settings ... }, "mappings": { "type_one": { ... any mappings ... }, "type_two": { ... any mappings ... }, ... } }
如果你想禁止自动创建索引,你 可以通过在config/elasticsearch.yml的每个节点下添加下面的配置:
action.auto_create_index: false
可以使用索引模板来预配置开启自动创建索引。这在索引日志数据的时候尤其有用:你将日志数据索引在一个以日期结尾命名的索引上,子夜时分,一个预配置的新索引将会自动进行创建。
2 删除一个索引
用以下的请求来删除索引:
DELETE /my_index
也可以这样删除多个索引:
DELETE /index_one,index_two DELETE /index_*
甚至可以这样删除全部索引:
DELETE /_all DELETE /*
对一些人来说,能够用单个命令来删除所有数据可能会导致可怕的后果。如果你想要避免意外的大量删除, 你可以在你的 elasticsearch.yml 做如下配置:
action.destructive_requires_name: true
3 索引设置
可以通过修改配置来自定义索引行为。Elasticsearch提供了优化好的默认配置。除非你理解这些配置的作用并且知道为什么要去修改,否则不要随意修改。
下面是两个 最重要的设置:
- number_of_shards
- 每个索引的主分片数,默认值是5。这个配置在索引创建后不能修改。
- number_of_replicas
- 每个主分片的副本数,默认值是1。对于活动的索引库,这个配置可以随时修改。
例如,我们可以创建只有一个主分片,没有副本的小索引:
PUT /my_temp_index { "settings": { "number_of_shards" : 1, "number_of_replicas" : 0 } }
然后,我们可以用update-index-settings API动态修改副本数:
PUT /my_temp_index/_settings
{
"number_of_replicas": 1
}
4 配置分析器
第三个重要的索引设置是analysis部分,用来配置已存在的分析器或针对你的索引创建新的自定义分析器。
分析器,用于将全文字符串转换为适合搜索的倒排索引。
standard分析器是用于全文字段的默认分析器,对于大部分西方语系来说是一个不错的选择。它包括了以下几点:
- standard分词器,通过单词边界分割输入的文本。
- standard语汇单元过滤器,目的是整理分词器触发的语汇单元(但是目前什么都没做)。
- lowercase语汇单元过滤器,转换所有的语汇单元为小写。
- stop语汇单元过滤器,删除停用词—对搜索相关性影响不大的常用词,如a,the,and,is。
默认情况下,停用词过滤器是被禁用的。如需启用它,你可以通过创建一个基于standard分析器的自定义分析器并设置stopwords参数。可以给分析器提供一个停用词列表,或者告知使用一个基于特定语言的预定义停用词列表。
在下面的例子中,我们创建了一个新的分析器,叫做es_std, 并使用预定义的西班牙语停用词列表:
curl -X PUT "localhost:9200/spanish_docs?pretty" -H 'Content-Type: application/json' -d' { "settings": { "analysis": { "analyzer": { "es_std": { "type": "standard", "stopwords": "_spanish_" } } } } } '
5 自定义分析器
虽然Elasticsearch带有一些现成的分析器,然而在分析器上Elasticsearch真正的强大之处在于,你可以通过在一个适合你的特定数据的设置之中组合字符过滤器、分词器、词汇单元过滤器来创建自定义的分析器。
一个分析器就是在一个包里面组合了三种函数的一个包装器, 三种函数按照顺序被执行:
- 字符过滤器
- 字符过滤器用来整理一个尚未被分词的字符串。例如,如果我们的文本是HTML格式的,它会包含像 <p>或者<div>这样的HTML标签,这些标签是我们不想索引的。我们可以使用html清除字符过滤器来移除掉所有的HTML标签,并且像把Á转换为相对应的Unicode字符Á这样,转换HTML实体。
- 一个分析器可能有0个或者多个字符过滤器。
- 分词器
- 一个分析器必须有一个唯一的分词器。分词器把字符串分解成单个词条或者词汇单元。标准分析器里使用的 标准分词器把一个字符串根据单词边界分解成单个词条,并且移除掉大部分的标点符号,然而还有其他不同行为的分词器存在。
- 例如,关键词分词器完整地输出接收到的同样的字符串,并不做任何分词。空格分词器只根据空格分割文本。正则分词器根据匹配正则表达式来分割文本。
- 词单元过滤器
- 经过分词,作为结果的 词单元流 会按照指定的顺序通过指定的词单元过滤器 。
- 词单元过滤器可以修改、添加或者移除词单元。我们已经提到过lowercase和stop词过滤器,但是在Elasticsearch里面还有很多可供选择的词单元过滤器。词干过滤器把单词遏制为词干。ascii_folding过滤器移除变音符,把一个像"très"这样的词转换为"tres"。ngram和edge_ngram词单元过滤器可以产生适合用于部分匹配或者自动补全的词单元。
和我们之前配置es_std分析器一样,我们可以在analysis下的相应位置设置字符过滤器、分词器和词单元过滤器:
PUT /my_index { "settings": { "analysis": { "char_filter": { ... custom character filters ... }, "tokenizer": { ... custom tokenizers ... }, "filter": { ... custom token filters ... }, "analyzer": { ... custom analyzers ... } } } }
6 类型和映射
类型在Elasticsearch中表示一类相似的文档。类型由名称—比如user或blogpost—和映射组成。
映射, 就像数据库中的schema,描述了文档可能具有的字段或属性、每个字段的数据类型—比如string,integer或date—以及Lucene是如何索引和存储这些字段的。
类型可以很好的抽象划分相似但不相同的数据。但由于Lucene的处理方式,类型的使用有些限制。
6.1 Lucene如何处理文档
在Lucene中,一个文档由一组简单的键值对组成。每个字段都可以有多个值,但至少要有一个值。类似的,一个字符串可以通过分析过程转化为多个值。Lucene不关心这些值是字符串、数字或日期—所有的值都被当做不透明字节。
当我们在Lucene中索引一个文档时,每个字段的值都被添加到相关字段的倒排索引中。也可以将未处理的原始数据存储起来,以便这些原始数据在之后也可以被检索到。
6.2 类型是如何实现的
Elasticsearch类型是以Lucene处理文档的这个方式为基础来实现的。一个索引可以有多个类型,这些类型的文档可以存储在相同的索引中。
Lucene没有文档类型的概念,每个文档的类型名被存储在一个叫_type的元数据字段上。当我们要检索某个类型的文档时,Elasticsearch通过在_type字段上使用过滤器限制只返回这个类型的文档。
Lucene也没有映射的概念。映射是Elasticsearch将复杂JSON文档映射成Lucene需要的扁平化数据的方式。
例如,在user类型中,name字段的映射可以声明这个字段是string类型,并且它的值被索引到名叫name的倒排索引之前,需要通过whitespace分词器分析:
"name": { "type": "string", "analyzer": "whitespace" }
6.3 避免类型陷阱
这导致了一个有趣的思想实验:如果有两个不同的类型,每个类型都有同名的字段,但映射不同(例如:一个是字符串一个是数字),将会出现什么情况?
简单回答是,Elasticsearch不会允许你定义这个映射。当你配置这个映射时,将会出现异常。
详细回答是,每个Lucene索引中的所有字段都包含一个单一的、扁平的模式。一个特定字段可以映射成string类型也可以是number类型,但是不能两者兼具。因为类型是Elasticsearch添加的优于Lucene的额外机制(以元数据_type字段的形式),在Elasticsearch中的所有类型最终都共享相同的映射。
对于整个索引,映射在本质上被扁平化成一个单一的、全局的模式。这就是为什么两个类型不能定义冲突的字段:当映射被扁平化时,Lucene不知道如何去处理。
6.4 类型结论
那么,这个讨论的结论是什么?技术上讲,多个类型可以在相同的索引中存在,只要它们的字段不冲突(要么因为字段是互为独占模式,要么因为它们共享相同的字段)。
重要的一点是:类型可以很好的区分同一个集合中的不同细分。在不同的细分中数据的整体模式是相同的(或相似的)。
类型不适合完全不同类型的数据。如果两个类型的字段集是互不相同的,这就意味着索引中将有一半的数据是空的(字段将是稀疏的),最终将导致性能问题。在这种情况下,最好是使用两个单独的索引。
7 根对象
映射的最高一层被称为根对象,它可能包含下面几项:
- 一个properties节点,列出了文档中可能包含的每个字段的映射
- 各种元数据字段,它们都以一个下划线开头,例如_type、_id和_source
- 设置项,控制如何动态处理新的字段,例如analyzer、dynamic_date_formats和dynamic_templates
- 其他设置,可以同时应用在根对象和其他object类型的字段上,例如enabled、dynamic和include_in_all
7.1 属性
文档字段和属性的三个最重要的设置:
- type
- 字段的数据类型,例如string或date
- index
- 字段是否应当被当成全文来搜索(analyzed),或被当成一个准确的值(not_analyzed),还是完全不可被搜索(no)
- analyzer
- 确定在索引和搜索时全文字段使用的analyzer
7.2 元数据:_source字段
默认地,Elasticsearch在_source字段存储代表文档体的JSON字符串。和所有被存储的字段一样,_source字段在被写入磁盘之前先会被压缩。
这个字段的存储几乎总是我们想要的,因为它意味着下面的这些:
- 搜索结果包括了整个可用的文档——不需要额外的从另一个的数据仓库来取文档。
- 如果没有_source字段,部分update请求不会生效。
- 当你的映射改变时,你需要重新索引你的数据,有了_source字段你可以直接从Elasticsearch这样做,而不必从另一个(通常是速度更慢的)数据仓库取回你的所有文档。
- 当你不需要看到整个文档时,单个字段可以从_source字段提取和通过get或者search请求返回。
- 调试查询语句更加简单,因为你可以直接看到每个文档包括什么,而不是从一列id猜测它们的内容。
7.3 元数据:_all字段
_all字段:一个把其它字段值当作一个大字符串来索引的特殊字段。query_string查询子句(搜索?q=john)在没有指定字段时默认使用_all字段。
_all字段在新应用的探索阶段,当你还不清楚文档的最终结构时是比较有用的。你可以使用这个字段来做任何查询,并且有很大可能找到需要的文档:
GET /_search { "match": { "_all": "john smith marketing" } }
随着应用的发展,搜索需求变得更加明确,你会发现自己越来越少使用_all字段。
7.4 元数据:文档标识
文档标识与四个元数据字段相关:
- _id
- 文档的ID字符串
- _type
- 文档的类型名
- _index
- 文档所在的索引
- _uid
- _type和_id连接在一起构造成type#id
默认情况下,_uid字段是被存储(可取回)和索引(可搜索)的。_type字段被索引但是没有存储,_id和_index字段则既没有被索引也没有被存储,这意味着它们并不是真实存在的。
8 动态映射
当Elasticsearch遇到文档中以前未遇到的字段,它用dynamic mapping来确定字段的数据类型并自动把新的字段添加到类型映射。
有时这是想要的行为有时又不希望这样。通常没有人知道以后会有什么新字段加到文档,但是又希望这些字段被自动的索引。也许你只想忽略它们。如果Elasticsearch是作为重要的数据存储,可能就会期望遇到新字段就会抛出异常,这样能及时发现问题。
幸运的是可以用dynamic配置来控制这种行为 ,可接受的选项如下:
- true
- 动态添加新的字段—缺省
- false
- 忽略新的字段
- strict
- 如果遇到新字段抛出异常
配置参数dynamic可以用在根object或任何object类型的字段上。你可以将dynamic的默认值设置为strict, 而只在指定的内部对象中开启它, 例如:
curl -X PUT "localhost:9200/my_index?pretty" -H 'Content-Type: application/json' -d' { "mappings": { "my_type": { "dynamic": "strict", "properties": { "title": { "type": "string"}, "stash": { "type": "object", "dynamic": true } } } } } '
如果遇到新字段,对象my_type就会抛出异常。
而内部对象stash遇到新字段就会动态创建新字段。
把dynamic设置为false一点儿也不会改变_source的字段内容。_source仍然包含被索引的整个JSON文档。只是新的字段不会被加到映射中也不可搜索。
9 自定义动态映射
如果你想在运行时增加新的字段,你可能会启用动态映射。然而,有时候,动态映射 规则 可能不太智能。幸运的是,我们可以通过设置去自定义这些规则,以便更好的适用于你的数据。
9.1 日期检查
当Elasticsearch遇到一个新的字符串字段时,它会检测这个字段是否包含一个可识别的日期,比如2014-01-01。如果它像日期,这个字段就会被作为date类型添加。否则,它会被作为string类型添加。
日期检测可以通过在根对象上设置date_detection为false来关闭。
9.2 动态模版
使用dynamic_templates,你可以完全控制新检测生成字段的映射。你甚至可以通过字段名称或数据类型来应用不同的映射。
每个模板都有一个名称,你可以用来描述这个模板的用途,一个mapping来指定映射应该怎样使用,以及至少一个参数(如match)来定义这个模板适用于哪个字段。
模板按照顺序来检测;第一个匹配的模板会被启用。例如,我们给string类型字段定义两个模板:
- es :以_es结尾的字段名需要使用spanish分词器。
- en:所有其他字段使用english分词器。
我们将es模板放在第一位,因为它比匹配所有字符串字段的en模板更特殊:
curl -X PUT "localhost:9200/my_index?pretty" -H 'Content-Type: application/json' -d' { "mappings": { "my_type": { "dynamic_templates": [ { "es": { "match": "*_es", "match_mapping_type": "string", "mapping": { "type": "string", "analyzer": "spanish" } }}, { "en": { "match": "*", "match_mapping_type": "string", "mapping": { "type": "string", "analyzer": "english" } }} ] }}} '
匹配字段名以_es结尾的字段。
匹配其他所有字符串类型字段。
match_mapping_type允许你应用模板到特定类型的字段上,就像有标准动态映射规则检测的一样,(例如string或long)。
match参数只匹配字段名称,path_match参数匹配字段在对象上的完整路径,所以address.*.name将匹配这样的字段:
{ "address": { "city": { "name": "New York" } } }
unmatch和path_unmatch将被用于未被匹配的字段。
10 缺省映射
通常,一个索引中的所有类型共享相同的字段和设置。_default_映射更加方便地指定通用设置,而不是每次创建新类型时都要重复设置。_default_映射是新类型的模板。在设置_default_映射之后创建的所有类型都将应用这些缺省的设置,除非类型在自己的映射中明确覆盖这些设置。
例如,我们可以使用_default_映射为所有的类型禁用_all字段,而只在blog类型启用:
curl -X PUT "localhost:9200/my_index?pretty" -H 'Content-Type: application/json' -d' { "mappings": { "_default_": { "_all": { "enabled": false } }, "blog": { "_all": { "enabled": true } } } } '
11 重新索引数据
尽管可以增加新的类型到索引中,或者增加新的字段到类型中,但是不能添加新的分析器或者对现有的字段做改动。如果你那么做的话,结果就是那些已经被索引的数据就不正确,搜索也不能正常工作。
对现有数据的这类改变最简单的办法就是重新索引:用新的设置创建新的索引并把文档从旧的索引复制到新的索引。
字段_source的一个优点是在Elasticsearch中已经有整个文档。你不必从源数据中重建索引,而且那样通常比较慢。
为了有效的重新索引所有在旧的索引中的文档,用scroll从旧的索引检索批量文档,然后用bulk API把文档推送到新的索引中。
12 索引别名和零停机
在前面提到的,重建索引的问题是必须更新应用中的索引名称。索引别名就是用来解决这个问题的!
索引别名就像一个快捷方式或软连接,可以指向一个或多个索引,也可以给任何一个需要索引名的API来使用。别名带给我们极大的灵活性,允许我们做下面这些:
- 在运行的集群中可以无缝的从一个索引切换到另一个索引
- 给多个索引分组(例如,last_three_months)
- 给索引的一个子集创建视图
有两种方式管理别名:_alias用于单个操作,_aliases用于执行多个原子级操作。
我们假设你的应用有一个叫my_index的索引。事实上,my_index是一个指向当前真实索引的别名。真实索引包含一个版本号:my_index_v1,my_index_v2等等。
首先,创建索引my_index_v1,然后将别名my_index指向它:
curl -X PUT "localhost:9200/my_index_v1?pretty" curl -X PUT "localhost:9200/my_index_v1/_alias/my_index?pretty"
检测这个别名指向哪一个索引:
curl -X GET "localhost:9200/*/_alias/my_index?pretty"
或哪些别名指向这个索引:
curl -X GET "localhost:9200/my_index_v1/_alias/*?pretty"
然后,我们决定修改索引中一个字段的映射。当然,我们不能修改现存的映射,所以我们必须重新索引数据。 首先, 我们用新映射创建索引my_index_v2:
curl -X PUT "localhost:9200/my_index_v2?pretty" -H 'Content-Type: application/json' -d' { "mappings": { "my_type": { "properties": { "tags": { "type": "string", "index": "not_analyzed" } } } } } '
然后我们将数据从my_index_v1索引到my_index_v2,下面的过程在重新索引你的数据中已经描述过。一旦我们确定文档已经被正确地重索引了,我们就将别名指向新的索引。
一个别名可以指向多个索引,所以我们在添加别名到新索引的同时必须从旧的索引中删除它。这个操作需要原子化,这意味着我们需要使用_aliases操作:
curl -X POST "localhost:9200/_aliases?pretty" -H 'Content-Type: application/json' -d' { "actions": [ { "remove": { "index": "my_index_v1", "alias": "my_index" }}, { "add": { "index": "my_index_v2", "alias": "my_index" }} ] } '
应用已经在零停机的情况下从旧索引迁移到新索引了。在你的应用中使用别名而不是索引名。然后你就可以在任何时候重建索引。别名的开销很小,应该广泛使用。