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标签,并且像把&Aacute;转换为相对应的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" }}
    ]
}
'

应用已经在零停机的情况下从旧索引迁移到新索引了。在你的应用中使用别名而不是索引名。然后你就可以在任何时候重建索引。别名的开销很小,应该广泛使用。