Go中的微服务:使用Elasticsearch进行搜索

491 阅读4分钟

什么是Elasticsearch?

...是一个分布式的、免费的、开放的搜索和分析引擎,适用于所有类型的数据,包括文本、数字、地理空间、结构化和非结构化的数据。

Elasticsearch是Elastic Stack的核心组件,通常被称为ELK,这个栈由以下服务组成。

  • E:Elasticsearch。
  • L:Logstash, 和
  • K:Kibana。

Elasticsearch是一个众所周知的工具,已经被所有三个大的云供应商所支持。

在Go中使用Elasticsearch

有两个流行的软件包可以在Go中与Elasticsearch进行交互。

我们应该选择哪一个呢?

这两个包都得到了很好的支持,而且都可以投入生产,但是在这两个包中,我更倾向于使用elastic/go-elasticsearch ,而不是olivere/elastic ,这是因为Oliver Eilhardolivere/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包括的内容不止我上面提到的这些,它是一个非常强大的工具,也可以用于分析,使用起来肯定会让人不知所措,但它是一个不应该被忽视的工具。