Go操作Elasticsearch
Elasticsearch
Elasticsearch
下载
https://www.elastic.co/cn/start
运行
解压后cd到解压目录 ./bin/elasticsearch
介绍
Elasticsearch(ES)是一个基于Lucene构建的开源、分布式、RESTful接口的全文搜索引擎。Elasticsearch还是一个分布式文档数据库,其中每个字段均可被索引,而且每个字段的数据均可被搜索,ES能够横向扩展至数以百计的服务器存储以及处理PB级的数据。可以在极短的时间内存储、搜索和分析大量的数据。通常作为具有复杂搜索场景情况下的核心发动机。
Elasticsearch能做什么
- 当你经营一家网上商店,你可以让你的客户搜索你卖的商品。在这种情况下,你可以使用ElasticSearch来存储你的整个产品目录和库存信息,为客户提供精准搜索,可以为客户推荐相关商品。
- 当你想收集日志或者交易数据的时候,需要分析和挖掘这些数据,寻找趋势,进行统计,总结,或发现异常。在这种情况下,你可以使用Logstash或者其他工具来进行收集数据,当这引起数据存储到ElasticsSearch中。你可以搜索和汇总这些数据,找到任何你感兴趣的信息。
- 对于程序员来说,比较有名的案例是GitHub,GitHub的搜索是基于ElasticSearch构建的,在github.com/search页面,你可以搜索项目、用户、issue、pull request,还有代码。共有40~50个索引库,分别用于索引网站需要跟踪的各种数据。虽然只索引项目的主分支(master),但这个数据量依然巨大,包括20亿个索引文档,30TB的索引文件。
Elasticsearch基本概念
Near Realtime(NRT) 几乎实时
Elasticsearch是一个几乎实时的搜索平台。意思是,从索引一个文档到这个文档可被搜索只需要一点点的延迟,这个时间一般为毫秒级。
Cluster 集群
群集是一个或多个节点(服务器)的集合, 这些节点共同保存整个数据,并在所有节点上提供联合索引和搜索功能。一个集群由一个唯一集群ID确定,并指定一个集群名(默认为“elasticsearch”)。该集群名非常重要,因为节点可以通过这个集群名加入群集,一个节点只能是群集的一部分。
确保在不同的环境中不要使用相同的群集名称,否则可能会导致连接错误的群集节点。例如,你可以使用logging-dev、logging-stage、logging-prod分别为开发、阶段产品、生产集群做记录。
Node节点
节点是单个服务器实例,它是群集的一部分,可以存储数据,并参与群集的索引和搜索功能。就像一个集群,节点的名称默认为一个随机的通用唯一标识符(UUID),确定在启动时分配给该节点。如果不希望默认,可以定义任何节点名。这个名字对管理很重要,目的是要确定你的网络服务器对应于你的ElasticSearch群集节点。
我们可以通过群集名配置节点以连接特定的群集。默认情况下,每个节点设置加入名为“elasticSearch”的集群。这意味着如果你启动多个节点在网络上,假设他们能发现彼此都会自动形成和加入一个名为“elasticsearch”的集群。
在单个群集中,你可以拥有尽可能多的节点。此外,如果“elasticsearch”在同一个网络中,没有其他节点正在运行,从单个节点的默认情况下会形成一个新的单节点名为”elasticsearch”的集群。
Index索引
索引是具有相似特性的文档集合。例如,可以为客户数据提供索引,为产品目录建立另一个索引,以及为订单数据建立另一个索引。索引由名称(必须全部为小写)标识,该名称用于在对其中的文档执行索引、搜索、更新和删除操作时引用索引。在单个群集中,你可以定义尽可能多的索引。
Type类型
在索引中,可以定义一个或多个类型。类型是索引的逻辑类别/分区,其语义完全取决于你。一般来说,类型定义为具有公共字段集的文档。例如,假设你运行一个博客平台,并将所有数据存储在一个索引中。在这个索引中,你可以为用户数据定义一种类型,为博客数据定义另一种类型,以及为注释数据定义另一类型。
Document文档
文档是可以被索引的信息的基本单位。例如,你可以为单个客户提供一个文档,单个产品提供另一个文档,以及单个订单提供另一个文档。本文件的表示形式为JSON(JavaScript Object Notation)格式,这是一种非常普遍的互联网数据交换格式。
在索引/类型中,你可以存储尽可能多的文档。请注意,尽管文档物理驻留在索引中,文档实际上必须索引或分配到索引中的类型。
Shards & Replicas分片与副本
索引可以存储大量的数据,这些数据可能超过单个节点的硬件限制。例如,十亿个文件占用磁盘空间1TB的单指标可能不适合对单个节点的磁盘或可能太慢服务仅从单个节点的搜索请求。
为了解决这一问题,Elasticsearch提供细分你的指标分成多个块称为分片的能力。当你创建一个索引,你可以简单地定义你想要的分片数量。每个分片本身是一个全功能的、独立的“指数”,可以托管在集群中的任何节点。
Shards分片的重要性主要体现在以下两个特征:
- 分片允许你水平拆分或缩放内容的大小
- 分片允许你分配和并行操作的碎片(可能在多个节点上)从而提高性能/吞吐量 这个机制中的碎片是分布式的以及其文件汇总到搜索请求是完全由ElasticSearch管理,对用户来说是透明的。
在同一个集群网络或云环境上,故障是任何时候都会出现的,拥有一个故障转移机制以防分片和节点因为某些原因离线或消失是非常有用的,并且被强烈推荐。为此,Elasticsearch允许你创建一个或多个拷贝,你的索引分片进入所谓的副本或称作复制品的分片,简称Replicas。
Replicas的重要性主要体现在以下两个特征:
- 副本为分片或节点失败提供了高可用性。为此,需要注意的是,一个副本的分片不会分配在同一个节点作为原始的或主分片,副本是从主分片那里复制过来的。
- 副本允许用户扩展你的搜索量或吞吐量,因为搜索可以在所有副本上并行执行。
ES基本概念与关系型数据库的比较
ES概念 | 关系型数据库 |
---|---|
Index(索引)支持全文检索 | Database(数据库) |
Type(类型) | Table(表) |
Document(文档),不同文档可以有不同的字段集合 | Row(数据行) |
Field(字段) | Column(数据列) |
Mapping(映射) | Schema(模式) |
ES API
以下示例使用curl
演示。
查看健康状态
curl -X GET 127.0.0.1:9200/_cat/health?v
输出:
epoch timestamp cluster status node.total node.data shards pri relo init unassign pending_tasks max_task_wait_time active_shards_percent
1564726309 06:11:49 elasticsearch yellow 1 1 3 3 0 0 1 0 - 75.0%
查询当前es集群中所有的indices
curl -X GET 127.0.0.1:9200/_cat/indices?v
输出:
health status index uuid pri rep docs.count docs.deleted store.size pri.store.size
green open .kibana_task_manager LUo-IxjDQdWeAbR-SYuYvQ 1 0 2 0 45.5kb 45.5kb
green open .kibana_1 PLvyZV1bRDWex05xkOrNNg 1 0 4 1 23.9kb 23.9kb
yellow open user o42mIpDeSgSWZ6eARWUfKw 1 1 0 0 283b 283b
创建索引
curl -X PUT 127.0.0.1:9200/www
输出:
{"acknowledged":true,"shards_acknowledged":true,"index":"www"}
删除索引
curl -X DELETE 127.0.0.1:9200/www
输出:
{"acknowledged":true}
插入记录
curl -H "ContentType:application/json" -X POST 127.0.0.1:9200/user/person -d '
{
"name": "dsb",
"age": 9000,
"married": true
}'
输出:
{
"_index": "user",
"_type": "person",
"_id": "MLcwUWwBvEa8j5UrLZj4",
"_version": 1,
"result": "created",
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
},
"_seq_no": 3,
"_primary_term": 1
}
也可以使用PUT方法,但是需要传入id
curl -H "ContentType:application/json" -X PUT 127.0.0.1:9200/user/person/4 -d '
{
"name": "sb",
"age": 9,
"married": false
}
检索
Elasticsearch的检索语法比较特别,使用GET方法携带JSON格式的查询条件。
全检索:
curl -X GET 127.0.0.1:9200/user/person/_search
按条件检索:
curl -H "ContentType:application/json" -X PUT 127.0.0.1:9200/user/person/4 -d '
{
"query":{
"match": {"name": "sb"}
}
}
ElasticSearch默认一次最多返回10条结果,可以像下面的示例通过size字段来设置返回结果的数目。
curl -H "ContentType:application/json" -X PUT 127.0.0.1:9200/user/person/4 -d '
{
"query":{
"match": {"name": "sb"},
"size": 2
}
}
Go操作Elasticsearch
elastic client
我们使用官方包 https://github.com/elastic/go-elasticsearch 来连接ES并进行操作。
注意下载与你的ES相同版本的client,例如我们这里使用的ES是7.2.1的版本,那么我们下载的client也要与之对应为github.com/olivere/elastic/v7
。
$ go get github.com/elastic/go-elasticsearch/v7 // 7.x
连接ES
默认连接
package main import ( "github.com/elastic/go-elasticsearch" "log" ) // es操作 // 创建客户端 func main() { // 连接es // NewDefaultClient 默认客户端 127.0.0.1:9200 es, err := elasticsearch.NewDefaultClient() if err != nil { log.Fatalf("Error creating the client: %s", err) } // 获取 当前es的详细信息 res, err := es.Info() if err != nil { log.Fatalf("Error getting response: %s", err) } defer res.Body.Close() log.Printf("%#v\n",res) log.Printf("%v\n",res) }
配置文件连接
package main import ( "github.com/elastic/go-elasticsearch" "log" ) // es操作 // 创建客户端 func main() { // 连接es // elasticsearch.Config 配置ES连接属性 /* type Config struct { Addresses []string // 要使用的Elasticsearch节点列表. Username string // HTTP基本身份验证的用户名. Password string // HTTP基本认证密码. CloudID string // 弹性服务的端点 (https://elastic.co/cloud). APIKey string // 用于授权的Base64编码令牌;如果设置,则覆盖用户名和密码. RetryOnStatus []int // 重试状态代码列表. 默认: 502, 503, 504. DisableRetry bool // 默认: false. EnableRetryOnTimeout bool // 默认: false. MaxRetries int // 默认: 3. DiscoverNodesOnStart bool // 初始化客户端时发现节点. Default: false. DiscoverNodesInterval time.Duration // 定期发现节点. Default: disabled. EnableMetrics bool // 启用指标收集. EnableDebugLogger bool // 启用调试日志记录. RetryBackoff func(attempt int) time.Duration // 可选的退避时间. 默认: nil. Transport http.RoundTripper // HTTP传输对象. Logger estransport.Logger // 记录器对象. Selector estransport.Selector // 选择器对象. // 自定义ConnectionPool的可选构造函数. Default: nil. ConnectionPoolFunc func([]*estransport.Connection, estransport.Selector) estransport.ConnectionPool } */ cfg := elasticsearch.Config{ Addresses: []string{ "http://localhost:9200", "http://localhost:9201", }, // ... } es, err := elasticsearch.NewClient(cfg) if err != nil { log.Fatalf("Error creating the client: %s", err) } // 获取 当前es的详细信息 res, err := es.Info() if err != nil { log.Fatalf("Error getting response: %s", err) } defer res.Body.Close() log.Printf("%#v\n", res) log.Printf("%v\n", res) }
增删改查
package main import ( "bytes" "context" "encoding/json" "github.com/elastic/go-elasticsearch" "github.com/elastic/go-elasticsearch/esapi" "log" "strconv" "strings" "sync" ) // es操作 // 创建客户端 func main() { // 连接es cfg := elasticsearch.Config{ Addresses: []string{ "http://localhost:9200", "http://localhost:9201", }, // ... } // 创建客户端 es, err := elasticsearch.NewClient(cfg) if err != nil { log.Fatalf("Error creating the client: %s", err) } // 获取 es集群消息 res, err := es.Info() if err != nil { log.Fatalf("Error getting response: %s", err) } log.Println(res) defer res.Body.Close() // 检查回应状态 if res.IsError() { log.Fatalf("Error: %s", res.String()) } // 将响应转化为map var ( r map[string]interface{} //wg sync.WaitGroup ) if err := json.NewDecoder(res.Body).Decode(&r); err != nil { log.Fatalf("Error parsing the response body: %s", err) } // 打印序列化的map r log.Printf("%#v", r) // 打印客户端和服务器版本号. log.Printf("Client: %s", elasticsearch.Version) log.Printf("Server: %v", r["version"].(map[string]interface{})["number"]) log.Println(strings.Repeat("~", 37)) // 创建索引 && 文件 var wg sync.WaitGroup // 循环list for i, title := range []string{"Test One", "Test Two"} { wg.Add(1) go func(i int, title string) { defer wg.Done() // 建立请求主体. // Builder创建json串 var b strings.Builder b.WriteString(`{"title" : "`) b.WriteString(title) b.WriteString(`"}`) // 设置请求对象. req := esapi.IndexRequest{ Index: "test", DocumentID: strconv.Itoa(i + 1), Body: strings.NewReader(b.String()), Refresh: "true", } // 与客户端执行请求. res, err := req.Do(context.Background(), es) if err != nil { log.Fatalf("Error getting response: %s", err) } defer res.Body.Close() if res.IsError() { log.Printf("[%s] Error indexing document ID=%d", res.Status(), i+1) } else { // Deserialize the response into a map. var r map[string]interface{} if err := json.NewDecoder(res.Body).Decode(&r); err != nil { log.Printf("Error parsing the response body: %s", err) } else { // 打印响应状态和索引文档版本. log.Printf("[%s] %s; version=%d", res.Status(), r["result"], int(r["_version"].(float64))) } } }(i, title) } wg.Wait() log.Println(strings.Repeat("-", 37)) // 搜索索引文件 // 建立请求主体. var buf bytes.Buffer query := map[string]interface{}{ "query": map[string]interface{}{ "match": map[string]interface{}{ "title": "test", }, }, } if err := json.NewEncoder(&buf).Encode(query); err != nil { log.Fatalf("Error encoding query: %s", err) } // 执行搜索请求. res, err = es.Search( es.Search.WithContext(context.Background()), es.Search.WithIndex("test"), es.Search.WithBody(&buf), es.Search.WithTrackTotalHits(true), es.Search.WithPretty(), ) if err != nil { log.Fatalf("Error getting response: %s", err) } defer res.Body.Close() if res.IsError() { var e map[string]interface{} if err := json.NewDecoder(res.Body).Decode(&e); err != nil { log.Fatalf("Error parsing the response body: %s", err) } else { // Print the response status and error information. log.Fatalf("[%s] %s: %s", res.Status(), e["error"].(map[string]interface{})["type"], e["error"].(map[string]interface{})["reason"], ) } } if err := json.NewDecoder(res.Body).Decode(&r); err != nil { log.Fatalf("Error parsing the response body: %s", err) } // 打印响应状态,结果数和请求持续时间. log.Printf( "[%s] %d hits; took: %dms", res.Status(), int(r["hits"].(map[string]interface{})["total"].(map[string]interface{})["value"].(float64)), int(r["took"].(float64)), ) // 打印每次匹配的ID和文档来源. for _, hit := range r["hits"].(map[string]interface{})["hits"].([]interface{}) { log.Printf(" * ID=%s, %s", hit.(map[string]interface{})["_id"], hit.(map[string]interface{})["_source"]) } log.Println(strings.Repeat("=", 37)) // 更新 //es.Update() // 删除 //es.Delete() }