Elasticsearch 基础入门详文

2022-04-24 13:10:11 浏览数 (2)

作者:lynneyli,腾讯 IEG 运营开发工程师

Elasticsearch(简称:ES)功能强大,其背后有很多默认值,或者默认操作。这些操作优劣并存,优势在于我们可以迅速上手使用 ES,劣势在于,其实这些默认值的背后涉及到很多底层原理,怎么做更合适,只有数据使用者知道。用 ES 的话来说,你比 ES 更懂你的数据,但一些配置信息、限制信息,还是需要在了解了 ES 的功能之后进行人工限制。

你是否遇到:在使用了一段时间 ES 之后,期望使用 ES 的其他功能,例如聚合、排序,但因为字段类型受限,无奈只能进行reindex等一系列问题?

题主在遇到一些问题后,发现用 ES 很简单,但是会用 ES 很难。这让我下定决心一定好好了解 ES,也就出现了本文。

前言

ES(全称 Elastic Search)是一款开源、近实时、高性能的分布式搜索引擎。在近 3 年的热门搜索引擎类数据统计中,ES 都霸居榜首(数据来源:DBRaking),可见的其深受大家的喜爱。

随着 ES 的功能越来越强大,其和数据库的边界也越来越小,除了进行全文检索,ES 也支持聚合/排序。ES 底层基于Lucene开发,针对Lucene的局限性,ES 提供了 RESTful API 风格的接口、支持分布式、可水平扩展,同时它可以被多种编程语言调用。

ES 很多基础概念以及底层实现其本质是 Lucene 的概念。

ps:本文所有的 dsl 查询、结果展示均基于 ES v7.7

历史背景

Lucene 的历史背景

下图这个人叫Doug Cutting,他是 Hadoop 语言和 Lucene 工具包的创始人。Doug Cutting 毕业于斯坦福大学,在 Xerox 积累了一定的工作经验后,从 1997 年开始,利用业余时间开发出了 Lucene。Lucene 面世于 1999 年,并于 2005 年成为 Apache 顶级开源项目。

Lucene的特点:

  • Lucene是基于 java 编写的,开源的全文检索引擎工具包
  • Lucene具有高性能:在相同的硬件环境下,基于 Hadoop 的 webmap(Lucene 的第一个应用) 的反应速度是之前系统的 33 倍

Lucene的局限性:

  • 仅限于 java 开发。
  • 类库的接口学习成本高:本质上Lucene就是一个编程库,可以按原始接口来调用,但是如果在应用程序中直接使用Lucene,需要覆盖大量的集成框架工作。
  • 原生并不支持水平扩展,若需实现海量数据的搜索引擎,需在此基础上格外开发以支持分布式。
ES 的历史背景

ElasticSearch创始人-Shay Banon

  • 2004 年,Shay Banon 基于 Lucene 开发了 Compass,在考虑 Compass 的第三个版本时,他意识到有必要重写 Compass 的大部分内容,以“创建一个可扩展的搜索解决方案”。因此,他创建了“一个从头构建的分布式解决方案”,并使用了一个公共接口,即 HTTP 上的 JSON,它也适用于 Java 以外的编程语言。
  • 2010 年,Shay Banon 在发布了 Elasticsearch 的第一个版本。

ES 多个版本可能出现破坏性变更,例如,在 6.x,ES 不允许一个 Index 中出现多个Type。在 ES 的官网,每个版本都对应着一个使用文档。

在使用 ES 之前,最好先了解 ES 的版本历史。下面列出一些比较重大的更新版本,可以在了解了基本概念之后再看。

  • 初始版本 0.7.0 2010 年 5 月 14 日
    • Zen Discovery 自动发现模块 - Groovy Client 支持 - 简单的插件管理机制 - 更好支持 ICU 分词器
  • 1.0.0 2014 年 2 月 14 日
    • 支持聚合分析 Aggregations
    • CAT API 支持
    • Doc values 引入
    • 支持联盟查询
    • 断路器支持
  • 2.0.0 2015 年 10 月 28 日
    • query/filter 查询合并,都合并到 query 中,根据不同的 context 执行不同的查询
    • 增加了 pipleline Aggregations 在 ES 中,有 Query 和 Filter 两种 Context - Query Context :相关性算分
    • Filter Context :不需要算分(YES OR NO), 可以利用 Cache 获得更好的性能
    • 存储压缩可配置
    • Rivers 模块被移除
    • Multicast 组播发现成为组件
  • 5.0.0 2016 年 10 月 26 日
    • Lucene 6.x 的支持,磁盘空间少一半;索引时间少一半;查询性能提升 25%;支持 IPV6。
    • Internal engine 级别移除了用于避免同一文档并发更新的竞争锁,带来 15%-20%的性能提升
    • Shrink API ,它可将分片数进行收缩成它的因数,如之前你是 15 个分片,你可以收缩成 5 个或者 3 个又或者 1 个,那么我们就可以想象成这样一种场景,在写入压力非常大的收集阶段,设置足够多的索引,充分利用 shard 的并行写能力,索引写完之后收缩成更少的 shard,提高查询性能
    • 引入新的字段类型 Text/Keyword 来替换 String
    • 提供了 Painless 脚本,代替 Groovy 脚本
    • 新增 Sliced Scroll 类型,现在 Scroll 接口可以并发来进行数据遍历了。每个 Scroll 请求,可以分成多个 Slice 请求,可以理解为切片,各 Slice 独立并行,利用 Scroll 重建或者遍历要快很多倍。- 限制索引请求大小,避免大量并发请求压垮 ES
    • 限制单个请求的 shards 数量,默认 1000 个
  • 6.0.0 2017 年 8 月 31 日
    • Index sorting,即索引阶段的排序。
    • 顺序号的支持,每个 es 的操作都有一个顺序编号(类似增量设计)
    • 无缝滚动升级
    • 逐步废弃 type,在 6.0 里面,开始不支持一个 index 里面存在多个 type
    • Index-template inheritance,索引版本的继承,目前索引模板是所有匹配的都会合并,这样会造成索引模板有一些冲突问题, 6.0 将会只匹配一个,索引创建时也会进行验证 - Load aware shard routing, 基于负载的请求路由,目前的搜索请求是全节点轮询,那么性能最慢的节点往往会造成整体的延迟增加,新的实现方式将基于队列的耗费时间自动调节队列长度,负载高的节点的队列长度将减少,让其他节点分摊更多的压力,搜索和索引都将基于这种机制。- 已经关闭的索引将也支持 replica 的自动处理,确保数据可靠。
  • 7.0.0 2019 年 4 月 10 日
    • 集群连接变化:TransportClient 被废弃 以至于,es7 的 java 代码,只能使用 restclient
    • 重大改进-正式废除单个索引下多 Type 的支持
    • es6 时,官方就提到了 es7 会删除 type,并且 es6 时已经规定每一个 index 只能有一个 type。在 es7 中使用默认的_doc 作为 type,官方说在 8.x 版本会彻底移除 type。api 请求方式也发送变化,如获得某索引的某 ID 的文档:GET index/_doc/id 其中 index 和 id 为具体的值
    • Lucene9.0 - 引入了真正的内存断路器,它可以更精准地检测出无法处理的请求,并防止它们使单个节点不稳定 - Zen2 是 Elasticsearch 的全新集群协调层,提高了可靠性、性能和用户体验,变得更快、更安全,并更易于使用 - 新功能 - New Cluster coordination - Feature - Complete High Level REST Client - Script Score Query - 性能优化 - Weak-AND 算法提高查询性能
    • 默认的 Primary Shared 数从 5 改为 1,避免 Over Sharding shard 也是一种资源,shard 过多会影响集群的稳定性。因为 shard 过多,元信息会变多,这些元信息会占用堆内存。shard 过多也会影响读写性能,因为每个读写请求都需要一个线程。所以如果 index 没有很大的数据量,不需要设置很多 shard。
    • 更快的前 k 个查询
    • 间隔查询(Intervals queries) 某些搜索用例(例如,法律和专利搜索)引入了查找单词或短语彼此相距一定距离的记录的需要。Elasticsearch 7.0 中的间隔查询引入了一种构建此类查询的全新方式,与之前的方法(跨度查询 span queries)相比,使用和定义更加简单。与跨度查询相比,间隔查询对边缘情况的适应性更强。

基础概念介绍

下图简单概述了 index、type、document 之间的关系,type 在新版本中废弃,所以画图时特殊标识了一下。

index

Index 翻译过来是索引的意思。在 ES 里,索引有两个含义:

  • 名词:一个索引相当于关系型数据库中的一个表(在 6.x 以前,一个 index 可以被认为是一个数据库)
  • 动词:将一份 document 保存在一个 index 里,这个过程也可以称为索引。
type

在 6.x 之前, index 可以被理解为关系型数据库中的【数据库】,而 type 则可以被认为是【数据库中的表】。使用 type 允许我们在一个 index 里存储多种类型的数据,数据筛选时可以指定 typetype 的存在从某种程度上可以减少 index 的数量,但是 type 存在以下限制:

  • 不同 type 里的字段需要保持一致。例如,一个 index 下的不同 type 里有两个名字相同的字段,他们的类型(string, date 等等)和配置也必须相同。
  • 只在某个 type 里存在的字段,在其他没有该字段的 type 中也会消耗资源。
  • 得分是由 index 内的统计数据来决定的。也就是说,一个 type 中的文档会影响另一个 type 中的文档的得分。

以上限制要求我们,只有同一个 index 的中的 type 都有类似的映射 (mapping) 时,才勉强适用 type 。否则,使用多个 type 可能比使用多个 index 消耗的资源更多。

这大概也是为什么 ES 决定废弃 type 这个概念,个人感觉 type 的存在,就像是一个语法糖,但是并未带来太大的收益,反而增加了复杂度。

document

index 中的单条记录称为 document (文档),可以理解为表中的一行数据。多条 document 组成了一个 index

代码语言:javascript复制
"hits" : {
    "total" : {
      "value" : 3563,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "test",
        "_type" : "_doc",
        "_id" : "3073",
        "_score" : 1.0,
        "_source" : {
   ...
   }
  }
]

上图为 ES 一条文档数据,其中:

  • _index :文档所属索引名称。
  • _type :文档所属类型名(此处已默认为_doc)。
  • _id :Doc 的主键。在写入的时候,可以指定该 Doc 的 ID 值,如果不指定,则系统自动生成一个唯一的 UUID 值。
  • _score :顾名思义,得分,也可称之为相关性,在查询是 ES 会 根据一些规则计算得分,并根据得分进行倒排。除此之外,ES 支持通过 Function score query 在查询时自定义 score 的计算规则。
  • _source :文档的原始 JSON 数据。
field

一个 document 会由一个或多个 field 组成,field 是 ES 中数据索引的最小定义单位,下面仅列举部分常用的类型。

⚠️ 在 ES 中,没有数组类型,任何字段都可以变成数组。

string
text
  • 索引全文值的字段,例如电子邮件正文产品描述
  • 如果您需要索引结构化内容,例如电子邮件地址、主机名、状态代码或标签,您可能应该使用 keyword 字段。
  • 出于不同目的,我们期望以不同方式索引同一字段,这就是 multi-fields 。例如,可以将 string 字段映射为用于全文搜索的 text 字段,并映射为用于排序或聚合的 keyword 字段:
代码语言:javascript复制
PUT my_index
{
  "mappings": {
    "properties": {
      "city": {
        "type": "text",
        "fields": {
          "raw": {
            "type":  "keyword"
          }
        }
      }
    }
  }
}
  • ⚠️text 字段默认无法进行排序或聚合
  • ⚠️ 使用text字段一定要使用合理的分词器
keyword
  • 用于索引结构化内容的字段,例如 ID、电子邮件地址、主机名、状态代码、邮政编码或标签。如果您需要索引全文内容,例如电子邮件正文或产品描述,你应该使用 text 字段。
  • 它们通常用于过滤(查找所有发布状态的博客文章)、排序和聚合keyword 字段只能精确匹配
numeric

long, integer, short, byte, double, float, half_float, scaled_float...

  • 就整数类型( byteshortintegerlong )而言,应该选择足以满足用例的最小类型。
  • 对于浮点类型,使用缩放因子将浮点数据存储到整数中通常更有效,这就是 scaled_float 类型的实现。
  • 下面这个 case, scaling_factor 缩放因子设置为 100,对于所有的 API 来说, price 看起来都像是一个双精度浮点数。但是对于 ES 内部,他其实是一个整数 long
代码语言:javascript复制
"price": {
        "type": "scaled_float",
        "scaling_factor": 100
      }
  • 如果 scaled_float 无法满足精度要求,可以使用 doublefloathalf_float
  • 不是所有的字段都适合存储为 numbericnumberic 类型更擅长 range 类查询,精确查询可以尝试使用 keyword
mapping

mapping 是一个定义 document 结构的过程, mapping 中定义了一个文档所包含的所有 field 信息。

定义字段索引过多会导致爆炸的映射,这可能会导致内存不足错误和难以恢复的情况, mapping 提供了一些配置对 field 进行限制,下面列举几个可能会比较常见的:

  • index.mapping.total_fields.limit 限制 field 的最大数量,默认值是 1000(field 和 object 内的所有字段,都会加入计数)。
  • index.mapping.depth.limit 限制 object 的最大深度,默认值是 20。
  • index.mapping.field_name_length.limit 限制中字段名的长度,默认是没有限制。
dynamic mapping

在索引 document 时,ES 的动态 mapping 会将新增内容中不存在的字段,自动的加入到映射关系中。ES 会自动检测新增字段的逻辑,并赋予其默认值。

  • One of the most important features of Elasticsearch is that it tries to get out of your way and let you start exploring your data as quickly as possible.
  • You know more about your data than Elasticsearch can guess, so while dynamic mapping can be useful to get started, at some point you will want to specify your own explicit mappings.

截取了部分 ES 官方文档中的话术,ES 认为一些自动化的操作会让新手上手更容易。但是同时,又提出,你肯定比 ES 更了解你的数据,可能刚开始使用起来觉得比较方便,但是最好还是自己明确定义映射关系。

0 人点赞