什么是Elasticsearch?
...是一个分布式的、免费的、开放的搜索和分析引擎,适用于所有类型的数据,包括文本、数字、地理空间、结构化和非结构化的数据。
Elasticsearch是Elastic Stack的核心组件,通常被称为ELK,这个栈由以下服务组成。
E:Elasticsearch。L:Logstash, 和K:Kibana。
Elasticsearch是一个众所周知的工具,已经被所有三个大的云供应商所支持。
- 亚马逊网络服务有托管Elasticsearch
- 谷歌云供应商有Elastic Cloud
- 微软Azure有Elastic Cloud
在Go中使用Elasticsearch
有两个流行的软件包可以在Go中与Elasticsearch进行交互。
- github.com/elastic/go-…:Elastic支持的官方Go包,和
- github.com/olivere/ela…:非官方软件包,在社区中很有名。
我们应该选择哪一个呢?
这两个包都得到了很好的支持,而且都可以投入生产,但是在这两个包中,我更倾向于使用elastic/go-elasticsearch ,而不是olivere/elastic ,这是因为Oliver Eilhard(olivere/elastic 的作者)在Github上的评论,他提到也许该包的v8 ,如果你打算在Elasticsearch版本8到来时升级到该版本,这可能是个问题。
公平地说,到目前为止,Oliver Eilhard所做的工作简化了很多与Elasticsearch API交互时需要的东西,否则在使用官方API时就必须明确说明,这是因为olivere/elastic/v7 的实现方式实际上涵盖了官方API中所有可能的选项,而且因为它是使用类似流畅的API实现的,所以它的用法和默认值从一开始就是预定义的。
使用elastic/go-elasticsearch
为了实现Elasticsearch数据存储,我们将遵循存储库模式来更新我们的 "To Do Microservice"。我们将定义一个具有新类型的elasticsearch 包,它将包括索引、搜索以及删除记录所需的相应逻辑。这个类型将作为已经存在的service.Task 的一部分被调用,最后我们将通过一个新的HTTP API将这个功能暴露给我们的客户,用于搜索任务。
在实践中,用于索引的代码看起来或多或少是这样的。
func (t *Task) Index(ctx context.Context, task internal.Task) error {
// XXX: Excluding OpenTelemetry and error checking for simplicity
body := indexedTask{
ID: task.ID,
Description: task.Description,
Priority: task.Priority,
IsDone: task.IsDone,
DateStart: task.Dates.Start.UnixNano(),
DateDue: task.Dates.Due.UnixNano(),
}
var buf bytes.Buffer
_ = json.NewEncoder(&buf).Encode(body) // XXX: error omitted
req := esv7api.IndexRequest{
Index: t.index,
Body: &buf,
DocumentID: task.ID,
Refresh: "true",
}
_ = req.Do(ctx, t.client) // XXX: error omitted
defer resp.Body.Close()
io.Copy(ioutil.Discard, resp.Body)
return nil
}
而对于搜索来说,它看起来类似于。
func (t *Task) Search(ctx context.Context, description *string, priority *internal.Priority, isDone *bool) ([]internal.Task, error) {
// XXX: Excluding OpenTelemetry and error checking for simplicity
if description == nil && priority == nil && isDone == nil {
return nil, nil
}
should := make([]interface{}, 0, 3)
if description != nil {
should = append(should, map[string]interface{}{
"match": map[string]interface{}{
"description": *description,
},
})
}
if priority != nil {
should = append(should, map[string]interface{}{
"match": map[string]interface{}{
"priority": *priority,
},
})
}
if isDone != nil {
should = append(should, map[string]interface{}{
"match": map[string]interface{}{
"is_done": *isDone,
},
})
}
var query map[string]interface{}
if len(should) > 1 {
query = map[string]interface{}{
"query": map[string]interface{}{
"bool": map[string]interface{}{
"should": should,
},
},
}
} else {
query = map[string]interface{}{
"query": should[0],
}
}
var buf bytes.Buffer
_ = json.NewEncoder(&buf).Encode(query)
req := esv7api.SearchRequest{
Index: []string{t.index},
Body: &buf,
}
resp, _ = req.Do(ctx, t.client) // XXX: error omitted
defer resp.Body.Close()
var hits struct {
Hits struct {
Hits []struct {
Source indexedTask `json:"_source"`
} `json:"hits"`
} `json:"hits"`
}
_ = json.NewDecoder(resp.Body).Decode(&hits) // XXX: error omitted
res := make([]internal.Task, len(hits.Hits.Hits))
for i, hit := range hits.Hits.Hits {
res[i].ID = hit.Source.ID
res[i].Description = hit.Source.Description
res[i].Priority = internal.Priority(hit.Source.Priority)
res[i].Dates.Due = time.Unix(0, hit.Source.DateDue).UTC()
res[i].Dates.Start = time.Unix(0, hit.Source.DateStart).UTC()
}
return res, nil
}
在这两种情况下,具体细节请参考原始实现,就像我之前提到的,还有一个Delete 方法,目的是在删除先前索引的任务时被调用。
为了连接这些调用,我们需要更新我们的service 类型,以明确调用搜索商店来执行我们已经定义的新动作;在未来的文章中,我将介绍如何使用事件而不是在服务实现中直接明确调用商店。
例如,service.Task 类型将在Create 中做类似于以下的事情。
// Create stores a new record.
func (t *Task) Create(ctx context.Context, description string, priority internal.Priority, dates internal.Dates) (internal.Task, error) {
// XXX: Excluding OpenTelemetry and error checking for simplicity
task, _ := t.repo.Create(ctx, description, priority, dates)
_ = t.search.Index(ctx, task) // XXX: New Search call to index and store records
return task, nil
}
类似的调用将被添加到索引和一个搜索记录的新方法。
func (t *Task) By(ctx context.Context, description *string, priority *internal.Priority, isDone *bool) ([]internal.Task, error) {
// XXX: Excluding OpenTelemetry and error checking for simplicity
res, _ := t.search.Search(ctx, description, priority, isDone) // XXX: error omitted
return res, nil
}
类似于这个service.Task 使用的其他数据存储,我们也使用依赖注入来引用具体初始化的elasticsearch.Task 存储。
总结
Elasticsearch是一个强大的工具,它允许以多种不同的方式搜索记录,它可以横向扩展,并支持多种方式来索引和搜索值,例如,当使用基于文本的字段时,它可以将数字转化为单词,以允许搜索实际的字面数字和表达该值的人性化方式。
同样,它允许对字段进行评分,根据它们提供的匹配数量进行排序,让你灵活地按受欢迎程度首先显示这些字段,以及其他流行的功能,如模糊搜索,匹配与搜索词相关的记录。
在使用Elasticsearch时需要注意的一点是,有时主要的版本升级会带来类似于行为的破坏性变化,即使API仍然是一样的。
Elasticsearch包括的内容不止我上面提到的这些,它是一个非常强大的工具,也可以用于分析,使用起来肯定会让人不知所措,但它是一个不应该被忽视的工具。