Elasticsearch 入门


二月其二,入门 Elasticsearch。

本文主要在写 Elasticsearch 是什么,捎带着写一点 Elasticsearch 怎么用。


Elasticsearch(简称 ES)是一款全文搜索引擎。

全文搜索(full-text search)的意思是从一大段文字中找出指定内容,比如从 100 篇文章中找出哪几篇包含“Hello world!”这句话,再比如百度、谷歌等搜索引擎也是。

Elasticsearch 有非常多的特点,下面罗列一些:

  • 分布式

    在多台机器上启动多个 Elasticsearch 进程实例,组成一个集群,可以扩展到上百台服务器,处理 PB 级数据。

    基于分布式,同时能够保证高可用(部分节点挂了,集群依然正常服务)和高伸缩性(副本分片数动态调整,提高读性能)。

  • 近实时查询(near real-time search)

    写数据后不能立即查到,过一段时间才能查到,默认 1 秒。

  • 基于 Lucene

    Lucene(音 [ˈluːsin])是一套开源的全文搜索程序库,基于 Java 编写,可以看做一个第三方 Java 库,使用起来不太方便。

    Elasticsearch 基于 Lucene 实现,并且做到高可用、分布式,它的每个分片都是一个功能齐全的 Lucene 索引。

  • REST APIs

    Elasticsearch 使用 RESTful 接口,例如查询 students 索引中 id=1 的记录:

    1
    GET students/_doc/1

    Elasticsearch 很喜欢自称使用 REST APIs,应该是因为 Lucene 用起来太复杂了。

  • Elastic Stack 的一部分

    Elasticsearch 的母公司是 Elastic,它们的核心产品是 Elastic Stack,一个由多个开源软件组成的软件栈:

    • Elasticsearch
    • Logstash(用于采集数据)
    • Kibana(用于数据可视化)
    • Beats(用于采集数据,轻量级+只采集日志)

    最开始只有 Elasticsearch、Logstash、Kibana,统称为 ELK,后来加入 Beats 后更名为 Elastic Stack。


Elasticsearch 历史

梳理一下 Elasticsearch 的历史。

Elasticsearch 的前身是 Compass,由 Shay Banon 在 2004 年开发,后来为了分布式和可扩展性,作者重写了一遍,命名 Elasticsearch(最初版本是 0.x),在 2014 年发布了 Elasticsearch 1.0。

2015 年 Elasticsearch 发布 2.0,官网上的中文教程《Elasticsearch: 权威指南》就是基于这个版本(其他版本乖乖看英文)。

Elasticsearch 为了和 Kibana 和 Logstash 统一版本号,没有 3.x 和 4.x,而直接是 5.x 版本。

前不久 Elasticsearch 刚刚发布 8.0,更新主要关于安全和机器学习:《Elastic 8.0: A new era of speed, scale, relevance, and simplicity》。

Elastic 官网上有相关发展史,可以一读:《发展历程》。


分享一个无用小知识:Elasticsearch 的前身 Compass,是作者失业后陪老婆去英国学厨,给老婆管理菜谱用的。

再分享一个无用小知识:Elasticsearch 的基础 Lucene,这个词没啥意思,是作者老婆的中间名。


启动 Elasticsearch

所有 Elasticsearch 的学习文档,上来都是教怎么安装 Elasticsearch 和 Kibana,洋洋洒洒一大篇。(因为 Elasticsearch 的操作界面是在 Kibana 上的,因此一般都同时安装 Kibana 并启动)

分享另一个官方思路(具体可以参考《Quick start》),使用 Docker 运行 Elasticsearch 和 Kibana,这样简单快速。

  1. 安装 Docker 并启动

  2. 执行下面的语句

    1
    2
    3
    4
    5
    6
    7
    # 拉取 Elasticsearch 镜像并启动
    docker network create elastic
    docker pull docker.elastic.co/elasticsearch/elasticsearch:8.0.0
    docker run --name es-node01 --net elastic -p 9200:9200 -p 9300:9300 -it docker.elastic.co/elasticsearch/elasticsearch:8.0.0
    # 拉取 Kibana 镜像并启动
    docker pull docker.elastic.co/kibana/kibana:8.0.0
    docker run --name kib-01 --net elastic -p 5601:5601 docker.elastic.co/kibana/kibana:8.0.0
  3. 打开 Kibana 控制台:http://localhost:5601/app/dev_tools#/console

    控制台默认就会有一个查询语句,用于查询所有数据。

    Kibana控制台

    也可以在 Kibana 上通过点击找到控制台:http://localhost:5601/ -> 左侧菜单栏 -> Management -> DevTools -> Console

然后就可以在 Kibana 控制台上操作 Elasticsearch 了。

Elasticsearch查询例子


基本概念

Elasticsearch 基础概念可以分为两部分,一部分关于数据库,一部分关于分布式。


数据库相关

如果简单地看待 Elasticsearch,它就是一个数据库(就像是 MySQL、MongoDB),同样有 表(Table)、数据行(Row)、查询语法(SQL) 等类似概念(但是术语不同)。

整理一下 Elasticsearch 与关系型数据库的术语对应关系:

关系型数据库 - 术语 Elasticsearch - 术语 Elasticsearch - 术语中文翻译 备注
Database Index 索引 Index 复数是 Indices
Table Type 类型 已废弃
6.0 开始,每个 Index 只能有一个 Type
8.0 开始,废弃使用 Type(每个 Index 只有一个默认不可改的 Type:_doc)
Row Document 文档 以 JSON 格式表示
Column Field 字段
Schema Mapping 映射/关联 可以不需要有建表这个行为
插入一条数据,将自动创建 Index、Field 并进行 mapping
SQL Query DSL 查询表达式,DSL (Domain Specific Language)

需要单独提一下 Elasticsearch 表的概念。

如果把 Elasticsearch 和 MySQL 做类比,一个数据库内包含多张表,一张表内有多条数据,关系应该是这样的:

  • MySQL => Databases => Tables => Columns/Rows
  • Elasticsearch => Indices => Types => Documents with Properties

但是,Elasticsearch 中的数据(Document)实际上是直接存储在数据库(Index)下的,表(Type)只是一个逻辑概念。

具体来讲:在 MySQL 中,不同 Table 下的 Column 是相互独立、没啥关系的(比如员工表和项目表,都可以有 name 字段 ),但是在 Elasticsearch 中,同一个 Index 下不可以有同名字段,即使是不一样的 Type,也不可以有重名的字段,Type 只是逻辑概念,字段是共用的。

Elasticsearch 8.0 开始正式废弃 Type。


我觉得 Elasticsearch 和 MySQL 等传统关系型数据库相比,有两个主要不同:

  1. Elasticsearch 是面向文档的 NoSQL 数据库(类似 MongoDB)。

    Elasticsearch和MySQL

  2. Elasticsearch 可以全文搜索(当然前提是高性能)。

    例如查询包含 error 一词的日志,SQL 语句类似如下(MySQL 肯定特别慢):

    1
    SELECT * FROM t_log WHERE content LIKE "%error%"

推荐阅读官方文档:《Data in: documents and indices》。


分布式相关

Elasticsearch 是一个分布式搜索引擎,通过在多台机器上启动 Elasticsearch 实例,组成一个 Elasticsearch 集群,实现高可用性和高伸缩性。

总体来讲,一个 Elasticsearch 集群(Cluster)由一个或多个节点(Node)组成,每个节点都是一个 Elasticsearch 实例。每个节点内部有任意多个分片(Primary Shard)和副本(Replica Shard),分片是把数据分布在多台机器上,用于突破单机限制,副本是冗余数据,用于高可用和提高吞吐量。

Elasticsearch 集群架构如下图所示(来自《Elasticsearch Sizing and Capacity Planning》):

ES分布式结构

  • 集群(Cluster)

    所有节点合在一起协同工作就是集群。(A collection of connected nodes is called a cluster.)

    集群有三种健康状态:green、yellow、red:

    • green:所有分片和副本都正常。
    • yellow:主分片正常运行(集群可以正常服务所有请求),但是副本没有全部正常运行(不是高可用状态)。
    • red:存在分片非正常运行(某些数据不可用),可能部分功能依然可用。
  • 节点(Node)

    每个节点都是一个 Elasticsearch 实例,由名称标识,启动时会生成唯一 UUID。

    每个节点都知道集群内的其他节点,并可以把请求转发给合适的节点。

    一台机器可以有多个节点,大多数情况下,每个节点运行在一个独立的容器或虚拟机里。

    根据分工的不同,节点有不同的角色(node.roles),具体可以参考官方文档:《Node》。

  • 分片(Primary Shard)

    Elasticsearch 是一个分布式搜索引擎,会把索引(文档的集合)拆分到不同的节点上,也就是分片,相当于一桶水用 N 个杯子装。

    每个节点内可以有零个、一个或多个分片,如下图所示(来自《Elasticsearch:权威指南 - 水平扩容》):

    ES分片

    实际上每个分片都是一个 Lucene Index,受 Lucene 底层限制,每个分片只能存放 Integer.MAX_VALUE - 128 个文档(21 亿多)。

  • 副本(Replica Shard)

    副本是分片的拷贝(A replica shard is just a copy of a primary shard.),也就是备份,不接受索引请求,可以用于查询。

    副本的作用一方面是容灾(高可用),另一方面副本可以用于查询,因此可以提高吞吐量(高性能)。

    分片和副本不会出现在同一个节点上(防止单点故障)。默认情况下将为每个分片创建一个副本(但只有一个节点的话就不会)。

推荐阅读《在ElasticSearch中,集群(Cluster),节点(Node),分片(Shard),Indices(索引),replicas(备份)之间是什么关系?》


疑难名词解释

有两个名词:分片、索引,在不同的语境下语义也不同,很容易绕晕,下面特别提一下。

分片(Shard)

上面提到的分片和副本,准确翻译应该是主分片(Primary Shard)和副本分片(Replica Shard)(A shard can be either a primary shard or a replica shard.),也就是说它们俩都是分片。

在中文语境下,我们一般说到 Elasticsearch 分片时,实际上指的是主分片,而不同时指代两者,也就是分片用于分布式存储数据,而不是用于备份冗余。

索引(Index)

索引是一种预排序的数据结构,是数据库常见的概念。但是在不同的语境下,索引可能有特殊的含义。

  • MySQL 索引

    我们通常想到索引时,想到的都是 MySQL 索引,代表着关系型数据库的存储数据结构,我们可以通过索引找到数据。

  • Elasticsearch 索引

    在 Elasticsearch 的语境下,通常把索引理解成文档的集合(An Index is a group of documents.),而不光是一种数据结构。换句话说,Elasticsearch 认为索引 = 数据库,它代表着一个大容器。

    一个 Elasticsearch 索引分布在多个分片上,每个分片都是一个 Lucene 索引(这就是下面要提到的)。

  • Lucene 索引

    在 Elasticsearch 语境下,分片 = Lucene 索引。

    Lucene 索引内部由 Lucene 段(segment)和提交点(commit point)组成(the word index in Lucene means “a collection of segments plus a commit point”.)。当我们看到 segment 这个词时,可以简单理解为 Elasticsearch 底层的数据存储结构。

    当学习 Elasticsearch 新增、查询数据时,会再次见到 Lucene 索引,到时具体分析 segment 的设计。

下图是 Elasticsearch 索引和 Lucene 索引的关系(来自《Getting Started with Elasticsearch》)。

ES索引与Lucene索引


全文搜索的底层:倒排索引

全文搜索是通过倒排索引实现的。

倒排索引对应于正排索引,下面举一个例子(这个例子来自于 ELasticsearch 官方文档《倒排索引》)。

假设有两个文档,每个文档的内容如下:

  1. The quick brown fox jumped over the lazy dog.
  2. Quick brown foxes leap over lazy dogs in summer.

通常用 MySQL 存储这两个文档,会这么存储(也就是正排索引):

doc_id content
1 The quick brown fox jumped over the lazy dog.
2 Quick brown foxes leap over lazy dogs in summer.

倒排索引换了一个思路,把每个文档拆碎成一个个词,记录每个词在哪篇文档里出现过:

term doc_1 doc_2
Quick ✅
The ✅
brown ✅ ✅
dog ✅
dogs ✅
fox ✅
foxes ✅
in ✅
jumped ✅
lazy ✅ ✅
leap ✅
over ✅ ✅
quick ✅
summer ✅
the ✅

使用倒排索引,我们能够根据词,找到哪些文档中包含这些词,从而实现全文搜索。试想一下,从 TB 量级的日志中,搜索包含某关键字的日志,使用倒排索引是不是很合适。

上面例子中的倒排索引,是一个最简单的倒排索引,实际上 Elasticsearch 还记录了词频(某个词在某文档中出现的次数,毕竟可能出现多次)、出现位置(是文档的第几个词)等信息。


Elasticsearch 有很多种数据类型(布尔、数字、地理位置等),其中使用倒排索引进行全文搜索的数据类型是 text(这应该是 Elasticsearch 最重要的数据结构了)。

Elasticsearch 把文本拆成词加入倒排索引的过程,叫做 Text analysis(文本分析),这部分组件叫做 analyzer。

文本分析主要分为下面三步(官方文档《Anatomy of an analyzer》):

  1. Character filters

    特殊字符转换过滤。比如删除标点符号,又比如把 & 替换成 and。

  2. Tokenizer

    分词。把文本拆成多个词或词组,不同的 analyzer 拆分逻辑不一样,取决于内部的分词器 tokenizer。

    顺便一提,拆分出的词/词组,叫做 term 或 token。

  3. Token fiters

    分词结果转换过滤。比如把 Name 替换成 name,把无效词 the 删掉。

下图很好地概括了文本分析(来自《Elasticsearch in Action》):

ES文本分析

可以使用 Analyze API 测试 analyzer 是如何解析文本的,例如:

ES Analyze-API

有关 Elasticsearch 文本分析,还可以参考社区文章《Elasticsearch: analyzer》。


Elasticsearch 实操

试着操作一下最简单的 CRUD:

  1. 在 user 索引中增加一个文档:name 属性是 pz,age 属性是 24。
  2. 查询这个文档

Elasticsearch查询例子

新增和查询命令的语法都很简单,看一下就能明白怎么回事,更复杂的 CRUD 之后再学习。

只提两个地方:

  1. 没有建表

    Elasticsearch 是 scheme-less 的,不需要显式建表,如果 index 不存在,在创建第一条数据时,会自动创建 index、field 等内容,并自动关联上去。

    当然也可以在写入第一条数据之前,先创建好 index 并规定好数据结构(生产环境肯定要这么做)。

    如果纯靠 Elasticsearch 自动创建 index 和数据结构,会按照默认规则创建,可能会有差错(比如地理位置字段被识别成文本字段),而当文档已经存在之后,数据结构就不能再修改了。

  2. Response body(返回体)包含元数据

    Elasticsearch 的查询结果,除了文档本身,还有用于描述文档信息的元数据(meta-fields),它们以下划线开头(但是为什么有的字段没有下划线,我还没搞清楚)。

    以查询文档的 GET 请求返回为例(可以参考官网文档《Get API - Response body》):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    {
    "_index" : "user", // 文档(document)所在索引(index)的名称
    "_type" : "_doc", // 文档类型,Elasticsearch 8.0 之后只能是 _doc,具体可见上文[基本概念-数据库相关]部分
    "_id" : "1", // 文档 ID
    "_version" : 1, // 文档版本号(document 的所有变动都会 +1)
    "_seq_no" : 1, // 文档所在索引的版本号(index 的所有变动都会 +1)
    "_primary_term" : 1, // 文档所在分片(shard)的编号
    "found" : true, // 是否找到,只能是 true/false
    "_source" : { // 文档内容
    "name" : "pz",
    "age" : 24
    }
    }

Elasticsearch 学习资料

整理一下学习资料。

  1. 官网文档

    《Elasticsearch Guide》英文最新教学文档

    《Elasticsearch: 权威指南》中文文档,版本 2.x

  2. Elastic 中国社区官方博客

    《Elastic:开发者上手指南》中文社区在 CSDN 上笔耕不辍,这篇是目录

    《Elasticsearch:inverted index,doc_values 及 source》倒排索引的存储底层

    《Elasticsearch: analyzer》分词器

  3. 第三方博客

    《Elasticsearch 高手之路》xiaoxiami 的博客,内容不全面,选择性阅读