04-Es API_b


1. 分片和分段

1.1 分片

  • 分片是 Es 处理的最小单元;
  • 一个分片是一个 Lucene 索引;
  • 一个包含倒排索引的文件目录;
  • 分片越多搜索越慢。

a. 扩展和容灾

  • 一个索引的所有分片会自动均匀分布在所有节点中;
  • 加入新节点后,原集群节点的分片会部分迁移到新节点;
  • 设置分片数量稍微大于节点数量,有利于横向扩容时,分片蔓延到所有新节点(每个节点都有分片时最理想的状态);
  • 主分片和所有副本分片都就绪时,索引的健康状态是绿色的。

  • 挂掉了 N 个节点时,如果副本分片是 N,那么剩下的副本分片将自动提升为主分片;
  • 所有的主分片能构成完整的索引,但是副本分片缺失,所以此时索引健康状态是黄色;
  • 如果挂掉 N+1 个节点,主分片将缺失,健康状态是红色;
  • 根据实际情况设置副本数量(副本太多会影响性能);
  • 通常同时挂掉两个节点的概率不高,一个副本可以满足常规容灾要求。

b. 索引和搜索数据

(1)索引请求:

  1. 索引文档请求到一个节点;
  2. 文档被随机到一个主分片;
  3. 从主分片同步到副本分片;
  4. 返回成功的结果。

副本分片越多,索引数据越慢,因为要所有副本都完成才算完成。

(2)搜索请求:

  1. 搜索请求到一个节点;
  2. 节点转发请求到本节点的一个分片(主副均可)和其他节点上的其他分片;
  3. 所有分片都返回搜索结果到发起节点;
  4. 发起节点返回搜索结果到请求方。

不同节点上的“主分片+副本分片”的总数越多,请求被分摊的越均匀,并发搜索性能越好(单个搜索无法通过增加分片副本的数量来加速)。但如果节点数很少,分片都集中到少数节点上,搜索速度会变慢,因为增加了开销,实际没有分摊负载。

c. 索引分片不是免费的吗?

【官方建议】JVM Heap 每 1G 不超过 20 个分片;每个分片大小在 20G~40G。

1.2 分段

  • Lucene 索引再分割成小单元;
  • 分段越多搜索越慢;
  • 分段不会被修改;
  • 索引新的文档会创建新的分段;
  • 分段会持续地被合并;
  • 删除文档时不会真的删除(标记删除)

a. 分段写入

  • 只能写打开的分段;
  • 删除是假删除,也是往打开的分段写删除标记(→ 分段不会被修改);
  • 分段大小超过一定阈值,会触发分段合并;
  • 为了查询加速,小分段会合并成大分段;合并过程是先创建一个大分段,把两个小的重写进去,然后再删掉两个小分段,这个过程会耗费大量资源;
  • 如果希望写入快,应该避免频繁分段合并。

b. 分段读取

  • 只能读关闭的分段(所以叫“准实时”)
  • 对一个分片查询,会等它所有的分段结果,所以分片过多 ...
  • 刷新
    • 刷新时,会关闭一批分段,这时数据才会被查到;
    • 刷新频率太快会导致分段碎片多;
    • 刷新频率太蛮会导致读写实时性低(只读关闭的);

c. 一切都是为了更快

  • 弱化关系、弱化一致性,都是为了速度;
  • 想读得快,就要牺牲写速度,反之亦然。
  • 刷新间隔:index#settings#refresh_interval

2. 索引、更新、删除数据

2.1 索引文档

a. 字段类型

关于数据的存储:简单来说就是压扁了存~

(1)动态映射

PUT /company/_doc/1
{
  "address": {
    "country": "china",
    "province": "guangdong",
    "city": "guangzhou"
  },
  "name": "jack",
  "age": 27,
  "join_date": "2019-01-01"
}

(2)查询映射

GET /company/_mapping
{
  "company" : {
    "mappings" : {
      "properties" : {
        "address" : {
          "properties" : {
            "city" : {
              "type" : "text",
              "fields" : {
                "keyword" : {
                  "type" : "keyword",
                  "ignore_above" : 256
                }
              }
            },
            "country" : {
              "type" : "text",
              "fields" : {
                "keyword" : {
                  "type" : "keyword",
                  "ignore_above" : 256
                }
              }
            },
            "province" : {
              "type" : "text",
              "fields" : {
                "keyword" : {
                  "type" : "keyword",
                  "ignore_above" : 256
                }
              }
            }
          }
        },
        "age" : {
          "type" : "long"
        },
        "join_date" : {
          "type" : "date"
        },
        "name" : {
          "type" : "text",
          "fields" : {
            "keyword" : {
              "type" : "keyword",
              "ignore_above" : 256
            }
          }
        }
      }
    }
  }
}

(3)docID=1 的底层存储格式

{
    "name":             [jack],
    "age":              [27],
    "join_date":        [2021-07-13],
    "address.country":  [china],
    "address.province": [jiangsu],
    "address.city":     [nanjing]
}

(4)扩展:对象数组

{
    "authors": [
        { "age": 26, "name": "Jack White"},
        { "age": 55, "name": "Tom Jones"},
        { "age": 39, "name": "Kitty Smith"}
    ]
}
·························· 存储格式 ··························
{
    "authors.age":    [26, 55, 39],
    "authors.name":   [jack, white, tom, jones, kitty, smith]
}

b. Mapping&Template

类似数据库的字段类型定义,你定义了一个字段,就要指定一个类型;如果不指定字段类型,Es 会在插入第一条数据时,自动创建一个 index,同时帮你创建 mapping 或者是索引了新字段,字段 mapping 也将会被动态设置。

字段如果和定义的类型不匹配,会插入失败。因此同一个字段不能有时候是字符串,有时候是数组。最好提前定义映射而不是依赖自动。

字段的 Mapping 配置中,处理指定类型,还可以为字段指定参数:① analyzer,指定分析器;② boost,指定字段的分值加成。

通过 模板(Template) 创建索引:除了每次创建 index 前,手动指定 index 的 mapping 和配置外,还可以自动从 Template 中获取 index 的 mapping 以及其他的设置(分片、副本数等),这是一个非常常用的操作。

  • dynamic field mapping:Es 会根据 Json 字段自动判断类型,甚至能够发现 string 里面填的是日期、数值还是文本来设置字段类型;
  • dynamic template:可以配置映射模板,自定义类型识别;
  • index template:索引模板,通过索引名命中模板,模板中设置字段类型;

【注】

  • 字段是否分析:不分析就不能全文检索这个字段,省性能;
  • 字段是否索引:不索引就不能搜这个字段,省性能;

b. 分析字段

  • 定义:解析、转变、分解文本,使得搜索变得更加相关;
  • 步骤:
    • 字符过滤(过滤器):使用字符过滤器转变字符(比如:大写变小写);
    • 文本切分为分词(分词器):将文本切分为单个或者多个分词(比如:英文文本用空格切位一堆单词);
    • 分词过滤(分词过滤器):转变每个分词(比如把 'a', 'an', 'of' 这类词干掉,或者复数单词转为单数单词);

2.2 更新文档

  • 前面提过 segment 创建之后不能修改,因此文档更新实际上是创建了一条新的然后删掉旧的。但其实删除也不是真删,而是加上一条删除标记。
    • 分段合并的时候会真删
    • 删除会消耗性能
    • 整个 index 删除是最快的
  • 文档更新步骤:检索文档(按 id)→ 处理文档 → 重新索引文档
  • 更新有 3 种方式:整条更新、字段合并、若存在则放弃更新
  • 可以使用自动生成 id 来插入新文档,这样可以节省检索文档所耗费的资源(如果手动指定,Es 会先去检索该 id 的文档是否存在,如果存在,则会新增 → 更新),加快索引速度。

Es 显然是分布式的,那么就会有并发问题:再一个更新获取原文档进行修改期间,可能会有另一个更新也在修改这篇文档,那么第一个更新就丢了 → Es 通过「文档版本」实现并发控制(类似‘乐观锁’)

  1. 为每个文档设置一个版本号;
  2. 文档创建时版本号是 1;
  3. 当更新后,版本号++变成 2;
  4. 如果此时有另一个更新,版本号也将是 2,此更新结束时发现已经有一个 2,那么将产生冲突;
  5. 发生冲突后,重试这个更新操作,如果不再冲突,那么完成更新,版本号设置为 3;

  • 可以更精确地控制冲突,默认情况下遇到冲突会更新失败,通过参数 retry_on_conflict 控制重试次数,默认为 0(不重试);
  • 可以显式指定版本号(插入和更新都可以),而不是默认取最新版本;
  • 可以使用外部版本号,比如时间戳;

2.3 删除文档

a. 删除文档

可以通过 id 删除单个,也可以通过条件批量删除,类似于 DELETE ... FROM ... WHERE ...

删除文档会拖慢查询和进一步的索引:因为删除只是标记为删除,搜索的时候还要检查一遍命中的文档是否已经被删除;只有分段合并的时候才彻底删除。

不推荐使用。

b. 删除索引

  • 删除索引是很快的,因为是直接移除索引相关的分片文件;
  • 删除是不可恢复的,在生产环境上也没有权限控制,一定要小心操作(7.0+可控制权限);
  • 小心!DELETE_ALL 会直接瞬间清空所有索引!

c. 关闭索引

  • 除了删除,还有一个更安全的操作,就是关闭索引;
  • 索引关闭之后,不能读取和写入,直到再次打开;
  • 关闭后的索引只占磁盘,非常 cheap,因此我们通常会关闭而不是删除索引;
[POST] /my_index/_close
[POST] /my_index/_open

d. 冻结索引

  • 扩展包功能
  • 介于打开和关闭之间:不能写入;分片开销很小;
    [POST] /my_index/_freeze
    [POST] /my_index/_unfreeze
    
  • 腾讯云版 Es 做了限制,需要特殊参数才能搜;
    [GET] /my_index/_search?ignore_throttled=false
    

2.4 reindex

  • 复制一个索引
    • 可用于重建索引
    • 可用于提取字段
    • 可以跨集群复制
  • 改变索引配置
    • 分段一旦生成就不能修改,因此索引一旦创建就无法改变;
    • 有些索引的配置也是不可改变的,比如分片数量、Mapping 映射等;
    • 只能通过重建索引修改
  • 提取字段
    • 有时字段休要通过脚本处理后才能满足新的使用需求;
    • 比如只存了航班 AA571,没有单独存航司,需要按航司聚类;
    • 可以用脚本字段聚类,但不建议使用;
    • 可以通过 reindex,写脚本来索引新字段;
  • 注意,reindex 操作不会复制索引的配置,所以需要体检设置或者配置 Template;
  • reindex 之前最好先把目标配置的副本数减为 0,并关闭刷新,加快写入;
[POST] _reindex
{
  "source": {
    "index": "source_index"
  },
  "dest": {
    "index": "dest_index"
  }
}

2.5 ILM

ILM(Index Lifecycle Management)索引生命周期管理

  • rollover 滚动存储:可让分片大小均匀在 30~40 G;
  • shrink 缩减分片:写的时候分片多可加速;读的时候收缩分片,减小内存消耗;
  • allocate 分片节点感知:冷热分离;远期日志放到冷节点;省钱+延长日志存放时间;
  • forcemerge 压缩分段:加速查询,节省开销;
  • freeze 把索引关闭:不占内存只占存储;进一步延长日志存放时间;
  • delete 彻底删除:自动删除,当前是用云脚本,不方便统一管理;

3. 搜索数据

Es 的搜索上下文分为「查询上下文(query context)」和「过滤上下文(filter context)」。区别在于过滤器不计算相关性,只关心是否命中条件。计算相关性需要计算匹配度分值,耗费性能(匹配度分值都是实时计算,无法缓存),所以尽量使用过滤查询以减少性能消耗加快查询速度。

3.1 Full-Text-Queries

全文检索,被查询的字段需要被分析;查询条件也会被分析。

a. match

最基本的全文检索查询,支持单词查询、模糊匹配、短语查询、近义词查询。

b. match_phrase

类似 match,专门查询短语,可以指定短语的间隔 slop(默认是 0)。例如希望含有 "quick brown fox" 的文档也能够匹配对 "quick fox" 的查询。

slop 参数告诉 match_phrase 查询词条能够相隔多远时仍然将文档视为匹配。相隔多远的意思是,你需要移动一个词条多少次来让查询和文档匹配?

尽管在使用了 slop 的短语匹配中,所有的单词都需要出现,但是单词的出现顺序可以不同。如果 slop 的值足够大,那么单词的顺序可以是任意的。

再比如说,想查一个词出现多次的:

{
    "query": {
        "match_phrase": {
            "products.product_name": {
                "query": "blue blue blue",
                "slop": 1000
            }
        }
    }
}

c. match_phrase_prefix

类似短语查询,但最后一个单词是前缀查询,用于最后一个单词想不起来的情况。比如“is t”可以命中“this is a test”。

d. multi_match

把 match 查询用在多个字段上。

e. common_terms

给非普通单词更大的权重。比如“eat”是普通单词,“tree6x7”是特殊单词。

f. query_string

使用 Lucene 查询语法的查询,可以指定各种 AND|OR|NOT 查询条件,而且支持在一条语句里对多字段查询。

g. simple_query_string

傻瓜版的 query_string,可以兼容错误的语法,不会搞挂查询,适合当作搜索框直接暴露给用户。

3.2 Term-Level-Queries

精确匹配查询,查询条件不会被分析。通常用于结构化的数据。比如数字、日期、枚举、keyword(当然,对于被分析过的字段也可以用)。

a. Normalizer

【场景】在 Es 中处理字符串类型的数据时,如果我们想把整个字符串作为一个完整的 term 存储,我们通常会将其类型 type 设定为 keyword。但有时这种设定又会给我们带来麻烦,比如同一个数据再写入时由于没有做好清洗,导致大小写不一致,比如 apple、Apple 两个实际都是 apple,但当我们去搜索 apple 时却无法返回 Apple 的文档。要解决这个问题,就需要 Normalizer 出场了。

PUT test_normalizer
{
  "mappings": {
    "doc":{
      "properties": {
        "type":{
          "type":"keyword"
        }
      }
    }
  }
}

PUT test_normalizer/doc/1
{
  "type":"apple"
}

PUT test_normalizer/doc/2
{
  "type":"Apple"
}

# 查询一 
GET test_normalizer/_search
{
  "query": {
    "match":{
      "type":"apple"
    }
  }
}

# 查询二
GET test_normalizer/_search
{
  "query": {
    "match":{
      "type":"aPple"
    }
  }
}

大家执行后会发现 查询一返回了文档 1,而 查询二没有文档返回,原因如下图所示:

  1. Docs 写入 Es 时由于 type 是 keyword,分词结果为原始字符串;
  2. 查询 Query 时分词默认是采用和字段写时相同的配置,因此这里也是 keyword,因此分词结果也是原始字符;
  3. 两边的分词进行匹对,便得出了我们上面的结果。

Normalizer 是 keyword 的一个属性,可以对 keyword 生成的单一 Term 再做进一步的处理,比如 lowercase,即做小写变换。使用方法和自定义分词器有些类似,需要自定义,如下所示:

DELETE test_normalizer

# 自定义 normalizer
PUT test_normalizer
{
  "settings": {
    "analysis": {
      "normalizer": {
        "lowercase": {
          "type": "custom",
          "filter": [
            "lowercase"
          ]
        }
      }
    }
  },
  "mappings": {
    "doc": {
      "properties": {
        "type": {
          "type": "keyword"
        },
        "type_normalizer": {
          "type": "keyword",
          "normalizer": "lowercase"
        }
      }
    }
  }
}

PUT test_normalizer/doc/1
{
  "type": "apple",
  "type_normalizer": "apple"
}

PUT test_normalizer/doc/2
{
  "type": "Apple",
  "type_normalizer": "Apple"
}

# 查询三
GET test_normalizer/_search
{
  "query": {
    "term":{
      "type":"aPple"
    }
  }
}

# 查询四
GET test_normalizer/_search
{
  "query": {
    "term":{
      "type_normalizer":"aPple"
    }
  }
}

上述是自定义了名为“lowercase”的 Normalizer,其中 filter 类似自定义分词器中的 filter ,但是可用的种类很少,详情大家可以查看官方文档。然后通过 Normalizer 属性设定到字段 type_normalizer 中,然后插入相同的 2 条文档。执行发现,查询 3 无结果返回,查询 4 返回 2 条文档。

  1. 文档写入时由于加入了 Normalizer,所有的 term 都会被做小写处理;
  2. 查询时搜索词同样采用有 Normalizer 的配置,因此处理后的 term 也是小写的;
  3. 两边分词匹对,就得到了我们上面的结果。

b. term

精确匹配整个查询语句。

c. terms

类似 terms,可以传入一个数组,匹配到其中一个即可。

d. terms_set

类似 terms,可以指定匹配条件数。支持脚本通过计算指定。

e. range

范围查询;可以按区间查日期、数字,甚至字符串。

f. exists

非空查询;

g. prefix

前缀匹配。

h. wildcard

通配符查询,支持单个 ? 和多个 *,通配符放在越前面,查询效率越低。

i. regexp

正则表达式查询;使用不当会造成效率低下的查询。不要出现过度通配。

查询结果不被缓存。

j. fuzzy

模糊查询,比如 ab 可以命中 ba

k. type

类型查询,指定被查询字段的 mapping。

l. ids

id 查询,可指定多个 id。

3.3 Compound-Queries

a. constant_score

包裹住的查询,会使用 filter 上下文,不计算相关性得分(可以指定常量分值)。

b. bool

最常用的组合查询,AND / OR / NOT / filter 可嵌套。

c. dis_max

对多个子查询的得分取最高,如果有子查询得分相近,还有加成选项。

d. function_score

可以对子查询的得分进行复杂计算,比如最大、最小、平均、随机、各种复杂的数学运算。

e. boosting

可以对子查询进行加分 positive 或者减分 negative,区别在于 bool 查询里的 NOT,不是去掉,而是降低命中者的权重。

3.4 Other-Queries

  • Join-Queries:类似关系型数据库那样的关联查询;
  • Geo-Queries:地理信息查询;
  • Specialized-Queries:特殊查询,比如脚本;
  • Span-Queries:跨度查询,将分词之间的距离纳入查询;被查询的字段需要被分析
elk