Elasticsearch(ES) 是一个基于 Apache Lucene 开源的分布式、高扩展、近实时的数据搜索与分析引擎,主要用于海量数据快速存储,实时检索,高效分析的场景。通过简单易用的 RESTful API,隐藏 Lucene 的复杂性,让全文搜索变得简单。
ES 功能总结有三点:
因为是分布式,可将海量数据分散到多台服务器上存储,检索和分析,只要是海量数据需要完成上面这三种操作的业务场景,一般都会考虑使用 ES,比如维基百科,Stack Overflow,GitHub 后台均有使用。 GitHub。
ES 为什么这么受欢迎,得益于其相较于传统数据库所拥有的强大功能。
说到 ES 必须要提一下 Kibana 。
ES 和 Logstash,Kibana 共同组成 ELK,ELK 是这三个开源项目的首字母缩写。ES 是一个搜索和分析引擎,Logstash 是服务器端数据处理管道,能够同时从多个来源采集数据,转换数据,然后将数据发送到诸如 ES 等存储库中。Kibana 则可以让用户在 Elasticsearch 中使用图形和图表对数据进行可视化。
Kibana 以 Web 后台的形式提供了一个可视化操作 ES 的系统,支持根据 ES 数据绘制图表,支持 ES 查询语法自动补全等高级特性,更加方便了我们操作 ES。
要想完成对 ES 的增删改查,必须先了解一下 ES 的存储结构。
大家对 MySQL 的存储结构应该是很清楚的,所以咱们在学习 ES 存储结构时,同时类比 MySQL,这样理解起来会更透彻。MySQL 的数据模型由数据库、表、字段、字段类型组成,自然 ES 也有自己的一套存储结构。
ES 存储结构 与 MySQL 存储结构的对应关系。
ES存储结构 | MySQL存储结构 |
---|---|
Index | 表 |
Document | 行 |
Field | 表字段 |
Mapping | 表结构定义 |
索引(index)类似 MySQL 的表,是文档(document)的集合。文档是 ES 中存储的一条数据,下面会详细介绍。
type 为文档类型,不过在 ES 7.0 以后的版本 已经废弃文档类型了,一个 index 中只有一个默认的 type,即 _doc。在 ES 老版本中文档类型代表一类文档的集合,index 类似 MySQL 的数据库,文档类型类似 MySQL 的表。既然 ES 新版本文档类型没什么作用了,那么 index(索引)就类似 MySQL 表的概念,ES 没有数据库的概念了。
ES 是面向文档的数据库,文档是 ES 存储的最基本的存储单元,文档类似 MySQL 表中的一行数据。在 ES 中,文档使用 JSON 格式存储,因此存储上要比 MySQL 灵活很多,因为 ES 支持任意格式的 JSON 数据。
{
"_index" : "order",
"_type" : "_doc",
"_id" : "1",
"_version" : 2,
"_seq_no" : 1,
"_primary_term" : 1,
"found" : true,
"_source" : {
"id" : 10000,
"status" : 0,
"total_price" : 10000,
"create_time" : "2020-09-06 17:30:22",
"user" : {
"id" : 10000,
"username" : "asong2020",
"phone" : "888888888",
"address" : "深圳人才区"
}
}
}
其中 _source 为记录的具体内容,其他字段为文档的元数据,是插入 JSON 记录时 ES 自动生成的系统字段,二者共同组成一个 document。
常用的元数据有:
文档由多个 JSON 字段组成,字段跟 MySQL 中表的字段类似,常用字段类型有:
mapping 类似于 MySQL 的表结构体定义,每个索引都有一个映射的规则,我们可以通过定义索引的映射规则,提前定义好文档的 JSON 结构和字段类型。如果没有定义索引的 mapping,ES 会在写入数据的时候,根据我们写入的数据字段推测出对应的字段类型,相当于自动定义索引的 mapping 。
注意: ES 的自动映射很方便,但是实际业务中,对于关键字段类型,通常预先定义好,这样可以避免 ES 自动生成不是你想要的字段类型。
除了数据结构的相关概念,因 ES 是一个分布式支持水平扩展的数据库系统,必然少不了分布式相关的概念,这个最好也需要了解一下。
一个集群由一个或多个节点组成,它们共同持有数据,一起提供存储搜索功能。
集群由一个唯一的名字进行区分,默认为"elasticsearch",集群中的节点通过整个唯一的名字加入集群。
节点是 ES 集群的一部分,只要多个节点在同个网络中,节点就可以通过指定集群的名称加入其中,与集群中的其他节点相互感知。
和集群类似,一个节点也是由一个名字来标识的。默认情况下,这个名字是一个随机的漫威漫画角色名字,这个名字会在启动的时候赋予节点。这个名字对于管理工作来说挺重要的,因为在这个管理过程中,你会去确定网络中的哪些服务器对应于 ES 集群中的哪些节点。
索引可以存储大量的数据,这些数据可能超过单个节点的硬件限制。为了解决这一问题,ES 提供细分索引的能力,即分片(shards)。
ES 可以把一个完整的索引分成多个分片,这样的好处是可以把一个大的索引拆分成多个,分布到不同的节点上,构成分布式搜索。分片的数量只能在索引创建前指定,并且索引创建后不能更改。
至于一个分片怎样分布,它的文档怎样聚合回搜索请求,完全由 ES 管理,对于作为用户来说,这些都是透明的。
在一个网络环境里,节点故障随时都可能发生,在某个分片/节点出现故障时,有一个备份机制是非常有用的。为此 ES 允许你为分片创建一份或多份拷贝,这些拷贝叫做副本(replicas)。
副本之所以重要,主要有两方面的原因:一是提高系统的容错性,当某个节点某个分片损坏或丢失时可以从副本中恢复。二是提高查询效率,ES 会自动对搜索请求进行负载均衡。
总之,每个索引可以被分成多个分片。一个索引也可以被复制 0 次(意思是没有复制)或多次。一旦复制了,每个索引就有了主分片和副分片(主分片的拷贝)。分片和复本的数量可以在索引创建的时候指定。在索引创建之后,你可以在任何时候动态地改变副本数量,但是不能改变分片的数量。
注意: 默认情况下,ES 中的每个索引被分为 5 个主分片和 1 份拷贝。不过从 7.x 版本开始,主分片由 5 改为了 1 个。如果你的集群中至少有两个节点,你的索引将会有 5 个主分片和另外 5 个副分片,这样的话每个索引总共就有 10 个分片。一个索引的多个分片可以存放在集群中的一台主机上,也可以分散存放在多台主机上,这取决于你的集群机器数量。主分片和副分片的具体位置是由 ES 内在的策略所决定的。
ES 的 Go 客户端较为流行的有 Elastic 公司官方库go-elasticsearch 和第三方库 olivere/elastic,后者较为流行。
因项目中使用的是 olivere/elastic/v7,所以本文将介绍通过该库完成对 ES 的增删改查。
注意,ES 不同版本需要使用对应版本的 olivere/elastic 包,对应关系如下:
Elasticsearch version | Elastic version | Package URL | Remarks |
---|---|---|---|
7.x | 7.0 | github.com/olivere/elastic/v7 | Use Go modules |
6.x | 6.0 | github.com/olivere/elastic | Use a dependency manager |
5.x | 5.0 | gopkg.in/olivere/elastic.v5 | Actively maintained |
本次使用 ES 的 7.x 的版本,所以使用 github.com/olivere/elastic/v7 作为客户端库,使用 go.mod 来管理依赖:
require(
github.com/olivere/elastic/v7 v7.0.24
)
前面铺垫了这么多基础概念,下面正式开始 Go ES 的增删改查。
在开始实战之前,先介绍一下本文代码示例要实现的功能:
在进行开发之前,需要创建一个 client,用于操作 ES。这里使用单例模式来实现。
// ES 客户端
var (
esOnce sync.Once
esCli *elastic.Client
)
// GetESClient 获取 ES client
func GetESClient() *elastic.Client {
if esCli != nil {
return esCli
}
esOnce.Do(func() {
cli, err := elastic.NewSimpleClient(
elastic.SetURL("http://test.es.db"), // 服务地址
elastic.SetBasicAuth("user", "secret"), // 账号密码
elastic.SetErrorLog(log.New(os.Stderr, "", log.LstdFlags)), // 设置错误日志输出
elastic.SetInfoLog(log.New(os.Stdout, "", log.LstdFlags)), // 设置info日志输出
)
if err != nil {
panic("new es client failed, err=" + err.Error())
}
esCli = cli
})
return esCli
}
这里创建 ES client 是使用的 NewSimpleClient()
这个方法进行实现的,当然也可以使用另外两个方法:
// NewClient creates a new client to work with Elasticsearch.
func NewClient(options ...ClientOptionFunc) (*Client, error)
// NewClientFromConfig initializes a client from a configuration
func NewClientFromConfig(cfg *config.Config) (*Client, error)
创建时可以提供 ES 连接参数。上面列举的不全,下面给大家介绍一下。
elastic.SetURL(url) 用来设置ES服务地址,如果是本地,就是127.0.0.1:9200。支持多个地址,用逗号分隔即可
elastic.SetBasicAuth("user", "secret") 这个是基于http base auth 验证机制的账号密码
elastic.SetGzip(true) 启动 gzip 压缩
elastic.SetHealthcheckInterval(10*time.Second) 用来设置监控检查时间间隔
elastic.SetMaxRetries(5) 设置请求失败最大重试次数,v7 版本以后已被弃用
elastic.SetSniff(false) 设置是否定期检查集群(默认为true)
elastic.SetErrorLog(log.New(os.Stderr, " ", log.LstdFlags)) 设置错误日志输出
elastic.SetInfoLog(log.New(os.Stdout, "", log.LstdFlags)) 设置info日志输出
上一步,我们创建了 client,接下来我们创建 index 和对应的 mapping。mapping 用于描述 document 的 JSON 结构和字段类型。
设计一个 mapping 用来描述我们要存储的用户信息,mapping 也用一个 JSON 串来表示。
ESMapping = `{
"mappings":{
"dynamic": "strict",
"properties":{
"id": { "type": "long" },
"username": { "type": "keyword" },
"nickname": { "type": "text" },
"phone": { "type": "keyword" },
"age": { "type": "long" },
"ancestral": { "type": "text" },
"identity": { "type": "text" },
"update_time": { "type": "long" },
"create_time": { "type": "long" }
}
}
}`
一般的,mapping 可以分为动态映射(dynamic mapping)和静态(显式)映射(explicit mapping)和精确(严格)映射(strict mappings),具体由 dynamic 属性控制。
其中 "dynamic": "strict"
表示字段需要严格匹配,新增或类型不一致写入将会报错。
索引名称定义为:index = es_index_userinfo
。
设计好了 index 及 mapping 后,我们开始编写代码进行创建:
// ESIndexExists 索引是否存在
func ESIndexExists(ctx context.Context, index string) (bool, error) {
return GetESClient().IndexExists(index).Do(ctx)
}
// CrtESIndex 创建 ES 索引
func CrtESIndex(ctx context.Context, index, mapping string) error {
exist, err := ESIndexExists(ctx, index)
if err != nil {
return err
}
// 已经创建
if exist {
return nil
}
// 重复创建会报错
_, err = GetESClient().CreateIndex(index).BodyString(mapping).Do(ctx)
return err
}
因为重复创建 index,ES 会报错,所以创建前先判断一下是否已经创建。
创建成功后,我们在 Kibana 上通过 Restful API 可以查看到刚刚创建的 index。
GET /es_index_userinfo
其中 number_of_shards 为主分片数,缺省为 1,只能在创建索引时指定,后期无法修改。number_of_replicas 是指每个分片有多少个副本,后期可以动态修改。
对应的 RESTful API 为:
PUT /es_index_userinfo
{
"mappings":{
"dynamic": "strict",
"properties":{
"id": { "type": "long" },
"username": { "type": "keyword" },
"nickname": { "type": "text" },
"phone": { "type": "keyword" },
"age": { "type": "long" },
"ancestral": { "type": "text" },
"identity": { "type": "text" },
"update_time": { "type": "long" },
"create_time": { "type": "long" }
}
}
}
本文从 ES 的基本概念讲起,然后通过包 github/olivere/elastic/v7 完成 ES 基本的增删改查,并给出对应的 RESTful API 操作语句。
ES 功能太过强大,以至于客户端接口也极其繁多,再加上缺少通俗易懂的说明文档,真地让人眼花缭乱,对初学者来说使用起来并不那么友好。
关于 ES 的搜索能力,本文并未给出过多的相关的示例,后续有机会再另起博文续更。
github/elastic/elasticsearch github/olivere/elastic/v7 pkg.go.dev/github.com/olivere/elastic/v7 百度百科.elasticsearch 云+社区.Elasticsearch数据更新全方位解析 golang elasticsearch入门教程 golang elasticsearch 查询教程 Elasticsearch: 权威指南 go-ElasticSearch入门看这一篇就够了(一) Golang梦工厂.go-elasticSearch 实战篇,带你学会增删改查 Elasticsearch - mappings之dynamic的三种状态 Elasticsearch Guide [7.13] » REST APIs » Document APIs » ?refresh Elasticsearch Guide [7.13] » REST APIs » Document APIs » Delete by query API Elasticsearch Guide [7.14] » Search your data » Retrieve selected fields from a search Elasticsearch Guide [7.14] » Query DSL » Compound queries » Boolean query Elasticsearch Guide [7.x] » Mapping » Field data types