🔍

【Go, Elasticsearch】ElasticSearchクライアント olivere を使ってみる(CRUD)

2021/12/12に公開
2

本記事は Go Advent Calendar 2021 の12日目の記事です。

https://qiita.com/advent-calendar/2021/go

はじめに

業務でElasticsearchを利用しているのですが、ライブラリをgo-elasticsearchからoliverに変えようという話があり、気になって少し調べたので、アウトプットの為に記事にします。
※Elasticsearchの一番重要な検索機能Searchについては次回の記事にまとめる予定で、今回はCRUDのみです。

前提

  • ElasticSearchについて細かくはこちらでは説明しません。詳細は、公式をご覧ください。
  • go modを使用します。
  • Dockerを使用します。

環境

  • go 1.17
  • github.com/olivere/elastic/v7 v7.0.29

ソースコード

docker-compose.yml

今回はDockerを利用します。

version: '3'
services:
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:7.4.0
    environment:
      - discovery.type=single-node
    ports:
      - 9200:9200
      - 9300:9300

main.go

全てのメソッドをmain.goに定義しています。

package main

import (
	"context"
	"fmt"
	"log"
	"time"

	"github.com/olivere/elastic/v7"
)

type User struct {
	Name      string    `json:"name"`
	Address   string    `json:"address"`
	Age       int       `json:"age"`
	Image     string    `json:"image,omitempty"`
	CreatedAt time.Time `json:"created,omitempty"`
	Comment   string    `json:"comment,omitempty"`
}

func main() {
	// clientを生成します
	client, err := elastic.NewClient(elastic.SetSniff(false))
	if err != nil {
		log.Fatal(err)
	}

	// ESのversionが取得出来ます
	version, err := client.ElasticsearchVersion("http://127.0.0.1:9200")
	if err != nil {
		// Handle error
		panic(err)
	}
	fmt.Printf("Elasticsearch version %s\n", version)

	// IndexExists 該当のindexが存在するかを確認します
	exists, err := client.IndexExists("user").Do(context.Background())
	if err != nil {
		panic(err)
	}
	
	mapping := ""
	
	// もし存在しなかったら新しいindexを作る為の値を用意します
	if !exists {
		mapping = `{
			"settings":{
				"number_of_shards":1,
				"number_of_replicas":0
			},
			"mappings":{
				"doc":{
					"properties":{
						"name":{
							"type":"keyword"
						},
						"address":{
							"type":"text",
							"store": true,
							"fielddata": true
						},
          	"age":{
          	    "type":"long"
          	}
					}
				}
			}
		}`
	}

	// CreateIndex indexを作成します
	createIndex(client, mapping)

	// put indexに追加します
	put(client)

	// get indexから取得します
	get(client)

	// update indexを更新します
	update(client)
	
	// updateした後にもう一度getします
	get(client)

	//delete indexを削除します
	delete(client)
}

// get indexから取得します
func get(client *elastic.Client) {
	get1, err := client.Get().
		Index("user").
		Type("doc").
		Id("1").
		Do(context.Background())
	if err != nil {
		switch {
		case elastic.IsNotFound(err):
			panic(fmt.Sprintf("Document not found: %v", err))
		case elastic.IsTimeout(err):
			panic(fmt.Sprintf("Timeout retrieving document: %v", err))
		case elastic.IsConnErr(err):
			panic(fmt.Sprintf("Connection problem: %v", err))
		default:
			fmt.Println("getに失敗しました")
			panic(err)
		}
	}
	fmt.Printf("Got document %s in version %d from index %s, type %s\n", get1.Id, get1.Version, get1.Index, get1.Type)
	
	// getした中身を確認する為にUser構造体へ変換します
	var user User
	err = json.Unmarshal(get1.Source, &user)
	if err != nil {
		fmt.Println(err)
	}
	// getしたuserを出力
	fmt.Println("get1:", user)
}

// createIndex indexを作成します
func createIndex(client *elastic.Client, mapping string) {
	createIndex, err := client.CreateIndex("user").Body(mapping).IncludeTypeName(true).Do(context.Background())
	if err != nil {
		fmt.Println("createIndexに失敗しました")
		panic(err)
	}
	if !createIndex.Acknowledged {
		fmt.Println("Not acknowledged")
	}
}

// put indexに追加します
func put(client *elastic.Client) {
	user1 := User{Name: "太郎", Address: "大阪府", Age: 20}

	put1, err := client.Index().
		Index("user").
		Type("doc").
		Id("1").
		BodyJson(user1).
		Do(context.Background())
	if err != nil {
		fmt.Println("putに失敗しました")
		panic(err)
	}
	fmt.Printf("Indexed user %s to index %s, type %s\n", put1.Id, put1.Index, put1.Type)

	user2 := `{"name" : "太郎", "image" : "写真.png"}`

	put2, err := client.Index().
		Index("user").
		Type("doc").
		Id("2").
		BodyString(user2).
		Do(context.Background())
	if err != nil {
		fmt.Println("putに失敗しました")
		panic(err)
	}
	fmt.Printf("Indexed user %s to index %s, type %s\n", put2.Id, put2.Index, put2.Type)
}

// update indexの1つの値を更新します
func update(client *elastic.Client) {
	script := elastic.NewScript("ctx._source.age += params.num").Param("num", 1)
	update, err := client.Update().Index("user").Type("doc").Id("1").
		Script(script).
		Upsert(map[string]interface{}{"age": 20}).
		Do(context.Background())
	if err != nil {
		fmt.Println("updateに失敗しました")
		panic(err)
	}
	fmt.Printf("New version of user %q is now %d", update.Id, update.Version)
}

// indexを削除します
func delete(client *elastic.Client) {
	deleteIndex, err := client.DeleteIndex("user").Do(context.Background())
	if err != nil {
		fmt.Println("deleteに失敗しました")
		panic(err)
	}
	if !deleteIndex.Acknowledged {
		fmt.Println("Not acknowledged")
	}
	fmt.Println(deleteIndex)
}

出力結果

Elasticsearch version 7.4.0
Indexed user 1 to index user, type doc
Indexed user 2 to index user, type doc
Got document 1 in version 824635831432 from index user, type doc
get1: {太郎 大阪府 20  0001-01-01 00:00:00 +0000 UTC }
New version of user "1" is now 2Got document 1 in version 824635831736 from index user, type doc
get1: {太郎 大阪府 21  0001-01-01 00:00:00 +0000 UTC }

解説

少しだけ解説します。

CreateIndex

indexを作成します。indexはテーブルのようなものです。
mappingはテーブル設計のようなものです。


mapping = `{
			"settings":{
				"number_of_shards":1,
				"number_of_replicas":0
			},
			"mappings":{
				"doc":{
					"properties":{
						"name":{
							"type":"keyword"
						},
						"address":{
							"type":"text",
							"store": true,
							"fielddata": true
						},
          	"age":{
          	    "type":"long"
          	}
					}
				}
			}
		}`

func createIndex(client *elastic.Client, mapping string) {
	createIndex, err := client.CreateIndex("user").Body(mapping).IncludeTypeName(true).Do(context.Background())
	if err != nil {
		fmt.Println("createIndexに失敗しました")
		panic(err)
	}
	if !createIndex.Acknowledged {
		fmt.Println("Not acknowledged")
	}
}

Get

indexから1つのdocument(レコードのようなもの)を取得します。
取得したものを表示する為、構造体へ変換しています。

func get(client *elastic.Client) {
	get1, err := client.Get().
		Index("user").
		Type("doc").
		Id("1").
		Do(context.Background())
	if err != nil {
		switch {
		case elastic.IsNotFound(err):
			panic(fmt.Sprintf("Document not found: %v", err))
		case elastic.IsTimeout(err):
			panic(fmt.Sprintf("Timeout retrieving document: %v", err))
		case elastic.IsConnErr(err):
			panic(fmt.Sprintf("Connection problem: %v", err))
		default:
			fmt.Println("getに失敗しました")
			panic(err)
		}
	}
	fmt.Printf("Got document %s in version %d from index %s, type %s\n", get1.Id, get1.Version, get1.Index, get1.Type)

	// getした中身を確認する為にUser構造体へ変換します
	var user User
	err = json.Unmarshal(get1.Source, &user)
	if err != nil {
		fmt.Println(err)
	}
	// getしたuserを出力
	fmt.Println("get1:", user)
}

Index(Put)

client.Indexを利用する事でindexへput出来ます。

func put(client *elastic.Client) {
	user1 := User{Name: "太郎", Address: "大阪府", Age: 20}

	put1, err := client.Index().
		Index("user").
		Type("doc").
		Id("1").
		BodyJson(user1).
		Do(context.Background())
	if err != nil {
		fmt.Println("putに失敗しました")
		panic(err)
	}
	fmt.Printf("Indexed user %s to index %s, type %s\n", put1.Id, put1.Index, put1.Type)

	user2 := `{"name" : "太郎", "image" : "写真.png"}`

	put2, err := client.Index().
		Index("user").
		Type("doc").
		Id("2").
		BodyString(user2).
		Do(context.Background())
	if err != nil {
		fmt.Println("putに失敗しました")
		panic(err)
	}
	fmt.Printf("Indexed user %s to index %s, type %s\n", put2.Id, put2.Index, put2.Type)
}

Update

client.Updateでdocumentの値の更新が出来ます。
Paramにnum = 1を渡して、ageに足す事で、Idが1のUserのageが1足されます。

func update(client *elastic.Client) {
	script := elastic.NewScript("ctx._source.age += params.num").Param("num", 1)
	update, err := client.Update().Index("user").Type("doc").Id("1").
		Script(script).
		Upsert(map[string]interface{}{"age": 20}).
		Do(context.Background())
	if err != nil {
		fmt.Println("updateに失敗しました")
		panic(err)
	}
	fmt.Printf("New version of user %q is now %d", update.Id, update.Version)
}

DeleteIndex

DeleteIndexはindex(テーブル)を削除します。
これとは別にdocument(レコード)を削除するDeleteもあります。

func delete(client *elastic.Client) {
	deleteIndex, err := client.DeleteIndex("user").Do(context.Background())
	if err != nil {
		fmt.Println("deleteに失敗しました")
		panic(err)
	}
	if !deleteIndex.Acknowledged {
		fmt.Println("Not acknowledged")
	}
}

さいごに

  • ES難しいですね。もっと仲良くなりたいと思いました。

参考

https://pkg.go.dev/github.com/olivere/elastic#pkg-constants
https://github.com/olivere/elastic/wiki/Services

Discussion

TFTF

ご存知かもしれませんが、ES v7系はあと一年ほどでEOLになること、olivereのv8対応があまり進んでないことなどから、公式版を継続された方が無難かとは思います。

A-ichiA-ichi

コメントありがとうございます。
そうなんですね。この記事を書いていた時は社内でElasticSearchを使っていたのですが、諸事情があり使わなくなってしまいまして、情報収集出来ておりませんでした。
もしまたElasticSearchを使う際は、公式版を使用するように社内で進言しようと思います。
良いアドバイスをして頂き、ありがとうございます。